403Webshell
Server IP : 146.59.209.152  /  Your IP : 216.73.216.46
Web Server : Apache
System : Linux webm005.cluster131.gra.hosting.ovh.net 5.15.167-ovh-vps-grsec-zfs-classid #1 SMP Tue Sep 17 08:14:20 UTC 2024 x86_64
User : infrafs ( 43850)
PHP Version : 8.2.29
Disable Function : _dyuweyrj4,_dyuweyrj4r,dl
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : ON  |  Sudo : OFF  |  Pkexec : OFF
Directory :  /home/i/n/f/infrafs/INFRABIKEIT/wp-content/plugins/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /home/i/n/f/infrafs/INFRABIKEIT/wp-content/plugins/includes.tar
export.php000060400000061735151327343360006615 0ustar00<?php
/**
 * WordPress Export Administration API
 *
 * @package WordPress
 * @subpackage Administration
 */

/**
 * Version number for the export format.
 *
 * Bump this when something changes that might affect compatibility.
 *
 * @since 2.5.0
 */
define( 'WXR_VERSION', '1.2' );

/**
 * Generates the WXR export file for download.
 *
 * Default behavior is to export all content, however, note that post content will only
 * be exported for post types with the `can_export` argument enabled. Any posts with the
 * 'auto-draft' status will be skipped.
 *
 * @since 2.1.0
 * @since 5.7.0 Added the `post_modified` and `post_modified_gmt` fields to the export file.
 *
 * @global wpdb    $wpdb WordPress database abstraction object.
 * @global WP_Post $post Global post object.
 *
 * @param array $args {
 *     Optional. Arguments for generating the WXR export file for download. Default empty array.
 *
 *     @type string $content    Type of content to export. If set, only the post content of this post type
 *                              will be exported. Accepts 'all', 'post', 'page', 'attachment', or a defined
 *                              custom post. If an invalid custom post type is supplied, every post type for
 *                              which `can_export` is enabled will be exported instead. If a valid custom post
 *                              type is supplied but `can_export` is disabled, then 'posts' will be exported
 *                              instead. When 'all' is supplied, only post types with `can_export` enabled will
 *                              be exported. Default 'all'.
 *     @type string $author     Author to export content for. Only used when `$content` is 'post', 'page', or
 *                              'attachment'. Accepts false (all) or a specific author ID. Default false (all).
 *     @type string $category   Category (slug) to export content for. Used only when `$content` is 'post'. If
 *                              set, only post content assigned to `$category` will be exported. Accepts false
 *                              or a specific category slug. Default is false (all categories).
 *     @type string $start_date Start date to export content from. Expected date format is 'Y-m-d'. Used only
 *                              when `$content` is 'post', 'page' or 'attachment'. Default false (since the
 *                              beginning of time).
 *     @type string $end_date   End date to export content to. Expected date format is 'Y-m-d'. Used only when
 *                              `$content` is 'post', 'page' or 'attachment'. Default false (latest publish date).
 *     @type string $status     Post status to export posts for. Used only when `$content` is 'post' or 'page'.
 *                              Accepts false (all statuses except 'auto-draft'), or a specific status, i.e.
 *                              'publish', 'pending', 'draft', 'auto-draft', 'future', 'private', 'inherit', or
 *                              'trash'. Default false (all statuses except 'auto-draft').
 * }
 */
function export_wp( $args = array() ) {
	global $wpdb, $post;

	$defaults = array(
		'content'    => 'all',
		'author'     => false,
		'category'   => false,
		'start_date' => false,
		'end_date'   => false,
		'status'     => false,
	);
	$args     = wp_parse_args( $args, $defaults );

	/**
	 * Fires at the beginning of an export, before any headers are sent.
	 *
	 * @since 2.3.0
	 *
	 * @param array $args An array of export arguments.
	 */
	do_action( 'export_wp', $args );

	$sitename = sanitize_key( get_bloginfo( 'name' ) );
	if ( ! empty( $sitename ) ) {
		$sitename .= '.';
	}
	$date        = gmdate( 'Y-m-d' );
	$wp_filename = $sitename . 'WordPress.' . $date . '.xml';
	/**
	 * Filters the export filename.
	 *
	 * @since 4.4.0
	 *
	 * @param string $wp_filename The name of the file for download.
	 * @param string $sitename    The site name.
	 * @param string $date        Today's date, formatted.
	 */
	$filename = apply_filters( 'export_wp_filename', $wp_filename, $sitename, $date );

	header( 'Content-Description: File Transfer' );
	header( 'Content-Disposition: attachment; filename=' . $filename );
	header( 'Content-Type: text/xml; charset=' . get_option( 'blog_charset' ), true );

	if ( 'all' !== $args['content'] && post_type_exists( $args['content'] ) ) {
		$ptype = get_post_type_object( $args['content'] );
		if ( ! $ptype->can_export ) {
			$args['content'] = 'post';
		}

		$where = $wpdb->prepare( "{$wpdb->posts}.post_type = %s", $args['content'] );
	} else {
		$post_types = get_post_types( array( 'can_export' => true ) );
		$esses      = array_fill( 0, count( $post_types ), '%s' );

		// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
		$where = $wpdb->prepare( "{$wpdb->posts}.post_type IN (" . implode( ',', $esses ) . ')', $post_types );
	}

	if ( $args['status'] && ( 'post' === $args['content'] || 'page' === $args['content'] ) ) {
		$where .= $wpdb->prepare( " AND {$wpdb->posts}.post_status = %s", $args['status'] );
	} else {
		$where .= " AND {$wpdb->posts}.post_status != 'auto-draft'";
	}

	$join = '';
	if ( $args['category'] && 'post' === $args['content'] ) {
		$term = term_exists( $args['category'], 'category' );
		if ( $term ) {
			$join   = "INNER JOIN {$wpdb->term_relationships} ON ({$wpdb->posts}.ID = {$wpdb->term_relationships}.object_id)";
			$where .= $wpdb->prepare( " AND {$wpdb->term_relationships}.term_taxonomy_id = %d", $term['term_taxonomy_id'] );
		}
	}

	if ( in_array( $args['content'], array( 'post', 'page', 'attachment' ), true ) ) {
		if ( $args['author'] ) {
			$where .= $wpdb->prepare( " AND {$wpdb->posts}.post_author = %d", $args['author'] );
		}

		if ( $args['start_date'] ) {
			$where .= $wpdb->prepare( " AND {$wpdb->posts}.post_date >= %s", gmdate( 'Y-m-d', strtotime( $args['start_date'] ) ) );
		}

		if ( $args['end_date'] ) {
			$where .= $wpdb->prepare( " AND {$wpdb->posts}.post_date < %s", gmdate( 'Y-m-d', strtotime( '+1 month', strtotime( $args['end_date'] ) ) ) );
		}
	}

	// Grab a snapshot of post IDs, just in case it changes during the export.
	$post_ids = $wpdb->get_col( "SELECT ID FROM {$wpdb->posts} $join WHERE $where" );

	// Get IDs for the attachments of each post, unless all content is already being exported.
	if ( ! in_array( $args['content'], array( 'all', 'attachment' ), true ) ) {
		// Array to hold all additional IDs (attachments and thumbnails).
		$additional_ids = array();

		// Create a copy of the post IDs array to avoid modifying the original array.
		$processing_ids = $post_ids;

		while ( $next_posts = array_splice( $processing_ids, 0, 20 ) ) {
			$posts_in     = array_map( 'absint', $next_posts );
			$placeholders = array_fill( 0, count( $posts_in ), '%d' );

			// Create a string for the placeholders.
			$in_placeholder = implode( ',', $placeholders );

			// Prepare the SQL statement for attachment ids.
			$attachment_ids = $wpdb->get_col(
				$wpdb->prepare(
					"
				SELECT ID
				FROM $wpdb->posts
				WHERE post_parent IN ($in_placeholder) AND post_type = 'attachment'
					",
					$posts_in
				)
			);

			$thumbnails_ids = $wpdb->get_col(
				$wpdb->prepare(
					"
				SELECT meta_value
				FROM $wpdb->postmeta
				WHERE $wpdb->postmeta.post_id IN ($in_placeholder)
				AND $wpdb->postmeta.meta_key = '_thumbnail_id'
					",
					$posts_in
				)
			);

			$additional_ids = array_merge( $additional_ids, $attachment_ids, $thumbnails_ids );
		}

		// Merge the additional IDs back with the original post IDs after processing all posts
		$post_ids = array_unique( array_merge( $post_ids, $additional_ids ) );
	}

	/*
	 * Get the requested terms ready, empty unless posts filtered by category
	 * or all content.
	 */
	$cats  = array();
	$tags  = array();
	$terms = array();
	if ( isset( $term ) && $term ) {
		$cat  = get_term( $term['term_id'], 'category' );
		$cats = array( $cat->term_id => $cat );
		unset( $term, $cat );
	} elseif ( 'all' === $args['content'] ) {
		$categories = (array) get_categories( array( 'get' => 'all' ) );
		$tags       = (array) get_tags( array( 'get' => 'all' ) );

		$custom_taxonomies = get_taxonomies( array( '_builtin' => false ) );
		$custom_terms      = (array) get_terms(
			array(
				'taxonomy' => $custom_taxonomies,
				'get'      => 'all',
			)
		);

		// Put categories in order with no child going before its parent.
		while ( $cat = array_shift( $categories ) ) {
			if ( ! $cat->parent || isset( $cats[ $cat->parent ] ) ) {
				$cats[ $cat->term_id ] = $cat;
			} else {
				$categories[] = $cat;
			}
		}

		// Put terms in order with no child going before its parent.
		while ( $t = array_shift( $custom_terms ) ) {
			if ( ! $t->parent || isset( $terms[ $t->parent ] ) ) {
				$terms[ $t->term_id ] = $t;
			} else {
				$custom_terms[] = $t;
			}
		}

		unset( $categories, $custom_taxonomies, $custom_terms );
	}

	/**
	 * Wraps given string in XML CDATA tag.
	 *
	 * @since 2.1.0
	 *
	 * @param string $str String to wrap in XML CDATA tag.
	 * @return string
	 */
	function wxr_cdata( $str ) {
		if ( ! seems_utf8( $str ) ) {
			$str = utf8_encode( $str );
		}
		// $str = ent2ncr(esc_html($str));
		$str = '<![CDATA[' . str_replace( ']]>', ']]]]><![CDATA[>', $str ) . ']]>';

		return $str;
	}

	/**
	 * Returns the URL of the site.
	 *
	 * @since 2.5.0
	 *
	 * @return string Site URL.
	 */
	function wxr_site_url() {
		if ( is_multisite() ) {
			// Multisite: the base URL.
			return network_home_url();
		} else {
			// WordPress (single site): the site URL.
			return get_bloginfo_rss( 'url' );
		}
	}

	/**
	 * Outputs a cat_name XML tag from a given category object.
	 *
	 * @since 2.1.0
	 *
	 * @param WP_Term $category Category Object.
	 */
	function wxr_cat_name( $category ) {
		if ( empty( $category->name ) ) {
			return;
		}

		echo '<wp:cat_name>' . wxr_cdata( $category->name ) . "</wp:cat_name>\n";
	}

	/**
	 * Outputs a category_description XML tag from a given category object.
	 *
	 * @since 2.1.0
	 *
	 * @param WP_Term $category Category Object.
	 */
	function wxr_category_description( $category ) {
		if ( empty( $category->description ) ) {
			return;
		}

		echo '<wp:category_description>' . wxr_cdata( $category->description ) . "</wp:category_description>\n";
	}

	/**
	 * Outputs a tag_name XML tag from a given tag object.
	 *
	 * @since 2.3.0
	 *
	 * @param WP_Term $tag Tag Object.
	 */
	function wxr_tag_name( $tag ) {
		if ( empty( $tag->name ) ) {
			return;
		}

		echo '<wp:tag_name>' . wxr_cdata( $tag->name ) . "</wp:tag_name>\n";
	}

	/**
	 * Outputs a tag_description XML tag from a given tag object.
	 *
	 * @since 2.3.0
	 *
	 * @param WP_Term $tag Tag Object.
	 */
	function wxr_tag_description( $tag ) {
		if ( empty( $tag->description ) ) {
			return;
		}

		echo '<wp:tag_description>' . wxr_cdata( $tag->description ) . "</wp:tag_description>\n";
	}

	/**
	 * Outputs a term_name XML tag from a given term object.
	 *
	 * @since 2.9.0
	 *
	 * @param WP_Term $term Term Object.
	 */
	function wxr_term_name( $term ) {
		if ( empty( $term->name ) ) {
			return;
		}

		echo '<wp:term_name>' . wxr_cdata( $term->name ) . "</wp:term_name>\n";
	}

	/**
	 * Outputs a term_description XML tag from a given term object.
	 *
	 * @since 2.9.0
	 *
	 * @param WP_Term $term Term Object.
	 */
	function wxr_term_description( $term ) {
		if ( empty( $term->description ) ) {
			return;
		}

		echo "\t\t<wp:term_description>" . wxr_cdata( $term->description ) . "</wp:term_description>\n";
	}

	/**
	 * Outputs term meta XML tags for a given term object.
	 *
	 * @since 4.6.0
	 *
	 * @global wpdb $wpdb WordPress database abstraction object.
	 *
	 * @param WP_Term $term Term object.
	 */
	function wxr_term_meta( $term ) {
		global $wpdb;

		$termmeta = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $wpdb->termmeta WHERE term_id = %d", $term->term_id ) );

		foreach ( $termmeta as $meta ) {
			/**
			 * Filters whether to selectively skip term meta used for WXR exports.
			 *
			 * Returning a truthy value from the filter will skip the current meta
			 * object from being exported.
			 *
			 * @since 4.6.0
			 *
			 * @param bool   $skip     Whether to skip the current piece of term meta. Default false.
			 * @param string $meta_key Current meta key.
			 * @param object $meta     Current meta object.
			 */
			if ( ! apply_filters( 'wxr_export_skip_termmeta', false, $meta->meta_key, $meta ) ) {
				printf( "\t\t<wp:termmeta>\n\t\t\t<wp:meta_key>%s</wp:meta_key>\n\t\t\t<wp:meta_value>%s</wp:meta_value>\n\t\t</wp:termmeta>\n", wxr_cdata( $meta->meta_key ), wxr_cdata( $meta->meta_value ) );
			}
		}
	}

	/**
	 * Outputs list of authors with posts.
	 *
	 * @since 3.1.0
	 *
	 * @global wpdb $wpdb WordPress database abstraction object.
	 *
	 * @param int[] $post_ids Optional. Array of post IDs to filter the query by.
	 */
	function wxr_authors_list( ?array $post_ids = null ) {
		global $wpdb;

		if ( ! empty( $post_ids ) ) {
			$post_ids = array_map( 'absint', $post_ids );
			$and      = 'AND ID IN ( ' . implode( ', ', $post_ids ) . ')';
		} else {
			$and = '';
		}

		$authors = array();
		$results = $wpdb->get_results( "SELECT DISTINCT post_author FROM $wpdb->posts WHERE post_status != 'auto-draft' $and" );
		foreach ( (array) $results as $result ) {
			$authors[] = get_userdata( $result->post_author );
		}

		$authors = array_filter( $authors );

		foreach ( $authors as $author ) {
			echo "\t<wp:author>";
			echo '<wp:author_id>' . (int) $author->ID . '</wp:author_id>';
			echo '<wp:author_login>' . wxr_cdata( $author->user_login ) . '</wp:author_login>';
			echo '<wp:author_email>' . wxr_cdata( $author->user_email ) . '</wp:author_email>';
			echo '<wp:author_display_name>' . wxr_cdata( $author->display_name ) . '</wp:author_display_name>';
			echo '<wp:author_first_name>' . wxr_cdata( $author->first_name ) . '</wp:author_first_name>';
			echo '<wp:author_last_name>' . wxr_cdata( $author->last_name ) . '</wp:author_last_name>';
			echo "</wp:author>\n";
		}
	}

	/**
	 * Outputs all navigation menu terms.
	 *
	 * @since 3.1.0
	 */
	function wxr_nav_menu_terms() {
		$nav_menus = wp_get_nav_menus();
		if ( empty( $nav_menus ) || ! is_array( $nav_menus ) ) {
			return;
		}

		foreach ( $nav_menus as $menu ) {
			echo "\t<wp:term>";
			echo '<wp:term_id>' . (int) $menu->term_id . '</wp:term_id>';
			echo '<wp:term_taxonomy>nav_menu</wp:term_taxonomy>';
			echo '<wp:term_slug>' . wxr_cdata( $menu->slug ) . '</wp:term_slug>';
			wxr_term_name( $menu );
			echo "</wp:term>\n";
		}
	}

	/**
	 * Outputs list of taxonomy terms, in XML tag format, associated with a post.
	 *
	 * @since 2.3.0
	 */
	function wxr_post_taxonomy() {
		$post = get_post();

		$taxonomies = get_object_taxonomies( $post->post_type );
		if ( empty( $taxonomies ) ) {
			return;
		}
		$terms = wp_get_object_terms( $post->ID, $taxonomies );

		foreach ( (array) $terms as $term ) {
			echo "\t\t<category domain=\"{$term->taxonomy}\" nicename=\"{$term->slug}\">" . wxr_cdata( $term->name ) . "</category>\n";
		}
	}

	/**
	 * Determines whether to selectively skip post meta used for WXR exports.
	 *
	 * @since 3.3.0
	 *
	 * @param bool   $return_me Whether to skip the current post meta. Default false.
	 * @param string $meta_key  Meta key.
	 * @return bool
	 */
	function wxr_filter_postmeta( $return_me, $meta_key ) {
		if ( '_edit_lock' === $meta_key ) {
			$return_me = true;
		}
		return $return_me;
	}
	add_filter( 'wxr_export_skip_postmeta', 'wxr_filter_postmeta', 10, 2 );

	echo '<?xml version="1.0" encoding="' . get_bloginfo( 'charset' ) . "\" ?>\n";

	?>
<!-- This is a WordPress eXtended RSS file generated by WordPress as an export of your site. -->
<!-- It contains information about your site's posts, pages, comments, categories, and other content. -->
<!-- You may use this file to transfer that content from one site to another. -->
<!-- This file is not intended to serve as a complete backup of your site. -->

<!-- To import this information into a WordPress site follow these steps: -->
<!-- 1. Log in to that site as an administrator. -->
<!-- 2. Go to Tools: Import in the WordPress admin panel. -->
<!-- 3. Install the "WordPress" importer from the list. -->
<!-- 4. Activate & Run Importer. -->
<!-- 5. Upload this file using the form provided on that page. -->
<!-- 6. You will first be asked to map the authors in this export file to users -->
<!--    on the site. For each author, you may choose to map to an -->
<!--    existing user on the site or to create a new user. -->
<!-- 7. WordPress will then import each of the posts, pages, comments, categories, etc. -->
<!--    contained in this file into your site. -->

	<?php the_generator( 'export' ); ?>
<rss version="2.0"
	xmlns:excerpt="http://wordpress.org/export/<?php echo WXR_VERSION; ?>/excerpt/"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:wp="http://wordpress.org/export/<?php echo WXR_VERSION; ?>/"
>

<channel>
	<title><?php bloginfo_rss( 'name' ); ?></title>
	<link><?php bloginfo_rss( 'url' ); ?></link>
	<description><?php bloginfo_rss( 'description' ); ?></description>
	<pubDate><?php echo gmdate( 'D, d M Y H:i:s +0000' ); ?></pubDate>
	<language><?php bloginfo_rss( 'language' ); ?></language>
	<wp:wxr_version><?php echo WXR_VERSION; ?></wp:wxr_version>
	<wp:base_site_url><?php echo wxr_site_url(); ?></wp:base_site_url>
	<wp:base_blog_url><?php bloginfo_rss( 'url' ); ?></wp:base_blog_url>

	<?php wxr_authors_list( $post_ids ); ?>

	<?php foreach ( $cats as $c ) : ?>
	<wp:category>
		<wp:term_id><?php echo (int) $c->term_id; ?></wp:term_id>
		<wp:category_nicename><?php echo wxr_cdata( $c->slug ); ?></wp:category_nicename>
		<wp:category_parent><?php echo wxr_cdata( $c->parent ? $cats[ $c->parent ]->slug : '' ); ?></wp:category_parent>
		<?php
		wxr_cat_name( $c );
		wxr_category_description( $c );
		wxr_term_meta( $c );
		?>
	</wp:category>
	<?php endforeach; ?>
	<?php foreach ( $tags as $t ) : ?>
	<wp:tag>
		<wp:term_id><?php echo (int) $t->term_id; ?></wp:term_id>
		<wp:tag_slug><?php echo wxr_cdata( $t->slug ); ?></wp:tag_slug>
		<?php
		wxr_tag_name( $t );
		wxr_tag_description( $t );
		wxr_term_meta( $t );
		?>
	</wp:tag>
	<?php endforeach; ?>
	<?php foreach ( $terms as $t ) : ?>
	<wp:term>
		<wp:term_id><?php echo (int) $t->term_id; ?></wp:term_id>
		<wp:term_taxonomy><?php echo wxr_cdata( $t->taxonomy ); ?></wp:term_taxonomy>
		<wp:term_slug><?php echo wxr_cdata( $t->slug ); ?></wp:term_slug>
		<wp:term_parent><?php echo wxr_cdata( $t->parent ? $terms[ $t->parent ]->slug : '' ); ?></wp:term_parent>
		<?php
		wxr_term_name( $t );
		wxr_term_description( $t );
		wxr_term_meta( $t );
		?>
	</wp:term>
	<?php endforeach; ?>
	<?php
	if ( 'all' === $args['content'] ) {
		wxr_nav_menu_terms();
	}
	?>

	<?php
	/** This action is documented in wp-includes/feed-rss2.php */
	do_action( 'rss2_head' );
	?>

	<?php
	if ( $post_ids ) {
		/**
		 * @global WP_Query $wp_query WordPress Query object.
		 */
		global $wp_query;

		// Fake being in the loop.
		$wp_query->in_the_loop = true;

		// Fetch 20 posts at a time rather than loading the entire table into memory.
		while ( $next_posts = array_splice( $post_ids, 0, 20 ) ) {
			$where = 'WHERE ID IN (' . implode( ',', $next_posts ) . ')';
			$posts = $wpdb->get_results( "SELECT * FROM {$wpdb->posts} $where" );

			// Begin Loop.
			foreach ( $posts as $post ) {
				setup_postdata( $post );

				/**
				 * Filters the post title used for WXR exports.
				 *
				 * @since 5.7.0
				 *
				 * @param string $post_title Title of the current post.
				 */
				$title = wxr_cdata( apply_filters( 'the_title_export', $post->post_title ) );

				/**
				 * Filters the post content used for WXR exports.
				 *
				 * @since 2.5.0
				 *
				 * @param string $post_content Content of the current post.
				 */
				$content = wxr_cdata( apply_filters( 'the_content_export', $post->post_content ) );

				/**
				 * Filters the post excerpt used for WXR exports.
				 *
				 * @since 2.6.0
				 *
				 * @param string $post_excerpt Excerpt for the current post.
				 */
				$excerpt = wxr_cdata( apply_filters( 'the_excerpt_export', $post->post_excerpt ) );

				$is_sticky = is_sticky( $post->ID ) ? 1 : 0;
				?>
	<item>
		<title><?php echo $title; ?></title>
		<link><?php the_permalink_rss(); ?></link>
		<pubDate><?php echo mysql2date( 'D, d M Y H:i:s +0000', get_post_time( 'Y-m-d H:i:s', true ), false ); ?></pubDate>
		<dc:creator><?php echo wxr_cdata( get_the_author_meta( 'login' ) ); ?></dc:creator>
		<guid isPermaLink="false"><?php the_guid(); ?></guid>
		<description></description>
		<content:encoded><?php echo $content; ?></content:encoded>
		<excerpt:encoded><?php echo $excerpt; ?></excerpt:encoded>
		<wp:post_id><?php echo (int) $post->ID; ?></wp:post_id>
		<wp:post_date><?php echo wxr_cdata( $post->post_date ); ?></wp:post_date>
		<wp:post_date_gmt><?php echo wxr_cdata( $post->post_date_gmt ); ?></wp:post_date_gmt>
		<wp:post_modified><?php echo wxr_cdata( $post->post_modified ); ?></wp:post_modified>
		<wp:post_modified_gmt><?php echo wxr_cdata( $post->post_modified_gmt ); ?></wp:post_modified_gmt>
		<wp:comment_status><?php echo wxr_cdata( $post->comment_status ); ?></wp:comment_status>
		<wp:ping_status><?php echo wxr_cdata( $post->ping_status ); ?></wp:ping_status>
		<wp:post_name><?php echo wxr_cdata( $post->post_name ); ?></wp:post_name>
		<wp:status><?php echo wxr_cdata( $post->post_status ); ?></wp:status>
		<wp:post_parent><?php echo (int) $post->post_parent; ?></wp:post_parent>
		<wp:menu_order><?php echo (int) $post->menu_order; ?></wp:menu_order>
		<wp:post_type><?php echo wxr_cdata( $post->post_type ); ?></wp:post_type>
		<wp:post_password><?php echo wxr_cdata( $post->post_password ); ?></wp:post_password>
		<wp:is_sticky><?php echo (int) $is_sticky; ?></wp:is_sticky>
				<?php	if ( 'attachment' === $post->post_type ) : ?>
		<wp:attachment_url><?php echo wxr_cdata( wp_get_attachment_url( $post->ID ) ); ?></wp:attachment_url>
	<?php endif; ?>
				<?php wxr_post_taxonomy(); ?>
				<?php
				$postmeta = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $wpdb->postmeta WHERE post_id = %d", $post->ID ) );
				foreach ( $postmeta as $meta ) :
					/**
					 * Filters whether to selectively skip post meta used for WXR exports.
					 *
					 * Returning a truthy value from the filter will skip the current meta
					 * object from being exported.
					 *
					 * @since 3.3.0
					 *
					 * @param bool   $skip     Whether to skip the current post meta. Default false.
					 * @param string $meta_key Current meta key.
					 * @param object $meta     Current meta object.
					 */
					if ( apply_filters( 'wxr_export_skip_postmeta', false, $meta->meta_key, $meta ) ) {
						continue;
					}
					?>
		<wp:postmeta>
		<wp:meta_key><?php echo wxr_cdata( $meta->meta_key ); ?></wp:meta_key>
		<wp:meta_value><?php echo wxr_cdata( $meta->meta_value ); ?></wp:meta_value>
		</wp:postmeta>
					<?php
	endforeach;

				$_comments = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $wpdb->comments WHERE comment_post_ID = %d AND comment_approved <> 'spam'", $post->ID ) );
				$comments  = array_map( 'get_comment', $_comments );
				foreach ( $comments as $c ) :
					?>
		<wp:comment>
			<wp:comment_id><?php echo (int) $c->comment_ID; ?></wp:comment_id>
			<wp:comment_author><?php echo wxr_cdata( $c->comment_author ); ?></wp:comment_author>
			<wp:comment_author_email><?php echo wxr_cdata( $c->comment_author_email ); ?></wp:comment_author_email>
			<wp:comment_author_url><?php echo sanitize_url( $c->comment_author_url ); ?></wp:comment_author_url>
			<wp:comment_author_IP><?php echo wxr_cdata( $c->comment_author_IP ); ?></wp:comment_author_IP>
			<wp:comment_date><?php echo wxr_cdata( $c->comment_date ); ?></wp:comment_date>
			<wp:comment_date_gmt><?php echo wxr_cdata( $c->comment_date_gmt ); ?></wp:comment_date_gmt>
			<wp:comment_content><?php echo wxr_cdata( $c->comment_content ); ?></wp:comment_content>
			<wp:comment_approved><?php echo wxr_cdata( $c->comment_approved ); ?></wp:comment_approved>
			<wp:comment_type><?php echo wxr_cdata( $c->comment_type ); ?></wp:comment_type>
			<wp:comment_parent><?php echo (int) $c->comment_parent; ?></wp:comment_parent>
			<wp:comment_user_id><?php echo (int) $c->user_id; ?></wp:comment_user_id>
					<?php
					$c_meta = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $wpdb->commentmeta WHERE comment_id = %d", $c->comment_ID ) );
					foreach ( $c_meta as $meta ) :
						/**
						 * Filters whether to selectively skip comment meta used for WXR exports.
						 *
						 * Returning a truthy value from the filter will skip the current meta
						 * object from being exported.
						 *
						 * @since 4.0.0
						 *
						 * @param bool   $skip     Whether to skip the current comment meta. Default false.
						 * @param string $meta_key Current meta key.
						 * @param object $meta     Current meta object.
						 */
						if ( apply_filters( 'wxr_export_skip_commentmeta', false, $meta->meta_key, $meta ) ) {
							continue;
						}
						?>
	<wp:commentmeta>
	<wp:meta_key><?php echo wxr_cdata( $meta->meta_key ); ?></wp:meta_key>
			<wp:meta_value><?php echo wxr_cdata( $meta->meta_value ); ?></wp:meta_value>
			</wp:commentmeta>
					<?php	endforeach; ?>
		</wp:comment>
			<?php	endforeach; ?>
		</item>
				<?php
			}
		}
	}
	?>
</channel>
</rss>
	<?php
}
class-pclzip.php000064400000600127151327343360007676 0ustar00<?php
// --------------------------------------------------------------------------------
// PhpConcept Library - Zip Module 2.8.2
// --------------------------------------------------------------------------------
// License GNU/LGPL - Vincent Blavet - August 2009
// http://www.phpconcept.net
// --------------------------------------------------------------------------------
//
// Presentation :
//   PclZip is a PHP library that manage ZIP archives.
//   So far tests show that archives generated by PclZip are readable by
//   WinZip application and other tools.
//
// Description :
//   See readme.txt and http://www.phpconcept.net
//
// Warning :
//   This library and the associated files are non commercial, non professional
//   work.
//   It should not have unexpected results. However if any damage is caused by
//   this software the author can not be responsible.
//   The use of this software is at the risk of the user.
//
// --------------------------------------------------------------------------------
// $Id: pclzip.lib.php,v 1.60 2009/09/30 21:01:04 vblavet Exp $
// --------------------------------------------------------------------------------

  // ----- Constants
  if (!defined('PCLZIP_READ_BLOCK_SIZE')) {
    define( 'PCLZIP_READ_BLOCK_SIZE', 2048 );
  }

  // ----- File list separator
  // In version 1.x of PclZip, the separator for file list is a space
  // (which is not a very smart choice, specifically for windows paths !).
  // A better separator should be a comma (,). This constant gives you the
  // ability to change that.
  // However notice that changing this value, may have impact on existing
  // scripts, using space separated filenames.
  // Recommended values for compatibility with older versions :
  //define( 'PCLZIP_SEPARATOR', ' ' );
  // Recommended values for smart separation of filenames.
  if (!defined('PCLZIP_SEPARATOR')) {
    define( 'PCLZIP_SEPARATOR', ',' );
  }

  // ----- Error configuration
  // 0 : PclZip Class integrated error handling
  // 1 : PclError external library error handling. By enabling this
  //     you must ensure that you have included PclError library.
  // [2,...] : reserved for future use
  if (!defined('PCLZIP_ERROR_EXTERNAL')) {
    define( 'PCLZIP_ERROR_EXTERNAL', 0 );
  }

  // ----- Optional static temporary directory
  //       By default temporary files are generated in the script current
  //       path.
  //       If defined :
  //       - MUST BE terminated by a '/'.
  //       - MUST be a valid, already created directory
  //       Samples :
  // define( 'PCLZIP_TEMPORARY_DIR', '/temp/' );
  // define( 'PCLZIP_TEMPORARY_DIR', 'C:/Temp/' );
  if (!defined('PCLZIP_TEMPORARY_DIR')) {
    define( 'PCLZIP_TEMPORARY_DIR', '' );
  }

  // ----- Optional threshold ratio for use of temporary files
  //       Pclzip sense the size of the file to add/extract and decide to
  //       use or not temporary file. The algorithm is looking for
  //       memory_limit of PHP and apply a ratio.
  //       threshold = memory_limit * ratio.
  //       Recommended values are under 0.5. Default 0.47.
  //       Samples :
  // define( 'PCLZIP_TEMPORARY_FILE_RATIO', 0.5 );
  if (!defined('PCLZIP_TEMPORARY_FILE_RATIO')) {
    define( 'PCLZIP_TEMPORARY_FILE_RATIO', 0.47 );
  }

// --------------------------------------------------------------------------------
// ***** UNDER THIS LINE NOTHING NEEDS TO BE MODIFIED *****
// --------------------------------------------------------------------------------

  // ----- Global variables
  $g_pclzip_version = "2.8.2";

  // ----- Error codes
  //   -1 : Unable to open file in binary write mode
  //   -2 : Unable to open file in binary read mode
  //   -3 : Invalid parameters
  //   -4 : File does not exist
  //   -5 : Filename is too long (max. 255)
  //   -6 : Not a valid zip file
  //   -7 : Invalid extracted file size
  //   -8 : Unable to create directory
  //   -9 : Invalid archive extension
  //  -10 : Invalid archive format
  //  -11 : Unable to delete file (unlink)
  //  -12 : Unable to rename file (rename)
  //  -13 : Invalid header checksum
  //  -14 : Invalid archive size
  define( 'PCLZIP_ERR_USER_ABORTED', 2 );
  define( 'PCLZIP_ERR_NO_ERROR', 0 );
  define( 'PCLZIP_ERR_WRITE_OPEN_FAIL', -1 );
  define( 'PCLZIP_ERR_READ_OPEN_FAIL', -2 );
  define( 'PCLZIP_ERR_INVALID_PARAMETER', -3 );
  define( 'PCLZIP_ERR_MISSING_FILE', -4 );
  define( 'PCLZIP_ERR_FILENAME_TOO_LONG', -5 );
  define( 'PCLZIP_ERR_INVALID_ZIP', -6 );
  define( 'PCLZIP_ERR_BAD_EXTRACTED_FILE', -7 );
  define( 'PCLZIP_ERR_DIR_CREATE_FAIL', -8 );
  define( 'PCLZIP_ERR_BAD_EXTENSION', -9 );
  define( 'PCLZIP_ERR_BAD_FORMAT', -10 );
  define( 'PCLZIP_ERR_DELETE_FILE_FAIL', -11 );
  define( 'PCLZIP_ERR_RENAME_FILE_FAIL', -12 );
  define( 'PCLZIP_ERR_BAD_CHECKSUM', -13 );
  define( 'PCLZIP_ERR_INVALID_ARCHIVE_ZIP', -14 );
  define( 'PCLZIP_ERR_MISSING_OPTION_VALUE', -15 );
  define( 'PCLZIP_ERR_INVALID_OPTION_VALUE', -16 );
  define( 'PCLZIP_ERR_ALREADY_A_DIRECTORY', -17 );
  define( 'PCLZIP_ERR_UNSUPPORTED_COMPRESSION', -18 );
  define( 'PCLZIP_ERR_UNSUPPORTED_ENCRYPTION', -19 );
  define( 'PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE', -20 );
  define( 'PCLZIP_ERR_DIRECTORY_RESTRICTION', -21 );

  // ----- Options values
  define( 'PCLZIP_OPT_PATH', 77001 );
  define( 'PCLZIP_OPT_ADD_PATH', 77002 );
  define( 'PCLZIP_OPT_REMOVE_PATH', 77003 );
  define( 'PCLZIP_OPT_REMOVE_ALL_PATH', 77004 );
  define( 'PCLZIP_OPT_SET_CHMOD', 77005 );
  define( 'PCLZIP_OPT_EXTRACT_AS_STRING', 77006 );
  define( 'PCLZIP_OPT_NO_COMPRESSION', 77007 );
  define( 'PCLZIP_OPT_BY_NAME', 77008 );
  define( 'PCLZIP_OPT_BY_INDEX', 77009 );
  define( 'PCLZIP_OPT_BY_EREG', 77010 );
  define( 'PCLZIP_OPT_BY_PREG', 77011 );
  define( 'PCLZIP_OPT_COMMENT', 77012 );
  define( 'PCLZIP_OPT_ADD_COMMENT', 77013 );
  define( 'PCLZIP_OPT_PREPEND_COMMENT', 77014 );
  define( 'PCLZIP_OPT_EXTRACT_IN_OUTPUT', 77015 );
  define( 'PCLZIP_OPT_REPLACE_NEWER', 77016 );
  define( 'PCLZIP_OPT_STOP_ON_ERROR', 77017 );
  // Having big trouble with crypt. Need to multiply 2 long int
  // which is not correctly supported by PHP ...
  //define( 'PCLZIP_OPT_CRYPT', 77018 );
  define( 'PCLZIP_OPT_EXTRACT_DIR_RESTRICTION', 77019 );
  define( 'PCLZIP_OPT_TEMP_FILE_THRESHOLD', 77020 );
  define( 'PCLZIP_OPT_ADD_TEMP_FILE_THRESHOLD', 77020 ); // alias
  define( 'PCLZIP_OPT_TEMP_FILE_ON', 77021 );
  define( 'PCLZIP_OPT_ADD_TEMP_FILE_ON', 77021 ); // alias
  define( 'PCLZIP_OPT_TEMP_FILE_OFF', 77022 );
  define( 'PCLZIP_OPT_ADD_TEMP_FILE_OFF', 77022 ); // alias

  // ----- File description attributes
  define( 'PCLZIP_ATT_FILE_NAME', 79001 );
  define( 'PCLZIP_ATT_FILE_NEW_SHORT_NAME', 79002 );
  define( 'PCLZIP_ATT_FILE_NEW_FULL_NAME', 79003 );
  define( 'PCLZIP_ATT_FILE_MTIME', 79004 );
  define( 'PCLZIP_ATT_FILE_CONTENT', 79005 );
  define( 'PCLZIP_ATT_FILE_COMMENT', 79006 );

  // ----- Call backs values
  define( 'PCLZIP_CB_PRE_EXTRACT', 78001 );
  define( 'PCLZIP_CB_POST_EXTRACT', 78002 );
  define( 'PCLZIP_CB_PRE_ADD', 78003 );
  define( 'PCLZIP_CB_POST_ADD', 78004 );
  /* For future use
  define( 'PCLZIP_CB_PRE_LIST', 78005 );
  define( 'PCLZIP_CB_POST_LIST', 78006 );
  define( 'PCLZIP_CB_PRE_DELETE', 78007 );
  define( 'PCLZIP_CB_POST_DELETE', 78008 );
  */

  // --------------------------------------------------------------------------------
  // Class : PclZip
  // Description :
  //   PclZip is the class that represent a Zip archive.
  //   The public methods allow the manipulation of the archive.
  // Attributes :
  //   Attributes must not be accessed directly.
  // Methods :
  //   PclZip() : Object creator
  //   create() : Creates the Zip archive
  //   listContent() : List the content of the Zip archive
  //   extract() : Extract the content of the archive
  //   properties() : List the properties of the archive
  // --------------------------------------------------------------------------------
  class PclZip
  {
    // ----- Filename of the zip file
    var $zipname = '';

    // ----- File descriptor of the zip file
    var $zip_fd = 0;

    // ----- Internal error handling
    var $error_code = 1;
    var $error_string = '';

    // ----- Current status of the magic_quotes_runtime
    // This value store the php configuration for magic_quotes
    // The class can then disable the magic_quotes and reset it after
    var $magic_quotes_status;

  // --------------------------------------------------------------------------------
  // Function : PclZip()
  // Description :
  //   Creates a PclZip object and set the name of the associated Zip archive
  //   filename.
  //   Note that no real action is taken, if the archive does not exist it is not
  //   created. Use create() for that.
  // --------------------------------------------------------------------------------
  function __construct($p_zipname)
  {

    // ----- Tests the zlib
    if (!function_exists('gzopen'))
    {
      die('Abort '.basename(__FILE__).' : Missing zlib extensions');
    }

    // ----- Set the attributes
    $this->zipname = $p_zipname;
    $this->zip_fd = 0;
    $this->magic_quotes_status = -1;

    // ----- Return
    return;
  }

  public function PclZip($p_zipname) {
    self::__construct($p_zipname);
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function :
  //   create($p_filelist, $p_add_dir="", $p_remove_dir="")
  //   create($p_filelist, $p_option, $p_option_value, ...)
  // Description :
  //   This method supports two different synopsis. The first one is historical.
  //   This method creates a Zip Archive. The Zip file is created in the
  //   filesystem. The files and directories indicated in $p_filelist
  //   are added in the archive. See the parameters description for the
  //   supported format of $p_filelist.
  //   When a directory is in the list, the directory and its content is added
  //   in the archive.
  //   In this synopsis, the function takes an optional variable list of
  //   options. See below the supported options.
  // Parameters :
  //   $p_filelist : An array containing file or directory names, or
  //                 a string containing one filename or one directory name, or
  //                 a string containing a list of filenames and/or directory
  //                 names separated by spaces.
  //   $p_add_dir : A path to add before the real path of the archived file,
  //                in order to have it memorized in the archive.
  //   $p_remove_dir : A path to remove from the real path of the file to archive,
  //                   in order to have a shorter path memorized in the archive.
  //                   When $p_add_dir and $p_remove_dir are set, $p_remove_dir
  //                   is removed first, before $p_add_dir is added.
  // Options :
  //   PCLZIP_OPT_ADD_PATH :
  //   PCLZIP_OPT_REMOVE_PATH :
  //   PCLZIP_OPT_REMOVE_ALL_PATH :
  //   PCLZIP_OPT_COMMENT :
  //   PCLZIP_CB_PRE_ADD :
  //   PCLZIP_CB_POST_ADD :
  // Return Values :
  //   0 on failure,
  //   The list of the added files, with a status of the add action.
  //   (see PclZip::listContent() for list entry format)
  // --------------------------------------------------------------------------------
  function create($p_filelist)
  {
    $v_result=1;

    // ----- Reset the error handler
    $this->privErrorReset();

    // ----- Set default values
    $v_options = array();
    $v_options[PCLZIP_OPT_NO_COMPRESSION] = FALSE;

    // ----- Look for variable options arguments
    $v_size = func_num_args();

    // ----- Look for arguments
    if ($v_size > 1) {
      // ----- Get the arguments
      $v_arg_list = func_get_args();

      // ----- Remove from the options list the first argument
      array_shift($v_arg_list);
      $v_size--;

      // ----- Look for first arg
      if ((is_integer($v_arg_list[0])) && ($v_arg_list[0] > 77000)) {

        // ----- Parse the options
        $v_result = $this->privParseOptions($v_arg_list, $v_size, $v_options,
                                            array (PCLZIP_OPT_REMOVE_PATH => 'optional',
                                                   PCLZIP_OPT_REMOVE_ALL_PATH => 'optional',
                                                   PCLZIP_OPT_ADD_PATH => 'optional',
                                                   PCLZIP_CB_PRE_ADD => 'optional',
                                                   PCLZIP_CB_POST_ADD => 'optional',
                                                   PCLZIP_OPT_NO_COMPRESSION => 'optional',
                                                   PCLZIP_OPT_COMMENT => 'optional',
                                                   PCLZIP_OPT_TEMP_FILE_THRESHOLD => 'optional',
                                                   PCLZIP_OPT_TEMP_FILE_ON => 'optional',
                                                   PCLZIP_OPT_TEMP_FILE_OFF => 'optional'
                                                   //, PCLZIP_OPT_CRYPT => 'optional'
                                             ));
        if ($v_result != 1) {
          return 0;
        }
      }

      // ----- Look for 2 args
      // Here we need to support the first historic synopsis of the
      // method.
      else {

        // ----- Get the first argument
        $v_options[PCLZIP_OPT_ADD_PATH] = $v_arg_list[0];

        // ----- Look for the optional second argument
        if ($v_size == 2) {
          $v_options[PCLZIP_OPT_REMOVE_PATH] = $v_arg_list[1];
        }
        else if ($v_size > 2) {
          PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER,
		                       "Invalid number / type of arguments");
          return 0;
        }
      }
    }

    // ----- Look for default option values
    $this->privOptionDefaultThreshold($v_options);

    // ----- Init
    $v_string_list = array();
    $v_att_list = array();
    $v_filedescr_list = array();
    $p_result_list = array();

    // ----- Look if the $p_filelist is really an array
    if (is_array($p_filelist)) {

      // ----- Look if the first element is also an array
      //       This will mean that this is a file description entry
      if (isset($p_filelist[0]) && is_array($p_filelist[0])) {
        $v_att_list = $p_filelist;
      }

      // ----- The list is a list of string names
      else {
        $v_string_list = $p_filelist;
      }
    }

    // ----- Look if the $p_filelist is a string
    else if (is_string($p_filelist)) {
      // ----- Create a list from the string
      $v_string_list = explode(PCLZIP_SEPARATOR, $p_filelist);
    }

    // ----- Invalid variable type for $p_filelist
    else {
      PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid variable type p_filelist");
      return 0;
    }

    // ----- Reformat the string list
    if (sizeof($v_string_list) != 0) {
      foreach ($v_string_list as $v_string) {
        if ($v_string != '') {
          $v_att_list[][PCLZIP_ATT_FILE_NAME] = $v_string;
        }
        else {
        }
      }
    }

    // ----- For each file in the list check the attributes
    $v_supported_attributes
    = array ( PCLZIP_ATT_FILE_NAME => 'mandatory'
             ,PCLZIP_ATT_FILE_NEW_SHORT_NAME => 'optional'
             ,PCLZIP_ATT_FILE_NEW_FULL_NAME => 'optional'
             ,PCLZIP_ATT_FILE_MTIME => 'optional'
             ,PCLZIP_ATT_FILE_CONTENT => 'optional'
             ,PCLZIP_ATT_FILE_COMMENT => 'optional'
						);
    foreach ($v_att_list as $v_entry) {
      $v_result = $this->privFileDescrParseAtt($v_entry,
                                               $v_filedescr_list[],
                                               $v_options,
                                               $v_supported_attributes);
      if ($v_result != 1) {
        return 0;
      }
    }

    // ----- Expand the filelist (expand directories)
    $v_result = $this->privFileDescrExpand($v_filedescr_list, $v_options);
    if ($v_result != 1) {
      return 0;
    }

    // ----- Call the create fct
    $v_result = $this->privCreate($v_filedescr_list, $p_result_list, $v_options);
    if ($v_result != 1) {
      return 0;
    }

    // ----- Return
    return $p_result_list;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function :
  //   add($p_filelist, $p_add_dir="", $p_remove_dir="")
  //   add($p_filelist, $p_option, $p_option_value, ...)
  // Description :
  //   This method supports two synopsis. The first one is historical.
  //   This methods add the list of files in an existing archive.
  //   If a file with the same name already exists, it is added at the end of the
  //   archive, the first one is still present.
  //   If the archive does not exist, it is created.
  // Parameters :
  //   $p_filelist : An array containing file or directory names, or
  //                 a string containing one filename or one directory name, or
  //                 a string containing a list of filenames and/or directory
  //                 names separated by spaces.
  //   $p_add_dir : A path to add before the real path of the archived file,
  //                in order to have it memorized in the archive.
  //   $p_remove_dir : A path to remove from the real path of the file to archive,
  //                   in order to have a shorter path memorized in the archive.
  //                   When $p_add_dir and $p_remove_dir are set, $p_remove_dir
  //                   is removed first, before $p_add_dir is added.
  // Options :
  //   PCLZIP_OPT_ADD_PATH :
  //   PCLZIP_OPT_REMOVE_PATH :
  //   PCLZIP_OPT_REMOVE_ALL_PATH :
  //   PCLZIP_OPT_COMMENT :
  //   PCLZIP_OPT_ADD_COMMENT :
  //   PCLZIP_OPT_PREPEND_COMMENT :
  //   PCLZIP_CB_PRE_ADD :
  //   PCLZIP_CB_POST_ADD :
  // Return Values :
  //   0 on failure,
  //   The list of the added files, with a status of the add action.
  //   (see PclZip::listContent() for list entry format)
  // --------------------------------------------------------------------------------
  function add($p_filelist)
  {
    $v_result=1;

    // ----- Reset the error handler
    $this->privErrorReset();

    // ----- Set default values
    $v_options = array();
    $v_options[PCLZIP_OPT_NO_COMPRESSION] = FALSE;

    // ----- Look for variable options arguments
    $v_size = func_num_args();

    // ----- Look for arguments
    if ($v_size > 1) {
      // ----- Get the arguments
      $v_arg_list = func_get_args();

      // ----- Remove form the options list the first argument
      array_shift($v_arg_list);
      $v_size--;

      // ----- Look for first arg
      if ((is_integer($v_arg_list[0])) && ($v_arg_list[0] > 77000)) {

        // ----- Parse the options
        $v_result = $this->privParseOptions($v_arg_list, $v_size, $v_options,
                                            array (PCLZIP_OPT_REMOVE_PATH => 'optional',
                                                   PCLZIP_OPT_REMOVE_ALL_PATH => 'optional',
                                                   PCLZIP_OPT_ADD_PATH => 'optional',
                                                   PCLZIP_CB_PRE_ADD => 'optional',
                                                   PCLZIP_CB_POST_ADD => 'optional',
                                                   PCLZIP_OPT_NO_COMPRESSION => 'optional',
                                                   PCLZIP_OPT_COMMENT => 'optional',
                                                   PCLZIP_OPT_ADD_COMMENT => 'optional',
                                                   PCLZIP_OPT_PREPEND_COMMENT => 'optional',
                                                   PCLZIP_OPT_TEMP_FILE_THRESHOLD => 'optional',
                                                   PCLZIP_OPT_TEMP_FILE_ON => 'optional',
                                                   PCLZIP_OPT_TEMP_FILE_OFF => 'optional'
                                                   //, PCLZIP_OPT_CRYPT => 'optional'
												   ));
        if ($v_result != 1) {
          return 0;
        }
      }

      // ----- Look for 2 args
      // Here we need to support the first historic synopsis of the
      // method.
      else {

        // ----- Get the first argument
        $v_options[PCLZIP_OPT_ADD_PATH] = $v_add_path = $v_arg_list[0];

        // ----- Look for the optional second argument
        if ($v_size == 2) {
          $v_options[PCLZIP_OPT_REMOVE_PATH] = $v_arg_list[1];
        }
        else if ($v_size > 2) {
          // ----- Error log
          PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid number / type of arguments");

          // ----- Return
          return 0;
        }
      }
    }

    // ----- Look for default option values
    $this->privOptionDefaultThreshold($v_options);

    // ----- Init
    $v_string_list = array();
    $v_att_list = array();
    $v_filedescr_list = array();
    $p_result_list = array();

    // ----- Look if the $p_filelist is really an array
    if (is_array($p_filelist)) {

      // ----- Look if the first element is also an array
      //       This will mean that this is a file description entry
      if (isset($p_filelist[0]) && is_array($p_filelist[0])) {
        $v_att_list = $p_filelist;
      }

      // ----- The list is a list of string names
      else {
        $v_string_list = $p_filelist;
      }
    }

    // ----- Look if the $p_filelist is a string
    else if (is_string($p_filelist)) {
      // ----- Create a list from the string
      $v_string_list = explode(PCLZIP_SEPARATOR, $p_filelist);
    }

    // ----- Invalid variable type for $p_filelist
    else {
      PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid variable type '".gettype($p_filelist)."' for p_filelist");
      return 0;
    }

    // ----- Reformat the string list
    if (sizeof($v_string_list) != 0) {
      foreach ($v_string_list as $v_string) {
        $v_att_list[][PCLZIP_ATT_FILE_NAME] = $v_string;
      }
    }

    // ----- For each file in the list check the attributes
    $v_supported_attributes
    = array ( PCLZIP_ATT_FILE_NAME => 'mandatory'
             ,PCLZIP_ATT_FILE_NEW_SHORT_NAME => 'optional'
             ,PCLZIP_ATT_FILE_NEW_FULL_NAME => 'optional'
             ,PCLZIP_ATT_FILE_MTIME => 'optional'
             ,PCLZIP_ATT_FILE_CONTENT => 'optional'
             ,PCLZIP_ATT_FILE_COMMENT => 'optional'
						);
    foreach ($v_att_list as $v_entry) {
      $v_result = $this->privFileDescrParseAtt($v_entry,
                                               $v_filedescr_list[],
                                               $v_options,
                                               $v_supported_attributes);
      if ($v_result != 1) {
        return 0;
      }
    }

    // ----- Expand the filelist (expand directories)
    $v_result = $this->privFileDescrExpand($v_filedescr_list, $v_options);
    if ($v_result != 1) {
      return 0;
    }

    // ----- Call the create fct
    $v_result = $this->privAdd($v_filedescr_list, $p_result_list, $v_options);
    if ($v_result != 1) {
      return 0;
    }

    // ----- Return
    return $p_result_list;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : listContent()
  // Description :
  //   This public method, gives the list of the files and directories, with their
  //   properties.
  //   The properties of each entries in the list are (used also in other functions) :
  //     filename : Name of the file. For a create or add action it is the filename
  //                given by the user. For an extract function it is the filename
  //                of the extracted file.
  //     stored_filename : Name of the file / directory stored in the archive.
  //     size : Size of the stored file.
  //     compressed_size : Size of the file's data compressed in the archive
  //                       (without the headers overhead)
  //     mtime : Last known modification date of the file (UNIX timestamp)
  //     comment : Comment associated with the file
  //     folder : true | false
  //     index : index of the file in the archive
  //     status : status of the action (depending of the action) :
  //              Values are :
  //                ok : OK !
  //                filtered : the file / dir is not extracted (filtered by user)
  //                already_a_directory : the file can not be extracted because a
  //                                      directory with the same name already exists
  //                write_protected : the file can not be extracted because a file
  //                                  with the same name already exists and is
  //                                  write protected
  //                newer_exist : the file was not extracted because a newer file exists
  //                path_creation_fail : the file is not extracted because the folder
  //                                     does not exist and can not be created
  //                write_error : the file was not extracted because there was an
  //                              error while writing the file
  //                read_error : the file was not extracted because there was an error
  //                             while reading the file
  //                invalid_header : the file was not extracted because of an archive
  //                                 format error (bad file header)
  //   Note that each time a method can continue operating when there
  //   is an action error on a file, the error is only logged in the file status.
  // Return Values :
  //   0 on an unrecoverable failure,
  //   The list of the files in the archive.
  // --------------------------------------------------------------------------------
  function listContent()
  {
    $v_result=1;

    // ----- Reset the error handler
    $this->privErrorReset();

    // ----- Check archive
    if (!$this->privCheckFormat()) {
      return(0);
    }

    // ----- Call the extracting fct
    $p_list = array();
    if (($v_result = $this->privList($p_list)) != 1)
    {
      unset($p_list);
      return(0);
    }

    // ----- Return
    return $p_list;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function :
  //   extract($p_path="./", $p_remove_path="")
  //   extract([$p_option, $p_option_value, ...])
  // Description :
  //   This method supports two synopsis. The first one is historical.
  //   This method extract all the files / directories from the archive to the
  //   folder indicated in $p_path.
  //   If you want to ignore the 'root' part of path of the memorized files
  //   you can indicate this in the optional $p_remove_path parameter.
  //   By default, if a newer file with the same name already exists, the
  //   file is not extracted.
  //
  //   If both PCLZIP_OPT_PATH and PCLZIP_OPT_ADD_PATH options
  //   are used, the path indicated in PCLZIP_OPT_ADD_PATH is append
  //   at the end of the path value of PCLZIP_OPT_PATH.
  // Parameters :
  //   $p_path : Path where the files and directories are to be extracted
  //   $p_remove_path : First part ('root' part) of the memorized path
  //                    (if any similar) to remove while extracting.
  // Options :
  //   PCLZIP_OPT_PATH :
  //   PCLZIP_OPT_ADD_PATH :
  //   PCLZIP_OPT_REMOVE_PATH :
  //   PCLZIP_OPT_REMOVE_ALL_PATH :
  //   PCLZIP_CB_PRE_EXTRACT :
  //   PCLZIP_CB_POST_EXTRACT :
  // Return Values :
  //   0 or a negative value on failure,
  //   The list of the extracted files, with a status of the action.
  //   (see PclZip::listContent() for list entry format)
  // --------------------------------------------------------------------------------
  function extract()
  {
    $v_result=1;

    // ----- Reset the error handler
    $this->privErrorReset();

    // ----- Check archive
    if (!$this->privCheckFormat()) {
      return(0);
    }

    // ----- Set default values
    $v_options = array();
//    $v_path = "./";
    $v_path = '';
    $v_remove_path = "";
    $v_remove_all_path = false;

    // ----- Look for variable options arguments
    $v_size = func_num_args();

    // ----- Default values for option
    $v_options[PCLZIP_OPT_EXTRACT_AS_STRING] = FALSE;

    // ----- Look for arguments
    if ($v_size > 0) {
      // ----- Get the arguments
      $v_arg_list = func_get_args();

      // ----- Look for first arg
      if ((is_integer($v_arg_list[0])) && ($v_arg_list[0] > 77000)) {

        // ----- Parse the options
        $v_result = $this->privParseOptions($v_arg_list, $v_size, $v_options,
                                            array (PCLZIP_OPT_PATH => 'optional',
                                                   PCLZIP_OPT_REMOVE_PATH => 'optional',
                                                   PCLZIP_OPT_REMOVE_ALL_PATH => 'optional',
                                                   PCLZIP_OPT_ADD_PATH => 'optional',
                                                   PCLZIP_CB_PRE_EXTRACT => 'optional',
                                                   PCLZIP_CB_POST_EXTRACT => 'optional',
                                                   PCLZIP_OPT_SET_CHMOD => 'optional',
                                                   PCLZIP_OPT_BY_NAME => 'optional',
                                                   PCLZIP_OPT_BY_EREG => 'optional',
                                                   PCLZIP_OPT_BY_PREG => 'optional',
                                                   PCLZIP_OPT_BY_INDEX => 'optional',
                                                   PCLZIP_OPT_EXTRACT_AS_STRING => 'optional',
                                                   PCLZIP_OPT_EXTRACT_IN_OUTPUT => 'optional',
                                                   PCLZIP_OPT_REPLACE_NEWER => 'optional'
                                                   ,PCLZIP_OPT_STOP_ON_ERROR => 'optional'
                                                   ,PCLZIP_OPT_EXTRACT_DIR_RESTRICTION => 'optional',
                                                   PCLZIP_OPT_TEMP_FILE_THRESHOLD => 'optional',
                                                   PCLZIP_OPT_TEMP_FILE_ON => 'optional',
                                                   PCLZIP_OPT_TEMP_FILE_OFF => 'optional'
												    ));
        if ($v_result != 1) {
          return 0;
        }

        // ----- Set the arguments
        if (isset($v_options[PCLZIP_OPT_PATH])) {
          $v_path = $v_options[PCLZIP_OPT_PATH];
        }
        if (isset($v_options[PCLZIP_OPT_REMOVE_PATH])) {
          $v_remove_path = $v_options[PCLZIP_OPT_REMOVE_PATH];
        }
        if (isset($v_options[PCLZIP_OPT_REMOVE_ALL_PATH])) {
          $v_remove_all_path = $v_options[PCLZIP_OPT_REMOVE_ALL_PATH];
        }
        if (isset($v_options[PCLZIP_OPT_ADD_PATH])) {
          // ----- Check for '/' in last path char
          if ((strlen($v_path) > 0) && (substr($v_path, -1) != '/')) {
            $v_path .= '/';
          }
          $v_path .= $v_options[PCLZIP_OPT_ADD_PATH];
        }
      }

      // ----- Look for 2 args
      // Here we need to support the first historic synopsis of the
      // method.
      else {

        // ----- Get the first argument
        $v_path = $v_arg_list[0];

        // ----- Look for the optional second argument
        if ($v_size == 2) {
          $v_remove_path = $v_arg_list[1];
        }
        else if ($v_size > 2) {
          // ----- Error log
          PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid number / type of arguments");

          // ----- Return
          return 0;
        }
      }
    }

    // ----- Look for default option values
    $this->privOptionDefaultThreshold($v_options);

    // ----- Trace

    // ----- Call the extracting fct
    $p_list = array();
    $v_result = $this->privExtractByRule($p_list, $v_path, $v_remove_path,
	                                     $v_remove_all_path, $v_options);
    if ($v_result < 1) {
      unset($p_list);
      return(0);
    }

    // ----- Return
    return $p_list;
  }
  // --------------------------------------------------------------------------------


  // --------------------------------------------------------------------------------
  // Function :
  //   extractByIndex($p_index, $p_path="./", $p_remove_path="")
  //   extractByIndex($p_index, [$p_option, $p_option_value, ...])
  // Description :
  //   This method supports two synopsis. The first one is historical.
  //   This method is doing a partial extract of the archive.
  //   The extracted files or folders are identified by their index in the
  //   archive (from 0 to n).
  //   Note that if the index identify a folder, only the folder entry is
  //   extracted, not all the files included in the archive.
  // Parameters :
  //   $p_index : A single index (integer) or a string of indexes of files to
  //              extract. The form of the string is "0,4-6,8-12" with only numbers
  //              and '-' for range or ',' to separate ranges. No spaces or ';'
  //              are allowed.
  //   $p_path : Path where the files and directories are to be extracted
  //   $p_remove_path : First part ('root' part) of the memorized path
  //                    (if any similar) to remove while extracting.
  // Options :
  //   PCLZIP_OPT_PATH :
  //   PCLZIP_OPT_ADD_PATH :
  //   PCLZIP_OPT_REMOVE_PATH :
  //   PCLZIP_OPT_REMOVE_ALL_PATH :
  //   PCLZIP_OPT_EXTRACT_AS_STRING : The files are extracted as strings and
  //     not as files.
  //     The resulting content is in a new field 'content' in the file
  //     structure.
  //     This option must be used alone (any other options are ignored).
  //   PCLZIP_CB_PRE_EXTRACT :
  //   PCLZIP_CB_POST_EXTRACT :
  // Return Values :
  //   0 on failure,
  //   The list of the extracted files, with a status of the action.
  //   (see PclZip::listContent() for list entry format)
  // --------------------------------------------------------------------------------
  //function extractByIndex($p_index, options...)
  function extractByIndex($p_index)
  {
    $v_result=1;

    // ----- Reset the error handler
    $this->privErrorReset();

    // ----- Check archive
    if (!$this->privCheckFormat()) {
      return(0);
    }

    // ----- Set default values
    $v_options = array();
//    $v_path = "./";
    $v_path = '';
    $v_remove_path = "";
    $v_remove_all_path = false;

    // ----- Look for variable options arguments
    $v_size = func_num_args();

    // ----- Default values for option
    $v_options[PCLZIP_OPT_EXTRACT_AS_STRING] = FALSE;

    // ----- Look for arguments
    if ($v_size > 1) {
      // ----- Get the arguments
      $v_arg_list = func_get_args();

      // ----- Remove form the options list the first argument
      array_shift($v_arg_list);
      $v_size--;

      // ----- Look for first arg
      if ((is_integer($v_arg_list[0])) && ($v_arg_list[0] > 77000)) {

        // ----- Parse the options
        $v_result = $this->privParseOptions($v_arg_list, $v_size, $v_options,
                                            array (PCLZIP_OPT_PATH => 'optional',
                                                   PCLZIP_OPT_REMOVE_PATH => 'optional',
                                                   PCLZIP_OPT_REMOVE_ALL_PATH => 'optional',
                                                   PCLZIP_OPT_EXTRACT_AS_STRING => 'optional',
                                                   PCLZIP_OPT_ADD_PATH => 'optional',
                                                   PCLZIP_CB_PRE_EXTRACT => 'optional',
                                                   PCLZIP_CB_POST_EXTRACT => 'optional',
                                                   PCLZIP_OPT_SET_CHMOD => 'optional',
                                                   PCLZIP_OPT_REPLACE_NEWER => 'optional'
                                                   ,PCLZIP_OPT_STOP_ON_ERROR => 'optional'
                                                   ,PCLZIP_OPT_EXTRACT_DIR_RESTRICTION => 'optional',
                                                   PCLZIP_OPT_TEMP_FILE_THRESHOLD => 'optional',
                                                   PCLZIP_OPT_TEMP_FILE_ON => 'optional',
                                                   PCLZIP_OPT_TEMP_FILE_OFF => 'optional'
												   ));
        if ($v_result != 1) {
          return 0;
        }

        // ----- Set the arguments
        if (isset($v_options[PCLZIP_OPT_PATH])) {
          $v_path = $v_options[PCLZIP_OPT_PATH];
        }
        if (isset($v_options[PCLZIP_OPT_REMOVE_PATH])) {
          $v_remove_path = $v_options[PCLZIP_OPT_REMOVE_PATH];
        }
        if (isset($v_options[PCLZIP_OPT_REMOVE_ALL_PATH])) {
          $v_remove_all_path = $v_options[PCLZIP_OPT_REMOVE_ALL_PATH];
        }
        if (isset($v_options[PCLZIP_OPT_ADD_PATH])) {
          // ----- Check for '/' in last path char
          if ((strlen($v_path) > 0) && (substr($v_path, -1) != '/')) {
            $v_path .= '/';
          }
          $v_path .= $v_options[PCLZIP_OPT_ADD_PATH];
        }
        if (!isset($v_options[PCLZIP_OPT_EXTRACT_AS_STRING])) {
          $v_options[PCLZIP_OPT_EXTRACT_AS_STRING] = FALSE;
        }
        else {
        }
      }

      // ----- Look for 2 args
      // Here we need to support the first historic synopsis of the
      // method.
      else {

        // ----- Get the first argument
        $v_path = $v_arg_list[0];

        // ----- Look for the optional second argument
        if ($v_size == 2) {
          $v_remove_path = $v_arg_list[1];
        }
        else if ($v_size > 2) {
          // ----- Error log
          PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid number / type of arguments");

          // ----- Return
          return 0;
        }
      }
    }

    // ----- Trace

    // ----- Trick
    // Here I want to reuse extractByRule(), so I need to parse the $p_index
    // with privParseOptions()
    $v_arg_trick = array (PCLZIP_OPT_BY_INDEX, $p_index);
    $v_options_trick = array();
    $v_result = $this->privParseOptions($v_arg_trick, sizeof($v_arg_trick), $v_options_trick,
                                        array (PCLZIP_OPT_BY_INDEX => 'optional' ));
    if ($v_result != 1) {
        return 0;
    }
    $v_options[PCLZIP_OPT_BY_INDEX] = $v_options_trick[PCLZIP_OPT_BY_INDEX];

    // ----- Look for default option values
    $this->privOptionDefaultThreshold($v_options);

    // ----- Call the extracting fct
    if (($v_result = $this->privExtractByRule($p_list, $v_path, $v_remove_path, $v_remove_all_path, $v_options)) < 1) {
        return(0);
    }

    // ----- Return
    return $p_list;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function :
  //   delete([$p_option, $p_option_value, ...])
  // Description :
  //   This method removes files from the archive.
  //   If no parameters are given, then all the archive is emptied.
  // Parameters :
  //   None or optional arguments.
  // Options :
  //   PCLZIP_OPT_BY_INDEX :
  //   PCLZIP_OPT_BY_NAME :
  //   PCLZIP_OPT_BY_EREG :
  //   PCLZIP_OPT_BY_PREG :
  // Return Values :
  //   0 on failure,
  //   The list of the files which are still present in the archive.
  //   (see PclZip::listContent() for list entry format)
  // --------------------------------------------------------------------------------
  function delete()
  {
    $v_result=1;

    // ----- Reset the error handler
    $this->privErrorReset();

    // ----- Check archive
    if (!$this->privCheckFormat()) {
      return(0);
    }

    // ----- Set default values
    $v_options = array();

    // ----- Look for variable options arguments
    $v_size = func_num_args();

    // ----- Look for arguments
    if ($v_size > 0) {
      // ----- Get the arguments
      $v_arg_list = func_get_args();

      // ----- Parse the options
      $v_result = $this->privParseOptions($v_arg_list, $v_size, $v_options,
                                        array (PCLZIP_OPT_BY_NAME => 'optional',
                                               PCLZIP_OPT_BY_EREG => 'optional',
                                               PCLZIP_OPT_BY_PREG => 'optional',
                                               PCLZIP_OPT_BY_INDEX => 'optional' ));
      if ($v_result != 1) {
          return 0;
      }
    }

    // ----- Magic quotes trick
    $this->privDisableMagicQuotes();

    // ----- Call the delete fct
    $v_list = array();
    if (($v_result = $this->privDeleteByRule($v_list, $v_options)) != 1) {
      $this->privSwapBackMagicQuotes();
      unset($v_list);
      return(0);
    }

    // ----- Magic quotes trick
    $this->privSwapBackMagicQuotes();

    // ----- Return
    return $v_list;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : deleteByIndex()
  // Description :
  //   ***** Deprecated *****
  //   delete(PCLZIP_OPT_BY_INDEX, $p_index) should be preferred.
  // --------------------------------------------------------------------------------
  function deleteByIndex($p_index)
  {

    $p_list = $this->delete(PCLZIP_OPT_BY_INDEX, $p_index);

    // ----- Return
    return $p_list;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : properties()
  // Description :
  //   This method gives the properties of the archive.
  //   The properties are :
  //     nb : Number of files in the archive
  //     comment : Comment associated with the archive file
  //     status : not_exist, ok
  // Parameters :
  //   None
  // Return Values :
  //   0 on failure,
  //   An array with the archive properties.
  // --------------------------------------------------------------------------------
  function properties()
  {

    // ----- Reset the error handler
    $this->privErrorReset();

    // ----- Magic quotes trick
    $this->privDisableMagicQuotes();

    // ----- Check archive
    if (!$this->privCheckFormat()) {
      $this->privSwapBackMagicQuotes();
      return(0);
    }

    // ----- Default properties
    $v_prop = array();
    $v_prop['comment'] = '';
    $v_prop['nb'] = 0;
    $v_prop['status'] = 'not_exist';

    // ----- Look if file exists
    if (@is_file($this->zipname))
    {
      // ----- Open the zip file
      if (($this->zip_fd = @fopen($this->zipname, 'rb')) == 0)
      {
        $this->privSwapBackMagicQuotes();

        // ----- Error log
        PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open archive \''.$this->zipname.'\' in binary read mode');

        // ----- Return
        return 0;
      }

      // ----- Read the central directory information
      $v_central_dir = array();
      if (($v_result = $this->privReadEndCentralDir($v_central_dir)) != 1)
      {
        $this->privSwapBackMagicQuotes();
        return 0;
      }

      // ----- Close the zip file
      $this->privCloseFd();

      // ----- Set the user attributes
      $v_prop['comment'] = $v_central_dir['comment'];
      $v_prop['nb'] = $v_central_dir['entries'];
      $v_prop['status'] = 'ok';
    }

    // ----- Magic quotes trick
    $this->privSwapBackMagicQuotes();

    // ----- Return
    return $v_prop;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : duplicate()
  // Description :
  //   This method creates an archive by copying the content of an other one. If
  //   the archive already exist, it is replaced by the new one without any warning.
  // Parameters :
  //   $p_archive : The filename of a valid archive, or
  //                a valid PclZip object.
  // Return Values :
  //   1 on success.
  //   0 or a negative value on error (error code).
  // --------------------------------------------------------------------------------
  function duplicate($p_archive)
  {
    $v_result = 1;

    // ----- Reset the error handler
    $this->privErrorReset();

    // ----- Look if the $p_archive is an instantiated PclZip object
    if ($p_archive instanceof pclzip)
    {

      // ----- Duplicate the archive
      $v_result = $this->privDuplicate($p_archive->zipname);
    }

    // ----- Look if the $p_archive is a string (so a filename)
    else if (is_string($p_archive))
    {

      // ----- Check that $p_archive is a valid zip file
      // TBC : Should also check the archive format
      if (!is_file($p_archive)) {
        // ----- Error log
        PclZip::privErrorLog(PCLZIP_ERR_MISSING_FILE, "No file with filename '".$p_archive."'");
        $v_result = PCLZIP_ERR_MISSING_FILE;
      }
      else {
        // ----- Duplicate the archive
        $v_result = $this->privDuplicate($p_archive);
      }
    }

    // ----- Invalid variable
    else
    {
      // ----- Error log
      PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid variable type p_archive_to_add");
      $v_result = PCLZIP_ERR_INVALID_PARAMETER;
    }

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : merge()
  // Description :
  //   This method merge the $p_archive_to_add archive at the end of the current
  //   one ($this).
  //   If the archive ($this) does not exist, the merge becomes a duplicate.
  //   If the $p_archive_to_add archive does not exist, the merge is a success.
  // Parameters :
  //   $p_archive_to_add : It can be directly the filename of a valid zip archive,
  //                       or a PclZip object archive.
  // Return Values :
  //   1 on success,
  //   0 or negative values on error (see below).
  // --------------------------------------------------------------------------------
  function merge($p_archive_to_add)
  {
    $v_result = 1;

    // ----- Reset the error handler
    $this->privErrorReset();

    // ----- Check archive
    if (!$this->privCheckFormat()) {
      return(0);
    }

    // ----- Look if the $p_archive_to_add is an instantiated PclZip object
    if ($p_archive_to_add instanceof pclzip)
    {

      // ----- Merge the archive
      $v_result = $this->privMerge($p_archive_to_add);
    }

    // ----- Look if the $p_archive_to_add is a string (so a filename)
    else if (is_string($p_archive_to_add))
    {

      // ----- Create a temporary archive
      $v_object_archive = new PclZip($p_archive_to_add);

      // ----- Merge the archive
      $v_result = $this->privMerge($v_object_archive);
    }

    // ----- Invalid variable
    else
    {
      // ----- Error log
      PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid variable type p_archive_to_add");
      $v_result = PCLZIP_ERR_INVALID_PARAMETER;
    }

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------



  // --------------------------------------------------------------------------------
  // Function : errorCode()
  // Description :
  // Parameters :
  // --------------------------------------------------------------------------------
  function errorCode()
  {
    if (PCLZIP_ERROR_EXTERNAL == 1) {
      return(PclErrorCode());
    }
    else {
      return($this->error_code);
    }
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : errorName()
  // Description :
  // Parameters :
  // --------------------------------------------------------------------------------
  function errorName($p_with_code=false)
  {
    $v_name = array ( PCLZIP_ERR_NO_ERROR => 'PCLZIP_ERR_NO_ERROR',
                      PCLZIP_ERR_WRITE_OPEN_FAIL => 'PCLZIP_ERR_WRITE_OPEN_FAIL',
                      PCLZIP_ERR_READ_OPEN_FAIL => 'PCLZIP_ERR_READ_OPEN_FAIL',
                      PCLZIP_ERR_INVALID_PARAMETER => 'PCLZIP_ERR_INVALID_PARAMETER',
                      PCLZIP_ERR_MISSING_FILE => 'PCLZIP_ERR_MISSING_FILE',
                      PCLZIP_ERR_FILENAME_TOO_LONG => 'PCLZIP_ERR_FILENAME_TOO_LONG',
                      PCLZIP_ERR_INVALID_ZIP => 'PCLZIP_ERR_INVALID_ZIP',
                      PCLZIP_ERR_BAD_EXTRACTED_FILE => 'PCLZIP_ERR_BAD_EXTRACTED_FILE',
                      PCLZIP_ERR_DIR_CREATE_FAIL => 'PCLZIP_ERR_DIR_CREATE_FAIL',
                      PCLZIP_ERR_BAD_EXTENSION => 'PCLZIP_ERR_BAD_EXTENSION',
                      PCLZIP_ERR_BAD_FORMAT => 'PCLZIP_ERR_BAD_FORMAT',
                      PCLZIP_ERR_DELETE_FILE_FAIL => 'PCLZIP_ERR_DELETE_FILE_FAIL',
                      PCLZIP_ERR_RENAME_FILE_FAIL => 'PCLZIP_ERR_RENAME_FILE_FAIL',
                      PCLZIP_ERR_BAD_CHECKSUM => 'PCLZIP_ERR_BAD_CHECKSUM',
                      PCLZIP_ERR_INVALID_ARCHIVE_ZIP => 'PCLZIP_ERR_INVALID_ARCHIVE_ZIP',
                      PCLZIP_ERR_MISSING_OPTION_VALUE => 'PCLZIP_ERR_MISSING_OPTION_VALUE',
                      PCLZIP_ERR_INVALID_OPTION_VALUE => 'PCLZIP_ERR_INVALID_OPTION_VALUE',
                      PCLZIP_ERR_UNSUPPORTED_COMPRESSION => 'PCLZIP_ERR_UNSUPPORTED_COMPRESSION',
                      PCLZIP_ERR_UNSUPPORTED_ENCRYPTION => 'PCLZIP_ERR_UNSUPPORTED_ENCRYPTION'
                      ,PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE => 'PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE'
                      ,PCLZIP_ERR_DIRECTORY_RESTRICTION => 'PCLZIP_ERR_DIRECTORY_RESTRICTION'
                    );

    if (isset($v_name[$this->error_code])) {
      $v_value = $v_name[$this->error_code];
    }
    else {
      $v_value = 'NoName';
    }

    if ($p_with_code) {
      return($v_value.' ('.$this->error_code.')');
    }
    else {
      return($v_value);
    }
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : errorInfo()
  // Description :
  // Parameters :
  // --------------------------------------------------------------------------------
  function errorInfo($p_full=false)
  {
    if (PCLZIP_ERROR_EXTERNAL == 1) {
      return(PclErrorString());
    }
    else {
      if ($p_full) {
        return($this->errorName(true)." : ".$this->error_string);
      }
      else {
        return($this->error_string." [code ".$this->error_code."]");
      }
    }
  }
  // --------------------------------------------------------------------------------


// --------------------------------------------------------------------------------
// ***** UNDER THIS LINE ARE DEFINED PRIVATE INTERNAL FUNCTIONS *****
// *****                                                        *****
// *****       THESES FUNCTIONS MUST NOT BE USED DIRECTLY       *****
// --------------------------------------------------------------------------------



  // --------------------------------------------------------------------------------
  // Function : privCheckFormat()
  // Description :
  //   This method check that the archive exists and is a valid zip archive.
  //   Several level of check exists. (future)
  // Parameters :
  //   $p_level : Level of check. Default 0.
  //              0 : Check the first bytes (magic codes) (default value))
  //              1 : 0 + Check the central directory (future)
  //              2 : 1 + Check each file header (future)
  // Return Values :
  //   true on success,
  //   false on error, the error code is set.
  // --------------------------------------------------------------------------------
  function privCheckFormat($p_level=0)
  {
    $v_result = true;

	// ----- Reset the file system cache
    clearstatcache();

    // ----- Reset the error handler
    $this->privErrorReset();

    // ----- Look if the file exits
    if (!is_file($this->zipname)) {
      // ----- Error log
      PclZip::privErrorLog(PCLZIP_ERR_MISSING_FILE, "Missing archive file '".$this->zipname."'");
      return(false);
    }

    // ----- Check that the file is readable
    if (!is_readable($this->zipname)) {
      // ----- Error log
      PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, "Unable to read archive '".$this->zipname."'");
      return(false);
    }

    // ----- Check the magic code
    // TBC

    // ----- Check the central header
    // TBC

    // ----- Check each file header
    // TBC

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privParseOptions()
  // Description :
  //   This internal methods reads the variable list of arguments ($p_options_list,
  //   $p_size) and generate an array with the options and values ($v_result_list).
  //   $v_requested_options contains the options that can be present and those that
  //   must be present.
  //   $v_requested_options is an array, with the option value as key, and 'optional',
  //   or 'mandatory' as value.
  // Parameters :
  //   See above.
  // Return Values :
  //   1 on success.
  //   0 on failure.
  // --------------------------------------------------------------------------------
  function privParseOptions(&$p_options_list, $p_size, &$v_result_list, $v_requested_options=false)
  {
    $v_result=1;

    // ----- Read the options
    $i=0;
    while ($i<$p_size) {

      // ----- Check if the option is supported
      if (!isset($v_requested_options[$p_options_list[$i]])) {
        // ----- Error log
        PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid optional parameter '".$p_options_list[$i]."' for this method");

        // ----- Return
        return PclZip::errorCode();
      }

      // ----- Look for next option
      switch ($p_options_list[$i]) {
        // ----- Look for options that request a path value
        case PCLZIP_OPT_PATH :
        case PCLZIP_OPT_REMOVE_PATH :
        case PCLZIP_OPT_ADD_PATH :
          // ----- Check the number of parameters
          if (($i+1) >= $p_size) {
            // ----- Error log
            PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '".PclZipUtilOptionText($p_options_list[$i])."'");

            // ----- Return
            return PclZip::errorCode();
          }

          // ----- Get the value
          $v_result_list[$p_options_list[$i]] = PclZipUtilTranslateWinPath($p_options_list[$i+1], FALSE);
          $i++;
        break;

        case PCLZIP_OPT_TEMP_FILE_THRESHOLD :
          // ----- Check the number of parameters
          if (($i+1) >= $p_size) {
            PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '".PclZipUtilOptionText($p_options_list[$i])."'");
            return PclZip::errorCode();
          }

          // ----- Check for incompatible options
          if (isset($v_result_list[PCLZIP_OPT_TEMP_FILE_OFF])) {
            PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Option '".PclZipUtilOptionText($p_options_list[$i])."' can not be used with option 'PCLZIP_OPT_TEMP_FILE_OFF'");
            return PclZip::errorCode();
          }

          // ----- Check the value
          $v_value = $p_options_list[$i+1];
          if ((!is_integer($v_value)) || ($v_value<0)) {
            PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Integer expected for option '".PclZipUtilOptionText($p_options_list[$i])."'");
            return PclZip::errorCode();
          }

          // ----- Get the value (and convert it in bytes)
          $v_result_list[$p_options_list[$i]] = $v_value*1048576;
          $i++;
        break;

        case PCLZIP_OPT_TEMP_FILE_ON :
          // ----- Check for incompatible options
          if (isset($v_result_list[PCLZIP_OPT_TEMP_FILE_OFF])) {
            PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Option '".PclZipUtilOptionText($p_options_list[$i])."' can not be used with option 'PCLZIP_OPT_TEMP_FILE_OFF'");
            return PclZip::errorCode();
          }

          $v_result_list[$p_options_list[$i]] = true;
        break;

        case PCLZIP_OPT_TEMP_FILE_OFF :
          // ----- Check for incompatible options
          if (isset($v_result_list[PCLZIP_OPT_TEMP_FILE_ON])) {
            PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Option '".PclZipUtilOptionText($p_options_list[$i])."' can not be used with option 'PCLZIP_OPT_TEMP_FILE_ON'");
            return PclZip::errorCode();
          }
          // ----- Check for incompatible options
          if (isset($v_result_list[PCLZIP_OPT_TEMP_FILE_THRESHOLD])) {
            PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Option '".PclZipUtilOptionText($p_options_list[$i])."' can not be used with option 'PCLZIP_OPT_TEMP_FILE_THRESHOLD'");
            return PclZip::errorCode();
          }

          $v_result_list[$p_options_list[$i]] = true;
        break;

        case PCLZIP_OPT_EXTRACT_DIR_RESTRICTION :
          // ----- Check the number of parameters
          if (($i+1) >= $p_size) {
            // ----- Error log
            PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '".PclZipUtilOptionText($p_options_list[$i])."'");

            // ----- Return
            return PclZip::errorCode();
          }

          // ----- Get the value
          if (   is_string($p_options_list[$i+1])
              && ($p_options_list[$i+1] != '')) {
            $v_result_list[$p_options_list[$i]] = PclZipUtilTranslateWinPath($p_options_list[$i+1], FALSE);
            $i++;
          }
          else {
          }
        break;

        // ----- Look for options that request an array of string for value
        case PCLZIP_OPT_BY_NAME :
          // ----- Check the number of parameters
          if (($i+1) >= $p_size) {
            // ----- Error log
            PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '".PclZipUtilOptionText($p_options_list[$i])."'");

            // ----- Return
            return PclZip::errorCode();
          }

          // ----- Get the value
          if (is_string($p_options_list[$i+1])) {
              $v_result_list[$p_options_list[$i]][0] = $p_options_list[$i+1];
          }
          else if (is_array($p_options_list[$i+1])) {
              $v_result_list[$p_options_list[$i]] = $p_options_list[$i+1];
          }
          else {
            // ----- Error log
            PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Wrong parameter value for option '".PclZipUtilOptionText($p_options_list[$i])."'");

            // ----- Return
            return PclZip::errorCode();
          }
          $i++;
        break;

        // ----- Look for options that request an EREG or PREG expression
        case PCLZIP_OPT_BY_EREG :
          // ereg() is deprecated starting with PHP 5.3. Move PCLZIP_OPT_BY_EREG
          // to PCLZIP_OPT_BY_PREG
          $p_options_list[$i] = PCLZIP_OPT_BY_PREG;
        case PCLZIP_OPT_BY_PREG :
        //case PCLZIP_OPT_CRYPT :
          // ----- Check the number of parameters
          if (($i+1) >= $p_size) {
            // ----- Error log
            PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '".PclZipUtilOptionText($p_options_list[$i])."'");

            // ----- Return
            return PclZip::errorCode();
          }

          // ----- Get the value
          if (is_string($p_options_list[$i+1])) {
              $v_result_list[$p_options_list[$i]] = $p_options_list[$i+1];
          }
          else {
            // ----- Error log
            PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Wrong parameter value for option '".PclZipUtilOptionText($p_options_list[$i])."'");

            // ----- Return
            return PclZip::errorCode();
          }
          $i++;
        break;

        // ----- Look for options that takes a string
        case PCLZIP_OPT_COMMENT :
        case PCLZIP_OPT_ADD_COMMENT :
        case PCLZIP_OPT_PREPEND_COMMENT :
          // ----- Check the number of parameters
          if (($i+1) >= $p_size) {
            // ----- Error log
            PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE,
			                     "Missing parameter value for option '"
								 .PclZipUtilOptionText($p_options_list[$i])
								 ."'");

            // ----- Return
            return PclZip::errorCode();
          }

          // ----- Get the value
          if (is_string($p_options_list[$i+1])) {
              $v_result_list[$p_options_list[$i]] = $p_options_list[$i+1];
          }
          else {
            // ----- Error log
            PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE,
			                     "Wrong parameter value for option '"
								 .PclZipUtilOptionText($p_options_list[$i])
								 ."'");

            // ----- Return
            return PclZip::errorCode();
          }
          $i++;
        break;

        // ----- Look for options that request an array of index
        case PCLZIP_OPT_BY_INDEX :
          // ----- Check the number of parameters
          if (($i+1) >= $p_size) {
            // ----- Error log
            PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '".PclZipUtilOptionText($p_options_list[$i])."'");

            // ----- Return
            return PclZip::errorCode();
          }

          // ----- Get the value
          $v_work_list = array();
          if (is_string($p_options_list[$i+1])) {

              // ----- Remove spaces
              $p_options_list[$i+1] = strtr($p_options_list[$i+1], ' ', '');

              // ----- Parse items
              $v_work_list = explode(",", $p_options_list[$i+1]);
          }
          else if (is_integer($p_options_list[$i+1])) {
              $v_work_list[0] = $p_options_list[$i+1].'-'.$p_options_list[$i+1];
          }
          else if (is_array($p_options_list[$i+1])) {
              $v_work_list = $p_options_list[$i+1];
          }
          else {
            // ----- Error log
            PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Value must be integer, string or array for option '".PclZipUtilOptionText($p_options_list[$i])."'");

            // ----- Return
            return PclZip::errorCode();
          }

          // ----- Reduce the index list
          // each index item in the list must be a couple with a start and
          // an end value : [0,3], [5-5], [8-10], ...
          // ----- Check the format of each item
          $v_sort_flag=false;
          $v_sort_value=0;
          for ($j=0; $j<sizeof($v_work_list); $j++) {
              // ----- Explode the item
              $v_item_list = explode("-", $v_work_list[$j]);
              $v_size_item_list = sizeof($v_item_list);

              // ----- TBC : Here we might check that each item is a
              // real integer ...

              // ----- Look for single value
              if ($v_size_item_list == 1) {
                  // ----- Set the option value
                  $v_result_list[$p_options_list[$i]][$j]['start'] = $v_item_list[0];
                  $v_result_list[$p_options_list[$i]][$j]['end'] = $v_item_list[0];
              }
              elseif ($v_size_item_list == 2) {
                  // ----- Set the option value
                  $v_result_list[$p_options_list[$i]][$j]['start'] = $v_item_list[0];
                  $v_result_list[$p_options_list[$i]][$j]['end'] = $v_item_list[1];
              }
              else {
                  // ----- Error log
                  PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Too many values in index range for option '".PclZipUtilOptionText($p_options_list[$i])."'");

                  // ----- Return
                  return PclZip::errorCode();
              }


              // ----- Look for list sort
              if ($v_result_list[$p_options_list[$i]][$j]['start'] < $v_sort_value) {
                  $v_sort_flag=true;

                  // ----- TBC : An automatic sort should be written ...
                  // ----- Error log
                  PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Invalid order of index range for option '".PclZipUtilOptionText($p_options_list[$i])."'");

                  // ----- Return
                  return PclZip::errorCode();
              }
              $v_sort_value = $v_result_list[$p_options_list[$i]][$j]['start'];
          }

          // ----- Sort the items
          if ($v_sort_flag) {
              // TBC : To Be Completed
          }

          // ----- Next option
          $i++;
        break;

        // ----- Look for options that request no value
        case PCLZIP_OPT_REMOVE_ALL_PATH :
        case PCLZIP_OPT_EXTRACT_AS_STRING :
        case PCLZIP_OPT_NO_COMPRESSION :
        case PCLZIP_OPT_EXTRACT_IN_OUTPUT :
        case PCLZIP_OPT_REPLACE_NEWER :
        case PCLZIP_OPT_STOP_ON_ERROR :
          $v_result_list[$p_options_list[$i]] = true;
        break;

        // ----- Look for options that request an octal value
        case PCLZIP_OPT_SET_CHMOD :
          // ----- Check the number of parameters
          if (($i+1) >= $p_size) {
            // ----- Error log
            PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '".PclZipUtilOptionText($p_options_list[$i])."'");

            // ----- Return
            return PclZip::errorCode();
          }

          // ----- Get the value
          $v_result_list[$p_options_list[$i]] = $p_options_list[$i+1];
          $i++;
        break;

        // ----- Look for options that request a call-back
        case PCLZIP_CB_PRE_EXTRACT :
        case PCLZIP_CB_POST_EXTRACT :
        case PCLZIP_CB_PRE_ADD :
        case PCLZIP_CB_POST_ADD :
        /* for future use
        case PCLZIP_CB_PRE_DELETE :
        case PCLZIP_CB_POST_DELETE :
        case PCLZIP_CB_PRE_LIST :
        case PCLZIP_CB_POST_LIST :
        */
          // ----- Check the number of parameters
          if (($i+1) >= $p_size) {
            // ----- Error log
            PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '".PclZipUtilOptionText($p_options_list[$i])."'");

            // ----- Return
            return PclZip::errorCode();
          }

          // ----- Get the value
          $v_function_name = $p_options_list[$i+1];

          // ----- Check that the value is a valid existing function
          if (!function_exists($v_function_name)) {
            // ----- Error log
            PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Function '".$v_function_name."()' is not an existing function for option '".PclZipUtilOptionText($p_options_list[$i])."'");

            // ----- Return
            return PclZip::errorCode();
          }

          // ----- Set the attribute
          $v_result_list[$p_options_list[$i]] = $v_function_name;
          $i++;
        break;

        default :
          // ----- Error log
          PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER,
		                       "Unknown parameter '"
							   .$p_options_list[$i]."'");

          // ----- Return
          return PclZip::errorCode();
      }

      // ----- Next options
      $i++;
    }

    // ----- Look for mandatory options
    if ($v_requested_options !== false) {
      for ($key=reset($v_requested_options); $key=key($v_requested_options); $key=next($v_requested_options)) {
        // ----- Look for mandatory option
        if ($v_requested_options[$key] == 'mandatory') {
          // ----- Look if present
          if (!isset($v_result_list[$key])) {
            // ----- Error log
            PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Missing mandatory parameter ".PclZipUtilOptionText($key)."(".$key.")");

            // ----- Return
            return PclZip::errorCode();
          }
        }
      }
    }

    // ----- Look for default values
    if (!isset($v_result_list[PCLZIP_OPT_TEMP_FILE_THRESHOLD])) {

    }

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privOptionDefaultThreshold()
  // Description :
  // Parameters :
  // Return Values :
  // --------------------------------------------------------------------------------
  function privOptionDefaultThreshold(&$p_options)
  {
    $v_result=1;

    if (isset($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD])
        || isset($p_options[PCLZIP_OPT_TEMP_FILE_OFF])) {
      return $v_result;
    }

    // ----- Get 'memory_limit' configuration value
    $v_memory_limit = ini_get('memory_limit');
    $v_memory_limit = trim($v_memory_limit);
    $v_memory_limit_int = (int) $v_memory_limit;
    $last = strtolower(substr($v_memory_limit, -1));

    if($last == 'g')
        //$v_memory_limit_int = $v_memory_limit_int*1024*1024*1024;
        $v_memory_limit_int = $v_memory_limit_int*1073741824;
    if($last == 'm')
        //$v_memory_limit_int = $v_memory_limit_int*1024*1024;
        $v_memory_limit_int = $v_memory_limit_int*1048576;
    if($last == 'k')
        $v_memory_limit_int = $v_memory_limit_int*1024;

    $p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD] = floor($v_memory_limit_int*PCLZIP_TEMPORARY_FILE_RATIO);


    // ----- Confidence check : No threshold if value lower than 1M
    if ($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD] < 1048576) {
      unset($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD]);
    }

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privFileDescrParseAtt()
  // Description :
  // Parameters :
  // Return Values :
  //   1 on success.
  //   0 on failure.
  // --------------------------------------------------------------------------------
  function privFileDescrParseAtt(&$p_file_list, &$p_filedescr, $v_options, $v_requested_options=false)
  {
    $v_result=1;

    // ----- For each file in the list check the attributes
    foreach ($p_file_list as $v_key => $v_value) {

      // ----- Check if the option is supported
      if (!isset($v_requested_options[$v_key])) {
        // ----- Error log
        PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid file attribute '".$v_key."' for this file");

        // ----- Return
        return PclZip::errorCode();
      }

      // ----- Look for attribute
      switch ($v_key) {
        case PCLZIP_ATT_FILE_NAME :
          if (!is_string($v_value)) {
            PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid type ".gettype($v_value).". String expected for attribute '".PclZipUtilOptionText($v_key)."'");
            return PclZip::errorCode();
          }

          $p_filedescr['filename'] = PclZipUtilPathReduction($v_value);

          if ($p_filedescr['filename'] == '') {
            PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid empty filename for attribute '".PclZipUtilOptionText($v_key)."'");
            return PclZip::errorCode();
          }

        break;

        case PCLZIP_ATT_FILE_NEW_SHORT_NAME :
          if (!is_string($v_value)) {
            PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid type ".gettype($v_value).". String expected for attribute '".PclZipUtilOptionText($v_key)."'");
            return PclZip::errorCode();
          }

          $p_filedescr['new_short_name'] = PclZipUtilPathReduction($v_value);

          if ($p_filedescr['new_short_name'] == '') {
            PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid empty short filename for attribute '".PclZipUtilOptionText($v_key)."'");
            return PclZip::errorCode();
          }
        break;

        case PCLZIP_ATT_FILE_NEW_FULL_NAME :
          if (!is_string($v_value)) {
            PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid type ".gettype($v_value).". String expected for attribute '".PclZipUtilOptionText($v_key)."'");
            return PclZip::errorCode();
          }

          $p_filedescr['new_full_name'] = PclZipUtilPathReduction($v_value);

          if ($p_filedescr['new_full_name'] == '') {
            PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid empty full filename for attribute '".PclZipUtilOptionText($v_key)."'");
            return PclZip::errorCode();
          }
        break;

        // ----- Look for options that takes a string
        case PCLZIP_ATT_FILE_COMMENT :
          if (!is_string($v_value)) {
            PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid type ".gettype($v_value).". String expected for attribute '".PclZipUtilOptionText($v_key)."'");
            return PclZip::errorCode();
          }

          $p_filedescr['comment'] = $v_value;
        break;

        case PCLZIP_ATT_FILE_MTIME :
          if (!is_integer($v_value)) {
            PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid type ".gettype($v_value).". Integer expected for attribute '".PclZipUtilOptionText($v_key)."'");
            return PclZip::errorCode();
          }

          $p_filedescr['mtime'] = $v_value;
        break;

        case PCLZIP_ATT_FILE_CONTENT :
          $p_filedescr['content'] = $v_value;
        break;

        default :
          // ----- Error log
          PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER,
		                           "Unknown parameter '".$v_key."'");

          // ----- Return
          return PclZip::errorCode();
      }

      // ----- Look for mandatory options
      if ($v_requested_options !== false) {
        for ($key=reset($v_requested_options); $key=key($v_requested_options); $key=next($v_requested_options)) {
          // ----- Look for mandatory option
          if ($v_requested_options[$key] == 'mandatory') {
            // ----- Look if present
            if (!isset($p_file_list[$key])) {
              PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Missing mandatory parameter ".PclZipUtilOptionText($key)."(".$key.")");
              return PclZip::errorCode();
            }
          }
        }
      }

    // end foreach
    }

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privFileDescrExpand()
  // Description :
  //   This method look for each item of the list to see if its a file, a folder
  //   or a string to be added as file. For any other type of files (link, other)
  //   just ignore the item.
  //   Then prepare the information that will be stored for that file.
  //   When its a folder, expand the folder with all the files that are in that
  //   folder (recursively).
  // Parameters :
  // Return Values :
  //   1 on success.
  //   0 on failure.
  // --------------------------------------------------------------------------------
  function privFileDescrExpand(&$p_filedescr_list, &$p_options)
  {
    $v_result=1;

    // ----- Create a result list
    $v_result_list = array();

    // ----- Look each entry
    for ($i=0; $i<sizeof($p_filedescr_list); $i++) {

      // ----- Get filedescr
      $v_descr = $p_filedescr_list[$i];

      // ----- Reduce the filename
      $v_descr['filename'] = PclZipUtilTranslateWinPath($v_descr['filename'], false);
      $v_descr['filename'] = PclZipUtilPathReduction($v_descr['filename']);

      // ----- Look for real file or folder
      if (file_exists($v_descr['filename'])) {
        if (@is_file($v_descr['filename'])) {
          $v_descr['type'] = 'file';
        }
        else if (@is_dir($v_descr['filename'])) {
          $v_descr['type'] = 'folder';
        }
        else if (@is_link($v_descr['filename'])) {
          // skip
          continue;
        }
        else {
          // skip
          continue;
        }
      }

      // ----- Look for string added as file
      else if (isset($v_descr['content'])) {
        $v_descr['type'] = 'virtual_file';
      }

      // ----- Missing file
      else {
        // ----- Error log
        PclZip::privErrorLog(PCLZIP_ERR_MISSING_FILE, "File '".$v_descr['filename']."' does not exist");

        // ----- Return
        return PclZip::errorCode();
      }

      // ----- Calculate the stored filename
      $this->privCalculateStoredFilename($v_descr, $p_options);

      // ----- Add the descriptor in result list
      $v_result_list[sizeof($v_result_list)] = $v_descr;

      // ----- Look for folder
      if ($v_descr['type'] == 'folder') {
        // ----- List of items in folder
        $v_dirlist_descr = array();
        $v_dirlist_nb = 0;
        if ($v_folder_handler = @opendir($v_descr['filename'])) {
          while (($v_item_handler = @readdir($v_folder_handler)) !== false) {

            // ----- Skip '.' and '..'
            if (($v_item_handler == '.') || ($v_item_handler == '..')) {
                continue;
            }

            // ----- Compose the full filename
            $v_dirlist_descr[$v_dirlist_nb]['filename'] = $v_descr['filename'].'/'.$v_item_handler;

            // ----- Look for different stored filename
            // Because the name of the folder was changed, the name of the
            // files/sub-folders also change
            if (($v_descr['stored_filename'] != $v_descr['filename'])
                 && (!isset($p_options[PCLZIP_OPT_REMOVE_ALL_PATH]))) {
              if ($v_descr['stored_filename'] != '') {
                $v_dirlist_descr[$v_dirlist_nb]['new_full_name'] = $v_descr['stored_filename'].'/'.$v_item_handler;
              }
              else {
                $v_dirlist_descr[$v_dirlist_nb]['new_full_name'] = $v_item_handler;
              }
            }

            $v_dirlist_nb++;
          }

          @closedir($v_folder_handler);
        }
        else {
          // TBC : unable to open folder in read mode
        }

        // ----- Expand each element of the list
        if ($v_dirlist_nb != 0) {
          // ----- Expand
          if (($v_result = $this->privFileDescrExpand($v_dirlist_descr, $p_options)) != 1) {
            return $v_result;
          }

          // ----- Concat the resulting list
          $v_result_list = array_merge($v_result_list, $v_dirlist_descr);
        }
        else {
        }

        // ----- Free local array
        unset($v_dirlist_descr);
      }
    }

    // ----- Get the result list
    $p_filedescr_list = $v_result_list;

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privCreate()
  // Description :
  // Parameters :
  // Return Values :
  // --------------------------------------------------------------------------------
  function privCreate($p_filedescr_list, &$p_result_list, &$p_options)
  {
    $v_result=1;
    $v_list_detail = array();

    // ----- Magic quotes trick
    $this->privDisableMagicQuotes();

    // ----- Open the file in write mode
    if (($v_result = $this->privOpenFd('wb')) != 1)
    {
      // ----- Return
      return $v_result;
    }

    // ----- Add the list of files
    $v_result = $this->privAddList($p_filedescr_list, $p_result_list, $p_options);

    // ----- Close
    $this->privCloseFd();

    // ----- Magic quotes trick
    $this->privSwapBackMagicQuotes();

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privAdd()
  // Description :
  // Parameters :
  // Return Values :
  // --------------------------------------------------------------------------------
  function privAdd($p_filedescr_list, &$p_result_list, &$p_options)
  {
    $v_result=1;
    $v_list_detail = array();

    // ----- Look if the archive exists or is empty
    if ((!is_file($this->zipname)) || (filesize($this->zipname) == 0))
    {

      // ----- Do a create
      $v_result = $this->privCreate($p_filedescr_list, $p_result_list, $p_options);

      // ----- Return
      return $v_result;
    }
    // ----- Magic quotes trick
    $this->privDisableMagicQuotes();

    // ----- Open the zip file
    if (($v_result=$this->privOpenFd('rb')) != 1)
    {
      // ----- Magic quotes trick
      $this->privSwapBackMagicQuotes();

      // ----- Return
      return $v_result;
    }

    // ----- Read the central directory information
    $v_central_dir = array();
    if (($v_result = $this->privReadEndCentralDir($v_central_dir)) != 1)
    {
      $this->privCloseFd();
      $this->privSwapBackMagicQuotes();
      return $v_result;
    }

    // ----- Go to beginning of File
    @rewind($this->zip_fd);

    // ----- Creates a temporary file
    $v_zip_temp_name = PCLZIP_TEMPORARY_DIR.uniqid('pclzip-').'.tmp';

    // ----- Open the temporary file in write mode
    if (($v_zip_temp_fd = @fopen($v_zip_temp_name, 'wb')) == 0)
    {
      $this->privCloseFd();
      $this->privSwapBackMagicQuotes();

      PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open temporary file \''.$v_zip_temp_name.'\' in binary write mode');

      // ----- Return
      return PclZip::errorCode();
    }

    // ----- Copy the files from the archive to the temporary file
    // TBC : Here I should better append the file and go back to erase the central dir
    $v_size = $v_central_dir['offset'];
    while ($v_size != 0)
    {
      $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
      $v_buffer = fread($this->zip_fd, $v_read_size);
      @fwrite($v_zip_temp_fd, $v_buffer, $v_read_size);
      $v_size -= $v_read_size;
    }

    // ----- Swap the file descriptor
    // Here is a trick : I swap the temporary fd with the zip fd, in order to use
    // the following methods on the temporary fil and not the real archive
    $v_swap = $this->zip_fd;
    $this->zip_fd = $v_zip_temp_fd;
    $v_zip_temp_fd = $v_swap;

    // ----- Add the files
    $v_header_list = array();
    if (($v_result = $this->privAddFileList($p_filedescr_list, $v_header_list, $p_options)) != 1)
    {
      fclose($v_zip_temp_fd);
      $this->privCloseFd();
      @unlink($v_zip_temp_name);
      $this->privSwapBackMagicQuotes();

      // ----- Return
      return $v_result;
    }

    // ----- Store the offset of the central dir
    $v_offset = @ftell($this->zip_fd);

    // ----- Copy the block of file headers from the old archive
    $v_size = $v_central_dir['size'];
    while ($v_size != 0)
    {
      $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
      $v_buffer = @fread($v_zip_temp_fd, $v_read_size);
      @fwrite($this->zip_fd, $v_buffer, $v_read_size);
      $v_size -= $v_read_size;
    }

    // ----- Create the Central Dir files header
    for ($i=0, $v_count=0; $i<sizeof($v_header_list); $i++)
    {
      // ----- Create the file header
      if ($v_header_list[$i]['status'] == 'ok') {
        if (($v_result = $this->privWriteCentralFileHeader($v_header_list[$i])) != 1) {
          fclose($v_zip_temp_fd);
          $this->privCloseFd();
          @unlink($v_zip_temp_name);
          $this->privSwapBackMagicQuotes();

          // ----- Return
          return $v_result;
        }
        $v_count++;
      }

      // ----- Transform the header to a 'usable' info
      $this->privConvertHeader2FileInfo($v_header_list[$i], $p_result_list[$i]);
    }

    // ----- Zip file comment
    $v_comment = $v_central_dir['comment'];
    if (isset($p_options[PCLZIP_OPT_COMMENT])) {
      $v_comment = $p_options[PCLZIP_OPT_COMMENT];
    }
    if (isset($p_options[PCLZIP_OPT_ADD_COMMENT])) {
      $v_comment = $v_comment.$p_options[PCLZIP_OPT_ADD_COMMENT];
    }
    if (isset($p_options[PCLZIP_OPT_PREPEND_COMMENT])) {
      $v_comment = $p_options[PCLZIP_OPT_PREPEND_COMMENT].$v_comment;
    }

    // ----- Calculate the size of the central header
    $v_size = @ftell($this->zip_fd)-$v_offset;

    // ----- Create the central dir footer
    if (($v_result = $this->privWriteCentralHeader($v_count+$v_central_dir['entries'], $v_size, $v_offset, $v_comment)) != 1)
    {
      // ----- Reset the file list
      unset($v_header_list);
      $this->privSwapBackMagicQuotes();

      // ----- Return
      return $v_result;
    }

    // ----- Swap back the file descriptor
    $v_swap = $this->zip_fd;
    $this->zip_fd = $v_zip_temp_fd;
    $v_zip_temp_fd = $v_swap;

    // ----- Close
    $this->privCloseFd();

    // ----- Close the temporary file
    @fclose($v_zip_temp_fd);

    // ----- Magic quotes trick
    $this->privSwapBackMagicQuotes();

    // ----- Delete the zip file
    // TBC : I should test the result ...
    @unlink($this->zipname);

    // ----- Rename the temporary file
    // TBC : I should test the result ...
    //@rename($v_zip_temp_name, $this->zipname);
    PclZipUtilRename($v_zip_temp_name, $this->zipname);

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privOpenFd()
  // Description :
  // Parameters :
  // --------------------------------------------------------------------------------
  function privOpenFd($p_mode)
  {
    $v_result=1;

    // ----- Look if already open
    if ($this->zip_fd != 0)
    {
      // ----- Error log
      PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Zip file \''.$this->zipname.'\' already open');

      // ----- Return
      return PclZip::errorCode();
    }

    // ----- Open the zip file
    if (($this->zip_fd = @fopen($this->zipname, $p_mode)) == 0)
    {
      // ----- Error log
      PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open archive \''.$this->zipname.'\' in '.$p_mode.' mode');

      // ----- Return
      return PclZip::errorCode();
    }

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privCloseFd()
  // Description :
  // Parameters :
  // --------------------------------------------------------------------------------
  function privCloseFd()
  {
    $v_result=1;

    if ($this->zip_fd != 0)
      @fclose($this->zip_fd);
    $this->zip_fd = 0;

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privAddList()
  // Description :
  //   $p_add_dir and $p_remove_dir will give the ability to memorize a path which is
  //   different from the real path of the file. This is useful if you want to have PclTar
  //   running in any directory, and memorize relative path from an other directory.
  // Parameters :
  //   $p_list : An array containing the file or directory names to add in the tar
  //   $p_result_list : list of added files with their properties (specially the status field)
  //   $p_add_dir : Path to add in the filename path archived
  //   $p_remove_dir : Path to remove in the filename path archived
  // Return Values :
  // --------------------------------------------------------------------------------
//  function privAddList($p_list, &$p_result_list, $p_add_dir, $p_remove_dir, $p_remove_all_dir, &$p_options)
  function privAddList($p_filedescr_list, &$p_result_list, &$p_options)
  {
    $v_result=1;

    // ----- Add the files
    $v_header_list = array();
    if (($v_result = $this->privAddFileList($p_filedescr_list, $v_header_list, $p_options)) != 1)
    {
      // ----- Return
      return $v_result;
    }

    // ----- Store the offset of the central dir
    $v_offset = @ftell($this->zip_fd);

    // ----- Create the Central Dir files header
    for ($i=0,$v_count=0; $i<sizeof($v_header_list); $i++)
    {
      // ----- Create the file header
      if ($v_header_list[$i]['status'] == 'ok') {
        if (($v_result = $this->privWriteCentralFileHeader($v_header_list[$i])) != 1) {
          // ----- Return
          return $v_result;
        }
        $v_count++;
      }

      // ----- Transform the header to a 'usable' info
      $this->privConvertHeader2FileInfo($v_header_list[$i], $p_result_list[$i]);
    }

    // ----- Zip file comment
    $v_comment = '';
    if (isset($p_options[PCLZIP_OPT_COMMENT])) {
      $v_comment = $p_options[PCLZIP_OPT_COMMENT];
    }

    // ----- Calculate the size of the central header
    $v_size = @ftell($this->zip_fd)-$v_offset;

    // ----- Create the central dir footer
    if (($v_result = $this->privWriteCentralHeader($v_count, $v_size, $v_offset, $v_comment)) != 1)
    {
      // ----- Reset the file list
      unset($v_header_list);

      // ----- Return
      return $v_result;
    }

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privAddFileList()
  // Description :
  // Parameters :
  //   $p_filedescr_list : An array containing the file description
  //                      or directory names to add in the zip
  //   $p_result_list : list of added files with their properties (specially the status field)
  // Return Values :
  // --------------------------------------------------------------------------------
  function privAddFileList($p_filedescr_list, &$p_result_list, &$p_options)
  {
    $v_result=1;
    $v_header = array();

    // ----- Recuperate the current number of elt in list
    $v_nb = sizeof($p_result_list);

    // ----- Loop on the files
    for ($j=0; ($j<sizeof($p_filedescr_list)) && ($v_result==1); $j++) {
      // ----- Format the filename
      $p_filedescr_list[$j]['filename']
      = PclZipUtilTranslateWinPath($p_filedescr_list[$j]['filename'], false);


      // ----- Skip empty file names
      // TBC : Can this be possible ? not checked in DescrParseAtt ?
      if ($p_filedescr_list[$j]['filename'] == "") {
        continue;
      }

      // ----- Check the filename
      if (   ($p_filedescr_list[$j]['type'] != 'virtual_file')
          && (!file_exists($p_filedescr_list[$j]['filename']))) {
        PclZip::privErrorLog(PCLZIP_ERR_MISSING_FILE, "File '".$p_filedescr_list[$j]['filename']."' does not exist");
        return PclZip::errorCode();
      }

      // ----- Look if it is a file or a dir with no all path remove option
      // or a dir with all its path removed
//      if (   (is_file($p_filedescr_list[$j]['filename']))
//          || (   is_dir($p_filedescr_list[$j]['filename'])
      if (   ($p_filedescr_list[$j]['type'] == 'file')
          || ($p_filedescr_list[$j]['type'] == 'virtual_file')
          || (   ($p_filedescr_list[$j]['type'] == 'folder')
              && (   !isset($p_options[PCLZIP_OPT_REMOVE_ALL_PATH])
                  || !$p_options[PCLZIP_OPT_REMOVE_ALL_PATH]))
          ) {

        // ----- Add the file
        $v_result = $this->privAddFile($p_filedescr_list[$j], $v_header,
                                       $p_options);
        if ($v_result != 1) {
          return $v_result;
        }

        // ----- Store the file infos
        $p_result_list[$v_nb++] = $v_header;
      }
    }

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privAddFile()
  // Description :
  // Parameters :
  // Return Values :
  // --------------------------------------------------------------------------------
  function privAddFile($p_filedescr, &$p_header, &$p_options)
  {
    $v_result=1;

    // ----- Working variable
    $p_filename = $p_filedescr['filename'];

    // TBC : Already done in the fileAtt check ... ?
    if ($p_filename == "") {
      // ----- Error log
      PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid file list parameter (invalid or empty list)");

      // ----- Return
      return PclZip::errorCode();
    }

    // ----- Look for a stored different filename
    /* TBC : Removed
    if (isset($p_filedescr['stored_filename'])) {
      $v_stored_filename = $p_filedescr['stored_filename'];
    }
    else {
      $v_stored_filename = $p_filedescr['stored_filename'];
    }
    */

    // ----- Set the file properties
    clearstatcache();
    $p_header['version'] = 20;
    $p_header['version_extracted'] = 10;
    $p_header['flag'] = 0;
    $p_header['compression'] = 0;
    $p_header['crc'] = 0;
    $p_header['compressed_size'] = 0;
    $p_header['filename_len'] = strlen($p_filename);
    $p_header['extra_len'] = 0;
    $p_header['disk'] = 0;
    $p_header['internal'] = 0;
    $p_header['offset'] = 0;
    $p_header['filename'] = $p_filename;
// TBC : Removed    $p_header['stored_filename'] = $v_stored_filename;
    $p_header['stored_filename'] = $p_filedescr['stored_filename'];
    $p_header['extra'] = '';
    $p_header['status'] = 'ok';
    $p_header['index'] = -1;

    // ----- Look for regular file
    if ($p_filedescr['type']=='file') {
      $p_header['external'] = 0x00000000;
      $p_header['size'] = filesize($p_filename);
    }

    // ----- Look for regular folder
    else if ($p_filedescr['type']=='folder') {
      $p_header['external'] = 0x00000010;
      $p_header['mtime'] = filemtime($p_filename);
      $p_header['size'] = filesize($p_filename);
    }

    // ----- Look for virtual file
    else if ($p_filedescr['type'] == 'virtual_file') {
      $p_header['external'] = 0x00000000;
      $p_header['size'] = strlen($p_filedescr['content']);
    }


    // ----- Look for filetime
    if (isset($p_filedescr['mtime'])) {
      $p_header['mtime'] = $p_filedescr['mtime'];
    }
    else if ($p_filedescr['type'] == 'virtual_file') {
      $p_header['mtime'] = time();
    }
    else {
      $p_header['mtime'] = filemtime($p_filename);
    }

    // ------ Look for file comment
    if (isset($p_filedescr['comment'])) {
      $p_header['comment_len'] = strlen($p_filedescr['comment']);
      $p_header['comment'] = $p_filedescr['comment'];
    }
    else {
      $p_header['comment_len'] = 0;
      $p_header['comment'] = '';
    }

    // ----- Look for pre-add callback
    if (isset($p_options[PCLZIP_CB_PRE_ADD])) {

      // ----- Generate a local information
      $v_local_header = array();
      $this->privConvertHeader2FileInfo($p_header, $v_local_header);

      // ----- Call the callback
      // Here I do not use call_user_func() because I need to send a reference to the
      // header.
      $v_result = $p_options[PCLZIP_CB_PRE_ADD](PCLZIP_CB_PRE_ADD, $v_local_header);
      if ($v_result == 0) {
        // ----- Change the file status
        $p_header['status'] = "skipped";
        $v_result = 1;
      }

      // ----- Update the information
      // Only some fields can be modified
      if ($p_header['stored_filename'] != $v_local_header['stored_filename']) {
        $p_header['stored_filename'] = PclZipUtilPathReduction($v_local_header['stored_filename']);
      }
    }

    // ----- Look for empty stored filename
    if ($p_header['stored_filename'] == "") {
      $p_header['status'] = "filtered";
    }

    // ----- Check the path length
    if (strlen($p_header['stored_filename']) > 0xFF) {
      $p_header['status'] = 'filename_too_long';
    }

    // ----- Look if no error, or file not skipped
    if ($p_header['status'] == 'ok') {

      // ----- Look for a file
      if ($p_filedescr['type'] == 'file') {
        // ----- Look for using temporary file to zip
        if ( (!isset($p_options[PCLZIP_OPT_TEMP_FILE_OFF]))
            && (isset($p_options[PCLZIP_OPT_TEMP_FILE_ON])
                || (isset($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD])
                    && ($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD] <= $p_header['size'])) ) ) {
          $v_result = $this->privAddFileUsingTempFile($p_filedescr, $p_header, $p_options);
          if ($v_result < PCLZIP_ERR_NO_ERROR) {
            return $v_result;
          }
        }

        // ----- Use "in memory" zip algo
        else {

        // ----- Open the source file
        if (($v_file = @fopen($p_filename, "rb")) == 0) {
          PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, "Unable to open file '$p_filename' in binary read mode");
          return PclZip::errorCode();
        }

        // ----- Read the file content
        if ($p_header['size'] > 0) {
          $v_content = @fread($v_file, $p_header['size']);
        }
        else {
          $v_content = '';
        }

        // ----- Close the file
        @fclose($v_file);

        // ----- Calculate the CRC
        $p_header['crc'] = @crc32($v_content);

        // ----- Look for no compression
        if ($p_options[PCLZIP_OPT_NO_COMPRESSION]) {
          // ----- Set header parameters
          $p_header['compressed_size'] = $p_header['size'];
          $p_header['compression'] = 0;
        }

        // ----- Look for normal compression
        else {
          // ----- Compress the content
          $v_content = @gzdeflate($v_content);

          // ----- Set header parameters
          $p_header['compressed_size'] = strlen($v_content);
          $p_header['compression'] = 8;
        }

        // ----- Call the header generation
        if (($v_result = $this->privWriteFileHeader($p_header)) != 1) {
          @fclose($v_file);
          return $v_result;
        }

        // ----- Write the compressed (or not) content
        @fwrite($this->zip_fd, $v_content, $p_header['compressed_size']);

        }

      }

      // ----- Look for a virtual file (a file from string)
      else if ($p_filedescr['type'] == 'virtual_file') {

        $v_content = $p_filedescr['content'];

        // ----- Calculate the CRC
        $p_header['crc'] = @crc32($v_content);

        // ----- Look for no compression
        if ($p_options[PCLZIP_OPT_NO_COMPRESSION]) {
          // ----- Set header parameters
          $p_header['compressed_size'] = $p_header['size'];
          $p_header['compression'] = 0;
        }

        // ----- Look for normal compression
        else {
          // ----- Compress the content
          $v_content = @gzdeflate($v_content);

          // ----- Set header parameters
          $p_header['compressed_size'] = strlen($v_content);
          $p_header['compression'] = 8;
        }

        // ----- Call the header generation
        if (($v_result = $this->privWriteFileHeader($p_header)) != 1) {
          @fclose($v_file);
          return $v_result;
        }

        // ----- Write the compressed (or not) content
        @fwrite($this->zip_fd, $v_content, $p_header['compressed_size']);
      }

      // ----- Look for a directory
      else if ($p_filedescr['type'] == 'folder') {
        // ----- Look for directory last '/'
        if (@substr($p_header['stored_filename'], -1) != '/') {
          $p_header['stored_filename'] .= '/';
        }

        // ----- Set the file properties
        $p_header['size'] = 0;
        //$p_header['external'] = 0x41FF0010;   // Value for a folder : to be checked
        $p_header['external'] = 0x00000010;   // Value for a folder : to be checked

        // ----- Call the header generation
        if (($v_result = $this->privWriteFileHeader($p_header)) != 1)
        {
          return $v_result;
        }
      }
    }

    // ----- Look for post-add callback
    if (isset($p_options[PCLZIP_CB_POST_ADD])) {

      // ----- Generate a local information
      $v_local_header = array();
      $this->privConvertHeader2FileInfo($p_header, $v_local_header);

      // ----- Call the callback
      // Here I do not use call_user_func() because I need to send a reference to the
      // header.
      $v_result = $p_options[PCLZIP_CB_POST_ADD](PCLZIP_CB_POST_ADD, $v_local_header);
      if ($v_result == 0) {
        // ----- Ignored
        $v_result = 1;
      }

      // ----- Update the information
      // Nothing can be modified
    }

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privAddFileUsingTempFile()
  // Description :
  // Parameters :
  // Return Values :
  // --------------------------------------------------------------------------------
  function privAddFileUsingTempFile($p_filedescr, &$p_header, &$p_options)
  {
    $v_result=PCLZIP_ERR_NO_ERROR;

    // ----- Working variable
    $p_filename = $p_filedescr['filename'];


    // ----- Open the source file
    if (($v_file = @fopen($p_filename, "rb")) == 0) {
      PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, "Unable to open file '$p_filename' in binary read mode");
      return PclZip::errorCode();
    }

    // ----- Creates a compressed temporary file
    $v_gzip_temp_name = PCLZIP_TEMPORARY_DIR.uniqid('pclzip-').'.gz';
    if (($v_file_compressed = @gzopen($v_gzip_temp_name, "wb")) == 0) {
      fclose($v_file);
      PclZip::privErrorLog(PCLZIP_ERR_WRITE_OPEN_FAIL, 'Unable to open temporary file \''.$v_gzip_temp_name.'\' in binary write mode');
      return PclZip::errorCode();
    }

    // ----- Read the file by PCLZIP_READ_BLOCK_SIZE octets blocks
    $v_size = filesize($p_filename);
    while ($v_size != 0) {
      $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
      $v_buffer = @fread($v_file, $v_read_size);
      //$v_binary_data = pack('a'.$v_read_size, $v_buffer);
      @gzputs($v_file_compressed, $v_buffer, $v_read_size);
      $v_size -= $v_read_size;
    }

    // ----- Close the file
    @fclose($v_file);
    @gzclose($v_file_compressed);

    // ----- Check the minimum file size
    if (filesize($v_gzip_temp_name) < 18) {
      PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'gzip temporary file \''.$v_gzip_temp_name.'\' has invalid filesize - should be minimum 18 bytes');
      return PclZip::errorCode();
    }

    // ----- Extract the compressed attributes
    if (($v_file_compressed = @fopen($v_gzip_temp_name, "rb")) == 0) {
      PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open temporary file \''.$v_gzip_temp_name.'\' in binary read mode');
      return PclZip::errorCode();
    }

    // ----- Read the gzip file header
    $v_binary_data = @fread($v_file_compressed, 10);
    $v_data_header = unpack('a1id1/a1id2/a1cm/a1flag/Vmtime/a1xfl/a1os', $v_binary_data);

    // ----- Check some parameters
    $v_data_header['os'] = bin2hex($v_data_header['os']);

    // ----- Read the gzip file footer
    @fseek($v_file_compressed, filesize($v_gzip_temp_name)-8);
    $v_binary_data = @fread($v_file_compressed, 8);
    $v_data_footer = unpack('Vcrc/Vcompressed_size', $v_binary_data);

    // ----- Set the attributes
    $p_header['compression'] = ord($v_data_header['cm']);
    //$p_header['mtime'] = $v_data_header['mtime'];
    $p_header['crc'] = $v_data_footer['crc'];
    $p_header['compressed_size'] = filesize($v_gzip_temp_name)-18;

    // ----- Close the file
    @fclose($v_file_compressed);

    // ----- Call the header generation
    if (($v_result = $this->privWriteFileHeader($p_header)) != 1) {
      return $v_result;
    }

    // ----- Add the compressed data
    if (($v_file_compressed = @fopen($v_gzip_temp_name, "rb")) == 0)
    {
      PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open temporary file \''.$v_gzip_temp_name.'\' in binary read mode');
      return PclZip::errorCode();
    }

    // ----- Read the file by PCLZIP_READ_BLOCK_SIZE octets blocks
    fseek($v_file_compressed, 10);
    $v_size = $p_header['compressed_size'];
    while ($v_size != 0)
    {
      $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
      $v_buffer = @fread($v_file_compressed, $v_read_size);
      //$v_binary_data = pack('a'.$v_read_size, $v_buffer);
      @fwrite($this->zip_fd, $v_buffer, $v_read_size);
      $v_size -= $v_read_size;
    }

    // ----- Close the file
    @fclose($v_file_compressed);

    // ----- Unlink the temporary file
    @unlink($v_gzip_temp_name);

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privCalculateStoredFilename()
  // Description :
  //   Based on file descriptor properties and global options, this method
  //   calculate the filename that will be stored in the archive.
  // Parameters :
  // Return Values :
  // --------------------------------------------------------------------------------
  function privCalculateStoredFilename(&$p_filedescr, &$p_options)
  {
    $v_result=1;

    // ----- Working variables
    $p_filename = $p_filedescr['filename'];
    if (isset($p_options[PCLZIP_OPT_ADD_PATH])) {
      $p_add_dir = $p_options[PCLZIP_OPT_ADD_PATH];
    }
    else {
      $p_add_dir = '';
    }
    if (isset($p_options[PCLZIP_OPT_REMOVE_PATH])) {
      $p_remove_dir = $p_options[PCLZIP_OPT_REMOVE_PATH];
    }
    else {
      $p_remove_dir = '';
    }
    if (isset($p_options[PCLZIP_OPT_REMOVE_ALL_PATH])) {
      $p_remove_all_dir = $p_options[PCLZIP_OPT_REMOVE_ALL_PATH];
    }
    else {
      $p_remove_all_dir = 0;
    }


    // ----- Look for full name change
    if (isset($p_filedescr['new_full_name'])) {
      // ----- Remove drive letter if any
      $v_stored_filename = PclZipUtilTranslateWinPath($p_filedescr['new_full_name']);
    }

    // ----- Look for path and/or short name change
    else {

      // ----- Look for short name change
      // Its when we change just the filename but not the path
      if (isset($p_filedescr['new_short_name'])) {
        $v_path_info = pathinfo($p_filename);
        $v_dir = '';
        if ($v_path_info['dirname'] != '') {
          $v_dir = $v_path_info['dirname'].'/';
        }
        $v_stored_filename = $v_dir.$p_filedescr['new_short_name'];
      }
      else {
        // ----- Calculate the stored filename
        $v_stored_filename = $p_filename;
      }

      // ----- Look for all path to remove
      if ($p_remove_all_dir) {
        $v_stored_filename = basename($p_filename);
      }
      // ----- Look for partial path remove
      else if ($p_remove_dir != "") {
        if (substr($p_remove_dir, -1) != '/')
          $p_remove_dir .= "/";

        if (   (substr($p_filename, 0, 2) == "./")
            || (substr($p_remove_dir, 0, 2) == "./")) {

          if (   (substr($p_filename, 0, 2) == "./")
              && (substr($p_remove_dir, 0, 2) != "./")) {
            $p_remove_dir = "./".$p_remove_dir;
          }
          if (   (substr($p_filename, 0, 2) != "./")
              && (substr($p_remove_dir, 0, 2) == "./")) {
            $p_remove_dir = substr($p_remove_dir, 2);
          }
        }

        $v_compare = PclZipUtilPathInclusion($p_remove_dir,
                                             $v_stored_filename);
        if ($v_compare > 0) {
          if ($v_compare == 2) {
            $v_stored_filename = "";
          }
          else {
            $v_stored_filename = substr($v_stored_filename,
                                        strlen($p_remove_dir));
          }
        }
      }

      // ----- Remove drive letter if any
      $v_stored_filename = PclZipUtilTranslateWinPath($v_stored_filename);

      // ----- Look for path to add
      if ($p_add_dir != "") {
        if (substr($p_add_dir, -1) == "/")
          $v_stored_filename = $p_add_dir.$v_stored_filename;
        else
          $v_stored_filename = $p_add_dir."/".$v_stored_filename;
      }
    }

    // ----- Filename (reduce the path of stored name)
    $v_stored_filename = PclZipUtilPathReduction($v_stored_filename);
    $p_filedescr['stored_filename'] = $v_stored_filename;

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privWriteFileHeader()
  // Description :
  // Parameters :
  // Return Values :
  // --------------------------------------------------------------------------------
  function privWriteFileHeader(&$p_header)
  {
    $v_result=1;

    // ----- Store the offset position of the file
    $p_header['offset'] = ftell($this->zip_fd);

    // ----- Transform UNIX mtime to DOS format mdate/mtime
    $v_date = getdate($p_header['mtime']);
    $v_mtime = ($v_date['hours']<<11) + ($v_date['minutes']<<5) + $v_date['seconds']/2;
    $v_mdate = (($v_date['year']-1980)<<9) + ($v_date['mon']<<5) + $v_date['mday'];

    // ----- Packed data
    $v_binary_data = pack("VvvvvvVVVvv", 0x04034b50,
	                      $p_header['version_extracted'], $p_header['flag'],
                          $p_header['compression'], $v_mtime, $v_mdate,
                          $p_header['crc'], $p_header['compressed_size'],
						  $p_header['size'],
                          strlen($p_header['stored_filename']),
						  $p_header['extra_len']);

    // ----- Write the first 148 bytes of the header in the archive
    fputs($this->zip_fd, $v_binary_data, 30);

    // ----- Write the variable fields
    if (strlen($p_header['stored_filename']) != 0)
    {
      fputs($this->zip_fd, $p_header['stored_filename'], strlen($p_header['stored_filename']));
    }
    if ($p_header['extra_len'] != 0)
    {
      fputs($this->zip_fd, $p_header['extra'], $p_header['extra_len']);
    }

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privWriteCentralFileHeader()
  // Description :
  // Parameters :
  // Return Values :
  // --------------------------------------------------------------------------------
  function privWriteCentralFileHeader(&$p_header)
  {
    $v_result=1;

    // TBC
    //for(reset($p_header); $key = key($p_header); next($p_header)) {
    //}

    // ----- Transform UNIX mtime to DOS format mdate/mtime
    $v_date = getdate($p_header['mtime']);
    $v_mtime = ($v_date['hours']<<11) + ($v_date['minutes']<<5) + $v_date['seconds']/2;
    $v_mdate = (($v_date['year']-1980)<<9) + ($v_date['mon']<<5) + $v_date['mday'];


    // ----- Packed data
    $v_binary_data = pack("VvvvvvvVVVvvvvvVV", 0x02014b50,
	                      $p_header['version'], $p_header['version_extracted'],
                          $p_header['flag'], $p_header['compression'],
						  $v_mtime, $v_mdate, $p_header['crc'],
                          $p_header['compressed_size'], $p_header['size'],
                          strlen($p_header['stored_filename']),
						  $p_header['extra_len'], $p_header['comment_len'],
                          $p_header['disk'], $p_header['internal'],
						  $p_header['external'], $p_header['offset']);

    // ----- Write the 42 bytes of the header in the zip file
    fputs($this->zip_fd, $v_binary_data, 46);

    // ----- Write the variable fields
    if (strlen($p_header['stored_filename']) != 0)
    {
      fputs($this->zip_fd, $p_header['stored_filename'], strlen($p_header['stored_filename']));
    }
    if ($p_header['extra_len'] != 0)
    {
      fputs($this->zip_fd, $p_header['extra'], $p_header['extra_len']);
    }
    if ($p_header['comment_len'] != 0)
    {
      fputs($this->zip_fd, $p_header['comment'], $p_header['comment_len']);
    }

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privWriteCentralHeader()
  // Description :
  // Parameters :
  // Return Values :
  // --------------------------------------------------------------------------------
  function privWriteCentralHeader($p_nb_entries, $p_size, $p_offset, $p_comment)
  {
    $v_result=1;

    // ----- Packed data
    $v_binary_data = pack("VvvvvVVv", 0x06054b50, 0, 0, $p_nb_entries,
	                      $p_nb_entries, $p_size,
						  $p_offset, strlen($p_comment));

    // ----- Write the 22 bytes of the header in the zip file
    fputs($this->zip_fd, $v_binary_data, 22);

    // ----- Write the variable fields
    if (strlen($p_comment) != 0)
    {
      fputs($this->zip_fd, $p_comment, strlen($p_comment));
    }

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privList()
  // Description :
  // Parameters :
  // Return Values :
  // --------------------------------------------------------------------------------
  function privList(&$p_list)
  {
    $v_result=1;

    // ----- Magic quotes trick
    $this->privDisableMagicQuotes();

    // ----- Open the zip file
    if (($this->zip_fd = @fopen($this->zipname, 'rb')) == 0)
    {
      // ----- Magic quotes trick
      $this->privSwapBackMagicQuotes();

      // ----- Error log
      PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open archive \''.$this->zipname.'\' in binary read mode');

      // ----- Return
      return PclZip::errorCode();
    }

    // ----- Read the central directory information
    $v_central_dir = array();
    if (($v_result = $this->privReadEndCentralDir($v_central_dir)) != 1)
    {
      $this->privSwapBackMagicQuotes();
      return $v_result;
    }

    // ----- Go to beginning of Central Dir
    @rewind($this->zip_fd);
    if (@fseek($this->zip_fd, $v_central_dir['offset']))
    {
      $this->privSwapBackMagicQuotes();

      // ----- Error log
      PclZip::privErrorLog(PCLZIP_ERR_INVALID_ARCHIVE_ZIP, 'Invalid archive size');

      // ----- Return
      return PclZip::errorCode();
    }

    // ----- Read each entry
    for ($i=0; $i<$v_central_dir['entries']; $i++)
    {
      // ----- Read the file header
      if (($v_result = $this->privReadCentralFileHeader($v_header)) != 1)
      {
        $this->privSwapBackMagicQuotes();
        return $v_result;
      }
      $v_header['index'] = $i;

      // ----- Get the only interesting attributes
      $this->privConvertHeader2FileInfo($v_header, $p_list[$i]);
      unset($v_header);
    }

    // ----- Close the zip file
    $this->privCloseFd();

    // ----- Magic quotes trick
    $this->privSwapBackMagicQuotes();

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privConvertHeader2FileInfo()
  // Description :
  //   This function takes the file information from the central directory
  //   entries and extract the interesting parameters that will be given back.
  //   The resulting file infos are set in the array $p_info
  //     $p_info['filename'] : Filename with full path. Given by user (add),
  //                           extracted in the filesystem (extract).
  //     $p_info['stored_filename'] : Stored filename in the archive.
  //     $p_info['size'] = Size of the file.
  //     $p_info['compressed_size'] = Compressed size of the file.
  //     $p_info['mtime'] = Last modification date of the file.
  //     $p_info['comment'] = Comment associated with the file.
  //     $p_info['folder'] = true/false : indicates if the entry is a folder or not.
  //     $p_info['status'] = status of the action on the file.
  //     $p_info['crc'] = CRC of the file content.
  // Parameters :
  // Return Values :
  // --------------------------------------------------------------------------------
  function privConvertHeader2FileInfo($p_header, &$p_info)
  {
    $v_result=1;

    // ----- Get the interesting attributes
    $v_temp_path = PclZipUtilPathReduction($p_header['filename']);
    $p_info['filename'] = $v_temp_path;
    $v_temp_path = PclZipUtilPathReduction($p_header['stored_filename']);
    $p_info['stored_filename'] = $v_temp_path;
    $p_info['size'] = $p_header['size'];
    $p_info['compressed_size'] = $p_header['compressed_size'];
    $p_info['mtime'] = $p_header['mtime'];
    $p_info['comment'] = $p_header['comment'];
    $p_info['folder'] = (($p_header['external']&0x00000010)==0x00000010);
    $p_info['index'] = $p_header['index'];
    $p_info['status'] = $p_header['status'];
    $p_info['crc'] = $p_header['crc'];

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privExtractByRule()
  // Description :
  //   Extract a file or directory depending of rules (by index, by name, ...)
  // Parameters :
  //   $p_file_list : An array where will be placed the properties of each
  //                  extracted file
  //   $p_path : Path to add while writing the extracted files
  //   $p_remove_path : Path to remove (from the file memorized path) while writing the
  //                    extracted files. If the path does not match the file path,
  //                    the file is extracted with its memorized path.
  //                    $p_remove_path does not apply to 'list' mode.
  //                    $p_path and $p_remove_path are commulative.
  // Return Values :
  //   1 on success,0 or less on error (see error code list)
  // --------------------------------------------------------------------------------
  function privExtractByRule(&$p_file_list, $p_path, $p_remove_path, $p_remove_all_path, &$p_options)
  {
    $v_result=1;

    // ----- Magic quotes trick
    $this->privDisableMagicQuotes();

    // ----- Check the path
    if (   ($p_path == "")
	    || (   (substr($p_path, 0, 1) != "/")
		    && (substr($p_path, 0, 3) != "../")
			&& (substr($p_path,1,2)!=":/")))
      $p_path = "./".$p_path;

    // ----- Reduce the path last (and duplicated) '/'
    if (($p_path != "./") && ($p_path != "/"))
    {
      // ----- Look for the path end '/'
      while (substr($p_path, -1) == "/")
      {
        $p_path = substr($p_path, 0, strlen($p_path)-1);
      }
    }

    // ----- Look for path to remove format (should end by /)
    if (($p_remove_path != "") && (substr($p_remove_path, -1) != '/'))
    {
      $p_remove_path .= '/';
    }
    $p_remove_path_size = strlen($p_remove_path);

    // ----- Open the zip file
    if (($v_result = $this->privOpenFd('rb')) != 1)
    {
      $this->privSwapBackMagicQuotes();
      return $v_result;
    }

    // ----- Read the central directory information
    $v_central_dir = array();
    if (($v_result = $this->privReadEndCentralDir($v_central_dir)) != 1)
    {
      // ----- Close the zip file
      $this->privCloseFd();
      $this->privSwapBackMagicQuotes();

      return $v_result;
    }

    // ----- Start at beginning of Central Dir
    $v_pos_entry = $v_central_dir['offset'];

    // ----- Read each entry
    $j_start = 0;
    for ($i=0, $v_nb_extracted=0; $i<$v_central_dir['entries']; $i++)
    {

      // ----- Read next Central dir entry
      @rewind($this->zip_fd);
      if (@fseek($this->zip_fd, $v_pos_entry))
      {
        // ----- Close the zip file
        $this->privCloseFd();
        $this->privSwapBackMagicQuotes();

        // ----- Error log
        PclZip::privErrorLog(PCLZIP_ERR_INVALID_ARCHIVE_ZIP, 'Invalid archive size');

        // ----- Return
        return PclZip::errorCode();
      }

      // ----- Read the file header
      $v_header = array();
      if (($v_result = $this->privReadCentralFileHeader($v_header)) != 1)
      {
        // ----- Close the zip file
        $this->privCloseFd();
        $this->privSwapBackMagicQuotes();

        return $v_result;
      }

      // ----- Store the index
      $v_header['index'] = $i;

      // ----- Store the file position
      $v_pos_entry = ftell($this->zip_fd);

      // ----- Look for the specific extract rules
      $v_extract = false;

      // ----- Look for extract by name rule
      if (   (isset($p_options[PCLZIP_OPT_BY_NAME]))
          && ($p_options[PCLZIP_OPT_BY_NAME] != 0)) {

          // ----- Look if the filename is in the list
          for ($j=0; ($j<sizeof($p_options[PCLZIP_OPT_BY_NAME])) && (!$v_extract); $j++) {

              // ----- Look for a directory
              if (substr($p_options[PCLZIP_OPT_BY_NAME][$j], -1) == "/") {

                  // ----- Look if the directory is in the filename path
                  if (   (strlen($v_header['stored_filename']) > strlen($p_options[PCLZIP_OPT_BY_NAME][$j]))
                      && (substr($v_header['stored_filename'], 0, strlen($p_options[PCLZIP_OPT_BY_NAME][$j])) == $p_options[PCLZIP_OPT_BY_NAME][$j])) {
                      $v_extract = true;
                  }
              }
              // ----- Look for a filename
              elseif ($v_header['stored_filename'] == $p_options[PCLZIP_OPT_BY_NAME][$j]) {
                  $v_extract = true;
              }
          }
      }

      // ----- Look for extract by ereg rule
      // ereg() is deprecated with PHP 5.3
      /*
      else if (   (isset($p_options[PCLZIP_OPT_BY_EREG]))
               && ($p_options[PCLZIP_OPT_BY_EREG] != "")) {

          if (ereg($p_options[PCLZIP_OPT_BY_EREG], $v_header['stored_filename'])) {
              $v_extract = true;
          }
      }
      */

      // ----- Look for extract by preg rule
      else if (   (isset($p_options[PCLZIP_OPT_BY_PREG]))
               && ($p_options[PCLZIP_OPT_BY_PREG] != "")) {

          if (preg_match($p_options[PCLZIP_OPT_BY_PREG], $v_header['stored_filename'])) {
              $v_extract = true;
          }
      }

      // ----- Look for extract by index rule
      else if (   (isset($p_options[PCLZIP_OPT_BY_INDEX]))
               && ($p_options[PCLZIP_OPT_BY_INDEX] != 0)) {

          // ----- Look if the index is in the list
          for ($j=$j_start; ($j<sizeof($p_options[PCLZIP_OPT_BY_INDEX])) && (!$v_extract); $j++) {

              if (($i>=$p_options[PCLZIP_OPT_BY_INDEX][$j]['start']) && ($i<=$p_options[PCLZIP_OPT_BY_INDEX][$j]['end'])) {
                  $v_extract = true;
              }
              if ($i>=$p_options[PCLZIP_OPT_BY_INDEX][$j]['end']) {
                  $j_start = $j+1;
              }

              if ($p_options[PCLZIP_OPT_BY_INDEX][$j]['start']>$i) {
                  break;
              }
          }
      }

      // ----- Look for no rule, which means extract all the archive
      else {
          $v_extract = true;
      }

	  // ----- Check compression method
	  if (   ($v_extract)
	      && (   ($v_header['compression'] != 8)
		      && ($v_header['compression'] != 0))) {
          $v_header['status'] = 'unsupported_compression';

          // ----- Look for PCLZIP_OPT_STOP_ON_ERROR
          if (   (isset($p_options[PCLZIP_OPT_STOP_ON_ERROR]))
		      && ($p_options[PCLZIP_OPT_STOP_ON_ERROR]===true)) {

              $this->privSwapBackMagicQuotes();

              PclZip::privErrorLog(PCLZIP_ERR_UNSUPPORTED_COMPRESSION,
			                       "Filename '".$v_header['stored_filename']."' is "
				  	    	  	   ."compressed by an unsupported compression "
				  	    	  	   ."method (".$v_header['compression'].") ");

              return PclZip::errorCode();
		  }
	  }

	  // ----- Check encrypted files
	  if (($v_extract) && (($v_header['flag'] & 1) == 1)) {
          $v_header['status'] = 'unsupported_encryption';

          // ----- Look for PCLZIP_OPT_STOP_ON_ERROR
          if (   (isset($p_options[PCLZIP_OPT_STOP_ON_ERROR]))
		      && ($p_options[PCLZIP_OPT_STOP_ON_ERROR]===true)) {

              $this->privSwapBackMagicQuotes();

              PclZip::privErrorLog(PCLZIP_ERR_UNSUPPORTED_ENCRYPTION,
			                       "Unsupported encryption for "
				  	    	  	   ." filename '".$v_header['stored_filename']
								   ."'");

              return PclZip::errorCode();
		  }
    }

      // ----- Look for real extraction
      if (($v_extract) && ($v_header['status'] != 'ok')) {
          $v_result = $this->privConvertHeader2FileInfo($v_header,
		                                        $p_file_list[$v_nb_extracted++]);
          if ($v_result != 1) {
              $this->privCloseFd();
              $this->privSwapBackMagicQuotes();
              return $v_result;
          }

          $v_extract = false;
      }

      // ----- Look for real extraction
      if ($v_extract)
      {

        // ----- Go to the file position
        @rewind($this->zip_fd);
        if (@fseek($this->zip_fd, $v_header['offset']))
        {
          // ----- Close the zip file
          $this->privCloseFd();

          $this->privSwapBackMagicQuotes();

          // ----- Error log
          PclZip::privErrorLog(PCLZIP_ERR_INVALID_ARCHIVE_ZIP, 'Invalid archive size');

          // ----- Return
          return PclZip::errorCode();
        }

        // ----- Look for extraction as string
        if ($p_options[PCLZIP_OPT_EXTRACT_AS_STRING]) {

          $v_string = '';

          // ----- Extracting the file
          $v_result1 = $this->privExtractFileAsString($v_header, $v_string, $p_options);
          if ($v_result1 < 1) {
            $this->privCloseFd();
            $this->privSwapBackMagicQuotes();
            return $v_result1;
          }

          // ----- Get the only interesting attributes
          if (($v_result = $this->privConvertHeader2FileInfo($v_header, $p_file_list[$v_nb_extracted])) != 1)
          {
            // ----- Close the zip file
            $this->privCloseFd();
            $this->privSwapBackMagicQuotes();

            return $v_result;
          }

          // ----- Set the file content
          $p_file_list[$v_nb_extracted]['content'] = $v_string;

          // ----- Next extracted file
          $v_nb_extracted++;

          // ----- Look for user callback abort
          if ($v_result1 == 2) {
          	break;
          }
        }
        // ----- Look for extraction in standard output
        elseif (   (isset($p_options[PCLZIP_OPT_EXTRACT_IN_OUTPUT]))
		        && ($p_options[PCLZIP_OPT_EXTRACT_IN_OUTPUT])) {
          // ----- Extracting the file in standard output
          $v_result1 = $this->privExtractFileInOutput($v_header, $p_options);
          if ($v_result1 < 1) {
            $this->privCloseFd();
            $this->privSwapBackMagicQuotes();
            return $v_result1;
          }

          // ----- Get the only interesting attributes
          if (($v_result = $this->privConvertHeader2FileInfo($v_header, $p_file_list[$v_nb_extracted++])) != 1) {
            $this->privCloseFd();
            $this->privSwapBackMagicQuotes();
            return $v_result;
          }

          // ----- Look for user callback abort
          if ($v_result1 == 2) {
          	break;
          }
        }
        // ----- Look for normal extraction
        else {
          // ----- Extracting the file
          $v_result1 = $this->privExtractFile($v_header,
		                                      $p_path, $p_remove_path,
											  $p_remove_all_path,
											  $p_options);
          if ($v_result1 < 1) {
            $this->privCloseFd();
            $this->privSwapBackMagicQuotes();
            return $v_result1;
          }

          // ----- Get the only interesting attributes
          if (($v_result = $this->privConvertHeader2FileInfo($v_header, $p_file_list[$v_nb_extracted++])) != 1)
          {
            // ----- Close the zip file
            $this->privCloseFd();
            $this->privSwapBackMagicQuotes();

            return $v_result;
          }

          // ----- Look for user callback abort
          if ($v_result1 == 2) {
          	break;
          }
        }
      }
    }

    // ----- Close the zip file
    $this->privCloseFd();
    $this->privSwapBackMagicQuotes();

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privExtractFile()
  // Description :
  // Parameters :
  // Return Values :
  //
  // 1 : ... ?
  // PCLZIP_ERR_USER_ABORTED(2) : User ask for extraction stop in callback
  // --------------------------------------------------------------------------------
  function privExtractFile(&$p_entry, $p_path, $p_remove_path, $p_remove_all_path, &$p_options)
  {
    $v_result=1;

    // ----- Read the file header
    if (($v_result = $this->privReadFileHeader($v_header)) != 1)
    {
      // ----- Return
      return $v_result;
    }


    // ----- Check that the file header is coherent with $p_entry info
    if ($this->privCheckFileHeaders($v_header, $p_entry) != 1) {
        // TBC
    }

    // ----- Look for all path to remove
    if ($p_remove_all_path == true) {
        // ----- Look for folder entry that not need to be extracted
        if (($p_entry['external']&0x00000010)==0x00000010) {

            $p_entry['status'] = "filtered";

            return $v_result;
        }

        // ----- Get the basename of the path
        $p_entry['filename'] = basename($p_entry['filename']);
    }

    // ----- Look for path to remove
    else if ($p_remove_path != "")
    {
      if (PclZipUtilPathInclusion($p_remove_path, $p_entry['filename']) == 2)
      {

        // ----- Change the file status
        $p_entry['status'] = "filtered";

        // ----- Return
        return $v_result;
      }

      $p_remove_path_size = strlen($p_remove_path);
      if (substr($p_entry['filename'], 0, $p_remove_path_size) == $p_remove_path)
      {

        // ----- Remove the path
        $p_entry['filename'] = substr($p_entry['filename'], $p_remove_path_size);

      }
    }

    // ----- Add the path
    if ($p_path != '') {
      $p_entry['filename'] = $p_path."/".$p_entry['filename'];
    }

    // ----- Check a base_dir_restriction
    if (isset($p_options[PCLZIP_OPT_EXTRACT_DIR_RESTRICTION])) {
      $v_inclusion
      = PclZipUtilPathInclusion($p_options[PCLZIP_OPT_EXTRACT_DIR_RESTRICTION],
                                $p_entry['filename']);
      if ($v_inclusion == 0) {

        PclZip::privErrorLog(PCLZIP_ERR_DIRECTORY_RESTRICTION,
			                     "Filename '".$p_entry['filename']."' is "
								 ."outside PCLZIP_OPT_EXTRACT_DIR_RESTRICTION");

        return PclZip::errorCode();
      }
    }

    // ----- Look for pre-extract callback
    if (isset($p_options[PCLZIP_CB_PRE_EXTRACT])) {

      // ----- Generate a local information
      $v_local_header = array();
      $this->privConvertHeader2FileInfo($p_entry, $v_local_header);

      // ----- Call the callback
      // Here I do not use call_user_func() because I need to send a reference to the
      // header.
      $v_result = $p_options[PCLZIP_CB_PRE_EXTRACT](PCLZIP_CB_PRE_EXTRACT, $v_local_header);
      if ($v_result == 0) {
        // ----- Change the file status
        $p_entry['status'] = "skipped";
        $v_result = 1;
      }

      // ----- Look for abort result
      if ($v_result == 2) {
        // ----- This status is internal and will be changed in 'skipped'
        $p_entry['status'] = "aborted";
      	$v_result = PCLZIP_ERR_USER_ABORTED;
      }

      // ----- Update the information
      // Only some fields can be modified
      $p_entry['filename'] = $v_local_header['filename'];
    }


    // ----- Look if extraction should be done
    if ($p_entry['status'] == 'ok') {

    // ----- Look for specific actions while the file exist
    if (file_exists($p_entry['filename']))
    {

      // ----- Look if file is a directory
      if (is_dir($p_entry['filename']))
      {

        // ----- Change the file status
        $p_entry['status'] = "already_a_directory";

        // ----- Look for PCLZIP_OPT_STOP_ON_ERROR
        // For historical reason first PclZip implementation does not stop
        // when this kind of error occurs.
        if (   (isset($p_options[PCLZIP_OPT_STOP_ON_ERROR]))
		    && ($p_options[PCLZIP_OPT_STOP_ON_ERROR]===true)) {

            PclZip::privErrorLog(PCLZIP_ERR_ALREADY_A_DIRECTORY,
			                     "Filename '".$p_entry['filename']."' is "
								 ."already used by an existing directory");

            return PclZip::errorCode();
		    }
      }
      // ----- Look if file is write protected
      else if (!is_writeable($p_entry['filename']))
      {

        // ----- Change the file status
        $p_entry['status'] = "write_protected";

        // ----- Look for PCLZIP_OPT_STOP_ON_ERROR
        // For historical reason first PclZip implementation does not stop
        // when this kind of error occurs.
        if (   (isset($p_options[PCLZIP_OPT_STOP_ON_ERROR]))
		    && ($p_options[PCLZIP_OPT_STOP_ON_ERROR]===true)) {

            PclZip::privErrorLog(PCLZIP_ERR_WRITE_OPEN_FAIL,
			                     "Filename '".$p_entry['filename']."' exists "
								 ."and is write protected");

            return PclZip::errorCode();
		    }
      }

      // ----- Look if the extracted file is older
      else if (filemtime($p_entry['filename']) > $p_entry['mtime'])
      {
        // ----- Change the file status
        if (   (isset($p_options[PCLZIP_OPT_REPLACE_NEWER]))
		    && ($p_options[PCLZIP_OPT_REPLACE_NEWER]===true)) {
	  	  }
		    else {
            $p_entry['status'] = "newer_exist";

            // ----- Look for PCLZIP_OPT_STOP_ON_ERROR
            // For historical reason first PclZip implementation does not stop
            // when this kind of error occurs.
            if (   (isset($p_options[PCLZIP_OPT_STOP_ON_ERROR]))
		        && ($p_options[PCLZIP_OPT_STOP_ON_ERROR]===true)) {

                PclZip::privErrorLog(PCLZIP_ERR_WRITE_OPEN_FAIL,
			             "Newer version of '".$p_entry['filename']."' exists "
					    ."and option PCLZIP_OPT_REPLACE_NEWER is not selected");

                return PclZip::errorCode();
		      }
		    }
      }
      else {
      }
    }

    // ----- Check the directory availability and create it if necessary
    else {
      if ((($p_entry['external']&0x00000010)==0x00000010) || (substr($p_entry['filename'], -1) == '/'))
        $v_dir_to_check = $p_entry['filename'];
      else if (!strstr($p_entry['filename'], "/"))
        $v_dir_to_check = "";
      else
        $v_dir_to_check = dirname($p_entry['filename']);

        if (($v_result = $this->privDirCheck($v_dir_to_check, (($p_entry['external']&0x00000010)==0x00000010))) != 1) {

          // ----- Change the file status
          $p_entry['status'] = "path_creation_fail";

          // ----- Return
          //return $v_result;
          $v_result = 1;
        }
      }
    }

    // ----- Look if extraction should be done
    if ($p_entry['status'] == 'ok') {

      // ----- Do the extraction (if not a folder)
      if (!(($p_entry['external']&0x00000010)==0x00000010))
      {
        // ----- Look for not compressed file
        if ($p_entry['compression'] == 0) {

    		  // ----- Opening destination file
          if (($v_dest_file = @fopen($p_entry['filename'], 'wb')) == 0)
          {

            // ----- Change the file status
            $p_entry['status'] = "write_error";

            // ----- Return
            return $v_result;
          }


          // ----- Read the file by PCLZIP_READ_BLOCK_SIZE octets blocks
          $v_size = $p_entry['compressed_size'];
          while ($v_size != 0)
          {
            $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
            $v_buffer = @fread($this->zip_fd, $v_read_size);
            /* Try to speed up the code
            $v_binary_data = pack('a'.$v_read_size, $v_buffer);
            @fwrite($v_dest_file, $v_binary_data, $v_read_size);
            */
            @fwrite($v_dest_file, $v_buffer, $v_read_size);
            $v_size -= $v_read_size;
          }

          // ----- Closing the destination file
          fclose($v_dest_file);

          // ----- Change the file mtime
          touch($p_entry['filename'], $p_entry['mtime']);


        }
        else {
          // ----- TBC
          // Need to be finished
          if (($p_entry['flag'] & 1) == 1) {
            PclZip::privErrorLog(PCLZIP_ERR_UNSUPPORTED_ENCRYPTION, 'File \''.$p_entry['filename'].'\' is encrypted. Encrypted files are not supported.');
            return PclZip::errorCode();
          }


          // ----- Look for using temporary file to unzip
          if ( (!isset($p_options[PCLZIP_OPT_TEMP_FILE_OFF]))
              && (isset($p_options[PCLZIP_OPT_TEMP_FILE_ON])
                  || (isset($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD])
                      && ($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD] <= $p_entry['size'])) ) ) {
            $v_result = $this->privExtractFileUsingTempFile($p_entry, $p_options);
            if ($v_result < PCLZIP_ERR_NO_ERROR) {
              return $v_result;
            }
          }

          // ----- Look for extract in memory
          else {


            // ----- Read the compressed file in a buffer (one shot)
            if ($p_entry['compressed_size'] > 0) {
              $v_buffer = @fread($this->zip_fd, $p_entry['compressed_size']);
            }
            else {
              $v_buffer = '';
            }

            // ----- Decompress the file
            $v_file_content = @gzinflate($v_buffer);
            unset($v_buffer);
            if ($v_file_content === FALSE) {

              // ----- Change the file status
              // TBC
              $p_entry['status'] = "error";

              return $v_result;
            }

            // ----- Opening destination file
            if (($v_dest_file = @fopen($p_entry['filename'], 'wb')) == 0) {

              // ----- Change the file status
              $p_entry['status'] = "write_error";

              return $v_result;
            }

            // ----- Write the uncompressed data
            @fwrite($v_dest_file, $v_file_content, $p_entry['size']);
            unset($v_file_content);

            // ----- Closing the destination file
            @fclose($v_dest_file);

          }

          // ----- Change the file mtime
          @touch($p_entry['filename'], $p_entry['mtime']);
        }

        // ----- Look for chmod option
        if (isset($p_options[PCLZIP_OPT_SET_CHMOD])) {

          // ----- Change the mode of the file
          @chmod($p_entry['filename'], $p_options[PCLZIP_OPT_SET_CHMOD]);
        }

      }
    }

  	// ----- Change abort status
  	if ($p_entry['status'] == "aborted") {
        $p_entry['status'] = "skipped";
  	}

    // ----- Look for post-extract callback
    elseif (isset($p_options[PCLZIP_CB_POST_EXTRACT])) {

      // ----- Generate a local information
      $v_local_header = array();
      $this->privConvertHeader2FileInfo($p_entry, $v_local_header);

      // ----- Call the callback
      // Here I do not use call_user_func() because I need to send a reference to the
      // header.
      $v_result = $p_options[PCLZIP_CB_POST_EXTRACT](PCLZIP_CB_POST_EXTRACT, $v_local_header);

      // ----- Look for abort result
      if ($v_result == 2) {
      	$v_result = PCLZIP_ERR_USER_ABORTED;
      }
    }

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privExtractFileUsingTempFile()
  // Description :
  // Parameters :
  // Return Values :
  // --------------------------------------------------------------------------------
  function privExtractFileUsingTempFile(&$p_entry, &$p_options)
  {
    $v_result=1;

    // ----- Creates a temporary file
    $v_gzip_temp_name = PCLZIP_TEMPORARY_DIR.uniqid('pclzip-').'.gz';
    if (($v_dest_file = @fopen($v_gzip_temp_name, "wb")) == 0) {
      fclose($v_file);
      PclZip::privErrorLog(PCLZIP_ERR_WRITE_OPEN_FAIL, 'Unable to open temporary file \''.$v_gzip_temp_name.'\' in binary write mode');
      return PclZip::errorCode();
    }


    // ----- Write gz file format header
    $v_binary_data = pack('va1a1Va1a1', 0x8b1f, Chr($p_entry['compression']), Chr(0x00), time(), Chr(0x00), Chr(3));
    @fwrite($v_dest_file, $v_binary_data, 10);

    // ----- Read the file by PCLZIP_READ_BLOCK_SIZE octets blocks
    $v_size = $p_entry['compressed_size'];
    while ($v_size != 0)
    {
      $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
      $v_buffer = @fread($this->zip_fd, $v_read_size);
      //$v_binary_data = pack('a'.$v_read_size, $v_buffer);
      @fwrite($v_dest_file, $v_buffer, $v_read_size);
      $v_size -= $v_read_size;
    }

    // ----- Write gz file format footer
    $v_binary_data = pack('VV', $p_entry['crc'], $p_entry['size']);
    @fwrite($v_dest_file, $v_binary_data, 8);

    // ----- Close the temporary file
    @fclose($v_dest_file);

    // ----- Opening destination file
    if (($v_dest_file = @fopen($p_entry['filename'], 'wb')) == 0) {
      $p_entry['status'] = "write_error";
      return $v_result;
    }

    // ----- Open the temporary gz file
    if (($v_src_file = @gzopen($v_gzip_temp_name, 'rb')) == 0) {
      @fclose($v_dest_file);
      $p_entry['status'] = "read_error";
      PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open temporary file \''.$v_gzip_temp_name.'\' in binary read mode');
      return PclZip::errorCode();
    }


    // ----- Read the file by PCLZIP_READ_BLOCK_SIZE octets blocks
    $v_size = $p_entry['size'];
    while ($v_size != 0) {
      $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
      $v_buffer = @gzread($v_src_file, $v_read_size);
      //$v_binary_data = pack('a'.$v_read_size, $v_buffer);
      @fwrite($v_dest_file, $v_buffer, $v_read_size);
      $v_size -= $v_read_size;
    }
    @fclose($v_dest_file);
    @gzclose($v_src_file);

    // ----- Delete the temporary file
    @unlink($v_gzip_temp_name);

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privExtractFileInOutput()
  // Description :
  // Parameters :
  // Return Values :
  // --------------------------------------------------------------------------------
  function privExtractFileInOutput(&$p_entry, &$p_options)
  {
    $v_result=1;

    // ----- Read the file header
    if (($v_result = $this->privReadFileHeader($v_header)) != 1) {
      return $v_result;
    }


    // ----- Check that the file header is coherent with $p_entry info
    if ($this->privCheckFileHeaders($v_header, $p_entry) != 1) {
        // TBC
    }

    // ----- Look for pre-extract callback
    if (isset($p_options[PCLZIP_CB_PRE_EXTRACT])) {

      // ----- Generate a local information
      $v_local_header = array();
      $this->privConvertHeader2FileInfo($p_entry, $v_local_header);

      // ----- Call the callback
      // Here I do not use call_user_func() because I need to send a reference to the
      // header.
//      eval('$v_result = '.$p_options[PCLZIP_CB_PRE_EXTRACT].'(PCLZIP_CB_PRE_EXTRACT, $v_local_header);');
      $v_result = $p_options[PCLZIP_CB_PRE_EXTRACT](PCLZIP_CB_PRE_EXTRACT, $v_local_header);
      if ($v_result == 0) {
        // ----- Change the file status
        $p_entry['status'] = "skipped";
        $v_result = 1;
      }

      // ----- Look for abort result
      if ($v_result == 2) {
        // ----- This status is internal and will be changed in 'skipped'
        $p_entry['status'] = "aborted";
      	$v_result = PCLZIP_ERR_USER_ABORTED;
      }

      // ----- Update the information
      // Only some fields can be modified
      $p_entry['filename'] = $v_local_header['filename'];
    }

    // ----- Trace

    // ----- Look if extraction should be done
    if ($p_entry['status'] == 'ok') {

      // ----- Do the extraction (if not a folder)
      if (!(($p_entry['external']&0x00000010)==0x00000010)) {
        // ----- Look for not compressed file
        if ($p_entry['compressed_size'] == $p_entry['size']) {

          // ----- Read the file in a buffer (one shot)
          if ($p_entry['compressed_size'] > 0) {
            $v_buffer = @fread($this->zip_fd, $p_entry['compressed_size']);
          }
          else {
            $v_buffer = '';
          }

          // ----- Send the file to the output
          echo $v_buffer;
          unset($v_buffer);
        }
        else {

          // ----- Read the compressed file in a buffer (one shot)
          if ($p_entry['compressed_size'] > 0) {
            $v_buffer = @fread($this->zip_fd, $p_entry['compressed_size']);
          }
          else {
            $v_buffer = '';
          }

          // ----- Decompress the file
          $v_file_content = gzinflate($v_buffer);
          unset($v_buffer);

          // ----- Send the file to the output
          echo $v_file_content;
          unset($v_file_content);
        }
      }
    }

	// ----- Change abort status
	if ($p_entry['status'] == "aborted") {
      $p_entry['status'] = "skipped";
	}

    // ----- Look for post-extract callback
    elseif (isset($p_options[PCLZIP_CB_POST_EXTRACT])) {

      // ----- Generate a local information
      $v_local_header = array();
      $this->privConvertHeader2FileInfo($p_entry, $v_local_header);

      // ----- Call the callback
      // Here I do not use call_user_func() because I need to send a reference to the
      // header.
      $v_result = $p_options[PCLZIP_CB_POST_EXTRACT](PCLZIP_CB_POST_EXTRACT, $v_local_header);

      // ----- Look for abort result
      if ($v_result == 2) {
      	$v_result = PCLZIP_ERR_USER_ABORTED;
      }
    }

    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privExtractFileAsString()
  // Description :
  // Parameters :
  // Return Values :
  // --------------------------------------------------------------------------------
  function privExtractFileAsString(&$p_entry, &$p_string, &$p_options)
  {
    $v_result=1;

    // ----- Read the file header
    $v_header = array();
    if (($v_result = $this->privReadFileHeader($v_header)) != 1)
    {
      // ----- Return
      return $v_result;
    }


    // ----- Check that the file header is coherent with $p_entry info
    if ($this->privCheckFileHeaders($v_header, $p_entry) != 1) {
        // TBC
    }

    // ----- Look for pre-extract callback
    if (isset($p_options[PCLZIP_CB_PRE_EXTRACT])) {

      // ----- Generate a local information
      $v_local_header = array();
      $this->privConvertHeader2FileInfo($p_entry, $v_local_header);

      // ----- Call the callback
      // Here I do not use call_user_func() because I need to send a reference to the
      // header.
      $v_result = $p_options[PCLZIP_CB_PRE_EXTRACT](PCLZIP_CB_PRE_EXTRACT, $v_local_header);
      if ($v_result == 0) {
        // ----- Change the file status
        $p_entry['status'] = "skipped";
        $v_result = 1;
      }

      // ----- Look for abort result
      if ($v_result == 2) {
        // ----- This status is internal and will be changed in 'skipped'
        $p_entry['status'] = "aborted";
      	$v_result = PCLZIP_ERR_USER_ABORTED;
      }

      // ----- Update the information
      // Only some fields can be modified
      $p_entry['filename'] = $v_local_header['filename'];
    }


    // ----- Look if extraction should be done
    if ($p_entry['status'] == 'ok') {

      // ----- Do the extraction (if not a folder)
      if (!(($p_entry['external']&0x00000010)==0x00000010)) {
        // ----- Look for not compressed file
  //      if ($p_entry['compressed_size'] == $p_entry['size'])
        if ($p_entry['compression'] == 0) {

          // ----- Reading the file
          if ($p_entry['compressed_size'] > 0) {
            $p_string = @fread($this->zip_fd, $p_entry['compressed_size']);
          }
          else {
            $p_string = '';
          }
        }
        else {

          // ----- Reading the file
          if ($p_entry['compressed_size'] > 0) {
            $v_data = @fread($this->zip_fd, $p_entry['compressed_size']);
          }
          else {
            $v_data = '';
          }

          // ----- Decompress the file
          if (($p_string = @gzinflate($v_data)) === FALSE) {
              // TBC
          }
        }

        // ----- Trace
      }
      else {
          // TBC : error : can not extract a folder in a string
      }

    }

  	// ----- Change abort status
  	if ($p_entry['status'] == "aborted") {
        $p_entry['status'] = "skipped";
  	}

    // ----- Look for post-extract callback
    elseif (isset($p_options[PCLZIP_CB_POST_EXTRACT])) {

      // ----- Generate a local information
      $v_local_header = array();
      $this->privConvertHeader2FileInfo($p_entry, $v_local_header);

      // ----- Swap the content to header
      $v_local_header['content'] = $p_string;
      $p_string = '';

      // ----- Call the callback
      // Here I do not use call_user_func() because I need to send a reference to the
      // header.
      $v_result = $p_options[PCLZIP_CB_POST_EXTRACT](PCLZIP_CB_POST_EXTRACT, $v_local_header);

      // ----- Swap back the content to header
      $p_string = $v_local_header['content'];
      unset($v_local_header['content']);

      // ----- Look for abort result
      if ($v_result == 2) {
      	$v_result = PCLZIP_ERR_USER_ABORTED;
      }
    }

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privReadFileHeader()
  // Description :
  // Parameters :
  // Return Values :
  // --------------------------------------------------------------------------------
  function privReadFileHeader(&$p_header)
  {
    $v_result=1;

    // ----- Read the 4 bytes signature
    $v_binary_data = @fread($this->zip_fd, 4);
    $v_data = unpack('Vid', $v_binary_data);

    // ----- Check signature
    if ($v_data['id'] != 0x04034b50)
    {

      // ----- Error log
      PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'Invalid archive structure');

      // ----- Return
      return PclZip::errorCode();
    }

    // ----- Read the first 42 bytes of the header
    $v_binary_data = fread($this->zip_fd, 26);

    // ----- Look for invalid block size
    if (strlen($v_binary_data) != 26)
    {
      $p_header['filename'] = "";
      $p_header['status'] = "invalid_header";

      // ----- Error log
      PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, "Invalid block size : ".strlen($v_binary_data));

      // ----- Return
      return PclZip::errorCode();
    }

    // ----- Extract the values
    $v_data = unpack('vversion/vflag/vcompression/vmtime/vmdate/Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len', $v_binary_data);

    // ----- Get filename
    $p_header['filename'] = fread($this->zip_fd, $v_data['filename_len']);

    // ----- Get extra_fields
    if ($v_data['extra_len'] != 0) {
      $p_header['extra'] = fread($this->zip_fd, $v_data['extra_len']);
    }
    else {
      $p_header['extra'] = '';
    }

    // ----- Extract properties
    $p_header['version_extracted'] = $v_data['version'];
    $p_header['compression'] = $v_data['compression'];
    $p_header['size'] = $v_data['size'];
    $p_header['compressed_size'] = $v_data['compressed_size'];
    $p_header['crc'] = $v_data['crc'];
    $p_header['flag'] = $v_data['flag'];
    $p_header['filename_len'] = $v_data['filename_len'];

    // ----- Recuperate date in UNIX format
    $p_header['mdate'] = $v_data['mdate'];
    $p_header['mtime'] = $v_data['mtime'];
    if ($p_header['mdate'] && $p_header['mtime'])
    {
      // ----- Extract time
      $v_hour = ($p_header['mtime'] & 0xF800) >> 11;
      $v_minute = ($p_header['mtime'] & 0x07E0) >> 5;
      $v_seconde = ($p_header['mtime'] & 0x001F)*2;

      // ----- Extract date
      $v_year = (($p_header['mdate'] & 0xFE00) >> 9) + 1980;
      $v_month = ($p_header['mdate'] & 0x01E0) >> 5;
      $v_day = $p_header['mdate'] & 0x001F;

      // ----- Get UNIX date format
      $p_header['mtime'] = @mktime($v_hour, $v_minute, $v_seconde, $v_month, $v_day, $v_year);

    }
    else
    {
      $p_header['mtime'] = time();
    }

    // TBC
    //for(reset($v_data); $key = key($v_data); next($v_data)) {
    //}

    // ----- Set the stored filename
    $p_header['stored_filename'] = $p_header['filename'];

    // ----- Set the status field
    $p_header['status'] = "ok";

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privReadCentralFileHeader()
  // Description :
  // Parameters :
  // Return Values :
  // --------------------------------------------------------------------------------
  function privReadCentralFileHeader(&$p_header)
  {
    $v_result=1;

    // ----- Read the 4 bytes signature
    $v_binary_data = @fread($this->zip_fd, 4);
    $v_data = unpack('Vid', $v_binary_data);

    // ----- Check signature
    if ($v_data['id'] != 0x02014b50)
    {

      // ----- Error log
      PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'Invalid archive structure');

      // ----- Return
      return PclZip::errorCode();
    }

    // ----- Read the first 42 bytes of the header
    $v_binary_data = fread($this->zip_fd, 42);

    // ----- Look for invalid block size
    if (strlen($v_binary_data) != 42)
    {
      $p_header['filename'] = "";
      $p_header['status'] = "invalid_header";

      // ----- Error log
      PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, "Invalid block size : ".strlen($v_binary_data));

      // ----- Return
      return PclZip::errorCode();
    }

    // ----- Extract the values
    $p_header = unpack('vversion/vversion_extracted/vflag/vcompression/vmtime/vmdate/Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len/vcomment_len/vdisk/vinternal/Vexternal/Voffset', $v_binary_data);

    // ----- Get filename
    if ($p_header['filename_len'] != 0)
      $p_header['filename'] = fread($this->zip_fd, $p_header['filename_len']);
    else
      $p_header['filename'] = '';

    // ----- Get extra
    if ($p_header['extra_len'] != 0)
      $p_header['extra'] = fread($this->zip_fd, $p_header['extra_len']);
    else
      $p_header['extra'] = '';

    // ----- Get comment
    if ($p_header['comment_len'] != 0)
      $p_header['comment'] = fread($this->zip_fd, $p_header['comment_len']);
    else
      $p_header['comment'] = '';

    // ----- Extract properties

    // ----- Recuperate date in UNIX format
    //if ($p_header['mdate'] && $p_header['mtime'])
    // TBC : bug : this was ignoring time with 0/0/0
    if (1)
    {
      // ----- Extract time
      $v_hour = ($p_header['mtime'] & 0xF800) >> 11;
      $v_minute = ($p_header['mtime'] & 0x07E0) >> 5;
      $v_seconde = ($p_header['mtime'] & 0x001F)*2;

      // ----- Extract date
      $v_year = (($p_header['mdate'] & 0xFE00) >> 9) + 1980;
      $v_month = ($p_header['mdate'] & 0x01E0) >> 5;
      $v_day = $p_header['mdate'] & 0x001F;

      // ----- Get UNIX date format
      $p_header['mtime'] = @mktime($v_hour, $v_minute, $v_seconde, $v_month, $v_day, $v_year);

    }
    else
    {
      $p_header['mtime'] = time();
    }

    // ----- Set the stored filename
    $p_header['stored_filename'] = $p_header['filename'];

    // ----- Set default status to ok
    $p_header['status'] = 'ok';

    // ----- Look if it is a directory
    if (substr($p_header['filename'], -1) == '/') {
      //$p_header['external'] = 0x41FF0010;
      $p_header['external'] = 0x00000010;
    }


    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privCheckFileHeaders()
  // Description :
  // Parameters :
  // Return Values :
  //   1 on success,
  //   0 on error;
  // --------------------------------------------------------------------------------
  function privCheckFileHeaders(&$p_local_header, &$p_central_header)
  {
    $v_result=1;

  	// ----- Check the static values
  	// TBC
  	if ($p_local_header['filename'] != $p_central_header['filename']) {
  	}
  	if ($p_local_header['version_extracted'] != $p_central_header['version_extracted']) {
  	}
  	if ($p_local_header['flag'] != $p_central_header['flag']) {
  	}
  	if ($p_local_header['compression'] != $p_central_header['compression']) {
  	}
  	if ($p_local_header['mtime'] != $p_central_header['mtime']) {
  	}
  	if ($p_local_header['filename_len'] != $p_central_header['filename_len']) {
  	}

  	// ----- Look for flag bit 3
  	if (($p_local_header['flag'] & 8) == 8) {
          $p_local_header['size'] = $p_central_header['size'];
          $p_local_header['compressed_size'] = $p_central_header['compressed_size'];
          $p_local_header['crc'] = $p_central_header['crc'];
  	}

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privReadEndCentralDir()
  // Description :
  // Parameters :
  // Return Values :
  // --------------------------------------------------------------------------------
  function privReadEndCentralDir(&$p_central_dir)
  {
    $v_result=1;

    // ----- Go to the end of the zip file
    $v_size = filesize($this->zipname);
    @fseek($this->zip_fd, $v_size);
    if (@ftell($this->zip_fd) != $v_size)
    {
      // ----- Error log
      PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'Unable to go to the end of the archive \''.$this->zipname.'\'');

      // ----- Return
      return PclZip::errorCode();
    }

    // ----- First try : look if this is an archive with no commentaries (most of the time)
    // in this case the end of central dir is at 22 bytes of the file end
    $v_found = 0;
    if ($v_size > 26) {
      @fseek($this->zip_fd, $v_size-22);
      if (($v_pos = @ftell($this->zip_fd)) != ($v_size-22))
      {
        // ----- Error log
        PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'Unable to seek back to the middle of the archive \''.$this->zipname.'\'');

        // ----- Return
        return PclZip::errorCode();
      }

      // ----- Read for bytes
      $v_binary_data = @fread($this->zip_fd, 4);
      $v_data = @unpack('Vid', $v_binary_data);

      // ----- Check signature
      if ($v_data['id'] == 0x06054b50) {
        $v_found = 1;
      }

      $v_pos = ftell($this->zip_fd);
    }

    // ----- Go back to the maximum possible size of the Central Dir End Record
    if (!$v_found) {
      $v_maximum_size = 65557; // 0xFFFF + 22;
      if ($v_maximum_size > $v_size)
        $v_maximum_size = $v_size;
      @fseek($this->zip_fd, $v_size-$v_maximum_size);
      if (@ftell($this->zip_fd) != ($v_size-$v_maximum_size))
      {
        // ----- Error log
        PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'Unable to seek back to the middle of the archive \''.$this->zipname.'\'');

        // ----- Return
        return PclZip::errorCode();
      }

      // ----- Read byte per byte in order to find the signature
      $v_pos = ftell($this->zip_fd);
      $v_bytes = 0x00000000;
      while ($v_pos < $v_size)
      {
        // ----- Read a byte
        $v_byte = @fread($this->zip_fd, 1);

        // -----  Add the byte
        //$v_bytes = ($v_bytes << 8) | Ord($v_byte);
        // Note we mask the old value down such that once shifted we can never end up with more than a 32bit number
        // Otherwise on systems where we have 64bit integers the check below for the magic number will fail.
        $v_bytes = ( ($v_bytes & 0xFFFFFF) << 8) | Ord($v_byte);

        // ----- Compare the bytes
        if ($v_bytes == 0x504b0506)
        {
          $v_pos++;
          break;
        }

        $v_pos++;
      }

      // ----- Look if not found end of central dir
      if ($v_pos == $v_size)
      {

        // ----- Error log
        PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, "Unable to find End of Central Dir Record signature");

        // ----- Return
        return PclZip::errorCode();
      }
    }

    // ----- Read the first 18 bytes of the header
    $v_binary_data = fread($this->zip_fd, 18);

    // ----- Look for invalid block size
    if (strlen($v_binary_data) != 18)
    {

      // ----- Error log
      PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, "Invalid End of Central Dir Record size : ".strlen($v_binary_data));

      // ----- Return
      return PclZip::errorCode();
    }

    // ----- Extract the values
    $v_data = unpack('vdisk/vdisk_start/vdisk_entries/ventries/Vsize/Voffset/vcomment_size', $v_binary_data);

    // ----- Check the global size
    if (($v_pos + $v_data['comment_size'] + 18) != $v_size) {

	  // ----- Removed in release 2.2 see readme file
	  // The check of the file size is a little too strict.
	  // Some bugs where found when a zip is encrypted/decrypted with 'crypt'.
	  // While decrypted, zip has training 0 bytes
	  if (0) {
      // ----- Error log
      PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT,
	                       'The central dir is not at the end of the archive.'
						   .' Some trailing bytes exists after the archive.');

      // ----- Return
      return PclZip::errorCode();
	  }
    }

    // ----- Get comment
    if ($v_data['comment_size'] != 0) {
      $p_central_dir['comment'] = fread($this->zip_fd, $v_data['comment_size']);
    }
    else
      $p_central_dir['comment'] = '';

    $p_central_dir['entries'] = $v_data['entries'];
    $p_central_dir['disk_entries'] = $v_data['disk_entries'];
    $p_central_dir['offset'] = $v_data['offset'];
    $p_central_dir['size'] = $v_data['size'];
    $p_central_dir['disk'] = $v_data['disk'];
    $p_central_dir['disk_start'] = $v_data['disk_start'];

    // TBC
    //for(reset($p_central_dir); $key = key($p_central_dir); next($p_central_dir)) {
    //}

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privDeleteByRule()
  // Description :
  // Parameters :
  // Return Values :
  // --------------------------------------------------------------------------------
  function privDeleteByRule(&$p_result_list, &$p_options)
  {
    $v_result=1;
    $v_list_detail = array();

    // ----- Open the zip file
    if (($v_result=$this->privOpenFd('rb')) != 1)
    {
      // ----- Return
      return $v_result;
    }

    // ----- Read the central directory information
    $v_central_dir = array();
    if (($v_result = $this->privReadEndCentralDir($v_central_dir)) != 1)
    {
      $this->privCloseFd();
      return $v_result;
    }

    // ----- Go to beginning of File
    @rewind($this->zip_fd);

    // ----- Scan all the files
    // ----- Start at beginning of Central Dir
    $v_pos_entry = $v_central_dir['offset'];
    @rewind($this->zip_fd);
    if (@fseek($this->zip_fd, $v_pos_entry))
    {
      // ----- Close the zip file
      $this->privCloseFd();

      // ----- Error log
      PclZip::privErrorLog(PCLZIP_ERR_INVALID_ARCHIVE_ZIP, 'Invalid archive size');

      // ----- Return
      return PclZip::errorCode();
    }

    // ----- Read each entry
    $v_header_list = array();
    $j_start = 0;
    for ($i=0, $v_nb_extracted=0; $i<$v_central_dir['entries']; $i++)
    {

      // ----- Read the file header
      $v_header_list[$v_nb_extracted] = array();
      if (($v_result = $this->privReadCentralFileHeader($v_header_list[$v_nb_extracted])) != 1)
      {
        // ----- Close the zip file
        $this->privCloseFd();

        return $v_result;
      }


      // ----- Store the index
      $v_header_list[$v_nb_extracted]['index'] = $i;

      // ----- Look for the specific extract rules
      $v_found = false;

      // ----- Look for extract by name rule
      if (   (isset($p_options[PCLZIP_OPT_BY_NAME]))
          && ($p_options[PCLZIP_OPT_BY_NAME] != 0)) {

          // ----- Look if the filename is in the list
          for ($j=0; ($j<sizeof($p_options[PCLZIP_OPT_BY_NAME])) && (!$v_found); $j++) {

              // ----- Look for a directory
              if (substr($p_options[PCLZIP_OPT_BY_NAME][$j], -1) == "/") {

                  // ----- Look if the directory is in the filename path
                  if (   (strlen($v_header_list[$v_nb_extracted]['stored_filename']) > strlen($p_options[PCLZIP_OPT_BY_NAME][$j]))
                      && (substr($v_header_list[$v_nb_extracted]['stored_filename'], 0, strlen($p_options[PCLZIP_OPT_BY_NAME][$j])) == $p_options[PCLZIP_OPT_BY_NAME][$j])) {
                      $v_found = true;
                  }
                  elseif (   (($v_header_list[$v_nb_extracted]['external']&0x00000010)==0x00000010) /* Indicates a folder */
                          && ($v_header_list[$v_nb_extracted]['stored_filename'].'/' == $p_options[PCLZIP_OPT_BY_NAME][$j])) {
                      $v_found = true;
                  }
              }
              // ----- Look for a filename
              elseif ($v_header_list[$v_nb_extracted]['stored_filename'] == $p_options[PCLZIP_OPT_BY_NAME][$j]) {
                  $v_found = true;
              }
          }
      }

      // ----- Look for extract by ereg rule
      // ereg() is deprecated with PHP 5.3
      /*
      else if (   (isset($p_options[PCLZIP_OPT_BY_EREG]))
               && ($p_options[PCLZIP_OPT_BY_EREG] != "")) {

          if (ereg($p_options[PCLZIP_OPT_BY_EREG], $v_header_list[$v_nb_extracted]['stored_filename'])) {
              $v_found = true;
          }
      }
      */

      // ----- Look for extract by preg rule
      else if (   (isset($p_options[PCLZIP_OPT_BY_PREG]))
               && ($p_options[PCLZIP_OPT_BY_PREG] != "")) {

          if (preg_match($p_options[PCLZIP_OPT_BY_PREG], $v_header_list[$v_nb_extracted]['stored_filename'])) {
              $v_found = true;
          }
      }

      // ----- Look for extract by index rule
      else if (   (isset($p_options[PCLZIP_OPT_BY_INDEX]))
               && ($p_options[PCLZIP_OPT_BY_INDEX] != 0)) {

          // ----- Look if the index is in the list
          for ($j=$j_start; ($j<sizeof($p_options[PCLZIP_OPT_BY_INDEX])) && (!$v_found); $j++) {

              if (($i>=$p_options[PCLZIP_OPT_BY_INDEX][$j]['start']) && ($i<=$p_options[PCLZIP_OPT_BY_INDEX][$j]['end'])) {
                  $v_found = true;
              }
              if ($i>=$p_options[PCLZIP_OPT_BY_INDEX][$j]['end']) {
                  $j_start = $j+1;
              }

              if ($p_options[PCLZIP_OPT_BY_INDEX][$j]['start']>$i) {
                  break;
              }
          }
      }
      else {
      	$v_found = true;
      }

      // ----- Look for deletion
      if ($v_found)
      {
        unset($v_header_list[$v_nb_extracted]);
      }
      else
      {
        $v_nb_extracted++;
      }
    }

    // ----- Look if something need to be deleted
    if ($v_nb_extracted > 0) {

        // ----- Creates a temporary file
        $v_zip_temp_name = PCLZIP_TEMPORARY_DIR.uniqid('pclzip-').'.tmp';

        // ----- Creates a temporary zip archive
        $v_temp_zip = new PclZip($v_zip_temp_name);

        // ----- Open the temporary zip file in write mode
        if (($v_result = $v_temp_zip->privOpenFd('wb')) != 1) {
            $this->privCloseFd();

            // ----- Return
            return $v_result;
        }

        // ----- Look which file need to be kept
        for ($i=0; $i<sizeof($v_header_list); $i++) {

            // ----- Calculate the position of the header
            @rewind($this->zip_fd);
            if (@fseek($this->zip_fd,  $v_header_list[$i]['offset'])) {
                // ----- Close the zip file
                $this->privCloseFd();
                $v_temp_zip->privCloseFd();
                @unlink($v_zip_temp_name);

                // ----- Error log
                PclZip::privErrorLog(PCLZIP_ERR_INVALID_ARCHIVE_ZIP, 'Invalid archive size');

                // ----- Return
                return PclZip::errorCode();
            }

            // ----- Read the file header
            $v_local_header = array();
            if (($v_result = $this->privReadFileHeader($v_local_header)) != 1) {
                // ----- Close the zip file
                $this->privCloseFd();
                $v_temp_zip->privCloseFd();
                @unlink($v_zip_temp_name);

                // ----- Return
                return $v_result;
            }

            // ----- Check that local file header is same as central file header
            if ($this->privCheckFileHeaders($v_local_header,
			                                $v_header_list[$i]) != 1) {
                // TBC
            }
            unset($v_local_header);

            // ----- Write the file header
            if (($v_result = $v_temp_zip->privWriteFileHeader($v_header_list[$i])) != 1) {
                // ----- Close the zip file
                $this->privCloseFd();
                $v_temp_zip->privCloseFd();
                @unlink($v_zip_temp_name);

                // ----- Return
                return $v_result;
            }

            // ----- Read/write the data block
            if (($v_result = PclZipUtilCopyBlock($this->zip_fd, $v_temp_zip->zip_fd, $v_header_list[$i]['compressed_size'])) != 1) {
                // ----- Close the zip file
                $this->privCloseFd();
                $v_temp_zip->privCloseFd();
                @unlink($v_zip_temp_name);

                // ----- Return
                return $v_result;
            }
        }

        // ----- Store the offset of the central dir
        $v_offset = @ftell($v_temp_zip->zip_fd);

        // ----- Re-Create the Central Dir files header
        for ($i=0; $i<sizeof($v_header_list); $i++) {
            // ----- Create the file header
            if (($v_result = $v_temp_zip->privWriteCentralFileHeader($v_header_list[$i])) != 1) {
                $v_temp_zip->privCloseFd();
                $this->privCloseFd();
                @unlink($v_zip_temp_name);

                // ----- Return
                return $v_result;
            }

            // ----- Transform the header to a 'usable' info
            $v_temp_zip->privConvertHeader2FileInfo($v_header_list[$i], $p_result_list[$i]);
        }


        // ----- Zip file comment
        $v_comment = '';
        if (isset($p_options[PCLZIP_OPT_COMMENT])) {
          $v_comment = $p_options[PCLZIP_OPT_COMMENT];
        }

        // ----- Calculate the size of the central header
        $v_size = @ftell($v_temp_zip->zip_fd)-$v_offset;

        // ----- Create the central dir footer
        if (($v_result = $v_temp_zip->privWriteCentralHeader(sizeof($v_header_list), $v_size, $v_offset, $v_comment)) != 1) {
            // ----- Reset the file list
            unset($v_header_list);
            $v_temp_zip->privCloseFd();
            $this->privCloseFd();
            @unlink($v_zip_temp_name);

            // ----- Return
            return $v_result;
        }

        // ----- Close
        $v_temp_zip->privCloseFd();
        $this->privCloseFd();

        // ----- Delete the zip file
        // TBC : I should test the result ...
        @unlink($this->zipname);

        // ----- Rename the temporary file
        // TBC : I should test the result ...
        //@rename($v_zip_temp_name, $this->zipname);
        PclZipUtilRename($v_zip_temp_name, $this->zipname);

        // ----- Destroy the temporary archive
        unset($v_temp_zip);
    }

    // ----- Remove every files : reset the file
    else if ($v_central_dir['entries'] != 0) {
        $this->privCloseFd();

        if (($v_result = $this->privOpenFd('wb')) != 1) {
          return $v_result;
        }

        if (($v_result = $this->privWriteCentralHeader(0, 0, 0, '')) != 1) {
          return $v_result;
        }

        $this->privCloseFd();
    }

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privDirCheck()
  // Description :
  //   Check if a directory exists, if not it creates it and all the parents directory
  //   which may be useful.
  // Parameters :
  //   $p_dir : Directory path to check.
  // Return Values :
  //    1 : OK
  //   -1 : Unable to create directory
  // --------------------------------------------------------------------------------
  function privDirCheck($p_dir, $p_is_dir=false)
  {
    $v_result = 1;


    // ----- Remove the final '/'
    if (($p_is_dir) && (substr($p_dir, -1)=='/'))
    {
      $p_dir = substr($p_dir, 0, strlen($p_dir)-1);
    }

    // ----- Check the directory availability
    if ((is_dir($p_dir)) || ($p_dir == ""))
    {
      return 1;
    }

    // ----- Extract parent directory
    $p_parent_dir = dirname($p_dir);

    // ----- Just a check
    if ($p_parent_dir != $p_dir)
    {
      // ----- Look for parent directory
      if ($p_parent_dir != "")
      {
        if (($v_result = $this->privDirCheck($p_parent_dir)) != 1)
        {
          return $v_result;
        }
      }
    }

    // ----- Create the directory
    if (!@mkdir($p_dir, 0777))
    {
      // ----- Error log
      PclZip::privErrorLog(PCLZIP_ERR_DIR_CREATE_FAIL, "Unable to create directory '$p_dir'");

      // ----- Return
      return PclZip::errorCode();
    }

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privMerge()
  // Description :
  //   If $p_archive_to_add does not exist, the function exit with a success result.
  // Parameters :
  // Return Values :
  // --------------------------------------------------------------------------------
  function privMerge(&$p_archive_to_add)
  {
    $v_result=1;

    // ----- Look if the archive_to_add exists
    if (!is_file($p_archive_to_add->zipname))
    {

      // ----- Nothing to merge, so merge is a success
      $v_result = 1;

      // ----- Return
      return $v_result;
    }

    // ----- Look if the archive exists
    if (!is_file($this->zipname))
    {

      // ----- Do a duplicate
      $v_result = $this->privDuplicate($p_archive_to_add->zipname);

      // ----- Return
      return $v_result;
    }

    // ----- Open the zip file
    if (($v_result=$this->privOpenFd('rb')) != 1)
    {
      // ----- Return
      return $v_result;
    }

    // ----- Read the central directory information
    $v_central_dir = array();
    if (($v_result = $this->privReadEndCentralDir($v_central_dir)) != 1)
    {
      $this->privCloseFd();
      return $v_result;
    }

    // ----- Go to beginning of File
    @rewind($this->zip_fd);

    // ----- Open the archive_to_add file
    if (($v_result=$p_archive_to_add->privOpenFd('rb')) != 1)
    {
      $this->privCloseFd();

      // ----- Return
      return $v_result;
    }

    // ----- Read the central directory information
    $v_central_dir_to_add = array();
    if (($v_result = $p_archive_to_add->privReadEndCentralDir($v_central_dir_to_add)) != 1)
    {
      $this->privCloseFd();
      $p_archive_to_add->privCloseFd();

      return $v_result;
    }

    // ----- Go to beginning of File
    @rewind($p_archive_to_add->zip_fd);

    // ----- Creates a temporary file
    $v_zip_temp_name = PCLZIP_TEMPORARY_DIR.uniqid('pclzip-').'.tmp';

    // ----- Open the temporary file in write mode
    if (($v_zip_temp_fd = @fopen($v_zip_temp_name, 'wb')) == 0)
    {
      $this->privCloseFd();
      $p_archive_to_add->privCloseFd();

      PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open temporary file \''.$v_zip_temp_name.'\' in binary write mode');

      // ----- Return
      return PclZip::errorCode();
    }

    // ----- Copy the files from the archive to the temporary file
    // TBC : Here I should better append the file and go back to erase the central dir
    $v_size = $v_central_dir['offset'];
    while ($v_size != 0)
    {
      $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
      $v_buffer = fread($this->zip_fd, $v_read_size);
      @fwrite($v_zip_temp_fd, $v_buffer, $v_read_size);
      $v_size -= $v_read_size;
    }

    // ----- Copy the files from the archive_to_add into the temporary file
    $v_size = $v_central_dir_to_add['offset'];
    while ($v_size != 0)
    {
      $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
      $v_buffer = fread($p_archive_to_add->zip_fd, $v_read_size);
      @fwrite($v_zip_temp_fd, $v_buffer, $v_read_size);
      $v_size -= $v_read_size;
    }

    // ----- Store the offset of the central dir
    $v_offset = @ftell($v_zip_temp_fd);

    // ----- Copy the block of file headers from the old archive
    $v_size = $v_central_dir['size'];
    while ($v_size != 0)
    {
      $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
      $v_buffer = @fread($this->zip_fd, $v_read_size);
      @fwrite($v_zip_temp_fd, $v_buffer, $v_read_size);
      $v_size -= $v_read_size;
    }

    // ----- Copy the block of file headers from the archive_to_add
    $v_size = $v_central_dir_to_add['size'];
    while ($v_size != 0)
    {
      $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
      $v_buffer = @fread($p_archive_to_add->zip_fd, $v_read_size);
      @fwrite($v_zip_temp_fd, $v_buffer, $v_read_size);
      $v_size -= $v_read_size;
    }

    // ----- Merge the file comments
    $v_comment = $v_central_dir['comment'].' '.$v_central_dir_to_add['comment'];

    // ----- Calculate the size of the (new) central header
    $v_size = @ftell($v_zip_temp_fd)-$v_offset;

    // ----- Swap the file descriptor
    // Here is a trick : I swap the temporary fd with the zip fd, in order to use
    // the following methods on the temporary fil and not the real archive fd
    $v_swap = $this->zip_fd;
    $this->zip_fd = $v_zip_temp_fd;
    $v_zip_temp_fd = $v_swap;

    // ----- Create the central dir footer
    if (($v_result = $this->privWriteCentralHeader($v_central_dir['entries']+$v_central_dir_to_add['entries'], $v_size, $v_offset, $v_comment)) != 1)
    {
      $this->privCloseFd();
      $p_archive_to_add->privCloseFd();
      @fclose($v_zip_temp_fd);
      $this->zip_fd = null;

      // ----- Reset the file list
      unset($v_header_list);

      // ----- Return
      return $v_result;
    }

    // ----- Swap back the file descriptor
    $v_swap = $this->zip_fd;
    $this->zip_fd = $v_zip_temp_fd;
    $v_zip_temp_fd = $v_swap;

    // ----- Close
    $this->privCloseFd();
    $p_archive_to_add->privCloseFd();

    // ----- Close the temporary file
    @fclose($v_zip_temp_fd);

    // ----- Delete the zip file
    // TBC : I should test the result ...
    @unlink($this->zipname);

    // ----- Rename the temporary file
    // TBC : I should test the result ...
    //@rename($v_zip_temp_name, $this->zipname);
    PclZipUtilRename($v_zip_temp_name, $this->zipname);

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privDuplicate()
  // Description :
  // Parameters :
  // Return Values :
  // --------------------------------------------------------------------------------
  function privDuplicate($p_archive_filename)
  {
    $v_result=1;

    // ----- Look if the $p_archive_filename exists
    if (!is_file($p_archive_filename))
    {

      // ----- Nothing to duplicate, so duplicate is a success.
      $v_result = 1;

      // ----- Return
      return $v_result;
    }

    // ----- Open the zip file
    if (($v_result=$this->privOpenFd('wb')) != 1)
    {
      // ----- Return
      return $v_result;
    }

    // ----- Open the temporary file in write mode
    if (($v_zip_temp_fd = @fopen($p_archive_filename, 'rb')) == 0)
    {
      $this->privCloseFd();

      PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open archive file \''.$p_archive_filename.'\' in binary write mode');

      // ----- Return
      return PclZip::errorCode();
    }

    // ----- Copy the files from the archive to the temporary file
    // TBC : Here I should better append the file and go back to erase the central dir
    $v_size = filesize($p_archive_filename);
    while ($v_size != 0)
    {
      $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
      $v_buffer = fread($v_zip_temp_fd, $v_read_size);
      @fwrite($this->zip_fd, $v_buffer, $v_read_size);
      $v_size -= $v_read_size;
    }

    // ----- Close
    $this->privCloseFd();

    // ----- Close the temporary file
    @fclose($v_zip_temp_fd);

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privErrorLog()
  // Description :
  // Parameters :
  // --------------------------------------------------------------------------------
  function privErrorLog($p_error_code=0, $p_error_string='')
  {
    if (PCLZIP_ERROR_EXTERNAL == 1) {
      PclError($p_error_code, $p_error_string);
    }
    else {
      $this->error_code = $p_error_code;
      $this->error_string = $p_error_string;
    }
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privErrorReset()
  // Description :
  // Parameters :
  // --------------------------------------------------------------------------------
  function privErrorReset()
  {
    if (PCLZIP_ERROR_EXTERNAL == 1) {
      PclErrorReset();
    }
    else {
      $this->error_code = 0;
      $this->error_string = '';
    }
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privDisableMagicQuotes()
  // Description :
  // Parameters :
  // Return Values :
  // --------------------------------------------------------------------------------
  function privDisableMagicQuotes()
  {
    $v_result=1;

	// EDIT for WordPress 5.3.0
	// magic_quote functions are deprecated in PHP 7.4, now assuming it's always off.
	/*

    // ----- Look if function exists
    if (   (!function_exists("get_magic_quotes_runtime"))
	    || (!function_exists("set_magic_quotes_runtime"))) {
      return $v_result;
	}

    // ----- Look if already done
    if ($this->magic_quotes_status != -1) {
      return $v_result;
	}

	// ----- Get and memorize the magic_quote value
	$this->magic_quotes_status = @get_magic_quotes_runtime();

	// ----- Disable magic_quotes
	if ($this->magic_quotes_status == 1) {
	  @set_magic_quotes_runtime(0);
	}
	*/

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : privSwapBackMagicQuotes()
  // Description :
  // Parameters :
  // Return Values :
  // --------------------------------------------------------------------------------
  function privSwapBackMagicQuotes()
  {
    $v_result=1;

	// EDIT for WordPress 5.3.0
	// magic_quote functions are deprecated in PHP 7.4, now assuming it's always off.
	/*

    // ----- Look if function exists
    if (   (!function_exists("get_magic_quotes_runtime"))
	    || (!function_exists("set_magic_quotes_runtime"))) {
      return $v_result;
	}

    // ----- Look if something to do
    if ($this->magic_quotes_status != -1) {
      return $v_result;
	}

	// ----- Swap back magic_quotes
	if ($this->magic_quotes_status == 1) {
  	  @set_magic_quotes_runtime($this->magic_quotes_status);
	}

	*/
    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  }
  // End of class
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : PclZipUtilPathReduction()
  // Description :
  // Parameters :
  // Return Values :
  // --------------------------------------------------------------------------------
  function PclZipUtilPathReduction($p_dir)
  {
    $v_result = "";

    // ----- Look for not empty path
    if ($p_dir != "") {
      // ----- Explode path by directory names
      $v_list = explode("/", $p_dir);

      // ----- Study directories from last to first
      $v_skip = 0;
      for ($i=sizeof($v_list)-1; $i>=0; $i--) {
        // ----- Look for current path
        if ($v_list[$i] == ".") {
          // ----- Ignore this directory
          // Should be the first $i=0, but no check is done
        }
        else if ($v_list[$i] == "..") {
		  $v_skip++;
        }
        else if ($v_list[$i] == "") {
		  // ----- First '/' i.e. root slash
		  if ($i == 0) {
            $v_result = "/".$v_result;
		    if ($v_skip > 0) {
		        // ----- It is an invalid path, so the path is not modified
		        // TBC
		        $v_result = $p_dir;
                $v_skip = 0;
		    }
		  }
		  // ----- Last '/' i.e. indicates a directory
		  else if ($i == (sizeof($v_list)-1)) {
            $v_result = $v_list[$i];
		  }
		  // ----- Double '/' inside the path
		  else {
            // ----- Ignore only the double '//' in path,
            // but not the first and last '/'
		  }
        }
        else {
		  // ----- Look for item to skip
		  if ($v_skip > 0) {
		    $v_skip--;
		  }
		  else {
            $v_result = $v_list[$i].($i!=(sizeof($v_list)-1)?"/".$v_result:"");
		  }
        }
      }

      // ----- Look for skip
      if ($v_skip > 0) {
        while ($v_skip > 0) {
            $v_result = '../'.$v_result;
            $v_skip--;
        }
      }
    }

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : PclZipUtilPathInclusion()
  // Description :
  //   This function indicates if the path $p_path is under the $p_dir tree. Or,
  //   said in an other way, if the file or sub-dir $p_path is inside the dir
  //   $p_dir.
  //   The function indicates also if the path is exactly the same as the dir.
  //   This function supports path with duplicated '/' like '//', but does not
  //   support '.' or '..' statements.
  // Parameters :
  // Return Values :
  //   0 if $p_path is not inside directory $p_dir
  //   1 if $p_path is inside directory $p_dir
  //   2 if $p_path is exactly the same as $p_dir
  // --------------------------------------------------------------------------------
  function PclZipUtilPathInclusion($p_dir, $p_path)
  {
    $v_result = 1;

    // ----- Look for path beginning by ./
    if (   ($p_dir == '.')
        || ((strlen($p_dir) >=2) && (substr($p_dir, 0, 2) == './'))) {
      $p_dir = PclZipUtilTranslateWinPath(getcwd(), FALSE).'/'.substr($p_dir, 1);
    }
    if (   ($p_path == '.')
        || ((strlen($p_path) >=2) && (substr($p_path, 0, 2) == './'))) {
      $p_path = PclZipUtilTranslateWinPath(getcwd(), FALSE).'/'.substr($p_path, 1);
    }

    // ----- Explode dir and path by directory separator
    $v_list_dir = explode("/", $p_dir);
    $v_list_dir_size = sizeof($v_list_dir);
    $v_list_path = explode("/", $p_path);
    $v_list_path_size = sizeof($v_list_path);

    // ----- Study directories paths
    $i = 0;
    $j = 0;
    while (($i < $v_list_dir_size) && ($j < $v_list_path_size) && ($v_result)) {

      // ----- Look for empty dir (path reduction)
      if ($v_list_dir[$i] == '') {
        $i++;
        continue;
      }
      if ($v_list_path[$j] == '') {
        $j++;
        continue;
      }

      // ----- Compare the items
      if (($v_list_dir[$i] != $v_list_path[$j]) && ($v_list_dir[$i] != '') && ( $v_list_path[$j] != ''))  {
        $v_result = 0;
      }

      // ----- Next items
      $i++;
      $j++;
    }

    // ----- Look if everything seems to be the same
    if ($v_result) {
      // ----- Skip all the empty items
      while (($j < $v_list_path_size) && ($v_list_path[$j] == '')) $j++;
      while (($i < $v_list_dir_size) && ($v_list_dir[$i] == '')) $i++;

      if (($i >= $v_list_dir_size) && ($j >= $v_list_path_size)) {
        // ----- There are exactly the same
        $v_result = 2;
      }
      else if ($i < $v_list_dir_size) {
        // ----- The path is shorter than the dir
        $v_result = 0;
      }
    }

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : PclZipUtilCopyBlock()
  // Description :
  // Parameters :
  //   $p_mode : read/write compression mode
  //             0 : src & dest normal
  //             1 : src gzip, dest normal
  //             2 : src normal, dest gzip
  //             3 : src & dest gzip
  // Return Values :
  // --------------------------------------------------------------------------------
  function PclZipUtilCopyBlock($p_src, $p_dest, $p_size, $p_mode=0)
  {
    $v_result = 1;

    if ($p_mode==0)
    {
      while ($p_size != 0)
      {
        $v_read_size = ($p_size < PCLZIP_READ_BLOCK_SIZE ? $p_size : PCLZIP_READ_BLOCK_SIZE);
        $v_buffer = @fread($p_src, $v_read_size);
        @fwrite($p_dest, $v_buffer, $v_read_size);
        $p_size -= $v_read_size;
      }
    }
    else if ($p_mode==1)
    {
      while ($p_size != 0)
      {
        $v_read_size = ($p_size < PCLZIP_READ_BLOCK_SIZE ? $p_size : PCLZIP_READ_BLOCK_SIZE);
        $v_buffer = @gzread($p_src, $v_read_size);
        @fwrite($p_dest, $v_buffer, $v_read_size);
        $p_size -= $v_read_size;
      }
    }
    else if ($p_mode==2)
    {
      while ($p_size != 0)
      {
        $v_read_size = ($p_size < PCLZIP_READ_BLOCK_SIZE ? $p_size : PCLZIP_READ_BLOCK_SIZE);
        $v_buffer = @fread($p_src, $v_read_size);
        @gzwrite($p_dest, $v_buffer, $v_read_size);
        $p_size -= $v_read_size;
      }
    }
    else if ($p_mode==3)
    {
      while ($p_size != 0)
      {
        $v_read_size = ($p_size < PCLZIP_READ_BLOCK_SIZE ? $p_size : PCLZIP_READ_BLOCK_SIZE);
        $v_buffer = @gzread($p_src, $v_read_size);
        @gzwrite($p_dest, $v_buffer, $v_read_size);
        $p_size -= $v_read_size;
      }
    }

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : PclZipUtilRename()
  // Description :
  //   This function tries to do a simple rename() function. If it fails, it
  //   tries to copy the $p_src file in a new $p_dest file and then unlink the
  //   first one.
  // Parameters :
  //   $p_src : Old filename
  //   $p_dest : New filename
  // Return Values :
  //   1 on success, 0 on failure.
  // --------------------------------------------------------------------------------
  function PclZipUtilRename($p_src, $p_dest)
  {
    $v_result = 1;

    // ----- Try to rename the files
    if (!@rename($p_src, $p_dest)) {

      // ----- Try to copy & unlink the src
      if (!@copy($p_src, $p_dest)) {
        $v_result = 0;
      }
      else if (!@unlink($p_src)) {
        $v_result = 0;
      }
    }

    // ----- Return
    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : PclZipUtilOptionText()
  // Description :
  //   Translate option value in text. Mainly for debug purpose.
  // Parameters :
  //   $p_option : the option value.
  // Return Values :
  //   The option text value.
  // --------------------------------------------------------------------------------
  function PclZipUtilOptionText($p_option)
  {

    $v_list = get_defined_constants();
    for (reset($v_list); $v_key = key($v_list); next($v_list)) {
	    $v_prefix = substr($v_key, 0, 10);
	    if ((   ($v_prefix == 'PCLZIP_OPT')
           || ($v_prefix == 'PCLZIP_CB_')
           || ($v_prefix == 'PCLZIP_ATT'))
	        && ($v_list[$v_key] == $p_option)) {
        return $v_key;
	    }
    }

    $v_result = 'Unknown';

    return $v_result;
  }
  // --------------------------------------------------------------------------------

  // --------------------------------------------------------------------------------
  // Function : PclZipUtilTranslateWinPath()
  // Description :
  //   Translate windows path by replacing '\' by '/' and optionally removing
  //   drive letter.
  // Parameters :
  //   $p_path : path to translate.
  //   $p_remove_disk_letter : true | false
  // Return Values :
  //   The path translated.
  // --------------------------------------------------------------------------------
  function PclZipUtilTranslateWinPath($p_path, $p_remove_disk_letter=true)
  {
    if (PHP_OS_FAMILY == 'Windows') {
      // ----- Look for potential disk letter
      if (($p_remove_disk_letter) && (($v_position = strpos($p_path, ':')) != false)) {
          $p_path = substr($p_path, $v_position+1);
      }
      // ----- Change potential windows directory separator
      if ((strpos($p_path, '\\') > 0) || (substr($p_path, 0,1) == '\\')) {
          $p_path = strtr($p_path, '\\', '/');
      }
    }
    return $p_path;
  }
  // --------------------------------------------------------------------------------


?>
admin-filters.php000064400000017542151327343360010033 0ustar00<?php
/**
 * Administration API: Default admin hooks
 *
 * @package WordPress
 * @subpackage Administration
 * @since 4.3.0
 */

// Bookmark hooks.
add_action( 'admin_page_access_denied', 'wp_link_manager_disabled_message' );

// Dashboard hooks.
add_action( 'activity_box_end', 'wp_dashboard_quota' );
add_action( 'welcome_panel', 'wp_welcome_panel' );

// Media hooks.
add_action( 'attachment_submitbox_misc_actions', 'attachment_submitbox_metadata' );
add_filter( 'plupload_init', 'wp_show_heic_upload_error' );

add_action( 'media_upload_image', 'wp_media_upload_handler' );
add_action( 'media_upload_audio', 'wp_media_upload_handler' );
add_action( 'media_upload_video', 'wp_media_upload_handler' );
add_action( 'media_upload_file', 'wp_media_upload_handler' );

add_action( 'post-plupload-upload-ui', 'media_upload_flash_bypass' );

add_action( 'post-html-upload-ui', 'media_upload_html_bypass' );

add_filter( 'async_upload_image', 'get_media_item', 10, 2 );
add_filter( 'async_upload_audio', 'get_media_item', 10, 2 );
add_filter( 'async_upload_video', 'get_media_item', 10, 2 );
add_filter( 'async_upload_file', 'get_media_item', 10, 2 );

add_filter( 'media_upload_gallery', 'media_upload_gallery' );
add_filter( 'media_upload_library', 'media_upload_library' );

add_filter( 'media_upload_tabs', 'update_gallery_tab' );

// Admin color schemes.
add_action( 'admin_init', 'register_admin_color_schemes', 1 );
add_action( 'admin_head', 'wp_color_scheme_settings' );
add_action( 'admin_color_scheme_picker', 'admin_color_scheme_picker' );

// Misc hooks.
add_action( 'admin_init', 'wp_admin_headers' );
add_action( 'admin_init', 'send_frame_options_header', 10, 0 );
add_action( 'admin_head', 'wp_admin_canonical_url' );
add_action( 'admin_head', 'wp_site_icon' );
add_action( 'admin_head', 'wp_admin_viewport_meta' );
add_action( 'customize_controls_head', 'wp_admin_viewport_meta' );
add_filter( 'nav_menu_meta_box_object', '_wp_nav_menu_meta_box_object' );

// Prerendering.
if ( ! is_customize_preview() ) {
	add_filter( 'admin_print_styles', 'wp_resource_hints', 1 );
}

add_action( 'admin_print_scripts', 'print_emoji_detection_script' );
add_action( 'admin_print_scripts', 'print_head_scripts', 20 );
add_action( 'admin_print_footer_scripts', '_wp_footer_scripts' );
add_action( 'admin_enqueue_scripts', 'wp_enqueue_emoji_styles' );
add_action( 'admin_print_styles', 'print_emoji_styles' ); // Retained for backwards-compatibility. Unhooked by wp_enqueue_emoji_styles().
add_action( 'admin_print_styles', 'print_admin_styles', 20 );

add_action( 'admin_print_scripts-index.php', 'wp_localize_community_events' );
add_action( 'admin_print_scripts-post.php', 'wp_page_reload_on_back_button_js' );
add_action( 'admin_print_scripts-post-new.php', 'wp_page_reload_on_back_button_js' );

add_action( 'update_option_home', 'update_home_siteurl', 10, 2 );
add_action( 'update_option_siteurl', 'update_home_siteurl', 10, 2 );
add_action( 'update_option_page_on_front', 'update_home_siteurl', 10, 2 );
add_action( 'update_option_admin_email', 'wp_site_admin_email_change_notification', 10, 3 );

add_action( 'add_option_new_admin_email', 'update_option_new_admin_email', 10, 2 );
add_action( 'update_option_new_admin_email', 'update_option_new_admin_email', 10, 2 );

add_filter( 'heartbeat_received', 'wp_check_locked_posts', 10, 3 );
add_filter( 'heartbeat_received', 'wp_refresh_post_lock', 10, 3 );
add_filter( 'heartbeat_received', 'heartbeat_autosave', 500, 2 );

add_filter( 'wp_refresh_nonces', 'wp_refresh_post_nonces', 10, 3 );
add_filter( 'wp_refresh_nonces', 'wp_refresh_metabox_loader_nonces', 10, 2 );
add_filter( 'wp_refresh_nonces', 'wp_refresh_heartbeat_nonces' );

add_filter( 'heartbeat_settings', 'wp_heartbeat_set_suspension' );

add_action( 'use_block_editor_for_post_type', '_disable_block_editor_for_navigation_post_type', 10, 2 );
add_action( 'edit_form_after_title', '_disable_content_editor_for_navigation_post_type' );
add_action( 'edit_form_after_editor', '_enable_content_editor_for_navigation_post_type' );

// Nav Menu hooks.
add_action( 'admin_head-nav-menus.php', '_wp_delete_orphaned_draft_menu_items' );

// Plugin hooks.
add_filter( 'allowed_options', 'option_update_filter' );

// Plugin Install hooks.
add_action( 'install_plugins_featured', 'install_dashboard' );
add_action( 'install_plugins_upload', 'install_plugins_upload' );
add_action( 'install_plugins_search', 'display_plugins_table' );
add_action( 'install_plugins_popular', 'display_plugins_table' );
add_action( 'install_plugins_recommended', 'display_plugins_table' );
add_action( 'install_plugins_new', 'display_plugins_table' );
add_action( 'install_plugins_beta', 'display_plugins_table' );
add_action( 'install_plugins_favorites', 'display_plugins_table' );
add_action( 'install_plugins_pre_plugin-information', 'install_plugin_information' );

// Template hooks.
add_action( 'admin_enqueue_scripts', array( 'WP_Internal_Pointers', 'enqueue_scripts' ) );
add_action( 'user_register', array( 'WP_Internal_Pointers', 'dismiss_pointers_for_new_users' ) );

// Theme hooks.
add_action( 'customize_controls_print_footer_scripts', 'customize_themes_print_templates' );

// Theme Install hooks.
add_action( 'install_themes_pre_theme-information', 'install_theme_information' );

// User hooks.
add_action( 'admin_init', 'default_password_nag_handler' );

add_action( 'admin_notices', 'default_password_nag' );
add_action( 'admin_notices', 'new_user_email_admin_notice' );

add_action( 'profile_update', 'default_password_nag_edit_user', 10, 2 );

add_action( 'personal_options_update', 'send_confirmation_on_profile_email' );

// Update hooks.
add_action( 'load-plugins.php', 'wp_plugin_update_rows', 20 ); // After wp_update_plugins() is called.
add_action( 'load-themes.php', 'wp_theme_update_rows', 20 ); // After wp_update_themes() is called.

add_action( 'admin_notices', 'update_nag', 3 );
add_action( 'admin_notices', 'deactivated_plugins_notice', 5 );
add_action( 'admin_notices', 'paused_plugins_notice', 5 );
add_action( 'admin_notices', 'paused_themes_notice', 5 );
add_action( 'admin_notices', 'maintenance_nag', 10 );
add_action( 'admin_notices', 'wp_recovery_mode_nag', 1 );

add_filter( 'update_footer', 'core_update_footer' );

// Update Core hooks.
add_action( '_core_updated_successfully', '_redirect_to_about_wordpress' );

// Upgrade hooks.
add_action( 'upgrader_process_complete', array( 'Language_Pack_Upgrader', 'async_upgrade' ), 20 );
add_action( 'upgrader_process_complete', 'wp_version_check', 10, 0 );
add_action( 'upgrader_process_complete', 'wp_update_plugins', 10, 0 );
add_action( 'upgrader_process_complete', 'wp_update_themes', 10, 0 );

// Privacy hooks.
add_filter( 'wp_privacy_personal_data_erasure_page', 'wp_privacy_process_personal_data_erasure_page', 10, 5 );
add_filter( 'wp_privacy_personal_data_export_page', 'wp_privacy_process_personal_data_export_page', 10, 7 );
add_action( 'wp_privacy_personal_data_export_file', 'wp_privacy_generate_personal_data_export_file', 10 );
add_action( 'wp_privacy_personal_data_erased', '_wp_privacy_send_erasure_fulfillment_notification', 10 );

// Privacy policy text changes check.
add_action( 'admin_init', array( 'WP_Privacy_Policy_Content', 'text_change_check' ), 100 );

// Show a "postbox" with the text suggestions for a privacy policy.
add_action( 'admin_notices', array( 'WP_Privacy_Policy_Content', 'notice' ) );

// Add the suggested policy text from WordPress.
add_action( 'admin_init', array( 'WP_Privacy_Policy_Content', 'add_suggested_content' ), 1 );

// Update the cached policy info when the policy page is updated.
add_action( 'post_updated', array( 'WP_Privacy_Policy_Content', '_policy_page_updated' ) );

// Append '(Draft)' to draft page titles in the privacy page dropdown.
add_filter( 'list_pages', '_wp_privacy_settings_filter_draft_page_titles', 10, 2 );

// Font management.
add_action( 'admin_print_styles', 'wp_print_font_faces', 50 );
add_action( 'admin_print_styles', 'wp_print_font_faces_from_style_variations', 50 );
class-wp-upgrader-skin.php000060400000015706151327343360011573 0ustar00<?php
/**
 * Upgrader API: WP_Upgrader_Skin class
 *
 * @package WordPress
 * @subpackage Upgrader
 * @since 4.6.0
 */

/**
 * Generic Skin for the WordPress Upgrader classes. This skin is designed to be extended for specific purposes.
 *
 * @since 2.8.0
 * @since 4.6.0 Moved to its own file from wp-admin/includes/class-wp-upgrader-skins.php.
 */
#[AllowDynamicProperties]
class WP_Upgrader_Skin {

	/**
	 * Holds the upgrader data.
	 *
	 * @since 2.8.0
	 * @var WP_Upgrader
	 */
	public $upgrader;

	/**
	 * Whether header is done.
	 *
	 * @since 2.8.0
	 * @var bool
	 */
	public $done_header = false;

	/**
	 * Whether footer is done.
	 *
	 * @since 2.8.0
	 * @var bool
	 */
	public $done_footer = false;

	/**
	 * Holds the result of an upgrade.
	 *
	 * @since 2.8.0
	 * @var string|bool|WP_Error
	 */
	public $result = false;

	/**
	 * Holds the options of an upgrade.
	 *
	 * @since 2.8.0
	 * @var array
	 */
	public $options = array();

	/**
	 * Constructor.
	 *
	 * Sets up the generic skin for the WordPress Upgrader classes.
	 *
	 * @since 2.8.0
	 *
	 * @param array $args Optional. The WordPress upgrader skin arguments to
	 *                    override default options. Default empty array.
	 */
	public function __construct( $args = array() ) {
		$defaults      = array(
			'url'     => '',
			'nonce'   => '',
			'title'   => '',
			'context' => false,
		);
		$this->options = wp_parse_args( $args, $defaults );
	}

	/**
	 * Sets the relationship between the skin being used and the upgrader.
	 *
	 * @since 2.8.0
	 *
	 * @param WP_Upgrader $upgrader
	 */
	public function set_upgrader( &$upgrader ) {
		if ( is_object( $upgrader ) ) {
			$this->upgrader =& $upgrader;
		}
		$this->add_strings();
	}

	/**
	 * Sets up the strings used in the update process.
	 *
	 * @since 3.0.0
	 */
	public function add_strings() {
	}

	/**
	 * Sets the result of an upgrade.
	 *
	 * @since 2.8.0
	 *
	 * @param string|bool|WP_Error $result The result of an upgrade.
	 */
	public function set_result( $result ) {
		$this->result = $result;
	}

	/**
	 * Displays a form to the user to request for their FTP/SSH details in order
	 * to connect to the filesystem.
	 *
	 * @since 2.8.0
	 * @since 4.6.0 The `$context` parameter default changed from `false` to an empty string.
	 *
	 * @see request_filesystem_credentials()
	 *
	 * @param bool|WP_Error $error                        Optional. Whether the current request has failed to connect,
	 *                                                    or an error object. Default false.
	 * @param string        $context                      Optional. Full path to the directory that is tested
	 *                                                    for being writable. Default empty.
	 * @param bool          $allow_relaxed_file_ownership Optional. Whether to allow Group/World writable. Default false.
	 * @return bool True on success, false on failure.
	 */
	public function request_filesystem_credentials( $error = false, $context = '', $allow_relaxed_file_ownership = false ) {
		$url = $this->options['url'];
		if ( ! $context ) {
			$context = $this->options['context'];
		}
		if ( ! empty( $this->options['nonce'] ) ) {
			$url = wp_nonce_url( $url, $this->options['nonce'] );
		}

		$extra_fields = array();

		return request_filesystem_credentials( $url, '', $error, $context, $extra_fields, $allow_relaxed_file_ownership );
	}

	/**
	 * Displays the header before the update process.
	 *
	 * @since 2.8.0
	 */
	public function header() {
		if ( $this->done_header ) {
			return;
		}
		$this->done_header = true;
		echo '<div class="wrap">';
		echo '<h1>' . $this->options['title'] . '</h1>';
	}

	/**
	 * Displays the footer following the update process.
	 *
	 * @since 2.8.0
	 */
	public function footer() {
		if ( $this->done_footer ) {
			return;
		}
		$this->done_footer = true;
		echo '</div>';
	}

	/**
	 * Displays an error message about the update.
	 *
	 * @since 2.8.0
	 *
	 * @param string|WP_Error $errors Errors.
	 */
	public function error( $errors ) {
		if ( ! $this->done_header ) {
			$this->header();
		}
		if ( is_string( $errors ) ) {
			$this->feedback( $errors );
		} elseif ( is_wp_error( $errors ) && $errors->has_errors() ) {
			foreach ( $errors->get_error_messages() as $message ) {
				if ( $errors->get_error_data() && is_string( $errors->get_error_data() ) ) {
					$this->feedback( $message . ' ' . esc_html( strip_tags( $errors->get_error_data() ) ) );
				} else {
					$this->feedback( $message );
				}
			}
		}
	}

	/**
	 * Displays a message about the update.
	 *
	 * @since 2.8.0
	 * @since 5.9.0 Renamed `$string` (a PHP reserved keyword) to `$feedback` for PHP 8 named parameter support.
	 *
	 * @param string $feedback Message data.
	 * @param mixed  ...$args  Optional text replacements.
	 */
	public function feedback( $feedback, ...$args ) {
		if ( isset( $this->upgrader->strings[ $feedback ] ) ) {
			$feedback = $this->upgrader->strings[ $feedback ];
		}

		if ( str_contains( $feedback, '%' ) ) {
			if ( $args ) {
				$args     = array_map( 'strip_tags', $args );
				$args     = array_map( 'esc_html', $args );
				$feedback = vsprintf( $feedback, $args );
			}
		}
		if ( empty( $feedback ) ) {
			return;
		}
		show_message( $feedback );
	}

	/**
	 * Performs an action before an update.
	 *
	 * @since 2.8.0
	 */
	public function before() {}

	/**
	 * Performs an action following an update.
	 *
	 * @since 2.8.0
	 */
	public function after() {}

	/**
	 * Outputs JavaScript that calls function to decrement the update counts.
	 *
	 * @since 3.9.0
	 *
	 * @param string $type Type of update count to decrement. Likely values include 'plugin',
	 *                     'theme', 'translation', etc.
	 */
	protected function decrement_update_count( $type ) {
		if ( ! $this->result || is_wp_error( $this->result ) || 'up_to_date' === $this->result ) {
			return;
		}

		if ( defined( 'IFRAME_REQUEST' ) ) {
			echo '<script type="text/javascript">
					if ( window.postMessage && JSON ) {
						window.parent.postMessage(
							JSON.stringify( {
								action: "decrementUpdateCount",
								upgradeType: "' . $type . '"
							} ),
							window.location.protocol + "//" + window.location.hostname
								+ ( "" !== window.location.port ? ":" + window.location.port : "" )
						);
					}
				</script>';
		} else {
			echo '<script type="text/javascript">
					(function( wp ) {
						if ( wp && wp.updates && wp.updates.decrementCount ) {
							wp.updates.decrementCount( "' . $type . '" );
						}
					})( window.wp );
				</script>';
		}
	}

	/**
	 * Displays the header before the bulk update process.
	 *
	 * @since 3.0.0
	 */
	public function bulk_header() {}

	/**
	 * Displays the footer following the bulk update process.
	 *
	 * @since 3.0.0
	 */
	public function bulk_footer() {}

	/**
	 * Hides the `process_failed` error message when updating by uploading a zip file.
	 *
	 * @since 5.5.0
	 *
	 * @param WP_Error $wp_error WP_Error object.
	 * @return bool True if the error should be hidden, false otherwise.
	 */
	public function hide_process_failed( $wp_error ) {
		return false;
	}
}
class-language-pack-upgrader-skin.php000060400000005466151327343360013646 0ustar00<?php
/**
 * Upgrader API: Language_Pack_Upgrader_Skin class
 *
 * @package WordPress
 * @subpackage Upgrader
 * @since 4.6.0
 */

/**
 * Translation Upgrader Skin for WordPress Translation Upgrades.
 *
 * @since 3.7.0
 * @since 4.6.0 Moved to its own file from wp-admin/includes/class-wp-upgrader-skins.php.
 *
 * @see WP_Upgrader_Skin
 */
class Language_Pack_Upgrader_Skin extends WP_Upgrader_Skin {
	public $language_update        = null;
	public $done_header            = false;
	public $done_footer            = false;
	public $display_footer_actions = true;

	/**
	 * Constructor.
	 *
	 * Sets up the language pack upgrader skin.
	 *
	 * @since 3.7.0
	 *
	 * @param array $args
	 */
	public function __construct( $args = array() ) {
		$defaults = array(
			'url'                => '',
			'nonce'              => '',
			'title'              => __( 'Update Translations' ),
			'skip_header_footer' => false,
		);
		$args     = wp_parse_args( $args, $defaults );
		if ( $args['skip_header_footer'] ) {
			$this->done_header            = true;
			$this->done_footer            = true;
			$this->display_footer_actions = false;
		}
		parent::__construct( $args );
	}

	/**
	 * Performs an action before a language pack update.
	 *
	 * @since 3.7.0
	 */
	public function before() {
		$name = $this->upgrader->get_name_for_update( $this->language_update );

		echo '<div class="update-messages lp-show-latest">';

		/* translators: 1: Project name (plugin, theme, or WordPress), 2: Language. */
		printf( '<h2>' . __( 'Updating translations for %1$s (%2$s)&#8230;' ) . '</h2>', $name, $this->language_update->language );
	}

	/**
	 * Displays an error message about the update.
	 *
	 * @since 3.7.0
	 * @since 5.9.0 Renamed `$error` to `$errors` for PHP 8 named parameter support.
	 *
	 * @param string|WP_Error $errors Errors.
	 */
	public function error( $errors ) {
		echo '<div class="lp-error">';
		parent::error( $errors );
		echo '</div>';
	}

	/**
	 * Performs an action following a language pack update.
	 *
	 * @since 3.7.0
	 */
	public function after() {
		echo '</div>';
	}

	/**
	 * Displays the footer following the bulk update process.
	 *
	 * @since 3.7.0
	 */
	public function bulk_footer() {
		$this->decrement_update_count( 'translation' );

		$update_actions = array(
			'updates_page' => sprintf(
				'<a href="%s" target="_parent">%s</a>',
				self_admin_url( 'update-core.php' ),
				__( 'Go to WordPress Updates page' )
			),
		);

		/**
		 * Filters the list of action links available following a translations update.
		 *
		 * @since 3.7.0
		 *
		 * @param string[] $update_actions Array of translations update links.
		 */
		$update_actions = apply_filters( 'update_translations_complete_actions', $update_actions );

		if ( $update_actions && $this->display_footer_actions ) {
			$this->feedback( implode( ' | ', $update_actions ) );
		}
	}
}
class-ftp-sockets.php000060400000020437151327343360010633 0ustar00<?php
/**
 * PemFTP - An Ftp implementation in pure PHP
 *
 * @package PemFTP
 * @since 2.5.0
 *
 * @version 1.0
 * @copyright Alexey Dotsenko
 * @author Alexey Dotsenko
 * @link https://www.phpclasses.org/package/1743-PHP-FTP-client-in-pure-PHP.html
 * @license LGPL https://opensource.org/licenses/lgpl-license.html
 */

/**
 * Socket Based FTP implementation
 *
 * @package PemFTP
 * @subpackage Socket
 * @since 2.5.0
 *
 * @version 1.0
 * @copyright Alexey Dotsenko
 * @author Alexey Dotsenko
 * @link https://www.phpclasses.org/package/1743-PHP-FTP-client-in-pure-PHP.html
 * @license LGPL https://opensource.org/licenses/lgpl-license.html
 */
class ftp_sockets extends ftp_base {

	function __construct($verb=FALSE, $le=FALSE) {
		parent::__construct(true, $verb, $le);
	}

// <!-- --------------------------------------------------------------------------------------- -->
// <!--       Private functions                                                                 -->
// <!-- --------------------------------------------------------------------------------------- -->

	function _settimeout($sock) {
		if(!@socket_set_option($sock, SOL_SOCKET, SO_RCVTIMEO, array("sec"=>$this->_timeout, "usec"=>0))) {
			$this->PushError('_connect','socket set receive timeout',socket_strerror(socket_last_error($sock)));
			@socket_close($sock);
			return FALSE;
		}
		if(!@socket_set_option($sock, SOL_SOCKET , SO_SNDTIMEO, array("sec"=>$this->_timeout, "usec"=>0))) {
			$this->PushError('_connect','socket set send timeout',socket_strerror(socket_last_error($sock)));
			@socket_close($sock);
			return FALSE;
		}
		return true;
	}

	function _connect($host, $port) {
		$this->SendMSG("Creating socket");
		if(!($sock = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP))) {
			$this->PushError('_connect','socket create failed',socket_strerror(socket_last_error($sock)));
			return FALSE;
		}
		if(!$this->_settimeout($sock)) return FALSE;
		$this->SendMSG("Connecting to \"".$host.":".$port."\"");
		if (!($res = @socket_connect($sock, $host, $port))) {
			$this->PushError('_connect','socket connect failed',socket_strerror(socket_last_error($sock)));
			@socket_close($sock);
			return FALSE;
		}
		$this->_connected=true;
		return $sock;
	}

	function _readmsg($fnction="_readmsg"){
		if(!$this->_connected) {
			$this->PushError($fnction,'Connect first');
			return FALSE;
		}
		$result=true;
		$this->_message="";
		$this->_code=0;
		$go=true;
		do {
			$tmp=@socket_read($this->_ftp_control_sock, 4096, PHP_BINARY_READ);
			if($tmp===false) {
				$go=$result=false;
				$this->PushError($fnction,'Read failed', socket_strerror(socket_last_error($this->_ftp_control_sock)));
			} else {
				$this->_message.=$tmp;
				$go = !preg_match("/^([0-9]{3})(-.+\\1)? [^".CRLF."]+".CRLF."$/Us", $this->_message, $regs);
			}
		} while($go);
		if($this->LocalEcho) echo "GET < ".rtrim($this->_message, CRLF).CRLF;
		$this->_code=(int)$regs[1];
		return $result;
	}

	function _exec($cmd, $fnction="_exec") {
		if(!$this->_ready) {
			$this->PushError($fnction,'Connect first');
			return FALSE;
		}
		if($this->LocalEcho) echo "PUT > ",$cmd,CRLF;
		$status=@socket_write($this->_ftp_control_sock, $cmd.CRLF);
		if($status===false) {
			$this->PushError($fnction,'socket write failed', socket_strerror(socket_last_error($this->stream)));
			return FALSE;
		}
		$this->_lastaction=time();
		if(!$this->_readmsg($fnction)) return FALSE;
		return TRUE;
	}

	function _data_prepare($mode=FTP_ASCII) {
		if(!$this->_settype($mode)) return FALSE;
		$this->SendMSG("Creating data socket");
		$this->_ftp_data_sock = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
		if ($this->_ftp_data_sock < 0) {
			$this->PushError('_data_prepare','socket create failed',socket_strerror(socket_last_error($this->_ftp_data_sock)));
			return FALSE;
		}
		if(!$this->_settimeout($this->_ftp_data_sock)) {
			$this->_data_close();
			return FALSE;
		}
		if($this->_passive) {
			if(!$this->_exec("PASV", "pasv")) {
				$this->_data_close();
				return FALSE;
			}
			if(!$this->_checkCode()) {
				$this->_data_close();
				return FALSE;
			}
			$ip_port = explode(",", preg_replace("/^.+ \\(?([0-9]{1,3},[0-9]{1,3},[0-9]{1,3},[0-9]{1,3},[0-9]+,[0-9]+)\\)?.*$/s", "\\1", $this->_message));
			$this->_datahost=$ip_port[0].".".$ip_port[1].".".$ip_port[2].".".$ip_port[3];
			$this->_dataport=(((int)$ip_port[4])<<8) + ((int)$ip_port[5]);
			$this->SendMSG("Connecting to ".$this->_datahost.":".$this->_dataport);
			if(!@socket_connect($this->_ftp_data_sock, $this->_datahost, $this->_dataport)) {
				$this->PushError("_data_prepare","socket_connect", socket_strerror(socket_last_error($this->_ftp_data_sock)));
				$this->_data_close();
				return FALSE;
			}
			else $this->_ftp_temp_sock=$this->_ftp_data_sock;
		} else {
			if(!@socket_getsockname($this->_ftp_control_sock, $addr, $port)) {
				$this->PushError("_data_prepare","cannot get control socket information", socket_strerror(socket_last_error($this->_ftp_control_sock)));
				$this->_data_close();
				return FALSE;
			}
			if(!@socket_bind($this->_ftp_data_sock,$addr)){
				$this->PushError("_data_prepare","cannot bind data socket", socket_strerror(socket_last_error($this->_ftp_data_sock)));
				$this->_data_close();
				return FALSE;
			}
			if(!@socket_listen($this->_ftp_data_sock)) {
				$this->PushError("_data_prepare","cannot listen data socket", socket_strerror(socket_last_error($this->_ftp_data_sock)));
				$this->_data_close();
				return FALSE;
			}
			if(!@socket_getsockname($this->_ftp_data_sock, $this->_datahost, $this->_dataport)) {
				$this->PushError("_data_prepare","cannot get data socket information", socket_strerror(socket_last_error($this->_ftp_data_sock)));
				$this->_data_close();
				return FALSE;
			}
			if(!$this->_exec('PORT '.str_replace('.',',',$this->_datahost.'.'.($this->_dataport>>8).'.'.($this->_dataport&0x00FF)), "_port")) {
				$this->_data_close();
				return FALSE;
			}
			if(!$this->_checkCode()) {
				$this->_data_close();
				return FALSE;
			}
		}
		return TRUE;
	}

	function _data_read($mode=FTP_ASCII, $fp=NULL) {
		$NewLine=$this->_eol_code[$this->OS_local];
		if(is_resource($fp)) $out=0;
		else $out="";
		if(!$this->_passive) {
			$this->SendMSG("Connecting to ".$this->_datahost.":".$this->_dataport);
			$this->_ftp_temp_sock=socket_accept($this->_ftp_data_sock);
			if($this->_ftp_temp_sock===FALSE) {
				$this->PushError("_data_read","socket_accept", socket_strerror(socket_last_error($this->_ftp_temp_sock)));
				$this->_data_close();
				return FALSE;
			}
		}

		while(($block=@socket_read($this->_ftp_temp_sock, $this->_ftp_buff_size, PHP_BINARY_READ))!==false) {
			if($block==="") break;
			if($mode!=FTP_BINARY) $block=preg_replace("/\r\n|\r|\n/", $this->_eol_code[$this->OS_local], $block);
			if(is_resource($fp)) $out+=fwrite($fp, $block, strlen($block));
			else $out.=$block;
		}
		return $out;
	}

	function _data_write($mode=FTP_ASCII, $fp=NULL) {
		$NewLine=$this->_eol_code[$this->OS_local];
		if(is_resource($fp)) $out=0;
		else $out="";
		if(!$this->_passive) {
			$this->SendMSG("Connecting to ".$this->_datahost.":".$this->_dataport);
			$this->_ftp_temp_sock=socket_accept($this->_ftp_data_sock);
			if($this->_ftp_temp_sock===FALSE) {
				$this->PushError("_data_write","socket_accept", socket_strerror(socket_last_error($this->_ftp_temp_sock)));
				$this->_data_close();
				return false;
			}
		}
		if(is_resource($fp)) {
			while(!feof($fp)) {
				$block=fread($fp, $this->_ftp_buff_size);
				if(!$this->_data_write_block($mode, $block)) return false;
			}
		} elseif(!$this->_data_write_block($mode, $fp)) return false;
		return true;
	}

	function _data_write_block($mode, $block) {
		if($mode!=FTP_BINARY) $block=preg_replace("/\r\n|\r|\n/", $this->_eol_code[$this->OS_remote], $block);
		do {
			if(($t=@socket_write($this->_ftp_temp_sock, $block))===FALSE) {
				$this->PushError("_data_write","socket_write", socket_strerror(socket_last_error($this->_ftp_temp_sock)));
				$this->_data_close();
				return FALSE;
			}
			$block=substr($block, $t);
		} while(!empty($block));
		return true;
	}

	function _data_close() {
		@socket_close($this->_ftp_temp_sock);
		@socket_close($this->_ftp_data_sock);
		$this->SendMSG("Disconnected data from remote host");
		return TRUE;
	}

	function _quit() {
		if($this->_connected) {
			@socket_close($this->_ftp_control_sock);
			$this->_connected=false;
			$this->SendMSG("Socket closed");
		}
	}
}
?>
upgrade.php000064400000340135151327343360006721 0ustar00<?php
/**
 * WordPress Upgrade API
 *
 * Most of the functions are pluggable and can be overwritten.
 *
 * @package WordPress
 * @subpackage Administration
 */

/** Include user installation customization script. */
if ( file_exists( WP_CONTENT_DIR . '/install.php' ) ) {
	require WP_CONTENT_DIR . '/install.php';
}

/** WordPress Administration API */
require_once ABSPATH . 'wp-admin/includes/admin.php';

/** WordPress Schema API */
require_once ABSPATH . 'wp-admin/includes/schema.php';

if ( ! function_exists( 'wp_install' ) ) :
	/**
	 * Installs the site.
	 *
	 * Runs the required functions to set up and populate the database,
	 * including primary admin user and initial options.
	 *
	 * @since 2.1.0
	 *
	 * @param string $blog_title    Site title.
	 * @param string $user_name     User's username.
	 * @param string $user_email    User's email.
	 * @param bool   $is_public     Whether the site is public.
	 * @param string $deprecated    Optional. Not used.
	 * @param string $user_password Optional. User's chosen password. Default empty (random password).
	 * @param string $language      Optional. Language chosen. Default empty.
	 * @return array {
	 *     Data for the newly installed site.
	 *
	 *     @type string $url              The URL of the site.
	 *     @type int    $user_id          The ID of the site owner.
	 *     @type string $password         The password of the site owner, if their user account didn't already exist.
	 *     @type string $password_message The explanatory message regarding the password.
	 * }
	 */
	function wp_install(
		$blog_title,
		$user_name,
		$user_email,
		$is_public,
		$deprecated = '',
		#[\SensitiveParameter]
		$user_password = '',
		$language = ''
	) {
		if ( ! empty( $deprecated ) ) {
			_deprecated_argument( __FUNCTION__, '2.6.0' );
		}

		wp_check_mysql_version();
		wp_cache_flush();
		make_db_current_silent();

		/*
		 * Ensure update checks are delayed after installation.
		 *
		 * This prevents users being presented with a maintenance mode screen
		 * immediately after installation.
		 */
		wp_unschedule_hook( 'wp_version_check' );
		wp_unschedule_hook( 'wp_update_plugins' );
		wp_unschedule_hook( 'wp_update_themes' );

		wp_schedule_event( time() + HOUR_IN_SECONDS, 'twicedaily', 'wp_version_check' );
		wp_schedule_event( time() + ( 1.5 * HOUR_IN_SECONDS ), 'twicedaily', 'wp_update_plugins' );
		wp_schedule_event( time() + ( 2 * HOUR_IN_SECONDS ), 'twicedaily', 'wp_update_themes' );

		populate_options();
		populate_roles();

		update_option( 'blogname', $blog_title );
		update_option( 'admin_email', $user_email );
		update_option( 'blog_public', $is_public );

		// Freshness of site - in the future, this could get more specific about actions taken, perhaps.
		update_option( 'fresh_site', 1, false );

		if ( $language ) {
			update_option( 'WPLANG', $language );
		}

		$guessurl = wp_guess_url();

		update_option( 'siteurl', $guessurl );

		// If not a public site, don't ping.
		if ( ! $is_public ) {
			update_option( 'default_pingback_flag', 0 );
		}

		/*
		 * Create default user. If the user already exists, the user tables are
		 * being shared among sites. Just set the role in that case.
		 */
		$user_id        = username_exists( $user_name );
		$user_password  = trim( $user_password );
		$email_password = false;
		$user_created   = false;

		if ( ! $user_id && empty( $user_password ) ) {
			$user_password = wp_generate_password( 12, false );
			$message       = __( '<strong><em>Note that password</em></strong> carefully! It is a <em>random</em> password that was generated just for you.' );
			$user_id       = wp_create_user( $user_name, $user_password, $user_email );
			update_user_meta( $user_id, 'default_password_nag', true );
			$email_password = true;
			$user_created   = true;
		} elseif ( ! $user_id ) {
			// Password has been provided.
			$message      = '<em>' . __( 'Your chosen password.' ) . '</em>';
			$user_id      = wp_create_user( $user_name, $user_password, $user_email );
			$user_created = true;
		} else {
			$message = __( 'User already exists. Password inherited.' );
		}

		$user = new WP_User( $user_id );
		$user->set_role( 'administrator' );

		if ( $user_created ) {
			$user->user_url = $guessurl;
			wp_update_user( $user );
		}

		wp_install_defaults( $user_id );

		wp_install_maybe_enable_pretty_permalinks();

		flush_rewrite_rules();

		wp_new_blog_notification( $blog_title, $guessurl, $user_id, ( $email_password ? $user_password : __( 'The password you chose during installation.' ) ) );

		wp_cache_flush();

		/**
		 * Fires after a site is fully installed.
		 *
		 * @since 3.9.0
		 *
		 * @param WP_User $user The site owner.
		 */
		do_action( 'wp_install', $user );

		return array(
			'url'              => $guessurl,
			'user_id'          => $user_id,
			'password'         => $user_password,
			'password_message' => $message,
		);
	}
endif;

if ( ! function_exists( 'wp_install_defaults' ) ) :
	/**
	 * Creates the initial content for a newly-installed site.
	 *
	 * Adds the default "Uncategorized" category, the first post (with comment),
	 * first page, and default widgets for default theme for the current version.
	 *
	 * @since 2.1.0
	 *
	 * @global wpdb       $wpdb         WordPress database abstraction object.
	 * @global WP_Rewrite $wp_rewrite   WordPress rewrite component.
	 * @global string     $table_prefix The database table prefix.
	 *
	 * @param int $user_id User ID.
	 */
	function wp_install_defaults( $user_id ) {
		global $wpdb, $wp_rewrite, $table_prefix;

		// Default category.
		$cat_name = __( 'Uncategorized' );
		/* translators: Default category slug. */
		$cat_slug = sanitize_title( _x( 'Uncategorized', 'Default category slug' ) );

		$cat_id = 1;

		$wpdb->insert(
			$wpdb->terms,
			array(
				'term_id'    => $cat_id,
				'name'       => $cat_name,
				'slug'       => $cat_slug,
				'term_group' => 0,
			)
		);
		$wpdb->insert(
			$wpdb->term_taxonomy,
			array(
				'term_id'     => $cat_id,
				'taxonomy'    => 'category',
				'description' => '',
				'parent'      => 0,
				'count'       => 1,
			)
		);
		$cat_tt_id = $wpdb->insert_id;

		// First post.
		$now             = current_time( 'mysql' );
		$now_gmt         = current_time( 'mysql', 1 );
		$first_post_guid = get_option( 'home' ) . '/?p=1';

		if ( is_multisite() ) {
			$first_post = get_site_option( 'first_post' );

			if ( ! $first_post ) {
				$first_post = "<!-- wp:paragraph -->\n<p>" .
				/* translators: First post content. %s: Site link. */
				__( 'Welcome to %s. This is your first post. Edit or delete it, then start writing!' ) .
				"</p>\n<!-- /wp:paragraph -->";
			}

			$first_post = sprintf(
				$first_post,
				sprintf( '<a href="%s">%s</a>', esc_url( network_home_url() ), get_network()->site_name )
			);

			// Back-compat for pre-4.4.
			$first_post = str_replace( 'SITE_URL', esc_url( network_home_url() ), $first_post );
			$first_post = str_replace( 'SITE_NAME', get_network()->site_name, $first_post );
		} else {
			$first_post = "<!-- wp:paragraph -->\n<p>" .
			/* translators: First post content. %s: Site link. */
			__( 'Welcome to WordPress. This is your first post. Edit or delete it, then start writing!' ) .
			"</p>\n<!-- /wp:paragraph -->";
		}

		$wpdb->insert(
			$wpdb->posts,
			array(
				'post_author'           => $user_id,
				'post_date'             => $now,
				'post_date_gmt'         => $now_gmt,
				'post_content'          => $first_post,
				'post_excerpt'          => '',
				'post_title'            => __( 'Hello world!' ),
				/* translators: Default post slug. */
				'post_name'             => sanitize_title( _x( 'hello-world', 'Default post slug' ) ),
				'post_modified'         => $now,
				'post_modified_gmt'     => $now_gmt,
				'guid'                  => $first_post_guid,
				'comment_count'         => 1,
				'to_ping'               => '',
				'pinged'                => '',
				'post_content_filtered' => '',
			)
		);

		if ( is_multisite() ) {
			update_posts_count();
		}

		$wpdb->insert(
			$wpdb->term_relationships,
			array(
				'term_taxonomy_id' => $cat_tt_id,
				'object_id'        => 1,
			)
		);

		// Default comment.
		if ( is_multisite() ) {
			$first_comment_author = get_site_option( 'first_comment_author' );
			$first_comment_email  = get_site_option( 'first_comment_email' );
			$first_comment_url    = get_site_option( 'first_comment_url', network_home_url() );
			$first_comment        = get_site_option( 'first_comment' );
		}

		$first_comment_author = ! empty( $first_comment_author ) ? $first_comment_author : __( 'A WordPress Commenter' );
		$first_comment_email  = ! empty( $first_comment_email ) ? $first_comment_email : 'wapuu@wordpress.example';
		$first_comment_url    = ! empty( $first_comment_url ) ? $first_comment_url : esc_url( __( 'https://wordpress.org/' ) );
		$first_comment        = ! empty( $first_comment ) ? $first_comment : sprintf(
			/* translators: %s: Gravatar URL. */
			__(
				'Hi, this is a comment.
To get started with moderating, editing, and deleting comments, please visit the Comments screen in the dashboard.
Commenter avatars come from <a href="%s">Gravatar</a>.'
			),
			/* translators: The localized Gravatar URL. */
			esc_url( __( 'https://gravatar.com/' ) )
		);
		$wpdb->insert(
			$wpdb->comments,
			array(
				'comment_post_ID'      => 1,
				'comment_author'       => $first_comment_author,
				'comment_author_email' => $first_comment_email,
				'comment_author_url'   => $first_comment_url,
				'comment_date'         => $now,
				'comment_date_gmt'     => $now_gmt,
				'comment_content'      => $first_comment,
				'comment_type'         => 'comment',
			)
		);

		// First page.
		if ( is_multisite() ) {
			$first_page = get_site_option( 'first_page' );
		}

		if ( empty( $first_page ) ) {
			$first_page = "<!-- wp:paragraph -->\n<p>";
			/* translators: First page content. */
			$first_page .= __( "This is an example page. It's different from a blog post because it will stay in one place and will show up in your site navigation (in most themes). Most people start with an About page that introduces them to potential site visitors. It might say something like this:" );
			$first_page .= "</p>\n<!-- /wp:paragraph -->\n\n";

			$first_page .= "<!-- wp:quote -->\n<blockquote class=\"wp-block-quote\"><p>";
			/* translators: First page content. */
			$first_page .= __( "Hi there! I'm a bike messenger by day, aspiring actor by night, and this is my website. I live in Los Angeles, have a great dog named Jack, and I like pi&#241;a coladas. (And gettin' caught in the rain.)" );
			$first_page .= "</p></blockquote>\n<!-- /wp:quote -->\n\n";

			$first_page .= "<!-- wp:paragraph -->\n<p>";
			/* translators: First page content. */
			$first_page .= __( '...or something like this:' );
			$first_page .= "</p>\n<!-- /wp:paragraph -->\n\n";

			$first_page .= "<!-- wp:quote -->\n<blockquote class=\"wp-block-quote\"><p>";
			/* translators: First page content. */
			$first_page .= __( 'The XYZ Doohickey Company was founded in 1971, and has been providing quality doohickeys to the public ever since. Located in Gotham City, XYZ employs over 2,000 people and does all kinds of awesome things for the Gotham community.' );
			$first_page .= "</p></blockquote>\n<!-- /wp:quote -->\n\n";

			$first_page .= "<!-- wp:paragraph -->\n<p>";
			$first_page .= sprintf(
				/* translators: First page content. %s: Site admin URL. */
				__( 'As a new WordPress user, you should go to <a href="%s">your dashboard</a> to delete this page and create new pages for your content. Have fun!' ),
				admin_url()
			);
			$first_page .= "</p>\n<!-- /wp:paragraph -->";
		}

		$first_post_guid = get_option( 'home' ) . '/?page_id=2';
		$wpdb->insert(
			$wpdb->posts,
			array(
				'post_author'           => $user_id,
				'post_date'             => $now,
				'post_date_gmt'         => $now_gmt,
				'post_content'          => $first_page,
				'post_excerpt'          => '',
				'comment_status'        => 'closed',
				'post_title'            => __( 'Sample Page' ),
				/* translators: Default page slug. */
				'post_name'             => __( 'sample-page' ),
				'post_modified'         => $now,
				'post_modified_gmt'     => $now_gmt,
				'guid'                  => $first_post_guid,
				'post_type'             => 'page',
				'to_ping'               => '',
				'pinged'                => '',
				'post_content_filtered' => '',
			)
		);
		$wpdb->insert(
			$wpdb->postmeta,
			array(
				'post_id'    => 2,
				'meta_key'   => '_wp_page_template',
				'meta_value' => 'default',
			)
		);

		// Privacy Policy page.
		if ( is_multisite() ) {
			// Disable by default unless the suggested content is provided.
			$privacy_policy_content = get_site_option( 'default_privacy_policy_content' );
		} else {
			if ( ! class_exists( 'WP_Privacy_Policy_Content' ) ) {
				require_once ABSPATH . 'wp-admin/includes/class-wp-privacy-policy-content.php';
			}

			$privacy_policy_content = WP_Privacy_Policy_Content::get_default_content();
		}

		if ( ! empty( $privacy_policy_content ) ) {
			$privacy_policy_guid = get_option( 'home' ) . '/?page_id=3';

			$wpdb->insert(
				$wpdb->posts,
				array(
					'post_author'           => $user_id,
					'post_date'             => $now,
					'post_date_gmt'         => $now_gmt,
					'post_content'          => $privacy_policy_content,
					'post_excerpt'          => '',
					'comment_status'        => 'closed',
					'post_title'            => __( 'Privacy Policy' ),
					/* translators: Privacy Policy page slug. */
					'post_name'             => __( 'privacy-policy' ),
					'post_modified'         => $now,
					'post_modified_gmt'     => $now_gmt,
					'guid'                  => $privacy_policy_guid,
					'post_type'             => 'page',
					'post_status'           => 'draft',
					'to_ping'               => '',
					'pinged'                => '',
					'post_content_filtered' => '',
				)
			);
			$wpdb->insert(
				$wpdb->postmeta,
				array(
					'post_id'    => 3,
					'meta_key'   => '_wp_page_template',
					'meta_value' => 'default',
				)
			);
			update_option( 'wp_page_for_privacy_policy', 3 );
		}

		// Set up default widgets for default theme.
		update_option(
			'widget_block',
			array(
				2              => array( 'content' => '<!-- wp:search /-->' ),
				3              => array( 'content' => '<!-- wp:group --><div class="wp-block-group"><!-- wp:heading --><h2>' . __( 'Recent Posts' ) . '</h2><!-- /wp:heading --><!-- wp:latest-posts /--></div><!-- /wp:group -->' ),
				4              => array( 'content' => '<!-- wp:group --><div class="wp-block-group"><!-- wp:heading --><h2>' . __( 'Recent Comments' ) . '</h2><!-- /wp:heading --><!-- wp:latest-comments {"displayAvatar":false,"displayDate":false,"displayExcerpt":false} /--></div><!-- /wp:group -->' ),
				5              => array( 'content' => '<!-- wp:group --><div class="wp-block-group"><!-- wp:heading --><h2>' . __( 'Archives' ) . '</h2><!-- /wp:heading --><!-- wp:archives /--></div><!-- /wp:group -->' ),
				6              => array( 'content' => '<!-- wp:group --><div class="wp-block-group"><!-- wp:heading --><h2>' . __( 'Categories' ) . '</h2><!-- /wp:heading --><!-- wp:categories /--></div><!-- /wp:group -->' ),
				'_multiwidget' => 1,
			)
		);
		update_option(
			'sidebars_widgets',
			array(
				'wp_inactive_widgets' => array(),
				'sidebar-1'           => array(
					0 => 'block-2',
					1 => 'block-3',
					2 => 'block-4',
				),
				'sidebar-2'           => array(
					0 => 'block-5',
					1 => 'block-6',
				),
				'array_version'       => 3,
			)
		);

		if ( ! is_multisite() ) {
			update_user_meta( $user_id, 'show_welcome_panel', 1 );
		} elseif ( ! is_super_admin( $user_id ) && ! metadata_exists( 'user', $user_id, 'show_welcome_panel' ) ) {
			update_user_meta( $user_id, 'show_welcome_panel', 2 );
		}

		if ( is_multisite() ) {
			// Flush rules to pick up the new page.
			$wp_rewrite->init();
			$wp_rewrite->flush_rules();

			$user = new WP_User( $user_id );
			$wpdb->update( $wpdb->options, array( 'option_value' => $user->user_email ), array( 'option_name' => 'admin_email' ) );

			// Remove all perms except for the login user.
			$wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->usermeta WHERE user_id != %d AND meta_key = %s", $user_id, $table_prefix . 'user_level' ) );
			$wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->usermeta WHERE user_id != %d AND meta_key = %s", $user_id, $table_prefix . 'capabilities' ) );

			/*
			 * Delete any caps that snuck into the previously active blog. (Hardcoded to blog 1 for now.)
			 * TODO: Get previous_blog_id.
			 */
			if ( ! is_super_admin( $user_id ) && 1 !== $user_id ) {
				$wpdb->delete(
					$wpdb->usermeta,
					array(
						'user_id'  => $user_id,
						'meta_key' => $wpdb->base_prefix . '1_capabilities',
					)
				);
			}
		}
	}
endif;

/**
 * Maybe enable pretty permalinks on installation.
 *
 * If after enabling pretty permalinks don't work, fallback to query-string permalinks.
 *
 * @since 4.2.0
 *
 * @global WP_Rewrite $wp_rewrite WordPress rewrite component.
 *
 * @return bool Whether pretty permalinks are enabled. False otherwise.
 */
function wp_install_maybe_enable_pretty_permalinks() {
	global $wp_rewrite;

	// Bail if a permalink structure is already enabled.
	if ( get_option( 'permalink_structure' ) ) {
		return true;
	}

	/*
	 * The Permalink structures to attempt.
	 *
	 * The first is designed for mod_rewrite or nginx rewriting.
	 *
	 * The second is PATHINFO-based permalinks for web server configurations
	 * without a true rewrite module enabled.
	 */
	$permalink_structures = array(
		'/%year%/%monthnum%/%day%/%postname%/',
		'/index.php/%year%/%monthnum%/%day%/%postname%/',
	);

	foreach ( (array) $permalink_structures as $permalink_structure ) {
		$wp_rewrite->set_permalink_structure( $permalink_structure );

		/*
		 * Flush rules with the hard option to force refresh of the web-server's
		 * rewrite config file (e.g. .htaccess or web.config).
		 */
		$wp_rewrite->flush_rules( true );

		$test_url = '';

		// Test against a real WordPress post.
		$first_post = get_page_by_path( sanitize_title( _x( 'hello-world', 'Default post slug' ) ), OBJECT, 'post' );
		if ( $first_post ) {
			$test_url = get_permalink( $first_post->ID );
		}

		/*
		 * Send a request to the site, and check whether
		 * the 'X-Pingback' header is returned as expected.
		 *
		 * Uses wp_remote_get() instead of wp_remote_head() because web servers
		 * can block head requests.
		 */
		$response          = wp_remote_get( $test_url, array( 'timeout' => 5 ) );
		$x_pingback_header = wp_remote_retrieve_header( $response, 'X-Pingback' );
		$pretty_permalinks = $x_pingback_header && get_bloginfo( 'pingback_url' ) === $x_pingback_header;

		if ( $pretty_permalinks ) {
			return true;
		}
	}

	/*
	 * If it makes it this far, pretty permalinks failed.
	 * Fallback to query-string permalinks.
	 */
	$wp_rewrite->set_permalink_structure( '' );
	$wp_rewrite->flush_rules( true );

	return false;
}

if ( ! function_exists( 'wp_new_blog_notification' ) ) :
	/**
	 * Notifies the site admin that the installation of WordPress is complete.
	 *
	 * Sends an email to the new administrator that the installation is complete
	 * and provides them with a record of their login credentials.
	 *
	 * @since 2.1.0
	 *
	 * @param string $blog_title Site title.
	 * @param string $blog_url   Site URL.
	 * @param int    $user_id    Administrator's user ID.
	 * @param string $password   Administrator's password. Note that a placeholder message is
	 *                           usually passed instead of the actual password.
	 */
	function wp_new_blog_notification(
		$blog_title,
		$blog_url,
		$user_id,
		#[\SensitiveParameter]
		$password
	) {
		$user      = new WP_User( $user_id );
		$email     = $user->user_email;
		$name      = $user->user_login;
		$login_url = wp_login_url();

		$message = sprintf(
			/* translators: New site notification email. 1: New site URL, 2: User login, 3: User password or password reset link, 4: Login URL. */
			__(
				'Your new WordPress site has been successfully set up at:

%1$s

You can log in to the administrator account with the following information:

Username: %2$s
Password: %3$s
Log in here: %4$s

We hope you enjoy your new site. Thanks!

--The WordPress Team
https://wordpress.org/
'
			),
			$blog_url,
			$name,
			$password,
			$login_url
		);

		$installed_email = array(
			'to'      => $email,
			'subject' => __( 'New WordPress Site' ),
			'message' => $message,
			'headers' => '',
		);

		/**
		 * Filters the contents of the email sent to the site administrator when WordPress is installed.
		 *
		 * @since 5.6.0
		 *
		 * @param array $installed_email {
		 *     Used to build wp_mail().
		 *
		 *     @type string $to      The email address of the recipient.
		 *     @type string $subject The subject of the email.
		 *     @type string $message The content of the email.
		 *     @type string $headers Headers.
		 * }
		 * @param WP_User $user          The site administrator user object.
		 * @param string  $blog_title    The site title.
		 * @param string  $blog_url      The site URL.
		 * @param string  $password      The site administrator's password. Note that a placeholder message
		 *                               is usually passed instead of the user's actual password.
		 */
		$installed_email = apply_filters( 'wp_installed_email', $installed_email, $user, $blog_title, $blog_url, $password );

		wp_mail(
			$installed_email['to'],
			$installed_email['subject'],
			$installed_email['message'],
			$installed_email['headers']
		);
	}
endif;

if ( ! function_exists( 'wp_upgrade' ) ) :
	/**
	 * Runs WordPress Upgrade functions.
	 *
	 * Upgrades the database if needed during a site update.
	 *
	 * @since 2.1.0
	 *
	 * @global int $wp_current_db_version The old (current) database version.
	 * @global int $wp_db_version         The new database version.
	 */
	function wp_upgrade() {
		global $wp_current_db_version, $wp_db_version;

		$wp_current_db_version = (int) __get_option( 'db_version' );

		// We are up to date. Nothing to do.
		if ( $wp_db_version === $wp_current_db_version ) {
			return;
		}

		if ( ! is_blog_installed() ) {
			return;
		}

		wp_check_mysql_version();
		wp_cache_flush();
		pre_schema_upgrade();
		make_db_current_silent();
		upgrade_all();
		if ( is_multisite() && is_main_site() ) {
			upgrade_network();
		}
		wp_cache_flush();

		if ( is_multisite() ) {
			update_site_meta( get_current_blog_id(), 'db_version', $wp_db_version );
			update_site_meta( get_current_blog_id(), 'db_last_updated', microtime() );
		}

		delete_transient( 'wp_core_block_css_files' );

		/**
		 * Fires after a site is fully upgraded.
		 *
		 * @since 3.9.0
		 *
		 * @param int $wp_db_version         The new $wp_db_version.
		 * @param int $wp_current_db_version The old (current) $wp_db_version.
		 */
		do_action( 'wp_upgrade', $wp_db_version, $wp_current_db_version );
	}
endif;

/**
 * Functions to be called in installation and upgrade scripts.
 *
 * Contains conditional checks to determine which upgrade scripts to run,
 * based on database version and WP version being updated-to.
 *
 * @ignore
 * @since 1.0.1
 *
 * @global int $wp_current_db_version The old (current) database version.
 * @global int $wp_db_version         The new database version.
 */
function upgrade_all() {
	global $wp_current_db_version, $wp_db_version;

	$wp_current_db_version = (int) __get_option( 'db_version' );

	// We are up to date. Nothing to do.
	if ( $wp_db_version === $wp_current_db_version ) {
		return;
	}

	// If the version is not set in the DB, try to guess the version.
	if ( empty( $wp_current_db_version ) ) {
		$wp_current_db_version = 0;

		// If the template option exists, we have 1.5.
		$template = __get_option( 'template' );
		if ( ! empty( $template ) ) {
			$wp_current_db_version = 2541;
		}
	}

	if ( $wp_current_db_version < 6039 ) {
		upgrade_230_options_table();
	}

	populate_options();

	if ( $wp_current_db_version < 2541 ) {
		upgrade_100();
		upgrade_101();
		upgrade_110();
		upgrade_130();
	}

	if ( $wp_current_db_version < 3308 ) {
		upgrade_160();
	}

	if ( $wp_current_db_version < 4772 ) {
		upgrade_210();
	}

	if ( $wp_current_db_version < 4351 ) {
		upgrade_old_slugs();
	}

	if ( $wp_current_db_version < 5539 ) {
		upgrade_230();
	}

	if ( $wp_current_db_version < 6124 ) {
		upgrade_230_old_tables();
	}

	if ( $wp_current_db_version < 7499 ) {
		upgrade_250();
	}

	if ( $wp_current_db_version < 7935 ) {
		upgrade_252();
	}

	if ( $wp_current_db_version < 8201 ) {
		upgrade_260();
	}

	if ( $wp_current_db_version < 8989 ) {
		upgrade_270();
	}

	if ( $wp_current_db_version < 10360 ) {
		upgrade_280();
	}

	if ( $wp_current_db_version < 11958 ) {
		upgrade_290();
	}

	if ( $wp_current_db_version < 15260 ) {
		upgrade_300();
	}

	if ( $wp_current_db_version < 19389 ) {
		upgrade_330();
	}

	if ( $wp_current_db_version < 20080 ) {
		upgrade_340();
	}

	if ( $wp_current_db_version < 22422 ) {
		upgrade_350();
	}

	if ( $wp_current_db_version < 25824 ) {
		upgrade_370();
	}

	if ( $wp_current_db_version < 26148 ) {
		upgrade_372();
	}

	if ( $wp_current_db_version < 26691 ) {
		upgrade_380();
	}

	if ( $wp_current_db_version < 29630 ) {
		upgrade_400();
	}

	if ( $wp_current_db_version < 33055 ) {
		upgrade_430();
	}

	if ( $wp_current_db_version < 33056 ) {
		upgrade_431();
	}

	if ( $wp_current_db_version < 35700 ) {
		upgrade_440();
	}

	if ( $wp_current_db_version < 36686 ) {
		upgrade_450();
	}

	if ( $wp_current_db_version < 37965 ) {
		upgrade_460();
	}

	if ( $wp_current_db_version < 44719 ) {
		upgrade_510();
	}

	if ( $wp_current_db_version < 45744 ) {
		upgrade_530();
	}

	if ( $wp_current_db_version < 48575 ) {
		upgrade_550();
	}

	if ( $wp_current_db_version < 49752 ) {
		upgrade_560();
	}

	if ( $wp_current_db_version < 51917 ) {
		upgrade_590();
	}

	if ( $wp_current_db_version < 53011 ) {
		upgrade_600();
	}

	if ( $wp_current_db_version < 55853 ) {
		upgrade_630();
	}

	if ( $wp_current_db_version < 56657 ) {
		upgrade_640();
	}

	if ( $wp_current_db_version < 57155 ) {
		upgrade_650();
	}

	if ( $wp_current_db_version < 58975 ) {
		upgrade_670();
	}

	if ( $wp_current_db_version < 60421 ) {
		upgrade_682();
	}

	maybe_disable_link_manager();

	maybe_disable_automattic_widgets();

	update_option( 'db_version', $wp_db_version );
	update_option( 'db_upgraded', true );
}

/**
 * Execute changes made in WordPress 1.0.
 *
 * @ignore
 * @since 1.0.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 */
function upgrade_100() {
	global $wpdb;

	// Get the title and ID of every post, post_name to check if it already has a value.
	$posts = $wpdb->get_results( "SELECT ID, post_title, post_name FROM $wpdb->posts WHERE post_name = ''" );
	if ( $posts ) {
		foreach ( $posts as $post ) {
			if ( '' === $post->post_name ) {
				$newtitle = sanitize_title( $post->post_title );
				$wpdb->query( $wpdb->prepare( "UPDATE $wpdb->posts SET post_name = %s WHERE ID = %d", $newtitle, $post->ID ) );
			}
		}
	}

	$categories = $wpdb->get_results( "SELECT cat_ID, cat_name, category_nicename FROM $wpdb->categories" );
	foreach ( $categories as $category ) {
		if ( '' === $category->category_nicename ) {
			$newtitle = sanitize_title( $category->cat_name );
			$wpdb->update( $wpdb->categories, array( 'category_nicename' => $newtitle ), array( 'cat_ID' => $category->cat_ID ) );
		}
	}

	$sql = "UPDATE $wpdb->options
		SET option_value = REPLACE(option_value, 'wp-links/links-images/', 'wp-images/links/')
		WHERE option_name LIKE %s
		AND option_value LIKE %s";
	$wpdb->query( $wpdb->prepare( $sql, $wpdb->esc_like( 'links_rating_image' ) . '%', $wpdb->esc_like( 'wp-links/links-images/' ) . '%' ) );

	$done_ids = $wpdb->get_results( "SELECT DISTINCT post_id FROM $wpdb->post2cat" );
	if ( $done_ids ) :
		$done_posts = array();
		foreach ( $done_ids as $done_id ) :
			$done_posts[] = $done_id->post_id;
		endforeach;
		$catwhere = ' AND ID NOT IN (' . implode( ',', $done_posts ) . ')';
	else :
		$catwhere = '';
	endif;

	$allposts = $wpdb->get_results( "SELECT ID, post_category FROM $wpdb->posts WHERE post_category != '0' $catwhere" );
	if ( $allposts ) :
		foreach ( $allposts as $post ) {
			// Check to see if it's already been imported.
			$cat = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->post2cat WHERE post_id = %d AND category_id = %d", $post->ID, $post->post_category ) );
			if ( ! $cat && 0 !== (int) $post->post_category ) { // If there's no result.
				$wpdb->insert(
					$wpdb->post2cat,
					array(
						'post_id'     => $post->ID,
						'category_id' => $post->post_category,
					)
				);
			}
		}
	endif;
}

/**
 * Execute changes made in WordPress 1.0.1.
 *
 * @ignore
 * @since 1.0.1
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 */
function upgrade_101() {
	global $wpdb;

	// Clean up indices, add a few.
	add_clean_index( $wpdb->posts, 'post_name' );
	add_clean_index( $wpdb->posts, 'post_status' );
	add_clean_index( $wpdb->categories, 'category_nicename' );
	add_clean_index( $wpdb->comments, 'comment_approved' );
	add_clean_index( $wpdb->comments, 'comment_post_ID' );
	add_clean_index( $wpdb->links, 'link_category' );
	add_clean_index( $wpdb->links, 'link_visible' );
}

/**
 * Execute changes made in WordPress 1.2.
 *
 * @ignore
 * @since 1.2.0
 * @since 6.8.0 User passwords are no longer hashed with md5.
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 */
function upgrade_110() {
	global $wpdb;

	// Set user_nicename.
	$users = $wpdb->get_results( "SELECT ID, user_nickname, user_nicename FROM $wpdb->users" );
	foreach ( $users as $user ) {
		if ( '' === $user->user_nicename ) {
			$newname = sanitize_title( $user->user_nickname );
			$wpdb->update( $wpdb->users, array( 'user_nicename' => $newname ), array( 'ID' => $user->ID ) );
		}
	}

	// Get the GMT offset, we'll use that later on.
	$all_options = get_alloptions_110();

	$time_difference = $all_options->time_difference;

	$server_time    = time() + (int) gmdate( 'Z' );
	$weblogger_time = $server_time + $time_difference * HOUR_IN_SECONDS;
	$gmt_time       = time();

	$diff_gmt_server       = ( $gmt_time - $server_time ) / HOUR_IN_SECONDS;
	$diff_weblogger_server = ( $weblogger_time - $server_time ) / HOUR_IN_SECONDS;
	$diff_gmt_weblogger    = $diff_gmt_server - $diff_weblogger_server;
	$gmt_offset            = -$diff_gmt_weblogger;

	// Add a gmt_offset option, with value $gmt_offset.
	add_option( 'gmt_offset', $gmt_offset );

	/*
	 * Check if we already set the GMT fields. If we did, then
	 * MAX(post_date_gmt) can't be '0000-00-00 00:00:00'.
	 * <michel_v> I just slapped myself silly for not thinking about it earlier.
	 */
	$got_gmt_fields = ( '0000-00-00 00:00:00' !== $wpdb->get_var( "SELECT MAX(post_date_gmt) FROM $wpdb->posts" ) );

	if ( ! $got_gmt_fields ) {

		// Add or subtract time to all dates, to get GMT dates.
		$add_hours   = (int) $diff_gmt_weblogger;
		$add_minutes = (int) ( 60 * ( $diff_gmt_weblogger - $add_hours ) );
		$wpdb->query( "UPDATE $wpdb->posts SET post_date_gmt = DATE_ADD(post_date, INTERVAL '$add_hours:$add_minutes' HOUR_MINUTE)" );
		$wpdb->query( "UPDATE $wpdb->posts SET post_modified = post_date" );
		$wpdb->query( "UPDATE $wpdb->posts SET post_modified_gmt = DATE_ADD(post_modified, INTERVAL '$add_hours:$add_minutes' HOUR_MINUTE) WHERE post_modified != '0000-00-00 00:00:00'" );
		$wpdb->query( "UPDATE $wpdb->comments SET comment_date_gmt = DATE_ADD(comment_date, INTERVAL '$add_hours:$add_minutes' HOUR_MINUTE)" );
		$wpdb->query( "UPDATE $wpdb->users SET user_registered = DATE_ADD(user_registered, INTERVAL '$add_hours:$add_minutes' HOUR_MINUTE)" );
	}
}

/**
 * Execute changes made in WordPress 1.5.
 *
 * @ignore
 * @since 1.5.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 */
function upgrade_130() {
	global $wpdb;

	// Remove extraneous backslashes.
	$posts = $wpdb->get_results( "SELECT ID, post_title, post_content, post_excerpt, guid, post_date, post_name, post_status, post_author FROM $wpdb->posts" );
	if ( $posts ) {
		foreach ( $posts as $post ) {
			$post_content = addslashes( deslash( $post->post_content ) );
			$post_title   = addslashes( deslash( $post->post_title ) );
			$post_excerpt = addslashes( deslash( $post->post_excerpt ) );
			if ( empty( $post->guid ) ) {
				$guid = get_permalink( $post->ID );
			} else {
				$guid = $post->guid;
			}

			$wpdb->update( $wpdb->posts, compact( 'post_title', 'post_content', 'post_excerpt', 'guid' ), array( 'ID' => $post->ID ) );

		}
	}

	// Remove extraneous backslashes.
	$comments = $wpdb->get_results( "SELECT comment_ID, comment_author, comment_content FROM $wpdb->comments" );
	if ( $comments ) {
		foreach ( $comments as $comment ) {
			$comment_content = deslash( $comment->comment_content );
			$comment_author  = deslash( $comment->comment_author );

			$wpdb->update( $wpdb->comments, compact( 'comment_content', 'comment_author' ), array( 'comment_ID' => $comment->comment_ID ) );
		}
	}

	// Remove extraneous backslashes.
	$links = $wpdb->get_results( "SELECT link_id, link_name, link_description FROM $wpdb->links" );
	if ( $links ) {
		foreach ( $links as $link ) {
			$link_name        = deslash( $link->link_name );
			$link_description = deslash( $link->link_description );

			$wpdb->update( $wpdb->links, compact( 'link_name', 'link_description' ), array( 'link_id' => $link->link_id ) );
		}
	}

	$active_plugins = __get_option( 'active_plugins' );

	/*
	 * If plugins are not stored in an array, they're stored in the old
	 * newline separated format. Convert to new format.
	 */
	if ( ! is_array( $active_plugins ) ) {
		$active_plugins = explode( "\n", trim( $active_plugins ) );
		update_option( 'active_plugins', $active_plugins );
	}

	// Obsolete tables.
	$wpdb->query( 'DROP TABLE IF EXISTS ' . $wpdb->prefix . 'optionvalues' );
	$wpdb->query( 'DROP TABLE IF EXISTS ' . $wpdb->prefix . 'optiontypes' );
	$wpdb->query( 'DROP TABLE IF EXISTS ' . $wpdb->prefix . 'optiongroups' );
	$wpdb->query( 'DROP TABLE IF EXISTS ' . $wpdb->prefix . 'optiongroup_options' );

	// Update comments table to use comment_type.
	$wpdb->query( "UPDATE $wpdb->comments SET comment_type='trackback', comment_content = REPLACE(comment_content, '<trackback />', '') WHERE comment_content LIKE '<trackback />%'" );
	$wpdb->query( "UPDATE $wpdb->comments SET comment_type='pingback', comment_content = REPLACE(comment_content, '<pingback />', '') WHERE comment_content LIKE '<pingback />%'" );

	// Some versions have multiple duplicate option_name rows with the same values.
	$options = $wpdb->get_results( "SELECT option_name, COUNT(option_name) AS dupes FROM `$wpdb->options` GROUP BY option_name" );
	foreach ( $options as $option ) {
		if ( $option->dupes > 1 ) { // Could this be done in the query?
			$limit    = $option->dupes - 1;
			$dupe_ids = $wpdb->get_col( $wpdb->prepare( "SELECT option_id FROM $wpdb->options WHERE option_name = %s LIMIT %d", $option->option_name, $limit ) );
			if ( $dupe_ids ) {
				$dupe_ids = implode( ',', $dupe_ids );
				$wpdb->query( "DELETE FROM $wpdb->options WHERE option_id IN ($dupe_ids)" );
			}
		}
	}

	make_site_theme();
}

/**
 * Execute changes made in WordPress 2.0.
 *
 * @ignore
 * @since 2.0.0
 *
 * @global wpdb $wpdb                  WordPress database abstraction object.
 * @global int  $wp_current_db_version The old (current) database version.
 */
function upgrade_160() {
	global $wpdb, $wp_current_db_version;

	populate_roles_160();

	$users = $wpdb->get_results( "SELECT * FROM $wpdb->users" );
	foreach ( $users as $user ) :
		if ( ! empty( $user->user_firstname ) ) {
			update_user_meta( $user->ID, 'first_name', wp_slash( $user->user_firstname ) );
		}
		if ( ! empty( $user->user_lastname ) ) {
			update_user_meta( $user->ID, 'last_name', wp_slash( $user->user_lastname ) );
		}
		if ( ! empty( $user->user_nickname ) ) {
			update_user_meta( $user->ID, 'nickname', wp_slash( $user->user_nickname ) );
		}
		if ( ! empty( $user->user_level ) ) {
			update_user_meta( $user->ID, $wpdb->prefix . 'user_level', $user->user_level );
		}
		if ( ! empty( $user->user_icq ) ) {
			update_user_meta( $user->ID, 'icq', wp_slash( $user->user_icq ) );
		}
		if ( ! empty( $user->user_aim ) ) {
			update_user_meta( $user->ID, 'aim', wp_slash( $user->user_aim ) );
		}
		if ( ! empty( $user->user_msn ) ) {
			update_user_meta( $user->ID, 'msn', wp_slash( $user->user_msn ) );
		}
		if ( ! empty( $user->user_yim ) ) {
			update_user_meta( $user->ID, 'yim', wp_slash( $user->user_icq ) );
		}
		if ( ! empty( $user->user_description ) ) {
			update_user_meta( $user->ID, 'description', wp_slash( $user->user_description ) );
		}

		if ( isset( $user->user_idmode ) ) :
			$idmode = $user->user_idmode;
			if ( 'nickname' === $idmode ) {
				$id = $user->user_nickname;
			}
			if ( 'login' === $idmode ) {
				$id = $user->user_login;
			}
			if ( 'firstname' === $idmode ) {
				$id = $user->user_firstname;
			}
			if ( 'lastname' === $idmode ) {
				$id = $user->user_lastname;
			}
			if ( 'namefl' === $idmode ) {
				$id = $user->user_firstname . ' ' . $user->user_lastname;
			}
			if ( 'namelf' === $idmode ) {
				$id = $user->user_lastname . ' ' . $user->user_firstname;
			}
			if ( ! $idmode ) {
				$id = $user->user_nickname;
			}
			$wpdb->update( $wpdb->users, array( 'display_name' => $id ), array( 'ID' => $user->ID ) );
		endif;

		// FIXME: RESET_CAPS is temporary code to reset roles and caps if flag is set.
		$caps = get_user_meta( $user->ID, $wpdb->prefix . 'capabilities' );
		if ( empty( $caps ) || defined( 'RESET_CAPS' ) ) {
			$level = get_user_meta( $user->ID, $wpdb->prefix . 'user_level', true );
			$role  = translate_level_to_role( $level );
			update_user_meta( $user->ID, $wpdb->prefix . 'capabilities', array( $role => true ) );
		}

	endforeach;
	$old_user_fields = array( 'user_firstname', 'user_lastname', 'user_icq', 'user_aim', 'user_msn', 'user_yim', 'user_idmode', 'user_ip', 'user_domain', 'user_browser', 'user_description', 'user_nickname', 'user_level' );
	$wpdb->hide_errors();
	foreach ( $old_user_fields as $old ) {
		$wpdb->query( "ALTER TABLE $wpdb->users DROP $old" );
	}
	$wpdb->show_errors();

	// Populate comment_count field of posts table.
	$comments = $wpdb->get_results( "SELECT comment_post_ID, COUNT(*) as c FROM $wpdb->comments WHERE comment_approved = '1' GROUP BY comment_post_ID" );
	if ( is_array( $comments ) ) {
		foreach ( $comments as $comment ) {
			$wpdb->update( $wpdb->posts, array( 'comment_count' => $comment->c ), array( 'ID' => $comment->comment_post_ID ) );
		}
	}

	/*
	 * Some alpha versions used a post status of object instead of attachment
	 * and put the mime type in post_type instead of post_mime_type.
	 */
	if ( $wp_current_db_version > 2541 && $wp_current_db_version <= 3091 ) {
		$objects = $wpdb->get_results( "SELECT ID, post_type FROM $wpdb->posts WHERE post_status = 'object'" );
		foreach ( $objects as $object ) {
			$wpdb->update(
				$wpdb->posts,
				array(
					'post_status'    => 'attachment',
					'post_mime_type' => $object->post_type,
					'post_type'      => '',
				),
				array( 'ID' => $object->ID )
			);

			$meta = get_post_meta( $object->ID, 'imagedata', true );
			if ( ! empty( $meta['file'] ) ) {
				update_attached_file( $object->ID, $meta['file'] );
			}
		}
	}
}

/**
 * Execute changes made in WordPress 2.1.
 *
 * @ignore
 * @since 2.1.0
 *
 * @global int  $wp_current_db_version The old (current) database version.
 * @global wpdb $wpdb                  WordPress database abstraction object.
 */
function upgrade_210() {
	global $wp_current_db_version, $wpdb;

	if ( $wp_current_db_version < 3506 ) {
		// Update status and type.
		$posts = $wpdb->get_results( "SELECT ID, post_status FROM $wpdb->posts" );

		if ( ! empty( $posts ) ) {
			foreach ( $posts as $post ) {
				$status = $post->post_status;
				$type   = 'post';

				if ( 'static' === $status ) {
					$status = 'publish';
					$type   = 'page';
				} elseif ( 'attachment' === $status ) {
					$status = 'inherit';
					$type   = 'attachment';
				}

				$wpdb->query( $wpdb->prepare( "UPDATE $wpdb->posts SET post_status = %s, post_type = %s WHERE ID = %d", $status, $type, $post->ID ) );
			}
		}
	}

	if ( $wp_current_db_version < 3845 ) {
		populate_roles_210();
	}

	if ( $wp_current_db_version < 3531 ) {
		// Give future posts a post_status of future.
		$now = gmdate( 'Y-m-d H:i:59' );
		$wpdb->query( "UPDATE $wpdb->posts SET post_status = 'future' WHERE post_status = 'publish' AND post_date_gmt > '$now'" );

		$posts = $wpdb->get_results( "SELECT ID, post_date FROM $wpdb->posts WHERE post_status ='future'" );
		if ( ! empty( $posts ) ) {
			foreach ( $posts as $post ) {
				wp_schedule_single_event( mysql2date( 'U', $post->post_date, false ), 'publish_future_post', array( $post->ID ) );
			}
		}
	}
}

/**
 * Execute changes made in WordPress 2.3.
 *
 * @ignore
 * @since 2.3.0
 *
 * @global int  $wp_current_db_version The old (current) database version.
 * @global wpdb $wpdb                  WordPress database abstraction object.
 */
function upgrade_230() {
	global $wp_current_db_version, $wpdb;

	if ( $wp_current_db_version < 5200 ) {
		populate_roles_230();
	}

	// Convert categories to terms.
	$tt_ids     = array();
	$have_tags  = false;
	$categories = $wpdb->get_results( "SELECT * FROM $wpdb->categories ORDER BY cat_ID" );
	foreach ( $categories as $category ) {
		$term_id     = (int) $category->cat_ID;
		$name        = $category->cat_name;
		$description = $category->category_description;
		$slug        = $category->category_nicename;
		$parent      = $category->category_parent;
		$term_group  = 0;

		// Associate terms with the same slug in a term group and make slugs unique.
		$exists = $wpdb->get_results( $wpdb->prepare( "SELECT term_id, term_group FROM $wpdb->terms WHERE slug = %s", $slug ) );
		if ( $exists ) {
			$term_group = $exists[0]->term_group;
			$id         = $exists[0]->term_id;
			$num        = 2;
			do {
				$alt_slug = $slug . "-$num";
				++$num;
				$slug_check = $wpdb->get_var( $wpdb->prepare( "SELECT slug FROM $wpdb->terms WHERE slug = %s", $alt_slug ) );
			} while ( $slug_check );

			$slug = $alt_slug;

			if ( empty( $term_group ) ) {
				$term_group = $wpdb->get_var( "SELECT MAX(term_group) FROM $wpdb->terms GROUP BY term_group" ) + 1;
				$wpdb->query( $wpdb->prepare( "UPDATE $wpdb->terms SET term_group = %d WHERE term_id = %d", $term_group, $id ) );
			}
		}

		$wpdb->query(
			$wpdb->prepare(
				"INSERT INTO $wpdb->terms (term_id, name, slug, term_group) VALUES
		(%d, %s, %s, %d)",
				$term_id,
				$name,
				$slug,
				$term_group
			)
		);

		$count = 0;
		if ( ! empty( $category->category_count ) ) {
			$count    = (int) $category->category_count;
			$taxonomy = 'category';
			$wpdb->query( $wpdb->prepare( "INSERT INTO $wpdb->term_taxonomy (term_id, taxonomy, description, parent, count) VALUES ( %d, %s, %s, %d, %d)", $term_id, $taxonomy, $description, $parent, $count ) );
			$tt_ids[ $term_id ][ $taxonomy ] = (int) $wpdb->insert_id;
		}

		if ( ! empty( $category->link_count ) ) {
			$count    = (int) $category->link_count;
			$taxonomy = 'link_category';
			$wpdb->query( $wpdb->prepare( "INSERT INTO $wpdb->term_taxonomy (term_id, taxonomy, description, parent, count) VALUES ( %d, %s, %s, %d, %d)", $term_id, $taxonomy, $description, $parent, $count ) );
			$tt_ids[ $term_id ][ $taxonomy ] = (int) $wpdb->insert_id;
		}

		if ( ! empty( $category->tag_count ) ) {
			$have_tags = true;
			$count     = (int) $category->tag_count;
			$taxonomy  = 'post_tag';
			$wpdb->insert( $wpdb->term_taxonomy, compact( 'term_id', 'taxonomy', 'description', 'parent', 'count' ) );
			$tt_ids[ $term_id ][ $taxonomy ] = (int) $wpdb->insert_id;
		}

		if ( empty( $count ) ) {
			$count    = 0;
			$taxonomy = 'category';
			$wpdb->insert( $wpdb->term_taxonomy, compact( 'term_id', 'taxonomy', 'description', 'parent', 'count' ) );
			$tt_ids[ $term_id ][ $taxonomy ] = (int) $wpdb->insert_id;
		}
	}

	$select = 'post_id, category_id';
	if ( $have_tags ) {
		$select .= ', rel_type';
	}

	$posts = $wpdb->get_results( "SELECT $select FROM $wpdb->post2cat GROUP BY post_id, category_id" );
	foreach ( $posts as $post ) {
		$post_id  = (int) $post->post_id;
		$term_id  = (int) $post->category_id;
		$taxonomy = 'category';
		if ( ! empty( $post->rel_type ) && 'tag' === $post->rel_type ) {
			$taxonomy = 'tag';
		}
		$tt_id = $tt_ids[ $term_id ][ $taxonomy ];
		if ( empty( $tt_id ) ) {
			continue;
		}

		$wpdb->insert(
			$wpdb->term_relationships,
			array(
				'object_id'        => $post_id,
				'term_taxonomy_id' => $tt_id,
			)
		);
	}

	// < 3570 we used linkcategories. >= 3570 we used categories and link2cat.
	if ( $wp_current_db_version < 3570 ) {
		/*
		 * Create link_category terms for link categories. Create a map of link
		 * category IDs to link_category terms.
		 */
		$link_cat_id_map  = array();
		$default_link_cat = 0;
		$tt_ids           = array();
		$link_cats        = $wpdb->get_results( 'SELECT cat_id, cat_name FROM ' . $wpdb->prefix . 'linkcategories' );
		foreach ( $link_cats as $category ) {
			$cat_id     = (int) $category->cat_id;
			$term_id    = 0;
			$name       = wp_slash( $category->cat_name );
			$slug       = sanitize_title( $name );
			$term_group = 0;

			// Associate terms with the same slug in a term group and make slugs unique.
			$exists = $wpdb->get_results( $wpdb->prepare( "SELECT term_id, term_group FROM $wpdb->terms WHERE slug = %s", $slug ) );
			if ( $exists ) {
				$term_group = $exists[0]->term_group;
				$term_id    = $exists[0]->term_id;
			}

			if ( empty( $term_id ) ) {
				$wpdb->insert( $wpdb->terms, compact( 'name', 'slug', 'term_group' ) );
				$term_id = (int) $wpdb->insert_id;
			}

			$link_cat_id_map[ $cat_id ] = $term_id;
			$default_link_cat           = $term_id;

			$wpdb->insert(
				$wpdb->term_taxonomy,
				array(
					'term_id'     => $term_id,
					'taxonomy'    => 'link_category',
					'description' => '',
					'parent'      => 0,
					'count'       => 0,
				)
			);
			$tt_ids[ $term_id ] = (int) $wpdb->insert_id;
		}

		// Associate links to categories.
		$links = $wpdb->get_results( "SELECT link_id, link_category FROM $wpdb->links" );
		if ( ! empty( $links ) ) {
			foreach ( $links as $link ) {
				if ( 0 === (int) $link->link_category ) {
					continue;
				}
				if ( ! isset( $link_cat_id_map[ $link->link_category ] ) ) {
					continue;
				}
				$term_id = $link_cat_id_map[ $link->link_category ];
				$tt_id   = $tt_ids[ $term_id ];
				if ( empty( $tt_id ) ) {
					continue;
				}

				$wpdb->insert(
					$wpdb->term_relationships,
					array(
						'object_id'        => $link->link_id,
						'term_taxonomy_id' => $tt_id,
					)
				);
			}
		}

		// Set default to the last category we grabbed during the upgrade loop.
		update_option( 'default_link_category', $default_link_cat );
	} else {
		$links = $wpdb->get_results( "SELECT link_id, category_id FROM $wpdb->link2cat GROUP BY link_id, category_id" );
		foreach ( $links as $link ) {
			$link_id  = (int) $link->link_id;
			$term_id  = (int) $link->category_id;
			$taxonomy = 'link_category';
			$tt_id    = $tt_ids[ $term_id ][ $taxonomy ];
			if ( empty( $tt_id ) ) {
				continue;
			}
			$wpdb->insert(
				$wpdb->term_relationships,
				array(
					'object_id'        => $link_id,
					'term_taxonomy_id' => $tt_id,
				)
			);
		}
	}

	if ( $wp_current_db_version < 4772 ) {
		// Obsolete linkcategories table.
		$wpdb->query( 'DROP TABLE IF EXISTS ' . $wpdb->prefix . 'linkcategories' );
	}

	// Recalculate all counts.
	$terms = $wpdb->get_results( "SELECT term_taxonomy_id, taxonomy FROM $wpdb->term_taxonomy" );
	foreach ( (array) $terms as $term ) {
		if ( 'post_tag' === $term->taxonomy || 'category' === $term->taxonomy ) {
			$count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_relationships, $wpdb->posts WHERE $wpdb->posts.ID = $wpdb->term_relationships.object_id AND post_status = 'publish' AND post_type = 'post' AND term_taxonomy_id = %d", $term->term_taxonomy_id ) );
		} else {
			$count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_relationships WHERE term_taxonomy_id = %d", $term->term_taxonomy_id ) );
		}
		$wpdb->update( $wpdb->term_taxonomy, array( 'count' => $count ), array( 'term_taxonomy_id' => $term->term_taxonomy_id ) );
	}
}

/**
 * Remove old options from the database.
 *
 * @ignore
 * @since 2.3.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 */
function upgrade_230_options_table() {
	global $wpdb;
	$old_options_fields = array( 'option_can_override', 'option_type', 'option_width', 'option_height', 'option_description', 'option_admin_level' );
	$wpdb->hide_errors();
	foreach ( $old_options_fields as $old ) {
		$wpdb->query( "ALTER TABLE $wpdb->options DROP $old" );
	}
	$wpdb->show_errors();
}

/**
 * Remove old categories, link2cat, and post2cat database tables.
 *
 * @ignore
 * @since 2.3.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 */
function upgrade_230_old_tables() {
	global $wpdb;
	$wpdb->query( 'DROP TABLE IF EXISTS ' . $wpdb->prefix . 'categories' );
	$wpdb->query( 'DROP TABLE IF EXISTS ' . $wpdb->prefix . 'link2cat' );
	$wpdb->query( 'DROP TABLE IF EXISTS ' . $wpdb->prefix . 'post2cat' );
}

/**
 * Upgrade old slugs made in version 2.2.
 *
 * @ignore
 * @since 2.2.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 */
function upgrade_old_slugs() {
	// Upgrade people who were using the Redirect Old Slugs plugin.
	global $wpdb;
	$wpdb->query( "UPDATE $wpdb->postmeta SET meta_key = '_wp_old_slug' WHERE meta_key = 'old_slug'" );
}

/**
 * Execute changes made in WordPress 2.5.0.
 *
 * @ignore
 * @since 2.5.0
 *
 * @global int $wp_current_db_version The old (current) database version.
 */
function upgrade_250() {
	global $wp_current_db_version;

	if ( $wp_current_db_version < 6689 ) {
		populate_roles_250();
	}
}

/**
 * Execute changes made in WordPress 2.5.2.
 *
 * @ignore
 * @since 2.5.2
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 */
function upgrade_252() {
	global $wpdb;

	$wpdb->query( "UPDATE $wpdb->users SET user_activation_key = ''" );
}

/**
 * Execute changes made in WordPress 2.6.
 *
 * @ignore
 * @since 2.6.0
 *
 * @global int $wp_current_db_version The old (current) database version.
 */
function upgrade_260() {
	global $wp_current_db_version;

	if ( $wp_current_db_version < 8000 ) {
		populate_roles_260();
	}
}

/**
 * Execute changes made in WordPress 2.7.
 *
 * @ignore
 * @since 2.7.0
 *
 * @global int  $wp_current_db_version The old (current) database version.
 * @global wpdb $wpdb                  WordPress database abstraction object.
 */
function upgrade_270() {
	global $wp_current_db_version, $wpdb;

	if ( $wp_current_db_version < 8980 ) {
		populate_roles_270();
	}

	// Update post_date for unpublished posts with empty timestamp.
	if ( $wp_current_db_version < 8921 ) {
		$wpdb->query( "UPDATE $wpdb->posts SET post_date = post_modified WHERE post_date = '0000-00-00 00:00:00'" );
	}
}

/**
 * Execute changes made in WordPress 2.8.
 *
 * @ignore
 * @since 2.8.0
 *
 * @global int  $wp_current_db_version The old (current) database version.
 * @global wpdb $wpdb                  WordPress database abstraction object.
 */
function upgrade_280() {
	global $wp_current_db_version, $wpdb;

	if ( $wp_current_db_version < 10360 ) {
		populate_roles_280();
	}
	if ( is_multisite() ) {
		$start = 0;
		while ( $rows = $wpdb->get_results( "SELECT option_name, option_value FROM $wpdb->options ORDER BY option_id LIMIT $start, 20" ) ) {
			foreach ( $rows as $row ) {
				$value = maybe_unserialize( $row->option_value );
				if ( $value === $row->option_value ) {
					$value = stripslashes( $value );
				}
				if ( $value !== $row->option_value ) {
					update_option( $row->option_name, $value );
				}
			}
			$start += 20;
		}
		clean_blog_cache( get_current_blog_id() );
	}
}

/**
 * Execute changes made in WordPress 2.9.
 *
 * @ignore
 * @since 2.9.0
 *
 * @global int $wp_current_db_version The old (current) database version.
 */
function upgrade_290() {
	global $wp_current_db_version;

	if ( $wp_current_db_version < 11958 ) {
		/*
		 * Previously, setting depth to 1 would redundantly disable threading,
		 * but now 2 is the minimum depth to avoid confusion.
		 */
		if ( 1 === (int) get_option( 'thread_comments_depth' ) ) {
			update_option( 'thread_comments_depth', 2 );
			update_option( 'thread_comments', 0 );
		}
	}
}

/**
 * Execute changes made in WordPress 3.0.
 *
 * @ignore
 * @since 3.0.0
 *
 * @global int  $wp_current_db_version The old (current) database version.
 * @global wpdb $wpdb                  WordPress database abstraction object.
 */
function upgrade_300() {
	global $wp_current_db_version, $wpdb;

	if ( $wp_current_db_version < 15093 ) {
		populate_roles_300();
	}

	if ( $wp_current_db_version < 14139 && is_multisite() && is_main_site() && ! defined( 'MULTISITE' ) && get_site_option( 'siteurl' ) === false ) {
		add_site_option( 'siteurl', '' );
	}

	// 3.0 screen options key name changes.
	if ( wp_should_upgrade_global_tables() ) {
		$sql    = "DELETE FROM $wpdb->usermeta
			WHERE meta_key LIKE %s
			OR meta_key LIKE %s
			OR meta_key LIKE %s
			OR meta_key LIKE %s
			OR meta_key LIKE %s
			OR meta_key LIKE %s
			OR meta_key = 'manageedittagscolumnshidden'
			OR meta_key = 'managecategoriescolumnshidden'
			OR meta_key = 'manageedit-tagscolumnshidden'
			OR meta_key = 'manageeditcolumnshidden'
			OR meta_key = 'categories_per_page'
			OR meta_key = 'edit_tags_per_page'";
		$prefix = $wpdb->esc_like( $wpdb->base_prefix );
		$wpdb->query(
			$wpdb->prepare(
				$sql,
				$prefix . '%' . $wpdb->esc_like( 'meta-box-hidden' ) . '%',
				$prefix . '%' . $wpdb->esc_like( 'closedpostboxes' ) . '%',
				$prefix . '%' . $wpdb->esc_like( 'manage-' ) . '%' . $wpdb->esc_like( '-columns-hidden' ) . '%',
				$prefix . '%' . $wpdb->esc_like( 'meta-box-order' ) . '%',
				$prefix . '%' . $wpdb->esc_like( 'metaboxorder' ) . '%',
				$prefix . '%' . $wpdb->esc_like( 'screen_layout' ) . '%'
			)
		);
	}
}

/**
 * Execute changes made in WordPress 3.3.
 *
 * @ignore
 * @since 3.3.0
 *
 * @global int   $wp_current_db_version The old (current) database version.
 * @global wpdb  $wpdb                  WordPress database abstraction object.
 * @global array $wp_registered_widgets
 * @global array $sidebars_widgets
 */
function upgrade_330() {
	global $wp_current_db_version, $wpdb, $wp_registered_widgets, $sidebars_widgets;

	if ( $wp_current_db_version < 19061 && wp_should_upgrade_global_tables() ) {
		$wpdb->query( "DELETE FROM $wpdb->usermeta WHERE meta_key IN ('show_admin_bar_admin', 'plugins_last_view')" );
	}

	if ( $wp_current_db_version >= 11548 ) {
		return;
	}

	$sidebars_widgets  = get_option( 'sidebars_widgets', array() );
	$_sidebars_widgets = array();

	if ( isset( $sidebars_widgets['wp_inactive_widgets'] ) || empty( $sidebars_widgets ) ) {
		$sidebars_widgets['array_version'] = 3;
	} elseif ( ! isset( $sidebars_widgets['array_version'] ) ) {
		$sidebars_widgets['array_version'] = 1;
	}

	switch ( $sidebars_widgets['array_version'] ) {
		case 1:
			foreach ( (array) $sidebars_widgets as $index => $sidebar ) {
				if ( is_array( $sidebar ) ) {
					foreach ( (array) $sidebar as $i => $name ) {
						$id = strtolower( $name );
						if ( isset( $wp_registered_widgets[ $id ] ) ) {
							$_sidebars_widgets[ $index ][ $i ] = $id;
							continue;
						}

						$id = sanitize_title( $name );
						if ( isset( $wp_registered_widgets[ $id ] ) ) {
							$_sidebars_widgets[ $index ][ $i ] = $id;
							continue;
						}

						$found = false;

						foreach ( $wp_registered_widgets as $widget_id => $widget ) {
							if ( strtolower( $widget['name'] ) === strtolower( $name ) ) {
								$_sidebars_widgets[ $index ][ $i ] = $widget['id'];

								$found = true;
								break;
							} elseif ( sanitize_title( $widget['name'] ) === sanitize_title( $name ) ) {
								$_sidebars_widgets[ $index ][ $i ] = $widget['id'];

								$found = true;
								break;
							}
						}

						if ( $found ) {
							continue;
						}

						unset( $_sidebars_widgets[ $index ][ $i ] );
					}
				}
			}
			$_sidebars_widgets['array_version'] = 2;
			$sidebars_widgets                   = $_sidebars_widgets;
			unset( $_sidebars_widgets );

			// Intentional fall-through to upgrade to the next version.
		case 2:
			$sidebars_widgets                  = retrieve_widgets();
			$sidebars_widgets['array_version'] = 3;
			update_option( 'sidebars_widgets', $sidebars_widgets );
	}
}

/**
 * Execute changes made in WordPress 3.4.
 *
 * @ignore
 * @since 3.4.0
 *
 * @global int  $wp_current_db_version The old (current) database version.
 * @global wpdb $wpdb                  WordPress database abstraction object.
 */
function upgrade_340() {
	global $wp_current_db_version, $wpdb;

	if ( $wp_current_db_version < 19798 ) {
		$wpdb->hide_errors();
		$wpdb->query( "ALTER TABLE $wpdb->options DROP COLUMN blog_id" );
		$wpdb->show_errors();
	}

	if ( $wp_current_db_version < 19799 ) {
		$wpdb->hide_errors();
		$wpdb->query( "ALTER TABLE $wpdb->comments DROP INDEX comment_approved" );
		$wpdb->show_errors();
	}

	if ( $wp_current_db_version < 20022 && wp_should_upgrade_global_tables() ) {
		$wpdb->query( "DELETE FROM $wpdb->usermeta WHERE meta_key = 'themes_last_view'" );
	}

	if ( $wp_current_db_version < 20080 ) {
		if ( 'yes' === $wpdb->get_var( "SELECT autoload FROM $wpdb->options WHERE option_name = 'uninstall_plugins'" ) ) {
			$uninstall_plugins = get_option( 'uninstall_plugins' );
			delete_option( 'uninstall_plugins' );
			add_option( 'uninstall_plugins', $uninstall_plugins, null, false );
		}
	}
}

/**
 * Execute changes made in WordPress 3.5.
 *
 * @ignore
 * @since 3.5.0
 *
 * @global int  $wp_current_db_version The old (current) database version.
 * @global wpdb $wpdb                  WordPress database abstraction object.
 */
function upgrade_350() {
	global $wp_current_db_version, $wpdb;

	if ( $wp_current_db_version < 22006 && $wpdb->get_var( "SELECT link_id FROM $wpdb->links LIMIT 1" ) ) {
		update_option( 'link_manager_enabled', 1 ); // Previously set to 0 by populate_options().
	}

	if ( $wp_current_db_version < 21811 && wp_should_upgrade_global_tables() ) {
		$meta_keys = array();
		foreach ( array_merge( get_post_types(), get_taxonomies() ) as $name ) {
			if ( str_contains( $name, '-' ) ) {
				$meta_keys[] = 'edit_' . str_replace( '-', '_', $name ) . '_per_page';
			}
		}
		if ( $meta_keys ) {
			$meta_keys = implode( "', '", $meta_keys );
			$wpdb->query( "DELETE FROM $wpdb->usermeta WHERE meta_key IN ('$meta_keys')" );
		}
	}

	if ( $wp_current_db_version < 22422 ) {
		$term = get_term_by( 'slug', 'post-format-standard', 'post_format' );
		if ( $term ) {
			wp_delete_term( $term->term_id, 'post_format' );
		}
	}
}

/**
 * Execute changes made in WordPress 3.7.
 *
 * @ignore
 * @since 3.7.0
 *
 * @global int $wp_current_db_version The old (current) database version.
 */
function upgrade_370() {
	global $wp_current_db_version;

	if ( $wp_current_db_version < 25824 ) {
		wp_clear_scheduled_hook( 'wp_auto_updates_maybe_update' );
	}
}

/**
 * Execute changes made in WordPress 3.7.2.
 *
 * @ignore
 * @since 3.7.2
 *
 * @global int $wp_current_db_version The old (current) database version.
 */
function upgrade_372() {
	global $wp_current_db_version;

	if ( $wp_current_db_version < 26148 ) {
		wp_clear_scheduled_hook( 'wp_maybe_auto_update' );
	}
}

/**
 * Execute changes made in WordPress 3.8.0.
 *
 * @ignore
 * @since 3.8.0
 *
 * @global int $wp_current_db_version The old (current) database version.
 */
function upgrade_380() {
	global $wp_current_db_version;

	if ( $wp_current_db_version < 26691 ) {
		deactivate_plugins( array( 'mp6/mp6.php' ), true );
	}
}

/**
 * Execute changes made in WordPress 4.0.0.
 *
 * @ignore
 * @since 4.0.0
 *
 * @global int $wp_current_db_version The old (current) database version.
 */
function upgrade_400() {
	global $wp_current_db_version;

	if ( $wp_current_db_version < 29630 ) {
		if ( ! is_multisite() && false === get_option( 'WPLANG' ) ) {
			if ( defined( 'WPLANG' ) && ( '' !== WPLANG ) && in_array( WPLANG, get_available_languages(), true ) ) {
				update_option( 'WPLANG', WPLANG );
			} else {
				update_option( 'WPLANG', '' );
			}
		}
	}
}

/**
 * Execute changes made in WordPress 4.2.0.
 *
 * @ignore
 * @since 4.2.0
 */
function upgrade_420() {}

/**
 * Executes changes made in WordPress 4.3.0.
 *
 * @ignore
 * @since 4.3.0
 *
 * @global int  $wp_current_db_version The old (current) database version.
 * @global wpdb $wpdb                  WordPress database abstraction object.
 */
function upgrade_430() {
	global $wp_current_db_version, $wpdb;

	if ( $wp_current_db_version < 32364 ) {
		upgrade_430_fix_comments();
	}

	// Shared terms are split in a separate process.
	if ( $wp_current_db_version < 32814 ) {
		update_option( 'finished_splitting_shared_terms', 0 );
		wp_schedule_single_event( time() + ( 1 * MINUTE_IN_SECONDS ), 'wp_split_shared_term_batch' );
	}

	if ( $wp_current_db_version < 33055 && 'utf8mb4' === $wpdb->charset ) {
		if ( is_multisite() ) {
			$tables = $wpdb->tables( 'blog' );
		} else {
			$tables = $wpdb->tables( 'all' );
			if ( ! wp_should_upgrade_global_tables() ) {
				$global_tables = $wpdb->tables( 'global' );
				$tables        = array_diff_assoc( $tables, $global_tables );
			}
		}

		foreach ( $tables as $table ) {
			maybe_convert_table_to_utf8mb4( $table );
		}
	}
}

/**
 * Executes comments changes made in WordPress 4.3.0.
 *
 * @ignore
 * @since 4.3.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 */
function upgrade_430_fix_comments() {
	global $wpdb;

	$content_length = $wpdb->get_col_length( $wpdb->comments, 'comment_content' );

	if ( is_wp_error( $content_length ) ) {
		return;
	}

	if ( false === $content_length ) {
		$content_length = array(
			'type'   => 'byte',
			'length' => 65535,
		);
	} elseif ( ! is_array( $content_length ) ) {
		$length         = (int) $content_length > 0 ? (int) $content_length : 65535;
		$content_length = array(
			'type'   => 'byte',
			'length' => $length,
		);
	}

	if ( 'byte' !== $content_length['type'] || 0 === $content_length['length'] ) {
		// Sites with malformed DB schemas are on their own.
		return;
	}

	$allowed_length = (int) $content_length['length'] - 10;

	$comments = $wpdb->get_results(
		"SELECT `comment_ID` FROM `{$wpdb->comments}`
			WHERE `comment_date_gmt` > '2015-04-26'
			AND LENGTH( `comment_content` ) >= {$allowed_length}
			AND ( `comment_content` LIKE '%<%' OR `comment_content` LIKE '%>%' )"
	);

	foreach ( $comments as $comment ) {
		wp_delete_comment( $comment->comment_ID, true );
	}
}

/**
 * Executes changes made in WordPress 4.3.1.
 *
 * @ignore
 * @since 4.3.1
 */
function upgrade_431() {
	// Fix incorrect cron entries for term splitting.
	$cron_array = _get_cron_array();
	if ( isset( $cron_array['wp_batch_split_terms'] ) ) {
		unset( $cron_array['wp_batch_split_terms'] );
		_set_cron_array( $cron_array );
	}
}

/**
 * Executes changes made in WordPress 4.4.0.
 *
 * @ignore
 * @since 4.4.0
 *
 * @global int  $wp_current_db_version The old (current) database version.
 * @global wpdb $wpdb                  WordPress database abstraction object.
 */
function upgrade_440() {
	global $wp_current_db_version, $wpdb;

	if ( $wp_current_db_version < 34030 ) {
		$wpdb->query( "ALTER TABLE {$wpdb->options} MODIFY option_name VARCHAR(191)" );
	}

	// Remove the unused 'add_users' role.
	$roles = wp_roles();
	foreach ( $roles->role_objects as $role ) {
		if ( $role->has_cap( 'add_users' ) ) {
			$role->remove_cap( 'add_users' );
		}
	}
}

/**
 * Executes changes made in WordPress 4.5.0.
 *
 * @ignore
 * @since 4.5.0
 *
 * @global int  $wp_current_db_version The old (current) database version.
 * @global wpdb $wpdb                  WordPress database abstraction object.
 */
function upgrade_450() {
	global $wp_current_db_version, $wpdb;

	if ( $wp_current_db_version < 36180 ) {
		wp_clear_scheduled_hook( 'wp_maybe_auto_update' );
	}

	// Remove unused email confirmation options, moved to usermeta.
	if ( $wp_current_db_version < 36679 && is_multisite() ) {
		$wpdb->query( "DELETE FROM $wpdb->options WHERE option_name REGEXP '^[0-9]+_new_email$'" );
	}

	// Remove unused user setting for wpLink.
	delete_user_setting( 'wplink' );
}

/**
 * Executes changes made in WordPress 4.6.0.
 *
 * @ignore
 * @since 4.6.0
 *
 * @global int $wp_current_db_version The old (current) database version.
 */
function upgrade_460() {
	global $wp_current_db_version;

	// Remove unused post meta.
	if ( $wp_current_db_version < 37854 ) {
		delete_post_meta_by_key( '_post_restored_from' );
	}

	// Remove plugins with callback as an array object/method as the uninstall hook, see #13786.
	if ( $wp_current_db_version < 37965 ) {
		$uninstall_plugins = get_option( 'uninstall_plugins', array() );

		if ( ! empty( $uninstall_plugins ) ) {
			foreach ( $uninstall_plugins as $basename => $callback ) {
				if ( is_array( $callback ) && is_object( $callback[0] ) ) {
					unset( $uninstall_plugins[ $basename ] );
				}
			}

			update_option( 'uninstall_plugins', $uninstall_plugins );
		}
	}
}

/**
 * Executes changes made in WordPress 5.0.0.
 *
 * @ignore
 * @since 5.0.0
 * @deprecated 5.1.0
 */
function upgrade_500() {
}

/**
 * Executes changes made in WordPress 5.1.0.
 *
 * @ignore
 * @since 5.1.0
 */
function upgrade_510() {
	delete_site_option( 'upgrade_500_was_gutenberg_active' );
}

/**
 * Executes changes made in WordPress 5.3.0.
 *
 * @ignore
 * @since 5.3.0
 */
function upgrade_530() {
	/*
	 * The `admin_email_lifespan` option may have been set by an admin that just logged in,
	 * saw the verification screen, clicked on a button there, and is now upgrading the db,
	 * or by populate_options() that is called earlier in upgrade_all().
	 * In the second case `admin_email_lifespan` should be reset so the verification screen
	 * is shown next time an admin logs in.
	 */
	if ( function_exists( 'current_user_can' ) && ! current_user_can( 'manage_options' ) ) {
		update_option( 'admin_email_lifespan', 0 );
	}
}

/**
 * Executes changes made in WordPress 5.5.0.
 *
 * @ignore
 * @since 5.5.0
 *
 * @global int $wp_current_db_version The old (current) database version.
 */
function upgrade_550() {
	global $wp_current_db_version;

	if ( $wp_current_db_version < 48121 ) {
		$comment_previously_approved = get_option( 'comment_whitelist', '' );
		update_option( 'comment_previously_approved', $comment_previously_approved );
		delete_option( 'comment_whitelist' );
	}

	if ( $wp_current_db_version < 48575 ) {
		// Use more clear and inclusive language.
		$disallowed_list = get_option( 'blacklist_keys' );

		/*
		 * This option key was briefly renamed `blocklist_keys`.
		 * Account for sites that have this key present when the original key does not exist.
		 */
		if ( false === $disallowed_list ) {
			$disallowed_list = get_option( 'blocklist_keys' );
		}

		update_option( 'disallowed_keys', $disallowed_list );
		delete_option( 'blacklist_keys' );
		delete_option( 'blocklist_keys' );
	}

	if ( $wp_current_db_version < 48748 ) {
		update_option( 'finished_updating_comment_type', 0 );
		wp_schedule_single_event( time() + ( 1 * MINUTE_IN_SECONDS ), 'wp_update_comment_type_batch' );
	}
}

/**
 * Executes changes made in WordPress 5.6.0.
 *
 * @ignore
 * @since 5.6.0
 *
 * @global int  $wp_current_db_version The old (current) database version.
 * @global wpdb $wpdb                  WordPress database abstraction object.
 */
function upgrade_560() {
	global $wp_current_db_version, $wpdb;

	if ( $wp_current_db_version < 49572 ) {
		/*
		 * Clean up the `post_category` column removed from schema in version 2.8.0.
		 * Its presence may conflict with `WP_Post::__get()`.
		 */
		$post_category_exists = $wpdb->get_var( "SHOW COLUMNS FROM $wpdb->posts LIKE 'post_category'" );
		if ( ! is_null( $post_category_exists ) ) {
			$wpdb->query( "ALTER TABLE $wpdb->posts DROP COLUMN `post_category`" );
		}

		/*
		 * When upgrading from WP < 5.6.0 set the core major auto-updates option to `unset` by default.
		 * This overrides the same option from populate_options() that is intended for new installs.
		 * See https://core.trac.wordpress.org/ticket/51742.
		 */
		update_option( 'auto_update_core_major', 'unset' );
	}

	if ( $wp_current_db_version < 49632 ) {
		/*
		 * Regenerate the .htaccess file to add the `HTTP_AUTHORIZATION` rewrite rule.
		 * See https://core.trac.wordpress.org/ticket/51723.
		 */
		save_mod_rewrite_rules();
	}

	if ( $wp_current_db_version < 49735 ) {
		delete_transient( 'dirsize_cache' );
	}

	if ( $wp_current_db_version < 49752 ) {
		$results = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT 1 FROM {$wpdb->usermeta} WHERE meta_key = %s LIMIT 1",
				WP_Application_Passwords::USERMETA_KEY_APPLICATION_PASSWORDS
			)
		);

		if ( ! empty( $results ) ) {
			$network_id = get_main_network_id();
			update_network_option( $network_id, WP_Application_Passwords::OPTION_KEY_IN_USE, 1 );
		}
	}
}

/**
 * Executes changes made in WordPress 5.9.0.
 *
 * @ignore
 * @since 5.9.0
 *
 * @global int $wp_current_db_version The old (current) database version.
 */
function upgrade_590() {
	global $wp_current_db_version;

	if ( $wp_current_db_version < 51917 ) {
		$crons = _get_cron_array();

		if ( $crons && is_array( $crons ) ) {
			// Remove errant `false` values, see #53950, #54906.
			$crons = array_filter( $crons );
			_set_cron_array( $crons );
		}
	}
}

/**
 * Executes changes made in WordPress 6.0.0.
 *
 * @ignore
 * @since 6.0.0
 *
 * @global int $wp_current_db_version The old (current) database version.
 */
function upgrade_600() {
	global $wp_current_db_version;

	if ( $wp_current_db_version < 53011 ) {
		wp_update_user_counts();
	}
}

/**
 * Executes changes made in WordPress 6.3.0.
 *
 * @ignore
 * @since 6.3.0
 *
 * @global int $wp_current_db_version The old (current) database version.
 */
function upgrade_630() {
	global $wp_current_db_version;

	if ( $wp_current_db_version < 55853 ) {
		if ( ! is_multisite() ) {
			// Replace non-autoload option can_compress_scripts with autoload option, see #55270
			$can_compress_scripts = get_option( 'can_compress_scripts', false );
			if ( false !== $can_compress_scripts ) {
				delete_option( 'can_compress_scripts' );
				add_option( 'can_compress_scripts', $can_compress_scripts, '', true );
			}
		}
	}
}

/**
 * Executes changes made in WordPress 6.4.0.
 *
 * @ignore
 * @since 6.4.0
 *
 * @global int $wp_current_db_version The old (current) database version.
 */
function upgrade_640() {
	global $wp_current_db_version;

	if ( $wp_current_db_version < 56657 ) {
		// Enable attachment pages.
		update_option( 'wp_attachment_pages_enabled', 1 );

		// Remove the wp_https_detection cron. Https status is checked directly in an async Site Health check.
		$scheduled = wp_get_scheduled_event( 'wp_https_detection' );
		if ( $scheduled ) {
			wp_clear_scheduled_hook( 'wp_https_detection' );
		}
	}
}

/**
 * Executes changes made in WordPress 6.5.0.
 *
 * @ignore
 * @since 6.5.0
 *
 * @global int  $wp_current_db_version The old (current) database version.
 * @global wpdb $wpdb                  WordPress database abstraction object.
 */
function upgrade_650() {
	global $wp_current_db_version, $wpdb;

	if ( $wp_current_db_version < 57155 ) {
		$stylesheet = get_stylesheet();

		// Set autoload=no for all themes except the current one.
		$theme_mods_options = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT option_name FROM $wpdb->options WHERE autoload = 'yes' AND option_name != %s AND option_name LIKE %s",
				"theme_mods_$stylesheet",
				$wpdb->esc_like( 'theme_mods_' ) . '%'
			)
		);

		$autoload = array_fill_keys( $theme_mods_options, false );
		wp_set_option_autoload_values( $autoload );
	}
}
/**
 * Executes changes made in WordPress 6.7.0.
 *
 * @ignore
 * @since 6.7.0
 *
 * @global int  $wp_current_db_version The old (current) database version.
 */
function upgrade_670() {
	global $wp_current_db_version;

	if ( $wp_current_db_version < 58975 ) {
		$options = array(
			'recently_activated',
			'_wp_suggested_policy_text_has_changed',
			'dashboard_widget_options',
			'ftp_credentials',
			'adminhash',
			'nav_menu_options',
			'wp_force_deactivated_plugins',
			'delete_blog_hash',
			'allowedthemes',
			'recovery_keys',
			'https_detection_errors',
			'fresh_site',
		);

		wp_set_options_autoload( $options, false );
	}
}

/**
 * Executes changes made in WordPress 6.8.2.
 *
 * @ignore
 * @since 6.8.2
 *
 * @global int $wp_current_db_version The old (current) database version.
 */
function upgrade_682() {
	global $wp_current_db_version;

	if ( $wp_current_db_version < 60421 ) {
		// Upgrade Ping-O-Matic and Twingly to use HTTPS.
		$ping_sites_value = get_option( 'ping_sites' );
		$ping_sites_value = explode( "\n", $ping_sites_value );
		$ping_sites_value = array_map(
			function ( $url ) {
				$url = trim( $url );
				$url = sanitize_url( $url );
				if (
					str_ends_with( trailingslashit( $url ), '://rpc.pingomatic.com/' )
					|| str_ends_with( trailingslashit( $url ), '://rpc.twingly.com/' )
				) {
					$url = set_url_scheme( $url, 'https' );
				}
				return $url;
			},
			$ping_sites_value
		);
		$ping_sites_value = array_filter( $ping_sites_value );
		$ping_sites_value = implode( "\n", $ping_sites_value );
		update_option( 'ping_sites', $ping_sites_value );
	}
}

/**
 * Executes network-level upgrade routines.
 *
 * @since 3.0.0
 *
 * @global int  $wp_current_db_version The old (current) database version.
 * @global wpdb $wpdb                  WordPress database abstraction object.
 */
function upgrade_network() {
	global $wp_current_db_version, $wpdb;

	// Always clear expired transients.
	delete_expired_transients( true );

	// 2.8.0
	if ( $wp_current_db_version < 11549 ) {
		$wpmu_sitewide_plugins   = get_site_option( 'wpmu_sitewide_plugins' );
		$active_sitewide_plugins = get_site_option( 'active_sitewide_plugins' );
		if ( $wpmu_sitewide_plugins ) {
			if ( ! $active_sitewide_plugins ) {
				$sitewide_plugins = (array) $wpmu_sitewide_plugins;
			} else {
				$sitewide_plugins = array_merge( (array) $active_sitewide_plugins, (array) $wpmu_sitewide_plugins );
			}

			update_site_option( 'active_sitewide_plugins', $sitewide_plugins );
		}
		delete_site_option( 'wpmu_sitewide_plugins' );
		delete_site_option( 'deactivated_sitewide_plugins' );

		$start = 0;
		while ( $rows = $wpdb->get_results( "SELECT meta_key, meta_value FROM {$wpdb->sitemeta} ORDER BY meta_id LIMIT $start, 20" ) ) {
			foreach ( $rows as $row ) {
				$value = $row->meta_value;
				if ( ! @unserialize( $value ) ) {
					$value = stripslashes( $value );
				}
				if ( $value !== $row->meta_value ) {
					update_site_option( $row->meta_key, $value );
				}
			}
			$start += 20;
		}
	}

	// 3.0.0
	if ( $wp_current_db_version < 13576 ) {
		update_site_option( 'global_terms_enabled', '1' );
	}

	// 3.3.0
	if ( $wp_current_db_version < 19390 ) {
		update_site_option( 'initial_db_version', $wp_current_db_version );
	}

	if ( $wp_current_db_version < 19470 ) {
		if ( false === get_site_option( 'active_sitewide_plugins' ) ) {
			update_site_option( 'active_sitewide_plugins', array() );
		}
	}

	// 3.4.0
	if ( $wp_current_db_version < 20148 ) {
		// 'allowedthemes' keys things by stylesheet. 'allowed_themes' keyed things by name.
		$allowedthemes  = get_site_option( 'allowedthemes' );
		$allowed_themes = get_site_option( 'allowed_themes' );
		if ( false === $allowedthemes && is_array( $allowed_themes ) && $allowed_themes ) {
			$converted = array();
			$themes    = wp_get_themes();
			foreach ( $themes as $stylesheet => $theme_data ) {
				if ( isset( $allowed_themes[ $theme_data->get( 'Name' ) ] ) ) {
					$converted[ $stylesheet ] = true;
				}
			}
			update_site_option( 'allowedthemes', $converted );
			delete_site_option( 'allowed_themes' );
		}
	}

	// 3.5.0
	if ( $wp_current_db_version < 21823 ) {
		update_site_option( 'ms_files_rewriting', '1' );
	}

	// 3.5.2
	if ( $wp_current_db_version < 24448 ) {
		$illegal_names = get_site_option( 'illegal_names' );
		if ( is_array( $illegal_names ) && count( $illegal_names ) === 1 ) {
			$illegal_name  = reset( $illegal_names );
			$illegal_names = explode( ' ', $illegal_name );
			update_site_option( 'illegal_names', $illegal_names );
		}
	}

	// 4.2.0
	if ( $wp_current_db_version < 31351 && 'utf8mb4' === $wpdb->charset ) {
		if ( wp_should_upgrade_global_tables() ) {
			$wpdb->query( "ALTER TABLE $wpdb->usermeta DROP INDEX meta_key, ADD INDEX meta_key(meta_key(191))" );
			$wpdb->query( "ALTER TABLE $wpdb->site DROP INDEX domain, ADD INDEX domain(domain(140),path(51))" );
			$wpdb->query( "ALTER TABLE $wpdb->sitemeta DROP INDEX meta_key, ADD INDEX meta_key(meta_key(191))" );
			$wpdb->query( "ALTER TABLE $wpdb->signups DROP INDEX domain_path, ADD INDEX domain_path(domain(140),path(51))" );

			$tables = $wpdb->tables( 'global' );

			// sitecategories may not exist.
			if ( ! $wpdb->get_var( "SHOW TABLES LIKE '{$tables['sitecategories']}'" ) ) {
				unset( $tables['sitecategories'] );
			}

			foreach ( $tables as $table ) {
				maybe_convert_table_to_utf8mb4( $table );
			}
		}
	}

	// 4.3.0
	if ( $wp_current_db_version < 33055 && 'utf8mb4' === $wpdb->charset ) {
		if ( wp_should_upgrade_global_tables() ) {
			$upgrade = false;
			$indexes = $wpdb->get_results( "SHOW INDEXES FROM $wpdb->signups" );
			foreach ( $indexes as $index ) {
				if ( 'domain_path' === $index->Key_name && 'domain' === $index->Column_name && '140' !== $index->Sub_part ) {
					$upgrade = true;
					break;
				}
			}

			if ( $upgrade ) {
				$wpdb->query( "ALTER TABLE $wpdb->signups DROP INDEX domain_path, ADD INDEX domain_path(domain(140),path(51))" );
			}

			$tables = $wpdb->tables( 'global' );

			// sitecategories may not exist.
			if ( ! $wpdb->get_var( "SHOW TABLES LIKE '{$tables['sitecategories']}'" ) ) {
				unset( $tables['sitecategories'] );
			}

			foreach ( $tables as $table ) {
				maybe_convert_table_to_utf8mb4( $table );
			}
		}
	}

	// 5.1.0
	if ( $wp_current_db_version < 44467 ) {
		$network_id = get_main_network_id();
		delete_network_option( $network_id, 'site_meta_supported' );
		is_site_meta_supported();
	}
}

//
// General functions we use to actually do stuff.
//

/**
 * Creates a table in the database, if it doesn't already exist.
 *
 * This method checks for an existing database table and creates a new one if it's not
 * already present. It doesn't rely on MySQL's "IF NOT EXISTS" statement, but chooses
 * to query all tables first and then run the SQL statement creating the table.
 *
 * @since 1.0.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param string $table_name Database table name.
 * @param string $create_ddl SQL statement to create table.
 * @return bool True on success or if the table already exists. False on failure.
 */
function maybe_create_table( $table_name, $create_ddl ) {
	global $wpdb;

	$query = $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table_name ) );

	if ( $wpdb->get_var( $query ) === $table_name ) {
		return true;
	}

	// Didn't find it, so try to create it.
	$wpdb->query( $create_ddl );

	// We cannot directly tell that whether this succeeded!
	if ( $wpdb->get_var( $query ) === $table_name ) {
		return true;
	}

	return false;
}

/**
 * Drops a specified index from a table.
 *
 * @since 1.0.1
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param string $table Database table name.
 * @param string $index Index name to drop.
 * @return true True, when finished.
 */
function drop_index( $table, $index ) {
	global $wpdb;

	$wpdb->hide_errors();

	$wpdb->query( "ALTER TABLE `$table` DROP INDEX `$index`" );

	// Now we need to take out all the extra ones we may have created.
	for ( $i = 0; $i < 25; $i++ ) {
		$wpdb->query( "ALTER TABLE `$table` DROP INDEX `{$index}_$i`" );
	}

	$wpdb->show_errors();

	return true;
}

/**
 * Adds an index to a specified table.
 *
 * @since 1.0.1
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param string $table Database table name.
 * @param string $index Database table index column.
 * @return true True, when done with execution.
 */
function add_clean_index( $table, $index ) {
	global $wpdb;

	drop_index( $table, $index );
	$wpdb->query( "ALTER TABLE `$table` ADD INDEX ( `$index` )" );

	return true;
}

/**
 * Adds column to a database table, if it doesn't already exist.
 *
 * @since 1.3.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param string $table_name  Database table name.
 * @param string $column_name Table column name.
 * @param string $create_ddl  SQL statement to add column.
 * @return bool True on success or if the column already exists. False on failure.
 */
function maybe_add_column( $table_name, $column_name, $create_ddl ) {
	global $wpdb;

	foreach ( $wpdb->get_col( "DESC $table_name", 0 ) as $column ) {
		if ( $column === $column_name ) {
			return true;
		}
	}

	// Didn't find it, so try to create it.
	$wpdb->query( $create_ddl );

	// We cannot directly tell that whether this succeeded!
	foreach ( $wpdb->get_col( "DESC $table_name", 0 ) as $column ) {
		if ( $column === $column_name ) {
			return true;
		}
	}

	return false;
}

/**
 * If a table only contains utf8 or utf8mb4 columns, convert it to utf8mb4.
 *
 * @since 4.2.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param string $table The table to convert.
 * @return bool True if the table was converted, false if it wasn't.
 */
function maybe_convert_table_to_utf8mb4( $table ) {
	global $wpdb;

	$results = $wpdb->get_results( "SHOW FULL COLUMNS FROM `$table`" );
	if ( ! $results ) {
		return false;
	}

	foreach ( $results as $column ) {
		if ( $column->Collation ) {
			list( $charset ) = explode( '_', $column->Collation );
			$charset         = strtolower( $charset );
			if ( 'utf8' !== $charset && 'utf8mb4' !== $charset ) {
				// Don't upgrade tables that have non-utf8 columns.
				return false;
			}
		}
	}

	$table_details = $wpdb->get_row( "SHOW TABLE STATUS LIKE '$table'" );
	if ( ! $table_details ) {
		return false;
	}

	list( $table_charset ) = explode( '_', $table_details->Collation );
	$table_charset         = strtolower( $table_charset );
	if ( 'utf8mb4' === $table_charset ) {
		return true;
	}

	return $wpdb->query( "ALTER TABLE $table CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci" );
}

/**
 * Retrieve all options as it was for 1.2.
 *
 * @since 1.2.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @return stdClass List of options.
 */
function get_alloptions_110() {
	global $wpdb;
	$all_options = new stdClass();
	$options     = $wpdb->get_results( "SELECT option_name, option_value FROM $wpdb->options" );
	if ( $options ) {
		foreach ( $options as $option ) {
			if ( 'siteurl' === $option->option_name || 'home' === $option->option_name || 'category_base' === $option->option_name ) {
				$option->option_value = untrailingslashit( $option->option_value );
			}
			$all_options->{$option->option_name} = stripslashes( $option->option_value );
		}
	}
	return $all_options;
}

/**
 * Utility version of get_option that is private to installation/upgrade.
 *
 * @ignore
 * @since 1.5.1
 * @access private
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param string $setting Option name.
 * @return mixed
 */
function __get_option( $setting ) { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionDoubleUnderscore,PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.FunctionDoubleUnderscore
	global $wpdb;

	if ( 'home' === $setting && defined( 'WP_HOME' ) ) {
		return untrailingslashit( WP_HOME );
	}

	if ( 'siteurl' === $setting && defined( 'WP_SITEURL' ) ) {
		return untrailingslashit( WP_SITEURL );
	}

	$option = $wpdb->get_var( $wpdb->prepare( "SELECT option_value FROM $wpdb->options WHERE option_name = %s", $setting ) );

	if ( 'home' === $setting && ! $option ) {
		return __get_option( 'siteurl' );
	}

	if ( in_array( $setting, array( 'siteurl', 'home', 'category_base', 'tag_base' ), true ) ) {
		$option = untrailingslashit( $option );
	}

	return maybe_unserialize( $option );
}

/**
 * Filters for content to remove unnecessary slashes.
 *
 * @since 1.5.0
 *
 * @param string $content The content to modify.
 * @return string The de-slashed content.
 */
function deslash( $content ) {
	// Note: \\\ inside a regex denotes a single backslash.

	/*
	 * Replace one or more backslashes followed by a single quote with
	 * a single quote.
	 */
	$content = preg_replace( "/\\\+'/", "'", $content );

	/*
	 * Replace one or more backslashes followed by a double quote with
	 * a double quote.
	 */
	$content = preg_replace( '/\\\+"/', '"', $content );

	// Replace one or more backslashes with one backslash.
	$content = preg_replace( '/\\\+/', '\\', $content );

	return $content;
}

/**
 * Modifies the database based on specified SQL statements.
 *
 * Useful for creating new tables and updating existing tables to a new structure.
 *
 * @since 1.5.0
 * @since 6.1.0 Ignores display width for integer data types on MySQL 8.0.17 or later,
 *              to match MySQL behavior. Note: This does not affect MariaDB.
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param string[]|string $queries Optional. The query to run. Can be multiple queries
 *                                 in an array, or a string of queries separated by
 *                                 semicolons. Default empty string.
 * @param bool            $execute Optional. Whether or not to execute the query right away.
 *                                 Default true.
 * @return string[] Strings containing the results of the various update queries.
 */
function dbDelta( $queries = '', $execute = true ) { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid
	global $wpdb;

	if ( in_array( $queries, array( '', 'all', 'blog', 'global', 'ms_global' ), true ) ) {
		$queries = wp_get_db_schema( $queries );
	}

	// Separate individual queries into an array.
	if ( ! is_array( $queries ) ) {
		$queries = explode( ';', $queries );
		$queries = array_filter( $queries );
	}

	/**
	 * Filters the dbDelta SQL queries.
	 *
	 * @since 3.3.0
	 *
	 * @param string[] $queries An array of dbDelta SQL queries.
	 */
	$queries = apply_filters( 'dbdelta_queries', $queries );

	$cqueries   = array(); // Creation queries.
	$iqueries   = array(); // Insertion queries.
	$for_update = array();

	// Create a tablename index for an array ($cqueries) of recognized query types.
	foreach ( $queries as $qry ) {
		if ( preg_match( '|CREATE TABLE ([^ ]*)|', $qry, $matches ) ) {
			$cqueries[ trim( $matches[1], '`' ) ] = $qry;
			$for_update[ $matches[1] ]            = 'Created table ' . $matches[1];
			continue;
		}

		if ( preg_match( '|CREATE DATABASE ([^ ]*)|', $qry, $matches ) ) {
			array_unshift( $cqueries, $qry );
			continue;
		}

		if ( preg_match( '|INSERT INTO ([^ ]*)|', $qry, $matches ) ) {
			$iqueries[] = $qry;
			continue;
		}

		if ( preg_match( '|UPDATE ([^ ]*)|', $qry, $matches ) ) {
			$iqueries[] = $qry;
			continue;
		}
	}

	/**
	 * Filters the dbDelta SQL queries for creating tables and/or databases.
	 *
	 * Queries filterable via this hook contain "CREATE TABLE" or "CREATE DATABASE".
	 *
	 * @since 3.3.0
	 *
	 * @param string[] $cqueries An array of dbDelta create SQL queries.
	 */
	$cqueries = apply_filters( 'dbdelta_create_queries', $cqueries );

	/**
	 * Filters the dbDelta SQL queries for inserting or updating.
	 *
	 * Queries filterable via this hook contain "INSERT INTO" or "UPDATE".
	 *
	 * @since 3.3.0
	 *
	 * @param string[] $iqueries An array of dbDelta insert or update SQL queries.
	 */
	$iqueries = apply_filters( 'dbdelta_insert_queries', $iqueries );

	$text_fields = array( 'tinytext', 'text', 'mediumtext', 'longtext' );
	$blob_fields = array( 'tinyblob', 'blob', 'mediumblob', 'longblob' );
	$int_fields  = array( 'tinyint', 'smallint', 'mediumint', 'int', 'integer', 'bigint' );

	$global_tables  = $wpdb->tables( 'global' );
	$db_version     = $wpdb->db_version();
	$db_server_info = $wpdb->db_server_info();

	foreach ( $cqueries as $table => $qry ) {
		// Upgrade global tables only for the main site. Don't upgrade at all if conditions are not optimal.
		if ( in_array( $table, $global_tables, true ) && ! wp_should_upgrade_global_tables() ) {
			unset( $cqueries[ $table ], $for_update[ $table ] );
			continue;
		}

		// Fetch the table column structure from the database.
		$suppress    = $wpdb->suppress_errors();
		$tablefields = $wpdb->get_results( "DESCRIBE {$table};" );
		$wpdb->suppress_errors( $suppress );

		if ( ! $tablefields ) {
			continue;
		}

		// Clear the field and index arrays.
		$cfields                  = array();
		$indices                  = array();
		$indices_without_subparts = array();

		// Get all of the field names in the query from between the parentheses.
		preg_match( '|\((.*)\)|ms', $qry, $match2 );
		$qryline = trim( $match2[1] );

		// Separate field lines into an array.
		$flds = explode( "\n", $qryline );

		// For every field line specified in the query.
		foreach ( $flds as $fld ) {
			$fld = trim( $fld, " \t\n\r\0\x0B," ); // Default trim characters, plus ','.

			// Extract the field name.
			preg_match( '|^([^ ]*)|', $fld, $fvals );
			$fieldname            = trim( $fvals[1], '`' );
			$fieldname_lowercased = strtolower( $fieldname );

			// Verify the found field name.
			$validfield = true;
			switch ( $fieldname_lowercased ) {
				case '':
				case 'primary':
				case 'index':
				case 'fulltext':
				case 'unique':
				case 'key':
				case 'spatial':
					$validfield = false;

					/*
					 * Normalize the index definition.
					 *
					 * This is done so the definition can be compared against the result of a
					 * `SHOW INDEX FROM $table_name` query which returns the current table
					 * index information.
					 */

					// Extract type, name and columns from the definition.
					preg_match(
						'/^
							(?P<index_type>             # 1) Type of the index.
								PRIMARY\s+KEY|(?:UNIQUE|FULLTEXT|SPATIAL)\s+(?:KEY|INDEX)|KEY|INDEX
							)
							\s+                         # Followed by at least one white space character.
							(?:                         # Name of the index. Optional if type is PRIMARY KEY.
								`?                      # Name can be escaped with a backtick.
									(?P<index_name>     # 2) Name of the index.
										(?:[0-9a-zA-Z$_-]|[\xC2-\xDF][\x80-\xBF])+
									)
								`?                      # Name can be escaped with a backtick.
								\s+                     # Followed by at least one white space character.
							)*
							\(                          # Opening bracket for the columns.
								(?P<index_columns>
									.+?                 # 3) Column names, index prefixes, and orders.
								)
							\)                          # Closing bracket for the columns.
						$/imx',
						$fld,
						$index_matches
					);

					// Uppercase the index type and normalize space characters.
					$index_type = strtoupper( preg_replace( '/\s+/', ' ', trim( $index_matches['index_type'] ) ) );

					// 'INDEX' is a synonym for 'KEY', standardize on 'KEY'.
					$index_type = str_replace( 'INDEX', 'KEY', $index_type );

					// Escape the index name with backticks. An index for a primary key has no name.
					$index_name = ( 'PRIMARY KEY' === $index_type ) ? '' : '`' . strtolower( $index_matches['index_name'] ) . '`';

					// Parse the columns. Multiple columns are separated by a comma.
					$index_columns                  = array_map( 'trim', explode( ',', $index_matches['index_columns'] ) );
					$index_columns_without_subparts = $index_columns;

					// Normalize columns.
					foreach ( $index_columns as $id => &$index_column ) {
						// Extract column name and number of indexed characters (sub_part).
						preg_match(
							'/
								`?                      # Name can be escaped with a backtick.
									(?P<column_name>    # 1) Name of the column.
										(?:[0-9a-zA-Z$_-]|[\xC2-\xDF][\x80-\xBF])+
									)
								`?                      # Name can be escaped with a backtick.
								(?:                     # Optional sub part.
									\s*                 # Optional white space character between name and opening bracket.
									\(                  # Opening bracket for the sub part.
										\s*             # Optional white space character after opening bracket.
										(?P<sub_part>
											\d+         # 2) Number of indexed characters.
										)
										\s*             # Optional white space character before closing bracket.
									\)                  # Closing bracket for the sub part.
								)?
							/x',
							$index_column,
							$index_column_matches
						);

						// Escape the column name with backticks.
						$index_column = '`' . $index_column_matches['column_name'] . '`';

						// We don't need to add the subpart to $index_columns_without_subparts
						$index_columns_without_subparts[ $id ] = $index_column;

						// Append the optional sup part with the number of indexed characters.
						if ( isset( $index_column_matches['sub_part'] ) ) {
							$index_column .= '(' . $index_column_matches['sub_part'] . ')';
						}
					}

					// Build the normalized index definition and add it to the list of indices.
					$indices[]                  = "{$index_type} {$index_name} (" . implode( ',', $index_columns ) . ')';
					$indices_without_subparts[] = "{$index_type} {$index_name} (" . implode( ',', $index_columns_without_subparts ) . ')';

					// Destroy no longer needed variables.
					unset( $index_column, $index_column_matches, $index_matches, $index_type, $index_name, $index_columns, $index_columns_without_subparts );

					break;
			}

			// If it's a valid field, add it to the field array.
			if ( $validfield ) {
				$cfields[ $fieldname_lowercased ] = $fld;
			}
		}

		// For every field in the table.
		foreach ( $tablefields as $tablefield ) {
			$tablefield_field_lowercased = strtolower( $tablefield->Field );
			$tablefield_type_lowercased  = strtolower( $tablefield->Type );

			$tablefield_type_without_parentheses = preg_replace(
				'/'
				. '(.+)'       // Field type, e.g. `int`.
				. '\(\d*\)'    // Display width.
				. '(.*)'       // Optional attributes, e.g. `unsigned`.
				. '/',
				'$1$2',
				$tablefield_type_lowercased
			);

			// Get the type without attributes, e.g. `int`.
			$tablefield_type_base = strtok( $tablefield_type_without_parentheses, ' ' );

			// If the table field exists in the field array...
			if ( array_key_exists( $tablefield_field_lowercased, $cfields ) ) {

				// Get the field type from the query.
				preg_match( '|`?' . $tablefield->Field . '`? ([^ ]*( unsigned)?)|i', $cfields[ $tablefield_field_lowercased ], $matches );
				$fieldtype            = $matches[1];
				$fieldtype_lowercased = strtolower( $fieldtype );

				$fieldtype_without_parentheses = preg_replace(
					'/'
					. '(.+)'       // Field type, e.g. `int`.
					. '\(\d*\)'    // Display width.
					. '(.*)'       // Optional attributes, e.g. `unsigned`.
					. '/',
					'$1$2',
					$fieldtype_lowercased
				);

				// Get the type without attributes, e.g. `int`.
				$fieldtype_base = strtok( $fieldtype_without_parentheses, ' ' );

				// Is actual field type different from the field type in query?
				if ( $tablefield->Type !== $fieldtype ) {
					$do_change = true;
					if ( in_array( $fieldtype_lowercased, $text_fields, true ) && in_array( $tablefield_type_lowercased, $text_fields, true ) ) {
						if ( array_search( $fieldtype_lowercased, $text_fields, true ) < array_search( $tablefield_type_lowercased, $text_fields, true ) ) {
							$do_change = false;
						}
					}

					if ( in_array( $fieldtype_lowercased, $blob_fields, true ) && in_array( $tablefield_type_lowercased, $blob_fields, true ) ) {
						if ( array_search( $fieldtype_lowercased, $blob_fields, true ) < array_search( $tablefield_type_lowercased, $blob_fields, true ) ) {
							$do_change = false;
						}
					}

					if ( in_array( $fieldtype_base, $int_fields, true ) && in_array( $tablefield_type_base, $int_fields, true )
						&& $fieldtype_without_parentheses === $tablefield_type_without_parentheses
					) {
						/*
						 * MySQL 8.0.17 or later does not support display width for integer data types,
						 * so if display width is the only difference, it can be safely ignored.
						 * Note: This is specific to MySQL and does not affect MariaDB.
						 */
						if ( version_compare( $db_version, '8.0.17', '>=' )
							&& ! str_contains( $db_server_info, 'MariaDB' )
						) {
							$do_change = false;
						}
					}

					if ( $do_change ) {
						// Add a query to change the column type.
						$cqueries[] = "ALTER TABLE {$table} CHANGE COLUMN `{$tablefield->Field}` " . $cfields[ $tablefield_field_lowercased ];

						$for_update[ $table . '.' . $tablefield->Field ] = "Changed type of {$table}.{$tablefield->Field} from {$tablefield->Type} to {$fieldtype}";
					}
				}

				// Get the default value from the array.
				if ( preg_match( "| DEFAULT '(.*?)'|i", $cfields[ $tablefield_field_lowercased ], $matches ) ) {
					$default_value = $matches[1];
					if ( $tablefield->Default !== $default_value ) {
						// Add a query to change the column's default value
						$cqueries[] = "ALTER TABLE {$table} ALTER COLUMN `{$tablefield->Field}` SET DEFAULT '{$default_value}'";

						$for_update[ $table . '.' . $tablefield->Field ] = "Changed default value of {$table}.{$tablefield->Field} from {$tablefield->Default} to {$default_value}";
					}
				}

				// Remove the field from the array (so it's not added).
				unset( $cfields[ $tablefield_field_lowercased ] );
			} else {
				// This field exists in the table, but not in the creation queries?
			}
		}

		// For every remaining field specified for the table.
		foreach ( $cfields as $fieldname => $fielddef ) {
			// Push a query line into $cqueries that adds the field to that table.
			$cqueries[] = "ALTER TABLE {$table} ADD COLUMN $fielddef";

			$for_update[ $table . '.' . $fieldname ] = 'Added column ' . $table . '.' . $fieldname;
		}

		// Index stuff goes here. Fetch the table index structure from the database.
		$tableindices = $wpdb->get_results( "SHOW INDEX FROM {$table};" );

		if ( $tableindices ) {
			// Clear the index array.
			$index_ary = array();

			// For every index in the table.
			foreach ( $tableindices as $tableindex ) {
				$keyname = strtolower( $tableindex->Key_name );

				// Add the index to the index data array.
				$index_ary[ $keyname ]['columns'][]  = array(
					'fieldname' => $tableindex->Column_name,
					'subpart'   => $tableindex->Sub_part,
				);
				$index_ary[ $keyname ]['unique']     = ( '0' === $tableindex->Non_unique ) ? true : false;
				$index_ary[ $keyname ]['index_type'] = $tableindex->Index_type;
			}

			// For each actual index in the index array.
			foreach ( $index_ary as $index_name => $index_data ) {

				// Build a create string to compare to the query.
				$index_string = '';
				if ( 'primary' === $index_name ) {
					$index_string .= 'PRIMARY ';
				} elseif ( $index_data['unique'] ) {
					$index_string .= 'UNIQUE ';
				}

				if ( 'FULLTEXT' === strtoupper( $index_data['index_type'] ) ) {
					$index_string .= 'FULLTEXT ';
				}

				if ( 'SPATIAL' === strtoupper( $index_data['index_type'] ) ) {
					$index_string .= 'SPATIAL ';
				}

				$index_string .= 'KEY ';
				if ( 'primary' !== $index_name ) {
					$index_string .= '`' . $index_name . '`';
				}

				$index_columns = '';

				// For each column in the index.
				foreach ( $index_data['columns'] as $column_data ) {
					if ( '' !== $index_columns ) {
						$index_columns .= ',';
					}

					// Add the field to the column list string.
					$index_columns .= '`' . $column_data['fieldname'] . '`';
				}

				// Add the column list to the index create string.
				$index_string .= " ($index_columns)";

				// Check if the index definition exists, ignoring subparts.
				$aindex = array_search( $index_string, $indices_without_subparts, true );
				if ( false !== $aindex ) {
					// If the index already exists (even with different subparts), we don't need to create it.
					unset( $indices_without_subparts[ $aindex ] );
					unset( $indices[ $aindex ] );
				}
			}
		}

		// For every remaining index specified for the table.
		foreach ( (array) $indices as $index ) {
			// Push a query line into $cqueries that adds the index to that table.
			$cqueries[] = "ALTER TABLE {$table} ADD $index";

			$for_update[] = 'Added index ' . $table . ' ' . $index;
		}

		// Remove the original table creation query from processing.
		unset( $cqueries[ $table ], $for_update[ $table ] );
	}

	$allqueries = array_merge( $cqueries, $iqueries );
	if ( $execute ) {
		foreach ( $allqueries as $query ) {
			$wpdb->query( $query );
		}
	}

	return $for_update;
}

/**
 * Updates the database tables to a new schema.
 *
 * By default, updates all the tables to use the latest defined schema, but can also
 * be used to update a specific set of tables in wp_get_db_schema().
 *
 * @since 1.5.0
 *
 * @uses dbDelta
 *
 * @param string $tables Optional. Which set of tables to update. Default is 'all'.
 */
function make_db_current( $tables = 'all' ) {
	$alterations = dbDelta( $tables );
	echo "<ol>\n";
	foreach ( $alterations as $alteration ) {
		echo "<li>$alteration</li>\n";
	}
	echo "</ol>\n";
}

/**
 * Updates the database tables to a new schema, but without displaying results.
 *
 * By default, updates all the tables to use the latest defined schema, but can
 * also be used to update a specific set of tables in wp_get_db_schema().
 *
 * @since 1.5.0
 *
 * @see make_db_current()
 *
 * @param string $tables Optional. Which set of tables to update. Default is 'all'.
 */
function make_db_current_silent( $tables = 'all' ) {
	dbDelta( $tables );
}

/**
 * Creates a site theme from an existing theme.
 *
 * {@internal Missing Long Description}}
 *
 * @since 1.5.0
 *
 * @param string $theme_name The name of the theme.
 * @param string $template   The directory name of the theme.
 * @return bool
 */
function make_site_theme_from_oldschool( $theme_name, $template ) {
	$home_path   = get_home_path();
	$site_dir    = WP_CONTENT_DIR . "/themes/$template";
	$default_dir = WP_CONTENT_DIR . '/themes/' . WP_DEFAULT_THEME;

	if ( ! file_exists( "$home_path/index.php" ) ) {
		return false;
	}

	/*
	 * Copy files from the old locations to the site theme.
	 * TODO: This does not copy arbitrary include dependencies. Only the standard WP files are copied.
	 */
	$files = array(
		'index.php'             => 'index.php',
		'wp-layout.css'         => 'style.css',
		'wp-comments.php'       => 'comments.php',
		'wp-comments-popup.php' => 'comments-popup.php',
	);

	foreach ( $files as $oldfile => $newfile ) {
		if ( 'index.php' === $oldfile ) {
			$oldpath = $home_path;
		} else {
			$oldpath = ABSPATH;
		}

		// Check to make sure it's not a new index.
		if ( 'index.php' === $oldfile ) {
			$index = implode( '', file( "$oldpath/$oldfile" ) );
			if ( str_contains( $index, 'WP_USE_THEMES' ) ) {
				if ( ! copy( "$default_dir/$oldfile", "$site_dir/$newfile" ) ) {
					return false;
				}

				// Don't copy anything.
				continue;
			}
		}

		if ( ! copy( "$oldpath/$oldfile", "$site_dir/$newfile" ) ) {
			return false;
		}

		chmod( "$site_dir/$newfile", 0777 );

		// Update the blog header include in each file.
		$lines = explode( "\n", implode( '', file( "$site_dir/$newfile" ) ) );
		if ( $lines ) {
			$f = fopen( "$site_dir/$newfile", 'w' );

			foreach ( $lines as $line ) {
				if ( preg_match( '/require.*wp-blog-header/', $line ) ) {
					$line = '//' . $line;
				}

				// Update stylesheet references.
				$line = str_replace(
					"<?php echo __get_option('siteurl'); ?>/wp-layout.css",
					"<?php bloginfo('stylesheet_url'); ?>",
					$line
				);

				// Update comments template inclusion.
				$line = str_replace(
					"<?php include(ABSPATH . 'wp-comments.php'); ?>",
					'<?php comments_template(); ?>',
					$line
				);

				fwrite( $f, "{$line}\n" );
			}
			fclose( $f );
		}
	}

	// Add a theme header.
	$header = "/*\n" .
		"Theme Name: $theme_name\n" .
		'Theme URI: ' . __get_option( 'siteurl' ) . "\n" .
		"Description: A theme automatically created by the update.\n" .
		"Version: 1.0\n" .
		"Author: Moi\n" .
		"*/\n";

	$stylelines = file_get_contents( "$site_dir/style.css" );
	if ( $stylelines ) {
		$f = fopen( "$site_dir/style.css", 'w' );

		fwrite( $f, $header );
		fwrite( $f, $stylelines );
		fclose( $f );
	}

	return true;
}

/**
 * Creates a site theme from the default theme.
 *
 * {@internal Missing Long Description}}
 *
 * @since 1.5.0
 *
 * @param string $theme_name The name of the theme.
 * @param string $template   The directory name of the theme.
 * @return void|false
 */
function make_site_theme_from_default( $theme_name, $template ) {
	$site_dir    = WP_CONTENT_DIR . "/themes/$template";
	$default_dir = WP_CONTENT_DIR . '/themes/' . WP_DEFAULT_THEME;

	/*
	 * Copy files from the default theme to the site theme.
	 * $files = array( 'index.php', 'comments.php', 'comments-popup.php', 'footer.php', 'header.php', 'sidebar.php', 'style.css' );
	 */

	$theme_dir = @opendir( $default_dir );
	if ( $theme_dir ) {
		while ( ( $theme_file = readdir( $theme_dir ) ) !== false ) {
			if ( is_dir( "$default_dir/$theme_file" ) ) {
				continue;
			}

			if ( ! copy( "$default_dir/$theme_file", "$site_dir/$theme_file" ) ) {
				return;
			}

			chmod( "$site_dir/$theme_file", 0777 );
		}

		closedir( $theme_dir );
	}

	// Rewrite the theme header.
	$stylelines = explode( "\n", implode( '', file( "$site_dir/style.css" ) ) );
	if ( $stylelines ) {
		$f = fopen( "$site_dir/style.css", 'w' );

		$headers = array(
			'Theme Name:'  => $theme_name,
			'Theme URI:'   => __get_option( 'url' ),
			'Description:' => 'Your theme.',
			'Version:'     => '1',
			'Author:'      => 'You',
		);

		foreach ( $stylelines as $line ) {
			foreach ( $headers as $header => $value ) {
				if ( str_contains( $line, $header ) ) {
					$line = $header . ' ' . $value;
					break;
				}
			}

			fwrite( $f, $line . "\n" );
		}

		fclose( $f );
	}

	// Copy the images.
	umask( 0 );
	if ( ! mkdir( "$site_dir/images", 0777 ) ) {
		return false;
	}

	$images_dir = @opendir( "$default_dir/images" );
	if ( $images_dir ) {
		while ( ( $image = readdir( $images_dir ) ) !== false ) {
			if ( is_dir( "$default_dir/images/$image" ) ) {
				continue;
			}

			if ( ! copy( "$default_dir/images/$image", "$site_dir/images/$image" ) ) {
				return;
			}

			chmod( "$site_dir/images/$image", 0777 );
		}

		closedir( $images_dir );
	}
}

/**
 * Creates a site theme.
 *
 * {@internal Missing Long Description}}
 *
 * @since 1.5.0
 *
 * @return string|false
 */
function make_site_theme() {
	// Name the theme after the blog.
	$theme_name = __get_option( 'blogname' );
	$template   = sanitize_title( $theme_name );
	$site_dir   = WP_CONTENT_DIR . "/themes/$template";

	// If the theme already exists, nothing to do.
	if ( is_dir( $site_dir ) ) {
		return false;
	}

	// We must be able to write to the themes dir.
	if ( ! is_writable( WP_CONTENT_DIR . '/themes' ) ) {
		return false;
	}

	umask( 0 );
	if ( ! mkdir( $site_dir, 0777 ) ) {
		return false;
	}

	if ( file_exists( ABSPATH . 'wp-layout.css' ) ) {
		if ( ! make_site_theme_from_oldschool( $theme_name, $template ) ) {
			// TODO: rm -rf the site theme directory.
			return false;
		}
	} else {
		if ( ! make_site_theme_from_default( $theme_name, $template ) ) {
			// TODO: rm -rf the site theme directory.
			return false;
		}
	}

	// Make the new site theme active.
	$current_template = __get_option( 'template' );
	if ( WP_DEFAULT_THEME === $current_template ) {
		update_option( 'template', $template );
		update_option( 'stylesheet', $template );
	}
	return $template;
}

/**
 * Translate user level to user role name.
 *
 * @since 2.0.0
 *
 * @param int $level User level.
 * @return string User role name.
 */
function translate_level_to_role( $level ) {
	switch ( $level ) {
		case 10:
		case 9:
		case 8:
			return 'administrator';
		case 7:
		case 6:
		case 5:
			return 'editor';
		case 4:
		case 3:
		case 2:
			return 'author';
		case 1:
			return 'contributor';
		case 0:
		default:
			return 'subscriber';
	}
}

/**
 * Checks the version of the installed MySQL binary.
 *
 * @since 2.1.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 */
function wp_check_mysql_version() {
	global $wpdb;
	$result = $wpdb->check_database_version();
	if ( is_wp_error( $result ) ) {
		wp_die( $result );
	}
}

/**
 * Disables the Automattic widgets plugin, which was merged into core.
 *
 * @since 2.2.0
 */
function maybe_disable_automattic_widgets() {
	$plugins = __get_option( 'active_plugins' );

	foreach ( (array) $plugins as $plugin ) {
		if ( 'widgets.php' === basename( $plugin ) ) {
			array_splice( $plugins, array_search( $plugin, $plugins, true ), 1 );
			update_option( 'active_plugins', $plugins );
			break;
		}
	}
}

/**
 * Disables the Link Manager on upgrade if, at the time of upgrade, no links exist in the DB.
 *
 * @since 3.5.0
 *
 * @global int  $wp_current_db_version The old (current) database version.
 * @global wpdb $wpdb                  WordPress database abstraction object.
 */
function maybe_disable_link_manager() {
	global $wp_current_db_version, $wpdb;

	if ( $wp_current_db_version >= 22006 && get_option( 'link_manager_enabled' ) && ! $wpdb->get_var( "SELECT link_id FROM $wpdb->links LIMIT 1" ) ) {
		update_option( 'link_manager_enabled', 0 );
	}
}

/**
 * Runs before the schema is upgraded.
 *
 * @since 2.9.0
 *
 * @global int  $wp_current_db_version The old (current) database version.
 * @global wpdb $wpdb                  WordPress database abstraction object.
 */
function pre_schema_upgrade() {
	global $wp_current_db_version, $wpdb;

	// Upgrade versions prior to 2.9.
	if ( $wp_current_db_version < 11557 ) {
		// Delete duplicate options. Keep the option with the highest option_id.
		$wpdb->query( "DELETE o1 FROM $wpdb->options AS o1 JOIN $wpdb->options AS o2 USING (`option_name`) WHERE o2.option_id > o1.option_id" );

		// Drop the old primary key and add the new.
		$wpdb->query( "ALTER TABLE $wpdb->options DROP PRIMARY KEY, ADD PRIMARY KEY(option_id)" );

		// Drop the old option_name index. dbDelta() doesn't do the drop.
		$wpdb->query( "ALTER TABLE $wpdb->options DROP INDEX option_name" );
	}

	// Multisite schema upgrades.
	if ( $wp_current_db_version < 25448 && is_multisite() && wp_should_upgrade_global_tables() ) {

		// Upgrade versions prior to 3.7.
		if ( $wp_current_db_version < 25179 ) {
			// New primary key for signups.
			$wpdb->query( "ALTER TABLE $wpdb->signups ADD signup_id BIGINT(20) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST" );
			$wpdb->query( "ALTER TABLE $wpdb->signups DROP INDEX domain" );
		}

		if ( $wp_current_db_version < 25448 ) {
			// Convert archived from enum to tinyint.
			$wpdb->query( "ALTER TABLE $wpdb->blogs CHANGE COLUMN archived archived varchar(1) NOT NULL default '0'" );
			$wpdb->query( "ALTER TABLE $wpdb->blogs CHANGE COLUMN archived archived tinyint(2) NOT NULL default 0" );
		}
	}

	// Upgrade versions prior to 4.2.
	if ( $wp_current_db_version < 31351 ) {
		if ( ! is_multisite() && wp_should_upgrade_global_tables() ) {
			$wpdb->query( "ALTER TABLE $wpdb->usermeta DROP INDEX meta_key, ADD INDEX meta_key(meta_key(191))" );
		}
		$wpdb->query( "ALTER TABLE $wpdb->terms DROP INDEX slug, ADD INDEX slug(slug(191))" );
		$wpdb->query( "ALTER TABLE $wpdb->terms DROP INDEX name, ADD INDEX name(name(191))" );
		$wpdb->query( "ALTER TABLE $wpdb->commentmeta DROP INDEX meta_key, ADD INDEX meta_key(meta_key(191))" );
		$wpdb->query( "ALTER TABLE $wpdb->postmeta DROP INDEX meta_key, ADD INDEX meta_key(meta_key(191))" );
		$wpdb->query( "ALTER TABLE $wpdb->posts DROP INDEX post_name, ADD INDEX post_name(post_name(191))" );
	}

	// Upgrade versions prior to 4.4.
	if ( $wp_current_db_version < 34978 ) {
		// If compatible termmeta table is found, use it, but enforce a proper index and update collation.
		if ( $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->termmeta}'" ) && $wpdb->get_results( "SHOW INDEX FROM {$wpdb->termmeta} WHERE Column_name = 'meta_key'" ) ) {
			$wpdb->query( "ALTER TABLE $wpdb->termmeta DROP INDEX meta_key, ADD INDEX meta_key(meta_key(191))" );
			maybe_convert_table_to_utf8mb4( $wpdb->termmeta );
		}
	}
}

/**
 * Determine if global tables should be upgraded.
 *
 * This function performs a series of checks to ensure the environment allows
 * for the safe upgrading of global WordPress database tables. It is necessary
 * because global tables will commonly grow to millions of rows on large
 * installations, and the ability to control their upgrade routines can be
 * critical to the operation of large networks.
 *
 * In a future iteration, this function may use `wp_is_large_network()` to more-
 * intelligently prevent global table upgrades. Until then, we make sure
 * WordPress is on the main site of the main network, to avoid running queries
 * more than once in multi-site or multi-network environments.
 *
 * @since 4.3.0
 *
 * @return bool Whether to run the upgrade routines on global tables.
 */
function wp_should_upgrade_global_tables() {

	// Return false early if explicitly not upgrading.
	if ( defined( 'DO_NOT_UPGRADE_GLOBAL_TABLES' ) ) {
		return false;
	}

	// Assume global tables should be upgraded.
	$should_upgrade = true;

	// Set to false if not on main network (does not matter if not multi-network).
	if ( ! is_main_network() ) {
		$should_upgrade = false;
	}

	// Set to false if not on main site of current network (does not matter if not multi-site).
	if ( ! is_main_site() ) {
		$should_upgrade = false;
	}

	/**
	 * Filters if upgrade routines should be run on global tables.
	 *
	 * @since 4.3.0
	 *
	 * @param bool $should_upgrade Whether to run the upgrade routines on global tables.
	 */
	return apply_filters( 'wp_should_upgrade_global_tables', $should_upgrade );
}
class-wp-filesystem-ssh2.php000060400000055416151327343360012063 0ustar00<?php
/**
 * WordPress Filesystem Class for implementing SSH2
 *
 * To use this class you must follow these steps for PHP 5.2.6+
 *
 * {@link http://kevin.vanzonneveld.net/techblog/article/make_ssh_connections_with_php/ - Installation Notes}
 *
 * Compile libssh2 (Note: Only 0.14 is officially working with PHP 5.2.6+ right now, But many users have found the latest versions work)
 *
 * cd /usr/src
 * wget https://www.libssh2.org/download/libssh2-0.14.tar.gz
 * tar -zxvf libssh2-0.14.tar.gz
 * cd libssh2-0.14/
 * ./configure
 * make all install
 *
 * Note: Do not leave the directory yet!
 *
 * Enter: pecl install -f ssh2
 *
 * Copy the ssh.so file it creates to your PHP Module Directory.
 * Open up your PHP.INI file and look for where extensions are placed.
 * Add in your PHP.ini file: extension=ssh2.so
 *
 * Restart Apache!
 * Check phpinfo() streams to confirm that: ssh2.shell, ssh2.exec, ssh2.tunnel, ssh2.scp, ssh2.sftp  exist.
 *
 * Note: As of WordPress 2.8, this utilizes the PHP5+ function `stream_get_contents()`.
 *
 * @since 2.7.0
 *
 * @package WordPress
 * @subpackage Filesystem
 */
class WP_Filesystem_SSH2 extends WP_Filesystem_Base {

	/**
	 * @since 2.7.0
	 * @var resource
	 */
	public $link = false;

	/**
	 * @since 2.7.0
	 * @var resource
	 */
	public $sftp_link;

	/**
	 * @since 2.7.0
	 * @var bool
	 */
	public $keys = false;

	/**
	 * Constructor.
	 *
	 * @since 2.7.0
	 *
	 * @param array $opt
	 */
	public function __construct( $opt = '' ) {
		$this->method = 'ssh2';
		$this->errors = new WP_Error();

		// Check if possible to use ssh2 functions.
		if ( ! extension_loaded( 'ssh2' ) ) {
			$this->errors->add( 'no_ssh2_ext', __( 'The ssh2 PHP extension is not available' ) );
			return;
		}

		// Set defaults:
		if ( empty( $opt['port'] ) ) {
			$this->options['port'] = 22;
		} else {
			$this->options['port'] = $opt['port'];
		}

		if ( empty( $opt['hostname'] ) ) {
			$this->errors->add( 'empty_hostname', __( 'SSH2 hostname is required' ) );
		} else {
			$this->options['hostname'] = $opt['hostname'];
		}

		// Check if the options provided are OK.
		if ( ! empty( $opt['public_key'] ) && ! empty( $opt['private_key'] ) ) {
			$this->options['public_key']  = $opt['public_key'];
			$this->options['private_key'] = $opt['private_key'];

			$this->options['hostkey'] = array( 'hostkey' => 'ssh-rsa,ssh-ed25519' );

			$this->keys = true;
		} elseif ( empty( $opt['username'] ) ) {
			$this->errors->add( 'empty_username', __( 'SSH2 username is required' ) );
		}

		if ( ! empty( $opt['username'] ) ) {
			$this->options['username'] = $opt['username'];
		}

		if ( empty( $opt['password'] ) ) {
			// Password can be blank if we are using keys.
			if ( ! $this->keys ) {
				$this->errors->add( 'empty_password', __( 'SSH2 password is required' ) );
			} else {
				$this->options['password'] = null;
			}
		} else {
			$this->options['password'] = $opt['password'];
		}
	}

	/**
	 * Connects filesystem.
	 *
	 * @since 2.7.0
	 *
	 * @return bool True on success, false on failure.
	 */
	public function connect() {
		if ( ! $this->keys ) {
			$this->link = @ssh2_connect( $this->options['hostname'], $this->options['port'] );
		} else {
			$this->link = @ssh2_connect( $this->options['hostname'], $this->options['port'], $this->options['hostkey'] );
		}

		if ( ! $this->link ) {
			$this->errors->add(
				'connect',
				sprintf(
					/* translators: %s: hostname:port */
					__( 'Failed to connect to SSH2 Server %s' ),
					$this->options['hostname'] . ':' . $this->options['port']
				)
			);

			return false;
		}

		if ( ! $this->keys ) {
			if ( ! @ssh2_auth_password( $this->link, $this->options['username'], $this->options['password'] ) ) {
				$this->errors->add(
					'auth',
					sprintf(
						/* translators: %s: Username. */
						__( 'Username/Password incorrect for %s' ),
						$this->options['username']
					)
				);

				return false;
			}
		} else {
			if ( ! @ssh2_auth_pubkey_file( $this->link, $this->options['username'], $this->options['public_key'], $this->options['private_key'], $this->options['password'] ) ) {
				$this->errors->add(
					'auth',
					sprintf(
						/* translators: %s: Username. */
						__( 'Public and Private keys incorrect for %s' ),
						$this->options['username']
					)
				);

				return false;
			}
		}

		$this->sftp_link = ssh2_sftp( $this->link );

		if ( ! $this->sftp_link ) {
			$this->errors->add(
				'connect',
				sprintf(
					/* translators: %s: hostname:port */
					__( 'Failed to initialize a SFTP subsystem session with the SSH2 Server %s' ),
					$this->options['hostname'] . ':' . $this->options['port']
				)
			);

			return false;
		}

		return true;
	}

	/**
	 * Gets the ssh2.sftp PHP stream wrapper path to open for the given file.
	 *
	 * This method also works around a PHP bug where the root directory (/) cannot
	 * be opened by PHP functions, causing a false failure. In order to work around
	 * this, the path is converted to /./ which is semantically the same as /
	 * See https://bugs.php.net/bug.php?id=64169 for more details.
	 *
	 * @since 4.4.0
	 *
	 * @param string $path The File/Directory path on the remote server to return
	 * @return string The ssh2.sftp:// wrapped path to use.
	 */
	public function sftp_path( $path ) {
		if ( '/' === $path ) {
			$path = '/./';
		}

		return 'ssh2.sftp://' . $this->sftp_link . '/' . ltrim( $path, '/' );
	}

	/**
	 * @since 2.7.0
	 *
	 * @param string $command
	 * @param bool   $returnbool
	 * @return bool|string True on success, false on failure. String if the command was executed, `$returnbool`
	 *                     is false (default), and data from the resulting stream was retrieved.
	 */
	public function run_command( $command, $returnbool = false ) {
		if ( ! $this->link ) {
			return false;
		}

		$stream = ssh2_exec( $this->link, $command );

		if ( ! $stream ) {
			$this->errors->add(
				'command',
				sprintf(
					/* translators: %s: Command. */
					__( 'Unable to perform command: %s' ),
					$command
				)
			);
		} else {
			stream_set_blocking( $stream, true );
			stream_set_timeout( $stream, FS_TIMEOUT );
			$data = stream_get_contents( $stream );
			fclose( $stream );

			if ( $returnbool ) {
				return ( false === $data ) ? false : '' !== trim( $data );
			} else {
				return $data;
			}
		}

		return false;
	}

	/**
	 * Reads entire file into a string.
	 *
	 * @since 2.7.0
	 *
	 * @param string $file Name of the file to read.
	 * @return string|false Read data on success, false if no temporary file could be opened,
	 *                      or if the file couldn't be retrieved.
	 */
	public function get_contents( $file ) {
		return file_get_contents( $this->sftp_path( $file ) );
	}

	/**
	 * Reads entire file into an array.
	 *
	 * @since 2.7.0
	 *
	 * @param string $file Path to the file.
	 * @return array|false File contents in an array on success, false on failure.
	 */
	public function get_contents_array( $file ) {
		return file( $this->sftp_path( $file ) );
	}

	/**
	 * Writes a string to a file.
	 *
	 * @since 2.7.0
	 *
	 * @param string    $file     Remote path to the file where to write the data.
	 * @param string    $contents The data to write.
	 * @param int|false $mode     Optional. The file permissions as octal number, usually 0644.
	 *                            Default false.
	 * @return bool True on success, false on failure.
	 */
	public function put_contents( $file, $contents, $mode = false ) {
		$ret = file_put_contents( $this->sftp_path( $file ), $contents );

		if ( strlen( $contents ) !== $ret ) {
			return false;
		}

		$this->chmod( $file, $mode );

		return true;
	}

	/**
	 * Gets the current working directory.
	 *
	 * @since 2.7.0
	 *
	 * @return string|false The current working directory on success, false on failure.
	 */
	public function cwd() {
		$cwd = ssh2_sftp_realpath( $this->sftp_link, '.' );

		if ( $cwd ) {
			$cwd = trailingslashit( trim( $cwd ) );
		}

		return $cwd;
	}

	/**
	 * Changes current directory.
	 *
	 * @since 2.7.0
	 *
	 * @param string $dir The new current directory.
	 * @return bool True on success, false on failure.
	 */
	public function chdir( $dir ) {
		return $this->run_command( 'cd ' . $dir, true );
	}

	/**
	 * Changes the file group.
	 *
	 * @since 2.7.0
	 *
	 * @param string     $file      Path to the file.
	 * @param string|int $group     A group name or number.
	 * @param bool       $recursive Optional. If set to true, changes file group recursively.
	 *                              Default false.
	 * @return bool True on success, false on failure.
	 */
	public function chgrp( $file, $group, $recursive = false ) {
		if ( ! $this->exists( $file ) ) {
			return false;
		}

		if ( ! $recursive || ! $this->is_dir( $file ) ) {
			return $this->run_command( sprintf( 'chgrp %s %s', escapeshellarg( $group ), escapeshellarg( $file ) ), true );
		}

		return $this->run_command( sprintf( 'chgrp -R %s %s', escapeshellarg( $group ), escapeshellarg( $file ) ), true );
	}

	/**
	 * Changes filesystem permissions.
	 *
	 * @since 2.7.0
	 *
	 * @param string    $file      Path to the file.
	 * @param int|false $mode      Optional. The permissions as octal number, usually 0644 for files,
	 *                             0755 for directories. Default false.
	 * @param bool      $recursive Optional. If set to true, changes file permissions recursively.
	 *                             Default false.
	 * @return bool True on success, false on failure.
	 */
	public function chmod( $file, $mode = false, $recursive = false ) {
		if ( ! $this->exists( $file ) ) {
			return false;
		}

		if ( ! $mode ) {
			if ( $this->is_file( $file ) ) {
				$mode = FS_CHMOD_FILE;
			} elseif ( $this->is_dir( $file ) ) {
				$mode = FS_CHMOD_DIR;
			} else {
				return false;
			}
		}

		if ( ! $recursive || ! $this->is_dir( $file ) ) {
			return $this->run_command( sprintf( 'chmod %o %s', $mode, escapeshellarg( $file ) ), true );
		}

		return $this->run_command( sprintf( 'chmod -R %o %s', $mode, escapeshellarg( $file ) ), true );
	}

	/**
	 * Changes the owner of a file or directory.
	 *
	 * @since 2.7.0
	 *
	 * @param string     $file      Path to the file or directory.
	 * @param string|int $owner     A user name or number.
	 * @param bool       $recursive Optional. If set to true, changes file owner recursively.
	 *                              Default false.
	 * @return bool True on success, false on failure.
	 */
	public function chown( $file, $owner, $recursive = false ) {
		if ( ! $this->exists( $file ) ) {
			return false;
		}

		if ( ! $recursive || ! $this->is_dir( $file ) ) {
			return $this->run_command( sprintf( 'chown %s %s', escapeshellarg( $owner ), escapeshellarg( $file ) ), true );
		}

		return $this->run_command( sprintf( 'chown -R %s %s', escapeshellarg( $owner ), escapeshellarg( $file ) ), true );
	}

	/**
	 * Gets the file owner.
	 *
	 * @since 2.7.0
	 *
	 * @param string $file Path to the file.
	 * @return string|false Username of the owner on success, false on failure.
	 */
	public function owner( $file ) {
		$owneruid = @fileowner( $this->sftp_path( $file ) );

		if ( ! $owneruid ) {
			return false;
		}

		if ( ! function_exists( 'posix_getpwuid' ) ) {
			return $owneruid;
		}

		$ownerarray = posix_getpwuid( $owneruid );

		if ( ! $ownerarray ) {
			return false;
		}

		return $ownerarray['name'];
	}

	/**
	 * Gets the permissions of the specified file or filepath in their octal format.
	 *
	 * @since 2.7.0
	 *
	 * @param string $file Path to the file.
	 * @return string Mode of the file (the last 3 digits).
	 */
	public function getchmod( $file ) {
		return substr( decoct( @fileperms( $this->sftp_path( $file ) ) ), -3 );
	}

	/**
	 * Gets the file's group.
	 *
	 * @since 2.7.0
	 *
	 * @param string $file Path to the file.
	 * @return string|false The group on success, false on failure.
	 */
	public function group( $file ) {
		$gid = @filegroup( $this->sftp_path( $file ) );

		if ( ! $gid ) {
			return false;
		}

		if ( ! function_exists( 'posix_getgrgid' ) ) {
			return $gid;
		}

		$grouparray = posix_getgrgid( $gid );

		if ( ! $grouparray ) {
			return false;
		}

		return $grouparray['name'];
	}

	/**
	 * Copies a file.
	 *
	 * @since 2.7.0
	 *
	 * @param string    $source      Path to the source file.
	 * @param string    $destination Path to the destination file.
	 * @param bool      $overwrite   Optional. Whether to overwrite the destination file if it exists.
	 *                               Default false.
	 * @param int|false $mode        Optional. The permissions as octal number, usually 0644 for files,
	 *                               0755 for dirs. Default false.
	 * @return bool True on success, false on failure.
	 */
	public function copy( $source, $destination, $overwrite = false, $mode = false ) {
		if ( ! $overwrite && $this->exists( $destination ) ) {
			return false;
		}

		$content = $this->get_contents( $source );

		if ( false === $content ) {
			return false;
		}

		return $this->put_contents( $destination, $content, $mode );
	}

	/**
	 * Moves a file or directory.
	 *
	 * After moving files or directories, OPcache will need to be invalidated.
	 *
	 * If moving a directory fails, `copy_dir()` can be used for a recursive copy.
	 *
	 * Use `move_dir()` for moving directories with OPcache invalidation and a
	 * fallback to `copy_dir()`.
	 *
	 * @since 2.7.0
	 *
	 * @param string $source      Path to the source file or directory.
	 * @param string $destination Path to the destination file or directory.
	 * @param bool   $overwrite   Optional. Whether to overwrite the destination if it exists.
	 *                            Default false.
	 * @return bool True on success, false on failure.
	 */
	public function move( $source, $destination, $overwrite = false ) {
		if ( $this->exists( $destination ) ) {
			if ( $overwrite ) {
				// We need to remove the destination before we can rename the source.
				$this->delete( $destination, false, 'f' );
			} else {
				// If we're not overwriting, the rename will fail, so return early.
				return false;
			}
		}

		return ssh2_sftp_rename( $this->sftp_link, $source, $destination );
	}

	/**
	 * Deletes a file or directory.
	 *
	 * @since 2.7.0
	 *
	 * @param string       $file      Path to the file or directory.
	 * @param bool         $recursive Optional. If set to true, deletes files and folders recursively.
	 *                                Default false.
	 * @param string|false $type      Type of resource. 'f' for file, 'd' for directory.
	 *                                Default false.
	 * @return bool True on success, false on failure.
	 */
	public function delete( $file, $recursive = false, $type = false ) {
		if ( 'f' === $type || $this->is_file( $file ) ) {
			return ssh2_sftp_unlink( $this->sftp_link, $file );
		}

		if ( ! $recursive ) {
			return ssh2_sftp_rmdir( $this->sftp_link, $file );
		}

		$filelist = $this->dirlist( $file );

		if ( is_array( $filelist ) ) {
			foreach ( $filelist as $filename => $fileinfo ) {
				$this->delete( $file . '/' . $filename, $recursive, $fileinfo['type'] );
			}
		}

		return ssh2_sftp_rmdir( $this->sftp_link, $file );
	}

	/**
	 * Checks if a file or directory exists.
	 *
	 * @since 2.7.0
	 *
	 * @param string $path Path to file or directory.
	 * @return bool Whether $path exists or not.
	 */
	public function exists( $path ) {
		return file_exists( $this->sftp_path( $path ) );
	}

	/**
	 * Checks if resource is a file.
	 *
	 * @since 2.7.0
	 *
	 * @param string $file File path.
	 * @return bool Whether $file is a file.
	 */
	public function is_file( $file ) {
		return is_file( $this->sftp_path( $file ) );
	}

	/**
	 * Checks if resource is a directory.
	 *
	 * @since 2.7.0
	 *
	 * @param string $path Directory path.
	 * @return bool Whether $path is a directory.
	 */
	public function is_dir( $path ) {
		return is_dir( $this->sftp_path( $path ) );
	}

	/**
	 * Checks if a file is readable.
	 *
	 * @since 2.7.0
	 *
	 * @param string $file Path to file.
	 * @return bool Whether $file is readable.
	 */
	public function is_readable( $file ) {
		return is_readable( $this->sftp_path( $file ) );
	}

	/**
	 * Checks if a file or directory is writable.
	 *
	 * @since 2.7.0
	 *
	 * @param string $path Path to file or directory.
	 * @return bool Whether $path is writable.
	 */
	public function is_writable( $path ) {
		// PHP will base its writable checks on system_user === file_owner, not ssh_user === file_owner.
		return true;
	}

	/**
	 * Gets the file's last access time.
	 *
	 * @since 2.7.0
	 *
	 * @param string $file Path to file.
	 * @return int|false Unix timestamp representing last access time, false on failure.
	 */
	public function atime( $file ) {
		return fileatime( $this->sftp_path( $file ) );
	}

	/**
	 * Gets the file modification time.
	 *
	 * @since 2.7.0
	 *
	 * @param string $file Path to file.
	 * @return int|false Unix timestamp representing modification time, false on failure.
	 */
	public function mtime( $file ) {
		return filemtime( $this->sftp_path( $file ) );
	}

	/**
	 * Gets the file size (in bytes).
	 *
	 * @since 2.7.0
	 *
	 * @param string $file Path to file.
	 * @return int|false Size of the file in bytes on success, false on failure.
	 */
	public function size( $file ) {
		return filesize( $this->sftp_path( $file ) );
	}

	/**
	 * Sets the access and modification times of a file.
	 *
	 * Note: Not implemented.
	 *
	 * @since 2.7.0
	 *
	 * @param string $file  Path to file.
	 * @param int    $time  Optional. Modified time to set for file.
	 *                      Default 0.
	 * @param int    $atime Optional. Access time to set for file.
	 *                      Default 0.
	 */
	public function touch( $file, $time = 0, $atime = 0 ) {
		// Not implemented.
	}

	/**
	 * Creates a directory.
	 *
	 * @since 2.7.0
	 *
	 * @param string           $path  Path for new directory.
	 * @param int|false        $chmod Optional. The permissions as octal number (or false to skip chmod).
	 *                                Default false.
	 * @param string|int|false $chown Optional. A user name or number (or false to skip chown).
	 *                                Default false.
	 * @param string|int|false $chgrp Optional. A group name or number (or false to skip chgrp).
	 *                                Default false.
	 * @return bool True on success, false on failure.
	 */
	public function mkdir( $path, $chmod = false, $chown = false, $chgrp = false ) {
		$path = untrailingslashit( $path );

		if ( empty( $path ) ) {
			return false;
		}

		if ( ! $chmod ) {
			$chmod = FS_CHMOD_DIR;
		}

		if ( ! ssh2_sftp_mkdir( $this->sftp_link, $path, $chmod, true ) ) {
			return false;
		}

		// Set directory permissions.
		ssh2_sftp_chmod( $this->sftp_link, $path, $chmod );

		if ( $chown ) {
			$this->chown( $path, $chown );
		}

		if ( $chgrp ) {
			$this->chgrp( $path, $chgrp );
		}

		return true;
	}

	/**
	 * Deletes a directory.
	 *
	 * @since 2.7.0
	 *
	 * @param string $path      Path to directory.
	 * @param bool   $recursive Optional. Whether to recursively remove files/directories.
	 *                          Default false.
	 * @return bool True on success, false on failure.
	 */
	public function rmdir( $path, $recursive = false ) {
		return $this->delete( $path, $recursive );
	}

	/**
	 * Gets details for files in a directory or a specific file.
	 *
	 * @since 2.7.0
	 *
	 * @param string $path           Path to directory or file.
	 * @param bool   $include_hidden Optional. Whether to include details of hidden ("." prefixed) files.
	 *                               Default true.
	 * @param bool   $recursive      Optional. Whether to recursively include file details in nested directories.
	 *                               Default false.
	 * @return array|false {
	 *     Array of arrays containing file information. False if unable to list directory contents.
	 *
	 *     @type array ...$0 {
	 *         Array of file information. Note that some elements may not be available on all filesystems.
	 *
	 *         @type string           $name        Name of the file or directory.
	 *         @type string           $perms       *nix representation of permissions.
	 *         @type string           $permsn      Octal representation of permissions.
	 *         @type false            $number      File number. Always false in this context.
	 *         @type string|false     $owner       Owner name or ID, or false if not available.
	 *         @type string|false     $group       File permissions group, or false if not available.
	 *         @type int|string|false $size        Size of file in bytes. May be a numeric string.
	 *                                             False if not available.
	 *         @type int|string|false $lastmodunix Last modified unix timestamp. May be a numeric string.
	 *                                             False if not available.
	 *         @type string|false     $lastmod     Last modified month (3 letters) and day (without leading 0), or
	 *                                             false if not available.
	 *         @type string|false     $time        Last modified time, or false if not available.
	 *         @type string           $type        Type of resource. 'f' for file, 'd' for directory, 'l' for link.
	 *         @type array|false      $files       If a directory and `$recursive` is true, contains another array of
	 *                                             files. False if unable to list directory contents.
	 *     }
	 * }
	 */
	public function dirlist( $path, $include_hidden = true, $recursive = false ) {
		if ( $this->is_file( $path ) ) {
			$limit_file = basename( $path );
			$path       = dirname( $path );
		} else {
			$limit_file = false;
		}

		if ( ! $this->is_dir( $path ) || ! $this->is_readable( $path ) ) {
			return false;
		}

		$ret = array();
		$dir = dir( $this->sftp_path( $path ) );

		if ( ! $dir ) {
			return false;
		}

		$path = trailingslashit( $path );

		while ( false !== ( $entry = $dir->read() ) ) {
			$struc         = array();
			$struc['name'] = $entry;

			if ( '.' === $struc['name'] || '..' === $struc['name'] ) {
				continue; // Do not care about these folders.
			}

			if ( ! $include_hidden && '.' === $struc['name'][0] ) {
				continue;
			}

			if ( $limit_file && $struc['name'] !== $limit_file ) {
				continue;
			}

			$struc['perms']       = $this->gethchmod( $path . $entry );
			$struc['permsn']      = $this->getnumchmodfromh( $struc['perms'] );
			$struc['number']      = false;
			$struc['owner']       = $this->owner( $path . $entry );
			$struc['group']       = $this->group( $path . $entry );
			$struc['size']        = $this->size( $path . $entry );
			$struc['lastmodunix'] = $this->mtime( $path . $entry );
			$struc['lastmod']     = gmdate( 'M j', $struc['lastmodunix'] );
			$struc['time']        = gmdate( 'h:i:s', $struc['lastmodunix'] );
			$struc['type']        = $this->is_dir( $path . $entry ) ? 'd' : 'f';

			if ( 'd' === $struc['type'] ) {
				if ( $recursive ) {
					$struc['files'] = $this->dirlist( $path . $struc['name'], $include_hidden, $recursive );
				} else {
					$struc['files'] = array();
				}
			}

			$ret[ $struc['name'] ] = $struc;
		}

		$dir->close();
		unset( $dir );

		return $ret;
	}
}
edit-tag-messages.php000060400000002706151327343360010570 0ustar00<?php
/**
 * Edit Tags Administration: Messages
 *
 * @package WordPress
 * @subpackage Administration
 * @since 4.4.0
 */

$messages = array();
// 0 = unused. Messages start at index 1.
$messages['_item'] = array(
	0 => '',
	1 => __( 'Item added.' ),
	2 => __( 'Item deleted.' ),
	3 => __( 'Item updated.' ),
	4 => __( 'Item not added.' ),
	5 => __( 'Item not updated.' ),
	6 => __( 'Items deleted.' ),
);

$messages['category'] = array(
	0 => '',
	1 => __( 'Category added.' ),
	2 => __( 'Category deleted.' ),
	3 => __( 'Category updated.' ),
	4 => __( 'Category not added.' ),
	5 => __( 'Category not updated.' ),
	6 => __( 'Categories deleted.' ),
);

$messages['post_tag'] = array(
	0 => '',
	1 => __( 'Tag added.' ),
	2 => __( 'Tag deleted.' ),
	3 => __( 'Tag updated.' ),
	4 => __( 'Tag not added.' ),
	5 => __( 'Tag not updated.' ),
	6 => __( 'Tags deleted.' ),
);

/**
 * Filters the messages displayed when a tag is updated.
 *
 * @since 3.7.0
 *
 * @param array[] $messages Array of arrays of messages to be displayed, keyed by taxonomy name.
 */
$messages = apply_filters( 'term_updated_messages', $messages );

$message = false;
if ( isset( $_REQUEST['message'] ) && (int) $_REQUEST['message'] ) {
	$msg = (int) $_REQUEST['message'];
	if ( isset( $messages[ $taxonomy ][ $msg ] ) ) {
		$message = $messages[ $taxonomy ][ $msg ];
	} elseif ( ! isset( $messages[ $taxonomy ] ) && isset( $messages['_item'][ $msg ] ) ) {
		$message = $messages['_item'][ $msg ];
	}
}
class-wp-site-health.php000064400000356772151327343360011246 0ustar00<?php
/**
 * Class for looking up a site's health based on a user's WordPress environment.
 *
 * @package WordPress
 * @subpackage Site_Health
 * @since 5.2.0
 */

#[AllowDynamicProperties]
class WP_Site_Health {
	private static $instance = null;

	private $is_acceptable_mysql_version;
	private $is_recommended_mysql_version;

	public $is_mariadb                   = false;
	private $mysql_server_version        = '';
	private $mysql_required_version      = '5.5';
	private $mysql_recommended_version   = '8.0';
	private $mariadb_recommended_version = '10.5';

	public $php_memory_limit;

	public $schedules;
	public $crons;
	public $last_missed_cron     = null;
	public $last_late_cron       = null;
	private $timeout_missed_cron = null;
	private $timeout_late_cron   = null;

	/**
	 * WP_Site_Health constructor.
	 *
	 * @since 5.2.0
	 */
	public function __construct() {
		$this->maybe_create_scheduled_event();

		// Save memory limit before it's affected by wp_raise_memory_limit( 'admin' ).
		$this->php_memory_limit = ini_get( 'memory_limit' );

		$this->timeout_late_cron   = 0;
		$this->timeout_missed_cron = - 5 * MINUTE_IN_SECONDS;

		if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
			$this->timeout_late_cron   = - 15 * MINUTE_IN_SECONDS;
			$this->timeout_missed_cron = - 1 * HOUR_IN_SECONDS;
		}

		add_filter( 'admin_body_class', array( $this, 'admin_body_class' ) );

		add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
		add_action( 'wp_site_health_scheduled_check', array( $this, 'wp_cron_scheduled_check' ) );

		add_action( 'site_health_tab_content', array( $this, 'show_site_health_tab' ) );
	}

	/**
	 * Outputs the content of a tab in the Site Health screen.
	 *
	 * @since 5.8.0
	 *
	 * @param string $tab Slug of the current tab being displayed.
	 */
	public function show_site_health_tab( $tab ) {
		if ( 'debug' === $tab ) {
			require_once ABSPATH . 'wp-admin/site-health-info.php';
		}
	}

	/**
	 * Returns an instance of the WP_Site_Health class, or create one if none exist yet.
	 *
	 * @since 5.4.0
	 *
	 * @return WP_Site_Health|null
	 */
	public static function get_instance() {
		if ( null === self::$instance ) {
			self::$instance = new WP_Site_Health();
		}

		return self::$instance;
	}

	/**
	 * Enqueues the site health scripts.
	 *
	 * @since 5.2.0
	 */
	public function enqueue_scripts() {
		$screen = get_current_screen();
		if ( 'site-health' !== $screen->id && 'dashboard' !== $screen->id ) {
			return;
		}

		$health_check_js_variables = array(
			'screen'      => $screen->id,
			'nonce'       => array(
				'site_status'        => wp_create_nonce( 'health-check-site-status' ),
				'site_status_result' => wp_create_nonce( 'health-check-site-status-result' ),
			),
			'site_status' => array(
				'direct' => array(),
				'async'  => array(),
				'issues' => array(
					'good'        => 0,
					'recommended' => 0,
					'critical'    => 0,
				),
			),
		);

		$issue_counts = get_transient( 'health-check-site-status-result' );

		if ( false !== $issue_counts ) {
			$issue_counts = json_decode( $issue_counts );

			$health_check_js_variables['site_status']['issues'] = $issue_counts;
		}

		if ( 'site-health' === $screen->id && ( ! isset( $_GET['tab'] ) || empty( $_GET['tab'] ) ) ) {
			$tests = WP_Site_Health::get_tests();

			// Don't run https test on development environments.
			if ( $this->is_development_environment() ) {
				unset( $tests['async']['https_status'] );
			}

			foreach ( $tests['direct'] as $test ) {
				if ( is_string( $test['test'] ) ) {
					$test_function = sprintf(
						'get_test_%s',
						$test['test']
					);

					if ( method_exists( $this, $test_function ) && is_callable( array( $this, $test_function ) ) ) {
						$health_check_js_variables['site_status']['direct'][] = $this->perform_test( array( $this, $test_function ) );
						continue;
					}
				}

				if ( is_callable( $test['test'] ) ) {
					$health_check_js_variables['site_status']['direct'][] = $this->perform_test( $test['test'] );
				}
			}

			foreach ( $tests['async'] as $test ) {
				if ( is_string( $test['test'] ) ) {
					$health_check_js_variables['site_status']['async'][] = array(
						'test'      => $test['test'],
						'has_rest'  => ( isset( $test['has_rest'] ) ? $test['has_rest'] : false ),
						'completed' => false,
						'headers'   => isset( $test['headers'] ) ? $test['headers'] : array(),
					);
				}
			}
		}

		wp_localize_script( 'site-health', 'SiteHealth', $health_check_js_variables );
	}

	/**
	 * Runs a Site Health test directly.
	 *
	 * @since 5.4.0
	 *
	 * @param callable $callback
	 * @return mixed|void
	 */
	private function perform_test( $callback ) {
		/**
		 * Filters the output of a finished Site Health test.
		 *
		 * @since 5.3.0
		 *
		 * @param array $test_result {
		 *     An associative array of test result data.
		 *
		 *     @type string $label       A label describing the test, and is used as a header in the output.
		 *     @type string $status      The status of the test, which can be a value of `good`, `recommended` or `critical`.
		 *     @type array  $badge {
		 *         Tests are put into categories which have an associated badge shown, these can be modified and assigned here.
		 *
		 *         @type string $label The test label, for example `Performance`.
		 *         @type string $color Default `blue`. A string representing a color to use for the label.
		 *     }
		 *     @type string $description A more descriptive explanation of what the test looks for, and why it is important for the end user.
		 *     @type string $actions     An action to direct the user to where they can resolve the issue, if one exists.
		 *     @type string $test        The name of the test being ran, used as a reference point.
		 * }
		 */
		return apply_filters( 'site_status_test_result', call_user_func( $callback ) );
	}

	/**
	 * Runs the SQL version checks.
	 *
	 * These values are used in later tests, but the part of preparing them is more easily managed
	 * early in the class for ease of access and discovery.
	 *
	 * @since 5.2.0
	 *
	 * @global wpdb $wpdb WordPress database abstraction object.
	 */
	private function prepare_sql_data() {
		global $wpdb;

		$mysql_server_type = $wpdb->db_server_info();

		$this->mysql_server_version = $wpdb->get_var( 'SELECT VERSION()' );

		if ( stristr( $mysql_server_type, 'mariadb' ) ) {
			$this->is_mariadb                = true;
			$this->mysql_recommended_version = $this->mariadb_recommended_version;
		}

		$this->is_acceptable_mysql_version  = version_compare( $this->mysql_required_version, $this->mysql_server_version, '<=' );
		$this->is_recommended_mysql_version = version_compare( $this->mysql_recommended_version, $this->mysql_server_version, '<=' );
	}

	/**
	 * Tests whether `wp_version_check` is blocked.
	 *
	 * It's possible to block updates with the `wp_version_check` filter, but this can't be checked
	 * during an Ajax call, as the filter is never introduced then.
	 *
	 * This filter overrides a standard page request if it's made by an admin through the Ajax call
	 * with the right query argument to check for this.
	 *
	 * @since 5.2.0
	 */
	public function check_wp_version_check_exists() {
		if ( ! is_admin() || ! is_user_logged_in() || ! current_user_can( 'update_core' ) || ! isset( $_GET['health-check-test-wp_version_check'] ) ) {
			return;
		}

		echo ( has_filter( 'wp_version_check', 'wp_version_check' ) ? 'yes' : 'no' );

		die();
	}

	/**
	 * Tests for WordPress version and outputs it.
	 *
	 * Gives various results depending on what kind of updates are available, if any, to encourage
	 * the user to install security updates as a priority.
	 *
	 * @since 5.2.0
	 *
	 * @return array The test result.
	 */
	public function get_test_wordpress_version() {
		$result = array(
			'label'       => '',
			'status'      => '',
			'badge'       => array(
				'label' => __( 'Performance' ),
				'color' => 'blue',
			),
			'description' => '',
			'actions'     => '',
			'test'        => 'wordpress_version',
		);

		$core_current_version = wp_get_wp_version();
		$core_updates         = get_core_updates();

		if ( ! is_array( $core_updates ) ) {
			$result['status'] = 'recommended';

			$result['label'] = sprintf(
				/* translators: %s: Your current version of WordPress. */
				__( 'WordPress version %s' ),
				$core_current_version
			);

			$result['description'] = sprintf(
				'<p>%s</p>',
				__( 'Unable to check if any new versions of WordPress are available.' )
			);

			$result['actions'] = sprintf(
				'<a href="%s">%s</a>',
				esc_url( admin_url( 'update-core.php?force-check=1' ) ),
				__( 'Check for updates manually' )
			);
		} else {
			foreach ( $core_updates as $core => $update ) {
				if ( 'upgrade' === $update->response ) {
					$current_version = explode( '.', $core_current_version );
					$new_version     = explode( '.', $update->version );

					$current_major = $current_version[0] . '.' . $current_version[1];
					$new_major     = $new_version[0] . '.' . $new_version[1];

					$result['label'] = sprintf(
						/* translators: %s: The latest version of WordPress available. */
						__( 'WordPress update available (%s)' ),
						$update->version
					);

					$result['actions'] = sprintf(
						'<a href="%s">%s</a>',
						esc_url( admin_url( 'update-core.php' ) ),
						__( 'Install the latest version of WordPress' )
					);

					if ( $current_major !== $new_major ) {
						// This is a major version mismatch.
						$result['status']      = 'recommended';
						$result['description'] = sprintf(
							'<p>%s</p>',
							__( 'A new version of WordPress is available.' )
						);
					} else {
						// This is a minor version, sometimes considered more critical.
						$result['status']         = 'critical';
						$result['badge']['label'] = __( 'Security' );
						$result['description']    = sprintf(
							'<p>%s</p>',
							__( 'A new minor update is available for your site. Because minor updates often address security, it&#8217;s important to install them.' )
						);
					}
				} else {
					$result['status'] = 'good';
					$result['label']  = sprintf(
						/* translators: %s: The current version of WordPress installed on this site. */
						__( 'Your version of WordPress (%s) is up to date' ),
						$core_current_version
					);

					$result['description'] = sprintf(
						'<p>%s</p>',
						__( 'You are currently running the latest version of WordPress available, keep it up!' )
					);
				}
			}
		}

		return $result;
	}

	/**
	 * Tests if plugins are outdated, or unnecessary.
	 *
	 * The test checks if your plugins are up to date, and encourages you to remove any
	 * that are not in use.
	 *
	 * @since 5.2.0
	 *
	 * @return array The test result.
	 */
	public function get_test_plugin_version() {
		$result = array(
			'label'       => __( 'Your plugins are all up to date' ),
			'status'      => 'good',
			'badge'       => array(
				'label' => __( 'Security' ),
				'color' => 'blue',
			),
			'description' => sprintf(
				'<p>%s</p>',
				__( 'Plugins extend your site&#8217;s functionality with things like contact forms, ecommerce and much more. That means they have deep access to your site, so it&#8217;s vital to keep them up to date.' )
			),
			'actions'     => sprintf(
				'<p><a href="%s">%s</a></p>',
				esc_url( admin_url( 'plugins.php' ) ),
				__( 'Manage your plugins' )
			),
			'test'        => 'plugin_version',
		);

		$plugins        = get_plugins();
		$plugin_updates = get_plugin_updates();

		$plugins_active      = 0;
		$plugins_total       = 0;
		$plugins_need_update = 0;

		// Loop over the available plugins and check their versions and active state.
		foreach ( $plugins as $plugin_path => $plugin ) {
			++$plugins_total;

			if ( is_plugin_active( $plugin_path ) ) {
				++$plugins_active;
			}

			if ( array_key_exists( $plugin_path, $plugin_updates ) ) {
				++$plugins_need_update;
			}
		}

		// Add a notice if there are outdated plugins.
		if ( $plugins_need_update > 0 ) {
			$result['status'] = 'critical';

			$result['label'] = __( 'You have plugins waiting to be updated' );

			$result['description'] .= sprintf(
				'<p>%s</p>',
				sprintf(
					/* translators: %d: The number of outdated plugins. */
					_n(
						'Your site has %d plugin waiting to be updated.',
						'Your site has %d plugins waiting to be updated.',
						$plugins_need_update
					),
					$plugins_need_update
				)
			);

			$result['actions'] .= sprintf(
				'<p><a href="%s">%s</a></p>',
				esc_url( network_admin_url( 'plugins.php?plugin_status=upgrade' ) ),
				__( 'Update your plugins' )
			);
		} else {
			if ( 1 === $plugins_active ) {
				$result['description'] .= sprintf(
					'<p>%s</p>',
					__( 'Your site has 1 active plugin, and it is up to date.' )
				);
			} elseif ( $plugins_active > 0 ) {
				$result['description'] .= sprintf(
					'<p>%s</p>',
					sprintf(
						/* translators: %d: The number of active plugins. */
						_n(
							'Your site has %d active plugin, and it is up to date.',
							'Your site has %d active plugins, and they are all up to date.',
							$plugins_active
						),
						$plugins_active
					)
				);
			} else {
				$result['description'] .= sprintf(
					'<p>%s</p>',
					__( 'Your site does not have any active plugins.' )
				);
			}
		}

		// Check if there are inactive plugins.
		if ( $plugins_total > $plugins_active && ! is_multisite() ) {
			$unused_plugins = $plugins_total - $plugins_active;

			$result['status'] = 'recommended';

			$result['label'] = __( 'You should remove inactive plugins' );

			$result['description'] .= sprintf(
				'<p>%s %s</p>',
				sprintf(
					/* translators: %d: The number of inactive plugins. */
					_n(
						'Your site has %d inactive plugin.',
						'Your site has %d inactive plugins.',
						$unused_plugins
					),
					$unused_plugins
				),
				__( 'Inactive plugins are tempting targets for attackers. If you are not going to use a plugin, you should consider removing it.' )
			);

			$result['actions'] .= sprintf(
				'<p><a href="%s">%s</a></p>',
				esc_url( admin_url( 'plugins.php?plugin_status=inactive' ) ),
				__( 'Manage inactive plugins' )
			);
		}

		return $result;
	}

	/**
	 * Tests if themes are outdated, or unnecessary.
	 *
	 * Checks if your site has a default theme (to fall back on if there is a need),
	 * if your themes are up to date and, finally, encourages you to remove any themes
	 * that are not needed.
	 *
	 * @since 5.2.0
	 *
	 * @return array The test results.
	 */
	public function get_test_theme_version() {
		$result = array(
			'label'       => __( 'Your themes are all up to date' ),
			'status'      => 'good',
			'badge'       => array(
				'label' => __( 'Security' ),
				'color' => 'blue',
			),
			'description' => sprintf(
				'<p>%s</p>',
				__( 'Themes add your site&#8217;s look and feel. It&#8217;s important to keep them up to date, to stay consistent with your brand and keep your site secure.' )
			),
			'actions'     => sprintf(
				'<p><a href="%s">%s</a></p>',
				esc_url( admin_url( 'themes.php' ) ),
				__( 'Manage your themes' )
			),
			'test'        => 'theme_version',
		);

		$theme_updates = get_theme_updates();

		$themes_total        = 0;
		$themes_need_updates = 0;
		$themes_inactive     = 0;

		// This value is changed during processing to determine how many themes are considered a reasonable amount.
		$allowed_theme_count = 1;

		$has_default_theme   = false;
		$has_unused_themes   = false;
		$show_unused_themes  = true;
		$using_default_theme = false;

		// Populate a list of all themes available in the install.
		$all_themes   = wp_get_themes();
		$active_theme = wp_get_theme();

		// If WP_DEFAULT_THEME doesn't exist, fall back to the latest core default theme.
		$default_theme = wp_get_theme( WP_DEFAULT_THEME );
		if ( ! $default_theme->exists() ) {
			$default_theme = WP_Theme::get_core_default_theme();
		}

		if ( $default_theme ) {
			$has_default_theme = true;

			if (
				$active_theme->get_stylesheet() === $default_theme->get_stylesheet()
			||
				is_child_theme() && $active_theme->get_template() === $default_theme->get_template()
			) {
				$using_default_theme = true;
			}
		}

		foreach ( $all_themes as $theme_slug => $theme ) {
			++$themes_total;

			if ( array_key_exists( $theme_slug, $theme_updates ) ) {
				++$themes_need_updates;
			}
		}

		// If this is a child theme, increase the allowed theme count by one, to account for the parent.
		if ( is_child_theme() ) {
			++$allowed_theme_count;
		}

		// If there's a default theme installed and not in use, we count that as allowed as well.
		if ( $has_default_theme && ! $using_default_theme ) {
			++$allowed_theme_count;
		}

		if ( $themes_total > $allowed_theme_count ) {
			$has_unused_themes = true;
			$themes_inactive   = ( $themes_total - $allowed_theme_count );
		}

		// Check if any themes need to be updated.
		if ( $themes_need_updates > 0 ) {
			$result['status'] = 'critical';

			$result['label'] = __( 'You have themes waiting to be updated' );

			$result['description'] .= sprintf(
				'<p>%s</p>',
				sprintf(
					/* translators: %d: The number of outdated themes. */
					_n(
						'Your site has %d theme waiting to be updated.',
						'Your site has %d themes waiting to be updated.',
						$themes_need_updates
					),
					$themes_need_updates
				)
			);
		} else {
			// Give positive feedback about the site being good about keeping things up to date.
			if ( 1 === $themes_total ) {
				$result['description'] .= sprintf(
					'<p>%s</p>',
					__( 'Your site has 1 installed theme, and it is up to date.' )
				);
			} elseif ( $themes_total > 0 ) {
				$result['description'] .= sprintf(
					'<p>%s</p>',
					sprintf(
						/* translators: %d: The number of themes. */
						_n(
							'Your site has %d installed theme, and it is up to date.',
							'Your site has %d installed themes, and they are all up to date.',
							$themes_total
						),
						$themes_total
					)
				);
			} else {
				$result['description'] .= sprintf(
					'<p>%s</p>',
					__( 'Your site does not have any installed themes.' )
				);
			}
		}

		if ( $has_unused_themes && $show_unused_themes && ! is_multisite() ) {

			// This is a child theme, so we want to be a bit more explicit in our messages.
			if ( $active_theme->parent() ) {
				// Recommend removing inactive themes, except a default theme, your current one, and the parent theme.
				$result['status'] = 'recommended';

				$result['label'] = __( 'You should remove inactive themes' );

				if ( $using_default_theme ) {
					$result['description'] .= sprintf(
						'<p>%s %s</p>',
						sprintf(
							/* translators: %d: The number of inactive themes. */
							_n(
								'Your site has %d inactive theme.',
								'Your site has %d inactive themes.',
								$themes_inactive
							),
							$themes_inactive
						),
						sprintf(
							/* translators: 1: The currently active theme. 2: The active theme's parent theme. */
							__( 'To enhance your site&#8217;s security, you should consider removing any themes you are not using. You should keep your active theme, %1$s, and %2$s, its parent theme.' ),
							$active_theme->name,
							$active_theme->parent()->name
						)
					);
				} else {
					$result['description'] .= sprintf(
						'<p>%s %s</p>',
						sprintf(
							/* translators: %d: The number of inactive themes. */
							_n(
								'Your site has %d inactive theme.',
								'Your site has %d inactive themes.',
								$themes_inactive
							),
							$themes_inactive
						),
						sprintf(
							/* translators: 1: The default theme for WordPress. 2: The currently active theme. 3: The active theme's parent theme. */
							__( 'To enhance your site&#8217;s security, you should consider removing any themes you are not using. You should keep %1$s, the default WordPress theme, %2$s, your active theme, and %3$s, its parent theme.' ),
							$default_theme ? $default_theme->name : WP_DEFAULT_THEME,
							$active_theme->name,
							$active_theme->parent()->name
						)
					);
				}
			} else {
				// Recommend removing all inactive themes.
				$result['status'] = 'recommended';

				$result['label'] = __( 'You should remove inactive themes' );

				if ( $using_default_theme ) {
					$result['description'] .= sprintf(
						'<p>%s %s</p>',
						sprintf(
							/* translators: 1: The amount of inactive themes. 2: The currently active theme. */
							_n(
								'Your site has %1$d inactive theme, other than %2$s, your active theme.',
								'Your site has %1$d inactive themes, other than %2$s, your active theme.',
								$themes_inactive
							),
							$themes_inactive,
							$active_theme->name
						),
						__( 'You should consider removing any unused themes to enhance your site&#8217;s security.' )
					);
				} else {
					$result['description'] .= sprintf(
						'<p>%s %s</p>',
						sprintf(
							/* translators: 1: The amount of inactive themes. 2: The default theme for WordPress. 3: The currently active theme. */
							_n(
								'Your site has %1$d inactive theme, other than %2$s, the default WordPress theme, and %3$s, your active theme.',
								'Your site has %1$d inactive themes, other than %2$s, the default WordPress theme, and %3$s, your active theme.',
								$themes_inactive
							),
							$themes_inactive,
							$default_theme ? $default_theme->name : WP_DEFAULT_THEME,
							$active_theme->name
						),
						__( 'You should consider removing any unused themes to enhance your site&#8217;s security.' )
					);
				}
			}
		}

		// If no default Twenty* theme exists.
		if ( ! $has_default_theme ) {
			$result['status'] = 'recommended';

			$result['label'] = __( 'Have a default theme available' );

			$result['description'] .= sprintf(
				'<p>%s</p>',
				__( 'Your site does not have any default theme. Default themes are used by WordPress automatically if anything is wrong with your chosen theme.' )
			);
		}

		return $result;
	}

	/**
	 * Tests if the supplied PHP version is supported.
	 *
	 * @since 5.2.0
	 *
	 * @return array The test results.
	 */
	public function get_test_php_version() {
		$response = wp_check_php_version();

		$result = array(
			'label'       => sprintf(
				/* translators: %s: The recommended PHP version. */
				__( 'Your site is running a recommended version of PHP (%s)' ),
				PHP_VERSION
			),
			'status'      => 'good',
			'badge'       => array(
				'label' => __( 'Performance' ),
				'color' => 'blue',
			),
			'description' => sprintf(
				'<p>%s</p>',
				sprintf(
					/* translators: %s: The minimum recommended PHP version. */
					__( 'PHP is one of the programming languages used to build WordPress. Newer versions of PHP receive regular security updates and may increase your site&#8217;s performance. The minimum recommended version of PHP is %s.' ),
					$response ? $response['recommended_version'] : ''
				)
			),
			'actions'     => sprintf(
				'<p><a href="%s" target="_blank">%s<span class="screen-reader-text"> %s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
				esc_url( wp_get_update_php_url() ),
				__( 'Learn more about updating PHP' ),
				/* translators: Hidden accessibility text. */
				__( '(opens in a new tab)' )
			),
			'test'        => 'php_version',
		);

		// PHP is up to date.
		if ( ! $response || version_compare( PHP_VERSION, $response['recommended_version'], '>=' ) ) {
			return $result;
		}

		// The PHP version is older than the recommended version, but still receiving active support.
		if ( $response['is_supported'] ) {
			$result['label'] = sprintf(
				/* translators: %s: The server PHP version. */
				__( 'Your site is running on an older version of PHP (%s)' ),
				PHP_VERSION
			);
			$result['status'] = 'recommended';

			return $result;
		}

		/*
		 * The PHP version is still receiving security fixes, but is lower than
		 * the expected minimum version that will be required by WordPress in the near future.
		 */
		if ( $response['is_secure'] && $response['is_lower_than_future_minimum'] ) {
			// The `is_secure` array key name doesn't actually imply this is a secure version of PHP. It only means it receives security updates.

			$result['label'] = sprintf(
				/* translators: %s: The server PHP version. */
				__( 'Your site is running on an outdated version of PHP (%s), which soon will not be supported by WordPress.' ),
				PHP_VERSION
			);

			$result['status']         = 'critical';
			$result['badge']['label'] = __( 'Requirements' );

			return $result;
		}

		// The PHP version is only receiving security fixes.
		if ( $response['is_secure'] ) {
			$result['label'] = sprintf(
				/* translators: %s: The server PHP version. */
				__( 'Your site is running on an older version of PHP (%s), which should be updated' ),
				PHP_VERSION
			);
			$result['status'] = 'recommended';

			return $result;
		}

		// No more security updates for the PHP version, and lower than the expected minimum version required by WordPress.
		if ( $response['is_lower_than_future_minimum'] ) {
			$message = sprintf(
				/* translators: %s: The server PHP version. */
				__( 'Your site is running on an outdated version of PHP (%s), which does not receive security updates and soon will not be supported by WordPress.' ),
				PHP_VERSION
			);
		} else {
			// No more security updates for the PHP version, must be updated.
			$message = sprintf(
				/* translators: %s: The server PHP version. */
				__( 'Your site is running on an outdated version of PHP (%s), which does not receive security updates. It should be updated.' ),
				PHP_VERSION
			);
		}

		$result['label']  = $message;
		$result['status'] = 'critical';

		$result['badge']['label'] = __( 'Security' );

		return $result;
	}

	/**
	 * Checks if the passed extension or function are available.
	 *
	 * Make the check for available PHP modules into a simple boolean operator for a cleaner test runner.
	 *
	 * @since 5.2.0
	 * @since 5.3.0 The `$constant_name` and `$class_name` parameters were added.
	 *
	 * @param string $extension_name Optional. The extension name to test. Default null.
	 * @param string $function_name  Optional. The function name to test. Default null.
	 * @param string $constant_name  Optional. The constant name to test for. Default null.
	 * @param string $class_name     Optional. The class name to test for. Default null.
	 * @return bool Whether or not the extension and function are available.
	 */
	private function test_php_extension_availability( $extension_name = null, $function_name = null, $constant_name = null, $class_name = null ) {
		// If no extension or function is passed, claim to fail testing, as we have nothing to test against.
		if ( ! $extension_name && ! $function_name && ! $constant_name && ! $class_name ) {
			return false;
		}

		if ( $extension_name && ! extension_loaded( $extension_name ) ) {
			return false;
		}

		if ( $function_name && ! function_exists( $function_name ) ) {
			return false;
		}

		if ( $constant_name && ! defined( $constant_name ) ) {
			return false;
		}

		if ( $class_name && ! class_exists( $class_name ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Tests if required PHP modules are installed on the host.
	 *
	 * This test builds on the recommendations made by the WordPress Hosting Team
	 * as seen at https://make.wordpress.org/hosting/handbook/handbook/server-environment/#php-extensions
	 *
	 * @since 5.2.0
	 *
	 * @return array
	 */
	public function get_test_php_extensions() {
		$result = array(
			'label'       => __( 'Required and recommended modules are installed' ),
			'status'      => 'good',
			'badge'       => array(
				'label' => __( 'Performance' ),
				'color' => 'blue',
			),
			'description' => sprintf(
				'<p>%s</p><p>%s</p>',
				__( 'PHP modules perform most of the tasks on the server that make your site run. Any changes to these must be made by your server administrator.' ),
				sprintf(
					/* translators: 1: Link to the hosting group page about recommended PHP modules. 2: Additional link attributes. 3: Accessibility text. */
					__( 'The WordPress Hosting Team maintains a list of those modules, both recommended and required, in <a href="%1$s" %2$s>the team handbook%3$s</a>.' ),
					/* translators: Localized team handbook, if one exists. */
					esc_url( __( 'https://make.wordpress.org/hosting/handbook/handbook/server-environment/#php-extensions' ) ),
					'target="_blank"',
					sprintf(
						'<span class="screen-reader-text"> %s</span><span aria-hidden="true" class="dashicons dashicons-external"></span>',
						/* translators: Hidden accessibility text. */
						__( '(opens in a new tab)' )
					)
				)
			),
			'actions'     => '',
			'test'        => 'php_extensions',
		);

		$modules = array(
			'curl'      => array(
				'function' => 'curl_version',
				'required' => false,
			),
			'dom'       => array(
				'class'    => 'DOMNode',
				'required' => false,
			),
			'exif'      => array(
				'function' => 'exif_read_data',
				'required' => false,
			),
			'fileinfo'  => array(
				'function' => 'finfo_file',
				'required' => false,
			),
			'hash'      => array(
				'function' => 'hash',
				'required' => true,
			),
			'imagick'   => array(
				'extension' => 'imagick',
				'required'  => false,
			),
			'json'      => array(
				'function' => 'json_last_error',
				'required' => true,
			),
			'mbstring'  => array(
				'function' => 'mb_check_encoding',
				'required' => false,
			),
			'mysqli'    => array(
				'function' => 'mysqli_connect',
				'required' => false,
			),
			'libsodium' => array(
				'constant'            => 'SODIUM_LIBRARY_VERSION',
				'required'            => false,
				'php_bundled_version' => '7.2.0',
			),
			'openssl'   => array(
				'function' => 'openssl_encrypt',
				'required' => false,
			),
			'pcre'      => array(
				'function' => 'preg_match',
				'required' => false,
			),
			'mod_xml'   => array(
				'extension' => 'libxml',
				'required'  => false,
			),
			'zip'       => array(
				'class'    => 'ZipArchive',
				'required' => false,
			),
			'filter'    => array(
				'function' => 'filter_list',
				'required' => false,
			),
			'gd'        => array(
				'extension'    => 'gd',
				'required'     => false,
				'fallback_for' => 'imagick',
			),
			'iconv'     => array(
				'function' => 'iconv',
				'required' => false,
			),
			'intl'      => array(
				'extension' => 'intl',
				'required'  => false,
			),
			'mcrypt'    => array(
				'extension'    => 'mcrypt',
				'required'     => false,
				'fallback_for' => 'libsodium',
			),
			'simplexml' => array(
				'extension'    => 'simplexml',
				'required'     => false,
				'fallback_for' => 'mod_xml',
			),
			'xmlreader' => array(
				'extension'    => 'xmlreader',
				'required'     => false,
				'fallback_for' => 'mod_xml',
			),
			'zlib'      => array(
				'extension'    => 'zlib',
				'required'     => false,
				'fallback_for' => 'zip',
			),
		);

		/**
		 * Filters the array representing all the modules we wish to test for.
		 *
		 * @since 5.2.0
		 * @since 5.3.0 The `$constant` and `$class` parameters were added.
		 *
		 * @param array $modules {
		 *     An associative array of modules to test for.
		 *
		 *     @type array ...$0 {
		 *         An associative array of module properties used during testing.
		 *         One of either `$function` or `$extension` must be provided, or they will fail by default.
		 *
		 *         @type string $function     Optional. A function name to test for the existence of.
		 *         @type string $extension    Optional. An extension to check if is loaded in PHP.
		 *         @type string $constant     Optional. A constant name to check for to verify an extension exists.
		 *         @type string $class        Optional. A class name to check for to verify an extension exists.
		 *         @type bool   $required     Is this a required feature or not.
		 *         @type string $fallback_for Optional. The module this module replaces as a fallback.
		 *     }
		 * }
		 */
		$modules = apply_filters( 'site_status_test_php_modules', $modules );

		$failures = array();

		foreach ( $modules as $library => $module ) {
			$extension_name = ( isset( $module['extension'] ) ? $module['extension'] : null );
			$function_name  = ( isset( $module['function'] ) ? $module['function'] : null );
			$constant_name  = ( isset( $module['constant'] ) ? $module['constant'] : null );
			$class_name     = ( isset( $module['class'] ) ? $module['class'] : null );

			// If this module is a fallback for another function, check if that other function passed.
			if ( isset( $module['fallback_for'] ) ) {
				/*
				 * If that other function has a failure, mark this module as required for usual operations.
				 * If that other function hasn't failed, skip this test as it's only a fallback.
				 */
				if ( isset( $failures[ $module['fallback_for'] ] ) ) {
					$module['required'] = true;
				} else {
					continue;
				}
			}

			if ( ! $this->test_php_extension_availability( $extension_name, $function_name, $constant_name, $class_name )
				&& ( ! isset( $module['php_bundled_version'] )
					|| version_compare( PHP_VERSION, $module['php_bundled_version'], '<' ) )
			) {
				if ( $module['required'] ) {
					$result['status'] = 'critical';

					$class = 'error';
					/* translators: Hidden accessibility text. */
					$screen_reader = __( 'Error' );
					$message       = sprintf(
						/* translators: %s: The module name. */
						__( 'The required module, %s, is not installed, or has been disabled.' ),
						$library
					);
				} else {
					$class = 'warning';
					/* translators: Hidden accessibility text. */
					$screen_reader = __( 'Warning' );
					$message       = sprintf(
						/* translators: %s: The module name. */
						__( 'The optional module, %s, is not installed, or has been disabled.' ),
						$library
					);
				}

				if ( ! $module['required'] && 'good' === $result['status'] ) {
					$result['status'] = 'recommended';
				}

				$failures[ $library ] = "<span class='dashicons $class'><span class='screen-reader-text'>$screen_reader</span></span> $message";
			}
		}

		if ( ! empty( $failures ) ) {
			$output = '<ul>';

			foreach ( $failures as $failure ) {
				$output .= sprintf(
					'<li>%s</li>',
					$failure
				);
			}

			$output .= '</ul>';
		}

		if ( 'good' !== $result['status'] ) {
			if ( 'recommended' === $result['status'] ) {
				$result['label'] = __( 'One or more recommended modules are missing' );
			}
			if ( 'critical' === $result['status'] ) {
				$result['label'] = __( 'One or more required modules are missing' );
			}

			$result['description'] .= $output;
		}

		return $result;
	}

	/**
	 * Tests if the PHP default timezone is set to UTC.
	 *
	 * @since 5.3.1
	 *
	 * @return array The test results.
	 */
	public function get_test_php_default_timezone() {
		$result = array(
			'label'       => __( 'PHP default timezone is valid' ),
			'status'      => 'good',
			'badge'       => array(
				'label' => __( 'Performance' ),
				'color' => 'blue',
			),
			'description' => sprintf(
				'<p>%s</p>',
				__( 'PHP default timezone was configured by WordPress on loading. This is necessary for correct calculations of dates and times.' )
			),
			'actions'     => '',
			'test'        => 'php_default_timezone',
		);

		if ( 'UTC' !== date_default_timezone_get() ) {
			$result['status'] = 'critical';

			$result['label'] = __( 'PHP default timezone is invalid' );

			$result['description'] = sprintf(
				'<p>%s</p>',
				sprintf(
					/* translators: %s: date_default_timezone_set() */
					__( 'PHP default timezone was changed after WordPress loading by a %s function call. This interferes with correct calculations of dates and times.' ),
					'<code>date_default_timezone_set()</code>'
				)
			);
		}

		return $result;
	}

	/**
	 * Tests if there's an active PHP session that can affect loopback requests.
	 *
	 * @since 5.5.0
	 *
	 * @return array The test results.
	 */
	public function get_test_php_sessions() {
		$result = array(
			'label'       => __( 'No PHP sessions detected' ),
			'status'      => 'good',
			'badge'       => array(
				'label' => __( 'Performance' ),
				'color' => 'blue',
			),
			'description' => sprintf(
				'<p>%s</p>',
				sprintf(
					/* translators: 1: session_start(), 2: session_write_close() */
					__( 'PHP sessions created by a %1$s function call may interfere with REST API and loopback requests. An active session should be closed by %2$s before making any HTTP requests.' ),
					'<code>session_start()</code>',
					'<code>session_write_close()</code>'
				)
			),
			'test'        => 'php_sessions',
		);

		if ( function_exists( 'session_status' ) && PHP_SESSION_ACTIVE === session_status() ) {
			$result['status'] = 'critical';

			$result['label'] = __( 'An active PHP session was detected' );

			$result['description'] = sprintf(
				'<p>%s</p>',
				sprintf(
					/* translators: 1: session_start(), 2: session_write_close() */
					__( 'A PHP session was created by a %1$s function call. This interferes with REST API and loopback requests. The session should be closed by %2$s before making any HTTP requests.' ),
					'<code>session_start()</code>',
					'<code>session_write_close()</code>'
				)
			);
		}

		return $result;
	}

	/**
	 * Tests if the SQL server is up to date.
	 *
	 * @since 5.2.0
	 *
	 * @return array The test results.
	 */
	public function get_test_sql_server() {
		if ( ! $this->mysql_server_version ) {
			$this->prepare_sql_data();
		}

		$result = array(
			'label'       => __( 'SQL server is up to date' ),
			'status'      => 'good',
			'badge'       => array(
				'label' => __( 'Performance' ),
				'color' => 'blue',
			),
			'description' => sprintf(
				'<p>%s</p>',
				__( 'The SQL server is a required piece of software for the database WordPress uses to store all your site&#8217;s content and settings.' )
			),
			'actions'     => sprintf(
				'<p><a href="%s" target="_blank">%s<span class="screen-reader-text"> %s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
				/* translators: Localized version of WordPress requirements if one exists. */
				esc_url( __( 'https://wordpress.org/about/requirements/' ) ),
				__( 'Learn more about what WordPress requires to run.' ),
				/* translators: Hidden accessibility text. */
				__( '(opens in a new tab)' )
			),
			'test'        => 'sql_server',
		);

		$db_dropin = file_exists( WP_CONTENT_DIR . '/db.php' );

		if ( ! $this->is_recommended_mysql_version ) {
			$result['status'] = 'recommended';

			$result['label'] = __( 'Outdated SQL server' );

			$result['description'] .= sprintf(
				'<p>%s</p>',
				sprintf(
					/* translators: 1: The database engine in use (MySQL or MariaDB). 2: Database server recommended version number. */
					__( 'For optimal performance and security reasons, you should consider running %1$s version %2$s or higher. Contact your web hosting company to correct this.' ),
					( $this->is_mariadb ? 'MariaDB' : 'MySQL' ),
					$this->mysql_recommended_version
				)
			);
		}

		if ( ! $this->is_acceptable_mysql_version ) {
			$result['status'] = 'critical';

			$result['label']          = __( 'Severely outdated SQL server' );
			$result['badge']['label'] = __( 'Security' );

			$result['description'] .= sprintf(
				'<p>%s</p>',
				sprintf(
					/* translators: 1: The database engine in use (MySQL or MariaDB). 2: Database server minimum version number. */
					__( 'WordPress requires %1$s version %2$s or higher. Contact your web hosting company to correct this.' ),
					( $this->is_mariadb ? 'MariaDB' : 'MySQL' ),
					$this->mysql_required_version
				)
			);
		}

		if ( $db_dropin ) {
			$result['description'] .= sprintf(
				'<p>%s</p>',
				wp_kses(
					sprintf(
						/* translators: 1: The name of the drop-in. 2: The name of the database engine. */
						__( 'You are using a %1$s drop-in which might mean that a %2$s database is not being used.' ),
						'<code>wp-content/db.php</code>',
						( $this->is_mariadb ? 'MariaDB' : 'MySQL' )
					),
					array(
						'code' => true,
					)
				)
			);
		}

		return $result;
	}

	/**
	 * Tests if the site can communicate with WordPress.org.
	 *
	 * @since 5.2.0
	 *
	 * @return array The test results.
	 */
	public function get_test_dotorg_communication() {
		$result = array(
			'label'       => __( 'Can communicate with WordPress.org' ),
			'status'      => '',
			'badge'       => array(
				'label' => __( 'Security' ),
				'color' => 'blue',
			),
			'description' => sprintf(
				'<p>%s</p>',
				__( 'Communicating with the WordPress servers is used to check for new versions, and to both install and update WordPress core, themes or plugins.' )
			),
			'actions'     => '',
			'test'        => 'dotorg_communication',
		);

		$wp_dotorg = wp_remote_get(
			'https://api.wordpress.org',
			array(
				'timeout' => 10,
			)
		);
		if ( ! is_wp_error( $wp_dotorg ) ) {
			$result['status'] = 'good';
		} else {
			$result['status'] = 'critical';

			$result['label'] = __( 'Could not reach WordPress.org' );

			$result['description'] .= sprintf(
				'<p>%s</p>',
				sprintf(
					'<span class="error"><span class="screen-reader-text">%s</span></span> %s',
					/* translators: Hidden accessibility text. */
					__( 'Error' ),
					sprintf(
						/* translators: 1: The IP address WordPress.org resolves to. 2: The error returned by the lookup. */
						__( 'Your site is unable to reach WordPress.org at %1$s, and returned the error: %2$s' ),
						gethostbyname( 'api.wordpress.org' ),
						$wp_dotorg->get_error_message()
					)
				)
			);

			$result['actions'] = sprintf(
				'<p><a href="%s" target="_blank">%s<span class="screen-reader-text"> %s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
				/* translators: Localized Support reference. */
				esc_url( __( 'https://wordpress.org/support/forums/' ) ),
				__( 'Get help resolving this issue.' ),
				/* translators: Hidden accessibility text. */
				__( '(opens in a new tab)' )
			);
		}

		return $result;
	}

	/**
	 * Tests if debug information is enabled.
	 *
	 * When WP_DEBUG is enabled, errors and information may be disclosed to site visitors,
	 * or logged to a publicly accessible file.
	 *
	 * Debugging is also frequently left enabled after looking for errors on a site,
	 * as site owners do not understand the implications of this.
	 *
	 * @since 5.2.0
	 *
	 * @return array The test results.
	 */
	public function get_test_is_in_debug_mode() {
		$result = array(
			'label'       => __( 'Your site is not set to output debug information' ),
			'status'      => 'good',
			'badge'       => array(
				'label' => __( 'Security' ),
				'color' => 'blue',
			),
			'description' => sprintf(
				'<p>%s</p>',
				__( 'Debug mode is often enabled to gather more details about an error or site failure, but may contain sensitive information which should not be available on a publicly available website.' )
			),
			'actions'     => sprintf(
				'<p><a href="%s" target="_blank">%s<span class="screen-reader-text"> %s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
				/* translators: Documentation explaining debugging in WordPress. */
				esc_url( __( 'https://developer.wordpress.org/advanced-administration/debug/debug-wordpress/' ) ),
				__( 'Learn more about debugging in WordPress.' ),
				/* translators: Hidden accessibility text. */
				__( '(opens in a new tab)' )
			),
			'test'        => 'is_in_debug_mode',
		);

		if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
			if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
				$result['label'] = __( 'Your site is set to log errors to a potentially public file' );

				$result['status'] = str_starts_with( ini_get( 'error_log' ), ABSPATH ) ? 'critical' : 'recommended';

				$result['description'] .= sprintf(
					'<p>%s</p>',
					sprintf(
						/* translators: %s: WP_DEBUG_LOG */
						__( 'The value, %s, has been added to this website&#8217;s configuration file. This means any errors on the site will be written to a file which is potentially available to all users.' ),
						'<code>WP_DEBUG_LOG</code>'
					)
				);
			}

			if ( defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY ) {
				$result['label'] = __( 'Your site is set to display errors to site visitors' );

				$result['status'] = 'critical';

				// On development environments, set the status to recommended.
				if ( $this->is_development_environment() ) {
					$result['status'] = 'recommended';
				}

				$result['description'] .= sprintf(
					'<p>%s</p>',
					sprintf(
						/* translators: 1: WP_DEBUG_DISPLAY, 2: WP_DEBUG */
						__( 'The value, %1$s, has either been enabled by %2$s or added to your configuration file. This will make errors display on the front end of your site.' ),
						'<code>WP_DEBUG_DISPLAY</code>',
						'<code>WP_DEBUG</code>'
					)
				);
			}
		}

		return $result;
	}

	/**
	 * Tests if the site is serving content over HTTPS.
	 *
	 * Many sites have varying degrees of HTTPS support, the most common of which is sites that have it
	 * enabled, but only if you visit the right site address.
	 *
	 * @since 5.2.0
	 * @since 5.7.0 Updated to rely on {@see wp_is_using_https()} and {@see wp_is_https_supported()}.
	 *
	 * @return array The test results.
	 */
	public function get_test_https_status() {
		/*
		 * Check HTTPS detection results.
		 */
		$errors = wp_get_https_detection_errors();

		$default_update_url = wp_get_default_update_https_url();

		$result = array(
			'label'       => __( 'Your website is using an active HTTPS connection' ),
			'status'      => 'good',
			'badge'       => array(
				'label' => __( 'Security' ),
				'color' => 'blue',
			),
			'description' => sprintf(
				'<p>%s</p>',
				__( 'An HTTPS connection is a more secure way of browsing the web. Many services now have HTTPS as a requirement. HTTPS allows you to take advantage of new features that can increase site speed, improve search rankings, and gain the trust of your visitors by helping to protect their online privacy.' )
			),
			'actions'     => sprintf(
				'<p><a href="%s" target="_blank">%s<span class="screen-reader-text"> %s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
				esc_url( $default_update_url ),
				__( 'Learn more about why you should use HTTPS' ),
				/* translators: Hidden accessibility text. */
				__( '(opens in a new tab)' )
			),
			'test'        => 'https_status',
		);

		if ( ! wp_is_using_https() ) {
			/*
			 * If the website is not using HTTPS, provide more information
			 * about whether it is supported and how it can be enabled.
			 */
			$result['status'] = 'recommended';
			$result['label']  = __( 'Your website does not use HTTPS' );

			if ( wp_is_site_url_using_https() ) {
				if ( is_ssl() ) {
					$result['description'] = sprintf(
						'<p>%s</p>',
						sprintf(
							/* translators: %s: URL to Settings > General > Site Address. */
							__( 'You are accessing this website using HTTPS, but your <a href="%s">Site Address</a> is not set up to use HTTPS by default.' ),
							esc_url( admin_url( 'options-general.php' ) . '#home' )
						)
					);
				} else {
					$result['description'] = sprintf(
						'<p>%s</p>',
						sprintf(
							/* translators: %s: URL to Settings > General > Site Address. */
							__( 'Your <a href="%s">Site Address</a> is not set up to use HTTPS.' ),
							esc_url( admin_url( 'options-general.php' ) . '#home' )
						)
					);
				}
			} else {
				if ( is_ssl() ) {
					$result['description'] = sprintf(
						'<p>%s</p>',
						sprintf(
							/* translators: 1: URL to Settings > General > WordPress Address, 2: URL to Settings > General > Site Address. */
							__( 'You are accessing this website using HTTPS, but your <a href="%1$s">WordPress Address</a> and <a href="%2$s">Site Address</a> are not set up to use HTTPS by default.' ),
							esc_url( admin_url( 'options-general.php' ) . '#siteurl' ),
							esc_url( admin_url( 'options-general.php' ) . '#home' )
						)
					);
				} else {
					$result['description'] = sprintf(
						'<p>%s</p>',
						sprintf(
							/* translators: 1: URL to Settings > General > WordPress Address, 2: URL to Settings > General > Site Address. */
							__( 'Your <a href="%1$s">WordPress Address</a> and <a href="%2$s">Site Address</a> are not set up to use HTTPS.' ),
							esc_url( admin_url( 'options-general.php' ) . '#siteurl' ),
							esc_url( admin_url( 'options-general.php' ) . '#home' )
						)
					);
				}
			}

			if ( wp_is_https_supported() ) {
				$result['description'] .= sprintf(
					'<p>%s</p>',
					__( 'HTTPS is already supported for your website.' )
				);

				if ( defined( 'WP_HOME' ) || defined( 'WP_SITEURL' ) ) {
					$result['description'] .= sprintf(
						'<p>%s</p>',
						sprintf(
							/* translators: 1: wp-config.php, 2: WP_HOME, 3: WP_SITEURL */
							__( 'However, your WordPress Address is currently controlled by a PHP constant and therefore cannot be updated. You need to edit your %1$s and remove or update the definitions of %2$s and %3$s.' ),
							'<code>wp-config.php</code>',
							'<code>WP_HOME</code>',
							'<code>WP_SITEURL</code>'
						)
					);
				} elseif ( current_user_can( 'update_https' ) ) {
					$default_direct_update_url = add_query_arg( 'action', 'update_https', wp_nonce_url( admin_url( 'site-health.php' ), 'wp_update_https' ) );
					$direct_update_url         = wp_get_direct_update_https_url();

					if ( ! empty( $direct_update_url ) ) {
						$result['actions'] = sprintf(
							'<p class="button-container"><a class="button button-primary" href="%1$s" target="_blank">%2$s<span class="screen-reader-text"> %3$s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
							esc_url( $direct_update_url ),
							__( 'Update your site to use HTTPS' ),
							/* translators: Hidden accessibility text. */
							__( '(opens in a new tab)' )
						);
					} else {
						$result['actions'] = sprintf(
							'<p class="button-container"><a class="button button-primary" href="%1$s">%2$s</a></p>',
							esc_url( $default_direct_update_url ),
							__( 'Update your site to use HTTPS' )
						);
					}
				}
			} else {
				// If host-specific "Update HTTPS" URL is provided, include a link.
				$update_url = wp_get_update_https_url();
				if ( $update_url !== $default_update_url ) {
					$result['description'] .= sprintf(
						'<p><a href="%s" target="_blank">%s<span class="screen-reader-text"> %s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
						esc_url( $update_url ),
						__( 'Talk to your web host about supporting HTTPS for your website.' ),
						/* translators: Hidden accessibility text. */
						__( '(opens in a new tab)' )
					);
				} else {
					$result['description'] .= sprintf(
						'<p>%s</p>',
						__( 'Talk to your web host about supporting HTTPS for your website.' )
					);
				}
			}
		}

		return $result;
	}

	/**
	 * Checks if the HTTP API can handle SSL/TLS requests.
	 *
	 * @since 5.2.0
	 *
	 * @return array The test result.
	 */
	public function get_test_ssl_support() {
		$result = array(
			'label'       => '',
			'status'      => '',
			'badge'       => array(
				'label' => __( 'Security' ),
				'color' => 'blue',
			),
			'description' => sprintf(
				'<p>%s</p>',
				__( 'Securely communicating between servers are needed for transactions such as fetching files, conducting sales on store sites, and much more.' )
			),
			'actions'     => '',
			'test'        => 'ssl_support',
		);

		$supports_https = wp_http_supports( array( 'ssl' ) );

		if ( $supports_https ) {
			$result['status'] = 'good';

			$result['label'] = __( 'Your site can communicate securely with other services' );
		} else {
			$result['status'] = 'critical';

			$result['label'] = __( 'Your site is unable to communicate securely with other services' );

			$result['description'] .= sprintf(
				'<p>%s</p>',
				__( 'Talk to your web host about OpenSSL support for PHP.' )
			);
		}

		return $result;
	}

	/**
	 * Tests if scheduled events run as intended.
	 *
	 * If scheduled events are not running, this may indicate something with WP_Cron is not working
	 * as intended, or that there are orphaned events hanging around from older code.
	 *
	 * @since 5.2.0
	 *
	 * @return array The test results.
	 */
	public function get_test_scheduled_events() {
		$result = array(
			'label'       => __( 'Scheduled events are running' ),
			'status'      => 'good',
			'badge'       => array(
				'label' => __( 'Performance' ),
				'color' => 'blue',
			),
			'description' => sprintf(
				'<p>%s</p>',
				__( 'Scheduled events are what periodically looks for updates to plugins, themes and WordPress itself. It is also what makes sure scheduled posts are published on time. It may also be used by various plugins to make sure that planned actions are executed.' )
			),
			'actions'     => '',
			'test'        => 'scheduled_events',
		);

		$this->wp_schedule_test_init();

		if ( is_wp_error( $this->has_missed_cron() ) ) {
			$result['status'] = 'critical';

			$result['label'] = __( 'It was not possible to check your scheduled events' );

			$result['description'] = sprintf(
				'<p>%s</p>',
				sprintf(
					/* translators: %s: The error message returned while from the cron scheduler. */
					__( 'While trying to test your site&#8217;s scheduled events, the following error was returned: %s' ),
					$this->has_missed_cron()->get_error_message()
				)
			);
		} elseif ( $this->has_missed_cron() ) {
			$result['status'] = 'recommended';

			$result['label'] = __( 'A scheduled event has failed' );

			$result['description'] = sprintf(
				'<p>%s</p>',
				sprintf(
					/* translators: %s: The name of the failed cron event. */
					__( 'The scheduled event, %s, failed to run. Your site still works, but this may indicate that scheduling posts or automated updates may not work as intended.' ),
					$this->last_missed_cron
				)
			);
		} elseif ( $this->has_late_cron() ) {
			$result['status'] = 'recommended';

			$result['label'] = __( 'A scheduled event is late' );

			$result['description'] = sprintf(
				'<p>%s</p>',
				sprintf(
					/* translators: %s: The name of the late cron event. */
					__( 'The scheduled event, %s, is late to run. Your site still works, but this may indicate that scheduling posts or automated updates may not work as intended.' ),
					$this->last_late_cron
				)
			);
		}

		return $result;
	}

	/**
	 * Tests if WordPress can run automated background updates.
	 *
	 * Background updates in WordPress are primarily used for minor releases and security updates.
	 * It's important to either have these working, or be aware that they are intentionally disabled
	 * for whatever reason.
	 *
	 * @since 5.2.0
	 *
	 * @return array The test results.
	 */
	public function get_test_background_updates() {
		$result = array(
			'label'       => __( 'Background updates are working' ),
			'status'      => 'good',
			'badge'       => array(
				'label' => __( 'Security' ),
				'color' => 'blue',
			),
			'description' => sprintf(
				'<p>%s</p>',
				__( 'Background updates ensure that WordPress can auto-update if a security update is released for the version you are currently using.' )
			),
			'actions'     => '',
			'test'        => 'background_updates',
		);

		if ( ! class_exists( 'WP_Site_Health_Auto_Updates' ) ) {
			require_once ABSPATH . 'wp-admin/includes/class-wp-site-health-auto-updates.php';
		}

		/*
		 * Run the auto-update tests in a separate class,
		 * as there are many considerations to be made.
		 */
		$automatic_updates = new WP_Site_Health_Auto_Updates();
		$tests             = $automatic_updates->run_tests();

		$output = '<ul>';

		foreach ( $tests as $test ) {
			/* translators: Hidden accessibility text. */
			$severity_string = __( 'Passed' );

			if ( 'fail' === $test->severity ) {
				$result['label'] = __( 'Background updates are not working as expected' );

				$result['status'] = 'critical';

				/* translators: Hidden accessibility text. */
				$severity_string = __( 'Error' );
			}

			if ( 'warning' === $test->severity && 'good' === $result['status'] ) {
				$result['label'] = __( 'Background updates may not be working properly' );

				$result['status'] = 'recommended';

				/* translators: Hidden accessibility text. */
				$severity_string = __( 'Warning' );
			}

			$output .= sprintf(
				'<li><span class="dashicons %s"><span class="screen-reader-text">%s</span></span> %s</li>',
				esc_attr( $test->severity ),
				$severity_string,
				$test->description
			);
		}

		$output .= '</ul>';

		if ( 'good' !== $result['status'] ) {
			$result['description'] .= $output;
		}

		return $result;
	}

	/**
	 * Tests if plugin and theme auto-updates appear to be configured correctly.
	 *
	 * @since 5.5.0
	 *
	 * @return array The test results.
	 */
	public function get_test_plugin_theme_auto_updates() {
		$result = array(
			'label'       => __( 'Plugin and theme auto-updates appear to be configured correctly' ),
			'status'      => 'good',
			'badge'       => array(
				'label' => __( 'Security' ),
				'color' => 'blue',
			),
			'description' => sprintf(
				'<p>%s</p>',
				__( 'Plugin and theme auto-updates ensure that the latest versions are always installed.' )
			),
			'actions'     => '',
			'test'        => 'plugin_theme_auto_updates',
		);

		$check_plugin_theme_updates = $this->detect_plugin_theme_auto_update_issues();

		$result['status'] = $check_plugin_theme_updates->status;

		if ( 'good' !== $result['status'] ) {
			$result['label'] = __( 'Your site may have problems auto-updating plugins and themes' );

			$result['description'] .= sprintf(
				'<p>%s</p>',
				$check_plugin_theme_updates->message
			);
		}

		return $result;
	}

	/**
	 * Tests available disk space for updates.
	 *
	 * @since 6.3.0
	 *
	 * @return array The test results.
	 */
	public function get_test_available_updates_disk_space() {
		$available_space = function_exists( 'disk_free_space' ) ? @disk_free_space( WP_CONTENT_DIR ) : false;

		$result = array(
			'label'       => __( 'Disk space available to safely perform updates' ),
			'status'      => 'good',
			'badge'       => array(
				'label' => __( 'Security' ),
				'color' => 'blue',
			),
			'description' => sprintf(
				/* translators: %s: Available disk space in MB or GB. */
				'<p>' . __( '%s available disk space was detected, update routines can be performed safely.' ) . '</p>',
				size_format( $available_space )
			),
			'actions'     => '',
			'test'        => 'available_updates_disk_space',
		);

		if ( false === $available_space ) {
			$result['description'] = __( 'Could not determine available disk space for updates.' );
			$result['status']      = 'recommended';
		} elseif ( $available_space < 20 * MB_IN_BYTES ) {
			$result['description'] = sprintf(
				/* translators: %s: Available disk space in MB or GB. */
				__( 'Available disk space is critically low, less than %s available. Proceed with caution, updates may fail.' ),
				size_format( 20 * MB_IN_BYTES )
			);
			$result['status'] = 'critical';
		} elseif ( $available_space < 100 * MB_IN_BYTES ) {
			$result['description'] = sprintf(
				/* translators: %s: Available disk space in MB or GB. */
				__( 'Available disk space is low, less than %s available.' ),
				size_format( 100 * MB_IN_BYTES )
			);
			$result['status'] = 'recommended';
		}

		return $result;
	}

	/**
	 * Tests if plugin and theme temporary backup directories are writable or can be created.
	 *
	 * @since 6.3.0
	 *
	 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
	 *
	 * @return array The test results.
	 */
	public function get_test_update_temp_backup_writable() {
		global $wp_filesystem;

		$result = array(
			'label'       => __( 'Plugin and theme temporary backup directory is writable' ),
			'status'      => 'good',
			'badge'       => array(
				'label' => __( 'Security' ),
				'color' => 'blue',
			),
			'description' => sprintf(
				/* translators: %s: wp-content/upgrade-temp-backup */
				'<p>' . __( 'The %s directory used to improve the stability of plugin and theme updates is writable.' ) . '</p>',
				'<code>wp-content/upgrade-temp-backup</code>'
			),
			'actions'     => '',
			'test'        => 'update_temp_backup_writable',
		);

		if ( ! function_exists( 'WP_Filesystem' ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
		}

		ob_start();
		$credentials = request_filesystem_credentials( '' );
		ob_end_clean();

		if ( false === $credentials || ! WP_Filesystem( $credentials ) ) {
			$result['status']      = 'recommended';
			$result['label']       = __( 'Could not access filesystem' );
			$result['description'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' );
			return $result;
		}

		$wp_content = $wp_filesystem->wp_content_dir();

		if ( ! $wp_content ) {
			$result['status']      = 'critical';
			$result['label']       = __( 'Unable to locate WordPress content directory' );
			$result['description'] = sprintf(
				/* translators: %s: wp-content */
				'<p>' . __( 'The %s directory cannot be located.' ) . '</p>',
				'<code>wp-content</code>'
			);
			return $result;
		}

		$upgrade_dir_exists      = $wp_filesystem->is_dir( "$wp_content/upgrade" );
		$upgrade_dir_is_writable = $wp_filesystem->is_writable( "$wp_content/upgrade" );
		$backup_dir_exists       = $wp_filesystem->is_dir( "$wp_content/upgrade-temp-backup" );
		$backup_dir_is_writable  = $wp_filesystem->is_writable( "$wp_content/upgrade-temp-backup" );

		$plugins_dir_exists      = $wp_filesystem->is_dir( "$wp_content/upgrade-temp-backup/plugins" );
		$plugins_dir_is_writable = $wp_filesystem->is_writable( "$wp_content/upgrade-temp-backup/plugins" );
		$themes_dir_exists       = $wp_filesystem->is_dir( "$wp_content/upgrade-temp-backup/themes" );
		$themes_dir_is_writable  = $wp_filesystem->is_writable( "$wp_content/upgrade-temp-backup/themes" );

		if ( $plugins_dir_exists && ! $plugins_dir_is_writable && $themes_dir_exists && ! $themes_dir_is_writable ) {
			$result['status']      = 'critical';
			$result['label']       = __( 'Plugin and theme temporary backup directories exist but are not writable' );
			$result['description'] = sprintf(
				/* translators: 1: wp-content/upgrade-temp-backup/plugins, 2: wp-content/upgrade-temp-backup/themes. */
				'<p>' . __( 'The %1$s and %2$s directories exist but are not writable. These directories are used to improve the stability of plugin updates. Please make sure the server has write permissions to these directories.' ) . '</p>',
				'<code>wp-content/upgrade-temp-backup/plugins</code>',
				'<code>wp-content/upgrade-temp-backup/themes</code>'
			);
			return $result;
		}

		if ( $plugins_dir_exists && ! $plugins_dir_is_writable ) {
			$result['status']      = 'critical';
			$result['label']       = __( 'Plugin temporary backup directory exists but is not writable' );
			$result['description'] = sprintf(
				/* translators: %s: wp-content/upgrade-temp-backup/plugins */
				'<p>' . __( 'The %s directory exists but is not writable. This directory is used to improve the stability of plugin updates. Please make sure the server has write permissions to this directory.' ) . '</p>',
				'<code>wp-content/upgrade-temp-backup/plugins</code>'
			);
			return $result;
		}

		if ( $themes_dir_exists && ! $themes_dir_is_writable ) {
			$result['status']      = 'critical';
			$result['label']       = __( 'Theme temporary backup directory exists but is not writable' );
			$result['description'] = sprintf(
				/* translators: %s: wp-content/upgrade-temp-backup/themes */
				'<p>' . __( 'The %s directory exists but is not writable. This directory is used to improve the stability of theme updates. Please make sure the server has write permissions to this directory.' ) . '</p>',
				'<code>wp-content/upgrade-temp-backup/themes</code>'
			);
			return $result;
		}

		if ( ( ! $plugins_dir_exists || ! $themes_dir_exists ) && $backup_dir_exists && ! $backup_dir_is_writable ) {
			$result['status']      = 'critical';
			$result['label']       = __( 'The temporary backup directory exists but is not writable' );
			$result['description'] = sprintf(
				/* translators: %s: wp-content/upgrade-temp-backup */
				'<p>' . __( 'The %s directory exists but is not writable. This directory is used to improve the stability of plugin and theme updates. Please make sure the server has write permissions to this directory.' ) . '</p>',
				'<code>wp-content/upgrade-temp-backup</code>'
			);
			return $result;
		}

		if ( ! $backup_dir_exists && $upgrade_dir_exists && ! $upgrade_dir_is_writable ) {
			$result['status']      = 'critical';
			$result['label']       = __( 'The upgrade directory exists but is not writable' );
			$result['description'] = sprintf(
				/* translators: %s: wp-content/upgrade */
				'<p>' . __( 'The %s directory exists but is not writable. This directory is used for plugin and theme updates. Please make sure the server has write permissions to this directory.' ) . '</p>',
				'<code>wp-content/upgrade</code>'
			);
			return $result;
		}

		if ( ! $upgrade_dir_exists && ! $wp_filesystem->is_writable( $wp_content ) ) {
			$result['status']      = 'critical';
			$result['label']       = __( 'The upgrade directory cannot be created' );
			$result['description'] = sprintf(
				/* translators: 1: wp-content/upgrade, 2: wp-content. */
				'<p>' . __( 'The %1$s directory does not exist, and the server does not have write permissions in %2$s to create it. This directory is used for plugin and theme updates. Please make sure the server has write permissions in %2$s.' ) . '</p>',
				'<code>wp-content/upgrade</code>',
				'<code>wp-content</code>'
			);
			return $result;
		}

		return $result;
	}

	/**
	 * Tests if loopbacks work as expected.
	 *
	 * A loopback is when WordPress queries itself, for example to start a new WP_Cron instance,
	 * or when editing a plugin or theme. This has shown itself to be a recurring issue,
	 * as code can very easily break this interaction.
	 *
	 * @since 5.2.0
	 *
	 * @return array The test results.
	 */
	public function get_test_loopback_requests() {
		$result = array(
			'label'       => __( 'Your site can perform loopback requests' ),
			'status'      => 'good',
			'badge'       => array(
				'label' => __( 'Performance' ),
				'color' => 'blue',
			),
			'description' => sprintf(
				'<p>%s</p>',
				__( 'Loopback requests are used to run scheduled events, and are also used by the built-in editors for themes and plugins to verify code stability.' )
			),
			'actions'     => '',
			'test'        => 'loopback_requests',
		);

		$check_loopback = $this->can_perform_loopback();

		$result['status'] = $check_loopback->status;

		if ( 'good' !== $result['status'] ) {
			$result['label'] = __( 'Your site could not complete a loopback request' );

			$result['description'] .= sprintf(
				'<p>%s</p>',
				$check_loopback->message
			);
		}

		return $result;
	}

	/**
	 * Tests if HTTP requests are blocked.
	 *
	 * It's possible to block all outgoing communication (with the possibility of allowing certain
	 * hosts) via the HTTP API. This may create problems for users as many features are running as
	 * services these days.
	 *
	 * @since 5.2.0
	 *
	 * @return array The test results.
	 */
	public function get_test_http_requests() {
		$result = array(
			'label'       => __( 'HTTP requests seem to be working as expected' ),
			'status'      => 'good',
			'badge'       => array(
				'label' => __( 'Performance' ),
				'color' => 'blue',
			),
			'description' => sprintf(
				'<p>%s</p>',
				__( 'It is possible for site maintainers to block all, or some, communication to other sites and services. If set up incorrectly, this may prevent plugins and themes from working as intended.' )
			),
			'actions'     => '',
			'test'        => 'http_requests',
		);

		$blocked = false;
		$hosts   = array();

		if ( defined( 'WP_HTTP_BLOCK_EXTERNAL' ) && WP_HTTP_BLOCK_EXTERNAL ) {
			$blocked = true;
		}

		if ( defined( 'WP_ACCESSIBLE_HOSTS' ) ) {
			$hosts = explode( ',', WP_ACCESSIBLE_HOSTS );
		}

		if ( $blocked && 0 === count( $hosts ) ) {
			$result['status'] = 'critical';

			$result['label'] = __( 'HTTP requests are blocked' );

			$result['description'] .= sprintf(
				'<p>%s</p>',
				sprintf(
					/* translators: %s: Name of the constant used. */
					__( 'HTTP requests have been blocked by the %s constant, with no allowed hosts.' ),
					'<code>WP_HTTP_BLOCK_EXTERNAL</code>'
				)
			);
		}

		if ( $blocked && 0 < count( $hosts ) ) {
			$result['status'] = 'recommended';

			$result['label'] = __( 'HTTP requests are partially blocked' );

			$result['description'] .= sprintf(
				'<p>%s</p>',
				sprintf(
					/* translators: 1: Name of the constant used. 2: List of allowed hostnames. */
					__( 'HTTP requests have been blocked by the %1$s constant, with some allowed hosts: %2$s.' ),
					'<code>WP_HTTP_BLOCK_EXTERNAL</code>',
					implode( ',', $hosts )
				)
			);
		}

		return $result;
	}

	/**
	 * Tests if the REST API is accessible.
	 *
	 * Various security measures may block the REST API from working, or it may have been disabled in general.
	 * This is required for the new block editor to work, so we explicitly test for this.
	 *
	 * @since 5.2.0
	 *
	 * @return array The test results.
	 */
	public function get_test_rest_availability() {
		$result = array(
			'label'       => __( 'The REST API is available' ),
			'status'      => 'good',
			'badge'       => array(
				'label' => __( 'Performance' ),
				'color' => 'blue',
			),
			'description' => sprintf(
				'<p>%s</p>',
				__( 'The REST API is one way that WordPress and other applications communicate with the server. For example, the block editor screen relies on the REST API to display and save your posts and pages.' )
			),
			'actions'     => '',
			'test'        => 'rest_availability',
		);

		$cookies = wp_unslash( $_COOKIE );
		$timeout = 10; // 10 seconds.
		$headers = array(
			'Cache-Control' => 'no-cache',
			'X-WP-Nonce'    => wp_create_nonce( 'wp_rest' ),
		);
		/** This filter is documented in wp-includes/class-wp-http-streams.php */
		$sslverify = apply_filters( 'https_local_ssl_verify', false );

		// Include Basic auth in loopback requests.
		if ( isset( $_SERVER['PHP_AUTH_USER'] ) && isset( $_SERVER['PHP_AUTH_PW'] ) ) {
			$headers['Authorization'] = 'Basic ' . base64_encode( wp_unslash( $_SERVER['PHP_AUTH_USER'] ) . ':' . wp_unslash( $_SERVER['PHP_AUTH_PW'] ) );
		}

		$url = rest_url( 'wp/v2/types/post' );

		// The context for this is editing with the new block editor.
		$url = add_query_arg(
			array(
				'context' => 'edit',
			),
			$url
		);

		$r = wp_remote_get( $url, compact( 'cookies', 'headers', 'timeout', 'sslverify' ) );

		if ( is_wp_error( $r ) ) {
			$result['status'] = 'critical';

			$result['label'] = __( 'The REST API encountered an error' );

			$result['description'] .= sprintf(
				'<p>%s</p><p>%s<br>%s</p>',
				__( 'When testing the REST API, an error was encountered:' ),
				sprintf(
					// translators: %s: The REST API URL.
					__( 'REST API Endpoint: %s' ),
					$url
				),
				sprintf(
					// translators: 1: The WordPress error code. 2: The WordPress error message.
					__( 'REST API Response: (%1$s) %2$s' ),
					$r->get_error_code(),
					$r->get_error_message()
				)
			);
		} elseif ( 200 !== wp_remote_retrieve_response_code( $r ) ) {
			$result['status'] = 'recommended';

			$result['label'] = __( 'The REST API encountered an unexpected result' );

			$result['description'] .= sprintf(
				'<p>%s</p><p>%s<br>%s</p>',
				__( 'When testing the REST API, an unexpected result was returned:' ),
				sprintf(
					// translators: %s: The REST API URL.
					__( 'REST API Endpoint: %s' ),
					$url
				),
				sprintf(
					// translators: 1: The WordPress error code. 2: The HTTP status code error message.
					__( 'REST API Response: (%1$s) %2$s' ),
					wp_remote_retrieve_response_code( $r ),
					wp_remote_retrieve_response_message( $r )
				)
			);
		} else {
			$json = json_decode( wp_remote_retrieve_body( $r ), true );

			if ( false !== $json && ! isset( $json['capabilities'] ) ) {
				$result['status'] = 'recommended';

				$result['label'] = __( 'The REST API did not behave correctly' );

				$result['description'] .= sprintf(
					'<p>%s</p>',
					sprintf(
						/* translators: %s: The name of the query parameter being tested. */
						__( 'The REST API did not process the %s query parameter correctly.' ),
						'<code>context</code>'
					)
				);
			}
		}

		return $result;
	}

	/**
	 * Tests if 'file_uploads' directive in PHP.ini is turned off.
	 *
	 * @since 5.5.0
	 *
	 * @return array The test results.
	 */
	public function get_test_file_uploads() {
		$result = array(
			'label'       => __( 'Files can be uploaded' ),
			'status'      => 'good',
			'badge'       => array(
				'label' => __( 'Performance' ),
				'color' => 'blue',
			),
			'description' => sprintf(
				'<p>%s</p>',
				sprintf(
					/* translators: 1: file_uploads, 2: php.ini */
					__( 'The %1$s directive in %2$s determines if uploading files is allowed on your site.' ),
					'<code>file_uploads</code>',
					'<code>php.ini</code>'
				)
			),
			'actions'     => '',
			'test'        => 'file_uploads',
		);

		if ( ! function_exists( 'ini_get' ) ) {
			$result['status']       = 'critical';
			$result['description'] .= sprintf(
				/* translators: %s: ini_get() */
				__( 'The %s function has been disabled, some media settings are unavailable because of this.' ),
				'<code>ini_get()</code>'
			);
			return $result;
		}

		if ( empty( ini_get( 'file_uploads' ) ) ) {
			$result['status']       = 'critical';
			$result['description'] .= sprintf(
				'<p>%s</p>',
				sprintf(
					/* translators: 1: file_uploads, 2: 0 */
					__( '%1$s is set to %2$s. You won\'t be able to upload files on your site.' ),
					'<code>file_uploads</code>',
					'<code>0</code>'
				)
			);
			return $result;
		}

		$post_max_size       = ini_get( 'post_max_size' );
		$upload_max_filesize = ini_get( 'upload_max_filesize' );

		if ( wp_convert_hr_to_bytes( $post_max_size ) < wp_convert_hr_to_bytes( $upload_max_filesize ) ) {
			$result['label'] = sprintf(
				/* translators: 1: post_max_size, 2: upload_max_filesize */
				__( 'The "%1$s" value is smaller than "%2$s"' ),
				'post_max_size',
				'upload_max_filesize'
			);
			$result['status'] = 'recommended';

			if ( 0 === wp_convert_hr_to_bytes( $post_max_size ) ) {
				$result['description'] = sprintf(
					'<p>%s</p>',
					sprintf(
						/* translators: 1: post_max_size, 2: upload_max_filesize */
						__( 'The setting for %1$s is currently configured as 0, this could cause some problems when trying to upload files through plugin or theme features that rely on various upload methods. It is recommended to configure this setting to a fixed value, ideally matching the value of %2$s, as some upload methods read the value 0 as either unlimited, or disabled.' ),
						'<code>post_max_size</code>',
						'<code>upload_max_filesize</code>'
					)
				);
			} else {
				$result['description'] = sprintf(
					'<p>%s</p>',
					sprintf(
						/* translators: 1: post_max_size, 2: upload_max_filesize */
						__( 'The setting for %1$s is smaller than %2$s, this could cause some problems when trying to upload files.' ),
						'<code>post_max_size</code>',
						'<code>upload_max_filesize</code>'
					)
				);
			}

			return $result;
		}

		return $result;
	}

	/**
	 * Tests if the Authorization header has the expected values.
	 *
	 * @since 5.6.0
	 *
	 * @return array
	 */
	public function get_test_authorization_header() {
		$result = array(
			'label'       => __( 'The Authorization header is working as expected' ),
			'status'      => 'good',
			'badge'       => array(
				'label' => __( 'Security' ),
				'color' => 'blue',
			),
			'description' => sprintf(
				'<p>%s</p>',
				__( 'The Authorization header is used by third-party applications you have approved for this site. Without this header, those apps cannot connect to your site.' )
			),
			'actions'     => '',
			'test'        => 'authorization_header',
		);

		if ( ! isset( $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'] ) ) {
			$result['label'] = __( 'The authorization header is missing' );
		} elseif ( 'user' !== $_SERVER['PHP_AUTH_USER'] || 'pwd' !== $_SERVER['PHP_AUTH_PW'] ) {
			$result['label'] = __( 'The authorization header is invalid' );
		} else {
			return $result;
		}

		$result['status']       = 'recommended';
		$result['description'] .= sprintf(
			'<p>%s</p>',
			__( 'If you are still seeing this warning after having tried the actions below, you may need to contact your hosting provider for further assistance.' )
		);

		if ( ! function_exists( 'got_mod_rewrite' ) ) {
			require_once ABSPATH . 'wp-admin/includes/misc.php';
		}

		if ( got_mod_rewrite() ) {
			$result['actions'] .= sprintf(
				'<p><a href="%s">%s</a></p>',
				esc_url( admin_url( 'options-permalink.php' ) ),
				__( 'Flush permalinks' )
			);
		} else {
			$result['actions'] .= sprintf(
				'<p><a href="%s" target="_blank">%s<span class="screen-reader-text"> %s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
				__( 'https://developer.wordpress.org/rest-api/frequently-asked-questions/#why-is-authentication-not-working' ),
				__( 'Learn how to configure the Authorization header.' ),
				/* translators: Hidden accessibility text. */
				__( '(opens in a new tab)' )
			);
		}

		return $result;
	}

	/**
	 * Tests if a full page cache is available.
	 *
	 * @since 6.1.0
	 *
	 * @return array The test result.
	 */
	public function get_test_page_cache() {
		$description  = '<p>' . __( 'Page cache enhances the speed and performance of your site by saving and serving static pages instead of calling for a page every time a user visits.' ) . '</p>';
		$description .= '<p>' . __( 'Page cache is detected by looking for an active page cache plugin as well as making three requests to the homepage and looking for one or more of the following HTTP client caching response headers:' ) . '</p>';
		$description .= '<code>' . implode( '</code>, <code>', array_keys( $this->get_page_cache_headers() ) ) . '.</code>';

		$result = array(
			'badge'       => array(
				'label' => __( 'Performance' ),
				'color' => 'blue',
			),
			'description' => wp_kses_post( $description ),
			'test'        => 'page_cache',
			'status'      => 'good',
			'label'       => '',
			'actions'     => sprintf(
				'<p><a href="%1$s" target="_blank" rel="noreferrer">%2$s<span class="screen-reader-text"> %3$s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
				__( 'https://developer.wordpress.org/advanced-administration/performance/optimization/#caching' ),
				__( 'Learn more about page cache' ),
				/* translators: Hidden accessibility text. */
				__( '(opens in a new tab)' )
			),
		);

		$page_cache_detail = $this->get_page_cache_detail();

		if ( is_wp_error( $page_cache_detail ) ) {
			$result['label']  = __( 'Unable to detect the presence of page cache' );
			$result['status'] = 'recommended';
			$error_info       = sprintf(
			/* translators: 1: Error message, 2: Error code. */
				__( 'Unable to detect page cache due to possible loopback request problem. Please verify that the loopback request test is passing. Error: %1$s (Code: %2$s)' ),
				$page_cache_detail->get_error_message(),
				$page_cache_detail->get_error_code()
			);
			$result['description'] = wp_kses_post( "<p>$error_info</p>" ) . $result['description'];
			return $result;
		}

		$result['status'] = $page_cache_detail['status'];

		switch ( $page_cache_detail['status'] ) {
			case 'recommended':
				$result['label'] = __( 'Page cache is not detected but the server response time is OK' );
				break;
			case 'good':
				$result['label'] = __( 'Page cache is detected and the server response time is good' );
				break;
			default:
				if ( empty( $page_cache_detail['headers'] ) && ! $page_cache_detail['advanced_cache_present'] ) {
					$result['label'] = __( 'Page cache is not detected and the server response time is slow' );
				} else {
					$result['label'] = __( 'Page cache is detected but the server response time is still slow' );
				}
		}

		$page_cache_test_summary = array();

		if ( empty( $page_cache_detail['response_time'] ) ) {
			$page_cache_test_summary[] = '<span class="dashicons dashicons-dismiss"></span> ' . __( 'Server response time could not be determined. Verify that loopback requests are working.' );
		} else {

			$threshold = $this->get_good_response_time_threshold();
			if ( $page_cache_detail['response_time'] < $threshold ) {
				$page_cache_test_summary[] = '<span class="dashicons dashicons-yes-alt"></span> ' . sprintf(
					/* translators: 1: The response time in milliseconds, 2: The recommended threshold in milliseconds. */
					__( 'Median server response time was %1$s milliseconds. This is less than the recommended %2$s milliseconds threshold.' ),
					number_format_i18n( $page_cache_detail['response_time'] ),
					number_format_i18n( $threshold )
				);
			} else {
				$page_cache_test_summary[] = '<span class="dashicons dashicons-warning"></span> ' . sprintf(
					/* translators: 1: The response time in milliseconds, 2: The recommended threshold in milliseconds. */
					__( 'Median server response time was %1$s milliseconds. It should be less than the recommended %2$s milliseconds threshold.' ),
					number_format_i18n( $page_cache_detail['response_time'] ),
					number_format_i18n( $threshold )
				);
			}

			if ( empty( $page_cache_detail['headers'] ) ) {
				$page_cache_test_summary[] = '<span class="dashicons dashicons-warning"></span> ' . __( 'No client caching response headers were detected.' );
			} else {
				$headers_summary  = '<span class="dashicons dashicons-yes-alt"></span>';
				$headers_summary .= ' ' . sprintf(
					/* translators: %d: Number of caching headers. */
					_n(
						'There was %d client caching response header detected:',
						'There were %d client caching response headers detected:',
						count( $page_cache_detail['headers'] )
					),
					count( $page_cache_detail['headers'] )
				);
				$headers_summary          .= ' <code>' . implode( '</code>, <code>', $page_cache_detail['headers'] ) . '</code>.';
				$page_cache_test_summary[] = $headers_summary;
			}
		}

		if ( $page_cache_detail['advanced_cache_present'] ) {
			$page_cache_test_summary[] = '<span class="dashicons dashicons-yes-alt"></span> ' . __( 'A page cache plugin was detected.' );
		} elseif ( ! ( is_array( $page_cache_detail ) && ! empty( $page_cache_detail['headers'] ) ) ) {
			// Note: This message is not shown if client caching response headers were present since an external caching layer may be employed.
			$page_cache_test_summary[] = '<span class="dashicons dashicons-warning"></span> ' . __( 'A page cache plugin was not detected.' );
		}

		$result['description'] .= '<ul><li>' . implode( '</li><li>', $page_cache_test_summary ) . '</li></ul>';
		return $result;
	}

	/**
	 * Tests if the site uses persistent object cache and recommends to use it if not.
	 *
	 * @since 6.1.0
	 *
	 * @return array The test result.
	 */
	public function get_test_persistent_object_cache() {
		/**
		 * Filters the action URL for the persistent object cache health check.
		 *
		 * @since 6.1.0
		 *
		 * @param string $action_url Learn more link for persistent object cache health check.
		 */
		$action_url = apply_filters(
			'site_status_persistent_object_cache_url',
			/* translators: Localized Support reference. */
			__( 'https://developer.wordpress.org/advanced-administration/performance/optimization/#persistent-object-cache' )
		);

		$result = array(
			'test'        => 'persistent_object_cache',
			'status'      => 'good',
			'badge'       => array(
				'label' => __( 'Performance' ),
				'color' => 'blue',
			),
			'label'       => __( 'A persistent object cache is being used' ),
			'description' => sprintf(
				'<p>%s</p>',
				__( 'A persistent object cache makes your site&#8217;s database more efficient, resulting in faster load times because WordPress can retrieve your site&#8217;s content and settings much more quickly.' )
			),
			'actions'     => sprintf(
				'<p><a href="%s" target="_blank">%s<span class="screen-reader-text"> %s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
				esc_url( $action_url ),
				__( 'Learn more about persistent object caching.' ),
				/* translators: Hidden accessibility text. */
				__( '(opens in a new tab)' )
			),
		);

		if ( wp_using_ext_object_cache() ) {
			return $result;
		}

		if ( ! $this->should_suggest_persistent_object_cache() ) {
			$result['label'] = __( 'A persistent object cache is not required' );

			return $result;
		}

		$available_services = $this->available_object_cache_services();

		$notes = __( 'Your hosting provider can tell you if a persistent object cache can be enabled on your site.' );

		if ( ! empty( $available_services ) ) {
			$notes .= ' ' . sprintf(
				/* translators: Available object caching services. */
				__( 'Your host appears to support the following object caching services: %s.' ),
				implode( ', ', $available_services )
			);
		}

		/**
		 * Filters the second paragraph of the health check's description
		 * when suggesting the use of a persistent object cache.
		 *
		 * Hosts may want to replace the notes to recommend their preferred object caching solution.
		 *
		 * Plugin authors may want to append notes (not replace) on why object caching is recommended for their plugin.
		 *
		 * @since 6.1.0
		 *
		 * @param string   $notes              The notes appended to the health check description.
		 * @param string[] $available_services The list of available persistent object cache services.
		 */
		$notes = apply_filters( 'site_status_persistent_object_cache_notes', $notes, $available_services );

		$result['status']       = 'recommended';
		$result['label']        = __( 'You should use a persistent object cache' );
		$result['description'] .= sprintf(
			'<p>%s</p>',
			wp_kses(
				$notes,
				array(
					'a'      => array( 'href' => true ),
					'code'   => true,
					'em'     => true,
					'strong' => true,
				)
			)
		);

		return $result;
	}

	/**
	 * Calculates total amount of autoloaded data.
	 *
	 * @since 6.6.0
	 *
	 * @return int Autoloaded data in bytes.
	 */
	public function get_autoloaded_options_size() {
		$alloptions = wp_load_alloptions();

		$total_length = 0;

		foreach ( $alloptions as $option_value ) {
			if ( is_array( $option_value ) || is_object( $option_value ) ) {
				$option_value = maybe_serialize( $option_value );
			}
			$total_length += strlen( (string) $option_value );
		}

		return $total_length;
	}

	/**
	 * Tests the number of autoloaded options.
	 *
	 * @since 6.6.0
	 *
	 * @return array The test results.
	 */
	public function get_test_autoloaded_options() {
		$autoloaded_options_size  = $this->get_autoloaded_options_size();
		$autoloaded_options_count = count( wp_load_alloptions() );

		$base_description = __( 'Autoloaded options are configuration settings for plugins and themes that are automatically loaded with every page load in WordPress. Having too many autoloaded options can slow down your site.' );

		$result = array(
			'label'       => __( 'Autoloaded options are acceptable' ),
			'status'      => 'good',
			'badge'       => array(
				'label' => __( 'Performance' ),
				'color' => 'blue',
			),
			'description' => sprintf(
				/* translators: 1: Number of autoloaded options, 2: Autoloaded options size. */
				'<p>' . esc_html( $base_description ) . ' ' . __( 'Your site has %1$s autoloaded options (size: %2$s) in the options table, which is acceptable.' ) . '</p>',
				$autoloaded_options_count,
				size_format( $autoloaded_options_size )
			),
			'actions'     => '',
			'test'        => 'autoloaded_options',
		);

		/**
		 * Filters max bytes threshold to trigger warning in Site Health.
		 *
		 * @since 6.6.0
		 *
		 * @param int $limit Autoloaded options threshold size. Default 800000.
		 */
		$limit = apply_filters( 'site_status_autoloaded_options_size_limit', 800000 );

		if ( $autoloaded_options_size < $limit ) {
			return $result;
		}

		$result['status']      = 'critical';
		$result['label']       = __( 'Autoloaded options could affect performance' );
		$result['description'] = sprintf(
			/* translators: 1: Number of autoloaded options, 2: Autoloaded options size. */
			'<p>' . esc_html( $base_description ) . ' ' . __( 'Your site has %1$s autoloaded options (size: %2$s) in the options table, which could cause your site to be slow. You can review the options being autoloaded in your database and remove any options that are no longer needed by your site.' ) . '</p>',
			$autoloaded_options_count,
			size_format( $autoloaded_options_size )
		);

		/**
		 * Filters description to be shown on Site Health warning when threshold is met.
		 *
		 * @since 6.6.0
		 *
		 * @param string $description Description message when autoloaded options bigger than threshold.
		 */
		$result['description'] = apply_filters( 'site_status_autoloaded_options_limit_description', $result['description'] );

		$result['actions'] = sprintf(
			/* translators: 1: HelpHub URL, 2: Link description. */
			'<p><a target="_blank" href="%1$s">%2$s</a></p>',
			esc_url( __( 'https://developer.wordpress.org/advanced-administration/performance/optimization/#autoloaded-options' ) ),
			__( 'More info about optimizing autoloaded options' )
		);

		/**
		 * Filters actionable information to tackle the problem. It can be a link to an external guide.
		 *
		 * @since 6.6.0
		 *
		 * @param string $actions Call to Action to be used to point to the right direction to solve the issue.
		 */
		$result['actions'] = apply_filters( 'site_status_autoloaded_options_action_to_perform', $result['actions'] );
		return $result;
	}

	/**
	 * Returns a set of tests that belong to the site status page.
	 *
	 * Each site status test is defined here, they may be `direct` tests, that run on page load, or `async` tests
	 * which will run later down the line via JavaScript calls to improve page performance and hopefully also user
	 * experiences.
	 *
	 * @since 5.2.0
	 * @since 5.6.0 Added support for `has_rest` and `permissions`.
	 *
	 * @return array The list of tests to run.
	 */
	public static function get_tests() {
		$tests = array(
			'direct' => array(
				'wordpress_version'            => array(
					'label' => __( 'WordPress Version' ),
					'test'  => 'wordpress_version',
				),
				'plugin_version'               => array(
					'label' => __( 'Plugin Versions' ),
					'test'  => 'plugin_version',
				),
				'theme_version'                => array(
					'label' => __( 'Theme Versions' ),
					'test'  => 'theme_version',
				),
				'php_version'                  => array(
					'label' => __( 'PHP Version' ),
					'test'  => 'php_version',
				),
				'php_extensions'               => array(
					'label' => __( 'PHP Extensions' ),
					'test'  => 'php_extensions',
				),
				'php_default_timezone'         => array(
					'label' => __( 'PHP Default Timezone' ),
					'test'  => 'php_default_timezone',
				),
				'php_sessions'                 => array(
					'label' => __( 'PHP Sessions' ),
					'test'  => 'php_sessions',
				),
				'sql_server'                   => array(
					'label' => __( 'Database Server version' ),
					'test'  => 'sql_server',
				),
				'ssl_support'                  => array(
					'label' => __( 'Secure communication' ),
					'test'  => 'ssl_support',
				),
				'scheduled_events'             => array(
					'label' => __( 'Scheduled events' ),
					'test'  => 'scheduled_events',
				),
				'http_requests'                => array(
					'label' => __( 'HTTP Requests' ),
					'test'  => 'http_requests',
				),
				'rest_availability'            => array(
					'label'     => __( 'REST API availability' ),
					'test'      => 'rest_availability',
					'skip_cron' => true,
				),
				'debug_enabled'                => array(
					'label' => __( 'Debugging enabled' ),
					'test'  => 'is_in_debug_mode',
				),
				'file_uploads'                 => array(
					'label' => __( 'File uploads' ),
					'test'  => 'file_uploads',
				),
				'plugin_theme_auto_updates'    => array(
					'label' => __( 'Plugin and theme auto-updates' ),
					'test'  => 'plugin_theme_auto_updates',
				),
				'update_temp_backup_writable'  => array(
					'label' => __( 'Plugin and theme temporary backup directory access' ),
					'test'  => 'update_temp_backup_writable',
				),
				'available_updates_disk_space' => array(
					'label' => __( 'Available disk space' ),
					'test'  => 'available_updates_disk_space',
				),
				'autoloaded_options'           => array(
					'label' => __( 'Autoloaded options' ),
					'test'  => 'autoloaded_options',
				),
			),
			'async'  => array(
				'dotorg_communication' => array(
					'label'             => __( 'Communication with WordPress.org' ),
					'test'              => rest_url( 'wp-site-health/v1/tests/dotorg-communication' ),
					'has_rest'          => true,
					'async_direct_test' => array( WP_Site_Health::get_instance(), 'get_test_dotorg_communication' ),
				),
				'background_updates'   => array(
					'label'             => __( 'Background updates' ),
					'test'              => rest_url( 'wp-site-health/v1/tests/background-updates' ),
					'has_rest'          => true,
					'async_direct_test' => array( WP_Site_Health::get_instance(), 'get_test_background_updates' ),
				),
				'loopback_requests'    => array(
					'label'             => __( 'Loopback request' ),
					'test'              => rest_url( 'wp-site-health/v1/tests/loopback-requests' ),
					'has_rest'          => true,
					'async_direct_test' => array( WP_Site_Health::get_instance(), 'get_test_loopback_requests' ),
				),
				'https_status'         => array(
					'label'             => __( 'HTTPS status' ),
					'test'              => rest_url( 'wp-site-health/v1/tests/https-status' ),
					'has_rest'          => true,
					'async_direct_test' => array( WP_Site_Health::get_instance(), 'get_test_https_status' ),
				),
			),
		);

		// Conditionally include Authorization header test if the site isn't protected by Basic Auth.
		if ( ! wp_is_site_protected_by_basic_auth() ) {
			$tests['async']['authorization_header'] = array(
				'label'     => __( 'Authorization header' ),
				'test'      => rest_url( 'wp-site-health/v1/tests/authorization-header' ),
				'has_rest'  => true,
				'headers'   => array( 'Authorization' => 'Basic ' . base64_encode( 'user:pwd' ) ),
				'skip_cron' => true,
			);
		}

		// Only check for caches in production environments.
		if ( 'production' === wp_get_environment_type() ) {
			$tests['async']['page_cache'] = array(
				'label'             => __( 'Page cache' ),
				'test'              => rest_url( 'wp-site-health/v1/tests/page-cache' ),
				'has_rest'          => true,
				'async_direct_test' => array( WP_Site_Health::get_instance(), 'get_test_page_cache' ),
			);

			$tests['direct']['persistent_object_cache'] = array(
				'label' => __( 'Persistent object cache' ),
				'test'  => 'persistent_object_cache',
			);
		}

		/**
		 * Filters which site status tests are run on a site.
		 *
		 * The site health is determined by a set of tests based on best practices from
		 * both the WordPress Hosting Team and web standards in general.
		 *
		 * Some sites may not have the same requirements, for example the automatic update
		 * checks may be handled by a host, and are therefore disabled in core.
		 * Or maybe you want to introduce a new test, is caching enabled/disabled/stale for example.
		 *
		 * Tests may be added either as direct, or asynchronous ones. Any test that may require some time
		 * to complete should run asynchronously, to avoid extended loading periods within wp-admin.
		 *
		 * @since 5.2.0
		 * @since 5.6.0 Added the `async_direct_test` array key for asynchronous tests.
		 *              Added the `skip_cron` array key for all tests.
		 *
		 * @param array[] $tests {
		 *     An associative array of direct and asynchronous tests.
		 *
		 *     @type array[] $direct {
		 *         An array of direct tests.
		 *
		 *         @type array ...$identifier {
		 *             `$identifier` should be a unique identifier for the test. Plugins and themes are encouraged to
		 *             prefix test identifiers with their slug to avoid collisions between tests.
		 *
		 *             @type string   $label     The friendly label to identify the test.
		 *             @type callable $test      The callback function that runs the test and returns its result.
		 *             @type bool     $skip_cron Whether to skip this test when running as cron.
		 *         }
		 *     }
		 *     @type array[] $async {
		 *         An array of asynchronous tests.
		 *
		 *         @type array ...$identifier {
		 *             `$identifier` should be a unique identifier for the test. Plugins and themes are encouraged to
		 *             prefix test identifiers with their slug to avoid collisions between tests.
		 *
		 *             @type string   $label             The friendly label to identify the test.
		 *             @type string   $test              An admin-ajax.php action to be called to perform the test, or
		 *                                               if `$has_rest` is true, a URL to a REST API endpoint to perform
		 *                                               the test.
		 *             @type bool     $has_rest          Whether the `$test` property points to a REST API endpoint.
		 *             @type bool     $skip_cron         Whether to skip this test when running as cron.
		 *             @type callable $async_direct_test A manner of directly calling the test marked as asynchronous,
		 *                                               as the scheduled event can not authenticate, and endpoints
		 *                                               may require authentication.
		 *         }
		 *     }
		 * }
		 */
		$tests = apply_filters( 'site_status_tests', $tests );

		// Ensure that the filtered tests contain the required array keys.
		$tests = array_merge(
			array(
				'direct' => array(),
				'async'  => array(),
			),
			$tests
		);

		return $tests;
	}

	/**
	 * Adds a class to the body HTML tag.
	 *
	 * Filters the body class string for admin pages and adds our own class for easier styling.
	 *
	 * @since 5.2.0
	 *
	 * @param string $body_class The body class string.
	 * @return string The modified body class string.
	 */
	public function admin_body_class( $body_class ) {
		$screen = get_current_screen();
		if ( 'site-health' !== $screen->id ) {
			return $body_class;
		}

		$body_class .= ' site-health';

		return $body_class;
	}

	/**
	 * Initiates the WP_Cron schedule test cases.
	 *
	 * @since 5.2.0
	 */
	private function wp_schedule_test_init() {
		$this->schedules = wp_get_schedules();
		$this->get_cron_tasks();
	}

	/**
	 * Populates the list of cron events and store them to a class-wide variable.
	 *
	 * @since 5.2.0
	 */
	private function get_cron_tasks() {
		$cron_tasks = _get_cron_array();

		if ( empty( $cron_tasks ) ) {
			$this->crons = new WP_Error( 'no_tasks', __( 'No scheduled events exist on this site.' ) );
			return;
		}

		$this->crons = array();

		foreach ( $cron_tasks as $time => $cron ) {
			foreach ( $cron as $hook => $dings ) {
				foreach ( $dings as $sig => $data ) {

					$this->crons[ "$hook-$sig-$time" ] = (object) array(
						'hook'     => $hook,
						'time'     => $time,
						'sig'      => $sig,
						'args'     => $data['args'],
						'schedule' => $data['schedule'],
						'interval' => isset( $data['interval'] ) ? $data['interval'] : null,
					);

				}
			}
		}
	}

	/**
	 * Checks if any scheduled tasks have been missed.
	 *
	 * Returns a boolean value of `true` if a scheduled task has been missed and ends processing.
	 *
	 * If the list of crons is an instance of WP_Error, returns the instance instead of a boolean value.
	 *
	 * @since 5.2.0
	 *
	 * @return bool|WP_Error True if a cron was missed, false if not. WP_Error if the cron is set to that.
	 */
	public function has_missed_cron() {
		if ( is_wp_error( $this->crons ) ) {
			return $this->crons;
		}

		foreach ( $this->crons as $id => $cron ) {
			if ( ( $cron->time - time() ) < $this->timeout_missed_cron ) {
				$this->last_missed_cron = $cron->hook;
				return true;
			}
		}

		return false;
	}

	/**
	 * Checks if any scheduled tasks are late.
	 *
	 * Returns a boolean value of `true` if a scheduled task is late and ends processing.
	 *
	 * If the list of crons is an instance of WP_Error, returns the instance instead of a boolean value.
	 *
	 * @since 5.3.0
	 *
	 * @return bool|WP_Error True if a cron is late, false if not. WP_Error if the cron is set to that.
	 */
	public function has_late_cron() {
		if ( is_wp_error( $this->crons ) ) {
			return $this->crons;
		}

		foreach ( $this->crons as $id => $cron ) {
			$cron_offset = $cron->time - time();
			if (
				$cron_offset >= $this->timeout_missed_cron &&
				$cron_offset < $this->timeout_late_cron
			) {
				$this->last_late_cron = $cron->hook;
				return true;
			}
		}

		return false;
	}

	/**
	 * Checks for potential issues with plugin and theme auto-updates.
	 *
	 * Though there is no way to 100% determine if plugin and theme auto-updates are configured
	 * correctly, a few educated guesses could be made to flag any conditions that would
	 * potentially cause unexpected behaviors.
	 *
	 * @since 5.5.0
	 *
	 * @return object The test results.
	 */
	public function detect_plugin_theme_auto_update_issues() {
		$mock_plugin = (object) array(
			'id'            => 'w.org/plugins/a-fake-plugin',
			'slug'          => 'a-fake-plugin',
			'plugin'        => 'a-fake-plugin/a-fake-plugin.php',
			'new_version'   => '9.9',
			'url'           => 'https://wordpress.org/plugins/a-fake-plugin/',
			'package'       => 'https://downloads.wordpress.org/plugin/a-fake-plugin.9.9.zip',
			'icons'         => array(
				'2x' => 'https://ps.w.org/a-fake-plugin/assets/icon-256x256.png',
				'1x' => 'https://ps.w.org/a-fake-plugin/assets/icon-128x128.png',
			),
			'banners'       => array(
				'2x' => 'https://ps.w.org/a-fake-plugin/assets/banner-1544x500.png',
				'1x' => 'https://ps.w.org/a-fake-plugin/assets/banner-772x250.png',
			),
			'banners_rtl'   => array(),
			'tested'        => '5.5.0',
			'requires_php'  => '5.6.20',
			'compatibility' => new stdClass(),
		);

		$mock_theme = (object) array(
			'theme'        => 'a-fake-theme',
			'new_version'  => '9.9',
			'url'          => 'https://wordpress.org/themes/a-fake-theme/',
			'package'      => 'https://downloads.wordpress.org/theme/a-fake-theme.9.9.zip',
			'requires'     => '5.0.0',
			'requires_php' => '5.6.20',
		);

		$test_plugins_enabled = wp_is_auto_update_forced_for_item( 'plugin', true, $mock_plugin );
		$test_themes_enabled  = wp_is_auto_update_forced_for_item( 'theme', true, $mock_theme );

		$ui_enabled_for_plugins = wp_is_auto_update_enabled_for_type( 'plugin' );
		$ui_enabled_for_themes  = wp_is_auto_update_enabled_for_type( 'theme' );
		$plugin_filter_present  = has_filter( 'auto_update_plugin' );
		$theme_filter_present   = has_filter( 'auto_update_theme' );

		if ( ( ! $test_plugins_enabled && $ui_enabled_for_plugins )
			|| ( ! $test_themes_enabled && $ui_enabled_for_themes )
		) {
			return (object) array(
				'status'  => 'critical',
				'message' => __( 'Auto-updates for plugins and/or themes appear to be disabled, but settings are still set to be displayed. This could cause auto-updates to not work as expected.' ),
			);
		}

		if ( ( ! $test_plugins_enabled && $plugin_filter_present )
			&& ( ! $test_themes_enabled && $theme_filter_present )
		) {
			return (object) array(
				'status'  => 'recommended',
				'message' => __( 'Auto-updates for plugins and themes appear to be disabled. This will prevent your site from receiving new versions automatically when available.' ),
			);
		} elseif ( ! $test_plugins_enabled && $plugin_filter_present ) {
			return (object) array(
				'status'  => 'recommended',
				'message' => __( 'Auto-updates for plugins appear to be disabled. This will prevent your site from receiving new versions automatically when available.' ),
			);
		} elseif ( ! $test_themes_enabled && $theme_filter_present ) {
			return (object) array(
				'status'  => 'recommended',
				'message' => __( 'Auto-updates for themes appear to be disabled. This will prevent your site from receiving new versions automatically when available.' ),
			);
		}

		return (object) array(
			'status'  => 'good',
			'message' => __( 'There appear to be no issues with plugin and theme auto-updates.' ),
		);
	}

	/**
	 * Runs a loopback test on the site.
	 *
	 * Loopbacks are what WordPress uses to communicate with itself to start up WP_Cron, scheduled posts,
	 * make sure plugin or theme edits don't cause site failures and similar.
	 *
	 * @since 5.2.0
	 *
	 * @return object The test results.
	 */
	public function can_perform_loopback() {
		$body    = array( 'site-health' => 'loopback-test' );
		$cookies = wp_unslash( $_COOKIE );
		$timeout = 10; // 10 seconds.
		$headers = array(
			'Cache-Control' => 'no-cache',
		);
		/** This filter is documented in wp-includes/class-wp-http-streams.php */
		$sslverify = apply_filters( 'https_local_ssl_verify', false );

		// Include Basic auth in loopback requests.
		if ( isset( $_SERVER['PHP_AUTH_USER'] ) && isset( $_SERVER['PHP_AUTH_PW'] ) ) {
			$headers['Authorization'] = 'Basic ' . base64_encode( wp_unslash( $_SERVER['PHP_AUTH_USER'] ) . ':' . wp_unslash( $_SERVER['PHP_AUTH_PW'] ) );
		}

		$url = site_url( 'wp-cron.php' );

		/*
		 * A post request is used for the wp-cron.php loopback test to cause the file
		 * to finish early without triggering cron jobs. This has two benefits:
		 * - cron jobs are not triggered a second time on the site health page,
		 * - the loopback request finishes sooner providing a quicker result.
		 *
		 * Using a POST request causes the loopback to differ slightly to the standard
		 * GET request WordPress uses for wp-cron.php loopback requests but is close
		 * enough. See https://core.trac.wordpress.org/ticket/52547
		 */
		$r = wp_remote_post( $url, compact( 'body', 'cookies', 'headers', 'timeout', 'sslverify' ) );

		if ( is_wp_error( $r ) ) {
			return (object) array(
				'status'  => 'critical',
				'message' => sprintf(
					'%s<br>%s',
					__( 'The loopback request to your site failed, this means features relying on them are not currently working as expected.' ),
					sprintf(
						/* translators: 1: The WordPress error message. 2: The WordPress error code. */
						__( 'Error: %1$s (%2$s)' ),
						$r->get_error_message(),
						$r->get_error_code()
					)
				),
			);
		}

		if ( 200 !== wp_remote_retrieve_response_code( $r ) ) {
			return (object) array(
				'status'  => 'recommended',
				'message' => sprintf(
					/* translators: %d: The HTTP response code returned. */
					__( 'The loopback request returned an unexpected http status code, %d, it was not possible to determine if this will prevent features from working as expected.' ),
					wp_remote_retrieve_response_code( $r )
				),
			);
		}

		return (object) array(
			'status'  => 'good',
			'message' => __( 'The loopback request to your site completed successfully.' ),
		);
	}

	/**
	 * Creates a weekly cron event, if one does not already exist.
	 *
	 * @since 5.4.0
	 */
	public function maybe_create_scheduled_event() {
		if ( ! wp_next_scheduled( 'wp_site_health_scheduled_check' ) && ! wp_installing() ) {
			wp_schedule_event( time() + DAY_IN_SECONDS, 'weekly', 'wp_site_health_scheduled_check' );
		}
	}

	/**
	 * Runs the scheduled event to check and update the latest site health status for the website.
	 *
	 * @since 5.4.0
	 */
	public function wp_cron_scheduled_check() {
		// Bootstrap wp-admin, as WP_Cron doesn't do this for us.
		require_once trailingslashit( ABSPATH ) . 'wp-admin/includes/admin.php';

		$tests = WP_Site_Health::get_tests();

		$results = array();

		$site_status = array(
			'good'        => 0,
			'recommended' => 0,
			'critical'    => 0,
		);

		// Don't run https test on development environments.
		if ( $this->is_development_environment() ) {
			unset( $tests['async']['https_status'] );
		}

		foreach ( $tests['direct'] as $test ) {
			if ( ! empty( $test['skip_cron'] ) ) {
				continue;
			}

			if ( is_string( $test['test'] ) ) {
				$test_function = sprintf(
					'get_test_%s',
					$test['test']
				);

				if ( method_exists( $this, $test_function ) && is_callable( array( $this, $test_function ) ) ) {
					$results[] = $this->perform_test( array( $this, $test_function ) );
					continue;
				}
			}

			if ( is_callable( $test['test'] ) ) {
				$results[] = $this->perform_test( $test['test'] );
			}
		}

		foreach ( $tests['async'] as $test ) {
			if ( ! empty( $test['skip_cron'] ) ) {
				continue;
			}

			// Local endpoints may require authentication, so asynchronous tests can pass a direct test runner as well.
			if ( ! empty( $test['async_direct_test'] ) && is_callable( $test['async_direct_test'] ) ) {
				// This test is callable, do so and continue to the next asynchronous check.
				$results[] = $this->perform_test( $test['async_direct_test'] );
				continue;
			}

			if ( is_string( $test['test'] ) ) {
				// Check if this test has a REST API endpoint.
				if ( isset( $test['has_rest'] ) && $test['has_rest'] ) {
					$result_fetch = wp_remote_get(
						$test['test'],
						array(
							'body' => array(
								'_wpnonce' => wp_create_nonce( 'wp_rest' ),
							),
						)
					);
				} else {
					$result_fetch = wp_remote_post(
						admin_url( 'admin-ajax.php' ),
						array(
							'body' => array(
								'action'   => $test['test'],
								'_wpnonce' => wp_create_nonce( 'health-check-site-status' ),
							),
						)
					);
				}

				if ( ! is_wp_error( $result_fetch ) && 200 === wp_remote_retrieve_response_code( $result_fetch ) ) {
					$result = json_decode( wp_remote_retrieve_body( $result_fetch ), true );
				} else {
					$result = false;
				}

				if ( is_array( $result ) ) {
					$results[] = $result;
				} else {
					$results[] = array(
						'status' => 'recommended',
						'label'  => __( 'A test is unavailable' ),
					);
				}
			}
		}

		foreach ( $results as $result ) {
			if ( 'critical' === $result['status'] ) {
				++$site_status['critical'];
			} elseif ( 'recommended' === $result['status'] ) {
				++$site_status['recommended'];
			} else {
				++$site_status['good'];
			}
		}

		set_transient( 'health-check-site-status-result', wp_json_encode( $site_status ) );
	}

	/**
	 * Checks if the current environment type is set to 'development' or 'local'.
	 *
	 * @since 5.6.0
	 *
	 * @return bool True if it is a development environment, false if not.
	 */
	public function is_development_environment() {
		return in_array( wp_get_environment_type(), array( 'development', 'local' ), true );
	}

	/**
	 * Returns a list of headers and its verification callback to verify if page cache is enabled or not.
	 *
	 * Note: key is header name and value could be callable function to verify header value.
	 * Empty value mean existence of header detect page cache is enabled.
	 *
	 * @since 6.1.0
	 *
	 * @return array List of client caching headers and their (optional) verification callbacks.
	 */
	public function get_page_cache_headers() {

		$cache_hit_callback = static function ( $header_value ) {
			return str_contains( strtolower( $header_value ), 'hit' );
		};

		$cache_headers = array(
			'cache-control'          => static function ( $header_value ) {
				return (bool) preg_match( '/max-age=[1-9]/', $header_value );
			},
			'expires'                => static function ( $header_value ) {
				return strtotime( $header_value ) > time();
			},
			'age'                    => static function ( $header_value ) {
				return is_numeric( $header_value ) && $header_value > 0;
			},
			'last-modified'          => '',
			'etag'                   => '',
			'x-cache-enabled'        => static function ( $header_value ) {
				return 'true' === strtolower( $header_value );
			},
			'x-cache-disabled'       => static function ( $header_value ) {
				return ( 'on' !== strtolower( $header_value ) );
			},
			'x-srcache-store-status' => $cache_hit_callback,
			'x-srcache-fetch-status' => $cache_hit_callback,
		);

		/**
		 * Filters the list of cache headers supported by core.
		 *
		 * @since 6.1.0
		 *
		 * @param array $cache_headers Array of supported cache headers.
		 */
		return apply_filters( 'site_status_page_cache_supported_cache_headers', $cache_headers );
	}

	/**
	 * Checks if site has page cache enabled or not.
	 *
	 * @since 6.1.0
	 *
	 * @return WP_Error|array {
	 *     Page cache detection details or else error information.
	 *
	 *     @type bool    $advanced_cache_present        Whether a page cache plugin is present.
	 *     @type array[] $page_caching_response_headers Sets of client caching headers for the responses.
	 *     @type float[] $response_timing               Response timings.
	 * }
	 */
	private function check_for_page_caching() {

		/** This filter is documented in wp-includes/class-wp-http-streams.php */
		$sslverify = apply_filters( 'https_local_ssl_verify', false );

		$headers = array();

		/*
		 * Include basic auth in loopback requests. Note that this will only pass along basic auth when user is
		 * initiating the test. If a site requires basic auth, the test will fail when it runs in WP Cron as part of
		 * wp_site_health_scheduled_check. This logic is copied from WP_Site_Health::can_perform_loopback().
		 */
		if ( isset( $_SERVER['PHP_AUTH_USER'] ) && isset( $_SERVER['PHP_AUTH_PW'] ) ) {
			$headers['Authorization'] = 'Basic ' . base64_encode( wp_unslash( $_SERVER['PHP_AUTH_USER'] ) . ':' . wp_unslash( $_SERVER['PHP_AUTH_PW'] ) );
		}

		$caching_headers               = $this->get_page_cache_headers();
		$page_caching_response_headers = array();
		$response_timing               = array();

		for ( $i = 1; $i <= 3; $i++ ) {
			$start_time    = microtime( true );
			$http_response = wp_remote_get( home_url( '/' ), compact( 'sslverify', 'headers' ) );
			$end_time      = microtime( true );

			if ( is_wp_error( $http_response ) ) {
				return $http_response;
			}
			if ( wp_remote_retrieve_response_code( $http_response ) !== 200 ) {
				return new WP_Error(
					'http_' . wp_remote_retrieve_response_code( $http_response ),
					wp_remote_retrieve_response_message( $http_response )
				);
			}

			$response_headers = array();

			foreach ( $caching_headers as $header => $callback ) {
				$header_values = wp_remote_retrieve_header( $http_response, $header );
				if ( empty( $header_values ) ) {
					continue;
				}
				$header_values = (array) $header_values;
				if ( empty( $callback ) || ( is_callable( $callback ) && count( array_filter( $header_values, $callback ) ) > 0 ) ) {
					$response_headers[ $header ] = $header_values;
				}
			}

			$page_caching_response_headers[] = $response_headers;
			$response_timing[]               = ( $end_time - $start_time ) * 1000;
		}

		return array(
			'advanced_cache_present'        => (
				file_exists( WP_CONTENT_DIR . '/advanced-cache.php' )
				&&
				( defined( 'WP_CACHE' ) && WP_CACHE )
				&&
				/** This filter is documented in wp-settings.php */
				apply_filters( 'enable_loading_advanced_cache_dropin', true )
			),
			'page_caching_response_headers' => $page_caching_response_headers,
			'response_timing'               => $response_timing,
		);
	}

	/**
	 * Gets page cache details.
	 *
	 * @since 6.1.0
	 *
	 * @return WP_Error|array {
	 *     Page cache detail or else a WP_Error if unable to determine.
	 *
	 *     @type string   $status                 Page cache status. Good, Recommended or Critical.
	 *     @type bool     $advanced_cache_present Whether page cache plugin is available or not.
	 *     @type string[] $headers                Client caching response headers detected.
	 *     @type float    $response_time          Response time of site.
	 * }
	 */
	private function get_page_cache_detail() {
		$page_cache_detail = $this->check_for_page_caching();
		if ( is_wp_error( $page_cache_detail ) ) {
			return $page_cache_detail;
		}

		// Use the median server response time.
		$response_timings = $page_cache_detail['response_timing'];
		rsort( $response_timings );
		$page_speed = $response_timings[ floor( count( $response_timings ) / 2 ) ];

		// Obtain unique set of all client caching response headers.
		$headers = array();
		foreach ( $page_cache_detail['page_caching_response_headers'] as $page_caching_response_headers ) {
			$headers = array_merge( $headers, array_keys( $page_caching_response_headers ) );
		}
		$headers = array_unique( $headers );

		// Page cache is detected if there are response headers or a page cache plugin is present.
		$has_page_caching = ( count( $headers ) > 0 || $page_cache_detail['advanced_cache_present'] );

		if ( $page_speed && $page_speed < $this->get_good_response_time_threshold() ) {
			$result = $has_page_caching ? 'good' : 'recommended';
		} else {
			$result = 'critical';
		}

		return array(
			'status'                 => $result,
			'advanced_cache_present' => $page_cache_detail['advanced_cache_present'],
			'headers'                => $headers,
			'response_time'          => $page_speed,
		);
	}

	/**
	 * Gets the threshold below which a response time is considered good.
	 *
	 * @since 6.1.0
	 *
	 * @return int Threshold in milliseconds.
	 */
	private function get_good_response_time_threshold() {
		/**
		 * Filters the threshold below which a response time is considered good.
		 *
		 * The default is based on https://web.dev/time-to-first-byte/.
		 *
		 * @param int $threshold Threshold in milliseconds. Default 600.
		 *
		 * @since 6.1.0
		 */
		return (int) apply_filters( 'site_status_good_response_time_threshold', 600 );
	}

	/**
	 * Determines whether to suggest using a persistent object cache.
	 *
	 * @since 6.1.0
	 *
	 * @global wpdb $wpdb WordPress database abstraction object.
	 *
	 * @return bool Whether to suggest using a persistent object cache.
	 */
	public function should_suggest_persistent_object_cache() {
		global $wpdb;

		/**
		 * Filters whether to suggest use of a persistent object cache and bypass default threshold checks.
		 *
		 * Using this filter allows to override the default logic, effectively short-circuiting the method.
		 *
		 * @since 6.1.0
		 *
		 * @param bool|null $suggest Boolean to short-circuit, for whether to suggest using a persistent object cache.
		 *                           Default null.
		 */
		$short_circuit = apply_filters( 'site_status_should_suggest_persistent_object_cache', null );
		if ( is_bool( $short_circuit ) ) {
			return $short_circuit;
		}

		if ( is_multisite() ) {
			return true;
		}

		/**
		 * Filters the thresholds used to determine whether to suggest the use of a persistent object cache.
		 *
		 * @since 6.1.0
		 *
		 * @param int[] $thresholds The list of threshold numbers keyed by threshold name.
		 */
		$thresholds = apply_filters(
			'site_status_persistent_object_cache_thresholds',
			array(
				'alloptions_count' => 500,
				'alloptions_bytes' => 100000,
				'comments_count'   => 1000,
				'options_count'    => 1000,
				'posts_count'      => 1000,
				'terms_count'      => 1000,
				'users_count'      => 1000,
			)
		);

		$alloptions = wp_load_alloptions();

		if ( $thresholds['alloptions_count'] < count( $alloptions ) ) {
			return true;
		}

		if ( $thresholds['alloptions_bytes'] < strlen( serialize( $alloptions ) ) ) {
			return true;
		}

		$table_names = implode( "','", array( $wpdb->comments, $wpdb->options, $wpdb->posts, $wpdb->terms, $wpdb->users ) );

		// With InnoDB the `TABLE_ROWS` are estimates, which are accurate enough and faster to retrieve than individual `COUNT()` queries.
		$results = $wpdb->get_results(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- This query cannot use interpolation.
				"SELECT TABLE_NAME AS 'table', TABLE_ROWS AS 'rows', SUM(data_length + index_length) as 'bytes' FROM information_schema.TABLES WHERE TABLE_SCHEMA = %s AND TABLE_NAME IN ('$table_names') GROUP BY TABLE_NAME;",
				DB_NAME
			),
			OBJECT_K
		);

		$threshold_map = array(
			'comments_count' => $wpdb->comments,
			'options_count'  => $wpdb->options,
			'posts_count'    => $wpdb->posts,
			'terms_count'    => $wpdb->terms,
			'users_count'    => $wpdb->users,
		);

		foreach ( $threshold_map as $threshold => $table ) {
			if ( $thresholds[ $threshold ] <= $results[ $table ]->rows ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Returns a list of available persistent object cache services.
	 *
	 * @since 6.1.0
	 *
	 * @return string[] The list of available persistent object cache services.
	 */
	private function available_object_cache_services() {
		$extensions = array_map(
			'extension_loaded',
			array(
				'APCu'      => 'apcu',
				'Redis'     => 'redis',
				'Relay'     => 'relay',
				'Memcache'  => 'memcache',
				'Memcached' => 'memcached',
			)
		);

		$services = array_keys( array_filter( $extensions ) );

		/**
		 * Filters the persistent object cache services available to the user.
		 *
		 * This can be useful to hide or add services not included in the defaults.
		 *
		 * @since 6.1.0
		 *
		 * @param string[] $services The list of available persistent object cache services.
		 */
		return apply_filters( 'site_status_available_object_cache_services', $services );
	}
}
class-wp-filesystem-ftpsockets.php000060400000044057151327343360013370 0ustar00<?php
/**
 * WordPress FTP Sockets Filesystem.
 *
 * @package WordPress
 * @subpackage Filesystem
 */

/**
 * WordPress Filesystem Class for implementing FTP Sockets.
 *
 * @since 2.5.0
 *
 * @see WP_Filesystem_Base
 */
class WP_Filesystem_ftpsockets extends WP_Filesystem_Base {

	/**
	 * @since 2.5.0
	 * @var ftp
	 */
	public $ftp;

	/**
	 * Constructor.
	 *
	 * @since 2.5.0
	 *
	 * @param array $opt
	 */
	public function __construct( $opt = '' ) {
		$this->method = 'ftpsockets';
		$this->errors = new WP_Error();

		// Check if possible to use ftp functions.
		if ( ! require_once ABSPATH . 'wp-admin/includes/class-ftp.php' ) {
			return;
		}

		$this->ftp = new ftp();

		if ( empty( $opt['port'] ) ) {
			$this->options['port'] = 21;
		} else {
			$this->options['port'] = (int) $opt['port'];
		}

		if ( empty( $opt['hostname'] ) ) {
			$this->errors->add( 'empty_hostname', __( 'FTP hostname is required' ) );
		} else {
			$this->options['hostname'] = $opt['hostname'];
		}

		// Check if the options provided are OK.
		if ( empty( $opt['username'] ) ) {
			$this->errors->add( 'empty_username', __( 'FTP username is required' ) );
		} else {
			$this->options['username'] = $opt['username'];
		}

		if ( empty( $opt['password'] ) ) {
			$this->errors->add( 'empty_password', __( 'FTP password is required' ) );
		} else {
			$this->options['password'] = $opt['password'];
		}
	}

	/**
	 * Connects filesystem.
	 *
	 * @since 2.5.0
	 *
	 * @return bool True on success, false on failure.
	 */
	public function connect() {
		if ( ! $this->ftp ) {
			return false;
		}

		$this->ftp->setTimeout( FS_CONNECT_TIMEOUT );

		if ( ! $this->ftp->SetServer( $this->options['hostname'], $this->options['port'] ) ) {
			$this->errors->add(
				'connect',
				sprintf(
					/* translators: %s: hostname:port */
					__( 'Failed to connect to FTP Server %s' ),
					$this->options['hostname'] . ':' . $this->options['port']
				)
			);

			return false;
		}

		if ( ! $this->ftp->connect() ) {
			$this->errors->add(
				'connect',
				sprintf(
					/* translators: %s: hostname:port */
					__( 'Failed to connect to FTP Server %s' ),
					$this->options['hostname'] . ':' . $this->options['port']
				)
			);

			return false;
		}

		if ( ! $this->ftp->login( $this->options['username'], $this->options['password'] ) ) {
			$this->errors->add(
				'auth',
				sprintf(
					/* translators: %s: Username. */
					__( 'Username/Password incorrect for %s' ),
					$this->options['username']
				)
			);

			return false;
		}

		$this->ftp->SetType( FTP_BINARY );
		$this->ftp->Passive( true );
		$this->ftp->setTimeout( FS_TIMEOUT );

		return true;
	}

	/**
	 * Reads entire file into a string.
	 *
	 * @since 2.5.0
	 *
	 * @param string $file Name of the file to read.
	 * @return string|false Read data on success, false if no temporary file could be opened,
	 *                      or if the file couldn't be retrieved.
	 */
	public function get_contents( $file ) {
		if ( ! $this->exists( $file ) ) {
			return false;
		}

		$tempfile   = wp_tempnam( $file );
		$temphandle = fopen( $tempfile, 'w+' );

		if ( ! $temphandle ) {
			unlink( $tempfile );
			return false;
		}

		mbstring_binary_safe_encoding();

		if ( ! $this->ftp->fget( $temphandle, $file ) ) {
			fclose( $temphandle );
			unlink( $tempfile );

			reset_mbstring_encoding();

			return ''; // Blank document. File does exist, it's just blank.
		}

		reset_mbstring_encoding();

		fseek( $temphandle, 0 ); // Skip back to the start of the file being written to.
		$contents = '';

		while ( ! feof( $temphandle ) ) {
			$contents .= fread( $temphandle, 8 * KB_IN_BYTES );
		}

		fclose( $temphandle );
		unlink( $tempfile );

		return $contents;
	}

	/**
	 * Reads entire file into an array.
	 *
	 * @since 2.5.0
	 *
	 * @param string $file Path to the file.
	 * @return array|false File contents in an array on success, false on failure.
	 */
	public function get_contents_array( $file ) {
		return explode( "\n", $this->get_contents( $file ) );
	}

	/**
	 * Writes a string to a file.
	 *
	 * @since 2.5.0
	 *
	 * @param string    $file     Remote path to the file where to write the data.
	 * @param string    $contents The data to write.
	 * @param int|false $mode     Optional. The file permissions as octal number, usually 0644.
	 *                            Default false.
	 * @return bool True on success, false on failure.
	 */
	public function put_contents( $file, $contents, $mode = false ) {
		$tempfile   = wp_tempnam( $file );
		$temphandle = @fopen( $tempfile, 'w+' );

		if ( ! $temphandle ) {
			unlink( $tempfile );
			return false;
		}

		// The FTP class uses string functions internally during file download/upload.
		mbstring_binary_safe_encoding();

		$bytes_written = fwrite( $temphandle, $contents );

		if ( false === $bytes_written || strlen( $contents ) !== $bytes_written ) {
			fclose( $temphandle );
			unlink( $tempfile );

			reset_mbstring_encoding();

			return false;
		}

		fseek( $temphandle, 0 ); // Skip back to the start of the file being written to.

		$ret = $this->ftp->fput( $file, $temphandle );

		reset_mbstring_encoding();

		fclose( $temphandle );
		unlink( $tempfile );

		$this->chmod( $file, $mode );

		return $ret;
	}

	/**
	 * Gets the current working directory.
	 *
	 * @since 2.5.0
	 *
	 * @return string|false The current working directory on success, false on failure.
	 */
	public function cwd() {
		$cwd = $this->ftp->pwd();

		if ( $cwd ) {
			$cwd = trailingslashit( $cwd );
		}

		return $cwd;
	}

	/**
	 * Changes current directory.
	 *
	 * @since 2.5.0
	 *
	 * @param string $dir The new current directory.
	 * @return bool True on success, false on failure.
	 */
	public function chdir( $dir ) {
		return $this->ftp->chdir( $dir );
	}

	/**
	 * Changes filesystem permissions.
	 *
	 * @since 2.5.0
	 *
	 * @param string    $file      Path to the file.
	 * @param int|false $mode      Optional. The permissions as octal number, usually 0644 for files,
	 *                             0755 for directories. Default false.
	 * @param bool      $recursive Optional. If set to true, changes file permissions recursively.
	 *                             Default false.
	 * @return bool True on success, false on failure.
	 */
	public function chmod( $file, $mode = false, $recursive = false ) {
		if ( ! $mode ) {
			if ( $this->is_file( $file ) ) {
				$mode = FS_CHMOD_FILE;
			} elseif ( $this->is_dir( $file ) ) {
				$mode = FS_CHMOD_DIR;
			} else {
				return false;
			}
		}

		// chmod any sub-objects if recursive.
		if ( $recursive && $this->is_dir( $file ) ) {
			$filelist = $this->dirlist( $file );

			foreach ( (array) $filelist as $filename => $filemeta ) {
				$this->chmod( $file . '/' . $filename, $mode, $recursive );
			}
		}

		// chmod the file or directory.
		return $this->ftp->chmod( $file, $mode );
	}

	/**
	 * Gets the file owner.
	 *
	 * @since 2.5.0
	 *
	 * @param string $file Path to the file.
	 * @return string|false Username of the owner on success, false on failure.
	 */
	public function owner( $file ) {
		$dir = $this->dirlist( $file );

		return $dir[ $file ]['owner'];
	}

	/**
	 * Gets the permissions of the specified file or filepath in their octal format.
	 *
	 * @since 2.5.0
	 *
	 * @param string $file Path to the file.
	 * @return string Mode of the file (the last 3 digits).
	 */
	public function getchmod( $file ) {
		$dir = $this->dirlist( $file );

		return $dir[ $file ]['permsn'];
	}

	/**
	 * Gets the file's group.
	 *
	 * @since 2.5.0
	 *
	 * @param string $file Path to the file.
	 * @return string|false The group on success, false on failure.
	 */
	public function group( $file ) {
		$dir = $this->dirlist( $file );

		return $dir[ $file ]['group'];
	}

	/**
	 * Copies a file.
	 *
	 * @since 2.5.0
	 *
	 * @param string    $source      Path to the source file.
	 * @param string    $destination Path to the destination file.
	 * @param bool      $overwrite   Optional. Whether to overwrite the destination file if it exists.
	 *                               Default false.
	 * @param int|false $mode        Optional. The permissions as octal number, usually 0644 for files,
	 *                               0755 for dirs. Default false.
	 * @return bool True on success, false on failure.
	 */
	public function copy( $source, $destination, $overwrite = false, $mode = false ) {
		if ( ! $overwrite && $this->exists( $destination ) ) {
			return false;
		}

		$content = $this->get_contents( $source );

		if ( false === $content ) {
			return false;
		}

		return $this->put_contents( $destination, $content, $mode );
	}

	/**
	 * Moves a file or directory.
	 *
	 * After moving files or directories, OPcache will need to be invalidated.
	 *
	 * If moving a directory fails, `copy_dir()` can be used for a recursive copy.
	 *
	 * Use `move_dir()` for moving directories with OPcache invalidation and a
	 * fallback to `copy_dir()`.
	 *
	 * @since 2.5.0
	 *
	 * @param string $source      Path to the source file or directory.
	 * @param string $destination Path to the destination file or directory.
	 * @param bool   $overwrite   Optional. Whether to overwrite the destination if it exists.
	 *                            Default false.
	 * @return bool True on success, false on failure.
	 */
	public function move( $source, $destination, $overwrite = false ) {
		return $this->ftp->rename( $source, $destination );
	}

	/**
	 * Deletes a file or directory.
	 *
	 * @since 2.5.0
	 *
	 * @param string       $file      Path to the file or directory.
	 * @param bool         $recursive Optional. If set to true, deletes files and folders recursively.
	 *                                Default false.
	 * @param string|false $type      Type of resource. 'f' for file, 'd' for directory.
	 *                                Default false.
	 * @return bool True on success, false on failure.
	 */
	public function delete( $file, $recursive = false, $type = false ) {
		if ( empty( $file ) ) {
			return false;
		}

		if ( 'f' === $type || $this->is_file( $file ) ) {
			return $this->ftp->delete( $file );
		}

		if ( ! $recursive ) {
			return $this->ftp->rmdir( $file );
		}

		return $this->ftp->mdel( $file );
	}

	/**
	 * Checks if a file or directory exists.
	 *
	 * @since 2.5.0
	 * @since 6.3.0 Returns false for an empty path.
	 *
	 * @param string $path Path to file or directory.
	 * @return bool Whether $path exists or not.
	 */
	public function exists( $path ) {
		/*
		 * Check for empty path. If ftp::nlist() receives an empty path,
		 * it checks the current working directory and may return true.
		 *
		 * See https://core.trac.wordpress.org/ticket/33058.
		 */
		if ( '' === $path ) {
			return false;
		}

		$list = $this->ftp->nlist( $path );

		if ( empty( $list ) && $this->is_dir( $path ) ) {
			return true; // File is an empty directory.
		}

		return ! empty( $list ); // Empty list = no file, so invert.
		// Return $this->ftp->is_exists($file); has issues with ABOR+426 responses on the ncFTPd server.
	}

	/**
	 * Checks if resource is a file.
	 *
	 * @since 2.5.0
	 *
	 * @param string $file File path.
	 * @return bool Whether $file is a file.
	 */
	public function is_file( $file ) {
		if ( $this->is_dir( $file ) ) {
			return false;
		}

		if ( $this->exists( $file ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Checks if resource is a directory.
	 *
	 * @since 2.5.0
	 *
	 * @param string $path Directory path.
	 * @return bool Whether $path is a directory.
	 */
	public function is_dir( $path ) {
		$cwd = $this->cwd();

		if ( $this->chdir( $path ) ) {
			$this->chdir( $cwd );
			return true;
		}

		return false;
	}

	/**
	 * Checks if a file is readable.
	 *
	 * @since 2.5.0
	 *
	 * @param string $file Path to file.
	 * @return bool Whether $file is readable.
	 */
	public function is_readable( $file ) {
		return true;
	}

	/**
	 * Checks if a file or directory is writable.
	 *
	 * @since 2.5.0
	 *
	 * @param string $path Path to file or directory.
	 * @return bool Whether $path is writable.
	 */
	public function is_writable( $path ) {
		return true;
	}

	/**
	 * Gets the file's last access time.
	 *
	 * @since 2.5.0
	 *
	 * @param string $file Path to file.
	 * @return int|false Unix timestamp representing last access time, false on failure.
	 */
	public function atime( $file ) {
		return false;
	}

	/**
	 * Gets the file modification time.
	 *
	 * @since 2.5.0
	 *
	 * @param string $file Path to file.
	 * @return int|false Unix timestamp representing modification time, false on failure.
	 */
	public function mtime( $file ) {
		return $this->ftp->mdtm( $file );
	}

	/**
	 * Gets the file size (in bytes).
	 *
	 * @since 2.5.0
	 *
	 * @param string $file Path to file.
	 * @return int|false Size of the file in bytes on success, false on failure.
	 */
	public function size( $file ) {
		return $this->ftp->filesize( $file );
	}

	/**
	 * Sets the access and modification times of a file.
	 *
	 * Note: If $file doesn't exist, it will be created.
	 *
	 * @since 2.5.0
	 *
	 * @param string $file  Path to file.
	 * @param int    $time  Optional. Modified time to set for file.
	 *                      Default 0.
	 * @param int    $atime Optional. Access time to set for file.
	 *                      Default 0.
	 * @return bool True on success, false on failure.
	 */
	public function touch( $file, $time = 0, $atime = 0 ) {
		return false;
	}

	/**
	 * Creates a directory.
	 *
	 * @since 2.5.0
	 *
	 * @param string           $path  Path for new directory.
	 * @param int|false        $chmod Optional. The permissions as octal number (or false to skip chmod).
	 *                                Default false.
	 * @param string|int|false $chown Optional. A user name or number (or false to skip chown).
	 *                                Default false.
	 * @param string|int|false $chgrp Optional. A group name or number (or false to skip chgrp).
	 *                                Default false.
	 * @return bool True on success, false on failure.
	 */
	public function mkdir( $path, $chmod = false, $chown = false, $chgrp = false ) {
		$path = untrailingslashit( $path );

		if ( empty( $path ) ) {
			return false;
		}

		if ( ! $this->ftp->mkdir( $path ) ) {
			return false;
		}

		if ( ! $chmod ) {
			$chmod = FS_CHMOD_DIR;
		}

		$this->chmod( $path, $chmod );

		return true;
	}

	/**
	 * Deletes a directory.
	 *
	 * @since 2.5.0
	 *
	 * @param string $path      Path to directory.
	 * @param bool   $recursive Optional. Whether to recursively remove files/directories.
	 *                          Default false.
	 * @return bool True on success, false on failure.
	 */
	public function rmdir( $path, $recursive = false ) {
		return $this->delete( $path, $recursive );
	}

	/**
	 * Gets details for files in a directory or a specific file.
	 *
	 * @since 2.5.0
	 *
	 * @param string $path           Path to directory or file.
	 * @param bool   $include_hidden Optional. Whether to include details of hidden ("." prefixed) files.
	 *                               Default true.
	 * @param bool   $recursive      Optional. Whether to recursively include file details in nested directories.
	 *                               Default false.
	 * @return array|false {
	 *     Array of arrays containing file information. False if unable to list directory contents.
	 *
	 *     @type array ...$0 {
	 *         Array of file information. Note that some elements may not be available on all filesystems.
	 *
	 *         @type string           $name        Name of the file or directory.
	 *         @type string           $perms       *nix representation of permissions.
	 *         @type string           $permsn      Octal representation of permissions.
	 *         @type int|string|false $number      File number. May be a numeric string. False if not available.
	 *         @type string|false     $owner       Owner name or ID, or false if not available.
	 *         @type string|false     $group       File permissions group, or false if not available.
	 *         @type int|string|false $size        Size of file in bytes. May be a numeric string.
	 *                                             False if not available.
	 *         @type int|string|false $lastmodunix Last modified unix timestamp. May be a numeric string.
	 *                                             False if not available.
	 *         @type string|false     $lastmod     Last modified month (3 letters) and day (without leading 0), or
	 *                                             false if not available.
	 *         @type string|false     $time        Last modified time, or false if not available.
	 *         @type string           $type        Type of resource. 'f' for file, 'd' for directory, 'l' for link.
	 *         @type array|false      $files       If a directory and `$recursive` is true, contains another array of
	 *                                             files. False if unable to list directory contents.
	 *     }
	 * }
	 */
	public function dirlist( $path = '.', $include_hidden = true, $recursive = false ) {
		if ( $this->is_file( $path ) ) {
			$limit_file = basename( $path );
			$path       = dirname( $path ) . '/';
		} else {
			$limit_file = false;
		}

		mbstring_binary_safe_encoding();

		$list = $this->ftp->dirlist( $path );

		if ( empty( $list ) && ! $this->exists( $path ) ) {

			reset_mbstring_encoding();

			return false;
		}

		$path = trailingslashit( $path );
		$ret  = array();

		foreach ( $list as $struc ) {

			if ( '.' === $struc['name'] || '..' === $struc['name'] ) {
				continue;
			}

			if ( ! $include_hidden && '.' === $struc['name'][0] ) {
				continue;
			}

			if ( $limit_file && $struc['name'] !== $limit_file ) {
				continue;
			}

			if ( 'd' === $struc['type'] ) {
				if ( $recursive ) {
					$struc['files'] = $this->dirlist( $path . $struc['name'], $include_hidden, $recursive );
				} else {
					$struc['files'] = array();
				}
			}

			// Replace symlinks formatted as "source -> target" with just the source name.
			if ( $struc['islink'] ) {
				$struc['name'] = preg_replace( '/(\s*->\s*.*)$/', '', $struc['name'] );
			}

			// Add the octal representation of the file permissions.
			$struc['permsn'] = $this->getnumchmodfromh( $struc['perms'] );

			$ret[ $struc['name'] ] = $struc;
		}

		reset_mbstring_encoding();

		return $ret;
	}

	/**
	 * Destructor.
	 *
	 * @since 2.5.0
	 */
	public function __destruct() {
		$this->ftp->quit();
	}
}
class-wp-privacy-requests-table.php000064400000034073151327343360013435 0ustar00<?php
/**
 * List Table API: WP_Privacy_Requests_Table class
 *
 * @package WordPress
 * @subpackage Administration
 * @since 4.9.6
 */

abstract class WP_Privacy_Requests_Table extends WP_List_Table {

	/**
	 * Action name for the requests this table will work with. Classes
	 * which inherit from WP_Privacy_Requests_Table should define this.
	 *
	 * Example: 'export_personal_data'.
	 *
	 * @since 4.9.6
	 *
	 * @var string $request_type Name of action.
	 */
	protected $request_type = 'INVALID';

	/**
	 * Post type to be used.
	 *
	 * @since 4.9.6
	 *
	 * @var string $post_type The post type.
	 */
	protected $post_type = 'INVALID';

	/**
	 * Gets columns to show in the list table.
	 *
	 * @since 4.9.6
	 *
	 * @return string[] Array of column titles keyed by their column name.
	 */
	public function get_columns() {
		$columns = array(
			'cb'                => '<input type="checkbox" />',
			'email'             => __( 'Requester' ),
			'status'            => __( 'Status' ),
			'created_timestamp' => __( 'Requested' ),
			'next_steps'        => __( 'Next steps' ),
		);
		return $columns;
	}

	/**
	 * Normalizes the admin URL to the current page (by request_type).
	 *
	 * @since 5.3.0
	 *
	 * @return string URL to the current admin page.
	 */
	protected function get_admin_url() {
		$pagenow = str_replace( '_', '-', $this->request_type );

		if ( 'remove-personal-data' === $pagenow ) {
			$pagenow = 'erase-personal-data';
		}

		return admin_url( $pagenow . '.php' );
	}

	/**
	 * Gets a list of sortable columns.
	 *
	 * @since 4.9.6
	 *
	 * @return array Default sortable columns.
	 */
	protected function get_sortable_columns() {
		/*
		 * The initial sorting is by 'Requested' (post_date) and descending.
		 * With initial sorting, the first click on 'Requested' should be ascending.
		 * With 'Requester' sorting active, the next click on 'Requested' should be descending.
		 */
		$desc_first = isset( $_GET['orderby'] );

		return array(
			'email'             => 'requester',
			'created_timestamp' => array( 'requested', $desc_first ),
		);
	}

	/**
	 * Returns the default primary column.
	 *
	 * @since 4.9.6
	 *
	 * @return string Default primary column name.
	 */
	protected function get_default_primary_column_name() {
		return 'email';
	}

	/**
	 * Counts the number of requests for each status.
	 *
	 * @since 4.9.6
	 *
	 * @global wpdb $wpdb WordPress database abstraction object.
	 *
	 * @return object Number of posts for each status.
	 */
	protected function get_request_counts() {
		global $wpdb;

		$cache_key = $this->post_type . '-' . $this->request_type;
		$counts    = wp_cache_get( $cache_key, 'counts' );

		if ( false !== $counts ) {
			return $counts;
		}

		$results = (array) $wpdb->get_results(
			$wpdb->prepare(
				"SELECT post_status, COUNT( * ) AS num_posts
				FROM {$wpdb->posts}
				WHERE post_type = %s
				AND post_name = %s
				GROUP BY post_status",
				$this->post_type,
				$this->request_type
			),
			ARRAY_A
		);

		$counts = array_fill_keys( get_post_stati(), 0 );

		foreach ( $results as $row ) {
			$counts[ $row['post_status'] ] = $row['num_posts'];
		}

		$counts = (object) $counts;
		wp_cache_set( $cache_key, $counts, 'counts' );

		return $counts;
	}

	/**
	 * Gets an associative array ( id => link ) with the list of views available on this table.
	 *
	 * @since 4.9.6
	 *
	 * @return string[] An array of HTML links keyed by their view.
	 */
	protected function get_views() {
		$current_status = isset( $_REQUEST['filter-status'] ) ? sanitize_text_field( $_REQUEST['filter-status'] ) : '';
		$statuses       = _wp_privacy_statuses();
		$views          = array();
		$counts         = $this->get_request_counts();
		$total_requests = absint( array_sum( (array) $counts ) );

		// Normalized admin URL.
		$admin_url = $this->get_admin_url();

		$status_label = sprintf(
			/* translators: %s: Number of requests. */
			_nx(
				'All <span class="count">(%s)</span>',
				'All <span class="count">(%s)</span>',
				$total_requests,
				'requests'
			),
			number_format_i18n( $total_requests )
		);

		$views['all'] = array(
			'url'     => esc_url( $admin_url ),
			'label'   => $status_label,
			'current' => empty( $current_status ),
		);

		foreach ( $statuses as $status => $label ) {
			$post_status = get_post_status_object( $status );
			if ( ! $post_status ) {
				continue;
			}

			$total_status_requests = absint( $counts->{$status} );

			if ( ! $total_status_requests ) {
				continue;
			}

			$status_label = sprintf(
				translate_nooped_plural( $post_status->label_count, $total_status_requests ),
				number_format_i18n( $total_status_requests )
			);

			$status_link = add_query_arg( 'filter-status', $status, $admin_url );

			$views[ $status ] = array(
				'url'     => esc_url( $status_link ),
				'label'   => $status_label,
				'current' => $status === $current_status,
			);
		}

		return $this->get_views_links( $views );
	}

	/**
	 * Gets bulk actions.
	 *
	 * @since 4.9.6
	 *
	 * @return array Array of bulk action labels keyed by their action.
	 */
	protected function get_bulk_actions() {
		return array(
			'resend'   => __( 'Resend confirmation requests' ),
			'complete' => __( 'Mark requests as completed' ),
			'delete'   => __( 'Delete requests' ),
		);
	}

	/**
	 * Process bulk actions.
	 *
	 * @since 4.9.6
	 * @since 5.6.0 Added support for the `complete` action.
	 */
	public function process_bulk_action() {
		$action      = $this->current_action();
		$request_ids = isset( $_REQUEST['request_id'] ) ? wp_parse_id_list( wp_unslash( $_REQUEST['request_id'] ) ) : array();

		if ( empty( $request_ids ) ) {
			return;
		}

		$count    = 0;
		$failures = 0;

		check_admin_referer( 'bulk-privacy_requests' );

		switch ( $action ) {
			case 'resend':
				foreach ( $request_ids as $request_id ) {
					$resend = _wp_privacy_resend_request( $request_id );

					if ( $resend && ! is_wp_error( $resend ) ) {
						++$count;
					} else {
						++$failures;
					}
				}

				if ( $failures ) {
					add_settings_error(
						'bulk_action',
						'bulk_action',
						sprintf(
							/* translators: %d: Number of requests. */
							_n(
								'%d confirmation request failed to resend.',
								'%d confirmation requests failed to resend.',
								$failures
							),
							$failures
						),
						'error'
					);
				}

				if ( $count ) {
					add_settings_error(
						'bulk_action',
						'bulk_action',
						sprintf(
							/* translators: %d: Number of requests. */
							_n(
								'%d confirmation request re-sent successfully.',
								'%d confirmation requests re-sent successfully.',
								$count
							),
							$count
						),
						'success'
					);
				}

				break;

			case 'complete':
				foreach ( $request_ids as $request_id ) {
					$result = _wp_privacy_completed_request( $request_id );

					if ( $result && ! is_wp_error( $result ) ) {
						++$count;
					}
				}

				add_settings_error(
					'bulk_action',
					'bulk_action',
					sprintf(
						/* translators: %d: Number of requests. */
						_n(
							'%d request marked as complete.',
							'%d requests marked as complete.',
							$count
						),
						$count
					),
					'success'
				);
				break;

			case 'delete':
				foreach ( $request_ids as $request_id ) {
					if ( wp_delete_post( $request_id, true ) ) {
						++$count;
					} else {
						++$failures;
					}
				}

				if ( $failures ) {
					add_settings_error(
						'bulk_action',
						'bulk_action',
						sprintf(
							/* translators: %d: Number of requests. */
							_n(
								'%d request failed to delete.',
								'%d requests failed to delete.',
								$failures
							),
							$failures
						),
						'error'
					);
				}

				if ( $count ) {
					add_settings_error(
						'bulk_action',
						'bulk_action',
						sprintf(
							/* translators: %d: Number of requests. */
							_n(
								'%d request deleted successfully.',
								'%d requests deleted successfully.',
								$count
							),
							$count
						),
						'success'
					);
				}

				break;
		}
	}

	/**
	 * Prepares items to output.
	 *
	 * @since 4.9.6
	 * @since 5.1.0 Added support for column sorting.
	 */
	public function prepare_items() {
		$this->items    = array();
		$posts_per_page = $this->get_items_per_page( $this->request_type . '_requests_per_page' );
		$args           = array(
			'post_type'      => $this->post_type,
			'post_name__in'  => array( $this->request_type ),
			'posts_per_page' => $posts_per_page,
			'offset'         => isset( $_REQUEST['paged'] ) ? max( 0, absint( $_REQUEST['paged'] ) - 1 ) * $posts_per_page : 0,
			'post_status'    => 'any',
			's'              => isset( $_REQUEST['s'] ) ? sanitize_text_field( $_REQUEST['s'] ) : '',
		);

		$orderby_mapping = array(
			'requester' => 'post_title',
			'requested' => 'post_date',
		);

		if ( isset( $_REQUEST['orderby'] ) && isset( $orderby_mapping[ $_REQUEST['orderby'] ] ) ) {
			$args['orderby'] = $orderby_mapping[ $_REQUEST['orderby'] ];
		}

		if ( isset( $_REQUEST['order'] ) && in_array( strtoupper( $_REQUEST['order'] ), array( 'ASC', 'DESC' ), true ) ) {
			$args['order'] = strtoupper( $_REQUEST['order'] );
		}

		if ( ! empty( $_REQUEST['filter-status'] ) ) {
			$filter_status       = isset( $_REQUEST['filter-status'] ) ? sanitize_text_field( $_REQUEST['filter-status'] ) : '';
			$args['post_status'] = $filter_status;
		}

		$requests_query = new WP_Query( $args );
		$requests       = $requests_query->posts;

		foreach ( $requests as $request ) {
			$this->items[] = wp_get_user_request( $request->ID );
		}

		$this->items = array_filter( $this->items );

		$this->set_pagination_args(
			array(
				'total_items' => $requests_query->found_posts,
				'per_page'    => $posts_per_page,
			)
		);
	}

	/**
	 * Returns the markup for the Checkbox column.
	 *
	 * @since 4.9.6
	 *
	 * @param WP_User_Request $item Item being shown.
	 * @return string Checkbox column markup.
	 */
	public function column_cb( $item ) {
		return sprintf(
			'<input type="checkbox" name="request_id[]" id="requester_%1$s" value="%1$s" />' .
			'<label for="requester_%1$s"><span class="screen-reader-text">%2$s</span></label><span class="spinner"></span>',
			esc_attr( $item->ID ),
			/* translators: Hidden accessibility text. %s: Email address. */
			sprintf( __( 'Select %s' ), $item->email )
		);
	}

	/**
	 * Status column.
	 *
	 * @since 4.9.6
	 *
	 * @param WP_User_Request $item Item being shown.
	 * @return string|void Status column markup. Returns a string if no status is found,
	 *                     otherwise it displays the markup.
	 */
	public function column_status( $item ) {
		$status        = get_post_status( $item->ID );
		$status_object = get_post_status_object( $status );

		if ( ! $status_object || empty( $status_object->label ) ) {
			return '-';
		}

		$timestamp = false;

		switch ( $status ) {
			case 'request-confirmed':
				$timestamp = $item->confirmed_timestamp;
				break;
			case 'request-completed':
				$timestamp = $item->completed_timestamp;
				break;
		}

		echo '<span class="status-label status-' . esc_attr( $status ) . '">';
		echo esc_html( $status_object->label );

		if ( $timestamp ) {
			echo ' (' . $this->get_timestamp_as_date( $timestamp ) . ')';
		}

		echo '</span>';
	}

	/**
	 * Converts a timestamp for display.
	 *
	 * @since 4.9.6
	 *
	 * @param int $timestamp Event timestamp.
	 * @return string Human readable date.
	 */
	protected function get_timestamp_as_date( $timestamp ) {
		if ( empty( $timestamp ) ) {
			return '';
		}

		$time_diff = time() - $timestamp;

		if ( $time_diff >= 0 && $time_diff < DAY_IN_SECONDS ) {
			/* translators: %s: Human-readable time difference. */
			return sprintf( __( '%s ago' ), human_time_diff( $timestamp ) );
		}

		return date_i18n( get_option( 'date_format' ), $timestamp );
	}

	/**
	 * Handles the default column.
	 *
	 * @since 4.9.6
	 * @since 5.7.0 Added `manage_{$this->screen->id}_custom_column` action.
	 *
	 * @param WP_User_Request $item        Item being shown.
	 * @param string          $column_name Name of column being shown.
	 */
	public function column_default( $item, $column_name ) {
		/**
		 * Fires for each custom column of a specific request type in the Privacy Requests list table.
		 *
		 * Custom columns are registered using the {@see 'manage_export-personal-data_columns'}
		 * and the {@see 'manage_erase-personal-data_columns'} filters.
		 *
		 * The dynamic portion of the hook name, `$this->screen->id`, refers to the ID given to the list table
		 * according to which screen it's displayed on.
		 *
		 * Possible hook names include:
		 *
		 *  - `manage_export-personal-data_custom_column`
		 *  - `manage_erase-personal-data_custom_column`
		 *
		 * @since 5.7.0
		 *
		 * @param string          $column_name The name of the column to display.
		 * @param WP_User_Request $item        The item being shown.
		 */
		do_action( "manage_{$this->screen->id}_custom_column", $column_name, $item );
	}

	/**
	 * Returns the markup for the Created timestamp column. Overridden by children.
	 *
	 * @since 5.7.0
	 *
	 * @param WP_User_Request $item Item being shown.
	 * @return string Human readable date.
	 */
	public function column_created_timestamp( $item ) {
		return $this->get_timestamp_as_date( $item->created_timestamp );
	}

	/**
	 * Actions column. Overridden by children.
	 *
	 * @since 4.9.6
	 *
	 * @param WP_User_Request $item Item being shown.
	 * @return string Email column markup.
	 */
	public function column_email( $item ) {
		return sprintf( '<a href="%1$s">%2$s</a> %3$s', esc_url( 'mailto:' . $item->email ), $item->email, $this->row_actions( array() ) );
	}

	/**
	 * Returns the markup for the next steps column. Overridden by children.
	 *
	 * @since 4.9.6
	 *
	 * @param WP_User_Request $item Item being shown.
	 */
	public function column_next_steps( $item ) {}

	/**
	 * Generates content for a single row of the table,
	 *
	 * @since 4.9.6
	 *
	 * @param WP_User_Request $item The current item.
	 */
	public function single_row( $item ) {
		$status = $item->status;

		echo '<tr id="request-' . esc_attr( $item->ID ) . '" class="status-' . esc_attr( $status ) . '">';
		$this->single_row_columns( $item );
		echo '</tr>';
	}

	/**
	 * Embeds scripts used to perform actions. Overridden by children.
	 *
	 * @since 4.9.6
	 */
	public function embed_scripts() {}
}
class-wp-list-table.php000064400000147252151327343360011066 0ustar00<?php
/**
 * Administration API: WP_List_Table class
 *
 * @package WordPress
 * @subpackage List_Table
 * @since 3.1.0
 */

/**
 * Base class for displaying a list of items in an ajaxified HTML table.
 *
 * @since 3.1.0
 */
#[AllowDynamicProperties]
class WP_List_Table {

	/**
	 * The current list of items.
	 *
	 * @since 3.1.0
	 * @var array
	 */
	public $items;

	/**
	 * Various information about the current table.
	 *
	 * @since 3.1.0
	 * @var array
	 */
	protected $_args;

	/**
	 * Various information needed for displaying the pagination.
	 *
	 * @since 3.1.0
	 * @var array
	 */
	protected $_pagination_args = array();

	/**
	 * The current screen.
	 *
	 * @since 3.1.0
	 * @var WP_Screen
	 */
	protected $screen;

	/**
	 * Cached bulk actions.
	 *
	 * @since 3.1.0
	 * @var array
	 */
	private $_actions;

	/**
	 * Cached pagination output.
	 *
	 * @since 3.1.0
	 * @var string
	 */
	private $_pagination;

	/**
	 * The view switcher modes.
	 *
	 * @since 4.1.0
	 * @var array
	 */
	protected $modes = array();

	/**
	 * Stores the value returned by ->get_column_info().
	 *
	 * @since 4.1.0
	 * @var array
	 */
	protected $_column_headers;

	/**
	 * {@internal Missing Summary}
	 *
	 * @var array
	 */
	protected $compat_fields = array( '_args', '_pagination_args', 'screen', '_actions', '_pagination' );

	/**
	 * {@internal Missing Summary}
	 *
	 * @var array
	 */
	protected $compat_methods = array(
		'set_pagination_args',
		'get_views',
		'get_bulk_actions',
		'bulk_actions',
		'row_actions',
		'months_dropdown',
		'view_switcher',
		'comments_bubble',
		'get_items_per_page',
		'pagination',
		'get_sortable_columns',
		'get_column_info',
		'get_table_classes',
		'display_tablenav',
		'extra_tablenav',
		'single_row_columns',
	);

	/**
	 * Constructor.
	 *
	 * The child class should call this constructor from its own constructor to override
	 * the default $args.
	 *
	 * @since 3.1.0
	 *
	 * @param array|string $args {
	 *     Array or string of arguments.
	 *
	 *     @type string $plural   Plural value used for labels and the objects being listed.
	 *                            This affects things such as CSS class-names and nonces used
	 *                            in the list table, e.g. 'posts'. Default empty.
	 *     @type string $singular Singular label for an object being listed, e.g. 'post'.
	 *                            Default empty
	 *     @type bool   $ajax     Whether the list table supports Ajax. This includes loading
	 *                            and sorting data, for example. If true, the class will call
	 *                            the _js_vars() method in the footer to provide variables
	 *                            to any scripts handling Ajax events. Default false.
	 *     @type string $screen   String containing the hook name used to determine the current
	 *                            screen. If left null, the current screen will be automatically set.
	 *                            Default null.
	 * }
	 */
	public function __construct( $args = array() ) {
		$args = wp_parse_args(
			$args,
			array(
				'plural'   => '',
				'singular' => '',
				'ajax'     => false,
				'screen'   => null,
			)
		);

		$this->screen = convert_to_screen( $args['screen'] );

		add_filter( "manage_{$this->screen->id}_columns", array( $this, 'get_columns' ), 0 );

		if ( ! $args['plural'] ) {
			$args['plural'] = $this->screen->base;
		}

		$args['plural']   = sanitize_key( $args['plural'] );
		$args['singular'] = sanitize_key( $args['singular'] );

		$this->_args = $args;

		if ( $args['ajax'] ) {
			// wp_enqueue_script( 'list-table' );
			add_action( 'admin_footer', array( $this, '_js_vars' ) );
		}

		if ( empty( $this->modes ) ) {
			$this->modes = array(
				'list'    => __( 'Compact view' ),
				'excerpt' => __( 'Extended view' ),
			);
		}
	}

	/**
	 * Makes private properties readable for backward compatibility.
	 *
	 * @since 4.0.0
	 * @since 6.4.0 Getting a dynamic property is deprecated.
	 *
	 * @param string $name Property to get.
	 * @return mixed Property.
	 */
	public function __get( $name ) {
		if ( in_array( $name, $this->compat_fields, true ) ) {
			return $this->$name;
		}

		wp_trigger_error(
			__METHOD__,
			"The property `{$name}` is not declared. Getting a dynamic property is " .
			'deprecated since version 6.4.0! Instead, declare the property on the class.',
			E_USER_DEPRECATED
		);
		return null;
	}

	/**
	 * Makes private properties settable for backward compatibility.
	 *
	 * @since 4.0.0
	 * @since 6.4.0 Setting a dynamic property is deprecated.
	 *
	 * @param string $name  Property to check if set.
	 * @param mixed  $value Property value.
	 */
	public function __set( $name, $value ) {
		if ( in_array( $name, $this->compat_fields, true ) ) {
			$this->$name = $value;
			return;
		}

		wp_trigger_error(
			__METHOD__,
			"The property `{$name}` is not declared. Setting a dynamic property is " .
			'deprecated since version 6.4.0! Instead, declare the property on the class.',
			E_USER_DEPRECATED
		);
	}

	/**
	 * Makes private properties checkable for backward compatibility.
	 *
	 * @since 4.0.0
	 * @since 6.4.0 Checking a dynamic property is deprecated.
	 *
	 * @param string $name Property to check if set.
	 * @return bool Whether the property is a back-compat property and it is set.
	 */
	public function __isset( $name ) {
		if ( in_array( $name, $this->compat_fields, true ) ) {
			return isset( $this->$name );
		}

		wp_trigger_error(
			__METHOD__,
			"The property `{$name}` is not declared. Checking `isset()` on a dynamic property " .
			'is deprecated since version 6.4.0! Instead, declare the property on the class.',
			E_USER_DEPRECATED
		);
		return false;
	}

	/**
	 * Makes private properties un-settable for backward compatibility.
	 *
	 * @since 4.0.0
	 * @since 6.4.0 Unsetting a dynamic property is deprecated.
	 *
	 * @param string $name Property to unset.
	 */
	public function __unset( $name ) {
		if ( in_array( $name, $this->compat_fields, true ) ) {
			unset( $this->$name );
			return;
		}

		wp_trigger_error(
			__METHOD__,
			"A property `{$name}` is not declared. Unsetting a dynamic property is " .
			'deprecated since version 6.4.0! Instead, declare the property on the class.',
			E_USER_DEPRECATED
		);
	}

	/**
	 * Makes private/protected methods readable for backward compatibility.
	 *
	 * @since 4.0.0
	 *
	 * @param string $name      Method to call.
	 * @param array  $arguments Arguments to pass when calling.
	 * @return mixed|bool Return value of the callback, false otherwise.
	 */
	public function __call( $name, $arguments ) {
		if ( in_array( $name, $this->compat_methods, true ) ) {
			return $this->$name( ...$arguments );
		}
		return false;
	}

	/**
	 * Checks the current user's permissions
	 *
	 * @since 3.1.0
	 * @abstract
	 */
	public function ajax_user_can() {
		die( 'function WP_List_Table::ajax_user_can() must be overridden in a subclass.' );
	}

	/**
	 * Prepares the list of items for displaying.
	 *
	 * @uses WP_List_Table::set_pagination_args()
	 *
	 * @since 3.1.0
	 * @abstract
	 */
	public function prepare_items() {
		die( 'function WP_List_Table::prepare_items() must be overridden in a subclass.' );
	}

	/**
	 * Sets all the necessary pagination arguments.
	 *
	 * @since 3.1.0
	 *
	 * @param array|string $args Array or string of arguments with information about the pagination.
	 */
	protected function set_pagination_args( $args ) {
		$args = wp_parse_args(
			$args,
			array(
				'total_items' => 0,
				'total_pages' => 0,
				'per_page'    => 0,
			)
		);

		if ( ! $args['total_pages'] && $args['per_page'] > 0 ) {
			$args['total_pages'] = (int) ceil( $args['total_items'] / $args['per_page'] );
		}

		// Redirect if page number is invalid and headers are not already sent.
		if ( ! headers_sent() && ! wp_doing_ajax() && $args['total_pages'] > 0 && $this->get_pagenum() > $args['total_pages'] ) {
			wp_redirect( add_query_arg( 'paged', $args['total_pages'] ) );
			exit;
		}

		$this->_pagination_args = $args;
	}

	/**
	 * Access the pagination args.
	 *
	 * @since 3.1.0
	 *
	 * @param string $key Pagination argument to retrieve. Common values include 'total_items',
	 *                    'total_pages', 'per_page', or 'infinite_scroll'.
	 * @return int Number of items that correspond to the given pagination argument.
	 */
	public function get_pagination_arg( $key ) {
		if ( 'page' === $key ) {
			return $this->get_pagenum();
		}

		if ( isset( $this->_pagination_args[ $key ] ) ) {
			return $this->_pagination_args[ $key ];
		}

		return 0;
	}

	/**
	 * Determines whether the table has items to display or not
	 *
	 * @since 3.1.0
	 *
	 * @return bool
	 */
	public function has_items() {
		return ! empty( $this->items );
	}

	/**
	 * Message to be displayed when there are no items
	 *
	 * @since 3.1.0
	 */
	public function no_items() {
		_e( 'No items found.' );
	}

	/**
	 * Displays the search box.
	 *
	 * @since 3.1.0
	 *
	 * @param string $text     The 'submit' button label.
	 * @param string $input_id ID attribute value for the search input field.
	 */
	public function search_box( $text, $input_id ) {
		if ( empty( $_REQUEST['s'] ) && ! $this->has_items() ) {
			return;
		}

		$input_id = $input_id . '-search-input';

		if ( ! empty( $_REQUEST['orderby'] ) ) {
			if ( is_array( $_REQUEST['orderby'] ) ) {
				foreach ( $_REQUEST['orderby'] as $key => $value ) {
					echo '<input type="hidden" name="orderby[' . esc_attr( $key ) . ']" value="' . esc_attr( $value ) . '" />';
				}
			} else {
				echo '<input type="hidden" name="orderby" value="' . esc_attr( $_REQUEST['orderby'] ) . '" />';
			}
		}
		if ( ! empty( $_REQUEST['order'] ) ) {
			echo '<input type="hidden" name="order" value="' . esc_attr( $_REQUEST['order'] ) . '" />';
		}
		if ( ! empty( $_REQUEST['post_mime_type'] ) ) {
			echo '<input type="hidden" name="post_mime_type" value="' . esc_attr( $_REQUEST['post_mime_type'] ) . '" />';
		}
		if ( ! empty( $_REQUEST['detached'] ) ) {
			echo '<input type="hidden" name="detached" value="' . esc_attr( $_REQUEST['detached'] ) . '" />';
		}
		?>
<p class="search-box">
	<label class="screen-reader-text" for="<?php echo esc_attr( $input_id ); ?>"><?php echo $text; ?>:</label>
	<input type="search" id="<?php echo esc_attr( $input_id ); ?>" name="s" value="<?php _admin_search_query(); ?>" />
		<?php submit_button( $text, '', '', false, array( 'id' => 'search-submit' ) ); ?>
</p>
		<?php
	}

	/**
	 * Generates views links.
	 *
	 * @since 6.1.0
	 *
	 * @param array $link_data {
	 *     An array of link data.
	 *
	 *     @type string $url     The link URL.
	 *     @type string $label   The link label.
	 *     @type bool   $current Optional. Whether this is the currently selected view.
	 * }
	 * @return string[] An array of link markup. Keys match the `$link_data` input array.
	 */
	protected function get_views_links( $link_data = array() ) {
		if ( ! is_array( $link_data ) ) {
			_doing_it_wrong(
				__METHOD__,
				sprintf(
					/* translators: %s: The $link_data argument. */
					__( 'The %s argument must be an array.' ),
					'<code>$link_data</code>'
				),
				'6.1.0'
			);

			return array( '' );
		}

		$views_links = array();

		foreach ( $link_data as $view => $link ) {
			if ( empty( $link['url'] ) || ! is_string( $link['url'] ) || '' === trim( $link['url'] ) ) {
				_doing_it_wrong(
					__METHOD__,
					sprintf(
						/* translators: %1$s: The argument name. %2$s: The view name. */
						__( 'The %1$s argument must be a non-empty string for %2$s.' ),
						'<code>url</code>',
						'<code>' . esc_html( $view ) . '</code>'
					),
					'6.1.0'
				);

				continue;
			}

			if ( empty( $link['label'] ) || ! is_string( $link['label'] ) || '' === trim( $link['label'] ) ) {
				_doing_it_wrong(
					__METHOD__,
					sprintf(
						/* translators: %1$s: The argument name. %2$s: The view name. */
						__( 'The %1$s argument must be a non-empty string for %2$s.' ),
						'<code>label</code>',
						'<code>' . esc_html( $view ) . '</code>'
					),
					'6.1.0'
				);

				continue;
			}

			$views_links[ $view ] = sprintf(
				'<a href="%s"%s>%s</a>',
				esc_url( $link['url'] ),
				isset( $link['current'] ) && true === $link['current'] ? ' class="current" aria-current="page"' : '',
				$link['label']
			);
		}

		return $views_links;
	}

	/**
	 * Gets the list of views available on this table.
	 *
	 * The format is an associative array:
	 * - `'id' => 'link'`
	 *
	 * @since 3.1.0
	 *
	 * @return array
	 */
	protected function get_views() {
		return array();
	}

	/**
	 * Displays the list of views available on this table.
	 *
	 * @since 3.1.0
	 */
	public function views() {
		$views = $this->get_views();
		/**
		 * Filters the list of available list table views.
		 *
		 * The dynamic portion of the hook name, `$this->screen->id`, refers
		 * to the ID of the current screen.
		 *
		 * @since 3.1.0
		 *
		 * @param string[] $views An array of available list table views.
		 */
		$views = apply_filters( "views_{$this->screen->id}", $views );

		if ( empty( $views ) ) {
			return;
		}

		$this->screen->render_screen_reader_content( 'heading_views' );

		echo "<ul class='subsubsub'>\n";
		foreach ( $views as $class => $view ) {
			$views[ $class ] = "\t<li class='$class'>$view";
		}
		echo implode( " |</li>\n", $views ) . "</li>\n";
		echo '</ul>';
	}

	/**
	 * Retrieves the list of bulk actions available for this table.
	 *
	 * The format is an associative array where each element represents either a top level option value and label, or
	 * an array representing an optgroup and its options.
	 *
	 * For a standard option, the array element key is the field value and the array element value is the field label.
	 *
	 * For an optgroup, the array element key is the label and the array element value is an associative array of
	 * options as above.
	 *
	 * Example:
	 *
	 *     [
	 *         'edit'         => 'Edit',
	 *         'delete'       => 'Delete',
	 *         'Change State' => [
	 *             'feature' => 'Featured',
	 *             'sale'    => 'On Sale',
	 *         ]
	 *     ]
	 *
	 * @since 3.1.0
	 * @since 5.6.0 A bulk action can now contain an array of options in order to create an optgroup.
	 *
	 * @return array
	 */
	protected function get_bulk_actions() {
		return array();
	}

	/**
	 * Displays the bulk actions dropdown.
	 *
	 * @since 3.1.0
	 *
	 * @param string $which The location of the bulk actions: Either 'top' or 'bottom'.
	 *                      This is designated as optional for backward compatibility.
	 */
	protected function bulk_actions( $which = '' ) {
		if ( is_null( $this->_actions ) ) {
			$this->_actions = $this->get_bulk_actions();

			/**
			 * Filters the items in the bulk actions menu of the list table.
			 *
			 * The dynamic portion of the hook name, `$this->screen->id`, refers
			 * to the ID of the current screen.
			 *
			 * @since 3.1.0
			 * @since 5.6.0 A bulk action can now contain an array of options in order to create an optgroup.
			 *
			 * @param array $actions An array of the available bulk actions.
			 */
			$this->_actions = apply_filters( "bulk_actions-{$this->screen->id}", $this->_actions ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores

			$two = '';
		} else {
			$two = '2';
		}

		if ( empty( $this->_actions ) ) {
			return;
		}

		echo '<label for="bulk-action-selector-' . esc_attr( $which ) . '" class="screen-reader-text">' .
			/* translators: Hidden accessibility text. */
			__( 'Select bulk action' ) .
		'</label>';
		echo '<select name="action' . $two . '" id="bulk-action-selector-' . esc_attr( $which ) . "\">\n";
		echo '<option value="-1">' . __( 'Bulk actions' ) . "</option>\n";

		foreach ( $this->_actions as $key => $value ) {
			if ( is_array( $value ) ) {
				echo "\t" . '<optgroup label="' . esc_attr( $key ) . '">' . "\n";

				foreach ( $value as $name => $title ) {
					$class = ( 'edit' === $name ) ? ' class="hide-if-no-js"' : '';

					echo "\t\t" . '<option value="' . esc_attr( $name ) . '"' . $class . '>' . $title . "</option>\n";
				}
				echo "\t" . "</optgroup>\n";
			} else {
				$class = ( 'edit' === $key ) ? ' class="hide-if-no-js"' : '';

				echo "\t" . '<option value="' . esc_attr( $key ) . '"' . $class . '>' . $value . "</option>\n";
			}
		}

		echo "</select>\n";

		submit_button( __( 'Apply' ), 'action', 'bulk_action', false, array( 'id' => "doaction$two" ) );
		echo "\n";
	}

	/**
	 * Gets the current action selected from the bulk actions dropdown.
	 *
	 * @since 3.1.0
	 *
	 * @return string|false The action name. False if no action was selected.
	 */
	public function current_action() {
		if ( isset( $_REQUEST['filter_action'] ) && ! empty( $_REQUEST['filter_action'] ) ) {
			return false;
		}

		if ( isset( $_REQUEST['action'] ) && '-1' !== $_REQUEST['action'] ) {
			return $_REQUEST['action'];
		}

		return false;
	}

	/**
	 * Generates the required HTML for a list of row action links.
	 *
	 * @since 3.1.0
	 *
	 * @param string[] $actions        An array of action links.
	 * @param bool     $always_visible Whether the actions should be always visible.
	 * @return string The HTML for the row actions.
	 */
	protected function row_actions( $actions, $always_visible = false ) {
		$action_count = count( $actions );

		if ( ! $action_count ) {
			return '';
		}

		$mode = get_user_setting( 'posts_list_mode', 'list' );

		if ( 'excerpt' === $mode ) {
			$always_visible = true;
		}

		$output = '<div class="' . ( $always_visible ? 'row-actions visible' : 'row-actions' ) . '">';

		$i = 0;

		foreach ( $actions as $action => $link ) {
			++$i;

			$separator = ( $i < $action_count ) ? ' | ' : '';

			$output .= "<span class='$action'>{$link}{$separator}</span>";
		}

		$output .= '</div>';

		$output .= '<button type="button" class="toggle-row"><span class="screen-reader-text">' .
			/* translators: Hidden accessibility text. */
			__( 'Show more details' ) .
		'</span></button>';

		return $output;
	}

	/**
	 * Displays a dropdown for filtering items in the list table by month.
	 *
	 * @since 3.1.0
	 *
	 * @global wpdb      $wpdb      WordPress database abstraction object.
	 * @global WP_Locale $wp_locale WordPress date and time locale object.
	 *
	 * @param string $post_type The post type.
	 */
	protected function months_dropdown( $post_type ) {
		global $wpdb, $wp_locale;

		/**
		 * Filters whether to remove the 'Months' drop-down from the post list table.
		 *
		 * @since 4.2.0
		 *
		 * @param bool   $disable   Whether to disable the drop-down. Default false.
		 * @param string $post_type The post type.
		 */
		if ( apply_filters( 'disable_months_dropdown', false, $post_type ) ) {
			return;
		}

		/**
		 * Filters whether to short-circuit performing the months dropdown query.
		 *
		 * @since 5.7.0
		 *
		 * @param object[]|false $months   'Months' drop-down results. Default false.
		 * @param string         $post_type The post type.
		 */
		$months = apply_filters( 'pre_months_dropdown_query', false, $post_type );

		if ( ! is_array( $months ) ) {
			$extra_checks = "AND post_status != 'auto-draft'";
			if ( ! isset( $_GET['post_status'] ) || 'trash' !== $_GET['post_status'] ) {
				$extra_checks .= " AND post_status != 'trash'";
			} elseif ( isset( $_GET['post_status'] ) ) {
				$extra_checks = $wpdb->prepare( ' AND post_status = %s', $_GET['post_status'] );
			}

			$months = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT DISTINCT YEAR( post_date ) AS year, MONTH( post_date ) AS month
					FROM $wpdb->posts
					WHERE post_type = %s
					$extra_checks
					ORDER BY post_date DESC",
					$post_type
				)
			);
		}

		/**
		 * Filters the 'Months' drop-down results.
		 *
		 * @since 3.7.0
		 *
		 * @param object[] $months    Array of the months drop-down query results.
		 * @param string   $post_type The post type.
		 */
		$months = apply_filters( 'months_dropdown_results', $months, $post_type );

		$month_count = count( $months );

		if ( ! $month_count || ( 1 === $month_count && 0 === (int) $months[0]->month ) ) {
			return;
		}

		$selected_month = isset( $_GET['m'] ) ? (int) $_GET['m'] : 0;
		?>
		<label for="filter-by-date" class="screen-reader-text"><?php echo get_post_type_object( $post_type )->labels->filter_by_date; ?></label>
		<select name="m" id="filter-by-date">
			<option<?php selected( $selected_month, 0 ); ?> value="0"><?php _e( 'All dates' ); ?></option>
		<?php
		foreach ( $months as $arc_row ) {
			if ( 0 === (int) $arc_row->year ) {
				continue;
			}

			$month = zeroise( $arc_row->month, 2 );
			$year  = $arc_row->year;

			printf(
				"<option %s value='%s'>%s</option>\n",
				selected( $selected_month, $year . $month, false ),
				esc_attr( $year . $month ),
				/* translators: 1: Month name, 2: 4-digit year. */
				esc_html( sprintf( __( '%1$s %2$d' ), $wp_locale->get_month( $month ), $year ) )
			);
		}
		?>
		</select>
		<?php
	}

	/**
	 * Displays a view switcher.
	 *
	 * @since 3.1.0
	 *
	 * @param string $current_mode
	 */
	protected function view_switcher( $current_mode ) {
		?>
		<input type="hidden" name="mode" value="<?php echo esc_attr( $current_mode ); ?>" />
		<div class="view-switch">
		<?php
		foreach ( $this->modes as $mode => $title ) {
			$classes      = array( 'view-' . $mode );
			$aria_current = '';

			if ( $current_mode === $mode ) {
				$classes[]    = 'current';
				$aria_current = ' aria-current="page"';
			}

			printf(
				"<a href='%s' class='%s' id='view-switch-$mode'$aria_current>" .
					"<span class='screen-reader-text'>%s</span>" .
				"</a>\n",
				esc_url( remove_query_arg( 'attachment-filter', add_query_arg( 'mode', $mode ) ) ),
				implode( ' ', $classes ),
				$title
			);
		}
		?>
		</div>
		<?php
	}

	/**
	 * Displays a comment count bubble.
	 *
	 * @since 3.1.0
	 *
	 * @param int $post_id          The post ID.
	 * @param int $pending_comments Number of pending comments.
	 */
	protected function comments_bubble( $post_id, $pending_comments ) {
		$post_object   = get_post( $post_id );
		$edit_post_cap = $post_object ? 'edit_post' : 'edit_posts';

		if ( ! current_user_can( $edit_post_cap, $post_id )
			&& ( post_password_required( $post_id )
				|| ! current_user_can( 'read_post', $post_id ) )
		) {
			// The user has no access to the post and thus cannot see the comments.
			return false;
		}

		$approved_comments = get_comments_number();

		$approved_comments_number = number_format_i18n( $approved_comments );
		$pending_comments_number  = number_format_i18n( $pending_comments );

		$approved_only_phrase = sprintf(
			/* translators: %s: Number of comments. */
			_n( '%s comment', '%s comments', $approved_comments ),
			$approved_comments_number
		);

		$approved_phrase = sprintf(
			/* translators: %s: Number of comments. */
			_n( '%s approved comment', '%s approved comments', $approved_comments ),
			$approved_comments_number
		);

		$pending_phrase = sprintf(
			/* translators: %s: Number of comments. */
			_n( '%s pending comment', '%s pending comments', $pending_comments ),
			$pending_comments_number
		);

		if ( ! $approved_comments && ! $pending_comments ) {
			// No comments at all.
			printf(
				'<span aria-hidden="true">&#8212;</span>' .
				'<span class="screen-reader-text">%s</span>',
				__( 'No comments' )
			);
		} elseif ( $approved_comments && 'trash' === get_post_status( $post_id ) ) {
			// Don't link the comment bubble for a trashed post.
			printf(
				'<span class="post-com-count post-com-count-approved">' .
					'<span class="comment-count-approved" aria-hidden="true">%s</span>' .
					'<span class="screen-reader-text">%s</span>' .
				'</span>',
				$approved_comments_number,
				$pending_comments ? $approved_phrase : $approved_only_phrase
			);
		} elseif ( $approved_comments ) {
			// Link the comment bubble to approved comments.
			printf(
				'<a href="%s" class="post-com-count post-com-count-approved">' .
					'<span class="comment-count-approved" aria-hidden="true">%s</span>' .
					'<span class="screen-reader-text">%s</span>' .
				'</a>',
				esc_url(
					add_query_arg(
						array(
							'p'              => $post_id,
							'comment_status' => 'approved',
						),
						admin_url( 'edit-comments.php' )
					)
				),
				$approved_comments_number,
				$pending_comments ? $approved_phrase : $approved_only_phrase
			);
		} else {
			// Don't link the comment bubble when there are no approved comments.
			printf(
				'<span class="post-com-count post-com-count-no-comments">' .
					'<span class="comment-count comment-count-no-comments" aria-hidden="true">%s</span>' .
					'<span class="screen-reader-text">%s</span>' .
				'</span>',
				$approved_comments_number,
				$pending_comments ?
				/* translators: Hidden accessibility text. */
				__( 'No approved comments' ) :
				/* translators: Hidden accessibility text. */
				__( 'No comments' )
			);
		}

		if ( $pending_comments ) {
			printf(
				'<a href="%s" class="post-com-count post-com-count-pending">' .
					'<span class="comment-count-pending" aria-hidden="true">%s</span>' .
					'<span class="screen-reader-text">%s</span>' .
				'</a>',
				esc_url(
					add_query_arg(
						array(
							'p'              => $post_id,
							'comment_status' => 'moderated',
						),
						admin_url( 'edit-comments.php' )
					)
				),
				$pending_comments_number,
				$pending_phrase
			);
		} else {
			printf(
				'<span class="post-com-count post-com-count-pending post-com-count-no-pending">' .
					'<span class="comment-count comment-count-no-pending" aria-hidden="true">%s</span>' .
					'<span class="screen-reader-text">%s</span>' .
				'</span>',
				$pending_comments_number,
				$approved_comments ?
				/* translators: Hidden accessibility text. */
				__( 'No pending comments' ) :
				/* translators: Hidden accessibility text. */
				__( 'No comments' )
			);
		}
	}

	/**
	 * Gets the current page number.
	 *
	 * @since 3.1.0
	 *
	 * @return int
	 */
	public function get_pagenum() {
		$pagenum = isset( $_REQUEST['paged'] ) ? absint( $_REQUEST['paged'] ) : 0;

		if ( isset( $this->_pagination_args['total_pages'] ) && $pagenum > $this->_pagination_args['total_pages'] ) {
			$pagenum = $this->_pagination_args['total_pages'];
		}

		return max( 1, $pagenum );
	}

	/**
	 * Gets the number of items to display on a single page.
	 *
	 * @since 3.1.0
	 *
	 * @param string $option        User option name.
	 * @param int    $default_value Optional. The number of items to display. Default 20.
	 * @return int
	 */
	protected function get_items_per_page( $option, $default_value = 20 ) {
		$per_page = (int) get_user_option( $option );
		if ( empty( $per_page ) || $per_page < 1 ) {
			$per_page = $default_value;
		}

		/**
		 * Filters the number of items to be displayed on each page of the list table.
		 *
		 * The dynamic hook name, `$option`, refers to the `per_page` option depending
		 * on the type of list table in use. Possible filter names include:
		 *
		 *  - `edit_comments_per_page`
		 *  - `sites_network_per_page`
		 *  - `site_themes_network_per_page`
		 *  - `themes_network_per_page`
		 *  - `users_network_per_page`
		 *  - `edit_post_per_page`
		 *  - `edit_page_per_page`
		 *  - `edit_{$post_type}_per_page`
		 *  - `edit_post_tag_per_page`
		 *  - `edit_category_per_page`
		 *  - `edit_{$taxonomy}_per_page`
		 *  - `site_users_network_per_page`
		 *  - `users_per_page`
		 *
		 * @since 2.9.0
		 *
		 * @param int $per_page Number of items to be displayed. Default 20.
		 */
		return (int) apply_filters( "{$option}", $per_page );
	}

	/**
	 * Displays the pagination.
	 *
	 * @since 3.1.0
	 *
	 * @param string $which The location of the pagination: Either 'top' or 'bottom'.
	 */
	protected function pagination( $which ) {
		if ( empty( $this->_pagination_args ) ) {
			return;
		}

		$total_items     = $this->_pagination_args['total_items'];
		$total_pages     = $this->_pagination_args['total_pages'];
		$infinite_scroll = false;
		if ( isset( $this->_pagination_args['infinite_scroll'] ) ) {
			$infinite_scroll = $this->_pagination_args['infinite_scroll'];
		}

		if ( 'top' === $which && $total_pages > 1 ) {
			$this->screen->render_screen_reader_content( 'heading_pagination' );
		}

		$output = '<span class="displaying-num">' . sprintf(
			/* translators: %s: Number of items. */
			_n( '%s item', '%s items', $total_items ),
			number_format_i18n( $total_items )
		) . '</span>';

		$current              = $this->get_pagenum();
		$removable_query_args = wp_removable_query_args();

		$current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );

		$current_url = remove_query_arg( $removable_query_args, $current_url );

		$page_links = array();

		$total_pages_before = '<span class="paging-input">';
		$total_pages_after  = '</span></span>';

		$disable_first = false;
		$disable_last  = false;
		$disable_prev  = false;
		$disable_next  = false;

		if ( 1 === $current ) {
			$disable_first = true;
			$disable_prev  = true;
		}
		if ( $total_pages === $current ) {
			$disable_last = true;
			$disable_next = true;
		}

		if ( $disable_first ) {
			$page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&laquo;</span>';
		} else {
			$page_links[] = sprintf(
				"<a class='first-page button' href='%s'>" .
					"<span class='screen-reader-text'>%s</span>" .
					"<span aria-hidden='true'>%s</span>" .
				'</a>',
				esc_url( remove_query_arg( 'paged', $current_url ) ),
				/* translators: Hidden accessibility text. */
				__( 'First page' ),
				'&laquo;'
			);
		}

		if ( $disable_prev ) {
			$page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&lsaquo;</span>';
		} else {
			$page_links[] = sprintf(
				"<a class='prev-page button' href='%s'>" .
					"<span class='screen-reader-text'>%s</span>" .
					"<span aria-hidden='true'>%s</span>" .
				'</a>',
				esc_url( add_query_arg( 'paged', max( 1, $current - 1 ), $current_url ) ),
				/* translators: Hidden accessibility text. */
				__( 'Previous page' ),
				'&lsaquo;'
			);
		}

		if ( 'bottom' === $which ) {
			$html_current_page  = $current;
			$total_pages_before = sprintf(
				'<span class="screen-reader-text">%s</span>' .
				'<span id="table-paging" class="paging-input">' .
				'<span class="tablenav-paging-text">',
				/* translators: Hidden accessibility text. */
				__( 'Current Page' )
			);
		} else {
			$html_current_page = sprintf(
				'<label for="current-page-selector" class="screen-reader-text">%s</label>' .
				"<input class='current-page' id='current-page-selector' type='text'
					name='paged' value='%s' size='%d' aria-describedby='table-paging' />" .
				"<span class='tablenav-paging-text'>",
				/* translators: Hidden accessibility text. */
				__( 'Current Page' ),
				$current,
				strlen( $total_pages )
			);
		}

		$html_total_pages = sprintf( "<span class='total-pages'>%s</span>", number_format_i18n( $total_pages ) );

		$page_links[] = $total_pages_before . sprintf(
			/* translators: 1: Current page, 2: Total pages. */
			_x( '%1$s of %2$s', 'paging' ),
			$html_current_page,
			$html_total_pages
		) . $total_pages_after;

		if ( $disable_next ) {
			$page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&rsaquo;</span>';
		} else {
			$page_links[] = sprintf(
				"<a class='next-page button' href='%s'>" .
					"<span class='screen-reader-text'>%s</span>" .
					"<span aria-hidden='true'>%s</span>" .
				'</a>',
				esc_url( add_query_arg( 'paged', min( $total_pages, $current + 1 ), $current_url ) ),
				/* translators: Hidden accessibility text. */
				__( 'Next page' ),
				'&rsaquo;'
			);
		}

		if ( $disable_last ) {
			$page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&raquo;</span>';
		} else {
			$page_links[] = sprintf(
				"<a class='last-page button' href='%s'>" .
					"<span class='screen-reader-text'>%s</span>" .
					"<span aria-hidden='true'>%s</span>" .
				'</a>',
				esc_url( add_query_arg( 'paged', $total_pages, $current_url ) ),
				/* translators: Hidden accessibility text. */
				__( 'Last page' ),
				'&raquo;'
			);
		}

		$pagination_links_class = 'pagination-links';
		if ( ! empty( $infinite_scroll ) ) {
			$pagination_links_class .= ' hide-if-js';
		}
		$output .= "\n<span class='$pagination_links_class'>" . implode( "\n", $page_links ) . '</span>';

		if ( $total_pages ) {
			$page_class = $total_pages < 2 ? ' one-page' : '';
		} else {
			$page_class = ' no-pages';
		}
		$this->_pagination = "<div class='tablenav-pages{$page_class}'>$output</div>";

		echo $this->_pagination;
	}

	/**
	 * Gets a list of columns.
	 *
	 * The format is:
	 * - `'internal-name' => 'Title'`
	 *
	 * @since 3.1.0
	 * @abstract
	 *
	 * @return array
	 */
	public function get_columns() {
		die( 'function WP_List_Table::get_columns() must be overridden in a subclass.' );
	}

	/**
	 * Gets a list of sortable columns.
	 *
	 * The format is:
	 * - `'internal-name' => 'orderby'`
	 * - `'internal-name' => array( 'orderby', bool, 'abbr', 'orderby-text', 'initially-sorted-column-order' )` -
	 * - `'internal-name' => array( 'orderby', 'asc' )` - The second element sets the initial sorting order.
	 * - `'internal-name' => array( 'orderby', true )`  - The second element makes the initial order descending.
	 *
	 * In the second format, passing true as second parameter will make the initial
	 * sorting order be descending. Following parameters add a short column name to
	 * be used as 'abbr' attribute, a translatable string for the current sorting,
	 * and the initial order for the initial sorted column, 'asc' or 'desc' (default: false).
	 *
	 * @since 3.1.0
	 * @since 6.3.0 Added 'abbr', 'orderby-text' and 'initially-sorted-column-order'.
	 *
	 * @return array
	 */
	protected function get_sortable_columns() {
		return array();
	}

	/**
	 * Gets the name of the default primary column.
	 *
	 * @since 4.3.0
	 *
	 * @return string Name of the default primary column, in this case, an empty string.
	 */
	protected function get_default_primary_column_name() {
		$columns = $this->get_columns();
		$column  = '';

		if ( empty( $columns ) ) {
			return $column;
		}

		/*
		 * We need a primary defined so responsive views show something,
		 * so let's fall back to the first non-checkbox column.
		 */
		foreach ( $columns as $col => $column_name ) {
			if ( 'cb' === $col ) {
				continue;
			}

			$column = $col;
			break;
		}

		return $column;
	}

	/**
	 * Gets the name of the primary column.
	 *
	 * Public wrapper for WP_List_Table::get_default_primary_column_name().
	 *
	 * @since 4.4.0
	 *
	 * @return string Name of the default primary column.
	 */
	public function get_primary_column() {
		return $this->get_primary_column_name();
	}

	/**
	 * Gets the name of the primary column.
	 *
	 * @since 4.3.0
	 *
	 * @return string The name of the primary column.
	 */
	protected function get_primary_column_name() {
		$columns = get_column_headers( $this->screen );
		$default = $this->get_default_primary_column_name();

		/*
		 * If the primary column doesn't exist,
		 * fall back to the first non-checkbox column.
		 */
		if ( ! isset( $columns[ $default ] ) ) {
			$default = self::get_default_primary_column_name();
		}

		/**
		 * Filters the name of the primary column for the current list table.
		 *
		 * @since 4.3.0
		 *
		 * @param string $default Column name default for the specific list table, e.g. 'name'.
		 * @param string $context Screen ID for specific list table, e.g. 'plugins'.
		 */
		$column = apply_filters( 'list_table_primary_column', $default, $this->screen->id );

		if ( empty( $column ) || ! isset( $columns[ $column ] ) ) {
			$column = $default;
		}

		return $column;
	}

	/**
	 * Gets a list of all, hidden, and sortable columns, with filter applied.
	 *
	 * @since 3.1.0
	 *
	 * @return array
	 */
	protected function get_column_info() {
		// $_column_headers is already set / cached.
		if (
			isset( $this->_column_headers ) &&
			is_array( $this->_column_headers )
		) {
			/*
			 * Backward compatibility for `$_column_headers` format prior to WordPress 4.3.
			 *
			 * In WordPress 4.3 the primary column name was added as a fourth item in the
			 * column headers property. This ensures the primary column name is included
			 * in plugins setting the property directly in the three item format.
			 */
			if ( 4 === count( $this->_column_headers ) ) {
				return $this->_column_headers;
			}

			$column_headers = array( array(), array(), array(), $this->get_primary_column_name() );
			foreach ( $this->_column_headers as $key => $value ) {
				$column_headers[ $key ] = $value;
			}

			$this->_column_headers = $column_headers;

			return $this->_column_headers;
		}

		$columns = get_column_headers( $this->screen );
		$hidden  = get_hidden_columns( $this->screen );

		$sortable_columns = $this->get_sortable_columns();
		/**
		 * Filters the list table sortable columns for a specific screen.
		 *
		 * The dynamic portion of the hook name, `$this->screen->id`, refers
		 * to the ID of the current screen.
		 *
		 * @since 3.1.0
		 *
		 * @param array $sortable_columns An array of sortable columns.
		 */
		$_sortable = apply_filters( "manage_{$this->screen->id}_sortable_columns", $sortable_columns );

		$sortable = array();
		foreach ( $_sortable as $id => $data ) {
			if ( empty( $data ) ) {
				continue;
			}

			$data = (array) $data;
			// Descending initial sorting.
			if ( ! isset( $data[1] ) ) {
				$data[1] = false;
			}
			// Current sorting translatable string.
			if ( ! isset( $data[2] ) ) {
				$data[2] = '';
			}
			// Initial view sorted column and asc/desc order, default: false.
			if ( ! isset( $data[3] ) ) {
				$data[3] = false;
			}
			// Initial order for the initial sorted column, default: false.
			if ( ! isset( $data[4] ) ) {
				$data[4] = false;
			}

			$sortable[ $id ] = $data;
		}

		$primary               = $this->get_primary_column_name();
		$this->_column_headers = array( $columns, $hidden, $sortable, $primary );

		return $this->_column_headers;
	}

	/**
	 * Returns the number of visible columns.
	 *
	 * @since 3.1.0
	 *
	 * @return int
	 */
	public function get_column_count() {
		list ( $columns, $hidden ) = $this->get_column_info();
		$hidden                    = array_intersect( array_keys( $columns ), array_filter( $hidden ) );
		return count( $columns ) - count( $hidden );
	}

	/**
	 * Prints column headers, accounting for hidden and sortable columns.
	 *
	 * @since 3.1.0
	 *
	 * @param bool $with_id Whether to set the ID attribute or not
	 */
	public function print_column_headers( $with_id = true ) {
		list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info();

		$current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
		$current_url = remove_query_arg( 'paged', $current_url );

		// When users click on a column header to sort by other columns.
		if ( isset( $_GET['orderby'] ) ) {
			$current_orderby = $_GET['orderby'];
			// In the initial view there's no orderby parameter.
		} else {
			$current_orderby = '';
		}

		// Not in the initial view and descending order.
		if ( isset( $_GET['order'] ) && 'desc' === $_GET['order'] ) {
			$current_order = 'desc';
		} else {
			// The initial view is not always 'asc', we'll take care of this below.
			$current_order = 'asc';
		}

		if ( ! empty( $columns['cb'] ) ) {
			static $cb_counter = 1;
			$columns['cb']     = '<input id="cb-select-all-' . $cb_counter . '" type="checkbox" />
			<label for="cb-select-all-' . $cb_counter . '">' .
				'<span class="screen-reader-text">' .
					/* translators: Hidden accessibility text. */
					__( 'Select All' ) .
				'</span>' .
				'</label>';
			++$cb_counter;
		}

		foreach ( $columns as $column_key => $column_display_name ) {
			$class          = array( 'manage-column', "column-$column_key" );
			$aria_sort_attr = '';
			$abbr_attr      = '';
			$order_text     = '';

			if ( in_array( $column_key, $hidden, true ) ) {
				$class[] = 'hidden';
			}

			if ( 'cb' === $column_key ) {
				$class[] = 'check-column';
			} elseif ( in_array( $column_key, array( 'posts', 'comments', 'links' ), true ) ) {
				$class[] = 'num';
			}

			if ( $column_key === $primary ) {
				$class[] = 'column-primary';
			}

			if ( isset( $sortable[ $column_key ] ) ) {
				$orderby       = isset( $sortable[ $column_key ][0] ) ? $sortable[ $column_key ][0] : '';
				$desc_first    = isset( $sortable[ $column_key ][1] ) ? $sortable[ $column_key ][1] : false;
				$abbr          = isset( $sortable[ $column_key ][2] ) ? $sortable[ $column_key ][2] : '';
				$orderby_text  = isset( $sortable[ $column_key ][3] ) ? $sortable[ $column_key ][3] : '';
				$initial_order = isset( $sortable[ $column_key ][4] ) ? $sortable[ $column_key ][4] : '';

				/*
				 * We're in the initial view and there's no $_GET['orderby'] then check if the
				 * initial sorting information is set in the sortable columns and use that.
				 */
				if ( '' === $current_orderby && $initial_order ) {
					// Use the initially sorted column $orderby as current orderby.
					$current_orderby = $orderby;
					// Use the initially sorted column asc/desc order as initial order.
					$current_order = $initial_order;
				}

				/*
				 * True in the initial view when an initial orderby is set via get_sortable_columns()
				 * and true in the sorted views when the actual $_GET['orderby'] is equal to $orderby.
				 */
				if ( $current_orderby === $orderby ) {
					// The sorted column. The `aria-sort` attribute must be set only on the sorted column.
					if ( 'asc' === $current_order ) {
						$order          = 'desc';
						$aria_sort_attr = ' aria-sort="ascending"';
					} else {
						$order          = 'asc';
						$aria_sort_attr = ' aria-sort="descending"';
					}

					$class[] = 'sorted';
					$class[] = $current_order;
				} else {
					// The other sortable columns.
					$order = strtolower( $desc_first );

					if ( ! in_array( $order, array( 'desc', 'asc' ), true ) ) {
						$order = $desc_first ? 'desc' : 'asc';
					}

					$class[] = 'sortable';
					$class[] = 'desc' === $order ? 'asc' : 'desc';

					/* translators: Hidden accessibility text. */
					$asc_text = __( 'Sort ascending.' );
					/* translators: Hidden accessibility text. */
					$desc_text  = __( 'Sort descending.' );
					$order_text = 'asc' === $order ? $asc_text : $desc_text;
				}

				if ( '' !== $order_text ) {
					$order_text = ' <span class="screen-reader-text">' . $order_text . '</span>';
				}

				// Print an 'abbr' attribute if a value is provided via get_sortable_columns().
				$abbr_attr = $abbr ? ' abbr="' . esc_attr( $abbr ) . '"' : '';

				$column_display_name = sprintf(
					'<a href="%1$s">' .
						'<span>%2$s</span>' .
						'<span class="sorting-indicators">' .
							'<span class="sorting-indicator asc" aria-hidden="true"></span>' .
							'<span class="sorting-indicator desc" aria-hidden="true"></span>' .
						'</span>' .
						'%3$s' .
					'</a>',
					esc_url( add_query_arg( compact( 'orderby', 'order' ), $current_url ) ),
					$column_display_name,
					$order_text
				);
			}

			$tag   = ( 'cb' === $column_key ) ? 'td' : 'th';
			$scope = ( 'th' === $tag ) ? 'scope="col"' : '';
			$id    = $with_id ? "id='$column_key'" : '';

			if ( ! empty( $class ) ) {
				$class = "class='" . implode( ' ', $class ) . "'";
			}

			echo "<$tag $scope $id $class $aria_sort_attr $abbr_attr>$column_display_name</$tag>";
		}
	}

	/**
	 * Print a table description with information about current sorting and order.
	 *
	 * For the table initial view, information about initial orderby and order
	 * should be provided via get_sortable_columns().
	 *
	 * @since 6.3.0
	 * @access public
	 */
	public function print_table_description() {
		list( $columns, $hidden, $sortable ) = $this->get_column_info();

		if ( empty( $sortable ) ) {
			return;
		}

		// When users click on a column header to sort by other columns.
		if ( isset( $_GET['orderby'] ) ) {
			$current_orderby = $_GET['orderby'];
			// In the initial view there's no orderby parameter.
		} else {
			$current_orderby = '';
		}

		// Not in the initial view and descending order.
		if ( isset( $_GET['order'] ) && 'desc' === $_GET['order'] ) {
			$current_order = 'desc';
		} else {
			// The initial view is not always 'asc', we'll take care of this below.
			$current_order = 'asc';
		}

		foreach ( array_keys( $columns ) as $column_key ) {

			if ( isset( $sortable[ $column_key ] ) ) {
				$orderby       = isset( $sortable[ $column_key ][0] ) ? $sortable[ $column_key ][0] : '';
				$desc_first    = isset( $sortable[ $column_key ][1] ) ? $sortable[ $column_key ][1] : false;
				$abbr          = isset( $sortable[ $column_key ][2] ) ? $sortable[ $column_key ][2] : '';
				$orderby_text  = isset( $sortable[ $column_key ][3] ) ? $sortable[ $column_key ][3] : '';
				$initial_order = isset( $sortable[ $column_key ][4] ) ? $sortable[ $column_key ][4] : '';

				if ( ! is_string( $orderby_text ) || '' === $orderby_text ) {
					return;
				}
				/*
				 * We're in the initial view and there's no $_GET['orderby'] then check if the
				 * initial sorting information is set in the sortable columns and use that.
				 */
				if ( '' === $current_orderby && $initial_order ) {
					// Use the initially sorted column $orderby as current orderby.
					$current_orderby = $orderby;
					// Use the initially sorted column asc/desc order as initial order.
					$current_order = $initial_order;
				}

				/*
				 * True in the initial view when an initial orderby is set via get_sortable_columns()
				 * and true in the sorted views when the actual $_GET['orderby'] is equal to $orderby.
				 */
				if ( $current_orderby === $orderby ) {
					/* translators: Hidden accessibility text. */
					$asc_text = __( 'Ascending.' );
					/* translators: Hidden accessibility text. */
					$desc_text  = __( 'Descending.' );
					$order_text = 'asc' === $current_order ? $asc_text : $desc_text;
					echo '<caption class="screen-reader-text">' . $orderby_text . ' ' . $order_text . '</caption>';

					return;
				}
			}
		}
	}

	/**
	 * Displays the table.
	 *
	 * @since 3.1.0
	 */
	public function display() {
		$singular = $this->_args['singular'];

		$this->display_tablenav( 'top' );

		$this->screen->render_screen_reader_content( 'heading_list' );
		?>
<table class="wp-list-table <?php echo implode( ' ', $this->get_table_classes() ); ?>">
		<?php $this->print_table_description(); ?>
	<thead>
	<tr>
		<?php $this->print_column_headers(); ?>
	</tr>
	</thead>

	<tbody id="the-list"
		<?php
		if ( $singular ) {
			echo " data-wp-lists='list:$singular'";
		}
		?>
		>
		<?php $this->display_rows_or_placeholder(); ?>
	</tbody>

	<tfoot>
	<tr>
		<?php $this->print_column_headers( false ); ?>
	</tr>
	</tfoot>

</table>
		<?php
		$this->display_tablenav( 'bottom' );
	}

	/**
	 * Gets a list of CSS classes for the WP_List_Table table tag.
	 *
	 * @since 3.1.0
	 *
	 * @return string[] Array of CSS classes for the table tag.
	 */
	protected function get_table_classes() {
		$mode = get_user_setting( 'posts_list_mode', 'list' );

		$mode_class = esc_attr( 'table-view-' . $mode );

		return array( 'widefat', 'fixed', 'striped', $mode_class, $this->_args['plural'] );
	}

	/**
	 * Generates the table navigation above or below the table
	 *
	 * @since 3.1.0
	 * @param string $which The location of the navigation: Either 'top' or 'bottom'.
	 */
	protected function display_tablenav( $which ) {
		if ( 'top' === $which ) {
			wp_nonce_field( 'bulk-' . $this->_args['plural'] );
		}
		?>
	<div class="tablenav <?php echo esc_attr( $which ); ?>">

		<?php if ( $this->has_items() ) : ?>
		<div class="alignleft actions bulkactions">
			<?php $this->bulk_actions( $which ); ?>
		</div>
			<?php
		endif;
		$this->extra_tablenav( $which );
		$this->pagination( $which );
		?>

		<br class="clear" />
	</div>
		<?php
	}

	/**
	 * Displays extra controls between bulk actions and pagination.
	 *
	 * @since 3.1.0
	 *
	 * @param string $which
	 */
	protected function extra_tablenav( $which ) {}

	/**
	 * Generates the tbody element for the list table.
	 *
	 * @since 3.1.0
	 */
	public function display_rows_or_placeholder() {
		if ( $this->has_items() ) {
			$this->display_rows();
		} else {
			echo '<tr class="no-items"><td class="colspanchange" colspan="' . $this->get_column_count() . '">';
			$this->no_items();
			echo '</td></tr>';
		}
	}

	/**
	 * Generates the list table rows.
	 *
	 * @since 3.1.0
	 */
	public function display_rows() {
		foreach ( $this->items as $item ) {
			$this->single_row( $item );
		}
	}

	/**
	 * Generates content for a single row of the table.
	 *
	 * @since 3.1.0
	 *
	 * @param object|array $item The current item
	 */
	public function single_row( $item ) {
		echo '<tr>';
		$this->single_row_columns( $item );
		echo '</tr>';
	}

	/**
	 * @param object|array $item
	 * @param string $column_name
	 */
	protected function column_default( $item, $column_name ) {}

	/**
	 * @param object|array $item
	 */
	protected function column_cb( $item ) {}

	/**
	 * Generates the columns for a single row of the table.
	 *
	 * @since 3.1.0
	 *
	 * @param object|array $item The current item.
	 */
	protected function single_row_columns( $item ) {
		list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info();

		foreach ( $columns as $column_name => $column_display_name ) {
			$classes = "$column_name column-$column_name";
			if ( $primary === $column_name ) {
				$classes .= ' has-row-actions column-primary';
			}

			if ( in_array( $column_name, $hidden, true ) ) {
				$classes .= ' hidden';
			}

			/*
			 * Comments column uses HTML in the display name with screen reader text.
			 * Strip tags to get closer to a user-friendly string.
			 */
			$data = 'data-colname="' . esc_attr( wp_strip_all_tags( $column_display_name ) ) . '"';

			$attributes = "class='$classes' $data";

			if ( 'cb' === $column_name ) {
				echo '<th scope="row" class="check-column">';
				echo $this->column_cb( $item );
				echo '</th>';
			} elseif ( method_exists( $this, '_column_' . $column_name ) ) {
				echo call_user_func(
					array( $this, '_column_' . $column_name ),
					$item,
					$classes,
					$data,
					$primary
				);
			} elseif ( method_exists( $this, 'column_' . $column_name ) ) {
				echo "<td $attributes>";
				echo call_user_func( array( $this, 'column_' . $column_name ), $item );
				echo $this->handle_row_actions( $item, $column_name, $primary );
				echo '</td>';
			} else {
				echo "<td $attributes>";
				echo $this->column_default( $item, $column_name );
				echo $this->handle_row_actions( $item, $column_name, $primary );
				echo '</td>';
			}
		}
	}

	/**
	 * Generates and display row actions links for the list table.
	 *
	 * @since 4.3.0
	 *
	 * @param object|array $item        The item being acted upon.
	 * @param string       $column_name Current column name.
	 * @param string       $primary     Primary column name.
	 * @return string The row actions HTML, or an empty string
	 *                if the current column is not the primary column.
	 */
	protected function handle_row_actions( $item, $column_name, $primary ) {
		return $column_name === $primary ? '<button type="button" class="toggle-row"><span class="screen-reader-text">' .
			/* translators: Hidden accessibility text. */
			__( 'Show more details' ) .
		'</span></button>' : '';
	}

	/**
	 * Handles an incoming ajax request (called from admin-ajax.php)
	 *
	 * @since 3.1.0
	 */
	public function ajax_response() {
		$this->prepare_items();

		ob_start();
		if ( ! empty( $_REQUEST['no_placeholder'] ) ) {
			$this->display_rows();
		} else {
			$this->display_rows_or_placeholder();
		}

		$rows = ob_get_clean();

		$response = array( 'rows' => $rows );

		if ( isset( $this->_pagination_args['total_items'] ) ) {
			$response['total_items_i18n'] = sprintf(
				/* translators: Number of items. */
				_n( '%s item', '%s items', $this->_pagination_args['total_items'] ),
				number_format_i18n( $this->_pagination_args['total_items'] )
			);
		}
		if ( isset( $this->_pagination_args['total_pages'] ) ) {
			$response['total_pages']      = $this->_pagination_args['total_pages'];
			$response['total_pages_i18n'] = number_format_i18n( $this->_pagination_args['total_pages'] );
		}

		die( wp_json_encode( $response ) );
	}

	/**
	 * Sends required variables to JavaScript land.
	 *
	 * @since 3.1.0
	 */
	public function _js_vars() {
		$args = array(
			'class'  => get_class( $this ),
			'screen' => array(
				'id'   => $this->screen->id,
				'base' => $this->screen->base,
			),
		);

		printf( "<script type='text/javascript'>list_args = %s;</script>\n", wp_json_encode( $args ) );
	}
}
class-ftp-pure.php000060400000012462151327343360010132 0ustar00<?php
/**
 * PemFTP - An Ftp implementation in pure PHP
 *
 * @package PemFTP
 * @since 2.5.0
 *
 * @version 1.0
 * @copyright Alexey Dotsenko
 * @author Alexey Dotsenko
 * @link https://www.phpclasses.org/package/1743-PHP-FTP-client-in-pure-PHP.html
 * @license LGPL https://opensource.org/licenses/lgpl-license.html
 */

/**
 * FTP implementation using fsockopen to connect.
 *
 * @package PemFTP
 * @subpackage Pure
 * @since 2.5.0
 *
 * @version 1.0
 * @copyright Alexey Dotsenko
 * @author Alexey Dotsenko
 * @link https://www.phpclasses.org/package/1743-PHP-FTP-client-in-pure-PHP.html
 * @license LGPL https://opensource.org/licenses/lgpl-license.html
 */
class ftp_pure extends ftp_base {

	function __construct($verb=FALSE, $le=FALSE) {
		parent::__construct(false, $verb, $le);
	}

// <!-- --------------------------------------------------------------------------------------- -->
// <!--       Private functions                                                                 -->
// <!-- --------------------------------------------------------------------------------------- -->

	function _settimeout($sock) {
		if(!@stream_set_timeout($sock, $this->_timeout)) {
			$this->PushError('_settimeout','socket set send timeout');
			$this->_quit();
			return FALSE;
		}
		return TRUE;
	}

	function _connect($host, $port) {
		$this->SendMSG("Creating socket");
		$sock = @fsockopen($host, $port, $errno, $errstr, $this->_timeout);
		if (!$sock) {
			$this->PushError('_connect','socket connect failed', $errstr." (".$errno.")");
			return FALSE;
		}
		$this->_connected=true;
		return $sock;
	}

	function _readmsg($fnction="_readmsg"){
		if(!$this->_connected) {
			$this->PushError($fnction, 'Connect first');
			return FALSE;
		}
		$result=true;
		$this->_message="";
		$this->_code=0;
		$go=true;
		do {
			$tmp=@fgets($this->_ftp_control_sock, 512);
			if($tmp===false) {
				$go=$result=false;
				$this->PushError($fnction,'Read failed');
			} else {
				$this->_message.=$tmp;
				if(preg_match("/^([0-9]{3})(-(.*[".CRLF."]{1,2})+\\1)? [^".CRLF."]+[".CRLF."]{1,2}$/", $this->_message, $regs)) $go=false;
			}
		} while($go);
		if($this->LocalEcho) echo "GET < ".rtrim($this->_message, CRLF).CRLF;
		$this->_code=(int)$regs[1];
		return $result;
	}

	function _exec($cmd, $fnction="_exec") {
		if(!$this->_ready) {
			$this->PushError($fnction,'Connect first');
			return FALSE;
		}
		if($this->LocalEcho) echo "PUT > ",$cmd,CRLF;
		$status=@fputs($this->_ftp_control_sock, $cmd.CRLF);
		if($status===false) {
			$this->PushError($fnction,'socket write failed');
			return FALSE;
		}
		$this->_lastaction=time();
		if(!$this->_readmsg($fnction)) return FALSE;
		return TRUE;
	}

	function _data_prepare($mode=FTP_ASCII) {
		if(!$this->_settype($mode)) return FALSE;
		if($this->_passive) {
			if(!$this->_exec("PASV", "pasv")) {
				$this->_data_close();
				return FALSE;
			}
			if(!$this->_checkCode()) {
				$this->_data_close();
				return FALSE;
			}
			$ip_port = explode(",", preg_replace("/^.+ \\(?([0-9]{1,3},[0-9]{1,3},[0-9]{1,3},[0-9]{1,3},[0-9]+,[0-9]+)\\)?.*$/s", "\\1", $this->_message));
			$this->_datahost=$ip_port[0].".".$ip_port[1].".".$ip_port[2].".".$ip_port[3];
            $this->_dataport=(((int)$ip_port[4])<<8) + ((int)$ip_port[5]);
			$this->SendMSG("Connecting to ".$this->_datahost.":".$this->_dataport);
			$this->_ftp_data_sock=@fsockopen($this->_datahost, $this->_dataport, $errno, $errstr, $this->_timeout);
			if(!$this->_ftp_data_sock) {
				$this->PushError("_data_prepare","fsockopen fails", $errstr." (".$errno.")");
				$this->_data_close();
				return FALSE;
			}
			else $this->_ftp_data_sock;
		} else {
			$this->SendMSG("Only passive connections available!");
			return FALSE;
		}
		return TRUE;
	}

	function _data_read($mode=FTP_ASCII, $fp=NULL) {
		if(is_resource($fp)) $out=0;
		else $out="";
		if(!$this->_passive) {
			$this->SendMSG("Only passive connections available!");
			return FALSE;
		}
		while (!feof($this->_ftp_data_sock)) {
			$block=fread($this->_ftp_data_sock, $this->_ftp_buff_size);
			if($mode!=FTP_BINARY) $block=preg_replace("/\r\n|\r|\n/", $this->_eol_code[$this->OS_local], $block);
			if(is_resource($fp)) $out+=fwrite($fp, $block, strlen($block));
			else $out.=$block;
		}
		return $out;
	}

	function _data_write($mode=FTP_ASCII, $fp=NULL) {
		if(is_resource($fp)) $out=0;
		else $out="";
		if(!$this->_passive) {
			$this->SendMSG("Only passive connections available!");
			return FALSE;
		}
		if(is_resource($fp)) {
			while(!feof($fp)) {
				$block=fread($fp, $this->_ftp_buff_size);
				if(!$this->_data_write_block($mode, $block)) return false;
			}
		} elseif(!$this->_data_write_block($mode, $fp)) return false;
		return TRUE;
	}

	function _data_write_block($mode, $block) {
		if($mode!=FTP_BINARY) $block=preg_replace("/\r\n|\r|\n/", $this->_eol_code[$this->OS_remote], $block);
		do {
			if(($t=@fwrite($this->_ftp_data_sock, $block))===FALSE) {
				$this->PushError("_data_write","Can't write to socket");
				return FALSE;
			}
			$block=substr($block, $t);
		} while(!empty($block));
		return true;
	}

	function _data_close() {
		@fclose($this->_ftp_data_sock);
		$this->SendMSG("Disconnected data from remote host");
		return TRUE;
	}

	function _quit($force=FALSE) {
		if($this->_connected or $force) {
			@fclose($this->_ftp_control_sock);
			$this->_connected=false;
			$this->SendMSG("Socket closed");
		}
	}
}

?>
class-wp-internal-pointers.php000060400000010741151327343360012467 0ustar00<?php
/**
 * Administration API: WP_Internal_Pointers class
 *
 * @package WordPress
 * @subpackage Administration
 * @since 4.4.0
 */

/**
 * Core class used to implement an internal admin pointers API.
 *
 * @since 3.3.0
 */
#[AllowDynamicProperties]
final class WP_Internal_Pointers {
	/**
	 * Initializes the new feature pointers.
	 *
	 * @since 3.3.0
	 *
	 * All pointers can be disabled using the following:
	 *     remove_action( 'admin_enqueue_scripts', array( 'WP_Internal_Pointers', 'enqueue_scripts' ) );
	 *
	 * Individual pointers (e.g. wp390_widgets) can be disabled using the following:
	 *
	 *    function yourprefix_remove_pointers() {
	 *        remove_action(
	 *            'admin_print_footer_scripts',
	 *            array( 'WP_Internal_Pointers', 'pointer_wp390_widgets' )
	 *        );
	 *    }
	 *    add_action( 'admin_enqueue_scripts', 'yourprefix_remove_pointers', 11 );
	 *
	 * @param string $hook_suffix The current admin page.
	 */
	public static function enqueue_scripts( $hook_suffix ) {
		/*
		 * Register feature pointers
		 *
		 * Format:
		 *     array(
		 *         hook_suffix => pointer callback
		 *     )
		 *
		 * Example:
		 *     array(
		 *         'themes.php' => 'wp390_widgets'
		 *     )
		 */
		$registered_pointers = array(
			// None currently.
		);

		// Check if screen related pointer is registered.
		if ( empty( $registered_pointers[ $hook_suffix ] ) ) {
			return;
		}

		$pointers = (array) $registered_pointers[ $hook_suffix ];

		/*
		 * Specify required capabilities for feature pointers
		 *
		 * Format:
		 *     array(
		 *         pointer callback => Array of required capabilities
		 *     )
		 *
		 * Example:
		 *     array(
		 *         'wp390_widgets' => array( 'edit_theme_options' )
		 *     )
		 */
		$caps_required = array(
			// None currently.
		);

		// Get dismissed pointers.
		$dismissed = explode( ',', (string) get_user_meta( get_current_user_id(), 'dismissed_wp_pointers', true ) );

		$got_pointers = false;
		foreach ( array_diff( $pointers, $dismissed ) as $pointer ) {
			if ( isset( $caps_required[ $pointer ] ) ) {
				foreach ( $caps_required[ $pointer ] as $cap ) {
					if ( ! current_user_can( $cap ) ) {
						continue 2;
					}
				}
			}

			// Bind pointer print function.
			add_action( 'admin_print_footer_scripts', array( 'WP_Internal_Pointers', 'pointer_' . $pointer ) );
			$got_pointers = true;
		}

		if ( ! $got_pointers ) {
			return;
		}

		// Add pointers script and style to queue.
		wp_enqueue_style( 'wp-pointer' );
		wp_enqueue_script( 'wp-pointer' );
	}

	/**
	 * Prints the pointer JavaScript data.
	 *
	 * @since 3.3.0
	 *
	 * @param string $pointer_id The pointer ID.
	 * @param string $selector The HTML elements, on which the pointer should be attached.
	 * @param array  $args Arguments to be passed to the pointer JS (see wp-pointer.js).
	 */
	private static function print_js( $pointer_id, $selector, $args ) {
		if ( empty( $pointer_id ) || empty( $selector ) || empty( $args ) || empty( $args['content'] ) ) {
			return;
		}

		?>
		<script type="text/javascript">
		(function($){
			var options = <?php echo wp_json_encode( $args ); ?>, setup;

			if ( ! options )
				return;

			options = $.extend( options, {
				close: function() {
					$.post( ajaxurl, {
						pointer: '<?php echo $pointer_id; ?>',
						action: 'dismiss-wp-pointer'
					});
				}
			});

			setup = function() {
				$('<?php echo $selector; ?>').first().pointer( options ).pointer('open');
			};

			if ( options.position && options.position.defer_loading )
				$(window).bind( 'load.wp-pointers', setup );
			else
				$( function() {
					setup();
				} );

		})( jQuery );
		</script>
		<?php
	}

	public static function pointer_wp330_toolbar() {}
	public static function pointer_wp330_media_uploader() {}
	public static function pointer_wp330_saving_widgets() {}
	public static function pointer_wp340_customize_current_theme_link() {}
	public static function pointer_wp340_choose_image_from_library() {}
	public static function pointer_wp350_media() {}
	public static function pointer_wp360_revisions() {}
	public static function pointer_wp360_locks() {}
	public static function pointer_wp390_widgets() {}
	public static function pointer_wp410_dfw() {}
	public static function pointer_wp496_privacy() {}

	/**
	 * Prevents new users from seeing existing 'new feature' pointers.
	 *
	 * @since 3.3.0
	 *
	 * @param int $user_id User ID.
	 */
	public static function dismiss_pointers_for_new_users( $user_id ) {
		add_user_meta( $user_id, 'dismissed_wp_pointers', '' );
	}
}
class-ftp.php000064400000065352151327343360007173 0ustar00<?php
/**
 * PemFTP - An Ftp implementation in pure PHP
 *
 * @package PemFTP
 * @since 2.5.0
 *
 * @version 1.0
 * @copyright Alexey Dotsenko
 * @author Alexey Dotsenko
 * @link https://www.phpclasses.org/package/1743-PHP-FTP-client-in-pure-PHP.html
 * @license LGPL https://opensource.org/licenses/lgpl-license.html
 */

/**
 * Defines the newline characters, if not defined already.
 *
 * This can be redefined.
 *
 * @since 2.5.0
 * @var string
 */
if ( ! defined( 'CRLF' ) ) {
	define( 'CRLF', "\r\n" );
}

/**
 * Sets whatever to autodetect ASCII mode.
 *
 * This can be redefined.
 *
 * @since 2.5.0
 * @var int
 */
if ( ! defined( 'FTP_AUTOASCII' ) ) {
	define( 'FTP_AUTOASCII', -1 );
}

/**
 *
 * This can be redefined.
 * @since 2.5.0
 * @var int
 */
if ( ! defined( 'FTP_BINARY' ) ) {
	define( 'FTP_BINARY', 1 );
}

/**
 *
 * This can be redefined.
 * @since 2.5.0
 * @var int
 */
if ( ! defined( 'FTP_ASCII' ) ) {
	define( 'FTP_ASCII', 0 );
}

/**
 * Whether to force FTP.
 *
 * This can be redefined.
 *
 * @since 2.5.0
 * @var bool
 */
if ( ! defined( 'FTP_FORCE' ) ) {
	define( 'FTP_FORCE', true );
}

/**
 * @since 2.5.0
 * @var string
 */
define('FTP_OS_Unix','u');

/**
 * @since 2.5.0
 * @var string
 */
define('FTP_OS_Windows','w');

/**
 * @since 2.5.0
 * @var string
 */
define('FTP_OS_Mac','m');

/**
 * PemFTP base class
 *
 */
class ftp_base {
	/* Public variables */
	var $LocalEcho;
	var $Verbose;
	var $OS_local;
	var $OS_remote;

	/* Private variables */
	var $_lastaction;
	var $_errors;
	var $_type;
	var $_umask;
	var $_timeout;
	var $_passive;
	var $_host;
	var $_fullhost;
	var $_port;
	var $_datahost;
	var $_dataport;
	var $_ftp_control_sock;
	var $_ftp_data_sock;
	var $_ftp_temp_sock;
	var $_ftp_buff_size;
	var $_login;
	var $_password;
	var $_connected;
	var $_ready;
	var $_code;
	var $_message;
	var $_can_restore;
	var $_port_available;
	var $_curtype;
	var $_features;

	var $_error_array;
	var $AuthorizedTransferMode;
	var $OS_FullName;
	var $_eol_code;
	var $AutoAsciiExt;

	/* Constructor */
	function __construct($port_mode=FALSE, $verb=FALSE, $le=FALSE) {
		$this->LocalEcho=$le;
		$this->Verbose=$verb;
		$this->_lastaction=NULL;
		$this->_error_array=array();
		$this->_eol_code=array(FTP_OS_Unix=>"\n", FTP_OS_Mac=>"\r", FTP_OS_Windows=>"\r\n");
		$this->AuthorizedTransferMode=array(FTP_AUTOASCII, FTP_ASCII, FTP_BINARY);
		$this->OS_FullName=array(FTP_OS_Unix => 'UNIX', FTP_OS_Windows => 'WINDOWS', FTP_OS_Mac => 'MACOS');
		$this->AutoAsciiExt=array("ASP","BAT","C","CPP","CSS","CSV","JS","H","HTM","HTML","SHTML","INI","LOG","PHP3","PHTML","PL","PERL","SH","SQL","TXT");
		$this->_port_available=($port_mode==TRUE);
		$this->SendMSG("Staring FTP client class".($this->_port_available?"":" without PORT mode support"));
		$this->_connected=FALSE;
		$this->_ready=FALSE;
		$this->_can_restore=FALSE;
		$this->_code=0;
		$this->_message="";
		$this->_ftp_buff_size=4096;
		$this->_curtype=NULL;
		$this->SetUmask(0022);
		$this->SetType(FTP_AUTOASCII);
		$this->SetTimeout(30);
		$this->Passive(!$this->_port_available);
		$this->_login="anonymous";
		$this->_password="anon@ftp.com";
		$this->_features=array();
	    $this->OS_local=FTP_OS_Unix;
		$this->OS_remote=FTP_OS_Unix;
		$this->features=array();
		if(strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') $this->OS_local=FTP_OS_Windows;
		elseif(strtoupper(substr(PHP_OS, 0, 3)) === 'MAC') $this->OS_local=FTP_OS_Mac;
	}

	function ftp_base($port_mode=FALSE) {
		$this->__construct($port_mode);
	}

// <!-- --------------------------------------------------------------------------------------- -->
// <!--       Public functions                                                                  -->
// <!-- --------------------------------------------------------------------------------------- -->

	function parselisting($line) {
		$is_windows = ($this->OS_remote == FTP_OS_Windows);
		if ($is_windows && preg_match("/([0-9]{2})-([0-9]{2})-([0-9]{2}) +([0-9]{2}):([0-9]{2})(AM|PM) +([0-9]+|<DIR>) +(.+)/",$line,$lucifer)) {
			$b = array();
			if ($lucifer[3]<70) { $lucifer[3]+=2000; } else { $lucifer[3]+=1900; } // 4digit year fix
			$b['isdir'] = ($lucifer[7]=="<DIR>");
			if ( $b['isdir'] )
				$b['type'] = 'd';
			else
				$b['type'] = 'f';
			$b['size'] = $lucifer[7];
			$b['month'] = $lucifer[1];
			$b['day'] = $lucifer[2];
			$b['year'] = $lucifer[3];
			$b['hour'] = $lucifer[4];
			$b['minute'] = $lucifer[5];
			$b['time'] = @mktime($lucifer[4]+(strcasecmp($lucifer[6],"PM")==0?12:0),$lucifer[5],0,$lucifer[1],$lucifer[2],$lucifer[3]);
			$b['am/pm'] = $lucifer[6];
			$b['name'] = $lucifer[8];
		} else if (!$is_windows && $lucifer=preg_split("/[ ]/",$line,9,PREG_SPLIT_NO_EMPTY)) {
			//echo $line."\n";
			$lcount=count($lucifer);
			if ($lcount<8) return '';
			$b = array();
			$b['isdir'] = $lucifer[0][0] === "d";
			$b['islink'] = $lucifer[0][0] === "l";
			if ( $b['isdir'] )
				$b['type'] = 'd';
			elseif ( $b['islink'] )
				$b['type'] = 'l';
			else
				$b['type'] = 'f';
			$b['perms'] = $lucifer[0];
			$b['number'] = $lucifer[1];
			$b['owner'] = $lucifer[2];
			$b['group'] = $lucifer[3];
			$b['size'] = $lucifer[4];
			if ($lcount==8) {
				sscanf($lucifer[5],"%d-%d-%d",$b['year'],$b['month'],$b['day']);
				sscanf($lucifer[6],"%d:%d",$b['hour'],$b['minute']);
				$b['time'] = @mktime($b['hour'],$b['minute'],0,$b['month'],$b['day'],$b['year']);
				$b['name'] = $lucifer[7];
			} else {
				$b['month'] = $lucifer[5];
				$b['day'] = $lucifer[6];
				if (preg_match("/([0-9]{2}):([0-9]{2})/",$lucifer[7],$l2)) {
					$b['year'] = gmdate("Y");
					$b['hour'] = $l2[1];
					$b['minute'] = $l2[2];
				} else {
					$b['year'] = $lucifer[7];
					$b['hour'] = 0;
					$b['minute'] = 0;
				}
				$b['time'] = strtotime(sprintf("%d %s %d %02d:%02d",$b['day'],$b['month'],$b['year'],$b['hour'],$b['minute']));
				$b['name'] = $lucifer[8];
			}
		}

		return $b;
	}

	function SendMSG($message = "", $crlf=true) {
		if ($this->Verbose) {
			echo $message.($crlf?CRLF:"");
			flush();
		}
		return TRUE;
	}

	function SetType($mode=FTP_AUTOASCII) {
		if(!in_array($mode, $this->AuthorizedTransferMode)) {
			$this->SendMSG("Wrong type");
			return FALSE;
		}
		$this->_type=$mode;
		$this->SendMSG("Transfer type: ".($this->_type==FTP_BINARY?"binary":($this->_type==FTP_ASCII?"ASCII":"auto ASCII") ) );
		return TRUE;
	}

	function _settype($mode=FTP_ASCII) {
		if($this->_ready) {
			if($mode==FTP_BINARY) {
				if($this->_curtype!=FTP_BINARY) {
					if(!$this->_exec("TYPE I", "SetType")) return FALSE;
					$this->_curtype=FTP_BINARY;
				}
			} elseif($this->_curtype!=FTP_ASCII) {
				if(!$this->_exec("TYPE A", "SetType")) return FALSE;
				$this->_curtype=FTP_ASCII;
			}
		} else return FALSE;
		return TRUE;
	}

	function Passive($pasv=NULL) {
		if(is_null($pasv)) $this->_passive=!$this->_passive;
		else $this->_passive=$pasv;
		if(!$this->_port_available and !$this->_passive) {
			$this->SendMSG("Only passive connections available!");
			$this->_passive=TRUE;
			return FALSE;
		}
		$this->SendMSG("Passive mode ".($this->_passive?"on":"off"));
		return TRUE;
	}

	function SetServer($host, $port=21, $reconnect=true) {
		if(!is_long($port)) {
	        $this->verbose=true;
    	    $this->SendMSG("Incorrect port syntax");
			return FALSE;
		} else {
			$ip=@gethostbyname($host);
	        $dns=@gethostbyaddr($host);
	        if(!$ip) $ip=$host;
	        if(!$dns) $dns=$host;
	        // Validate the IPAddress PHP4 returns -1 for invalid, PHP5 false
	        // -1 === "255.255.255.255" which is the broadcast address which is also going to be invalid
	        $ipaslong = ip2long($ip);
			if ( ($ipaslong == false) || ($ipaslong === -1) ) {
				$this->SendMSG("Wrong host name/address \"".$host."\"");
				return FALSE;
			}
	        $this->_host=$ip;
	        $this->_fullhost=$dns;
	        $this->_port=$port;
	        $this->_dataport=$port-1;
		}
		$this->SendMSG("Host \"".$this->_fullhost."(".$this->_host."):".$this->_port."\"");
		if($reconnect){
			if($this->_connected) {
				$this->SendMSG("Reconnecting");
				if(!$this->quit(FTP_FORCE)) return FALSE;
				if(!$this->connect()) return FALSE;
			}
		}
		return TRUE;
	}

	function SetUmask($umask=0022) {
		$this->_umask=$umask;
		umask($this->_umask);
		$this->SendMSG("UMASK 0".decoct($this->_umask));
		return TRUE;
	}

	function SetTimeout($timeout=30) {
		$this->_timeout=$timeout;
		$this->SendMSG("Timeout ".$this->_timeout);
		if($this->_connected)
			if(!$this->_settimeout($this->_ftp_control_sock)) return FALSE;
		return TRUE;
	}

	function connect($server=NULL) {
		if(!empty($server)) {
			if(!$this->SetServer($server)) return false;
		}
		if($this->_ready) return true;
	    $this->SendMsg('Local OS : '.$this->OS_FullName[$this->OS_local]);
		if(!($this->_ftp_control_sock = $this->_connect($this->_host, $this->_port))) {
			$this->SendMSG("Error : Cannot connect to remote host \"".$this->_fullhost." :".$this->_port."\"");
			return FALSE;
		}
		$this->SendMSG("Connected to remote host \"".$this->_fullhost.":".$this->_port."\". Waiting for greeting.");
		do {
			if(!$this->_readmsg()) return FALSE;
			if(!$this->_checkCode()) return FALSE;
			$this->_lastaction=time();
		} while($this->_code<200);
		$this->_ready=true;
		$syst=$this->systype();
		if(!$syst) $this->SendMSG("Cannot detect remote OS");
		else {
			if(preg_match("/win|dos|novell/i", $syst[0])) $this->OS_remote=FTP_OS_Windows;
			elseif(preg_match("/os/i", $syst[0])) $this->OS_remote=FTP_OS_Mac;
			elseif(preg_match("/(li|u)nix/i", $syst[0])) $this->OS_remote=FTP_OS_Unix;
			else $this->OS_remote=FTP_OS_Mac;
			$this->SendMSG("Remote OS: ".$this->OS_FullName[$this->OS_remote]);
		}
		if(!$this->features()) $this->SendMSG("Cannot get features list. All supported - disabled");
		else $this->SendMSG("Supported features: ".implode(", ", array_keys($this->_features)));
		return TRUE;
	}

	function quit($force=false) {
		if($this->_ready) {
			if(!$this->_exec("QUIT") and !$force) return FALSE;
			if(!$this->_checkCode() and !$force) return FALSE;
			$this->_ready=false;
			$this->SendMSG("Session finished");
		}
		$this->_quit();
		return TRUE;
	}

	function login($user=NULL, $pass=NULL) {
		if(!is_null($user)) $this->_login=$user;
		else $this->_login="anonymous";
		if(!is_null($pass)) $this->_password=$pass;
		else $this->_password="anon@anon.com";
		if(!$this->_exec("USER ".$this->_login, "login")) return FALSE;
		if(!$this->_checkCode()) return FALSE;
		if($this->_code!=230) {
			if(!$this->_exec((($this->_code==331)?"PASS ":"ACCT ").$this->_password, "login")) return FALSE;
			if(!$this->_checkCode()) return FALSE;
		}
		$this->SendMSG("Authentication succeeded");
		if(empty($this->_features)) {
			if(!$this->features()) $this->SendMSG("Cannot get features list. All supported - disabled");
			else $this->SendMSG("Supported features: ".implode(", ", array_keys($this->_features)));
		}
		return TRUE;
	}

	function pwd() {
		if(!$this->_exec("PWD", "pwd")) return FALSE;
		if(!$this->_checkCode()) return FALSE;
		return preg_replace("/^[0-9]{3} \"(.+)\".*$/s", "\\1", $this->_message);
	}

	function cdup() {
		if(!$this->_exec("CDUP", "cdup")) return FALSE;
		if(!$this->_checkCode()) return FALSE;
		return true;
	}

	function chdir($pathname) {
		if(!$this->_exec("CWD ".$pathname, "chdir")) return FALSE;
		if(!$this->_checkCode()) return FALSE;
		return TRUE;
	}

	function rmdir($pathname) {
		if(!$this->_exec("RMD ".$pathname, "rmdir")) return FALSE;
		if(!$this->_checkCode()) return FALSE;
		return TRUE;
	}

	function mkdir($pathname) {
		if(!$this->_exec("MKD ".$pathname, "mkdir")) return FALSE;
		if(!$this->_checkCode()) return FALSE;
		return TRUE;
	}

	function rename($from, $to) {
		if(!$this->_exec("RNFR ".$from, "rename")) return FALSE;
		if(!$this->_checkCode()) return FALSE;
		if($this->_code==350) {
			if(!$this->_exec("RNTO ".$to, "rename")) return FALSE;
			if(!$this->_checkCode()) return FALSE;
		} else return FALSE;
		return TRUE;
	}

	function filesize($pathname) {
		if(!isset($this->_features["SIZE"])) {
			$this->PushError("filesize", "not supported by server");
			return FALSE;
		}
		if(!$this->_exec("SIZE ".$pathname, "filesize")) return FALSE;
		if(!$this->_checkCode()) return FALSE;
		return preg_replace("/^[0-9]{3} ([0-9]+).*$/s", "\\1", $this->_message);
	}

	function abort() {
		if(!$this->_exec("ABOR", "abort")) return FALSE;
		if(!$this->_checkCode()) {
			if($this->_code!=426) return FALSE;
			if(!$this->_readmsg("abort")) return FALSE;
			if(!$this->_checkCode()) return FALSE;
		}
		return true;
	}

	function mdtm($pathname) {
		if(!isset($this->_features["MDTM"])) {
			$this->PushError("mdtm", "not supported by server");
			return FALSE;
		}
		if(!$this->_exec("MDTM ".$pathname, "mdtm")) return FALSE;
		if(!$this->_checkCode()) return FALSE;
		$mdtm = preg_replace("/^[0-9]{3} ([0-9]+).*$/s", "\\1", $this->_message);
		$date = sscanf($mdtm, "%4d%2d%2d%2d%2d%2d");
		$timestamp = mktime($date[3], $date[4], $date[5], $date[1], $date[2], $date[0]);
		return $timestamp;
	}

	function systype() {
		if(!$this->_exec("SYST", "systype")) return FALSE;
		if(!$this->_checkCode()) return FALSE;
		$DATA = explode(" ", $this->_message);
		return array($DATA[1], $DATA[3]);
	}

	function delete($pathname) {
		if(!$this->_exec("DELE ".$pathname, "delete")) return FALSE;
		if(!$this->_checkCode()) return FALSE;
		return TRUE;
	}

	function site($command, $fnction="site") {
		if(!$this->_exec("SITE ".$command, $fnction)) return FALSE;
		if(!$this->_checkCode()) return FALSE;
		return TRUE;
	}

	function chmod($pathname, $mode) {
		if(!$this->site( sprintf('CHMOD %o %s', $mode, $pathname), "chmod")) return FALSE;
		return TRUE;
	}

	function restore($from) {
		if(!isset($this->_features["REST"])) {
			$this->PushError("restore", "not supported by server");
			return FALSE;
		}
		if($this->_curtype!=FTP_BINARY) {
			$this->PushError("restore", "cannot restore in ASCII mode");
			return FALSE;
		}
		if(!$this->_exec("REST ".$from, "restore")) return FALSE;
		if(!$this->_checkCode()) return FALSE;
		return TRUE;
	}

	function features() {
		if(!$this->_exec("FEAT", "features")) return FALSE;
		if(!$this->_checkCode()) return FALSE;
		$f=preg_split("/[".CRLF."]+/", preg_replace("/[0-9]{3}[ -].*[".CRLF."]+/", "", $this->_message), -1, PREG_SPLIT_NO_EMPTY);
		$this->_features=array();
		foreach($f as $k=>$v) {
			$v=explode(" ", trim($v));
			$this->_features[array_shift($v)]=$v;
		}
		return true;
	}

	function rawlist($pathname="", $arg="") {
		return $this->_list(($arg?" ".$arg:"").($pathname?" ".$pathname:""), "LIST", "rawlist");
	}

	function nlist($pathname="", $arg="") {
		return $this->_list(($arg?" ".$arg:"").($pathname?" ".$pathname:""), "NLST", "nlist");
	}

	function is_exists($pathname) {
		return $this->file_exists($pathname);
	}

	function file_exists($pathname) {
		$exists=true;
		if(!$this->_exec("RNFR ".$pathname, "rename")) $exists=FALSE;
		else {
			if(!$this->_checkCode()) $exists=FALSE;
			$this->abort();
		}
		if($exists) $this->SendMSG("Remote file ".$pathname." exists");
		else $this->SendMSG("Remote file ".$pathname." does not exist");
		return $exists;
	}

	function fget($fp, $remotefile, $rest=0) {
		if($this->_can_restore and $rest!=0) fseek($fp, $rest);
		$pi=pathinfo($remotefile);
		if($this->_type==FTP_ASCII or ($this->_type==FTP_AUTOASCII and in_array(strtoupper($pi["extension"]), $this->AutoAsciiExt))) $mode=FTP_ASCII;
		else $mode=FTP_BINARY;
		if(!$this->_data_prepare($mode)) {
			return FALSE;
		}
		if($this->_can_restore and $rest!=0) $this->restore($rest);
		if(!$this->_exec("RETR ".$remotefile, "get")) {
			$this->_data_close();
			return FALSE;
		}
		if(!$this->_checkCode()) {
			$this->_data_close();
			return FALSE;
		}
		$out=$this->_data_read($mode, $fp);
		$this->_data_close();
		if(!$this->_readmsg()) return FALSE;
		if(!$this->_checkCode()) return FALSE;
		return $out;
	}

	function get($remotefile, $localfile=NULL, $rest=0) {
		if(is_null($localfile)) $localfile=$remotefile;
		if (@file_exists($localfile)) $this->SendMSG("Warning : local file will be overwritten");
		$fp = @fopen($localfile, "w");
		if (!$fp) {
			$this->PushError("get","cannot open local file", "Cannot create \"".$localfile."\"");
			return FALSE;
		}
		if($this->_can_restore and $rest!=0) fseek($fp, $rest);
		$pi=pathinfo($remotefile);
		if($this->_type==FTP_ASCII or ($this->_type==FTP_AUTOASCII and in_array(strtoupper($pi["extension"]), $this->AutoAsciiExt))) $mode=FTP_ASCII;
		else $mode=FTP_BINARY;
		if(!$this->_data_prepare($mode)) {
			fclose($fp);
			return FALSE;
		}
		if($this->_can_restore and $rest!=0) $this->restore($rest);
		if(!$this->_exec("RETR ".$remotefile, "get")) {
			$this->_data_close();
			fclose($fp);
			return FALSE;
		}
		if(!$this->_checkCode()) {
			$this->_data_close();
			fclose($fp);
			return FALSE;
		}
		$out=$this->_data_read($mode, $fp);
		fclose($fp);
		$this->_data_close();
		if(!$this->_readmsg()) return FALSE;
		if(!$this->_checkCode()) return FALSE;
		return $out;
	}

	function fput($remotefile, $fp, $rest=0) {
		if($this->_can_restore and $rest!=0) fseek($fp, $rest);
		$pi=pathinfo($remotefile);
		if($this->_type==FTP_ASCII or ($this->_type==FTP_AUTOASCII and in_array(strtoupper($pi["extension"]), $this->AutoAsciiExt))) $mode=FTP_ASCII;
		else $mode=FTP_BINARY;
		if(!$this->_data_prepare($mode)) {
			return FALSE;
		}
		if($this->_can_restore and $rest!=0) $this->restore($rest);
		if(!$this->_exec("STOR ".$remotefile, "put")) {
			$this->_data_close();
			return FALSE;
		}
		if(!$this->_checkCode()) {
			$this->_data_close();
			return FALSE;
		}
		$ret=$this->_data_write($mode, $fp);
		$this->_data_close();
		if(!$this->_readmsg()) return FALSE;
		if(!$this->_checkCode()) return FALSE;
		return $ret;
	}

	function put($localfile, $remotefile=NULL, $rest=0) {
		if(is_null($remotefile)) $remotefile=$localfile;
		if (!file_exists($localfile)) {
			$this->PushError("put","cannot open local file", "No such file or directory \"".$localfile."\"");
			return FALSE;
		}
		$fp = @fopen($localfile, "r");

		if (!$fp) {
			$this->PushError("put","cannot open local file", "Cannot read file \"".$localfile."\"");
			return FALSE;
		}
		if($this->_can_restore and $rest!=0) fseek($fp, $rest);
		$pi=pathinfo($localfile);
		if($this->_type==FTP_ASCII or ($this->_type==FTP_AUTOASCII and in_array(strtoupper($pi["extension"]), $this->AutoAsciiExt))) $mode=FTP_ASCII;
		else $mode=FTP_BINARY;
		if(!$this->_data_prepare($mode)) {
			fclose($fp);
			return FALSE;
		}
		if($this->_can_restore and $rest!=0) $this->restore($rest);
		if(!$this->_exec("STOR ".$remotefile, "put")) {
			$this->_data_close();
			fclose($fp);
			return FALSE;
		}
		if(!$this->_checkCode()) {
			$this->_data_close();
			fclose($fp);
			return FALSE;
		}
		$ret=$this->_data_write($mode, $fp);
		fclose($fp);
		$this->_data_close();
		if(!$this->_readmsg()) return FALSE;
		if(!$this->_checkCode()) return FALSE;
		return $ret;
	}

	function mput($local=".", $remote=NULL, $continious=false) {
		$local=realpath($local);
		if(!@file_exists($local)) {
			$this->PushError("mput","cannot open local folder", "Cannot stat folder \"".$local."\"");
			return FALSE;
		}
		if(!is_dir($local)) return $this->put($local, $remote);
		if(empty($remote)) $remote=".";
		elseif(!$this->file_exists($remote) and !$this->mkdir($remote)) return FALSE;
		if($handle = opendir($local)) {
			$list=array();
			while (false !== ($file = readdir($handle))) {
				if ($file != "." && $file != "..") $list[]=$file;
			}
			closedir($handle);
		} else {
			$this->PushError("mput","cannot open local folder", "Cannot read folder \"".$local."\"");
			return FALSE;
		}
		if(empty($list)) return TRUE;
		$ret=true;
		foreach($list as $el) {
			if(is_dir($local."/".$el)) $t=$this->mput($local."/".$el, $remote."/".$el);
			else $t=$this->put($local."/".$el, $remote."/".$el);
			if(!$t) {
				$ret=FALSE;
				if(!$continious) break;
			}
		}
		return $ret;

	}

	function mget($remote, $local=".", $continious=false) {
		$list=$this->rawlist($remote, "-lA");
		if($list===false) {
			$this->PushError("mget","cannot read remote folder list", "Cannot read remote folder \"".$remote."\" contents");
			return FALSE;
		}
		if(empty($list)) return true;
		if(!@file_exists($local)) {
			if(!@mkdir($local)) {
				$this->PushError("mget","cannot create local folder", "Cannot create folder \"".$local."\"");
				return FALSE;
			}
		}
		foreach($list as $k=>$v) {
			$list[$k]=$this->parselisting($v);
			if( ! $list[$k] or $list[$k]["name"]=="." or $list[$k]["name"]=="..") unset($list[$k]);
		}
		$ret=true;
		foreach($list as $el) {
			if($el["type"]=="d") {
				if(!$this->mget($remote."/".$el["name"], $local."/".$el["name"], $continious)) {
					$this->PushError("mget", "cannot copy folder", "Cannot copy remote folder \"".$remote."/".$el["name"]."\" to local \"".$local."/".$el["name"]."\"");
					$ret=false;
					if(!$continious) break;
				}
			} else {
				if(!$this->get($remote."/".$el["name"], $local."/".$el["name"])) {
					$this->PushError("mget", "cannot copy file", "Cannot copy remote file \"".$remote."/".$el["name"]."\" to local \"".$local."/".$el["name"]."\"");
					$ret=false;
					if(!$continious) break;
				}
			}
			@chmod($local."/".$el["name"], $el["perms"]);
			$t=strtotime($el["date"]);
			if($t!==-1 and $t!==false) @touch($local."/".$el["name"], $t);
		}
		return $ret;
	}

	function mdel($remote, $continious=false) {
		$list=$this->rawlist($remote, "-la");
		if($list===false) {
			$this->PushError("mdel","cannot read remote folder list", "Cannot read remote folder \"".$remote."\" contents");
			return false;
		}

		foreach($list as $k=>$v) {
			$list[$k]=$this->parselisting($v);
			if( ! $list[$k] or $list[$k]["name"]=="." or $list[$k]["name"]=="..") unset($list[$k]);
		}
		$ret=true;

		foreach($list as $el) {
			if ( empty($el) )
				continue;

			if($el["type"]=="d") {
				if(!$this->mdel($remote."/".$el["name"], $continious)) {
					$ret=false;
					if(!$continious) break;
				}
			} else {
				if (!$this->delete($remote."/".$el["name"])) {
					$this->PushError("mdel", "cannot delete file", "Cannot delete remote file \"".$remote."/".$el["name"]."\"");
					$ret=false;
					if(!$continious) break;
				}
			}
		}

		if(!$this->rmdir($remote)) {
			$this->PushError("mdel", "cannot delete folder", "Cannot delete remote folder \"".$remote."/".$el["name"]."\"");
			$ret=false;
		}
		return $ret;
	}

	function mmkdir($dir, $mode = 0777) {
		if(empty($dir)) return FALSE;
		if($this->is_exists($dir) or $dir == "/" ) return TRUE;
		if(!$this->mmkdir(dirname($dir), $mode)) return false;
		$r=$this->mkdir($dir, $mode);
		$this->chmod($dir,$mode);
		return $r;
	}

	function glob($pattern, $handle=NULL) {
		$path=$output=null;
		if(PHP_OS=='WIN32') $slash='\\';
		else $slash='/';
		$lastpos=strrpos($pattern,$slash);
		if(!($lastpos===false)) {
			$path=substr($pattern,0,-$lastpos-1);
			$pattern=substr($pattern,$lastpos);
		} else $path=getcwd();
		if(is_array($handle) and !empty($handle)) {
			foreach($handle as $dir) {
				if($this->glob_pattern_match($pattern,$dir))
				$output[]=$dir;
			}
		} else {
			$handle=@opendir($path);
			if($handle===false) return false;
			while($dir=readdir($handle)) {
				if($this->glob_pattern_match($pattern,$dir))
				$output[]=$dir;
			}
			closedir($handle);
		}
		if(is_array($output)) return $output;
		return false;
	}

	function glob_pattern_match($pattern,$subject) {
		$out=null;
		$chunks=explode(';',$pattern);
		foreach($chunks as $pattern) {
			$escape=array('$','^','.','{','}','(',')','[',']','|');
			while(str_contains($pattern,'**'))
				$pattern=str_replace('**','*',$pattern);
			foreach($escape as $probe)
				$pattern=str_replace($probe,"\\$probe",$pattern);
			$pattern=str_replace('?*','*',
				str_replace('*?','*',
					str_replace('*',".*",
						str_replace('?','.{1,1}',$pattern))));
			$out[]=$pattern;
		}
		if(count($out)==1) return($this->glob_regexp("^$out[0]$",$subject));
		else {
			foreach($out as $tester)
				// TODO: This should probably be glob_regexp(), but needs tests.
				if($this->my_regexp("^$tester$",$subject)) return true;
		}
		return false;
	}

	function glob_regexp($pattern,$subject) {
		$sensitive=(PHP_OS!='WIN32');
		return ($sensitive?
			preg_match( '/' . preg_quote( $pattern, '/' ) . '/', $subject ) :
			preg_match( '/' . preg_quote( $pattern, '/' ) . '/i', $subject )
		);
	}

	function dirlist($remote) {
		$list=$this->rawlist($remote, "-la");
		if($list===false) {
			$this->PushError("dirlist","cannot read remote folder list", "Cannot read remote folder \"".$remote."\" contents");
			return false;
		}

		$dirlist = array();
		foreach($list as $k=>$v) {
			$entry=$this->parselisting($v);
			if ( empty($entry) )
				continue;

			if($entry["name"]=="." or $entry["name"]=="..")
				continue;

			$dirlist[$entry['name']] = $entry;
		}

		return $dirlist;
	}
// <!-- --------------------------------------------------------------------------------------- -->
// <!--       Private functions                                                                 -->
// <!-- --------------------------------------------------------------------------------------- -->
	function _checkCode() {
		return ($this->_code<400 and $this->_code>0);
	}

	function _list($arg="", $cmd="LIST", $fnction="_list") {
		if(!$this->_data_prepare()) return false;
		if(!$this->_exec($cmd.$arg, $fnction)) {
			$this->_data_close();
			return FALSE;
		}
		if(!$this->_checkCode()) {
			$this->_data_close();
			return FALSE;
		}
		$out="";
		if($this->_code<200) {
			$out=$this->_data_read();
			$this->_data_close();
			if(!$this->_readmsg()) return FALSE;
			if(!$this->_checkCode()) return FALSE;
			if($out === FALSE ) return FALSE;
			$out=preg_split("/[".CRLF."]+/", $out, -1, PREG_SPLIT_NO_EMPTY);
//			$this->SendMSG(implode($this->_eol_code[$this->OS_local], $out));
		}
		return $out;
	}

// <!-- --------------------------------------------------------------------------------------- -->
// <!-- Partie : gestion des erreurs                                                            -->
// <!-- --------------------------------------------------------------------------------------- -->
// Gnre une erreur pour traitement externe  la classe
	function PushError($fctname,$msg,$desc=false){
		$error=array();
		$error['time']=time();
		$error['fctname']=$fctname;
		$error['msg']=$msg;
		$error['desc']=$desc;
		if($desc) $tmp=' ('.$desc.')'; else $tmp='';
		$this->SendMSG($fctname.': '.$msg.$tmp);
		return(array_push($this->_error_array,$error));
	}

// Rcupre une erreur externe
	function PopError(){
		if(count($this->_error_array)) return(array_pop($this->_error_array));
			else return(false);
	}
}

$mod_sockets = extension_loaded( 'sockets' );
if ( ! $mod_sockets && function_exists( 'dl' ) && is_callable( 'dl' ) ) {
	$prefix = ( PHP_SHLIB_SUFFIX == 'dll' ) ? 'php_' : '';
	@dl( $prefix . 'sockets.' . PHP_SHLIB_SUFFIX ); // phpcs:ignore PHPCompatibility.FunctionUse.RemovedFunctions.dlDeprecated
	$mod_sockets = extension_loaded( 'sockets' );
}

require_once __DIR__ . "/class-ftp-" . ( $mod_sockets ? "sockets" : "pure" ) . ".php";

if ( $mod_sockets ) {
	class ftp extends ftp_sockets {}
} else {
	class ftp extends ftp_pure {}
}
credits.php000064400000013356151327343360006731 0ustar00<?php
/**
 * WordPress Credits Administration API.
 *
 * @package WordPress
 * @subpackage Administration
 * @since 4.4.0
 */

/**
 * Retrieves the contributor credits.
 *
 * @since 3.2.0
 * @since 5.6.0 Added the `$version` and `$locale` parameters.
 *
 * @param string $version WordPress version. Defaults to the current version.
 * @param string $locale  WordPress locale. Defaults to the current user's locale.
 * @return array|false A list of all of the contributors, or false on error.
 */
function wp_credits( $version = '', $locale = '' ) {
	if ( ! $version ) {
		$version = wp_get_wp_version();
	}

	if ( ! $locale ) {
		$locale = get_user_locale();
	}

	$results = get_site_transient( 'wordpress_credits_' . $locale );

	if ( ! is_array( $results )
		|| str_contains( $version, '-' )
		|| ( isset( $results['data']['version'] ) && ! str_starts_with( $version, $results['data']['version'] ) )
	) {
		$url     = "http://api.wordpress.org/core/credits/1.1/?version={$version}&locale={$locale}";
		$options = array( 'user-agent' => 'WordPress/' . $version . '; ' . home_url( '/' ) );

		if ( wp_http_supports( array( 'ssl' ) ) ) {
			$url = set_url_scheme( $url, 'https' );
		}

		$response = wp_remote_get( $url, $options );

		if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
			return false;
		}

		$results = json_decode( wp_remote_retrieve_body( $response ), true );

		if ( ! is_array( $results ) ) {
			return false;
		}

		set_site_transient( 'wordpress_credits_' . $locale, $results, DAY_IN_SECONDS );
	}

	return $results;
}

/**
 * Retrieves the link to a contributor's WordPress.org profile page.
 *
 * @access private
 * @since 3.2.0
 *
 * @param string $display_name  The contributor's display name (passed by reference).
 * @param string $username      The contributor's username.
 * @param string $profiles      URL to the contributor's WordPress.org profile page.
 */
function _wp_credits_add_profile_link( &$display_name, $username, $profiles ) {
	$display_name = '<a href="' . esc_url( sprintf( $profiles, $username ) ) . '">' . esc_html( $display_name ) . '</a>';
}

/**
 * Retrieves the link to an external library used in WordPress.
 *
 * @access private
 * @since 3.2.0
 *
 * @param string $data External library data (passed by reference).
 */
function _wp_credits_build_object_link( &$data ) {
	$data = '<a href="' . esc_url( $data[1] ) . '">' . esc_html( $data[0] ) . '</a>';
}

/**
 * Displays the title for a given group of contributors.
 *
 * @since 5.3.0
 *
 * @param array $group_data The current contributor group.
 */
function wp_credits_section_title( $group_data = array() ) {
	if ( ! count( $group_data ) ) {
		return;
	}

	if ( $group_data['name'] ) {
		if ( 'Translators' === $group_data['name'] ) {
			// Considered a special slug in the API response. (Also, will never be returned for en_US.)
			$title = _x( 'Translators', 'Translate this to be the equivalent of English Translators in your language for the credits page Translators section' );
		} elseif ( isset( $group_data['placeholders'] ) ) {
			// phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText
			$title = vsprintf( translate( $group_data['name'] ), $group_data['placeholders'] );
		} else {
			// phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText
			$title = translate( $group_data['name'] );
		}

		echo '<h2 class="wp-people-group-title">' . esc_html( $title ) . "</h2>\n";
	}
}

/**
 * Displays a list of contributors for a given group.
 *
 * @since 5.3.0
 *
 * @param array  $credits The credits groups returned from the API.
 * @param string $slug    The current group to display.
 */
function wp_credits_section_list( $credits = array(), $slug = '' ) {
	$group_data   = isset( $credits['groups'][ $slug ] ) ? $credits['groups'][ $slug ] : array();
	$credits_data = $credits['data'];
	if ( ! count( $group_data ) ) {
		return;
	}

	if ( ! empty( $group_data['shuffle'] ) ) {
		shuffle( $group_data['data'] ); // We were going to sort by ability to pronounce "hierarchical," but that wouldn't be fair to Matt.
	}

	switch ( $group_data['type'] ) {
		case 'list':
			array_walk( $group_data['data'], '_wp_credits_add_profile_link', $credits_data['profiles'] );
			echo '<p class="wp-credits-list">' . wp_sprintf( '%l.', $group_data['data'] ) . "</p>\n\n";
			break;
		case 'libraries':
			array_walk( $group_data['data'], '_wp_credits_build_object_link' );
			echo '<p class="wp-credits-list">' . wp_sprintf( '%l.', $group_data['data'] ) . "</p>\n\n";
			break;
		default:
			$compact = 'compact' === $group_data['type'];
			$classes = 'wp-people-group ' . ( $compact ? 'compact' : '' );
			echo '<ul class="' . $classes . '" id="wp-people-group-' . $slug . '">' . "\n";
			foreach ( $group_data['data'] as $person_data ) {
				echo '<li class="wp-person" id="wp-person-' . esc_attr( $person_data[2] ) . '">' . "\n\t";
				echo '<a href="' . esc_url( sprintf( $credits_data['profiles'], $person_data[2] ) ) . '" class="web">';
				$size   = $compact ? 80 : 160;
				$data   = get_avatar_data( $person_data[1] . '@sha256.gravatar.com', array( 'size' => $size ) );
				$data2x = get_avatar_data( $person_data[1] . '@sha256.gravatar.com', array( 'size' => $size * 2 ) );
				echo '<span class="wp-person-avatar"><img src="' . esc_url( $data['url'] ) . '" srcset="' . esc_url( $data2x['url'] ) . ' 2x" class="gravatar" alt="" /></span>' . "\n";
				echo esc_html( $person_data[0] ) . "</a>\n\t";
				if ( ! $compact && ! empty( $person_data[3] ) ) {
					// phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText
					echo '<span class="title">' . translate( $person_data[3] ) . "</span>\n";
				}
				echo "</li>\n";
			}
			echo "</ul>\n";
			break;
	}
}
class-file-upload-upgrader.php000060400000010103151327343360012366 0ustar00<?php
/**
 * Upgrade API: File_Upload_Upgrader class
 *
 * @package WordPress
 * @subpackage Upgrader
 * @since 4.6.0
 */

/**
 * Core class used for handling file uploads.
 *
 * This class handles the upload process and passes it as if it's a local file
 * to the Upgrade/Installer functions.
 *
 * @since 2.8.0
 * @since 4.6.0 Moved to its own file from wp-admin/includes/class-wp-upgrader.php.
 */
#[AllowDynamicProperties]
class File_Upload_Upgrader {

	/**
	 * The full path to the file package.
	 *
	 * @since 2.8.0
	 * @var string $package
	 */
	public $package;

	/**
	 * The name of the file.
	 *
	 * @since 2.8.0
	 * @var string $filename
	 */
	public $filename;

	/**
	 * The ID of the attachment post for this file.
	 *
	 * @since 3.3.0
	 * @var int $id
	 */
	public $id = 0;

	/**
	 * Construct the upgrader for a form.
	 *
	 * @since 2.8.0
	 *
	 * @param string $form      The name of the form the file was uploaded from.
	 * @param string $urlholder The name of the `GET` parameter that holds the filename.
	 */
	public function __construct( $form, $urlholder ) {

		if ( empty( $_FILES[ $form ]['name'] ) && empty( $_GET[ $urlholder ] ) ) {
			wp_die( __( 'Please select a file' ) );
		}

		// Handle a newly uploaded file. Else, assume it's already been uploaded.
		if ( ! empty( $_FILES ) ) {
			$overrides = array(
				'test_form' => false,
				'test_type' => false,
			);
			$file      = wp_handle_upload( $_FILES[ $form ], $overrides );

			if ( isset( $file['error'] ) ) {
				wp_die( $file['error'] );
			}

			if ( 'pluginzip' === $form || 'themezip' === $form ) {
				if ( ! wp_zip_file_is_valid( $file['file'] ) ) {
					wp_delete_file( $file['file'] );

					if ( 'pluginzip' === $form ) {
						$plugins_page = sprintf(
							'<a href="%s">%s</a>',
							self_admin_url( 'plugin-install.php' ),
							__( 'Return to the Plugin Installer' )
						);
						wp_die( __( 'Incompatible Archive.' ) . '<br />' . $plugins_page );
					}

					if ( 'themezip' === $form ) {
						$themes_page = sprintf(
							'<a href="%s" target="_parent">%s</a>',
							self_admin_url( 'theme-install.php' ),
							__( 'Return to the Theme Installer' )
						);
						wp_die( __( 'Incompatible Archive.' ) . '<br />' . $themes_page );
					}
				}
			}

			$this->filename = $_FILES[ $form ]['name'];
			$this->package  = $file['file'];

			// Construct the attachment array.
			$attachment = array(
				'post_title'     => $this->filename,
				'post_content'   => $file['url'],
				'post_mime_type' => $file['type'],
				'guid'           => $file['url'],
				'context'        => 'upgrader',
				'post_status'    => 'private',
			);

			// Save the data.
			$this->id = wp_insert_attachment( $attachment, $file['file'] );

			// Schedule a cleanup for 2 hours from now in case of failed installation.
			wp_schedule_single_event( time() + 2 * HOUR_IN_SECONDS, 'upgrader_scheduled_cleanup', array( $this->id ) );

		} elseif ( is_numeric( $_GET[ $urlholder ] ) ) {
			// Numeric Package = previously uploaded file, see above.
			$this->id   = (int) $_GET[ $urlholder ];
			$attachment = get_post( $this->id );
			if ( empty( $attachment ) ) {
				wp_die( __( 'Please select a file' ) );
			}

			$this->filename = $attachment->post_title;
			$this->package  = get_attached_file( $attachment->ID );
		} else {
			// Else, It's set to something, Back compat for plugins using the old (pre-3.3) File_Uploader handler.
			$uploads = wp_upload_dir();
			if ( ! ( $uploads && false === $uploads['error'] ) ) {
				wp_die( $uploads['error'] );
			}

			$this->filename = sanitize_file_name( $_GET[ $urlholder ] );
			$this->package  = $uploads['basedir'] . '/' . $this->filename;

			if ( ! str_starts_with( realpath( $this->package ), realpath( $uploads['basedir'] ) ) ) {
				wp_die( __( 'Please select a file' ) );
			}
		}
	}

	/**
	 * Deletes the attachment/uploaded file.
	 *
	 * @since 3.2.2
	 *
	 * @return bool Whether the cleanup was successful.
	 */
	public function cleanup() {
		if ( $this->id ) {
			wp_delete_attachment( $this->id );

		} elseif ( file_exists( $this->package ) ) {
			return @unlink( $this->package );
		}

		return true;
	}
}
ms-admin-filters.php000060400000002420151327343360010431 0ustar00<?php
/**
 * Multisite Administration hooks
 *
 * @package WordPress
 * @subpackage Administration
 * @since 4.3.0
 */

// Media hooks.
add_filter( 'wp_handle_upload_prefilter', 'check_upload_size' );

// User hooks.
add_action( 'user_admin_notices', 'new_user_email_admin_notice' );
add_action( 'network_admin_notices', 'new_user_email_admin_notice' );

add_action( 'admin_page_access_denied', '_access_denied_splash', 99 );

// Site hooks.
add_action( 'wpmueditblogaction', 'upload_space_setting' );

// Network hooks.
add_action( 'update_site_option_admin_email', 'wp_network_admin_email_change_notification', 10, 4 );

// Post hooks.
add_filter( 'wp_insert_post_data', 'avoid_blog_page_permalink_collision', 10, 2 );

// Tools hooks.
add_filter( 'import_allow_create_users', 'check_import_new_users' );

// Notices hooks.
add_action( 'admin_notices', 'site_admin_notice' );
add_action( 'network_admin_notices', 'site_admin_notice' );

// Update hooks.
add_action( 'network_admin_notices', 'update_nag', 3 );
add_action( 'network_admin_notices', 'maintenance_nag', 10 );

// Network Admin hooks.
add_action( 'add_site_option_new_admin_email', 'update_network_option_new_admin_email', 10, 2 );
add_action( 'update_site_option_new_admin_email', 'update_network_option_new_admin_email', 10, 2 );
comment.php000060400000013751151327343360006731 0ustar00<?php
/**
 * WordPress Comment Administration API.
 *
 * @package WordPress
 * @subpackage Administration
 * @since 2.3.0
 */

/**
 * Determines if a comment exists based on author and date.
 *
 * For best performance, use `$timezone = 'gmt'`, which queries a field that is properly indexed. The default value
 * for `$timezone` is 'blog' for legacy reasons.
 *
 * @since 2.0.0
 * @since 4.4.0 Added the `$timezone` parameter.
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param string $comment_author Author of the comment.
 * @param string $comment_date   Date of the comment.
 * @param string $timezone       Timezone. Accepts 'blog' or 'gmt'. Default 'blog'.
 * @return string|null Comment post ID on success.
 */
function comment_exists( $comment_author, $comment_date, $timezone = 'blog' ) {
	global $wpdb;

	$date_field = 'comment_date';
	if ( 'gmt' === $timezone ) {
		$date_field = 'comment_date_gmt';
	}

	return $wpdb->get_var(
		$wpdb->prepare(
			"SELECT comment_post_ID FROM $wpdb->comments
			WHERE comment_author = %s AND $date_field = %s",
			stripslashes( $comment_author ),
			stripslashes( $comment_date )
		)
	);
}

/**
 * Updates a comment with values provided in $_POST.
 *
 * @since 2.0.0
 * @since 5.5.0 A return value was added.
 *
 * @return int|WP_Error The value 1 if the comment was updated, 0 if not updated.
 *                      A WP_Error object on failure.
 */
function edit_comment() {
	if ( ! current_user_can( 'edit_comment', (int) $_POST['comment_ID'] ) ) {
		wp_die( __( 'Sorry, you are not allowed to edit comments on this post.' ) );
	}

	if ( isset( $_POST['newcomment_author'] ) ) {
		$_POST['comment_author'] = $_POST['newcomment_author'];
	}
	if ( isset( $_POST['newcomment_author_email'] ) ) {
		$_POST['comment_author_email'] = $_POST['newcomment_author_email'];
	}
	if ( isset( $_POST['newcomment_author_url'] ) ) {
		$_POST['comment_author_url'] = $_POST['newcomment_author_url'];
	}
	if ( isset( $_POST['comment_status'] ) ) {
		$_POST['comment_approved'] = $_POST['comment_status'];
	}
	if ( isset( $_POST['content'] ) ) {
		$_POST['comment_content'] = $_POST['content'];
	}
	if ( isset( $_POST['comment_ID'] ) ) {
		$_POST['comment_ID'] = (int) $_POST['comment_ID'];
	}

	foreach ( array( 'aa', 'mm', 'jj', 'hh', 'mn' ) as $timeunit ) {
		if ( ! empty( $_POST[ 'hidden_' . $timeunit ] ) && $_POST[ 'hidden_' . $timeunit ] !== $_POST[ $timeunit ] ) {
			$_POST['edit_date'] = '1';
			break;
		}
	}

	if ( ! empty( $_POST['edit_date'] ) ) {
		$aa = $_POST['aa'];
		$mm = $_POST['mm'];
		$jj = $_POST['jj'];
		$hh = $_POST['hh'];
		$mn = $_POST['mn'];
		$ss = $_POST['ss'];
		$jj = ( $jj > 31 ) ? 31 : $jj;
		$hh = ( $hh > 23 ) ? $hh - 24 : $hh;
		$mn = ( $mn > 59 ) ? $mn - 60 : $mn;
		$ss = ( $ss > 59 ) ? $ss - 60 : $ss;

		$_POST['comment_date'] = "$aa-$mm-$jj $hh:$mn:$ss";
	}

	return wp_update_comment( $_POST, true );
}

/**
 * Returns a WP_Comment object based on comment ID.
 *
 * @since 2.0.0
 *
 * @param int $id ID of comment to retrieve.
 * @return WP_Comment|false Comment if found. False on failure.
 */
function get_comment_to_edit( $id ) {
	$comment = get_comment( $id );
	if ( ! $comment ) {
		return false;
	}

	$comment->comment_ID      = (int) $comment->comment_ID;
	$comment->comment_post_ID = (int) $comment->comment_post_ID;

	$comment->comment_content = format_to_edit( $comment->comment_content );
	/**
	 * Filters the comment content before editing.
	 *
	 * @since 2.0.0
	 *
	 * @param string $comment_content Comment content.
	 */
	$comment->comment_content = apply_filters( 'comment_edit_pre', $comment->comment_content );

	$comment->comment_author       = format_to_edit( $comment->comment_author );
	$comment->comment_author_email = format_to_edit( $comment->comment_author_email );
	$comment->comment_author_url   = format_to_edit( $comment->comment_author_url );
	$comment->comment_author_url   = esc_url( $comment->comment_author_url );

	return $comment;
}

/**
 * Gets the number of pending comments on a post or posts.
 *
 * @since 2.3.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param int|int[] $post_id Either a single Post ID or an array of Post IDs
 * @return int|int[] Either a single Posts pending comments as an int or an array of ints keyed on the Post IDs
 */
function get_pending_comments_num( $post_id ) {
	global $wpdb;

	$single = false;
	if ( ! is_array( $post_id ) ) {
		$post_id_array = (array) $post_id;
		$single        = true;
	} else {
		$post_id_array = $post_id;
	}
	$post_id_array = array_map( 'intval', $post_id_array );
	$post_id_in    = "'" . implode( "', '", $post_id_array ) . "'";

	$pending = $wpdb->get_results( "SELECT comment_post_ID, COUNT(comment_ID) as num_comments FROM $wpdb->comments WHERE comment_post_ID IN ( $post_id_in ) AND comment_approved = '0' GROUP BY comment_post_ID", ARRAY_A );

	if ( $single ) {
		if ( empty( $pending ) ) {
			return 0;
		} else {
			return absint( $pending[0]['num_comments'] );
		}
	}

	$pending_keyed = array();

	// Default to zero pending for all posts in request.
	foreach ( $post_id_array as $id ) {
		$pending_keyed[ $id ] = 0;
	}

	if ( ! empty( $pending ) ) {
		foreach ( $pending as $pend ) {
			$pending_keyed[ $pend['comment_post_ID'] ] = absint( $pend['num_comments'] );
		}
	}

	return $pending_keyed;
}

/**
 * Adds avatars to relevant places in admin.
 *
 * @since 2.5.0
 *
 * @param string $name User name.
 * @return string Avatar with the user name.
 */
function floated_admin_avatar( $name ) {
	$avatar = get_avatar( get_comment(), 32, 'mystery' );
	return "$avatar $name";
}

/**
 * Enqueues comment shortcuts jQuery script.
 *
 * @since 2.7.0
 */
function enqueue_comment_hotkeys_js() {
	if ( 'true' === get_user_option( 'comment_shortcuts' ) ) {
		wp_enqueue_script( 'jquery-table-hotkeys' );
	}
}

/**
 * Displays error message at bottom of comments.
 *
 * @param string $msg Error Message. Assumed to contain HTML and be sanitized.
 */
function comment_footer_die( $msg ) {
	echo "<div class='wrap'><p>$msg</p></div>";
	require_once ABSPATH . 'wp-admin/admin-footer.php';
	die;
}
class-wp-ms-users-list-table.php000064400000036547151327343360012646 0ustar00<?php
/**
 * List Table API: WP_MS_Users_List_Table class
 *
 * @package WordPress
 * @subpackage Administration
 * @since 3.1.0
 */

/**
 * Core class used to implement displaying users in a list table for the network admin.
 *
 * @since 3.1.0
 *
 * @see WP_List_Table
 */
class WP_MS_Users_List_Table extends WP_List_Table {
	/**
	 * @return bool
	 */
	public function ajax_user_can() {
		return current_user_can( 'manage_network_users' );
	}

	/**
	 * @global string $mode       List table view mode.
	 * @global string $usersearch
	 * @global string $role
	 */
	public function prepare_items() {
		global $mode, $usersearch, $role;

		if ( ! empty( $_REQUEST['mode'] ) ) {
			$mode = 'excerpt' === $_REQUEST['mode'] ? 'excerpt' : 'list';
			set_user_setting( 'network_users_list_mode', $mode );
		} else {
			$mode = get_user_setting( 'network_users_list_mode', 'list' );
		}

		$usersearch = isset( $_REQUEST['s'] ) ? wp_unslash( trim( $_REQUEST['s'] ) ) : '';

		$users_per_page = $this->get_items_per_page( 'users_network_per_page' );

		$role = isset( $_REQUEST['role'] ) ? $_REQUEST['role'] : '';

		$paged = $this->get_pagenum();

		$args = array(
			'number'  => $users_per_page,
			'offset'  => ( $paged - 1 ) * $users_per_page,
			'search'  => $usersearch,
			'blog_id' => 0,
			'fields'  => 'all_with_meta',
		);

		if ( wp_is_large_network( 'users' ) ) {
			$args['search'] = ltrim( $args['search'], '*' );
		} elseif ( '' !== $args['search'] ) {
			$args['search'] = trim( $args['search'], '*' );
			$args['search'] = '*' . $args['search'] . '*';
		}

		if ( 'super' === $role ) {
			$args['login__in'] = get_super_admins();
		}

		/*
		 * If the network is large and a search is not being performed,
		 * show only the latest users with no paging in order to avoid
		 * expensive count queries.
		 */
		if ( ! $usersearch && wp_is_large_network( 'users' ) ) {
			if ( ! isset( $_REQUEST['orderby'] ) ) {
				$_GET['orderby']     = 'id';
				$_REQUEST['orderby'] = 'id';
			}
			if ( ! isset( $_REQUEST['order'] ) ) {
				$_GET['order']     = 'DESC';
				$_REQUEST['order'] = 'DESC';
			}
			$args['count_total'] = false;
		}

		if ( isset( $_REQUEST['orderby'] ) ) {
			$args['orderby'] = $_REQUEST['orderby'];
		}

		if ( isset( $_REQUEST['order'] ) ) {
			$args['order'] = $_REQUEST['order'];
		}

		/** This filter is documented in wp-admin/includes/class-wp-users-list-table.php */
		$args = apply_filters( 'users_list_table_query_args', $args );

		// Query the user IDs for this page.
		$wp_user_search = new WP_User_Query( $args );

		$this->items = $wp_user_search->get_results();

		$this->set_pagination_args(
			array(
				'total_items' => $wp_user_search->get_total(),
				'per_page'    => $users_per_page,
			)
		);
	}

	/**
	 * @return array
	 */
	protected function get_bulk_actions() {
		$actions = array();
		if ( current_user_can( 'delete_users' ) ) {
			$actions['delete'] = __( 'Delete' );
		}
		$actions['spam']    = _x( 'Mark as spam', 'user' );
		$actions['notspam'] = _x( 'Not spam', 'user' );

		return $actions;
	}

	/**
	 */
	public function no_items() {
		_e( 'No users found.' );
	}

	/**
	 * @global string $role
	 * @return array
	 */
	protected function get_views() {
		global $role;

		$total_users  = get_user_count();
		$super_admins = get_super_admins();
		$total_admins = count( $super_admins );

		$role_links        = array();
		$role_links['all'] = array(
			'url'     => network_admin_url( 'users.php' ),
			'label'   => sprintf(
				/* translators: Number of users. */
				_nx(
					'All <span class="count">(%s)</span>',
					'All <span class="count">(%s)</span>',
					$total_users,
					'users'
				),
				number_format_i18n( $total_users )
			),
			'current' => 'super' !== $role,
		);

		$role_links['super'] = array(
			'url'     => network_admin_url( 'users.php?role=super' ),
			'label'   => sprintf(
				/* translators: Number of users. */
				_n(
					'Super Admin <span class="count">(%s)</span>',
					'Super Admins <span class="count">(%s)</span>',
					$total_admins
				),
				number_format_i18n( $total_admins )
			),
			'current' => 'super' === $role,
		);

		return $this->get_views_links( $role_links );
	}

	/**
	 * @global string $mode List table view mode.
	 *
	 * @param string $which
	 */
	protected function pagination( $which ) {
		global $mode;

		parent::pagination( $which );

		if ( 'top' === $which ) {
			$this->view_switcher( $mode );
		}
	}

	/**
	 * @return string[] Array of column titles keyed by their column name.
	 */
	public function get_columns() {
		$users_columns = array(
			'cb'         => '<input type="checkbox" />',
			'username'   => __( 'Username' ),
			'name'       => __( 'Name' ),
			'email'      => __( 'Email' ),
			'registered' => _x( 'Registered', 'user' ),
			'blogs'      => __( 'Sites' ),
		);
		/**
		 * Filters the columns displayed in the Network Admin Users list table.
		 *
		 * @since MU (3.0.0)
		 *
		 * @param string[] $users_columns An array of user columns. Default 'cb', 'username',
		 *                                'name', 'email', 'registered', 'blogs'.
		 */
		return apply_filters( 'wpmu_users_columns', $users_columns );
	}

	/**
	 * @return array
	 */
	protected function get_sortable_columns() {
		return array(
			'username'   => array( 'login', false, __( 'Username' ), __( 'Table ordered by Username.' ), 'asc' ),
			'name'       => array( 'name', false, __( 'Name' ), __( 'Table ordered by Name.' ) ),
			'email'      => array( 'email', false, __( 'E-mail' ), __( 'Table ordered by E-mail.' ) ),
			'registered' => array( 'id', false, _x( 'Registered', 'user' ), __( 'Table ordered by User Registered Date.' ) ),
		);
	}

	/**
	 * Handles the checkbox column output.
	 *
	 * @since 4.3.0
	 * @since 5.9.0 Renamed `$user` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param WP_User $item The current WP_User object.
	 */
	public function column_cb( $item ) {
		// Restores the more descriptive, specific name for use within this method.
		$user = $item;

		if ( is_super_admin( $user->ID ) ) {
			return;
		}
		?>
		<input type="checkbox" id="blog_<?php echo $user->ID; ?>" name="allusers[]" value="<?php echo esc_attr( $user->ID ); ?>" />
		<label for="blog_<?php echo $user->ID; ?>">
			<span class="screen-reader-text">
			<?php
			/* translators: Hidden accessibility text. %s: User login. */
			printf( __( 'Select %s' ), $user->user_login );
			?>
			</span>
		</label>
		<?php
	}

	/**
	 * Handles the ID column output.
	 *
	 * @since 4.4.0
	 *
	 * @param WP_User $user The current WP_User object.
	 */
	public function column_id( $user ) {
		echo $user->ID;
	}

	/**
	 * Handles the username column output.
	 *
	 * @since 4.3.0
	 *
	 * @param WP_User $user The current WP_User object.
	 */
	public function column_username( $user ) {
		$super_admins = get_super_admins();
		$avatar       = get_avatar( $user->user_email, 32 );

		echo $avatar;

		if ( current_user_can( 'edit_user', $user->ID ) ) {
			$edit_link = esc_url( add_query_arg( 'wp_http_referer', urlencode( wp_unslash( $_SERVER['REQUEST_URI'] ) ), get_edit_user_link( $user->ID ) ) );
			$edit      = "<a href=\"{$edit_link}\">{$user->user_login}</a>";
		} else {
			$edit = $user->user_login;
		}

		?>
		<strong>
			<?php
			echo $edit;

			if ( in_array( $user->user_login, $super_admins, true ) ) {
				echo ' &mdash; ' . __( 'Super Admin' );
			}
			?>
		</strong>
		<?php
	}

	/**
	 * Handles the name column output.
	 *
	 * @since 4.3.0
	 *
	 * @param WP_User $user The current WP_User object.
	 */
	public function column_name( $user ) {
		if ( $user->first_name && $user->last_name ) {
			printf(
				/* translators: 1: User's first name, 2: Last name. */
				_x( '%1$s %2$s', 'Display name based on first name and last name' ),
				$user->first_name,
				$user->last_name
			);
		} elseif ( $user->first_name ) {
			echo $user->first_name;
		} elseif ( $user->last_name ) {
			echo $user->last_name;
		} else {
			echo '<span aria-hidden="true">&#8212;</span><span class="screen-reader-text">' .
				/* translators: Hidden accessibility text. */
				_x( 'Unknown', 'name' ) .
			'</span>';
		}
	}

	/**
	 * Handles the email column output.
	 *
	 * @since 4.3.0
	 *
	 * @param WP_User $user The current WP_User object.
	 */
	public function column_email( $user ) {
		echo "<a href='" . esc_url( "mailto:$user->user_email" ) . "'>$user->user_email</a>";
	}

	/**
	 * Handles the registered date column output.
	 *
	 * @since 4.3.0
	 *
	 * @global string $mode List table view mode.
	 *
	 * @param WP_User $user The current WP_User object.
	 */
	public function column_registered( $user ) {
		global $mode;
		if ( 'list' === $mode ) {
			$date = __( 'Y/m/d' );
		} else {
			$date = __( 'Y/m/d g:i:s a' );
		}
		echo mysql2date( $date, $user->user_registered );
	}

	/**
	 * @since 4.3.0
	 *
	 * @param WP_User $user
	 * @param string  $classes
	 * @param string  $data
	 * @param string  $primary
	 */
	protected function _column_blogs( $user, $classes, $data, $primary ) {
		echo '<td class="', $classes, ' has-row-actions" ', $data, '>';
		echo $this->column_blogs( $user );
		echo $this->handle_row_actions( $user, 'blogs', $primary );
		echo '</td>';
	}

	/**
	 * Handles the sites column output.
	 *
	 * @since 4.3.0
	 *
	 * @param WP_User $user The current WP_User object.
	 */
	public function column_blogs( $user ) {
		$blogs = get_blogs_of_user( $user->ID, true );
		if ( ! is_array( $blogs ) ) {
			return;
		}

		foreach ( $blogs as $site ) {
			if ( ! can_edit_network( $site->site_id ) ) {
				continue;
			}

			$path         = ( '/' === $site->path ) ? '' : $site->path;
			$site_classes = array( 'site-' . $site->site_id );

			/**
			 * Filters the span class for a site listing on the multisite user list table.
			 *
			 * @since 5.2.0
			 *
			 * @param string[] $site_classes Array of class names used within the span tag.
			 *                               Default "site-#" with the site's network ID.
			 * @param int      $site_id      Site ID.
			 * @param int      $network_id   Network ID.
			 * @param WP_User  $user         WP_User object.
			 */
			$site_classes = apply_filters( 'ms_user_list_site_class', $site_classes, $site->userblog_id, $site->site_id, $user );

			if ( is_array( $site_classes ) && ! empty( $site_classes ) ) {
				$site_classes = array_map( 'sanitize_html_class', array_unique( $site_classes ) );
				echo '<span class="' . esc_attr( implode( ' ', $site_classes ) ) . '">';
			} else {
				echo '<span>';
			}

			echo '<a href="' . esc_url( network_admin_url( 'site-info.php?id=' . $site->userblog_id ) ) . '">' . str_replace( '.' . get_network()->domain, '', $site->domain . $path ) . '</a>';
			echo ' <small class="row-actions">';

			$actions         = array();
			$actions['edit'] = '<a href="' . esc_url( network_admin_url( 'site-info.php?id=' . $site->userblog_id ) ) . '">' . __( 'Edit' ) . '</a>';

			$class = '';
			if ( 1 === (int) $site->spam ) {
				$class .= 'site-spammed ';
			}
			if ( 1 === (int) $site->mature ) {
				$class .= 'site-mature ';
			}
			if ( 1 === (int) $site->deleted ) {
				$class .= 'site-deleted ';
			}
			if ( 1 === (int) $site->archived ) {
				$class .= 'site-archived ';
			}

			$actions['view'] = '<a class="' . $class . '" href="' . esc_url( get_home_url( $site->userblog_id ) ) . '">' . __( 'View' ) . '</a>';

			/**
			 * Filters the action links displayed next the sites a user belongs to
			 * in the Network Admin Users list table.
			 *
			 * @since 3.1.0
			 *
			 * @param string[] $actions     An array of action links to be displayed. Default 'Edit', 'View'.
			 * @param int      $userblog_id The site ID.
			 */
			$actions = apply_filters( 'ms_user_list_site_actions', $actions, $site->userblog_id );

			$action_count = count( $actions );

			$i = 0;

			foreach ( $actions as $action => $link ) {
				++$i;

				$separator = ( $i < $action_count ) ? ' | ' : '';

				echo "<span class='$action'>{$link}{$separator}</span>";
			}

			echo '</small></span><br />';
		}
	}

	/**
	 * Handles the default column output.
	 *
	 * @since 4.3.0
	 * @since 5.9.0 Renamed `$user` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param WP_User $item        The current WP_User object.
	 * @param string  $column_name The current column name.
	 */
	public function column_default( $item, $column_name ) {
		// Restores the more descriptive, specific name for use within this method.
		$user = $item;

		/** This filter is documented in wp-admin/includes/class-wp-users-list-table.php */
		$column_output = apply_filters( 'manage_users_custom_column', '', $column_name, $user->ID );

		/**
		 * Filters the display output of custom columns in the Network Users list table.
		 *
		 * @since 6.8.0
		 *
		 * @param string $output      Custom column output. Default empty.
		 * @param string $column_name Name of the custom column.
		 * @param int    $user_id     ID of the currently-listed user.
		 */
		echo apply_filters( 'manage_users-network_custom_column', $column_output, $column_name, $user->ID );
	}

	/**
	 * Generates the list table rows.
	 *
	 * @since 3.1.0
	 */
	public function display_rows() {
		foreach ( $this->items as $user ) {
			$class = '';

			$status_list = array(
				'spam'    => 'site-spammed',
				'deleted' => 'site-deleted',
			);

			foreach ( $status_list as $status => $col ) {
				if ( $user->$status ) {
					$class .= " $col";
				}
			}

			?>
			<tr class="<?php echo trim( $class ); ?>">
				<?php $this->single_row_columns( $user ); ?>
			</tr>
			<?php
		}
	}

	/**
	 * Gets the name of the default primary column.
	 *
	 * @since 4.3.0
	 *
	 * @return string Name of the default primary column, in this case, 'username'.
	 */
	protected function get_default_primary_column_name() {
		return 'username';
	}

	/**
	 * Generates and displays row action links.
	 *
	 * @since 4.3.0
	 * @since 5.9.0 Renamed `$user` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param WP_User $item        User being acted upon.
	 * @param string  $column_name Current column name.
	 * @param string  $primary     Primary column name.
	 * @return string Row actions output for users in Multisite, or an empty string
	 *                if the current column is not the primary column.
	 */
	protected function handle_row_actions( $item, $column_name, $primary ) {
		if ( $primary !== $column_name ) {
			return '';
		}

		// Restores the more descriptive, specific name for use within this method.
		$user = $item;

		$super_admins = get_super_admins();
		$actions      = array();

		if ( current_user_can( 'edit_user', $user->ID ) ) {
			$edit_link       = esc_url( add_query_arg( 'wp_http_referer', urlencode( wp_unslash( $_SERVER['REQUEST_URI'] ) ), get_edit_user_link( $user->ID ) ) );
			$actions['edit'] = '<a href="' . $edit_link . '">' . __( 'Edit' ) . '</a>';
		}

		if ( current_user_can( 'delete_user', $user->ID ) && ! in_array( $user->user_login, $super_admins, true ) ) {
			$actions['delete'] = '<a href="' . esc_url( network_admin_url( add_query_arg( '_wp_http_referer', urlencode( wp_unslash( $_SERVER['REQUEST_URI'] ) ), wp_nonce_url( 'users.php', 'deleteuser' ) . '&amp;action=deleteuser&amp;id=' . $user->ID ) ) ) . '" class="delete">' . __( 'Delete' ) . '</a>';
		}

		/**
		 * Filters the action links displayed under each user in the Network Admin Users list table.
		 *
		 * @since 3.2.0
		 *
		 * @param string[] $actions An array of action links to be displayed. Default 'Edit', 'Delete'.
		 * @param WP_User  $user    WP_User object.
		 */
		$actions = apply_filters( 'ms_user_row_actions', $actions, $user );

		return $this->row_actions( $actions );
	}
}
class-walker-nav-menu-edit.php000060400000033544151327343360012330 0ustar00<?php
/**
 * Navigation Menu API: Walker_Nav_Menu_Edit class
 *
 * @package WordPress
 * @subpackage Administration
 * @since 4.4.0
 */

/**
 * Create HTML list of nav menu input items.
 *
 * @since 3.0.0
 *
 * @see Walker_Nav_Menu
 */
class Walker_Nav_Menu_Edit extends Walker_Nav_Menu {
	/**
	 * Starts the list before the elements are added.
	 *
	 * @see Walker_Nav_Menu::start_lvl()
	 *
	 * @since 3.0.0
	 *
	 * @param string   $output Passed by reference.
	 * @param int      $depth  Depth of menu item. Used for padding.
	 * @param stdClass $args   Not used.
	 */
	public function start_lvl( &$output, $depth = 0, $args = null ) {}

	/**
	 * Ends the list of after the elements are added.
	 *
	 * @see Walker_Nav_Menu::end_lvl()
	 *
	 * @since 3.0.0
	 *
	 * @param string   $output Passed by reference.
	 * @param int      $depth  Depth of menu item. Used for padding.
	 * @param stdClass $args   Not used.
	 */
	public function end_lvl( &$output, $depth = 0, $args = null ) {}

	/**
	 * Start the element output.
	 *
	 * @see Walker_Nav_Menu::start_el()
	 * @since 3.0.0
	 * @since 5.9.0 Renamed `$item` to `$data_object` and `$id` to `$current_object_id`
	 *              to match parent class for PHP 8 named parameter support.
	 *
	 * @global int $_wp_nav_menu_max_depth
	 *
	 * @param string   $output            Used to append additional content (passed by reference).
	 * @param WP_Post  $data_object       Menu item data object.
	 * @param int      $depth             Depth of menu item. Used for padding.
	 * @param stdClass $args              Not used.
	 * @param int      $current_object_id Optional. ID of the current menu item. Default 0.
	 */
	public function start_el( &$output, $data_object, $depth = 0, $args = null, $current_object_id = 0 ) {
		global $_wp_nav_menu_max_depth;

		// Restores the more descriptive, specific name for use within this method.
		$menu_item = $data_object;

		$_wp_nav_menu_max_depth = $depth > $_wp_nav_menu_max_depth ? $depth : $_wp_nav_menu_max_depth;

		ob_start();
		$item_id      = esc_attr( $menu_item->ID );
		$removed_args = array(
			'action',
			'customlink-tab',
			'edit-menu-item',
			'menu-item',
			'page-tab',
			'_wpnonce',
		);

		$original_title = false;

		if ( 'taxonomy' === $menu_item->type ) {
			$original_object = get_term( (int) $menu_item->object_id, $menu_item->object );
			if ( $original_object && ! is_wp_error( $original_object ) ) {
				$original_title = $original_object->name;
			}
		} elseif ( 'post_type' === $menu_item->type ) {
			$original_object = get_post( $menu_item->object_id );
			if ( $original_object ) {
				$original_title = get_the_title( $original_object->ID );
			}
		} elseif ( 'post_type_archive' === $menu_item->type ) {
			$original_object = get_post_type_object( $menu_item->object );
			if ( $original_object ) {
				$original_title = $original_object->labels->archives;
			}
		}

		$classes = array(
			'menu-item menu-item-depth-' . $depth,
			'menu-item-' . esc_attr( $menu_item->object ),
			'menu-item-edit-' . ( ( isset( $_GET['edit-menu-item'] ) && $item_id === $_GET['edit-menu-item'] ) ? 'active' : 'inactive' ),
		);

		$title = $menu_item->title;

		if ( ! empty( $menu_item->_invalid ) ) {
			$classes[] = 'menu-item-invalid';
			/* translators: %s: Title of an invalid menu item. */
			$title = sprintf( __( '%s (Invalid)' ), $menu_item->title );
		} elseif ( isset( $menu_item->post_status ) && 'draft' === $menu_item->post_status ) {
			$classes[] = 'pending';
			/* translators: %s: Title of a menu item in draft status. */
			$title = sprintf( __( '%s (Pending)' ), $menu_item->title );
		}

		$title = ( ! isset( $menu_item->label ) || '' === $menu_item->label ) ? $title : $menu_item->label;

		$submenu_text = '';
		if ( 0 === $depth ) {
			$submenu_text = 'style="display: none;"';
		}

		?>
		<li id="menu-item-<?php echo $item_id; ?>" class="<?php echo implode( ' ', $classes ); ?>">
			<div class="menu-item-bar">
				<div class="menu-item-handle">
					<label class="item-title" for="menu-item-checkbox-<?php echo $item_id; ?>">
						<input id="menu-item-checkbox-<?php echo $item_id; ?>" type="checkbox" class="menu-item-checkbox" data-menu-item-id="<?php echo $item_id; ?>" disabled="disabled" />
						<span class="menu-item-title"><?php echo esc_html( $title ); ?></span>
						<span class="is-submenu" <?php echo $submenu_text; ?>><?php _e( 'sub item' ); ?></span>
					</label>
					<span class="item-controls">
						<span class="item-type"><?php echo esc_html( $menu_item->type_label ); ?></span>
						<span class="item-order hide-if-js">
							<?php
							printf(
								'<a href="%s" class="item-move-up" aria-label="%s">&#8593;</a>',
								wp_nonce_url(
									add_query_arg(
										array(
											'action'    => 'move-up-menu-item',
											'menu-item' => $item_id,
										),
										remove_query_arg( $removed_args, admin_url( 'nav-menus.php' ) )
									),
									'move-menu_item'
								),
								esc_attr__( 'Move up' )
							);
							?>
							|
							<?php
							printf(
								'<a href="%s" class="item-move-down" aria-label="%s">&#8595;</a>',
								wp_nonce_url(
									add_query_arg(
										array(
											'action'    => 'move-down-menu-item',
											'menu-item' => $item_id,
										),
										remove_query_arg( $removed_args, admin_url( 'nav-menus.php' ) )
									),
									'move-menu_item'
								),
								esc_attr__( 'Move down' )
							);
							?>
						</span>
						<?php
						if ( isset( $_GET['edit-menu-item'] ) && $item_id === $_GET['edit-menu-item'] ) {
							$edit_url = admin_url( 'nav-menus.php' );
						} else {
							$edit_url = add_query_arg(
								array(
									'edit-menu-item' => $item_id,
								),
								remove_query_arg( $removed_args, admin_url( 'nav-menus.php#menu-item-settings-' . $item_id ) )
							);
						}

						printf(
							'<a class="item-edit" id="edit-%s" href="%s" aria-label="%s"><span class="screen-reader-text">%s</span></a>',
							$item_id,
							esc_url( $edit_url ),
							esc_attr__( 'Edit menu item' ),
							/* translators: Hidden accessibility text. */
							__( 'Edit' )
						);
						?>
					</span>
				</div>
			</div>

			<div class="menu-item-settings wp-clearfix" id="menu-item-settings-<?php echo $item_id; ?>">
				<?php if ( 'custom' === $menu_item->type ) : ?>
					<p class="field-url description description-wide">
						<label for="edit-menu-item-url-<?php echo $item_id; ?>">
							<?php _e( 'URL' ); ?><br />
							<input type="text" id="edit-menu-item-url-<?php echo $item_id; ?>" class="widefat code edit-menu-item-url" name="menu-item-url[<?php echo $item_id; ?>]" value="<?php echo esc_attr( $menu_item->url ); ?>" />
						</label>
					</p>
				<?php endif; ?>
				<p class="description description-wide">
					<label for="edit-menu-item-title-<?php echo $item_id; ?>">
						<?php _e( 'Navigation Label' ); ?><br />
						<input type="text" id="edit-menu-item-title-<?php echo $item_id; ?>" class="widefat edit-menu-item-title" name="menu-item-title[<?php echo $item_id; ?>]" value="<?php echo esc_attr( $menu_item->title ); ?>" />
					</label>
				</p>
				<p class="field-title-attribute field-attr-title description description-wide">
					<label for="edit-menu-item-attr-title-<?php echo $item_id; ?>">
						<?php _e( 'Title Attribute' ); ?><br />
						<input type="text" id="edit-menu-item-attr-title-<?php echo $item_id; ?>" class="widefat edit-menu-item-attr-title" name="menu-item-attr-title[<?php echo $item_id; ?>]" value="<?php echo esc_attr( $menu_item->post_excerpt ); ?>" />
					</label>
				</p>
				<p class="field-link-target description">
					<label for="edit-menu-item-target-<?php echo $item_id; ?>">
						<input type="checkbox" id="edit-menu-item-target-<?php echo $item_id; ?>" value="_blank" name="menu-item-target[<?php echo $item_id; ?>]"<?php checked( $menu_item->target, '_blank' ); ?> />
						<?php _e( 'Open link in a new tab' ); ?>
					</label>
				</p>
				<div class="description-group">
					<p class="field-css-classes description description-thin">
						<label for="edit-menu-item-classes-<?php echo $item_id; ?>">
							<?php _e( 'CSS Classes (optional)' ); ?><br />
							<input type="text" id="edit-menu-item-classes-<?php echo $item_id; ?>" class="widefat code edit-menu-item-classes" name="menu-item-classes[<?php echo $item_id; ?>]" value="<?php echo esc_attr( implode( ' ', $menu_item->classes ) ); ?>" />
						</label>
					</p>
					<p class="field-xfn description description-thin">
						<label for="edit-menu-item-xfn-<?php echo $item_id; ?>">
							<?php _e( 'Link Relationship (XFN)' ); ?><br />
							<input type="text" id="edit-menu-item-xfn-<?php echo $item_id; ?>" class="widefat code edit-menu-item-xfn" name="menu-item-xfn[<?php echo $item_id; ?>]" value="<?php echo esc_attr( $menu_item->xfn ); ?>" />
						</label>
					</p>
				</div>
				<p class="field-description description description-wide">
					<label for="edit-menu-item-description-<?php echo $item_id; ?>">
						<?php _e( 'Description' ); ?><br />
						<textarea id="edit-menu-item-description-<?php echo $item_id; ?>" class="widefat edit-menu-item-description" rows="3" cols="20" name="menu-item-description[<?php echo $item_id; ?>]"><?php echo esc_html( $menu_item->description ); // textarea_escaped ?></textarea>
						<span class="description"><?php _e( 'The description will be displayed in the menu if the active theme supports it.' ); ?></span>
					</label>
				</p>

				<?php
				/**
				 * Update parent and order of menu item using select inputs.
				 *
				 * @since 6.7.0
				 */
				?>
	
				<div class="field-move-combo description-group">
					<p class="description description-wide">
						<label for="edit-menu-item-parent-<?php echo $item_id; ?>">
							<?php _e( 'Menu Parent' ); ?>
						</label>
						<select class="edit-menu-item-parent widefat" id="edit-menu-item-parent-<?php echo $item_id; ?>" name="menu-item-parent[<?php echo $item_id; ?>]">
						</select>
					</p>
					<p class="description description-wide">
						<label for="edit-menu-item-order-<?php echo $item_id; ?>">
							<?php _e( 'Menu Order' ); ?>
						</label>
						<select class="edit-menu-item-order widefat" id="edit-menu-item-order-<?php echo $item_id; ?>" name="menu-item-order[<?php echo $item_id; ?>]">
						</select>
					</p>
				</div>

				<?php
				/**
				 * Fires just before the move buttons of a nav menu item in the menu editor.
				 *
				 * @since 5.4.0
				 *
				 * @param string        $item_id           Menu item ID as a numeric string.
				 * @param WP_Post       $menu_item         Menu item data object.
				 * @param int           $depth             Depth of menu item. Used for padding.
				 * @param stdClass|null $args              An object of menu item arguments.
				 * @param int           $current_object_id Nav menu ID.
				 */
				do_action( 'wp_nav_menu_item_custom_fields', $item_id, $menu_item, $depth, $args, $current_object_id );
				?>

				<fieldset class="field-move hide-if-no-js description description-wide">
					<span class="field-move-visual-label" aria-hidden="true"><?php _e( 'Move' ); ?></span>
					<button type="button" class="button-link menus-move menus-move-up" data-dir="up"><?php _e( 'Up one' ); ?></button>
					<button type="button" class="button-link menus-move menus-move-down" data-dir="down"><?php _e( 'Down one' ); ?></button>
					<button type="button" class="button-link menus-move menus-move-left" data-dir="left"></button>
					<button type="button" class="button-link menus-move menus-move-right" data-dir="right"></button>
					<button type="button" class="button-link menus-move menus-move-top" data-dir="top"><?php _e( 'To the top' ); ?></button>
				</fieldset>

				<div class="menu-item-actions description-wide submitbox">
					<?php if ( 'custom' !== $menu_item->type && false !== $original_title ) : ?>
						<p class="link-to-original">
							<?php
							/* translators: %s: Link to menu item's original object. */
							printf( __( 'Original: %s' ), '<a href="' . esc_url( $menu_item->url ) . '">' . esc_html( $original_title ) . '</a>' );
							?>
						</p>
					<?php endif; ?>

					<?php
					printf(
						'<a class="item-delete submitdelete deletion" id="delete-%s" href="%s">%s</a>',
						$item_id,
						wp_nonce_url(
							add_query_arg(
								array(
									'action'    => 'delete-menu-item',
									'menu-item' => $item_id,
								),
								admin_url( 'nav-menus.php' )
							),
							'delete-menu_item_' . $item_id
						),
						__( 'Remove' )
					);
					?>
					<span class="meta-sep hide-if-no-js"> | </span>
					<?php
					printf(
						'<a class="item-cancel submitcancel hide-if-no-js" id="cancel-%s" href="%s#menu-item-settings-%s">%s</a>',
						$item_id,
						esc_url(
							add_query_arg(
								array(
									'edit-menu-item' => $item_id,
									'cancel'         => time(),
								),
								admin_url( 'nav-menus.php' )
							)
						),
						$item_id,
						__( 'Cancel' )
					);
					?>
				</div>

				<input class="menu-item-data-db-id" type="hidden" name="menu-item-db-id[<?php echo $item_id; ?>]" value="<?php echo $item_id; ?>" />
				<input class="menu-item-data-object-id" type="hidden" name="menu-item-object-id[<?php echo $item_id; ?>]" value="<?php echo esc_attr( $menu_item->object_id ); ?>" />
				<input class="menu-item-data-object" type="hidden" name="menu-item-object[<?php echo $item_id; ?>]" value="<?php echo esc_attr( $menu_item->object ); ?>" />
				<input class="menu-item-data-parent-id" type="hidden" name="menu-item-parent-id[<?php echo $item_id; ?>]" value="<?php echo esc_attr( $menu_item->menu_item_parent ); ?>" />
				<input class="menu-item-data-position" type="hidden" name="menu-item-position[<?php echo $item_id; ?>]" value="<?php echo esc_attr( $menu_item->menu_order ); ?>" />
				<input class="menu-item-data-type" type="hidden" name="menu-item-type[<?php echo $item_id; ?>]" value="<?php echo esc_attr( $menu_item->type ); ?>" />
			</div><!-- .menu-item-settings-->
			<ul class="menu-item-transport"></ul>
		<?php
		$output .= ob_get_clean();
	}
}class-bulk-upgrader-skin.php000060400000015141151327343360012073 0ustar00<?php
/**
 * Upgrader API: Bulk_Upgrader_Skin class
 *
 * @package WordPress
 * @subpackage Upgrader
 * @since 4.6.0
 */

/**
 * Generic Bulk Upgrader Skin for WordPress Upgrades.
 *
 * @since 3.0.0
 * @since 4.6.0 Moved to its own file from wp-admin/includes/class-wp-upgrader-skins.php.
 *
 * @see WP_Upgrader_Skin
 */
class Bulk_Upgrader_Skin extends WP_Upgrader_Skin {

	/**
	 * Whether the bulk update process has started.
	 *
	 * @since 3.0.0
	 * @var bool
	 */
	public $in_loop = false;

	/**
	 * Stores an error message about the update.
	 *
	 * @since 3.0.0
	 * @var string|false
	 */
	public $error = false;

	/**
	 * Constructor.
	 *
	 * Sets up the generic skin for the Bulk Upgrader classes.
	 *
	 * @since 3.0.0
	 *
	 * @param array $args
	 */
	public function __construct( $args = array() ) {
		$defaults = array(
			'url'   => '',
			'nonce' => '',
		);
		$args     = wp_parse_args( $args, $defaults );

		parent::__construct( $args );
	}

	/**
	 * Sets up the strings used in the update process.
	 *
	 * @since 3.0.0
	 */
	public function add_strings() {
		$this->upgrader->strings['skin_upgrade_start'] = __( 'The update process is starting. This process may take a while on some hosts, so please be patient.' );
		/* translators: 1: Title of an update, 2: Error message. */
		$this->upgrader->strings['skin_update_failed_error'] = __( 'An error occurred while updating %1$s: %2$s' );
		/* translators: %s: Title of an update. */
		$this->upgrader->strings['skin_update_failed'] = __( 'The update of %s failed.' );
		/* translators: %s: Title of an update. */
		$this->upgrader->strings['skin_update_successful'] = __( '%s updated successfully.' );
		$this->upgrader->strings['skin_upgrade_end']       = __( 'All updates have been completed.' );
	}

	/**
	 * Displays a message about the update.
	 *
	 * @since 3.0.0
	 * @since 5.9.0 Renamed `$string` (a PHP reserved keyword) to `$feedback` for PHP 8 named parameter support.
	 *
	 * @param string $feedback Message data.
	 * @param mixed  ...$args  Optional text replacements.
	 */
	public function feedback( $feedback, ...$args ) {
		if ( isset( $this->upgrader->strings[ $feedback ] ) ) {
			$feedback = $this->upgrader->strings[ $feedback ];
		}

		if ( str_contains( $feedback, '%' ) ) {
			if ( $args ) {
				$args     = array_map( 'strip_tags', $args );
				$args     = array_map( 'esc_html', $args );
				$feedback = vsprintf( $feedback, $args );
			}
		}
		if ( empty( $feedback ) ) {
			return;
		}
		if ( $this->in_loop ) {
			echo "$feedback<br />\n";
		} else {
			echo "<p>$feedback</p>\n";
		}
	}

	/**
	 * Displays the header before the update process.
	 *
	 * @since 3.0.0
	 */
	public function header() {
		// Nothing. This will be displayed within an iframe.
	}

	/**
	 * Displays the footer following the update process.
	 *
	 * @since 3.0.0
	 */
	public function footer() {
		// Nothing. This will be displayed within an iframe.
	}

	/**
	 * Displays an error message about the update.
	 *
	 * @since 3.0.0
	 * @since 5.9.0 Renamed `$error` to `$errors` for PHP 8 named parameter support.
	 *
	 * @param string|WP_Error $errors Errors.
	 */
	public function error( $errors ) {
		if ( is_string( $errors ) && isset( $this->upgrader->strings[ $errors ] ) ) {
			$this->error = $this->upgrader->strings[ $errors ];
		}

		if ( is_wp_error( $errors ) ) {
			$messages = array();
			foreach ( $errors->get_error_messages() as $emessage ) {
				if ( $errors->get_error_data() && is_string( $errors->get_error_data() ) ) {
					$messages[] = $emessage . ' ' . esc_html( strip_tags( $errors->get_error_data() ) );
				} else {
					$messages[] = $emessage;
				}
			}
			$this->error = implode( ', ', $messages );
		}
		echo '<script type="text/javascript">jQuery(\'.waiting-' . esc_js( $this->upgrader->update_current ) . '\').hide();</script>';
	}

	/**
	 * Displays the header before the bulk update process.
	 *
	 * @since 3.0.0
	 */
	public function bulk_header() {
		$this->feedback( 'skin_upgrade_start' );
	}

	/**
	 * Displays the footer following the bulk update process.
	 *
	 * @since 3.0.0
	 */
	public function bulk_footer() {
		$this->feedback( 'skin_upgrade_end' );
	}

	/**
	 * Performs an action before a bulk update.
	 *
	 * @since 3.0.0
	 *
	 * @param string $title
	 */
	public function before( $title = '' ) {
		$this->in_loop = true;
		printf( '<h2>' . $this->upgrader->strings['skin_before_update_header'] . ' <span class="spinner waiting-' . $this->upgrader->update_current . '"></span></h2>', $title, $this->upgrader->update_current, $this->upgrader->update_count );
		echo '<script type="text/javascript">jQuery(\'.waiting-' . esc_js( $this->upgrader->update_current ) . '\').css("display", "inline-block");</script>';
		// This progress messages div gets moved via JavaScript when clicking on "More details.".
		echo '<div class="update-messages hide-if-js" id="progress-' . esc_attr( $this->upgrader->update_current ) . '"><p>';
		$this->flush_output();
	}

	/**
	 * Performs an action following a bulk update.
	 *
	 * @since 3.0.0
	 *
	 * @param string $title
	 */
	public function after( $title = '' ) {
		echo '</p></div>';
		if ( $this->error || ! $this->result ) {
			if ( $this->error ) {
				$after_error_message = sprintf( $this->upgrader->strings['skin_update_failed_error'], $title, '<strong>' . $this->error . '</strong>' );
			} else {
				$after_error_message = sprintf( $this->upgrader->strings['skin_update_failed'], $title );
			}
			wp_admin_notice(
				$after_error_message,
				array(
					'additional_classes' => array( 'error' ),
				)
			);

			echo '<script type="text/javascript">jQuery(\'#progress-' . esc_js( $this->upgrader->update_current ) . '\').show();</script>';
		}
		if ( $this->result && ! is_wp_error( $this->result ) ) {
			if ( ! $this->error ) {
				echo '<div class="updated js-update-details" data-update-details="progress-' . esc_attr( $this->upgrader->update_current ) . '">' .
					'<p>' . sprintf( $this->upgrader->strings['skin_update_successful'], $title ) .
					' <button type="button" class="hide-if-no-js button-link js-update-details-toggle" aria-expanded="false">' . __( 'More details.' ) . '<span class="dashicons dashicons-arrow-down" aria-hidden="true"></span></button>' .
					'</p></div>';
			}

			echo '<script type="text/javascript">jQuery(\'.waiting-' . esc_js( $this->upgrader->update_current ) . '\').hide();</script>';
		}

		$this->reset();
		$this->flush_output();
	}

	/**
	 * Resets the properties used in the update process.
	 *
	 * @since 3.0.0
	 */
	public function reset() {
		$this->in_loop = false;
		$this->error   = false;
	}

	/**
	 * Flushes all output buffers.
	 *
	 * @since 3.0.0
	 */
	public function flush_output() {
		wp_ob_end_flush_all();
		flush();
	}
}
class-wp-themes-list-table.php000064400000024155151327343360012345 0ustar00<?php
/**
 * List Table API: WP_Themes_List_Table class
 *
 * @package WordPress
 * @subpackage Administration
 * @since 3.1.0
 */

/**
 * Core class used to implement displaying installed themes in a list table.
 *
 * @since 3.1.0
 *
 * @see WP_List_Table
 */
class WP_Themes_List_Table extends WP_List_Table {

	protected $search_terms = array();
	public $features        = array();

	/**
	 * Constructor.
	 *
	 * @since 3.1.0
	 *
	 * @see WP_List_Table::__construct() for more information on default arguments.
	 *
	 * @param array $args An associative array of arguments.
	 */
	public function __construct( $args = array() ) {
		parent::__construct(
			array(
				'ajax'   => true,
				'screen' => isset( $args['screen'] ) ? $args['screen'] : null,
			)
		);
	}

	/**
	 * @return bool
	 */
	public function ajax_user_can() {
		// Do not check edit_theme_options here. Ajax calls for available themes require switch_themes.
		return current_user_can( 'switch_themes' );
	}

	/**
	 */
	public function prepare_items() {
		$themes = wp_get_themes( array( 'allowed' => true ) );

		if ( ! empty( $_REQUEST['s'] ) ) {
			$this->search_terms = array_unique( array_filter( array_map( 'trim', explode( ',', strtolower( wp_unslash( $_REQUEST['s'] ) ) ) ) ) );
		}

		if ( ! empty( $_REQUEST['features'] ) ) {
			$this->features = $_REQUEST['features'];
		}

		if ( $this->search_terms || $this->features ) {
			foreach ( $themes as $key => $theme ) {
				if ( ! $this->search_theme( $theme ) ) {
					unset( $themes[ $key ] );
				}
			}
		}

		unset( $themes[ get_option( 'stylesheet' ) ] );
		WP_Theme::sort_by_name( $themes );

		$per_page = 36;
		$page     = $this->get_pagenum();

		$start = ( $page - 1 ) * $per_page;

		$this->items = array_slice( $themes, $start, $per_page, true );

		$this->set_pagination_args(
			array(
				'total_items'     => count( $themes ),
				'per_page'        => $per_page,
				'infinite_scroll' => true,
			)
		);
	}

	/**
	 */
	public function no_items() {
		if ( $this->search_terms || $this->features ) {
			_e( 'No items found.' );
			return;
		}

		$blog_id = get_current_blog_id();
		if ( is_multisite() ) {
			if ( current_user_can( 'install_themes' ) && current_user_can( 'manage_network_themes' ) ) {
				printf(
					/* translators: 1: URL to Themes tab on Edit Site screen, 2: URL to Add Themes screen. */
					__( 'You only have one theme enabled for this site right now. Visit the Network Admin to <a href="%1$s">enable</a> or <a href="%2$s">install</a> more themes.' ),
					network_admin_url( 'site-themes.php?id=' . $blog_id ),
					network_admin_url( 'theme-install.php' )
				);

				return;
			} elseif ( current_user_can( 'manage_network_themes' ) ) {
				printf(
					/* translators: %s: URL to Themes tab on Edit Site screen. */
					__( 'You only have one theme enabled for this site right now. Visit the Network Admin to <a href="%s">enable</a> more themes.' ),
					network_admin_url( 'site-themes.php?id=' . $blog_id )
				);

				return;
			}
			// Else, fallthrough. install_themes doesn't help if you can't enable it.
		} else {
			if ( current_user_can( 'install_themes' ) ) {
				printf(
					/* translators: %s: URL to Add Themes screen. */
					__( 'You only have one theme installed right now. Live a little! You can choose from over 1,000 free themes in the WordPress Theme Directory at any time: just click on the <a href="%s">Install Themes</a> tab above.' ),
					admin_url( 'theme-install.php' )
				);

				return;
			}
		}
		// Fallthrough.
		printf(
			/* translators: %s: Network title. */
			__( 'Only the active theme is available to you. Contact the %s administrator for information about accessing additional themes.' ),
			get_site_option( 'site_name' )
		);
	}

	/**
	 * @param string $which
	 */
	public function tablenav( $which = 'top' ) {
		if ( $this->get_pagination_arg( 'total_pages' ) <= 1 ) {
			return;
		}
		?>
		<div class="tablenav themes <?php echo $which; ?>">
			<?php $this->pagination( $which ); ?>
			<span class="spinner"></span>
			<br class="clear" />
		</div>
		<?php
	}

	/**
	 * Displays the themes table.
	 *
	 * Overrides the parent display() method to provide a different container.
	 *
	 * @since 3.1.0
	 */
	public function display() {
		wp_nonce_field( 'fetch-list-' . get_class( $this ), '_ajax_fetch_list_nonce' );
		?>
		<?php $this->tablenav( 'top' ); ?>

		<div id="availablethemes">
			<?php $this->display_rows_or_placeholder(); ?>
		</div>

		<?php $this->tablenav( 'bottom' ); ?>
		<?php
	}

	/**
	 * @return string[] Array of column titles keyed by their column name.
	 */
	public function get_columns() {
		return array();
	}

	/**
	 */
	public function display_rows_or_placeholder() {
		if ( $this->has_items() ) {
			$this->display_rows();
		} else {
			echo '<div class="no-items">';
			$this->no_items();
			echo '</div>';
		}
	}

	/**
	 * Generates the list table rows.
	 *
	 * @since 3.1.0
	 */
	public function display_rows() {
		$themes = $this->items;

		foreach ( $themes as $theme ) :
			?>
			<div class="available-theme">
			<?php

			$template   = $theme->get_template();
			$stylesheet = $theme->get_stylesheet();
			$title      = $theme->display( 'Name' );
			$version    = $theme->display( 'Version' );
			$author     = $theme->display( 'Author' );

			$activate_link = wp_nonce_url( 'themes.php?action=activate&amp;template=' . urlencode( $template ) . '&amp;stylesheet=' . urlencode( $stylesheet ), 'switch-theme_' . $stylesheet );

			$actions             = array();
			$actions['activate'] = sprintf(
				'<a href="%s" class="activatelink" aria-label="%s">%s</a>',
				$activate_link,
				/* translators: %s: Theme name. */
				esc_attr( sprintf( _x( 'Activate &#8220;%s&#8221;', 'theme' ), $title ) ),
				_x( 'Activate', 'theme' )
			);

			if ( current_user_can( 'edit_theme_options' ) && current_user_can( 'customize' ) ) {
				$actions['preview'] .= sprintf(
					'<a href="%s" class="load-customize hide-if-no-customize">%s</a>',
					wp_customize_url( $stylesheet ),
					__( 'Live Preview' )
				);
			}

			if ( ! is_multisite() && current_user_can( 'delete_themes' ) ) {
				$actions['delete'] = sprintf(
					'<a class="submitdelete deletion" href="%s" onclick="return confirm( \'%s\' );">%s</a>',
					wp_nonce_url( 'themes.php?action=delete&amp;stylesheet=' . urlencode( $stylesheet ), 'delete-theme_' . $stylesheet ),
					/* translators: %s: Theme name. */
					esc_js( sprintf( __( "You are about to delete this theme '%s'\n  'Cancel' to stop, 'OK' to delete." ), $title ) ),
					__( 'Delete' )
				);
			}

			/** This filter is documented in wp-admin/includes/class-wp-ms-themes-list-table.php */
			$actions = apply_filters( 'theme_action_links', $actions, $theme, 'all' );

			/** This filter is documented in wp-admin/includes/class-wp-ms-themes-list-table.php */
			$actions       = apply_filters( "theme_action_links_{$stylesheet}", $actions, $theme, 'all' );
			$delete_action = isset( $actions['delete'] ) ? '<div class="delete-theme">' . $actions['delete'] . '</div>' : '';
			unset( $actions['delete'] );

			$screenshot = $theme->get_screenshot();
			?>

			<span class="screenshot hide-if-customize">
				<?php if ( $screenshot ) : ?>
					<img src="<?php echo esc_url( $screenshot . '?ver=' . $theme->version ); ?>" alt="" />
				<?php endif; ?>
			</span>
			<a href="<?php echo wp_customize_url( $stylesheet ); ?>" class="screenshot load-customize hide-if-no-customize">
				<?php if ( $screenshot ) : ?>
					<img src="<?php echo esc_url( $screenshot . '?ver=' . $theme->version ); ?>" alt="" />
				<?php endif; ?>
			</a>

			<h3><?php echo $title; ?></h3>
			<div class="theme-author">
				<?php
					/* translators: %s: Theme author. */
					printf( __( 'By %s' ), $author );
				?>
			</div>
			<div class="action-links">
				<ul>
					<?php foreach ( $actions as $action ) : ?>
						<li><?php echo $action; ?></li>
					<?php endforeach; ?>
					<li class="hide-if-no-js"><a href="#" class="theme-detail"><?php _e( 'Details' ); ?></a></li>
				</ul>
				<?php echo $delete_action; ?>

				<?php theme_update_available( $theme ); ?>
			</div>

			<div class="themedetaildiv hide-if-js">
				<p><strong><?php _e( 'Version:' ); ?></strong> <?php echo $version; ?></p>
				<p><?php echo $theme->display( 'Description' ); ?></p>
				<?php
				if ( $theme->parent() ) {
					printf(
						/* translators: 1: Link to documentation on child themes, 2: Name of parent theme. */
						' <p class="howto">' . __( 'This <a href="%1$s">child theme</a> requires its parent theme, %2$s.' ) . '</p>',
						__( 'https://developer.wordpress.org/themes/advanced-topics/child-themes/' ),
						$theme->parent()->display( 'Name' )
					);
				}
				?>
			</div>

			</div>
			<?php
		endforeach;
	}

	/**
	 * @param WP_Theme $theme
	 * @return bool
	 */
	public function search_theme( $theme ) {
		// Search the features.
		foreach ( $this->features as $word ) {
			if ( ! in_array( $word, $theme->get( 'Tags' ), true ) ) {
				return false;
			}
		}

		// Match all phrases.
		foreach ( $this->search_terms as $word ) {
			if ( in_array( $word, $theme->get( 'Tags' ), true ) ) {
				continue;
			}

			foreach ( array( 'Name', 'Description', 'Author', 'AuthorURI' ) as $header ) {
				// Don't mark up; Do translate.
				if ( false !== stripos( strip_tags( $theme->display( $header, false, true ) ), $word ) ) {
					continue 2;
				}
			}

			if ( false !== stripos( $theme->get_stylesheet(), $word ) ) {
				continue;
			}

			if ( false !== stripos( $theme->get_template(), $word ) ) {
				continue;
			}

			return false;
		}

		return true;
	}

	/**
	 * Send required variables to JavaScript land
	 *
	 * @since 3.4.0
	 *
	 * @param array $extra_args
	 */
	public function _js_vars( $extra_args = array() ) {
		$search_string = isset( $_REQUEST['s'] ) ? esc_attr( wp_unslash( $_REQUEST['s'] ) ) : '';

		$args = array(
			'search'      => $search_string,
			'features'    => $this->features,
			'paged'       => $this->get_pagenum(),
			'total_pages' => ! empty( $this->_pagination_args['total_pages'] ) ? $this->_pagination_args['total_pages'] : 1,
		);

		if ( is_array( $extra_args ) ) {
			$args = array_merge( $args, $extra_args );
		}

		printf( "<script type='text/javascript'>var theme_list_args = %s;</script>\n", wp_json_encode( $args ) );
		parent::_js_vars();
	}
}
class-wp-privacy-data-export-requests-list-table.php000060400000012673151327343360016632 0ustar00<?php
/**
 * List Table API: WP_Privacy_Data_Export_Requests_List_Table class
 *
 * @package WordPress
 * @subpackage Administration
 * @since 4.9.6
 */

if ( ! class_exists( 'WP_Privacy_Requests_Table' ) ) {
	require_once ABSPATH . 'wp-admin/includes/class-wp-privacy-requests-table.php';
}

/**
 * WP_Privacy_Data_Export_Requests_Table class.
 *
 * @since 4.9.6
 */
class WP_Privacy_Data_Export_Requests_List_Table extends WP_Privacy_Requests_Table {
	/**
	 * Action name for the requests this table will work with.
	 *
	 * @since 4.9.6
	 *
	 * @var string $request_type Name of action.
	 */
	protected $request_type = 'export_personal_data';

	/**
	 * Post type for the requests.
	 *
	 * @since 4.9.6
	 *
	 * @var string $post_type The post type.
	 */
	protected $post_type = 'user_request';

	/**
	 * Actions column.
	 *
	 * @since 4.9.6
	 *
	 * @param WP_User_Request $item Item being shown.
	 * @return string Email column markup.
	 */
	public function column_email( $item ) {
		/** This filter is documented in wp-admin/includes/ajax-actions.php */
		$exporters       = apply_filters( 'wp_privacy_personal_data_exporters', array() );
		$exporters_count = count( $exporters );
		$status          = $item->status;
		$request_id      = $item->ID;
		$nonce           = wp_create_nonce( 'wp-privacy-export-personal-data-' . $request_id );

		$download_data_markup = '<span class="export-personal-data" ' .
			'data-exporters-count="' . esc_attr( $exporters_count ) . '" ' .
			'data-request-id="' . esc_attr( $request_id ) . '" ' .
			'data-nonce="' . esc_attr( $nonce ) .
			'">';

		$download_data_markup .= '<span class="export-personal-data-idle"><button type="button" class="button-link export-personal-data-handle">' . __( 'Download personal data' ) . '</button></span>' .
			'<span class="export-personal-data-processing hidden">' . __( 'Downloading data...' ) . ' <span class="export-progress"></span></span>' .
			'<span class="export-personal-data-success hidden"><button type="button" class="button-link export-personal-data-handle">' . __( 'Download personal data again' ) . '</button></span>' .
			'<span class="export-personal-data-failed hidden">' . __( 'Download failed.' ) . ' <button type="button" class="button-link export-personal-data-handle">' . __( 'Retry' ) . '</button></span>';

		$download_data_markup .= '</span>';

		$row_actions['download-data'] = $download_data_markup;

		if ( 'request-completed' !== $status ) {
			$complete_request_markup  = '<span>';
			$complete_request_markup .= sprintf(
				'<a href="%s" class="complete-request" aria-label="%s">%s</a>',
				esc_url(
					wp_nonce_url(
						add_query_arg(
							array(
								'action'     => 'complete',
								'request_id' => array( $request_id ),
							),
							admin_url( 'export-personal-data.php' )
						),
						'bulk-privacy_requests'
					)
				),
				esc_attr(
					sprintf(
						/* translators: %s: Request email. */
						__( 'Mark export request for &#8220;%s&#8221; as completed.' ),
						$item->email
					)
				),
				__( 'Complete request' )
			);
			$complete_request_markup .= '</span>';
		}

		if ( ! empty( $complete_request_markup ) ) {
			$row_actions['complete-request'] = $complete_request_markup;
		}

		return sprintf( '<a href="%1$s">%2$s</a> %3$s', esc_url( 'mailto:' . $item->email ), $item->email, $this->row_actions( $row_actions ) );
	}

	/**
	 * Displays the next steps column.
	 *
	 * @since 4.9.6
	 *
	 * @param WP_User_Request $item Item being shown.
	 */
	public function column_next_steps( $item ) {
		$status = $item->status;

		switch ( $status ) {
			case 'request-pending':
				esc_html_e( 'Waiting for confirmation' );
				break;
			case 'request-confirmed':
				/** This filter is documented in wp-admin/includes/ajax-actions.php */
				$exporters       = apply_filters( 'wp_privacy_personal_data_exporters', array() );
				$exporters_count = count( $exporters );
				$request_id      = $item->ID;
				$nonce           = wp_create_nonce( 'wp-privacy-export-personal-data-' . $request_id );

				echo '<div class="export-personal-data" ' .
					'data-send-as-email="1" ' .
					'data-exporters-count="' . esc_attr( $exporters_count ) . '" ' .
					'data-request-id="' . esc_attr( $request_id ) . '" ' .
					'data-nonce="' . esc_attr( $nonce ) .
					'">';

				?>
				<span class="export-personal-data-idle"><button type="button" class="button-link export-personal-data-handle"><?php _e( 'Send export link' ); ?></button></span>
				<span class="export-personal-data-processing hidden"><?php _e( 'Sending email...' ); ?> <span class="export-progress"></span></span>
				<span class="export-personal-data-success success-message hidden"><?php _e( 'Email sent.' ); ?></span>
				<span class="export-personal-data-failed hidden"><?php _e( 'Email could not be sent.' ); ?> <button type="button" class="button-link export-personal-data-handle"><?php _e( 'Retry' ); ?></button></span>
				<?php

				echo '</div>';
				break;
			case 'request-failed':
				echo '<button type="submit" class="button-link" name="privacy_action_email_retry[' . $item->ID . ']" id="privacy_action_email_retry[' . $item->ID . ']">' . __( 'Retry' ) . '</button>';
				break;
			case 'request-completed':
				echo '<a href="' . esc_url(
					wp_nonce_url(
						add_query_arg(
							array(
								'action'     => 'delete',
								'request_id' => array( $item->ID ),
							),
							admin_url( 'export-personal-data.php' )
						),
						'bulk-privacy_requests'
					)
				) . '">' . esc_html__( 'Remove request' ) . '</a>';
				break;
		}
	}
}
class-plugin-installer-skin.php000060400000027425151327343360012630 0ustar00<?php
/**
 * Upgrader API: Plugin_Installer_Skin class
 *
 * @package WordPress
 * @subpackage Upgrader
 * @since 4.6.0
 */

/**
 * Plugin Installer Skin for WordPress Plugin Installer.
 *
 * @since 2.8.0
 * @since 4.6.0 Moved to its own file from wp-admin/includes/class-wp-upgrader-skins.php.
 *
 * @see WP_Upgrader_Skin
 */
class Plugin_Installer_Skin extends WP_Upgrader_Skin {
	public $api;
	public $type;
	public $url;
	public $overwrite;

	private $is_downgrading = false;

	/**
	 * Constructor.
	 *
	 * Sets up the plugin installer skin.
	 *
	 * @since 2.8.0
	 *
	 * @param array $args
	 */
	public function __construct( $args = array() ) {
		$defaults = array(
			'type'      => 'web',
			'url'       => '',
			'plugin'    => '',
			'nonce'     => '',
			'title'     => '',
			'overwrite' => '',
		);
		$args     = wp_parse_args( $args, $defaults );

		$this->type      = $args['type'];
		$this->url       = $args['url'];
		$this->api       = isset( $args['api'] ) ? $args['api'] : array();
		$this->overwrite = $args['overwrite'];

		parent::__construct( $args );
	}

	/**
	 * Performs an action before installing a plugin.
	 *
	 * @since 2.8.0
	 */
	public function before() {
		if ( ! empty( $this->api ) ) {
			$this->upgrader->strings['process_success'] = sprintf(
				$this->upgrader->strings['process_success_specific'],
				$this->api->name,
				$this->api->version
			);
		}
	}

	/**
	 * Hides the `process_failed` error when updating a plugin by uploading a zip file.
	 *
	 * @since 5.5.0
	 *
	 * @param WP_Error $wp_error WP_Error object.
	 * @return bool True if the error should be hidden, false otherwise.
	 */
	public function hide_process_failed( $wp_error ) {
		if (
			'upload' === $this->type &&
			'' === $this->overwrite &&
			$wp_error->get_error_code() === 'folder_exists'
		) {
			return true;
		}

		return false;
	}

	/**
	 * Performs an action following a plugin install.
	 *
	 * @since 2.8.0
	 */
	public function after() {
		// Check if the plugin can be overwritten and output the HTML.
		if ( $this->do_overwrite() ) {
			return;
		}

		$plugin_file = $this->upgrader->plugin_info();

		$install_actions = array();

		$from = isset( $_GET['from'] ) ? wp_unslash( $_GET['from'] ) : 'plugins';

		if ( 'import' === $from ) {
			$install_actions['activate_plugin'] = sprintf(
				'<a class="button button-primary" href="%s" target="_parent">%s</a>',
				wp_nonce_url( 'plugins.php?action=activate&amp;from=import&amp;plugin=' . urlencode( $plugin_file ), 'activate-plugin_' . $plugin_file ),
				__( 'Activate Plugin &amp; Run Importer' )
			);
		} elseif ( 'press-this' === $from ) {
			$install_actions['activate_plugin'] = sprintf(
				'<a class="button button-primary" href="%s" target="_parent">%s</a>',
				wp_nonce_url( 'plugins.php?action=activate&amp;from=press-this&amp;plugin=' . urlencode( $plugin_file ), 'activate-plugin_' . $plugin_file ),
				__( 'Activate Plugin &amp; Go to Press This' )
			);
		} else {
			$install_actions['activate_plugin'] = sprintf(
				'<a class="button button-primary" href="%s" target="_parent">%s</a>',
				wp_nonce_url( 'plugins.php?action=activate&amp;plugin=' . urlencode( $plugin_file ), 'activate-plugin_' . $plugin_file ),
				__( 'Activate Plugin' )
			);
		}

		if ( is_multisite() && current_user_can( 'manage_network_plugins' ) ) {
			$install_actions['network_activate'] = sprintf(
				'<a class="button button-primary" href="%s" target="_parent">%s</a>',
				wp_nonce_url( 'plugins.php?action=activate&amp;networkwide=1&amp;plugin=' . urlencode( $plugin_file ), 'activate-plugin_' . $plugin_file ),
				_x( 'Network Activate', 'plugin' )
			);
			unset( $install_actions['activate_plugin'] );
		}

		if ( 'import' === $from ) {
			$install_actions['importers_page'] = sprintf(
				'<a href="%s" target="_parent">%s</a>',
				admin_url( 'import.php' ),
				__( 'Go to Importers' )
			);
		} elseif ( 'web' === $this->type ) {
			$install_actions['plugins_page'] = sprintf(
				'<a href="%s" target="_parent">%s</a>',
				self_admin_url( 'plugin-install.php' ),
				__( 'Go to Plugin Installer' )
			);
		} elseif ( 'upload' === $this->type && 'plugins' === $from ) {
			$install_actions['plugins_page'] = sprintf(
				'<a href="%s">%s</a>',
				self_admin_url( 'plugin-install.php' ),
				__( 'Go to Plugin Installer' )
			);
		} else {
			$install_actions['plugins_page'] = sprintf(
				'<a href="%s" target="_parent">%s</a>',
				self_admin_url( 'plugins.php' ),
				__( 'Go to Plugins page' )
			);
		}

		if ( ! $this->result || is_wp_error( $this->result ) ) {
			unset( $install_actions['activate_plugin'], $install_actions['network_activate'] );
		} elseif ( ! current_user_can( 'activate_plugin', $plugin_file ) || is_plugin_active( $plugin_file ) ) {
			unset( $install_actions['activate_plugin'] );
		}

		/**
		 * Filters the list of action links available following a single plugin installation.
		 *
		 * @since 2.7.0
		 *
		 * @param string[] $install_actions Array of plugin action links.
		 * @param object   $api             Object containing WordPress.org API plugin data. Empty
		 *                                  for non-API installs, such as when a plugin is installed
		 *                                  via upload.
		 * @param string   $plugin_file     Path to the plugin file relative to the plugins directory.
		 */
		$install_actions = apply_filters( 'install_plugin_complete_actions', $install_actions, $this->api, $plugin_file );

		if ( ! empty( $install_actions ) ) {
			$this->feedback( implode( ' ', (array) $install_actions ) );
		}
	}

	/**
	 * Checks if the plugin can be overwritten and outputs the HTML for overwriting a plugin on upload.
	 *
	 * @since 5.5.0
	 *
	 * @return bool Whether the plugin can be overwritten and HTML was outputted.
	 */
	private function do_overwrite() {
		if ( 'upload' !== $this->type || ! is_wp_error( $this->result ) || 'folder_exists' !== $this->result->get_error_code() ) {
			return false;
		}

		$folder = $this->result->get_error_data( 'folder_exists' );
		$folder = ltrim( substr( $folder, strlen( WP_PLUGIN_DIR ) ), '/' );

		$current_plugin_data = false;
		$all_plugins         = get_plugins();

		foreach ( $all_plugins as $plugin => $plugin_data ) {
			if ( strrpos( $plugin, $folder ) !== 0 ) {
				continue;
			}

			$current_plugin_data = $plugin_data;
		}

		$new_plugin_data = $this->upgrader->new_plugin_data;

		if ( ! $current_plugin_data || ! $new_plugin_data ) {
			return false;
		}

		echo '<h2 class="update-from-upload-heading">' . esc_html__( 'This plugin is already installed.' ) . '</h2>';

		$this->is_downgrading = version_compare( $current_plugin_data['Version'], $new_plugin_data['Version'], '>' );

		$rows = array(
			'Name'        => __( 'Plugin name' ),
			'Version'     => __( 'Version' ),
			'Author'      => __( 'Author' ),
			'RequiresWP'  => __( 'Required WordPress version' ),
			'RequiresPHP' => __( 'Required PHP version' ),
		);

		$table  = '<table class="update-from-upload-comparison"><tbody>';
		$table .= '<tr><th></th><th>' . esc_html_x( 'Current', 'plugin' ) . '</th>';
		$table .= '<th>' . esc_html_x( 'Uploaded', 'plugin' ) . '</th></tr>';

		$is_same_plugin = true; // Let's consider only these rows.

		foreach ( $rows as $field => $label ) {
			$old_value = ! empty( $current_plugin_data[ $field ] ) ? (string) $current_plugin_data[ $field ] : '-';
			$new_value = ! empty( $new_plugin_data[ $field ] ) ? (string) $new_plugin_data[ $field ] : '-';

			$is_same_plugin = $is_same_plugin && ( $old_value === $new_value );

			$diff_field   = ( 'Version' !== $field && $new_value !== $old_value );
			$diff_version = ( 'Version' === $field && $this->is_downgrading );

			$table .= '<tr><td class="name-label">' . $label . '</td><td>' . wp_strip_all_tags( $old_value ) . '</td>';
			$table .= ( $diff_field || $diff_version ) ? '<td class="warning">' : '<td>';
			$table .= wp_strip_all_tags( $new_value ) . '</td></tr>';
		}

		$table .= '</tbody></table>';

		/**
		 * Filters the compare table output for overwriting a plugin package on upload.
		 *
		 * @since 5.5.0
		 *
		 * @param string $table               The output table with Name, Version, Author, RequiresWP, and RequiresPHP info.
		 * @param array  $current_plugin_data Array with current plugin data.
		 * @param array  $new_plugin_data     Array with uploaded plugin data.
		 */
		echo apply_filters( 'install_plugin_overwrite_comparison', $table, $current_plugin_data, $new_plugin_data );

		$install_actions = array();
		$can_update      = true;

		$blocked_message  = '<p>' . esc_html__( 'The plugin cannot be updated due to the following:' ) . '</p>';
		$blocked_message .= '<ul class="ul-disc">';

		$requires_php = isset( $new_plugin_data['RequiresPHP'] ) ? $new_plugin_data['RequiresPHP'] : null;
		$requires_wp  = isset( $new_plugin_data['RequiresWP'] ) ? $new_plugin_data['RequiresWP'] : null;

		if ( ! is_php_version_compatible( $requires_php ) ) {
			$error = sprintf(
				/* translators: 1: Current PHP version, 2: Version required by the uploaded plugin. */
				__( 'The PHP version on your server is %1$s, however the uploaded plugin requires %2$s.' ),
				PHP_VERSION,
				$requires_php
			);

			$blocked_message .= '<li>' . esc_html( $error ) . '</li>';
			$can_update       = false;
		}

		if ( ! is_wp_version_compatible( $requires_wp ) ) {
			$error = sprintf(
				/* translators: 1: Current WordPress version, 2: Version required by the uploaded plugin. */
				__( 'Your WordPress version is %1$s, however the uploaded plugin requires %2$s.' ),
				esc_html( wp_get_wp_version() ),
				$requires_wp
			);

			$blocked_message .= '<li>' . esc_html( $error ) . '</li>';
			$can_update       = false;
		}

		$blocked_message .= '</ul>';

		if ( $can_update ) {
			if ( $this->is_downgrading ) {
				$warning = sprintf(
					/* translators: %s: Documentation URL. */
					__( 'You are uploading an older version of a current plugin. You can continue to install the older version, but be sure to <a href="%s">back up your database and files</a> first.' ),
					__( 'https://developer.wordpress.org/advanced-administration/security/backup/' )
				);
			} else {
				$warning = sprintf(
					/* translators: %s: Documentation URL. */
					__( 'You are updating a plugin. Be sure to <a href="%s">back up your database and files</a> first.' ),
					__( 'https://developer.wordpress.org/advanced-administration/security/backup/' )
				);
			}

			echo '<p class="update-from-upload-notice">' . $warning . '</p>';

			$overwrite = $this->is_downgrading ? 'downgrade-plugin' : 'update-plugin';

			$install_actions['overwrite_plugin'] = sprintf(
				'<a class="button button-primary update-from-upload-overwrite" href="%s" target="_parent">%s</a>',
				wp_nonce_url( add_query_arg( 'overwrite', $overwrite, $this->url ), 'plugin-upload' ),
				_x( 'Replace current with uploaded', 'plugin' )
			);
		} else {
			echo $blocked_message;
		}

		$cancel_url = add_query_arg( 'action', 'upload-plugin-cancel-overwrite', $this->url );

		$install_actions['plugins_page'] = sprintf(
			'<a class="button" href="%s">%s</a>',
			wp_nonce_url( $cancel_url, 'plugin-upload-cancel-overwrite' ),
			__( 'Cancel and go back' )
		);

		/**
		 * Filters the list of action links available following a single plugin installation failure
		 * when overwriting is allowed.
		 *
		 * @since 5.5.0
		 *
		 * @param string[] $install_actions Array of plugin action links.
		 * @param object   $api             Object containing WordPress.org API plugin data.
		 * @param array    $new_plugin_data Array with uploaded plugin data.
		 */
		$install_actions = apply_filters( 'install_plugin_overwrite_actions', $install_actions, $this->api, $new_plugin_data );

		if ( ! empty( $install_actions ) ) {
			printf(
				'<p class="update-from-upload-expired hidden">%s</p>',
				__( 'The uploaded file has expired. Please go back and upload it again.' )
			);
			echo '<p class="update-from-upload-actions">' . implode( ' ', (array) $install_actions ) . '</p>';
		}

		return true;
	}
}
plugin-install.php000060400000115062151327343360010227 0ustar00<?php
/**
 * WordPress Plugin Install Administration API
 *
 * @package WordPress
 * @subpackage Administration
 */

/**
 * Retrieves plugin installer pages from the WordPress.org Plugins API.
 *
 * It is possible for a plugin to override the Plugin API result with three
 * filters. Assume this is for plugins, which can extend on the Plugin Info to
 * offer more choices. This is very powerful and must be used with care when
 * overriding the filters.
 *
 * The first filter, {@see 'plugins_api_args'}, is for the args and gives the action
 * as the second parameter. The hook for {@see 'plugins_api_args'} must ensure that
 * an object is returned.
 *
 * The second filter, {@see 'plugins_api'}, allows a plugin to override the WordPress.org
 * Plugin Installation API entirely. If `$action` is 'query_plugins' or 'plugin_information',
 * an object MUST be passed. If `$action` is 'hot_tags', an array MUST be passed.
 *
 * Finally, the third filter, {@see 'plugins_api_result'}, makes it possible to filter the
 * response object or array, depending on the `$action` type.
 *
 * Supported arguments per action:
 *
 * | Argument Name        | query_plugins | plugin_information | hot_tags |
 * | -------------------- | :-----------: | :----------------: | :------: |
 * | `$slug`              | No            |  Yes               | No       |
 * | `$per_page`          | Yes           |  No                | No       |
 * | `$page`              | Yes           |  No                | No       |
 * | `$number`            | No            |  No                | Yes      |
 * | `$search`            | Yes           |  No                | No       |
 * | `$tag`               | Yes           |  No                | No       |
 * | `$author`            | Yes           |  No                | No       |
 * | `$user`              | Yes           |  No                | No       |
 * | `$browse`            | Yes           |  No                | No       |
 * | `$locale`            | Yes           |  Yes               | No       |
 * | `$installed_plugins` | Yes           |  No                | No       |
 * | `$is_ssl`            | Yes           |  Yes               | No       |
 * | `$fields`            | Yes           |  Yes               | No       |
 *
 * @since 2.7.0
 *
 * @param string       $action API action to perform: 'query_plugins', 'plugin_information',
 *                             or 'hot_tags'.
 * @param array|object $args   {
 *     Optional. Array or object of arguments to serialize for the Plugin Info API.
 *
 *     @type string  $slug              The plugin slug. Default empty.
 *     @type int     $per_page          Number of plugins per page. Default 24.
 *     @type int     $page              Number of current page. Default 1.
 *     @type int     $number            Number of tags or categories to be queried.
 *     @type string  $search            A search term. Default empty.
 *     @type string  $tag               Tag to filter plugins. Default empty.
 *     @type string  $author            Username of an plugin author to filter plugins. Default empty.
 *     @type string  $user              Username to query for their favorites. Default empty.
 *     @type string  $browse            Browse view: 'popular', 'new', 'beta', 'recommended'.
 *     @type string  $locale            Locale to provide context-sensitive results. Default is the value
 *                                      of get_locale().
 *     @type string  $installed_plugins Installed plugins to provide context-sensitive results.
 *     @type bool    $is_ssl            Whether links should be returned with https or not. Default false.
 *     @type array   $fields            {
 *         Array of fields which should or should not be returned.
 *
 *         @type bool $short_description Whether to return the plugin short description. Default true.
 *         @type bool $description       Whether to return the plugin full description. Default false.
 *         @type bool $sections          Whether to return the plugin readme sections: description, installation,
 *                                       FAQ, screenshots, other notes, and changelog. Default false.
 *         @type bool $tested            Whether to return the 'Compatible up to' value. Default true.
 *         @type bool $requires          Whether to return the required WordPress version. Default true.
 *         @type bool $requires_php      Whether to return the required PHP version. Default true.
 *         @type bool $rating            Whether to return the rating in percent and total number of ratings.
 *                                       Default true.
 *         @type bool $ratings           Whether to return the number of rating for each star (1-5). Default true.
 *         @type bool $downloaded        Whether to return the download count. Default true.
 *         @type bool $downloadlink      Whether to return the download link for the package. Default true.
 *         @type bool $last_updated      Whether to return the date of the last update. Default true.
 *         @type bool $added             Whether to return the date when the plugin was added to the wordpress.org
 *                                       repository. Default true.
 *         @type bool $tags              Whether to return the assigned tags. Default true.
 *         @type bool $compatibility     Whether to return the WordPress compatibility list. Default true.
 *         @type bool $homepage          Whether to return the plugin homepage link. Default true.
 *         @type bool $versions          Whether to return the list of all available versions. Default false.
 *         @type bool $donate_link       Whether to return the donation link. Default true.
 *         @type bool $reviews           Whether to return the plugin reviews. Default false.
 *         @type bool $banners           Whether to return the banner images links. Default false.
 *         @type bool $icons             Whether to return the icon links. Default false.
 *         @type bool $active_installs   Whether to return the number of active installations. Default false.
 *         @type bool $contributors      Whether to return the list of contributors. Default false.
 *     }
 * }
 * @return object|array|WP_Error Response object or array on success, WP_Error on failure. See the
 *         {@link https://developer.wordpress.org/reference/functions/plugins_api/ function reference article}
 *         for more information on the make-up of possible return values depending on the value of `$action`.
 */
function plugins_api( $action, $args = array() ) {
	if ( is_array( $args ) ) {
		$args = (object) $args;
	}

	if ( 'query_plugins' === $action ) {
		if ( ! isset( $args->per_page ) ) {
			$args->per_page = 24;
		}
	}

	if ( ! isset( $args->locale ) ) {
		$args->locale = get_user_locale();
	}

	if ( ! isset( $args->wp_version ) ) {
		$args->wp_version = substr( wp_get_wp_version(), 0, 3 ); // x.y
	}

	/**
	 * Filters the WordPress.org Plugin Installation API arguments.
	 *
	 * Important: An object MUST be returned to this filter.
	 *
	 * @since 2.7.0
	 *
	 * @param object $args   Plugin API arguments.
	 * @param string $action The type of information being requested from the Plugin Installation API.
	 */
	$args = apply_filters( 'plugins_api_args', $args, $action );

	/**
	 * Filters the response for the current WordPress.org Plugin Installation API request.
	 *
	 * Returning a non-false value will effectively short-circuit the WordPress.org API request.
	 *
	 * If `$action` is 'query_plugins' or 'plugin_information', an object MUST be passed.
	 * If `$action` is 'hot_tags', an array should be passed.
	 *
	 * @since 2.7.0
	 *
	 * @param false|object|array $result The result object or array. Default false.
	 * @param string             $action The type of information being requested from the Plugin Installation API.
	 * @param object             $args   Plugin API arguments.
	 */
	$res = apply_filters( 'plugins_api', false, $action, $args );

	if ( false === $res ) {

		$url = 'http://api.wordpress.org/plugins/info/1.2/';
		$url = add_query_arg(
			array(
				'action'  => $action,
				'request' => $args,
			),
			$url
		);

		$http_url = $url;
		$ssl      = wp_http_supports( array( 'ssl' ) );
		if ( $ssl ) {
			$url = set_url_scheme( $url, 'https' );
		}

		$http_args = array(
			'timeout'    => 15,
			'user-agent' => 'WordPress/' . wp_get_wp_version() . '; ' . home_url( '/' ),
		);
		$request   = wp_remote_get( $url, $http_args );

		if ( $ssl && is_wp_error( $request ) ) {
			if ( ! wp_is_json_request() ) {
				wp_trigger_error(
					__FUNCTION__,
					sprintf(
						/* translators: %s: Support forums URL. */
						__( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server&#8217;s configuration. If you continue to have problems, please try the <a href="%s">support forums</a>.' ),
						__( 'https://wordpress.org/support/forums/' )
					) . ' ' . __( '(WordPress could not establish a secure connection to WordPress.org. Please contact your server administrator.)' ),
					headers_sent() || WP_DEBUG ? E_USER_WARNING : E_USER_NOTICE
				);
			}

			$request = wp_remote_get( $http_url, $http_args );
		}

		if ( is_wp_error( $request ) ) {
			$res = new WP_Error(
				'plugins_api_failed',
				sprintf(
					/* translators: %s: Support forums URL. */
					__( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server&#8217;s configuration. If you continue to have problems, please try the <a href="%s">support forums</a>.' ),
					__( 'https://wordpress.org/support/forums/' )
				),
				$request->get_error_message()
			);
		} else {
			$res = json_decode( wp_remote_retrieve_body( $request ), true );
			if ( is_array( $res ) ) {
				// Object casting is required in order to match the info/1.0 format.
				$res = (object) $res;
			} elseif ( null === $res ) {
				$res = new WP_Error(
					'plugins_api_failed',
					sprintf(
						/* translators: %s: Support forums URL. */
						__( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server&#8217;s configuration. If you continue to have problems, please try the <a href="%s">support forums</a>.' ),
						__( 'https://wordpress.org/support/forums/' )
					),
					wp_remote_retrieve_body( $request )
				);
			}

			if ( isset( $res->error ) ) {
				$res = new WP_Error( 'plugins_api_failed', $res->error );
			}
		}
	} elseif ( ! is_wp_error( $res ) ) {
		$res->external = true;
	}

	/**
	 * Filters the Plugin Installation API response results.
	 *
	 * @since 2.7.0
	 *
	 * @param object|WP_Error $res    Response object or WP_Error.
	 * @param string          $action The type of information being requested from the Plugin Installation API.
	 * @param object          $args   Plugin API arguments.
	 */
	return apply_filters( 'plugins_api_result', $res, $action, $args );
}

/**
 * Retrieves popular WordPress plugin tags.
 *
 * @since 2.7.0
 *
 * @param array $args
 * @return array|WP_Error
 */
function install_popular_tags( $args = array() ) {
	$key  = md5( serialize( $args ) );
	$tags = get_site_transient( 'poptags_' . $key );
	if ( false !== $tags ) {
		return $tags;
	}

	$tags = plugins_api( 'hot_tags', $args );

	if ( is_wp_error( $tags ) ) {
		return $tags;
	}

	set_site_transient( 'poptags_' . $key, $tags, 3 * HOUR_IN_SECONDS );

	return $tags;
}

/**
 * Displays the Featured tab of Add Plugins screen.
 *
 * @since 2.7.0
 */
function install_dashboard() {
	display_plugins_table();
	?>

	<div class="plugins-popular-tags-wrapper">
	<h2><?php _e( 'Popular tags' ); ?></h2>
	<p><?php _e( 'You may also browse based on the most popular tags in the Plugin Directory:' ); ?></p>
	<?php

	$api_tags = install_popular_tags();

	echo '<p class="popular-tags">';
	if ( is_wp_error( $api_tags ) ) {
		echo $api_tags->get_error_message();
	} else {
		// Set up the tags in a way which can be interpreted by wp_generate_tag_cloud().
		$tags = array();
		foreach ( (array) $api_tags as $tag ) {
			$url                  = self_admin_url( 'plugin-install.php?tab=search&type=tag&s=' . urlencode( $tag['name'] ) );
			$data                 = array(
				'link'  => esc_url( $url ),
				'name'  => $tag['name'],
				'slug'  => $tag['slug'],
				'id'    => sanitize_title_with_dashes( $tag['name'] ),
				'count' => $tag['count'],
			);
			$tags[ $tag['name'] ] = (object) $data;
		}
		echo wp_generate_tag_cloud(
			$tags,
			array(
				/* translators: %s: Number of plugins. */
				'single_text'   => __( '%s plugin' ),
				/* translators: %s: Number of plugins. */
				'multiple_text' => __( '%s plugins' ),
			)
		);
	}
	echo '</p><br class="clear" /></div>';
}

/**
 * Displays a search form for searching plugins.
 *
 * @since 2.7.0
 * @since 4.6.0 The `$type_selector` parameter was deprecated.
 *
 * @param bool $deprecated Not used.
 */
function install_search_form( $deprecated = true ) {
	$type = isset( $_REQUEST['type'] ) ? wp_unslash( $_REQUEST['type'] ) : 'term';
	$term = isset( $_REQUEST['s'] ) ? urldecode( wp_unslash( $_REQUEST['s'] ) ) : '';
	?>
	<form class="search-form search-plugins" method="get">
		<input type="hidden" name="tab" value="search" />
		<label for="search-plugins"><?php _e( 'Search Plugins' ); ?></label>
		<input type="search" name="s" id="search-plugins" value="<?php echo esc_attr( $term ); ?>" class="wp-filter-search" />
		<label class="screen-reader-text" for="typeselector">
			<?php
			/* translators: Hidden accessibility text. */
			_e( 'Search plugins by:' );
			?>
		</label>
		<select name="type" id="typeselector">
			<option value="term"<?php selected( 'term', $type ); ?>><?php _e( 'Keyword' ); ?></option>
			<option value="author"<?php selected( 'author', $type ); ?>><?php _e( 'Author' ); ?></option>
			<option value="tag"<?php selected( 'tag', $type ); ?>><?php _ex( 'Tag', 'Plugin Installer' ); ?></option>
		</select>
		<?php submit_button( __( 'Search Plugins' ), 'hide-if-js', false, false, array( 'id' => 'search-submit' ) ); ?>
	</form>
	<?php
}

/**
 * Displays a form to upload plugins from zip files.
 *
 * @since 2.8.0
 */
function install_plugins_upload() {
	?>
<div class="upload-plugin">
	<p class="install-help"><?php _e( 'If you have a plugin in a .zip format, you may install or update it by uploading it here.' ); ?></p>
	<form method="post" enctype="multipart/form-data" class="wp-upload-form" action="<?php echo esc_url( self_admin_url( 'update.php?action=upload-plugin' ) ); ?>">
		<?php wp_nonce_field( 'plugin-upload' ); ?>
		<label class="screen-reader-text" for="pluginzip">
			<?php
			/* translators: Hidden accessibility text. */
			_e( 'Plugin zip file' );
			?>
		</label>
		<input type="file" id="pluginzip" name="pluginzip" accept=".zip" />
		<?php submit_button( _x( 'Install Now', 'plugin' ), '', 'install-plugin-submit', false ); ?>
	</form>
</div>
	<?php
}

/**
 * Shows a username form for the favorites page.
 *
 * @since 3.5.0
 */
function install_plugins_favorites_form() {
	$user   = get_user_option( 'wporg_favorites' );
	$action = 'save_wporg_username_' . get_current_user_id();
	?>
	<p><?php _e( 'If you have marked plugins as favorites on WordPress.org, you can browse them here.' ); ?></p>
	<form method="get">
		<input type="hidden" name="tab" value="favorites" />
		<p>
			<label for="user"><?php _e( 'Your WordPress.org username:' ); ?></label>
			<input type="search" id="user" name="user" value="<?php echo esc_attr( $user ); ?>" />
			<input type="submit" class="button" value="<?php esc_attr_e( 'Get Favorites' ); ?>" />
			<input type="hidden" id="wporg-username-nonce" name="_wpnonce" value="<?php echo esc_attr( wp_create_nonce( $action ) ); ?>" />
		</p>
	</form>
	<?php
}

/**
 * Displays plugin content based on plugin list.
 *
 * @since 2.7.0
 *
 * @global WP_List_Table $wp_list_table
 */
function display_plugins_table() {
	global $wp_list_table;

	switch ( current_filter() ) {
		case 'install_plugins_beta':
			printf(
				/* translators: %s: URL to "Features as Plugins" page. */
				'<p>' . __( 'You are using a development version of WordPress. These feature plugins are also under development. <a href="%s">Learn more</a>.' ) . '</p>',
				'https://make.wordpress.org/core/handbook/about/release-cycle/features-as-plugins/'
			);
			break;
		case 'install_plugins_featured':
			printf(
				/* translators: %s: https://wordpress.org/plugins/ */
				'<p>' . __( 'Plugins extend and expand the functionality of WordPress. You may install plugins in the <a href="%s">WordPress Plugin Directory</a> right from here, or upload a plugin in .zip format by clicking the button at the top of this page.' ) . '</p>',
				__( 'https://wordpress.org/plugins/' )
			);
			break;
		case 'install_plugins_recommended':
			echo '<p>' . __( 'These suggestions are based on the plugins you and other users have installed.' ) . '</p>';
			break;
		case 'install_plugins_favorites':
			if ( empty( $_GET['user'] ) && ! get_user_option( 'wporg_favorites' ) ) {
				return;
			}
			break;
	}
	?>
	<form id="plugin-filter" method="post">
		<?php $wp_list_table->display(); ?>
	</form>
	<?php
}

/**
 * Determines the status we can perform on a plugin.
 *
 * @since 3.0.0
 *
 * @param array|object $api  Data about the plugin retrieved from the API.
 * @param bool         $loop Optional. Disable further loops. Default false.
 * @return array {
 *     Plugin installation status data.
 *
 *     @type string $status  Status of a plugin. Could be one of 'install', 'update_available', 'latest_installed' or 'newer_installed'.
 *     @type string $url     Plugin installation URL.
 *     @type string $version The most recent version of the plugin.
 *     @type string $file    Plugin filename relative to the plugins directory.
 * }
 */
function install_plugin_install_status( $api, $loop = false ) {
	// This function is called recursively, $loop prevents further loops.
	if ( is_array( $api ) ) {
		$api = (object) $api;
	}

	// Default to a "new" plugin.
	$status      = 'install';
	$url         = false;
	$update_file = false;
	$version     = '';

	/*
	 * Check to see if this plugin is known to be installed,
	 * and has an update awaiting it.
	 */
	$update_plugins = get_site_transient( 'update_plugins' );
	if ( isset( $update_plugins->response ) ) {
		foreach ( (array) $update_plugins->response as $file => $plugin ) {
			if ( $plugin->slug === $api->slug ) {
				$status      = 'update_available';
				$update_file = $file;
				$version     = $plugin->new_version;
				if ( current_user_can( 'update_plugins' ) ) {
					$url = wp_nonce_url( self_admin_url( 'update.php?action=upgrade-plugin&plugin=' . $update_file ), 'upgrade-plugin_' . $update_file );
				}
				break;
			}
		}
	}

	if ( 'install' === $status ) {
		if ( is_dir( WP_PLUGIN_DIR . '/' . $api->slug ) ) {
			$installed_plugin = get_plugins( '/' . $api->slug );
			if ( empty( $installed_plugin ) ) {
				if ( current_user_can( 'install_plugins' ) ) {
					$url = wp_nonce_url( self_admin_url( 'update.php?action=install-plugin&plugin=' . $api->slug ), 'install-plugin_' . $api->slug );
				}
			} else {
				$key = array_keys( $installed_plugin );
				/*
				 * Use the first plugin regardless of the name.
				 * Could have issues for multiple plugins in one directory if they share different version numbers.
				 */
				$key = reset( $key );

				$update_file = $api->slug . '/' . $key;
				if ( version_compare( $api->version, $installed_plugin[ $key ]['Version'], '=' ) ) {
					$status = 'latest_installed';
				} elseif ( version_compare( $api->version, $installed_plugin[ $key ]['Version'], '<' ) ) {
					$status  = 'newer_installed';
					$version = $installed_plugin[ $key ]['Version'];
				} else {
					// If the above update check failed, then that probably means that the update checker has out-of-date information, force a refresh.
					if ( ! $loop ) {
						delete_site_transient( 'update_plugins' );
						wp_update_plugins();
						return install_plugin_install_status( $api, true );
					}
				}
			}
		} else {
			// "install" & no directory with that slug.
			if ( current_user_can( 'install_plugins' ) ) {
				$url = wp_nonce_url( self_admin_url( 'update.php?action=install-plugin&plugin=' . $api->slug ), 'install-plugin_' . $api->slug );
			}
		}
	}
	if ( isset( $_GET['from'] ) ) {
		$url .= '&amp;from=' . urlencode( wp_unslash( $_GET['from'] ) );
	}

	$file = $update_file;
	return compact( 'status', 'url', 'version', 'file' );
}

/**
 * Displays plugin information in dialog box form.
 *
 * @since 2.7.0
 *
 * @global string $tab
 */
function install_plugin_information() {
	global $tab;

	if ( empty( $_REQUEST['plugin'] ) ) {
		return;
	}

	$api = plugins_api(
		'plugin_information',
		array(
			'slug' => wp_unslash( $_REQUEST['plugin'] ),
		)
	);

	if ( is_wp_error( $api ) ) {
		wp_die( $api );
	}

	$plugins_allowedtags = array(
		'a'          => array(
			'href'   => array(),
			'title'  => array(),
			'target' => array(),
		),
		'abbr'       => array( 'title' => array() ),
		'acronym'    => array( 'title' => array() ),
		'code'       => array(),
		'pre'        => array(),
		'em'         => array(),
		'strong'     => array(),
		'div'        => array( 'class' => array() ),
		'span'       => array( 'class' => array() ),
		'p'          => array(),
		'br'         => array(),
		'ul'         => array(),
		'ol'         => array(),
		'li'         => array(),
		'h1'         => array(),
		'h2'         => array(),
		'h3'         => array(),
		'h4'         => array(),
		'h5'         => array(),
		'h6'         => array(),
		'img'        => array(
			'src'   => array(),
			'class' => array(),
			'alt'   => array(),
		),
		'blockquote' => array( 'cite' => true ),
	);

	$plugins_section_titles = array(
		'description'  => _x( 'Description', 'Plugin installer section title' ),
		'installation' => _x( 'Installation', 'Plugin installer section title' ),
		'faq'          => _x( 'FAQ', 'Plugin installer section title' ),
		'screenshots'  => _x( 'Screenshots', 'Plugin installer section title' ),
		'changelog'    => _x( 'Changelog', 'Plugin installer section title' ),
		'reviews'      => _x( 'Reviews', 'Plugin installer section title' ),
		'other_notes'  => _x( 'Other Notes', 'Plugin installer section title' ),
	);

	// Sanitize HTML.
	foreach ( (array) $api->sections as $section_name => $content ) {
		$api->sections[ $section_name ] = wp_kses( $content, $plugins_allowedtags );
	}

	foreach ( array( 'version', 'author', 'requires', 'tested', 'homepage', 'downloaded', 'slug' ) as $key ) {
		if ( isset( $api->$key ) ) {
			$api->$key = wp_kses( $api->$key, $plugins_allowedtags );
		}
	}

	$_tab = esc_attr( $tab );

	// Default to the Description tab, Do not translate, API returns English.
	$section = isset( $_REQUEST['section'] ) ? wp_unslash( $_REQUEST['section'] ) : 'description';
	if ( empty( $section ) || ! isset( $api->sections[ $section ] ) ) {
		$section_titles = array_keys( (array) $api->sections );
		$section        = reset( $section_titles );
	}

	iframe_header( __( 'Plugin Installation' ) );

	$_with_banner = '';

	if ( ! empty( $api->banners ) && ( ! empty( $api->banners['low'] ) || ! empty( $api->banners['high'] ) ) ) {
		$_with_banner = 'with-banner';
		$low          = empty( $api->banners['low'] ) ? $api->banners['high'] : $api->banners['low'];
		$high         = empty( $api->banners['high'] ) ? $api->banners['low'] : $api->banners['high'];
		?>
		<style type="text/css">
			#plugin-information-title.with-banner {
				background-image: url( <?php echo esc_url( $low ); ?> );
			}
			@media only screen and ( -webkit-min-device-pixel-ratio: 1.5 ) {
				#plugin-information-title.with-banner {
					background-image: url( <?php echo esc_url( $high ); ?> );
				}
			}
		</style>
		<?php
	}

	echo '<div id="plugin-information-scrollable">';
	echo "<div id='{$_tab}-title' class='{$_with_banner}'><div class='vignette'></div><h2>{$api->name}</h2></div>";
	echo "<div id='{$_tab}-tabs' class='{$_with_banner}'>\n";

	foreach ( (array) $api->sections as $section_name => $content ) {
		if ( 'reviews' === $section_name && ( empty( $api->ratings ) || 0 === array_sum( (array) $api->ratings ) ) ) {
			continue;
		}

		if ( isset( $plugins_section_titles[ $section_name ] ) ) {
			$title = $plugins_section_titles[ $section_name ];
		} else {
			$title = ucwords( str_replace( '_', ' ', $section_name ) );
		}

		$class       = ( $section_name === $section ) ? ' class="current"' : '';
		$href        = add_query_arg(
			array(
				'tab'     => $tab,
				'section' => $section_name,
			)
		);
		$href        = esc_url( $href );
		$san_section = esc_attr( $section_name );
		echo "\t<a name='$san_section' href='$href' $class>$title</a>\n";
	}

	echo "</div>\n";

	?>
<div id="<?php echo $_tab; ?>-content" class='<?php echo $_with_banner; ?>'>
	<div class="fyi">
		<ul>
			<?php if ( ! empty( $api->version ) ) { ?>
				<li><strong><?php _e( 'Version:' ); ?></strong> <?php echo $api->version; ?></li>
			<?php } if ( ! empty( $api->author ) ) { ?>
				<li><strong><?php _e( 'Author:' ); ?></strong> <?php echo links_add_target( $api->author, '_blank' ); ?></li>
			<?php } if ( ! empty( $api->last_updated ) ) { ?>
				<li><strong><?php _e( 'Last Updated:' ); ?></strong>
					<?php
					/* translators: %s: Human-readable time difference. */
					printf( __( '%s ago' ), human_time_diff( strtotime( $api->last_updated ) ) );
					?>
				</li>
			<?php } if ( ! empty( $api->requires ) ) { ?>
				<li>
					<strong><?php _e( 'Requires WordPress Version:' ); ?></strong>
					<?php
					/* translators: %s: Version number. */
					printf( __( '%s or higher' ), $api->requires );
					?>
				</li>
			<?php } if ( ! empty( $api->tested ) ) { ?>
				<li><strong><?php _e( 'Compatible up to:' ); ?></strong> <?php echo $api->tested; ?></li>
			<?php } if ( ! empty( $api->requires_php ) ) { ?>
				<li>
					<strong><?php _e( 'Requires PHP Version:' ); ?></strong>
					<?php
					/* translators: %s: Version number. */
					printf( __( '%s or higher' ), $api->requires_php );
					?>
				</li>
			<?php } if ( isset( $api->active_installs ) ) { ?>
				<li><strong><?php _e( 'Active Installations:' ); ?></strong>
				<?php
				if ( $api->active_installs >= 1000000 ) {
					$active_installs_millions = floor( $api->active_installs / 1000000 );
					printf(
						/* translators: %s: Number of millions. */
						_nx( '%s+ Million', '%s+ Million', $active_installs_millions, 'Active plugin installations' ),
						number_format_i18n( $active_installs_millions )
					);
				} elseif ( $api->active_installs < 10 ) {
					_ex( 'Less Than 10', 'Active plugin installations' );
				} else {
					echo number_format_i18n( $api->active_installs ) . '+';
				}
				?>
				</li>
			<?php } if ( ! empty( $api->slug ) && empty( $api->external ) ) { ?>
				<li><a target="_blank" href="<?php echo esc_url( __( 'https://wordpress.org/plugins/' ) . $api->slug ); ?>/"><?php _e( 'WordPress.org Plugin Page &#187;' ); ?></a></li>
			<?php } if ( ! empty( $api->homepage ) ) { ?>
				<li><a target="_blank" href="<?php echo esc_url( $api->homepage ); ?>"><?php _e( 'Plugin Homepage &#187;' ); ?></a></li>
			<?php } if ( ! empty( $api->donate_link ) && empty( $api->contributors ) ) { ?>
				<li><a target="_blank" href="<?php echo esc_url( $api->donate_link ); ?>"><?php _e( 'Donate to this plugin &#187;' ); ?></a></li>
			<?php } ?>
		</ul>
		<?php if ( ! empty( $api->rating ) ) { ?>
			<h3><?php _e( 'Average Rating' ); ?></h3>
			<?php
			wp_star_rating(
				array(
					'rating' => $api->rating,
					'type'   => 'percent',
					'number' => $api->num_ratings,
				)
			);
			?>
			<p aria-hidden="true" class="fyi-description">
				<?php
				printf(
					/* translators: %s: Number of ratings. */
					_n( '(based on %s rating)', '(based on %s ratings)', $api->num_ratings ),
					number_format_i18n( $api->num_ratings )
				);
				?>
			</p>
			<?php
		}

		if ( ! empty( $api->ratings ) && array_sum( (array) $api->ratings ) > 0 ) {
			?>
			<h3><?php _e( 'Reviews' ); ?></h3>
			<p class="fyi-description"><?php _e( 'Read all reviews on WordPress.org or write your own!' ); ?></p>
			<?php
			foreach ( $api->ratings as $key => $ratecount ) {
				// Avoid div-by-zero.
				$_rating    = $api->num_ratings ? ( $ratecount / $api->num_ratings ) : 0;
				$aria_label = esc_attr(
					sprintf(
						/* translators: 1: Number of stars (used to determine singular/plural), 2: Number of reviews. */
						_n(
							'Reviews with %1$d star: %2$s. Opens in a new tab.',
							'Reviews with %1$d stars: %2$s. Opens in a new tab.',
							$key
						),
						$key,
						number_format_i18n( $ratecount )
					)
				);
				?>
				<div class="counter-container">
						<span class="counter-label">
							<?php
							printf(
								'<a href="%s" target="_blank" aria-label="%s">%s</a>',
								"https://wordpress.org/support/plugin/{$api->slug}/reviews/?filter={$key}",
								$aria_label,
								/* translators: %s: Number of stars. */
								sprintf( _n( '%d star', '%d stars', $key ), $key )
							);
							?>
						</span>
						<span class="counter-back">
							<span class="counter-bar" style="width: <?php echo 92 * $_rating; ?>px;"></span>
						</span>
					<span class="counter-count" aria-hidden="true"><?php echo number_format_i18n( $ratecount ); ?></span>
				</div>
				<?php
			}
		}
		if ( ! empty( $api->contributors ) ) {
			?>
			<h3><?php _e( 'Contributors' ); ?></h3>
			<ul class="contributors">
				<?php
				foreach ( (array) $api->contributors as $contrib_username => $contrib_details ) {
					$contrib_name = $contrib_details['display_name'];
					if ( ! $contrib_name ) {
						$contrib_name = $contrib_username;
					}
					$contrib_name = esc_html( $contrib_name );

					$contrib_profile = esc_url( $contrib_details['profile'] );
					$contrib_avatar  = esc_url( add_query_arg( 's', '36', $contrib_details['avatar'] ) );

					echo "<li><a href='{$contrib_profile}' target='_blank'><img src='{$contrib_avatar}' width='18' height='18' alt='' />{$contrib_name}</a></li>";
				}
				?>
			</ul>
					<?php if ( ! empty( $api->donate_link ) ) { ?>
				<a target="_blank" href="<?php echo esc_url( $api->donate_link ); ?>"><?php _e( 'Donate to this plugin &#187;' ); ?></a>
			<?php } ?>
				<?php } ?>
	</div>
	<div id="section-holder">
	<?php
	$requires_php = isset( $api->requires_php ) ? $api->requires_php : null;
	$requires_wp  = isset( $api->requires ) ? $api->requires : null;

	$compatible_php = is_php_version_compatible( $requires_php );
	$compatible_wp  = is_wp_version_compatible( $requires_wp );
	$tested_wp      = ( empty( $api->tested ) || version_compare( get_bloginfo( 'version' ), $api->tested, '<=' ) );

	if ( ! $compatible_php ) {
		$compatible_php_notice_message  = '<p>';
		$compatible_php_notice_message .= __( '<strong>Error:</strong> This plugin <strong>requires a newer version of PHP</strong>.' );

		if ( current_user_can( 'update_php' ) ) {
			$compatible_php_notice_message .= sprintf(
				/* translators: %s: URL to Update PHP page. */
				' ' . __( '<a href="%s" target="_blank">Click here to learn more about updating PHP</a>.' ),
				esc_url( wp_get_update_php_url() )
			) . wp_update_php_annotation( '</p><p><em>', '</em>', false );
		} else {
			$compatible_php_notice_message .= '</p>';
		}

		wp_admin_notice(
			$compatible_php_notice_message,
			array(
				'type'               => 'error',
				'additional_classes' => array( 'notice-alt' ),
				'paragraph_wrap'     => false,
			)
		);
	}

	if ( ! $tested_wp ) {
		wp_admin_notice(
			__( '<strong>Warning:</strong> This plugin <strong>has not been tested</strong> with your current version of WordPress.' ),
			array(
				'type'               => 'warning',
				'additional_classes' => array( 'notice-alt' ),
			)
		);
	} elseif ( ! $compatible_wp ) {
		$compatible_wp_notice_message = __( '<strong>Error:</strong> This plugin <strong>requires a newer version of WordPress</strong>.' );
		if ( current_user_can( 'update_core' ) ) {
			$compatible_wp_notice_message .= sprintf(
				/* translators: %s: URL to WordPress Updates screen. */
				' ' . __( '<a href="%s" target="_parent">Click here to update WordPress</a>.' ),
				esc_url( self_admin_url( 'update-core.php' ) )
			);
		}

		wp_admin_notice(
			$compatible_wp_notice_message,
			array(
				'type'               => 'error',
				'additional_classes' => array( 'notice-alt' ),
			)
		);
	}

	foreach ( (array) $api->sections as $section_name => $content ) {
		$content = links_add_base_url( $content, 'https://wordpress.org/plugins/' . $api->slug . '/' );
		$content = links_add_target( $content, '_blank' );

		$san_section = esc_attr( $section_name );

		$display = ( $section_name === $section ) ? 'block' : 'none';

		echo "\t<div id='section-{$san_section}' class='section' style='display: {$display};'>\n";
		echo $content;
		echo "\t</div>\n";
	}
	echo "</div>\n";
	echo "</div>\n";
	echo "</div>\n"; // #plugin-information-scrollable
	echo "<div id='$tab-footer'>\n";
	if ( ! empty( $api->download_link ) && ( current_user_can( 'install_plugins' ) || current_user_can( 'update_plugins' ) ) ) {
		$button = wp_get_plugin_action_button( $api->name, $api, $compatible_php, $compatible_wp );
		$button = str_replace( 'class="', 'class="right ', $button );

		if ( ! str_contains( $button, _x( 'Activate', 'plugin' ) ) ) {
			$button = str_replace( 'class="', 'id="plugin_install_from_iframe" class="', $button );
		}

		echo wp_kses_post( $button );
	}
	echo "</div>\n";

	wp_print_request_filesystem_credentials_modal();
	wp_print_admin_notice_templates();

	iframe_footer();
	exit;
}

/**
 * Gets the markup for the plugin install action button.
 *
 * @since 6.5.0
 *
 * @param string       $name           Plugin name.
 * @param array|object $data           {
 *     An array or object of plugin data. Can be retrieved from the API.
 *
 *     @type string   $slug             The plugin slug.
 *     @type string[] $requires_plugins An array of plugin dependency slugs.
 *     @type string   $version          The plugin's version string. Used when getting the install status.
 * }
 * @param bool         $compatible_php   The result of a PHP compatibility check.
 * @param bool         $compatible_wp    The result of a WP compatibility check.
 * @return string The markup for the dependency row button. An empty string if the user does not have capabilities.
 */
function wp_get_plugin_action_button( $name, $data, $compatible_php, $compatible_wp ) {
	$button           = '';
	$data             = (object) $data;
	$status           = install_plugin_install_status( $data );
	$requires_plugins = $data->requires_plugins ?? array();

	// Determine the status of plugin dependencies.
	$installed_plugins                   = get_plugins();
	$active_plugins                      = get_option( 'active_plugins', array() );
	$plugin_dependencies_count           = count( $requires_plugins );
	$installed_plugin_dependencies_count = 0;
	$active_plugin_dependencies_count    = 0;
	foreach ( $requires_plugins as $dependency ) {
		foreach ( array_keys( $installed_plugins ) as $installed_plugin_file ) {
			if ( str_contains( $installed_plugin_file, '/' ) && explode( '/', $installed_plugin_file )[0] === $dependency ) {
				++$installed_plugin_dependencies_count;
			}
		}

		foreach ( $active_plugins as $active_plugin_file ) {
			if ( str_contains( $active_plugin_file, '/' ) && explode( '/', $active_plugin_file )[0] === $dependency ) {
				++$active_plugin_dependencies_count;
			}
		}
	}
	$all_plugin_dependencies_installed = $installed_plugin_dependencies_count === $plugin_dependencies_count;
	$all_plugin_dependencies_active    = $active_plugin_dependencies_count === $plugin_dependencies_count;

	if ( current_user_can( 'install_plugins' ) || current_user_can( 'update_plugins' ) ) {
		switch ( $status['status'] ) {
			case 'install':
				if ( $status['url'] ) {
					if ( $compatible_php && $compatible_wp && $all_plugin_dependencies_installed && ! empty( $data->download_link ) ) {
						$button = sprintf(
							'<a class="install-now button" data-slug="%s" href="%s" aria-label="%s" data-name="%s" role="button">%s</a>',
							esc_attr( $data->slug ),
							esc_url( $status['url'] ),
							/* translators: %s: Plugin name and version. */
							esc_attr( sprintf( _x( 'Install %s now', 'plugin' ), $name ) ),
							esc_attr( $name ),
							_x( 'Install Now', 'plugin' )
						);
					} else {
						$button = sprintf(
							'<button type="button" class="install-now button button-disabled" disabled="disabled">%s</button>',
							_x( 'Install Now', 'plugin' )
						);
					}
				}
				break;

			case 'update_available':
				if ( $status['url'] ) {
					if ( $compatible_php && $compatible_wp ) {
						$button = sprintf(
							'<a class="update-now button aria-button-if-js" data-plugin="%s" data-slug="%s" href="%s" aria-label="%s" data-name="%s" role="button">%s</a>',
							esc_attr( $status['file'] ),
							esc_attr( $data->slug ),
							esc_url( $status['url'] ),
							/* translators: %s: Plugin name and version. */
							esc_attr( sprintf( _x( 'Update %s now', 'plugin' ), $name ) ),
							esc_attr( $name ),
							_x( 'Update Now', 'plugin' )
						);
					} else {
						$button = sprintf(
							'<button type="button" class="button button-disabled" disabled="disabled">%s</button>',
							_x( 'Update Now', 'plugin' )
						);
					}
				}
				break;

			case 'latest_installed':
			case 'newer_installed':
				if ( is_plugin_active( $status['file'] ) ) {
					$button = sprintf(
						'<button type="button" class="button button-disabled" disabled="disabled">%s</button>',
						_x( 'Active', 'plugin' )
					);
				} elseif ( current_user_can( 'activate_plugin', $status['file'] ) ) {
					if ( $compatible_php && $compatible_wp && $all_plugin_dependencies_active ) {
						$button_text = _x( 'Activate', 'plugin' );
						/* translators: %s: Plugin name. */
						$button_label = _x( 'Activate %s', 'plugin' );
						$activate_url = add_query_arg(
							array(
								'_wpnonce' => wp_create_nonce( 'activate-plugin_' . $status['file'] ),
								'action'   => 'activate',
								'plugin'   => $status['file'],
							),
							network_admin_url( 'plugins.php' )
						);

						if ( is_network_admin() ) {
							$button_text = _x( 'Network Activate', 'plugin' );
							/* translators: %s: Plugin name. */
							$button_label = _x( 'Network Activate %s', 'plugin' );
							$activate_url = add_query_arg( array( 'networkwide' => 1 ), $activate_url );
						}

						$button = sprintf(
							'<a href="%1$s" data-name="%2$s" data-slug="%3$s" data-plugin="%4$s" class="button button-primary activate-now" aria-label="%5$s" role="button">%6$s</a>',
							esc_url( $activate_url ),
							esc_attr( $name ),
							esc_attr( $data->slug ),
							esc_attr( $status['file'] ),
							esc_attr( sprintf( $button_label, $name ) ),
							$button_text
						);
					} else {
						$button = sprintf(
							'<button type="button" class="button button-disabled" disabled="disabled">%s</button>',
							is_network_admin() ? _x( 'Network Activate', 'plugin' ) : _x( 'Activate', 'plugin' )
						);
					}
				} else {
					$button = sprintf(
						'<button type="button" class="button button-disabled" disabled="disabled">%s</button>',
						_x( 'Installed', 'plugin' )
					);
				}
				break;
		}
	}

	return $button;
}
translation-install.php000060400000025503151327343360011267 0ustar00<?php
/**
 * WordPress Translation Installation Administration API
 *
 * @package WordPress
 * @subpackage Administration
 */


/**
 * Retrieve translations from WordPress Translation API.
 *
 * @since 4.0.0
 *
 * @param string       $type Type of translations. Accepts 'plugins', 'themes', 'core'.
 * @param array|object $args Translation API arguments. Optional.
 * @return array|WP_Error {
 *     On success an associative array of translations, WP_Error on failure.
 *
 *     @type array $translations {
 *         List of translations, each an array of data.
 *
 *         @type array ...$0 {
 *             @type string   $language     Language code.
 *             @type string   $version      WordPress version.
 *             @type string   $updated      Date the translation was last updated, in MySQL datetime format.
 *             @type string   $english_name English name of the language.
 *             @type string   $native_name  Native name of the language.
 *             @type string   $package      URL to download the translation package.
 *             @type string[] $iso          Array of ISO language codes.
 *             @type array    $strings      Array of translated strings used in the installation process.
 *         }
 *     }
 * }
 */
function translations_api( $type, $args = null ) {
	if ( ! in_array( $type, array( 'plugins', 'themes', 'core' ), true ) ) {
		return new WP_Error( 'invalid_type', __( 'Invalid translation type.' ) );
	}

	/**
	 * Allows a plugin to override the WordPress.org Translation Installation API entirely.
	 *
	 * @since 4.0.0
	 *
	 * @param false|array $result The result array. Default false.
	 * @param string      $type   The type of translations being requested.
	 * @param object      $args   Translation API arguments.
	 */
	$res = apply_filters( 'translations_api', false, $type, $args );

	if ( false === $res ) {
		$url      = 'http://api.wordpress.org/translations/' . $type . '/1.0/';
		$http_url = $url;
		$ssl      = wp_http_supports( array( 'ssl' ) );
		if ( $ssl ) {
			$url = set_url_scheme( $url, 'https' );
		}

		$options = array(
			'timeout' => 3,
			'body'    => array(
				'wp_version' => wp_get_wp_version(),
				'locale'     => get_locale(),
				'version'    => $args['version'], // Version of plugin, theme or core.
			),
		);

		if ( 'core' !== $type ) {
			$options['body']['slug'] = $args['slug']; // Plugin or theme slug.
		}

		$request = wp_remote_post( $url, $options );

		if ( $ssl && is_wp_error( $request ) ) {
			wp_trigger_error(
				__FUNCTION__,
				sprintf(
					/* translators: %s: Support forums URL. */
					__( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server&#8217;s configuration. If you continue to have problems, please try the <a href="%s">support forums</a>.' ),
					__( 'https://wordpress.org/support/forums/' )
				) . ' ' . __( '(WordPress could not establish a secure connection to WordPress.org. Please contact your server administrator.)' ),
				headers_sent() || WP_DEBUG ? E_USER_WARNING : E_USER_NOTICE
			);

			$request = wp_remote_post( $http_url, $options );
		}

		if ( is_wp_error( $request ) ) {
			$res = new WP_Error(
				'translations_api_failed',
				sprintf(
					/* translators: %s: Support forums URL. */
					__( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server&#8217;s configuration. If you continue to have problems, please try the <a href="%s">support forums</a>.' ),
					__( 'https://wordpress.org/support/forums/' )
				),
				$request->get_error_message()
			);
		} else {
			$res = json_decode( wp_remote_retrieve_body( $request ), true );
			if ( ! is_object( $res ) && ! is_array( $res ) ) {
				$res = new WP_Error(
					'translations_api_failed',
					sprintf(
						/* translators: %s: Support forums URL. */
						__( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server&#8217;s configuration. If you continue to have problems, please try the <a href="%s">support forums</a>.' ),
						__( 'https://wordpress.org/support/forums/' )
					),
					wp_remote_retrieve_body( $request )
				);
			}
		}
	}

	/**
	 * Filters the Translation Installation API response results.
	 *
	 * @since 4.0.0
	 *
	 * @param array|WP_Error $res  {
	 *     On success an associative array of translations, WP_Error on failure.
	 *
	 *     @type array $translations {
	 *         List of translations, each an array of data.
	 *
	 *         @type array ...$0 {
	 *             @type string   $language     Language code.
	 *             @type string   $version      WordPress version.
	 *             @type string   $updated      Date the translation was last updated, in MySQL datetime format.
	 *             @type string   $english_name English name of the language.
	 *             @type string   $native_name  Native name of the language.
	 *             @type string   $package      URL to download the translation package.
	 *             @type string[] $iso          Array of ISO language codes.
	 *             @type array    $strings      Array of translated strings used in the installation process.
	 *         }
	 *     }
	 * }
	 * @param string         $type The type of translations being requested.
	 * @param object         $args Translation API arguments.
	 */
	return apply_filters( 'translations_api_result', $res, $type, $args );
}

/**
 * Get available translations from the WordPress.org API.
 *
 * @since 4.0.0
 *
 * @see translations_api()
 *
 * @return array {
 *     Array of translations keyed by the language code, each an associative array of data.
 *     If the API response results in an error, an empty array will be returned.
 *
 *     @type array ...$0 {
 *         @type string   $language     Language code.
 *         @type string   $version      WordPress version.
 *         @type string   $updated      Date the translation was last updated, in MySQL datetime format.
 *         @type string   $english_name English name of the language.
 *         @type string   $native_name  Native name of the language.
 *         @type string   $package      URL to download the translation package.
 *         @type string[] $iso          Array of ISO language codes.
 *         @type array    $strings      Array of translated strings used in the installation process.
 *     }
 * }
 */
function wp_get_available_translations() {
	if ( ! wp_installing() ) {
		$translations = get_site_transient( 'available_translations' );
		if ( false !== $translations ) {
			return $translations;
		}
	}

	$api = translations_api( 'core', array( 'version' => wp_get_wp_version() ) );

	if ( is_wp_error( $api ) || empty( $api['translations'] ) ) {
		return array();
	}

	$translations = array();
	// Key the array with the language code.
	foreach ( $api['translations'] as $translation ) {
		$translations[ $translation['language'] ] = $translation;
	}

	if ( ! defined( 'WP_INSTALLING' ) ) {
		set_site_transient( 'available_translations', $translations, 3 * HOUR_IN_SECONDS );
	}

	return $translations;
}

/**
 * Output the select form for the language selection on the installation screen.
 *
 * @since 4.0.0
 *
 * @global string $wp_local_package Locale code of the package.
 *
 * @param array[] $languages Array of available languages (populated via the Translation API).
 */
function wp_install_language_form( $languages ) {
	global $wp_local_package;

	$installed_languages = get_available_languages();

	echo "<label class='screen-reader-text' for='language'>Select a default language</label>\n";
	echo "<select size='14' name='language' id='language'>\n";
	echo '<option value="" lang="en" selected="selected" data-continue="Continue" data-installed="1">English (United States)</option>';
	echo "\n";

	if ( ! empty( $wp_local_package ) && isset( $languages[ $wp_local_package ] ) ) {
		if ( isset( $languages[ $wp_local_package ] ) ) {
			$language = $languages[ $wp_local_package ];
			printf(
				'<option value="%s" lang="%s" data-continue="%s"%s>%s</option>' . "\n",
				esc_attr( $language['language'] ),
				esc_attr( current( $language['iso'] ) ),
				esc_attr( $language['strings']['continue'] ? $language['strings']['continue'] : 'Continue' ),
				in_array( $language['language'], $installed_languages, true ) ? ' data-installed="1"' : '',
				esc_html( $language['native_name'] )
			);

			unset( $languages[ $wp_local_package ] );
		}
	}

	foreach ( $languages as $language ) {
		printf(
			'<option value="%s" lang="%s" data-continue="%s"%s>%s</option>' . "\n",
			esc_attr( $language['language'] ),
			esc_attr( current( $language['iso'] ) ),
			esc_attr( $language['strings']['continue'] ? $language['strings']['continue'] : 'Continue' ),
			in_array( $language['language'], $installed_languages, true ) ? ' data-installed="1"' : '',
			esc_html( $language['native_name'] )
		);
	}
	echo "</select>\n";
	echo '<p class="step"><span class="spinner"></span><input id="language-continue" type="submit" class="button button-primary button-large" value="Continue" /></p>';
}

/**
 * Download a language pack.
 *
 * @since 4.0.0
 *
 * @see wp_get_available_translations()
 *
 * @param string $download Language code to download.
 * @return string|false Returns the language code if successfully downloaded
 *                      (or already installed), or false on failure.
 */
function wp_download_language_pack( $download ) {
	// Check if the translation is already installed.
	if ( in_array( $download, get_available_languages(), true ) ) {
		return $download;
	}

	if ( ! wp_is_file_mod_allowed( 'download_language_pack' ) ) {
		return false;
	}

	// Confirm the translation is one we can download.
	$translations = wp_get_available_translations();
	if ( ! $translations ) {
		return false;
	}
	foreach ( $translations as $translation ) {
		if ( $translation['language'] === $download ) {
			$translation_to_load = true;
			break;
		}
	}

	if ( empty( $translation_to_load ) ) {
		return false;
	}
	$translation = (object) $translation;

	require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
	$skin              = new Automatic_Upgrader_Skin();
	$upgrader          = new Language_Pack_Upgrader( $skin );
	$translation->type = 'core';
	$result            = $upgrader->upgrade( $translation, array( 'clear_update_cache' => false ) );

	if ( ! $result || is_wp_error( $result ) ) {
		return false;
	}

	return $translation->language;
}

/**
 * Check if WordPress has access to the filesystem without asking for
 * credentials.
 *
 * @since 4.0.0
 *
 * @return bool Returns true on success, false on failure.
 */
function wp_can_install_language_pack() {
	if ( ! wp_is_file_mod_allowed( 'can_install_language_pack' ) ) {
		return false;
	}

	require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
	$skin     = new Automatic_Upgrader_Skin();
	$upgrader = new Language_Pack_Upgrader( $skin );
	$upgrader->init();

	$check = $upgrader->fs_connect( array( WP_CONTENT_DIR, WP_LANG_DIR ) );

	if ( ! $check || is_wp_error( $check ) ) {
		return false;
	}

	return true;
}
image-edit.php000064400000125553151327343360007304 0ustar00<?php
/**
 * WordPress Image Editor
 *
 * @package WordPress
 * @subpackage Administration
 */

/**
 * Loads the WP image-editing interface.
 *
 * @since 2.9.0
 *
 * @param int          $post_id Attachment post ID.
 * @param false|object $msg     Optional. Message to display for image editor updates or errors.
 *                              Default false.
 */
function wp_image_editor( $post_id, $msg = false ) {
	$nonce     = wp_create_nonce( "image_editor-$post_id" );
	$meta      = wp_get_attachment_metadata( $post_id );
	$thumb     = image_get_intermediate_size( $post_id, 'thumbnail' );
	$sub_sizes = isset( $meta['sizes'] ) && is_array( $meta['sizes'] );
	$note      = '';

	if ( isset( $meta['width'], $meta['height'] ) ) {
		$big = max( $meta['width'], $meta['height'] );
	} else {
		die( __( 'Image data does not exist. Please re-upload the image.' ) );
	}

	$sizer = $big > 600 ? 600 / $big : 1;

	$backup_sizes = get_post_meta( $post_id, '_wp_attachment_backup_sizes', true );
	$can_restore  = false;

	if ( ! empty( $backup_sizes ) && isset( $backup_sizes['full-orig'], $meta['file'] ) ) {
		$can_restore = wp_basename( $meta['file'] ) !== $backup_sizes['full-orig']['file'];
	}

	if ( $msg ) {
		if ( isset( $msg->error ) ) {
			$note = "<div class='notice notice-error' role='alert'><p>$msg->error</p></div>";
		} elseif ( isset( $msg->msg ) ) {
			$note = "<div class='notice notice-success' role='alert'><p>$msg->msg</p></div>";
		}
	}

	/**
	 * Shows the settings in the Image Editor that allow selecting to edit only the thumbnail of an image.
	 *
	 * @since 6.3.0
	 *
	 * @param bool $show Whether to show the settings in the Image Editor. Default false.
	 */
	$edit_thumbnails_separately = (bool) apply_filters( 'image_edit_thumbnails_separately', false );

	?>
	<div class="imgedit-wrap wp-clearfix">
	<div id="imgedit-panel-<?php echo $post_id; ?>">
	<?php echo $note; ?>
	<div class="imgedit-panel-content imgedit-panel-tools wp-clearfix">
		<div class="imgedit-menu wp-clearfix">
			<button type="button" onclick="imageEdit.toggleCropTool( <?php echo "$post_id, '$nonce'"; ?>, this );" aria-expanded="false" aria-controls="imgedit-crop" class="imgedit-crop button disabled" disabled><?php esc_html_e( 'Crop' ); ?></button>
			<button type="button" class="imgedit-scale button" onclick="imageEdit.toggleControls(this);" aria-expanded="false" aria-controls="imgedit-scale"><?php esc_html_e( 'Scale' ); ?></button>
			<div class="imgedit-rotate-menu-container">
				<button type="button" aria-controls="imgedit-rotate-menu" class="imgedit-rotate button" aria-expanded="false" onclick="imageEdit.togglePopup(this)" onblur="imageEdit.monitorPopup()"><?php esc_html_e( 'Image Rotation' ); ?></button>
				<div id="imgedit-rotate-menu" class="imgedit-popup-menu">
			<?php
			// On some setups GD library does not provide imagerotate() - Ticket #11536.
			if ( wp_image_editor_supports(
				array(
					'mime_type' => get_post_mime_type( $post_id ),
					'methods'   => array( 'rotate' ),
				)
			) ) {
				$note_no_rotate = '';
				?>
					<button type="button" class="imgedit-rleft button" onkeydown="imageEdit.browsePopup(event, this)" onclick="imageEdit.rotate( 90, <?php echo "$post_id, '$nonce'"; ?>, this)" onblur="imageEdit.monitorPopup()"><?php esc_html_e( 'Rotate 90&deg; left' ); ?></button>
					<button type="button" class="imgedit-rright button" onkeydown="imageEdit.browsePopup(event, this)" onclick="imageEdit.rotate(-90, <?php echo "$post_id, '$nonce'"; ?>, this)" onblur="imageEdit.monitorPopup()"><?php esc_html_e( 'Rotate 90&deg; right' ); ?></button>
					<button type="button" class="imgedit-rfull button" onkeydown="imageEdit.browsePopup(event, this)" onclick="imageEdit.rotate(180, <?php echo "$post_id, '$nonce'"; ?>, this)" onblur="imageEdit.monitorPopup()"><?php esc_html_e( 'Rotate 180&deg;' ); ?></button>
				<?php
			} else {
				$note_no_rotate = '<p class="note-no-rotate"><em>' . __( 'Image rotation is not supported by your web host.' ) . '</em></p>';
				?>
					<button type="button" class="imgedit-rleft button disabled" disabled></button>
					<button type="button" class="imgedit-rright button disabled" disabled></button>
				<?php
			}
			?>
					<hr />
					<button type="button" onkeydown="imageEdit.browsePopup(event, this)" onclick="imageEdit.flip(1, <?php echo "$post_id, '$nonce'"; ?>, this)" onblur="imageEdit.monitorPopup()" class="imgedit-flipv button"><?php esc_html_e( 'Flip vertical' ); ?></button>
					<button type="button" onkeydown="imageEdit.browsePopup(event, this)" onclick="imageEdit.flip(2, <?php echo "$post_id, '$nonce'"; ?>, this)" onblur="imageEdit.monitorPopup()" class="imgedit-fliph button"><?php esc_html_e( 'Flip horizontal' ); ?></button>
					<?php echo $note_no_rotate; ?>
				</div>
			</div>
		</div>
		<div class="imgedit-submit imgedit-menu">
			<button type="button" id="image-undo-<?php echo $post_id; ?>" onclick="imageEdit.undo(<?php echo "$post_id, '$nonce'"; ?>, this)" class="imgedit-undo button disabled" disabled><?php esc_html_e( 'Undo' ); ?></button>
			<button type="button" id="image-redo-<?php echo $post_id; ?>" onclick="imageEdit.redo(<?php echo "$post_id, '$nonce'"; ?>, this)" class="imgedit-redo button disabled" disabled><?php esc_html_e( 'Redo' ); ?></button>
			<button type="button" onclick="imageEdit.close(<?php echo $post_id; ?>, 1)" class="button imgedit-cancel-btn"><?php esc_html_e( 'Cancel Editing' ); ?></button>
			<button type="button" onclick="imageEdit.save(<?php echo "$post_id, '$nonce'"; ?>)" disabled="disabled" class="button button-primary imgedit-submit-btn"><?php esc_html_e( 'Save Edits' ); ?></button>
		</div>
	</div>

	<div class="imgedit-panel-content wp-clearfix">
		<div class="imgedit-tools">
			<input type="hidden" id="imgedit-nonce-<?php echo $post_id; ?>" value="<?php echo $nonce; ?>" />
			<input type="hidden" id="imgedit-sizer-<?php echo $post_id; ?>" value="<?php echo $sizer; ?>" />
			<input type="hidden" id="imgedit-history-<?php echo $post_id; ?>" value="" />
			<input type="hidden" id="imgedit-undone-<?php echo $post_id; ?>" value="0" />
			<input type="hidden" id="imgedit-selection-<?php echo $post_id; ?>" value="" />
			<input type="hidden" id="imgedit-x-<?php echo $post_id; ?>" value="<?php echo isset( $meta['width'] ) ? $meta['width'] : 0; ?>" />
			<input type="hidden" id="imgedit-y-<?php echo $post_id; ?>" value="<?php echo isset( $meta['height'] ) ? $meta['height'] : 0; ?>" />

			<div id="imgedit-crop-<?php echo $post_id; ?>" class="imgedit-crop-wrap">
			<div class="imgedit-crop-grid"></div>
			<img id="image-preview-<?php echo $post_id; ?>" onload="imageEdit.imgLoaded('<?php echo $post_id; ?>')"
				src="<?php echo esc_url( admin_url( 'admin-ajax.php', 'relative' ) ) . '?action=imgedit-preview&amp;_ajax_nonce=' . $nonce . '&amp;postid=' . $post_id . '&amp;rand=' . rand( 1, 99999 ); ?>" alt="" />
			</div>
		</div>
		<div class="imgedit-settings">
			<div class="imgedit-tool-active">
				<div class="imgedit-group">
				<div id="imgedit-scale" tabindex="-1" class="imgedit-group-controls">
					<div class="imgedit-group-top">
						<h2><?php _e( 'Scale Image' ); ?></h2>
						<button type="button" class="dashicons dashicons-editor-help imgedit-help-toggle" onclick="imageEdit.toggleHelp(this);" aria-expanded="false"><span class="screen-reader-text">
						<?php
						/* translators: Hidden accessibility text. */
						esc_html_e( 'Scale Image Help' );
						?>
						</span></button>
						<div class="imgedit-help">
						<p><?php _e( 'You can proportionally scale the original image. For best results, scaling should be done before you crop, flip, or rotate. Images can only be scaled down, not up.' ); ?></p>
						</div>
						<?php if ( isset( $meta['width'], $meta['height'] ) ) : ?>
						<p>
							<?php
							printf(
								/* translators: %s: Image width and height in pixels. */
								__( 'Original dimensions %s' ),
								'<span class="imgedit-original-dimensions">' . $meta['width'] . ' &times; ' . $meta['height'] . '</span>'
							);
							?>
						</p>
						<?php endif; ?>
						<div class="imgedit-submit">
						<fieldset class="imgedit-scale-controls">
							<legend><?php _e( 'New dimensions:' ); ?></legend>
							<div class="nowrap">
							<label for="imgedit-scale-width-<?php echo $post_id; ?>" class="screen-reader-text">
							<?php
							/* translators: Hidden accessibility text. */
							_e( 'scale height' );
							?>
							</label>
							<input type="number" step="1" min="0" max="<?php echo isset( $meta['width'] ) ? $meta['width'] : ''; ?>" aria-describedby="imgedit-scale-warn-<?php echo $post_id; ?>"  id="imgedit-scale-width-<?php echo $post_id; ?>" onkeyup="imageEdit.scaleChanged(<?php echo $post_id; ?>, 1, this)" onblur="imageEdit.scaleChanged(<?php echo $post_id; ?>, 1, this)" value="<?php echo isset( $meta['width'] ) ? $meta['width'] : 0; ?>" />
							<span class="imgedit-separator" aria-hidden="true">&times;</span>
							<label for="imgedit-scale-height-<?php echo $post_id; ?>" class="screen-reader-text"><?php _e( 'scale height' ); ?></label>
							<input type="number" step="1" min="0" max="<?php echo isset( $meta['height'] ) ? $meta['height'] : ''; ?>" aria-describedby="imgedit-scale-warn-<?php echo $post_id; ?>" id="imgedit-scale-height-<?php echo $post_id; ?>" onkeyup="imageEdit.scaleChanged(<?php echo $post_id; ?>, 0, this)" onblur="imageEdit.scaleChanged(<?php echo $post_id; ?>, 0, this)" value="<?php echo isset( $meta['height'] ) ? $meta['height'] : 0; ?>" />
							<button id="imgedit-scale-button" type="button" onclick="imageEdit.action(<?php echo "$post_id, '$nonce'"; ?>, 'scale')" class="button button-primary"><?php esc_html_e( 'Scale' ); ?></button>
							</div>
							<span class="imgedit-scale-warn" id="imgedit-scale-warn-<?php echo $post_id; ?>"><span class="dashicons dashicons-warning" aria-hidden="true"></span><?php esc_html_e( 'Images cannot be scaled to a size larger than the original.' ); ?></span>
						</fieldset>
						</div>
					</div>
				</div>
			</div>

		<?php if ( $can_restore ) { ?>
				<div class="imgedit-group">
				<div class="imgedit-group-top">
					<h2><button type="button" onclick="imageEdit.toggleHelp(this);" class="button-link" aria-expanded="false"><?php _e( 'Restore original image' ); ?> <span class="dashicons dashicons-arrow-down imgedit-help-toggle"></span></button></h2>
					<div class="imgedit-help imgedit-restore">
					<p>
					<?php
					_e( 'Discard any changes and restore the original image.' );
					if ( ! defined( 'IMAGE_EDIT_OVERWRITE' ) || ! IMAGE_EDIT_OVERWRITE ) {
						echo ' ' . __( 'Previously edited copies of the image will not be deleted.' );
					}
					?>
					</p>
					<div class="imgedit-submit">
						<input type="button" onclick="imageEdit.action(<?php echo "$post_id, '$nonce'"; ?>, 'restore')" class="button button-primary" value="<?php esc_attr_e( 'Restore image' ); ?>" <?php echo $can_restore; ?> />
					</div>
				</div>
			</div>
			</div>
		<?php } ?>
			<div class="imgedit-group">
				<div id="imgedit-crop" tabindex="-1" class="imgedit-group-controls">
				<div class="imgedit-group-top">
					<h2><?php _e( 'Crop Image' ); ?></h2>
					<button type="button" class="dashicons dashicons-editor-help imgedit-help-toggle" onclick="imageEdit.toggleHelp(this);" aria-expanded="false"><span class="screen-reader-text">
					<?php
					/* translators: Hidden accessibility text. */
					_e( 'Image Crop Help' );
					?>
					</span></button>
					<div class="imgedit-help">
						<p><?php _e( 'To crop the image, click on it and drag to make your selection.' ); ?></p>
						<p><strong><?php _e( 'Crop Aspect Ratio' ); ?></strong><br />
						<?php _e( 'The aspect ratio is the relationship between the width and height. You can preserve the aspect ratio by holding down the shift key while resizing your selection. Use the input box to specify the aspect ratio, e.g. 1:1 (square), 4:3, 16:9, etc.' ); ?></p>

						<p><strong><?php _e( 'Crop Selection' ); ?></strong><br />
						<?php _e( 'Once you have made your selection, you can adjust it by entering the size in pixels. The minimum selection size is the thumbnail size as set in the Media settings.' ); ?></p>
					</div>
				</div>
				<fieldset class="imgedit-crop-ratio">
					<legend><?php _e( 'Aspect ratio:' ); ?></legend>
					<div class="nowrap">
					<label for="imgedit-crop-width-<?php echo $post_id; ?>" class="screen-reader-text">
					<?php
					/* translators: Hidden accessibility text. */
					_e( 'crop ratio width' );
					?>
					</label>
					<input type="number" step="1" min="1" id="imgedit-crop-width-<?php echo $post_id; ?>" onkeyup="imageEdit.setRatioSelection(<?php echo $post_id; ?>, 0, this)" onblur="imageEdit.setRatioSelection(<?php echo $post_id; ?>, 0, this)" />
					<span class="imgedit-separator" aria-hidden="true">:</span>
					<label for="imgedit-crop-height-<?php echo $post_id; ?>" class="screen-reader-text">
					<?php
					/* translators: Hidden accessibility text. */
					_e( 'crop ratio height' );
					?>
					</label>
					<input  type="number" step="1" min="0" id="imgedit-crop-height-<?php echo $post_id; ?>" onkeyup="imageEdit.setRatioSelection(<?php echo $post_id; ?>, 1, this)" onblur="imageEdit.setRatioSelection(<?php echo $post_id; ?>, 1, this)" />
					</div>
				</fieldset>
				<fieldset id="imgedit-crop-sel-<?php echo $post_id; ?>" class="imgedit-crop-sel">
					<legend><?php _e( 'Selection:' ); ?></legend>
					<div class="nowrap">
					<label for="imgedit-sel-width-<?php echo $post_id; ?>" class="screen-reader-text">
					<?php
					/* translators: Hidden accessibility text. */
					_e( 'selection width' );
					?>
					</label>
					<input  type="number" step="1" min="0" id="imgedit-sel-width-<?php echo $post_id; ?>" onkeyup="imageEdit.setNumSelection(<?php echo $post_id; ?>, this)" onblur="imageEdit.setNumSelection(<?php echo $post_id; ?>, this)" />
					<span class="imgedit-separator" aria-hidden="true">&times;</span>
					<label for="imgedit-sel-height-<?php echo $post_id; ?>" class="screen-reader-text">
					<?php
					/* translators: Hidden accessibility text. */
					_e( 'selection height' );
					?>
					</label>
					<input  type="number" step="1" min="0" id="imgedit-sel-height-<?php echo $post_id; ?>" onkeyup="imageEdit.setNumSelection(<?php echo $post_id; ?>, this)" onblur="imageEdit.setNumSelection(<?php echo $post_id; ?>, this)" />
					</div>
				</fieldset>
				<fieldset id="imgedit-crop-sel-<?php echo $post_id; ?>" class="imgedit-crop-sel">
					<legend><?php _e( 'Starting Coordinates:' ); ?></legend>
					<div class="nowrap">
					<label for="imgedit-start-x-<?php echo $post_id; ?>" class="screen-reader-text">
					<?php
					/* translators: Hidden accessibility text. */
					_e( 'horizontal start position' );
					?>
					</label>
					<input  type="number" step="1" min="0" id="imgedit-start-x-<?php echo $post_id; ?>" onkeyup="imageEdit.setNumSelection(<?php echo $post_id; ?>, this)" onblur="imageEdit.setNumSelection(<?php echo $post_id; ?>, this)" value="0" />
					<span class="imgedit-separator" aria-hidden="true">&times;</span>
					<label for="imgedit-start-y-<?php echo $post_id; ?>" class="screen-reader-text">
					<?php
					/* translators: Hidden accessibility text. */
					_e( 'vertical start position' );
					?>
					</label>
					<input  type="number" step="1" min="0" id="imgedit-start-y-<?php echo $post_id; ?>" onkeyup="imageEdit.setNumSelection(<?php echo $post_id; ?>, this)" onblur="imageEdit.setNumSelection(<?php echo $post_id; ?>, this)" value="0" />
					</div>
				</fieldset>
				<div class="imgedit-crop-apply imgedit-menu container">
					<button class="button-primary" type="button" onclick="imageEdit.handleCropToolClick( <?php echo "$post_id, '$nonce'"; ?>, this );" class="imgedit-crop-apply button"><?php esc_html_e( 'Apply Crop' ); ?></button> <button type="button" onclick="imageEdit.handleCropToolClick( <?php echo "$post_id, '$nonce'"; ?>, this );" class="imgedit-crop-clear button" disabled="disabled"><?php esc_html_e( 'Clear Crop' ); ?></button>
				</div>
			</div>
		</div>
	</div>

	<?php
	if ( $edit_thumbnails_separately && $thumb && $sub_sizes ) {
		$thumb_img = wp_constrain_dimensions( $thumb['width'], $thumb['height'], 160, 120 );
		?>

	<div class="imgedit-group imgedit-applyto">
		<div class="imgedit-group-top">
			<h2><?php _e( 'Thumbnail Settings' ); ?></h2>
			<button type="button" class="dashicons dashicons-editor-help imgedit-help-toggle" onclick="imageEdit.toggleHelp(this);" aria-expanded="false"><span class="screen-reader-text">
			<?php
			/* translators: Hidden accessibility text. */
			esc_html_e( 'Thumbnail Settings Help' );
			?>
			</span></button>
			<div class="imgedit-help">
			<p><?php _e( 'You can edit the image while preserving the thumbnail. For example, you may wish to have a square thumbnail that displays just a section of the image.' ); ?></p>
			</div>
		</div>
		<div class="imgedit-thumbnail-preview-group">
			<figure class="imgedit-thumbnail-preview">
				<img src="<?php echo esc_url( $thumb['url'] ); ?>" width="<?php echo esc_attr( $thumb_img[0] ); ?>" height="<?php echo esc_attr( $thumb_img[1] ); ?>" class="imgedit-size-preview" alt="" draggable="false" />
				<figcaption class="imgedit-thumbnail-preview-caption"><?php _e( 'Current thumbnail' ); ?></figcaption>
			</figure>
			<div id="imgedit-save-target-<?php echo $post_id; ?>" class="imgedit-save-target">
			<fieldset>
				<legend><?php _e( 'Apply changes to:' ); ?></legend>

				<span class="imgedit-label">
					<input type="radio" id="imgedit-target-all" name="imgedit-target-<?php echo $post_id; ?>" value="all" checked="checked" />
					<label for="imgedit-target-all"><?php _e( 'All image sizes' ); ?></label>
				</span>

				<span class="imgedit-label">
					<input type="radio" id="imgedit-target-thumbnail" name="imgedit-target-<?php echo $post_id; ?>" value="thumbnail" />
					<label for="imgedit-target-thumbnail"><?php _e( 'Thumbnail' ); ?></label>
				</span>

				<span class="imgedit-label">
					<input type="radio" id="imgedit-target-nothumb" name="imgedit-target-<?php echo $post_id; ?>" value="nothumb" />
					<label for="imgedit-target-nothumb"><?php _e( 'All sizes except thumbnail' ); ?></label>
				</span>

				</fieldset>
			</div>
		</div>
	</div>
	<?php } ?>
		</div>
	</div>

	</div>

	<div class="imgedit-wait" id="imgedit-wait-<?php echo $post_id; ?>"></div>
	<div class="hidden" id="imgedit-leaving-<?php echo $post_id; ?>"><?php _e( "There are unsaved changes that will be lost. 'OK' to continue, 'Cancel' to return to the Image Editor." ); ?></div>
	</div>
	<?php
}

/**
 * Streams image in WP_Image_Editor to browser.
 *
 * @since 2.9.0
 *
 * @param WP_Image_Editor $image         The image editor instance.
 * @param string          $mime_type     The mime type of the image.
 * @param int             $attachment_id The image's attachment post ID.
 * @return bool True on success, false on failure.
 */
function wp_stream_image( $image, $mime_type, $attachment_id ) {
	if ( $image instanceof WP_Image_Editor ) {

		/**
		 * Filters the WP_Image_Editor instance for the image to be streamed to the browser.
		 *
		 * @since 3.5.0
		 *
		 * @param WP_Image_Editor $image         The image editor instance.
		 * @param int             $attachment_id The attachment post ID.
		 */
		$image = apply_filters( 'image_editor_save_pre', $image, $attachment_id );

		if ( is_wp_error( $image->stream( $mime_type ) ) ) {
			return false;
		}

		return true;
	} else {
		/* translators: 1: $image, 2: WP_Image_Editor */
		_deprecated_argument( __FUNCTION__, '3.5.0', sprintf( __( '%1$s needs to be a %2$s object.' ), '$image', 'WP_Image_Editor' ) );

		/**
		 * Filters the GD image resource to be streamed to the browser.
		 *
		 * @since 2.9.0
		 * @deprecated 3.5.0 Use {@see 'image_editor_save_pre'} instead.
		 *
		 * @param resource|GdImage $image         Image resource to be streamed.
		 * @param int              $attachment_id The attachment post ID.
		 */
		$image = apply_filters_deprecated( 'image_save_pre', array( $image, $attachment_id ), '3.5.0', 'image_editor_save_pre' );

		switch ( $mime_type ) {
			case 'image/jpeg':
				header( 'Content-Type: image/jpeg' );
				return imagejpeg( $image, null, 90 );
			case 'image/png':
				header( 'Content-Type: image/png' );
				return imagepng( $image );
			case 'image/gif':
				header( 'Content-Type: image/gif' );
				return imagegif( $image );
			case 'image/webp':
				if ( function_exists( 'imagewebp' ) ) {
					header( 'Content-Type: image/webp' );
					return imagewebp( $image, null, 90 );
				}
				return false;
			case 'image/avif':
				if ( function_exists( 'imageavif' ) ) {
					header( 'Content-Type: image/avif' );
					return imageavif( $image, null, 90 );
				}
				return false;
			default:
				return false;
		}
	}
}

/**
 * Saves image to file.
 *
 * @since 2.9.0
 * @since 3.5.0 The `$image` parameter expects a `WP_Image_Editor` instance.
 * @since 6.0.0 The `$filesize` value was added to the returned array.
 *
 * @param string          $filename  Name of the file to be saved.
 * @param WP_Image_Editor $image     The image editor instance.
 * @param string          $mime_type The mime type of the image.
 * @param int             $post_id   Attachment post ID.
 * @return array|WP_Error|bool {
 *     Array on success or WP_Error if the file failed to save.
 *     When called with a deprecated value for the `$image` parameter,
 *     i.e. a non-`WP_Image_Editor` image resource or `GdImage` instance,
 *     the function will return true on success, false on failure.
 *
 *     @type string $path      Path to the image file.
 *     @type string $file      Name of the image file.
 *     @type int    $width     Image width.
 *     @type int    $height    Image height.
 *     @type string $mime-type The mime type of the image.
 *     @type int    $filesize  File size of the image.
 * }
 */
function wp_save_image_file( $filename, $image, $mime_type, $post_id ) {
	if ( $image instanceof WP_Image_Editor ) {

		/** This filter is documented in wp-admin/includes/image-edit.php */
		$image = apply_filters( 'image_editor_save_pre', $image, $post_id );

		/**
		 * Filters whether to skip saving the image file.
		 *
		 * Returning a non-null value will short-circuit the save method,
		 * returning that value instead.
		 *
		 * @since 3.5.0
		 *
		 * @param bool|null       $override  Value to return instead of saving. Default null.
		 * @param string          $filename  Name of the file to be saved.
		 * @param WP_Image_Editor $image     The image editor instance.
		 * @param string          $mime_type The mime type of the image.
		 * @param int             $post_id   Attachment post ID.
		 */
		$saved = apply_filters( 'wp_save_image_editor_file', null, $filename, $image, $mime_type, $post_id );

		if ( null !== $saved ) {
			return $saved;
		}

		return $image->save( $filename, $mime_type );
	} else {
		/* translators: 1: $image, 2: WP_Image_Editor */
		_deprecated_argument( __FUNCTION__, '3.5.0', sprintf( __( '%1$s needs to be a %2$s object.' ), '$image', 'WP_Image_Editor' ) );

		/** This filter is documented in wp-admin/includes/image-edit.php */
		$image = apply_filters_deprecated( 'image_save_pre', array( $image, $post_id ), '3.5.0', 'image_editor_save_pre' );

		/**
		 * Filters whether to skip saving the image file.
		 *
		 * Returning a non-null value will short-circuit the save method,
		 * returning that value instead.
		 *
		 * @since 2.9.0
		 * @deprecated 3.5.0 Use {@see 'wp_save_image_editor_file'} instead.
		 *
		 * @param bool|null        $override  Value to return instead of saving. Default null.
		 * @param string           $filename  Name of the file to be saved.
		 * @param resource|GdImage $image     Image resource or GdImage instance.
		 * @param string           $mime_type The mime type of the image.
		 * @param int              $post_id   Attachment post ID.
		 */
		$saved = apply_filters_deprecated(
			'wp_save_image_file',
			array( null, $filename, $image, $mime_type, $post_id ),
			'3.5.0',
			'wp_save_image_editor_file'
		);

		if ( null !== $saved ) {
			return $saved;
		}

		switch ( $mime_type ) {
			case 'image/jpeg':
				/** This filter is documented in wp-includes/class-wp-image-editor.php */
				return imagejpeg( $image, $filename, apply_filters( 'jpeg_quality', 90, 'edit_image' ) );
			case 'image/png':
				return imagepng( $image, $filename );
			case 'image/gif':
				return imagegif( $image, $filename );
			case 'image/webp':
				if ( function_exists( 'imagewebp' ) ) {
					return imagewebp( $image, $filename );
				}
				return false;
			case 'image/avif':
				if ( function_exists( 'imageavif' ) ) {
					return imageavif( $image, $filename );
				}
				return false;
			default:
				return false;
		}
	}
}

/**
 * Image preview ratio. Internal use only.
 *
 * @since 2.9.0
 *
 * @ignore
 * @param int $w Image width in pixels.
 * @param int $h Image height in pixels.
 * @return float|int Image preview ratio.
 */
function _image_get_preview_ratio( $w, $h ) {
	$max = max( $w, $h );
	return $max > 600 ? ( 600 / $max ) : 1;
}

/**
 * Returns an image resource. Internal use only.
 *
 * @since 2.9.0
 * @deprecated 3.5.0 Use WP_Image_Editor::rotate()
 * @see WP_Image_Editor::rotate()
 *
 * @ignore
 * @param resource|GdImage $img   Image resource.
 * @param float|int        $angle Image rotation angle, in degrees.
 * @return resource|GdImage|false GD image resource or GdImage instance, false otherwise.
 */
function _rotate_image_resource( $img, $angle ) {
	_deprecated_function( __FUNCTION__, '3.5.0', 'WP_Image_Editor::rotate()' );

	if ( function_exists( 'imagerotate' ) ) {
		$rotated = imagerotate( $img, $angle, 0 );

		if ( is_gd_image( $rotated ) ) {
			imagedestroy( $img );
			$img = $rotated;
		}
	}

	return $img;
}

/**
 * Flips an image resource. Internal use only.
 *
 * @since 2.9.0
 * @deprecated 3.5.0 Use WP_Image_Editor::flip()
 * @see WP_Image_Editor::flip()
 *
 * @ignore
 * @param resource|GdImage $img  Image resource or GdImage instance.
 * @param bool             $horz Whether to flip horizontally.
 * @param bool             $vert Whether to flip vertically.
 * @return resource|GdImage (maybe) flipped image resource or GdImage instance.
 */
function _flip_image_resource( $img, $horz, $vert ) {
	_deprecated_function( __FUNCTION__, '3.5.0', 'WP_Image_Editor::flip()' );

	$w   = imagesx( $img );
	$h   = imagesy( $img );
	$dst = wp_imagecreatetruecolor( $w, $h );

	if ( is_gd_image( $dst ) ) {
		$sx = $vert ? ( $w - 1 ) : 0;
		$sy = $horz ? ( $h - 1 ) : 0;
		$sw = $vert ? -$w : $w;
		$sh = $horz ? -$h : $h;

		if ( imagecopyresampled( $dst, $img, 0, 0, $sx, $sy, $w, $h, $sw, $sh ) ) {
			imagedestroy( $img );
			$img = $dst;
		}
	}

	return $img;
}

/**
 * Crops an image resource. Internal use only.
 *
 * @since 2.9.0
 *
 * @ignore
 * @param resource|GdImage $img Image resource or GdImage instance.
 * @param float            $x   Source point x-coordinate.
 * @param float            $y   Source point y-coordinate.
 * @param float            $w   Source width.
 * @param float            $h   Source height.
 * @return resource|GdImage (maybe) cropped image resource or GdImage instance.
 */
function _crop_image_resource( $img, $x, $y, $w, $h ) {
	$dst = wp_imagecreatetruecolor( $w, $h );

	if ( is_gd_image( $dst ) ) {
		if ( imagecopy( $dst, $img, 0, 0, $x, $y, $w, $h ) ) {
			imagedestroy( $img );
			$img = $dst;
		}
	}

	return $img;
}

/**
 * Performs group of changes on Editor specified.
 *
 * @since 2.9.0
 *
 * @param WP_Image_Editor $image   WP_Image_Editor instance.
 * @param array           $changes Array of change operations.
 * @return WP_Image_Editor WP_Image_Editor instance with changes applied.
 */
function image_edit_apply_changes( $image, $changes ) {
	if ( is_gd_image( $image ) ) {
		/* translators: 1: $image, 2: WP_Image_Editor */
		_deprecated_argument( __FUNCTION__, '3.5.0', sprintf( __( '%1$s needs to be a %2$s object.' ), '$image', 'WP_Image_Editor' ) );
	}

	if ( ! is_array( $changes ) ) {
		return $image;
	}

	// Expand change operations.
	foreach ( $changes as $key => $obj ) {
		if ( isset( $obj->r ) ) {
			$obj->type  = 'rotate';
			$obj->angle = $obj->r;
			unset( $obj->r );
		} elseif ( isset( $obj->f ) ) {
			$obj->type = 'flip';
			$obj->axis = $obj->f;
			unset( $obj->f );
		} elseif ( isset( $obj->c ) ) {
			$obj->type = 'crop';
			$obj->sel  = $obj->c;
			unset( $obj->c );
		}

		$changes[ $key ] = $obj;
	}

	// Combine operations.
	if ( count( $changes ) > 1 ) {
		$filtered = array( $changes[0] );

		for ( $i = 0, $j = 1, $c = count( $changes ); $j < $c; $j++ ) {
			$combined = false;

			if ( $filtered[ $i ]->type === $changes[ $j ]->type ) {
				switch ( $filtered[ $i ]->type ) {
					case 'rotate':
						$filtered[ $i ]->angle += $changes[ $j ]->angle;
						$combined               = true;
						break;
					case 'flip':
						$filtered[ $i ]->axis ^= $changes[ $j ]->axis;
						$combined              = true;
						break;
				}
			}

			if ( ! $combined ) {
				$filtered[ ++$i ] = $changes[ $j ];
			}
		}

		$changes = $filtered;
		unset( $filtered );
	}

	// Image resource before applying the changes.
	if ( $image instanceof WP_Image_Editor ) {

		/**
		 * Filters the WP_Image_Editor instance before applying changes to the image.
		 *
		 * @since 3.5.0
		 *
		 * @param WP_Image_Editor $image   WP_Image_Editor instance.
		 * @param array           $changes Array of change operations.
		 */
		$image = apply_filters( 'wp_image_editor_before_change', $image, $changes );
	} elseif ( is_gd_image( $image ) ) {

		/**
		 * Filters the GD image resource before applying changes to the image.
		 *
		 * @since 2.9.0
		 * @deprecated 3.5.0 Use {@see 'wp_image_editor_before_change'} instead.
		 *
		 * @param resource|GdImage $image   GD image resource or GdImage instance.
		 * @param array            $changes Array of change operations.
		 */
		$image = apply_filters_deprecated( 'image_edit_before_change', array( $image, $changes ), '3.5.0', 'wp_image_editor_before_change' );
	}

	foreach ( $changes as $operation ) {
		switch ( $operation->type ) {
			case 'rotate':
				if ( 0 !== $operation->angle ) {
					if ( $image instanceof WP_Image_Editor ) {
						$image->rotate( $operation->angle );
					} else {
						$image = _rotate_image_resource( $image, $operation->angle );
					}
				}
				break;
			case 'flip':
				if ( 0 !== $operation->axis ) {
					if ( $image instanceof WP_Image_Editor ) {
						$image->flip( ( $operation->axis & 1 ) !== 0, ( $operation->axis & 2 ) !== 0 );
					} else {
						$image = _flip_image_resource( $image, ( $operation->axis & 1 ) !== 0, ( $operation->axis & 2 ) !== 0 );
					}
				}
				break;
			case 'crop':
				$sel = $operation->sel;

				if ( $image instanceof WP_Image_Editor ) {
					$size = $image->get_size();
					$w    = $size['width'];
					$h    = $size['height'];

					$scale = isset( $sel->r ) ? $sel->r : 1 / _image_get_preview_ratio( $w, $h ); // Discard preview scaling.
					$image->crop( (int) ( $sel->x * $scale ), (int) ( $sel->y * $scale ), (int) ( $sel->w * $scale ), (int) ( $sel->h * $scale ) );
				} else {
					$scale = isset( $sel->r ) ? $sel->r : 1 / _image_get_preview_ratio( imagesx( $image ), imagesy( $image ) ); // Discard preview scaling.
					$image = _crop_image_resource( $image, $sel->x * $scale, $sel->y * $scale, $sel->w * $scale, $sel->h * $scale );
				}
				break;
		}
	}

	return $image;
}


/**
 * Streams image in post to browser, along with enqueued changes
 * in `$_REQUEST['history']`.
 *
 * @since 2.9.0
 *
 * @param int $post_id Attachment post ID.
 * @return bool True on success, false on failure.
 */
function stream_preview_image( $post_id ) {
	$post = get_post( $post_id );

	wp_raise_memory_limit( 'admin' );

	$img = wp_get_image_editor( _load_image_to_edit_path( $post_id ) );

	if ( is_wp_error( $img ) ) {
		return false;
	}

	$changes = ! empty( $_REQUEST['history'] ) ? json_decode( wp_unslash( $_REQUEST['history'] ) ) : null;
	if ( $changes ) {
		$img = image_edit_apply_changes( $img, $changes );
	}

	// Scale the image.
	$size = $img->get_size();
	$w    = $size['width'];
	$h    = $size['height'];

	$ratio = _image_get_preview_ratio( $w, $h );
	$w2    = max( 1, $w * $ratio );
	$h2    = max( 1, $h * $ratio );

	if ( is_wp_error( $img->resize( $w2, $h2 ) ) ) {
		return false;
	}

	return wp_stream_image( $img, $post->post_mime_type, $post_id );
}

/**
 * Restores the metadata for a given attachment.
 *
 * @since 2.9.0
 *
 * @param int $post_id Attachment post ID.
 * @return stdClass Image restoration message object.
 */
function wp_restore_image( $post_id ) {
	$meta             = wp_get_attachment_metadata( $post_id );
	$file             = get_attached_file( $post_id );
	$backup_sizes     = get_post_meta( $post_id, '_wp_attachment_backup_sizes', true );
	$old_backup_sizes = $backup_sizes;
	$restored         = false;
	$msg              = new stdClass();

	if ( ! is_array( $backup_sizes ) ) {
		$msg->error = __( 'Cannot load image metadata.' );
		return $msg;
	}

	$parts         = pathinfo( $file );
	$suffix        = time() . rand( 100, 999 );
	$default_sizes = get_intermediate_image_sizes();

	if ( isset( $backup_sizes['full-orig'] ) && is_array( $backup_sizes['full-orig'] ) ) {
		$data = $backup_sizes['full-orig'];

		if ( $parts['basename'] !== $data['file'] ) {
			if ( defined( 'IMAGE_EDIT_OVERWRITE' ) && IMAGE_EDIT_OVERWRITE ) {
				// Delete only if it's an edited image.
				if ( preg_match( '/-e[0-9]{13}\./', $parts['basename'] ) ) {
					wp_delete_file( $file );
				}
			} elseif ( isset( $meta['width'], $meta['height'] ) ) {
				$backup_sizes[ "full-$suffix" ] = array(
					'width'    => $meta['width'],
					'height'   => $meta['height'],
					'filesize' => $meta['filesize'],
					'file'     => $parts['basename'],
				);
			}
		}

		$restored_file = path_join( $parts['dirname'], $data['file'] );
		$restored      = update_attached_file( $post_id, $restored_file );

		$meta['file']   = _wp_relative_upload_path( $restored_file );
		$meta['width']  = $data['width'];
		$meta['height'] = $data['height'];
		if ( isset( $data['filesize'] ) ) {
			/*
			 * Restore the original filesize if it was backed up.
			 *
			 * See https://core.trac.wordpress.org/ticket/59684.
			 */
			$meta['filesize'] = $data['filesize'];
		}
	}

	foreach ( $default_sizes as $default_size ) {
		if ( isset( $backup_sizes[ "$default_size-orig" ] ) ) {
			$data = $backup_sizes[ "$default_size-orig" ];

			if ( isset( $meta['sizes'][ $default_size ] ) && $meta['sizes'][ $default_size ]['file'] !== $data['file'] ) {
				if ( defined( 'IMAGE_EDIT_OVERWRITE' ) && IMAGE_EDIT_OVERWRITE ) {
					// Delete only if it's an edited image.
					if ( preg_match( '/-e[0-9]{13}-/', $meta['sizes'][ $default_size ]['file'] ) ) {
						$delete_file = path_join( $parts['dirname'], $meta['sizes'][ $default_size ]['file'] );
						wp_delete_file( $delete_file );
					}
				} else {
					$backup_sizes[ "$default_size-{$suffix}" ] = $meta['sizes'][ $default_size ];
				}
			}

			$meta['sizes'][ $default_size ] = $data;
		} else {
			unset( $meta['sizes'][ $default_size ] );
		}
	}

	if ( ! wp_update_attachment_metadata( $post_id, $meta )
		|| ( $old_backup_sizes !== $backup_sizes && ! update_post_meta( $post_id, '_wp_attachment_backup_sizes', $backup_sizes ) )
	) {
		$msg->error = __( 'Cannot save image metadata.' );
		return $msg;
	}

	if ( ! $restored ) {
		$msg->error = __( 'Image metadata is inconsistent.' );
	} else {
		$msg->msg = __( 'Image restored successfully.' );

		if ( defined( 'IMAGE_EDIT_OVERWRITE' ) && IMAGE_EDIT_OVERWRITE ) {
			delete_post_meta( $post_id, '_wp_attachment_backup_sizes' );
		}
	}

	return $msg;
}

/**
 * Saves image to post, along with enqueued changes
 * in `$_REQUEST['history']`.
 *
 * @since 2.9.0
 *
 * @param int $post_id Attachment post ID.
 * @return stdClass
 */
function wp_save_image( $post_id ) {
	$_wp_additional_image_sizes = wp_get_additional_image_sizes();

	$return  = new stdClass();
	$success = false;
	$delete  = false;
	$scaled  = false;
	$nocrop  = false;
	$post    = get_post( $post_id );

	$img = wp_get_image_editor( _load_image_to_edit_path( $post_id, 'full' ) );

	if ( is_wp_error( $img ) ) {
		$return->error = esc_js( __( 'Unable to create new image.' ) );
		return $return;
	}

	$full_width  = ! empty( $_REQUEST['fwidth'] ) ? (int) $_REQUEST['fwidth'] : 0;
	$full_height = ! empty( $_REQUEST['fheight'] ) ? (int) $_REQUEST['fheight'] : 0;
	$target      = ! empty( $_REQUEST['target'] ) ? preg_replace( '/[^a-z0-9_-]+/i', '', $_REQUEST['target'] ) : '';
	$scale       = ! empty( $_REQUEST['do'] ) && 'scale' === $_REQUEST['do'];

	/** This filter is documented in wp-admin/includes/image-edit.php */
	$edit_thumbnails_separately = (bool) apply_filters( 'image_edit_thumbnails_separately', false );

	if ( $scale ) {
		$size            = $img->get_size();
		$original_width  = $size['width'];
		$original_height = $size['height'];

		if ( $full_width > $original_width || $full_height > $original_height ) {
			$return->error = esc_js( __( 'Images cannot be scaled to a size larger than the original.' ) );
			return $return;
		}

		if ( $full_width > 0 && $full_height > 0 ) {
			// Check if it has roughly the same w / h ratio.
			$diff = round( $original_width / $original_height, 2 ) - round( $full_width / $full_height, 2 );
			if ( -0.1 < $diff && $diff < 0.1 ) {
				// Scale the full size image.
				if ( $img->resize( $full_width, $full_height ) ) {
					$scaled = true;
				}
			}

			if ( ! $scaled ) {
				$return->error = esc_js( __( 'Error while saving the scaled image. Please reload the page and try again.' ) );
				return $return;
			}
		}
	} elseif ( ! empty( $_REQUEST['history'] ) ) {
		$changes = json_decode( wp_unslash( $_REQUEST['history'] ) );
		if ( $changes ) {
			$img = image_edit_apply_changes( $img, $changes );
		}
	} else {
		$return->error = esc_js( __( 'Nothing to save, the image has not changed.' ) );
		return $return;
	}

	$meta         = wp_get_attachment_metadata( $post_id );
	$backup_sizes = get_post_meta( $post->ID, '_wp_attachment_backup_sizes', true );

	if ( ! is_array( $meta ) ) {
		$return->error = esc_js( __( 'Image data does not exist. Please re-upload the image.' ) );
		return $return;
	}

	if ( ! is_array( $backup_sizes ) ) {
		$backup_sizes = array();
	}

	// Generate new filename.
	$path = get_attached_file( $post_id );

	$basename = pathinfo( $path, PATHINFO_BASENAME );
	$dirname  = pathinfo( $path, PATHINFO_DIRNAME );
	$ext      = pathinfo( $path, PATHINFO_EXTENSION );
	$filename = pathinfo( $path, PATHINFO_FILENAME );
	$suffix   = time() . rand( 100, 999 );

	if ( defined( 'IMAGE_EDIT_OVERWRITE' ) && IMAGE_EDIT_OVERWRITE
		&& isset( $backup_sizes['full-orig'] ) && $backup_sizes['full-orig']['file'] !== $basename
	) {

		if ( $edit_thumbnails_separately && 'thumbnail' === $target ) {
			$new_path = "{$dirname}/{$filename}-temp.{$ext}";
		} else {
			$new_path = $path;
		}
	} else {
		while ( true ) {
			$filename     = preg_replace( '/-e([0-9]+)$/', '', $filename );
			$filename    .= "-e{$suffix}";
			$new_filename = "{$filename}.{$ext}";
			$new_path     = "{$dirname}/$new_filename";

			if ( file_exists( $new_path ) ) {
				++$suffix;
			} else {
				break;
			}
		}
	}

	$saved_image = wp_save_image_file( $new_path, $img, $post->post_mime_type, $post_id );
	// Save the full-size file, also needed to create sub-sizes.
	if ( ! $saved_image ) {
		$return->error = esc_js( __( 'Unable to save the image.' ) );
		return $return;
	}

	if ( 'nothumb' === $target || 'all' === $target || 'full' === $target || $scaled ) {
		$tag = false;

		if ( isset( $backup_sizes['full-orig'] ) ) {
			if ( ( ! defined( 'IMAGE_EDIT_OVERWRITE' ) || ! IMAGE_EDIT_OVERWRITE )
				&& $backup_sizes['full-orig']['file'] !== $basename
			) {
				$tag = "full-$suffix";
			}
		} else {
			$tag = 'full-orig';
		}

		if ( $tag ) {
			$backup_sizes[ $tag ] = array(
				'width'    => $meta['width'],
				'height'   => $meta['height'],
				'filesize' => $meta['filesize'],
				'file'     => $basename,
			);
		}

		$success = ( $path === $new_path ) || update_attached_file( $post_id, $new_path );

		$meta['file'] = _wp_relative_upload_path( $new_path );

		$size             = $img->get_size();
		$meta['width']    = $size['width'];
		$meta['height']   = $size['height'];
		$meta['filesize'] = $saved_image['filesize'];

		if ( $success && ( 'nothumb' === $target || 'all' === $target ) ) {
			$sizes = get_intermediate_image_sizes();

			if ( $edit_thumbnails_separately && 'nothumb' === $target ) {
				$sizes = array_diff( $sizes, array( 'thumbnail' ) );
			}
		}

		$return->fw = $meta['width'];
		$return->fh = $meta['height'];
	} elseif ( $edit_thumbnails_separately && 'thumbnail' === $target ) {
		$sizes   = array( 'thumbnail' );
		$success = true;
		$delete  = true;
		$nocrop  = true;
	}

	/*
	 * We need to remove any existing resized image files because
	 * a new crop or rotate could generate different sizes (and hence, filenames),
	 * keeping the new resized images from overwriting the existing image files.
	 * https://core.trac.wordpress.org/ticket/32171
	 */
	if ( defined( 'IMAGE_EDIT_OVERWRITE' ) && IMAGE_EDIT_OVERWRITE && ! empty( $meta['sizes'] ) ) {
		foreach ( $meta['sizes'] as $size ) {
			if ( ! empty( $size['file'] ) && preg_match( '/-e[0-9]{13}-/', $size['file'] ) ) {
				$delete_file = path_join( $dirname, $size['file'] );
				wp_delete_file( $delete_file );
			}
		}
	}

	if ( isset( $sizes ) ) {
		$_sizes = array();

		foreach ( $sizes as $size ) {
			$tag = false;

			if ( isset( $meta['sizes'][ $size ] ) ) {
				if ( isset( $backup_sizes[ "$size-orig" ] ) ) {
					if ( ( ! defined( 'IMAGE_EDIT_OVERWRITE' ) || ! IMAGE_EDIT_OVERWRITE )
						&& $backup_sizes[ "$size-orig" ]['file'] !== $meta['sizes'][ $size ]['file']
					) {
						$tag = "$size-$suffix";
					}
				} else {
					$tag = "$size-orig";
				}

				if ( $tag ) {
					$backup_sizes[ $tag ] = $meta['sizes'][ $size ];
				}
			}

			if ( isset( $_wp_additional_image_sizes[ $size ] ) ) {
				$width  = (int) $_wp_additional_image_sizes[ $size ]['width'];
				$height = (int) $_wp_additional_image_sizes[ $size ]['height'];
				$crop   = ( $nocrop ) ? false : $_wp_additional_image_sizes[ $size ]['crop'];
			} else {
				$height = get_option( "{$size}_size_h" );
				$width  = get_option( "{$size}_size_w" );
				$crop   = ( $nocrop ) ? false : get_option( "{$size}_crop" );
			}

			$_sizes[ $size ] = array(
				'width'  => $width,
				'height' => $height,
				'crop'   => $crop,
			);
		}

		$meta['sizes'] = array_merge( $meta['sizes'], $img->multi_resize( $_sizes ) );
	}

	unset( $img );

	if ( $success ) {
		wp_update_attachment_metadata( $post_id, $meta );
		update_post_meta( $post_id, '_wp_attachment_backup_sizes', $backup_sizes );

		if ( 'thumbnail' === $target || 'all' === $target || 'full' === $target ) {
			// Check if it's an image edit from attachment edit screen.
			if ( ! empty( $_REQUEST['context'] ) && 'edit-attachment' === $_REQUEST['context'] ) {
				$thumb_url = wp_get_attachment_image_src( $post_id, array( 900, 600 ), true );

				$return->thumbnail = $thumb_url[0];
			} else {
				$file_url = wp_get_attachment_url( $post_id );

				if ( ! empty( $meta['sizes']['thumbnail'] ) ) {
					$thumb             = $meta['sizes']['thumbnail'];
					$return->thumbnail = path_join( dirname( $file_url ), $thumb['file'] );
				} else {
					$return->thumbnail = "$file_url?w=128&h=128";
				}
			}
		}
	} else {
		$delete = true;
	}

	if ( $delete ) {
		wp_delete_file( $new_path );
	}

	$return->msg = esc_js( __( 'Image saved' ) );

	return $return;
}
class-plugin-upgrader.php000060400000055313151327343360011477 0ustar00<?php
/**
 * Upgrade API: Plugin_Upgrader class
 *
 * @package WordPress
 * @subpackage Upgrader
 * @since 4.6.0
 */

/**
 * Core class used for upgrading/installing plugins.
 *
 * It is designed to upgrade/install plugins from a local zip, remote zip URL,
 * or uploaded zip file.
 *
 * @since 2.8.0
 * @since 4.6.0 Moved to its own file from wp-admin/includes/class-wp-upgrader.php.
 *
 * @see WP_Upgrader
 */
class Plugin_Upgrader extends WP_Upgrader {

	/**
	 * Plugin upgrade result.
	 *
	 * @since 2.8.0
	 * @var array|WP_Error $result
	 *
	 * @see WP_Upgrader::$result
	 */
	public $result;

	/**
	 * Whether a bulk upgrade/installation is being performed.
	 *
	 * @since 2.9.0
	 * @var bool $bulk
	 */
	public $bulk = false;

	/**
	 * New plugin info.
	 *
	 * @since 5.5.0
	 * @var array $new_plugin_data
	 *
	 * @see check_package()
	 */
	public $new_plugin_data = array();

	/**
	 * Initializes the upgrade strings.
	 *
	 * @since 2.8.0
	 */
	public function upgrade_strings() {
		$this->strings['up_to_date'] = __( 'The plugin is at the latest version.' );
		$this->strings['no_package'] = __( 'Update package not available.' );
		/* translators: %s: Package URL. */
		$this->strings['downloading_package']  = sprintf( __( 'Downloading update from %s&#8230;' ), '<span class="code pre">%s</span>' );
		$this->strings['unpack_package']       = __( 'Unpacking the update&#8230;' );
		$this->strings['remove_old']           = __( 'Removing the old version of the plugin&#8230;' );
		$this->strings['remove_old_failed']    = __( 'Could not remove the old plugin.' );
		$this->strings['process_failed']       = __( 'Plugin update failed.' );
		$this->strings['process_success']      = __( 'Plugin updated successfully.' );
		$this->strings['process_bulk_success'] = __( 'Plugins updated successfully.' );
	}

	/**
	 * Initializes the installation strings.
	 *
	 * @since 2.8.0
	 */
	public function install_strings() {
		$this->strings['no_package'] = __( 'Installation package not available.' );
		/* translators: %s: Package URL. */
		$this->strings['downloading_package'] = sprintf( __( 'Downloading installation package from %s&#8230;' ), '<span class="code pre">%s</span>' );
		$this->strings['unpack_package']      = __( 'Unpacking the package&#8230;' );
		$this->strings['installing_package']  = __( 'Installing the plugin&#8230;' );
		$this->strings['remove_old']          = __( 'Removing the current plugin&#8230;' );
		$this->strings['remove_old_failed']   = __( 'Could not remove the current plugin.' );
		$this->strings['no_files']            = __( 'The plugin contains no files.' );
		$this->strings['process_failed']      = __( 'Plugin installation failed.' );
		$this->strings['process_success']     = __( 'Plugin installed successfully.' );
		/* translators: 1: Plugin name, 2: Plugin version. */
		$this->strings['process_success_specific'] = __( 'Successfully installed the plugin <strong>%1$s %2$s</strong>.' );

		if ( ! empty( $this->skin->overwrite ) ) {
			if ( 'update-plugin' === $this->skin->overwrite ) {
				$this->strings['installing_package'] = __( 'Updating the plugin&#8230;' );
				$this->strings['process_failed']     = __( 'Plugin update failed.' );
				$this->strings['process_success']    = __( 'Plugin updated successfully.' );
			}

			if ( 'downgrade-plugin' === $this->skin->overwrite ) {
				$this->strings['installing_package'] = __( 'Downgrading the plugin&#8230;' );
				$this->strings['process_failed']     = __( 'Plugin downgrade failed.' );
				$this->strings['process_success']    = __( 'Plugin downgraded successfully.' );
			}
		}
	}

	/**
	 * Install a plugin package.
	 *
	 * @since 2.8.0
	 * @since 3.7.0 The `$args` parameter was added, making clearing the plugin update cache optional.
	 *
	 * @param string $package The full local path or URI of the package.
	 * @param array  $args {
	 *     Optional. Other arguments for installing a plugin package. Default empty array.
	 *
	 *     @type bool $clear_update_cache Whether to clear the plugin updates cache if successful.
	 *                                    Default true.
	 * }
	 * @return bool|WP_Error True if the installation was successful, false or a WP_Error otherwise.
	 */
	public function install( $package, $args = array() ) {
		$defaults    = array(
			'clear_update_cache' => true,
			'overwrite_package'  => false, // Do not overwrite files.
		);
		$parsed_args = wp_parse_args( $args, $defaults );

		$this->init();
		$this->install_strings();

		add_filter( 'upgrader_source_selection', array( $this, 'check_package' ) );

		if ( $parsed_args['clear_update_cache'] ) {
			// Clear cache so wp_update_plugins() knows about the new plugin.
			add_action( 'upgrader_process_complete', 'wp_clean_plugins_cache', 9, 0 );
		}

		$this->run(
			array(
				'package'           => $package,
				'destination'       => WP_PLUGIN_DIR,
				'clear_destination' => $parsed_args['overwrite_package'],
				'clear_working'     => true,
				'hook_extra'        => array(
					'type'   => 'plugin',
					'action' => 'install',
				),
			)
		);

		remove_action( 'upgrader_process_complete', 'wp_clean_plugins_cache', 9 );
		remove_filter( 'upgrader_source_selection', array( $this, 'check_package' ) );

		if ( ! $this->result || is_wp_error( $this->result ) ) {
			return $this->result;
		}

		// Force refresh of plugin update information.
		wp_clean_plugins_cache( $parsed_args['clear_update_cache'] );

		if ( $parsed_args['overwrite_package'] ) {
			/**
			 * Fires when the upgrader has successfully overwritten a currently installed
			 * plugin or theme with an uploaded zip package.
			 *
			 * @since 5.5.0
			 *
			 * @param string  $package      The package file.
			 * @param array   $data         The new plugin or theme data.
			 * @param string  $package_type The package type ('plugin' or 'theme').
			 */
			do_action( 'upgrader_overwrote_package', $package, $this->new_plugin_data, 'plugin' );
		}

		return true;
	}

	/**
	 * Upgrades a plugin.
	 *
	 * @since 2.8.0
	 * @since 3.7.0 The `$args` parameter was added, making clearing the plugin update cache optional.
	 *
	 * @param string $plugin Path to the plugin file relative to the plugins directory.
	 * @param array  $args {
	 *     Optional. Other arguments for upgrading a plugin package. Default empty array.
	 *
	 *     @type bool $clear_update_cache Whether to clear the plugin updates cache if successful.
	 *                                    Default true.
	 * }
	 * @return bool|WP_Error True if the upgrade was successful, false or a WP_Error object otherwise.
	 */
	public function upgrade( $plugin, $args = array() ) {
		$defaults    = array(
			'clear_update_cache' => true,
		);
		$parsed_args = wp_parse_args( $args, $defaults );

		$this->init();
		$this->upgrade_strings();

		$current = get_site_transient( 'update_plugins' );
		if ( ! isset( $current->response[ $plugin ] ) ) {
			$this->skin->before();
			$this->skin->set_result( false );
			$this->skin->error( 'up_to_date' );
			$this->skin->after();
			return false;
		}

		// Get the URL to the zip file.
		$r = $current->response[ $plugin ];

		add_filter( 'upgrader_pre_install', array( $this, 'deactivate_plugin_before_upgrade' ), 10, 2 );
		add_filter( 'upgrader_pre_install', array( $this, 'active_before' ), 10, 2 );
		add_filter( 'upgrader_clear_destination', array( $this, 'delete_old_plugin' ), 10, 4 );
		add_filter( 'upgrader_post_install', array( $this, 'active_after' ), 10, 2 );
		/*
		 * There's a Trac ticket to move up the directory for zips which are made a bit differently, useful for non-.org plugins.
		 * 'source_selection' => array( $this, 'source_selection' ),
		 */
		if ( $parsed_args['clear_update_cache'] ) {
			// Clear cache so wp_update_plugins() knows about the new plugin.
			add_action( 'upgrader_process_complete', 'wp_clean_plugins_cache', 9, 0 );
		}

		$this->run(
			array(
				'package'           => $r->package,
				'destination'       => WP_PLUGIN_DIR,
				'clear_destination' => true,
				'clear_working'     => true,
				'hook_extra'        => array(
					'plugin'      => $plugin,
					'type'        => 'plugin',
					'action'      => 'update',
					'temp_backup' => array(
						'slug' => dirname( $plugin ),
						'src'  => WP_PLUGIN_DIR,
						'dir'  => 'plugins',
					),
				),
			)
		);

		// Cleanup our hooks, in case something else does an upgrade on this connection.
		remove_action( 'upgrader_process_complete', 'wp_clean_plugins_cache', 9 );
		remove_filter( 'upgrader_pre_install', array( $this, 'deactivate_plugin_before_upgrade' ) );
		remove_filter( 'upgrader_pre_install', array( $this, 'active_before' ) );
		remove_filter( 'upgrader_clear_destination', array( $this, 'delete_old_plugin' ) );
		remove_filter( 'upgrader_post_install', array( $this, 'active_after' ) );

		if ( ! $this->result || is_wp_error( $this->result ) ) {
			return $this->result;
		}

		// Force refresh of plugin update information.
		wp_clean_plugins_cache( $parsed_args['clear_update_cache'] );

		/*
		 * Ensure any future auto-update failures trigger a failure email by removing
		 * the last failure notification from the list when plugins update successfully.
		 */
		$past_failure_emails = get_option( 'auto_plugin_theme_update_emails', array() );

		if ( isset( $past_failure_emails[ $plugin ] ) ) {
			unset( $past_failure_emails[ $plugin ] );
			update_option( 'auto_plugin_theme_update_emails', $past_failure_emails );
		}

		return true;
	}

	/**
	 * Upgrades several plugins at once.
	 *
	 * @since 2.8.0
	 * @since 3.7.0 The `$args` parameter was added, making clearing the plugin update cache optional.
	 *
	 * @param string[] $plugins Array of paths to plugin files relative to the plugins directory.
	 * @param array    $args {
	 *     Optional. Other arguments for upgrading several plugins at once.
	 *
	 *     @type bool $clear_update_cache Whether to clear the plugin updates cache if successful. Default true.
	 * }
	 * @return array|false An array of results indexed by plugin file, or false if unable to connect to the filesystem.
	 */
	public function bulk_upgrade( $plugins, $args = array() ) {
		$wp_version = wp_get_wp_version();

		$defaults    = array(
			'clear_update_cache' => true,
		);
		$parsed_args = wp_parse_args( $args, $defaults );

		$this->init();
		$this->bulk = true;
		$this->upgrade_strings();

		$current = get_site_transient( 'update_plugins' );

		add_filter( 'upgrader_clear_destination', array( $this, 'delete_old_plugin' ), 10, 4 );

		$this->skin->header();

		// Connect to the filesystem first.
		$res = $this->fs_connect( array( WP_CONTENT_DIR, WP_PLUGIN_DIR ) );
		if ( ! $res ) {
			$this->skin->footer();
			return false;
		}

		$this->skin->bulk_header();

		/*
		 * Only start maintenance mode if:
		 * - running Multisite and there are one or more plugins specified, OR
		 * - a plugin with an update available is currently active.
		 * @todo For multisite, maintenance mode should only kick in for individual sites if at all possible.
		 */
		$maintenance = ( is_multisite() && ! empty( $plugins ) );
		foreach ( $plugins as $plugin ) {
			$maintenance = $maintenance || ( is_plugin_active( $plugin ) && isset( $current->response[ $plugin ] ) );
		}
		if ( $maintenance ) {
			$this->maintenance_mode( true );
		}

		$results = array();

		$this->update_count   = count( $plugins );
		$this->update_current = 0;
		foreach ( $plugins as $plugin ) {
			++$this->update_current;
			$this->skin->plugin_info = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin, false, true );

			if ( ! isset( $current->response[ $plugin ] ) ) {
				$this->skin->set_result( 'up_to_date' );
				$this->skin->before();
				$this->skin->feedback( 'up_to_date' );
				$this->skin->after();
				$results[ $plugin ] = true;
				continue;
			}

			// Get the URL to the zip file.
			$r = $current->response[ $plugin ];

			$this->skin->plugin_active = is_plugin_active( $plugin );

			if ( isset( $r->requires ) && ! is_wp_version_compatible( $r->requires ) ) {
				$result = new WP_Error(
					'incompatible_wp_required_version',
					sprintf(
						/* translators: 1: Current WordPress version, 2: WordPress version required by the new plugin version. */
						__( 'Your WordPress version is %1$s, however the new plugin version requires %2$s.' ),
						$wp_version,
						$r->requires
					)
				);

				$this->skin->before( $result );
				$this->skin->error( $result );
				$this->skin->after();
			} elseif ( isset( $r->requires_php ) && ! is_php_version_compatible( $r->requires_php ) ) {
				$result = new WP_Error(
					'incompatible_php_required_version',
					sprintf(
						/* translators: 1: Current PHP version, 2: PHP version required by the new plugin version. */
						__( 'The PHP version on your server is %1$s, however the new plugin version requires %2$s.' ),
						PHP_VERSION,
						$r->requires_php
					)
				);

				$this->skin->before( $result );
				$this->skin->error( $result );
				$this->skin->after();
			} else {
				add_filter( 'upgrader_source_selection', array( $this, 'check_package' ) );
				$result = $this->run(
					array(
						'package'           => $r->package,
						'destination'       => WP_PLUGIN_DIR,
						'clear_destination' => true,
						'clear_working'     => true,
						'is_multi'          => true,
						'hook_extra'        => array(
							'plugin'      => $plugin,
							'temp_backup' => array(
								'slug' => dirname( $plugin ),
								'src'  => WP_PLUGIN_DIR,
								'dir'  => 'plugins',
							),
						),
					)
				);
				remove_filter( 'upgrader_source_selection', array( $this, 'check_package' ) );
			}

			$results[ $plugin ] = $result;

			// Prevent credentials auth screen from displaying multiple times.
			if ( false === $result ) {
				break;
			}
		} // End foreach $plugins.

		$this->maintenance_mode( false );

		// Force refresh of plugin update information.
		wp_clean_plugins_cache( $parsed_args['clear_update_cache'] );

		/** This action is documented in wp-admin/includes/class-wp-upgrader.php */
		do_action(
			'upgrader_process_complete',
			$this,
			array(
				'action'  => 'update',
				'type'    => 'plugin',
				'bulk'    => true,
				'plugins' => $plugins,
			)
		);

		$this->skin->bulk_footer();

		$this->skin->footer();

		// Cleanup our hooks, in case something else does an upgrade on this connection.
		remove_filter( 'upgrader_clear_destination', array( $this, 'delete_old_plugin' ) );

		/*
		 * Ensure any future auto-update failures trigger a failure email by removing
		 * the last failure notification from the list when plugins update successfully.
		 */
		$past_failure_emails = get_option( 'auto_plugin_theme_update_emails', array() );

		foreach ( $results as $plugin => $result ) {
			// Maintain last failure notification when plugins failed to update manually.
			if ( ! $result || is_wp_error( $result ) || ! isset( $past_failure_emails[ $plugin ] ) ) {
				continue;
			}

			unset( $past_failure_emails[ $plugin ] );
		}

		update_option( 'auto_plugin_theme_update_emails', $past_failure_emails );

		return $results;
	}

	/**
	 * Checks that the source package contains a valid plugin.
	 *
	 * Hooked to the {@see 'upgrader_source_selection'} filter by Plugin_Upgrader::install().
	 *
	 * @since 3.3.0
	 *
	 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
	 *
	 * @param string $source The path to the downloaded package source.
	 * @return string|WP_Error The source as passed, or a WP_Error object on failure.
	 */
	public function check_package( $source ) {
		global $wp_filesystem;

		$wp_version            = wp_get_wp_version();
		$this->new_plugin_data = array();

		if ( is_wp_error( $source ) ) {
			return $source;
		}

		$working_directory = str_replace( $wp_filesystem->wp_content_dir(), trailingslashit( WP_CONTENT_DIR ), $source );
		if ( ! is_dir( $working_directory ) ) { // Confidence check, if the above fails, let's not prevent installation.
			return $source;
		}

		// Check that the folder contains at least 1 valid plugin.
		$files = glob( $working_directory . '*.php' );
		if ( $files ) {
			foreach ( $files as $file ) {
				$info = get_plugin_data( $file, false, false );
				if ( ! empty( $info['Name'] ) ) {
					$this->new_plugin_data = $info;
					break;
				}
			}
		}

		if ( empty( $this->new_plugin_data ) ) {
			return new WP_Error( 'incompatible_archive_no_plugins', $this->strings['incompatible_archive'], __( 'No valid plugins were found.' ) );
		}

		$requires_php = isset( $info['RequiresPHP'] ) ? $info['RequiresPHP'] : null;
		$requires_wp  = isset( $info['RequiresWP'] ) ? $info['RequiresWP'] : null;

		if ( ! is_php_version_compatible( $requires_php ) ) {
			$error = sprintf(
				/* translators: 1: Current PHP version, 2: Version required by the uploaded plugin. */
				__( 'The PHP version on your server is %1$s, however the uploaded plugin requires %2$s.' ),
				PHP_VERSION,
				$requires_php
			);

			return new WP_Error( 'incompatible_php_required_version', $this->strings['incompatible_archive'], $error );
		}

		if ( ! is_wp_version_compatible( $requires_wp ) ) {
			$error = sprintf(
				/* translators: 1: Current WordPress version, 2: Version required by the uploaded plugin. */
				__( 'Your WordPress version is %1$s, however the uploaded plugin requires %2$s.' ),
				$wp_version,
				$requires_wp
			);

			return new WP_Error( 'incompatible_wp_required_version', $this->strings['incompatible_archive'], $error );
		}

		return $source;
	}

	/**
	 * Retrieves the path to the file that contains the plugin info.
	 *
	 * This isn't used internally in the class, but is called by the skins.
	 *
	 * @since 2.8.0
	 *
	 * @return string|false The full path to the main plugin file, or false.
	 */
	public function plugin_info() {
		if ( ! is_array( $this->result ) ) {
			return false;
		}
		if ( empty( $this->result['destination_name'] ) ) {
			return false;
		}

		// Ensure to pass with leading slash.
		$plugin = get_plugins( '/' . $this->result['destination_name'] );
		if ( empty( $plugin ) ) {
			return false;
		}

		// Assume the requested plugin is the first in the list.
		$pluginfiles = array_keys( $plugin );

		return $this->result['destination_name'] . '/' . $pluginfiles[0];
	}

	/**
	 * Deactivates a plugin before it is upgraded.
	 *
	 * Hooked to the {@see 'upgrader_pre_install'} filter by Plugin_Upgrader::upgrade().
	 *
	 * @since 2.8.0
	 * @since 4.1.0 Added a return value.
	 *
	 * @param bool|WP_Error $response The installation response before the installation has started.
	 * @param array         $plugin   Plugin package arguments.
	 * @return bool|WP_Error The original `$response` parameter or WP_Error.
	 */
	public function deactivate_plugin_before_upgrade( $response, $plugin ) {

		if ( is_wp_error( $response ) ) { // Bypass.
			return $response;
		}

		// When in cron (background updates) don't deactivate the plugin, as we require a browser to reactivate it.
		if ( wp_doing_cron() ) {
			return $response;
		}

		$plugin = isset( $plugin['plugin'] ) ? $plugin['plugin'] : '';
		if ( empty( $plugin ) ) {
			return new WP_Error( 'bad_request', $this->strings['bad_request'] );
		}

		if ( is_plugin_active( $plugin ) ) {
			// Deactivate the plugin silently, Prevent deactivation hooks from running.
			deactivate_plugins( $plugin, true );
		}

		return $response;
	}

	/**
	 * Turns on maintenance mode before attempting to background update an active plugin.
	 *
	 * Hooked to the {@see 'upgrader_pre_install'} filter by Plugin_Upgrader::upgrade().
	 *
	 * @since 5.4.0
	 *
	 * @param bool|WP_Error $response The installation response before the installation has started.
	 * @param array         $plugin   Plugin package arguments.
	 * @return bool|WP_Error The original `$response` parameter or WP_Error.
	 */
	public function active_before( $response, $plugin ) {
		if ( is_wp_error( $response ) ) {
			return $response;
		}

		// Only enable maintenance mode when in cron (background update).
		if ( ! wp_doing_cron() ) {
			return $response;
		}

		$plugin = isset( $plugin['plugin'] ) ? $plugin['plugin'] : '';

		// Only run if plugin is active.
		if ( ! is_plugin_active( $plugin ) ) {
			return $response;
		}

		// Change to maintenance mode. Bulk edit handles this separately.
		if ( ! $this->bulk ) {
			$this->maintenance_mode( true );
		}

		return $response;
	}

	/**
	 * Turns off maintenance mode after upgrading an active plugin.
	 *
	 * Hooked to the {@see 'upgrader_post_install'} filter by Plugin_Upgrader::upgrade().
	 *
	 * @since 5.4.0
	 *
	 * @param bool|WP_Error $response The installation response after the installation has finished.
	 * @param array         $plugin   Plugin package arguments.
	 * @return bool|WP_Error The original `$response` parameter or WP_Error.
	 */
	public function active_after( $response, $plugin ) {
		if ( is_wp_error( $response ) ) {
			return $response;
		}

		// Only disable maintenance mode when in cron (background update).
		if ( ! wp_doing_cron() ) {
			return $response;
		}

		$plugin = isset( $plugin['plugin'] ) ? $plugin['plugin'] : '';

		// Only run if plugin is active.
		if ( ! is_plugin_active( $plugin ) ) {
			return $response;
		}

		// Time to remove maintenance mode. Bulk edit handles this separately.
		if ( ! $this->bulk ) {
			$this->maintenance_mode( false );
		}

		return $response;
	}

	/**
	 * Deletes the old plugin during an upgrade.
	 *
	 * Hooked to the {@see 'upgrader_clear_destination'} filter by
	 * Plugin_Upgrader::upgrade() and Plugin_Upgrader::bulk_upgrade().
	 *
	 * @since 2.8.0
	 *
	 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
	 *
	 * @param bool|WP_Error $removed            Whether the destination was cleared.
	 *                                          True on success, WP_Error on failure.
	 * @param string        $local_destination  The local package destination.
	 * @param string        $remote_destination The remote package destination.
	 * @param array         $plugin             Extra arguments passed to hooked filters.
	 * @return bool|WP_Error
	 */
	public function delete_old_plugin( $removed, $local_destination, $remote_destination, $plugin ) {
		global $wp_filesystem;

		if ( is_wp_error( $removed ) ) {
			return $removed; // Pass errors through.
		}

		$plugin = isset( $plugin['plugin'] ) ? $plugin['plugin'] : '';
		if ( empty( $plugin ) ) {
			return new WP_Error( 'bad_request', $this->strings['bad_request'] );
		}

		$plugins_dir     = $wp_filesystem->wp_plugins_dir();
		$this_plugin_dir = trailingslashit( dirname( $plugins_dir . $plugin ) );

		if ( ! $wp_filesystem->exists( $this_plugin_dir ) ) { // If it's already vanished.
			return $removed;
		}

		/*
		 * If plugin is in its own directory, recursively delete the directory.
		 * Base check on if plugin includes directory separator AND that it's not the root plugin folder.
		 */
		if ( strpos( $plugin, '/' ) && $this_plugin_dir !== $plugins_dir ) {
			$deleted = $wp_filesystem->delete( $this_plugin_dir, true );
		} else {
			$deleted = $wp_filesystem->delete( $plugins_dir . $plugin );
		}

		if ( ! $deleted ) {
			return new WP_Error( 'remove_old_failed', $this->strings['remove_old_failed'] );
		}

		return true;
	}
}
class-wp-links-list-table.php000060400000022031151327343360012163 0ustar00<?php
/**
 * List Table API: WP_Links_List_Table class
 *
 * @package WordPress
 * @subpackage Administration
 * @since 3.1.0
 */

/**
 * Core class used to implement displaying links in a list table.
 *
 * @since 3.1.0
 *
 * @see WP_List_Table
 */
class WP_Links_List_Table extends WP_List_Table {

	/**
	 * Constructor.
	 *
	 * @since 3.1.0
	 *
	 * @see WP_List_Table::__construct() for more information on default arguments.
	 *
	 * @param array $args An associative array of arguments.
	 */
	public function __construct( $args = array() ) {
		parent::__construct(
			array(
				'plural' => 'bookmarks',
				'screen' => isset( $args['screen'] ) ? $args['screen'] : null,
			)
		);
	}

	/**
	 * @return bool
	 */
	public function ajax_user_can() {
		return current_user_can( 'manage_links' );
	}

	/**
	 * @global int    $cat_id
	 * @global string $s
	 * @global string $orderby
	 * @global string $order
	 */
	public function prepare_items() {
		global $cat_id, $s, $orderby, $order;

		$cat_id  = ! empty( $_REQUEST['cat_id'] ) ? absint( $_REQUEST['cat_id'] ) : 0;
		$orderby = ! empty( $_REQUEST['orderby'] ) ? sanitize_text_field( $_REQUEST['orderby'] ) : '';
		$order   = ! empty( $_REQUEST['order'] ) ? sanitize_text_field( $_REQUEST['order'] ) : '';
		$s       = ! empty( $_REQUEST['s'] ) ? sanitize_text_field( $_REQUEST['s'] ) : '';

		$args = array(
			'hide_invisible' => 0,
			'hide_empty'     => 0,
		);

		if ( 'all' !== $cat_id ) {
			$args['category'] = $cat_id;
		}
		if ( ! empty( $s ) ) {
			$args['search'] = $s;
		}
		if ( ! empty( $orderby ) ) {
			$args['orderby'] = $orderby;
		}
		if ( ! empty( $order ) ) {
			$args['order'] = $order;
		}

		$this->items = get_bookmarks( $args );
	}

	/**
	 */
	public function no_items() {
		_e( 'No links found.' );
	}

	/**
	 * @return array
	 */
	protected function get_bulk_actions() {
		$actions           = array();
		$actions['delete'] = __( 'Delete' );

		return $actions;
	}

	/**
	 * @global int $cat_id
	 * @param string $which
	 */
	protected function extra_tablenav( $which ) {
		global $cat_id;

		if ( 'top' !== $which ) {
			return;
		}
		?>
		<div class="alignleft actions">
			<?php
			$dropdown_options = array(
				'selected'        => $cat_id,
				'name'            => 'cat_id',
				'taxonomy'        => 'link_category',
				'show_option_all' => get_taxonomy( 'link_category' )->labels->all_items,
				'hide_empty'      => true,
				'hierarchical'    => 1,
				'show_count'      => 0,
				'orderby'         => 'name',
			);

			echo '<label class="screen-reader-text" for="cat_id">' . get_taxonomy( 'link_category' )->labels->filter_by_item . '</label>';

			wp_dropdown_categories( $dropdown_options );

			submit_button( __( 'Filter' ), '', 'filter_action', false, array( 'id' => 'post-query-submit' ) );
			?>
		</div>
		<?php
	}

	/**
	 * @return string[] Array of column titles keyed by their column name.
	 */
	public function get_columns() {
		return array(
			'cb'         => '<input type="checkbox" />',
			'name'       => _x( 'Name', 'link name' ),
			'url'        => __( 'URL' ),
			'categories' => __( 'Categories' ),
			'rel'        => __( 'Relationship' ),
			'visible'    => __( 'Visible' ),
			'rating'     => __( 'Rating' ),
		);
	}

	/**
	 * @return array
	 */
	protected function get_sortable_columns() {
		return array(
			'name'    => array( 'name', false, _x( 'Name', 'link name' ), __( 'Table ordered by Name.' ), 'asc' ),
			'url'     => array( 'url', false, __( 'URL' ), __( 'Table ordered by URL.' ) ),
			'visible' => array( 'visible', false, __( 'Visible' ), __( 'Table ordered by Visibility.' ) ),
			'rating'  => array( 'rating', false, __( 'Rating' ), __( 'Table ordered by Rating.' ) ),
		);
	}

	/**
	 * Gets the name of the default primary column.
	 *
	 * @since 4.3.0
	 *
	 * @return string Name of the default primary column, in this case, 'name'.
	 */
	protected function get_default_primary_column_name() {
		return 'name';
	}

	/**
	 * Handles the checkbox column output.
	 *
	 * @since 4.3.0
	 * @since 5.9.0 Renamed `$link` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param object $item The current link object.
	 */
	public function column_cb( $item ) {
		// Restores the more descriptive, specific name for use within this method.
		$link = $item;

		?>
		<input type="checkbox" name="linkcheck[]" id="cb-select-<?php echo $link->link_id; ?>" value="<?php echo esc_attr( $link->link_id ); ?>" />
		<label for="cb-select-<?php echo $link->link_id; ?>">
			<span class="screen-reader-text">
			<?php
			/* translators: Hidden accessibility text. %s: Link name. */
			printf( __( 'Select %s' ), $link->link_name );
			?>
			</span>
		</label>
		<?php
	}

	/**
	 * Handles the link name column output.
	 *
	 * @since 4.3.0
	 *
	 * @param object $link The current link object.
	 */
	public function column_name( $link ) {
		$edit_link = get_edit_bookmark_link( $link );
		printf(
			'<strong><a class="row-title" href="%s" aria-label="%s">%s</a></strong>',
			$edit_link,
			/* translators: %s: Link name. */
			esc_attr( sprintf( __( 'Edit &#8220;%s&#8221;' ), $link->link_name ) ),
			$link->link_name
		);
	}

	/**
	 * Handles the link URL column output.
	 *
	 * @since 4.3.0
	 *
	 * @param object $link The current link object.
	 */
	public function column_url( $link ) {
		$short_url = url_shorten( $link->link_url );
		echo "<a href='$link->link_url'>$short_url</a>";
	}

	/**
	 * Handles the link categories column output.
	 *
	 * @since 4.3.0
	 *
	 * @global int $cat_id
	 *
	 * @param object $link The current link object.
	 */
	public function column_categories( $link ) {
		global $cat_id;

		$cat_names = array();
		foreach ( $link->link_category as $category ) {
			$cat = get_term( $category, 'link_category', OBJECT, 'display' );
			if ( is_wp_error( $cat ) ) {
				echo $cat->get_error_message();
			}
			$cat_name = $cat->name;
			if ( (int) $cat_id !== $category ) {
				$cat_name = "<a href='link-manager.php?cat_id=$category'>$cat_name</a>";
			}
			$cat_names[] = $cat_name;
		}
		echo implode( ', ', $cat_names );
	}

	/**
	 * Handles the link relation column output.
	 *
	 * @since 4.3.0
	 *
	 * @param object $link The current link object.
	 */
	public function column_rel( $link ) {
		echo empty( $link->link_rel ) ? '<br />' : $link->link_rel;
	}

	/**
	 * Handles the link visibility column output.
	 *
	 * @since 4.3.0
	 *
	 * @param object $link The current link object.
	 */
	public function column_visible( $link ) {
		if ( 'Y' === $link->link_visible ) {
			_e( 'Yes' );
		} else {
			_e( 'No' );
		}
	}

	/**
	 * Handles the link rating column output.
	 *
	 * @since 4.3.0
	 *
	 * @param object $link The current link object.
	 */
	public function column_rating( $link ) {
		echo $link->link_rating;
	}

	/**
	 * Handles the default column output.
	 *
	 * @since 4.3.0
	 * @since 5.9.0 Renamed `$link` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param object $item        Link object.
	 * @param string $column_name Current column name.
	 */
	public function column_default( $item, $column_name ) {
		// Restores the more descriptive, specific name for use within this method.
		$link = $item;

		/**
		 * Fires for each registered custom link column.
		 *
		 * @since 2.1.0
		 *
		 * @param string $column_name Name of the custom column.
		 * @param int    $link_id     Link ID.
		 */
		do_action( 'manage_link_custom_column', $column_name, $link->link_id );
	}

	/**
	 * Generates the list table rows.
	 *
	 * @since 3.1.0
	 */
	public function display_rows() {
		foreach ( $this->items as $link ) {
			$link                = sanitize_bookmark( $link );
			$link->link_name     = esc_attr( $link->link_name );
			$link->link_category = wp_get_link_cats( $link->link_id );
			?>
		<tr id="link-<?php echo $link->link_id; ?>">
			<?php $this->single_row_columns( $link ); ?>
		</tr>
			<?php
		}
	}

	/**
	 * Generates and displays row action links.
	 *
	 * @since 4.3.0
	 * @since 5.9.0 Renamed `$link` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param object $item        Link being acted upon.
	 * @param string $column_name Current column name.
	 * @param string $primary     Primary column name.
	 * @return string Row actions output for links, or an empty string
	 *                if the current column is not the primary column.
	 */
	protected function handle_row_actions( $item, $column_name, $primary ) {
		if ( $primary !== $column_name ) {
			return '';
		}

		// Restores the more descriptive, specific name for use within this method.
		$link = $item;

		$edit_link = get_edit_bookmark_link( $link );

		$actions           = array();
		$actions['edit']   = '<a href="' . $edit_link . '">' . __( 'Edit' ) . '</a>';
		$actions['delete'] = sprintf(
			'<a class="submitdelete" href="%s" onclick="return confirm( \'%s\' );">%s</a>',
			wp_nonce_url( "link.php?action=delete&amp;link_id=$link->link_id", 'delete-bookmark_' . $link->link_id ),
			/* translators: %s: Link name. */
			esc_js( sprintf( __( "You are about to delete this link '%s'\n  'Cancel' to stop, 'OK' to delete." ), $link->link_name ) ),
			__( 'Delete' )
		);

		return $this->row_actions( $actions );
	}
}
plugin.php000060400000265375151327343360006600 0ustar00<?php
/**
 * WordPress Plugin Administration API
 *
 * @package WordPress
 * @subpackage Administration
 */

/**
 * Parses the plugin contents to retrieve plugin's metadata.
 *
 * All plugin headers must be on their own line. Plugin description must not have
 * any newlines, otherwise only parts of the description will be displayed.
 * The below is formatted for printing.
 *
 *     /*
 *     Plugin Name: Name of the plugin.
 *     Plugin URI: The home page of the plugin.
 *     Description: Plugin description.
 *     Author: Plugin author's name.
 *     Author URI: Link to the author's website.
 *     Version: Plugin version.
 *     Text Domain: Optional. Unique identifier, should be same as the one used in
 *          load_plugin_textdomain().
 *     Domain Path: Optional. Only useful if the translations are located in a
 *          folder above the plugin's base path. For example, if .mo files are
 *          located in the locale folder then Domain Path will be "/locale/" and
 *          must have the first slash. Defaults to the base folder the plugin is
 *          located in.
 *     Network: Optional. Specify "Network: true" to require that a plugin is activated
 *          across all sites in an installation. This will prevent a plugin from being
 *          activated on a single site when Multisite is enabled.
 *     Requires at least: Optional. Specify the minimum required WordPress version.
 *     Requires PHP: Optional. Specify the minimum required PHP version.
 *     * / # Remove the space to close comment.
 *
 * The first 8 KB of the file will be pulled in and if the plugin data is not
 * within that first 8 KB, then the plugin author should correct their plugin
 * and move the plugin data headers to the top.
 *
 * The plugin file is assumed to have permissions to allow for scripts to read
 * the file. This is not checked however and the file is only opened for
 * reading.
 *
 * @since 1.5.0
 * @since 5.3.0 Added support for `Requires at least` and `Requires PHP` headers.
 * @since 5.8.0 Added support for `Update URI` header.
 * @since 6.5.0 Added support for `Requires Plugins` header.
 *
 * @param string $plugin_file Absolute path to the main plugin file.
 * @param bool   $markup      Optional. If the returned data should have HTML markup applied.
 *                            Default true.
 * @param bool   $translate   Optional. If the returned data should be translated. Default true.
 * @return array {
 *     Plugin data. Values will be empty if not supplied by the plugin.
 *
 *     @type string $Name            Name of the plugin. Should be unique.
 *     @type string $PluginURI       Plugin URI.
 *     @type string $Version         Plugin version.
 *     @type string $Description     Plugin description.
 *     @type string $Author          Plugin author's name.
 *     @type string $AuthorURI       Plugin author's website address (if set).
 *     @type string $TextDomain      Plugin textdomain.
 *     @type string $DomainPath      Plugin's relative directory path to .mo files.
 *     @type bool   $Network         Whether the plugin can only be activated network-wide.
 *     @type string $RequiresWP      Minimum required version of WordPress.
 *     @type string $RequiresPHP     Minimum required version of PHP.
 *     @type string $UpdateURI       ID of the plugin for update purposes, should be a URI.
 *     @type string $RequiresPlugins Comma separated list of dot org plugin slugs.
 *     @type string $Title           Title of the plugin and link to the plugin's site (if set).
 *     @type string $AuthorName      Plugin author's name.
 * }
 */
function get_plugin_data( $plugin_file, $markup = true, $translate = true ) {

	$default_headers = array(
		'Name'            => 'Plugin Name',
		'PluginURI'       => 'Plugin URI',
		'Version'         => 'Version',
		'Description'     => 'Description',
		'Author'          => 'Author',
		'AuthorURI'       => 'Author URI',
		'TextDomain'      => 'Text Domain',
		'DomainPath'      => 'Domain Path',
		'Network'         => 'Network',
		'RequiresWP'      => 'Requires at least',
		'RequiresPHP'     => 'Requires PHP',
		'UpdateURI'       => 'Update URI',
		'RequiresPlugins' => 'Requires Plugins',
		// Site Wide Only is deprecated in favor of Network.
		'_sitewide'       => 'Site Wide Only',
	);

	$plugin_data = get_file_data( $plugin_file, $default_headers, 'plugin' );

	// Site Wide Only is the old header for Network.
	if ( ! $plugin_data['Network'] && $plugin_data['_sitewide'] ) {
		/* translators: 1: Site Wide Only: true, 2: Network: true */
		_deprecated_argument( __FUNCTION__, '3.0.0', sprintf( __( 'The %1$s plugin header is deprecated. Use %2$s instead.' ), '<code>Site Wide Only: true</code>', '<code>Network: true</code>' ) );
		$plugin_data['Network'] = $plugin_data['_sitewide'];
	}
	$plugin_data['Network'] = ( 'true' === strtolower( $plugin_data['Network'] ) );
	unset( $plugin_data['_sitewide'] );

	// If no text domain is defined fall back to the plugin slug.
	if ( ! $plugin_data['TextDomain'] ) {
		$plugin_slug = dirname( plugin_basename( $plugin_file ) );
		if ( '.' !== $plugin_slug && ! str_contains( $plugin_slug, '/' ) ) {
			$plugin_data['TextDomain'] = $plugin_slug;
		}
	}

	if ( $markup || $translate ) {
		$plugin_data = _get_plugin_data_markup_translate( $plugin_file, $plugin_data, $markup, $translate );
	} else {
		$plugin_data['Title']      = $plugin_data['Name'];
		$plugin_data['AuthorName'] = $plugin_data['Author'];
	}

	return $plugin_data;
}

/**
 * Sanitizes plugin data, optionally adds markup, optionally translates.
 *
 * @since 2.7.0
 *
 * @see get_plugin_data()
 *
 * @access private
 *
 * @param string $plugin_file Path to the main plugin file.
 * @param array  $plugin_data An array of plugin data. See get_plugin_data().
 * @param bool   $markup      Optional. If the returned data should have HTML markup applied.
 *                            Default true.
 * @param bool   $translate   Optional. If the returned data should be translated. Default true.
 * @return array Plugin data. Values will be empty if not supplied by the plugin.
 *               See get_plugin_data() for the list of possible values.
 */
function _get_plugin_data_markup_translate( $plugin_file, $plugin_data, $markup = true, $translate = true ) {

	// Sanitize the plugin filename to a WP_PLUGIN_DIR relative path.
	$plugin_file = plugin_basename( $plugin_file );

	// Translate fields.
	if ( $translate ) {
		$textdomain = $plugin_data['TextDomain'];
		if ( $textdomain ) {
			if ( ! is_textdomain_loaded( $textdomain ) ) {
				if ( $plugin_data['DomainPath'] ) {
					load_plugin_textdomain( $textdomain, false, dirname( $plugin_file ) . $plugin_data['DomainPath'] );
				} else {
					load_plugin_textdomain( $textdomain, false, dirname( $plugin_file ) );
				}
			}
		} elseif ( 'hello.php' === basename( $plugin_file ) ) {
			$textdomain = 'default';
		}
		if ( $textdomain ) {
			foreach ( array( 'Name', 'PluginURI', 'Description', 'Author', 'AuthorURI', 'Version' ) as $field ) {
				if ( ! empty( $plugin_data[ $field ] ) ) {
					// phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain
					$plugin_data[ $field ] = translate( $plugin_data[ $field ], $textdomain );
				}
			}
		}
	}

	// Sanitize fields.
	$allowed_tags_in_links = array(
		'abbr'    => array( 'title' => true ),
		'acronym' => array( 'title' => true ),
		'code'    => true,
		'em'      => true,
		'strong'  => true,
	);

	$allowed_tags      = $allowed_tags_in_links;
	$allowed_tags['a'] = array(
		'href'  => true,
		'title' => true,
	);

	/*
	 * Name is marked up inside <a> tags. Don't allow these.
	 * Author is too, but some plugins have used <a> here (omitting Author URI).
	 */
	$plugin_data['Name']   = wp_kses( $plugin_data['Name'], $allowed_tags_in_links );
	$plugin_data['Author'] = wp_kses( $plugin_data['Author'], $allowed_tags );

	$plugin_data['Description'] = wp_kses( $plugin_data['Description'], $allowed_tags );
	$plugin_data['Version']     = wp_kses( $plugin_data['Version'], $allowed_tags );

	$plugin_data['PluginURI'] = esc_url( $plugin_data['PluginURI'] );
	$plugin_data['AuthorURI'] = esc_url( $plugin_data['AuthorURI'] );

	$plugin_data['Title']      = $plugin_data['Name'];
	$plugin_data['AuthorName'] = $plugin_data['Author'];

	// Apply markup.
	if ( $markup ) {
		if ( $plugin_data['PluginURI'] && $plugin_data['Name'] ) {
			$plugin_data['Title'] = '<a href="' . $plugin_data['PluginURI'] . '">' . $plugin_data['Name'] . '</a>';
		}

		if ( $plugin_data['AuthorURI'] && $plugin_data['Author'] ) {
			$plugin_data['Author'] = '<a href="' . $plugin_data['AuthorURI'] . '">' . $plugin_data['Author'] . '</a>';
		}

		$plugin_data['Description'] = wptexturize( $plugin_data['Description'] );

		if ( $plugin_data['Author'] ) {
			$plugin_data['Description'] .= sprintf(
				/* translators: %s: Plugin author. */
				' <cite>' . __( 'By %s.' ) . '</cite>',
				$plugin_data['Author']
			);
		}
	}

	return $plugin_data;
}

/**
 * Gets a list of a plugin's files.
 *
 * @since 2.8.0
 *
 * @param string $plugin Path to the plugin file relative to the plugins directory.
 * @return string[] Array of file names relative to the plugin root.
 */
function get_plugin_files( $plugin ) {
	$plugin_file = WP_PLUGIN_DIR . '/' . $plugin;
	$dir         = dirname( $plugin_file );

	$plugin_files = array( plugin_basename( $plugin_file ) );

	if ( is_dir( $dir ) && WP_PLUGIN_DIR !== $dir ) {

		/**
		 * Filters the array of excluded directories and files while scanning the folder.
		 *
		 * @since 4.9.0
		 *
		 * @param string[] $exclusions Array of excluded directories and files.
		 */
		$exclusions = (array) apply_filters( 'plugin_files_exclusions', array( 'CVS', 'node_modules', 'vendor', 'bower_components' ) );

		$list_files = list_files( $dir, 100, $exclusions );
		$list_files = array_map( 'plugin_basename', $list_files );

		$plugin_files = array_merge( $plugin_files, $list_files );
		$plugin_files = array_values( array_unique( $plugin_files ) );
	}

	return $plugin_files;
}

/**
 * Checks the plugins directory and retrieve all plugin files with plugin data.
 *
 * WordPress only supports plugin files in the base plugins directory
 * (wp-content/plugins) and in one directory above the plugins directory
 * (wp-content/plugins/my-plugin). The file it looks for has the plugin data
 * and must be found in those two locations. It is recommended to keep your
 * plugin files in their own directories.
 *
 * The file with the plugin data is the file that will be included and therefore
 * needs to have the main execution for the plugin. This does not mean
 * everything must be contained in the file and it is recommended that the file
 * be split for maintainability. Keep everything in one file for extreme
 * optimization purposes.
 *
 * @since 1.5.0
 *
 * @param string $plugin_folder Optional. Relative path to single plugin folder.
 * @return array[] Array of arrays of plugin data, keyed by plugin file name. See get_plugin_data().
 */
function get_plugins( $plugin_folder = '' ) {

	$cache_plugins = wp_cache_get( 'plugins', 'plugins' );
	if ( ! $cache_plugins ) {
		$cache_plugins = array();
	}

	if ( isset( $cache_plugins[ $plugin_folder ] ) ) {
		return $cache_plugins[ $plugin_folder ];
	}

	$wp_plugins  = array();
	$plugin_root = WP_PLUGIN_DIR;
	if ( ! empty( $plugin_folder ) ) {
		$plugin_root .= $plugin_folder;
	}

	// Files in wp-content/plugins directory.
	$plugins_dir  = @opendir( $plugin_root );
	$plugin_files = array();

	if ( $plugins_dir ) {
		while ( ( $file = readdir( $plugins_dir ) ) !== false ) {
			if ( str_starts_with( $file, '.' ) ) {
				continue;
			}

			if ( is_dir( $plugin_root . '/' . $file ) ) {
				$plugins_subdir = @opendir( $plugin_root . '/' . $file );

				if ( $plugins_subdir ) {
					while ( ( $subfile = readdir( $plugins_subdir ) ) !== false ) {
						if ( str_starts_with( $subfile, '.' ) ) {
							continue;
						}

						if ( str_ends_with( $subfile, '.php' ) ) {
							$plugin_files[] = "$file/$subfile";
						}
					}

					closedir( $plugins_subdir );
				}
			} elseif ( str_ends_with( $file, '.php' ) ) {
				$plugin_files[] = $file;
			}
		}

		closedir( $plugins_dir );
	}

	if ( empty( $plugin_files ) ) {
		return $wp_plugins;
	}

	foreach ( $plugin_files as $plugin_file ) {
		if ( ! is_readable( "$plugin_root/$plugin_file" ) ) {
			continue;
		}

		// Do not apply markup/translate as it will be cached.
		$plugin_data = get_plugin_data( "$plugin_root/$plugin_file", false, false );

		if ( empty( $plugin_data['Name'] ) ) {
			continue;
		}

		$wp_plugins[ plugin_basename( $plugin_file ) ] = $plugin_data;
	}

	uasort( $wp_plugins, '_sort_uname_callback' );

	$cache_plugins[ $plugin_folder ] = $wp_plugins;
	wp_cache_set( 'plugins', $cache_plugins, 'plugins' );

	return $wp_plugins;
}

/**
 * Checks the mu-plugins directory and retrieve all mu-plugin files with any plugin data.
 *
 * WordPress only includes mu-plugin files in the base mu-plugins directory (wp-content/mu-plugins).
 *
 * @since 3.0.0
 * @return array[] Array of arrays of mu-plugin data, keyed by plugin file name. See get_plugin_data().
 */
function get_mu_plugins() {
	$wp_plugins   = array();
	$plugin_files = array();

	if ( ! is_dir( WPMU_PLUGIN_DIR ) ) {
		return $wp_plugins;
	}

	// Files in wp-content/mu-plugins directory.
	$plugins_dir = @opendir( WPMU_PLUGIN_DIR );
	if ( $plugins_dir ) {
		while ( ( $file = readdir( $plugins_dir ) ) !== false ) {
			if ( str_ends_with( $file, '.php' ) ) {
				$plugin_files[] = $file;
			}
		}
	} else {
		return $wp_plugins;
	}

	closedir( $plugins_dir );

	if ( empty( $plugin_files ) ) {
		return $wp_plugins;
	}

	foreach ( $plugin_files as $plugin_file ) {
		if ( ! is_readable( WPMU_PLUGIN_DIR . "/$plugin_file" ) ) {
			continue;
		}

		// Do not apply markup/translate as it will be cached.
		$plugin_data = get_plugin_data( WPMU_PLUGIN_DIR . "/$plugin_file", false, false );

		if ( empty( $plugin_data['Name'] ) ) {
			$plugin_data['Name'] = $plugin_file;
		}

		$wp_plugins[ $plugin_file ] = $plugin_data;
	}

	if ( isset( $wp_plugins['index.php'] ) && filesize( WPMU_PLUGIN_DIR . '/index.php' ) <= 30 ) {
		// Silence is golden.
		unset( $wp_plugins['index.php'] );
	}

	uasort( $wp_plugins, '_sort_uname_callback' );

	return $wp_plugins;
}

/**
 * Declares a callback to sort array by a 'Name' key.
 *
 * @since 3.1.0
 *
 * @access private
 *
 * @param array $a array with 'Name' key.
 * @param array $b array with 'Name' key.
 * @return int Return 0 or 1 based on two string comparison.
 */
function _sort_uname_callback( $a, $b ) {
	return strnatcasecmp( $a['Name'], $b['Name'] );
}

/**
 * Checks the wp-content directory and retrieve all drop-ins with any plugin data.
 *
 * @since 3.0.0
 * @return array[] Array of arrays of dropin plugin data, keyed by plugin file name. See get_plugin_data().
 */
function get_dropins() {
	$dropins      = array();
	$plugin_files = array();

	$_dropins = _get_dropins();

	// Files in wp-content directory.
	$plugins_dir = @opendir( WP_CONTENT_DIR );
	if ( $plugins_dir ) {
		while ( ( $file = readdir( $plugins_dir ) ) !== false ) {
			if ( isset( $_dropins[ $file ] ) ) {
				$plugin_files[] = $file;
			}
		}
	} else {
		return $dropins;
	}

	closedir( $plugins_dir );

	if ( empty( $plugin_files ) ) {
		return $dropins;
	}

	foreach ( $plugin_files as $plugin_file ) {
		if ( ! is_readable( WP_CONTENT_DIR . "/$plugin_file" ) ) {
			continue;
		}

		// Do not apply markup/translate as it will be cached.
		$plugin_data = get_plugin_data( WP_CONTENT_DIR . "/$plugin_file", false, false );

		if ( empty( $plugin_data['Name'] ) ) {
			$plugin_data['Name'] = $plugin_file;
		}

		$dropins[ $plugin_file ] = $plugin_data;
	}

	uksort( $dropins, 'strnatcasecmp' );

	return $dropins;
}

/**
 * Returns drop-in plugins that WordPress uses.
 *
 * Includes Multisite drop-ins only when is_multisite()
 *
 * @since 3.0.0
 *
 * @return array[] {
 *     Key is file name. The value is an array of data about the drop-in.
 *
 *     @type array ...$0 {
 *         Data about the drop-in.
 *
 *         @type string      $0 The purpose of the drop-in.
 *         @type string|true $1 Name of the constant that must be true for the drop-in
 *                              to be used, or true if no constant is required.
 *     }
 * }
 */
function _get_dropins() {
	$dropins = array(
		'advanced-cache.php'      => array( __( 'Advanced caching plugin.' ), 'WP_CACHE' ),  // WP_CACHE
		'db.php'                  => array( __( 'Custom database class.' ), true ),          // Auto on load.
		'db-error.php'            => array( __( 'Custom database error message.' ), true ),  // Auto on error.
		'install.php'             => array( __( 'Custom installation script.' ), true ),     // Auto on installation.
		'maintenance.php'         => array( __( 'Custom maintenance message.' ), true ),     // Auto on maintenance.
		'object-cache.php'        => array( __( 'External object cache.' ), true ),          // Auto on load.
		'php-error.php'           => array( __( 'Custom PHP error message.' ), true ),       // Auto on error.
		'fatal-error-handler.php' => array( __( 'Custom PHP fatal error handler.' ), true ), // Auto on error.
	);

	if ( is_multisite() ) {
		$dropins['sunrise.php']        = array( __( 'Executed before Multisite is loaded.' ), 'SUNRISE' ); // SUNRISE
		$dropins['blog-deleted.php']   = array( __( 'Custom site deleted message.' ), true );   // Auto on deleted blog.
		$dropins['blog-inactive.php']  = array( __( 'Custom site inactive message.' ), true );  // Auto on inactive blog.
		$dropins['blog-suspended.php'] = array( __( 'Custom site suspended message.' ), true ); // Auto on archived or spammed blog.
	}

	return $dropins;
}

/**
 * Determines whether a plugin is active.
 *
 * Only plugins installed in the plugins/ folder can be active.
 *
 * Plugins in the mu-plugins/ folder can't be "activated," so this function will
 * return false for those plugins.
 *
 * For more information on this and similar theme functions, check out
 * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/
 * Conditional Tags} article in the Theme Developer Handbook.
 *
 * @since 2.5.0
 *
 * @param string $plugin Path to the plugin file relative to the plugins directory.
 * @return bool True, if in the active plugins list. False, not in the list.
 */
function is_plugin_active( $plugin ) {
	return in_array( $plugin, (array) get_option( 'active_plugins', array() ), true ) || is_plugin_active_for_network( $plugin );
}

/**
 * Determines whether the plugin is inactive.
 *
 * Reverse of is_plugin_active(). Used as a callback.
 *
 * For more information on this and similar theme functions, check out
 * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/
 * Conditional Tags} article in the Theme Developer Handbook.
 *
 * @since 3.1.0
 *
 * @see is_plugin_active()
 *
 * @param string $plugin Path to the plugin file relative to the plugins directory.
 * @return bool True if inactive. False if active.
 */
function is_plugin_inactive( $plugin ) {
	return ! is_plugin_active( $plugin );
}

/**
 * Determines whether the plugin is active for the entire network.
 *
 * Only plugins installed in the plugins/ folder can be active.
 *
 * Plugins in the mu-plugins/ folder can't be "activated," so this function will
 * return false for those plugins.
 *
 * For more information on this and similar theme functions, check out
 * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/
 * Conditional Tags} article in the Theme Developer Handbook.
 *
 * @since 3.0.0
 *
 * @param string $plugin Path to the plugin file relative to the plugins directory.
 * @return bool True if active for the network, otherwise false.
 */
function is_plugin_active_for_network( $plugin ) {
	if ( ! is_multisite() ) {
		return false;
	}

	$plugins = get_site_option( 'active_sitewide_plugins' );
	if ( isset( $plugins[ $plugin ] ) ) {
		return true;
	}

	return false;
}

/**
 * Checks for "Network: true" in the plugin header to see if this should
 * be activated only as a network wide plugin. The plugin would also work
 * when Multisite is not enabled.
 *
 * Checks for "Site Wide Only: true" for backward compatibility.
 *
 * @since 3.0.0
 *
 * @param string $plugin Path to the plugin file relative to the plugins directory.
 * @return bool True if plugin is network only, false otherwise.
 */
function is_network_only_plugin( $plugin ) {
	$plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin );
	if ( $plugin_data ) {
		return $plugin_data['Network'];
	}
	return false;
}

/**
 * Attempts activation of plugin in a "sandbox" and redirects on success.
 *
 * A plugin that is already activated will not attempt to be activated again.
 *
 * The way it works is by setting the redirection to the error before trying to
 * include the plugin file. If the plugin fails, then the redirection will not
 * be overwritten with the success message. Also, the options will not be
 * updated and the activation hook will not be called on plugin error.
 *
 * It should be noted that in no way the below code will actually prevent errors
 * within the file. The code should not be used elsewhere to replicate the
 * "sandbox", which uses redirection to work.
 * {@source 13 1}
 *
 * If any errors are found or text is outputted, then it will be captured to
 * ensure that the success redirection will update the error redirection.
 *
 * @since 2.5.0
 * @since 5.2.0 Test for WordPress version and PHP version compatibility.
 *
 * @param string $plugin       Path to the plugin file relative to the plugins directory.
 * @param string $redirect     Optional. URL to redirect to.
 * @param bool   $network_wide Optional. Whether to enable the plugin for all sites in the network
 *                             or just the current site. Multisite only. Default false.
 * @param bool   $silent       Optional. Whether to prevent calling activation hooks. Default false.
 * @return null|WP_Error Null on success, WP_Error on invalid file.
 */
function activate_plugin( $plugin, $redirect = '', $network_wide = false, $silent = false ) {
	$plugin = plugin_basename( trim( $plugin ) );

	if ( is_multisite() && ( $network_wide || is_network_only_plugin( $plugin ) ) ) {
		$network_wide        = true;
		$current             = get_site_option( 'active_sitewide_plugins', array() );
		$_GET['networkwide'] = 1; // Back compat for plugins looking for this value.
	} else {
		$current = get_option( 'active_plugins', array() );
	}

	$valid = validate_plugin( $plugin );
	if ( is_wp_error( $valid ) ) {
		return $valid;
	}

	$requirements = validate_plugin_requirements( $plugin );
	if ( is_wp_error( $requirements ) ) {
		return $requirements;
	}

	if ( $network_wide && ! isset( $current[ $plugin ] )
		|| ! $network_wide && ! in_array( $plugin, $current, true )
	) {
		if ( ! empty( $redirect ) ) {
			// We'll override this later if the plugin can be included without fatal error.
			wp_redirect( add_query_arg( '_error_nonce', wp_create_nonce( 'plugin-activation-error_' . $plugin ), $redirect ) );
		}

		ob_start();

		// Load the plugin to test whether it throws any errors.
		plugin_sandbox_scrape( $plugin );

		if ( ! $silent ) {
			/**
			 * Fires before a plugin is activated.
			 *
			 * If a plugin is silently activated (such as during an update),
			 * this hook does not fire.
			 *
			 * @since 2.9.0
			 *
			 * @param string $plugin       Path to the plugin file relative to the plugins directory.
			 * @param bool   $network_wide Whether to enable the plugin for all sites in the network
			 *                             or just the current site. Multisite only. Default false.
			 */
			do_action( 'activate_plugin', $plugin, $network_wide );

			/**
			 * Fires as a specific plugin is being activated.
			 *
			 * This hook is the "activation" hook used internally by register_activation_hook().
			 * The dynamic portion of the hook name, `$plugin`, refers to the plugin basename.
			 *
			 * If a plugin is silently activated (such as during an update), this hook does not fire.
			 *
			 * @since 2.0.0
			 *
			 * @param bool $network_wide Whether to enable the plugin for all sites in the network
			 *                           or just the current site. Multisite only. Default false.
			 */
			do_action( "activate_{$plugin}", $network_wide );
		}

		if ( $network_wide ) {
			$current            = get_site_option( 'active_sitewide_plugins', array() );
			$current[ $plugin ] = time();
			update_site_option( 'active_sitewide_plugins', $current );
		} else {
			$current   = get_option( 'active_plugins', array() );
			$current[] = $plugin;
			sort( $current );
			update_option( 'active_plugins', $current );
		}

		if ( ! $silent ) {
			/**
			 * Fires after a plugin has been activated.
			 *
			 * If a plugin is silently activated (such as during an update),
			 * this hook does not fire.
			 *
			 * @since 2.9.0
			 *
			 * @param string $plugin       Path to the plugin file relative to the plugins directory.
			 * @param bool   $network_wide Whether to enable the plugin for all sites in the network
			 *                             or just the current site. Multisite only. Default false.
			 */
			do_action( 'activated_plugin', $plugin, $network_wide );
		}

		if ( ob_get_length() > 0 ) {
			$output = ob_get_clean();
			return new WP_Error( 'unexpected_output', __( 'The plugin generated unexpected output.' ), $output );
		}

		ob_end_clean();
	}

	return null;
}

/**
 * Deactivates a single plugin or multiple plugins.
 *
 * The deactivation hook is disabled by the plugin upgrader by using the $silent
 * parameter.
 *
 * @since 2.5.0
 *
 * @param string|string[] $plugins      Single plugin or list of plugins to deactivate.
 * @param bool            $silent       Prevent calling deactivation hooks. Default false.
 * @param bool|null       $network_wide Whether to deactivate the plugin for all sites in the network.
 *                                      A value of null will deactivate plugins for both the network
 *                                      and the current site. Multisite only. Default null.
 */
function deactivate_plugins( $plugins, $silent = false, $network_wide = null ) {
	if ( is_multisite() ) {
		$network_current = get_site_option( 'active_sitewide_plugins', array() );
	}
	$current    = get_option( 'active_plugins', array() );
	$do_blog    = false;
	$do_network = false;

	foreach ( (array) $plugins as $plugin ) {
		$plugin = plugin_basename( trim( $plugin ) );
		if ( ! is_plugin_active( $plugin ) ) {
			continue;
		}

		$network_deactivating = ( false !== $network_wide ) && is_plugin_active_for_network( $plugin );

		if ( ! $silent ) {
			/**
			 * Fires before a plugin is deactivated.
			 *
			 * If a plugin is silently deactivated (such as during an update),
			 * this hook does not fire.
			 *
			 * @since 2.9.0
			 *
			 * @param string $plugin               Path to the plugin file relative to the plugins directory.
			 * @param bool   $network_deactivating Whether the plugin is deactivated for all sites in the network
			 *                                     or just the current site. Multisite only. Default false.
			 */
			do_action( 'deactivate_plugin', $plugin, $network_deactivating );
		}

		if ( false !== $network_wide ) {
			if ( is_plugin_active_for_network( $plugin ) ) {
				$do_network = true;
				unset( $network_current[ $plugin ] );
			} elseif ( $network_wide ) {
				continue;
			}
		}

		if ( true !== $network_wide ) {
			$key = array_search( $plugin, $current, true );
			if ( false !== $key ) {
				$do_blog = true;
				unset( $current[ $key ] );
			}
		}

		if ( $do_blog && wp_is_recovery_mode() ) {
			list( $extension ) = explode( '/', $plugin );
			wp_paused_plugins()->delete( $extension );
		}

		if ( ! $silent ) {
			/**
			 * Fires as a specific plugin is being deactivated.
			 *
			 * This hook is the "deactivation" hook used internally by register_deactivation_hook().
			 * The dynamic portion of the hook name, `$plugin`, refers to the plugin basename.
			 *
			 * If a plugin is silently deactivated (such as during an update), this hook does not fire.
			 *
			 * @since 2.0.0
			 *
			 * @param bool $network_deactivating Whether the plugin is deactivated for all sites in the network
			 *                                   or just the current site. Multisite only. Default false.
			 */
			do_action( "deactivate_{$plugin}", $network_deactivating );

			/**
			 * Fires after a plugin is deactivated.
			 *
			 * If a plugin is silently deactivated (such as during an update),
			 * this hook does not fire.
			 *
			 * @since 2.9.0
			 *
			 * @param string $plugin               Path to the plugin file relative to the plugins directory.
			 * @param bool   $network_deactivating Whether the plugin is deactivated for all sites in the network
			 *                                     or just the current site. Multisite only. Default false.
			 */
			do_action( 'deactivated_plugin', $plugin, $network_deactivating );
		}
	}

	if ( $do_blog ) {
		update_option( 'active_plugins', $current );
	}
	if ( $do_network ) {
		update_site_option( 'active_sitewide_plugins', $network_current );
	}
}

/**
 * Activates multiple plugins.
 *
 * When WP_Error is returned, it does not mean that one of the plugins had
 * errors. It means that one or more of the plugin file paths were invalid.
 *
 * The execution will be halted as soon as one of the plugins has an error.
 *
 * @since 2.6.0
 *
 * @param string|string[] $plugins      Single plugin or list of plugins to activate.
 * @param string          $redirect     Redirect to page after successful activation.
 * @param bool            $network_wide Whether to enable the plugin for all sites in the network.
 *                                      Default false.
 * @param bool            $silent       Prevent calling activation hooks. Default false.
 * @return true|WP_Error True when finished or WP_Error if there were errors during a plugin activation.
 */
function activate_plugins( $plugins, $redirect = '', $network_wide = false, $silent = false ) {
	if ( ! is_array( $plugins ) ) {
		$plugins = array( $plugins );
	}

	$errors = array();
	foreach ( $plugins as $plugin ) {
		if ( ! empty( $redirect ) ) {
			$redirect = add_query_arg( 'plugin', $plugin, $redirect );
		}
		$result = activate_plugin( $plugin, $redirect, $network_wide, $silent );
		if ( is_wp_error( $result ) ) {
			$errors[ $plugin ] = $result;
		}
	}

	if ( ! empty( $errors ) ) {
		return new WP_Error( 'plugins_invalid', __( 'One of the plugins is invalid.' ), $errors );
	}

	return true;
}

/**
 * Removes directory and files of a plugin for a list of plugins.
 *
 * @since 2.6.0
 *
 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
 *
 * @param string[] $plugins    List of plugin paths to delete, relative to the plugins directory.
 * @param string   $deprecated Not used.
 * @return bool|null|WP_Error True on success, false if `$plugins` is empty, `WP_Error` on failure.
 *                            `null` if filesystem credentials are required to proceed.
 */
function delete_plugins( $plugins, $deprecated = '' ) {
	global $wp_filesystem;

	if ( empty( $plugins ) ) {
		return false;
	}

	$checked = array();
	foreach ( $plugins as $plugin ) {
		$checked[] = 'checked[]=' . $plugin;
	}

	$url = wp_nonce_url( 'plugins.php?action=delete-selected&verify-delete=1&' . implode( '&', $checked ), 'bulk-plugins' );

	ob_start();
	$credentials = request_filesystem_credentials( $url );
	$data        = ob_get_clean();

	if ( false === $credentials ) {
		if ( ! empty( $data ) ) {
			require_once ABSPATH . 'wp-admin/admin-header.php';
			echo $data;
			require_once ABSPATH . 'wp-admin/admin-footer.php';
			exit;
		}
		return;
	}

	if ( ! WP_Filesystem( $credentials ) ) {
		ob_start();
		// Failed to connect. Error and request again.
		request_filesystem_credentials( $url, '', true );
		$data = ob_get_clean();

		if ( ! empty( $data ) ) {
			require_once ABSPATH . 'wp-admin/admin-header.php';
			echo $data;
			require_once ABSPATH . 'wp-admin/admin-footer.php';
			exit;
		}
		return;
	}

	if ( ! is_object( $wp_filesystem ) ) {
		return new WP_Error( 'fs_unavailable', __( 'Could not access filesystem.' ) );
	}

	if ( is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
		return new WP_Error( 'fs_error', __( 'Filesystem error.' ), $wp_filesystem->errors );
	}

	// Get the base plugin folder.
	$plugins_dir = $wp_filesystem->wp_plugins_dir();
	if ( empty( $plugins_dir ) ) {
		return new WP_Error( 'fs_no_plugins_dir', __( 'Unable to locate WordPress plugin directory.' ) );
	}

	$plugins_dir = trailingslashit( $plugins_dir );

	$plugin_translations = wp_get_installed_translations( 'plugins' );

	$errors = array();

	foreach ( $plugins as $plugin_file ) {
		// Run Uninstall hook.
		if ( is_uninstallable_plugin( $plugin_file ) ) {
			uninstall_plugin( $plugin_file );
		}

		/**
		 * Fires immediately before a plugin deletion attempt.
		 *
		 * @since 4.4.0
		 *
		 * @param string $plugin_file Path to the plugin file relative to the plugins directory.
		 */
		do_action( 'delete_plugin', $plugin_file );

		$this_plugin_dir = trailingslashit( dirname( $plugins_dir . $plugin_file ) );

		/*
		 * If plugin is in its own directory, recursively delete the directory.
		 * Base check on if plugin includes directory separator AND that it's not the root plugin folder.
		 */
		if ( strpos( $plugin_file, '/' ) && $this_plugin_dir !== $plugins_dir ) {
			$deleted = $wp_filesystem->delete( $this_plugin_dir, true );
		} else {
			$deleted = $wp_filesystem->delete( $plugins_dir . $plugin_file );
		}

		/**
		 * Fires immediately after a plugin deletion attempt.
		 *
		 * @since 4.4.0
		 *
		 * @param string $plugin_file Path to the plugin file relative to the plugins directory.
		 * @param bool   $deleted     Whether the plugin deletion was successful.
		 */
		do_action( 'deleted_plugin', $plugin_file, $deleted );

		if ( ! $deleted ) {
			$errors[] = $plugin_file;
			continue;
		}

		$plugin_slug = dirname( $plugin_file );

		if ( 'hello.php' === $plugin_file ) {
			$plugin_slug = 'hello-dolly';
		}

		// Remove language files, silently.
		if ( '.' !== $plugin_slug && ! empty( $plugin_translations[ $plugin_slug ] ) ) {
			$translations = $plugin_translations[ $plugin_slug ];

			foreach ( $translations as $translation => $data ) {
				$wp_filesystem->delete( WP_LANG_DIR . '/plugins/' . $plugin_slug . '-' . $translation . '.po' );
				$wp_filesystem->delete( WP_LANG_DIR . '/plugins/' . $plugin_slug . '-' . $translation . '.mo' );
				$wp_filesystem->delete( WP_LANG_DIR . '/plugins/' . $plugin_slug . '-' . $translation . '.l10n.php' );

				$json_translation_files = glob( WP_LANG_DIR . '/plugins/' . $plugin_slug . '-' . $translation . '-*.json' );
				if ( $json_translation_files ) {
					array_map( array( $wp_filesystem, 'delete' ), $json_translation_files );
				}
			}
		}
	}

	// Remove deleted plugins from the plugin updates list.
	$current = get_site_transient( 'update_plugins' );
	if ( $current ) {
		// Don't remove the plugins that weren't deleted.
		$deleted = array_diff( $plugins, $errors );

		foreach ( $deleted as $plugin_file ) {
			unset( $current->response[ $plugin_file ] );
		}

		set_site_transient( 'update_plugins', $current );
	}

	if ( ! empty( $errors ) ) {
		if ( 1 === count( $errors ) ) {
			/* translators: %s: Plugin filename. */
			$message = __( 'Could not fully remove the plugin %s.' );
		} else {
			/* translators: %s: Comma-separated list of plugin filenames. */
			$message = __( 'Could not fully remove the plugins %s.' );
		}

		return new WP_Error( 'could_not_remove_plugin', sprintf( $message, implode( ', ', $errors ) ) );
	}

	return true;
}

/**
 * Validates active plugins.
 *
 * Validate all active plugins, deactivates invalid and
 * returns an array of deactivated ones.
 *
 * @since 2.5.0
 * @return WP_Error[] Array of plugin errors keyed by plugin file name.
 */
function validate_active_plugins() {
	$plugins = get_option( 'active_plugins', array() );
	// Validate vartype: array.
	if ( ! is_array( $plugins ) ) {
		update_option( 'active_plugins', array() );
		$plugins = array();
	}

	if ( is_multisite() && current_user_can( 'manage_network_plugins' ) ) {
		$network_plugins = (array) get_site_option( 'active_sitewide_plugins', array() );
		$plugins         = array_merge( $plugins, array_keys( $network_plugins ) );
	}

	if ( empty( $plugins ) ) {
		return array();
	}

	$invalid = array();

	// Invalid plugins get deactivated.
	foreach ( $plugins as $plugin ) {
		$result = validate_plugin( $plugin );
		if ( is_wp_error( $result ) ) {
			$invalid[ $plugin ] = $result;
			deactivate_plugins( $plugin, true );
		}
	}
	return $invalid;
}

/**
 * Validates the plugin path.
 *
 * Checks that the main plugin file exists and is a valid plugin. See validate_file().
 *
 * @since 2.5.0
 *
 * @param string $plugin Path to the plugin file relative to the plugins directory.
 * @return int|WP_Error 0 on success, WP_Error on failure.
 */
function validate_plugin( $plugin ) {
	if ( validate_file( $plugin ) ) {
		return new WP_Error( 'plugin_invalid', __( 'Invalid plugin path.' ) );
	}
	if ( ! file_exists( WP_PLUGIN_DIR . '/' . $plugin ) ) {
		return new WP_Error( 'plugin_not_found', __( 'Plugin file does not exist.' ) );
	}

	$installed_plugins = get_plugins();
	if ( ! isset( $installed_plugins[ $plugin ] ) ) {
		return new WP_Error( 'no_plugin_header', __( 'The plugin does not have a valid header.' ) );
	}
	return 0;
}

/**
 * Validates the plugin requirements for WordPress version and PHP version.
 *
 * Uses the information from `Requires at least`, `Requires PHP` and `Requires Plugins` headers
 * defined in the plugin's main PHP file.
 *
 * @since 5.2.0
 * @since 5.3.0 Added support for reading the headers from the plugin's
 *              main PHP file, with `readme.txt` as a fallback.
 * @since 5.8.0 Removed support for using `readme.txt` as a fallback.
 * @since 6.5.0 Added support for the 'Requires Plugins' header.
 *
 * @param string $plugin Path to the plugin file relative to the plugins directory.
 * @return true|WP_Error True if requirements are met, WP_Error on failure.
 */
function validate_plugin_requirements( $plugin ) {
	$plugin_headers = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin );

	$requirements = array(
		'requires'         => ! empty( $plugin_headers['RequiresWP'] ) ? $plugin_headers['RequiresWP'] : '',
		'requires_php'     => ! empty( $plugin_headers['RequiresPHP'] ) ? $plugin_headers['RequiresPHP'] : '',
		'requires_plugins' => ! empty( $plugin_headers['RequiresPlugins'] ) ? $plugin_headers['RequiresPlugins'] : '',
	);

	$compatible_wp  = is_wp_version_compatible( $requirements['requires'] );
	$compatible_php = is_php_version_compatible( $requirements['requires_php'] );

	$php_update_message = '</p><p>' . sprintf(
		/* translators: %s: URL to Update PHP page. */
		__( '<a href="%s">Learn more about updating PHP</a>.' ),
		esc_url( wp_get_update_php_url() )
	);

	$annotation = wp_get_update_php_annotation();

	if ( $annotation ) {
		$php_update_message .= '</p><p><em>' . $annotation . '</em>';
	}

	if ( ! $compatible_wp && ! $compatible_php ) {
		return new WP_Error(
			'plugin_wp_php_incompatible',
			'<p>' . sprintf(
				/* translators: 1: Current WordPress version, 2: Current PHP version, 3: Plugin name, 4: Required WordPress version, 5: Required PHP version. */
				_x( '<strong>Error:</strong> Current versions of WordPress (%1$s) and PHP (%2$s) do not meet minimum requirements for %3$s. The plugin requires WordPress %4$s and PHP %5$s.', 'plugin' ),
				get_bloginfo( 'version' ),
				PHP_VERSION,
				$plugin_headers['Name'],
				$requirements['requires'],
				$requirements['requires_php']
			) . $php_update_message . '</p>'
		);
	} elseif ( ! $compatible_php ) {
		return new WP_Error(
			'plugin_php_incompatible',
			'<p>' . sprintf(
				/* translators: 1: Current PHP version, 2: Plugin name, 3: Required PHP version. */
				_x( '<strong>Error:</strong> Current PHP version (%1$s) does not meet minimum requirements for %2$s. The plugin requires PHP %3$s.', 'plugin' ),
				PHP_VERSION,
				$plugin_headers['Name'],
				$requirements['requires_php']
			) . $php_update_message . '</p>'
		);
	} elseif ( ! $compatible_wp ) {
		return new WP_Error(
			'plugin_wp_incompatible',
			'<p>' . sprintf(
				/* translators: 1: Current WordPress version, 2: Plugin name, 3: Required WordPress version. */
				_x( '<strong>Error:</strong> Current WordPress version (%1$s) does not meet minimum requirements for %2$s. The plugin requires WordPress %3$s.', 'plugin' ),
				get_bloginfo( 'version' ),
				$plugin_headers['Name'],
				$requirements['requires']
			) . '</p>'
		);
	}

	WP_Plugin_Dependencies::initialize();

	if ( WP_Plugin_Dependencies::has_unmet_dependencies( $plugin ) ) {
		$dependency_names       = WP_Plugin_Dependencies::get_dependency_names( $plugin );
		$unmet_dependencies     = array();
		$unmet_dependency_names = array();

		foreach ( $dependency_names as $dependency => $dependency_name ) {
			$dependency_file = WP_Plugin_Dependencies::get_dependency_filepath( $dependency );

			if ( false === $dependency_file ) {
				$unmet_dependencies['not_installed'][ $dependency ] = $dependency_name;
				$unmet_dependency_names[]                           = $dependency_name;
			} elseif ( is_plugin_inactive( $dependency_file ) ) {
				$unmet_dependencies['inactive'][ $dependency ] = $dependency_name;
				$unmet_dependency_names[]                      = $dependency_name;
			}
		}

		$error_message = sprintf(
			/* translators: 1: Plugin name, 2: Number of plugins, 3: A comma-separated list of plugin names. */
			_n(
				'<strong>Error:</strong> %1$s requires %2$d plugin to be installed and activated: %3$s.',
				'<strong>Error:</strong> %1$s requires %2$d plugins to be installed and activated: %3$s.',
				count( $unmet_dependency_names )
			),
			$plugin_headers['Name'],
			count( $unmet_dependency_names ),
			implode( wp_get_list_item_separator(), $unmet_dependency_names )
		);

		if ( is_multisite() ) {
			if ( current_user_can( 'manage_network_plugins' ) ) {
				$error_message .= ' ' . sprintf(
					/* translators: %s: Link to the plugins page. */
					__( '<a href="%s">Manage plugins</a>.' ),
					esc_url( network_admin_url( 'plugins.php' ) )
				);
			} else {
				$error_message .= ' ' . __( 'Please contact your network administrator.' );
			}
		} else {
			$error_message .= ' ' . sprintf(
				/* translators: %s: Link to the plugins page. */
				__( '<a href="%s">Manage plugins</a>.' ),
				esc_url( admin_url( 'plugins.php' ) )
			);
		}

		return new WP_Error(
			'plugin_missing_dependencies',
			"<p>{$error_message}</p>",
			$unmet_dependencies
		);
	}

	return true;
}

/**
 * Determines whether the plugin can be uninstalled.
 *
 * @since 2.7.0
 *
 * @param string $plugin Path to the plugin file relative to the plugins directory.
 * @return bool Whether plugin can be uninstalled.
 */
function is_uninstallable_plugin( $plugin ) {
	$file = plugin_basename( $plugin );

	$uninstallable_plugins = (array) get_option( 'uninstall_plugins' );
	if ( isset( $uninstallable_plugins[ $file ] ) || file_exists( WP_PLUGIN_DIR . '/' . dirname( $file ) . '/uninstall.php' ) ) {
		return true;
	}

	return false;
}

/**
 * Uninstalls a single plugin.
 *
 * Calls the uninstall hook, if it is available.
 *
 * @since 2.7.0
 *
 * @param string $plugin Path to the plugin file relative to the plugins directory.
 * @return true|void True if a plugin's uninstall.php file has been found and included.
 *                   Void otherwise.
 */
function uninstall_plugin( $plugin ) {
	$file = plugin_basename( $plugin );

	$uninstallable_plugins = (array) get_option( 'uninstall_plugins' );

	/**
	 * Fires in uninstall_plugin() immediately before the plugin is uninstalled.
	 *
	 * @since 4.5.0
	 *
	 * @param string $plugin                Path to the plugin file relative to the plugins directory.
	 * @param array  $uninstallable_plugins Uninstallable plugins.
	 */
	do_action( 'pre_uninstall_plugin', $plugin, $uninstallable_plugins );

	if ( file_exists( WP_PLUGIN_DIR . '/' . dirname( $file ) . '/uninstall.php' ) ) {
		if ( isset( $uninstallable_plugins[ $file ] ) ) {
			unset( $uninstallable_plugins[ $file ] );
			update_option( 'uninstall_plugins', $uninstallable_plugins );
		}
		unset( $uninstallable_plugins );

		define( 'WP_UNINSTALL_PLUGIN', $file );

		wp_register_plugin_realpath( WP_PLUGIN_DIR . '/' . $file );
		include_once WP_PLUGIN_DIR . '/' . dirname( $file ) . '/uninstall.php';

		return true;
	}

	if ( isset( $uninstallable_plugins[ $file ] ) ) {
		$callable = $uninstallable_plugins[ $file ];
		unset( $uninstallable_plugins[ $file ] );
		update_option( 'uninstall_plugins', $uninstallable_plugins );
		unset( $uninstallable_plugins );

		wp_register_plugin_realpath( WP_PLUGIN_DIR . '/' . $file );
		include_once WP_PLUGIN_DIR . '/' . $file;

		add_action( "uninstall_{$file}", $callable );

		/**
		 * Fires in uninstall_plugin() once the plugin has been uninstalled.
		 *
		 * The action concatenates the 'uninstall_' prefix with the basename of the
		 * plugin passed to uninstall_plugin() to create a dynamically-named action.
		 *
		 * @since 2.7.0
		 */
		do_action( "uninstall_{$file}" );
	}
}

//
// Menu.
//

/**
 * Adds a top-level menu page.
 *
 * This function takes a capability which will be used to determine whether
 * or not a page is included in the menu.
 *
 * The function which is hooked in to handle the output of the page must check
 * that the user has the required capability as well.
 *
 * @since 1.5.0
 *
 * @global array $menu
 * @global array $admin_page_hooks
 * @global array $_registered_pages
 * @global array $_parent_pages
 *
 * @param string    $page_title The text to be displayed in the title tags of the page when the menu is selected.
 * @param string    $menu_title The text to be used for the menu.
 * @param string    $capability The capability required for this menu to be displayed to the user.
 * @param string    $menu_slug  The slug name to refer to this menu by. Should be unique for this menu page and only
 *                              include lowercase alphanumeric, dashes, and underscores characters to be compatible
 *                              with sanitize_key().
 * @param callable  $callback   Optional. The function to be called to output the content for this page.
 * @param string    $icon_url   Optional. The URL to the icon to be used for this menu.
 *                              * Pass a base64-encoded SVG using a data URI, which will be colored to match
 *                                the color scheme. This should begin with 'data:image/svg+xml;base64,'.
 *                              * Pass the name of a Dashicons helper class to use a font icon,
 *                                e.g. 'dashicons-chart-pie'.
 *                              * Pass 'none' to leave div.wp-menu-image empty so an icon can be added via CSS.
 * @param int|float $position   Optional. The position in the menu order this item should appear.
 * @return string The resulting page's hook_suffix.
 */
function add_menu_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $icon_url = '', $position = null ) {
	global $menu, $admin_page_hooks, $_registered_pages, $_parent_pages;

	$menu_slug = plugin_basename( $menu_slug );

	$admin_page_hooks[ $menu_slug ] = sanitize_title( $menu_title );

	$hookname = get_plugin_page_hookname( $menu_slug, '' );

	if ( ! empty( $callback ) && ! empty( $hookname ) && current_user_can( $capability ) ) {
		add_action( $hookname, $callback );
	}

	if ( empty( $icon_url ) ) {
		$icon_url   = 'dashicons-admin-generic';
		$icon_class = 'menu-icon-generic ';
	} else {
		$icon_url   = set_url_scheme( $icon_url );
		$icon_class = '';
	}

	$new_menu = array( $menu_title, $capability, $menu_slug, $page_title, 'menu-top ' . $icon_class . $hookname, $hookname, $icon_url );

	if ( null !== $position && ! is_numeric( $position ) ) {
		_doing_it_wrong(
			__FUNCTION__,
			sprintf(
				/* translators: %s: add_menu_page() */
				__( 'The seventh parameter passed to %s should be numeric representing menu position.' ),
				'<code>add_menu_page()</code>'
			),
			'6.0.0'
		);
		$position = null;
	}

	if ( null === $position || ! is_numeric( $position ) ) {
		$menu[] = $new_menu;
	} elseif ( isset( $menu[ (string) $position ] ) ) {
		$collision_avoider = base_convert( substr( md5( $menu_slug . $menu_title ), -4 ), 16, 10 ) * 0.00001;
		$position          = (string) ( $position + $collision_avoider );
		$menu[ $position ] = $new_menu;
	} else {
		/*
		 * Cast menu position to a string.
		 *
		 * This allows for floats to be passed as the position. PHP will normally cast a float to an
		 * integer value, this ensures the float retains its mantissa (positive fractional part).
		 *
		 * A string containing an integer value, eg "10", is treated as a numeric index.
		 */
		$position          = (string) $position;
		$menu[ $position ] = $new_menu;
	}

	$_registered_pages[ $hookname ] = true;

	// No parent as top level.
	$_parent_pages[ $menu_slug ] = false;

	return $hookname;
}

/**
 * Adds a submenu page.
 *
 * This function takes a capability which will be used to determine whether
 * or not a page is included in the menu.
 *
 * The function which is hooked in to handle the output of the page must check
 * that the user has the required capability as well.
 *
 * @since 1.5.0
 * @since 5.3.0 Added the `$position` parameter.
 *
 * @global array $submenu
 * @global array $menu
 * @global array $_wp_real_parent_file
 * @global bool  $_wp_submenu_nopriv
 * @global array $_registered_pages
 * @global array $_parent_pages
 *
 * @param string    $parent_slug The slug name for the parent menu (or the file name of a standard
 *                               WordPress admin page).
 * @param string    $page_title  The text to be displayed in the title tags of the page when the menu
 *                               is selected.
 * @param string    $menu_title  The text to be used for the menu.
 * @param string    $capability  The capability required for this menu to be displayed to the user.
 * @param string    $menu_slug   The slug name to refer to this menu by. Should be unique for this menu
 *                               and only include lowercase alphanumeric, dashes, and underscores characters
 *                               to be compatible with sanitize_key().
 * @param callable  $callback    Optional. The function to be called to output the content for this page.
 * @param int|float $position    Optional. The position in the menu order this item should appear.
 * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required.
 */
function add_submenu_page( $parent_slug, $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) {
	global $submenu, $menu, $_wp_real_parent_file, $_wp_submenu_nopriv,
		$_registered_pages, $_parent_pages;

	$menu_slug   = plugin_basename( $menu_slug );
	$parent_slug = plugin_basename( $parent_slug );

	if ( isset( $_wp_real_parent_file[ $parent_slug ] ) ) {
		$parent_slug = $_wp_real_parent_file[ $parent_slug ];
	}

	if ( ! current_user_can( $capability ) ) {
		$_wp_submenu_nopriv[ $parent_slug ][ $menu_slug ] = true;
		return false;
	}

	/*
	 * If the parent doesn't already have a submenu, add a link to the parent
	 * as the first item in the submenu. If the submenu file is the same as the
	 * parent file someone is trying to link back to the parent manually. In
	 * this case, don't automatically add a link back to avoid duplication.
	 */
	if ( ! isset( $submenu[ $parent_slug ] ) && $menu_slug !== $parent_slug ) {
		foreach ( (array) $menu as $parent_menu ) {
			if ( $parent_menu[2] === $parent_slug && current_user_can( $parent_menu[1] ) ) {
				$submenu[ $parent_slug ][] = array_slice( $parent_menu, 0, 4 );
			}
		}
	}

	$new_sub_menu = array( $menu_title, $capability, $menu_slug, $page_title );

	if ( null !== $position && ! is_numeric( $position ) ) {
		_doing_it_wrong(
			__FUNCTION__,
			sprintf(
				/* translators: %s: add_submenu_page() */
				__( 'The seventh parameter passed to %s should be numeric representing menu position.' ),
				'<code>add_submenu_page()</code>'
			),
			'5.3.0'
		);
		$position = null;
	}

	if (
		null === $position ||
		( ! isset( $submenu[ $parent_slug ] ) || $position >= count( $submenu[ $parent_slug ] ) )
	) {
		$submenu[ $parent_slug ][] = $new_sub_menu;
	} else {
		// Test for a negative position.
		$position = max( $position, 0 );
		if ( 0 === $position ) {
			// For negative or `0` positions, prepend the submenu.
			array_unshift( $submenu[ $parent_slug ], $new_sub_menu );
		} else {
			$position = absint( $position );
			// Grab all of the items before the insertion point.
			$before_items = array_slice( $submenu[ $parent_slug ], 0, $position, true );
			// Grab all of the items after the insertion point.
			$after_items = array_slice( $submenu[ $parent_slug ], $position, null, true );
			// Add the new item.
			$before_items[] = $new_sub_menu;
			// Merge the items.
			$submenu[ $parent_slug ] = array_merge( $before_items, $after_items );
		}
	}

	// Sort the parent array.
	ksort( $submenu[ $parent_slug ] );

	$hookname = get_plugin_page_hookname( $menu_slug, $parent_slug );
	if ( ! empty( $callback ) && ! empty( $hookname ) ) {
		add_action( $hookname, $callback );
	}

	$_registered_pages[ $hookname ] = true;

	/*
	 * Backward-compatibility for plugins using add_management_page().
	 * See wp-admin/admin.php for redirect from edit.php to tools.php.
	 */
	if ( 'tools.php' === $parent_slug ) {
		$_registered_pages[ get_plugin_page_hookname( $menu_slug, 'edit.php' ) ] = true;
	}

	// No parent as top level.
	$_parent_pages[ $menu_slug ] = $parent_slug;

	return $hookname;
}

/**
 * Adds a submenu page to the Tools main menu.
 *
 * This function takes a capability which will be used to determine whether
 * or not a page is included in the menu.
 *
 * The function which is hooked in to handle the output of the page must check
 * that the user has the required capability as well.
 *
 * @since 1.5.0
 * @since 5.3.0 Added the `$position` parameter.
 *
 * @param string   $page_title The text to be displayed in the title tags of the page when the menu is selected.
 * @param string   $menu_title The text to be used for the menu.
 * @param string   $capability The capability required for this menu to be displayed to the user.
 * @param string   $menu_slug  The slug name to refer to this menu by (should be unique for this menu).
 * @param callable $callback   Optional. The function to be called to output the content for this page.
 * @param int      $position   Optional. The position in the menu order this item should appear.
 * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required.
 */
function add_management_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) {
	return add_submenu_page( 'tools.php', $page_title, $menu_title, $capability, $menu_slug, $callback, $position );
}

/**
 * Adds a submenu page to the Settings main menu.
 *
 * This function takes a capability which will be used to determine whether
 * or not a page is included in the menu.
 *
 * The function which is hooked in to handle the output of the page must check
 * that the user has the required capability as well.
 *
 * @since 1.5.0
 * @since 5.3.0 Added the `$position` parameter.
 *
 * @param string   $page_title The text to be displayed in the title tags of the page when the menu is selected.
 * @param string   $menu_title The text to be used for the menu.
 * @param string   $capability The capability required for this menu to be displayed to the user.
 * @param string   $menu_slug  The slug name to refer to this menu by (should be unique for this menu).
 * @param callable $callback   Optional. The function to be called to output the content for this page.
 * @param int      $position   Optional. The position in the menu order this item should appear.
 * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required.
 */
function add_options_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) {
	return add_submenu_page( 'options-general.php', $page_title, $menu_title, $capability, $menu_slug, $callback, $position );
}

/**
 * Adds a submenu page to the Appearance main menu.
 *
 * This function takes a capability which will be used to determine whether
 * or not a page is included in the menu.
 *
 * The function which is hooked in to handle the output of the page must check
 * that the user has the required capability as well.
 *
 * @since 2.0.0
 * @since 5.3.0 Added the `$position` parameter.
 *
 * @param string   $page_title The text to be displayed in the title tags of the page when the menu is selected.
 * @param string   $menu_title The text to be used for the menu.
 * @param string   $capability The capability required for this menu to be displayed to the user.
 * @param string   $menu_slug  The slug name to refer to this menu by (should be unique for this menu).
 * @param callable $callback   Optional. The function to be called to output the content for this page.
 * @param int      $position   Optional. The position in the menu order this item should appear.
 * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required.
 */
function add_theme_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) {
	return add_submenu_page( 'themes.php', $page_title, $menu_title, $capability, $menu_slug, $callback, $position );
}

/**
 * Adds a submenu page to the Plugins main menu.
 *
 * This function takes a capability which will be used to determine whether
 * or not a page is included in the menu.
 *
 * The function which is hooked in to handle the output of the page must check
 * that the user has the required capability as well.
 *
 * @since 3.0.0
 * @since 5.3.0 Added the `$position` parameter.
 *
 * @param string   $page_title The text to be displayed in the title tags of the page when the menu is selected.
 * @param string   $menu_title The text to be used for the menu.
 * @param string   $capability The capability required for this menu to be displayed to the user.
 * @param string   $menu_slug  The slug name to refer to this menu by (should be unique for this menu).
 * @param callable $callback   Optional. The function to be called to output the content for this page.
 * @param int      $position   Optional. The position in the menu order this item should appear.
 * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required.
 */
function add_plugins_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) {
	return add_submenu_page( 'plugins.php', $page_title, $menu_title, $capability, $menu_slug, $callback, $position );
}

/**
 * Adds a submenu page to the Users/Profile main menu.
 *
 * This function takes a capability which will be used to determine whether
 * or not a page is included in the menu.
 *
 * The function which is hooked in to handle the output of the page must check
 * that the user has the required capability as well.
 *
 * @since 2.1.3
 * @since 5.3.0 Added the `$position` parameter.
 *
 * @param string   $page_title The text to be displayed in the title tags of the page when the menu is selected.
 * @param string   $menu_title The text to be used for the menu.
 * @param string   $capability The capability required for this menu to be displayed to the user.
 * @param string   $menu_slug  The slug name to refer to this menu by (should be unique for this menu).
 * @param callable $callback   Optional. The function to be called to output the content for this page.
 * @param int      $position   Optional. The position in the menu order this item should appear.
 * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required.
 */
function add_users_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) {
	if ( current_user_can( 'edit_users' ) ) {
		$parent = 'users.php';
	} else {
		$parent = 'profile.php';
	}
	return add_submenu_page( $parent, $page_title, $menu_title, $capability, $menu_slug, $callback, $position );
}

/**
 * Adds a submenu page to the Dashboard main menu.
 *
 * This function takes a capability which will be used to determine whether
 * or not a page is included in the menu.
 *
 * The function which is hooked in to handle the output of the page must check
 * that the user has the required capability as well.
 *
 * @since 2.7.0
 * @since 5.3.0 Added the `$position` parameter.
 *
 * @param string   $page_title The text to be displayed in the title tags of the page when the menu is selected.
 * @param string   $menu_title The text to be used for the menu.
 * @param string   $capability The capability required for this menu to be displayed to the user.
 * @param string   $menu_slug  The slug name to refer to this menu by (should be unique for this menu).
 * @param callable $callback   Optional. The function to be called to output the content for this page.
 * @param int      $position   Optional. The position in the menu order this item should appear.
 * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required.
 */
function add_dashboard_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) {
	return add_submenu_page( 'index.php', $page_title, $menu_title, $capability, $menu_slug, $callback, $position );
}

/**
 * Adds a submenu page to the Posts main menu.
 *
 * This function takes a capability which will be used to determine whether
 * or not a page is included in the menu.
 *
 * The function which is hooked in to handle the output of the page must check
 * that the user has the required capability as well.
 *
 * @since 2.7.0
 * @since 5.3.0 Added the `$position` parameter.
 *
 * @param string   $page_title The text to be displayed in the title tags of the page when the menu is selected.
 * @param string   $menu_title The text to be used for the menu.
 * @param string   $capability The capability required for this menu to be displayed to the user.
 * @param string   $menu_slug  The slug name to refer to this menu by (should be unique for this menu).
 * @param callable $callback   Optional. The function to be called to output the content for this page.
 * @param int      $position   Optional. The position in the menu order this item should appear.
 * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required.
 */
function add_posts_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) {
	return add_submenu_page( 'edit.php', $page_title, $menu_title, $capability, $menu_slug, $callback, $position );
}

/**
 * Adds a submenu page to the Media main menu.
 *
 * This function takes a capability which will be used to determine whether
 * or not a page is included in the menu.
 *
 * The function which is hooked in to handle the output of the page must check
 * that the user has the required capability as well.
 *
 * @since 2.7.0
 * @since 5.3.0 Added the `$position` parameter.
 *
 * @param string   $page_title The text to be displayed in the title tags of the page when the menu is selected.
 * @param string   $menu_title The text to be used for the menu.
 * @param string   $capability The capability required for this menu to be displayed to the user.
 * @param string   $menu_slug  The slug name to refer to this menu by (should be unique for this menu).
 * @param callable $callback   Optional. The function to be called to output the content for this page.
 * @param int      $position   Optional. The position in the menu order this item should appear.
 * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required.
 */
function add_media_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) {
	return add_submenu_page( 'upload.php', $page_title, $menu_title, $capability, $menu_slug, $callback, $position );
}

/**
 * Adds a submenu page to the Links main menu.
 *
 * This function takes a capability which will be used to determine whether
 * or not a page is included in the menu.
 *
 * The function which is hooked in to handle the output of the page must check
 * that the user has the required capability as well.
 *
 * @since 2.7.0
 * @since 5.3.0 Added the `$position` parameter.
 *
 * @param string   $page_title The text to be displayed in the title tags of the page when the menu is selected.
 * @param string   $menu_title The text to be used for the menu.
 * @param string   $capability The capability required for this menu to be displayed to the user.
 * @param string   $menu_slug  The slug name to refer to this menu by (should be unique for this menu).
 * @param callable $callback   Optional. The function to be called to output the content for this page.
 * @param int      $position   Optional. The position in the menu order this item should appear.
 * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required.
 */
function add_links_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) {
	return add_submenu_page( 'link-manager.php', $page_title, $menu_title, $capability, $menu_slug, $callback, $position );
}

/**
 * Adds a submenu page to the Pages main menu.
 *
 * This function takes a capability which will be used to determine whether
 * or not a page is included in the menu.
 *
 * The function which is hooked in to handle the output of the page must check
 * that the user has the required capability as well.
 *
 * @since 2.7.0
 * @since 5.3.0 Added the `$position` parameter.
 *
 * @param string   $page_title The text to be displayed in the title tags of the page when the menu is selected.
 * @param string   $menu_title The text to be used for the menu.
 * @param string   $capability The capability required for this menu to be displayed to the user.
 * @param string   $menu_slug  The slug name to refer to this menu by (should be unique for this menu).
 * @param callable $callback   Optional. The function to be called to output the content for this page.
 * @param int      $position   Optional. The position in the menu order this item should appear.
 * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required.
 */
function add_pages_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) {
	return add_submenu_page( 'edit.php?post_type=page', $page_title, $menu_title, $capability, $menu_slug, $callback, $position );
}

/**
 * Adds a submenu page to the Comments main menu.
 *
 * This function takes a capability which will be used to determine whether
 * or not a page is included in the menu.
 *
 * The function which is hooked in to handle the output of the page must check
 * that the user has the required capability as well.
 *
 * @since 2.7.0
 * @since 5.3.0 Added the `$position` parameter.
 *
 * @param string   $page_title The text to be displayed in the title tags of the page when the menu is selected.
 * @param string   $menu_title The text to be used for the menu.
 * @param string   $capability The capability required for this menu to be displayed to the user.
 * @param string   $menu_slug  The slug name to refer to this menu by (should be unique for this menu).
 * @param callable $callback   Optional. The function to be called to output the content for this page.
 * @param int      $position   Optional. The position in the menu order this item should appear.
 * @return string|false The resulting page's hook_suffix, or false if the user does not have the capability required.
 */
function add_comments_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) {
	return add_submenu_page( 'edit-comments.php', $page_title, $menu_title, $capability, $menu_slug, $callback, $position );
}

/**
 * Removes a top-level admin menu.
 *
 * Example usage:
 *
 *  - `remove_menu_page( 'tools.php' )`
 *  - `remove_menu_page( 'plugin_menu_slug' )`
 *
 * @since 3.1.0
 *
 * @global array $menu
 *
 * @param string $menu_slug The slug of the menu.
 * @return array|false The removed menu on success, false if not found.
 */
function remove_menu_page( $menu_slug ) {
	global $menu;

	foreach ( $menu as $i => $item ) {
		if ( $menu_slug === $item[2] ) {
			unset( $menu[ $i ] );
			return $item;
		}
	}

	return false;
}

/**
 * Removes an admin submenu.
 *
 * Example usage:
 *
 *  - `remove_submenu_page( 'themes.php', 'nav-menus.php' )`
 *  - `remove_submenu_page( 'tools.php', 'plugin_submenu_slug' )`
 *  - `remove_submenu_page( 'plugin_menu_slug', 'plugin_submenu_slug' )`
 *
 * @since 3.1.0
 *
 * @global array $submenu
 *
 * @param string $menu_slug    The slug for the parent menu.
 * @param string $submenu_slug The slug of the submenu.
 * @return array|false The removed submenu on success, false if not found.
 */
function remove_submenu_page( $menu_slug, $submenu_slug ) {
	global $submenu;

	if ( ! isset( $submenu[ $menu_slug ] ) ) {
		return false;
	}

	foreach ( $submenu[ $menu_slug ] as $i => $item ) {
		if ( $submenu_slug === $item[2] ) {
			unset( $submenu[ $menu_slug ][ $i ] );
			return $item;
		}
	}

	return false;
}

/**
 * Gets the URL to access a particular menu page based on the slug it was registered with.
 *
 * If the slug hasn't been registered properly, no URL will be returned.
 *
 * @since 3.0.0
 *
 * @global array $_parent_pages
 *
 * @param string $menu_slug The slug name to refer to this menu by (should be unique for this menu).
 * @param bool   $display   Optional. Whether or not to display the URL. Default true.
 * @return string The menu page URL.
 */
function menu_page_url( $menu_slug, $display = true ) {
	global $_parent_pages;

	if ( isset( $_parent_pages[ $menu_slug ] ) ) {
		$parent_slug = $_parent_pages[ $menu_slug ];

		if ( $parent_slug && ! isset( $_parent_pages[ $parent_slug ] ) ) {
			$url = admin_url( add_query_arg( 'page', $menu_slug, $parent_slug ) );
		} else {
			$url = admin_url( 'admin.php?page=' . $menu_slug );
		}
	} else {
		$url = '';
	}

	$url = esc_url( $url );

	if ( $display ) {
		echo $url;
	}

	return $url;
}

//
// Pluggable Menu Support -- Private.
//
/**
 * Gets the parent file of the current admin page.
 *
 * @since 1.5.0
 *
 * @global string $parent_file
 * @global array  $menu
 * @global array  $submenu
 * @global string $pagenow              The filename of the current screen.
 * @global string $typenow              The post type of the current screen.
 * @global string $plugin_page
 * @global array  $_wp_real_parent_file
 * @global array  $_wp_menu_nopriv
 * @global array  $_wp_submenu_nopriv
 *
 * @param string $parent_page Optional. The slug name for the parent menu (or the file name
 *                            of a standard WordPress admin page). Default empty string.
 * @return string The parent file of the current admin page.
 */
function get_admin_page_parent( $parent_page = '' ) {
	global $parent_file, $menu, $submenu, $pagenow, $typenow,
		$plugin_page, $_wp_real_parent_file, $_wp_menu_nopriv, $_wp_submenu_nopriv;

	if ( ! empty( $parent_page ) && 'admin.php' !== $parent_page ) {
		if ( isset( $_wp_real_parent_file[ $parent_page ] ) ) {
			$parent_page = $_wp_real_parent_file[ $parent_page ];
		}

		return $parent_page;
	}

	if ( 'admin.php' === $pagenow && isset( $plugin_page ) ) {
		foreach ( (array) $menu as $parent_menu ) {
			if ( $parent_menu[2] === $plugin_page ) {
				$parent_file = $plugin_page;

				if ( isset( $_wp_real_parent_file[ $parent_file ] ) ) {
					$parent_file = $_wp_real_parent_file[ $parent_file ];
				}

				return $parent_file;
			}
		}
		if ( isset( $_wp_menu_nopriv[ $plugin_page ] ) ) {
			$parent_file = $plugin_page;

			if ( isset( $_wp_real_parent_file[ $parent_file ] ) ) {
					$parent_file = $_wp_real_parent_file[ $parent_file ];
			}

			return $parent_file;
		}
	}

	if ( isset( $plugin_page ) && isset( $_wp_submenu_nopriv[ $pagenow ][ $plugin_page ] ) ) {
		$parent_file = $pagenow;

		if ( isset( $_wp_real_parent_file[ $parent_file ] ) ) {
			$parent_file = $_wp_real_parent_file[ $parent_file ];
		}

		return $parent_file;
	}

	foreach ( array_keys( (array) $submenu ) as $parent_page ) {
		foreach ( $submenu[ $parent_page ] as $submenu_array ) {
			if ( isset( $_wp_real_parent_file[ $parent_page ] ) ) {
				$parent_page = $_wp_real_parent_file[ $parent_page ];
			}

			if ( ! empty( $typenow ) && "$pagenow?post_type=$typenow" === $submenu_array[2] ) {
				$parent_file = $parent_page;
				return $parent_page;
			} elseif ( empty( $typenow ) && $pagenow === $submenu_array[2]
				&& ( empty( $parent_file ) || ! str_contains( $parent_file, '?' ) )
			) {
				$parent_file = $parent_page;
				return $parent_page;
			} elseif ( isset( $plugin_page ) && $plugin_page === $submenu_array[2] ) {
				$parent_file = $parent_page;
				return $parent_page;
			}
		}
	}

	if ( empty( $parent_file ) ) {
		$parent_file = '';
	}
	return '';
}

/**
 * Gets the title of the current admin page.
 *
 * @since 1.5.0
 *
 * @global string $title       The title of the current screen.
 * @global array  $menu
 * @global array  $submenu
 * @global string $pagenow     The filename of the current screen.
 * @global string $typenow     The post type of the current screen.
 * @global string $plugin_page
 *
 * @return string The title of the current admin page.
 */
function get_admin_page_title() {
	global $title, $menu, $submenu, $pagenow, $typenow, $plugin_page;

	if ( ! empty( $title ) ) {
		return $title;
	}

	$hook = get_plugin_page_hook( $plugin_page, $pagenow );

	$parent  = get_admin_page_parent();
	$parent1 = $parent;

	if ( empty( $parent ) ) {
		foreach ( (array) $menu as $menu_array ) {
			if ( isset( $menu_array[3] ) ) {
				if ( $menu_array[2] === $pagenow ) {
					$title = $menu_array[3];
					return $menu_array[3];
				} elseif ( isset( $plugin_page ) && $plugin_page === $menu_array[2] && $hook === $menu_array[5] ) {
					$title = $menu_array[3];
					return $menu_array[3];
				}
			} else {
				$title = $menu_array[0];
				return $title;
			}
		}
	} else {
		foreach ( array_keys( $submenu ) as $parent ) {
			foreach ( $submenu[ $parent ] as $submenu_array ) {
				if ( isset( $plugin_page )
					&& $plugin_page === $submenu_array[2]
					&& ( $pagenow === $parent
						|| $plugin_page === $parent
						|| $plugin_page === $hook
						|| 'admin.php' === $pagenow && $parent1 !== $submenu_array[2]
						|| ! empty( $typenow ) && "$pagenow?post_type=$typenow" === $parent )
					) {
						$title = $submenu_array[3];
						return $submenu_array[3];
				}

				if ( $submenu_array[2] !== $pagenow || isset( $_GET['page'] ) ) { // Not the current page.
					continue;
				}

				if ( isset( $submenu_array[3] ) ) {
					$title = $submenu_array[3];
					return $submenu_array[3];
				} else {
					$title = $submenu_array[0];
					return $title;
				}
			}
		}
		if ( empty( $title ) ) {
			foreach ( $menu as $menu_array ) {
				if ( isset( $plugin_page )
					&& $plugin_page === $menu_array[2]
					&& 'admin.php' === $pagenow
					&& $parent1 === $menu_array[2]
				) {
						$title = $menu_array[3];
						return $menu_array[3];
				}
			}
		}
	}

	return $title;
}

/**
 * Gets the hook attached to the administrative page of a plugin.
 *
 * @since 1.5.0
 *
 * @param string $plugin_page The slug name of the plugin page.
 * @param string $parent_page The slug name for the parent menu (or the file name of a standard
 *                            WordPress admin page).
 * @return string|null Hook attached to the plugin page, null otherwise.
 */
function get_plugin_page_hook( $plugin_page, $parent_page ) {
	$hook = get_plugin_page_hookname( $plugin_page, $parent_page );
	if ( has_action( $hook ) ) {
		return $hook;
	} else {
		return null;
	}
}

/**
 * Gets the hook name for the administrative page of a plugin.
 *
 * @since 1.5.0
 *
 * @global array $admin_page_hooks
 *
 * @param string $plugin_page The slug name of the plugin page.
 * @param string $parent_page The slug name for the parent menu (or the file name of a standard
 *                            WordPress admin page).
 * @return string Hook name for the plugin page.
 */
function get_plugin_page_hookname( $plugin_page, $parent_page ) {
	global $admin_page_hooks;

	$parent = get_admin_page_parent( $parent_page );

	$page_type = 'admin';
	if ( empty( $parent_page ) || 'admin.php' === $parent_page || isset( $admin_page_hooks[ $plugin_page ] ) ) {
		if ( isset( $admin_page_hooks[ $plugin_page ] ) ) {
			$page_type = 'toplevel';
		} elseif ( isset( $admin_page_hooks[ $parent ] ) ) {
			$page_type = $admin_page_hooks[ $parent ];
		}
	} elseif ( isset( $admin_page_hooks[ $parent ] ) ) {
		$page_type = $admin_page_hooks[ $parent ];
	}

	$plugin_name = preg_replace( '!\.php!', '', $plugin_page );

	return $page_type . '_page_' . $plugin_name;
}

/**
 * Determines whether the current user can access the current admin page.
 *
 * @since 1.5.0
 *
 * @global string $pagenow            The filename of the current screen.
 * @global array  $menu
 * @global array  $submenu
 * @global array  $_wp_menu_nopriv
 * @global array  $_wp_submenu_nopriv
 * @global string $plugin_page
 * @global array  $_registered_pages
 *
 * @return bool True if the current user can access the admin page, false otherwise.
 */
function user_can_access_admin_page() {
	global $pagenow, $menu, $submenu, $_wp_menu_nopriv, $_wp_submenu_nopriv,
		$plugin_page, $_registered_pages;

	$parent = get_admin_page_parent();

	if ( ! isset( $plugin_page ) && isset( $_wp_submenu_nopriv[ $parent ][ $pagenow ] ) ) {
		return false;
	}

	if ( isset( $plugin_page ) ) {
		if ( isset( $_wp_submenu_nopriv[ $parent ][ $plugin_page ] ) ) {
			return false;
		}

		$hookname = get_plugin_page_hookname( $plugin_page, $parent );

		if ( ! isset( $_registered_pages[ $hookname ] ) ) {
			return false;
		}
	}

	if ( empty( $parent ) ) {
		if ( isset( $_wp_menu_nopriv[ $pagenow ] ) ) {
			return false;
		}
		if ( isset( $_wp_submenu_nopriv[ $pagenow ][ $pagenow ] ) ) {
			return false;
		}
		if ( isset( $plugin_page ) && isset( $_wp_submenu_nopriv[ $pagenow ][ $plugin_page ] ) ) {
			return false;
		}
		if ( isset( $plugin_page ) && isset( $_wp_menu_nopriv[ $plugin_page ] ) ) {
			return false;
		}

		foreach ( array_keys( $_wp_submenu_nopriv ) as $key ) {
			if ( isset( $_wp_submenu_nopriv[ $key ][ $pagenow ] ) ) {
				return false;
			}
			if ( isset( $plugin_page ) && isset( $_wp_submenu_nopriv[ $key ][ $plugin_page ] ) ) {
				return false;
			}
		}

		return true;
	}

	if ( isset( $plugin_page ) && $plugin_page === $parent && isset( $_wp_menu_nopriv[ $plugin_page ] ) ) {
		return false;
	}

	if ( isset( $submenu[ $parent ] ) ) {
		foreach ( $submenu[ $parent ] as $submenu_array ) {
			if ( isset( $plugin_page ) && $submenu_array[2] === $plugin_page ) {
				return current_user_can( $submenu_array[1] );
			} elseif ( $submenu_array[2] === $pagenow ) {
				return current_user_can( $submenu_array[1] );
			}
		}
	}

	foreach ( $menu as $menu_array ) {
		if ( $menu_array[2] === $parent ) {
			return current_user_can( $menu_array[1] );
		}
	}

	return true;
}

/* Allowed list functions */

/**
 * Refreshes the value of the allowed options list available via the 'allowed_options' hook.
 *
 * See the {@see 'allowed_options'} filter.
 *
 * @since 2.7.0
 * @since 5.5.0 `$new_whitelist_options` was renamed to `$new_allowed_options`.
 *              Please consider writing more inclusive code.
 *
 * @global array $new_allowed_options
 *
 * @param array $options
 * @return array
 */
function option_update_filter( $options ) {
	global $new_allowed_options;

	if ( is_array( $new_allowed_options ) ) {
		$options = add_allowed_options( $new_allowed_options, $options );
	}

	return $options;
}

/**
 * Adds an array of options to the list of allowed options.
 *
 * @since 5.5.0
 *
 * @global array $allowed_options
 *
 * @param array        $new_options
 * @param string|array $options
 * @return array
 */
function add_allowed_options( $new_options, $options = '' ) {
	if ( '' === $options ) {
		global $allowed_options;
	} else {
		$allowed_options = $options;
	}

	foreach ( $new_options as $page => $keys ) {
		foreach ( $keys as $key ) {
			if ( ! isset( $allowed_options[ $page ] ) || ! is_array( $allowed_options[ $page ] ) ) {
				$allowed_options[ $page ]   = array();
				$allowed_options[ $page ][] = $key;
			} else {
				$pos = array_search( $key, $allowed_options[ $page ], true );
				if ( false === $pos ) {
					$allowed_options[ $page ][] = $key;
				}
			}
		}
	}

	return $allowed_options;
}

/**
 * Removes a list of options from the allowed options list.
 *
 * @since 5.5.0
 *
 * @global array $allowed_options
 *
 * @param array        $del_options
 * @param string|array $options
 * @return array
 */
function remove_allowed_options( $del_options, $options = '' ) {
	if ( '' === $options ) {
		global $allowed_options;
	} else {
		$allowed_options = $options;
	}

	foreach ( $del_options as $page => $keys ) {
		foreach ( $keys as $key ) {
			if ( isset( $allowed_options[ $page ] ) && is_array( $allowed_options[ $page ] ) ) {
				$pos = array_search( $key, $allowed_options[ $page ], true );
				if ( false !== $pos ) {
					unset( $allowed_options[ $page ][ $pos ] );
				}
			}
		}
	}

	return $allowed_options;
}

/**
 * Outputs nonce, action, and option_page fields for a settings page.
 *
 * @since 2.7.0
 *
 * @param string $option_group A settings group name. This should match the group name
 *                             used in register_setting().
 */
function settings_fields( $option_group ) {
	echo "<input type='hidden' name='option_page' value='" . esc_attr( $option_group ) . "' />";
	echo '<input type="hidden" name="action" value="update" />';
	wp_nonce_field( "$option_group-options" );
}

/**
 * Clears the plugins cache used by get_plugins() and by default, the plugin updates cache.
 *
 * @since 3.7.0
 *
 * @param bool $clear_update_cache Whether to clear the plugin updates cache. Default true.
 */
function wp_clean_plugins_cache( $clear_update_cache = true ) {
	if ( $clear_update_cache ) {
		delete_site_transient( 'update_plugins' );
	}
	wp_cache_delete( 'plugins', 'plugins' );
}

/**
 * Loads a given plugin attempt to generate errors.
 *
 * @since 3.0.0
 * @since 4.4.0 Function was moved into the `wp-admin/includes/plugin.php` file.
 *
 * @param string $plugin Path to the plugin file relative to the plugins directory.
 */
function plugin_sandbox_scrape( $plugin ) {
	if ( ! defined( 'WP_SANDBOX_SCRAPING' ) ) {
		define( 'WP_SANDBOX_SCRAPING', true );
	}

	wp_register_plugin_realpath( WP_PLUGIN_DIR . '/' . $plugin );
	include_once WP_PLUGIN_DIR . '/' . $plugin;
}

/**
 * Declares a helper function for adding content to the Privacy Policy Guide.
 *
 * Plugins and themes should suggest text for inclusion in the site's privacy policy.
 * The suggested text should contain information about any functionality that affects user privacy,
 * and will be shown on the Privacy Policy Guide screen.
 *
 * A plugin or theme can use this function multiple times as long as it will help to better present
 * the suggested policy content. For example modular plugins such as WooCommerse or Jetpack
 * can add or remove suggested content depending on the modules/extensions that are enabled.
 * For more information see the Plugin Handbook:
 * https://developer.wordpress.org/plugins/privacy/suggesting-text-for-the-site-privacy-policy/.
 *
 * The HTML contents of the `$policy_text` supports use of a specialized `.privacy-policy-tutorial`
 * CSS class which can be used to provide supplemental information. Any content contained within
 * HTML elements that have the `.privacy-policy-tutorial` CSS class applied will be omitted
 * from the clipboard when the section content is copied.
 *
 * Intended for use with the `'admin_init'` action.
 *
 * @since 4.9.6
 *
 * @param string $plugin_name The name of the plugin or theme that is suggesting content
 *                            for the site's privacy policy.
 * @param string $policy_text The suggested content for inclusion in the policy.
 */
function wp_add_privacy_policy_content( $plugin_name, $policy_text ) {
	if ( ! is_admin() ) {
		_doing_it_wrong(
			__FUNCTION__,
			sprintf(
				/* translators: %s: admin_init */
				__( 'The suggested privacy policy content should be added only in wp-admin by using the %s (or later) action.' ),
				'<code>admin_init</code>'
			),
			'4.9.7'
		);
		return;
	} elseif ( ! doing_action( 'admin_init' ) && ! did_action( 'admin_init' ) ) {
		_doing_it_wrong(
			__FUNCTION__,
			sprintf(
				/* translators: %s: admin_init */
				__( 'The suggested privacy policy content should be added by using the %s (or later) action. Please see the inline documentation.' ),
				'<code>admin_init</code>'
			),
			'4.9.7'
		);
		return;
	}

	if ( ! class_exists( 'WP_Privacy_Policy_Content' ) ) {
		require_once ABSPATH . 'wp-admin/includes/class-wp-privacy-policy-content.php';
	}

	WP_Privacy_Policy_Content::add( $plugin_name, $policy_text );
}

/**
 * Determines whether a plugin is technically active but was paused while
 * loading.
 *
 * For more information on this and similar theme functions, check out
 * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/
 * Conditional Tags} article in the Theme Developer Handbook.
 *
 * @since 5.2.0
 *
 * @global WP_Paused_Extensions_Storage $_paused_plugins
 *
 * @param string $plugin Path to the plugin file relative to the plugins directory.
 * @return bool True, if in the list of paused plugins. False, if not in the list.
 */
function is_plugin_paused( $plugin ) {
	if ( ! isset( $GLOBALS['_paused_plugins'] ) ) {
		return false;
	}

	if ( ! is_plugin_active( $plugin ) ) {
		return false;
	}

	list( $plugin ) = explode( '/', $plugin );

	return array_key_exists( $plugin, $GLOBALS['_paused_plugins'] );
}

/**
 * Gets the error that was recorded for a paused plugin.
 *
 * @since 5.2.0
 *
 * @global WP_Paused_Extensions_Storage $_paused_plugins
 *
 * @param string $plugin Path to the plugin file relative to the plugins directory.
 * @return array|false Array of error information as returned by `error_get_last()`,
 *                     or false if none was recorded.
 */
function wp_get_plugin_error( $plugin ) {
	if ( ! isset( $GLOBALS['_paused_plugins'] ) ) {
		return false;
	}

	list( $plugin ) = explode( '/', $plugin );

	if ( ! array_key_exists( $plugin, $GLOBALS['_paused_plugins'] ) ) {
		return false;
	}

	return $GLOBALS['_paused_plugins'][ $plugin ];
}

/**
 * Tries to resume a single plugin.
 *
 * If a redirect was provided, we first ensure the plugin does not throw fatal
 * errors anymore.
 *
 * The way it works is by setting the redirection to the error before trying to
 * include the plugin file. If the plugin fails, then the redirection will not
 * be overwritten with the success message and the plugin will not be resumed.
 *
 * @since 5.2.0
 *
 * @param string $plugin   Single plugin to resume.
 * @param string $redirect Optional. URL to redirect to. Default empty string.
 * @return true|WP_Error True on success, false if `$plugin` was not paused,
 *                       `WP_Error` on failure.
 */
function resume_plugin( $plugin, $redirect = '' ) {
	/*
	 * We'll override this later if the plugin could be resumed without
	 * creating a fatal error.
	 */
	if ( ! empty( $redirect ) ) {
		wp_redirect(
			add_query_arg(
				'_error_nonce',
				wp_create_nonce( 'plugin-resume-error_' . $plugin ),
				$redirect
			)
		);

		// Load the plugin to test whether it throws a fatal error.
		ob_start();
		plugin_sandbox_scrape( $plugin );
		ob_clean();
	}

	list( $extension ) = explode( '/', $plugin );

	$result = wp_paused_plugins()->delete( $extension );

	if ( ! $result ) {
		return new WP_Error(
			'could_not_resume_plugin',
			__( 'Could not resume the plugin.' )
		);
	}

	return true;
}

/**
 * Renders an admin notice in case some plugins have been paused due to errors.
 *
 * @since 5.2.0
 *
 * @global string                       $pagenow         The filename of the current screen.
 * @global WP_Paused_Extensions_Storage $_paused_plugins
 */
function paused_plugins_notice() {
	if ( 'plugins.php' === $GLOBALS['pagenow'] ) {
		return;
	}

	if ( ! current_user_can( 'resume_plugins' ) ) {
		return;
	}

	if ( ! isset( $GLOBALS['_paused_plugins'] ) || empty( $GLOBALS['_paused_plugins'] ) ) {
		return;
	}

	$message = sprintf(
		'<strong>%s</strong><br>%s</p><p><a href="%s">%s</a>',
		__( 'One or more plugins failed to load properly.' ),
		__( 'You can find more details and make changes on the Plugins screen.' ),
		esc_url( admin_url( 'plugins.php?plugin_status=paused' ) ),
		__( 'Go to the Plugins screen' )
	);
	wp_admin_notice(
		$message,
		array( 'type' => 'error' )
	);
}

/**
 * Renders an admin notice when a plugin was deactivated during an update.
 *
 * Displays an admin notice in case a plugin has been deactivated during an
 * upgrade due to incompatibility with the current version of WordPress.
 *
 * @since 5.8.0
 * @access private
 *
 * @global string $pagenow    The filename of the current screen.
 * @global string $wp_version The WordPress version string.
 */
function deactivated_plugins_notice() {
	if ( 'plugins.php' === $GLOBALS['pagenow'] ) {
		return;
	}

	if ( ! current_user_can( 'activate_plugins' ) ) {
		return;
	}

	$blog_deactivated_plugins = get_option( 'wp_force_deactivated_plugins' );
	$site_deactivated_plugins = array();

	if ( false === $blog_deactivated_plugins ) {
		// Option not in database, add an empty array to avoid extra DB queries on subsequent loads.
		update_option( 'wp_force_deactivated_plugins', array(), false );
	}

	if ( is_multisite() ) {
		$site_deactivated_plugins = get_site_option( 'wp_force_deactivated_plugins' );
		if ( false === $site_deactivated_plugins ) {
			// Option not in database, add an empty array to avoid extra DB queries on subsequent loads.
			update_site_option( 'wp_force_deactivated_plugins', array() );
		}
	}

	if ( empty( $blog_deactivated_plugins ) && empty( $site_deactivated_plugins ) ) {
		// No deactivated plugins.
		return;
	}

	$deactivated_plugins = array_merge( $blog_deactivated_plugins, $site_deactivated_plugins );

	foreach ( $deactivated_plugins as $plugin ) {
		if ( ! empty( $plugin['version_compatible'] ) && ! empty( $plugin['version_deactivated'] ) ) {
			$explanation = sprintf(
				/* translators: 1: Name of deactivated plugin, 2: Plugin version deactivated, 3: Current WP version, 4: Compatible plugin version. */
				__( '%1$s %2$s was deactivated due to incompatibility with WordPress %3$s, please upgrade to %1$s %4$s or later.' ),
				$plugin['plugin_name'],
				$plugin['version_deactivated'],
				$GLOBALS['wp_version'],
				$plugin['version_compatible']
			);
		} else {
			$explanation = sprintf(
				/* translators: 1: Name of deactivated plugin, 2: Plugin version deactivated, 3: Current WP version. */
				__( '%1$s %2$s was deactivated due to incompatibility with WordPress %3$s.' ),
				$plugin['plugin_name'],
				! empty( $plugin['version_deactivated'] ) ? $plugin['version_deactivated'] : '',
				$GLOBALS['wp_version'],
				$plugin['version_compatible']
			);
		}

		$message = sprintf(
			'<strong>%s</strong><br>%s</p><p><a href="%s">%s</a>',
			sprintf(
				/* translators: %s: Name of deactivated plugin. */
				__( '%s plugin deactivated during WordPress upgrade.' ),
				$plugin['plugin_name']
			),
			$explanation,
			esc_url( admin_url( 'plugins.php?plugin_status=inactive' ) ),
			__( 'Go to the Plugins screen' )
		);
		wp_admin_notice( $message, array( 'type' => 'warning' ) );
	}

	// Empty the options.
	update_option( 'wp_force_deactivated_plugins', array(), false );
	if ( is_multisite() ) {
		update_site_option( 'wp_force_deactivated_plugins', array() );
	}
}
class-wp-filesystem-ftpext.php000060400000055075151327343360012517 0ustar00<?php
/**
 * WordPress FTP Filesystem.
 *
 * @package WordPress
 * @subpackage Filesystem
 */

/**
 * WordPress Filesystem Class for implementing FTP.
 *
 * @since 2.5.0
 *
 * @see WP_Filesystem_Base
 */
class WP_Filesystem_FTPext extends WP_Filesystem_Base {

	/**
	 * @since 2.5.0
	 * @var resource
	 */
	public $link;

	/**
	 * Constructor.
	 *
	 * @since 2.5.0
	 *
	 * @param array $opt
	 */
	public function __construct( $opt = '' ) {
		$this->method = 'ftpext';
		$this->errors = new WP_Error();

		// Check if possible to use ftp functions.
		if ( ! extension_loaded( 'ftp' ) ) {
			$this->errors->add( 'no_ftp_ext', __( 'The ftp PHP extension is not available' ) );
			return;
		}

		// This class uses the timeout on a per-connection basis, others use it on a per-action basis.
		if ( ! defined( 'FS_TIMEOUT' ) ) {
			define( 'FS_TIMEOUT', 4 * MINUTE_IN_SECONDS );
		}

		if ( empty( $opt['port'] ) ) {
			$this->options['port'] = 21;
		} else {
			$this->options['port'] = $opt['port'];
		}

		if ( empty( $opt['hostname'] ) ) {
			$this->errors->add( 'empty_hostname', __( 'FTP hostname is required' ) );
		} else {
			$this->options['hostname'] = $opt['hostname'];
		}

		// Check if the options provided are OK.
		if ( empty( $opt['username'] ) ) {
			$this->errors->add( 'empty_username', __( 'FTP username is required' ) );
		} else {
			$this->options['username'] = $opt['username'];
		}

		if ( empty( $opt['password'] ) ) {
			$this->errors->add( 'empty_password', __( 'FTP password is required' ) );
		} else {
			$this->options['password'] = $opt['password'];
		}

		$this->options['ssl'] = false;

		if ( isset( $opt['connection_type'] ) && 'ftps' === $opt['connection_type'] ) {
			$this->options['ssl'] = true;
		}
	}

	/**
	 * Connects filesystem.
	 *
	 * @since 2.5.0
	 *
	 * @return bool True on success, false on failure.
	 */
	public function connect() {
		if ( isset( $this->options['ssl'] ) && $this->options['ssl'] && function_exists( 'ftp_ssl_connect' ) ) {
			$this->link = @ftp_ssl_connect( $this->options['hostname'], $this->options['port'], FS_CONNECT_TIMEOUT );
		} else {
			$this->link = @ftp_connect( $this->options['hostname'], $this->options['port'], FS_CONNECT_TIMEOUT );
		}

		if ( ! $this->link ) {
			$this->errors->add(
				'connect',
				sprintf(
					/* translators: %s: hostname:port */
					__( 'Failed to connect to FTP Server %s' ),
					$this->options['hostname'] . ':' . $this->options['port']
				)
			);

			return false;
		}

		if ( ! @ftp_login( $this->link, $this->options['username'], $this->options['password'] ) ) {
			$this->errors->add(
				'auth',
				sprintf(
					/* translators: %s: Username. */
					__( 'Username/Password incorrect for %s' ),
					$this->options['username']
				)
			);

			return false;
		}

		// Set the connection to use Passive FTP.
		ftp_pasv( $this->link, true );

		if ( @ftp_get_option( $this->link, FTP_TIMEOUT_SEC ) < FS_TIMEOUT ) {
			@ftp_set_option( $this->link, FTP_TIMEOUT_SEC, FS_TIMEOUT );
		}

		return true;
	}

	/**
	 * Reads entire file into a string.
	 *
	 * @since 2.5.0
	 *
	 * @param string $file Name of the file to read.
	 * @return string|false Read data on success, false if no temporary file could be opened,
	 *                      or if the file couldn't be retrieved.
	 */
	public function get_contents( $file ) {
		$tempfile   = wp_tempnam( $file );
		$temphandle = fopen( $tempfile, 'w+' );

		if ( ! $temphandle ) {
			unlink( $tempfile );
			return false;
		}

		if ( ! ftp_fget( $this->link, $temphandle, $file, FTP_BINARY ) ) {
			fclose( $temphandle );
			unlink( $tempfile );
			return false;
		}

		fseek( $temphandle, 0 ); // Skip back to the start of the file being written to.
		$contents = '';

		while ( ! feof( $temphandle ) ) {
			$contents .= fread( $temphandle, 8 * KB_IN_BYTES );
		}

		fclose( $temphandle );
		unlink( $tempfile );

		return $contents;
	}

	/**
	 * Reads entire file into an array.
	 *
	 * @since 2.5.0
	 *
	 * @param string $file Path to the file.
	 * @return array|false File contents in an array on success, false on failure.
	 */
	public function get_contents_array( $file ) {
		return explode( "\n", $this->get_contents( $file ) );
	}

	/**
	 * Writes a string to a file.
	 *
	 * @since 2.5.0
	 *
	 * @param string    $file     Remote path to the file where to write the data.
	 * @param string    $contents The data to write.
	 * @param int|false $mode     Optional. The file permissions as octal number, usually 0644.
	 *                            Default false.
	 * @return bool True on success, false on failure.
	 */
	public function put_contents( $file, $contents, $mode = false ) {
		$tempfile   = wp_tempnam( $file );
		$temphandle = fopen( $tempfile, 'wb+' );

		if ( ! $temphandle ) {
			unlink( $tempfile );
			return false;
		}

		mbstring_binary_safe_encoding();

		$data_length   = strlen( $contents );
		$bytes_written = fwrite( $temphandle, $contents );

		reset_mbstring_encoding();

		if ( $data_length !== $bytes_written ) {
			fclose( $temphandle );
			unlink( $tempfile );
			return false;
		}

		fseek( $temphandle, 0 ); // Skip back to the start of the file being written to.

		$ret = ftp_fput( $this->link, $file, $temphandle, FTP_BINARY );

		fclose( $temphandle );
		unlink( $tempfile );

		$this->chmod( $file, $mode );

		return $ret;
	}

	/**
	 * Gets the current working directory.
	 *
	 * @since 2.5.0
	 *
	 * @return string|false The current working directory on success, false on failure.
	 */
	public function cwd() {
		$cwd = ftp_pwd( $this->link );

		if ( $cwd ) {
			$cwd = trailingslashit( $cwd );
		}

		return $cwd;
	}

	/**
	 * Changes current directory.
	 *
	 * @since 2.5.0
	 *
	 * @param string $dir The new current directory.
	 * @return bool True on success, false on failure.
	 */
	public function chdir( $dir ) {
		return @ftp_chdir( $this->link, $dir );
	}

	/**
	 * Changes filesystem permissions.
	 *
	 * @since 2.5.0
	 *
	 * @param string    $file      Path to the file.
	 * @param int|false $mode      Optional. The permissions as octal number, usually 0644 for files,
	 *                             0755 for directories. Default false.
	 * @param bool      $recursive Optional. If set to true, changes file permissions recursively.
	 *                             Default false.
	 * @return bool True on success, false on failure.
	 */
	public function chmod( $file, $mode = false, $recursive = false ) {
		if ( ! $mode ) {
			if ( $this->is_file( $file ) ) {
				$mode = FS_CHMOD_FILE;
			} elseif ( $this->is_dir( $file ) ) {
				$mode = FS_CHMOD_DIR;
			} else {
				return false;
			}
		}

		// chmod any sub-objects if recursive.
		if ( $recursive && $this->is_dir( $file ) ) {
			$filelist = $this->dirlist( $file );

			foreach ( (array) $filelist as $filename => $filemeta ) {
				$this->chmod( $file . '/' . $filename, $mode, $recursive );
			}
		}

		// chmod the file or directory.
		if ( ! function_exists( 'ftp_chmod' ) ) {
			return (bool) ftp_site( $this->link, sprintf( 'CHMOD %o %s', $mode, $file ) );
		}

		return (bool) ftp_chmod( $this->link, $mode, $file );
	}

	/**
	 * Gets the file owner.
	 *
	 * @since 2.5.0
	 *
	 * @param string $file Path to the file.
	 * @return string|false Username of the owner on success, false on failure.
	 */
	public function owner( $file ) {
		$dir = $this->dirlist( $file );

		return $dir[ $file ]['owner'];
	}

	/**
	 * Gets the permissions of the specified file or filepath in their octal format.
	 *
	 * @since 2.5.0
	 *
	 * @param string $file Path to the file.
	 * @return string Mode of the file (the last 3 digits).
	 */
	public function getchmod( $file ) {
		$dir = $this->dirlist( $file );

		return $dir[ $file ]['permsn'];
	}

	/**
	 * Gets the file's group.
	 *
	 * @since 2.5.0
	 *
	 * @param string $file Path to the file.
	 * @return string|false The group on success, false on failure.
	 */
	public function group( $file ) {
		$dir = $this->dirlist( $file );

		return $dir[ $file ]['group'];
	}

	/**
	 * Copies a file.
	 *
	 * @since 2.5.0
	 *
	 * @param string    $source      Path to the source file.
	 * @param string    $destination Path to the destination file.
	 * @param bool      $overwrite   Optional. Whether to overwrite the destination file if it exists.
	 *                               Default false.
	 * @param int|false $mode        Optional. The permissions as octal number, usually 0644 for files,
	 *                               0755 for dirs. Default false.
	 * @return bool True on success, false on failure.
	 */
	public function copy( $source, $destination, $overwrite = false, $mode = false ) {
		if ( ! $overwrite && $this->exists( $destination ) ) {
			return false;
		}

		$content = $this->get_contents( $source );

		if ( false === $content ) {
			return false;
		}

		return $this->put_contents( $destination, $content, $mode );
	}

	/**
	 * Moves a file or directory.
	 *
	 * After moving files or directories, OPcache will need to be invalidated.
	 *
	 * If moving a directory fails, `copy_dir()` can be used for a recursive copy.
	 *
	 * Use `move_dir()` for moving directories with OPcache invalidation and a
	 * fallback to `copy_dir()`.
	 *
	 * @since 2.5.0
	 *
	 * @param string $source      Path to the source file or directory.
	 * @param string $destination Path to the destination file or directory.
	 * @param bool   $overwrite   Optional. Whether to overwrite the destination if it exists.
	 *                            Default false.
	 * @return bool True on success, false on failure.
	 */
	public function move( $source, $destination, $overwrite = false ) {
		return ftp_rename( $this->link, $source, $destination );
	}

	/**
	 * Deletes a file or directory.
	 *
	 * @since 2.5.0
	 *
	 * @param string       $file      Path to the file or directory.
	 * @param bool         $recursive Optional. If set to true, deletes files and folders recursively.
	 *                                Default false.
	 * @param string|false $type      Type of resource. 'f' for file, 'd' for directory.
	 *                                Default false.
	 * @return bool True on success, false on failure.
	 */
	public function delete( $file, $recursive = false, $type = false ) {
		if ( empty( $file ) ) {
			return false;
		}

		if ( 'f' === $type || $this->is_file( $file ) ) {
			return ftp_delete( $this->link, $file );
		}

		if ( ! $recursive ) {
			return ftp_rmdir( $this->link, $file );
		}

		$filelist = $this->dirlist( trailingslashit( $file ) );

		if ( ! empty( $filelist ) ) {
			foreach ( $filelist as $delete_file ) {
				$this->delete( trailingslashit( $file ) . $delete_file['name'], $recursive, $delete_file['type'] );
			}
		}

		return ftp_rmdir( $this->link, $file );
	}

	/**
	 * Checks if a file or directory exists.
	 *
	 * @since 2.5.0
	 * @since 6.3.0 Returns false for an empty path.
	 *
	 * @param string $path Path to file or directory.
	 * @return bool Whether $path exists or not.
	 */
	public function exists( $path ) {
		/*
		 * Check for empty path. If ftp_nlist() receives an empty path,
		 * it checks the current working directory and may return true.
		 *
		 * See https://core.trac.wordpress.org/ticket/33058.
		 */
		if ( '' === $path ) {
			return false;
		}

		$list = ftp_nlist( $this->link, $path );

		if ( empty( $list ) && $this->is_dir( $path ) ) {
			return true; // File is an empty directory.
		}

		return ! empty( $list ); // Empty list = no file, so invert.
	}

	/**
	 * Checks if resource is a file.
	 *
	 * @since 2.5.0
	 *
	 * @param string $file File path.
	 * @return bool Whether $file is a file.
	 */
	public function is_file( $file ) {
		return $this->exists( $file ) && ! $this->is_dir( $file );
	}

	/**
	 * Checks if resource is a directory.
	 *
	 * @since 2.5.0
	 *
	 * @param string $path Directory path.
	 * @return bool Whether $path is a directory.
	 */
	public function is_dir( $path ) {
		$cwd    = $this->cwd();
		$result = @ftp_chdir( $this->link, trailingslashit( $path ) );

		if ( $result && $path === $this->cwd() || $this->cwd() !== $cwd ) {
			@ftp_chdir( $this->link, $cwd );
			return true;
		}

		return false;
	}

	/**
	 * Checks if a file is readable.
	 *
	 * @since 2.5.0
	 *
	 * @param string $file Path to file.
	 * @return bool Whether $file is readable.
	 */
	public function is_readable( $file ) {
		return true;
	}

	/**
	 * Checks if a file or directory is writable.
	 *
	 * @since 2.5.0
	 *
	 * @param string $path Path to file or directory.
	 * @return bool Whether $path is writable.
	 */
	public function is_writable( $path ) {
		return true;
	}

	/**
	 * Gets the file's last access time.
	 *
	 * @since 2.5.0
	 *
	 * @param string $file Path to file.
	 * @return int|false Unix timestamp representing last access time, false on failure.
	 */
	public function atime( $file ) {
		return false;
	}

	/**
	 * Gets the file modification time.
	 *
	 * @since 2.5.0
	 *
	 * @param string $file Path to file.
	 * @return int|false Unix timestamp representing modification time, false on failure.
	 */
	public function mtime( $file ) {
		return ftp_mdtm( $this->link, $file );
	}

	/**
	 * Gets the file size (in bytes).
	 *
	 * @since 2.5.0
	 *
	 * @param string $file Path to file.
	 * @return int|false Size of the file in bytes on success, false on failure.
	 */
	public function size( $file ) {
		$size = ftp_size( $this->link, $file );

		return ( $size > -1 ) ? $size : false;
	}

	/**
	 * Sets the access and modification times of a file.
	 *
	 * Note: If $file doesn't exist, it will be created.
	 *
	 * @since 2.5.0
	 *
	 * @param string $file  Path to file.
	 * @param int    $time  Optional. Modified time to set for file.
	 *                      Default 0.
	 * @param int    $atime Optional. Access time to set for file.
	 *                      Default 0.
	 * @return bool True on success, false on failure.
	 */
	public function touch( $file, $time = 0, $atime = 0 ) {
		return false;
	}

	/**
	 * Creates a directory.
	 *
	 * @since 2.5.0
	 *
	 * @param string           $path  Path for new directory.
	 * @param int|false        $chmod Optional. The permissions as octal number (or false to skip chmod).
	 *                                Default false.
	 * @param string|int|false $chown Optional. A user name or number (or false to skip chown).
	 *                                Default false.
	 * @param string|int|false $chgrp Optional. A group name or number (or false to skip chgrp).
	 *                                Default false.
	 * @return bool True on success, false on failure.
	 */
	public function mkdir( $path, $chmod = false, $chown = false, $chgrp = false ) {
		$path = untrailingslashit( $path );

		if ( empty( $path ) ) {
			return false;
		}

		if ( ! ftp_mkdir( $this->link, $path ) ) {
			return false;
		}

		$this->chmod( $path, $chmod );

		return true;
	}

	/**
	 * Deletes a directory.
	 *
	 * @since 2.5.0
	 *
	 * @param string $path      Path to directory.
	 * @param bool   $recursive Optional. Whether to recursively remove files/directories.
	 *                          Default false.
	 * @return bool True on success, false on failure.
	 */
	public function rmdir( $path, $recursive = false ) {
		return $this->delete( $path, $recursive );
	}

	/**
	 * @param string $line
	 * @return array {
	 *     Array of file information.
	 *
	 *     @type string       $name        Name of the file or directory.
	 *     @type string       $perms       *nix representation of permissions.
	 *     @type string       $permsn      Octal representation of permissions.
	 *     @type string|false $number      File number as a string, or false if not available.
	 *     @type string|false $owner       Owner name or ID, or false if not available.
	 *     @type string|false $group       File permissions group, or false if not available.
	 *     @type string|false $size        Size of file in bytes as a string, or false if not available.
	 *     @type string|false $lastmodunix Last modified unix timestamp as a string, or false if not available.
	 *     @type string|false $lastmod     Last modified month (3 letters) and day (without leading 0), or
	 *                                     false if not available.
	 *     @type string|false $time        Last modified time, or false if not available.
	 *     @type string       $type        Type of resource. 'f' for file, 'd' for directory, 'l' for link.
	 *     @type array|false  $files       If a directory and `$recursive` is true, contains another array of files.
	 *                                     False if unable to list directory contents.
	 * }
	 */
	public function parselisting( $line ) {
		static $is_windows = null;

		if ( is_null( $is_windows ) ) {
			$is_windows = stripos( ftp_systype( $this->link ), 'win' ) !== false;
		}

		if ( $is_windows && preg_match( '/([0-9]{2})-([0-9]{2})-([0-9]{2}) +([0-9]{2}):([0-9]{2})(AM|PM) +([0-9]+|<DIR>) +(.+)/', $line, $lucifer ) ) {
			$b = array();

			if ( $lucifer[3] < 70 ) {
				$lucifer[3] += 2000;
			} else {
				$lucifer[3] += 1900; // 4-digit year fix.
			}

			$b['isdir'] = ( '<DIR>' === $lucifer[7] );

			if ( $b['isdir'] ) {
				$b['type'] = 'd';
			} else {
				$b['type'] = 'f';
			}

			$b['size']   = $lucifer[7];
			$b['month']  = $lucifer[1];
			$b['day']    = $lucifer[2];
			$b['year']   = $lucifer[3];
			$b['hour']   = $lucifer[4];
			$b['minute'] = $lucifer[5];
			$b['time']   = mktime( $lucifer[4] + ( strcasecmp( $lucifer[6], 'PM' ) === 0 ? 12 : 0 ), $lucifer[5], 0, $lucifer[1], $lucifer[2], $lucifer[3] );
			$b['am/pm']  = $lucifer[6];
			$b['name']   = $lucifer[8];
		} elseif ( ! $is_windows ) {
			$lucifer = preg_split( '/[ ]/', $line, 9, PREG_SPLIT_NO_EMPTY );

			if ( $lucifer ) {
				// echo $line."\n";
				$lcount = count( $lucifer );

				if ( $lcount < 8 ) {
					return '';
				}

				$b           = array();
				$b['isdir']  = 'd' === $lucifer[0][0];
				$b['islink'] = 'l' === $lucifer[0][0];

				if ( $b['isdir'] ) {
					$b['type'] = 'd';
				} elseif ( $b['islink'] ) {
					$b['type'] = 'l';
				} else {
					$b['type'] = 'f';
				}

				$b['perms']  = $lucifer[0];
				$b['permsn'] = $this->getnumchmodfromh( $b['perms'] );
				$b['number'] = $lucifer[1];
				$b['owner']  = $lucifer[2];
				$b['group']  = $lucifer[3];
				$b['size']   = $lucifer[4];

				if ( 8 === $lcount ) {
					sscanf( $lucifer[5], '%d-%d-%d', $b['year'], $b['month'], $b['day'] );
					sscanf( $lucifer[6], '%d:%d', $b['hour'], $b['minute'] );

					$b['time'] = mktime( $b['hour'], $b['minute'], 0, $b['month'], $b['day'], $b['year'] );
					$b['name'] = $lucifer[7];
				} else {
					$b['month'] = $lucifer[5];
					$b['day']   = $lucifer[6];

					if ( preg_match( '/([0-9]{2}):([0-9]{2})/', $lucifer[7], $l2 ) ) {
						$b['year']   = gmdate( 'Y' );
						$b['hour']   = $l2[1];
						$b['minute'] = $l2[2];
					} else {
						$b['year']   = $lucifer[7];
						$b['hour']   = 0;
						$b['minute'] = 0;
					}

					$b['time'] = strtotime( sprintf( '%d %s %d %02d:%02d', $b['day'], $b['month'], $b['year'], $b['hour'], $b['minute'] ) );
					$b['name'] = $lucifer[8];
				}
			}
		}

		// Replace symlinks formatted as "source -> target" with just the source name.
		if ( isset( $b['islink'] ) && $b['islink'] ) {
			$b['name'] = preg_replace( '/(\s*->\s*.*)$/', '', $b['name'] );
		}

		return $b;
	}

	/**
	 * Gets details for files in a directory or a specific file.
	 *
	 * @since 2.5.0
	 *
	 * @param string $path           Path to directory or file.
	 * @param bool   $include_hidden Optional. Whether to include details of hidden ("." prefixed) files.
	 *                               Default true.
	 * @param bool   $recursive      Optional. Whether to recursively include file details in nested directories.
	 *                               Default false.
	 * @return array|false {
	 *     Array of arrays containing file information. False if unable to list directory contents.
	 *
	 *     @type array ...$0 {
	 *         Array of file information. Note that some elements may not be available on all filesystems.
	 *
	 *         @type string           $name        Name of the file or directory.
	 *         @type string           $perms       *nix representation of permissions.
	 *         @type string           $permsn      Octal representation of permissions.
	 *         @type int|string|false $number      File number. May be a numeric string. False if not available.
	 *         @type string|false     $owner       Owner name or ID, or false if not available.
	 *         @type string|false     $group       File permissions group, or false if not available.
	 *         @type int|string|false $size        Size of file in bytes. May be a numeric string.
	 *                                             False if not available.
	 *         @type int|string|false $lastmodunix Last modified unix timestamp. May be a numeric string.
	 *                                             False if not available.
	 *         @type string|false     $lastmod     Last modified month (3 letters) and day (without leading 0), or
	 *                                             false if not available.
	 *         @type string|false     $time        Last modified time, or false if not available.
	 *         @type string           $type        Type of resource. 'f' for file, 'd' for directory, 'l' for link.
	 *         @type array|false      $files       If a directory and `$recursive` is true, contains another array of
	 *                                             files. False if unable to list directory contents.
	 *     }
	 * }
	 */
	public function dirlist( $path = '.', $include_hidden = true, $recursive = false ) {
		if ( $this->is_file( $path ) ) {
			$limit_file = basename( $path );
			$path       = dirname( $path ) . '/';
		} else {
			$limit_file = false;
		}

		$pwd = ftp_pwd( $this->link );

		if ( ! @ftp_chdir( $this->link, $path ) ) { // Can't change to folder = folder doesn't exist.
			return false;
		}

		$list = ftp_rawlist( $this->link, '-a', false );

		@ftp_chdir( $this->link, $pwd );

		if ( empty( $list ) ) { // Empty array = non-existent folder (real folder will show . at least).
			return false;
		}

		$dirlist = array();

		foreach ( $list as $k => $v ) {
			$entry = $this->parselisting( $v );

			if ( empty( $entry ) ) {
				continue;
			}

			if ( '.' === $entry['name'] || '..' === $entry['name'] ) {
				continue;
			}

			if ( ! $include_hidden && '.' === $entry['name'][0] ) {
				continue;
			}

			if ( $limit_file && $entry['name'] !== $limit_file ) {
				continue;
			}

			$dirlist[ $entry['name'] ] = $entry;
		}

		$path = trailingslashit( $path );
		$ret  = array();

		foreach ( (array) $dirlist as $struc ) {
			if ( 'd' === $struc['type'] ) {
				if ( $recursive ) {
					$struc['files'] = $this->dirlist( $path . $struc['name'], $include_hidden, $recursive );
				} else {
					$struc['files'] = array();
				}
			}

			$ret[ $struc['name'] ] = $struc;
		}

		return $ret;
	}

	/**
	 * Destructor.
	 *
	 * @since 2.5.0
	 */
	public function __destruct() {
		if ( $this->link ) {
			ftp_close( $this->link );
		}
	}
}
class-wp-privacy-policy-content.php000064400000077562151327343360013456 0ustar00<?php
/**
 * WP_Privacy_Policy_Content class.
 *
 * @package WordPress
 * @subpackage Administration
 * @since 4.9.6
 */

#[AllowDynamicProperties]
final class WP_Privacy_Policy_Content {

	private static $policy_content = array();

	/**
	 * Constructor
	 *
	 * @since 4.9.6
	 */
	private function __construct() {}

	/**
	 * Adds content to the postbox shown when editing the privacy policy.
	 *
	 * Plugins and themes should suggest text for inclusion in the site's privacy policy.
	 * The suggested text should contain information about any functionality that affects user privacy,
	 * and will be shown in the Suggested Privacy Policy Content postbox.
	 *
	 * Intended for use from `wp_add_privacy_policy_content()`.
	 *
	 * @since 4.9.6
	 *
	 * @param string $plugin_name The name of the plugin or theme that is suggesting content for the site's privacy policy.
	 * @param string $policy_text The suggested content for inclusion in the policy.
	 */
	public static function add( $plugin_name, $policy_text ) {
		if ( empty( $plugin_name ) || empty( $policy_text ) ) {
			return;
		}

		$data = array(
			'plugin_name' => $plugin_name,
			'policy_text' => $policy_text,
		);

		if ( ! in_array( $data, self::$policy_content, true ) ) {
			self::$policy_content[] = $data;
		}
	}

	/**
	 * Performs a quick check to determine whether any privacy info has changed.
	 *
	 * @since 4.9.6
	 */
	public static function text_change_check() {

		$policy_page_id = (int) get_option( 'wp_page_for_privacy_policy' );

		// The site doesn't have a privacy policy.
		if ( empty( $policy_page_id ) ) {
			return false;
		}

		if ( ! current_user_can( 'edit_post', $policy_page_id ) ) {
			return false;
		}

		$old = (array) get_post_meta( $policy_page_id, '_wp_suggested_privacy_policy_content' );

		// Updates are not relevant if the user has not reviewed any suggestions yet.
		if ( empty( $old ) ) {
			return false;
		}

		$cached = get_option( '_wp_suggested_policy_text_has_changed' );

		/*
		 * When this function is called before `admin_init`, `self::$policy_content`
		 * has not been populated yet, so use the cached result from the last
		 * execution instead.
		 */
		if ( ! did_action( 'admin_init' ) ) {
			return 'changed' === $cached;
		}

		$new = self::$policy_content;

		// Remove the extra values added to the meta.
		foreach ( $old as $key => $data ) {
			if ( ! is_array( $data ) || ! empty( $data['removed'] ) ) {
				unset( $old[ $key ] );
				continue;
			}

			$old[ $key ] = array(
				'plugin_name' => $data['plugin_name'],
				'policy_text' => $data['policy_text'],
			);
		}

		// Normalize the order of texts, to facilitate comparison.
		sort( $old );
		sort( $new );

		/*
		 * The == operator (equal, not identical) was used intentionally.
		 * See https://www.php.net/manual/en/language.operators.array.php
		 */
		if ( $new != $old ) {
			/*
			 * A plugin was activated or deactivated, or some policy text has changed.
			 * Show a notice on the relevant screens to inform the admin.
			 */
			add_action( 'admin_notices', array( 'WP_Privacy_Policy_Content', 'policy_text_changed_notice' ) );
			$state = 'changed';
		} else {
			$state = 'not-changed';
		}

		// Cache the result for use before `admin_init` (see above).
		if ( $cached !== $state ) {
			update_option( '_wp_suggested_policy_text_has_changed', $state, false );
		}

		return 'changed' === $state;
	}

	/**
	 * Outputs a warning when some privacy info has changed.
	 *
	 * @since 4.9.6
	 */
	public static function policy_text_changed_notice() {
		$screen = get_current_screen()->id;

		if ( 'privacy' !== $screen ) {
			return;
		}

		$privacy_message = sprintf(
			/* translators: %s: Privacy Policy Guide URL. */
			__( 'The suggested privacy policy text has changed. Please <a href="%s">review the guide</a> and update your privacy policy.' ),
			esc_url( admin_url( 'privacy-policy-guide.php?tab=policyguide' ) )
		);

		wp_admin_notice(
			$privacy_message,
			array(
				'type'               => 'warning',
				'additional_classes' => array( 'policy-text-updated' ),
				'dismissible'        => true,
			)
		);
	}

	/**
	 * Updates the cached policy info when the policy page is updated.
	 *
	 * @since 4.9.6
	 * @access private
	 *
	 * @param int $post_id The ID of the updated post.
	 */
	public static function _policy_page_updated( $post_id ) {
		$policy_page_id = (int) get_option( 'wp_page_for_privacy_policy' );

		if ( ! $policy_page_id || $policy_page_id !== (int) $post_id ) {
			return;
		}

		// Remove updated|removed status.
		$old          = (array) get_post_meta( $policy_page_id, '_wp_suggested_privacy_policy_content' );
		$done         = array();
		$update_cache = false;

		foreach ( $old as $old_key => $old_data ) {
			if ( ! empty( $old_data['removed'] ) ) {
				// Remove the old policy text.
				$update_cache = true;
				continue;
			}

			if ( ! empty( $old_data['updated'] ) ) {
				// 'updated' is now 'added'.
				$done[]       = array(
					'plugin_name' => $old_data['plugin_name'],
					'policy_text' => $old_data['policy_text'],
					'added'       => $old_data['updated'],
				);
				$update_cache = true;
			} else {
				$done[] = $old_data;
			}
		}

		if ( $update_cache ) {
			delete_post_meta( $policy_page_id, '_wp_suggested_privacy_policy_content' );
			// Update the cache.
			foreach ( $done as $data ) {
				add_post_meta( $policy_page_id, '_wp_suggested_privacy_policy_content', $data );
			}
		}
	}

	/**
	 * Checks for updated, added or removed privacy policy information from plugins.
	 *
	 * Caches the current info in post_meta of the policy page.
	 *
	 * @since 4.9.6
	 *
	 * @return array The privacy policy text/information added by core and plugins.
	 */
	public static function get_suggested_policy_text() {
		$policy_page_id = (int) get_option( 'wp_page_for_privacy_policy' );
		$checked        = array();
		$time           = time();
		$update_cache   = false;
		$new            = self::$policy_content;
		$old            = array();

		if ( $policy_page_id ) {
			$old = (array) get_post_meta( $policy_page_id, '_wp_suggested_privacy_policy_content' );
		}

		// Check for no-changes and updates.
		foreach ( $new as $new_key => $new_data ) {
			foreach ( $old as $old_key => $old_data ) {
				$found = false;

				if ( $new_data['policy_text'] === $old_data['policy_text'] ) {
					// Use the new plugin name in case it was changed, translated, etc.
					if ( $old_data['plugin_name'] !== $new_data['plugin_name'] ) {
						$old_data['plugin_name'] = $new_data['plugin_name'];
						$update_cache            = true;
					}

					// A plugin was re-activated.
					if ( ! empty( $old_data['removed'] ) ) {
						unset( $old_data['removed'] );
						$old_data['added'] = $time;
						$update_cache      = true;
					}

					$checked[] = $old_data;
					$found     = true;
				} elseif ( $new_data['plugin_name'] === $old_data['plugin_name'] ) {
					// The info for the policy was updated.
					$checked[]    = array(
						'plugin_name' => $new_data['plugin_name'],
						'policy_text' => $new_data['policy_text'],
						'updated'     => $time,
					);
					$found        = true;
					$update_cache = true;
				}

				if ( $found ) {
					unset( $new[ $new_key ], $old[ $old_key ] );
					continue 2;
				}
			}
		}

		if ( ! empty( $new ) ) {
			// A plugin was activated.
			foreach ( $new as $new_data ) {
				if ( ! empty( $new_data['plugin_name'] ) && ! empty( $new_data['policy_text'] ) ) {
					$new_data['added'] = $time;
					$checked[]         = $new_data;
				}
			}
			$update_cache = true;
		}

		if ( ! empty( $old ) ) {
			// A plugin was deactivated.
			foreach ( $old as $old_data ) {
				if ( ! empty( $old_data['plugin_name'] ) && ! empty( $old_data['policy_text'] ) ) {
					$data = array(
						'plugin_name' => $old_data['plugin_name'],
						'policy_text' => $old_data['policy_text'],
						'removed'     => $time,
					);

					$checked[] = $data;
				}
			}
			$update_cache = true;
		}

		if ( $update_cache && $policy_page_id ) {
			delete_post_meta( $policy_page_id, '_wp_suggested_privacy_policy_content' );
			// Update the cache.
			foreach ( $checked as $data ) {
				add_post_meta( $policy_page_id, '_wp_suggested_privacy_policy_content', $data );
			}
		}

		return $checked;
	}

	/**
	 * Adds a notice with a link to the guide when editing the privacy policy page.
	 *
	 * @since 4.9.6
	 * @since 5.0.0 The `$post` parameter was made optional.
	 *
	 * @global WP_Post $post Global post object.
	 *
	 * @param WP_Post|null $post The currently edited post. Default null.
	 */
	public static function notice( $post = null ) {
		if ( is_null( $post ) ) {
			global $post;
		} else {
			$post = get_post( $post );
		}

		if ( ! ( $post instanceof WP_Post ) ) {
			return;
		}

		if ( ! current_user_can( 'manage_privacy_options' ) ) {
			return;
		}

		$current_screen = get_current_screen();
		$policy_page_id = (int) get_option( 'wp_page_for_privacy_policy' );

		if ( 'post' !== $current_screen->base || $policy_page_id !== $post->ID ) {
			return;
		}

		$message = __( 'Need help putting together your new Privacy Policy page? Check out the guide for recommendations on what content to include, along with policies suggested by your plugins and theme.' );
		$url     = esc_url( admin_url( 'options-privacy.php?tab=policyguide' ) );
		$label   = __( 'View Privacy Policy Guide.' );

		if ( get_current_screen()->is_block_editor() ) {
			wp_enqueue_script( 'wp-notices' );
			$action = array(
				'url'   => $url,
				'label' => $label,
			);
			wp_add_inline_script(
				'wp-notices',
				sprintf(
					'wp.data.dispatch( "core/notices" ).createWarningNotice( "%s", { actions: [ %s ], isDismissible: false } )',
					$message,
					wp_json_encode( $action )
				),
				'after'
			);
		} else {
			$message .= sprintf(
				' <a href="%s" target="_blank">%s <span class="screen-reader-text">%s</span></a>',
				$url,
				$label,
				/* translators: Hidden accessibility text. */
				__( '(opens in a new tab)' )
			);
			wp_admin_notice(
				$message,
				array(
					'type'               => 'warning',
					'additional_classes' => array( 'inline', 'wp-pp-notice' ),
				)
			);
		}
	}

	/**
	 * Outputs the privacy policy guide together with content from the theme and plugins.
	 *
	 * @since 4.9.6
	 */
	public static function privacy_policy_guide() {

		$content_array = self::get_suggested_policy_text();
		$date_format   = __( 'F j, Y' );

		$i = 0;

		foreach ( $content_array as $section ) {
			++$i;

			$removed = '';
			if ( ! empty( $section['removed'] ) ) {
				$badge_class = ' red';
				$date        = date_i18n( $date_format, $section['removed'] );
				/* translators: %s: Date of plugin deactivation. */
				$badge_title = sprintf( __( 'Removed %s.' ), $date );

				/* translators: %s: Date of plugin deactivation. */
				$removed = sprintf( __( 'You deactivated this plugin on %s and may no longer need this policy.' ), $date );
				$removed = wp_get_admin_notice(
					$removed,
					array(
						'type'               => 'info',
						'additional_classes' => array( 'inline' ),
					)
				);
			} elseif ( ! empty( $section['updated'] ) ) {
				$badge_class = ' blue';
				$date        = date_i18n( $date_format, $section['updated'] );
				/* translators: %s: Date of privacy policy text update. */
				$badge_title = sprintf( __( 'Updated %s.' ), $date );
			}

			$plugin_name = esc_html( $section['plugin_name'] );
			?>
			<h4 class="privacy-settings-accordion-heading">
				<button aria-expanded="false" class="privacy-settings-accordion-trigger" aria-controls="privacy-settings-accordion-block-<?php echo $i; ?>" type="button">
				<span class="title"><?php echo $plugin_name; ?></span>
				<?php if ( ! empty( $section['removed'] ) || ! empty( $section['updated'] ) ) : ?>
				<span class="badge <?php echo $badge_class; ?>"> <?php echo $badge_title; ?></span>
				<?php endif; ?>
				<span class="icon"></span>
			</button>
			</h4>
			<div id="privacy-settings-accordion-block-<?php echo $i; ?>" class="privacy-settings-accordion-panel privacy-text-box-body" hidden="hidden">
				<?php
				echo $removed;
				echo $section['policy_text'];
				?>
				<?php if ( empty( $section['removed'] ) ) : ?>
				<div class="privacy-settings-accordion-actions">
					<span class="success" aria-hidden="true"><?php _e( 'Copied!' ); ?></span>
					<button type="button" class="privacy-text-copy button">
						<span aria-hidden="true"><?php _e( 'Copy suggested policy text to clipboard' ); ?></span>
						<span class="screen-reader-text">
							<?php
							/* translators: Hidden accessibility text. %s: Plugin name. */
							printf( __( 'Copy suggested policy text from %s.' ), $plugin_name );
							?>
						</span>
					</button>
				</div>
				<?php endif; ?>
			</div>
			<?php
		}
	}

	/**
	 * Returns the default suggested privacy policy content.
	 *
	 * @since 4.9.6
	 * @since 5.0.0 Added the `$blocks` parameter.
	 *
	 * @param bool $description Whether to include the descriptions under the section headings. Default false.
	 * @param bool $blocks      Whether to format the content for the block editor. Default true.
	 * @return string The default policy content.
	 */
	public static function get_default_content( $description = false, $blocks = true ) {
		$suggested_text = '<strong class="privacy-policy-tutorial">' . __( 'Suggested text:' ) . ' </strong>';
		$content        = '';
		$strings        = array();

		// Start of the suggested privacy policy text.
		if ( $description ) {
			$strings[] = '<div class="wp-suggested-text">';
		}

		/* translators: Default privacy policy heading. */
		$strings[] = '<h2 class="wp-block-heading">' . __( 'Who we are' ) . '</h2>';

		if ( $description ) {
			/* translators: Privacy policy tutorial. */
			$strings[] = '<p class="privacy-policy-tutorial">' . __( 'In this section you should note your site URL, as well as the name of the company, organization, or individual behind it, and some accurate contact information.' ) . '</p>';
			/* translators: Privacy policy tutorial. */
			$strings[] = '<p class="privacy-policy-tutorial">' . __( 'The amount of information you may be required to show will vary depending on your local or national business regulations. You may, for example, be required to display a physical address, a registered address, or your company registration number.' ) . '</p>';
		} else {
			/* translators: Default privacy policy text. %s: Site URL. */
			$strings[] = '<p>' . $suggested_text . sprintf( __( 'Our website address is: %s.' ), get_bloginfo( 'url', 'display' ) ) . '</p>';
		}

		if ( $description ) {
			/* translators: Default privacy policy heading. */
			$strings[] = '<h2>' . __( 'What personal data we collect and why we collect it' ) . '</h2>';
			/* translators: Privacy policy tutorial. */
			$strings[] = '<p class="privacy-policy-tutorial">' . __( 'In this section you should note what personal data you collect from users and site visitors. This may include personal data, such as name, email address, personal account preferences; transactional data, such as purchase information; and technical data, such as information about cookies.' ) . '</p>';
			/* translators: Privacy policy tutorial. */
			$strings[] = '<p class="privacy-policy-tutorial">' . __( 'You should also note any collection and retention of sensitive personal data, such as data concerning health.' ) . '</p>';
			/* translators: Privacy policy tutorial. */
			$strings[] = '<p class="privacy-policy-tutorial">' . __( 'In addition to listing what personal data you collect, you need to note why you collect it. These explanations must note either the legal basis for your data collection and retention or the active consent the user has given.' ) . '</p>';
			/* translators: Privacy policy tutorial. */
			$strings[] = '<p class="privacy-policy-tutorial">' . __( 'Personal data is not just created by a user&#8217;s interactions with your site. Personal data is also generated from technical processes such as contact forms, comments, cookies, analytics, and third party embeds.' ) . '</p>';
			/* translators: Privacy policy tutorial. */
			$strings[] = '<p class="privacy-policy-tutorial">' . __( 'By default WordPress does not collect any personal data about visitors, and only collects the data shown on the User Profile screen from registered users. However some of your plugins may collect personal data. You should add the relevant information below.' ) . '</p>';
		}

		/* translators: Default privacy policy heading. */
		$strings[] = '<h2 class="wp-block-heading">' . __( 'Comments' ) . '</h2>';

		if ( $description ) {
			/* translators: Privacy policy tutorial. */
			$strings[] = '<p class="privacy-policy-tutorial">' . __( 'In this subsection you should note what information is captured through comments. We have noted the data which WordPress collects by default.' ) . '</p>';
		} else {
			/* translators: Default privacy policy text. */
			$strings[] = '<p>' . $suggested_text . __( 'When visitors leave comments on the site we collect the data shown in the comments form, and also the visitor&#8217;s IP address and browser user agent string to help spam detection.' ) . '</p>';
			/* translators: Default privacy policy text. */
			$strings[] = '<p>' . __( 'An anonymized string created from your email address (also called a hash) may be provided to the Gravatar service to see if you are using it. The Gravatar service privacy policy is available here: https://automattic.com/privacy/. After approval of your comment, your profile picture is visible to the public in the context of your comment.' ) . '</p>';
		}

		/* translators: Default privacy policy heading. */
		$strings[] = '<h2 class="wp-block-heading">' . __( 'Media' ) . '</h2>';

		if ( $description ) {
			/* translators: Privacy policy tutorial. */
			$strings[] = '<p class="privacy-policy-tutorial">' . __( 'In this subsection you should note what information may be disclosed by users who can upload media files. All uploaded files are usually publicly accessible.' ) . '</p>';
		} else {
			/* translators: Default privacy policy text. */
			$strings[] = '<p>' . $suggested_text . __( 'If you upload images to the website, you should avoid uploading images with embedded location data (EXIF GPS) included. Visitors to the website can download and extract any location data from images on the website.' ) . '</p>';
		}

		if ( $description ) {
			/* translators: Default privacy policy heading. */
			$strings[] = '<h2>' . __( 'Contact forms' ) . '</h2>';
			/* translators: Privacy policy tutorial. */
			$strings[] = '<p class="privacy-policy-tutorial">' . __( 'By default, WordPress does not include a contact form. If you use a contact form plugin, use this subsection to note what personal data is captured when someone submits a contact form, and how long you keep it. For example, you may note that you keep contact form submissions for a certain period for customer service purposes, but you do not use the information submitted through them for marketing purposes.' ) . '</p>';
		}

		/* translators: Default privacy policy heading. */
		$strings[] = '<h2 class="wp-block-heading">' . __( 'Cookies' ) . '</h2>';

		if ( $description ) {
			/* translators: Privacy policy tutorial. */
			$strings[] = '<p class="privacy-policy-tutorial">' . __( 'In this subsection you should list the cookies your website uses, including those set by your plugins, social media, and analytics. We have provided the cookies which WordPress installs by default.' ) . '</p>';
		} else {
			/* translators: Default privacy policy text. */
			$strings[] = '<p>' . $suggested_text . __( 'If you leave a comment on our site you may opt-in to saving your name, email address and website in cookies. These are for your convenience so that you do not have to fill in your details again when you leave another comment. These cookies will last for one year.' ) . '</p>';
			/* translators: Default privacy policy text. */
			$strings[] = '<p>' . __( 'If you visit our login page, we will set a temporary cookie to determine if your browser accepts cookies. This cookie contains no personal data and is discarded when you close your browser.' ) . '</p>';
			/* translators: Default privacy policy text. */
			$strings[] = '<p>' . __( 'When you log in, we will also set up several cookies to save your login information and your screen display choices. Login cookies last for two days, and screen options cookies last for a year. If you select &quot;Remember Me&quot;, your login will persist for two weeks. If you log out of your account, the login cookies will be removed.' ) . '</p>';
			/* translators: Default privacy policy text. */
			$strings[] = '<p>' . __( 'If you edit or publish an article, an additional cookie will be saved in your browser. This cookie includes no personal data and simply indicates the post ID of the article you just edited. It expires after 1 day.' ) . '</p>';
		}

		if ( ! $description ) {
			/* translators: Default privacy policy heading. */
			$strings[] = '<h2 class="wp-block-heading">' . __( 'Embedded content from other websites' ) . '</h2>';
			/* translators: Default privacy policy text. */
			$strings[] = '<p>' . $suggested_text . __( 'Articles on this site may include embedded content (e.g. videos, images, articles, etc.). Embedded content from other websites behaves in the exact same way as if the visitor has visited the other website.' ) . '</p>';
			/* translators: Default privacy policy text. */
			$strings[] = '<p>' . __( 'These websites may collect data about you, use cookies, embed additional third-party tracking, and monitor your interaction with that embedded content, including tracking your interaction with the embedded content if you have an account and are logged in to that website.' ) . '</p>';
		}

		if ( $description ) {
			/* translators: Default privacy policy heading. */
			$strings[] = '<h2>' . __( 'Analytics' ) . '</h2>';
			/* translators: Privacy policy tutorial. */
			$strings[] = '<p class="privacy-policy-tutorial">' . __( 'In this subsection you should note what analytics package you use, how users can opt out of analytics tracking, and a link to your analytics provider&#8217;s privacy policy, if any.' ) . '</p>';
			/* translators: Privacy policy tutorial. */
			$strings[] = '<p class="privacy-policy-tutorial">' . __( 'By default WordPress does not collect any analytics data. However, many web hosting accounts collect some anonymous analytics data. You may also have installed a WordPress plugin that provides analytics services. In that case, add information from that plugin here.' ) . '</p>';
		}

		/* translators: Default privacy policy heading. */
		$strings[] = '<h2 class="wp-block-heading">' . __( 'Who we share your data with' ) . '</h2>';

		if ( $description ) {
			/* translators: Privacy policy tutorial. */
			$strings[] = '<p class="privacy-policy-tutorial">' . __( 'In this section you should name and list all third party providers with whom you share site data, including partners, cloud-based services, payment processors, and third party service providers, and note what data you share with them and why. Link to their own privacy policies if possible.' ) . '</p>';
			/* translators: Privacy policy tutorial. */
			$strings[] = '<p class="privacy-policy-tutorial">' . __( 'By default WordPress does not share any personal data with anyone.' ) . '</p>';
		} else {
			/* translators: Default privacy policy text. */
			$strings[] = '<p>' . $suggested_text . __( 'If you request a password reset, your IP address will be included in the reset email.' ) . '</p>';
		}

		/* translators: Default privacy policy heading. */
		$strings[] = '<h2 class="wp-block-heading">' . __( 'How long we retain your data' ) . '</h2>';

		if ( $description ) {
			/* translators: Privacy policy tutorial. */
			$strings[] = '<p class="privacy-policy-tutorial">' . __( 'In this section you should explain how long you retain personal data collected or processed by the website. While it is your responsibility to come up with the schedule of how long you keep each dataset for and why you keep it, that information does need to be listed here. For example, you may want to say that you keep contact form entries for six months, analytics records for a year, and customer purchase records for ten years.' ) . '</p>';
		} else {
			/* translators: Default privacy policy text. */
			$strings[] = '<p>' . $suggested_text . __( 'If you leave a comment, the comment and its metadata are retained indefinitely. This is so we can recognize and approve any follow-up comments automatically instead of holding them in a moderation queue.' ) . '</p>';
			/* translators: Default privacy policy text. */
			$strings[] = '<p>' . __( 'For users that register on our website (if any), we also store the personal information they provide in their user profile. All users can see, edit, or delete their personal information at any time (except they cannot change their username). Website administrators can also see and edit that information.' ) . '</p>';
		}

		/* translators: Default privacy policy heading. */
		$strings[] = '<h2 class="wp-block-heading">' . __( 'What rights you have over your data' ) . '</h2>';

		if ( $description ) {
			/* translators: Privacy policy tutorial. */
			$strings[] = '<p class="privacy-policy-tutorial">' . __( 'In this section you should explain what rights your users have over their data and how they can invoke those rights.' ) . '</p>';
		} else {
			/* translators: Default privacy policy text. */
			$strings[] = '<p>' . $suggested_text . __( 'If you have an account on this site, or have left comments, you can request to receive an exported file of the personal data we hold about you, including any data you have provided to us. You can also request that we erase any personal data we hold about you. This does not include any data we are obliged to keep for administrative, legal, or security purposes.' ) . '</p>';
		}

		/* translators: Default privacy policy heading. */
		$strings[] = '<h2 class="wp-block-heading">' . __( 'Where your data is sent' ) . '</h2>';

		if ( $description ) {
			/* translators: Privacy policy tutorial. */
			$strings[] = '<p class="privacy-policy-tutorial">' . __( 'In this section you should list all transfers of your site data outside the European Union and describe the means by which that data is safeguarded to European data protection standards. This could include your web hosting, cloud storage, or other third party services.' ) . '</p>';
			/* translators: Privacy policy tutorial. */
			$strings[] = '<p class="privacy-policy-tutorial">' . __( 'European data protection law requires data about European residents which is transferred outside the European Union to be safeguarded to the same standards as if the data was in Europe. So in addition to listing where data goes, you should describe how you ensure that these standards are met either by yourself or by your third party providers, whether that is through an agreement such as Privacy Shield, model clauses in your contracts, or binding corporate rules.' ) . '</p>';
		} else {
			/* translators: Default privacy policy text. */
			$strings[] = '<p>' . $suggested_text . __( 'Visitor comments may be checked through an automated spam detection service.' ) . '</p>';
		}

		if ( $description ) {
			/* translators: Default privacy policy heading. */
			$strings[] = '<h2>' . __( 'Contact information' ) . '</h2>';
			/* translators: Privacy policy tutorial. */
			$strings[] = '<p class="privacy-policy-tutorial">' . __( 'In this section you should provide a contact method for privacy-specific concerns. If you are required to have a Data Protection Officer, list their name and full contact details here as well.' ) . '</p>';
		}

		if ( $description ) {
			/* translators: Default privacy policy heading. */
			$strings[] = '<h2>' . __( 'Additional information' ) . '</h2>';
			/* translators: Privacy policy tutorial. */
			$strings[] = '<p class="privacy-policy-tutorial">' . __( 'If you use your site for commercial purposes and you engage in more complex collection or processing of personal data, you should note the following information in your privacy policy in addition to the information we have already discussed.' ) . '</p>';
		}

		if ( $description ) {
			/* translators: Default privacy policy heading. */
			$strings[] = '<h2>' . __( 'How we protect your data' ) . '</h2>';
			/* translators: Privacy policy tutorial. */
			$strings[] = '<p class="privacy-policy-tutorial">' . __( 'In this section you should explain what measures you have taken to protect your users&#8217; data. This could include technical measures such as encryption; security measures such as two factor authentication; and measures such as staff training in data protection. If you have carried out a Privacy Impact Assessment, you can mention it here too.' ) . '</p>';
		}

		if ( $description ) {
			/* translators: Default privacy policy heading. */
			$strings[] = '<h2>' . __( 'What data breach procedures we have in place' ) . '</h2>';
			/* translators: Privacy policy tutorial. */
			$strings[] = '<p class="privacy-policy-tutorial">' . __( 'In this section you should explain what procedures you have in place to deal with data breaches, either potential or real, such as internal reporting systems, contact mechanisms, or bug bounties.' ) . '</p>';
		}

		if ( $description ) {
			/* translators: Default privacy policy heading. */
			$strings[] = '<h2>' . __( 'What third parties we receive data from' ) . '</h2>';
			/* translators: Privacy policy tutorial. */
			$strings[] = '<p class="privacy-policy-tutorial">' . __( 'If your website receives data about users from third parties, including advertisers, this information must be included within the section of your privacy policy dealing with third party data.' ) . '</p>';
		}

		if ( $description ) {
			/* translators: Default privacy policy heading. */
			$strings[] = '<h2>' . __( 'What automated decision making and/or profiling we do with user data' ) . '</h2>';
			/* translators: Privacy policy tutorial. */
			$strings[] = '<p class="privacy-policy-tutorial">' . __( 'If your website provides a service which includes automated decision making - for example, allowing customers to apply for credit, or aggregating their data into an advertising profile - you must note that this is taking place, and include information about how that information is used, what decisions are made with that aggregated data, and what rights users have over decisions made without human intervention.' ) . '</p>';
		}

		if ( $description ) {
			/* translators: Default privacy policy heading. */
			$strings[] = '<h2>' . __( 'Industry regulatory disclosure requirements' ) . '</h2>';
			/* translators: Privacy policy tutorial. */
			$strings[] = '<p class="privacy-policy-tutorial">' . __( 'If you are a member of a regulated industry, or if you are subject to additional privacy laws, you may be required to disclose that information here.' ) . '</p>';
			$strings[] = '</div>';
		}

		if ( $blocks ) {
			foreach ( $strings as $key => $string ) {
				if ( str_starts_with( $string, '<p>' ) ) {
					$strings[ $key ] = "<!-- wp:paragraph -->\n" . $string . "\n<!-- /wp:paragraph -->\n";
				}

				if ( str_starts_with( $string, '<h2 ' ) ) {
					$strings[ $key ] = "<!-- wp:heading -->\n" . $string . "\n<!-- /wp:heading -->\n";
				}
			}
		}

		$content = implode( '', $strings );
		// End of the suggested privacy policy text.

		/**
		 * Filters the default content suggested for inclusion in a privacy policy.
		 *
		 * @since 4.9.6
		 * @since 5.0.0 Added the `$strings`, `$description`, and `$blocks` parameters.
		 * @deprecated 5.7.0 Use wp_add_privacy_policy_content() instead.
		 *
		 * @param string   $content     The default policy content.
		 * @param string[] $strings     An array of privacy policy content strings.
		 * @param bool     $description Whether policy descriptions should be included.
		 * @param bool     $blocks      Whether the content should be formatted for the block editor.
		 */
		return apply_filters_deprecated(
			'wp_get_default_privacy_policy_content',
			array( $content, $strings, $description, $blocks ),
			'5.7.0',
			'wp_add_privacy_policy_content()'
		);
	}

	/**
	 * Adds the suggested privacy policy text to the policy postbox.
	 *
	 * @since 4.9.6
	 */
	public static function add_suggested_content() {
		$content = self::get_default_content( false, false );
		wp_add_privacy_policy_content( __( 'WordPress' ), $content );
	}
}
class-wp-privacy-data-removal-requests-list-table.php000060400000013123151327343360016745 0ustar00<?php
/**
 * List Table API: WP_Privacy_Data_Removal_Requests_List_Table class
 *
 * @package WordPress
 * @subpackage Administration
 * @since 4.9.6
 */

if ( ! class_exists( 'WP_Privacy_Requests_Table' ) ) {
	require_once ABSPATH . 'wp-admin/includes/class-wp-privacy-requests-table.php';
}

/**
 * WP_Privacy_Data_Removal_Requests_List_Table class.
 *
 * @since 4.9.6
 */
class WP_Privacy_Data_Removal_Requests_List_Table extends WP_Privacy_Requests_Table {
	/**
	 * Action name for the requests this table will work with.
	 *
	 * @since 4.9.6
	 *
	 * @var string $request_type Name of action.
	 */
	protected $request_type = 'remove_personal_data';

	/**
	 * Post type for the requests.
	 *
	 * @since 4.9.6
	 *
	 * @var string $post_type The post type.
	 */
	protected $post_type = 'user_request';

	/**
	 * Outputs the Actions column.
	 *
	 * @since 4.9.6
	 *
	 * @param WP_User_Request $item Item being shown.
	 * @return string Email column markup.
	 */
	public function column_email( $item ) {
		$row_actions = array();

		// Allow the administrator to "force remove" the personal data even if confirmation has not yet been received.
		$status      = $item->status;
		$request_id  = $item->ID;
		$row_actions = array();
		if ( 'request-confirmed' !== $status ) {
			/** This filter is documented in wp-admin/includes/ajax-actions.php */
			$erasers       = apply_filters( 'wp_privacy_personal_data_erasers', array() );
			$erasers_count = count( $erasers );
			$nonce         = wp_create_nonce( 'wp-privacy-erase-personal-data-' . $request_id );

			$remove_data_markup = '<span class="remove-personal-data force-remove-personal-data" ' .
				'data-erasers-count="' . esc_attr( $erasers_count ) . '" ' .
				'data-request-id="' . esc_attr( $request_id ) . '" ' .
				'data-nonce="' . esc_attr( $nonce ) .
				'">';

			$remove_data_markup .= '<span class="remove-personal-data-idle"><button type="button" class="button-link remove-personal-data-handle">' . __( 'Force erase personal data' ) . '</button></span>' .
				'<span class="remove-personal-data-processing hidden">' . __( 'Erasing data...' ) . ' <span class="erasure-progress"></span></span>' .
				'<span class="remove-personal-data-success hidden">' . __( 'Erasure completed.' ) . '</span>' .
				'<span class="remove-personal-data-failed hidden">' . __( 'Force erasure has failed.' ) . ' <button type="button" class="button-link remove-personal-data-handle">' . __( 'Retry' ) . '</button></span>';

			$remove_data_markup .= '</span>';

			$row_actions['remove-data'] = $remove_data_markup;
		}

		if ( 'request-completed' !== $status ) {
			$complete_request_markup  = '<span>';
			$complete_request_markup .= sprintf(
				'<a href="%s" class="complete-request" aria-label="%s">%s</a>',
				esc_url(
					wp_nonce_url(
						add_query_arg(
							array(
								'action'     => 'complete',
								'request_id' => array( $request_id ),
							),
							admin_url( 'erase-personal-data.php' )
						),
						'bulk-privacy_requests'
					)
				),
				esc_attr(
					sprintf(
						/* translators: %s: Request email. */
						__( 'Mark export request for &#8220;%s&#8221; as completed.' ),
						$item->email
					)
				),
				__( 'Complete request' )
			);
			$complete_request_markup .= '</span>';
		}

		if ( ! empty( $complete_request_markup ) ) {
			$row_actions['complete-request'] = $complete_request_markup;
		}

		return sprintf( '<a href="%1$s">%2$s</a> %3$s', esc_url( 'mailto:' . $item->email ), $item->email, $this->row_actions( $row_actions ) );
	}

	/**
	 * Outputs the Next steps column.
	 *
	 * @since 4.9.6
	 *
	 * @param WP_User_Request $item Item being shown.
	 */
	public function column_next_steps( $item ) {
		$status = $item->status;

		switch ( $status ) {
			case 'request-pending':
				esc_html_e( 'Waiting for confirmation' );
				break;
			case 'request-confirmed':
				/** This filter is documented in wp-admin/includes/ajax-actions.php */
				$erasers       = apply_filters( 'wp_privacy_personal_data_erasers', array() );
				$erasers_count = count( $erasers );
				$request_id    = $item->ID;
				$nonce         = wp_create_nonce( 'wp-privacy-erase-personal-data-' . $request_id );

				echo '<div class="remove-personal-data" ' .
					'data-force-erase="1" ' .
					'data-erasers-count="' . esc_attr( $erasers_count ) . '" ' .
					'data-request-id="' . esc_attr( $request_id ) . '" ' .
					'data-nonce="' . esc_attr( $nonce ) .
					'">';

				?>
				<span class="remove-personal-data-idle"><button type="button" class="button-link remove-personal-data-handle"><?php _e( 'Erase personal data' ); ?></button></span>
				<span class="remove-personal-data-processing hidden"><?php _e( 'Erasing data...' ); ?> <span class="erasure-progress"></span></span>
				<span class="remove-personal-data-success success-message hidden" ><?php _e( 'Erasure completed.' ); ?></span>
				<span class="remove-personal-data-failed hidden"><?php _e( 'Data erasure has failed.' ); ?> <button type="button" class="button-link remove-personal-data-handle"><?php _e( 'Retry' ); ?></button></span>
				<?php

				echo '</div>';

				break;
			case 'request-failed':
				echo '<button type="submit" class="button-link" name="privacy_action_email_retry[' . $item->ID . ']" id="privacy_action_email_retry[' . $item->ID . ']">' . __( 'Retry' ) . '</button>';
				break;
			case 'request-completed':
				echo '<a href="' . esc_url(
					wp_nonce_url(
						add_query_arg(
							array(
								'action'     => 'delete',
								'request_id' => array( $item->ID ),
							),
							admin_url( 'erase-personal-data.php' )
						),
						'bulk-privacy_requests'
					)
				) . '">' . esc_html__( 'Remove request' ) . '</a>';
				break;
		}
	}
}
class-wp-media-list-table.php000064400000062453151327343360012142 0ustar00<?php
/**
 * List Table API: WP_Media_List_Table class
 *
 * @package WordPress
 * @subpackage Administration
 * @since 3.1.0
 */

/**
 * Core class used to implement displaying media items in a list table.
 *
 * @since 3.1.0
 *
 * @see WP_List_Table
 */
class WP_Media_List_Table extends WP_List_Table {
	/**
	 * Holds the number of pending comments for each post.
	 *
	 * @since 4.4.0
	 * @var array
	 */
	protected $comment_pending_count = array();

	private $detached;

	private $is_trash;

	/**
	 * Constructor.
	 *
	 * @since 3.1.0
	 *
	 * @see WP_List_Table::__construct() for more information on default arguments.
	 *
	 * @param array $args An associative array of arguments.
	 */
	public function __construct( $args = array() ) {
		$this->detached = ( isset( $_REQUEST['attachment-filter'] ) && 'detached' === $_REQUEST['attachment-filter'] );

		$this->modes = array(
			'list' => __( 'List view' ),
			'grid' => __( 'Grid view' ),
		);

		parent::__construct(
			array(
				'plural' => 'media',
				'screen' => isset( $args['screen'] ) ? $args['screen'] : null,
			)
		);
	}

	/**
	 * @return bool
	 */
	public function ajax_user_can() {
		return current_user_can( 'upload_files' );
	}

	/**
	 * @global string   $mode                  List table view mode.
	 * @global WP_Query $wp_query              WordPress Query object.
	 * @global array    $post_mime_types
	 * @global array    $avail_post_mime_types
	 */
	public function prepare_items() {
		global $mode, $wp_query, $post_mime_types, $avail_post_mime_types;

		$mode = empty( $_REQUEST['mode'] ) ? 'list' : $_REQUEST['mode'];

		/*
		 * Exclude attachments scheduled for deletion in the next two hours
		 * if they are for zip packages for interrupted or failed updates.
		 * See File_Upload_Upgrader class.
		 */
		$not_in = array();

		$crons = _get_cron_array();

		if ( is_array( $crons ) ) {
			foreach ( $crons as $cron ) {
				if ( isset( $cron['upgrader_scheduled_cleanup'] ) ) {
					$details = reset( $cron['upgrader_scheduled_cleanup'] );

					if ( ! empty( $details['args'][0] ) ) {
						$not_in[] = (int) $details['args'][0];
					}
				}
			}
		}

		if ( ! empty( $_REQUEST['post__not_in'] ) && is_array( $_REQUEST['post__not_in'] ) ) {
			$not_in = array_merge( array_values( $_REQUEST['post__not_in'] ), $not_in );
		}

		if ( ! empty( $not_in ) ) {
			$_REQUEST['post__not_in'] = $not_in;
		}

		list( $post_mime_types, $avail_post_mime_types ) = wp_edit_attachments_query( $_REQUEST );

		$this->is_trash = isset( $_REQUEST['attachment-filter'] ) && 'trash' === $_REQUEST['attachment-filter'];

		$this->set_pagination_args(
			array(
				'total_items' => $wp_query->found_posts,
				'total_pages' => $wp_query->max_num_pages,
				'per_page'    => $wp_query->query_vars['posts_per_page'],
			)
		);
		if ( $wp_query->posts ) {
			update_post_thumbnail_cache( $wp_query );
			update_post_parent_caches( $wp_query->posts );
		}
	}

	/**
	 * @global array $post_mime_types
	 * @global array $avail_post_mime_types
	 * @return array
	 */
	protected function get_views() {
		global $post_mime_types, $avail_post_mime_types;

		$type_links = array();

		$filter = empty( $_GET['attachment-filter'] ) ? '' : $_GET['attachment-filter'];

		$type_links['all'] = sprintf(
			'<option value=""%s>%s</option>',
			selected( $filter, true, false ),
			__( 'All media items' )
		);

		foreach ( $post_mime_types as $mime_type => $label ) {
			if ( ! wp_match_mime_types( $mime_type, $avail_post_mime_types ) ) {
				continue;
			}

			$selected = selected(
				$filter && str_starts_with( $filter, 'post_mime_type:' ) &&
					wp_match_mime_types( $mime_type, str_replace( 'post_mime_type:', '', $filter ) ),
				true,
				false
			);

			$type_links[ $mime_type ] = sprintf(
				'<option value="post_mime_type:%s"%s>%s</option>',
				esc_attr( $mime_type ),
				$selected,
				$label[0]
			);
		}

		$type_links['detached'] = '<option value="detached"' . ( $this->detached ? ' selected="selected"' : '' ) . '>' . _x( 'Unattached', 'media items' ) . '</option>';

		$type_links['mine'] = sprintf(
			'<option value="mine"%s>%s</option>',
			selected( 'mine' === $filter, true, false ),
			_x( 'Mine', 'media items' )
		);

		if ( $this->is_trash || ( defined( 'MEDIA_TRASH' ) && MEDIA_TRASH ) ) {
			$type_links['trash'] = sprintf(
				'<option value="trash"%s>%s</option>',
				selected( 'trash' === $filter, true, false ),
				_x( 'Trash', 'attachment filter' )
			);
		}

		return $type_links;
	}

	/**
	 * @return array
	 */
	protected function get_bulk_actions() {
		$actions = array();

		if ( MEDIA_TRASH ) {
			if ( $this->is_trash ) {
				$actions['untrash'] = __( 'Restore' );
				$actions['delete']  = __( 'Delete permanently' );
			} else {
				$actions['trash'] = __( 'Move to Trash' );
			}
		} else {
			$actions['delete'] = __( 'Delete permanently' );
		}

		if ( $this->detached ) {
			$actions['attach'] = __( 'Attach' );
		}

		return $actions;
	}

	/**
	 * @param string $which
	 */
	protected function extra_tablenav( $which ) {
		if ( 'bar' !== $which ) {
			return;
		}
		?>
		<div class="actions">
			<?php
			if ( ! $this->is_trash ) {
				$this->months_dropdown( 'attachment' );
			}

			/** This action is documented in wp-admin/includes/class-wp-posts-list-table.php */
			do_action( 'restrict_manage_posts', $this->screen->post_type, $which );

			submit_button( __( 'Filter' ), '', 'filter_action', false, array( 'id' => 'post-query-submit' ) );

			if ( $this->is_trash && $this->has_items()
				&& current_user_can( 'edit_others_posts' )
			) {
				submit_button( __( 'Empty Trash' ), 'apply', 'delete_all', false );
			}
			?>
		</div>
		<?php
	}

	/**
	 * @return string
	 */
	public function current_action() {
		if ( isset( $_REQUEST['found_post_id'] ) && isset( $_REQUEST['media'] ) ) {
			return 'attach';
		}

		if ( isset( $_REQUEST['parent_post_id'] ) && isset( $_REQUEST['media'] ) ) {
			return 'detach';
		}

		if ( isset( $_REQUEST['delete_all'] ) || isset( $_REQUEST['delete_all2'] ) ) {
			return 'delete_all';
		}

		return parent::current_action();
	}

	/**
	 * @return bool
	 */
	public function has_items() {
		return have_posts();
	}

	/**
	 */
	public function no_items() {
		if ( $this->is_trash ) {
			_e( 'No media files found in Trash.' );
		} else {
			_e( 'No media files found.' );
		}
	}

	/**
	 * Overrides parent views to use the filter bar display.
	 *
	 * @global string $mode List table view mode.
	 */
	public function views() {
		global $mode;

		$views = $this->get_views();

		$this->screen->render_screen_reader_content( 'heading_views' );
		?>
		<div class="wp-filter">
			<div class="filter-items">
				<?php $this->view_switcher( $mode ); ?>

				<label for="attachment-filter" class="screen-reader-text">
					<?php
					/* translators: Hidden accessibility text. */
					_e( 'Filter by type' );
					?>
				</label>
				<select class="attachment-filters" name="attachment-filter" id="attachment-filter">
					<?php
					if ( ! empty( $views ) ) {
						foreach ( $views as $class => $view ) {
							echo "\t$view\n";
						}
					}
					?>
				</select>

				<?php
				$this->extra_tablenav( 'bar' );

				/** This filter is documented in wp-admin/includes/class-wp-list-table.php */
				$views = apply_filters( "views_{$this->screen->id}", array() );

				// Back compat for pre-4.0 view links.
				if ( ! empty( $views ) ) {
					echo '<ul class="filter-links">';
					foreach ( $views as $class => $view ) {
						echo "<li class='$class'>$view</li>";
					}
					echo '</ul>';
				}
				?>
			</div>

			<div class="search-form">
				<p class="search-box">
					<label class="screen-reader-text" for="media-search-input">
					<?php
					/* translators: Hidden accessibility text. */
					esc_html_e( 'Search Media' );
					?>
					</label>
					<input type="search" id="media-search-input" class="search" name="s" value="<?php _admin_search_query(); ?>">
					<input id="search-submit" type="submit" class="button" value="<?php esc_attr_e( 'Search Media' ); ?>">
				</p>
			</div>
		</div>
		<?php
	}

	/**
	 * @return string[] Array of column titles keyed by their column name.
	 */
	public function get_columns() {
		$posts_columns       = array();
		$posts_columns['cb'] = '<input type="checkbox" />';
		/* translators: Column name. */
		$posts_columns['title']  = _x( 'File', 'column name' );
		$posts_columns['author'] = __( 'Author' );

		$taxonomies = get_taxonomies_for_attachments( 'objects' );
		$taxonomies = wp_filter_object_list( $taxonomies, array( 'show_admin_column' => true ), 'and', 'name' );

		/**
		 * Filters the taxonomy columns for attachments in the Media list table.
		 *
		 * @since 3.5.0
		 *
		 * @param string[] $taxonomies An array of registered taxonomy names to show for attachments.
		 * @param string   $post_type  The post type. Default 'attachment'.
		 */
		$taxonomies = apply_filters( 'manage_taxonomies_for_attachment_columns', $taxonomies, 'attachment' );
		$taxonomies = array_filter( $taxonomies, 'taxonomy_exists' );

		foreach ( $taxonomies as $taxonomy ) {
			if ( 'category' === $taxonomy ) {
				$column_key = 'categories';
			} elseif ( 'post_tag' === $taxonomy ) {
				$column_key = 'tags';
			} else {
				$column_key = 'taxonomy-' . $taxonomy;
			}

			$posts_columns[ $column_key ] = get_taxonomy( $taxonomy )->labels->name;
		}

		/* translators: Column name. */
		if ( ! $this->detached ) {
			$posts_columns['parent'] = _x( 'Uploaded to', 'column name' );

			if ( post_type_supports( 'attachment', 'comments' ) ) {
				$posts_columns['comments'] = sprintf(
					'<span class="vers comment-grey-bubble" title="%1$s" aria-hidden="true"></span><span class="screen-reader-text">%2$s</span>',
					esc_attr__( 'Comments' ),
					/* translators: Hidden accessibility text. */
					__( 'Comments' )
				);
			}
		}

		/* translators: Column name. */
		$posts_columns['date'] = _x( 'Date', 'column name' );

		/**
		 * Filters the Media list table columns.
		 *
		 * @since 2.5.0
		 *
		 * @param string[] $posts_columns An array of columns displayed in the Media list table.
		 * @param bool     $detached      Whether the list table contains media not attached
		 *                                to any posts. Default true.
		 */
		return apply_filters( 'manage_media_columns', $posts_columns, $this->detached );
	}

	/**
	 * @return array
	 */
	protected function get_sortable_columns() {
		return array(
			'title'    => array( 'title', false, _x( 'File', 'column name' ), __( 'Table ordered by File Name.' ) ),
			'author'   => array( 'author', false, __( 'Author' ), __( 'Table ordered by Author.' ) ),
			'parent'   => array( 'parent', false, _x( 'Uploaded to', 'column name' ), __( 'Table ordered by Uploaded To.' ) ),
			'comments' => array( 'comment_count', __( 'Comments' ), false, __( 'Table ordered by Comments.' ) ),
			'date'     => array( 'date', true, __( 'Date' ), __( 'Table ordered by Date.' ), 'desc' ),
		);
	}

	/**
	 * Handles the checkbox column output.
	 *
	 * @since 4.3.0
	 * @since 5.9.0 Renamed `$post` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param WP_Post $item The current WP_Post object.
	 */
	public function column_cb( $item ) {
		// Restores the more descriptive, specific name for use within this method.
		$post = $item;

		if ( current_user_can( 'edit_post', $post->ID ) ) {
			?>
			<input type="checkbox" name="media[]" id="cb-select-<?php echo $post->ID; ?>" value="<?php echo $post->ID; ?>" />
			<label for="cb-select-<?php echo $post->ID; ?>">
				<span class="screen-reader-text">
				<?php
				/* translators: Hidden accessibility text. %s: Attachment title. */
				printf( __( 'Select %s' ), _draft_or_post_title() );
				?>
				</span>
			</label>
			<?php
		}
	}

	/**
	 * Handles the title column output.
	 *
	 * @since 4.3.0
	 *
	 * @param WP_Post $post The current WP_Post object.
	 */
	public function column_title( $post ) {
		list( $mime ) = explode( '/', $post->post_mime_type );

		$attachment_id = $post->ID;

		if ( has_post_thumbnail( $post ) ) {
			$thumbnail_id = get_post_thumbnail_id( $post );

			if ( ! empty( $thumbnail_id ) ) {
				$attachment_id = $thumbnail_id;
			}
		}

		$title      = _draft_or_post_title();
		$thumb      = wp_get_attachment_image( $attachment_id, array( 60, 60 ), true, array( 'alt' => '' ) );
		$link_start = '';
		$link_end   = '';

		if ( current_user_can( 'edit_post', $post->ID ) && ! $this->is_trash ) {
			$link_start = sprintf(
				'<a href="%s" aria-label="%s">',
				get_edit_post_link( $post->ID ),
				/* translators: %s: Attachment title. */
				esc_attr( sprintf( __( '&#8220;%s&#8221; (Edit)' ), $title ) )
			);
			$link_end = '</a>';
		}

		$class = $thumb ? ' class="has-media-icon"' : '';
		?>
		<strong<?php echo $class; ?>>
			<?php
			echo $link_start;

			if ( $thumb ) :
				?>
				<span class="media-icon <?php echo sanitize_html_class( $mime . '-icon' ); ?>"><?php echo $thumb; ?></span>
				<?php
			endif;

			echo $title . $link_end;

			_media_states( $post );
			?>
		</strong>
		<p class="filename">
			<span class="screen-reader-text">
				<?php
				/* translators: Hidden accessibility text. */
				_e( 'File name:' );
				?>
			</span>
			<?php
			$file = get_attached_file( $post->ID );
			echo esc_html( wp_basename( $file ) );
			?>
		</p>
		<?php
	}

	/**
	 * Handles the author column output.
	 *
	 * @since 4.3.0
	 * @since 6.8.0 Added fallback text when author's name is unknown.
	 *
	 * @param WP_Post $post The current WP_Post object.
	 */
	public function column_author( $post ) {
		$author = get_the_author();

		if ( ! empty( $author ) ) {
			printf(
				'<a href="%s">%s</a>',
				esc_url( add_query_arg( array( 'author' => get_the_author_meta( 'ID' ) ), 'upload.php' ) ),
				esc_html( $author )
			);
		} else {
			echo '<span aria-hidden="true">&#8212;</span><span class="screen-reader-text">' . __( '(no author)' ) . '</span>';
		}
	}

	/**
	 * Handles the description column output.
	 *
	 * @since 4.3.0
	 * @deprecated 6.2.0
	 *
	 * @param WP_Post $post The current WP_Post object.
	 */
	public function column_desc( $post ) {
		_deprecated_function( __METHOD__, '6.2.0' );

		echo has_excerpt() ? $post->post_excerpt : '';
	}

	/**
	 * Handles the date column output.
	 *
	 * @since 4.3.0
	 *
	 * @param WP_Post $post The current WP_Post object.
	 */
	public function column_date( $post ) {
		if ( '0000-00-00 00:00:00' === $post->post_date ) {
			$h_time = __( 'Unpublished' );
		} else {
			$time      = get_post_timestamp( $post );
			$time_diff = time() - $time;

			if ( $time && $time_diff > 0 && $time_diff < DAY_IN_SECONDS ) {
				/* translators: %s: Human-readable time difference. */
				$h_time = sprintf( __( '%s ago' ), human_time_diff( $time ) );
			} else {
				$h_time = get_the_time( __( 'Y/m/d' ), $post );
			}
		}

		/**
		 * Filters the published time of an attachment displayed in the Media list table.
		 *
		 * @since 6.0.0
		 *
		 * @param string  $h_time      The published time.
		 * @param WP_Post $post        Attachment object.
		 * @param string  $column_name The column name.
		 */
		echo apply_filters( 'media_date_column_time', $h_time, $post, 'date' );
	}

	/**
	 * Handles the parent column output.
	 *
	 * @since 4.3.0
	 *
	 * @param WP_Post $post The current WP_Post object.
	 */
	public function column_parent( $post ) {
		$user_can_edit = current_user_can( 'edit_post', $post->ID );

		if ( $post->post_parent > 0 ) {
			$parent = get_post( $post->post_parent );
		} else {
			$parent = false;
		}

		if ( $parent ) {
			$title       = _draft_or_post_title( $post->post_parent );
			$parent_type = get_post_type_object( $parent->post_type );

			if ( $parent_type && $parent_type->show_ui && current_user_can( 'edit_post', $post->post_parent ) ) {
				printf( '<strong><a href="%s">%s</a></strong>', get_edit_post_link( $post->post_parent ), $title );
			} elseif ( $parent_type && current_user_can( 'read_post', $post->post_parent ) ) {
				printf( '<strong>%s</strong>', $title );
			} else {
				_e( '(Private post)' );
			}

			if ( $user_can_edit ) :
				$detach_url = add_query_arg(
					array(
						'parent_post_id' => $post->post_parent,
						'media[]'        => $post->ID,
						'_wpnonce'       => wp_create_nonce( 'bulk-' . $this->_args['plural'] ),
					),
					'upload.php'
				);
				printf(
					'<br /><a href="%s" class="hide-if-no-js detach-from-parent" aria-label="%s">%s</a>',
					$detach_url,
					/* translators: %s: Title of the post the attachment is attached to. */
					esc_attr( sprintf( __( 'Detach from &#8220;%s&#8221;' ), $title ) ),
					__( 'Detach' )
				);
			endif;
		} else {
			_e( '(Unattached)' );
			?>
			<?php
			if ( $user_can_edit ) {
				$title = _draft_or_post_title( $post->post_parent );
				printf(
					'<br /><a href="#the-list" onclick="findPosts.open( \'media[]\', \'%s\' ); return false;" class="hide-if-no-js aria-button-if-js" aria-label="%s">%s</a>',
					$post->ID,
					/* translators: %s: Attachment title. */
					esc_attr( sprintf( __( 'Attach &#8220;%s&#8221; to existing content' ), $title ) ),
					__( 'Attach' )
				);
			}
		}
	}

	/**
	 * Handles the comments column output.
	 *
	 * @since 4.3.0
	 *
	 * @param WP_Post $post The current WP_Post object.
	 */
	public function column_comments( $post ) {
		echo '<div class="post-com-count-wrapper">';

		if ( isset( $this->comment_pending_count[ $post->ID ] ) ) {
			$pending_comments = $this->comment_pending_count[ $post->ID ];
		} else {
			$pending_comments = get_pending_comments_num( $post->ID );
		}

		$this->comments_bubble( $post->ID, $pending_comments );

		echo '</div>';
	}

	/**
	 * Handles output for the default column.
	 *
	 * @since 4.3.0
	 * @since 5.9.0 Renamed `$post` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param WP_Post $item        The current WP_Post object.
	 * @param string  $column_name Current column name.
	 */
	public function column_default( $item, $column_name ) {
		// Restores the more descriptive, specific name for use within this method.
		$post = $item;

		if ( 'categories' === $column_name ) {
			$taxonomy = 'category';
		} elseif ( 'tags' === $column_name ) {
			$taxonomy = 'post_tag';
		} elseif ( str_starts_with( $column_name, 'taxonomy-' ) ) {
			$taxonomy = substr( $column_name, 9 );
		} else {
			$taxonomy = false;
		}

		if ( $taxonomy ) {
			$terms = get_the_terms( $post->ID, $taxonomy );

			if ( is_array( $terms ) ) {
				$output = array();

				foreach ( $terms as $t ) {
					$posts_in_term_qv             = array();
					$posts_in_term_qv['taxonomy'] = $taxonomy;
					$posts_in_term_qv['term']     = $t->slug;

					$output[] = sprintf(
						'<a href="%s">%s</a>',
						esc_url( add_query_arg( $posts_in_term_qv, 'upload.php' ) ),
						esc_html( sanitize_term_field( 'name', $t->name, $t->term_id, $taxonomy, 'display' ) )
					);
				}

				echo implode( wp_get_list_item_separator(), $output );
			} else {
				echo '<span aria-hidden="true">&#8212;</span><span class="screen-reader-text">' . get_taxonomy( $taxonomy )->labels->no_terms . '</span>';
			}

			return;
		}

		/**
		 * Fires for each custom column in the Media list table.
		 *
		 * Custom columns are registered using the {@see 'manage_media_columns'} filter.
		 *
		 * @since 2.5.0
		 *
		 * @param string $column_name Name of the custom column.
		 * @param int    $post_id     Attachment ID.
		 */
		do_action( 'manage_media_custom_column', $column_name, $post->ID );
	}

	/**
	 * Generates the list table rows.
	 *
	 * @since 3.1.0
	 *
	 * @global WP_Post  $post     Global post object.
	 * @global WP_Query $wp_query WordPress Query object.
	 */
	public function display_rows() {
		global $post, $wp_query;

		$post_ids = wp_list_pluck( $wp_query->posts, 'ID' );
		reset( $wp_query->posts );

		$this->comment_pending_count = get_pending_comments_num( $post_ids );

		add_filter( 'the_title', 'esc_html' );

		while ( have_posts() ) :
			the_post();

			if ( $this->is_trash && 'trash' !== $post->post_status
				|| ! $this->is_trash && 'trash' === $post->post_status
			) {
				continue;
			}

			$post_owner = ( get_current_user_id() === (int) $post->post_author ) ? 'self' : 'other';
			?>
			<tr id="post-<?php echo $post->ID; ?>" class="<?php echo trim( ' author-' . $post_owner . ' status-' . $post->post_status ); ?>">
				<?php $this->single_row_columns( $post ); ?>
			</tr>
			<?php
		endwhile;
	}

	/**
	 * Gets the name of the default primary column.
	 *
	 * @since 4.3.0
	 *
	 * @return string Name of the default primary column, in this case, 'title'.
	 */
	protected function get_default_primary_column_name() {
		return 'title';
	}

	/**
	 * @param WP_Post $post
	 * @param string  $att_title
	 * @return array
	 */
	private function _get_row_actions( $post, $att_title ) {
		$actions = array();

		if ( ! $this->is_trash && current_user_can( 'edit_post', $post->ID ) ) {
			$actions['edit'] = sprintf(
				'<a href="%s" aria-label="%s">%s</a>',
				esc_url( get_edit_post_link( $post->ID ) ),
				/* translators: %s: Attachment title. */
				esc_attr( sprintf( __( 'Edit &#8220;%s&#8221;' ), $att_title ) ),
				__( 'Edit' )
			);
		}

		if ( current_user_can( 'delete_post', $post->ID ) ) {
			if ( $this->is_trash ) {
				$actions['untrash'] = sprintf(
					'<a href="%s" class="submitdelete aria-button-if-js" aria-label="%s">%s</a>',
					esc_url( wp_nonce_url( "post.php?action=untrash&amp;post=$post->ID", 'untrash-post_' . $post->ID ) ),
					/* translators: %s: Attachment title. */
					esc_attr( sprintf( __( 'Restore &#8220;%s&#8221; from the Trash' ), $att_title ) ),
					__( 'Restore' )
				);
			} elseif ( EMPTY_TRASH_DAYS && MEDIA_TRASH ) {
				$actions['trash'] = sprintf(
					'<a href="%s" class="submitdelete aria-button-if-js" aria-label="%s">%s</a>',
					esc_url( wp_nonce_url( "post.php?action=trash&amp;post=$post->ID", 'trash-post_' . $post->ID ) ),
					/* translators: %s: Attachment title. */
					esc_attr( sprintf( __( 'Move &#8220;%s&#8221; to the Trash' ), $att_title ) ),
					_x( 'Trash', 'verb' )
				);
			}

			if ( $this->is_trash || ! EMPTY_TRASH_DAYS || ! MEDIA_TRASH ) {
				$show_confirmation = ( ! $this->is_trash && ! MEDIA_TRASH ) ? " onclick='return showNotice.warn();'" : '';

				$actions['delete'] = sprintf(
					'<a href="%s" class="submitdelete aria-button-if-js"%s aria-label="%s">%s</a>',
					esc_url( wp_nonce_url( "post.php?action=delete&amp;post=$post->ID", 'delete-post_' . $post->ID ) ),
					$show_confirmation,
					/* translators: %s: Attachment title. */
					esc_attr( sprintf( __( 'Delete &#8220;%s&#8221; permanently' ), $att_title ) ),
					__( 'Delete Permanently' )
				);
			}
		}

		$attachment_url = wp_get_attachment_url( $post->ID );

		if ( ! $this->is_trash ) {
			$permalink = get_permalink( $post->ID );

			if ( $permalink ) {
				$actions['view'] = sprintf(
					'<a href="%s" aria-label="%s" rel="bookmark">%s</a>',
					esc_url( $permalink ),
					/* translators: %s: Attachment title. */
					esc_attr( sprintf( __( 'View &#8220;%s&#8221;' ), $att_title ) ),
					__( 'View' )
				);
			}

			if ( $attachment_url ) {
				$actions['copy'] = sprintf(
					'<span class="copy-to-clipboard-container"><button type="button" class="button-link copy-attachment-url media-library" data-clipboard-text="%s" aria-label="%s">%s</button><span class="success hidden" aria-hidden="true">%s</span></span>',
					esc_url( $attachment_url ),
					/* translators: %s: Attachment title. */
					esc_attr( sprintf( __( 'Copy &#8220;%s&#8221; URL to clipboard' ), $att_title ) ),
					__( 'Copy URL' ),
					__( 'Copied!' )
				);
			}
		}

		if ( $attachment_url ) {
			$actions['download'] = sprintf(
				'<a href="%s" aria-label="%s" download>%s</a>',
				esc_url( $attachment_url ),
				/* translators: %s: Attachment title. */
				esc_attr( sprintf( __( 'Download &#8220;%s&#8221;' ), $att_title ) ),
				__( 'Download file' )
			);
		}

		if ( $this->detached && current_user_can( 'edit_post', $post->ID ) ) {
			$actions['attach'] = sprintf(
				'<a href="#the-list" onclick="findPosts.open( \'media[]\', \'%s\' ); return false;" class="hide-if-no-js aria-button-if-js" aria-label="%s">%s</a>',
				$post->ID,
				/* translators: %s: Attachment title. */
				esc_attr( sprintf( __( 'Attach &#8220;%s&#8221; to existing content' ), $att_title ) ),
				__( 'Attach' )
			);
		}

		/**
		 * Filters the action links for each attachment in the Media list table.
		 *
		 * @since 2.8.0
		 *
		 * @param string[] $actions  An array of action links for each attachment.
		 *                           Includes 'Edit', 'Delete Permanently', 'View',
		 *                           'Copy URL' and 'Download file'.
		 * @param WP_Post  $post     WP_Post object for the current attachment.
		 * @param bool     $detached Whether the list table contains media not attached
		 *                           to any posts. Default true.
		 */
		return apply_filters( 'media_row_actions', $actions, $post, $this->detached );
	}

	/**
	 * Generates and displays row action links.
	 *
	 * @since 4.3.0
	 * @since 5.9.0 Renamed `$post` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param WP_Post $item        Attachment being acted upon.
	 * @param string  $column_name Current column name.
	 * @param string  $primary     Primary column name.
	 * @return string Row actions output for media attachments, or an empty string
	 *                if the current column is not the primary column.
	 */
	protected function handle_row_actions( $item, $column_name, $primary ) {
		if ( $primary !== $column_name ) {
			return '';
		}

		// Restores the more descriptive, specific name for use within this method.
		$post = $item;

		$att_title = _draft_or_post_title();
		$actions   = $this->_get_row_actions( $post, $att_title );

		return $this->row_actions( $actions );
	}
}
update-core.php000064400000211002151327343360007470 0ustar00<?php
/**
 * WordPress core upgrade functionality.
 *
 * Note: Newly introduced functions and methods cannot be used here.
 * All functions must be present in the previous version being upgraded from
 * as this file is used there too.
 *
 * @package WordPress
 * @subpackage Administration
 * @since 2.7.0
 */

/**
 * Stores files to be deleted.
 *
 * Bundled theme files should not be included in this list.
 *
 * @since 2.7.0
 *
 * @global string[] $_old_files
 * @var string[]
 * @name $_old_files
 */
global $_old_files;

$_old_files = array(
	// 2.0
	'wp-admin/import-b2.php',
	'wp-admin/import-blogger.php',
	'wp-admin/import-greymatter.php',
	'wp-admin/import-livejournal.php',
	'wp-admin/import-mt.php',
	'wp-admin/import-rss.php',
	'wp-admin/import-textpattern.php',
	'wp-admin/quicktags.js',
	'wp-images/fade-butt.png',
	'wp-images/get-firefox.png',
	'wp-images/header-shadow.png',
	'wp-images/smilies',
	'wp-images/wp-small.png',
	'wp-images/wpminilogo.png',
	'wp.php',
	// 2.1
	'wp-admin/edit-form-ajax-cat.php',
	'wp-admin/execute-pings.php',
	'wp-admin/inline-uploading.php',
	'wp-admin/link-categories.php',
	'wp-admin/list-manipulation.js',
	'wp-admin/list-manipulation.php',
	'wp-includes/comment-functions.php',
	'wp-includes/feed-functions.php',
	'wp-includes/functions-compat.php',
	'wp-includes/functions-formatting.php',
	'wp-includes/functions-post.php',
	'wp-includes/js/dbx-key.js',
	'wp-includes/links.php',
	'wp-includes/pluggable-functions.php',
	'wp-includes/template-functions-author.php',
	'wp-includes/template-functions-category.php',
	'wp-includes/template-functions-general.php',
	'wp-includes/template-functions-links.php',
	'wp-includes/template-functions-post.php',
	'wp-includes/wp-l10n.php',
	// 2.2
	'wp-admin/cat-js.php',
	'wp-includes/js/autosave-js.php',
	'wp-includes/js/list-manipulation-js.php',
	'wp-includes/js/wp-ajax-js.php',
	// 2.3
	'wp-admin/admin-db.php',
	'wp-admin/cat.js',
	'wp-admin/categories.js',
	'wp-admin/custom-fields.js',
	'wp-admin/dbx-admin-key.js',
	'wp-admin/edit-comments.js',
	'wp-admin/install-rtl.css',
	'wp-admin/install.css',
	'wp-admin/upgrade-schema.php',
	'wp-admin/upload-functions.php',
	'wp-admin/upload-rtl.css',
	'wp-admin/upload.css',
	'wp-admin/upload.js',
	'wp-admin/users.js',
	'wp-admin/widgets-rtl.css',
	'wp-admin/widgets.css',
	'wp-admin/xfn.js',
	'wp-includes/js/tinymce/license.html',
	// 2.5
	'wp-admin/css/upload.css',
	'wp-admin/images/box-bg-left.gif',
	'wp-admin/images/box-bg-right.gif',
	'wp-admin/images/box-bg.gif',
	'wp-admin/images/box-butt-left.gif',
	'wp-admin/images/box-butt-right.gif',
	'wp-admin/images/box-butt.gif',
	'wp-admin/images/box-head-left.gif',
	'wp-admin/images/box-head-right.gif',
	'wp-admin/images/box-head.gif',
	'wp-admin/images/heading-bg.gif',
	'wp-admin/images/login-bkg-bottom.gif',
	'wp-admin/images/login-bkg-tile.gif',
	'wp-admin/images/notice.gif',
	'wp-admin/images/toggle.gif',
	'wp-admin/includes/upload.php',
	'wp-admin/js/dbx-admin-key.js',
	'wp-admin/js/link-cat.js',
	'wp-admin/profile-update.php',
	'wp-admin/templates.php',
	'wp-includes/js/dbx.js',
	'wp-includes/js/fat.js',
	'wp-includes/js/list-manipulation.js',
	'wp-includes/js/tinymce/langs/en.js',
	'wp-includes/js/tinymce/plugins/directionality/images',
	'wp-includes/js/tinymce/plugins/directionality/langs',
	'wp-includes/js/tinymce/plugins/paste/images',
	'wp-includes/js/tinymce/plugins/paste/jscripts',
	'wp-includes/js/tinymce/plugins/paste/langs',
	'wp-includes/js/tinymce/plugins/wordpress/images',
	'wp-includes/js/tinymce/plugins/wordpress/langs',
	'wp-includes/js/tinymce/plugins/wordpress/wordpress.css',
	'wp-includes/js/tinymce/plugins/wphelp',
	// 2.5.1
	'wp-includes/js/tinymce/tiny_mce_gzip.php',
	// 2.6
	'wp-admin/bookmarklet.php',
	'wp-includes/js/jquery/jquery.dimensions.min.js',
	'wp-includes/js/tinymce/plugins/wordpress/popups.css',
	'wp-includes/js/wp-ajax.js',
	// 2.7
	'wp-admin/css/press-this-ie-rtl.css',
	'wp-admin/css/press-this-ie.css',
	'wp-admin/css/upload-rtl.css',
	'wp-admin/edit-form.php',
	'wp-admin/images/comment-pill.gif',
	'wp-admin/images/comment-stalk-classic.gif',
	'wp-admin/images/comment-stalk-fresh.gif',
	'wp-admin/images/comment-stalk-rtl.gif',
	'wp-admin/images/del.png',
	'wp-admin/images/gear.png',
	'wp-admin/images/media-button-gallery.gif',
	'wp-admin/images/media-buttons.gif',
	'wp-admin/images/postbox-bg.gif',
	'wp-admin/images/tab.png',
	'wp-admin/images/tail.gif',
	'wp-admin/js/forms.js',
	'wp-admin/js/upload.js',
	'wp-admin/link-import.php',
	'wp-includes/images/audio.png',
	'wp-includes/images/css.png',
	'wp-includes/images/default.png',
	'wp-includes/images/doc.png',
	'wp-includes/images/exe.png',
	'wp-includes/images/html.png',
	'wp-includes/images/js.png',
	'wp-includes/images/pdf.png',
	'wp-includes/images/swf.png',
	'wp-includes/images/tar.png',
	'wp-includes/images/text.png',
	'wp-includes/images/video.png',
	'wp-includes/images/zip.png',
	'wp-includes/js/tinymce/tiny_mce_config.php',
	'wp-includes/js/tinymce/tiny_mce_ext.js',
	// 2.8
	'wp-admin/js/users.js',
	'wp-includes/js/swfupload/swfupload_f9.swf',
	'wp-includes/js/tinymce/plugins/autosave',
	'wp-includes/js/tinymce/plugins/paste/css',
	'wp-includes/js/tinymce/utils/mclayer.js',
	'wp-includes/js/tinymce/wordpress.css',
	// 2.9
	'wp-admin/js/page.dev.js',
	'wp-admin/js/page.js',
	'wp-admin/js/set-post-thumbnail-handler.dev.js',
	'wp-admin/js/set-post-thumbnail-handler.js',
	'wp-admin/js/slug.dev.js',
	'wp-admin/js/slug.js',
	'wp-includes/gettext.php',
	'wp-includes/js/tinymce/plugins/wordpress/js',
	'wp-includes/streams.php',
	// MU
	'README.txt',
	'htaccess.dist',
	'index-install.php',
	'wp-admin/css/mu-rtl.css',
	'wp-admin/css/mu.css',
	'wp-admin/images/site-admin.png',
	'wp-admin/includes/mu.php',
	'wp-admin/wpmu-admin.php',
	'wp-admin/wpmu-blogs.php',
	'wp-admin/wpmu-edit.php',
	'wp-admin/wpmu-options.php',
	'wp-admin/wpmu-themes.php',
	'wp-admin/wpmu-upgrade-site.php',
	'wp-admin/wpmu-users.php',
	'wp-includes/images/wordpress-mu.png',
	'wp-includes/wpmu-default-filters.php',
	'wp-includes/wpmu-functions.php',
	'wpmu-settings.php',
	// 3.0
	'wp-admin/categories.php',
	'wp-admin/edit-category-form.php',
	'wp-admin/edit-page-form.php',
	'wp-admin/edit-pages.php',
	'wp-admin/images/admin-header-footer.png',
	'wp-admin/images/browse-happy.gif',
	'wp-admin/images/ico-add.png',
	'wp-admin/images/ico-close.png',
	'wp-admin/images/ico-edit.png',
	'wp-admin/images/ico-viewpage.png',
	'wp-admin/images/fav-top.png',
	'wp-admin/images/screen-options-left.gif',
	'wp-admin/images/wp-logo-vs.gif',
	'wp-admin/images/wp-logo.gif',
	'wp-admin/import',
	'wp-admin/js/wp-gears.dev.js',
	'wp-admin/js/wp-gears.js',
	'wp-admin/options-misc.php',
	'wp-admin/page-new.php',
	'wp-admin/page.php',
	'wp-admin/rtl.css',
	'wp-admin/rtl.dev.css',
	'wp-admin/update-links.php',
	'wp-admin/wp-admin.css',
	'wp-admin/wp-admin.dev.css',
	'wp-includes/js/codepress',
	'wp-includes/js/jquery/autocomplete.dev.js',
	'wp-includes/js/jquery/autocomplete.js',
	'wp-includes/js/jquery/interface.js',
	// Following file added back in 5.1, see #45645.
	//'wp-includes/js/tinymce/wp-tinymce.js',
	// 3.1
	'wp-admin/edit-attachment-rows.php',
	'wp-admin/edit-link-categories.php',
	'wp-admin/edit-link-category-form.php',
	'wp-admin/edit-post-rows.php',
	'wp-admin/images/button-grad-active-vs.png',
	'wp-admin/images/button-grad-vs.png',
	'wp-admin/images/fav-arrow-vs-rtl.gif',
	'wp-admin/images/fav-arrow-vs.gif',
	'wp-admin/images/fav-top-vs.gif',
	'wp-admin/images/list-vs.png',
	'wp-admin/images/screen-options-right-up.gif',
	'wp-admin/images/screen-options-right.gif',
	'wp-admin/images/visit-site-button-grad-vs.gif',
	'wp-admin/images/visit-site-button-grad.gif',
	'wp-admin/link-category.php',
	'wp-admin/sidebar.php',
	'wp-includes/classes.php',
	'wp-includes/js/tinymce/blank.htm',
	'wp-includes/js/tinymce/plugins/media/img',
	'wp-includes/js/tinymce/plugins/safari',
	// 3.2
	'wp-admin/images/logo-login.gif',
	'wp-admin/images/star.gif',
	'wp-admin/js/list-table.dev.js',
	'wp-admin/js/list-table.js',
	'wp-includes/default-embeds.php',
	// 3.3
	'wp-admin/css/colors-classic-rtl.css',
	'wp-admin/css/colors-classic-rtl.dev.css',
	'wp-admin/css/colors-fresh-rtl.css',
	'wp-admin/css/colors-fresh-rtl.dev.css',
	'wp-admin/css/dashboard-rtl.dev.css',
	'wp-admin/css/dashboard.dev.css',
	'wp-admin/css/global-rtl.css',
	'wp-admin/css/global-rtl.dev.css',
	'wp-admin/css/global.css',
	'wp-admin/css/global.dev.css',
	'wp-admin/css/install-rtl.dev.css',
	'wp-admin/css/login-rtl.dev.css',
	'wp-admin/css/login.dev.css',
	'wp-admin/css/ms.css',
	'wp-admin/css/ms.dev.css',
	'wp-admin/css/nav-menu-rtl.css',
	'wp-admin/css/nav-menu-rtl.dev.css',
	'wp-admin/css/nav-menu.css',
	'wp-admin/css/nav-menu.dev.css',
	'wp-admin/css/plugin-install-rtl.css',
	'wp-admin/css/plugin-install-rtl.dev.css',
	'wp-admin/css/plugin-install.css',
	'wp-admin/css/plugin-install.dev.css',
	'wp-admin/css/press-this-rtl.dev.css',
	'wp-admin/css/press-this.dev.css',
	'wp-admin/css/theme-editor-rtl.css',
	'wp-admin/css/theme-editor-rtl.dev.css',
	'wp-admin/css/theme-editor.css',
	'wp-admin/css/theme-editor.dev.css',
	'wp-admin/css/theme-install-rtl.css',
	'wp-admin/css/theme-install-rtl.dev.css',
	'wp-admin/css/theme-install.css',
	'wp-admin/css/theme-install.dev.css',
	'wp-admin/css/widgets-rtl.dev.css',
	'wp-admin/css/widgets.dev.css',
	'wp-admin/includes/internal-linking.php',
	'wp-includes/images/admin-bar-sprite-rtl.png',
	'wp-includes/js/jquery/ui.button.js',
	'wp-includes/js/jquery/ui.core.js',
	'wp-includes/js/jquery/ui.dialog.js',
	'wp-includes/js/jquery/ui.draggable.js',
	'wp-includes/js/jquery/ui.droppable.js',
	'wp-includes/js/jquery/ui.mouse.js',
	'wp-includes/js/jquery/ui.position.js',
	'wp-includes/js/jquery/ui.resizable.js',
	'wp-includes/js/jquery/ui.selectable.js',
	'wp-includes/js/jquery/ui.sortable.js',
	'wp-includes/js/jquery/ui.tabs.js',
	'wp-includes/js/jquery/ui.widget.js',
	'wp-includes/js/l10n.dev.js',
	'wp-includes/js/l10n.js',
	'wp-includes/js/tinymce/plugins/wplink/css',
	'wp-includes/js/tinymce/plugins/wplink/img',
	'wp-includes/js/tinymce/plugins/wplink/js',
	// Don't delete, yet: 'wp-rss.php',
	// Don't delete, yet: 'wp-rdf.php',
	// Don't delete, yet: 'wp-rss2.php',
	// Don't delete, yet: 'wp-commentsrss2.php',
	// Don't delete, yet: 'wp-atom.php',
	// Don't delete, yet: 'wp-feed.php',
	// 3.4
	'wp-admin/images/gray-star.png',
	'wp-admin/images/logo-login.png',
	'wp-admin/images/star.png',
	'wp-admin/index-extra.php',
	'wp-admin/network/index-extra.php',
	'wp-admin/user/index-extra.php',
	'wp-includes/css/editor-buttons.css',
	'wp-includes/css/editor-buttons.dev.css',
	'wp-includes/js/tinymce/plugins/paste/blank.htm',
	'wp-includes/js/tinymce/plugins/wordpress/css',
	'wp-includes/js/tinymce/plugins/wordpress/editor_plugin.dev.js',
	'wp-includes/js/tinymce/plugins/wpdialogs/editor_plugin.dev.js',
	'wp-includes/js/tinymce/plugins/wpeditimage/editor_plugin.dev.js',
	'wp-includes/js/tinymce/plugins/wpgallery/editor_plugin.dev.js',
	'wp-includes/js/tinymce/plugins/wplink/editor_plugin.dev.js',
	// Don't delete, yet: 'wp-pass.php',
	// Don't delete, yet: 'wp-register.php',
	// 3.5
	'wp-admin/gears-manifest.php',
	'wp-admin/includes/manifest.php',
	'wp-admin/images/archive-link.png',
	'wp-admin/images/blue-grad.png',
	'wp-admin/images/button-grad-active.png',
	'wp-admin/images/button-grad.png',
	'wp-admin/images/ed-bg-vs.gif',
	'wp-admin/images/ed-bg.gif',
	'wp-admin/images/fade-butt.png',
	'wp-admin/images/fav-arrow-rtl.gif',
	'wp-admin/images/fav-arrow.gif',
	'wp-admin/images/fav-vs.png',
	'wp-admin/images/fav.png',
	'wp-admin/images/gray-grad.png',
	'wp-admin/images/loading-publish.gif',
	'wp-admin/images/logo-ghost.png',
	'wp-admin/images/logo.gif',
	'wp-admin/images/menu-arrow-frame-rtl.png',
	'wp-admin/images/menu-arrow-frame.png',
	'wp-admin/images/menu-arrows.gif',
	'wp-admin/images/menu-bits-rtl-vs.gif',
	'wp-admin/images/menu-bits-rtl.gif',
	'wp-admin/images/menu-bits-vs.gif',
	'wp-admin/images/menu-bits.gif',
	'wp-admin/images/menu-dark-rtl-vs.gif',
	'wp-admin/images/menu-dark-rtl.gif',
	'wp-admin/images/menu-dark-vs.gif',
	'wp-admin/images/menu-dark.gif',
	'wp-admin/images/required.gif',
	'wp-admin/images/screen-options-toggle-vs.gif',
	'wp-admin/images/screen-options-toggle.gif',
	'wp-admin/images/toggle-arrow-rtl.gif',
	'wp-admin/images/toggle-arrow.gif',
	'wp-admin/images/upload-classic.png',
	'wp-admin/images/upload-fresh.png',
	'wp-admin/images/white-grad-active.png',
	'wp-admin/images/white-grad.png',
	'wp-admin/images/widgets-arrow-vs.gif',
	'wp-admin/images/widgets-arrow.gif',
	'wp-admin/images/wpspin_dark.gif',
	'wp-includes/images/upload.png',
	'wp-includes/js/prototype.js',
	'wp-includes/js/scriptaculous',
	'wp-admin/css/wp-admin-rtl.dev.css',
	'wp-admin/css/wp-admin.dev.css',
	'wp-admin/css/media-rtl.dev.css',
	'wp-admin/css/media.dev.css',
	'wp-admin/css/colors-classic.dev.css',
	'wp-admin/css/customize-controls-rtl.dev.css',
	'wp-admin/css/customize-controls.dev.css',
	'wp-admin/css/ie-rtl.dev.css',
	'wp-admin/css/ie.dev.css',
	'wp-admin/css/install.dev.css',
	'wp-admin/css/colors-fresh.dev.css',
	'wp-includes/js/customize-base.dev.js',
	'wp-includes/js/json2.dev.js',
	'wp-includes/js/comment-reply.dev.js',
	'wp-includes/js/customize-preview.dev.js',
	'wp-includes/js/wplink.dev.js',
	'wp-includes/js/tw-sack.dev.js',
	'wp-includes/js/wp-list-revisions.dev.js',
	'wp-includes/js/autosave.dev.js',
	'wp-includes/js/admin-bar.dev.js',
	'wp-includes/js/quicktags.dev.js',
	'wp-includes/js/wp-ajax-response.dev.js',
	'wp-includes/js/wp-pointer.dev.js',
	'wp-includes/js/hoverIntent.dev.js',
	'wp-includes/js/colorpicker.dev.js',
	'wp-includes/js/wp-lists.dev.js',
	'wp-includes/js/customize-loader.dev.js',
	'wp-includes/js/jquery/jquery.table-hotkeys.dev.js',
	'wp-includes/js/jquery/jquery.color.dev.js',
	'wp-includes/js/jquery/jquery.color.js',
	'wp-includes/js/jquery/jquery.hotkeys.dev.js',
	'wp-includes/js/jquery/jquery.form.dev.js',
	'wp-includes/js/jquery/suggest.dev.js',
	'wp-admin/js/xfn.dev.js',
	'wp-admin/js/set-post-thumbnail.dev.js',
	'wp-admin/js/comment.dev.js',
	'wp-admin/js/theme.dev.js',
	'wp-admin/js/cat.dev.js',
	'wp-admin/js/password-strength-meter.dev.js',
	'wp-admin/js/user-profile.dev.js',
	'wp-admin/js/theme-preview.dev.js',
	'wp-admin/js/post.dev.js',
	'wp-admin/js/media-upload.dev.js',
	'wp-admin/js/word-count.dev.js',
	'wp-admin/js/plugin-install.dev.js',
	'wp-admin/js/edit-comments.dev.js',
	'wp-admin/js/media-gallery.dev.js',
	'wp-admin/js/custom-fields.dev.js',
	'wp-admin/js/custom-background.dev.js',
	'wp-admin/js/common.dev.js',
	'wp-admin/js/inline-edit-tax.dev.js',
	'wp-admin/js/gallery.dev.js',
	'wp-admin/js/utils.dev.js',
	'wp-admin/js/widgets.dev.js',
	'wp-admin/js/wp-fullscreen.dev.js',
	'wp-admin/js/nav-menu.dev.js',
	'wp-admin/js/dashboard.dev.js',
	'wp-admin/js/link.dev.js',
	'wp-admin/js/user-suggest.dev.js',
	'wp-admin/js/postbox.dev.js',
	'wp-admin/js/tags.dev.js',
	'wp-admin/js/image-edit.dev.js',
	'wp-admin/js/media.dev.js',
	'wp-admin/js/customize-controls.dev.js',
	'wp-admin/js/inline-edit-post.dev.js',
	'wp-admin/js/categories.dev.js',
	'wp-admin/js/editor.dev.js',
	'wp-includes/js/plupload/handlers.dev.js',
	'wp-includes/js/plupload/wp-plupload.dev.js',
	'wp-includes/js/swfupload/handlers.dev.js',
	'wp-includes/js/jcrop/jquery.Jcrop.dev.js',
	'wp-includes/js/jcrop/jquery.Jcrop.js',
	'wp-includes/js/jcrop/jquery.Jcrop.css',
	'wp-includes/js/imgareaselect/jquery.imgareaselect.dev.js',
	'wp-includes/css/wp-pointer.dev.css',
	'wp-includes/css/editor.dev.css',
	'wp-includes/css/jquery-ui-dialog.dev.css',
	'wp-includes/css/admin-bar-rtl.dev.css',
	'wp-includes/css/admin-bar.dev.css',
	'wp-includes/js/jquery/ui/jquery.effects.clip.min.js',
	'wp-includes/js/jquery/ui/jquery.effects.scale.min.js',
	'wp-includes/js/jquery/ui/jquery.effects.blind.min.js',
	'wp-includes/js/jquery/ui/jquery.effects.core.min.js',
	'wp-includes/js/jquery/ui/jquery.effects.shake.min.js',
	'wp-includes/js/jquery/ui/jquery.effects.fade.min.js',
	'wp-includes/js/jquery/ui/jquery.effects.explode.min.js',
	'wp-includes/js/jquery/ui/jquery.effects.slide.min.js',
	'wp-includes/js/jquery/ui/jquery.effects.drop.min.js',
	'wp-includes/js/jquery/ui/jquery.effects.highlight.min.js',
	'wp-includes/js/jquery/ui/jquery.effects.bounce.min.js',
	'wp-includes/js/jquery/ui/jquery.effects.pulsate.min.js',
	'wp-includes/js/jquery/ui/jquery.effects.transfer.min.js',
	'wp-includes/js/jquery/ui/jquery.effects.fold.min.js',
	'wp-admin/js/utils.js',
	// Added back in 5.3 [45448], see #43895.
	// 'wp-admin/options-privacy.php',
	'wp-app.php',
	'wp-includes/class-wp-atom-server.php',
	// 3.5.2
	'wp-includes/js/swfupload/swfupload-all.js',
	// 3.6
	'wp-admin/js/revisions-js.php',
	'wp-admin/images/screenshots',
	'wp-admin/js/categories.js',
	'wp-admin/js/categories.min.js',
	'wp-admin/js/custom-fields.js',
	'wp-admin/js/custom-fields.min.js',
	// 3.7
	'wp-admin/js/cat.js',
	'wp-admin/js/cat.min.js',
	// 3.8
	'wp-includes/js/thickbox/tb-close-2x.png',
	'wp-includes/js/thickbox/tb-close.png',
	'wp-includes/images/wpmini-blue-2x.png',
	'wp-includes/images/wpmini-blue.png',
	'wp-admin/css/colors-fresh.css',
	'wp-admin/css/colors-classic.css',
	'wp-admin/css/colors-fresh.min.css',
	'wp-admin/css/colors-classic.min.css',
	'wp-admin/js/about.min.js',
	'wp-admin/js/about.js',
	'wp-admin/images/arrows-dark-vs-2x.png',
	'wp-admin/images/wp-logo-vs.png',
	'wp-admin/images/arrows-dark-vs.png',
	'wp-admin/images/wp-logo.png',
	'wp-admin/images/arrows-pr.png',
	'wp-admin/images/arrows-dark.png',
	'wp-admin/images/press-this.png',
	'wp-admin/images/press-this-2x.png',
	'wp-admin/images/arrows-vs-2x.png',
	'wp-admin/images/welcome-icons.png',
	'wp-admin/images/wp-logo-2x.png',
	'wp-admin/images/stars-rtl-2x.png',
	'wp-admin/images/arrows-dark-2x.png',
	'wp-admin/images/arrows-pr-2x.png',
	'wp-admin/images/menu-shadow-rtl.png',
	'wp-admin/images/arrows-vs.png',
	'wp-admin/images/about-search-2x.png',
	'wp-admin/images/bubble_bg-rtl-2x.gif',
	'wp-admin/images/wp-badge-2x.png',
	'wp-admin/images/wordpress-logo-2x.png',
	'wp-admin/images/bubble_bg-rtl.gif',
	'wp-admin/images/wp-badge.png',
	'wp-admin/images/menu-shadow.png',
	'wp-admin/images/about-globe-2x.png',
	'wp-admin/images/welcome-icons-2x.png',
	'wp-admin/images/stars-rtl.png',
	'wp-admin/images/wp-logo-vs-2x.png',
	'wp-admin/images/about-updates-2x.png',
	// 3.9
	'wp-admin/css/colors.css',
	'wp-admin/css/colors.min.css',
	'wp-admin/css/colors-rtl.css',
	'wp-admin/css/colors-rtl.min.css',
	// Following files added back in 4.5, see #36083.
	// 'wp-admin/css/media-rtl.min.css',
	// 'wp-admin/css/media.min.css',
	// 'wp-admin/css/farbtastic-rtl.min.css',
	'wp-admin/images/lock-2x.png',
	'wp-admin/images/lock.png',
	'wp-admin/js/theme-preview.js',
	'wp-admin/js/theme-install.min.js',
	'wp-admin/js/theme-install.js',
	'wp-admin/js/theme-preview.min.js',
	'wp-includes/js/plupload/plupload.html4.js',
	'wp-includes/js/plupload/plupload.html5.js',
	'wp-includes/js/plupload/changelog.txt',
	'wp-includes/js/plupload/plupload.silverlight.js',
	'wp-includes/js/plupload/plupload.flash.js',
	// Added back in 4.9 [41328], see #41755.
	// 'wp-includes/js/plupload/plupload.js',
	'wp-includes/js/tinymce/plugins/spellchecker',
	'wp-includes/js/tinymce/plugins/inlinepopups',
	'wp-includes/js/tinymce/plugins/media/js',
	'wp-includes/js/tinymce/plugins/media/css',
	'wp-includes/js/tinymce/plugins/wordpress/img',
	'wp-includes/js/tinymce/plugins/wpdialogs/js',
	'wp-includes/js/tinymce/plugins/wpeditimage/img',
	'wp-includes/js/tinymce/plugins/wpeditimage/js',
	'wp-includes/js/tinymce/plugins/wpeditimage/css',
	'wp-includes/js/tinymce/plugins/wpgallery/img',
	'wp-includes/js/tinymce/plugins/paste/js',
	'wp-includes/js/tinymce/themes/advanced',
	'wp-includes/js/tinymce/tiny_mce.js',
	'wp-includes/js/tinymce/mark_loaded_src.js',
	'wp-includes/js/tinymce/wp-tinymce-schema.js',
	'wp-includes/js/tinymce/plugins/media/editor_plugin.js',
	'wp-includes/js/tinymce/plugins/media/editor_plugin_src.js',
	'wp-includes/js/tinymce/plugins/media/media.htm',
	'wp-includes/js/tinymce/plugins/wpview/editor_plugin_src.js',
	'wp-includes/js/tinymce/plugins/wpview/editor_plugin.js',
	'wp-includes/js/tinymce/plugins/directionality/editor_plugin.js',
	'wp-includes/js/tinymce/plugins/directionality/editor_plugin_src.js',
	'wp-includes/js/tinymce/plugins/wordpress/editor_plugin.js',
	'wp-includes/js/tinymce/plugins/wordpress/editor_plugin_src.js',
	'wp-includes/js/tinymce/plugins/wpdialogs/editor_plugin_src.js',
	'wp-includes/js/tinymce/plugins/wpdialogs/editor_plugin.js',
	'wp-includes/js/tinymce/plugins/wpeditimage/editimage.html',
	'wp-includes/js/tinymce/plugins/wpeditimage/editor_plugin.js',
	'wp-includes/js/tinymce/plugins/wpeditimage/editor_plugin_src.js',
	'wp-includes/js/tinymce/plugins/fullscreen/editor_plugin_src.js',
	'wp-includes/js/tinymce/plugins/fullscreen/fullscreen.htm',
	'wp-includes/js/tinymce/plugins/fullscreen/editor_plugin.js',
	'wp-includes/js/tinymce/plugins/wplink/editor_plugin_src.js',
	'wp-includes/js/tinymce/plugins/wplink/editor_plugin.js',
	'wp-includes/js/tinymce/plugins/wpgallery/editor_plugin_src.js',
	'wp-includes/js/tinymce/plugins/wpgallery/editor_plugin.js',
	'wp-includes/js/tinymce/plugins/tabfocus/editor_plugin.js',
	'wp-includes/js/tinymce/plugins/tabfocus/editor_plugin_src.js',
	'wp-includes/js/tinymce/plugins/paste/editor_plugin.js',
	'wp-includes/js/tinymce/plugins/paste/pasteword.htm',
	'wp-includes/js/tinymce/plugins/paste/editor_plugin_src.js',
	'wp-includes/js/tinymce/plugins/paste/pastetext.htm',
	'wp-includes/js/tinymce/langs/wp-langs.php',
	// 4.1
	'wp-includes/js/jquery/ui/jquery.ui.accordion.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.autocomplete.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.button.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.core.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.datepicker.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.dialog.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.draggable.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.droppable.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.effect-blind.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.effect-bounce.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.effect-clip.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.effect-drop.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.effect-explode.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.effect-fade.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.effect-fold.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.effect-highlight.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.effect-pulsate.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.effect-scale.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.effect-shake.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.effect-slide.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.effect-transfer.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.effect.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.menu.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.mouse.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.position.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.progressbar.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.resizable.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.selectable.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.slider.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.sortable.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.spinner.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.tabs.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.tooltip.min.js',
	'wp-includes/js/jquery/ui/jquery.ui.widget.min.js',
	'wp-includes/js/tinymce/skins/wordpress/images/dashicon-no-alt.png',
	// 4.3
	'wp-admin/js/wp-fullscreen.js',
	'wp-admin/js/wp-fullscreen.min.js',
	'wp-includes/js/tinymce/wp-mce-help.php',
	'wp-includes/js/tinymce/plugins/wpfullscreen',
	// 4.5
	'wp-includes/theme-compat/comments-popup.php',
	// 4.6
	'wp-admin/includes/class-wp-automatic-upgrader.php', // Wrong file name, see #37628.
	// 4.8
	'wp-includes/js/tinymce/plugins/wpembed',
	'wp-includes/js/tinymce/plugins/media/moxieplayer.swf',
	'wp-includes/js/tinymce/skins/lightgray/fonts/readme.md',
	'wp-includes/js/tinymce/skins/lightgray/fonts/tinymce-small.json',
	'wp-includes/js/tinymce/skins/lightgray/fonts/tinymce.json',
	'wp-includes/js/tinymce/skins/lightgray/skin.ie7.min.css',
	// 4.9
	'wp-admin/css/press-this-editor-rtl.css',
	'wp-admin/css/press-this-editor-rtl.min.css',
	'wp-admin/css/press-this-editor.css',
	'wp-admin/css/press-this-editor.min.css',
	'wp-admin/css/press-this-rtl.css',
	'wp-admin/css/press-this-rtl.min.css',
	'wp-admin/css/press-this.css',
	'wp-admin/css/press-this.min.css',
	'wp-admin/includes/class-wp-press-this.php',
	'wp-admin/js/bookmarklet.js',
	'wp-admin/js/bookmarklet.min.js',
	'wp-admin/js/press-this.js',
	'wp-admin/js/press-this.min.js',
	'wp-includes/js/mediaelement/background.png',
	'wp-includes/js/mediaelement/bigplay.png',
	'wp-includes/js/mediaelement/bigplay.svg',
	'wp-includes/js/mediaelement/controls.png',
	'wp-includes/js/mediaelement/controls.svg',
	'wp-includes/js/mediaelement/flashmediaelement.swf',
	'wp-includes/js/mediaelement/froogaloop.min.js',
	'wp-includes/js/mediaelement/jumpforward.png',
	'wp-includes/js/mediaelement/loading.gif',
	'wp-includes/js/mediaelement/silverlightmediaelement.xap',
	'wp-includes/js/mediaelement/skipback.png',
	'wp-includes/js/plupload/plupload.flash.swf',
	'wp-includes/js/plupload/plupload.full.min.js',
	'wp-includes/js/plupload/plupload.silverlight.xap',
	'wp-includes/js/swfupload/plugins',
	'wp-includes/js/swfupload/swfupload.swf',
	// 4.9.2
	'wp-includes/js/mediaelement/lang',
	'wp-includes/js/mediaelement/mediaelement-flash-audio-ogg.swf',
	'wp-includes/js/mediaelement/mediaelement-flash-audio.swf',
	'wp-includes/js/mediaelement/mediaelement-flash-video-hls.swf',
	'wp-includes/js/mediaelement/mediaelement-flash-video-mdash.swf',
	'wp-includes/js/mediaelement/mediaelement-flash-video.swf',
	'wp-includes/js/mediaelement/renderers/dailymotion.js',
	'wp-includes/js/mediaelement/renderers/dailymotion.min.js',
	'wp-includes/js/mediaelement/renderers/facebook.js',
	'wp-includes/js/mediaelement/renderers/facebook.min.js',
	'wp-includes/js/mediaelement/renderers/soundcloud.js',
	'wp-includes/js/mediaelement/renderers/soundcloud.min.js',
	'wp-includes/js/mediaelement/renderers/twitch.js',
	'wp-includes/js/mediaelement/renderers/twitch.min.js',
	// 5.0
	'wp-includes/js/codemirror/jshint.js',
	// 5.1
	'wp-includes/js/tinymce/wp-tinymce.js.gz',
	// 5.3
	'wp-includes/js/wp-a11y.js',     // Moved to: wp-includes/js/dist/a11y.js
	'wp-includes/js/wp-a11y.min.js', // Moved to: wp-includes/js/dist/a11y.min.js
	// 5.4
	'wp-admin/js/wp-fullscreen-stub.js',
	'wp-admin/js/wp-fullscreen-stub.min.js',
	// 5.5
	'wp-admin/css/ie.css',
	'wp-admin/css/ie.min.css',
	'wp-admin/css/ie-rtl.css',
	'wp-admin/css/ie-rtl.min.css',
	// 5.6
	'wp-includes/js/jquery/ui/position.min.js',
	'wp-includes/js/jquery/ui/widget.min.js',
	// 5.7
	'wp-includes/blocks/classic/block.json',
	// 5.8
	'wp-admin/images/freedoms.png',
	'wp-admin/images/privacy.png',
	'wp-admin/images/about-badge.svg',
	'wp-admin/images/about-color-palette.svg',
	'wp-admin/images/about-color-palette-vert.svg',
	'wp-admin/images/about-header-brushes.svg',
	'wp-includes/block-patterns/large-header.php',
	'wp-includes/block-patterns/heading-paragraph.php',
	'wp-includes/block-patterns/quote.php',
	'wp-includes/block-patterns/text-three-columns-buttons.php',
	'wp-includes/block-patterns/two-buttons.php',
	'wp-includes/block-patterns/two-images.php',
	'wp-includes/block-patterns/three-buttons.php',
	'wp-includes/block-patterns/text-two-columns-with-images.php',
	'wp-includes/block-patterns/text-two-columns.php',
	'wp-includes/block-patterns/large-header-button.php',
	'wp-includes/blocks/subhead',
	'wp-includes/css/dist/editor/editor-styles.css',
	'wp-includes/css/dist/editor/editor-styles.min.css',
	'wp-includes/css/dist/editor/editor-styles-rtl.css',
	'wp-includes/css/dist/editor/editor-styles-rtl.min.css',
	// 5.9
	'wp-includes/blocks/heading/editor.css',
	'wp-includes/blocks/heading/editor.min.css',
	'wp-includes/blocks/heading/editor-rtl.css',
	'wp-includes/blocks/heading/editor-rtl.min.css',
	'wp-includes/blocks/query-title/editor.css',
	'wp-includes/blocks/query-title/editor.min.css',
	'wp-includes/blocks/query-title/editor-rtl.css',
	'wp-includes/blocks/query-title/editor-rtl.min.css',
	/*
	 * Restored in WordPress 6.7
	 *
	 * 'wp-includes/blocks/tag-cloud/editor.css',
	 * 'wp-includes/blocks/tag-cloud/editor.min.css',
	 * 'wp-includes/blocks/tag-cloud/editor-rtl.css',
	 * 'wp-includes/blocks/tag-cloud/editor-rtl.min.css',
	 */
	// 6.1
	'wp-includes/blocks/post-comments.php',
	'wp-includes/blocks/post-comments',
	'wp-includes/blocks/comments-query-loop',
	// 6.3
	'wp-includes/images/wlw',
	'wp-includes/wlwmanifest.xml',
	'wp-includes/random_compat',
	// 6.4
	'wp-includes/navigation-fallback.php',
	'wp-includes/blocks/navigation/view-modal.min.js',
	'wp-includes/blocks/navigation/view-modal.js',
	// 6.5
	'wp-includes/ID3/license.commercial.txt',
	'wp-includes/blocks/query/style-rtl.min.css',
	'wp-includes/blocks/query/style.min.css',
	'wp-includes/blocks/query/style-rtl.css',
	'wp-includes/blocks/query/style.css',
	'wp-admin/images/about-header-privacy.svg',
	'wp-admin/images/about-header-about.svg',
	'wp-admin/images/about-header-credits.svg',
	'wp-admin/images/about-header-freedoms.svg',
	'wp-admin/images/about-header-contribute.svg',
	'wp-admin/images/about-header-background.svg',
	// 6.6
	'wp-includes/blocks/block/editor.css',
	'wp-includes/blocks/block/editor.min.css',
	'wp-includes/blocks/block/editor-rtl.css',
	'wp-includes/blocks/block/editor-rtl.min.css',
	/*
	 * 6.7
	 *
	 * WordPress 6.7 included a SimplePie upgrade that included a major
	 * refactoring of the file structure and library. The old files are
	 * split in to two sections to account for this: files and directories.
	 *
	 * See https://core.trac.wordpress.org/changeset/59141
	 */
	// 6.7 - files
	'wp-includes/js/dist/interactivity-router.asset.php',
	'wp-includes/js/dist/interactivity-router.js',
	'wp-includes/js/dist/interactivity-router.min.js',
	'wp-includes/js/dist/interactivity-router.min.asset.php',
	'wp-includes/js/dist/interactivity.js',
	'wp-includes/js/dist/interactivity.min.js',
	'wp-includes/js/dist/vendor/react-dom.min.js.LICENSE.txt',
	'wp-includes/js/dist/vendor/react.min.js.LICENSE.txt',
	'wp-includes/js/dist/vendor/wp-polyfill-importmap.js',
	'wp-includes/js/dist/vendor/wp-polyfill-importmap.min.js',
	'wp-includes/sodium_compat/src/Core/Base64/Common.php',
	'wp-includes/SimplePie/Author.php',
	'wp-includes/SimplePie/Cache.php',
	'wp-includes/SimplePie/Caption.php',
	'wp-includes/SimplePie/Category.php',
	'wp-includes/SimplePie/Copyright.php',
	'wp-includes/SimplePie/Core.php',
	'wp-includes/SimplePie/Credit.php',
	'wp-includes/SimplePie/Enclosure.php',
	'wp-includes/SimplePie/Exception.php',
	'wp-includes/SimplePie/File.php',
	'wp-includes/SimplePie/gzdecode.php',
	'wp-includes/SimplePie/IRI.php',
	'wp-includes/SimplePie/Item.php',
	'wp-includes/SimplePie/Locator.php',
	'wp-includes/SimplePie/Misc.php',
	'wp-includes/SimplePie/Parser.php',
	'wp-includes/SimplePie/Rating.php',
	'wp-includes/SimplePie/Registry.php',
	'wp-includes/SimplePie/Restriction.php',
	'wp-includes/SimplePie/Sanitize.php',
	'wp-includes/SimplePie/Source.php',
	// 6.7 - directories
	'wp-includes/SimplePie/Cache/',
	'wp-includes/SimplePie/Content/',
	'wp-includes/SimplePie/Decode/',
	'wp-includes/SimplePie/HTTP/',
	'wp-includes/SimplePie/Net/',
	'wp-includes/SimplePie/Parse/',
	'wp-includes/SimplePie/XML/',
	// 6.8
	'wp-includes/blocks/post-content/editor.css',
	'wp-includes/blocks/post-content/editor.min.css',
	'wp-includes/blocks/post-content/editor-rtl.css',
	'wp-includes/blocks/post-content/editor-rtl.min.css',
	'wp-includes/blocks/post-template/editor.css',
	'wp-includes/blocks/post-template/editor.min.css',
	'wp-includes/blocks/post-template/editor-rtl.css',
	'wp-includes/blocks/post-template/editor-rtl.min.css',
	'wp-includes/js/dist/undo-manager.js',
	'wp-includes/js/dist/undo-manager.min.js',
	'wp-includes/js/dist/fields.min.js',
	'wp-includes/js/dist/fields.js',
);

/**
 * Stores Requests files to be preloaded and deleted.
 *
 * For classes/interfaces, use the class/interface name
 * as the array key.
 *
 * All other files/directories should not have a key.
 *
 * @since 6.2.0
 *
 * @global string[] $_old_requests_files
 * @var string[]
 * @name $_old_requests_files
 */
global $_old_requests_files;

$_old_requests_files = array(
	// Interfaces.
	'Requests_Auth'                              => 'wp-includes/Requests/Auth.php',
	'Requests_Hooker'                            => 'wp-includes/Requests/Hooker.php',
	'Requests_Proxy'                             => 'wp-includes/Requests/Proxy.php',
	'Requests_Transport'                         => 'wp-includes/Requests/Transport.php',

	// Classes.
	'Requests_Auth_Basic'                        => 'wp-includes/Requests/Auth/Basic.php',
	'Requests_Cookie_Jar'                        => 'wp-includes/Requests/Cookie/Jar.php',
	'Requests_Exception_HTTP'                    => 'wp-includes/Requests/Exception/HTTP.php',
	'Requests_Exception_Transport'               => 'wp-includes/Requests/Exception/Transport.php',
	'Requests_Exception_HTTP_304'                => 'wp-includes/Requests/Exception/HTTP/304.php',
	'Requests_Exception_HTTP_305'                => 'wp-includes/Requests/Exception/HTTP/305.php',
	'Requests_Exception_HTTP_306'                => 'wp-includes/Requests/Exception/HTTP/306.php',
	'Requests_Exception_HTTP_400'                => 'wp-includes/Requests/Exception/HTTP/400.php',
	'Requests_Exception_HTTP_401'                => 'wp-includes/Requests/Exception/HTTP/401.php',
	'Requests_Exception_HTTP_402'                => 'wp-includes/Requests/Exception/HTTP/402.php',
	'Requests_Exception_HTTP_403'                => 'wp-includes/Requests/Exception/HTTP/403.php',
	'Requests_Exception_HTTP_404'                => 'wp-includes/Requests/Exception/HTTP/404.php',
	'Requests_Exception_HTTP_405'                => 'wp-includes/Requests/Exception/HTTP/405.php',
	'Requests_Exception_HTTP_406'                => 'wp-includes/Requests/Exception/HTTP/406.php',
	'Requests_Exception_HTTP_407'                => 'wp-includes/Requests/Exception/HTTP/407.php',
	'Requests_Exception_HTTP_408'                => 'wp-includes/Requests/Exception/HTTP/408.php',
	'Requests_Exception_HTTP_409'                => 'wp-includes/Requests/Exception/HTTP/409.php',
	'Requests_Exception_HTTP_410'                => 'wp-includes/Requests/Exception/HTTP/410.php',
	'Requests_Exception_HTTP_411'                => 'wp-includes/Requests/Exception/HTTP/411.php',
	'Requests_Exception_HTTP_412'                => 'wp-includes/Requests/Exception/HTTP/412.php',
	'Requests_Exception_HTTP_413'                => 'wp-includes/Requests/Exception/HTTP/413.php',
	'Requests_Exception_HTTP_414'                => 'wp-includes/Requests/Exception/HTTP/414.php',
	'Requests_Exception_HTTP_415'                => 'wp-includes/Requests/Exception/HTTP/415.php',
	'Requests_Exception_HTTP_416'                => 'wp-includes/Requests/Exception/HTTP/416.php',
	'Requests_Exception_HTTP_417'                => 'wp-includes/Requests/Exception/HTTP/417.php',
	'Requests_Exception_HTTP_418'                => 'wp-includes/Requests/Exception/HTTP/418.php',
	'Requests_Exception_HTTP_428'                => 'wp-includes/Requests/Exception/HTTP/428.php',
	'Requests_Exception_HTTP_429'                => 'wp-includes/Requests/Exception/HTTP/429.php',
	'Requests_Exception_HTTP_431'                => 'wp-includes/Requests/Exception/HTTP/431.php',
	'Requests_Exception_HTTP_500'                => 'wp-includes/Requests/Exception/HTTP/500.php',
	'Requests_Exception_HTTP_501'                => 'wp-includes/Requests/Exception/HTTP/501.php',
	'Requests_Exception_HTTP_502'                => 'wp-includes/Requests/Exception/HTTP/502.php',
	'Requests_Exception_HTTP_503'                => 'wp-includes/Requests/Exception/HTTP/503.php',
	'Requests_Exception_HTTP_504'                => 'wp-includes/Requests/Exception/HTTP/504.php',
	'Requests_Exception_HTTP_505'                => 'wp-includes/Requests/Exception/HTTP/505.php',
	'Requests_Exception_HTTP_511'                => 'wp-includes/Requests/Exception/HTTP/511.php',
	'Requests_Exception_HTTP_Unknown'            => 'wp-includes/Requests/Exception/HTTP/Unknown.php',
	'Requests_Exception_Transport_cURL'          => 'wp-includes/Requests/Exception/Transport/cURL.php',
	'Requests_Proxy_HTTP'                        => 'wp-includes/Requests/Proxy/HTTP.php',
	'Requests_Response_Headers'                  => 'wp-includes/Requests/Response/Headers.php',
	'Requests_Transport_cURL'                    => 'wp-includes/Requests/Transport/cURL.php',
	'Requests_Transport_fsockopen'               => 'wp-includes/Requests/Transport/fsockopen.php',
	'Requests_Utility_CaseInsensitiveDictionary' => 'wp-includes/Requests/Utility/CaseInsensitiveDictionary.php',
	'Requests_Utility_FilteredIterator'          => 'wp-includes/Requests/Utility/FilteredIterator.php',
	'Requests_Cookie'                            => 'wp-includes/Requests/Cookie.php',
	'Requests_Exception'                         => 'wp-includes/Requests/Exception.php',
	'Requests_Hooks'                             => 'wp-includes/Requests/Hooks.php',
	'Requests_IDNAEncoder'                       => 'wp-includes/Requests/IDNAEncoder.php',
	'Requests_IPv6'                              => 'wp-includes/Requests/IPv6.php',
	'Requests_IRI'                               => 'wp-includes/Requests/IRI.php',
	'Requests_Response'                          => 'wp-includes/Requests/Response.php',
	'Requests_SSL'                               => 'wp-includes/Requests/SSL.php',
	'Requests_Session'                           => 'wp-includes/Requests/Session.php',

	// Directories.
	'wp-includes/Requests/Auth/',
	'wp-includes/Requests/Cookie/',
	'wp-includes/Requests/Exception/HTTP/',
	'wp-includes/Requests/Exception/Transport/',
	'wp-includes/Requests/Exception/',
	'wp-includes/Requests/Proxy/',
	'wp-includes/Requests/Response/',
	'wp-includes/Requests/Transport/',
	'wp-includes/Requests/Utility/',
);

/**
 * Stores new files in wp-content to copy
 *
 * The contents of this array indicate any new bundled plugins/themes which
 * should be installed with the WordPress Upgrade. These items will not be
 * re-installed in future upgrades, this behavior is controlled by the
 * introduced version present here being older than the current installed version.
 *
 * The content of this array should follow the following format:
 * Filename (relative to wp-content) => Introduced version
 * Directories should be noted by suffixing it with a trailing slash (/)
 *
 * @since 3.2.0
 * @since 4.7.0 New themes were not automatically installed for 4.4-4.6 on
 *              upgrade. New themes are now installed again. To disable new
 *              themes from being installed on upgrade, explicitly define
 *              CORE_UPGRADE_SKIP_NEW_BUNDLED as true.
 * @global string[] $_new_bundled_files
 * @var string[]
 * @name $_new_bundled_files
 */
global $_new_bundled_files;

$_new_bundled_files = array(
	'plugins/akismet/'          => '2.0',
	'themes/twentyten/'         => '3.0',
	'themes/twentyeleven/'      => '3.2',
	'themes/twentytwelve/'      => '3.5',
	'themes/twentythirteen/'    => '3.6',
	'themes/twentyfourteen/'    => '3.8',
	'themes/twentyfifteen/'     => '4.1',
	'themes/twentysixteen/'     => '4.4',
	'themes/twentyseventeen/'   => '4.7',
	'themes/twentynineteen/'    => '5.0',
	'themes/twentytwenty/'      => '5.3',
	'themes/twentytwentyone/'   => '5.6',
	'themes/twentytwentytwo/'   => '5.9',
	'themes/twentytwentythree/' => '6.1',
	'themes/twentytwentyfour/'  => '6.4',
	'themes/twentytwentyfive/'  => '6.7',
);

/**
 * Upgrades the core of WordPress.
 *
 * This will create a .maintenance file at the base of the WordPress directory
 * to ensure that people can not access the website, when the files are being
 * copied to their locations.
 *
 * The files in the `$_old_files` list will be removed and the new files
 * copied from the zip file after the database is upgraded.
 *
 * The files in the `$_new_bundled_files` list will be added to the installation
 * if the version is greater than or equal to the old version being upgraded.
 *
 * The steps for the upgrader for after the new release is downloaded and
 * unzipped is:
 *
 *   1. Test unzipped location for select files to ensure that unzipped worked.
 *   2. Create the .maintenance file in current WordPress base.
 *   3. Copy new WordPress directory over old WordPress files.
 *   4. Upgrade WordPress to new version.
 *      1. Copy all files/folders other than wp-content
 *      2. Copy any language files to `WP_LANG_DIR` (which may differ from `WP_CONTENT_DIR`
 *      3. Copy any new bundled themes/plugins to their respective locations
 *   5. Delete new WordPress directory path.
 *   6. Delete .maintenance file.
 *   7. Remove old files.
 *   8. Delete 'update_core' option.
 *
 * There are several areas of failure. For instance if PHP times out before step
 * 6, then you will not be able to access any portion of your site. Also, since
 * the upgrade will not continue where it left off, you will not be able to
 * automatically remove old files and remove the 'update_core' option. This
 * isn't that bad.
 *
 * If the copy of the new WordPress over the old fails, then the worse is that
 * the new WordPress directory will remain.
 *
 * If it is assumed that every file will be copied over, including plugins and
 * themes, then if you edit the default theme, you should rename it, so that
 * your changes remain.
 *
 * @since 2.7.0
 *
 * @global WP_Filesystem_Base $wp_filesystem          WordPress filesystem subclass.
 * @global string[]           $_old_files
 * @global string[]           $_old_requests_files
 * @global string[]           $_new_bundled_files
 * @global wpdb               $wpdb                   WordPress database abstraction object.
 *
 * @param string $from New release unzipped path.
 * @param string $to   Path to old WordPress installation.
 * @return string|WP_Error New WordPress version on success, WP_Error on failure.
 */
function update_core( $from, $to ) {
	global $wp_filesystem, $_old_files, $_old_requests_files, $_new_bundled_files, $wpdb;

	/*
	 * Give core update script an additional 300 seconds (5 minutes)
	 * to finish updating large files when running on slower servers.
	 */
	if ( function_exists( 'set_time_limit' ) ) {
		set_time_limit( 300 );
	}

	/*
	 * Merge the old Requests files and directories into the `$_old_files`.
	 * Then preload these Requests files first, before the files are deleted
	 * and replaced to ensure the code is in memory if needed.
	 */
	$_old_files = array_merge( $_old_files, array_values( $_old_requests_files ) );
	_preload_old_requests_classes_and_interfaces( $to );

	/**
	 * Filters feedback messages displayed during the core update process.
	 *
	 * The filter is first evaluated after the zip file for the latest version
	 * has been downloaded and unzipped. It is evaluated five more times during
	 * the process:
	 *
	 * 1. Before WordPress begins the core upgrade process.
	 * 2. Before Maintenance Mode is enabled.
	 * 3. Before WordPress begins copying over the necessary files.
	 * 4. Before Maintenance Mode is disabled.
	 * 5. Before the database is upgraded.
	 *
	 * @since 2.5.0
	 *
	 * @param string $feedback The core update feedback messages.
	 */
	apply_filters( 'update_feedback', __( 'Verifying the unpacked files&#8230;' ) );

	// Confidence check the unzipped distribution.
	$distro = '';
	$roots  = array( '/wordpress/', '/wordpress-mu/' );

	foreach ( $roots as $root ) {
		if ( $wp_filesystem->exists( $from . $root . 'readme.html' )
			&& $wp_filesystem->exists( $from . $root . 'wp-includes/version.php' )
		) {
			$distro = $root;
			break;
		}
	}

	if ( ! $distro ) {
		$wp_filesystem->delete( $from, true );

		return new WP_Error( 'insane_distro', __( 'The update could not be unpacked' ) );
	}

	/*
	 * Import $wp_version, $required_php_version, $required_php_extensions, and $required_mysql_version from the new version.
	 * DO NOT globalize any variables imported from `version-current.php` in this function.
	 *
	 * BC Note: $wp_filesystem->wp_content_dir() returned unslashed pre-2.8.
	 */
	$versions_file = trailingslashit( $wp_filesystem->wp_content_dir() ) . 'upgrade/version-current.php';

	if ( ! $wp_filesystem->copy( $from . $distro . 'wp-includes/version.php', $versions_file ) ) {
		$wp_filesystem->delete( $from, true );

		return new WP_Error(
			'copy_failed_for_version_file',
			__( 'The update cannot be installed because some files could not be copied. This is usually due to inconsistent file permissions.' ),
			'wp-includes/version.php'
		);
	}

	$wp_filesystem->chmod( $versions_file, FS_CHMOD_FILE );

	/*
	 * `wp_opcache_invalidate()` only exists in WordPress 5.5 or later,
	 * so don't run it when upgrading from older versions.
	 */
	if ( function_exists( 'wp_opcache_invalidate' ) ) {
		wp_opcache_invalidate( $versions_file );
	}

	require WP_CONTENT_DIR . '/upgrade/version-current.php';
	$wp_filesystem->delete( $versions_file );

	$php_version    = PHP_VERSION;
	$mysql_version  = $wpdb->db_version();
	$old_wp_version = $GLOBALS['wp_version']; // The version of WordPress we're updating from.
	/*
	 * Note: str_contains() is not used here, as this file is included
	 * when updating from older WordPress versions, in which case
	 * the polyfills from wp-includes/compat.php may not be available.
	 */
	$development_build = ( false !== strpos( $old_wp_version . $wp_version, '-' ) ); // A dash in the version indicates a development release.
	$php_compat        = version_compare( $php_version, $required_php_version, '>=' );

	if ( file_exists( WP_CONTENT_DIR . '/db.php' ) && empty( $wpdb->is_mysql ) ) {
		$mysql_compat = true;
	} else {
		$mysql_compat = version_compare( $mysql_version, $required_mysql_version, '>=' );
	}

	if ( ! $mysql_compat || ! $php_compat ) {
		$wp_filesystem->delete( $from, true );
	}

	$php_update_message = '';

	if ( function_exists( 'wp_get_update_php_url' ) ) {
		$php_update_message = '</p><p>' . sprintf(
			/* translators: %s: URL to Update PHP page. */
			__( '<a href="%s">Learn more about updating PHP</a>.' ),
			esc_url( wp_get_update_php_url() )
		);

		if ( function_exists( 'wp_get_update_php_annotation' ) ) {
			$annotation = wp_get_update_php_annotation();

			if ( $annotation ) {
				$php_update_message .= '</p><p><em>' . $annotation . '</em>';
			}
		}
	}

	if ( ! $mysql_compat && ! $php_compat ) {
		return new WP_Error(
			'php_mysql_not_compatible',
			sprintf(
				/* translators: 1: WordPress version number, 2: Minimum required PHP version number, 3: Minimum required MySQL version number, 4: Current PHP version number, 5: Current MySQL version number. */
				__( 'The update cannot be installed because WordPress %1$s requires PHP version %2$s or higher and MySQL version %3$s or higher. You are running PHP version %4$s and MySQL version %5$s.' ),
				$wp_version,
				$required_php_version,
				$required_mysql_version,
				$php_version,
				$mysql_version
			) . $php_update_message
		);
	} elseif ( ! $php_compat ) {
		return new WP_Error(
			'php_not_compatible',
			sprintf(
				/* translators: 1: WordPress version number, 2: Minimum required PHP version number, 3: Current PHP version number. */
				__( 'The update cannot be installed because WordPress %1$s requires PHP version %2$s or higher. You are running version %3$s.' ),
				$wp_version,
				$required_php_version,
				$php_version
			) . $php_update_message
		);
	} elseif ( ! $mysql_compat ) {
		return new WP_Error(
			'mysql_not_compatible',
			sprintf(
				/* translators: 1: WordPress version number, 2: Minimum required MySQL version number, 3: Current MySQL version number. */
				__( 'The update cannot be installed because WordPress %1$s requires MySQL version %2$s or higher. You are running version %3$s.' ),
				$wp_version,
				$required_mysql_version,
				$mysql_version
			)
		);
	}

	if ( isset( $required_php_extensions ) && is_array( $required_php_extensions ) ) {
		$missing_extensions = new WP_Error();

		foreach ( $required_php_extensions as $extension ) {
			if ( extension_loaded( $extension ) ) {
				continue;
			}

			$missing_extensions->add(
				"php_not_compatible_{$extension}",
				sprintf(
					/* translators: 1: WordPress version number, 2: The PHP extension name needed. */
					__( 'The update cannot be installed because WordPress %1$s requires the %2$s PHP extension.' ),
					$wp_version,
					$extension
				)
			);
		}

		// Add a warning when required PHP extensions are missing.
		if ( ! empty( $missing_extensions->errors ) ) {
			return $missing_extensions;
		}
	}

	/** This filter is documented in wp-admin/includes/update-core.php */
	apply_filters( 'update_feedback', __( 'Preparing to install the latest version&#8230;' ) );

	/*
	 * Don't copy wp-content, we'll deal with that below.
	 * We also copy version.php last so failed updates report their old version.
	 */
	$skip              = array( 'wp-content', 'wp-includes/version.php' );
	$check_is_writable = array();

	// Check to see which files don't really need updating - only available for 3.7 and higher.
	if ( function_exists( 'get_core_checksums' ) ) {
		// Find the local version of the working directory.
		$working_dir_local = WP_CONTENT_DIR . '/upgrade/' . basename( $from ) . $distro;

		$checksums = get_core_checksums( $wp_version, isset( $wp_local_package ) ? $wp_local_package : 'en_US' );

		if ( is_array( $checksums ) && isset( $checksums[ $wp_version ] ) ) {
			$checksums = $checksums[ $wp_version ]; // Compat code for 3.7-beta2.
		}

		if ( is_array( $checksums ) ) {
			foreach ( $checksums as $file => $checksum ) {
				/*
				 * Note: str_starts_with() is not used here, as this file is included
				 * when updating from older WordPress versions, in which case
				 * the polyfills from wp-includes/compat.php may not be available.
				 */
				if ( 'wp-content' === substr( $file, 0, 10 ) ) {
					continue;
				}

				if ( ! file_exists( ABSPATH . $file ) ) {
					continue;
				}

				if ( ! file_exists( $working_dir_local . $file ) ) {
					continue;
				}

				if ( '.' === dirname( $file )
					&& in_array( pathinfo( $file, PATHINFO_EXTENSION ), array( 'html', 'txt' ), true )
				) {
					continue;
				}

				if ( md5_file( ABSPATH . $file ) === $checksum ) {
					$skip[] = $file;
				} else {
					$check_is_writable[ $file ] = ABSPATH . $file;
				}
			}
		}
	}

	// If we're using the direct method, we can predict write failures that are due to permissions.
	if ( $check_is_writable && 'direct' === $wp_filesystem->method ) {
		$files_writable = array_filter( $check_is_writable, array( $wp_filesystem, 'is_writable' ) );

		if ( $files_writable !== $check_is_writable ) {
			$files_not_writable = array_diff_key( $check_is_writable, $files_writable );

			foreach ( $files_not_writable as $relative_file_not_writable => $file_not_writable ) {
				// If the writable check failed, chmod file to 0644 and try again, same as copy_dir().
				$wp_filesystem->chmod( $file_not_writable, FS_CHMOD_FILE );

				if ( $wp_filesystem->is_writable( $file_not_writable ) ) {
					unset( $files_not_writable[ $relative_file_not_writable ] );
				}
			}

			// Store package-relative paths (the key) of non-writable files in the WP_Error object.
			$error_data = version_compare( $old_wp_version, '3.7-beta2', '>' ) ? array_keys( $files_not_writable ) : '';

			if ( $files_not_writable ) {
				return new WP_Error(
					'files_not_writable',
					__( 'The update cannot be installed because your site is unable to copy some files. This is usually due to inconsistent file permissions.' ),
					implode( ', ', $error_data )
				);
			}
		}
	}

	/** This filter is documented in wp-admin/includes/update-core.php */
	apply_filters( 'update_feedback', __( 'Enabling Maintenance mode&#8230;' ) );

	// Create maintenance file to signal that we are upgrading.
	$maintenance_string = '<?php $upgrading = ' . time() . '; ?>';
	$maintenance_file   = $to . '.maintenance';
	$wp_filesystem->delete( $maintenance_file );
	$wp_filesystem->put_contents( $maintenance_file, $maintenance_string, FS_CHMOD_FILE );

	/** This filter is documented in wp-admin/includes/update-core.php */
	apply_filters( 'update_feedback', __( 'Copying the required files&#8230;' ) );

	// Copy new versions of WP files into place.
	$result = copy_dir( $from . $distro, $to, $skip );

	if ( is_wp_error( $result ) ) {
		$result = new WP_Error(
			$result->get_error_code(),
			$result->get_error_message(),
			substr( $result->get_error_data(), strlen( $to ) )
		);
	}

	// Since we know the core files have copied over, we can now copy the version file.
	if ( ! is_wp_error( $result ) ) {
		if ( ! $wp_filesystem->copy( $from . $distro . 'wp-includes/version.php', $to . 'wp-includes/version.php', true /* overwrite */ ) ) {
			$wp_filesystem->delete( $from, true );
			$result = new WP_Error(
				'copy_failed_for_version_file',
				__( 'The update cannot be installed because your site is unable to copy some files. This is usually due to inconsistent file permissions.' ),
				'wp-includes/version.php'
			);
		}

		$wp_filesystem->chmod( $to . 'wp-includes/version.php', FS_CHMOD_FILE );

		/*
		 * `wp_opcache_invalidate()` only exists in WordPress 5.5 or later,
		 * so don't run it when upgrading from older versions.
		 */
		if ( function_exists( 'wp_opcache_invalidate' ) ) {
			wp_opcache_invalidate( $to . 'wp-includes/version.php' );
		}
	}

	// Check to make sure everything copied correctly, ignoring the contents of wp-content.
	$skip   = array( 'wp-content' );
	$failed = array();

	if ( isset( $checksums ) && is_array( $checksums ) ) {
		foreach ( $checksums as $file => $checksum ) {
			/*
			 * Note: str_starts_with() is not used here, as this file is included
			 * when updating from older WordPress versions, in which case
			 * the polyfills from wp-includes/compat.php may not be available.
			 */
			if ( 'wp-content' === substr( $file, 0, 10 ) ) {
				continue;
			}

			if ( ! file_exists( $working_dir_local . $file ) ) {
				continue;
			}

			if ( '.' === dirname( $file )
				&& in_array( pathinfo( $file, PATHINFO_EXTENSION ), array( 'html', 'txt' ), true )
			) {
				$skip[] = $file;
				continue;
			}

			if ( file_exists( ABSPATH . $file ) && md5_file( ABSPATH . $file ) === $checksum ) {
				$skip[] = $file;
			} else {
				$failed[] = $file;
			}
		}
	}

	// Some files didn't copy properly.
	if ( ! empty( $failed ) ) {
		$total_size = 0;

		foreach ( $failed as $file ) {
			if ( file_exists( $working_dir_local . $file ) ) {
				$total_size += filesize( $working_dir_local . $file );
			}
		}

		/*
		 * If we don't have enough free space, it isn't worth trying again.
		 * Unlikely to be hit due to the check in unzip_file().
		 */
		$available_space = function_exists( 'disk_free_space' ) ? @disk_free_space( ABSPATH ) : false;

		if ( $available_space && $total_size >= $available_space ) {
			$result = new WP_Error( 'disk_full', __( 'There is not enough free disk space to complete the update.' ) );
		} else {
			$result = copy_dir( $from . $distro, $to, $skip );

			if ( is_wp_error( $result ) ) {
				$result = new WP_Error(
					$result->get_error_code() . '_retry',
					$result->get_error_message(),
					substr( $result->get_error_data(), strlen( $to ) )
				);
			}
		}
	}

	/*
	 * Custom content directory needs updating now.
	 * Copy languages.
	 */
	if ( ! is_wp_error( $result ) && $wp_filesystem->is_dir( $from . $distro . 'wp-content/languages' ) ) {
		if ( WP_LANG_DIR !== ABSPATH . WPINC . '/languages' || @is_dir( WP_LANG_DIR ) ) {
			$lang_dir = WP_LANG_DIR;
		} else {
			$lang_dir = WP_CONTENT_DIR . '/languages';
		}
		/*
		 * Note: str_starts_with() is not used here, as this file is included
		 * when updating from older WordPress versions, in which case
		 * the polyfills from wp-includes/compat.php may not be available.
		 */
		// Check if the language directory exists first.
		if ( ! @is_dir( $lang_dir ) && 0 === strpos( $lang_dir, ABSPATH ) ) {
			// If it's within the ABSPATH we can handle it here, otherwise they're out of luck.
			$wp_filesystem->mkdir( $to . str_replace( ABSPATH, '', $lang_dir ), FS_CHMOD_DIR );
			clearstatcache(); // For FTP, need to clear the stat cache.
		}

		if ( @is_dir( $lang_dir ) ) {
			$wp_lang_dir = $wp_filesystem->find_folder( $lang_dir );

			if ( $wp_lang_dir ) {
				$result = copy_dir( $from . $distro . 'wp-content/languages/', $wp_lang_dir );

				if ( is_wp_error( $result ) ) {
					$result = new WP_Error(
						$result->get_error_code() . '_languages',
						$result->get_error_message(),
						substr( $result->get_error_data(), strlen( $wp_lang_dir ) )
					);
				}
			}
		}
	}

	/** This filter is documented in wp-admin/includes/update-core.php */
	apply_filters( 'update_feedback', __( 'Disabling Maintenance mode&#8230;' ) );

	// Remove maintenance file, we're done with potential site-breaking changes.
	$wp_filesystem->delete( $maintenance_file );

	/*
	 * 3.5 -> 3.5+ - an empty twentytwelve directory was created upon upgrade to 3.5 for some users,
	 * preventing installation of Twenty Twelve.
	 */
	if ( '3.5' === $old_wp_version ) {
		if ( is_dir( WP_CONTENT_DIR . '/themes/twentytwelve' )
			&& ! file_exists( WP_CONTENT_DIR . '/themes/twentytwelve/style.css' )
		) {
			$wp_filesystem->delete( $wp_filesystem->wp_themes_dir() . 'twentytwelve/' );
		}
	}

	/*
	 * Copy new bundled plugins & themes.
	 * This gives us the ability to install new plugins & themes bundled with
	 * future versions of WordPress whilst avoiding the re-install upon upgrade issue.
	 * $development_build controls us overwriting bundled themes and plugins when a non-stable release is being updated.
	 */
	if ( ! is_wp_error( $result )
		&& ( ! defined( 'CORE_UPGRADE_SKIP_NEW_BUNDLED' ) || ! CORE_UPGRADE_SKIP_NEW_BUNDLED )
	) {
		foreach ( (array) $_new_bundled_files as $file => $introduced_version ) {
			// If a $development_build or if $introduced version is greater than what the site was previously running.
			if ( $development_build || version_compare( $introduced_version, $old_wp_version, '>' ) ) {
				$directory = ( '/' === $file[ strlen( $file ) - 1 ] );

				list( $type, $filename ) = explode( '/', $file, 2 );

				// Check to see if the bundled items exist before attempting to copy them.
				if ( ! $wp_filesystem->exists( $from . $distro . 'wp-content/' . $file ) ) {
					continue;
				}

				if ( 'plugins' === $type ) {
					$dest = $wp_filesystem->wp_plugins_dir();
				} elseif ( 'themes' === $type ) {
					// Back-compat, ::wp_themes_dir() did not return trailingslash'd pre-3.2.
					$dest = trailingslashit( $wp_filesystem->wp_themes_dir() );
				} else {
					continue;
				}

				if ( ! $directory ) {
					if ( ! $development_build && $wp_filesystem->exists( $dest . $filename ) ) {
						continue;
					}

					if ( ! $wp_filesystem->copy( $from . $distro . 'wp-content/' . $file, $dest . $filename, FS_CHMOD_FILE ) ) {
						$result = new WP_Error( "copy_failed_for_new_bundled_$type", __( 'Could not copy file.' ), $dest . $filename );
					}
				} else {
					if ( ! $development_build && $wp_filesystem->is_dir( $dest . $filename ) ) {
						continue;
					}

					$wp_filesystem->mkdir( $dest . $filename, FS_CHMOD_DIR );
					$_result = copy_dir( $from . $distro . 'wp-content/' . $file, $dest . $filename );

					/*
					 * If an error occurs partway through this final step,
					 * keep the error flowing through, but keep the process going.
					 */
					if ( is_wp_error( $_result ) ) {
						if ( ! is_wp_error( $result ) ) {
							$result = new WP_Error();
						}

						$result->add(
							$_result->get_error_code() . "_$type",
							$_result->get_error_message(),
							substr( $_result->get_error_data(), strlen( $dest ) )
						);
					}
				}
			}
		} // End foreach.
	}

	// Handle $result error from the above blocks.
	if ( is_wp_error( $result ) ) {
		$wp_filesystem->delete( $from, true );

		return $result;
	}

	// Remove old files.
	foreach ( $_old_files as $old_file ) {
		$old_file = $to . $old_file;

		if ( ! $wp_filesystem->exists( $old_file ) ) {
			continue;
		}

		// If the file isn't deleted, try writing an empty string to the file instead.
		if ( ! $wp_filesystem->delete( $old_file, true ) && $wp_filesystem->is_file( $old_file ) ) {
			$wp_filesystem->put_contents( $old_file, '' );
		}
	}

	// Remove any Genericons example.html's from the filesystem.
	_upgrade_422_remove_genericons();

	// Deactivate the REST API plugin if its version is 2.0 Beta 4 or lower.
	_upgrade_440_force_deactivate_incompatible_plugins();

	// Deactivate incompatible plugins.
	_upgrade_core_deactivate_incompatible_plugins();

	// Upgrade DB with separate request.
	/** This filter is documented in wp-admin/includes/update-core.php */
	apply_filters( 'update_feedback', __( 'Upgrading database&#8230;' ) );

	$db_upgrade_url = admin_url( 'upgrade.php?step=upgrade_db' );
	wp_remote_post( $db_upgrade_url, array( 'timeout' => 60 ) );

	// Clear the cache to prevent an update_option() from saving a stale db_version to the cache.
	wp_cache_flush();
	// Not all cache back ends listen to 'flush'.
	wp_cache_delete( 'alloptions', 'options' );

	// Remove working directory.
	$wp_filesystem->delete( $from, true );

	// Force refresh of update information.
	if ( function_exists( 'delete_site_transient' ) ) {
		delete_site_transient( 'update_core' );
	} else {
		delete_option( 'update_core' );
	}

	/**
	 * Fires after WordPress core has been successfully updated.
	 *
	 * @since 3.3.0
	 *
	 * @param string $wp_version The current WordPress version.
	 */
	do_action( '_core_updated_successfully', $wp_version );

	// Clear the option that blocks auto-updates after failures, now that we've been successful.
	if ( function_exists( 'delete_site_option' ) ) {
		delete_site_option( 'auto_core_update_failed' );
	}

	return $wp_version;
}

/**
 * Preloads old Requests classes and interfaces.
 *
 * This function preloads the old Requests code into memory before the
 * upgrade process deletes the files. Why? Requests code is loaded into
 * memory via an autoloader, meaning when a class or interface is needed
 * If a request is in process, Requests could attempt to access code. If
 * the file is not there, a fatal error could occur. If the file was
 * replaced, the new code is not compatible with the old, resulting in
 * a fatal error. Preloading ensures the code is in memory before the
 * code is updated.
 *
 * @since 6.2.0
 *
 * @global string[]           $_old_requests_files Requests files to be preloaded.
 * @global WP_Filesystem_Base $wp_filesystem       WordPress filesystem subclass.
 * @global string             $wp_version          The WordPress version string.
 *
 * @param string $to Path to old WordPress installation.
 */
function _preload_old_requests_classes_and_interfaces( $to ) {
	global $_old_requests_files, $wp_filesystem, $wp_version;

	/*
	 * Requests was introduced in WordPress 4.6.
	 *
	 * Skip preloading if the website was previously using
	 * an earlier version of WordPress.
	 */
	if ( version_compare( $wp_version, '4.6', '<' ) ) {
		return;
	}

	if ( ! defined( 'REQUESTS_SILENCE_PSR0_DEPRECATIONS' ) ) {
		define( 'REQUESTS_SILENCE_PSR0_DEPRECATIONS', true );
	}

	foreach ( $_old_requests_files as $name => $file ) {
		// Skip files that aren't interfaces or classes.
		if ( is_int( $name ) ) {
			continue;
		}

		// Skip if it's already loaded.
		if ( class_exists( $name ) || interface_exists( $name ) ) {
			continue;
		}

		// Skip if the file is missing.
		if ( ! $wp_filesystem->is_file( $to . $file ) ) {
			continue;
		}

		require_once $to . $file;
	}
}

/**
 * Redirect to the About WordPress page after a successful upgrade.
 *
 * This function is only needed when the existing installation is older than 3.4.0.
 *
 * @since 3.3.0
 *
 * @global string $wp_version The WordPress version string.
 * @global string $pagenow    The filename of the current screen.
 * @global string $action
 *
 * @param string $new_version
 */
function _redirect_to_about_wordpress( $new_version ) {
	global $wp_version, $pagenow, $action;

	if ( version_compare( $wp_version, '3.4-RC1', '>=' ) ) {
		return;
	}

	// Ensure we only run this on the update-core.php page. The Core_Upgrader may be used in other contexts.
	if ( 'update-core.php' !== $pagenow ) {
		return;
	}

	if ( 'do-core-upgrade' !== $action && 'do-core-reinstall' !== $action ) {
		return;
	}

	// Load the updated default text localization domain for new strings.
	load_default_textdomain();

	// See do_core_upgrade().
	show_message( __( 'WordPress updated successfully.' ) );

	// self_admin_url() won't exist when upgrading from <= 3.0, so relative URLs are intentional.
	show_message(
		'<span class="hide-if-no-js">' . sprintf(
			/* translators: 1: WordPress version, 2: URL to About screen. */
			__( 'Welcome to WordPress %1$s. You will be redirected to the About WordPress screen. If not, click <a href="%2$s">here</a>.' ),
			$new_version,
			'about.php?updated'
		) . '</span>'
	);
	show_message(
		'<span class="hide-if-js">' . sprintf(
			/* translators: 1: WordPress version, 2: URL to About screen. */
			__( 'Welcome to WordPress %1$s. <a href="%2$s">Learn more</a>.' ),
			$new_version,
			'about.php?updated'
		) . '</span>'
	);
	echo '</div>';
	?>
<script type="text/javascript">
window.location = 'about.php?updated';
</script>
	<?php

	// Include admin-footer.php and exit.
	require_once ABSPATH . 'wp-admin/admin-footer.php';
	exit;
}

/**
 * Cleans up Genericons example files.
 *
 * @since 4.2.2
 *
 * @global string[]           $wp_theme_directories
 * @global WP_Filesystem_Base $wp_filesystem
 */
function _upgrade_422_remove_genericons() {
	global $wp_theme_directories, $wp_filesystem;

	// A list of the affected files using the filesystem absolute paths.
	$affected_files = array();

	// Themes.
	foreach ( $wp_theme_directories as $directory ) {
		$affected_theme_files = _upgrade_422_find_genericons_files_in_folder( $directory );
		$affected_files       = array_merge( $affected_files, $affected_theme_files );
	}

	// Plugins.
	$affected_plugin_files = _upgrade_422_find_genericons_files_in_folder( WP_PLUGIN_DIR );
	$affected_files        = array_merge( $affected_files, $affected_plugin_files );

	foreach ( $affected_files as $file ) {
		$gen_dir = $wp_filesystem->find_folder( trailingslashit( dirname( $file ) ) );

		if ( empty( $gen_dir ) ) {
			continue;
		}

		// The path when the file is accessed via WP_Filesystem may differ in the case of FTP.
		$remote_file = $gen_dir . basename( $file );

		if ( ! $wp_filesystem->exists( $remote_file ) ) {
			continue;
		}

		if ( ! $wp_filesystem->delete( $remote_file, false, 'f' ) ) {
			$wp_filesystem->put_contents( $remote_file, '' );
		}
	}
}

/**
 * Recursively find Genericons example files in a given folder.
 *
 * @ignore
 * @since 4.2.2
 *
 * @param string $directory Directory path. Expects trailingslashed.
 * @return string[]
 */
function _upgrade_422_find_genericons_files_in_folder( $directory ) {
	$directory = trailingslashit( $directory );
	$files     = array();

	if ( file_exists( "{$directory}example.html" )
		/*
		 * Note: str_contains() is not used here, as this file is included
		 * when updating from older WordPress versions, in which case
		 * the polyfills from wp-includes/compat.php may not be available.
		 */
		&& false !== strpos( file_get_contents( "{$directory}example.html" ), '<title>Genericons</title>' )
	) {
		$files[] = "{$directory}example.html";
	}

	$dirs = glob( $directory . '*', GLOB_ONLYDIR );
	$dirs = array_filter(
		$dirs,
		static function ( $dir ) {
			/*
			 * Skip any node_modules directories.
			 *
			 * Note: str_contains() is not used here, as this file is included
			 * when updating from older WordPress versions, in which case
			 * the polyfills from wp-includes/compat.php may not be available.
			 */
			return false === strpos( $dir, 'node_modules' );
		}
	);

	if ( $dirs ) {
		foreach ( $dirs as $dir ) {
			$files = array_merge( $files, _upgrade_422_find_genericons_files_in_folder( $dir ) );
		}
	}

	return $files;
}

/**
 * @ignore
 * @since 4.4.0
 */
function _upgrade_440_force_deactivate_incompatible_plugins() {
	if ( defined( 'REST_API_VERSION' ) && version_compare( REST_API_VERSION, '2.0-beta4', '<=' ) ) {
		deactivate_plugins( array( 'rest-api/plugin.php' ), true );
	}
}

/**
 * @access private
 * @ignore
 * @since 5.8.0
 * @since 5.9.0 The minimum compatible version of Gutenberg is 11.9.
 * @since 6.1.1 The minimum compatible version of Gutenberg is 14.1.
 * @since 6.4.0 The minimum compatible version of Gutenberg is 16.5.
 * @since 6.5.0 The minimum compatible version of Gutenberg is 17.6.
 */
function _upgrade_core_deactivate_incompatible_plugins() {
	if ( defined( 'GUTENBERG_VERSION' ) && version_compare( GUTENBERG_VERSION, '17.6', '<' ) ) {
		$deactivated_gutenberg['gutenberg'] = array(
			'plugin_name'         => 'Gutenberg',
			'version_deactivated' => GUTENBERG_VERSION,
			'version_compatible'  => '17.6',
		);
		if ( is_plugin_active_for_network( 'gutenberg/gutenberg.php' ) ) {
			$deactivated_plugins = get_site_option( 'wp_force_deactivated_plugins', array() );
			$deactivated_plugins = array_merge( $deactivated_plugins, $deactivated_gutenberg );
			update_site_option( 'wp_force_deactivated_plugins', $deactivated_plugins );
		} else {
			$deactivated_plugins = get_option( 'wp_force_deactivated_plugins', array() );
			$deactivated_plugins = array_merge( $deactivated_plugins, $deactivated_gutenberg );
			update_option( 'wp_force_deactivated_plugins', $deactivated_plugins, false );
		}
		deactivate_plugins( array( 'gutenberg/gutenberg.php' ), true );
	}
}
user.php000064400000055754151327343360006262 0ustar00<?php
/**
 * WordPress user administration API.
 *
 * @package WordPress
 * @subpackage Administration
 */

/**
 * Creates a new user from the "Users" form using $_POST information.
 *
 * @since 2.0.0
 *
 * @return int|WP_Error WP_Error or User ID.
 */
function add_user() {
	return edit_user();
}

/**
 * Edit user settings based on contents of $_POST
 *
 * Used on user-edit.php and profile.php to manage and process user options, passwords etc.
 *
 * @since 2.0.0
 *
 * @param int $user_id Optional. User ID.
 * @return int|WP_Error User ID of the updated user or WP_Error on failure.
 */
function edit_user( $user_id = 0 ) {
	$wp_roles = wp_roles();
	$user     = new stdClass();
	$user_id  = (int) $user_id;
	if ( $user_id ) {
		$update           = true;
		$user->ID         = $user_id;
		$userdata         = get_userdata( $user_id );
		$user->user_login = wp_slash( $userdata->user_login );
	} else {
		$update = false;
	}

	if ( ! $update && isset( $_POST['user_login'] ) ) {
		$user->user_login = sanitize_user( wp_unslash( $_POST['user_login'] ), true );
	}

	$pass1 = '';
	$pass2 = '';
	if ( isset( $_POST['pass1'] ) ) {
		$pass1 = trim( $_POST['pass1'] );
	}
	if ( isset( $_POST['pass2'] ) ) {
		$pass2 = trim( $_POST['pass2'] );
	}

	if ( isset( $_POST['role'] ) && current_user_can( 'promote_users' ) && ( ! $user_id || current_user_can( 'promote_user', $user_id ) ) ) {
		$new_role = sanitize_text_field( $_POST['role'] );

		// If the new role isn't editable by the logged-in user die with error.
		$editable_roles = get_editable_roles();
		if ( ! empty( $new_role ) && empty( $editable_roles[ $new_role ] ) ) {
			wp_die( __( 'Sorry, you are not allowed to give users that role.' ), 403 );
		}

		$potential_role = isset( $wp_roles->role_objects[ $new_role ] ) ? $wp_roles->role_objects[ $new_role ] : false;

		/*
		 * Don't let anyone with 'promote_users' edit their own role to something without it.
		 * Multisite super admins can freely edit their roles, they possess all caps.
		 */
		if (
			( is_multisite() && current_user_can( 'manage_network_users' ) ) ||
			get_current_user_id() !== $user_id ||
			( $potential_role && $potential_role->has_cap( 'promote_users' ) )
		) {
			$user->role = $new_role;
		}
	}

	if ( isset( $_POST['email'] ) ) {
		$user->user_email = sanitize_text_field( wp_unslash( $_POST['email'] ) );
	}
	if ( isset( $_POST['url'] ) ) {
		if ( empty( $_POST['url'] ) || 'http://' === $_POST['url'] ) {
			$user->user_url = '';
		} else {
			$user->user_url = sanitize_url( $_POST['url'] );
			$protocols      = implode( '|', array_map( 'preg_quote', wp_allowed_protocols() ) );
			$user->user_url = preg_match( '/^(' . $protocols . '):/is', $user->user_url ) ? $user->user_url : 'http://' . $user->user_url;
		}
	}
	if ( isset( $_POST['first_name'] ) ) {
		$user->first_name = sanitize_text_field( $_POST['first_name'] );
	}
	if ( isset( $_POST['last_name'] ) ) {
		$user->last_name = sanitize_text_field( $_POST['last_name'] );
	}
	if ( isset( $_POST['nickname'] ) ) {
		$user->nickname = sanitize_text_field( $_POST['nickname'] );
	}
	if ( isset( $_POST['display_name'] ) ) {
		$user->display_name = sanitize_text_field( $_POST['display_name'] );
	}

	if ( isset( $_POST['description'] ) ) {
		$user->description = trim( $_POST['description'] );
	}

	foreach ( wp_get_user_contact_methods( $user ) as $method => $name ) {
		if ( isset( $_POST[ $method ] ) ) {
			$user->$method = sanitize_text_field( $_POST[ $method ] );
		}
	}

	if ( isset( $_POST['locale'] ) ) {
		$locale = sanitize_text_field( $_POST['locale'] );
		if ( 'site-default' === $locale ) {
			$locale = '';
		} elseif ( '' === $locale ) {
			$locale = 'en_US';
		} elseif ( ! in_array( $locale, get_available_languages(), true ) ) {
			if ( current_user_can( 'install_languages' ) && wp_can_install_language_pack() ) {
				if ( ! wp_download_language_pack( $locale ) ) {
					$locale = '';
				}
			} else {
				$locale = '';
			}
		}

		$user->locale = $locale;
	}

	if ( $update ) {
		$user->rich_editing         = isset( $_POST['rich_editing'] ) && 'false' === $_POST['rich_editing'] ? 'false' : 'true';
		$user->syntax_highlighting  = isset( $_POST['syntax_highlighting'] ) && 'false' === $_POST['syntax_highlighting'] ? 'false' : 'true';
		$user->admin_color          = isset( $_POST['admin_color'] ) ? sanitize_text_field( $_POST['admin_color'] ) : 'fresh';
		$user->show_admin_bar_front = isset( $_POST['admin_bar_front'] ) ? 'true' : 'false';
	}

	$user->comment_shortcuts = isset( $_POST['comment_shortcuts'] ) && 'true' === $_POST['comment_shortcuts'] ? 'true' : '';

	$user->use_ssl = 0;
	if ( ! empty( $_POST['use_ssl'] ) ) {
		$user->use_ssl = 1;
	}

	$errors = new WP_Error();

	/* checking that username has been typed */
	if ( '' === $user->user_login ) {
		$errors->add( 'user_login', __( '<strong>Error:</strong> Please enter a username.' ) );
	}

	/* checking that nickname has been typed */
	if ( $update && empty( $user->nickname ) ) {
		$errors->add( 'nickname', __( '<strong>Error:</strong> Please enter a nickname.' ) );
	}

	/**
	 * Fires before the password and confirm password fields are checked for congruity.
	 *
	 * @since 1.5.1
	 *
	 * @param string $user_login The username.
	 * @param string $pass1     The password (passed by reference).
	 * @param string $pass2     The confirmed password (passed by reference).
	 */
	do_action_ref_array( 'check_passwords', array( $user->user_login, &$pass1, &$pass2 ) );

	// Check for blank password when adding a user.
	if ( ! $update && empty( $pass1 ) ) {
		$errors->add( 'pass', __( '<strong>Error:</strong> Please enter a password.' ), array( 'form-field' => 'pass1' ) );
	}

	// Check for "\" in password.
	if ( str_contains( wp_unslash( $pass1 ), '\\' ) ) {
		$errors->add( 'pass', __( '<strong>Error:</strong> Passwords may not contain the character "\\".' ), array( 'form-field' => 'pass1' ) );
	}

	// Checking the password has been typed twice the same.
	if ( ( $update || ! empty( $pass1 ) ) && $pass1 !== $pass2 ) {
		$errors->add( 'pass', __( '<strong>Error:</strong> Passwords do not match. Please enter the same password in both password fields.' ), array( 'form-field' => 'pass1' ) );
	}

	if ( ! empty( $pass1 ) ) {
		$user->user_pass = $pass1;
	}

	if ( ! $update && isset( $_POST['user_login'] ) && ! validate_username( $_POST['user_login'] ) ) {
		$errors->add( 'user_login', __( '<strong>Error:</strong> This username is invalid because it uses illegal characters. Please enter a valid username.' ) );
	}

	if ( ! $update && username_exists( $user->user_login ) ) {
		$errors->add( 'user_login', __( '<strong>Error:</strong> This username is already registered. Please choose another one.' ) );
	}

	/** This filter is documented in wp-includes/user.php */
	$illegal_logins = (array) apply_filters( 'illegal_user_logins', array() );

	if ( in_array( strtolower( $user->user_login ), array_map( 'strtolower', $illegal_logins ), true ) ) {
		$errors->add( 'invalid_username', __( '<strong>Error:</strong> Sorry, that username is not allowed.' ) );
	}

	// Checking email address.
	if ( empty( $user->user_email ) ) {
		$errors->add( 'empty_email', __( '<strong>Error:</strong> Please enter an email address.' ), array( 'form-field' => 'email' ) );
	} elseif ( ! is_email( $user->user_email ) ) {
		$errors->add( 'invalid_email', __( '<strong>Error:</strong> The email address is not correct.' ), array( 'form-field' => 'email' ) );
	} else {
		$owner_id = email_exists( $user->user_email );
		if ( $owner_id && ( ! $update || ( $owner_id !== $user->ID ) ) ) {
			$errors->add( 'email_exists', __( '<strong>Error:</strong> This email is already registered. Please choose another one.' ), array( 'form-field' => 'email' ) );
		}
	}

	/**
	 * Fires before user profile update errors are returned.
	 *
	 * @since 2.8.0
	 *
	 * @param WP_Error $errors WP_Error object (passed by reference).
	 * @param bool     $update Whether this is a user update.
	 * @param stdClass $user   User object (passed by reference).
	 */
	do_action_ref_array( 'user_profile_update_errors', array( &$errors, $update, &$user ) );

	if ( $errors->has_errors() ) {
		return $errors;
	}

	if ( $update ) {
		$user_id = wp_update_user( $user );
	} else {
		$user_id = wp_insert_user( $user );
		$notify  = isset( $_POST['send_user_notification'] ) ? 'both' : 'admin';

		/**
		 * Fires after a new user has been created.
		 *
		 * @since 4.4.0
		 *
		 * @param int|WP_Error $user_id ID of the newly created user or WP_Error on failure.
		 * @param string       $notify  Type of notification that should happen. See
		 *                              wp_send_new_user_notifications() for more information.
		 */
		do_action( 'edit_user_created_user', $user_id, $notify );
	}
	return $user_id;
}

/**
 * Fetch a filtered list of user roles that the current user is
 * allowed to edit.
 *
 * Simple function whose main purpose is to allow filtering of the
 * list of roles in the $wp_roles object so that plugins can remove
 * inappropriate ones depending on the situation or user making edits.
 * Specifically because without filtering anyone with the edit_users
 * capability can edit others to be administrators, even if they are
 * only editors or authors. This filter allows admins to delegate
 * user management.
 *
 * @since 2.8.0
 *
 * @return array[] Array of arrays containing role information.
 */
function get_editable_roles() {
	$all_roles = wp_roles()->roles;

	/**
	 * Filters the list of editable roles.
	 *
	 * @since 2.8.0
	 *
	 * @param array[] $all_roles Array of arrays containing role information.
	 */
	$editable_roles = apply_filters( 'editable_roles', $all_roles );

	return $editable_roles;
}

/**
 * Retrieve user data and filter it.
 *
 * @since 2.0.5
 *
 * @param int $user_id User ID.
 * @return WP_User|false WP_User object on success, false on failure.
 */
function get_user_to_edit( $user_id ) {
	$user = get_userdata( $user_id );

	if ( $user ) {
		$user->filter = 'edit';
	}

	return $user;
}

/**
 * Retrieve the user's drafts.
 *
 * @since 2.0.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param int $user_id User ID.
 * @return array
 */
function get_users_drafts( $user_id ) {
	global $wpdb;
	$query = $wpdb->prepare( "SELECT ID, post_title FROM $wpdb->posts WHERE post_type = 'post' AND post_status = 'draft' AND post_author = %d ORDER BY post_modified DESC", $user_id );

	/**
	 * Filters the SQL query string for the user's drafts query.
	 *
	 * @since 2.0.0
	 *
	 * @param string $query The user's drafts query string.
	 */
	$query = apply_filters( 'get_users_drafts', $query );
	return $wpdb->get_results( $query );
}

/**
 * Delete user and optionally reassign posts and links to another user.
 *
 * Note that on a Multisite installation the user only gets removed from the site
 * and does not get deleted from the database.
 *
 * If the `$reassign` parameter is not assigned to a user ID, then all posts will
 * be deleted of that user. The action {@see 'delete_user'} that is passed the user ID
 * being deleted will be run after the posts are either reassigned or deleted.
 * The user meta will also be deleted that are for that user ID.
 *
 * @since 2.0.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param int $id       User ID.
 * @param int $reassign Optional. Reassign posts and links to new User ID.
 * @return bool True when finished.
 */
function wp_delete_user( $id, $reassign = null ) {
	global $wpdb;

	if ( ! is_numeric( $id ) ) {
		return false;
	}

	$id   = (int) $id;
	$user = new WP_User( $id );

	if ( ! $user->exists() ) {
		return false;
	}

	// Normalize $reassign to null or a user ID. 'novalue' was an older default.
	if ( 'novalue' === $reassign ) {
		$reassign = null;
	} elseif ( null !== $reassign ) {
		$reassign = (int) $reassign;
	}

	/**
	 * Fires immediately before a user is deleted from the site.
	 *
	 * Note that on a Multisite installation the user only gets removed from the site
	 * and does not get deleted from the database.
	 *
	 * @since 2.0.0
	 * @since 5.5.0 Added the `$user` parameter.
	 *
	 * @param int      $id       ID of the user to delete.
	 * @param int|null $reassign ID of the user to reassign posts and links to.
	 *                           Default null, for no reassignment.
	 * @param WP_User  $user     WP_User object of the user to delete.
	 */
	do_action( 'delete_user', $id, $reassign, $user );

	if ( null === $reassign ) {
		$post_types_to_delete = array();
		foreach ( get_post_types( array(), 'objects' ) as $post_type ) {
			if ( $post_type->delete_with_user ) {
				$post_types_to_delete[] = $post_type->name;
			} elseif ( null === $post_type->delete_with_user && post_type_supports( $post_type->name, 'author' ) ) {
				$post_types_to_delete[] = $post_type->name;
			}
		}

		/**
		 * Filters the list of post types to delete with a user.
		 *
		 * @since 3.4.0
		 *
		 * @param string[] $post_types_to_delete Array of post types to delete.
		 * @param int      $id                   User ID.
		 */
		$post_types_to_delete = apply_filters( 'post_types_to_delete_with_user', $post_types_to_delete, $id );
		$post_types_to_delete = implode( "', '", $post_types_to_delete );
		$post_ids             = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_author = %d AND post_type IN ('$post_types_to_delete')", $id ) );
		if ( $post_ids ) {
			foreach ( $post_ids as $post_id ) {
				wp_delete_post( $post_id );
			}
		}

		// Clean links.
		$link_ids = $wpdb->get_col( $wpdb->prepare( "SELECT link_id FROM $wpdb->links WHERE link_owner = %d", $id ) );

		if ( $link_ids ) {
			foreach ( $link_ids as $link_id ) {
				wp_delete_link( $link_id );
			}
		}
	} else {
		$post_ids = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_author = %d", $id ) );
		$wpdb->update( $wpdb->posts, array( 'post_author' => $reassign ), array( 'post_author' => $id ) );
		if ( ! empty( $post_ids ) ) {
			foreach ( $post_ids as $post_id ) {
				clean_post_cache( $post_id );
			}
		}
		$link_ids = $wpdb->get_col( $wpdb->prepare( "SELECT link_id FROM $wpdb->links WHERE link_owner = %d", $id ) );
		$wpdb->update( $wpdb->links, array( 'link_owner' => $reassign ), array( 'link_owner' => $id ) );
		if ( ! empty( $link_ids ) ) {
			foreach ( $link_ids as $link_id ) {
				clean_bookmark_cache( $link_id );
			}
		}
	}

	// FINALLY, delete user.
	if ( is_multisite() ) {
		remove_user_from_blog( $id, get_current_blog_id() );
	} else {
		$meta = $wpdb->get_col( $wpdb->prepare( "SELECT umeta_id FROM $wpdb->usermeta WHERE user_id = %d", $id ) );
		foreach ( $meta as $mid ) {
			delete_metadata_by_mid( 'user', $mid );
		}

		$wpdb->delete( $wpdb->users, array( 'ID' => $id ) );
	}

	clean_user_cache( $user );

	/**
	 * Fires immediately after a user is deleted from the site.
	 *
	 * Note that on a Multisite installation the user may not have been deleted from
	 * the database depending on whether `wp_delete_user()` or `wpmu_delete_user()`
	 * was called.
	 *
	 * @since 2.9.0
	 * @since 5.5.0 Added the `$user` parameter.
	 *
	 * @param int      $id       ID of the deleted user.
	 * @param int|null $reassign ID of the user to reassign posts and links to.
	 *                           Default null, for no reassignment.
	 * @param WP_User  $user     WP_User object of the deleted user.
	 */
	do_action( 'deleted_user', $id, $reassign, $user );

	return true;
}

/**
 * Remove all capabilities from user.
 *
 * @since 2.1.0
 *
 * @param int $id User ID.
 */
function wp_revoke_user( $id ) {
	$id = (int) $id;

	$user = new WP_User( $id );
	$user->remove_all_caps();
}

/**
 * @since 2.8.0
 *
 * @global int $user_ID
 *
 * @param false $errors Deprecated.
 */
function default_password_nag_handler( $errors = false ) {
	global $user_ID;
	// Short-circuit it.
	if ( ! get_user_option( 'default_password_nag' ) ) {
		return;
	}

	// get_user_setting() = JS-saved UI setting. Else no-js-fallback code.
	if ( 'hide' === get_user_setting( 'default_password_nag' )
		|| isset( $_GET['default_password_nag'] ) && '0' === $_GET['default_password_nag']
	) {
		delete_user_setting( 'default_password_nag' );
		update_user_meta( $user_ID, 'default_password_nag', false );
	}
}

/**
 * @since 2.8.0
 *
 * @param int     $user_ID
 * @param WP_User $old_data
 */
function default_password_nag_edit_user( $user_ID, $old_data ) {
	// Short-circuit it.
	if ( ! get_user_option( 'default_password_nag', $user_ID ) ) {
		return;
	}

	$new_data = get_userdata( $user_ID );

	// Remove the nag if the password has been changed.
	if ( $new_data->user_pass !== $old_data->user_pass ) {
		delete_user_setting( 'default_password_nag' );
		update_user_meta( $user_ID, 'default_password_nag', false );
	}
}

/**
 * @since 2.8.0
 *
 * @global string $pagenow The filename of the current screen.
 */
function default_password_nag() {
	global $pagenow;

	// Short-circuit it.
	if ( 'profile.php' === $pagenow || ! get_user_option( 'default_password_nag' ) ) {
		return;
	}

	$default_password_nag_message  = sprintf(
		'<p><strong>%1$s</strong> %2$s</p>',
		__( 'Notice:' ),
		__( 'You are using the auto-generated password for your account. Would you like to change it?' )
	);
	$default_password_nag_message .= sprintf(
		'<p><a href="%1$s">%2$s</a> | ',
		esc_url( get_edit_profile_url() . '#password' ),
		__( 'Yes, take me to my profile page' )
	);
	$default_password_nag_message .= sprintf(
		'<a href="%1$s" id="default-password-nag-no">%2$s</a></p>',
		'?default_password_nag=0',
		__( 'No thanks, do not remind me again' )
	);

	wp_admin_notice(
		$default_password_nag_message,
		array(
			'additional_classes' => array( 'error', 'default-password-nag' ),
			'paragraph_wrap'     => false,
		)
	);
}

/**
 * @since 3.5.0
 * @access private
 */
function delete_users_add_js() {
	?>
<script>
jQuery( function($) {
	var submit = $('#submit').prop('disabled', true);
	$('input[name="delete_option"]').one('change', function() {
		submit.prop('disabled', false);
	});
	$('#reassign_user').focus( function() {
		$('#delete_option1').prop('checked', true).trigger('change');
	});
} );
</script>
	<?php
}

/**
 * Optional SSL preference that can be turned on by hooking to the 'personal_options' action.
 *
 * See the {@see 'personal_options'} action.
 *
 * @since 2.7.0
 *
 * @param WP_User $user User data object.
 */
function use_ssl_preference( $user ) {
	?>
	<tr class="user-use-ssl-wrap">
		<th scope="row"><?php _e( 'Use https' ); ?></th>
		<td><label for="use_ssl"><input name="use_ssl" type="checkbox" id="use_ssl" value="1" <?php checked( '1', $user->use_ssl ); ?> /> <?php _e( 'Always use https when visiting the admin' ); ?></label></td>
	</tr>
	<?php
}

/**
 * @since MU (3.0.0)
 *
 * @param string $text
 * @return string
 */
function admin_created_user_email( $text ) {
	$roles = get_editable_roles();
	$role  = $roles[ $_REQUEST['role'] ];

	if ( '' !== get_bloginfo( 'name' ) ) {
		$site_title = wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES );
	} else {
		$site_title = parse_url( home_url(), PHP_URL_HOST );
	}

	return sprintf(
		/* translators: 1: Site title, 2: Site URL, 3: User role. */
		__(
			'Hi,
You\'ve been invited to join \'%1$s\' at
%2$s with the role of %3$s.
If you do not want to join this site please ignore
this email. This invitation will expire in a few days.

Please click the following link to activate your user account:
%%s'
		),
		$site_title,
		home_url(),
		wp_specialchars_decode( translate_user_role( $role['name'] ) )
	);
}

/**
 * Checks if the Authorize Application Password request is valid.
 *
 * @since 5.6.0
 * @since 6.2.0 Allow insecure HTTP connections for the local environment.
 * @since 6.3.2 Validates the success and reject URLs to prevent `javascript` pseudo protocol from being executed.
 *
 * @param array   $request {
 *     The array of request data. All arguments are optional and may be empty.
 *
 *     @type string $app_name    The suggested name of the application.
 *     @type string $app_id      A UUID provided by the application to uniquely identify it.
 *     @type string $success_url The URL the user will be redirected to after approving the application.
 *     @type string $reject_url  The URL the user will be redirected to after rejecting the application.
 * }
 * @param WP_User $user The user authorizing the application.
 * @return true|WP_Error True if the request is valid, a WP_Error object contains errors if not.
 */
function wp_is_authorize_application_password_request_valid( $request, $user ) {
	$error = new WP_Error();

	if ( isset( $request['success_url'] ) ) {
		$validated_success_url = wp_is_authorize_application_redirect_url_valid( $request['success_url'] );
		if ( is_wp_error( $validated_success_url ) ) {
			$error->add(
				$validated_success_url->get_error_code(),
				$validated_success_url->get_error_message()
			);
		}
	}

	if ( isset( $request['reject_url'] ) ) {
		$validated_reject_url = wp_is_authorize_application_redirect_url_valid( $request['reject_url'] );
		if ( is_wp_error( $validated_reject_url ) ) {
			$error->add(
				$validated_reject_url->get_error_code(),
				$validated_reject_url->get_error_message()
			);
		}
	}

	if ( ! empty( $request['app_id'] ) && ! wp_is_uuid( $request['app_id'] ) ) {
		$error->add(
			'invalid_app_id',
			__( 'The application ID must be a UUID.' )
		);
	}

	/**
	 * Fires before application password errors are returned.
	 *
	 * @since 5.6.0
	 *
	 * @param WP_Error $error   The error object.
	 * @param array    $request The array of request data.
	 * @param WP_User  $user    The user authorizing the application.
	 */
	do_action( 'wp_authorize_application_password_request_errors', $error, $request, $user );

	if ( $error->has_errors() ) {
		return $error;
	}

	return true;
}

/**
 * Validates the redirect URL protocol scheme. The protocol can be anything except `http` and `javascript`.
 *
 * @since 6.3.2
 *
 * @param string $url The redirect URL to be validated.
 * @return true|WP_Error True if the redirect URL is valid, a WP_Error object otherwise.
 */
function wp_is_authorize_application_redirect_url_valid( $url ) {
	$bad_protocols = array( 'javascript', 'data' );
	if ( empty( $url ) ) {
		return true;
	}

	// Based on https://www.rfc-editor.org/rfc/rfc2396#section-3.1
	$valid_scheme_regex = '/^[a-zA-Z][a-zA-Z0-9+.-]*:/';
	if ( ! preg_match( $valid_scheme_regex, $url ) ) {
		return new WP_Error(
			'invalid_redirect_url_format',
			__( 'Invalid URL format.' )
		);
	}

	/**
	 * Filters the list of invalid protocols used in applications redirect URLs.
	 *
	 * @since 6.3.2
	 *
	 * @param string[] $bad_protocols Array of invalid protocols.
	 * @param string   $url The redirect URL to be validated.
	 */
	$invalid_protocols = apply_filters( 'wp_authorize_application_redirect_url_invalid_protocols', $bad_protocols, $url );
	$invalid_protocols = array_map( 'strtolower', $invalid_protocols );

	$scheme   = wp_parse_url( $url, PHP_URL_SCHEME );
	$host     = wp_parse_url( $url, PHP_URL_HOST );
	$is_local = 'local' === wp_get_environment_type();

	// Validates if the proper URI format is applied to the URL.
	if ( empty( $host ) || empty( $scheme ) || in_array( strtolower( $scheme ), $invalid_protocols, true ) ) {
		return new WP_Error(
			'invalid_redirect_url_format',
			__( 'Invalid URL format.' )
		);
	}

	if ( 'http' === $scheme && ! $is_local ) {
		return new WP_Error(
			'invalid_redirect_scheme',
			__( 'The URL must be served over a secure connection.' )
		);
	}

	return true;
}
class-wp-filesystem-direct.php000060400000043337151327343360012455 0ustar00<?php
/**
 * WordPress Direct Filesystem.
 *
 * @package WordPress
 * @subpackage Filesystem
 */

/**
 * WordPress Filesystem Class for direct PHP file and folder manipulation.
 *
 * @since 2.5.0
 *
 * @see WP_Filesystem_Base
 */
class WP_Filesystem_Direct extends WP_Filesystem_Base {

	/**
	 * Constructor.
	 *
	 * @since 2.5.0
	 *
	 * @param mixed $arg Not used.
	 */
	public function __construct( $arg ) {
		$this->method = 'direct';
		$this->errors = new WP_Error();
	}

	/**
	 * Reads entire file into a string.
	 *
	 * @since 2.5.0
	 *
	 * @param string $file Name of the file to read.
	 * @return string|false Read data on success, false on failure.
	 */
	public function get_contents( $file ) {
		return @file_get_contents( $file );
	}

	/**
	 * Reads entire file into an array.
	 *
	 * @since 2.5.0
	 *
	 * @param string $file Path to the file.
	 * @return array|false File contents in an array on success, false on failure.
	 */
	public function get_contents_array( $file ) {
		return @file( $file );
	}

	/**
	 * Writes a string to a file.
	 *
	 * @since 2.5.0
	 *
	 * @param string    $file     Remote path to the file where to write the data.
	 * @param string    $contents The data to write.
	 * @param int|false $mode     Optional. The file permissions as octal number, usually 0644.
	 *                            Default false.
	 * @return bool True on success, false on failure.
	 */
	public function put_contents( $file, $contents, $mode = false ) {
		$fp = @fopen( $file, 'wb' );

		if ( ! $fp ) {
			return false;
		}

		mbstring_binary_safe_encoding();

		$data_length = strlen( $contents );

		$bytes_written = fwrite( $fp, $contents );

		reset_mbstring_encoding();

		fclose( $fp );

		if ( $data_length !== $bytes_written ) {
			return false;
		}

		$this->chmod( $file, $mode );

		return true;
	}

	/**
	 * Gets the current working directory.
	 *
	 * @since 2.5.0
	 *
	 * @return string|false The current working directory on success, false on failure.
	 */
	public function cwd() {
		return getcwd();
	}

	/**
	 * Changes current directory.
	 *
	 * @since 2.5.0
	 *
	 * @param string $dir The new current directory.
	 * @return bool True on success, false on failure.
	 */
	public function chdir( $dir ) {
		return @chdir( $dir );
	}

	/**
	 * Changes the file group.
	 *
	 * @since 2.5.0
	 *
	 * @param string     $file      Path to the file.
	 * @param string|int $group     A group name or number.
	 * @param bool       $recursive Optional. If set to true, changes file group recursively.
	 *                              Default false.
	 * @return bool True on success, false on failure.
	 */
	public function chgrp( $file, $group, $recursive = false ) {
		if ( ! $this->exists( $file ) ) {
			return false;
		}

		if ( ! $recursive ) {
			return chgrp( $file, $group );
		}

		if ( ! $this->is_dir( $file ) ) {
			return chgrp( $file, $group );
		}

		// Is a directory, and we want recursive.
		$file     = trailingslashit( $file );
		$filelist = $this->dirlist( $file );

		foreach ( $filelist as $filename ) {
			$this->chgrp( $file . $filename, $group, $recursive );
		}

		return true;
	}

	/**
	 * Changes filesystem permissions.
	 *
	 * @since 2.5.0
	 *
	 * @param string    $file      Path to the file.
	 * @param int|false $mode      Optional. The permissions as octal number, usually 0644 for files,
	 *                             0755 for directories. Default false.
	 * @param bool      $recursive Optional. If set to true, changes file permissions recursively.
	 *                             Default false.
	 * @return bool True on success, false on failure.
	 */
	public function chmod( $file, $mode = false, $recursive = false ) {
		if ( ! $mode ) {
			if ( $this->is_file( $file ) ) {
				$mode = FS_CHMOD_FILE;
			} elseif ( $this->is_dir( $file ) ) {
				$mode = FS_CHMOD_DIR;
			} else {
				return false;
			}
		}

		if ( ! $recursive || ! $this->is_dir( $file ) ) {
			return chmod( $file, $mode );
		}

		// Is a directory, and we want recursive.
		$file     = trailingslashit( $file );
		$filelist = $this->dirlist( $file );

		foreach ( (array) $filelist as $filename => $filemeta ) {
			$this->chmod( $file . $filename, $mode, $recursive );
		}

		return true;
	}

	/**
	 * Changes the owner of a file or directory.
	 *
	 * @since 2.5.0
	 *
	 * @param string     $file      Path to the file or directory.
	 * @param string|int $owner     A user name or number.
	 * @param bool       $recursive Optional. If set to true, changes file owner recursively.
	 *                              Default false.
	 * @return bool True on success, false on failure.
	 */
	public function chown( $file, $owner, $recursive = false ) {
		if ( ! $this->exists( $file ) ) {
			return false;
		}

		if ( ! $recursive ) {
			return chown( $file, $owner );
		}

		if ( ! $this->is_dir( $file ) ) {
			return chown( $file, $owner );
		}

		// Is a directory, and we want recursive.
		$filelist = $this->dirlist( $file );

		foreach ( $filelist as $filename ) {
			$this->chown( $file . '/' . $filename, $owner, $recursive );
		}

		return true;
	}

	/**
	 * Gets the file owner.
	 *
	 * @since 2.5.0
	 *
	 * @param string $file Path to the file.
	 * @return string|false Username of the owner on success, false on failure.
	 */
	public function owner( $file ) {
		$owneruid = @fileowner( $file );

		if ( ! $owneruid ) {
			return false;
		}

		if ( ! function_exists( 'posix_getpwuid' ) ) {
			return $owneruid;
		}

		$ownerarray = posix_getpwuid( $owneruid );

		if ( ! $ownerarray ) {
			return false;
		}

		return $ownerarray['name'];
	}

	/**
	 * Gets the permissions of the specified file or filepath in their octal format.
	 *
	 * FIXME does not handle errors in fileperms()
	 *
	 * @since 2.5.0
	 *
	 * @param string $file Path to the file.
	 * @return string Mode of the file (the last 3 digits).
	 */
	public function getchmod( $file ) {
		return substr( decoct( @fileperms( $file ) ), -3 );
	}

	/**
	 * Gets the file's group.
	 *
	 * @since 2.5.0
	 *
	 * @param string $file Path to the file.
	 * @return string|false The group on success, false on failure.
	 */
	public function group( $file ) {
		$gid = @filegroup( $file );

		if ( ! $gid ) {
			return false;
		}

		if ( ! function_exists( 'posix_getgrgid' ) ) {
			return $gid;
		}

		$grouparray = posix_getgrgid( $gid );

		if ( ! $grouparray ) {
			return false;
		}

		return $grouparray['name'];
	}

	/**
	 * Copies a file.
	 *
	 * @since 2.5.0
	 *
	 * @param string    $source      Path to the source file.
	 * @param string    $destination Path to the destination file.
	 * @param bool      $overwrite   Optional. Whether to overwrite the destination file if it exists.
	 *                               Default false.
	 * @param int|false $mode        Optional. The permissions as octal number, usually 0644 for files,
	 *                               0755 for dirs. Default false.
	 * @return bool True on success, false on failure.
	 */
	public function copy( $source, $destination, $overwrite = false, $mode = false ) {
		if ( ! $overwrite && $this->exists( $destination ) ) {
			return false;
		}

		$rtval = copy( $source, $destination );

		if ( $mode ) {
			$this->chmod( $destination, $mode );
		}

		return $rtval;
	}

	/**
	 * Moves a file or directory.
	 *
	 * After moving files or directories, OPcache will need to be invalidated.
	 *
	 * If moving a directory fails, `copy_dir()` can be used for a recursive copy.
	 *
	 * Use `move_dir()` for moving directories with OPcache invalidation and a
	 * fallback to `copy_dir()`.
	 *
	 * @since 2.5.0
	 *
	 * @param string $source      Path to the source file.
	 * @param string $destination Path to the destination file.
	 * @param bool   $overwrite   Optional. Whether to overwrite the destination file if it exists.
	 *                            Default false.
	 * @return bool True on success, false on failure.
	 */
	public function move( $source, $destination, $overwrite = false ) {
		if ( ! $overwrite && $this->exists( $destination ) ) {
			return false;
		}

		if ( $overwrite && $this->exists( $destination ) && ! $this->delete( $destination, true ) ) {
			// Can't overwrite if the destination couldn't be deleted.
			return false;
		}

		// Try using rename first. if that fails (for example, source is read only) try copy.
		if ( @rename( $source, $destination ) ) {
			return true;
		}

		// Backward compatibility: Only fall back to `::copy()` for single files.
		if ( $this->is_file( $source ) && $this->copy( $source, $destination, $overwrite ) && $this->exists( $destination ) ) {
			$this->delete( $source );

			return true;
		} else {
			return false;
		}
	}

	/**
	 * Deletes a file or directory.
	 *
	 * @since 2.5.0
	 *
	 * @param string       $file      Path to the file or directory.
	 * @param bool         $recursive Optional. If set to true, deletes files and folders recursively.
	 *                                Default false.
	 * @param string|false $type      Type of resource. 'f' for file, 'd' for directory.
	 *                                Default false.
	 * @return bool True on success, false on failure.
	 */
	public function delete( $file, $recursive = false, $type = false ) {
		if ( empty( $file ) ) {
			// Some filesystems report this as /, which can cause non-expected recursive deletion of all files in the filesystem.
			return false;
		}

		$file = str_replace( '\\', '/', $file ); // For Win32, occasional problems deleting files otherwise.

		if ( 'f' === $type || $this->is_file( $file ) ) {
			return @unlink( $file );
		}

		if ( ! $recursive && $this->is_dir( $file ) ) {
			return @rmdir( $file );
		}

		// At this point it's a folder, and we're in recursive mode.
		$file     = trailingslashit( $file );
		$filelist = $this->dirlist( $file, true );

		$retval = true;

		if ( is_array( $filelist ) ) {
			foreach ( $filelist as $filename => $fileinfo ) {
				if ( ! $this->delete( $file . $filename, $recursive, $fileinfo['type'] ) ) {
					$retval = false;
				}
			}
		}

		if ( file_exists( $file ) && ! @rmdir( $file ) ) {
			$retval = false;
		}

		return $retval;
	}

	/**
	 * Checks if a file or directory exists.
	 *
	 * @since 2.5.0
	 *
	 * @param string $path Path to file or directory.
	 * @return bool Whether $path exists or not.
	 */
	public function exists( $path ) {
		return @file_exists( $path );
	}

	/**
	 * Checks if resource is a file.
	 *
	 * @since 2.5.0
	 *
	 * @param string $file File path.
	 * @return bool Whether $file is a file.
	 */
	public function is_file( $file ) {
		return @is_file( $file );
	}

	/**
	 * Checks if resource is a directory.
	 *
	 * @since 2.5.0
	 *
	 * @param string $path Directory path.
	 * @return bool Whether $path is a directory.
	 */
	public function is_dir( $path ) {
		return @is_dir( $path );
	}

	/**
	 * Checks if a file is readable.
	 *
	 * @since 2.5.0
	 *
	 * @param string $file Path to file.
	 * @return bool Whether $file is readable.
	 */
	public function is_readable( $file ) {
		return @is_readable( $file );
	}

	/**
	 * Checks if a file or directory is writable.
	 *
	 * @since 2.5.0
	 *
	 * @param string $path Path to file or directory.
	 * @return bool Whether $path is writable.
	 */
	public function is_writable( $path ) {
		return @is_writable( $path );
	}

	/**
	 * Gets the file's last access time.
	 *
	 * @since 2.5.0
	 *
	 * @param string $file Path to file.
	 * @return int|false Unix timestamp representing last access time, false on failure.
	 */
	public function atime( $file ) {
		return @fileatime( $file );
	}

	/**
	 * Gets the file modification time.
	 *
	 * @since 2.5.0
	 *
	 * @param string $file Path to file.
	 * @return int|false Unix timestamp representing modification time, false on failure.
	 */
	public function mtime( $file ) {
		return @filemtime( $file );
	}

	/**
	 * Gets the file size (in bytes).
	 *
	 * @since 2.5.0
	 *
	 * @param string $file Path to file.
	 * @return int|false Size of the file in bytes on success, false on failure.
	 */
	public function size( $file ) {
		return @filesize( $file );
	}

	/**
	 * Sets the access and modification times of a file.
	 *
	 * Note: If $file doesn't exist, it will be created.
	 *
	 * @since 2.5.0
	 *
	 * @param string $file  Path to file.
	 * @param int    $time  Optional. Modified time to set for file.
	 *                      Default 0.
	 * @param int    $atime Optional. Access time to set for file.
	 *                      Default 0.
	 * @return bool True on success, false on failure.
	 */
	public function touch( $file, $time = 0, $atime = 0 ) {
		if ( 0 === $time ) {
			$time = time();
		}

		if ( 0 === $atime ) {
			$atime = time();
		}

		return touch( $file, $time, $atime );
	}

	/**
	 * Creates a directory.
	 *
	 * @since 2.5.0
	 *
	 * @param string           $path  Path for new directory.
	 * @param int|false        $chmod Optional. The permissions as octal number (or false to skip chmod).
	 *                                Default false.
	 * @param string|int|false $chown Optional. A user name or number (or false to skip chown).
	 *                                Default false.
	 * @param string|int|false $chgrp Optional. A group name or number (or false to skip chgrp).
	 *                                Default false.
	 * @return bool True on success, false on failure.
	 */
	public function mkdir( $path, $chmod = false, $chown = false, $chgrp = false ) {
		// Safe mode fails with a trailing slash under certain PHP versions.
		$path = untrailingslashit( $path );

		if ( empty( $path ) ) {
			return false;
		}

		if ( ! $chmod ) {
			$chmod = FS_CHMOD_DIR;
		}

		if ( ! @mkdir( $path ) ) {
			return false;
		}

		$this->chmod( $path, $chmod );

		if ( $chown ) {
			$this->chown( $path, $chown );
		}

		if ( $chgrp ) {
			$this->chgrp( $path, $chgrp );
		}

		return true;
	}

	/**
	 * Deletes a directory.
	 *
	 * @since 2.5.0
	 *
	 * @param string $path      Path to directory.
	 * @param bool   $recursive Optional. Whether to recursively remove files/directories.
	 *                          Default false.
	 * @return bool True on success, false on failure.
	 */
	public function rmdir( $path, $recursive = false ) {
		return $this->delete( $path, $recursive );
	}

	/**
	 * Gets details for files in a directory or a specific file.
	 *
	 * @since 2.5.0
	 *
	 * @param string $path           Path to directory or file.
	 * @param bool   $include_hidden Optional. Whether to include details of hidden ("." prefixed) files.
	 *                               Default true.
	 * @param bool   $recursive      Optional. Whether to recursively include file details in nested directories.
	 *                               Default false.
	 * @return array|false {
	 *     Array of arrays containing file information. False if unable to list directory contents.
	 *
	 *     @type array ...$0 {
	 *         Array of file information. Note that some elements may not be available on all filesystems.
	 *
	 *         @type string           $name        Name of the file or directory.
	 *         @type string           $perms       *nix representation of permissions.
	 *         @type string           $permsn      Octal representation of permissions.
	 *         @type false            $number      File number. Always false in this context.
	 *         @type string|false     $owner       Owner name or ID, or false if not available.
	 *         @type string|false     $group       File permissions group, or false if not available.
	 *         @type int|string|false $size        Size of file in bytes. May be a numeric string.
	 *                                             False if not available.
	 *         @type int|string|false $lastmodunix Last modified unix timestamp. May be a numeric string.
	 *                                             False if not available.
	 *         @type string|false     $lastmod     Last modified month (3 letters) and day (without leading 0), or
	 *                                             false if not available.
	 *         @type string|false     $time        Last modified time, or false if not available.
	 *         @type string           $type        Type of resource. 'f' for file, 'd' for directory, 'l' for link.
	 *         @type array|false      $files       If a directory and `$recursive` is true, contains another array of
	 *                                             files. False if unable to list directory contents.
	 *     }
	 * }
	 */
	public function dirlist( $path, $include_hidden = true, $recursive = false ) {
		if ( $this->is_file( $path ) ) {
			$limit_file = basename( $path );
			$path       = dirname( $path );
		} else {
			$limit_file = false;
		}

		if ( ! $this->is_dir( $path ) || ! $this->is_readable( $path ) ) {
			return false;
		}

		$dir = dir( $path );

		if ( ! $dir ) {
			return false;
		}

		$path = trailingslashit( $path );
		$ret  = array();

		while ( false !== ( $entry = $dir->read() ) ) {
			$struc         = array();
			$struc['name'] = $entry;

			if ( '.' === $struc['name'] || '..' === $struc['name'] ) {
				continue;
			}

			if ( ! $include_hidden && '.' === $struc['name'][0] ) {
				continue;
			}

			if ( $limit_file && $struc['name'] !== $limit_file ) {
				continue;
			}

			$struc['perms']       = $this->gethchmod( $path . $entry );
			$struc['permsn']      = $this->getnumchmodfromh( $struc['perms'] );
			$struc['number']      = false;
			$struc['owner']       = $this->owner( $path . $entry );
			$struc['group']       = $this->group( $path . $entry );
			$struc['size']        = $this->size( $path . $entry );
			$struc['lastmodunix'] = $this->mtime( $path . $entry );
			$struc['lastmod']     = gmdate( 'M j', $struc['lastmodunix'] );
			$struc['time']        = gmdate( 'h:i:s', $struc['lastmodunix'] );
			$struc['type']        = $this->is_dir( $path . $entry ) ? 'd' : 'f';

			if ( 'd' === $struc['type'] ) {
				if ( $recursive ) {
					$struc['files'] = $this->dirlist( $path . $struc['name'], $include_hidden, $recursive );
				} else {
					$struc['files'] = array();
				}
			}

			$ret[ $struc['name'] ] = $struc;
		}

		$dir->close();
		unset( $dir );

		return $ret;
	}
}
dashboard.php000060400000210116151327343360007210 0ustar00<?php
/**
 * WordPress Dashboard Widget Administration Screen API
 *
 * @package WordPress
 * @subpackage Administration
 */

/**
 * Registers dashboard widgets.
 *
 * Handles POST data, sets up filters.
 *
 * @since 2.5.0
 *
 * @global array $wp_registered_widgets
 * @global array $wp_registered_widget_controls
 * @global callable[] $wp_dashboard_control_callbacks
 */
function wp_dashboard_setup() {
	global $wp_registered_widgets, $wp_registered_widget_controls, $wp_dashboard_control_callbacks;

	$screen = get_current_screen();

	/* Register Widgets and Controls */
	$wp_dashboard_control_callbacks = array();

	// Browser version
	$check_browser = wp_check_browser_version();

	if ( $check_browser && $check_browser['upgrade'] ) {
		add_filter( 'postbox_classes_dashboard_dashboard_browser_nag', 'dashboard_browser_nag_class' );

		if ( $check_browser['insecure'] ) {
			wp_add_dashboard_widget( 'dashboard_browser_nag', __( 'You are using an insecure browser!' ), 'wp_dashboard_browser_nag' );
		} else {
			wp_add_dashboard_widget( 'dashboard_browser_nag', __( 'Your browser is out of date!' ), 'wp_dashboard_browser_nag' );
		}
	}

	// PHP Version.
	$check_php = wp_check_php_version();

	if ( $check_php && current_user_can( 'update_php' ) ) {
		// If "not acceptable" the widget will be shown.
		if ( isset( $check_php['is_acceptable'] ) && ! $check_php['is_acceptable'] ) {
			add_filter( 'postbox_classes_dashboard_dashboard_php_nag', 'dashboard_php_nag_class' );

			if ( $check_php['is_lower_than_future_minimum'] ) {
				wp_add_dashboard_widget( 'dashboard_php_nag', __( 'PHP Update Required' ), 'wp_dashboard_php_nag' );
			} else {
				wp_add_dashboard_widget( 'dashboard_php_nag', __( 'PHP Update Recommended' ), 'wp_dashboard_php_nag' );
			}
		}
	}

	// Site Health.
	if ( current_user_can( 'view_site_health_checks' ) && ! is_network_admin() ) {
		if ( ! class_exists( 'WP_Site_Health' ) ) {
			require_once ABSPATH . 'wp-admin/includes/class-wp-site-health.php';
		}

		WP_Site_Health::get_instance();

		wp_enqueue_style( 'site-health' );
		wp_enqueue_script( 'site-health' );

		wp_add_dashboard_widget( 'dashboard_site_health', __( 'Site Health Status' ), 'wp_dashboard_site_health' );
	}

	// Right Now.
	if ( is_blog_admin() && current_user_can( 'edit_posts' ) ) {
		wp_add_dashboard_widget( 'dashboard_right_now', __( 'At a Glance' ), 'wp_dashboard_right_now' );
	}

	if ( is_network_admin() ) {
		wp_add_dashboard_widget( 'network_dashboard_right_now', __( 'Right Now' ), 'wp_network_dashboard_right_now' );
	}

	// Activity Widget.
	if ( is_blog_admin() ) {
		wp_add_dashboard_widget( 'dashboard_activity', __( 'Activity' ), 'wp_dashboard_site_activity' );
	}

	// QuickPress Widget.
	if ( is_blog_admin() && current_user_can( get_post_type_object( 'post' )->cap->create_posts ) ) {
		$quick_draft_title = sprintf( '<span class="hide-if-no-js">%1$s</span> <span class="hide-if-js">%2$s</span>', __( 'Quick Draft' ), __( 'Your Recent Drafts' ) );
		wp_add_dashboard_widget( 'dashboard_quick_press', $quick_draft_title, 'wp_dashboard_quick_press' );
	}

	// WordPress Events and News.
	wp_add_dashboard_widget( 'dashboard_primary', __( 'WordPress Events and News' ), 'wp_dashboard_events_news' );

	if ( is_network_admin() ) {

		/**
		 * Fires after core widgets for the Network Admin dashboard have been registered.
		 *
		 * @since 3.1.0
		 */
		do_action( 'wp_network_dashboard_setup' );

		/**
		 * Filters the list of widgets to load for the Network Admin dashboard.
		 *
		 * @since 3.1.0
		 *
		 * @param string[] $dashboard_widgets An array of dashboard widget IDs.
		 */
		$dashboard_widgets = apply_filters( 'wp_network_dashboard_widgets', array() );
	} elseif ( is_user_admin() ) {

		/**
		 * Fires after core widgets for the User Admin dashboard have been registered.
		 *
		 * @since 3.1.0
		 */
		do_action( 'wp_user_dashboard_setup' );

		/**
		 * Filters the list of widgets to load for the User Admin dashboard.
		 *
		 * @since 3.1.0
		 *
		 * @param string[] $dashboard_widgets An array of dashboard widget IDs.
		 */
		$dashboard_widgets = apply_filters( 'wp_user_dashboard_widgets', array() );
	} else {

		/**
		 * Fires after core widgets for the admin dashboard have been registered.
		 *
		 * @since 2.5.0
		 */
		do_action( 'wp_dashboard_setup' );

		/**
		 * Filters the list of widgets to load for the admin dashboard.
		 *
		 * @since 2.5.0
		 *
		 * @param string[] $dashboard_widgets An array of dashboard widget IDs.
		 */
		$dashboard_widgets = apply_filters( 'wp_dashboard_widgets', array() );
	}

	foreach ( $dashboard_widgets as $widget_id ) {
		$name = empty( $wp_registered_widgets[ $widget_id ]['all_link'] ) ? $wp_registered_widgets[ $widget_id ]['name'] : $wp_registered_widgets[ $widget_id ]['name'] . " <a href='{$wp_registered_widgets[$widget_id]['all_link']}' class='edit-box open-box'>" . __( 'View all' ) . '</a>';
		wp_add_dashboard_widget( $widget_id, $name, $wp_registered_widgets[ $widget_id ]['callback'], $wp_registered_widget_controls[ $widget_id ]['callback'] );
	}

	if ( 'POST' === $_SERVER['REQUEST_METHOD'] && isset( $_POST['widget_id'] ) ) {
		check_admin_referer( 'edit-dashboard-widget_' . $_POST['widget_id'], 'dashboard-widget-nonce' );
		ob_start(); // Hack - but the same hack wp-admin/widgets.php uses.
		wp_dashboard_trigger_widget_control( $_POST['widget_id'] );
		ob_end_clean();
		wp_redirect( remove_query_arg( 'edit' ) );
		exit;
	}

	/** This action is documented in wp-admin/includes/meta-boxes.php */
	do_action( 'do_meta_boxes', $screen->id, 'normal', '' );

	/** This action is documented in wp-admin/includes/meta-boxes.php */
	do_action( 'do_meta_boxes', $screen->id, 'side', '' );
}

/**
 * Adds a new dashboard widget.
 *
 * @since 2.7.0
 * @since 5.6.0 The `$context` and `$priority` parameters were added.
 *
 * @global callable[] $wp_dashboard_control_callbacks
 *
 * @param string   $widget_id        Widget ID  (used in the 'id' attribute for the widget).
 * @param string   $widget_name      Title of the widget.
 * @param callable $callback         Function that fills the widget with the desired content.
 *                                   The function should echo its output.
 * @param callable $control_callback Optional. Function that outputs controls for the widget. Default null.
 * @param array    $callback_args    Optional. Data that should be set as the $args property of the widget array
 *                                   (which is the second parameter passed to your callback). Default null.
 * @param string   $context          Optional. The context within the screen where the box should display.
 *                                   Accepts 'normal', 'side', 'column3', or 'column4'. Default 'normal'.
 * @param string   $priority         Optional. The priority within the context where the box should show.
 *                                   Accepts 'high', 'core', 'default', or 'low'. Default 'core'.
 */
function wp_add_dashboard_widget( $widget_id, $widget_name, $callback, $control_callback = null, $callback_args = null, $context = 'normal', $priority = 'core' ) {
	global $wp_dashboard_control_callbacks;

	$screen = get_current_screen();

	$private_callback_args = array( '__widget_basename' => $widget_name );

	if ( is_null( $callback_args ) ) {
		$callback_args = $private_callback_args;
	} elseif ( is_array( $callback_args ) ) {
		$callback_args = array_merge( $callback_args, $private_callback_args );
	}

	if ( $control_callback && is_callable( $control_callback ) && current_user_can( 'edit_dashboard' ) ) {
		$wp_dashboard_control_callbacks[ $widget_id ] = $control_callback;

		if ( isset( $_GET['edit'] ) && $widget_id === $_GET['edit'] ) {
			list($url)    = explode( '#', add_query_arg( 'edit', false ), 2 );
			$widget_name .= ' <span class="postbox-title-action"><a href="' . esc_url( $url ) . '">' . __( 'Cancel' ) . '</a></span>';
			$callback     = '_wp_dashboard_control_callback';
		} else {
			list($url)    = explode( '#', add_query_arg( 'edit', $widget_id ), 2 );
			$widget_name .= ' <span class="postbox-title-action"><a href="' . esc_url( "$url#$widget_id" ) . '" class="edit-box open-box">' . __( 'Configure' ) . '</a></span>';
		}
	}

	$side_widgets = array( 'dashboard_quick_press', 'dashboard_primary' );

	if ( in_array( $widget_id, $side_widgets, true ) ) {
		$context = 'side';
	}

	$high_priority_widgets = array( 'dashboard_browser_nag', 'dashboard_php_nag' );

	if ( in_array( $widget_id, $high_priority_widgets, true ) ) {
		$priority = 'high';
	}

	if ( empty( $context ) ) {
		$context = 'normal';
	}

	if ( empty( $priority ) ) {
		$priority = 'core';
	}

	add_meta_box( $widget_id, $widget_name, $callback, $screen, $context, $priority, $callback_args );
}

/**
 * Outputs controls for the current dashboard widget.
 *
 * @access private
 * @since 2.7.0
 *
 * @param mixed $dashboard
 * @param array $meta_box
 */
function _wp_dashboard_control_callback( $dashboard, $meta_box ) {
	echo '<form method="post" class="dashboard-widget-control-form wp-clearfix">';
	wp_dashboard_trigger_widget_control( $meta_box['id'] );
	wp_nonce_field( 'edit-dashboard-widget_' . $meta_box['id'], 'dashboard-widget-nonce' );
	echo '<input type="hidden" name="widget_id" value="' . esc_attr( $meta_box['id'] ) . '" />';
	submit_button( __( 'Save Changes' ) );
	echo '</form>';
}

/**
 * Displays the dashboard.
 *
 * @since 2.5.0
 */
function wp_dashboard() {
	$screen      = get_current_screen();
	$columns     = absint( $screen->get_columns() );
	$columns_css = '';

	if ( $columns ) {
		$columns_css = " columns-$columns";
	}
	?>
<div id="dashboard-widgets" class="metabox-holder<?php echo $columns_css; ?>">
	<div id="postbox-container-1" class="postbox-container">
	<?php do_meta_boxes( $screen->id, 'normal', '' ); ?>
	</div>
	<div id="postbox-container-2" class="postbox-container">
	<?php do_meta_boxes( $screen->id, 'side', '' ); ?>
	</div>
	<div id="postbox-container-3" class="postbox-container">
	<?php do_meta_boxes( $screen->id, 'column3', '' ); ?>
	</div>
	<div id="postbox-container-4" class="postbox-container">
	<?php do_meta_boxes( $screen->id, 'column4', '' ); ?>
	</div>
</div>

	<?php
	wp_nonce_field( 'closedpostboxes', 'closedpostboxesnonce', false );
	wp_nonce_field( 'meta-box-order', 'meta-box-order-nonce', false );
}

//
// Dashboard Widgets.
//

/**
 * Dashboard widget that displays some basic stats about the site.
 *
 * Formerly 'Right Now'. A streamlined 'At a Glance' as of 3.8.
 *
 * @since 2.7.0
 */
function wp_dashboard_right_now() {
	?>
	<div class="main">
	<ul>
	<?php
	// Posts and Pages.
	foreach ( array( 'post', 'page' ) as $post_type ) {
		$num_posts = wp_count_posts( $post_type );

		if ( $num_posts && $num_posts->publish ) {
			if ( 'post' === $post_type ) {
				/* translators: %s: Number of posts. */
				$text = _n( '%s Post', '%s Posts', $num_posts->publish );
			} else {
				/* translators: %s: Number of pages. */
				$text = _n( '%s Page', '%s Pages', $num_posts->publish );
			}

			$text             = sprintf( $text, number_format_i18n( $num_posts->publish ) );
			$post_type_object = get_post_type_object( $post_type );

			if ( $post_type_object && current_user_can( $post_type_object->cap->edit_posts ) ) {
				printf( '<li class="%1$s-count"><a href="edit.php?post_type=%1$s">%2$s</a></li>', $post_type, $text );
			} else {
				printf( '<li class="%1$s-count"><span>%2$s</span></li>', $post_type, $text );
			}
		}
	}

	// Comments.
	$num_comm = wp_count_comments();

	if ( $num_comm && ( $num_comm->approved || $num_comm->moderated ) ) {
		/* translators: %s: Number of comments. */
		$text = sprintf( _n( '%s Comment', '%s Comments', $num_comm->approved ), number_format_i18n( $num_comm->approved ) );
		?>
		<li class="comment-count">
			<a href="edit-comments.php"><?php echo $text; ?></a>
		</li>
		<?php
		$moderated_comments_count_i18n = number_format_i18n( $num_comm->moderated );
		/* translators: %s: Number of comments. */
		$text = sprintf( _n( '%s Comment in moderation', '%s Comments in moderation', $num_comm->moderated ), $moderated_comments_count_i18n );
		?>
		<li class="comment-mod-count<?php echo ! $num_comm->moderated ? ' hidden' : ''; ?>">
			<a href="edit-comments.php?comment_status=moderated" class="comments-in-moderation-text"><?php echo $text; ?></a>
		</li>
		<?php
	}

	/**
	 * Filters the array of extra elements to list in the 'At a Glance'
	 * dashboard widget.
	 *
	 * Prior to 3.8.0, the widget was named 'Right Now'. Each element
	 * is wrapped in list-item tags on output.
	 *
	 * @since 3.8.0
	 *
	 * @param string[] $items Array of extra 'At a Glance' widget items.
	 */
	$elements = apply_filters( 'dashboard_glance_items', array() );

	if ( $elements ) {
		echo '<li>' . implode( "</li>\n<li>", $elements ) . "</li>\n";
	}

	?>
	</ul>
	<?php
	update_right_now_message();

	// Check if search engines are asked not to index this site.
	if ( ! is_network_admin() && ! is_user_admin()
		&& current_user_can( 'manage_options' ) && ! get_option( 'blog_public' )
	) {

		/**
		 * Filters the link title attribute for the 'Search engines discouraged'
		 * message displayed in the 'At a Glance' dashboard widget.
		 *
		 * Prior to 3.8.0, the widget was named 'Right Now'.
		 *
		 * @since 3.0.0
		 * @since 4.5.0 The default for `$title` was updated to an empty string.
		 *
		 * @param string $title Default attribute text.
		 */
		$title = apply_filters( 'privacy_on_link_title', '' );

		/**
		 * Filters the link label for the 'Search engines discouraged' message
		 * displayed in the 'At a Glance' dashboard widget.
		 *
		 * Prior to 3.8.0, the widget was named 'Right Now'.
		 *
		 * @since 3.0.0
		 *
		 * @param string $content Default text.
		 */
		$content = apply_filters( 'privacy_on_link_text', __( 'Search engines discouraged' ) );

		$title_attr = '' === $title ? '' : " title='$title'";

		echo "<p class='search-engines-info'><a href='options-reading.php'$title_attr>$content</a></p>";
	}
	?>
	</div>
	<?php
	/*
	 * activity_box_end has a core action, but only prints content when multisite.
	 * Using an output buffer is the only way to really check if anything's displayed here.
	 */
	ob_start();

	/**
	 * Fires at the end of the 'At a Glance' dashboard widget.
	 *
	 * Prior to 3.8.0, the widget was named 'Right Now'.
	 *
	 * @since 2.5.0
	 */
	do_action( 'rightnow_end' );

	/**
	 * Fires at the end of the 'At a Glance' dashboard widget.
	 *
	 * Prior to 3.8.0, the widget was named 'Right Now'.
	 *
	 * @since 2.0.0
	 */
	do_action( 'activity_box_end' );

	$actions = ob_get_clean();

	if ( ! empty( $actions ) ) :
		?>
	<div class="sub">
		<?php echo $actions; ?>
	</div>
		<?php
	endif;
}

/**
 * @since 3.1.0
 */
function wp_network_dashboard_right_now() {
	$actions = array();

	if ( current_user_can( 'create_sites' ) ) {
		$actions['create-site'] = '<a href="' . network_admin_url( 'site-new.php' ) . '">' . __( 'Create a New Site' ) . '</a>';
	}
	if ( current_user_can( 'create_users' ) ) {
		$actions['create-user'] = '<a href="' . network_admin_url( 'user-new.php' ) . '">' . __( 'Create a New User' ) . '</a>';
	}

	$c_users = get_user_count();
	$c_blogs = get_blog_count();

	/* translators: %s: Number of users on the network. */
	$user_text = sprintf( _n( '%s user', '%s users', $c_users ), number_format_i18n( $c_users ) );
	/* translators: %s: Number of sites on the network. */
	$blog_text = sprintf( _n( '%s site', '%s sites', $c_blogs ), number_format_i18n( $c_blogs ) );

	/* translators: 1: Text indicating the number of sites on the network, 2: Text indicating the number of users on the network. */
	$sentence = sprintf( __( 'You have %1$s and %2$s.' ), $blog_text, $user_text );

	if ( $actions ) {
		echo '<ul class="subsubsub">';
		foreach ( $actions as $class => $action ) {
			$actions[ $class ] = "\t<li class='$class'>$action";
		}
		echo implode( " |</li>\n", $actions ) . "</li>\n";
		echo '</ul>';
	}
	?>
	<br class="clear" />

	<p class="youhave"><?php echo $sentence; ?></p>


	<?php
		/**
		 * Fires in the Network Admin 'Right Now' dashboard widget
		 * just before the user and site search form fields.
		 *
		 * @since MU (3.0.0)
		 */
		do_action( 'wpmuadminresult' );
	?>

	<form action="<?php echo esc_url( network_admin_url( 'users.php' ) ); ?>" method="get">
		<p>
			<label class="screen-reader-text" for="search-users">
				<?php
				/* translators: Hidden accessibility text. */
				_e( 'Search Users' );
				?>
			</label>
			<input type="search" name="s" value="" size="30" autocomplete="off" id="search-users" />
			<?php submit_button( __( 'Search Users' ), '', false, false, array( 'id' => 'submit_users' ) ); ?>
		</p>
	</form>

	<form action="<?php echo esc_url( network_admin_url( 'sites.php' ) ); ?>" method="get">
		<p>
			<label class="screen-reader-text" for="search-sites">
				<?php
				/* translators: Hidden accessibility text. */
				_e( 'Search Sites' );
				?>
			</label>
			<input type="search" name="s" value="" size="30" autocomplete="off" id="search-sites" />
			<?php submit_button( __( 'Search Sites' ), '', false, false, array( 'id' => 'submit_sites' ) ); ?>
		</p>
	</form>
	<?php
	/**
	 * Fires at the end of the 'Right Now' widget in the Network Admin dashboard.
	 *
	 * @since MU (3.0.0)
	 */
	do_action( 'mu_rightnow_end' );

	/**
	 * Fires at the end of the 'Right Now' widget in the Network Admin dashboard.
	 *
	 * @since MU (3.0.0)
	 */
	do_action( 'mu_activity_box_end' );
}

/**
 * Displays the Quick Draft widget.
 *
 * @since 3.8.0
 *
 * @global int $post_ID
 *
 * @param string|false $error_msg Optional. Error message. Default false.
 */
function wp_dashboard_quick_press( $error_msg = false ) {
	global $post_ID;

	if ( ! current_user_can( 'edit_posts' ) ) {
		return;
	}

	// Check if a new auto-draft (= no new post_ID) is needed or if the old can be used.
	$last_post_id = (int) get_user_option( 'dashboard_quick_press_last_post_id' ); // Get the last post_ID.

	if ( $last_post_id ) {
		$post = get_post( $last_post_id );

		if ( empty( $post ) || 'auto-draft' !== $post->post_status ) { // auto-draft doesn't exist anymore.
			$post = get_default_post_to_edit( 'post', true );
			update_user_option( get_current_user_id(), 'dashboard_quick_press_last_post_id', (int) $post->ID ); // Save post_ID.
		} else {
			$post->post_title = ''; // Remove the auto draft title.
		}
	} else {
		$post    = get_default_post_to_edit( 'post', true );
		$user_id = get_current_user_id();

		// Don't create an option if this is a super admin who does not belong to this site.
		if ( in_array( get_current_blog_id(), array_keys( get_blogs_of_user( $user_id ) ), true ) ) {
			update_user_option( $user_id, 'dashboard_quick_press_last_post_id', (int) $post->ID ); // Save post_ID.
		}
	}

	$post_ID = (int) $post->ID;
	?>

	<form name="post" action="<?php echo esc_url( admin_url( 'post.php' ) ); ?>" method="post" id="quick-press" class="initial-form hide-if-no-js">

		<?php
		if ( $error_msg ) {
			wp_admin_notice(
				$error_msg,
				array(
					'additional_classes' => array( 'error' ),
				)
			);
		}
		?>

		<div class="input-text-wrap" id="title-wrap">
			<label for="title">
				<?php
				/** This filter is documented in wp-admin/edit-form-advanced.php */
				echo apply_filters( 'enter_title_here', __( 'Title' ), $post );
				?>
			</label>
			<input type="text" name="post_title" id="title" autocomplete="off" />
		</div>

		<div class="textarea-wrap" id="description-wrap">
			<label for="content"><?php _e( 'Content' ); ?></label>
			<textarea name="content" id="content" placeholder="<?php esc_attr_e( 'What&#8217;s on your mind?' ); ?>" class="mceEditor" rows="3" cols="15" autocomplete="off"></textarea>
		</div>

		<p class="submit">
			<input type="hidden" name="action" id="quickpost-action" value="post-quickdraft-save" />
			<input type="hidden" name="post_ID" value="<?php echo $post_ID; ?>" />
			<input type="hidden" name="post_type" value="post" />
			<?php wp_nonce_field( 'add-post' ); ?>
			<?php submit_button( __( 'Save Draft' ), 'primary', 'save', false, array( 'id' => 'save-post' ) ); ?>
			<br class="clear" />
		</p>

	</form>
	<?php
	wp_dashboard_recent_drafts();
}

/**
 * Show recent drafts of the user on the dashboard.
 *
 * @since 2.7.0
 *
 * @param WP_Post[]|false $drafts Optional. Array of posts to display. Default false.
 */
function wp_dashboard_recent_drafts( $drafts = false ) {
	if ( ! $drafts ) {
		$query_args = array(
			'post_type'      => 'post',
			'post_status'    => 'draft',
			'author'         => get_current_user_id(),
			'posts_per_page' => 4,
			'orderby'        => 'modified',
			'order'          => 'DESC',
		);

		/**
		 * Filters the post query arguments for the 'Recent Drafts' dashboard widget.
		 *
		 * @since 4.4.0
		 *
		 * @param array $query_args The query arguments for the 'Recent Drafts' dashboard widget.
		 */
		$query_args = apply_filters( 'dashboard_recent_drafts_query_args', $query_args );

		$drafts = get_posts( $query_args );
		if ( ! $drafts ) {
			return;
		}
	}

	echo '<div class="drafts">';

	if ( count( $drafts ) > 3 ) {
		printf(
			'<p class="view-all"><a href="%s">%s</a></p>' . "\n",
			esc_url( admin_url( 'edit.php?post_status=draft' ) ),
			__( 'View all drafts' )
		);
	}

	echo '<h2 class="hide-if-no-js">' . __( 'Your Recent Drafts' ) . "</h2>\n";
	echo '<ul>';

	/* translators: Maximum number of words used in a preview of a draft on the dashboard. */
	$draft_length = (int) _x( '10', 'draft_length' );

	$drafts = array_slice( $drafts, 0, 3 );
	foreach ( $drafts as $draft ) {
		$url   = get_edit_post_link( $draft->ID );
		$title = _draft_or_post_title( $draft->ID );

		echo "<li>\n";
		printf(
			'<div class="draft-title"><a href="%s" aria-label="%s">%s</a><time datetime="%s">%s</time></div>',
			esc_url( $url ),
			/* translators: %s: Post title. */
			esc_attr( sprintf( __( 'Edit &#8220;%s&#8221;' ), $title ) ),
			esc_html( $title ),
			get_the_time( 'c', $draft ),
			get_the_time( __( 'F j, Y' ), $draft )
		);

		$the_content = wp_trim_words( $draft->post_content, $draft_length );

		if ( $the_content ) {
			echo '<p>' . $the_content . '</p>';
		}
		echo "</li>\n";
	}

	echo "</ul>\n";
	echo '</div>';
}

/**
 * Outputs a row for the Recent Comments widget.
 *
 * @access private
 * @since 2.7.0
 *
 * @global WP_Comment $comment Global comment object.
 *
 * @param WP_Comment $comment   The current comment.
 * @param bool       $show_date Optional. Whether to display the date.
 */
function _wp_dashboard_recent_comments_row( &$comment, $show_date = true ) {
	$GLOBALS['comment'] = clone $comment;

	if ( $comment->comment_post_ID > 0 ) {
		$comment_post_title = _draft_or_post_title( $comment->comment_post_ID );
		$comment_post_url   = get_the_permalink( $comment->comment_post_ID );
		$comment_post_link  = '<a href="' . esc_url( $comment_post_url ) . '">' . $comment_post_title . '</a>';
	} else {
		$comment_post_link = '';
	}

	$actions_string = '';
	if ( current_user_can( 'edit_comment', $comment->comment_ID ) ) {
		// Pre-order it: Approve | Reply | Edit | Spam | Trash.
		$actions = array(
			'approve'   => '',
			'unapprove' => '',
			'reply'     => '',
			'edit'      => '',
			'spam'      => '',
			'trash'     => '',
			'delete'    => '',
			'view'      => '',
		);

		$approve_nonce = esc_html( '_wpnonce=' . wp_create_nonce( 'approve-comment_' . $comment->comment_ID ) );
		$del_nonce     = esc_html( '_wpnonce=' . wp_create_nonce( 'delete-comment_' . $comment->comment_ID ) );

		$action_string = 'comment.php?action=%s&p=' . $comment->comment_post_ID . '&c=' . $comment->comment_ID . '&%s';

		$approve_url   = sprintf( $action_string, 'approvecomment', $approve_nonce );
		$unapprove_url = sprintf( $action_string, 'unapprovecomment', $approve_nonce );
		$spam_url      = sprintf( $action_string, 'spamcomment', $del_nonce );
		$trash_url     = sprintf( $action_string, 'trashcomment', $del_nonce );
		$delete_url    = sprintf( $action_string, 'deletecomment', $del_nonce );

		$actions['approve'] = sprintf(
			'<a href="%s" data-wp-lists="%s" class="vim-a aria-button-if-js" aria-label="%s">%s</a>',
			esc_url( $approve_url ),
			"dim:the-comment-list:comment-{$comment->comment_ID}:unapproved:e7e7d3:e7e7d3:new=approved",
			esc_attr__( 'Approve this comment' ),
			__( 'Approve' )
		);

		$actions['unapprove'] = sprintf(
			'<a href="%s" data-wp-lists="%s" class="vim-u aria-button-if-js" aria-label="%s">%s</a>',
			esc_url( $unapprove_url ),
			"dim:the-comment-list:comment-{$comment->comment_ID}:unapproved:e7e7d3:e7e7d3:new=unapproved",
			esc_attr__( 'Unapprove this comment' ),
			__( 'Unapprove' )
		);

		$actions['edit'] = sprintf(
			'<a href="%s" aria-label="%s">%s</a>',
			"comment.php?action=editcomment&amp;c={$comment->comment_ID}",
			esc_attr__( 'Edit this comment' ),
			__( 'Edit' )
		);

		$actions['reply'] = sprintf(
			'<button type="button" onclick="window.commentReply && commentReply.open(\'%s\',\'%s\');" class="vim-r button-link hide-if-no-js" aria-label="%s">%s</button>',
			$comment->comment_ID,
			$comment->comment_post_ID,
			esc_attr__( 'Reply to this comment' ),
			__( 'Reply' )
		);

		$actions['spam'] = sprintf(
			'<a href="%s" data-wp-lists="%s" class="vim-s vim-destructive aria-button-if-js" aria-label="%s">%s</a>',
			esc_url( $spam_url ),
			"delete:the-comment-list:comment-{$comment->comment_ID}::spam=1",
			esc_attr__( 'Mark this comment as spam' ),
			/* translators: "Mark as spam" link. */
			_x( 'Spam', 'verb' )
		);

		if ( ! EMPTY_TRASH_DAYS ) {
			$actions['delete'] = sprintf(
				'<a href="%s" data-wp-lists="%s" class="delete vim-d vim-destructive aria-button-if-js" aria-label="%s">%s</a>',
				esc_url( $delete_url ),
				"delete:the-comment-list:comment-{$comment->comment_ID}::trash=1",
				esc_attr__( 'Delete this comment permanently' ),
				__( 'Delete Permanently' )
			);
		} else {
			$actions['trash'] = sprintf(
				'<a href="%s" data-wp-lists="%s" class="delete vim-d vim-destructive aria-button-if-js" aria-label="%s">%s</a>',
				esc_url( $trash_url ),
				"delete:the-comment-list:comment-{$comment->comment_ID}::trash=1",
				esc_attr__( 'Move this comment to the Trash' ),
				_x( 'Trash', 'verb' )
			);
		}

		$actions['view'] = sprintf(
			'<a class="comment-link" href="%s" aria-label="%s">%s</a>',
			esc_url( get_comment_link( $comment ) ),
			esc_attr__( 'View this comment' ),
			__( 'View' )
		);

		/** This filter is documented in wp-admin/includes/class-wp-comments-list-table.php */
		$actions = apply_filters( 'comment_row_actions', array_filter( $actions ), $comment );

		$i = 0;

		foreach ( $actions as $action => $link ) {
			++$i;

			if ( ( ( 'approve' === $action || 'unapprove' === $action ) && 2 === $i )
				|| 1 === $i
			) {
				$separator = '';
			} else {
				$separator = ' | ';
			}

			// Reply and quickedit need a hide-if-no-js span.
			if ( 'reply' === $action || 'quickedit' === $action ) {
				$action .= ' hide-if-no-js';
			}

			if ( 'view' === $action && '1' !== $comment->comment_approved ) {
				$action .= ' hidden';
			}

			$actions_string .= "<span class='$action'>{$separator}{$link}</span>";
		}
	}
	?>

		<li id="comment-<?php echo $comment->comment_ID; ?>" <?php comment_class( array( 'comment-item', wp_get_comment_status( $comment ) ), $comment ); ?>>

			<?php
			$comment_row_class = '';

			if ( get_option( 'show_avatars' ) ) {
				echo get_avatar( $comment, 50, 'mystery' );
				$comment_row_class .= ' has-avatar';
			}
			?>

			<?php if ( ! $comment->comment_type || 'comment' === $comment->comment_type ) : ?>

			<div class="dashboard-comment-wrap has-row-actions <?php echo $comment_row_class; ?>">
			<p class="comment-meta">
				<?php
				// Comments might not have a post they relate to, e.g. programmatically created ones.
				if ( $comment_post_link ) {
					printf(
						/* translators: 1: Comment author, 2: Post link, 3: Notification if the comment is pending. */
						__( 'From %1$s on %2$s %3$s' ),
						'<cite class="comment-author">' . get_comment_author_link( $comment ) . '</cite>',
						$comment_post_link,
						'<span class="approve">' . __( '[Pending]' ) . '</span>'
					);
				} else {
					printf(
						/* translators: 1: Comment author, 2: Notification if the comment is pending. */
						__( 'From %1$s %2$s' ),
						'<cite class="comment-author">' . get_comment_author_link( $comment ) . '</cite>',
						'<span class="approve">' . __( '[Pending]' ) . '</span>'
					);
				}
				?>
			</p>

				<?php
			else :
				switch ( $comment->comment_type ) {
					case 'pingback':
						$type = __( 'Pingback' );
						break;
					case 'trackback':
						$type = __( 'Trackback' );
						break;
					default:
						$type = ucwords( $comment->comment_type );
				}
				$type = esc_html( $type );
				?>
			<div class="dashboard-comment-wrap has-row-actions">
			<p class="comment-meta">
				<?php
				// Pingbacks, Trackbacks or custom comment types might not have a post they relate to, e.g. programmatically created ones.
				if ( $comment_post_link ) {
					printf(
						/* translators: 1: Type of comment, 2: Post link, 3: Notification if the comment is pending. */
						_x( '%1$s on %2$s %3$s', 'dashboard' ),
						"<strong>$type</strong>",
						$comment_post_link,
						'<span class="approve">' . __( '[Pending]' ) . '</span>'
					);
				} else {
					printf(
						/* translators: 1: Type of comment, 2: Notification if the comment is pending. */
						_x( '%1$s %2$s', 'dashboard' ),
						"<strong>$type</strong>",
						'<span class="approve">' . __( '[Pending]' ) . '</span>'
					);
				}
				?>
			</p>
			<p class="comment-author"><?php comment_author_link( $comment ); ?></p>

			<?php endif; // comment_type ?>
			<blockquote><p><?php comment_excerpt( $comment ); ?></p></blockquote>
			<?php if ( $actions_string ) : ?>
			<p class="row-actions"><?php echo $actions_string; ?></p>
			<?php endif; ?>
			</div>
		</li>
	<?php
	$GLOBALS['comment'] = null;
}

/**
 * Outputs the Activity widget.
 *
 * Callback function for {@see 'dashboard_activity'}.
 *
 * @since 3.8.0
 */
function wp_dashboard_site_activity() {

	echo '<div id="activity-widget">';

	$future_posts = wp_dashboard_recent_posts(
		array(
			'max'    => 5,
			'status' => 'future',
			'order'  => 'ASC',
			'title'  => __( 'Publishing Soon' ),
			'id'     => 'future-posts',
		)
	);
	$recent_posts = wp_dashboard_recent_posts(
		array(
			'max'    => 5,
			'status' => 'publish',
			'order'  => 'DESC',
			'title'  => __( 'Recently Published' ),
			'id'     => 'published-posts',
		)
	);

	$recent_comments = wp_dashboard_recent_comments();

	if ( ! $future_posts && ! $recent_posts && ! $recent_comments ) {
		echo '<div class="no-activity">';
		echo '<p>' . __( 'No activity yet!' ) . '</p>';
		echo '</div>';
	}

	echo '</div>';
}

/**
 * Generates Publishing Soon and Recently Published sections.
 *
 * @since 3.8.0
 *
 * @param array $args {
 *     An array of query and display arguments.
 *
 *     @type int    $max     Number of posts to display.
 *     @type string $status  Post status.
 *     @type string $order   Designates ascending ('ASC') or descending ('DESC') order.
 *     @type string $title   Section title.
 *     @type string $id      The container id.
 * }
 * @return bool False if no posts were found. True otherwise.
 */
function wp_dashboard_recent_posts( $args ) {
	$query_args = array(
		'post_type'      => 'post',
		'post_status'    => $args['status'],
		'orderby'        => 'date',
		'order'          => $args['order'],
		'posts_per_page' => (int) $args['max'],
		'no_found_rows'  => true,
		'cache_results'  => true,
		'perm'           => ( 'future' === $args['status'] ) ? 'editable' : 'readable',
	);

	/**
	 * Filters the query arguments used for the Recent Posts widget.
	 *
	 * @since 4.2.0
	 *
	 * @param array $query_args The arguments passed to WP_Query to produce the list of posts.
	 */
	$query_args = apply_filters( 'dashboard_recent_posts_query_args', $query_args );

	$posts = new WP_Query( $query_args );

	if ( $posts->have_posts() ) {

		echo '<div id="' . $args['id'] . '" class="activity-block">';

		echo '<h3>' . $args['title'] . '</h3>';

		echo '<ul>';

		$today    = current_time( 'Y-m-d' );
		$tomorrow = current_datetime()->modify( '+1 day' )->format( 'Y-m-d' );
		$year     = current_time( 'Y' );

		while ( $posts->have_posts() ) {
			$posts->the_post();

			$time = get_the_time( 'U' );

			if ( gmdate( 'Y-m-d', $time ) === $today ) {
				$relative = __( 'Today' );
			} elseif ( gmdate( 'Y-m-d', $time ) === $tomorrow ) {
				$relative = __( 'Tomorrow' );
			} elseif ( gmdate( 'Y', $time ) !== $year ) {
				/* translators: Date and time format for recent posts on the dashboard, from a different calendar year, see https://www.php.net/manual/datetime.format.php */
				$relative = date_i18n( __( 'M jS Y' ), $time );
			} else {
				/* translators: Date and time format for recent posts on the dashboard, see https://www.php.net/manual/datetime.format.php */
				$relative = date_i18n( __( 'M jS' ), $time );
			}

			// Use the post edit link for those who can edit, the permalink otherwise.
			$recent_post_link = current_user_can( 'edit_post', get_the_ID() ) ? get_edit_post_link() : get_permalink();

			$draft_or_post_title = _draft_or_post_title();
			printf(
				'<li><span>%1$s</span> <a href="%2$s" aria-label="%3$s">%4$s</a></li>',
				/* translators: 1: Relative date, 2: Time. */
				sprintf( _x( '%1$s, %2$s', 'dashboard' ), $relative, get_the_time() ),
				$recent_post_link,
				/* translators: %s: Post title. */
				esc_attr( sprintf( __( 'Edit &#8220;%s&#8221;' ), $draft_or_post_title ) ),
				$draft_or_post_title
			);
		}

		echo '</ul>';
		echo '</div>';

	} else {
		return false;
	}

	wp_reset_postdata();

	return true;
}

/**
 * Show Comments section.
 *
 * @since 3.8.0
 *
 * @param int $total_items Optional. Number of comments to query. Default 5.
 * @return bool False if no comments were found. True otherwise.
 */
function wp_dashboard_recent_comments( $total_items = 5 ) {
	// Select all comment types and filter out spam later for better query performance.
	$comments = array();

	$comments_query = array(
		'number' => $total_items * 5,
		'offset' => 0,
	);

	if ( ! current_user_can( 'edit_posts' ) ) {
		$comments_query['status'] = 'approve';
	}

	while ( count( $comments ) < $total_items && $possible = get_comments( $comments_query ) ) {
		if ( ! is_array( $possible ) ) {
			break;
		}

		foreach ( $possible as $comment ) {
			if ( ! current_user_can( 'edit_post', $comment->comment_post_ID )
				&& ( post_password_required( $comment->comment_post_ID )
					|| ! current_user_can( 'read_post', $comment->comment_post_ID ) )
			) {
				// The user has no access to the post and thus cannot see the comments.
				continue;
			}

			$comments[] = $comment;

			if ( count( $comments ) === $total_items ) {
				break 2;
			}
		}

		$comments_query['offset'] += $comments_query['number'];
		$comments_query['number']  = $total_items * 10;
	}

	if ( $comments ) {
		echo '<div id="latest-comments" class="activity-block table-view-list">';
		echo '<h3>' . __( 'Recent Comments' ) . '</h3>';

		echo '<ul id="the-comment-list" data-wp-lists="list:comment">';
		foreach ( $comments as $comment ) {
			_wp_dashboard_recent_comments_row( $comment );
		}
		echo '</ul>';

		if ( current_user_can( 'edit_posts' ) ) {
			echo '<h3 class="screen-reader-text">' .
				/* translators: Hidden accessibility text. */
				__( 'View more comments' ) .
			'</h3>';
			_get_list_table( 'WP_Comments_List_Table' )->views();
		}

		wp_comment_reply( -1, false, 'dashboard', false );
		wp_comment_trashnotice();

		echo '</div>';
	} else {
		return false;
	}
	return true;
}

/**
 * Display generic dashboard RSS widget feed.
 *
 * @since 2.5.0
 *
 * @param string $widget_id
 */
function wp_dashboard_rss_output( $widget_id ) {
	$widgets = get_option( 'dashboard_widget_options' );
	echo '<div class="rss-widget">';
	wp_widget_rss_output( $widgets[ $widget_id ] );
	echo '</div>';
}

/**
 * Checks to see if all of the feed url in $check_urls are cached.
 *
 * If $check_urls is empty, look for the rss feed url found in the dashboard
 * widget options of $widget_id. If cached, call $callback, a function that
 * echoes out output for this widget. If not cache, echo a "Loading..." stub
 * which is later replaced by Ajax call (see top of /wp-admin/index.php)
 *
 * @since 2.5.0
 * @since 5.3.0 Formalized the existing and already documented `...$args` parameter
 *              by adding it to the function signature.
 *
 * @param string   $widget_id  The widget ID.
 * @param callable $callback   The callback function used to display each feed.
 * @param array    $check_urls RSS feeds.
 * @param mixed    ...$args    Optional additional parameters to pass to the callback function.
 * @return bool True on success, false on failure.
 */
function wp_dashboard_cached_rss_widget( $widget_id, $callback, $check_urls = array(), ...$args ) {
	$doing_ajax = wp_doing_ajax();
	$loading    = '<p class="widget-loading hide-if-no-js">' . __( 'Loading&hellip;' ) . '</p>';
	$loading   .= wp_get_admin_notice(
		__( 'This widget requires JavaScript.' ),
		array(
			'type'               => 'error',
			'additional_classes' => array( 'inline', 'hide-if-js' ),
		)
	);

	if ( empty( $check_urls ) ) {
		$widgets = get_option( 'dashboard_widget_options' );

		if ( empty( $widgets[ $widget_id ]['url'] ) && ! $doing_ajax ) {
			echo $loading;
			return false;
		}

		$check_urls = array( $widgets[ $widget_id ]['url'] );
	}

	$locale    = get_user_locale();
	$cache_key = 'dash_v2_' . md5( $widget_id . '_' . $locale );
	$output    = get_transient( $cache_key );

	if ( false !== $output ) {
		echo $output;
		return true;
	}

	if ( ! $doing_ajax ) {
		echo $loading;
		return false;
	}

	if ( $callback && is_callable( $callback ) ) {
		array_unshift( $args, $widget_id, $check_urls );
		ob_start();
		call_user_func_array( $callback, $args );
		// Default lifetime in cache of 12 hours (same as the feeds).
		set_transient( $cache_key, ob_get_flush(), 12 * HOUR_IN_SECONDS );
	}

	return true;
}

//
// Dashboard Widgets Controls.
//

/**
 * Calls widget control callback.
 *
 * @since 2.5.0
 *
 * @global callable[] $wp_dashboard_control_callbacks
 *
 * @param int|false $widget_control_id Optional. Registered widget ID. Default false.
 */
function wp_dashboard_trigger_widget_control( $widget_control_id = false ) {
	global $wp_dashboard_control_callbacks;

	if ( is_scalar( $widget_control_id ) && $widget_control_id
		&& isset( $wp_dashboard_control_callbacks[ $widget_control_id ] )
		&& is_callable( $wp_dashboard_control_callbacks[ $widget_control_id ] )
	) {
		call_user_func(
			$wp_dashboard_control_callbacks[ $widget_control_id ],
			'',
			array(
				'id'       => $widget_control_id,
				'callback' => $wp_dashboard_control_callbacks[ $widget_control_id ],
			)
		);
	}
}

/**
 * Sets up the RSS dashboard widget control and $args to be used as input to wp_widget_rss_form().
 *
 * Handles POST data from RSS-type widgets.
 *
 * @since 2.5.0
 *
 * @param string $widget_id
 * @param array  $form_inputs
 */
function wp_dashboard_rss_control( $widget_id, $form_inputs = array() ) {
	$widget_options = get_option( 'dashboard_widget_options' );

	if ( ! $widget_options ) {
		$widget_options = array();
	}

	if ( ! isset( $widget_options[ $widget_id ] ) ) {
		$widget_options[ $widget_id ] = array();
	}

	$number = 1; // Hack to use wp_widget_rss_form().

	$widget_options[ $widget_id ]['number'] = $number;

	if ( 'POST' === $_SERVER['REQUEST_METHOD'] && isset( $_POST['widget-rss'][ $number ] ) ) {
		$_POST['widget-rss'][ $number ]         = wp_unslash( $_POST['widget-rss'][ $number ] );
		$widget_options[ $widget_id ]           = wp_widget_rss_process( $_POST['widget-rss'][ $number ] );
		$widget_options[ $widget_id ]['number'] = $number;

		// Title is optional. If black, fill it if possible.
		if ( ! $widget_options[ $widget_id ]['title'] && isset( $_POST['widget-rss'][ $number ]['title'] ) ) {
			$rss = fetch_feed( $widget_options[ $widget_id ]['url'] );
			if ( is_wp_error( $rss ) ) {
				$widget_options[ $widget_id ]['title'] = htmlentities( __( 'Unknown Feed' ) );
			} else {
				$widget_options[ $widget_id ]['title'] = htmlentities( strip_tags( $rss->get_title() ) );
				$rss->__destruct();
				unset( $rss );
			}
		}

		update_option( 'dashboard_widget_options', $widget_options, false );

		$locale    = get_user_locale();
		$cache_key = 'dash_v2_' . md5( $widget_id . '_' . $locale );
		delete_transient( $cache_key );
	}

	wp_widget_rss_form( $widget_options[ $widget_id ], $form_inputs );
}


/**
 * Renders the Events and News dashboard widget.
 *
 * @since 4.8.0
 */
function wp_dashboard_events_news() {
	wp_print_community_events_markup();

	?>

	<div class="wordpress-news hide-if-no-js">
		<?php wp_dashboard_primary(); ?>
	</div>

	<p class="community-events-footer">
		<?php
			printf(
				'<a href="%1$s" target="_blank">%2$s <span class="screen-reader-text"> %3$s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a>',
				'https://make.wordpress.org/community/meetups-landing-page',
				__( 'Meetups' ),
				/* translators: Hidden accessibility text. */
				__( '(opens in a new tab)' )
			);
		?>

		|

		<?php
			printf(
				'<a href="%1$s" target="_blank">%2$s <span class="screen-reader-text"> %3$s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a>',
				'https://central.wordcamp.org/schedule/',
				__( 'WordCamps' ),
				/* translators: Hidden accessibility text. */
				__( '(opens in a new tab)' )
			);
		?>

		|

		<?php
			printf(
				'<a href="%1$s" target="_blank">%2$s <span class="screen-reader-text"> %3$s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a>',
				/* translators: If a Rosetta site exists (e.g. https://es.wordpress.org/news/), then use that. Otherwise, leave untranslated. */
				esc_url( _x( 'https://wordpress.org/news/', 'Events and News dashboard widget' ) ),
				__( 'News' ),
				/* translators: Hidden accessibility text. */
				__( '(opens in a new tab)' )
			);
		?>
	</p>

	<?php
}

/**
 * Prints the markup for the Community Events section of the Events and News Dashboard widget.
 *
 * @since 4.8.0
 */
function wp_print_community_events_markup() {
	$community_events_notice  = '<p class="hide-if-js">' . ( 'This widget requires JavaScript.' ) . '</p>';
	$community_events_notice .= '<p class="community-events-error-occurred" aria-hidden="true">' . __( 'An error occurred. Please try again.' ) . '</p>';
	$community_events_notice .= '<p class="community-events-could-not-locate" aria-hidden="true"></p>';

	wp_admin_notice(
		$community_events_notice,
		array(
			'type'               => 'error',
			'additional_classes' => array( 'community-events-errors', 'inline', 'hide-if-js' ),
			'paragraph_wrap'     => false,
		)
	);

	/*
	 * Hide the main element when the page first loads, because the content
	 * won't be ready until wp.communityEvents.renderEventsTemplate() has run.
	 */
	?>
	<div id="community-events" class="community-events" aria-hidden="true">
		<div class="activity-block">
			<p>
				<span id="community-events-location-message"></span>

				<button class="button-link community-events-toggle-location" aria-expanded="false">
					<span class="dashicons dashicons-location" aria-hidden="true"></span>
					<span class="community-events-location-edit"><?php _e( 'Select location' ); ?></span>
				</button>
			</p>

			<form class="community-events-form" aria-hidden="true" action="<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>" method="post">
				<label for="community-events-location">
					<?php _e( 'City:' ); ?>
				</label>
				<?php
				/* translators: Replace with a city related to your locale.
				 * Test that it matches the expected location and has upcoming
				 * events before including it. If no cities related to your
				 * locale have events, then use a city related to your locale
				 * that would be recognizable to most users. Use only the city
				 * name itself, without any region or country. Use the endonym
				 * (native locale name) instead of the English name if possible.
				 */
				?>
				<input id="community-events-location" class="regular-text" type="text" name="community-events-location" placeholder="<?php esc_attr_e( 'Cincinnati' ); ?>" />

				<?php submit_button( __( 'Submit' ), 'secondary', 'community-events-submit', false ); ?>

				<button class="community-events-cancel button-link" type="button" aria-expanded="false">
					<?php _e( 'Cancel' ); ?>
				</button>

				<span class="spinner"></span>
			</form>
		</div>

		<ul class="community-events-results activity-block last"></ul>
	</div>

	<?php
}

/**
 * Renders the events templates for the Event and News widget.
 *
 * @since 4.8.0
 */
function wp_print_community_events_templates() {
	?>

	<script id="tmpl-community-events-attend-event-near" type="text/template">
		<?php
		printf(
			/* translators: %s: The name of a city. */
			__( 'Attend an upcoming event near %s.' ),
			'<strong>{{ data.location.description }}</strong>'
		);
		?>
	</script>

	<script id="tmpl-community-events-could-not-locate" type="text/template">
		<?php
		printf(
			/* translators: %s is the name of the city we couldn't locate.
			 * Replace the examples with cities in your locale, but test
			 * that they match the expected location before including them.
			 * Use endonyms (native locale names) whenever possible.
			 */
			__( '%s could not be located. Please try another nearby city. For example: Kansas City; Springfield; Portland.' ),
			'<em>{{data.unknownCity}}</em>'
		);
		?>
	</script>

	<script id="tmpl-community-events-event-list" type="text/template">
		<# _.each( data.events, function( event ) { #>
			<li class="event event-{{ event.type }} wp-clearfix">
				<div class="event-info">
					<div class="dashicons event-icon" aria-hidden="true"></div>
					<div class="event-info-inner">
						<a class="event-title" href="{{ event.url }}">{{ event.title }}</a>
						<# if ( event.type ) {
							const titleCaseEventType = event.type.replace(
								/\w\S*/g,
								function ( type ) { return type.charAt(0).toUpperCase() + type.substr(1).toLowerCase(); }
							);
						#>
							{{ 'wordcamp' === event.type ? 'WordCamp' : titleCaseEventType }}
							<span class="ce-separator"></span>
						<# } #>
						<span class="event-city">{{ event.location.location }}</span>
					</div>
				</div>

				<div class="event-date-time">
					<span class="event-date">{{ event.user_formatted_date }}</span>
					<# if ( 'meetup' === event.type ) { #>
						<span class="event-time">
							{{ event.user_formatted_time }} {{ event.timeZoneAbbreviation }}
						</span>
					<# } #>
				</div>
			</li>
		<# } ) #>

		<# if ( data.events.length <= 2 ) { #>
			<li class="event-none">
				<?php
				printf(
					/* translators: %s: Localized meetup organization documentation URL. */
					__( 'Want more events? <a href="%s">Help organize the next one</a>!' ),
					__( 'https://make.wordpress.org/community/organize-event-landing-page/' )
				);
				?>
			</li>
		<# } #>

	</script>

	<script id="tmpl-community-events-no-upcoming-events" type="text/template">
		<li class="event-none">
			<# if ( data.location.description ) { #>
				<?php
				printf(
					/* translators: 1: The city the user searched for, 2: Meetup organization documentation URL. */
					__( 'There are no events scheduled near %1$s at the moment. Would you like to <a href="%2$s">organize a WordPress event</a>?' ),
					'{{ data.location.description }}',
					__( 'https://make.wordpress.org/community/handbook/meetup-organizer/welcome/' )
				);
				?>

			<# } else { #>
				<?php
				printf(
					/* translators: %s: Meetup organization documentation URL. */
					__( 'There are no events scheduled near you at the moment. Would you like to <a href="%s">organize a WordPress event</a>?' ),
					__( 'https://make.wordpress.org/community/handbook/meetup-organizer/welcome/' )
				);
				?>
			<# } #>
		</li>
	</script>
	<?php
}

/**
 * 'WordPress Events and News' dashboard widget.
 *
 * @since 2.7.0
 * @since 4.8.0 Removed popular plugins feed.
 */
function wp_dashboard_primary() {
	$feeds = array(
		'news'   => array(

			/**
			 * Filters the primary link URL for the 'WordPress Events and News' dashboard widget.
			 *
			 * @since 2.5.0
			 *
			 * @param string $link The widget's primary link URL.
			 */
			'link'         => apply_filters( 'dashboard_primary_link', __( 'https://wordpress.org/news/' ) ),

			/**
			 * Filters the primary feed URL for the 'WordPress Events and News' dashboard widget.
			 *
			 * @since 2.3.0
			 *
			 * @param string $url The widget's primary feed URL.
			 */
			'url'          => apply_filters( 'dashboard_primary_feed', __( 'https://wordpress.org/news/feed/' ) ),

			/**
			 * Filters the primary link title for the 'WordPress Events and News' dashboard widget.
			 *
			 * @since 2.3.0
			 *
			 * @param string $title Title attribute for the widget's primary link.
			 */
			'title'        => apply_filters( 'dashboard_primary_title', __( 'WordPress Blog' ) ),
			'items'        => 2,
			'show_summary' => 0,
			'show_author'  => 0,
			'show_date'    => 0,
		),
		'planet' => array(

			/**
			 * Filters the secondary link URL for the 'WordPress Events and News' dashboard widget.
			 *
			 * @since 2.3.0
			 *
			 * @param string $link The widget's secondary link URL.
			 */
			'link'         => apply_filters(
				'dashboard_secondary_link',
				/* translators: Link to the Planet website of the locale. */
				__( 'https://planet.wordpress.org/' )
			),

			/**
			 * Filters the secondary feed URL for the 'WordPress Events and News' dashboard widget.
			 *
			 * @since 2.3.0
			 *
			 * @param string $url The widget's secondary feed URL.
			 */
			'url'          => apply_filters(
				'dashboard_secondary_feed',
				/* translators: Link to the Planet feed of the locale. */
				__( 'https://planet.wordpress.org/feed/' )
			),

			/**
			 * Filters the secondary link title for the 'WordPress Events and News' dashboard widget.
			 *
			 * @since 2.3.0
			 *
			 * @param string $title Title attribute for the widget's secondary link.
			 */
			'title'        => apply_filters( 'dashboard_secondary_title', __( 'Other WordPress News' ) ),

			/**
			 * Filters the number of secondary link items for the 'WordPress Events and News' dashboard widget.
			 *
			 * @since 4.4.0
			 *
			 * @param string $items How many items to show in the secondary feed.
			 */
			'items'        => apply_filters( 'dashboard_secondary_items', 3 ),
			'show_summary' => 0,
			'show_author'  => 0,
			'show_date'    => 0,
		),
	);

	wp_dashboard_cached_rss_widget( 'dashboard_primary', 'wp_dashboard_primary_output', $feeds );
}

/**
 * Displays the WordPress events and news feeds.
 *
 * @since 3.8.0
 * @since 4.8.0 Removed popular plugins feed.
 *
 * @param string $widget_id Widget ID.
 * @param array  $feeds     Array of RSS feeds.
 */
function wp_dashboard_primary_output( $widget_id, $feeds ) {
	foreach ( $feeds as $type => $args ) {
		$args['type'] = $type;
		echo '<div class="rss-widget">';
			wp_widget_rss_output( $args['url'], $args );
		echo '</div>';
	}
}

/**
 * Displays file upload quota on dashboard.
 *
 * Runs on the {@see 'activity_box_end'} hook in wp_dashboard_right_now().
 *
 * @since 3.0.0
 *
 * @return true|void True if not multisite, user can't upload files, or the space check option is disabled.
 */
function wp_dashboard_quota() {
	if ( ! is_multisite() || ! current_user_can( 'upload_files' )
		|| get_site_option( 'upload_space_check_disabled' )
	) {
		return true;
	}

	$quota = get_space_allowed();
	$used  = get_space_used();

	if ( $used > $quota ) {
		$percentused = '100';
	} else {
		$percentused = ( $used / $quota ) * 100;
	}

	$used_class  = ( $percentused >= 70 ) ? ' warning' : '';
	$used        = round( $used, 2 );
	$percentused = number_format( $percentused );

	?>
	<h3 class="mu-storage"><?php _e( 'Storage Space' ); ?></h3>
	<div class="mu-storage">
	<ul>
		<li class="storage-count">
			<?php
			$text = sprintf(
				/* translators: %s: Number of megabytes. */
				__( '%s MB Space Allowed' ),
				number_format_i18n( $quota )
			);
			printf(
				'<a href="%1$s">%2$s<span class="screen-reader-text"> (%3$s)</span></a>',
				esc_url( admin_url( 'upload.php' ) ),
				$text,
				/* translators: Hidden accessibility text. */
				__( 'Manage Uploads' )
			);
			?>
		</li><li class="storage-count <?php echo $used_class; ?>">
			<?php
			$text = sprintf(
				/* translators: 1: Number of megabytes, 2: Percentage. */
				__( '%1$s MB (%2$s%%) Space Used' ),
				number_format_i18n( $used, 2 ),
				$percentused
			);
			printf(
				'<a href="%1$s" class="musublink">%2$s<span class="screen-reader-text"> (%3$s)</span></a>',
				esc_url( admin_url( 'upload.php' ) ),
				$text,
				/* translators: Hidden accessibility text. */
				__( 'Manage Uploads' )
			);
			?>
		</li>
	</ul>
	</div>
	<?php
}

/**
 * Displays the browser update nag.
 *
 * @since 3.2.0
 * @since 5.8.0 Added a special message for Internet Explorer users.
 *
 * @global bool $is_IE
 */
function wp_dashboard_browser_nag() {
	global $is_IE;

	$notice   = '';
	$response = wp_check_browser_version();

	if ( $response ) {
		if ( $is_IE ) {
			$msg = __( 'Internet Explorer does not give you the best WordPress experience. Switch to Microsoft Edge, or another more modern browser to get the most from your site.' );
		} elseif ( $response['insecure'] ) {
			$msg = sprintf(
				/* translators: %s: Browser name and link. */
				__( "It looks like you're using an insecure version of %s. Using an outdated browser makes your computer unsafe. For the best WordPress experience, please update your browser." ),
				sprintf( '<a href="%s">%s</a>', esc_url( $response['update_url'] ), esc_html( $response['name'] ) )
			);
		} else {
			$msg = sprintf(
				/* translators: %s: Browser name and link. */
				__( "It looks like you're using an old version of %s. For the best WordPress experience, please update your browser." ),
				sprintf( '<a href="%s">%s</a>', esc_url( $response['update_url'] ), esc_html( $response['name'] ) )
			);
		}

		$browser_nag_class = '';
		if ( ! empty( $response['img_src'] ) ) {
			$img_src = ( is_ssl() && ! empty( $response['img_src_ssl'] ) ) ? $response['img_src_ssl'] : $response['img_src'];

			$notice           .= '<div class="alignright browser-icon"><img src="' . esc_url( $img_src ) . '" alt="" /></div>';
			$browser_nag_class = ' has-browser-icon';
		}
		$notice .= "<p class='browser-update-nag{$browser_nag_class}'>{$msg}</p>";

		$browsehappy = 'https://browsehappy.com/';
		$locale      = get_user_locale();
		if ( 'en_US' !== $locale ) {
			$browsehappy = add_query_arg( 'locale', $locale, $browsehappy );
		}

		if ( $is_IE ) {
			$msg_browsehappy = sprintf(
				/* translators: %s: Browse Happy URL. */
				__( 'Learn how to <a href="%s" class="update-browser-link">browse happy</a>' ),
				esc_url( $browsehappy )
			);
		} else {
			$msg_browsehappy = sprintf(
				/* translators: 1: Browser update URL, 2: Browser name, 3: Browse Happy URL. */
				__( '<a href="%1$s" class="update-browser-link">Update %2$s</a> or learn how to <a href="%3$s" class="browse-happy-link">browse happy</a>' ),
				esc_attr( $response['update_url'] ),
				esc_html( $response['name'] ),
				esc_url( $browsehappy )
			);
		}

		$notice .= '<p>' . $msg_browsehappy . '</p>';
		$notice .= '<p class="hide-if-no-js"><a href="" class="dismiss" aria-label="' . esc_attr__( 'Dismiss the browser warning panel' ) . '">' . __( 'Dismiss' ) . '</a></p>';
		$notice .= '<div class="clear"></div>';
	}

	/**
	 * Filters the notice output for the 'Browse Happy' nag meta box.
	 *
	 * @since 3.2.0
	 *
	 * @param string      $notice   The notice content.
	 * @param array|false $response An array containing web browser information, or
	 *                              false on failure. See wp_check_browser_version().
	 */
	echo apply_filters( 'browse-happy-notice', $notice, $response ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
}

/**
 * Adds an additional class to the browser nag if the current version is insecure.
 *
 * @since 3.2.0
 *
 * @param string[] $classes Array of meta box classes.
 * @return string[] Modified array of meta box classes.
 */
function dashboard_browser_nag_class( $classes ) {
	$response = wp_check_browser_version();

	if ( $response && $response['insecure'] ) {
		$classes[] = 'browser-insecure';
	}

	return $classes;
}

/**
 * Checks if the user needs a browser update.
 *
 * @since 3.2.0
 *
 * @return array|false Array of browser data on success, false on failure.
 */
function wp_check_browser_version() {
	if ( empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
		return false;
	}

	$key = md5( $_SERVER['HTTP_USER_AGENT'] );

	$response = get_site_transient( 'browser_' . $key );

	if ( false === $response ) {
		$url     = 'http://api.wordpress.org/core/browse-happy/1.1/';
		$options = array(
			'body'       => array( 'useragent' => $_SERVER['HTTP_USER_AGENT'] ),
			'user-agent' => 'WordPress/' . wp_get_wp_version() . '; ' . home_url( '/' ),
		);

		if ( wp_http_supports( array( 'ssl' ) ) ) {
			$url = set_url_scheme( $url, 'https' );
		}

		$response = wp_remote_post( $url, $options );

		if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
			return false;
		}

		/**
		 * Response should be an array with:
		 *  'platform' - string - A user-friendly platform name, if it can be determined
		 *  'name' - string - A user-friendly browser name
		 *  'version' - string - The version of the browser the user is using
		 *  'current_version' - string - The most recent version of the browser
		 *  'upgrade' - boolean - Whether the browser needs an upgrade
		 *  'insecure' - boolean - Whether the browser is deemed insecure
		 *  'update_url' - string - The url to visit to upgrade
		 *  'img_src' - string - An image representing the browser
		 *  'img_src_ssl' - string - An image (over SSL) representing the browser
		 */
		$response = json_decode( wp_remote_retrieve_body( $response ), true );

		if ( ! is_array( $response ) ) {
			return false;
		}

		set_site_transient( 'browser_' . $key, $response, WEEK_IN_SECONDS );
	}

	return $response;
}

/**
 * Displays the PHP update nag.
 *
 * @since 5.1.0
 */
function wp_dashboard_php_nag() {
	$response = wp_check_php_version();

	if ( ! $response ) {
		return;
	}

	if ( isset( $response['is_secure'] ) && ! $response['is_secure'] ) {
		// The `is_secure` array key name doesn't actually imply this is a secure version of PHP. It only means it receives security updates.

		if ( $response['is_lower_than_future_minimum'] ) {
			$message = sprintf(
				/* translators: %s: The server PHP version. */
				__( 'Your site is running on an outdated version of PHP (%s), which does not receive security updates and soon will not be supported by WordPress. Ensure that PHP is updated on your server as soon as possible. Otherwise you will not be able to upgrade WordPress.' ),
				PHP_VERSION
			);
		} else {
			$message = sprintf(
				/* translators: %s: The server PHP version. */
				__( 'Your site is running on an outdated version of PHP (%s), which does not receive security updates. It should be updated.' ),
				PHP_VERSION
			);
		}
	} elseif ( $response['is_lower_than_future_minimum'] ) {
		$message = sprintf(
			/* translators: %s: The server PHP version. */
			__( 'Your site is running on an outdated version of PHP (%s), which soon will not be supported by WordPress. Ensure that PHP is updated on your server as soon as possible. Otherwise you will not be able to upgrade WordPress.' ),
			PHP_VERSION
		);
	} else {
		$message = sprintf(
			/* translators: %s: The server PHP version. */
			__( 'Your site is running on an outdated version of PHP (%s), which should be updated.' ),
			PHP_VERSION
		);
	}
	?>
	<p class="bigger-bolder-text"><?php echo $message; ?></p>

	<p><?php _e( 'What is PHP and how does it affect my site?' ); ?></p>
	<p>
		<?php _e( 'PHP is one of the programming languages used to build WordPress. Newer versions of PHP receive regular security updates and may increase your site&#8217;s performance.' ); ?>
		<?php
		if ( ! empty( $response['recommended_version'] ) ) {
			printf(
				/* translators: %s: The minimum recommended PHP version. */
				__( 'The minimum recommended version of PHP is %s.' ),
				$response['recommended_version']
			);
		}
		?>
	</p>

	<p class="button-container">
		<?php
		printf(
			'<a class="button button-primary" href="%1$s" target="_blank">%2$s<span class="screen-reader-text"> %3$s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a>',
			esc_url( wp_get_update_php_url() ),
			__( 'Learn more about updating PHP' ),
			/* translators: Hidden accessibility text. */
			__( '(opens in a new tab)' )
		);
		?>
	</p>
	<?php

	wp_update_php_annotation();
	wp_direct_php_update_button();
}

/**
 * Adds an additional class to the PHP nag if the current version is insecure.
 *
 * @since 5.1.0
 *
 * @param string[] $classes Array of meta box classes.
 * @return string[] Modified array of meta box classes.
 */
function dashboard_php_nag_class( $classes ) {
	$response = wp_check_php_version();

	if ( ! $response ) {
		return $classes;
	}

	if ( isset( $response['is_secure'] ) && ! $response['is_secure'] ) {
		$classes[] = 'php-no-security-updates';
	} elseif ( $response['is_lower_than_future_minimum'] ) {
		$classes[] = 'php-version-lower-than-future-minimum';
	}

	return $classes;
}

/**
 * Displays the Site Health Status widget.
 *
 * @since 5.4.0
 */
function wp_dashboard_site_health() {
	$get_issues = get_transient( 'health-check-site-status-result' );

	$issue_counts = array();

	if ( false !== $get_issues ) {
		$issue_counts = json_decode( $get_issues, true );
	}

	if ( ! is_array( $issue_counts ) || ! $issue_counts ) {
		$issue_counts = array(
			'good'        => 0,
			'recommended' => 0,
			'critical'    => 0,
		);
	}

	$issues_total = $issue_counts['recommended'] + $issue_counts['critical'];
	?>
	<div class="health-check-widget">
		<div class="health-check-widget-title-section site-health-progress-wrapper loading hide-if-no-js">
			<div class="site-health-progress">
				<svg aria-hidden="true" focusable="false" width="100%" height="100%" viewBox="0 0 200 200" version="1.1" xmlns="http://www.w3.org/2000/svg">
					<circle r="90" cx="100" cy="100" fill="transparent" stroke-dasharray="565.48" stroke-dashoffset="0"></circle>
					<circle id="bar" r="90" cx="100" cy="100" fill="transparent" stroke-dasharray="565.48" stroke-dashoffset="0"></circle>
				</svg>
			</div>
			<div class="site-health-progress-label">
				<?php if ( false === $get_issues ) : ?>
					<?php _e( 'No information yet&hellip;' ); ?>
				<?php else : ?>
					<?php _e( 'Results are still loading&hellip;' ); ?>
				<?php endif; ?>
			</div>
		</div>

		<div class="site-health-details">
			<?php if ( false === $get_issues ) : ?>
				<p>
					<?php
					printf(
						/* translators: %s: URL to Site Health screen. */
						__( 'Site health checks will automatically run periodically to gather information about your site. You can also <a href="%s">visit the Site Health screen</a> to gather information about your site now.' ),
						esc_url( admin_url( 'site-health.php' ) )
					);
					?>
				</p>
			<?php else : ?>
				<p>
					<?php if ( $issues_total <= 0 ) : ?>
						<?php _e( 'Great job! Your site currently passes all site health checks.' ); ?>
					<?php elseif ( 1 === (int) $issue_counts['critical'] ) : ?>
						<?php _e( 'Your site has a critical issue that should be addressed as soon as possible to improve its performance and security.' ); ?>
					<?php elseif ( $issue_counts['critical'] > 1 ) : ?>
						<?php _e( 'Your site has critical issues that should be addressed as soon as possible to improve its performance and security.' ); ?>
					<?php elseif ( 1 === (int) $issue_counts['recommended'] ) : ?>
						<?php _e( 'Your site&#8217;s health is looking good, but there is still one thing you can do to improve its performance and security.' ); ?>
					<?php else : ?>
						<?php _e( 'Your site&#8217;s health is looking good, but there are still some things you can do to improve its performance and security.' ); ?>
					<?php endif; ?>
				</p>
			<?php endif; ?>

			<?php if ( $issues_total > 0 && false !== $get_issues ) : ?>
				<p>
					<?php
					printf(
						/* translators: 1: Number of issues. 2: URL to Site Health screen. */
						_n(
							'Take a look at the <strong>%1$d item</strong> on the <a href="%2$s">Site Health screen</a>.',
							'Take a look at the <strong>%1$d items</strong> on the <a href="%2$s">Site Health screen</a>.',
							$issues_total
						),
						$issues_total,
						esc_url( admin_url( 'site-health.php' ) )
					);
					?>
				</p>
			<?php endif; ?>
		</div>
	</div>

	<?php
}

/**
 * Outputs empty dashboard widget to be populated by JS later.
 *
 * Usable by plugins.
 *
 * @since 2.5.0
 */
function wp_dashboard_empty() {}

/**
 * Displays a welcome panel to introduce users to WordPress.
 *
 * @since 3.3.0
 * @since 5.9.0 Send users to the Site Editor if the active theme is block-based.
 */
function wp_welcome_panel() {
	list( $display_version ) = explode( '-', wp_get_wp_version() );
	$can_customize           = current_user_can( 'customize' );
	$is_block_theme          = wp_is_block_theme();
	?>
	<div class="welcome-panel-content">
	<div class="welcome-panel-header">
		<div class="welcome-panel-header-image">
			<?php echo file_get_contents( dirname( __DIR__ ) . '/images/dashboard-background.svg' ); ?>
		</div>
		<h2><?php _e( 'Welcome to WordPress!' ); ?></h2>
		<p>
			<a href="<?php echo esc_url( admin_url( 'about.php' ) ); ?>">
			<?php
				/* translators: %s: Current WordPress version. */
				printf( __( 'Learn more about the %s version.' ), esc_html( $display_version ) );
			?>
			</a>
		</p>
	</div>
	<div class="welcome-panel-column-container">
		<div class="welcome-panel-column">
			<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
				<rect width="48" height="48" rx="4" fill="#1E1E1E"/>
				<path fill-rule="evenodd" clip-rule="evenodd" d="M32.0668 17.0854L28.8221 13.9454L18.2008 24.671L16.8983 29.0827L21.4257 27.8309L32.0668 17.0854ZM16 32.75H24V31.25H16V32.75Z" fill="white"/>
			</svg>
			<div class="welcome-panel-column-content">
				<h3><?php _e( 'Author rich content with blocks and patterns' ); ?></h3>
				<p><?php _e( 'Block patterns are pre-configured block layouts. Use them to get inspired or create new pages in a flash.' ); ?></p>
				<a href="<?php echo esc_url( admin_url( 'post-new.php?post_type=page' ) ); ?>"><?php _e( 'Add a new page' ); ?></a>
			</div>
		</div>
		<div class="welcome-panel-column">
			<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
				<rect width="48" height="48" rx="4" fill="#1E1E1E"/>
				<path fill-rule="evenodd" clip-rule="evenodd" d="M18 16h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H18a2 2 0 0 1-2-2V18a2 2 0 0 1 2-2zm12 1.5H18a.5.5 0 0 0-.5.5v3h13v-3a.5.5 0 0 0-.5-.5zm.5 5H22v8h8a.5.5 0 0 0 .5-.5v-7.5zm-10 0h-3V30a.5.5 0 0 0 .5.5h2.5v-8z" fill="#fff"/>
			</svg>
			<div class="welcome-panel-column-content">
			<?php if ( $is_block_theme ) : ?>
				<h3><?php _e( 'Customize your entire site with block themes' ); ?></h3>
				<p><?php _e( 'Design everything on your site &#8212; from the header down to the footer, all using blocks and patterns.' ); ?></p>
				<a href="<?php echo esc_url( admin_url( 'site-editor.php' ) ); ?>"><?php _e( 'Open site editor' ); ?></a>
			<?php else : ?>
				<h3><?php _e( 'Start Customizing' ); ?></h3>
				<p><?php _e( 'Configure your site&#8217;s logo, header, menus, and more in the Customizer.' ); ?></p>
				<?php if ( $can_customize ) : ?>
					<a class="load-customize hide-if-no-customize" href="<?php echo wp_customize_url(); ?>"><?php _e( 'Open the Customizer' ); ?></a>
				<?php endif; ?>
			<?php endif; ?>
			</div>
		</div>
		<div class="welcome-panel-column">
			<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
				<rect width="48" height="48" rx="4" fill="#1E1E1E"/>
				<path fill-rule="evenodd" clip-rule="evenodd" d="M31 24a7 7 0 0 1-7 7V17a7 7 0 0 1 7 7zm-7-8a8 8 0 1 1 0 16 8 8 0 0 1 0-16z" fill="#fff"/>
			</svg>
			<div class="welcome-panel-column-content">
			<?php if ( $is_block_theme ) : ?>
				<h3><?php _e( 'Switch up your site&#8217;s look & feel with Styles' ); ?></h3>
				<p><?php _e( 'Tweak your site, or give it a whole new look! Get creative &#8212; how about a new color palette or font?' ); ?></p>
				<a href="<?php echo esc_url( admin_url( '/site-editor.php?path=%2Fwp_global_styles' ) ); ?>"><?php _e( 'Edit styles' ); ?></a>
			<?php else : ?>
				<h3><?php _e( 'Discover a new way to build your site.' ); ?></h3>
				<p><?php _e( 'There is a new kind of WordPress theme, called a block theme, that lets you build the site you&#8217;ve always wanted &#8212; with blocks and styles.' ); ?></p>
				<a href="<?php echo esc_url( __( 'https://wordpress.org/documentation/article/block-themes/' ) ); ?>"><?php _e( 'Learn about block themes' ); ?></a>
			<?php endif; ?>
			</div>
		</div>
	</div>
	</div>
	<?php
}
class-walker-category-checklist.php000060400000011743151327343360013440 0ustar00<?php
/**
 * Taxonomy API: Walker_Category_Checklist class
 *
 * @package WordPress
 * @subpackage Administration
 * @since 4.4.0
 */

/**
 * Core walker class to output an unordered list of category checkbox input elements.
 *
 * @since 2.5.1
 *
 * @see Walker
 * @see wp_category_checklist()
 * @see wp_terms_checklist()
 */
class Walker_Category_Checklist extends Walker {
	public $tree_type = 'category';
	public $db_fields = array(
		'parent' => 'parent',
		'id'     => 'term_id',
	); // TODO: Decouple this.

	/**
	 * Starts the list before the elements are added.
	 *
	 * @see Walker:start_lvl()
	 *
	 * @since 2.5.1
	 *
	 * @param string $output Used to append additional content (passed by reference).
	 * @param int    $depth  Depth of category. Used for tab indentation.
	 * @param array  $args   An array of arguments. See {@see wp_terms_checklist()}.
	 */
	public function start_lvl( &$output, $depth = 0, $args = array() ) {
		$indent  = str_repeat( "\t", $depth );
		$output .= "$indent<ul class='children'>\n";
	}

	/**
	 * Ends the list of after the elements are added.
	 *
	 * @see Walker::end_lvl()
	 *
	 * @since 2.5.1
	 *
	 * @param string $output Used to append additional content (passed by reference).
	 * @param int    $depth  Depth of category. Used for tab indentation.
	 * @param array  $args   An array of arguments. See {@see wp_terms_checklist()}.
	 */
	public function end_lvl( &$output, $depth = 0, $args = array() ) {
		$indent  = str_repeat( "\t", $depth );
		$output .= "$indent</ul>\n";
	}

	/**
	 * Start the element output.
	 *
	 * @see Walker::start_el()
	 *
	 * @since 2.5.1
	 * @since 5.9.0 Renamed `$category` to `$data_object` and `$id` to `$current_object_id`
	 *              to match parent class for PHP 8 named parameter support.
	 *
	 * @param string  $output            Used to append additional content (passed by reference).
	 * @param WP_Term $data_object       The current term object.
	 * @param int     $depth             Depth of the term in reference to parents. Default 0.
	 * @param array   $args              An array of arguments. See {@see wp_terms_checklist()}.
	 * @param int     $current_object_id Optional. ID of the current term. Default 0.
	 */
	public function start_el( &$output, $data_object, $depth = 0, $args = array(), $current_object_id = 0 ) {
		// Restores the more descriptive, specific name for use within this method.
		$category = $data_object;

		if ( empty( $args['taxonomy'] ) ) {
			$taxonomy = 'category';
		} else {
			$taxonomy = $args['taxonomy'];
		}

		if ( 'category' === $taxonomy ) {
			$name = 'post_category';
		} else {
			$name = 'tax_input[' . $taxonomy . ']';
		}

		$args['popular_cats'] = ! empty( $args['popular_cats'] ) ? array_map( 'intval', $args['popular_cats'] ) : array();

		$class = in_array( $category->term_id, $args['popular_cats'], true ) ? ' class="popular-category"' : '';

		$args['selected_cats'] = ! empty( $args['selected_cats'] ) ? array_map( 'intval', $args['selected_cats'] ) : array();

		if ( ! empty( $args['list_only'] ) ) {
			$aria_checked = 'false';
			$inner_class  = 'category';

			if ( in_array( $category->term_id, $args['selected_cats'], true ) ) {
				$inner_class .= ' selected';
				$aria_checked = 'true';
			}

			$output .= "\n" . '<li' . $class . '>' .
				'<div class="' . $inner_class . '" data-term-id=' . $category->term_id .
				' tabindex="0" role="checkbox" aria-checked="' . $aria_checked . '">' .
				/** This filter is documented in wp-includes/category-template.php */
				esc_html( apply_filters( 'the_category', $category->name, '', '' ) ) . '</div>';
		} else {
			$is_selected         = in_array( $category->term_id, $args['selected_cats'], true );
			$is_disabled         = ! empty( $args['disabled'] );
			$li_element_id       = wp_unique_prefixed_id( "in-{$taxonomy}-{$category->term_id}-" );
			$checkbox_element_id = wp_unique_prefixed_id( "in-{$taxonomy}-{$category->term_id}-" );

			$output .= "\n<li id='" . esc_attr( $li_element_id ) . "'$class>" .
				'<label class="selectit"><input value="' . $category->term_id . '" type="checkbox" name="' . $name . '[]" id="' . esc_attr( $checkbox_element_id ) . '"' .
				checked( $is_selected, true, false ) .
				disabled( $is_disabled, true, false ) . ' /> ' .
				/** This filter is documented in wp-includes/category-template.php */
				esc_html( apply_filters( 'the_category', $category->name, '', '' ) ) . '</label>';
		}
	}

	/**
	 * Ends the element output, if needed.
	 *
	 * @see Walker::end_el()
	 *
	 * @since 2.5.1
	 * @since 5.9.0 Renamed `$category` to `$data_object` to match parent class for PHP 8 named parameter support.
	 *
	 * @param string  $output      Used to append additional content (passed by reference).
	 * @param WP_Term $data_object The current term object.
	 * @param int     $depth       Depth of the term in reference to parents. Default 0.
	 * @param array   $args        An array of arguments. See {@see wp_terms_checklist()}.
	 */
	public function end_el( &$output, $data_object, $depth = 0, $args = array() ) {
		$output .= "</li>\n";
	}
}
class-wp-site-icon.php000060400000014416151327343360010707 0ustar00<?php
/**
 * Administration API: WP_Site_Icon class
 *
 * @package WordPress
 * @subpackage Administration
 * @since 4.3.0
 */

/**
 * Core class used to implement site icon functionality.
 *
 * @since 4.3.0
 */
#[AllowDynamicProperties]
class WP_Site_Icon {

	/**
	 * The minimum size of the site icon.
	 *
	 * @since 4.3.0
	 * @var int
	 */
	public $min_size = 512;

	/**
	 * The size to which to crop the image so that we can display it in the UI nicely.
	 *
	 * @since 4.3.0
	 * @var int
	 */
	public $page_crop = 512;

	/**
	 * List of site icon sizes.
	 *
	 * @since 4.3.0
	 * @var int[]
	 */
	public $site_icon_sizes = array(
		/*
		 * Square, medium sized tiles for IE11+.
		 *
		 * See https://msdn.microsoft.com/library/dn455106(v=vs.85).aspx
		 */
		270,

		/*
		 * App icon for Android/Chrome.
		 *
		 * @link https://developers.google.com/web/updates/2014/11/Support-for-theme-color-in-Chrome-39-for-Android
		 * @link https://developer.chrome.com/multidevice/android/installtohomescreen
		 */
		192,

		/*
		 * App icons up to iPhone 6 Plus.
		 *
		 * See https://developer.apple.com/library/prerelease/ios/documentation/UserExperience/Conceptual/MobileHIG/IconMatrix.html
		 */
		180,

		// Our regular Favicon.
		32,
	);

	/**
	 * Registers actions and filters.
	 *
	 * @since 4.3.0
	 */
	public function __construct() {
		add_action( 'delete_attachment', array( $this, 'delete_attachment_data' ) );
		add_filter( 'get_post_metadata', array( $this, 'get_post_metadata' ), 10, 4 );
	}

	/**
	 * Creates an attachment 'object'.
	 *
	 * @since 4.3.0
	 * @deprecated 6.5.0
	 *
	 * @param string $cropped              Cropped image URL.
	 * @param int    $parent_attachment_id Attachment ID of parent image.
	 * @return array An array with attachment object data.
	 */
	public function create_attachment_object( $cropped, $parent_attachment_id ) {
		_deprecated_function( __METHOD__, '6.5.0', 'wp_copy_parent_attachment_properties()' );

		$parent     = get_post( $parent_attachment_id );
		$parent_url = wp_get_attachment_url( $parent->ID );
		$url        = str_replace( wp_basename( $parent_url ), wp_basename( $cropped ), $parent_url );

		$size       = wp_getimagesize( $cropped );
		$image_type = ( $size ) ? $size['mime'] : 'image/jpeg';

		$attachment = array(
			'ID'             => $parent_attachment_id,
			'post_title'     => wp_basename( $cropped ),
			'post_content'   => $url,
			'post_mime_type' => $image_type,
			'guid'           => $url,
			'context'        => 'site-icon',
		);

		return $attachment;
	}

	/**
	 * Inserts an attachment.
	 *
	 * @since 4.3.0
	 *
	 * @param array  $attachment An array with attachment object data.
	 * @param string $file       File path of the attached image.
	 * @return int               Attachment ID.
	 */
	public function insert_attachment( $attachment, $file ) {
		$attachment_id = wp_insert_attachment( $attachment, $file );
		$metadata      = wp_generate_attachment_metadata( $attachment_id, $file );

		/**
		 * Filters the site icon attachment metadata.
		 *
		 * @since 4.3.0
		 *
		 * @see wp_generate_attachment_metadata()
		 *
		 * @param array $metadata Attachment metadata.
		 */
		$metadata = apply_filters( 'site_icon_attachment_metadata', $metadata );
		wp_update_attachment_metadata( $attachment_id, $metadata );

		return $attachment_id;
	}

	/**
	 * Adds additional sizes to be made when creating the site icon images.
	 *
	 * @since 4.3.0
	 *
	 * @param array[] $sizes Array of arrays containing information for additional sizes.
	 * @return array[] Array of arrays containing additional image sizes.
	 */
	public function additional_sizes( $sizes = array() ) {
		$only_crop_sizes = array();

		/**
		 * Filters the different dimensions that a site icon is saved in.
		 *
		 * @since 4.3.0
		 *
		 * @param int[] $site_icon_sizes Array of sizes available for the Site Icon.
		 */
		$this->site_icon_sizes = apply_filters( 'site_icon_image_sizes', $this->site_icon_sizes );

		// Use a natural sort of numbers.
		natsort( $this->site_icon_sizes );
		$this->site_icon_sizes = array_reverse( $this->site_icon_sizes );

		// Ensure that we only resize the image into sizes that allow cropping.
		foreach ( $sizes as $name => $size_array ) {
			if ( isset( $size_array['crop'] ) ) {
				$only_crop_sizes[ $name ] = $size_array;
			}
		}

		foreach ( $this->site_icon_sizes as $size ) {
			if ( $size < $this->min_size ) {
				$only_crop_sizes[ 'site_icon-' . $size ] = array(
					'width ' => $size,
					'height' => $size,
					'crop'   => true,
				);
			}
		}

		return $only_crop_sizes;
	}

	/**
	 * Adds Site Icon sizes to the array of image sizes on demand.
	 *
	 * @since 4.3.0
	 *
	 * @param string[] $sizes Array of image size names.
	 * @return string[] Array of image size names.
	 */
	public function intermediate_image_sizes( $sizes = array() ) {
		/** This filter is documented in wp-admin/includes/class-wp-site-icon.php */
		$this->site_icon_sizes = apply_filters( 'site_icon_image_sizes', $this->site_icon_sizes );
		foreach ( $this->site_icon_sizes as $size ) {
			$sizes[] = 'site_icon-' . $size;
		}

		return $sizes;
	}

	/**
	 * Deletes the Site Icon when the image file is deleted.
	 *
	 * @since 4.3.0
	 *
	 * @param int $post_id Attachment ID.
	 */
	public function delete_attachment_data( $post_id ) {
		$site_icon_id = (int) get_option( 'site_icon' );

		if ( $site_icon_id && $post_id === $site_icon_id ) {
			delete_option( 'site_icon' );
		}
	}

	/**
	 * Adds custom image sizes when meta data for an image is requested, that happens to be used as Site Icon.
	 *
	 * @since 4.3.0
	 *
	 * @param null|array|string $value    The value get_metadata() should return a single metadata value, or an
	 *                                    array of values.
	 * @param int               $post_id  Post ID.
	 * @param string            $meta_key Meta key.
	 * @param bool              $single   Whether to return only the first value of the specified `$meta_key`.
	 * @return array|null|string The attachment metadata value, array of values, or null.
	 */
	public function get_post_metadata( $value, $post_id, $meta_key, $single ) {
		if ( $single && '_wp_attachment_backup_sizes' === $meta_key ) {
			$site_icon_id = (int) get_option( 'site_icon' );

			if ( $post_id === $site_icon_id ) {
				add_filter( 'intermediate_image_sizes', array( $this, 'intermediate_image_sizes' ) );
			}
		}

		return $value;
	}
}
nav-menu.php000064400000137621151327343360007024 0ustar00<?php
/**
 * Core Navigation Menu API
 *
 * @package WordPress
 * @subpackage Nav_Menus
 * @since 3.0.0
 */

/** Walker_Nav_Menu_Edit class */
require_once ABSPATH . 'wp-admin/includes/class-walker-nav-menu-edit.php';

/** Walker_Nav_Menu_Checklist class */
require_once ABSPATH . 'wp-admin/includes/class-walker-nav-menu-checklist.php';

/**
 * Prints the appropriate response to a menu quick search.
 *
 * @since 3.0.0
 *
 * @param array $request The unsanitized request values.
 */
function _wp_ajax_menu_quick_search( $request = array() ) {
	$args            = array();
	$type            = isset( $request['type'] ) ? $request['type'] : '';
	$object_type     = isset( $request['object_type'] ) ? $request['object_type'] : '';
	$query           = isset( $request['q'] ) ? $request['q'] : '';
	$response_format = isset( $request['response-format'] ) ? $request['response-format'] : '';

	if ( ! $response_format || ! in_array( $response_format, array( 'json', 'markup' ), true ) ) {
		$response_format = 'json';
	}

	if ( 'markup' === $response_format ) {
		$args['walker'] = new Walker_Nav_Menu_Checklist();
	}

	if ( 'get-post-item' === $type ) {
		if ( post_type_exists( $object_type ) ) {
			if ( isset( $request['ID'] ) ) {
				$object_id = (int) $request['ID'];

				if ( 'markup' === $response_format ) {
					echo walk_nav_menu_tree(
						array_map( 'wp_setup_nav_menu_item', array( get_post( $object_id ) ) ),
						0,
						(object) $args
					);
				} elseif ( 'json' === $response_format ) {
					echo wp_json_encode(
						array(
							'ID'         => $object_id,
							'post_title' => get_the_title( $object_id ),
							'post_type'  => get_post_type( $object_id ),
						)
					);
					echo "\n";
				}
			}
		} elseif ( taxonomy_exists( $object_type ) ) {
			if ( isset( $request['ID'] ) ) {
				$object_id = (int) $request['ID'];

				if ( 'markup' === $response_format ) {
					echo walk_nav_menu_tree(
						array_map( 'wp_setup_nav_menu_item', array( get_term( $object_id, $object_type ) ) ),
						0,
						(object) $args
					);
				} elseif ( 'json' === $response_format ) {
					$post_obj = get_term( $object_id, $object_type );
					echo wp_json_encode(
						array(
							'ID'         => $object_id,
							'post_title' => $post_obj->name,
							'post_type'  => $object_type,
						)
					);
					echo "\n";
				}
			}
		}
	} elseif ( preg_match( '/quick-search-(posttype|taxonomy)-([a-zA-Z_-]*\b)/', $type, $matches ) ) {
		if ( 'posttype' === $matches[1] && get_post_type_object( $matches[2] ) ) {
			$post_type_obj = _wp_nav_menu_meta_box_object( get_post_type_object( $matches[2] ) );
			$args          = array_merge(
				$args,
				array(
					'no_found_rows'          => true,
					'update_post_meta_cache' => false,
					'update_post_term_cache' => false,
					'posts_per_page'         => 10,
					'post_type'              => $matches[2],
					's'                      => $query,
				)
			);

			if ( isset( $post_type_obj->_default_query ) ) {
				$args = array_merge( $args, (array) $post_type_obj->_default_query );
			}

			$search_results_query = new WP_Query( $args );
			if ( ! $search_results_query->have_posts() ) {
				return;
			}

			while ( $search_results_query->have_posts() ) {
				$post = $search_results_query->next_post();

				if ( 'markup' === $response_format ) {
					$var_by_ref = $post->ID;
					echo walk_nav_menu_tree(
						array_map( 'wp_setup_nav_menu_item', array( get_post( $var_by_ref ) ) ),
						0,
						(object) $args
					);
				} elseif ( 'json' === $response_format ) {
					echo wp_json_encode(
						array(
							'ID'         => $post->ID,
							'post_title' => get_the_title( $post->ID ),
							'post_type'  => $matches[2],
						)
					);
					echo "\n";
				}
			}
		} elseif ( 'taxonomy' === $matches[1] ) {
			$terms = get_terms(
				array(
					'taxonomy'   => $matches[2],
					'name__like' => $query,
					'number'     => 10,
					'hide_empty' => false,
				)
			);

			if ( empty( $terms ) || is_wp_error( $terms ) ) {
				return;
			}

			foreach ( (array) $terms as $term ) {
				if ( 'markup' === $response_format ) {
					echo walk_nav_menu_tree(
						array_map( 'wp_setup_nav_menu_item', array( $term ) ),
						0,
						(object) $args
					);
				} elseif ( 'json' === $response_format ) {
					echo wp_json_encode(
						array(
							'ID'         => $term->term_id,
							'post_title' => $term->name,
							'post_type'  => $matches[2],
						)
					);
					echo "\n";
				}
			}
		}
	}
}

/**
 * Register nav menu meta boxes and advanced menu items.
 *
 * @since 3.0.0
 */
function wp_nav_menu_setup() {
	// Register meta boxes.
	wp_nav_menu_post_type_meta_boxes();
	add_meta_box(
		'add-custom-links',
		__( 'Custom Links' ),
		'wp_nav_menu_item_link_meta_box',
		'nav-menus',
		'side',
		'default'
	);
	wp_nav_menu_taxonomy_meta_boxes();

	// Register advanced menu items (columns).
	add_filter( 'manage_nav-menus_columns', 'wp_nav_menu_manage_columns' );

	// If first time editing, disable advanced items by default.
	if ( false === get_user_option( 'managenav-menuscolumnshidden' ) ) {
		$user = wp_get_current_user();
		update_user_meta(
			$user->ID,
			'managenav-menuscolumnshidden',
			array(
				0 => 'link-target',
				1 => 'css-classes',
				2 => 'xfn',
				3 => 'description',
				4 => 'title-attribute',
			)
		);
	}
}

/**
 * Limit the amount of meta boxes to pages, posts, links, and categories for first time users.
 *
 * @since 3.0.0
 *
 * @global array $wp_meta_boxes Global meta box state.
 */
function wp_initial_nav_menu_meta_boxes() {
	global $wp_meta_boxes;

	if ( get_user_option( 'metaboxhidden_nav-menus' ) !== false || ! is_array( $wp_meta_boxes ) ) {
		return;
	}

	$initial_meta_boxes = array( 'add-post-type-page', 'add-post-type-post', 'add-custom-links', 'add-category' );
	$hidden_meta_boxes  = array();

	foreach ( array_keys( $wp_meta_boxes['nav-menus'] ) as $context ) {
		foreach ( array_keys( $wp_meta_boxes['nav-menus'][ $context ] ) as $priority ) {
			foreach ( $wp_meta_boxes['nav-menus'][ $context ][ $priority ] as $box ) {
				if ( in_array( $box['id'], $initial_meta_boxes, true ) ) {
					unset( $box['id'] );
				} else {
					$hidden_meta_boxes[] = $box['id'];
				}
			}
		}
	}

	$user = wp_get_current_user();
	update_user_meta( $user->ID, 'metaboxhidden_nav-menus', $hidden_meta_boxes );
}

/**
 * Creates meta boxes for any post type menu item..
 *
 * @since 3.0.0
 */
function wp_nav_menu_post_type_meta_boxes() {
	$post_types = get_post_types( array( 'show_in_nav_menus' => true ), 'object' );

	if ( ! $post_types ) {
		return;
	}

	foreach ( $post_types as $post_type ) {
		/**
		 * Filters whether a menu items meta box will be added for the current
		 * object type.
		 *
		 * If a falsey value is returned instead of an object, the menu items
		 * meta box for the current meta box object will not be added.
		 *
		 * @since 3.0.0
		 *
		 * @param WP_Post_Type|false $post_type The current object to add a menu items
		 *                                      meta box for.
		 */
		$post_type = apply_filters( 'nav_menu_meta_box_object', $post_type );

		if ( $post_type ) {
			$id = $post_type->name;
			// Give pages a higher priority.
			$priority = ( 'page' === $post_type->name ? 'core' : 'default' );
			add_meta_box(
				"add-post-type-{$id}",
				$post_type->labels->name,
				'wp_nav_menu_item_post_type_meta_box',
				'nav-menus',
				'side',
				$priority,
				$post_type
			);
		}
	}
}

/**
 * Creates meta boxes for any taxonomy menu item.
 *
 * @since 3.0.0
 */
function wp_nav_menu_taxonomy_meta_boxes() {
	$taxonomies = get_taxonomies( array( 'show_in_nav_menus' => true ), 'object' );

	if ( ! $taxonomies ) {
		return;
	}

	foreach ( $taxonomies as $tax ) {
		/** This filter is documented in wp-admin/includes/nav-menu.php */
		$tax = apply_filters( 'nav_menu_meta_box_object', $tax );

		if ( $tax ) {
			$id = $tax->name;
			add_meta_box(
				"add-{$id}",
				$tax->labels->name,
				'wp_nav_menu_item_taxonomy_meta_box',
				'nav-menus',
				'side',
				'default',
				$tax
			);
		}
	}
}

/**
 * Check whether to disable the Menu Locations meta box submit button and inputs.
 *
 * @since 3.6.0
 * @since 5.3.1 The `$display` parameter was added.
 *
 * @global bool $one_theme_location_no_menus to determine if no menus exist
 *
 * @param int|string $nav_menu_selected_id ID, name, or slug of the currently selected menu.
 * @param bool       $display              Whether to display or just return the string.
 * @return string|false Disabled attribute if at least one menu exists, false if not.
 */
function wp_nav_menu_disabled_check( $nav_menu_selected_id, $display = true ) {
	global $one_theme_location_no_menus;

	if ( $one_theme_location_no_menus ) {
		return false;
	}

	return disabled( $nav_menu_selected_id, 0, $display );
}

/**
 * Displays a meta box for the custom links menu item.
 *
 * @since 3.0.0
 *
 * @global int        $_nav_menu_placeholder
 * @global int|string $nav_menu_selected_id
 */
function wp_nav_menu_item_link_meta_box() {
	global $_nav_menu_placeholder, $nav_menu_selected_id;

	$_nav_menu_placeholder = 0 > $_nav_menu_placeholder ? $_nav_menu_placeholder - 1 : -1;

	?>
	<div class="customlinkdiv" id="customlinkdiv">
		<input type="hidden" value="custom" name="menu-item[<?php echo $_nav_menu_placeholder; ?>][menu-item-type]" />
		<p id="menu-item-url-wrap" class="wp-clearfix">
			<label class="howto" for="custom-menu-item-url"><?php _e( 'URL' ); ?></label>
			<input id="custom-menu-item-url" name="menu-item[<?php echo $_nav_menu_placeholder; ?>][menu-item-url]"
				type="text"<?php wp_nav_menu_disabled_check( $nav_menu_selected_id ); ?>
				class="code menu-item-textbox form-required" placeholder="https://"
			/>
			<span id="custom-url-error" class="error-message" style="display: none;"><?php _e( 'Please provide a valid link.' ); ?></span>
		</p>

		<p id="menu-item-name-wrap" class="wp-clearfix">
			<label class="howto" for="custom-menu-item-name"><?php _e( 'Link Text' ); ?></label>
			<input id="custom-menu-item-name" name="menu-item[<?php echo $_nav_menu_placeholder; ?>][menu-item-title]"
				type="text"<?php wp_nav_menu_disabled_check( $nav_menu_selected_id ); ?>
				class="regular-text menu-item-textbox"
			/>
		</p>

		<p class="button-controls wp-clearfix">
			<span class="add-to-menu">
				<input id="submit-customlinkdiv" name="add-custom-menu-item"
					type="submit"<?php wp_nav_menu_disabled_check( $nav_menu_selected_id ); ?>
					class="button submit-add-to-menu right" value="<?php esc_attr_e( 'Add to Menu' ); ?>"
				/>
				<span class="spinner"></span>
			</span>
		</p>

	</div><!-- /.customlinkdiv -->
	<?php
}

/**
 * Displays a meta box for a post type menu item.
 *
 * @since 3.0.0
 *
 * @global int        $_nav_menu_placeholder
 * @global int|string $nav_menu_selected_id
 *
 * @param string $data_object Not used.
 * @param array  $box {
 *     Post type menu item meta box arguments.
 *
 *     @type string       $id       Meta box 'id' attribute.
 *     @type string       $title    Meta box title.
 *     @type callable     $callback Meta box display callback.
 *     @type WP_Post_Type $args     Extra meta box arguments (the post type object for this meta box).
 * }
 */
function wp_nav_menu_item_post_type_meta_box( $data_object, $box ) {
	global $_nav_menu_placeholder, $nav_menu_selected_id;

	$post_type_name = $box['args']->name;
	$post_type      = get_post_type_object( $post_type_name );
	$tab_name       = $post_type_name . '-tab';

	// Paginate browsing for large numbers of post objects.
	$per_page = 50;
	$pagenum  = isset( $_REQUEST[ $tab_name ] ) && isset( $_REQUEST['paged'] ) ? absint( $_REQUEST['paged'] ) : 1;
	$offset   = 0 < $pagenum ? $per_page * ( $pagenum - 1 ) : 0;

	$args = array(
		'offset'                 => $offset,
		'order'                  => 'ASC',
		'orderby'                => 'title',
		'posts_per_page'         => $per_page,
		'post_type'              => $post_type_name,
		'suppress_filters'       => true,
		'update_post_term_cache' => false,
		'update_post_meta_cache' => false,
	);

	if ( isset( $box['args']->_default_query ) ) {
		$args = array_merge( $args, (array) $box['args']->_default_query );
	}

	/*
	 * If we're dealing with pages, let's prioritize the Front Page,
	 * Posts Page and Privacy Policy Page at the top of the list.
	 */
	$important_pages = array();
	if ( 'page' === $post_type_name ) {
		$suppress_page_ids = array();

		// Insert Front Page or custom Home link.
		$front_page = 'page' === get_option( 'show_on_front' ) ? (int) get_option( 'page_on_front' ) : 0;

		$front_page_obj = null;

		if ( ! empty( $front_page ) ) {
			$front_page_obj = get_post( $front_page );
		}

		if ( $front_page_obj ) {
			$front_page_obj->front_or_home = true;

			$important_pages[]   = $front_page_obj;
			$suppress_page_ids[] = $front_page_obj->ID;
		} else {
			$_nav_menu_placeholder = ( 0 > $_nav_menu_placeholder ) ? (int) $_nav_menu_placeholder - 1 : -1;
			$front_page_obj        = (object) array(
				'front_or_home' => true,
				'ID'            => 0,
				'object_id'     => $_nav_menu_placeholder,
				'post_content'  => '',
				'post_excerpt'  => '',
				'post_parent'   => '',
				'post_title'    => _x( 'Home', 'nav menu home label' ),
				'post_type'     => 'nav_menu_item',
				'type'          => 'custom',
				'url'           => home_url( '/' ),
			);

			$important_pages[] = $front_page_obj;
		}

		// Insert Posts Page.
		$posts_page = 'page' === get_option( 'show_on_front' ) ? (int) get_option( 'page_for_posts' ) : 0;

		if ( ! empty( $posts_page ) ) {
			$posts_page_obj = get_post( $posts_page );

			if ( $posts_page_obj ) {
				$front_page_obj->posts_page = true;

				$important_pages[]   = $posts_page_obj;
				$suppress_page_ids[] = $posts_page_obj->ID;
			}
		}

		// Insert Privacy Policy Page.
		$privacy_policy_page_id = (int) get_option( 'wp_page_for_privacy_policy' );

		if ( ! empty( $privacy_policy_page_id ) ) {
			$privacy_policy_page = get_post( $privacy_policy_page_id );

			if ( $privacy_policy_page instanceof WP_Post && 'publish' === $privacy_policy_page->post_status ) {
				$privacy_policy_page->privacy_policy_page = true;

				$important_pages[]   = $privacy_policy_page;
				$suppress_page_ids[] = $privacy_policy_page->ID;
			}
		}

		// Add suppression array to arguments for WP_Query.
		if ( ! empty( $suppress_page_ids ) ) {
			$args['post__not_in'] = $suppress_page_ids;
		}
	}

	// @todo Transient caching of these results with proper invalidation on updating of a post of this type.
	$get_posts = new WP_Query();
	$posts     = $get_posts->query( $args );

	// Only suppress and insert when more than just suppression pages available.
	if ( ! $get_posts->post_count ) {
		if ( ! empty( $suppress_page_ids ) ) {
			unset( $args['post__not_in'] );
			$get_posts = new WP_Query();
			$posts     = $get_posts->query( $args );
		} else {
			echo '<p>' . __( 'No items.' ) . '</p>';
			return;
		}
	} elseif ( ! empty( $important_pages ) ) {
		$posts = array_merge( $important_pages, $posts );
	}

	$num_pages = $get_posts->max_num_pages;

	$page_links = paginate_links(
		array(
			'base'               => add_query_arg(
				array(
					$tab_name     => 'all',
					'paged'       => '%#%',
					'item-type'   => 'post_type',
					'item-object' => $post_type_name,
				)
			),
			'format'             => '',
			'prev_text'          => '<span aria-label="' . esc_attr__( 'Previous page' ) . '">' . __( '&laquo;' ) . '</span>',
			'next_text'          => '<span aria-label="' . esc_attr__( 'Next page' ) . '">' . __( '&raquo;' ) . '</span>',
			/* translators: Hidden accessibility text. */
			'before_page_number' => '<span class="screen-reader-text">' . __( 'Page' ) . '</span> ',
			'total'              => $num_pages,
			'current'            => $pagenum,
		)
	);

	$db_fields = false;
	if ( is_post_type_hierarchical( $post_type_name ) ) {
		$db_fields = array(
			'parent' => 'post_parent',
			'id'     => 'ID',
		);
	}

	$walker = new Walker_Nav_Menu_Checklist( $db_fields );

	$current_tab = 'most-recent';

	if ( isset( $_REQUEST[ $tab_name ] ) && in_array( $_REQUEST[ $tab_name ], array( 'all', 'search' ), true ) ) {
		$current_tab = $_REQUEST[ $tab_name ];
	}

	if ( ! empty( $_REQUEST[ "quick-search-posttype-{$post_type_name}" ] ) ) {
		$current_tab = 'search';
	}

	$removed_args = array(
		'action',
		'customlink-tab',
		'edit-menu-item',
		'menu-item',
		'page-tab',
		'_wpnonce',
	);

	$most_recent_url = '';
	$view_all_url    = '';
	$search_url      = '';

	if ( $nav_menu_selected_id ) {
		$most_recent_url = add_query_arg( $tab_name, 'most-recent', remove_query_arg( $removed_args ) );
		$view_all_url    = add_query_arg( $tab_name, 'all', remove_query_arg( $removed_args ) );
		$search_url      = add_query_arg( $tab_name, 'search', remove_query_arg( $removed_args ) );
	}
	?>
	<div id="<?php echo esc_attr( "posttype-{$post_type_name}" ); ?>" class="posttypediv">
		<ul id="<?php echo esc_attr( "posttype-{$post_type_name}-tabs" ); ?>" class="posttype-tabs add-menu-item-tabs">
			<li <?php echo ( 'most-recent' === $current_tab ? ' class="tabs"' : '' ); ?>>
				<a class="nav-tab-link"
					data-type="<?php echo esc_attr( "tabs-panel-posttype-{$post_type_name}-most-recent" ); ?>"
					href="<?php echo esc_url( $most_recent_url . "#tabs-panel-posttype-{$post_type_name}-most-recent" ); ?>"
				>
					<?php _e( 'Most Recent' ); ?>
				</a>
			</li>
			<li <?php echo ( 'all' === $current_tab ? ' class="tabs"' : '' ); ?>>
				<a class="nav-tab-link"
					data-type="<?php echo esc_attr( "{$post_type_name}-all" ); ?>"
					href="<?php echo esc_url( $view_all_url . "#{$post_type_name}-all" ); ?>"
				>
					<?php _e( 'View All' ); ?>
				</a>
			</li>
			<li <?php echo ( 'search' === $current_tab ? ' class="tabs"' : '' ); ?>>
				<a class="nav-tab-link"
					data-type="<?php echo esc_attr( "tabs-panel-posttype-{$post_type_name}-search" ); ?>"
					href="<?php echo esc_url( $search_url . "#tabs-panel-posttype-{$post_type_name}-search" ); ?>"
				>
					<?php _e( 'Search' ); ?>
				</a>
			</li>
		</ul><!-- .posttype-tabs -->

		<div id="<?php echo esc_attr( "tabs-panel-posttype-{$post_type_name}-most-recent" ); ?>"
			class="tabs-panel <?php echo ( 'most-recent' === $current_tab ? 'tabs-panel-active' : 'tabs-panel-inactive' ); ?>"
			role="region" aria-label="<?php esc_attr_e( 'Most Recent' ); ?>" tabindex="0"
		>
			<ul id="<?php echo esc_attr( "{$post_type_name}checklist-most-recent" ); ?>"
				class="categorychecklist form-no-clear"
			>
				<?php
				$recent_args = array_merge(
					$args,
					array(
						'orderby'        => 'post_date',
						'order'          => 'DESC',
						'posts_per_page' => 15,
					)
				);
				$most_recent = $get_posts->query( $recent_args );

				$args['walker'] = $walker;

				/**
				 * Filters the posts displayed in the 'Most Recent' tab of the current
				 * post type's menu items meta box.
				 *
				 * The dynamic portion of the hook name, `$post_type_name`, refers to the post type name.
				 *
				 * Possible hook names include:
				 *
				 *  - `nav_menu_items_post_recent`
				 *  - `nav_menu_items_page_recent`
				 *
				 * @since 4.3.0
				 * @since 4.9.0 Added the `$recent_args` parameter.
				 *
				 * @param WP_Post[] $most_recent An array of post objects being listed.
				 * @param array     $args        An array of `WP_Query` arguments for the meta box.
				 * @param array     $box         Arguments passed to `wp_nav_menu_item_post_type_meta_box()`.
				 * @param array     $recent_args An array of `WP_Query` arguments for 'Most Recent' tab.
				 */
				$most_recent = apply_filters(
					"nav_menu_items_{$post_type_name}_recent",
					$most_recent,
					$args,
					$box,
					$recent_args
				);

				echo walk_nav_menu_tree(
					array_map( 'wp_setup_nav_menu_item', $most_recent ),
					0,
					(object) $args
				);
				?>
			</ul>
		</div><!-- /.tabs-panel -->

		<div id="<?php echo esc_attr( "tabs-panel-posttype-{$post_type_name}-search" ); ?>"
			class="tabs-panel <?php echo ( 'search' === $current_tab ? 'tabs-panel-active' : 'tabs-panel-inactive' ); ?>"
			role="region" aria-label="<?php echo esc_attr( $post_type->labels->search_items ); ?>" tabindex="0"
		>
			<?php
			if ( isset( $_REQUEST[ "quick-search-posttype-{$post_type_name}" ] ) ) {
				$searched       = esc_attr( $_REQUEST[ "quick-search-posttype-{$post_type_name}" ] );
				$search_results = get_posts(
					array(
						's'         => $searched,
						'post_type' => $post_type_name,
						'fields'    => 'all',
						'order'     => 'DESC',
					)
				);
			} else {
				$searched       = '';
				$search_results = array();
			}
			?>
			<p class="quick-search-wrap">
				<label for="<?php echo esc_attr( "quick-search-posttype-{$post_type_name}" ); ?>" class="screen-reader-text">
					<?php
					/* translators: Hidden accessibility text. */
					_e( 'Search' );
					?>
				</label>
				<input type="search"<?php wp_nav_menu_disabled_check( $nav_menu_selected_id ); ?>
					class="quick-search" value="<?php echo $searched; ?>"
					name="<?php echo esc_attr( "quick-search-posttype-{$post_type_name}" ); ?>"
					id="<?php echo esc_attr( "quick-search-posttype-{$post_type_name}" ); ?>"
				/>
				<span class="spinner"></span>
				<?php
				submit_button(
					__( 'Search' ),
					'small quick-search-submit hide-if-js',
					'submit',
					false,
					array( 'id' => "submit-quick-search-posttype-{$post_type_name}" )
				);
				?>
			</p>

			<ul id="<?php echo esc_attr( "{$post_type_name}-search-checklist" ); ?>"
				data-wp-lists="<?php echo esc_attr( "list:{$post_type_name}" ); ?>"
				class="categorychecklist form-no-clear"
			>
			<?php if ( ! empty( $search_results ) && ! is_wp_error( $search_results ) ) : ?>
				<?php
				$args['walker'] = $walker;
				echo walk_nav_menu_tree(
					array_map( 'wp_setup_nav_menu_item', $search_results ),
					0,
					(object) $args
				);
				?>
			<?php elseif ( is_wp_error( $search_results ) ) : ?>
				<li><?php echo $search_results->get_error_message(); ?></li>
			<?php elseif ( ! empty( $searched ) ) : ?>
				<li><?php _e( 'No results found.' ); ?></li>
			<?php endif; ?>
			</ul>
		</div><!-- /.tabs-panel -->

		<div id="<?php echo esc_attr( "{$post_type_name}-all" ); ?>"
			class="tabs-panel tabs-panel-view-all <?php echo ( 'all' === $current_tab ? 'tabs-panel-active' : 'tabs-panel-inactive' ); ?>"
			role="region" aria-label="<?php echo esc_attr( $post_type->labels->all_items ); ?>" tabindex="0"
		>
			<?php if ( ! empty( $page_links ) ) : ?>
				<div class="add-menu-item-pagelinks">
					<?php echo $page_links; ?>
				</div>
			<?php endif; ?>

			<ul id="<?php echo esc_attr( "{$post_type_name}checklist" ); ?>"
				data-wp-lists="<?php echo esc_attr( "list:{$post_type_name}" ); ?>"
				class="categorychecklist form-no-clear"
			>
				<?php
				$args['walker'] = $walker;

				if ( $post_type->has_archive ) {
					$_nav_menu_placeholder = ( 0 > $_nav_menu_placeholder ) ? (int) $_nav_menu_placeholder - 1 : -1;
					array_unshift(
						$posts,
						(object) array(
							'ID'           => 0,
							'object_id'    => $_nav_menu_placeholder,
							'object'       => $post_type_name,
							'post_content' => '',
							'post_excerpt' => '',
							'post_title'   => $post_type->labels->archives,
							'post_type'    => 'nav_menu_item',
							'type'         => 'post_type_archive',
							'url'          => get_post_type_archive_link( $post_type_name ),
						)
					);
				}

				/**
				 * Filters the posts displayed in the 'View All' tab of the current
				 * post type's menu items meta box.
				 *
				 * The dynamic portion of the hook name, `$post_type_name`, refers
				 * to the slug of the current post type.
				 *
				 * Possible hook names include:
				 *
				 *  - `nav_menu_items_post`
				 *  - `nav_menu_items_page`
				 *
				 * @since 3.2.0
				 * @since 4.6.0 Converted the `$post_type` parameter to accept a WP_Post_Type object.
				 *
				 * @see WP_Query::query()
				 *
				 * @param object[]     $posts     The posts for the current post type. Mostly `WP_Post` objects, but
				 *                                can also contain "fake" post objects to represent other menu items.
				 * @param array        $args      An array of `WP_Query` arguments.
				 * @param WP_Post_Type $post_type The current post type object for this menu item meta box.
				 */
				$posts = apply_filters(
					"nav_menu_items_{$post_type_name}",
					$posts,
					$args,
					$post_type
				);

				$checkbox_items = walk_nav_menu_tree(
					array_map( 'wp_setup_nav_menu_item', $posts ),
					0,
					(object) $args
				);

				echo $checkbox_items;
				?>
			</ul>

			<?php if ( ! empty( $page_links ) ) : ?>
				<div class="add-menu-item-pagelinks">
					<?php echo $page_links; ?>
				</div>
			<?php endif; ?>
		</div><!-- /.tabs-panel -->

		<p class="button-controls wp-clearfix" data-items-type="<?php echo esc_attr( "posttype-{$post_type_name}" ); ?>">
			<span class="list-controls hide-if-no-js">
				<input type="checkbox"<?php wp_nav_menu_disabled_check( $nav_menu_selected_id ); ?>
					id="<?php echo esc_attr( $tab_name ); ?>" class="select-all"
				/>
				<label for="<?php echo esc_attr( $tab_name ); ?>"><?php _e( 'Select All' ); ?></label>
			</span>

			<span class="add-to-menu">
				<input type="submit"<?php wp_nav_menu_disabled_check( $nav_menu_selected_id ); ?>
					class="button submit-add-to-menu right" value="<?php esc_attr_e( 'Add to Menu' ); ?>"
					name="add-post-type-menu-item" id="<?php echo esc_attr( "submit-posttype-{$post_type_name}" ); ?>"
				/>
				<span class="spinner"></span>
			</span>
		</p>

	</div><!-- /.posttypediv -->
	<?php
}

/**
 * Displays a meta box for a taxonomy menu item.
 *
 * @since 3.0.0
 *
 * @global int|string $nav_menu_selected_id
 *
 * @param string $data_object Not used.
 * @param array  $box {
 *     Taxonomy menu item meta box arguments.
 *
 *     @type string   $id       Meta box 'id' attribute.
 *     @type string   $title    Meta box title.
 *     @type callable $callback Meta box display callback.
 *     @type object   $args     Extra meta box arguments (the taxonomy object for this meta box).
 * }
 */
function wp_nav_menu_item_taxonomy_meta_box( $data_object, $box ) {
	global $nav_menu_selected_id;

	$taxonomy_name = $box['args']->name;
	$taxonomy      = get_taxonomy( $taxonomy_name );
	$tab_name      = $taxonomy_name . '-tab';

	// Paginate browsing for large numbers of objects.
	$per_page = 50;
	$pagenum  = isset( $_REQUEST[ $tab_name ] ) && isset( $_REQUEST['paged'] ) ? absint( $_REQUEST['paged'] ) : 1;
	$offset   = 0 < $pagenum ? $per_page * ( $pagenum - 1 ) : 0;

	$args = array(
		'taxonomy'     => $taxonomy_name,
		'child_of'     => 0,
		'exclude'      => '',
		'hide_empty'   => false,
		'hierarchical' => 1,
		'include'      => '',
		'number'       => $per_page,
		'offset'       => $offset,
		'order'        => 'ASC',
		'orderby'      => 'name',
		'pad_counts'   => false,
	);

	$terms = get_terms( $args );

	if ( ! $terms || is_wp_error( $terms ) ) {
		echo '<p>' . __( 'No items.' ) . '</p>';
		return;
	}

	$num_pages = (int) ceil(
		(int) wp_count_terms(
			array_merge(
				$args,
				array(
					'number' => '',
					'offset' => '',
				)
			)
		) / $per_page
	);

	$page_links = paginate_links(
		array(
			'base'               => add_query_arg(
				array(
					$tab_name     => 'all',
					'paged'       => '%#%',
					'item-type'   => 'taxonomy',
					'item-object' => $taxonomy_name,
				)
			),
			'format'             => '',
			'prev_text'          => '<span aria-label="' . esc_attr__( 'Previous page' ) . '">' . __( '&laquo;' ) . '</span>',
			'next_text'          => '<span aria-label="' . esc_attr__( 'Next page' ) . '">' . __( '&raquo;' ) . '</span>',
			/* translators: Hidden accessibility text. */
			'before_page_number' => '<span class="screen-reader-text">' . __( 'Page' ) . '</span> ',
			'total'              => $num_pages,
			'current'            => $pagenum,
		)
	);

	$db_fields = false;
	if ( is_taxonomy_hierarchical( $taxonomy_name ) ) {
		$db_fields = array(
			'parent' => 'parent',
			'id'     => 'term_id',
		);
	}

	$walker = new Walker_Nav_Menu_Checklist( $db_fields );

	$current_tab = 'most-used';

	if ( isset( $_REQUEST[ $tab_name ] ) && in_array( $_REQUEST[ $tab_name ], array( 'all', 'most-used', 'search' ), true ) ) {
		$current_tab = $_REQUEST[ $tab_name ];
	}

	if ( ! empty( $_REQUEST[ "quick-search-taxonomy-{$taxonomy_name}" ] ) ) {
		$current_tab = 'search';
	}

	$removed_args = array(
		'action',
		'customlink-tab',
		'edit-menu-item',
		'menu-item',
		'page-tab',
		'_wpnonce',
	);

	$most_used_url = '';
	$view_all_url  = '';
	$search_url    = '';

	if ( $nav_menu_selected_id ) {
		$most_used_url = add_query_arg( $tab_name, 'most-used', remove_query_arg( $removed_args ) );
		$view_all_url  = add_query_arg( $tab_name, 'all', remove_query_arg( $removed_args ) );
		$search_url    = add_query_arg( $tab_name, 'search', remove_query_arg( $removed_args ) );
	}
	?>
	<div id="<?php echo esc_attr( "taxonomy-{$taxonomy_name}" ); ?>" class="taxonomydiv">
		<ul id="<?php echo esc_attr( "taxonomy-{$taxonomy_name}-tabs" ); ?>" class="taxonomy-tabs add-menu-item-tabs">
			<li <?php echo ( 'most-used' === $current_tab ? ' class="tabs"' : '' ); ?>>
				<a class="nav-tab-link"
					data-type="<?php echo esc_attr( "tabs-panel-{$taxonomy_name}-pop" ); ?>"
					href="<?php echo esc_url( $most_used_url . "#tabs-panel-{$taxonomy_name}-pop" ); ?>"
				>
					<?php echo esc_html( $taxonomy->labels->most_used ); ?>
				</a>
			</li>
			<li <?php echo ( 'all' === $current_tab ? ' class="tabs"' : '' ); ?>>
				<a class="nav-tab-link"
					data-type="<?php echo esc_attr( "tabs-panel-{$taxonomy_name}-all" ); ?>"
					href="<?php echo esc_url( $view_all_url . "#tabs-panel-{$taxonomy_name}-all" ); ?>"
				>
					<?php _e( 'View All' ); ?>
				</a>
			</li>
			<li <?php echo ( 'search' === $current_tab ? ' class="tabs"' : '' ); ?>>
				<a class="nav-tab-link"
					data-type="<?php echo esc_attr( "tabs-panel-search-taxonomy-{$taxonomy_name}" ); ?>"
					href="<?php echo esc_url( $search_url . "#tabs-panel-search-taxonomy-{$taxonomy_name}" ); ?>"
				>
					<?php _e( 'Search' ); ?>
				</a>
			</li>
		</ul><!-- .taxonomy-tabs -->

		<div id="<?php echo esc_attr( "tabs-panel-{$taxonomy_name}-pop" ); ?>"
			class="tabs-panel <?php echo ( 'most-used' === $current_tab ? 'tabs-panel-active' : 'tabs-panel-inactive' ); ?>"
			role="region" aria-label="<?php echo esc_attr( $taxonomy->labels->most_used ); ?>" tabindex="0"
		>
			<ul id="<?php echo esc_attr( "{$taxonomy_name}checklist-pop" ); ?>"
				class="categorychecklist form-no-clear"
			>
				<?php
				$popular_terms = get_terms(
					array(
						'taxonomy'     => $taxonomy_name,
						'orderby'      => 'count',
						'order'        => 'DESC',
						'number'       => 10,
						'hierarchical' => false,
					)
				);

				$args['walker'] = $walker;
				echo walk_nav_menu_tree(
					array_map( 'wp_setup_nav_menu_item', $popular_terms ),
					0,
					(object) $args
				);
				?>
			</ul>
		</div><!-- /.tabs-panel -->

		<div id="<?php echo esc_attr( "tabs-panel-{$taxonomy_name}-all" ); ?>"
			class="tabs-panel tabs-panel-view-all <?php echo ( 'all' === $current_tab ? 'tabs-panel-active' : 'tabs-panel-inactive' ); ?>"
			role="region" aria-label="<?php echo esc_attr( $taxonomy->labels->all_items ); ?>" tabindex="0"
		>
			<?php if ( ! empty( $page_links ) ) : ?>
				<div class="add-menu-item-pagelinks">
					<?php echo $page_links; ?>
				</div>
			<?php endif; ?>

			<ul id="<?php echo esc_attr( "{$taxonomy_name}checklist" ); ?>"
				data-wp-lists="<?php echo esc_attr( "list:{$taxonomy_name}" ); ?>"
				class="categorychecklist form-no-clear"
			>
				<?php
				$args['walker'] = $walker;
				echo walk_nav_menu_tree(
					array_map( 'wp_setup_nav_menu_item', $terms ),
					0,
					(object) $args
				);
				?>
			</ul>

			<?php if ( ! empty( $page_links ) ) : ?>
				<div class="add-menu-item-pagelinks">
					<?php echo $page_links; ?>
				</div>
			<?php endif; ?>
		</div><!-- /.tabs-panel -->

		<div id="<?php echo esc_attr( "tabs-panel-search-taxonomy-{$taxonomy_name}" ); ?>"
			class="tabs-panel <?php echo ( 'search' === $current_tab ? 'tabs-panel-active' : 'tabs-panel-inactive' ); ?>"
			role="region" aria-label="<?php echo esc_attr( $taxonomy->labels->search_items ); ?>" tabindex="0">
			<?php
			if ( isset( $_REQUEST[ "quick-search-taxonomy-{$taxonomy_name}" ] ) ) {
				$searched       = esc_attr( $_REQUEST[ "quick-search-taxonomy-{$taxonomy_name}" ] );
				$search_results = get_terms(
					array(
						'taxonomy'     => $taxonomy_name,
						'name__like'   => $searched,
						'fields'       => 'all',
						'orderby'      => 'count',
						'order'        => 'DESC',
						'hierarchical' => false,
					)
				);
			} else {
				$searched       = '';
				$search_results = array();
			}
			?>
			<p class="quick-search-wrap">
				<label for="<?php echo esc_attr( "quick-search-taxonomy-{$taxonomy_name}" ); ?>" class="screen-reader-text">
					<?php
					/* translators: Hidden accessibility text. */
					_e( 'Search' );
					?>
				</label>
				<input type="search"
					class="quick-search" value="<?php echo $searched; ?>"
					name="<?php echo esc_attr( "quick-search-taxonomy-{$taxonomy_name}" ); ?>"
					id="<?php echo esc_attr( "quick-search-taxonomy-{$taxonomy_name}" ); ?>"
				/>
				<span class="spinner"></span>
				<?php
				submit_button(
					__( 'Search' ),
					'small quick-search-submit hide-if-js',
					'submit',
					false,
					array( 'id' => "submit-quick-search-taxonomy-{$taxonomy_name}" )
				);
				?>
			</p>

			<ul id="<?php echo esc_attr( "{$taxonomy_name}-search-checklist" ); ?>"
				data-wp-lists="<?php echo esc_attr( "list:{$taxonomy_name}" ); ?>"
				class="categorychecklist form-no-clear"
			>
			<?php if ( ! empty( $search_results ) && ! is_wp_error( $search_results ) ) : ?>
				<?php
				$args['walker'] = $walker;
				echo walk_nav_menu_tree(
					array_map( 'wp_setup_nav_menu_item', $search_results ),
					0,
					(object) $args
				);
				?>
			<?php elseif ( is_wp_error( $search_results ) ) : ?>
				<li><?php echo $search_results->get_error_message(); ?></li>
			<?php elseif ( ! empty( $searched ) ) : ?>
				<li><?php _e( 'No results found.' ); ?></li>
			<?php endif; ?>
			</ul>
		</div><!-- /.tabs-panel -->

		<p class="button-controls wp-clearfix" data-items-type="<?php echo esc_attr( "taxonomy-{$taxonomy_name}" ); ?>">
			<span class="list-controls hide-if-no-js">
				<input type="checkbox"<?php wp_nav_menu_disabled_check( $nav_menu_selected_id ); ?>
					id="<?php echo esc_attr( $tab_name ); ?>" class="select-all"
				/>
				<label for="<?php echo esc_attr( $tab_name ); ?>"><?php _e( 'Select All' ); ?></label>
			</span>

			<span class="add-to-menu">
				<input type="submit"<?php wp_nav_menu_disabled_check( $nav_menu_selected_id ); ?>
					class="button submit-add-to-menu right" value="<?php esc_attr_e( 'Add to Menu' ); ?>"
					name="add-taxonomy-menu-item" id="<?php echo esc_attr( "submit-taxonomy-{$taxonomy_name}" ); ?>"
				/>
				<span class="spinner"></span>
			</span>
		</p>

	</div><!-- /.taxonomydiv -->
	<?php
}

/**
 * Save posted nav menu item data.
 *
 * @since 3.0.0
 *
 * @param int     $menu_id   The menu ID for which to save this item. Value of 0 makes a draft, orphaned menu item. Default 0.
 * @param array[] $menu_data The unsanitized POSTed menu item data.
 * @return int[] The database IDs of the items saved
 */
function wp_save_nav_menu_items( $menu_id = 0, $menu_data = array() ) {
	$menu_id     = (int) $menu_id;
	$items_saved = array();

	if ( 0 === $menu_id || is_nav_menu( $menu_id ) ) {

		// Loop through all the menu items' POST values.
		foreach ( (array) $menu_data as $_possible_db_id => $_item_object_data ) {
			if (
				// Checkbox is not checked.
				empty( $_item_object_data['menu-item-object-id'] ) &&
				(
					// And item type either isn't set.
					! isset( $_item_object_data['menu-item-type'] ) ||
					// Or URL is the default.
					in_array( $_item_object_data['menu-item-url'], array( 'https://', 'http://', '' ), true ) ||
					// Or it's not a custom menu item (but not the custom home page).
					! ( 'custom' === $_item_object_data['menu-item-type'] && ! isset( $_item_object_data['menu-item-db-id'] ) ) ||
					// Or it *is* a custom menu item that already exists.
					! empty( $_item_object_data['menu-item-db-id'] )
				)
			) {
				// Then this potential menu item is not getting added to this menu.
				continue;
			}

			// If this possible menu item doesn't actually have a menu database ID yet.
			if (
				empty( $_item_object_data['menu-item-db-id'] ) ||
				( 0 > $_possible_db_id ) ||
				$_possible_db_id !== (int) $_item_object_data['menu-item-db-id']
			) {
				$_actual_db_id = 0;
			} else {
				$_actual_db_id = (int) $_item_object_data['menu-item-db-id'];
			}

			$args = array(
				'menu-item-db-id'       => ( isset( $_item_object_data['menu-item-db-id'] ) ? $_item_object_data['menu-item-db-id'] : '' ),
				'menu-item-object-id'   => ( isset( $_item_object_data['menu-item-object-id'] ) ? $_item_object_data['menu-item-object-id'] : '' ),
				'menu-item-object'      => ( isset( $_item_object_data['menu-item-object'] ) ? $_item_object_data['menu-item-object'] : '' ),
				'menu-item-parent-id'   => ( isset( $_item_object_data['menu-item-parent-id'] ) ? $_item_object_data['menu-item-parent-id'] : '' ),
				'menu-item-position'    => ( isset( $_item_object_data['menu-item-position'] ) ? $_item_object_data['menu-item-position'] : '' ),
				'menu-item-type'        => ( isset( $_item_object_data['menu-item-type'] ) ? $_item_object_data['menu-item-type'] : '' ),
				'menu-item-title'       => ( isset( $_item_object_data['menu-item-title'] ) ? $_item_object_data['menu-item-title'] : '' ),
				'menu-item-url'         => ( isset( $_item_object_data['menu-item-url'] ) ? $_item_object_data['menu-item-url'] : '' ),
				'menu-item-description' => ( isset( $_item_object_data['menu-item-description'] ) ? $_item_object_data['menu-item-description'] : '' ),
				'menu-item-attr-title'  => ( isset( $_item_object_data['menu-item-attr-title'] ) ? $_item_object_data['menu-item-attr-title'] : '' ),
				'menu-item-target'      => ( isset( $_item_object_data['menu-item-target'] ) ? $_item_object_data['menu-item-target'] : '' ),
				'menu-item-classes'     => ( isset( $_item_object_data['menu-item-classes'] ) ? $_item_object_data['menu-item-classes'] : '' ),
				'menu-item-xfn'         => ( isset( $_item_object_data['menu-item-xfn'] ) ? $_item_object_data['menu-item-xfn'] : '' ),
			);

			$items_saved[] = wp_update_nav_menu_item( $menu_id, $_actual_db_id, $args );

		}
	}

	return $items_saved;
}

/**
 * Adds custom arguments to some of the meta box object types.
 *
 * @since 3.0.0
 *
 * @access private
 *
 * @param object $data_object The post type or taxonomy meta-object.
 * @return object The post type or taxonomy object.
 */
function _wp_nav_menu_meta_box_object( $data_object = null ) {
	if ( isset( $data_object->name ) ) {

		if ( 'page' === $data_object->name ) {
			$data_object->_default_query = array(
				'orderby'     => 'menu_order title',
				'post_status' => 'publish',
			);

			// Posts should show only published items.
		} elseif ( 'post' === $data_object->name ) {
			$data_object->_default_query = array(
				'post_status' => 'publish',
			);

			// Categories should be in reverse chronological order.
		} elseif ( 'category' === $data_object->name ) {
			$data_object->_default_query = array(
				'orderby' => 'id',
				'order'   => 'DESC',
			);

			// Custom post types should show only published items.
		} else {
			$data_object->_default_query = array(
				'post_status' => 'publish',
			);
		}
	}

	return $data_object;
}

/**
 * Returns the menu formatted to edit.
 *
 * @since 3.0.0
 *
 * @param int $menu_id Optional. The ID of the menu to format. Default 0.
 * @return string|WP_Error The menu formatted to edit or error object on failure.
 */
function wp_get_nav_menu_to_edit( $menu_id = 0 ) {
	$menu = wp_get_nav_menu_object( $menu_id );

	// If the menu exists, get its items.
	if ( is_nav_menu( $menu ) ) {
		$menu_items = wp_get_nav_menu_items( $menu->term_id, array( 'post_status' => 'any' ) );
		$result     = '<div id="menu-instructions" class="post-body-plain';
		$result    .= ( ! empty( $menu_items ) ) ? ' menu-instructions-inactive">' : '">';
		$result    .= '<p>' . __( 'Add menu items from the column on the left.' ) . '</p>';
		$result    .= '</div>';

		if ( empty( $menu_items ) ) {
			return $result . ' <ul class="menu" id="menu-to-edit"> </ul>';
		}

		/**
		 * Filters the Walker class used when adding nav menu items.
		 *
		 * @since 3.0.0
		 *
		 * @param string $class   The walker class to use. Default 'Walker_Nav_Menu_Edit'.
		 * @param int    $menu_id ID of the menu being rendered.
		 */
		$walker_class_name = apply_filters( 'wp_edit_nav_menu_walker', 'Walker_Nav_Menu_Edit', $menu_id );

		if ( class_exists( $walker_class_name ) ) {
			$walker = new $walker_class_name();
		} else {
			return new WP_Error(
				'menu_walker_not_exist',
				sprintf(
					/* translators: %s: Walker class name. */
					__( 'The Walker class named %s does not exist.' ),
					'<strong>' . $walker_class_name . '</strong>'
				)
			);
		}

		$some_pending_menu_items = false;
		$some_invalid_menu_items = false;

		foreach ( (array) $menu_items as $menu_item ) {
			if ( isset( $menu_item->post_status ) && 'draft' === $menu_item->post_status ) {
				$some_pending_menu_items = true;
			}
			if ( ! empty( $menu_item->_invalid ) ) {
				$some_invalid_menu_items = true;
			}
		}

		if ( $some_pending_menu_items ) {
			$message     = __( 'Click Save Menu to make pending menu items public.' );
			$notice_args = array(
				'type'               => 'info',
				'additional_classes' => array( 'notice-alt', 'inline' ),
			);
			$result     .= wp_get_admin_notice( $message, $notice_args );
		}

		if ( $some_invalid_menu_items ) {
			$message     = __( 'There are some invalid menu items. Please check or delete them.' );
			$notice_args = array(
				'type'               => 'error',
				'additional_classes' => array( 'notice-alt', 'inline' ),
			);
			$result     .= wp_get_admin_notice( $message, $notice_args );
		}

		$result .= '<ul class="menu" id="menu-to-edit"> ';
		$result .= walk_nav_menu_tree(
			array_map( 'wp_setup_nav_menu_item', $menu_items ),
			0,
			(object) array( 'walker' => $walker )
		);
		$result .= ' </ul> ';

		return $result;
	} elseif ( is_wp_error( $menu ) ) {
		return $menu;
	}
}

/**
 * Returns the columns for the nav menus page.
 *
 * @since 3.0.0
 *
 * @return string[] Array of column titles keyed by their column name.
 */
function wp_nav_menu_manage_columns() {
	return array(
		'_title'          => __( 'Show advanced menu properties' ),
		'cb'              => '<input type="checkbox" />',
		'link-target'     => __( 'Link Target' ),
		'title-attribute' => __( 'Title Attribute' ),
		'css-classes'     => __( 'CSS Classes' ),
		'xfn'             => __( 'Link Relationship (XFN)' ),
		'description'     => __( 'Description' ),
	);
}

/**
 * Deletes orphaned draft menu items
 *
 * @access private
 * @since 3.0.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 */
function _wp_delete_orphaned_draft_menu_items() {
	global $wpdb;

	$delete_timestamp = time() - ( DAY_IN_SECONDS * EMPTY_TRASH_DAYS );

	// Delete orphaned draft menu items.
	$menu_items_to_delete = $wpdb->get_col(
		$wpdb->prepare(
			"SELECT ID FROM $wpdb->posts AS p
			LEFT JOIN $wpdb->postmeta AS m ON p.ID = m.post_id
			WHERE post_type = 'nav_menu_item' AND post_status = 'draft'
			AND meta_key = '_menu_item_orphaned' AND meta_value < %d",
			$delete_timestamp
		)
	);

	foreach ( (array) $menu_items_to_delete as $menu_item_id ) {
		wp_delete_post( $menu_item_id, true );
	}
}

/**
 * Saves nav menu items.
 *
 * @since 3.6.0
 *
 * @param int|string $nav_menu_selected_id    ID, slug, or name of the currently-selected menu.
 * @param string     $nav_menu_selected_title Title of the currently-selected menu.
 * @return string[] The menu updated messages.
 */
function wp_nav_menu_update_menu_items( $nav_menu_selected_id, $nav_menu_selected_title ) {
	$unsorted_menu_items = wp_get_nav_menu_items(
		$nav_menu_selected_id,
		array(
			'orderby'     => 'ID',
			'output'      => ARRAY_A,
			'output_key'  => 'ID',
			'post_status' => 'draft,publish',
		)
	);

	$messages   = array();
	$menu_items = array();

	// Index menu items by DB ID.
	foreach ( $unsorted_menu_items as $_item ) {
		$menu_items[ $_item->db_id ] = $_item;
	}

	$post_fields = array(
		'menu-item-db-id',
		'menu-item-object-id',
		'menu-item-object',
		'menu-item-parent-id',
		'menu-item-position',
		'menu-item-type',
		'menu-item-title',
		'menu-item-url',
		'menu-item-description',
		'menu-item-attr-title',
		'menu-item-target',
		'menu-item-classes',
		'menu-item-xfn',
	);

	wp_defer_term_counting( true );

	// Loop through all the menu items' POST variables.
	if ( ! empty( $_POST['menu-item-db-id'] ) ) {
		foreach ( (array) $_POST['menu-item-db-id'] as $_key => $k ) {

			// Menu item title can't be blank.
			if ( ! isset( $_POST['menu-item-title'][ $_key ] ) || '' === $_POST['menu-item-title'][ $_key ] ) {
				continue;
			}

			$args = array();
			foreach ( $post_fields as $field ) {
				$args[ $field ] = isset( $_POST[ $field ][ $_key ] ) ? $_POST[ $field ][ $_key ] : '';
			}

			$menu_item_db_id = wp_update_nav_menu_item(
				$nav_menu_selected_id,
				( (int) $_POST['menu-item-db-id'][ $_key ] !== $_key ? 0 : $_key ),
				$args
			);

			if ( is_wp_error( $menu_item_db_id ) ) {
				$messages[] = wp_get_admin_notice(
					$menu_item_db_id->get_error_message(),
					array(
						'id'                 => 'message',
						'additional_classes' => array( 'error' ),
					)
				);
			} else {
				unset( $menu_items[ $menu_item_db_id ] );
			}
		}
	}

	// Remove menu items from the menu that weren't in $_POST.
	if ( ! empty( $menu_items ) ) {
		foreach ( array_keys( $menu_items ) as $menu_item_id ) {
			if ( is_nav_menu_item( $menu_item_id ) ) {
				wp_delete_post( $menu_item_id );
			}
		}
	}

	// Store 'auto-add' pages.
	$auto_add        = ! empty( $_POST['auto-add-pages'] );
	$nav_menu_option = (array) get_option( 'nav_menu_options' );

	if ( ! isset( $nav_menu_option['auto_add'] ) ) {
		$nav_menu_option['auto_add'] = array();
	}

	if ( $auto_add ) {
		if ( ! in_array( $nav_menu_selected_id, $nav_menu_option['auto_add'], true ) ) {
			$nav_menu_option['auto_add'][] = $nav_menu_selected_id;
		}
	} else {
		$key = array_search( $nav_menu_selected_id, $nav_menu_option['auto_add'], true );
		if ( false !== $key ) {
			unset( $nav_menu_option['auto_add'][ $key ] );
		}
	}

	// Remove non-existent/deleted menus.
	$nav_menu_option['auto_add'] = array_intersect(
		$nav_menu_option['auto_add'],
		wp_get_nav_menus( array( 'fields' => 'ids' ) )
	);

	update_option( 'nav_menu_options', $nav_menu_option, false );

	wp_defer_term_counting( false );

	/** This action is documented in wp-includes/nav-menu.php */
	do_action( 'wp_update_nav_menu', $nav_menu_selected_id );

	/* translators: %s: Nav menu title. */
	$message     = sprintf( __( '%s has been updated.' ), '<strong>' . $nav_menu_selected_title . '</strong>' );
	$notice_args = array(
		'id'                 => 'message',
		'dismissible'        => true,
		'additional_classes' => array( 'updated' ),
	);

	$messages[] = wp_get_admin_notice( $message, $notice_args );

	unset( $menu_items, $unsorted_menu_items );

	return $messages;
}

/**
 * If a JSON blob of navigation menu data is in POST data, expand it and inject
 * it into `$_POST` to avoid PHP `max_input_vars` limitations. See #14134.
 *
 * @ignore
 * @since 4.5.3
 * @access private
 */
function _wp_expand_nav_menu_post_data() {
	if ( ! isset( $_POST['nav-menu-data'] ) ) {
		return;
	}

	$data = json_decode( stripslashes( $_POST['nav-menu-data'] ) );

	if ( ! is_null( $data ) && $data ) {
		foreach ( $data as $post_input_data ) {
			/*
			 * For input names that are arrays (e.g. `menu-item-db-id[3][4][5]`),
			 * derive the array path keys via regex and set the value in $_POST.
			 */
			preg_match( '#([^\[]*)(\[(.+)\])?#', $post_input_data->name, $matches );

			$array_bits = array( $matches[1] );

			if ( isset( $matches[3] ) ) {
				$array_bits = array_merge( $array_bits, explode( '][', $matches[3] ) );
			}

			$new_post_data = array();

			// Build the new array value from leaf to trunk.
			for ( $i = count( $array_bits ) - 1; $i >= 0; $i-- ) {
				if ( count( $array_bits ) - 1 === $i ) {
					$new_post_data[ $array_bits[ $i ] ] = wp_slash( $post_input_data->value );
				} else {
					$new_post_data = array( $array_bits[ $i ] => $new_post_data );
				}
			}

			$_POST = array_replace_recursive( $_POST, $new_post_data );
		}
	}
}
noop.php000060400000002174151327343360006237 0ustar00<?php
/**
 * Noop functions for load-scripts.php and load-styles.php.
 *
 * @package WordPress
 * @subpackage Administration
 * @since 4.4.0
 */

/**
 * @ignore
 */
function __() {}

/**
 * @ignore
 */
function _x() {}

/**
 * @ignore
 */
function add_filter() {}

/**
 * @ignore
 */
function has_filter() {
	return false;
}

/**
 * @ignore
 */
function esc_attr() {}

/**
 * @ignore
 */
function apply_filters() {}

/**
 * @ignore
 */
function get_option() {}

/**
 * @ignore
 */
function is_lighttpd_before_150() {}

/**
 * @ignore
 */
function add_action() {}

/**
 * @ignore
 */
function did_action() {}

/**
 * @ignore
 */
function do_action_ref_array() {}

/**
 * @ignore
 */
function get_bloginfo() {}

/**
 * @ignore
 */
function is_admin() {
	return true;
}

/**
 * @ignore
 */
function site_url() {}

/**
 * @ignore
 */
function admin_url() {}

/**
 * @ignore
 */
function home_url() {}

/**
 * @ignore
 */
function includes_url() {}

/**
 * @ignore
 */
function wp_guess_url() {}

function get_file( $path ) {

	$path = realpath( $path );

	if ( ! $path || ! @is_file( $path ) ) {
		return '';
	}

	return @file_get_contents( $path );
}
privacy-tools.php000060400000101266151327343360010101 0ustar00<?php
/**
 * WordPress Administration Privacy Tools API.
 *
 * @package WordPress
 * @subpackage Administration
 */

/**
 * Resend an existing request and return the result.
 *
 * @since 4.9.6
 * @access private
 *
 * @param int $request_id Request ID.
 * @return true|WP_Error Returns true if sending the email was successful, or a WP_Error object.
 */
function _wp_privacy_resend_request( $request_id ) {
	$request_id = absint( $request_id );
	$request    = get_post( $request_id );

	if ( ! $request || 'user_request' !== $request->post_type ) {
		return new WP_Error( 'privacy_request_error', __( 'Invalid personal data request.' ) );
	}

	$result = wp_send_user_request( $request_id );

	if ( is_wp_error( $result ) ) {
		return $result;
	} elseif ( ! $result ) {
		return new WP_Error( 'privacy_request_error', __( 'Unable to initiate confirmation for personal data request.' ) );
	}

	return true;
}

/**
 * Marks a request as completed by the admin and logs the current timestamp.
 *
 * @since 4.9.6
 * @access private
 *
 * @param int $request_id Request ID.
 * @return int|WP_Error Request ID on success, or a WP_Error on failure.
 */
function _wp_privacy_completed_request( $request_id ) {
	// Get the request.
	$request_id = absint( $request_id );
	$request    = wp_get_user_request( $request_id );

	if ( ! $request ) {
		return new WP_Error( 'privacy_request_error', __( 'Invalid personal data request.' ) );
	}

	update_post_meta( $request_id, '_wp_user_request_completed_timestamp', time() );

	$result = wp_update_post(
		array(
			'ID'          => $request_id,
			'post_status' => 'request-completed',
		)
	);

	return $result;
}

/**
 * Handle list table actions.
 *
 * @since 4.9.6
 * @access private
 */
function _wp_personal_data_handle_actions() {
	if ( isset( $_POST['privacy_action_email_retry'] ) ) {
		check_admin_referer( 'bulk-privacy_requests' );

		$request_id = absint( current( array_keys( (array) wp_unslash( $_POST['privacy_action_email_retry'] ) ) ) );
		$result     = _wp_privacy_resend_request( $request_id );

		if ( is_wp_error( $result ) ) {
			add_settings_error(
				'privacy_action_email_retry',
				'privacy_action_email_retry',
				$result->get_error_message(),
				'error'
			);
		} else {
			add_settings_error(
				'privacy_action_email_retry',
				'privacy_action_email_retry',
				__( 'Confirmation request sent again successfully.' ),
				'success'
			);
		}
	} elseif ( isset( $_POST['action'] ) ) {
		$action = ! empty( $_POST['action'] ) ? sanitize_key( wp_unslash( $_POST['action'] ) ) : '';

		switch ( $action ) {
			case 'add_export_personal_data_request':
			case 'add_remove_personal_data_request':
				check_admin_referer( 'personal-data-request' );

				if ( ! isset( $_POST['type_of_action'], $_POST['username_or_email_for_privacy_request'] ) ) {
					add_settings_error(
						'action_type',
						'action_type',
						__( 'Invalid personal data action.' ),
						'error'
					);
				}
				$action_type               = sanitize_text_field( wp_unslash( $_POST['type_of_action'] ) );
				$username_or_email_address = sanitize_text_field( wp_unslash( $_POST['username_or_email_for_privacy_request'] ) );
				$email_address             = '';
				$status                    = 'pending';

				if ( ! isset( $_POST['send_confirmation_email'] ) ) {
					$status = 'confirmed';
				}

				if ( ! in_array( $action_type, _wp_privacy_action_request_types(), true ) ) {
					add_settings_error(
						'action_type',
						'action_type',
						__( 'Invalid personal data action.' ),
						'error'
					);
				}

				if ( ! is_email( $username_or_email_address ) ) {
					$user = get_user_by( 'login', $username_or_email_address );
					if ( ! $user instanceof WP_User ) {
						add_settings_error(
							'username_or_email_for_privacy_request',
							'username_or_email_for_privacy_request',
							__( 'Unable to add this request. A valid email address or username must be supplied.' ),
							'error'
						);
					} else {
						$email_address = $user->user_email;
					}
				} else {
					$email_address = $username_or_email_address;
				}

				if ( empty( $email_address ) ) {
					break;
				}

				$request_id = wp_create_user_request( $email_address, $action_type, array(), $status );
				$message    = '';

				if ( is_wp_error( $request_id ) ) {
					$message = $request_id->get_error_message();
				} elseif ( ! $request_id ) {
					$message = __( 'Unable to initiate confirmation request.' );
				}

				if ( $message ) {
					add_settings_error(
						'username_or_email_for_privacy_request',
						'username_or_email_for_privacy_request',
						$message,
						'error'
					);
					break;
				}

				if ( 'pending' === $status ) {
					wp_send_user_request( $request_id );

					$message = __( 'Confirmation request initiated successfully.' );
				} elseif ( 'confirmed' === $status ) {
					$message = __( 'Request added successfully.' );
				}

				if ( $message ) {
					add_settings_error(
						'username_or_email_for_privacy_request',
						'username_or_email_for_privacy_request',
						$message,
						'success'
					);
					break;
				}
		}
	}
}

/**
 * Cleans up failed and expired requests before displaying the list table.
 *
 * @since 4.9.6
 * @access private
 */
function _wp_personal_data_cleanup_requests() {
	/** This filter is documented in wp-includes/user.php */
	$expires = (int) apply_filters( 'user_request_key_expiration', DAY_IN_SECONDS );

	$requests_query = new WP_Query(
		array(
			'post_type'      => 'user_request',
			'posts_per_page' => -1,
			'post_status'    => 'request-pending',
			'fields'         => 'ids',
			'date_query'     => array(
				array(
					'column' => 'post_modified_gmt',
					'before' => $expires . ' seconds ago',
				),
			),
		)
	);

	$request_ids = $requests_query->posts;

	foreach ( $request_ids as $request_id ) {
		wp_update_post(
			array(
				'ID'            => $request_id,
				'post_status'   => 'request-failed',
				'post_password' => '',
			)
		);
	}
}

/**
 * Generate a single group for the personal data export report.
 *
 * @since 4.9.6
 * @since 5.4.0 Added the `$group_id` and `$groups_count` parameters.
 *
 * @param array  $group_data {
 *     The group data to render.
 *
 *     @type string $group_label  The user-facing heading for the group, e.g. 'Comments'.
 *     @type array  $items        {
 *         An array of group items.
 *
 *         @type array  $group_item_data  {
 *             An array of name-value pairs for the item.
 *
 *             @type string $name   The user-facing name of an item name-value pair, e.g. 'IP Address'.
 *             @type string $value  The user-facing value of an item data pair, e.g. '50.60.70.0'.
 *         }
 *     }
 * }
 * @param string $group_id     The group identifier.
 * @param int    $groups_count The number of all groups
 * @return string The HTML for this group and its items.
 */
function wp_privacy_generate_personal_data_export_group_html( $group_data, $group_id = '', $groups_count = 1 ) {
	$group_id_attr = sanitize_title_with_dashes( $group_data['group_label'] . '-' . $group_id );

	$group_html  = '<h2 id="' . esc_attr( $group_id_attr ) . '">';
	$group_html .= esc_html( $group_data['group_label'] );

	$items_count = count( (array) $group_data['items'] );
	if ( $items_count > 1 ) {
		$group_html .= sprintf( ' <span class="count">(%d)</span>', $items_count );
	}

	$group_html .= '</h2>';

	if ( ! empty( $group_data['group_description'] ) ) {
		$group_html .= '<p>' . esc_html( $group_data['group_description'] ) . '</p>';
	}

	$group_html .= '<div>';

	foreach ( (array) $group_data['items'] as $group_item_id => $group_item_data ) {
		$group_html .= '<table>';
		$group_html .= '<tbody>';

		foreach ( (array) $group_item_data as $group_item_datum ) {
			$value = $group_item_datum['value'];
			// If it looks like a link, make it a link.
			if ( ! str_contains( $value, ' ' ) && ( str_starts_with( $value, 'http://' ) || str_starts_with( $value, 'https://' ) ) ) {
				$value = '<a href="' . esc_url( $value ) . '">' . esc_html( $value ) . '</a>';
			}

			$group_html .= '<tr>';
			$group_html .= '<th>' . esc_html( $group_item_datum['name'] ) . '</th>';
			$group_html .= '<td>' . wp_kses( $value, 'personal_data_export' ) . '</td>';
			$group_html .= '</tr>';
		}

		$group_html .= '</tbody>';
		$group_html .= '</table>';
	}

	if ( $groups_count > 1 ) {
		$group_html .= '<div class="return-to-top">';
		$group_html .= '<a href="#top"><span aria-hidden="true">&uarr; </span> ' . esc_html__( 'Go to top' ) . '</a>';
		$group_html .= '</div>';
	}

	$group_html .= '</div>';

	return $group_html;
}

/**
 * Generate the personal data export file.
 *
 * @since 4.9.6
 *
 * @param int $request_id The export request ID.
 */
function wp_privacy_generate_personal_data_export_file( $request_id ) {
	if ( ! class_exists( 'ZipArchive' ) ) {
		wp_send_json_error( __( 'Unable to generate personal data export file. ZipArchive not available.' ) );
	}

	// Get the request.
	$request = wp_get_user_request( $request_id );

	if ( ! $request || 'export_personal_data' !== $request->action_name ) {
		wp_send_json_error( __( 'Invalid request ID when generating personal data export file.' ) );
	}

	$email_address = $request->email;

	if ( ! is_email( $email_address ) ) {
		wp_send_json_error( __( 'Invalid email address when generating personal data export file.' ) );
	}

	// Create the exports folder if needed.
	$exports_dir = wp_privacy_exports_dir();
	$exports_url = wp_privacy_exports_url();

	if ( ! wp_mkdir_p( $exports_dir ) ) {
		wp_send_json_error( __( 'Unable to create personal data export folder.' ) );
	}

	// Protect export folder from browsing.
	$index_pathname = $exports_dir . 'index.php';
	if ( ! file_exists( $index_pathname ) ) {
		$file = fopen( $index_pathname, 'w' );
		if ( false === $file ) {
			wp_send_json_error( __( 'Unable to protect personal data export folder from browsing.' ) );
		}
		fwrite( $file, "<?php\n// Silence is golden.\n" );
		fclose( $file );
	}

	$obscura              = wp_generate_password( 32, false, false );
	$file_basename        = 'wp-personal-data-file-' . $obscura;
	$html_report_filename = wp_unique_filename( $exports_dir, $file_basename . '.html' );
	$html_report_pathname = wp_normalize_path( $exports_dir . $html_report_filename );
	$json_report_filename = $file_basename . '.json';
	$json_report_pathname = wp_normalize_path( $exports_dir . $json_report_filename );

	/*
	 * Gather general data needed.
	 */

	// Title.
	$title = sprintf(
		/* translators: %s: User's email address. */
		__( 'Personal Data Export for %s' ),
		$email_address
	);

	// First, build an "About" group on the fly for this report.
	$about_group = array(
		/* translators: Header for the About section in a personal data export. */
		'group_label'       => _x( 'About', 'personal data group label' ),
		/* translators: Description for the About section in a personal data export. */
		'group_description' => _x( 'Overview of export report.', 'personal data group description' ),
		'items'             => array(
			'about-1' => array(
				array(
					'name'  => _x( 'Report generated for', 'email address' ),
					'value' => $email_address,
				),
				array(
					'name'  => _x( 'For site', 'website name' ),
					'value' => get_bloginfo( 'name' ),
				),
				array(
					'name'  => _x( 'At URL', 'website URL' ),
					'value' => get_bloginfo( 'url' ),
				),
				array(
					'name'  => _x( 'On', 'date/time' ),
					'value' => current_time( 'mysql' ),
				),
			),
		),
	);

	// And now, all the Groups.
	$groups = get_post_meta( $request_id, '_export_data_grouped', true );
	if ( is_array( $groups ) ) {
		// Merge in the special "About" group.
		$groups       = array_merge( array( 'about' => $about_group ), $groups );
		$groups_count = count( $groups );
	} else {
		if ( false !== $groups ) {
			_doing_it_wrong(
				__FUNCTION__,
				/* translators: %s: Post meta key. */
				sprintf( __( 'The %s post meta must be an array.' ), '<code>_export_data_grouped</code>' ),
				'5.8.0'
			);
		}

		$groups       = null;
		$groups_count = 0;
	}

	// Convert the groups to JSON format.
	$groups_json = wp_json_encode( $groups );

	if ( false === $groups_json ) {
		$error_message = sprintf(
			/* translators: %s: Error message. */
			__( 'Unable to encode the personal data for export. Error: %s' ),
			json_last_error_msg()
		);

		wp_send_json_error( $error_message );
	}

	/*
	 * Handle the JSON export.
	 */
	$file = fopen( $json_report_pathname, 'w' );

	if ( false === $file ) {
		wp_send_json_error( __( 'Unable to open personal data export file (JSON report) for writing.' ) );
	}

	fwrite( $file, '{' );
	fwrite( $file, '"' . $title . '":' );
	fwrite( $file, $groups_json );
	fwrite( $file, '}' );
	fclose( $file );

	/*
	 * Handle the HTML export.
	 */
	$file = fopen( $html_report_pathname, 'w' );

	if ( false === $file ) {
		wp_send_json_error( __( 'Unable to open personal data export (HTML report) for writing.' ) );
	}

	fwrite( $file, "<!DOCTYPE html>\n" );
	fwrite( $file, "<html>\n" );
	fwrite( $file, "<head>\n" );
	fwrite( $file, "<meta http-equiv='Content-Type' content='text/html; charset=UTF-8' />\n" );
	fwrite( $file, "<style type='text/css'>" );
	fwrite( $file, 'body { color: black; font-family: Arial, sans-serif; font-size: 11pt; margin: 15px auto; width: 860px; }' );
	fwrite( $file, 'table { background: #f0f0f0; border: 1px solid #ddd; margin-bottom: 20px; width: 100%; }' );
	fwrite( $file, 'th { padding: 5px; text-align: left; width: 20%; }' );
	fwrite( $file, 'td { padding: 5px; }' );
	fwrite( $file, 'tr:nth-child(odd) { background-color: #fafafa; }' );
	fwrite( $file, '.return-to-top { text-align: right; }' );
	fwrite( $file, '</style>' );
	fwrite( $file, '<title>' );
	fwrite( $file, esc_html( $title ) );
	fwrite( $file, '</title>' );
	fwrite( $file, "</head>\n" );
	fwrite( $file, "<body>\n" );
	fwrite( $file, '<h1 id="top">' . esc_html__( 'Personal Data Export' ) . '</h1>' );

	// Create TOC.
	if ( $groups_count > 1 ) {
		fwrite( $file, '<div id="table_of_contents">' );
		fwrite( $file, '<h2>' . esc_html__( 'Table of Contents' ) . '</h2>' );
		fwrite( $file, '<ul>' );
		foreach ( (array) $groups as $group_id => $group_data ) {
			$group_label       = esc_html( $group_data['group_label'] );
			$group_id_attr     = sanitize_title_with_dashes( $group_data['group_label'] . '-' . $group_id );
			$group_items_count = count( (array) $group_data['items'] );
			if ( $group_items_count > 1 ) {
				$group_label .= sprintf( ' <span class="count">(%d)</span>', $group_items_count );
			}
			fwrite( $file, '<li>' );
			fwrite( $file, '<a href="#' . esc_attr( $group_id_attr ) . '">' . $group_label . '</a>' );
			fwrite( $file, '</li>' );
		}
		fwrite( $file, '</ul>' );
		fwrite( $file, '</div>' );
	}

	// Now, iterate over every group in $groups and have the formatter render it in HTML.
	foreach ( (array) $groups as $group_id => $group_data ) {
		fwrite( $file, wp_privacy_generate_personal_data_export_group_html( $group_data, $group_id, $groups_count ) );
	}

	fwrite( $file, "</body>\n" );
	fwrite( $file, "</html>\n" );
	fclose( $file );

	/*
	 * Now, generate the ZIP.
	 *
	 * If an archive has already been generated, then remove it and reuse the filename,
	 * to avoid breaking any URLs that may have been previously sent via email.
	 */
	$error = false;

	// This meta value is used from version 5.5.
	$archive_filename = get_post_meta( $request_id, '_export_file_name', true );

	// This one stored an absolute path and is used for backward compatibility.
	$archive_pathname = get_post_meta( $request_id, '_export_file_path', true );

	// If a filename meta exists, use it.
	if ( ! empty( $archive_filename ) ) {
		$archive_pathname = $exports_dir . $archive_filename;
	} elseif ( ! empty( $archive_pathname ) ) {
		// If a full path meta exists, use it and create the new meta value.
		$archive_filename = basename( $archive_pathname );

		update_post_meta( $request_id, '_export_file_name', $archive_filename );

		// Remove the back-compat meta values.
		delete_post_meta( $request_id, '_export_file_url' );
		delete_post_meta( $request_id, '_export_file_path' );
	} else {
		// If there's no filename or full path stored, create a new file.
		$archive_filename = $file_basename . '.zip';
		$archive_pathname = $exports_dir . $archive_filename;

		update_post_meta( $request_id, '_export_file_name', $archive_filename );
	}

	$archive_url = $exports_url . $archive_filename;

	if ( ! empty( $archive_pathname ) && file_exists( $archive_pathname ) ) {
		wp_delete_file( $archive_pathname );
	}

	$zip = new ZipArchive();
	if ( true === $zip->open( $archive_pathname, ZipArchive::CREATE ) ) {
		if ( ! $zip->addFile( $json_report_pathname, 'export.json' ) ) {
			$error = __( 'Unable to archive the personal data export file (JSON format).' );
		}

		if ( ! $zip->addFile( $html_report_pathname, 'index.html' ) ) {
			$error = __( 'Unable to archive the personal data export file (HTML format).' );
		}

		$zip->close();

		if ( ! $error ) {
			/**
			 * Fires right after all personal data has been written to the export file.
			 *
			 * @since 4.9.6
			 * @since 5.4.0 Added the `$json_report_pathname` parameter.
			 *
			 * @param string $archive_pathname     The full path to the export file on the filesystem.
			 * @param string $archive_url          The URL of the archive file.
			 * @param string $html_report_pathname The full path to the HTML personal data report on the filesystem.
			 * @param int    $request_id           The export request ID.
			 * @param string $json_report_pathname The full path to the JSON personal data report on the filesystem.
			 */
			do_action( 'wp_privacy_personal_data_export_file_created', $archive_pathname, $archive_url, $html_report_pathname, $request_id, $json_report_pathname );
		}
	} else {
		$error = __( 'Unable to open personal data export file (archive) for writing.' );
	}

	// Remove the JSON file.
	unlink( $json_report_pathname );

	// Remove the HTML file.
	unlink( $html_report_pathname );

	if ( $error ) {
		wp_send_json_error( $error );
	}
}

/**
 * Send an email to the user with a link to the personal data export file
 *
 * @since 4.9.6
 *
 * @param int $request_id The request ID for this personal data export.
 * @return true|WP_Error True on success or `WP_Error` on failure.
 */
function wp_privacy_send_personal_data_export_email( $request_id ) {
	// Get the request.
	$request = wp_get_user_request( $request_id );

	if ( ! $request || 'export_personal_data' !== $request->action_name ) {
		return new WP_Error( 'invalid_request', __( 'Invalid request ID when sending personal data export email.' ) );
	}

	// Localize message content for user; fallback to site default for visitors.
	if ( ! empty( $request->user_id ) ) {
		$switched_locale = switch_to_user_locale( $request->user_id );
	} else {
		$switched_locale = switch_to_locale( get_locale() );
	}

	/** This filter is documented in wp-includes/functions.php */
	$expiration      = apply_filters( 'wp_privacy_export_expiration', 3 * DAY_IN_SECONDS );
	$expiration_date = date_i18n( get_option( 'date_format' ), time() + $expiration );

	$exports_url      = wp_privacy_exports_url();
	$export_file_name = get_post_meta( $request_id, '_export_file_name', true );
	$export_file_url  = $exports_url . $export_file_name;

	$site_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
	$site_url  = home_url();

	/**
	 * Filters the recipient of the personal data export email notification.
	 * Should be used with great caution to avoid sending the data export link to wrong emails.
	 *
	 * @since 5.3.0
	 *
	 * @param string          $request_email The email address of the notification recipient.
	 * @param WP_User_Request $request       The request that is initiating the notification.
	 */
	$request_email = apply_filters( 'wp_privacy_personal_data_email_to', $request->email, $request );

	$email_data = array(
		'request'           => $request,
		'expiration'        => $expiration,
		'expiration_date'   => $expiration_date,
		'message_recipient' => $request_email,
		'export_file_url'   => $export_file_url,
		'sitename'          => $site_name,
		'siteurl'           => $site_url,
	);

	/* translators: Personal data export notification email subject. %s: Site title. */
	$subject = sprintf( __( '[%s] Personal Data Export' ), $site_name );

	/**
	 * Filters the subject of the email sent when an export request is completed.
	 *
	 * @since 5.3.0
	 *
	 * @param string $subject    The email subject.
	 * @param string $sitename   The name of the site.
	 * @param array  $email_data {
	 *     Data relating to the account action email.
	 *
	 *     @type WP_User_Request $request           User request object.
	 *     @type int             $expiration        The time in seconds until the export file expires.
	 *     @type string          $expiration_date   The localized date and time when the export file expires.
	 *     @type string          $message_recipient The address that the email will be sent to. Defaults
	 *                                              to the value of `$request->email`, but can be changed
	 *                                              by the `wp_privacy_personal_data_email_to` filter.
	 *     @type string          $export_file_url   The export file URL.
	 *     @type string          $sitename          The site name sending the mail.
	 *     @type string          $siteurl           The site URL sending the mail.
	 * }
	 */
	$subject = apply_filters( 'wp_privacy_personal_data_email_subject', $subject, $site_name, $email_data );

	/* translators: Do not translate EXPIRATION, LINK, SITENAME, SITEURL: those are placeholders. */
	$email_text = __(
		'Howdy,

Your request for an export of personal data has been completed. You may
download your personal data by clicking on the link below. For privacy
and security, we will automatically delete the file on ###EXPIRATION###,
so please download it before then.

###LINK###

Regards,
All at ###SITENAME###
###SITEURL###'
	);

	/**
	 * Filters the text of the email sent with a personal data export file.
	 *
	 * The following strings have a special meaning and will get replaced dynamically:
	 * ###EXPIRATION###         The date when the URL will be automatically deleted.
	 * ###LINK###               URL of the personal data export file for the user.
	 * ###SITENAME###           The name of the site.
	 * ###SITEURL###            The URL to the site.
	 *
	 * @since 4.9.6
	 * @since 5.3.0 Introduced the `$email_data` array.
	 *
	 * @param string $email_text Text in the email.
	 * @param int    $request_id The request ID for this personal data export.
	 * @param array  $email_data {
	 *     Data relating to the account action email.
	 *
	 *     @type WP_User_Request $request           User request object.
	 *     @type int             $expiration        The time in seconds until the export file expires.
	 *     @type string          $expiration_date   The localized date and time when the export file expires.
	 *     @type string          $message_recipient The address that the email will be sent to. Defaults
	 *                                              to the value of `$request->email`, but can be changed
	 *                                              by the `wp_privacy_personal_data_email_to` filter.
	 *     @type string          $export_file_url   The export file URL.
	 *     @type string          $sitename          The site name sending the mail.
	 *     @type string          $siteurl           The site URL sending the mail.
	 */
	$content = apply_filters( 'wp_privacy_personal_data_email_content', $email_text, $request_id, $email_data );

	$content = str_replace( '###EXPIRATION###', $expiration_date, $content );
	$content = str_replace( '###LINK###', sanitize_url( $export_file_url ), $content );
	$content = str_replace( '###EMAIL###', $request_email, $content );
	$content = str_replace( '###SITENAME###', $site_name, $content );
	$content = str_replace( '###SITEURL###', sanitize_url( $site_url ), $content );

	$headers = '';

	/**
	 * Filters the headers of the email sent with a personal data export file.
	 *
	 * @since 5.4.0
	 *
	 * @param string|array $headers    The email headers.
	 * @param string       $subject    The email subject.
	 * @param string       $content    The email content.
	 * @param int          $request_id The request ID.
	 * @param array        $email_data {
	 *     Data relating to the account action email.
	 *
	 *     @type WP_User_Request $request           User request object.
	 *     @type int             $expiration        The time in seconds until the export file expires.
	 *     @type string          $expiration_date   The localized date and time when the export file expires.
	 *     @type string          $message_recipient The address that the email will be sent to. Defaults
	 *                                              to the value of `$request->email`, but can be changed
	 *                                              by the `wp_privacy_personal_data_email_to` filter.
	 *     @type string          $export_file_url   The export file URL.
	 *     @type string          $sitename          The site name sending the mail.
	 *     @type string          $siteurl           The site URL sending the mail.
	 * }
	 */
	$headers = apply_filters( 'wp_privacy_personal_data_email_headers', $headers, $subject, $content, $request_id, $email_data );

	$mail_success = wp_mail( $request_email, $subject, $content, $headers );

	if ( $switched_locale ) {
		restore_previous_locale();
	}

	if ( ! $mail_success ) {
		return new WP_Error( 'privacy_email_error', __( 'Unable to send personal data export email.' ) );
	}

	return true;
}

/**
 * Intercept personal data exporter page Ajax responses in order to assemble the personal data export file.
 *
 * @since 4.9.6
 *
 * @see 'wp_privacy_personal_data_export_page'
 *
 * @param array  $response        The response from the personal data exporter for the given page.
 * @param int    $exporter_index  The index of the personal data exporter. Begins at 1.
 * @param string $email_address   The email address of the user whose personal data this is.
 * @param int    $page            The page of personal data for this exporter. Begins at 1.
 * @param int    $request_id      The request ID for this personal data export.
 * @param bool   $send_as_email   Whether the final results of the export should be emailed to the user.
 * @param string $exporter_key    The slug (key) of the exporter.
 * @return array The filtered response.
 */
function wp_privacy_process_personal_data_export_page( $response, $exporter_index, $email_address, $page, $request_id, $send_as_email, $exporter_key ) {
	/* Do some simple checks on the shape of the response from the exporter.
	 * If the exporter response is malformed, don't attempt to consume it - let it
	 * pass through to generate a warning to the user by default Ajax processing.
	 */
	if ( ! is_array( $response ) ) {
		return $response;
	}

	if ( ! array_key_exists( 'done', $response ) ) {
		return $response;
	}

	if ( ! array_key_exists( 'data', $response ) ) {
		return $response;
	}

	if ( ! is_array( $response['data'] ) ) {
		return $response;
	}

	// Get the request.
	$request = wp_get_user_request( $request_id );

	if ( ! $request || 'export_personal_data' !== $request->action_name ) {
		wp_send_json_error( __( 'Invalid request ID when merging personal data to export.' ) );
	}

	$export_data = array();

	// First exporter, first page? Reset the report data accumulation array.
	if ( 1 === $exporter_index && 1 === $page ) {
		update_post_meta( $request_id, '_export_data_raw', $export_data );
	} else {
		$accumulated_data = get_post_meta( $request_id, '_export_data_raw', true );

		if ( $accumulated_data ) {
			$export_data = $accumulated_data;
		}
	}

	// Now, merge the data from the exporter response into the data we have accumulated already.
	$export_data = array_merge( $export_data, $response['data'] );
	update_post_meta( $request_id, '_export_data_raw', $export_data );

	// If we are not yet on the last page of the last exporter, return now.
	/** This filter is documented in wp-admin/includes/ajax-actions.php */
	$exporters        = apply_filters( 'wp_privacy_personal_data_exporters', array() );
	$is_last_exporter = count( $exporters ) === $exporter_index;
	$exporter_done    = $response['done'];
	if ( ! $is_last_exporter || ! $exporter_done ) {
		return $response;
	}

	// Last exporter, last page - let's prepare the export file.

	// First we need to re-organize the raw data hierarchically in groups and items.
	$groups = array();
	foreach ( (array) $export_data as $export_datum ) {
		$group_id    = $export_datum['group_id'];
		$group_label = $export_datum['group_label'];

		$group_description = '';
		if ( ! empty( $export_datum['group_description'] ) ) {
			$group_description = $export_datum['group_description'];
		}

		if ( ! array_key_exists( $group_id, $groups ) ) {
			$groups[ $group_id ] = array(
				'group_label'       => $group_label,
				'group_description' => $group_description,
				'items'             => array(),
			);
		}

		$item_id = $export_datum['item_id'];
		if ( ! array_key_exists( $item_id, $groups[ $group_id ]['items'] ) ) {
			$groups[ $group_id ]['items'][ $item_id ] = array();
		}

		$old_item_data                            = $groups[ $group_id ]['items'][ $item_id ];
		$merged_item_data                         = array_merge( $export_datum['data'], $old_item_data );
		$groups[ $group_id ]['items'][ $item_id ] = $merged_item_data;
	}

	// Then save the grouped data into the request.
	delete_post_meta( $request_id, '_export_data_raw' );
	update_post_meta( $request_id, '_export_data_grouped', $groups );

	/**
	 * Generate the export file from the collected, grouped personal data.
	 *
	 * @since 4.9.6
	 *
	 * @param int $request_id The export request ID.
	 */
	do_action( 'wp_privacy_personal_data_export_file', $request_id );

	// Clear the grouped data now that it is no longer needed.
	delete_post_meta( $request_id, '_export_data_grouped' );

	// If the destination is email, send it now.
	if ( $send_as_email ) {
		$mail_success = wp_privacy_send_personal_data_export_email( $request_id );
		if ( is_wp_error( $mail_success ) ) {
			wp_send_json_error( $mail_success->get_error_message() );
		}

		// Update the request to completed state when the export email is sent.
		_wp_privacy_completed_request( $request_id );
	} else {
		// Modify the response to include the URL of the export file so the browser can fetch it.
		$exports_url      = wp_privacy_exports_url();
		$export_file_name = get_post_meta( $request_id, '_export_file_name', true );
		$export_file_url  = $exports_url . $export_file_name;

		if ( ! empty( $export_file_url ) ) {
			$response['url'] = $export_file_url;
		}
	}

	return $response;
}

/**
 * Mark erasure requests as completed after processing is finished.
 *
 * This intercepts the Ajax responses to personal data eraser page requests, and
 * monitors the status of a request. Once all of the processing has finished, the
 * request is marked as completed.
 *
 * @since 4.9.6
 *
 * @see 'wp_privacy_personal_data_erasure_page'
 *
 * @param array  $response      The response from the personal data eraser for
 *                              the given page.
 * @param int    $eraser_index  The index of the personal data eraser. Begins
 *                              at 1.
 * @param string $email_address The email address of the user whose personal
 *                              data this is.
 * @param int    $page          The page of personal data for this eraser.
 *                              Begins at 1.
 * @param int    $request_id    The request ID for this personal data erasure.
 * @return array The filtered response.
 */
function wp_privacy_process_personal_data_erasure_page( $response, $eraser_index, $email_address, $page, $request_id ) {
	/*
	 * If the eraser response is malformed, don't attempt to consume it; let it
	 * pass through, so that the default Ajax processing will generate a warning
	 * to the user.
	 */
	if ( ! is_array( $response ) ) {
		return $response;
	}

	if ( ! array_key_exists( 'done', $response ) ) {
		return $response;
	}

	if ( ! array_key_exists( 'items_removed', $response ) ) {
		return $response;
	}

	if ( ! array_key_exists( 'items_retained', $response ) ) {
		return $response;
	}

	if ( ! array_key_exists( 'messages', $response ) ) {
		return $response;
	}

	// Get the request.
	$request = wp_get_user_request( $request_id );

	if ( ! $request || 'remove_personal_data' !== $request->action_name ) {
		wp_send_json_error( __( 'Invalid request ID when processing personal data to erase.' ) );
	}

	/** This filter is documented in wp-admin/includes/ajax-actions.php */
	$erasers        = apply_filters( 'wp_privacy_personal_data_erasers', array() );
	$is_last_eraser = count( $erasers ) === $eraser_index;
	$eraser_done    = $response['done'];

	if ( ! $is_last_eraser || ! $eraser_done ) {
		return $response;
	}

	_wp_privacy_completed_request( $request_id );

	/**
	 * Fires immediately after a personal data erasure request has been marked completed.
	 *
	 * @since 4.9.6
	 *
	 * @param int $request_id The privacy request post ID associated with this request.
	 */
	do_action( 'wp_privacy_personal_data_erased', $request_id );

	return $response;
}
class-wp-site-health-auto-updates.php000064400000034001151327343360013631 0ustar00<?php
/**
 * Class for testing automatic updates in the WordPress code.
 *
 * @package WordPress
 * @subpackage Site_Health
 * @since 5.2.0
 */

#[AllowDynamicProperties]
class WP_Site_Health_Auto_Updates {
	/**
	 * WP_Site_Health_Auto_Updates constructor.
	 *
	 * @since 5.2.0
	 */
	public function __construct() {
		require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
	}


	/**
	 * Runs tests to determine if auto-updates can run.
	 *
	 * @since 5.2.0
	 *
	 * @return array The test results.
	 */
	public function run_tests() {
		$tests = array(
			$this->test_constants( 'WP_AUTO_UPDATE_CORE', array( true, 'beta', 'rc', 'development', 'branch-development', 'minor' ) ),
			$this->test_wp_version_check_attached(),
			$this->test_filters_automatic_updater_disabled(),
			$this->test_wp_automatic_updates_disabled(),
			$this->test_if_failed_update(),
			$this->test_vcs_abspath(),
			$this->test_check_wp_filesystem_method(),
			$this->test_all_files_writable(),
			$this->test_accepts_dev_updates(),
			$this->test_accepts_minor_updates(),
		);

		$tests = array_filter( $tests );
		$tests = array_map(
			static function ( $test ) {
				$test = (object) $test;

				if ( empty( $test->severity ) ) {
					$test->severity = 'warning';
				}

				return $test;
			},
			$tests
		);

		return $tests;
	}

	/**
	 * Tests if auto-updates related constants are set correctly.
	 *
	 * @since 5.2.0
	 * @since 5.5.1 The `$value` parameter can accept an array.
	 *
	 * @param string $constant         The name of the constant to check.
	 * @param bool|string|array $value The value that the constant should be, if set,
	 *                                 or an array of acceptable values.
	 * @return array|null The test results if there are any constants set incorrectly,
	 *                    or null if the test passed.
	 */
	public function test_constants( $constant, $value ) {
		$acceptable_values = (array) $value;

		if ( defined( $constant ) && ! in_array( constant( $constant ), $acceptable_values, true ) ) {
			return array(
				'description' => sprintf(
					/* translators: 1: Name of the constant used. 2: Value of the constant used. */
					__( 'The %1$s constant is defined as %2$s' ),
					"<code>$constant</code>",
					'<code>' . esc_html( var_export( constant( $constant ), true ) ) . '</code>'
				),
				'severity'    => 'fail',
			);
		}

		return null;
	}

	/**
	 * Checks if updates are intercepted by a filter.
	 *
	 * @since 5.2.0
	 *
	 * @return array|null The test results if wp_version_check() is disabled,
	 *                    or null if the test passed.
	 */
	public function test_wp_version_check_attached() {
		if ( ( ! is_multisite() || is_main_site() && is_network_admin() )
			&& ! has_filter( 'wp_version_check', 'wp_version_check' )
		) {
			return array(
				'description' => sprintf(
					/* translators: %s: Name of the filter used. */
					__( 'A plugin has prevented updates by disabling %s.' ),
					'<code>wp_version_check()</code>'
				),
				'severity'    => 'fail',
			);
		}

		return null;
	}

	/**
	 * Checks if automatic updates are disabled by a filter.
	 *
	 * @since 5.2.0
	 *
	 * @return array|null The test results if the {@see 'automatic_updater_disabled'} filter is set,
	 *                    or null if the test passed.
	 */
	public function test_filters_automatic_updater_disabled() {
		/** This filter is documented in wp-admin/includes/class-wp-automatic-updater.php */
		if ( apply_filters( 'automatic_updater_disabled', false ) ) {
			return array(
				'description' => sprintf(
					/* translators: %s: Name of the filter used. */
					__( 'The %s filter is enabled.' ),
					'<code>automatic_updater_disabled</code>'
				),
				'severity'    => 'fail',
			);
		}

		return null;
	}

	/**
	 * Checks if automatic updates are disabled.
	 *
	 * @since 5.3.0
	 *
	 * @return array|false The test results if auto-updates are disabled, false otherwise.
	 */
	public function test_wp_automatic_updates_disabled() {
		if ( ! class_exists( 'WP_Automatic_Updater' ) ) {
			require_once ABSPATH . 'wp-admin/includes/class-wp-automatic-updater.php';
		}

		$auto_updates = new WP_Automatic_Updater();

		if ( ! $auto_updates->is_disabled() ) {
			return false;
		}

		return array(
			'description' => __( 'All automatic updates are disabled.' ),
			'severity'    => 'fail',
		);
	}

	/**
	 * Checks if automatic updates have tried to run, but failed, previously.
	 *
	 * @since 5.2.0
	 *
	 * @return array|false The test results if auto-updates previously failed, false otherwise.
	 */
	public function test_if_failed_update() {
		$failed = get_site_option( 'auto_core_update_failed' );

		if ( ! $failed ) {
			return false;
		}

		if ( ! empty( $failed['critical'] ) ) {
			$description  = __( 'A previous automatic background update ended with a critical failure, so updates are now disabled.' );
			$description .= ' ' . __( 'You would have received an email because of this.' );
			$description .= ' ' . __( "When you've been able to update using the \"Update now\" button on Dashboard > Updates, this error will be cleared for future update attempts." );
			$description .= ' ' . sprintf(
				/* translators: %s: Code of error shown. */
				__( 'The error code was %s.' ),
				'<code>' . $failed['error_code'] . '</code>'
			);
			return array(
				'description' => $description,
				'severity'    => 'warning',
			);
		}

		$description = __( 'A previous automatic background update could not occur.' );
		if ( empty( $failed['retry'] ) ) {
			$description .= ' ' . __( 'You would have received an email because of this.' );
		}

		$description .= ' ' . __( 'Another attempt will be made with the next release.' );
		$description .= ' ' . sprintf(
			/* translators: %s: Code of error shown. */
			__( 'The error code was %s.' ),
			'<code>' . $failed['error_code'] . '</code>'
		);
		return array(
			'description' => $description,
			'severity'    => 'warning',
		);
	}

	/**
	 * Checks if WordPress is controlled by a VCS (Git, Subversion etc).
	 *
	 * @since 5.2.0
	 *
	 * @return array The test results.
	 */
	public function test_vcs_abspath() {
		$context_dirs = array( ABSPATH );
		$vcs_dirs     = array( '.svn', '.git', '.hg', '.bzr' );
		$check_dirs   = array();

		foreach ( $context_dirs as $context_dir ) {
			// Walk up from $context_dir to the root.
			do {
				$check_dirs[] = $context_dir;

				// Once we've hit '/' or 'C:\', we need to stop. dirname will keep returning the input here.
				if ( dirname( $context_dir ) === $context_dir ) {
					break;
				}

				// Continue one level at a time.
			} while ( $context_dir = dirname( $context_dir ) );
		}

		$check_dirs = array_unique( $check_dirs );
		$updater    = new WP_Automatic_Updater();
		$checkout   = false;

		// Search all directories we've found for evidence of version control.
		foreach ( $vcs_dirs as $vcs_dir ) {
			foreach ( $check_dirs as $check_dir ) {
				if ( ! $updater->is_allowed_dir( $check_dir ) ) {
					continue;
				}

				$checkout = is_dir( rtrim( $check_dir, '\\/' ) . "/$vcs_dir" );
				if ( $checkout ) {
					break 2;
				}
			}
		}

		/** This filter is documented in wp-admin/includes/class-wp-automatic-updater.php */
		if ( $checkout && ! apply_filters( 'automatic_updates_is_vcs_checkout', true, ABSPATH ) ) {
			return array(
				'description' => sprintf(
					/* translators: 1: Folder name. 2: Version control directory. 3: Filter name. */
					__( 'The folder %1$s was detected as being under version control (%2$s), but the %3$s filter is allowing updates.' ),
					'<code>' . $check_dir . '</code>',
					"<code>$vcs_dir</code>",
					'<code>automatic_updates_is_vcs_checkout</code>'
				),
				'severity'    => 'info',
			);
		}

		if ( $checkout ) {
			return array(
				'description' => sprintf(
					/* translators: 1: Folder name. 2: Version control directory. */
					__( 'The folder %1$s was detected as being under version control (%2$s).' ),
					'<code>' . $check_dir . '</code>',
					"<code>$vcs_dir</code>"
				),
				'severity'    => 'warning',
			);
		}

		return array(
			'description' => __( 'No version control systems were detected.' ),
			'severity'    => 'pass',
		);
	}

	/**
	 * Checks if we can access files without providing credentials.
	 *
	 * @since 5.2.0
	 *
	 * @return array The test results.
	 */
	public function test_check_wp_filesystem_method() {
		// Make sure the `request_filesystem_credentials()` function is available during our REST API call.
		if ( ! function_exists( 'request_filesystem_credentials' ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
		}

		$skin    = new Automatic_Upgrader_Skin();
		$success = $skin->request_filesystem_credentials( false, ABSPATH );

		if ( ! $success ) {
			$description  = __( 'Your installation of WordPress prompts for FTP credentials to perform updates.' );
			$description .= ' ' . __( '(Your site is performing updates over FTP due to file ownership. Talk to your hosting company.)' );

			return array(
				'description' => $description,
				'severity'    => 'fail',
			);
		}

		return array(
			'description' => __( 'Your installation of WordPress does not require FTP credentials to perform updates.' ),
			'severity'    => 'pass',
		);
	}

	/**
	 * Checks if core files are writable by the web user/group.
	 *
	 * @since 5.2.0
	 *
	 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
	 *
	 * @return array|false The test results if at least some of WordPress core files are writeable,
	 *                     or if a list of the checksums could not be retrieved from WordPress.org.
	 *                     False if the core files are not writeable.
	 */
	public function test_all_files_writable() {
		global $wp_filesystem;

		require ABSPATH . WPINC . '/version.php'; // $wp_version; // x.y.z

		$skin    = new Automatic_Upgrader_Skin();
		$success = $skin->request_filesystem_credentials( false, ABSPATH );

		if ( ! $success ) {
			return false;
		}

		WP_Filesystem();

		if ( 'direct' !== $wp_filesystem->method ) {
			return false;
		}

		// Make sure the `get_core_checksums()` function is available during our REST API call.
		if ( ! function_exists( 'get_core_checksums' ) ) {
			require_once ABSPATH . 'wp-admin/includes/update.php';
		}

		$checksums = get_core_checksums( $wp_version, 'en_US' );
		$dev       = ( str_contains( $wp_version, '-' ) );
		// Get the last stable version's files and test against that.
		if ( ! $checksums && $dev ) {
			$checksums = get_core_checksums( (float) $wp_version - 0.1, 'en_US' );
		}

		// There aren't always checksums for development releases, so just skip the test if we still can't find any.
		if ( ! $checksums && $dev ) {
			return false;
		}

		if ( ! $checksums ) {
			$description = sprintf(
				/* translators: %s: WordPress version. */
				__( "Couldn't retrieve a list of the checksums for WordPress %s." ),
				$wp_version
			);
			$description .= ' ' . __( 'This could mean that connections are failing to WordPress.org.' );
			return array(
				'description' => $description,
				'severity'    => 'warning',
			);
		}

		$unwritable_files = array();
		foreach ( array_keys( $checksums ) as $file ) {
			if ( str_starts_with( $file, 'wp-content' ) ) {
				continue;
			}
			if ( ! file_exists( ABSPATH . $file ) ) {
				continue;
			}
			if ( ! is_writable( ABSPATH . $file ) ) {
				$unwritable_files[] = $file;
			}
		}

		if ( $unwritable_files ) {
			if ( count( $unwritable_files ) > 20 ) {
				$unwritable_files   = array_slice( $unwritable_files, 0, 20 );
				$unwritable_files[] = '...';
			}
			return array(
				'description' => __( 'Some files are not writable by WordPress:' ) . ' <ul><li>' . implode( '</li><li>', $unwritable_files ) . '</li></ul>',
				'severity'    => 'fail',
			);
		} else {
			return array(
				'description' => __( 'All of your WordPress files are writable.' ),
				'severity'    => 'pass',
			);
		}
	}

	/**
	 * Checks if the install is using a development branch and can use nightly packages.
	 *
	 * @since 5.2.0
	 *
	 * @return array|false|null The test results if development updates are blocked.
	 *                          False if it isn't a development version. Null if the test passed.
	 */
	public function test_accepts_dev_updates() {
		require ABSPATH . WPINC . '/version.php'; // $wp_version; // x.y.z
		// Only for dev versions.
		if ( ! str_contains( $wp_version, '-' ) ) {
			return false;
		}

		if ( defined( 'WP_AUTO_UPDATE_CORE' ) && ( 'minor' === WP_AUTO_UPDATE_CORE || false === WP_AUTO_UPDATE_CORE ) ) {
			return array(
				'description' => sprintf(
					/* translators: %s: Name of the constant used. */
					__( 'WordPress development updates are blocked by the %s constant.' ),
					'<code>WP_AUTO_UPDATE_CORE</code>'
				),
				'severity'    => 'fail',
			);
		}

		/** This filter is documented in wp-admin/includes/class-core-upgrader.php */
		if ( ! apply_filters( 'allow_dev_auto_core_updates', $wp_version ) ) {
			return array(
				'description' => sprintf(
					/* translators: %s: Name of the filter used. */
					__( 'WordPress development updates are blocked by the %s filter.' ),
					'<code>allow_dev_auto_core_updates</code>'
				),
				'severity'    => 'fail',
			);
		}

		return null;
	}

	/**
	 * Checks if the site supports automatic minor updates.
	 *
	 * @since 5.2.0
	 *
	 * @return array|null The test results if minor updates are blocked,
	 *                    or null if the test passed.
	 */
	public function test_accepts_minor_updates() {
		if ( defined( 'WP_AUTO_UPDATE_CORE' ) && false === WP_AUTO_UPDATE_CORE ) {
			return array(
				'description' => sprintf(
					/* translators: %s: Name of the constant used. */
					__( 'WordPress security and maintenance releases are blocked by %s.' ),
					"<code>define( 'WP_AUTO_UPDATE_CORE', false );</code>"
				),
				'severity'    => 'fail',
			);
		}

		/** This filter is documented in wp-admin/includes/class-core-upgrader.php */
		if ( ! apply_filters( 'allow_minor_auto_core_updates', true ) ) {
			return array(
				'description' => sprintf(
					/* translators: %s: Name of the filter used. */
					__( 'WordPress security and maintenance releases are blocked by the %s filter.' ),
					'<code>allow_minor_auto_core_updates</code>'
				),
				'severity'    => 'fail',
			);
		}

		return null;
	}
}
class-wp-plugin-install-list-table.php000060400000060322151327343360014012 0ustar00<?php
/**
 * List Table API: WP_Plugin_Install_List_Table class
 *
 * @package WordPress
 * @subpackage Administration
 * @since 3.1.0
 */

/**
 * Core class used to implement displaying plugins to install in a list table.
 *
 * @since 3.1.0
 *
 * @see WP_List_Table
 */
class WP_Plugin_Install_List_Table extends WP_List_Table {

	public $order   = 'ASC';
	public $orderby = null;
	public $groups  = array();

	private $error;

	/**
	 * @return bool
	 */
	public function ajax_user_can() {
		return current_user_can( 'install_plugins' );
	}

	/**
	 * Returns the list of known plugins.
	 *
	 * Uses the transient data from the updates API to determine the known
	 * installed plugins.
	 *
	 * @since 4.9.0
	 * @access protected
	 *
	 * @return array
	 */
	protected function get_installed_plugins() {
		$plugins = array();

		$plugin_info = get_site_transient( 'update_plugins' );
		if ( isset( $plugin_info->no_update ) ) {
			foreach ( $plugin_info->no_update as $plugin ) {
				if ( isset( $plugin->slug ) ) {
					$plugin->upgrade          = false;
					$plugins[ $plugin->slug ] = $plugin;
				}
			}
		}

		if ( isset( $plugin_info->response ) ) {
			foreach ( $plugin_info->response as $plugin ) {
				if ( isset( $plugin->slug ) ) {
					$plugin->upgrade          = true;
					$plugins[ $plugin->slug ] = $plugin;
				}
			}
		}

		return $plugins;
	}

	/**
	 * Returns a list of slugs of installed plugins, if known.
	 *
	 * Uses the transient data from the updates API to determine the slugs of
	 * known installed plugins. This might be better elsewhere, perhaps even
	 * within get_plugins().
	 *
	 * @since 4.0.0
	 *
	 * @return array
	 */
	protected function get_installed_plugin_slugs() {
		return array_keys( $this->get_installed_plugins() );
	}

	/**
	 * @global array  $tabs
	 * @global string $tab
	 * @global int    $paged
	 * @global string $type
	 * @global string $term
	 */
	public function prepare_items() {
		require_once ABSPATH . 'wp-admin/includes/plugin-install.php';

		global $tabs, $tab, $paged, $type, $term;

		$tab = ! empty( $_REQUEST['tab'] ) ? sanitize_text_field( $_REQUEST['tab'] ) : '';

		$paged = $this->get_pagenum();

		$per_page = 36;

		// These are the tabs which are shown on the page.
		$tabs = array();

		if ( 'search' === $tab ) {
			$tabs['search'] = __( 'Search Results' );
		}

		if ( 'beta' === $tab || str_contains( get_bloginfo( 'version' ), '-' ) ) {
			$tabs['beta'] = _x( 'Beta Testing', 'Plugin Installer' );
		}

		$tabs['featured']    = _x( 'Featured', 'Plugin Installer' );
		$tabs['popular']     = _x( 'Popular', 'Plugin Installer' );
		$tabs['recommended'] = _x( 'Recommended', 'Plugin Installer' );
		$tabs['favorites']   = _x( 'Favorites', 'Plugin Installer' );

		if ( current_user_can( 'upload_plugins' ) ) {
			/*
			 * No longer a real tab. Here for filter compatibility.
			 * Gets skipped in get_views().
			 */
			$tabs['upload'] = __( 'Upload Plugin' );
		}

		$nonmenu_tabs = array( 'plugin-information' ); // Valid actions to perform which do not have a Menu item.

		/**
		 * Filters the tabs shown on the Add Plugins screen.
		 *
		 * @since 2.7.0
		 *
		 * @param string[] $tabs The tabs shown on the Add Plugins screen. Defaults include
		 *                       'featured', 'popular', 'recommended', 'favorites', and 'upload'.
		 */
		$tabs = apply_filters( 'install_plugins_tabs', $tabs );

		/**
		 * Filters tabs not associated with a menu item on the Add Plugins screen.
		 *
		 * @since 2.7.0
		 *
		 * @param string[] $nonmenu_tabs The tabs that don't have a menu item on the Add Plugins screen.
		 */
		$nonmenu_tabs = apply_filters( 'install_plugins_nonmenu_tabs', $nonmenu_tabs );

		// If a non-valid menu tab has been selected, And it's not a non-menu action.
		if ( empty( $tab ) || ( ! isset( $tabs[ $tab ] ) && ! in_array( $tab, (array) $nonmenu_tabs, true ) ) ) {
			$tab = key( $tabs );
		}

		$installed_plugins = $this->get_installed_plugins();

		$args = array(
			'page'     => $paged,
			'per_page' => $per_page,
			// Send the locale to the API so it can provide context-sensitive results.
			'locale'   => get_user_locale(),
		);

		switch ( $tab ) {
			case 'search':
				$type = isset( $_REQUEST['type'] ) ? wp_unslash( $_REQUEST['type'] ) : 'term';
				$term = isset( $_REQUEST['s'] ) ? wp_unslash( $_REQUEST['s'] ) : '';

				switch ( $type ) {
					case 'tag':
						$args['tag'] = sanitize_title_with_dashes( $term );
						break;
					case 'term':
						$args['search'] = $term;
						break;
					case 'author':
						$args['author'] = $term;
						break;
				}

				break;

			case 'featured':
			case 'popular':
			case 'new':
			case 'beta':
				$args['browse'] = $tab;
				break;
			case 'recommended':
				$args['browse'] = $tab;
				// Include the list of installed plugins so we can get relevant results.
				$args['installed_plugins'] = array_keys( $installed_plugins );
				break;

			case 'favorites':
				$action = 'save_wporg_username_' . get_current_user_id();
				if ( isset( $_GET['_wpnonce'] ) && wp_verify_nonce( wp_unslash( $_GET['_wpnonce'] ), $action ) ) {
					$user = isset( $_GET['user'] ) ? wp_unslash( $_GET['user'] ) : get_user_option( 'wporg_favorites' );

					// If the save url parameter is passed with a falsey value, don't save the favorite user.
					if ( ! isset( $_GET['save'] ) || $_GET['save'] ) {
						update_user_meta( get_current_user_id(), 'wporg_favorites', $user );
					}
				} else {
					$user = get_user_option( 'wporg_favorites' );
				}
				if ( $user ) {
					$args['user'] = $user;
				} else {
					$args = false;
				}

				add_action( 'install_plugins_favorites', 'install_plugins_favorites_form', 9, 0 );
				break;

			default:
				$args = false;
				break;
		}

		/**
		 * Filters API request arguments for each Add Plugins screen tab.
		 *
		 * The dynamic portion of the hook name, `$tab`, refers to the plugin install tabs.
		 *
		 * Possible hook names include:
		 *
		 *  - `install_plugins_table_api_args_favorites`
		 *  - `install_plugins_table_api_args_featured`
		 *  - `install_plugins_table_api_args_popular`
		 *  - `install_plugins_table_api_args_recommended`
		 *  - `install_plugins_table_api_args_upload`
		 *  - `install_plugins_table_api_args_search`
		 *  - `install_plugins_table_api_args_beta`
		 *
		 * @since 3.7.0
		 *
		 * @param array|false $args Plugin install API arguments.
		 */
		$args = apply_filters( "install_plugins_table_api_args_{$tab}", $args );

		if ( ! $args ) {
			return;
		}

		$api = plugins_api( 'query_plugins', $args );

		if ( is_wp_error( $api ) ) {
			$this->error = $api;
			return;
		}

		$this->items = $api->plugins;

		if ( $this->orderby ) {
			uasort( $this->items, array( $this, 'order_callback' ) );
		}

		$this->set_pagination_args(
			array(
				'total_items' => $api->info['results'],
				'per_page'    => $args['per_page'],
			)
		);

		if ( isset( $api->info['groups'] ) ) {
			$this->groups = $api->info['groups'];
		}

		if ( $installed_plugins ) {
			$js_plugins = array_fill_keys(
				array( 'all', 'search', 'active', 'inactive', 'recently_activated', 'mustuse', 'dropins' ),
				array()
			);

			$js_plugins['all'] = array_values( wp_list_pluck( $installed_plugins, 'plugin' ) );
			$upgrade_plugins   = wp_filter_object_list( $installed_plugins, array( 'upgrade' => true ), 'and', 'plugin' );

			if ( $upgrade_plugins ) {
				$js_plugins['upgrade'] = array_values( $upgrade_plugins );
			}

			wp_localize_script(
				'updates',
				'_wpUpdatesItemCounts',
				array(
					'plugins' => $js_plugins,
					'totals'  => wp_get_update_data(),
				)
			);
		}
	}

	/**
	 */
	public function no_items() {
		if ( isset( $this->error ) ) {
			$error_message  = '<p>' . $this->error->get_error_message() . '</p>';
			$error_message .= '<p class="hide-if-no-js"><button class="button try-again">' . __( 'Try Again' ) . '</button></p>';
			wp_admin_notice(
				$error_message,
				array(
					'additional_classes' => array( 'inline', 'error' ),
					'paragraph_wrap'     => false,
				)
			);
			?>
		<?php } else { ?>
			<div class="no-plugin-results"><?php _e( 'No plugins found. Try a different search.' ); ?></div>
			<?php
		}
	}

	/**
	 * @global array $tabs
	 * @global string $tab
	 *
	 * @return array
	 */
	protected function get_views() {
		global $tabs, $tab;

		$display_tabs = array();
		foreach ( (array) $tabs as $action => $text ) {
			$display_tabs[ 'plugin-install-' . $action ] = array(
				'url'     => self_admin_url( 'plugin-install.php?tab=' . $action ),
				'label'   => $text,
				'current' => $action === $tab,
			);
		}
		// No longer a real tab.
		unset( $display_tabs['plugin-install-upload'] );

		return $this->get_views_links( $display_tabs );
	}

	/**
	 * Overrides parent views so we can use the filter bar display.
	 */
	public function views() {
		$views = $this->get_views();

		/** This filter is documented in wp-admin/includes/class-wp-list-table.php */
		$views = apply_filters( "views_{$this->screen->id}", $views );

		$this->screen->render_screen_reader_content( 'heading_views' );
		?>
<div class="wp-filter">
	<ul class="filter-links">
		<?php
		if ( ! empty( $views ) ) {
			foreach ( $views as $class => $view ) {
				$views[ $class ] = "\t<li class='$class'>$view";
			}
			echo implode( " </li>\n", $views ) . "</li>\n";
		}
		?>
	</ul>

		<?php install_search_form(); ?>
</div>
		<?php
	}

	/**
	 * Displays the plugin install table.
	 *
	 * Overrides the parent display() method to provide a different container.
	 *
	 * @since 4.0.0
	 */
	public function display() {
		$singular = $this->_args['singular'];

		$data_attr = '';

		if ( $singular ) {
			$data_attr = " data-wp-lists='list:$singular'";
		}

		$this->display_tablenav( 'top' );

		?>
<div class="wp-list-table <?php echo implode( ' ', $this->get_table_classes() ); ?>">
		<?php
		$this->screen->render_screen_reader_content( 'heading_list' );
		?>
	<div id="the-list"<?php echo $data_attr; ?>>
		<?php $this->display_rows_or_placeholder(); ?>
	</div>
</div>
		<?php
		$this->display_tablenav( 'bottom' );
	}

	/**
	 * @global string $tab
	 *
	 * @param string $which
	 */
	protected function display_tablenav( $which ) {
		if ( 'featured' === $GLOBALS['tab'] ) {
			return;
		}

		if ( 'top' === $which ) {
			wp_referer_field();
			?>
			<div class="tablenav top">
				<div class="alignleft actions">
					<?php
					/**
					 * Fires before the Plugin Install table header pagination is displayed.
					 *
					 * @since 2.7.0
					 */
					do_action( 'install_plugins_table_header' );
					?>
				</div>
				<?php $this->pagination( $which ); ?>
				<br class="clear" />
			</div>
		<?php } else { ?>
			<div class="tablenav bottom">
				<?php $this->pagination( $which ); ?>
				<br class="clear" />
			</div>
			<?php
		}
	}

	/**
	 * @return array
	 */
	protected function get_table_classes() {
		return array( 'widefat', $this->_args['plural'] );
	}

	/**
	 * @return string[] Array of column titles keyed by their column name.
	 */
	public function get_columns() {
		return array();
	}

	/**
	 * @param object $plugin_a
	 * @param object $plugin_b
	 * @return int
	 */
	private function order_callback( $plugin_a, $plugin_b ) {
		$orderby = $this->orderby;
		if ( ! isset( $plugin_a->$orderby, $plugin_b->$orderby ) ) {
			return 0;
		}

		$a = $plugin_a->$orderby;
		$b = $plugin_b->$orderby;

		if ( $a === $b ) {
			return 0;
		}

		if ( 'DESC' === $this->order ) {
			return ( $a < $b ) ? 1 : -1;
		} else {
			return ( $a < $b ) ? -1 : 1;
		}
	}

	/**
	 * Generates the list table rows.
	 *
	 * @since 3.1.0
	 */
	public function display_rows() {
		$plugins_allowedtags = array(
			'a'       => array(
				'href'   => array(),
				'title'  => array(),
				'target' => array(),
			),
			'abbr'    => array( 'title' => array() ),
			'acronym' => array( 'title' => array() ),
			'code'    => array(),
			'pre'     => array(),
			'em'      => array(),
			'strong'  => array(),
			'ul'      => array(),
			'ol'      => array(),
			'li'      => array(),
			'p'       => array(),
			'br'      => array(),
		);

		$plugins_group_titles = array(
			'Performance' => _x( 'Performance', 'Plugin installer group title' ),
			'Social'      => _x( 'Social', 'Plugin installer group title' ),
			'Tools'       => _x( 'Tools', 'Plugin installer group title' ),
		);

		$group = null;

		foreach ( (array) $this->items as $plugin ) {
			if ( is_object( $plugin ) ) {
				$plugin = (array) $plugin;
			}

			// Display the group heading if there is one.
			if ( isset( $plugin['group'] ) && $plugin['group'] !== $group ) {
				if ( isset( $this->groups[ $plugin['group'] ] ) ) {
					$group_name = $this->groups[ $plugin['group'] ];
					if ( isset( $plugins_group_titles[ $group_name ] ) ) {
						$group_name = $plugins_group_titles[ $group_name ];
					}
				} else {
					$group_name = $plugin['group'];
				}

				// Starting a new group, close off the divs of the last one.
				if ( ! empty( $group ) ) {
					echo '</div></div>';
				}

				echo '<div class="plugin-group"><h3>' . esc_html( $group_name ) . '</h3>';
				// Needs an extra wrapping div for nth-child selectors to work.
				echo '<div class="plugin-items">';

				$group = $plugin['group'];
			}

			$title = wp_kses( $plugin['name'], $plugins_allowedtags );

			// Remove any HTML from the description.
			$description = strip_tags( $plugin['short_description'] );

			/**
			 * Filters the plugin card description on the Add Plugins screen.
			 *
			 * @since 6.0.0
			 *
			 * @param string $description Plugin card description.
			 * @param array  $plugin      An array of plugin data. See {@see plugins_api()}
			 *                            for the list of possible values.
			 */
			$description = apply_filters( 'plugin_install_description', $description, $plugin );

			$version = wp_kses( $plugin['version'], $plugins_allowedtags );

			$name = strip_tags( $title . ' ' . $version );

			$author = wp_kses( $plugin['author'], $plugins_allowedtags );
			if ( ! empty( $author ) ) {
				/* translators: %s: Plugin author. */
				$author = ' <cite>' . sprintf( __( 'By %s' ), $author ) . '</cite>';
			}

			$requires_php = isset( $plugin['requires_php'] ) ? $plugin['requires_php'] : null;
			$requires_wp  = isset( $plugin['requires'] ) ? $plugin['requires'] : null;

			$compatible_php = is_php_version_compatible( $requires_php );
			$compatible_wp  = is_wp_version_compatible( $requires_wp );
			$tested_wp      = ( empty( $plugin['tested'] ) || version_compare( get_bloginfo( 'version' ), $plugin['tested'], '<=' ) );

			$action_links = array();

			$action_links[] = wp_get_plugin_action_button( $name, $plugin, $compatible_php, $compatible_wp );

			$details_link = self_admin_url(
				'plugin-install.php?tab=plugin-information&amp;plugin=' . $plugin['slug'] .
				'&amp;TB_iframe=true&amp;width=600&amp;height=550'
			);

			$action_links[] = sprintf(
				'<a href="%s" class="thickbox open-plugin-details-modal" aria-label="%s" data-title="%s">%s</a>',
				esc_url( $details_link ),
				/* translators: %s: Plugin name and version. */
				esc_attr( sprintf( __( 'More information about %s' ), $name ) ),
				esc_attr( $name ),
				__( 'More Details' )
			);

			if ( ! empty( $plugin['icons']['svg'] ) ) {
				$plugin_icon_url = $plugin['icons']['svg'];
			} elseif ( ! empty( $plugin['icons']['2x'] ) ) {
				$plugin_icon_url = $plugin['icons']['2x'];
			} elseif ( ! empty( $plugin['icons']['1x'] ) ) {
				$plugin_icon_url = $plugin['icons']['1x'];
			} else {
				$plugin_icon_url = $plugin['icons']['default'];
			}

			/**
			 * Filters the install action links for a plugin.
			 *
			 * @since 2.7.0
			 *
			 * @param string[] $action_links An array of plugin action links.
			 *                               Defaults are links to Details and Install Now.
			 * @param array    $plugin       An array of plugin data. See {@see plugins_api()}
			 *                               for the list of possible values.
			 */
			$action_links = apply_filters( 'plugin_install_action_links', $action_links, $plugin );

			$last_updated_timestamp = strtotime( $plugin['last_updated'] );
			?>
		<div class="plugin-card plugin-card-<?php echo sanitize_html_class( $plugin['slug'] ); ?>">
			<?php
			if ( ! $compatible_php || ! $compatible_wp ) {
				$incompatible_notice_message = '';
				if ( ! $compatible_php && ! $compatible_wp ) {
					$incompatible_notice_message .= __( 'This plugin does not work with your versions of WordPress and PHP.' );
					if ( current_user_can( 'update_core' ) && current_user_can( 'update_php' ) ) {
						$incompatible_notice_message .= sprintf(
							/* translators: 1: URL to WordPress Updates screen, 2: URL to Update PHP page. */
							' ' . __( '<a href="%1$s">Please update WordPress</a>, and then <a href="%2$s">learn more about updating PHP</a>.' ),
							self_admin_url( 'update-core.php' ),
							esc_url( wp_get_update_php_url() )
						);
						$incompatible_notice_message .= wp_update_php_annotation( '</p><p><em>', '</em>', false );
					} elseif ( current_user_can( 'update_core' ) ) {
						$incompatible_notice_message .= sprintf(
							/* translators: %s: URL to WordPress Updates screen. */
							' ' . __( '<a href="%s">Please update WordPress</a>.' ),
							self_admin_url( 'update-core.php' )
						);
					} elseif ( current_user_can( 'update_php' ) ) {
						$incompatible_notice_message .= sprintf(
							/* translators: %s: URL to Update PHP page. */
							' ' . __( '<a href="%s">Learn more about updating PHP</a>.' ),
							esc_url( wp_get_update_php_url() )
						);
						$incompatible_notice_message .= wp_update_php_annotation( '</p><p><em>', '</em>', false );
					}
				} elseif ( ! $compatible_wp ) {
					$incompatible_notice_message .= __( 'This plugin does not work with your version of WordPress.' );
					if ( current_user_can( 'update_core' ) ) {
						$incompatible_notice_message .= sprintf(
							/* translators: %s: URL to WordPress Updates screen. */
							' ' . __( '<a href="%s">Please update WordPress</a>.' ),
							self_admin_url( 'update-core.php' )
						);
					}
				} elseif ( ! $compatible_php ) {
					$incompatible_notice_message .= __( 'This plugin does not work with your version of PHP.' );
					if ( current_user_can( 'update_php' ) ) {
						$incompatible_notice_message .= sprintf(
							/* translators: %s: URL to Update PHP page. */
							' ' . __( '<a href="%s">Learn more about updating PHP</a>.' ),
							esc_url( wp_get_update_php_url() )
						);
						$incompatible_notice_message .= wp_update_php_annotation( '</p><p><em>', '</em>', false );
					}
				}

				wp_admin_notice(
					$incompatible_notice_message,
					array(
						'type'               => 'error',
						'additional_classes' => array( 'notice-alt', 'inline' ),
					)
				);
			}
			?>
			<div class="plugin-card-top">
				<div class="name column-name">
					<h3>
						<a href="<?php echo esc_url( $details_link ); ?>" class="thickbox open-plugin-details-modal">
						<?php echo $title; ?>
						<img src="<?php echo esc_url( $plugin_icon_url ); ?>" class="plugin-icon" alt="" />
						</a>
					</h3>
				</div>
				<div class="action-links">
					<?php
					if ( $action_links ) {
						echo '<ul class="plugin-action-buttons"><li>' . implode( '</li><li>', $action_links ) . '</li></ul>';
					}
					?>
				</div>
				<div class="desc column-description">
					<p><?php echo $description; ?></p>
					<p class="authors"><?php echo $author; ?></p>
				</div>
			</div>
			<?php
			$dependencies_notice = $this->get_dependencies_notice( $plugin );
			if ( ! empty( $dependencies_notice ) ) {
				echo $dependencies_notice;
			}
			?>
			<div class="plugin-card-bottom">
				<div class="vers column-rating">
					<?php
					wp_star_rating(
						array(
							'rating' => $plugin['rating'],
							'type'   => 'percent',
							'number' => $plugin['num_ratings'],
						)
					);
					?>
					<span class="num-ratings" aria-hidden="true">(<?php echo number_format_i18n( $plugin['num_ratings'] ); ?>)</span>
				</div>
				<div class="column-updated">
					<strong><?php _e( 'Last Updated:' ); ?></strong>
					<?php
						/* translators: %s: Human-readable time difference. */
						printf( __( '%s ago' ), human_time_diff( $last_updated_timestamp ) );
					?>
				</div>
				<div class="column-downloaded">
					<?php
					if ( $plugin['active_installs'] >= 1000000 ) {
						$active_installs_millions = floor( $plugin['active_installs'] / 1000000 );
						$active_installs_text     = sprintf(
							/* translators: %s: Number of millions. */
							_nx( '%s+ Million', '%s+ Million', $active_installs_millions, 'Active plugin installations' ),
							number_format_i18n( $active_installs_millions )
						);
					} elseif ( 0 === $plugin['active_installs'] ) {
						$active_installs_text = _x( 'Less Than 10', 'Active plugin installations' );
					} else {
						$active_installs_text = number_format_i18n( $plugin['active_installs'] ) . '+';
					}
					/* translators: %s: Number of installations. */
					printf( __( '%s Active Installations' ), $active_installs_text );
					?>
				</div>
				<div class="column-compatibility">
					<?php
					if ( ! $tested_wp ) {
						echo '<span class="compatibility-untested">' . __( 'Untested with your version of WordPress' ) . '</span>';
					} elseif ( ! $compatible_wp ) {
						echo '<span class="compatibility-incompatible">' . __( '<strong>Incompatible</strong> with your version of WordPress' ) . '</span>';
					} else {
						echo '<span class="compatibility-compatible">' . __( '<strong>Compatible</strong> with your version of WordPress' ) . '</span>';
					}
					?>
				</div>
			</div>
		</div>
			<?php
		}

		// Close off the group divs of the last one.
		if ( ! empty( $group ) ) {
			echo '</div></div>';
		}
	}

	/**
	 * Returns a notice containing a list of dependencies required by the plugin.
	 *
	 * @since 6.5.0
	 *
	 * @param array  $plugin_data An array of plugin data. See {@see plugins_api()}
	 *                            for the list of possible values.
	 * @return string A notice containing a list of dependencies required by the plugin,
	 *                or an empty string if none is required.
	 */
	protected function get_dependencies_notice( $plugin_data ) {
		if ( empty( $plugin_data['requires_plugins'] ) ) {
			return '';
		}

		$no_name_markup  = '<div class="plugin-dependency"><span class="plugin-dependency-name">%s</span></div>';
		$has_name_markup = '<div class="plugin-dependency"><span class="plugin-dependency-name">%s</span> %s</div>';

		$dependencies_list = '';
		foreach ( $plugin_data['requires_plugins'] as $dependency ) {
			$dependency_data = WP_Plugin_Dependencies::get_dependency_data( $dependency );

			if (
				false !== $dependency_data &&
				! empty( $dependency_data['name'] ) &&
				! empty( $dependency_data['slug'] ) &&
				! empty( $dependency_data['version'] )
			) {
				$more_details_link  = $this->get_more_details_link( $dependency_data['name'], $dependency_data['slug'] );
				$dependencies_list .= sprintf( $has_name_markup, esc_html( $dependency_data['name'] ), $more_details_link );
				continue;
			}

			$result = plugins_api( 'plugin_information', array( 'slug' => $dependency ) );

			if ( ! empty( $result->name ) ) {
				$more_details_link  = $this->get_more_details_link( $result->name, $result->slug );
				$dependencies_list .= sprintf( $has_name_markup, esc_html( $result->name ), $more_details_link );
				continue;
			}

			$dependencies_list .= sprintf( $no_name_markup, esc_html( $dependency ) );
		}

		$dependencies_notice = sprintf(
			'<div class="plugin-dependencies notice notice-alt notice-info inline"><p class="plugin-dependencies-explainer-text">%s</p> %s</div>',
			'<strong>' . __( 'Additional plugins are required' ) . '</strong>',
			$dependencies_list
		);

		return $dependencies_notice;
	}

	/**
	 * Creates a 'More details' link for the plugin.
	 *
	 * @since 6.5.0
	 *
	 * @param string $name The plugin's name.
	 * @param string $slug The plugin's slug.
	 * @return string The 'More details' link for the plugin.
	 */
	protected function get_more_details_link( $name, $slug ) {
		$url = add_query_arg(
			array(
				'tab'       => 'plugin-information',
				'plugin'    => $slug,
				'TB_iframe' => 'true',
				'width'     => '600',
				'height'    => '550',
			),
			network_admin_url( 'plugin-install.php' )
		);

		$more_details_link = sprintf(
			'<a href="%1$s" class="more-details-link thickbox open-plugin-details-modal" aria-label="%2$s" data-title="%3$s">%4$s</a>',
			esc_url( $url ),
			/* translators: %s: Plugin name. */
			sprintf( __( 'More information about %s' ), esc_html( $name ) ),
			esc_attr( $name ),
			__( 'More Details' )
		);

		return $more_details_link;
	}
}
continents-cities.php000060400000050074151327343360010730 0ustar00<?php
/**
 * Translation API: Continent and city translations for timezone selection
 *
 * This file is not included anywhere. It exists solely for use by xgettext.
 *
 * @package WordPress
 * @subpackage i18n
 * @since 2.8.0
 */

__( 'Africa', 'continents-cities' );
__( 'Abidjan', 'continents-cities' );
__( 'Accra', 'continents-cities' );
__( 'Addis Ababa', 'continents-cities' );
__( 'Algiers', 'continents-cities' );
__( 'Asmara', 'continents-cities' );
__( 'Asmera', 'continents-cities' );
__( 'Bamako', 'continents-cities' );
__( 'Bangui', 'continents-cities' );
__( 'Banjul', 'continents-cities' );
__( 'Bissau', 'continents-cities' );
__( 'Blantyre', 'continents-cities' );
__( 'Brazzaville', 'continents-cities' );
__( 'Bujumbura', 'continents-cities' );
__( 'Cairo', 'continents-cities' );
__( 'Casablanca', 'continents-cities' );
__( 'Ceuta', 'continents-cities' );
__( 'Conakry', 'continents-cities' );
__( 'Dakar', 'continents-cities' );
__( 'Dar es Salaam', 'continents-cities' );
__( 'Djibouti', 'continents-cities' );
__( 'Douala', 'continents-cities' );
__( 'El Aaiun', 'continents-cities' );
__( 'Freetown', 'continents-cities' );
__( 'Gaborone', 'continents-cities' );
__( 'Harare', 'continents-cities' );
__( 'Johannesburg', 'continents-cities' );
__( 'Juba', 'continents-cities' );
__( 'Kampala', 'continents-cities' );
__( 'Khartoum', 'continents-cities' );
__( 'Kigali', 'continents-cities' );
__( 'Kinshasa', 'continents-cities' );
__( 'Lagos', 'continents-cities' );
__( 'Libreville', 'continents-cities' );
__( 'Lome', 'continents-cities' );
__( 'Luanda', 'continents-cities' );
__( 'Lubumbashi', 'continents-cities' );
__( 'Lusaka', 'continents-cities' );
__( 'Malabo', 'continents-cities' );
__( 'Maputo', 'continents-cities' );
__( 'Maseru', 'continents-cities' );
__( 'Mbabane', 'continents-cities' );
__( 'Mogadishu', 'continents-cities' );
__( 'Monrovia', 'continents-cities' );
__( 'Nairobi', 'continents-cities' );
__( 'Ndjamena', 'continents-cities' );
__( 'Niamey', 'continents-cities' );
__( 'Nouakchott', 'continents-cities' );
__( 'Ouagadougou', 'continents-cities' );
__( 'Porto-Novo', 'continents-cities' );
__( 'Sao Tome', 'continents-cities' );
__( 'Timbuktu', 'continents-cities' );
__( 'Tripoli', 'continents-cities' );
__( 'Tunis', 'continents-cities' );
__( 'Windhoek', 'continents-cities' );

__( 'America', 'continents-cities' );
__( 'Adak', 'continents-cities' );
__( 'Anchorage', 'continents-cities' );
__( 'Anguilla', 'continents-cities' );
__( 'Antigua', 'continents-cities' );
__( 'Araguaina', 'continents-cities' );
__( 'Argentina', 'continents-cities' );
__( 'Buenos Aires', 'continents-cities' );
__( 'Catamarca', 'continents-cities' );
__( 'ComodRivadavia', 'continents-cities' );
__( 'Cordoba', 'continents-cities' );
__( 'Jujuy', 'continents-cities' );
__( 'La Rioja', 'continents-cities' );
__( 'Mendoza', 'continents-cities' );
__( 'Rio Gallegos', 'continents-cities' );
__( 'Salta', 'continents-cities' );
__( 'San Juan', 'continents-cities' );
__( 'San Luis', 'continents-cities' );
__( 'Tucuman', 'continents-cities' );
__( 'Ushuaia', 'continents-cities' );
__( 'Aruba', 'continents-cities' );
__( 'Asuncion', 'continents-cities' );
__( 'Atikokan', 'continents-cities' );
__( 'Atka', 'continents-cities' );
__( 'Bahia', 'continents-cities' );
__( 'Bahia Banderas', 'continents-cities' );
__( 'Barbados', 'continents-cities' );
__( 'Belem', 'continents-cities' );
__( 'Belize', 'continents-cities' );
__( 'Blanc-Sablon', 'continents-cities' );
__( 'Boa Vista', 'continents-cities' );
__( 'Bogota', 'continents-cities' );
__( 'Boise', 'continents-cities' );
__( 'Cambridge Bay', 'continents-cities' );
__( 'Campo Grande', 'continents-cities' );
__( 'Cancun', 'continents-cities' );
__( 'Caracas', 'continents-cities' );
__( 'Cayenne', 'continents-cities' );
__( 'Cayman', 'continents-cities' );
__( 'Chicago', 'continents-cities' );
__( 'Chihuahua', 'continents-cities' );
__( 'Coral Harbour', 'continents-cities' );
__( 'Costa Rica', 'continents-cities' );
__( 'Creston', 'continents-cities' );
__( 'Cuiaba', 'continents-cities' );
__( 'Curacao', 'continents-cities' );
__( 'Danmarkshavn', 'continents-cities' );
__( 'Dawson', 'continents-cities' );
__( 'Dawson Creek', 'continents-cities' );
__( 'Denver', 'continents-cities' );
__( 'Detroit', 'continents-cities' );
__( 'Dominica', 'continents-cities' );
__( 'Edmonton', 'continents-cities' );
__( 'Eirunepe', 'continents-cities' );
__( 'El Salvador', 'continents-cities' );
__( 'Ensenada', 'continents-cities' );
__( 'Fort Nelson', 'continents-cities' );
__( 'Fort Wayne', 'continents-cities' );
__( 'Fortaleza', 'continents-cities' );
__( 'Glace Bay', 'continents-cities' );
__( 'Godthab', 'continents-cities' );
__( 'Goose Bay', 'continents-cities' );
__( 'Grand Turk', 'continents-cities' );
__( 'Grenada', 'continents-cities' );
__( 'Guadeloupe', 'continents-cities' );
__( 'Guatemala', 'continents-cities' );
__( 'Guayaquil', 'continents-cities' );
__( 'Guyana', 'continents-cities' );
__( 'Halifax', 'continents-cities' );
__( 'Havana', 'continents-cities' );
__( 'Hermosillo', 'continents-cities' );
__( 'Indiana', 'continents-cities' );
__( 'Indianapolis', 'continents-cities' );
__( 'Knox', 'continents-cities' );
__( 'Marengo', 'continents-cities' );
__( 'Petersburg', 'continents-cities' );
__( 'Tell City', 'continents-cities' );
__( 'Vevay', 'continents-cities' );
__( 'Vincennes', 'continents-cities' );
__( 'Winamac', 'continents-cities' );
__( 'Inuvik', 'continents-cities' );
__( 'Iqaluit', 'continents-cities' );
__( 'Jamaica', 'continents-cities' );
__( 'Juneau', 'continents-cities' );
__( 'Kentucky', 'continents-cities' );
__( 'Louisville', 'continents-cities' );
__( 'Monticello', 'continents-cities' );
__( 'Knox IN', 'continents-cities' );
__( 'Kralendijk', 'continents-cities' );
__( 'La Paz', 'continents-cities' );
__( 'Lima', 'continents-cities' );
__( 'Los Angeles', 'continents-cities' );
__( 'Lower Princes', 'continents-cities' );
__( 'Maceio', 'continents-cities' );
__( 'Managua', 'continents-cities' );
__( 'Manaus', 'continents-cities' );
__( 'Marigot', 'continents-cities' );
__( 'Martinique', 'continents-cities' );
__( 'Matamoros', 'continents-cities' );
__( 'Mazatlan', 'continents-cities' );
__( 'Menominee', 'continents-cities' );
__( 'Merida', 'continents-cities' );
__( 'Metlakatla', 'continents-cities' );
__( 'Mexico City', 'continents-cities' );
__( 'Miquelon', 'continents-cities' );
__( 'Moncton', 'continents-cities' );
__( 'Monterrey', 'continents-cities' );
__( 'Montevideo', 'continents-cities' );
__( 'Montreal', 'continents-cities' );
__( 'Montserrat', 'continents-cities' );
__( 'Nassau', 'continents-cities' );
__( 'New York', 'continents-cities' );
__( 'Nipigon', 'continents-cities' );
__( 'Nome', 'continents-cities' );
__( 'Noronha', 'continents-cities' );
__( 'North Dakota', 'continents-cities' );
__( 'Beulah', 'continents-cities' );
__( 'Center', 'continents-cities' );
__( 'New Salem', 'continents-cities' );
__( 'Nuuk', 'continents-cities' );
__( 'Ojinaga', 'continents-cities' );
__( 'Panama', 'continents-cities' );
__( 'Pangnirtung', 'continents-cities' );
__( 'Paramaribo', 'continents-cities' );
__( 'Phoenix', 'continents-cities' );
__( 'Port-au-Prince', 'continents-cities' );
__( 'Port of Spain', 'continents-cities' );
__( 'Porto Acre', 'continents-cities' );
__( 'Porto Velho', 'continents-cities' );
__( 'Puerto Rico', 'continents-cities' );
__( 'Punta Arenas', 'continents-cities' );
__( 'Rainy River', 'continents-cities' );
__( 'Rankin Inlet', 'continents-cities' );
__( 'Recife', 'continents-cities' );
__( 'Regina', 'continents-cities' );
__( 'Resolute', 'continents-cities' );
__( 'Rio Branco', 'continents-cities' );
__( 'Rosario', 'continents-cities' );
__( 'Santa Isabel', 'continents-cities' );
__( 'Santarem', 'continents-cities' );
__( 'Santiago', 'continents-cities' );
__( 'Santo Domingo', 'continents-cities' );
__( 'Sao Paulo', 'continents-cities' );
__( 'Scoresbysund', 'continents-cities' );
__( 'Shiprock', 'continents-cities' );
__( 'Sitka', 'continents-cities' );
__( 'St Barthelemy', 'continents-cities' );
__( 'St Johns', 'continents-cities' );
__( 'St Kitts', 'continents-cities' );
__( 'St Lucia', 'continents-cities' );
__( 'St Thomas', 'continents-cities' );
__( 'St Vincent', 'continents-cities' );
__( 'Swift Current', 'continents-cities' );
__( 'Tegucigalpa', 'continents-cities' );
__( 'Thule', 'continents-cities' );
__( 'Thunder Bay', 'continents-cities' );
__( 'Tijuana', 'continents-cities' );
__( 'Toronto', 'continents-cities' );
__( 'Tortola', 'continents-cities' );
__( 'Vancouver', 'continents-cities' );
__( 'Virgin', 'continents-cities' );
__( 'Whitehorse', 'continents-cities' );
__( 'Winnipeg', 'continents-cities' );
__( 'Yakutat', 'continents-cities' );
__( 'Yellowknife', 'continents-cities' );

__( 'Antarctica', 'continents-cities' );
__( 'Casey', 'continents-cities' );
__( 'Davis', 'continents-cities' );
__( 'DumontDUrville', 'continents-cities' );
__( 'Macquarie', 'continents-cities' );
__( 'Mawson', 'continents-cities' );
__( 'McMurdo', 'continents-cities' );
__( 'Palmer', 'continents-cities' );
__( 'Rothera', 'continents-cities' );
__( 'South Pole', 'continents-cities' );
__( 'Syowa', 'continents-cities' );
__( 'Troll', 'continents-cities' );
__( 'Vostok', 'continents-cities' );

__( 'Arctic', 'continents-cities' );
__( 'Longyearbyen', 'continents-cities' );

__( 'Asia', 'continents-cities' );
__( 'Aden', 'continents-cities' );
__( 'Almaty', 'continents-cities' );
__( 'Amman', 'continents-cities' );
__( 'Anadyr', 'continents-cities' );
__( 'Aqtau', 'continents-cities' );
__( 'Aqtobe', 'continents-cities' );
__( 'Ashgabat', 'continents-cities' );
__( 'Ashkhabad', 'continents-cities' );
__( 'Atyrau', 'continents-cities' );
__( 'Baghdad', 'continents-cities' );
__( 'Bahrain', 'continents-cities' );
__( 'Baku', 'continents-cities' );
__( 'Bangkok', 'continents-cities' );
__( 'Barnaul', 'continents-cities' );
__( 'Beirut', 'continents-cities' );
__( 'Bishkek', 'continents-cities' );
__( 'Brunei', 'continents-cities' );
__( 'Calcutta', 'continents-cities' );
__( 'Chita', 'continents-cities' );
__( 'Choibalsan', 'continents-cities' );
__( 'Chongqing', 'continents-cities' );
__( 'Chungking', 'continents-cities' );
__( 'Colombo', 'continents-cities' );
__( 'Dacca', 'continents-cities' );
__( 'Damascus', 'continents-cities' );
__( 'Dhaka', 'continents-cities' );
__( 'Dili', 'continents-cities' );
__( 'Dubai', 'continents-cities' );
__( 'Dushanbe', 'continents-cities' );
__( 'Famagusta', 'continents-cities' );
__( 'Gaza', 'continents-cities' );
__( 'Harbin', 'continents-cities' );
__( 'Hebron', 'continents-cities' );
__( 'Ho Chi Minh', 'continents-cities' );
__( 'Hong Kong', 'continents-cities' );
__( 'Hovd', 'continents-cities' );
__( 'Irkutsk', 'continents-cities' );
__( 'Jakarta', 'continents-cities' );
__( 'Jayapura', 'continents-cities' );
__( 'Jerusalem', 'continents-cities' );
__( 'Kabul', 'continents-cities' );
__( 'Kamchatka', 'continents-cities' );
__( 'Karachi', 'continents-cities' );
__( 'Kashgar', 'continents-cities' );
__( 'Kathmandu', 'continents-cities' );
__( 'Katmandu', 'continents-cities' );
__( 'Khandyga', 'continents-cities' );
__( 'Kolkata', 'continents-cities' );
__( 'Krasnoyarsk', 'continents-cities' );
__( 'Kuala Lumpur', 'continents-cities' );
__( 'Kuching', 'continents-cities' );
__( 'Kuwait', 'continents-cities' );
__( 'Macao', 'continents-cities' );
__( 'Macau', 'continents-cities' );
__( 'Magadan', 'continents-cities' );
__( 'Makassar', 'continents-cities' );
__( 'Manila', 'continents-cities' );
__( 'Muscat', 'continents-cities' );
__( 'Nicosia', 'continents-cities' );
__( 'Novokuznetsk', 'continents-cities' );
__( 'Novosibirsk', 'continents-cities' );
__( 'Omsk', 'continents-cities' );
__( 'Oral', 'continents-cities' );
__( 'Phnom Penh', 'continents-cities' );
__( 'Pontianak', 'continents-cities' );
__( 'Pyongyang', 'continents-cities' );
__( 'Qatar', 'continents-cities' );
__( 'Qostanay', 'continents-cities' );
__( 'Qyzylorda', 'continents-cities' );
__( 'Rangoon', 'continents-cities' );
__( 'Riyadh', 'continents-cities' );
__( 'Saigon', 'continents-cities' );
__( 'Sakhalin', 'continents-cities' );
__( 'Samarkand', 'continents-cities' );
__( 'Seoul', 'continents-cities' );
__( 'Shanghai', 'continents-cities' );
__( 'Singapore', 'continents-cities' );
__( 'Srednekolymsk', 'continents-cities' );
__( 'Taipei', 'continents-cities' );
__( 'Tashkent', 'continents-cities' );
__( 'Tbilisi', 'continents-cities' );
__( 'Tehran', 'continents-cities' );
__( 'Tel Aviv', 'continents-cities' );
__( 'Thimbu', 'continents-cities' );
__( 'Thimphu', 'continents-cities' );
__( 'Tokyo', 'continents-cities' );
__( 'Tomsk', 'continents-cities' );
__( 'Ujung Pandang', 'continents-cities' );
__( 'Ulaanbaatar', 'continents-cities' );
__( 'Ulan Bator', 'continents-cities' );
__( 'Urumqi', 'continents-cities' );
__( 'Ust-Nera', 'continents-cities' );
__( 'Vientiane', 'continents-cities' );
__( 'Vladivostok', 'continents-cities' );
__( 'Yakutsk', 'continents-cities' );
__( 'Yangon', 'continents-cities' );
__( 'Yekaterinburg', 'continents-cities' );
__( 'Yerevan', 'continents-cities' );

__( 'Atlantic', 'continents-cities' );
__( 'Azores', 'continents-cities' );
__( 'Bermuda', 'continents-cities' );
__( 'Canary', 'continents-cities' );
__( 'Cape Verde', 'continents-cities' );
__( 'Faeroe', 'continents-cities' );
__( 'Faroe', 'continents-cities' );
__( 'Jan Mayen', 'continents-cities' );
__( 'Madeira', 'continents-cities' );
__( 'Reykjavik', 'continents-cities' );
__( 'South Georgia', 'continents-cities' );
__( 'St Helena', 'continents-cities' );
__( 'Stanley', 'continents-cities' );

__( 'Australia', 'continents-cities' );
__( 'ACT', 'continents-cities' );
__( 'Adelaide', 'continents-cities' );
__( 'Brisbane', 'continents-cities' );
__( 'Broken Hill', 'continents-cities' );
__( 'Canberra', 'continents-cities' );
__( 'Currie', 'continents-cities' );
__( 'Darwin', 'continents-cities' );
__( 'Eucla', 'continents-cities' );
__( 'Hobart', 'continents-cities' );
__( 'LHI', 'continents-cities' );
__( 'Lindeman', 'continents-cities' );
__( 'Lord Howe', 'continents-cities' );
__( 'Melbourne', 'continents-cities' );
__( 'NSW', 'continents-cities' );
__( 'North', 'continents-cities' );
__( 'Perth', 'continents-cities' );
__( 'Queensland', 'continents-cities' );
__( 'South', 'continents-cities' );
__( 'Sydney', 'continents-cities' );
__( 'Tasmania', 'continents-cities' );
__( 'Victoria', 'continents-cities' );
__( 'West', 'continents-cities' );
__( 'Yancowinna', 'continents-cities' );

__( 'Etc', 'continents-cities' );
__( 'GMT', 'continents-cities' );
__( 'GMT+0', 'continents-cities' );
__( 'GMT+1', 'continents-cities' );
__( 'GMT+10', 'continents-cities' );
__( 'GMT+11', 'continents-cities' );
__( 'GMT+12', 'continents-cities' );
__( 'GMT+2', 'continents-cities' );
__( 'GMT+3', 'continents-cities' );
__( 'GMT+4', 'continents-cities' );
__( 'GMT+5', 'continents-cities' );
__( 'GMT+6', 'continents-cities' );
__( 'GMT+7', 'continents-cities' );
__( 'GMT+8', 'continents-cities' );
__( 'GMT+9', 'continents-cities' );
__( 'GMT-0', 'continents-cities' );
__( 'GMT-1', 'continents-cities' );
__( 'GMT-10', 'continents-cities' );
__( 'GMT-11', 'continents-cities' );
__( 'GMT-12', 'continents-cities' );
__( 'GMT-13', 'continents-cities' );
__( 'GMT-14', 'continents-cities' );
__( 'GMT-2', 'continents-cities' );
__( 'GMT-3', 'continents-cities' );
__( 'GMT-4', 'continents-cities' );
__( 'GMT-5', 'continents-cities' );
__( 'GMT-6', 'continents-cities' );
__( 'GMT-7', 'continents-cities' );
__( 'GMT-8', 'continents-cities' );
__( 'GMT-9', 'continents-cities' );
__( 'GMT0', 'continents-cities' );
__( 'Greenwich', 'continents-cities' );
__( 'UCT', 'continents-cities' );
__( 'UTC', 'continents-cities' );
__( 'Universal', 'continents-cities' );
__( 'Zulu', 'continents-cities' );

__( 'Europe', 'continents-cities' );
__( 'Amsterdam', 'continents-cities' );
__( 'Andorra', 'continents-cities' );
__( 'Astrakhan', 'continents-cities' );
__( 'Athens', 'continents-cities' );
__( 'Belfast', 'continents-cities' );
__( 'Belgrade', 'continents-cities' );
__( 'Berlin', 'continents-cities' );
__( 'Bratislava', 'continents-cities' );
__( 'Brussels', 'continents-cities' );
__( 'Bucharest', 'continents-cities' );
__( 'Budapest', 'continents-cities' );
__( 'Busingen', 'continents-cities' );
__( 'Chisinau', 'continents-cities' );
__( 'Copenhagen', 'continents-cities' );
__( 'Dublin', 'continents-cities' );
__( 'Gibraltar', 'continents-cities' );
__( 'Guernsey', 'continents-cities' );
__( 'Helsinki', 'continents-cities' );
__( 'Isle of Man', 'continents-cities' );
__( 'Istanbul', 'continents-cities' );
__( 'Jersey', 'continents-cities' );
__( 'Kaliningrad', 'continents-cities' );
__( 'Kiev', 'continents-cities' );
__( 'Kyiv', 'continents-cities' );
__( 'Kirov', 'continents-cities' );
__( 'Lisbon', 'continents-cities' );
__( 'Ljubljana', 'continents-cities' );
__( 'London', 'continents-cities' );
__( 'Luxembourg', 'continents-cities' );
__( 'Madrid', 'continents-cities' );
__( 'Malta', 'continents-cities' );
__( 'Mariehamn', 'continents-cities' );
__( 'Minsk', 'continents-cities' );
__( 'Monaco', 'continents-cities' );
__( 'Moscow', 'continents-cities' );
__( 'Oslo', 'continents-cities' );
__( 'Paris', 'continents-cities' );
__( 'Podgorica', 'continents-cities' );
__( 'Prague', 'continents-cities' );
__( 'Riga', 'continents-cities' );
__( 'Rome', 'continents-cities' );
__( 'Samara', 'continents-cities' );
__( 'San Marino', 'continents-cities' );
__( 'Sarajevo', 'continents-cities' );
__( 'Saratov', 'continents-cities' );
__( 'Simferopol', 'continents-cities' );
__( 'Skopje', 'continents-cities' );
__( 'Sofia', 'continents-cities' );
__( 'Stockholm', 'continents-cities' );
__( 'Tallinn', 'continents-cities' );
__( 'Tirane', 'continents-cities' );
__( 'Tiraspol', 'continents-cities' );
__( 'Ulyanovsk', 'continents-cities' );
__( 'Uzhgorod', 'continents-cities' );
__( 'Vaduz', 'continents-cities' );
__( 'Vatican', 'continents-cities' );
__( 'Vienna', 'continents-cities' );
__( 'Vilnius', 'continents-cities' );
__( 'Volgograd', 'continents-cities' );
__( 'Warsaw', 'continents-cities' );
__( 'Zagreb', 'continents-cities' );
__( 'Zaporozhye', 'continents-cities' );
__( 'Zurich', 'continents-cities' );

__( 'Indian', 'continents-cities' );
__( 'Antananarivo', 'continents-cities' );
__( 'Chagos', 'continents-cities' );
__( 'Christmas', 'continents-cities' );
__( 'Cocos', 'continents-cities' );
__( 'Comoro', 'continents-cities' );
__( 'Kerguelen', 'continents-cities' );
__( 'Mahe', 'continents-cities' );
__( 'Maldives', 'continents-cities' );
__( 'Mauritius', 'continents-cities' );
__( 'Mayotte', 'continents-cities' );
__( 'Reunion', 'continents-cities' );

__( 'Pacific', 'continents-cities' );
__( 'Apia', 'continents-cities' );
__( 'Auckland', 'continents-cities' );
__( 'Bougainville', 'continents-cities' );
__( 'Chatham', 'continents-cities' );
__( 'Chuuk', 'continents-cities' );
__( 'Easter', 'continents-cities' );
__( 'Efate', 'continents-cities' );
__( 'Enderbury', 'continents-cities' );
__( 'Fakaofo', 'continents-cities' );
__( 'Fiji', 'continents-cities' );
__( 'Funafuti', 'continents-cities' );
__( 'Galapagos', 'continents-cities' );
__( 'Gambier', 'continents-cities' );
__( 'Guadalcanal', 'continents-cities' );
__( 'Guam', 'continents-cities' );
__( 'Honolulu', 'continents-cities' );
__( 'Johnston', 'continents-cities' );
__( 'Kanton', 'continents-cities' );
__( 'Kiritimati', 'continents-cities' );
__( 'Kosrae', 'continents-cities' );
__( 'Kwajalein', 'continents-cities' );
__( 'Majuro', 'continents-cities' );
__( 'Marquesas', 'continents-cities' );
__( 'Midway', 'continents-cities' );
__( 'Nauru', 'continents-cities' );
__( 'Niue', 'continents-cities' );
__( 'Norfolk', 'continents-cities' );
__( 'Noumea', 'continents-cities' );
__( 'Pago Pago', 'continents-cities' );
__( 'Palau', 'continents-cities' );
__( 'Pitcairn', 'continents-cities' );
__( 'Pohnpei', 'continents-cities' );
__( 'Ponape', 'continents-cities' );
__( 'Port Moresby', 'continents-cities' );
__( 'Rarotonga', 'continents-cities' );
__( 'Saipan', 'continents-cities' );
__( 'Samoa', 'continents-cities' );
__( 'Tahiti', 'continents-cities' );
__( 'Tarawa', 'continents-cities' );
__( 'Tongatapu', 'continents-cities' );
__( 'Truk', 'continents-cities' );
__( 'Wake', 'continents-cities' );
__( 'Wallis', 'continents-cities' );
__( 'Yap', 'continents-cities' );
class-wp-importer.php000064400000016513151327343360010662 0ustar00<?php
/**
 * WP_Importer base class
 */
#[AllowDynamicProperties]
class WP_Importer {
	/**
	 * Class Constructor
	 */
	public function __construct() {}

	/**
	 * Returns array with imported permalinks from WordPress database.
	 *
	 * @global wpdb $wpdb WordPress database abstraction object.
	 *
	 * @param string $importer_name
	 * @param string $blog_id
	 * @return array
	 */
	public function get_imported_posts( $importer_name, $blog_id ) {
		global $wpdb;

		$hashtable = array();

		$limit  = 100;
		$offset = 0;

		// Grab all posts in chunks.
		do {
			$meta_key = $importer_name . '_' . $blog_id . '_permalink';
			$sql      = $wpdb->prepare( "SELECT post_id, meta_value FROM $wpdb->postmeta WHERE meta_key = %s LIMIT %d,%d", $meta_key, $offset, $limit );
			$results  = $wpdb->get_results( $sql );

			// Increment offset.
			$offset = ( $limit + $offset );

			if ( ! empty( $results ) ) {
				foreach ( $results as $r ) {
					// Set permalinks into array.
					$hashtable[ $r->meta_value ] = (int) $r->post_id;
				}
			}
		} while ( count( $results ) === $limit );

		return $hashtable;
	}

	/**
	 * Returns count of imported permalinks from WordPress database.
	 *
	 * @global wpdb $wpdb WordPress database abstraction object.
	 *
	 * @param string $importer_name
	 * @param string $blog_id
	 * @return int
	 */
	public function count_imported_posts( $importer_name, $blog_id ) {
		global $wpdb;

		$count = 0;

		// Get count of permalinks.
		$meta_key = $importer_name . '_' . $blog_id . '_permalink';
		$sql      = $wpdb->prepare( "SELECT COUNT( post_id ) AS cnt FROM $wpdb->postmeta WHERE meta_key = %s", $meta_key );

		$result = $wpdb->get_results( $sql );

		if ( ! empty( $result ) ) {
			$count = (int) $result[0]->cnt;
		}

		return $count;
	}

	/**
	 * Sets array with imported comments from WordPress database.
	 *
	 * @global wpdb $wpdb WordPress database abstraction object.
	 *
	 * @param string $blog_id
	 * @return array
	 */
	public function get_imported_comments( $blog_id ) {
		global $wpdb;

		$hashtable = array();

		$limit  = 100;
		$offset = 0;

		// Grab all comments in chunks.
		do {
			$sql     = $wpdb->prepare( "SELECT comment_ID, comment_agent FROM $wpdb->comments LIMIT %d,%d", $offset, $limit );
			$results = $wpdb->get_results( $sql );

			// Increment offset.
			$offset = ( $limit + $offset );

			if ( ! empty( $results ) ) {
				foreach ( $results as $r ) {
					// Explode comment_agent key.
					list ( $comment_agent_blog_id, $source_comment_id ) = explode( '-', $r->comment_agent );

					$source_comment_id = (int) $source_comment_id;

					// Check if this comment came from this blog.
					if ( (int) $blog_id === (int) $comment_agent_blog_id ) {
						$hashtable[ $source_comment_id ] = (int) $r->comment_ID;
					}
				}
			}
		} while ( count( $results ) === $limit );

		return $hashtable;
	}

	/**
	 * @param int $blog_id
	 * @return int|void
	 */
	public function set_blog( $blog_id ) {
		if ( is_numeric( $blog_id ) ) {
			$blog_id = (int) $blog_id;
		} else {
			$blog   = 'http://' . preg_replace( '#^https?://#', '', $blog_id );
			$parsed = parse_url( $blog );
			if ( ! $parsed || empty( $parsed['host'] ) ) {
				fwrite( STDERR, "Error: can not determine blog_id from $blog_id\n" );
				exit;
			}
			if ( empty( $parsed['path'] ) ) {
				$parsed['path'] = '/';
			}
			$blogs = get_sites(
				array(
					'domain' => $parsed['host'],
					'number' => 1,
					'path'   => $parsed['path'],
				)
			);
			if ( ! $blogs ) {
				fwrite( STDERR, "Error: Could not find blog\n" );
				exit;
			}
			$blog    = array_shift( $blogs );
			$blog_id = (int) $blog->blog_id;
		}

		if ( function_exists( 'is_multisite' ) ) {
			if ( is_multisite() ) {
				switch_to_blog( $blog_id );
			}
		}

		return $blog_id;
	}

	/**
	 * @param int $user_id
	 * @return int|void
	 */
	public function set_user( $user_id ) {
		if ( is_numeric( $user_id ) ) {
			$user_id = (int) $user_id;
		} else {
			$user_id = (int) username_exists( $user_id );
		}

		if ( ! $user_id || ! wp_set_current_user( $user_id ) ) {
			fwrite( STDERR, "Error: can not find user\n" );
			exit;
		}

		return $user_id;
	}

	/**
	 * Sorts by strlen, longest string first.
	 *
	 * @param string $a
	 * @param string $b
	 * @return int
	 */
	public function cmpr_strlen( $a, $b ) {
		return strlen( $b ) - strlen( $a );
	}

	/**
	 * Gets URL.
	 *
	 * @param string $url
	 * @param string $username
	 * @param string $password
	 * @param bool   $head
	 * @return array
	 */
	public function get_page(
		$url,
		$username = '',
		#[\SensitiveParameter]
		$password = '',
		$head = false
	) {
		// Increase the timeout.
		add_filter( 'http_request_timeout', array( $this, 'bump_request_timeout' ) );

		$headers = array();
		$args    = array();
		if ( true === $head ) {
			$args['method'] = 'HEAD';
		}
		if ( ! empty( $username ) && ! empty( $password ) ) {
			$headers['Authorization'] = 'Basic ' . base64_encode( "$username:$password" );
		}

		$args['headers'] = $headers;

		return wp_safe_remote_request( $url, $args );
	}

	/**
	 * Bumps up the request timeout for http requests.
	 *
	 * @param int $val
	 * @return int
	 */
	public function bump_request_timeout( $val ) {
		return 60;
	}

	/**
	 * Checks if user has exceeded disk quota.
	 *
	 * @return bool
	 */
	public function is_user_over_quota() {
		if ( function_exists( 'upload_is_user_over_quota' ) ) {
			if ( upload_is_user_over_quota() ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Replaces newlines, tabs, and multiple spaces with a single space.
	 *
	 * @param string $text
	 * @return string
	 */
	public function min_whitespace( $text ) {
		return preg_replace( '|[\r\n\t ]+|', ' ', $text );
	}

	/**
	 * Resets global variables that grow out of control during imports.
	 *
	 * @since 3.0.0
	 *
	 * @global wpdb  $wpdb       WordPress database abstraction object.
	 * @global int[] $wp_actions
	 */
	public function stop_the_insanity() {
		global $wpdb, $wp_actions;
		// Or define( 'WP_IMPORTING', true );
		$wpdb->queries = array();
		// Reset $wp_actions to keep it from growing out of control.
		$wp_actions = array();
	}
}

/**
 * Returns value of command line params.
 * Exits when a required param is not set.
 *
 * @param string $param
 * @param bool   $required
 * @return mixed
 */
function get_cli_args( $param, $required = false ) {
	$args = $_SERVER['argv'];
	if ( ! is_array( $args ) ) {
		$args = array();
	}

	$out = array();

	$last_arg = null;
	$return   = null;

	$il = count( $args );

	for ( $i = 1, $il; $i < $il; $i++ ) {
		if ( (bool) preg_match( '/^--(.+)/', $args[ $i ], $match ) ) {
			$parts = explode( '=', $match[1] );
			$key   = preg_replace( '/[^a-z0-9]+/', '', $parts[0] );

			if ( isset( $parts[1] ) ) {
				$out[ $key ] = $parts[1];
			} else {
				$out[ $key ] = true;
			}

			$last_arg = $key;
		} elseif ( (bool) preg_match( '/^-([a-zA-Z0-9]+)/', $args[ $i ], $match ) ) {
			for ( $j = 0, $jl = strlen( $match[1] ); $j < $jl; $j++ ) {
				$key         = $match[1][ $j ];
				$out[ $key ] = true;
			}

			$last_arg = $key;
		} elseif ( null !== $last_arg ) {
			$out[ $last_arg ] = $args[ $i ];
		}
	}

	// Check array for specified param.
	if ( isset( $out[ $param ] ) ) {
		// Set return value.
		$return = $out[ $param ];
	}

	// Check for missing required param.
	if ( ! isset( $out[ $param ] ) && $required ) {
		// Display message and exit.
		echo "\"$param\" parameter is required but was not specified\n";
		exit;
	}

	return $return;
}
class-wp-plugins-list-table.php000060400000160672151327343360012542 0ustar00<?php
/**
 * List Table API: WP_Plugins_List_Table class
 *
 * @package WordPress
 * @subpackage Administration
 * @since 3.1.0
 */

/**
 * Core class used to implement displaying installed plugins in a list table.
 *
 * @since 3.1.0
 *
 * @see WP_List_Table
 */
class WP_Plugins_List_Table extends WP_List_Table {
	/**
	 * Whether to show the auto-updates UI.
	 *
	 * @since 5.5.0
	 *
	 * @var bool True if auto-updates UI is to be shown, false otherwise.
	 */
	protected $show_autoupdates = true;

	/**
	 * Constructor.
	 *
	 * @since 3.1.0
	 *
	 * @see WP_List_Table::__construct() for more information on default arguments.
	 *
	 * @global string $status
	 * @global int    $page
	 *
	 * @param array $args An associative array of arguments.
	 */
	public function __construct( $args = array() ) {
		global $status, $page;

		parent::__construct(
			array(
				'plural' => 'plugins',
				'screen' => isset( $args['screen'] ) ? $args['screen'] : null,
			)
		);

		$allowed_statuses = array( 'active', 'inactive', 'recently_activated', 'upgrade', 'mustuse', 'dropins', 'search', 'paused', 'auto-update-enabled', 'auto-update-disabled' );

		$status = 'all';
		if ( isset( $_REQUEST['plugin_status'] ) && in_array( $_REQUEST['plugin_status'], $allowed_statuses, true ) ) {
			$status = $_REQUEST['plugin_status'];
		}

		if ( isset( $_REQUEST['s'] ) ) {
			$_SERVER['REQUEST_URI'] = add_query_arg( 's', wp_unslash( $_REQUEST['s'] ) );
		}

		$page = $this->get_pagenum();

		$this->show_autoupdates = wp_is_auto_update_enabled_for_type( 'plugin' )
			&& current_user_can( 'update_plugins' )
			&& ( ! is_multisite() || $this->screen->in_admin( 'network' ) );
	}

	/**
	 * @return array
	 */
	protected function get_table_classes() {
		return array( 'widefat', $this->_args['plural'] );
	}

	/**
	 * @return bool
	 */
	public function ajax_user_can() {
		return current_user_can( 'activate_plugins' );
	}

	/**
	 * @global string $status
	 * @global array  $plugins
	 * @global array  $totals
	 * @global int    $page
	 * @global string $orderby
	 * @global string $order
	 * @global string $s
	 */
	public function prepare_items() {
		global $status, $plugins, $totals, $page, $orderby, $order, $s;

		$orderby = ! empty( $_REQUEST['orderby'] ) ? sanitize_text_field( $_REQUEST['orderby'] ) : '';
		$order   = ! empty( $_REQUEST['order'] ) ? sanitize_text_field( $_REQUEST['order'] ) : '';

		/**
		 * Filters the full array of plugins to list in the Plugins list table.
		 *
		 * @since 3.0.0
		 *
		 * @see get_plugins()
		 *
		 * @param array $all_plugins An array of plugins to display in the list table.
		 */
		$all_plugins = apply_filters( 'all_plugins', get_plugins() );

		$plugins = array(
			'all'                => $all_plugins,
			'search'             => array(),
			'active'             => array(),
			'inactive'           => array(),
			'recently_activated' => array(),
			'upgrade'            => array(),
			'mustuse'            => array(),
			'dropins'            => array(),
			'paused'             => array(),
		);
		if ( $this->show_autoupdates ) {
			$auto_updates = (array) get_site_option( 'auto_update_plugins', array() );

			$plugins['auto-update-enabled']  = array();
			$plugins['auto-update-disabled'] = array();
		}

		$screen = $this->screen;

		if ( ! is_multisite() || ( $screen->in_admin( 'network' ) && current_user_can( 'manage_network_plugins' ) ) ) {

			/**
			 * Filters whether to display the advanced plugins list table.
			 *
			 * There are two types of advanced plugins - must-use and drop-ins -
			 * which can be used in a single site or Multisite network.
			 *
			 * The $type parameter allows you to differentiate between the type of advanced
			 * plugins to filter the display of. Contexts include 'mustuse' and 'dropins'.
			 *
			 * @since 3.0.0
			 *
			 * @param bool   $show Whether to show the advanced plugins for the specified
			 *                     plugin type. Default true.
			 * @param string $type The plugin type. Accepts 'mustuse', 'dropins'.
			 */
			if ( apply_filters( 'show_advanced_plugins', true, 'mustuse' ) ) {
				$plugins['mustuse'] = get_mu_plugins();
			}

			/** This action is documented in wp-admin/includes/class-wp-plugins-list-table.php */
			if ( apply_filters( 'show_advanced_plugins', true, 'dropins' ) ) {
				$plugins['dropins'] = get_dropins();
			}

			if ( current_user_can( 'update_plugins' ) ) {
				$current = get_site_transient( 'update_plugins' );
				foreach ( (array) $plugins['all'] as $plugin_file => $plugin_data ) {
					if ( isset( $current->response[ $plugin_file ] ) ) {
						$plugins['all'][ $plugin_file ]['update'] = true;
						$plugins['upgrade'][ $plugin_file ]       = $plugins['all'][ $plugin_file ];
					}
				}
			}
		}

		if ( ! $screen->in_admin( 'network' ) ) {
			$show = current_user_can( 'manage_network_plugins' );
			/**
			 * Filters whether to display network-active plugins alongside plugins active for the current site.
			 *
			 * This also controls the display of inactive network-only plugins (plugins with
			 * "Network: true" in the plugin header).
			 *
			 * Plugins cannot be network-activated or network-deactivated from this screen.
			 *
			 * @since 4.4.0
			 *
			 * @param bool $show Whether to show network-active plugins. Default is whether the current
			 *                   user can manage network plugins (ie. a Super Admin).
			 */
			$show_network_active = apply_filters( 'show_network_active_plugins', $show );
		}

		if ( $screen->in_admin( 'network' ) ) {
			$recently_activated = get_site_option( 'recently_activated', array() );
		} else {
			$recently_activated = get_option( 'recently_activated', array() );
		}

		foreach ( $recently_activated as $key => $time ) {
			if ( $time + WEEK_IN_SECONDS < time() ) {
				unset( $recently_activated[ $key ] );
			}
		}

		if ( $screen->in_admin( 'network' ) ) {
			update_site_option( 'recently_activated', $recently_activated );
		} else {
			update_option( 'recently_activated', $recently_activated, false );
		}

		$plugin_info = get_site_transient( 'update_plugins' );

		foreach ( (array) $plugins['all'] as $plugin_file => $plugin_data ) {
			// Extra info if known. array_merge() ensures $plugin_data has precedence if keys collide.
			if ( isset( $plugin_info->response[ $plugin_file ] ) ) {
				$plugin_data = array_merge( (array) $plugin_info->response[ $plugin_file ], array( 'update-supported' => true ), $plugin_data );
			} elseif ( isset( $plugin_info->no_update[ $plugin_file ] ) ) {
				$plugin_data = array_merge( (array) $plugin_info->no_update[ $plugin_file ], array( 'update-supported' => true ), $plugin_data );
			} elseif ( empty( $plugin_data['update-supported'] ) ) {
				$plugin_data['update-supported'] = false;
			}

			/*
			 * Create the payload that's used for the auto_update_plugin filter.
			 * This is the same data contained within $plugin_info->(response|no_update) however
			 * not all plugins will be contained in those keys, this avoids unexpected warnings.
			 */
			$filter_payload = array(
				'id'            => $plugin_file,
				'slug'          => '',
				'plugin'        => $plugin_file,
				'new_version'   => '',
				'url'           => '',
				'package'       => '',
				'icons'         => array(),
				'banners'       => array(),
				'banners_rtl'   => array(),
				'tested'        => '',
				'requires_php'  => '',
				'compatibility' => new stdClass(),
			);

			$filter_payload = (object) wp_parse_args( $plugin_data, $filter_payload );

			$auto_update_forced = wp_is_auto_update_forced_for_item( 'plugin', null, $filter_payload );

			if ( ! is_null( $auto_update_forced ) ) {
				$plugin_data['auto-update-forced'] = $auto_update_forced;
			}

			$plugins['all'][ $plugin_file ] = $plugin_data;
			// Make sure that $plugins['upgrade'] also receives the extra info since it is used on ?plugin_status=upgrade.
			if ( isset( $plugins['upgrade'][ $plugin_file ] ) ) {
				$plugins['upgrade'][ $plugin_file ] = $plugin_data;
			}

			// Filter into individual sections.
			if ( is_multisite() && ! $screen->in_admin( 'network' ) && is_network_only_plugin( $plugin_file ) && ! is_plugin_active( $plugin_file ) ) {
				if ( $show_network_active ) {
					// On the non-network screen, show inactive network-only plugins if allowed.
					$plugins['inactive'][ $plugin_file ] = $plugin_data;
				} else {
					// On the non-network screen, filter out network-only plugins as long as they're not individually active.
					unset( $plugins['all'][ $plugin_file ] );
				}
			} elseif ( ! $screen->in_admin( 'network' ) && is_plugin_active_for_network( $plugin_file ) ) {
				if ( $show_network_active ) {
					// On the non-network screen, show network-active plugins if allowed.
					$plugins['active'][ $plugin_file ] = $plugin_data;
				} else {
					// On the non-network screen, filter out network-active plugins.
					unset( $plugins['all'][ $plugin_file ] );
				}
			} elseif ( ( ! $screen->in_admin( 'network' ) && is_plugin_active( $plugin_file ) )
				|| ( $screen->in_admin( 'network' ) && is_plugin_active_for_network( $plugin_file ) ) ) {
				/*
				 * On the non-network screen, populate the active list with plugins that are individually activated.
				 * On the network admin screen, populate the active list with plugins that are network-activated.
				 */
				$plugins['active'][ $plugin_file ] = $plugin_data;

				if ( ! $screen->in_admin( 'network' ) && is_plugin_paused( $plugin_file ) ) {
					$plugins['paused'][ $plugin_file ] = $plugin_data;
				}
			} else {
				if ( isset( $recently_activated[ $plugin_file ] ) ) {
					// Populate the recently activated list with plugins that have been recently activated.
					$plugins['recently_activated'][ $plugin_file ] = $plugin_data;
				}
				// Populate the inactive list with plugins that aren't activated.
				$plugins['inactive'][ $plugin_file ] = $plugin_data;
			}

			if ( $this->show_autoupdates ) {
				$enabled = in_array( $plugin_file, $auto_updates, true ) && $plugin_data['update-supported'];
				if ( isset( $plugin_data['auto-update-forced'] ) ) {
					$enabled = (bool) $plugin_data['auto-update-forced'];
				}

				if ( $enabled ) {
					$plugins['auto-update-enabled'][ $plugin_file ] = $plugin_data;
				} else {
					$plugins['auto-update-disabled'][ $plugin_file ] = $plugin_data;
				}
			}
		}

		if ( strlen( $s ) ) {
			$status            = 'search';
			$plugins['search'] = array_filter( $plugins['all'], array( $this, '_search_callback' ) );
		}

		/**
		 * Filters the array of plugins for the list table.
		 *
		 * @since 6.3.0
		 *
		 * @param array[] $plugins An array of arrays of plugin data, keyed by context.
		 */
		$plugins = apply_filters( 'plugins_list', $plugins );

		$totals = array();
		foreach ( $plugins as $type => $list ) {
			$totals[ $type ] = count( $list );
		}

		if ( empty( $plugins[ $status ] ) && ! in_array( $status, array( 'all', 'search' ), true ) ) {
			$status = 'all';
		}

		$this->items = array();
		foreach ( $plugins[ $status ] as $plugin_file => $plugin_data ) {
			// Translate, don't apply markup, sanitize HTML.
			$this->items[ $plugin_file ] = _get_plugin_data_markup_translate( $plugin_file, $plugin_data, false, true );
		}

		$total_this_page = $totals[ $status ];

		$js_plugins = array();
		foreach ( $plugins as $key => $list ) {
			$js_plugins[ $key ] = array_keys( $list );
		}

		wp_localize_script(
			'updates',
			'_wpUpdatesItemCounts',
			array(
				'plugins' => $js_plugins,
				'totals'  => wp_get_update_data(),
			)
		);

		if ( ! $orderby ) {
			$orderby = 'Name';
		} else {
			$orderby = ucfirst( $orderby );
		}

		$order = strtoupper( $order );

		uasort( $this->items, array( $this, '_order_callback' ) );

		$plugins_per_page = $this->get_items_per_page( str_replace( '-', '_', $screen->id . '_per_page' ), 999 );

		$start = ( $page - 1 ) * $plugins_per_page;

		if ( $total_this_page > $plugins_per_page ) {
			$this->items = array_slice( $this->items, $start, $plugins_per_page );
		}

		$this->set_pagination_args(
			array(
				'total_items' => $total_this_page,
				'per_page'    => $plugins_per_page,
			)
		);
	}

	/**
	 * @global string $s URL encoded search term.
	 *
	 * @param array $plugin
	 * @return bool
	 */
	public function _search_callback( $plugin ) {
		global $s;

		foreach ( $plugin as $value ) {
			if ( is_string( $value ) && false !== stripos( strip_tags( $value ), urldecode( $s ) ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * @global string $orderby
	 * @global string $order
	 * @param array $plugin_a
	 * @param array $plugin_b
	 * @return int
	 */
	public function _order_callback( $plugin_a, $plugin_b ) {
		global $orderby, $order;

		$a = $plugin_a[ $orderby ];
		$b = $plugin_b[ $orderby ];

		if ( $a === $b ) {
			return 0;
		}

		if ( 'DESC' === $order ) {
			return strcasecmp( $b, $a );
		} else {
			return strcasecmp( $a, $b );
		}
	}

	/**
	 * @global array $plugins
	 */
	public function no_items() {
		global $plugins;

		if ( ! empty( $_REQUEST['s'] ) ) {
			$s = esc_html( urldecode( wp_unslash( $_REQUEST['s'] ) ) );

			/* translators: %s: Plugin search term. */
			printf( __( 'No plugins found for: %s.' ), '<strong>' . $s . '</strong>' );

			// We assume that somebody who can install plugins in multisite is experienced enough to not need this helper link.
			if ( ! is_multisite() && current_user_can( 'install_plugins' ) ) {
				echo ' <a href="' . esc_url( admin_url( 'plugin-install.php?tab=search&s=' . urlencode( $s ) ) ) . '">' . __( 'Search for plugins in the WordPress Plugin Directory.' ) . '</a>';
			}
		} elseif ( ! empty( $plugins['all'] ) ) {
			_e( 'No plugins found.' );
		} else {
			_e( 'No plugins are currently available.' );
		}
	}

	/**
	 * Displays the search box.
	 *
	 * @since 4.6.0
	 *
	 * @param string $text     The 'submit' button label.
	 * @param string $input_id ID attribute value for the search input field.
	 */
	public function search_box( $text, $input_id ) {
		if ( empty( $_REQUEST['s'] ) && ! $this->has_items() ) {
			return;
		}

		$input_id = $input_id . '-search-input';

		if ( ! empty( $_REQUEST['orderby'] ) ) {
			echo '<input type="hidden" name="orderby" value="' . esc_attr( $_REQUEST['orderby'] ) . '" />';
		}
		if ( ! empty( $_REQUEST['order'] ) ) {
			echo '<input type="hidden" name="order" value="' . esc_attr( $_REQUEST['order'] ) . '" />';
		}
		?>
		<p class="search-box">
			<label for="<?php echo esc_attr( $input_id ); ?>"><?php echo $text; ?></label>
			<input type="search" id="<?php echo esc_attr( $input_id ); ?>" class="wp-filter-search" name="s" value="<?php _admin_search_query(); ?>" />
			<?php submit_button( $text, 'hide-if-js', '', false, array( 'id' => 'search-submit' ) ); ?>
		</p>
		<?php
	}

	/**
	 * @global string $status
	 *
	 * @return string[] Array of column titles keyed by their column name.
	 */
	public function get_columns() {
		global $status;

		$columns = array(
			'cb'          => ! in_array( $status, array( 'mustuse', 'dropins' ), true ) ? '<input type="checkbox" />' : '',
			'name'        => __( 'Plugin' ),
			'description' => __( 'Description' ),
		);

		if ( $this->show_autoupdates && ! in_array( $status, array( 'mustuse', 'dropins' ), true ) ) {
			$columns['auto-updates'] = __( 'Automatic Updates' );
		}

		return $columns;
	}

	/**
	 * @return array
	 */
	protected function get_sortable_columns() {
		return array();
	}

	/**
	 * @global array $totals
	 * @global string $status
	 * @return array
	 */
	protected function get_views() {
		global $totals, $status;

		$status_links = array();
		foreach ( $totals as $type => $count ) {
			if ( ! $count ) {
				continue;
			}

			switch ( $type ) {
				case 'all':
					/* translators: %s: Number of plugins. */
					$text = _nx(
						'All <span class="count">(%s)</span>',
						'All <span class="count">(%s)</span>',
						$count,
						'plugins'
					);
					break;
				case 'active':
					/* translators: %s: Number of plugins. */
					$text = _n(
						'Active <span class="count">(%s)</span>',
						'Active <span class="count">(%s)</span>',
						$count
					);
					break;
				case 'recently_activated':
					/* translators: %s: Number of plugins. */
					$text = _n(
						'Recently Active <span class="count">(%s)</span>',
						'Recently Active <span class="count">(%s)</span>',
						$count
					);
					break;
				case 'inactive':
					/* translators: %s: Number of plugins. */
					$text = _n(
						'Inactive <span class="count">(%s)</span>',
						'Inactive <span class="count">(%s)</span>',
						$count
					);
					break;
				case 'mustuse':
					/* translators: %s: Number of plugins. */
					$text = _n(
						'Must-Use <span class="count">(%s)</span>',
						'Must-Use <span class="count">(%s)</span>',
						$count
					);
					break;
				case 'dropins':
					/* translators: %s: Number of plugins. */
					$text = _n(
						'Drop-in <span class="count">(%s)</span>',
						'Drop-ins <span class="count">(%s)</span>',
						$count
					);
					break;
				case 'paused':
					/* translators: %s: Number of plugins. */
					$text = _n(
						'Paused <span class="count">(%s)</span>',
						'Paused <span class="count">(%s)</span>',
						$count
					);
					break;
				case 'upgrade':
					/* translators: %s: Number of plugins. */
					$text = _n(
						'Update Available <span class="count">(%s)</span>',
						'Update Available <span class="count">(%s)</span>',
						$count
					);
					break;
				case 'auto-update-enabled':
					/* translators: %s: Number of plugins. */
					$text = _n(
						'Auto-updates Enabled <span class="count">(%s)</span>',
						'Auto-updates Enabled <span class="count">(%s)</span>',
						$count
					);
					break;
				case 'auto-update-disabled':
					/* translators: %s: Number of plugins. */
					$text = _n(
						'Auto-updates Disabled <span class="count">(%s)</span>',
						'Auto-updates Disabled <span class="count">(%s)</span>',
						$count
					);
					break;
			}

			if ( 'search' !== $type ) {
				$status_links[ $type ] = array(
					'url'     => add_query_arg( 'plugin_status', $type, 'plugins.php' ),
					'label'   => sprintf( $text, number_format_i18n( $count ) ),
					'current' => $type === $status,
				);
			}
		}

		return $this->get_views_links( $status_links );
	}

	/**
	 * @global string $status
	 * @return array
	 */
	protected function get_bulk_actions() {
		global $status;

		$actions = array();

		if ( 'active' !== $status ) {
			$actions['activate-selected'] = $this->screen->in_admin( 'network' ) ? _x( 'Network Activate', 'plugin' ) : _x( 'Activate', 'plugin' );
		}

		if ( 'inactive' !== $status && 'recent' !== $status ) {
			$actions['deactivate-selected'] = $this->screen->in_admin( 'network' ) ? _x( 'Network Deactivate', 'plugin' ) : _x( 'Deactivate', 'plugin' );
		}

		if ( ! is_multisite() || $this->screen->in_admin( 'network' ) ) {
			if ( current_user_can( 'update_plugins' ) ) {
				$actions['update-selected'] = __( 'Update' );
			}

			if ( current_user_can( 'delete_plugins' ) && ( 'active' !== $status ) ) {
				$actions['delete-selected'] = __( 'Delete' );
			}

			if ( $this->show_autoupdates ) {
				if ( 'auto-update-enabled' !== $status ) {
					$actions['enable-auto-update-selected'] = __( 'Enable Auto-updates' );
				}
				if ( 'auto-update-disabled' !== $status ) {
					$actions['disable-auto-update-selected'] = __( 'Disable Auto-updates' );
				}
			}
		}

		return $actions;
	}

	/**
	 * @global string $status
	 * @param string $which
	 */
	public function bulk_actions( $which = '' ) {
		global $status;

		if ( in_array( $status, array( 'mustuse', 'dropins' ), true ) ) {
			return;
		}

		parent::bulk_actions( $which );
	}

	/**
	 * @global string $status
	 * @param string $which
	 */
	protected function extra_tablenav( $which ) {
		global $status;

		if ( ! in_array( $status, array( 'recently_activated', 'mustuse', 'dropins' ), true ) ) {
			return;
		}

		echo '<div class="alignleft actions">';

		if ( 'recently_activated' === $status ) {
			submit_button( __( 'Clear List' ), '', 'clear-recent-list', false );
		} elseif ( 'top' === $which && 'mustuse' === $status ) {
			echo '<p>' . sprintf(
				/* translators: %s: mu-plugins directory name. */
				__( 'Files in the %s directory are executed automatically.' ),
				'<code>' . str_replace( ABSPATH, '/', WPMU_PLUGIN_DIR ) . '</code>'
			) . '</p>';
		} elseif ( 'top' === $which && 'dropins' === $status ) {
			echo '<p>' . sprintf(
				/* translators: %s: wp-content directory name. */
				__( 'Drop-ins are single files, found in the %s directory, that replace or enhance WordPress features in ways that are not possible for traditional plugins.' ),
				'<code>' . str_replace( ABSPATH, '', WP_CONTENT_DIR ) . '</code>'
			) . '</p>';
		}
		echo '</div>';
	}

	/**
	 * @return string
	 */
	public function current_action() {
		if ( isset( $_POST['clear-recent-list'] ) ) {
			return 'clear-recent-list';
		}

		return parent::current_action();
	}

	/**
	 * Generates the list table rows.
	 *
	 * @since 3.1.0
	 *
	 * @global string $status
	 */
	public function display_rows() {
		global $status;

		if ( is_multisite() && ! $this->screen->in_admin( 'network' ) && in_array( $status, array( 'mustuse', 'dropins' ), true ) ) {
			return;
		}

		foreach ( $this->items as $plugin_file => $plugin_data ) {
			$this->single_row( array( $plugin_file, $plugin_data ) );
		}
	}

	/**
	 * @global string $status
	 * @global int $page
	 * @global string $s
	 * @global array $totals
	 *
	 * @param array $item
	 */
	public function single_row( $item ) {
		global $status, $page, $s, $totals;
		static $plugin_id_attrs = array();

		list( $plugin_file, $plugin_data ) = $item;

		$plugin_slug    = isset( $plugin_data['slug'] ) ? $plugin_data['slug'] : sanitize_title( $plugin_data['Name'] );
		$plugin_id_attr = $plugin_slug;

		// Ensure the ID attribute is unique.
		$suffix = 2;
		while ( in_array( $plugin_id_attr, $plugin_id_attrs, true ) ) {
			$plugin_id_attr = "$plugin_slug-$suffix";
			++$suffix;
		}

		$plugin_id_attrs[] = $plugin_id_attr;

		$context = $status;
		$screen  = $this->screen;

		// Pre-order.
		$actions = array(
			'deactivate' => '',
			'activate'   => '',
			'details'    => '',
			'delete'     => '',
		);

		// Do not restrict by default.
		$restrict_network_active = false;
		$restrict_network_only   = false;

		$requires_php = isset( $plugin_data['RequiresPHP'] ) ? $plugin_data['RequiresPHP'] : null;
		$requires_wp  = isset( $plugin_data['RequiresWP'] ) ? $plugin_data['RequiresWP'] : null;

		$compatible_php = is_php_version_compatible( $requires_php );
		$compatible_wp  = is_wp_version_compatible( $requires_wp );

		$has_dependents          = WP_Plugin_Dependencies::has_dependents( $plugin_file );
		$has_active_dependents   = WP_Plugin_Dependencies::has_active_dependents( $plugin_file );
		$has_unmet_dependencies  = WP_Plugin_Dependencies::has_unmet_dependencies( $plugin_file );
		$has_circular_dependency = WP_Plugin_Dependencies::has_circular_dependency( $plugin_file );

		if ( 'mustuse' === $context ) {
			$is_active = true;
		} elseif ( 'dropins' === $context ) {
			$dropins     = _get_dropins();
			$plugin_name = $plugin_file;

			if ( $plugin_file !== $plugin_data['Name'] ) {
				$plugin_name .= '<br />' . $plugin_data['Name'];
			}

			if ( true === ( $dropins[ $plugin_file ][1] ) ) { // Doesn't require a constant.
				$is_active   = true;
				$description = '<p><strong>' . $dropins[ $plugin_file ][0] . '</strong></p>';
			} elseif ( defined( $dropins[ $plugin_file ][1] ) && constant( $dropins[ $plugin_file ][1] ) ) { // Constant is true.
				$is_active   = true;
				$description = '<p><strong>' . $dropins[ $plugin_file ][0] . '</strong></p>';
			} else {
				$is_active   = false;
				$description = '<p><strong>' . $dropins[ $plugin_file ][0] . ' <span class="error-message">' . __( 'Inactive:' ) . '</span></strong> ' .
					sprintf(
						/* translators: 1: Drop-in constant name, 2: wp-config.php */
						__( 'Requires %1$s in %2$s file.' ),
						"<code>define('" . $dropins[ $plugin_file ][1] . "', true);</code>",
						'<code>wp-config.php</code>'
					) . '</p>';
			}

			if ( $plugin_data['Description'] ) {
				$description .= '<p>' . $plugin_data['Description'] . '</p>';
			}
		} else {
			if ( $screen->in_admin( 'network' ) ) {
				$is_active = is_plugin_active_for_network( $plugin_file );
			} else {
				$is_active               = is_plugin_active( $plugin_file );
				$restrict_network_active = ( is_multisite() && is_plugin_active_for_network( $plugin_file ) );
				$restrict_network_only   = ( is_multisite() && is_network_only_plugin( $plugin_file ) && ! $is_active );
			}

			if ( $screen->in_admin( 'network' ) ) {
				if ( $is_active ) {
					if ( current_user_can( 'manage_network_plugins' ) ) {
						if ( $has_active_dependents ) {
							$actions['deactivate'] = __( 'Deactivate' ) .
								'<span class="screen-reader-text">' .
								__( 'You cannot deactivate this plugin as other plugins require it.' ) .
								'</span>';

						} else {
							$deactivate_url = 'plugins.php?action=deactivate' .
								'&amp;plugin=' . urlencode( $plugin_file ) .
								'&amp;plugin_status=' . $context .
								'&amp;paged=' . $page .
								'&amp;s=' . $s;

							$actions['deactivate'] = sprintf(
								'<a href="%s" id="deactivate-%s" aria-label="%s">%s</a>',
								wp_nonce_url( $deactivate_url, 'deactivate-plugin_' . $plugin_file ),
								esc_attr( $plugin_id_attr ),
								/* translators: %s: Plugin name. */
								esc_attr( sprintf( _x( 'Network Deactivate %s', 'plugin' ), $plugin_data['Name'] ) ),
								_x( 'Network Deactivate', 'plugin' )
							);
						}
					}
				} else {
					if ( current_user_can( 'manage_network_plugins' ) ) {
						if ( $compatible_php && $compatible_wp ) {
							if ( $has_unmet_dependencies ) {
								$actions['activate'] = _x( 'Network Activate', 'plugin' ) .
									'<span class="screen-reader-text">' .
									__( 'You cannot activate this plugin as it has unmet requirements.' ) .
									'</span>';
							} else {
								$activate_url = 'plugins.php?action=activate' .
									'&amp;plugin=' . urlencode( $plugin_file ) .
									'&amp;plugin_status=' . $context .
									'&amp;paged=' . $page .
									'&amp;s=' . $s;

								$actions['activate'] = sprintf(
									'<a href="%s" id="activate-%s" class="edit" aria-label="%s">%s</a>',
									wp_nonce_url( $activate_url, 'activate-plugin_' . $plugin_file ),
									esc_attr( $plugin_id_attr ),
									/* translators: %s: Plugin name. */
									esc_attr( sprintf( _x( 'Network Activate %s', 'plugin' ), $plugin_data['Name'] ) ),
									_x( 'Network Activate', 'plugin' )
								);
							}
						} else {
							$actions['activate'] = sprintf(
								'<span>%s</span>',
								_x( 'Cannot Activate', 'plugin' )
							);
						}
					}

					if ( current_user_can( 'delete_plugins' ) && ! is_plugin_active( $plugin_file ) ) {
						if ( $has_dependents && ! $has_circular_dependency ) {
							$actions['delete'] = __( 'Delete' ) .
								'<span class="screen-reader-text">' .
								__( 'You cannot delete this plugin as other plugins require it.' ) .
								'</span>';
						} else {
							$delete_url = 'plugins.php?action=delete-selected' .
								'&amp;checked[]=' . urlencode( $plugin_file ) .
								'&amp;plugin_status=' . $context .
								'&amp;paged=' . $page .
								'&amp;s=' . $s;

							$actions['delete'] = sprintf(
								'<a href="%s" id="delete-%s" class="delete" aria-label="%s">%s</a>',
								wp_nonce_url( $delete_url, 'bulk-plugins' ),
								esc_attr( $plugin_id_attr ),
								/* translators: %s: Plugin name. */
								esc_attr( sprintf( _x( 'Delete %s', 'plugin' ), $plugin_data['Name'] ) ),
								__( 'Delete' )
							);
						}
					}
				}
			} else {
				if ( $restrict_network_active ) {
					$actions = array(
						'network_active' => __( 'Network Active' ),
					);
				} elseif ( $restrict_network_only ) {
					$actions = array(
						'network_only' => __( 'Network Only' ),
					);
				} elseif ( $is_active ) {
					if ( current_user_can( 'deactivate_plugin', $plugin_file ) ) {
						if ( $has_active_dependents ) {
							$actions['deactivate'] = __( 'Deactivate' ) .
								'<span class="screen-reader-text">' .
								__( 'You cannot deactivate this plugin as other plugins depend on it.' ) .
								'</span>';
						} else {
							$deactivate_url = 'plugins.php?action=deactivate' .
								'&amp;plugin=' . urlencode( $plugin_file ) .
								'&amp;plugin_status=' . $context .
								'&amp;paged=' . $page .
								'&amp;s=' . $s;

							$actions['deactivate'] = sprintf(
								'<a href="%s" id="deactivate-%s" aria-label="%s">%s</a>',
								wp_nonce_url( $deactivate_url, 'deactivate-plugin_' . $plugin_file ),
								esc_attr( $plugin_id_attr ),
								/* translators: %s: Plugin name. */
								esc_attr( sprintf( _x( 'Deactivate %s', 'plugin' ), $plugin_data['Name'] ) ),
								__( 'Deactivate' )
							);
						}
					}

					if ( current_user_can( 'resume_plugin', $plugin_file ) && is_plugin_paused( $plugin_file ) ) {
						$resume_url = 'plugins.php?action=resume' .
							'&amp;plugin=' . urlencode( $plugin_file ) .
							'&amp;plugin_status=' . $context .
							'&amp;paged=' . $page .
							'&amp;s=' . $s;

						$actions['resume'] = sprintf(
							'<a href="%s" id="resume-%s" class="resume-link" aria-label="%s">%s</a>',
							wp_nonce_url( $resume_url, 'resume-plugin_' . $plugin_file ),
							esc_attr( $plugin_id_attr ),
							/* translators: %s: Plugin name. */
							esc_attr( sprintf( _x( 'Resume %s', 'plugin' ), $plugin_data['Name'] ) ),
							__( 'Resume' )
						);
					}
				} else {
					if ( current_user_can( 'activate_plugin', $plugin_file ) ) {
						if ( $compatible_php && $compatible_wp ) {
							if ( $has_unmet_dependencies ) {
								$actions['activate'] = _x( 'Activate', 'plugin' ) .
									'<span class="screen-reader-text">' .
									__( 'You cannot activate this plugin as it has unmet requirements.' ) .
									'</span>';
							} else {
								$activate_url = 'plugins.php?action=activate' .
									'&amp;plugin=' . urlencode( $plugin_file ) .
									'&amp;plugin_status=' . $context .
									'&amp;paged=' . $page .
									'&amp;s=' . $s;

								$actions['activate'] = sprintf(
									'<a href="%s" id="activate-%s" class="edit" aria-label="%s">%s</a>',
									wp_nonce_url( $activate_url, 'activate-plugin_' . $plugin_file ),
									esc_attr( $plugin_id_attr ),
									/* translators: %s: Plugin name. */
									esc_attr( sprintf( _x( 'Activate %s', 'plugin' ), $plugin_data['Name'] ) ),
									_x( 'Activate', 'plugin' )
								);
							}
						} else {
							$actions['activate'] = sprintf(
								'<span>%s</span>',
								_x( 'Cannot Activate', 'plugin' )
							);
						}
					}

					if ( ! is_multisite() && current_user_can( 'delete_plugins' ) ) {
						if ( $has_dependents && ! $has_circular_dependency ) {
							$actions['delete'] = __( 'Delete' ) .
								'<span class="screen-reader-text">' .
								__( 'You cannot delete this plugin as other plugins require it.' ) .
								'</span>';
						} else {
							$delete_url = 'plugins.php?action=delete-selected' .
								'&amp;checked[]=' . urlencode( $plugin_file ) .
								'&amp;plugin_status=' . $context .
								'&amp;paged=' . $page .
								'&amp;s=' . $s;

							$actions['delete'] = sprintf(
								'<a href="%s" id="delete-%s" class="delete" aria-label="%s">%s</a>',
								wp_nonce_url( $delete_url, 'bulk-plugins' ),
								esc_attr( $plugin_id_attr ),
								/* translators: %s: Plugin name. */
								esc_attr( sprintf( _x( 'Delete %s', 'plugin' ), $plugin_data['Name'] ) ),
								__( 'Delete' )
							);
						}
					}
				} // End if $is_active.
			} // End if $screen->in_admin( 'network' ).
		} // End if $context.

		$actions = array_filter( $actions );

		if ( $screen->in_admin( 'network' ) ) {

			/**
			 * Filters the action links displayed for each plugin in the Network Admin Plugins list table.
			 *
			 * @since 3.1.0
			 *
			 * @param string[] $actions     An array of plugin action links. By default this can include
			 *                              'activate', 'deactivate', and 'delete'.
			 * @param string   $plugin_file Path to the plugin file relative to the plugins directory.
			 * @param array    $plugin_data An array of plugin data. See get_plugin_data()
			 *                              and the {@see 'plugin_row_meta'} filter for the list
			 *                              of possible values.
			 * @param string   $context     The plugin context. By default this can include 'all',
			 *                              'active', 'inactive', 'recently_activated', 'upgrade',
			 *                              'mustuse', 'dropins', and 'search'.
			 */
			$actions = apply_filters( 'network_admin_plugin_action_links', $actions, $plugin_file, $plugin_data, $context );

			/**
			 * Filters the list of action links displayed for a specific plugin in the Network Admin Plugins list table.
			 *
			 * The dynamic portion of the hook name, `$plugin_file`, refers to the path
			 * to the plugin file, relative to the plugins directory.
			 *
			 * @since 3.1.0
			 *
			 * @param string[] $actions     An array of plugin action links. By default this can include
			 *                              'activate', 'deactivate', and 'delete'.
			 * @param string   $plugin_file Path to the plugin file relative to the plugins directory.
			 * @param array    $plugin_data An array of plugin data. See get_plugin_data()
			 *                              and the {@see 'plugin_row_meta'} filter for the list
			 *                              of possible values.
			 * @param string   $context     The plugin context. By default this can include 'all',
			 *                              'active', 'inactive', 'recently_activated', 'upgrade',
			 *                              'mustuse', 'dropins', and 'search'.
			 */
			$actions = apply_filters( "network_admin_plugin_action_links_{$plugin_file}", $actions, $plugin_file, $plugin_data, $context );

		} else {

			/**
			 * Filters the action links displayed for each plugin in the Plugins list table.
			 *
			 * @since 2.5.0
			 * @since 2.6.0 The `$context` parameter was added.
			 * @since 4.9.0 The 'Edit' link was removed from the list of action links.
			 *
			 * @param string[] $actions     An array of plugin action links. By default this can include
			 *                              'activate', 'deactivate', and 'delete'. With Multisite active
			 *                              this can also include 'network_active' and 'network_only' items.
			 * @param string   $plugin_file Path to the plugin file relative to the plugins directory.
			 * @param array    $plugin_data An array of plugin data. See get_plugin_data()
			 *                              and the {@see 'plugin_row_meta'} filter for the list
			 *                              of possible values.
			 * @param string   $context     The plugin context. By default this can include 'all',
			 *                              'active', 'inactive', 'recently_activated', 'upgrade',
			 *                              'mustuse', 'dropins', and 'search'.
			 */
			$actions = apply_filters( 'plugin_action_links', $actions, $plugin_file, $plugin_data, $context );

			/**
			 * Filters the list of action links displayed for a specific plugin in the Plugins list table.
			 *
			 * The dynamic portion of the hook name, `$plugin_file`, refers to the path
			 * to the plugin file, relative to the plugins directory.
			 *
			 * @since 2.7.0
			 * @since 4.9.0 The 'Edit' link was removed from the list of action links.
			 *
			 * @param string[] $actions     An array of plugin action links. By default this can include
			 *                              'activate', 'deactivate', and 'delete'. With Multisite active
			 *                              this can also include 'network_active' and 'network_only' items.
			 * @param string   $plugin_file Path to the plugin file relative to the plugins directory.
			 * @param array    $plugin_data An array of plugin data. See get_plugin_data()
			 *                              and the {@see 'plugin_row_meta'} filter for the list
			 *                              of possible values.
			 * @param string   $context     The plugin context. By default this can include 'all',
			 *                              'active', 'inactive', 'recently_activated', 'upgrade',
			 *                              'mustuse', 'dropins', and 'search'.
			 */
			$actions = apply_filters( "plugin_action_links_{$plugin_file}", $actions, $plugin_file, $plugin_data, $context );

		}

		$class       = $is_active ? 'active' : 'inactive';
		$checkbox_id = 'checkbox_' . md5( $plugin_file );
		$disabled    = '';

		if ( $has_dependents || $has_unmet_dependencies ) {
			$disabled = 'disabled';
		}

		if (
			$restrict_network_active ||
			$restrict_network_only ||
			in_array( $status, array( 'mustuse', 'dropins' ), true ) ||
			! $compatible_php
		) {
			$checkbox = '';
		} else {
			$checkbox = sprintf(
				'<label class="label-covers-full-cell" for="%1$s">' .
				'<span class="screen-reader-text">%2$s</span></label>' .
				'<input type="checkbox" name="checked[]" value="%3$s" id="%1$s" ' . $disabled . '/>',
				$checkbox_id,
				/* translators: Hidden accessibility text. %s: Plugin name. */
				sprintf( __( 'Select %s' ), $plugin_data['Name'] ),
				esc_attr( $plugin_file )
			);
		}

		if ( 'dropins' !== $context ) {
			$description = '<p>' . ( $plugin_data['Description'] ? $plugin_data['Description'] : '&nbsp;' ) . '</p>';
			$plugin_name = $plugin_data['Name'];
		}

		if (
			! empty( $totals['upgrade'] ) &&
			! empty( $plugin_data['update'] ) ||
			! $compatible_php ||
			! $compatible_wp
		) {
			$class .= ' update';
		}

		$paused = ! $screen->in_admin( 'network' ) && is_plugin_paused( $plugin_file );

		if ( $paused ) {
			$class .= ' paused';
		}

		if ( is_uninstallable_plugin( $plugin_file ) ) {
			$class .= ' is-uninstallable';
		}

		printf(
			'<tr class="%s" data-slug="%s" data-plugin="%s">',
			esc_attr( $class ),
			esc_attr( $plugin_slug ),
			esc_attr( $plugin_file )
		);

		list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info();

		$auto_updates = (array) get_site_option( 'auto_update_plugins', array() );

		foreach ( $columns as $column_name => $column_display_name ) {
			$extra_classes = '';
			if ( in_array( $column_name, $hidden, true ) ) {
				$extra_classes = ' hidden';
			}

			switch ( $column_name ) {
				case 'cb':
					echo "<th scope='row' class='check-column'>$checkbox</th>";
					break;
				case 'name':
					echo "<td class='plugin-title column-primary'><strong>$plugin_name</strong>";
					echo $this->row_actions( $actions, true );
					echo '</td>';
					break;
				case 'description':
					$classes = 'column-description desc';

					echo "<td class='$classes{$extra_classes}'>
						<div class='plugin-description'>$description</div>
						<div class='$class second plugin-version-author-uri'>";

					$plugin_meta = array();

					if ( ! empty( $plugin_data['Version'] ) ) {
						/* translators: %s: Plugin version number. */
						$plugin_meta[] = sprintf( __( 'Version %s' ), $plugin_data['Version'] );
					}

					if ( ! empty( $plugin_data['Author'] ) ) {
						$author = $plugin_data['Author'];

						if ( ! empty( $plugin_data['AuthorURI'] ) ) {
							$author = '<a href="' . $plugin_data['AuthorURI'] . '">' . $plugin_data['Author'] . '</a>';
						}

						/* translators: %s: Plugin author name. */
						$plugin_meta[] = sprintf( __( 'By %s' ), $author );
					}

					// Details link using API info, if available.
					if ( isset( $plugin_data['slug'] ) && current_user_can( 'install_plugins' ) ) {
						$plugin_meta[] = sprintf(
							'<a href="%s" class="thickbox open-plugin-details-modal" aria-label="%s" data-title="%s">%s</a>',
							esc_url(
								network_admin_url(
									'plugin-install.php?tab=plugin-information&plugin=' . $plugin_data['slug'] .
									'&TB_iframe=true&width=600&height=550'
								)
							),
							/* translators: %s: Plugin name. */
							esc_attr( sprintf( __( 'More information about %s' ), $plugin_name ) ),
							esc_attr( $plugin_name ),
							__( 'View details' )
						);
					} elseif ( ! empty( $plugin_data['PluginURI'] ) ) {
						/* translators: %s: Plugin name. */
						$aria_label = sprintf( __( 'Visit plugin site for %s' ), $plugin_name );

						$plugin_meta[] = sprintf(
							'<a href="%s" aria-label="%s">%s</a>',
							esc_url( $plugin_data['PluginURI'] ),
							esc_attr( $aria_label ),
							__( 'Visit plugin site' )
						);
					}

					/**
					 * Filters the array of row meta for each plugin in the Plugins list table.
					 *
					 * @since 2.8.0
					 *
					 * @param string[] $plugin_meta An array of the plugin's metadata, including
					 *                              the version, author, author URI, and plugin URI.
					 * @param string   $plugin_file Path to the plugin file relative to the plugins directory.
					 * @param array    $plugin_data {
					 *     An array of plugin data.
					 *
					 *     @type string   $id               Plugin ID, e.g. `w.org/plugins/[plugin-name]`.
					 *     @type string   $slug             Plugin slug.
					 *     @type string   $plugin           Plugin basename.
					 *     @type string   $new_version      New plugin version.
					 *     @type string   $url              Plugin URL.
					 *     @type string   $package          Plugin update package URL.
					 *     @type string[] $icons            An array of plugin icon URLs.
					 *     @type string[] $banners          An array of plugin banner URLs.
					 *     @type string[] $banners_rtl      An array of plugin RTL banner URLs.
					 *     @type string   $requires         The version of WordPress which the plugin requires.
					 *     @type string   $tested           The version of WordPress the plugin is tested against.
					 *     @type string   $requires_php     The version of PHP which the plugin requires.
					 *     @type string   $upgrade_notice   The upgrade notice for the new plugin version.
					 *     @type bool     $update-supported Whether the plugin supports updates.
					 *     @type string   $Name             The human-readable name of the plugin.
					 *     @type string   $PluginURI        Plugin URI.
					 *     @type string   $Version          Plugin version.
					 *     @type string   $Description      Plugin description.
					 *     @type string   $Author           Plugin author.
					 *     @type string   $AuthorURI        Plugin author URI.
					 *     @type string   $TextDomain       Plugin textdomain.
					 *     @type string   $DomainPath       Relative path to the plugin's .mo file(s).
					 *     @type bool     $Network          Whether the plugin can only be activated network-wide.
					 *     @type string   $RequiresWP       The version of WordPress which the plugin requires.
					 *     @type string   $RequiresPHP      The version of PHP which the plugin requires.
					 *     @type string   $UpdateURI        ID of the plugin for update purposes, should be a URI.
					 *     @type string   $Title            The human-readable title of the plugin.
					 *     @type string   $AuthorName       Plugin author's name.
					 *     @type bool     $update           Whether there's an available update. Default null.
					 * }
					 * @param string   $status      Status filter currently applied to the plugin list. Possible
					 *                              values are: 'all', 'active', 'inactive', 'recently_activated',
					 *                              'upgrade', 'mustuse', 'dropins', 'search', 'paused',
					 *                              'auto-update-enabled', 'auto-update-disabled'.
					 */
					$plugin_meta = apply_filters( 'plugin_row_meta', $plugin_meta, $plugin_file, $plugin_data, $status );

					echo implode( ' | ', $plugin_meta );

					echo '</div>';

					if ( $has_dependents ) {
						$this->add_dependents_to_dependency_plugin_row( $plugin_file );
					}

					if ( WP_Plugin_Dependencies::has_dependencies( $plugin_file ) ) {
						$this->add_dependencies_to_dependent_plugin_row( $plugin_file );
					}

					/**
					 * Fires after plugin row meta.
					 *
					 * @since 6.5.0
					 *
					 * @param string $plugin_file Refer to {@see 'plugin_row_meta'} filter.
					 * @param array  $plugin_data Refer to {@see 'plugin_row_meta'} filter.
					 */
					do_action( 'after_plugin_row_meta', $plugin_file, $plugin_data );

					if ( $paused ) {
						$notice_text = __( 'This plugin failed to load properly and is paused during recovery mode.' );

						printf( '<p><span class="dashicons dashicons-warning"></span> <strong>%s</strong></p>', $notice_text );

						$error = wp_get_plugin_error( $plugin_file );

						if ( false !== $error ) {
							printf( '<div class="error-display"><p>%s</p></div>', wp_get_extension_error_description( $error ) );
						}
					}

					echo '</td>';
					break;
				case 'auto-updates':
					if ( ! $this->show_autoupdates || in_array( $status, array( 'mustuse', 'dropins' ), true ) ) {
						break;
					}

					echo "<td class='column-auto-updates{$extra_classes}'>";

					$html = array();

					if ( isset( $plugin_data['auto-update-forced'] ) ) {
						if ( $plugin_data['auto-update-forced'] ) {
							// Forced on.
							$text = __( 'Auto-updates enabled' );
						} else {
							$text = __( 'Auto-updates disabled' );
						}
						$action     = 'unavailable';
						$time_class = ' hidden';
					} elseif ( empty( $plugin_data['update-supported'] ) ) {
						$text       = '';
						$action     = 'unavailable';
						$time_class = ' hidden';
					} elseif ( in_array( $plugin_file, $auto_updates, true ) ) {
						$text       = __( 'Disable auto-updates' );
						$action     = 'disable';
						$time_class = '';
					} else {
						$text       = __( 'Enable auto-updates' );
						$action     = 'enable';
						$time_class = ' hidden';
					}

					$query_args = array(
						'action'        => "{$action}-auto-update",
						'plugin'        => $plugin_file,
						'paged'         => $page,
						'plugin_status' => $status,
					);

					$url = add_query_arg( $query_args, 'plugins.php' );

					if ( 'unavailable' === $action ) {
						$html[] = '<span class="label">' . $text . '</span>';
					} else {
						$html[] = sprintf(
							'<a href="%s" class="toggle-auto-update aria-button-if-js" data-wp-action="%s">',
							wp_nonce_url( $url, 'updates' ),
							$action
						);

						$html[] = '<span class="dashicons dashicons-update spin hidden" aria-hidden="true"></span>';
						$html[] = '<span class="label">' . $text . '</span>';
						$html[] = '</a>';
					}

					if ( ! empty( $plugin_data['update'] ) ) {
						$html[] = sprintf(
							'<div class="auto-update-time%s">%s</div>',
							$time_class,
							wp_get_auto_update_message()
						);
					}

					$html = implode( '', $html );

					/**
					 * Filters the HTML of the auto-updates setting for each plugin in the Plugins list table.
					 *
					 * @since 5.5.0
					 *
					 * @param string $html        The HTML of the plugin's auto-update column content,
					 *                            including toggle auto-update action links and
					 *                            time to next update.
					 * @param string $plugin_file Path to the plugin file relative to the plugins directory.
					 * @param array  $plugin_data An array of plugin data. See get_plugin_data()
					 *                            and the {@see 'plugin_row_meta'} filter for the list
					 *                            of possible values.
					 */
					echo apply_filters( 'plugin_auto_update_setting_html', $html, $plugin_file, $plugin_data );

					wp_admin_notice(
						'',
						array(
							'type'               => 'error',
							'additional_classes' => array( 'notice-alt', 'inline', 'hidden' ),
						)
					);

					echo '</td>';

					break;
				default:
					$classes = "$column_name column-$column_name $class";

					echo "<td class='$classes{$extra_classes}'>";

					/**
					 * Fires inside each custom column of the Plugins list table.
					 *
					 * @since 3.1.0
					 *
					 * @param string $column_name Name of the column.
					 * @param string $plugin_file Path to the plugin file relative to the plugins directory.
					 * @param array  $plugin_data An array of plugin data. See get_plugin_data()
					 *                            and the {@see 'plugin_row_meta'} filter for the list
					 *                            of possible values.
					 */
					do_action( 'manage_plugins_custom_column', $column_name, $plugin_file, $plugin_data );

					echo '</td>';
			}
		}

		echo '</tr>';

		if ( ! $compatible_php || ! $compatible_wp ) {
			printf(
				'<tr class="plugin-update-tr"><td colspan="%s" class="plugin-update colspanchange">',
				esc_attr( $this->get_column_count() )
			);

			$incompatible_message = '';
			if ( ! $compatible_php && ! $compatible_wp ) {
				$incompatible_message .= __( 'This plugin does not work with your versions of WordPress and PHP.' );
				if ( current_user_can( 'update_core' ) && current_user_can( 'update_php' ) ) {
					$incompatible_message .= sprintf(
						/* translators: 1: URL to WordPress Updates screen, 2: URL to Update PHP page. */
						' ' . __( '<a href="%1$s">Please update WordPress</a>, and then <a href="%2$s">learn more about updating PHP</a>.' ),
						self_admin_url( 'update-core.php' ),
						esc_url( wp_get_update_php_url() )
					);
					$incompatible_message .= wp_update_php_annotation( '</p><p><em>', '</em>', false );
				} elseif ( current_user_can( 'update_core' ) ) {
					$incompatible_message .= sprintf(
						/* translators: %s: URL to WordPress Updates screen. */
						' ' . __( '<a href="%s">Please update WordPress</a>.' ),
						self_admin_url( 'update-core.php' )
					);
				} elseif ( current_user_can( 'update_php' ) ) {
					$incompatible_message .= sprintf(
						/* translators: %s: URL to Update PHP page. */
						' ' . __( '<a href="%s">Learn more about updating PHP</a>.' ),
						esc_url( wp_get_update_php_url() )
					);
					$incompatible_message .= wp_update_php_annotation( '</p><p><em>', '</em>', false );
				}
			} elseif ( ! $compatible_wp ) {
				$incompatible_message .= __( 'This plugin does not work with your version of WordPress.' );
				if ( current_user_can( 'update_core' ) ) {
					$incompatible_message .= sprintf(
						/* translators: %s: URL to WordPress Updates screen. */
						' ' . __( '<a href="%s">Please update WordPress</a>.' ),
						self_admin_url( 'update-core.php' )
					);
				}
			} elseif ( ! $compatible_php ) {
				$incompatible_message .= __( 'This plugin does not work with your version of PHP.' );
				if ( current_user_can( 'update_php' ) ) {
					$incompatible_message .= sprintf(
						/* translators: %s: URL to Update PHP page. */
						' ' . __( '<a href="%s">Learn more about updating PHP</a>.' ),
						esc_url( wp_get_update_php_url() )
					);
					$incompatible_message .= wp_update_php_annotation( '</p><p><em>', '</em>', false );
				}
			}

			wp_admin_notice(
				$incompatible_message,
				array(
					'type'               => 'error',
					'additional_classes' => array( 'notice-alt', 'inline', 'update-message' ),
				)
			);

			echo '</td></tr>';
		}

		/**
		 * Fires after each row in the Plugins list table.
		 *
		 * @since 2.3.0
		 * @since 5.5.0 Added 'auto-update-enabled' and 'auto-update-disabled'
		 *              to possible values for `$status`.
		 *
		 * @param string $plugin_file Path to the plugin file relative to the plugins directory.
		 * @param array  $plugin_data An array of plugin data. See get_plugin_data()
		 *                            and the {@see 'plugin_row_meta'} filter for the list
		 *                            of possible values.
		 * @param string $status      Status filter currently applied to the plugin list.
		 *                            Possible values are: 'all', 'active', 'inactive',
		 *                            'recently_activated', 'upgrade', 'mustuse', 'dropins',
		 *                            'search', 'paused', 'auto-update-enabled', 'auto-update-disabled'.
		 */
		do_action( 'after_plugin_row', $plugin_file, $plugin_data, $status );

		/**
		 * Fires after each specific row in the Plugins list table.
		 *
		 * The dynamic portion of the hook name, `$plugin_file`, refers to the path
		 * to the plugin file, relative to the plugins directory.
		 *
		 * @since 2.7.0
		 * @since 5.5.0 Added 'auto-update-enabled' and 'auto-update-disabled'
		 *              to possible values for `$status`.
		 *
		 * @param string $plugin_file Path to the plugin file relative to the plugins directory.
		 * @param array  $plugin_data An array of plugin data. See get_plugin_data()
		 *                            and the {@see 'plugin_row_meta'} filter for the list
		 *                            of possible values.
		 * @param string $status      Status filter currently applied to the plugin list.
		 *                            Possible values are: 'all', 'active', 'inactive',
		 *                            'recently_activated', 'upgrade', 'mustuse', 'dropins',
		 *                            'search', 'paused', 'auto-update-enabled', 'auto-update-disabled'.
		 */
		do_action( "after_plugin_row_{$plugin_file}", $plugin_file, $plugin_data, $status );
	}

	/**
	 * Gets the name of the primary column for this specific list table.
	 *
	 * @since 4.3.0
	 *
	 * @return string Unalterable name for the primary column, in this case, 'name'.
	 */
	protected function get_primary_column_name() {
		return 'name';
	}

	/**
	 * Prints a list of other plugins that depend on the plugin.
	 *
	 * @since 6.5.0
	 *
	 * @param string $dependency The dependency's filepath, relative to the plugins directory.
	 */
	protected function add_dependents_to_dependency_plugin_row( $dependency ) {
		$dependent_names = WP_Plugin_Dependencies::get_dependent_names( $dependency );

		if ( empty( $dependent_names ) ) {
			return;
		}

		$dependency_note = __( 'Note: This plugin cannot be deactivated or deleted until the plugins that require it are deactivated or deleted.' );

		$comma       = wp_get_list_item_separator();
		$required_by = sprintf(
			/* translators: %s: List of dependencies. */
			__( '<strong>Required by:</strong> %s' ),
			implode( $comma, $dependent_names )
		);

		printf(
			'<div class="required-by"><p>%1$s</p><p>%2$s</p></div>',
			$required_by,
			$dependency_note
		);
	}

	/**
	 * Prints a list of other plugins that the plugin depends on.
	 *
	 * @since 6.5.0
	 *
	 * @param string $dependent The dependent plugin's filepath, relative to the plugins directory.
	 */
	protected function add_dependencies_to_dependent_plugin_row( $dependent ) {
		$dependency_names = WP_Plugin_Dependencies::get_dependency_names( $dependent );

		if ( array() === $dependency_names ) {
			return;
		}

		$links = array();
		foreach ( $dependency_names as $slug => $name ) {
			$links[] = $this->get_dependency_view_details_link( $name, $slug );
		}

		$is_active = is_multisite() ? is_plugin_active_for_network( $dependent ) : is_plugin_active( $dependent );
		$comma     = wp_get_list_item_separator();
		$requires  = sprintf(
			/* translators: %s: List of dependency names. */
			__( '<strong>Requires:</strong> %s' ),
			implode( $comma, $links )
		);

		$notice        = '';
		$error_message = '';
		if ( WP_Plugin_Dependencies::has_unmet_dependencies( $dependent ) ) {
			if ( $is_active ) {
				$error_message = __( 'This plugin is active but may not function correctly because required plugins are missing or inactive.' );
			} else {
				$error_message = __( 'This plugin cannot be activated because required plugins are missing or inactive.' );
			}
			$notice = wp_get_admin_notice(
				$error_message,
				array(
					'type'               => 'error',
					'additional_classes' => array( 'inline', 'notice-alt' ),
				)
			);
		}

		printf(
			'<div class="requires"><p>%1$s</p>%2$s</div>',
			$requires,
			$notice
		);
	}

	/**
	 * Returns a 'View details' like link for a dependency.
	 *
	 * @since 6.5.0
	 *
	 * @param string $name The dependency's name.
	 * @param string $slug The dependency's slug.
	 * @return string A 'View details' link for the dependency.
	 */
	protected function get_dependency_view_details_link( $name, $slug ) {
		$dependency_data = WP_Plugin_Dependencies::get_dependency_data( $slug );

		if ( false === $dependency_data
			|| $name === $slug
			|| $name !== $dependency_data['name']
			|| empty( $dependency_data['version'] )
		) {
			return $name;
		}

		return $this->get_view_details_link( $name, $slug );
	}

	/**
	 * Returns a 'View details' link for the plugin.
	 *
	 * @since 6.5.0
	 *
	 * @param string $name The plugin's name.
	 * @param string $slug The plugin's slug.
	 * @return string A 'View details' link for the plugin.
	 */
	protected function get_view_details_link( $name, $slug ) {
		$url = add_query_arg(
			array(
				'tab'       => 'plugin-information',
				'plugin'    => $slug,
				'TB_iframe' => 'true',
				'width'     => '600',
				'height'    => '550',
			),
			network_admin_url( 'plugin-install.php' )
		);

		$name_attr = esc_attr( $name );
		return sprintf(
			"<a href='%s' class='thickbox open-plugin-details-modal' aria-label='%s' data-title='%s'>%s</a>",
			esc_url( $url ),
			/* translators: %s: Plugin name. */
			sprintf( __( 'More information about %s' ), $name_attr ),
			$name_attr,
			esc_html( $name )
		);
	}
}
class-wp-theme-install-list-table.php000064400000036357151327343360013635 0ustar00<?php
/**
 * List Table API: WP_Theme_Install_List_Table class
 *
 * @package WordPress
 * @subpackage Administration
 * @since 3.1.0
 */

/**
 * Core class used to implement displaying themes to install in a list table.
 *
 * @since 3.1.0
 *
 * @see WP_Themes_List_Table
 */
class WP_Theme_Install_List_Table extends WP_Themes_List_Table {

	public $features = array();

	/**
	 * @return bool
	 */
	public function ajax_user_can() {
		return current_user_can( 'install_themes' );
	}

	/**
	 * @global array  $tabs
	 * @global string $tab
	 * @global int    $paged
	 * @global string $type
	 * @global array  $theme_field_defaults
	 */
	public function prepare_items() {
		require ABSPATH . 'wp-admin/includes/theme-install.php';

		global $tabs, $tab, $paged, $type, $theme_field_defaults;

		$tab = ! empty( $_REQUEST['tab'] ) ? sanitize_text_field( $_REQUEST['tab'] ) : '';

		$search_terms  = array();
		$search_string = '';
		if ( ! empty( $_REQUEST['s'] ) ) {
			$search_string = strtolower( wp_unslash( $_REQUEST['s'] ) );
			$search_terms  = array_unique( array_filter( array_map( 'trim', explode( ',', $search_string ) ) ) );
		}

		if ( ! empty( $_REQUEST['features'] ) ) {
			$this->features = $_REQUEST['features'];
		}

		$paged = $this->get_pagenum();

		$per_page = 36;

		// These are the tabs which are shown on the page,
		$tabs              = array();
		$tabs['dashboard'] = __( 'Search' );
		if ( 'search' === $tab ) {
			$tabs['search'] = __( 'Search Results' );
		}
		$tabs['upload']   = _x( 'Upload', 'noun' );
		$tabs['featured'] = _x( 'Featured', 'themes' );
		//$tabs['popular']  = _x( 'Popular', 'themes' );
		$tabs['new']     = _x( 'Latest', 'themes' );
		$tabs['updated'] = _x( 'Recently Updated', 'themes' );

		$nonmenu_tabs = array( 'theme-information' ); // Valid actions to perform which do not have a Menu item.

		/** This filter is documented in wp-admin/theme-install.php */
		$tabs = apply_filters( 'install_themes_tabs', $tabs );

		/**
		 * Filters tabs not associated with a menu item on the Install Themes screen.
		 *
		 * @since 2.8.0
		 *
		 * @param string[] $nonmenu_tabs The tabs that don't have a menu item on
		 *                               the Install Themes screen.
		 */
		$nonmenu_tabs = apply_filters( 'install_themes_nonmenu_tabs', $nonmenu_tabs );

		// If a non-valid menu tab has been selected, And it's not a non-menu action.
		if ( empty( $tab ) || ( ! isset( $tabs[ $tab ] ) && ! in_array( $tab, (array) $nonmenu_tabs, true ) ) ) {
			$tab = key( $tabs );
		}

		$args = array(
			'page'     => $paged,
			'per_page' => $per_page,
			'fields'   => $theme_field_defaults,
		);

		switch ( $tab ) {
			case 'search':
				$type = isset( $_REQUEST['type'] ) ? wp_unslash( $_REQUEST['type'] ) : 'term';
				switch ( $type ) {
					case 'tag':
						$args['tag'] = array_map( 'sanitize_key', $search_terms );
						break;
					case 'term':
						$args['search'] = $search_string;
						break;
					case 'author':
						$args['author'] = $search_string;
						break;
				}

				if ( ! empty( $this->features ) ) {
					$args['tag']      = $this->features;
					$_REQUEST['s']    = implode( ',', $this->features );
					$_REQUEST['type'] = 'tag';
				}

				add_action( 'install_themes_table_header', 'install_theme_search_form', 10, 0 );
				break;

			case 'featured':
				// case 'popular':
			case 'new':
			case 'updated':
				$args['browse'] = $tab;
				break;

			default:
				$args = false;
				break;
		}

		/**
		 * Filters API request arguments for each Install Themes screen tab.
		 *
		 * The dynamic portion of the hook name, `$tab`, refers to the theme install
		 * tab.
		 *
		 * Possible hook names include:
		 *
		 *  - `install_themes_table_api_args_dashboard`
		 *  - `install_themes_table_api_args_featured`
		 *  - `install_themes_table_api_args_new`
		 *  - `install_themes_table_api_args_search`
		 *  - `install_themes_table_api_args_updated`
		 *  - `install_themes_table_api_args_upload`
		 *
		 * @since 3.7.0
		 *
		 * @param array|false $args Theme install API arguments.
		 */
		$args = apply_filters( "install_themes_table_api_args_{$tab}", $args );

		if ( ! $args ) {
			return;
		}

		$api = themes_api( 'query_themes', $args );

		if ( is_wp_error( $api ) ) {
			wp_die( '<p>' . $api->get_error_message() . '</p> <p><a href="#" onclick="document.location.reload(); return false;">' . __( 'Try Again' ) . '</a></p>' );
		}

		$this->items = $api->themes;

		$this->set_pagination_args(
			array(
				'total_items'     => $api->info['results'],
				'per_page'        => $args['per_page'],
				'infinite_scroll' => true,
			)
		);
	}

	/**
	 */
	public function no_items() {
		_e( 'No themes match your request.' );
	}

	/**
	 * @global array $tabs
	 * @global string $tab
	 * @return array
	 */
	protected function get_views() {
		global $tabs, $tab;

		$display_tabs = array();
		foreach ( (array) $tabs as $action => $text ) {
			$display_tabs[ 'theme-install-' . $action ] = array(
				'url'     => self_admin_url( 'theme-install.php?tab=' . $action ),
				'label'   => $text,
				'current' => $action === $tab,
			);
		}

		return $this->get_views_links( $display_tabs );
	}

	/**
	 * Displays the theme install table.
	 *
	 * Overrides the parent display() method to provide a different container.
	 *
	 * @since 3.1.0
	 */
	public function display() {
		wp_nonce_field( 'fetch-list-' . get_class( $this ), '_ajax_fetch_list_nonce' );
		?>
		<div class="tablenav top themes">
			<div class="alignleft actions">
				<?php
				/**
				 * Fires in the Install Themes list table header.
				 *
				 * @since 2.8.0
				 */
				do_action( 'install_themes_table_header' );
				?>
			</div>
			<?php $this->pagination( 'top' ); ?>
			<br class="clear" />
		</div>

		<div id="availablethemes">
			<?php $this->display_rows_or_placeholder(); ?>
		</div>

		<?php
		$this->tablenav( 'bottom' );
	}

	/**
	 * Generates the list table rows.
	 *
	 * @since 3.1.0
	 */
	public function display_rows() {
		$themes = $this->items;
		foreach ( $themes as $theme ) {
			?>
				<div class="available-theme installable-theme">
				<?php
					$this->single_row( $theme );
				?>
				</div>
			<?php
		} // End foreach $theme_names.

		$this->theme_installer();
	}

	/**
	 * Prints a theme from the WordPress.org API.
	 *
	 * @since 3.1.0
	 *
	 * @global array $themes_allowedtags
	 *
	 * @param stdClass $theme {
	 *     An object that contains theme data returned by the WordPress.org API.
	 *
	 *     @type string $name           Theme name, e.g. 'Twenty Twenty-One'.
	 *     @type string $slug           Theme slug, e.g. 'twentytwentyone'.
	 *     @type string $version        Theme version, e.g. '1.1'.
	 *     @type string $author         Theme author username, e.g. 'melchoyce'.
	 *     @type string $preview_url    Preview URL, e.g. 'https://2021.wordpress.net/'.
	 *     @type string $screenshot_url Screenshot URL, e.g. 'https://wordpress.org/themes/twentytwentyone/'.
	 *     @type float  $rating         Rating score.
	 *     @type int    $num_ratings    The number of ratings.
	 *     @type string $homepage       Theme homepage, e.g. 'https://wordpress.org/themes/twentytwentyone/'.
	 *     @type string $description    Theme description.
	 *     @type string $download_link  Theme ZIP download URL.
	 * }
	 */
	public function single_row( $theme ) {
		global $themes_allowedtags;

		if ( empty( $theme ) ) {
			return;
		}

		$name   = wp_kses( $theme->name, $themes_allowedtags );
		$author = wp_kses( $theme->author, $themes_allowedtags );

		/* translators: %s: Theme name. */
		$preview_title = sprintf( __( 'Preview &#8220;%s&#8221;' ), $name );
		$preview_url   = add_query_arg(
			array(
				'tab'   => 'theme-information',
				'theme' => $theme->slug,
			),
			self_admin_url( 'theme-install.php' )
		);

		$actions = array();

		$install_url = add_query_arg(
			array(
				'action' => 'install-theme',
				'theme'  => $theme->slug,
			),
			self_admin_url( 'update.php' )
		);

		$update_url = add_query_arg(
			array(
				'action' => 'upgrade-theme',
				'theme'  => $theme->slug,
			),
			self_admin_url( 'update.php' )
		);

		$status = $this->_get_theme_status( $theme );

		switch ( $status ) {
			case 'update_available':
				$actions[] = sprintf(
					'<a class="install-now" href="%s" aria-label="%s">%s</a>',
					esc_url( wp_nonce_url( $update_url, 'upgrade-theme_' . $theme->slug ) ),
					/* translators: %s: Theme version. */
					esc_attr( sprintf( __( 'Update to version %s' ), $theme->version ) ),
					__( 'Update' )
				);
				break;
			case 'newer_installed':
			case 'latest_installed':
				$actions[] = sprintf(
					'<span class="install-now">%s</span>',
					_x( 'Installed', 'theme' )
				);
				break;
			case 'install':
			default:
				$actions[] = sprintf(
					'<a class="install-now" href="%s" aria-label="%s">%s</a>',
					esc_url( wp_nonce_url( $install_url, 'install-theme_' . $theme->slug ) ),
					/* translators: %s: Theme name. */
					esc_attr( sprintf( _x( 'Install %s', 'theme' ), $name ) ),
					_x( 'Install Now', 'theme' )
				);
				break;
		}

		$actions[] = sprintf(
			'<a class="install-theme-preview" href="%s" aria-label="%s">%s</a>',
			esc_url( $preview_url ),
			esc_attr( $preview_title ),
			__( 'Preview' )
		);

		/**
		 * Filters the install action links for a theme in the Install Themes list table.
		 *
		 * @since 3.4.0
		 *
		 * @param string[] $actions An array of theme action links. Defaults are
		 *                          links to Install Now, Preview, and Details.
		 * @param stdClass $theme   An object that contains theme data returned by the
		 *                          WordPress.org API.
		 */
		$actions = apply_filters( 'theme_install_actions', $actions, $theme );

		?>
		<a class="screenshot install-theme-preview" href="<?php echo esc_url( $preview_url ); ?>" aria-label="<?php echo esc_attr( $preview_title ); ?>">
			<img src="<?php echo esc_url( $theme->screenshot_url . '?ver=' . $theme->version ); ?>" width="150" alt="" />
		</a>

		<h3><?php echo $name; ?></h3>
		<div class="theme-author">
		<?php
			/* translators: %s: Theme author. */
			printf( __( 'By %s' ), $author );
		?>
		</div>

		<div class="action-links">
			<ul>
				<?php foreach ( $actions as $action ) : ?>
					<li><?php echo $action; ?></li>
				<?php endforeach; ?>
				<li class="hide-if-no-js"><a href="#" class="theme-detail"><?php _e( 'Details' ); ?></a></li>
			</ul>
		</div>

		<?php
		$this->install_theme_info( $theme );
	}

	/**
	 * Prints the wrapper for the theme installer.
	 */
	public function theme_installer() {
		?>
		<div id="theme-installer" class="wp-full-overlay expanded">
			<div class="wp-full-overlay-sidebar">
				<div class="wp-full-overlay-header">
					<a href="#" class="close-full-overlay button"><?php _e( 'Close' ); ?></a>
					<span class="theme-install"></span>
				</div>
				<div class="wp-full-overlay-sidebar-content">
					<div class="install-theme-info"></div>
				</div>
				<div class="wp-full-overlay-footer">
					<button type="button" class="collapse-sidebar button" aria-expanded="true" aria-label="<?php esc_attr_e( 'Collapse Sidebar' ); ?>">
						<span class="collapse-sidebar-arrow"></span>
						<span class="collapse-sidebar-label"><?php _e( 'Collapse' ); ?></span>
					</button>
				</div>
			</div>
			<div class="wp-full-overlay-main"></div>
		</div>
		<?php
	}

	/**
	 * Prints the wrapper for the theme installer with a provided theme's data.
	 * Used to make the theme installer work for no-js.
	 *
	 * @param stdClass $theme A WordPress.org Theme API object.
	 */
	public function theme_installer_single( $theme ) {
		?>
		<div id="theme-installer" class="wp-full-overlay single-theme">
			<div class="wp-full-overlay-sidebar">
				<?php $this->install_theme_info( $theme ); ?>
			</div>
			<div class="wp-full-overlay-main">
				<iframe src="<?php echo esc_url( $theme->preview_url ); ?>"></iframe>
			</div>
		</div>
		<?php
	}

	/**
	 * Prints the info for a theme (to be used in the theme installer modal).
	 *
	 * @global array $themes_allowedtags
	 *
	 * @param stdClass $theme A WordPress.org Theme API object.
	 */
	public function install_theme_info( $theme ) {
		global $themes_allowedtags;

		if ( empty( $theme ) ) {
			return;
		}

		$name   = wp_kses( $theme->name, $themes_allowedtags );
		$author = wp_kses( $theme->author, $themes_allowedtags );

		$install_url = add_query_arg(
			array(
				'action' => 'install-theme',
				'theme'  => $theme->slug,
			),
			self_admin_url( 'update.php' )
		);

		$update_url = add_query_arg(
			array(
				'action' => 'upgrade-theme',
				'theme'  => $theme->slug,
			),
			self_admin_url( 'update.php' )
		);

		$status = $this->_get_theme_status( $theme );

		?>
		<div class="install-theme-info">
		<?php
		switch ( $status ) {
			case 'update_available':
				printf(
					'<a class="theme-install button button-primary" href="%s" aria-label="%s">%s</a>',
					esc_url( wp_nonce_url( $update_url, 'upgrade-theme_' . $theme->slug ) ),
					/* translators: %s: Theme version. */
					esc_attr( sprintf( __( 'Update to version %s' ), $theme->version ) ),
					__( 'Update' )
				);
				break;
			case 'newer_installed':
			case 'latest_installed':
				printf(
					'<span class="theme-install">%s</span>',
					_x( 'Installed', 'theme' )
				);
				break;
			case 'install':
			default:
				printf(
					'<a class="theme-install button button-primary" href="%s">%s</a>',
					esc_url( wp_nonce_url( $install_url, 'install-theme_' . $theme->slug ) ),
					__( 'Install' )
				);
				break;
		}
		?>
			<h3 class="theme-name"><?php echo $name; ?></h3>
			<span class="theme-by">
			<?php
				/* translators: %s: Theme author. */
				printf( __( 'By %s' ), $author );
			?>
			</span>
			<?php if ( isset( $theme->screenshot_url ) ) : ?>
				<img class="theme-screenshot" src="<?php echo esc_url( $theme->screenshot_url . '?ver=' . $theme->version ); ?>" alt="" />
			<?php endif; ?>
			<div class="theme-details">
				<?php
				wp_star_rating(
					array(
						'rating' => $theme->rating,
						'type'   => 'percent',
						'number' => $theme->num_ratings,
					)
				);
				?>
				<div class="theme-version">
					<strong><?php _e( 'Version:' ); ?> </strong>
					<?php echo wp_kses( $theme->version, $themes_allowedtags ); ?>
				</div>
				<div class="theme-description">
					<?php echo wp_kses( $theme->description, $themes_allowedtags ); ?>
				</div>
			</div>
			<input class="theme-preview-url" type="hidden" value="<?php echo esc_url( $theme->preview_url ); ?>" />
		</div>
		<?php
	}

	/**
	 * Send required variables to JavaScript land
	 *
	 * @since 3.4.0
	 *
	 * @global string $tab  Current tab within Themes->Install screen
	 * @global string $type Type of search.
	 *
	 * @param array $extra_args Unused.
	 */
	public function _js_vars( $extra_args = array() ) {
		global $tab, $type;
		parent::_js_vars( compact( 'tab', 'type' ) );
	}

	/**
	 * Checks to see if the theme is already installed.
	 *
	 * @since 3.4.0
	 *
	 * @param stdClass $theme A WordPress.org Theme API object.
	 * @return string Theme status.
	 */
	private function _get_theme_status( $theme ) {
		$status = 'install';

		$installed_theme = wp_get_theme( $theme->slug );
		if ( $installed_theme->exists() ) {
			if ( version_compare( $installed_theme->get( 'Version' ), $theme->version, '=' ) ) {
				$status = 'latest_installed';
			} elseif ( version_compare( $installed_theme->get( 'Version' ), $theme->version, '>' ) ) {
				$status = 'newer_installed';
			} else {
				$status = 'update_available';
			}
		}

		return $status;
	}
}
theme-install.php000060400000015516151327343360010036 0ustar00<?php
/**
 * WordPress Theme Installation Administration API
 *
 * @package WordPress
 * @subpackage Administration
 */

$themes_allowedtags = array(
	'a'       => array(
		'href'   => array(),
		'title'  => array(),
		'target' => array(),
	),
	'abbr'    => array( 'title' => array() ),
	'acronym' => array( 'title' => array() ),
	'code'    => array(),
	'pre'     => array(),
	'em'      => array(),
	'strong'  => array(),
	'div'     => array(),
	'p'       => array(),
	'ul'      => array(),
	'ol'      => array(),
	'li'      => array(),
	'h1'      => array(),
	'h2'      => array(),
	'h3'      => array(),
	'h4'      => array(),
	'h5'      => array(),
	'h6'      => array(),
	'img'     => array(
		'src'   => array(),
		'class' => array(),
		'alt'   => array(),
	),
);

$theme_field_defaults = array(
	'description'  => true,
	'sections'     => false,
	'tested'       => true,
	'requires'     => true,
	'rating'       => true,
	'downloaded'   => true,
	'downloadlink' => true,
	'last_updated' => true,
	'homepage'     => true,
	'tags'         => true,
	'num_ratings'  => true,
);

/**
 * Retrieves the list of WordPress theme features (aka theme tags).
 *
 * @since 2.8.0
 *
 * @deprecated 3.1.0 Use get_theme_feature_list() instead.
 *
 * @return array
 */
function install_themes_feature_list() {
	_deprecated_function( __FUNCTION__, '3.1.0', 'get_theme_feature_list()' );

	$cache = get_transient( 'wporg_theme_feature_list' );
	if ( ! $cache ) {
		set_transient( 'wporg_theme_feature_list', array(), 3 * HOUR_IN_SECONDS );
	}

	if ( $cache ) {
		return $cache;
	}

	$feature_list = themes_api( 'feature_list', array() );
	if ( is_wp_error( $feature_list ) ) {
		return array();
	}

	set_transient( 'wporg_theme_feature_list', $feature_list, 3 * HOUR_IN_SECONDS );

	return $feature_list;
}

/**
 * Displays search form for searching themes.
 *
 * @since 2.8.0
 *
 * @param bool $type_selector
 */
function install_theme_search_form( $type_selector = true ) {
	$type = isset( $_REQUEST['type'] ) ? wp_unslash( $_REQUEST['type'] ) : 'term';
	$term = isset( $_REQUEST['s'] ) ? wp_unslash( $_REQUEST['s'] ) : '';
	if ( ! $type_selector ) {
		echo '<p class="install-help">' . __( 'Search for themes by keyword.' ) . '</p>';
	}
	?>
<form id="search-themes" method="get">
	<input type="hidden" name="tab" value="search" />
	<?php if ( $type_selector ) : ?>
	<label class="screen-reader-text" for="typeselector">
		<?php
		/* translators: Hidden accessibility text. */
		_e( 'Type of search' );
		?>
	</label>
	<select	name="type" id="typeselector">
	<option value="term" <?php selected( 'term', $type ); ?>><?php _e( 'Keyword' ); ?></option>
	<option value="author" <?php selected( 'author', $type ); ?>><?php _e( 'Author' ); ?></option>
	<option value="tag" <?php selected( 'tag', $type ); ?>><?php _ex( 'Tag', 'Theme Installer' ); ?></option>
	</select>
	<label class="screen-reader-text" for="s">
		<?php
		switch ( $type ) {
			case 'term':
				/* translators: Hidden accessibility text. */
				_e( 'Search by keyword' );
				break;
			case 'author':
				/* translators: Hidden accessibility text. */
				_e( 'Search by author' );
				break;
			case 'tag':
				/* translators: Hidden accessibility text. */
				_e( 'Search by tag' );
				break;
		}
		?>
	</label>
	<?php else : ?>
	<label class="screen-reader-text" for="s">
		<?php
		/* translators: Hidden accessibility text. */
		_e( 'Search by keyword' );
		?>
	</label>
	<?php endif; ?>
	<input type="search" name="s" id="s" size="30" value="<?php echo esc_attr( $term ); ?>" autofocus="autofocus" />
	<?php submit_button( __( 'Search' ), '', 'search', false ); ?>
</form>
	<?php
}

/**
 * Displays tags filter for themes.
 *
 * @since 2.8.0
 */
function install_themes_dashboard() {
	install_theme_search_form( false );
	?>
<h4><?php _e( 'Feature Filter' ); ?></h4>
<p class="install-help"><?php _e( 'Find a theme based on specific features.' ); ?></p>

<form method="get">
	<input type="hidden" name="tab" value="search" />
	<?php
	$feature_list = get_theme_feature_list();
	echo '<div class="feature-filter">';

	foreach ( (array) $feature_list as $feature_name => $features ) {
		$feature_name = esc_html( $feature_name );
		echo '<div class="feature-name">' . $feature_name . '</div>';

		echo '<ol class="feature-group">';
		foreach ( $features as $feature => $feature_name ) {
			$feature_name = esc_html( $feature_name );
			$feature      = esc_attr( $feature );
			?>

<li>
	<input type="checkbox" name="features[]" id="feature-id-<?php echo $feature; ?>" value="<?php echo $feature; ?>" />
	<label for="feature-id-<?php echo $feature; ?>"><?php echo $feature_name; ?></label>
</li>

<?php	} ?>
</ol>
<br class="clear" />
		<?php
	}
	?>

</div>
<br class="clear" />
	<?php submit_button( __( 'Find Themes' ), '', 'search' ); ?>
</form>
	<?php
}

/**
 * Displays a form to upload themes from zip files.
 *
 * @since 2.8.0
 */
function install_themes_upload() {
	?>
<p class="install-help"><?php _e( 'If you have a theme in a .zip format, you may install or update it by uploading it here.' ); ?></p>
<form method="post" enctype="multipart/form-data" class="wp-upload-form" action="<?php echo esc_url( self_admin_url( 'update.php?action=upload-theme' ) ); ?>">
	<?php wp_nonce_field( 'theme-upload' ); ?>
	<label class="screen-reader-text" for="themezip">
		<?php
		/* translators: Hidden accessibility text. */
		_e( 'Theme zip file' );
		?>
	</label>
	<input type="file" id="themezip" name="themezip" accept=".zip" />
	<?php submit_button( _x( 'Install Now', 'theme' ), '', 'install-theme-submit', false ); ?>
</form>
	<?php
}

/**
 * Prints a theme on the Install Themes pages.
 *
 * @deprecated 3.4.0
 *
 * @global WP_Theme_Install_List_Table $wp_list_table
 *
 * @param object $theme
 */
function display_theme( $theme ) {
	_deprecated_function( __FUNCTION__, '3.4.0' );
	global $wp_list_table;
	if ( ! isset( $wp_list_table ) ) {
		$wp_list_table = _get_list_table( 'WP_Theme_Install_List_Table' );
	}
	$wp_list_table->prepare_items();
	$wp_list_table->single_row( $theme );
}

/**
 * Displays theme content based on theme list.
 *
 * @since 2.8.0
 *
 * @global WP_Theme_Install_List_Table $wp_list_table
 */
function display_themes() {
	global $wp_list_table;

	if ( ! isset( $wp_list_table ) ) {
		$wp_list_table = _get_list_table( 'WP_Theme_Install_List_Table' );
	}
	$wp_list_table->prepare_items();
	$wp_list_table->display();
}

/**
 * Displays theme information in dialog box form.
 *
 * @since 2.8.0
 *
 * @global WP_Theme_Install_List_Table $wp_list_table
 */
function install_theme_information() {
	global $wp_list_table;

	$theme = themes_api( 'theme_information', array( 'slug' => wp_unslash( $_REQUEST['theme'] ) ) );

	if ( is_wp_error( $theme ) ) {
		wp_die( $theme );
	}

	iframe_header( __( 'Theme Installation' ) );
	if ( ! isset( $wp_list_table ) ) {
		$wp_list_table = _get_list_table( 'WP_Theme_Install_List_Table' );
	}
	$wp_list_table->theme_installer_single( $theme );
	iframe_footer();
	exit;
}
class-theme-upgrader.php000060400000064116151327343360011304 0ustar00<?php
/**
 * Upgrade API: Theme_Upgrader class
 *
 * @package WordPress
 * @subpackage Upgrader
 * @since 4.6.0
 */

/**
 * Core class used for upgrading/installing themes.
 *
 * It is designed to upgrade/install themes from a local zip, remote zip URL,
 * or uploaded zip file.
 *
 * @since 2.8.0
 * @since 4.6.0 Moved to its own file from wp-admin/includes/class-wp-upgrader.php.
 *
 * @see WP_Upgrader
 */
class Theme_Upgrader extends WP_Upgrader {

	/**
	 * Result of the theme upgrade offer.
	 *
	 * @since 2.8.0
	 * @var array|WP_Error $result
	 * @see WP_Upgrader::$result
	 */
	public $result;

	/**
	 * Whether multiple themes are being upgraded/installed in bulk.
	 *
	 * @since 2.9.0
	 * @var bool $bulk
	 */
	public $bulk = false;

	/**
	 * New theme info.
	 *
	 * @since 5.5.0
	 * @var array $new_theme_data
	 *
	 * @see check_package()
	 */
	public $new_theme_data = array();

	/**
	 * Initializes the upgrade strings.
	 *
	 * @since 2.8.0
	 */
	public function upgrade_strings() {
		$this->strings['up_to_date'] = __( 'The theme is at the latest version.' );
		$this->strings['no_package'] = __( 'Update package not available.' );
		/* translators: %s: Package URL. */
		$this->strings['downloading_package'] = sprintf( __( 'Downloading update from %s&#8230;' ), '<span class="code pre">%s</span>' );
		$this->strings['unpack_package']      = __( 'Unpacking the update&#8230;' );
		$this->strings['remove_old']          = __( 'Removing the old version of the theme&#8230;' );
		$this->strings['remove_old_failed']   = __( 'Could not remove the old theme.' );
		$this->strings['process_failed']      = __( 'Theme update failed.' );
		$this->strings['process_success']     = __( 'Theme updated successfully.' );
	}

	/**
	 * Initializes the installation strings.
	 *
	 * @since 2.8.0
	 */
	public function install_strings() {
		$this->strings['no_package'] = __( 'Installation package not available.' );
		/* translators: %s: Package URL. */
		$this->strings['downloading_package'] = sprintf( __( 'Downloading installation package from %s&#8230;' ), '<span class="code pre">%s</span>' );
		$this->strings['unpack_package']      = __( 'Unpacking the package&#8230;' );
		$this->strings['installing_package']  = __( 'Installing the theme&#8230;' );
		$this->strings['remove_old']          = __( 'Removing the old version of the theme&#8230;' );
		$this->strings['remove_old_failed']   = __( 'Could not remove the old theme.' );
		$this->strings['no_files']            = __( 'The theme contains no files.' );
		$this->strings['process_failed']      = __( 'Theme installation failed.' );
		$this->strings['process_success']     = __( 'Theme installed successfully.' );
		/* translators: 1: Theme name, 2: Theme version. */
		$this->strings['process_success_specific'] = __( 'Successfully installed the theme <strong>%1$s %2$s</strong>.' );
		$this->strings['parent_theme_search']      = __( 'This theme requires a parent theme. Checking if it is installed&#8230;' );
		/* translators: 1: Theme name, 2: Theme version. */
		$this->strings['parent_theme_prepare_install'] = __( 'Preparing to install <strong>%1$s %2$s</strong>&#8230;' );
		/* translators: 1: Theme name, 2: Theme version. */
		$this->strings['parent_theme_currently_installed'] = __( 'The parent theme, <strong>%1$s %2$s</strong>, is currently installed.' );
		/* translators: 1: Theme name, 2: Theme version. */
		$this->strings['parent_theme_install_success'] = __( 'Successfully installed the parent theme, <strong>%1$s %2$s</strong>.' );
		/* translators: %s: Theme name. */
		$this->strings['parent_theme_not_found'] = sprintf( __( '<strong>The parent theme could not be found.</strong> You will need to install the parent theme, %s, before you can use this child theme.' ), '<strong>%s</strong>' );
		/* translators: %s: Theme error. */
		$this->strings['current_theme_has_errors'] = __( 'The active theme has the following error: "%s".' );

		if ( ! empty( $this->skin->overwrite ) ) {
			if ( 'update-theme' === $this->skin->overwrite ) {
				$this->strings['installing_package'] = __( 'Updating the theme&#8230;' );
				$this->strings['process_failed']     = __( 'Theme update failed.' );
				$this->strings['process_success']    = __( 'Theme updated successfully.' );
			}

			if ( 'downgrade-theme' === $this->skin->overwrite ) {
				$this->strings['installing_package'] = __( 'Downgrading the theme&#8230;' );
				$this->strings['process_failed']     = __( 'Theme downgrade failed.' );
				$this->strings['process_success']    = __( 'Theme downgraded successfully.' );
			}
		}
	}

	/**
	 * Checks if a child theme is being installed and its parent also needs to be installed.
	 *
	 * Hooked to the {@see 'upgrader_post_install'} filter by Theme_Upgrader::install().
	 *
	 * @since 3.4.0
	 *
	 * @param bool  $install_result
	 * @param array $hook_extra
	 * @param array $child_result
	 * @return bool
	 */
	public function check_parent_theme_filter( $install_result, $hook_extra, $child_result ) {
		// Check to see if we need to install a parent theme.
		$theme_info = $this->theme_info();

		if ( ! $theme_info->parent() ) {
			return $install_result;
		}

		$this->skin->feedback( 'parent_theme_search' );

		if ( ! $theme_info->parent()->errors() ) {
			$this->skin->feedback( 'parent_theme_currently_installed', $theme_info->parent()->display( 'Name' ), $theme_info->parent()->display( 'Version' ) );
			// We already have the theme, fall through.
			return $install_result;
		}

		// We don't have the parent theme, let's install it.
		$api = themes_api(
			'theme_information',
			array(
				'slug'   => $theme_info->get( 'Template' ),
				'fields' => array(
					'sections' => false,
					'tags'     => false,
				),
			)
		); // Save on a bit of bandwidth.

		if ( ! $api || is_wp_error( $api ) ) {
			$this->skin->feedback( 'parent_theme_not_found', $theme_info->get( 'Template' ) );
			// Don't show activate or preview actions after installation.
			add_filter( 'install_theme_complete_actions', array( $this, 'hide_activate_preview_actions' ) );
			return $install_result;
		}

		// Backup required data we're going to override:
		$child_api             = $this->skin->api;
		$child_success_message = $this->strings['process_success'];

		// Override them.
		$this->skin->api = $api;

		$this->strings['process_success_specific'] = $this->strings['parent_theme_install_success'];

		$this->skin->feedback( 'parent_theme_prepare_install', $api->name, $api->version );

		add_filter( 'install_theme_complete_actions', '__return_false', 999 ); // Don't show any actions after installing the theme.

		// Install the parent theme.
		$parent_result = $this->run(
			array(
				'package'           => $api->download_link,
				'destination'       => get_theme_root(),
				'clear_destination' => false, // Do not overwrite files.
				'clear_working'     => true,
			)
		);

		if ( is_wp_error( $parent_result ) ) {
			add_filter( 'install_theme_complete_actions', array( $this, 'hide_activate_preview_actions' ) );
		}

		// Start cleaning up after the parent's installation.
		remove_filter( 'install_theme_complete_actions', '__return_false', 999 );

		// Reset child's result and data.
		$this->result                     = $child_result;
		$this->skin->api                  = $child_api;
		$this->strings['process_success'] = $child_success_message;

		return $install_result;
	}

	/**
	 * Don't display the activate and preview actions to the user.
	 *
	 * Hooked to the {@see 'install_theme_complete_actions'} filter by
	 * Theme_Upgrader::check_parent_theme_filter() when installing
	 * a child theme and installing the parent theme fails.
	 *
	 * @since 3.4.0
	 *
	 * @param array $actions Preview actions.
	 * @return array
	 */
	public function hide_activate_preview_actions( $actions ) {
		unset( $actions['activate'], $actions['preview'] );
		return $actions;
	}

	/**
	 * Install a theme package.
	 *
	 * @since 2.8.0
	 * @since 3.7.0 The `$args` parameter was added, making clearing the update cache optional.
	 *
	 * @param string $package The full local path or URI of the package.
	 * @param array  $args {
	 *     Optional. Other arguments for installing a theme package. Default empty array.
	 *
	 *     @type bool $clear_update_cache Whether to clear the updates cache if successful.
	 *                                    Default true.
	 * }
	 *
	 * @return bool|WP_Error True if the installation was successful, false or a WP_Error object otherwise.
	 */
	public function install( $package, $args = array() ) {
		$defaults    = array(
			'clear_update_cache' => true,
			'overwrite_package'  => false, // Do not overwrite files.
		);
		$parsed_args = wp_parse_args( $args, $defaults );

		$this->init();
		$this->install_strings();

		add_filter( 'upgrader_source_selection', array( $this, 'check_package' ) );
		add_filter( 'upgrader_post_install', array( $this, 'check_parent_theme_filter' ), 10, 3 );

		if ( $parsed_args['clear_update_cache'] ) {
			// Clear cache so wp_update_themes() knows about the new theme.
			add_action( 'upgrader_process_complete', 'wp_clean_themes_cache', 9, 0 );
		}

		$this->run(
			array(
				'package'           => $package,
				'destination'       => get_theme_root(),
				'clear_destination' => $parsed_args['overwrite_package'],
				'clear_working'     => true,
				'hook_extra'        => array(
					'type'   => 'theme',
					'action' => 'install',
				),
			)
		);

		remove_action( 'upgrader_process_complete', 'wp_clean_themes_cache', 9 );
		remove_filter( 'upgrader_source_selection', array( $this, 'check_package' ) );
		remove_filter( 'upgrader_post_install', array( $this, 'check_parent_theme_filter' ) );

		if ( ! $this->result || is_wp_error( $this->result ) ) {
			return $this->result;
		}

		// Refresh the Theme Update information.
		wp_clean_themes_cache( $parsed_args['clear_update_cache'] );

		if ( $parsed_args['overwrite_package'] ) {
			/** This action is documented in wp-admin/includes/class-plugin-upgrader.php */
			do_action( 'upgrader_overwrote_package', $package, $this->new_theme_data, 'theme' );
		}

		return true;
	}

	/**
	 * Upgrades a theme.
	 *
	 * @since 2.8.0
	 * @since 3.7.0 The `$args` parameter was added, making clearing the update cache optional.
	 *
	 * @param string $theme The theme slug.
	 * @param array  $args {
	 *     Optional. Other arguments for upgrading a theme. Default empty array.
	 *
	 *     @type bool $clear_update_cache Whether to clear the update cache if successful.
	 *                                    Default true.
	 * }
	 * @return bool|WP_Error True if the upgrade was successful, false or a WP_Error object otherwise.
	 */
	public function upgrade( $theme, $args = array() ) {
		$defaults    = array(
			'clear_update_cache' => true,
		);
		$parsed_args = wp_parse_args( $args, $defaults );

		$this->init();
		$this->upgrade_strings();

		// Is an update available?
		$current = get_site_transient( 'update_themes' );
		if ( ! isset( $current->response[ $theme ] ) ) {
			$this->skin->before();
			$this->skin->set_result( false );
			$this->skin->error( 'up_to_date' );
			$this->skin->after();
			return false;
		}

		$r = $current->response[ $theme ];

		add_filter( 'upgrader_pre_install', array( $this, 'current_before' ), 10, 2 );
		add_filter( 'upgrader_post_install', array( $this, 'current_after' ), 10, 2 );
		add_filter( 'upgrader_clear_destination', array( $this, 'delete_old_theme' ), 10, 4 );
		if ( $parsed_args['clear_update_cache'] ) {
			// Clear cache so wp_update_themes() knows about the new theme.
			add_action( 'upgrader_process_complete', 'wp_clean_themes_cache', 9, 0 );
		}

		$this->run(
			array(
				'package'           => $r['package'],
				'destination'       => get_theme_root( $theme ),
				'clear_destination' => true,
				'clear_working'     => true,
				'hook_extra'        => array(
					'theme'       => $theme,
					'type'        => 'theme',
					'action'      => 'update',
					'temp_backup' => array(
						'slug' => $theme,
						'src'  => get_theme_root( $theme ),
						'dir'  => 'themes',
					),
				),
			)
		);

		remove_action( 'upgrader_process_complete', 'wp_clean_themes_cache', 9 );
		remove_filter( 'upgrader_pre_install', array( $this, 'current_before' ) );
		remove_filter( 'upgrader_post_install', array( $this, 'current_after' ) );
		remove_filter( 'upgrader_clear_destination', array( $this, 'delete_old_theme' ) );

		if ( ! $this->result || is_wp_error( $this->result ) ) {
			return $this->result;
		}

		wp_clean_themes_cache( $parsed_args['clear_update_cache'] );

		/*
		 * Ensure any future auto-update failures trigger a failure email by removing
		 * the last failure notification from the list when themes update successfully.
		 */
		$past_failure_emails = get_option( 'auto_plugin_theme_update_emails', array() );

		if ( isset( $past_failure_emails[ $theme ] ) ) {
			unset( $past_failure_emails[ $theme ] );
			update_option( 'auto_plugin_theme_update_emails', $past_failure_emails );
		}

		return true;
	}

	/**
	 * Upgrades several themes at once.
	 *
	 * @since 3.0.0
	 * @since 3.7.0 The `$args` parameter was added, making clearing the update cache optional.
	 *
	 * @param string[] $themes Array of the theme slugs.
	 * @param array    $args {
	 *     Optional. Other arguments for upgrading several themes at once. Default empty array.
	 *
	 *     @type bool $clear_update_cache Whether to clear the update cache if successful.
	 *                                    Default true.
	 * }
	 * @return array[]|false An array of results, or false if unable to connect to the filesystem.
	 */
	public function bulk_upgrade( $themes, $args = array() ) {
		$wp_version  = wp_get_wp_version();
		$defaults    = array(
			'clear_update_cache' => true,
		);
		$parsed_args = wp_parse_args( $args, $defaults );

		$this->init();
		$this->bulk = true;
		$this->upgrade_strings();

		$current = get_site_transient( 'update_themes' );

		add_filter( 'upgrader_pre_install', array( $this, 'current_before' ), 10, 2 );
		add_filter( 'upgrader_post_install', array( $this, 'current_after' ), 10, 2 );
		add_filter( 'upgrader_clear_destination', array( $this, 'delete_old_theme' ), 10, 4 );

		$this->skin->header();

		// Connect to the filesystem first.
		$res = $this->fs_connect( array( WP_CONTENT_DIR ) );
		if ( ! $res ) {
			$this->skin->footer();
			return false;
		}

		$this->skin->bulk_header();

		/*
		 * Only start maintenance mode if:
		 * - running Multisite and there are one or more themes specified, OR
		 * - a theme with an update available is currently in use.
		 * @todo For multisite, maintenance mode should only kick in for individual sites if at all possible.
		 */
		$maintenance = ( is_multisite() && ! empty( $themes ) );
		foreach ( $themes as $theme ) {
			$maintenance = $maintenance || get_stylesheet() === $theme || get_template() === $theme;
		}
		if ( $maintenance ) {
			$this->maintenance_mode( true );
		}

		$results = array();

		$this->update_count   = count( $themes );
		$this->update_current = 0;
		foreach ( $themes as $theme ) {
			++$this->update_current;

			$this->skin->theme_info = $this->theme_info( $theme );

			if ( ! isset( $current->response[ $theme ] ) ) {
				$this->skin->set_result( true );
				$this->skin->before();
				$this->skin->feedback( 'up_to_date' );
				$this->skin->after();
				$results[ $theme ] = true;
				continue;
			}

			// Get the URL to the zip file.
			$r = $current->response[ $theme ];

			if ( isset( $r['requires'] ) && ! is_wp_version_compatible( $r['requires'] ) ) {
				$result = new WP_Error(
					'incompatible_wp_required_version',
					sprintf(
						/* translators: 1: Current WordPress version, 2: WordPress version required by the new theme version. */
						__( 'Your WordPress version is %1$s, however the new theme version requires %2$s.' ),
						$wp_version,
						$r['requires']
					)
				);

				$this->skin->before( $result );
				$this->skin->error( $result );
				$this->skin->after();
			} elseif ( isset( $r['requires_php'] ) && ! is_php_version_compatible( $r['requires_php'] ) ) {
				$result = new WP_Error(
					'incompatible_php_required_version',
					sprintf(
						/* translators: 1: Current PHP version, 2: PHP version required by the new theme version. */
						__( 'The PHP version on your server is %1$s, however the new theme version requires %2$s.' ),
						PHP_VERSION,
						$r['requires_php']
					)
				);

				$this->skin->before( $result );
				$this->skin->error( $result );
				$this->skin->after();
			} else {
				add_filter( 'upgrader_source_selection', array( $this, 'check_package' ) );
				$result = $this->run(
					array(
						'package'           => $r['package'],
						'destination'       => get_theme_root( $theme ),
						'clear_destination' => true,
						'clear_working'     => true,
						'is_multi'          => true,
						'hook_extra'        => array(
							'theme'       => $theme,
							'temp_backup' => array(
								'slug' => $theme,
								'src'  => get_theme_root( $theme ),
								'dir'  => 'themes',
							),
						),
					)
				);
				remove_filter( 'upgrader_source_selection', array( $this, 'check_package' ) );
			}

			$results[ $theme ] = $result;

			// Prevent credentials auth screen from displaying multiple times.
			if ( false === $result ) {
				break;
			}
		} // End foreach $themes.

		$this->maintenance_mode( false );

		// Refresh the Theme Update information.
		wp_clean_themes_cache( $parsed_args['clear_update_cache'] );

		/** This action is documented in wp-admin/includes/class-wp-upgrader.php */
		do_action(
			'upgrader_process_complete',
			$this,
			array(
				'action' => 'update',
				'type'   => 'theme',
				'bulk'   => true,
				'themes' => $themes,
			)
		);

		$this->skin->bulk_footer();

		$this->skin->footer();

		// Cleanup our hooks, in case something else does an upgrade on this connection.
		remove_filter( 'upgrader_pre_install', array( $this, 'current_before' ) );
		remove_filter( 'upgrader_post_install', array( $this, 'current_after' ) );
		remove_filter( 'upgrader_clear_destination', array( $this, 'delete_old_theme' ) );

		/*
		 * Ensure any future auto-update failures trigger a failure email by removing
		 * the last failure notification from the list when themes update successfully.
		 */
		$past_failure_emails = get_option( 'auto_plugin_theme_update_emails', array() );

		foreach ( $results as $theme => $result ) {
			// Maintain last failure notification when themes failed to update manually.
			if ( ! $result || is_wp_error( $result ) || ! isset( $past_failure_emails[ $theme ] ) ) {
				continue;
			}

			unset( $past_failure_emails[ $theme ] );
		}

		update_option( 'auto_plugin_theme_update_emails', $past_failure_emails );

		return $results;
	}

	/**
	 * Checks that the package source contains a valid theme.
	 *
	 * Hooked to the {@see 'upgrader_source_selection'} filter by Theme_Upgrader::install().
	 *
	 * @since 3.3.0
	 *
	 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
	 *
	 * @param string $source The path to the downloaded package source.
	 * @return string|WP_Error The source as passed, or a WP_Error object on failure.
	 */
	public function check_package( $source ) {
		global $wp_filesystem;

		$wp_version           = wp_get_wp_version();
		$this->new_theme_data = array();

		if ( is_wp_error( $source ) ) {
			return $source;
		}

		// Check that the folder contains a valid theme.
		$working_directory = str_replace( $wp_filesystem->wp_content_dir(), trailingslashit( WP_CONTENT_DIR ), $source );
		if ( ! is_dir( $working_directory ) ) { // Confidence check, if the above fails, let's not prevent installation.
			return $source;
		}

		// A proper archive should have a style.css file in the single subdirectory.
		if ( ! file_exists( $working_directory . 'style.css' ) ) {
			return new WP_Error(
				'incompatible_archive_theme_no_style',
				$this->strings['incompatible_archive'],
				sprintf(
					/* translators: %s: style.css */
					__( 'The theme is missing the %s stylesheet.' ),
					'<code>style.css</code>'
				)
			);
		}

		// All these headers are needed on Theme_Installer_Skin::do_overwrite().
		$info = get_file_data(
			$working_directory . 'style.css',
			array(
				'Name'        => 'Theme Name',
				'Version'     => 'Version',
				'Author'      => 'Author',
				'Template'    => 'Template',
				'RequiresWP'  => 'Requires at least',
				'RequiresPHP' => 'Requires PHP',
			)
		);

		if ( empty( $info['Name'] ) ) {
			return new WP_Error(
				'incompatible_archive_theme_no_name',
				$this->strings['incompatible_archive'],
				sprintf(
					/* translators: %s: style.css */
					__( 'The %s stylesheet does not contain a valid theme header.' ),
					'<code>style.css</code>'
				)
			);
		}

		/*
		 * Parent themes must contain an index file:
		 * - classic themes require /index.php
		 * - block themes require /templates/index.html or block-templates/index.html (deprecated 5.9.0).
		 */
		if (
			empty( $info['Template'] ) &&
			! file_exists( $working_directory . 'index.php' ) &&
			! file_exists( $working_directory . 'templates/index.html' ) &&
			! file_exists( $working_directory . 'block-templates/index.html' )
		) {
			return new WP_Error(
				'incompatible_archive_theme_no_index',
				$this->strings['incompatible_archive'],
				sprintf(
					/* translators: 1: templates/index.html, 2: index.php, 3: Documentation URL, 4: Template, 5: style.css */
					__( 'Template is missing. Standalone themes need to have a %1$s or %2$s template file. <a href="%3$s">Child themes</a> need to have a %4$s header in the %5$s stylesheet.' ),
					'<code>templates/index.html</code>',
					'<code>index.php</code>',
					__( 'https://developer.wordpress.org/themes/advanced-topics/child-themes/' ),
					'<code>Template</code>',
					'<code>style.css</code>'
				)
			);
		}

		$requires_php = isset( $info['RequiresPHP'] ) ? $info['RequiresPHP'] : null;
		$requires_wp  = isset( $info['RequiresWP'] ) ? $info['RequiresWP'] : null;

		if ( ! is_php_version_compatible( $requires_php ) ) {
			$error = sprintf(
				/* translators: 1: Current PHP version, 2: Version required by the uploaded theme. */
				__( 'The PHP version on your server is %1$s, however the uploaded theme requires %2$s.' ),
				PHP_VERSION,
				$requires_php
			);

			return new WP_Error( 'incompatible_php_required_version', $this->strings['incompatible_archive'], $error );
		}
		if ( ! is_wp_version_compatible( $requires_wp ) ) {
			$error = sprintf(
				/* translators: 1: Current WordPress version, 2: Version required by the uploaded theme. */
				__( 'Your WordPress version is %1$s, however the uploaded theme requires %2$s.' ),
				$wp_version,
				$requires_wp
			);

			return new WP_Error( 'incompatible_wp_required_version', $this->strings['incompatible_archive'], $error );
		}

		$this->new_theme_data = $info;

		return $source;
	}

	/**
	 * Turns on maintenance mode before attempting to upgrade the active theme.
	 *
	 * Hooked to the {@see 'upgrader_pre_install'} filter by Theme_Upgrader::upgrade() and
	 * Theme_Upgrader::bulk_upgrade().
	 *
	 * @since 2.8.0
	 *
	 * @param bool|WP_Error $response The installation response before the installation has started.
	 * @param array         $theme    Theme arguments.
	 * @return bool|WP_Error The original `$response` parameter or WP_Error.
	 */
	public function current_before( $response, $theme ) {
		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$theme = isset( $theme['theme'] ) ? $theme['theme'] : '';

		// Only run if active theme.
		if ( get_stylesheet() !== $theme ) {
			return $response;
		}

		// Change to maintenance mode. Bulk edit handles this separately.
		if ( ! $this->bulk ) {
			$this->maintenance_mode( true );
		}

		return $response;
	}

	/**
	 * Turns off maintenance mode after upgrading the active theme.
	 *
	 * Hooked to the {@see 'upgrader_post_install'} filter by Theme_Upgrader::upgrade()
	 * and Theme_Upgrader::bulk_upgrade().
	 *
	 * @since 2.8.0
	 *
	 * @param bool|WP_Error $response The installation response after the installation has finished.
	 * @param array         $theme    Theme arguments.
	 * @return bool|WP_Error The original `$response` parameter or WP_Error.
	 */
	public function current_after( $response, $theme ) {
		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$theme = isset( $theme['theme'] ) ? $theme['theme'] : '';

		// Only run if active theme.
		if ( get_stylesheet() !== $theme ) {
			return $response;
		}

		// Ensure stylesheet name hasn't changed after the upgrade:
		if ( get_stylesheet() === $theme && $theme !== $this->result['destination_name'] ) {
			wp_clean_themes_cache();
			$stylesheet = $this->result['destination_name'];
			switch_theme( $stylesheet );
		}

		// Time to remove maintenance mode. Bulk edit handles this separately.
		if ( ! $this->bulk ) {
			$this->maintenance_mode( false );
		}
		return $response;
	}

	/**
	 * Deletes the old theme during an upgrade.
	 *
	 * Hooked to the {@see 'upgrader_clear_destination'} filter by Theme_Upgrader::upgrade()
	 * and Theme_Upgrader::bulk_upgrade().
	 *
	 * @since 2.8.0
	 *
	 * @global WP_Filesystem_Base $wp_filesystem Subclass
	 *
	 * @param bool   $removed
	 * @param string $local_destination
	 * @param string $remote_destination
	 * @param array  $theme
	 * @return bool
	 */
	public function delete_old_theme( $removed, $local_destination, $remote_destination, $theme ) {
		global $wp_filesystem;

		if ( is_wp_error( $removed ) ) {
			return $removed; // Pass errors through.
		}

		if ( ! isset( $theme['theme'] ) ) {
			return $removed;
		}

		$theme      = $theme['theme'];
		$themes_dir = trailingslashit( $wp_filesystem->wp_themes_dir( $theme ) );
		if ( $wp_filesystem->exists( $themes_dir . $theme ) ) {
			if ( ! $wp_filesystem->delete( $themes_dir . $theme, true ) ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Gets the WP_Theme object for a theme.
	 *
	 * @since 2.8.0
	 * @since 3.0.0 The `$theme` argument was added.
	 *
	 * @param string $theme The directory name of the theme. This is optional, and if not supplied,
	 *                      the directory name from the last result will be used.
	 * @return WP_Theme|false The theme's info object, or false `$theme` is not supplied
	 *                        and the last result isn't set.
	 */
	public function theme_info( $theme = null ) {
		if ( empty( $theme ) ) {
			if ( ! empty( $this->result['destination_name'] ) ) {
				$theme = $this->result['destination_name'];
			} else {
				return false;
			}
		}

		$theme = wp_get_theme( $theme );
		$theme->cache_delete();

		return $theme;
	}
}
menu.php000060400000022622151327343360006230 0ustar00<?php
/**
 * Build Administration Menu.
 *
 * @package WordPress
 * @subpackage Administration
 */

if ( is_network_admin() ) {

	/**
	 * Fires before the administration menu loads in the Network Admin.
	 *
	 * The hook fires before menus and sub-menus are removed based on user privileges.
	 *
	 * @since 3.1.0
	 * @access private
	 */
	do_action( '_network_admin_menu' );
} elseif ( is_user_admin() ) {

	/**
	 * Fires before the administration menu loads in the User Admin.
	 *
	 * The hook fires before menus and sub-menus are removed based on user privileges.
	 *
	 * @since 3.1.0
	 * @access private
	 */
	do_action( '_user_admin_menu' );
} else {

	/**
	 * Fires before the administration menu loads in the admin.
	 *
	 * The hook fires before menus and sub-menus are removed based on user privileges.
	 *
	 * @since 2.2.0
	 * @access private
	 */
	do_action( '_admin_menu' );
}

// Create list of page plugin hook names.
foreach ( $menu as $menu_page ) {
	$pos = strpos( $menu_page[2], '?' );

	if ( false !== $pos ) {
		// Handle post_type=post|page|foo pages.
		$hook_name = substr( $menu_page[2], 0, $pos );
		$hook_args = substr( $menu_page[2], $pos + 1 );
		wp_parse_str( $hook_args, $hook_args );

		// Set the hook name to be the post type.
		if ( isset( $hook_args['post_type'] ) ) {
			$hook_name = $hook_args['post_type'];
		} else {
			$hook_name = basename( $hook_name, '.php' );
		}
		unset( $hook_args );
	} else {
		$hook_name = basename( $menu_page[2], '.php' );
	}

	$hook_name = sanitize_title( $hook_name );

	if ( isset( $compat[ $hook_name ] ) ) {
		$hook_name = $compat[ $hook_name ];
	} elseif ( ! $hook_name ) {
		continue;
	}

	$admin_page_hooks[ $menu_page[2] ] = $hook_name;
}
unset( $menu_page, $compat );

$_wp_submenu_nopriv = array();
$_wp_menu_nopriv    = array();
// Loop over submenus and remove pages for which the user does not have privs.
foreach ( $submenu as $parent => $sub ) {
	foreach ( $sub as $index => $data ) {
		if ( ! current_user_can( $data[1] ) ) {
			unset( $submenu[ $parent ][ $index ] );
			$_wp_submenu_nopriv[ $parent ][ $data[2] ] = true;
		}
	}
	unset( $index, $data );

	if ( empty( $submenu[ $parent ] ) ) {
		unset( $submenu[ $parent ] );
	}
}
unset( $sub, $parent );

/*
 * Loop over the top-level menu.
 * Menus for which the original parent is not accessible due to lack of privileges
 * will have the next submenu in line be assigned as the new menu parent.
 */
foreach ( $menu as $id => $data ) {
	if ( empty( $submenu[ $data[2] ] ) ) {
		continue;
	}

	$subs       = $submenu[ $data[2] ];
	$first_sub  = reset( $subs );
	$old_parent = $data[2];
	$new_parent = $first_sub[2];

	/*
	 * If the first submenu is not the same as the assigned parent,
	 * make the first submenu the new parent.
	 */
	if ( $new_parent !== $old_parent ) {
		$_wp_real_parent_file[ $old_parent ] = $new_parent;

		$menu[ $id ][2] = $new_parent;

		foreach ( $submenu[ $old_parent ] as $index => $data ) {
			$submenu[ $new_parent ][ $index ] = $submenu[ $old_parent ][ $index ];
			unset( $submenu[ $old_parent ][ $index ] );
		}
		unset( $submenu[ $old_parent ], $index );

		if ( isset( $_wp_submenu_nopriv[ $old_parent ] ) ) {
			$_wp_submenu_nopriv[ $new_parent ] = $_wp_submenu_nopriv[ $old_parent ];
		}
	}
}
unset( $id, $data, $subs, $first_sub, $old_parent, $new_parent );

if ( is_network_admin() ) {

	/**
	 * Fires before the administration menu loads in the Network Admin.
	 *
	 * @since 3.1.0
	 *
	 * @param string $context Empty context.
	 */
	do_action( 'network_admin_menu', '' );
} elseif ( is_user_admin() ) {

	/**
	 * Fires before the administration menu loads in the User Admin.
	 *
	 * @since 3.1.0
	 *
	 * @param string $context Empty context.
	 */
	do_action( 'user_admin_menu', '' );
} else {

	/**
	 * Fires before the administration menu loads in the admin.
	 *
	 * @since 1.5.0
	 *
	 * @param string $context Empty context.
	 */
	do_action( 'admin_menu', '' );
}

/*
 * Remove menus that have no accessible submenus and require privileges
 * that the user does not have. Run re-parent loop again.
 */
foreach ( $menu as $id => $data ) {
	if ( ! current_user_can( $data[1] ) ) {
		$_wp_menu_nopriv[ $data[2] ] = true;
	}

	/*
	 * If there is only one submenu and it is has same destination as the parent,
	 * remove the submenu.
	 */
	if ( ! empty( $submenu[ $data[2] ] ) && 1 === count( $submenu[ $data[2] ] ) ) {
		$subs      = $submenu[ $data[2] ];
		$first_sub = reset( $subs );

		if ( $data[2] === $first_sub[2] ) {
			unset( $submenu[ $data[2] ] );
		}
	}

	// If submenu is empty...
	if ( empty( $submenu[ $data[2] ] ) ) {
		// And user doesn't have privs, remove menu.
		if ( isset( $_wp_menu_nopriv[ $data[2] ] ) ) {
			unset( $menu[ $id ] );
		}
	}
}
unset( $id, $data, $subs, $first_sub );

/**
 * Adds a CSS class to a string.
 *
 * @since 2.7.0
 *
 * @param string $class_to_add The CSS class to add.
 * @param string $classes      The string to add the CSS class to.
 * @return string The string with the CSS class added.
 */
function add_cssclass( $class_to_add, $classes ) {
	if ( empty( $classes ) ) {
		return $class_to_add;
	}

	return $classes . ' ' . $class_to_add;
}

/**
 * Adds CSS classes for top-level administration menu items.
 *
 * The list of added classes includes `.menu-top-first` and `.menu-top-last`.
 *
 * @since 2.7.0
 *
 * @param array $menu The array of administration menu items.
 * @return array The array of administration menu items with the CSS classes added.
 */
function add_menu_classes( $menu ) {
	$first_item  = false;
	$last_order  = false;
	$items_count = count( $menu );

	$i = 0;

	foreach ( $menu as $order => $top ) {
		++$i;

		if ( 0 === $order ) { // Dashboard is always shown/single.
			$menu[0][4] = add_cssclass( 'menu-top-first', $top[4] );
			$last_order = 0;
			continue;
		}

		if ( str_starts_with( $top[2], 'separator' ) && false !== $last_order ) { // If separator.
			$first_item = true;
			$classes    = $menu[ $last_order ][4];

			$menu[ $last_order ][4] = add_cssclass( 'menu-top-last', $classes );
			continue;
		}

		if ( $first_item ) {
			$first_item = false;
			$classes    = $menu[ $order ][4];

			$menu[ $order ][4] = add_cssclass( 'menu-top-first', $classes );
		}

		if ( $i === $items_count ) { // Last item.
			$classes = $menu[ $order ][4];

			$menu[ $order ][4] = add_cssclass( 'menu-top-last', $classes );
		}

		$last_order = $order;
	}

	/**
	 * Filters administration menu array with classes added for top-level items.
	 *
	 * @since 2.7.0
	 *
	 * @param array $menu Associative array of administration menu items.
	 */
	return apply_filters( 'add_menu_classes', $menu );
}

uksort( $menu, 'strnatcasecmp' ); // Make it all pretty.

/**
 * Filters whether to enable custom ordering of the administration menu.
 *
 * See the {@see 'menu_order'} filter for reordering menu items.
 *
 * @since 2.8.0
 *
 * @param bool $custom Whether custom ordering is enabled. Default false.
 */
if ( apply_filters( 'custom_menu_order', false ) ) {
	$menu_order = array();

	foreach ( $menu as $menu_item ) {
		$menu_order[] = $menu_item[2];
	}
	unset( $menu_item );

	$default_menu_order = $menu_order;

	/**
	 * Filters the order of administration menu items.
	 *
	 * A truthy value must first be passed to the {@see 'custom_menu_order'} filter
	 * for this filter to work. Use the following to enable custom menu ordering:
	 *
	 *     add_filter( 'custom_menu_order', '__return_true' );
	 *
	 * @since 2.8.0
	 *
	 * @param array $menu_order An ordered array of menu items.
	 */
	$menu_order = apply_filters( 'menu_order', $menu_order );
	$menu_order = array_flip( $menu_order );

	$default_menu_order = array_flip( $default_menu_order );

	/**
	 * @global array $menu_order
	 * @global array $default_menu_order
	 *
	 * @param array $a
	 * @param array $b
	 * @return int
	 */
	function sort_menu( $a, $b ) {
		global $menu_order, $default_menu_order;

		$a = $a[2];
		$b = $b[2];

		if ( isset( $menu_order[ $a ] ) && ! isset( $menu_order[ $b ] ) ) {
			return -1;
		} elseif ( ! isset( $menu_order[ $a ] ) && isset( $menu_order[ $b ] ) ) {
			return 1;
		} elseif ( isset( $menu_order[ $a ] ) && isset( $menu_order[ $b ] ) ) {
			if ( $menu_order[ $a ] === $menu_order[ $b ] ) {
				return 0;
			}
			return ( $menu_order[ $a ] < $menu_order[ $b ] ) ? -1 : 1;
		} else {
			return ( $default_menu_order[ $a ] <= $default_menu_order[ $b ] ) ? -1 : 1;
		}
	}

	usort( $menu, 'sort_menu' );
	unset( $menu_order, $default_menu_order );
}

// Prevent adjacent separators.
$prev_menu_was_separator = false;
foreach ( $menu as $id => $data ) {
	if ( false === stristr( $data[4], 'wp-menu-separator' ) ) {

		// This item is not a separator, so falsey the toggler and do nothing.
		$prev_menu_was_separator = false;
	} else {

		// The previous item was a separator, so unset this one.
		if ( true === $prev_menu_was_separator ) {
			unset( $menu[ $id ] );
		}

		// This item is a separator, so truthy the toggler and move on.
		$prev_menu_was_separator = true;
	}
}
unset( $id, $data, $prev_menu_was_separator );

// Remove the last menu item if it is a separator.
$last_menu_key = array_keys( $menu );
$last_menu_key = array_pop( $last_menu_key );
if ( ! empty( $menu ) && 'wp-menu-separator' === $menu[ $last_menu_key ][4] ) {
	unset( $menu[ $last_menu_key ] );
}
unset( $last_menu_key );

if ( ! user_can_access_admin_page() ) {

	/**
	 * Fires when access to an admin page is denied.
	 *
	 * @since 2.5.0
	 */
	do_action( 'admin_page_access_denied' );

	wp_die( __( 'Sorry, you are not allowed to access this page.' ), 403 );
}

$menu = add_menu_classes( $menu );
class-custom-image-header.php000064400000140220151327343360012206 0ustar00<?php
/**
 * The custom header image script.
 *
 * @package WordPress
 * @subpackage Administration
 */

/**
 * The custom header image class.
 *
 * @since 2.1.0
 */
#[AllowDynamicProperties]
class Custom_Image_Header {

	/**
	 * Callback for administration header.
	 *
	 * @since 2.1.0
	 * @var callable
	 */
	public $admin_header_callback;

	/**
	 * Callback for header div.
	 *
	 * @since 3.0.0
	 * @var callable
	 */
	public $admin_image_div_callback;

	/**
	 * Holds default headers.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	public $default_headers = array();

	/**
	 * Used to trigger a success message when settings updated and set to true.
	 *
	 * @since 3.0.0
	 * @var bool
	 */
	private $updated;

	/**
	 * Constructor - Registers administration header callback.
	 *
	 * @since 2.1.0
	 *
	 * @param callable $admin_header_callback    Administration header callback.
	 * @param callable $admin_image_div_callback Optional. Custom image div output callback.
	 *                                           Default empty string.
	 */
	public function __construct( $admin_header_callback, $admin_image_div_callback = '' ) {
		$this->admin_header_callback    = $admin_header_callback;
		$this->admin_image_div_callback = $admin_image_div_callback;

		add_action( 'admin_menu', array( $this, 'init' ) );

		add_action( 'customize_save_after', array( $this, 'customize_set_last_used' ) );
		add_action( 'wp_ajax_custom-header-crop', array( $this, 'ajax_header_crop' ) );
		add_action( 'wp_ajax_custom-header-add', array( $this, 'ajax_header_add' ) );
		add_action( 'wp_ajax_custom-header-remove', array( $this, 'ajax_header_remove' ) );
	}

	/**
	 * Sets up the hooks for the Custom Header admin page.
	 *
	 * @since 2.1.0
	 */
	public function init() {
		$page = add_theme_page(
			_x( 'Header', 'custom image header' ),
			_x( 'Header', 'custom image header' ),
			'edit_theme_options',
			'custom-header',
			array( $this, 'admin_page' )
		);

		if ( ! $page ) {
			return;
		}

		add_action( "admin_print_scripts-{$page}", array( $this, 'js_includes' ) );
		add_action( "admin_print_styles-{$page}", array( $this, 'css_includes' ) );
		add_action( "admin_head-{$page}", array( $this, 'help' ) );
		add_action( "admin_head-{$page}", array( $this, 'take_action' ), 50 );
		add_action( "admin_head-{$page}", array( $this, 'js' ), 50 );

		if ( $this->admin_header_callback ) {
			add_action( "admin_head-{$page}", $this->admin_header_callback, 51 );
		}
	}

	/**
	 * Adds contextual help.
	 *
	 * @since 3.0.0
	 */
	public function help() {
		get_current_screen()->add_help_tab(
			array(
				'id'      => 'overview',
				'title'   => __( 'Overview' ),
				'content' =>
					'<p>' . __( 'This screen is used to customize the header section of your theme.' ) . '</p>' .
					'<p>' . __( 'You can choose from the theme&#8217;s default header images, or use one of your own. You can also customize how your Site Title and Tagline are displayed.' ) . '<p>',
			)
		);

		get_current_screen()->add_help_tab(
			array(
				'id'      => 'set-header-image',
				'title'   => __( 'Header Image' ),
				'content' =>
					'<p>' . __( 'You can set a custom image header for your site. Simply upload the image and crop it, and the new header will go live immediately. Alternatively, you can use an image that has already been uploaded to your Media Library by clicking the &#8220;Choose Image&#8221; button.' ) . '</p>' .
					'<p>' . __( 'Some themes come with additional header images bundled. If you see multiple images displayed, select the one you would like and click the &#8220;Save Changes&#8221; button.' ) . '</p>' .
					'<p>' . __( 'If your theme has more than one default header image, or you have uploaded more than one custom header image, you have the option of having WordPress display a randomly different image on each page of your site. Click the &#8220;Random&#8221; radio button next to the Uploaded Images or Default Images section to enable this feature.' ) . '</p>' .
					'<p>' . __( 'If you do not want a header image to be displayed on your site at all, click the &#8220;Remove Header Image&#8221; button at the bottom of the Header Image section of this page. If you want to re-enable the header image later, you just have to select one of the other image options and click &#8220;Save Changes&#8221;.' ) . '</p>',
			)
		);

		get_current_screen()->add_help_tab(
			array(
				'id'      => 'set-header-text',
				'title'   => __( 'Header Text' ),
				'content' =>
					'<p>' . sprintf(
						/* translators: %s: URL to General Settings screen. */
						__( 'For most themes, the header text is your Site Title and Tagline, as defined in the <a href="%s">General Settings</a> section.' ),
						admin_url( 'options-general.php' )
					) .
					'</p>' .
					'<p>' . __( 'In the Header Text section of this page, you can choose whether to display this text or hide it. You can also choose a color for the text by clicking the Select Color button and either typing in a legitimate HTML hex value, e.g. &#8220;#ff0000&#8221; for red, or by choosing a color using the color picker.' ) . '</p>' .
					'<p>' . __( 'Do not forget to click &#8220;Save Changes&#8221; when you are done!' ) . '</p>',
			)
		);

		get_current_screen()->set_help_sidebar(
			'<p><strong>' . __( 'For more information:' ) . '</strong></p>' .
			'<p>' . __( '<a href="https://codex.wordpress.org/Appearance_Header_Screen">Documentation on Custom Header</a>' ) . '</p>' .
			'<p>' . __( '<a href="https://wordpress.org/support/forums/">Support forums</a>' ) . '</p>'
		);
	}

	/**
	 * Gets the current step.
	 *
	 * @since 2.6.0
	 *
	 * @return int Current step.
	 */
	public function step() {
		if ( ! isset( $_GET['step'] ) ) {
			return 1;
		}

		$step = (int) $_GET['step'];
		if ( $step < 1 || 3 < $step ||
			( 2 === $step && ! wp_verify_nonce( $_REQUEST['_wpnonce-custom-header-upload'], 'custom-header-upload' ) ) ||
			( 3 === $step && ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'custom-header-crop-image' ) )
		) {
			return 1;
		}

		return $step;
	}

	/**
	 * Sets up the enqueue for the JavaScript files.
	 *
	 * @since 2.1.0
	 */
	public function js_includes() {
		$step = $this->step();

		if ( ( 1 === $step || 3 === $step ) ) {
			wp_enqueue_media();
			wp_enqueue_script( 'custom-header' );
			if ( current_theme_supports( 'custom-header', 'header-text' ) ) {
				wp_enqueue_script( 'wp-color-picker' );
			}
		} elseif ( 2 === $step ) {
			wp_enqueue_script( 'imgareaselect' );
		}
	}

	/**
	 * Sets up the enqueue for the CSS files.
	 *
	 * @since 2.7.0
	 */
	public function css_includes() {
		$step = $this->step();

		if ( ( 1 === $step || 3 === $step ) && current_theme_supports( 'custom-header', 'header-text' ) ) {
			wp_enqueue_style( 'wp-color-picker' );
		} elseif ( 2 === $step ) {
			wp_enqueue_style( 'imgareaselect' );
		}
	}

	/**
	 * Executes custom header modification.
	 *
	 * @since 2.6.0
	 */
	public function take_action() {
		if ( ! current_user_can( 'edit_theme_options' ) ) {
			return;
		}

		if ( empty( $_POST ) ) {
			return;
		}

		$this->updated = true;

		if ( isset( $_POST['resetheader'] ) ) {
			check_admin_referer( 'custom-header-options', '_wpnonce-custom-header-options' );

			$this->reset_header_image();

			return;
		}

		if ( isset( $_POST['removeheader'] ) ) {
			check_admin_referer( 'custom-header-options', '_wpnonce-custom-header-options' );

			$this->remove_header_image();

			return;
		}

		if ( isset( $_POST['text-color'] ) && ! isset( $_POST['display-header-text'] ) ) {
			check_admin_referer( 'custom-header-options', '_wpnonce-custom-header-options' );

			set_theme_mod( 'header_textcolor', 'blank' );
		} elseif ( isset( $_POST['text-color'] ) ) {
			check_admin_referer( 'custom-header-options', '_wpnonce-custom-header-options' );

			$_POST['text-color'] = str_replace( '#', '', $_POST['text-color'] );

			$color = preg_replace( '/[^0-9a-fA-F]/', '', $_POST['text-color'] );

			if ( strlen( $color ) === 6 || strlen( $color ) === 3 ) {
				set_theme_mod( 'header_textcolor', $color );
			} elseif ( ! $color ) {
				set_theme_mod( 'header_textcolor', 'blank' );
			}
		}

		if ( isset( $_POST['default-header'] ) ) {
			check_admin_referer( 'custom-header-options', '_wpnonce-custom-header-options' );

			$this->set_header_image( $_POST['default-header'] );

			return;
		}
	}

	/**
	 * Processes the default headers.
	 *
	 * @since 3.0.0
	 *
	 * @global array $_wp_default_headers
	 */
	public function process_default_headers() {
		global $_wp_default_headers;

		if ( ! isset( $_wp_default_headers ) ) {
			return;
		}

		if ( ! empty( $this->default_headers ) ) {
			return;
		}

		$this->default_headers    = $_wp_default_headers;
		$template_directory_uri   = get_template_directory_uri();
		$stylesheet_directory_uri = get_stylesheet_directory_uri();

		foreach ( array_keys( $this->default_headers ) as $header ) {
			$this->default_headers[ $header ]['url'] = sprintf(
				$this->default_headers[ $header ]['url'],
				$template_directory_uri,
				$stylesheet_directory_uri
			);

			$this->default_headers[ $header ]['thumbnail_url'] = sprintf(
				$this->default_headers[ $header ]['thumbnail_url'],
				$template_directory_uri,
				$stylesheet_directory_uri
			);
		}
	}

	/**
	 * Displays UI for selecting one of several default headers.
	 *
	 * Shows the random image option if this theme has multiple header images.
	 * Random image option is on by default if no header has been set.
	 *
	 * @since 3.0.0
	 *
	 * @param string $type The header type. One of 'default' (for the Uploaded Images control)
	 *                     or 'uploaded' (for the Uploaded Images control).
	 */
	public function show_header_selector( $type = 'default' ) {
		if ( 'default' === $type ) {
			$headers = $this->default_headers;
		} else {
			$headers = get_uploaded_header_images();
			$type    = 'uploaded';
		}

		if ( 1 < count( $headers ) ) {
			echo '<div class="random-header">';
			echo '<label><input name="default-header" type="radio" value="random-' . $type . '-image"' . checked( is_random_header_image( $type ), true, false ) . ' />';
			_e( '<strong>Random:</strong> Show a different image on each page.' );
			echo '</label>';
			echo '</div>';
		}

		echo '<div class="available-headers">';

		foreach ( $headers as $header_key => $header ) {
			$header_thumbnail = $header['thumbnail_url'];
			$header_url       = $header['url'];
			$header_alt_text  = empty( $header['alt_text'] ) ? '' : $header['alt_text'];

			echo '<div class="default-header">';
			echo '<label><input name="default-header" type="radio" value="' . esc_attr( $header_key ) . '" ' . checked( $header_url, get_theme_mod( 'header_image' ), false ) . ' />';
			$width = '';
			if ( ! empty( $header['attachment_id'] ) ) {
				$width = ' width="230"';
			}
			echo '<img src="' . esc_url( set_url_scheme( $header_thumbnail ) ) . '" alt="' . esc_attr( $header_alt_text ) . '"' . $width . ' /></label>';
			echo '</div>';
		}

		echo '<div class="clear"></div></div>';
	}

	/**
	 * Executes JavaScript depending on step.
	 *
	 * @since 2.1.0
	 */
	public function js() {
		$step = $this->step();

		if ( ( 1 === $step || 3 === $step ) && current_theme_supports( 'custom-header', 'header-text' ) ) {
			$this->js_1();
		} elseif ( 2 === $step ) {
			$this->js_2();
		}
	}

	/**
	 * Displays JavaScript based on Step 1 and 3.
	 *
	 * @since 2.6.0
	 */
	public function js_1() {
		$default_color = '';
		if ( current_theme_supports( 'custom-header', 'default-text-color' ) ) {
			$default_color = get_theme_support( 'custom-header', 'default-text-color' );
			if ( $default_color && ! str_contains( $default_color, '#' ) ) {
				$default_color = '#' . $default_color;
			}
		}
		?>
<script type="text/javascript">
(function($){
	var default_color = '<?php echo esc_js( $default_color ); ?>',
		header_text_fields;

	function pickColor(color) {
		$('#name').css('color', color);
		$('#desc').css('color', color);
		$('#text-color').val(color);
	}

	function toggle_text() {
		var checked = $('#display-header-text').prop('checked'),
			text_color;
		header_text_fields.toggle( checked );
		if ( ! checked )
			return;
		text_color = $('#text-color');
		if ( '' === text_color.val().replace('#', '') ) {
			text_color.val( default_color );
			pickColor( default_color );
		} else {
			pickColor( text_color.val() );
		}
	}

	$( function() {
		var text_color = $('#text-color');
		header_text_fields = $('.displaying-header-text');
		text_color.wpColorPicker({
			change: function( event, ui ) {
				pickColor( text_color.wpColorPicker('color') );
			},
			clear: function() {
				pickColor( '' );
			}
		});
		$('#display-header-text').click( toggle_text );
		<?php if ( ! display_header_text() ) : ?>
		toggle_text();
		<?php endif; ?>
	} );
})(jQuery);
</script>
		<?php
	}

	/**
	 * Displays JavaScript based on Step 2.
	 *
	 * @since 2.6.0
	 */
	public function js_2() {

		?>
<script type="text/javascript">
	function onEndCrop( coords ) {
		jQuery( '#x1' ).val(coords.x);
		jQuery( '#y1' ).val(coords.y);
		jQuery( '#width' ).val(coords.w);
		jQuery( '#height' ).val(coords.h);
	}

	jQuery( function() {
		var xinit = <?php echo absint( get_theme_support( 'custom-header', 'width' ) ); ?>;
		var yinit = <?php echo absint( get_theme_support( 'custom-header', 'height' ) ); ?>;
		var ratio = xinit / yinit;
		var ximg = jQuery('img#upload').width();
		var yimg = jQuery('img#upload').height();

		if ( yimg < yinit || ximg < xinit ) {
			if ( ximg / yimg > ratio ) {
				yinit = yimg;
				xinit = yinit * ratio;
			} else {
				xinit = ximg;
				yinit = xinit / ratio;
			}
		}

		jQuery('img#upload').imgAreaSelect({
			handles: true,
			keys: true,
			show: true,
			x1: 0,
			y1: 0,
			x2: xinit,
			y2: yinit,
			<?php
			if ( ! current_theme_supports( 'custom-header', 'flex-height' )
				&& ! current_theme_supports( 'custom-header', 'flex-width' )
			) {
				?>
			aspectRatio: xinit + ':' + yinit,
				<?php
			}
			if ( ! current_theme_supports( 'custom-header', 'flex-height' ) ) {
				?>
			maxHeight: <?php echo get_theme_support( 'custom-header', 'height' ); ?>,
				<?php
			}
			if ( ! current_theme_supports( 'custom-header', 'flex-width' ) ) {
				?>
			maxWidth: <?php echo get_theme_support( 'custom-header', 'width' ); ?>,
				<?php
			}
			?>
			onInit: function () {
				jQuery('#width').val(xinit);
				jQuery('#height').val(yinit);
			},
			onSelectChange: function(img, c) {
				jQuery('#x1').val(c.x1);
				jQuery('#y1').val(c.y1);
				jQuery('#width').val(c.width);
				jQuery('#height').val(c.height);
			}
		});
	} );
</script>
		<?php
	}

	/**
	 * Displays first step of custom header image page.
	 *
	 * @since 2.1.0
	 */
	public function step_1() {
		$this->process_default_headers();
		?>

<div class="wrap">
<h1><?php _e( 'Custom Header' ); ?></h1>

		<?php
		if ( current_user_can( 'customize' ) ) {
			$message = sprintf(
				/* translators: %s: URL to header image configuration in Customizer. */
				__( 'You can now manage and live-preview Custom Header in the <a href="%s">Customizer</a>.' ),
				admin_url( 'customize.php?autofocus[control]=header_image' )
			);
			wp_admin_notice(
				$message,
				array(
					'type'               => 'info',
					'additional_classes' => array( 'hide-if-no-customize' ),
				)
			);
		}

		if ( ! empty( $this->updated ) ) {
			$updated_message = sprintf(
				/* translators: %s: Home URL. */
				__( 'Header updated. <a href="%s">Visit your site</a> to see how it looks.' ),
				esc_url( home_url( '/' ) )
			);
			wp_admin_notice(
				$updated_message,
				array(
					'id'                 => 'message',
					'additional_classes' => array( 'updated' ),
				)
			);
		}
		?>

<h2><?php _e( 'Header Image' ); ?></h2>

<table class="form-table" role="presentation">
<tbody>

		<?php if ( get_custom_header() || display_header_text() ) : ?>
<tr>
<th scope="row"><?php _e( 'Preview' ); ?></th>
<td>
			<?php
			if ( $this->admin_image_div_callback ) {
				call_user_func( $this->admin_image_div_callback );
			} else {
				$custom_header = get_custom_header();
				$header_image  = get_header_image();

				if ( $header_image ) {
					$header_image_style = 'background-image:url(' . esc_url( $header_image ) . ');';
				} else {
					$header_image_style = '';
				}

				if ( $custom_header->width ) {
					$header_image_style .= 'max-width:' . $custom_header->width . 'px;';
				}
				if ( $custom_header->height ) {
					$header_image_style .= 'height:' . $custom_header->height . 'px;';
				}
				?>
	<div id="headimg" style="<?php echo $header_image_style; ?>">
				<?php
				if ( display_header_text() ) {
					$style = ' style="color:#' . get_header_textcolor() . ';"';
				} else {
					$style = ' style="display:none;"';
				}
				?>
		<h1><a id="name" class="displaying-header-text" <?php echo $style; ?> onclick="return false;" href="<?php bloginfo( 'url' ); ?>" tabindex="-1"><?php bloginfo( 'name' ); ?></a></h1>
		<div id="desc" class="displaying-header-text" <?php echo $style; ?>><?php bloginfo( 'description' ); ?></div>
	</div>
			<?php } ?>
</td>
</tr>
		<?php endif; ?>

		<?php if ( current_user_can( 'upload_files' ) && current_theme_supports( 'custom-header', 'uploads' ) ) : ?>
<tr>
<th scope="row"><?php _e( 'Select Image' ); ?></th>
<td>
	<p><?php _e( 'You can select an image to be shown at the top of your site by uploading from your computer or choosing from your media library. After selecting an image you will be able to crop it.' ); ?><br />
			<?php
			if ( ! current_theme_supports( 'custom-header', 'flex-height' )
				&& ! current_theme_supports( 'custom-header', 'flex-width' )
			) {
				printf(
					/* translators: 1: Image width in pixels, 2: Image height in pixels. */
					__( 'Images of exactly <strong>%1$d &times; %2$d pixels</strong> will be used as-is.' ) . '<br />',
					get_theme_support( 'custom-header', 'width' ),
					get_theme_support( 'custom-header', 'height' )
				);
			} elseif ( current_theme_supports( 'custom-header', 'flex-height' ) ) {
				if ( ! current_theme_supports( 'custom-header', 'flex-width' ) ) {
					printf(
						/* translators: %s: Size in pixels. */
						__( 'Images should be at least %s wide.' ) . ' ',
						sprintf(
							/* translators: %d: Custom header width. */
							'<strong>' . __( '%d pixels' ) . '</strong>',
							get_theme_support( 'custom-header', 'width' )
						)
					);
				}
			} elseif ( current_theme_supports( 'custom-header', 'flex-width' ) ) {
				if ( ! current_theme_supports( 'custom-header', 'flex-height' ) ) {
					printf(
						/* translators: %s: Size in pixels. */
						__( 'Images should be at least %s tall.' ) . ' ',
						sprintf(
							/* translators: %d: Custom header height. */
							'<strong>' . __( '%d pixels' ) . '</strong>',
							get_theme_support( 'custom-header', 'height' )
						)
					);
				}
			}

			if ( current_theme_supports( 'custom-header', 'flex-height' )
				|| current_theme_supports( 'custom-header', 'flex-width' )
			) {
				if ( current_theme_supports( 'custom-header', 'width' ) ) {
					printf(
						/* translators: %s: Size in pixels. */
						__( 'Suggested width is %s.' ) . ' ',
						sprintf(
							/* translators: %d: Custom header width. */
							'<strong>' . __( '%d pixels' ) . '</strong>',
							get_theme_support( 'custom-header', 'width' )
						)
					);
				}

				if ( current_theme_supports( 'custom-header', 'height' ) ) {
					printf(
						/* translators: %s: Size in pixels. */
						__( 'Suggested height is %s.' ) . ' ',
						sprintf(
							/* translators: %d: Custom header height. */
							'<strong>' . __( '%d pixels' ) . '</strong>',
							get_theme_support( 'custom-header', 'height' )
						)
					);
				}
			}
			?>
	</p>
	<form enctype="multipart/form-data" id="upload-form" class="wp-upload-form" method="post" action="<?php echo esc_url( add_query_arg( 'step', 2 ) ); ?>">
	<p>
		<label for="upload"><?php _e( 'Choose an image from your computer:' ); ?></label><br />
		<input type="file" id="upload" name="import" />
		<input type="hidden" name="action" value="save" />
			<?php wp_nonce_field( 'custom-header-upload', '_wpnonce-custom-header-upload' ); ?>
			<?php submit_button( _x( 'Upload', 'verb' ), '', 'submit', false ); ?>
	</p>
			<?php
			$modal_update_href = add_query_arg(
				array(
					'page'                          => 'custom-header',
					'step'                          => 2,
					'_wpnonce-custom-header-upload' => wp_create_nonce( 'custom-header-upload' ),
				),
				admin_url( 'themes.php' )
			);
			?>
	<p>
		<label for="choose-from-library-link"><?php _e( 'Or choose an image from your media library:' ); ?></label><br />
		<button id="choose-from-library-link" class="button"
			data-update-link="<?php echo esc_url( $modal_update_href ); ?>"
			data-choose="<?php esc_attr_e( 'Choose a Custom Header' ); ?>"
			data-update="<?php esc_attr_e( 'Set as header' ); ?>"><?php _e( 'Choose Image' ); ?></button>
	</p>
	</form>
</td>
</tr>
		<?php endif; ?>
</tbody>
</table>

<form method="post" action="<?php echo esc_url( add_query_arg( 'step', 1 ) ); ?>">
		<?php submit_button( null, 'screen-reader-text', 'save-header-options', false ); ?>
<table class="form-table" role="presentation">
<tbody>
		<?php if ( get_uploaded_header_images() ) : ?>
<tr>
<th scope="row"><?php _e( 'Uploaded Images' ); ?></th>
<td>
	<p><?php _e( 'You can choose one of your previously uploaded headers, or show a random one.' ); ?></p>
			<?php
			$this->show_header_selector( 'uploaded' );
			?>
</td>
</tr>
			<?php
	endif;
		if ( ! empty( $this->default_headers ) ) :
			?>
<tr>
<th scope="row"><?php _e( 'Default Images' ); ?></th>
<td>
			<?php if ( current_theme_supports( 'custom-header', 'uploads' ) ) : ?>
	<p><?php _e( 'If you do not want to upload your own image, you can use one of these cool headers, or show a random one.' ); ?></p>
	<?php else : ?>
	<p><?php _e( 'You can use one of these cool headers or show a random one on each page.' ); ?></p>
	<?php endif; ?>
			<?php
			$this->show_header_selector( 'default' );
			?>
</td>
</tr>
			<?php
	endif;
		if ( get_header_image() ) :
			?>
<tr>
<th scope="row"><?php _e( 'Remove Image' ); ?></th>
<td>
	<p><?php _e( 'This will remove the header image. You will not be able to restore any customizations.' ); ?></p>
			<?php submit_button( __( 'Remove Header Image' ), '', 'removeheader', false ); ?>
</td>
</tr>
			<?php
	endif;

		$default_image = sprintf(
			get_theme_support( 'custom-header', 'default-image' ),
			get_template_directory_uri(),
			get_stylesheet_directory_uri()
		);

		if ( $default_image && get_header_image() !== $default_image ) :
			?>
<tr>
<th scope="row"><?php _e( 'Reset Image' ); ?></th>
<td>
	<p><?php _e( 'This will restore the original header image. You will not be able to restore any customizations.' ); ?></p>
			<?php submit_button( __( 'Restore Original Header Image' ), '', 'resetheader', false ); ?>
</td>
</tr>
	<?php endif; ?>
</tbody>
</table>

		<?php if ( current_theme_supports( 'custom-header', 'header-text' ) ) : ?>

<h2><?php _e( 'Header Text' ); ?></h2>

<table class="form-table" role="presentation">
<tbody>
<tr>
<th scope="row"><?php _e( 'Header Text' ); ?></th>
<td>
	<p>
	<label><input type="checkbox" name="display-header-text" id="display-header-text"<?php checked( display_header_text() ); ?> /> <?php _e( 'Show header text with your image.' ); ?></label>
	</p>
</td>
</tr>

<tr class="displaying-header-text">
<th scope="row"><?php _e( 'Text Color' ); ?></th>
<td>
	<p>
			<?php
			$default_color = '';
			if ( current_theme_supports( 'custom-header', 'default-text-color' ) ) {
				$default_color = get_theme_support( 'custom-header', 'default-text-color' );
				if ( $default_color && ! str_contains( $default_color, '#' ) ) {
					$default_color = '#' . $default_color;
				}
			}

			$default_color_attr = $default_color ? ' data-default-color="' . esc_attr( $default_color ) . '"' : '';

			$header_textcolor = display_header_text() ? get_header_textcolor() : get_theme_support( 'custom-header', 'default-text-color' );
			if ( $header_textcolor && ! str_contains( $header_textcolor, '#' ) ) {
				$header_textcolor = '#' . $header_textcolor;
			}

			echo '<input type="text" name="text-color" id="text-color" value="' . esc_attr( $header_textcolor ) . '"' . $default_color_attr . ' />';
			if ( $default_color ) {
				/* translators: %s: Default text color. */
				echo ' <span class="description hide-if-js">' . sprintf( _x( 'Default: %s', 'color' ), esc_html( $default_color ) ) . '</span>';
			}
			?>
	</p>
</td>
</tr>
</tbody>
</table>
			<?php
endif;

		/**
		 * Fires just before the submit button in the custom header options form.
		 *
		 * @since 3.1.0
		 */
		do_action( 'custom_header_options' );

		wp_nonce_field( 'custom-header-options', '_wpnonce-custom-header-options' );
		?>

		<?php submit_button( null, 'primary', 'save-header-options' ); ?>
</form>
</div>

		<?php
	}

	/**
	 * Displays second step of custom header image page.
	 *
	 * @since 2.1.0
	 */
	public function step_2() {
		check_admin_referer( 'custom-header-upload', '_wpnonce-custom-header-upload' );

		if ( ! current_theme_supports( 'custom-header', 'uploads' ) ) {
			wp_die(
				'<h1>' . __( 'An error occurred while processing your header image.' ) . '</h1>' .
				'<p>' . __( 'The active theme does not support uploading a custom header image. Please ensure your theme supports custom headers and try again.' ) . '</p>',
				403
			);
		}

		if ( empty( $_POST ) && isset( $_GET['file'] ) ) {
			$attachment_id = absint( $_GET['file'] );
			$file          = get_attached_file( $attachment_id, true );
			$url           = wp_get_attachment_image_src( $attachment_id, 'full' );
			$url           = $url[0];
		} elseif ( isset( $_POST ) ) {
			$data          = $this->step_2_manage_upload();
			$attachment_id = $data['attachment_id'];
			$file          = $data['file'];
			$url           = $data['url'];
		}

		if ( file_exists( $file ) ) {
			list( $width, $height, $type, $attr ) = wp_getimagesize( $file );
		} else {
			$data   = wp_get_attachment_metadata( $attachment_id );
			$height = isset( $data['height'] ) ? (int) $data['height'] : 0;
			$width  = isset( $data['width'] ) ? (int) $data['width'] : 0;
			unset( $data );
		}

		$max_width = 0;

		// For flex, limit size of image displayed to 1500px unless theme says otherwise.
		if ( current_theme_supports( 'custom-header', 'flex-width' ) ) {
			$max_width = 1500;
		}

		if ( current_theme_supports( 'custom-header', 'max-width' ) ) {
			$max_width = max( $max_width, get_theme_support( 'custom-header', 'max-width' ) );
		}

		$max_width = max( $max_width, get_theme_support( 'custom-header', 'width' ) );

		// If flexible height isn't supported and the image is the exact right size.
		if ( ! current_theme_supports( 'custom-header', 'flex-height' )
			&& ! current_theme_supports( 'custom-header', 'flex-width' )
			&& (int) get_theme_support( 'custom-header', 'width' ) === $width
			&& (int) get_theme_support( 'custom-header', 'height' ) === $height
		) {
			// Add the metadata.
			if ( file_exists( $file ) ) {
				wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $file ) );
			}

			$this->set_header_image( compact( 'url', 'attachment_id', 'width', 'height' ) );

			/**
			 * Filters the attachment file path after the custom header or background image is set.
			 *
			 * Used for file replication.
			 *
			 * @since 2.1.0
			 *
			 * @param string $file          Path to the file.
			 * @param int    $attachment_id Attachment ID.
			 */
			$file = apply_filters( 'wp_create_file_in_uploads', $file, $attachment_id ); // For replication.

			return $this->finished();
		} elseif ( $width > $max_width ) {
			$oitar = $width / $max_width;

			$image = wp_crop_image(
				$attachment_id,
				0,
				0,
				$width,
				$height,
				$max_width,
				$height / $oitar,
				false,
				str_replace( wp_basename( $file ), 'midsize-' . wp_basename( $file ), $file )
			);

			if ( ! $image || is_wp_error( $image ) ) {
				wp_die( __( 'Image could not be processed. Please go back and try again.' ), __( 'Image Processing Error' ) );
			}

			/** This filter is documented in wp-admin/includes/class-custom-image-header.php */
			$image = apply_filters( 'wp_create_file_in_uploads', $image, $attachment_id ); // For replication.

			$url    = str_replace( wp_basename( $url ), wp_basename( $image ), $url );
			$width  = $width / $oitar;
			$height = $height / $oitar;
		} else {
			$oitar = 1;
		}
		?>

<div class="wrap">
<h1><?php _e( 'Crop Header Image' ); ?></h1>

<form method="post" action="<?php echo esc_url( add_query_arg( 'step', 3 ) ); ?>">
	<p class="hide-if-no-js"><?php _e( 'Choose the part of the image you want to use as your header.' ); ?></p>
	<p class="hide-if-js"><strong><?php _e( 'You need JavaScript to choose a part of the image.' ); ?></strong></p>

	<div id="crop_image" style="position: relative">
		<img src="<?php echo esc_url( $url ); ?>" id="upload" width="<?php echo esc_attr( $width ); ?>" height="<?php echo esc_attr( $height ); ?>" alt="" />
	</div>

	<input type="hidden" name="x1" id="x1" value="0" />
	<input type="hidden" name="y1" id="y1" value="0" />
	<input type="hidden" name="width" id="width" value="<?php echo esc_attr( $width ); ?>" />
	<input type="hidden" name="height" id="height" value="<?php echo esc_attr( $height ); ?>" />
	<input type="hidden" name="attachment_id" id="attachment_id" value="<?php echo esc_attr( $attachment_id ); ?>" />
	<input type="hidden" name="oitar" id="oitar" value="<?php echo esc_attr( $oitar ); ?>" />
		<?php if ( empty( $_POST ) && isset( $_GET['file'] ) ) { ?>
	<input type="hidden" name="create-new-attachment" value="true" />
	<?php } ?>
		<?php wp_nonce_field( 'custom-header-crop-image' ); ?>

	<p class="submit">
		<?php submit_button( __( 'Crop and Publish' ), 'primary', 'submit', false ); ?>
		<?php
		if ( isset( $oitar ) && 1 === $oitar
			&& ( current_theme_supports( 'custom-header', 'flex-height' )
				|| current_theme_supports( 'custom-header', 'flex-width' ) )
		) {
			submit_button( __( 'Skip Cropping, Publish Image as Is' ), '', 'skip-cropping', false );
		}
		?>
	</p>
</form>
</div>
		<?php
	}


	/**
	 * Uploads the file to be cropped in the second step.
	 *
	 * @since 3.4.0
	 */
	public function step_2_manage_upload() {
		$overrides = array( 'test_form' => false );

		$uploaded_file = $_FILES['import'];
		$wp_filetype   = wp_check_filetype_and_ext( $uploaded_file['tmp_name'], $uploaded_file['name'] );

		if ( ! wp_match_mime_types( 'image', $wp_filetype['type'] ) ) {
			wp_die( __( 'The uploaded file is not a valid image. Please try again.' ) );
		}

		$file = wp_handle_upload( $uploaded_file, $overrides );

		if ( isset( $file['error'] ) ) {
			wp_die( $file['error'], __( 'Image Upload Error' ) );
		}

		$url      = $file['url'];
		$type     = $file['type'];
		$file     = $file['file'];
		$filename = wp_basename( $file );

		// Construct the attachment array.
		$attachment = array(
			'post_title'     => $filename,
			'post_content'   => $url,
			'post_mime_type' => $type,
			'guid'           => $url,
			'context'        => 'custom-header',
		);

		// Save the data.
		$attachment_id = wp_insert_attachment( $attachment, $file );

		return compact( 'attachment_id', 'file', 'filename', 'url', 'type' );
	}

	/**
	 * Displays third step of custom header image page.
	 *
	 * @since 2.1.0
	 * @since 4.4.0 Switched to using wp_get_attachment_url() instead of the guid
	 *              for retrieving the header image URL.
	 */
	public function step_3() {
		check_admin_referer( 'custom-header-crop-image' );

		if ( ! current_theme_supports( 'custom-header', 'uploads' ) ) {
			wp_die(
				'<h1>' . __( 'An error occurred while processing your header image.' ) . '</h1>' .
				'<p>' . __( 'The active theme does not support uploading a custom header image. Please ensure your theme supports custom headers and try again.' ) . '</p>',
				403
			);
		}

		if ( ! empty( $_POST['skip-cropping'] )
			&& ! current_theme_supports( 'custom-header', 'flex-height' )
			&& ! current_theme_supports( 'custom-header', 'flex-width' )
		) {
			wp_die(
				'<h1>' . __( 'An error occurred while processing your header image.' ) . '</h1>' .
				'<p>' . __( 'The active theme does not support a flexible sized header image.' ) . '</p>',
				403
			);
		}

		if ( $_POST['oitar'] > 1 ) {
			$_POST['x1']     = $_POST['x1'] * $_POST['oitar'];
			$_POST['y1']     = $_POST['y1'] * $_POST['oitar'];
			$_POST['width']  = $_POST['width'] * $_POST['oitar'];
			$_POST['height'] = $_POST['height'] * $_POST['oitar'];
		}

		$attachment_id = absint( $_POST['attachment_id'] );
		$original      = get_attached_file( $attachment_id );

		$dimensions = $this->get_header_dimensions(
			array(
				'height' => $_POST['height'],
				'width'  => $_POST['width'],
			)
		);
		$height     = $dimensions['dst_height'];
		$width      = $dimensions['dst_width'];

		if ( empty( $_POST['skip-cropping'] ) ) {
			$cropped = wp_crop_image(
				$attachment_id,
				(int) $_POST['x1'],
				(int) $_POST['y1'],
				(int) $_POST['width'],
				(int) $_POST['height'],
				$width,
				$height
			);
		} elseif ( ! empty( $_POST['create-new-attachment'] ) ) {
			$cropped = _copy_image_file( $attachment_id );
		} else {
			$cropped = get_attached_file( $attachment_id );
		}

		if ( ! $cropped || is_wp_error( $cropped ) ) {
			wp_die( __( 'Image could not be processed. Please go back and try again.' ), __( 'Image Processing Error' ) );
		}

		/** This filter is documented in wp-admin/includes/class-custom-image-header.php */
		$cropped = apply_filters( 'wp_create_file_in_uploads', $cropped, $attachment_id ); // For replication.

		$attachment = wp_copy_parent_attachment_properties( $cropped, $attachment_id, 'custom-header' );

		if ( ! empty( $_POST['create-new-attachment'] ) ) {
			unset( $attachment['ID'] );
		}

		// Update the attachment.
		$attachment_id = $this->insert_attachment( $attachment, $cropped );

		$url = wp_get_attachment_url( $attachment_id );
		$this->set_header_image( compact( 'url', 'attachment_id', 'width', 'height' ) );

		// Cleanup.
		$medium = str_replace( wp_basename( $original ), 'midsize-' . wp_basename( $original ), $original );
		if ( file_exists( $medium ) ) {
			wp_delete_file( $medium );
		}

		if ( empty( $_POST['create-new-attachment'] ) && empty( $_POST['skip-cropping'] ) ) {
			wp_delete_file( $original );
		}

		return $this->finished();
	}

	/**
	 * Displays last step of custom header image page.
	 *
	 * @since 2.1.0
	 */
	public function finished() {
		$this->updated = true;
		$this->step_1();
	}

	/**
	 * Displays the page based on the current step.
	 *
	 * @since 2.1.0
	 */
	public function admin_page() {
		if ( ! current_user_can( 'edit_theme_options' ) ) {
			wp_die( __( 'Sorry, you are not allowed to customize headers.' ) );
		}

		$step = $this->step();

		if ( 2 === $step ) {
			$this->step_2();
		} elseif ( 3 === $step ) {
			$this->step_3();
		} else {
			$this->step_1();
		}
	}

	/**
	 * Unused since 3.5.0.
	 *
	 * @since 3.4.0
	 *
	 * @param array $form_fields
	 * @return array $form_fields
	 */
	public function attachment_fields_to_edit( $form_fields ) {
		return $form_fields;
	}

	/**
	 * Unused since 3.5.0.
	 *
	 * @since 3.4.0
	 *
	 * @param array $tabs
	 * @return array $tabs
	 */
	public function filter_upload_tabs( $tabs ) {
		return $tabs;
	}

	/**
	 * Chooses a header image, selected from existing uploaded and default headers,
	 * or provides an array of uploaded header data (either new, or from media library).
	 *
	 * @since 3.4.0
	 *
	 * @param mixed $choice Which header image to select. Allows for values of 'random-default-image',
	 *                      for randomly cycling among the default images; 'random-uploaded-image',
	 *                      for randomly cycling among the uploaded images; the key of a default image
	 *                      registered for that theme; and the key of an image uploaded for that theme
	 *                      (the attachment ID of the image). Or an array of arguments: attachment_id,
	 *                      url, width, height. All are required.
	 */
	final public function set_header_image( $choice ) {
		if ( is_array( $choice ) || is_object( $choice ) ) {
			$choice = (array) $choice;

			if ( ! isset( $choice['attachment_id'] ) || ! isset( $choice['url'] ) ) {
				return;
			}

			$choice['url'] = sanitize_url( $choice['url'] );

			$header_image_data = (object) array(
				'attachment_id' => $choice['attachment_id'],
				'url'           => $choice['url'],
				'thumbnail_url' => $choice['url'],
				'height'        => $choice['height'],
				'width'         => $choice['width'],
			);

			update_post_meta( $choice['attachment_id'], '_wp_attachment_is_custom_header', get_stylesheet() );

			set_theme_mod( 'header_image', $choice['url'] );
			set_theme_mod( 'header_image_data', $header_image_data );

			return;
		}

		if ( in_array( $choice, array( 'remove-header', 'random-default-image', 'random-uploaded-image' ), true ) ) {
			set_theme_mod( 'header_image', $choice );
			remove_theme_mod( 'header_image_data' );

			return;
		}

		$uploaded = get_uploaded_header_images();

		if ( $uploaded && isset( $uploaded[ $choice ] ) ) {
			$header_image_data = $uploaded[ $choice ];
		} else {
			$this->process_default_headers();
			if ( isset( $this->default_headers[ $choice ] ) ) {
				$header_image_data = $this->default_headers[ $choice ];
			} else {
				return;
			}
		}

		set_theme_mod( 'header_image', sanitize_url( $header_image_data['url'] ) );
		set_theme_mod( 'header_image_data', $header_image_data );
	}

	/**
	 * Removes a header image.
	 *
	 * @since 3.4.0
	 */
	final public function remove_header_image() {
		$this->set_header_image( 'remove-header' );
	}

	/**
	 * Resets a header image to the default image for the theme.
	 *
	 * This method does not do anything if the theme does not have a default header image.
	 *
	 * @since 3.4.0
	 */
	final public function reset_header_image() {
		$this->process_default_headers();
		$default = get_theme_support( 'custom-header', 'default-image' );

		if ( ! $default ) {
			$this->remove_header_image();
			return;
		}

		$default = sprintf( $default, get_template_directory_uri(), get_stylesheet_directory_uri() );

		$default_data = array();
		foreach ( $this->default_headers as $header => $details ) {
			if ( $details['url'] === $default ) {
				$default_data = $details;
				break;
			}
		}

		set_theme_mod( 'header_image', $default );
		set_theme_mod( 'header_image_data', (object) $default_data );
	}

	/**
	 * Calculates width and height based on what the currently selected theme supports.
	 *
	 * @since 3.9.0
	 *
	 * @param array $dimensions
	 * @return array dst_height and dst_width of header image.
	 */
	final public function get_header_dimensions( $dimensions ) {
		$max_width       = 0;
		$width           = absint( $dimensions['width'] );
		$height          = absint( $dimensions['height'] );
		$theme_height    = get_theme_support( 'custom-header', 'height' );
		$theme_width     = get_theme_support( 'custom-header', 'width' );
		$has_flex_width  = current_theme_supports( 'custom-header', 'flex-width' );
		$has_flex_height = current_theme_supports( 'custom-header', 'flex-height' );
		$has_max_width   = current_theme_supports( 'custom-header', 'max-width' );
		$dst             = array(
			'dst_height' => null,
			'dst_width'  => null,
		);

		// For flex, limit size of image displayed to 1500px unless theme says otherwise.
		if ( $has_flex_width ) {
			$max_width = 1500;
		}

		if ( $has_max_width ) {
			$max_width = max( $max_width, get_theme_support( 'custom-header', 'max-width' ) );
		}
		$max_width = max( $max_width, $theme_width );

		if ( $has_flex_height && ( ! $has_flex_width || $width > $max_width ) ) {
			$dst['dst_height'] = absint( $height * ( $max_width / $width ) );
		} elseif ( $has_flex_height && $has_flex_width ) {
			$dst['dst_height'] = $height;
		} else {
			$dst['dst_height'] = $theme_height;
		}

		if ( $has_flex_width && ( ! $has_flex_height || $width > $max_width ) ) {
			$dst['dst_width'] = absint( $width * ( $max_width / $width ) );
		} elseif ( $has_flex_width && $has_flex_height ) {
			$dst['dst_width'] = $width;
		} else {
			$dst['dst_width'] = $theme_width;
		}

		return $dst;
	}

	/**
	 * Creates an attachment 'object'.
	 *
	 * @since 3.9.0
	 * @deprecated 6.5.0
	 *
	 * @param string $cropped              Cropped image URL.
	 * @param int    $parent_attachment_id Attachment ID of parent image.
	 * @return array An array with attachment object data.
	 */
	final public function create_attachment_object( $cropped, $parent_attachment_id ) {
		_deprecated_function( __METHOD__, '6.5.0', 'wp_copy_parent_attachment_properties()' );
		$parent     = get_post( $parent_attachment_id );
		$parent_url = wp_get_attachment_url( $parent->ID );
		$url        = str_replace( wp_basename( $parent_url ), wp_basename( $cropped ), $parent_url );

		$size       = wp_getimagesize( $cropped );
		$image_type = ( $size ) ? $size['mime'] : 'image/jpeg';

		$attachment = array(
			'ID'             => $parent_attachment_id,
			'post_title'     => wp_basename( $cropped ),
			'post_mime_type' => $image_type,
			'guid'           => $url,
			'context'        => 'custom-header',
			'post_parent'    => $parent_attachment_id,
		);

		return $attachment;
	}

	/**
	 * Inserts an attachment and its metadata.
	 *
	 * @since 3.9.0
	 *
	 * @param array  $attachment An array with attachment object data.
	 * @param string $cropped    File path to cropped image.
	 * @return int Attachment ID.
	 */
	final public function insert_attachment( $attachment, $cropped ) {
		$parent_id = isset( $attachment['post_parent'] ) ? $attachment['post_parent'] : null;
		unset( $attachment['post_parent'] );

		$attachment_id = wp_insert_attachment( $attachment, $cropped );
		$metadata      = wp_generate_attachment_metadata( $attachment_id, $cropped );

		// If this is a crop, save the original attachment ID as metadata.
		if ( $parent_id ) {
			$metadata['attachment_parent'] = $parent_id;
		}

		/**
		 * Filters the header image attachment metadata.
		 *
		 * @since 3.9.0
		 *
		 * @see wp_generate_attachment_metadata()
		 *
		 * @param array $metadata Attachment metadata.
		 */
		$metadata = apply_filters( 'wp_header_image_attachment_metadata', $metadata );

		wp_update_attachment_metadata( $attachment_id, $metadata );

		return $attachment_id;
	}

	/**
	 * Gets attachment uploaded by Media Manager, crops it, then saves it as a
	 * new object. Returns JSON-encoded object details.
	 *
	 * @since 3.9.0
	 */
	public function ajax_header_crop() {
		check_ajax_referer( 'image_editor-' . $_POST['id'], 'nonce' );

		if ( ! current_user_can( 'edit_theme_options' ) ) {
			wp_send_json_error();
		}

		if ( ! current_theme_supports( 'custom-header', 'uploads' ) ) {
			wp_send_json_error();
		}

		$crop_details = $_POST['cropDetails'];

		$dimensions = $this->get_header_dimensions(
			array(
				'height' => $crop_details['height'],
				'width'  => $crop_details['width'],
			)
		);

		$attachment_id = absint( $_POST['id'] );

		$cropped = wp_crop_image(
			$attachment_id,
			(int) $crop_details['x1'],
			(int) $crop_details['y1'],
			(int) $crop_details['width'],
			(int) $crop_details['height'],
			(int) $dimensions['dst_width'],
			(int) $dimensions['dst_height']
		);

		if ( ! $cropped || is_wp_error( $cropped ) ) {
			wp_send_json_error( array( 'message' => __( 'Image could not be processed. Please go back and try again.' ) ) );
		}

		/** This filter is documented in wp-admin/includes/class-custom-image-header.php */
		$cropped = apply_filters( 'wp_create_file_in_uploads', $cropped, $attachment_id ); // For replication.

		$attachment = wp_copy_parent_attachment_properties( $cropped, $attachment_id, 'custom-header' );

		$previous = $this->get_previous_crop( $attachment );

		if ( $previous ) {
			$attachment['ID'] = $previous;
		} else {
			unset( $attachment['ID'] );
		}

		$new_attachment_id = $this->insert_attachment( $attachment, $cropped );

		$attachment['attachment_id'] = $new_attachment_id;
		$attachment['url']           = wp_get_attachment_url( $new_attachment_id );

		$attachment['width']  = $dimensions['dst_width'];
		$attachment['height'] = $dimensions['dst_height'];

		wp_send_json_success( $attachment );
	}

	/**
	 * Given an attachment ID for a header image, updates its "last used"
	 * timestamp to now.
	 *
	 * Triggered when the user tries adds a new header image from the
	 * Media Manager, even if s/he doesn't save that change.
	 *
	 * @since 3.9.0
	 */
	public function ajax_header_add() {
		check_ajax_referer( 'header-add', 'nonce' );

		if ( ! current_user_can( 'edit_theme_options' ) ) {
			wp_send_json_error();
		}

		$attachment_id = absint( $_POST['attachment_id'] );
		if ( $attachment_id < 1 ) {
			wp_send_json_error();
		}

		$key = '_wp_attachment_custom_header_last_used_' . get_stylesheet();
		update_post_meta( $attachment_id, $key, time() );
		update_post_meta( $attachment_id, '_wp_attachment_is_custom_header', get_stylesheet() );

		wp_send_json_success();
	}

	/**
	 * Given an attachment ID for a header image, unsets it as a user-uploaded
	 * header image for the active theme.
	 *
	 * Triggered when the user clicks the overlay "X" button next to each image
	 * choice in the Customizer's Header tool.
	 *
	 * @since 3.9.0
	 */
	public function ajax_header_remove() {
		check_ajax_referer( 'header-remove', 'nonce' );

		if ( ! current_user_can( 'edit_theme_options' ) ) {
			wp_send_json_error();
		}

		$attachment_id = absint( $_POST['attachment_id'] );
		if ( $attachment_id < 1 ) {
			wp_send_json_error();
		}

		$key = '_wp_attachment_custom_header_last_used_' . get_stylesheet();
		delete_post_meta( $attachment_id, $key );
		delete_post_meta( $attachment_id, '_wp_attachment_is_custom_header', get_stylesheet() );

		wp_send_json_success();
	}

	/**
	 * Updates the last-used postmeta on a header image attachment after saving a new header image via the Customizer.
	 *
	 * @since 3.9.0
	 *
	 * @param WP_Customize_Manager $wp_customize Customize manager.
	 */
	public function customize_set_last_used( $wp_customize ) {

		$header_image_data_setting = $wp_customize->get_setting( 'header_image_data' );

		if ( ! $header_image_data_setting ) {
			return;
		}

		$data = $header_image_data_setting->post_value();

		if ( ! isset( $data['attachment_id'] ) ) {
			return;
		}

		$attachment_id = $data['attachment_id'];
		$key           = '_wp_attachment_custom_header_last_used_' . get_stylesheet();
		update_post_meta( $attachment_id, $key, time() );
	}

	/**
	 * Gets the details of default header images if defined.
	 *
	 * @since 3.9.0
	 *
	 * @return array Default header images.
	 */
	public function get_default_header_images() {
		$this->process_default_headers();

		// Get the default image if there is one.
		$default = get_theme_support( 'custom-header', 'default-image' );

		if ( ! $default ) { // If not, easy peasy.
			return $this->default_headers;
		}

		$default = sprintf( $default, get_template_directory_uri(), get_stylesheet_directory_uri() );

		$already_has_default = false;

		foreach ( $this->default_headers as $k => $h ) {
			if ( $h['url'] === $default ) {
				$already_has_default = true;
				break;
			}
		}

		if ( $already_has_default ) {
			return $this->default_headers;
		}

		// If the one true image isn't included in the default set, prepend it.
		$header_images            = array();
		$header_images['default'] = array(
			'url'           => $default,
			'thumbnail_url' => $default,
			'description'   => 'Default',
		);

		// The rest of the set comes after.
		return array_merge( $header_images, $this->default_headers );
	}

	/**
	 * Gets the previously uploaded header images.
	 *
	 * @since 3.9.0
	 *
	 * @return array Uploaded header images.
	 */
	public function get_uploaded_header_images() {
		$header_images = get_uploaded_header_images();
		$timestamp_key = '_wp_attachment_custom_header_last_used_' . get_stylesheet();
		$alt_text_key  = '_wp_attachment_image_alt';

		foreach ( $header_images as &$header_image ) {
			$header_meta               = get_post_meta( $header_image['attachment_id'] );
			$header_image['timestamp'] = isset( $header_meta[ $timestamp_key ] ) ? $header_meta[ $timestamp_key ] : '';
			$header_image['alt_text']  = isset( $header_meta[ $alt_text_key ] ) ? $header_meta[ $alt_text_key ] : '';
		}

		return $header_images;
	}

	/**
	 * Gets the ID of a previous crop from the same base image.
	 *
	 * @since 4.9.0
	 *
	 * @param array $attachment An array with a cropped attachment object data.
	 * @return int|false An attachment ID if one exists. False if none.
	 */
	public function get_previous_crop( $attachment ) {
		$header_images = $this->get_uploaded_header_images();

		// Bail early if there are no header images.
		if ( empty( $header_images ) ) {
			return false;
		}

		$previous = false;

		foreach ( $header_images as $image ) {
			if ( $image['attachment_parent'] === $attachment['post_parent'] ) {
				$previous = $image['attachment_id'];
				break;
			}
		}

		return $previous;
	}
}
file.php000064400000277571151327343360006226 0ustar00<?php
/**
 * Filesystem API: Top-level functionality
 *
 * Functions for reading, writing, modifying, and deleting files on the file system.
 * Includes functionality for theme-specific files as well as operations for uploading,
 * archiving, and rendering output when necessary.
 *
 * @package WordPress
 * @subpackage Filesystem
 * @since 2.3.0
 */

/** The descriptions for theme files. */
$wp_file_descriptions = array(
	'functions.php'         => __( 'Theme Functions' ),
	'header.php'            => __( 'Theme Header' ),
	'footer.php'            => __( 'Theme Footer' ),
	'sidebar.php'           => __( 'Sidebar' ),
	'comments.php'          => __( 'Comments' ),
	'searchform.php'        => __( 'Search Form' ),
	'404.php'               => __( '404 Template' ),
	'link.php'              => __( 'Links Template' ),
	'theme.json'            => __( 'Theme Styles & Block Settings' ),
	// Archives.
	'index.php'             => __( 'Main Index Template' ),
	'archive.php'           => __( 'Archives' ),
	'author.php'            => __( 'Author Template' ),
	'taxonomy.php'          => __( 'Taxonomy Template' ),
	'category.php'          => __( 'Category Template' ),
	'tag.php'               => __( 'Tag Template' ),
	'home.php'              => __( 'Posts Page' ),
	'search.php'            => __( 'Search Results' ),
	'date.php'              => __( 'Date Template' ),
	// Content.
	'singular.php'          => __( 'Singular Template' ),
	'single.php'            => __( 'Single Post' ),
	'page.php'              => __( 'Single Page' ),
	'front-page.php'        => __( 'Homepage' ),
	'privacy-policy.php'    => __( 'Privacy Policy Page' ),
	// Attachments.
	'attachment.php'        => __( 'Attachment Template' ),
	'image.php'             => __( 'Image Attachment Template' ),
	'video.php'             => __( 'Video Attachment Template' ),
	'audio.php'             => __( 'Audio Attachment Template' ),
	'application.php'       => __( 'Application Attachment Template' ),
	// Embeds.
	'embed.php'             => __( 'Embed Template' ),
	'embed-404.php'         => __( 'Embed 404 Template' ),
	'embed-content.php'     => __( 'Embed Content Template' ),
	'header-embed.php'      => __( 'Embed Header Template' ),
	'footer-embed.php'      => __( 'Embed Footer Template' ),
	// Stylesheets.
	'style.css'             => __( 'Stylesheet' ),
	'editor-style.css'      => __( 'Visual Editor Stylesheet' ),
	'editor-style-rtl.css'  => __( 'Visual Editor RTL Stylesheet' ),
	'rtl.css'               => __( 'RTL Stylesheet' ),
	// Other.
	'my-hacks.php'          => __( 'my-hacks.php (legacy hacks support)' ),
	'.htaccess'             => __( '.htaccess (for rewrite rules )' ),
	// Deprecated files.
	'wp-layout.css'         => __( 'Stylesheet' ),
	'wp-comments.php'       => __( 'Comments Template' ),
	'wp-comments-popup.php' => __( 'Popup Comments Template' ),
	'comments-popup.php'    => __( 'Popup Comments' ),
);

/**
 * Gets the description for standard WordPress theme files.
 *
 * @since 1.5.0
 *
 * @global array $wp_file_descriptions Theme file descriptions.
 * @global array $allowed_files        List of allowed files.
 *
 * @param string $file Filesystem path or filename.
 * @return string Description of file from $wp_file_descriptions or basename of $file if description doesn't exist.
 *                Appends 'Page Template' to basename of $file if the file is a page template.
 */
function get_file_description( $file ) {
	global $wp_file_descriptions, $allowed_files;

	$dirname   = pathinfo( $file, PATHINFO_DIRNAME );
	$file_path = $allowed_files[ $file ];

	if ( isset( $wp_file_descriptions[ basename( $file ) ] ) && '.' === $dirname ) {
		return $wp_file_descriptions[ basename( $file ) ];
	} elseif ( file_exists( $file_path ) && is_file( $file_path ) ) {
		$template_data = implode( '', file( $file_path ) );

		if ( preg_match( '|Template Name:(.*)$|mi', $template_data, $name ) ) {
			/* translators: %s: Template name. */
			return sprintf( __( '%s Page Template' ), _cleanup_header_comment( $name[1] ) );
		}
	}

	return trim( basename( $file ) );
}

/**
 * Gets the absolute filesystem path to the root of the WordPress installation.
 *
 * @since 1.5.0
 *
 * @return string Full filesystem path to the root of the WordPress installation.
 */
function get_home_path() {
	$home    = set_url_scheme( get_option( 'home' ), 'http' );
	$siteurl = set_url_scheme( get_option( 'siteurl' ), 'http' );

	if ( ! empty( $home ) && 0 !== strcasecmp( $home, $siteurl ) ) {
		$wp_path_rel_to_home = str_ireplace( $home, '', $siteurl ); /* $siteurl - $home */
		$pos                 = strripos( str_replace( '\\', '/', $_SERVER['SCRIPT_FILENAME'] ), trailingslashit( $wp_path_rel_to_home ) );
		$home_path           = substr( $_SERVER['SCRIPT_FILENAME'], 0, $pos );
		$home_path           = trailingslashit( $home_path );
	} else {
		$home_path = ABSPATH;
	}

	return str_replace( '\\', '/', $home_path );
}

/**
 * Returns a listing of all files in the specified folder and all subdirectories up to 100 levels deep.
 *
 * The depth of the recursiveness can be controlled by the $levels param.
 *
 * @since 2.6.0
 * @since 4.9.0 Added the `$exclusions` parameter.
 * @since 6.3.0 Added the `$include_hidden` parameter.
 *
 * @param string   $folder         Optional. Full path to folder. Default empty.
 * @param int      $levels         Optional. Levels of folders to follow, Default 100 (PHP Loop limit).
 * @param string[] $exclusions     Optional. List of folders and files to skip.
 * @param bool     $include_hidden Optional. Whether to include details of hidden ("." prefixed) files.
 *                                 Default false.
 * @return string[]|false Array of files on success, false on failure.
 */
function list_files( $folder = '', $levels = 100, $exclusions = array(), $include_hidden = false ) {
	if ( empty( $folder ) ) {
		return false;
	}

	$folder = trailingslashit( $folder );

	if ( ! $levels ) {
		return false;
	}

	$files = array();

	$dir = @opendir( $folder );

	if ( $dir ) {
		while ( ( $file = readdir( $dir ) ) !== false ) {
			// Skip current and parent folder links.
			if ( in_array( $file, array( '.', '..' ), true ) ) {
				continue;
			}

			// Skip hidden and excluded files.
			if ( ( ! $include_hidden && '.' === $file[0] ) || in_array( $file, $exclusions, true ) ) {
				continue;
			}

			if ( is_dir( $folder . $file ) ) {
				$files2 = list_files( $folder . $file, $levels - 1, array(), $include_hidden );
				if ( $files2 ) {
					$files = array_merge( $files, $files2 );
				} else {
					$files[] = $folder . $file . '/';
				}
			} else {
				$files[] = $folder . $file;
			}
		}

		closedir( $dir );
	}

	return $files;
}

/**
 * Gets the list of file extensions that are editable in plugins.
 *
 * @since 4.9.0
 *
 * @param string $plugin Path to the plugin file relative to the plugins directory.
 * @return string[] Array of editable file extensions.
 */
function wp_get_plugin_file_editable_extensions( $plugin ) {

	$default_types = array(
		'bash',
		'conf',
		'css',
		'diff',
		'htm',
		'html',
		'http',
		'inc',
		'include',
		'js',
		'json',
		'jsx',
		'less',
		'md',
		'patch',
		'php',
		'php3',
		'php4',
		'php5',
		'php7',
		'phps',
		'phtml',
		'sass',
		'scss',
		'sh',
		'sql',
		'svg',
		'text',
		'txt',
		'xml',
		'yaml',
		'yml',
	);

	/**
	 * Filters the list of file types allowed for editing in the plugin file editor.
	 *
	 * @since 2.8.0
	 * @since 4.9.0 Added the `$plugin` parameter.
	 *
	 * @param string[] $default_types An array of editable plugin file extensions.
	 * @param string   $plugin        Path to the plugin file relative to the plugins directory.
	 */
	$file_types = (array) apply_filters( 'editable_extensions', $default_types, $plugin );

	return $file_types;
}

/**
 * Gets the list of file extensions that are editable for a given theme.
 *
 * @since 4.9.0
 *
 * @param WP_Theme $theme Theme object.
 * @return string[] Array of editable file extensions.
 */
function wp_get_theme_file_editable_extensions( $theme ) {

	$default_types = array(
		'bash',
		'conf',
		'css',
		'diff',
		'htm',
		'html',
		'http',
		'inc',
		'include',
		'js',
		'json',
		'jsx',
		'less',
		'md',
		'patch',
		'php',
		'php3',
		'php4',
		'php5',
		'php7',
		'phps',
		'phtml',
		'sass',
		'scss',
		'sh',
		'sql',
		'svg',
		'text',
		'txt',
		'xml',
		'yaml',
		'yml',
	);

	/**
	 * Filters the list of file types allowed for editing in the theme file editor.
	 *
	 * @since 4.4.0
	 *
	 * @param string[] $default_types An array of editable theme file extensions.
	 * @param WP_Theme $theme         The active theme object.
	 */
	$file_types = apply_filters( 'wp_theme_editor_filetypes', $default_types, $theme );

	// Ensure that default types are still there.
	return array_unique( array_merge( $file_types, $default_types ) );
}

/**
 * Prints file editor templates (for plugins and themes).
 *
 * @since 4.9.0
 */
function wp_print_file_editor_templates() {
	?>
	<script type="text/html" id="tmpl-wp-file-editor-notice">
		<div class="notice inline notice-{{ data.type || 'info' }} {{ data.alt ? 'notice-alt' : '' }} {{ data.dismissible ? 'is-dismissible' : '' }} {{ data.classes || '' }}">
			<# if ( 'php_error' === data.code ) { #>
				<p>
					<?php
					printf(
						/* translators: 1: Line number, 2: File path. */
						__( 'Your PHP code changes were not applied due to an error on line %1$s of file %2$s. Please fix and try saving again.' ),
						'{{ data.line }}',
						'{{ data.file }}'
					);
					?>
				</p>
				<pre>{{ data.message }}</pre>
			<# } else if ( 'file_not_writable' === data.code ) { #>
				<p>
					<?php
					printf(
						/* translators: %s: Documentation URL. */
						__( 'You need to make this file writable before you can save your changes. See <a href="%s">Changing File Permissions</a> for more information.' ),
						__( 'https://developer.wordpress.org/advanced-administration/server/file-permissions/' )
					);
					?>
				</p>
			<# } else { #>
				<p>{{ data.message || data.code }}</p>

				<# if ( 'lint_errors' === data.code ) { #>
					<p>
						<# var elementId = 'el-' + String( Math.random() ); #>
						<input id="{{ elementId }}"  type="checkbox">
						<label for="{{ elementId }}"><?php _e( 'Update anyway, even though it might break your site?' ); ?></label>
					</p>
				<# } #>
			<# } #>
			<# if ( data.dismissible ) { #>
				<button type="button" class="notice-dismiss"><span class="screen-reader-text">
					<?php
					/* translators: Hidden accessibility text. */
					_e( 'Dismiss' );
					?>
				</span></button>
			<# } #>
		</div>
	</script>
	<?php
}

/**
 * Attempts to edit a file for a theme or plugin.
 *
 * When editing a PHP file, loopback requests will be made to the admin and the homepage
 * to attempt to see if there is a fatal error introduced. If so, the PHP change will be
 * reverted.
 *
 * @since 4.9.0
 *
 * @param string[] $args {
 *     Args. Note that all of the arg values are already unslashed. They are, however,
 *     coming straight from `$_POST` and are not validated or sanitized in any way.
 *
 *     @type string $file       Relative path to file.
 *     @type string $plugin     Path to the plugin file relative to the plugins directory.
 *     @type string $theme      Theme being edited.
 *     @type string $newcontent New content for the file.
 *     @type string $nonce      Nonce.
 * }
 * @return true|WP_Error True on success or `WP_Error` on failure.
 */
function wp_edit_theme_plugin_file( $args ) {
	if ( empty( $args['file'] ) ) {
		return new WP_Error( 'missing_file' );
	}

	if ( 0 !== validate_file( $args['file'] ) ) {
		return new WP_Error( 'bad_file' );
	}

	if ( ! isset( $args['newcontent'] ) ) {
		return new WP_Error( 'missing_content' );
	}

	if ( ! isset( $args['nonce'] ) ) {
		return new WP_Error( 'missing_nonce' );
	}

	$file    = $args['file'];
	$content = $args['newcontent'];

	$plugin    = null;
	$theme     = null;
	$real_file = null;

	if ( ! empty( $args['plugin'] ) ) {
		$plugin = $args['plugin'];

		if ( ! current_user_can( 'edit_plugins' ) ) {
			return new WP_Error( 'unauthorized', __( 'Sorry, you are not allowed to edit plugins for this site.' ) );
		}

		if ( ! wp_verify_nonce( $args['nonce'], 'edit-plugin_' . $file ) ) {
			return new WP_Error( 'nonce_failure' );
		}

		if ( ! array_key_exists( $plugin, get_plugins() ) ) {
			return new WP_Error( 'invalid_plugin' );
		}

		if ( 0 !== validate_file( $file, get_plugin_files( $plugin ) ) ) {
			return new WP_Error( 'bad_plugin_file_path', __( 'Sorry, that file cannot be edited.' ) );
		}

		$editable_extensions = wp_get_plugin_file_editable_extensions( $plugin );

		$real_file = WP_PLUGIN_DIR . '/' . $file;

		$is_active = in_array(
			$plugin,
			(array) get_option( 'active_plugins', array() ),
			true
		);

	} elseif ( ! empty( $args['theme'] ) ) {
		$stylesheet = $args['theme'];

		if ( 0 !== validate_file( $stylesheet ) ) {
			return new WP_Error( 'bad_theme_path' );
		}

		if ( ! current_user_can( 'edit_themes' ) ) {
			return new WP_Error( 'unauthorized', __( 'Sorry, you are not allowed to edit templates for this site.' ) );
		}

		$theme = wp_get_theme( $stylesheet );
		if ( ! $theme->exists() ) {
			return new WP_Error( 'non_existent_theme', __( 'The requested theme does not exist.' ) );
		}

		if ( ! wp_verify_nonce( $args['nonce'], 'edit-theme_' . $stylesheet . '_' . $file ) ) {
			return new WP_Error( 'nonce_failure' );
		}

		if ( $theme->errors() && 'theme_no_stylesheet' === $theme->errors()->get_error_code() ) {
			return new WP_Error(
				'theme_no_stylesheet',
				__( 'The requested theme does not exist.' ) . ' ' . $theme->errors()->get_error_message()
			);
		}

		$editable_extensions = wp_get_theme_file_editable_extensions( $theme );

		$allowed_files = array();
		foreach ( $editable_extensions as $type ) {
			switch ( $type ) {
				case 'php':
					$allowed_files = array_merge( $allowed_files, $theme->get_files( 'php', -1 ) );
					break;
				case 'css':
					$style_files                = $theme->get_files( 'css', -1 );
					$allowed_files['style.css'] = $style_files['style.css'];
					$allowed_files              = array_merge( $allowed_files, $style_files );
					break;
				default:
					$allowed_files = array_merge( $allowed_files, $theme->get_files( $type, -1 ) );
					break;
			}
		}

		// Compare based on relative paths.
		if ( 0 !== validate_file( $file, array_keys( $allowed_files ) ) ) {
			return new WP_Error( 'disallowed_theme_file', __( 'Sorry, that file cannot be edited.' ) );
		}

		$real_file = $theme->get_stylesheet_directory() . '/' . $file;

		$is_active = ( get_stylesheet() === $stylesheet || get_template() === $stylesheet );

	} else {
		return new WP_Error( 'missing_theme_or_plugin' );
	}

	// Ensure file is real.
	if ( ! is_file( $real_file ) ) {
		return new WP_Error( 'file_does_not_exist', __( 'File does not exist! Please double check the name and try again.' ) );
	}

	// Ensure file extension is allowed.
	$extension = null;
	if ( preg_match( '/\.([^.]+)$/', $real_file, $matches ) ) {
		$extension = strtolower( $matches[1] );
		if ( ! in_array( $extension, $editable_extensions, true ) ) {
			return new WP_Error( 'illegal_file_type', __( 'Files of this type are not editable.' ) );
		}
	}

	$previous_content = file_get_contents( $real_file );

	if ( ! is_writable( $real_file ) ) {
		return new WP_Error( 'file_not_writable' );
	}

	$f = fopen( $real_file, 'w+' );

	if ( false === $f ) {
		return new WP_Error( 'file_not_writable' );
	}

	$written = fwrite( $f, $content );
	fclose( $f );

	if ( false === $written ) {
		return new WP_Error( 'unable_to_write', __( 'Unable to write to file.' ) );
	}

	wp_opcache_invalidate( $real_file, true );

	if ( $is_active && 'php' === $extension ) {

		$scrape_key   = md5( rand() );
		$transient    = 'scrape_key_' . $scrape_key;
		$scrape_nonce = (string) rand();
		// It shouldn't take more than 60 seconds to make the two loopback requests.
		set_transient( $transient, $scrape_nonce, 60 );

		$cookies       = wp_unslash( $_COOKIE );
		$scrape_params = array(
			'wp_scrape_key'   => $scrape_key,
			'wp_scrape_nonce' => $scrape_nonce,
		);
		$headers       = array(
			'Cache-Control' => 'no-cache',
		);

		/** This filter is documented in wp-includes/class-wp-http-streams.php */
		$sslverify = apply_filters( 'https_local_ssl_verify', false );

		// Include Basic auth in loopback requests.
		if ( isset( $_SERVER['PHP_AUTH_USER'] ) && isset( $_SERVER['PHP_AUTH_PW'] ) ) {
			$headers['Authorization'] = 'Basic ' . base64_encode( wp_unslash( $_SERVER['PHP_AUTH_USER'] ) . ':' . wp_unslash( $_SERVER['PHP_AUTH_PW'] ) );
		}

		// Make sure PHP process doesn't die before loopback requests complete.
		if ( function_exists( 'set_time_limit' ) ) {
			set_time_limit( 5 * MINUTE_IN_SECONDS );
		}

		// Time to wait for loopback requests to finish.
		$timeout = 100; // 100 seconds.

		$needle_start = "###### wp_scraping_result_start:$scrape_key ######";
		$needle_end   = "###### wp_scraping_result_end:$scrape_key ######";

		// Attempt loopback request to editor to see if user just whitescreened themselves.
		if ( $plugin ) {
			$url = add_query_arg( compact( 'plugin', 'file' ), admin_url( 'plugin-editor.php' ) );
		} elseif ( isset( $stylesheet ) ) {
			$url = add_query_arg(
				array(
					'theme' => $stylesheet,
					'file'  => $file,
				),
				admin_url( 'theme-editor.php' )
			);
		} else {
			$url = admin_url();
		}

		if ( function_exists( 'session_status' ) && PHP_SESSION_ACTIVE === session_status() ) {
			/*
			 * Close any active session to prevent HTTP requests from timing out
			 * when attempting to connect back to the site.
			 */
			session_write_close();
		}

		$url                    = add_query_arg( $scrape_params, $url );
		$r                      = wp_remote_get( $url, compact( 'cookies', 'headers', 'timeout', 'sslverify' ) );
		$body                   = wp_remote_retrieve_body( $r );
		$scrape_result_position = strpos( $body, $needle_start );

		$loopback_request_failure = array(
			'code'    => 'loopback_request_failed',
			'message' => __( 'Unable to communicate back with site to check for fatal errors, so the PHP change was reverted. You will need to upload your PHP file change by some other means, such as by using SFTP.' ),
		);
		$json_parse_failure       = array(
			'code' => 'json_parse_error',
		);

		$result = null;

		if ( false === $scrape_result_position ) {
			$result = $loopback_request_failure;
		} else {
			$error_output = substr( $body, $scrape_result_position + strlen( $needle_start ) );
			$error_output = substr( $error_output, 0, strpos( $error_output, $needle_end ) );
			$result       = json_decode( trim( $error_output ), true );
			if ( empty( $result ) ) {
				$result = $json_parse_failure;
			}
		}

		// Try making request to homepage as well to see if visitors have been whitescreened.
		if ( true === $result ) {
			$url                    = home_url( '/' );
			$url                    = add_query_arg( $scrape_params, $url );
			$r                      = wp_remote_get( $url, compact( 'cookies', 'headers', 'timeout', 'sslverify' ) );
			$body                   = wp_remote_retrieve_body( $r );
			$scrape_result_position = strpos( $body, $needle_start );

			if ( false === $scrape_result_position ) {
				$result = $loopback_request_failure;
			} else {
				$error_output = substr( $body, $scrape_result_position + strlen( $needle_start ) );
				$error_output = substr( $error_output, 0, strpos( $error_output, $needle_end ) );
				$result       = json_decode( trim( $error_output ), true );
				if ( empty( $result ) ) {
					$result = $json_parse_failure;
				}
			}
		}

		delete_transient( $transient );

		if ( true !== $result ) {
			// Roll-back file change.
			file_put_contents( $real_file, $previous_content );
			wp_opcache_invalidate( $real_file, true );

			if ( ! isset( $result['message'] ) ) {
				$message = __( 'An error occurred. Please try again later.' );
			} else {
				$message = $result['message'];
				unset( $result['message'] );
			}

			return new WP_Error( 'php_error', $message, $result );
		}
	}

	if ( $theme instanceof WP_Theme ) {
		$theme->cache_delete();
	}

	return true;
}


/**
 * Returns a filename of a temporary unique file.
 *
 * Please note that the calling function must delete or move the file.
 *
 * The filename is based off the passed parameter or defaults to the current unix timestamp,
 * while the directory can either be passed as well, or by leaving it blank, default to a writable
 * temporary directory.
 *
 * @since 2.6.0
 *
 * @param string $filename Optional. Filename to base the Unique file off. Default empty.
 * @param string $dir      Optional. Directory to store the file in. Default empty.
 * @return string A writable filename.
 */
function wp_tempnam( $filename = '', $dir = '' ) {
	if ( empty( $dir ) ) {
		$dir = get_temp_dir();
	}

	if ( empty( $filename ) || in_array( $filename, array( '.', '/', '\\' ), true ) ) {
		$filename = uniqid();
	}

	// Use the basename of the given file without the extension as the name for the temporary directory.
	$temp_filename = basename( $filename );
	$temp_filename = preg_replace( '|\.[^.]*$|', '', $temp_filename );

	// If the folder is falsey, use its parent directory name instead.
	if ( ! $temp_filename ) {
		return wp_tempnam( dirname( $filename ), $dir );
	}

	// Suffix some random data to avoid filename conflicts.
	$temp_filename .= '-' . wp_generate_password( 6, false );
	$temp_filename .= '.tmp';
	$temp_filename  = wp_unique_filename( $dir, $temp_filename );

	/*
	 * Filesystems typically have a limit of 255 characters for a filename.
	 *
	 * If the generated unique filename exceeds this, truncate the initial
	 * filename and try again.
	 *
	 * As it's possible that the truncated filename may exist, producing a
	 * suffix of "-1" or "-10" which could exceed the limit again, truncate
	 * it to 252 instead.
	 */
	$characters_over_limit = strlen( $temp_filename ) - 252;
	if ( $characters_over_limit > 0 ) {
		$filename = substr( $filename, 0, -$characters_over_limit );
		return wp_tempnam( $filename, $dir );
	}

	$temp_filename = $dir . $temp_filename;

	$fp = @fopen( $temp_filename, 'x' );

	if ( ! $fp && is_writable( $dir ) && file_exists( $temp_filename ) ) {
		return wp_tempnam( $filename, $dir );
	}

	if ( $fp ) {
		fclose( $fp );
	}

	return $temp_filename;
}

/**
 * Makes sure that the file that was requested to be edited is allowed to be edited.
 *
 * Function will die if you are not allowed to edit the file.
 *
 * @since 1.5.0
 *
 * @param string   $file          File the user is attempting to edit.
 * @param string[] $allowed_files Optional. Array of allowed files to edit.
 *                                `$file` must match an entry exactly.
 * @return string|void Returns the file name on success, dies on failure.
 */
function validate_file_to_edit( $file, $allowed_files = array() ) {
	$code = validate_file( $file, $allowed_files );

	if ( ! $code ) {
		return $file;
	}

	switch ( $code ) {
		case 1:
			wp_die( __( 'Sorry, that file cannot be edited.' ) );

			// case 2 :
			// wp_die( __('Sorry, cannot call files with their real path.' ));

		case 3:
			wp_die( __( 'Sorry, that file cannot be edited.' ) );
	}
}

/**
 * Handles PHP uploads in WordPress.
 *
 * Sanitizes file names, checks extensions for mime type, and moves the file
 * to the appropriate directory within the uploads directory.
 *
 * @access private
 * @since 4.0.0
 *
 * @see wp_handle_upload_error
 *
 * @param array       $file      {
 *     Reference to a single element from `$_FILES`. Call the function once for each uploaded file.
 *
 *     @type string $name     The original name of the file on the client machine.
 *     @type string $type     The mime type of the file, if the browser provided this information.
 *     @type string $tmp_name The temporary filename of the file in which the uploaded file was stored on the server.
 *     @type int    $size     The size, in bytes, of the uploaded file.
 *     @type int    $error    The error code associated with this file upload.
 * }
 * @param array|false $overrides {
 *     An array of override parameters for this file, or boolean false if none are provided.
 *
 *     @type callable $upload_error_handler     Function to call when there is an error during the upload process.
 *                                              See {@see wp_handle_upload_error()}.
 *     @type callable $unique_filename_callback Function to call when determining a unique file name for the file.
 *                                              See {@see wp_unique_filename()}.
 *     @type string[] $upload_error_strings     The strings that describe the error indicated in
 *                                              `$_FILES[{form field}]['error']`.
 *     @type bool     $test_form                Whether to test that the `$_POST['action']` parameter is as expected.
 *     @type bool     $test_size                Whether to test that the file size is greater than zero bytes.
 *     @type bool     $test_type                Whether to test that the mime type of the file is as expected.
 *     @type string[] $mimes                    Array of allowed mime types keyed by their file extension regex.
 * }
 * @param string      $time      Time formatted in 'yyyy/mm'.
 * @param string      $action    Expected value for `$_POST['action']`.
 * @return array {
 *     On success, returns an associative array of file attributes.
 *     On failure, returns `$overrides['upload_error_handler']( &$file, $message )`
 *     or `array( 'error' => $message )`.
 *
 *     @type string $file Filename of the newly-uploaded file.
 *     @type string $url  URL of the newly-uploaded file.
 *     @type string $type Mime type of the newly-uploaded file.
 * }
 */
function _wp_handle_upload( &$file, $overrides, $time, $action ) {
	// The default error handler.
	if ( ! function_exists( 'wp_handle_upload_error' ) ) {
		function wp_handle_upload_error( &$file, $message ) {
			return array( 'error' => $message );
		}
	}

	/**
	 * Filters the data for a file before it is uploaded to WordPress.
	 *
	 * The dynamic portion of the hook name, `$action`, refers to the post action.
	 *
	 * Possible hook names include:
	 *
	 *  - `wp_handle_sideload_prefilter`
	 *  - `wp_handle_upload_prefilter`
	 *
	 * @since 2.9.0 as 'wp_handle_upload_prefilter'.
	 * @since 4.0.0 Converted to a dynamic hook with `$action`.
	 *
	 * @param array $file {
	 *     Reference to a single element from `$_FILES`.
	 *
	 *     @type string $name     The original name of the file on the client machine.
	 *     @type string $type     The mime type of the file, if the browser provided this information.
	 *     @type string $tmp_name The temporary filename of the file in which the uploaded file was stored on the server.
	 *     @type int    $size     The size, in bytes, of the uploaded file.
	 *     @type int    $error    The error code associated with this file upload.
	 * }
	 */
	$file = apply_filters( "{$action}_prefilter", $file );

	/**
	 * Filters the override parameters for a file before it is uploaded to WordPress.
	 *
	 * The dynamic portion of the hook name, `$action`, refers to the post action.
	 *
	 * Possible hook names include:
	 *
	 *  - `wp_handle_sideload_overrides`
	 *  - `wp_handle_upload_overrides`
	 *
	 * @since 5.7.0
	 *
	 * @param array|false $overrides An array of override parameters for this file. Boolean false if none are
	 *                               provided. See {@see _wp_handle_upload()}.
	 * @param array       $file      {
	 *     Reference to a single element from `$_FILES`.
	 *
	 *     @type string $name     The original name of the file on the client machine.
	 *     @type string $type     The mime type of the file, if the browser provided this information.
	 *     @type string $tmp_name The temporary filename of the file in which the uploaded file was stored on the server.
	 *     @type int    $size     The size, in bytes, of the uploaded file.
	 *     @type int    $error    The error code associated with this file upload.
	 * }
	 */
	$overrides = apply_filters( "{$action}_overrides", $overrides, $file );

	// You may define your own function and pass the name in $overrides['upload_error_handler'].
	$upload_error_handler = 'wp_handle_upload_error';
	if ( isset( $overrides['upload_error_handler'] ) ) {
		$upload_error_handler = $overrides['upload_error_handler'];
	}

	// You may have had one or more 'wp_handle_upload_prefilter' functions error out the file. Handle that gracefully.
	if ( isset( $file['error'] ) && ! is_numeric( $file['error'] ) && $file['error'] ) {
		return call_user_func_array( $upload_error_handler, array( &$file, $file['error'] ) );
	}

	// Install user overrides. Did we mention that this voids your warranty?

	// You may define your own function and pass the name in $overrides['unique_filename_callback'].
	$unique_filename_callback = null;
	if ( isset( $overrides['unique_filename_callback'] ) ) {
		$unique_filename_callback = $overrides['unique_filename_callback'];
	}

	/*
	 * This may not have originally been intended to be overridable,
	 * but historically has been.
	 */
	if ( isset( $overrides['upload_error_strings'] ) ) {
		$upload_error_strings = $overrides['upload_error_strings'];
	} else {
		// Courtesy of php.net, the strings that describe the error indicated in $_FILES[{form field}]['error'].
		$upload_error_strings = array(
			false,
			sprintf(
				/* translators: 1: upload_max_filesize, 2: php.ini */
				__( 'The uploaded file exceeds the %1$s directive in %2$s.' ),
				'upload_max_filesize',
				'php.ini'
			),
			sprintf(
				/* translators: %s: MAX_FILE_SIZE */
				__( 'The uploaded file exceeds the %s directive that was specified in the HTML form.' ),
				'MAX_FILE_SIZE'
			),
			__( 'The uploaded file was only partially uploaded.' ),
			__( 'No file was uploaded.' ),
			'',
			__( 'Missing a temporary folder.' ),
			__( 'Failed to write file to disk.' ),
			__( 'File upload stopped by extension.' ),
		);
	}

	// All tests are on by default. Most can be turned off by $overrides[{test_name}] = false;
	$test_form = isset( $overrides['test_form'] ) ? $overrides['test_form'] : true;
	$test_size = isset( $overrides['test_size'] ) ? $overrides['test_size'] : true;

	// If you override this, you must provide $ext and $type!!
	$test_type = isset( $overrides['test_type'] ) ? $overrides['test_type'] : true;
	$mimes     = isset( $overrides['mimes'] ) ? $overrides['mimes'] : null;

	// A correct form post will pass this test.
	if ( $test_form && ( ! isset( $_POST['action'] ) || $_POST['action'] !== $action ) ) {
		return call_user_func_array( $upload_error_handler, array( &$file, __( 'Invalid form submission.' ) ) );
	}

	// A successful upload will pass this test. It makes no sense to override this one.
	if ( isset( $file['error'] ) && $file['error'] > 0 ) {
		return call_user_func_array( $upload_error_handler, array( &$file, $upload_error_strings[ $file['error'] ] ) );
	}

	// A properly uploaded file will pass this test. There should be no reason to override this one.
	$test_uploaded_file = 'wp_handle_upload' === $action ? is_uploaded_file( $file['tmp_name'] ) : @is_readable( $file['tmp_name'] );
	if ( ! $test_uploaded_file ) {
		return call_user_func_array( $upload_error_handler, array( &$file, __( 'Specified file failed upload test.' ) ) );
	}

	$test_file_size = 'wp_handle_upload' === $action ? $file['size'] : filesize( $file['tmp_name'] );
	// A non-empty file will pass this test.
	if ( $test_size && ! ( $test_file_size > 0 ) ) {
		if ( is_multisite() ) {
			$error_msg = __( 'File is empty. Please upload something more substantial.' );
		} else {
			$error_msg = sprintf(
				/* translators: 1: php.ini, 2: post_max_size, 3: upload_max_filesize */
				__( 'File is empty. Please upload something more substantial. This error could also be caused by uploads being disabled in your %1$s file or by %2$s being defined as smaller than %3$s in %1$s.' ),
				'php.ini',
				'post_max_size',
				'upload_max_filesize'
			);
		}

		return call_user_func_array( $upload_error_handler, array( &$file, $error_msg ) );
	}

	// A correct MIME type will pass this test. Override $mimes or use the upload_mimes filter.
	if ( $test_type ) {
		$wp_filetype     = wp_check_filetype_and_ext( $file['tmp_name'], $file['name'], $mimes );
		$ext             = empty( $wp_filetype['ext'] ) ? '' : $wp_filetype['ext'];
		$type            = empty( $wp_filetype['type'] ) ? '' : $wp_filetype['type'];
		$proper_filename = empty( $wp_filetype['proper_filename'] ) ? '' : $wp_filetype['proper_filename'];

		// Check to see if wp_check_filetype_and_ext() determined the filename was incorrect.
		if ( $proper_filename ) {
			$file['name'] = $proper_filename;
		}

		if ( ( ! $type || ! $ext ) && ! current_user_can( 'unfiltered_upload' ) ) {
			return call_user_func_array( $upload_error_handler, array( &$file, __( 'Sorry, you are not allowed to upload this file type.' ) ) );
		}

		if ( ! $type ) {
			$type = $file['type'];
		}
	} else {
		$type = '';
	}

	/*
	 * A writable uploads dir will pass this test. Again, there's no point
	 * overriding this one.
	 */
	$uploads = wp_upload_dir( $time );
	if ( ! ( $uploads && false === $uploads['error'] ) ) {
		return call_user_func_array( $upload_error_handler, array( &$file, $uploads['error'] ) );
	}

	$filename = wp_unique_filename( $uploads['path'], $file['name'], $unique_filename_callback );

	// Move the file to the uploads dir.
	$new_file = $uploads['path'] . "/$filename";

	/**
	 * Filters whether to short-circuit moving the uploaded file after passing all checks.
	 *
	 * If a non-null value is returned from the filter, moving the file and any related
	 * error reporting will be completely skipped.
	 *
	 * @since 4.9.0
	 *
	 * @param mixed    $move_new_file If null (default) move the file after the upload.
	 * @param array    $file          {
	 *     Reference to a single element from `$_FILES`.
	 *
	 *     @type string $name     The original name of the file on the client machine.
	 *     @type string $type     The mime type of the file, if the browser provided this information.
	 *     @type string $tmp_name The temporary filename of the file in which the uploaded file was stored on the server.
	 *     @type int    $size     The size, in bytes, of the uploaded file.
	 *     @type int    $error    The error code associated with this file upload.
	 * }
	 * @param string   $new_file      Filename of the newly-uploaded file.
	 * @param string   $type          Mime type of the newly-uploaded file.
	 */
	$move_new_file = apply_filters( 'pre_move_uploaded_file', null, $file, $new_file, $type );

	if ( null === $move_new_file ) {
		if ( 'wp_handle_upload' === $action ) {
			$move_new_file = @move_uploaded_file( $file['tmp_name'], $new_file );
		} else {
			// Use copy and unlink because rename breaks streams.
			// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
			$move_new_file = @copy( $file['tmp_name'], $new_file );
			unlink( $file['tmp_name'] );
		}

		if ( false === $move_new_file ) {
			if ( str_starts_with( $uploads['basedir'], ABSPATH ) ) {
				$error_path = str_replace( ABSPATH, '', $uploads['basedir'] ) . $uploads['subdir'];
			} else {
				$error_path = basename( $uploads['basedir'] ) . $uploads['subdir'];
			}

			return $upload_error_handler(
				$file,
				sprintf(
					/* translators: %s: Destination file path. */
					__( 'The uploaded file could not be moved to %s.' ),
					$error_path
				)
			);
		}
	}

	// Set correct file permissions.
	$stat  = stat( dirname( $new_file ) );
	$perms = $stat['mode'] & 0000666;
	chmod( $new_file, $perms );

	// Compute the URL.
	$url = $uploads['url'] . "/$filename";

	if ( is_multisite() ) {
		clean_dirsize_cache( $new_file );
	}

	/**
	 * Filters the data array for the uploaded file.
	 *
	 * @since 2.1.0
	 *
	 * @param array  $upload {
	 *     Array of upload data.
	 *
	 *     @type string $file Filename of the newly-uploaded file.
	 *     @type string $url  URL of the newly-uploaded file.
	 *     @type string $type Mime type of the newly-uploaded file.
	 * }
	 * @param string $context The type of upload action. Values include 'upload' or 'sideload'.
	 */
	return apply_filters(
		'wp_handle_upload',
		array(
			'file' => $new_file,
			'url'  => $url,
			'type' => $type,
		),
		'wp_handle_sideload' === $action ? 'sideload' : 'upload'
	);
}

/**
 * Wrapper for _wp_handle_upload().
 *
 * Passes the {@see 'wp_handle_upload'} action.
 *
 * @since 2.0.0
 *
 * @see _wp_handle_upload()
 *
 * @param array       $file      Reference to a single element of `$_FILES`.
 *                               Call the function once for each uploaded file.
 *                               See _wp_handle_upload() for accepted values.
 * @param array|false $overrides Optional. An associative array of names => values
 *                               to override default variables. Default false.
 *                               See _wp_handle_upload() for accepted values.
 * @param string|null $time      Optional. Time formatted in 'yyyy/mm'. Default null.
 * @return array See _wp_handle_upload() for return value.
 */
function wp_handle_upload( &$file, $overrides = false, $time = null ) {
	/*
	 *  $_POST['action'] must be set and its value must equal $overrides['action']
	 *  or this:
	 */
	$action = 'wp_handle_upload';
	if ( isset( $overrides['action'] ) ) {
		$action = $overrides['action'];
	}

	return _wp_handle_upload( $file, $overrides, $time, $action );
}

/**
 * Wrapper for _wp_handle_upload().
 *
 * Passes the {@see 'wp_handle_sideload'} action.
 *
 * @since 2.6.0
 *
 * @see _wp_handle_upload()
 *
 * @param array       $file      Reference to a single element of `$_FILES`.
 *                               Call the function once for each uploaded file.
 *                               See _wp_handle_upload() for accepted values.
 * @param array|false $overrides Optional. An associative array of names => values
 *                               to override default variables. Default false.
 *                               See _wp_handle_upload() for accepted values.
 * @param string|null $time      Optional. Time formatted in 'yyyy/mm'. Default null.
 * @return array See _wp_handle_upload() for return value.
 */
function wp_handle_sideload( &$file, $overrides = false, $time = null ) {
	/*
	 *  $_POST['action'] must be set and its value must equal $overrides['action']
	 *  or this:
	 */
	$action = 'wp_handle_sideload';
	if ( isset( $overrides['action'] ) ) {
		$action = $overrides['action'];
	}

	return _wp_handle_upload( $file, $overrides, $time, $action );
}

/**
 * Downloads a URL to a local temporary file using the WordPress HTTP API.
 *
 * Please note that the calling function must delete or move the file.
 *
 * @since 2.5.0
 * @since 5.2.0 Signature Verification with SoftFail was added.
 * @since 5.9.0 Support for Content-Disposition filename was added.
 *
 * @param string $url                    The URL of the file to download.
 * @param int    $timeout                The timeout for the request to download the file.
 *                                       Default 300 seconds.
 * @param bool   $signature_verification Whether to perform Signature Verification.
 *                                       Default false.
 * @return string|WP_Error Filename on success, WP_Error on failure.
 */
function download_url( $url, $timeout = 300, $signature_verification = false ) {
	// WARNING: The file is not automatically deleted, the script must delete or move the file.
	if ( ! $url ) {
		return new WP_Error( 'http_no_url', __( 'No URL Provided.' ) );
	}

	$url_path     = parse_url( $url, PHP_URL_PATH );
	$url_filename = '';
	if ( is_string( $url_path ) && '' !== $url_path ) {
		$url_filename = basename( $url_path );
	}

	$tmpfname = wp_tempnam( $url_filename );
	if ( ! $tmpfname ) {
		return new WP_Error( 'http_no_file', __( 'Could not create temporary file.' ) );
	}

	$response = wp_safe_remote_get(
		$url,
		array(
			'timeout'  => $timeout,
			'stream'   => true,
			'filename' => $tmpfname,
		)
	);

	if ( is_wp_error( $response ) ) {
		unlink( $tmpfname );
		return $response;
	}

	$response_code = wp_remote_retrieve_response_code( $response );

	if ( 200 !== $response_code ) {
		$data = array(
			'code' => $response_code,
		);

		// Retrieve a sample of the response body for debugging purposes.
		$tmpf = fopen( $tmpfname, 'rb' );

		if ( $tmpf ) {
			/**
			 * Filters the maximum error response body size in `download_url()`.
			 *
			 * @since 5.1.0
			 *
			 * @see download_url()
			 *
			 * @param int $size The maximum error response body size. Default 1 KB.
			 */
			$response_size = apply_filters( 'download_url_error_max_body_size', KB_IN_BYTES );

			$data['body'] = fread( $tmpf, $response_size );
			fclose( $tmpf );
		}

		unlink( $tmpfname );

		return new WP_Error( 'http_404', trim( wp_remote_retrieve_response_message( $response ) ), $data );
	}

	$content_disposition = wp_remote_retrieve_header( $response, 'Content-Disposition' );

	if ( $content_disposition ) {
		$content_disposition = strtolower( $content_disposition );

		if ( str_starts_with( $content_disposition, 'attachment; filename=' ) ) {
			$tmpfname_disposition = sanitize_file_name( substr( $content_disposition, 21 ) );
		} else {
			$tmpfname_disposition = '';
		}

		// Potential file name must be valid string.
		if ( $tmpfname_disposition && is_string( $tmpfname_disposition )
			&& ( 0 === validate_file( $tmpfname_disposition ) )
		) {
			$tmpfname_disposition = dirname( $tmpfname ) . '/' . $tmpfname_disposition;

			if ( rename( $tmpfname, $tmpfname_disposition ) ) {
				$tmpfname = $tmpfname_disposition;
			}

			if ( ( $tmpfname !== $tmpfname_disposition ) && file_exists( $tmpfname_disposition ) ) {
				unlink( $tmpfname_disposition );
			}
		}
	}

	$mime_type = wp_remote_retrieve_header( $response, 'content-type' );
	if ( $mime_type && 'tmp' === pathinfo( $tmpfname, PATHINFO_EXTENSION ) ) {
		$valid_mime_types = array_flip( get_allowed_mime_types() );
		if ( ! empty( $valid_mime_types[ $mime_type ] ) ) {
			$extensions     = explode( '|', $valid_mime_types[ $mime_type ] );
			$new_image_name = substr( $tmpfname, 0, -4 ) . ".{$extensions[0]}";
			if ( 0 === validate_file( $new_image_name ) ) {
				if ( rename( $tmpfname, $new_image_name ) ) {
					$tmpfname = $new_image_name;
				}

				if ( ( $tmpfname !== $new_image_name ) && file_exists( $new_image_name ) ) {
					unlink( $new_image_name );
				}
			}
		}
	}

	$content_md5 = wp_remote_retrieve_header( $response, 'Content-MD5' );

	if ( $content_md5 ) {
		$md5_check = verify_file_md5( $tmpfname, $content_md5 );

		if ( is_wp_error( $md5_check ) ) {
			unlink( $tmpfname );
			return $md5_check;
		}
	}

	// If the caller expects signature verification to occur, check to see if this URL supports it.
	if ( $signature_verification ) {
		/**
		 * Filters the list of hosts which should have Signature Verification attempted on.
		 *
		 * @since 5.2.0
		 *
		 * @param string[] $hostnames List of hostnames.
		 */
		$signed_hostnames = apply_filters( 'wp_signature_hosts', array( 'wordpress.org', 'downloads.wordpress.org', 's.w.org' ) );

		$signature_verification = in_array( parse_url( $url, PHP_URL_HOST ), $signed_hostnames, true );
	}

	// Perform signature validation if supported.
	if ( $signature_verification ) {
		$signature = wp_remote_retrieve_header( $response, 'X-Content-Signature' );

		if ( ! $signature ) {
			/*
			 * Retrieve signatures from a file if the header wasn't included.
			 * WordPress.org stores signatures at $package_url.sig.
			 */

			$signature_url = false;

			if ( is_string( $url_path ) && ( str_ends_with( $url_path, '.zip' ) || str_ends_with( $url_path, '.tar.gz' ) ) ) {
				$signature_url = str_replace( $url_path, $url_path . '.sig', $url );
			}

			/**
			 * Filters the URL where the signature for a file is located.
			 *
			 * @since 5.2.0
			 *
			 * @param false|string $signature_url The URL where signatures can be found for a file, or false if none are known.
			 * @param string $url                 The URL being verified.
			 */
			$signature_url = apply_filters( 'wp_signature_url', $signature_url, $url );

			if ( $signature_url ) {
				$signature_request = wp_safe_remote_get(
					$signature_url,
					array(
						'limit_response_size' => 10 * KB_IN_BYTES, // 10KB should be large enough for quite a few signatures.
					)
				);

				if ( ! is_wp_error( $signature_request ) && 200 === wp_remote_retrieve_response_code( $signature_request ) ) {
					$signature = explode( "\n", wp_remote_retrieve_body( $signature_request ) );
				}
			}
		}

		// Perform the checks.
		$signature_verification = verify_file_signature( $tmpfname, $signature, $url_filename );
	}

	if ( is_wp_error( $signature_verification ) ) {
		if (
			/**
			 * Filters whether Signature Verification failures should be allowed to soft fail.
			 *
			 * WARNING: This may be removed from a future release.
			 *
			 * @since 5.2.0
			 *
			 * @param bool   $signature_softfail If a softfail is allowed.
			 * @param string $url                The url being accessed.
			 */
			apply_filters( 'wp_signature_softfail', true, $url )
		) {
			$signature_verification->add_data( $tmpfname, 'softfail-filename' );
		} else {
			// Hard-fail.
			unlink( $tmpfname );
		}

		return $signature_verification;
	}

	return $tmpfname;
}

/**
 * Calculates and compares the MD5 of a file to its expected value.
 *
 * @since 3.7.0
 *
 * @param string $filename     The filename to check the MD5 of.
 * @param string $expected_md5 The expected MD5 of the file, either a base64-encoded raw md5,
 *                             or a hex-encoded md5.
 * @return bool|WP_Error True on success, false when the MD5 format is unknown/unexpected,
 *                       WP_Error on failure.
 */
function verify_file_md5( $filename, $expected_md5 ) {
	if ( 32 === strlen( $expected_md5 ) ) {
		$expected_raw_md5 = pack( 'H*', $expected_md5 );
	} elseif ( 24 === strlen( $expected_md5 ) ) {
		$expected_raw_md5 = base64_decode( $expected_md5 );
	} else {
		return false; // Unknown format.
	}

	$file_md5 = md5_file( $filename, true );

	if ( $file_md5 === $expected_raw_md5 ) {
		return true;
	}

	return new WP_Error(
		'md5_mismatch',
		sprintf(
			/* translators: 1: File checksum, 2: Expected checksum value. */
			__( 'The checksum of the file (%1$s) does not match the expected checksum value (%2$s).' ),
			bin2hex( $file_md5 ),
			bin2hex( $expected_raw_md5 )
		)
	);
}

/**
 * Verifies the contents of a file against its ED25519 signature.
 *
 * @since 5.2.0
 *
 * @param string       $filename            The file to validate.
 * @param string|array $signatures          A Signature provided for the file.
 * @param string|false $filename_for_errors Optional. A friendly filename for errors.
 * @return bool|WP_Error True on success, false if verification not attempted,
 *                       or WP_Error describing an error condition.
 */
function verify_file_signature( $filename, $signatures, $filename_for_errors = false ) {
	if ( ! $filename_for_errors ) {
		$filename_for_errors = wp_basename( $filename );
	}

	// Check we can process signatures.
	if ( ! function_exists( 'sodium_crypto_sign_verify_detached' ) || ! in_array( 'sha384', array_map( 'strtolower', hash_algos() ), true ) ) {
		return new WP_Error(
			'signature_verification_unsupported',
			sprintf(
				/* translators: %s: The filename of the package. */
				__( 'The authenticity of %s could not be verified as signature verification is unavailable on this system.' ),
				'<span class="code">' . esc_html( $filename_for_errors ) . '</span>'
			),
			( ! function_exists( 'sodium_crypto_sign_verify_detached' ) ? 'sodium_crypto_sign_verify_detached' : 'sha384' )
		);
	}

	// Verify runtime speed of Sodium_Compat is acceptable.
	if ( ! extension_loaded( 'sodium' ) && ! ParagonIE_Sodium_Compat::polyfill_is_fast() ) {
		$sodium_compat_is_fast = false;

		// Allow for an old version of Sodium_Compat being loaded before the bundled WordPress one.
		if ( method_exists( 'ParagonIE_Sodium_Compat', 'runtime_speed_test' ) ) {
			/*
			 * Run `ParagonIE_Sodium_Compat::runtime_speed_test()` in optimized integer mode,
			 * as that's what WordPress utilizes during signing verifications.
			 */
			// phpcs:disable WordPress.NamingConventions.ValidVariableName
			$old_fastMult                      = ParagonIE_Sodium_Compat::$fastMult;
			ParagonIE_Sodium_Compat::$fastMult = true;
			$sodium_compat_is_fast             = ParagonIE_Sodium_Compat::runtime_speed_test( 100, 10 );
			ParagonIE_Sodium_Compat::$fastMult = $old_fastMult;
			// phpcs:enable
		}

		/*
		 * This cannot be performed in a reasonable amount of time.
		 * https://github.com/paragonie/sodium_compat#help-sodium_compat-is-slow-how-can-i-make-it-fast
		 */
		if ( ! $sodium_compat_is_fast ) {
			return new WP_Error(
				'signature_verification_unsupported',
				sprintf(
					/* translators: %s: The filename of the package. */
					__( 'The authenticity of %s could not be verified as signature verification is unavailable on this system.' ),
					'<span class="code">' . esc_html( $filename_for_errors ) . '</span>'
				),
				array(
					'php'                => PHP_VERSION,
					'sodium'             => defined( 'SODIUM_LIBRARY_VERSION' ) ? SODIUM_LIBRARY_VERSION : ( defined( 'ParagonIE_Sodium_Compat::VERSION_STRING' ) ? ParagonIE_Sodium_Compat::VERSION_STRING : false ),
					'polyfill_is_fast'   => false,
					'max_execution_time' => ini_get( 'max_execution_time' ),
				)
			);
		}
	}

	if ( ! $signatures ) {
		return new WP_Error(
			'signature_verification_no_signature',
			sprintf(
				/* translators: %s: The filename of the package. */
				__( 'The authenticity of %s could not be verified as no signature was found.' ),
				'<span class="code">' . esc_html( $filename_for_errors ) . '</span>'
			),
			array(
				'filename' => $filename_for_errors,
			)
		);
	}

	$trusted_keys = wp_trusted_keys();
	$file_hash    = hash_file( 'sha384', $filename, true );

	mbstring_binary_safe_encoding();

	$skipped_key       = 0;
	$skipped_signature = 0;

	foreach ( (array) $signatures as $signature ) {
		$signature_raw = base64_decode( $signature );

		// Ensure only valid-length signatures are considered.
		if ( SODIUM_CRYPTO_SIGN_BYTES !== strlen( $signature_raw ) ) {
			++$skipped_signature;
			continue;
		}

		foreach ( (array) $trusted_keys as $key ) {
			$key_raw = base64_decode( $key );

			// Only pass valid public keys through.
			if ( SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES !== strlen( $key_raw ) ) {
				++$skipped_key;
				continue;
			}

			if ( sodium_crypto_sign_verify_detached( $signature_raw, $file_hash, $key_raw ) ) {
				reset_mbstring_encoding();
				return true;
			}
		}
	}

	reset_mbstring_encoding();

	return new WP_Error(
		'signature_verification_failed',
		sprintf(
			/* translators: %s: The filename of the package. */
			__( 'The authenticity of %s could not be verified.' ),
			'<span class="code">' . esc_html( $filename_for_errors ) . '</span>'
		),
		// Error data helpful for debugging:
		array(
			'filename'    => $filename_for_errors,
			'keys'        => $trusted_keys,
			'signatures'  => $signatures,
			'hash'        => bin2hex( $file_hash ),
			'skipped_key' => $skipped_key,
			'skipped_sig' => $skipped_signature,
			'php'         => PHP_VERSION,
			'sodium'      => defined( 'SODIUM_LIBRARY_VERSION' ) ? SODIUM_LIBRARY_VERSION : ( defined( 'ParagonIE_Sodium_Compat::VERSION_STRING' ) ? ParagonIE_Sodium_Compat::VERSION_STRING : false ),
		)
	);
}

/**
 * Retrieves the list of signing keys trusted by WordPress.
 *
 * @since 5.2.0
 *
 * @return string[] Array of base64-encoded signing keys.
 */
function wp_trusted_keys() {
	$trusted_keys = array();

	if ( time() < 1617235200 ) {
		// WordPress.org Key #1 - This key is only valid before April 1st, 2021.
		$trusted_keys[] = 'fRPyrxb/MvVLbdsYi+OOEv4xc+Eqpsj+kkAS6gNOkI0=';
	}

	// TODO: Add key #2 with longer expiration.

	/**
	 * Filters the valid signing keys used to verify the contents of files.
	 *
	 * @since 5.2.0
	 *
	 * @param string[] $trusted_keys The trusted keys that may sign packages.
	 */
	return apply_filters( 'wp_trusted_keys', $trusted_keys );
}

/**
 * Determines whether the given file is a valid ZIP file.
 *
 * This function does not test to ensure that a file exists. Non-existent files
 * are not valid ZIPs, so those will also return false.
 *
 * @since 6.4.4
 *
 * @param string $file Full path to the ZIP file.
 * @return bool Whether the file is a valid ZIP file.
 */
function wp_zip_file_is_valid( $file ) {
	/** This filter is documented in wp-admin/includes/file.php */
	if ( class_exists( 'ZipArchive', false ) && apply_filters( 'unzip_file_use_ziparchive', true ) ) {
		$archive          = new ZipArchive();
		$archive_is_valid = $archive->open( $file, ZipArchive::CHECKCONS );
		if ( true === $archive_is_valid ) {
			$archive->close();
			return true;
		}
	}

	// Fall through to PclZip if ZipArchive is not available, or encountered an error opening the file.
	require_once ABSPATH . 'wp-admin/includes/class-pclzip.php';

	$archive          = new PclZip( $file );
	$archive_is_valid = is_array( $archive->properties() );

	return $archive_is_valid;
}

/**
 * Unzips a specified ZIP file to a location on the filesystem via the WordPress
 * Filesystem Abstraction.
 *
 * Assumes that WP_Filesystem() has already been called and set up. Does not extract
 * a root-level __MACOSX directory, if present.
 *
 * Attempts to increase the PHP memory limit to 256M before uncompressing. However,
 * the most memory required shouldn't be much larger than the archive itself.
 *
 * @since 2.5.0
 *
 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
 *
 * @param string $file Full path and filename of ZIP archive.
 * @param string $to   Full path on the filesystem to extract archive to.
 * @return true|WP_Error True on success, WP_Error on failure.
 */
function unzip_file( $file, $to ) {
	global $wp_filesystem;

	if ( ! $wp_filesystem || ! is_object( $wp_filesystem ) ) {
		return new WP_Error( 'fs_unavailable', __( 'Could not access filesystem.' ) );
	}

	// Unzip can use a lot of memory, but not this much hopefully.
	wp_raise_memory_limit( 'admin' );

	$needed_dirs = array();
	$to          = trailingslashit( $to );

	// Determine any parent directories needed (of the upgrade directory).
	if ( ! $wp_filesystem->is_dir( $to ) ) { // Only do parents if no children exist.
		$path = preg_split( '![/\\\]!', untrailingslashit( $to ) );
		for ( $i = count( $path ); $i >= 0; $i-- ) {
			if ( empty( $path[ $i ] ) ) {
				continue;
			}

			$dir = implode( '/', array_slice( $path, 0, $i + 1 ) );
			if ( preg_match( '!^[a-z]:$!i', $dir ) ) { // Skip it if it looks like a Windows Drive letter.
				continue;
			}

			if ( ! $wp_filesystem->is_dir( $dir ) ) {
				$needed_dirs[] = $dir;
			} else {
				break; // A folder exists, therefore we don't need to check the levels below this.
			}
		}
	}

	/**
	 * Filters whether to use ZipArchive to unzip archives.
	 *
	 * @since 3.0.0
	 *
	 * @param bool $ziparchive Whether to use ZipArchive. Default true.
	 */
	if ( class_exists( 'ZipArchive', false ) && apply_filters( 'unzip_file_use_ziparchive', true ) ) {
		$result = _unzip_file_ziparchive( $file, $to, $needed_dirs );
		if ( true === $result ) {
			return $result;
		} elseif ( is_wp_error( $result ) ) {
			if ( 'incompatible_archive' !== $result->get_error_code() ) {
				return $result;
			}
		}
	}
	// Fall through to PclZip if ZipArchive is not available, or encountered an error opening the file.
	return _unzip_file_pclzip( $file, $to, $needed_dirs );
}

/**
 * Attempts to unzip an archive using the ZipArchive class.
 *
 * This function should not be called directly, use `unzip_file()` instead.
 *
 * Assumes that WP_Filesystem() has already been called and set up.
 *
 * @since 3.0.0
 * @access private
 *
 * @see unzip_file()
 *
 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
 *
 * @param string   $file        Full path and filename of ZIP archive.
 * @param string   $to          Full path on the filesystem to extract archive to.
 * @param string[] $needed_dirs A partial list of required folders needed to be created.
 * @return true|WP_Error True on success, WP_Error on failure.
 */
function _unzip_file_ziparchive( $file, $to, $needed_dirs = array() ) {
	global $wp_filesystem;

	$z = new ZipArchive();

	$zopen = $z->open( $file, ZIPARCHIVE::CHECKCONS );

	if ( true !== $zopen ) {
		return new WP_Error( 'incompatible_archive', __( 'Incompatible Archive.' ), array( 'ziparchive_error' => $zopen ) );
	}

	$uncompressed_size = 0;

	for ( $i = 0; $i < $z->numFiles; $i++ ) {
		$info = $z->statIndex( $i );

		if ( ! $info ) {
			$z->close();
			return new WP_Error( 'stat_failed_ziparchive', __( 'Could not retrieve file from archive.' ) );
		}

		if ( str_starts_with( $info['name'], '__MACOSX/' ) ) { // Skip the OS X-created __MACOSX directory.
			continue;
		}

		// Don't extract invalid files:
		if ( 0 !== validate_file( $info['name'] ) ) {
			continue;
		}

		$uncompressed_size += $info['size'];

		$dirname = dirname( $info['name'] );

		if ( str_ends_with( $info['name'], '/' ) ) {
			// Directory.
			$needed_dirs[] = $to . untrailingslashit( $info['name'] );
		} elseif ( '.' !== $dirname ) {
			// Path to a file.
			$needed_dirs[] = $to . untrailingslashit( $dirname );
		}
	}

	// Enough space to unzip the file and copy its contents, with a 10% buffer.
	$required_space = $uncompressed_size * 2.1;

	/*
	 * disk_free_space() could return false. Assume that any falsey value is an error.
	 * A disk that has zero free bytes has bigger problems.
	 * Require we have enough space to unzip the file and copy its contents, with a 10% buffer.
	 */
	if ( wp_doing_cron() ) {
		$available_space = function_exists( 'disk_free_space' ) ? @disk_free_space( WP_CONTENT_DIR ) : false;

		if ( $available_space && ( $required_space > $available_space ) ) {
			$z->close();
			return new WP_Error(
				'disk_full_unzip_file',
				__( 'Could not copy files. You may have run out of disk space.' ),
				compact( 'uncompressed_size', 'available_space' )
			);
		}
	}

	$needed_dirs = array_unique( $needed_dirs );

	foreach ( $needed_dirs as $dir ) {
		// Check the parent folders of the folders all exist within the creation array.
		if ( untrailingslashit( $to ) === $dir ) { // Skip over the working directory, we know this exists (or will exist).
			continue;
		}

		if ( ! str_contains( $dir, $to ) ) { // If the directory is not within the working directory, skip it.
			continue;
		}

		$parent_folder = dirname( $dir );

		while ( ! empty( $parent_folder )
			&& untrailingslashit( $to ) !== $parent_folder
			&& ! in_array( $parent_folder, $needed_dirs, true )
		) {
			$needed_dirs[] = $parent_folder;
			$parent_folder = dirname( $parent_folder );
		}
	}

	asort( $needed_dirs );

	// Create those directories if need be:
	foreach ( $needed_dirs as $_dir ) {
		// Only check to see if the Dir exists upon creation failure. Less I/O this way.
		if ( ! $wp_filesystem->mkdir( $_dir, FS_CHMOD_DIR ) && ! $wp_filesystem->is_dir( $_dir ) ) {
			$z->close();
			return new WP_Error( 'mkdir_failed_ziparchive', __( 'Could not create directory.' ), $_dir );
		}
	}

	/**
	 * Filters archive unzipping to override with a custom process.
	 *
	 * @since 6.4.0
	 *
	 * @param null|true|WP_Error $result         The result of the override. True on success, otherwise WP Error. Default null.
	 * @param string             $file           Full path and filename of ZIP archive.
	 * @param string             $to             Full path on the filesystem to extract archive to.
	 * @param string[]           $needed_dirs    A full list of required folders that need to be created.
	 * @param float              $required_space The space required to unzip the file and copy its contents, with a 10% buffer.
	 */
	$pre = apply_filters( 'pre_unzip_file', null, $file, $to, $needed_dirs, $required_space );

	if ( null !== $pre ) {
		// Ensure the ZIP file archive has been closed.
		$z->close();

		return $pre;
	}

	for ( $i = 0; $i < $z->numFiles; $i++ ) {
		$info = $z->statIndex( $i );

		if ( ! $info ) {
			$z->close();
			return new WP_Error( 'stat_failed_ziparchive', __( 'Could not retrieve file from archive.' ) );
		}

		if ( str_ends_with( $info['name'], '/' ) ) { // Directory.
			continue;
		}

		if ( str_starts_with( $info['name'], '__MACOSX/' ) ) { // Don't extract the OS X-created __MACOSX directory files.
			continue;
		}

		// Don't extract invalid files:
		if ( 0 !== validate_file( $info['name'] ) ) {
			continue;
		}

		$contents = $z->getFromIndex( $i );

		if ( false === $contents ) {
			$z->close();
			return new WP_Error( 'extract_failed_ziparchive', __( 'Could not extract file from archive.' ), $info['name'] );
		}

		if ( ! $wp_filesystem->put_contents( $to . $info['name'], $contents, FS_CHMOD_FILE ) ) {
			$z->close();
			return new WP_Error( 'copy_failed_ziparchive', __( 'Could not copy file.' ), $info['name'] );
		}
	}

	$z->close();

	/**
	 * Filters the result of unzipping an archive.
	 *
	 * @since 6.4.0
	 *
	 * @param true|WP_Error $result         The result of unzipping the archive. True on success, otherwise WP_Error. Default true.
	 * @param string        $file           Full path and filename of ZIP archive.
	 * @param string        $to             Full path on the filesystem the archive was extracted to.
	 * @param string[]      $needed_dirs    A full list of required folders that were created.
	 * @param float         $required_space The space required to unzip the file and copy its contents, with a 10% buffer.
	 */
	$result = apply_filters( 'unzip_file', true, $file, $to, $needed_dirs, $required_space );

	unset( $needed_dirs );

	return $result;
}

/**
 * Attempts to unzip an archive using the PclZip library.
 *
 * This function should not be called directly, use `unzip_file()` instead.
 *
 * Assumes that WP_Filesystem() has already been called and set up.
 *
 * @since 3.0.0
 * @access private
 *
 * @see unzip_file()
 *
 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
 *
 * @param string   $file        Full path and filename of ZIP archive.
 * @param string   $to          Full path on the filesystem to extract archive to.
 * @param string[] $needed_dirs A partial list of required folders needed to be created.
 * @return true|WP_Error True on success, WP_Error on failure.
 */
function _unzip_file_pclzip( $file, $to, $needed_dirs = array() ) {
	global $wp_filesystem;

	mbstring_binary_safe_encoding();

	require_once ABSPATH . 'wp-admin/includes/class-pclzip.php';

	$archive = new PclZip( $file );

	$archive_files = $archive->extract( PCLZIP_OPT_EXTRACT_AS_STRING );

	reset_mbstring_encoding();

	// Is the archive valid?
	if ( ! is_array( $archive_files ) ) {
		return new WP_Error( 'incompatible_archive', __( 'Incompatible Archive.' ), $archive->errorInfo( true ) );
	}

	if ( 0 === count( $archive_files ) ) {
		return new WP_Error( 'empty_archive_pclzip', __( 'Empty archive.' ) );
	}

	$uncompressed_size = 0;

	// Determine any children directories needed (From within the archive).
	foreach ( $archive_files as $file ) {
		if ( str_starts_with( $file['filename'], '__MACOSX/' ) ) { // Skip the OS X-created __MACOSX directory.
			continue;
		}

		$uncompressed_size += $file['size'];

		$needed_dirs[] = $to . untrailingslashit( $file['folder'] ? $file['filename'] : dirname( $file['filename'] ) );
	}

	// Enough space to unzip the file and copy its contents, with a 10% buffer.
	$required_space = $uncompressed_size * 2.1;

	/*
	 * disk_free_space() could return false. Assume that any falsey value is an error.
	 * A disk that has zero free bytes has bigger problems.
	 * Require we have enough space to unzip the file and copy its contents, with a 10% buffer.
	 */
	if ( wp_doing_cron() ) {
		$available_space = function_exists( 'disk_free_space' ) ? @disk_free_space( WP_CONTENT_DIR ) : false;

		if ( $available_space && ( $required_space > $available_space ) ) {
			return new WP_Error(
				'disk_full_unzip_file',
				__( 'Could not copy files. You may have run out of disk space.' ),
				compact( 'uncompressed_size', 'available_space' )
			);
		}
	}

	$needed_dirs = array_unique( $needed_dirs );

	foreach ( $needed_dirs as $dir ) {
		// Check the parent folders of the folders all exist within the creation array.
		if ( untrailingslashit( $to ) === $dir ) { // Skip over the working directory, we know this exists (or will exist).
			continue;
		}

		if ( ! str_contains( $dir, $to ) ) { // If the directory is not within the working directory, skip it.
			continue;
		}

		$parent_folder = dirname( $dir );

		while ( ! empty( $parent_folder )
			&& untrailingslashit( $to ) !== $parent_folder
			&& ! in_array( $parent_folder, $needed_dirs, true )
		) {
			$needed_dirs[] = $parent_folder;
			$parent_folder = dirname( $parent_folder );
		}
	}

	asort( $needed_dirs );

	// Create those directories if need be:
	foreach ( $needed_dirs as $_dir ) {
		// Only check to see if the dir exists upon creation failure. Less I/O this way.
		if ( ! $wp_filesystem->mkdir( $_dir, FS_CHMOD_DIR ) && ! $wp_filesystem->is_dir( $_dir ) ) {
			return new WP_Error( 'mkdir_failed_pclzip', __( 'Could not create directory.' ), $_dir );
		}
	}

	/** This filter is documented in src/wp-admin/includes/file.php */
	$pre = apply_filters( 'pre_unzip_file', null, $file, $to, $needed_dirs, $required_space );

	if ( null !== $pre ) {
		return $pre;
	}

	// Extract the files from the zip.
	foreach ( $archive_files as $file ) {
		if ( $file['folder'] ) {
			continue;
		}

		if ( str_starts_with( $file['filename'], '__MACOSX/' ) ) { // Don't extract the OS X-created __MACOSX directory files.
			continue;
		}

		// Don't extract invalid files:
		if ( 0 !== validate_file( $file['filename'] ) ) {
			continue;
		}

		if ( ! $wp_filesystem->put_contents( $to . $file['filename'], $file['content'], FS_CHMOD_FILE ) ) {
			return new WP_Error( 'copy_failed_pclzip', __( 'Could not copy file.' ), $file['filename'] );
		}
	}

	/** This action is documented in src/wp-admin/includes/file.php */
	$result = apply_filters( 'unzip_file', true, $file, $to, $needed_dirs, $required_space );

	unset( $needed_dirs );

	return $result;
}

/**
 * Copies a directory from one location to another via the WordPress Filesystem
 * Abstraction.
 *
 * Assumes that WP_Filesystem() has already been called and setup.
 *
 * @since 2.5.0
 *
 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
 *
 * @param string   $from      Source directory.
 * @param string   $to        Destination directory.
 * @param string[] $skip_list An array of files/folders to skip copying.
 * @return true|WP_Error True on success, WP_Error on failure.
 */
function copy_dir( $from, $to, $skip_list = array() ) {
	global $wp_filesystem;

	$dirlist = $wp_filesystem->dirlist( $from );

	if ( false === $dirlist ) {
		return new WP_Error( 'dirlist_failed_copy_dir', __( 'Directory listing failed.' ), basename( $from ) );
	}

	$from = trailingslashit( $from );
	$to   = trailingslashit( $to );

	if ( ! $wp_filesystem->exists( $to ) && ! $wp_filesystem->mkdir( $to ) ) {
		return new WP_Error(
			'mkdir_destination_failed_copy_dir',
			__( 'Could not create the destination directory.' ),
			basename( $to )
		);
	}

	foreach ( (array) $dirlist as $filename => $fileinfo ) {
		if ( in_array( $filename, $skip_list, true ) ) {
			continue;
		}

		if ( 'f' === $fileinfo['type'] ) {
			if ( ! $wp_filesystem->copy( $from . $filename, $to . $filename, true, FS_CHMOD_FILE ) ) {
				// If copy failed, chmod file to 0644 and try again.
				$wp_filesystem->chmod( $to . $filename, FS_CHMOD_FILE );

				if ( ! $wp_filesystem->copy( $from . $filename, $to . $filename, true, FS_CHMOD_FILE ) ) {
					return new WP_Error( 'copy_failed_copy_dir', __( 'Could not copy file.' ), $to . $filename );
				}
			}

			wp_opcache_invalidate( $to . $filename );
		} elseif ( 'd' === $fileinfo['type'] ) {
			if ( ! $wp_filesystem->is_dir( $to . $filename ) ) {
				if ( ! $wp_filesystem->mkdir( $to . $filename, FS_CHMOD_DIR ) ) {
					return new WP_Error( 'mkdir_failed_copy_dir', __( 'Could not create directory.' ), $to . $filename );
				}
			}

			// Generate the $sub_skip_list for the subdirectory as a sub-set of the existing $skip_list.
			$sub_skip_list = array();

			foreach ( $skip_list as $skip_item ) {
				if ( str_starts_with( $skip_item, $filename . '/' ) ) {
					$sub_skip_list[] = preg_replace( '!^' . preg_quote( $filename, '!' ) . '/!i', '', $skip_item );
				}
			}

			$result = copy_dir( $from . $filename, $to . $filename, $sub_skip_list );

			if ( is_wp_error( $result ) ) {
				return $result;
			}
		}
	}

	return true;
}

/**
 * Moves a directory from one location to another.
 *
 * Recursively invalidates OPcache on success.
 *
 * If the renaming failed, falls back to copy_dir().
 *
 * Assumes that WP_Filesystem() has already been called and setup.
 *
 * This function is not designed to merge directories, copy_dir() should be used instead.
 *
 * @since 6.2.0
 *
 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
 *
 * @param string $from      Source directory.
 * @param string $to        Destination directory.
 * @param bool   $overwrite Optional. Whether to overwrite the destination directory if it exists.
 *                          Default false.
 * @return true|WP_Error True on success, WP_Error on failure.
 */
function move_dir( $from, $to, $overwrite = false ) {
	global $wp_filesystem;

	if ( trailingslashit( strtolower( $from ) ) === trailingslashit( strtolower( $to ) ) ) {
		return new WP_Error( 'source_destination_same_move_dir', __( 'The source and destination are the same.' ) );
	}

	if ( $wp_filesystem->exists( $to ) ) {
		if ( ! $overwrite ) {
			return new WP_Error( 'destination_already_exists_move_dir', __( 'The destination folder already exists.' ), $to );
		} elseif ( ! $wp_filesystem->delete( $to, true ) ) {
			// Can't overwrite if the destination couldn't be deleted.
			return new WP_Error( 'destination_not_deleted_move_dir', __( 'The destination directory already exists and could not be removed.' ) );
		}
	}

	if ( $wp_filesystem->move( $from, $to ) ) {
		/*
		 * When using an environment with shared folders,
		 * there is a delay in updating the filesystem's cache.
		 *
		 * This is a known issue in environments with a VirtualBox provider.
		 *
		 * A 200ms delay gives time for the filesystem to update its cache,
		 * prevents "Operation not permitted", and "No such file or directory" warnings.
		 *
		 * This delay is used in other projects, including Composer.
		 * @link https://github.com/composer/composer/blob/2.5.1/src/Composer/Util/Platform.php#L228-L233
		 */
		usleep( 200000 );
		wp_opcache_invalidate_directory( $to );

		return true;
	}

	// Fall back to a recursive copy.
	if ( ! $wp_filesystem->is_dir( $to ) ) {
		if ( ! $wp_filesystem->mkdir( $to, FS_CHMOD_DIR ) ) {
			return new WP_Error( 'mkdir_failed_move_dir', __( 'Could not create directory.' ), $to );
		}
	}

	$result = copy_dir( $from, $to, array( basename( $to ) ) );

	// Clear the source directory.
	if ( true === $result ) {
		$wp_filesystem->delete( $from, true );
	}

	return $result;
}

/**
 * Initializes and connects the WordPress Filesystem Abstraction classes.
 *
 * This function will include the chosen transport and attempt connecting.
 *
 * Plugins may add extra transports, And force WordPress to use them by returning
 * the filename via the {@see 'filesystem_method_file'} filter.
 *
 * @since 2.5.0
 *
 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
 *
 * @param array|false  $args                         Optional. Connection args, These are passed
 *                                                   directly to the `WP_Filesystem_*()` classes.
 *                                                   Default false.
 * @param string|false $context                      Optional. Context for get_filesystem_method().
 *                                                   Default false.
 * @param bool         $allow_relaxed_file_ownership Optional. Whether to allow Group/World writable.
 *                                                   Default false.
 * @return bool|null True on success, false on failure,
 *                   null if the filesystem method class file does not exist.
 */
function WP_Filesystem( $args = false, $context = false, $allow_relaxed_file_ownership = false ) { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid
	global $wp_filesystem;

	require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';

	$method = get_filesystem_method( $args, $context, $allow_relaxed_file_ownership );

	if ( ! $method ) {
		return false;
	}

	if ( ! class_exists( "WP_Filesystem_$method" ) ) {

		/**
		 * Filters the path for a specific filesystem method class file.
		 *
		 * @since 2.6.0
		 *
		 * @see get_filesystem_method()
		 *
		 * @param string $path   Path to the specific filesystem method class file.
		 * @param string $method The filesystem method to use.
		 */
		$abstraction_file = apply_filters( 'filesystem_method_file', ABSPATH . 'wp-admin/includes/class-wp-filesystem-' . $method . '.php', $method );

		if ( ! file_exists( $abstraction_file ) ) {
			return;
		}

		require_once $abstraction_file;
	}
	$method = "WP_Filesystem_$method";

	$wp_filesystem = new $method( $args );

	/*
	 * Define the timeouts for the connections. Only available after the constructor is called
	 * to allow for per-transport overriding of the default.
	 */
	if ( ! defined( 'FS_CONNECT_TIMEOUT' ) ) {
		define( 'FS_CONNECT_TIMEOUT', 30 ); // 30 seconds.
	}
	if ( ! defined( 'FS_TIMEOUT' ) ) {
		define( 'FS_TIMEOUT', 30 ); // 30 seconds.
	}

	if ( is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
		return false;
	}

	if ( ! $wp_filesystem->connect() ) {
		return false; // There was an error connecting to the server.
	}

	// Set the permission constants if not already set.
	if ( ! defined( 'FS_CHMOD_DIR' ) ) {
		define( 'FS_CHMOD_DIR', ( fileperms( ABSPATH ) & 0777 | 0755 ) );
	}
	if ( ! defined( 'FS_CHMOD_FILE' ) ) {
		define( 'FS_CHMOD_FILE', ( fileperms( ABSPATH . 'index.php' ) & 0777 | 0644 ) );
	}

	return true;
}

/**
 * Determines which method to use for reading, writing, modifying, or deleting
 * files on the filesystem.
 *
 * The priority of the transports are: Direct, SSH2, FTP PHP Extension, FTP Sockets
 * (Via Sockets class, or `fsockopen()`). Valid values for these are: 'direct', 'ssh2',
 * 'ftpext' or 'ftpsockets'.
 *
 * The return value can be overridden by defining the `FS_METHOD` constant in `wp-config.php`,
 * or filtering via {@see 'filesystem_method'}.
 *
 * @link https://developer.wordpress.org/advanced-administration/wordpress/wp-config/#wordpress-upgrade-constants
 *
 * Plugins may define a custom transport handler, See WP_Filesystem().
 *
 * @since 2.5.0
 *
 * @global callable $_wp_filesystem_direct_method
 *
 * @param array  $args                         Optional. Connection details. Default empty array.
 * @param string $context                      Optional. Full path to the directory that is tested
 *                                             for being writable. Default empty.
 * @param bool   $allow_relaxed_file_ownership Optional. Whether to allow Group/World writable.
 *                                             Default false.
 * @return string The transport to use, see description for valid return values.
 */
function get_filesystem_method( $args = array(), $context = '', $allow_relaxed_file_ownership = false ) {
	// Please ensure that this is either 'direct', 'ssh2', 'ftpext', or 'ftpsockets'.
	$method = defined( 'FS_METHOD' ) ? FS_METHOD : false;

	if ( ! $context ) {
		$context = WP_CONTENT_DIR;
	}

	// If the directory doesn't exist (wp-content/languages) then use the parent directory as we'll create it.
	if ( WP_LANG_DIR === $context && ! is_dir( $context ) ) {
		$context = dirname( $context );
	}

	$context = trailingslashit( $context );

	if ( ! $method ) {

		$temp_file_name = $context . 'temp-write-test-' . str_replace( '.', '-', uniqid( '', true ) );
		$temp_handle    = @fopen( $temp_file_name, 'w' );
		if ( $temp_handle ) {

			// Attempt to determine the file owner of the WordPress files, and that of newly created files.
			$wp_file_owner   = false;
			$temp_file_owner = false;
			if ( function_exists( 'fileowner' ) ) {
				$wp_file_owner   = @fileowner( __FILE__ );
				$temp_file_owner = @fileowner( $temp_file_name );
			}

			if ( false !== $wp_file_owner && $wp_file_owner === $temp_file_owner ) {
				/*
				 * WordPress is creating files as the same owner as the WordPress files,
				 * this means it's safe to modify & create new files via PHP.
				 */
				$method                                  = 'direct';
				$GLOBALS['_wp_filesystem_direct_method'] = 'file_owner';
			} elseif ( $allow_relaxed_file_ownership ) {
				/*
				 * The $context directory is writable, and $allow_relaxed_file_ownership is set,
				 * this means we can modify files safely in this directory.
				 * This mode doesn't create new files, only alter existing ones.
				 */
				$method                                  = 'direct';
				$GLOBALS['_wp_filesystem_direct_method'] = 'relaxed_ownership';
			}

			fclose( $temp_handle );
			@unlink( $temp_file_name );
		}
	}

	if ( ! $method && isset( $args['connection_type'] ) && 'ssh' === $args['connection_type'] && extension_loaded( 'ssh2' ) ) {
		$method = 'ssh2';
	}
	if ( ! $method && extension_loaded( 'ftp' ) ) {
		$method = 'ftpext';
	}
	if ( ! $method && ( extension_loaded( 'sockets' ) || function_exists( 'fsockopen' ) ) ) {
		$method = 'ftpsockets'; // Sockets: Socket extension; PHP Mode: FSockopen / fwrite / fread.
	}

	/**
	 * Filters the filesystem method to use.
	 *
	 * @since 2.6.0
	 *
	 * @param string $method                       Filesystem method to return.
	 * @param array  $args                         An array of connection details for the method.
	 * @param string $context                      Full path to the directory that is tested for being writable.
	 * @param bool   $allow_relaxed_file_ownership Whether to allow Group/World writable.
	 */
	return apply_filters( 'filesystem_method', $method, $args, $context, $allow_relaxed_file_ownership );
}

/**
 * Displays a form to the user to request for their FTP/SSH details in order
 * to connect to the filesystem.
 *
 * All chosen/entered details are saved, excluding the password.
 *
 * Hostnames may be in the form of hostname:portnumber (eg: wordpress.org:2467)
 * to specify an alternate FTP/SSH port.
 *
 * Plugins may override this form by returning true|false via the {@see 'request_filesystem_credentials'} filter.
 *
 * @since 2.5.0
 * @since 4.6.0 The `$context` parameter default changed from `false` to an empty string.
 *
 * @global string $pagenow The filename of the current screen.
 *
 * @param string        $form_post                    The URL to post the form to.
 * @param string        $type                         Optional. Chosen type of filesystem. Default empty.
 * @param bool|WP_Error $error                        Optional. Whether the current request has failed
 *                                                    to connect, or an error object. Default false.
 * @param string        $context                      Optional. Full path to the directory that is tested
 *                                                    for being writable. Default empty.
 * @param array         $extra_fields                 Optional. Extra `POST` fields to be checked
 *                                                    for inclusion in the post. Default null.
 * @param bool          $allow_relaxed_file_ownership Optional. Whether to allow Group/World writable.
 *                                                    Default false.
 * @return bool|array True if no filesystem credentials are required,
 *                    false if they are required but have not been provided,
 *                    array of credentials if they are required and have been provided.
 */
function request_filesystem_credentials( $form_post, $type = '', $error = false, $context = '', $extra_fields = null, $allow_relaxed_file_ownership = false ) {
	global $pagenow;

	/**
	 * Filters the filesystem credentials.
	 *
	 * Returning anything other than an empty string will effectively short-circuit
	 * output of the filesystem credentials form, returning that value instead.
	 *
	 * A filter should return true if no filesystem credentials are required, false if they are required but have not been
	 * provided, or an array of credentials if they are required and have been provided.
	 *
	 * @since 2.5.0
	 * @since 4.6.0 The `$context` parameter default changed from `false` to an empty string.
	 *
	 * @param mixed         $credentials                  Credentials to return instead. Default empty string.
	 * @param string        $form_post                    The URL to post the form to.
	 * @param string        $type                         Chosen type of filesystem.
	 * @param bool|WP_Error $error                        Whether the current request has failed to connect,
	 *                                                    or an error object.
	 * @param string        $context                      Full path to the directory that is tested for
	 *                                                    being writable.
	 * @param array         $extra_fields                 Extra POST fields.
	 * @param bool          $allow_relaxed_file_ownership Whether to allow Group/World writable.
	 */
	$req_cred = apply_filters( 'request_filesystem_credentials', '', $form_post, $type, $error, $context, $extra_fields, $allow_relaxed_file_ownership );

	if ( '' !== $req_cred ) {
		return $req_cred;
	}

	if ( empty( $type ) ) {
		$type = get_filesystem_method( array(), $context, $allow_relaxed_file_ownership );
	}

	if ( 'direct' === $type ) {
		return true;
	}

	if ( is_null( $extra_fields ) ) {
		$extra_fields = array( 'version', 'locale' );
	}

	$credentials = get_option(
		'ftp_credentials',
		array(
			'hostname' => '',
			'username' => '',
		)
	);

	$submitted_form = wp_unslash( $_POST );

	// Verify nonce, or unset submitted form field values on failure.
	if ( ! isset( $_POST['_fs_nonce'] ) || ! wp_verify_nonce( $_POST['_fs_nonce'], 'filesystem-credentials' ) ) {
		unset(
			$submitted_form['hostname'],
			$submitted_form['username'],
			$submitted_form['password'],
			$submitted_form['public_key'],
			$submitted_form['private_key'],
			$submitted_form['connection_type']
		);
	}

	$ftp_constants = array(
		'hostname'    => 'FTP_HOST',
		'username'    => 'FTP_USER',
		'password'    => 'FTP_PASS',
		'public_key'  => 'FTP_PUBKEY',
		'private_key' => 'FTP_PRIKEY',
	);

	/*
	 * If defined, set it to that. Else, if POST'd, set it to that. If not, set it to an empty string.
	 * Otherwise, keep it as it previously was (saved details in option).
	 */
	foreach ( $ftp_constants as $key => $constant ) {
		if ( defined( $constant ) ) {
			$credentials[ $key ] = constant( $constant );
		} elseif ( ! empty( $submitted_form[ $key ] ) ) {
			$credentials[ $key ] = $submitted_form[ $key ];
		} elseif ( ! isset( $credentials[ $key ] ) ) {
			$credentials[ $key ] = '';
		}
	}

	// Sanitize the hostname, some people might pass in odd data.
	$credentials['hostname'] = preg_replace( '|\w+://|', '', $credentials['hostname'] ); // Strip any schemes off.

	if ( strpos( $credentials['hostname'], ':' ) ) {
		list( $credentials['hostname'], $credentials['port'] ) = explode( ':', $credentials['hostname'], 2 );
		if ( ! is_numeric( $credentials['port'] ) ) {
			unset( $credentials['port'] );
		}
	} else {
		unset( $credentials['port'] );
	}

	if ( ( defined( 'FTP_SSH' ) && FTP_SSH ) || ( defined( 'FS_METHOD' ) && 'ssh2' === FS_METHOD ) ) {
		$credentials['connection_type'] = 'ssh';
	} elseif ( ( defined( 'FTP_SSL' ) && FTP_SSL ) && 'ftpext' === $type ) { // Only the FTP Extension understands SSL.
		$credentials['connection_type'] = 'ftps';
	} elseif ( ! empty( $submitted_form['connection_type'] ) ) {
		$credentials['connection_type'] = $submitted_form['connection_type'];
	} elseif ( ! isset( $credentials['connection_type'] ) ) { // All else fails (and it's not defaulted to something else saved), default to FTP.
		$credentials['connection_type'] = 'ftp';
	}

	if ( ! $error
		&& ( ! empty( $credentials['hostname'] ) && ! empty( $credentials['username'] ) && ! empty( $credentials['password'] )
			|| 'ssh' === $credentials['connection_type'] && ! empty( $credentials['public_key'] ) && ! empty( $credentials['private_key'] )
		)
	) {
		$stored_credentials = $credentials;

		if ( ! empty( $stored_credentials['port'] ) ) { // Save port as part of hostname to simplify above code.
			$stored_credentials['hostname'] .= ':' . $stored_credentials['port'];
		}

		unset(
			$stored_credentials['password'],
			$stored_credentials['port'],
			$stored_credentials['private_key'],
			$stored_credentials['public_key']
		);

		if ( ! wp_installing() ) {
			update_option( 'ftp_credentials', $stored_credentials, false );
		}

		return $credentials;
	}

	$hostname        = isset( $credentials['hostname'] ) ? $credentials['hostname'] : '';
	$username        = isset( $credentials['username'] ) ? $credentials['username'] : '';
	$public_key      = isset( $credentials['public_key'] ) ? $credentials['public_key'] : '';
	$private_key     = isset( $credentials['private_key'] ) ? $credentials['private_key'] : '';
	$port            = isset( $credentials['port'] ) ? $credentials['port'] : '';
	$connection_type = isset( $credentials['connection_type'] ) ? $credentials['connection_type'] : '';

	if ( $error ) {
		$error_string = __( '<strong>Error:</strong> Could not connect to the server. Please verify the settings are correct.' );
		if ( is_wp_error( $error ) ) {
			$error_string = esc_html( $error->get_error_message() );
		}
		wp_admin_notice(
			$error_string,
			array(
				'id'                 => 'message',
				'additional_classes' => array( 'error' ),
			)
		);
	}

	$types = array();
	if ( extension_loaded( 'ftp' ) || extension_loaded( 'sockets' ) || function_exists( 'fsockopen' ) ) {
		$types['ftp'] = __( 'FTP' );
	}
	if ( extension_loaded( 'ftp' ) ) { // Only this supports FTPS.
		$types['ftps'] = __( 'FTPS (SSL)' );
	}
	if ( extension_loaded( 'ssh2' ) ) {
		$types['ssh'] = __( 'SSH2' );
	}

	/**
	 * Filters the connection types to output to the filesystem credentials form.
	 *
	 * @since 2.9.0
	 * @since 4.6.0 The `$context` parameter default changed from `false` to an empty string.
	 *
	 * @param string[]      $types       Types of connections.
	 * @param array         $credentials Credentials to connect with.
	 * @param string        $type        Chosen filesystem method.
	 * @param bool|WP_Error $error       Whether the current request has failed to connect,
	 *                                   or an error object.
	 * @param string        $context     Full path to the directory that is tested for being writable.
	 */
	$types = apply_filters( 'fs_ftp_connection_types', $types, $credentials, $type, $error, $context );
	?>
<form action="<?php echo esc_url( $form_post ); ?>" method="post">
<div id="request-filesystem-credentials-form" class="request-filesystem-credentials-form">
	<?php
	// Print a H1 heading in the FTP credentials modal dialog, default is a H2.
	$heading_tag = 'h2';
	if ( 'plugins.php' === $pagenow || 'plugin-install.php' === $pagenow ) {
		$heading_tag = 'h1';
	}
	echo "<$heading_tag id='request-filesystem-credentials-title'>" . __( 'Connection Information' ) . "</$heading_tag>";
	?>
<p id="request-filesystem-credentials-desc">
	<?php
	$label_user = __( 'Username' );
	$label_pass = __( 'Password' );
	_e( 'To perform the requested action, WordPress needs to access your web server.' );
	echo ' ';
	if ( ( isset( $types['ftp'] ) || isset( $types['ftps'] ) ) ) {
		if ( isset( $types['ssh'] ) ) {
			_e( 'Please enter your FTP or SSH credentials to proceed.' );
			$label_user = __( 'FTP/SSH Username' );
			$label_pass = __( 'FTP/SSH Password' );
		} else {
			_e( 'Please enter your FTP credentials to proceed.' );
			$label_user = __( 'FTP Username' );
			$label_pass = __( 'FTP Password' );
		}
		echo ' ';
	}
	_e( 'If you do not remember your credentials, you should contact your web host.' );

	$hostname_value = esc_attr( $hostname );
	if ( ! empty( $port ) ) {
		$hostname_value .= ":$port";
	}

	$password_value = '';
	if ( defined( 'FTP_PASS' ) ) {
		$password_value = '*****';
	}
	?>
</p>
<label for="hostname">
	<span class="field-title"><?php _e( 'Hostname' ); ?></span>
	<input name="hostname" type="text" id="hostname" aria-describedby="request-filesystem-credentials-desc" class="code" placeholder="<?php esc_attr_e( 'example: www.wordpress.org' ); ?>" value="<?php echo $hostname_value; ?>"<?php disabled( defined( 'FTP_HOST' ) ); ?> />
</label>
<div class="ftp-username">
	<label for="username">
		<span class="field-title"><?php echo $label_user; ?></span>
		<input name="username" type="text" id="username" value="<?php echo esc_attr( $username ); ?>"<?php disabled( defined( 'FTP_USER' ) ); ?> />
	</label>
</div>
<div class="ftp-password">
	<label for="password">
		<span class="field-title"><?php echo $label_pass; ?></span>
		<input name="password" type="password" id="password" value="<?php echo $password_value; ?>"<?php disabled( defined( 'FTP_PASS' ) ); ?> spellcheck="false" />
		<?php
		if ( ! defined( 'FTP_PASS' ) ) {
			_e( 'This password will not be stored on the server.' );
		}
		?>
	</label>
</div>
<fieldset>
<legend><?php _e( 'Connection Type' ); ?></legend>
	<?php
	$disabled = disabled( ( defined( 'FTP_SSL' ) && FTP_SSL ) || ( defined( 'FTP_SSH' ) && FTP_SSH ), true, false );
	foreach ( $types as $name => $text ) :
		?>
	<label for="<?php echo esc_attr( $name ); ?>">
		<input type="radio" name="connection_type" id="<?php echo esc_attr( $name ); ?>" value="<?php echo esc_attr( $name ); ?>" <?php checked( $name, $connection_type ); ?> <?php echo $disabled; ?> />
		<?php echo $text; ?>
	</label>
		<?php
	endforeach;
	?>
</fieldset>
	<?php
	if ( isset( $types['ssh'] ) ) {
		$hidden_class = '';
		if ( 'ssh' !== $connection_type || empty( $connection_type ) ) {
			$hidden_class = ' class="hidden"';
		}
		?>
<fieldset id="ssh-keys"<?php echo $hidden_class; ?>>
<legend><?php _e( 'Authentication Keys' ); ?></legend>
<label for="public_key">
	<span class="field-title"><?php _e( 'Public Key:' ); ?></span>
	<input name="public_key" type="text" id="public_key" aria-describedby="auth-keys-desc" value="<?php echo esc_attr( $public_key ); ?>"<?php disabled( defined( 'FTP_PUBKEY' ) ); ?> />
</label>
<label for="private_key">
	<span class="field-title"><?php _e( 'Private Key:' ); ?></span>
	<input name="private_key" type="text" id="private_key" value="<?php echo esc_attr( $private_key ); ?>"<?php disabled( defined( 'FTP_PRIKEY' ) ); ?> />
</label>
<p id="auth-keys-desc"><?php _e( 'Enter the location on the server where the public and private keys are located. If a passphrase is needed, enter that in the password field above.' ); ?></p>
</fieldset>
		<?php
	}

	foreach ( (array) $extra_fields as $field ) {
		if ( isset( $submitted_form[ $field ] ) ) {
			echo '<input type="hidden" name="' . esc_attr( $field ) . '" value="' . esc_attr( $submitted_form[ $field ] ) . '" />';
		}
	}

	/*
	 * Make sure the `submit_button()` function is available during the REST API call
	 * from WP_Site_Health_Auto_Updates::test_check_wp_filesystem_method().
	 */
	if ( ! function_exists( 'submit_button' ) ) {
		require_once ABSPATH . 'wp-admin/includes/template.php';
	}
	?>
	<p class="request-filesystem-credentials-action-buttons">
		<?php wp_nonce_field( 'filesystem-credentials', '_fs_nonce', false, true ); ?>
		<button class="button cancel-button" data-js-action="close" type="button"><?php _e( 'Cancel' ); ?></button>
		<?php submit_button( __( 'Proceed' ), 'primary', 'upgrade', false ); ?>
	</p>
</div>
</form>
	<?php
	return false;
}

/**
 * Prints the filesystem credentials modal when needed.
 *
 * @since 4.2.0
 */
function wp_print_request_filesystem_credentials_modal() {
	$filesystem_method = get_filesystem_method();

	ob_start();
	$filesystem_credentials_are_stored = request_filesystem_credentials( self_admin_url() );
	ob_end_clean();

	$request_filesystem_credentials = ( 'direct' !== $filesystem_method && ! $filesystem_credentials_are_stored );
	if ( ! $request_filesystem_credentials ) {
		return;
	}
	?>
	<div id="request-filesystem-credentials-dialog" class="notification-dialog-wrap request-filesystem-credentials-dialog">
		<div class="notification-dialog-background"></div>
		<div class="notification-dialog" role="dialog" aria-labelledby="request-filesystem-credentials-title" tabindex="0">
			<div class="request-filesystem-credentials-dialog-content">
				<?php request_filesystem_credentials( site_url() ); ?>
			</div>
		</div>
	</div>
	<?php
}

/**
 * Attempts to clear the opcode cache for an individual PHP file.
 *
 * This function can be called safely without having to check the file extension
 * or availability of the OPcache extension.
 *
 * Whether or not invalidation is possible is cached to improve performance.
 *
 * @since 5.5.0
 *
 * @link https://www.php.net/manual/en/function.opcache-invalidate.php
 *
 * @param string $filepath Path to the file, including extension, for which the opcode cache is to be cleared.
 * @param bool   $force    Invalidate even if the modification time is not newer than the file in cache.
 *                         Default false.
 * @return bool True if opcache was invalidated for `$filepath`, or there was nothing to invalidate.
 *              False if opcache invalidation is not available, or is disabled via filter.
 */
function wp_opcache_invalidate( $filepath, $force = false ) {
	static $can_invalidate = null;

	/*
	 * Check to see if WordPress is able to run `opcache_invalidate()` or not, and cache the value.
	 *
	 * First, check to see if the function is available to call, then if the host has restricted
	 * the ability to run the function to avoid a PHP warning.
	 *
	 * `opcache.restrict_api` can specify the path for files allowed to call `opcache_invalidate()`.
	 *
	 * If the host has this set, check whether the path in `opcache.restrict_api` matches
	 * the beginning of the path of the origin file.
	 *
	 * `$_SERVER['SCRIPT_FILENAME']` approximates the origin file's path, but `realpath()`
	 * is necessary because `SCRIPT_FILENAME` can be a relative path when run from CLI.
	 *
	 * For more details, see:
	 * - https://www.php.net/manual/en/opcache.configuration.php
	 * - https://www.php.net/manual/en/reserved.variables.server.php
	 * - https://core.trac.wordpress.org/ticket/36455
	 */
	if ( null === $can_invalidate
		&& function_exists( 'opcache_invalidate' )
		&& ( ! ini_get( 'opcache.restrict_api' )
			|| stripos( realpath( $_SERVER['SCRIPT_FILENAME'] ), ini_get( 'opcache.restrict_api' ) ) === 0 )
	) {
		$can_invalidate = true;
	}

	// If invalidation is not available, return early.
	if ( ! $can_invalidate ) {
		return false;
	}

	// Verify that file to be invalidated has a PHP extension.
	if ( '.php' !== strtolower( substr( $filepath, -4 ) ) ) {
		return false;
	}

	/**
	 * Filters whether to invalidate a file from the opcode cache.
	 *
	 * @since 5.5.0
	 *
	 * @param bool   $will_invalidate Whether WordPress will invalidate `$filepath`. Default true.
	 * @param string $filepath        The path to the PHP file to invalidate.
	 */
	if ( apply_filters( 'wp_opcache_invalidate_file', true, $filepath ) ) {
		return opcache_invalidate( $filepath, $force );
	}

	return false;
}

/**
 * Attempts to clear the opcode cache for a directory of files.
 *
 * @since 6.2.0
 *
 * @see wp_opcache_invalidate()
 * @link https://www.php.net/manual/en/function.opcache-invalidate.php
 *
 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
 *
 * @param string $dir The path to the directory for which the opcode cache is to be cleared.
 */
function wp_opcache_invalidate_directory( $dir ) {
	global $wp_filesystem;

	if ( ! is_string( $dir ) || '' === trim( $dir ) ) {
		if ( WP_DEBUG ) {
			$error_message = sprintf(
				/* translators: %s: The function name. */
				__( '%s expects a non-empty string.' ),
				'<code>wp_opcache_invalidate_directory()</code>'
			);
			wp_trigger_error( '', $error_message );
		}
		return;
	}

	$dirlist = $wp_filesystem->dirlist( $dir, false, true );

	if ( empty( $dirlist ) ) {
		return;
	}

	/*
	 * Recursively invalidate opcache of files in a directory.
	 *
	 * WP_Filesystem_*::dirlist() returns an array of file and directory information.
	 *
	 * This does not include a path to the file or directory.
	 * To invalidate files within sub-directories, recursion is needed
	 * to prepend an absolute path containing the sub-directory's name.
	 *
	 * @param array  $dirlist Array of file/directory information from WP_Filesystem_Base::dirlist(),
	 *                        with sub-directories represented as nested arrays.
	 * @param string $path    Absolute path to the directory.
	 */
	$invalidate_directory = static function ( $dirlist, $path ) use ( &$invalidate_directory ) {
		$path = trailingslashit( $path );

		foreach ( $dirlist as $name => $details ) {
			if ( 'f' === $details['type'] ) {
				wp_opcache_invalidate( $path . $name, true );
			} elseif ( is_array( $details['files'] ) && ! empty( $details['files'] ) ) {
				$invalidate_directory( $details['files'], $path . $name );
			}
		}
	};

	$invalidate_directory( $dirlist, $dir );
}
media.php000064400000350505151327343360006353 0ustar00<?php
/**
 * WordPress Administration Media API.
 *
 * @package WordPress
 * @subpackage Administration
 */

/**
 * Defines the default media upload tabs.
 *
 * @since 2.5.0
 *
 * @return string[] Default tabs.
 */
function media_upload_tabs() {
	$_default_tabs = array(
		'type'     => __( 'From Computer' ), // Handler action suffix => tab text.
		'type_url' => __( 'From URL' ),
		'gallery'  => __( 'Gallery' ),
		'library'  => __( 'Media Library' ),
	);

	/**
	 * Filters the available tabs in the legacy (pre-3.5.0) media popup.
	 *
	 * @since 2.5.0
	 *
	 * @param string[] $_default_tabs An array of media tabs.
	 */
	return apply_filters( 'media_upload_tabs', $_default_tabs );
}

/**
 * Adds the gallery tab back to the tabs array if post has image attachments.
 *
 * @since 2.5.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param array $tabs
 * @return array $tabs with gallery if post has image attachment
 */
function update_gallery_tab( $tabs ) {
	global $wpdb;

	if ( ! isset( $_REQUEST['post_id'] ) ) {
		unset( $tabs['gallery'] );
		return $tabs;
	}

	$post_id = (int) $_REQUEST['post_id'];

	if ( $post_id ) {
		$attachments = (int) $wpdb->get_var( $wpdb->prepare( "SELECT count(*) FROM $wpdb->posts WHERE post_type = 'attachment' AND post_status != 'trash' AND post_parent = %d", $post_id ) );
	}

	if ( empty( $attachments ) ) {
		unset( $tabs['gallery'] );
		return $tabs;
	}

	/* translators: %s: Number of attachments. */
	$tabs['gallery'] = sprintf( __( 'Gallery (%s)' ), "<span id='attachments-count'>$attachments</span>" );

	return $tabs;
}

/**
 * Outputs the legacy media upload tabs UI.
 *
 * @since 2.5.0
 *
 * @global string $redir_tab
 */
function the_media_upload_tabs() {
	global $redir_tab;
	$tabs    = media_upload_tabs();
	$default = 'type';

	if ( ! empty( $tabs ) ) {
		echo "<ul id='sidemenu'>\n";

		if ( isset( $redir_tab ) && array_key_exists( $redir_tab, $tabs ) ) {
			$current = $redir_tab;
		} elseif ( isset( $_GET['tab'] ) && array_key_exists( $_GET['tab'], $tabs ) ) {
			$current = $_GET['tab'];
		} else {
			/** This filter is documented in wp-admin/media-upload.php */
			$current = apply_filters( 'media_upload_default_tab', $default );
		}

		foreach ( $tabs as $callback => $text ) {
			$class = '';

			if ( $current === $callback ) {
				$class = " class='current'";
			}

			$href = add_query_arg(
				array(
					'tab'            => $callback,
					's'              => false,
					'paged'          => false,
					'post_mime_type' => false,
					'm'              => false,
				)
			);
			$link = "<a href='" . esc_url( $href ) . "'$class>$text</a>";
			echo "\t<li id='" . esc_attr( "tab-$callback" ) . "'>$link</li>\n";
		}

		echo "</ul>\n";
	}
}

/**
 * Retrieves the image HTML to send to the editor.
 *
 * @since 2.5.0
 *
 * @param int          $id      Image attachment ID.
 * @param string       $caption Image caption.
 * @param string       $title   Image title attribute.
 * @param string       $align   Image CSS alignment property.
 * @param string       $url     Optional. Image src URL. Default empty.
 * @param bool|string  $rel     Optional. Value for rel attribute or whether to add a default value. Default false.
 * @param string|int[] $size    Optional. Image size. Accepts any registered image size name, or an array of
 *                              width and height values in pixels (in that order). Default 'medium'.
 * @param string       $alt     Optional. Image alt attribute. Default empty.
 * @return string The HTML output to insert into the editor.
 */
function get_image_send_to_editor( $id, $caption, $title, $align, $url = '', $rel = false, $size = 'medium', $alt = '' ) {

	$html = get_image_tag( $id, $alt, '', $align, $size );

	if ( $rel ) {
		if ( is_string( $rel ) ) {
			$rel = ' rel="' . esc_attr( $rel ) . '"';
		} else {
			$rel = ' rel="attachment wp-att-' . (int) $id . '"';
		}
	} else {
		$rel = '';
	}

	if ( $url ) {
		$html = '<a href="' . esc_url( $url ) . '"' . $rel . '>' . $html . '</a>';
	}

	/**
	 * Filters the image HTML markup to send to the editor when inserting an image.
	 *
	 * @since 2.5.0
	 * @since 5.6.0 The `$rel` parameter was added.
	 *
	 * @param string       $html    The image HTML markup to send.
	 * @param int          $id      The attachment ID.
	 * @param string       $caption The image caption.
	 * @param string       $title   The image title.
	 * @param string       $align   The image alignment.
	 * @param string       $url     The image source URL.
	 * @param string|int[] $size    Requested image size. Can be any registered image size name, or
	 *                              an array of width and height values in pixels (in that order).
	 * @param string       $alt     The image alternative, or alt, text.
	 * @param string       $rel     The image rel attribute.
	 */
	$html = apply_filters( 'image_send_to_editor', $html, $id, $caption, $title, $align, $url, $size, $alt, $rel );

	return $html;
}

/**
 * Adds image shortcode with caption to editor.
 *
 * @since 2.6.0
 *
 * @param string  $html    The image HTML markup to send.
 * @param int     $id      Image attachment ID.
 * @param string  $caption Image caption.
 * @param string  $title   Image title attribute (not used).
 * @param string  $align   Image CSS alignment property.
 * @param string  $url     Image source URL (not used).
 * @param string  $size    Image size (not used).
 * @param string  $alt     Image `alt` attribute (not used).
 * @return string The image HTML markup with caption shortcode.
 */
function image_add_caption( $html, $id, $caption, $title, $align, $url, $size, $alt = '' ) {

	/**
	 * Filters the caption text.
	 *
	 * Note: If the caption text is empty, the caption shortcode will not be appended
	 * to the image HTML when inserted into the editor.
	 *
	 * Passing an empty value also prevents the {@see 'image_add_caption_shortcode'}
	 * Filters from being evaluated at the end of image_add_caption().
	 *
	 * @since 4.1.0
	 *
	 * @param string $caption The original caption text.
	 * @param int    $id      The attachment ID.
	 */
	$caption = apply_filters( 'image_add_caption_text', $caption, $id );

	/**
	 * Filters whether to disable captions.
	 *
	 * Prevents image captions from being appended to image HTML when inserted into the editor.
	 *
	 * @since 2.6.0
	 *
	 * @param bool $bool Whether to disable appending captions. Returning true from the filter
	 *                   will disable captions. Default empty string.
	 */
	if ( empty( $caption ) || apply_filters( 'disable_captions', '' ) ) {
		return $html;
	}

	$id = ( 0 < (int) $id ) ? 'attachment_' . $id : '';

	if ( ! preg_match( '/width=["\']([0-9]+)/', $html, $matches ) ) {
		return $html;
	}

	$width = $matches[1];

	$caption = str_replace( array( "\r\n", "\r" ), "\n", $caption );
	$caption = preg_replace_callback( '/<[a-zA-Z0-9]+(?: [^<>]+>)*/', '_cleanup_image_add_caption', $caption );

	// Convert any remaining line breaks to <br />.
	$caption = preg_replace( '/[ \n\t]*\n[ \t]*/', '<br />', $caption );

	$html = preg_replace( '/(class=["\'][^\'"]*)align(none|left|right|center)\s?/', '$1', $html );
	if ( empty( $align ) ) {
		$align = 'none';
	}

	$shcode = '[caption id="' . $id . '" align="align' . $align . '" width="' . $width . '"]' . $html . ' ' . $caption . '[/caption]';

	/**
	 * Filters the image HTML markup including the caption shortcode.
	 *
	 * @since 2.6.0
	 *
	 * @param string $shcode The image HTML markup with caption shortcode.
	 * @param string $html   The image HTML markup.
	 */
	return apply_filters( 'image_add_caption_shortcode', $shcode, $html );
}

/**
 * Private preg_replace callback used in image_add_caption().
 *
 * @access private
 * @since 3.4.0
 *
 * @param array $matches Single regex match.
 * @return string Cleaned up HTML for caption.
 */
function _cleanup_image_add_caption( $matches ) {
	// Remove any line breaks from inside the tags.
	return preg_replace( '/[\r\n\t]+/', ' ', $matches[0] );
}

/**
 * Adds image HTML to editor.
 *
 * @since 2.5.0
 *
 * @param string $html
 */
function media_send_to_editor( $html ) {
	?>
	<script type="text/javascript">
	var win = window.dialogArguments || opener || parent || top;
	win.send_to_editor( <?php echo wp_json_encode( $html ); ?> );
	</script>
	<?php
	exit;
}

/**
 * Saves a file submitted from a POST request and create an attachment post for it.
 *
 * @since 2.5.0
 *
 * @param string $file_id   Index of the `$_FILES` array that the file was sent.
 * @param int    $post_id   The post ID of a post to attach the media item to. Required, but can
 *                          be set to 0, creating a media item that has no relationship to a post.
 * @param array  $post_data Optional. Overwrite some of the attachment.
 * @param array  $overrides Optional. Override the wp_handle_upload() behavior.
 * @return int|WP_Error ID of the attachment or a WP_Error object on failure.
 */
function media_handle_upload( $file_id, $post_id, $post_data = array(), $overrides = array( 'test_form' => false ) ) {
	$time = current_time( 'mysql' );
	$post = get_post( $post_id );

	if ( $post ) {
		// The post date doesn't usually matter for pages, so don't backdate this upload.
		if ( 'page' !== $post->post_type && substr( $post->post_date, 0, 4 ) > 0 ) {
			$time = $post->post_date;
		}
	}

	$file = wp_handle_upload( $_FILES[ $file_id ], $overrides, $time );

	if ( isset( $file['error'] ) ) {
		return new WP_Error( 'upload_error', $file['error'] );
	}

	$name = $_FILES[ $file_id ]['name'];
	$ext  = pathinfo( $name, PATHINFO_EXTENSION );
	$name = wp_basename( $name, ".$ext" );

	$url     = $file['url'];
	$type    = $file['type'];
	$file    = $file['file'];
	$title   = sanitize_text_field( $name );
	$content = '';
	$excerpt = '';

	if ( preg_match( '#^audio#', $type ) ) {
		$meta = wp_read_audio_metadata( $file );

		if ( ! empty( $meta['title'] ) ) {
			$title = $meta['title'];
		}

		if ( ! empty( $title ) ) {

			if ( ! empty( $meta['album'] ) && ! empty( $meta['artist'] ) ) {
				/* translators: 1: Audio track title, 2: Album title, 3: Artist name. */
				$content .= sprintf( __( '"%1$s" from %2$s by %3$s.' ), $title, $meta['album'], $meta['artist'] );
			} elseif ( ! empty( $meta['album'] ) ) {
				/* translators: 1: Audio track title, 2: Album title. */
				$content .= sprintf( __( '"%1$s" from %2$s.' ), $title, $meta['album'] );
			} elseif ( ! empty( $meta['artist'] ) ) {
				/* translators: 1: Audio track title, 2: Artist name. */
				$content .= sprintf( __( '"%1$s" by %2$s.' ), $title, $meta['artist'] );
			} else {
				/* translators: %s: Audio track title. */
				$content .= sprintf( __( '"%s".' ), $title );
			}
		} elseif ( ! empty( $meta['album'] ) ) {

			if ( ! empty( $meta['artist'] ) ) {
				/* translators: 1: Audio album title, 2: Artist name. */
				$content .= sprintf( __( '%1$s by %2$s.' ), $meta['album'], $meta['artist'] );
			} else {
				$content .= $meta['album'] . '.';
			}
		} elseif ( ! empty( $meta['artist'] ) ) {

			$content .= $meta['artist'] . '.';

		}

		if ( ! empty( $meta['year'] ) ) {
			/* translators: Audio file track information. %d: Year of audio track release. */
			$content .= ' ' . sprintf( __( 'Released: %d.' ), $meta['year'] );
		}

		if ( ! empty( $meta['track_number'] ) ) {
			$track_number = explode( '/', $meta['track_number'] );

			if ( is_numeric( $track_number[0] ) ) {
				if ( isset( $track_number[1] ) && is_numeric( $track_number[1] ) ) {
					$content .= ' ' . sprintf(
						/* translators: Audio file track information. 1: Audio track number, 2: Total audio tracks. */
						__( 'Track %1$s of %2$s.' ),
						number_format_i18n( $track_number[0] ),
						number_format_i18n( $track_number[1] )
					);
				} else {
					$content .= ' ' . sprintf(
						/* translators: Audio file track information. %s: Audio track number. */
						__( 'Track %s.' ),
						number_format_i18n( $track_number[0] )
					);
				}
			}
		}

		if ( ! empty( $meta['genre'] ) ) {
			/* translators: Audio file genre information. %s: Audio genre name. */
			$content .= ' ' . sprintf( __( 'Genre: %s.' ), $meta['genre'] );
		}

		// Use image exif/iptc data for title and caption defaults if possible.
	} elseif ( str_starts_with( $type, 'image/' ) ) {
		$image_meta = wp_read_image_metadata( $file );

		if ( $image_meta ) {
			if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) {
				$title = $image_meta['title'];
			}

			if ( trim( $image_meta['caption'] ) ) {
				$excerpt = $image_meta['caption'];
			}
		}
	}

	// Construct the attachment array.
	$attachment = array_merge(
		array(
			'post_mime_type' => $type,
			'guid'           => $url,
			'post_parent'    => $post_id,
			'post_title'     => $title,
			'post_content'   => $content,
			'post_excerpt'   => $excerpt,
		),
		$post_data
	);

	// This should never be set as it would then overwrite an existing attachment.
	unset( $attachment['ID'] );

	// Save the data.
	$attachment_id = wp_insert_attachment( $attachment, $file, $post_id, true );

	if ( ! is_wp_error( $attachment_id ) ) {
		/*
		 * Set a custom header with the attachment_id.
		 * Used by the browser/client to resume creating image sub-sizes after a PHP fatal error.
		 */
		if ( ! headers_sent() ) {
			header( 'X-WP-Upload-Attachment-ID: ' . $attachment_id );
		}

		/*
		 * The image sub-sizes are created during wp_generate_attachment_metadata().
		 * This is generally slow and may cause timeouts or out of memory errors.
		 */
		wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $file ) );
	}

	return $attachment_id;
}

/**
 * Handles a side-loaded file in the same way as an uploaded file is handled by media_handle_upload().
 *
 * @since 2.6.0
 * @since 5.3.0 The `$post_id` parameter was made optional.
 *
 * @param string[] $file_array Array that represents a `$_FILES` upload array.
 * @param int      $post_id    Optional. The post ID the media is associated with.
 * @param string   $desc       Optional. Description of the side-loaded file. Default null.
 * @param array    $post_data  Optional. Post data to override. Default empty array.
 * @return int|WP_Error The ID of the attachment or a WP_Error on failure.
 */
function media_handle_sideload( $file_array, $post_id = 0, $desc = null, $post_data = array() ) {
	$overrides = array( 'test_form' => false );

	if ( isset( $post_data['post_date'] ) && substr( $post_data['post_date'], 0, 4 ) > 0 ) {
		$time = $post_data['post_date'];
	} else {
		$post = get_post( $post_id );
		if ( $post && substr( $post->post_date, 0, 4 ) > 0 ) {
			$time = $post->post_date;
		} else {
			$time = current_time( 'mysql' );
		}
	}

	$file = wp_handle_sideload( $file_array, $overrides, $time );

	if ( isset( $file['error'] ) ) {
		return new WP_Error( 'upload_error', $file['error'] );
	}

	$url     = $file['url'];
	$type    = $file['type'];
	$file    = $file['file'];
	$title   = preg_replace( '/\.[^.]+$/', '', wp_basename( $file ) );
	$content = '';

	// Use image exif/iptc data for title and caption defaults if possible.
	$image_meta = wp_read_image_metadata( $file );

	if ( $image_meta ) {
		if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) {
			$title = $image_meta['title'];
		}

		if ( trim( $image_meta['caption'] ) ) {
			$content = $image_meta['caption'];
		}
	}

	if ( isset( $desc ) ) {
		$title = $desc;
	}

	// Construct the attachment array.
	$attachment = array_merge(
		array(
			'post_mime_type' => $type,
			'guid'           => $url,
			'post_parent'    => $post_id,
			'post_title'     => $title,
			'post_content'   => $content,
		),
		$post_data
	);

	// This should never be set as it would then overwrite an existing attachment.
	unset( $attachment['ID'] );

	// Save the attachment metadata.
	$attachment_id = wp_insert_attachment( $attachment, $file, $post_id, true );

	if ( ! is_wp_error( $attachment_id ) ) {
		wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $file ) );
	}

	return $attachment_id;
}

/**
 * Outputs the iframe to display the media upload page.
 *
 * @since 2.5.0
 * @since 5.3.0 Formalized the existing and already documented `...$args` parameter
 *              by adding it to the function signature.
 *
 * @global string $body_id
 *
 * @param callable $content_func Function that outputs the content.
 * @param mixed    ...$args      Optional additional parameters to pass to the callback function when it's called.
 */
function wp_iframe( $content_func, ...$args ) {
	global $body_id;

	_wp_admin_html_begin();
	?>
	<title><?php bloginfo( 'name' ); ?> &rsaquo; <?php _e( 'Uploads' ); ?> &#8212; <?php _e( 'WordPress' ); ?></title>
	<?php

	wp_enqueue_style( 'colors' );
	// Check callback name for 'media'.
	if (
		( is_array( $content_func ) && ! empty( $content_func[1] ) && str_starts_with( (string) $content_func[1], 'media' ) ) ||
		( ! is_array( $content_func ) && str_starts_with( $content_func, 'media' ) )
	) {
		wp_enqueue_style( 'deprecated-media' );
	}

	?>
	<script type="text/javascript">
	addLoadEvent = function(func){if(typeof jQuery!=='undefined')jQuery(function(){func();});else if(typeof wpOnload!=='function'){wpOnload=func;}else{var oldonload=wpOnload;wpOnload=function(){oldonload();func();}}};
	var ajaxurl = '<?php echo esc_js( admin_url( 'admin-ajax.php', 'relative' ) ); ?>', pagenow = 'media-upload-popup', adminpage = 'media-upload-popup',
	isRtl = <?php echo (int) is_rtl(); ?>;
	</script>
	<?php
	/** This action is documented in wp-admin/admin-header.php */
	do_action( 'admin_enqueue_scripts', 'media-upload-popup' );

	/**
	 * Fires when admin styles enqueued for the legacy (pre-3.5.0) media upload popup are printed.
	 *
	 * @since 2.9.0
	 */
	do_action( 'admin_print_styles-media-upload-popup' );  // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores

	/** This action is documented in wp-admin/admin-header.php */
	do_action( 'admin_print_styles' );

	/**
	 * Fires when admin scripts enqueued for the legacy (pre-3.5.0) media upload popup are printed.
	 *
	 * @since 2.9.0
	 */
	do_action( 'admin_print_scripts-media-upload-popup' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores

	/** This action is documented in wp-admin/admin-header.php */
	do_action( 'admin_print_scripts' );

	/**
	 * Fires when scripts enqueued for the admin header for the legacy (pre-3.5.0)
	 * media upload popup are printed.
	 *
	 * @since 2.9.0
	 */
	do_action( 'admin_head-media-upload-popup' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores

	/** This action is documented in wp-admin/admin-header.php */
	do_action( 'admin_head' );

	if ( is_string( $content_func ) ) {
		/**
		 * Fires in the admin header for each specific form tab in the legacy
		 * (pre-3.5.0) media upload popup.
		 *
		 * The dynamic portion of the hook name, `$content_func`, refers to the form
		 * callback for the media upload type.
		 *
		 * @since 2.5.0
		 */
		do_action( "admin_head_{$content_func}" );
	}

	$body_id_attr = '';

	if ( isset( $body_id ) ) {
		$body_id_attr = ' id="' . $body_id . '"';
	}

	?>
	</head>
	<body<?php echo $body_id_attr; ?> class="wp-core-ui no-js">
	<script type="text/javascript">
	document.body.className = document.body.className.replace('no-js', 'js');
	</script>
	<?php

	call_user_func_array( $content_func, $args );

	/** This action is documented in wp-admin/admin-footer.php */
	do_action( 'admin_print_footer_scripts' );

	?>
	<script type="text/javascript">if(typeof wpOnload==='function')wpOnload();</script>
	</body>
	</html>
	<?php
}

/**
 * Adds the media button to the editor.
 *
 * @since 2.5.0
 *
 * @global int $post_ID
 *
 * @param string $editor_id
 */
function media_buttons( $editor_id = 'content' ) {
	static $instance = 0;
	++$instance;

	$post = get_post();

	if ( ! $post && ! empty( $GLOBALS['post_ID'] ) ) {
		$post = $GLOBALS['post_ID'];
	}

	wp_enqueue_media( array( 'post' => $post ) );

	$img = '<span class="wp-media-buttons-icon"></span> ';

	$id_attribute = 1 === $instance ? ' id="insert-media-button"' : '';

	printf(
		'<button type="button"%s class="button insert-media add_media" data-editor="%s">%s</button>',
		$id_attribute,
		esc_attr( $editor_id ),
		$img . __( 'Add Media' )
	);

	/**
	 * Filters the legacy (pre-3.5.0) media buttons.
	 *
	 * Use {@see 'media_buttons'} action instead.
	 *
	 * @since 2.5.0
	 * @deprecated 3.5.0 Use {@see 'media_buttons'} action instead.
	 *
	 * @param string $string Media buttons context. Default empty.
	 */
	$legacy_filter = apply_filters_deprecated( 'media_buttons_context', array( '' ), '3.5.0', 'media_buttons' );

	if ( $legacy_filter ) {
		// #WP22559. Close <a> if a plugin started by closing <a> to open their own <a> tag.
		if ( 0 === stripos( trim( $legacy_filter ), '</a>' ) ) {
			$legacy_filter .= '</a>';
		}
		echo $legacy_filter;
	}
}

/**
 * Retrieves the upload iframe source URL.
 *
 * @since 3.0.0
 *
 * @global int $post_ID
 *
 * @param string $type    Media type.
 * @param int    $post_id Post ID.
 * @param string $tab     Media upload tab.
 * @return string Upload iframe source URL.
 */
function get_upload_iframe_src( $type = null, $post_id = null, $tab = null ) {
	global $post_ID;

	if ( empty( $post_id ) ) {
		$post_id = $post_ID;
	}

	$upload_iframe_src = add_query_arg( 'post_id', (int) $post_id, admin_url( 'media-upload.php' ) );

	if ( $type && 'media' !== $type ) {
		$upload_iframe_src = add_query_arg( 'type', $type, $upload_iframe_src );
	}

	if ( ! empty( $tab ) ) {
		$upload_iframe_src = add_query_arg( 'tab', $tab, $upload_iframe_src );
	}

	/**
	 * Filters the upload iframe source URL for a specific media type.
	 *
	 * The dynamic portion of the hook name, `$type`, refers to the type
	 * of media uploaded.
	 *
	 * Possible hook names include:
	 *
	 *  - `image_upload_iframe_src`
	 *  - `media_upload_iframe_src`
	 *
	 * @since 3.0.0
	 *
	 * @param string $upload_iframe_src The upload iframe source URL.
	 */
	$upload_iframe_src = apply_filters( "{$type}_upload_iframe_src", $upload_iframe_src );

	return add_query_arg( 'TB_iframe', true, $upload_iframe_src );
}

/**
 * Handles form submissions for the legacy media uploader.
 *
 * @since 2.5.0
 *
 * @return null|array|void Array of error messages keyed by attachment ID, null or void on success.
 */
function media_upload_form_handler() {
	check_admin_referer( 'media-form' );

	$errors = null;

	if ( isset( $_POST['send'] ) ) {
		$keys    = array_keys( $_POST['send'] );
		$send_id = (int) reset( $keys );
	}

	if ( ! empty( $_POST['attachments'] ) ) {
		foreach ( $_POST['attachments'] as $attachment_id => $attachment ) {
			$post  = get_post( $attachment_id, ARRAY_A );
			$_post = $post;

			if ( ! current_user_can( 'edit_post', $attachment_id ) ) {
				continue;
			}

			if ( isset( $attachment['post_content'] ) ) {
				$post['post_content'] = $attachment['post_content'];
			}

			if ( isset( $attachment['post_title'] ) ) {
				$post['post_title'] = $attachment['post_title'];
			}

			if ( isset( $attachment['post_excerpt'] ) ) {
				$post['post_excerpt'] = $attachment['post_excerpt'];
			}

			if ( isset( $attachment['menu_order'] ) ) {
				$post['menu_order'] = $attachment['menu_order'];
			}

			if ( isset( $send_id ) && $attachment_id === $send_id ) {
				if ( isset( $attachment['post_parent'] ) ) {
					$post['post_parent'] = $attachment['post_parent'];
				}
			}

			/**
			 * Filters the attachment fields to be saved.
			 *
			 * @since 2.5.0
			 *
			 * @see wp_get_attachment_metadata()
			 *
			 * @param array $post       An array of post data.
			 * @param array $attachment An array of attachment metadata.
			 */
			$post = apply_filters( 'attachment_fields_to_save', $post, $attachment );

			if ( isset( $attachment['image_alt'] ) ) {
				$image_alt = wp_unslash( $attachment['image_alt'] );

				if ( get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) !== $image_alt ) {
					$image_alt = wp_strip_all_tags( $image_alt, true );

					// update_post_meta() expects slashed.
					update_post_meta( $attachment_id, '_wp_attachment_image_alt', wp_slash( $image_alt ) );
				}
			}

			if ( isset( $post['errors'] ) ) {
				$errors[ $attachment_id ] = $post['errors'];
				unset( $post['errors'] );
			}

			if ( $post != $_post ) {
				wp_update_post( $post );
			}

			foreach ( get_attachment_taxonomies( $post ) as $t ) {
				if ( isset( $attachment[ $t ] ) ) {
					wp_set_object_terms( $attachment_id, array_map( 'trim', preg_split( '/,+/', $attachment[ $t ] ) ), $t, false );
				}
			}
		}
	}

	if ( isset( $_POST['insert-gallery'] ) || isset( $_POST['update-gallery'] ) ) {
		?>
		<script type="text/javascript">
		var win = window.dialogArguments || opener || parent || top;
		win.tb_remove();
		</script>
		<?php

		exit;
	}

	if ( isset( $send_id ) ) {
		$attachment = wp_unslash( $_POST['attachments'][ $send_id ] );
		$html       = isset( $attachment['post_title'] ) ? $attachment['post_title'] : '';

		if ( ! empty( $attachment['url'] ) ) {
			$rel = '';

			if ( str_contains( $attachment['url'], 'attachment_id' ) || get_attachment_link( $send_id ) === $attachment['url'] ) {
				$rel = " rel='attachment wp-att-" . esc_attr( $send_id ) . "'";
			}

			$html = "<a href='{$attachment['url']}'$rel>$html</a>";
		}

		/**
		 * Filters the HTML markup for a media item sent to the editor.
		 *
		 * @since 2.5.0
		 *
		 * @see wp_get_attachment_metadata()
		 *
		 * @param string $html       HTML markup for a media item sent to the editor.
		 * @param int    $send_id    The first key from the $_POST['send'] data.
		 * @param array  $attachment Array of attachment metadata.
		 */
		$html = apply_filters( 'media_send_to_editor', $html, $send_id, $attachment );

		return media_send_to_editor( $html );
	}

	return $errors;
}

/**
 * Handles the process of uploading media.
 *
 * @since 2.5.0
 *
 * @return null|string
 */
function wp_media_upload_handler() {
	$errors = array();
	$id     = 0;

	if ( isset( $_POST['html-upload'] ) && ! empty( $_FILES ) ) {
		check_admin_referer( 'media-form' );
		// Upload File button was clicked.
		$id = media_handle_upload( 'async-upload', $_REQUEST['post_id'] );
		unset( $_FILES );

		if ( is_wp_error( $id ) ) {
			$errors['upload_error'] = $id;
			$id                     = false;
		}
	}

	if ( ! empty( $_POST['insertonlybutton'] ) ) {
		$src = $_POST['src'];

		if ( ! empty( $src ) && ! strpos( $src, '://' ) ) {
			$src = "http://$src";
		}

		if ( isset( $_POST['media_type'] ) && 'image' !== $_POST['media_type'] ) {
			$title = esc_html( wp_unslash( $_POST['title'] ) );
			if ( empty( $title ) ) {
				$title = esc_html( wp_basename( $src ) );
			}

			if ( $title && $src ) {
				$html = "<a href='" . esc_url( $src ) . "'>$title</a>";
			}

			$type = 'file';
			$ext  = preg_replace( '/^.+?\.([^.]+)$/', '$1', $src );

			if ( $ext ) {
				$ext_type = wp_ext2type( $ext );
				if ( 'audio' === $ext_type || 'video' === $ext_type ) {
					$type = $ext_type;
				}
			}

			/**
			 * Filters the URL sent to the editor for a specific media type.
			 *
			 * The dynamic portion of the hook name, `$type`, refers to the type
			 * of media being sent.
			 *
			 * Possible hook names include:
			 *
			 *  - `audio_send_to_editor_url`
			 *  - `file_send_to_editor_url`
			 *  - `video_send_to_editor_url`
			 *
			 * @since 3.3.0
			 *
			 * @param string $html  HTML markup sent to the editor.
			 * @param string $src   Media source URL.
			 * @param string $title Media title.
			 */
			$html = apply_filters( "{$type}_send_to_editor_url", $html, sanitize_url( $src ), $title );
		} else {
			$align = '';
			$alt   = esc_attr( wp_unslash( $_POST['alt'] ) );

			if ( isset( $_POST['align'] ) ) {
				$align = esc_attr( wp_unslash( $_POST['align'] ) );
				$class = " class='align$align'";
			}

			if ( ! empty( $src ) ) {
				$html = "<img src='" . esc_url( $src ) . "' alt='$alt'$class />";
			}

			/**
			 * Filters the image URL sent to the editor.
			 *
			 * @since 2.8.0
			 *
			 * @param string $html  HTML markup sent to the editor for an image.
			 * @param string $src   Image source URL.
			 * @param string $alt   Image alternate, or alt, text.
			 * @param string $align The image alignment. Default 'alignnone'. Possible values include
			 *                      'alignleft', 'aligncenter', 'alignright', 'alignnone'.
			 */
			$html = apply_filters( 'image_send_to_editor_url', $html, sanitize_url( $src ), $alt, $align );
		}

		return media_send_to_editor( $html );
	}

	if ( isset( $_POST['save'] ) ) {
		$errors['upload_notice'] = __( 'Saved.' );
		wp_enqueue_script( 'admin-gallery' );

		return wp_iframe( 'media_upload_gallery_form', $errors );

	} elseif ( ! empty( $_POST ) ) {
		$return = media_upload_form_handler();

		if ( is_string( $return ) ) {
			return $return;
		}

		if ( is_array( $return ) ) {
			$errors = $return;
		}
	}

	if ( isset( $_GET['tab'] ) && 'type_url' === $_GET['tab'] ) {
		$type = 'image';

		if ( isset( $_GET['type'] ) && in_array( $_GET['type'], array( 'video', 'audio', 'file' ), true ) ) {
			$type = $_GET['type'];
		}

		return wp_iframe( 'media_upload_type_url_form', $type, $errors, $id );
	}

	return wp_iframe( 'media_upload_type_form', 'image', $errors, $id );
}

/**
 * Downloads an image from the specified URL, saves it as an attachment, and optionally attaches it to a post.
 *
 * @since 2.6.0
 * @since 4.2.0 Introduced the `$return_type` parameter.
 * @since 4.8.0 Introduced the 'id' option for the `$return_type` parameter.
 * @since 5.3.0 The `$post_id` parameter was made optional.
 * @since 5.4.0 The original URL of the attachment is stored in the `_source_url`
 *              post meta value.
 * @since 5.8.0 Added 'webp' to the default list of allowed file extensions.
 *
 * @param string $file        The URL of the image to download.
 * @param int    $post_id     Optional. The post ID the media is to be associated with.
 * @param string $desc        Optional. Description of the image.
 * @param string $return_type Optional. Accepts 'html' (image tag html) or 'src' (URL),
 *                            or 'id' (attachment ID). Default 'html'.
 * @return string|int|WP_Error Populated HTML img tag, attachment ID, or attachment source
 *                             on success, WP_Error object otherwise.
 */
function media_sideload_image( $file, $post_id = 0, $desc = null, $return_type = 'html' ) {
	if ( ! empty( $file ) ) {

		$allowed_extensions = array( 'jpg', 'jpeg', 'jpe', 'png', 'gif', 'webp' );

		/**
		 * Filters the list of allowed file extensions when sideloading an image from a URL.
		 *
		 * The default allowed extensions are:
		 *
		 *  - `jpg`
		 *  - `jpeg`
		 *  - `jpe`
		 *  - `png`
		 *  - `gif`
		 *  - `webp`
		 *
		 * @since 5.6.0
		 * @since 5.8.0 Added 'webp' to the default list of allowed file extensions.
		 *
		 * @param string[] $allowed_extensions Array of allowed file extensions.
		 * @param string   $file               The URL of the image to download.
		 */
		$allowed_extensions = apply_filters( 'image_sideload_extensions', $allowed_extensions, $file );
		$allowed_extensions = array_map( 'preg_quote', $allowed_extensions );

		// Set variables for storage, fix file filename for query strings.
		preg_match( '/[^\?]+\.(' . implode( '|', $allowed_extensions ) . ')\b/i', $file, $matches );

		if ( ! $matches ) {
			return new WP_Error( 'image_sideload_failed', __( 'Invalid image URL.' ) );
		}

		$file_array         = array();
		$file_array['name'] = wp_basename( $matches[0] );

		// Download file to temp location.
		$file_array['tmp_name'] = download_url( $file );

		// If error storing temporarily, return the error.
		if ( is_wp_error( $file_array['tmp_name'] ) ) {
			return $file_array['tmp_name'];
		}

		// Do the validation and storage stuff.
		$id = media_handle_sideload( $file_array, $post_id, $desc );

		// If error storing permanently, unlink.
		if ( is_wp_error( $id ) ) {
			@unlink( $file_array['tmp_name'] );
			return $id;
		}

		// Store the original attachment source in meta.
		add_post_meta( $id, '_source_url', $file );

		// If attachment ID was requested, return it.
		if ( 'id' === $return_type ) {
			return $id;
		}

		$src = wp_get_attachment_url( $id );
	}

	// Finally, check to make sure the file has been saved, then return the HTML.
	if ( ! empty( $src ) ) {
		if ( 'src' === $return_type ) {
			return $src;
		}

		$alt  = isset( $desc ) ? esc_attr( $desc ) : '';
		$html = "<img src='$src' alt='$alt' />";

		return $html;
	} else {
		return new WP_Error( 'image_sideload_failed' );
	}
}

/**
 * Retrieves the legacy media uploader form in an iframe.
 *
 * @since 2.5.0
 *
 * @return string|null
 */
function media_upload_gallery() {
	$errors = array();

	if ( ! empty( $_POST ) ) {
		$return = media_upload_form_handler();

		if ( is_string( $return ) ) {
			return $return;
		}

		if ( is_array( $return ) ) {
			$errors = $return;
		}
	}

	wp_enqueue_script( 'admin-gallery' );
	return wp_iframe( 'media_upload_gallery_form', $errors );
}

/**
 * Retrieves the legacy media library form in an iframe.
 *
 * @since 2.5.0
 *
 * @return string|null
 */
function media_upload_library() {
	$errors = array();

	if ( ! empty( $_POST ) ) {
		$return = media_upload_form_handler();

		if ( is_string( $return ) ) {
			return $return;
		}
		if ( is_array( $return ) ) {
			$errors = $return;
		}
	}

	return wp_iframe( 'media_upload_library_form', $errors );
}

/**
 * Retrieves HTML for the image alignment radio buttons with the specified one checked.
 *
 * @since 2.7.0
 *
 * @param WP_Post $post
 * @param string  $checked
 * @return string
 */
function image_align_input_fields( $post, $checked = '' ) {

	if ( empty( $checked ) ) {
		$checked = get_user_setting( 'align', 'none' );
	}

	$alignments = array(
		'none'   => __( 'None' ),
		'left'   => __( 'Left' ),
		'center' => __( 'Center' ),
		'right'  => __( 'Right' ),
	);

	if ( ! array_key_exists( (string) $checked, $alignments ) ) {
		$checked = 'none';
	}

	$output = array();

	foreach ( $alignments as $name => $label ) {
		$name     = esc_attr( $name );
		$output[] = "<input type='radio' name='attachments[{$post->ID}][align]' id='image-align-{$name}-{$post->ID}' value='$name'" .
			( $checked === $name ? " checked='checked'" : '' ) .
			" /><label for='image-align-{$name}-{$post->ID}' class='align image-align-{$name}-label'>$label</label>";
	}

	return implode( "\n", $output );
}

/**
 * Retrieves HTML for the size radio buttons with the specified one checked.
 *
 * @since 2.7.0
 *
 * @param WP_Post     $post
 * @param bool|string $check
 * @return array
 */
function image_size_input_fields( $post, $check = '' ) {
	/**
	 * Filters the names and labels of the default image sizes.
	 *
	 * @since 3.3.0
	 *
	 * @param string[] $size_names Array of image size labels keyed by their name. Default values
	 *                             include 'Thumbnail', 'Medium', 'Large', and 'Full Size'.
	 */
	$size_names = apply_filters(
		'image_size_names_choose',
		array(
			'thumbnail' => __( 'Thumbnail' ),
			'medium'    => __( 'Medium' ),
			'large'     => __( 'Large' ),
			'full'      => __( 'Full Size' ),
		)
	);

	if ( empty( $check ) ) {
		$check = get_user_setting( 'imgsize', 'medium' );
	}

	$output = array();

	foreach ( $size_names as $size => $label ) {
		$downsize = image_downsize( $post->ID, $size );
		$checked  = '';

		// Is this size selectable?
		$enabled = ( $downsize[3] || 'full' === $size );
		$css_id  = "image-size-{$size}-{$post->ID}";

		// If this size is the default but that's not available, don't select it.
		if ( $size === $check ) {
			if ( $enabled ) {
				$checked = " checked='checked'";
			} else {
				$check = '';
			}
		} elseif ( ! $check && $enabled && 'thumbnail' !== $size ) {
			/*
			 * If $check is not enabled, default to the first available size
			 * that's bigger than a thumbnail.
			 */
			$check   = $size;
			$checked = " checked='checked'";
		}

		$html = "<div class='image-size-item'><input type='radio' " . disabled( $enabled, false, false ) . "name='attachments[$post->ID][image-size]' id='{$css_id}' value='{$size}'$checked />";

		$html .= "<label for='{$css_id}'>$label</label>";

		// Only show the dimensions if that choice is available.
		if ( $enabled ) {
			$html .= " <label for='{$css_id}' class='help'>" . sprintf( '(%d&nbsp;&times;&nbsp;%d)', $downsize[1], $downsize[2] ) . '</label>';
		}
		$html .= '</div>';

		$output[] = $html;
	}

	return array(
		'label' => __( 'Size' ),
		'input' => 'html',
		'html'  => implode( "\n", $output ),
	);
}

/**
 * Retrieves HTML for the Link URL buttons with the default link type as specified.
 *
 * @since 2.7.0
 *
 * @param WP_Post $post
 * @param string  $url_type
 * @return string
 */
function image_link_input_fields( $post, $url_type = '' ) {

	$file = wp_get_attachment_url( $post->ID );
	$link = get_attachment_link( $post->ID );

	if ( empty( $url_type ) ) {
		$url_type = get_user_setting( 'urlbutton', 'post' );
	}

	$url = '';

	if ( 'file' === $url_type ) {
		$url = $file;
	} elseif ( 'post' === $url_type ) {
		$url = $link;
	}

	return "
	<input type='text' class='text urlfield' name='attachments[$post->ID][url]' value='" . esc_attr( $url ) . "' /><br />
	<button type='button' class='button urlnone' data-link-url=''>" . __( 'None' ) . "</button>
	<button type='button' class='button urlfile' data-link-url='" . esc_url( $file ) . "'>" . __( 'File URL' ) . "</button>
	<button type='button' class='button urlpost' data-link-url='" . esc_url( $link ) . "'>" . __( 'Attachment Post URL' ) . '</button>
';
}

/**
 * Outputs a textarea element for inputting an attachment caption.
 *
 * @since 3.4.0
 *
 * @param WP_Post $edit_post Attachment WP_Post object.
 * @return string HTML markup for the textarea element.
 */
function wp_caption_input_textarea( $edit_post ) {
	// Post data is already escaped.
	$name = "attachments[{$edit_post->ID}][post_excerpt]";

	return '<textarea name="' . $name . '" id="' . $name . '">' . $edit_post->post_excerpt . '</textarea>';
}

/**
 * Retrieves the image attachment fields to edit form fields.
 *
 * @since 2.5.0
 *
 * @param array  $form_fields
 * @param object $post
 * @return array
 */
function image_attachment_fields_to_edit( $form_fields, $post ) {
	return $form_fields;
}

/**
 * Retrieves the single non-image attachment fields to edit form fields.
 *
 * @since 2.5.0
 *
 * @param array   $form_fields An array of attachment form fields.
 * @param WP_Post $post        The WP_Post attachment object.
 * @return array Filtered attachment form fields.
 */
function media_single_attachment_fields_to_edit( $form_fields, $post ) {
	unset( $form_fields['url'], $form_fields['align'], $form_fields['image-size'] );
	return $form_fields;
}

/**
 * Retrieves the post non-image attachment fields to edit form fields.
 *
 * @since 2.8.0
 *
 * @param array   $form_fields An array of attachment form fields.
 * @param WP_Post $post        The WP_Post attachment object.
 * @return array Filtered attachment form fields.
 */
function media_post_single_attachment_fields_to_edit( $form_fields, $post ) {
	unset( $form_fields['image_url'] );
	return $form_fields;
}

/**
 * Retrieves the media element HTML to send to the editor.
 *
 * @since 2.5.0
 *
 * @param string  $html
 * @param int     $attachment_id
 * @param array   $attachment
 * @return string
 */
function image_media_send_to_editor( $html, $attachment_id, $attachment ) {
	$post = get_post( $attachment_id );

	if ( str_starts_with( $post->post_mime_type, 'image' ) ) {
		$url   = $attachment['url'];
		$align = ! empty( $attachment['align'] ) ? $attachment['align'] : 'none';
		$size  = ! empty( $attachment['image-size'] ) ? $attachment['image-size'] : 'medium';
		$alt   = ! empty( $attachment['image_alt'] ) ? $attachment['image_alt'] : '';
		$rel   = ( str_contains( $url, 'attachment_id' ) || get_attachment_link( $attachment_id ) === $url );

		return get_image_send_to_editor( $attachment_id, $attachment['post_excerpt'], $attachment['post_title'], $align, $url, $rel, $size, $alt );
	}

	return $html;
}

/**
 * Retrieves the attachment fields to edit form fields.
 *
 * @since 2.5.0
 *
 * @param WP_Post $post
 * @param array   $errors
 * @return array
 */
function get_attachment_fields_to_edit( $post, $errors = null ) {
	if ( is_int( $post ) ) {
		$post = get_post( $post );
	}

	if ( is_array( $post ) ) {
		$post = new WP_Post( (object) $post );
	}

	$image_url = wp_get_attachment_url( $post->ID );

	$edit_post = sanitize_post( $post, 'edit' );

	$form_fields = array(
		'post_title'   => array(
			'label' => __( 'Title' ),
			'value' => $edit_post->post_title,
		),
		'image_alt'    => array(),
		'post_excerpt' => array(
			'label' => __( 'Caption' ),
			'input' => 'html',
			'html'  => wp_caption_input_textarea( $edit_post ),
		),
		'post_content' => array(
			'label' => __( 'Description' ),
			'value' => $edit_post->post_content,
			'input' => 'textarea',
		),
		'url'          => array(
			'label' => __( 'Link URL' ),
			'input' => 'html',
			'html'  => image_link_input_fields( $post, get_option( 'image_default_link_type' ) ),
			'helps' => __( 'Enter a link URL or click above for presets.' ),
		),
		'menu_order'   => array(
			'label' => __( 'Order' ),
			'value' => $edit_post->menu_order,
		),
		'image_url'    => array(
			'label' => __( 'File URL' ),
			'input' => 'html',
			'html'  => "<input type='text' class='text urlfield' readonly='readonly' name='attachments[$post->ID][url]' value='" . esc_attr( $image_url ) . "' /><br />",
			'value' => wp_get_attachment_url( $post->ID ),
			'helps' => __( 'Location of the uploaded file.' ),
		),
	);

	foreach ( get_attachment_taxonomies( $post ) as $taxonomy ) {
		$t = (array) get_taxonomy( $taxonomy );

		if ( ! $t['public'] || ! $t['show_ui'] ) {
			continue;
		}

		if ( empty( $t['label'] ) ) {
			$t['label'] = $taxonomy;
		}

		if ( empty( $t['args'] ) ) {
			$t['args'] = array();
		}

		$terms = get_object_term_cache( $post->ID, $taxonomy );

		if ( false === $terms ) {
			$terms = wp_get_object_terms( $post->ID, $taxonomy, $t['args'] );
		}

		$values = array();

		foreach ( $terms as $term ) {
			$values[] = $term->slug;
		}

		$t['value'] = implode( ', ', $values );

		$form_fields[ $taxonomy ] = $t;
	}

	/*
	 * Merge default fields with their errors, so any key passed with the error
	 * (e.g. 'error', 'helps', 'value') will replace the default.
	 * The recursive merge is easily traversed with array casting:
	 * foreach ( (array) $things as $thing )
	 */
	$form_fields = array_merge_recursive( $form_fields, (array) $errors );

	// This was formerly in image_attachment_fields_to_edit().
	if ( str_starts_with( $post->post_mime_type, 'image' ) ) {
		$alt = get_post_meta( $post->ID, '_wp_attachment_image_alt', true );

		if ( empty( $alt ) ) {
			$alt = '';
		}

		$form_fields['post_title']['required'] = true;

		$form_fields['image_alt'] = array(
			'value' => $alt,
			'label' => __( 'Alternative Text' ),
			'helps' => __( 'Alt text for the image, e.g. &#8220;The Mona Lisa&#8221;' ),
		);

		$form_fields['align'] = array(
			'label' => __( 'Alignment' ),
			'input' => 'html',
			'html'  => image_align_input_fields( $post, get_option( 'image_default_align' ) ),
		);

		$form_fields['image-size'] = image_size_input_fields( $post, get_option( 'image_default_size', 'medium' ) );

	} else {
		unset( $form_fields['image_alt'] );
	}

	/**
	 * Filters the attachment fields to edit.
	 *
	 * @since 2.5.0
	 *
	 * @param array   $form_fields An array of attachment form fields.
	 * @param WP_Post $post        The WP_Post attachment object.
	 */
	$form_fields = apply_filters( 'attachment_fields_to_edit', $form_fields, $post );

	return $form_fields;
}

/**
 * Retrieves HTML for media items of post gallery.
 *
 * The HTML markup retrieved will be created for the progress of SWF Upload
 * component. Will also create link for showing and hiding the form to modify
 * the image attachment.
 *
 * @since 2.5.0
 *
 * @global WP_Query $wp_the_query WordPress Query object.
 *
 * @param int   $post_id Post ID.
 * @param array $errors  Errors for attachment, if any.
 * @return string HTML content for media items of post gallery.
 */
function get_media_items( $post_id, $errors ) {
	$attachments = array();

	if ( $post_id ) {
		$post = get_post( $post_id );

		if ( $post && 'attachment' === $post->post_type ) {
			$attachments = array( $post->ID => $post );
		} else {
			$attachments = get_children(
				array(
					'post_parent' => $post_id,
					'post_type'   => 'attachment',
					'orderby'     => 'menu_order ASC, ID',
					'order'       => 'DESC',
				)
			);
		}
	} else {
		if ( is_array( $GLOBALS['wp_the_query']->posts ) ) {
			foreach ( $GLOBALS['wp_the_query']->posts as $attachment ) {
				$attachments[ $attachment->ID ] = $attachment;
			}
		}
	}

	$output = '';
	foreach ( (array) $attachments as $id => $attachment ) {
		if ( 'trash' === $attachment->post_status ) {
			continue;
		}

		$item = get_media_item( $id, array( 'errors' => isset( $errors[ $id ] ) ? $errors[ $id ] : null ) );

		if ( $item ) {
			$output .= "\n<div id='media-item-$id' class='media-item child-of-$attachment->post_parent preloaded'><div class='progress hidden'><div class='bar'></div></div><div id='media-upload-error-$id' class='hidden'></div><div class='filename hidden'></div>$item\n</div>";
		}
	}

	return $output;
}

/**
 * Retrieves HTML form for modifying the image attachment.
 *
 * @since 2.5.0
 *
 * @global string $redir_tab
 *
 * @param int          $attachment_id Attachment ID for modification.
 * @param string|array $args          Optional. Override defaults.
 * @return string HTML form for attachment.
 */
function get_media_item( $attachment_id, $args = null ) {
	global $redir_tab;

	$thumb_url     = false;
	$attachment_id = (int) $attachment_id;

	if ( $attachment_id ) {
		$thumb_url = wp_get_attachment_image_src( $attachment_id, 'thumbnail', true );

		if ( $thumb_url ) {
			$thumb_url = $thumb_url[0];
		}
	}

	$post            = get_post( $attachment_id );
	$current_post_id = ! empty( $_GET['post_id'] ) ? (int) $_GET['post_id'] : 0;

	$default_args = array(
		'errors'     => null,
		'send'       => $current_post_id ? post_type_supports( get_post_type( $current_post_id ), 'editor' ) : true,
		'delete'     => true,
		'toggle'     => true,
		'show_title' => true,
	);

	$parsed_args = wp_parse_args( $args, $default_args );

	/**
	 * Filters the arguments used to retrieve an image for the edit image form.
	 *
	 * @since 3.1.0
	 *
	 * @see get_media_item
	 *
	 * @param array $parsed_args An array of arguments.
	 */
	$parsed_args = apply_filters( 'get_media_item_args', $parsed_args );

	$toggle_on  = __( 'Show' );
	$toggle_off = __( 'Hide' );

	$file     = get_attached_file( $post->ID );
	$filename = esc_html( wp_basename( $file ) );
	$title    = esc_attr( $post->post_title );

	$post_mime_types = get_post_mime_types();
	$keys            = array_keys( wp_match_mime_types( array_keys( $post_mime_types ), $post->post_mime_type ) );
	$type            = reset( $keys );
	$type_html       = "<input type='hidden' id='type-of-$attachment_id' value='" . esc_attr( $type ) . "' />";

	$form_fields = get_attachment_fields_to_edit( $post, $parsed_args['errors'] );

	if ( $parsed_args['toggle'] ) {
		$class        = empty( $parsed_args['errors'] ) ? 'startclosed' : 'startopen';
		$toggle_links = "
		<a class='toggle describe-toggle-on' href='#'>$toggle_on</a>
		<a class='toggle describe-toggle-off' href='#'>$toggle_off</a>";
	} else {
		$class        = '';
		$toggle_links = '';
	}

	$display_title = ( ! empty( $title ) ) ? $title : $filename; // $title shouldn't ever be empty, but just in case.
	$display_title = $parsed_args['show_title'] ? "<div class='filename new'><span class='title'>" . wp_html_excerpt( $display_title, 60, '&hellip;' ) . '</span></div>' : '';

	$gallery = ( ( isset( $_REQUEST['tab'] ) && 'gallery' === $_REQUEST['tab'] ) || ( isset( $redir_tab ) && 'gallery' === $redir_tab ) );
	$order   = '';

	foreach ( $form_fields as $key => $val ) {
		if ( 'menu_order' === $key ) {
			if ( $gallery ) {
				$order = "<div class='menu_order'> <input class='menu_order_input' type='text' id='attachments[$attachment_id][menu_order]' name='attachments[$attachment_id][menu_order]' value='" . esc_attr( $val['value'] ) . "' /></div>";
			} else {
				$order = "<input type='hidden' name='attachments[$attachment_id][menu_order]' value='" . esc_attr( $val['value'] ) . "' />";
			}

			unset( $form_fields['menu_order'] );
			break;
		}
	}

	$media_dims = '';
	$meta       = wp_get_attachment_metadata( $post->ID );

	if ( isset( $meta['width'], $meta['height'] ) ) {
		/* translators: 1: A number of pixels wide, 2: A number of pixels tall. */
		$media_dims .= "<span id='media-dims-$post->ID'>" . sprintf( __( '%1$s by %2$s pixels' ), $meta['width'], $meta['height'] ) . '</span>';
	}

	/**
	 * Filters the media metadata.
	 *
	 * @since 2.5.0
	 *
	 * @param string  $media_dims The HTML markup containing the media dimensions.
	 * @param WP_Post $post       The WP_Post attachment object.
	 */
	$media_dims = apply_filters( 'media_meta', $media_dims, $post );

	$image_edit_button = '';

	if ( wp_attachment_is_image( $post->ID ) && wp_image_editor_supports( array( 'mime_type' => $post->post_mime_type ) ) ) {
		$nonce             = wp_create_nonce( "image_editor-$post->ID" );
		$image_edit_button = "<input type='button' id='imgedit-open-btn-$post->ID' onclick='imageEdit.open( $post->ID, \"$nonce\" )' class='button' value='" . esc_attr__( 'Edit Image' ) . "' /> <span class='spinner'></span>";
	}

	$attachment_url = get_permalink( $attachment_id );

	$item = "
		$type_html
		$toggle_links
		$order
		$display_title
		<table class='slidetoggle describe $class'>
			<thead class='media-item-info' id='media-head-$post->ID'>
			<tr>
			<td class='A1B1' id='thumbnail-head-$post->ID'>
			<p><a href='$attachment_url' target='_blank'><img class='thumbnail' src='$thumb_url' alt='' /></a></p>
			<p>$image_edit_button</p>
			</td>
			<td>
			<p><strong>" . __( 'File name:' ) . "</strong> $filename</p>
			<p><strong>" . __( 'File type:' ) . "</strong> $post->post_mime_type</p>
			<p><strong>" . __( 'Upload date:' ) . '</strong> ' . mysql2date( __( 'F j, Y' ), $post->post_date ) . '</p>';

	if ( ! empty( $media_dims ) ) {
		$item .= '<p><strong>' . __( 'Dimensions:' ) . "</strong> $media_dims</p>\n";
	}

	$item .= "</td></tr>\n";

	$item .= "
		</thead>
		<tbody>
		<tr><td colspan='2' class='imgedit-response' id='imgedit-response-$post->ID'></td></tr>\n
		<tr><td style='display:none' colspan='2' class='image-editor' id='image-editor-$post->ID'></td></tr>\n
		<tr><td colspan='2'><p class='media-types media-types-required-info'>" .
			wp_required_field_message() .
		"</p></td></tr>\n";

	$defaults = array(
		'input'      => 'text',
		'required'   => false,
		'value'      => '',
		'extra_rows' => array(),
	);

	if ( $parsed_args['send'] ) {
		$parsed_args['send'] = get_submit_button( __( 'Insert into Post' ), '', "send[$attachment_id]", false );
	}

	$delete = empty( $parsed_args['delete'] ) ? '' : $parsed_args['delete'];
	if ( $delete && current_user_can( 'delete_post', $attachment_id ) ) {
		if ( ! EMPTY_TRASH_DAYS ) {
			$delete = "<a href='" . wp_nonce_url( "post.php?action=delete&amp;post=$attachment_id", 'delete-post_' . $attachment_id ) . "' id='del[$attachment_id]' class='delete-permanently'>" . __( 'Delete Permanently' ) . '</a>';
		} elseif ( ! MEDIA_TRASH ) {
			$delete = "<a href='#' class='del-link' onclick=\"document.getElementById('del_attachment_$attachment_id').style.display='block';return false;\">" . __( 'Delete' ) . "</a>
				<div id='del_attachment_$attachment_id' class='del-attachment' style='display:none;'>" .
				/* translators: %s: File name. */
				'<p>' . sprintf( __( 'You are about to delete %s.' ), '<strong>' . $filename . '</strong>' ) . "</p>
				<a href='" . wp_nonce_url( "post.php?action=delete&amp;post=$attachment_id", 'delete-post_' . $attachment_id ) . "' id='del[$attachment_id]' class='button'>" . __( 'Continue' ) . "</a>
				<a href='#' class='button' onclick=\"this.parentNode.style.display='none';return false;\">" . __( 'Cancel' ) . '</a>
				</div>';
		} else {
			$delete = "<a href='" . wp_nonce_url( "post.php?action=trash&amp;post=$attachment_id", 'trash-post_' . $attachment_id ) . "' id='del[$attachment_id]' class='delete'>" . __( 'Move to Trash' ) . "</a>
			<a href='" . wp_nonce_url( "post.php?action=untrash&amp;post=$attachment_id", 'untrash-post_' . $attachment_id ) . "' id='undo[$attachment_id]' class='undo hidden'>" . __( 'Undo' ) . '</a>';
		}
	} else {
		$delete = '';
	}

	$thumbnail       = '';
	$calling_post_id = 0;

	if ( isset( $_GET['post_id'] ) ) {
		$calling_post_id = absint( $_GET['post_id'] );
	} elseif ( isset( $_POST ) && count( $_POST ) ) {// Like for async-upload where $_GET['post_id'] isn't set.
		$calling_post_id = $post->post_parent;
	}

	if ( 'image' === $type && $calling_post_id
		&& current_theme_supports( 'post-thumbnails', get_post_type( $calling_post_id ) )
		&& post_type_supports( get_post_type( $calling_post_id ), 'thumbnail' )
		&& get_post_thumbnail_id( $calling_post_id ) !== $attachment_id
	) {

		$calling_post             = get_post( $calling_post_id );
		$calling_post_type_object = get_post_type_object( $calling_post->post_type );

		$ajax_nonce = wp_create_nonce( "set_post_thumbnail-$calling_post_id" );
		$thumbnail  = "<a class='wp-post-thumbnail' id='wp-post-thumbnail-" . $attachment_id . "' href='#' onclick='WPSetAsThumbnail(\"$attachment_id\", \"$ajax_nonce\");return false;'>" . esc_html( $calling_post_type_object->labels->use_featured_image ) . '</a>';
	}

	if ( ( $parsed_args['send'] || $thumbnail || $delete ) && ! isset( $form_fields['buttons'] ) ) {
		$form_fields['buttons'] = array( 'tr' => "\t\t<tr class='submit'><td></td><td class='savesend'>" . $parsed_args['send'] . " $thumbnail $delete</td></tr>\n" );
	}

	$hidden_fields = array();

	foreach ( $form_fields as $id => $field ) {
		if ( '_' === $id[0] ) {
			continue;
		}

		if ( ! empty( $field['tr'] ) ) {
			$item .= $field['tr'];
			continue;
		}

		$field = array_merge( $defaults, $field );
		$name  = "attachments[$attachment_id][$id]";

		if ( 'hidden' === $field['input'] ) {
			$hidden_fields[ $name ] = $field['value'];
			continue;
		}

		$required      = $field['required'] ? ' ' . wp_required_field_indicator() : '';
		$required_attr = $field['required'] ? ' required' : '';
		$class         = $id;
		$class        .= $field['required'] ? ' form-required' : '';

		$item .= "\t\t<tr class='$class'>\n\t\t\t<th scope='row' class='label'><label for='$name'><span class='alignleft'>{$field['label']}{$required}</span><br class='clear' /></label></th>\n\t\t\t<td class='field'>";

		if ( ! empty( $field[ $field['input'] ] ) ) {
			$item .= $field[ $field['input'] ];
		} elseif ( 'textarea' === $field['input'] ) {
			if ( 'post_content' === $id && user_can_richedit() ) {
				// Sanitize_post() skips the post_content when user_can_richedit.
				$field['value'] = htmlspecialchars( $field['value'], ENT_QUOTES );
			}
			// Post_excerpt is already escaped by sanitize_post() in get_attachment_fields_to_edit().
			$item .= "<textarea id='$name' name='$name'{$required_attr}>" . $field['value'] . '</textarea>';
		} else {
			$item .= "<input type='text' class='text' id='$name' name='$name' value='" . esc_attr( $field['value'] ) . "'{$required_attr} />";
		}

		if ( ! empty( $field['helps'] ) ) {
			$item .= "<p class='help'>" . implode( "</p>\n<p class='help'>", array_unique( (array) $field['helps'] ) ) . '</p>';
		}
		$item .= "</td>\n\t\t</tr>\n";

		$extra_rows = array();

		if ( ! empty( $field['errors'] ) ) {
			foreach ( array_unique( (array) $field['errors'] ) as $error ) {
				$extra_rows['error'][] = $error;
			}
		}

		if ( ! empty( $field['extra_rows'] ) ) {
			foreach ( $field['extra_rows'] as $class => $rows ) {
				foreach ( (array) $rows as $html ) {
					$extra_rows[ $class ][] = $html;
				}
			}
		}

		foreach ( $extra_rows as $class => $rows ) {
			foreach ( $rows as $html ) {
				$item .= "\t\t<tr><td></td><td class='$class'>$html</td></tr>\n";
			}
		}
	}

	if ( ! empty( $form_fields['_final'] ) ) {
		$item .= "\t\t<tr class='final'><td colspan='2'>{$form_fields['_final']}</td></tr>\n";
	}

	$item .= "\t</tbody>\n";
	$item .= "\t</table>\n";

	foreach ( $hidden_fields as $name => $value ) {
		$item .= "\t<input type='hidden' name='$name' id='$name' value='" . esc_attr( $value ) . "' />\n";
	}

	if ( $post->post_parent < 1 && isset( $_REQUEST['post_id'] ) ) {
		$parent      = (int) $_REQUEST['post_id'];
		$parent_name = "attachments[$attachment_id][post_parent]";
		$item       .= "\t<input type='hidden' name='$parent_name' id='$parent_name' value='$parent' />\n";
	}

	return $item;
}

/**
 * @since 3.5.0
 *
 * @param int   $attachment_id
 * @param array $args
 * @return array
 */
function get_compat_media_markup( $attachment_id, $args = null ) {
	$post = get_post( $attachment_id );

	$default_args = array(
		'errors'   => null,
		'in_modal' => false,
	);

	$user_can_edit = current_user_can( 'edit_post', $attachment_id );

	$args = wp_parse_args( $args, $default_args );

	/** This filter is documented in wp-admin/includes/media.php */
	$args = apply_filters( 'get_media_item_args', $args );

	$form_fields = array();

	if ( $args['in_modal'] ) {
		foreach ( get_attachment_taxonomies( $post ) as $taxonomy ) {
			$t = (array) get_taxonomy( $taxonomy );

			if ( ! $t['public'] || ! $t['show_ui'] ) {
				continue;
			}

			if ( empty( $t['label'] ) ) {
				$t['label'] = $taxonomy;
			}

			if ( empty( $t['args'] ) ) {
				$t['args'] = array();
			}

			$terms = get_object_term_cache( $post->ID, $taxonomy );

			if ( false === $terms ) {
				$terms = wp_get_object_terms( $post->ID, $taxonomy, $t['args'] );
			}

			$values = array();

			foreach ( $terms as $term ) {
				$values[] = $term->slug;
			}

			$t['value']    = implode( ', ', $values );
			$t['taxonomy'] = true;

			$form_fields[ $taxonomy ] = $t;
		}
	}

	/*
	 * Merge default fields with their errors, so any key passed with the error
	 * (e.g. 'error', 'helps', 'value') will replace the default.
	 * The recursive merge is easily traversed with array casting:
	 * foreach ( (array) $things as $thing )
	 */
	$form_fields = array_merge_recursive( $form_fields, (array) $args['errors'] );

	/** This filter is documented in wp-admin/includes/media.php */
	$form_fields = apply_filters( 'attachment_fields_to_edit', $form_fields, $post );

	unset(
		$form_fields['image-size'],
		$form_fields['align'],
		$form_fields['image_alt'],
		$form_fields['post_title'],
		$form_fields['post_excerpt'],
		$form_fields['post_content'],
		$form_fields['url'],
		$form_fields['menu_order'],
		$form_fields['image_url']
	);

	/** This filter is documented in wp-admin/includes/media.php */
	$media_meta = apply_filters( 'media_meta', '', $post );

	$defaults = array(
		'input'         => 'text',
		'required'      => false,
		'value'         => '',
		'extra_rows'    => array(),
		'show_in_edit'  => true,
		'show_in_modal' => true,
	);

	$hidden_fields = array();

	$item = '';

	foreach ( $form_fields as $id => $field ) {
		if ( '_' === $id[0] ) {
			continue;
		}

		$name    = "attachments[$attachment_id][$id]";
		$id_attr = "attachments-$attachment_id-$id";

		if ( ! empty( $field['tr'] ) ) {
			$item .= $field['tr'];
			continue;
		}

		$field = array_merge( $defaults, $field );

		if ( ( ! $field['show_in_edit'] && ! $args['in_modal'] ) || ( ! $field['show_in_modal'] && $args['in_modal'] ) ) {
			continue;
		}

		if ( 'hidden' === $field['input'] ) {
			$hidden_fields[ $name ] = $field['value'];
			continue;
		}

		$readonly      = ! $user_can_edit && ! empty( $field['taxonomy'] ) ? " readonly='readonly' " : '';
		$required      = $field['required'] ? ' ' . wp_required_field_indicator() : '';
		$required_attr = $field['required'] ? ' required' : '';
		$class         = 'compat-field-' . $id;
		$class        .= $field['required'] ? ' form-required' : '';

		$item .= "\t\t<tr class='$class'>";
		$item .= "\t\t\t<th scope='row' class='label'><label for='$id_attr'><span class='alignleft'>{$field['label']}</span>$required<br class='clear' /></label>";
		$item .= "</th>\n\t\t\t<td class='field'>";

		if ( ! empty( $field[ $field['input'] ] ) ) {
			$item .= $field[ $field['input'] ];
		} elseif ( 'textarea' === $field['input'] ) {
			if ( 'post_content' === $id && user_can_richedit() ) {
				// sanitize_post() skips the post_content when user_can_richedit.
				$field['value'] = htmlspecialchars( $field['value'], ENT_QUOTES );
			}
			$item .= "<textarea id='$id_attr' name='$name'{$required_attr}>" . $field['value'] . '</textarea>';
		} else {
			$item .= "<input type='text' class='text' id='$id_attr' name='$name' value='" . esc_attr( $field['value'] ) . "' $readonly{$required_attr} />";
		}

		if ( ! empty( $field['helps'] ) ) {
			$item .= "<p class='help'>" . implode( "</p>\n<p class='help'>", array_unique( (array) $field['helps'] ) ) . '</p>';
		}

		$item .= "</td>\n\t\t</tr>\n";

		$extra_rows = array();

		if ( ! empty( $field['errors'] ) ) {
			foreach ( array_unique( (array) $field['errors'] ) as $error ) {
				$extra_rows['error'][] = $error;
			}
		}

		if ( ! empty( $field['extra_rows'] ) ) {
			foreach ( $field['extra_rows'] as $class => $rows ) {
				foreach ( (array) $rows as $html ) {
					$extra_rows[ $class ][] = $html;
				}
			}
		}

		foreach ( $extra_rows as $class => $rows ) {
			foreach ( $rows as $html ) {
				$item .= "\t\t<tr><td></td><td class='$class'>$html</td></tr>\n";
			}
		}
	}

	if ( ! empty( $form_fields['_final'] ) ) {
		$item .= "\t\t<tr class='final'><td colspan='2'>{$form_fields['_final']}</td></tr>\n";
	}

	if ( $item ) {
		$item = '<p class="media-types media-types-required-info">' .
			wp_required_field_message() .
			'</p>' .
			'<table class="compat-attachment-fields">' . $item . '</table>';
	}

	foreach ( $hidden_fields as $hidden_field => $value ) {
		$item .= '<input type="hidden" name="' . esc_attr( $hidden_field ) . '" value="' . esc_attr( $value ) . '" />' . "\n";
	}

	if ( $item ) {
		$item = '<input type="hidden" name="attachments[' . $attachment_id . '][menu_order]" value="' . esc_attr( $post->menu_order ) . '" />' . $item;
	}

	return array(
		'item' => $item,
		'meta' => $media_meta,
	);
}

/**
 * Outputs the legacy media upload header.
 *
 * @since 2.5.0
 */
function media_upload_header() {
	$post_id = isset( $_REQUEST['post_id'] ) ? (int) $_REQUEST['post_id'] : 0;

	echo '<script type="text/javascript">post_id = ' . $post_id . ';</script>';

	if ( empty( $_GET['chromeless'] ) ) {
		echo '<div id="media-upload-header">';
		the_media_upload_tabs();
		echo '</div>';
	}
}

/**
 * Outputs the legacy media upload form.
 *
 * @since 2.5.0
 *
 * @global string $type
 * @global string $tab
 *
 * @param array $errors
 */
function media_upload_form( $errors = null ) {
	global $type, $tab;

	if ( ! _device_can_upload() ) {
		echo '<p>' . sprintf(
			/* translators: %s: https://apps.wordpress.org/ */
			__( 'The web browser on your device cannot be used to upload files. You may be able to use the <a href="%s">native app for your device</a> instead.' ),
			'https://apps.wordpress.org/'
		) . '</p>';
		return;
	}

	$upload_action_url = admin_url( 'async-upload.php' );
	$post_id           = isset( $_REQUEST['post_id'] ) ? (int) $_REQUEST['post_id'] : 0;
	$_type             = isset( $type ) ? $type : '';
	$_tab              = isset( $tab ) ? $tab : '';

	$max_upload_size = wp_max_upload_size();
	if ( ! $max_upload_size ) {
		$max_upload_size = 0;
	}

	?>
	<div id="media-upload-notice">
	<?php

	if ( isset( $errors['upload_notice'] ) ) {
		echo $errors['upload_notice'];
	}

	?>
	</div>
	<div id="media-upload-error">
	<?php

	if ( isset( $errors['upload_error'] ) && is_wp_error( $errors['upload_error'] ) ) {
		echo $errors['upload_error']->get_error_message();
	}

	?>
	</div>
	<?php

	if ( is_multisite() && ! is_upload_space_available() ) {
		/**
		 * Fires when an upload will exceed the defined upload space quota for a network site.
		 *
		 * @since 3.5.0
		 */
		do_action( 'upload_ui_over_quota' );
		return;
	}

	/**
	 * Fires just before the legacy (pre-3.5.0) upload interface is loaded.
	 *
	 * @since 2.6.0
	 */
	do_action( 'pre-upload-ui' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores

	$post_params = array(
		'post_id'  => $post_id,
		'_wpnonce' => wp_create_nonce( 'media-form' ),
		'type'     => $_type,
		'tab'      => $_tab,
		'short'    => '1',
	);

	/**
	 * Filters the media upload post parameters.
	 *
	 * @since 3.1.0 As 'swfupload_post_params'
	 * @since 3.3.0
	 *
	 * @param array $post_params An array of media upload parameters used by Plupload.
	 */
	$post_params = apply_filters( 'upload_post_params', $post_params );

	/*
	* Since 4.9 the `runtimes` setting is hardcoded in our version of Plupload to `html5,html4`,
	* and the `flash_swf_url` and `silverlight_xap_url` are not used.
	*/
	$plupload_init = array(
		'browse_button'    => 'plupload-browse-button',
		'container'        => 'plupload-upload-ui',
		'drop_element'     => 'drag-drop-area',
		'file_data_name'   => 'async-upload',
		'url'              => $upload_action_url,
		'filters'          => array( 'max_file_size' => $max_upload_size . 'b' ),
		'multipart_params' => $post_params,
	);

	/*
	 * Currently only iOS Safari supports multiple files uploading,
	 * but iOS 7.x has a bug that prevents uploading of videos when enabled.
	 * See #29602.
	 */
	if (
		wp_is_mobile() &&
		str_contains( $_SERVER['HTTP_USER_AGENT'], 'OS 7_' ) &&
		str_contains( $_SERVER['HTTP_USER_AGENT'], 'like Mac OS X' )
	) {
		$plupload_init['multi_selection'] = false;
	}

	/** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php */
	$prevent_unsupported_uploads = apply_filters( 'wp_prevent_unsupported_mime_type_uploads', true, null );

	if ( $prevent_unsupported_uploads ) {
		// Check if WebP images can be edited.
		if ( ! wp_image_editor_supports( array( 'mime_type' => 'image/webp' ) ) ) {
			$plupload_init['webp_upload_error'] = true;
		}

		// Check if AVIF images can be edited.
		if ( ! wp_image_editor_supports( array( 'mime_type' => 'image/avif' ) ) ) {
			$plupload_init['avif_upload_error'] = true;
		}
	}

	/**
	 * Filters the default Plupload settings.
	 *
	 * @since 3.3.0
	 *
	 * @param array $plupload_init An array of default settings used by Plupload.
	 */
	$plupload_init = apply_filters( 'plupload_init', $plupload_init );

	?>
	<script type="text/javascript">
	<?php
	// Verify size is an int. If not return default value.
	$large_size_h = absint( get_option( 'large_size_h' ) );

	if ( ! $large_size_h ) {
		$large_size_h = 1024;
	}

	$large_size_w = absint( get_option( 'large_size_w' ) );

	if ( ! $large_size_w ) {
		$large_size_w = 1024;
	}

	?>
	var resize_height = <?php echo $large_size_h; ?>, resize_width = <?php echo $large_size_w; ?>,
	wpUploaderInit = <?php echo wp_json_encode( $plupload_init ); ?>;
	</script>

	<div id="plupload-upload-ui" class="hide-if-no-js">
	<?php
	/**
	 * Fires before the upload interface loads.
	 *
	 * @since 2.6.0 As 'pre-flash-upload-ui'
	 * @since 3.3.0
	 */
	do_action( 'pre-plupload-upload-ui' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores

	?>
	<div id="drag-drop-area">
		<div class="drag-drop-inside">
		<p class="drag-drop-info"><?php _e( 'Drop files to upload' ); ?></p>
		<p><?php _ex( 'or', 'Uploader: Drop files here - or - Select Files' ); ?></p>
		<p class="drag-drop-buttons"><input id="plupload-browse-button" type="button" value="<?php esc_attr_e( 'Select Files' ); ?>" class="button" /></p>
		</div>
	</div>
	<?php
	/**
	 * Fires after the upload interface loads.
	 *
	 * @since 2.6.0 As 'post-flash-upload-ui'
	 * @since 3.3.0
	 */
	do_action( 'post-plupload-upload-ui' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
	?>
	</div>

	<div id="html-upload-ui" class="hide-if-js">
	<?php
	/**
	 * Fires before the upload button in the media upload interface.
	 *
	 * @since 2.6.0
	 */
	do_action( 'pre-html-upload-ui' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores

	?>
	<p id="async-upload-wrap">
		<label class="screen-reader-text" for="async-upload">
			<?php
			/* translators: Hidden accessibility text. */
			_ex( 'Upload', 'verb' );
			?>
		</label>
		<input type="file" name="async-upload" id="async-upload" />
		<?php submit_button( _x( 'Upload', 'verb' ), 'primary', 'html-upload', false ); ?>
		<a href="#" onclick="try{top.tb_remove();}catch(e){}; return false;"><?php _e( 'Cancel' ); ?></a>
	</p>
	<div class="clear"></div>
	<?php
	/**
	 * Fires after the upload button in the media upload interface.
	 *
	 * @since 2.6.0
	 */
	do_action( 'post-html-upload-ui' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores

	?>
	</div>

<p class="max-upload-size">
	<?php
	/* translators: %s: Maximum allowed file size. */
	printf( __( 'Maximum upload file size: %s.' ), esc_html( size_format( $max_upload_size ) ) );
	?>
</p>
	<?php

	/**
	 * Fires on the post upload UI screen.
	 *
	 * Legacy (pre-3.5.0) media workflow hook.
	 *
	 * @since 2.6.0
	 */
	do_action( 'post-upload-ui' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
}

/**
 * Outputs the legacy media upload form for a given media type.
 *
 * @since 2.5.0
 *
 * @param string       $type
 * @param array        $errors
 * @param int|WP_Error $id
 */
function media_upload_type_form( $type = 'file', $errors = null, $id = null ) {

	media_upload_header();

	$post_id = isset( $_REQUEST['post_id'] ) ? (int) $_REQUEST['post_id'] : 0;

	$form_action_url = admin_url( "media-upload.php?type=$type&tab=type&post_id=$post_id" );

	/**
	 * Filters the media upload form action URL.
	 *
	 * @since 2.6.0
	 *
	 * @param string $form_action_url The media upload form action URL.
	 * @param string $type            The type of media. Default 'file'.
	 */
	$form_action_url = apply_filters( 'media_upload_form_url', $form_action_url, $type );
	$form_class      = 'media-upload-form type-form validate';

	if ( get_user_setting( 'uploader' ) ) {
		$form_class .= ' html-uploader';
	}

	?>
	<form enctype="multipart/form-data" method="post" action="<?php echo esc_url( $form_action_url ); ?>" class="<?php echo $form_class; ?>" id="<?php echo $type; ?>-form">
		<?php submit_button( '', 'hidden', 'save', false ); ?>
	<input type="hidden" name="post_id" id="post_id" value="<?php echo (int) $post_id; ?>" />
		<?php wp_nonce_field( 'media-form' ); ?>

	<h3 class="media-title"><?php _e( 'Add media files from your computer' ); ?></h3>

	<?php media_upload_form( $errors ); ?>

	<script type="text/javascript">
	jQuery(function($){
		var preloaded = $(".media-item.preloaded");
		if ( preloaded.length > 0 ) {
			preloaded.each(function(){prepareMediaItem({id:this.id.replace(/[^0-9]/g, '')},'');});
		}
		updateMediaForm();
	});
	</script>
	<div id="media-items">
	<?php

	if ( $id ) {
		if ( ! is_wp_error( $id ) ) {
			add_filter( 'attachment_fields_to_edit', 'media_post_single_attachment_fields_to_edit', 10, 2 );
			echo get_media_items( $id, $errors );
		} else {
			echo '<div id="media-upload-error">' . esc_html( $id->get_error_message() ) . '</div></div>';
			exit;
		}
	}

	?>
	</div>

	<p class="savebutton ml-submit">
		<?php submit_button( __( 'Save all changes' ), '', 'save', false ); ?>
	</p>
	</form>
	<?php
}

/**
 * Outputs the legacy media upload form for external media.
 *
 * @since 2.7.0
 *
 * @param string  $type
 * @param object  $errors
 * @param int     $id
 */
function media_upload_type_url_form( $type = null, $errors = null, $id = null ) {
	if ( null === $type ) {
		$type = 'image';
	}

	media_upload_header();

	$post_id = isset( $_REQUEST['post_id'] ) ? (int) $_REQUEST['post_id'] : 0;

	$form_action_url = admin_url( "media-upload.php?type=$type&tab=type&post_id=$post_id" );
	/** This filter is documented in wp-admin/includes/media.php */
	$form_action_url = apply_filters( 'media_upload_form_url', $form_action_url, $type );
	$form_class      = 'media-upload-form type-form validate';

	if ( get_user_setting( 'uploader' ) ) {
		$form_class .= ' html-uploader';
	}

	?>
	<form enctype="multipart/form-data" method="post" action="<?php echo esc_url( $form_action_url ); ?>" class="<?php echo $form_class; ?>" id="<?php echo $type; ?>-form">
	<input type="hidden" name="post_id" id="post_id" value="<?php echo (int) $post_id; ?>" />
		<?php wp_nonce_field( 'media-form' ); ?>

	<h3 class="media-title"><?php _e( 'Insert media from another website' ); ?></h3>

	<script type="text/javascript">
	var addExtImage = {

	width : '',
	height : '',
	align : 'alignnone',

	insert : function() {
		var t = this, html, f = document.forms[0], cls, title = '', alt = '', caption = '';

		if ( '' === f.src.value || '' === t.width )
			return false;

		if ( f.alt.value )
			alt = f.alt.value.replace(/'/g, '&#039;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');

		<?php
		/** This filter is documented in wp-admin/includes/media.php */
		if ( ! apply_filters( 'disable_captions', '' ) ) {
			?>
			if ( f.caption.value ) {
				caption = f.caption.value.replace(/\r\n|\r/g, '\n');
				caption = caption.replace(/<[a-zA-Z0-9]+( [^<>]+)?>/g, function(a){
					return a.replace(/[\r\n\t]+/, ' ');
				});

				caption = caption.replace(/\s*\n\s*/g, '<br />');
			}
			<?php
		}

		?>
		cls = caption ? '' : ' class="'+t.align+'"';

		html = '<img alt="'+alt+'" src="'+f.src.value+'"'+cls+' width="'+t.width+'" height="'+t.height+'" />';

		if ( f.url.value ) {
			url = f.url.value.replace(/'/g, '&#039;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
			html = '<a href="'+url+'">'+html+'</a>';
		}

		if ( caption )
			html = '[caption id="" align="'+t.align+'" width="'+t.width+'"]'+html+caption+'[/caption]';

		var win = window.dialogArguments || opener || parent || top;
		win.send_to_editor(html);
		return false;
	},

	resetImageData : function() {
		var t = addExtImage;

		t.width = t.height = '';
		document.getElementById('go_button').style.color = '#bbb';
		if ( ! document.forms[0].src.value )
			document.getElementById('status_img').innerHTML = '';
		else document.getElementById('status_img').innerHTML = '<img src="<?php echo esc_url( admin_url( 'images/no.png' ) ); ?>" alt="" />';
	},

	updateImageData : function() {
		var t = addExtImage;

		t.width = t.preloadImg.width;
		t.height = t.preloadImg.height;
		document.getElementById('go_button').style.color = '#333';
		document.getElementById('status_img').innerHTML = '<img src="<?php echo esc_url( admin_url( 'images/yes.png' ) ); ?>" alt="" />';
	},

	getImageData : function() {
		if ( jQuery('table.describe').hasClass('not-image') )
			return;

		var t = addExtImage, src = document.forms[0].src.value;

		if ( ! src ) {
			t.resetImageData();
			return false;
		}

		document.getElementById('status_img').innerHTML = '<img src="<?php echo esc_url( admin_url( 'images/spinner-2x.gif' ) ); ?>" alt="" width="16" height="16" />';
		t.preloadImg = new Image();
		t.preloadImg.onload = t.updateImageData;
		t.preloadImg.onerror = t.resetImageData;
		t.preloadImg.src = src;
	}
	};

	jQuery( function($) {
		$('.media-types input').click( function() {
			$('table.describe').toggleClass('not-image', $('#not-image').prop('checked') );
		});
	} );
	</script>

	<div id="media-items">
	<div class="media-item media-blank">
	<?php
	/**
	 * Filters the insert media from URL form HTML.
	 *
	 * @since 3.3.0
	 *
	 * @param string $form_html The insert from URL form HTML.
	 */
	echo apply_filters( 'type_url_form_media', wp_media_insert_url_form( $type ) );

	?>
	</div>
	</div>
	</form>
	<?php
}

/**
 * Adds gallery form to upload iframe.
 *
 * @since 2.5.0
 *
 * @global string $redir_tab
 * @global string $type
 * @global string $tab
 *
 * @param array $errors
 */
function media_upload_gallery_form( $errors ) {
	global $redir_tab, $type;

	$redir_tab = 'gallery';
	media_upload_header();

	$post_id         = (int) $_REQUEST['post_id'];
	$form_action_url = admin_url( "media-upload.php?type=$type&tab=gallery&post_id=$post_id" );
	/** This filter is documented in wp-admin/includes/media.php */
	$form_action_url = apply_filters( 'media_upload_form_url', $form_action_url, $type );
	$form_class      = 'media-upload-form validate';

	if ( get_user_setting( 'uploader' ) ) {
		$form_class .= ' html-uploader';
	}

	?>
	<script type="text/javascript">
	jQuery(function($){
		var preloaded = $(".media-item.preloaded");
		if ( preloaded.length > 0 ) {
			preloaded.each(function(){prepareMediaItem({id:this.id.replace(/[^0-9]/g, '')},'');});
			updateMediaForm();
		}
	});
	</script>
	<div id="sort-buttons" class="hide-if-no-js">
	<span>
		<?php _e( 'All Tabs:' ); ?>
	<a href="#" id="showall"><?php _e( 'Show' ); ?></a>
	<a href="#" id="hideall" style="display:none;"><?php _e( 'Hide' ); ?></a>
	</span>
		<?php _e( 'Sort Order:' ); ?>
	<a href="#" id="asc"><?php _e( 'Ascending' ); ?></a> |
	<a href="#" id="desc"><?php _e( 'Descending' ); ?></a> |
	<a href="#" id="clear"><?php _ex( 'Clear', 'verb' ); ?></a>
	</div>
	<form enctype="multipart/form-data" method="post" action="<?php echo esc_url( $form_action_url ); ?>" class="<?php echo $form_class; ?>" id="gallery-form">
		<?php wp_nonce_field( 'media-form' ); ?>
	<table class="widefat">
	<thead><tr>
	<th><?php _e( 'Media' ); ?></th>
	<th class="order-head"><?php _e( 'Order' ); ?></th>
	<th class="actions-head"><?php _e( 'Actions' ); ?></th>
	</tr></thead>
	</table>
	<div id="media-items">
		<?php add_filter( 'attachment_fields_to_edit', 'media_post_single_attachment_fields_to_edit', 10, 2 ); ?>
		<?php echo get_media_items( $post_id, $errors ); ?>
	</div>

	<p class="ml-submit">
		<?php
		submit_button(
			__( 'Save all changes' ),
			'savebutton',
			'save',
			false,
			array(
				'id'    => 'save-all',
				'style' => 'display: none;',
			)
		);
		?>
	<input type="hidden" name="post_id" id="post_id" value="<?php echo (int) $post_id; ?>" />
	<input type="hidden" name="type" value="<?php echo esc_attr( $GLOBALS['type'] ); ?>" />
	<input type="hidden" name="tab" value="<?php echo esc_attr( $GLOBALS['tab'] ); ?>" />
	</p>

	<div id="gallery-settings" style="display:none;">
	<div class="title"><?php _e( 'Gallery Settings' ); ?></div>
	<table id="basic" class="describe"><tbody>
		<tr>
		<th scope="row" class="label">
			<label>
			<span class="alignleft"><?php _e( 'Link thumbnails to:' ); ?></span>
			</label>
		</th>
		<td class="field">
			<input type="radio" name="linkto" id="linkto-file" value="file" />
			<label for="linkto-file" class="radio"><?php _e( 'Image File' ); ?></label>

			<input type="radio" checked="checked" name="linkto" id="linkto-post" value="post" />
			<label for="linkto-post" class="radio"><?php _e( 'Attachment Page' ); ?></label>
		</td>
		</tr>

		<tr>
		<th scope="row" class="label">
			<label>
			<span class="alignleft"><?php _e( 'Order images by:' ); ?></span>
			</label>
		</th>
		<td class="field">
			<select id="orderby" name="orderby">
				<option value="menu_order" selected="selected"><?php _e( 'Menu order' ); ?></option>
				<option value="title"><?php _e( 'Title' ); ?></option>
				<option value="post_date"><?php _e( 'Date/Time' ); ?></option>
				<option value="rand"><?php _e( 'Random' ); ?></option>
			</select>
		</td>
		</tr>

		<tr>
		<th scope="row" class="label">
			<label>
			<span class="alignleft"><?php _e( 'Order:' ); ?></span>
			</label>
		</th>
		<td class="field">
			<input type="radio" checked="checked" name="order" id="order-asc" value="asc" />
			<label for="order-asc" class="radio"><?php _e( 'Ascending' ); ?></label>

			<input type="radio" name="order" id="order-desc" value="desc" />
			<label for="order-desc" class="radio"><?php _e( 'Descending' ); ?></label>
		</td>
		</tr>

		<tr>
		<th scope="row" class="label">
			<label>
			<span class="alignleft"><?php _e( 'Gallery columns:' ); ?></span>
			</label>
		</th>
		<td class="field">
			<select id="columns" name="columns">
				<option value="1">1</option>
				<option value="2">2</option>
				<option value="3" selected="selected">3</option>
				<option value="4">4</option>
				<option value="5">5</option>
				<option value="6">6</option>
				<option value="7">7</option>
				<option value="8">8</option>
				<option value="9">9</option>
			</select>
		</td>
		</tr>
	</tbody></table>

	<p class="ml-submit">
	<input type="button" class="button" style="display:none;" onMouseDown="wpgallery.update();" name="insert-gallery" id="insert-gallery" value="<?php esc_attr_e( 'Insert gallery' ); ?>" />
	<input type="button" class="button" style="display:none;" onMouseDown="wpgallery.update();" name="update-gallery" id="update-gallery" value="<?php esc_attr_e( 'Update gallery settings' ); ?>" />
	</p>
	</div>
	</form>
	<?php
}

/**
 * Outputs the legacy media upload form for the media library.
 *
 * @since 2.5.0
 *
 * @global wpdb      $wpdb            WordPress database abstraction object.
 * @global WP_Query  $wp_query        WordPress Query object.
 * @global WP_Locale $wp_locale       WordPress date and time locale object.
 * @global string    $type
 * @global string    $tab
 * @global array     $post_mime_types
 *
 * @param array $errors
 */
function media_upload_library_form( $errors ) {
	global $wpdb, $wp_query, $wp_locale, $type, $tab, $post_mime_types;

	media_upload_header();

	$post_id = isset( $_REQUEST['post_id'] ) ? (int) $_REQUEST['post_id'] : 0;

	$form_action_url = admin_url( "media-upload.php?type=$type&tab=library&post_id=$post_id" );
	/** This filter is documented in wp-admin/includes/media.php */
	$form_action_url = apply_filters( 'media_upload_form_url', $form_action_url, $type );
	$form_class      = 'media-upload-form validate';

	if ( get_user_setting( 'uploader' ) ) {
		$form_class .= ' html-uploader';
	}

	$q                   = $_GET;
	$q['posts_per_page'] = 10;
	$q['paged']          = isset( $q['paged'] ) ? (int) $q['paged'] : 0;
	if ( $q['paged'] < 1 ) {
		$q['paged'] = 1;
	}
	$q['offset'] = ( $q['paged'] - 1 ) * 10;
	if ( $q['offset'] < 1 ) {
		$q['offset'] = 0;
	}

	list($post_mime_types, $avail_post_mime_types) = wp_edit_attachments_query( $q );

	?>
	<form id="filter" method="get">
	<input type="hidden" name="type" value="<?php echo esc_attr( $type ); ?>" />
	<input type="hidden" name="tab" value="<?php echo esc_attr( $tab ); ?>" />
	<input type="hidden" name="post_id" value="<?php echo (int) $post_id; ?>" />
	<input type="hidden" name="post_mime_type" value="<?php echo isset( $_GET['post_mime_type'] ) ? esc_attr( $_GET['post_mime_type'] ) : ''; ?>" />
	<input type="hidden" name="context" value="<?php echo isset( $_GET['context'] ) ? esc_attr( $_GET['context'] ) : ''; ?>" />

	<p id="media-search" class="search-box">
		<label class="screen-reader-text" for="media-search-input">
			<?php
			/* translators: Hidden accessibility text. */
			_e( 'Search Media:' );
			?>
		</label>
		<input type="search" id="media-search-input" name="s" value="<?php the_search_query(); ?>" />
		<?php submit_button( __( 'Search Media' ), '', '', false ); ?>
	</p>

	<ul class="subsubsub">
		<?php
		$type_links = array();
		$_num_posts = (array) wp_count_attachments();
		$matches    = wp_match_mime_types( array_keys( $post_mime_types ), array_keys( $_num_posts ) );
		foreach ( $matches as $_type => $reals ) {
			foreach ( $reals as $real ) {
				if ( isset( $num_posts[ $_type ] ) ) {
					$num_posts[ $_type ] += $_num_posts[ $real ];
				} else {
					$num_posts[ $_type ] = $_num_posts[ $real ];
				}
			}
		}
		// If available type specified by media button clicked, filter by that type.
		if ( empty( $_GET['post_mime_type'] ) && ! empty( $num_posts[ $type ] ) ) {
			$_GET['post_mime_type']                        = $type;
			list($post_mime_types, $avail_post_mime_types) = wp_edit_attachments_query();
		}
		if ( empty( $_GET['post_mime_type'] ) || 'all' === $_GET['post_mime_type'] ) {
			$class = ' class="current"';
		} else {
			$class = '';
		}
		$type_links[] = '<li><a href="' . esc_url(
			add_query_arg(
				array(
					'post_mime_type' => 'all',
					'paged'          => false,
					'm'              => false,
				)
			)
		) . '"' . $class . '>' . __( 'All Types' ) . '</a>';
		foreach ( $post_mime_types as $mime_type => $label ) {
			$class = '';

			if ( ! wp_match_mime_types( $mime_type, $avail_post_mime_types ) ) {
				continue;
			}

			if ( isset( $_GET['post_mime_type'] ) && wp_match_mime_types( $mime_type, $_GET['post_mime_type'] ) ) {
				$class = ' class="current"';
			}

			$type_links[] = '<li><a href="' . esc_url(
				add_query_arg(
					array(
						'post_mime_type' => $mime_type,
						'paged'          => false,
					)
				)
			) . '"' . $class . '>' . sprintf( translate_nooped_plural( $label[2], $num_posts[ $mime_type ] ), '<span id="' . $mime_type . '-counter">' . number_format_i18n( $num_posts[ $mime_type ] ) . '</span>' ) . '</a>';
		}
		/**
		 * Filters the media upload mime type list items.
		 *
		 * Returned values should begin with an `<li>` tag.
		 *
		 * @since 3.1.0
		 *
		 * @param string[] $type_links An array of list items containing mime type link HTML.
		 */
		echo implode( ' | </li>', apply_filters( 'media_upload_mime_type_links', $type_links ) ) . '</li>';
		unset( $type_links );
		?>
	</ul>

	<div class="tablenav">

		<?php
		$page_links = paginate_links(
			array(
				'base'      => add_query_arg( 'paged', '%#%' ),
				'format'    => '',
				'prev_text' => __( '&laquo;' ),
				'next_text' => __( '&raquo;' ),
				'total'     => (int) ceil( $wp_query->found_posts / 10 ),
				'current'   => $q['paged'],
			)
		);

		if ( $page_links ) {
			echo "<div class='tablenav-pages'>$page_links</div>";
		}
		?>

	<div class="alignleft actions">
		<?php
		$months = $wpdb->get_results(
			"SELECT DISTINCT YEAR( post_date ) AS year, MONTH( post_date ) AS month
			FROM $wpdb->posts
			WHERE post_type = 'attachment'
			ORDER BY post_date DESC"
		);

		$month_count    = count( $months );
		$selected_month = isset( $_GET['m'] ) ? (int) $_GET['m'] : 0;

		if ( $month_count && ( 1 !== $month_count || 0 !== (int) $months[0]->month ) ) {
			?>
			<select name='m'>
				<option<?php selected( $selected_month, 0 ); ?> value='0'><?php _e( 'All dates' ); ?></option>
			<?php
			foreach ( $months as $arc_row ) {
				if ( 0 === (int) $arc_row->year ) {
					continue;
				}

				$month = zeroise( $arc_row->month, 2 );
				$year  = $arc_row->year;

				printf(
					"<option %s value='%s'>%s</option>\n",
					selected( $selected_month, $year . $month, false ),
					esc_attr( $year . $month ),
					/* translators: 1: Month name, 2: 4-digit year. */
					esc_html( sprintf( __( '%1$s %2$d' ), $wp_locale->get_month( $month ), $year ) )
				);
			}
			?>
			</select>
		<?php } ?>

		<?php submit_button( __( 'Filter &#187;' ), '', 'post-query-submit', false ); ?>

	</div>

	<br class="clear" />
	</div>
	</form>

	<form enctype="multipart/form-data" method="post" action="<?php echo esc_url( $form_action_url ); ?>" class="<?php echo $form_class; ?>" id="library-form">
	<?php wp_nonce_field( 'media-form' ); ?>

	<script type="text/javascript">
	jQuery(function($){
		var preloaded = $(".media-item.preloaded");
		if ( preloaded.length > 0 ) {
			preloaded.each(function(){prepareMediaItem({id:this.id.replace(/[^0-9]/g, '')},'');});
			updateMediaForm();
		}
	});
	</script>

	<div id="media-items">
		<?php add_filter( 'attachment_fields_to_edit', 'media_post_single_attachment_fields_to_edit', 10, 2 ); ?>
		<?php echo get_media_items( null, $errors ); ?>
	</div>
	<p class="ml-submit">
		<?php submit_button( __( 'Save all changes' ), 'savebutton', 'save', false ); ?>
	<input type="hidden" name="post_id" id="post_id" value="<?php echo (int) $post_id; ?>" />
	</p>
	</form>
	<?php
}

/**
 * Creates the form for external url.
 *
 * @since 2.7.0
 *
 * @param string $default_view
 * @return string HTML content of the form.
 */
function wp_media_insert_url_form( $default_view = 'image' ) {
	/** This filter is documented in wp-admin/includes/media.php */
	if ( ! apply_filters( 'disable_captions', '' ) ) {
		$caption = '
		<tr class="image-only">
			<th scope="row" class="label">
				<label for="caption"><span class="alignleft">' . __( 'Image Caption' ) . '</span></label>
			</th>
			<td class="field"><textarea id="caption" name="caption"></textarea></td>
		</tr>';
	} else {
		$caption = '';
	}

	$default_align = get_option( 'image_default_align' );

	if ( empty( $default_align ) ) {
		$default_align = 'none';
	}

	if ( 'image' === $default_view ) {
		$view        = 'image-only';
		$table_class = '';
	} else {
		$view        = 'not-image';
		$table_class = $view;
	}

	return '
	<p class="media-types"><label><input type="radio" name="media_type" value="image" id="image-only"' . checked( 'image-only', $view, false ) . ' /> ' . __( 'Image' ) . '</label> &nbsp; &nbsp; <label><input type="radio" name="media_type" value="generic" id="not-image"' . checked( 'not-image', $view, false ) . ' /> ' . __( 'Audio, Video, or Other File' ) . '</label></p>
	<p class="media-types media-types-required-info">' .
		wp_required_field_message() .
	'</p>
	<table class="describe ' . $table_class . '"><tbody>
		<tr>
			<th scope="row" class="label" style="width:130px;">
				<label for="src"><span class="alignleft">' . __( 'URL' ) . '</span> ' . wp_required_field_indicator() . '</label>
				<span class="alignright" id="status_img"></span>
			</th>
			<td class="field"><input id="src" name="src" value="" type="text" required onblur="addExtImage.getImageData()" /></td>
		</tr>

		<tr>
			<th scope="row" class="label">
				<label for="title"><span class="alignleft">' . __( 'Title' ) . '</span> ' . wp_required_field_indicator() . '</label>
			</th>
			<td class="field"><input id="title" name="title" value="" type="text" required /></td>
		</tr>

		<tr class="not-image"><td></td><td><p class="help">' . __( 'Link text, e.g. &#8220;Ransom Demands (PDF)&#8221;' ) . '</p></td></tr>

		<tr class="image-only">
			<th scope="row" class="label">
				<label for="alt"><span class="alignleft">' . __( 'Alternative Text' ) . '</span> ' . wp_required_field_indicator() . '</label>
			</th>
			<td class="field"><input id="alt" name="alt" value="" type="text" required />
			<p class="help">' . __( 'Alt text for the image, e.g. &#8220;The Mona Lisa&#8221;' ) . '</p></td>
		</tr>
		' . $caption . '
		<tr class="align image-only">
			<th scope="row" class="label"><p><label for="align">' . __( 'Alignment' ) . '</label></p></th>
			<td class="field">
				<input name="align" id="align-none" value="none" onclick="addExtImage.align=\'align\'+this.value" type="radio"' . ( 'none' === $default_align ? ' checked="checked"' : '' ) . ' />
				<label for="align-none" class="align image-align-none-label">' . __( 'None' ) . '</label>
				<input name="align" id="align-left" value="left" onclick="addExtImage.align=\'align\'+this.value" type="radio"' . ( 'left' === $default_align ? ' checked="checked"' : '' ) . ' />
				<label for="align-left" class="align image-align-left-label">' . __( 'Left' ) . '</label>
				<input name="align" id="align-center" value="center" onclick="addExtImage.align=\'align\'+this.value" type="radio"' . ( 'center' === $default_align ? ' checked="checked"' : '' ) . ' />
				<label for="align-center" class="align image-align-center-label">' . __( 'Center' ) . '</label>
				<input name="align" id="align-right" value="right" onclick="addExtImage.align=\'align\'+this.value" type="radio"' . ( 'right' === $default_align ? ' checked="checked"' : '' ) . ' />
				<label for="align-right" class="align image-align-right-label">' . __( 'Right' ) . '</label>
			</td>
		</tr>

		<tr class="image-only">
			<th scope="row" class="label">
				<label for="url"><span class="alignleft">' . __( 'Link Image To:' ) . '</span></label>
			</th>
			<td class="field"><input id="url" name="url" value="" type="text" /><br />

			<button type="button" class="button" value="" onclick="document.forms[0].url.value=null">' . __( 'None' ) . '</button>
			<button type="button" class="button" value="" onclick="document.forms[0].url.value=document.forms[0].src.value">' . __( 'Link to image' ) . '</button>
			<p class="help">' . __( 'Enter a link URL or click above for presets.' ) . '</p></td>
		</tr>
		<tr class="image-only">
			<td></td>
			<td>
				<input type="button" class="button" id="go_button" style="color:#bbb;" onclick="addExtImage.insert()" value="' . esc_attr__( 'Insert into Post' ) . '" />
			</td>
		</tr>
		<tr class="not-image">
			<td></td>
			<td>
				' . get_submit_button( __( 'Insert into Post' ), '', 'insertonlybutton', false ) . '
			</td>
		</tr>
	</tbody></table>';
}

/**
 * Displays the multi-file uploader message.
 *
 * @since 2.6.0
 *
 * @global int $post_ID
 */
function media_upload_flash_bypass() {
	$browser_uploader = admin_url( 'media-new.php?browser-uploader' );

	$post = get_post();
	if ( $post ) {
		$browser_uploader .= '&amp;post_id=' . (int) $post->ID;
	} elseif ( ! empty( $GLOBALS['post_ID'] ) ) {
		$browser_uploader .= '&amp;post_id=' . (int) $GLOBALS['post_ID'];
	}

	?>
	<p class="upload-flash-bypass">
	<?php
		printf(
			/* translators: 1: URL to browser uploader, 2: Additional link attributes. */
			__( 'You are using the multi-file uploader. Problems? Try the <a href="%1$s" %2$s>browser uploader</a> instead.' ),
			$browser_uploader,
			'target="_blank"'
		);
	?>
	</p>
	<?php
}

/**
 * Displays the browser's built-in uploader message.
 *
 * @since 2.6.0
 */
function media_upload_html_bypass() {
	?>
	<p class="upload-html-bypass hide-if-no-js">
		<?php _e( 'You are using the browser&#8217;s built-in file uploader. The WordPress uploader includes multiple file selection and drag and drop capability. <a href="#">Switch to the multi-file uploader</a>.' ); ?>
	</p>
	<?php
}

/**
 * Used to display a "After a file has been uploaded..." help message.
 *
 * @since 3.3.0
 */
function media_upload_text_after() {}

/**
 * Displays the checkbox to scale images.
 *
 * @since 3.3.0
 */
function media_upload_max_image_resize() {
	$checked = get_user_setting( 'upload_resize' ) ? ' checked="true"' : '';
	$a       = '';
	$end     = '';

	if ( current_user_can( 'manage_options' ) ) {
		$a   = '<a href="' . esc_url( admin_url( 'options-media.php' ) ) . '" target="_blank">';
		$end = '</a>';
	}

	?>
	<p class="hide-if-no-js"><label>
	<input name="image_resize" type="checkbox" id="image_resize" value="true"<?php echo $checked; ?> />
	<?php
	/* translators: 1: Link start tag, 2: Link end tag, 3: Width, 4: Height. */
	printf( __( 'Scale images to match the large size selected in %1$simage options%2$s (%3$d &times; %4$d).' ), $a, $end, (int) get_option( 'large_size_w', '1024' ), (int) get_option( 'large_size_h', '1024' ) );

	?>
	</label></p>
	<?php
}

/**
 * Displays the out of storage quota message in Multisite.
 *
 * @since 3.5.0
 */
function multisite_over_quota_message() {
	echo '<p>' . sprintf(
		/* translators: %s: Allowed space allocation. */
		__( 'Sorry, you have used your space allocation of %s. Please delete some files to upload more files.' ),
		size_format( get_space_allowed() * MB_IN_BYTES )
	) . '</p>';
}

/**
 * Displays the image and editor in the post editor
 *
 * @since 3.5.0
 *
 * @param WP_Post $post A post object.
 */
function edit_form_image_editor( $post ) {
	$open = isset( $_GET['image-editor'] );

	if ( $open ) {
		require_once ABSPATH . 'wp-admin/includes/image-edit.php';
	}

	$thumb_url     = false;
	$attachment_id = (int) $post->ID;

	if ( $attachment_id ) {
		$thumb_url = wp_get_attachment_image_src( $attachment_id, array( 900, 450 ), true );
	}

	$alt_text = get_post_meta( $post->ID, '_wp_attachment_image_alt', true );

	$att_url = wp_get_attachment_url( $post->ID );
	?>
	<div class="wp_attachment_holder wp-clearfix">
	<?php

	if ( wp_attachment_is_image( $post->ID ) ) :
		$image_edit_button = '';
		if ( wp_image_editor_supports( array( 'mime_type' => $post->post_mime_type ) ) ) {
			$nonce             = wp_create_nonce( "image_editor-$post->ID" );
			$image_edit_button = "<input type='button' id='imgedit-open-btn-$post->ID' onclick='imageEdit.open( $post->ID, \"$nonce\" )' class='button' value='" . esc_attr__( 'Edit Image' ) . "' /> <span class='spinner'></span>";
		}

		$open_style     = '';
		$not_open_style = '';

		if ( $open ) {
			$open_style = ' style="display:none"';
		} else {
			$not_open_style = ' style="display:none"';
		}

		?>
		<div class="imgedit-response" id="imgedit-response-<?php echo $attachment_id; ?>"></div>

		<div<?php echo $open_style; ?> class="wp_attachment_image wp-clearfix" id="media-head-<?php echo $attachment_id; ?>">
			<p id="thumbnail-head-<?php echo $attachment_id; ?>"><img class="thumbnail" src="<?php echo set_url_scheme( $thumb_url[0] ); ?>" style="max-width:100%" alt="" /></p>
			<p><?php echo $image_edit_button; ?></p>
		</div>
		<div<?php echo $not_open_style; ?> class="image-editor" id="image-editor-<?php echo $attachment_id; ?>">
		<?php

		if ( $open ) {
			wp_image_editor( $attachment_id );
		}

		?>
		</div>
		<?php
	elseif ( $attachment_id && wp_attachment_is( 'audio', $post ) ) :

		wp_maybe_generate_attachment_metadata( $post );

		echo wp_audio_shortcode( array( 'src' => $att_url ) );

	elseif ( $attachment_id && wp_attachment_is( 'video', $post ) ) :

		wp_maybe_generate_attachment_metadata( $post );

		$meta = wp_get_attachment_metadata( $attachment_id );
		$w    = ! empty( $meta['width'] ) ? min( $meta['width'], 640 ) : 0;
		$h    = ! empty( $meta['height'] ) ? $meta['height'] : 0;

		if ( $h && $w < $meta['width'] ) {
			$h = round( ( $meta['height'] * $w ) / $meta['width'] );
		}

		$attr = array( 'src' => $att_url );

		if ( ! empty( $w ) && ! empty( $h ) ) {
			$attr['width']  = $w;
			$attr['height'] = $h;
		}

		$thumb_id = get_post_thumbnail_id( $attachment_id );

		if ( ! empty( $thumb_id ) ) {
			$attr['poster'] = wp_get_attachment_url( $thumb_id );
		}

		echo wp_video_shortcode( $attr );

	elseif ( isset( $thumb_url[0] ) ) :
		?>
		<div class="wp_attachment_image wp-clearfix" id="media-head-<?php echo $attachment_id; ?>">
			<p id="thumbnail-head-<?php echo $attachment_id; ?>">
				<img class="thumbnail" src="<?php echo set_url_scheme( $thumb_url[0] ); ?>" style="max-width:100%" alt="" />
			</p>
		</div>
		<?php

	else :

		/**
		 * Fires when an attachment type can't be rendered in the edit form.
		 *
		 * @since 4.6.0
		 *
		 * @param WP_Post $post A post object.
		 */
		do_action( 'wp_edit_form_attachment_display', $post );

	endif;

	?>
	</div>
	<div class="wp_attachment_details edit-form-section">
	<?php if ( str_starts_with( $post->post_mime_type, 'image' ) ) : ?>
		<p class="attachment-alt-text">
			<label for="attachment_alt"><strong><?php _e( 'Alternative Text' ); ?></strong></label><br />
			<textarea class="widefat" name="_wp_attachment_image_alt" id="attachment_alt" aria-describedby="alt-text-description"><?php echo esc_attr( $alt_text ); ?></textarea>
		</p>
		<p class="attachment-alt-text-description" id="alt-text-description">
		<?php

		printf(
			/* translators: 1: Link to tutorial, 2: Additional link attributes, 3: Accessibility text. */
			__( '<a href="%1$s" %2$s>Learn how to describe the purpose of the image%3$s</a>. Leave empty if the image is purely decorative.' ),
			/* translators: Localized tutorial, if one exists. W3C Web Accessibility Initiative link has list of existing translations. */
			esc_url( __( 'https://www.w3.org/WAI/tutorials/images/decision-tree/' ) ),
			'target="_blank"',
			sprintf(
				'<span class="screen-reader-text"> %s</span>',
				/* translators: Hidden accessibility text. */
				__( '(opens in a new tab)' )
			)
		);

		?>
		</p>
	<?php endif; ?>

		<p>
			<label for="attachment_caption"><strong><?php _e( 'Caption' ); ?></strong></label><br />
			<textarea class="widefat" name="excerpt" id="attachment_caption"><?php echo $post->post_excerpt; ?></textarea>
		</p>

	<?php

	$quicktags_settings = array( 'buttons' => 'strong,em,link,block,del,ins,img,ul,ol,li,code,close' );
	$editor_args        = array(
		'textarea_name' => 'content',
		'textarea_rows' => 5,
		'media_buttons' => false,
		/**
		 * Filters the TinyMCE argument for the media description field on the attachment details screen.
		 *
		 * @since 6.6.0
		 *
		 * @param bool $tinymce Whether to activate TinyMCE in media description field. Default false.
		 */
		'tinymce'       => apply_filters( 'activate_tinymce_for_media_description', false ),
		'quicktags'     => $quicktags_settings,
	);

	?>

	<label for="attachment_content" class="attachment-content-description"><strong><?php _e( 'Description' ); ?></strong>
	<?php

	if ( preg_match( '#^(audio|video)/#', $post->post_mime_type ) ) {
		echo ': ' . __( 'Displayed on attachment pages.' );
	}

	?>
	</label>
	<?php wp_editor( format_to_edit( $post->post_content ), 'attachment_content', $editor_args ); ?>

	</div>
	<?php

	$extras = get_compat_media_markup( $post->ID );
	echo $extras['item'];
	echo '<input type="hidden" id="image-edit-context" value="edit-attachment" />' . "\n";
}

/**
 * Displays non-editable attachment metadata in the publish meta box.
 *
 * @since 3.5.0
 */
function attachment_submitbox_metadata() {
	$post          = get_post();
	$attachment_id = $post->ID;

	$file     = get_attached_file( $attachment_id );
	$filename = esc_html( wp_basename( $file ) );

	$media_dims = '';
	$meta       = wp_get_attachment_metadata( $attachment_id );

	if ( isset( $meta['width'], $meta['height'] ) ) {
		/* translators: 1: A number of pixels wide, 2: A number of pixels tall. */
		$media_dims .= "<span id='media-dims-$attachment_id'>" . sprintf( __( '%1$s by %2$s pixels' ), $meta['width'], $meta['height'] ) . '</span>';
	}
	/** This filter is documented in wp-admin/includes/media.php */
	$media_dims = apply_filters( 'media_meta', $media_dims, $post );

	$att_url = wp_get_attachment_url( $attachment_id );

	$author = new WP_User( $post->post_author );

	$uploaded_by_name = __( '(no author)' );
	$uploaded_by_link = '';

	if ( $author->exists() ) {
		$uploaded_by_name = $author->display_name ? $author->display_name : $author->nickname;
		$uploaded_by_link = get_edit_user_link( $author->ID );
	}
	?>
	<div class="misc-pub-section misc-pub-uploadedby">
		<?php if ( $uploaded_by_link ) { ?>
			<?php _e( 'Uploaded by:' ); ?> <a href="<?php echo $uploaded_by_link; ?>"><strong><?php echo $uploaded_by_name; ?></strong></a>
		<?php } else { ?>
			<?php _e( 'Uploaded by:' ); ?> <strong><?php echo $uploaded_by_name; ?></strong>
		<?php } ?>
	</div>

	<?php
	if ( $post->post_parent ) {
		$post_parent = get_post( $post->post_parent );
		if ( $post_parent ) {
			$uploaded_to_title = $post_parent->post_title ? $post_parent->post_title : __( '(no title)' );
			$uploaded_to_link  = get_edit_post_link( $post->post_parent, 'raw' );
			?>
			<div class="misc-pub-section misc-pub-uploadedto">
				<?php if ( $uploaded_to_link ) { ?>
					<?php _e( 'Uploaded to:' ); ?> <a href="<?php echo $uploaded_to_link; ?>"><strong><?php echo $uploaded_to_title; ?></strong></a>
				<?php } else { ?>
					<?php _e( 'Uploaded to:' ); ?> <strong><?php echo $uploaded_to_title; ?></strong>
				<?php } ?>
			</div>
			<?php
		}
	}
	?>

	<div class="misc-pub-section misc-pub-attachment">
		<label for="attachment_url"><?php _e( 'File URL:' ); ?></label>
		<input type="text" class="widefat urlfield" readonly="readonly" name="attachment_url" id="attachment_url" value="<?php echo esc_attr( $att_url ); ?>" />
		<span class="copy-to-clipboard-container">
			<button type="button" class="button copy-attachment-url edit-media" data-clipboard-target="#attachment_url"><?php _e( 'Copy URL to clipboard' ); ?></button>
			<span class="success hidden" aria-hidden="true"><?php _e( 'Copied!' ); ?></span>
		</span>
	</div>
	<div class="misc-pub-section misc-pub-download">
		<a href="<?php echo esc_attr( $att_url ); ?>" download><?php _e( 'Download file' ); ?></a>
	</div>
	<div class="misc-pub-section misc-pub-filename">
		<?php _e( 'File name:' ); ?> <strong><?php echo $filename; ?></strong>
	</div>
	<div class="misc-pub-section misc-pub-filetype">
		<?php _e( 'File type:' ); ?>
		<strong>
		<?php

		if ( preg_match( '/^.*?\.(\w+)$/', get_attached_file( $post->ID ), $matches ) ) {
			echo esc_html( strtoupper( $matches[1] ) );
			list( $mime_type ) = explode( '/', $post->post_mime_type );
			if ( 'image' !== $mime_type && ! empty( $meta['mime_type'] ) ) {
				if ( "$mime_type/" . strtolower( $matches[1] ) !== $meta['mime_type'] ) {
					echo ' (' . $meta['mime_type'] . ')';
				}
			}
		} else {
			echo strtoupper( str_replace( 'image/', '', $post->post_mime_type ) );
		}

		?>
		</strong>
	</div>

	<?php

	$file_size = false;

	if ( isset( $meta['filesize'] ) ) {
		$file_size = $meta['filesize'];
	} elseif ( file_exists( $file ) ) {
		$file_size = wp_filesize( $file );
	}

	if ( ! empty( $file_size ) ) {
		?>
		<div class="misc-pub-section misc-pub-filesize">
			<?php _e( 'File size:' ); ?> <strong><?php echo size_format( $file_size ); ?></strong>
		</div>
		<?php
	}

	if ( preg_match( '#^(audio|video)/#', $post->post_mime_type ) ) {
		$fields = array(
			'length_formatted' => __( 'Length:' ),
			'bitrate'          => __( 'Bitrate:' ),
		);

		/**
		 * Filters the audio and video metadata fields to be shown in the publish meta box.
		 *
		 * The key for each item in the array should correspond to an attachment
		 * metadata key, and the value should be the desired label.
		 *
		 * @since 3.7.0
		 * @since 4.9.0 Added the `$post` parameter.
		 *
		 * @param array   $fields An array of the attachment metadata keys and labels.
		 * @param WP_Post $post   WP_Post object for the current attachment.
		 */
		$fields = apply_filters( 'media_submitbox_misc_sections', $fields, $post );

		foreach ( $fields as $key => $label ) {
			if ( empty( $meta[ $key ] ) ) {
				continue;
			}

			?>
			<div class="misc-pub-section misc-pub-mime-meta misc-pub-<?php echo sanitize_html_class( $key ); ?>">
				<?php echo $label; ?>
				<strong>
				<?php

				switch ( $key ) {
					case 'bitrate':
						echo round( $meta['bitrate'] / 1000 ) . 'kb/s';
						if ( ! empty( $meta['bitrate_mode'] ) ) {
							echo ' ' . strtoupper( esc_html( $meta['bitrate_mode'] ) );
						}
						break;
					case 'length_formatted':
						echo human_readable_duration( $meta['length_formatted'] );
						break;
					default:
						echo esc_html( $meta[ $key ] );
						break;
				}

				?>
				</strong>
			</div>
			<?php
		}

		$fields = array(
			'dataformat' => __( 'Audio Format:' ),
			'codec'      => __( 'Audio Codec:' ),
		);

		/**
		 * Filters the audio attachment metadata fields to be shown in the publish meta box.
		 *
		 * The key for each item in the array should correspond to an attachment
		 * metadata key, and the value should be the desired label.
		 *
		 * @since 3.7.0
		 * @since 4.9.0 Added the `$post` parameter.
		 *
		 * @param array   $fields An array of the attachment metadata keys and labels.
		 * @param WP_Post $post   WP_Post object for the current attachment.
		 */
		$audio_fields = apply_filters( 'audio_submitbox_misc_sections', $fields, $post );

		foreach ( $audio_fields as $key => $label ) {
			if ( empty( $meta['audio'][ $key ] ) ) {
				continue;
			}

			?>
			<div class="misc-pub-section misc-pub-audio misc-pub-<?php echo sanitize_html_class( $key ); ?>">
				<?php echo $label; ?> <strong><?php echo esc_html( $meta['audio'][ $key ] ); ?></strong>
			</div>
			<?php
		}
	}

	if ( $media_dims ) {
		?>
		<div class="misc-pub-section misc-pub-dimensions">
			<?php _e( 'Dimensions:' ); ?> <strong><?php echo $media_dims; ?></strong>
		</div>
		<?php
	}

	if ( ! empty( $meta['original_image'] ) ) {
		?>
		<div class="misc-pub-section misc-pub-original-image word-wrap-break-word">
			<?php _e( 'Original image:' ); ?>
			<a href="<?php echo esc_url( wp_get_original_image_url( $attachment_id ) ); ?>">
				<strong><?php echo esc_html( wp_basename( wp_get_original_image_path( $attachment_id ) ) ); ?></strong>
			</a>
		</div>
		<?php
	}
}

/**
 * Parses ID3v2, ID3v1, and getID3 comments to extract usable data.
 *
 * @since 3.6.0
 *
 * @param array $metadata An existing array with data.
 * @param array $data Data supplied by ID3 tags.
 */
function wp_add_id3_tag_data( &$metadata, $data ) {
	foreach ( array( 'id3v2', 'id3v1' ) as $version ) {
		if ( ! empty( $data[ $version ]['comments'] ) ) {
			foreach ( $data[ $version ]['comments'] as $key => $list ) {
				if ( 'length' !== $key && ! empty( $list ) ) {
					$metadata[ $key ] = wp_kses_post( reset( $list ) );
					// Fix bug in byte stream analysis.
					if ( 'terms_of_use' === $key && str_starts_with( $metadata[ $key ], 'yright notice.' ) ) {
						$metadata[ $key ] = 'Cop' . $metadata[ $key ];
					}
				}
			}
			break;
		}
	}

	if ( ! empty( $data['id3v2']['APIC'] ) ) {
		$image = reset( $data['id3v2']['APIC'] );
		if ( ! empty( $image['data'] ) ) {
			$metadata['image'] = array(
				'data'   => $image['data'],
				'mime'   => $image['image_mime'],
				'width'  => $image['image_width'],
				'height' => $image['image_height'],
			);
		}
	} elseif ( ! empty( $data['comments']['picture'] ) ) {
		$image = reset( $data['comments']['picture'] );
		if ( ! empty( $image['data'] ) ) {
			$metadata['image'] = array(
				'data' => $image['data'],
				'mime' => $image['image_mime'],
			);
		}
	}
}

/**
 * Retrieves metadata from a video file's ID3 tags.
 *
 * @since 3.6.0
 *
 * @param string $file Path to file.
 * @return array|false Returns array of metadata, if found.
 */
function wp_read_video_metadata( $file ) {
	if ( ! file_exists( $file ) ) {
		return false;
	}

	$metadata = array();

	if ( ! defined( 'GETID3_TEMP_DIR' ) ) {
		define( 'GETID3_TEMP_DIR', get_temp_dir() );
	}

	if ( ! class_exists( 'getID3', false ) ) {
		require ABSPATH . WPINC . '/ID3/getid3.php';
	}

	$id3 = new getID3();
	// Required to get the `created_timestamp` value.
	$id3->options_audiovideo_quicktime_ReturnAtomData = true; // phpcs:ignore WordPress.NamingConventions.ValidVariableName

	$data = $id3->analyze( $file );

	if ( isset( $data['video']['lossless'] ) ) {
		$metadata['lossless'] = $data['video']['lossless'];
	}

	if ( ! empty( $data['video']['bitrate'] ) ) {
		$metadata['bitrate'] = (int) $data['video']['bitrate'];
	}

	if ( ! empty( $data['video']['bitrate_mode'] ) ) {
		$metadata['bitrate_mode'] = $data['video']['bitrate_mode'];
	}

	if ( ! empty( $data['filesize'] ) ) {
		$metadata['filesize'] = (int) $data['filesize'];
	}

	if ( ! empty( $data['mime_type'] ) ) {
		$metadata['mime_type'] = $data['mime_type'];
	}

	if ( ! empty( $data['playtime_seconds'] ) ) {
		$metadata['length'] = (int) round( $data['playtime_seconds'] );
	}

	if ( ! empty( $data['playtime_string'] ) ) {
		$metadata['length_formatted'] = $data['playtime_string'];
	}

	if ( ! empty( $data['video']['resolution_x'] ) ) {
		$metadata['width'] = (int) $data['video']['resolution_x'];
	}

	if ( ! empty( $data['video']['resolution_y'] ) ) {
		$metadata['height'] = (int) $data['video']['resolution_y'];
	}

	if ( ! empty( $data['fileformat'] ) ) {
		$metadata['fileformat'] = $data['fileformat'];
	}

	if ( ! empty( $data['video']['dataformat'] ) ) {
		$metadata['dataformat'] = $data['video']['dataformat'];
	}

	if ( ! empty( $data['video']['encoder'] ) ) {
		$metadata['encoder'] = $data['video']['encoder'];
	}

	if ( ! empty( $data['video']['codec'] ) ) {
		$metadata['codec'] = $data['video']['codec'];
	}

	if ( ! empty( $data['audio'] ) ) {
		unset( $data['audio']['streams'] );
		$metadata['audio'] = $data['audio'];
	}

	if ( empty( $metadata['created_timestamp'] ) ) {
		$created_timestamp = wp_get_media_creation_timestamp( $data );

		if ( false !== $created_timestamp ) {
			$metadata['created_timestamp'] = $created_timestamp;
		}
	}

	wp_add_id3_tag_data( $metadata, $data );

	$file_format = isset( $metadata['fileformat'] ) ? $metadata['fileformat'] : null;

	/**
	 * Filters the array of metadata retrieved from a video.
	 *
	 * In core, usually this selection is what is stored.
	 * More complete data can be parsed from the `$data` parameter.
	 *
	 * @since 4.9.0
	 *
	 * @param array       $metadata    Filtered video metadata.
	 * @param string      $file        Path to video file.
	 * @param string|null $file_format File format of video, as analyzed by getID3.
	 *                                 Null if unknown.
	 * @param array       $data        Raw metadata from getID3.
	 */
	return apply_filters( 'wp_read_video_metadata', $metadata, $file, $file_format, $data );
}

/**
 * Retrieves metadata from an audio file's ID3 tags.
 *
 * @since 3.6.0
 *
 * @param string $file Path to file.
 * @return array|false Returns array of metadata, if found.
 */
function wp_read_audio_metadata( $file ) {
	if ( ! file_exists( $file ) ) {
		return false;
	}

	$metadata = array();

	if ( ! defined( 'GETID3_TEMP_DIR' ) ) {
		define( 'GETID3_TEMP_DIR', get_temp_dir() );
	}

	if ( ! class_exists( 'getID3', false ) ) {
		require ABSPATH . WPINC . '/ID3/getid3.php';
	}

	$id3 = new getID3();
	// Required to get the `created_timestamp` value.
	$id3->options_audiovideo_quicktime_ReturnAtomData = true; // phpcs:ignore WordPress.NamingConventions.ValidVariableName

	$data = $id3->analyze( $file );

	if ( ! empty( $data['audio'] ) ) {
		unset( $data['audio']['streams'] );
		$metadata = $data['audio'];
	}

	if ( ! empty( $data['fileformat'] ) ) {
		$metadata['fileformat'] = $data['fileformat'];
	}

	if ( ! empty( $data['filesize'] ) ) {
		$metadata['filesize'] = (int) $data['filesize'];
	}

	if ( ! empty( $data['mime_type'] ) ) {
		$metadata['mime_type'] = $data['mime_type'];
	}

	if ( ! empty( $data['playtime_seconds'] ) ) {
		$metadata['length'] = (int) round( $data['playtime_seconds'] );
	}

	if ( ! empty( $data['playtime_string'] ) ) {
		$metadata['length_formatted'] = $data['playtime_string'];
	}

	if ( empty( $metadata['created_timestamp'] ) ) {
		$created_timestamp = wp_get_media_creation_timestamp( $data );

		if ( false !== $created_timestamp ) {
			$metadata['created_timestamp'] = $created_timestamp;
		}
	}

	wp_add_id3_tag_data( $metadata, $data );

	$file_format = isset( $metadata['fileformat'] ) ? $metadata['fileformat'] : null;

	/**
	 * Filters the array of metadata retrieved from an audio file.
	 *
	 * In core, usually this selection is what is stored.
	 * More complete data can be parsed from the `$data` parameter.
	 *
	 * @since 6.1.0
	 *
	 * @param array       $metadata    Filtered audio metadata.
	 * @param string      $file        Path to audio file.
	 * @param string|null $file_format File format of audio, as analyzed by getID3.
	 *                                 Null if unknown.
	 * @param array       $data        Raw metadata from getID3.
	 */
	return apply_filters( 'wp_read_audio_metadata', $metadata, $file, $file_format, $data );
}

/**
 * Parses creation date from media metadata.
 *
 * The getID3 library doesn't have a standard method for getting creation dates,
 * so the location of this data can vary based on the MIME type.
 *
 * @since 4.9.0
 *
 * @link https://github.com/JamesHeinrich/getID3/blob/master/structure.txt
 *
 * @param array $metadata The metadata returned by getID3::analyze().
 * @return int|false A UNIX timestamp for the media's creation date if available
 *                   or a boolean FALSE if a timestamp could not be determined.
 */
function wp_get_media_creation_timestamp( $metadata ) {
	$creation_date = false;

	if ( empty( $metadata['fileformat'] ) ) {
		return $creation_date;
	}

	switch ( $metadata['fileformat'] ) {
		case 'asf':
			if ( isset( $metadata['asf']['file_properties_object']['creation_date_unix'] ) ) {
				$creation_date = (int) $metadata['asf']['file_properties_object']['creation_date_unix'];
			}
			break;

		case 'matroska':
		case 'webm':
			if ( isset( $metadata['matroska']['comments']['creation_time'][0] ) ) {
				$creation_date = strtotime( $metadata['matroska']['comments']['creation_time'][0] );
			} elseif ( isset( $metadata['matroska']['info'][0]['DateUTC_unix'] ) ) {
				$creation_date = (int) $metadata['matroska']['info'][0]['DateUTC_unix'];
			}
			break;

		case 'quicktime':
		case 'mp4':
			if ( isset( $metadata['quicktime']['moov']['subatoms'][0]['creation_time_unix'] ) ) {
				$creation_date = (int) $metadata['quicktime']['moov']['subatoms'][0]['creation_time_unix'];
			}
			break;
	}

	return $creation_date;
}

/**
 * Encapsulates the logic for Attach/Detach actions.
 *
 * @since 4.2.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param int    $parent_id Attachment parent ID.
 * @param string $action    Optional. Attach/detach action. Accepts 'attach' or 'detach'.
 *                          Default 'attach'.
 */
function wp_media_attach_action( $parent_id, $action = 'attach' ) {
	global $wpdb;

	if ( ! $parent_id ) {
		return;
	}

	if ( ! current_user_can( 'edit_post', $parent_id ) ) {
		wp_die( __( 'Sorry, you are not allowed to edit this post.' ) );
	}

	$ids = array();

	foreach ( (array) $_REQUEST['media'] as $attachment_id ) {
		$attachment_id = (int) $attachment_id;

		if ( ! current_user_can( 'edit_post', $attachment_id ) ) {
			continue;
		}

		$ids[] = $attachment_id;
	}

	if ( ! empty( $ids ) ) {
		$ids_string = implode( ',', $ids );

		if ( 'attach' === $action ) {
			$result = $wpdb->query( $wpdb->prepare( "UPDATE $wpdb->posts SET post_parent = %d WHERE post_type = 'attachment' AND ID IN ( $ids_string )", $parent_id ) );
		} else {
			$result = $wpdb->query( "UPDATE $wpdb->posts SET post_parent = 0 WHERE post_type = 'attachment' AND ID IN ( $ids_string )" );
		}
	}

	if ( isset( $result ) ) {
		foreach ( $ids as $attachment_id ) {
			/**
			 * Fires when media is attached or detached from a post.
			 *
			 * @since 5.5.0
			 *
			 * @param string $action        Attach/detach action. Accepts 'attach' or 'detach'.
			 * @param int    $attachment_id The attachment ID.
			 * @param int    $parent_id     Attachment parent ID.
			 */
			do_action( 'wp_media_attach_action', $action, $attachment_id, $parent_id );

			clean_attachment_cache( $attachment_id );
		}

		$location = 'upload.php';
		$referer  = wp_get_referer();

		if ( $referer ) {
			if ( str_contains( $referer, 'upload.php' ) ) {
				$location = remove_query_arg( array( 'attached', 'detach' ), $referer );
			}
		}

		$key      = 'attach' === $action ? 'attached' : 'detach';
		$location = add_query_arg( array( $key => $result ), $location );

		wp_redirect( $location );
		exit;
	}
}
class-wp-upgrader.php000064400000135611151327343360010633 0ustar00<?php
/**
 * Upgrade API: WP_Upgrader class
 *
 * Requires skin classes and WP_Upgrader subclasses for backward compatibility.
 *
 * @package WordPress
 * @subpackage Upgrader
 * @since 2.8.0
 */

/** WP_Upgrader_Skin class */
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader-skin.php';

/** Plugin_Upgrader_Skin class */
require_once ABSPATH . 'wp-admin/includes/class-plugin-upgrader-skin.php';

/** Theme_Upgrader_Skin class */
require_once ABSPATH . 'wp-admin/includes/class-theme-upgrader-skin.php';

/** Bulk_Upgrader_Skin class */
require_once ABSPATH . 'wp-admin/includes/class-bulk-upgrader-skin.php';

/** Bulk_Plugin_Upgrader_Skin class */
require_once ABSPATH . 'wp-admin/includes/class-bulk-plugin-upgrader-skin.php';

/** Bulk_Theme_Upgrader_Skin class */
require_once ABSPATH . 'wp-admin/includes/class-bulk-theme-upgrader-skin.php';

/** Plugin_Installer_Skin class */
require_once ABSPATH . 'wp-admin/includes/class-plugin-installer-skin.php';

/** Theme_Installer_Skin class */
require_once ABSPATH . 'wp-admin/includes/class-theme-installer-skin.php';

/** Language_Pack_Upgrader_Skin class */
require_once ABSPATH . 'wp-admin/includes/class-language-pack-upgrader-skin.php';

/** Automatic_Upgrader_Skin class */
require_once ABSPATH . 'wp-admin/includes/class-automatic-upgrader-skin.php';

/** WP_Ajax_Upgrader_Skin class */
require_once ABSPATH . 'wp-admin/includes/class-wp-ajax-upgrader-skin.php';

/**
 * Core class used for upgrading/installing a local set of files via
 * the Filesystem Abstraction classes from a Zip file.
 *
 * @since 2.8.0
 */
#[AllowDynamicProperties]
class WP_Upgrader {

	/**
	 * The error/notification strings used to update the user on the progress.
	 *
	 * @since 2.8.0
	 * @var array $strings
	 */
	public $strings = array();

	/**
	 * The upgrader skin being used.
	 *
	 * @since 2.8.0
	 * @var Automatic_Upgrader_Skin|WP_Upgrader_Skin $skin
	 */
	public $skin = null;

	/**
	 * The result of the installation.
	 *
	 * This is set by WP_Upgrader::install_package(), only when the package is installed
	 * successfully. It will then be an array, unless a WP_Error is returned by the
	 * {@see 'upgrader_post_install'} filter. In that case, the WP_Error will be assigned to
	 * it.
	 *
	 * @since 2.8.0
	 *
	 * @var array|WP_Error $result {
	 *     @type string $source             The full path to the source the files were installed from.
	 *     @type string $source_files       List of all the files in the source directory.
	 *     @type string $destination        The full path to the installation destination folder.
	 *     @type string $destination_name   The name of the destination folder, or empty if `$destination`
	 *                                      and `$local_destination` are the same.
	 *     @type string $local_destination  The full local path to the destination folder. This is usually
	 *                                      the same as `$destination`.
	 *     @type string $remote_destination The full remote path to the destination folder
	 *                                      (i.e., from `$wp_filesystem`).
	 *     @type bool   $clear_destination  Whether the destination folder was cleared.
	 * }
	 */
	public $result = array();

	/**
	 * The total number of updates being performed.
	 *
	 * Set by the bulk update methods.
	 *
	 * @since 3.0.0
	 * @var int $update_count
	 */
	public $update_count = 0;

	/**
	 * The current update if multiple updates are being performed.
	 *
	 * Used by the bulk update methods, and incremented for each update.
	 *
	 * @since 3.0.0
	 * @var int
	 */
	public $update_current = 0;

	/**
	 * Stores the list of plugins or themes added to temporary backup directory.
	 *
	 * Used by the rollback functions.
	 *
	 * @since 6.3.0
	 * @var array
	 */
	private $temp_backups = array();

	/**
	 * Stores the list of plugins or themes to be restored from temporary backup directory.
	 *
	 * Used by the rollback functions.
	 *
	 * @since 6.3.0
	 * @var array
	 */
	private $temp_restores = array();

	/**
	 * Construct the upgrader with a skin.
	 *
	 * @since 2.8.0
	 *
	 * @param WP_Upgrader_Skin $skin The upgrader skin to use. Default is a WP_Upgrader_Skin
	 *                               instance.
	 */
	public function __construct( $skin = null ) {
		if ( null === $skin ) {
			$this->skin = new WP_Upgrader_Skin();
		} else {
			$this->skin = $skin;
		}
	}

	/**
	 * Initializes the upgrader.
	 *
	 * This will set the relationship between the skin being used and this upgrader,
	 * and also add the generic strings to `WP_Upgrader::$strings`.
	 *
	 * Additionally, it will schedule a weekly task to clean up the temporary backup directory.
	 *
	 * @since 2.8.0
	 * @since 6.3.0 Added the `schedule_temp_backup_cleanup()` task.
	 */
	public function init() {
		$this->skin->set_upgrader( $this );
		$this->generic_strings();

		if ( ! wp_installing() ) {
			$this->schedule_temp_backup_cleanup();
		}
	}

	/**
	 * Schedules the cleanup of the temporary backup directory.
	 *
	 * @since 6.3.0
	 */
	protected function schedule_temp_backup_cleanup() {
		if ( false === wp_next_scheduled( 'wp_delete_temp_updater_backups' ) ) {
			wp_schedule_event( time(), 'weekly', 'wp_delete_temp_updater_backups' );
		}
	}

	/**
	 * Adds the generic strings to WP_Upgrader::$strings.
	 *
	 * @since 2.8.0
	 */
	public function generic_strings() {
		$this->strings['bad_request']    = __( 'Invalid data provided.' );
		$this->strings['fs_unavailable'] = __( 'Could not access filesystem.' );
		$this->strings['fs_error']       = __( 'Filesystem error.' );
		$this->strings['fs_no_root_dir'] = __( 'Unable to locate WordPress root directory.' );
		/* translators: %s: Directory name. */
		$this->strings['fs_no_content_dir'] = sprintf( __( 'Unable to locate WordPress content directory (%s).' ), 'wp-content' );
		$this->strings['fs_no_plugins_dir'] = __( 'Unable to locate WordPress plugin directory.' );
		$this->strings['fs_no_themes_dir']  = __( 'Unable to locate WordPress theme directory.' );
		/* translators: %s: Directory name. */
		$this->strings['fs_no_folder'] = __( 'Unable to locate needed folder (%s).' );

		$this->strings['no_package']           = __( 'Package not available.' );
		$this->strings['download_failed']      = __( 'Download failed.' );
		$this->strings['installing_package']   = __( 'Installing the latest version&#8230;' );
		$this->strings['no_files']             = __( 'The package contains no files.' );
		$this->strings['folder_exists']        = __( 'Destination folder already exists.' );
		$this->strings['mkdir_failed']         = __( 'Could not create directory.' );
		$this->strings['incompatible_archive'] = __( 'The package could not be installed.' );
		$this->strings['files_not_writable']   = __( 'The update cannot be installed because some files could not be copied. This is usually due to inconsistent file permissions.' );
		$this->strings['dir_not_readable']     = __( 'A directory could not be read.' );

		$this->strings['maintenance_start'] = __( 'Enabling Maintenance mode&#8230;' );
		$this->strings['maintenance_end']   = __( 'Disabling Maintenance mode&#8230;' );

		/* translators: %s: upgrade-temp-backup */
		$this->strings['temp_backup_mkdir_failed'] = sprintf( __( 'Could not create the %s directory.' ), 'upgrade-temp-backup' );
		/* translators: %s: upgrade-temp-backup */
		$this->strings['temp_backup_move_failed'] = sprintf( __( 'Could not move the old version to the %s directory.' ), 'upgrade-temp-backup' );
		/* translators: %s: The plugin or theme slug. */
		$this->strings['temp_backup_restore_failed'] = __( 'Could not restore the original version of %s.' );
		/* translators: %s: The plugin or theme slug. */
		$this->strings['temp_backup_delete_failed'] = __( 'Could not delete the temporary backup directory for %s.' );
	}

	/**
	 * Connects to the filesystem.
	 *
	 * @since 2.8.0
	 *
	 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
	 *
	 * @param string[] $directories                  Optional. Array of directories. If any of these do
	 *                                               not exist, a WP_Error object will be returned.
	 *                                               Default empty array.
	 * @param bool     $allow_relaxed_file_ownership Whether to allow relaxed file ownership.
	 *                                               Default false.
	 * @return bool|WP_Error True if able to connect, false or a WP_Error otherwise.
	 */
	public function fs_connect( $directories = array(), $allow_relaxed_file_ownership = false ) {
		global $wp_filesystem;

		$credentials = $this->skin->request_filesystem_credentials( false, $directories[0], $allow_relaxed_file_ownership );
		if ( false === $credentials ) {
			return false;
		}

		if ( ! WP_Filesystem( $credentials, $directories[0], $allow_relaxed_file_ownership ) ) {
			$error = true;
			if ( is_object( $wp_filesystem ) && $wp_filesystem->errors->has_errors() ) {
				$error = $wp_filesystem->errors;
			}
			// Failed to connect. Error and request again.
			$this->skin->request_filesystem_credentials( $error, $directories[0], $allow_relaxed_file_ownership );
			return false;
		}

		if ( ! is_object( $wp_filesystem ) ) {
			return new WP_Error( 'fs_unavailable', $this->strings['fs_unavailable'] );
		}

		if ( is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
			return new WP_Error( 'fs_error', $this->strings['fs_error'], $wp_filesystem->errors );
		}

		foreach ( (array) $directories as $dir ) {
			switch ( $dir ) {
				case ABSPATH:
					if ( ! $wp_filesystem->abspath() ) {
						return new WP_Error( 'fs_no_root_dir', $this->strings['fs_no_root_dir'] );
					}
					break;
				case WP_CONTENT_DIR:
					if ( ! $wp_filesystem->wp_content_dir() ) {
						return new WP_Error( 'fs_no_content_dir', $this->strings['fs_no_content_dir'] );
					}
					break;
				case WP_PLUGIN_DIR:
					if ( ! $wp_filesystem->wp_plugins_dir() ) {
						return new WP_Error( 'fs_no_plugins_dir', $this->strings['fs_no_plugins_dir'] );
					}
					break;
				case get_theme_root():
					if ( ! $wp_filesystem->wp_themes_dir() ) {
						return new WP_Error( 'fs_no_themes_dir', $this->strings['fs_no_themes_dir'] );
					}
					break;
				default:
					if ( ! $wp_filesystem->find_folder( $dir ) ) {
						return new WP_Error( 'fs_no_folder', sprintf( $this->strings['fs_no_folder'], esc_html( basename( $dir ) ) ) );
					}
					break;
			}
		}
		return true;
	}

	/**
	 * Downloads a package.
	 *
	 * @since 2.8.0
	 * @since 5.2.0 Added the `$check_signatures` parameter.
	 * @since 5.5.0 Added the `$hook_extra` parameter.
	 *
	 * @param string $package          The URI of the package. If this is the full path to an
	 *                                 existing local file, it will be returned untouched.
	 * @param bool   $check_signatures Whether to validate file signatures. Default false.
	 * @param array  $hook_extra       Extra arguments to pass to the filter hooks. Default empty array.
	 * @return string|WP_Error The full path to the downloaded package file, or a WP_Error object.
	 */
	public function download_package( $package, $check_signatures = false, $hook_extra = array() ) {
		/**
		 * Filters whether to return the package.
		 *
		 * @since 3.7.0
		 * @since 5.5.0 Added the `$hook_extra` parameter.
		 *
		 * @param bool        $reply      Whether to bail without returning the package.
		 *                                Default false.
		 * @param string      $package    The package file name.
		 * @param WP_Upgrader $upgrader   The WP_Upgrader instance.
		 * @param array       $hook_extra Extra arguments passed to hooked filters.
		 */
		$reply = apply_filters( 'upgrader_pre_download', false, $package, $this, $hook_extra );
		if ( false !== $reply ) {
			return $reply;
		}

		if ( ! preg_match( '!^(http|https|ftp)://!i', $package ) && file_exists( $package ) ) { // Local file or remote?
			return $package; // Must be a local file.
		}

		if ( empty( $package ) ) {
			return new WP_Error( 'no_package', $this->strings['no_package'] );
		}

		$this->skin->feedback( 'downloading_package', $package );

		$download_file = download_url( $package, 300, $check_signatures );

		if ( is_wp_error( $download_file ) && ! $download_file->get_error_data( 'softfail-filename' ) ) {
			return new WP_Error( 'download_failed', $this->strings['download_failed'], $download_file->get_error_message() );
		}

		return $download_file;
	}

	/**
	 * Unpacks a compressed package file.
	 *
	 * @since 2.8.0
	 *
	 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
	 *
	 * @param string $package        Full path to the package file.
	 * @param bool   $delete_package Optional. Whether to delete the package file after attempting
	 *                               to unpack it. Default true.
	 * @return string|WP_Error The path to the unpacked contents, or a WP_Error on failure.
	 */
	public function unpack_package( $package, $delete_package = true ) {
		global $wp_filesystem;

		$this->skin->feedback( 'unpack_package' );

		if ( ! $wp_filesystem->wp_content_dir() ) {
			return new WP_Error( 'fs_no_content_dir', $this->strings['fs_no_content_dir'] );
		}

		$upgrade_folder = $wp_filesystem->wp_content_dir() . 'upgrade/';

		// Clean up contents of upgrade directory beforehand.
		$upgrade_files = $wp_filesystem->dirlist( $upgrade_folder );
		if ( ! empty( $upgrade_files ) ) {
			foreach ( $upgrade_files as $file ) {
				$wp_filesystem->delete( $upgrade_folder . $file['name'], true );
			}
		}

		// We need a working directory - strip off any .tmp or .zip suffixes.
		$working_dir = $upgrade_folder . basename( basename( $package, '.tmp' ), '.zip' );

		// Clean up working directory.
		if ( $wp_filesystem->is_dir( $working_dir ) ) {
			$wp_filesystem->delete( $working_dir, true );
		}

		// Unzip package to working directory.
		$result = unzip_file( $package, $working_dir );

		// Once extracted, delete the package if required.
		if ( $delete_package ) {
			unlink( $package );
		}

		if ( is_wp_error( $result ) ) {
			$wp_filesystem->delete( $working_dir, true );
			if ( 'incompatible_archive' === $result->get_error_code() ) {
				return new WP_Error( 'incompatible_archive', $this->strings['incompatible_archive'], $result->get_error_data() );
			}
			return $result;
		}

		return $working_dir;
	}

	/**
	 * Flattens the results of WP_Filesystem_Base::dirlist() for iterating over.
	 *
	 * @since 4.9.0
	 * @access protected
	 *
	 * @param array  $nested_files Array of files as returned by WP_Filesystem_Base::dirlist().
	 * @param string $path         Relative path to prepend to child nodes. Optional.
	 * @return array A flattened array of the $nested_files specified.
	 */
	protected function flatten_dirlist( $nested_files, $path = '' ) {
		$files = array();

		foreach ( $nested_files as $name => $details ) {
			$files[ $path . $name ] = $details;

			// Append children recursively.
			if ( ! empty( $details['files'] ) ) {
				$children = $this->flatten_dirlist( $details['files'], $path . $name . '/' );

				// Merge keeping possible numeric keys, which array_merge() will reindex from 0..n.
				$files = $files + $children;
			}
		}

		return $files;
	}

	/**
	 * Clears the directory where this item is going to be installed into.
	 *
	 * @since 4.3.0
	 *
	 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
	 *
	 * @param string $remote_destination The location on the remote filesystem to be cleared.
	 * @return true|WP_Error True upon success, WP_Error on failure.
	 */
	public function clear_destination( $remote_destination ) {
		global $wp_filesystem;

		$files = $wp_filesystem->dirlist( $remote_destination, true, true );

		// False indicates that the $remote_destination doesn't exist.
		if ( false === $files ) {
			return true;
		}

		// Flatten the file list to iterate over.
		$files = $this->flatten_dirlist( $files );

		// Check all files are writable before attempting to clear the destination.
		$unwritable_files = array();

		// Check writability.
		foreach ( $files as $filename => $file_details ) {
			if ( ! $wp_filesystem->is_writable( $remote_destination . $filename ) ) {
				// Attempt to alter permissions to allow writes and try again.
				$wp_filesystem->chmod( $remote_destination . $filename, ( 'd' === $file_details['type'] ? FS_CHMOD_DIR : FS_CHMOD_FILE ) );
				if ( ! $wp_filesystem->is_writable( $remote_destination . $filename ) ) {
					$unwritable_files[] = $filename;
				}
			}
		}

		if ( ! empty( $unwritable_files ) ) {
			return new WP_Error( 'files_not_writable', $this->strings['files_not_writable'], implode( ', ', $unwritable_files ) );
		}

		if ( ! $wp_filesystem->delete( $remote_destination, true ) ) {
			return new WP_Error( 'remove_old_failed', $this->strings['remove_old_failed'] );
		}

		return true;
	}

	/**
	 * Install a package.
	 *
	 * Copies the contents of a package from a source directory, and installs them in
	 * a destination directory. Optionally removes the source. It can also optionally
	 * clear out the destination folder if it already exists.
	 *
	 * @since 2.8.0
	 * @since 6.2.0 Use move_dir() instead of copy_dir() when possible.
	 *
	 * @global WP_Filesystem_Base $wp_filesystem        WordPress filesystem subclass.
	 * @global string[]           $wp_theme_directories
	 *
	 * @param array|string $args {
	 *     Optional. Array or string of arguments for installing a package. Default empty array.
	 *
	 *     @type string $source                      Required path to the package source. Default empty.
	 *     @type string $destination                 Required path to a folder to install the package in.
	 *                                               Default empty.
	 *     @type bool   $clear_destination           Whether to delete any files already in the destination
	 *                                               folder. Default false.
	 *     @type bool   $clear_working               Whether to delete the files from the working directory
	 *                                               after copying them to the destination. Default false.
	 *     @type bool   $abort_if_destination_exists Whether to abort the installation if
	 *                                               the destination folder already exists. Default true.
	 *     @type array  $hook_extra                  Extra arguments to pass to the filter hooks called by
	 *                                               WP_Upgrader::install_package(). Default empty array.
	 * }
	 *
	 * @return array|WP_Error The result (also stored in `WP_Upgrader::$result`), or a WP_Error on failure.
	 */
	public function install_package( $args = array() ) {
		global $wp_filesystem, $wp_theme_directories;

		$defaults = array(
			'source'                      => '', // Please always pass this.
			'destination'                 => '', // ...and this.
			'clear_destination'           => false,
			'clear_working'               => false,
			'abort_if_destination_exists' => true,
			'hook_extra'                  => array(),
		);

		$args = wp_parse_args( $args, $defaults );

		// These were previously extract()'d.
		$source            = $args['source'];
		$destination       = $args['destination'];
		$clear_destination = $args['clear_destination'];

		/*
		 * Give the upgrade an additional 300 seconds (5 minutes) to ensure the install
		 * doesn't prematurely timeout having used up the maximum script execution time
		 * upacking and downloading in WP_Upgrader->run().
		 */
		if ( function_exists( 'set_time_limit' ) ) {
			set_time_limit( 300 );
		}

		if (
			( ! is_string( $source ) || '' === $source || trim( $source ) !== $source ) ||
			( ! is_string( $destination ) || '' === $destination || trim( $destination ) !== $destination )
		) {
			return new WP_Error( 'bad_request', $this->strings['bad_request'] );
		}
		$this->skin->feedback( 'installing_package' );

		/**
		 * Filters the installation response before the installation has started.
		 *
		 * Returning a value that could be evaluated as a `WP_Error` will effectively
		 * short-circuit the installation, returning that value instead.
		 *
		 * @since 2.8.0
		 *
		 * @param bool|WP_Error $response   Installation response.
		 * @param array         $hook_extra Extra arguments passed to hooked filters.
		 */
		$res = apply_filters( 'upgrader_pre_install', true, $args['hook_extra'] );

		if ( is_wp_error( $res ) ) {
			return $res;
		}

		// Retain the original source and destinations.
		$remote_source     = $args['source'];
		$local_destination = $destination;

		$dirlist = $wp_filesystem->dirlist( $remote_source );

		if ( false === $dirlist ) {
			return new WP_Error( 'source_read_failed', $this->strings['fs_error'], $this->strings['dir_not_readable'] );
		}

		$source_files       = array_keys( $dirlist );
		$remote_destination = $wp_filesystem->find_folder( $local_destination );

		// Locate which directory to copy to the new folder. This is based on the actual folder holding the files.
		if ( 1 === count( $source_files ) && $wp_filesystem->is_dir( trailingslashit( $args['source'] ) . $source_files[0] . '/' ) ) {
			// Only one folder? Then we want its contents.
			$source = trailingslashit( $args['source'] ) . trailingslashit( $source_files[0] );
		} elseif ( 0 === count( $source_files ) ) {
			// There are no files?
			return new WP_Error( 'incompatible_archive_empty', $this->strings['incompatible_archive'], $this->strings['no_files'] );
		} else {
			/*
			 * It's only a single file, the upgrader will use the folder name of this file as the destination folder.
			 * Folder name is based on zip filename.
			 */
			$source = trailingslashit( $args['source'] );
		}

		/**
		 * Filters the source file location for the upgrade package.
		 *
		 * @since 2.8.0
		 * @since 4.4.0 The $hook_extra parameter became available.
		 *
		 * @param string      $source        File source location.
		 * @param string      $remote_source Remote file source location.
		 * @param WP_Upgrader $upgrader      WP_Upgrader instance.
		 * @param array       $hook_extra    Extra arguments passed to hooked filters.
		 */
		$source = apply_filters( 'upgrader_source_selection', $source, $remote_source, $this, $args['hook_extra'] );

		if ( is_wp_error( $source ) ) {
			return $source;
		}

		if ( ! empty( $args['hook_extra']['temp_backup'] ) ) {
			$temp_backup = $this->move_to_temp_backup_dir( $args['hook_extra']['temp_backup'] );

			if ( is_wp_error( $temp_backup ) ) {
				return $temp_backup;
			}

			$this->temp_backups[] = $args['hook_extra']['temp_backup'];
		}

		// Has the source location changed? If so, we need a new source_files list.
		if ( $source !== $remote_source ) {
			$dirlist = $wp_filesystem->dirlist( $source );

			if ( false === $dirlist ) {
				return new WP_Error( 'new_source_read_failed', $this->strings['fs_error'], $this->strings['dir_not_readable'] );
			}

			$source_files = array_keys( $dirlist );
		}

		/*
		 * Protection against deleting files in any important base directories.
		 * Theme_Upgrader & Plugin_Upgrader also trigger this, as they pass the
		 * destination directory (WP_PLUGIN_DIR / wp-content/themes) intending
		 * to copy the directory into the directory, whilst they pass the source
		 * as the actual files to copy.
		 */
		$protected_directories = array( ABSPATH, WP_CONTENT_DIR, WP_PLUGIN_DIR, WP_CONTENT_DIR . '/themes' );

		if ( is_array( $wp_theme_directories ) ) {
			$protected_directories = array_merge( $protected_directories, $wp_theme_directories );
		}

		if ( in_array( $destination, $protected_directories, true ) ) {
			$remote_destination = trailingslashit( $remote_destination ) . trailingslashit( basename( $source ) );
			$destination        = trailingslashit( $destination ) . trailingslashit( basename( $source ) );
		}

		if ( $clear_destination ) {
			// We're going to clear the destination if there's something there.
			$this->skin->feedback( 'remove_old' );

			$removed = $this->clear_destination( $remote_destination );

			/**
			 * Filters whether the upgrader cleared the destination.
			 *
			 * @since 2.8.0
			 *
			 * @param true|WP_Error $removed            Whether the destination was cleared.
			 *                                          True upon success, WP_Error on failure.
			 * @param string        $local_destination  The local package destination.
			 * @param string        $remote_destination The remote package destination.
			 * @param array         $hook_extra         Extra arguments passed to hooked filters.
			 */
			$removed = apply_filters( 'upgrader_clear_destination', $removed, $local_destination, $remote_destination, $args['hook_extra'] );

			if ( is_wp_error( $removed ) ) {
				return $removed;
			}
		} elseif ( $args['abort_if_destination_exists'] && $wp_filesystem->exists( $remote_destination ) ) {
			/*
			 * If we're not clearing the destination folder and something exists there already, bail.
			 * But first check to see if there are actually any files in the folder.
			 */
			$_files = $wp_filesystem->dirlist( $remote_destination );
			if ( ! empty( $_files ) ) {
				$wp_filesystem->delete( $remote_source, true ); // Clear out the source files.
				return new WP_Error( 'folder_exists', $this->strings['folder_exists'], $remote_destination );
			}
		}

		/*
		 * If 'clear_working' is false, the source should not be removed, so use copy_dir() instead.
		 *
		 * Partial updates, like language packs, may want to retain the destination.
		 * If the destination exists or has contents, this may be a partial update,
		 * and the destination should not be removed, so use copy_dir() instead.
		 */
		if ( $args['clear_working']
			&& (
				// Destination does not exist or has no contents.
				! $wp_filesystem->exists( $remote_destination )
				|| empty( $wp_filesystem->dirlist( $remote_destination ) )
			)
		) {
			$result = move_dir( $source, $remote_destination, true );
		} else {
			// Create destination if needed.
			if ( ! $wp_filesystem->exists( $remote_destination ) ) {
				if ( ! $wp_filesystem->mkdir( $remote_destination, FS_CHMOD_DIR ) ) {
					return new WP_Error( 'mkdir_failed_destination', $this->strings['mkdir_failed'], $remote_destination );
				}
			}
			$result = copy_dir( $source, $remote_destination );
		}

		// Clear the working directory?
		if ( $args['clear_working'] ) {
			$wp_filesystem->delete( $remote_source, true );
		}

		if ( is_wp_error( $result ) ) {
			return $result;
		}

		$destination_name = basename( str_replace( $local_destination, '', $destination ) );
		if ( '.' === $destination_name ) {
			$destination_name = '';
		}

		$this->result = compact( 'source', 'source_files', 'destination', 'destination_name', 'local_destination', 'remote_destination', 'clear_destination' );

		/**
		 * Filters the installation response after the installation has finished.
		 *
		 * @since 2.8.0
		 *
		 * @param bool  $response   Installation response.
		 * @param array $hook_extra Extra arguments passed to hooked filters.
		 * @param array $result     Installation result data.
		 */
		$res = apply_filters( 'upgrader_post_install', true, $args['hook_extra'], $this->result );

		if ( is_wp_error( $res ) ) {
			$this->result = $res;
			return $res;
		}

		// Bombard the calling function will all the info which we've just used.
		return $this->result;
	}

	/**
	 * Runs an upgrade/installation.
	 *
	 * Attempts to download the package (if it is not a local file), unpack it, and
	 * install it in the destination folder.
	 *
	 * @since 2.8.0
	 *
	 * @param array $options {
	 *     Array or string of arguments for upgrading/installing a package.
	 *
	 *     @type string $package                     The full path or URI of the package to install.
	 *                                               Default empty.
	 *     @type string $destination                 The full path to the destination folder.
	 *                                               Default empty.
	 *     @type bool   $clear_destination           Whether to delete any files already in the
	 *                                               destination folder. Default false.
	 *     @type bool   $clear_working               Whether to delete the files from the working
	 *                                               directory after copying them to the destination.
	 *                                               Default true.
	 *     @type bool   $abort_if_destination_exists Whether to abort the installation if the destination
	 *                                               folder already exists. When true, `$clear_destination`
	 *                                               should be false. Default true.
	 *     @type bool   $is_multi                    Whether this run is one of multiple upgrade/installation
	 *                                               actions being performed in bulk. When true, the skin
	 *                                               WP_Upgrader::header() and WP_Upgrader::footer()
	 *                                               aren't called. Default false.
	 *     @type array  $hook_extra                  Extra arguments to pass to the filter hooks called by
	 *                                               WP_Upgrader::run().
	 * }
	 * @return array|false|WP_Error The result from self::install_package() on success, otherwise a WP_Error,
	 *                              or false if unable to connect to the filesystem.
	 */
	public function run( $options ) {

		$defaults = array(
			'package'                     => '', // Please always pass this.
			'destination'                 => '', // ...and this.
			'clear_destination'           => false,
			'clear_working'               => true,
			'abort_if_destination_exists' => true, // Abort if the destination directory exists. Pass clear_destination as false please.
			'is_multi'                    => false,
			'hook_extra'                  => array(), // Pass any extra $hook_extra args here, this will be passed to any hooked filters.
		);

		$options = wp_parse_args( $options, $defaults );

		/**
		 * Filters the package options before running an update.
		 *
		 * See also {@see 'upgrader_process_complete'}.
		 *
		 * @since 4.3.0
		 *
		 * @param array $options {
		 *     Options used by the upgrader.
		 *
		 *     @type string $package                     Package for update.
		 *     @type string $destination                 Update location.
		 *     @type bool   $clear_destination           Clear the destination resource.
		 *     @type bool   $clear_working               Clear the working resource.
		 *     @type bool   $abort_if_destination_exists Abort if the Destination directory exists.
		 *     @type bool   $is_multi                    Whether the upgrader is running multiple times.
		 *     @type array  $hook_extra {
		 *         Extra hook arguments.
		 *
		 *         @type string $action               Type of action. Default 'update'.
		 *         @type string $type                 Type of update process. Accepts 'plugin', 'theme', or 'core'.
		 *         @type bool   $bulk                 Whether the update process is a bulk update. Default true.
		 *         @type string $plugin               Path to the plugin file relative to the plugins directory.
		 *         @type string $theme                The stylesheet or template name of the theme.
		 *         @type string $language_update_type The language pack update type. Accepts 'plugin', 'theme',
		 *                                            or 'core'.
		 *         @type object $language_update      The language pack update offer.
		 *     }
		 * }
		 */
		$options = apply_filters( 'upgrader_package_options', $options );

		if ( ! $options['is_multi'] ) { // Call $this->header separately if running multiple times.
			$this->skin->header();
		}

		// Connect to the filesystem first.
		$res = $this->fs_connect( array( WP_CONTENT_DIR, $options['destination'] ) );
		// Mainly for non-connected filesystem.
		if ( ! $res ) {
			if ( ! $options['is_multi'] ) {
				$this->skin->footer();
			}
			return false;
		}

		$this->skin->before();

		if ( is_wp_error( $res ) ) {
			$this->skin->error( $res );
			$this->skin->after();
			if ( ! $options['is_multi'] ) {
				$this->skin->footer();
			}
			return $res;
		}

		/*
		 * Download the package. Note: If the package is the full path
		 * to an existing local file, it will be returned untouched.
		 */
		$download = $this->download_package( $options['package'], false, $options['hook_extra'] );

		/*
		 * Allow for signature soft-fail.
		 * WARNING: This may be removed in the future.
		 */
		if ( is_wp_error( $download ) && $download->get_error_data( 'softfail-filename' ) ) {

			// Don't output the 'no signature could be found' failure message for now.
			if ( 'signature_verification_no_signature' !== $download->get_error_code() || WP_DEBUG ) {
				// Output the failure error as a normal feedback, and not as an error.
				$this->skin->feedback( $download->get_error_message() );

				// Report this failure back to WordPress.org for debugging purposes.
				wp_version_check(
					array(
						'signature_failure_code' => $download->get_error_code(),
						'signature_failure_data' => $download->get_error_data(),
					)
				);
			}

			// Pretend this error didn't happen.
			$download = $download->get_error_data( 'softfail-filename' );
		}

		if ( is_wp_error( $download ) ) {
			$this->skin->error( $download );
			$this->skin->after();
			if ( ! $options['is_multi'] ) {
				$this->skin->footer();
			}
			return $download;
		}

		$delete_package = ( $download !== $options['package'] ); // Do not delete a "local" file.

		// Unzips the file into a temporary directory.
		$working_dir = $this->unpack_package( $download, $delete_package );
		if ( is_wp_error( $working_dir ) ) {
			$this->skin->error( $working_dir );
			$this->skin->after();
			if ( ! $options['is_multi'] ) {
				$this->skin->footer();
			}
			return $working_dir;
		}

		// With the given options, this installs it to the destination directory.
		$result = $this->install_package(
			array(
				'source'                      => $working_dir,
				'destination'                 => $options['destination'],
				'clear_destination'           => $options['clear_destination'],
				'abort_if_destination_exists' => $options['abort_if_destination_exists'],
				'clear_working'               => $options['clear_working'],
				'hook_extra'                  => $options['hook_extra'],
			)
		);

		/**
		 * Filters the result of WP_Upgrader::install_package().
		 *
		 * @since 5.7.0
		 *
		 * @param array|WP_Error $result     Result from WP_Upgrader::install_package().
		 * @param array          $hook_extra Extra arguments passed to hooked filters.
		 */
		$result = apply_filters( 'upgrader_install_package_result', $result, $options['hook_extra'] );

		$this->skin->set_result( $result );

		if ( is_wp_error( $result ) ) {
			// An automatic plugin update will have already performed its rollback.
			if ( ! empty( $options['hook_extra']['temp_backup'] ) ) {
				$this->temp_restores[] = $options['hook_extra']['temp_backup'];

				/*
				 * Restore the backup on shutdown.
				 * Actions running on `shutdown` are immune to PHP timeouts,
				 * so in case the failure was due to a PHP timeout,
				 * it will still be able to properly restore the previous version.
				 *
				 * Zero arguments are accepted as a string can sometimes be passed
				 * internally during actions, causing an error because
				 * `WP_Upgrader::restore_temp_backup()` expects an array.
				 */
				add_action( 'shutdown', array( $this, 'restore_temp_backup' ), 10, 0 );
			}
			$this->skin->error( $result );

			if ( ! method_exists( $this->skin, 'hide_process_failed' ) || ! $this->skin->hide_process_failed( $result ) ) {
				$this->skin->feedback( 'process_failed' );
			}
		} else {
			// Installation succeeded.
			$this->skin->feedback( 'process_success' );
		}

		$this->skin->after();

		// Clean up the backup kept in the temporary backup directory.
		if ( ! empty( $options['hook_extra']['temp_backup'] ) ) {
			// Delete the backup on `shutdown` to avoid a PHP timeout.
			add_action( 'shutdown', array( $this, 'delete_temp_backup' ), 100, 0 );
		}

		if ( ! $options['is_multi'] ) {

			/**
			 * Fires when the upgrader process is complete.
			 *
			 * See also {@see 'upgrader_package_options'}.
			 *
			 * @since 3.6.0
			 * @since 3.7.0 Added to WP_Upgrader::run().
			 * @since 4.6.0 `$translations` was added as a possible argument to `$hook_extra`.
			 *
			 * @param WP_Upgrader $upgrader   WP_Upgrader instance. In other contexts this might be a
			 *                                Theme_Upgrader, Plugin_Upgrader, Core_Upgrade, or Language_Pack_Upgrader instance.
			 * @param array       $hook_extra {
			 *     Array of bulk item update data.
			 *
			 *     @type string $action       Type of action. Default 'update'.
			 *     @type string $type         Type of update process. Accepts 'plugin', 'theme', 'translation', or 'core'.
			 *     @type bool   $bulk         Whether the update process is a bulk update. Default true.
			 *     @type array  $plugins      Array of the basename paths of the plugins' main files.
			 *     @type array  $themes       The theme slugs.
			 *     @type array  $translations {
			 *         Array of translations update data.
			 *
			 *         @type string $language The locale the translation is for.
			 *         @type string $type     Type of translation. Accepts 'plugin', 'theme', or 'core'.
			 *         @type string $slug     Text domain the translation is for. The slug of a theme/plugin or
			 *                                'default' for core translations.
			 *         @type string $version  The version of a theme, plugin, or core.
			 *     }
			 * }
			 */
			do_action( 'upgrader_process_complete', $this, $options['hook_extra'] );

			$this->skin->footer();
		}

		return $result;
	}

	/**
	 * Toggles maintenance mode for the site.
	 *
	 * Creates/deletes the maintenance file to enable/disable maintenance mode.
	 *
	 * @since 2.8.0
	 *
	 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
	 *
	 * @param bool $enable True to enable maintenance mode, false to disable.
	 */
	public function maintenance_mode( $enable = false ) {
		global $wp_filesystem;

		if ( ! $wp_filesystem ) {
			if ( ! function_exists( 'WP_Filesystem' ) ) {
				require_once ABSPATH . 'wp-admin/includes/file.php';
			}

			ob_start();
			$credentials = request_filesystem_credentials( '' );
			ob_end_clean();

			if ( false === $credentials || ! WP_Filesystem( $credentials ) ) {
				wp_trigger_error( __FUNCTION__, __( 'Could not access filesystem.' ) );
				return;
			}
		}

		$file = $wp_filesystem->abspath() . '.maintenance';
		if ( $enable ) {
			if ( ! wp_doing_cron() ) {
				$this->skin->feedback( 'maintenance_start' );
			}
			// Create maintenance file to signal that we are upgrading.
			$maintenance_string = '<?php $upgrading = ' . time() . '; ?>';
			$wp_filesystem->delete( $file );
			$wp_filesystem->put_contents( $file, $maintenance_string, FS_CHMOD_FILE );
		} elseif ( ! $enable && $wp_filesystem->exists( $file ) ) {
			if ( ! wp_doing_cron() ) {
				$this->skin->feedback( 'maintenance_end' );
			}
			$wp_filesystem->delete( $file );
		}
	}

	/**
	 * Creates a lock using WordPress options.
	 *
	 * @since 4.5.0
	 *
	 * @global wpdb $wpdb The WordPress database abstraction object.
	 *
	 * @param string $lock_name       The name of this unique lock.
	 * @param int    $release_timeout Optional. The duration in seconds to respect an existing lock.
	 *                                Default: 1 hour.
	 * @return bool False if a lock couldn't be created or if the lock is still valid. True otherwise.
	 */
	public static function create_lock( $lock_name, $release_timeout = null ) {
		global $wpdb;
		if ( ! $release_timeout ) {
			$release_timeout = HOUR_IN_SECONDS;
		}
		$lock_option = $lock_name . '.lock';

		// Try to lock.
		$lock_result = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'off') /* LOCK */", $lock_option, time() ) );

		if ( ! $lock_result ) {
			$lock_result = get_option( $lock_option );

			// If a lock couldn't be created, and there isn't a lock, bail.
			if ( ! $lock_result ) {
				return false;
			}

			// Check to see if the lock is still valid. If it is, bail.
			if ( $lock_result > ( time() - $release_timeout ) ) {
				return false;
			}

			// There must exist an expired lock, clear it and re-gain it.
			WP_Upgrader::release_lock( $lock_name );

			return WP_Upgrader::create_lock( $lock_name, $release_timeout );
		}

		// Update the lock, as by this point we've definitely got a lock, just need to fire the actions.
		update_option( $lock_option, time(), false );

		return true;
	}

	/**
	 * Releases an upgrader lock.
	 *
	 * @since 4.5.0
	 *
	 * @see WP_Upgrader::create_lock()
	 *
	 * @param string $lock_name The name of this unique lock.
	 * @return bool True if the lock was successfully released. False on failure.
	 */
	public static function release_lock( $lock_name ) {
		return delete_option( $lock_name . '.lock' );
	}

	/**
	 * Moves the plugin or theme being updated into a temporary backup directory.
	 *
	 * @since 6.3.0
	 *
	 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
	 *
	 * @param string[] $args {
	 *     Array of data for the temporary backup.
	 *
	 *     @type string $slug Plugin or theme slug.
	 *     @type string $src  Path to the root directory for plugins or themes.
	 *     @type string $dir  Destination subdirectory name. Accepts 'plugins' or 'themes'.
	 * }
	 *
	 * @return bool|WP_Error True on success, false on early exit, otherwise WP_Error.
	 */
	public function move_to_temp_backup_dir( $args ) {
		global $wp_filesystem;

		if ( empty( $args['slug'] ) || empty( $args['src'] ) || empty( $args['dir'] ) ) {
			return false;
		}

		/*
		 * Skip any plugin that has "." as its slug.
		 * A slug of "." will result in a `$src` value ending in a period.
		 *
		 * On Windows, this will cause the 'plugins' folder to be moved,
		 * and will cause a failure when attempting to call `mkdir()`.
		 */
		if ( '.' === $args['slug'] ) {
			return false;
		}

		if ( ! $wp_filesystem->wp_content_dir() ) {
			return new WP_Error( 'fs_no_content_dir', $this->strings['fs_no_content_dir'] );
		}

		$dest_dir = $wp_filesystem->wp_content_dir() . 'upgrade-temp-backup/';
		$sub_dir  = $dest_dir . $args['dir'] . '/';

		// Create the temporary backup directory if it does not exist.
		if ( ! $wp_filesystem->is_dir( $sub_dir ) ) {
			if ( ! $wp_filesystem->is_dir( $dest_dir ) ) {
				$wp_filesystem->mkdir( $dest_dir, FS_CHMOD_DIR );
			}

			if ( ! $wp_filesystem->mkdir( $sub_dir, FS_CHMOD_DIR ) ) {
				// Could not create the backup directory.
				return new WP_Error( 'fs_temp_backup_mkdir', $this->strings['temp_backup_mkdir_failed'] );
			}
		}

		$src_dir = $wp_filesystem->find_folder( $args['src'] );
		$src     = trailingslashit( $src_dir ) . $args['slug'];
		$dest    = $dest_dir . trailingslashit( $args['dir'] ) . $args['slug'];

		// Delete the temporary backup directory if it already exists.
		if ( $wp_filesystem->is_dir( $dest ) ) {
			$wp_filesystem->delete( $dest, true );
		}

		// Move to the temporary backup directory.
		$result = move_dir( $src, $dest, true );
		if ( is_wp_error( $result ) ) {
			return new WP_Error( 'fs_temp_backup_move', $this->strings['temp_backup_move_failed'] );
		}

		return true;
	}

	/**
	 * Restores the plugin or theme from temporary backup.
	 *
	 * @since 6.3.0
	 * @since 6.6.0 Added the `$temp_backups` parameter.
	 *
	 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
	 *
	 * @param array[] $temp_backups {
	 *     Optional. An array of temporary backups.
	 *
	 *     @type array ...$0 {
	 *         Information about the backup.
	 *
	 *         @type string $dir  The temporary backup location in the upgrade-temp-backup directory.
	 *         @type string $slug The item's slug.
	 *         @type string $src  The directory where the original is stored. For example, `WP_PLUGIN_DIR`.
	 *     }
	 * }
	 * @return bool|WP_Error True on success, false on early exit, otherwise WP_Error.
	 */
	public function restore_temp_backup( array $temp_backups = array() ) {
		global $wp_filesystem;

		$errors = new WP_Error();

		if ( empty( $temp_backups ) ) {
			$temp_backups = $this->temp_restores;
		}

		foreach ( $temp_backups as $args ) {
			if ( empty( $args['slug'] ) || empty( $args['src'] ) || empty( $args['dir'] ) ) {
				return false;
			}

			if ( ! $wp_filesystem->wp_content_dir() ) {
				$errors->add( 'fs_no_content_dir', $this->strings['fs_no_content_dir'] );
				return $errors;
			}

			$src      = $wp_filesystem->wp_content_dir() . 'upgrade-temp-backup/' . $args['dir'] . '/' . $args['slug'];
			$dest_dir = $wp_filesystem->find_folder( $args['src'] );
			$dest     = trailingslashit( $dest_dir ) . $args['slug'];

			if ( $wp_filesystem->is_dir( $src ) ) {
				// Cleanup.
				if ( $wp_filesystem->is_dir( $dest ) && ! $wp_filesystem->delete( $dest, true ) ) {
					$errors->add(
						'fs_temp_backup_delete',
						sprintf( $this->strings['temp_backup_restore_failed'], $args['slug'] )
					);
					continue;
				}

				// Move it.
				$result = move_dir( $src, $dest, true );
				if ( is_wp_error( $result ) ) {
					$errors->add(
						'fs_temp_backup_delete',
						sprintf( $this->strings['temp_backup_restore_failed'], $args['slug'] )
					);
					continue;
				}
			}
		}

		return $errors->has_errors() ? $errors : true;
	}

	/**
	 * Deletes a temporary backup.
	 *
	 * @since 6.3.0
	 * @since 6.6.0 Added the `$temp_backups` parameter.
	 *
	 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
	 *
	 * @param array[] $temp_backups {
	 *     Optional. An array of temporary backups.
	 *
	 *     @type array ...$0 {
	 *         Information about the backup.
	 *
	 *         @type string $dir  The temporary backup location in the upgrade-temp-backup directory.
	 *         @type string $slug The item's slug.
	 *         @type string $src  The directory where the original is stored. For example, `WP_PLUGIN_DIR`.
	 *     }
	 * }
	 * @return bool|WP_Error True on success, false on early exit, otherwise WP_Error.
	 */
	public function delete_temp_backup( array $temp_backups = array() ) {
		global $wp_filesystem;

		$errors = new WP_Error();

		if ( empty( $temp_backups ) ) {
			$temp_backups = $this->temp_backups;
		}

		foreach ( $temp_backups as $args ) {
			if ( empty( $args['slug'] ) || empty( $args['dir'] ) ) {
				return false;
			}

			if ( ! $wp_filesystem->wp_content_dir() ) {
				$errors->add( 'fs_no_content_dir', $this->strings['fs_no_content_dir'] );
				return $errors;
			}

			$temp_backup_dir = $wp_filesystem->wp_content_dir() . "upgrade-temp-backup/{$args['dir']}/{$args['slug']}";

			if ( ! $wp_filesystem->delete( $temp_backup_dir, true ) ) {
				$errors->add(
					'temp_backup_delete_failed',
					sprintf( $this->strings['temp_backup_delete_failed'], $args['slug'] )
				);
				continue;
			}
		}

		return $errors->has_errors() ? $errors : true;
	}
}

/** Plugin_Upgrader class */
require_once ABSPATH . 'wp-admin/includes/class-plugin-upgrader.php';

/** Theme_Upgrader class */
require_once ABSPATH . 'wp-admin/includes/class-theme-upgrader.php';

/** Language_Pack_Upgrader class */
require_once ABSPATH . 'wp-admin/includes/class-language-pack-upgrader.php';

/** Core_Upgrader class */
require_once ABSPATH . 'wp-admin/includes/class-core-upgrader.php';

/** File_Upload_Upgrader class */
require_once ABSPATH . 'wp-admin/includes/class-file-upload-upgrader.php';

/** WP_Automatic_Updater class */
require_once ABSPATH . 'wp-admin/includes/class-wp-automatic-updater.php';
revision.php000064400000040267151327343360007133 0ustar00<?php
/**
 * WordPress Administration Revisions API
 *
 * @package WordPress
 * @subpackage Administration
 * @since 3.6.0
 */

/**
 * Get the revision UI diff.
 *
 * @since 3.6.0
 *
 * @param WP_Post|int $post         The post object or post ID.
 * @param int         $compare_from The revision ID to compare from.
 * @param int         $compare_to   The revision ID to come to.
 * @return array|false Associative array of a post's revisioned fields and their diffs.
 *                     Or, false on failure.
 */
function wp_get_revision_ui_diff( $post, $compare_from, $compare_to ) {
	$post = get_post( $post );
	if ( ! $post ) {
		return false;
	}

	if ( $compare_from ) {
		$compare_from = get_post( $compare_from );
		if ( ! $compare_from ) {
			return false;
		}
	} else {
		// If we're dealing with the first revision...
		$compare_from = false;
	}

	$compare_to = get_post( $compare_to );
	if ( ! $compare_to ) {
		return false;
	}

	/*
	 * If comparing revisions, make sure we are dealing with the right post parent.
	 * The parent post may be a 'revision' when revisions are disabled and we're looking at autosaves.
	 */
	if ( $compare_from && $compare_from->post_parent !== $post->ID && $compare_from->ID !== $post->ID ) {
		return false;
	}
	if ( $compare_to->post_parent !== $post->ID && $compare_to->ID !== $post->ID ) {
		return false;
	}

	if ( $compare_from && strtotime( $compare_from->post_date_gmt ) > strtotime( $compare_to->post_date_gmt ) ) {
		$temp         = $compare_from;
		$compare_from = $compare_to;
		$compare_to   = $temp;
	}

	// Add default title if title field is empty.
	if ( $compare_from && empty( $compare_from->post_title ) ) {
		$compare_from->post_title = __( '(no title)' );
	}
	if ( empty( $compare_to->post_title ) ) {
		$compare_to->post_title = __( '(no title)' );
	}

	$return = array();

	foreach ( _wp_post_revision_fields( $post ) as $field => $name ) {
		/**
		 * Contextually filter a post revision field.
		 *
		 * The dynamic portion of the hook name, `$field`, corresponds to a name of a
		 * field of the revision object.
		 *
		 * Possible hook names include:
		 *
		 *  - `_wp_post_revision_field_post_title`
		 *  - `_wp_post_revision_field_post_content`
		 *  - `_wp_post_revision_field_post_excerpt`
		 *
		 * @since 3.6.0
		 *
		 * @param string  $revision_field The current revision field to compare to or from.
		 * @param string  $field          The current revision field.
		 * @param WP_Post $compare_from   The revision post object to compare to or from.
		 * @param string  $context        The context of whether the current revision is the old
		 *                                or the new one. Either 'to' or 'from'.
		 */
		$content_from = $compare_from ? apply_filters( "_wp_post_revision_field_{$field}", $compare_from->$field, $field, $compare_from, 'from' ) : '';

		/** This filter is documented in wp-admin/includes/revision.php */
		$content_to = apply_filters( "_wp_post_revision_field_{$field}", $compare_to->$field, $field, $compare_to, 'to' );

		$args = array(
			'show_split_view' => true,
			'title_left'      => __( 'Removed' ),
			'title_right'     => __( 'Added' ),
		);

		/**
		 * Filters revisions text diff options.
		 *
		 * Filters the options passed to wp_text_diff() when viewing a post revision.
		 *
		 * @since 4.1.0
		 *
		 * @param array   $args {
		 *     Associative array of options to pass to wp_text_diff().
		 *
		 *     @type bool $show_split_view True for split view (two columns), false for
		 *                                 un-split view (single column). Default true.
		 * }
		 * @param string  $field        The current revision field.
		 * @param WP_Post $compare_from The revision post to compare from.
		 * @param WP_Post $compare_to   The revision post to compare to.
		 */
		$args = apply_filters( 'revision_text_diff_options', $args, $field, $compare_from, $compare_to );

		$diff = wp_text_diff( $content_from, $content_to, $args );

		if ( ! $diff && 'post_title' === $field ) {
			/*
			 * It's a better user experience to still show the Title, even if it didn't change.
			 * No, you didn't see this.
			 */
			$diff = '<table class="diff"><colgroup><col class="content diffsplit left"><col class="content diffsplit middle"><col class="content diffsplit right"></colgroup><tbody><tr>';

			// In split screen mode, show the title before/after side by side.
			if ( true === $args['show_split_view'] ) {
				$diff .= '<td>' . esc_html( $compare_from->post_title ) . '</td><td></td><td>' . esc_html( $compare_to->post_title ) . '</td>';
			} else {
				$diff .= '<td>' . esc_html( $compare_from->post_title ) . '</td>';

				// In single column mode, only show the title once if unchanged.
				if ( $compare_from->post_title !== $compare_to->post_title ) {
					$diff .= '</tr><tr><td>' . esc_html( $compare_to->post_title ) . '</td>';
				}
			}

			$diff .= '</tr></tbody>';
			$diff .= '</table>';
		}

		if ( $diff ) {
			$return[] = array(
				'id'   => $field,
				'name' => $name,
				'diff' => $diff,
			);
		}
	}

	/**
	 * Filters the fields displayed in the post revision diff UI.
	 *
	 * @since 4.1.0
	 *
	 * @param array[] $return       Array of revision UI fields. Each item is an array of id, name, and diff.
	 * @param WP_Post $compare_from The revision post to compare from.
	 * @param WP_Post $compare_to   The revision post to compare to.
	 */
	return apply_filters( 'wp_get_revision_ui_diff', $return, $compare_from, $compare_to );
}

/**
 * Prepare revisions for JavaScript.
 *
 * @since 3.6.0
 *
 * @param WP_Post|int $post                 The post object or post ID.
 * @param int         $selected_revision_id The selected revision ID.
 * @param int         $from                 Optional. The revision ID to compare from.
 * @return array An associative array of revision data and related settings.
 */
function wp_prepare_revisions_for_js( $post, $selected_revision_id, $from = null ) {
	$post    = get_post( $post );
	$authors = array();
	$now_gmt = time();

	$revisions = wp_get_post_revisions(
		$post->ID,
		array(
			'order'         => 'ASC',
			'check_enabled' => false,
		)
	);
	// If revisions are disabled, we only want autosaves and the current post.
	if ( ! wp_revisions_enabled( $post ) ) {
		foreach ( $revisions as $revision_id => $revision ) {
			if ( ! wp_is_post_autosave( $revision ) ) {
				unset( $revisions[ $revision_id ] );
			}
		}
		$revisions = array( $post->ID => $post ) + $revisions;
	}

	$show_avatars = get_option( 'show_avatars' );

	update_post_author_caches( $revisions );

	$can_restore = current_user_can( 'edit_post', $post->ID );
	$current_id  = false;

	foreach ( $revisions as $revision ) {
		$modified     = strtotime( $revision->post_modified );
		$modified_gmt = strtotime( $revision->post_modified_gmt . ' +0000' );
		if ( $can_restore ) {
			$restore_link = str_replace(
				'&amp;',
				'&',
				wp_nonce_url(
					add_query_arg(
						array(
							'revision' => $revision->ID,
							'action'   => 'restore',
						),
						admin_url( 'revision.php' )
					),
					"restore-post_{$revision->ID}"
				)
			);
		}

		if ( ! isset( $authors[ $revision->post_author ] ) ) {
			$authors[ $revision->post_author ] = array(
				'id'     => (int) $revision->post_author,
				'avatar' => $show_avatars ? get_avatar( $revision->post_author, 32 ) : '',
				'name'   => get_the_author_meta( 'display_name', $revision->post_author ),
			);
		}

		$autosave = (bool) wp_is_post_autosave( $revision );
		$current  = ! $autosave && $revision->post_modified_gmt === $post->post_modified_gmt;
		if ( $current && ! empty( $current_id ) ) {
			// If multiple revisions have the same post_modified_gmt, highest ID is current.
			if ( $current_id < $revision->ID ) {
				$revisions[ $current_id ]['current'] = false;
				$current_id                          = $revision->ID;
			} else {
				$current = false;
			}
		} elseif ( $current ) {
			$current_id = $revision->ID;
		}

		$revisions_data = array(
			'id'         => $revision->ID,
			'title'      => get_the_title( $post->ID ),
			'author'     => $authors[ $revision->post_author ],
			'date'       => date_i18n( __( 'M j, Y @ H:i' ), $modified ),
			'dateShort'  => date_i18n( _x( 'j M @ H:i', 'revision date short format' ), $modified ),
			/* translators: %s: Human-readable time difference. */
			'timeAgo'    => sprintf( __( '%s ago' ), human_time_diff( $modified_gmt, $now_gmt ) ),
			'autosave'   => $autosave,
			'current'    => $current,
			'restoreUrl' => $can_restore ? $restore_link : false,
		);

		/**
		 * Filters the array of revisions used on the revisions screen.
		 *
		 * @since 4.4.0
		 *
		 * @param array   $revisions_data {
		 *     The bootstrapped data for the revisions screen.
		 *
		 *     @type int        $id         Revision ID.
		 *     @type string     $title      Title for the revision's parent WP_Post object.
		 *     @type int        $author     Revision post author ID.
		 *     @type string     $date       Date the revision was modified.
		 *     @type string     $dateShort  Short-form version of the date the revision was modified.
		 *     @type string     $timeAgo    GMT-aware amount of time ago the revision was modified.
		 *     @type bool       $autosave   Whether the revision is an autosave.
		 *     @type bool       $current    Whether the revision is both not an autosave and the post
		 *                                  modified date matches the revision modified date (GMT-aware).
		 *     @type bool|false $restoreUrl URL if the revision can be restored, false otherwise.
		 * }
		 * @param WP_Post $revision       The revision's WP_Post object.
		 * @param WP_Post $post           The revision's parent WP_Post object.
		 */
		$revisions[ $revision->ID ] = apply_filters( 'wp_prepare_revision_for_js', $revisions_data, $revision, $post );
	}

	/*
	 * If we only have one revision, the initial revision is missing. This happens
	 * when we have an autosave and the user has clicked 'View the Autosave'.
	 */
	if ( 1 === count( $revisions ) ) {
		$revisions[ $post->ID ] = array(
			'id'         => $post->ID,
			'title'      => get_the_title( $post->ID ),
			'author'     => $authors[ $revision->post_author ],
			'date'       => date_i18n( __( 'M j, Y @ H:i' ), strtotime( $post->post_modified ) ),
			'dateShort'  => date_i18n( _x( 'j M @ H:i', 'revision date short format' ), strtotime( $post->post_modified ) ),
			/* translators: %s: Human-readable time difference. */
			'timeAgo'    => sprintf( __( '%s ago' ), human_time_diff( strtotime( $post->post_modified_gmt ), $now_gmt ) ),
			'autosave'   => false,
			'current'    => true,
			'restoreUrl' => false,
		);
		$current_id             = $post->ID;
	}

	/*
	 * If a post has been saved since the latest revision (no revisioned fields
	 * were changed), we may not have a "current" revision. Mark the latest
	 * revision as "current".
	 */
	if ( empty( $current_id ) ) {
		if ( $revisions[ $revision->ID ]['autosave'] ) {
			$revision = end( $revisions );
			while ( $revision['autosave'] ) {
				$revision = prev( $revisions );
			}
			$current_id = $revision['id'];
		} else {
			$current_id = $revision->ID;
		}
		$revisions[ $current_id ]['current'] = true;
	}

	// Now, grab the initial diff.
	$compare_two_mode = is_numeric( $from );
	if ( ! $compare_two_mode ) {
		$found = array_search( $selected_revision_id, array_keys( $revisions ), true );
		if ( $found ) {
			$from = array_keys( array_slice( $revisions, $found - 1, 1, true ) );
			$from = reset( $from );
		} else {
			$from = 0;
		}
	}

	$from = absint( $from );

	$diffs = array(
		array(
			'id'     => $from . ':' . $selected_revision_id,
			'fields' => wp_get_revision_ui_diff( $post->ID, $from, $selected_revision_id ),
		),
	);

	return array(
		'postId'         => $post->ID,
		'nonce'          => wp_create_nonce( 'revisions-ajax-nonce' ),
		'revisionData'   => array_values( $revisions ),
		'to'             => $selected_revision_id,
		'from'           => $from,
		'diffData'       => $diffs,
		'baseUrl'        => parse_url( admin_url( 'revision.php' ), PHP_URL_PATH ),
		'compareTwoMode' => absint( $compare_two_mode ), // Apparently booleans are not allowed.
		'revisionIds'    => array_keys( $revisions ),
	);
}

/**
 * Print JavaScript templates required for the revisions experience.
 *
 * @since 4.1.0
 *
 * @global WP_Post $post Global post object.
 */
function wp_print_revision_templates() {
	global $post;
	?><script id="tmpl-revisions-frame" type="text/html">
		<div class="revisions-control-frame"></div>
		<div class="revisions-diff-frame"></div>
	</script>

	<script id="tmpl-revisions-buttons" type="text/html">
		<div class="revisions-previous">
			<input class="button" type="button" value="<?php echo esc_attr_x( 'Previous', 'Button label for a previous revision' ); ?>" />
		</div>

		<div class="revisions-next">
			<input class="button" type="button" value="<?php echo esc_attr_x( 'Next', 'Button label for a next revision' ); ?>" />
		</div>
	</script>

	<script id="tmpl-revisions-slider-hidden-help" type="text/html">
		<h2 class="screen-reader-text"><?php esc_html_e( 'Select a revision' ); ?></h2>
		<p id="revisions-slider-hidden-help" hidden><?php esc_html_e( 'Change revision by using the left and right arrow keys' ); ?></p>
	</script>

	<script id="tmpl-revisions-checkbox" type="text/html">
		<div class="revision-toggle-compare-mode">
			<label>
				<input type="checkbox" class="compare-two-revisions"
				<#
				if ( 'undefined' !== typeof data && data.model.attributes.compareTwoMode ) {
					#> checked="checked"<#
				}
				#>
				/>
				<?php esc_html_e( 'Compare any two revisions' ); ?>
			</label>
		</div>
	</script>

	<script id="tmpl-revisions-meta" type="text/html">
		<# if ( ! _.isUndefined( data.attributes ) ) { #>
			<div class="diff-title">
				<# if ( 'from' === data.type ) { #>
					<strong id="diff-title-from"><?php _ex( 'From:', 'Followed by post revision info' ); ?></strong>
				<# } else if ( 'to' === data.type ) { #>
					<strong id="diff-title-to"><?php _ex( 'To:', 'Followed by post revision info' ); ?></strong>
				<# } #>
				<div class="author-card<# if ( data.attributes.autosave ) { #> autosave<# } #>">
					<div>
						{{{ data.attributes.author.avatar }}}
						<div class="author-info" id="diff-title-author">
						<# if ( data.attributes.autosave ) { #>
							<span class="byline">
							<?php
							printf(
								/* translators: %s: User's display name. */
								__( 'Autosave by %s' ),
								'<span class="author-name">{{ data.attributes.author.name }}</span>'
							);
							?>
								</span>
						<# } else if ( data.attributes.current ) { #>
							<span class="byline">
							<?php
							printf(
								/* translators: %s: User's display name. */
								__( 'Current Revision by %s' ),
								'<span class="author-name">{{ data.attributes.author.name }}</span>'
							);
							?>
								</span>
						<# } else { #>
							<span class="byline">
							<?php
							printf(
								/* translators: %s: User's display name. */
								__( 'Revision by %s' ),
								'<span class="author-name">{{ data.attributes.author.name }}</span>'
							);
							?>
								</span>
						<# } #>
							<span class="time-ago">{{ data.attributes.timeAgo }}</span>
							<span class="date">({{ data.attributes.dateShort }})</span>
						</div>
					</div>
				<# if ( 'to' === data.type && data.attributes.restoreUrl ) { #>
					<input  <?php if ( wp_check_post_lock( $post->ID ) ) { ?>
						disabled="disabled"
					<?php } else { ?>
						<# if ( data.attributes.current ) { #>
							disabled="disabled"
						<# } #>
					<?php } ?>
					<# if ( data.attributes.autosave ) { #>
						type="button" class="restore-revision button button-primary" value="<?php esc_attr_e( 'Restore This Autosave' ); ?>" />
					<# } else { #>
						type="button" class="restore-revision button button-primary" value="<?php esc_attr_e( 'Restore This Revision' ); ?>" />
					<# } #>
				<# } #>
			</div>
		<# if ( 'tooltip' === data.type ) { #>
			<div class="revisions-tooltip-arrow"><span></span></div>
		<# } #>
	<# } #>
	</script>

	<script id="tmpl-revisions-diff" type="text/html">
		<div class="loading-indicator"><span class="spinner"></span></div>
		<div class="diff-error"><?php _e( 'An error occurred while loading the comparison. Please refresh the page and try again.' ); ?></div>
		<div class="diff">
		<# _.each( data.fields, function( field ) { #>
			<h2>{{ field.name }}</h2>
			{{{ field.diff }}}
		<# }); #>
		</div>
	</script>
	<?php
}
widgets.php000060400000025240151327343360006731 0ustar00<?php
/**
 * WordPress Widgets Administration API
 *
 * @package WordPress
 * @subpackage Administration
 */

/**
 * Display list of the available widgets.
 *
 * @since 2.5.0
 *
 * @global array $wp_registered_widgets
 * @global array $wp_registered_widget_controls
 */
function wp_list_widgets() {
	global $wp_registered_widgets, $wp_registered_widget_controls;

	$sort = $wp_registered_widgets;
	usort( $sort, '_sort_name_callback' );
	$done = array();

	foreach ( $sort as $widget ) {
		if ( in_array( $widget['callback'], $done, true ) ) { // We already showed this multi-widget.
			continue;
		}

		$sidebar = is_active_widget( $widget['callback'], $widget['id'], false, false );
		$done[]  = $widget['callback'];

		if ( ! isset( $widget['params'][0] ) ) {
			$widget['params'][0] = array();
		}

		$args = array(
			'widget_id'   => $widget['id'],
			'widget_name' => $widget['name'],
			'_display'    => 'template',
		);

		if ( isset( $wp_registered_widget_controls[ $widget['id'] ]['id_base'] ) && isset( $widget['params'][0]['number'] ) ) {
			$id_base            = $wp_registered_widget_controls[ $widget['id'] ]['id_base'];
			$args['_temp_id']   = "$id_base-__i__";
			$args['_multi_num'] = next_widget_id_number( $id_base );
			$args['_add']       = 'multi';
		} else {
			$args['_add'] = 'single';
			if ( $sidebar ) {
				$args['_hide'] = '1';
			}
		}

		$control_args = array(
			0 => $args,
			1 => $widget['params'][0],
		);
		$sidebar_args = wp_list_widget_controls_dynamic_sidebar( $control_args );

		wp_widget_control( ...$sidebar_args );
	}
}

/**
 * Callback to sort array by a 'name' key.
 *
 * @since 3.1.0
 * @access private
 *
 * @param array $a First array.
 * @param array $b Second array.
 * @return int
 */
function _sort_name_callback( $a, $b ) {
	return strnatcasecmp( $a['name'], $b['name'] );
}

/**
 * Show the widgets and their settings for a sidebar.
 * Used in the admin widget config screen.
 *
 * @since 2.5.0
 *
 * @param string $sidebar      Sidebar ID.
 * @param string $sidebar_name Optional. Sidebar name. Default empty.
 */
function wp_list_widget_controls( $sidebar, $sidebar_name = '' ) {
	add_filter( 'dynamic_sidebar_params', 'wp_list_widget_controls_dynamic_sidebar' );

	$description = wp_sidebar_description( $sidebar );

	echo '<div id="' . esc_attr( $sidebar ) . '" class="widgets-sortables">';

	if ( $sidebar_name ) {
		$add_to = sprintf(
			/* translators: %s: Widgets sidebar name. */
			__( 'Add to: %s' ),
			$sidebar_name
		);
		?>
		<div class="sidebar-name" data-add-to="<?php echo esc_attr( $add_to ); ?>">
			<button type="button" class="handlediv hide-if-no-js" aria-expanded="true">
				<span class="screen-reader-text"><?php echo esc_html( $sidebar_name ); ?></span>
				<span class="toggle-indicator" aria-hidden="true"></span>
			</button>
			<h2><?php echo esc_html( $sidebar_name ); ?> <span class="spinner"></span></h2>
		</div>
		<?php
	}

	if ( ! empty( $description ) ) {
		?>
		<div class="sidebar-description">
			<p class="description"><?php echo $description; ?></p>
		</div>
		<?php
	}

	dynamic_sidebar( $sidebar );

	echo '</div>';
}

/**
 * Retrieves the widget control arguments.
 *
 * @since 2.5.0
 *
 * @global array $wp_registered_widgets
 *
 * @param array $params
 * @return array
 */
function wp_list_widget_controls_dynamic_sidebar( $params ) {
	global $wp_registered_widgets;
	static $i = 0;
	++$i;

	$widget_id = $params[0]['widget_id'];
	$id        = isset( $params[0]['_temp_id'] ) ? $params[0]['_temp_id'] : $widget_id;
	$hidden    = isset( $params[0]['_hide'] ) ? ' style="display:none;"' : '';

	$params[0]['before_widget'] = "<div id='widget-{$i}_{$id}' class='widget'$hidden>";
	$params[0]['after_widget']  = '</div>';
	$params[0]['before_title']  = '%BEG_OF_TITLE%'; // Deprecated.
	$params[0]['after_title']   = '%END_OF_TITLE%'; // Deprecated.

	if ( is_callable( $wp_registered_widgets[ $widget_id ]['callback'] ) ) {
		$wp_registered_widgets[ $widget_id ]['_callback'] = $wp_registered_widgets[ $widget_id ]['callback'];
		$wp_registered_widgets[ $widget_id ]['callback']  = 'wp_widget_control';
	}

	return $params;
}

/**
 * @global array $wp_registered_widgets
 *
 * @param string $id_base
 * @return int
 */
function next_widget_id_number( $id_base ) {
	global $wp_registered_widgets;
	$number = 1;

	foreach ( $wp_registered_widgets as $widget_id => $widget ) {
		if ( preg_match( '/' . preg_quote( $id_base, '/' ) . '-([0-9]+)$/', $widget_id, $matches ) ) {
			$number = max( $number, $matches[1] );
		}
	}
	++$number;

	return $number;
}

/**
 * Meta widget used to display the control form for a widget.
 *
 * Called from dynamic_sidebar().
 *
 * @since 2.5.0
 *
 * @global array $wp_registered_widgets
 * @global array $wp_registered_widget_controls
 * @global array $sidebars_widgets
 *
 * @param array $sidebar_args
 * @return array
 */
function wp_widget_control( $sidebar_args ) {
	global $wp_registered_widgets, $wp_registered_widget_controls, $sidebars_widgets;

	$widget_id  = $sidebar_args['widget_id'];
	$sidebar_id = isset( $sidebar_args['id'] ) ? $sidebar_args['id'] : false;
	$key        = $sidebar_id ? array_search( $widget_id, $sidebars_widgets[ $sidebar_id ], true ) : '-1'; // Position of widget in sidebar.
	$control    = isset( $wp_registered_widget_controls[ $widget_id ] ) ? $wp_registered_widget_controls[ $widget_id ] : array();
	$widget     = $wp_registered_widgets[ $widget_id ];

	$id_format     = $widget['id'];
	$widget_number = isset( $control['params'][0]['number'] ) ? $control['params'][0]['number'] : '';
	$id_base       = isset( $control['id_base'] ) ? $control['id_base'] : $widget_id;
	$width         = isset( $control['width'] ) ? $control['width'] : '';
	$height        = isset( $control['height'] ) ? $control['height'] : '';
	$multi_number  = isset( $sidebar_args['_multi_num'] ) ? $sidebar_args['_multi_num'] : '';
	$add_new       = isset( $sidebar_args['_add'] ) ? $sidebar_args['_add'] : '';

	$before_form           = isset( $sidebar_args['before_form'] ) ? $sidebar_args['before_form'] : '<form method="post">';
	$after_form            = isset( $sidebar_args['after_form'] ) ? $sidebar_args['after_form'] : '</form>';
	$before_widget_content = isset( $sidebar_args['before_widget_content'] ) ? $sidebar_args['before_widget_content'] : '<div class="widget-content">';
	$after_widget_content  = isset( $sidebar_args['after_widget_content'] ) ? $sidebar_args['after_widget_content'] : '</div>';

	$query_arg = array( 'editwidget' => $widget['id'] );
	if ( $add_new ) {
		$query_arg['addnew'] = 1;
		if ( $multi_number ) {
			$query_arg['num']  = $multi_number;
			$query_arg['base'] = $id_base;
		}
	} else {
		$query_arg['sidebar'] = $sidebar_id;
		$query_arg['key']     = $key;
	}

	/*
	 * We aren't showing a widget control, we're outputting a template
	 * for a multi-widget control.
	 */
	if ( isset( $sidebar_args['_display'] ) && 'template' === $sidebar_args['_display'] && $widget_number ) {
		// number == -1 implies a template where id numbers are replaced by a generic '__i__'.
		$control['params'][0]['number'] = -1;
		// With id_base widget ID's are constructed like {$id_base}-{$id_number}.
		if ( isset( $control['id_base'] ) ) {
			$id_format = $control['id_base'] . '-__i__';
		}
	}

	$wp_registered_widgets[ $widget_id ]['callback'] = $wp_registered_widgets[ $widget_id ]['_callback'];
	unset( $wp_registered_widgets[ $widget_id ]['_callback'] );

	$widget_title = esc_html( strip_tags( $sidebar_args['widget_name'] ) );
	$has_form     = 'noform';

	echo $sidebar_args['before_widget'];
	?>
	<div class="widget-top">
	<div class="widget-title-action">
		<button type="button" class="widget-action hide-if-no-js" aria-expanded="false">
			<span class="screen-reader-text edit">
				<?php
				/* translators: Hidden accessibility text. %s: Widget title. */
				printf( __( 'Edit widget: %s' ), $widget_title );
				?>
			</span>
			<span class="screen-reader-text add">
				<?php
				/* translators: Hidden accessibility text. %s: Widget title. */
				printf( __( 'Add widget: %s' ), $widget_title );
				?>
			</span>
			<span class="toggle-indicator" aria-hidden="true"></span>
		</button>
		<a class="widget-control-edit hide-if-js" href="<?php echo esc_url( add_query_arg( $query_arg ) ); ?>">
			<span class="edit"><?php _ex( 'Edit', 'widget' ); ?></span>
			<span class="add"><?php _ex( 'Add', 'widget' ); ?></span>
			<span class="screen-reader-text"><?php echo $widget_title; ?></span>
		</a>
	</div>
	<div class="widget-title"><h3><?php echo $widget_title; ?><span class="in-widget-title"></span></h3></div>
	</div>

	<div class="widget-inside">
	<?php echo $before_form; ?>
	<?php echo $before_widget_content; ?>
	<?php
	if ( isset( $control['callback'] ) ) {
		$has_form = call_user_func_array( $control['callback'], $control['params'] );
	} else {
		echo "\t\t<p>" . __( 'There are no options for this widget.' ) . "</p>\n";
	}

	$noform_class = '';
	if ( 'noform' === $has_form ) {
		$noform_class = ' widget-control-noform';
	}
	?>
	<?php echo $after_widget_content; ?>
	<input type="hidden" name="widget-id" class="widget-id" value="<?php echo esc_attr( $id_format ); ?>" />
	<input type="hidden" name="id_base" class="id_base" value="<?php echo esc_attr( $id_base ); ?>" />
	<input type="hidden" name="widget-width" class="widget-width" value="<?php echo esc_attr( $width ); ?>" />
	<input type="hidden" name="widget-height" class="widget-height" value="<?php echo esc_attr( $height ); ?>" />
	<input type="hidden" name="widget_number" class="widget_number" value="<?php echo esc_attr( $widget_number ); ?>" />
	<input type="hidden" name="multi_number" class="multi_number" value="<?php echo esc_attr( $multi_number ); ?>" />
	<input type="hidden" name="add_new" class="add_new" value="<?php echo esc_attr( $add_new ); ?>" />

	<div class="widget-control-actions">
		<div class="alignleft">
			<button type="button" class="button-link button-link-delete widget-control-remove"><?php _e( 'Delete' ); ?></button>
			<span class="widget-control-close-wrapper">
				| <button type="button" class="button-link widget-control-close"><?php _e( 'Done' ); ?></button>
			</span>
		</div>
		<div class="alignright<?php echo $noform_class; ?>">
			<?php submit_button( __( 'Save' ), 'primary widget-control-save right', 'savewidget', false, array( 'id' => 'widget-' . esc_attr( $id_format ) . '-savewidget' ) ); ?>
			<span class="spinner"></span>
		</div>
		<br class="clear" />
	</div>
	<?php echo $after_form; ?>
	</div>

	<div class="widget-description">
	<?php
	$widget_description = wp_widget_description( $widget_id );
	echo ( $widget_description ) ? "$widget_description\n" : "$widget_title\n";
	?>
	</div>
	<?php
	echo $sidebar_args['after_widget'];

	return $sidebar_args;
}

/**
 * @param string $classes
 * @return string
 */
function wp_widgets_access_body_class( $classes ) {
	return "$classes widgets_access ";
}
class-bulk-plugin-upgrader-skin.php000060400000005036151327343360013371 0ustar00<?php
/**
 * Upgrader API: Bulk_Plugin_Upgrader_Skin class
 *
 * @package WordPress
 * @subpackage Upgrader
 * @since 4.6.0
 */

/**
 * Bulk Plugin Upgrader Skin for WordPress Plugin Upgrades.
 *
 * @since 3.0.0
 * @since 4.6.0 Moved to its own file from wp-admin/includes/class-wp-upgrader-skins.php.
 *
 * @see Bulk_Upgrader_Skin
 */
class Bulk_Plugin_Upgrader_Skin extends Bulk_Upgrader_Skin {

	/**
	 * Plugin info.
	 *
	 * The Plugin_Upgrader::bulk_upgrade() method will fill this in
	 * with info retrieved from the get_plugin_data() function.
	 *
	 * @since 3.0.0
	 * @var array Plugin data. Values will be empty if not supplied by the plugin.
	 */
	public $plugin_info = array();

	/**
	 * Sets up the strings used in the update process.
	 *
	 * @since 3.0.0
	 */
	public function add_strings() {
		parent::add_strings();
		/* translators: 1: Plugin name, 2: Number of the plugin, 3: Total number of plugins being updated. */
		$this->upgrader->strings['skin_before_update_header'] = __( 'Updating Plugin %1$s (%2$d/%3$d)' );
	}

	/**
	 * Performs an action before a bulk plugin update.
	 *
	 * @since 3.0.0
	 *
	 * @param string $title
	 */
	public function before( $title = '' ) {
		parent::before( $this->plugin_info['Title'] );
	}

	/**
	 * Performs an action following a bulk plugin update.
	 *
	 * @since 3.0.0
	 *
	 * @param string $title
	 */
	public function after( $title = '' ) {
		parent::after( $this->plugin_info['Title'] );
		$this->decrement_update_count( 'plugin' );
	}

	/**
	 * Displays the footer following the bulk update process.
	 *
	 * @since 3.0.0
	 */
	public function bulk_footer() {
		parent::bulk_footer();

		$update_actions = array(
			'plugins_page' => sprintf(
				'<a href="%s" target="_parent">%s</a>',
				self_admin_url( 'plugins.php' ),
				__( 'Go to Plugins page' )
			),
			'updates_page' => sprintf(
				'<a href="%s" target="_parent">%s</a>',
				self_admin_url( 'update-core.php' ),
				__( 'Go to WordPress Updates page' )
			),
		);

		if ( ! current_user_can( 'activate_plugins' ) ) {
			unset( $update_actions['plugins_page'] );
		}

		/**
		 * Filters the list of action links available following bulk plugin updates.
		 *
		 * @since 3.0.0
		 *
		 * @param string[] $update_actions Array of plugin action links.
		 * @param array    $plugin_info    Array of information for the last-updated plugin.
		 */
		$update_actions = apply_filters( 'update_bulk_plugins_complete_actions', $update_actions, $this->plugin_info );

		if ( ! empty( $update_actions ) ) {
			$this->feedback( implode( ' | ', (array) $update_actions ) );
		}
	}
}
class-plugin-upgrader-skin.php000060400000006316151327343360012440 0ustar00<?php
/**
 * Upgrader API: Plugin_Upgrader_Skin class
 *
 * @package WordPress
 * @subpackage Upgrader
 * @since 4.6.0
 */

/**
 * Plugin Upgrader Skin for WordPress Plugin Upgrades.
 *
 * @since 2.8.0
 * @since 4.6.0 Moved to its own file from wp-admin/includes/class-wp-upgrader-skins.php.
 *
 * @see WP_Upgrader_Skin
 */
class Plugin_Upgrader_Skin extends WP_Upgrader_Skin {

	/**
	 * Holds the plugin slug in the Plugin Directory.
	 *
	 * @since 2.8.0
	 *
	 * @var string
	 */
	public $plugin = '';

	/**
	 * Whether the plugin is active.
	 *
	 * @since 2.8.0
	 *
	 * @var bool
	 */
	public $plugin_active = false;

	/**
	 * Whether the plugin is active for the entire network.
	 *
	 * @since 2.8.0
	 *
	 * @var bool
	 */
	public $plugin_network_active = false;

	/**
	 * Constructor.
	 *
	 * Sets up the plugin upgrader skin.
	 *
	 * @since 2.8.0
	 *
	 * @param array $args Optional. The plugin upgrader skin arguments to
	 *                    override default options. Default empty array.
	 */
	public function __construct( $args = array() ) {
		$defaults = array(
			'url'    => '',
			'plugin' => '',
			'nonce'  => '',
			'title'  => __( 'Update Plugin' ),
		);
		$args     = wp_parse_args( $args, $defaults );

		$this->plugin = $args['plugin'];

		$this->plugin_active         = is_plugin_active( $this->plugin );
		$this->plugin_network_active = is_plugin_active_for_network( $this->plugin );

		parent::__construct( $args );
	}

	/**
	 * Performs an action following a single plugin update.
	 *
	 * @since 2.8.0
	 */
	public function after() {
		$this->plugin = $this->upgrader->plugin_info();
		if ( ! empty( $this->plugin ) && ! is_wp_error( $this->result ) && $this->plugin_active ) {
			// Currently used only when JS is off for a single plugin update?
			printf(
				'<iframe title="%s" style="border:0;overflow:hidden" width="100%%" height="170" src="%s"></iframe>',
				esc_attr__( 'Update progress' ),
				wp_nonce_url( 'update.php?action=activate-plugin&networkwide=' . $this->plugin_network_active . '&plugin=' . urlencode( $this->plugin ), 'activate-plugin_' . $this->plugin )
			);
		}

		$this->decrement_update_count( 'plugin' );

		$update_actions = array(
			'activate_plugin' => sprintf(
				'<a href="%s" target="_parent">%s</a>',
				wp_nonce_url( 'plugins.php?action=activate&amp;plugin=' . urlencode( $this->plugin ), 'activate-plugin_' . $this->plugin ),
				__( 'Activate Plugin' )
			),
			'plugins_page'    => sprintf(
				'<a href="%s" target="_parent">%s</a>',
				self_admin_url( 'plugins.php' ),
				__( 'Go to Plugins page' )
			),
		);

		if ( $this->plugin_active || ! $this->result || is_wp_error( $this->result ) || ! current_user_can( 'activate_plugin', $this->plugin ) ) {
			unset( $update_actions['activate_plugin'] );
		}

		/**
		 * Filters the list of action links available following a single plugin update.
		 *
		 * @since 2.7.0
		 *
		 * @param string[] $update_actions Array of plugin action links.
		 * @param string   $plugin         Path to the plugin file relative to the plugins directory.
		 */
		$update_actions = apply_filters( 'update_plugin_complete_actions', $update_actions, $this->plugin );

		if ( ! empty( $update_actions ) ) {
			$this->feedback( implode( ' | ', (array) $update_actions ) );
		}
	}
}
import.php000060400000014731151327343360006600 0ustar00<?php
/**
 * WordPress Administration Importer API.
 *
 * @package WordPress
 * @subpackage Administration
 */

/**
 * Retrieves the list of importers.
 *
 * @since 2.0.0
 *
 * @global array $wp_importers
 * @return array
 */
function get_importers() {
	global $wp_importers;
	if ( is_array( $wp_importers ) ) {
		uasort( $wp_importers, '_usort_by_first_member' );
	}
	return $wp_importers;
}

/**
 * Sorts a multidimensional array by first member of each top level member.
 *
 * Used by uasort() as a callback, should not be used directly.
 *
 * @since 2.9.0
 * @access private
 *
 * @param array $a
 * @param array $b
 * @return int
 */
function _usort_by_first_member( $a, $b ) {
	return strnatcasecmp( $a[0], $b[0] );
}

/**
 * Registers importer for WordPress.
 *
 * @since 2.0.0
 *
 * @global array $wp_importers
 *
 * @param string   $id          Importer tag. Used to uniquely identify importer.
 * @param string   $name        Importer name and title.
 * @param string   $description Importer description.
 * @param callable $callback    Callback to run.
 * @return void|WP_Error Void on success. WP_Error when $callback is WP_Error.
 */
function register_importer( $id, $name, $description, $callback ) {
	global $wp_importers;
	if ( is_wp_error( $callback ) ) {
		return $callback;
	}
	$wp_importers[ $id ] = array( $name, $description, $callback );
}

/**
 * Cleanup importer.
 *
 * Removes attachment based on ID.
 *
 * @since 2.0.0
 *
 * @param string $id Importer ID.
 */
function wp_import_cleanup( $id ) {
	wp_delete_attachment( $id );
}

/**
 * Handles importer uploading and adds attachment.
 *
 * @since 2.0.0
 *
 * @return array Uploaded file's details on success, error message on failure.
 */
function wp_import_handle_upload() {
	if ( ! isset( $_FILES['import'] ) ) {
		return array(
			'error' => sprintf(
				/* translators: 1: php.ini, 2: post_max_size, 3: upload_max_filesize */
				__( 'File is empty. Please upload something more substantial. This error could also be caused by uploads being disabled in your %1$s file or by %2$s being defined as smaller than %3$s in %1$s.' ),
				'php.ini',
				'post_max_size',
				'upload_max_filesize'
			),
		);
	}

	$overrides                 = array(
		'test_form' => false,
		'test_type' => false,
	);
	$_FILES['import']['name'] .= '.txt';
	$upload                    = wp_handle_upload( $_FILES['import'], $overrides );

	if ( isset( $upload['error'] ) ) {
		return $upload;
	}

	// Construct the attachment array.
	$attachment = array(
		'post_title'     => wp_basename( $upload['file'] ),
		'post_content'   => $upload['url'],
		'post_mime_type' => $upload['type'],
		'guid'           => $upload['url'],
		'context'        => 'import',
		'post_status'    => 'private',
	);

	// Save the data.
	$id = wp_insert_attachment( $attachment, $upload['file'] );

	/*
	 * Schedule a cleanup for one day from now in case of failed
	 * import or missing wp_import_cleanup() call.
	 */
	wp_schedule_single_event( time() + DAY_IN_SECONDS, 'importer_scheduled_cleanup', array( $id ) );

	return array(
		'file' => $upload['file'],
		'id'   => $id,
	);
}

/**
 * Returns a list from WordPress.org of popular importer plugins.
 *
 * @since 3.5.0
 *
 * @return array Importers with metadata for each.
 */
function wp_get_popular_importers() {
	$locale            = get_user_locale();
	$cache_key         = 'popular_importers_' . md5( $locale . wp_get_wp_version() );
	$popular_importers = get_site_transient( $cache_key );

	if ( ! $popular_importers ) {
		$url     = add_query_arg(
			array(
				'locale'  => $locale,
				'version' => wp_get_wp_version(),
			),
			'http://api.wordpress.org/core/importers/1.1/'
		);
		$options = array( 'user-agent' => 'WordPress/' . wp_get_wp_version() . '; ' . home_url( '/' ) );

		if ( wp_http_supports( array( 'ssl' ) ) ) {
			$url = set_url_scheme( $url, 'https' );
		}

		$response          = wp_remote_get( $url, $options );
		$popular_importers = json_decode( wp_remote_retrieve_body( $response ), true );

		if ( is_array( $popular_importers ) ) {
			set_site_transient( $cache_key, $popular_importers, 2 * DAY_IN_SECONDS );
		} else {
			$popular_importers = false;
		}
	}

	if ( is_array( $popular_importers ) ) {
		// If the data was received as translated, return it as-is.
		if ( $popular_importers['translated'] ) {
			return $popular_importers['importers'];
		}

		foreach ( $popular_importers['importers'] as &$importer ) {
			// phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText
			$importer['description'] = translate( $importer['description'] );
			if ( 'WordPress' !== $importer['name'] ) {
				// phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText
				$importer['name'] = translate( $importer['name'] );
			}
		}
		return $popular_importers['importers'];
	}

	return array(
		// slug => name, description, plugin slug, and register_importer() slug.
		'blogger'     => array(
			'name'        => __( 'Blogger' ),
			'description' => __( 'Import posts, comments, and users from a Blogger blog.' ),
			'plugin-slug' => 'blogger-importer',
			'importer-id' => 'blogger',
		),
		'wpcat2tag'   => array(
			'name'        => __( 'Categories and Tags Converter' ),
			'description' => __( 'Convert existing categories to tags or tags to categories, selectively.' ),
			'plugin-slug' => 'wpcat2tag-importer',
			'importer-id' => 'wp-cat2tag',
		),
		'livejournal' => array(
			'name'        => __( 'LiveJournal' ),
			'description' => __( 'Import posts from LiveJournal using their API.' ),
			'plugin-slug' => 'livejournal-importer',
			'importer-id' => 'livejournal',
		),
		'movabletype' => array(
			'name'        => __( 'Movable Type and TypePad' ),
			'description' => __( 'Import posts and comments from a Movable Type or TypePad blog.' ),
			'plugin-slug' => 'movabletype-importer',
			'importer-id' => 'mt',
		),
		'rss'         => array(
			'name'        => __( 'RSS' ),
			'description' => __( 'Import posts from an RSS feed.' ),
			'plugin-slug' => 'rss-importer',
			'importer-id' => 'rss',
		),
		'tumblr'      => array(
			'name'        => __( 'Tumblr' ),
			'description' => __( 'Import posts &amp; media from Tumblr using their API.' ),
			'plugin-slug' => 'tumblr-importer',
			'importer-id' => 'tumblr',
		),
		'wordpress'   => array(
			'name'        => 'WordPress',
			'description' => __( 'Import posts, pages, comments, custom fields, categories, and tags from a WordPress export file.' ),
			'plugin-slug' => 'wordpress-importer',
			'importer-id' => 'wordpress',
		),
	);
}
class-wp-automatic-updater.php000064400000170716151327343360012457 0ustar00<?php
/**
 * Upgrade API: WP_Automatic_Updater class
 *
 * @package WordPress
 * @subpackage Upgrader
 * @since 4.6.0
 */

/**
 * Core class used for handling automatic background updates.
 *
 * @since 3.7.0
 * @since 4.6.0 Moved to its own file from wp-admin/includes/class-wp-upgrader.php.
 */
#[AllowDynamicProperties]
class WP_Automatic_Updater {

	/**
	 * Tracks update results during processing.
	 *
	 * @var array
	 */
	protected $update_results = array();

	/**
	 * Determines whether the entire automatic updater is disabled.
	 *
	 * @since 3.7.0
	 *
	 * @return bool True if the automatic updater is disabled, false otherwise.
	 */
	public function is_disabled() {
		// Background updates are disabled if you don't want file changes.
		if ( ! wp_is_file_mod_allowed( 'automatic_updater' ) ) {
			return true;
		}

		if ( wp_installing() ) {
			return true;
		}

		// More fine grained control can be done through the WP_AUTO_UPDATE_CORE constant and filters.
		$disabled = defined( 'AUTOMATIC_UPDATER_DISABLED' ) && AUTOMATIC_UPDATER_DISABLED;

		/**
		 * Filters whether to entirely disable background updates.
		 *
		 * There are more fine-grained filters and controls for selective disabling.
		 * This filter parallels the AUTOMATIC_UPDATER_DISABLED constant in name.
		 *
		 * This also disables update notification emails. That may change in the future.
		 *
		 * @since 3.7.0
		 *
		 * @param bool $disabled Whether the updater should be disabled.
		 */
		return apply_filters( 'automatic_updater_disabled', $disabled );
	}

	/**
	 * Checks whether access to a given directory is allowed.
	 *
	 * This is used when detecting version control checkouts. Takes into account
	 * the PHP `open_basedir` restrictions, so that WordPress does not try to access
	 * directories it is not allowed to.
	 *
	 * @since 6.2.0
	 *
	 * @param string $dir The directory to check.
	 * @return bool True if access to the directory is allowed, false otherwise.
	 */
	public function is_allowed_dir( $dir ) {
		if ( is_string( $dir ) ) {
			$dir = trim( $dir );
		}

		if ( ! is_string( $dir ) || '' === $dir ) {
			_doing_it_wrong(
				__METHOD__,
				sprintf(
					/* translators: %s: The "$dir" argument. */
					__( 'The "%s" argument must be a non-empty string.' ),
					'$dir'
				),
				'6.2.0'
			);

			return false;
		}

		$open_basedir = ini_get( 'open_basedir' );

		if ( empty( $open_basedir ) ) {
			return true;
		}

		$open_basedir_list = explode( PATH_SEPARATOR, $open_basedir );

		foreach ( $open_basedir_list as $basedir ) {
			if ( '' !== trim( $basedir ) && str_starts_with( $dir, $basedir ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Checks for version control checkouts.
	 *
	 * Checks for Subversion, Git, Mercurial, and Bazaar. It recursively looks up the
	 * filesystem to the top of the drive, erring on the side of detecting a VCS
	 * checkout somewhere.
	 *
	 * ABSPATH is always checked in addition to whatever `$context` is (which may be the
	 * wp-content directory, for example). The underlying assumption is that if you are
	 * using version control *anywhere*, then you should be making decisions for
	 * how things get updated.
	 *
	 * @since 3.7.0
	 *
	 * @param string $context The filesystem path to check, in addition to ABSPATH.
	 * @return bool True if a VCS checkout was discovered at `$context` or ABSPATH,
	 *              or anywhere higher. False otherwise.
	 */
	public function is_vcs_checkout( $context ) {
		$context_dirs = array( untrailingslashit( $context ) );
		if ( ABSPATH !== $context ) {
			$context_dirs[] = untrailingslashit( ABSPATH );
		}

		$vcs_dirs   = array( '.svn', '.git', '.hg', '.bzr' );
		$check_dirs = array();

		foreach ( $context_dirs as $context_dir ) {
			// Walk up from $context_dir to the root.
			do {
				$check_dirs[] = $context_dir;

				// Once we've hit '/' or 'C:\', we need to stop. dirname will keep returning the input here.
				if ( dirname( $context_dir ) === $context_dir ) {
					break;
				}

				// Continue one level at a time.
			} while ( $context_dir = dirname( $context_dir ) );
		}

		$check_dirs = array_unique( $check_dirs );
		$checkout   = false;

		// Search all directories we've found for evidence of version control.
		foreach ( $vcs_dirs as $vcs_dir ) {
			foreach ( $check_dirs as $check_dir ) {
				if ( ! $this->is_allowed_dir( $check_dir ) ) {
					continue;
				}

				$checkout = is_dir( rtrim( $check_dir, '\\/' ) . "/$vcs_dir" );
				if ( $checkout ) {
					break 2;
				}
			}
		}

		/**
		 * Filters whether the automatic updater should consider a filesystem
		 * location to be potentially managed by a version control system.
		 *
		 * @since 3.7.0
		 *
		 * @param bool $checkout  Whether a VCS checkout was discovered at `$context`
		 *                        or ABSPATH, or anywhere higher.
		 * @param string $context The filesystem context (a path) against which
		 *                        filesystem status should be checked.
		 */
		return apply_filters( 'automatic_updates_is_vcs_checkout', $checkout, $context );
	}

	/**
	 * Tests to see if we can and should update a specific item.
	 *
	 * @since 3.7.0
	 *
	 * @global wpdb $wpdb WordPress database abstraction object.
	 *
	 * @param string $type    The type of update being checked: 'core', 'theme',
	 *                        'plugin', 'translation'.
	 * @param object $item    The update offer.
	 * @param string $context The filesystem context (a path) against which filesystem
	 *                        access and status should be checked.
	 * @return bool True if the item should be updated, false otherwise.
	 */
	public function should_update( $type, $item, $context ) {
		// Used to see if WP_Filesystem is set up to allow unattended updates.
		$skin = new Automatic_Upgrader_Skin();

		if ( $this->is_disabled() ) {
			return false;
		}

		// Only relax the filesystem checks when the update doesn't include new files.
		$allow_relaxed_file_ownership = false;
		if ( 'core' === $type && isset( $item->new_files ) && ! $item->new_files ) {
			$allow_relaxed_file_ownership = true;
		}

		// If we can't do an auto core update, we may still be able to email the user.
		if ( ! $skin->request_filesystem_credentials( false, $context, $allow_relaxed_file_ownership )
			|| $this->is_vcs_checkout( $context )
		) {
			if ( 'core' === $type ) {
				$this->send_core_update_notification_email( $item );
			}
			return false;
		}

		// Next up, is this an item we can update?
		if ( 'core' === $type ) {
			$update = Core_Upgrader::should_update_to_version( $item->current );
		} elseif ( 'plugin' === $type || 'theme' === $type ) {
			$update = ! empty( $item->autoupdate );

			if ( ! $update && wp_is_auto_update_enabled_for_type( $type ) ) {
				// Check if the site admin has enabled auto-updates by default for the specific item.
				$auto_updates = (array) get_site_option( "auto_update_{$type}s", array() );
				$update       = in_array( $item->{$type}, $auto_updates, true );
			}
		} else {
			$update = ! empty( $item->autoupdate );
		}

		// If the `disable_autoupdate` flag is set, override any user-choice, but allow filters.
		if ( ! empty( $item->disable_autoupdate ) ) {
			$update = false;
		}

		/**
		 * Filters whether to automatically update core, a plugin, a theme, or a language.
		 *
		 * The dynamic portion of the hook name, `$type`, refers to the type of update
		 * being checked.
		 *
		 * Possible hook names include:
		 *
		 *  - `auto_update_core`
		 *  - `auto_update_plugin`
		 *  - `auto_update_theme`
		 *  - `auto_update_translation`
		 *
		 * Since WordPress 3.7, minor and development versions of core, and translations have
		 * been auto-updated by default. New installs on WordPress 5.6 or higher will also
		 * auto-update major versions by default. Starting in 5.6, older sites can opt-in to
		 * major version auto-updates, and auto-updates for plugins and themes.
		 *
		 * See the {@see 'allow_dev_auto_core_updates'}, {@see 'allow_minor_auto_core_updates'},
		 * and {@see 'allow_major_auto_core_updates'} filters for a more straightforward way to
		 * adjust core updates.
		 *
		 * @since 3.7.0
		 * @since 5.5.0 The `$update` parameter accepts the value of null.
		 *
		 * @param bool|null $update Whether to update. The value of null is internally used
		 *                          to detect whether nothing has hooked into this filter.
		 * @param object    $item   The update offer.
		 */
		$update = apply_filters( "auto_update_{$type}", $update, $item );

		if ( ! $update ) {
			if ( 'core' === $type ) {
				$this->send_core_update_notification_email( $item );
			}
			return false;
		}

		// If it's a core update, are we actually compatible with its requirements?
		if ( 'core' === $type ) {
			global $wpdb;

			$php_compat = version_compare( PHP_VERSION, $item->php_version, '>=' );
			if ( file_exists( WP_CONTENT_DIR . '/db.php' ) && empty( $wpdb->is_mysql ) ) {
				$mysql_compat = true;
			} else {
				$mysql_compat = version_compare( $wpdb->db_version(), $item->mysql_version, '>=' );
			}

			if ( ! $php_compat || ! $mysql_compat ) {
				return false;
			}
		}

		// If updating a plugin or theme, ensure the minimum PHP version requirements are satisfied.
		if ( in_array( $type, array( 'plugin', 'theme' ), true ) ) {
			if ( ! empty( $item->requires_php ) && version_compare( PHP_VERSION, $item->requires_php, '<' ) ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Notifies an administrator of a core update.
	 *
	 * @since 3.7.0
	 *
	 * @param object $item The update offer.
	 * @return bool True if the site administrator is notified of a core update,
	 *              false otherwise.
	 */
	protected function send_core_update_notification_email( $item ) {
		$notified = get_site_option( 'auto_core_update_notified' );

		// Don't notify if we've already notified the same email address of the same version.
		if ( $notified
			&& get_site_option( 'admin_email' ) === $notified['email']
			&& $notified['version'] === $item->current
		) {
			return false;
		}

		// See if we need to notify users of a core update.
		$notify = ! empty( $item->notify_email );

		/**
		 * Filters whether to notify the site administrator of a new core update.
		 *
		 * By default, administrators are notified when the update offer received
		 * from WordPress.org sets a particular flag. This allows some discretion
		 * in if and when to notify.
		 *
		 * This filter is only evaluated once per release. If the same email address
		 * was already notified of the same new version, WordPress won't repeatedly
		 * email the administrator.
		 *
		 * This filter is also used on about.php to check if a plugin has disabled
		 * these notifications.
		 *
		 * @since 3.7.0
		 *
		 * @param bool   $notify Whether the site administrator is notified.
		 * @param object $item   The update offer.
		 */
		if ( ! apply_filters( 'send_core_update_notification_email', $notify, $item ) ) {
			return false;
		}

		$this->send_email( 'manual', $item );
		return true;
	}

	/**
	 * Updates an item, if appropriate.
	 *
	 * @since 3.7.0
	 *
	 * @param string $type The type of update being checked: 'core', 'theme', 'plugin', 'translation'.
	 * @param object $item The update offer.
	 * @return null|WP_Error
	 */
	public function update( $type, $item ) {
		$skin = new Automatic_Upgrader_Skin();

		switch ( $type ) {
			case 'core':
				// The Core upgrader doesn't use the Upgrader's skin during the actual main part of the upgrade, instead, firing a filter.
				add_filter( 'update_feedback', array( $skin, 'feedback' ) );
				$upgrader = new Core_Upgrader( $skin );
				$context  = ABSPATH;
				break;
			case 'plugin':
				$upgrader = new Plugin_Upgrader( $skin );
				$context  = WP_PLUGIN_DIR; // We don't support custom Plugin directories, or updates for WPMU_PLUGIN_DIR.
				break;
			case 'theme':
				$upgrader = new Theme_Upgrader( $skin );
				$context  = get_theme_root( $item->theme );
				break;
			case 'translation':
				$upgrader = new Language_Pack_Upgrader( $skin );
				$context  = WP_CONTENT_DIR; // WP_LANG_DIR;
				break;
		}

		// Determine whether we can and should perform this update.
		if ( ! $this->should_update( $type, $item, $context ) ) {
			return false;
		}

		/**
		 * Fires immediately prior to an auto-update.
		 *
		 * @since 4.4.0
		 *
		 * @param string $type    The type of update being checked: 'core', 'theme', 'plugin', or 'translation'.
		 * @param object $item    The update offer.
		 * @param string $context The filesystem context (a path) against which filesystem access and status
		 *                        should be checked.
		 */
		do_action( 'pre_auto_update', $type, $item, $context );

		$upgrader_item = $item;
		switch ( $type ) {
			case 'core':
				/* translators: %s: WordPress version. */
				$skin->feedback( __( 'Updating to WordPress %s' ), $item->version );
				/* translators: %s: WordPress version. */
				$item_name = sprintf( __( 'WordPress %s' ), $item->version );
				break;
			case 'theme':
				$upgrader_item = $item->theme;
				$theme         = wp_get_theme( $upgrader_item );
				$item_name     = $theme->Get( 'Name' );
				// Add the current version so that it can be reported in the notification email.
				$item->current_version = $theme->get( 'Version' );
				if ( empty( $item->current_version ) ) {
					$item->current_version = false;
				}
				/* translators: %s: Theme name. */
				$skin->feedback( __( 'Updating theme: %s' ), $item_name );
				break;
			case 'plugin':
				$upgrader_item = $item->plugin;
				$plugin_data   = get_plugin_data( $context . '/' . $upgrader_item );
				$item_name     = $plugin_data['Name'];
				// Add the current version so that it can be reported in the notification email.
				$item->current_version = $plugin_data['Version'];
				if ( empty( $item->current_version ) ) {
					$item->current_version = false;
				}
				/* translators: %s: Plugin name. */
				$skin->feedback( __( 'Updating plugin: %s' ), $item_name );
				break;
			case 'translation':
				$language_item_name = $upgrader->get_name_for_update( $item );
				/* translators: %s: Project name (plugin, theme, or WordPress). */
				$item_name = sprintf( __( 'Translations for %s' ), $language_item_name );
				/* translators: 1: Project name (plugin, theme, or WordPress), 2: Language. */
				$skin->feedback( sprintf( __( 'Updating translations for %1$s (%2$s)&#8230;' ), $language_item_name, $item->language ) );
				break;
		}

		$allow_relaxed_file_ownership = false;
		if ( 'core' === $type && isset( $item->new_files ) && ! $item->new_files ) {
			$allow_relaxed_file_ownership = true;
		}

		$is_debug = WP_DEBUG && WP_DEBUG_LOG;
		if ( 'plugin' === $type ) {
			$was_active = is_plugin_active( $upgrader_item );
			if ( $is_debug ) {
				error_log( '    Upgrading plugin ' . var_export( $item->slug, true ) . '...' );
			}
		}

		if ( 'theme' === $type && $is_debug ) {
			error_log( '    Upgrading theme ' . var_export( $item->theme, true ) . '...' );
		}

		/*
		 * Enable maintenance mode before upgrading the plugin or theme.
		 *
		 * This avoids potential non-fatal errors being detected
		 * while scraping for a fatal error if some files are still
		 * being moved.
		 *
		 * While these checks are intended only for plugins,
		 * maintenance mode is enabled for all upgrade types as any
		 * update could contain an error or warning, which could cause
		 * the scrape to miss a fatal error in the plugin update.
		 */
		if ( 'translation' !== $type ) {
			$upgrader->maintenance_mode( true );
		}

		// Boom, this site's about to get a whole new splash of paint!
		$upgrade_result = $upgrader->upgrade(
			$upgrader_item,
			array(
				'clear_update_cache'           => false,
				// Always use partial builds if possible for core updates.
				'pre_check_md5'                => false,
				// Only available for core updates.
				'attempt_rollback'             => true,
				// Allow relaxed file ownership in some scenarios.
				'allow_relaxed_file_ownership' => $allow_relaxed_file_ownership,
			)
		);

		/*
		 * After WP_Upgrader::upgrade() completes, maintenance mode is disabled.
		 *
		 * Re-enable maintenance mode while attempting to detect fatal errors
		 * and potentially rolling back.
		 *
		 * This avoids errors if the site is visited while fatal errors exist
		 * or while files are still being moved.
		 */
		if ( 'translation' !== $type ) {
			$upgrader->maintenance_mode( true );
		}

		// If the filesystem is unavailable, false is returned.
		if ( false === $upgrade_result ) {
			$upgrade_result = new WP_Error( 'fs_unavailable', __( 'Could not access filesystem.' ) );
		}

		if ( 'core' === $type ) {
			if ( is_wp_error( $upgrade_result )
				&& ( 'up_to_date' === $upgrade_result->get_error_code()
					|| 'locked' === $upgrade_result->get_error_code() )
			) {
				// Allow visitors to browse the site again.
				$upgrader->maintenance_mode( false );

				/*
				 * These aren't actual errors, treat it as a skipped-update instead
				 * to avoid triggering the post-core update failure routines.
				 */
				return false;
			}

			// Core doesn't output this, so let's append it, so we don't get confused.
			if ( is_wp_error( $upgrade_result ) ) {
				$upgrade_result->add( 'installation_failed', __( 'Installation failed.' ) );
				$skin->error( $upgrade_result );
			} else {
				$skin->feedback( __( 'WordPress updated successfully.' ) );
			}
		}

		$is_debug = WP_DEBUG && WP_DEBUG_LOG;

		if ( 'theme' === $type && $is_debug ) {
			error_log( '    Theme ' . var_export( $item->theme, true ) . ' has been upgraded.' );
		}

		if ( 'plugin' === $type ) {
			if ( $is_debug ) {
				error_log( '    Plugin ' . var_export( $item->slug, true ) . ' has been upgraded.' );
				if ( is_plugin_inactive( $upgrader_item ) ) {
					error_log( '    ' . var_export( $upgrader_item, true ) . ' is inactive and will not be checked for fatal errors.' );
				}
			}

			if ( $was_active && ! is_wp_error( $upgrade_result ) ) {

				/*
				 * The usual time limit is five minutes. However, as a loopback request
				 * is about to be performed, increase the time limit to account for this.
				 */
				if ( function_exists( 'set_time_limit' ) ) {
					set_time_limit( 10 * MINUTE_IN_SECONDS );
				}

				/*
				 * Avoids a race condition when there are 2 sequential plugins that have
				 * fatal errors. It seems a slight delay is required for the loopback to
				 * use the updated plugin code in the request. This can cause the second
				 * plugin's fatal error checking to be inaccurate, and may also affect
				 * subsequent plugin checks.
				 */
				sleep( 2 );

				if ( $this->has_fatal_error() ) {
					$upgrade_result = new WP_Error();
					$temp_backup    = array(
						array(
							'dir'  => 'plugins',
							'slug' => $item->slug,
							'src'  => WP_PLUGIN_DIR,
						),
					);

					$backup_restored = $upgrader->restore_temp_backup( $temp_backup );
					if ( is_wp_error( $backup_restored ) ) {
						$upgrade_result->add(
							'plugin_update_fatal_error_rollback_failed',
							sprintf(
								/* translators: %s: The plugin's slug. */
								__( "The update for '%s' contained a fatal error. The previously installed version could not be restored." ),
								$item->slug
							)
						);

						$upgrade_result->merge_from( $backup_restored );
					} else {
						$upgrade_result->add(
							'plugin_update_fatal_error_rollback_successful',
							sprintf(
								/* translators: %s: The plugin's slug. */
								__( "The update for '%s' contained a fatal error. The previously installed version has been restored." ),
								$item->slug
							)
						);

						$backup_deleted = $upgrader->delete_temp_backup( $temp_backup );
						if ( is_wp_error( $backup_deleted ) ) {
							$upgrade_result->merge_from( $backup_deleted );
						}
					}

					/*
					 * Should emails not be working, log the message(s) so that
					 * the log file contains context for the fatal error,
					 * and whether a rollback was performed.
					 *
					 * `trigger_error()` is not used as it outputs a stack trace
					 * to this location rather than to the fatal error, which will
					 * appear above this entry in the log file.
					 */
					if ( $is_debug ) {
						error_log( '    ' . implode( "\n", $upgrade_result->get_error_messages() ) );
					}
				} elseif ( $is_debug ) {
					error_log( '    The update for ' . var_export( $item->slug, true ) . ' has no fatal errors.' );
				}
			}
		}

		// All processes are complete. Allow visitors to browse the site again.
		if ( 'translation' !== $type ) {
			$upgrader->maintenance_mode( false );
		}

		$this->update_results[ $type ][] = (object) array(
			'item'     => $item,
			'result'   => $upgrade_result,
			'name'     => $item_name,
			'messages' => $skin->get_upgrade_messages(),
		);

		return $upgrade_result;
	}

	/**
	 * Kicks off the background update process, looping through all pending updates.
	 *
	 * @since 3.7.0
	 */
	public function run() {
		if ( $this->is_disabled() ) {
			return;
		}

		if ( ! is_main_network() || ! is_main_site() ) {
			return;
		}

		if ( ! WP_Upgrader::create_lock( 'auto_updater' ) ) {
			return;
		}

		$is_debug = WP_DEBUG && WP_DEBUG_LOG;

		if ( $is_debug ) {
			error_log( 'Automatic updates starting...' );
		}

		// Don't automatically run these things, as we'll handle it ourselves.
		remove_action( 'upgrader_process_complete', array( 'Language_Pack_Upgrader', 'async_upgrade' ), 20 );
		remove_action( 'upgrader_process_complete', 'wp_version_check' );
		remove_action( 'upgrader_process_complete', 'wp_update_plugins' );
		remove_action( 'upgrader_process_complete', 'wp_update_themes' );

		// Next, plugins.
		wp_update_plugins(); // Check for plugin updates.
		$plugin_updates = get_site_transient( 'update_plugins' );
		if ( $plugin_updates && ! empty( $plugin_updates->response ) ) {
			if ( $is_debug ) {
				error_log( '  Automatic plugin updates starting...' );
			}

			foreach ( $plugin_updates->response as $plugin ) {
				$this->update( 'plugin', $plugin );
			}

			// Force refresh of plugin update information.
			wp_clean_plugins_cache();

			if ( $is_debug ) {
				error_log( '  Automatic plugin updates complete.' );
			}
		}

		// Next, those themes we all love.
		wp_update_themes();  // Check for theme updates.
		$theme_updates = get_site_transient( 'update_themes' );
		if ( $theme_updates && ! empty( $theme_updates->response ) ) {
			if ( $is_debug ) {
				error_log( '  Automatic theme updates starting...' );
			}

			foreach ( $theme_updates->response as $theme ) {
				$this->update( 'theme', (object) $theme );
			}
			// Force refresh of theme update information.
			wp_clean_themes_cache();

			if ( $is_debug ) {
				error_log( '  Automatic theme updates complete.' );
			}
		}

		if ( $is_debug ) {
			error_log( 'Automatic updates complete.' );
		}

		// Next, process any core update.
		wp_version_check(); // Check for core updates.
		$core_update = find_core_auto_update();

		if ( $core_update ) {
			$this->update( 'core', $core_update );
		}

		/*
		 * Clean up, and check for any pending translations.
		 * (Core_Upgrader checks for core updates.)
		 */
		$theme_stats = array();
		if ( isset( $this->update_results['theme'] ) ) {
			foreach ( $this->update_results['theme'] as $upgrade ) {
				$theme_stats[ $upgrade->item->theme ] = ( true === $upgrade->result );
			}
		}
		wp_update_themes( $theme_stats ); // Check for theme updates.

		$plugin_stats = array();
		if ( isset( $this->update_results['plugin'] ) ) {
			foreach ( $this->update_results['plugin'] as $upgrade ) {
				$plugin_stats[ $upgrade->item->plugin ] = ( true === $upgrade->result );
			}
		}
		wp_update_plugins( $plugin_stats ); // Check for plugin updates.

		// Finally, process any new translations.
		$language_updates = wp_get_translation_updates();
		if ( $language_updates ) {
			foreach ( $language_updates as $update ) {
				$this->update( 'translation', $update );
			}

			// Clear existing caches.
			wp_clean_update_cache();

			wp_version_check();  // Check for core updates.
			wp_update_themes();  // Check for theme updates.
			wp_update_plugins(); // Check for plugin updates.
		}

		// Send debugging email to admin for all development installations.
		if ( ! empty( $this->update_results ) ) {
			$development_version = str_contains( wp_get_wp_version(), '-' );

			/**
			 * Filters whether to send a debugging email for each automatic background update.
			 *
			 * @since 3.7.0
			 *
			 * @param bool $development_version By default, emails are sent if the
			 *                                  install is a development version.
			 *                                  Return false to avoid the email.
			 */
			if ( apply_filters( 'automatic_updates_send_debug_email', $development_version ) ) {
				$this->send_debug_email();
			}

			if ( ! empty( $this->update_results['core'] ) ) {
				$this->after_core_update( $this->update_results['core'][0] );
			} elseif ( ! empty( $this->update_results['plugin'] ) || ! empty( $this->update_results['theme'] ) ) {
				$this->after_plugin_theme_update( $this->update_results );
			}

			/**
			 * Fires after all automatic updates have run.
			 *
			 * @since 3.8.0
			 *
			 * @param array $update_results The results of all attempted updates.
			 */
			do_action( 'automatic_updates_complete', $this->update_results );
		}

		WP_Upgrader::release_lock( 'auto_updater' );
	}

	/**
	 * Checks whether to send an email and avoid processing future updates after
	 * attempting a core update.
	 *
	 * @since 3.7.0
	 *
	 * @param object $update_result The result of the core update. Includes the update offer and result.
	 */
	protected function after_core_update( $update_result ) {
		$wp_version = wp_get_wp_version();

		$core_update = $update_result->item;
		$result      = $update_result->result;

		if ( ! is_wp_error( $result ) ) {
			$this->send_email( 'success', $core_update );
			return;
		}

		$error_code = $result->get_error_code();

		/*
		 * Any of these WP_Error codes are critical failures, as in they occurred after we started to copy core files.
		 * We should not try to perform a background update again until there is a successful one-click update performed by the user.
		 */
		$critical = false;
		if ( 'disk_full' === $error_code || str_contains( $error_code, '__copy_dir' ) ) {
			$critical = true;
		} elseif ( 'rollback_was_required' === $error_code && is_wp_error( $result->get_error_data()->rollback ) ) {
			// A rollback is only critical if it failed too.
			$critical        = true;
			$rollback_result = $result->get_error_data()->rollback;
		} elseif ( str_contains( $error_code, 'do_rollback' ) ) {
			$critical = true;
		}

		if ( $critical ) {
			$critical_data = array(
				'attempted'  => $core_update->current,
				'current'    => $wp_version,
				'error_code' => $error_code,
				'error_data' => $result->get_error_data(),
				'timestamp'  => time(),
				'critical'   => true,
			);
			if ( isset( $rollback_result ) ) {
				$critical_data['rollback_code'] = $rollback_result->get_error_code();
				$critical_data['rollback_data'] = $rollback_result->get_error_data();
			}
			update_site_option( 'auto_core_update_failed', $critical_data );
			$this->send_email( 'critical', $core_update, $result );
			return;
		}

		/*
		 * Any other WP_Error code (like download_failed or files_not_writable) occurs before
		 * we tried to copy over core files. Thus, the failures are early and graceful.
		 *
		 * We should avoid trying to perform a background update again for the same version.
		 * But we can try again if another version is released.
		 *
		 * For certain 'transient' failures, like download_failed, we should allow retries.
		 * In fact, let's schedule a special update for an hour from now. (It's possible
		 * the issue could actually be on WordPress.org's side.) If that one fails, then email.
		 */
		$send               = true;
		$transient_failures = array( 'incompatible_archive', 'download_failed', 'insane_distro', 'locked' );
		if ( in_array( $error_code, $transient_failures, true ) && ! get_site_option( 'auto_core_update_failed' ) ) {
			wp_schedule_single_event( time() + HOUR_IN_SECONDS, 'wp_maybe_auto_update' );
			$send = false;
		}

		$notified = get_site_option( 'auto_core_update_notified' );

		// Don't notify if we've already notified the same email address of the same version of the same notification type.
		if ( $notified
			&& 'fail' === $notified['type']
			&& get_site_option( 'admin_email' ) === $notified['email']
			&& $notified['version'] === $core_update->current
		) {
			$send = false;
		}

		update_site_option(
			'auto_core_update_failed',
			array(
				'attempted'  => $core_update->current,
				'current'    => $wp_version,
				'error_code' => $error_code,
				'error_data' => $result->get_error_data(),
				'timestamp'  => time(),
				'retry'      => in_array( $error_code, $transient_failures, true ),
			)
		);

		if ( $send ) {
			$this->send_email( 'fail', $core_update, $result );
		}
	}

	/**
	 * Sends an email upon the completion or failure of a background core update.
	 *
	 * @since 3.7.0
	 *
	 * @param string $type        The type of email to send. Can be one of 'success', 'fail', 'manual', 'critical'.
	 * @param object $core_update The update offer that was attempted.
	 * @param mixed  $result      Optional. The result for the core update. Can be WP_Error.
	 */
	protected function send_email( $type, $core_update, $result = null ) {
		update_site_option(
			'auto_core_update_notified',
			array(
				'type'      => $type,
				'email'     => get_site_option( 'admin_email' ),
				'version'   => $core_update->current,
				'timestamp' => time(),
			)
		);

		$next_user_core_update = get_preferred_from_update_core();

		// If the update transient is empty, use the update we just performed.
		if ( ! $next_user_core_update ) {
			$next_user_core_update = $core_update;
		}

		if ( 'upgrade' === $next_user_core_update->response
			&& version_compare( $next_user_core_update->version, $core_update->version, '>' )
		) {
			$newer_version_available = true;
		} else {
			$newer_version_available = false;
		}

		/**
		 * Filters whether to send an email following an automatic background core update.
		 *
		 * @since 3.7.0
		 *
		 * @param bool   $send        Whether to send the email. Default true.
		 * @param string $type        The type of email to send. Can be one of
		 *                            'success', 'fail', 'critical'.
		 * @param object $core_update The update offer that was attempted.
		 * @param mixed  $result      The result for the core update. Can be WP_Error.
		 */
		if ( 'manual' !== $type && ! apply_filters( 'auto_core_update_send_email', true, $type, $core_update, $result ) ) {
			return;
		}

		$admin_user = get_user_by( 'email', get_site_option( 'admin_email' ) );

		if ( $admin_user ) {
			$switched_locale = switch_to_user_locale( $admin_user->ID );
		} else {
			$switched_locale = switch_to_locale( get_locale() );
		}

		switch ( $type ) {
			case 'success': // We updated.
				/* translators: Site updated notification email subject. 1: Site title, 2: WordPress version. */
				$subject = __( '[%1$s] Your site has updated to WordPress %2$s' );
				break;

			case 'fail':   // We tried to update but couldn't.
			case 'manual': // We can't update (and made no attempt).
				/* translators: Update available notification email subject. 1: Site title, 2: WordPress version. */
				$subject = __( '[%1$s] WordPress %2$s is available. Please update!' );
				break;

			case 'critical': // We tried to update, started to copy files, then things went wrong.
				/* translators: Site down notification email subject. 1: Site title. */
				$subject = __( '[%1$s] URGENT: Your site may be down due to a failed update' );
				break;

			default:
				return;
		}

		// If the auto-update is not to the latest version, say that the current version of WP is available instead.
		$version = 'success' === $type ? $core_update->current : $next_user_core_update->current;
		$subject = sprintf( $subject, wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ), $version );

		$body = '';

		switch ( $type ) {
			case 'success':
				$body .= sprintf(
					/* translators: 1: Home URL, 2: WordPress version. */
					__( 'Howdy! Your site at %1$s has been updated automatically to WordPress %2$s.' ),
					home_url(),
					$core_update->current
				);
				$body .= "\n\n";
				if ( ! $newer_version_available ) {
					$body .= __( 'No further action is needed on your part.' ) . ' ';
				}

				// Can only reference the About screen if their update was successful.
				list( $about_version ) = explode( '-', $core_update->current, 2 );
				/* translators: %s: WordPress version. */
				$body .= sprintf( __( 'For more on version %s, see the About WordPress screen:' ), $about_version );
				$body .= "\n" . admin_url( 'about.php' );

				if ( $newer_version_available ) {
					/* translators: %s: WordPress latest version. */
					$body .= "\n\n" . sprintf( __( 'WordPress %s is also now available.' ), $next_user_core_update->current ) . ' ';
					$body .= __( 'Updating is easy and only takes a few moments:' );
					$body .= "\n" . network_admin_url( 'update-core.php' );
				}

				break;

			case 'fail':
			case 'manual':
				$body .= sprintf(
					/* translators: 1: Home URL, 2: WordPress version. */
					__( 'Please update your site at %1$s to WordPress %2$s.' ),
					home_url(),
					$next_user_core_update->current
				);

				$body .= "\n\n";

				/*
				 * Don't show this message if there is a newer version available.
				 * Potential for confusion, and also not useful for them to know at this point.
				 */
				if ( 'fail' === $type && ! $newer_version_available ) {
					$body .= __( 'An attempt was made, but your site could not be updated automatically.' ) . ' ';
				}

				$body .= __( 'Updating is easy and only takes a few moments:' );
				$body .= "\n" . network_admin_url( 'update-core.php' );
				break;

			case 'critical':
				if ( $newer_version_available ) {
					$body .= sprintf(
						/* translators: 1: Home URL, 2: WordPress version. */
						__( 'Your site at %1$s experienced a critical failure while trying to update WordPress to version %2$s.' ),
						home_url(),
						$core_update->current
					);
				} else {
					$body .= sprintf(
						/* translators: 1: Home URL, 2: WordPress latest version. */
						__( 'Your site at %1$s experienced a critical failure while trying to update to the latest version of WordPress, %2$s.' ),
						home_url(),
						$core_update->current
					);
				}

				$body .= "\n\n" . __( "This means your site may be offline or broken. Don't panic; this can be fixed." );

				$body .= "\n\n" . __( "Please check out your site now. It's possible that everything is working. If it says you need to update, you should do so:" );
				$body .= "\n" . network_admin_url( 'update-core.php' );
				break;
		}

		$critical_support = 'critical' === $type && ! empty( $core_update->support_email );
		if ( $critical_support ) {
			// Support offer if available.
			$body .= "\n\n" . sprintf(
				/* translators: %s: Support email address. */
				__( 'The WordPress team is willing to help you. Forward this email to %s and the team will work with you to make sure your site is working.' ),
				$core_update->support_email
			);
		} else {
			// Add a note about the support forums.
			$body .= "\n\n" . __( 'If you experience any issues or need support, the volunteers in the WordPress.org support forums may be able to help.' );
			$body .= "\n" . __( 'https://wordpress.org/support/forums/' );
		}

		// Updates are important!
		if ( 'success' !== $type || $newer_version_available ) {
			$body .= "\n\n" . __( 'Keeping your site updated is important for security. It also makes the internet a safer place for you and your readers.' );
		}

		if ( $critical_support ) {
			$body .= ' ' . __( "Reach out to WordPress Core developers to ensure you'll never have this problem again." );
		}

		// If things are successful and we're now on the latest, mention plugins and themes if any are out of date.
		if ( 'success' === $type && ! $newer_version_available && ( get_plugin_updates() || get_theme_updates() ) ) {
			$body .= "\n\n" . __( 'You also have some plugins or themes with updates available. Update them now:' );
			$body .= "\n" . network_admin_url();
		}

		$body .= "\n\n" . __( 'The WordPress Team' ) . "\n";

		if ( 'critical' === $type && is_wp_error( $result ) ) {
			$body .= "\n***\n\n";
			/* translators: %s: WordPress version. */
			$body .= sprintf( __( 'Your site was running version %s.' ), get_bloginfo( 'version' ) );
			$body .= ' ' . __( 'Some data that describes the error your site encountered has been put together.' );
			$body .= ' ' . __( 'Your hosting company, support forum volunteers, or a friendly developer may be able to use this information to help you:' );

			/*
			 * If we had a rollback and we're still critical, then the rollback failed too.
			 * Loop through all errors (the main WP_Error, the update result, the rollback result) for code, data, etc.
			 */
			if ( 'rollback_was_required' === $result->get_error_code() ) {
				$errors = array( $result, $result->get_error_data()->update, $result->get_error_data()->rollback );
			} else {
				$errors = array( $result );
			}

			foreach ( $errors as $error ) {
				if ( ! is_wp_error( $error ) ) {
					continue;
				}

				$error_code = $error->get_error_code();
				/* translators: %s: Error code. */
				$body .= "\n\n" . sprintf( __( 'Error code: %s' ), $error_code );

				if ( 'rollback_was_required' === $error_code ) {
					continue;
				}

				if ( $error->get_error_message() ) {
					$body .= "\n" . $error->get_error_message();
				}

				$error_data = $error->get_error_data();
				if ( $error_data ) {
					$body .= "\n" . implode( ', ', (array) $error_data );
				}
			}

			$body .= "\n";
		}

		$to      = get_site_option( 'admin_email' );
		$headers = '';

		$email = compact( 'to', 'subject', 'body', 'headers' );

		/**
		 * Filters the email sent following an automatic background core update.
		 *
		 * @since 3.7.0
		 *
		 * @param array $email {
		 *     Array of email arguments that will be passed to wp_mail().
		 *
		 *     @type string $to      The email recipient. An array of emails
		 *                            can be returned, as handled by wp_mail().
		 *     @type string $subject The email's subject.
		 *     @type string $body    The email message body.
		 *     @type string $headers Any email headers, defaults to no headers.
		 * }
		 * @param string $type        The type of email being sent. Can be one of
		 *                            'success', 'fail', 'manual', 'critical'.
		 * @param object $core_update The update offer that was attempted.
		 * @param mixed  $result      The result for the core update. Can be WP_Error.
		 */
		$email = apply_filters( 'auto_core_update_email', $email, $type, $core_update, $result );

		wp_mail( $email['to'], wp_specialchars_decode( $email['subject'] ), $email['body'], $email['headers'] );

		if ( $switched_locale ) {
			restore_previous_locale();
		}
	}

	/**
	 * Checks whether an email should be sent after attempting plugin or theme updates.
	 *
	 * @since 5.5.0
	 *
	 * @param array $update_results The results of update tasks.
	 */
	protected function after_plugin_theme_update( $update_results ) {
		$successful_updates = array();
		$failed_updates     = array();

		if ( ! empty( $update_results['plugin'] ) ) {
			/**
			 * Filters whether to send an email following an automatic background plugin update.
			 *
			 * @since 5.5.0
			 * @since 5.5.1 Added the `$update_results` parameter.
			 *
			 * @param bool  $enabled        True if plugin update notifications are enabled, false otherwise.
			 * @param array $update_results The results of plugins update tasks.
			 */
			$notifications_enabled = apply_filters( 'auto_plugin_update_send_email', true, $update_results['plugin'] );

			if ( $notifications_enabled ) {
				foreach ( $update_results['plugin'] as $update_result ) {
					if ( true === $update_result->result ) {
						$successful_updates['plugin'][] = $update_result;
					} else {
						$failed_updates['plugin'][] = $update_result;
					}
				}
			}
		}

		if ( ! empty( $update_results['theme'] ) ) {
			/**
			 * Filters whether to send an email following an automatic background theme update.
			 *
			 * @since 5.5.0
			 * @since 5.5.1 Added the `$update_results` parameter.
			 *
			 * @param bool  $enabled        True if theme update notifications are enabled, false otherwise.
			 * @param array $update_results The results of theme update tasks.
			 */
			$notifications_enabled = apply_filters( 'auto_theme_update_send_email', true, $update_results['theme'] );

			if ( $notifications_enabled ) {
				foreach ( $update_results['theme'] as $update_result ) {
					if ( true === $update_result->result ) {
						$successful_updates['theme'][] = $update_result;
					} else {
						$failed_updates['theme'][] = $update_result;
					}
				}
			}
		}

		if ( empty( $successful_updates ) && empty( $failed_updates ) ) {
			return;
		}

		if ( empty( $failed_updates ) ) {
			$this->send_plugin_theme_email( 'success', $successful_updates, $failed_updates );
		} elseif ( empty( $successful_updates ) ) {
			$this->send_plugin_theme_email( 'fail', $successful_updates, $failed_updates );
		} else {
			$this->send_plugin_theme_email( 'mixed', $successful_updates, $failed_updates );
		}
	}

	/**
	 * Sends an email upon the completion or failure of a plugin or theme background update.
	 *
	 * @since 5.5.0
	 *
	 * @param string $type               The type of email to send. Can be one of 'success', 'fail', 'mixed'.
	 * @param array  $successful_updates A list of updates that succeeded.
	 * @param array  $failed_updates     A list of updates that failed.
	 */
	protected function send_plugin_theme_email( $type, $successful_updates, $failed_updates ) {
		// No updates were attempted.
		if ( empty( $successful_updates ) && empty( $failed_updates ) ) {
			return;
		}

		$unique_failures     = false;
		$past_failure_emails = get_option( 'auto_plugin_theme_update_emails', array() );

		/*
		 * When only failures have occurred, an email should only be sent if there are unique failures.
		 * A failure is considered unique if an email has not been sent for an update attempt failure
		 * to a plugin or theme with the same new_version.
		 */
		if ( 'fail' === $type ) {
			foreach ( $failed_updates as $update_type => $failures ) {
				foreach ( $failures as $failed_update ) {
					if ( ! isset( $past_failure_emails[ $failed_update->item->{$update_type} ] ) ) {
						$unique_failures = true;
						continue;
					}

					// Check that the failure represents a new failure based on the new_version.
					if ( version_compare( $past_failure_emails[ $failed_update->item->{$update_type} ], $failed_update->item->new_version, '<' ) ) {
						$unique_failures = true;
					}
				}
			}

			if ( ! $unique_failures ) {
				return;
			}
		}

		$admin_user = get_user_by( 'email', get_site_option( 'admin_email' ) );

		if ( $admin_user ) {
			$switched_locale = switch_to_user_locale( $admin_user->ID );
		} else {
			$switched_locale = switch_to_locale( get_locale() );
		}

		$body               = array();
		$successful_plugins = ( ! empty( $successful_updates['plugin'] ) );
		$successful_themes  = ( ! empty( $successful_updates['theme'] ) );
		$failed_plugins     = ( ! empty( $failed_updates['plugin'] ) );
		$failed_themes      = ( ! empty( $failed_updates['theme'] ) );

		switch ( $type ) {
			case 'success':
				if ( $successful_plugins && $successful_themes ) {
					/* translators: %s: Site title. */
					$subject = __( '[%s] Some plugins and themes have automatically updated' );
					$body[]  = sprintf(
						/* translators: %s: Home URL. */
						__( 'Howdy! Some plugins and themes have automatically updated to their latest versions on your site at %s. No further action is needed on your part.' ),
						home_url()
					);
				} elseif ( $successful_plugins ) {
					/* translators: %s: Site title. */
					$subject = __( '[%s] Some plugins were automatically updated' );
					$body[]  = sprintf(
						/* translators: %s: Home URL. */
						__( 'Howdy! Some plugins have automatically updated to their latest versions on your site at %s. No further action is needed on your part.' ),
						home_url()
					);
				} else {
					/* translators: %s: Site title. */
					$subject = __( '[%s] Some themes were automatically updated' );
					$body[]  = sprintf(
						/* translators: %s: Home URL. */
						__( 'Howdy! Some themes have automatically updated to their latest versions on your site at %s. No further action is needed on your part.' ),
						home_url()
					);
				}

				break;
			case 'fail':
			case 'mixed':
				if ( $failed_plugins && $failed_themes ) {
					/* translators: %s: Site title. */
					$subject = __( '[%s] Some plugins and themes have failed to update' );
					$body[]  = sprintf(
						/* translators: %s: Home URL. */
						__( 'Howdy! Plugins and themes failed to update on your site at %s.' ),
						home_url()
					);
				} elseif ( $failed_plugins ) {
					/* translators: %s: Site title. */
					$subject = __( '[%s] Some plugins have failed to update' );
					$body[]  = sprintf(
						/* translators: %s: Home URL. */
						__( 'Howdy! Plugins failed to update on your site at %s.' ),
						home_url()
					);
				} else {
					/* translators: %s: Site title. */
					$subject = __( '[%s] Some themes have failed to update' );
					$body[]  = sprintf(
						/* translators: %s: Home URL. */
						__( 'Howdy! Themes failed to update on your site at %s.' ),
						home_url()
					);
				}

				break;
		}

		if ( in_array( $type, array( 'fail', 'mixed' ), true ) ) {
			$body[] = "\n";
			$body[] = __( 'Please check your site now. It’s possible that everything is working. If there are updates available, you should update.' );
			$body[] = "\n";

			// List failed plugin updates.
			if ( ! empty( $failed_updates['plugin'] ) ) {
				$body[] = __( 'The following plugins failed to update. If there was a fatal error in the update, the previously installed version has been restored.' );

				foreach ( $failed_updates['plugin'] as $item ) {
					$body_message = '';
					$item_url     = '';

					if ( ! empty( $item->item->url ) ) {
						$item_url = ' : ' . esc_url( $item->item->url );
					}

					if ( $item->item->current_version ) {
						$body_message .= sprintf(
							/* translators: 1: Plugin name, 2: Current version number, 3: New version number, 4: Plugin URL. */
							__( '- %1$s (from version %2$s to %3$s)%4$s' ),
							html_entity_decode( $item->name ),
							$item->item->current_version,
							$item->item->new_version,
							$item_url
						);
					} else {
						$body_message .= sprintf(
							/* translators: 1: Plugin name, 2: Version number, 3: Plugin URL. */
							__( '- %1$s version %2$s%3$s' ),
							html_entity_decode( $item->name ),
							$item->item->new_version,
							$item_url
						);
					}

					$body[] = $body_message;

					$past_failure_emails[ $item->item->plugin ] = $item->item->new_version;
				}

				$body[] = "\n";
			}

			// List failed theme updates.
			if ( ! empty( $failed_updates['theme'] ) ) {
				$body[] = __( 'These themes failed to update:' );

				foreach ( $failed_updates['theme'] as $item ) {
					if ( $item->item->current_version ) {
						$body[] = sprintf(
							/* translators: 1: Theme name, 2: Current version number, 3: New version number. */
							__( '- %1$s (from version %2$s to %3$s)' ),
							html_entity_decode( $item->name ),
							$item->item->current_version,
							$item->item->new_version
						);
					} else {
						$body[] = sprintf(
							/* translators: 1: Theme name, 2: Version number. */
							__( '- %1$s version %2$s' ),
							html_entity_decode( $item->name ),
							$item->item->new_version
						);
					}

					$past_failure_emails[ $item->item->theme ] = $item->item->new_version;
				}

				$body[] = "\n";
			}
		}

		// List successful updates.
		if ( in_array( $type, array( 'success', 'mixed' ), true ) ) {
			$body[] = "\n";

			// List successful plugin updates.
			if ( ! empty( $successful_updates['plugin'] ) ) {
				$body[] = __( 'These plugins are now up to date:' );

				foreach ( $successful_updates['plugin'] as $item ) {
					$body_message = '';
					$item_url     = '';

					if ( ! empty( $item->item->url ) ) {
						$item_url = ' : ' . esc_url( $item->item->url );
					}

					if ( $item->item->current_version ) {
						$body_message .= sprintf(
							/* translators: 1: Plugin name, 2: Current version number, 3: New version number, 4: Plugin URL. */
							__( '- %1$s (from version %2$s to %3$s)%4$s' ),
							html_entity_decode( $item->name ),
							$item->item->current_version,
							$item->item->new_version,
							$item_url
						);
					} else {
						$body_message .= sprintf(
							/* translators: 1: Plugin name, 2: Version number, 3: Plugin URL. */
							__( '- %1$s version %2$s%3$s' ),
							html_entity_decode( $item->name ),
							$item->item->new_version,
							$item_url
						);
					}
					$body[] = $body_message;

					unset( $past_failure_emails[ $item->item->plugin ] );
				}

				$body[] = "\n";
			}

			// List successful theme updates.
			if ( ! empty( $successful_updates['theme'] ) ) {
				$body[] = __( 'These themes are now up to date:' );

				foreach ( $successful_updates['theme'] as $item ) {
					if ( $item->item->current_version ) {
						$body[] = sprintf(
							/* translators: 1: Theme name, 2: Current version number, 3: New version number. */
							__( '- %1$s (from version %2$s to %3$s)' ),
							html_entity_decode( $item->name ),
							$item->item->current_version,
							$item->item->new_version
						);
					} else {
						$body[] = sprintf(
							/* translators: 1: Theme name, 2: Version number. */
							__( '- %1$s version %2$s' ),
							html_entity_decode( $item->name ),
							$item->item->new_version
						);
					}

					unset( $past_failure_emails[ $item->item->theme ] );
				}

				$body[] = "\n";
			}
		}

		if ( $failed_plugins ) {
			$body[] = sprintf(
				/* translators: %s: Plugins screen URL. */
				__( 'To manage plugins on your site, visit the Plugins page: %s' ),
				admin_url( 'plugins.php' )
			);
			$body[] = "\n";
		}

		if ( $failed_themes ) {
			$body[] = sprintf(
				/* translators: %s: Themes screen URL. */
				__( 'To manage themes on your site, visit the Themes page: %s' ),
				admin_url( 'themes.php' )
			);
			$body[] = "\n";
		}

		// Add a note about the support forums.
		$body[] = __( 'If you experience any issues or need support, the volunteers in the WordPress.org support forums may be able to help.' );
		$body[] = __( 'https://wordpress.org/support/forums/' );
		$body[] = "\n" . __( 'The WordPress Team' );

		if ( '' !== get_option( 'blogname' ) ) {
			$site_title = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
		} else {
			$site_title = parse_url( home_url(), PHP_URL_HOST );
		}

		$body    = implode( "\n", $body );
		$to      = get_site_option( 'admin_email' );
		$subject = sprintf( $subject, $site_title );
		$headers = '';

		$email = compact( 'to', 'subject', 'body', 'headers' );

		/**
		 * Filters the email sent following an automatic background update for plugins and themes.
		 *
		 * @since 5.5.0
		 *
		 * @param array  $email {
		 *     Array of email arguments that will be passed to wp_mail().
		 *
		 *     @type string $to      The email recipient. An array of emails
		 *                           can be returned, as handled by wp_mail().
		 *     @type string $subject The email's subject.
		 *     @type string $body    The email message body.
		 *     @type string $headers Any email headers, defaults to no headers.
		 * }
		 * @param string $type               The type of email being sent. Can be one of 'success', 'fail', 'mixed'.
		 * @param array  $successful_updates A list of updates that succeeded.
		 * @param array  $failed_updates     A list of updates that failed.
		 */
		$email = apply_filters( 'auto_plugin_theme_update_email', $email, $type, $successful_updates, $failed_updates );

		$result = wp_mail( $email['to'], wp_specialchars_decode( $email['subject'] ), $email['body'], $email['headers'] );

		if ( $result ) {
			update_option( 'auto_plugin_theme_update_emails', $past_failure_emails );
		}

		if ( $switched_locale ) {
			restore_previous_locale();
		}
	}

	/**
	 * Prepares and sends an email of a full log of background update results, useful for debugging and geekery.
	 *
	 * @since 3.7.0
	 */
	protected function send_debug_email() {
		$admin_user = get_user_by( 'email', get_site_option( 'admin_email' ) );

		if ( $admin_user ) {
			$switched_locale = switch_to_user_locale( $admin_user->ID );
		} else {
			$switched_locale = switch_to_locale( get_locale() );
		}

		$body     = array();
		$failures = 0;

		/* translators: %s: Network home URL. */
		$body[] = sprintf( __( 'WordPress site: %s' ), network_home_url( '/' ) );

		// Core.
		if ( isset( $this->update_results['core'] ) ) {
			$result = $this->update_results['core'][0];

			if ( $result->result && ! is_wp_error( $result->result ) ) {
				/* translators: %s: WordPress version. */
				$body[] = sprintf( __( 'SUCCESS: WordPress was successfully updated to %s' ), $result->name );
			} else {
				/* translators: %s: WordPress version. */
				$body[] = sprintf( __( 'FAILED: WordPress failed to update to %s' ), $result->name );
				++$failures;
			}

			$body[] = '';
		}

		// Plugins, Themes, Translations.
		foreach ( array( 'plugin', 'theme', 'translation' ) as $type ) {
			if ( ! isset( $this->update_results[ $type ] ) ) {
				continue;
			}

			$success_items = wp_list_filter( $this->update_results[ $type ], array( 'result' => true ) );

			if ( $success_items ) {
				$messages = array(
					'plugin'      => __( 'The following plugins were successfully updated:' ),
					'theme'       => __( 'The following themes were successfully updated:' ),
					'translation' => __( 'The following translations were successfully updated:' ),
				);

				$body[] = $messages[ $type ];
				foreach ( wp_list_pluck( $success_items, 'name' ) as $name ) {
					/* translators: %s: Name of plugin / theme / translation. */
					$body[] = ' * ' . sprintf( __( 'SUCCESS: %s' ), $name );
				}
			}

			if ( $success_items !== $this->update_results[ $type ] ) {
				// Failed updates.
				$messages = array(
					'plugin'      => __( 'The following plugins failed to update:' ),
					'theme'       => __( 'The following themes failed to update:' ),
					'translation' => __( 'The following translations failed to update:' ),
				);

				$body[] = $messages[ $type ];

				foreach ( $this->update_results[ $type ] as $item ) {
					if ( ! $item->result || is_wp_error( $item->result ) ) {
						/* translators: %s: Name of plugin / theme / translation. */
						$body[] = ' * ' . sprintf( __( 'FAILED: %s' ), $item->name );
						++$failures;
					}
				}
			}

			$body[] = '';
		}

		if ( '' !== get_bloginfo( 'name' ) ) {
			$site_title = wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES );
		} else {
			$site_title = parse_url( home_url(), PHP_URL_HOST );
		}

		if ( $failures ) {
			$body[] = trim(
				__(
					"BETA TESTING?
=============

This debugging email is sent when you are using a development version of WordPress.

If you think these failures might be due to a bug in WordPress, could you report it?
 * Open a thread in the support forums: https://wordpress.org/support/forum/alphabeta
 * Or, if you're comfortable writing a bug report: https://core.trac.wordpress.org/

Thanks! -- The WordPress Team"
				)
			);
			$body[] = '';

			/* translators: Background update failed notification email subject. %s: Site title. */
			$subject = sprintf( __( '[%s] Background Update Failed' ), $site_title );
		} else {
			/* translators: Background update finished notification email subject. %s: Site title. */
			$subject = sprintf( __( '[%s] Background Update Finished' ), $site_title );
		}

		$body[] = trim(
			__(
				'UPDATE LOG
=========='
			)
		);
		$body[] = '';

		foreach ( array( 'core', 'plugin', 'theme', 'translation' ) as $type ) {
			if ( ! isset( $this->update_results[ $type ] ) ) {
				continue;
			}

			foreach ( $this->update_results[ $type ] as $update ) {
				$body[] = $update->name;
				$body[] = str_repeat( '-', strlen( $update->name ) );

				foreach ( $update->messages as $message ) {
					$body[] = '  ' . html_entity_decode( str_replace( '&#8230;', '...', $message ) );
				}

				if ( is_wp_error( $update->result ) ) {
					$results = array( 'update' => $update->result );

					// If we rolled back, we want to know an error that occurred then too.
					if ( 'rollback_was_required' === $update->result->get_error_code() ) {
						$results = (array) $update->result->get_error_data();
					}

					foreach ( $results as $result_type => $result ) {
						if ( ! is_wp_error( $result ) ) {
							continue;
						}

						if ( 'rollback' === $result_type ) {
							/* translators: 1: Error code, 2: Error message. */
							$body[] = '  ' . sprintf( __( 'Rollback Error: [%1$s] %2$s' ), $result->get_error_code(), $result->get_error_message() );
						} else {
							/* translators: 1: Error code, 2: Error message. */
							$body[] = '  ' . sprintf( __( 'Error: [%1$s] %2$s' ), $result->get_error_code(), $result->get_error_message() );
						}

						if ( $result->get_error_data() ) {
							$body[] = '         ' . implode( ', ', (array) $result->get_error_data() );
						}
					}
				}

				$body[] = '';
			}
		}

		$email = array(
			'to'      => get_site_option( 'admin_email' ),
			'subject' => $subject,
			'body'    => implode( "\n", $body ),
			'headers' => '',
		);

		/**
		 * Filters the debug email that can be sent following an automatic
		 * background core update.
		 *
		 * @since 3.8.0
		 *
		 * @param array $email {
		 *     Array of email arguments that will be passed to wp_mail().
		 *
		 *     @type string $to      The email recipient. An array of emails
		 *                           can be returned, as handled by wp_mail().
		 *     @type string $subject Email subject.
		 *     @type string $body    Email message body.
		 *     @type string $headers Any email headers. Default empty.
		 * }
		 * @param int   $failures The number of failures encountered while upgrading.
		 * @param mixed $results  The results of all attempted updates.
		 */
		$email = apply_filters( 'automatic_updates_debug_email', $email, $failures, $this->update_results );

		wp_mail( $email['to'], wp_specialchars_decode( $email['subject'] ), $email['body'], $email['headers'] );

		if ( $switched_locale ) {
			restore_previous_locale();
		}
	}

	/**
	 * Performs a loopback request to check for potential fatal errors.
	 *
	 * Fatal errors cannot be detected unless maintenance mode is enabled.
	 *
	 * @since 6.6.0
	 *
	 * @global int $upgrading The Unix timestamp marking when upgrading WordPress began.
	 *
	 * @return bool Whether a fatal error was detected.
	 */
	protected function has_fatal_error() {
		global $upgrading;

		$maintenance_file = ABSPATH . '.maintenance';
		if ( ! file_exists( $maintenance_file ) ) {
			return false;
		}

		require $maintenance_file;
		if ( ! is_int( $upgrading ) ) {
			return false;
		}

		$scrape_key   = md5( $upgrading );
		$scrape_nonce = (string) $upgrading;
		$transient    = 'scrape_key_' . $scrape_key;
		set_transient( $transient, $scrape_nonce, 30 );

		$cookies       = wp_unslash( $_COOKIE );
		$scrape_params = array(
			'wp_scrape_key'   => $scrape_key,
			'wp_scrape_nonce' => $scrape_nonce,
		);
		$headers       = array(
			'Cache-Control' => 'no-cache',
		);

		/** This filter is documented in wp-includes/class-wp-http-streams.php */
		$sslverify = apply_filters( 'https_local_ssl_verify', false );

		// Include Basic auth in the loopback request.
		if ( isset( $_SERVER['PHP_AUTH_USER'] ) && isset( $_SERVER['PHP_AUTH_PW'] ) ) {
			$headers['Authorization'] = 'Basic ' . base64_encode( wp_unslash( $_SERVER['PHP_AUTH_USER'] ) . ':' . wp_unslash( $_SERVER['PHP_AUTH_PW'] ) );
		}

		// Time to wait for loopback request to finish.
		$timeout = 50; // 50 seconds.

		$is_debug = WP_DEBUG && WP_DEBUG_LOG;
		if ( $is_debug ) {
			error_log( '    Scraping home page...' );
		}

		$needle_start = "###### wp_scraping_result_start:$scrape_key ######";
		$needle_end   = "###### wp_scraping_result_end:$scrape_key ######";
		$url          = add_query_arg( $scrape_params, home_url( '/' ) );
		$response     = wp_remote_get( $url, compact( 'cookies', 'headers', 'timeout', 'sslverify' ) );

		if ( is_wp_error( $response ) ) {
			if ( $is_debug ) {
				error_log( 'Loopback request failed: ' . $response->get_error_message() );
			}
			return true;
		}

		// If this outputs `true` in the log, it means there were no fatal errors detected.
		if ( $is_debug ) {
			error_log( var_export( substr( $response['body'], strpos( $response['body'], '###### wp_scraping_result_start:' ) ), true ) );
		}

		$body                   = wp_remote_retrieve_body( $response );
		$scrape_result_position = strpos( $body, $needle_start );
		$result                 = null;

		if ( false !== $scrape_result_position ) {
			$error_output = substr( $body, $scrape_result_position + strlen( $needle_start ) );
			$error_output = substr( $error_output, 0, strpos( $error_output, $needle_end ) );
			$result       = json_decode( trim( $error_output ), true );
		}

		delete_transient( $transient );

		// Only fatal errors will result in a 'type' key.
		return isset( $result['type'] );
	}
}
class-custom-background.php000064400000052222151327343360012021 0ustar00<?php
/**
 * The custom background script.
 *
 * @package WordPress
 * @subpackage Administration
 */

/**
 * The custom background class.
 *
 * @since 3.0.0
 */
#[AllowDynamicProperties]
class Custom_Background {

	/**
	 * Callback for administration header.
	 *
	 * @since 3.0.0
	 * @var callable
	 */
	public $admin_header_callback;

	/**
	 * Callback for header div.
	 *
	 * @since 3.0.0
	 * @var callable
	 */
	public $admin_image_div_callback;

	/**
	 * Used to trigger a success message when settings updated and set to true.
	 *
	 * @since 3.0.0
	 * @var bool
	 */
	private $updated;

	/**
	 * Constructor - Registers administration header callback.
	 *
	 * @since 3.0.0
	 *
	 * @param callable $admin_header_callback    Optional. Administration header callback.
	 *                                           Default empty string.
	 * @param callable $admin_image_div_callback Optional. Custom image div output callback.
	 *                                           Default empty string.
	 */
	public function __construct( $admin_header_callback = '', $admin_image_div_callback = '' ) {
		$this->admin_header_callback    = $admin_header_callback;
		$this->admin_image_div_callback = $admin_image_div_callback;

		add_action( 'admin_menu', array( $this, 'init' ) );

		add_action( 'wp_ajax_custom-background-add', array( $this, 'ajax_background_add' ) );

		// Unused since 3.5.0.
		add_action( 'wp_ajax_set-background-image', array( $this, 'wp_set_background_image' ) );
	}

	/**
	 * Sets up the hooks for the Custom Background admin page.
	 *
	 * @since 3.0.0
	 */
	public function init() {
		$page = add_theme_page(
			_x( 'Background', 'custom background' ),
			_x( 'Background', 'custom background' ),
			'edit_theme_options',
			'custom-background',
			array( $this, 'admin_page' )
		);

		if ( ! $page ) {
			return;
		}

		add_action( "load-{$page}", array( $this, 'admin_load' ) );
		add_action( "load-{$page}", array( $this, 'take_action' ), 49 );
		add_action( "load-{$page}", array( $this, 'handle_upload' ), 49 );

		if ( $this->admin_header_callback ) {
			add_action( "admin_head-{$page}", $this->admin_header_callback, 51 );
		}
	}

	/**
	 * Sets up the enqueue for the CSS & JavaScript files.
	 *
	 * @since 3.0.0
	 */
	public function admin_load() {
		get_current_screen()->add_help_tab(
			array(
				'id'      => 'overview',
				'title'   => __( 'Overview' ),
				'content' =>
					'<p>' . __( 'You can customize the look of your site without touching any of your theme&#8217;s code by using a custom background. Your background can be an image or a color.' ) . '</p>' .
					'<p>' . __( 'To use a background image, simply upload it or choose an image that has already been uploaded to your Media Library by clicking the &#8220;Choose Image&#8221; button. You can display a single instance of your image, or tile it to fill the screen. You can have your background fixed in place, so your site content moves on top of it, or you can have it scroll with your site.' ) . '</p>' .
					'<p>' . __( 'You can also choose a background color by clicking the Select Color button and either typing in a legitimate HTML hex value, e.g. &#8220;#ff0000&#8221; for red, or by choosing a color using the color picker.' ) . '</p>' .
					'<p>' . __( 'Do not forget to click on the Save Changes button when you are finished.' ) . '</p>',
			)
		);

		get_current_screen()->set_help_sidebar(
			'<p><strong>' . __( 'For more information:' ) . '</strong></p>' .
			'<p>' . __( '<a href="https://codex.wordpress.org/Appearance_Background_Screen">Documentation on Custom Background</a>' ) . '</p>' .
			'<p>' . __( '<a href="https://wordpress.org/support/forums/">Support forums</a>' ) . '</p>'
		);

		wp_enqueue_media();
		wp_enqueue_script( 'custom-background' );
		wp_enqueue_style( 'wp-color-picker' );
	}

	/**
	 * Executes custom background modification.
	 *
	 * @since 3.0.0
	 */
	public function take_action() {
		if ( empty( $_POST ) ) {
			return;
		}

		if ( isset( $_POST['reset-background'] ) ) {
			check_admin_referer( 'custom-background-reset', '_wpnonce-custom-background-reset' );

			remove_theme_mod( 'background_image' );
			remove_theme_mod( 'background_image_thumb' );

			$this->updated = true;
			return;
		}

		if ( isset( $_POST['remove-background'] ) ) {
			// @todo Uploaded files are not removed here.
			check_admin_referer( 'custom-background-remove', '_wpnonce-custom-background-remove' );

			set_theme_mod( 'background_image', '' );
			set_theme_mod( 'background_image_thumb', '' );

			$this->updated = true;
			wp_safe_redirect( $_POST['_wp_http_referer'] );
			return;
		}

		if ( isset( $_POST['background-preset'] ) ) {
			check_admin_referer( 'custom-background' );

			if ( in_array( $_POST['background-preset'], array( 'default', 'fill', 'fit', 'repeat', 'custom' ), true ) ) {
				$preset = $_POST['background-preset'];
			} else {
				$preset = 'default';
			}

			set_theme_mod( 'background_preset', $preset );
		}

		if ( isset( $_POST['background-position'] ) ) {
			check_admin_referer( 'custom-background' );

			$position = explode( ' ', $_POST['background-position'] );

			if ( in_array( $position[0], array( 'left', 'center', 'right' ), true ) ) {
				$position_x = $position[0];
			} else {
				$position_x = 'left';
			}

			if ( in_array( $position[1], array( 'top', 'center', 'bottom' ), true ) ) {
				$position_y = $position[1];
			} else {
				$position_y = 'top';
			}

			set_theme_mod( 'background_position_x', $position_x );
			set_theme_mod( 'background_position_y', $position_y );
		}

		if ( isset( $_POST['background-size'] ) ) {
			check_admin_referer( 'custom-background' );

			if ( in_array( $_POST['background-size'], array( 'auto', 'contain', 'cover' ), true ) ) {
				$size = $_POST['background-size'];
			} else {
				$size = 'auto';
			}

			set_theme_mod( 'background_size', $size );
		}

		if ( isset( $_POST['background-repeat'] ) ) {
			check_admin_referer( 'custom-background' );

			$repeat = $_POST['background-repeat'];

			if ( 'no-repeat' !== $repeat ) {
				$repeat = 'repeat';
			}

			set_theme_mod( 'background_repeat', $repeat );
		}

		if ( isset( $_POST['background-attachment'] ) ) {
			check_admin_referer( 'custom-background' );

			$attachment = $_POST['background-attachment'];

			if ( 'fixed' !== $attachment ) {
				$attachment = 'scroll';
			}

			set_theme_mod( 'background_attachment', $attachment );
		}

		if ( isset( $_POST['background-color'] ) ) {
			check_admin_referer( 'custom-background' );

			$color = preg_replace( '/[^0-9a-fA-F]/', '', $_POST['background-color'] );

			if ( strlen( $color ) === 6 || strlen( $color ) === 3 ) {
				set_theme_mod( 'background_color', $color );
			} else {
				set_theme_mod( 'background_color', '' );
			}
		}

		$this->updated = true;
	}

	/**
	 * Displays the custom background page.
	 *
	 * @since 3.0.0
	 */
	public function admin_page() {
		?>
<div class="wrap" id="custom-background">
<h1><?php _e( 'Custom Background' ); ?></h1>

		<?php
		if ( current_user_can( 'customize' ) ) {
			$message = sprintf(
				/* translators: %s: URL to background image configuration in Customizer. */
				__( 'You can now manage and live-preview Custom Backgrounds in the <a href="%s">Customizer</a>.' ),
				admin_url( 'customize.php?autofocus[control]=background_image' )
			);
			wp_admin_notice(
				$message,
				array(
					'type'               => 'info',
					'additional_classes' => array( 'hide-if-no-customize' ),
				)
			);
		}

		if ( ! empty( $this->updated ) ) {
			$updated_message = sprintf(
				/* translators: %s: Home URL. */
				__( 'Background updated. <a href="%s">Visit your site</a> to see how it looks.' ),
				esc_url( home_url( '/' ) )
			);
			wp_admin_notice(
				$updated_message,
				array(
					'id'                 => 'message',
					'additional_classes' => array( 'updated' ),
				)
			);
		}
		?>

<h2><?php _e( 'Background Image' ); ?></h2>

<table class="form-table" role="presentation">
<tbody>
<tr>
<th scope="row"><?php _e( 'Preview' ); ?></th>
<td>
		<?php
		if ( $this->admin_image_div_callback ) {
			call_user_func( $this->admin_image_div_callback );
		} else {
			$background_styles = '';
			$bgcolor           = get_background_color();
			if ( $bgcolor ) {
				$background_styles .= 'background-color: ' . maybe_hash_hex_color( $bgcolor ) . ';';
			}

			$background_image_thumb = get_background_image();
			if ( $background_image_thumb ) {
				$background_image_thumb = esc_url( set_url_scheme( get_theme_mod( 'background_image_thumb', str_replace( '%', '%%', $background_image_thumb ) ) ) );
				$background_position_x  = get_theme_mod( 'background_position_x', get_theme_support( 'custom-background', 'default-position-x' ) );
				$background_position_y  = get_theme_mod( 'background_position_y', get_theme_support( 'custom-background', 'default-position-y' ) );
				$background_size        = get_theme_mod( 'background_size', get_theme_support( 'custom-background', 'default-size' ) );
				$background_repeat      = get_theme_mod( 'background_repeat', get_theme_support( 'custom-background', 'default-repeat' ) );
				$background_attachment  = get_theme_mod( 'background_attachment', get_theme_support( 'custom-background', 'default-attachment' ) );

				// Background-image URL must be single quote, see below.
				$background_styles .= " background-image: url('$background_image_thumb');"
				. " background-size: $background_size;"
				. " background-position: $background_position_x $background_position_y;"
				. " background-repeat: $background_repeat;"
				. " background-attachment: $background_attachment;";
			}
			?>
	<div id="custom-background-image" style="<?php echo $background_styles; ?>"><?php // Must be double quote, see above. ?>
			<?php if ( $background_image_thumb ) { ?>
		<img class="custom-background-image" src="<?php echo $background_image_thumb; ?>" style="visibility:hidden;" alt="" /><br />
		<img class="custom-background-image" src="<?php echo $background_image_thumb; ?>" style="visibility:hidden;" alt="" />
		<?php } ?>
	</div>
	<?php } ?>
</td>
</tr>

		<?php if ( get_background_image() ) : ?>
<tr>
<th scope="row"><?php _e( 'Remove Image' ); ?></th>
<td>
<form method="post">
			<?php wp_nonce_field( 'custom-background-remove', '_wpnonce-custom-background-remove' ); ?>
			<?php submit_button( __( 'Remove Background Image' ), '', 'remove-background', false ); ?><br />
			<?php _e( 'This will remove the background image. You will not be able to restore any customizations.' ); ?>
</form>
</td>
</tr>
		<?php endif; ?>

		<?php $default_image = get_theme_support( 'custom-background', 'default-image' ); ?>
		<?php if ( $default_image && get_background_image() !== $default_image ) : ?>
<tr>
<th scope="row"><?php _e( 'Restore Original Image' ); ?></th>
<td>
<form method="post">
			<?php wp_nonce_field( 'custom-background-reset', '_wpnonce-custom-background-reset' ); ?>
			<?php submit_button( __( 'Restore Original Image' ), '', 'reset-background', false ); ?><br />
			<?php _e( 'This will restore the original background image. You will not be able to restore any customizations.' ); ?>
</form>
</td>
</tr>
		<?php endif; ?>

		<?php if ( current_user_can( 'upload_files' ) ) : ?>
<tr>
<th scope="row"><?php _e( 'Select Image' ); ?></th>
<td><form enctype="multipart/form-data" id="upload-form" class="wp-upload-form" method="post">
	<p>
		<label for="upload"><?php _e( 'Choose an image from your computer:' ); ?></label><br />
		<input type="file" id="upload" name="import" />
		<input type="hidden" name="action" value="save" />
			<?php wp_nonce_field( 'custom-background-upload', '_wpnonce-custom-background-upload' ); ?>
			<?php submit_button( _x( 'Upload', 'verb' ), '', 'submit', false ); ?>
	</p>
	<p>
		<label for="choose-from-library-link"><?php _e( 'Or choose an image from your media library:' ); ?></label><br />
		<button id="choose-from-library-link" class="button"
			data-choose="<?php esc_attr_e( 'Choose a Background Image' ); ?>"
			data-update="<?php esc_attr_e( 'Set as background' ); ?>"><?php _e( 'Choose Image' ); ?></button>
	</p>
	</form>
</td>
</tr>
		<?php endif; ?>
</tbody>
</table>

<h2><?php _e( 'Display Options' ); ?></h2>
<form method="post">
<table class="form-table" role="presentation">
<tbody>
		<?php if ( get_background_image() ) : ?>
<input name="background-preset" type="hidden" value="custom">

			<?php
			$background_position = sprintf(
				'%s %s',
				get_theme_mod( 'background_position_x', get_theme_support( 'custom-background', 'default-position-x' ) ),
				get_theme_mod( 'background_position_y', get_theme_support( 'custom-background', 'default-position-y' ) )
			);

			$background_position_options = array(
				array(
					'left top'   => array(
						'label' => __( 'Top Left' ),
						'icon'  => 'dashicons dashicons-arrow-left-alt',
					),
					'center top' => array(
						'label' => __( 'Top' ),
						'icon'  => 'dashicons dashicons-arrow-up-alt',
					),
					'right top'  => array(
						'label' => __( 'Top Right' ),
						'icon'  => 'dashicons dashicons-arrow-right-alt',
					),
				),
				array(
					'left center'   => array(
						'label' => __( 'Left' ),
						'icon'  => 'dashicons dashicons-arrow-left-alt',
					),
					'center center' => array(
						'label' => __( 'Center' ),
						'icon'  => 'background-position-center-icon',
					),
					'right center'  => array(
						'label' => __( 'Right' ),
						'icon'  => 'dashicons dashicons-arrow-right-alt',
					),
				),
				array(
					'left bottom'   => array(
						'label' => __( 'Bottom Left' ),
						'icon'  => 'dashicons dashicons-arrow-left-alt',
					),
					'center bottom' => array(
						'label' => __( 'Bottom' ),
						'icon'  => 'dashicons dashicons-arrow-down-alt',
					),
					'right bottom'  => array(
						'label' => __( 'Bottom Right' ),
						'icon'  => 'dashicons dashicons-arrow-right-alt',
					),
				),
			);
			?>
<tr>
<th scope="row"><?php _e( 'Image Position' ); ?></th>
<td><fieldset><legend class="screen-reader-text"><span>
			<?php
			/* translators: Hidden accessibility text. */
			_e( 'Image Position' );
			?>
</span></legend>
<div class="background-position-control">
			<?php foreach ( $background_position_options as $group ) : ?>
	<div class="button-group">
				<?php foreach ( $group as $value => $input ) : ?>
		<label>
			<input class="ui-helper-hidden-accessible" name="background-position" type="radio" value="<?php echo esc_attr( $value ); ?>"<?php checked( $value, $background_position ); ?>>
			<span class="button display-options position"><span class="<?php echo esc_attr( $input['icon'] ); ?>" aria-hidden="true"></span></span>
			<span class="screen-reader-text"><?php echo $input['label']; ?></span>
		</label>
	<?php endforeach; ?>
	</div>
<?php endforeach; ?>
</div>
</fieldset></td>
</tr>

<tr>
<th scope="row"><label for="background-size"><?php _e( 'Image Size' ); ?></label></th>
<td><fieldset><legend class="screen-reader-text"><span>
			<?php
			/* translators: Hidden accessibility text. */
			_e( 'Image Size' );
			?>
</span></legend>
<select id="background-size" name="background-size">
<option value="auto"<?php selected( 'auto', get_theme_mod( 'background_size', get_theme_support( 'custom-background', 'default-size' ) ) ); ?>><?php _ex( 'Original', 'Original Size' ); ?></option>
<option value="contain"<?php selected( 'contain', get_theme_mod( 'background_size', get_theme_support( 'custom-background', 'default-size' ) ) ); ?>><?php _e( 'Fit to Screen' ); ?></option>
<option value="cover"<?php selected( 'cover', get_theme_mod( 'background_size', get_theme_support( 'custom-background', 'default-size' ) ) ); ?>><?php _e( 'Fill Screen' ); ?></option>
</select>
</fieldset></td>
</tr>

<tr>
<th scope="row"><?php _ex( 'Repeat', 'Background Repeat' ); ?></th>
<td><fieldset><legend class="screen-reader-text"><span>
			<?php
			/* translators: Hidden accessibility text. */
			_ex( 'Repeat', 'Background Repeat' );
			?>
</span></legend>
<input name="background-repeat" type="hidden" value="no-repeat">
<label><input type="checkbox" name="background-repeat" value="repeat"<?php checked( 'repeat', get_theme_mod( 'background_repeat', get_theme_support( 'custom-background', 'default-repeat' ) ) ); ?>> <?php _e( 'Repeat Background Image' ); ?></label>
</fieldset></td>
</tr>

<tr>
<th scope="row"><?php _ex( 'Scroll', 'Background Scroll' ); ?></th>
<td><fieldset><legend class="screen-reader-text"><span>
			<?php
			/* translators: Hidden accessibility text. */
			_ex( 'Scroll', 'Background Scroll' );
			?>
</span></legend>
<input name="background-attachment" type="hidden" value="fixed">
<label><input name="background-attachment" type="checkbox" value="scroll" <?php checked( 'scroll', get_theme_mod( 'background_attachment', get_theme_support( 'custom-background', 'default-attachment' ) ) ); ?>> <?php _e( 'Scroll with Page' ); ?></label>
</fieldset></td>
</tr>
<?php endif; // get_background_image() ?>
<tr>
<th scope="row"><?php _e( 'Background Color' ); ?></th>
<td><fieldset><legend class="screen-reader-text"><span>
		<?php
		/* translators: Hidden accessibility text. */
		_e( 'Background Color' );
		?>
</span></legend>
		<?php
		$default_color = '';
		if ( current_theme_supports( 'custom-background', 'default-color' ) ) {
			$default_color = ' data-default-color="#' . esc_attr( get_theme_support( 'custom-background', 'default-color' ) ) . '"';
		}
		?>
<input type="text" name="background-color" id="background-color" value="#<?php echo esc_attr( get_background_color() ); ?>"<?php echo $default_color; ?>>
</fieldset></td>
</tr>
</tbody>
</table>

		<?php wp_nonce_field( 'custom-background' ); ?>
		<?php submit_button( null, 'primary', 'save-background-options' ); ?>
</form>

</div>
		<?php
	}

	/**
	 * Handles an Image upload for the background image.
	 *
	 * @since 3.0.0
	 */
	public function handle_upload() {
		if ( empty( $_FILES ) ) {
			return;
		}

		check_admin_referer( 'custom-background-upload', '_wpnonce-custom-background-upload' );

		$overrides = array( 'test_form' => false );

		$uploaded_file = $_FILES['import'];
		$wp_filetype   = wp_check_filetype_and_ext( $uploaded_file['tmp_name'], $uploaded_file['name'] );
		if ( ! wp_match_mime_types( 'image', $wp_filetype['type'] ) ) {
			wp_die( __( 'The uploaded file is not a valid image. Please try again.' ) );
		}

		$file = wp_handle_upload( $uploaded_file, $overrides );

		if ( isset( $file['error'] ) ) {
			wp_die( $file['error'] );
		}

		$url      = $file['url'];
		$type     = $file['type'];
		$file     = $file['file'];
		$filename = wp_basename( $file );

		// Construct the attachment array.
		$attachment = array(
			'post_title'     => $filename,
			'post_content'   => $url,
			'post_mime_type' => $type,
			'guid'           => $url,
			'context'        => 'custom-background',
		);

		// Save the data.
		$id = wp_insert_attachment( $attachment, $file );

		// Add the metadata.
		wp_update_attachment_metadata( $id, wp_generate_attachment_metadata( $id, $file ) );
		update_post_meta( $id, '_wp_attachment_is_custom_background', get_option( 'stylesheet' ) );

		set_theme_mod( 'background_image', sanitize_url( $url ) );

		$thumbnail = wp_get_attachment_image_src( $id, 'thumbnail' );
		set_theme_mod( 'background_image_thumb', sanitize_url( $thumbnail[0] ) );

		/** This filter is documented in wp-admin/includes/class-custom-image-header.php */
		$file = apply_filters( 'wp_create_file_in_uploads', $file, $id ); // For replication.

		$this->updated = true;
	}

	/**
	 * Handles Ajax request for adding custom background context to an attachment.
	 *
	 * Triggers when the user adds a new background image from the
	 * Media Manager.
	 *
	 * @since 4.1.0
	 */
	public function ajax_background_add() {
		check_ajax_referer( 'background-add', 'nonce' );

		if ( ! current_user_can( 'edit_theme_options' ) ) {
			wp_send_json_error();
		}

		$attachment_id = absint( $_POST['attachment_id'] );
		if ( $attachment_id < 1 ) {
			wp_send_json_error();
		}

		update_post_meta( $attachment_id, '_wp_attachment_is_custom_background', get_stylesheet() );

		wp_send_json_success();
	}

	/**
	 * @since 3.4.0
	 * @deprecated 3.5.0
	 *
	 * @param array $form_fields
	 * @return array $form_fields
	 */
	public function attachment_fields_to_edit( $form_fields ) {
		return $form_fields;
	}

	/**
	 * @since 3.4.0
	 * @deprecated 3.5.0
	 *
	 * @param array $tabs
	 * @return array $tabs
	 */
	public function filter_upload_tabs( $tabs ) {
		return $tabs;
	}

	/**
	 * @since 3.4.0
	 * @deprecated 3.5.0
	 */
	public function wp_set_background_image() {
		check_ajax_referer( 'custom-background' );

		if ( ! current_user_can( 'edit_theme_options' ) || ! isset( $_POST['attachment_id'] ) ) {
			exit;
		}

		$attachment_id = absint( $_POST['attachment_id'] );

		$sizes = array_keys(
			/** This filter is documented in wp-admin/includes/media.php */
			apply_filters(
				'image_size_names_choose',
				array(
					'thumbnail' => __( 'Thumbnail' ),
					'medium'    => __( 'Medium' ),
					'large'     => __( 'Large' ),
					'full'      => __( 'Full Size' ),
				)
			)
		);

		$size = 'thumbnail';
		if ( in_array( $_POST['size'], $sizes, true ) ) {
			$size = esc_attr( $_POST['size'] );
		}

		update_post_meta( $attachment_id, '_wp_attachment_is_custom_background', get_option( 'stylesheet' ) );

		$url       = wp_get_attachment_image_src( $attachment_id, $size );
		$thumbnail = wp_get_attachment_image_src( $attachment_id, 'thumbnail' );
		set_theme_mod( 'background_image', sanitize_url( $url[0] ) );
		set_theme_mod( 'background_image_thumb', sanitize_url( $thumbnail[0] ) );
		exit;
	}
}
schema.php000064400000123563151327343360006536 0ustar00<?php
/**
 * WordPress Administration Scheme API
 *
 * Here we keep the DB structure and option values.
 *
 * @package WordPress
 * @subpackage Administration
 */

/**
 * Declare these as global in case schema.php is included from a function.
 *
 * @global wpdb   $wpdb            WordPress database abstraction object.
 * @global array  $wp_queries
 * @global string $charset_collate
 */
global $wpdb, $wp_queries, $charset_collate;

/**
 * The database character collate.
 */
$charset_collate = $wpdb->get_charset_collate();

/**
 * Retrieve the SQL for creating database tables.
 *
 * @since 3.3.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param string $scope   Optional. The tables for which to retrieve SQL. Can be all, global, ms_global, or blog tables. Defaults to all.
 * @param int    $blog_id Optional. The site ID for which to retrieve SQL. Default is the current site ID.
 * @return string The SQL needed to create the requested tables.
 */
function wp_get_db_schema( $scope = 'all', $blog_id = null ) {
	global $wpdb;

	$charset_collate = $wpdb->get_charset_collate();

	if ( $blog_id && (int) $blog_id !== $wpdb->blogid ) {
		$old_blog_id = $wpdb->set_blog_id( $blog_id );
	}

	// Engage multisite if in the middle of turning it on from network.php.
	$is_multisite = is_multisite() || ( defined( 'WP_INSTALLING_NETWORK' ) && WP_INSTALLING_NETWORK );

	/*
	 * Indexes have a maximum size of 767 bytes. Historically, we haven't need to be concerned about that.
	 * As of 4.2, however, we moved to utf8mb4, which uses 4 bytes per character. This means that an index which
	 * used to have room for floor(767/3) = 255 characters, now only has room for floor(767/4) = 191 characters.
	 */
	$max_index_length = 191;

	// Blog-specific tables.
	$blog_tables = "CREATE TABLE $wpdb->termmeta (
	meta_id bigint(20) unsigned NOT NULL auto_increment,
	term_id bigint(20) unsigned NOT NULL default '0',
	meta_key varchar(255) default NULL,
	meta_value longtext,
	PRIMARY KEY  (meta_id),
	KEY term_id (term_id),
	KEY meta_key (meta_key($max_index_length))
) $charset_collate;
CREATE TABLE $wpdb->terms (
 term_id bigint(20) unsigned NOT NULL auto_increment,
 name varchar(200) NOT NULL default '',
 slug varchar(200) NOT NULL default '',
 term_group bigint(10) NOT NULL default 0,
 PRIMARY KEY  (term_id),
 KEY slug (slug($max_index_length)),
 KEY name (name($max_index_length))
) $charset_collate;
CREATE TABLE $wpdb->term_taxonomy (
 term_taxonomy_id bigint(20) unsigned NOT NULL auto_increment,
 term_id bigint(20) unsigned NOT NULL default 0,
 taxonomy varchar(32) NOT NULL default '',
 description longtext NOT NULL,
 parent bigint(20) unsigned NOT NULL default 0,
 count bigint(20) NOT NULL default 0,
 PRIMARY KEY  (term_taxonomy_id),
 UNIQUE KEY term_id_taxonomy (term_id,taxonomy),
 KEY taxonomy (taxonomy)
) $charset_collate;
CREATE TABLE $wpdb->term_relationships (
 object_id bigint(20) unsigned NOT NULL default 0,
 term_taxonomy_id bigint(20) unsigned NOT NULL default 0,
 term_order int(11) NOT NULL default 0,
 PRIMARY KEY  (object_id,term_taxonomy_id),
 KEY term_taxonomy_id (term_taxonomy_id)
) $charset_collate;
CREATE TABLE $wpdb->commentmeta (
	meta_id bigint(20) unsigned NOT NULL auto_increment,
	comment_id bigint(20) unsigned NOT NULL default '0',
	meta_key varchar(255) default NULL,
	meta_value longtext,
	PRIMARY KEY  (meta_id),
	KEY comment_id (comment_id),
	KEY meta_key (meta_key($max_index_length))
) $charset_collate;
CREATE TABLE $wpdb->comments (
	comment_ID bigint(20) unsigned NOT NULL auto_increment,
	comment_post_ID bigint(20) unsigned NOT NULL default '0',
	comment_author tinytext NOT NULL,
	comment_author_email varchar(100) NOT NULL default '',
	comment_author_url varchar(200) NOT NULL default '',
	comment_author_IP varchar(100) NOT NULL default '',
	comment_date datetime NOT NULL default '0000-00-00 00:00:00',
	comment_date_gmt datetime NOT NULL default '0000-00-00 00:00:00',
	comment_content text NOT NULL,
	comment_karma int(11) NOT NULL default '0',
	comment_approved varchar(20) NOT NULL default '1',
	comment_agent varchar(255) NOT NULL default '',
	comment_type varchar(20) NOT NULL default 'comment',
	comment_parent bigint(20) unsigned NOT NULL default '0',
	user_id bigint(20) unsigned NOT NULL default '0',
	PRIMARY KEY  (comment_ID),
	KEY comment_post_ID (comment_post_ID),
	KEY comment_approved_date_gmt (comment_approved,comment_date_gmt),
	KEY comment_date_gmt (comment_date_gmt),
	KEY comment_parent (comment_parent),
	KEY comment_author_email (comment_author_email(10))
) $charset_collate;
CREATE TABLE $wpdb->links (
	link_id bigint(20) unsigned NOT NULL auto_increment,
	link_url varchar(255) NOT NULL default '',
	link_name varchar(255) NOT NULL default '',
	link_image varchar(255) NOT NULL default '',
	link_target varchar(25) NOT NULL default '',
	link_description varchar(255) NOT NULL default '',
	link_visible varchar(20) NOT NULL default 'Y',
	link_owner bigint(20) unsigned NOT NULL default '1',
	link_rating int(11) NOT NULL default '0',
	link_updated datetime NOT NULL default '0000-00-00 00:00:00',
	link_rel varchar(255) NOT NULL default '',
	link_notes mediumtext NOT NULL,
	link_rss varchar(255) NOT NULL default '',
	PRIMARY KEY  (link_id),
	KEY link_visible (link_visible)
) $charset_collate;
CREATE TABLE $wpdb->options (
	option_id bigint(20) unsigned NOT NULL auto_increment,
	option_name varchar(191) NOT NULL default '',
	option_value longtext NOT NULL,
	autoload varchar(20) NOT NULL default 'yes',
	PRIMARY KEY  (option_id),
	UNIQUE KEY option_name (option_name),
	KEY autoload (autoload)
) $charset_collate;
CREATE TABLE $wpdb->postmeta (
	meta_id bigint(20) unsigned NOT NULL auto_increment,
	post_id bigint(20) unsigned NOT NULL default '0',
	meta_key varchar(255) default NULL,
	meta_value longtext,
	PRIMARY KEY  (meta_id),
	KEY post_id (post_id),
	KEY meta_key (meta_key($max_index_length))
) $charset_collate;
CREATE TABLE $wpdb->posts (
	ID bigint(20) unsigned NOT NULL auto_increment,
	post_author bigint(20) unsigned NOT NULL default '0',
	post_date datetime NOT NULL default '0000-00-00 00:00:00',
	post_date_gmt datetime NOT NULL default '0000-00-00 00:00:00',
	post_content longtext NOT NULL,
	post_title text NOT NULL,
	post_excerpt text NOT NULL,
	post_status varchar(20) NOT NULL default 'publish',
	comment_status varchar(20) NOT NULL default 'open',
	ping_status varchar(20) NOT NULL default 'open',
	post_password varchar(255) NOT NULL default '',
	post_name varchar(200) NOT NULL default '',
	to_ping text NOT NULL,
	pinged text NOT NULL,
	post_modified datetime NOT NULL default '0000-00-00 00:00:00',
	post_modified_gmt datetime NOT NULL default '0000-00-00 00:00:00',
	post_content_filtered longtext NOT NULL,
	post_parent bigint(20) unsigned NOT NULL default '0',
	guid varchar(255) NOT NULL default '',
	menu_order int(11) NOT NULL default '0',
	post_type varchar(20) NOT NULL default 'post',
	post_mime_type varchar(100) NOT NULL default '',
	comment_count bigint(20) NOT NULL default '0',
	PRIMARY KEY  (ID),
	KEY post_name (post_name($max_index_length)),
	KEY type_status_date (post_type,post_status,post_date,ID),
	KEY post_parent (post_parent),
	KEY post_author (post_author)
) $charset_collate;\n";

	// Single site users table. The multisite flavor of the users table is handled below.
	$users_single_table = "CREATE TABLE $wpdb->users (
	ID bigint(20) unsigned NOT NULL auto_increment,
	user_login varchar(60) NOT NULL default '',
	user_pass varchar(255) NOT NULL default '',
	user_nicename varchar(50) NOT NULL default '',
	user_email varchar(100) NOT NULL default '',
	user_url varchar(100) NOT NULL default '',
	user_registered datetime NOT NULL default '0000-00-00 00:00:00',
	user_activation_key varchar(255) NOT NULL default '',
	user_status int(11) NOT NULL default '0',
	display_name varchar(250) NOT NULL default '',
	PRIMARY KEY  (ID),
	KEY user_login_key (user_login),
	KEY user_nicename (user_nicename),
	KEY user_email (user_email)
) $charset_collate;\n";

	// Multisite users table.
	$users_multi_table = "CREATE TABLE $wpdb->users (
	ID bigint(20) unsigned NOT NULL auto_increment,
	user_login varchar(60) NOT NULL default '',
	user_pass varchar(255) NOT NULL default '',
	user_nicename varchar(50) NOT NULL default '',
	user_email varchar(100) NOT NULL default '',
	user_url varchar(100) NOT NULL default '',
	user_registered datetime NOT NULL default '0000-00-00 00:00:00',
	user_activation_key varchar(255) NOT NULL default '',
	user_status int(11) NOT NULL default '0',
	display_name varchar(250) NOT NULL default '',
	spam tinyint(2) NOT NULL default '0',
	deleted tinyint(2) NOT NULL default '0',
	PRIMARY KEY  (ID),
	KEY user_login_key (user_login),
	KEY user_nicename (user_nicename),
	KEY user_email (user_email)
) $charset_collate;\n";

	// Usermeta.
	$usermeta_table = "CREATE TABLE $wpdb->usermeta (
	umeta_id bigint(20) unsigned NOT NULL auto_increment,
	user_id bigint(20) unsigned NOT NULL default '0',
	meta_key varchar(255) default NULL,
	meta_value longtext,
	PRIMARY KEY  (umeta_id),
	KEY user_id (user_id),
	KEY meta_key (meta_key($max_index_length))
) $charset_collate;\n";

	// Global tables.
	if ( $is_multisite ) {
		$global_tables = $users_multi_table . $usermeta_table;
	} else {
		$global_tables = $users_single_table . $usermeta_table;
	}

	// Multisite global tables.
	$ms_global_tables = "CREATE TABLE $wpdb->blogs (
	blog_id bigint(20) NOT NULL auto_increment,
	site_id bigint(20) NOT NULL default '0',
	domain varchar(200) NOT NULL default '',
	path varchar(100) NOT NULL default '',
	registered datetime NOT NULL default '0000-00-00 00:00:00',
	last_updated datetime NOT NULL default '0000-00-00 00:00:00',
	public tinyint(2) NOT NULL default '1',
	archived tinyint(2) NOT NULL default '0',
	mature tinyint(2) NOT NULL default '0',
	spam tinyint(2) NOT NULL default '0',
	deleted tinyint(2) NOT NULL default '0',
	lang_id int(11) NOT NULL default '0',
	PRIMARY KEY  (blog_id),
	KEY domain (domain(50),path(5)),
	KEY lang_id (lang_id)
) $charset_collate;
CREATE TABLE $wpdb->blogmeta (
	meta_id bigint(20) unsigned NOT NULL auto_increment,
	blog_id bigint(20) NOT NULL default '0',
	meta_key varchar(255) default NULL,
	meta_value longtext,
	PRIMARY KEY  (meta_id),
	KEY meta_key (meta_key($max_index_length)),
	KEY blog_id (blog_id)
) $charset_collate;
CREATE TABLE $wpdb->registration_log (
	ID bigint(20) NOT NULL auto_increment,
	email varchar(255) NOT NULL default '',
	IP varchar(30) NOT NULL default '',
	blog_id bigint(20) NOT NULL default '0',
	date_registered datetime NOT NULL default '0000-00-00 00:00:00',
	PRIMARY KEY  (ID),
	KEY IP (IP)
) $charset_collate;
CREATE TABLE $wpdb->site (
	id bigint(20) NOT NULL auto_increment,
	domain varchar(200) NOT NULL default '',
	path varchar(100) NOT NULL default '',
	PRIMARY KEY  (id),
	KEY domain (domain(140),path(51))
) $charset_collate;
CREATE TABLE $wpdb->sitemeta (
	meta_id bigint(20) NOT NULL auto_increment,
	site_id bigint(20) NOT NULL default '0',
	meta_key varchar(255) default NULL,
	meta_value longtext,
	PRIMARY KEY  (meta_id),
	KEY meta_key (meta_key($max_index_length)),
	KEY site_id (site_id)
) $charset_collate;
CREATE TABLE $wpdb->signups (
	signup_id bigint(20) NOT NULL auto_increment,
	domain varchar(200) NOT NULL default '',
	path varchar(100) NOT NULL default '',
	title longtext NOT NULL,
	user_login varchar(60) NOT NULL default '',
	user_email varchar(100) NOT NULL default '',
	registered datetime NOT NULL default '0000-00-00 00:00:00',
	activated datetime NOT NULL default '0000-00-00 00:00:00',
	active tinyint(1) NOT NULL default '0',
	activation_key varchar(50) NOT NULL default '',
	meta longtext,
	PRIMARY KEY  (signup_id),
	KEY activation_key (activation_key),
	KEY user_email (user_email),
	KEY user_login_email (user_login,user_email),
	KEY domain_path (domain(140),path(51))
) $charset_collate;";

	switch ( $scope ) {
		case 'blog':
			$queries = $blog_tables;
			break;
		case 'global':
			$queries = $global_tables;
			if ( $is_multisite ) {
				$queries .= $ms_global_tables;
			}
			break;
		case 'ms_global':
			$queries = $ms_global_tables;
			break;
		case 'all':
		default:
			$queries = $global_tables . $blog_tables;
			if ( $is_multisite ) {
				$queries .= $ms_global_tables;
			}
			break;
	}

	if ( isset( $old_blog_id ) ) {
		$wpdb->set_blog_id( $old_blog_id );
	}

	return $queries;
}

// Populate for back compat.
$wp_queries = wp_get_db_schema( 'all' );

/**
 * Create WordPress options and set the default values.
 *
 * @since 1.5.0
 * @since 5.1.0 The $options parameter has been added.
 *
 * @global wpdb $wpdb                  WordPress database abstraction object.
 * @global int  $wp_db_version         WordPress database version.
 * @global int  $wp_current_db_version The old (current) database version.
 *
 * @param array $options Optional. Custom option $key => $value pairs to use. Default empty array.
 */
function populate_options( array $options = array() ) {
	global $wpdb, $wp_db_version, $wp_current_db_version;

	$guessurl = wp_guess_url();
	/**
	 * Fires before creating WordPress options and populating their default values.
	 *
	 * @since 2.6.0
	 */
	do_action( 'populate_options' );

	// If WP_DEFAULT_THEME doesn't exist, fall back to the latest core default theme.
	$stylesheet = WP_DEFAULT_THEME;
	$template   = WP_DEFAULT_THEME;
	$theme      = wp_get_theme( WP_DEFAULT_THEME );
	if ( ! $theme->exists() ) {
		$theme = WP_Theme::get_core_default_theme();
	}

	// If we can't find a core default theme, WP_DEFAULT_THEME is the best we can do.
	if ( $theme ) {
		$stylesheet = $theme->get_stylesheet();
		$template   = $theme->get_template();
	}

	$timezone_string = '';
	$gmt_offset      = 0;
	/*
	 * translators: default GMT offset or timezone string. Must be either a valid offset (-12 to 14)
	 * or a valid timezone string (America/New_York). See https://www.php.net/manual/en/timezones.php
	 * for all timezone strings currently supported by PHP.
	 *
	 * Important: When a previous timezone string, like `Europe/Kiev`, has been superseded by an
	 * updated one, like `Europe/Kyiv`, as a rule of thumb, the **old** timezone name should be used
	 * in the "translation" to allow for the default timezone setting to be PHP cross-version compatible,
	 * as old timezone names will be recognized in new PHP versions, while new timezone names cannot
	 * be recognized in old PHP versions.
	 *
	 * To verify which timezone strings are available in the _oldest_ PHP version supported, you can
	 * use https://3v4l.org/6YQAt#v5.6.20 and replace the "BR" (Brazil) in the code line with the
	 * country code for which you want to look up the supported timezone names.
	 */
	$offset_or_tz = _x( '0', 'default GMT offset or timezone string' );
	if ( is_numeric( $offset_or_tz ) ) {
		$gmt_offset = $offset_or_tz;
	} elseif ( $offset_or_tz && in_array( $offset_or_tz, timezone_identifiers_list( DateTimeZone::ALL_WITH_BC ), true ) ) {
		$timezone_string = $offset_or_tz;
	}

	$defaults = array(
		'siteurl'                         => $guessurl,
		'home'                            => $guessurl,
		'blogname'                        => __( 'My Site' ),
		'blogdescription'                 => '',
		'users_can_register'              => 0,
		'admin_email'                     => 'you@example.com',
		/* translators: Default start of the week. 0 = Sunday, 1 = Monday. */
		'start_of_week'                   => _x( '1', 'start of week' ),
		'use_balanceTags'                 => 0,
		'use_smilies'                     => 1,
		'require_name_email'              => 1,
		'comments_notify'                 => 1,
		'posts_per_rss'                   => 10,
		'rss_use_excerpt'                 => 0,
		'mailserver_url'                  => 'mail.example.com',
		'mailserver_login'                => 'login@example.com',
		'mailserver_pass'                 => '',
		'mailserver_port'                 => 110,
		'default_category'                => 1,
		'default_comment_status'          => 'open',
		'default_ping_status'             => 'open',
		'default_pingback_flag'           => 1,
		'posts_per_page'                  => 10,
		/* translators: Default date format, see https://www.php.net/manual/datetime.format.php */
		'date_format'                     => __( 'F j, Y' ),
		/* translators: Default time format, see https://www.php.net/manual/datetime.format.php */
		'time_format'                     => __( 'g:i a' ),
		/* translators: Links last updated date format, see https://www.php.net/manual/datetime.format.php */
		'links_updated_date_format'       => __( 'F j, Y g:i a' ),
		'comment_moderation'              => 0,
		'moderation_notify'               => 1,
		'permalink_structure'             => '',
		'rewrite_rules'                   => '',
		'hack_file'                       => 0,
		'blog_charset'                    => 'UTF-8',
		'moderation_keys'                 => '',
		'active_plugins'                  => array(),
		'category_base'                   => '',
		'ping_sites'                      => 'https://rpc.pingomatic.com/',
		'comment_max_links'               => 2,
		'gmt_offset'                      => $gmt_offset,

		// 1.5.0
		'default_email_category'          => 1,
		'recently_edited'                 => '',
		'template'                        => $template,
		'stylesheet'                      => $stylesheet,
		'comment_registration'            => 0,
		'html_type'                       => 'text/html',

		// 1.5.1
		'use_trackback'                   => 0,

		// 2.0.0
		'default_role'                    => 'subscriber',
		'db_version'                      => $wp_db_version,

		// 2.0.1
		'uploads_use_yearmonth_folders'   => 1,
		'upload_path'                     => '',

		// 2.1.0
		'blog_public'                     => '1',
		'default_link_category'           => 2,
		'show_on_front'                   => 'posts',

		// 2.2.0
		'tag_base'                        => '',

		// 2.5.0
		'show_avatars'                    => '1',
		'avatar_rating'                   => 'G',
		'upload_url_path'                 => '',
		'thumbnail_size_w'                => 150,
		'thumbnail_size_h'                => 150,
		'thumbnail_crop'                  => 1,
		'medium_size_w'                   => 300,
		'medium_size_h'                   => 300,

		// 2.6.0
		'avatar_default'                  => 'mystery',

		// 2.7.0
		'large_size_w'                    => 1024,
		'large_size_h'                    => 1024,
		'image_default_link_type'         => 'none',
		'image_default_size'              => '',
		'image_default_align'             => '',
		'close_comments_for_old_posts'    => 0,
		'close_comments_days_old'         => 14,
		'thread_comments'                 => 1,
		'thread_comments_depth'           => 5,
		'page_comments'                   => 0,
		'comments_per_page'               => 50,
		'default_comments_page'           => 'newest',
		'comment_order'                   => 'asc',
		'sticky_posts'                    => array(),
		'widget_categories'               => array(),
		'widget_text'                     => array(),
		'widget_rss'                      => array(),
		'uninstall_plugins'               => array(),

		// 2.8.0
		'timezone_string'                 => $timezone_string,

		// 3.0.0
		'page_for_posts'                  => 0,
		'page_on_front'                   => 0,

		// 3.1.0
		'default_post_format'             => 0,

		// 3.5.0
		'link_manager_enabled'            => 0,

		// 4.3.0
		'finished_splitting_shared_terms' => 1,
		'site_icon'                       => 0,

		// 4.4.0
		'medium_large_size_w'             => 768,
		'medium_large_size_h'             => 0,

		// 4.9.6
		'wp_page_for_privacy_policy'      => 0,

		// 4.9.8
		'show_comments_cookies_opt_in'    => 1,

		// 5.3.0
		'admin_email_lifespan'            => ( time() + 6 * MONTH_IN_SECONDS ),

		// 5.5.0
		'disallowed_keys'                 => '',
		'comment_previously_approved'     => 1,
		'auto_plugin_theme_update_emails' => array(),

		// 5.6.0
		'auto_update_core_dev'            => 'enabled',
		'auto_update_core_minor'          => 'enabled',
		/*
		 * Default to enabled for new installs.
		 * See https://core.trac.wordpress.org/ticket/51742.
		 */
		'auto_update_core_major'          => 'enabled',

		// 5.8.0
		'wp_force_deactivated_plugins'    => array(),

		// 6.4.0
		'wp_attachment_pages_enabled'     => 0,
	);

	// 3.3.0
	if ( ! is_multisite() ) {
		$defaults['initial_db_version'] = ! empty( $wp_current_db_version ) && $wp_current_db_version < $wp_db_version
			? $wp_current_db_version : $wp_db_version;
	}

	// 3.0.0 multisite.
	if ( is_multisite() ) {
		$defaults['permalink_structure'] = '/%year%/%monthnum%/%day%/%postname%/';
	}

	$options = wp_parse_args( $options, $defaults );

	// Set autoload to no for these options.
	$fat_options = array(
		'moderation_keys',
		'recently_edited',
		'disallowed_keys',
		'uninstall_plugins',
		'auto_plugin_theme_update_emails',
	);

	$keys             = "'" . implode( "', '", array_keys( $options ) ) . "'";
	$existing_options = $wpdb->get_col( "SELECT option_name FROM $wpdb->options WHERE option_name in ( $keys )" ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

	$insert = '';

	foreach ( $options as $option => $value ) {
		if ( in_array( $option, $existing_options, true ) ) {
			continue;
		}

		if ( in_array( $option, $fat_options, true ) ) {
			$autoload = 'off';
		} else {
			$autoload = 'on';
		}

		if ( ! empty( $insert ) ) {
			$insert .= ', ';
		}

		$value = maybe_serialize( sanitize_option( $option, $value ) );

		$insert .= $wpdb->prepare( '(%s, %s, %s)', $option, $value, $autoload );
	}

	if ( ! empty( $insert ) ) {
		$wpdb->query( "INSERT INTO $wpdb->options (option_name, option_value, autoload) VALUES " . $insert ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
	}

	// In case it is set, but blank, update "home".
	if ( ! __get_option( 'home' ) ) {
		update_option( 'home', $guessurl );
	}

	// Delete unused options.
	$unusedoptions = array(
		'blodotgsping_url',
		'bodyterminator',
		'emailtestonly',
		'phoneemail_separator',
		'smilies_directory',
		'subjectprefix',
		'use_bbcode',
		'use_blodotgsping',
		'use_phoneemail',
		'use_quicktags',
		'use_weblogsping',
		'weblogs_cache_file',
		'use_preview',
		'use_htmltrans',
		'smilies_directory',
		'fileupload_allowedusers',
		'use_phoneemail',
		'default_post_status',
		'default_post_category',
		'archive_mode',
		'time_difference',
		'links_minadminlevel',
		'links_use_adminlevels',
		'links_rating_type',
		'links_rating_char',
		'links_rating_ignore_zero',
		'links_rating_single_image',
		'links_rating_image0',
		'links_rating_image1',
		'links_rating_image2',
		'links_rating_image3',
		'links_rating_image4',
		'links_rating_image5',
		'links_rating_image6',
		'links_rating_image7',
		'links_rating_image8',
		'links_rating_image9',
		'links_recently_updated_time',
		'links_recently_updated_prepend',
		'links_recently_updated_append',
		'weblogs_cacheminutes',
		'comment_allowed_tags',
		'search_engine_friendly_urls',
		'default_geourl_lat',
		'default_geourl_lon',
		'use_default_geourl',
		'weblogs_xml_url',
		'new_users_can_blog',
		'_wpnonce',
		'_wp_http_referer',
		'Update',
		'action',
		'rich_editing',
		'autosave_interval',
		'deactivated_plugins',
		'can_compress_scripts',
		'page_uris',
		'update_core',
		'update_plugins',
		'update_themes',
		'doing_cron',
		'random_seed',
		'rss_excerpt_length',
		'secret',
		'use_linksupdate',
		'default_comment_status_page',
		'wporg_popular_tags',
		'what_to_show',
		'rss_language',
		'language',
		'enable_xmlrpc',
		'enable_app',
		'embed_autourls',
		'default_post_edit_rows',
		'gzipcompression',
		'advanced_edit',
	);
	foreach ( $unusedoptions as $option ) {
		delete_option( $option );
	}

	// Delete obsolete magpie stuff.
	$wpdb->query( "DELETE FROM $wpdb->options WHERE option_name REGEXP '^rss_[0-9a-f]{32}(_ts)?$'" );

	// Clear expired transients.
	delete_expired_transients( true );
}

/**
 * Execute WordPress role creation for the various WordPress versions.
 *
 * @since 2.0.0
 */
function populate_roles() {
	populate_roles_160();
	populate_roles_210();
	populate_roles_230();
	populate_roles_250();
	populate_roles_260();
	populate_roles_270();
	populate_roles_280();
	populate_roles_300();
}

/**
 * Create the roles for WordPress 2.0
 *
 * @since 2.0.0
 */
function populate_roles_160() {
	// Add roles.
	add_role( 'administrator', 'Administrator' );
	add_role( 'editor', 'Editor' );
	add_role( 'author', 'Author' );
	add_role( 'contributor', 'Contributor' );
	add_role( 'subscriber', 'Subscriber' );

	// Add caps for Administrator role.
	$role = get_role( 'administrator' );
	$role->add_cap( 'switch_themes' );
	$role->add_cap( 'edit_themes' );
	$role->add_cap( 'activate_plugins' );
	$role->add_cap( 'edit_plugins' );
	$role->add_cap( 'edit_users' );
	$role->add_cap( 'edit_files' );
	$role->add_cap( 'manage_options' );
	$role->add_cap( 'moderate_comments' );
	$role->add_cap( 'manage_categories' );
	$role->add_cap( 'manage_links' );
	$role->add_cap( 'upload_files' );
	$role->add_cap( 'import' );
	$role->add_cap( 'unfiltered_html' );
	$role->add_cap( 'edit_posts' );
	$role->add_cap( 'edit_others_posts' );
	$role->add_cap( 'edit_published_posts' );
	$role->add_cap( 'publish_posts' );
	$role->add_cap( 'edit_pages' );
	$role->add_cap( 'read' );
	$role->add_cap( 'level_10' );
	$role->add_cap( 'level_9' );
	$role->add_cap( 'level_8' );
	$role->add_cap( 'level_7' );
	$role->add_cap( 'level_6' );
	$role->add_cap( 'level_5' );
	$role->add_cap( 'level_4' );
	$role->add_cap( 'level_3' );
	$role->add_cap( 'level_2' );
	$role->add_cap( 'level_1' );
	$role->add_cap( 'level_0' );

	// Add caps for Editor role.
	$role = get_role( 'editor' );
	$role->add_cap( 'moderate_comments' );
	$role->add_cap( 'manage_categories' );
	$role->add_cap( 'manage_links' );
	$role->add_cap( 'upload_files' );
	$role->add_cap( 'unfiltered_html' );
	$role->add_cap( 'edit_posts' );
	$role->add_cap( 'edit_others_posts' );
	$role->add_cap( 'edit_published_posts' );
	$role->add_cap( 'publish_posts' );
	$role->add_cap( 'edit_pages' );
	$role->add_cap( 'read' );
	$role->add_cap( 'level_7' );
	$role->add_cap( 'level_6' );
	$role->add_cap( 'level_5' );
	$role->add_cap( 'level_4' );
	$role->add_cap( 'level_3' );
	$role->add_cap( 'level_2' );
	$role->add_cap( 'level_1' );
	$role->add_cap( 'level_0' );

	// Add caps for Author role.
	$role = get_role( 'author' );
	$role->add_cap( 'upload_files' );
	$role->add_cap( 'edit_posts' );
	$role->add_cap( 'edit_published_posts' );
	$role->add_cap( 'publish_posts' );
	$role->add_cap( 'read' );
	$role->add_cap( 'level_2' );
	$role->add_cap( 'level_1' );
	$role->add_cap( 'level_0' );

	// Add caps for Contributor role.
	$role = get_role( 'contributor' );
	$role->add_cap( 'edit_posts' );
	$role->add_cap( 'read' );
	$role->add_cap( 'level_1' );
	$role->add_cap( 'level_0' );

	// Add caps for Subscriber role.
	$role = get_role( 'subscriber' );
	$role->add_cap( 'read' );
	$role->add_cap( 'level_0' );
}

/**
 * Create and modify WordPress roles for WordPress 2.1.
 *
 * @since 2.1.0
 */
function populate_roles_210() {
	$roles = array( 'administrator', 'editor' );
	foreach ( $roles as $role ) {
		$role = get_role( $role );
		if ( empty( $role ) ) {
			continue;
		}

		$role->add_cap( 'edit_others_pages' );
		$role->add_cap( 'edit_published_pages' );
		$role->add_cap( 'publish_pages' );
		$role->add_cap( 'delete_pages' );
		$role->add_cap( 'delete_others_pages' );
		$role->add_cap( 'delete_published_pages' );
		$role->add_cap( 'delete_posts' );
		$role->add_cap( 'delete_others_posts' );
		$role->add_cap( 'delete_published_posts' );
		$role->add_cap( 'delete_private_posts' );
		$role->add_cap( 'edit_private_posts' );
		$role->add_cap( 'read_private_posts' );
		$role->add_cap( 'delete_private_pages' );
		$role->add_cap( 'edit_private_pages' );
		$role->add_cap( 'read_private_pages' );
	}

	$role = get_role( 'administrator' );
	if ( ! empty( $role ) ) {
		$role->add_cap( 'delete_users' );
		$role->add_cap( 'create_users' );
	}

	$role = get_role( 'author' );
	if ( ! empty( $role ) ) {
		$role->add_cap( 'delete_posts' );
		$role->add_cap( 'delete_published_posts' );
	}

	$role = get_role( 'contributor' );
	if ( ! empty( $role ) ) {
		$role->add_cap( 'delete_posts' );
	}
}

/**
 * Create and modify WordPress roles for WordPress 2.3.
 *
 * @since 2.3.0
 */
function populate_roles_230() {
	$role = get_role( 'administrator' );

	if ( ! empty( $role ) ) {
		$role->add_cap( 'unfiltered_upload' );
	}
}

/**
 * Create and modify WordPress roles for WordPress 2.5.
 *
 * @since 2.5.0
 */
function populate_roles_250() {
	$role = get_role( 'administrator' );

	if ( ! empty( $role ) ) {
		$role->add_cap( 'edit_dashboard' );
	}
}

/**
 * Create and modify WordPress roles for WordPress 2.6.
 *
 * @since 2.6.0
 */
function populate_roles_260() {
	$role = get_role( 'administrator' );

	if ( ! empty( $role ) ) {
		$role->add_cap( 'update_plugins' );
		$role->add_cap( 'delete_plugins' );
	}
}

/**
 * Create and modify WordPress roles for WordPress 2.7.
 *
 * @since 2.7.0
 */
function populate_roles_270() {
	$role = get_role( 'administrator' );

	if ( ! empty( $role ) ) {
		$role->add_cap( 'install_plugins' );
		$role->add_cap( 'update_themes' );
	}
}

/**
 * Create and modify WordPress roles for WordPress 2.8.
 *
 * @since 2.8.0
 */
function populate_roles_280() {
	$role = get_role( 'administrator' );

	if ( ! empty( $role ) ) {
		$role->add_cap( 'install_themes' );
	}
}

/**
 * Create and modify WordPress roles for WordPress 3.0.
 *
 * @since 3.0.0
 */
function populate_roles_300() {
	$role = get_role( 'administrator' );

	if ( ! empty( $role ) ) {
		$role->add_cap( 'update_core' );
		$role->add_cap( 'list_users' );
		$role->add_cap( 'remove_users' );
		$role->add_cap( 'promote_users' );
		$role->add_cap( 'edit_theme_options' );
		$role->add_cap( 'delete_themes' );
		$role->add_cap( 'export' );
	}
}

if ( ! function_exists( 'install_network' ) ) :
	/**
	 * Install Network.
	 *
	 * @since 3.0.0
	 */
	function install_network() {
		if ( ! defined( 'WP_INSTALLING_NETWORK' ) ) {
			define( 'WP_INSTALLING_NETWORK', true );
		}

		dbDelta( wp_get_db_schema( 'global' ) );
	}
endif;

/**
 * Populate network settings.
 *
 * @since 3.0.0
 *
 * @global wpdb       $wpdb         WordPress database abstraction object.
 * @global object     $current_site
 * @global WP_Rewrite $wp_rewrite   WordPress rewrite component.
 *
 * @param int    $network_id        ID of network to populate.
 * @param string $domain            The domain name for the network. Example: "example.com".
 * @param string $email             Email address for the network administrator.
 * @param string $site_name         The name of the network.
 * @param string $path              Optional. The path to append to the network's domain name. Default '/'.
 * @param bool   $subdomain_install Optional. Whether the network is a subdomain installation or a subdirectory installation.
 *                                  Default false, meaning the network is a subdirectory installation.
 * @return true|WP_Error True on success, or WP_Error on warning (with the installation otherwise successful,
 *                       so the error code must be checked) or failure.
 */
function populate_network( $network_id = 1, $domain = '', $email = '', $site_name = '', $path = '/', $subdomain_install = false ) {
	global $wpdb, $current_site, $wp_rewrite;

	$network_id = (int) $network_id;

	$errors = new WP_Error();
	if ( '' === $domain ) {
		$errors->add( 'empty_domain', __( 'You must provide a domain name.' ) );
	}
	if ( '' === $site_name ) {
		$errors->add( 'empty_sitename', __( 'You must provide a name for your network of sites.' ) );
	}

	// Check for network collision.
	$network_exists = false;
	if ( is_multisite() ) {
		if ( get_network( $network_id ) ) {
			$errors->add( 'siteid_exists', __( 'The network already exists.' ) );
		}
	} else {
		if ( $network_id === (int) $wpdb->get_var(
			$wpdb->prepare( "SELECT id FROM $wpdb->site WHERE id = %d", $network_id )
		) ) {
			$errors->add( 'siteid_exists', __( 'The network already exists.' ) );
		}
	}

	if ( ! is_email( $email ) ) {
		$errors->add( 'invalid_email', __( 'You must provide a valid email address.' ) );
	}

	if ( $errors->has_errors() ) {
		return $errors;
	}

	if ( 1 === $network_id ) {
		$wpdb->insert(
			$wpdb->site,
			array(
				'domain' => $domain,
				'path'   => $path,
			)
		);
		$network_id = $wpdb->insert_id;
	} else {
		$wpdb->insert(
			$wpdb->site,
			array(
				'domain' => $domain,
				'path'   => $path,
				'id'     => $network_id,
			)
		);
	}

	populate_network_meta(
		$network_id,
		array(
			'admin_email'       => $email,
			'site_name'         => $site_name,
			'subdomain_install' => $subdomain_install,
		)
	);

	// Remove the cron event since Recovery Mode is not used in Multisite.
	if ( wp_next_scheduled( 'recovery_mode_clean_expired_keys' ) ) {
		wp_clear_scheduled_hook( 'recovery_mode_clean_expired_keys' );
	}

	/*
	 * When upgrading from single to multisite, assume the current site will
	 * become the main site of the network. When using populate_network()
	 * to create another network in an existing multisite environment, skip
	 * these steps since the main site of the new network has not yet been
	 * created.
	 */
	if ( ! is_multisite() ) {
		$current_site            = new stdClass();
		$current_site->domain    = $domain;
		$current_site->path      = $path;
		$current_site->site_name = ucfirst( $domain );
		$wpdb->insert(
			$wpdb->blogs,
			array(
				'site_id'    => $network_id,
				'blog_id'    => 1,
				'domain'     => $domain,
				'path'       => $path,
				'registered' => current_time( 'mysql' ),
			)
		);
		$current_site->blog_id = $wpdb->insert_id;

		$site_user_id = (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT meta_value
				FROM $wpdb->sitemeta
				WHERE meta_key = %s AND site_id = %d",
				'admin_user_id',
				$network_id
			)
		);

		update_user_meta( $site_user_id, 'source_domain', $domain );
		update_user_meta( $site_user_id, 'primary_blog', $current_site->blog_id );

		// Unable to use update_network_option() while populating the network.
		$wpdb->insert(
			$wpdb->sitemeta,
			array(
				'site_id'    => $network_id,
				'meta_key'   => 'main_site',
				'meta_value' => $current_site->blog_id,
			)
		);

		if ( $subdomain_install ) {
			$wp_rewrite->set_permalink_structure( '/%year%/%monthnum%/%day%/%postname%/' );
		} else {
			$wp_rewrite->set_permalink_structure( '/blog/%year%/%monthnum%/%day%/%postname%/' );
		}

		flush_rewrite_rules();

		if ( ! $subdomain_install ) {
			return true;
		}

		$vhost_ok = false;
		$errstr   = '';
		$hostname = substr( md5( time() ), 0, 6 ) . '.' . $domain; // Very random hostname!
		$page     = wp_remote_get(
			'http://' . $hostname,
			array(
				'timeout'     => 5,
				'httpversion' => '1.1',
			)
		);
		if ( is_wp_error( $page ) ) {
			$errstr = $page->get_error_message();
		} elseif ( 200 === wp_remote_retrieve_response_code( $page ) ) {
				$vhost_ok = true;
		}

		if ( ! $vhost_ok ) {
			$msg = '<p><strong>' . __( 'Warning! Wildcard DNS may not be configured correctly!' ) . '</strong></p>';

			$msg .= '<p>' . sprintf(
				/* translators: %s: Host name. */
				__( 'The installer attempted to contact a random hostname (%s) on your domain.' ),
				'<code>' . $hostname . '</code>'
			);
			if ( ! empty( $errstr ) ) {
				/* translators: %s: Error message. */
				$msg .= ' ' . sprintf( __( 'This resulted in an error message: %s' ), '<code>' . $errstr . '</code>' );
			}
			$msg .= '</p>';

			$msg .= '<p>' . sprintf(
				/* translators: %s: Asterisk symbol (*). */
				__( 'To use a subdomain configuration, you must have a wildcard entry in your DNS. This usually means adding a %s hostname record pointing at your web server in your DNS configuration tool.' ),
				'<code>*</code>'
			) . '</p>';

			$msg .= '<p>' . __( 'You can still use your site but any subdomain you create may not be accessible. If you know your DNS is correct, ignore this message.' ) . '</p>';

			return new WP_Error( 'no_wildcard_dns', $msg );
		}
	}

	return true;
}

/**
 * Creates WordPress network meta and sets the default values.
 *
 * @since 5.1.0
 *
 * @global wpdb $wpdb          WordPress database abstraction object.
 * @global int  $wp_db_version WordPress database version.
 *
 * @param int   $network_id Network ID to populate meta for.
 * @param array $meta       Optional. Custom meta $key => $value pairs to use. Default empty array.
 */
function populate_network_meta( $network_id, array $meta = array() ) {
	global $wpdb, $wp_db_version;

	$network_id = (int) $network_id;

	$email             = ! empty( $meta['admin_email'] ) ? $meta['admin_email'] : '';
	$subdomain_install = isset( $meta['subdomain_install'] ) ? (int) $meta['subdomain_install'] : 0;

	// If a user with the provided email does not exist, default to the current user as the new network admin.
	$site_user = ! empty( $email ) ? get_user_by( 'email', $email ) : false;
	if ( false === $site_user ) {
		$site_user = wp_get_current_user();
	}

	if ( empty( $email ) ) {
		$email = $site_user->user_email;
	}

	$template       = get_option( 'template' );
	$stylesheet     = get_option( 'stylesheet' );
	$allowed_themes = array( $stylesheet => true );

	if ( $template !== $stylesheet ) {
		$allowed_themes[ $template ] = true;
	}

	if ( WP_DEFAULT_THEME !== $stylesheet && WP_DEFAULT_THEME !== $template ) {
		$allowed_themes[ WP_DEFAULT_THEME ] = true;
	}

	// If WP_DEFAULT_THEME doesn't exist, also include the latest core default theme.
	if ( ! wp_get_theme( WP_DEFAULT_THEME )->exists() ) {
		$core_default = WP_Theme::get_core_default_theme();
		if ( $core_default ) {
			$allowed_themes[ $core_default->get_stylesheet() ] = true;
		}
	}

	if ( function_exists( 'clean_network_cache' ) ) {
		clean_network_cache( $network_id );
	} else {
		wp_cache_delete( $network_id, 'networks' );
	}

	if ( ! is_multisite() ) {
		$site_admins = array( $site_user->user_login );
		$users       = get_users(
			array(
				'fields' => array( 'user_login' ),
				'role'   => 'administrator',
			)
		);
		if ( $users ) {
			foreach ( $users as $user ) {
				$site_admins[] = $user->user_login;
			}

			$site_admins = array_unique( $site_admins );
		}
	} else {
		$site_admins = get_site_option( 'site_admins' );
	}

	/* translators: Do not translate USERNAME, SITE_NAME, BLOG_URL, PASSWORD: those are placeholders. */
	$welcome_email = __(
		'Howdy USERNAME,

Your new SITE_NAME site has been successfully set up at:
BLOG_URL

You can log in to the administrator account with the following information:

Username: USERNAME
Password: PASSWORD
Log in here: BLOG_URLwp-login.php

We hope you enjoy your new site. Thanks!

--The Team @ SITE_NAME'
	);

	$allowed_file_types = array();
	$all_mime_types     = get_allowed_mime_types();

	foreach ( $all_mime_types as $ext => $mime ) {
		array_push( $allowed_file_types, ...explode( '|', $ext ) );
	}
	$upload_filetypes = array_unique( $allowed_file_types );

	$sitemeta = array(
		'site_name'                   => __( 'My Network' ),
		'admin_email'                 => $email,
		'admin_user_id'               => $site_user->ID,
		'registration'                => 'none',
		'upload_filetypes'            => implode( ' ', $upload_filetypes ),
		'blog_upload_space'           => 100,
		'fileupload_maxk'             => 1500,
		'site_admins'                 => $site_admins,
		'allowedthemes'               => $allowed_themes,
		'illegal_names'               => array( 'www', 'web', 'root', 'admin', 'main', 'invite', 'administrator', 'files' ),
		'wpmu_upgrade_site'           => $wp_db_version,
		'welcome_email'               => $welcome_email,
		/* translators: %s: Site link. */
		'first_post'                  => __( 'Welcome to %s. This is your first post. Edit or delete it, then start writing!' ),
		// @todo - Network admins should have a method of editing the network siteurl (used for cookie hash).
		'siteurl'                     => get_option( 'siteurl' ) . '/',
		'add_new_users'               => '0',
		'upload_space_check_disabled' => is_multisite() ? get_site_option( 'upload_space_check_disabled' ) : '1',
		'subdomain_install'           => $subdomain_install,
		'ms_files_rewriting'          => is_multisite() ? get_site_option( 'ms_files_rewriting' ) : '0',
		'user_count'                  => get_site_option( 'user_count' ),
		'initial_db_version'          => get_option( 'initial_db_version' ),
		'active_sitewide_plugins'     => array(),
		'WPLANG'                      => get_locale(),
	);
	if ( ! $subdomain_install ) {
		$sitemeta['illegal_names'][] = 'blog';
	}

	$sitemeta = wp_parse_args( $meta, $sitemeta );

	/**
	 * Filters meta for a network on creation.
	 *
	 * @since 3.7.0
	 *
	 * @param array $sitemeta   Associative array of network meta keys and values to be inserted.
	 * @param int   $network_id ID of network to populate.
	 */
	$sitemeta = apply_filters( 'populate_network_meta', $sitemeta, $network_id );

	$insert = '';
	foreach ( $sitemeta as $meta_key => $meta_value ) {
		if ( is_array( $meta_value ) ) {
			$meta_value = serialize( $meta_value );
		}
		if ( ! empty( $insert ) ) {
			$insert .= ', ';
		}
		$insert .= $wpdb->prepare( '( %d, %s, %s)', $network_id, $meta_key, $meta_value );
	}
	$wpdb->query( "INSERT INTO $wpdb->sitemeta ( site_id, meta_key, meta_value ) VALUES " . $insert ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}

/**
 * Creates WordPress site meta and sets the default values.
 *
 * @since 5.1.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param int   $site_id Site ID to populate meta for.
 * @param array $meta    Optional. Custom meta $key => $value pairs to use. Default empty array.
 */
function populate_site_meta( $site_id, array $meta = array() ) {
	global $wpdb;

	$site_id = (int) $site_id;

	if ( ! is_site_meta_supported() ) {
		return;
	}

	if ( empty( $meta ) ) {
		return;
	}

	/**
	 * Filters meta for a site on creation.
	 *
	 * @since 5.2.0
	 *
	 * @param array $meta    Associative array of site meta keys and values to be inserted.
	 * @param int   $site_id ID of site to populate.
	 */
	$site_meta = apply_filters( 'populate_site_meta', $meta, $site_id );

	$insert = '';
	foreach ( $site_meta as $meta_key => $meta_value ) {
		if ( is_array( $meta_value ) ) {
			$meta_value = serialize( $meta_value );
		}
		if ( ! empty( $insert ) ) {
			$insert .= ', ';
		}
		$insert .= $wpdb->prepare( '( %d, %s, %s)', $site_id, $meta_key, $meta_value );
	}

	$wpdb->query( "INSERT INTO $wpdb->blogmeta ( blog_id, meta_key, meta_value ) VALUES " . $insert ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

	wp_cache_delete( $site_id, 'blog_meta' );
	wp_cache_set_sites_last_changed();
}
class-wp-posts-list-table.php000064400000175330151327343360012232 0ustar00<?php
/**
 * List Table API: WP_Posts_List_Table class
 *
 * @package WordPress
 * @subpackage Administration
 * @since 3.1.0
 */

/**
 * Core class used to implement displaying posts in a list table.
 *
 * @since 3.1.0
 *
 * @see WP_List_Table
 */
class WP_Posts_List_Table extends WP_List_Table {

	/**
	 * Whether the items should be displayed hierarchically or linearly.
	 *
	 * @since 3.1.0
	 * @var bool
	 */
	protected $hierarchical_display;

	/**
	 * Holds the number of pending comments for each post.
	 *
	 * @since 3.1.0
	 * @var array
	 */
	protected $comment_pending_count;

	/**
	 * Holds the number of posts for this user.
	 *
	 * @since 3.1.0
	 * @var int
	 */
	private $user_posts_count;

	/**
	 * Holds the number of posts which are sticky.
	 *
	 * @since 3.1.0
	 * @var int
	 */
	private $sticky_posts_count = 0;

	private $is_trash;

	/**
	 * Current level for output.
	 *
	 * @since 4.3.0
	 * @var int
	 */
	protected $current_level = 0;

	/**
	 * Constructor.
	 *
	 * @since 3.1.0
	 *
	 * @see WP_List_Table::__construct() for more information on default arguments.
	 *
	 * @global WP_Post_Type $post_type_object Global post type object.
	 * @global wpdb         $wpdb             WordPress database abstraction object.
	 *
	 * @param array $args An associative array of arguments.
	 */
	public function __construct( $args = array() ) {
		global $post_type_object, $wpdb;

		parent::__construct(
			array(
				'plural' => 'posts',
				'screen' => isset( $args['screen'] ) ? $args['screen'] : null,
			)
		);

		$post_type        = $this->screen->post_type;
		$post_type_object = get_post_type_object( $post_type );

		$exclude_states = get_post_stati(
			array(
				'show_in_admin_all_list' => false,
			)
		);

		$this->user_posts_count = (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT( 1 )
				FROM $wpdb->posts
				WHERE post_type = %s
				AND post_status NOT IN ( '" . implode( "','", $exclude_states ) . "' )
				AND post_author = %d",
				$post_type,
				get_current_user_id()
			)
		);

		if ( $this->user_posts_count
			&& ! current_user_can( $post_type_object->cap->edit_others_posts )
			&& empty( $_REQUEST['post_status'] ) && empty( $_REQUEST['all_posts'] )
			&& empty( $_REQUEST['author'] ) && empty( $_REQUEST['show_sticky'] )
		) {
			$_GET['author'] = get_current_user_id();
		}

		$sticky_posts = get_option( 'sticky_posts' );

		if ( 'post' === $post_type && $sticky_posts ) {
			$sticky_posts = implode( ', ', array_map( 'absint', (array) $sticky_posts ) );

			$this->sticky_posts_count = (int) $wpdb->get_var(
				$wpdb->prepare(
					"SELECT COUNT( 1 )
					FROM $wpdb->posts
					WHERE post_type = %s
					AND post_status NOT IN ('trash', 'auto-draft')
					AND ID IN ($sticky_posts)",
					$post_type
				)
			);
		}
	}

	/**
	 * Sets whether the table layout should be hierarchical or not.
	 *
	 * @since 4.2.0
	 *
	 * @param bool $display Whether the table layout should be hierarchical.
	 */
	public function set_hierarchical_display( $display ) {
		$this->hierarchical_display = $display;
	}

	/**
	 * @return bool
	 */
	public function ajax_user_can() {
		return current_user_can( get_post_type_object( $this->screen->post_type )->cap->edit_posts );
	}

	/**
	 * @global string   $mode             List table view mode.
	 * @global array    $avail_post_stati
	 * @global WP_Query $wp_query         WordPress Query object.
	 * @global int      $per_page
	 */
	public function prepare_items() {
		global $mode, $avail_post_stati, $wp_query, $per_page;

		if ( ! empty( $_REQUEST['mode'] ) ) {
			$mode = 'excerpt' === $_REQUEST['mode'] ? 'excerpt' : 'list';
			set_user_setting( 'posts_list_mode', $mode );
		} else {
			$mode = get_user_setting( 'posts_list_mode', 'list' );
		}

		// Is going to call wp().
		$avail_post_stati = wp_edit_posts_query();

		$this->set_hierarchical_display(
			is_post_type_hierarchical( $this->screen->post_type )
			&& 'menu_order title' === $wp_query->query['orderby']
		);

		$post_type = $this->screen->post_type;
		$per_page  = $this->get_items_per_page( 'edit_' . $post_type . '_per_page' );

		/** This filter is documented in wp-admin/includes/post.php */
		$per_page = apply_filters( 'edit_posts_per_page', $per_page, $post_type );

		if ( $this->hierarchical_display ) {
			$total_items = $wp_query->post_count;
		} elseif ( $wp_query->found_posts || $this->get_pagenum() === 1 ) {
			$total_items = $wp_query->found_posts;
		} else {
			$post_counts = (array) wp_count_posts( $post_type, 'readable' );

			if ( isset( $_REQUEST['post_status'] ) && in_array( $_REQUEST['post_status'], $avail_post_stati, true ) ) {
				$total_items = $post_counts[ $_REQUEST['post_status'] ];
			} elseif ( isset( $_REQUEST['show_sticky'] ) && $_REQUEST['show_sticky'] ) {
				$total_items = $this->sticky_posts_count;
			} elseif ( isset( $_GET['author'] ) && get_current_user_id() === (int) $_GET['author'] ) {
				$total_items = $this->user_posts_count;
			} else {
				$total_items = array_sum( $post_counts );

				// Subtract post types that are not included in the admin all list.
				foreach ( get_post_stati( array( 'show_in_admin_all_list' => false ) ) as $state ) {
					$total_items -= $post_counts[ $state ];
				}
			}
		}

		$this->is_trash = isset( $_REQUEST['post_status'] ) && 'trash' === $_REQUEST['post_status'];

		$this->set_pagination_args(
			array(
				'total_items' => $total_items,
				'per_page'    => $per_page,
			)
		);
	}

	/**
	 * @return bool
	 */
	public function has_items() {
		return have_posts();
	}

	/**
	 */
	public function no_items() {
		if ( isset( $_REQUEST['post_status'] ) && 'trash' === $_REQUEST['post_status'] ) {
			echo get_post_type_object( $this->screen->post_type )->labels->not_found_in_trash;
		} else {
			echo get_post_type_object( $this->screen->post_type )->labels->not_found;
		}
	}

	/**
	 * Determines if the current view is the "All" view.
	 *
	 * @since 4.2.0
	 *
	 * @return bool Whether the current view is the "All" view.
	 */
	protected function is_base_request() {
		$vars = $_GET;
		unset( $vars['paged'] );

		if ( empty( $vars ) ) {
			return true;
		} elseif ( 1 === count( $vars ) && ! empty( $vars['post_type'] ) ) {
			return $this->screen->post_type === $vars['post_type'];
		}

		return 1 === count( $vars ) && ! empty( $vars['mode'] );
	}

	/**
	 * Creates a link to edit.php with params.
	 *
	 * @since 4.4.0
	 *
	 * @param string[] $args      Associative array of URL parameters for the link.
	 * @param string   $link_text Link text.
	 * @param string   $css_class Optional. Class attribute. Default empty string.
	 * @return string The formatted link string.
	 */
	protected function get_edit_link( $args, $link_text, $css_class = '' ) {
		$url = add_query_arg( $args, 'edit.php' );

		$class_html   = '';
		$aria_current = '';

		if ( ! empty( $css_class ) ) {
			$class_html = sprintf(
				' class="%s"',
				esc_attr( $css_class )
			);

			if ( 'current' === $css_class ) {
				$aria_current = ' aria-current="page"';
			}
		}

		return sprintf(
			'<a href="%s"%s%s>%s</a>',
			esc_url( $url ),
			$class_html,
			$aria_current,
			$link_text
		);
	}

	/**
	 * @global array $locked_post_status This seems to be deprecated.
	 * @global array $avail_post_stati
	 * @return array
	 */
	protected function get_views() {
		global $locked_post_status, $avail_post_stati;

		$post_type = $this->screen->post_type;

		if ( ! empty( $locked_post_status ) ) {
			return array();
		}

		$status_links = array();
		$num_posts    = wp_count_posts( $post_type, 'readable' );
		$total_posts  = array_sum( (array) $num_posts );
		$class        = '';

		$current_user_id = get_current_user_id();
		$all_args        = array( 'post_type' => $post_type );
		$mine            = '';

		// Subtract post types that are not included in the admin all list.
		foreach ( get_post_stati( array( 'show_in_admin_all_list' => false ) ) as $state ) {
			$total_posts -= $num_posts->$state;
		}

		if ( $this->user_posts_count && $this->user_posts_count !== $total_posts ) {
			if ( isset( $_GET['author'] ) && ( $current_user_id === (int) $_GET['author'] ) ) {
				$class = 'current';
			}

			$mine_args = array(
				'post_type' => $post_type,
				'author'    => $current_user_id,
			);

			$mine_inner_html = sprintf(
				/* translators: %s: Number of posts. */
				_nx(
					'Mine <span class="count">(%s)</span>',
					'Mine <span class="count">(%s)</span>',
					$this->user_posts_count,
					'posts'
				),
				number_format_i18n( $this->user_posts_count )
			);

			$mine = array(
				'url'     => esc_url( add_query_arg( $mine_args, 'edit.php' ) ),
				'label'   => $mine_inner_html,
				'current' => isset( $_GET['author'] ) && ( $current_user_id === (int) $_GET['author'] ),
			);

			$all_args['all_posts'] = 1;
			$class                 = '';
		}

		$all_inner_html = sprintf(
			/* translators: %s: Number of posts. */
			_nx(
				'All <span class="count">(%s)</span>',
				'All <span class="count">(%s)</span>',
				$total_posts,
				'posts'
			),
			number_format_i18n( $total_posts )
		);

		$status_links['all'] = array(
			'url'     => esc_url( add_query_arg( $all_args, 'edit.php' ) ),
			'label'   => $all_inner_html,
			'current' => empty( $class ) && ( $this->is_base_request() || isset( $_REQUEST['all_posts'] ) ),
		);

		if ( $mine ) {
			$status_links['mine'] = $mine;
		}

		foreach ( get_post_stati( array( 'show_in_admin_status_list' => true ), 'objects' ) as $status ) {
			$class = '';

			$status_name = $status->name;

			if ( ! in_array( $status_name, $avail_post_stati, true ) || empty( $num_posts->$status_name ) ) {
				continue;
			}

			if ( isset( $_REQUEST['post_status'] ) && $status_name === $_REQUEST['post_status'] ) {
				$class = 'current';
			}

			$status_args = array(
				'post_status' => $status_name,
				'post_type'   => $post_type,
			);

			$status_label = sprintf(
				translate_nooped_plural( $status->label_count, $num_posts->$status_name ),
				number_format_i18n( $num_posts->$status_name )
			);

			$status_links[ $status_name ] = array(
				'url'     => esc_url( add_query_arg( $status_args, 'edit.php' ) ),
				'label'   => $status_label,
				'current' => isset( $_REQUEST['post_status'] ) && $status_name === $_REQUEST['post_status'],
			);
		}

		if ( ! empty( $this->sticky_posts_count ) ) {
			$class = ! empty( $_REQUEST['show_sticky'] ) ? 'current' : '';

			$sticky_args = array(
				'post_type'   => $post_type,
				'show_sticky' => 1,
			);

			$sticky_inner_html = sprintf(
				/* translators: %s: Number of posts. */
				_nx(
					'Sticky <span class="count">(%s)</span>',
					'Sticky <span class="count">(%s)</span>',
					$this->sticky_posts_count,
					'posts'
				),
				number_format_i18n( $this->sticky_posts_count )
			);

			$sticky_link = array(
				'sticky' => array(
					'url'     => esc_url( add_query_arg( $sticky_args, 'edit.php' ) ),
					'label'   => $sticky_inner_html,
					'current' => ! empty( $_REQUEST['show_sticky'] ),
				),
			);

			// Sticky comes after Publish, or if not listed, after All.
			$split        = 1 + array_search( ( isset( $status_links['publish'] ) ? 'publish' : 'all' ), array_keys( $status_links ), true );
			$status_links = array_merge( array_slice( $status_links, 0, $split ), $sticky_link, array_slice( $status_links, $split ) );
		}

		return $this->get_views_links( $status_links );
	}

	/**
	 * @return array
	 */
	protected function get_bulk_actions() {
		$actions       = array();
		$post_type_obj = get_post_type_object( $this->screen->post_type );

		if ( current_user_can( $post_type_obj->cap->edit_posts ) ) {
			if ( $this->is_trash ) {
				$actions['untrash'] = __( 'Restore' );
			} else {
				$actions['edit'] = __( 'Edit' );
			}
		}

		if ( current_user_can( $post_type_obj->cap->delete_posts ) ) {
			if ( $this->is_trash || ! EMPTY_TRASH_DAYS ) {
				$actions['delete'] = __( 'Delete permanently' );
			} else {
				$actions['trash'] = __( 'Move to Trash' );
			}
		}

		return $actions;
	}

	/**
	 * Displays a categories drop-down for filtering on the Posts list table.
	 *
	 * @since 4.6.0
	 *
	 * @global int $cat Currently selected category.
	 *
	 * @param string $post_type Post type slug.
	 */
	protected function categories_dropdown( $post_type ) {
		global $cat;

		/**
		 * Filters whether to remove the 'Categories' drop-down from the post list table.
		 *
		 * @since 4.6.0
		 *
		 * @param bool   $disable   Whether to disable the categories drop-down. Default false.
		 * @param string $post_type Post type slug.
		 */
		if ( false !== apply_filters( 'disable_categories_dropdown', false, $post_type ) ) {
			return;
		}

		if ( is_object_in_taxonomy( $post_type, 'category' ) ) {
			$dropdown_options = array(
				'show_option_all' => get_taxonomy( 'category' )->labels->all_items,
				'hide_empty'      => 0,
				'hierarchical'    => 1,
				'show_count'      => 0,
				'orderby'         => 'name',
				'selected'        => $cat,
			);

			echo '<label class="screen-reader-text" for="cat">' . get_taxonomy( 'category' )->labels->filter_by_item . '</label>';

			wp_dropdown_categories( $dropdown_options );
		}
	}

	/**
	 * Displays a formats drop-down for filtering items.
	 *
	 * @since 5.2.0
	 * @access protected
	 *
	 * @param string $post_type Post type slug.
	 */
	protected function formats_dropdown( $post_type ) {
		/**
		 * Filters whether to remove the 'Formats' drop-down from the post list table.
		 *
		 * @since 5.2.0
		 * @since 5.5.0 The `$post_type` parameter was added.
		 *
		 * @param bool   $disable   Whether to disable the drop-down. Default false.
		 * @param string $post_type Post type slug.
		 */
		if ( apply_filters( 'disable_formats_dropdown', false, $post_type ) ) {
			return;
		}

		// Return if the post type doesn't have post formats or if we're in the Trash.
		if ( ! is_object_in_taxonomy( $post_type, 'post_format' ) || $this->is_trash ) {
			return;
		}

		// Make sure the dropdown shows only formats with a post count greater than 0.
		$used_post_formats = get_terms(
			array(
				'taxonomy'   => 'post_format',
				'hide_empty' => true,
			)
		);

		// Return if there are no posts using formats.
		if ( ! $used_post_formats ) {
			return;
		}

		$displayed_post_format = isset( $_GET['post_format'] ) ? $_GET['post_format'] : '';
		?>
		<label for="filter-by-format" class="screen-reader-text">
			<?php
			/* translators: Hidden accessibility text. */
			_e( 'Filter by post format' );
			?>
		</label>
		<select name="post_format" id="filter-by-format">
			<option<?php selected( $displayed_post_format, '' ); ?> value=""><?php _e( 'All formats' ); ?></option>
			<?php
			foreach ( $used_post_formats as $used_post_format ) {
				// Post format slug.
				$slug = str_replace( 'post-format-', '', $used_post_format->slug );
				// Pretty, translated version of the post format slug.
				$pretty_name = get_post_format_string( $slug );

				// Skip the standard post format.
				if ( 'standard' === $slug ) {
					continue;
				}
				?>
				<option<?php selected( $displayed_post_format, $slug ); ?> value="<?php echo esc_attr( $slug ); ?>"><?php echo esc_html( $pretty_name ); ?></option>
				<?php
			}
			?>
		</select>
		<?php
	}

	/**
	 * @param string $which
	 */
	protected function extra_tablenav( $which ) {
		?>
		<div class="alignleft actions">
		<?php
		if ( 'top' === $which ) {
			ob_start();

			$this->months_dropdown( $this->screen->post_type );
			$this->categories_dropdown( $this->screen->post_type );
			$this->formats_dropdown( $this->screen->post_type );

			/**
			 * Fires before the Filter button on the Posts and Pages list tables.
			 *
			 * The Filter button allows sorting by date and/or category on the
			 * Posts list table, and sorting by date on the Pages list table.
			 *
			 * @since 2.1.0
			 * @since 4.4.0 The `$post_type` parameter was added.
			 * @since 4.6.0 The `$which` parameter was added.
			 *
			 * @param string $post_type The post type slug.
			 * @param string $which     The location of the extra table nav markup:
			 *                          'top' or 'bottom' for WP_Posts_List_Table,
			 *                          'bar' for WP_Media_List_Table.
			 */
			do_action( 'restrict_manage_posts', $this->screen->post_type, $which );

			$output = ob_get_clean();

			if ( ! empty( $output ) ) {
				echo $output;
				submit_button( __( 'Filter' ), '', 'filter_action', false, array( 'id' => 'post-query-submit' ) );
			}
		}

		if ( $this->is_trash && $this->has_items()
			&& current_user_can( get_post_type_object( $this->screen->post_type )->cap->edit_others_posts )
		) {
			submit_button( __( 'Empty Trash' ), 'apply', 'delete_all', false );
		}
		?>
		</div>
		<?php
		/**
		 * Fires immediately following the closing "actions" div in the tablenav for the posts
		 * list table.
		 *
		 * @since 4.4.0
		 *
		 * @param string $which The location of the extra table nav markup: 'top' or 'bottom'.
		 */
		do_action( 'manage_posts_extra_tablenav', $which );
	}

	/**
	 * @return string
	 */
	public function current_action() {
		if ( isset( $_REQUEST['delete_all'] ) || isset( $_REQUEST['delete_all2'] ) ) {
			return 'delete_all';
		}

		return parent::current_action();
	}

	/**
	 * @global string $mode List table view mode.
	 *
	 * @return array
	 */
	protected function get_table_classes() {
		global $mode;

		$mode_class = esc_attr( 'table-view-' . $mode );

		return array(
			'widefat',
			'fixed',
			'striped',
			$mode_class,
			is_post_type_hierarchical( $this->screen->post_type ) ? 'pages' : 'posts',
		);
	}

	/**
	 * @return string[] Array of column titles keyed by their column name.
	 */
	public function get_columns() {
		$post_type = $this->screen->post_type;

		$posts_columns = array();

		$posts_columns['cb'] = '<input type="checkbox" />';

		/* translators: Posts screen column name. */
		$posts_columns['title'] = _x( 'Title', 'column name' );

		if ( post_type_supports( $post_type, 'author' ) ) {
			$posts_columns['author'] = __( 'Author' );
		}

		$taxonomies = get_object_taxonomies( $post_type, 'objects' );
		$taxonomies = wp_filter_object_list( $taxonomies, array( 'show_admin_column' => true ), 'and', 'name' );

		/**
		 * Filters the taxonomy columns in the Posts list table.
		 *
		 * The dynamic portion of the hook name, `$post_type`, refers to the post
		 * type slug.
		 *
		 * Possible hook names include:
		 *
		 *  - `manage_taxonomies_for_post_columns`
		 *  - `manage_taxonomies_for_page_columns`
		 *
		 * @since 3.5.0
		 *
		 * @param string[] $taxonomies Array of taxonomy names to show columns for.
		 * @param string   $post_type  The post type.
		 */
		$taxonomies = apply_filters( "manage_taxonomies_for_{$post_type}_columns", $taxonomies, $post_type );
		$taxonomies = array_filter( $taxonomies, 'taxonomy_exists' );

		foreach ( $taxonomies as $taxonomy ) {
			if ( 'category' === $taxonomy ) {
				$column_key = 'categories';
			} elseif ( 'post_tag' === $taxonomy ) {
				$column_key = 'tags';
			} else {
				$column_key = 'taxonomy-' . $taxonomy;
			}

			$posts_columns[ $column_key ] = get_taxonomy( $taxonomy )->labels->name;
		}

		$post_status = ! empty( $_REQUEST['post_status'] ) ? $_REQUEST['post_status'] : 'all';

		if ( post_type_supports( $post_type, 'comments' )
			&& ! in_array( $post_status, array( 'pending', 'draft', 'future' ), true )
		) {
			$posts_columns['comments'] = sprintf(
				'<span class="vers comment-grey-bubble" title="%1$s" aria-hidden="true"></span><span class="screen-reader-text">%2$s</span>',
				esc_attr__( 'Comments' ),
				/* translators: Hidden accessibility text. */
				__( 'Comments' )
			);
		}

		$posts_columns['date'] = __( 'Date' );

		if ( 'page' === $post_type ) {

			/**
			 * Filters the columns displayed in the Pages list table.
			 *
			 * @since 2.5.0
			 *
			 * @param string[] $posts_columns An associative array of column headings.
			 */
			$posts_columns = apply_filters( 'manage_pages_columns', $posts_columns );
		} else {

			/**
			 * Filters the columns displayed in the Posts list table.
			 *
			 * @since 1.5.0
			 *
			 * @param string[] $posts_columns An associative array of column headings.
			 * @param string   $post_type     The post type slug.
			 */
			$posts_columns = apply_filters( 'manage_posts_columns', $posts_columns, $post_type );
		}

		/**
		 * Filters the columns displayed in the Posts list table for a specific post type.
		 *
		 * The dynamic portion of the hook name, `$post_type`, refers to the post type slug.
		 *
		 * Possible hook names include:
		 *
		 *  - `manage_post_posts_columns`
		 *  - `manage_page_posts_columns`
		 *
		 * @since 3.0.0
		 *
		 * @param string[] $posts_columns An associative array of column headings.
		 */
		return apply_filters( "manage_{$post_type}_posts_columns", $posts_columns );
	}

	/**
	 * @return array
	 */
	protected function get_sortable_columns() {

		$post_type = $this->screen->post_type;

		if ( 'page' === $post_type ) {
			if ( isset( $_GET['orderby'] ) ) {
				$title_orderby_text = __( 'Table ordered by Title.' );
			} else {
				$title_orderby_text = __( 'Table ordered by Hierarchical Menu Order and Title.' );
			}

			$sortables = array(
				'title'    => array( 'title', false, __( 'Title' ), $title_orderby_text, 'asc' ),
				'parent'   => array( 'parent', false ),
				'comments' => array( 'comment_count', false, __( 'Comments' ), __( 'Table ordered by Comments.' ) ),
				'date'     => array( 'date', true, __( 'Date' ), __( 'Table ordered by Date.' ) ),
			);
		} else {
			$sortables = array(
				'title'    => array( 'title', false, __( 'Title' ), __( 'Table ordered by Title.' ) ),
				'parent'   => array( 'parent', false ),
				'comments' => array( 'comment_count', false, __( 'Comments' ), __( 'Table ordered by Comments.' ) ),
				'date'     => array( 'date', true, __( 'Date' ), __( 'Table ordered by Date.' ), 'desc' ),
			);
		}
		// Custom Post Types: there's a filter for that, see get_column_info().

		return $sortables;
	}

	/**
	 * Generates the list table rows.
	 *
	 * @since 3.1.0
	 *
	 * @global WP_Query $wp_query WordPress Query object.
	 * @global int      $per_page
	 *
	 * @param array $posts
	 * @param int   $level
	 */
	public function display_rows( $posts = array(), $level = 0 ) {
		global $wp_query, $per_page;

		if ( empty( $posts ) ) {
			$posts = $wp_query->posts;
		}

		add_filter( 'the_title', 'esc_html' );

		if ( $this->hierarchical_display ) {
			$this->_display_rows_hierarchical( $posts, $this->get_pagenum(), $per_page );
		} else {
			$this->_display_rows( $posts, $level );
		}
	}

	/**
	 * @param array $posts
	 * @param int   $level
	 */
	private function _display_rows( $posts, $level = 0 ) {
		$post_type = $this->screen->post_type;

		// Create array of post IDs.
		$post_ids = array();

		foreach ( $posts as $a_post ) {
			$post_ids[] = $a_post->ID;
		}

		if ( post_type_supports( $post_type, 'comments' ) ) {
			$this->comment_pending_count = get_pending_comments_num( $post_ids );
		}
		update_post_author_caches( $posts );

		foreach ( $posts as $post ) {
			$this->single_row( $post, $level );
		}
	}

	/**
	 * @global wpdb    $wpdb WordPress database abstraction object.
	 * @global WP_Post $post Global post object.
	 * @param array $pages
	 * @param int   $pagenum
	 * @param int   $per_page
	 */
	private function _display_rows_hierarchical( $pages, $pagenum = 1, $per_page = 20 ) {
		global $wpdb;

		$level = 0;

		if ( ! $pages ) {
			$pages = get_pages( array( 'sort_column' => 'menu_order' ) );

			if ( ! $pages ) {
				return;
			}
		}

		/*
		 * Arrange pages into two parts: top level pages and children_pages.
		 * children_pages is two dimensional array. Example:
		 * children_pages[10][] contains all sub-pages whose parent is 10.
		 * It only takes O( N ) to arrange this and it takes O( 1 ) for subsequent lookup operations
		 * If searching, ignore hierarchy and treat everything as top level
		 */
		if ( empty( $_REQUEST['s'] ) ) {
			$top_level_pages = array();
			$children_pages  = array();

			foreach ( $pages as $page ) {
				// Catch and repair bad pages.
				if ( $page->post_parent === $page->ID ) {
					$page->post_parent = 0;
					$wpdb->update( $wpdb->posts, array( 'post_parent' => 0 ), array( 'ID' => $page->ID ) );
					clean_post_cache( $page );
				}

				if ( $page->post_parent > 0 ) {
					$children_pages[ $page->post_parent ][] = $page;
				} else {
					$top_level_pages[] = $page;
				}
			}

			$pages = &$top_level_pages;
		}

		$count      = 0;
		$start      = ( $pagenum - 1 ) * $per_page;
		$end        = $start + $per_page;
		$to_display = array();

		foreach ( $pages as $page ) {
			if ( $count >= $end ) {
				break;
			}

			if ( $count >= $start ) {
				$to_display[ $page->ID ] = $level;
			}

			++$count;

			if ( isset( $children_pages ) ) {
				$this->_page_rows( $children_pages, $count, $page->ID, $level + 1, $pagenum, $per_page, $to_display );
			}
		}

		// If it is the last pagenum and there are orphaned pages, display them with paging as well.
		if ( isset( $children_pages ) && $count < $end ) {
			foreach ( $children_pages as $orphans ) {
				foreach ( $orphans as $op ) {
					if ( $count >= $end ) {
						break;
					}

					if ( $count >= $start ) {
						$to_display[ $op->ID ] = 0;
					}

					++$count;
				}
			}
		}

		$ids = array_keys( $to_display );
		_prime_post_caches( $ids );
		$_posts = array_map( 'get_post', $ids );
		update_post_author_caches( $_posts );

		if ( ! isset( $GLOBALS['post'] ) ) {
			$GLOBALS['post'] = reset( $ids );
		}

		foreach ( $to_display as $page_id => $level ) {
			echo "\t";
			$this->single_row( $page_id, $level );
		}
	}

	/**
	 * Displays the nested hierarchy of sub-pages together with paging
	 * support, based on a top level page ID.
	 *
	 * @since 3.1.0 (Standalone function exists since 2.6.0)
	 * @since 4.2.0 Added the `$to_display` parameter.
	 *
	 * @param array $children_pages
	 * @param int   $count
	 * @param int   $parent_page
	 * @param int   $level
	 * @param int   $pagenum
	 * @param int   $per_page
	 * @param array $to_display List of pages to be displayed. Passed by reference.
	 */
	private function _page_rows( &$children_pages, &$count, $parent_page, $level, $pagenum, $per_page, &$to_display ) {
		if ( ! isset( $children_pages[ $parent_page ] ) ) {
			return;
		}

		$start = ( $pagenum - 1 ) * $per_page;
		$end   = $start + $per_page;

		foreach ( $children_pages[ $parent_page ] as $page ) {
			if ( $count >= $end ) {
				break;
			}

			// If the page starts in a subtree, print the parents.
			if ( $count === $start && $page->post_parent > 0 ) {
				$my_parents = array();
				$my_parent  = $page->post_parent;

				while ( $my_parent ) {
					// Get the ID from the list or the attribute if my_parent is an object.
					$parent_id = $my_parent;

					if ( is_object( $my_parent ) ) {
						$parent_id = $my_parent->ID;
					}

					$my_parent    = get_post( $parent_id );
					$my_parents[] = $my_parent;

					if ( ! $my_parent->post_parent ) {
						break;
					}

					$my_parent = $my_parent->post_parent;
				}

				$num_parents = count( $my_parents );

				while ( $my_parent = array_pop( $my_parents ) ) {
					$to_display[ $my_parent->ID ] = $level - $num_parents;
					--$num_parents;
				}
			}

			if ( $count >= $start ) {
				$to_display[ $page->ID ] = $level;
			}

			++$count;

			$this->_page_rows( $children_pages, $count, $page->ID, $level + 1, $pagenum, $per_page, $to_display );
		}

		unset( $children_pages[ $parent_page ] ); // Required in order to keep track of orphans.
	}

	/**
	 * Handles the checkbox column output.
	 *
	 * @since 4.3.0
	 * @since 5.9.0 Renamed `$post` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param WP_Post $item The current WP_Post object.
	 */
	public function column_cb( $item ) {
		// Restores the more descriptive, specific name for use within this method.
		$post = $item;

		$show = current_user_can( 'edit_post', $post->ID );

		/**
		 * Filters whether to show the bulk edit checkbox for a post in its list table.
		 *
		 * By default the checkbox is only shown if the current user can edit the post.
		 *
		 * @since 5.7.0
		 *
		 * @param bool    $show Whether to show the checkbox.
		 * @param WP_Post $post The current WP_Post object.
		 */
		if ( apply_filters( 'wp_list_table_show_post_checkbox', $show, $post ) ) :
			?>
			<input id="cb-select-<?php the_ID(); ?>" type="checkbox" name="post[]" value="<?php the_ID(); ?>" />
			<label for="cb-select-<?php the_ID(); ?>">
				<span class="screen-reader-text">
				<?php
					/* translators: %s: Post title. */
					printf( __( 'Select %s' ), _draft_or_post_title() );
				?>
				</span>
			</label>
			<div class="locked-indicator">
				<span class="locked-indicator-icon" aria-hidden="true"></span>
				<span class="screen-reader-text">
				<?php
				printf(
					/* translators: Hidden accessibility text. %s: Post title. */
					__( '&#8220;%s&#8221; is locked' ),
					_draft_or_post_title()
				);
				?>
				</span>
			</div>
			<?php
		endif;
	}

	/**
	 * @since 4.3.0
	 *
	 * @param WP_Post $post
	 * @param string  $classes
	 * @param string  $data
	 * @param string  $primary
	 */
	protected function _column_title( $post, $classes, $data, $primary ) {
		echo '<td class="' . $classes . ' page-title" ', $data, '>';
		echo $this->column_title( $post );
		echo $this->handle_row_actions( $post, 'title', $primary );
		echo '</td>';
	}

	/**
	 * Handles the title column output.
	 *
	 * @since 4.3.0
	 *
	 * @global string $mode List table view mode.
	 *
	 * @param WP_Post $post The current WP_Post object.
	 */
	public function column_title( $post ) {
		global $mode;

		if ( $this->hierarchical_display ) {
			if ( 0 === $this->current_level && (int) $post->post_parent > 0 ) {
				// Sent level 0 by accident, by default, or because we don't know the actual level.
				$find_main_page = (int) $post->post_parent;

				while ( $find_main_page > 0 ) {
					$parent = get_post( $find_main_page );

					if ( is_null( $parent ) ) {
						break;
					}

					++$this->current_level;
					$find_main_page = (int) $parent->post_parent;

					if ( ! isset( $parent_name ) ) {
						/** This filter is documented in wp-includes/post-template.php */
						$parent_name = apply_filters( 'the_title', $parent->post_title, $parent->ID );
					}
				}
			}
		}

		$can_edit_post = current_user_can( 'edit_post', $post->ID );

		if ( $can_edit_post && 'trash' !== $post->post_status ) {
			$lock_holder = wp_check_post_lock( $post->ID );

			if ( $lock_holder ) {
				$lock_holder   = get_userdata( $lock_holder );
				$locked_avatar = get_avatar( $lock_holder->ID, 18 );
				/* translators: %s: User's display name. */
				$locked_text = esc_html( sprintf( __( '%s is currently editing' ), $lock_holder->display_name ) );
			} else {
				$locked_avatar = '';
				$locked_text   = '';
			}

			echo '<div class="locked-info"><span class="locked-avatar">' . $locked_avatar . '</span> <span class="locked-text">' . $locked_text . "</span></div>\n";
		}

		$pad = str_repeat( '&#8212; ', $this->current_level );
		echo '<strong>';

		$title = _draft_or_post_title();

		if ( $can_edit_post && 'trash' !== $post->post_status ) {
			printf(
				'<a class="row-title" href="%s" aria-label="%s">%s%s</a>',
				get_edit_post_link( $post->ID ),
				/* translators: %s: Post title. */
				esc_attr( sprintf( __( '&#8220;%s&#8221; (Edit)' ), $title ) ),
				$pad,
				$title
			);
		} else {
			printf(
				'<span>%s%s</span>',
				$pad,
				$title
			);
		}
		_post_states( $post );

		if ( isset( $parent_name ) ) {
			$post_type_object = get_post_type_object( $post->post_type );
			echo ' | ' . $post_type_object->labels->parent_item_colon . ' ' . esc_html( $parent_name );
		}

		echo "</strong>\n";

		if ( 'excerpt' === $mode
			&& ! is_post_type_hierarchical( $this->screen->post_type )
			&& current_user_can( 'read_post', $post->ID )
		) {
			if ( post_password_required( $post ) ) {
				echo '<span class="protected-post-excerpt">' . esc_html( get_the_excerpt() ) . '</span>';
			} else {
				echo esc_html( get_the_excerpt() );
			}
		}

		/** This filter is documented in wp-admin/includes/class-wp-posts-list-table.php */
		$quick_edit_enabled = apply_filters( 'quick_edit_enabled_for_post_type', true, $post->post_type );

		if ( $quick_edit_enabled ) {
			get_inline_data( $post );
		}
	}

	/**
	 * Handles the post date column output.
	 *
	 * @since 4.3.0
	 *
	 * @global string $mode List table view mode.
	 *
	 * @param WP_Post $post The current WP_Post object.
	 */
	public function column_date( $post ) {
		global $mode;

		if ( '0000-00-00 00:00:00' === $post->post_date ) {
			$t_time    = __( 'Unpublished' );
			$time_diff = 0;
		} else {
			$t_time = sprintf(
				/* translators: 1: Post date, 2: Post time. */
				__( '%1$s at %2$s' ),
				/* translators: Post date format. See https://www.php.net/manual/datetime.format.php */
				get_the_time( __( 'Y/m/d' ), $post ),
				/* translators: Post time format. See https://www.php.net/manual/datetime.format.php */
				get_the_time( __( 'g:i a' ), $post )
			);

			$time      = get_post_timestamp( $post );
			$time_diff = time() - $time;
		}

		if ( 'publish' === $post->post_status ) {
			$status = __( 'Published' );
		} elseif ( 'future' === $post->post_status ) {
			if ( $time_diff > 0 ) {
				$status = '<strong class="error-message">' . __( 'Missed schedule' ) . '</strong>';
			} else {
				$status = __( 'Scheduled' );
			}
		} else {
			$status = __( 'Last Modified' );
		}

		/**
		 * Filters the status text of the post.
		 *
		 * @since 4.8.0
		 *
		 * @param string  $status      The status text.
		 * @param WP_Post $post        Post object.
		 * @param string  $column_name The column name.
		 * @param string  $mode        The list display mode ('excerpt' or 'list').
		 */
		$status = apply_filters( 'post_date_column_status', $status, $post, 'date', $mode );

		if ( $status ) {
			echo $status . '<br />';
		}

		/**
		 * Filters the published, scheduled, or unpublished time of the post.
		 *
		 * @since 2.5.1
		 * @since 5.5.0 Removed the difference between 'excerpt' and 'list' modes.
		 *              The published time and date are both displayed now,
		 *              which is equivalent to the previous 'excerpt' mode.
		 *
		 * @param string  $t_time      The published time.
		 * @param WP_Post $post        Post object.
		 * @param string  $column_name The column name.
		 * @param string  $mode        The list display mode ('excerpt' or 'list').
		 */
		echo apply_filters( 'post_date_column_time', $t_time, $post, 'date', $mode );
	}

	/**
	 * Handles the comments column output.
	 *
	 * @since 4.3.0
	 *
	 * @param WP_Post $post The current WP_Post object.
	 */
	public function column_comments( $post ) {
		?>
		<div class="post-com-count-wrapper">
		<?php
			$pending_comments = isset( $this->comment_pending_count[ $post->ID ] ) ? $this->comment_pending_count[ $post->ID ] : 0;

			$this->comments_bubble( $post->ID, $pending_comments );
		?>
		</div>
		<?php
	}

	/**
	 * Handles the post author column output.
	 *
	 * @since 4.3.0
	 * @since 6.8.0 Added fallback text when author's name is unknown.
	 *
	 * @param WP_Post $post The current WP_Post object.
	 */
	public function column_author( $post ) {
		$author = get_the_author();

		if ( ! empty( $author ) ) {
			$args = array(
				'post_type' => $post->post_type,
				'author'    => get_the_author_meta( 'ID' ),
			);
			echo $this->get_edit_link( $args, esc_html( $author ) );
		} else {
			echo '<span aria-hidden="true">&#8212;</span><span class="screen-reader-text">' . __( '(no author)' ) . '</span>';
		}
	}

	/**
	 * Handles the default column output.
	 *
	 * @since 4.3.0
	 * @since 5.9.0 Renamed `$post` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param WP_Post $item        The current WP_Post object.
	 * @param string  $column_name The current column name.
	 */
	public function column_default( $item, $column_name ) {
		// Restores the more descriptive, specific name for use within this method.
		$post = $item;

		if ( 'categories' === $column_name ) {
			$taxonomy = 'category';
		} elseif ( 'tags' === $column_name ) {
			$taxonomy = 'post_tag';
		} elseif ( str_starts_with( $column_name, 'taxonomy-' ) ) {
			$taxonomy = substr( $column_name, 9 );
		} else {
			$taxonomy = false;
		}

		if ( $taxonomy ) {
			$taxonomy_object = get_taxonomy( $taxonomy );
			$terms           = get_the_terms( $post->ID, $taxonomy );

			if ( is_array( $terms ) ) {
				$term_links = array();

				foreach ( $terms as $t ) {
					$posts_in_term_qv = array();

					if ( 'post' !== $post->post_type ) {
						$posts_in_term_qv['post_type'] = $post->post_type;
					}

					if ( $taxonomy_object->query_var ) {
						$posts_in_term_qv[ $taxonomy_object->query_var ] = $t->slug;
					} else {
						$posts_in_term_qv['taxonomy'] = $taxonomy;
						$posts_in_term_qv['term']     = $t->slug;
					}

					$label = esc_html( sanitize_term_field( 'name', $t->name, $t->term_id, $taxonomy, 'display' ) );

					$term_links[] = $this->get_edit_link( $posts_in_term_qv, $label );
				}

				/**
				 * Filters the links in `$taxonomy` column of edit.php.
				 *
				 * @since 5.2.0
				 *
				 * @param string[]  $term_links Array of term editing links.
				 * @param string    $taxonomy   Taxonomy name.
				 * @param WP_Term[] $terms      Array of term objects appearing in the post row.
				 */
				$term_links = apply_filters( 'post_column_taxonomy_links', $term_links, $taxonomy, $terms );

				echo implode( wp_get_list_item_separator(), $term_links );
			} else {
				echo '<span aria-hidden="true">&#8212;</span><span class="screen-reader-text">' . $taxonomy_object->labels->no_terms . '</span>';
			}
			return;
		}

		if ( is_post_type_hierarchical( $post->post_type ) ) {

			/**
			 * Fires in each custom column on the Posts list table.
			 *
			 * This hook only fires if the current post type is hierarchical,
			 * such as pages.
			 *
			 * @since 2.5.0
			 *
			 * @param string $column_name The name of the column to display.
			 * @param int    $post_id     The current post ID.
			 */
			do_action( 'manage_pages_custom_column', $column_name, $post->ID );
		} else {

			/**
			 * Fires in each custom column in the Posts list table.
			 *
			 * This hook only fires if the current post type is non-hierarchical,
			 * such as posts.
			 *
			 * @since 1.5.0
			 *
			 * @param string $column_name The name of the column to display.
			 * @param int    $post_id     The current post ID.
			 */
			do_action( 'manage_posts_custom_column', $column_name, $post->ID );
		}

		/**
		 * Fires for each custom column of a specific post type in the Posts list table.
		 *
		 * The dynamic portion of the hook name, `$post->post_type`, refers to the post type.
		 *
		 * Possible hook names include:
		 *
		 *  - `manage_post_posts_custom_column`
		 *  - `manage_page_posts_custom_column`
		 *
		 * @since 3.1.0
		 *
		 * @param string $column_name The name of the column to display.
		 * @param int    $post_id     The current post ID.
		 */
		do_action( "manage_{$post->post_type}_posts_custom_column", $column_name, $post->ID );
	}

	/**
	 * @global WP_Post $post Global post object.
	 *
	 * @param int|WP_Post $post
	 * @param int         $level
	 */
	public function single_row( $post, $level = 0 ) {
		$global_post = get_post();

		$post                = get_post( $post );
		$this->current_level = $level;

		$GLOBALS['post'] = $post;
		setup_postdata( $post );

		$classes = 'iedit author-' . ( get_current_user_id() === (int) $post->post_author ? 'self' : 'other' );

		$lock_holder = wp_check_post_lock( $post->ID );

		if ( $lock_holder ) {
			$classes .= ' wp-locked';
		}

		if ( $post->post_parent ) {
			$count    = count( get_post_ancestors( $post->ID ) );
			$classes .= ' level-' . $count;
		} else {
			$classes .= ' level-0';
		}
		?>
		<tr id="post-<?php echo $post->ID; ?>" class="<?php echo implode( ' ', get_post_class( $classes, $post->ID ) ); ?>">
			<?php $this->single_row_columns( $post ); ?>
		</tr>
		<?php
		$GLOBALS['post'] = $global_post;
	}

	/**
	 * Gets the name of the default primary column.
	 *
	 * @since 4.3.0
	 *
	 * @return string Name of the default primary column, in this case, 'title'.
	 */
	protected function get_default_primary_column_name() {
		return 'title';
	}

	/**
	 * Generates and displays row action links.
	 *
	 * @since 4.3.0
	 * @since 5.9.0 Renamed `$post` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param WP_Post $item        Post being acted upon.
	 * @param string  $column_name Current column name.
	 * @param string  $primary     Primary column name.
	 * @return string Row actions output for posts, or an empty string
	 *                if the current column is not the primary column.
	 */
	protected function handle_row_actions( $item, $column_name, $primary ) {
		if ( $primary !== $column_name ) {
			return '';
		}

		// Restores the more descriptive, specific name for use within this method.
		$post = $item;

		$post_type_object = get_post_type_object( $post->post_type );
		$can_edit_post    = current_user_can( 'edit_post', $post->ID );
		$actions          = array();
		$title            = _draft_or_post_title();

		if ( $can_edit_post && 'trash' !== $post->post_status ) {
			$actions['edit'] = sprintf(
				'<a href="%s" aria-label="%s">%s</a>',
				get_edit_post_link( $post->ID ),
				/* translators: %s: Post title. */
				esc_attr( sprintf( __( 'Edit &#8220;%s&#8221;' ), $title ) ),
				__( 'Edit' )
			);

			/**
			 * Filters whether Quick Edit should be enabled for the given post type.
			 *
			 * @since 6.4.0
			 *
			 * @param bool   $enable    Whether to enable the Quick Edit functionality. Default true.
			 * @param string $post_type Post type name.
			 */
			$quick_edit_enabled = apply_filters( 'quick_edit_enabled_for_post_type', true, $post->post_type );

			if ( $quick_edit_enabled && 'wp_block' !== $post->post_type ) {
				$actions['inline hide-if-no-js'] = sprintf(
					'<button type="button" class="button-link editinline" aria-label="%s" aria-expanded="false">%s</button>',
					/* translators: %s: Post title. */
					esc_attr( sprintf( __( 'Quick edit &#8220;%s&#8221; inline' ), $title ) ),
					__( 'Quick&nbsp;Edit' )
				);
			}
		}

		if ( current_user_can( 'delete_post', $post->ID ) ) {
			if ( 'trash' === $post->post_status ) {
				$actions['untrash'] = sprintf(
					'<a href="%s" aria-label="%s">%s</a>',
					wp_nonce_url( admin_url( sprintf( $post_type_object->_edit_link . '&amp;action=untrash', $post->ID ) ), 'untrash-post_' . $post->ID ),
					/* translators: %s: Post title. */
					esc_attr( sprintf( __( 'Restore &#8220;%s&#8221; from the Trash' ), $title ) ),
					__( 'Restore' )
				);
			} elseif ( EMPTY_TRASH_DAYS ) {
				$actions['trash'] = sprintf(
					'<a href="%s" class="submitdelete" aria-label="%s">%s</a>',
					get_delete_post_link( $post->ID ),
					/* translators: %s: Post title. */
					esc_attr( sprintf( __( 'Move &#8220;%s&#8221; to the Trash' ), $title ) ),
					_x( 'Trash', 'verb' )
				);
			}

			if ( 'trash' === $post->post_status || ! EMPTY_TRASH_DAYS ) {
				$actions['delete'] = sprintf(
					'<a href="%s" class="submitdelete" aria-label="%s">%s</a>',
					get_delete_post_link( $post->ID, '', true ),
					/* translators: %s: Post title. */
					esc_attr( sprintf( __( 'Delete &#8220;%s&#8221; permanently' ), $title ) ),
					__( 'Delete Permanently' )
				);
			}
		}

		if ( is_post_type_viewable( $post_type_object ) ) {
			if ( in_array( $post->post_status, array( 'pending', 'draft', 'future' ), true ) ) {
				if ( $can_edit_post ) {
					$preview_link    = get_preview_post_link( $post );
					$actions['view'] = sprintf(
						'<a href="%s" rel="bookmark" aria-label="%s">%s</a>',
						esc_url( $preview_link ),
						/* translators: %s: Post title. */
						esc_attr( sprintf( __( 'Preview &#8220;%s&#8221;' ), $title ) ),
						__( 'Preview' )
					);
				}
			} elseif ( 'trash' !== $post->post_status ) {
				$actions['view'] = sprintf(
					'<a href="%s" rel="bookmark" aria-label="%s">%s</a>',
					get_permalink( $post->ID ),
					/* translators: %s: Post title. */
					esc_attr( sprintf( __( 'View &#8220;%s&#8221;' ), $title ) ),
					__( 'View' )
				);
			}
		}

		if ( 'wp_block' === $post->post_type ) {
			$actions['export'] = sprintf(
				'<button type="button" class="wp-list-reusable-blocks__export button-link" data-id="%s" aria-label="%s">%s</button>',
				$post->ID,
				/* translators: %s: Post title. */
				esc_attr( sprintf( __( 'Export &#8220;%s&#8221; as JSON' ), $title ) ),
				__( 'Export as JSON' )
			);
		}

		if ( is_post_type_hierarchical( $post->post_type ) ) {

			/**
			 * Filters the array of row action links on the Pages list table.
			 *
			 * The filter is evaluated only for hierarchical post types.
			 *
			 * @since 2.8.0
			 *
			 * @param string[] $actions An array of row action links. Defaults are
			 *                          'Edit', 'Quick Edit', 'Restore', 'Trash',
			 *                          'Delete Permanently', 'Preview', and 'View'.
			 * @param WP_Post  $post    The post object.
			 */
			$actions = apply_filters( 'page_row_actions', $actions, $post );
		} else {

			/**
			 * Filters the array of row action links on the Posts list table.
			 *
			 * The filter is evaluated only for non-hierarchical post types.
			 *
			 * @since 2.8.0
			 *
			 * @param string[] $actions An array of row action links. Defaults are
			 *                          'Edit', 'Quick Edit', 'Restore', 'Trash',
			 *                          'Delete Permanently', 'Preview', and 'View'.
			 * @param WP_Post  $post    The post object.
			 */
			$actions = apply_filters( 'post_row_actions', $actions, $post );
		}

		return $this->row_actions( $actions );
	}

	/**
	 * Outputs the hidden row displayed when inline editing
	 *
	 * @since 3.1.0
	 *
	 * @global string $mode List table view mode.
	 */
	public function inline_edit() {
		global $mode;

		$screen = $this->screen;

		$post             = get_default_post_to_edit( $screen->post_type );
		$post_type_object = get_post_type_object( $screen->post_type );

		$taxonomy_names          = get_object_taxonomies( $screen->post_type );
		$hierarchical_taxonomies = array();
		$flat_taxonomies         = array();

		foreach ( $taxonomy_names as $taxonomy_name ) {
			$taxonomy = get_taxonomy( $taxonomy_name );

			$show_in_quick_edit = $taxonomy->show_in_quick_edit;

			/**
			 * Filters whether the current taxonomy should be shown in the Quick Edit panel.
			 *
			 * @since 4.2.0
			 *
			 * @param bool   $show_in_quick_edit Whether to show the current taxonomy in Quick Edit.
			 * @param string $taxonomy_name      Taxonomy name.
			 * @param string $post_type          Post type of current Quick Edit post.
			 */
			if ( ! apply_filters( 'quick_edit_show_taxonomy', $show_in_quick_edit, $taxonomy_name, $screen->post_type ) ) {
				continue;
			}

			if ( $taxonomy->hierarchical ) {
				$hierarchical_taxonomies[] = $taxonomy;
			} else {
				$flat_taxonomies[] = $taxonomy;
			}
		}

		$m            = ( isset( $mode ) && 'excerpt' === $mode ) ? 'excerpt' : 'list';
		$can_publish  = current_user_can( $post_type_object->cap->publish_posts );
		$core_columns = array(
			'cb'         => true,
			'date'       => true,
			'title'      => true,
			'categories' => true,
			'tags'       => true,
			'comments'   => true,
			'author'     => true,
		);
		?>

		<form method="get">
		<table style="display: none"><tbody id="inlineedit">
		<?php
		$hclass              = count( $hierarchical_taxonomies ) ? 'post' : 'page';
		$inline_edit_classes = "inline-edit-row inline-edit-row-$hclass";
		$bulk_edit_classes   = "bulk-edit-row bulk-edit-row-$hclass bulk-edit-{$screen->post_type}";
		$quick_edit_classes  = "quick-edit-row quick-edit-row-$hclass inline-edit-{$screen->post_type}";

		$bulk = 0;

		while ( $bulk < 2 ) :
			$classes  = $inline_edit_classes . ' ';
			$classes .= $bulk ? $bulk_edit_classes : $quick_edit_classes;
			?>
			<tr id="<?php echo $bulk ? 'bulk-edit' : 'inline-edit'; ?>" class="<?php echo $classes; ?>" style="display: none">
			<td colspan="<?php echo $this->get_column_count(); ?>" class="colspanchange">
			<div class="inline-edit-wrapper" role="region" aria-labelledby="<?php echo $bulk ? 'bulk' : 'quick'; ?>-edit-legend">
			<fieldset class="inline-edit-col-left">
				<legend class="inline-edit-legend" id="<?php echo $bulk ? 'bulk' : 'quick'; ?>-edit-legend"><?php echo $bulk ? __( 'Bulk Edit' ) : __( 'Quick Edit' ); ?></legend>
				<div class="inline-edit-col">

				<?php if ( post_type_supports( $screen->post_type, 'title' ) ) : ?>

					<?php if ( $bulk ) : ?>

						<div id="bulk-title-div">
							<div id="bulk-titles"></div>
						</div>

					<?php else : // $bulk ?>

						<label>
							<span class="title"><?php _e( 'Title' ); ?></span>
							<span class="input-text-wrap"><input type="text" name="post_title" class="ptitle" value="" /></span>
						</label>

						<?php if ( is_post_type_viewable( $screen->post_type ) ) : ?>

							<label>
								<span class="title"><?php _e( 'Slug' ); ?></span>
								<span class="input-text-wrap"><input type="text" name="post_name" value="" autocomplete="off" spellcheck="false" /></span>
							</label>

						<?php endif; // is_post_type_viewable() ?>

					<?php endif; // $bulk ?>

				<?php endif; // post_type_supports( ... 'title' ) ?>

				<?php if ( ! $bulk ) : ?>
					<fieldset class="inline-edit-date">
						<legend><span class="title"><?php _e( 'Date' ); ?></span></legend>
						<?php touch_time( 1, 1, 0, 1 ); ?>
					</fieldset>
					<br class="clear" />
				<?php endif; // $bulk ?>

				<?php
				if ( post_type_supports( $screen->post_type, 'author' ) ) {
					$authors_dropdown = '';

					if ( current_user_can( $post_type_object->cap->edit_others_posts ) ) {
						$dropdown_name  = 'post_author';
						$dropdown_class = 'authors';
						if ( wp_is_large_user_count() ) {
							$authors_dropdown = sprintf( '<select name="%s" class="%s hidden"></select>', esc_attr( $dropdown_name ), esc_attr( $dropdown_class ) );
						} else {
							$users_opt = array(
								'hide_if_only_one_author' => false,
								'capability'              => array( $post_type_object->cap->edit_posts ),
								'name'                    => $dropdown_name,
								'class'                   => $dropdown_class,
								'multi'                   => 1,
								'echo'                    => 0,
								'show'                    => 'display_name_with_login',
							);

							if ( $bulk ) {
								$users_opt['show_option_none'] = __( '&mdash; No Change &mdash;' );
							}

							/**
							 * Filters the arguments used to generate the Quick Edit authors drop-down.
							 *
							 * @since 5.6.0
							 *
							 * @see wp_dropdown_users()
							 *
							 * @param array $users_opt An array of arguments passed to wp_dropdown_users().
							 * @param bool $bulk A flag to denote if it's a bulk action.
							 */
							$users_opt = apply_filters( 'quick_edit_dropdown_authors_args', $users_opt, $bulk );

							$authors = wp_dropdown_users( $users_opt );

							if ( $authors ) {
								$authors_dropdown  = '<label class="inline-edit-author">';
								$authors_dropdown .= '<span class="title">' . __( 'Author' ) . '</span>';
								$authors_dropdown .= $authors;
								$authors_dropdown .= '</label>';
							}
						}
					} // current_user_can( 'edit_others_posts' )

					if ( ! $bulk ) {
						echo $authors_dropdown;
					}
				} // post_type_supports( ... 'author' )
				?>

				<?php if ( ! $bulk && $can_publish ) : ?>

					<div class="inline-edit-group wp-clearfix">
						<label class="alignleft">
							<span class="title"><?php _e( 'Password' ); ?></span>
							<span class="input-text-wrap"><input type="text" name="post_password" class="inline-edit-password-input" value="" /></span>
						</label>

						<span class="alignleft inline-edit-or">
							<?php
							/* translators: Between password field and private checkbox on post quick edit interface. */
							_e( '&ndash;OR&ndash;' );
							?>
						</span>
						<label class="alignleft inline-edit-private">
							<input type="checkbox" name="keep_private" value="private" />
							<span class="checkbox-title"><?php _e( 'Private' ); ?></span>
						</label>
					</div>

				<?php endif; ?>

				</div>
			</fieldset>

			<?php if ( count( $hierarchical_taxonomies ) && ! $bulk ) : ?>

				<fieldset class="inline-edit-col-center inline-edit-categories">
					<div class="inline-edit-col">

					<?php foreach ( $hierarchical_taxonomies as $taxonomy ) : ?>

						<span class="title inline-edit-categories-label"><?php echo esc_html( $taxonomy->labels->name ); ?></span>
						<input type="hidden" name="<?php echo ( 'category' === $taxonomy->name ) ? 'post_category[]' : 'tax_input[' . esc_attr( $taxonomy->name ) . '][]'; ?>" value="0" />
						<ul class="cat-checklist <?php echo esc_attr( $taxonomy->name ); ?>-checklist">
							<?php wp_terms_checklist( 0, array( 'taxonomy' => $taxonomy->name ) ); ?>
						</ul>

					<?php endforeach; // $hierarchical_taxonomies as $taxonomy ?>

					</div>
				</fieldset>

			<?php endif; // count( $hierarchical_taxonomies ) && ! $bulk ?>

			<fieldset class="inline-edit-col-right">
				<div class="inline-edit-col">

				<?php
				if ( post_type_supports( $screen->post_type, 'author' ) && $bulk ) {
					echo $authors_dropdown;
				}
				?>

				<?php if ( post_type_supports( $screen->post_type, 'page-attributes' ) ) : ?>

					<?php if ( $post_type_object->hierarchical ) : ?>

						<label>
							<span class="title"><?php _e( 'Parent' ); ?></span>
							<?php
							$dropdown_args = array(
								'post_type'         => $post_type_object->name,
								'selected'          => $post->post_parent,
								'name'              => 'post_parent',
								'show_option_none'  => __( 'Main Page (no parent)' ),
								'option_none_value' => 0,
								'sort_column'       => 'menu_order, post_title',
							);

							if ( $bulk ) {
								$dropdown_args['show_option_no_change'] = __( '&mdash; No Change &mdash;' );
								$dropdown_args['id']                    = 'bulk_edit_post_parent';
							}

							/**
							 * Filters the arguments used to generate the Quick Edit page-parent drop-down.
							 *
							 * @since 2.7.0
							 * @since 5.6.0 The `$bulk` parameter was added.
							 *
							 * @see wp_dropdown_pages()
							 *
							 * @param array $dropdown_args An array of arguments passed to wp_dropdown_pages().
							 * @param bool  $bulk          A flag to denote if it's a bulk action.
							 */
							$dropdown_args = apply_filters( 'quick_edit_dropdown_pages_args', $dropdown_args, $bulk );

							wp_dropdown_pages( $dropdown_args );
							?>
						</label>

					<?php endif; // hierarchical ?>

					<?php if ( ! $bulk ) : ?>

						<label>
							<span class="title"><?php _e( 'Order' ); ?></span>
							<span class="input-text-wrap"><input type="text" name="menu_order" class="inline-edit-menu-order-input" value="<?php echo $post->menu_order; ?>" /></span>
						</label>

					<?php endif; // ! $bulk ?>

				<?php endif; // post_type_supports( ... 'page-attributes' ) ?>

				<?php if ( 0 < count( get_page_templates( null, $screen->post_type ) ) ) : ?>

					<label>
						<span class="title"><?php _e( 'Template' ); ?></span>
						<select name="page_template">
							<?php if ( $bulk ) : ?>
							<option value="-1"><?php _e( '&mdash; No Change &mdash;' ); ?></option>
							<?php endif; // $bulk ?>
							<?php
							/** This filter is documented in wp-admin/includes/meta-boxes.php */
							$default_title = apply_filters( 'default_page_template_title', __( 'Default template' ), 'quick-edit' );
							?>
							<option value="default"><?php echo esc_html( $default_title ); ?></option>
							<?php page_template_dropdown( '', $screen->post_type ); ?>
						</select>
					</label>

				<?php endif; ?>

				<?php if ( count( $flat_taxonomies ) && ! $bulk ) : ?>

					<?php foreach ( $flat_taxonomies as $taxonomy ) : ?>

						<?php if ( current_user_can( $taxonomy->cap->assign_terms ) ) : ?>
							<?php $taxonomy_name = esc_attr( $taxonomy->name ); ?>
							<div class="inline-edit-tags-wrap">
							<label class="inline-edit-tags">
								<span class="title"><?php echo esc_html( $taxonomy->labels->name ); ?></span>
								<textarea data-wp-taxonomy="<?php echo $taxonomy_name; ?>" cols="22" rows="1" name="tax_input[<?php echo esc_attr( $taxonomy->name ); ?>]" class="tax_input_<?php echo esc_attr( $taxonomy->name ); ?>" aria-describedby="inline-edit-<?php echo esc_attr( $taxonomy->name ); ?>-desc"></textarea>
							</label>
							<p class="howto" id="inline-edit-<?php echo esc_attr( $taxonomy->name ); ?>-desc"><?php echo esc_html( $taxonomy->labels->separate_items_with_commas ); ?></p>
							</div>
						<?php endif; // current_user_can( 'assign_terms' ) ?>

					<?php endforeach; // $flat_taxonomies as $taxonomy ?>

				<?php endif; // count( $flat_taxonomies ) && ! $bulk ?>

				<?php if ( post_type_supports( $screen->post_type, 'comments' ) || post_type_supports( $screen->post_type, 'trackbacks' ) ) : ?>

					<?php if ( $bulk ) : ?>

						<div class="inline-edit-group wp-clearfix">

						<?php if ( post_type_supports( $screen->post_type, 'comments' ) ) : ?>

							<label class="alignleft">
								<span class="title"><?php _e( 'Comments' ); ?></span>
								<select name="comment_status">
									<option value=""><?php _e( '&mdash; No Change &mdash;' ); ?></option>
									<option value="open"><?php _e( 'Allow' ); ?></option>
									<option value="closed"><?php _e( 'Do not allow' ); ?></option>
								</select>
							</label>

						<?php endif; ?>

						<?php if ( post_type_supports( $screen->post_type, 'trackbacks' ) ) : ?>

							<label class="alignright">
								<span class="title"><?php _e( 'Pings' ); ?></span>
								<select name="ping_status">
									<option value=""><?php _e( '&mdash; No Change &mdash;' ); ?></option>
									<option value="open"><?php _e( 'Allow' ); ?></option>
									<option value="closed"><?php _e( 'Do not allow' ); ?></option>
								</select>
							</label>

						<?php endif; ?>

						</div>

					<?php else : // $bulk ?>

						<div class="inline-edit-group wp-clearfix">

						<?php if ( post_type_supports( $screen->post_type, 'comments' ) ) : ?>

							<label class="alignleft">
								<input type="checkbox" name="comment_status" value="open" />
								<span class="checkbox-title"><?php _e( 'Allow Comments' ); ?></span>
							</label>

						<?php endif; ?>

						<?php if ( post_type_supports( $screen->post_type, 'trackbacks' ) ) : ?>

							<label class="alignleft">
								<input type="checkbox" name="ping_status" value="open" />
								<span class="checkbox-title"><?php _e( 'Allow Pings' ); ?></span>
							</label>

						<?php endif; ?>

						</div>

					<?php endif; // $bulk ?>

				<?php endif; // post_type_supports( ... comments or pings ) ?>

					<div class="inline-edit-group wp-clearfix">

						<label class="inline-edit-status alignleft">
							<span class="title"><?php _e( 'Status' ); ?></span>
							<select name="_status">
								<?php if ( $bulk ) : ?>
									<option value="-1"><?php _e( '&mdash; No Change &mdash;' ); ?></option>
								<?php endif; // $bulk ?>

								<?php if ( $can_publish ) : // Contributors only get "Unpublished" and "Pending Review". ?>
									<option value="publish"><?php _e( 'Published' ); ?></option>
									<option value="future"><?php _e( 'Scheduled' ); ?></option>
									<?php if ( $bulk ) : ?>
										<option value="private"><?php _e( 'Private' ); ?></option>
									<?php endif; // $bulk ?>
								<?php endif; ?>

								<option value="pending"><?php _e( 'Pending Review' ); ?></option>
								<option value="draft"><?php _e( 'Draft' ); ?></option>
							</select>
						</label>

						<?php if ( 'post' === $screen->post_type && $can_publish && current_user_can( $post_type_object->cap->edit_others_posts ) ) : ?>

							<?php if ( $bulk ) : ?>

								<label class="alignright">
									<span class="title"><?php _e( 'Sticky' ); ?></span>
									<select name="sticky">
										<option value="-1"><?php _e( '&mdash; No Change &mdash;' ); ?></option>
										<option value="sticky"><?php _e( 'Sticky' ); ?></option>
										<option value="unsticky"><?php _e( 'Not Sticky' ); ?></option>
									</select>
								</label>

							<?php else : // $bulk ?>

								<label class="alignleft">
									<input type="checkbox" name="sticky" value="sticky" />
									<span class="checkbox-title"><?php _e( 'Make this post sticky' ); ?></span>
								</label>

							<?php endif; // $bulk ?>

						<?php endif; // 'post' && $can_publish && current_user_can( 'edit_others_posts' ) ?>

					</div>

				<?php if ( $bulk && current_theme_supports( 'post-formats' ) && post_type_supports( $screen->post_type, 'post-formats' ) ) : ?>
					<?php $post_formats = get_theme_support( 'post-formats' ); ?>

					<label class="alignleft">
						<span class="title"><?php _ex( 'Format', 'post format' ); ?></span>
						<select name="post_format">
							<option value="-1"><?php _e( '&mdash; No Change &mdash;' ); ?></option>
							<option value="0"><?php echo get_post_format_string( 'standard' ); ?></option>
							<?php if ( is_array( $post_formats[0] ) ) : ?>
								<?php foreach ( $post_formats[0] as $format ) : ?>
									<option value="<?php echo esc_attr( $format ); ?>"><?php echo esc_html( get_post_format_string( $format ) ); ?></option>
								<?php endforeach; ?>
							<?php endif; ?>
						</select>
					</label>

				<?php endif; ?>

				</div>
			</fieldset>

			<?php
			list( $columns ) = $this->get_column_info();

			foreach ( $columns as $column_name => $column_display_name ) {
				if ( isset( $core_columns[ $column_name ] ) ) {
					continue;
				}

				if ( $bulk ) {

					/**
					 * Fires once for each column in Bulk Edit mode.
					 *
					 * @since 2.7.0
					 *
					 * @param string $column_name Name of the column to edit.
					 * @param string $post_type   The post type slug.
					 */
					do_action( 'bulk_edit_custom_box', $column_name, $screen->post_type );
				} else {

					/**
					 * Fires once for each column in Quick Edit mode.
					 *
					 * @since 2.7.0
					 *
					 * @param string $column_name Name of the column to edit.
					 * @param string $post_type   The post type slug, or current screen name if this is a taxonomy list table.
					 * @param string $taxonomy    The taxonomy name, if any.
					 */
					do_action( 'quick_edit_custom_box', $column_name, $screen->post_type, '' );
				}
			}
			?>

			<div class="submit inline-edit-save">
				<?php if ( ! $bulk ) : ?>
					<?php wp_nonce_field( 'inlineeditnonce', '_inline_edit', false ); ?>
					<button type="button" class="button button-primary save"><?php _e( 'Update' ); ?></button>
				<?php else : ?>
					<?php submit_button( __( 'Update' ), 'primary', 'bulk_edit', false ); ?>
				<?php endif; ?>

				<button type="button" class="button cancel"><?php _e( 'Cancel' ); ?></button>

				<?php if ( ! $bulk ) : ?>
					<span class="spinner"></span>
				<?php endif; ?>

				<input type="hidden" name="post_view" value="<?php echo esc_attr( $m ); ?>" />
				<input type="hidden" name="screen" value="<?php echo esc_attr( $screen->id ); ?>" />
				<?php if ( ! $bulk && ! post_type_supports( $screen->post_type, 'author' ) ) : ?>
					<input type="hidden" name="post_author" value="<?php echo esc_attr( $post->post_author ); ?>" />
				<?php endif; ?>

				<?php
				wp_admin_notice(
					'<p class="error"></p>',
					array(
						'type'               => 'error',
						'additional_classes' => array( 'notice-alt', 'inline', 'hidden' ),
						'paragraph_wrap'     => false,
					)
				);
				?>
			</div>
		</div> <!-- end of .inline-edit-wrapper -->

			</td></tr>

			<?php
			++$bulk;
		endwhile;
		?>
		</tbody></table>
		</form>
		<?php
	}
}
deprecated.php000060400000121460151327343360007364 0ustar00<?php
/**
 * Deprecated admin functions from past WordPress versions. You shouldn't use these
 * functions and look for the alternatives instead. The functions will be removed
 * in a later version.
 *
 * @package WordPress
 * @subpackage Deprecated
 */

/*
 * Deprecated functions come here to die.
 */

/**
 * @since 2.1.0
 * @deprecated 2.1.0 Use wp_editor()
 * @see wp_editor()
 */
function tinymce_include() {
	_deprecated_function( __FUNCTION__, '2.1.0', 'wp_editor()' );

	wp_tiny_mce();
}

/**
 * Unused Admin function.
 *
 * @since 2.0.0
 * @deprecated 2.5.0
 *
 */
function documentation_link() {
	_deprecated_function( __FUNCTION__, '2.5.0' );
}

/**
 * Calculates the new dimensions for a downsampled image.
 *
 * @since 2.0.0
 * @deprecated 3.0.0 Use wp_constrain_dimensions()
 * @see wp_constrain_dimensions()
 *
 * @param int $width Current width of the image
 * @param int $height Current height of the image
 * @param int $wmax Maximum wanted width
 * @param int $hmax Maximum wanted height
 * @return array Shrunk dimensions (width, height).
 */
function wp_shrink_dimensions( $width, $height, $wmax = 128, $hmax = 96 ) {
	_deprecated_function( __FUNCTION__, '3.0.0', 'wp_constrain_dimensions()' );
	return wp_constrain_dimensions( $width, $height, $wmax, $hmax );
}

/**
 * Calculated the new dimensions for a downsampled image.
 *
 * @since 2.0.0
 * @deprecated 3.5.0 Use wp_constrain_dimensions()
 * @see wp_constrain_dimensions()
 *
 * @param int $width Current width of the image
 * @param int $height Current height of the image
 * @return array Shrunk dimensions (width, height).
 */
function get_udims( $width, $height ) {
	_deprecated_function( __FUNCTION__, '3.5.0', 'wp_constrain_dimensions()' );
	return wp_constrain_dimensions( $width, $height, 128, 96 );
}

/**
 * Legacy function used to generate the categories checklist control.
 *
 * @since 0.71
 * @deprecated 2.6.0 Use wp_category_checklist()
 * @see wp_category_checklist()
 *
 * @global int $post_ID
 *
 * @param int   $default_category Unused.
 * @param int   $category_parent  Unused.
 * @param array $popular_ids      Unused.
 */
function dropdown_categories( $default_category = 0, $category_parent = 0, $popular_ids = array() ) {
	_deprecated_function( __FUNCTION__, '2.6.0', 'wp_category_checklist()' );
	global $post_ID;
	wp_category_checklist( $post_ID );
}

/**
 * Legacy function used to generate a link categories checklist control.
 *
 * @since 2.1.0
 * @deprecated 2.6.0 Use wp_link_category_checklist()
 * @see wp_link_category_checklist()
 *
 * @global int $link_id
 *
 * @param int $default_link_category Unused.
 */
function dropdown_link_categories( $default_link_category = 0 ) {
	_deprecated_function( __FUNCTION__, '2.6.0', 'wp_link_category_checklist()' );
	global $link_id;
	wp_link_category_checklist( $link_id );
}

/**
 * Get the real filesystem path to a file to edit within the admin.
 *
 * @since 1.5.0
 * @deprecated 2.9.0
 * @uses WP_CONTENT_DIR Full filesystem path to the wp-content directory.
 *
 * @param string $file Filesystem path relative to the wp-content directory.
 * @return string Full filesystem path to edit.
 */
function get_real_file_to_edit( $file ) {
	_deprecated_function( __FUNCTION__, '2.9.0' );

	return WP_CONTENT_DIR . $file;
}

/**
 * Legacy function used for generating a categories drop-down control.
 *
 * @since 1.2.0
 * @deprecated 3.0.0 Use wp_dropdown_categories()
 * @see wp_dropdown_categories()
 *
 * @param int $current_cat     Optional. ID of the current category. Default 0.
 * @param int $current_parent  Optional. Current parent category ID. Default 0.
 * @param int $category_parent Optional. Parent ID to retrieve categories for. Default 0.
 * @param int $level           Optional. Number of levels deep to display. Default 0.
 * @param array $categories    Optional. Categories to include in the control. Default 0.
 * @return void|false Void on success, false if no categories were found.
 */
function wp_dropdown_cats( $current_cat = 0, $current_parent = 0, $category_parent = 0, $level = 0, $categories = 0 ) {
	_deprecated_function( __FUNCTION__, '3.0.0', 'wp_dropdown_categories()' );
	if (!$categories )
		$categories = get_categories( array('hide_empty' => 0) );

	if ( $categories ) {
		foreach ( $categories as $category ) {
			if ( $current_cat != $category->term_id && $category_parent == $category->parent) {
				$pad = str_repeat( '&#8211; ', $level );
				$category->name = esc_html( $category->name );
				echo "\n\t<option value='$category->term_id'";
				if ( $current_parent == $category->term_id )
					echo " selected='selected'";
				echo ">$pad$category->name</option>";
				wp_dropdown_cats( $current_cat, $current_parent, $category->term_id, $level +1, $categories );
			}
		}
	} else {
		return false;
	}
}

/**
 * Register a setting and its sanitization callback
 *
 * @since 2.7.0
 * @deprecated 3.0.0 Use register_setting()
 * @see register_setting()
 *
 * @param string   $option_group      A settings group name. Should correspond to an allowed option key name.
 *                                    Default allowed option key names include 'general', 'discussion', 'media',
 *                                    'reading', 'writing', and 'options'.
 * @param string   $option_name       The name of an option to sanitize and save.
 * @param callable $sanitize_callback Optional. A callback function that sanitizes the option's value.
 */
function add_option_update_handler( $option_group, $option_name, $sanitize_callback = '' ) {
	_deprecated_function( __FUNCTION__, '3.0.0', 'register_setting()' );
	register_setting( $option_group, $option_name, $sanitize_callback );
}

/**
 * Unregister a setting
 *
 * @since 2.7.0
 * @deprecated 3.0.0 Use unregister_setting()
 * @see unregister_setting()
 *
 * @param string   $option_group      The settings group name used during registration.
 * @param string   $option_name       The name of the option to unregister.
 * @param callable $sanitize_callback Optional. Deprecated.
 */
function remove_option_update_handler( $option_group, $option_name, $sanitize_callback = '' ) {
	_deprecated_function( __FUNCTION__, '3.0.0', 'unregister_setting()' );
	unregister_setting( $option_group, $option_name, $sanitize_callback );
}

/**
 * Determines the language to use for CodePress syntax highlighting.
 *
 * @since 2.8.0
 * @deprecated 3.0.0
 *
 * @param string $filename
 */
function codepress_get_lang( $filename ) {
	_deprecated_function( __FUNCTION__, '3.0.0' );
}

/**
 * Adds JavaScript required to make CodePress work on the theme/plugin file editors.
 *
 * @since 2.8.0
 * @deprecated 3.0.0
 */
function codepress_footer_js() {
	_deprecated_function( __FUNCTION__, '3.0.0' );
}

/**
 * Determine whether to use CodePress.
 *
 * @since 2.8.0
 * @deprecated 3.0.0
 */
function use_codepress() {
	_deprecated_function( __FUNCTION__, '3.0.0' );
}

/**
 * Get all user IDs.
 *
 * @deprecated 3.1.0 Use get_users()
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @return array List of user IDs.
 */
function get_author_user_ids() {
	_deprecated_function( __FUNCTION__, '3.1.0', 'get_users()' );

	global $wpdb;
	if ( !is_multisite() )
		$level_key = $wpdb->get_blog_prefix() . 'user_level';
	else
		$level_key = $wpdb->get_blog_prefix() . 'capabilities'; // WPMU site admins don't have user_levels.

	return $wpdb->get_col( $wpdb->prepare("SELECT user_id FROM $wpdb->usermeta WHERE meta_key = %s AND meta_value != '0'", $level_key) );
}

/**
 * Gets author users who can edit posts.
 *
 * @deprecated 3.1.0 Use get_users()
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param int $user_id User ID.
 * @return array|false List of editable authors. False if no editable users.
 */
function get_editable_authors( $user_id ) {
	_deprecated_function( __FUNCTION__, '3.1.0', 'get_users()' );

	global $wpdb;

	$editable = get_editable_user_ids( $user_id );

	if ( !$editable ) {
		return false;
	} else {
		$editable = join(',', $editable);
		$authors = $wpdb->get_results( "SELECT * FROM $wpdb->users WHERE ID IN ($editable) ORDER BY display_name" );
	}

	return apply_filters('get_editable_authors', $authors);
}

/**
 * Gets the IDs of any users who can edit posts.
 *
 * @deprecated 3.1.0 Use get_users()
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param int  $user_id       User ID.
 * @param bool $exclude_zeros Optional. Whether to exclude zeroes. Default true.
 * @return array Array of editable user IDs, empty array otherwise.
 */
function get_editable_user_ids( $user_id, $exclude_zeros = true, $post_type = 'post' ) {
	_deprecated_function( __FUNCTION__, '3.1.0', 'get_users()' );

	global $wpdb;

	if ( ! $user = get_userdata( $user_id ) )
		return array();
	$post_type_obj = get_post_type_object($post_type);

	if ( ! $user->has_cap($post_type_obj->cap->edit_others_posts) ) {
		if ( $user->has_cap($post_type_obj->cap->edit_posts) || ! $exclude_zeros )
			return array($user->ID);
		else
			return array();
	}

	if ( !is_multisite() )
		$level_key = $wpdb->get_blog_prefix() . 'user_level';
	else
		$level_key = $wpdb->get_blog_prefix() . 'capabilities'; // WPMU site admins don't have user_levels.

	$query = $wpdb->prepare("SELECT user_id FROM $wpdb->usermeta WHERE meta_key = %s", $level_key);
	if ( $exclude_zeros )
		$query .= " AND meta_value != '0'";

	return $wpdb->get_col( $query );
}

/**
 * Gets all users who are not authors.
 *
 * @deprecated 3.1.0 Use get_users()
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 */
function get_nonauthor_user_ids() {
	_deprecated_function( __FUNCTION__, '3.1.0', 'get_users()' );

	global $wpdb;

	if ( !is_multisite() )
		$level_key = $wpdb->get_blog_prefix() . 'user_level';
	else
		$level_key = $wpdb->get_blog_prefix() . 'capabilities'; // WPMU site admins don't have user_levels.

	return $wpdb->get_col( $wpdb->prepare("SELECT user_id FROM $wpdb->usermeta WHERE meta_key = %s AND meta_value = '0'", $level_key) );
}

if ( ! class_exists( 'WP_User_Search', false ) ) :
/**
 * WordPress User Search class.
 *
 * @since 2.1.0
 * @deprecated 3.1.0 Use WP_User_Query
 */
class WP_User_Search {

	/**
	 * {@internal Missing Description}}
	 *
	 * @since 2.1.0
	 * @access private
	 * @var mixed
	 */
	var $results;

	/**
	 * {@internal Missing Description}}
	 *
	 * @since 2.1.0
	 * @access private
	 * @var string
	 */
	var $search_term;

	/**
	 * Page number.
	 *
	 * @since 2.1.0
	 * @access private
	 * @var int
	 */
	var $page;

	/**
	 * Role name that users have.
	 *
	 * @since 2.5.0
	 * @access private
	 * @var string
	 */
	var $role;

	/**
	 * Raw page number.
	 *
	 * @since 2.1.0
	 * @access private
	 * @var int|bool
	 */
	var $raw_page;

	/**
	 * Amount of users to display per page.
	 *
	 * @since 2.1.0
	 * @access public
	 * @var int
	 */
	var $users_per_page = 50;

	/**
	 * {@internal Missing Description}}
	 *
	 * @since 2.1.0
	 * @access private
	 * @var int
	 */
	var $first_user;

	/**
	 * {@internal Missing Description}}
	 *
	 * @since 2.1.0
	 * @access private
	 * @var int
	 */
	var $last_user;

	/**
	 * {@internal Missing Description}}
	 *
	 * @since 2.1.0
	 * @access private
	 * @var string
	 */
	var $query_limit;

	/**
	 * {@internal Missing Description}}
	 *
	 * @since 3.0.0
	 * @access private
	 * @var string
	 */
	var $query_orderby;

	/**
	 * {@internal Missing Description}}
	 *
	 * @since 3.0.0
	 * @access private
	 * @var string
	 */
	var $query_from;

	/**
	 * {@internal Missing Description}}
	 *
	 * @since 3.0.0
	 * @access private
	 * @var string
	 */
	var $query_where;

	/**
	 * {@internal Missing Description}}
	 *
	 * @since 2.1.0
	 * @access private
	 * @var int
	 */
	var $total_users_for_query = 0;

	/**
	 * {@internal Missing Description}}
	 *
	 * @since 2.1.0
	 * @access private
	 * @var bool
	 */
	var $too_many_total_users = false;

	/**
	 * {@internal Missing Description}}
	 *
	 * @since 2.1.0
	 * @access private
	 * @var WP_Error
	 */
	var $search_errors;

	/**
	 * {@internal Missing Description}}
	 *
	 * @since 2.7.0
	 * @access private
	 * @var string
	 */
	var $paging_text;

	/**
	 * PHP5 Constructor - Sets up the object properties.
	 *
	 * @since 2.1.0
	 *
	 * @param string $search_term Search terms string.
	 * @param int $page Optional. Page ID.
	 * @param string $role Role name.
	 * @return WP_User_Search
	 */
	function __construct( $search_term = '', $page = '', $role = '' ) {
		_deprecated_class( 'WP_User_Search', '3.1.0', 'WP_User_Query' );

		$this->search_term = wp_unslash( $search_term );
		$this->raw_page = ( '' == $page ) ? false : (int) $page;
		$this->page = ( '' == $page ) ? 1 : (int) $page;
		$this->role = $role;

		$this->prepare_query();
		$this->query();
		$this->do_paging();
	}

	/**
	 * PHP4 Constructor - Sets up the object properties.
	 *
	 * @since 2.1.0
	 *
	 * @param string $search_term Search terms string.
	 * @param int $page Optional. Page ID.
	 * @param string $role Role name.
	 * @return WP_User_Search
	 */
	public function WP_User_Search( $search_term = '', $page = '', $role = '' ) {
		_deprecated_constructor( 'WP_User_Search', '3.1.0', get_class( $this ) );
		self::__construct( $search_term, $page, $role );
	}

	/**
	 * Prepares the user search query (legacy).
	 *
	 * @since 2.1.0
	 * @access public
	 *
	 * @global wpdb $wpdb WordPress database abstraction object.
	 */
	public function prepare_query() {
		global $wpdb;
		$this->first_user = ($this->page - 1) * $this->users_per_page;

		$this->query_limit = $wpdb->prepare(" LIMIT %d, %d", $this->first_user, $this->users_per_page);
		$this->query_orderby = ' ORDER BY user_login';

		$search_sql = '';
		if ( $this->search_term ) {
			$searches = array();
			$search_sql = 'AND (';
			foreach ( array('user_login', 'user_nicename', 'user_email', 'user_url', 'display_name') as $col )
				$searches[] = $wpdb->prepare( $col . ' LIKE %s', '%' . like_escape($this->search_term) . '%' );
			$search_sql .= implode(' OR ', $searches);
			$search_sql .= ')';
		}

		$this->query_from = " FROM $wpdb->users";
		$this->query_where = " WHERE 1=1 $search_sql";

		if ( $this->role ) {
			$this->query_from .= " INNER JOIN $wpdb->usermeta ON $wpdb->users.ID = $wpdb->usermeta.user_id";
			$this->query_where .= $wpdb->prepare(" AND $wpdb->usermeta.meta_key = '{$wpdb->prefix}capabilities' AND $wpdb->usermeta.meta_value LIKE %s", '%' . $this->role . '%');
		} elseif ( is_multisite() ) {
			$level_key = $wpdb->prefix . 'capabilities'; // WPMU site admins don't have user_levels.
			$this->query_from .= ", $wpdb->usermeta";
			$this->query_where .= " AND $wpdb->users.ID = $wpdb->usermeta.user_id AND meta_key = '{$level_key}'";
		}

		do_action_ref_array( 'pre_user_search', array( &$this ) );
	}

	/**
	 * Executes the user search query.
	 *
	 * @since 2.1.0
	 * @access public
	 *
	 * @global wpdb $wpdb WordPress database abstraction object.
	 */
	public function query() {
		global $wpdb;

		$this->results = $wpdb->get_col("SELECT DISTINCT($wpdb->users.ID)" . $this->query_from . $this->query_where . $this->query_orderby . $this->query_limit);

		if ( $this->results )
			$this->total_users_for_query = $wpdb->get_var("SELECT COUNT(DISTINCT($wpdb->users.ID))" . $this->query_from . $this->query_where); // No limit.
		else
			$this->search_errors = new WP_Error('no_matching_users_found', __('No users found.'));
	}

	/**
	 * Prepares variables for use in templates.
	 *
	 * @since 2.1.0
	 * @access public
	 */
	function prepare_vars_for_template_usage() {}

	/**
	 * Handles paging for the user search query.
	 *
	 * @since 2.1.0
	 * @access public
	 */
	public function do_paging() {
		if ( $this->total_users_for_query > $this->users_per_page ) { // Have to page the results.
			$args = array();
			if ( ! empty($this->search_term) )
				$args['usersearch'] = urlencode($this->search_term);
			if ( ! empty($this->role) )
				$args['role'] = urlencode($this->role);

			$this->paging_text = paginate_links( array(
				'total' => ceil($this->total_users_for_query / $this->users_per_page),
				'current' => $this->page,
				'base' => 'users.php?%_%',
				'format' => 'userspage=%#%',
				'add_args' => $args
			) );
			if ( $this->paging_text ) {
				$this->paging_text = sprintf(
					/* translators: 1: Starting number of users on the current page, 2: Ending number of users, 3: Total number of users. */
					'<span class="displaying-num">' . __( 'Displaying %1$s&#8211;%2$s of %3$s' ) . '</span>%s',
					number_format_i18n( ( $this->page - 1 ) * $this->users_per_page + 1 ),
					number_format_i18n( min( $this->page * $this->users_per_page, $this->total_users_for_query ) ),
					number_format_i18n( $this->total_users_for_query ),
					$this->paging_text
				);
			}
		}
	}

	/**
	 * Retrieves the user search query results.
	 *
	 * @since 2.1.0
	 * @access public
	 *
	 * @return array
	 */
	public function get_results() {
		return (array) $this->results;
	}

	/**
	 * Displaying paging text.
	 *
	 * @see do_paging() Builds paging text.
	 *
	 * @since 2.1.0
	 * @access public
	 */
	function page_links() {
		echo $this->paging_text;
	}

	/**
	 * Whether paging is enabled.
	 *
	 * @see do_paging() Builds paging text.
	 *
	 * @since 2.1.0
	 * @access public
	 *
	 * @return bool
	 */
	function results_are_paged() {
		if ( $this->paging_text )
			return true;
		return false;
	}

	/**
	 * Whether there are search terms.
	 *
	 * @since 2.1.0
	 * @access public
	 *
	 * @return bool
	 */
	function is_search() {
		if ( $this->search_term )
			return true;
		return false;
	}
}
endif;

/**
 * Retrieves editable posts from other users.
 *
 * @since 2.3.0
 * @deprecated 3.1.0 Use get_posts()
 * @see get_posts()
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param int    $user_id User ID to not retrieve posts from.
 * @param string $type    Optional. Post type to retrieve. Accepts 'draft', 'pending' or 'any' (all).
 *                        Default 'any'.
 * @return array List of posts from others.
 */
function get_others_unpublished_posts( $user_id, $type = 'any' ) {
	_deprecated_function( __FUNCTION__, '3.1.0' );

	global $wpdb;

	$editable = get_editable_user_ids( $user_id );

	if ( in_array($type, array('draft', 'pending')) )
		$type_sql = " post_status = '$type' ";
	else
		$type_sql = " ( post_status = 'draft' OR post_status = 'pending' ) ";

	$dir = ( 'pending' == $type ) ? 'ASC' : 'DESC';

	if ( !$editable ) {
		$other_unpubs = '';
	} else {
		$editable = join(',', $editable);
		$other_unpubs = $wpdb->get_results( $wpdb->prepare("SELECT ID, post_title, post_author FROM $wpdb->posts WHERE post_type = 'post' AND $type_sql AND post_author IN ($editable) AND post_author != %d ORDER BY post_modified $dir", $user_id) );
	}

	return apply_filters('get_others_drafts', $other_unpubs);
}

/**
 * Retrieve drafts from other users.
 *
 * @deprecated 3.1.0 Use get_posts()
 * @see get_posts()
 *
 * @param int $user_id User ID.
 * @return array List of drafts from other users.
 */
function get_others_drafts($user_id) {
	_deprecated_function( __FUNCTION__, '3.1.0' );

	return get_others_unpublished_posts($user_id, 'draft');
}

/**
 * Retrieve pending review posts from other users.
 *
 * @deprecated 3.1.0 Use get_posts()
 * @see get_posts()
 *
 * @param int $user_id User ID.
 * @return array List of posts with pending review post type from other users.
 */
function get_others_pending($user_id) {
	_deprecated_function( __FUNCTION__, '3.1.0' );

	return get_others_unpublished_posts($user_id, 'pending');
}

/**
 * Output the QuickPress dashboard widget.
 *
 * @since 3.0.0
 * @deprecated 3.2.0 Use wp_dashboard_quick_press()
 * @see wp_dashboard_quick_press()
 */
function wp_dashboard_quick_press_output() {
	_deprecated_function( __FUNCTION__, '3.2.0', 'wp_dashboard_quick_press()' );
	wp_dashboard_quick_press();
}

/**
 * Outputs the TinyMCE editor.
 *
 * @since 2.7.0
 * @deprecated 3.3.0 Use wp_editor()
 * @see wp_editor()
 */
function wp_tiny_mce( $teeny = false, $settings = false ) {
	_deprecated_function( __FUNCTION__, '3.3.0', 'wp_editor()' );

	static $num = 1;

	if ( ! class_exists( '_WP_Editors', false ) )
		require_once ABSPATH . WPINC . '/class-wp-editor.php';

	$editor_id = 'content' . $num++;

	$set = array(
		'teeny' => $teeny,
		'tinymce' => $settings ? $settings : true,
		'quicktags' => false
	);

	$set = _WP_Editors::parse_settings($editor_id, $set);
	_WP_Editors::editor_settings($editor_id, $set);
}

/**
 * Preloads TinyMCE dialogs.
 *
 * @deprecated 3.3.0 Use wp_editor()
 * @see wp_editor()
 */
function wp_preload_dialogs() {
	_deprecated_function( __FUNCTION__, '3.3.0', 'wp_editor()' );
}

/**
 * Prints TinyMCE editor JS.
 *
 * @deprecated 3.3.0 Use wp_editor()
 * @see wp_editor()
 */
function wp_print_editor_js() {
	_deprecated_function( __FUNCTION__, '3.3.0', 'wp_editor()' );
}

/**
 * Handles quicktags.
 *
 * @deprecated 3.3.0 Use wp_editor()
 * @see wp_editor()
 */
function wp_quicktags() {
	_deprecated_function( __FUNCTION__, '3.3.0', 'wp_editor()' );
}

/**
 * Returns the screen layout options.
 *
 * @since 2.8.0
 * @deprecated 3.3.0 WP_Screen::render_screen_layout()
 * @see WP_Screen::render_screen_layout()
 */
function screen_layout( $screen ) {
	_deprecated_function( __FUNCTION__, '3.3.0', '$current_screen->render_screen_layout()' );

	$current_screen = get_current_screen();

	if ( ! $current_screen )
		return '';

	ob_start();
	$current_screen->render_screen_layout();
	return ob_get_clean();
}

/**
 * Returns the screen's per-page options.
 *
 * @since 2.8.0
 * @deprecated 3.3.0 Use WP_Screen::render_per_page_options()
 * @see WP_Screen::render_per_page_options()
 */
function screen_options( $screen ) {
	_deprecated_function( __FUNCTION__, '3.3.0', '$current_screen->render_per_page_options()' );

	$current_screen = get_current_screen();

	if ( ! $current_screen )
		return '';

	ob_start();
	$current_screen->render_per_page_options();
	return ob_get_clean();
}

/**
 * Renders the screen's help.
 *
 * @since 2.7.0
 * @deprecated 3.3.0 Use WP_Screen::render_screen_meta()
 * @see WP_Screen::render_screen_meta()
 */
function screen_meta( $screen ) {
	$current_screen = get_current_screen();
	$current_screen->render_screen_meta();
}

/**
 * Favorite actions were deprecated in version 3.2. Use the admin bar instead.
 *
 * @since 2.7.0
 * @deprecated 3.2.0 Use WP_Admin_Bar
 * @see WP_Admin_Bar
 */
function favorite_actions() {
	_deprecated_function( __FUNCTION__, '3.2.0', 'WP_Admin_Bar' );
}

/**
 * Handles uploading an image.
 *
 * @deprecated 3.3.0 Use wp_media_upload_handler()
 * @see wp_media_upload_handler()
 *
 * @return null|string
 */
function media_upload_image() {
	_deprecated_function( __FUNCTION__, '3.3.0', 'wp_media_upload_handler()' );
	return wp_media_upload_handler();
}

/**
 * Handles uploading an audio file.
 *
 * @deprecated 3.3.0 Use wp_media_upload_handler()
 * @see wp_media_upload_handler()
 *
 * @return null|string
 */
function media_upload_audio() {
	_deprecated_function( __FUNCTION__, '3.3.0', 'wp_media_upload_handler()' );
	return wp_media_upload_handler();
}

/**
 * Handles uploading a video file.
 *
 * @deprecated 3.3.0 Use wp_media_upload_handler()
 * @see wp_media_upload_handler()
 *
 * @return null|string
 */
function media_upload_video() {
	_deprecated_function( __FUNCTION__, '3.3.0', 'wp_media_upload_handler()' );
	return wp_media_upload_handler();
}

/**
 * Handles uploading a generic file.
 *
 * @deprecated 3.3.0 Use wp_media_upload_handler()
 * @see wp_media_upload_handler()
 *
 * @return null|string
 */
function media_upload_file() {
	_deprecated_function( __FUNCTION__, '3.3.0', 'wp_media_upload_handler()' );
	return wp_media_upload_handler();
}

/**
 * Handles retrieving the insert-from-URL form for an image.
 *
 * @deprecated 3.3.0 Use wp_media_insert_url_form()
 * @see wp_media_insert_url_form()
 *
 * @return string
 */
function type_url_form_image() {
	_deprecated_function( __FUNCTION__, '3.3.0', "wp_media_insert_url_form('image')" );
	return wp_media_insert_url_form( 'image' );
}

/**
 * Handles retrieving the insert-from-URL form for an audio file.
 *
 * @deprecated 3.3.0 Use wp_media_insert_url_form()
 * @see wp_media_insert_url_form()
 *
 * @return string
 */
function type_url_form_audio() {
	_deprecated_function( __FUNCTION__, '3.3.0', "wp_media_insert_url_form('audio')" );
	return wp_media_insert_url_form( 'audio' );
}

/**
 * Handles retrieving the insert-from-URL form for a video file.
 *
 * @deprecated 3.3.0 Use wp_media_insert_url_form()
 * @see wp_media_insert_url_form()
 *
 * @return string
 */
function type_url_form_video() {
	_deprecated_function( __FUNCTION__, '3.3.0', "wp_media_insert_url_form('video')" );
	return wp_media_insert_url_form( 'video' );
}

/**
 * Handles retrieving the insert-from-URL form for a generic file.
 *
 * @deprecated 3.3.0 Use wp_media_insert_url_form()
 * @see wp_media_insert_url_form()
 *
 * @return string
 */
function type_url_form_file() {
	_deprecated_function( __FUNCTION__, '3.3.0', "wp_media_insert_url_form('file')" );
	return wp_media_insert_url_form( 'file' );
}

/**
 * Add contextual help text for a page.
 *
 * Creates an 'Overview' help tab.
 *
 * @since 2.7.0
 * @deprecated 3.3.0 Use WP_Screen::add_help_tab()
 * @see WP_Screen::add_help_tab()
 *
 * @param string    $screen The handle for the screen to add help to. This is usually
 *                          the hook name returned by the `add_*_page()` functions.
 * @param string    $help   The content of an 'Overview' help tab.
 */
function add_contextual_help( $screen, $help ) {
	_deprecated_function( __FUNCTION__, '3.3.0', 'get_current_screen()->add_help_tab()' );

	if ( is_string( $screen ) )
		$screen = convert_to_screen( $screen );

	WP_Screen::add_old_compat_help( $screen, $help );
}

/**
 * Get the allowed themes for the current site.
 *
 * @since 3.0.0
 * @deprecated 3.4.0 Use wp_get_themes()
 * @see wp_get_themes()
 *
 * @return WP_Theme[] Array of WP_Theme objects keyed by their name.
 */
function get_allowed_themes() {
	_deprecated_function( __FUNCTION__, '3.4.0', "wp_get_themes( array( 'allowed' => true ) )" );

	$themes = wp_get_themes( array( 'allowed' => true ) );

	$wp_themes = array();
	foreach ( $themes as $theme ) {
		$wp_themes[ $theme->get('Name') ] = $theme;
	}

	return $wp_themes;
}

/**
 * Retrieves a list of broken themes.
 *
 * @since 1.5.0
 * @deprecated 3.4.0 Use wp_get_themes()
 * @see wp_get_themes()
 *
 * @return array
 */
function get_broken_themes() {
	_deprecated_function( __FUNCTION__, '3.4.0', "wp_get_themes( array( 'errors' => true )" );

	$themes = wp_get_themes( array( 'errors' => true ) );
	$broken = array();
	foreach ( $themes as $theme ) {
		$name = $theme->get('Name');
		$broken[ $name ] = array(
			'Name' => $name,
			'Title' => $name,
			'Description' => $theme->errors()->get_error_message(),
		);
	}
	return $broken;
}

/**
 * Retrieves information on the current active theme.
 *
 * @since 2.0.0
 * @deprecated 3.4.0 Use wp_get_theme()
 * @see wp_get_theme()
 *
 * @return WP_Theme
 */
function current_theme_info() {
	_deprecated_function( __FUNCTION__, '3.4.0', 'wp_get_theme()' );

	return wp_get_theme();
}

/**
 * This was once used to display an 'Insert into Post' button.
 *
 * Now it is deprecated and stubbed.
 *
 * @deprecated 3.5.0
 */
function _insert_into_post_button( $type ) {
	_deprecated_function( __FUNCTION__, '3.5.0' );
}

/**
 * This was once used to display a media button.
 *
 * Now it is deprecated and stubbed.
 *
 * @deprecated 3.5.0
 */
function _media_button($title, $icon, $type, $id) {
	_deprecated_function( __FUNCTION__, '3.5.0' );
}

/**
 * Gets an existing post and format it for editing.
 *
 * @since 2.0.0
 * @deprecated 3.5.0 Use get_post()
 * @see get_post()
 *
 * @param int $id
 * @return WP_Post
 */
function get_post_to_edit( $id ) {
	_deprecated_function( __FUNCTION__, '3.5.0', 'get_post()' );

	return get_post( $id, OBJECT, 'edit' );
}

/**
 * Gets the default page information to use.
 *
 * @since 2.5.0
 * @deprecated 3.5.0 Use get_default_post_to_edit()
 * @see get_default_post_to_edit()
 *
 * @return WP_Post Post object containing all the default post data as attributes
 */
function get_default_page_to_edit() {
	_deprecated_function( __FUNCTION__, '3.5.0', "get_default_post_to_edit( 'page' )" );

	$page = get_default_post_to_edit();
	$page->post_type = 'page';
	return $page;
}

/**
 * This was once used to create a thumbnail from an Image given a maximum side size.
 *
 * @since 1.2.0
 * @deprecated 3.5.0 Use image_resize()
 * @see image_resize()
 *
 * @param mixed $file Filename of the original image, Or attachment ID.
 * @param int $max_side Maximum length of a single side for the thumbnail.
 * @param mixed $deprecated Never used.
 * @return string Thumbnail path on success, Error string on failure.
 */
function wp_create_thumbnail( $file, $max_side, $deprecated = '' ) {
	_deprecated_function( __FUNCTION__, '3.5.0', 'image_resize()' );
	return apply_filters( 'wp_create_thumbnail', image_resize( $file, $max_side, $max_side ) );
}

/**
 * This was once used to display a meta box for the nav menu theme locations.
 *
 * Deprecated in favor of a 'Manage Locations' tab added to nav menus management screen.
 *
 * @since 3.0.0
 * @deprecated 3.6.0
 */
function wp_nav_menu_locations_meta_box() {
	_deprecated_function( __FUNCTION__, '3.6.0' );
}

/**
 * This was once used to kick-off the Core Updater.
 *
 * Deprecated in favor of instantiating a Core_Upgrader instance directly,
 * and calling the 'upgrade' method.
 *
 * @since 2.7.0
 * @deprecated 3.7.0 Use Core_Upgrader
 * @see Core_Upgrader
 */
function wp_update_core($current, $feedback = '') {
	_deprecated_function( __FUNCTION__, '3.7.0', 'new Core_Upgrader();' );

	if ( !empty($feedback) )
		add_filter('update_feedback', $feedback);

	require ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
	$upgrader = new Core_Upgrader();
	return $upgrader->upgrade($current);

}

/**
 * This was once used to kick-off the Plugin Updater.
 *
 * Deprecated in favor of instantiating a Plugin_Upgrader instance directly,
 * and calling the 'upgrade' method.
 * Unused since 2.8.0.
 *
 * @since 2.5.0
 * @deprecated 3.7.0 Use Plugin_Upgrader
 * @see Plugin_Upgrader
 */
function wp_update_plugin($plugin, $feedback = '') {
	_deprecated_function( __FUNCTION__, '3.7.0', 'new Plugin_Upgrader();' );

	if ( !empty($feedback) )
		add_filter('update_feedback', $feedback);

	require ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
	$upgrader = new Plugin_Upgrader();
	return $upgrader->upgrade($plugin);
}

/**
 * This was once used to kick-off the Theme Updater.
 *
 * Deprecated in favor of instantiating a Theme_Upgrader instance directly,
 * and calling the 'upgrade' method.
 * Unused since 2.8.0.
 *
 * @since 2.7.0
 * @deprecated 3.7.0 Use Theme_Upgrader
 * @see Theme_Upgrader
 */
function wp_update_theme($theme, $feedback = '') {
	_deprecated_function( __FUNCTION__, '3.7.0', 'new Theme_Upgrader();' );

	if ( !empty($feedback) )
		add_filter('update_feedback', $feedback);

	require ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
	$upgrader = new Theme_Upgrader();
	return $upgrader->upgrade($theme);
}

/**
 * This was once used to display attachment links. Now it is deprecated and stubbed.
 *
 * @since 2.0.0
 * @deprecated 3.7.0
 *
 * @param int|bool $id
 */
function the_attachment_links( $id = false ) {
	_deprecated_function( __FUNCTION__, '3.7.0' );
}

/**
 * Displays a screen icon.
 *
 * @since 2.7.0
 * @deprecated 3.8.0
 */
function screen_icon() {
	_deprecated_function( __FUNCTION__, '3.8.0' );
	echo get_screen_icon();
}

/**
 * Retrieves the screen icon (no longer used in 3.8+).
 *
 * @since 3.2.0
 * @deprecated 3.8.0
 *
 * @return string An HTML comment explaining that icons are no longer used.
 */
function get_screen_icon() {
	_deprecated_function( __FUNCTION__, '3.8.0' );
	return '<!-- Screen icons are no longer used as of WordPress 3.8. -->';
}

/**
 * Deprecated dashboard widget controls.
 *
 * @since 2.5.0
 * @deprecated 3.8.0
 */
function wp_dashboard_incoming_links_output() {}

/**
 * Deprecated dashboard secondary output.
 *
 * @deprecated 3.8.0
 */
function wp_dashboard_secondary_output() {}

/**
 * Deprecated dashboard widget controls.
 *
 * @since 2.7.0
 * @deprecated 3.8.0
 */
function wp_dashboard_incoming_links() {}

/**
 * Deprecated dashboard incoming links control.
 *
 * @deprecated 3.8.0
 */
function wp_dashboard_incoming_links_control() {}

/**
 * Deprecated dashboard plugins control.
 *
 * @deprecated 3.8.0
 */
function wp_dashboard_plugins() {}

/**
 * Deprecated dashboard primary control.
 *
 * @deprecated 3.8.0
 */
function wp_dashboard_primary_control() {}

/**
 * Deprecated dashboard recent comments control.
 *
 * @deprecated 3.8.0
 */
function wp_dashboard_recent_comments_control() {}

/**
 * Deprecated dashboard secondary section.
 *
 * @deprecated 3.8.0
 */
function wp_dashboard_secondary() {}

/**
 * Deprecated dashboard secondary control.
 *
 * @deprecated 3.8.0
 */
function wp_dashboard_secondary_control() {}

/**
 * Display plugins text for the WordPress news widget.
 *
 * @since 2.5.0
 * @deprecated 4.8.0
 *
 * @param string $rss  The RSS feed URL.
 * @param array  $args Array of arguments for this RSS feed.
 */
function wp_dashboard_plugins_output( $rss, $args = array() ) {
	_deprecated_function( __FUNCTION__, '4.8.0' );

	// Plugin feeds plus link to install them.
	$popular = fetch_feed( $args['url']['popular'] );

	if ( false === $plugin_slugs = get_transient( 'plugin_slugs' ) ) {
		$plugin_slugs = array_keys( get_plugins() );
		set_transient( 'plugin_slugs', $plugin_slugs, DAY_IN_SECONDS );
	}

	echo '<ul>';

	foreach ( array( $popular ) as $feed ) {
		if ( is_wp_error( $feed ) || ! $feed->get_item_quantity() )
			continue;

		$items = $feed->get_items(0, 5);

		// Pick a random, non-installed plugin.
		while ( true ) {
			// Abort this foreach loop iteration if there's no plugins left of this type.
			if ( 0 === count($items) )
				continue 2;

			$item_key = array_rand($items);
			$item = $items[$item_key];

			list($link, $frag) = explode( '#', $item->get_link() );

			$link = esc_url($link);
			if ( preg_match( '|/([^/]+?)/?$|', $link, $matches ) )
				$slug = $matches[1];
			else {
				unset( $items[$item_key] );
				continue;
			}

			// Is this random plugin's slug already installed? If so, try again.
			reset( $plugin_slugs );
			foreach ( $plugin_slugs as $plugin_slug ) {
				if ( str_starts_with( $plugin_slug, $slug ) ) {
					unset( $items[$item_key] );
					continue 2;
				}
			}

			// If we get to this point, then the random plugin isn't installed and we can stop the while().
			break;
		}

		// Eliminate some common badly formed plugin descriptions.
		while ( ( null !== $item_key = array_rand($items) ) && str_contains( $items[$item_key]->get_description(), 'Plugin Name:' ) )
			unset($items[$item_key]);

		if ( !isset($items[$item_key]) )
			continue;

		$raw_title = $item->get_title();

		$ilink = wp_nonce_url('plugin-install.php?tab=plugin-information&plugin=' . $slug, 'install-plugin_' . $slug) . '&amp;TB_iframe=true&amp;width=600&amp;height=800';
		echo '<li class="dashboard-news-plugin"><span>' . __( 'Popular Plugin' ) . ':</span> ' . esc_html( $raw_title ) .
			'&nbsp;<a href="' . $ilink . '" class="thickbox open-plugin-details-modal" aria-label="' .
			/* translators: %s: Plugin name. */
			esc_attr( sprintf( _x( 'Install %s', 'plugin' ), $raw_title ) ) . '">(' . __( 'Install' ) . ')</a></li>';

		$feed->__destruct();
		unset( $feed );
	}

	echo '</ul>';
}

/**
 * This was once used to move child posts to a new parent.
 *
 * @since 2.3.0
 * @deprecated 3.9.0
 * @access private
 *
 * @param int $old_ID
 * @param int $new_ID
 */
function _relocate_children( $old_ID, $new_ID ) {
	_deprecated_function( __FUNCTION__, '3.9.0' );
}

/**
 * Add a top-level menu page in the 'objects' section.
 *
 * This function takes a capability which will be used to determine whether
 * or not a page is included in the menu.
 *
 * The function which is hooked in to handle the output of the page must check
 * that the user has the required capability as well.
 *
 * @since 2.7.0
 *
 * @deprecated 4.5.0 Use add_menu_page()
 * @see add_menu_page()
 * @global int $_wp_last_object_menu
 *
 * @param string   $page_title The text to be displayed in the title tags of the page when the menu is selected.
 * @param string   $menu_title The text to be used for the menu.
 * @param string   $capability The capability required for this menu to be displayed to the user.
 * @param string   $menu_slug  The slug name to refer to this menu by (should be unique for this menu).
 * @param callable $callback   Optional. The function to be called to output the content for this page.
 * @param string   $icon_url   Optional. The URL to the icon to be used for this menu.
 * @return string The resulting page's hook_suffix.
 */
function add_object_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $icon_url = '') {
	_deprecated_function( __FUNCTION__, '4.5.0', 'add_menu_page()' );

	global $_wp_last_object_menu;

	$_wp_last_object_menu++;

	return add_menu_page($page_title, $menu_title, $capability, $menu_slug, $callback, $icon_url, $_wp_last_object_menu);
}

/**
 * Add a top-level menu page in the 'utility' section.
 *
 * This function takes a capability which will be used to determine whether
 * or not a page is included in the menu.
 *
 * The function which is hooked in to handle the output of the page must check
 * that the user has the required capability as well.
 *
 * @since 2.7.0
 *
 * @deprecated 4.5.0 Use add_menu_page()
 * @see add_menu_page()
 * @global int $_wp_last_utility_menu
 *
 * @param string   $page_title The text to be displayed in the title tags of the page when the menu is selected.
 * @param string   $menu_title The text to be used for the menu.
 * @param string   $capability The capability required for this menu to be displayed to the user.
 * @param string   $menu_slug  The slug name to refer to this menu by (should be unique for this menu).
 * @param callable $callback   Optional. The function to be called to output the content for this page.
 * @param string   $icon_url   Optional. The URL to the icon to be used for this menu.
 * @return string The resulting page's hook_suffix.
 */
function add_utility_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $icon_url = '') {
	_deprecated_function( __FUNCTION__, '4.5.0', 'add_menu_page()' );

	global $_wp_last_utility_menu;

	$_wp_last_utility_menu++;

	return add_menu_page($page_title, $menu_title, $capability, $menu_slug, $callback, $icon_url, $_wp_last_utility_menu);
}

/**
 * Disables autocomplete on the 'post' form (Add/Edit Post screens) for WebKit browsers,
 * as they disregard the autocomplete setting on the editor textarea. That can break the editor
 * when the user navigates to it with the browser's Back button. See #28037
 *
 * Replaced with wp_page_reload_on_back_button_js() that also fixes this problem.
 *
 * @since 4.0.0
 * @deprecated 4.6.0
 *
 * @link https://core.trac.wordpress.org/ticket/35852
 *
 * @global bool $is_safari
 * @global bool $is_chrome
 */
function post_form_autocomplete_off() {
	global $is_safari, $is_chrome;

	_deprecated_function( __FUNCTION__, '4.6.0' );

	if ( $is_safari || $is_chrome ) {
		echo ' autocomplete="off"';
	}
}

/**
 * Display JavaScript on the page.
 *
 * @since 3.5.0
 * @deprecated 4.9.0
 */
function options_permalink_add_js() {
	?>
	<script type="text/javascript">
		jQuery( function() {
			jQuery('.permalink-structure input:radio').change(function() {
				if ( 'custom' == this.value )
					return;
				jQuery('#permalink_structure').val( this.value );
			});
			jQuery( '#permalink_structure' ).on( 'click input', function() {
				jQuery( '#custom_selection' ).prop( 'checked', true );
			});
		} );
	</script>
	<?php
}

/**
 * Previous class for list table for privacy data export requests.
 *
 * @since 4.9.6
 * @deprecated 5.3.0
 */
class WP_Privacy_Data_Export_Requests_Table extends WP_Privacy_Data_Export_Requests_List_Table {
	function __construct( $args ) {
		_deprecated_function( __CLASS__, '5.3.0', 'WP_Privacy_Data_Export_Requests_List_Table' );

		if ( ! isset( $args['screen'] ) || $args['screen'] === 'export_personal_data' ) {
			$args['screen'] = 'export-personal-data';
		}

		parent::__construct( $args );
	}
}

/**
 * Previous class for list table for privacy data erasure requests.
 *
 * @since 4.9.6
 * @deprecated 5.3.0
 */
class WP_Privacy_Data_Removal_Requests_Table extends WP_Privacy_Data_Removal_Requests_List_Table {
	function __construct( $args ) {
		_deprecated_function( __CLASS__, '5.3.0', 'WP_Privacy_Data_Removal_Requests_List_Table' );

		if ( ! isset( $args['screen'] ) || $args['screen'] === 'remove_personal_data' ) {
			$args['screen'] = 'erase-personal-data';
		}

		parent::__construct( $args );
	}
}

/**
 * Was used to add options for the privacy requests screens before they were separate files.
 *
 * @since 4.9.8
 * @access private
 * @deprecated 5.3.0
 */
function _wp_privacy_requests_screen_options() {
	_deprecated_function( __FUNCTION__, '5.3.0' );
}

/**
 * Was used to filter input from media_upload_form_handler() and to assign a default
 * post_title from the file name if none supplied.
 *
 * @since 2.5.0
 * @deprecated 6.0.0
 *
 * @param array $post       The WP_Post attachment object converted to an array.
 * @param array $attachment An array of attachment metadata.
 * @return array Attachment post object converted to an array.
 */
function image_attachment_fields_to_save( $post, $attachment ) {
	_deprecated_function( __FUNCTION__, '6.0.0' );

	return $post;
}
bookmark.php000064400000026715151327343360007104 0ustar00<?php
/**
 * WordPress Bookmark Administration API
 *
 * @package WordPress
 * @subpackage Administration
 */

/**
 * Adds a link using values provided in $_POST.
 *
 * @since 2.0.0
 *
 * @return int|WP_Error Value 0 or WP_Error on failure. The link ID on success.
 */
function add_link() {
	return edit_link();
}

/**
 * Updates or inserts a link using values provided in $_POST.
 *
 * @since 2.0.0
 *
 * @param int $link_id Optional. ID of the link to edit. Default 0.
 * @return int|WP_Error Value 0 or WP_Error on failure. The link ID on success.
 */
function edit_link( $link_id = 0 ) {
	if ( ! current_user_can( 'manage_links' ) ) {
		wp_die(
			'<h1>' . __( 'You need a higher level of permission.' ) . '</h1>' .
			'<p>' . __( 'Sorry, you are not allowed to edit the links for this site.' ) . '</p>',
			403
		);
	}

	$_POST['link_url']   = esc_url( $_POST['link_url'] );
	$_POST['link_name']  = esc_html( $_POST['link_name'] );
	$_POST['link_image'] = esc_html( $_POST['link_image'] );
	$_POST['link_rss']   = esc_url( $_POST['link_rss'] );
	if ( ! isset( $_POST['link_visible'] ) || 'N' !== $_POST['link_visible'] ) {
		$_POST['link_visible'] = 'Y';
	}

	if ( ! empty( $link_id ) ) {
		$_POST['link_id'] = $link_id;
		return wp_update_link( $_POST );
	} else {
		return wp_insert_link( $_POST );
	}
}

/**
 * Retrieves the default link for editing.
 *
 * @since 2.0.0
 *
 * @return stdClass Default link object.
 */
function get_default_link_to_edit() {
	$link = new stdClass();
	if ( isset( $_GET['linkurl'] ) ) {
		$link->link_url = esc_url( wp_unslash( $_GET['linkurl'] ) );
	} else {
		$link->link_url = '';
	}

	if ( isset( $_GET['name'] ) ) {
		$link->link_name = esc_attr( wp_unslash( $_GET['name'] ) );
	} else {
		$link->link_name = '';
	}

	$link->link_visible = 'Y';

	return $link;
}

/**
 * Deletes a specified link from the database.
 *
 * @since 2.0.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param int $link_id ID of the link to delete.
 * @return true Always true.
 */
function wp_delete_link( $link_id ) {
	global $wpdb;
	/**
	 * Fires before a link is deleted.
	 *
	 * @since 2.0.0
	 *
	 * @param int $link_id ID of the link to delete.
	 */
	do_action( 'delete_link', $link_id );

	wp_delete_object_term_relationships( $link_id, 'link_category' );

	$wpdb->delete( $wpdb->links, array( 'link_id' => $link_id ) );

	/**
	 * Fires after a link has been deleted.
	 *
	 * @since 2.2.0
	 *
	 * @param int $link_id ID of the deleted link.
	 */
	do_action( 'deleted_link', $link_id );

	clean_bookmark_cache( $link_id );

	return true;
}

/**
 * Retrieves the link category IDs associated with the link specified.
 *
 * @since 2.1.0
 *
 * @param int $link_id Link ID to look up.
 * @return int[] The IDs of the requested link's categories.
 */
function wp_get_link_cats( $link_id = 0 ) {
	$cats = wp_get_object_terms( $link_id, 'link_category', array( 'fields' => 'ids' ) );
	return array_unique( $cats );
}

/**
 * Retrieves link data based on its ID.
 *
 * @since 2.0.0
 *
 * @param int|stdClass $link Link ID or object to retrieve.
 * @return object Link object for editing.
 */
function get_link_to_edit( $link ) {
	return get_bookmark( $link, OBJECT, 'edit' );
}

/**
 * Inserts a link into the database, or updates an existing link.
 *
 * Runs all the necessary sanitizing, provides default values if arguments are missing,
 * and finally saves the link.
 *
 * @since 2.0.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param array $linkdata {
 *     Elements that make up the link to insert.
 *
 *     @type int    $link_id          Optional. The ID of the existing link if updating.
 *     @type string $link_url         The URL the link points to.
 *     @type string $link_name        The title of the link.
 *     @type string $link_image       Optional. A URL of an image.
 *     @type string $link_target      Optional. The target element for the anchor tag.
 *     @type string $link_description Optional. A short description of the link.
 *     @type string $link_visible     Optional. 'Y' means visible, anything else means not.
 *     @type int    $link_owner       Optional. A user ID.
 *     @type int    $link_rating      Optional. A rating for the link.
 *     @type string $link_rel         Optional. A relationship of the link to you.
 *     @type string $link_notes       Optional. An extended description of or notes on the link.
 *     @type string $link_rss         Optional. A URL of an associated RSS feed.
 *     @type int    $link_category    Optional. The term ID of the link category.
 *                                    If empty, uses default link category.
 * }
 * @param bool  $wp_error Optional. Whether to return a WP_Error object on failure. Default false.
 * @return int|WP_Error Value 0 or WP_Error on failure. The link ID on success.
 */
function wp_insert_link( $linkdata, $wp_error = false ) {
	global $wpdb;

	$defaults = array(
		'link_id'     => 0,
		'link_name'   => '',
		'link_url'    => '',
		'link_rating' => 0,
	);

	$parsed_args = wp_parse_args( $linkdata, $defaults );
	$parsed_args = wp_unslash( sanitize_bookmark( $parsed_args, 'db' ) );

	$link_id   = $parsed_args['link_id'];
	$link_name = $parsed_args['link_name'];
	$link_url  = $parsed_args['link_url'];

	$update = false;
	if ( ! empty( $link_id ) ) {
		$update = true;
	}

	if ( '' === trim( $link_name ) ) {
		if ( '' !== trim( $link_url ) ) {
			$link_name = $link_url;
		} else {
			return 0;
		}
	}

	if ( '' === trim( $link_url ) ) {
		return 0;
	}

	$link_rating      = ( ! empty( $parsed_args['link_rating'] ) ) ? $parsed_args['link_rating'] : 0;
	$link_image       = ( ! empty( $parsed_args['link_image'] ) ) ? $parsed_args['link_image'] : '';
	$link_target      = ( ! empty( $parsed_args['link_target'] ) ) ? $parsed_args['link_target'] : '';
	$link_visible     = ( ! empty( $parsed_args['link_visible'] ) ) ? $parsed_args['link_visible'] : 'Y';
	$link_owner       = ( ! empty( $parsed_args['link_owner'] ) ) ? $parsed_args['link_owner'] : get_current_user_id();
	$link_notes       = ( ! empty( $parsed_args['link_notes'] ) ) ? $parsed_args['link_notes'] : '';
	$link_description = ( ! empty( $parsed_args['link_description'] ) ) ? $parsed_args['link_description'] : '';
	$link_rss         = ( ! empty( $parsed_args['link_rss'] ) ) ? $parsed_args['link_rss'] : '';
	$link_rel         = ( ! empty( $parsed_args['link_rel'] ) ) ? $parsed_args['link_rel'] : '';
	$link_category    = ( ! empty( $parsed_args['link_category'] ) ) ? $parsed_args['link_category'] : array();
	$link_updated     = gmdate( 'Y-m-d H:i:s', current_time( 'timestamp', 0 ) );

	// Make sure we set a valid category.
	if ( ! is_array( $link_category ) || 0 === count( $link_category ) ) {
		$link_category = array( get_option( 'default_link_category' ) );
	}

	if ( $update ) {
		if ( false === $wpdb->update( $wpdb->links, compact( 'link_url', 'link_name', 'link_image', 'link_target', 'link_description', 'link_visible', 'link_owner', 'link_rating', 'link_rel', 'link_notes', 'link_rss', 'link_updated' ), compact( 'link_id' ) ) ) {
			if ( $wp_error ) {
				return new WP_Error( 'db_update_error', __( 'Could not update link in the database.' ), $wpdb->last_error );
			} else {
				return 0;
			}
		}
	} else {
		if ( false === $wpdb->insert( $wpdb->links, compact( 'link_url', 'link_name', 'link_image', 'link_target', 'link_description', 'link_visible', 'link_owner', 'link_rating', 'link_rel', 'link_notes', 'link_rss', 'link_updated' ) ) ) {
			if ( $wp_error ) {
				return new WP_Error( 'db_insert_error', __( 'Could not insert link into the database.' ), $wpdb->last_error );
			} else {
				return 0;
			}
		}
		$link_id = (int) $wpdb->insert_id;
	}

	wp_set_link_cats( $link_id, $link_category );

	if ( $update ) {
		/**
		 * Fires after a link was updated in the database.
		 *
		 * @since 2.0.0
		 *
		 * @param int $link_id ID of the link that was updated.
		 */
		do_action( 'edit_link', $link_id );
	} else {
		/**
		 * Fires after a link was added to the database.
		 *
		 * @since 2.0.0
		 *
		 * @param int $link_id ID of the link that was added.
		 */
		do_action( 'add_link', $link_id );
	}
	clean_bookmark_cache( $link_id );

	return $link_id;
}

/**
 * Updates link with the specified link categories.
 *
 * @since 2.1.0
 *
 * @param int   $link_id         ID of the link to update.
 * @param int[] $link_categories Array of link category IDs to add the link to.
 */
function wp_set_link_cats( $link_id = 0, $link_categories = array() ) {
	// If $link_categories isn't already an array, make it one:
	if ( ! is_array( $link_categories ) || 0 === count( $link_categories ) ) {
		$link_categories = array( get_option( 'default_link_category' ) );
	}

	$link_categories = array_map( 'intval', $link_categories );
	$link_categories = array_unique( $link_categories );

	wp_set_object_terms( $link_id, $link_categories, 'link_category' );

	clean_bookmark_cache( $link_id );
}

/**
 * Updates a link in the database.
 *
 * @since 2.0.0
 *
 * @param array $linkdata Link data to update. See wp_insert_link() for accepted arguments.
 * @return int|WP_Error Value 0 or WP_Error on failure. The updated link ID on success.
 */
function wp_update_link( $linkdata ) {
	$link_id = (int) $linkdata['link_id'];

	$link = get_bookmark( $link_id, ARRAY_A );

	// Escape data pulled from DB.
	$link = wp_slash( $link );

	// Passed link category list overwrites existing category list if not empty.
	if ( isset( $linkdata['link_category'] ) && is_array( $linkdata['link_category'] )
		&& count( $linkdata['link_category'] ) > 0
	) {
		$link_cats = $linkdata['link_category'];
	} else {
		$link_cats = $link['link_category'];
	}

	// Merge old and new fields with new fields overwriting old ones.
	$linkdata                  = array_merge( $link, $linkdata );
	$linkdata['link_category'] = $link_cats;

	return wp_insert_link( $linkdata );
}

/**
 * Outputs the 'disabled' message for the WordPress Link Manager.
 *
 * @since 3.5.0
 * @access private
 *
 * @global string $pagenow The filename of the current screen.
 */
function wp_link_manager_disabled_message() {
	global $pagenow;

	if ( ! in_array( $pagenow, array( 'link-manager.php', 'link-add.php', 'link.php' ), true ) ) {
		return;
	}

	add_filter( 'pre_option_link_manager_enabled', '__return_true', 100 );
	$really_can_manage_links = current_user_can( 'manage_links' );
	remove_filter( 'pre_option_link_manager_enabled', '__return_true', 100 );

	if ( $really_can_manage_links ) {
		$plugins = get_plugins();

		if ( empty( $plugins['link-manager/link-manager.php'] ) ) {
			if ( current_user_can( 'install_plugins' ) ) {
				$install_url = wp_nonce_url(
					self_admin_url( 'update.php?action=install-plugin&plugin=link-manager' ),
					'install-plugin_link-manager'
				);

				wp_die(
					sprintf(
						/* translators: %s: A link to install the Link Manager plugin. */
						__( 'If you are looking to use the link manager, please install the <a href="%s">Link Manager plugin</a>.' ),
						esc_url( $install_url )
					)
				);
			}
		} elseif ( is_plugin_inactive( 'link-manager/link-manager.php' ) ) {
			if ( current_user_can( 'activate_plugins' ) ) {
				$activate_url = wp_nonce_url(
					self_admin_url( 'plugins.php?action=activate&plugin=link-manager/link-manager.php' ),
					'activate-plugin_link-manager/link-manager.php'
				);

				wp_die(
					sprintf(
						/* translators: %s: A link to activate the Link Manager plugin. */
						__( 'Please activate the <a href="%s">Link Manager plugin</a> to use the link manager.' ),
						esc_url( $activate_url )
					)
				);
			}
		}
	}

	wp_die( __( 'Sorry, you are not allowed to edit the links for this site.' ) );
}
ms-deprecated.php000060400000007272151327343360010005 0ustar00<?php
/**
 * Multisite: Deprecated admin functions from past versions and WordPress MU
 *
 * These functions should not be used and will be removed in a later version.
 * It is suggested to use for the alternatives instead when available.
 *
 * @package WordPress
 * @subpackage Deprecated
 * @since 3.0.0
 */

/**
 * Outputs the WPMU menu.
 *
 * @deprecated 3.0.0
 */
function wpmu_menu() {
	_deprecated_function( __FUNCTION__, '3.0.0' );
	// Deprecated. See #11763.
}

/**
 * Determines if the available space defined by the admin has been exceeded by the user.
 *
 * @deprecated 3.0.0 Use is_upload_space_available()
 * @see is_upload_space_available()
 */
function wpmu_checkAvailableSpace() {
	_deprecated_function( __FUNCTION__, '3.0.0', 'is_upload_space_available()' );

	if ( ! is_upload_space_available() ) {
		wp_die( sprintf(
			/* translators: %s: Allowed space allocation. */
			__( 'Sorry, you have used your space allocation of %s. Please delete some files to upload more files.' ),
			size_format( get_space_allowed() * MB_IN_BYTES )
		) );
	}
}

/**
 * WPMU options.
 *
 * @deprecated 3.0.0
 */
function mu_options( $options ) {
	_deprecated_function( __FUNCTION__, '3.0.0' );
	return $options;
}

/**
 * Deprecated functionality for activating a network-only plugin.
 *
 * @deprecated 3.0.0 Use activate_plugin()
 * @see activate_plugin()
 */
function activate_sitewide_plugin() {
	_deprecated_function( __FUNCTION__, '3.0.0', 'activate_plugin()' );
	return false;
}

/**
 * Deprecated functionality for deactivating a network-only plugin.
 *
 * @deprecated 3.0.0 Use deactivate_plugin()
 * @see deactivate_plugin()
 */
function deactivate_sitewide_plugin( $plugin = false ) {
	_deprecated_function( __FUNCTION__, '3.0.0', 'deactivate_plugin()' );
}

/**
 * Deprecated functionality for determining if the current plugin is network-only.
 *
 * @deprecated 3.0.0 Use is_network_only_plugin()
 * @see is_network_only_plugin()
 */
function is_wpmu_sitewide_plugin( $file ) {
	_deprecated_function( __FUNCTION__, '3.0.0', 'is_network_only_plugin()' );
	return is_network_only_plugin( $file );
}

/**
 * Deprecated functionality for getting themes network-enabled themes.
 *
 * @deprecated 3.4.0 Use WP_Theme::get_allowed_on_network()
 * @see WP_Theme::get_allowed_on_network()
 */
function get_site_allowed_themes() {
	_deprecated_function( __FUNCTION__, '3.4.0', 'WP_Theme::get_allowed_on_network()' );
	return array_map( 'intval', WP_Theme::get_allowed_on_network() );
}

/**
 * Deprecated functionality for getting themes allowed on a specific site.
 *
 * @deprecated 3.4.0 Use WP_Theme::get_allowed_on_site()
 * @see WP_Theme::get_allowed_on_site()
 */
function wpmu_get_blog_allowedthemes( $blog_id = 0 ) {
	_deprecated_function( __FUNCTION__, '3.4.0', 'WP_Theme::get_allowed_on_site()' );
	return array_map( 'intval', WP_Theme::get_allowed_on_site( $blog_id ) );
}

/**
 * Deprecated functionality for determining whether a file is deprecated.
 *
 * @deprecated 3.5.0
 */
function ms_deprecated_blogs_file() {}

if ( ! function_exists( 'install_global_terms' ) ) :
	/**
	 * Install global terms.
	 *
	 * @since 3.0.0
	 * @since 6.1.0 This function no longer does anything.
	 * @deprecated 6.1.0
	 */
	function install_global_terms() {
		_deprecated_function( __FUNCTION__, '6.1.0' );
	}
endif;

/**
 * Synchronizes category and post tag slugs when global terms are enabled.
 *
 * @since 3.0.0
 * @since 6.1.0 This function no longer does anything.
 * @deprecated 6.1.0
 *
 * @param WP_Term|array $term     The term.
 * @param string        $taxonomy The taxonomy for `$term`.
 * @return WP_Term|array Always returns `$term`.
 */
function sync_category_tag_slugs( $term, $taxonomy ) {
	_deprecated_function( __FUNCTION__, '6.1.0' );

	return $term;
}
class-bulk-theme-upgrader-skin.php000060400000005144151327343360013175 0ustar00<?php
/**
 * Upgrader API: Bulk_Plugin_Upgrader_Skin class
 *
 * @package WordPress
 * @subpackage Upgrader
 * @since 4.6.0
 */

/**
 * Bulk Theme Upgrader Skin for WordPress Theme Upgrades.
 *
 * @since 3.0.0
 * @since 4.6.0 Moved to its own file from wp-admin/includes/class-wp-upgrader-skins.php.
 *
 * @see Bulk_Upgrader_Skin
 */
class Bulk_Theme_Upgrader_Skin extends Bulk_Upgrader_Skin {

	/**
	 * Theme info.
	 *
	 * The Theme_Upgrader::bulk_upgrade() method will fill this in
	 * with info retrieved from the Theme_Upgrader::theme_info() method,
	 * which in turn calls the wp_get_theme() function.
	 *
	 * @since 3.0.0
	 * @var WP_Theme|false The theme's info object, or false.
	 */
	public $theme_info = false;

	/**
	 * Sets up the strings used in the update process.
	 *
	 * @since 3.0.0
	 */
	public function add_strings() {
		parent::add_strings();
		/* translators: 1: Theme name, 2: Number of the theme, 3: Total number of themes being updated. */
		$this->upgrader->strings['skin_before_update_header'] = __( 'Updating Theme %1$s (%2$d/%3$d)' );
	}

	/**
	 * Performs an action before a bulk theme update.
	 *
	 * @since 3.0.0
	 *
	 * @param string $title
	 */
	public function before( $title = '' ) {
		parent::before( $this->theme_info->display( 'Name' ) );
	}

	/**
	 * Performs an action following a bulk theme update.
	 *
	 * @since 3.0.0
	 *
	 * @param string $title
	 */
	public function after( $title = '' ) {
		parent::after( $this->theme_info->display( 'Name' ) );
		$this->decrement_update_count( 'theme' );
	}

	/**
	 * Displays the footer following the bulk update process.
	 *
	 * @since 3.0.0
	 */
	public function bulk_footer() {
		parent::bulk_footer();

		$update_actions = array(
			'themes_page'  => sprintf(
				'<a href="%s" target="_parent">%s</a>',
				self_admin_url( 'themes.php' ),
				__( 'Go to Themes page' )
			),
			'updates_page' => sprintf(
				'<a href="%s" target="_parent">%s</a>',
				self_admin_url( 'update-core.php' ),
				__( 'Go to WordPress Updates page' )
			),
		);

		if ( ! current_user_can( 'switch_themes' ) && ! current_user_can( 'edit_theme_options' ) ) {
			unset( $update_actions['themes_page'] );
		}

		/**
		 * Filters the list of action links available following bulk theme updates.
		 *
		 * @since 3.0.0
		 *
		 * @param string[] $update_actions Array of theme action links.
		 * @param WP_Theme $theme_info     Theme object for the last-updated theme.
		 */
		$update_actions = apply_filters( 'update_bulk_theme_complete_actions', $update_actions, $this->theme_info );

		if ( ! empty( $update_actions ) ) {
			$this->feedback( implode( ' | ', (array) $update_actions ) );
		}
	}
}
class-wp-post-comments-list-table.php000060400000002655151327343360013665 0ustar00<?php
/**
 * List Table API: WP_Post_Comments_List_Table class
 *
 * @package WordPress
 * @subpackage Administration
 * @since 4.4.0
 */

/**
 * Core class used to implement displaying post comments in a list table.
 *
 * @since 3.1.0
 *
 * @see WP_Comments_List_Table
 */
class WP_Post_Comments_List_Table extends WP_Comments_List_Table {

	/**
	 * @return array
	 */
	protected function get_column_info() {
		return array(
			array(
				'author'  => __( 'Author' ),
				'comment' => _x( 'Comment', 'column name' ),
			),
			array(),
			array(),
			'comment',
		);
	}

	/**
	 * @return array
	 */
	protected function get_table_classes() {
		$classes   = parent::get_table_classes();
		$classes[] = 'wp-list-table';
		$classes[] = 'comments-box';
		return $classes;
	}

	/**
	 * @param bool $output_empty
	 */
	public function display( $output_empty = false ) {
		$singular = $this->_args['singular'];

		wp_nonce_field( 'fetch-list-' . get_class( $this ), '_ajax_fetch_list_nonce' );
		?>
<table class="<?php echo implode( ' ', $this->get_table_classes() ); ?>" style="display:none;">
	<tbody id="the-comment-list"
		<?php
		if ( $singular ) {
			echo " data-wp-lists='list:$singular'";
		}
		?>
		>
		<?php
		if ( ! $output_empty ) {
			$this->display_rows_or_placeholder();
		}
		?>
	</tbody>
</table>
		<?php
	}

	/**
	 * @param bool $comment_status
	 * @return int
	 */
	public function get_per_page( $comment_status = false ) {
		return 10;
	}
}
class-automatic-upgrader-skin.php000060400000007117151327343360013130 0ustar00<?php
/**
 * Upgrader API: Automatic_Upgrader_Skin class
 *
 * @package WordPress
 * @subpackage Upgrader
 * @since 4.6.0
 */

/**
 * Upgrader Skin for Automatic WordPress Upgrades.
 *
 * This skin is designed to be used when no output is intended, all output
 * is captured and stored for the caller to process and log/email/discard.
 *
 * @since 3.7.0
 * @since 4.6.0 Moved to its own file from wp-admin/includes/class-wp-upgrader-skins.php.
 *
 * @see Bulk_Upgrader_Skin
 */
class Automatic_Upgrader_Skin extends WP_Upgrader_Skin {
	protected $messages = array();

	/**
	 * Determines whether the upgrader needs FTP/SSH details in order to connect
	 * to the filesystem.
	 *
	 * @since 3.7.0
	 * @since 4.6.0 The `$context` parameter default changed from `false` to an empty string.
	 *
	 * @see request_filesystem_credentials()
	 *
	 * @param bool|WP_Error $error                        Optional. Whether the current request has failed to connect,
	 *                                                    or an error object. Default false.
	 * @param string        $context                      Optional. Full path to the directory that is tested
	 *                                                    for being writable. Default empty.
	 * @param bool          $allow_relaxed_file_ownership Optional. Whether to allow Group/World writable. Default false.
	 * @return bool True on success, false on failure.
	 */
	public function request_filesystem_credentials( $error = false, $context = '', $allow_relaxed_file_ownership = false ) {
		if ( $context ) {
			$this->options['context'] = $context;
		}
		/*
		 * TODO: Fix up request_filesystem_credentials(), or split it, to allow us to request a no-output version.
		 * This will output a credentials form in event of failure. We don't want that, so just hide with a buffer.
		 */
		ob_start();
		$result = parent::request_filesystem_credentials( $error, $context, $allow_relaxed_file_ownership );
		ob_end_clean();
		return $result;
	}

	/**
	 * Retrieves the upgrade messages.
	 *
	 * @since 3.7.0
	 *
	 * @return string[] Messages during an upgrade.
	 */
	public function get_upgrade_messages() {
		return $this->messages;
	}

	/**
	 * Stores a message about the upgrade.
	 *
	 * @since 3.7.0
	 * @since 5.9.0 Renamed `$data` to `$feedback` for PHP 8 named parameter support.
	 *
	 * @param string|array|WP_Error $feedback Message data.
	 * @param mixed                 ...$args  Optional text replacements.
	 */
	public function feedback( $feedback, ...$args ) {
		if ( is_wp_error( $feedback ) ) {
			$string = $feedback->get_error_message();
		} elseif ( is_array( $feedback ) ) {
			return;
		} else {
			$string = $feedback;
		}

		if ( ! empty( $this->upgrader->strings[ $string ] ) ) {
			$string = $this->upgrader->strings[ $string ];
		}

		if ( str_contains( $string, '%' ) ) {
			if ( ! empty( $args ) ) {
				$string = vsprintf( $string, $args );
			}
		}

		$string = trim( $string );

		// Only allow basic HTML in the messages, as it'll be used in emails/logs rather than direct browser output.
		$string = wp_kses(
			$string,
			array(
				'a'      => array(
					'href' => true,
				),
				'br'     => true,
				'em'     => true,
				'strong' => true,
			)
		);

		if ( empty( $string ) ) {
			return;
		}

		$this->messages[] = $string;
	}

	/**
	 * Creates a new output buffer.
	 *
	 * @since 3.7.0
	 */
	public function header() {
		ob_start();
	}

	/**
	 * Retrieves the buffered content, deletes the buffer, and processes the output.
	 *
	 * @since 3.7.0
	 */
	public function footer() {
		$output = ob_get_clean();
		if ( ! empty( $output ) ) {
			$this->feedback( $output );
		}
	}
}
list-table.php000060400000007332151327343360007325 0ustar00<?php
/**
 * Helper functions for displaying a list of items in an ajaxified HTML table.
 *
 * @package WordPress
 * @subpackage List_Table
 * @since 3.1.0
 */

/**
 * Fetches an instance of a WP_List_Table class.
 *
 * @since 3.1.0
 *
 * @global string $hook_suffix
 *
 * @param string $class_name The type of the list table, which is the class name.
 * @param array  $args       Optional. Arguments to pass to the class. Accepts 'screen'.
 * @return WP_List_Table|false List table object on success, false if the class does not exist.
 */
function _get_list_table( $class_name, $args = array() ) {
	$core_classes = array(
		// Site Admin.
		'WP_Posts_List_Table'                         => 'posts',
		'WP_Media_List_Table'                         => 'media',
		'WP_Terms_List_Table'                         => 'terms',
		'WP_Users_List_Table'                         => 'users',
		'WP_Comments_List_Table'                      => 'comments',
		'WP_Post_Comments_List_Table'                 => array( 'comments', 'post-comments' ),
		'WP_Links_List_Table'                         => 'links',
		'WP_Plugin_Install_List_Table'                => 'plugin-install',
		'WP_Themes_List_Table'                        => 'themes',
		'WP_Theme_Install_List_Table'                 => array( 'themes', 'theme-install' ),
		'WP_Plugins_List_Table'                       => 'plugins',
		'WP_Application_Passwords_List_Table'         => 'application-passwords',

		// Network Admin.
		'WP_MS_Sites_List_Table'                      => 'ms-sites',
		'WP_MS_Users_List_Table'                      => 'ms-users',
		'WP_MS_Themes_List_Table'                     => 'ms-themes',

		// Privacy requests tables.
		'WP_Privacy_Data_Export_Requests_List_Table'  => 'privacy-data-export-requests',
		'WP_Privacy_Data_Removal_Requests_List_Table' => 'privacy-data-removal-requests',
	);

	if ( isset( $core_classes[ $class_name ] ) ) {
		foreach ( (array) $core_classes[ $class_name ] as $required ) {
			require_once ABSPATH . 'wp-admin/includes/class-wp-' . $required . '-list-table.php';
		}

		if ( isset( $args['screen'] ) ) {
			$args['screen'] = convert_to_screen( $args['screen'] );
		} elseif ( isset( $GLOBALS['hook_suffix'] ) ) {
			$args['screen'] = get_current_screen();
		} else {
			$args['screen'] = null;
		}

		/**
		 * Filters the list table class to instantiate.
		 *
		 * @since 6.1.0
		 *
		 * @param string $class_name The list table class to use.
		 * @param array  $args       An array containing _get_list_table() arguments.
		 */
		$custom_class_name = apply_filters( 'wp_list_table_class_name', $class_name, $args );

		if ( is_string( $custom_class_name ) && class_exists( $custom_class_name ) ) {
			$class_name = $custom_class_name;
		}

		return new $class_name( $args );
	}

	return false;
}

/**
 * Register column headers for a particular screen.
 *
 * @see get_column_headers(), print_column_headers(), get_hidden_columns()
 *
 * @since 2.7.0
 *
 * @param string    $screen The handle for the screen to register column headers for. This is
 *                          usually the hook name returned by the `add_*_page()` functions.
 * @param string[] $columns An array of columns with column IDs as the keys and translated
 *                          column names as the values.
 */
function register_column_headers( $screen, $columns ) {
	new _WP_List_Table_Compat( $screen, $columns );
}

/**
 * Prints column headers for a particular screen.
 *
 * @since 2.7.0
 *
 * @param string|WP_Screen $screen  The screen hook name or screen object.
 * @param bool             $with_id Whether to set the ID attribute or not.
 */
function print_column_headers( $screen, $with_id = true ) {
	$wp_list_table = new _WP_List_Table_Compat( $screen );

	$wp_list_table->print_column_headers( $with_id );
}
admin.php000060400000007054151327343360006356 0ustar00<?php
/**
 * Core Administration API
 *
 * @package WordPress
 * @subpackage Administration
 * @since 2.3.0
 */

if ( ! defined( 'WP_ADMIN' ) ) {
	/*
	 * This file is being included from a file other than wp-admin/admin.php, so
	 * some setup was skipped. Make sure the admin message catalog is loaded since
	 * load_default_textdomain() will not have done so in this context.
	 */
	$admin_locale = get_locale();
	load_textdomain( 'default', WP_LANG_DIR . '/admin-' . $admin_locale . '.mo', $admin_locale );
	unset( $admin_locale );
}

/** WordPress Administration Hooks */
require_once ABSPATH . 'wp-admin/includes/admin-filters.php';

/** WordPress Bookmark Administration API */
require_once ABSPATH . 'wp-admin/includes/bookmark.php';

/** WordPress Comment Administration API */
require_once ABSPATH . 'wp-admin/includes/comment.php';

/** WordPress Administration File API */
require_once ABSPATH . 'wp-admin/includes/file.php';

/** WordPress Image Administration API */
require_once ABSPATH . 'wp-admin/includes/image.php';

/** WordPress Media Administration API */
require_once ABSPATH . 'wp-admin/includes/media.php';

/** WordPress Import Administration API */
require_once ABSPATH . 'wp-admin/includes/import.php';

/** WordPress Misc Administration API */
require_once ABSPATH . 'wp-admin/includes/misc.php';

/** WordPress Misc Administration API */
require_once ABSPATH . 'wp-admin/includes/class-wp-privacy-policy-content.php';

/** WordPress Options Administration API */
require_once ABSPATH . 'wp-admin/includes/options.php';

/** WordPress Plugin Administration API */
require_once ABSPATH . 'wp-admin/includes/plugin.php';

/** WordPress Post Administration API */
require_once ABSPATH . 'wp-admin/includes/post.php';

/** WordPress Administration Screen API */
require_once ABSPATH . 'wp-admin/includes/class-wp-screen.php';
require_once ABSPATH . 'wp-admin/includes/screen.php';

/** WordPress Taxonomy Administration API */
require_once ABSPATH . 'wp-admin/includes/taxonomy.php';

/** WordPress Template Administration API */
require_once ABSPATH . 'wp-admin/includes/template.php';

/** WordPress List Table Administration API and base class */
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table-compat.php';
require_once ABSPATH . 'wp-admin/includes/list-table.php';

/** WordPress Theme Administration API */
require_once ABSPATH . 'wp-admin/includes/theme.php';

/** WordPress Privacy Functions */
require_once ABSPATH . 'wp-admin/includes/privacy-tools.php';

/** WordPress Privacy List Table classes. */
// Previously in wp-admin/includes/user.php. Need to be loaded for backward compatibility.
require_once ABSPATH . 'wp-admin/includes/class-wp-privacy-requests-table.php';
require_once ABSPATH . 'wp-admin/includes/class-wp-privacy-data-export-requests-list-table.php';
require_once ABSPATH . 'wp-admin/includes/class-wp-privacy-data-removal-requests-list-table.php';

/** WordPress User Administration API */
require_once ABSPATH . 'wp-admin/includes/user.php';

/** WordPress Site Icon API */
require_once ABSPATH . 'wp-admin/includes/class-wp-site-icon.php';

/** WordPress Update Administration API */
require_once ABSPATH . 'wp-admin/includes/update.php';

/** WordPress Deprecated Administration API */
require_once ABSPATH . 'wp-admin/includes/deprecated.php';

/** WordPress Multisite support API */
if ( is_multisite() ) {
	require_once ABSPATH . 'wp-admin/includes/ms-admin-filters.php';
	require_once ABSPATH . 'wp-admin/includes/ms.php';
	require_once ABSPATH . 'wp-admin/includes/ms-deprecated.php';
}
image.php000064400000123250151327343360006351 0ustar00<?php
/**
 * File contains all the administration image manipulation functions.
 *
 * @package WordPress
 * @subpackage Administration
 */

/**
 * Crops an image to a given size.
 *
 * @since 2.1.0
 *
 * @param string|int   $src      The source file or Attachment ID.
 * @param int          $src_x    The start x position to crop from.
 * @param int          $src_y    The start y position to crop from.
 * @param int          $src_w    The width to crop.
 * @param int          $src_h    The height to crop.
 * @param int          $dst_w    The destination width.
 * @param int          $dst_h    The destination height.
 * @param bool|false   $src_abs  Optional. If the source crop points are absolute.
 * @param string|false $dst_file Optional. The destination file to write to.
 * @return string|WP_Error New filepath on success, WP_Error on failure.
 */
function wp_crop_image( $src, $src_x, $src_y, $src_w, $src_h, $dst_w, $dst_h, $src_abs = false, $dst_file = false ) {
	$src_file = $src;
	if ( is_numeric( $src ) ) { // Handle int as attachment ID.
		$src_file = get_attached_file( $src );

		if ( ! file_exists( $src_file ) ) {
			/*
			 * If the file doesn't exist, attempt a URL fopen on the src link.
			 * This can occur with certain file replication plugins.
			 */
			$src = _load_image_to_edit_path( $src, 'full' );
		} else {
			$src = $src_file;
		}
	}

	$editor = wp_get_image_editor( $src );
	if ( is_wp_error( $editor ) ) {
		return $editor;
	}

	$src = $editor->crop( $src_x, $src_y, $src_w, $src_h, $dst_w, $dst_h, $src_abs );
	if ( is_wp_error( $src ) ) {
		return $src;
	}

	if ( ! $dst_file ) {
		$dst_file = str_replace( wp_basename( $src_file ), 'cropped-' . wp_basename( $src_file ), $src_file );
	}

	/*
	 * The directory containing the original file may no longer exist when
	 * using a replication plugin.
	 */
	wp_mkdir_p( dirname( $dst_file ) );

	$dst_file = dirname( $dst_file ) . '/' . wp_unique_filename( dirname( $dst_file ), wp_basename( $dst_file ) );

	$result = $editor->save( $dst_file );
	if ( is_wp_error( $result ) ) {
		return $result;
	}

	if ( ! empty( $result['path'] ) ) {
		return $result['path'];
	}

	return $dst_file;
}

/**
 * Compare the existing image sub-sizes (as saved in the attachment meta)
 * to the currently registered image sub-sizes, and return the difference.
 *
 * Registered sub-sizes that are larger than the image are skipped.
 *
 * @since 5.3.0
 *
 * @param int $attachment_id The image attachment post ID.
 * @return array[] Associative array of arrays of image sub-size information for
 *                 missing image sizes, keyed by image size name.
 */
function wp_get_missing_image_subsizes( $attachment_id ) {
	if ( ! wp_attachment_is_image( $attachment_id ) ) {
		return array();
	}

	$registered_sizes = wp_get_registered_image_subsizes();
	$image_meta       = wp_get_attachment_metadata( $attachment_id );

	// Meta error?
	if ( empty( $image_meta ) ) {
		return $registered_sizes;
	}

	// Use the originally uploaded image dimensions as full_width and full_height.
	if ( ! empty( $image_meta['original_image'] ) ) {
		$image_file = wp_get_original_image_path( $attachment_id );
		$imagesize  = wp_getimagesize( $image_file );
	}

	if ( ! empty( $imagesize ) ) {
		$full_width  = $imagesize[0];
		$full_height = $imagesize[1];
	} else {
		$full_width  = (int) $image_meta['width'];
		$full_height = (int) $image_meta['height'];
	}

	$possible_sizes = array();

	// Skip registered sizes that are too large for the uploaded image.
	foreach ( $registered_sizes as $size_name => $size_data ) {
		if ( image_resize_dimensions( $full_width, $full_height, $size_data['width'], $size_data['height'], $size_data['crop'] ) ) {
			$possible_sizes[ $size_name ] = $size_data;
		}
	}

	if ( empty( $image_meta['sizes'] ) ) {
		$image_meta['sizes'] = array();
	}

	/*
	 * Remove sizes that already exist. Only checks for matching "size names".
	 * It is possible that the dimensions for a particular size name have changed.
	 * For example the user has changed the values on the Settings -> Media screen.
	 * However we keep the old sub-sizes with the previous dimensions
	 * as the image may have been used in an older post.
	 */
	$missing_sizes = array_diff_key( $possible_sizes, $image_meta['sizes'] );

	/**
	 * Filters the array of missing image sub-sizes for an uploaded image.
	 *
	 * @since 5.3.0
	 *
	 * @param array[] $missing_sizes Associative array of arrays of image sub-size information for
	 *                               missing image sizes, keyed by image size name.
	 * @param array   $image_meta    The image meta data.
	 * @param int     $attachment_id The image attachment post ID.
	 */
	return apply_filters( 'wp_get_missing_image_subsizes', $missing_sizes, $image_meta, $attachment_id );
}

/**
 * If any of the currently registered image sub-sizes are missing,
 * create them and update the image meta data.
 *
 * @since 5.3.0
 *
 * @param int $attachment_id The image attachment post ID.
 * @return array|WP_Error The updated image meta data array or WP_Error object
 *                        if both the image meta and the attached file are missing.
 */
function wp_update_image_subsizes( $attachment_id ) {
	$image_meta = wp_get_attachment_metadata( $attachment_id );
	$image_file = wp_get_original_image_path( $attachment_id );

	if ( empty( $image_meta ) || ! is_array( $image_meta ) ) {
		/*
		 * Previously failed upload?
		 * If there is an uploaded file, make all sub-sizes and generate all of the attachment meta.
		 */
		if ( ! empty( $image_file ) ) {
			$image_meta = wp_create_image_subsizes( $image_file, $attachment_id );
		} else {
			return new WP_Error( 'invalid_attachment', __( 'The attached file cannot be found.' ) );
		}
	} else {
		$missing_sizes = wp_get_missing_image_subsizes( $attachment_id );

		if ( empty( $missing_sizes ) ) {
			return $image_meta;
		}

		// This also updates the image meta.
		$image_meta = _wp_make_subsizes( $missing_sizes, $image_file, $image_meta, $attachment_id );
	}

	/** This filter is documented in wp-admin/includes/image.php */
	$image_meta = apply_filters( 'wp_generate_attachment_metadata', $image_meta, $attachment_id, 'update' );

	// Save the updated metadata.
	wp_update_attachment_metadata( $attachment_id, $image_meta );

	return $image_meta;
}

/**
 * Updates the attached file and image meta data when the original image was edited.
 *
 * @since 5.3.0
 * @since 6.0.0 The `$filesize` value was added to the returned array.
 * @access private
 *
 * @param array  $saved_data    The data returned from WP_Image_Editor after successfully saving an image.
 * @param string $original_file Path to the original file.
 * @param array  $image_meta    The image meta data.
 * @param int    $attachment_id The attachment post ID.
 * @return array The updated image meta data.
 */
function _wp_image_meta_replace_original( $saved_data, $original_file, $image_meta, $attachment_id ) {
	$new_file = $saved_data['path'];

	// Update the attached file meta.
	update_attached_file( $attachment_id, $new_file );

	// Width and height of the new image.
	$image_meta['width']  = $saved_data['width'];
	$image_meta['height'] = $saved_data['height'];

	// Make the file path relative to the upload dir.
	$image_meta['file'] = _wp_relative_upload_path( $new_file );

	// Add image file size.
	$image_meta['filesize'] = wp_filesize( $new_file );

	// Store the original image file name in image_meta.
	$image_meta['original_image'] = wp_basename( $original_file );

	return $image_meta;
}

/**
 * Creates image sub-sizes, adds the new data to the image meta `sizes` array, and updates the image metadata.
 *
 * Intended for use after an image is uploaded. Saves/updates the image metadata after each
 * sub-size is created. If there was an error, it is added to the returned image metadata array.
 *
 * @since 5.3.0
 *
 * @param string $file          Full path to the image file.
 * @param int    $attachment_id Attachment ID to process.
 * @return array The image attachment meta data.
 */
function wp_create_image_subsizes( $file, $attachment_id ) {
	$imagesize = wp_getimagesize( $file );

	if ( empty( $imagesize ) ) {
		// File is not an image.
		return array();
	}

	// Default image meta.
	$image_meta = array(
		'width'    => $imagesize[0],
		'height'   => $imagesize[1],
		'file'     => _wp_relative_upload_path( $file ),
		'filesize' => wp_filesize( $file ),
		'sizes'    => array(),
	);

	// Fetch additional metadata from EXIF/IPTC.
	$exif_meta = wp_read_image_metadata( $file );

	if ( $exif_meta ) {
		$image_meta['image_meta'] = $exif_meta;
	}

	/**
	 * Filters the "BIG image" threshold value.
	 *
	 * If the original image width or height is above the threshold, it will be scaled down. The threshold is
	 * used as max width and max height. The scaled down image will be used as the largest available size, including
	 * the `_wp_attached_file` post meta value.
	 *
	 * Returning `false` from the filter callback will disable the scaling.
	 *
	 * @since 5.3.0
	 *
	 * @param int    $threshold     The threshold value in pixels. Default 2560.
	 * @param array  $imagesize     {
	 *     Indexed array of the image width and height in pixels.
	 *
	 *     @type int $0 The image width.
	 *     @type int $1 The image height.
	 * }
	 * @param string $file          Full path to the uploaded image file.
	 * @param int    $attachment_id Attachment post ID.
	 */
	$threshold = (int) apply_filters( 'big_image_size_threshold', 2560, $imagesize, $file, $attachment_id );

	/*
	 * If the original image's dimensions are over the threshold,
	 * scale the image and use it as the "full" size.
	 */
	$scale_down = false;
	$convert    = false;

	if ( $threshold && ( $image_meta['width'] > $threshold || $image_meta['height'] > $threshold ) ) {
		// The image will be converted if needed on saving.
		$scale_down = true;
	} else {
		// The image may need to be converted regardless of its dimensions.
		$output_format = wp_get_image_editor_output_format( $file, $imagesize['mime'] );

		if (
			is_array( $output_format ) &&
			array_key_exists( $imagesize['mime'], $output_format ) &&
			$output_format[ $imagesize['mime'] ] !== $imagesize['mime']
		) {
			$convert = true;
		}
	}

	if ( $scale_down || $convert ) {
		$editor = wp_get_image_editor( $file );

		if ( is_wp_error( $editor ) ) {
			// This image cannot be edited.
			return $image_meta;
		}

		if ( $scale_down ) {
			// Resize the image. This will also convet it if needed.
			$resized = $editor->resize( $threshold, $threshold );
		} elseif ( $convert ) {
			// The image will be converted (if possible) when saved.
			$resized = true;
		}

		$rotated = null;

		// If there is EXIF data, rotate according to EXIF Orientation.
		if ( ! is_wp_error( $resized ) && is_array( $exif_meta ) ) {
			$resized = $editor->maybe_exif_rotate();
			$rotated = $resized; // bool true or WP_Error
		}

		if ( ! is_wp_error( $resized ) ) {
			/*
			 * Append "-scaled" to the image file name. It will look like "my_image-scaled.jpg".
			 * This doesn't affect the sub-sizes names as they are generated from the original image (for best quality).
			 */
			if ( $scale_down ) {
				$saved = $editor->save( $editor->generate_filename( 'scaled' ) );
			} elseif ( $convert ) {
				// Pass an empty string to avoid adding a suffix to converted file names.
				$saved = $editor->save( $editor->generate_filename( '' ) );
			} else {
				$saved = $editor->save();
			}

			if ( ! is_wp_error( $saved ) ) {
				$image_meta = _wp_image_meta_replace_original( $saved, $file, $image_meta, $attachment_id );

				// If the image was rotated update the stored EXIF data.
				if ( true === $rotated && ! empty( $image_meta['image_meta']['orientation'] ) ) {
					$image_meta['image_meta']['orientation'] = 1;
				}
			} else {
				// TODO: Log errors.
			}
		} else {
			// TODO: Log errors.
		}
	} elseif ( ! empty( $exif_meta['orientation'] ) && 1 !== (int) $exif_meta['orientation'] ) {
		// Rotate the whole original image if there is EXIF data and "orientation" is not 1.
		$editor = wp_get_image_editor( $file );

		if ( is_wp_error( $editor ) ) {
			// This image cannot be edited.
			return $image_meta;
		}

		// Rotate the image.
		$rotated = $editor->maybe_exif_rotate();

		if ( true === $rotated ) {
			// Append `-rotated` to the image file name.
			$saved = $editor->save( $editor->generate_filename( 'rotated' ) );

			if ( ! is_wp_error( $saved ) ) {
				$image_meta = _wp_image_meta_replace_original( $saved, $file, $image_meta, $attachment_id );

				// Update the stored EXIF data.
				if ( ! empty( $image_meta['image_meta']['orientation'] ) ) {
					$image_meta['image_meta']['orientation'] = 1;
				}
			} else {
				// TODO: Log errors.
			}
		}
	}

	/*
	 * Initial save of the new metadata.
	 * At this point the file was uploaded and moved to the uploads directory
	 * but the image sub-sizes haven't been created yet and the `sizes` array is empty.
	 */
	wp_update_attachment_metadata( $attachment_id, $image_meta );

	$new_sizes = wp_get_registered_image_subsizes();

	/**
	 * Filters the image sizes automatically generated when uploading an image.
	 *
	 * @since 2.9.0
	 * @since 4.4.0 Added the `$image_meta` argument.
	 * @since 5.3.0 Added the `$attachment_id` argument.
	 *
	 * @param array $new_sizes     Associative array of image sizes to be created.
	 * @param array $image_meta    The image meta data: width, height, file, sizes, etc.
	 * @param int   $attachment_id The attachment post ID for the image.
	 */
	$new_sizes = apply_filters( 'intermediate_image_sizes_advanced', $new_sizes, $image_meta, $attachment_id );

	return _wp_make_subsizes( $new_sizes, $file, $image_meta, $attachment_id );
}

/**
 * Low-level function to create image sub-sizes.
 *
 * Updates the image meta after each sub-size is created.
 * Errors are stored in the returned image metadata array.
 *
 * @since 5.3.0
 * @access private
 *
 * @param array  $new_sizes     Array defining what sizes to create.
 * @param string $file          Full path to the image file.
 * @param array  $image_meta    The attachment meta data array.
 * @param int    $attachment_id Attachment ID to process.
 * @return array The attachment meta data with updated `sizes` array. Includes an array of errors encountered while resizing.
 */
function _wp_make_subsizes( $new_sizes, $file, $image_meta, $attachment_id ) {
	if ( empty( $image_meta ) || ! is_array( $image_meta ) ) {
		// Not an image attachment.
		return array();
	}

	// Check if any of the new sizes already exist.
	if ( isset( $image_meta['sizes'] ) && is_array( $image_meta['sizes'] ) ) {
		foreach ( $image_meta['sizes'] as $size_name => $size_meta ) {
			/*
			 * Only checks "size name" so we don't override existing images even if the dimensions
			 * don't match the currently defined size with the same name.
			 * To change the behavior, unset changed/mismatched sizes in the `sizes` array in image meta.
			 */
			if ( array_key_exists( $size_name, $new_sizes ) ) {
				unset( $new_sizes[ $size_name ] );
			}
		}
	} else {
		$image_meta['sizes'] = array();
	}

	if ( empty( $new_sizes ) ) {
		// Nothing to do...
		return $image_meta;
	}

	/*
	 * Sort the image sub-sizes in order of priority when creating them.
	 * This ensures there is an appropriate sub-size the user can access immediately
	 * even when there was an error and not all sub-sizes were created.
	 */
	$priority = array(
		'medium'       => null,
		'large'        => null,
		'thumbnail'    => null,
		'medium_large' => null,
	);

	$new_sizes = array_filter( array_merge( $priority, $new_sizes ) );

	$editor = wp_get_image_editor( $file );

	if ( is_wp_error( $editor ) ) {
		// The image cannot be edited.
		return $image_meta;
	}

	// If stored EXIF data exists, rotate the source image before creating sub-sizes.
	if ( ! empty( $image_meta['image_meta'] ) ) {
		$rotated = $editor->maybe_exif_rotate();

		if ( is_wp_error( $rotated ) ) {
			// TODO: Log errors.
		}
	}

	if ( method_exists( $editor, 'make_subsize' ) ) {
		foreach ( $new_sizes as $new_size_name => $new_size_data ) {
			$new_size_meta = $editor->make_subsize( $new_size_data );

			if ( is_wp_error( $new_size_meta ) ) {
				// TODO: Log errors.
			} else {
				// Save the size meta value.
				$image_meta['sizes'][ $new_size_name ] = $new_size_meta;
				wp_update_attachment_metadata( $attachment_id, $image_meta );
			}
		}
	} else {
		// Fall back to `$editor->multi_resize()`.
		$created_sizes = $editor->multi_resize( $new_sizes );

		if ( ! empty( $created_sizes ) ) {
			$image_meta['sizes'] = array_merge( $image_meta['sizes'], $created_sizes );
			wp_update_attachment_metadata( $attachment_id, $image_meta );
		}
	}

	return $image_meta;
}

/**
 * Copy parent attachment properties to newly cropped image.
 *
 * @since 6.5.0
 *
 * @param string $cropped              Path to the cropped image file.
 * @param int    $parent_attachment_id Parent file Attachment ID.
 * @param string $context              Control calling the function.
 * @return array Properties of attachment.
 */
function wp_copy_parent_attachment_properties( $cropped, $parent_attachment_id, $context = '' ) {
	$parent          = get_post( $parent_attachment_id );
	$parent_url      = wp_get_attachment_url( $parent->ID );
	$parent_basename = wp_basename( $parent_url );
	$url             = str_replace( wp_basename( $parent_url ), wp_basename( $cropped ), $parent_url );

	$size       = wp_getimagesize( $cropped );
	$image_type = $size ? $size['mime'] : 'image/jpeg';

	$sanitized_post_title = sanitize_file_name( $parent->post_title );
	$use_original_title   = (
		( '' !== trim( $parent->post_title ) ) &&
		/*
		 * Check if the original image has a title other than the "filename" default,
		 * meaning the image had a title when originally uploaded or its title was edited.
		 */
		( $parent_basename !== $sanitized_post_title ) &&
		( pathinfo( $parent_basename, PATHINFO_FILENAME ) !== $sanitized_post_title )
	);
	$use_original_description = ( '' !== trim( $parent->post_content ) );

	$attachment = array(
		'post_title'     => $use_original_title ? $parent->post_title : wp_basename( $cropped ),
		'post_content'   => $use_original_description ? $parent->post_content : $url,
		'post_mime_type' => $image_type,
		'guid'           => $url,
		'context'        => $context,
	);

	// Copy the image caption attribute (post_excerpt field) from the original image.
	if ( '' !== trim( $parent->post_excerpt ) ) {
		$attachment['post_excerpt'] = $parent->post_excerpt;
	}

	// Copy the image alt text attribute from the original image.
	if ( '' !== trim( $parent->_wp_attachment_image_alt ) ) {
		$attachment['meta_input'] = array(
			'_wp_attachment_image_alt' => wp_slash( $parent->_wp_attachment_image_alt ),
		);
	}

	$attachment['post_parent'] = $parent_attachment_id;

	return $attachment;
}

/**
 * Generates attachment meta data and create image sub-sizes for images.
 *
 * @since 2.1.0
 * @since 6.0.0 The `$filesize` value was added to the returned array.
 * @since 6.7.0 The 'image/heic' mime type is supported.
 *
 * @param int    $attachment_id Attachment ID to process.
 * @param string $file          Filepath of the attached image.
 * @return array Metadata for attachment.
 */
function wp_generate_attachment_metadata( $attachment_id, $file ) {
	$attachment = get_post( $attachment_id );

	$metadata  = array();
	$support   = false;
	$mime_type = get_post_mime_type( $attachment );

	if ( 'image/heic' === $mime_type || ( preg_match( '!^image/!', $mime_type ) && file_is_displayable_image( $file ) ) ) {
		// Make thumbnails and other intermediate sizes.
		$metadata = wp_create_image_subsizes( $file, $attachment_id );
	} elseif ( wp_attachment_is( 'video', $attachment ) ) {
		$metadata = wp_read_video_metadata( $file );
		$support  = current_theme_supports( 'post-thumbnails', 'attachment:video' ) || post_type_supports( 'attachment:video', 'thumbnail' );
	} elseif ( wp_attachment_is( 'audio', $attachment ) ) {
		$metadata = wp_read_audio_metadata( $file );
		$support  = current_theme_supports( 'post-thumbnails', 'attachment:audio' ) || post_type_supports( 'attachment:audio', 'thumbnail' );
	}

	/*
	 * wp_read_video_metadata() and wp_read_audio_metadata() return `false`
	 * if the attachment does not exist in the local filesystem,
	 * so make sure to convert the value to an array.
	 */
	if ( ! is_array( $metadata ) ) {
		$metadata = array();
	}

	if ( $support && ! empty( $metadata['image']['data'] ) ) {
		// Check for existing cover.
		$hash   = md5( $metadata['image']['data'] );
		$posts  = get_posts(
			array(
				'fields'         => 'ids',
				'post_type'      => 'attachment',
				'post_mime_type' => $metadata['image']['mime'],
				'post_status'    => 'inherit',
				'posts_per_page' => 1,
				'meta_key'       => '_cover_hash',
				'meta_value'     => $hash,
			)
		);
		$exists = reset( $posts );

		if ( ! empty( $exists ) ) {
			update_post_meta( $attachment_id, '_thumbnail_id', $exists );
		} else {
			$ext = '.jpg';
			switch ( $metadata['image']['mime'] ) {
				case 'image/gif':
					$ext = '.gif';
					break;
				case 'image/png':
					$ext = '.png';
					break;
				case 'image/webp':
					$ext = '.webp';
					break;
			}
			$basename = str_replace( '.', '-', wp_basename( $file ) ) . '-image' . $ext;
			$uploaded = wp_upload_bits( $basename, '', $metadata['image']['data'] );
			if ( false === $uploaded['error'] ) {
				$image_attachment = array(
					'post_mime_type' => $metadata['image']['mime'],
					'post_type'      => 'attachment',
					'post_content'   => '',
				);
				/**
				 * Filters the parameters for the attachment thumbnail creation.
				 *
				 * @since 3.9.0
				 *
				 * @param array $image_attachment An array of parameters to create the thumbnail.
				 * @param array $metadata         Current attachment metadata.
				 * @param array $uploaded         {
				 *     Information about the newly-uploaded file.
				 *
				 *     @type string $file  Filename of the newly-uploaded file.
				 *     @type string $url   URL of the uploaded file.
				 *     @type string $type  File type.
				 * }
				 */
				$image_attachment = apply_filters( 'attachment_thumbnail_args', $image_attachment, $metadata, $uploaded );

				$sub_attachment_id = wp_insert_attachment( $image_attachment, $uploaded['file'] );
				add_post_meta( $sub_attachment_id, '_cover_hash', $hash );
				$attach_data = wp_generate_attachment_metadata( $sub_attachment_id, $uploaded['file'] );
				wp_update_attachment_metadata( $sub_attachment_id, $attach_data );
				update_post_meta( $attachment_id, '_thumbnail_id', $sub_attachment_id );
			}
		}
	} elseif ( 'application/pdf' === $mime_type ) {
		// Try to create image thumbnails for PDFs.

		$fallback_sizes = array(
			'thumbnail',
			'medium',
			'large',
		);

		/**
		 * Filters the image sizes generated for non-image mime types.
		 *
		 * @since 4.7.0
		 *
		 * @param string[] $fallback_sizes An array of image size names.
		 * @param array    $metadata       Current attachment metadata.
		 */
		$fallback_sizes = apply_filters( 'fallback_intermediate_image_sizes', $fallback_sizes, $metadata );

		$registered_sizes = wp_get_registered_image_subsizes();
		$merged_sizes     = array_intersect_key( $registered_sizes, array_flip( $fallback_sizes ) );

		// Force thumbnails to be soft crops.
		if ( isset( $merged_sizes['thumbnail'] ) && is_array( $merged_sizes['thumbnail'] ) ) {
			$merged_sizes['thumbnail']['crop'] = false;
		}

		// Only load PDFs in an image editor if we're processing sizes.
		if ( ! empty( $merged_sizes ) ) {
			$editor = wp_get_image_editor( $file );

			if ( ! is_wp_error( $editor ) ) { // No support for this type of file.
				/*
				 * PDFs may have the same file filename as JPEGs.
				 * Ensure the PDF preview image does not overwrite any JPEG images that already exist.
				 */
				$dirname      = dirname( $file ) . '/';
				$ext          = '.' . pathinfo( $file, PATHINFO_EXTENSION );
				$preview_file = $dirname . wp_unique_filename( $dirname, wp_basename( $file, $ext ) . '-pdf.jpg' );

				$uploaded = $editor->save( $preview_file, 'image/jpeg' );
				unset( $editor );

				// Resize based on the full size image, rather than the source.
				if ( ! is_wp_error( $uploaded ) ) {
					$image_file = $uploaded['path'];
					unset( $uploaded['path'] );

					$metadata['sizes'] = array(
						'full' => $uploaded,
					);

					// Save the meta data before any image post-processing errors could happen.
					wp_update_attachment_metadata( $attachment_id, $metadata );

					// Create sub-sizes saving the image meta after each.
					$metadata = _wp_make_subsizes( $merged_sizes, $image_file, $metadata, $attachment_id );
				}
			}
		}
	}

	// Remove the blob of binary data from the array.
	unset( $metadata['image']['data'] );

	// Capture file size for cases where it has not been captured yet, such as PDFs.
	if ( ! isset( $metadata['filesize'] ) && file_exists( $file ) ) {
		$metadata['filesize'] = wp_filesize( $file );
	}

	/**
	 * Filters the generated attachment meta data.
	 *
	 * @since 2.1.0
	 * @since 5.3.0 The `$context` parameter was added.
	 *
	 * @param array  $metadata      An array of attachment meta data.
	 * @param int    $attachment_id Current attachment ID.
	 * @param string $context       Additional context. Can be 'create' when metadata was initially created for new attachment
	 *                              or 'update' when the metadata was updated.
	 */
	return apply_filters( 'wp_generate_attachment_metadata', $metadata, $attachment_id, 'create' );
}

/**
 * Converts a fraction string to a decimal.
 *
 * @since 2.5.0
 *
 * @param string $str Fraction string.
 * @return int|float Returns calculated fraction or integer 0 on invalid input.
 */
function wp_exif_frac2dec( $str ) {
	if ( ! is_scalar( $str ) || is_bool( $str ) ) {
		return 0;
	}

	if ( ! is_string( $str ) ) {
		return $str; // This can only be an integer or float, so this is fine.
	}

	// Fractions passed as a string must contain a single `/`.
	if ( substr_count( $str, '/' ) !== 1 ) {
		if ( is_numeric( $str ) ) {
			return (float) $str;
		}

		return 0;
	}

	list( $numerator, $denominator ) = explode( '/', $str );

	// Both the numerator and the denominator must be numbers.
	if ( ! is_numeric( $numerator ) || ! is_numeric( $denominator ) ) {
		return 0;
	}

	// The denominator must not be zero.
	if ( 0 == $denominator ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual -- Deliberate loose comparison.
		return 0;
	}

	return $numerator / $denominator;
}

/**
 * Converts the exif date format to a unix timestamp.
 *
 * @since 2.5.0
 *
 * @param string $str A date string expected to be in Exif format (Y:m:d H:i:s).
 * @return int|false The unix timestamp, or false on failure.
 */
function wp_exif_date2ts( $str ) {
	list( $date, $time ) = explode( ' ', trim( $str ) );
	list( $y, $m, $d )   = explode( ':', $date );

	return strtotime( "{$y}-{$m}-{$d} {$time}" );
}

/**
 * Gets extended image metadata, exif or iptc as available.
 *
 * Retrieves the EXIF metadata aperture, credit, camera, caption, copyright, iso
 * created_timestamp, focal_length, shutter_speed, and title.
 *
 * The IPTC metadata that is retrieved is APP13, credit, byline, created date
 * and time, caption, copyright, and title. Also includes FNumber, Model,
 * DateTimeDigitized, FocalLength, ISOSpeedRatings, and ExposureTime.
 *
 * @todo Try other exif libraries if available.
 * @since 2.5.0
 *
 * @param string $file
 * @return array|false Image metadata array on success, false on failure.
 */
function wp_read_image_metadata( $file ) {
	if ( ! file_exists( $file ) ) {
		return false;
	}

	list( , , $image_type ) = wp_getimagesize( $file );

	/*
	 * EXIF contains a bunch of data we'll probably never need formatted in ways
	 * that are difficult to use. We'll normalize it and just extract the fields
	 * that are likely to be useful. Fractions and numbers are converted to
	 * floats, dates to unix timestamps, and everything else to strings.
	 */
	$meta = array(
		'aperture'          => 0,
		'credit'            => '',
		'camera'            => '',
		'caption'           => '',
		'created_timestamp' => 0,
		'copyright'         => '',
		'focal_length'      => 0,
		'iso'               => 0,
		'shutter_speed'     => 0,
		'title'             => '',
		'orientation'       => 0,
		'keywords'          => array(),
	);

	$iptc = array();
	$info = array();
	/*
	 * Read IPTC first, since it might contain data not available in exif such
	 * as caption, description etc.
	 */
	if ( is_callable( 'iptcparse' ) ) {
		wp_getimagesize( $file, $info );

		if ( ! empty( $info['APP13'] ) ) {
			// Don't silence errors when in debug mode, unless running unit tests.
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG
				&& ! defined( 'WP_RUN_CORE_TESTS' )
			) {
				$iptc = iptcparse( $info['APP13'] );
			} else {
				// Silencing notice and warning is intentional. See https://core.trac.wordpress.org/ticket/42480
				$iptc = @iptcparse( $info['APP13'] );
			}

			if ( ! is_array( $iptc ) ) {
				$iptc = array();
			}

			// Headline, "A brief synopsis of the caption".
			if ( ! empty( $iptc['2#105'][0] ) ) {
				$meta['title'] = trim( $iptc['2#105'][0] );
				/*
				* Title, "Many use the Title field to store the filename of the image,
				* though the field may be used in many ways".
				*/
			} elseif ( ! empty( $iptc['2#005'][0] ) ) {
				$meta['title'] = trim( $iptc['2#005'][0] );
			}

			if ( ! empty( $iptc['2#120'][0] ) ) { // Description / legacy caption.
				$caption = trim( $iptc['2#120'][0] );

				mbstring_binary_safe_encoding();
				$caption_length = strlen( $caption );
				reset_mbstring_encoding();

				if ( empty( $meta['title'] ) && $caption_length < 80 ) {
					// Assume the title is stored in 2:120 if it's short.
					$meta['title'] = $caption;
				}

				$meta['caption'] = $caption;
			}

			if ( ! empty( $iptc['2#110'][0] ) ) { // Credit.
				$meta['credit'] = trim( $iptc['2#110'][0] );
			} elseif ( ! empty( $iptc['2#080'][0] ) ) { // Creator / legacy byline.
				$meta['credit'] = trim( $iptc['2#080'][0] );
			}

			if ( ! empty( $iptc['2#055'][0] ) && ! empty( $iptc['2#060'][0] ) ) { // Created date and time.
				$meta['created_timestamp'] = strtotime( $iptc['2#055'][0] . ' ' . $iptc['2#060'][0] );
			}

			if ( ! empty( $iptc['2#116'][0] ) ) { // Copyright.
				$meta['copyright'] = trim( $iptc['2#116'][0] );
			}

			if ( ! empty( $iptc['2#025'][0] ) ) { // Keywords array.
				$meta['keywords'] = array_values( $iptc['2#025'] );
			}
		}
	}

	$exif = array();

	/**
	 * Filters the image types to check for exif data.
	 *
	 * @since 2.5.0
	 *
	 * @param int[] $image_types Array of image types to check for exif data. Each value
	 *                           is usually one of the `IMAGETYPE_*` constants.
	 */
	$exif_image_types = apply_filters( 'wp_read_image_metadata_types', array( IMAGETYPE_JPEG, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM ) );

	if ( is_callable( 'exif_read_data' ) && in_array( $image_type, $exif_image_types, true ) ) {
		// Don't silence errors when in debug mode, unless running unit tests.
		if ( defined( 'WP_DEBUG' ) && WP_DEBUG
			&& ! defined( 'WP_RUN_CORE_TESTS' )
		) {
			$exif = exif_read_data( $file );
		} else {
			// Silencing notice and warning is intentional. See https://core.trac.wordpress.org/ticket/42480
			$exif = @exif_read_data( $file );
		}

		if ( ! is_array( $exif ) ) {
			$exif = array();
		}

		$exif_description = '';
		$exif_usercomment = '';
		if ( ! empty( $exif['ImageDescription'] ) ) {
			$exif_description = trim( $exif['ImageDescription'] );
		}

		if ( ! empty( $exif['COMPUTED']['UserComment'] ) ) {
			$exif_usercomment = trim( $exif['COMPUTED']['UserComment'] );
		}

		if ( $exif_description ) {
			mbstring_binary_safe_encoding();
			$description_length = strlen( $exif_description );
			reset_mbstring_encoding();
			if ( empty( $meta['title'] ) && $description_length < 80 ) {
				// Assume the title is stored in ImageDescription.
				$meta['title'] = $exif_description;
			}

			// If both user comments and description are present.
			if ( empty( $meta['caption'] ) && $exif_description && $exif_usercomment ) {
				if ( ! empty( $meta['title'] ) && $exif_description === $meta['title'] ) {
					$caption = $exif_usercomment;
				} else {
					if ( $exif_description === $exif_usercomment ) {
						$caption = $exif_description;
					} else {
						$caption = trim( $exif_description . ' ' . $exif_usercomment );
					}
				}
				$meta['caption'] = $caption;
			}

			if ( empty( $meta['caption'] ) && $exif_usercomment ) {
				$meta['caption'] = $exif_usercomment;
			}

			if ( empty( $meta['caption'] ) ) {
				$meta['caption'] = $exif_description;
			}
		} elseif ( empty( $meta['caption'] ) && $exif_usercomment ) {
			$meta['caption']    = $exif_usercomment;
			$description_length = strlen( $exif_usercomment );
			if ( empty( $meta['title'] ) && $description_length < 80 ) {
				$meta['title'] = trim( $exif_usercomment );
			}
		} elseif ( empty( $meta['caption'] ) && ! empty( $exif['Comments'] ) ) {
			$meta['caption'] = trim( $exif['Comments'] );
		}

		if ( empty( $meta['credit'] ) ) {
			if ( ! empty( $exif['Artist'] ) ) {
				$meta['credit'] = trim( $exif['Artist'] );
			} elseif ( ! empty( $exif['Author'] ) ) {
				$meta['credit'] = trim( $exif['Author'] );
			}
		}

		if ( empty( $meta['copyright'] ) && ! empty( $exif['Copyright'] ) ) {
			$meta['copyright'] = trim( $exif['Copyright'] );
		}
		if ( ! empty( $exif['FNumber'] ) && is_scalar( $exif['FNumber'] ) ) {
			$meta['aperture'] = round( wp_exif_frac2dec( $exif['FNumber'] ), 2 );
		}
		if ( ! empty( $exif['Model'] ) ) {
			$meta['camera'] = trim( $exif['Model'] );
		}
		if ( empty( $meta['created_timestamp'] ) && ! empty( $exif['DateTimeDigitized'] ) ) {
			$meta['created_timestamp'] = wp_exif_date2ts( $exif['DateTimeDigitized'] );
		}
		if ( ! empty( $exif['FocalLength'] ) ) {
			$meta['focal_length'] = (string) $exif['FocalLength'];
			if ( is_scalar( $exif['FocalLength'] ) ) {
				$meta['focal_length'] = (string) wp_exif_frac2dec( $exif['FocalLength'] );
			}
		}
		if ( ! empty( $exif['ISOSpeedRatings'] ) ) {
			$meta['iso'] = is_array( $exif['ISOSpeedRatings'] ) ? reset( $exif['ISOSpeedRatings'] ) : $exif['ISOSpeedRatings'];
			$meta['iso'] = trim( $meta['iso'] );
		}
		if ( ! empty( $exif['ExposureTime'] ) ) {
			$meta['shutter_speed'] = (string) $exif['ExposureTime'];
			if ( is_scalar( $exif['ExposureTime'] ) ) {
				$meta['shutter_speed'] = (string) wp_exif_frac2dec( $exif['ExposureTime'] );
			}
		}
		if ( ! empty( $exif['Orientation'] ) ) {
			$meta['orientation'] = $exif['Orientation'];
		}
	}

	foreach ( array( 'title', 'caption', 'credit', 'copyright', 'camera', 'iso' ) as $key ) {
		if ( $meta[ $key ] && ! seems_utf8( $meta[ $key ] ) ) {
			$meta[ $key ] = utf8_encode( $meta[ $key ] );
		}
	}

	foreach ( $meta['keywords'] as $key => $keyword ) {
		if ( ! seems_utf8( $keyword ) ) {
			$meta['keywords'][ $key ] = utf8_encode( $keyword );
		}
	}

	$meta = wp_kses_post_deep( $meta );

	/**
	 * Filters the array of meta data read from an image's exif data.
	 *
	 * @since 2.5.0
	 * @since 4.4.0 The `$iptc` parameter was added.
	 * @since 5.0.0 The `$exif` parameter was added.
	 *
	 * @param array  $meta       Image meta data.
	 * @param string $file       Path to image file.
	 * @param int    $image_type Type of image, one of the `IMAGETYPE_XXX` constants.
	 * @param array  $iptc       IPTC data.
	 * @param array  $exif       EXIF data.
	 */
	return apply_filters( 'wp_read_image_metadata', $meta, $file, $image_type, $iptc, $exif );
}

/**
 * Validates that file is an image.
 *
 * @since 2.5.0
 *
 * @param string $path File path to test if valid image.
 * @return bool True if valid image, false if not valid image.
 */
function file_is_valid_image( $path ) {
	$size = wp_getimagesize( $path );
	return ! empty( $size );
}

/**
 * Validates that file is suitable for displaying within a web page.
 *
 * @since 2.5.0
 *
 * @param string $path File path to test.
 * @return bool True if suitable, false if not suitable.
 */
function file_is_displayable_image( $path ) {
	$displayable_image_types = array( IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_BMP, IMAGETYPE_ICO, IMAGETYPE_WEBP, IMAGETYPE_AVIF );

	$info = wp_getimagesize( $path );
	if ( empty( $info ) ) {
		$result = false;
	} elseif ( ! in_array( $info[2], $displayable_image_types, true ) ) {
		$result = false;
	} else {
		$result = true;
	}

	/**
	 * Filters whether the current image is displayable in the browser.
	 *
	 * @since 2.5.0
	 *
	 * @param bool   $result Whether the image can be displayed. Default true.
	 * @param string $path   Path to the image.
	 */
	return apply_filters( 'file_is_displayable_image', $result, $path );
}

/**
 * Loads an image resource for editing.
 *
 * @since 2.9.0
 *
 * @param int          $attachment_id Attachment ID.
 * @param string       $mime_type     Image mime type.
 * @param string|int[] $size          Optional. Image size. Accepts any registered image size name, or an array
 *                                    of width and height values in pixels (in that order). Default 'full'.
 * @return resource|GdImage|false The resulting image resource or GdImage instance on success,
 *                                false on failure.
 */
function load_image_to_edit( $attachment_id, $mime_type, $size = 'full' ) {
	$filepath = _load_image_to_edit_path( $attachment_id, $size );
	if ( empty( $filepath ) ) {
		return false;
	}

	switch ( $mime_type ) {
		case 'image/jpeg':
			$image = imagecreatefromjpeg( $filepath );
			break;
		case 'image/png':
			$image = imagecreatefrompng( $filepath );
			break;
		case 'image/gif':
			$image = imagecreatefromgif( $filepath );
			break;
		case 'image/webp':
			$image = false;
			if ( function_exists( 'imagecreatefromwebp' ) ) {
				$image = imagecreatefromwebp( $filepath );
			}
			break;
		default:
			$image = false;
			break;
	}

	if ( is_gd_image( $image ) ) {
		/**
		 * Filters the current image being loaded for editing.
		 *
		 * @since 2.9.0
		 *
		 * @param resource|GdImage $image         Current image.
		 * @param int              $attachment_id Attachment ID.
		 * @param string|int[]     $size          Requested image size. Can be any registered image size name, or
		 *                                        an array of width and height values in pixels (in that order).
		 */
		$image = apply_filters( 'load_image_to_edit', $image, $attachment_id, $size );

		if ( function_exists( 'imagealphablending' ) && function_exists( 'imagesavealpha' ) ) {
			imagealphablending( $image, false );
			imagesavealpha( $image, true );
		}
	}

	return $image;
}

/**
 * Retrieves the path or URL of an attachment's attached file.
 *
 * If the attached file is not present on the local filesystem (usually due to replication plugins),
 * then the URL of the file is returned if `allow_url_fopen` is supported.
 *
 * @since 3.4.0
 * @access private
 *
 * @param int          $attachment_id Attachment ID.
 * @param string|int[] $size          Optional. Image size. Accepts any registered image size name, or an array
 *                                    of width and height values in pixels (in that order). Default 'full'.
 * @return string|false File path or URL on success, false on failure.
 */
function _load_image_to_edit_path( $attachment_id, $size = 'full' ) {
	$filepath = get_attached_file( $attachment_id );

	if ( $filepath && file_exists( $filepath ) ) {
		if ( 'full' !== $size ) {
			$data = image_get_intermediate_size( $attachment_id, $size );

			if ( $data ) {
				$filepath = path_join( dirname( $filepath ), $data['file'] );

				/**
				 * Filters the path to an attachment's file when editing the image.
				 *
				 * The filter is evaluated for all image sizes except 'full'.
				 *
				 * @since 3.1.0
				 *
				 * @param string       $path          Path to the current image.
				 * @param int          $attachment_id Attachment ID.
				 * @param string|int[] $size          Requested image size. Can be any registered image size name, or
				 *                                    an array of width and height values in pixels (in that order).
				 */
				$filepath = apply_filters( 'load_image_to_edit_filesystempath', $filepath, $attachment_id, $size );
			}
		}
	} elseif ( function_exists( 'fopen' ) && ini_get( 'allow_url_fopen' ) ) {
		/**
		 * Filters the path to an attachment's URL when editing the image.
		 *
		 * The filter is only evaluated if the file isn't stored locally and `allow_url_fopen` is enabled on the server.
		 *
		 * @since 3.1.0
		 *
		 * @param string|false $image_url     Current image URL.
		 * @param int          $attachment_id Attachment ID.
		 * @param string|int[] $size          Requested image size. Can be any registered image size name, or
		 *                                    an array of width and height values in pixels (in that order).
		 */
		$filepath = apply_filters( 'load_image_to_edit_attachmenturl', wp_get_attachment_url( $attachment_id ), $attachment_id, $size );
	}

	/**
	 * Filters the returned path or URL of the current image.
	 *
	 * @since 2.9.0
	 *
	 * @param string|false $filepath      File path or URL to current image, or false.
	 * @param int          $attachment_id Attachment ID.
	 * @param string|int[] $size          Requested image size. Can be any registered image size name, or
	 *                                    an array of width and height values in pixels (in that order).
	 */
	return apply_filters( 'load_image_to_edit_path', $filepath, $attachment_id, $size );
}

/**
 * Copies an existing image file.
 *
 * @since 3.4.0
 * @access private
 *
 * @param int $attachment_id Attachment ID.
 * @return string|false New file path on success, false on failure.
 */
function _copy_image_file( $attachment_id ) {
	$dst_file = get_attached_file( $attachment_id );
	$src_file = $dst_file;

	if ( ! file_exists( $src_file ) ) {
		$src_file = _load_image_to_edit_path( $attachment_id );
	}

	if ( $src_file ) {
		$dst_file = str_replace( wp_basename( $dst_file ), 'copy-' . wp_basename( $dst_file ), $dst_file );
		$dst_file = dirname( $dst_file ) . '/' . wp_unique_filename( dirname( $dst_file ), wp_basename( $dst_file ) );

		/*
		 * The directory containing the original file may no longer
		 * exist when using a replication plugin.
		 */
		wp_mkdir_p( dirname( $dst_file ) );

		if ( ! copy( $src_file, $dst_file ) ) {
			$dst_file = false;
		}
	} else {
		$dst_file = false;
	}

	return $dst_file;
}
post.php000064400000240433151327343360006257 0ustar00<?php
/**
 * WordPress Post Administration API.
 *
 * @package WordPress
 * @subpackage Administration
 */

/**
 * Renames `$_POST` data from form names to DB post columns.
 *
 * Manipulates `$_POST` directly.
 *
 * @since 2.6.0
 *
 * @param bool       $update    Whether the post already exists.
 * @param array|null $post_data Optional. The array of post data to process.
 *                              Defaults to the `$_POST` superglobal.
 * @return array|WP_Error Array of post data on success, WP_Error on failure.
 */
function _wp_translate_postdata( $update = false, $post_data = null ) {

	if ( empty( $post_data ) ) {
		$post_data = &$_POST;
	}

	if ( $update ) {
		$post_data['ID'] = (int) $post_data['post_ID'];
	}

	$ptype = get_post_type_object( $post_data['post_type'] );

	if ( $update && ! current_user_can( 'edit_post', $post_data['ID'] ) ) {
		if ( 'page' === $post_data['post_type'] ) {
			return new WP_Error( 'edit_others_pages', __( 'Sorry, you are not allowed to edit pages as this user.' ) );
		} else {
			return new WP_Error( 'edit_others_posts', __( 'Sorry, you are not allowed to edit posts as this user.' ) );
		}
	} elseif ( ! $update && ! current_user_can( $ptype->cap->create_posts ) ) {
		if ( 'page' === $post_data['post_type'] ) {
			return new WP_Error( 'edit_others_pages', __( 'Sorry, you are not allowed to create pages as this user.' ) );
		} else {
			return new WP_Error( 'edit_others_posts', __( 'Sorry, you are not allowed to create posts as this user.' ) );
		}
	}

	if ( isset( $post_data['content'] ) ) {
		$post_data['post_content'] = $post_data['content'];
	}

	if ( isset( $post_data['excerpt'] ) ) {
		$post_data['post_excerpt'] = $post_data['excerpt'];
	}

	if ( isset( $post_data['parent_id'] ) ) {
		$post_data['post_parent'] = (int) $post_data['parent_id'];
	}

	if ( isset( $post_data['trackback_url'] ) ) {
		$post_data['to_ping'] = $post_data['trackback_url'];
	}

	$post_data['user_ID'] = get_current_user_id();

	if ( ! empty( $post_data['post_author_override'] ) ) {
		$post_data['post_author'] = (int) $post_data['post_author_override'];
	} else {
		if ( ! empty( $post_data['post_author'] ) ) {
			$post_data['post_author'] = (int) $post_data['post_author'];
		} else {
			$post_data['post_author'] = (int) $post_data['user_ID'];
		}
	}

	if ( isset( $post_data['user_ID'] ) && ( $post_data['post_author'] !== $post_data['user_ID'] )
		&& ! current_user_can( $ptype->cap->edit_others_posts ) ) {

		if ( $update ) {
			if ( 'page' === $post_data['post_type'] ) {
				return new WP_Error( 'edit_others_pages', __( 'Sorry, you are not allowed to edit pages as this user.' ) );
			} else {
				return new WP_Error( 'edit_others_posts', __( 'Sorry, you are not allowed to edit posts as this user.' ) );
			}
		} else {
			if ( 'page' === $post_data['post_type'] ) {
				return new WP_Error( 'edit_others_pages', __( 'Sorry, you are not allowed to create pages as this user.' ) );
			} else {
				return new WP_Error( 'edit_others_posts', __( 'Sorry, you are not allowed to create posts as this user.' ) );
			}
		}
	}

	if ( ! empty( $post_data['post_status'] ) ) {
		$post_data['post_status'] = sanitize_key( $post_data['post_status'] );

		// No longer an auto-draft.
		if ( 'auto-draft' === $post_data['post_status'] ) {
			$post_data['post_status'] = 'draft';
		}

		if ( ! get_post_status_object( $post_data['post_status'] ) ) {
			unset( $post_data['post_status'] );
		}
	}

	// What to do based on which button they pressed.
	if ( isset( $post_data['saveasdraft'] ) && '' !== $post_data['saveasdraft'] ) {
		$post_data['post_status'] = 'draft';
	}
	if ( isset( $post_data['saveasprivate'] ) && '' !== $post_data['saveasprivate'] ) {
		$post_data['post_status'] = 'private';
	}
	if ( isset( $post_data['publish'] ) && ( '' !== $post_data['publish'] )
		&& ( ! isset( $post_data['post_status'] ) || 'private' !== $post_data['post_status'] )
	) {
		$post_data['post_status'] = 'publish';
	}
	if ( isset( $post_data['advanced'] ) && '' !== $post_data['advanced'] ) {
		$post_data['post_status'] = 'draft';
	}
	if ( isset( $post_data['pending'] ) && '' !== $post_data['pending'] ) {
		$post_data['post_status'] = 'pending';
	}

	if ( isset( $post_data['ID'] ) ) {
		$post_id = $post_data['ID'];
	} else {
		$post_id = false;
	}
	$previous_status = $post_id ? get_post_field( 'post_status', $post_id ) : false;

	if ( isset( $post_data['post_status'] ) && 'private' === $post_data['post_status'] && ! current_user_can( $ptype->cap->publish_posts ) ) {
		$post_data['post_status'] = $previous_status ? $previous_status : 'pending';
	}

	$published_statuses = array( 'publish', 'future' );

	/*
	 * Posts 'submitted for approval' are submitted to $_POST the same as if they were being published.
	 * Change status from 'publish' to 'pending' if user lacks permissions to publish or to resave published posts.
	 */
	if ( isset( $post_data['post_status'] )
		&& ( in_array( $post_data['post_status'], $published_statuses, true )
		&& ! current_user_can( $ptype->cap->publish_posts ) )
	) {
		if ( ! in_array( $previous_status, $published_statuses, true ) || ! current_user_can( 'edit_post', $post_id ) ) {
			$post_data['post_status'] = 'pending';
		}
	}

	if ( ! isset( $post_data['post_status'] ) ) {
		$post_data['post_status'] = 'auto-draft' === $previous_status ? 'draft' : $previous_status;
	}

	if ( isset( $post_data['post_password'] ) && ! current_user_can( $ptype->cap->publish_posts ) ) {
		unset( $post_data['post_password'] );
	}

	if ( ! isset( $post_data['comment_status'] ) ) {
		$post_data['comment_status'] = 'closed';
	}

	if ( ! isset( $post_data['ping_status'] ) ) {
		$post_data['ping_status'] = 'closed';
	}

	foreach ( array( 'aa', 'mm', 'jj', 'hh', 'mn' ) as $timeunit ) {
		if ( ! empty( $post_data[ 'hidden_' . $timeunit ] ) && $post_data[ 'hidden_' . $timeunit ] !== $post_data[ $timeunit ] ) {
			$post_data['edit_date'] = '1';
			break;
		}
	}

	if ( ! empty( $post_data['edit_date'] ) ) {
		$aa = $post_data['aa'];
		$mm = $post_data['mm'];
		$jj = $post_data['jj'];
		$hh = $post_data['hh'];
		$mn = $post_data['mn'];
		$ss = $post_data['ss'];
		$aa = ( $aa <= 0 ) ? gmdate( 'Y' ) : $aa;
		$mm = ( $mm <= 0 ) ? gmdate( 'n' ) : $mm;
		$jj = ( $jj > 31 ) ? 31 : $jj;
		$jj = ( $jj <= 0 ) ? gmdate( 'j' ) : $jj;
		$hh = ( $hh > 23 ) ? $hh - 24 : $hh;
		$mn = ( $mn > 59 ) ? $mn - 60 : $mn;
		$ss = ( $ss > 59 ) ? $ss - 60 : $ss;

		$post_data['post_date'] = sprintf( '%04d-%02d-%02d %02d:%02d:%02d', $aa, $mm, $jj, $hh, $mn, $ss );

		$valid_date = wp_checkdate( $mm, $jj, $aa, $post_data['post_date'] );
		if ( ! $valid_date ) {
			return new WP_Error( 'invalid_date', __( 'Invalid date.' ) );
		}

		/*
		 * Only assign a post date if the user has explicitly set a new value.
		 * See #59125 and #19907.
		 */
		$previous_date = $post_id ? get_post_field( 'post_date', $post_id ) : false;
		if ( $previous_date && $previous_date !== $post_data['post_date'] ) {
			$post_data['edit_date']     = true;
			$post_data['post_date_gmt'] = get_gmt_from_date( $post_data['post_date'] );
		} else {
			$post_data['edit_date'] = false;
			unset( $post_data['post_date'] );
			unset( $post_data['post_date_gmt'] );
		}
	}

	if ( isset( $post_data['post_category'] ) ) {
		$category_object = get_taxonomy( 'category' );
		if ( ! current_user_can( $category_object->cap->assign_terms ) ) {
			unset( $post_data['post_category'] );
		}
	}

	return $post_data;
}

/**
 * Returns only allowed post data fields.
 *
 * @since 5.0.1
 *
 * @param array|WP_Error|null $post_data The array of post data to process, or an error object.
 *                                       Defaults to the `$_POST` superglobal.
 * @return array|WP_Error Array of post data on success, WP_Error on failure.
 */
function _wp_get_allowed_postdata( $post_data = null ) {
	if ( empty( $post_data ) ) {
		$post_data = $_POST;
	}

	// Pass through errors.
	if ( is_wp_error( $post_data ) ) {
		return $post_data;
	}

	return array_diff_key( $post_data, array_flip( array( 'meta_input', 'file', 'guid' ) ) );
}

/**
 * Updates an existing post with values provided in `$_POST`.
 *
 * If post data is passed as an argument, it is treated as an array of data
 * keyed appropriately for turning into a post object.
 *
 * If post data is not passed, the `$_POST` global variable is used instead.
 *
 * @since 1.5.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param array|null $post_data Optional. The array of post data to process.
 *                              Defaults to the `$_POST` superglobal.
 * @return int Post ID.
 */
function edit_post( $post_data = null ) {
	global $wpdb;

	if ( empty( $post_data ) ) {
		$post_data = &$_POST;
	}

	// Clear out any data in internal vars.
	unset( $post_data['filter'] );

	$post_id = (int) $post_data['post_ID'];
	$post    = get_post( $post_id );

	$post_data['post_type']      = $post->post_type;
	$post_data['post_mime_type'] = $post->post_mime_type;

	if ( ! empty( $post_data['post_status'] ) ) {
		$post_data['post_status'] = sanitize_key( $post_data['post_status'] );

		if ( 'inherit' === $post_data['post_status'] ) {
			unset( $post_data['post_status'] );
		}
	}

	$ptype = get_post_type_object( $post_data['post_type'] );
	if ( ! current_user_can( 'edit_post', $post_id ) ) {
		if ( 'page' === $post_data['post_type'] ) {
			wp_die( __( 'Sorry, you are not allowed to edit this page.' ) );
		} else {
			wp_die( __( 'Sorry, you are not allowed to edit this post.' ) );
		}
	}

	if ( post_type_supports( $ptype->name, 'revisions' ) ) {
		$revisions = wp_get_post_revisions(
			$post_id,
			array(
				'order'          => 'ASC',
				'posts_per_page' => 1,
			)
		);
		$revision  = current( $revisions );

		// Check if the revisions have been upgraded.
		if ( $revisions && _wp_get_post_revision_version( $revision ) < 1 ) {
			_wp_upgrade_revisions_of_post( $post, wp_get_post_revisions( $post_id ) );
		}
	}

	if ( isset( $post_data['visibility'] ) ) {
		switch ( $post_data['visibility'] ) {
			case 'public':
				$post_data['post_password'] = '';
				break;
			case 'password':
				unset( $post_data['sticky'] );
				break;
			case 'private':
				$post_data['post_status']   = 'private';
				$post_data['post_password'] = '';
				unset( $post_data['sticky'] );
				break;
		}
	}

	$post_data = _wp_translate_postdata( true, $post_data );
	if ( is_wp_error( $post_data ) ) {
		wp_die( $post_data->get_error_message() );
	}
	$translated = _wp_get_allowed_postdata( $post_data );

	// Post formats.
	if ( isset( $post_data['post_format'] ) ) {
		set_post_format( $post_id, $post_data['post_format'] );
	}

	$format_meta_urls = array( 'url', 'link_url', 'quote_source_url' );
	foreach ( $format_meta_urls as $format_meta_url ) {
		$keyed = '_format_' . $format_meta_url;
		if ( isset( $post_data[ $keyed ] ) ) {
			update_post_meta( $post_id, $keyed, wp_slash( sanitize_url( wp_unslash( $post_data[ $keyed ] ) ) ) );
		}
	}

	$format_keys = array( 'quote', 'quote_source_name', 'image', 'gallery', 'audio_embed', 'video_embed' );

	foreach ( $format_keys as $key ) {
		$keyed = '_format_' . $key;
		if ( isset( $post_data[ $keyed ] ) ) {
			if ( current_user_can( 'unfiltered_html' ) ) {
				update_post_meta( $post_id, $keyed, $post_data[ $keyed ] );
			} else {
				update_post_meta( $post_id, $keyed, wp_filter_post_kses( $post_data[ $keyed ] ) );
			}
		}
	}

	if ( 'attachment' === $post_data['post_type'] && preg_match( '#^(audio|video)/#', $post_data['post_mime_type'] ) ) {
		$id3data = wp_get_attachment_metadata( $post_id );
		if ( ! is_array( $id3data ) ) {
			$id3data = array();
		}

		foreach ( wp_get_attachment_id3_keys( $post, 'edit' ) as $key => $label ) {
			if ( isset( $post_data[ 'id3_' . $key ] ) ) {
				$id3data[ $key ] = sanitize_text_field( wp_unslash( $post_data[ 'id3_' . $key ] ) );
			}
		}
		wp_update_attachment_metadata( $post_id, $id3data );
	}

	// Meta stuff.
	if ( isset( $post_data['meta'] ) && $post_data['meta'] ) {
		foreach ( $post_data['meta'] as $key => $value ) {
			$meta = get_post_meta_by_id( $key );
			if ( ! $meta ) {
				continue;
			}

			if ( (int) $meta->post_id !== $post_id ) {
				continue;
			}

			if ( is_protected_meta( $meta->meta_key, 'post' )
				|| ! current_user_can( 'edit_post_meta', $post_id, $meta->meta_key )
			) {
				continue;
			}

			if ( is_protected_meta( $value['key'], 'post' )
				|| ! current_user_can( 'edit_post_meta', $post_id, $value['key'] )
			) {
				continue;
			}

			update_meta( $key, $value['key'], $value['value'] );
		}
	}

	if ( isset( $post_data['deletemeta'] ) && $post_data['deletemeta'] ) {
		foreach ( $post_data['deletemeta'] as $key => $value ) {
			$meta = get_post_meta_by_id( $key );
			if ( ! $meta ) {
				continue;
			}

			if ( (int) $meta->post_id !== $post_id ) {
				continue;
			}

			if ( is_protected_meta( $meta->meta_key, 'post' )
				|| ! current_user_can( 'delete_post_meta', $post_id, $meta->meta_key )
			) {
				continue;
			}

			delete_meta( $key );
		}
	}

	// Attachment stuff.
	if ( 'attachment' === $post_data['post_type'] ) {
		if ( isset( $post_data['_wp_attachment_image_alt'] ) ) {
			$image_alt = wp_unslash( $post_data['_wp_attachment_image_alt'] );

			if ( get_post_meta( $post_id, '_wp_attachment_image_alt', true ) !== $image_alt ) {
				$image_alt = wp_strip_all_tags( $image_alt, true );

				// update_post_meta() expects slashed.
				update_post_meta( $post_id, '_wp_attachment_image_alt', wp_slash( $image_alt ) );
			}
		}

		$attachment_data = isset( $post_data['attachments'][ $post_id ] ) ? $post_data['attachments'][ $post_id ] : array();

		/** This filter is documented in wp-admin/includes/media.php */
		$translated = apply_filters( 'attachment_fields_to_save', $translated, $attachment_data );
	}

	// Convert taxonomy input to term IDs, to avoid ambiguity.
	if ( isset( $post_data['tax_input'] ) ) {
		foreach ( (array) $post_data['tax_input'] as $taxonomy => $terms ) {
			$tax_object = get_taxonomy( $taxonomy );

			if ( $tax_object && isset( $tax_object->meta_box_sanitize_cb ) ) {
				$translated['tax_input'][ $taxonomy ] = call_user_func_array( $tax_object->meta_box_sanitize_cb, array( $taxonomy, $terms ) );
			}
		}
	}

	add_meta( $post_id );

	update_post_meta( $post_id, '_edit_last', get_current_user_id() );

	$success = wp_update_post( $translated );

	// If the save failed, see if we can confidence check the main fields and try again.
	if ( ! $success && is_callable( array( $wpdb, 'strip_invalid_text_for_column' ) ) ) {
		$fields = array( 'post_title', 'post_content', 'post_excerpt' );

		foreach ( $fields as $field ) {
			if ( isset( $translated[ $field ] ) ) {
				$translated[ $field ] = $wpdb->strip_invalid_text_for_column( $wpdb->posts, $field, $translated[ $field ] );
			}
		}

		wp_update_post( $translated );
	}

	// Now that we have an ID we can fix any attachment anchor hrefs.
	_fix_attachment_links( $post_id );

	wp_set_post_lock( $post_id );

	if ( current_user_can( $ptype->cap->edit_others_posts ) && current_user_can( $ptype->cap->publish_posts ) ) {
		if ( ! empty( $post_data['sticky'] ) ) {
			stick_post( $post_id );
		} else {
			unstick_post( $post_id );
		}
	}

	return $post_id;
}

/**
 * Processes the post data for the bulk editing of posts.
 *
 * Updates all bulk edited posts/pages, adding (but not removing) tags and
 * categories. Skips pages when they would be their own parent or child.
 *
 * @since 2.7.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param array|null $post_data Optional. The array of post data to process.
 *                              Defaults to the `$_POST` superglobal.
 * @return array {
 *     An array of updated, skipped, and locked post IDs.
 *
 *     @type int[] $updated An array of updated post IDs.
 *     @type int[] $skipped An array of skipped post IDs.
 *     @type int[] $locked  An array of locked post IDs.
 * }
 */
function bulk_edit_posts( $post_data = null ) {
	global $wpdb;

	if ( empty( $post_data ) ) {
		$post_data = &$_POST;
	}

	if ( isset( $post_data['post_type'] ) ) {
		$ptype = get_post_type_object( $post_data['post_type'] );
	} else {
		$ptype = get_post_type_object( 'post' );
	}

	if ( ! current_user_can( $ptype->cap->edit_posts ) ) {
		if ( 'page' === $ptype->name ) {
			wp_die( __( 'Sorry, you are not allowed to edit pages.' ) );
		} else {
			wp_die( __( 'Sorry, you are not allowed to edit posts.' ) );
		}
	}

	if ( '-1' === $post_data['_status'] ) {
		$post_data['post_status'] = null;
		unset( $post_data['post_status'] );
	} else {
		$post_data['post_status'] = $post_data['_status'];
	}
	unset( $post_data['_status'] );

	if ( ! empty( $post_data['post_status'] ) ) {
		$post_data['post_status'] = sanitize_key( $post_data['post_status'] );

		if ( 'inherit' === $post_data['post_status'] ) {
			unset( $post_data['post_status'] );
		}
	}

	$post_ids = array_map( 'intval', (array) $post_data['post'] );

	$reset = array(
		'post_author',
		'post_status',
		'post_password',
		'post_parent',
		'page_template',
		'comment_status',
		'ping_status',
		'keep_private',
		'tax_input',
		'post_category',
		'sticky',
		'post_format',
	);

	foreach ( $reset as $field ) {
		if ( isset( $post_data[ $field ] ) && ( '' === $post_data[ $field ] || '-1' === $post_data[ $field ] ) ) {
			unset( $post_data[ $field ] );
		}
	}

	if ( isset( $post_data['post_category'] ) ) {
		if ( is_array( $post_data['post_category'] ) && ! empty( $post_data['post_category'] ) ) {
			$new_cats = array_map( 'absint', $post_data['post_category'] );
		} else {
			unset( $post_data['post_category'] );
		}
	}

	$tax_input = array();
	if ( isset( $post_data['tax_input'] ) ) {
		foreach ( $post_data['tax_input'] as $tax_name => $terms ) {
			if ( empty( $terms ) ) {
				continue;
			}

			if ( is_taxonomy_hierarchical( $tax_name ) ) {
				$tax_input[ $tax_name ] = array_map( 'absint', $terms );
			} else {
				$comma = _x( ',', 'tag delimiter' );
				if ( ',' !== $comma ) {
					$terms = str_replace( $comma, ',', $terms );
				}
				$tax_input[ $tax_name ] = explode( ',', trim( $terms, " \n\t\r\0\x0B," ) );
			}
		}
	}

	if ( isset( $post_data['post_parent'] ) && (int) $post_data['post_parent'] ) {
		$parent   = (int) $post_data['post_parent'];
		$pages    = $wpdb->get_results( "SELECT ID, post_parent FROM $wpdb->posts WHERE post_type = 'page'" );
		$children = array();

		for ( $i = 0; $i < 50 && $parent > 0; $i++ ) {
			$children[] = $parent;

			foreach ( $pages as $page ) {
				if ( (int) $page->ID === $parent ) {
					$parent = (int) $page->post_parent;
					break;
				}
			}
		}
	}

	$updated          = array();
	$skipped          = array();
	$locked           = array();
	$shared_post_data = $post_data;

	foreach ( $post_ids as $post_id ) {
		// Start with fresh post data with each iteration.
		$post_data = $shared_post_data;

		$post_type_object = get_post_type_object( get_post_type( $post_id ) );

		if ( ! isset( $post_type_object )
			|| ( isset( $children ) && in_array( $post_id, $children, true ) )
			|| ! current_user_can( 'edit_post', $post_id )
		) {
			$skipped[] = $post_id;
			continue;
		}

		if ( wp_check_post_lock( $post_id ) ) {
			$locked[] = $post_id;
			continue;
		}

		$post      = get_post( $post_id );
		$tax_names = get_object_taxonomies( $post );

		foreach ( $tax_names as $tax_name ) {
			$taxonomy_obj = get_taxonomy( $tax_name );

			if ( ! $taxonomy_obj->show_in_quick_edit ) {
				continue;
			}

			if ( isset( $tax_input[ $tax_name ] ) && current_user_can( $taxonomy_obj->cap->assign_terms ) ) {
				$new_terms = $tax_input[ $tax_name ];
			} else {
				$new_terms = array();
			}

			if ( $taxonomy_obj->hierarchical ) {
				$current_terms = (array) wp_get_object_terms( $post_id, $tax_name, array( 'fields' => 'ids' ) );
			} else {
				$current_terms = (array) wp_get_object_terms( $post_id, $tax_name, array( 'fields' => 'names' ) );
			}

			$post_data['tax_input'][ $tax_name ] = array_merge( $current_terms, $new_terms );
		}

		if ( isset( $new_cats ) && in_array( 'category', $tax_names, true ) ) {
			$cats = (array) wp_get_post_categories( $post_id );

			if (
				isset( $post_data['indeterminate_post_category'] )
				&& is_array( $post_data['indeterminate_post_category'] )
			) {
				$indeterminate_post_category = $post_data['indeterminate_post_category'];
			} else {
				$indeterminate_post_category = array();
			}

			$indeterminate_cats         = array_intersect( $cats, $indeterminate_post_category );
			$determinate_cats           = array_diff( $new_cats, $indeterminate_post_category );
			$post_data['post_category'] = array_unique( array_merge( $indeterminate_cats, $determinate_cats ) );

			unset( $post_data['tax_input']['category'] );
		}

		$post_data['post_ID']        = $post_id;
		$post_data['post_type']      = $post->post_type;
		$post_data['post_mime_type'] = $post->post_mime_type;

		foreach ( array( 'comment_status', 'ping_status', 'post_author' ) as $field ) {
			if ( ! isset( $post_data[ $field ] ) ) {
				$post_data[ $field ] = $post->$field;
			}
		}

		$post_data = _wp_translate_postdata( true, $post_data );
		if ( is_wp_error( $post_data ) ) {
			$skipped[] = $post_id;
			continue;
		}
		$post_data = _wp_get_allowed_postdata( $post_data );

		if ( isset( $shared_post_data['post_format'] ) ) {
			set_post_format( $post_id, $shared_post_data['post_format'] );
		}

		// Prevent wp_insert_post() from overwriting post format with the old data.
		unset( $post_data['tax_input']['post_format'] );

		// Reset post date of scheduled post to be published.
		if (
			in_array( $post->post_status, array( 'future', 'draft' ), true ) &&
			'publish' === $post_data['post_status']
		) {
			$post_data['post_date']     = current_time( 'mysql' );
			$post_data['post_date_gmt'] = '';
		}

		$post_id = wp_update_post( $post_data );
		update_post_meta( $post_id, '_edit_last', get_current_user_id() );
		$updated[] = $post_id;

		if ( isset( $post_data['sticky'] ) && current_user_can( $ptype->cap->edit_others_posts ) ) {
			if ( 'sticky' === $post_data['sticky'] ) {
				stick_post( $post_id );
			} else {
				unstick_post( $post_id );
			}
		}
	}

	/**
	 * Fires after processing the post data for bulk edit.
	 *
	 * @since 6.3.0
	 *
	 * @param int[] $updated          An array of updated post IDs.
	 * @param array $shared_post_data Associative array containing the post data.
	 */
	do_action( 'bulk_edit_posts', $updated, $shared_post_data );

	return array(
		'updated' => $updated,
		'skipped' => $skipped,
		'locked'  => $locked,
	);
}

/**
 * Returns default post information to use when populating the "Write Post" form.
 *
 * @since 2.0.0
 *
 * @param string $post_type    Optional. A post type string. Default 'post'.
 * @param bool   $create_in_db Optional. Whether to insert the post into database. Default false.
 * @return WP_Post Post object containing all the default post data as attributes
 */
function get_default_post_to_edit( $post_type = 'post', $create_in_db = false ) {
	$post_title = '';
	if ( ! empty( $_REQUEST['post_title'] ) ) {
		$post_title = esc_html( wp_unslash( $_REQUEST['post_title'] ) );
	}

	$post_content = '';
	if ( ! empty( $_REQUEST['content'] ) ) {
		$post_content = esc_html( wp_unslash( $_REQUEST['content'] ) );
	}

	$post_excerpt = '';
	if ( ! empty( $_REQUEST['excerpt'] ) ) {
		$post_excerpt = esc_html( wp_unslash( $_REQUEST['excerpt'] ) );
	}

	if ( $create_in_db ) {
		$post_id = wp_insert_post(
			array(
				'post_title'  => __( 'Auto Draft' ),
				'post_type'   => $post_type,
				'post_status' => 'auto-draft',
			),
			false,
			false
		);
		$post    = get_post( $post_id );
		if ( current_theme_supports( 'post-formats' ) && post_type_supports( $post->post_type, 'post-formats' ) && get_option( 'default_post_format' ) ) {
			set_post_format( $post, get_option( 'default_post_format' ) );
		}
		wp_after_insert_post( $post, false, null );

		// Schedule auto-draft cleanup.
		if ( ! wp_next_scheduled( 'wp_scheduled_auto_draft_delete' ) ) {
			wp_schedule_event( time(), 'daily', 'wp_scheduled_auto_draft_delete' );
		}
	} else {
		$post                 = new stdClass();
		$post->ID             = 0;
		$post->post_author    = '';
		$post->post_date      = '';
		$post->post_date_gmt  = '';
		$post->post_password  = '';
		$post->post_name      = '';
		$post->post_type      = $post_type;
		$post->post_status    = 'draft';
		$post->to_ping        = '';
		$post->pinged         = '';
		$post->comment_status = get_default_comment_status( $post_type );
		$post->ping_status    = get_default_comment_status( $post_type, 'pingback' );
		$post->post_pingback  = get_option( 'default_pingback_flag' );
		$post->post_category  = get_option( 'default_category' );
		$post->page_template  = 'default';
		$post->post_parent    = 0;
		$post->menu_order     = 0;
		$post                 = new WP_Post( $post );
	}

	/**
	 * Filters the default post content initially used in the "Write Post" form.
	 *
	 * @since 1.5.0
	 *
	 * @param string  $post_content Default post content.
	 * @param WP_Post $post         Post object.
	 */
	$post->post_content = (string) apply_filters( 'default_content', $post_content, $post );

	/**
	 * Filters the default post title initially used in the "Write Post" form.
	 *
	 * @since 1.5.0
	 *
	 * @param string  $post_title Default post title.
	 * @param WP_Post $post       Post object.
	 */
	$post->post_title = (string) apply_filters( 'default_title', $post_title, $post );

	/**
	 * Filters the default post excerpt initially used in the "Write Post" form.
	 *
	 * @since 1.5.0
	 *
	 * @param string  $post_excerpt Default post excerpt.
	 * @param WP_Post $post         Post object.
	 */
	$post->post_excerpt = (string) apply_filters( 'default_excerpt', $post_excerpt, $post );

	return $post;
}

/**
 * Determines if a post exists based on title, content, date and type.
 *
 * @since 2.0.0
 * @since 5.2.0 Added the `$type` parameter.
 * @since 5.8.0 Added the `$status` parameter.
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param string $title   Post title.
 * @param string $content Optional. Post content.
 * @param string $date    Optional. Post date.
 * @param string $type    Optional. Post type.
 * @param string $status  Optional. Post status.
 * @return int Post ID if post exists, 0 otherwise.
 */
function post_exists( $title, $content = '', $date = '', $type = '', $status = '' ) {
	global $wpdb;

	$post_title   = wp_unslash( sanitize_post_field( 'post_title', $title, 0, 'db' ) );
	$post_content = wp_unslash( sanitize_post_field( 'post_content', $content, 0, 'db' ) );
	$post_date    = wp_unslash( sanitize_post_field( 'post_date', $date, 0, 'db' ) );
	$post_type    = wp_unslash( sanitize_post_field( 'post_type', $type, 0, 'db' ) );
	$post_status  = wp_unslash( sanitize_post_field( 'post_status', $status, 0, 'db' ) );

	$query = "SELECT ID FROM $wpdb->posts WHERE 1=1";
	$args  = array();

	if ( ! empty( $date ) ) {
		$query .= ' AND post_date = %s';
		$args[] = $post_date;
	}

	if ( ! empty( $title ) ) {
		$query .= ' AND post_title = %s';
		$args[] = $post_title;
	}

	if ( ! empty( $content ) ) {
		$query .= ' AND post_content = %s';
		$args[] = $post_content;
	}

	if ( ! empty( $type ) ) {
		$query .= ' AND post_type = %s';
		$args[] = $post_type;
	}

	if ( ! empty( $status ) ) {
		$query .= ' AND post_status = %s';
		$args[] = $post_status;
	}

	if ( ! empty( $args ) ) {
		return (int) $wpdb->get_var( $wpdb->prepare( $query, $args ) );
	}

	return 0;
}

/**
 * Creates a new post from the "Write Post" form using `$_POST` information.
 *
 * @since 2.1.0
 *
 * @global WP_User $current_user
 *
 * @return int|WP_Error Post ID on success, WP_Error on failure.
 */
function wp_write_post() {
	if ( isset( $_POST['post_type'] ) ) {
		$ptype = get_post_type_object( $_POST['post_type'] );
	} else {
		$ptype = get_post_type_object( 'post' );
	}

	if ( ! current_user_can( $ptype->cap->edit_posts ) ) {
		if ( 'page' === $ptype->name ) {
			return new WP_Error( 'edit_pages', __( 'Sorry, you are not allowed to create pages on this site.' ) );
		} else {
			return new WP_Error( 'edit_posts', __( 'Sorry, you are not allowed to create posts or drafts on this site.' ) );
		}
	}

	$_POST['post_mime_type'] = '';

	// Clear out any data in internal vars.
	unset( $_POST['filter'] );

	// Edit, don't write, if we have a post ID.
	if ( isset( $_POST['post_ID'] ) ) {
		return edit_post();
	}

	if ( isset( $_POST['visibility'] ) ) {
		switch ( $_POST['visibility'] ) {
			case 'public':
				$_POST['post_password'] = '';
				break;
			case 'password':
				unset( $_POST['sticky'] );
				break;
			case 'private':
				$_POST['post_status']   = 'private';
				$_POST['post_password'] = '';
				unset( $_POST['sticky'] );
				break;
		}
	}

	$translated = _wp_translate_postdata( false );
	if ( is_wp_error( $translated ) ) {
		return $translated;
	}
	$translated = _wp_get_allowed_postdata( $translated );

	// Create the post.
	$post_id = wp_insert_post( $translated );
	if ( is_wp_error( $post_id ) ) {
		return $post_id;
	}

	if ( empty( $post_id ) ) {
		return 0;
	}

	add_meta( $post_id );

	add_post_meta( $post_id, '_edit_last', $GLOBALS['current_user']->ID );

	// Now that we have an ID we can fix any attachment anchor hrefs.
	_fix_attachment_links( $post_id );

	wp_set_post_lock( $post_id );

	return $post_id;
}

/**
 * Calls wp_write_post() and handles the errors.
 *
 * @since 2.0.0
 *
 * @return int|void Post ID on success, void on failure.
 */
function write_post() {
	$result = wp_write_post();
	if ( is_wp_error( $result ) ) {
		wp_die( $result->get_error_message() );
	} else {
		return $result;
	}
}

//
// Post Meta.
//

/**
 * Adds post meta data defined in the `$_POST` superglobal for a post with given ID.
 *
 * @since 1.2.0
 *
 * @param int $post_id
 * @return int|bool
 */
function add_meta( $post_id ) {
	$post_id = (int) $post_id;

	$metakeyselect = isset( $_POST['metakeyselect'] ) ? wp_unslash( trim( $_POST['metakeyselect'] ) ) : '';
	$metakeyinput  = isset( $_POST['metakeyinput'] ) ? wp_unslash( trim( $_POST['metakeyinput'] ) ) : '';
	$metavalue     = isset( $_POST['metavalue'] ) ? $_POST['metavalue'] : '';
	if ( is_string( $metavalue ) ) {
		$metavalue = trim( $metavalue );
	}

	if ( ( ( '#NONE#' !== $metakeyselect ) && ! empty( $metakeyselect ) ) || ! empty( $metakeyinput ) ) {
		/*
		 * We have a key/value pair. If both the select and the input
		 * for the key have data, the input takes precedence.
		 */
		if ( '#NONE#' !== $metakeyselect ) {
			$metakey = $metakeyselect;
		}

		if ( $metakeyinput ) {
			$metakey = $metakeyinput; // Default.
		}

		if ( is_protected_meta( $metakey, 'post' ) || ! current_user_can( 'add_post_meta', $post_id, $metakey ) ) {
			return false;
		}

		$metakey = wp_slash( $metakey );

		return add_post_meta( $post_id, $metakey, $metavalue );
	}

	return false;
}

/**
 * Deletes post meta data by meta ID.
 *
 * @since 1.2.0
 *
 * @param int $mid
 * @return bool
 */
function delete_meta( $mid ) {
	return delete_metadata_by_mid( 'post', $mid );
}

/**
 * Returns a list of previously defined keys.
 *
 * @since 1.2.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @return string[] Array of meta key names.
 */
function get_meta_keys() {
	global $wpdb;

	$keys = $wpdb->get_col(
		"SELECT meta_key
		FROM $wpdb->postmeta
		GROUP BY meta_key
		ORDER BY meta_key"
	);

	return $keys;
}

/**
 * Returns post meta data by meta ID.
 *
 * @since 2.1.0
 *
 * @param int $mid
 * @return object|bool
 */
function get_post_meta_by_id( $mid ) {
	return get_metadata_by_mid( 'post', $mid );
}

/**
 * Returns meta data for the given post ID.
 *
 * @since 1.2.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param int $post_id A post ID.
 * @return array[] {
 *     Array of meta data arrays for the given post ID.
 *
 *     @type array ...$0 {
 *         Associative array of meta data.
 *
 *         @type string $meta_key   Meta key.
 *         @type mixed  $meta_value Meta value.
 *         @type string $meta_id    Meta ID as a numeric string.
 *         @type string $post_id    Post ID as a numeric string.
 *     }
 * }
 */
function has_meta( $post_id ) {
	global $wpdb;

	return $wpdb->get_results(
		$wpdb->prepare(
			"SELECT meta_key, meta_value, meta_id, post_id
			FROM $wpdb->postmeta WHERE post_id = %d
			ORDER BY meta_key,meta_id",
			$post_id
		),
		ARRAY_A
	);
}

/**
 * Updates post meta data by meta ID.
 *
 * @since 1.2.0
 *
 * @param int    $meta_id    Meta ID.
 * @param string $meta_key   Meta key. Expect slashed.
 * @param string $meta_value Meta value. Expect slashed.
 * @return bool
 */
function update_meta( $meta_id, $meta_key, $meta_value ) {
	$meta_key   = wp_unslash( $meta_key );
	$meta_value = wp_unslash( $meta_value );

	return update_metadata_by_mid( 'post', $meta_id, $meta_value, $meta_key );
}

//
// Private.
//

/**
 * Replaces hrefs of attachment anchors with up-to-date permalinks.
 *
 * @since 2.3.0
 * @access private
 *
 * @param int|WP_Post $post Post ID or post object.
 * @return void|int|WP_Error Void if nothing fixed. 0 or WP_Error on update failure. The post ID on update success.
 */
function _fix_attachment_links( $post ) {
	$post    = get_post( $post, ARRAY_A );
	$content = $post['post_content'];

	// Don't run if no pretty permalinks or post is not published, scheduled, or privately published.
	if ( ! get_option( 'permalink_structure' ) || ! in_array( $post['post_status'], array( 'publish', 'future', 'private' ), true ) ) {
		return;
	}

	// Short if there aren't any links or no '?attachment_id=' strings (strpos cannot be zero).
	if ( ! strpos( $content, '?attachment_id=' ) || ! preg_match_all( '/<a ([^>]+)>[\s\S]+?<\/a>/', $content, $link_matches ) ) {
		return;
	}

	$site_url = get_bloginfo( 'url' );
	$site_url = substr( $site_url, (int) strpos( $site_url, '://' ) ); // Remove the http(s).
	$replace  = '';

	foreach ( $link_matches[1] as $key => $value ) {
		if ( ! strpos( $value, '?attachment_id=' ) || ! strpos( $value, 'wp-att-' )
			|| ! preg_match( '/href=(["\'])[^"\']*\?attachment_id=(\d+)[^"\']*\\1/', $value, $url_match )
			|| ! preg_match( '/rel=["\'][^"\']*wp-att-(\d+)/', $value, $rel_match ) ) {
				continue;
		}

		$quote  = $url_match[1]; // The quote (single or double).
		$url_id = (int) $url_match[2];
		$rel_id = (int) $rel_match[1];

		if ( ! $url_id || ! $rel_id || $url_id !== $rel_id || ! str_contains( $url_match[0], $site_url ) ) {
			continue;
		}

		$link    = $link_matches[0][ $key ];
		$replace = str_replace( $url_match[0], 'href=' . $quote . get_attachment_link( $url_id ) . $quote, $link );

		$content = str_replace( $link, $replace, $content );
	}

	if ( $replace ) {
		$post['post_content'] = $content;
		// Escape data pulled from DB.
		$post = add_magic_quotes( $post );

		return wp_update_post( $post );
	}
}

/**
 * Returns all the possible statuses for a post type.
 *
 * @since 2.5.0
 *
 * @param string $type The post_type you want the statuses for. Default 'post'.
 * @return string[] An array of all the statuses for the supplied post type.
 */
function get_available_post_statuses( $type = 'post' ) {
	$statuses = wp_count_posts( $type );

	return array_keys( get_object_vars( $statuses ) );
}

/**
 * Runs the query to fetch the posts for listing on the edit posts page.
 *
 * @since 2.5.0
 *
 * @param array|false $q Optional. Array of query variables to use to build the query.
 *                       Defaults to the `$_GET` superglobal.
 * @return string[] An array of all the statuses for the queried post type.
 */
function wp_edit_posts_query( $q = false ) {
	if ( false === $q ) {
		$q = $_GET;
	}

	$q['m']   = isset( $q['m'] ) ? (int) $q['m'] : 0;
	$q['cat'] = isset( $q['cat'] ) ? (int) $q['cat'] : 0;

	$post_statuses = get_post_stati();

	if ( isset( $q['post_type'] ) && in_array( $q['post_type'], get_post_types(), true ) ) {
		$post_type = $q['post_type'];
	} else {
		$post_type = 'post';
	}

	$avail_post_stati = get_available_post_statuses( $post_type );
	$post_status      = '';
	$perm             = '';

	if ( isset( $q['post_status'] ) && in_array( $q['post_status'], $post_statuses, true ) ) {
		$post_status = $q['post_status'];
		$perm        = 'readable';
	}

	$orderby = '';

	if ( isset( $q['orderby'] ) ) {
		$orderby = $q['orderby'];
	} elseif ( isset( $q['post_status'] ) && in_array( $q['post_status'], array( 'pending', 'draft' ), true ) ) {
		$orderby = 'modified';
	}

	$order = '';

	if ( isset( $q['order'] ) ) {
		$order = $q['order'];
	} elseif ( isset( $q['post_status'] ) && 'pending' === $q['post_status'] ) {
		$order = 'ASC';
	}

	$per_page       = "edit_{$post_type}_per_page";
	$posts_per_page = (int) get_user_option( $per_page );
	if ( empty( $posts_per_page ) || $posts_per_page < 1 ) {
		$posts_per_page = 20;
	}

	/**
	 * Filters the number of items per page to show for a specific 'per_page' type.
	 *
	 * The dynamic portion of the hook name, `$post_type`, refers to the post type.
	 *
	 * Possible hook names include:
	 *
	 *  - `edit_post_per_page`
	 *  - `edit_page_per_page`
	 *  - `edit_attachment_per_page`
	 *
	 * @since 3.0.0
	 *
	 * @param int $posts_per_page Number of posts to display per page for the given post
	 *                            type. Default 20.
	 */
	$posts_per_page = apply_filters( "edit_{$post_type}_per_page", $posts_per_page );

	/**
	 * Filters the number of posts displayed per page when specifically listing "posts".
	 *
	 * @since 2.8.0
	 *
	 * @param int    $posts_per_page Number of posts to be displayed. Default 20.
	 * @param string $post_type      The post type.
	 */
	$posts_per_page = apply_filters( 'edit_posts_per_page', $posts_per_page, $post_type );

	$query = compact( 'post_type', 'post_status', 'perm', 'order', 'orderby', 'posts_per_page' );

	// Hierarchical types require special args.
	if ( is_post_type_hierarchical( $post_type ) && empty( $orderby ) ) {
		$query['orderby']                = 'menu_order title';
		$query['order']                  = 'asc';
		$query['posts_per_page']         = -1;
		$query['posts_per_archive_page'] = -1;
		$query['fields']                 = 'id=>parent';
	}

	if ( ! empty( $q['show_sticky'] ) ) {
		$query['post__in'] = (array) get_option( 'sticky_posts' );
	}

	wp( $query );

	return $avail_post_stati;
}

/**
 * Returns the query variables for the current attachments request.
 *
 * @since 4.2.0
 *
 * @param array|false $q Optional. Array of query variables to use to build the query.
 *                       Defaults to the `$_GET` superglobal.
 * @return array The parsed query vars.
 */
function wp_edit_attachments_query_vars( $q = false ) {
	if ( false === $q ) {
		$q = $_GET;
	}
	$q['m']         = isset( $q['m'] ) ? (int) $q['m'] : 0;
	$q['cat']       = isset( $q['cat'] ) ? (int) $q['cat'] : 0;
	$q['post_type'] = 'attachment';
	$post_type      = get_post_type_object( 'attachment' );
	$states         = 'inherit';
	if ( current_user_can( $post_type->cap->read_private_posts ) ) {
		$states .= ',private';
	}

	$q['post_status'] = isset( $q['status'] ) && 'trash' === $q['status'] ? 'trash' : $states;
	$q['post_status'] = isset( $q['attachment-filter'] ) && 'trash' === $q['attachment-filter'] ? 'trash' : $states;

	$media_per_page = (int) get_user_option( 'upload_per_page' );
	if ( empty( $media_per_page ) || $media_per_page < 1 ) {
		$media_per_page = 20;
	}

	/**
	 * Filters the number of items to list per page when listing media items.
	 *
	 * @since 2.9.0
	 *
	 * @param int $media_per_page Number of media to list. Default 20.
	 */
	$q['posts_per_page'] = apply_filters( 'upload_per_page', $media_per_page );

	$post_mime_types = get_post_mime_types();
	if ( isset( $q['post_mime_type'] ) && ! array_intersect( (array) $q['post_mime_type'], array_keys( $post_mime_types ) ) ) {
		unset( $q['post_mime_type'] );
	}

	foreach ( array_keys( $post_mime_types ) as $type ) {
		if ( isset( $q['attachment-filter'] ) && "post_mime_type:$type" === $q['attachment-filter'] ) {
			$q['post_mime_type'] = $type;
			break;
		}
	}

	if ( isset( $q['detached'] ) || ( isset( $q['attachment-filter'] ) && 'detached' === $q['attachment-filter'] ) ) {
		$q['post_parent'] = 0;
	}

	if ( isset( $q['mine'] ) || ( isset( $q['attachment-filter'] ) && 'mine' === $q['attachment-filter'] ) ) {
		$q['author'] = get_current_user_id();
	}

	// Filter query clauses to include filenames.
	if ( isset( $q['s'] ) ) {
		add_filter( 'wp_allow_query_attachment_by_filename', '__return_true' );
	}

	return $q;
}

/**
 * Executes a query for attachments. An array of WP_Query arguments
 * can be passed in, which will override the arguments set by this function.
 *
 * @since 2.5.0
 *
 * @param array|false $q Optional. Array of query variables to use to build the query.
 *                       Defaults to the `$_GET` superglobal.
 * @return array {
 *     Array containing the post mime types and available post mime types.
 *
 *     @type array[]  $post_mime_types       Post mime types.
 *     @type string[] $avail_post_mime_types Available post mime types.
 * }
 */
function wp_edit_attachments_query( $q = false ) {
	wp( wp_edit_attachments_query_vars( $q ) );

	$post_mime_types       = get_post_mime_types();
	$avail_post_mime_types = get_available_post_mime_types( 'attachment' );

	return array( $post_mime_types, $avail_post_mime_types );
}

/**
 * Returns the list of classes to be used by a meta box.
 *
 * @since 2.5.0
 *
 * @param string $box_id    Meta box ID (used in the 'id' attribute for the meta box).
 * @param string $screen_id The screen on which the meta box is shown.
 * @return string Space-separated string of class names.
 */
function postbox_classes( $box_id, $screen_id ) {
	if ( isset( $_GET['edit'] ) && $_GET['edit'] === $box_id ) {
		$classes = array( '' );
	} elseif ( get_user_option( 'closedpostboxes_' . $screen_id ) ) {
		$closed = get_user_option( 'closedpostboxes_' . $screen_id );
		if ( ! is_array( $closed ) ) {
			$classes = array( '' );
		} else {
			$classes = in_array( $box_id, $closed, true ) ? array( 'closed' ) : array( '' );
		}
	} else {
		$classes = array( '' );
	}

	/**
	 * Filters the postbox classes for a specific screen and box ID combo.
	 *
	 * The dynamic portions of the hook name, `$screen_id` and `$box_id`, refer to
	 * the screen ID and meta box ID, respectively.
	 *
	 * @since 3.2.0
	 *
	 * @param string[] $classes An array of postbox classes.
	 */
	$classes = apply_filters( "postbox_classes_{$screen_id}_{$box_id}", $classes );

	return implode( ' ', $classes );
}

/**
 * Returns a sample permalink based on the post name.
 *
 * @since 2.5.0
 *
 * @param int|WP_Post $post  Post ID or post object.
 * @param string|null $title Optional. Title to override the post's current title
 *                           when generating the post name. Default null.
 * @param string|null $name  Optional. Name to override the post name. Default null.
 * @return array {
 *     Array containing the sample permalink with placeholder for the post name, and the post name.
 *
 *     @type string $0 The permalink with placeholder for the post name.
 *     @type string $1 The post name.
 * }
 */
function get_sample_permalink( $post, $title = null, $name = null ) {
	$post = get_post( $post );

	if ( ! $post ) {
		return array( '', '' );
	}

	$ptype = get_post_type_object( $post->post_type );

	$original_status = $post->post_status;
	$original_date   = $post->post_date;
	$original_name   = $post->post_name;
	$original_filter = $post->filter;

	// Hack: get_permalink() would return plain permalink for drafts, so we will fake that our post is published.
	if ( in_array( $post->post_status, array( 'auto-draft', 'draft', 'pending', 'future' ), true ) ) {
		$post->post_status = 'publish';
		$post->post_name   = sanitize_title( $post->post_name ? $post->post_name : $post->post_title, $post->ID );
	}

	/*
	 * If the user wants to set a new name -- override the current one.
	 * Note: if empty name is supplied -- use the title instead, see #6072.
	 */
	if ( ! is_null( $name ) ) {
		$post->post_name = sanitize_title( $name ? $name : $title, $post->ID );
	}

	$post->post_name = wp_unique_post_slug( $post->post_name, $post->ID, $post->post_status, $post->post_type, $post->post_parent );

	$post->filter = 'sample';

	$permalink = get_permalink( $post, true );

	// Replace custom post_type token with generic pagename token for ease of use.
	$permalink = str_replace( "%$post->post_type%", '%pagename%', $permalink );

	// Handle page hierarchy.
	if ( $ptype->hierarchical ) {
		$uri = get_page_uri( $post );
		if ( $uri ) {
			$uri = untrailingslashit( $uri );
			$uri = strrev( stristr( strrev( $uri ), '/' ) );
			$uri = untrailingslashit( $uri );
		}

		/** This filter is documented in wp-admin/edit-tag-form.php */
		$uri = apply_filters( 'editable_slug', $uri, $post );
		if ( ! empty( $uri ) ) {
			$uri .= '/';
		}
		$permalink = str_replace( '%pagename%', "{$uri}%pagename%", $permalink );
	}

	/** This filter is documented in wp-admin/edit-tag-form.php */
	$permalink         = array( $permalink, apply_filters( 'editable_slug', $post->post_name, $post ) );
	$post->post_status = $original_status;
	$post->post_date   = $original_date;
	$post->post_name   = $original_name;
	$post->filter      = $original_filter;

	/**
	 * Filters the sample permalink.
	 *
	 * @since 4.4.0
	 *
	 * @param array   $permalink {
	 *     Array containing the sample permalink with placeholder for the post name, and the post name.
	 *
	 *     @type string $0 The permalink with placeholder for the post name.
	 *     @type string $1 The post name.
	 * }
	 * @param int     $post_id Post ID.
	 * @param string  $title   Post title.
	 * @param string  $name    Post name (slug).
	 * @param WP_Post $post    Post object.
	 */
	return apply_filters( 'get_sample_permalink', $permalink, $post->ID, $title, $name, $post );
}

/**
 * Returns the HTML of the sample permalink slug editor.
 *
 * @since 2.5.0
 *
 * @param int|WP_Post $post      Post ID or post object.
 * @param string|null $new_title Optional. New title. Default null.
 * @param string|null $new_slug  Optional. New slug. Default null.
 * @return string The HTML of the sample permalink slug editor.
 */
function get_sample_permalink_html( $post, $new_title = null, $new_slug = null ) {
	$post = get_post( $post );

	if ( ! $post ) {
		return '';
	}

	list($permalink, $post_name) = get_sample_permalink( $post->ID, $new_title, $new_slug );

	$view_link      = false;
	$preview_target = '';

	if ( current_user_can( 'read_post', $post->ID ) ) {
		if ( 'draft' === $post->post_status || empty( $post->post_name ) ) {
			$view_link      = get_preview_post_link( $post );
			$preview_target = " target='wp-preview-{$post->ID}'";
		} else {
			if ( 'publish' === $post->post_status || 'attachment' === $post->post_type ) {
				$view_link = get_permalink( $post );
			} else {
				// Allow non-published (private, future) to be viewed at a pretty permalink, in case $post->post_name is set.
				$view_link = str_replace( array( '%pagename%', '%postname%' ), $post->post_name, $permalink );
			}
		}
	}

	// Permalinks without a post/page name placeholder don't have anything to edit.
	if ( ! str_contains( $permalink, '%postname%' ) && ! str_contains( $permalink, '%pagename%' ) ) {
		$return = '<strong>' . __( 'Permalink:' ) . "</strong>\n";

		if ( false !== $view_link ) {
			$display_link = urldecode( $view_link );
			$return      .= '<a id="sample-permalink" href="' . esc_url( $view_link ) . '"' . $preview_target . '>' . esc_html( $display_link ) . "</a>\n";
		} else {
			$return .= '<span id="sample-permalink">' . $permalink . "</span>\n";
		}

		// Encourage a pretty permalink setting.
		if ( ! get_option( 'permalink_structure' ) && current_user_can( 'manage_options' )
			&& ! ( 'page' === get_option( 'show_on_front' ) && (int) get_option( 'page_on_front' ) === $post->ID )
		) {
			$return .= '<span id="change-permalinks"><a href="options-permalink.php" class="button button-small">' . __( 'Change Permalink Structure' ) . "</a></span>\n";
		}
	} else {
		if ( mb_strlen( $post_name ) > 34 ) {
			$post_name_abridged = mb_substr( $post_name, 0, 16 ) . '&hellip;' . mb_substr( $post_name, -16 );
		} else {
			$post_name_abridged = $post_name;
		}

		$post_name_html = '<span id="editable-post-name">' . esc_html( $post_name_abridged ) . '</span>';
		$display_link   = str_replace( array( '%pagename%', '%postname%' ), $post_name_html, esc_html( urldecode( $permalink ) ) );

		$return  = '<strong>' . __( 'Permalink:' ) . "</strong>\n";
		$return .= '<span id="sample-permalink"><a href="' . esc_url( $view_link ) . '"' . $preview_target . '>' . $display_link . "</a></span>\n";
		$return .= '&lrm;'; // Fix bi-directional text display defect in RTL languages.
		$return .= '<span id="edit-slug-buttons"><button type="button" class="edit-slug button button-small hide-if-no-js" aria-label="' . __( 'Edit permalink' ) . '">' . __( 'Edit' ) . "</button></span>\n";
		$return .= '<span id="editable-post-name-full">' . esc_html( $post_name ) . "</span>\n";
	}

	/**
	 * Filters the sample permalink HTML markup.
	 *
	 * @since 2.9.0
	 * @since 4.4.0 Added `$post` parameter.
	 *
	 * @param string      $return    Sample permalink HTML markup.
	 * @param int         $post_id   Post ID.
	 * @param string|null $new_title New sample permalink title.
	 * @param string|null $new_slug  New sample permalink slug.
	 * @param WP_Post     $post      Post object.
	 */
	$return = apply_filters( 'get_sample_permalink_html', $return, $post->ID, $new_title, $new_slug, $post );

	return $return;
}

/**
 * Returns HTML for the post thumbnail meta box.
 *
 * @since 2.9.0
 *
 * @param int|null         $thumbnail_id Optional. Thumbnail attachment ID. Default null.
 * @param int|WP_Post|null $post         Optional. The post ID or object associated
 *                                       with the thumbnail. Defaults to global $post.
 * @return string The post thumbnail HTML.
 */
function _wp_post_thumbnail_html( $thumbnail_id = null, $post = null ) {
	$_wp_additional_image_sizes = wp_get_additional_image_sizes();

	$post               = get_post( $post );
	$post_type_object   = get_post_type_object( $post->post_type );
	$set_thumbnail_link = '<p class="hide-if-no-js"><a href="%s" id="set-post-thumbnail"%s class="thickbox">%s</a></p>';
	$upload_iframe_src  = get_upload_iframe_src( 'image', $post->ID );

	$content = sprintf(
		$set_thumbnail_link,
		esc_url( $upload_iframe_src ),
		'', // Empty when there's no featured image set, `aria-describedby` attribute otherwise.
		esc_html( $post_type_object->labels->set_featured_image )
	);

	if ( $thumbnail_id && get_post( $thumbnail_id ) ) {
		$size = isset( $_wp_additional_image_sizes['post-thumbnail'] ) ? 'post-thumbnail' : array( 266, 266 );

		/**
		 * Filters the size used to display the post thumbnail image in the 'Featured image' meta box.
		 *
		 * Note: When a theme adds 'post-thumbnail' support, a special 'post-thumbnail'
		 * image size is registered, which differs from the 'thumbnail' image size
		 * managed via the Settings > Media screen.
		 *
		 * @since 4.4.0
		 *
		 * @param string|int[] $size         Requested image size. Can be any registered image size name, or
		 *                                   an array of width and height values in pixels (in that order).
		 * @param int          $thumbnail_id Post thumbnail attachment ID.
		 * @param WP_Post      $post         The post object associated with the thumbnail.
		 */
		$size = apply_filters( 'admin_post_thumbnail_size', $size, $thumbnail_id, $post );

		$thumbnail_html = wp_get_attachment_image( $thumbnail_id, $size );

		if ( ! empty( $thumbnail_html ) ) {
			$content  = sprintf(
				$set_thumbnail_link,
				esc_url( $upload_iframe_src ),
				' aria-describedby="set-post-thumbnail-desc"',
				$thumbnail_html
			);
			$content .= '<p class="hide-if-no-js howto" id="set-post-thumbnail-desc">' . __( 'Click the image to edit or update' ) . '</p>';
			$content .= '<p class="hide-if-no-js"><a href="#" id="remove-post-thumbnail">' . esc_html( $post_type_object->labels->remove_featured_image ) . '</a></p>';
		}
	}

	$content .= '<input type="hidden" id="_thumbnail_id" name="_thumbnail_id" value="' . esc_attr( $thumbnail_id ? $thumbnail_id : '-1' ) . '" />';

	/**
	 * Filters the admin post thumbnail HTML markup to return.
	 *
	 * @since 2.9.0
	 * @since 3.5.0 Added the `$post_id` parameter.
	 * @since 4.6.0 Added the `$thumbnail_id` parameter.
	 *
	 * @param string   $content      Admin post thumbnail HTML markup.
	 * @param int      $post_id      Post ID.
	 * @param int|null $thumbnail_id Thumbnail attachment ID, or null if there isn't one.
	 */
	return apply_filters( 'admin_post_thumbnail_html', $content, $post->ID, $thumbnail_id );
}

/**
 * Determines whether the post is currently being edited by another user.
 *
 * @since 2.5.0
 *
 * @param int|WP_Post $post ID or object of the post to check for editing.
 * @return int|false ID of the user with lock. False if the post does not exist, post is not locked,
 *                   the user with lock does not exist, or the post is locked by current user.
 */
function wp_check_post_lock( $post ) {
	$post = get_post( $post );

	if ( ! $post ) {
		return false;
	}

	$lock = get_post_meta( $post->ID, '_edit_lock', true );

	if ( ! $lock ) {
		return false;
	}

	$lock = explode( ':', $lock );
	$time = $lock[0];
	$user = isset( $lock[1] ) ? (int) $lock[1] : (int) get_post_meta( $post->ID, '_edit_last', true );

	if ( ! get_userdata( $user ) ) {
		return false;
	}

	/** This filter is documented in wp-admin/includes/ajax-actions.php */
	$time_window = apply_filters( 'wp_check_post_lock_window', 150 );

	if ( $time && $time > time() - $time_window && get_current_user_id() !== $user ) {
		return $user;
	}

	return false;
}

/**
 * Marks the post as currently being edited by the current user.
 *
 * @since 2.5.0
 *
 * @param int|WP_Post $post ID or object of the post being edited.
 * @return array|false {
 *     Array of the lock time and user ID. False if the post does not exist, or there
 *     is no current user.
 *
 *     @type int $0 The current time as a Unix timestamp.
 *     @type int $1 The ID of the current user.
 * }
 */
function wp_set_post_lock( $post ) {
	$post = get_post( $post );

	if ( ! $post ) {
		return false;
	}

	$user_id = get_current_user_id();

	if ( 0 === $user_id ) {
		return false;
	}

	$now  = time();
	$lock = "$now:$user_id";

	update_post_meta( $post->ID, '_edit_lock', $lock );

	return array( $now, $user_id );
}

/**
 * Outputs the HTML for the notice to say that someone else is editing or has taken over editing of this post.
 *
 * @since 2.8.5
 */
function _admin_notice_post_locked() {
	$post = get_post();

	if ( ! $post ) {
		return;
	}

	$user    = null;
	$user_id = wp_check_post_lock( $post->ID );

	if ( $user_id ) {
		$user = get_userdata( $user_id );
	}

	if ( $user ) {
		/**
		 * Filters whether to show the post locked dialog.
		 *
		 * Returning false from the filter will prevent the dialog from being displayed.
		 *
		 * @since 3.6.0
		 *
		 * @param bool    $display Whether to display the dialog. Default true.
		 * @param WP_Post $post    Post object.
		 * @param WP_User $user    The user with the lock for the post.
		 */
		if ( ! apply_filters( 'show_post_locked_dialog', true, $post, $user ) ) {
			return;
		}

		$locked = true;
	} else {
		$locked = false;
	}

	$sendback      = wp_get_referer();
	$sendback_text = __( 'Go back' );

	if ( ! $locked || ! $sendback || str_contains( $sendback, 'post.php' ) || str_contains( $sendback, 'post-new.php' ) ) {
		$sendback = admin_url( 'edit.php' );

		if ( 'post' !== $post->post_type ) {
			$sendback = add_query_arg( 'post_type', $post->post_type, $sendback );
		}

		$post_type_object = get_post_type_object( $post->post_type );

		if ( $post_type_object ) {
			$sendback_text = $post_type_object->labels->all_items;
		}
	}

	$hidden = $locked ? '' : ' hidden';

	?>
	<div id="post-lock-dialog" class="notification-dialog-wrap<?php echo $hidden; ?>">
	<div class="notification-dialog-background"></div>
	<div class="notification-dialog">
	<?php

	if ( $locked ) {
		$query_args = array();
		if ( get_post_type_object( $post->post_type )->public ) {
			if ( 'publish' === $post->post_status || $user->ID !== (int) $post->post_author ) {
				// Latest content is in autosave.
				$nonce                       = wp_create_nonce( 'post_preview_' . $post->ID );
				$query_args['preview_id']    = $post->ID;
				$query_args['preview_nonce'] = $nonce;
			}
		}

		$preview_link = get_preview_post_link( $post->ID, $query_args );

		/**
		 * Filters whether to allow the post lock to be overridden.
		 *
		 * Returning false from the filter will disable the ability
		 * to override the post lock.
		 *
		 * @since 3.6.0
		 *
		 * @param bool    $override Whether to allow the post lock to be overridden. Default true.
		 * @param WP_Post $post     Post object.
		 * @param WP_User $user     The user with the lock for the post.
		 */
		$override = apply_filters( 'override_post_lock', true, $post, $user );
		$tab_last = $override ? '' : ' wp-tab-last';

		?>
		<div class="post-locked-message">
		<div class="post-locked-avatar"><?php echo get_avatar( $user->ID, 64 ); ?></div>
		<p class="currently-editing wp-tab-first" tabindex="0">
		<?php
		if ( $override ) {
			/* translators: %s: User's display name. */
			printf( __( '%s is currently editing this post. Do you want to take over?' ), esc_html( $user->display_name ) );
		} else {
			/* translators: %s: User's display name. */
			printf( __( '%s is currently editing this post.' ), esc_html( $user->display_name ) );
		}
		?>
		</p>
		<?php
		/**
		 * Fires inside the post locked dialog before the buttons are displayed.
		 *
		 * @since 3.6.0
		 * @since 5.4.0 The $user parameter was added.
		 *
		 * @param WP_Post $post Post object.
		 * @param WP_User $user The user with the lock for the post.
		 */
		do_action( 'post_locked_dialog', $post, $user );
		?>
		<p>
		<a class="button" href="<?php echo esc_url( $sendback ); ?>"><?php echo $sendback_text; ?></a>
		<?php if ( $preview_link ) { ?>
		<a class="button<?php echo $tab_last; ?>" href="<?php echo esc_url( $preview_link ); ?>"><?php _e( 'Preview' ); ?></a>
			<?php
		}

		// Allow plugins to prevent some users overriding the post lock.
		if ( $override ) {
			?>
	<a class="button button-primary wp-tab-last" href="<?php echo esc_url( add_query_arg( 'get-post-lock', '1', wp_nonce_url( get_edit_post_link( $post->ID, 'url' ), 'lock-post_' . $post->ID ) ) ); ?>"><?php _e( 'Take over' ); ?></a>
			<?php
		}

		?>
		</p>
		</div>
		<?php
	} else {
		?>
		<div class="post-taken-over">
			<div class="post-locked-avatar"></div>
			<p class="wp-tab-first" tabindex="0">
			<span class="currently-editing"></span><br />
			<span class="locked-saving hidden"><img src="<?php echo esc_url( admin_url( 'images/spinner-2x.gif' ) ); ?>" width="16" height="16" alt="" /> <?php _e( 'Saving revision&hellip;' ); ?></span>
			<span class="locked-saved hidden"><?php _e( 'Your latest changes were saved as a revision.' ); ?></span>
			</p>
			<?php
			/**
			 * Fires inside the dialog displayed when a user has lost the post lock.
			 *
			 * @since 3.6.0
			 *
			 * @param WP_Post $post Post object.
			 */
			do_action( 'post_lock_lost_dialog', $post );
			?>
			<p><a class="button button-primary wp-tab-last" href="<?php echo esc_url( $sendback ); ?>"><?php echo $sendback_text; ?></a></p>
		</div>
		<?php
	}

	?>
	</div>
	</div>
	<?php
}

/**
 * Creates autosave data for the specified post from `$_POST` data.
 *
 * @since 2.6.0
 *
 * @param array|int $post_data Associative array containing the post data, or integer post ID.
 *                             If a numeric post ID is provided, will use the `$_POST` superglobal.
 * @return int|WP_Error The autosave revision ID. WP_Error or 0 on error.
 */
function wp_create_post_autosave( $post_data ) {
	if ( is_numeric( $post_data ) ) {
		$post_id   = $post_data;
		$post_data = $_POST;
	} else {
		$post_id = (int) $post_data['post_ID'];
	}

	$post_data = _wp_translate_postdata( true, $post_data );
	if ( is_wp_error( $post_data ) ) {
		return $post_data;
	}
	$post_data = _wp_get_allowed_postdata( $post_data );

	$post_author = get_current_user_id();

	// Store one autosave per author. If there is already an autosave, overwrite it.
	$old_autosave = wp_get_post_autosave( $post_id, $post_author );
	if ( $old_autosave ) {
		$new_autosave                = _wp_post_revision_data( $post_data, true );
		$new_autosave['ID']          = $old_autosave->ID;
		$new_autosave['post_author'] = $post_author;

		$post = get_post( $post_id );

		// If the new autosave has the same content as the post, delete the autosave.
		$autosave_is_different = false;
		foreach ( array_intersect( array_keys( $new_autosave ), array_keys( _wp_post_revision_fields( $post ) ) ) as $field ) {
			if ( normalize_whitespace( $new_autosave[ $field ] ) !== normalize_whitespace( $post->$field ) ) {
				$autosave_is_different = true;
				break;
			}
		}

		if ( ! $autosave_is_different ) {
			wp_delete_post_revision( $old_autosave->ID );
			return 0;
		}

		/**
		 * Fires before an autosave is stored.
		 *
		 * @since 4.1.0
		 * @since 6.4.0 The `$is_update` parameter was added to indicate if the autosave is being updated or was newly created.
		 *
		 * @param array $new_autosave Post array - the autosave that is about to be saved.
		 * @param bool  $is_update    Whether this is an existing autosave.
		 */
		do_action( 'wp_creating_autosave', $new_autosave, true );
		return wp_update_post( $new_autosave );
	}

	// _wp_put_post_revision() expects unescaped.
	$post_data = wp_unslash( $post_data );

	// Otherwise create the new autosave as a special post revision.
	$revision = _wp_put_post_revision( $post_data, true );

	if ( ! is_wp_error( $revision ) && 0 !== $revision ) {

		/** This action is documented in wp-admin/includes/post.php */
		do_action( 'wp_creating_autosave', get_post( $revision, ARRAY_A ), false );
	}

	return $revision;
}

/**
 * Autosaves the revisioned meta fields.
 *
 * Iterates through the revisioned meta fields and checks each to see if they are set,
 * and have a changed value. If so, the meta value is saved and attached to the autosave.
 *
 * @since 6.4.0
 *
 * @param array $new_autosave The new post data being autosaved.
 */
function wp_autosave_post_revisioned_meta_fields( $new_autosave ) {
	/*
	 * The post data arrives as either $_POST['data']['wp_autosave'] or the $_POST
	 * itself. This sets $posted_data to the correct variable.
	 *
	 * Ignoring sanitization to avoid altering meta. Ignoring the nonce check because
	 * this is hooked on inner core hooks where a valid nonce was already checked.
	 */
	$posted_data = isset( $_POST['data']['wp_autosave'] ) ? $_POST['data']['wp_autosave'] : $_POST;

	$post_type = get_post_type( $new_autosave['post_parent'] );

	/*
	 * Go through the revisioned meta keys and save them as part of the autosave,
	 * if the meta key is part of the posted data, the meta value is not blank,
	 * and the meta value has changes from the last autosaved value.
	 */
	foreach ( wp_post_revision_meta_keys( $post_type ) as $meta_key ) {

		if ( isset( $posted_data[ $meta_key ] )
			&& get_post_meta( $new_autosave['ID'], $meta_key, true ) !== wp_unslash( $posted_data[ $meta_key ] )
		) {
			/*
			 * Use the underlying delete_metadata() and add_metadata() functions
			 * vs delete_post_meta() and add_post_meta() to make sure we're working
			 * with the actual revision meta.
			 */
			delete_metadata( 'post', $new_autosave['ID'], $meta_key );

			// One last check to ensure meta value is not empty.
			if ( ! empty( $posted_data[ $meta_key ] ) ) {
				// Add the revisions meta data to the autosave.
				add_metadata( 'post', $new_autosave['ID'], $meta_key, $posted_data[ $meta_key ] );
			}
		}
	}
}

/**
 * Saves a draft or manually autosaves for the purpose of showing a post preview.
 *
 * @since 2.7.0
 *
 * @return string URL to redirect to show the preview.
 */
function post_preview() {

	$post_id     = (int) $_POST['post_ID'];
	$_POST['ID'] = $post_id;

	$post = get_post( $post_id );

	if ( ! $post ) {
		wp_die( __( 'Sorry, you are not allowed to edit this post.' ) );
	}

	if ( ! current_user_can( 'edit_post', $post->ID ) ) {
		wp_die( __( 'Sorry, you are not allowed to edit this post.' ) );
	}

	$is_autosave = false;

	if ( ! wp_check_post_lock( $post->ID ) && get_current_user_id() === (int) $post->post_author
		&& ( 'draft' === $post->post_status || 'auto-draft' === $post->post_status )
	) {
		$saved_post_id = edit_post();
	} else {
		$is_autosave = true;

		if ( isset( $_POST['post_status'] ) && 'auto-draft' === $_POST['post_status'] ) {
			$_POST['post_status'] = 'draft';
		}

		$saved_post_id = wp_create_post_autosave( $post->ID );
	}

	if ( is_wp_error( $saved_post_id ) ) {
		wp_die( $saved_post_id->get_error_message() );
	}

	$query_args = array();

	if ( $is_autosave && $saved_post_id ) {
		$query_args['preview_id']    = $post->ID;
		$query_args['preview_nonce'] = wp_create_nonce( 'post_preview_' . $post->ID );

		if ( isset( $_POST['post_format'] ) ) {
			$query_args['post_format'] = empty( $_POST['post_format'] ) ? 'standard' : sanitize_key( $_POST['post_format'] );
		}

		if ( isset( $_POST['_thumbnail_id'] ) ) {
			$query_args['_thumbnail_id'] = ( (int) $_POST['_thumbnail_id'] <= 0 ) ? '-1' : (int) $_POST['_thumbnail_id'];
		}
	}

	return get_preview_post_link( $post, $query_args );
}

/**
 * Saves a post submitted with XHR.
 *
 * Intended for use with heartbeat and autosave.js
 *
 * @since 3.9.0
 *
 * @param array $post_data Associative array of the submitted post data.
 * @return mixed The value 0 or WP_Error on failure. The saved post ID on success.
 *               The ID can be the draft post_id or the autosave revision post_id.
 */
function wp_autosave( $post_data ) {
	// Back-compat.
	if ( ! defined( 'DOING_AUTOSAVE' ) ) {
		define( 'DOING_AUTOSAVE', true );
	}

	$post_id              = (int) $post_data['post_id'];
	$post_data['ID']      = $post_id;
	$post_data['post_ID'] = $post_id;

	if ( false === wp_verify_nonce( $post_data['_wpnonce'], 'update-post_' . $post_id ) ) {
		return new WP_Error( 'invalid_nonce', __( 'Error while saving.' ) );
	}

	$post = get_post( $post_id );

	if ( ! current_user_can( 'edit_post', $post->ID ) ) {
		return new WP_Error( 'edit_posts', __( 'Sorry, you are not allowed to edit this item.' ) );
	}

	if ( 'auto-draft' === $post->post_status ) {
		$post_data['post_status'] = 'draft';
	}

	if ( 'page' !== $post_data['post_type'] && ! empty( $post_data['catslist'] ) ) {
		$post_data['post_category'] = explode( ',', $post_data['catslist'] );
	}

	if ( ! wp_check_post_lock( $post->ID ) && get_current_user_id() === (int) $post->post_author
		&& ( 'auto-draft' === $post->post_status || 'draft' === $post->post_status )
	) {
		// Drafts and auto-drafts are just overwritten by autosave for the same user if the post is not locked.
		return edit_post( wp_slash( $post_data ) );
	} else {
		/*
		 * Non-drafts or other users' drafts are not overwritten.
		 * The autosave is stored in a special post revision for each user.
		 */
		return wp_create_post_autosave( wp_slash( $post_data ) );
	}
}

/**
 * Redirects to previous page.
 *
 * @since 2.7.0
 *
 * @param int $post_id Optional. Post ID.
 */
function redirect_post( $post_id = '' ) {
	if ( isset( $_POST['save'] ) || isset( $_POST['publish'] ) ) {
		$status = get_post_status( $post_id );

		switch ( $status ) {
			case 'pending':
				$message = 8;
				break;
			case 'future':
				$message = 9;
				break;
			case 'draft':
				$message = 10;
				break;
			default:
				$message = isset( $_POST['publish'] ) ? 6 : 1;
				break;
		}

		$location = add_query_arg( 'message', $message, get_edit_post_link( $post_id, 'url' ) );
	} elseif ( isset( $_POST['addmeta'] ) && $_POST['addmeta'] ) {
		$location = add_query_arg( 'message', 2, wp_get_referer() );
		$location = explode( '#', $location );
		$location = $location[0] . '#postcustom';
	} elseif ( isset( $_POST['deletemeta'] ) && $_POST['deletemeta'] ) {
		$location = add_query_arg( 'message', 3, wp_get_referer() );
		$location = explode( '#', $location );
		$location = $location[0] . '#postcustom';
	} else {
		$location = add_query_arg( 'message', 4, get_edit_post_link( $post_id, 'url' ) );
	}

	/**
	 * Filters the post redirect destination URL.
	 *
	 * @since 2.9.0
	 *
	 * @param string $location The destination URL.
	 * @param int    $post_id  The post ID.
	 */
	wp_redirect( apply_filters( 'redirect_post_location', $location, $post_id ) );
	exit;
}

/**
 * Sanitizes POST values from a checkbox taxonomy metabox.
 *
 * @since 5.1.0
 *
 * @param string $taxonomy The taxonomy name.
 * @param array  $terms    Raw term data from the 'tax_input' field.
 * @return int[] Array of sanitized term IDs.
 */
function taxonomy_meta_box_sanitize_cb_checkboxes( $taxonomy, $terms ) {
	return array_map( 'intval', $terms );
}

/**
 * Sanitizes POST values from an input taxonomy metabox.
 *
 * @since 5.1.0
 *
 * @param string       $taxonomy The taxonomy name.
 * @param array|string $terms    Raw term data from the 'tax_input' field.
 * @return array
 */
function taxonomy_meta_box_sanitize_cb_input( $taxonomy, $terms ) {
	/*
	 * Assume that a 'tax_input' string is a comma-separated list of term names.
	 * Some languages may use a character other than a comma as a delimiter, so we standardize on
	 * commas before parsing the list.
	 */
	if ( ! is_array( $terms ) ) {
		$comma = _x( ',', 'tag delimiter' );
		if ( ',' !== $comma ) {
			$terms = str_replace( $comma, ',', $terms );
		}
		$terms = explode( ',', trim( $terms, " \n\t\r\0\x0B," ) );
	}

	$clean_terms = array();
	foreach ( $terms as $term ) {
		// Empty terms are invalid input.
		if ( empty( $term ) ) {
			continue;
		}

		$_term = get_terms(
			array(
				'taxonomy'   => $taxonomy,
				'name'       => $term,
				'fields'     => 'ids',
				'hide_empty' => false,
			)
		);

		if ( ! empty( $_term ) ) {
			$clean_terms[] = (int) $_term[0];
		} else {
			// No existing term was found, so pass the string. A new term will be created.
			$clean_terms[] = $term;
		}
	}

	return $clean_terms;
}

/**
 * Prepares server-registered blocks for the block editor.
 *
 * Returns an associative array of registered block data keyed by block name. Data includes properties
 * of a block relevant for client registration.
 *
 * @since 5.0.0
 * @since 6.3.0 Added `selectors` field.
 * @since 6.4.0 Added `block_hooks` field.
 *
 * @return array An associative array of registered block data.
 */
function get_block_editor_server_block_settings() {
	$block_registry = WP_Block_Type_Registry::get_instance();
	$blocks         = array();
	$fields_to_pick = array(
		'api_version'      => 'apiVersion',
		'title'            => 'title',
		'description'      => 'description',
		'icon'             => 'icon',
		'attributes'       => 'attributes',
		'provides_context' => 'providesContext',
		'uses_context'     => 'usesContext',
		'block_hooks'      => 'blockHooks',
		'selectors'        => 'selectors',
		'supports'         => 'supports',
		'category'         => 'category',
		'styles'           => 'styles',
		'textdomain'       => 'textdomain',
		'parent'           => 'parent',
		'ancestor'         => 'ancestor',
		'keywords'         => 'keywords',
		'example'          => 'example',
		'variations'       => 'variations',
		'allowed_blocks'   => 'allowedBlocks',
	);

	foreach ( $block_registry->get_all_registered() as $block_name => $block_type ) {
		foreach ( $fields_to_pick as $field => $key ) {
			if ( ! isset( $block_type->{ $field } ) ) {
				continue;
			}

			if ( ! isset( $blocks[ $block_name ] ) ) {
				$blocks[ $block_name ] = array();
			}

			$blocks[ $block_name ][ $key ] = $block_type->{ $field };
		}
	}

	return $blocks;
}

/**
 * Renders the meta boxes forms.
 *
 * @since 5.0.0
 *
 * @global WP_Post   $post           Global post object.
 * @global WP_Screen $current_screen WordPress current screen object.
 * @global array     $wp_meta_boxes  Global meta box state.
 */
function the_block_editor_meta_boxes() {
	global $post, $current_screen, $wp_meta_boxes;

	// Handle meta box state.
	$_original_meta_boxes = $wp_meta_boxes;

	/**
	 * Fires right before the meta boxes are rendered.
	 *
	 * This allows for the filtering of meta box data, that should already be
	 * present by this point. Do not use as a means of adding meta box data.
	 *
	 * @since 5.0.0
	 *
	 * @param array $wp_meta_boxes Global meta box state.
	 */
	$wp_meta_boxes = apply_filters( 'filter_block_editor_meta_boxes', $wp_meta_boxes );
	$locations     = array( 'side', 'normal', 'advanced' );
	$priorities    = array( 'high', 'sorted', 'core', 'default', 'low' );

	// Render meta boxes.
	?>
	<form class="metabox-base-form">
	<?php the_block_editor_meta_box_post_form_hidden_fields( $post ); ?>
	</form>
	<form id="toggle-custom-fields-form" method="post" action="<?php echo esc_url( admin_url( 'post.php' ) ); ?>">
		<?php wp_nonce_field( 'toggle-custom-fields', 'toggle-custom-fields-nonce' ); ?>
		<input type="hidden" name="action" value="toggle-custom-fields" />
	</form>
	<?php foreach ( $locations as $location ) : ?>
		<form class="metabox-location-<?php echo esc_attr( $location ); ?>" onsubmit="return false;">
			<div id="poststuff" class="sidebar-open">
				<div id="postbox-container-2" class="postbox-container">
					<?php
					do_meta_boxes(
						$current_screen,
						$location,
						$post
					);
					?>
				</div>
			</div>
		</form>
	<?php endforeach; ?>
	<?php

	$meta_boxes_per_location = array();
	foreach ( $locations as $location ) {
		$meta_boxes_per_location[ $location ] = array();

		if ( ! isset( $wp_meta_boxes[ $current_screen->id ][ $location ] ) ) {
			continue;
		}

		foreach ( $priorities as $priority ) {
			if ( ! isset( $wp_meta_boxes[ $current_screen->id ][ $location ][ $priority ] ) ) {
				continue;
			}

			$meta_boxes = (array) $wp_meta_boxes[ $current_screen->id ][ $location ][ $priority ];
			foreach ( $meta_boxes as $meta_box ) {
				if ( false === $meta_box || ! $meta_box['title'] ) {
					continue;
				}

				// If a meta box is just here for back compat, don't show it in the block editor.
				if ( isset( $meta_box['args']['__back_compat_meta_box'] ) && $meta_box['args']['__back_compat_meta_box'] ) {
					continue;
				}

				$meta_boxes_per_location[ $location ][] = array(
					'id'    => $meta_box['id'],
					'title' => $meta_box['title'],
				);
			}
		}
	}

	/*
	 * Sadly we probably cannot add this data directly into editor settings.
	 *
	 * Some meta boxes need `admin_head` to fire for meta box registry.
	 * `admin_head` fires after `admin_enqueue_scripts`, which is where we create
	 * our editor instance.
	 */
	$script = 'window._wpLoadBlockEditor.then( function() {
		wp.data.dispatch( \'core/edit-post\' ).setAvailableMetaBoxesPerLocation( ' . wp_json_encode( $meta_boxes_per_location ) . ' );
	} );';

	wp_add_inline_script( 'wp-edit-post', $script );

	/*
	 * When `wp-edit-post` is output in the `<head>`, the inline script needs to be manually printed.
	 * Otherwise, meta boxes will not display because inline scripts for `wp-edit-post`
	 * will not be printed again after this point.
	 */
	if ( wp_script_is( 'wp-edit-post', 'done' ) ) {
		printf( "<script type='text/javascript'>\n%s\n</script>\n", trim( $script ) );
	}

	/*
	 * If the 'postcustom' meta box is enabled, then we need to perform
	 * some extra initialization on it.
	 */
	$enable_custom_fields = (bool) get_user_meta( get_current_user_id(), 'enable_custom_fields', true );

	if ( $enable_custom_fields ) {
		$script = "( function( $ ) {
			if ( $('#postcustom').length ) {
				$( '#the-list' ).wpList( {
					addBefore: function( s ) {
						s.data += '&post_id=$post->ID';
						return s;
					},
					addAfter: function() {
						$('table#list-table').show();
					}
				});
			}
		} )( jQuery );";
		wp_enqueue_script( 'wp-lists' );
		wp_add_inline_script( 'wp-lists', $script );
	}

	/*
	 * Refresh nonces used by the meta box loader.
	 *
	 * The logic is very similar to that provided by post.js for the classic editor.
	 */
	$script = "( function( $ ) {
		var check, timeout;

		function schedule() {
			check = false;
			window.clearTimeout( timeout );
			timeout = window.setTimeout( function() { check = true; }, 300000 );
		}

		$( document ).on( 'heartbeat-send.wp-refresh-nonces', function( e, data ) {
			var post_id, \$authCheck = $( '#wp-auth-check-wrap' );

			if ( check || ( \$authCheck.length && ! \$authCheck.hasClass( 'hidden' ) ) ) {
				if ( ( post_id = $( '#post_ID' ).val() ) && $( '#_wpnonce' ).val() ) {
					data['wp-refresh-metabox-loader-nonces'] = {
						post_id: post_id
					};
				}
			}
		}).on( 'heartbeat-tick.wp-refresh-nonces', function( e, data ) {
			var nonces = data['wp-refresh-metabox-loader-nonces'];

			if ( nonces ) {
				if ( nonces.replace ) {
					if ( nonces.replace.metabox_loader_nonce && window._wpMetaBoxUrl && wp.url ) {
						window._wpMetaBoxUrl= wp.url.addQueryArgs( window._wpMetaBoxUrl, { 'meta-box-loader-nonce': nonces.replace.metabox_loader_nonce } );
					}

					if ( nonces.replace._wpnonce ) {
						$( '#_wpnonce' ).val( nonces.replace._wpnonce );
					}
				}
			}
		}).ready( function() {
			schedule();
		});
	} )( jQuery );";
	wp_add_inline_script( 'heartbeat', $script );

	// Reset meta box data.
	$wp_meta_boxes = $_original_meta_boxes;
}

/**
 * Renders the hidden form required for the meta boxes form.
 *
 * @since 5.0.0
 *
 * @param WP_Post $post Current post object.
 */
function the_block_editor_meta_box_post_form_hidden_fields( $post ) {
	$form_extra = '';
	if ( 'auto-draft' === $post->post_status ) {
		$form_extra .= "<input type='hidden' id='auto_draft' name='auto_draft' value='1' />";
	}
	$form_action  = 'editpost';
	$nonce_action = 'update-post_' . $post->ID;
	$form_extra  .= "<input type='hidden' id='post_ID' name='post_ID' value='" . esc_attr( $post->ID ) . "' />";
	$referer      = wp_get_referer();
	$current_user = wp_get_current_user();
	$user_id      = $current_user->ID;
	wp_nonce_field( $nonce_action );

	/*
	 * Some meta boxes hook into these actions to add hidden input fields in the classic post form.
	 * For backward compatibility, we can capture the output from these actions,
	 * and extract the hidden input fields.
	 */
	ob_start();
	/** This filter is documented in wp-admin/edit-form-advanced.php */
	do_action( 'edit_form_after_title', $post );
	/** This filter is documented in wp-admin/edit-form-advanced.php */
	do_action( 'edit_form_advanced', $post );
	$classic_output = ob_get_clean();

	$classic_elements = wp_html_split( $classic_output );
	$hidden_inputs    = '';
	foreach ( $classic_elements as $element ) {
		if ( ! str_starts_with( $element, '<input ' ) ) {
			continue;
		}

		if ( preg_match( '/\stype=[\'"]hidden[\'"]\s/', $element ) ) {
			echo $element;
		}
	}
	?>
	<input type="hidden" id="user-id" name="user_ID" value="<?php echo (int) $user_id; ?>" />
	<input type="hidden" id="hiddenaction" name="action" value="<?php echo esc_attr( $form_action ); ?>" />
	<input type="hidden" id="originalaction" name="originalaction" value="<?php echo esc_attr( $form_action ); ?>" />
	<input type="hidden" id="post_type" name="post_type" value="<?php echo esc_attr( $post->post_type ); ?>" />
	<input type="hidden" id="original_post_status" name="original_post_status" value="<?php echo esc_attr( $post->post_status ); ?>" />
	<input type="hidden" id="referredby" name="referredby" value="<?php echo $referer ? esc_url( $referer ) : ''; ?>" />

	<?php
	if ( 'draft' !== get_post_status( $post ) ) {
		wp_original_referer_field( true, 'previous' );
	}
	echo $form_extra;
	wp_nonce_field( 'meta-box-order', 'meta-box-order-nonce', false );
	wp_nonce_field( 'closedpostboxes', 'closedpostboxesnonce', false );
	// Permalink title nonce.
	wp_nonce_field( 'samplepermalink', 'samplepermalinknonce', false );

	/**
	 * Adds hidden input fields to the meta box save form.
	 *
	 * Hook into this action to print `<input type="hidden" ... />` fields, which will be POSTed back to
	 * the server when meta boxes are saved.
	 *
	 * @since 5.0.0
	 *
	 * @param WP_Post $post The post that is being edited.
	 */
	do_action( 'block_editor_meta_box_hidden_fields', $post );
}

/**
 * Disables block editor for wp_navigation type posts so they can be managed via the UI.
 *
 * @since 5.9.0
 * @access private
 *
 * @param bool   $value Whether the CPT supports block editor or not.
 * @param string $post_type Post type.
 * @return bool Whether the block editor should be disabled or not.
 */
function _disable_block_editor_for_navigation_post_type( $value, $post_type ) {
	if ( 'wp_navigation' === $post_type ) {
		return false;
	}

	return $value;
}

/**
 * This callback disables the content editor for wp_navigation type posts.
 * Content editor cannot handle wp_navigation type posts correctly.
 * We cannot disable the "editor" feature in the wp_navigation's CPT definition
 * because it disables the ability to save navigation blocks via REST API.
 *
 * @since 5.9.0
 * @access private
 *
 * @param WP_Post $post An instance of WP_Post class.
 */
function _disable_content_editor_for_navigation_post_type( $post ) {
	$post_type = get_post_type( $post );
	if ( 'wp_navigation' !== $post_type ) {
		return;
	}

	remove_post_type_support( $post_type, 'editor' );
}

/**
 * This callback enables content editor for wp_navigation type posts.
 * We need to enable it back because we disable it to hide
 * the content editor for wp_navigation type posts.
 *
 * @since 5.9.0
 * @access private
 *
 * @see _disable_content_editor_for_navigation_post_type
 *
 * @param WP_Post $post An instance of WP_Post class.
 */
function _enable_content_editor_for_navigation_post_type( $post ) {
	$post_type = get_post_type( $post );
	if ( 'wp_navigation' !== $post_type ) {
		return;
	}

	add_post_type_support( $post_type, 'editor' );
}
class-wp-users-list-table.php000060400000045162151327343360012216 0ustar00<?php
/**
 * List Table API: WP_Users_List_Table class
 *
 * @package WordPress
 * @subpackage Administration
 * @since 3.1.0
 */

/**
 * Core class used to implement displaying users in a list table.
 *
 * @since 3.1.0
 *
 * @see WP_List_Table
 */
class WP_Users_List_Table extends WP_List_Table {

	/**
	 * Site ID to generate the Users list table for.
	 *
	 * @since 3.1.0
	 * @var int
	 */
	public $site_id;

	/**
	 * Whether or not the current Users list table is for Multisite.
	 *
	 * @since 3.1.0
	 * @var bool
	 */
	public $is_site_users;

	/**
	 * Constructor.
	 *
	 * @since 3.1.0
	 *
	 * @see WP_List_Table::__construct() for more information on default arguments.
	 *
	 * @param array $args An associative array of arguments.
	 */
	public function __construct( $args = array() ) {
		parent::__construct(
			array(
				'singular' => 'user',
				'plural'   => 'users',
				'screen'   => isset( $args['screen'] ) ? $args['screen'] : null,
			)
		);

		$this->is_site_users = 'site-users-network' === $this->screen->id;

		if ( $this->is_site_users ) {
			$this->site_id = isset( $_REQUEST['id'] ) ? (int) $_REQUEST['id'] : 0;
		}
	}

	/**
	 * Checks the current user's permissions.
	 *
	 * @since 3.1.0
	 *
	 * @return bool
	 */
	public function ajax_user_can() {
		if ( $this->is_site_users ) {
			return current_user_can( 'manage_sites' );
		} else {
			return current_user_can( 'list_users' );
		}
	}

	/**
	 * Prepares the users list for display.
	 *
	 * @since 3.1.0
	 *
	 * @global string $role
	 * @global string $usersearch
	 */
	public function prepare_items() {
		global $role, $usersearch;

		$usersearch = isset( $_REQUEST['s'] ) ? wp_unslash( trim( $_REQUEST['s'] ) ) : '';

		$role = isset( $_REQUEST['role'] ) ? $_REQUEST['role'] : '';

		$per_page       = ( $this->is_site_users ) ? 'site_users_network_per_page' : 'users_per_page';
		$users_per_page = $this->get_items_per_page( $per_page );

		$paged = $this->get_pagenum();

		if ( 'none' === $role ) {
			$args = array(
				'number'  => $users_per_page,
				'offset'  => ( $paged - 1 ) * $users_per_page,
				'include' => wp_get_users_with_no_role( $this->site_id ),
				'search'  => $usersearch,
				'fields'  => 'all_with_meta',
			);
		} else {
			$args = array(
				'number' => $users_per_page,
				'offset' => ( $paged - 1 ) * $users_per_page,
				'role'   => $role,
				'search' => $usersearch,
				'fields' => 'all_with_meta',
			);
		}

		if ( '' !== $args['search'] ) {
			$args['search'] = '*' . $args['search'] . '*';
		}

		if ( $this->is_site_users ) {
			$args['blog_id'] = $this->site_id;
		}

		if ( isset( $_REQUEST['orderby'] ) ) {
			$args['orderby'] = $_REQUEST['orderby'];
		}

		if ( isset( $_REQUEST['order'] ) ) {
			$args['order'] = $_REQUEST['order'];
		}

		/**
		 * Filters the query arguments used to retrieve users for the current users list table.
		 *
		 * @since 4.4.0
		 *
		 * @param array $args Arguments passed to WP_User_Query to retrieve items for the current
		 *                    users list table.
		 */
		$args = apply_filters( 'users_list_table_query_args', $args );

		// Query the user IDs for this page.
		$wp_user_search = new WP_User_Query( $args );

		$this->items = $wp_user_search->get_results();

		$this->set_pagination_args(
			array(
				'total_items' => $wp_user_search->get_total(),
				'per_page'    => $users_per_page,
			)
		);
	}

	/**
	 * Outputs 'no users' message.
	 *
	 * @since 3.1.0
	 */
	public function no_items() {
		_e( 'No users found.' );
	}

	/**
	 * Returns an associative array listing all the views that can be used
	 * with this table.
	 *
	 * Provides a list of roles and user count for that role for easy
	 * filtering of the user table.
	 *
	 * @since 3.1.0
	 *
	 * @global string $role
	 *
	 * @return string[] An array of HTML links keyed by their view.
	 */
	protected function get_views() {
		global $role;

		$wp_roles = wp_roles();

		$count_users = ! wp_is_large_user_count();

		if ( $this->is_site_users ) {
			$url = 'site-users.php?id=' . $this->site_id;
		} else {
			$url = 'users.php';
		}

		$role_links  = array();
		$avail_roles = array();
		$all_text    = __( 'All' );

		if ( $count_users ) {
			if ( $this->is_site_users ) {
				switch_to_blog( $this->site_id );
				$users_of_blog = count_users( 'time', $this->site_id );
				restore_current_blog();
			} else {
				$users_of_blog = count_users();
			}

			$total_users = $users_of_blog['total_users'];
			$avail_roles =& $users_of_blog['avail_roles'];
			unset( $users_of_blog );

			$all_text = sprintf(
				/* translators: %s: Number of users. */
				_nx(
					'All <span class="count">(%s)</span>',
					'All <span class="count">(%s)</span>',
					$total_users,
					'users'
				),
				number_format_i18n( $total_users )
			);
		}

		$role_links['all'] = array(
			'url'     => $url,
			'label'   => $all_text,
			'current' => empty( $role ),
		);

		foreach ( $wp_roles->get_names() as $this_role => $name ) {
			if ( $count_users && ! isset( $avail_roles[ $this_role ] ) ) {
				continue;
			}

			$name = translate_user_role( $name );
			if ( $count_users ) {
				$name = sprintf(
					/* translators: 1: User role name, 2: Number of users. */
					__( '%1$s <span class="count">(%2$s)</span>' ),
					$name,
					number_format_i18n( $avail_roles[ $this_role ] )
				);
			}

			$role_links[ $this_role ] = array(
				'url'     => esc_url( add_query_arg( 'role', $this_role, $url ) ),
				'label'   => $name,
				'current' => $this_role === $role,
			);
		}

		if ( ! empty( $avail_roles['none'] ) ) {

			$name = __( 'No role' );
			$name = sprintf(
				/* translators: 1: User role name, 2: Number of users. */
				__( '%1$s <span class="count">(%2$s)</span>' ),
				$name,
				number_format_i18n( $avail_roles['none'] )
			);

			$role_links['none'] = array(
				'url'     => esc_url( add_query_arg( 'role', 'none', $url ) ),
				'label'   => $name,
				'current' => 'none' === $role,
			);
		}

		return $this->get_views_links( $role_links );
	}

	/**
	 * Retrieves an associative array of bulk actions available on this table.
	 *
	 * @since 3.1.0
	 *
	 * @return array Array of bulk action labels keyed by their action.
	 */
	protected function get_bulk_actions() {
		$actions = array();

		if ( is_multisite() ) {
			if ( current_user_can( 'remove_users' ) ) {
				$actions['remove'] = __( 'Remove' );
			}
		} else {
			if ( current_user_can( 'delete_users' ) ) {
				$actions['delete'] = __( 'Delete' );
			}
		}

		// Add a password reset link to the bulk actions dropdown.
		if ( current_user_can( 'edit_users' ) ) {
			$actions['resetpassword'] = __( 'Send password reset' );
		}

		return $actions;
	}

	/**
	 * Outputs the controls to allow user roles to be changed in bulk.
	 *
	 * @since 3.1.0
	 *
	 * @param string $which Whether this is being invoked above ("top")
	 *                      or below the table ("bottom").
	 */
	protected function extra_tablenav( $which ) {
		$id        = 'bottom' === $which ? 'new_role2' : 'new_role';
		$button_id = 'bottom' === $which ? 'changeit2' : 'changeit';
		?>
	<div class="alignleft actions">
		<?php if ( current_user_can( 'promote_users' ) && $this->has_items() ) : ?>
		<label class="screen-reader-text" for="<?php echo $id; ?>">
			<?php
			/* translators: Hidden accessibility text. */
			_e( 'Change role to&hellip;' );
			?>
		</label>
		<select name="<?php echo $id; ?>" id="<?php echo $id; ?>">
			<option value=""><?php _e( 'Change role to&hellip;' ); ?></option>
			<?php wp_dropdown_roles(); ?>
			<option value="none"><?php _e( '&mdash; No role for this site &mdash;' ); ?></option>
		</select>
			<?php
			submit_button( __( 'Change' ), '', $button_id, false );
		endif;

		/**
		 * Fires just before the closing div containing the bulk role-change controls
		 * in the Users list table.
		 *
		 * @since 3.5.0
		 * @since 4.6.0 The `$which` parameter was added.
		 *
		 * @param string $which The location of the extra table nav markup: 'top' or 'bottom'.
		 */
		do_action( 'restrict_manage_users', $which );
		?>
		</div>
		<?php
		/**
		 * Fires immediately following the closing "actions" div in the tablenav for the users
		 * list table.
		 *
		 * @since 4.9.0
		 *
		 * @param string $which The location of the extra table nav markup: 'top' or 'bottom'.
		 */
		do_action( 'manage_users_extra_tablenav', $which );
	}

	/**
	 * Captures the bulk action required, and return it.
	 *
	 * Overridden from the base class implementation to capture
	 * the role change drop-down.
	 *
	 * @since 3.1.0
	 *
	 * @return string The bulk action required.
	 */
	public function current_action() {
		if ( isset( $_REQUEST['changeit'] ) ) {
			return 'promote';
		}

		return parent::current_action();
	}

	/**
	 * Gets a list of columns for the list table.
	 *
	 * @since 3.1.0
	 *
	 * @return string[] Array of column titles keyed by their column name.
	 */
	public function get_columns() {
		$columns = array(
			'cb'       => '<input type="checkbox" />',
			'username' => __( 'Username' ),
			'name'     => __( 'Name' ),
			'email'    => __( 'Email' ),
			'role'     => __( 'Role' ),
			'posts'    => _x( 'Posts', 'post type general name' ),
		);

		if ( $this->is_site_users ) {
			unset( $columns['posts'] );
		}

		return $columns;
	}

	/**
	 * Gets a list of sortable columns for the list table.
	 *
	 * @since 3.1.0
	 *
	 * @return array Array of sortable columns.
	 */
	protected function get_sortable_columns() {
		$columns = array(
			'username' => array( 'login', false, __( 'Username' ), __( 'Table ordered by Username.' ), 'asc' ),
			'email'    => array( 'email', false, __( 'E-mail' ), __( 'Table ordered by E-mail.' ) ),
		);

		return $columns;
	}

	/**
	 * Generates the list table rows.
	 *
	 * @since 3.1.0
	 */
	public function display_rows() {
		// Query the post counts for this page.
		if ( ! $this->is_site_users ) {
			$post_counts = count_many_users_posts( array_keys( $this->items ) );
		}

		foreach ( $this->items as $userid => $user_object ) {
			echo "\n\t" . $this->single_row( $user_object, '', '', isset( $post_counts ) ? $post_counts[ $userid ] : 0 );
		}
	}

	/**
	 * Generates HTML for a single row on the users.php admin panel.
	 *
	 * @since 3.1.0
	 * @since 4.2.0 The `$style` parameter was deprecated.
	 * @since 4.4.0 The `$role` parameter was deprecated.
	 *
	 * @param WP_User $user_object The current user object.
	 * @param string  $style       Deprecated. Not used.
	 * @param string  $role        Deprecated. Not used.
	 * @param int     $numposts    Optional. Post count to display for this user. Defaults
	 *                             to zero, as in, a new user has made zero posts.
	 * @return string Output for a single row.
	 */
	public function single_row( $user_object, $style = '', $role = '', $numposts = 0 ) {
		if ( ! ( $user_object instanceof WP_User ) ) {
			$user_object = get_userdata( (int) $user_object );
		}
		$user_object->filter = 'display';
		$email               = $user_object->user_email;

		if ( $this->is_site_users ) {
			$url = "site-users.php?id={$this->site_id}&amp;";
		} else {
			$url = 'users.php?';
		}

		$user_roles = $this->get_role_list( $user_object );

		// Set up the hover actions for this user.
		$actions     = array();
		$checkbox    = '';
		$super_admin = '';

		if ( is_multisite() && current_user_can( 'manage_network_users' ) ) {
			if ( in_array( $user_object->user_login, get_super_admins(), true ) ) {
				$super_admin = ' &mdash; ' . __( 'Super Admin' );
			}
		}

		// Check if the user for this row is editable.
		if ( current_user_can( 'list_users' ) ) {
			// Set up the user editing link.
			$edit_link = esc_url(
				add_query_arg(
					'wp_http_referer',
					urlencode( wp_unslash( $_SERVER['REQUEST_URI'] ) ),
					get_edit_user_link( $user_object->ID )
				)
			);

			if ( current_user_can( 'edit_user', $user_object->ID ) ) {
				$edit            = "<strong><a href=\"{$edit_link}\">{$user_object->user_login}</a>{$super_admin}</strong><br />";
				$actions['edit'] = '<a href="' . $edit_link . '">' . __( 'Edit' ) . '</a>';
			} else {
				$edit = "<strong>{$user_object->user_login}{$super_admin}</strong><br />";
			}

			if ( ! is_multisite()
				&& get_current_user_id() !== $user_object->ID
				&& current_user_can( 'delete_user', $user_object->ID )
			) {
				$actions['delete'] = "<a class='submitdelete' href='" . wp_nonce_url( "users.php?action=delete&amp;user=$user_object->ID", 'bulk-users' ) . "'>" . __( 'Delete' ) . '</a>';
			}

			if ( is_multisite()
				&& current_user_can( 'remove_user', $user_object->ID )
			) {
				$actions['remove'] = "<a class='submitdelete' href='" . wp_nonce_url( $url . "action=remove&amp;user=$user_object->ID", 'bulk-users' ) . "'>" . __( 'Remove' ) . '</a>';
			}

			// Add a link to the user's author archive, if not empty.
			$author_posts_url = get_author_posts_url( $user_object->ID );
			if ( $author_posts_url ) {
				$actions['view'] = sprintf(
					'<a href="%s" aria-label="%s">%s</a>',
					esc_url( $author_posts_url ),
					/* translators: %s: Author's display name. */
					esc_attr( sprintf( __( 'View posts by %s' ), $user_object->display_name ) ),
					__( 'View' )
				);
			}

			// Add a link to send the user a reset password link by email.
			if ( get_current_user_id() !== $user_object->ID
				&& current_user_can( 'edit_user', $user_object->ID )
				&& true === wp_is_password_reset_allowed_for_user( $user_object )
			) {
				$actions['resetpassword'] = "<a class='resetpassword' href='" . wp_nonce_url( "users.php?action=resetpassword&amp;users=$user_object->ID", 'bulk-users' ) . "'>" . __( 'Send password reset' ) . '</a>';
			}

			/**
			 * Filters the action links displayed under each user in the Users list table.
			 *
			 * @since 2.8.0
			 *
			 * @param string[] $actions     An array of action links to be displayed.
			 *                              Default 'Edit', 'Delete' for single site, and
			 *                              'Edit', 'Remove' for Multisite.
			 * @param WP_User  $user_object WP_User object for the currently listed user.
			 */
			$actions = apply_filters( 'user_row_actions', $actions, $user_object );

			// Role classes.
			$role_classes = esc_attr( implode( ' ', array_keys( $user_roles ) ) );

			// Set up the checkbox (because the user is editable, otherwise it's empty).
			$checkbox = sprintf(
				'<input type="checkbox" name="users[]" id="user_%1$s" class="%2$s" value="%1$s" />' .
				'<label for="user_%1$s"><span class="screen-reader-text">%3$s</span></label>',
				$user_object->ID,
				$role_classes,
				/* translators: Hidden accessibility text. %s: User login. */
				sprintf( __( 'Select %s' ), $user_object->user_login )
			);

		} else {
			$edit = "<strong>{$user_object->user_login}{$super_admin}</strong>";
		}

		$avatar = get_avatar( $user_object->ID, 32 );

		// Comma-separated list of user roles.
		$roles_list = implode( ', ', $user_roles );

		$row = "<tr id='user-$user_object->ID'>";

		list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info();

		foreach ( $columns as $column_name => $column_display_name ) {
			$classes = "$column_name column-$column_name";
			if ( $primary === $column_name ) {
				$classes .= ' has-row-actions column-primary';
			}
			if ( 'posts' === $column_name ) {
				$classes .= ' num'; // Special case for that column.
			}

			if ( in_array( $column_name, $hidden, true ) ) {
				$classes .= ' hidden';
			}

			$data = 'data-colname="' . esc_attr( wp_strip_all_tags( $column_display_name ) ) . '"';

			$attributes = "class='$classes' $data";

			if ( 'cb' === $column_name ) {
				$row .= "<th scope='row' class='check-column'>$checkbox</th>";
			} else {
				$row .= "<td $attributes>";
				switch ( $column_name ) {
					case 'username':
						$row .= "$avatar $edit";
						break;
					case 'name':
						if ( $user_object->first_name && $user_object->last_name ) {
							$row .= sprintf(
								/* translators: 1: User's first name, 2: Last name. */
								_x( '%1$s %2$s', 'Display name based on first name and last name' ),
								$user_object->first_name,
								$user_object->last_name
							);
						} elseif ( $user_object->first_name ) {
							$row .= $user_object->first_name;
						} elseif ( $user_object->last_name ) {
							$row .= $user_object->last_name;
						} else {
							$row .= sprintf(
								'<span aria-hidden="true">&#8212;</span><span class="screen-reader-text">%s</span>',
								/* translators: Hidden accessibility text. */
								_x( 'Unknown', 'name' )
							);
						}
						break;
					case 'email':
						$row .= "<a href='" . esc_url( "mailto:$email" ) . "'>$email</a>";
						break;
					case 'role':
						$row .= esc_html( $roles_list );
						break;
					case 'posts':
						if ( $numposts > 0 ) {
							$row .= sprintf(
								'<a href="%s" class="edit"><span aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></a>',
								"edit.php?author={$user_object->ID}",
								$numposts,
								sprintf(
									/* translators: Hidden accessibility text. %s: Number of posts. */
									_n( '%s post by this author', '%s posts by this author', $numposts ),
									number_format_i18n( $numposts )
								)
							);
						} else {
							$row .= 0;
						}
						break;
					default:
						/**
						 * Filters the display output of custom columns in the Users list table.
						 *
						 * @since 2.8.0
						 *
						 * @param string $output      Custom column output. Default empty.
						 * @param string $column_name Column name.
						 * @param int    $user_id     ID of the currently-listed user.
						 */
						$row .= apply_filters( 'manage_users_custom_column', '', $column_name, $user_object->ID );
				}

				if ( $primary === $column_name ) {
					$row .= $this->row_actions( $actions );
				}
				$row .= '</td>';
			}
		}
		$row .= '</tr>';

		return $row;
	}

	/**
	 * Gets the name of the default primary column.
	 *
	 * @since 4.3.0
	 *
	 * @return string Name of the default primary column, in this case, 'username'.
	 */
	protected function get_default_primary_column_name() {
		return 'username';
	}

	/**
	 * Returns an array of translated user role names for a given user object.
	 *
	 * @since 4.4.0
	 *
	 * @param WP_User $user_object The WP_User object.
	 * @return string[] An array of user role names keyed by role.
	 */
	protected function get_role_list( $user_object ) {
		$wp_roles = wp_roles();

		$role_list = array();

		foreach ( $user_object->roles as $role ) {
			if ( isset( $wp_roles->role_names[ $role ] ) ) {
				$role_list[ $role ] = translate_user_role( $wp_roles->role_names[ $role ] );
			}
		}

		if ( empty( $role_list ) ) {
			$role_list['none'] = _x( 'None', 'no user roles' );
		}

		/**
		 * Filters the returned array of translated role names for a user.
		 *
		 * @since 4.4.0
		 *
		 * @param string[] $role_list   An array of translated user role names keyed by role.
		 * @param WP_User  $user_object A WP_User object.
		 */
		return apply_filters( 'get_role_list', $role_list, $user_object );
	}
}
template.php000064400000300341151327343360007100 0ustar00<?php
/**
 * Template WordPress Administration API.
 *
 * A Big Mess. Also some neat functions that are nicely written.
 *
 * @package WordPress
 * @subpackage Administration
 */

/** Walker_Category_Checklist class */
require_once ABSPATH . 'wp-admin/includes/class-walker-category-checklist.php';

/** WP_Internal_Pointers class */
require_once ABSPATH . 'wp-admin/includes/class-wp-internal-pointers.php';

//
// Category Checklists.
//

/**
 * Outputs an unordered list of checkbox input elements labeled with category names.
 *
 * @since 2.5.1
 *
 * @see wp_terms_checklist()
 *
 * @param int         $post_id              Optional. Post to generate a categories checklist for. Default 0.
 *                                          $selected_cats must not be an array. Default 0.
 * @param int         $descendants_and_self Optional. ID of the category to output along with its descendants.
 *                                          Default 0.
 * @param int[]|false $selected_cats        Optional. Array of category IDs to mark as checked. Default false.
 * @param int[]|false $popular_cats         Optional. Array of category IDs to receive the "popular-category" class.
 *                                          Default false.
 * @param Walker      $walker               Optional. Walker object to use to build the output.
 *                                          Default is a Walker_Category_Checklist instance.
 * @param bool        $checked_ontop        Optional. Whether to move checked items out of the hierarchy and to
 *                                          the top of the list. Default true.
 */
function wp_category_checklist( $post_id = 0, $descendants_and_self = 0, $selected_cats = false, $popular_cats = false, $walker = null, $checked_ontop = true ) {
	wp_terms_checklist(
		$post_id,
		array(
			'taxonomy'             => 'category',
			'descendants_and_self' => $descendants_and_self,
			'selected_cats'        => $selected_cats,
			'popular_cats'         => $popular_cats,
			'walker'               => $walker,
			'checked_ontop'        => $checked_ontop,
		)
	);
}

/**
 * Outputs an unordered list of checkbox input elements labelled with term names.
 *
 * Taxonomy-independent version of wp_category_checklist().
 *
 * @since 3.0.0
 * @since 4.4.0 Introduced the `$echo` argument.
 *
 * @param int          $post_id Optional. Post ID. Default 0.
 * @param array|string $args {
 *     Optional. Array or string of arguments for generating a terms checklist. Default empty array.
 *
 *     @type int    $descendants_and_self ID of the category to output along with its descendants.
 *                                        Default 0.
 *     @type int[]  $selected_cats        Array of category IDs to mark as checked. Default false.
 *     @type int[]  $popular_cats         Array of category IDs to receive the "popular-category" class.
 *                                        Default false.
 *     @type Walker $walker               Walker object to use to build the output. Default empty which
 *                                        results in a Walker_Category_Checklist instance being used.
 *     @type string $taxonomy             Taxonomy to generate the checklist for. Default 'category'.
 *     @type bool   $checked_ontop        Whether to move checked items out of the hierarchy and to
 *                                        the top of the list. Default true.
 *     @type bool   $echo                 Whether to echo the generated markup. False to return the markup instead
 *                                        of echoing it. Default true.
 * }
 * @return string HTML list of input elements.
 */
function wp_terms_checklist( $post_id = 0, $args = array() ) {
	$defaults = array(
		'descendants_and_self' => 0,
		'selected_cats'        => false,
		'popular_cats'         => false,
		'walker'               => null,
		'taxonomy'             => 'category',
		'checked_ontop'        => true,
		'echo'                 => true,
	);

	/**
	 * Filters the taxonomy terms checklist arguments.
	 *
	 * @since 3.4.0
	 *
	 * @see wp_terms_checklist()
	 *
	 * @param array|string $args    An array or string of arguments.
	 * @param int          $post_id The post ID.
	 */
	$params = apply_filters( 'wp_terms_checklist_args', $args, $post_id );

	$parsed_args = wp_parse_args( $params, $defaults );

	if ( empty( $parsed_args['walker'] ) || ! ( $parsed_args['walker'] instanceof Walker ) ) {
		$walker = new Walker_Category_Checklist();
	} else {
		$walker = $parsed_args['walker'];
	}

	$taxonomy             = $parsed_args['taxonomy'];
	$descendants_and_self = (int) $parsed_args['descendants_and_self'];

	$args = array( 'taxonomy' => $taxonomy );

	$tax              = get_taxonomy( $taxonomy );
	$args['disabled'] = ! current_user_can( $tax->cap->assign_terms );

	$args['list_only'] = ! empty( $parsed_args['list_only'] );

	if ( is_array( $parsed_args['selected_cats'] ) ) {
		$args['selected_cats'] = array_map( 'intval', $parsed_args['selected_cats'] );
	} elseif ( $post_id ) {
		$args['selected_cats'] = wp_get_object_terms( $post_id, $taxonomy, array_merge( $args, array( 'fields' => 'ids' ) ) );
	} else {
		$args['selected_cats'] = array();
	}

	if ( is_array( $parsed_args['popular_cats'] ) ) {
		$args['popular_cats'] = array_map( 'intval', $parsed_args['popular_cats'] );
	} else {
		$args['popular_cats'] = get_terms(
			array(
				'taxonomy'     => $taxonomy,
				'fields'       => 'ids',
				'orderby'      => 'count',
				'order'        => 'DESC',
				'number'       => 10,
				'hierarchical' => false,
			)
		);
	}

	if ( $descendants_and_self ) {
		$categories = (array) get_terms(
			array(
				'taxonomy'     => $taxonomy,
				'child_of'     => $descendants_and_self,
				'hierarchical' => 0,
				'hide_empty'   => 0,
			)
		);
		$self       = get_term( $descendants_and_self, $taxonomy );
		array_unshift( $categories, $self );
	} else {
		$categories = (array) get_terms(
			array(
				'taxonomy' => $taxonomy,
				'get'      => 'all',
			)
		);
	}

	$output = '';

	if ( $parsed_args['checked_ontop'] ) {
		/*
		 * Post-process $categories rather than adding an exclude to the get_terms() query
		 * to keep the query the same across all posts (for any query cache).
		 */
		$checked_categories = array();
		$keys               = array_keys( $categories );

		foreach ( $keys as $k ) {
			if ( in_array( $categories[ $k ]->term_id, $args['selected_cats'], true ) ) {
				$checked_categories[] = $categories[ $k ];
				unset( $categories[ $k ] );
			}
		}

		// Put checked categories on top.
		$output .= $walker->walk( $checked_categories, 0, $args );
	}
	// Then the rest of them.
	$output .= $walker->walk( $categories, 0, $args );

	if ( $parsed_args['echo'] ) {
		echo $output;
	}

	return $output;
}

/**
 * Retrieves a list of the most popular terms from the specified taxonomy.
 *
 * If the `$display` argument is true then the elements for a list of checkbox
 * `<input>` elements labelled with the names of the selected terms is output.
 * If the `$post_ID` global is not empty then the terms associated with that
 * post will be marked as checked.
 *
 * @since 2.5.0
 *
 * @param string $taxonomy     Taxonomy to retrieve terms from.
 * @param int    $default_term Optional. Not used.
 * @param int    $number       Optional. Number of terms to retrieve. Default 10.
 * @param bool   $display      Optional. Whether to display the list as well. Default true.
 * @return int[] Array of popular term IDs.
 */
function wp_popular_terms_checklist( $taxonomy, $default_term = 0, $number = 10, $display = true ) {
	$post = get_post();

	if ( $post && $post->ID ) {
		$checked_terms = wp_get_object_terms( $post->ID, $taxonomy, array( 'fields' => 'ids' ) );
	} else {
		$checked_terms = array();
	}

	$terms = get_terms(
		array(
			'taxonomy'     => $taxonomy,
			'orderby'      => 'count',
			'order'        => 'DESC',
			'number'       => $number,
			'hierarchical' => false,
		)
	);

	$tax = get_taxonomy( $taxonomy );

	$popular_ids = array();

	foreach ( (array) $terms as $term ) {
		$popular_ids[] = $term->term_id;

		if ( ! $display ) { // Hack for Ajax use.
			continue;
		}

		$id      = "popular-$taxonomy-$term->term_id";
		$checked = in_array( $term->term_id, $checked_terms, true ) ? 'checked="checked"' : '';
		?>

		<li id="<?php echo $id; ?>" class="popular-category">
			<label class="selectit">
				<input id="in-<?php echo $id; ?>" type="checkbox" <?php echo $checked; ?> value="<?php echo (int) $term->term_id; ?>" <?php disabled( ! current_user_can( $tax->cap->assign_terms ) ); ?> />
				<?php
				/** This filter is documented in wp-includes/category-template.php */
				echo esc_html( apply_filters( 'the_category', $term->name, '', '' ) );
				?>
			</label>
		</li>

		<?php
	}
	return $popular_ids;
}

/**
 * Outputs a link category checklist element.
 *
 * @since 2.5.1
 *
 * @param int $link_id Optional. The link ID. Default 0.
 */
function wp_link_category_checklist( $link_id = 0 ) {
	$default = 1;

	$checked_categories = array();

	if ( $link_id ) {
		$checked_categories = wp_get_link_cats( $link_id );
		// No selected categories, strange.
		if ( ! count( $checked_categories ) ) {
			$checked_categories[] = $default;
		}
	} else {
		$checked_categories[] = $default;
	}

	$categories = get_terms(
		array(
			'taxonomy'   => 'link_category',
			'orderby'    => 'name',
			'hide_empty' => 0,
		)
	);

	if ( empty( $categories ) ) {
		return;
	}

	foreach ( $categories as $category ) {
		$cat_id = $category->term_id;

		/** This filter is documented in wp-includes/category-template.php */
		$name    = esc_html( apply_filters( 'the_category', $category->name, '', '' ) );
		$checked = in_array( $cat_id, $checked_categories, true ) ? ' checked="checked"' : '';
		echo '<li id="link-category-', $cat_id, '"><label for="in-link-category-', $cat_id, '" class="selectit"><input value="', $cat_id, '" type="checkbox" name="link_category[]" id="in-link-category-', $cat_id, '"', $checked, '/> ', $name, '</label></li>';
	}
}

/**
 * Adds hidden fields with the data for use in the inline editor for posts and pages.
 *
 * @since 2.7.0
 *
 * @param WP_Post $post Post object.
 */
function get_inline_data( $post ) {
	$post_type_object = get_post_type_object( $post->post_type );
	if ( ! current_user_can( 'edit_post', $post->ID ) ) {
		return;
	}

	$title = esc_textarea( trim( $post->post_title ) );

	echo '
<div class="hidden" id="inline_' . $post->ID . '">
	<div class="post_title">' . $title . '</div>' .
	/** This filter is documented in wp-admin/edit-tag-form.php */
	'<div class="post_name">' . apply_filters( 'editable_slug', $post->post_name, $post ) . '</div>
	<div class="post_author">' . $post->post_author . '</div>
	<div class="comment_status">' . esc_html( $post->comment_status ) . '</div>
	<div class="ping_status">' . esc_html( $post->ping_status ) . '</div>
	<div class="_status">' . esc_html( $post->post_status ) . '</div>
	<div class="jj">' . mysql2date( 'd', $post->post_date, false ) . '</div>
	<div class="mm">' . mysql2date( 'm', $post->post_date, false ) . '</div>
	<div class="aa">' . mysql2date( 'Y', $post->post_date, false ) . '</div>
	<div class="hh">' . mysql2date( 'H', $post->post_date, false ) . '</div>
	<div class="mn">' . mysql2date( 'i', $post->post_date, false ) . '</div>
	<div class="ss">' . mysql2date( 's', $post->post_date, false ) . '</div>
	<div class="post_password">' . esc_html( $post->post_password ) . '</div>';

	if ( $post_type_object->hierarchical ) {
		echo '<div class="post_parent">' . $post->post_parent . '</div>';
	}

	echo '<div class="page_template">' . ( $post->page_template ? esc_html( $post->page_template ) : 'default' ) . '</div>';

	if ( post_type_supports( $post->post_type, 'page-attributes' ) ) {
		echo '<div class="menu_order">' . $post->menu_order . '</div>';
	}

	$taxonomy_names = get_object_taxonomies( $post->post_type );

	foreach ( $taxonomy_names as $taxonomy_name ) {
		$taxonomy = get_taxonomy( $taxonomy_name );

		if ( ! $taxonomy->show_in_quick_edit ) {
			continue;
		}

		if ( $taxonomy->hierarchical ) {

			$terms = get_object_term_cache( $post->ID, $taxonomy_name );
			if ( false === $terms ) {
				$terms = wp_get_object_terms( $post->ID, $taxonomy_name );
				wp_cache_add( $post->ID, wp_list_pluck( $terms, 'term_id' ), $taxonomy_name . '_relationships' );
			}
			$term_ids = empty( $terms ) ? array() : wp_list_pluck( $terms, 'term_id' );

			echo '<div class="post_category" id="' . $taxonomy_name . '_' . $post->ID . '">' . implode( ',', $term_ids ) . '</div>';

		} else {

			$terms_to_edit = get_terms_to_edit( $post->ID, $taxonomy_name );
			if ( ! is_string( $terms_to_edit ) ) {
				$terms_to_edit = '';
			}

			echo '<div class="tags_input" id="' . $taxonomy_name . '_' . $post->ID . '">'
				. esc_html( str_replace( ',', ', ', $terms_to_edit ) ) . '</div>';

		}
	}

	if ( ! $post_type_object->hierarchical ) {
		echo '<div class="sticky">' . ( is_sticky( $post->ID ) ? 'sticky' : '' ) . '</div>';
	}

	if ( post_type_supports( $post->post_type, 'post-formats' ) ) {
		echo '<div class="post_format">' . esc_html( get_post_format( $post->ID ) ) . '</div>';
	}

	/**
	 * Fires after outputting the fields for the inline editor for posts and pages.
	 *
	 * @since 4.9.8
	 *
	 * @param WP_Post      $post             The current post object.
	 * @param WP_Post_Type $post_type_object The current post's post type object.
	 */
	do_action( 'add_inline_data', $post, $post_type_object );

	echo '</div>';
}

/**
 * Outputs the in-line comment reply-to form in the Comments list table.
 *
 * @since 2.7.0
 *
 * @global WP_List_Table $wp_list_table
 *
 * @param int    $position  Optional. The value of the 'position' input field. Default 1.
 * @param bool   $checkbox  Optional. The value of the 'checkbox' input field. Default false.
 * @param string $mode      Optional. If set to 'single', will use WP_Post_Comments_List_Table,
 *                          otherwise WP_Comments_List_Table. Default 'single'.
 * @param bool   $table_row Optional. Whether to use a table instead of a div element. Default true.
 */
function wp_comment_reply( $position = 1, $checkbox = false, $mode = 'single', $table_row = true ) {
	global $wp_list_table;
	/**
	 * Filters the in-line comment reply-to form output in the Comments
	 * list table.
	 *
	 * Returning a non-empty value here will short-circuit display
	 * of the in-line comment-reply form in the Comments list table,
	 * echoing the returned value instead.
	 *
	 * @since 2.7.0
	 *
	 * @see wp_comment_reply()
	 *
	 * @param string $content The reply-to form content.
	 * @param array  $args    An array of default args.
	 */
	$content = apply_filters(
		'wp_comment_reply',
		'',
		array(
			'position' => $position,
			'checkbox' => $checkbox,
			'mode'     => $mode,
		)
	);

	if ( ! empty( $content ) ) {
		echo $content;
		return;
	}

	if ( ! $wp_list_table ) {
		if ( 'single' === $mode ) {
			$wp_list_table = _get_list_table( 'WP_Post_Comments_List_Table' );
		} else {
			$wp_list_table = _get_list_table( 'WP_Comments_List_Table' );
		}
	}

	?>
<form method="get">
	<?php if ( $table_row ) : ?>
<table style="display:none;"><tbody id="com-reply"><tr id="replyrow" class="inline-edit-row" style="display:none;"><td colspan="<?php echo $wp_list_table->get_column_count(); ?>" class="colspanchange">
<?php else : ?>
<div id="com-reply" style="display:none;"><div id="replyrow" style="display:none;">
<?php endif; ?>
	<fieldset class="comment-reply">
	<legend>
		<span class="hidden" id="editlegend"><?php _e( 'Edit Comment' ); ?></span>
		<span class="hidden" id="replyhead"><?php _e( 'Reply to Comment' ); ?></span>
		<span class="hidden" id="addhead"><?php _e( 'Add Comment' ); ?></span>
	</legend>

	<div id="replycontainer">
	<label for="replycontent" class="screen-reader-text">
		<?php
		/* translators: Hidden accessibility text. */
		_e( 'Comment' );
		?>
	</label>
	<?php
	$quicktags_settings = array( 'buttons' => 'strong,em,link,block,del,ins,img,ul,ol,li,code,close' );
	wp_editor(
		'',
		'replycontent',
		array(
			'media_buttons' => false,
			'tinymce'       => false,
			'quicktags'     => $quicktags_settings,
		)
	);
	?>
	</div>

	<div id="edithead" style="display:none;">
		<div class="inside">
		<label for="author-name"><?php _e( 'Name' ); ?></label>
		<input type="text" name="newcomment_author" size="50" value="" id="author-name" />
		</div>

		<div class="inside">
		<label for="author-email"><?php _e( 'Email' ); ?></label>
		<input type="text" name="newcomment_author_email" size="50" value="" id="author-email" />
		</div>

		<div class="inside">
		<label for="author-url"><?php _e( 'URL' ); ?></label>
		<input type="text" id="author-url" name="newcomment_author_url" class="code" size="103" value="" />
		</div>
	</div>

	<div id="replysubmit" class="submit">
		<p class="reply-submit-buttons">
			<button type="button" class="save button button-primary">
				<span id="addbtn" style="display: none;"><?php _e( 'Add Comment' ); ?></span>
				<span id="savebtn" style="display: none;"><?php _e( 'Update Comment' ); ?></span>
				<span id="replybtn" style="display: none;"><?php _e( 'Submit Reply' ); ?></span>
			</button>
			<button type="button" class="cancel button"><?php _e( 'Cancel' ); ?></button>
			<span class="waiting spinner"></span>
		</p>
		<?php
		wp_admin_notice(
			'<p class="error"></p>',
			array(
				'type'               => 'error',
				'additional_classes' => array( 'notice-alt', 'inline', 'hidden' ),
				'paragraph_wrap'     => false,
			)
		);
		?>
	</div>

	<input type="hidden" name="action" id="action" value="" />
	<input type="hidden" name="comment_ID" id="comment_ID" value="" />
	<input type="hidden" name="comment_post_ID" id="comment_post_ID" value="" />
	<input type="hidden" name="status" id="status" value="" />
	<input type="hidden" name="position" id="position" value="<?php echo $position; ?>" />
	<input type="hidden" name="checkbox" id="checkbox" value="<?php echo $checkbox ? 1 : 0; ?>" />
	<input type="hidden" name="mode" id="mode" value="<?php echo esc_attr( $mode ); ?>" />
	<?php
		wp_nonce_field( 'replyto-comment', '_ajax_nonce-replyto-comment', false );
	if ( current_user_can( 'unfiltered_html' ) ) {
		wp_nonce_field( 'unfiltered-html-comment', '_wp_unfiltered_html_comment', false );
	}
	?>
	</fieldset>
	<?php if ( $table_row ) : ?>
</td></tr></tbody></table>
	<?php else : ?>
</div></div>
	<?php endif; ?>
</form>
	<?php
}

/**
 * Outputs 'undo move to Trash' text for comments.
 *
 * @since 2.9.0
 */
function wp_comment_trashnotice() {
	?>
<div class="hidden" id="trash-undo-holder">
	<div class="trash-undo-inside">
		<?php
		/* translators: %s: Comment author, filled by Ajax. */
		printf( __( 'Comment by %s moved to the Trash.' ), '<strong></strong>' );
		?>
		<span class="undo untrash"><a href="#"><?php _e( 'Undo' ); ?></a></span>
	</div>
</div>
<div class="hidden" id="spam-undo-holder">
	<div class="spam-undo-inside">
		<?php
		/* translators: %s: Comment author, filled by Ajax. */
		printf( __( 'Comment by %s marked as spam.' ), '<strong></strong>' );
		?>
		<span class="undo unspam"><a href="#"><?php _e( 'Undo' ); ?></a></span>
	</div>
</div>
	<?php
}

/**
 * Outputs a post's public meta data in the Custom Fields meta box.
 *
 * @since 1.2.0
 *
 * @param array[] $meta An array of meta data arrays keyed on 'meta_key' and 'meta_value'.
 */
function list_meta( $meta ) {
	// Exit if no meta.
	if ( ! $meta ) {
		echo '
<table id="list-table" style="display: none;">
	<thead>
	<tr>
		<th class="left">' . _x( 'Name', 'meta name' ) . '</th>
		<th>' . __( 'Value' ) . '</th>
	</tr>
	</thead>
	<tbody id="the-list" data-wp-lists="list:meta">
	<tr><td></td></tr>
	</tbody>
</table>'; // TBODY needed for list-manipulation JS.
		return;
	}
	$count = 0;
	?>
<table id="list-table">
	<thead>
	<tr>
		<th class="left"><?php _ex( 'Name', 'meta name' ); ?></th>
		<th><?php _e( 'Value' ); ?></th>
	</tr>
	</thead>
	<tbody id='the-list' data-wp-lists='list:meta'>
	<?php
	foreach ( $meta as $entry ) {
		echo _list_meta_row( $entry, $count );
	}
	?>
	</tbody>
</table>
	<?php
}

/**
 * Outputs a single row of public meta data in the Custom Fields meta box.
 *
 * @since 2.5.0
 *
 * @param array $entry An array of meta data keyed on 'meta_key' and 'meta_value'.
 * @param int   $count Reference to the row number.
 * @return string A single row of public meta data.
 */
function _list_meta_row( $entry, &$count ) {
	static $update_nonce = '';

	if ( is_protected_meta( $entry['meta_key'], 'post' ) ) {
		return '';
	}

	if ( ! $update_nonce ) {
		$update_nonce = wp_create_nonce( 'add-meta' );
	}

	$r = '';
	++$count;

	if ( is_serialized( $entry['meta_value'] ) ) {
		if ( is_serialized_string( $entry['meta_value'] ) ) {
			// This is a serialized string, so we should display it.
			$entry['meta_value'] = maybe_unserialize( $entry['meta_value'] );
		} else {
			// This is a serialized array/object so we should NOT display it.
			--$count;
			return '';
		}
	}

	$entry['meta_key']   = esc_attr( $entry['meta_key'] );
	$entry['meta_value'] = esc_textarea( $entry['meta_value'] ); // Using a <textarea />.
	$entry['meta_id']    = (int) $entry['meta_id'];

	$delete_nonce = wp_create_nonce( 'delete-meta_' . $entry['meta_id'] );

	$r .= "\n\t<tr id='meta-{$entry['meta_id']}'>";
	$r .= "\n\t\t<td class='left'><label class='screen-reader-text' for='meta-{$entry['meta_id']}-key'>" .
		/* translators: Hidden accessibility text. */
		__( 'Key' ) .
	"</label><input name='meta[{$entry['meta_id']}][key]' id='meta-{$entry['meta_id']}-key' type='text' size='20' value='{$entry['meta_key']}' />";

	$r .= "\n\t\t<div class='submit'>";
	$r .= get_submit_button( __( 'Delete' ), 'deletemeta small', "deletemeta[{$entry['meta_id']}]", false, array( 'data-wp-lists' => "delete:the-list:meta-{$entry['meta_id']}::_ajax_nonce=$delete_nonce" ) );
	$r .= "\n\t\t";
	$r .= get_submit_button( __( 'Update' ), 'updatemeta small', "meta-{$entry['meta_id']}-submit", false, array( 'data-wp-lists' => "add:the-list:meta-{$entry['meta_id']}::_ajax_nonce-add-meta=$update_nonce" ) );
	$r .= '</div>';
	$r .= wp_nonce_field( 'change-meta', '_ajax_nonce', false, false );
	$r .= '</td>';

	$r .= "\n\t\t<td><label class='screen-reader-text' for='meta-{$entry['meta_id']}-value'>" .
		/* translators: Hidden accessibility text. */
		__( 'Value' ) .
	"</label><textarea name='meta[{$entry['meta_id']}][value]' id='meta-{$entry['meta_id']}-value' rows='2' cols='30'>{$entry['meta_value']}</textarea></td>\n\t</tr>";
	return $r;
}

/**
 * Prints the form in the Custom Fields meta box.
 *
 * @since 1.2.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param WP_Post $post Optional. The post being edited.
 */
function meta_form( $post = null ) {
	global $wpdb;
	$post = get_post( $post );

	/**
	 * Filters values for the meta key dropdown in the Custom Fields meta box.
	 *
	 * Returning a non-null value will effectively short-circuit and avoid a
	 * potentially expensive query against postmeta.
	 *
	 * @since 4.4.0
	 *
	 * @param array|null $keys Pre-defined meta keys to be used in place of a postmeta query. Default null.
	 * @param WP_Post    $post The current post object.
	 */
	$keys = apply_filters( 'postmeta_form_keys', null, $post );

	if ( null === $keys ) {
		/**
		 * Filters the number of custom fields to retrieve for the drop-down
		 * in the Custom Fields meta box.
		 *
		 * @since 2.1.0
		 *
		 * @param int $limit Number of custom fields to retrieve. Default 30.
		 */
		$limit = apply_filters( 'postmeta_form_limit', 30 );

		$keys = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT DISTINCT meta_key
				FROM $wpdb->postmeta
				WHERE meta_key NOT BETWEEN '_' AND '_z'
				HAVING meta_key NOT LIKE %s
				ORDER BY meta_key
				LIMIT %d",
				$wpdb->esc_like( '_' ) . '%',
				$limit
			)
		);
	}

	if ( $keys ) {
		natcasesort( $keys );
	}
	?>
<p><strong><?php _e( 'Add Custom Field:' ); ?></strong></p>
<table id="newmeta">
<thead>
<tr>
<th class="left"><label for="metakeyselect"><?php _ex( 'Name', 'meta name' ); ?></label></th>
<th><label for="metavalue"><?php _e( 'Value' ); ?></label></th>
</tr>
</thead>

<tbody>
<tr>
<td id="newmetaleft" class="left">
	<?php if ( $keys ) { ?>
<select id="metakeyselect" name="metakeyselect">
<option value="#NONE#"><?php _e( '&mdash; Select &mdash;' ); ?></option>
		<?php
		foreach ( $keys as $key ) {
			if ( is_protected_meta( $key, 'post' ) || ! current_user_can( 'add_post_meta', $post->ID, $key ) ) {
				continue;
			}
			echo "\n<option value='" . esc_attr( $key ) . "'>" . esc_html( $key ) . '</option>';
		}
		?>
</select>
<input class="hidden" type="text" id="metakeyinput" name="metakeyinput" value="" aria-label="<?php _e( 'New custom field name' ); ?>" />
<button type="button" id="newmeta-button" class="button button-small hide-if-no-js" onclick="jQuery('#metakeyinput, #metakeyselect, #enternew, #cancelnew').toggleClass('hidden');jQuery('#metakeyinput, #metakeyselect').filter(':visible').trigger('focus');">
<span id="enternew"><?php _e( 'Enter new' ); ?></span>
<span id="cancelnew" class="hidden"><?php _e( 'Cancel' ); ?></span></button>
<?php } else { ?>
<input type="text" id="metakeyinput" name="metakeyinput" value="" />
<?php } ?>
</td>
<td><textarea id="metavalue" name="metavalue" rows="2" cols="25"></textarea>
	<?php wp_nonce_field( 'add-meta', '_ajax_nonce-add-meta', false ); ?>
</td>
</tr>
</tbody>
</table>
<div class="submit add-custom-field">
	<?php
	submit_button(
		__( 'Add Custom Field' ),
		'',
		'addmeta',
		false,
		array(
			'id'            => 'newmeta-submit',
			'data-wp-lists' => 'add:the-list:newmeta',
		)
	);
	?>
</div>
	<?php
}

/**
 * Prints out HTML form date elements for editing post or comment publish date.
 *
 * @since 0.71
 * @since 4.4.0 Converted to use get_comment() instead of the global `$comment`.
 *
 * @global WP_Locale $wp_locale WordPress date and time locale object.
 *
 * @param int|bool $edit      Accepts 1|true for editing the date, 0|false for adding the date.
 * @param int|bool $for_post  Accepts 1|true for applying the date to a post, 0|false for a comment.
 * @param int      $tab_index The tabindex attribute to add. Default 0.
 * @param int|bool $multi     Optional. Whether the additional fields and buttons should be added.
 *                            Default 0|false.
 */
function touch_time( $edit = 1, $for_post = 1, $tab_index = 0, $multi = 0 ) {
	global $wp_locale;
	$post = get_post();

	if ( $for_post ) {
		$edit = ! ( in_array( $post->post_status, array( 'draft', 'pending' ), true ) && ( ! $post->post_date_gmt || '0000-00-00 00:00:00' === $post->post_date_gmt ) );
	}

	$tab_index_attribute = '';
	if ( (int) $tab_index > 0 ) {
		$tab_index_attribute = " tabindex=\"$tab_index\"";
	}

	// @todo Remove this?
	// echo '<label for="timestamp" style="display: block;"><input type="checkbox" class="checkbox" name="edit_date" value="1" id="timestamp"'.$tab_index_attribute.' /> '.__( 'Edit timestamp' ).'</label><br />';

	$post_date = ( $for_post ) ? $post->post_date : get_comment()->comment_date;
	$jj        = ( $edit ) ? mysql2date( 'd', $post_date, false ) : current_time( 'd' );
	$mm        = ( $edit ) ? mysql2date( 'm', $post_date, false ) : current_time( 'm' );
	$aa        = ( $edit ) ? mysql2date( 'Y', $post_date, false ) : current_time( 'Y' );
	$hh        = ( $edit ) ? mysql2date( 'H', $post_date, false ) : current_time( 'H' );
	$mn        = ( $edit ) ? mysql2date( 'i', $post_date, false ) : current_time( 'i' );
	$ss        = ( $edit ) ? mysql2date( 's', $post_date, false ) : current_time( 's' );

	$cur_jj = current_time( 'd' );
	$cur_mm = current_time( 'm' );
	$cur_aa = current_time( 'Y' );
	$cur_hh = current_time( 'H' );
	$cur_mn = current_time( 'i' );

	$month = '<label><span class="screen-reader-text">' .
		/* translators: Hidden accessibility text. */
		__( 'Month' ) .
	'</span><select class="form-required" ' . ( $multi ? '' : 'id="mm" ' ) . 'name="mm"' . $tab_index_attribute . ">\n";
	for ( $i = 1; $i < 13; $i = $i + 1 ) {
		$monthnum  = zeroise( $i, 2 );
		$monthtext = $wp_locale->get_month_abbrev( $wp_locale->get_month( $i ) );
		$month    .= "\t\t\t" . '<option value="' . $monthnum . '" data-text="' . $monthtext . '" ' . selected( $monthnum, $mm, false ) . '>';
		/* translators: 1: Month number (01, 02, etc.), 2: Month abbreviation. */
		$month .= sprintf( __( '%1$s-%2$s' ), $monthnum, $monthtext ) . "</option>\n";
	}
	$month .= '</select></label>';

	$day = '<label><span class="screen-reader-text">' .
		/* translators: Hidden accessibility text. */
		__( 'Day' ) .
	'</span><input type="text" ' . ( $multi ? '' : 'id="jj" ' ) . 'name="jj" value="' . $jj . '" size="2" maxlength="2"' . $tab_index_attribute . ' autocomplete="off" class="form-required" inputmode="numeric" /></label>';
	$year = '<label><span class="screen-reader-text">' .
		/* translators: Hidden accessibility text. */
		__( 'Year' ) .
	'</span><input type="text" ' . ( $multi ? '' : 'id="aa" ' ) . 'name="aa" value="' . $aa . '" size="4" maxlength="4"' . $tab_index_attribute . ' autocomplete="off" class="form-required" inputmode="numeric" /></label>';
	$hour = '<label><span class="screen-reader-text">' .
		/* translators: Hidden accessibility text. */
		__( 'Hour' ) .
	'</span><input type="text" ' . ( $multi ? '' : 'id="hh" ' ) . 'name="hh" value="' . $hh . '" size="2" maxlength="2"' . $tab_index_attribute . ' autocomplete="off" class="form-required" inputmode="numeric" /></label>';
	$minute = '<label><span class="screen-reader-text">' .
		/* translators: Hidden accessibility text. */
		__( 'Minute' ) .
	'</span><input type="text" ' . ( $multi ? '' : 'id="mn" ' ) . 'name="mn" value="' . $mn . '" size="2" maxlength="2"' . $tab_index_attribute . ' autocomplete="off" class="form-required" inputmode="numeric" /></label>';

	echo '<div class="timestamp-wrap">';
	/* translators: 1: Month, 2: Day, 3: Year, 4: Hour, 5: Minute. */
	printf( __( '%1$s %2$s, %3$s at %4$s:%5$s' ), $month, $day, $year, $hour, $minute );

	echo '</div><input type="hidden" id="ss" name="ss" value="' . $ss . '" />';

	if ( $multi ) {
		return;
	}

	echo "\n\n";

	$map = array(
		'mm' => array( $mm, $cur_mm ),
		'jj' => array( $jj, $cur_jj ),
		'aa' => array( $aa, $cur_aa ),
		'hh' => array( $hh, $cur_hh ),
		'mn' => array( $mn, $cur_mn ),
	);

	foreach ( $map as $timeunit => $value ) {
		list( $unit, $curr ) = $value;

		echo '<input type="hidden" id="hidden_' . $timeunit . '" name="hidden_' . $timeunit . '" value="' . $unit . '" />' . "\n";
		$cur_timeunit = 'cur_' . $timeunit;
		echo '<input type="hidden" id="' . $cur_timeunit . '" name="' . $cur_timeunit . '" value="' . $curr . '" />' . "\n";
	}
	?>

<p>
<a href="#edit_timestamp" class="save-timestamp hide-if-no-js button"><?php _e( 'OK' ); ?></a>
<a href="#edit_timestamp" class="cancel-timestamp hide-if-no-js button-cancel"><?php _e( 'Cancel' ); ?></a>
</p>
	<?php
}

/**
 * Prints out option HTML elements for the page templates drop-down.
 *
 * @since 1.5.0
 * @since 4.7.0 Added the `$post_type` parameter.
 *
 * @param string $default_template Optional. The template file name. Default empty.
 * @param string $post_type        Optional. Post type to get templates for. Default 'page'.
 */
function page_template_dropdown( $default_template = '', $post_type = 'page' ) {
	$templates = get_page_templates( null, $post_type );

	ksort( $templates );

	foreach ( array_keys( $templates ) as $template ) {
		$selected = selected( $default_template, $templates[ $template ], false );
		echo "\n\t<option value='" . esc_attr( $templates[ $template ] ) . "' $selected>" . esc_html( $template ) . '</option>';
	}
}

/**
 * Prints out option HTML elements for the page parents drop-down.
 *
 * @since 1.5.0
 * @since 4.4.0 `$post` argument was added.
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param int         $default_page Optional. The default page ID to be pre-selected. Default 0.
 * @param int         $parent_page  Optional. The parent page ID. Default 0.
 * @param int         $level        Optional. Page depth level. Default 0.
 * @param int|WP_Post $post         Post ID or WP_Post object.
 * @return void|false Void on success, false if the page has no children.
 */
function parent_dropdown( $default_page = 0, $parent_page = 0, $level = 0, $post = null ) {
	global $wpdb;

	$post  = get_post( $post );
	$items = $wpdb->get_results(
		$wpdb->prepare(
			"SELECT ID, post_parent, post_title
			FROM $wpdb->posts
			WHERE post_parent = %d AND post_type = 'page'
			ORDER BY menu_order",
			$parent_page
		)
	);

	if ( $items ) {
		foreach ( $items as $item ) {
			// A page cannot be its own parent.
			if ( $post && $post->ID && (int) $item->ID === $post->ID ) {
				continue;
			}

			$pad      = str_repeat( '&nbsp;', $level * 3 );
			$selected = selected( $default_page, $item->ID, false );

			echo "\n\t<option class='level-$level' value='$item->ID' $selected>$pad " . esc_html( $item->post_title ) . '</option>';
			parent_dropdown( $default_page, $item->ID, $level + 1 );
		}
	} else {
		return false;
	}
}

/**
 * Prints out option HTML elements for role selectors.
 *
 * @since 2.1.0
 *
 * @param string $selected Slug for the role that should be already selected.
 */
function wp_dropdown_roles( $selected = '' ) {
	$r = '';

	$editable_roles = array_reverse( get_editable_roles() );

	foreach ( $editable_roles as $role => $details ) {
		$name = translate_user_role( $details['name'] );
		// Preselect specified role.
		if ( $selected === $role ) {
			$r .= "\n\t<option selected='selected' value='" . esc_attr( $role ) . "'>$name</option>";
		} else {
			$r .= "\n\t<option value='" . esc_attr( $role ) . "'>$name</option>";
		}
	}

	echo $r;
}

/**
 * Outputs the form used by the importers to accept the data to be imported.
 *
 * @since 2.0.0
 *
 * @param string $action The action attribute for the form.
 */
function wp_import_upload_form( $action ) {

	/**
	 * Filters the maximum allowed upload size for import files.
	 *
	 * @since 2.3.0
	 *
	 * @see wp_max_upload_size()
	 *
	 * @param int $max_upload_size Allowed upload size. Default 1 MB.
	 */
	$bytes      = apply_filters( 'import_upload_size_limit', wp_max_upload_size() );
	$size       = size_format( $bytes );
	$upload_dir = wp_upload_dir();
	if ( ! empty( $upload_dir['error'] ) ) :
		$upload_directory_error  = '<p>' . __( 'Before you can upload your import file, you will need to fix the following error:' ) . '</p>';
		$upload_directory_error .= '<p><strong>' . $upload_dir['error'] . '</strong></p>';
		wp_admin_notice(
			$upload_directory_error,
			array(
				'additional_classes' => array( 'error' ),
				'paragraph_wrap'     => false,
			)
		);
	else :
		?>
<form enctype="multipart/form-data" id="import-upload-form" method="post" class="wp-upload-form" action="<?php echo esc_url( wp_nonce_url( $action, 'import-upload' ) ); ?>">
<p>
		<?php
		printf(
			'<label for="upload">%s</label> (%s)',
			__( 'Choose a file from your computer:' ),
			/* translators: %s: Maximum allowed file size. */
			sprintf( __( 'Maximum size: %s' ), $size )
		);
		?>
<input type="file" id="upload" name="import" size="25" />
<input type="hidden" name="action" value="save" />
<input type="hidden" name="max_file_size" value="<?php echo $bytes; ?>" />
</p>
		<?php submit_button( __( 'Upload file and import' ), 'primary' ); ?>
</form>
		<?php
	endif;
}

/**
 * Adds a meta box to one or more screens.
 *
 * @since 2.5.0
 * @since 4.4.0 The `$screen` parameter now accepts an array of screen IDs.
 *
 * @global array $wp_meta_boxes Global meta box state.
 *
 * @param string                 $id            Meta box ID (used in the 'id' attribute for the meta box).
 * @param string                 $title         Title of the meta box.
 * @param callable               $callback      Function that fills the box with the desired content.
 *                                              The function should echo its output.
 * @param string|array|WP_Screen $screen        Optional. The screen or screens on which to show the box
 *                                              (such as a post type, 'link', or 'comment'). Accepts a single
 *                                              screen ID, WP_Screen object, or array of screen IDs. Default
 *                                              is the current screen.  If you have used add_menu_page() or
 *                                              add_submenu_page() to create a new screen (and hence screen_id),
 *                                              make sure your menu slug conforms to the limits of sanitize_key()
 *                                              otherwise the 'screen' menu may not correctly render on your page.
 * @param string                 $context       Optional. The context within the screen where the box
 *                                              should display. Available contexts vary from screen to
 *                                              screen. Post edit screen contexts include 'normal', 'side',
 *                                              and 'advanced'. Comments screen contexts include 'normal'
 *                                              and 'side'. Menus meta boxes (accordion sections) all use
 *                                              the 'side' context. Global default is 'advanced'.
 * @param string                 $priority      Optional. The priority within the context where the box should show.
 *                                              Accepts 'high', 'core', 'default', or 'low'. Default 'default'.
 * @param array                  $callback_args Optional. Data that should be set as the $args property
 *                                              of the box array (which is the second parameter passed
 *                                              to your callback). Default null.
 */
function add_meta_box( $id, $title, $callback, $screen = null, $context = 'advanced', $priority = 'default', $callback_args = null ) {
	global $wp_meta_boxes;

	if ( empty( $screen ) ) {
		$screen = get_current_screen();
	} elseif ( is_string( $screen ) ) {
		$screen = convert_to_screen( $screen );
	} elseif ( is_array( $screen ) ) {
		foreach ( $screen as $single_screen ) {
			add_meta_box( $id, $title, $callback, $single_screen, $context, $priority, $callback_args );
		}
	}

	if ( ! isset( $screen->id ) ) {
		return;
	}

	$page = $screen->id;

	if ( ! isset( $wp_meta_boxes ) ) {
		$wp_meta_boxes = array();
	}
	if ( ! isset( $wp_meta_boxes[ $page ] ) ) {
		$wp_meta_boxes[ $page ] = array();
	}
	if ( ! isset( $wp_meta_boxes[ $page ][ $context ] ) ) {
		$wp_meta_boxes[ $page ][ $context ] = array();
	}

	foreach ( array_keys( $wp_meta_boxes[ $page ] ) as $a_context ) {
		foreach ( array( 'high', 'core', 'default', 'low' ) as $a_priority ) {
			if ( ! isset( $wp_meta_boxes[ $page ][ $a_context ][ $a_priority ][ $id ] ) ) {
				continue;
			}

			// If a core box was previously removed, don't add.
			if ( ( 'core' === $priority || 'sorted' === $priority )
				&& false === $wp_meta_boxes[ $page ][ $a_context ][ $a_priority ][ $id ]
			) {
				return;
			}

			// If a core box was previously added by a plugin, don't add.
			if ( 'core' === $priority ) {
				/*
				 * If the box was added with default priority, give it core priority
				 * to maintain sort order.
				 */
				if ( 'default' === $a_priority ) {
					$wp_meta_boxes[ $page ][ $a_context ]['core'][ $id ] = $wp_meta_boxes[ $page ][ $a_context ]['default'][ $id ];
					unset( $wp_meta_boxes[ $page ][ $a_context ]['default'][ $id ] );
				}
				return;
			}

			// If no priority given and ID already present, use existing priority.
			if ( empty( $priority ) ) {
				$priority = $a_priority;
				/*
				 * Else, if we're adding to the sorted priority, we don't know the title
				 * or callback. Grab them from the previously added context/priority.
				 */
			} elseif ( 'sorted' === $priority ) {
				$title         = $wp_meta_boxes[ $page ][ $a_context ][ $a_priority ][ $id ]['title'];
				$callback      = $wp_meta_boxes[ $page ][ $a_context ][ $a_priority ][ $id ]['callback'];
				$callback_args = $wp_meta_boxes[ $page ][ $a_context ][ $a_priority ][ $id ]['args'];
			}

			// An ID can be in only one priority and one context.
			if ( $priority !== $a_priority || $context !== $a_context ) {
				unset( $wp_meta_boxes[ $page ][ $a_context ][ $a_priority ][ $id ] );
			}
		}
	}

	if ( empty( $priority ) ) {
		$priority = 'low';
	}

	if ( ! isset( $wp_meta_boxes[ $page ][ $context ][ $priority ] ) ) {
		$wp_meta_boxes[ $page ][ $context ][ $priority ] = array();
	}

	$wp_meta_boxes[ $page ][ $context ][ $priority ][ $id ] = array(
		'id'       => $id,
		'title'    => $title,
		'callback' => $callback,
		'args'     => $callback_args,
	);
}


/**
 * Renders a "fake" meta box with an information message,
 * shown on the block editor, when an incompatible meta box is found.
 *
 * @since 5.0.0
 *
 * @param mixed $data_object The data object being rendered on this screen.
 * @param array $box         {
 *     Custom formats meta box arguments.
 *
 *     @type string   $id           Meta box 'id' attribute.
 *     @type string   $title        Meta box title.
 *     @type callable $old_callback The original callback for this meta box.
 *     @type array    $args         Extra meta box arguments.
 * }
 */
function do_block_editor_incompatible_meta_box( $data_object, $box ) {
	$plugin  = _get_plugin_from_callback( $box['old_callback'] );
	$plugins = get_plugins();
	echo '<p>';
	if ( $plugin ) {
		/* translators: %s: The name of the plugin that generated this meta box. */
		printf( __( 'This meta box, from the %s plugin, is not compatible with the block editor.' ), "<strong>{$plugin['Name']}</strong>" );
	} else {
		_e( 'This meta box is not compatible with the block editor.' );
	}
	echo '</p>';

	if ( empty( $plugins['classic-editor/classic-editor.php'] ) ) {
		if ( current_user_can( 'install_plugins' ) ) {
			$install_url = wp_nonce_url(
				self_admin_url( 'plugin-install.php?tab=favorites&user=wordpressdotorg&save=0' ),
				'save_wporg_username_' . get_current_user_id()
			);

			echo '<p>';
			/* translators: %s: A link to install the Classic Editor plugin. */
			printf( __( 'Please install the <a href="%s">Classic Editor plugin</a> to use this meta box.' ), esc_url( $install_url ) );
			echo '</p>';
		}
	} elseif ( is_plugin_inactive( 'classic-editor/classic-editor.php' ) ) {
		if ( current_user_can( 'activate_plugins' ) ) {
			$activate_url = wp_nonce_url(
				self_admin_url( 'plugins.php?action=activate&plugin=classic-editor/classic-editor.php' ),
				'activate-plugin_classic-editor/classic-editor.php'
			);

			echo '<p>';
			/* translators: %s: A link to activate the Classic Editor plugin. */
			printf( __( 'Please activate the <a href="%s">Classic Editor plugin</a> to use this meta box.' ), esc_url( $activate_url ) );
			echo '</p>';
		}
	} elseif ( $data_object instanceof WP_Post ) {
		$edit_url = add_query_arg(
			array(
				'classic-editor'         => '',
				'classic-editor__forget' => '',
			),
			get_edit_post_link( $data_object )
		);
		echo '<p>';
		/* translators: %s: A link to use the Classic Editor plugin. */
		printf( __( 'Please open the <a href="%s">classic editor</a> to use this meta box.' ), esc_url( $edit_url ) );
		echo '</p>';
	}
}

/**
 * Internal helper function to find the plugin from a meta box callback.
 *
 * @since 5.0.0
 *
 * @access private
 *
 * @param callable $callback The callback function to check.
 * @return array|null The plugin that the callback belongs to, or null if it doesn't belong to a plugin.
 */
function _get_plugin_from_callback( $callback ) {
	try {
		if ( is_array( $callback ) ) {
			$reflection = new ReflectionMethod( $callback[0], $callback[1] );
		} elseif ( is_string( $callback ) && str_contains( $callback, '::' ) ) {
			$reflection = new ReflectionMethod( $callback );
		} else {
			$reflection = new ReflectionFunction( $callback );
		}
	} catch ( ReflectionException $exception ) {
		// We could not properly reflect on the callable, so we abort here.
		return null;
	}

	// Don't show an error if it's an internal PHP function.
	if ( ! $reflection->isInternal() ) {

		// Only show errors if the meta box was registered by a plugin.
		$filename   = wp_normalize_path( $reflection->getFileName() );
		$plugin_dir = wp_normalize_path( WP_PLUGIN_DIR );

		if ( str_starts_with( $filename, $plugin_dir ) ) {
			$filename = str_replace( $plugin_dir, '', $filename );
			$filename = preg_replace( '|^/([^/]*/).*$|', '\\1', $filename );

			$plugins = get_plugins();

			foreach ( $plugins as $name => $plugin ) {
				if ( str_starts_with( $name, $filename ) ) {
					return $plugin;
				}
			}
		}
	}

	return null;
}

/**
 * Meta-Box template function.
 *
 * @since 2.5.0
 *
 * @global array $wp_meta_boxes Global meta box state.
 *
 * @param string|WP_Screen $screen      The screen identifier. If you have used add_menu_page() or
 *                                      add_submenu_page() to create a new screen (and hence screen_id)
 *                                      make sure your menu slug conforms to the limits of sanitize_key()
 *                                      otherwise the 'screen' menu may not correctly render on your page.
 * @param string           $context     The screen context for which to display meta boxes.
 * @param mixed            $data_object Gets passed to the meta box callback function as the first parameter.
 *                                      Often this is the object that's the focus of the current screen,
 *                                      for example a `WP_Post` or `WP_Comment` object.
 * @return int Number of meta_boxes.
 */
function do_meta_boxes( $screen, $context, $data_object ) {
	global $wp_meta_boxes;
	static $already_sorted = false;

	if ( empty( $screen ) ) {
		$screen = get_current_screen();
	} elseif ( is_string( $screen ) ) {
		$screen = convert_to_screen( $screen );
	}

	$page = $screen->id;

	$hidden = get_hidden_meta_boxes( $screen );

	printf( '<div id="%s-sortables" class="meta-box-sortables">', esc_attr( $context ) );

	/*
	 * Grab the ones the user has manually sorted.
	 * Pull them out of their previous context/priority and into the one the user chose.
	 */
	$sorted = get_user_option( "meta-box-order_$page" );

	if ( ! $already_sorted && $sorted ) {
		foreach ( $sorted as $box_context => $ids ) {
			foreach ( explode( ',', $ids ) as $id ) {
				if ( $id && 'dashboard_browser_nag' !== $id ) {
					add_meta_box( $id, null, null, $screen, $box_context, 'sorted' );
				}
			}
		}
	}

	$already_sorted = true;

	$i = 0;

	if ( isset( $wp_meta_boxes[ $page ][ $context ] ) ) {
		foreach ( array( 'high', 'sorted', 'core', 'default', 'low' ) as $priority ) {
			if ( isset( $wp_meta_boxes[ $page ][ $context ][ $priority ] ) ) {
				foreach ( (array) $wp_meta_boxes[ $page ][ $context ][ $priority ] as $box ) {
					if ( false === $box || ! $box['title'] ) {
						continue;
					}

					$block_compatible = true;
					if ( is_array( $box['args'] ) ) {
						// If a meta box is just here for back compat, don't show it in the block editor.
						if ( $screen->is_block_editor() && isset( $box['args']['__back_compat_meta_box'] ) && $box['args']['__back_compat_meta_box'] ) {
							continue;
						}

						if ( isset( $box['args']['__block_editor_compatible_meta_box'] ) ) {
							$block_compatible = (bool) $box['args']['__block_editor_compatible_meta_box'];
							unset( $box['args']['__block_editor_compatible_meta_box'] );
						}

						// If the meta box is declared as incompatible with the block editor, override the callback function.
						if ( ! $block_compatible && $screen->is_block_editor() ) {
							$box['old_callback'] = $box['callback'];
							$box['callback']     = 'do_block_editor_incompatible_meta_box';
						}

						if ( isset( $box['args']['__back_compat_meta_box'] ) ) {
							$block_compatible = $block_compatible || (bool) $box['args']['__back_compat_meta_box'];
							unset( $box['args']['__back_compat_meta_box'] );
						}
					}

					++$i;
					// get_hidden_meta_boxes() doesn't apply in the block editor.
					$hidden_class = ( ! $screen->is_block_editor() && in_array( $box['id'], $hidden, true ) ) ? ' hide-if-js' : '';
					echo '<div id="' . $box['id'] . '" class="postbox ' . postbox_classes( $box['id'], $page ) . $hidden_class . '" ' . '>' . "\n";

					echo '<div class="postbox-header">';
					echo '<h2 class="hndle">';
					if ( 'dashboard_php_nag' === $box['id'] ) {
						echo '<span aria-hidden="true" class="dashicons dashicons-warning"></span>';
						echo '<span class="screen-reader-text">' .
							/* translators: Hidden accessibility text. */
							__( 'Warning:' ) .
						' </span>';
					}
					echo $box['title'];
					echo "</h2>\n";

					if ( 'dashboard_browser_nag' !== $box['id'] ) {
						$widget_title = $box['title'];

						if ( is_array( $box['args'] ) && isset( $box['args']['__widget_basename'] ) ) {
							$widget_title = $box['args']['__widget_basename'];
							// Do not pass this parameter to the user callback function.
							unset( $box['args']['__widget_basename'] );
						}

						echo '<div class="handle-actions hide-if-no-js">';

						echo '<button type="button" class="handle-order-higher" aria-disabled="false" aria-describedby="' . $box['id'] . '-handle-order-higher-description">';
						echo '<span class="screen-reader-text">' .
							/* translators: Hidden accessibility text. */
							__( 'Move up' ) .
						'</span>';
						echo '<span class="order-higher-indicator" aria-hidden="true"></span>';
						echo '</button>';
						echo '<span class="hidden" id="' . $box['id'] . '-handle-order-higher-description">' . sprintf(
							/* translators: %s: Meta box title. */
							__( 'Move %s box up' ),
							$widget_title
						) . '</span>';

						echo '<button type="button" class="handle-order-lower" aria-disabled="false" aria-describedby="' . $box['id'] . '-handle-order-lower-description">';
						echo '<span class="screen-reader-text">' .
							/* translators: Hidden accessibility text. */
							__( 'Move down' ) .
						'</span>';
						echo '<span class="order-lower-indicator" aria-hidden="true"></span>';
						echo '</button>';
						echo '<span class="hidden" id="' . $box['id'] . '-handle-order-lower-description">' . sprintf(
							/* translators: %s: Meta box title. */
							__( 'Move %s box down' ),
							$widget_title
						) . '</span>';

						echo '<button type="button" class="handlediv" aria-expanded="true">';
						echo '<span class="screen-reader-text">' . sprintf(
							/* translators: %s: Hidden accessibility text. Meta box title. */
							__( 'Toggle panel: %s' ),
							$widget_title
						) . '</span>';
						echo '<span class="toggle-indicator" aria-hidden="true"></span>';
						echo '</button>';

						echo '</div>';
					}
					echo '</div>';

					echo '<div class="inside">' . "\n";

					if ( WP_DEBUG && ! $block_compatible && 'edit' === $screen->parent_base && ! $screen->is_block_editor() && ! isset( $_GET['meta-box-loader'] ) ) {
						$plugin = _get_plugin_from_callback( $box['callback'] );
						if ( $plugin ) {
							$meta_box_not_compatible_message = sprintf(
								/* translators: %s: The name of the plugin that generated this meta box. */
								__( 'This meta box, from the %s plugin, is not compatible with the block editor.' ),
								"<strong>{$plugin['Name']}</strong>"
							);
							wp_admin_notice(
								$meta_box_not_compatible_message,
								array(
									'additional_classes' => array( 'error', 'inline' ),
								)
							);
						}
					}

					call_user_func( $box['callback'], $data_object, $box );
					echo "</div>\n";
					echo "</div>\n";
				}
			}
		}
	}

	echo '</div>';

	return $i;
}

/**
 * Removes a meta box from one or more screens.
 *
 * @since 2.6.0
 * @since 4.4.0 The `$screen` parameter now accepts an array of screen IDs.
 *
 * @global array $wp_meta_boxes Global meta box state.
 *
 * @param string                 $id      Meta box ID (used in the 'id' attribute for the meta box).
 * @param string|array|WP_Screen $screen  The screen or screens on which the meta box is shown (such as a
 *                                        post type, 'link', or 'comment'). Accepts a single screen ID,
 *                                        WP_Screen object, or array of screen IDs.
 * @param string                 $context The context within the screen where the box is set to display.
 *                                        Contexts vary from screen to screen. Post edit screen contexts
 *                                        include 'normal', 'side', and 'advanced'. Comments screen contexts
 *                                        include 'normal' and 'side'. Menus meta boxes (accordion sections)
 *                                        all use the 'side' context.
 */
function remove_meta_box( $id, $screen, $context ) {
	global $wp_meta_boxes;

	if ( empty( $screen ) ) {
		$screen = get_current_screen();
	} elseif ( is_string( $screen ) ) {
		$screen = convert_to_screen( $screen );
	} elseif ( is_array( $screen ) ) {
		foreach ( $screen as $single_screen ) {
			remove_meta_box( $id, $single_screen, $context );
		}
	}

	if ( ! isset( $screen->id ) ) {
		return;
	}

	$page = $screen->id;

	if ( ! isset( $wp_meta_boxes ) ) {
		$wp_meta_boxes = array();
	}
	if ( ! isset( $wp_meta_boxes[ $page ] ) ) {
		$wp_meta_boxes[ $page ] = array();
	}
	if ( ! isset( $wp_meta_boxes[ $page ][ $context ] ) ) {
		$wp_meta_boxes[ $page ][ $context ] = array();
	}

	foreach ( array( 'high', 'core', 'default', 'low' ) as $priority ) {
		$wp_meta_boxes[ $page ][ $context ][ $priority ][ $id ] = false;
	}
}

/**
 * Meta Box Accordion Template Function.
 *
 * Largely made up of abstracted code from do_meta_boxes(), this
 * function serves to build meta boxes as list items for display as
 * a collapsible accordion.
 *
 * @since 3.6.0
 *
 * @uses global $wp_meta_boxes Used to retrieve registered meta boxes.
 *
 * @param string|object $screen      The screen identifier.
 * @param string        $context     The screen context for which to display accordion sections.
 * @param mixed         $data_object Gets passed to the section callback function as the first parameter.
 * @return int Number of meta boxes as accordion sections.
 */
function do_accordion_sections( $screen, $context, $data_object ) {
	global $wp_meta_boxes;

	wp_enqueue_script( 'accordion' );

	if ( empty( $screen ) ) {
		$screen = get_current_screen();
	} elseif ( is_string( $screen ) ) {
		$screen = convert_to_screen( $screen );
	}

	$page = $screen->id;

	$hidden = get_hidden_meta_boxes( $screen );
	?>
	<div id="side-sortables" class="accordion-container">
		<ul class="outer-border">
	<?php
	$i          = 0;
	$first_open = false;

	if ( isset( $wp_meta_boxes[ $page ][ $context ] ) ) {
		foreach ( array( 'high', 'core', 'default', 'low' ) as $priority ) {
			if ( isset( $wp_meta_boxes[ $page ][ $context ][ $priority ] ) ) {
				foreach ( $wp_meta_boxes[ $page ][ $context ][ $priority ] as $box ) {
					if ( false === $box || ! $box['title'] ) {
						continue;
					}

					++$i;
					$hidden_class = in_array( $box['id'], $hidden, true ) ? 'hide-if-js' : '';

					$open_class    = '';
					$aria_expanded = 'false';
					if ( ! $first_open && empty( $hidden_class ) ) {
						$first_open    = true;
						$open_class    = 'open';
						$aria_expanded = 'true';
					}
					?>
					<li class="control-section accordion-section <?php echo $hidden_class; ?> <?php echo $open_class; ?> <?php echo esc_attr( $box['id'] ); ?>" id="<?php echo esc_attr( $box['id'] ); ?>">
						<h3 class="accordion-section-title hndle">
							<button type="button" class="accordion-trigger" aria-expanded="<?php echo $aria_expanded; ?>" aria-controls="<?php echo esc_attr( $box['id'] ); ?>-content">
								<span class="accordion-title">
									<?php echo esc_html( $box['title'] ); ?>
									<span class="dashicons dashicons-arrow-down" aria-hidden="true"></span>
								</span>
							</button>
						</h3>
						<div class="accordion-section-content <?php postbox_classes( $box['id'], $page ); ?>" id="<?php echo esc_attr( $box['id'] ); ?>-content">
							<div class="inside">
								<?php call_user_func( $box['callback'], $data_object, $box ); ?>
							</div><!-- .inside -->
						</div><!-- .accordion-section-content -->
					</li><!-- .accordion-section -->
					<?php
				}
			}
		}
	}
	?>
		</ul><!-- .outer-border -->
	</div><!-- .accordion-container -->
	<?php
	return $i;
}

/**
 * Adds a new section to a settings page.
 *
 * Part of the Settings API. Use this to define new settings sections for an admin page.
 * Show settings sections in your admin page callback function with do_settings_sections().
 * Add settings fields to your section with add_settings_field().
 *
 * The $callback argument should be the name of a function that echoes out any
 * content you want to show at the top of the settings section before the actual
 * fields. It can output nothing if you want.
 *
 * @since 2.7.0
 * @since 6.1.0 Added an `$args` parameter for the section's HTML wrapper and class name.
 *
 * @global array $wp_settings_sections Storage array of all settings sections added to admin pages.
 *
 * @param string   $id       Slug-name to identify the section. Used in the 'id' attribute of tags.
 * @param string   $title    Formatted title of the section. Shown as the heading for the section.
 * @param callable $callback Function that echos out any content at the top of the section (between heading and fields).
 * @param string   $page     The slug-name of the settings page on which to show the section. Built-in pages include
 *                           'general', 'reading', 'writing', 'discussion', 'media', etc. Create your own using
 *                           add_options_page();
 * @param array    $args     {
 *     Arguments used to create the settings section.
 *
 *     @type string $before_section HTML content to prepend to the section's HTML output.
 *                                  Receives the section's class name as `%s`. Default empty.
 *     @type string $after_section  HTML content to append to the section's HTML output. Default empty.
 *     @type string $section_class  The class name to use for the section. Default empty.
 * }
 */
function add_settings_section( $id, $title, $callback, $page, $args = array() ) {
	global $wp_settings_sections;

	$defaults = array(
		'id'             => $id,
		'title'          => $title,
		'callback'       => $callback,
		'before_section' => '',
		'after_section'  => '',
		'section_class'  => '',
	);

	$section = wp_parse_args( $args, $defaults );

	if ( 'misc' === $page ) {
		_deprecated_argument(
			__FUNCTION__,
			'3.0.0',
			sprintf(
				/* translators: %s: misc */
				__( 'The "%s" options group has been removed. Use another settings group.' ),
				'misc'
			)
		);
		$page = 'general';
	}

	if ( 'privacy' === $page ) {
		_deprecated_argument(
			__FUNCTION__,
			'3.5.0',
			sprintf(
				/* translators: %s: privacy */
				__( 'The "%s" options group has been removed. Use another settings group.' ),
				'privacy'
			)
		);
		$page = 'reading';
	}

	$wp_settings_sections[ $page ][ $id ] = $section;
}

/**
 * Adds a new field to a section of a settings page.
 *
 * Part of the Settings API. Use this to define a settings field that will show
 * as part of a settings section inside a settings page. The fields are shown using
 * do_settings_fields() in do_settings_sections().
 *
 * The $callback argument should be the name of a function that echoes out the
 * HTML input tags for this setting field. Use get_option() to retrieve existing
 * values to show.
 *
 * @since 2.7.0
 * @since 4.2.0 The `$class` argument was added.
 *
 * @global array $wp_settings_fields Storage array of settings fields and info about their pages/sections.
 *
 * @param string   $id       Slug-name to identify the field. Used in the 'id' attribute of tags.
 * @param string   $title    Formatted title of the field. Shown as the label for the field
 *                           during output.
 * @param callable $callback Function that fills the field with the desired form inputs. The
 *                           function should echo its output.
 * @param string   $page     The slug-name of the settings page on which to show the section
 *                           (general, reading, writing, ...).
 * @param string   $section  Optional. The slug-name of the section of the settings page
 *                           in which to show the box. Default 'default'.
 * @param array    $args {
 *     Optional. Extra arguments that get passed to the callback function.
 *
 *     @type string $label_for When supplied, the setting title will be wrapped
 *                             in a `<label>` element, its `for` attribute populated
 *                             with this value.
 *     @type string $class     CSS Class to be added to the `<tr>` element when the
 *                             field is output.
 * }
 */
function add_settings_field( $id, $title, $callback, $page, $section = 'default', $args = array() ) {
	global $wp_settings_fields;

	if ( 'misc' === $page ) {
		_deprecated_argument(
			__FUNCTION__,
			'3.0.0',
			sprintf(
				/* translators: %s: misc */
				__( 'The "%s" options group has been removed. Use another settings group.' ),
				'misc'
			)
		);
		$page = 'general';
	}

	if ( 'privacy' === $page ) {
		_deprecated_argument(
			__FUNCTION__,
			'3.5.0',
			sprintf(
				/* translators: %s: privacy */
				__( 'The "%s" options group has been removed. Use another settings group.' ),
				'privacy'
			)
		);
		$page = 'reading';
	}

	$wp_settings_fields[ $page ][ $section ][ $id ] = array(
		'id'       => $id,
		'title'    => $title,
		'callback' => $callback,
		'args'     => $args,
	);
}

/**
 * Prints out all settings sections added to a particular settings page.
 *
 * Part of the Settings API. Use this in a settings page callback function
 * to output all the sections and fields that were added to that $page with
 * add_settings_section() and add_settings_field()
 *
 * @global array $wp_settings_sections Storage array of all settings sections added to admin pages.
 * @global array $wp_settings_fields Storage array of settings fields and info about their pages/sections.
 * @since 2.7.0
 *
 * @param string $page The slug name of the page whose settings sections you want to output.
 */
function do_settings_sections( $page ) {
	global $wp_settings_sections, $wp_settings_fields;

	if ( ! isset( $wp_settings_sections[ $page ] ) ) {
		return;
	}

	foreach ( (array) $wp_settings_sections[ $page ] as $section ) {
		if ( '' !== $section['before_section'] ) {
			if ( '' !== $section['section_class'] ) {
				echo wp_kses_post( sprintf( $section['before_section'], esc_attr( $section['section_class'] ) ) );
			} else {
				echo wp_kses_post( $section['before_section'] );
			}
		}

		if ( $section['title'] ) {
			echo "<h2>{$section['title']}</h2>\n";
		}

		if ( $section['callback'] ) {
			call_user_func( $section['callback'], $section );
		}

		if ( isset( $wp_settings_fields[ $page ][ $section['id'] ] ) ) {
			echo '<table class="form-table" role="presentation">';
			do_settings_fields( $page, $section['id'] );
			echo '</table>';
		}

		if ( '' !== $section['after_section'] ) {
			echo wp_kses_post( $section['after_section'] );
		}
	}
}

/**
 * Prints out the settings fields for a particular settings section.
 *
 * Part of the Settings API. Use this in a settings page to output
 * a specific section. Should normally be called by do_settings_sections()
 * rather than directly.
 *
 * @global array $wp_settings_fields Storage array of settings fields and their pages/sections.
 *
 * @since 2.7.0
 *
 * @param string $page Slug title of the admin page whose settings fields you want to show.
 * @param string $section Slug title of the settings section whose fields you want to show.
 */
function do_settings_fields( $page, $section ) {
	global $wp_settings_fields;

	if ( ! isset( $wp_settings_fields[ $page ][ $section ] ) ) {
		return;
	}

	foreach ( (array) $wp_settings_fields[ $page ][ $section ] as $field ) {
		$class = '';

		if ( ! empty( $field['args']['class'] ) ) {
			$class = ' class="' . esc_attr( $field['args']['class'] ) . '"';
		}

		echo "<tr{$class}>";

		if ( ! empty( $field['args']['label_for'] ) ) {
			echo '<th scope="row"><label for="' . esc_attr( $field['args']['label_for'] ) . '">' . $field['title'] . '</label></th>';
		} else {
			echo '<th scope="row">' . $field['title'] . '</th>';
		}

		echo '<td>';
		call_user_func( $field['callback'], $field['args'] );
		echo '</td>';
		echo '</tr>';
	}
}

/**
 * Registers a settings error to be displayed to the user.
 *
 * Part of the Settings API. Use this to show messages to users about settings validation
 * problems, missing settings or anything else.
 *
 * Settings errors should be added inside the $sanitize_callback function defined in
 * register_setting() for a given setting to give feedback about the submission.
 *
 * By default messages will show immediately after the submission that generated the error.
 * Additional calls to settings_errors() can be used to show errors even when the settings
 * page is first accessed.
 *
 * @since 3.0.0
 * @since 5.3.0 Added `warning` and `info` as possible values for `$type`.
 *
 * @global array[] $wp_settings_errors Storage array of errors registered during this pageload
 *
 * @param string $setting Slug title of the setting to which this error applies.
 * @param string $code    Slug-name to identify the error. Used as part of 'id' attribute in HTML output.
 * @param string $message The formatted message text to display to the user (will be shown inside styled
 *                        `<div>` and `<p>` tags).
 * @param string $type    Optional. Message type, controls HTML class. Possible values include 'error',
 *                        'success', 'warning', 'info'. Default 'error'.
 */
function add_settings_error( $setting, $code, $message, $type = 'error' ) {
	global $wp_settings_errors;

	$wp_settings_errors[] = array(
		'setting' => $setting,
		'code'    => $code,
		'message' => $message,
		'type'    => $type,
	);
}

/**
 * Fetches settings errors registered by add_settings_error().
 *
 * Checks the $wp_settings_errors array for any errors declared during the current
 * pageload and returns them.
 *
 * If changes were just submitted ($_GET['settings-updated']) and settings errors were saved
 * to the 'settings_errors' transient then those errors will be returned instead. This
 * is used to pass errors back across pageloads.
 *
 * Use the $sanitize argument to manually re-sanitize the option before returning errors.
 * This is useful if you have errors or notices you want to show even when the user
 * hasn't submitted data (i.e. when they first load an options page, or in the {@see 'admin_notices'}
 * action hook).
 *
 * @since 3.0.0
 *
 * @global array[] $wp_settings_errors Storage array of errors registered during this pageload
 *
 * @param string $setting  Optional. Slug title of a specific setting whose errors you want.
 * @param bool   $sanitize Optional. Whether to re-sanitize the setting value before returning errors.
 * @return array[] {
 *     Array of settings error arrays.
 *
 *     @type array ...$0 {
 *         Associative array of setting error data.
 *
 *         @type string $setting Slug title of the setting to which this error applies.
 *         @type string $code    Slug-name to identify the error. Used as part of 'id' attribute in HTML output.
 *         @type string $message The formatted message text to display to the user (will be shown inside styled
 *                               `<div>` and `<p>` tags).
 *         @type string $type    Optional. Message type, controls HTML class. Possible values include 'error',
 *                               'success', 'warning', 'info'. Default 'error'.
 *     }
 * }
 */
function get_settings_errors( $setting = '', $sanitize = false ) {
	global $wp_settings_errors;

	/*
	 * If $sanitize is true, manually re-run the sanitization for this option
	 * This allows the $sanitize_callback from register_setting() to run, adding
	 * any settings errors you want to show by default.
	 */
	if ( $sanitize ) {
		sanitize_option( $setting, get_option( $setting ) );
	}

	// If settings were passed back from options.php then use them.
	if ( isset( $_GET['settings-updated'] ) && $_GET['settings-updated'] && get_transient( 'settings_errors' ) ) {
		$wp_settings_errors = array_merge( (array) $wp_settings_errors, get_transient( 'settings_errors' ) );
		delete_transient( 'settings_errors' );
	}

	// Check global in case errors have been added on this pageload.
	if ( empty( $wp_settings_errors ) ) {
		return array();
	}

	// Filter the results to those of a specific setting if one was set.
	if ( $setting ) {
		$setting_errors = array();

		foreach ( (array) $wp_settings_errors as $key => $details ) {
			if ( $setting === $details['setting'] ) {
				$setting_errors[] = $wp_settings_errors[ $key ];
			}
		}

		return $setting_errors;
	}

	return $wp_settings_errors;
}

/**
 * Displays settings errors registered by add_settings_error().
 *
 * Part of the Settings API. Outputs a div for each error retrieved by
 * get_settings_errors().
 *
 * This is called automatically after a settings page based on the
 * Settings API is submitted. Errors should be added during the validation
 * callback function for a setting defined in register_setting().
 *
 * The $sanitize option is passed into get_settings_errors() and will
 * re-run the setting sanitization
 * on its current value.
 *
 * The $hide_on_update option will cause errors to only show when the settings
 * page is first loaded. if the user has already saved new values it will be
 * hidden to avoid repeating messages already shown in the default error
 * reporting after submission. This is useful to show general errors like
 * missing settings when the user arrives at the settings page.
 *
 * @since 3.0.0
 * @since 5.3.0 Legacy `error` and `updated` CSS classes are mapped to
 *              `notice-error` and `notice-success`.
 *
 * @param string $setting        Optional slug title of a specific setting whose errors you want.
 * @param bool   $sanitize       Whether to re-sanitize the setting value before returning errors.
 * @param bool   $hide_on_update If set to true errors will not be shown if the settings page has
 *                               already been submitted.
 */
function settings_errors( $setting = '', $sanitize = false, $hide_on_update = false ) {

	if ( $hide_on_update && ! empty( $_GET['settings-updated'] ) ) {
		return;
	}

	$settings_errors = get_settings_errors( $setting, $sanitize );

	if ( empty( $settings_errors ) ) {
		return;
	}

	$output = '';

	foreach ( $settings_errors as $key => $details ) {
		if ( 'updated' === $details['type'] ) {
			$details['type'] = 'success';
		}

		if ( in_array( $details['type'], array( 'error', 'success', 'warning', 'info' ), true ) ) {
			$details['type'] = 'notice-' . $details['type'];
		}

		$css_id    = sprintf(
			'setting-error-%s',
			esc_attr( $details['code'] )
		);
		$css_class = sprintf(
			'notice %s settings-error is-dismissible',
			esc_attr( $details['type'] )
		);

		$output .= "<div id='$css_id' class='$css_class'> \n";
		$output .= "<p><strong>{$details['message']}</strong></p>";
		$output .= "</div> \n";
	}

	echo $output;
}

/**
 * Outputs the modal window used for attaching media to posts or pages in the media-listing screen.
 *
 * @since 2.7.0
 *
 * @param string $found_action Optional. The value of the 'found_action' input field. Default empty string.
 */
function find_posts_div( $found_action = '' ) {
	?>
	<div id="find-posts" class="find-box" style="display: none;">
		<div id="find-posts-head" class="find-box-head">
			<?php _e( 'Attach to existing content' ); ?>
			<button type="button" id="find-posts-close"><span class="screen-reader-text">
				<?php
				/* translators: Hidden accessibility text. */
				_e( 'Close media attachment panel' );
				?>
			</span></button>
		</div>
		<div class="find-box-inside">
			<div class="find-box-search">
				<?php if ( $found_action ) { ?>
					<input type="hidden" name="found_action" value="<?php echo esc_attr( $found_action ); ?>" />
				<?php } ?>
				<input type="hidden" name="affected" id="affected" value="" />
				<?php wp_nonce_field( 'find-posts', '_ajax_nonce', false ); ?>
				<label class="screen-reader-text" for="find-posts-input">
					<?php
					/* translators: Hidden accessibility text. */
					_e( 'Search' );
					?>
				</label>
				<input type="text" id="find-posts-input" name="ps" value="" />
				<span class="spinner"></span>
				<input type="button" id="find-posts-search" value="<?php esc_attr_e( 'Search' ); ?>" class="button" />
				<div class="clear"></div>
			</div>
			<div id="find-posts-response"></div>
		</div>
		<div class="find-box-buttons">
			<?php submit_button( __( 'Select' ), 'primary alignright', 'find-posts-submit', false ); ?>
			<div class="clear"></div>
		</div>
	</div>
	<?php
}

/**
 * Displays the post password.
 *
 * The password is passed through esc_attr() to ensure that it is safe for placing in an HTML attribute.
 *
 * @since 2.7.0
 */
function the_post_password() {
	$post = get_post();
	if ( isset( $post->post_password ) ) {
		echo esc_attr( $post->post_password );
	}
}

/**
 * Gets the post title.
 *
 * The post title is fetched and if it is blank then a default string is
 * returned.
 *
 * @since 2.7.0
 *
 * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post.
 * @return string The post title if set.
 */
function _draft_or_post_title( $post = 0 ) {
	$title = get_the_title( $post );
	if ( empty( $title ) ) {
		$title = __( '(no title)' );
	}
	return esc_html( $title );
}

/**
 * Displays the search query.
 *
 * A simple wrapper to display the "s" parameter in a `GET` URI. This function
 * should only be used when the_search_query() cannot.
 *
 * @since 2.7.0
 */
function _admin_search_query() {
	echo isset( $_REQUEST['s'] ) ? esc_attr( wp_unslash( $_REQUEST['s'] ) ) : '';
}

/**
 * Generic Iframe header for use with Thickbox.
 *
 * @since 2.7.0
 *
 * @global string    $hook_suffix
 * @global string    $admin_body_class
 * @global string    $body_id
 * @global WP_Locale $wp_locale        WordPress date and time locale object.
 *
 * @param string $title      Optional. Title of the Iframe page. Default empty.
 * @param bool   $deprecated Not used.
 */
function iframe_header( $title = '', $deprecated = false ) {
	global $hook_suffix, $admin_body_class, $body_id, $wp_locale;

	show_admin_bar( false );

	$admin_body_class = preg_replace( '/[^a-z0-9_-]+/i', '-', $hook_suffix );

	$current_screen = get_current_screen();

	header( 'Content-Type: ' . get_option( 'html_type' ) . '; charset=' . get_option( 'blog_charset' ) );
	_wp_admin_html_begin();
	?>
<title><?php bloginfo( 'name' ); ?> &rsaquo; <?php echo $title; ?> &#8212; <?php _e( 'WordPress' ); ?></title>
	<?php
	wp_enqueue_style( 'colors' );
	?>
<script type="text/javascript">
addLoadEvent = function(func){if(typeof jQuery!=='undefined')jQuery(function(){func();});else if(typeof wpOnload!=='function'){wpOnload=func;}else{var oldonload=wpOnload;wpOnload=function(){oldonload();func();}}};
function tb_close(){var win=window.dialogArguments||opener||parent||top;win.tb_remove();}
var ajaxurl = '<?php echo esc_js( admin_url( 'admin-ajax.php', 'relative' ) ); ?>',
	pagenow = '<?php echo esc_js( $current_screen->id ); ?>',
	typenow = '<?php echo esc_js( $current_screen->post_type ); ?>',
	adminpage = '<?php echo esc_js( $admin_body_class ); ?>',
	thousandsSeparator = '<?php echo esc_js( $wp_locale->number_format['thousands_sep'] ); ?>',
	decimalPoint = '<?php echo esc_js( $wp_locale->number_format['decimal_point'] ); ?>',
	isRtl = <?php echo (int) is_rtl(); ?>;
</script>
	<?php
	/** This action is documented in wp-admin/admin-header.php */
	do_action( 'admin_enqueue_scripts', $hook_suffix );

	/** This action is documented in wp-admin/admin-header.php */
	do_action( "admin_print_styles-{$hook_suffix}" );  // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores

	/** This action is documented in wp-admin/admin-header.php */
	do_action( 'admin_print_styles' );

	/** This action is documented in wp-admin/admin-header.php */
	do_action( "admin_print_scripts-{$hook_suffix}" ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores

	/** This action is documented in wp-admin/admin-header.php */
	do_action( 'admin_print_scripts' );

	/** This action is documented in wp-admin/admin-header.php */
	do_action( "admin_head-{$hook_suffix}" ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores

	/** This action is documented in wp-admin/admin-header.php */
	do_action( 'admin_head' );

	$admin_body_class .= ' locale-' . sanitize_html_class( strtolower( str_replace( '_', '-', get_user_locale() ) ) );

	if ( is_rtl() ) {
		$admin_body_class .= ' rtl';
	}

	?>
</head>
	<?php
	$admin_body_id = isset( $body_id ) ? 'id="' . $body_id . '" ' : '';

	/** This filter is documented in wp-admin/admin-header.php */
	$admin_body_classes = apply_filters( 'admin_body_class', '' );
	$admin_body_classes = ltrim( $admin_body_classes . ' ' . $admin_body_class );
	?>
<body <?php echo $admin_body_id; ?>class="wp-admin wp-core-ui no-js iframe <?php echo esc_attr( $admin_body_classes ); ?>">
<script type="text/javascript">
(function(){
var c = document.body.className;
c = c.replace(/no-js/, 'js');
document.body.className = c;
})();
</script>
	<?php
}

/**
 * Generic Iframe footer for use with Thickbox.
 *
 * @since 2.7.0
 */
function iframe_footer() {
	/*
	 * We're going to hide any footer output on iFrame pages,
	 * but run the hooks anyway since they output JavaScript
	 * or other needed content.
	 */

	/**
	 * @global string $hook_suffix
	 */
	global $hook_suffix;
	?>
	<div class="hidden">
	<?php
	/** This action is documented in wp-admin/admin-footer.php */
	do_action( 'admin_footer', $hook_suffix );

	/** This action is documented in wp-admin/admin-footer.php */
	do_action( "admin_print_footer_scripts-{$hook_suffix}" ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores

	/** This action is documented in wp-admin/admin-footer.php */
	do_action( 'admin_print_footer_scripts' );
	?>
	</div>
<script type="text/javascript">if(typeof wpOnload==='function')wpOnload();</script>
</body>
</html>
	<?php
}

/**
 * Echoes or returns the post states as HTML.
 *
 * @since 2.7.0
 * @since 5.3.0 Added the `$display` parameter and a return value.
 *
 * @see get_post_states()
 *
 * @param WP_Post $post    The post to retrieve states for.
 * @param bool    $display Optional. Whether to display the post states as an HTML string.
 *                         Default true.
 * @return string Post states string.
 */
function _post_states( $post, $display = true ) {
	$post_states        = get_post_states( $post );
	$post_states_string = '';

	if ( ! empty( $post_states ) ) {
		$state_count = count( $post_states );

		$i = 0;

		$post_states_string .= ' &mdash; ';

		foreach ( $post_states as $state ) {
			++$i;

			$separator = ( $i < $state_count ) ? ', ' : '';

			$post_states_string .= "<span class='post-state'>{$state}{$separator}</span>";
		}
	}

	if ( $display ) {
		echo $post_states_string;
	}

	return $post_states_string;
}

/**
 * Retrieves an array of post states from a post.
 *
 * @since 5.3.0
 *
 * @param WP_Post $post The post to retrieve states for.
 * @return string[] Array of post state labels keyed by their state.
 */
function get_post_states( $post ) {
	$post_states = array();

	if ( isset( $_REQUEST['post_status'] ) ) {
		$post_status = $_REQUEST['post_status'];
	} else {
		$post_status = '';
	}

	if ( ! empty( $post->post_password ) ) {
		$post_states['protected'] = _x( 'Password protected', 'post status' );
	}

	if ( 'private' === $post->post_status && 'private' !== $post_status ) {
		$post_states['private'] = _x( 'Private', 'post status' );
	}

	if ( 'draft' === $post->post_status ) {
		if ( get_post_meta( $post->ID, '_customize_changeset_uuid', true ) ) {
			$post_states[] = __( 'Customization Draft' );
		} elseif ( 'draft' !== $post_status ) {
			$post_states['draft'] = _x( 'Draft', 'post status' );
		}
	} elseif ( 'trash' === $post->post_status && get_post_meta( $post->ID, '_customize_changeset_uuid', true ) ) {
		$post_states[] = _x( 'Customization Draft', 'post status' );
	}

	if ( 'pending' === $post->post_status && 'pending' !== $post_status ) {
		$post_states['pending'] = _x( 'Pending', 'post status' );
	}

	if ( is_sticky( $post->ID ) ) {
		$post_states['sticky'] = _x( 'Sticky', 'post status' );
	}

	if ( 'future' === $post->post_status ) {
		$post_states['scheduled'] = _x( 'Scheduled', 'post status' );
	}

	if ( 'page' === get_option( 'show_on_front' ) ) {
		if ( (int) get_option( 'page_on_front' ) === $post->ID ) {
			$post_states['page_on_front'] = _x( 'Front Page', 'page label' );
		}

		if ( (int) get_option( 'page_for_posts' ) === $post->ID ) {
			$post_states['page_for_posts'] = _x( 'Posts Page', 'page label' );
		}
	}

	if ( (int) get_option( 'wp_page_for_privacy_policy' ) === $post->ID ) {
		$post_states['page_for_privacy_policy'] = _x( 'Privacy Policy Page', 'page label' );
	}

	/**
	 * Filters the default post display states used in the posts list table.
	 *
	 * @since 2.8.0
	 * @since 3.6.0 Added the `$post` parameter.
	 * @since 5.5.0 Also applied in the Customizer context. If any admin functions
	 *              are used within the filter, their existence should be checked
	 *              with `function_exists()` before being used.
	 *
	 * @param string[] $post_states An array of post display states.
	 * @param WP_Post  $post        The current post object.
	 */
	return apply_filters( 'display_post_states', $post_states, $post );
}

/**
 * Outputs the attachment media states as HTML.
 *
 * @since 3.2.0
 * @since 5.6.0 Added the `$display` parameter and a return value.
 *
 * @param WP_Post $post    The attachment post to retrieve states for.
 * @param bool    $display Optional. Whether to display the post states as an HTML string.
 *                         Default true.
 * @return string Media states string.
 */
function _media_states( $post, $display = true ) {
	$media_states        = get_media_states( $post );
	$media_states_string = '';

	if ( ! empty( $media_states ) ) {
		$state_count = count( $media_states );

		$i = 0;

		$media_states_string .= ' &mdash; ';

		foreach ( $media_states as $state ) {
			++$i;

			$separator = ( $i < $state_count ) ? ', ' : '';

			$media_states_string .= "<span class='post-state'>{$state}{$separator}</span>";
		}
	}

	if ( $display ) {
		echo $media_states_string;
	}

	return $media_states_string;
}

/**
 * Retrieves an array of media states from an attachment.
 *
 * @since 5.6.0
 *
 * @param WP_Post $post The attachment to retrieve states for.
 * @return string[] Array of media state labels keyed by their state.
 */
function get_media_states( $post ) {
	static $header_images;

	$media_states = array();
	$stylesheet   = get_option( 'stylesheet' );

	if ( current_theme_supports( 'custom-header' ) ) {
		$meta_header = get_post_meta( $post->ID, '_wp_attachment_is_custom_header', true );

		if ( is_random_header_image() ) {
			if ( ! isset( $header_images ) ) {
				$header_images = wp_list_pluck( get_uploaded_header_images(), 'attachment_id' );
			}

			if ( $meta_header === $stylesheet && in_array( $post->ID, $header_images, true ) ) {
				$media_states[] = __( 'Header Image' );
			}
		} else {
			$header_image = get_header_image();

			// Display "Header Image" if the image was ever used as a header image.
			if ( ! empty( $meta_header ) && $meta_header === $stylesheet && wp_get_attachment_url( $post->ID ) !== $header_image ) {
				$media_states[] = __( 'Header Image' );
			}

			// Display "Current Header Image" if the image is currently the header image.
			if ( $header_image && wp_get_attachment_url( $post->ID ) === $header_image ) {
				$media_states[] = __( 'Current Header Image' );
			}
		}

		if ( get_theme_support( 'custom-header', 'video' ) && has_header_video() ) {
			$mods = get_theme_mods();
			if ( isset( $mods['header_video'] ) && $post->ID === $mods['header_video'] ) {
				$media_states[] = __( 'Current Header Video' );
			}
		}
	}

	if ( current_theme_supports( 'custom-background' ) ) {
		$meta_background = get_post_meta( $post->ID, '_wp_attachment_is_custom_background', true );

		if ( ! empty( $meta_background ) && $meta_background === $stylesheet ) {
			$media_states[] = __( 'Background Image' );

			$background_image = get_background_image();
			if ( $background_image && wp_get_attachment_url( $post->ID ) === $background_image ) {
				$media_states[] = __( 'Current Background Image' );
			}
		}
	}

	if ( (int) get_option( 'site_icon' ) === $post->ID ) {
		$media_states[] = __( 'Site Icon' );
	}

	if ( (int) get_theme_mod( 'custom_logo' ) === $post->ID ) {
		$media_states[] = __( 'Logo' );
	}

	/**
	 * Filters the default media display states for items in the Media list table.
	 *
	 * @since 3.2.0
	 * @since 4.8.0 Added the `$post` parameter.
	 *
	 * @param string[] $media_states An array of media states. Default 'Header Image',
	 *                               'Background Image', 'Site Icon', 'Logo'.
	 * @param WP_Post  $post         The current attachment object.
	 */
	return apply_filters( 'display_media_states', $media_states, $post );
}

/**
 * Tests support for compressing JavaScript from PHP.
 *
 * Outputs JavaScript that tests if compression from PHP works as expected
 * and sets an option with the result. Has no effect when the current user
 * is not an administrator. To run the test again the option 'can_compress_scripts'
 * has to be deleted.
 *
 * @since 2.8.0
 */
function compression_test() {
	?>
	<script type="text/javascript">
	var compressionNonce = <?php echo wp_json_encode( wp_create_nonce( 'update_can_compress_scripts' ) ); ?>;
	var testCompression = {
		get : function(test) {
			var x;
			if ( window.XMLHttpRequest ) {
				x = new XMLHttpRequest();
			} else {
				try{x=new ActiveXObject('Msxml2.XMLHTTP');}catch(e){try{x=new ActiveXObject('Microsoft.XMLHTTP');}catch(e){};}
			}

			if (x) {
				x.onreadystatechange = function() {
					var r, h;
					if ( x.readyState == 4 ) {
						r = x.responseText.substr(0, 18);
						h = x.getResponseHeader('Content-Encoding');
						testCompression.check(r, h, test);
					}
				};

				x.open('GET', ajaxurl + '?action=wp-compression-test&test='+test+'&_ajax_nonce='+compressionNonce+'&'+(new Date()).getTime(), true);
				x.send('');
			}
		},

		check : function(r, h, test) {
			if ( ! r && ! test )
				this.get(1);

			if ( 1 == test ) {
				if ( h && ( h.match(/deflate/i) || h.match(/gzip/i) ) )
					this.get('no');
				else
					this.get(2);

				return;
			}

			if ( 2 == test ) {
				if ( '"wpCompressionTest' === r )
					this.get('yes');
				else
					this.get('no');
			}
		}
	};
	testCompression.check();
	</script>
	<?php
}

/**
 * Echoes a submit button, with provided text and appropriate class(es).
 *
 * @since 3.1.0
 *
 * @see get_submit_button()
 *
 * @param string       $text             Optional. The text of the button. Defaults to 'Save Changes'.
 * @param string       $type             Optional. The type and CSS class(es) of the button. Core values
 *                                       include 'primary', 'small', and 'large'. Default 'primary'.
 * @param string       $name             Optional. The HTML name of the submit button. If no `id` attribute
 *                                       is given in the `$other_attributes` parameter, `$name` will be used
 *                                       as the button's `id`. Default 'submit'.
 * @param bool         $wrap             Optional. True if the output button should be wrapped in a paragraph tag,
 *                                       false otherwise. Default true.
 * @param array|string $other_attributes Optional. Other attributes that should be output with the button,
 *                                       mapping attributes to their values, e.g. `array( 'id' => 'search-submit' )`.
 *                                       These key/value attribute pairs will be output as `attribute="value"`,
 *                                       where attribute is the key. Attributes can also be provided as a string,
 *                                       e.g. `id="search-submit"`, though the array format is generally preferred.
 *                                       Default empty string.
 */
function submit_button( $text = '', $type = 'primary', $name = 'submit', $wrap = true, $other_attributes = '' ) {
	echo get_submit_button( $text, $type, $name, $wrap, $other_attributes );
}

/**
 * Returns a submit button, with provided text and appropriate class.
 *
 * @since 3.1.0
 *
 * @param string       $text             Optional. The text of the button. Defaults to 'Save Changes'.
 * @param string       $type             Optional. The type and CSS class(es) of the button. Core values
 *                                       include 'primary', 'small', and 'large'. Default 'primary large'.
 * @param string       $name             Optional. The HTML name of the submit button. If no `id` attribute
 *                                       is given in the `$other_attributes` parameter, `$name` will be used
 *                                       as the button's `id`. Default 'submit'.
 * @param bool         $wrap             Optional. True if the output button should be wrapped in a paragraph tag,
 *                                       false otherwise. Default true.
 * @param array|string $other_attributes Optional. Other attributes that should be output with the button,
 *                                       mapping attributes to their values, e.g. `array( 'id' => 'search-submit' )`.
 *                                       These key/value attribute pairs will be output as `attribute="value"`,
 *                                       where attribute is the key. Attributes can also be provided as a string,
 *                                       e.g. `id="search-submit"`, though the array format is generally preferred.
 *                                       Default empty string.
 * @return string Submit button HTML.
 */
function get_submit_button( $text = '', $type = 'primary large', $name = 'submit', $wrap = true, $other_attributes = '' ) {
	if ( ! is_array( $type ) ) {
		$type = explode( ' ', $type );
	}

	$button_shorthand = array( 'primary', 'small', 'large' );
	$classes          = array( 'button' );

	foreach ( $type as $t ) {
		if ( 'secondary' === $t || 'button-secondary' === $t ) {
			continue;
		}

		$classes[] = in_array( $t, $button_shorthand, true ) ? 'button-' . $t : $t;
	}

	// Remove empty items, remove duplicate items, and finally build a string.
	$class = implode( ' ', array_unique( array_filter( $classes ) ) );

	$text = $text ? $text : __( 'Save Changes' );

	// Default the id attribute to $name unless an id was specifically provided in $other_attributes.
	$id = $name;
	if ( is_array( $other_attributes ) && isset( $other_attributes['id'] ) ) {
		$id = $other_attributes['id'];
		unset( $other_attributes['id'] );
	}

	$attributes = '';
	if ( is_array( $other_attributes ) ) {
		foreach ( $other_attributes as $attribute => $value ) {
			$attributes .= $attribute . '="' . esc_attr( $value ) . '" '; // Trailing space is important.
		}
	} elseif ( ! empty( $other_attributes ) ) { // Attributes provided as a string.
		$attributes = $other_attributes;
	}

	// Don't output empty name and id attributes.
	$name_attr = $name ? ' name="' . esc_attr( $name ) . '"' : '';
	$id_attr   = $id ? ' id="' . esc_attr( $id ) . '"' : '';

	$button  = '<input type="submit"' . $name_attr . $id_attr . ' class="' . esc_attr( $class );
	$button .= '" value="' . esc_attr( $text ) . '" ' . $attributes . ' />';

	if ( $wrap ) {
		$button = '<p class="submit">' . $button . '</p>';
	}

	return $button;
}

/**
 * Prints out the beginning of the admin HTML header.
 *
 * @global bool $is_IE
 */
function _wp_admin_html_begin() {
	global $is_IE;

	$admin_html_class = ( is_admin_bar_showing() ) ? 'wp-toolbar' : '';

	if ( $is_IE ) {
		header( 'X-UA-Compatible: IE=edge' );
	}

	?>
<!DOCTYPE html>
<html class="<?php echo $admin_html_class; ?>"
	<?php
	/**
	 * Fires inside the HTML tag in the admin header.
	 *
	 * @since 2.2.0
	 */
	do_action( 'admin_xml_ns' );

	language_attributes();
	?>
>
<head>
<meta http-equiv="Content-Type" content="<?php bloginfo( 'html_type' ); ?>; charset=<?php echo get_option( 'blog_charset' ); ?>" />
	<?php
}

/**
 * Converts a screen string to a screen object.
 *
 * @since 3.0.0
 *
 * @param string $hook_name The hook name (also known as the hook suffix) used to determine the screen.
 * @return WP_Screen Screen object.
 */
function convert_to_screen( $hook_name ) {
	if ( ! class_exists( 'WP_Screen' ) ) {
		_doing_it_wrong(
			'convert_to_screen(), add_meta_box()',
			sprintf(
				/* translators: 1: wp-admin/includes/template.php, 2: add_meta_box(), 3: add_meta_boxes */
				__( 'Likely direct inclusion of %1$s in order to use %2$s. This is very wrong. Hook the %2$s call into the %3$s action instead.' ),
				'<code>wp-admin/includes/template.php</code>',
				'<code>add_meta_box()</code>',
				'<code>add_meta_boxes</code>'
			),
			'3.3.0'
		);
		return (object) array(
			'id'   => '_invalid',
			'base' => '_are_belong_to_us',
		);
	}

	return WP_Screen::get( $hook_name );
}

/**
 * Outputs the HTML for restoring the post data from DOM storage
 *
 * @since 3.6.0
 * @access private
 */
function _local_storage_notice() {
	$local_storage_message  = '<p class="local-restore">';
	$local_storage_message .= __( 'The backup of this post in your browser is different from the version below.' );
	$local_storage_message .= '<button type="button" class="button restore-backup">' . __( 'Restore the backup' ) . '</button></p>';
	$local_storage_message .= '<p class="help">';
	$local_storage_message .= __( 'This will replace the current editor content with the last backup version. You can use undo and redo in the editor to get the old content back or to return to the restored version.' );
	$local_storage_message .= '</p>';

	wp_admin_notice(
		$local_storage_message,
		array(
			'id'                 => 'local-storage-notice',
			'additional_classes' => array( 'hidden' ),
			'dismissible'        => true,
			'paragraph_wrap'     => false,
		)
	);
}

/**
 * Outputs a HTML element with a star rating for a given rating.
 *
 * Outputs a HTML element with the star rating exposed on a 0..5 scale in
 * half star increments (ie. 1, 1.5, 2 stars). Optionally, if specified, the
 * number of ratings may also be displayed by passing the $number parameter.
 *
 * @since 3.8.0
 * @since 4.4.0 Introduced the `echo` parameter.
 *
 * @param array $args {
 *     Optional. Array of star ratings arguments.
 *
 *     @type int|float $rating The rating to display, expressed in either a 0.5 rating increment,
 *                             or percentage. Default 0.
 *     @type string    $type   Format that the $rating is in. Valid values are 'rating' (default),
 *                             or, 'percent'. Default 'rating'.
 *     @type int       $number The number of ratings that makes up this rating. Default 0.
 *     @type bool      $echo   Whether to echo the generated markup. False to return the markup instead
 *                             of echoing it. Default true.
 * }
 * @return string Star rating HTML.
 */
function wp_star_rating( $args = array() ) {
	$defaults    = array(
		'rating' => 0,
		'type'   => 'rating',
		'number' => 0,
		'echo'   => true,
	);
	$parsed_args = wp_parse_args( $args, $defaults );

	// Non-English decimal places when the $rating is coming from a string.
	$rating = (float) str_replace( ',', '.', $parsed_args['rating'] );

	// Convert percentage to star rating, 0..5 in .5 increments.
	if ( 'percent' === $parsed_args['type'] ) {
		$rating = round( $rating / 10, 0 ) / 2;
	}

	// Calculate the number of each type of star needed.
	$full_stars  = floor( $rating );
	$half_stars  = ceil( $rating - $full_stars );
	$empty_stars = 5 - $full_stars - $half_stars;

	if ( $parsed_args['number'] ) {
		/* translators: Hidden accessibility text. 1: The rating, 2: The number of ratings. */
		$format = _n( '%1$s rating based on %2$s rating', '%1$s rating based on %2$s ratings', $parsed_args['number'] );
		$title  = sprintf( $format, number_format_i18n( $rating, 1 ), number_format_i18n( $parsed_args['number'] ) );
	} else {
		/* translators: Hidden accessibility text. %s: The rating. */
		$title = sprintf( __( '%s rating' ), number_format_i18n( $rating, 1 ) );
	}

	$output  = '<div class="star-rating">';
	$output .= '<span class="screen-reader-text">' . $title . '</span>';
	$output .= str_repeat( '<div class="star star-full" aria-hidden="true"></div>', $full_stars );
	$output .= str_repeat( '<div class="star star-half" aria-hidden="true"></div>', $half_stars );
	$output .= str_repeat( '<div class="star star-empty" aria-hidden="true"></div>', $empty_stars );
	$output .= '</div>';

	if ( $parsed_args['echo'] ) {
		echo $output;
	}

	return $output;
}

/**
 * Outputs a notice when editing the page for posts (internal use only).
 *
 * @ignore
 * @since 4.2.0
 */
function _wp_posts_page_notice() {
	wp_admin_notice(
		__( 'You are currently editing the page that shows your latest posts.' ),
		array(
			'type'               => 'warning',
			'additional_classes' => array( 'inline' ),
		)
	);
}

/**
 * Outputs a notice when editing the page for posts in the block editor (internal use only).
 *
 * @ignore
 * @since 5.8.0
 */
function _wp_block_editor_posts_page_notice() {
	wp_add_inline_script(
		'wp-notices',
		sprintf(
			'wp.data.dispatch( "core/notices" ).createWarningNotice( "%s", { isDismissible: false } )',
			__( 'You are currently editing the page that shows your latest posts.' )
		),
		'after'
	);
}
class-wp-terms-list-table.php000060400000051276151327343360012212 0ustar00<?php
/**
 * List Table API: WP_Terms_List_Table class
 *
 * @package WordPress
 * @subpackage Administration
 * @since 3.1.0
 */

/**
 * Core class used to implement displaying terms in a list table.
 *
 * @since 3.1.0
 *
 * @see WP_List_Table
 */
class WP_Terms_List_Table extends WP_List_Table {

	public $callback_args;

	private $level;

	/**
	 * Constructor.
	 *
	 * @since 3.1.0
	 *
	 * @see WP_List_Table::__construct() for more information on default arguments.
	 *
	 * @global string $post_type Global post type.
	 * @global string $taxonomy  Global taxonomy.
	 * @global string $action
	 * @global object $tax
	 *
	 * @param array $args An associative array of arguments.
	 */
	public function __construct( $args = array() ) {
		global $post_type, $taxonomy, $action, $tax;

		parent::__construct(
			array(
				'plural'   => 'tags',
				'singular' => 'tag',
				'screen'   => isset( $args['screen'] ) ? $args['screen'] : null,
			)
		);

		$action    = $this->screen->action;
		$post_type = $this->screen->post_type;
		$taxonomy  = $this->screen->taxonomy;

		if ( empty( $taxonomy ) ) {
			$taxonomy = 'post_tag';
		}

		if ( ! taxonomy_exists( $taxonomy ) ) {
			wp_die( __( 'Invalid taxonomy.' ) );
		}

		$tax = get_taxonomy( $taxonomy );

		// @todo Still needed? Maybe just the show_ui part.
		if ( empty( $post_type ) || ! in_array( $post_type, get_post_types( array( 'show_ui' => true ) ), true ) ) {
			$post_type = 'post';
		}
	}

	/**
	 * @return bool
	 */
	public function ajax_user_can() {
		return current_user_can( get_taxonomy( $this->screen->taxonomy )->cap->manage_terms );
	}

	/**
	 */
	public function prepare_items() {
		$taxonomy = $this->screen->taxonomy;

		$tags_per_page = $this->get_items_per_page( "edit_{$taxonomy}_per_page" );

		if ( 'post_tag' === $taxonomy ) {
			/**
			 * Filters the number of terms displayed per page for the Tags list table.
			 *
			 * @since 2.8.0
			 *
			 * @param int $tags_per_page Number of tags to be displayed. Default 20.
			 */
			$tags_per_page = apply_filters( 'edit_tags_per_page', $tags_per_page );

			/**
			 * Filters the number of terms displayed per page for the Tags list table.
			 *
			 * @since 2.7.0
			 * @deprecated 2.8.0 Use {@see 'edit_tags_per_page'} instead.
			 *
			 * @param int $tags_per_page Number of tags to be displayed. Default 20.
			 */
			$tags_per_page = apply_filters_deprecated( 'tagsperpage', array( $tags_per_page ), '2.8.0', 'edit_tags_per_page' );
		} elseif ( 'category' === $taxonomy ) {
			/**
			 * Filters the number of terms displayed per page for the Categories list table.
			 *
			 * @since 2.8.0
			 *
			 * @param int $tags_per_page Number of categories to be displayed. Default 20.
			 */
			$tags_per_page = apply_filters( 'edit_categories_per_page', $tags_per_page );
		}

		$search = ! empty( $_REQUEST['s'] ) ? trim( wp_unslash( $_REQUEST['s'] ) ) : '';

		$args = array(
			'taxonomy'   => $taxonomy,
			'search'     => $search,
			'page'       => $this->get_pagenum(),
			'number'     => $tags_per_page,
			'hide_empty' => 0,
		);

		if ( ! empty( $_REQUEST['orderby'] ) ) {
			$args['orderby'] = trim( wp_unslash( $_REQUEST['orderby'] ) );
		}

		if ( ! empty( $_REQUEST['order'] ) ) {
			$args['order'] = trim( wp_unslash( $_REQUEST['order'] ) );
		}

		$args['offset'] = ( $args['page'] - 1 ) * $args['number'];

		// Save the values because 'number' and 'offset' can be subsequently overridden.
		$this->callback_args = $args;

		if ( is_taxonomy_hierarchical( $taxonomy ) && ! isset( $args['orderby'] ) ) {
			// We'll need the full set of terms then.
			$args['number'] = 0;
			$args['offset'] = $args['number'];
		}

		$this->items = get_terms( $args );

		$this->set_pagination_args(
			array(
				'total_items' => wp_count_terms(
					array(
						'taxonomy' => $taxonomy,
						'search'   => $search,
					)
				),
				'per_page'    => $tags_per_page,
			)
		);
	}

	/**
	 */
	public function no_items() {
		echo get_taxonomy( $this->screen->taxonomy )->labels->not_found;
	}

	/**
	 * @return array
	 */
	protected function get_bulk_actions() {
		$actions = array();

		if ( current_user_can( get_taxonomy( $this->screen->taxonomy )->cap->delete_terms ) ) {
			$actions['delete'] = __( 'Delete' );
		}

		return $actions;
	}

	/**
	 * @return string
	 */
	public function current_action() {
		if ( isset( $_REQUEST['action'] ) && isset( $_REQUEST['delete_tags'] ) && 'delete' === $_REQUEST['action'] ) {
			return 'bulk-delete';
		}

		return parent::current_action();
	}

	/**
	 * @return string[] Array of column titles keyed by their column name.
	 */
	public function get_columns() {
		$columns = array(
			'cb'          => '<input type="checkbox" />',
			'name'        => _x( 'Name', 'term name' ),
			'description' => __( 'Description' ),
			'slug'        => __( 'Slug' ),
		);

		if ( 'link_category' === $this->screen->taxonomy ) {
			$columns['links'] = __( 'Links' );
		} else {
			$columns['posts'] = _x( 'Count', 'Number/count of items' );
		}

		return $columns;
	}

	/**
	 * @return array
	 */
	protected function get_sortable_columns() {
		$taxonomy = $this->screen->taxonomy;

		if ( ! isset( $_GET['orderby'] ) && is_taxonomy_hierarchical( $taxonomy ) ) {
			$name_orderby_text = __( 'Table ordered hierarchically.' );
		} else {
			$name_orderby_text = __( 'Table ordered by Name.' );
		}

		return array(
			'name'        => array( 'name', false, _x( 'Name', 'term name' ), $name_orderby_text, 'asc' ),
			'description' => array( 'description', false, __( 'Description' ), __( 'Table ordered by Description.' ) ),
			'slug'        => array( 'slug', false, __( 'Slug' ), __( 'Table ordered by Slug.' ) ),
			'posts'       => array( 'count', false, _x( 'Count', 'Number/count of items' ), __( 'Table ordered by Posts Count.' ) ),
			'links'       => array( 'count', false, __( 'Links' ), __( 'Table ordered by Links.' ) ),
		);
	}

	/**
	 */
	public function display_rows_or_placeholder() {
		$taxonomy = $this->screen->taxonomy;

		$number = $this->callback_args['number'];
		$offset = $this->callback_args['offset'];

		// Convert it to table rows.
		$count = 0;

		if ( empty( $this->items ) || ! is_array( $this->items ) ) {
			echo '<tr class="no-items"><td class="colspanchange" colspan="' . $this->get_column_count() . '">';
			$this->no_items();
			echo '</td></tr>';
			return;
		}

		if ( is_taxonomy_hierarchical( $taxonomy ) && ! isset( $this->callback_args['orderby'] ) ) {
			if ( ! empty( $this->callback_args['search'] ) ) {// Ignore children on searches.
				$children = array();
			} else {
				$children = _get_term_hierarchy( $taxonomy );
			}

			/*
			 * Some funky recursion to get the job done (paging & parents mainly) is contained within.
			 * Skip it for non-hierarchical taxonomies for performance sake.
			 */
			$this->_rows( $taxonomy, $this->items, $children, $offset, $number, $count );
		} else {
			foreach ( $this->items as $term ) {
				$this->single_row( $term );
			}
		}
	}

	/**
	 * @param string $taxonomy
	 * @param array  $terms
	 * @param array  $children
	 * @param int    $start
	 * @param int    $per_page
	 * @param int    $count
	 * @param int    $parent_term
	 * @param int    $level
	 */
	private function _rows( $taxonomy, $terms, &$children, $start, $per_page, &$count, $parent_term = 0, $level = 0 ) {

		$end = $start + $per_page;

		foreach ( $terms as $key => $term ) {

			if ( $count >= $end ) {
				break;
			}

			if ( $term->parent !== $parent_term && empty( $_REQUEST['s'] ) ) {
				continue;
			}

			// If the page starts in a subtree, print the parents.
			if ( $count === $start && $term->parent > 0 && empty( $_REQUEST['s'] ) ) {
				$my_parents = array();
				$parent_ids = array();
				$p          = $term->parent;

				while ( $p ) {
					$my_parent    = get_term( $p, $taxonomy );
					$my_parents[] = $my_parent;
					$p            = $my_parent->parent;

					if ( in_array( $p, $parent_ids, true ) ) { // Prevent parent loops.
						break;
					}

					$parent_ids[] = $p;
				}

				unset( $parent_ids );

				$num_parents = count( $my_parents );

				while ( $my_parent = array_pop( $my_parents ) ) {
					echo "\t";
					$this->single_row( $my_parent, $level - $num_parents );
					--$num_parents;
				}
			}

			if ( $count >= $start ) {
				echo "\t";
				$this->single_row( $term, $level );
			}

			++$count;

			unset( $terms[ $key ] );

			if ( isset( $children[ $term->term_id ] ) && empty( $_REQUEST['s'] ) ) {
				$this->_rows( $taxonomy, $terms, $children, $start, $per_page, $count, $term->term_id, $level + 1 );
			}
		}
	}

	/**
	 * @global string $taxonomy Global taxonomy.
	 *
	 * @param WP_Term $tag   Term object.
	 * @param int     $level
	 */
	public function single_row( $tag, $level = 0 ) {
		global $taxonomy;
		$tag = sanitize_term( $tag, $taxonomy );

		$this->level = $level;

		if ( $tag->parent ) {
			$count = count( get_ancestors( $tag->term_id, $taxonomy, 'taxonomy' ) );
			$level = 'level-' . $count;
		} else {
			$level = 'level-0';
		}

		echo '<tr id="tag-' . $tag->term_id . '" class="' . $level . '">';
		$this->single_row_columns( $tag );
		echo '</tr>';
	}

	/**
	 * @since 5.9.0 Renamed `$tag` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param WP_Term $item Term object.
	 * @return string
	 */
	public function column_cb( $item ) {
		// Restores the more descriptive, specific name for use within this method.
		$tag = $item;

		if ( current_user_can( 'delete_term', $tag->term_id ) ) {
			return sprintf(
				'<input type="checkbox" name="delete_tags[]" value="%1$s" id="cb-select-%1$s" />' .
				'<label for="cb-select-%1$s"><span class="screen-reader-text">%2$s</span></label>',
				$tag->term_id,
				/* translators: Hidden accessibility text. %s: Taxonomy term name. */
				sprintf( __( 'Select %s' ), $tag->name )
			);
		}

		return '&nbsp;';
	}

	/**
	 * @param WP_Term $tag Term object.
	 * @return string
	 */
	public function column_name( $tag ) {
		$taxonomy = $this->screen->taxonomy;

		$pad = str_repeat( '&#8212; ', max( 0, $this->level ) );

		/**
		 * Filters display of the term name in the terms list table.
		 *
		 * The default output may include padding due to the term's
		 * current level in the term hierarchy.
		 *
		 * @since 2.5.0
		 *
		 * @see WP_Terms_List_Table::column_name()
		 *
		 * @param string $pad_tag_name The term name, padded if not top-level.
		 * @param WP_Term $tag         Term object.
		 */
		$name = apply_filters( 'term_name', $pad . ' ' . $tag->name, $tag );

		$qe_data = get_term( $tag->term_id, $taxonomy, OBJECT, 'edit' );

		$uri = wp_doing_ajax() ? wp_get_referer() : $_SERVER['REQUEST_URI'];

		$edit_link = get_edit_term_link( $tag, $taxonomy, $this->screen->post_type );

		if ( $edit_link ) {
			$edit_link = add_query_arg(
				'wp_http_referer',
				urlencode( wp_unslash( $uri ) ),
				$edit_link
			);
			$name      = sprintf(
				'<a class="row-title" href="%s" aria-label="%s">%s</a>',
				esc_url( $edit_link ),
				/* translators: %s: Taxonomy term name. */
				esc_attr( sprintf( __( '&#8220;%s&#8221; (Edit)' ), $tag->name ) ),
				$name
			);
		}

		$output = sprintf(
			'<strong>%s</strong><br />',
			$name
		);

		/** This filter is documented in wp-admin/includes/class-wp-terms-list-table.php */
		$quick_edit_enabled = apply_filters( 'quick_edit_enabled_for_taxonomy', true, $taxonomy );

		if ( $quick_edit_enabled ) {
			$output .= '<div class="hidden" id="inline_' . $qe_data->term_id . '">';
			$output .= '<div class="name">' . $qe_data->name . '</div>';

			/** This filter is documented in wp-admin/edit-tag-form.php */
			$output .= '<div class="slug">' . apply_filters( 'editable_slug', $qe_data->slug, $qe_data ) . '</div>';
			$output .= '<div class="parent">' . $qe_data->parent . '</div></div>';
		}

		return $output;
	}

	/**
	 * Gets the name of the default primary column.
	 *
	 * @since 4.3.0
	 *
	 * @return string Name of the default primary column, in this case, 'name'.
	 */
	protected function get_default_primary_column_name() {
		return 'name';
	}

	/**
	 * Generates and displays row action links.
	 *
	 * @since 4.3.0
	 * @since 5.9.0 Renamed `$tag` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param WP_Term $item        Tag being acted upon.
	 * @param string  $column_name Current column name.
	 * @param string  $primary     Primary column name.
	 * @return string Row actions output for terms, or an empty string
	 *                if the current column is not the primary column.
	 */
	protected function handle_row_actions( $item, $column_name, $primary ) {
		if ( $primary !== $column_name ) {
			return '';
		}

		// Restores the more descriptive, specific name for use within this method.
		$tag = $item;

		$taxonomy = $this->screen->taxonomy;
		$uri      = wp_doing_ajax() ? wp_get_referer() : $_SERVER['REQUEST_URI'];

		$actions = array();

		if ( current_user_can( 'edit_term', $tag->term_id ) ) {
			$actions['edit'] = sprintf(
				'<a href="%s" aria-label="%s">%s</a>',
				esc_url(
					add_query_arg(
						'wp_http_referer',
						urlencode( wp_unslash( $uri ) ),
						get_edit_term_link( $tag, $taxonomy, $this->screen->post_type )
					)
				),
				/* translators: %s: Taxonomy term name. */
				esc_attr( sprintf( __( 'Edit &#8220;%s&#8221;' ), $tag->name ) ),
				__( 'Edit' )
			);

			/**
			 * Filters whether Quick Edit should be enabled for the given taxonomy.
			 *
			 * @since 6.4.0
			 *
			 * @param bool   $enable   Whether to enable the Quick Edit functionality. Default true.
			 * @param string $taxonomy Taxonomy name.
			 */
			$quick_edit_enabled = apply_filters( 'quick_edit_enabled_for_taxonomy', true, $taxonomy );

			if ( $quick_edit_enabled ) {
				$actions['inline hide-if-no-js'] = sprintf(
					'<button type="button" class="button-link editinline" aria-label="%s" aria-expanded="false">%s</button>',
					/* translators: %s: Taxonomy term name. */
					esc_attr( sprintf( __( 'Quick edit &#8220;%s&#8221; inline' ), $tag->name ) ),
					__( 'Quick&nbsp;Edit' )
				);
			}
		}

		if ( current_user_can( 'delete_term', $tag->term_id ) ) {
			$actions['delete'] = sprintf(
				'<a href="%s" class="delete-tag aria-button-if-js" aria-label="%s">%s</a>',
				wp_nonce_url( "edit-tags.php?action=delete&amp;taxonomy=$taxonomy&amp;tag_ID=$tag->term_id", 'delete-tag_' . $tag->term_id ),
				/* translators: %s: Taxonomy term name. */
				esc_attr( sprintf( __( 'Delete &#8220;%s&#8221;' ), $tag->name ) ),
				__( 'Delete' )
			);
		}

		if ( is_term_publicly_viewable( $tag ) ) {
			$actions['view'] = sprintf(
				'<a href="%s" aria-label="%s">%s</a>',
				get_term_link( $tag ),
				/* translators: %s: Taxonomy term name. */
				esc_attr( sprintf( __( 'View &#8220;%s&#8221; archive' ), $tag->name ) ),
				__( 'View' )
			);
		}

		/**
		 * Filters the action links displayed for each term in the Tags list table.
		 *
		 * @since 2.8.0
		 * @since 3.0.0 Deprecated in favor of {@see '{$taxonomy}_row_actions'} filter.
		 * @since 5.4.2 Restored (un-deprecated).
		 *
		 * @param string[] $actions An array of action links to be displayed. Default
		 *                          'Edit', 'Quick Edit', 'Delete', and 'View'.
		 * @param WP_Term  $tag     Term object.
		 */
		$actions = apply_filters( 'tag_row_actions', $actions, $tag );

		/**
		 * Filters the action links displayed for each term in the terms list table.
		 *
		 * The dynamic portion of the hook name, `$taxonomy`, refers to the taxonomy slug.
		 *
		 * Possible hook names include:
		 *
		 *  - `category_row_actions`
		 *  - `post_tag_row_actions`
		 *
		 * @since 3.0.0
		 *
		 * @param string[] $actions An array of action links to be displayed. Default
		 *                          'Edit', 'Quick Edit', 'Delete', and 'View'.
		 * @param WP_Term  $tag     Term object.
		 */
		$actions = apply_filters( "{$taxonomy}_row_actions", $actions, $tag );

		return $this->row_actions( $actions );
	}

	/**
	 * @param WP_Term $tag Term object.
	 * @return string
	 */
	public function column_description( $tag ) {
		if ( $tag->description ) {
			return $tag->description;
		} else {
			return '<span aria-hidden="true">&#8212;</span><span class="screen-reader-text">' .
				/* translators: Hidden accessibility text. */
				__( 'No description' ) .
			'</span>';
		}
	}

	/**
	 * @param WP_Term $tag Term object.
	 * @return string
	 */
	public function column_slug( $tag ) {
		/** This filter is documented in wp-admin/edit-tag-form.php */
		return apply_filters( 'editable_slug', $tag->slug, $tag );
	}

	/**
	 * @param WP_Term $tag Term object.
	 * @return string
	 */
	public function column_posts( $tag ) {
		$count = number_format_i18n( $tag->count );

		$tax = get_taxonomy( $this->screen->taxonomy );

		$ptype_object = get_post_type_object( $this->screen->post_type );
		if ( ! $ptype_object->show_ui ) {
			return $count;
		}

		if ( $tax->query_var ) {
			$args = array( $tax->query_var => $tag->slug );
		} else {
			$args = array(
				'taxonomy' => $tax->name,
				'term'     => $tag->slug,
			);
		}

		if ( 'post' !== $this->screen->post_type ) {
			$args['post_type'] = $this->screen->post_type;
		}

		if ( 'attachment' === $this->screen->post_type ) {
			return "<a href='" . esc_url( add_query_arg( $args, 'upload.php' ) ) . "'>$count</a>";
		}

		return "<a href='" . esc_url( add_query_arg( $args, 'edit.php' ) ) . "'>$count</a>";
	}

	/**
	 * @param WP_Term $tag Term object.
	 * @return string
	 */
	public function column_links( $tag ) {
		$count = number_format_i18n( $tag->count );

		if ( $count ) {
			$count = "<a href='link-manager.php?cat_id=$tag->term_id'>$count</a>";
		}

		return $count;
	}

	/**
	 * @since 5.9.0 Renamed `$tag` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param WP_Term $item        Term object.
	 * @param string  $column_name Name of the column.
	 * @return string
	 */
	public function column_default( $item, $column_name ) {
		// Restores the more descriptive, specific name for use within this method.
		$tag = $item;

		/**
		 * Filters the displayed columns in the terms list table.
		 *
		 * The dynamic portion of the hook name, `$this->screen->taxonomy`,
		 * refers to the slug of the current taxonomy.
		 *
		 * Possible hook names include:
		 *
		 *  - `manage_category_custom_column`
		 *  - `manage_post_tag_custom_column`
		 *
		 * @since 2.8.0
		 *
		 * @param string $string      Custom column output. Default empty.
		 * @param string $column_name Name of the column.
		 * @param int    $term_id     Term ID.
		 */
		return apply_filters( "manage_{$this->screen->taxonomy}_custom_column", '', $column_name, $tag->term_id );
	}

	/**
	 * Outputs the hidden row displayed when inline editing
	 *
	 * @since 3.1.0
	 */
	public function inline_edit() {
		$tax = get_taxonomy( $this->screen->taxonomy );

		if ( ! current_user_can( $tax->cap->edit_terms ) ) {
			return;
		}
		?>

		<form method="get">
		<table style="display: none"><tbody id="inlineedit">

			<tr id="inline-edit" class="inline-edit-row" style="display: none">
			<td colspan="<?php echo $this->get_column_count(); ?>" class="colspanchange">
			<div class="inline-edit-wrapper">

			<fieldset>
				<legend class="inline-edit-legend"><?php _e( 'Quick Edit' ); ?></legend>
				<div class="inline-edit-col">
				<label>
					<span class="title"><?php _ex( 'Name', 'term name' ); ?></span>
					<span class="input-text-wrap"><input type="text" name="name" class="ptitle" value="" /></span>
				</label>

				<label>
					<span class="title"><?php _e( 'Slug' ); ?></span>
					<span class="input-text-wrap"><input type="text" name="slug" class="ptitle" value="" /></span>
				</label>
				</div>
			</fieldset>

			<?php
			$core_columns = array(
				'cb'          => true,
				'description' => true,
				'name'        => true,
				'slug'        => true,
				'posts'       => true,
			);

			list( $columns ) = $this->get_column_info();

			foreach ( $columns as $column_name => $column_display_name ) {
				if ( isset( $core_columns[ $column_name ] ) ) {
					continue;
				}

				/** This action is documented in wp-admin/includes/class-wp-posts-list-table.php */
				do_action( 'quick_edit_custom_box', $column_name, 'edit-tags', $this->screen->taxonomy );
			}
			?>

			<div class="inline-edit-save submit">
				<button type="button" class="save button button-primary"><?php echo $tax->labels->update_item; ?></button>
				<button type="button" class="cancel button"><?php _e( 'Cancel' ); ?></button>
				<span class="spinner"></span>

				<?php wp_nonce_field( 'taxinlineeditnonce', '_inline_edit', false ); ?>
				<input type="hidden" name="taxonomy" value="<?php echo esc_attr( $this->screen->taxonomy ); ?>" />
				<input type="hidden" name="post_type" value="<?php echo esc_attr( $this->screen->post_type ); ?>" />

				<?php
				wp_admin_notice(
					'<p class="error"></p>',
					array(
						'type'               => 'error',
						'additional_classes' => array( 'notice-alt', 'inline', 'hidden' ),
						'paragraph_wrap'     => false,
					)
				);
				?>
			</div>
			</div>

			</td></tr>

		</tbody></table>
		</form>
		<?php
	}
}
class-wp-list-table-compat.php000060400000002731151327343360012333 0ustar00<?php
/**
 * Helper functions for displaying a list of items in an ajaxified HTML table.
 *
 * @package WordPress
 * @subpackage List_Table
 * @since 4.7.0
 */

/**
 * Helper class to be used only by back compat functions.
 *
 * @since 3.1.0
 */
class _WP_List_Table_Compat extends WP_List_Table {
	public $_screen;
	public $_columns;

	/**
	 * Constructor.
	 *
	 * @since 3.1.0
	 *
	 * @param string|WP_Screen $screen  The screen hook name or screen object.
	 * @param string[]         $columns An array of columns with column IDs as the keys
	 *                                  and translated column names as the values.
	 */
	public function __construct( $screen, $columns = array() ) {
		if ( is_string( $screen ) ) {
			$screen = convert_to_screen( $screen );
		}

		$this->_screen = $screen;

		if ( ! empty( $columns ) ) {
			$this->_columns = $columns;
			add_filter( 'manage_' . $screen->id . '_columns', array( $this, 'get_columns' ), 0 );
		}
	}

	/**
	 * Gets a list of all, hidden, and sortable columns.
	 *
	 * @since 3.1.0
	 *
	 * @return array
	 */
	protected function get_column_info() {
		$columns  = get_column_headers( $this->_screen );
		$hidden   = get_hidden_columns( $this->_screen );
		$sortable = array();
		$primary  = $this->get_default_primary_column_name();

		return array( $columns, $hidden, $sortable, $primary );
	}

	/**
	 * Gets a list of columns.
	 *
	 * @since 3.1.0
	 *
	 * @return array
	 */
	public function get_columns() {
		return $this->_columns;
	}
}
class-wp-screen.php000060400000110717151327343360010275 0ustar00<?php
/**
 * Screen API: WP_Screen class
 *
 * @package WordPress
 * @subpackage Administration
 * @since 4.4.0
 */

/**
 * Core class used to implement an admin screen API.
 *
 * @since 3.3.0
 */
#[AllowDynamicProperties]
final class WP_Screen {
	/**
	 * Any action associated with the screen.
	 *
	 * 'add' for *-add.php and *-new.php screens. Empty otherwise.
	 *
	 * @since 3.3.0
	 * @var string
	 */
	public $action;

	/**
	 * The base type of the screen.
	 *
	 * This is typically the same as `$id` but with any post types and taxonomies stripped.
	 * For example, for an `$id` of 'edit-post' the base is 'edit'.
	 *
	 * @since 3.3.0
	 * @var string
	 */
	public $base;

	/**
	 * The number of columns to display. Access with get_columns().
	 *
	 * @since 3.4.0
	 * @var int
	 */
	private $columns = 0;

	/**
	 * The unique ID of the screen.
	 *
	 * @since 3.3.0
	 * @var string
	 */
	public $id;

	/**
	 * Which admin the screen is in. network | user | site | false
	 *
	 * @since 3.5.0
	 * @var string
	 */
	protected $in_admin;

	/**
	 * Whether the screen is in the network admin.
	 *
	 * Deprecated. Use in_admin() instead.
	 *
	 * @since 3.3.0
	 * @deprecated 3.5.0
	 * @var bool
	 */
	public $is_network;

	/**
	 * Whether the screen is in the user admin.
	 *
	 * Deprecated. Use in_admin() instead.
	 *
	 * @since 3.3.0
	 * @deprecated 3.5.0
	 * @var bool
	 */
	public $is_user;

	/**
	 * The base menu parent.
	 *
	 * This is derived from `$parent_file` by removing the query string and any .php extension.
	 * `$parent_file` values of 'edit.php?post_type=page' and 'edit.php?post_type=post'
	 * have a `$parent_base` of 'edit'.
	 *
	 * @since 3.3.0
	 * @var string|null
	 */
	public $parent_base;

	/**
	 * The parent_file for the screen per the admin menu system.
	 *
	 * Some `$parent_file` values are 'edit.php?post_type=page', 'edit.php', and 'options-general.php'.
	 *
	 * @since 3.3.0
	 * @var string|null
	 */
	public $parent_file;

	/**
	 * The post type associated with the screen, if any.
	 *
	 * The 'edit.php?post_type=page' screen has a post type of 'page'.
	 * The 'edit-tags.php?taxonomy=$taxonomy&post_type=page' screen has a post type of 'page'.
	 *
	 * @since 3.3.0
	 * @var string
	 */
	public $post_type;

	/**
	 * The taxonomy associated with the screen, if any.
	 *
	 * The 'edit-tags.php?taxonomy=category' screen has a taxonomy of 'category'.
	 *
	 * @since 3.3.0
	 * @var string
	 */
	public $taxonomy;

	/**
	 * The help tab data associated with the screen, if any.
	 *
	 * @since 3.3.0
	 * @var array
	 */
	private $_help_tabs = array();

	/**
	 * The help sidebar data associated with screen, if any.
	 *
	 * @since 3.3.0
	 * @var string
	 */
	private $_help_sidebar = '';

	/**
	 * The accessible hidden headings and text associated with the screen, if any.
	 *
	 * @since 4.4.0
	 * @var string[]
	 */
	private $_screen_reader_content = array();

	/**
	 * Stores old string-based help.
	 *
	 * @var array
	 */
	private static $_old_compat_help = array();

	/**
	 * The screen options associated with screen, if any.
	 *
	 * @since 3.3.0
	 * @var array
	 */
	private $_options = array();

	/**
	 * The screen object registry.
	 *
	 * @since 3.3.0
	 *
	 * @var array
	 */
	private static $_registry = array();

	/**
	 * Stores the result of the public show_screen_options function.
	 *
	 * @since 3.3.0
	 * @var bool
	 */
	private $_show_screen_options;

	/**
	 * Stores the 'screen_settings' section of screen options.
	 *
	 * @since 3.3.0
	 * @var string
	 */
	private $_screen_settings;

	/**
	 * Whether the screen is using the block editor.
	 *
	 * @since 5.0.0
	 * @var bool
	 */
	public $is_block_editor = false;

	/**
	 * Fetches a screen object.
	 *
	 * @since 3.3.0
	 *
	 * @global string $hook_suffix
	 *
	 * @param string|WP_Screen $hook_name Optional. The hook name (also known as the hook suffix) used to determine the screen.
	 *                                    Defaults to the current $hook_suffix global.
	 * @return WP_Screen Screen object.
	 */
	public static function get( $hook_name = '' ) {
		if ( $hook_name instanceof WP_Screen ) {
			return $hook_name;
		}

		$id              = '';
		$post_type       = null;
		$taxonomy        = null;
		$in_admin        = false;
		$action          = '';
		$is_block_editor = false;

		if ( $hook_name ) {
			$id = $hook_name;
		} elseif ( ! empty( $GLOBALS['hook_suffix'] ) ) {
			$id = $GLOBALS['hook_suffix'];
		}

		// For those pesky meta boxes.
		if ( $hook_name && post_type_exists( $hook_name ) ) {
			$post_type = $id;
			$id        = 'post'; // Changes later. Ends up being $base.
		} else {
			if ( str_ends_with( $id, '.php' ) ) {
				$id = substr( $id, 0, -4 );
			}

			if ( in_array( $id, array( 'post-new', 'link-add', 'media-new', 'user-new' ), true ) ) {
				$id     = substr( $id, 0, -4 );
				$action = 'add';
			}
		}

		if ( ! $post_type && $hook_name ) {
			if ( str_ends_with( $id, '-network' ) ) {
				$id       = substr( $id, 0, -8 );
				$in_admin = 'network';
			} elseif ( str_ends_with( $id, '-user' ) ) {
				$id       = substr( $id, 0, -5 );
				$in_admin = 'user';
			}

			$id = sanitize_key( $id );
			if ( 'edit-comments' !== $id && 'edit-tags' !== $id && str_starts_with( $id, 'edit-' ) ) {
				$maybe = substr( $id, 5 );
				if ( taxonomy_exists( $maybe ) ) {
					$id       = 'edit-tags';
					$taxonomy = $maybe;
				} elseif ( post_type_exists( $maybe ) ) {
					$id        = 'edit';
					$post_type = $maybe;
				}
			}

			if ( ! $in_admin ) {
				$in_admin = 'site';
			}
		} else {
			if ( defined( 'WP_NETWORK_ADMIN' ) && WP_NETWORK_ADMIN ) {
				$in_admin = 'network';
			} elseif ( defined( 'WP_USER_ADMIN' ) && WP_USER_ADMIN ) {
				$in_admin = 'user';
			} else {
				$in_admin = 'site';
			}
		}

		if ( 'index' === $id ) {
			$id = 'dashboard';
		} elseif ( 'front' === $id ) {
			$in_admin = false;
		}

		$base = $id;

		// If this is the current screen, see if we can be more accurate for post types and taxonomies.
		if ( ! $hook_name ) {
			if ( isset( $_REQUEST['post_type'] ) ) {
				$post_type = post_type_exists( $_REQUEST['post_type'] ) ? $_REQUEST['post_type'] : false;
			}
			if ( isset( $_REQUEST['taxonomy'] ) ) {
				$taxonomy = taxonomy_exists( $_REQUEST['taxonomy'] ) ? $_REQUEST['taxonomy'] : false;
			}

			switch ( $base ) {
				case 'post':
					if ( isset( $_GET['post'] ) && isset( $_POST['post_ID'] ) && (int) $_GET['post'] !== (int) $_POST['post_ID'] ) {
						wp_die( __( 'A post ID mismatch has been detected.' ), __( 'Sorry, you are not allowed to edit this item.' ), 400 );
					} elseif ( isset( $_GET['post'] ) ) {
						$post_id = (int) $_GET['post'];
					} elseif ( isset( $_POST['post_ID'] ) ) {
						$post_id = (int) $_POST['post_ID'];
					} else {
						$post_id = 0;
					}

					if ( $post_id ) {
						$post = get_post( $post_id );
						if ( $post ) {
							$post_type = $post->post_type;

							/** This filter is documented in wp-admin/post.php */
							$replace_editor = apply_filters( 'replace_editor', false, $post );

							if ( ! $replace_editor ) {
								$is_block_editor = use_block_editor_for_post( $post );
							}
						}
					}
					break;
				case 'edit-tags':
				case 'term':
					if ( null === $post_type && is_object_in_taxonomy( 'post', $taxonomy ? $taxonomy : 'post_tag' ) ) {
						$post_type = 'post';
					}
					break;
				case 'upload':
					$post_type = 'attachment';
					break;
			}
		}

		switch ( $base ) {
			case 'post':
				if ( null === $post_type ) {
					$post_type = 'post';
				}

				// When creating a new post, use the default block editor support value for the post type.
				if ( empty( $post_id ) ) {
					$is_block_editor = use_block_editor_for_post_type( $post_type );
				}

				$id = $post_type;
				break;
			case 'edit':
				if ( null === $post_type ) {
					$post_type = 'post';
				}
				$id .= '-' . $post_type;
				break;
			case 'edit-tags':
			case 'term':
				if ( null === $taxonomy ) {
					$taxonomy = 'post_tag';
				}
				// The edit-tags ID does not contain the post type. Look for it in the request.
				if ( null === $post_type ) {
					$post_type = 'post';
					if ( isset( $_REQUEST['post_type'] ) && post_type_exists( $_REQUEST['post_type'] ) ) {
						$post_type = $_REQUEST['post_type'];
					}
				}

				$id = 'edit-' . $taxonomy;
				break;
		}

		if ( 'network' === $in_admin ) {
			$id   .= '-network';
			$base .= '-network';
		} elseif ( 'user' === $in_admin ) {
			$id   .= '-user';
			$base .= '-user';
		}

		if ( isset( self::$_registry[ $id ] ) ) {
			$screen = self::$_registry[ $id ];
			if ( get_current_screen() === $screen ) {
				return $screen;
			}
		} else {
			$screen     = new self();
			$screen->id = $id;
		}

		$screen->base            = $base;
		$screen->action          = $action;
		$screen->post_type       = (string) $post_type;
		$screen->taxonomy        = (string) $taxonomy;
		$screen->is_user         = ( 'user' === $in_admin );
		$screen->is_network      = ( 'network' === $in_admin );
		$screen->in_admin        = $in_admin;
		$screen->is_block_editor = $is_block_editor;

		self::$_registry[ $id ] = $screen;

		return $screen;
	}

	/**
	 * Makes the screen object the current screen.
	 *
	 * @see set_current_screen()
	 * @since 3.3.0
	 *
	 * @global WP_Screen $current_screen WordPress current screen object.
	 * @global string    $typenow        The post type of the current screen.
	 * @global string    $taxnow         The taxonomy of the current screen.
	 */
	public function set_current_screen() {
		global $current_screen, $taxnow, $typenow;

		$current_screen = $this;
		$typenow        = $this->post_type;
		$taxnow         = $this->taxonomy;

		/**
		 * Fires after the current screen has been set.
		 *
		 * @since 3.0.0
		 *
		 * @param WP_Screen $current_screen Current WP_Screen object.
		 */
		do_action( 'current_screen', $current_screen );
	}

	/**
	 * Constructor
	 *
	 * @since 3.3.0
	 */
	private function __construct() {}

	/**
	 * Indicates whether the screen is in a particular admin.
	 *
	 * @since 3.5.0
	 *
	 * @param string $admin The admin to check against (network | user | site).
	 *                      If empty any of the three admins will result in true.
	 * @return bool True if the screen is in the indicated admin, false otherwise.
	 */
	public function in_admin( $admin = null ) {
		if ( empty( $admin ) ) {
			return (bool) $this->in_admin;
		}

		return ( $admin === $this->in_admin );
	}

	/**
	 * Sets or returns whether the block editor is loading on the current screen.
	 *
	 * @since 5.0.0
	 *
	 * @param bool $set Optional. Sets whether the block editor is loading on the current screen or not.
	 * @return bool True if the block editor is being loaded, false otherwise.
	 */
	public function is_block_editor( $set = null ) {
		if ( null !== $set ) {
			$this->is_block_editor = (bool) $set;
		}

		return $this->is_block_editor;
	}

	/**
	 * Sets the old string-based contextual help for the screen for backward compatibility.
	 *
	 * @since 3.3.0
	 *
	 * @param WP_Screen $screen A screen object.
	 * @param string    $help   Help text.
	 */
	public static function add_old_compat_help( $screen, $help ) {
		self::$_old_compat_help[ $screen->id ] = $help;
	}

	/**
	 * Sets the parent information for the screen.
	 *
	 * This is called in admin-header.php after the menu parent for the screen has been determined.
	 *
	 * @since 3.3.0
	 *
	 * @param string $parent_file The parent file of the screen. Typically the $parent_file global.
	 */
	public function set_parentage( $parent_file ) {
		$this->parent_file         = $parent_file;
		list( $this->parent_base ) = explode( '?', $parent_file );
		$this->parent_base         = str_replace( '.php', '', $this->parent_base );
	}

	/**
	 * Adds an option for the screen.
	 *
	 * Call this in template files after admin.php is loaded and before admin-header.php is loaded
	 * to add screen options.
	 *
	 * @since 3.3.0
	 *
	 * @param string $option Option ID.
	 * @param mixed  $args   Option-dependent arguments.
	 */
	public function add_option( $option, $args = array() ) {
		$this->_options[ $option ] = $args;
	}

	/**
	 * Removes an option from the screen.
	 *
	 * @since 3.8.0
	 *
	 * @param string $option Option ID.
	 */
	public function remove_option( $option ) {
		unset( $this->_options[ $option ] );
	}

	/**
	 * Removes all options from the screen.
	 *
	 * @since 3.8.0
	 */
	public function remove_options() {
		$this->_options = array();
	}

	/**
	 * Gets the options registered for the screen.
	 *
	 * @since 3.8.0
	 *
	 * @return array Options with arguments.
	 */
	public function get_options() {
		return $this->_options;
	}

	/**
	 * Gets the arguments for an option for the screen.
	 *
	 * @since 3.3.0
	 *
	 * @param string       $option Option name.
	 * @param string|false $key    Optional. Specific array key for when the option is an array.
	 *                             Default false.
	 * @return string The option value if set, null otherwise.
	 */
	public function get_option( $option, $key = false ) {
		if ( ! isset( $this->_options[ $option ] ) ) {
			return null;
		}
		if ( $key ) {
			if ( isset( $this->_options[ $option ][ $key ] ) ) {
				return $this->_options[ $option ][ $key ];
			}
			return null;
		}
		return $this->_options[ $option ];
	}

	/**
	 * Gets the help tabs registered for the screen.
	 *
	 * @since 3.4.0
	 * @since 4.4.0 Help tabs are ordered by their priority.
	 *
	 * @return array Help tabs with arguments.
	 */
	public function get_help_tabs() {
		$help_tabs = $this->_help_tabs;

		$priorities = array();
		foreach ( $help_tabs as $help_tab ) {
			if ( isset( $priorities[ $help_tab['priority'] ] ) ) {
				$priorities[ $help_tab['priority'] ][] = $help_tab;
			} else {
				$priorities[ $help_tab['priority'] ] = array( $help_tab );
			}
		}

		ksort( $priorities );

		$sorted = array();
		foreach ( $priorities as $list ) {
			foreach ( $list as $tab ) {
				$sorted[ $tab['id'] ] = $tab;
			}
		}

		return $sorted;
	}

	/**
	 * Gets the arguments for a help tab.
	 *
	 * @since 3.4.0
	 *
	 * @param string $id Help Tab ID.
	 * @return array Help tab arguments.
	 */
	public function get_help_tab( $id ) {
		if ( ! isset( $this->_help_tabs[ $id ] ) ) {
			return null;
		}
		return $this->_help_tabs[ $id ];
	}

	/**
	 * Adds a help tab to the contextual help for the screen.
	 *
	 * Call this on the `load-$pagenow` hook for the relevant screen,
	 * or fetch the `$current_screen` object, or use get_current_screen()
	 * and then call the method from the object.
	 *
	 * You may need to filter `$current_screen` using an if or switch statement
	 * to prevent new help tabs from being added to ALL admin screens.
	 *
	 * @since 3.3.0
	 * @since 4.4.0 The `$priority` argument was added.
	 *
	 * @param array $args {
	 *     Array of arguments used to display the help tab.
	 *
	 *     @type string   $title    Title for the tab. Default false.
	 *     @type string   $id       Tab ID. Must be HTML-safe and should be unique for this menu.
	 *                              It is NOT allowed to contain any empty spaces. Default false.
	 *     @type string   $content  Optional. Help tab content in plain text or HTML. Default empty string.
	 *     @type callable $callback Optional. A callback to generate the tab content. Default false.
	 *     @type int      $priority Optional. The priority of the tab, used for ordering. Default 10.
	 * }
	 */
	public function add_help_tab( $args ) {
		$defaults = array(
			'title'    => false,
			'id'       => false,
			'content'  => '',
			'callback' => false,
			'priority' => 10,
		);
		$args     = wp_parse_args( $args, $defaults );

		$args['id'] = sanitize_html_class( $args['id'] );

		// Ensure we have an ID and title.
		if ( ! $args['id'] || ! $args['title'] ) {
			return;
		}

		// Allows for overriding an existing tab with that ID.
		$this->_help_tabs[ $args['id'] ] = $args;
	}

	/**
	 * Removes a help tab from the contextual help for the screen.
	 *
	 * @since 3.3.0
	 *
	 * @param string $id The help tab ID.
	 */
	public function remove_help_tab( $id ) {
		unset( $this->_help_tabs[ $id ] );
	}

	/**
	 * Removes all help tabs from the contextual help for the screen.
	 *
	 * @since 3.3.0
	 */
	public function remove_help_tabs() {
		$this->_help_tabs = array();
	}

	/**
	 * Gets the content from a contextual help sidebar.
	 *
	 * @since 3.4.0
	 *
	 * @return string Contents of the help sidebar.
	 */
	public function get_help_sidebar() {
		return $this->_help_sidebar;
	}

	/**
	 * Adds a sidebar to the contextual help for the screen.
	 *
	 * Call this in template files after admin.php is loaded and before admin-header.php is loaded
	 * to add a sidebar to the contextual help.
	 *
	 * @since 3.3.0
	 *
	 * @param string $content Sidebar content in plain text or HTML.
	 */
	public function set_help_sidebar( $content ) {
		$this->_help_sidebar = $content;
	}

	/**
	 * Gets the number of layout columns the user has selected.
	 *
	 * The layout_columns option controls the max number and default number of
	 * columns. This method returns the number of columns within that range selected
	 * by the user via Screen Options. If no selection has been made, the default
	 * provisioned in layout_columns is returned. If the screen does not support
	 * selecting the number of layout columns, 0 is returned.
	 *
	 * @since 3.4.0
	 *
	 * @return int Number of columns to display.
	 */
	public function get_columns() {
		return $this->columns;
	}

	/**
	 * Gets the accessible hidden headings and text used in the screen.
	 *
	 * @since 4.4.0
	 *
	 * @see set_screen_reader_content() For more information on the array format.
	 *
	 * @return string[] An associative array of screen reader text strings.
	 */
	public function get_screen_reader_content() {
		return $this->_screen_reader_content;
	}

	/**
	 * Gets a screen reader text string.
	 *
	 * @since 4.4.0
	 *
	 * @param string $key Screen reader text array named key.
	 * @return string Screen reader text string.
	 */
	public function get_screen_reader_text( $key ) {
		if ( ! isset( $this->_screen_reader_content[ $key ] ) ) {
			return null;
		}
		return $this->_screen_reader_content[ $key ];
	}

	/**
	 * Adds accessible hidden headings and text for the screen.
	 *
	 * @since 4.4.0
	 *
	 * @param array $content {
	 *     An associative array of screen reader text strings.
	 *
	 *     @type string $heading_views      Screen reader text for the filter links heading.
	 *                                      Default 'Filter items list'.
	 *     @type string $heading_pagination Screen reader text for the pagination heading.
	 *                                      Default 'Items list navigation'.
	 *     @type string $heading_list       Screen reader text for the items list heading.
	 *                                      Default 'Items list'.
	 * }
	 */
	public function set_screen_reader_content( $content = array() ) {
		$defaults = array(
			'heading_views'      => __( 'Filter items list' ),
			'heading_pagination' => __( 'Items list navigation' ),
			'heading_list'       => __( 'Items list' ),
		);
		$content  = wp_parse_args( $content, $defaults );

		$this->_screen_reader_content = $content;
	}

	/**
	 * Removes all the accessible hidden headings and text for the screen.
	 *
	 * @since 4.4.0
	 */
	public function remove_screen_reader_content() {
		$this->_screen_reader_content = array();
	}

	/**
	 * Renders the screen's help section.
	 *
	 * This will trigger the deprecated filters for backward compatibility.
	 *
	 * @since 3.3.0
	 *
	 * @global string $screen_layout_columns
	 */
	public function render_screen_meta() {

		/**
		 * Filters the legacy contextual help list.
		 *
		 * @since 2.7.0
		 * @deprecated 3.3.0 Use {@see get_current_screen()->add_help_tab()} or
		 *                   {@see get_current_screen()->remove_help_tab()} instead.
		 *
		 * @param array     $old_compat_help Old contextual help.
		 * @param WP_Screen $screen          Current WP_Screen instance.
		 */
		self::$_old_compat_help = apply_filters_deprecated(
			'contextual_help_list',
			array( self::$_old_compat_help, $this ),
			'3.3.0',
			'get_current_screen()->add_help_tab(), get_current_screen()->remove_help_tab()'
		);

		$old_help = isset( self::$_old_compat_help[ $this->id ] ) ? self::$_old_compat_help[ $this->id ] : '';

		/**
		 * Filters the legacy contextual help text.
		 *
		 * @since 2.7.0
		 * @deprecated 3.3.0 Use {@see get_current_screen()->add_help_tab()} or
		 *                   {@see get_current_screen()->remove_help_tab()} instead.
		 *
		 * @param string    $old_help  Help text that appears on the screen.
		 * @param string    $screen_id Screen ID.
		 * @param WP_Screen $screen    Current WP_Screen instance.
		 */
		$old_help = apply_filters_deprecated(
			'contextual_help',
			array( $old_help, $this->id, $this ),
			'3.3.0',
			'get_current_screen()->add_help_tab(), get_current_screen()->remove_help_tab()'
		);

		// Default help only if there is no old-style block of text and no new-style help tabs.
		if ( empty( $old_help ) && ! $this->get_help_tabs() ) {

			/**
			 * Filters the default legacy contextual help text.
			 *
			 * @since 2.8.0
			 * @deprecated 3.3.0 Use {@see get_current_screen()->add_help_tab()} or
			 *                   {@see get_current_screen()->remove_help_tab()} instead.
			 *
			 * @param string $old_help_default Default contextual help text.
			 */
			$default_help = apply_filters_deprecated(
				'default_contextual_help',
				array( '' ),
				'3.3.0',
				'get_current_screen()->add_help_tab(), get_current_screen()->remove_help_tab()'
			);
			if ( $default_help ) {
				$old_help = '<p>' . $default_help . '</p>';
			}
		}

		if ( $old_help ) {
			$this->add_help_tab(
				array(
					'id'      => 'old-contextual-help',
					'title'   => __( 'Overview' ),
					'content' => $old_help,
				)
			);
		}

		$help_sidebar = $this->get_help_sidebar();

		$help_class = 'hidden';
		if ( ! $help_sidebar ) {
			$help_class .= ' no-sidebar';
		}

		// Time to render!
		?>
		<div id="screen-meta" class="metabox-prefs">

			<div id="contextual-help-wrap" class="<?php echo esc_attr( $help_class ); ?>" tabindex="-1" aria-label="<?php esc_attr_e( 'Contextual Help Tab' ); ?>">
				<div id="contextual-help-back"></div>
				<div id="contextual-help-columns">
					<div class="contextual-help-tabs">
						<ul>
						<?php
						$class = ' class="active"';
						foreach ( $this->get_help_tabs() as $tab ) :
							$link_id  = "tab-link-{$tab['id']}";
							$panel_id = "tab-panel-{$tab['id']}";
							?>

							<li id="<?php echo esc_attr( $link_id ); ?>"<?php echo $class; ?>>
								<a href="<?php echo esc_url( "#$panel_id" ); ?>" aria-controls="<?php echo esc_attr( $panel_id ); ?>">
									<?php echo esc_html( $tab['title'] ); ?>
								</a>
							</li>
							<?php
							$class = '';
						endforeach;
						?>
						</ul>
					</div>

					<?php if ( $help_sidebar ) : ?>
					<div class="contextual-help-sidebar">
						<?php echo $help_sidebar; ?>
					</div>
					<?php endif; ?>

					<div class="contextual-help-tabs-wrap">
						<?php
						$classes = 'help-tab-content active';
						foreach ( $this->get_help_tabs() as $tab ) :
							$panel_id = "tab-panel-{$tab['id']}";
							?>

							<div id="<?php echo esc_attr( $panel_id ); ?>" class="<?php echo $classes; ?>">
								<?php
								// Print tab content.
								echo $tab['content'];

								// If it exists, fire tab callback.
								if ( ! empty( $tab['callback'] ) ) {
									call_user_func_array( $tab['callback'], array( $this, $tab ) );
								}
								?>
							</div>
							<?php
							$classes = 'help-tab-content';
						endforeach;
						?>
					</div>
				</div>
			</div>
		<?php
		// Setup layout columns.

		/**
		 * Filters the array of screen layout columns.
		 *
		 * This hook provides back-compat for plugins using the back-compat
		 * Filters instead of add_screen_option().
		 *
		 * @since 2.8.0
		 *
		 * @param array     $empty_columns Empty array.
		 * @param string    $screen_id     Screen ID.
		 * @param WP_Screen $screen        Current WP_Screen instance.
		 */
		$columns = apply_filters( 'screen_layout_columns', array(), $this->id, $this );

		if ( ! empty( $columns ) && isset( $columns[ $this->id ] ) ) {
			$this->add_option( 'layout_columns', array( 'max' => $columns[ $this->id ] ) );
		}

		if ( $this->get_option( 'layout_columns' ) ) {
			$this->columns = (int) get_user_option( "screen_layout_$this->id" );

			if ( ! $this->columns && $this->get_option( 'layout_columns', 'default' ) ) {
				$this->columns = $this->get_option( 'layout_columns', 'default' );
			}
		}
		$GLOBALS['screen_layout_columns'] = $this->columns; // Set the global for back-compat.

		// Add screen options.
		if ( $this->show_screen_options() ) {
			$this->render_screen_options();
		}
		?>
		</div>
		<?php
		if ( ! $this->get_help_tabs() && ! $this->show_screen_options() ) {
			return;
		}
		?>
		<div id="screen-meta-links">
		<?php if ( $this->show_screen_options() ) : ?>
			<div id="screen-options-link-wrap" class="hide-if-no-js screen-meta-toggle">
			<button type="button" id="show-settings-link" class="button show-settings" aria-controls="screen-options-wrap" aria-expanded="false"><?php _e( 'Screen Options' ); ?></button>
			</div>
			<?php
		endif;
		if ( $this->get_help_tabs() ) :
			?>
			<div id="contextual-help-link-wrap" class="hide-if-no-js screen-meta-toggle">
			<button type="button" id="contextual-help-link" class="button show-settings" aria-controls="contextual-help-wrap" aria-expanded="false"><?php _e( 'Help' ); ?></button>
			</div>
		<?php endif; ?>
		</div>
		<?php
	}

	/**
	 * @global array $wp_meta_boxes Global meta box state.
	 *
	 * @return bool
	 */
	public function show_screen_options() {
		global $wp_meta_boxes;

		if ( is_bool( $this->_show_screen_options ) ) {
			return $this->_show_screen_options;
		}

		$columns = get_column_headers( $this );

		$show_screen = ! empty( $wp_meta_boxes[ $this->id ] ) || $columns || $this->get_option( 'per_page' );

		$this->_screen_settings = '';

		if ( 'post' === $this->base ) {
			$expand                 = '<fieldset class="editor-expand hidden"><legend>' . __( 'Additional settings' ) . '</legend><label for="editor-expand-toggle">';
			$expand                .= '<input type="checkbox" id="editor-expand-toggle"' . checked( get_user_setting( 'editor_expand', 'on' ), 'on', false ) . ' />';
			$expand                .= __( 'Enable full-height editor and distraction-free functionality.' ) . '</label></fieldset>';
			$this->_screen_settings = $expand;
		}

		/**
		 * Filters the screen settings text displayed in the Screen Options tab.
		 *
		 * @since 3.0.0
		 *
		 * @param string    $screen_settings Screen settings.
		 * @param WP_Screen $screen          WP_Screen object.
		 */
		$this->_screen_settings = apply_filters( 'screen_settings', $this->_screen_settings, $this );

		if ( $this->_screen_settings || $this->_options ) {
			$show_screen = true;
		}

		/**
		 * Filters whether to show the Screen Options tab.
		 *
		 * @since 3.2.0
		 *
		 * @param bool      $show_screen Whether to show Screen Options tab.
		 *                               Default true.
		 * @param WP_Screen $screen      Current WP_Screen instance.
		 */
		$this->_show_screen_options = apply_filters( 'screen_options_show_screen', $show_screen, $this );
		return $this->_show_screen_options;
	}

	/**
	 * Renders the screen options tab.
	 *
	 * @since 3.3.0
	 *
	 * @param array $options {
	 *     Options for the tab.
	 *
	 *     @type bool $wrap Whether the screen-options-wrap div will be included. Defaults to true.
	 * }
	 */
	public function render_screen_options( $options = array() ) {
		$options = wp_parse_args(
			$options,
			array(
				'wrap' => true,
			)
		);

		$wrapper_start = '';
		$wrapper_end   = '';
		$form_start    = '';
		$form_end      = '';

		// Output optional wrapper.
		if ( $options['wrap'] ) {
			$wrapper_start = '<div id="screen-options-wrap" class="hidden" tabindex="-1" aria-label="' . esc_attr__( 'Screen Options Tab' ) . '">';
			$wrapper_end   = '</div>';
		}

		// Don't output the form and nonce for the widgets accessibility mode links.
		if ( 'widgets' !== $this->base ) {
			$form_start = "\n<form id='adv-settings' method='post'>\n";
			$form_end   = "\n" . wp_nonce_field( 'screen-options-nonce', 'screenoptionnonce', false, false ) . "\n</form>\n";
		}

		echo $wrapper_start . $form_start;

		$this->render_meta_boxes_preferences();
		$this->render_list_table_columns_preferences();
		$this->render_screen_layout();
		$this->render_per_page_options();
		$this->render_view_mode();
		echo $this->_screen_settings;

		/**
		 * Filters whether to show the Screen Options submit button.
		 *
		 * @since 4.4.0
		 *
		 * @param bool      $show_button Whether to show Screen Options submit button.
		 *                               Default false.
		 * @param WP_Screen $screen      Current WP_Screen instance.
		 */
		$show_button = apply_filters( 'screen_options_show_submit', false, $this );

		if ( $show_button ) {
			submit_button( __( 'Apply' ), 'primary', 'screen-options-apply', true );
		}

		echo $form_end . $wrapper_end;
	}

	/**
	 * Renders the meta boxes preferences.
	 *
	 * @since 4.4.0
	 *
	 * @global array $wp_meta_boxes Global meta box state.
	 */
	public function render_meta_boxes_preferences() {
		global $wp_meta_boxes;

		if ( ! isset( $wp_meta_boxes[ $this->id ] ) ) {
			return;
		}
		?>
		<fieldset class="metabox-prefs">
		<legend><?php _e( 'Screen elements' ); ?></legend>
		<p>
			<?php _e( 'Some screen elements can be shown or hidden by using the checkboxes.' ); ?>
			<?php _e( 'Expand or collapse the elements by clicking on their headings, and arrange them by dragging their headings or by clicking on the up and down arrows.' ); ?>
		</p>
		<div class="metabox-prefs-container">
		<?php

		meta_box_prefs( $this );

		if ( 'dashboard' === $this->id && has_action( 'welcome_panel' ) && current_user_can( 'edit_theme_options' ) ) {
			if ( isset( $_GET['welcome'] ) ) {
				$welcome_checked = empty( $_GET['welcome'] ) ? 0 : 1;
				update_user_meta( get_current_user_id(), 'show_welcome_panel', $welcome_checked );
			} else {
				$welcome_checked = (int) get_user_meta( get_current_user_id(), 'show_welcome_panel', true );
				if ( 2 === $welcome_checked && wp_get_current_user()->user_email !== get_option( 'admin_email' ) ) {
					$welcome_checked = false;
				}
			}
			echo '<label for="wp_welcome_panel-hide">';
			echo '<input type="checkbox" id="wp_welcome_panel-hide"' . checked( (bool) $welcome_checked, true, false ) . ' />';
			echo _x( 'Welcome', 'Welcome panel' ) . "</label>\n";
		}
		?>
		</div>
		</fieldset>
		<?php
	}

	/**
	 * Renders the list table columns preferences.
	 *
	 * @since 4.4.0
	 */
	public function render_list_table_columns_preferences() {

		$columns = get_column_headers( $this );
		$hidden  = get_hidden_columns( $this );

		if ( ! $columns ) {
			return;
		}

		$legend = ! empty( $columns['_title'] ) ? $columns['_title'] : __( 'Columns' );
		?>
		<fieldset class="metabox-prefs">
		<legend><?php echo $legend; ?></legend>
		<?php
		$special = array( '_title', 'cb', 'comment', 'media', 'name', 'title', 'username', 'blogname' );

		foreach ( $columns as $column => $title ) {
			// Can't hide these for they are special.
			if ( in_array( $column, $special, true ) ) {
				continue;
			}

			if ( empty( $title ) ) {
				continue;
			}

			/*
			 * The Comments column uses HTML in the display name with some screen
			 * reader text. Make sure to strip tags from the Comments column
			 * title and any other custom column title plugins might add.
			 */
			$title = wp_strip_all_tags( $title );

			$id = "$column-hide";
			echo '<label>';
			echo '<input class="hide-column-tog" name="' . $id . '" type="checkbox" id="' . $id . '" value="' . $column . '"' . checked( ! in_array( $column, $hidden, true ), true, false ) . ' />';
			echo "$title</label>\n";
		}
		?>
		</fieldset>
		<?php
	}

	/**
	 * Renders the option for number of columns on the page.
	 *
	 * @since 3.3.0
	 */
	public function render_screen_layout() {
		if ( ! $this->get_option( 'layout_columns' ) ) {
			return;
		}

		$screen_layout_columns = $this->get_columns();
		$num                   = $this->get_option( 'layout_columns', 'max' );

		?>
		<fieldset class='columns-prefs'>
		<legend class="screen-layout"><?php _e( 'Layout' ); ?></legend>
		<?php for ( $i = 1; $i <= $num; ++$i ) : ?>
			<label class="columns-prefs-<?php echo $i; ?>">
			<input type='radio' name='screen_columns' value='<?php echo esc_attr( $i ); ?>' <?php checked( $screen_layout_columns, $i ); ?> />
			<?php
				printf(
					/* translators: %s: Number of columns on the page. */
					_n( '%s column', '%s columns', $i ),
					number_format_i18n( $i )
				);
			?>
			</label>
		<?php endfor; ?>
		</fieldset>
		<?php
	}

	/**
	 * Renders the items per page option.
	 *
	 * @since 3.3.0
	 */
	public function render_per_page_options() {
		if ( null === $this->get_option( 'per_page' ) ) {
			return;
		}

		$per_page_label = $this->get_option( 'per_page', 'label' );
		if ( null === $per_page_label ) {
			$per_page_label = __( 'Number of items per page:' );
		}

		$option = $this->get_option( 'per_page', 'option' );
		if ( ! $option ) {
			$option = str_replace( '-', '_', "{$this->id}_per_page" );
		}

		$per_page = (int) get_user_option( $option );
		if ( empty( $per_page ) || $per_page < 1 ) {
			$per_page = $this->get_option( 'per_page', 'default' );
			if ( ! $per_page ) {
				$per_page = 20;
			}
		}

		if ( 'edit_comments_per_page' === $option ) {
			$comment_status = isset( $_REQUEST['comment_status'] ) ? $_REQUEST['comment_status'] : 'all';

			/** This filter is documented in wp-admin/includes/class-wp-comments-list-table.php */
			$per_page = apply_filters( 'comments_per_page', $per_page, $comment_status );
		} elseif ( 'categories_per_page' === $option ) {
			/** This filter is documented in wp-admin/includes/class-wp-terms-list-table.php */
			$per_page = apply_filters( 'edit_categories_per_page', $per_page );
		} else {
			/** This filter is documented in wp-admin/includes/class-wp-list-table.php */
			$per_page = apply_filters( "{$option}", $per_page );
		}

		// Back compat.
		if ( isset( $this->post_type ) ) {
			/** This filter is documented in wp-admin/includes/post.php */
			$per_page = apply_filters( 'edit_posts_per_page', $per_page, $this->post_type );
		}

		// This needs a submit button.
		add_filter( 'screen_options_show_submit', '__return_true' );

		?>
		<fieldset class="screen-options">
		<legend><?php _e( 'Pagination' ); ?></legend>
			<?php if ( $per_page_label ) : ?>
				<label for="<?php echo esc_attr( $option ); ?>"><?php echo $per_page_label; ?></label>
				<input type="number" step="1" min="1" max="999" class="screen-per-page" name="wp_screen_options[value]"
					id="<?php echo esc_attr( $option ); ?>"
					value="<?php echo esc_attr( $per_page ); ?>" />
			<?php endif; ?>
				<input type="hidden" name="wp_screen_options[option]" value="<?php echo esc_attr( $option ); ?>" />
		</fieldset>
		<?php
	}

	/**
	 * Renders the list table view mode preferences.
	 *
	 * @since 4.4.0
	 *
	 * @global string $mode List table view mode.
	 */
	public function render_view_mode() {
		global $mode;

		$screen = get_current_screen();

		// Currently only enabled for posts and comments lists.
		if ( 'edit' !== $screen->base && 'edit-comments' !== $screen->base ) {
			return;
		}

		$view_mode_post_types = get_post_types( array( 'show_ui' => true ) );

		/**
		 * Filters the post types that have different view mode options.
		 *
		 * @since 4.4.0
		 *
		 * @param string[] $view_mode_post_types Array of post types that can change view modes.
		 *                                       Default post types with show_ui on.
		 */
		$view_mode_post_types = apply_filters( 'view_mode_post_types', $view_mode_post_types );

		if ( 'edit' === $screen->base && ! in_array( $this->post_type, $view_mode_post_types, true ) ) {
			return;
		}

		if ( ! isset( $mode ) ) {
			$mode = get_user_setting( 'posts_list_mode', 'list' );
		}

		// This needs a submit button.
		add_filter( 'screen_options_show_submit', '__return_true' );
		?>
		<fieldset class="metabox-prefs view-mode">
			<legend><?php _e( 'View mode' ); ?></legend>
			<label for="list-view-mode">
				<input id="list-view-mode" type="radio" name="mode" value="list" <?php checked( 'list', $mode ); ?> />
				<?php _e( 'Compact view' ); ?>
			</label>
			<label for="excerpt-view-mode">
				<input id="excerpt-view-mode" type="radio" name="mode" value="excerpt" <?php checked( 'excerpt', $mode ); ?> />
				<?php _e( 'Extended view' ); ?>
			</label>
		</fieldset>
		<?php
	}

	/**
	 * Renders screen reader text.
	 *
	 * @since 4.4.0
	 *
	 * @param string $key The screen reader text array named key.
	 * @param string $tag Optional. The HTML tag to wrap the screen reader text. Default h2.
	 */
	public function render_screen_reader_content( $key = '', $tag = 'h2' ) {

		if ( ! isset( $this->_screen_reader_content[ $key ] ) ) {
			return;
		}
		echo "<$tag class='screen-reader-text'>" . $this->_screen_reader_content[ $key ] . "</$tag>";
	}
}
class-wp-upgrader-skins.php000060400000002705151327343360011751 0ustar00<?php
/**
 * The User Interface "Skins" for the WordPress File Upgrader
 *
 * @package WordPress
 * @subpackage Upgrader
 * @since 2.8.0
 * @deprecated 4.7.0
 */

_deprecated_file( basename( __FILE__ ), '4.7.0', 'class-wp-upgrader.php' );

/** WP_Upgrader_Skin class */
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader-skin.php';

/** Plugin_Upgrader_Skin class */
require_once ABSPATH . 'wp-admin/includes/class-plugin-upgrader-skin.php';

/** Theme_Upgrader_Skin class */
require_once ABSPATH . 'wp-admin/includes/class-theme-upgrader-skin.php';

/** Bulk_Upgrader_Skin class */
require_once ABSPATH . 'wp-admin/includes/class-bulk-upgrader-skin.php';

/** Bulk_Plugin_Upgrader_Skin class */
require_once ABSPATH . 'wp-admin/includes/class-bulk-plugin-upgrader-skin.php';

/** Bulk_Theme_Upgrader_Skin class */
require_once ABSPATH . 'wp-admin/includes/class-bulk-theme-upgrader-skin.php';

/** Plugin_Installer_Skin class */
require_once ABSPATH . 'wp-admin/includes/class-plugin-installer-skin.php';

/** Theme_Installer_Skin class */
require_once ABSPATH . 'wp-admin/includes/class-theme-installer-skin.php';

/** Language_Pack_Upgrader_Skin class */
require_once ABSPATH . 'wp-admin/includes/class-language-pack-upgrader-skin.php';

/** Automatic_Upgrader_Skin class */
require_once ABSPATH . 'wp-admin/includes/class-automatic-upgrader-skin.php';

/** WP_Ajax_Upgrader_Skin class */
require_once ABSPATH . 'wp-admin/includes/class-wp-ajax-upgrader-skin.php';
class-core-upgrader.php000060400000035527151327343360011136 0ustar00<?php
/**
 * Upgrade API: Core_Upgrader class
 *
 * @package WordPress
 * @subpackage Upgrader
 * @since 4.6.0
 */

/**
 * Core class used for updating core.
 *
 * It allows for WordPress to upgrade itself in combination with
 * the wp-admin/includes/update-core.php file.
 *
 * Note: Newly introduced functions and methods cannot be used here.
 * All functions must be present in the previous version being upgraded from
 * as this file is used there too.
 *
 * @since 2.8.0
 * @since 4.6.0 Moved to its own file from wp-admin/includes/class-wp-upgrader.php.
 *
 * @see WP_Upgrader
 */
class Core_Upgrader extends WP_Upgrader {

	/**
	 * Initializes the upgrade strings.
	 *
	 * @since 2.8.0
	 */
	public function upgrade_strings() {
		$this->strings['up_to_date'] = __( 'WordPress is at the latest version.' );
		$this->strings['locked']     = __( 'Another update is currently in progress.' );
		$this->strings['no_package'] = __( 'Update package not available.' );
		/* translators: %s: Package URL. */
		$this->strings['downloading_package']   = sprintf( __( 'Downloading update from %s&#8230;' ), '<span class="code pre">%s</span>' );
		$this->strings['unpack_package']        = __( 'Unpacking the update&#8230;' );
		$this->strings['copy_failed']           = __( 'Could not copy files.' );
		$this->strings['copy_failed_space']     = __( 'Could not copy files. You may have run out of disk space.' );
		$this->strings['start_rollback']        = __( 'Attempting to restore the previous version.' );
		$this->strings['rollback_was_required'] = __( 'Due to an error during updating, WordPress has been restored to your previous version.' );
	}

	/**
	 * Upgrades WordPress core.
	 *
	 * @since 2.8.0
	 *
	 * @global WP_Filesystem_Base $wp_filesystem                WordPress filesystem subclass.
	 * @global callable           $_wp_filesystem_direct_method
	 *
	 * @param object $current Response object for whether WordPress is current.
	 * @param array  $args {
	 *     Optional. Arguments for upgrading WordPress core. Default empty array.
	 *
	 *     @type bool $pre_check_md5    Whether to check the file checksums before
	 *                                  attempting the upgrade. Default true.
	 *     @type bool $attempt_rollback Whether to attempt to rollback the chances if
	 *                                  there is a problem. Default false.
	 *     @type bool $do_rollback      Whether to perform this "upgrade" as a rollback.
	 *                                  Default false.
	 * }
	 * @return string|false|WP_Error New WordPress version on success, false or WP_Error on failure.
	 */
	public function upgrade( $current, $args = array() ) {
		global $wp_filesystem;

		require ABSPATH . WPINC . '/version.php'; // $wp_version;

		$start_time = time();

		$defaults    = array(
			'pre_check_md5'                => true,
			'attempt_rollback'             => false,
			'do_rollback'                  => false,
			'allow_relaxed_file_ownership' => false,
		);
		$parsed_args = wp_parse_args( $args, $defaults );

		$this->init();
		$this->upgrade_strings();

		// Is an update available?
		if ( ! isset( $current->response ) || 'latest' === $current->response ) {
			return new WP_Error( 'up_to_date', $this->strings['up_to_date'] );
		}

		$res = $this->fs_connect( array( ABSPATH, WP_CONTENT_DIR ), $parsed_args['allow_relaxed_file_ownership'] );
		if ( ! $res || is_wp_error( $res ) ) {
			return $res;
		}

		$wp_dir = trailingslashit( $wp_filesystem->abspath() );

		$partial = true;
		if ( $parsed_args['do_rollback'] ) {
			$partial = false;
		} elseif ( $parsed_args['pre_check_md5'] && ! $this->check_files() ) {
			$partial = false;
		}

		/*
		 * If partial update is returned from the API, use that, unless we're doing
		 * a reinstallation. If we cross the new_bundled version number, then use
		 * the new_bundled zip. Don't though if the constant is set to skip bundled items.
		 * If the API returns a no_content zip, go with it. Finally, default to the full zip.
		 */
		if ( $parsed_args['do_rollback'] && $current->packages->rollback ) {
			$to_download = 'rollback';
		} elseif ( $current->packages->partial && 'reinstall' !== $current->response && $wp_version === $current->partial_version && $partial ) {
			$to_download = 'partial';
		} elseif ( $current->packages->new_bundled && version_compare( $wp_version, $current->new_bundled, '<' )
			&& ( ! defined( 'CORE_UPGRADE_SKIP_NEW_BUNDLED' ) || ! CORE_UPGRADE_SKIP_NEW_BUNDLED ) ) {
			$to_download = 'new_bundled';
		} elseif ( $current->packages->no_content ) {
			$to_download = 'no_content';
		} else {
			$to_download = 'full';
		}

		// Lock to prevent multiple Core Updates occurring.
		$lock = WP_Upgrader::create_lock( 'core_updater', 15 * MINUTE_IN_SECONDS );
		if ( ! $lock ) {
			return new WP_Error( 'locked', $this->strings['locked'] );
		}

		$download = $this->download_package( $current->packages->$to_download, false );

		/*
		 * Allow for signature soft-fail.
		 * WARNING: This may be removed in the future.
		 */
		if ( is_wp_error( $download ) && $download->get_error_data( 'softfail-filename' ) ) {
			// Output the failure error as a normal feedback, and not as an error:
			/** This filter is documented in wp-admin/includes/update-core.php */
			apply_filters( 'update_feedback', $download->get_error_message() );

			// Report this failure back to WordPress.org for debugging purposes.
			wp_version_check(
				array(
					'signature_failure_code' => $download->get_error_code(),
					'signature_failure_data' => $download->get_error_data(),
				)
			);

			// Pretend this error didn't happen.
			$download = $download->get_error_data( 'softfail-filename' );
		}

		if ( is_wp_error( $download ) ) {
			WP_Upgrader::release_lock( 'core_updater' );
			return $download;
		}

		$working_dir = $this->unpack_package( $download );
		if ( is_wp_error( $working_dir ) ) {
			WP_Upgrader::release_lock( 'core_updater' );
			return $working_dir;
		}

		// Copy update-core.php from the new version into place.
		if ( ! $wp_filesystem->copy( $working_dir . '/wordpress/wp-admin/includes/update-core.php', $wp_dir . 'wp-admin/includes/update-core.php', true ) ) {
			$wp_filesystem->delete( $working_dir, true );
			WP_Upgrader::release_lock( 'core_updater' );
			return new WP_Error( 'copy_failed_for_update_core_file', __( 'The update cannot be installed because some files could not be copied. This is usually due to inconsistent file permissions.' ), 'wp-admin/includes/update-core.php' );
		}
		$wp_filesystem->chmod( $wp_dir . 'wp-admin/includes/update-core.php', FS_CHMOD_FILE );

		wp_opcache_invalidate( ABSPATH . 'wp-admin/includes/update-core.php' );
		require_once ABSPATH . 'wp-admin/includes/update-core.php';

		if ( ! function_exists( 'update_core' ) ) {
			WP_Upgrader::release_lock( 'core_updater' );
			return new WP_Error( 'copy_failed_space', $this->strings['copy_failed_space'] );
		}

		$result = update_core( $working_dir, $wp_dir );

		// In the event of an issue, we may be able to roll back.
		if ( $parsed_args['attempt_rollback'] && $current->packages->rollback && ! $parsed_args['do_rollback'] ) {
			$try_rollback = false;
			if ( is_wp_error( $result ) ) {
				$error_code = $result->get_error_code();
				/*
				 * Not all errors are equal. These codes are critical: copy_failed__copy_dir,
				 * mkdir_failed__copy_dir, copy_failed__copy_dir_retry, and disk_full.
				 * do_rollback allows for update_core() to trigger a rollback if needed.
				 */
				if ( str_contains( $error_code, 'do_rollback' ) ) {
					$try_rollback = true;
				} elseif ( str_contains( $error_code, '__copy_dir' ) ) {
					$try_rollback = true;
				} elseif ( 'disk_full' === $error_code ) {
					$try_rollback = true;
				}
			}

			if ( $try_rollback ) {
				/** This filter is documented in wp-admin/includes/update-core.php */
				apply_filters( 'update_feedback', $result );

				/** This filter is documented in wp-admin/includes/update-core.php */
				apply_filters( 'update_feedback', $this->strings['start_rollback'] );

				$rollback_result = $this->upgrade( $current, array_merge( $parsed_args, array( 'do_rollback' => true ) ) );

				$original_result = $result;
				$result          = new WP_Error(
					'rollback_was_required',
					$this->strings['rollback_was_required'],
					(object) array(
						'update'   => $original_result,
						'rollback' => $rollback_result,
					)
				);
			}
		}

		/** This action is documented in wp-admin/includes/class-wp-upgrader.php */
		do_action(
			'upgrader_process_complete',
			$this,
			array(
				'action' => 'update',
				'type'   => 'core',
			)
		);

		// Clear the current updates.
		delete_site_transient( 'update_core' );

		if ( ! $parsed_args['do_rollback'] ) {
			$stats = array(
				'update_type'      => $current->response,
				'success'          => true,
				'fs_method'        => $wp_filesystem->method,
				'fs_method_forced' => defined( 'FS_METHOD' ) || has_filter( 'filesystem_method' ),
				'fs_method_direct' => ! empty( $GLOBALS['_wp_filesystem_direct_method'] ) ? $GLOBALS['_wp_filesystem_direct_method'] : '',
				'time_taken'       => time() - $start_time,
				'reported'         => $wp_version,
				'attempted'        => $current->version,
			);

			if ( is_wp_error( $result ) ) {
				$stats['success'] = false;
				// Did a rollback occur?
				if ( ! empty( $try_rollback ) ) {
					$stats['error_code'] = $original_result->get_error_code();
					$stats['error_data'] = $original_result->get_error_data();
					// Was the rollback successful? If not, collect its error too.
					$stats['rollback'] = ! is_wp_error( $rollback_result );
					if ( is_wp_error( $rollback_result ) ) {
						$stats['rollback_code'] = $rollback_result->get_error_code();
						$stats['rollback_data'] = $rollback_result->get_error_data();
					}
				} else {
					$stats['error_code'] = $result->get_error_code();
					$stats['error_data'] = $result->get_error_data();
				}
			}

			wp_version_check( $stats );
		}

		WP_Upgrader::release_lock( 'core_updater' );

		return $result;
	}

	/**
	 * Determines if this WordPress Core version should update to an offered version or not.
	 *
	 * @since 3.7.0
	 *
	 * @param string $offered_ver The offered version, of the format x.y.z.
	 * @return bool True if we should update to the offered version, otherwise false.
	 */
	public static function should_update_to_version( $offered_ver ) {
		require ABSPATH . WPINC . '/version.php'; // $wp_version; // x.y.z

		$current_branch = implode( '.', array_slice( preg_split( '/[.-]/', $wp_version ), 0, 2 ) ); // x.y
		$new_branch     = implode( '.', array_slice( preg_split( '/[.-]/', $offered_ver ), 0, 2 ) ); // x.y

		$current_is_development_version = (bool) strpos( $wp_version, '-' );

		// Defaults:
		$upgrade_dev   = get_site_option( 'auto_update_core_dev', 'enabled' ) === 'enabled';
		$upgrade_minor = get_site_option( 'auto_update_core_minor', 'enabled' ) === 'enabled';
		$upgrade_major = get_site_option( 'auto_update_core_major', 'unset' ) === 'enabled';

		// WP_AUTO_UPDATE_CORE = true (all), 'beta', 'rc', 'development', 'branch-development', 'minor', false.
		if ( defined( 'WP_AUTO_UPDATE_CORE' ) ) {
			if ( false === WP_AUTO_UPDATE_CORE ) {
				// Defaults to turned off, unless a filter allows it.
				$upgrade_dev   = false;
				$upgrade_minor = false;
				$upgrade_major = false;
			} elseif ( true === WP_AUTO_UPDATE_CORE
				|| in_array( WP_AUTO_UPDATE_CORE, array( 'beta', 'rc', 'development', 'branch-development' ), true )
			) {
				// ALL updates for core.
				$upgrade_dev   = true;
				$upgrade_minor = true;
				$upgrade_major = true;
			} elseif ( 'minor' === WP_AUTO_UPDATE_CORE ) {
				// Only minor updates for core.
				$upgrade_dev   = false;
				$upgrade_minor = true;
				$upgrade_major = false;
			}
		}

		// 1: If we're already on that version, not much point in updating?
		if ( $offered_ver === $wp_version ) {
			return false;
		}

		// 2: If we're running a newer version, that's a nope.
		if ( version_compare( $wp_version, $offered_ver, '>' ) ) {
			return false;
		}

		$failure_data = get_site_option( 'auto_core_update_failed' );
		if ( $failure_data ) {
			// If this was a critical update failure, cannot update.
			if ( ! empty( $failure_data['critical'] ) ) {
				return false;
			}

			// Don't claim we can update on update-core.php if we have a non-critical failure logged.
			if ( $wp_version === $failure_data['current'] && str_contains( $offered_ver, '.1.next.minor' ) ) {
				return false;
			}

			/*
			 * Cannot update if we're retrying the same A to B update that caused a non-critical failure.
			 * Some non-critical failures do allow retries, like download_failed.
			 * 3.7.1 => 3.7.2 resulted in files_not_writable, if we are still on 3.7.1 and still trying to update to 3.7.2.
			 */
			if ( empty( $failure_data['retry'] ) && $wp_version === $failure_data['current'] && $offered_ver === $failure_data['attempted'] ) {
				return false;
			}
		}

		// 3: 3.7-alpha-25000 -> 3.7-alpha-25678 -> 3.7-beta1 -> 3.7-beta2.
		if ( $current_is_development_version ) {

			/**
			 * Filters whether to enable automatic core updates for development versions.
			 *
			 * @since 3.7.0
			 *
			 * @param bool $upgrade_dev Whether to enable automatic updates for
			 *                          development versions.
			 */
			if ( ! apply_filters( 'allow_dev_auto_core_updates', $upgrade_dev ) ) {
				return false;
			}
			// Else fall through to minor + major branches below.
		}

		// 4: Minor in-branch updates (3.7.0 -> 3.7.1 -> 3.7.2 -> 3.7.4).
		if ( $current_branch === $new_branch ) {

			/**
			 * Filters whether to enable minor automatic core updates.
			 *
			 * @since 3.7.0
			 *
			 * @param bool $upgrade_minor Whether to enable minor automatic core updates.
			 */
			return apply_filters( 'allow_minor_auto_core_updates', $upgrade_minor );
		}

		// 5: Major version updates (3.7.0 -> 3.8.0 -> 3.9.1).
		if ( version_compare( $new_branch, $current_branch, '>' ) ) {

			/**
			 * Filters whether to enable major automatic core updates.
			 *
			 * @since 3.7.0
			 *
			 * @param bool $upgrade_major Whether to enable major automatic core updates.
			 */
			return apply_filters( 'allow_major_auto_core_updates', $upgrade_major );
		}

		// If we're not sure, we don't want it.
		return false;
	}

	/**
	 * Compares the disk file checksums against the expected checksums.
	 *
	 * @since 3.7.0
	 *
	 * @global string $wp_version       The WordPress version string.
	 * @global string $wp_local_package Locale code of the package.
	 *
	 * @return bool True if the checksums match, otherwise false.
	 */
	public function check_files() {
		global $wp_version, $wp_local_package;

		$checksums = get_core_checksums( $wp_version, isset( $wp_local_package ) ? $wp_local_package : 'en_US' );

		if ( ! is_array( $checksums ) ) {
			return false;
		}

		foreach ( $checksums as $file => $checksum ) {
			// Skip files which get updated.
			if ( str_starts_with( $file, 'wp-content' ) ) {
				continue;
			}
			if ( ! file_exists( ABSPATH . $file ) || md5_file( ABSPATH . $file ) !== $checksum ) {
				return false;
			}
		}

		return true;
	}
}
class-wp-application-passwords-list-table.php000060400000015445151327343360015404 0ustar00<?php
/**
 * List Table API: WP_Application_Passwords_List_Table class
 *
 * @package WordPress
 * @subpackage Administration
 * @since 5.6.0
 */

/**
 * Class for displaying the list of application password items.
 *
 * @since 5.6.0
 *
 * @see WP_List_Table
 */
class WP_Application_Passwords_List_Table extends WP_List_Table {

	/**
	 * Gets the list of columns.
	 *
	 * @since 5.6.0
	 *
	 * @return string[] Array of column titles keyed by their column name.
	 */
	public function get_columns() {
		return array(
			'name'      => __( 'Name' ),
			'created'   => __( 'Created' ),
			'last_used' => __( 'Last Used' ),
			'last_ip'   => __( 'Last IP' ),
			'revoke'    => __( 'Revoke' ),
		);
	}

	/**
	 * Prepares the list of items for displaying.
	 *
	 * @since 5.6.0
	 *
	 * @global int $user_id User ID.
	 */
	public function prepare_items() {
		global $user_id;
		$this->items = array_reverse( WP_Application_Passwords::get_user_application_passwords( $user_id ) );
	}

	/**
	 * Handles the name column output.
	 *
	 * @since 5.6.0
	 *
	 * @param array $item The current application password item.
	 */
	public function column_name( $item ) {
		echo esc_html( $item['name'] );
	}

	/**
	 * Handles the created column output.
	 *
	 * @since 5.6.0
	 *
	 * @param array $item The current application password item.
	 */
	public function column_created( $item ) {
		if ( empty( $item['created'] ) ) {
			echo '&mdash;';
		} else {
			echo date_i18n( __( 'F j, Y' ), $item['created'] );
		}
	}

	/**
	 * Handles the last used column output.
	 *
	 * @since 5.6.0
	 *
	 * @param array $item The current application password item.
	 */
	public function column_last_used( $item ) {
		if ( empty( $item['last_used'] ) ) {
			echo '&mdash;';
		} else {
			echo date_i18n( __( 'F j, Y' ), $item['last_used'] );
		}
	}

	/**
	 * Handles the last ip column output.
	 *
	 * @since 5.6.0
	 *
	 * @param array $item The current application password item.
	 */
	public function column_last_ip( $item ) {
		if ( empty( $item['last_ip'] ) ) {
			echo '&mdash;';
		} else {
			echo $item['last_ip'];
		}
	}

	/**
	 * Handles the revoke column output.
	 *
	 * @since 5.6.0
	 *
	 * @param array $item The current application password item.
	 */
	public function column_revoke( $item ) {
		$name = 'revoke-application-password-' . $item['uuid'];
		printf(
			'<button type="button" name="%1$s" id="%1$s" class="button delete" aria-label="%2$s">%3$s</button>',
			esc_attr( $name ),
			/* translators: %s: the application password's given name. */
			esc_attr( sprintf( __( 'Revoke "%s"' ), $item['name'] ) ),
			__( 'Revoke' )
		);
	}

	/**
	 * Generates content for a single row of the table
	 *
	 * @since 5.6.0
	 *
	 * @param array  $item        The current item.
	 * @param string $column_name The current column name.
	 */
	protected function column_default( $item, $column_name ) {
		/**
		 * Fires for each custom column in the Application Passwords list table.
		 *
		 * Custom columns are registered using the {@see 'manage_application-passwords-user_columns'} filter.
		 *
		 * @since 5.6.0
		 *
		 * @param string $column_name Name of the custom column.
		 * @param array  $item        The application password item.
		 */
		do_action( "manage_{$this->screen->id}_custom_column", $column_name, $item );
	}

	/**
	 * Generates custom table navigation to prevent conflicting nonces.
	 *
	 * @since 5.6.0
	 *
	 * @param string $which The location of the bulk actions: Either 'top' or 'bottom'.
	 */
	protected function display_tablenav( $which ) {
		?>
		<div class="tablenav <?php echo esc_attr( $which ); ?>">
			<?php if ( 'bottom' === $which ) : ?>
				<div class="alignright">
					<button type="button" name="revoke-all-application-passwords" id="revoke-all-application-passwords" class="button delete"><?php _e( 'Revoke all application passwords' ); ?></button>
				</div>
			<?php endif; ?>
			<div class="alignleft actions bulkactions">
				<?php $this->bulk_actions( $which ); ?>
			</div>
			<?php
			$this->extra_tablenav( $which );
			$this->pagination( $which );
			?>
			<br class="clear" />
		</div>
		<?php
	}

	/**
	 * Generates content for a single row of the table.
	 *
	 * @since 5.6.0
	 *
	 * @param array $item The current item.
	 */
	public function single_row( $item ) {
		echo '<tr data-uuid="' . esc_attr( $item['uuid'] ) . '">';
		$this->single_row_columns( $item );
		echo '</tr>';
	}

	/**
	 * Gets the name of the default primary column.
	 *
	 * @since 5.6.0
	 *
	 * @return string Name of the default primary column, in this case, 'name'.
	 */
	protected function get_default_primary_column_name() {
		return 'name';
	}

	/**
	 * Prints the JavaScript template for the new row item.
	 *
	 * @since 5.6.0
	 */
	public function print_js_template_row() {
		list( $columns, $hidden, , $primary ) = $this->get_column_info();

		echo '<tr data-uuid="{{ data.uuid }}">';

		foreach ( $columns as $column_name => $display_name ) {
			$is_primary = $primary === $column_name;
			$classes    = "{$column_name} column-{$column_name}";

			if ( $is_primary ) {
				$classes .= ' has-row-actions column-primary';
			}

			if ( in_array( $column_name, $hidden, true ) ) {
				$classes .= ' hidden';
			}

			printf( '<td class="%s" data-colname="%s">', esc_attr( $classes ), esc_attr( wp_strip_all_tags( $display_name ) ) );

			switch ( $column_name ) {
				case 'name':
					echo '{{ data.name }}';
					break;
				case 'created':
					// JSON encoding automatically doubles backslashes to ensure they don't get lost when printing the inline JS.
					echo '<# print( wp.date.dateI18n( ' . wp_json_encode( __( 'F j, Y' ) ) . ', data.created ) ) #>';
					break;
				case 'last_used':
					echo '<# print( data.last_used !== null ? wp.date.dateI18n( ' . wp_json_encode( __( 'F j, Y' ) ) . ", data.last_used ) : '—' ) #>";
					break;
				case 'last_ip':
					echo "{{ data.last_ip || '—' }}";
					break;
				case 'revoke':
					printf(
						'<button type="button" class="button delete" aria-label="%1$s">%2$s</button>',
						/* translators: %s: the application password's given name. */
						esc_attr( sprintf( __( 'Revoke "%s"' ), '{{ data.name }}' ) ),
						esc_html__( 'Revoke' )
					);
					break;
				default:
					/**
					 * Fires in the JavaScript row template for each custom column in the Application Passwords list table.
					 *
					 * Custom columns are registered using the {@see 'manage_application-passwords-user_columns'} filter.
					 *
					 * @since 5.6.0
					 *
					 * @param string $column_name Name of the custom column.
					 */
					do_action( "manage_{$this->screen->id}_custom_column_js_template", $column_name );
					break;
			}

			if ( $is_primary ) {
				echo '<button type="button" class="toggle-row"><span class="screen-reader-text">' .
					/* translators: Hidden accessibility text. */
					__( 'Show more details' ) .
				'</span></button>';
			}

			echo '</td>';
		}

		echo '</tr>';
	}
}
update.php000064400000103205151327343360006547 0ustar00<?php
/**
 * WordPress Administration Update API
 *
 * @package WordPress
 * @subpackage Administration
 */

/**
 * Selects the first update version from the update_core option.
 *
 * @since 2.7.0
 *
 * @return object|array|false The response from the API on success, false on failure.
 */
function get_preferred_from_update_core() {
	$updates = get_core_updates();

	if ( ! is_array( $updates ) ) {
		return false;
	}

	if ( empty( $updates ) ) {
		return (object) array( 'response' => 'latest' );
	}

	return $updates[0];
}

/**
 * Gets available core updates.
 *
 * @since 2.7.0
 *
 * @param array $options Set $options['dismissed'] to true to show dismissed upgrades too,
 *                       set $options['available'] to false to skip not-dismissed updates.
 * @return array|false Array of the update objects on success, false on failure.
 */
function get_core_updates( $options = array() ) {
	$options = array_merge(
		array(
			'available' => true,
			'dismissed' => false,
		),
		$options
	);

	$dismissed = get_site_option( 'dismissed_update_core' );

	if ( ! is_array( $dismissed ) ) {
		$dismissed = array();
	}

	$from_api = get_site_transient( 'update_core' );

	if ( ! isset( $from_api->updates ) || ! is_array( $from_api->updates ) ) {
		return false;
	}

	$updates = $from_api->updates;
	$result  = array();

	foreach ( $updates as $update ) {
		if ( 'autoupdate' === $update->response ) {
			continue;
		}

		if ( array_key_exists( $update->current . '|' . $update->locale, $dismissed ) ) {
			if ( $options['dismissed'] ) {
				$update->dismissed = true;
				$result[]          = $update;
			}
		} else {
			if ( $options['available'] ) {
				$update->dismissed = false;
				$result[]          = $update;
			}
		}
	}

	return $result;
}

/**
 * Gets the best available (and enabled) Auto-Update for WordPress core.
 *
 * If there's 1.2.3 and 1.3 on offer, it'll choose 1.3 if the installation allows it, else, 1.2.3.
 *
 * @since 3.7.0
 *
 * @return object|false The core update offering on success, false on failure.
 */
function find_core_auto_update() {
	$updates = get_site_transient( 'update_core' );

	if ( ! $updates || empty( $updates->updates ) ) {
		return false;
	}

	require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';

	$auto_update = false;
	$upgrader    = new WP_Automatic_Updater();

	foreach ( $updates->updates as $update ) {
		if ( 'autoupdate' !== $update->response ) {
			continue;
		}

		if ( ! $upgrader->should_update( 'core', $update, ABSPATH ) ) {
			continue;
		}

		if ( ! $auto_update || version_compare( $update->current, $auto_update->current, '>' ) ) {
			$auto_update = $update;
		}
	}

	return $auto_update;
}

/**
 * Gets and caches the checksums for the given version of WordPress.
 *
 * @since 3.7.0
 *
 * @param string $version Version string to query.
 * @param string $locale  Locale to query.
 * @return array|false An array of checksums on success, false on failure.
 */
function get_core_checksums( $version, $locale ) {
	$http_url = 'http://api.wordpress.org/core/checksums/1.0/?' . http_build_query( compact( 'version', 'locale' ), '', '&' );
	$url      = $http_url;

	$ssl = wp_http_supports( array( 'ssl' ) );

	if ( $ssl ) {
		$url = set_url_scheme( $url, 'https' );
	}

	$options = array(
		'timeout' => wp_doing_cron() ? 30 : 3,
	);

	$response = wp_remote_get( $url, $options );

	if ( $ssl && is_wp_error( $response ) ) {
		wp_trigger_error(
			__FUNCTION__,
			sprintf(
				/* translators: %s: Support forums URL. */
				__( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server&#8217;s configuration. If you continue to have problems, please try the <a href="%s">support forums</a>.' ),
				__( 'https://wordpress.org/support/forums/' )
			) . ' ' . __( '(WordPress could not establish a secure connection to WordPress.org. Please contact your server administrator.)' ),
			headers_sent() || WP_DEBUG ? E_USER_WARNING : E_USER_NOTICE
		);

		$response = wp_remote_get( $http_url, $options );
	}

	if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
		return false;
	}

	$body = trim( wp_remote_retrieve_body( $response ) );
	$body = json_decode( $body, true );

	if ( ! is_array( $body ) || ! isset( $body['checksums'] ) || ! is_array( $body['checksums'] ) ) {
		return false;
	}

	return $body['checksums'];
}

/**
 * Dismisses core update.
 *
 * @since 2.7.0
 *
 * @param object $update
 * @return bool
 */
function dismiss_core_update( $update ) {
	$dismissed = get_site_option( 'dismissed_update_core' );
	$dismissed[ $update->current . '|' . $update->locale ] = true;

	return update_site_option( 'dismissed_update_core', $dismissed );
}

/**
 * Undismisses core update.
 *
 * @since 2.7.0
 *
 * @param string $version
 * @param string $locale
 * @return bool
 */
function undismiss_core_update( $version, $locale ) {
	$dismissed = get_site_option( 'dismissed_update_core' );
	$key       = $version . '|' . $locale;

	if ( ! isset( $dismissed[ $key ] ) ) {
		return false;
	}

	unset( $dismissed[ $key ] );

	return update_site_option( 'dismissed_update_core', $dismissed );
}

/**
 * Finds the available update for WordPress core.
 *
 * @since 2.7.0
 *
 * @param string $version Version string to find the update for.
 * @param string $locale  Locale to find the update for.
 * @return object|false The core update offering on success, false on failure.
 */
function find_core_update( $version, $locale ) {
	$from_api = get_site_transient( 'update_core' );

	if ( ! isset( $from_api->updates ) || ! is_array( $from_api->updates ) ) {
		return false;
	}

	$updates = $from_api->updates;

	foreach ( $updates as $update ) {
		if ( $update->current === $version && $update->locale === $locale ) {
			return $update;
		}
	}

	return false;
}

/**
 * Returns core update footer message.
 *
 * @since 2.3.0
 *
 * @param string $msg
 * @return string
 */
function core_update_footer( $msg = '' ) {
	if ( ! current_user_can( 'update_core' ) ) {
		/* translators: %s: WordPress version. */
		return sprintf( __( 'Version %s' ), get_bloginfo( 'version', 'display' ) );
	}

	$cur = get_preferred_from_update_core();

	if ( ! is_object( $cur ) ) {
		$cur = new stdClass();
	}

	if ( ! isset( $cur->current ) ) {
		$cur->current = '';
	}

	if ( ! isset( $cur->response ) ) {
		$cur->response = '';
	}

	$is_development_version = preg_match( '/alpha|beta|RC/', wp_get_wp_version() );

	if ( $is_development_version ) {
		return sprintf(
			/* translators: 1: WordPress version number, 2: URL to WordPress Updates screen. */
			__( 'You are using a development version (%1$s). Cool! Please <a href="%2$s">stay updated</a>.' ),
			get_bloginfo( 'version', 'display' ),
			network_admin_url( 'update-core.php' )
		);
	}

	switch ( $cur->response ) {
		case 'upgrade':
			return sprintf(
				'<strong><a href="%s">%s</a></strong>',
				network_admin_url( 'update-core.php' ),
				/* translators: %s: WordPress version. */
				sprintf( __( 'Get Version %s' ), $cur->current )
			);

		case 'latest':
		default:
			/* translators: %s: WordPress version. */
			return sprintf( __( 'Version %s' ), get_bloginfo( 'version', 'display' ) );
	}
}

/**
 * Returns core update notification message.
 *
 * @since 2.3.0
 *
 * @global string $pagenow The filename of the current screen.
 * @return void|false
 */
function update_nag() {
	global $pagenow;

	if ( is_multisite() && ! current_user_can( 'update_core' ) ) {
		return false;
	}

	if ( 'update-core.php' === $pagenow ) {
		return;
	}

	$cur = get_preferred_from_update_core();

	if ( ! isset( $cur->response ) || 'upgrade' !== $cur->response ) {
		return false;
	}

	$version_url = sprintf(
		/* translators: %s: WordPress version. */
		esc_url( __( 'https://wordpress.org/documentation/wordpress-version/version-%s/' ) ),
		sanitize_title( $cur->current )
	);

	if ( current_user_can( 'update_core' ) ) {
		$msg = sprintf(
			/* translators: 1: URL to WordPress release notes, 2: New WordPress version, 3: URL to network admin, 4: Accessibility text. */
			__( '<a href="%1$s">WordPress %2$s</a> is available! <a href="%3$s" aria-label="%4$s">Please update now</a>.' ),
			$version_url,
			$cur->current,
			network_admin_url( 'update-core.php' ),
			esc_attr__( 'Please update WordPress now' )
		);
	} else {
		$msg = sprintf(
			/* translators: 1: URL to WordPress release notes, 2: New WordPress version. */
			__( '<a href="%1$s">WordPress %2$s</a> is available! Please notify the site administrator.' ),
			$version_url,
			$cur->current
		);
	}

	wp_admin_notice(
		$msg,
		array(
			'type'               => 'warning',
			'additional_classes' => array( 'update-nag', 'inline' ),
			'paragraph_wrap'     => false,
		)
	);
}

/**
 * Displays WordPress version and active theme in the 'At a Glance' dashboard widget.
 *
 * @since 2.5.0
 */
function update_right_now_message() {
	$theme_name = wp_get_theme();

	if ( current_user_can( 'switch_themes' ) ) {
		$theme_name = sprintf( '<a href="themes.php">%1$s</a>', $theme_name );
	}

	$msg = '';

	if ( current_user_can( 'update_core' ) ) {
		$cur = get_preferred_from_update_core();

		if ( isset( $cur->response ) && 'upgrade' === $cur->response ) {
			$msg .= sprintf(
				'<a href="%s" class="button" aria-describedby="wp-version">%s</a> ',
				network_admin_url( 'update-core.php' ),
				/* translators: %s: WordPress version number, or 'Latest' string. */
				sprintf( __( 'Update to %s' ), $cur->current ? $cur->current : __( 'Latest' ) )
			);
		}
	}

	/* translators: 1: Version number, 2: Theme name. */
	$content = __( 'WordPress %1$s running %2$s theme.' );

	/**
	 * Filters the text displayed in the 'At a Glance' dashboard widget.
	 *
	 * Prior to 3.8.0, the widget was named 'Right Now'.
	 *
	 * @since 4.4.0
	 *
	 * @param string $content Default text.
	 */
	$content = apply_filters( 'update_right_now_text', $content );

	$msg .= sprintf( '<span id="wp-version">' . $content . '</span>', get_bloginfo( 'version', 'display' ), $theme_name );

	echo "<p id='wp-version-message'>$msg</p>";
}

/**
 * Retrieves plugins with updates available.
 *
 * @since 2.9.0
 *
 * @return object[]
 */
function get_plugin_updates() {
	$all_plugins     = get_plugins();
	$upgrade_plugins = array();
	$current         = get_site_transient( 'update_plugins' );

	foreach ( (array) $all_plugins as $plugin_file => $plugin_data ) {
		if ( isset( $current->response[ $plugin_file ] ) ) {
			$upgrade_plugins[ $plugin_file ]         = (object) $plugin_data;
			$upgrade_plugins[ $plugin_file ]->update = $current->response[ $plugin_file ];
		}
	}

	return $upgrade_plugins;
}

/**
 * Adds a callback to display update information for plugins with updates available.
 *
 * @since 2.9.0
 */
function wp_plugin_update_rows() {
	if ( ! current_user_can( 'update_plugins' ) ) {
		return;
	}

	$plugins = get_site_transient( 'update_plugins' );

	if ( isset( $plugins->response ) && is_array( $plugins->response ) ) {
		$plugins = array_keys( $plugins->response );

		foreach ( $plugins as $plugin_file ) {
			add_action( "after_plugin_row_{$plugin_file}", 'wp_plugin_update_row', 10, 2 );
		}
	}
}

/**
 * Displays update information for a plugin.
 *
 * @since 2.3.0
 *
 * @param string $file        Plugin basename.
 * @param array  $plugin_data Plugin information.
 * @return void|false
 */
function wp_plugin_update_row( $file, $plugin_data ) {
	$current = get_site_transient( 'update_plugins' );

	if ( ! isset( $current->response[ $file ] ) ) {
		return false;
	}

	$response = $current->response[ $file ];

	$plugins_allowedtags = array(
		'a'       => array(
			'href'  => array(),
			'title' => array(),
		),
		'abbr'    => array( 'title' => array() ),
		'acronym' => array( 'title' => array() ),
		'code'    => array(),
		'em'      => array(),
		'strong'  => array(),
	);

	$plugin_name = wp_kses( $plugin_data['Name'], $plugins_allowedtags );
	$plugin_slug = isset( $response->slug ) ? $response->slug : $response->id;

	if ( isset( $response->slug ) ) {
		$details_url = self_admin_url( 'plugin-install.php?tab=plugin-information&plugin=' . $plugin_slug . '&section=changelog' );
	} elseif ( isset( $response->url ) ) {
		$details_url = $response->url;
	} else {
		$details_url = $plugin_data['PluginURI'];
	}

	$details_url = add_query_arg(
		array(
			'TB_iframe' => 'true',
			'width'     => 600,
			'height'    => 800,
		),
		$details_url
	);

	/** @var WP_Plugins_List_Table $wp_list_table */
	$wp_list_table = _get_list_table(
		'WP_Plugins_List_Table',
		array(
			'screen' => get_current_screen(),
		)
	);

	if ( is_network_admin() || ! is_multisite() ) {
		if ( is_network_admin() ) {
			$active_class = is_plugin_active_for_network( $file ) ? ' active' : '';
		} else {
			$active_class = is_plugin_active( $file ) ? ' active' : '';
		}

		$requires_php   = isset( $response->requires_php ) ? $response->requires_php : null;
		$compatible_php = is_php_version_compatible( $requires_php );
		$notice_type    = $compatible_php ? 'notice-warning' : 'notice-error';

		printf(
			'<tr class="plugin-update-tr%s" id="%s" data-slug="%s" data-plugin="%s">' .
			'<td colspan="%s" class="plugin-update colspanchange">' .
			'<div class="update-message notice inline %s notice-alt"><p>',
			$active_class,
			esc_attr( $plugin_slug . '-update' ),
			esc_attr( $plugin_slug ),
			esc_attr( $file ),
			esc_attr( $wp_list_table->get_column_count() ),
			$notice_type
		);

		if ( ! current_user_can( 'update_plugins' ) ) {
			printf(
				/* translators: 1: Plugin name, 2: Details URL, 3: Additional link attributes, 4: Version number. */
				__( 'There is a new version of %1$s available. <a href="%2$s" %3$s>View version %4$s details</a>.' ),
				$plugin_name,
				esc_url( $details_url ),
				sprintf(
					'class="thickbox open-plugin-details-modal" aria-label="%s"',
					/* translators: 1: Plugin name, 2: Version number. */
					esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $plugin_name, $response->new_version ) )
				),
				esc_attr( $response->new_version )
			);
		} elseif ( empty( $response->package ) ) {
			printf(
				/* translators: 1: Plugin name, 2: Details URL, 3: Additional link attributes, 4: Version number. */
				__( 'There is a new version of %1$s available. <a href="%2$s" %3$s>View version %4$s details</a>. <em>Automatic update is unavailable for this plugin.</em>' ),
				$plugin_name,
				esc_url( $details_url ),
				sprintf(
					'class="thickbox open-plugin-details-modal" aria-label="%s"',
					/* translators: 1: Plugin name, 2: Version number. */
					esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $plugin_name, $response->new_version ) )
				),
				esc_attr( $response->new_version )
			);
		} else {
			if ( $compatible_php ) {
				printf(
					/* translators: 1: Plugin name, 2: Details URL, 3: Additional link attributes, 4: Version number, 5: Update URL, 6: Additional link attributes. */
					__( 'There is a new version of %1$s available. <a href="%2$s" %3$s>View version %4$s details</a> or <a href="%5$s" %6$s>update now</a>.' ),
					$plugin_name,
					esc_url( $details_url ),
					sprintf(
						'class="thickbox open-plugin-details-modal" aria-label="%s"',
						/* translators: 1: Plugin name, 2: Version number. */
						esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $plugin_name, $response->new_version ) )
					),
					esc_attr( $response->new_version ),
					wp_nonce_url( self_admin_url( 'update.php?action=upgrade-plugin&plugin=' ) . $file, 'upgrade-plugin_' . $file ),
					sprintf(
						'class="update-link" aria-label="%s"',
						/* translators: %s: Plugin name. */
						esc_attr( sprintf( _x( 'Update %s now', 'plugin' ), $plugin_name ) )
					)
				);
			} else {
				printf(
					/* translators: 1: Plugin name, 2: Details URL, 3: Additional link attributes, 4: Version number 5: URL to Update PHP page. */
					__( 'There is a new version of %1$s available, but it does not work with your version of PHP. <a href="%2$s" %3$s>View version %4$s details</a> or <a href="%5$s">learn more about updating PHP</a>.' ),
					$plugin_name,
					esc_url( $details_url ),
					sprintf(
						'class="thickbox open-plugin-details-modal" aria-label="%s"',
						/* translators: 1: Plugin name, 2: Version number. */
						esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $plugin_name, $response->new_version ) )
					),
					esc_attr( $response->new_version ),
					esc_url( wp_get_update_php_url() )
				);
				wp_update_php_annotation( '<br><em>', '</em>' );
			}
		}

		/**
		 * Fires at the end of the update message container in each
		 * row of the plugins list table.
		 *
		 * The dynamic portion of the hook name, `$file`, refers to the path
		 * of the plugin's primary file relative to the plugins directory.
		 *
		 * @since 2.8.0
		 *
		 * @param array  $plugin_data An array of plugin metadata. See get_plugin_data()
		 *                            and the {@see 'plugin_row_meta'} filter for the list
		 *                            of possible values.
		 * @param object $response {
		 *     An object of metadata about the available plugin update.
		 *
		 *     @type string   $id           Plugin ID, e.g. `w.org/plugins/[plugin-name]`.
		 *     @type string   $slug         Plugin slug.
		 *     @type string   $plugin       Plugin basename.
		 *     @type string   $new_version  New plugin version.
		 *     @type string   $url          Plugin URL.
		 *     @type string   $package      Plugin update package URL.
		 *     @type string[] $icons        An array of plugin icon URLs.
		 *     @type string[] $banners      An array of plugin banner URLs.
		 *     @type string[] $banners_rtl  An array of plugin RTL banner URLs.
		 *     @type string   $requires     The version of WordPress which the plugin requires.
		 *     @type string   $tested       The version of WordPress the plugin is tested against.
		 *     @type string   $requires_php The version of PHP which the plugin requires.
		 * }
		 */
		do_action( "in_plugin_update_message-{$file}", $plugin_data, $response ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores

		echo '</p></div></td></tr>';
	}
}

/**
 * Retrieves themes with updates available.
 *
 * @since 2.9.0
 *
 * @return WP_Theme[]
 */
function get_theme_updates() {
	$current = get_site_transient( 'update_themes' );

	if ( ! isset( $current->response ) ) {
		return array();
	}

	$update_themes = array();

	foreach ( $current->response as $stylesheet => $data ) {
		$update_themes[ $stylesheet ]         = wp_get_theme( $stylesheet );
		$update_themes[ $stylesheet ]->update = $data;
	}

	return $update_themes;
}

/**
 * Adds a callback to display update information for themes with updates available.
 *
 * @since 3.1.0
 */
function wp_theme_update_rows() {
	if ( ! current_user_can( 'update_themes' ) ) {
		return;
	}

	$themes = get_site_transient( 'update_themes' );

	if ( isset( $themes->response ) && is_array( $themes->response ) ) {
		$themes = array_keys( $themes->response );

		foreach ( $themes as $theme ) {
			add_action( "after_theme_row_{$theme}", 'wp_theme_update_row', 10, 2 );
		}
	}
}

/**
 * Displays update information for a theme.
 *
 * @since 3.1.0
 *
 * @param string   $theme_key Theme stylesheet.
 * @param WP_Theme $theme     Theme object.
 * @return void|false
 */
function wp_theme_update_row( $theme_key, $theme ) {
	$current = get_site_transient( 'update_themes' );

	if ( ! isset( $current->response[ $theme_key ] ) ) {
		return false;
	}

	$response = $current->response[ $theme_key ];

	$details_url = add_query_arg(
		array(
			'TB_iframe' => 'true',
			'width'     => 1024,
			'height'    => 800,
		),
		$current->response[ $theme_key ]['url']
	);

	/** @var WP_MS_Themes_List_Table $wp_list_table */
	$wp_list_table = _get_list_table( 'WP_MS_Themes_List_Table' );

	$active = $theme->is_allowed( 'network' ) ? ' active' : '';

	$requires_wp  = isset( $response['requires'] ) ? $response['requires'] : null;
	$requires_php = isset( $response['requires_php'] ) ? $response['requires_php'] : null;

	$compatible_wp  = is_wp_version_compatible( $requires_wp );
	$compatible_php = is_php_version_compatible( $requires_php );

	printf(
		'<tr class="plugin-update-tr%s" id="%s" data-slug="%s">' .
		'<td colspan="%s" class="plugin-update colspanchange">' .
		'<div class="update-message notice inline notice-warning notice-alt"><p>',
		$active,
		esc_attr( $theme->get_stylesheet() . '-update' ),
		esc_attr( $theme->get_stylesheet() ),
		$wp_list_table->get_column_count()
	);

	if ( $compatible_wp && $compatible_php ) {
		if ( ! current_user_can( 'update_themes' ) ) {
			printf(
				/* translators: 1: Theme name, 2: Details URL, 3: Additional link attributes, 4: Version number. */
				__( 'There is a new version of %1$s available. <a href="%2$s" %3$s>View version %4$s details</a>.' ),
				$theme['Name'],
				esc_url( $details_url ),
				sprintf(
					'class="thickbox open-plugin-details-modal" aria-label="%s"',
					/* translators: 1: Theme name, 2: Version number. */
					esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $theme['Name'], $response['new_version'] ) )
				),
				$response['new_version']
			);
		} elseif ( empty( $response['package'] ) ) {
			printf(
				/* translators: 1: Theme name, 2: Details URL, 3: Additional link attributes, 4: Version number. */
				__( 'There is a new version of %1$s available. <a href="%2$s" %3$s>View version %4$s details</a>. <em>Automatic update is unavailable for this theme.</em>' ),
				$theme['Name'],
				esc_url( $details_url ),
				sprintf(
					'class="thickbox open-plugin-details-modal" aria-label="%s"',
					/* translators: 1: Theme name, 2: Version number. */
					esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $theme['Name'], $response['new_version'] ) )
				),
				$response['new_version']
			);
		} else {
			printf(
				/* translators: 1: Theme name, 2: Details URL, 3: Additional link attributes, 4: Version number, 5: Update URL, 6: Additional link attributes. */
				__( 'There is a new version of %1$s available. <a href="%2$s" %3$s>View version %4$s details</a> or <a href="%5$s" %6$s>update now</a>.' ),
				$theme['Name'],
				esc_url( $details_url ),
				sprintf(
					'class="thickbox open-plugin-details-modal" aria-label="%s"',
					/* translators: 1: Theme name, 2: Version number. */
					esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $theme['Name'], $response['new_version'] ) )
				),
				$response['new_version'],
				wp_nonce_url( self_admin_url( 'update.php?action=upgrade-theme&theme=' ) . $theme_key, 'upgrade-theme_' . $theme_key ),
				sprintf(
					'class="update-link" aria-label="%s"',
					/* translators: %s: Theme name. */
					esc_attr( sprintf( _x( 'Update %s now', 'theme' ), $theme['Name'] ) )
				)
			);
		}
	} else {
		if ( ! $compatible_wp && ! $compatible_php ) {
			printf(
				/* translators: %s: Theme name. */
				__( 'There is a new version of %s available, but it does not work with your versions of WordPress and PHP.' ),
				$theme['Name']
			);
			if ( current_user_can( 'update_core' ) && current_user_can( 'update_php' ) ) {
				printf(
					/* translators: 1: URL to WordPress Updates screen, 2: URL to Update PHP page. */
					' ' . __( '<a href="%1$s">Please update WordPress</a>, and then <a href="%2$s">learn more about updating PHP</a>.' ),
					self_admin_url( 'update-core.php' ),
					esc_url( wp_get_update_php_url() )
				);
				wp_update_php_annotation( '</p><p><em>', '</em>' );
			} elseif ( current_user_can( 'update_core' ) ) {
				printf(
					/* translators: %s: URL to WordPress Updates screen. */
					' ' . __( '<a href="%s">Please update WordPress</a>.' ),
					self_admin_url( 'update-core.php' )
				);
			} elseif ( current_user_can( 'update_php' ) ) {
				printf(
					/* translators: %s: URL to Update PHP page. */
					' ' . __( '<a href="%s">Learn more about updating PHP</a>.' ),
					esc_url( wp_get_update_php_url() )
				);
				wp_update_php_annotation( '</p><p><em>', '</em>' );
			}
		} elseif ( ! $compatible_wp ) {
			printf(
				/* translators: %s: Theme name. */
				__( 'There is a new version of %s available, but it does not work with your version of WordPress.' ),
				$theme['Name']
			);
			if ( current_user_can( 'update_core' ) ) {
				printf(
					/* translators: %s: URL to WordPress Updates screen. */
					' ' . __( '<a href="%s">Please update WordPress</a>.' ),
					self_admin_url( 'update-core.php' )
				);
			}
		} elseif ( ! $compatible_php ) {
			printf(
				/* translators: %s: Theme name. */
				__( 'There is a new version of %s available, but it does not work with your version of PHP.' ),
				$theme['Name']
			);
			if ( current_user_can( 'update_php' ) ) {
				printf(
					/* translators: %s: URL to Update PHP page. */
					' ' . __( '<a href="%s">Learn more about updating PHP</a>.' ),
					esc_url( wp_get_update_php_url() )
				);
				wp_update_php_annotation( '</p><p><em>', '</em>' );
			}
		}
	}

	/**
	 * Fires at the end of the update message container in each
	 * row of the themes list table.
	 *
	 * The dynamic portion of the hook name, `$theme_key`, refers to
	 * the theme slug as found in the WordPress.org themes repository.
	 *
	 * @since 3.1.0
	 *
	 * @param WP_Theme $theme    The WP_Theme object.
	 * @param array    $response {
	 *     An array of metadata about the available theme update.
	 *
	 *     @type string $new_version New theme version.
	 *     @type string $url         Theme URL.
	 *     @type string $package     Theme update package URL.
	 * }
	 */
	do_action( "in_theme_update_message-{$theme_key}", $theme, $response ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores

	echo '</p></div></td></tr>';
}

/**
 * Displays maintenance nag HTML message.
 *
 * @since 2.7.0
 *
 * @global int $upgrading
 *
 * @return void|false
 */
function maintenance_nag() {
	global $upgrading;

	$nag = isset( $upgrading );

	if ( ! $nag ) {
		$failed = get_site_option( 'auto_core_update_failed' );
		/*
		 * If an update failed critically, we may have copied over version.php but not other files.
		 * In that case, if the installation claims we're running the version we attempted, nag.
		 * This is serious enough to err on the side of nagging.
		 *
		 * If we simply failed to update before we tried to copy any files, then assume things are
		 * OK if they are now running the latest.
		 *
		 * This flag is cleared whenever a successful update occurs using Core_Upgrader.
		 */
		$comparison = ! empty( $failed['critical'] ) ? '>=' : '>';
		if ( isset( $failed['attempted'] ) && version_compare( $failed['attempted'], wp_get_wp_version(), $comparison ) ) {
			$nag = true;
		}
	}

	if ( ! $nag ) {
		return false;
	}

	if ( current_user_can( 'update_core' ) ) {
		$msg = sprintf(
			/* translators: %s: URL to WordPress Updates screen. */
			__( 'An automated WordPress update has failed to complete - <a href="%s">please attempt the update again now</a>.' ),
			'update-core.php'
		);
	} else {
		$msg = __( 'An automated WordPress update has failed to complete! Please notify the site administrator.' );
	}

	wp_admin_notice(
		$msg,
		array(
			'type'               => 'warning',
			'additional_classes' => array( 'update-nag', 'inline' ),
			'paragraph_wrap'     => false,
		)
	);
}

/**
 * Prints the JavaScript templates for update admin notices.
 *
 * @since 4.6.0
 *
 * Template takes one argument with four values:
 *
 *     param {object} data {
 *         Arguments for admin notice.
 *
 *         @type string id        ID of the notice.
 *         @type string className Class names for the notice.
 *         @type string message   The notice's message.
 *         @type string type      The type of update the notice is for. Either 'plugin' or 'theme'.
 *     }
 */
function wp_print_admin_notice_templates() {
	?>
	<script id="tmpl-wp-updates-admin-notice" type="text/html">
		<div <# if ( data.id ) { #>id="{{ data.id }}"<# } #> class="notice {{ data.className }}"><p>{{{ data.message }}}</p></div>
	</script>
	<script id="tmpl-wp-bulk-updates-admin-notice" type="text/html">
		<div id="{{ data.id }}" class="{{ data.className }} notice <# if ( data.errorMessage ) { #>notice-error<# } else { #>notice-success<# } #>">
			<p>
				<# if ( data.successMessage ) { #>
					{{{ data.successMessage }}}
				<# } #>
				<# if ( data.errorMessage ) { #>
					<button class="button-link bulk-action-errors-collapsed" aria-expanded="false">
						{{{ data.errorMessage }}}
						<span class="screen-reader-text">
							<?php
							/* translators: Hidden accessibility text. */
							_e( 'Show more details' );
							?>
						</span>
						<span class="toggle-indicator" aria-hidden="true"></span>
					</button>
				<# } #>
			</p>
			<# if ( data.errorMessages ) { #>
				<ul class="bulk-action-errors hidden">
					<# _.each( data.errorMessages, function( errorMessage ) { #>
						<li>{{ errorMessage }}</li>
					<# } ); #>
				</ul>
			<# } #>
		</div>
	</script>
	<?php
}

/**
 * Prints the JavaScript templates for update and deletion rows in list tables.
 *
 * @since 4.6.0
 *
 * The update template takes one argument with four values:
 *
 *     param {object} data {
 *         Arguments for the update row
 *
 *         @type string slug    Plugin slug.
 *         @type string plugin  Plugin base name.
 *         @type string colspan The number of table columns this row spans.
 *         @type string content The row content.
 *     }
 *
 * The delete template takes one argument with four values:
 *
 *     param {object} data {
 *         Arguments for the update row
 *
 *         @type string slug    Plugin slug.
 *         @type string plugin  Plugin base name.
 *         @type string name    Plugin name.
 *         @type string colspan The number of table columns this row spans.
 *     }
 */
function wp_print_update_row_templates() {
	?>
	<script id="tmpl-item-update-row" type="text/template">
		<tr class="plugin-update-tr update" id="{{ data.slug }}-update" data-slug="{{ data.slug }}" <# if ( data.plugin ) { #>data-plugin="{{ data.plugin }}"<# } #>>
			<td colspan="{{ data.colspan }}" class="plugin-update colspanchange">
				{{{ data.content }}}
			</td>
		</tr>
	</script>
	<script id="tmpl-item-deleted-row" type="text/template">
		<tr class="plugin-deleted-tr inactive deleted" id="{{ data.slug }}-deleted" data-slug="{{ data.slug }}" <# if ( data.plugin ) { #>data-plugin="{{ data.plugin }}"<# } #>>
			<td colspan="{{ data.colspan }}" class="plugin-update colspanchange">
				<# if ( data.plugin ) { #>
					<?php
					printf(
						/* translators: %s: Plugin name. */
						_x( '%s was successfully deleted.', 'plugin' ),
						'<strong>{{{ data.name }}}</strong>'
					);
					?>
				<# } else { #>
					<?php
					printf(
						/* translators: %s: Theme name. */
						_x( '%s was successfully deleted.', 'theme' ),
						'<strong>{{{ data.name }}}</strong>'
					);
					?>
				<# } #>
			</td>
		</tr>
	</script>
	<?php
}

/**
 * Displays a notice when the user is in recovery mode.
 *
 * @since 5.2.0
 */
function wp_recovery_mode_nag() {
	if ( ! wp_is_recovery_mode() ) {
		return;
	}

	$url = wp_login_url();
	$url = add_query_arg( 'action', WP_Recovery_Mode::EXIT_ACTION, $url );
	$url = wp_nonce_url( $url, WP_Recovery_Mode::EXIT_ACTION );

	$message = sprintf(
		/* translators: %s: Recovery Mode exit link. */
		__( 'You are in recovery mode. This means there may be an error with a theme or plugin. To exit recovery mode, log out or use the Exit button. <a href="%s">Exit Recovery Mode</a>' ),
		esc_url( $url )
	);
	wp_admin_notice( $message, array( 'type' => 'info' ) );
}

/**
 * Checks whether auto-updates are enabled.
 *
 * @since 5.5.0
 *
 * @param string $type The type of update being checked: Either 'theme' or 'plugin'.
 * @return bool True if auto-updates are enabled for `$type`, false otherwise.
 */
function wp_is_auto_update_enabled_for_type( $type ) {
	if ( ! class_exists( 'WP_Automatic_Updater' ) ) {
		require_once ABSPATH . 'wp-admin/includes/class-wp-automatic-updater.php';
	}

	$updater = new WP_Automatic_Updater();
	$enabled = ! $updater->is_disabled();

	switch ( $type ) {
		case 'plugin':
			/**
			 * Filters whether plugins auto-update is enabled.
			 *
			 * @since 5.5.0
			 *
			 * @param bool $enabled True if plugins auto-update is enabled, false otherwise.
			 */
			return apply_filters( 'plugins_auto_update_enabled', $enabled );
		case 'theme':
			/**
			 * Filters whether themes auto-update is enabled.
			 *
			 * @since 5.5.0
			 *
			 * @param bool $enabled True if themes auto-update is enabled, false otherwise.
			 */
			return apply_filters( 'themes_auto_update_enabled', $enabled );
	}

	return false;
}

/**
 * Checks whether auto-updates are forced for an item.
 *
 * @since 5.6.0
 *
 * @param string    $type   The type of update being checked: Either 'theme' or 'plugin'.
 * @param bool|null $update Whether to update. The value of null is internally used
 *                          to detect whether nothing has hooked into this filter.
 * @param object    $item   The update offer.
 * @return bool True if auto-updates are forced for `$item`, false otherwise.
 */
function wp_is_auto_update_forced_for_item( $type, $update, $item ) {
	/** This filter is documented in wp-admin/includes/class-wp-automatic-updater.php */
	return apply_filters( "auto_update_{$type}", $update, $item );
}

/**
 * Determines the appropriate auto-update message to be displayed.
 *
 * @since 5.5.0
 *
 * @return string The update message to be shown.
 */
function wp_get_auto_update_message() {
	$next_update_time = wp_next_scheduled( 'wp_version_check' );

	// Check if the event exists.
	if ( false === $next_update_time ) {
		$message = __( 'Automatic update not scheduled. There may be a problem with WP-Cron.' );
	} else {
		$time_to_next_update = human_time_diff( (int) $next_update_time );

		// See if cron is overdue.
		$overdue = ( time() - $next_update_time ) > 0;

		if ( $overdue ) {
			$message = sprintf(
				/* translators: %s: Duration that WP-Cron has been overdue. */
				__( 'Automatic update overdue by %s. There may be a problem with WP-Cron.' ),
				$time_to_next_update
			);
		} else {
			$message = sprintf(
				/* translators: %s: Time until the next update. */
				__( 'Automatic update scheduled in %s.' ),
				$time_to_next_update
			);
		}
	}

	return $message;
}
class-wp-ms-themes-list-table.php000064400000067426151327343360012772 0ustar00<?php
/**
 * List Table API: WP_MS_Themes_List_Table class
 *
 * @package WordPress
 * @subpackage Administration
 * @since 3.1.0
 */

/**
 * Core class used to implement displaying themes in a list table for the network admin.
 *
 * @since 3.1.0
 *
 * @see WP_List_Table
 */
class WP_MS_Themes_List_Table extends WP_List_Table {

	public $site_id;
	public $is_site_themes;

	private $has_items;

	/**
	 * Whether to show the auto-updates UI.
	 *
	 * @since 5.5.0
	 *
	 * @var bool True if auto-updates UI is to be shown, false otherwise.
	 */
	protected $show_autoupdates = true;

	/**
	 * Constructor.
	 *
	 * @since 3.1.0
	 *
	 * @see WP_List_Table::__construct() for more information on default arguments.
	 *
	 * @global string $status
	 * @global int    $page
	 *
	 * @param array $args An associative array of arguments.
	 */
	public function __construct( $args = array() ) {
		global $status, $page;

		parent::__construct(
			array(
				'plural' => 'themes',
				'screen' => isset( $args['screen'] ) ? $args['screen'] : null,
			)
		);

		$status = isset( $_REQUEST['theme_status'] ) ? $_REQUEST['theme_status'] : 'all';
		if ( ! in_array( $status, array( 'all', 'enabled', 'disabled', 'upgrade', 'search', 'broken', 'auto-update-enabled', 'auto-update-disabled' ), true ) ) {
			$status = 'all';
		}

		$page = $this->get_pagenum();

		$this->is_site_themes = ( 'site-themes-network' === $this->screen->id ) ? true : false;

		if ( $this->is_site_themes ) {
			$this->site_id = isset( $_REQUEST['id'] ) ? (int) $_REQUEST['id'] : 0;
		}

		$this->show_autoupdates = wp_is_auto_update_enabled_for_type( 'theme' ) &&
			! $this->is_site_themes && current_user_can( 'update_themes' );
	}

	/**
	 * @return array
	 */
	protected function get_table_classes() {
		// @todo Remove and add CSS for .themes.
		return array( 'widefat', 'plugins' );
	}

	/**
	 * @return bool
	 */
	public function ajax_user_can() {
		if ( $this->is_site_themes ) {
			return current_user_can( 'manage_sites' );
		} else {
			return current_user_can( 'manage_network_themes' );
		}
	}

	/**
	 * @global string $status
	 * @global array $totals
	 * @global int $page
	 * @global string $orderby
	 * @global string $order
	 * @global string $s
	 */
	public function prepare_items() {
		global $status, $totals, $page, $orderby, $order, $s;

		$orderby = ! empty( $_REQUEST['orderby'] ) ? sanitize_text_field( $_REQUEST['orderby'] ) : '';
		$order   = ! empty( $_REQUEST['order'] ) ? sanitize_text_field( $_REQUEST['order'] ) : '';
		$s       = ! empty( $_REQUEST['s'] ) ? sanitize_text_field( $_REQUEST['s'] ) : '';

		$themes = array(
			/**
			 * Filters the full array of WP_Theme objects to list in the Multisite
			 * themes list table.
			 *
			 * @since 3.1.0
			 *
			 * @param WP_Theme[] $all Array of WP_Theme objects to display in the list table.
			 */
			'all'      => apply_filters( 'all_themes', wp_get_themes() ),
			'search'   => array(),
			'enabled'  => array(),
			'disabled' => array(),
			'upgrade'  => array(),
			'broken'   => $this->is_site_themes ? array() : wp_get_themes( array( 'errors' => true ) ),
		);

		if ( $this->show_autoupdates ) {
			$auto_updates = (array) get_site_option( 'auto_update_themes', array() );

			$themes['auto-update-enabled']  = array();
			$themes['auto-update-disabled'] = array();
		}

		if ( $this->is_site_themes ) {
			$themes_per_page = $this->get_items_per_page( 'site_themes_network_per_page' );
			$allowed_where   = 'site';
		} else {
			$themes_per_page = $this->get_items_per_page( 'themes_network_per_page' );
			$allowed_where   = 'network';
		}

		$current      = get_site_transient( 'update_themes' );
		$maybe_update = current_user_can( 'update_themes' ) && ! $this->is_site_themes && $current;

		foreach ( (array) $themes['all'] as $key => $theme ) {
			if ( $this->is_site_themes && $theme->is_allowed( 'network' ) ) {
				unset( $themes['all'][ $key ] );
				continue;
			}

			if ( $maybe_update && isset( $current->response[ $key ] ) ) {
				$themes['all'][ $key ]->update = true;
				$themes['upgrade'][ $key ]     = $themes['all'][ $key ];
			}

			$filter                    = $theme->is_allowed( $allowed_where, $this->site_id ) ? 'enabled' : 'disabled';
			$themes[ $filter ][ $key ] = $themes['all'][ $key ];

			$theme_data = array(
				'update_supported' => isset( $theme->update_supported ) ? $theme->update_supported : true,
			);

			// Extra info if known. array_merge() ensures $theme_data has precedence if keys collide.
			if ( isset( $current->response[ $key ] ) ) {
				$theme_data = array_merge( (array) $current->response[ $key ], $theme_data );
			} elseif ( isset( $current->no_update[ $key ] ) ) {
				$theme_data = array_merge( (array) $current->no_update[ $key ], $theme_data );
			} else {
				$theme_data['update_supported'] = false;
			}

			$theme->update_supported = $theme_data['update_supported'];

			/*
			 * Create the expected payload for the auto_update_theme filter, this is the same data
			 * as contained within $updates or $no_updates but used when the Theme is not known.
			 */
			$filter_payload = array(
				'theme'        => $key,
				'new_version'  => '',
				'url'          => '',
				'package'      => '',
				'requires'     => '',
				'requires_php' => '',
			);

			$filter_payload = (object) array_merge( $filter_payload, array_intersect_key( $theme_data, $filter_payload ) );

			$auto_update_forced = wp_is_auto_update_forced_for_item( 'theme', null, $filter_payload );

			if ( ! is_null( $auto_update_forced ) ) {
				$theme->auto_update_forced = $auto_update_forced;
			}

			if ( $this->show_autoupdates ) {
				$enabled = in_array( $key, $auto_updates, true ) && $theme->update_supported;
				if ( isset( $theme->auto_update_forced ) ) {
					$enabled = (bool) $theme->auto_update_forced;
				}

				if ( $enabled ) {
					$themes['auto-update-enabled'][ $key ] = $theme;
				} else {
					$themes['auto-update-disabled'][ $key ] = $theme;
				}
			}
		}

		if ( $s ) {
			$status           = 'search';
			$themes['search'] = array_filter( array_merge( $themes['all'], $themes['broken'] ), array( $this, '_search_callback' ) );
		}

		$totals    = array();
		$js_themes = array();
		foreach ( $themes as $type => $list ) {
			$totals[ $type ]    = count( $list );
			$js_themes[ $type ] = array_keys( $list );
		}

		if ( empty( $themes[ $status ] ) && ! in_array( $status, array( 'all', 'search' ), true ) ) {
			$status = 'all';
		}

		$this->items = $themes[ $status ];
		WP_Theme::sort_by_name( $this->items );

		$this->has_items = ! empty( $themes['all'] );
		$total_this_page = $totals[ $status ];

		wp_localize_script(
			'updates',
			'_wpUpdatesItemCounts',
			array(
				'themes' => $js_themes,
				'totals' => wp_get_update_data(),
			)
		);

		if ( $orderby ) {
			$orderby = ucfirst( $orderby );
			$order   = strtoupper( $order );

			if ( 'Name' === $orderby ) {
				if ( 'ASC' === $order ) {
					$this->items = array_reverse( $this->items );
				}
			} else {
				uasort( $this->items, array( $this, '_order_callback' ) );
			}
		}

		$start = ( $page - 1 ) * $themes_per_page;

		if ( $total_this_page > $themes_per_page ) {
			$this->items = array_slice( $this->items, $start, $themes_per_page, true );
		}

		$this->set_pagination_args(
			array(
				'total_items' => $total_this_page,
				'per_page'    => $themes_per_page,
			)
		);
	}

	/**
	 * @param WP_Theme $theme
	 * @return bool
	 */
	public function _search_callback( $theme ) {
		static $term = null;
		if ( is_null( $term ) ) {
			$term = wp_unslash( $_REQUEST['s'] );
		}

		foreach ( array( 'Name', 'Description', 'Author', 'Author', 'AuthorURI' ) as $field ) {
			// Don't mark up; Do translate.
			if ( false !== stripos( $theme->display( $field, false, true ), $term ) ) {
				return true;
			}
		}

		if ( false !== stripos( $theme->get_stylesheet(), $term ) ) {
			return true;
		}

		if ( false !== stripos( $theme->get_template(), $term ) ) {
			return true;
		}

		return false;
	}

	// Not used by any core columns.
	/**
	 * @global string $orderby
	 * @global string $order
	 * @param array $theme_a
	 * @param array $theme_b
	 * @return int
	 */
	public function _order_callback( $theme_a, $theme_b ) {
		global $orderby, $order;

		$a = $theme_a[ $orderby ];
		$b = $theme_b[ $orderby ];

		if ( $a === $b ) {
			return 0;
		}

		if ( 'DESC' === $order ) {
			return ( $a < $b ) ? 1 : -1;
		} else {
			return ( $a < $b ) ? -1 : 1;
		}
	}

	/**
	 */
	public function no_items() {
		if ( $this->has_items ) {
			_e( 'No themes found.' );
		} else {
			_e( 'No themes are currently available.' );
		}
	}

	/**
	 * @return string[] Array of column titles keyed by their column name.
	 */
	public function get_columns() {
		$columns = array(
			'cb'          => '<input type="checkbox" />',
			'name'        => __( 'Theme' ),
			'description' => __( 'Description' ),
		);

		if ( $this->show_autoupdates ) {
			$columns['auto-updates'] = __( 'Automatic Updates' );
		}

		return $columns;
	}

	/**
	 * @return array
	 */
	protected function get_sortable_columns() {
		return array(
			'name' => array( 'name', false, __( 'Theme' ), __( 'Table ordered by Theme Name.' ), 'asc' ),
		);
	}

	/**
	 * Gets the name of the primary column.
	 *
	 * @since 4.3.0
	 *
	 * @return string Unalterable name of the primary column name, in this case, 'name'.
	 */
	protected function get_primary_column_name() {
		return 'name';
	}

	/**
	 * @global array $totals
	 * @global string $status
	 * @return array
	 */
	protected function get_views() {
		global $totals, $status;

		$status_links = array();
		foreach ( $totals as $type => $count ) {
			if ( ! $count ) {
				continue;
			}

			switch ( $type ) {
				case 'all':
					/* translators: %s: Number of themes. */
					$text = _nx(
						'All <span class="count">(%s)</span>',
						'All <span class="count">(%s)</span>',
						$count,
						'themes'
					);
					break;
				case 'enabled':
					/* translators: %s: Number of themes. */
					$text = _nx(
						'Enabled <span class="count">(%s)</span>',
						'Enabled <span class="count">(%s)</span>',
						$count,
						'themes'
					);
					break;
				case 'disabled':
					/* translators: %s: Number of themes. */
					$text = _nx(
						'Disabled <span class="count">(%s)</span>',
						'Disabled <span class="count">(%s)</span>',
						$count,
						'themes'
					);
					break;
				case 'upgrade':
					/* translators: %s: Number of themes. */
					$text = _nx(
						'Update Available <span class="count">(%s)</span>',
						'Update Available <span class="count">(%s)</span>',
						$count,
						'themes'
					);
					break;
				case 'broken':
					/* translators: %s: Number of themes. */
					$text = _nx(
						'Broken <span class="count">(%s)</span>',
						'Broken <span class="count">(%s)</span>',
						$count,
						'themes'
					);
					break;
				case 'auto-update-enabled':
					/* translators: %s: Number of themes. */
					$text = _n(
						'Auto-updates Enabled <span class="count">(%s)</span>',
						'Auto-updates Enabled <span class="count">(%s)</span>',
						$count
					);
					break;
				case 'auto-update-disabled':
					/* translators: %s: Number of themes. */
					$text = _n(
						'Auto-updates Disabled <span class="count">(%s)</span>',
						'Auto-updates Disabled <span class="count">(%s)</span>',
						$count
					);
					break;
			}

			if ( $this->is_site_themes ) {
				$url = 'site-themes.php?id=' . $this->site_id;
			} else {
				$url = 'themes.php';
			}

			if ( 'search' !== $type ) {
				$status_links[ $type ] = array(
					'url'     => esc_url( add_query_arg( 'theme_status', $type, $url ) ),
					'label'   => sprintf( $text, number_format_i18n( $count ) ),
					'current' => $type === $status,
				);
			}
		}

		return $this->get_views_links( $status_links );
	}

	/**
	 * @global string $status
	 *
	 * @return array
	 */
	protected function get_bulk_actions() {
		global $status;

		$actions = array();
		if ( 'enabled' !== $status ) {
			$actions['enable-selected'] = $this->is_site_themes ? __( 'Enable' ) : __( 'Network Enable' );
		}
		if ( 'disabled' !== $status ) {
			$actions['disable-selected'] = $this->is_site_themes ? __( 'Disable' ) : __( 'Network Disable' );
		}
		if ( ! $this->is_site_themes ) {
			if ( current_user_can( 'update_themes' ) ) {
				$actions['update-selected'] = __( 'Update' );
			}
			if ( current_user_can( 'delete_themes' ) ) {
				$actions['delete-selected'] = __( 'Delete' );
			}
		}

		if ( $this->show_autoupdates ) {
			if ( 'auto-update-enabled' !== $status ) {
				$actions['enable-auto-update-selected'] = __( 'Enable Auto-updates' );
			}

			if ( 'auto-update-disabled' !== $status ) {
				$actions['disable-auto-update-selected'] = __( 'Disable Auto-updates' );
			}
		}

		return $actions;
	}

	/**
	 * Generates the list table rows.
	 *
	 * @since 3.1.0
	 */
	public function display_rows() {
		foreach ( $this->items as $theme ) {
			$this->single_row( $theme );
		}
	}

	/**
	 * Handles the checkbox column output.
	 *
	 * @since 4.3.0
	 * @since 5.9.0 Renamed `$theme` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param WP_Theme $item The current WP_Theme object.
	 */
	public function column_cb( $item ) {
		// Restores the more descriptive, specific name for use within this method.
		$theme = $item;

		$checkbox_id = 'checkbox_' . md5( $theme->get( 'Name' ) );
		?>
		<input type="checkbox" name="checked[]" value="<?php echo esc_attr( $theme->get_stylesheet() ); ?>" id="<?php echo $checkbox_id; ?>" />
		<label for="<?php echo $checkbox_id; ?>" >
			<span class="screen-reader-text">
			<?php
			printf(
				/* translators: Hidden accessibility text. %s: Theme name */
				__( 'Select %s' ),
				$theme->display( 'Name' )
			);
			?>
			</span>
		</label>
		<?php
	}

	/**
	 * Handles the name column output.
	 *
	 * @since 4.3.0
	 *
	 * @global string $status
	 * @global int    $page
	 * @global string $s
	 *
	 * @param WP_Theme $theme The current WP_Theme object.
	 */
	public function column_name( $theme ) {
		global $status, $page, $s;

		$context = $status;

		if ( $this->is_site_themes ) {
			$url     = "site-themes.php?id={$this->site_id}&amp;";
			$allowed = $theme->is_allowed( 'site', $this->site_id );
		} else {
			$url     = 'themes.php?';
			$allowed = $theme->is_allowed( 'network' );
		}

		// Pre-order.
		$actions = array(
			'enable'  => '',
			'disable' => '',
			'delete'  => '',
		);

		$stylesheet = $theme->get_stylesheet();
		$theme_key  = urlencode( $stylesheet );

		if ( ! $allowed ) {
			if ( ! $theme->errors() ) {
				$url = add_query_arg(
					array(
						'action' => 'enable',
						'theme'  => $theme_key,
						'paged'  => $page,
						's'      => $s,
					),
					$url
				);

				if ( $this->is_site_themes ) {
					/* translators: %s: Theme name. */
					$aria_label = sprintf( __( 'Enable %s' ), $theme->display( 'Name' ) );
				} else {
					/* translators: %s: Theme name. */
					$aria_label = sprintf( __( 'Network Enable %s' ), $theme->display( 'Name' ) );
				}

				$actions['enable'] = sprintf(
					'<a href="%s" class="edit" aria-label="%s">%s</a>',
					esc_url( wp_nonce_url( $url, 'enable-theme_' . $stylesheet ) ),
					esc_attr( $aria_label ),
					( $this->is_site_themes ? __( 'Enable' ) : __( 'Network Enable' ) )
				);
			}
		} else {
			$url = add_query_arg(
				array(
					'action' => 'disable',
					'theme'  => $theme_key,
					'paged'  => $page,
					's'      => $s,
				),
				$url
			);

			if ( $this->is_site_themes ) {
				/* translators: %s: Theme name. */
				$aria_label = sprintf( __( 'Disable %s' ), $theme->display( 'Name' ) );
			} else {
				/* translators: %s: Theme name. */
				$aria_label = sprintf( __( 'Network Disable %s' ), $theme->display( 'Name' ) );
			}

			$actions['disable'] = sprintf(
				'<a href="%s" aria-label="%s">%s</a>',
				esc_url( wp_nonce_url( $url, 'disable-theme_' . $stylesheet ) ),
				esc_attr( $aria_label ),
				( $this->is_site_themes ? __( 'Disable' ) : __( 'Network Disable' ) )
			);
		}

		if ( ! $allowed && ! $this->is_site_themes
			&& current_user_can( 'delete_themes' )
			&& get_option( 'stylesheet' ) !== $stylesheet
			&& get_option( 'template' ) !== $stylesheet
		) {
			$url = add_query_arg(
				array(
					'action'       => 'delete-selected',
					'checked[]'    => $theme_key,
					'theme_status' => $context,
					'paged'        => $page,
					's'            => $s,
				),
				'themes.php'
			);

			/* translators: %s: Theme name. */
			$aria_label = sprintf( _x( 'Delete %s', 'theme' ), $theme->display( 'Name' ) );

			$actions['delete'] = sprintf(
				'<a href="%s" class="delete" aria-label="%s">%s</a>',
				esc_url( wp_nonce_url( $url, 'bulk-themes' ) ),
				esc_attr( $aria_label ),
				__( 'Delete' )
			);
		}
		/**
		 * Filters the action links displayed for each theme in the Multisite
		 * themes list table.
		 *
		 * The action links displayed are determined by the theme's status, and
		 * which Multisite themes list table is being displayed - the Network
		 * themes list table (themes.php), which displays all installed themes,
		 * or the Site themes list table (site-themes.php), which displays the
		 * non-network enabled themes when editing a site in the Network admin.
		 *
		 * The default action links for the Network themes list table include
		 * 'Network Enable', 'Network Disable', and 'Delete'.
		 *
		 * The default action links for the Site themes list table include
		 * 'Enable', and 'Disable'.
		 *
		 * @since 2.8.0
		 *
		 * @param string[] $actions An array of action links.
		 * @param WP_Theme $theme   The current WP_Theme object.
		 * @param string   $context Status of the theme, one of 'all', 'enabled', or 'disabled'.
		 */
		$actions = apply_filters( 'theme_action_links', array_filter( $actions ), $theme, $context );

		/**
		 * Filters the action links of a specific theme in the Multisite themes
		 * list table.
		 *
		 * The dynamic portion of the hook name, `$stylesheet`, refers to the
		 * directory name of the theme, which in most cases is synonymous
		 * with the template name.
		 *
		 * @since 3.1.0
		 *
		 * @param string[] $actions An array of action links.
		 * @param WP_Theme $theme   The current WP_Theme object.
		 * @param string   $context Status of the theme, one of 'all', 'enabled', or 'disabled'.
		 */
		$actions = apply_filters( "theme_action_links_{$stylesheet}", $actions, $theme, $context );

		echo $this->row_actions( $actions, true );
	}

	/**
	 * Handles the description column output.
	 *
	 * @since 4.3.0
	 *
	 * @global string $status
	 * @global array  $totals
	 *
	 * @param WP_Theme $theme The current WP_Theme object.
	 */
	public function column_description( $theme ) {
		global $status, $totals;

		if ( $theme->errors() ) {
			$pre = 'broken' === $status ? '<strong class="error-message">' . __( 'Broken Theme:' ) . '</strong> ' : '';
			wp_admin_notice(
				$pre . $theme->errors()->get_error_message(),
				array(
					'type'               => 'error',
					'additional_classes' => 'inline',
				)
			);
		}

		if ( $this->is_site_themes ) {
			$allowed = $theme->is_allowed( 'site', $this->site_id );
		} else {
			$allowed = $theme->is_allowed( 'network' );
		}

		$class = ! $allowed ? 'inactive' : 'active';
		if ( ! empty( $totals['upgrade'] ) && ! empty( $theme->update ) ) {
			$class .= ' update';
		}

		echo "<div class='theme-description'><p>" . $theme->display( 'Description' ) . "</p></div>
			<div class='$class second theme-version-author-uri'>";

		$stylesheet = $theme->get_stylesheet();
		$theme_meta = array();

		if ( $theme->get( 'Version' ) ) {
			/* translators: %s: Theme version. */
			$theme_meta[] = sprintf( __( 'Version %s' ), $theme->display( 'Version' ) );
		}

		/* translators: %s: Theme author. */
		$theme_meta[] = sprintf( __( 'By %s' ), $theme->display( 'Author' ) );

		if ( $theme->get( 'ThemeURI' ) ) {
			/* translators: %s: Theme name. */
			$aria_label = sprintf( __( 'Visit theme site for %s' ), $theme->display( 'Name' ) );

			$theme_meta[] = sprintf(
				'<a href="%s" aria-label="%s">%s</a>',
				$theme->display( 'ThemeURI' ),
				esc_attr( $aria_label ),
				__( 'Visit Theme Site' )
			);
		}

		if ( $theme->parent() ) {
			$theme_meta[] = sprintf(
				/* translators: %s: Theme name. */
				__( 'Child theme of %s' ),
				'<strong>' . $theme->parent()->display( 'Name' ) . '</strong>'
			);
		}

		/**
		 * Filters the array of row meta for each theme in the Multisite themes
		 * list table.
		 *
		 * @since 3.1.0
		 *
		 * @param string[] $theme_meta An array of the theme's metadata, including
		 *                             the version, author, and theme URI.
		 * @param string   $stylesheet Directory name of the theme.
		 * @param WP_Theme $theme      WP_Theme object.
		 * @param string   $status     Status of the theme.
		 */
		$theme_meta = apply_filters( 'theme_row_meta', $theme_meta, $stylesheet, $theme, $status );

		echo implode( ' | ', $theme_meta );

		echo '</div>';
	}

	/**
	 * Handles the auto-updates column output.
	 *
	 * @since 5.5.0
	 *
	 * @global string $status
	 * @global int  $page
	 *
	 * @param WP_Theme $theme The current WP_Theme object.
	 */
	public function column_autoupdates( $theme ) {
		global $status, $page;

		static $auto_updates, $available_updates;

		if ( ! $auto_updates ) {
			$auto_updates = (array) get_site_option( 'auto_update_themes', array() );
		}
		if ( ! $available_updates ) {
			$available_updates = get_site_transient( 'update_themes' );
		}

		$stylesheet = $theme->get_stylesheet();

		if ( isset( $theme->auto_update_forced ) ) {
			if ( $theme->auto_update_forced ) {
				// Forced on.
				$text = __( 'Auto-updates enabled' );
			} else {
				$text = __( 'Auto-updates disabled' );
			}
			$action     = 'unavailable';
			$time_class = ' hidden';
		} elseif ( empty( $theme->update_supported ) ) {
			$text       = '';
			$action     = 'unavailable';
			$time_class = ' hidden';
		} elseif ( in_array( $stylesheet, $auto_updates, true ) ) {
			$text       = __( 'Disable auto-updates' );
			$action     = 'disable';
			$time_class = '';
		} else {
			$text       = __( 'Enable auto-updates' );
			$action     = 'enable';
			$time_class = ' hidden';
		}

		$query_args = array(
			'action'       => "{$action}-auto-update",
			'theme'        => $stylesheet,
			'paged'        => $page,
			'theme_status' => $status,
		);

		$url = add_query_arg( $query_args, 'themes.php' );

		if ( 'unavailable' === $action ) {
			$html[] = '<span class="label">' . $text . '</span>';
		} else {
			$html[] = sprintf(
				'<a href="%s" class="toggle-auto-update aria-button-if-js" data-wp-action="%s">',
				wp_nonce_url( $url, 'updates' ),
				$action
			);

			$html[] = '<span class="dashicons dashicons-update spin hidden" aria-hidden="true"></span>';
			$html[] = '<span class="label">' . $text . '</span>';
			$html[] = '</a>';

		}

		if ( isset( $available_updates->response[ $stylesheet ] ) ) {
			$html[] = sprintf(
				'<div class="auto-update-time%s">%s</div>',
				$time_class,
				wp_get_auto_update_message()
			);
		}

		$html = implode( '', $html );

		/**
		 * Filters the HTML of the auto-updates setting for each theme in the Themes list table.
		 *
		 * @since 5.5.0
		 *
		 * @param string   $html       The HTML for theme's auto-update setting, including
		 *                             toggle auto-update action link and time to next update.
		 * @param string   $stylesheet Directory name of the theme.
		 * @param WP_Theme $theme      WP_Theme object.
		 */
		echo apply_filters( 'theme_auto_update_setting_html', $html, $stylesheet, $theme );

		wp_admin_notice(
			'',
			array(
				'type'               => 'error',
				'additional_classes' => array( 'notice-alt', 'inline', 'hidden' ),
			)
		);
	}

	/**
	 * Handles default column output.
	 *
	 * @since 4.3.0
	 * @since 5.9.0 Renamed `$theme` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param WP_Theme $item        The current WP_Theme object.
	 * @param string   $column_name The current column name.
	 */
	public function column_default( $item, $column_name ) {
		// Restores the more descriptive, specific name for use within this method.
		$theme = $item;

		$stylesheet = $theme->get_stylesheet();

		/**
		 * Fires inside each custom column of the Multisite themes list table.
		 *
		 * @since 3.1.0
		 *
		 * @param string   $column_name Name of the column.
		 * @param string   $stylesheet  Directory name of the theme.
		 * @param WP_Theme $theme       Current WP_Theme object.
		 */
		do_action( 'manage_themes_custom_column', $column_name, $stylesheet, $theme );
	}

	/**
	 * Handles the output for a single table row.
	 *
	 * @since 4.3.0
	 *
	 * @param WP_Theme $item The current WP_Theme object.
	 */
	public function single_row_columns( $item ) {
		list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info();

		foreach ( $columns as $column_name => $column_display_name ) {
			$extra_classes = '';
			if ( in_array( $column_name, $hidden, true ) ) {
				$extra_classes .= ' hidden';
			}

			switch ( $column_name ) {
				case 'cb':
					echo '<th scope="row" class="check-column">';

					$this->column_cb( $item );

					echo '</th>';
					break;

				case 'name':
					$active_theme_label = '';

					/* The presence of the site_id property means that this is a subsite view and a label for the active theme needs to be added */
					if ( ! empty( $this->site_id ) ) {
						$stylesheet = get_blog_option( $this->site_id, 'stylesheet' );
						$template   = get_blog_option( $this->site_id, 'template' );

						/* Add a label for the active template */
						if ( $item->get_template() === $template ) {
							$active_theme_label = ' &mdash; ' . __( 'Active Theme' );
						}

						/* In case this is a child theme, label it properly */
						if ( $stylesheet !== $template && $item->get_stylesheet() === $stylesheet ) {
							$active_theme_label = ' &mdash; ' . __( 'Active Child Theme' );
						}
					}

					echo "<td class='theme-title column-primary{$extra_classes}'><strong>" . $item->display( 'Name' ) . $active_theme_label . '</strong>';

					$this->column_name( $item );

					echo '</td>';
					break;

				case 'description':
					echo "<td class='column-description desc{$extra_classes}'>";

					$this->column_description( $item );

					echo '</td>';
					break;

				case 'auto-updates':
					echo "<td class='column-auto-updates{$extra_classes}'>";

					$this->column_autoupdates( $item );

					echo '</td>';
					break;
				default:
					echo "<td class='$column_name column-$column_name{$extra_classes}'>";

					$this->column_default( $item, $column_name );

					echo '</td>';
					break;
			}
		}
	}

	/**
	 * @global string $status
	 * @global array  $totals
	 *
	 * @param WP_Theme $theme
	 */
	public function single_row( $theme ) {
		global $status, $totals;

		if ( $this->is_site_themes ) {
			$allowed = $theme->is_allowed( 'site', $this->site_id );
		} else {
			$allowed = $theme->is_allowed( 'network' );
		}

		$stylesheet = $theme->get_stylesheet();

		$class = ! $allowed ? 'inactive' : 'active';
		if ( ! empty( $totals['upgrade'] ) && ! empty( $theme->update ) ) {
			$class .= ' update';
		}

		printf(
			'<tr class="%s" data-slug="%s">',
			esc_attr( $class ),
			esc_attr( $stylesheet )
		);

		$this->single_row_columns( $theme );

		echo '</tr>';

		if ( $this->is_site_themes ) {
			remove_action( "after_theme_row_$stylesheet", 'wp_theme_update_row' );
		}

		/**
		 * Fires after each row in the Multisite themes list table.
		 *
		 * @since 3.1.0
		 *
		 * @param string   $stylesheet Directory name of the theme.
		 * @param WP_Theme $theme      Current WP_Theme object.
		 * @param string   $status     Status of the theme.
		 */
		do_action( 'after_theme_row', $stylesheet, $theme, $status );

		/**
		 * Fires after each specific row in the Multisite themes list table.
		 *
		 * The dynamic portion of the hook name, `$stylesheet`, refers to the
		 * directory name of the theme, most often synonymous with the template
		 * name of the theme.
		 *
		 * @since 3.5.0
		 *
		 * @param string   $stylesheet Directory name of the theme.
		 * @param WP_Theme $theme      Current WP_Theme object.
		 * @param string   $status     Status of the theme.
		 */
		do_action( "after_theme_row_{$stylesheet}", $stylesheet, $theme, $status );
	}
}
class-wp-debug-data.php000064400000202073151327343360011014 0ustar00<?php
/**
 * Class for providing debug data based on a users WordPress environment.
 *
 * @package WordPress
 * @subpackage Site_Health
 * @since 5.2.0
 */

#[AllowDynamicProperties]
class WP_Debug_Data {
	/**
	 * Calls all core functions to check for updates.
	 *
	 * @since 5.2.0
	 */
	public static function check_for_updates() {
		wp_version_check();
		wp_update_plugins();
		wp_update_themes();
	}

	/**
	 * Static function for generating site debug data when required.
	 *
	 * @since 5.2.0
	 * @since 5.3.0 Added database charset, database collation,
	 *              and timezone information.
	 * @since 5.5.0 Added pretty permalinks support information.
	 * @since 6.7.0 Modularized into separate theme-oriented methods.
	 *
	 * @throws ImagickException
	 *
	 * @return array The debug data for the site.
	 */
	public static function debug_data() {
		/*
		 * Set up the array that holds all debug information.
		 *
		 * When iterating through the debug data, the ordering of the sections
		 * occurs in insertion-order of the assignments into this array.
		 *
		 * This is the single assignment of the sections before filtering. Null-entries will
		 * be automatically be removed.
		 */
		$info = array(
			'wp-core'             => self::get_wp_core(),
			'wp-paths-sizes'      => self::get_wp_paths_sizes(),
			'wp-dropins'          => self::get_wp_dropins(),
			'wp-active-theme'     => self::get_wp_active_theme(),
			'wp-parent-theme'     => self::get_wp_parent_theme(),
			'wp-themes-inactive'  => self::get_wp_themes_inactive(),
			'wp-mu-plugins'       => self::get_wp_mu_plugins(),
			'wp-plugins-active'   => self::get_wp_plugins_active(),
			'wp-plugins-inactive' => self::get_wp_plugins_inactive(),
			'wp-media'            => self::get_wp_media(),
			'wp-server'           => self::get_wp_server(),
			'wp-database'         => self::get_wp_database(),
			'wp-constants'        => self::get_wp_constants(),
			'wp-filesystem'       => self::get_wp_filesystem(),
		);

		/*
		 * Remove null elements from the array. The individual methods are
		 * allowed to return `null`, which communicates that the category
		 * of debug data isn't relevant and shouldn't be passed through.
		 */
		$info = array_filter(
			$info,
			static function ( $section ) {
				return isset( $section );
			}
		);

		/**
		 * Filters the debug information shown on the Tools -> Site Health -> Info screen.
		 *
		 * Plugin or themes may wish to introduce their own debug information without creating
		 * additional admin pages. They can utilize this filter to introduce their own sections
		 * or add more data to existing sections.
		 *
		 * Array keys for sections added by core are all prefixed with `wp-`. Plugins and themes
		 * should use their own slug as a prefix, both for consistency as well as avoiding
		 * key collisions. Note that the array keys are used as labels for the copied data.
		 *
		 * All strings are expected to be plain text except `$description` that can contain
		 * inline HTML tags (see below).
		 *
		 * @since 5.2.0
		 *
		 * @param array $args {
		 *     The debug information to be added to the core information page.
		 *
		 *     This is an associative multi-dimensional array, up to three levels deep.
		 *     The topmost array holds the sections, keyed by section ID.
		 *
		 *     @type array ...$0 {
		 *         Each section has a `$fields` associative array (see below), and each `$value` in `$fields`
		 *         can be another associative array of name/value pairs when there is more structured data
		 *         to display.
		 *
		 *         @type string $label       Required. The title for this section of the debug output.
		 *         @type string $description Optional. A description for your information section which
		 *                                   may contain basic HTML markup, inline tags only as it is
		 *                                   outputted in a paragraph.
		 *         @type bool   $show_count  Optional. If set to `true`, the amount of fields will be included
		 *                                   in the title for this section. Default false.
		 *         @type bool   $private     Optional. If set to `true`, the section and all associated fields
		 *                                   will be excluded from the copied data. Default false.
		 *         @type array  $fields {
		 *             Required. An associative array containing the fields to be displayed in the section,
		 *             keyed by field ID.
		 *
		 *             @type array ...$0 {
		 *                 An associative array containing the data to be displayed for the field.
		 *
		 *                 @type string $label    Required. The label for this piece of information.
		 *                 @type mixed  $value    Required. The output that is displayed for this field.
		 *                                        Text should be translated. Can be an associative array
		 *                                        that is displayed as name/value pairs.
		 *                                        Accepted types: `string|int|float|(string|int|float)[]`.
		 *                 @type string $debug    Optional. The output that is used for this field when
		 *                                        the user copies the data. It should be more concise and
		 *                                        not translated. If not set, the content of `$value`
		 *                                        is used. Note that the array keys are used as labels
		 *                                        for the copied data.
		 *                 @type bool   $private  Optional. If set to `true`, the field will be excluded
		 *                                        from the copied data, allowing you to show, for example,
		 *                                        API keys here. Default false.
		 *             }
		 *         }
		 *     }
		 * }
		 */
		$info = apply_filters( 'debug_information', $info );

		return $info;
	}

	/**
	 * Gets the WordPress core section of the debug data.
	 *
	 * @since 6.7.0
	 *
	 * @return array
	 */
	private static function get_wp_core(): array {
		// Save few function calls.
		$permalink_structure    = get_option( 'permalink_structure' );
		$is_ssl                 = is_ssl();
		$users_can_register     = get_option( 'users_can_register' );
		$blog_public            = get_option( 'blog_public' );
		$default_comment_status = get_option( 'default_comment_status' );
		$environment_type       = wp_get_environment_type();
		$core_version           = wp_get_wp_version();
		$core_updates           = get_core_updates();
		$core_update_needed     = '';

		if ( is_array( $core_updates ) ) {
			foreach ( $core_updates as $core => $update ) {
				if ( 'upgrade' === $update->response ) {
					/* translators: %s: Latest WordPress version number. */
					$core_update_needed = ' ' . sprintf( __( '(Latest version: %s)' ), $update->version );
				} else {
					$core_update_needed = '';
				}
			}
		}

		$fields = array(
			'version'                => array(
				'label' => __( 'Version' ),
				'value' => $core_version . $core_update_needed,
				'debug' => $core_version,
			),
			'site_language'          => array(
				'label' => __( 'Site Language' ),
				'value' => get_locale(),
			),
			'user_language'          => array(
				'label' => __( 'User Language' ),
				'value' => get_user_locale(),
			),
			'timezone'               => array(
				'label' => __( 'Timezone' ),
				'value' => wp_timezone_string(),
			),
			'home_url'               => array(
				'label'   => __( 'Home URL' ),
				'value'   => get_bloginfo( 'url' ),
				'private' => true,
			),
			'site_url'               => array(
				'label'   => __( 'Site URL' ),
				'value'   => get_bloginfo( 'wpurl' ),
				'private' => true,
			),
			'permalink'              => array(
				'label' => __( 'Permalink structure' ),
				'value' => $permalink_structure ? $permalink_structure : __( 'No permalink structure set' ),
				'debug' => $permalink_structure,
			),
			'https_status'           => array(
				'label' => __( 'Is this site using HTTPS?' ),
				'value' => $is_ssl ? __( 'Yes' ) : __( 'No' ),
				'debug' => $is_ssl,
			),
			'multisite'              => array(
				'label' => __( 'Is this a multisite?' ),
				'value' => is_multisite() ? __( 'Yes' ) : __( 'No' ),
				'debug' => is_multisite(),
			),
			'user_registration'      => array(
				'label' => __( 'Can anyone register on this site?' ),
				'value' => $users_can_register ? __( 'Yes' ) : __( 'No' ),
				'debug' => $users_can_register,
			),
			'blog_public'            => array(
				'label' => __( 'Is this site discouraging search engines?' ),
				'value' => $blog_public ? __( 'No' ) : __( 'Yes' ),
				'debug' => $blog_public,
			),
			'default_comment_status' => array(
				'label' => __( 'Default comment status' ),
				'value' => 'open' === $default_comment_status ? _x( 'Open', 'comment status' ) : _x( 'Closed', 'comment status' ),
				'debug' => $default_comment_status,
			),
			'environment_type'       => array(
				'label' => __( 'Environment type' ),
				'value' => $environment_type,
				'debug' => $environment_type,
			),
		);

		// Conditionally add debug information for multisite setups.
		if ( is_multisite() ) {
			$site_id = get_current_blog_id();

			$fields['site_id'] = array(
				'label' => __( 'Site ID' ),
				'value' => $site_id,
				'debug' => $site_id,
			);

			$network_query = new WP_Network_Query();
			$network_ids   = $network_query->query(
				array(
					'fields'        => 'ids',
					'number'        => 100,
					'no_found_rows' => false,
				)
			);

			$site_count = 0;
			foreach ( $network_ids as $network_id ) {
				$site_count += get_blog_count( $network_id );
			}

			$fields['site_count'] = array(
				'label' => __( 'Site count' ),
				'value' => $site_count,
			);

			$fields['network_count'] = array(
				'label' => __( 'Network count' ),
				'value' => $network_query->found_networks,
			);
		}

		$fields['user_count'] = array(
			'label' => __( 'User count' ),
			'value' => get_user_count(),
		);

		// WordPress features requiring processing.
		$wp_dotorg = wp_remote_get( 'https://wordpress.org', array( 'timeout' => 10 ) );

		if ( ! is_wp_error( $wp_dotorg ) ) {
			$fields['dotorg_communication'] = array(
				'label' => __( 'Communication with WordPress.org' ),
				'value' => __( 'WordPress.org is reachable' ),
				'debug' => 'true',
			);
		} else {
			$fields['dotorg_communication'] = array(
				'label' => __( 'Communication with WordPress.org' ),
				'value' => sprintf(
				/* translators: 1: The IP address WordPress.org resolves to. 2: The error returned by the lookup. */
					__( 'Unable to reach WordPress.org at %1$s: %2$s' ),
					gethostbyname( 'wordpress.org' ),
					$wp_dotorg->get_error_message()
				),
				'debug' => $wp_dotorg->get_error_message(),
			);
		}

		return array(
			'label'  => __( 'WordPress' ),
			'fields' => $fields,
		);
	}

	/**
	 * Gets the WordPress drop-in section of the debug data.
	 *
	 * @since 6.7.0
	 *
	 * @return array
	 */
	private static function get_wp_dropins(): array {
		// Get a list of all drop-in replacements.
		$dropins = get_dropins();

		// Get drop-ins descriptions.
		$dropin_descriptions = _get_dropins();

		$fields = array();
		foreach ( $dropins as $dropin_key => $dropin ) {
			$fields[ sanitize_text_field( $dropin_key ) ] = array(
				'label' => $dropin_key,
				'value' => $dropin_descriptions[ $dropin_key ][0],
				'debug' => 'true',
			);
		}

		return array(
			'label'       => __( 'Drop-ins' ),
			'show_count'  => true,
			'description' => sprintf(
				/* translators: %s: wp-content directory name. */
				__( 'Drop-ins are single files, found in the %s directory, that replace or enhance WordPress features in ways that are not possible for traditional plugins.' ),
				'<code>' . str_replace( ABSPATH, '', WP_CONTENT_DIR ) . '</code>'
			),
			'fields'      => $fields,
		);
	}

	/**
	 * Gets the WordPress server section of the debug data.
	 *
	 * @since 6.7.0
	 *
	 * @return array
	 */
	private static function get_wp_server(): array {
		// Populate the server debug fields.
		if ( function_exists( 'php_uname' ) ) {
			$server_architecture = sprintf( '%s %s %s', php_uname( 's' ), php_uname( 'r' ), php_uname( 'm' ) );
		} else {
			$server_architecture = 'unknown';
		}

		$php_version_debug = PHP_VERSION;
		// Whether PHP supports 64-bit.
		$php64bit = ( PHP_INT_SIZE * 8 === 64 );

		$php_version = sprintf(
			'%s %s',
			$php_version_debug,
			( $php64bit ? __( '(Supports 64bit values)' ) : __( '(Does not support 64bit values)' ) )
		);

		if ( $php64bit ) {
			$php_version_debug .= ' 64bit';
		}

		$fields = array();

		$fields['server_architecture'] = array(
			'label' => __( 'Server architecture' ),
			'value' => ( 'unknown' !== $server_architecture ? $server_architecture : __( 'Unable to determine server architecture' ) ),
			'debug' => $server_architecture,
		);
		$fields['httpd_software']      = array(
			'label' => __( 'Web server' ),
			'value' => ( isset( $_SERVER['SERVER_SOFTWARE'] ) ? $_SERVER['SERVER_SOFTWARE'] : __( 'Unable to determine what web server software is used' ) ),
			'debug' => ( isset( $_SERVER['SERVER_SOFTWARE'] ) ? $_SERVER['SERVER_SOFTWARE'] : 'unknown' ),
		);
		$fields['php_version']         = array(
			'label' => __( 'PHP version' ),
			'value' => $php_version,
			'debug' => $php_version_debug,
		);
		$fields['php_sapi']            = array(
			'label' => __( 'PHP SAPI' ),
			'value' => PHP_SAPI,
			'debug' => PHP_SAPI,
		);

		// Some servers disable `ini_set()` and `ini_get()`, we check this before trying to get configuration values.
		if ( ! function_exists( 'ini_get' ) ) {
			$fields['ini_get'] = array(
				'label' => __( 'Server settings' ),
				'value' => sprintf(
				/* translators: %s: ini_get() */
					__( 'Unable to determine some settings, as the %s function has been disabled.' ),
					'ini_get()'
				),
				'debug' => 'ini_get() is disabled',
			);
		} else {
			$fields['max_input_variables'] = array(
				'label' => __( 'PHP max input variables' ),
				'value' => ini_get( 'max_input_vars' ),
			);
			$fields['time_limit']          = array(
				'label' => __( 'PHP time limit' ),
				'value' => ini_get( 'max_execution_time' ),
			);

			if ( WP_Site_Health::get_instance()->php_memory_limit !== ini_get( 'memory_limit' ) ) {
				$fields['memory_limit']       = array(
					'label' => __( 'PHP memory limit' ),
					'value' => WP_Site_Health::get_instance()->php_memory_limit,
				);
				$fields['admin_memory_limit'] = array(
					'label' => __( 'PHP memory limit (only for admin screens)' ),
					'value' => ini_get( 'memory_limit' ),
				);
			} else {
				$fields['memory_limit'] = array(
					'label' => __( 'PHP memory limit' ),
					'value' => ini_get( 'memory_limit' ),
				);
			}

			$fields['max_input_time']      = array(
				'label' => __( 'Max input time' ),
				'value' => ini_get( 'max_input_time' ),
			);
			$fields['upload_max_filesize'] = array(
				'label' => __( 'Upload max filesize' ),
				'value' => ini_get( 'upload_max_filesize' ),
			);
			$fields['php_post_max_size']   = array(
				'label' => __( 'PHP post max size' ),
				'value' => ini_get( 'post_max_size' ),
			);
		}

		if ( function_exists( 'curl_version' ) ) {
			$curl = curl_version();

			$fields['curl_version'] = array(
				'label' => __( 'cURL version' ),
				'value' => sprintf( '%s %s', $curl['version'], $curl['ssl_version'] ),
			);
		} else {
			$fields['curl_version'] = array(
				'label' => __( 'cURL version' ),
				'value' => __( 'Not available' ),
				'debug' => 'not available',
			);
		}

		// SUHOSIN.
		$suhosin_loaded = ( extension_loaded( 'suhosin' ) || ( defined( 'SUHOSIN_PATCH' ) && constant( 'SUHOSIN_PATCH' ) ) );

		$fields['suhosin'] = array(
			'label' => __( 'Is SUHOSIN installed?' ),
			'value' => ( $suhosin_loaded ? __( 'Yes' ) : __( 'No' ) ),
			'debug' => $suhosin_loaded,
		);

		// Imagick.
		$imagick_loaded = extension_loaded( 'imagick' );

		$fields['imagick_availability'] = array(
			'label' => __( 'Is the Imagick library available?' ),
			'value' => ( $imagick_loaded ? __( 'Yes' ) : __( 'No' ) ),
			'debug' => $imagick_loaded,
		);

		// Pretty permalinks.
		$pretty_permalinks_supported = got_url_rewrite();

		$fields['pretty_permalinks'] = array(
			'label' => __( 'Are pretty permalinks supported?' ),
			'value' => ( $pretty_permalinks_supported ? __( 'Yes' ) : __( 'No' ) ),
			'debug' => $pretty_permalinks_supported,
		);

		// Check if a .htaccess file exists.
		if ( is_file( ABSPATH . '.htaccess' ) ) {
			// If the file exists, grab the content of it.
			$htaccess_content = file_get_contents( ABSPATH . '.htaccess' );

			// Filter away the core WordPress rules.
			$filtered_htaccess_content = trim( preg_replace( '/\# BEGIN WordPress[\s\S]+?# END WordPress/si', '', $htaccess_content ) );
			$filtered_htaccess_content = ! empty( $filtered_htaccess_content );

			if ( $filtered_htaccess_content ) {
				/* translators: %s: .htaccess */
				$htaccess_rules_string = sprintf( __( 'Custom rules have been added to your %s file.' ), '.htaccess' );
			} else {
				/* translators: %s: .htaccess */
				$htaccess_rules_string = sprintf( __( 'Your %s file contains only core WordPress features.' ), '.htaccess' );
			}

			$fields['htaccess_extra_rules'] = array(
				'label' => __( '.htaccess rules' ),
				'value' => $htaccess_rules_string,
				'debug' => $filtered_htaccess_content,
			);
		}

		// Check if a robots.txt file exists.
		if ( is_file( ABSPATH . 'robots.txt' ) ) {
			// If the file exists, turn debug info to true.
			$robotstxt_debug = true;

			/* translators: %s: robots.txt */
			$robotstxt_string = sprintf( __( 'There is a static %s file in your installation folder. WordPress cannot dynamically serve one.' ), 'robots.txt' );
		} elseif ( got_url_rewrite() ) {
			// No robots.txt file available and rewrite rules in place, turn debug info to false.
			$robotstxt_debug = false;

			/* translators: %s: robots.txt */
			$robotstxt_string = sprintf( __( 'Your site is using the dynamic %s file which is generated by WordPress.' ), 'robots.txt' );
		} else {
			// No robots.txt file, but without rewrite rules WP can't serve one.
			$robotstxt_debug = true;

			/* translators: %s: robots.txt */
			$robotstxt_string = sprintf( __( 'WordPress cannot dynamically serve a %s file due to a lack of rewrite rule support' ), 'robots.txt' );

		}

		$fields['static_robotstxt_file'] = array(
			'label' => __( 'robots.txt' ),
			'value' => $robotstxt_string,
			'debug' => $robotstxt_debug,
		);

		// Server time.
		$date = new DateTime( 'now', new DateTimeZone( 'UTC' ) );

		$fields['current']     = array(
			'label' => __( 'Current time' ),
			'value' => $date->format( DateTime::ATOM ),
		);
		$fields['utc-time']    = array(
			'label' => __( 'Current UTC time' ),
			'value' => $date->format( DateTime::RFC850 ),
		);
		$fields['server-time'] = array(
			'label' => __( 'Current Server time' ),
			'value' => wp_date( 'c', $_SERVER['REQUEST_TIME'] ),
		);

		return array(
			'label'       => __( 'Server' ),
			'description' => __( 'The options shown below relate to your server setup. If changes are required, you may need your web host&#8217;s assistance.' ),
			'fields'      => $fields,
		);
	}

	/**
	 * Gets the WordPress media section of the debug data.
	 *
	 * @since 6.7.0
	 *
	 * @throws ImagickException
	 * @return array
	 */
	private static function get_wp_media(): array {
		// Spare few function calls.
		$not_available = __( 'Not available' );

		// Populate the media fields.
		$fields['image_editor'] = array(
			'label' => __( 'Active editor' ),
			'value' => _wp_image_editor_choose(),
		);

		// Get ImageMagic information, if available.
		if ( class_exists( 'Imagick' ) ) {
			// Save the Imagick instance for later use.
			$imagick             = new Imagick();
			$imagemagick_version = $imagick->getVersion();
		} else {
			$imagemagick_version = __( 'Not available' );
		}

		$fields['imagick_module_version'] = array(
			'label' => __( 'ImageMagick version number' ),
			'value' => ( is_array( $imagemagick_version ) ? $imagemagick_version['versionNumber'] : $imagemagick_version ),
		);

		$fields['imagemagick_version'] = array(
			'label' => __( 'ImageMagick version string' ),
			'value' => ( is_array( $imagemagick_version ) ? $imagemagick_version['versionString'] : $imagemagick_version ),
		);

		$imagick_version = phpversion( 'imagick' );

		$fields['imagick_version'] = array(
			'label' => __( 'Imagick version' ),
			'value' => ( $imagick_version ) ? $imagick_version : __( 'Not available' ),
		);

		if ( ! function_exists( 'ini_get' ) ) {
			$fields['ini_get'] = array(
				'label' => __( 'File upload settings' ),
				'value' => sprintf(
				/* translators: %s: ini_get() */
					__( 'Unable to determine some settings, as the %s function has been disabled.' ),
					'ini_get()'
				),
				'debug' => 'ini_get() is disabled',
			);
		} else {
			// Get the PHP ini directive values.
			$file_uploads        = ini_get( 'file_uploads' );
			$post_max_size       = ini_get( 'post_max_size' );
			$upload_max_filesize = ini_get( 'upload_max_filesize' );
			$max_file_uploads    = ini_get( 'max_file_uploads' );
			$effective           = min( wp_convert_hr_to_bytes( $post_max_size ), wp_convert_hr_to_bytes( $upload_max_filesize ) );

			// Add info in Media section.
			$fields['file_uploads']        = array(
				'label' => __( 'File uploads' ),
				'value' => $file_uploads ? __( 'Enabled' ) : __( 'Disabled' ),
				'debug' => $file_uploads,
			);
			$fields['post_max_size']       = array(
				'label' => __( 'Max size of post data allowed' ),
				'value' => $post_max_size,
			);
			$fields['upload_max_filesize'] = array(
				'label' => __( 'Max size of an uploaded file' ),
				'value' => $upload_max_filesize,
			);
			$fields['max_effective_size']  = array(
				'label' => __( 'Max effective file size' ),
				'value' => size_format( $effective ),
			);
			$fields['max_file_uploads']    = array(
				'label' => __( 'Max simultaneous file uploads' ),
				'value' => $max_file_uploads,
			);
		}

		// If Imagick is used as our editor, provide some more information about its limitations.
		if ( 'WP_Image_Editor_Imagick' === _wp_image_editor_choose() && isset( $imagick ) && $imagick instanceof Imagick ) {
			$limits = array(
				'area'   => ( defined( 'imagick::RESOURCETYPE_AREA' ) ? size_format( $imagick->getResourceLimit( imagick::RESOURCETYPE_AREA ) ) : $not_available ),
				'disk'   => ( defined( 'imagick::RESOURCETYPE_DISK' ) ? $imagick->getResourceLimit( imagick::RESOURCETYPE_DISK ) : $not_available ),
				'file'   => ( defined( 'imagick::RESOURCETYPE_FILE' ) ? $imagick->getResourceLimit( imagick::RESOURCETYPE_FILE ) : $not_available ),
				'map'    => ( defined( 'imagick::RESOURCETYPE_MAP' ) ? size_format( $imagick->getResourceLimit( imagick::RESOURCETYPE_MAP ) ) : $not_available ),
				'memory' => ( defined( 'imagick::RESOURCETYPE_MEMORY' ) ? size_format( $imagick->getResourceLimit( imagick::RESOURCETYPE_MEMORY ) ) : $not_available ),
				'thread' => ( defined( 'imagick::RESOURCETYPE_THREAD' ) ? $imagick->getResourceLimit( imagick::RESOURCETYPE_THREAD ) : $not_available ),
				'time'   => ( defined( 'imagick::RESOURCETYPE_TIME' ) ? $imagick->getResourceLimit( imagick::RESOURCETYPE_TIME ) : $not_available ),
			);

			$limits_debug = array(
				'imagick::RESOURCETYPE_AREA'   => ( defined( 'imagick::RESOURCETYPE_AREA' ) ? size_format( $imagick->getResourceLimit( imagick::RESOURCETYPE_AREA ) ) : 'not available' ),
				'imagick::RESOURCETYPE_DISK'   => ( defined( 'imagick::RESOURCETYPE_DISK' ) ? $imagick->getResourceLimit( imagick::RESOURCETYPE_DISK ) : 'not available' ),
				'imagick::RESOURCETYPE_FILE'   => ( defined( 'imagick::RESOURCETYPE_FILE' ) ? $imagick->getResourceLimit( imagick::RESOURCETYPE_FILE ) : 'not available' ),
				'imagick::RESOURCETYPE_MAP'    => ( defined( 'imagick::RESOURCETYPE_MAP' ) ? size_format( $imagick->getResourceLimit( imagick::RESOURCETYPE_MAP ) ) : 'not available' ),
				'imagick::RESOURCETYPE_MEMORY' => ( defined( 'imagick::RESOURCETYPE_MEMORY' ) ? size_format( $imagick->getResourceLimit( imagick::RESOURCETYPE_MEMORY ) ) : 'not available' ),
				'imagick::RESOURCETYPE_THREAD' => ( defined( 'imagick::RESOURCETYPE_THREAD' ) ? $imagick->getResourceLimit( imagick::RESOURCETYPE_THREAD ) : 'not available' ),
				'imagick::RESOURCETYPE_TIME'   => ( defined( 'imagick::RESOURCETYPE_TIME' ) ? $imagick->getResourceLimit( imagick::RESOURCETYPE_TIME ) : 'not available' ),
			);

			$fields['imagick_limits'] = array(
				'label' => __( 'Imagick Resource Limits' ),
				'value' => $limits,
				'debug' => $limits_debug,
			);

			try {
				$formats = Imagick::queryFormats( '*' );
			} catch ( Exception $e ) {
				$formats = array();
			}

			$fields['imagemagick_file_formats'] = array(
				'label' => __( 'ImageMagick supported file formats' ),
				'value' => ( empty( $formats ) ) ? __( 'Unable to determine' ) : implode( ', ', $formats ),
				'debug' => ( empty( $formats ) ) ? 'Unable to determine' : implode( ', ', $formats ),
			);
		}

		// Get GD information, if available.
		if ( function_exists( 'gd_info' ) ) {
			$gd = gd_info();
		} else {
			$gd = false;
		}

		$fields['gd_version'] = array(
			'label' => __( 'GD version' ),
			'value' => ( is_array( $gd ) ? $gd['GD Version'] : $not_available ),
			'debug' => ( is_array( $gd ) ? $gd['GD Version'] : 'not available' ),
		);

		$gd_image_formats     = array();
		$gd_supported_formats = array(
			'GIF Create' => 'GIF',
			'JPEG'       => 'JPEG',
			'PNG'        => 'PNG',
			'WebP'       => 'WebP',
			'BMP'        => 'BMP',
			'AVIF'       => 'AVIF',
			'HEIF'       => 'HEIF',
			'TIFF'       => 'TIFF',
			'XPM'        => 'XPM',
		);

		foreach ( $gd_supported_formats as $format_key => $format ) {
			$index = $format_key . ' Support';
			if ( isset( $gd[ $index ] ) && $gd[ $index ] ) {
				array_push( $gd_image_formats, $format );
			}
		}

		if ( ! empty( $gd_image_formats ) ) {
			$fields['gd_formats'] = array(
				'label' => __( 'GD supported file formats' ),
				'value' => implode( ', ', $gd_image_formats ),
			);
		}

		// Get Ghostscript information, if available.
		if ( function_exists( 'exec' ) ) {
			$gs = exec( 'gs --version' );

			if ( empty( $gs ) ) {
				$gs       = $not_available;
				$gs_debug = 'not available';
			} else {
				$gs_debug = $gs;
			}
		} else {
			$gs       = __( 'Unable to determine if Ghostscript is installed' );
			$gs_debug = 'unknown';
		}

		$fields['ghostscript_version'] = array(
			'label' => __( 'Ghostscript version' ),
			'value' => $gs,
			'debug' => $gs_debug,
		);

		return array(
			'label'  => __( 'Media Handling' ),
			'fields' => $fields,
		);
	}

	/**
	 * Gets the WordPress MU plugins section of the debug data.
	 *
	 * @since 6.7.0
	 *
	 * @return array
	 */
	private static function get_wp_mu_plugins(): array {
		// List must use plugins if there are any.
		$mu_plugins = get_mu_plugins();
		$fields     = array();

		foreach ( $mu_plugins as $plugin_path => $plugin ) {
			$plugin_version = $plugin['Version'];
			$plugin_author  = $plugin['Author'];

			$plugin_version_string       = __( 'No version or author information is available.' );
			$plugin_version_string_debug = 'author: (undefined), version: (undefined)';

			if ( ! empty( $plugin_version ) && ! empty( $plugin_author ) ) {
				/* translators: 1: Plugin version number. 2: Plugin author name. */
				$plugin_version_string       = sprintf( __( 'Version %1$s by %2$s' ), $plugin_version, $plugin_author );
				$plugin_version_string_debug = sprintf( 'version: %s, author: %s', $plugin_version, $plugin_author );
			} else {
				if ( ! empty( $plugin_author ) ) {
					/* translators: %s: Plugin author name. */
					$plugin_version_string       = sprintf( __( 'By %s' ), $plugin_author );
					$plugin_version_string_debug = sprintf( 'author: %s, version: (undefined)', $plugin_author );
				}

				if ( ! empty( $plugin_version ) ) {
					/* translators: %s: Plugin version number. */
					$plugin_version_string       = sprintf( __( 'Version %s' ), $plugin_version );
					$plugin_version_string_debug = sprintf( 'author: (undefined), version: %s', $plugin_version );
				}
			}

			$fields[ sanitize_text_field( $plugin['Name'] ) ] = array(
				'label' => $plugin['Name'],
				'value' => $plugin_version_string,
				'debug' => $plugin_version_string_debug,
			);
		}

		return array(
			'label'      => __( 'Must Use Plugins' ),
			'show_count' => true,
			'fields'     => $fields,
		);
	}

	/**
	 * Gets the WordPress paths and sizes section of the debug data.
	 *
	 * @since 6.7.0
	 *
	 * @return array|null Paths and sizes debug data for single sites,
	 *                    otherwise `null` for multi-site installs.
	 */
	private static function get_wp_paths_sizes(): ?array {
		if ( is_multisite() ) {
			return null;
		}

		$loading = __( 'Loading&hellip;' );

		$fields = array(
			'wordpress_path' => array(
				'label' => __( 'WordPress directory location' ),
				'value' => untrailingslashit( ABSPATH ),
			),
			'wordpress_size' => array(
				'label' => __( 'WordPress directory size' ),
				'value' => $loading,
				'debug' => 'loading...',
			),
			'uploads_path'   => array(
				'label' => __( 'Uploads directory location' ),
				'value' => wp_upload_dir()['basedir'],
			),
			'uploads_size'   => array(
				'label' => __( 'Uploads directory size' ),
				'value' => $loading,
				'debug' => 'loading...',
			),
			'themes_path'    => array(
				'label' => __( 'Themes directory location' ),
				'value' => get_theme_root(),
			),
			'themes_size'    => array(
				'label' => __( 'Themes directory size' ),
				'value' => $loading,
				'debug' => 'loading...',
			),
			'plugins_path'   => array(
				'label' => __( 'Plugins directory location' ),
				'value' => WP_PLUGIN_DIR,
			),
			'plugins_size'   => array(
				'label' => __( 'Plugins directory size' ),
				'value' => $loading,
				'debug' => 'loading...',
			),
			'fonts_path'     => array(
				'label' => __( 'Fonts directory location' ),
				'value' => wp_get_font_dir()['basedir'],
			),
			'fonts_size'     => array(
				'label' => __( 'Fonts directory size' ),
				'value' => $loading,
				'debug' => 'loading...',
			),
			'database_size'  => array(
				'label' => __( 'Database size' ),
				'value' => $loading,
				'debug' => 'loading...',
			),
			'total_size'     => array(
				'label' => __( 'Total installation size' ),
				'value' => $loading,
				'debug' => 'loading...',
			),
		);

		return array(
			/* translators: Filesystem directory paths and storage sizes. */
			'label'  => __( 'Directories and Sizes' ),
			'fields' => $fields,
		);
	}

	/**
	 * Gets the WordPress active plugins section of the debug data.
	 *
	 * @since 6.7.0
	 *
	 * @return array
	 */
	private static function get_wp_plugins_active(): array {
		return array(
			'label'      => __( 'Active Plugins' ),
			'show_count' => true,
			'fields'     => self::get_wp_plugins_raw_data()['wp-plugins-active'],
		);
	}

	/**
	 * Gets the WordPress inactive plugins section of the debug data.
	 *
	 * @since 6.7.0
	 *
	 * @return array
	 */
	private static function get_wp_plugins_inactive(): array {
		return array(
			'label'      => __( 'Inactive Plugins' ),
			'show_count' => true,
			'fields'     => self::get_wp_plugins_raw_data()['wp-plugins-inactive'],
		);
	}

	/**
	 * Gets the raw plugin data for the WordPress active and inactive sections of the debug data.
	 *
	 * @since 6.7.0
	 *
	 * @return array
	 */
	private static function get_wp_plugins_raw_data(): array {
		// List all available plugins.
		$plugins        = get_plugins();
		$plugin_updates = get_plugin_updates();
		$transient      = get_site_transient( 'update_plugins' );

		$auto_updates = array();
		$fields       = array(
			'wp-plugins-active'   => array(),
			'wp-plugins-inactive' => array(),
		);

		$auto_updates_enabled = wp_is_auto_update_enabled_for_type( 'plugin' );

		if ( $auto_updates_enabled ) {
			$auto_updates = (array) get_site_option( 'auto_update_plugins', array() );
		}

		foreach ( $plugins as $plugin_path => $plugin ) {
			$plugin_part = ( is_plugin_active( $plugin_path ) ) ? 'wp-plugins-active' : 'wp-plugins-inactive';

			$plugin_version = $plugin['Version'];
			$plugin_author  = $plugin['Author'];

			$plugin_version_string       = __( 'No version or author information is available.' );
			$plugin_version_string_debug = 'author: (undefined), version: (undefined)';

			if ( ! empty( $plugin_version ) && ! empty( $plugin_author ) ) {
				/* translators: 1: Plugin version number. 2: Plugin author name. */
				$plugin_version_string       = sprintf( __( 'Version %1$s by %2$s' ), $plugin_version, $plugin_author );
				$plugin_version_string_debug = sprintf( 'version: %s, author: %s', $plugin_version, $plugin_author );
			} else {
				if ( ! empty( $plugin_author ) ) {
					/* translators: %s: Plugin author name. */
					$plugin_version_string       = sprintf( __( 'By %s' ), $plugin_author );
					$plugin_version_string_debug = sprintf( 'author: %s, version: (undefined)', $plugin_author );
				}

				if ( ! empty( $plugin_version ) ) {
					/* translators: %s: Plugin version number. */
					$plugin_version_string       = sprintf( __( 'Version %s' ), $plugin_version );
					$plugin_version_string_debug = sprintf( 'author: (undefined), version: %s', $plugin_version );
				}
			}

			if ( array_key_exists( $plugin_path, $plugin_updates ) ) {
				/* translators: %s: Latest plugin version number. */
				$plugin_version_string       .= ' ' . sprintf( __( '(Latest version: %s)' ), $plugin_updates[ $plugin_path ]->update->new_version );
				$plugin_version_string_debug .= sprintf( ' (latest version: %s)', $plugin_updates[ $plugin_path ]->update->new_version );
			}

			if ( $auto_updates_enabled ) {
				if ( isset( $transient->response[ $plugin_path ] ) ) {
					$item = $transient->response[ $plugin_path ];
				} elseif ( isset( $transient->no_update[ $plugin_path ] ) ) {
					$item = $transient->no_update[ $plugin_path ];
				} else {
					$item = array(
						'id'            => $plugin_path,
						'slug'          => '',
						'plugin'        => $plugin_path,
						'new_version'   => '',
						'url'           => '',
						'package'       => '',
						'icons'         => array(),
						'banners'       => array(),
						'banners_rtl'   => array(),
						'tested'        => '',
						'requires_php'  => '',
						'compatibility' => new stdClass(),
					);
					$item = wp_parse_args( $plugin, $item );
				}

				$auto_update_forced = wp_is_auto_update_forced_for_item( 'plugin', null, (object) $item );

				if ( ! is_null( $auto_update_forced ) ) {
					$enabled = $auto_update_forced;
				} else {
					$enabled = in_array( $plugin_path, $auto_updates, true );
				}

				if ( $enabled ) {
					$auto_updates_string = __( 'Auto-updates enabled' );
				} else {
					$auto_updates_string = __( 'Auto-updates disabled' );
				}

				/**
				 * Filters the text string of the auto-updates setting for each plugin in the Site Health debug data.
				 *
				 * @since 5.5.0
				 *
				 * @param string $auto_updates_string The string output for the auto-updates column.
				 * @param string $plugin_path         The path to the plugin file.
				 * @param array  $plugin              An array of plugin data.
				 * @param bool   $enabled             Whether auto-updates are enabled for this item.
				 */
				$auto_updates_string = apply_filters( 'plugin_auto_update_debug_string', $auto_updates_string, $plugin_path, $plugin, $enabled );

				$plugin_version_string       .= ' | ' . $auto_updates_string;
				$plugin_version_string_debug .= ', ' . $auto_updates_string;
			}

			$fields[ $plugin_part ][ sanitize_text_field( $plugin['Name'] ) ] = array(
				'label' => $plugin['Name'],
				'value' => $plugin_version_string,
				'debug' => $plugin_version_string_debug,
			);
		}

		return $fields;
	}

	/**
	 * Gets the WordPress active theme section of the debug data.
	 *
	 * @since 6.7.0
	 *
	 * @global array $_wp_theme_features
	 *
	 * @return array
	 */
	private static function get_wp_active_theme(): array {
		global $_wp_theme_features;

		// Populate the section for the currently active theme.
		$theme_features = array();

		if ( ! empty( $_wp_theme_features ) ) {
			foreach ( $_wp_theme_features as $feature => $options ) {
				$theme_features[] = $feature;
			}
		}

		$active_theme  = wp_get_theme();
		$theme_updates = get_theme_updates();
		$transient     = get_site_transient( 'update_themes' );

		$active_theme_version       = $active_theme->version;
		$active_theme_version_debug = $active_theme_version;

		$auto_updates         = array();
		$auto_updates_enabled = wp_is_auto_update_enabled_for_type( 'theme' );
		if ( $auto_updates_enabled ) {
			$auto_updates = (array) get_site_option( 'auto_update_themes', array() );
		}

		if ( array_key_exists( $active_theme->stylesheet, $theme_updates ) ) {
			$theme_update_new_version = $theme_updates[ $active_theme->stylesheet ]->update['new_version'];

			/* translators: %s: Latest theme version number. */
			$active_theme_version       .= ' ' . sprintf( __( '(Latest version: %s)' ), $theme_update_new_version );
			$active_theme_version_debug .= sprintf( ' (latest version: %s)', $theme_update_new_version );
		}

		$active_theme_author_uri = $active_theme->display( 'AuthorURI' );

		if ( $active_theme->parent_theme ) {
			$active_theme_parent_theme = sprintf(
				/* translators: 1: Theme name. 2: Theme slug. */
				__( '%1$s (%2$s)' ),
				$active_theme->parent_theme,
				$active_theme->template
			);
			$active_theme_parent_theme_debug = sprintf(
				'%s (%s)',
				$active_theme->parent_theme,
				$active_theme->template
			);
		} else {
			$active_theme_parent_theme       = __( 'None' );
			$active_theme_parent_theme_debug = 'none';
		}

		$fields = array(
			'name'           => array(
				'label' => __( 'Name' ),
				'value' => sprintf(
					/* translators: 1: Theme name. 2: Theme slug. */
					__( '%1$s (%2$s)' ),
					$active_theme->name,
					$active_theme->stylesheet
				),
			),
			'version'        => array(
				'label' => __( 'Version' ),
				'value' => $active_theme_version,
				'debug' => $active_theme_version_debug,
			),
			'author'         => array(
				'label' => __( 'Author' ),
				'value' => wp_kses( $active_theme->author, array() ),
			),
			'author_website' => array(
				'label' => __( 'Author website' ),
				'value' => ( $active_theme_author_uri ? $active_theme_author_uri : __( 'Undefined' ) ),
				'debug' => ( $active_theme_author_uri ? $active_theme_author_uri : '(undefined)' ),
			),
			'parent_theme'   => array(
				'label' => __( 'Parent theme' ),
				'value' => $active_theme_parent_theme,
				'debug' => $active_theme_parent_theme_debug,
			),
			'theme_features' => array(
				'label' => __( 'Theme features' ),
				'value' => implode( ', ', $theme_features ),
			),
			'theme_path'     => array(
				'label' => __( 'Theme directory location' ),
				'value' => get_stylesheet_directory(),
			),
		);

		if ( $auto_updates_enabled ) {
			if ( isset( $transient->response[ $active_theme->stylesheet ] ) ) {
				$item = $transient->response[ $active_theme->stylesheet ];
			} elseif ( isset( $transient->no_update[ $active_theme->stylesheet ] ) ) {
				$item = $transient->no_update[ $active_theme->stylesheet ];
			} else {
				$item = array(
					'theme'        => $active_theme->stylesheet,
					'new_version'  => $active_theme->version,
					'url'          => '',
					'package'      => '',
					'requires'     => '',
					'requires_php' => '',
				);
			}

			$auto_update_forced = wp_is_auto_update_forced_for_item( 'theme', null, (object) $item );

			if ( ! is_null( $auto_update_forced ) ) {
				$enabled = $auto_update_forced;
			} else {
				$enabled = in_array( $active_theme->stylesheet, $auto_updates, true );
			}

			if ( $enabled ) {
				$auto_updates_string = __( 'Enabled' );
			} else {
				$auto_updates_string = __( 'Disabled' );
			}

			/** This filter is documented in wp-admin/includes/class-wp-debug-data.php */
			$auto_updates_string = apply_filters( 'theme_auto_update_debug_string', $auto_updates_string, $active_theme, $enabled );

			$fields['auto_update'] = array(
				'label' => __( 'Auto-updates' ),
				'value' => $auto_updates_string,
				'debug' => $auto_updates_string,
			);
		}

		return array(
			'label'  => __( 'Active Theme' ),
			'fields' => $fields,
		);
	}

	/**
	 * Gets the WordPress parent theme section of the debug data.
	 *
	 * @since 6.7.0
	 *
	 * @return array
	 */
	private static function get_wp_parent_theme(): array {
		$theme_updates = get_theme_updates();
		$transient     = get_site_transient( 'update_themes' );

		$auto_updates         = array();
		$auto_updates_enabled = wp_is_auto_update_enabled_for_type( 'theme' );
		if ( $auto_updates_enabled ) {
			$auto_updates = (array) get_site_option( 'auto_update_themes', array() );
		}

		$active_theme = wp_get_theme();
		$parent_theme = $active_theme->parent();
		$fields       = array();

		if ( $parent_theme ) {
			$parent_theme_version       = $parent_theme->version;
			$parent_theme_version_debug = $parent_theme_version;

			if ( array_key_exists( $parent_theme->stylesheet, $theme_updates ) ) {
				$parent_theme_update_new_version = $theme_updates[ $parent_theme->stylesheet ]->update['new_version'];

				/* translators: %s: Latest theme version number. */
				$parent_theme_version       .= ' ' . sprintf( __( '(Latest version: %s)' ), $parent_theme_update_new_version );
				$parent_theme_version_debug .= sprintf( ' (latest version: %s)', $parent_theme_update_new_version );
			}

			$parent_theme_author_uri = $parent_theme->display( 'AuthorURI' );

			$fields = array(
				'name'           => array(
					'label' => __( 'Name' ),
					'value' => sprintf(
						/* translators: 1: Theme name. 2: Theme slug. */
						__( '%1$s (%2$s)' ),
						$parent_theme->name,
						$parent_theme->stylesheet
					),
				),
				'version'        => array(
					'label' => __( 'Version' ),
					'value' => $parent_theme_version,
					'debug' => $parent_theme_version_debug,
				),
				'author'         => array(
					'label' => __( 'Author' ),
					'value' => wp_kses( $parent_theme->author, array() ),
				),
				'author_website' => array(
					'label' => __( 'Author website' ),
					'value' => ( $parent_theme_author_uri ? $parent_theme_author_uri : __( 'Undefined' ) ),
					'debug' => ( $parent_theme_author_uri ? $parent_theme_author_uri : '(undefined)' ),
				),
				'theme_path'     => array(
					'label' => __( 'Theme directory location' ),
					'value' => get_template_directory(),
				),
			);

			if ( $auto_updates_enabled ) {
				if ( isset( $transient->response[ $parent_theme->stylesheet ] ) ) {
					$item = $transient->response[ $parent_theme->stylesheet ];
				} elseif ( isset( $transient->no_update[ $parent_theme->stylesheet ] ) ) {
					$item = $transient->no_update[ $parent_theme->stylesheet ];
				} else {
					$item = array(
						'theme'        => $parent_theme->stylesheet,
						'new_version'  => $parent_theme->version,
						'url'          => '',
						'package'      => '',
						'requires'     => '',
						'requires_php' => '',
					);
				}

				$auto_update_forced = wp_is_auto_update_forced_for_item( 'theme', null, (object) $item );

				if ( ! is_null( $auto_update_forced ) ) {
					$enabled = $auto_update_forced;
				} else {
					$enabled = in_array( $parent_theme->stylesheet, $auto_updates, true );
				}

				if ( $enabled ) {
					$parent_theme_auto_update_string = __( 'Enabled' );
				} else {
					$parent_theme_auto_update_string = __( 'Disabled' );
				}

				/** This filter is documented in wp-admin/includes/class-wp-debug-data.php */
				$parent_theme_auto_update_string = apply_filters( 'theme_auto_update_debug_string', $parent_theme_auto_update_string, $parent_theme, $enabled );

				$fields['auto_update'] = array(
					'label' => __( 'Auto-update' ),
					'value' => $parent_theme_auto_update_string,
					'debug' => $parent_theme_auto_update_string,
				);
			}
		}

		return array(
			'label'  => __( 'Parent Theme' ),
			'fields' => $fields,
		);
	}

	/**
	 * Gets the WordPress inactive themes section of the debug data.
	 *
	 * @since 6.7.0
	 *
	 * @return array
	 */
	private static function get_wp_themes_inactive(): array {
		$active_theme  = wp_get_theme();
		$parent_theme  = $active_theme->parent();
		$theme_updates = get_theme_updates();

		$auto_updates         = array();
		$auto_updates_enabled = wp_is_auto_update_enabled_for_type( 'theme' );
		if ( $auto_updates_enabled ) {
			$auto_updates = (array) get_site_option( 'auto_update_themes', array() );
		}

		// Populate a list of all themes available in the installation.
		$all_themes = wp_get_themes();
		$fields     = array();

		foreach ( $all_themes as $theme_slug => $theme ) {
			// Exclude the currently active theme from the list of all themes.
			if ( $active_theme->stylesheet === $theme_slug ) {
				continue;
			}

			// Exclude the currently active parent theme from the list of all themes.
			if ( ! empty( $parent_theme ) && $parent_theme->stylesheet === $theme_slug ) {
				continue;
			}

			$theme_version = $theme->version;
			$theme_author  = $theme->author;

			// Sanitize.
			$theme_author = wp_kses( $theme_author, array() );

			$theme_version_string       = __( 'No version or author information is available.' );
			$theme_version_string_debug = 'undefined';

			if ( ! empty( $theme_version ) && ! empty( $theme_author ) ) {
				/* translators: 1: Theme version number. 2: Theme author name. */
				$theme_version_string       = sprintf( __( 'Version %1$s by %2$s' ), $theme_version, $theme_author );
				$theme_version_string_debug = sprintf( 'version: %s, author: %s', $theme_version, $theme_author );
			} else {
				if ( ! empty( $theme_author ) ) {
					/* translators: %s: Theme author name. */
					$theme_version_string       = sprintf( __( 'By %s' ), $theme_author );
					$theme_version_string_debug = sprintf( 'author: %s, version: (undefined)', $theme_author );
				}

				if ( ! empty( $theme_version ) ) {
					/* translators: %s: Theme version number. */
					$theme_version_string       = sprintf( __( 'Version %s' ), $theme_version );
					$theme_version_string_debug = sprintf( 'author: (undefined), version: %s', $theme_version );
				}
			}

			if ( array_key_exists( $theme_slug, $theme_updates ) ) {
				/* translators: %s: Latest theme version number. */
				$theme_version_string       .= ' ' . sprintf( __( '(Latest version: %s)' ), $theme_updates[ $theme_slug ]->update['new_version'] );
				$theme_version_string_debug .= sprintf( ' (latest version: %s)', $theme_updates[ $theme_slug ]->update['new_version'] );
			}

			if ( $auto_updates_enabled ) {
				if ( isset( $transient->response[ $theme_slug ] ) ) {
					$item = $transient->response[ $theme_slug ];
				} elseif ( isset( $transient->no_update[ $theme_slug ] ) ) {
					$item = $transient->no_update[ $theme_slug ];
				} else {
					$item = array(
						'theme'        => $theme_slug,
						'new_version'  => $theme->version,
						'url'          => '',
						'package'      => '',
						'requires'     => '',
						'requires_php' => '',
					);
				}

				$auto_update_forced = wp_is_auto_update_forced_for_item( 'theme', null, (object) $item );

				if ( ! is_null( $auto_update_forced ) ) {
					$enabled = $auto_update_forced;
				} else {
					$enabled = in_array( $theme_slug, $auto_updates, true );
				}

				if ( $enabled ) {
					$auto_updates_string = __( 'Auto-updates enabled' );
				} else {
					$auto_updates_string = __( 'Auto-updates disabled' );
				}

				/**
				 * Filters the text string of the auto-updates setting for each theme in the Site Health debug data.
				 *
				 * @since 5.5.0
				 *
				 * @param string   $auto_updates_string The string output for the auto-updates column.
				 * @param WP_Theme $theme               An object of theme data.
				 * @param bool     $enabled             Whether auto-updates are enabled for this item.
				 */
				$auto_updates_string = apply_filters( 'theme_auto_update_debug_string', $auto_updates_string, $theme, $enabled );

				$theme_version_string       .= ' | ' . $auto_updates_string;
				$theme_version_string_debug .= ', ' . $auto_updates_string;
			}

			$fields[ sanitize_text_field( $theme->name ) ] = array(
				'label' => sprintf(
					/* translators: 1: Theme name. 2: Theme slug. */
					__( '%1$s (%2$s)' ),
					$theme->name,
					$theme_slug
				),
				'value' => $theme_version_string,
				'debug' => $theme_version_string_debug,
			);
		}

		return array(
			'label'      => __( 'Inactive Themes' ),
			'show_count' => true,
			'fields'     => $fields,
		);
	}

	/**
	 * Gets the WordPress constants section of the debug data.
	 *
	 * @since 6.7.0
	 *
	 * @return array
	 */
	private static function get_wp_constants(): array {
		// Check if WP_DEBUG_LOG is set.
		$wp_debug_log_value = __( 'Disabled' );
		if ( is_string( WP_DEBUG_LOG ) ) {
			$wp_debug_log_value = WP_DEBUG_LOG;
		} elseif ( WP_DEBUG_LOG ) {
			$wp_debug_log_value = __( 'Enabled' );
		}

		// Check CONCATENATE_SCRIPTS.
		if ( defined( 'CONCATENATE_SCRIPTS' ) ) {
			$concatenate_scripts       = CONCATENATE_SCRIPTS ? __( 'Enabled' ) : __( 'Disabled' );
			$concatenate_scripts_debug = CONCATENATE_SCRIPTS ? 'true' : 'false';
		} else {
			$concatenate_scripts       = __( 'Undefined' );
			$concatenate_scripts_debug = 'undefined';
		}

		// Check COMPRESS_SCRIPTS.
		if ( defined( 'COMPRESS_SCRIPTS' ) ) {
			$compress_scripts       = COMPRESS_SCRIPTS ? __( 'Enabled' ) : __( 'Disabled' );
			$compress_scripts_debug = COMPRESS_SCRIPTS ? 'true' : 'false';
		} else {
			$compress_scripts       = __( 'Undefined' );
			$compress_scripts_debug = 'undefined';
		}

		// Check COMPRESS_CSS.
		if ( defined( 'COMPRESS_CSS' ) ) {
			$compress_css       = COMPRESS_CSS ? __( 'Enabled' ) : __( 'Disabled' );
			$compress_css_debug = COMPRESS_CSS ? 'true' : 'false';
		} else {
			$compress_css       = __( 'Undefined' );
			$compress_css_debug = 'undefined';
		}

		// Check WP_ENVIRONMENT_TYPE.
		if ( defined( 'WP_ENVIRONMENT_TYPE' ) ) {
			$wp_environment_type       = WP_ENVIRONMENT_TYPE ? WP_ENVIRONMENT_TYPE : __( 'Empty value' );
			$wp_environment_type_debug = WP_ENVIRONMENT_TYPE;
		} else {
			$wp_environment_type       = __( 'Undefined' );
			$wp_environment_type_debug = 'undefined';
		}

		// Check DB_COLLATE.
		if ( defined( 'DB_COLLATE' ) ) {
			$db_collate       = DB_COLLATE ? DB_COLLATE : __( 'Empty value' );
			$db_collate_debug = DB_COLLATE;
		} else {
			$db_collate       = __( 'Undefined' );
			$db_collate_debug = 'undefined';
		}

		$fields = array(
			'ABSPATH'             => array(
				'label'   => 'ABSPATH',
				'value'   => ABSPATH,
				'private' => true,
			),
			'WP_HOME'             => array(
				'label' => 'WP_HOME',
				'value' => ( defined( 'WP_HOME' ) ? WP_HOME : __( 'Undefined' ) ),
				'debug' => ( defined( 'WP_HOME' ) ? WP_HOME : 'undefined' ),
			),
			'WP_SITEURL'          => array(
				'label' => 'WP_SITEURL',
				'value' => ( defined( 'WP_SITEURL' ) ? WP_SITEURL : __( 'Undefined' ) ),
				'debug' => ( defined( 'WP_SITEURL' ) ? WP_SITEURL : 'undefined' ),
			),
			'WP_CONTENT_DIR'      => array(
				'label' => 'WP_CONTENT_DIR',
				'value' => WP_CONTENT_DIR,
			),
			'WP_PLUGIN_DIR'       => array(
				'label' => 'WP_PLUGIN_DIR',
				'value' => WP_PLUGIN_DIR,
			),
			'WP_MEMORY_LIMIT'     => array(
				'label' => 'WP_MEMORY_LIMIT',
				'value' => WP_MEMORY_LIMIT,
			),
			'WP_MAX_MEMORY_LIMIT' => array(
				'label' => 'WP_MAX_MEMORY_LIMIT',
				'value' => WP_MAX_MEMORY_LIMIT,
			),
			'WP_DEBUG'            => array(
				'label' => 'WP_DEBUG',
				'value' => WP_DEBUG ? __( 'Enabled' ) : __( 'Disabled' ),
				'debug' => WP_DEBUG,
			),
			'WP_DEBUG_DISPLAY'    => array(
				'label' => 'WP_DEBUG_DISPLAY',
				'value' => WP_DEBUG_DISPLAY ? __( 'Enabled' ) : __( 'Disabled' ),
				'debug' => WP_DEBUG_DISPLAY,
			),
			'WP_DEBUG_LOG'        => array(
				'label' => 'WP_DEBUG_LOG',
				'value' => $wp_debug_log_value,
				'debug' => WP_DEBUG_LOG,
			),
			'SCRIPT_DEBUG'        => array(
				'label' => 'SCRIPT_DEBUG',
				'value' => SCRIPT_DEBUG ? __( 'Enabled' ) : __( 'Disabled' ),
				'debug' => SCRIPT_DEBUG,
			),
			'WP_CACHE'            => array(
				'label' => 'WP_CACHE',
				'value' => WP_CACHE ? __( 'Enabled' ) : __( 'Disabled' ),
				'debug' => WP_CACHE,
			),
			'CONCATENATE_SCRIPTS' => array(
				'label' => 'CONCATENATE_SCRIPTS',
				'value' => $concatenate_scripts,
				'debug' => $concatenate_scripts_debug,
			),
			'COMPRESS_SCRIPTS'    => array(
				'label' => 'COMPRESS_SCRIPTS',
				'value' => $compress_scripts,
				'debug' => $compress_scripts_debug,
			),
			'COMPRESS_CSS'        => array(
				'label' => 'COMPRESS_CSS',
				'value' => $compress_css,
				'debug' => $compress_css_debug,
			),
			'WP_ENVIRONMENT_TYPE' => array(
				'label' => 'WP_ENVIRONMENT_TYPE',
				'value' => $wp_environment_type,
				'debug' => $wp_environment_type_debug,
			),
			'WP_DEVELOPMENT_MODE' => array(
				'label' => 'WP_DEVELOPMENT_MODE',
				'value' => WP_DEVELOPMENT_MODE ? WP_DEVELOPMENT_MODE : __( 'Disabled' ),
				'debug' => WP_DEVELOPMENT_MODE,
			),
			'DB_CHARSET'          => array(
				'label' => 'DB_CHARSET',
				'value' => ( defined( 'DB_CHARSET' ) ? DB_CHARSET : __( 'Undefined' ) ),
				'debug' => ( defined( 'DB_CHARSET' ) ? DB_CHARSET : 'undefined' ),
			),
			'DB_COLLATE'          => array(
				'label' => 'DB_COLLATE',
				'value' => $db_collate,
				'debug' => $db_collate_debug,
			),
		);

		return array(
			'label'       => __( 'WordPress Constants' ),
			'description' => __( 'These settings alter where and how parts of WordPress are loaded.' ),
			'fields'      => $fields,
		);
	}

	/**
	 * Gets the WordPress database section of the debug data.
	 *
	 * @since 6.7.0
	 *
	 * @global wpdb $wpdb WordPress database abstraction object.
	 *
	 * @return array
	 */
	private static function get_wp_database(): array {
		global $wpdb;

		// Populate the database debug fields.
		if ( is_object( $wpdb->dbh ) ) {
			// mysqli or PDO.
			$extension = get_class( $wpdb->dbh );
		} else {
			// Unknown sql extension.
			$extension = null;
		}

		$server = $wpdb->get_var( 'SELECT VERSION()' );

		$client_version = $wpdb->dbh->client_info;

		$fields = array(
			'extension'          => array(
				'label' => __( 'Database Extension' ),
				'value' => $extension,
			),
			'server_version'     => array(
				'label' => __( 'Server version' ),
				'value' => $server,
			),
			'client_version'     => array(
				'label' => __( 'Client version' ),
				'value' => $client_version,
			),
			'database_user'      => array(
				'label'   => __( 'Database username' ),
				'value'   => $wpdb->dbuser,
				'private' => true,
			),
			'database_host'      => array(
				'label'   => __( 'Database host' ),
				'value'   => $wpdb->dbhost,
				'private' => true,
			),
			'database_name'      => array(
				'label'   => __( 'Database name' ),
				'value'   => $wpdb->dbname,
				'private' => true,
			),
			'database_prefix'    => array(
				'label'   => __( 'Table prefix' ),
				'value'   => $wpdb->prefix,
				'private' => true,
			),
			'database_charset'   => array(
				'label'   => __( 'Database charset' ),
				'value'   => $wpdb->charset,
				'private' => true,
			),
			'database_collate'   => array(
				'label'   => __( 'Database collation' ),
				'value'   => $wpdb->collate,
				'private' => true,
			),
			'max_allowed_packet' => array(
				'label' => __( 'Max allowed packet size' ),
				'value' => self::get_mysql_var( 'max_allowed_packet' ),
			),
			'max_connections'    => array(
				'label' => __( 'Max connections number' ),
				'value' => self::get_mysql_var( 'max_connections' ),
			),
		);

		return array(
			'label'  => __( 'Database' ),
			'fields' => $fields,
		);
	}

	/**
	 * Gets the file system section of the debug data.
	 *
	 * @since 6.7.0
	 *
	 * @return array
	 */
	private static function get_wp_filesystem(): array {
		$upload_dir                     = wp_upload_dir();
		$fonts_dir_exists               = file_exists( wp_get_font_dir()['basedir'] );
		$is_writable_abspath            = wp_is_writable( ABSPATH );
		$is_writable_wp_content_dir     = wp_is_writable( WP_CONTENT_DIR );
		$is_writable_upload_dir         = wp_is_writable( $upload_dir['basedir'] );
		$is_writable_wp_plugin_dir      = wp_is_writable( WP_PLUGIN_DIR );
		$is_writable_template_directory = wp_is_writable( get_theme_root( get_template() ) );
		$is_writable_fonts_dir          = $fonts_dir_exists ? wp_is_writable( wp_get_font_dir()['basedir'] ) : false;

		$fields = array(
			'wordpress'  => array(
				'label' => __( 'The main WordPress directory' ),
				'value' => ( $is_writable_abspath ? __( 'Writable' ) : __( 'Not writable' ) ),
				'debug' => ( $is_writable_abspath ? 'writable' : 'not writable' ),
			),
			'wp-content' => array(
				'label' => __( 'The wp-content directory' ),
				'value' => ( $is_writable_wp_content_dir ? __( 'Writable' ) : __( 'Not writable' ) ),
				'debug' => ( $is_writable_wp_content_dir ? 'writable' : 'not writable' ),
			),
			'uploads'    => array(
				'label' => __( 'The uploads directory' ),
				'value' => ( $is_writable_upload_dir ? __( 'Writable' ) : __( 'Not writable' ) ),
				'debug' => ( $is_writable_upload_dir ? 'writable' : 'not writable' ),
			),
			'plugins'    => array(
				'label' => __( 'The plugins directory' ),
				'value' => ( $is_writable_wp_plugin_dir ? __( 'Writable' ) : __( 'Not writable' ) ),
				'debug' => ( $is_writable_wp_plugin_dir ? 'writable' : 'not writable' ),
			),
			'themes'     => array(
				'label' => __( 'The themes directory' ),
				'value' => ( $is_writable_template_directory ? __( 'Writable' ) : __( 'Not writable' ) ),
				'debug' => ( $is_writable_template_directory ? 'writable' : 'not writable' ),
			),
			'fonts'      => array(
				'label' => __( 'The fonts directory' ),
				'value' => $fonts_dir_exists
					? ( $is_writable_fonts_dir ? __( 'Writable' ) : __( 'Not writable' ) )
					: __( 'Does not exist' ),
				'debug' => $fonts_dir_exists
					? ( $is_writable_fonts_dir ? 'writable' : 'not writable' )
					: 'does not exist',
			),
		);

		// Add more filesystem checks.
		if ( defined( 'WPMU_PLUGIN_DIR' ) && is_dir( WPMU_PLUGIN_DIR ) ) {
			$is_writable_wpmu_plugin_dir = wp_is_writable( WPMU_PLUGIN_DIR );

			$fields['mu-plugins'] = array(
				'label' => __( 'The must use plugins directory' ),
				'value' => ( $is_writable_wpmu_plugin_dir ? __( 'Writable' ) : __( 'Not writable' ) ),
				'debug' => ( $is_writable_wpmu_plugin_dir ? 'writable' : 'not writable' ),
			);
		}

		return array(
			'label'       => __( 'Filesystem Permissions' ),
			'description' => __( 'Shows whether WordPress is able to write to the directories it needs access to.' ),
			'fields'      => $fields,
		);
	}

	/**
	 * Returns the value of a MySQL system variable.
	 *
	 * @since 5.9.0
	 *
	 * @global wpdb $wpdb WordPress database abstraction object.
	 *
	 * @param string $mysql_var Name of the MySQL system variable.
	 * @return string|null The variable value on success. Null if the variable does not exist.
	 */
	public static function get_mysql_var( $mysql_var ) {
		global $wpdb;

		$result = $wpdb->get_row(
			$wpdb->prepare( 'SHOW VARIABLES LIKE %s', $mysql_var ),
			ARRAY_A
		);

		if ( ! empty( $result ) && array_key_exists( 'Value', $result ) ) {
			return $result['Value'];
		}

		return null;
	}

	/**
	 * Formats the information gathered for debugging, in a manner suitable for copying to a forum or support ticket.
	 *
	 * @since 5.2.0
	 *
	 * @param array  $info_array Information gathered from the `WP_Debug_Data::debug_data()` function.
	 * @param string $data_type  The data type to return, either 'info' or 'debug'.
	 * @return string The formatted data.
	 */
	public static function format( $info_array, $data_type ) {
		$return = "`\n";

		foreach ( $info_array as $section => $details ) {
			// Skip this section if there are no fields, or the section has been declared as private.
			if ( empty( $details['fields'] ) || ( isset( $details['private'] ) && $details['private'] ) ) {
				continue;
			}

			$section_label = 'debug' === $data_type ? $section : $details['label'];

			$return .= sprintf(
				"### %s%s ###\n\n",
				$section_label,
				( isset( $details['show_count'] ) && $details['show_count'] ? sprintf( ' (%d)', count( $details['fields'] ) ) : '' )
			);

			foreach ( $details['fields'] as $field_name => $field ) {
				if ( isset( $field['private'] ) && true === $field['private'] ) {
					continue;
				}

				if ( 'debug' === $data_type && isset( $field['debug'] ) ) {
					$debug_data = $field['debug'];
				} else {
					$debug_data = $field['value'];
				}

				// Can be array, one level deep only.
				if ( is_array( $debug_data ) ) {
					$value = '';

					foreach ( $debug_data as $sub_field_name => $sub_field_value ) {
						$value .= sprintf( "\n\t%s: %s", $sub_field_name, $sub_field_value );
					}
				} elseif ( is_bool( $debug_data ) ) {
					$value = $debug_data ? 'true' : 'false';
				} elseif ( empty( $debug_data ) && '0' !== $debug_data ) {
					$value = 'undefined';
				} else {
					$value = $debug_data;
				}

				if ( 'debug' === $data_type ) {
					$label = $field_name;
				} else {
					$label = $field['label'];
				}

				$return .= sprintf( "%s: %s\n", $label, $value );
			}

			$return .= "\n";
		}

		$return .= '`';

		return $return;
	}

	/**
	 * Fetches the total size of all the database tables for the active database user.
	 *
	 * @since 5.2.0
	 *
	 * @global wpdb $wpdb WordPress database abstraction object.
	 *
	 * @return int The size of the database, in bytes.
	 */
	public static function get_database_size() {
		global $wpdb;
		$size = 0;
		$rows = $wpdb->get_results( 'SHOW TABLE STATUS', ARRAY_A );

		if ( $wpdb->num_rows > 0 ) {
			foreach ( $rows as $row ) {
				$size += $row['Data_length'] + $row['Index_length'];
			}
		}

		return (int) $size;
	}

	/**
	 * Fetches the sizes of the WordPress directories: `wordpress` (ABSPATH), `plugins`, `themes`, and `uploads`.
	 * Intended to supplement the array returned by `WP_Debug_Data::debug_data()`.
	 *
	 * @since 5.2.0
	 *
	 * @return array The sizes of the directories, also the database size and total installation size.
	 */
	public static function get_sizes() {
		$size_db    = self::get_database_size();
		$upload_dir = wp_get_upload_dir();

		/*
		 * We will be using the PHP max execution time to prevent the size calculations
		 * from causing a timeout. The default value is 30 seconds, and some
		 * hosts do not allow you to read configuration values.
		 */
		if ( function_exists( 'ini_get' ) ) {
			$max_execution_time = ini_get( 'max_execution_time' );
		}

		/*
		 * The max_execution_time defaults to 0 when PHP runs from cli.
		 * We still want to limit it below.
		 */
		if ( empty( $max_execution_time ) ) {
			$max_execution_time = 30; // 30 seconds.
		}

		if ( $max_execution_time > 20 ) {
			/*
			 * If the max_execution_time is set to lower than 20 seconds, reduce it a bit to prevent
			 * edge-case timeouts that may happen after the size loop has finished running.
			 */
			$max_execution_time -= 2;
		}

		/*
		 * Go through the various installation directories and calculate their sizes.
		 * No trailing slashes.
		 */
		$paths = array(
			'wordpress_size' => untrailingslashit( ABSPATH ),
			'themes_size'    => get_theme_root(),
			'plugins_size'   => WP_PLUGIN_DIR,
			'uploads_size'   => $upload_dir['basedir'],
			'fonts_size'     => wp_get_font_dir()['basedir'],
		);

		$exclude = $paths;
		unset( $exclude['wordpress_size'] );
		$exclude = array_values( $exclude );

		$size_total = 0;
		$all_sizes  = array();

		// Loop over all the directories we want to gather the sizes for.
		foreach ( $paths as $name => $path ) {
			$dir_size = null; // Default to timeout.
			$results  = array(
				'path' => $path,
				'raw'  => 0,
			);

			// If the directory does not exist, skip checking it, as it will skew the other results.
			if ( ! is_dir( $path ) ) {
				$all_sizes[ $name ] = array(
					'path'  => $path,
					'raw'   => 0,
					'size'  => __( 'The directory does not exist.' ),
					'debug' => 'directory not found',
				);

				continue;
			}

			if ( microtime( true ) - WP_START_TIMESTAMP < $max_execution_time ) {
				if ( 'wordpress_size' === $name ) {
					$dir_size = recurse_dirsize( $path, $exclude, $max_execution_time );
				} else {
					$dir_size = recurse_dirsize( $path, null, $max_execution_time );
				}
			}

			if ( false === $dir_size ) {
				// Error reading.
				$results['size']  = __( 'The size cannot be calculated. The directory is not accessible. Usually caused by invalid permissions.' );
				$results['debug'] = 'not accessible';

				// Stop total size calculation.
				$size_total = null;
			} elseif ( null === $dir_size ) {
				// Timeout.
				$results['size']  = __( 'The directory size calculation has timed out. Usually caused by a very large number of sub-directories and files.' );
				$results['debug'] = 'timeout while calculating size';

				// Stop total size calculation.
				$size_total = null;
			} else {
				if ( null !== $size_total ) {
					$size_total += $dir_size;
				}

				$results['raw']   = $dir_size;
				$results['size']  = size_format( $dir_size, 2 );
				$results['debug'] = $results['size'] . " ({$dir_size} bytes)";
			}

			$all_sizes[ $name ] = $results;
		}

		if ( $size_db > 0 ) {
			$database_size = size_format( $size_db, 2 );

			$all_sizes['database_size'] = array(
				'raw'   => $size_db,
				'size'  => $database_size,
				'debug' => $database_size . " ({$size_db} bytes)",
			);
		} else {
			$all_sizes['database_size'] = array(
				'size'  => __( 'Not available' ),
				'debug' => 'not available',
			);
		}

		if ( null !== $size_total && $size_db > 0 ) {
			$total_size    = $size_total + $size_db;
			$total_size_mb = size_format( $total_size, 2 );

			$all_sizes['total_size'] = array(
				'raw'   => $total_size,
				'size'  => $total_size_mb,
				'debug' => $total_size_mb . " ({$total_size} bytes)",
			);
		} else {
			$all_sizes['total_size'] = array(
				'size'  => __( 'Total size is not available. Some errors were encountered when determining the size of your installation.' ),
				'debug' => 'not available',
			);
		}

		return $all_sizes;
	}
}
options.php000060400000010233151327343360006752 0ustar00<?php
/**
 * WordPress Options Administration API.
 *
 * @package WordPress
 * @subpackage Administration
 * @since 4.4.0
 */

/**
 * Output JavaScript to toggle display of additional settings if avatars are disabled.
 *
 * @since 4.2.0
 */
function options_discussion_add_js() {
	?>
	<script>
	(function($){
		var parent = $( '#show_avatars' ),
			children = $( '.avatar-settings' );
		parent.on( 'change', function(){
			children.toggleClass( 'hide-if-js', ! this.checked );
		});
	})(jQuery);
	</script>
	<?php
}

/**
 * Display JavaScript on the page.
 *
 * @since 3.5.0
 */
function options_general_add_js() {
	?>
<script type="text/javascript">
	jQuery( function($) {
		var $siteName = $( '#wp-admin-bar-site-name' ).children( 'a' ).first(),
			$siteIconPreview = $('#site-icon-preview-site-title'),
			homeURL = ( <?php echo wp_json_encode( get_home_url() ); ?> || '' ).replace( /^(https?:\/\/)?(www\.)?/, '' );

		$( '#blogname' ).on( 'input', function() {
			var title = $.trim( $( this ).val() ) || homeURL;

			// Truncate to 40 characters.
			if ( 40 < title.length ) {
				title = title.substring( 0, 40 ) + '\u2026';
			}

			$siteName.text( title );
			$siteIconPreview.text( title );
		});

		$( 'input[name="date_format"]' ).on( 'click', function() {
			if ( 'date_format_custom_radio' !== $(this).attr( 'id' ) )
				$( 'input[name="date_format_custom"]' ).val( $( this ).val() ).closest( 'fieldset' ).find( '.example' ).text( $( this ).parent( 'label' ).children( '.format-i18n' ).text() );
		});

		$( 'input[name="date_format_custom"]' ).on( 'click input', function() {
			$( '#date_format_custom_radio' ).prop( 'checked', true );
		});

		$( 'input[name="time_format"]' ).on( 'click', function() {
			if ( 'time_format_custom_radio' !== $(this).attr( 'id' ) )
				$( 'input[name="time_format_custom"]' ).val( $( this ).val() ).closest( 'fieldset' ).find( '.example' ).text( $( this ).parent( 'label' ).children( '.format-i18n' ).text() );
		});

		$( 'input[name="time_format_custom"]' ).on( 'click input', function() {
			$( '#time_format_custom_radio' ).prop( 'checked', true );
		});

		$( 'input[name="date_format_custom"], input[name="time_format_custom"]' ).on( 'input', function() {
			var format = $( this ),
				fieldset = format.closest( 'fieldset' ),
				example = fieldset.find( '.example' ),
				spinner = fieldset.find( '.spinner' );

			// Debounce the event callback while users are typing.
			clearTimeout( $.data( this, 'timer' ) );
			$( this ).data( 'timer', setTimeout( function() {
				// If custom date is not empty.
				if ( format.val() ) {
					spinner.addClass( 'is-active' );

					$.post( ajaxurl, {
						action: 'date_format_custom' === format.attr( 'name' ) ? 'date_format' : 'time_format',
						date 	: format.val()
					}, function( d ) { spinner.removeClass( 'is-active' ); example.text( d ); } );
				}
			}, 500 ) );
		} );

		var languageSelect = $( '#WPLANG' );
		$( 'form' ).on( 'submit', function() {
			/*
			 * Don't show a spinner for English and installed languages,
			 * as there is nothing to download.
			 */
			if ( ! languageSelect.find( 'option:selected' ).data( 'installed' ) ) {
				$( '#submit', this ).after( '<span class="spinner language-install-spinner is-active" />' );
			}
		});
	} );
</script>
	<?php
}

/**
 * Display JavaScript on the page.
 *
 * @since 3.5.0
 */
function options_reading_add_js() {
	?>
<script type="text/javascript">
	jQuery( function($) {
		var section = $('#front-static-pages'),
			staticPage = section.find('input:radio[value="page"]'),
			selects = section.find('select'),
			check_disabled = function(){
				selects.prop( 'disabled', ! staticPage.prop('checked') );
			};
		check_disabled();
		section.find( 'input:radio' ).on( 'change', check_disabled );
	} );
</script>
	<?php
}

/**
 * Render the site charset setting.
 *
 * @since 3.5.0
 */
function options_reading_blog_charset() {
	echo '<input name="blog_charset" type="text" id="blog_charset" value="' . esc_attr( get_option( 'blog_charset' ) ) . '" class="regular-text" />';
	echo '<p class="description">' . __( 'The <a href="https://wordpress.org/documentation/article/wordpress-glossary/#character-set">character encoding</a> of your site (UTF-8 is recommended)' ) . '</p>';
}
class-wp-ms-sites-list-table.php000064400000053003151327343360012616 0ustar00<?php
/**
 * List Table API: WP_MS_Sites_List_Table class
 *
 * @package WordPress
 * @subpackage Administration
 * @since 3.1.0
 */

/**
 * Core class used to implement displaying sites in a list table for the network admin.
 *
 * @since 3.1.0
 *
 * @see WP_List_Table
 */
class WP_MS_Sites_List_Table extends WP_List_Table {

	/**
	 * Site status list.
	 *
	 * @since 4.3.0
	 * @var array
	 */
	public $status_list;

	/**
	 * Constructor.
	 *
	 * @since 3.1.0
	 *
	 * @see WP_List_Table::__construct() for more information on default arguments.
	 *
	 * @param array $args An associative array of arguments.
	 */
	public function __construct( $args = array() ) {
		$this->status_list = array(
			'archived' => array( 'site-archived', __( 'Archived' ) ),
			'spam'     => array( 'site-spammed', _x( 'Spam', 'site' ) ),
			'deleted'  => array( 'site-deleted', __( 'Deleted' ) ),
			'mature'   => array( 'site-mature', __( 'Mature' ) ),
		);

		parent::__construct(
			array(
				'plural' => 'sites',
				'screen' => isset( $args['screen'] ) ? $args['screen'] : null,
			)
		);
	}

	/**
	 * @return bool
	 */
	public function ajax_user_can() {
		return current_user_can( 'manage_sites' );
	}

	/**
	 * Prepares the list of sites for display.
	 *
	 * @since 3.1.0
	 *
	 * @global string $mode List table view mode.
	 * @global string $s
	 * @global wpdb   $wpdb WordPress database abstraction object.
	 */
	public function prepare_items() {
		global $mode, $s, $wpdb;

		if ( ! empty( $_REQUEST['mode'] ) ) {
			$mode = 'excerpt' === $_REQUEST['mode'] ? 'excerpt' : 'list';
			set_user_setting( 'sites_list_mode', $mode );
		} else {
			$mode = get_user_setting( 'sites_list_mode', 'list' );
		}

		$per_page = $this->get_items_per_page( 'sites_network_per_page' );

		$pagenum = $this->get_pagenum();

		$s    = isset( $_REQUEST['s'] ) ? wp_unslash( trim( $_REQUEST['s'] ) ) : '';
		$wild = '';
		if ( str_contains( $s, '*' ) ) {
			$wild = '*';
			$s    = trim( $s, '*' );
		}

		/*
		 * If the network is large and a search is not being performed, show only
		 * the latest sites with no paging in order to avoid expensive count queries.
		 */
		if ( ! $s && wp_is_large_network() ) {
			if ( ! isset( $_REQUEST['orderby'] ) ) {
				$_GET['orderby']     = '';
				$_REQUEST['orderby'] = '';
			}
			if ( ! isset( $_REQUEST['order'] ) ) {
				$_GET['order']     = 'DESC';
				$_REQUEST['order'] = 'DESC';
			}
		}

		$args = array(
			'number'     => (int) $per_page,
			'offset'     => (int) ( ( $pagenum - 1 ) * $per_page ),
			'network_id' => get_current_network_id(),
		);

		if ( empty( $s ) ) {
			// Nothing to do.
		} elseif ( preg_match( '/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/', $s )
			|| preg_match( '/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.?$/', $s )
			|| preg_match( '/^[0-9]{1,3}\.[0-9]{1,3}\.?$/', $s )
			|| preg_match( '/^[0-9]{1,3}\.$/', $s )
		) {
			// IPv4 address.
			$reg_blog_ids = $wpdb->get_col(
				$wpdb->prepare(
					"SELECT blog_id FROM {$wpdb->registration_log} WHERE {$wpdb->registration_log}.IP LIKE %s",
					$wpdb->esc_like( $s ) . ( ! empty( $wild ) ? '%' : '' )
				)
			);

			if ( $reg_blog_ids ) {
				$args['site__in'] = $reg_blog_ids;
			}
		} elseif ( is_numeric( $s ) && empty( $wild ) ) {
			$args['ID'] = $s;
		} else {
			$args['search'] = $s;

			if ( ! is_subdomain_install() ) {
				$args['search_columns'] = array( 'path' );
			}
		}

		$order_by = isset( $_REQUEST['orderby'] ) ? $_REQUEST['orderby'] : '';
		if ( 'registered' === $order_by ) {
			// 'registered' is a valid field name.
		} elseif ( 'lastupdated' === $order_by ) {
			$order_by = 'last_updated';
		} elseif ( 'blogname' === $order_by ) {
			if ( is_subdomain_install() ) {
				$order_by = 'domain';
			} else {
				$order_by = 'path';
			}
		} elseif ( 'blog_id' === $order_by ) {
			$order_by = 'id';
		} elseif ( ! $order_by ) {
			$order_by = false;
		}

		$args['orderby'] = $order_by;

		if ( $order_by ) {
			$args['order'] = ( isset( $_REQUEST['order'] ) && 'DESC' === strtoupper( $_REQUEST['order'] ) ) ? 'DESC' : 'ASC';
		}

		if ( wp_is_large_network() ) {
			$args['no_found_rows'] = true;
		} else {
			$args['no_found_rows'] = false;
		}

		// Take into account the role the user has selected.
		$status = isset( $_REQUEST['status'] ) ? wp_unslash( trim( $_REQUEST['status'] ) ) : '';
		if ( in_array( $status, array( 'public', 'archived', 'mature', 'spam', 'deleted' ), true ) ) {
			$args[ $status ] = 1;
		}

		/**
		 * Filters the arguments for the site query in the sites list table.
		 *
		 * @since 4.6.0
		 *
		 * @param array $args An array of get_sites() arguments.
		 */
		$args = apply_filters( 'ms_sites_list_table_query_args', $args );

		$_sites = get_sites( $args );
		if ( is_array( $_sites ) ) {
			update_site_cache( $_sites );

			$this->items = array_slice( $_sites, 0, $per_page );
		}

		$total_sites = get_sites(
			array_merge(
				$args,
				array(
					'count'  => true,
					'offset' => 0,
					'number' => 0,
				)
			)
		);

		$this->set_pagination_args(
			array(
				'total_items' => $total_sites,
				'per_page'    => $per_page,
			)
		);
	}

	/**
	 */
	public function no_items() {
		_e( 'No sites found.' );
	}

	/**
	 * Gets links to filter sites by status.
	 *
	 * @since 5.3.0
	 *
	 * @return array
	 */
	protected function get_views() {
		$counts = wp_count_sites();

		$statuses = array(
			/* translators: %s: Number of sites. */
			'all'      => _nx_noop(
				'All <span class="count">(%s)</span>',
				'All <span class="count">(%s)</span>',
				'sites'
			),

			/* translators: %s: Number of sites. */
			'public'   => _n_noop(
				'Public <span class="count">(%s)</span>',
				'Public <span class="count">(%s)</span>'
			),

			/* translators: %s: Number of sites. */
			'archived' => _n_noop(
				'Archived <span class="count">(%s)</span>',
				'Archived <span class="count">(%s)</span>'
			),

			/* translators: %s: Number of sites. */
			'mature'   => _n_noop(
				'Mature <span class="count">(%s)</span>',
				'Mature <span class="count">(%s)</span>'
			),

			/* translators: %s: Number of sites. */
			'spam'     => _nx_noop(
				'Spam <span class="count">(%s)</span>',
				'Spam <span class="count">(%s)</span>',
				'sites'
			),

			/* translators: %s: Number of sites. */
			'deleted'  => _n_noop(
				'Deleted <span class="count">(%s)</span>',
				'Deleted <span class="count">(%s)</span>'
			),
		);

		$view_links       = array();
		$requested_status = isset( $_REQUEST['status'] ) ? wp_unslash( trim( $_REQUEST['status'] ) ) : '';
		$url              = 'sites.php';

		foreach ( $statuses as $status => $label_count ) {
			if ( (int) $counts[ $status ] > 0 ) {
				$label = sprintf(
					translate_nooped_plural( $label_count, $counts[ $status ] ),
					number_format_i18n( $counts[ $status ] )
				);

				$full_url = 'all' === $status ? $url : add_query_arg( 'status', $status, $url );

				$view_links[ $status ] = array(
					'url'     => esc_url( $full_url ),
					'label'   => $label,
					'current' => $requested_status === $status || ( '' === $requested_status && 'all' === $status ),
				);
			}
		}

		return $this->get_views_links( $view_links );
	}

	/**
	 * @return array
	 */
	protected function get_bulk_actions() {
		$actions = array();
		if ( current_user_can( 'delete_sites' ) ) {
			$actions['delete'] = __( 'Delete' );
		}
		$actions['spam']    = _x( 'Mark as spam', 'site' );
		$actions['notspam'] = _x( 'Not spam', 'site' );

		return $actions;
	}

	/**
	 * @global string $mode List table view mode.
	 *
	 * @param string $which The location of the pagination nav markup: Either 'top' or 'bottom'.
	 */
	protected function pagination( $which ) {
		global $mode;

		parent::pagination( $which );

		if ( 'top' === $which ) {
			$this->view_switcher( $mode );
		}
	}

	/**
	 * Displays extra controls between bulk actions and pagination.
	 *
	 * @since 5.3.0
	 *
	 * @param string $which The location of the extra table nav markup: Either 'top' or 'bottom'.
	 */
	protected function extra_tablenav( $which ) {
		?>
		<div class="alignleft actions">
		<?php
		if ( 'top' === $which ) {
			ob_start();

			/**
			 * Fires before the Filter button on the MS sites list table.
			 *
			 * @since 5.3.0
			 *
			 * @param string $which The location of the extra table nav markup: Either 'top' or 'bottom'.
			 */
			do_action( 'restrict_manage_sites', $which );

			$output = ob_get_clean();

			if ( ! empty( $output ) ) {
				echo $output;
				submit_button( __( 'Filter' ), '', 'filter_action', false, array( 'id' => 'site-query-submit' ) );
			}
		}
		?>
		</div>
		<?php
		/**
		 * Fires immediately following the closing "actions" div in the tablenav for the
		 * MS sites list table.
		 *
		 * @since 5.3.0
		 *
		 * @param string $which The location of the extra table nav markup: Either 'top' or 'bottom'.
		 */
		do_action( 'manage_sites_extra_tablenav', $which );
	}

	/**
	 * @return string[] Array of column titles keyed by their column name.
	 */
	public function get_columns() {
		$sites_columns = array(
			'cb'          => '<input type="checkbox" />',
			'blogname'    => __( 'URL' ),
			'lastupdated' => __( 'Last Updated' ),
			'registered'  => _x( 'Registered', 'site' ),
			'users'       => __( 'Users' ),
		);

		if ( has_filter( 'wpmublogsaction' ) ) {
			$sites_columns['plugins'] = __( 'Actions' );
		}

		/**
		 * Filters the displayed site columns in Sites list table.
		 *
		 * @since MU (3.0.0)
		 *
		 * @param string[] $sites_columns An array of displayed site columns. Default 'cb',
		 *                               'blogname', 'lastupdated', 'registered', 'users'.
		 */
		return apply_filters( 'wpmu_blogs_columns', $sites_columns );
	}

	/**
	 * @return array
	 */
	protected function get_sortable_columns() {

		if ( is_subdomain_install() ) {
			$blogname_abbr         = __( 'Domain' );
			$blogname_orderby_text = __( 'Table ordered by Site Domain Name.' );
		} else {
			$blogname_abbr         = __( 'Path' );
			$blogname_orderby_text = __( 'Table ordered by Site Path.' );
		}

		return array(
			'blogname'    => array( 'blogname', false, $blogname_abbr, $blogname_orderby_text ),
			'lastupdated' => array( 'lastupdated', true, __( 'Last Updated' ), __( 'Table ordered by Last Updated.' ) ),
			'registered'  => array( 'blog_id', true, _x( 'Registered', 'site' ), __( 'Table ordered by Site Registered Date.' ), 'desc' ),
		);
	}

	/**
	 * Handles the checkbox column output.
	 *
	 * @since 4.3.0
	 * @since 5.9.0 Renamed `$blog` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param array $item Current site.
	 */
	public function column_cb( $item ) {
		// Restores the more descriptive, specific name for use within this method.
		$blog = $item;

		if ( ! is_main_site( $blog['blog_id'] ) ) :
			$blogname = untrailingslashit( $blog['domain'] . $blog['path'] );
			?>
			<input type="checkbox" id="blog_<?php echo $blog['blog_id']; ?>" name="allblogs[]" value="<?php echo esc_attr( $blog['blog_id'] ); ?>" />
			<label for="blog_<?php echo $blog['blog_id']; ?>">
				<span class="screen-reader-text">
				<?php
				/* translators: %s: Site URL. */
				printf( __( 'Select %s' ), $blogname );
				?>
				</span>
			</label>
			<?php
		endif;
	}

	/**
	 * Handles the ID column output.
	 *
	 * @since 4.4.0
	 *
	 * @param array $blog Current site.
	 */
	public function column_id( $blog ) {
		echo $blog['blog_id'];
	}

	/**
	 * Handles the site name column output.
	 *
	 * @since 4.3.0
	 *
	 * @global string $mode List table view mode.
	 *
	 * @param array $blog Current site.
	 */
	public function column_blogname( $blog ) {
		global $mode;

		$blogname = untrailingslashit( $blog['domain'] . $blog['path'] );

		?>
		<strong>
			<?php
			printf(
				'<a href="%1$s" class="edit">%2$s</a>',
				esc_url( network_admin_url( 'site-info.php?id=' . $blog['blog_id'] ) ),
				$blogname
			);

			$this->site_states( $blog );
			?>
		</strong>
		<?php
		if ( 'list' !== $mode ) {
			switch_to_blog( $blog['blog_id'] );
			echo '<p>';
			printf(
				/* translators: 1: Site title, 2: Site tagline. */
				__( '%1$s &#8211; %2$s' ),
				get_option( 'blogname' ),
				'<em>' . get_option( 'blogdescription' ) . '</em>'
			);
			echo '</p>';
			restore_current_blog();
		}
	}

	/**
	 * Handles the lastupdated column output.
	 *
	 * @since 4.3.0
	 *
	 * @global string $mode List table view mode.
	 *
	 * @param array $blog Current site.
	 */
	public function column_lastupdated( $blog ) {
		global $mode;

		if ( 'list' === $mode ) {
			$date = __( 'Y/m/d' );
		} else {
			$date = __( 'Y/m/d g:i:s a' );
		}

		if ( '0000-00-00 00:00:00' === $blog['last_updated'] ) {
			_e( 'Never' );
		} else {
			echo mysql2date( $date, $blog['last_updated'] );
		}
	}

	/**
	 * Handles the registered column output.
	 *
	 * @since 4.3.0
	 *
	 * @global string $mode List table view mode.
	 *
	 * @param array $blog Current site.
	 */
	public function column_registered( $blog ) {
		global $mode;

		if ( 'list' === $mode ) {
			$date = __( 'Y/m/d' );
		} else {
			$date = __( 'Y/m/d g:i:s a' );
		}

		if ( '0000-00-00 00:00:00' === $blog['registered'] ) {
			echo '&#x2014;';
		} else {
			echo mysql2date( $date, $blog['registered'] );
		}
	}

	/**
	 * Handles the users column output.
	 *
	 * @since 4.3.0
	 *
	 * @param array $blog Current site.
	 */
	public function column_users( $blog ) {
		$user_count = wp_cache_get( $blog['blog_id'] . '_user_count', 'blog-details' );
		if ( ! $user_count ) {
			$blog_users = new WP_User_Query(
				array(
					'blog_id'     => $blog['blog_id'],
					'fields'      => 'ID',
					'number'      => 1,
					'count_total' => true,
				)
			);
			$user_count = $blog_users->get_total();
			wp_cache_set( $blog['blog_id'] . '_user_count', $user_count, 'blog-details', 12 * HOUR_IN_SECONDS );
		}

		printf(
			'<a href="%1$s">%2$s</a>',
			esc_url( network_admin_url( 'site-users.php?id=' . $blog['blog_id'] ) ),
			number_format_i18n( $user_count )
		);
	}

	/**
	 * Handles the plugins column output.
	 *
	 * @since 4.3.0
	 *
	 * @param array $blog Current site.
	 */
	public function column_plugins( $blog ) {
		if ( has_filter( 'wpmublogsaction' ) ) {
			/**
			 * Fires inside the auxiliary 'Actions' column of the Sites list table.
			 *
			 * By default this column is hidden unless something is hooked to the action.
			 *
			 * @since MU (3.0.0)
			 *
			 * @param int $blog_id The site ID.
			 */
			do_action( 'wpmublogsaction', $blog['blog_id'] );
		}
	}

	/**
	 * Handles output for the default column.
	 *
	 * @since 4.3.0
	 * @since 5.9.0 Renamed `$blog` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param array  $item        Current site.
	 * @param string $column_name Current column name.
	 */
	public function column_default( $item, $column_name ) {
		// Restores the more descriptive, specific name for use within this method.
		$blog = $item;

		/**
		 * Fires for each registered custom column in the Sites list table.
		 *
		 * @since 3.1.0
		 *
		 * @param string $column_name The name of the column to display.
		 * @param int    $blog_id     The site ID.
		 */
		do_action( 'manage_sites_custom_column', $column_name, $blog['blog_id'] );
	}

	/**
	 * Generates the list table rows.
	 *
	 * @since 3.1.0
	 */
	public function display_rows() {
		foreach ( $this->items as $blog ) {
			$blog  = $blog->to_array();
			$class = '';
			reset( $this->status_list );

			foreach ( $this->status_list as $status => $col ) {
				if ( '1' === $blog[ $status ] ) {
					$class = " class='{$col[0]}'";
				}
			}

			echo "<tr{$class}>";

			$this->single_row_columns( $blog );

			echo '</tr>';
		}
	}

	/**
	 * Determines whether to output comma-separated site states.
	 *
	 * @since 5.3.0
	 *
	 * @param array $site
	 */
	protected function site_states( $site ) {
		$site_states = array();

		// $site is still an array, so get the object.
		$_site = WP_Site::get_instance( $site['blog_id'] );

		if ( is_main_site( $_site->id ) ) {
			$site_states['main'] = __( 'Main' );
		}

		reset( $this->status_list );

		$site_status = isset( $_REQUEST['status'] ) ? wp_unslash( trim( $_REQUEST['status'] ) ) : '';
		foreach ( $this->status_list as $status => $col ) {
			if ( '1' === $_site->{$status} && $site_status !== $status ) {
				$site_states[ $col[0] ] = $col[1];
			}
		}

		/**
		 * Filters the default site display states for items in the Sites list table.
		 *
		 * @since 5.3.0
		 *
		 * @param string[] $site_states An array of site states. Default 'Main',
		 *                              'Archived', 'Mature', 'Spam', 'Deleted'.
		 * @param WP_Site  $site        The current site object.
		 */
		$site_states = apply_filters( 'display_site_states', $site_states, $_site );

		if ( ! empty( $site_states ) ) {
			$state_count = count( $site_states );

			$i = 0;

			echo ' &mdash; ';

			foreach ( $site_states as $state ) {
				++$i;

				$separator = ( $i < $state_count ) ? ', ' : '';

				echo "<span class='post-state'>{$state}{$separator}</span>";
			}
		}
	}

	/**
	 * Gets the name of the default primary column.
	 *
	 * @since 4.3.0
	 *
	 * @return string Name of the default primary column, in this case, 'blogname'.
	 */
	protected function get_default_primary_column_name() {
		return 'blogname';
	}

	/**
	 * Generates and displays row action links.
	 *
	 * @since 4.3.0
	 * @since 5.9.0 Renamed `$blog` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param array  $item        Site being acted upon.
	 * @param string $column_name Current column name.
	 * @param string $primary     Primary column name.
	 * @return string Row actions output for sites in Multisite, or an empty string
	 *                if the current column is not the primary column.
	 */
	protected function handle_row_actions( $item, $column_name, $primary ) {
		if ( $primary !== $column_name ) {
			return '';
		}

		// Restores the more descriptive, specific name for use within this method.
		$blog = $item;

		$blogname = untrailingslashit( $blog['domain'] . $blog['path'] );

		// Preordered.
		$actions = array(
			'edit'       => '',
			'backend'    => '',
			'activate'   => '',
			'deactivate' => '',
			'archive'    => '',
			'unarchive'  => '',
			'spam'       => '',
			'unspam'     => '',
			'delete'     => '',
			'visit'      => '',
		);

		$actions['edit'] = sprintf(
			'<a href="%1$s">%2$s</a>',
			esc_url( network_admin_url( 'site-info.php?id=' . $blog['blog_id'] ) ),
			__( 'Edit' )
		);

		$actions['backend'] = sprintf(
			'<a href="%1$s" class="edit">%2$s</a>',
			esc_url( get_admin_url( $blog['blog_id'] ) ),
			__( 'Dashboard' )
		);

		if ( ! is_main_site( $blog['blog_id'] ) ) {
			if ( '1' === $blog['deleted'] ) {
				$actions['activate'] = sprintf(
					'<a href="%1$s">%2$s</a>',
					esc_url(
						wp_nonce_url(
							network_admin_url( 'sites.php?action=confirm&amp;action2=activateblog&amp;id=' . $blog['blog_id'] ),
							'activateblog_' . $blog['blog_id']
						)
					),
					_x( 'Activate', 'site' )
				);
			} else {
				$actions['deactivate'] = sprintf(
					'<a href="%1$s">%2$s</a>',
					esc_url(
						wp_nonce_url(
							network_admin_url( 'sites.php?action=confirm&amp;action2=deactivateblog&amp;id=' . $blog['blog_id'] ),
							'deactivateblog_' . $blog['blog_id']
						)
					),
					__( 'Deactivate' )
				);
			}

			if ( '1' === $blog['archived'] ) {
				$actions['unarchive'] = sprintf(
					'<a href="%1$s">%2$s</a>',
					esc_url(
						wp_nonce_url(
							network_admin_url( 'sites.php?action=confirm&amp;action2=unarchiveblog&amp;id=' . $blog['blog_id'] ),
							'unarchiveblog_' . $blog['blog_id']
						)
					),
					__( 'Unarchive' )
				);
			} else {
				$actions['archive'] = sprintf(
					'<a href="%1$s">%2$s</a>',
					esc_url(
						wp_nonce_url(
							network_admin_url( 'sites.php?action=confirm&amp;action2=archiveblog&amp;id=' . $blog['blog_id'] ),
							'archiveblog_' . $blog['blog_id']
						)
					),
					_x( 'Archive', 'verb; site' )
				);
			}

			if ( '1' === $blog['spam'] ) {
				$actions['unspam'] = sprintf(
					'<a href="%1$s">%2$s</a>',
					esc_url(
						wp_nonce_url(
							network_admin_url( 'sites.php?action=confirm&amp;action2=unspamblog&amp;id=' . $blog['blog_id'] ),
							'unspamblog_' . $blog['blog_id']
						)
					),
					_x( 'Not Spam', 'site' )
				);
			} else {
				$actions['spam'] = sprintf(
					'<a href="%1$s">%2$s</a>',
					esc_url(
						wp_nonce_url(
							network_admin_url( 'sites.php?action=confirm&amp;action2=spamblog&amp;id=' . $blog['blog_id'] ),
							'spamblog_' . $blog['blog_id']
						)
					),
					_x( 'Spam', 'site' )
				);
			}

			if ( current_user_can( 'delete_site', $blog['blog_id'] ) ) {
				$actions['delete'] = sprintf(
					'<a href="%1$s">%2$s</a>',
					esc_url(
						wp_nonce_url(
							network_admin_url( 'sites.php?action=confirm&amp;action2=deleteblog&amp;id=' . $blog['blog_id'] ),
							'deleteblog_' . $blog['blog_id']
						)
					),
					__( 'Delete' )
				);
			}
		}

		$actions['visit'] = sprintf(
			'<a href="%1$s" rel="bookmark">%2$s</a>',
			esc_url( get_home_url( $blog['blog_id'], '/' ) ),
			__( 'Visit' )
		);

		/**
		 * Filters the action links displayed for each site in the Sites list table.
		 *
		 * The 'Edit', 'Dashboard', 'Delete', and 'Visit' links are displayed by
		 * default for each site. The site's status determines whether to show the
		 * 'Activate' or 'Deactivate' link, 'Unarchive' or 'Archive' links, and
		 * 'Not Spam' or 'Spam' link for each site.
		 *
		 * @since 3.1.0
		 *
		 * @param string[] $actions  An array of action links to be displayed.
		 * @param int      $blog_id  The site ID.
		 * @param string   $blogname Site path, formatted depending on whether it is a sub-domain
		 *                           or subdirectory multisite installation.
		 */
		$actions = apply_filters( 'manage_sites_action_links', array_filter( $actions ), $blog['blog_id'], $blogname );

		return $this->row_actions( $actions );
	}
}
class-wp-comments-list-table.php000064400000100440151327343360012675 0ustar00<?php
/**
 * List Table API: WP_Comments_List_Table class
 *
 * @package WordPress
 * @subpackage Administration
 * @since 3.1.0
 */

/**
 * Core class used to implement displaying comments in a list table.
 *
 * @since 3.1.0
 *
 * @see WP_List_Table
 */
class WP_Comments_List_Table extends WP_List_Table {

	public $checkbox = true;

	public $pending_count = array();

	public $extra_items;

	private $user_can;

	/**
	 * Constructor.
	 *
	 * @since 3.1.0
	 *
	 * @see WP_List_Table::__construct() for more information on default arguments.
	 *
	 * @global int $post_id
	 *
	 * @param array $args An associative array of arguments.
	 */
	public function __construct( $args = array() ) {
		global $post_id;

		$post_id = isset( $_REQUEST['p'] ) ? absint( $_REQUEST['p'] ) : 0;

		if ( get_option( 'show_avatars' ) ) {
			add_filter( 'comment_author', array( $this, 'floated_admin_avatar' ), 10, 2 );
		}

		parent::__construct(
			array(
				'plural'   => 'comments',
				'singular' => 'comment',
				'ajax'     => true,
				'screen'   => isset( $args['screen'] ) ? $args['screen'] : null,
			)
		);
	}

	/**
	 * Adds avatars to comment author names.
	 *
	 * @since 3.1.0
	 *
	 * @param string $name       Comment author name.
	 * @param int    $comment_id Comment ID.
	 * @return string Avatar with the user name.
	 */
	public function floated_admin_avatar( $name, $comment_id ) {
		$comment = get_comment( $comment_id );
		$avatar  = get_avatar( $comment, 32, 'mystery' );
		return "$avatar $name";
	}

	/**
	 * @return bool
	 */
	public function ajax_user_can() {
		return current_user_can( 'edit_posts' );
	}

	/**
	 * @global string $mode           List table view mode.
	 * @global int    $post_id
	 * @global string $comment_status
	 * @global string $comment_type
	 * @global string $search
	 */
	public function prepare_items() {
		global $mode, $post_id, $comment_status, $comment_type, $search;

		if ( ! empty( $_REQUEST['mode'] ) ) {
			$mode = 'excerpt' === $_REQUEST['mode'] ? 'excerpt' : 'list';
			set_user_setting( 'posts_list_mode', $mode );
		} else {
			$mode = get_user_setting( 'posts_list_mode', 'list' );
		}

		$comment_status = isset( $_REQUEST['comment_status'] ) ? $_REQUEST['comment_status'] : 'all';

		if ( ! in_array( $comment_status, array( 'all', 'mine', 'moderated', 'approved', 'spam', 'trash' ), true ) ) {
			$comment_status = 'all';
		}

		$comment_type = ! empty( $_REQUEST['comment_type'] ) ? $_REQUEST['comment_type'] : '';

		$search = ( isset( $_REQUEST['s'] ) ) ? $_REQUEST['s'] : '';

		$post_type = ( isset( $_REQUEST['post_type'] ) ) ? sanitize_key( $_REQUEST['post_type'] ) : '';

		$user_id = ( isset( $_REQUEST['user_id'] ) ) ? $_REQUEST['user_id'] : '';

		$orderby = ( isset( $_REQUEST['orderby'] ) ) ? $_REQUEST['orderby'] : '';
		$order   = ( isset( $_REQUEST['order'] ) ) ? $_REQUEST['order'] : '';

		$comments_per_page = $this->get_per_page( $comment_status );

		$doing_ajax = wp_doing_ajax();

		if ( isset( $_REQUEST['number'] ) ) {
			$number = (int) $_REQUEST['number'];
		} else {
			$number = $comments_per_page + min( 8, $comments_per_page ); // Grab a few extra.
		}

		$page = $this->get_pagenum();

		if ( isset( $_REQUEST['start'] ) ) {
			$start = $_REQUEST['start'];
		} else {
			$start = ( $page - 1 ) * $comments_per_page;
		}

		if ( $doing_ajax && isset( $_REQUEST['offset'] ) ) {
			$start += $_REQUEST['offset'];
		}

		$status_map = array(
			'mine'      => '',
			'moderated' => 'hold',
			'approved'  => 'approve',
			'all'       => '',
		);

		$args = array(
			'status'                    => isset( $status_map[ $comment_status ] ) ? $status_map[ $comment_status ] : $comment_status,
			'search'                    => $search,
			'user_id'                   => $user_id,
			'offset'                    => $start,
			'number'                    => $number,
			'post_id'                   => $post_id,
			'type'                      => $comment_type,
			'orderby'                   => $orderby,
			'order'                     => $order,
			'post_type'                 => $post_type,
			'update_comment_post_cache' => true,
		);

		/**
		 * Filters the arguments for the comment query in the comments list table.
		 *
		 * @since 5.1.0
		 *
		 * @param array $args An array of get_comments() arguments.
		 */
		$args = apply_filters( 'comments_list_table_query_args', $args );

		$_comments = get_comments( $args );

		if ( is_array( $_comments ) ) {
			$this->items       = array_slice( $_comments, 0, $comments_per_page );
			$this->extra_items = array_slice( $_comments, $comments_per_page );

			$_comment_post_ids = array_unique( wp_list_pluck( $_comments, 'comment_post_ID' ) );

			$this->pending_count = get_pending_comments_num( $_comment_post_ids );
		}

		$total_comments = get_comments(
			array_merge(
				$args,
				array(
					'count'   => true,
					'offset'  => 0,
					'number'  => 0,
					'orderby' => 'none',
				)
			)
		);

		$this->set_pagination_args(
			array(
				'total_items' => $total_comments,
				'per_page'    => $comments_per_page,
			)
		);
	}

	/**
	 * @param string $comment_status
	 * @return int
	 */
	public function get_per_page( $comment_status = 'all' ) {
		$comments_per_page = $this->get_items_per_page( 'edit_comments_per_page' );

		/**
		 * Filters the number of comments listed per page in the comments list table.
		 *
		 * @since 2.6.0
		 *
		 * @param int    $comments_per_page The number of comments to list per page.
		 * @param string $comment_status    The comment status name. Default 'All'.
		 */
		return apply_filters( 'comments_per_page', $comments_per_page, $comment_status );
	}

	/**
	 * @global string $comment_status
	 */
	public function no_items() {
		global $comment_status;

		if ( 'moderated' === $comment_status ) {
			_e( 'No comments awaiting moderation.' );
		} elseif ( 'trash' === $comment_status ) {
			_e( 'No comments found in Trash.' );
		} else {
			_e( 'No comments found.' );
		}
	}

	/**
	 * @global int $post_id
	 * @global string $comment_status
	 * @global string $comment_type
	 */
	protected function get_views() {
		global $post_id, $comment_status, $comment_type;

		$status_links = array();
		$num_comments = ( $post_id ) ? wp_count_comments( $post_id ) : wp_count_comments();

		$statuses = array(
			/* translators: %s: Number of comments. */
			'all'       => _nx_noop(
				'All <span class="count">(%s)</span>',
				'All <span class="count">(%s)</span>',
				'comments'
			), // Singular not used.

			/* translators: %s: Number of comments. */
			'mine'      => _nx_noop(
				'Mine <span class="count">(%s)</span>',
				'Mine <span class="count">(%s)</span>',
				'comments'
			),

			/* translators: %s: Number of comments. */
			'moderated' => _nx_noop(
				'Pending <span class="count">(%s)</span>',
				'Pending <span class="count">(%s)</span>',
				'comments'
			),

			/* translators: %s: Number of comments. */
			'approved'  => _nx_noop(
				'Approved <span class="count">(%s)</span>',
				'Approved <span class="count">(%s)</span>',
				'comments'
			),

			/* translators: %s: Number of comments. */
			'spam'      => _nx_noop(
				'Spam <span class="count">(%s)</span>',
				'Spam <span class="count">(%s)</span>',
				'comments'
			),

			/* translators: %s: Number of comments. */
			'trash'     => _nx_noop(
				'Trash <span class="count">(%s)</span>',
				'Trash <span class="count">(%s)</span>',
				'comments'
			),
		);

		if ( ! EMPTY_TRASH_DAYS ) {
			unset( $statuses['trash'] );
		}

		$link = admin_url( 'edit-comments.php' );

		if ( ! empty( $comment_type ) && 'all' !== $comment_type ) {
			$link = add_query_arg( 'comment_type', $comment_type, $link );
		}

		foreach ( $statuses as $status => $label ) {
			if ( 'mine' === $status ) {
				$current_user_id    = get_current_user_id();
				$num_comments->mine = get_comments(
					array(
						'post_id' => $post_id ? $post_id : 0,
						'user_id' => $current_user_id,
						'count'   => true,
						'orderby' => 'none',
					)
				);
				$link               = add_query_arg( 'user_id', $current_user_id, $link );
			} else {
				$link = remove_query_arg( 'user_id', $link );
			}

			if ( ! isset( $num_comments->$status ) ) {
				$num_comments->$status = 10;
			}

			$link = add_query_arg( 'comment_status', $status, $link );

			if ( $post_id ) {
				$link = add_query_arg( 'p', absint( $post_id ), $link );
			}

			/*
			// I toyed with this, but decided against it. Leaving it in here in case anyone thinks it is a good idea. ~ Mark
			if ( !empty( $_REQUEST['s'] ) )
				$link = add_query_arg( 's', esc_attr( wp_unslash( $_REQUEST['s'] ) ), $link );
			*/

			$status_links[ $status ] = array(
				'url'     => esc_url( $link ),
				'label'   => sprintf(
					translate_nooped_plural( $label, $num_comments->$status ),
					sprintf(
						'<span class="%s-count">%s</span>',
						( 'moderated' === $status ) ? 'pending' : $status,
						number_format_i18n( $num_comments->$status )
					)
				),
				'current' => $status === $comment_status,
			);
		}

		/**
		 * Filters the comment status links.
		 *
		 * @since 2.5.0
		 * @since 5.1.0 The 'Mine' link was added.
		 *
		 * @param string[] $status_links An associative array of fully-formed comment status links. Includes 'All', 'Mine',
		 *                              'Pending', 'Approved', 'Spam', and 'Trash'.
		 */
		return apply_filters( 'comment_status_links', $this->get_views_links( $status_links ) );
	}

	/**
	 * @global string $comment_status
	 *
	 * @return array
	 */
	protected function get_bulk_actions() {
		global $comment_status;

		if ( ! current_user_can( 'moderate_comments' ) ) {
			return array(); // Return an empty array if the user doesn't have permission
		}

		$actions = array();

		if ( in_array( $comment_status, array( 'all', 'approved' ), true ) ) {
			$actions['unapprove'] = __( 'Unapprove' );
		}

		if ( in_array( $comment_status, array( 'all', 'moderated' ), true ) ) {
			$actions['approve'] = __( 'Approve' );
		}

		if ( in_array( $comment_status, array( 'all', 'moderated', 'approved', 'trash' ), true ) ) {
			$actions['spam'] = _x( 'Mark as spam', 'comment' );
		}

		if ( 'trash' === $comment_status ) {
			$actions['untrash'] = __( 'Restore' );
		} elseif ( 'spam' === $comment_status ) {
			$actions['unspam'] = _x( 'Not spam', 'comment' );
		}

		if ( in_array( $comment_status, array( 'trash', 'spam' ), true ) || ! EMPTY_TRASH_DAYS ) {
			$actions['delete'] = __( 'Delete permanently' );
		} else {
			$actions['trash'] = __( 'Move to Trash' );
		}

		return $actions;
	}

	/**
	 * @global string $comment_status
	 * @global string $comment_type
	 *
	 * @param string $which
	 */
	protected function extra_tablenav( $which ) {
		global $comment_status, $comment_type;
		static $has_items;

		if ( ! isset( $has_items ) ) {
			$has_items = $this->has_items();
		}

		echo '<div class="alignleft actions">';

		if ( 'top' === $which ) {
			ob_start();

			$this->comment_type_dropdown( $comment_type );

			/**
			 * Fires just before the Filter submit button for comment types.
			 *
			 * @since 3.5.0
			 */
			do_action( 'restrict_manage_comments' );

			$output = ob_get_clean();

			if ( ! empty( $output ) && $this->has_items() ) {
				echo $output;
				submit_button( __( 'Filter' ), '', 'filter_action', false, array( 'id' => 'post-query-submit' ) );
			}
		}

		if ( ( 'spam' === $comment_status || 'trash' === $comment_status ) && $has_items
			&& current_user_can( 'moderate_comments' )
		) {
			wp_nonce_field( 'bulk-destroy', '_destroy_nonce' );
			$title = ( 'spam' === $comment_status ) ? esc_attr__( 'Empty Spam' ) : esc_attr__( 'Empty Trash' );
			submit_button( $title, 'apply', 'delete_all', false );
		}

		/**
		 * Fires after the Filter submit button for comment types.
		 *
		 * @since 2.5.0
		 * @since 5.6.0 The `$which` parameter was added.
		 *
		 * @param string $comment_status The comment status name. Default 'All'.
		 * @param string $which          The location of the extra table nav markup: Either 'top' or 'bottom'.
		 */
		do_action( 'manage_comments_nav', $comment_status, $which );

		echo '</div>';
	}

	/**
	 * @return string|false
	 */
	public function current_action() {
		if ( isset( $_REQUEST['delete_all'] ) || isset( $_REQUEST['delete_all2'] ) ) {
			return 'delete_all';
		}

		return parent::current_action();
	}

	/**
	 * @global int $post_id
	 *
	 * @return string[] Array of column titles keyed by their column name.
	 */
	public function get_columns() {
		global $post_id;

		$columns = array();

		if ( $this->checkbox ) {
			$columns['cb'] = '<input type="checkbox" />';
		}

		$columns['author']  = __( 'Author' );
		$columns['comment'] = _x( 'Comment', 'column name' );

		if ( ! $post_id ) {
			/* translators: Column name or table row header. */
			$columns['response'] = __( 'In response to' );
		}

		$columns['date'] = _x( 'Submitted on', 'column name' );

		return $columns;
	}

	/**
	 * Displays a comment type drop-down for filtering on the Comments list table.
	 *
	 * @since 5.5.0
	 * @since 5.6.0 Renamed from `comment_status_dropdown()` to `comment_type_dropdown()`.
	 *
	 * @param string $comment_type The current comment type slug.
	 */
	protected function comment_type_dropdown( $comment_type ) {
		/**
		 * Filters the comment types shown in the drop-down menu on the Comments list table.
		 *
		 * @since 2.7.0
		 *
		 * @param string[] $comment_types Array of comment type labels keyed by their name.
		 */
		$comment_types = apply_filters(
			'admin_comment_types_dropdown',
			array(
				'comment' => __( 'Comments' ),
				'pings'   => __( 'Pings' ),
			)
		);

		if ( $comment_types && is_array( $comment_types ) ) {
			printf(
				'<label class="screen-reader-text" for="filter-by-comment-type">%s</label>',
				/* translators: Hidden accessibility text. */
				__( 'Filter by comment type' )
			);

			echo '<select id="filter-by-comment-type" name="comment_type">';

			printf( "\t<option value=''>%s</option>", __( 'All comment types' ) );

			foreach ( $comment_types as $type => $label ) {
				if ( get_comments(
					array(
						'count'   => true,
						'orderby' => 'none',
						'type'    => $type,
					)
				) ) {
					printf(
						"\t<option value='%s'%s>%s</option>\n",
						esc_attr( $type ),
						selected( $comment_type, $type, false ),
						esc_html( $label )
					);
				}
			}

			echo '</select>';
		}
	}

	/**
	 * @return array
	 */
	protected function get_sortable_columns() {
		return array(
			'author'   => array( 'comment_author', false, __( 'Author' ), __( 'Table ordered by Comment Author.' ) ),
			'response' => array( 'comment_post_ID', false, _x( 'In Response To', 'column name' ), __( 'Table ordered by Post Replied To.' ) ),
			'date'     => 'comment_date',
		);
	}

	/**
	 * Gets the name of the default primary column.
	 *
	 * @since 4.3.0
	 *
	 * @return string Name of the default primary column, in this case, 'comment'.
	 */
	protected function get_default_primary_column_name() {
		return 'comment';
	}

	/**
	 * Displays the comments table.
	 *
	 * Overrides the parent display() method to render extra comments.
	 *
	 * @since 3.1.0
	 */
	public function display() {
		wp_nonce_field( 'fetch-list-' . get_class( $this ), '_ajax_fetch_list_nonce' );
		static $has_items;

		if ( ! isset( $has_items ) ) {
			$has_items = $this->has_items();

			if ( $has_items ) {
				$this->display_tablenav( 'top' );
			}
		}

		$this->screen->render_screen_reader_content( 'heading_list' );

		?>
<table class="wp-list-table <?php echo implode( ' ', $this->get_table_classes() ); ?>">
		<?php
		if ( ! isset( $_GET['orderby'] ) ) {
			// In the initial view, Comments are ordered by comment's date but there's no column for that.
			echo '<caption class="screen-reader-text">' .
			/* translators: Hidden accessibility text. */
			__( 'Ordered by Comment Date, descending.' ) .
			'</caption>';
		} else {
			$this->print_table_description();
		}
		?>
	<thead>
	<tr>
		<?php $this->print_column_headers(); ?>
	</tr>
	</thead>

	<tbody id="the-comment-list" data-wp-lists="list:comment">
		<?php $this->display_rows_or_placeholder(); ?>
	</tbody>

	<tbody id="the-extra-comment-list" data-wp-lists="list:comment" style="display: none;">
		<?php
			/*
			 * Back up the items to restore after printing the extra items markup.
			 * The extra items may be empty, which will prevent the table nav from displaying later.
			 */
			$items       = $this->items;
			$this->items = $this->extra_items;
			$this->display_rows_or_placeholder();
			$this->items = $items;
		?>
	</tbody>

	<tfoot>
	<tr>
		<?php $this->print_column_headers( false ); ?>
	</tr>
	</tfoot>

</table>
		<?php

		$this->display_tablenav( 'bottom' );
	}

	/**
	 * @global WP_Post    $post    Global post object.
	 * @global WP_Comment $comment Global comment object.
	 *
	 * @param WP_Comment $item
	 */
	public function single_row( $item ) {
		global $post, $comment;

		// Restores the more descriptive, specific name for use within this method.
		$comment = $item;

		if ( $comment->comment_post_ID > 0 ) {
			$post = get_post( $comment->comment_post_ID );
		}

		$edit_post_cap = $post ? 'edit_post' : 'edit_posts';

		if ( ! current_user_can( $edit_post_cap, $comment->comment_post_ID )
			&& ( post_password_required( $comment->comment_post_ID )
				|| ! current_user_can( 'read_post', $comment->comment_post_ID ) )
		) {
			// The user has no access to the post and thus cannot see the comments.
			return false;
		}

		$the_comment_class = wp_get_comment_status( $comment );

		if ( ! $the_comment_class ) {
			$the_comment_class = '';
		}

		$the_comment_class = implode( ' ', get_comment_class( $the_comment_class, $comment, $comment->comment_post_ID ) );

		$this->user_can = current_user_can( 'edit_comment', $comment->comment_ID );

		echo "<tr id='comment-$comment->comment_ID' class='$the_comment_class'>";
		$this->single_row_columns( $comment );
		echo "</tr>\n";

		unset( $GLOBALS['post'], $GLOBALS['comment'] );
	}

	/**
	 * Generates and displays row actions links.
	 *
	 * @since 4.3.0
	 * @since 5.9.0 Renamed `$comment` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @global string $comment_status Status for the current listed comments.
	 *
	 * @param WP_Comment $item        The comment object.
	 * @param string     $column_name Current column name.
	 * @param string     $primary     Primary column name.
	 * @return string Row actions output for comments. An empty string
	 *                if the current column is not the primary column,
	 *                or if the current user cannot edit the comment.
	 */
	protected function handle_row_actions( $item, $column_name, $primary ) {
		global $comment_status;

		if ( $primary !== $column_name ) {
			return '';
		}

		if ( ! $this->user_can ) {
			return '';
		}

		// Restores the more descriptive, specific name for use within this method.
		$comment = $item;

		$the_comment_status = wp_get_comment_status( $comment );

		$output = '';

		$approve_nonce = esc_html( '_wpnonce=' . wp_create_nonce( 'approve-comment_' . $comment->comment_ID ) );
		$del_nonce     = esc_html( '_wpnonce=' . wp_create_nonce( 'delete-comment_' . $comment->comment_ID ) );

		$action_string = 'comment.php?action=%s&c=' . $comment->comment_ID . '&%s';

		$approve_url   = sprintf( $action_string, 'approvecomment', $approve_nonce );
		$unapprove_url = sprintf( $action_string, 'unapprovecomment', $approve_nonce );
		$spam_url      = sprintf( $action_string, 'spamcomment', $del_nonce );
		$unspam_url    = sprintf( $action_string, 'unspamcomment', $del_nonce );
		$trash_url     = sprintf( $action_string, 'trashcomment', $del_nonce );
		$untrash_url   = sprintf( $action_string, 'untrashcomment', $del_nonce );
		$delete_url    = sprintf( $action_string, 'deletecomment', $del_nonce );

		// Preorder it: Approve | Reply | Quick Edit | Edit | Spam | Trash.
		$actions = array(
			'approve'   => '',
			'unapprove' => '',
			'reply'     => '',
			'quickedit' => '',
			'edit'      => '',
			'spam'      => '',
			'unspam'    => '',
			'trash'     => '',
			'untrash'   => '',
			'delete'    => '',
		);

		// Not looking at all comments.
		if ( $comment_status && 'all' !== $comment_status ) {
			if ( 'approved' === $the_comment_status ) {
				$actions['unapprove'] = sprintf(
					'<a href="%s" data-wp-lists="%s" class="vim-u vim-destructive aria-button-if-js" aria-label="%s">%s</a>',
					esc_url( $unapprove_url ),
					"delete:the-comment-list:comment-{$comment->comment_ID}:e7e7d3:action=dim-comment&amp;new=unapproved",
					esc_attr__( 'Unapprove this comment' ),
					__( 'Unapprove' )
				);
			} elseif ( 'unapproved' === $the_comment_status ) {
				$actions['approve'] = sprintf(
					'<a href="%s" data-wp-lists="%s" class="vim-a vim-destructive aria-button-if-js" aria-label="%s">%s</a>',
					esc_url( $approve_url ),
					"delete:the-comment-list:comment-{$comment->comment_ID}:e7e7d3:action=dim-comment&amp;new=approved",
					esc_attr__( 'Approve this comment' ),
					__( 'Approve' )
				);
			}
		} else {
			$actions['approve'] = sprintf(
				'<a href="%s" data-wp-lists="%s" class="vim-a aria-button-if-js" aria-label="%s">%s</a>',
				esc_url( $approve_url ),
				"dim:the-comment-list:comment-{$comment->comment_ID}:unapproved:e7e7d3:e7e7d3:new=approved",
				esc_attr__( 'Approve this comment' ),
				__( 'Approve' )
			);

			$actions['unapprove'] = sprintf(
				'<a href="%s" data-wp-lists="%s" class="vim-u aria-button-if-js" aria-label="%s">%s</a>',
				esc_url( $unapprove_url ),
				"dim:the-comment-list:comment-{$comment->comment_ID}:unapproved:e7e7d3:e7e7d3:new=unapproved",
				esc_attr__( 'Unapprove this comment' ),
				__( 'Unapprove' )
			);
		}

		if ( 'spam' !== $the_comment_status ) {
			$actions['spam'] = sprintf(
				'<a href="%s" data-wp-lists="%s" class="vim-s vim-destructive aria-button-if-js" aria-label="%s">%s</a>',
				esc_url( $spam_url ),
				"delete:the-comment-list:comment-{$comment->comment_ID}::spam=1",
				esc_attr__( 'Mark this comment as spam' ),
				/* translators: "Mark as spam" link. */
				_x( 'Spam', 'verb' )
			);
		} elseif ( 'spam' === $the_comment_status ) {
			$actions['unspam'] = sprintf(
				'<a href="%s" data-wp-lists="%s" class="vim-z vim-destructive aria-button-if-js" aria-label="%s">%s</a>',
				esc_url( $unspam_url ),
				"delete:the-comment-list:comment-{$comment->comment_ID}:66cc66:unspam=1",
				esc_attr__( 'Restore this comment from the spam' ),
				_x( 'Not Spam', 'comment' )
			);
		}

		if ( 'trash' === $the_comment_status ) {
			$actions['untrash'] = sprintf(
				'<a href="%s" data-wp-lists="%s" class="vim-z vim-destructive aria-button-if-js" aria-label="%s">%s</a>',
				esc_url( $untrash_url ),
				"delete:the-comment-list:comment-{$comment->comment_ID}:66cc66:untrash=1",
				esc_attr__( 'Restore this comment from the Trash' ),
				__( 'Restore' )
			);
		}

		if ( 'spam' === $the_comment_status || 'trash' === $the_comment_status || ! EMPTY_TRASH_DAYS ) {
			$actions['delete'] = sprintf(
				'<a href="%s" data-wp-lists="%s" class="delete vim-d vim-destructive aria-button-if-js" aria-label="%s">%s</a>',
				esc_url( $delete_url ),
				"delete:the-comment-list:comment-{$comment->comment_ID}::delete=1",
				esc_attr__( 'Delete this comment permanently' ),
				__( 'Delete Permanently' )
			);
		} else {
			$actions['trash'] = sprintf(
				'<a href="%s" data-wp-lists="%s" class="delete vim-d vim-destructive aria-button-if-js" aria-label="%s">%s</a>',
				esc_url( $trash_url ),
				"delete:the-comment-list:comment-{$comment->comment_ID}::trash=1",
				esc_attr__( 'Move this comment to the Trash' ),
				_x( 'Trash', 'verb' )
			);
		}

		if ( 'spam' !== $the_comment_status && 'trash' !== $the_comment_status ) {
			$actions['edit'] = sprintf(
				'<a href="%s" aria-label="%s">%s</a>',
				"comment.php?action=editcomment&amp;c={$comment->comment_ID}",
				esc_attr__( 'Edit this comment' ),
				__( 'Edit' )
			);

			$format = '<button type="button" data-comment-id="%d" data-post-id="%d" data-action="%s" class="%s button-link" aria-expanded="false" aria-label="%s">%s</button>';

			$actions['quickedit'] = sprintf(
				$format,
				$comment->comment_ID,
				$comment->comment_post_ID,
				'edit',
				'vim-q comment-inline',
				esc_attr__( 'Quick edit this comment inline' ),
				__( 'Quick&nbsp;Edit' )
			);

			$actions['reply'] = sprintf(
				$format,
				$comment->comment_ID,
				$comment->comment_post_ID,
				'replyto',
				'vim-r comment-inline',
				esc_attr__( 'Reply to this comment' ),
				__( 'Reply' )
			);
		}

		/**
		 * Filters the action links displayed for each comment in the Comments list table.
		 *
		 * @since 2.6.0
		 *
		 * @param string[]   $actions An array of comment actions. Default actions include:
		 *                            'Approve', 'Unapprove', 'Edit', 'Reply', 'Spam',
		 *                            'Delete', and 'Trash'.
		 * @param WP_Comment $comment The comment object.
		 */
		$actions = apply_filters( 'comment_row_actions', array_filter( $actions ), $comment );

		$always_visible = false;

		$mode = get_user_setting( 'posts_list_mode', 'list' );

		if ( 'excerpt' === $mode ) {
			$always_visible = true;
		}

		$output .= '<div class="' . ( $always_visible ? 'row-actions visible' : 'row-actions' ) . '">';

		$i = 0;

		foreach ( $actions as $action => $link ) {
			++$i;

			if ( ( ( 'approve' === $action || 'unapprove' === $action ) && 2 === $i )
				|| 1 === $i
			) {
				$separator = '';
			} else {
				$separator = ' | ';
			}

			// Reply and quickedit need a hide-if-no-js span when not added with Ajax.
			if ( ( 'reply' === $action || 'quickedit' === $action ) && ! wp_doing_ajax() ) {
				$action .= ' hide-if-no-js';
			} elseif ( ( 'untrash' === $action && 'trash' === $the_comment_status )
				|| ( 'unspam' === $action && 'spam' === $the_comment_status )
			) {
				if ( '1' === get_comment_meta( $comment->comment_ID, '_wp_trash_meta_status', true ) ) {
					$action .= ' approve';
				} else {
					$action .= ' unapprove';
				}
			}

			$output .= "<span class='$action'>{$separator}{$link}</span>";
		}

		$output .= '</div>';

		$output .= '<button type="button" class="toggle-row"><span class="screen-reader-text">' .
			/* translators: Hidden accessibility text. */
			__( 'Show more details' ) .
		'</span></button>';

		return $output;
	}

	/**
	 * @since 5.9.0 Renamed `$comment` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param WP_Comment $item The comment object.
	 */
	public function column_cb( $item ) {
		// Restores the more descriptive, specific name for use within this method.
		$comment = $item;

		if ( $this->user_can ) {
			?>
		<input id="cb-select-<?php echo $comment->comment_ID; ?>" type="checkbox" name="delete_comments[]" value="<?php echo $comment->comment_ID; ?>" />
		<label for="cb-select-<?php echo $comment->comment_ID; ?>">
			<span class="screen-reader-text">
			<?php
			/* translators: Hidden accessibility text. */
			_e( 'Select comment' );
			?>
			</span>
		</label>
			<?php
		}
	}

	/**
	 * @param WP_Comment $comment The comment object.
	 */
	public function column_comment( $comment ) {
		echo '<div class="comment-author">';
			$this->column_author( $comment );
		echo '</div>';

		if ( $comment->comment_parent ) {
			$parent = get_comment( $comment->comment_parent );

			if ( $parent ) {
				$parent_link = esc_url( get_comment_link( $parent ) );
				$name        = get_comment_author( $parent );
				printf(
					/* translators: %s: Comment link. */
					__( 'In reply to %s.' ),
					'<a href="' . $parent_link . '">' . $name . '</a>'
				);
			}
		}

		comment_text( $comment );

		if ( $this->user_can ) {
			/** This filter is documented in wp-admin/includes/comment.php */
			$comment_content = apply_filters( 'comment_edit_pre', $comment->comment_content );
			?>
		<div id="inline-<?php echo $comment->comment_ID; ?>" class="hidden">
			<textarea class="comment" rows="1" cols="1"><?php echo esc_textarea( $comment_content ); ?></textarea>
			<div class="author-email"><?php echo esc_html( $comment->comment_author_email ); ?></div>
			<div class="author"><?php echo esc_html( $comment->comment_author ); ?></div>
			<div class="author-url"><?php echo esc_url( $comment->comment_author_url ); ?></div>
			<div class="comment_status"><?php echo $comment->comment_approved; ?></div>
		</div>
			<?php
		}
	}

	/**
	 * @global string $comment_status
	 *
	 * @param WP_Comment $comment The comment object.
	 */
	public function column_author( $comment ) {
		global $comment_status;

		$author_url = get_comment_author_url( $comment );

		$author_url_display = untrailingslashit( preg_replace( '|^http(s)?://(www\.)?|i', '', $author_url ) );

		if ( strlen( $author_url_display ) > 50 ) {
			$author_url_display = wp_html_excerpt( $author_url_display, 49, '&hellip;' );
		}

		echo '<strong>';
		comment_author( $comment );
		echo '</strong><br />';

		if ( ! empty( $author_url_display ) ) {
			// Print link to author URL, and disallow referrer information (without using target="_blank").
			printf(
				'<a href="%s" rel="noopener noreferrer">%s</a><br />',
				esc_url( $author_url ),
				esc_html( $author_url_display )
			);
		}

		if ( $this->user_can ) {
			if ( ! empty( $comment->comment_author_email ) ) {
				/** This filter is documented in wp-includes/comment-template.php */
				$email = apply_filters( 'comment_email', $comment->comment_author_email, $comment );

				if ( ! empty( $email ) && '@' !== $email ) {
					printf( '<a href="%1$s">%2$s</a><br />', esc_url( 'mailto:' . $email ), esc_html( $email ) );
				}
			}

			$author_ip = get_comment_author_IP( $comment );

			if ( $author_ip ) {
				$author_ip_url = add_query_arg(
					array(
						's'    => $author_ip,
						'mode' => 'detail',
					),
					admin_url( 'edit-comments.php' )
				);

				if ( 'spam' === $comment_status ) {
					$author_ip_url = add_query_arg( 'comment_status', 'spam', $author_ip_url );
				}

				printf( '<a href="%1$s">%2$s</a>', esc_url( $author_ip_url ), esc_html( $author_ip ) );
			}
		}
	}

	/**
	 * @param WP_Comment $comment The comment object.
	 */
	public function column_date( $comment ) {
		$submitted = sprintf(
			/* translators: 1: Comment date, 2: Comment time. */
			__( '%1$s at %2$s' ),
			/* translators: Comment date format. See https://www.php.net/manual/datetime.format.php */
			get_comment_date( __( 'Y/m/d' ), $comment ),
			/* translators: Comment time format. See https://www.php.net/manual/datetime.format.php */
			get_comment_date( __( 'g:i a' ), $comment )
		);

		echo '<div class="submitted-on">';

		if ( 'approved' === wp_get_comment_status( $comment ) && ! empty( $comment->comment_post_ID ) ) {
			printf(
				'<a href="%s">%s</a>',
				esc_url( get_comment_link( $comment ) ),
				$submitted
			);
		} else {
			echo $submitted;
		}

		echo '</div>';
	}

	/**
	 * @param WP_Comment $comment The comment object.
	 */
	public function column_response( $comment ) {
		$post = get_post();

		if ( ! $post ) {
			return;
		}

		if ( isset( $this->pending_count[ $post->ID ] ) ) {
			$pending_comments = $this->pending_count[ $post->ID ];
		} else {
			$_pending_count_temp              = get_pending_comments_num( array( $post->ID ) );
			$pending_comments                 = $_pending_count_temp[ $post->ID ];
			$this->pending_count[ $post->ID ] = $pending_comments;
		}

		if ( current_user_can( 'edit_post', $post->ID ) ) {
			$post_link  = "<a href='" . get_edit_post_link( $post->ID ) . "' class='comments-edit-item-link'>";
			$post_link .= esc_html( get_the_title( $post->ID ) ) . '</a>';
		} else {
			$post_link = esc_html( get_the_title( $post->ID ) );
		}

		echo '<div class="response-links">';

		if ( 'attachment' === $post->post_type ) {
			$thumb = wp_get_attachment_image( $post->ID, array( 80, 60 ), true );
			if ( $thumb ) {
				echo $thumb;
			}
		}

		echo $post_link;

		$post_type_object = get_post_type_object( $post->post_type );
		echo "<a href='" . get_permalink( $post->ID ) . "' class='comments-view-item-link'>" . $post_type_object->labels->view_item . '</a>';

		echo '<span class="post-com-count-wrapper post-com-count-', $post->ID, '">';
		$this->comments_bubble( $post->ID, $pending_comments );
		echo '</span> ';

		echo '</div>';
	}

	/**
	 * @since 5.9.0 Renamed `$comment` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param WP_Comment $item        The comment object.
	 * @param string     $column_name The custom column's name.
	 */
	public function column_default( $item, $column_name ) {
		// Restores the more descriptive, specific name for use within this method.
		$comment = $item;

		/**
		 * Fires when the default column output is displayed for a single row.
		 *
		 * @since 2.8.0
		 *
		 * @param string $column_name The custom column's name.
		 * @param string $comment_id  The comment ID as a numeric string.
		 */
		do_action( 'manage_comments_custom_column', $column_name, $comment->comment_ID );
	}
}
ajax-actions.php000064400000450115151327343360007653 0ustar00<?php
/**
 * Administration API: Core Ajax handlers
 *
 * @package WordPress
 * @subpackage Administration
 * @since 2.1.0
 */

//
// No-privilege Ajax handlers.
//

/**
 * Handles the Heartbeat API in the no-privilege context via AJAX .
 *
 * Runs when the user is not logged in.
 *
 * @since 3.6.0
 */
function wp_ajax_nopriv_heartbeat() {
	$response = array();

	// 'screen_id' is the same as $current_screen->id and the JS global 'pagenow'.
	if ( ! empty( $_POST['screen_id'] ) ) {
		$screen_id = sanitize_key( $_POST['screen_id'] );
	} else {
		$screen_id = 'front';
	}

	if ( ! empty( $_POST['data'] ) ) {
		$data = wp_unslash( (array) $_POST['data'] );

		/**
		 * Filters Heartbeat Ajax response in no-privilege environments.
		 *
		 * @since 3.6.0
		 *
		 * @param array  $response  The no-priv Heartbeat response.
		 * @param array  $data      The $_POST data sent.
		 * @param string $screen_id The screen ID.
		 */
		$response = apply_filters( 'heartbeat_nopriv_received', $response, $data, $screen_id );
	}

	/**
	 * Filters Heartbeat Ajax response in no-privilege environments when no data is passed.
	 *
	 * @since 3.6.0
	 *
	 * @param array  $response  The no-priv Heartbeat response.
	 * @param string $screen_id The screen ID.
	 */
	$response = apply_filters( 'heartbeat_nopriv_send', $response, $screen_id );

	/**
	 * Fires when Heartbeat ticks in no-privilege environments.
	 *
	 * Allows the transport to be easily replaced with long-polling.
	 *
	 * @since 3.6.0
	 *
	 * @param array  $response  The no-priv Heartbeat response.
	 * @param string $screen_id The screen ID.
	 */
	do_action( 'heartbeat_nopriv_tick', $response, $screen_id );

	// Send the current time according to the server.
	$response['server_time'] = time();

	wp_send_json( $response );
}

//
// GET-based Ajax handlers.
//

/**
 * Handles fetching a list table via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_fetch_list() {
	$list_class = $_GET['list_args']['class'];
	check_ajax_referer( "fetch-list-$list_class", '_ajax_fetch_list_nonce' );

	$wp_list_table = _get_list_table( $list_class, array( 'screen' => $_GET['list_args']['screen']['id'] ) );
	if ( ! $wp_list_table ) {
		wp_die( 0 );
	}

	if ( ! $wp_list_table->ajax_user_can() ) {
		wp_die( -1 );
	}

	$wp_list_table->ajax_response();

	wp_die( 0 );
}

/**
 * Handles tag search via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_ajax_tag_search() {
	if ( ! isset( $_GET['tax'] ) ) {
		wp_die( 0 );
	}

	$taxonomy        = sanitize_key( $_GET['tax'] );
	$taxonomy_object = get_taxonomy( $taxonomy );

	if ( ! $taxonomy_object ) {
		wp_die( 0 );
	}

	if ( ! current_user_can( $taxonomy_object->cap->assign_terms ) ) {
		wp_die( -1 );
	}

	$search = wp_unslash( $_GET['q'] );

	$comma = _x( ',', 'tag delimiter' );
	if ( ',' !== $comma ) {
		$search = str_replace( $comma, ',', $search );
	}

	if ( str_contains( $search, ',' ) ) {
		$search = explode( ',', $search );
		$search = $search[ count( $search ) - 1 ];
	}

	$search = trim( $search );

	/**
	 * Filters the minimum number of characters required to fire a tag search via Ajax.
	 *
	 * @since 4.0.0
	 *
	 * @param int         $characters      The minimum number of characters required. Default 2.
	 * @param WP_Taxonomy $taxonomy_object The taxonomy object.
	 * @param string      $search          The search term.
	 */
	$term_search_min_chars = (int) apply_filters( 'term_search_min_chars', 2, $taxonomy_object, $search );

	/*
	 * Require $term_search_min_chars chars for matching (default: 2)
	 * ensure it's a non-negative, non-zero integer.
	 */
	if ( ( 0 === $term_search_min_chars ) || ( strlen( $search ) < $term_search_min_chars ) ) {
		wp_die();
	}

	$results = get_terms(
		array(
			'taxonomy'   => $taxonomy,
			'name__like' => $search,
			'fields'     => 'names',
			'hide_empty' => false,
			'number'     => isset( $_GET['number'] ) ? (int) $_GET['number'] : 0,
		)
	);

	/**
	 * Filters the Ajax term search results.
	 *
	 * @since 6.1.0
	 *
	 * @param string[]    $results         Array of term names.
	 * @param WP_Taxonomy $taxonomy_object The taxonomy object.
	 * @param string      $search          The search term.
	 */
	$results = apply_filters( 'ajax_term_search_results', $results, $taxonomy_object, $search );

	echo implode( "\n", $results );
	wp_die();
}

/**
 * Handles compression testing via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_wp_compression_test() {
	if ( ! current_user_can( 'manage_options' ) ) {
		wp_die( -1 );
	}

	if ( ini_get( 'zlib.output_compression' ) || 'ob_gzhandler' === ini_get( 'output_handler' ) ) {
		// Use `update_option()` on single site to mark the option for autoloading.
		if ( is_multisite() ) {
			update_site_option( 'can_compress_scripts', 0 );
		} else {
			update_option( 'can_compress_scripts', 0, true );
		}
		wp_die( 0 );
	}

	if ( isset( $_GET['test'] ) ) {
		header( 'Expires: Wed, 11 Jan 1984 05:00:00 GMT' );
		header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s' ) . ' GMT' );
		header( 'Cache-Control: no-cache, must-revalidate, max-age=0' );
		header( 'Content-Type: application/javascript; charset=UTF-8' );
		$force_gzip = ( defined( 'ENFORCE_GZIP' ) && ENFORCE_GZIP );
		$test_str   = '"wpCompressionTest Lorem ipsum dolor sit amet consectetuer mollis sapien urna ut a. Eu nonummy condimentum fringilla tempor pretium platea vel nibh netus Maecenas. Hac molestie amet justo quis pellentesque est ultrices interdum nibh Morbi. Cras mattis pretium Phasellus ante ipsum ipsum ut sociis Suspendisse Lorem. Ante et non molestie. Porta urna Vestibulum egestas id congue nibh eu risus gravida sit. Ac augue auctor Ut et non a elit massa id sodales. Elit eu Nulla at nibh adipiscing mattis lacus mauris at tempus. Netus nibh quis suscipit nec feugiat eget sed lorem et urna. Pellentesque lacus at ut massa consectetuer ligula ut auctor semper Pellentesque. Ut metus massa nibh quam Curabitur molestie nec mauris congue. Volutpat molestie elit justo facilisis neque ac risus Ut nascetur tristique. Vitae sit lorem tellus et quis Phasellus lacus tincidunt nunc Fusce. Pharetra wisi Suspendisse mus sagittis libero lacinia Integer consequat ac Phasellus. Et urna ac cursus tortor aliquam Aliquam amet tellus volutpat Vestibulum. Justo interdum condimentum In augue congue tellus sollicitudin Quisque quis nibh."';

		if ( '1' === $_GET['test'] ) {
			echo $test_str;
			wp_die();
		} elseif ( '2' === $_GET['test'] ) {
			if ( ! isset( $_SERVER['HTTP_ACCEPT_ENCODING'] ) ) {
				wp_die( -1 );
			}

			if ( false !== stripos( $_SERVER['HTTP_ACCEPT_ENCODING'], 'deflate' ) && function_exists( 'gzdeflate' ) && ! $force_gzip ) {
				header( 'Content-Encoding: deflate' );
				$out = gzdeflate( $test_str, 1 );
			} elseif ( false !== stripos( $_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip' ) && function_exists( 'gzencode' ) ) {
				header( 'Content-Encoding: gzip' );
				$out = gzencode( $test_str, 1 );
			} else {
				wp_die( -1 );
			}

			echo $out;
			wp_die();
		} elseif ( 'no' === $_GET['test'] ) {
			check_ajax_referer( 'update_can_compress_scripts' );
			// Use `update_option()` on single site to mark the option for autoloading.
			if ( is_multisite() ) {
				update_site_option( 'can_compress_scripts', 0 );
			} else {
				update_option( 'can_compress_scripts', 0, true );
			}
		} elseif ( 'yes' === $_GET['test'] ) {
			check_ajax_referer( 'update_can_compress_scripts' );
			// Use `update_option()` on single site to mark the option for autoloading.
			if ( is_multisite() ) {
				update_site_option( 'can_compress_scripts', 1 );
			} else {
				update_option( 'can_compress_scripts', 1, true );
			}
		}
	}

	wp_die( 0 );
}

/**
 * Handles image editor previews via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_imgedit_preview() {
	$post_id = (int) $_GET['postid'];
	if ( empty( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) {
		wp_die( -1 );
	}

	check_ajax_referer( "image_editor-$post_id" );

	require_once ABSPATH . 'wp-admin/includes/image-edit.php';

	if ( ! stream_preview_image( $post_id ) ) {
		wp_die( -1 );
	}

	wp_die();
}

/**
 * Handles oEmbed caching via AJAX.
 *
 * @since 3.1.0
 *
 * @global WP_Embed $wp_embed WordPress Embed object.
 */
function wp_ajax_oembed_cache() {
	$GLOBALS['wp_embed']->cache_oembed( $_GET['post'] );
	wp_die( 0 );
}

/**
 * Handles user autocomplete via AJAX.
 *
 * @since 3.4.0
 */
function wp_ajax_autocomplete_user() {
	if ( ! is_multisite() || ! current_user_can( 'promote_users' ) || wp_is_large_network( 'users' ) ) {
		wp_die( -1 );
	}

	/** This filter is documented in wp-admin/user-new.php */
	if ( ! current_user_can( 'manage_network_users' ) && ! apply_filters( 'autocomplete_users_for_site_admins', false ) ) {
		wp_die( -1 );
	}

	$return = array();

	/*
	 * Check the type of request.
	 * Current allowed values are `add` and `search`.
	 */
	if ( isset( $_REQUEST['autocomplete_type'] ) && 'search' === $_REQUEST['autocomplete_type'] ) {
		$type = $_REQUEST['autocomplete_type'];
	} else {
		$type = 'add';
	}

	/*
	 * Check the desired field for value.
	 * Current allowed values are `user_email` and `user_login`.
	 */
	if ( isset( $_REQUEST['autocomplete_field'] ) && 'user_email' === $_REQUEST['autocomplete_field'] ) {
		$field = $_REQUEST['autocomplete_field'];
	} else {
		$field = 'user_login';
	}

	// Exclude current users of this blog.
	if ( isset( $_REQUEST['site_id'] ) ) {
		$id = absint( $_REQUEST['site_id'] );
	} else {
		$id = get_current_blog_id();
	}

	$include_blog_users = ( 'search' === $type ? get_users(
		array(
			'blog_id' => $id,
			'fields'  => 'ID',
		)
	) : array() );

	$exclude_blog_users = ( 'add' === $type ? get_users(
		array(
			'blog_id' => $id,
			'fields'  => 'ID',
		)
	) : array() );

	$users = get_users(
		array(
			'blog_id'        => false,
			'search'         => '*' . $_REQUEST['term'] . '*',
			'include'        => $include_blog_users,
			'exclude'        => $exclude_blog_users,
			'search_columns' => array( 'user_login', 'user_nicename', 'user_email' ),
		)
	);

	foreach ( $users as $user ) {
		$return[] = array(
			/* translators: 1: User login, 2: User email address. */
			'label' => sprintf( _x( '%1$s (%2$s)', 'user autocomplete result' ), $user->user_login, $user->user_email ),
			'value' => $user->$field,
		);
	}

	wp_die( wp_json_encode( $return ) );
}

/**
 * Handles Ajax requests for community events
 *
 * @since 4.8.0
 */
function wp_ajax_get_community_events() {
	require_once ABSPATH . 'wp-admin/includes/class-wp-community-events.php';

	check_ajax_referer( 'community_events' );

	$search         = isset( $_POST['location'] ) ? wp_unslash( $_POST['location'] ) : '';
	$timezone       = isset( $_POST['timezone'] ) ? wp_unslash( $_POST['timezone'] ) : '';
	$user_id        = get_current_user_id();
	$saved_location = get_user_option( 'community-events-location', $user_id );
	$events_client  = new WP_Community_Events( $user_id, $saved_location );
	$events         = $events_client->get_events( $search, $timezone );
	$ip_changed     = false;

	if ( is_wp_error( $events ) ) {
		wp_send_json_error(
			array(
				'error' => $events->get_error_message(),
			)
		);
	} else {
		if ( empty( $saved_location['ip'] ) && ! empty( $events['location']['ip'] ) ) {
			$ip_changed = true;
		} elseif ( isset( $saved_location['ip'] ) && ! empty( $events['location']['ip'] ) && $saved_location['ip'] !== $events['location']['ip'] ) {
			$ip_changed = true;
		}

		/*
		 * The location should only be updated when it changes. The API doesn't always return
		 * a full location; sometimes it's missing the description or country. The location
		 * that was saved during the initial request is known to be good and complete, though.
		 * It should be left intact until the user explicitly changes it (either by manually
		 * searching for a new location, or by changing their IP address).
		 *
		 * If the location was updated with an incomplete response from the API, then it could
		 * break assumptions that the UI makes (e.g., that there will always be a description
		 * that corresponds to a latitude/longitude location).
		 *
		 * The location is stored network-wide, so that the user doesn't have to set it on each site.
		 */
		if ( $ip_changed || $search ) {
			update_user_meta( $user_id, 'community-events-location', $events['location'] );
		}

		wp_send_json_success( $events );
	}
}

/**
 * Handles dashboard widgets via AJAX.
 *
 * @since 3.4.0
 */
function wp_ajax_dashboard_widgets() {
	require_once ABSPATH . 'wp-admin/includes/dashboard.php';

	$pagenow = $_GET['pagenow'];
	if ( 'dashboard-user' === $pagenow || 'dashboard-network' === $pagenow || 'dashboard' === $pagenow ) {
		set_current_screen( $pagenow );
	}

	switch ( $_GET['widget'] ) {
		case 'dashboard_primary':
			wp_dashboard_primary();
			break;
	}
	wp_die();
}

/**
 * Handles Customizer preview logged-in status via AJAX.
 *
 * @since 3.4.0
 */
function wp_ajax_logged_in() {
	wp_die( 1 );
}

//
// Ajax helpers.
//

/**
 * Sends back current comment total and new page links if they need to be updated.
 *
 * Contrary to normal success Ajax response ("1"), die with time() on success.
 *
 * @since 2.7.0
 * @access private
 *
 * @param int $comment_id
 * @param int $delta
 */
function _wp_ajax_delete_comment_response( $comment_id, $delta = -1 ) {
	$total    = isset( $_POST['_total'] ) ? (int) $_POST['_total'] : 0;
	$per_page = isset( $_POST['_per_page'] ) ? (int) $_POST['_per_page'] : 0;
	$page     = isset( $_POST['_page'] ) ? (int) $_POST['_page'] : 0;
	$url      = isset( $_POST['_url'] ) ? sanitize_url( $_POST['_url'] ) : '';

	// JS didn't send us everything we need to know. Just die with success message.
	if ( ! $total || ! $per_page || ! $page || ! $url ) {
		$time           = time();
		$comment        = get_comment( $comment_id );
		$comment_status = '';
		$comment_link   = '';

		if ( $comment ) {
			$comment_status = $comment->comment_approved;
		}

		if ( 1 === (int) $comment_status ) {
			$comment_link = get_comment_link( $comment );
		}

		$counts = wp_count_comments();

		$x = new WP_Ajax_Response(
			array(
				'what'         => 'comment',
				// Here for completeness - not used.
				'id'           => $comment_id,
				'supplemental' => array(
					'status'               => $comment_status,
					'postId'               => $comment ? $comment->comment_post_ID : '',
					'time'                 => $time,
					'in_moderation'        => $counts->moderated,
					'i18n_comments_text'   => sprintf(
						/* translators: %s: Number of comments. */
						_n( '%s Comment', '%s Comments', $counts->approved ),
						number_format_i18n( $counts->approved )
					),
					'i18n_moderation_text' => sprintf(
						/* translators: %s: Number of comments. */
						_n( '%s Comment in moderation', '%s Comments in moderation', $counts->moderated ),
						number_format_i18n( $counts->moderated )
					),
					'comment_link'         => $comment_link,
				),
			)
		);
		$x->send();
	}

	$total += $delta;
	if ( $total < 0 ) {
		$total = 0;
	}

	// Only do the expensive stuff on a page-break, and about 1 other time per page.
	if ( 0 === $total % $per_page || 1 === mt_rand( 1, $per_page ) ) {
		$post_id = 0;
		// What type of comment count are we looking for?
		$status = 'all';
		$parsed = parse_url( $url );

		if ( isset( $parsed['query'] ) ) {
			parse_str( $parsed['query'], $query_vars );

			if ( ! empty( $query_vars['comment_status'] ) ) {
				$status = $query_vars['comment_status'];
			}

			if ( ! empty( $query_vars['p'] ) ) {
				$post_id = (int) $query_vars['p'];
			}

			if ( ! empty( $query_vars['comment_type'] ) ) {
				$type = $query_vars['comment_type'];
			}
		}

		if ( empty( $type ) ) {
			// Only use the comment count if not filtering by a comment_type.
			$comment_count = wp_count_comments( $post_id );

			// We're looking for a known type of comment count.
			if ( isset( $comment_count->$status ) ) {
				$total = $comment_count->$status;
			}
		}
		// Else use the decremented value from above.
	}

	// The time since the last comment count.
	$time    = time();
	$comment = get_comment( $comment_id );
	$counts  = wp_count_comments();

	$x = new WP_Ajax_Response(
		array(
			'what'         => 'comment',
			'id'           => $comment_id,
			'supplemental' => array(
				'status'               => $comment ? $comment->comment_approved : '',
				'postId'               => $comment ? $comment->comment_post_ID : '',
				/* translators: %s: Number of comments. */
				'total_items_i18n'     => sprintf( _n( '%s item', '%s items', $total ), number_format_i18n( $total ) ),
				'total_pages'          => (int) ceil( $total / $per_page ),
				'total_pages_i18n'     => number_format_i18n( (int) ceil( $total / $per_page ) ),
				'total'                => $total,
				'time'                 => $time,
				'in_moderation'        => $counts->moderated,
				'i18n_moderation_text' => sprintf(
					/* translators: %s: Number of comments. */
					_n( '%s Comment in moderation', '%s Comments in moderation', $counts->moderated ),
					number_format_i18n( $counts->moderated )
				),
			),
		)
	);
	$x->send();
}

//
// POST-based Ajax handlers.
//

/**
 * Handles adding a hierarchical term via AJAX.
 *
 * @since 3.1.0
 * @access private
 */
function _wp_ajax_add_hierarchical_term() {
	$action   = $_POST['action'];
	$taxonomy = get_taxonomy( substr( $action, 4 ) );
	check_ajax_referer( $action, '_ajax_nonce-add-' . $taxonomy->name );

	if ( ! current_user_can( $taxonomy->cap->edit_terms ) ) {
		wp_die( -1 );
	}

	$names  = explode( ',', $_POST[ 'new' . $taxonomy->name ] );
	$parent = isset( $_POST[ 'new' . $taxonomy->name . '_parent' ] ) ? (int) $_POST[ 'new' . $taxonomy->name . '_parent' ] : 0;

	if ( 0 > $parent ) {
		$parent = 0;
	}

	if ( 'category' === $taxonomy->name ) {
		$post_category = isset( $_POST['post_category'] ) ? (array) $_POST['post_category'] : array();
	} else {
		$post_category = ( isset( $_POST['tax_input'] ) && isset( $_POST['tax_input'][ $taxonomy->name ] ) ) ? (array) $_POST['tax_input'][ $taxonomy->name ] : array();
	}

	$checked_categories = array_map( 'absint', (array) $post_category );
	$popular_ids        = wp_popular_terms_checklist( $taxonomy->name, 0, 10, false );

	foreach ( $names as $cat_name ) {
		$cat_name          = trim( $cat_name );
		$category_nicename = sanitize_title( $cat_name );

		if ( '' === $category_nicename ) {
			continue;
		}

		$cat_id = wp_insert_term( $cat_name, $taxonomy->name, array( 'parent' => $parent ) );

		if ( ! $cat_id || is_wp_error( $cat_id ) ) {
			continue;
		} else {
			$cat_id = $cat_id['term_id'];
		}

		$checked_categories[] = $cat_id;

		if ( $parent ) { // Do these all at once in a second.
			continue;
		}

		ob_start();

		wp_terms_checklist(
			0,
			array(
				'taxonomy'             => $taxonomy->name,
				'descendants_and_self' => $cat_id,
				'selected_cats'        => $checked_categories,
				'popular_cats'         => $popular_ids,
			)
		);

		$data = ob_get_clean();

		$add = array(
			'what'     => $taxonomy->name,
			'id'       => $cat_id,
			'data'     => str_replace( array( "\n", "\t" ), '', $data ),
			'position' => -1,
		);
	}

	if ( $parent ) { // Foncy - replace the parent and all its children.
		$parent  = get_term( $parent, $taxonomy->name );
		$term_id = $parent->term_id;

		while ( $parent->parent ) { // Get the top parent.
			$parent = get_term( $parent->parent, $taxonomy->name );
			if ( is_wp_error( $parent ) ) {
				break;
			}
			$term_id = $parent->term_id;
		}

		ob_start();

		wp_terms_checklist(
			0,
			array(
				'taxonomy'             => $taxonomy->name,
				'descendants_and_self' => $term_id,
				'selected_cats'        => $checked_categories,
				'popular_cats'         => $popular_ids,
			)
		);

		$data = ob_get_clean();

		$add = array(
			'what'     => $taxonomy->name,
			'id'       => $term_id,
			'data'     => str_replace( array( "\n", "\t" ), '', $data ),
			'position' => -1,
		);
	}

	ob_start();

	wp_dropdown_categories(
		array(
			'taxonomy'         => $taxonomy->name,
			'hide_empty'       => 0,
			'name'             => 'new' . $taxonomy->name . '_parent',
			'orderby'          => 'name',
			'hierarchical'     => 1,
			'show_option_none' => '&mdash; ' . $taxonomy->labels->parent_item . ' &mdash;',
		)
	);

	$sup = ob_get_clean();

	$add['supplemental'] = array( 'newcat_parent' => $sup );

	$x = new WP_Ajax_Response( $add );
	$x->send();
}

/**
 * Handles deleting a comment via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_delete_comment() {
	$id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;

	$comment = get_comment( $id );

	if ( ! $comment ) {
		wp_die( time() );
	}

	if ( ! current_user_can( 'edit_comment', $comment->comment_ID ) ) {
		wp_die( -1 );
	}

	check_ajax_referer( "delete-comment_$id" );
	$status = wp_get_comment_status( $comment );
	$delta  = -1;

	if ( isset( $_POST['trash'] ) && '1' === $_POST['trash'] ) {
		if ( 'trash' === $status ) {
			wp_die( time() );
		}

		$r = wp_trash_comment( $comment );
	} elseif ( isset( $_POST['untrash'] ) && '1' === $_POST['untrash'] ) {
		if ( 'trash' !== $status ) {
			wp_die( time() );
		}

		$r = wp_untrash_comment( $comment );

		// Undo trash, not in Trash.
		if ( ! isset( $_POST['comment_status'] ) || 'trash' !== $_POST['comment_status'] ) {
			$delta = 1;
		}
	} elseif ( isset( $_POST['spam'] ) && '1' === $_POST['spam'] ) {
		if ( 'spam' === $status ) {
			wp_die( time() );
		}

		$r = wp_spam_comment( $comment );
	} elseif ( isset( $_POST['unspam'] ) && '1' === $_POST['unspam'] ) {
		if ( 'spam' !== $status ) {
			wp_die( time() );
		}

		$r = wp_unspam_comment( $comment );

		// Undo spam, not in spam.
		if ( ! isset( $_POST['comment_status'] ) || 'spam' !== $_POST['comment_status'] ) {
			$delta = 1;
		}
	} elseif ( isset( $_POST['delete'] ) && '1' === $_POST['delete'] ) {
		$r = wp_delete_comment( $comment );
	} else {
		wp_die( -1 );
	}

	if ( $r ) {
		// Decide if we need to send back '1' or a more complicated response including page links and comment counts.
		_wp_ajax_delete_comment_response( $comment->comment_ID, $delta );
	}

	wp_die( 0 );
}

/**
 * Handles deleting a tag via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_delete_tag() {
	$tag_id = (int) $_POST['tag_ID'];
	check_ajax_referer( "delete-tag_$tag_id" );

	if ( ! current_user_can( 'delete_term', $tag_id ) ) {
		wp_die( -1 );
	}

	$taxonomy = ! empty( $_POST['taxonomy'] ) ? $_POST['taxonomy'] : 'post_tag';
	$tag      = get_term( $tag_id, $taxonomy );

	if ( ! $tag || is_wp_error( $tag ) ) {
		wp_die( 1 );
	}

	if ( wp_delete_term( $tag_id, $taxonomy ) ) {
		wp_die( 1 );
	} else {
		wp_die( 0 );
	}
}

/**
 * Handles deleting a link via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_delete_link() {
	$id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;

	check_ajax_referer( "delete-bookmark_$id" );

	if ( ! current_user_can( 'manage_links' ) ) {
		wp_die( -1 );
	}

	$link = get_bookmark( $id );
	if ( ! $link || is_wp_error( $link ) ) {
		wp_die( 1 );
	}

	if ( wp_delete_link( $id ) ) {
		wp_die( 1 );
	} else {
		wp_die( 0 );
	}
}

/**
 * Handles deleting meta via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_delete_meta() {
	$id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;

	check_ajax_referer( "delete-meta_$id" );
	$meta = get_metadata_by_mid( 'post', $id );

	if ( ! $meta ) {
		wp_die( 1 );
	}

	if ( is_protected_meta( $meta->meta_key, 'post' ) || ! current_user_can( 'delete_post_meta', $meta->post_id, $meta->meta_key ) ) {
		wp_die( -1 );
	}

	if ( delete_meta( $meta->meta_id ) ) {
		wp_die( 1 );
	}

	wp_die( 0 );
}

/**
 * Handles deleting a post via AJAX.
 *
 * @since 3.1.0
 *
 * @param string $action Action to perform.
 */
function wp_ajax_delete_post( $action ) {
	if ( empty( $action ) ) {
		$action = 'delete-post';
	}

	$id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;
	check_ajax_referer( "{$action}_$id" );

	if ( ! current_user_can( 'delete_post', $id ) ) {
		wp_die( -1 );
	}

	if ( ! get_post( $id ) ) {
		wp_die( 1 );
	}

	if ( wp_delete_post( $id ) ) {
		wp_die( 1 );
	} else {
		wp_die( 0 );
	}
}

/**
 * Handles sending a post to the Trash via AJAX.
 *
 * @since 3.1.0
 *
 * @param string $action Action to perform.
 */
function wp_ajax_trash_post( $action ) {
	if ( empty( $action ) ) {
		$action = 'trash-post';
	}

	$id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;
	check_ajax_referer( "{$action}_$id" );

	if ( ! current_user_can( 'delete_post', $id ) ) {
		wp_die( -1 );
	}

	if ( ! get_post( $id ) ) {
		wp_die( 1 );
	}

	if ( 'trash-post' === $action ) {
		$done = wp_trash_post( $id );
	} else {
		$done = wp_untrash_post( $id );
	}

	if ( $done ) {
		wp_die( 1 );
	}

	wp_die( 0 );
}

/**
 * Handles restoring a post from the Trash via AJAX.
 *
 * @since 3.1.0
 *
 * @param string $action Action to perform.
 */
function wp_ajax_untrash_post( $action ) {
	if ( empty( $action ) ) {
		$action = 'untrash-post';
	}

	wp_ajax_trash_post( $action );
}

/**
 * Handles deleting a page via AJAX.
 *
 * @since 3.1.0
 *
 * @param string $action Action to perform.
 */
function wp_ajax_delete_page( $action ) {
	if ( empty( $action ) ) {
		$action = 'delete-page';
	}

	$id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;
	check_ajax_referer( "{$action}_$id" );

	if ( ! current_user_can( 'delete_page', $id ) ) {
		wp_die( -1 );
	}

	if ( ! get_post( $id ) ) {
		wp_die( 1 );
	}

	if ( wp_delete_post( $id ) ) {
		wp_die( 1 );
	} else {
		wp_die( 0 );
	}
}

/**
 * Handles dimming a comment via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_dim_comment() {
	$id      = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;
	$comment = get_comment( $id );

	if ( ! $comment ) {
		$x = new WP_Ajax_Response(
			array(
				'what' => 'comment',
				'id'   => new WP_Error(
					'invalid_comment',
					/* translators: %d: Comment ID. */
					sprintf( __( 'Comment %d does not exist' ), $id )
				),
			)
		);
		$x->send();
	}

	if ( ! current_user_can( 'edit_comment', $comment->comment_ID ) && ! current_user_can( 'moderate_comments' ) ) {
		wp_die( -1 );
	}

	$current = wp_get_comment_status( $comment );

	if ( isset( $_POST['new'] ) && $_POST['new'] === $current ) {
		wp_die( time() );
	}

	check_ajax_referer( "approve-comment_$id" );

	if ( in_array( $current, array( 'unapproved', 'spam' ), true ) ) {
		$result = wp_set_comment_status( $comment, 'approve', true );
	} else {
		$result = wp_set_comment_status( $comment, 'hold', true );
	}

	if ( is_wp_error( $result ) ) {
		$x = new WP_Ajax_Response(
			array(
				'what' => 'comment',
				'id'   => $result,
			)
		);
		$x->send();
	}

	// Decide if we need to send back '1' or a more complicated response including page links and comment counts.
	_wp_ajax_delete_comment_response( $comment->comment_ID );
	wp_die( 0 );
}

/**
 * Handles adding a link category via AJAX.
 *
 * @since 3.1.0
 *
 * @param string $action Action to perform.
 */
function wp_ajax_add_link_category( $action ) {
	if ( empty( $action ) ) {
		$action = 'add-link-category';
	}

	check_ajax_referer( $action );

	$taxonomy_object = get_taxonomy( 'link_category' );

	if ( ! current_user_can( $taxonomy_object->cap->manage_terms ) ) {
		wp_die( -1 );
	}

	$names = explode( ',', wp_unslash( $_POST['newcat'] ) );
	$x     = new WP_Ajax_Response();

	foreach ( $names as $cat_name ) {
		$cat_name = trim( $cat_name );
		$slug     = sanitize_title( $cat_name );

		if ( '' === $slug ) {
			continue;
		}

		$cat_id = wp_insert_term( $cat_name, 'link_category' );

		if ( ! $cat_id || is_wp_error( $cat_id ) ) {
			continue;
		} else {
			$cat_id = $cat_id['term_id'];
		}

		$cat_name = esc_html( $cat_name );

		$x->add(
			array(
				'what'     => 'link-category',
				'id'       => $cat_id,
				'data'     => "<li id='link-category-$cat_id'><label for='in-link-category-$cat_id' class='selectit'><input value='" . esc_attr( $cat_id ) . "' type='checkbox' checked='checked' name='link_category[]' id='in-link-category-$cat_id'/> $cat_name</label></li>",
				'position' => -1,
			)
		);
	}
	$x->send();
}

/**
 * Handles adding a tag via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_add_tag() {
	check_ajax_referer( 'add-tag', '_wpnonce_add-tag' );

	$taxonomy        = ! empty( $_POST['taxonomy'] ) ? $_POST['taxonomy'] : 'post_tag';
	$taxonomy_object = get_taxonomy( $taxonomy );

	if ( ! current_user_can( $taxonomy_object->cap->edit_terms ) ) {
		wp_die( -1 );
	}

	$x = new WP_Ajax_Response();

	$tag = wp_insert_term( $_POST['tag-name'], $taxonomy, $_POST );

	if ( $tag && ! is_wp_error( $tag ) ) {
		$tag = get_term( $tag['term_id'], $taxonomy );
	}

	if ( ! $tag || is_wp_error( $tag ) ) {
		$message    = __( 'An error has occurred. Please reload the page and try again.' );
		$error_code = 'error';

		if ( is_wp_error( $tag ) && $tag->get_error_message() ) {
			$message = $tag->get_error_message();
		}

		if ( is_wp_error( $tag ) && $tag->get_error_code() ) {
			$error_code = $tag->get_error_code();
		}

		$x->add(
			array(
				'what' => 'taxonomy',
				'data' => new WP_Error( $error_code, $message ),
			)
		);
		$x->send();
	}

	$wp_list_table = _get_list_table( 'WP_Terms_List_Table', array( 'screen' => $_POST['screen'] ) );

	$level     = 0;
	$noparents = '';

	if ( is_taxonomy_hierarchical( $taxonomy ) ) {
		$level = count( get_ancestors( $tag->term_id, $taxonomy, 'taxonomy' ) );
		ob_start();
		$wp_list_table->single_row( $tag, $level );
		$noparents = ob_get_clean();
	}

	ob_start();
	$wp_list_table->single_row( $tag );
	$parents = ob_get_clean();

	require ABSPATH . 'wp-admin/includes/edit-tag-messages.php';

	$message = '';
	if ( isset( $messages[ $taxonomy_object->name ][1] ) ) {
		$message = $messages[ $taxonomy_object->name ][1];
	} elseif ( isset( $messages['_item'][1] ) ) {
		$message = $messages['_item'][1];
	}

	$x->add(
		array(
			'what'         => 'taxonomy',
			'data'         => $message,
			'supplemental' => array(
				'parents'   => $parents,
				'noparents' => $noparents,
				'notice'    => $message,
			),
		)
	);

	$x->add(
		array(
			'what'         => 'term',
			'position'     => $level,
			'supplemental' => (array) $tag,
		)
	);

	$x->send();
}

/**
 * Handles getting a tagcloud via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_get_tagcloud() {
	if ( ! isset( $_POST['tax'] ) ) {
		wp_die( 0 );
	}

	$taxonomy        = sanitize_key( $_POST['tax'] );
	$taxonomy_object = get_taxonomy( $taxonomy );

	if ( ! $taxonomy_object ) {
		wp_die( 0 );
	}

	if ( ! current_user_can( $taxonomy_object->cap->assign_terms ) ) {
		wp_die( -1 );
	}

	$tags = get_terms(
		array(
			'taxonomy' => $taxonomy,
			'number'   => 45,
			'orderby'  => 'count',
			'order'    => 'DESC',
		)
	);

	if ( empty( $tags ) ) {
		wp_die( $taxonomy_object->labels->not_found );
	}

	if ( is_wp_error( $tags ) ) {
		wp_die( $tags->get_error_message() );
	}

	foreach ( $tags as $key => $tag ) {
		$tags[ $key ]->link = '#';
		$tags[ $key ]->id   = $tag->term_id;
	}

	// We need raw tag names here, so don't filter the output.
	$return = wp_generate_tag_cloud(
		$tags,
		array(
			'filter' => 0,
			'format' => 'list',
		)
	);

	if ( empty( $return ) ) {
		wp_die( 0 );
	}

	echo $return;
	wp_die();
}

/**
 * Handles getting comments via AJAX.
 *
 * @since 3.1.0
 *
 * @global int $post_id
 *
 * @param string $action Action to perform.
 */
function wp_ajax_get_comments( $action ) {
	global $post_id;

	if ( empty( $action ) ) {
		$action = 'get-comments';
	}

	check_ajax_referer( $action );

	if ( empty( $post_id ) && ! empty( $_REQUEST['p'] ) ) {
		$id = absint( $_REQUEST['p'] );
		if ( ! empty( $id ) ) {
			$post_id = $id;
		}
	}

	if ( empty( $post_id ) ) {
		wp_die( -1 );
	}

	$wp_list_table = _get_list_table( 'WP_Post_Comments_List_Table', array( 'screen' => 'edit-comments' ) );

	if ( ! current_user_can( 'edit_post', $post_id ) ) {
		wp_die( -1 );
	}

	$wp_list_table->prepare_items();

	if ( ! $wp_list_table->has_items() ) {
		wp_die( 1 );
	}

	$x = new WP_Ajax_Response();

	ob_start();
	foreach ( $wp_list_table->items as $comment ) {
		if ( ! current_user_can( 'edit_comment', $comment->comment_ID ) && 0 === $comment->comment_approved ) {
			continue;
		}
		get_comment( $comment );
		$wp_list_table->single_row( $comment );
	}
	$comment_list_item = ob_get_clean();

	$x->add(
		array(
			'what' => 'comments',
			'data' => $comment_list_item,
		)
	);

	$x->send();
}

/**
 * Handles replying to a comment via AJAX.
 *
 * @since 3.1.0
 *
 * @param string $action Action to perform.
 */
function wp_ajax_replyto_comment( $action ) {
	if ( empty( $action ) ) {
		$action = 'replyto-comment';
	}

	check_ajax_referer( $action, '_ajax_nonce-replyto-comment' );

	$comment_post_id = (int) $_POST['comment_post_ID'];
	$post            = get_post( $comment_post_id );

	if ( ! $post ) {
		wp_die( -1 );
	}

	if ( ! current_user_can( 'edit_post', $comment_post_id ) ) {
		wp_die( -1 );
	}

	if ( empty( $post->post_status ) ) {
		wp_die( 1 );
	} elseif ( in_array( $post->post_status, array( 'draft', 'pending', 'trash' ), true ) ) {
		wp_die( __( 'You cannot reply to a comment on a draft post.' ) );
	}

	$user = wp_get_current_user();

	if ( $user->exists() ) {
		$comment_author       = wp_slash( $user->display_name );
		$comment_author_email = wp_slash( $user->user_email );
		$comment_author_url   = wp_slash( $user->user_url );
		$user_id              = $user->ID;

		if ( current_user_can( 'unfiltered_html' ) ) {
			if ( ! isset( $_POST['_wp_unfiltered_html_comment'] ) ) {
				$_POST['_wp_unfiltered_html_comment'] = '';
			}

			if ( wp_create_nonce( 'unfiltered-html-comment' ) !== $_POST['_wp_unfiltered_html_comment'] ) {
				kses_remove_filters(); // Start with a clean slate.
				kses_init_filters();   // Set up the filters.
				remove_filter( 'pre_comment_content', 'wp_filter_post_kses' );
				add_filter( 'pre_comment_content', 'wp_filter_kses' );
			}
		}
	} else {
		wp_die( __( 'Sorry, you must be logged in to reply to a comment.' ) );
	}

	$comment_content = trim( $_POST['content'] );

	if ( '' === $comment_content ) {
		wp_die( __( 'Please type your comment text.' ) );
	}

	$comment_type = isset( $_POST['comment_type'] ) ? trim( $_POST['comment_type'] ) : 'comment';

	$comment_parent = 0;

	if ( isset( $_POST['comment_ID'] ) ) {
		$comment_parent = absint( $_POST['comment_ID'] );
	}

	$comment_auto_approved = false;

	$commentdata = array(
		'comment_post_ID' => $comment_post_id,
	);

	$commentdata += compact(
		'comment_author',
		'comment_author_email',
		'comment_author_url',
		'comment_content',
		'comment_type',
		'comment_parent',
		'user_id'
	);

	// Automatically approve parent comment.
	if ( ! empty( $_POST['approve_parent'] ) ) {
		$parent = get_comment( $comment_parent );

		if ( $parent && '0' === $parent->comment_approved && (int) $parent->comment_post_ID === $comment_post_id ) {
			if ( ! current_user_can( 'edit_comment', $parent->comment_ID ) ) {
				wp_die( -1 );
			}

			if ( wp_set_comment_status( $parent, 'approve' ) ) {
				$comment_auto_approved = true;
			}
		}
	}

	$comment_id = wp_new_comment( $commentdata );

	if ( is_wp_error( $comment_id ) ) {
		wp_die( $comment_id->get_error_message() );
	}

	$comment = get_comment( $comment_id );

	if ( ! $comment ) {
		wp_die( 1 );
	}

	$position = ( isset( $_POST['position'] ) && (int) $_POST['position'] ) ? (int) $_POST['position'] : '-1';

	ob_start();
	if ( isset( $_REQUEST['mode'] ) && 'dashboard' === $_REQUEST['mode'] ) {
		require_once ABSPATH . 'wp-admin/includes/dashboard.php';
		_wp_dashboard_recent_comments_row( $comment );
	} else {
		if ( isset( $_REQUEST['mode'] ) && 'single' === $_REQUEST['mode'] ) {
			$wp_list_table = _get_list_table( 'WP_Post_Comments_List_Table', array( 'screen' => 'edit-comments' ) );
		} else {
			$wp_list_table = _get_list_table( 'WP_Comments_List_Table', array( 'screen' => 'edit-comments' ) );
		}
		$wp_list_table->single_row( $comment );
	}
	$comment_list_item = ob_get_clean();

	$response = array(
		'what'     => 'comment',
		'id'       => $comment->comment_ID,
		'data'     => $comment_list_item,
		'position' => $position,
	);

	$counts                   = wp_count_comments();
	$response['supplemental'] = array(
		'in_moderation'        => $counts->moderated,
		'i18n_comments_text'   => sprintf(
			/* translators: %s: Number of comments. */
			_n( '%s Comment', '%s Comments', $counts->approved ),
			number_format_i18n( $counts->approved )
		),
		'i18n_moderation_text' => sprintf(
			/* translators: %s: Number of comments. */
			_n( '%s Comment in moderation', '%s Comments in moderation', $counts->moderated ),
			number_format_i18n( $counts->moderated )
		),
	);

	if ( $comment_auto_approved ) {
		$response['supplemental']['parent_approved'] = $parent->comment_ID;
		$response['supplemental']['parent_post_id']  = $parent->comment_post_ID;
	}

	$x = new WP_Ajax_Response();
	$x->add( $response );
	$x->send();
}

/**
 * Handles editing a comment via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_edit_comment() {
	check_ajax_referer( 'replyto-comment', '_ajax_nonce-replyto-comment' );

	$comment_id = (int) $_POST['comment_ID'];

	if ( ! current_user_can( 'edit_comment', $comment_id ) ) {
		wp_die( -1 );
	}

	if ( '' === $_POST['content'] ) {
		wp_die( __( 'Please type your comment text.' ) );
	}

	if ( isset( $_POST['status'] ) ) {
		$_POST['comment_status'] = $_POST['status'];
	}

	$updated = edit_comment();
	if ( is_wp_error( $updated ) ) {
		wp_die( $updated->get_error_message() );
	}

	$position = ( isset( $_POST['position'] ) && (int) $_POST['position'] ) ? (int) $_POST['position'] : '-1';
	/*
	 * Checkbox is used to differentiate between the Edit Comments screen (1)
	 * and the Comments section on the Edit Post screen (0).
	 */
	$checkbox      = ( isset( $_POST['checkbox'] ) && '1' === $_POST['checkbox'] ) ? 1 : 0;
	$wp_list_table = _get_list_table( $checkbox ? 'WP_Comments_List_Table' : 'WP_Post_Comments_List_Table', array( 'screen' => 'edit-comments' ) );

	$comment = get_comment( $comment_id );

	if ( empty( $comment->comment_ID ) ) {
		wp_die( -1 );
	}

	ob_start();
	$wp_list_table->single_row( $comment );
	$comment_list_item = ob_get_clean();

	$x = new WP_Ajax_Response();

	$x->add(
		array(
			'what'     => 'edit_comment',
			'id'       => $comment->comment_ID,
			'data'     => $comment_list_item,
			'position' => $position,
		)
	);

	$x->send();
}

/**
 * Handles adding a menu item via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_add_menu_item() {
	check_ajax_referer( 'add-menu_item', 'menu-settings-column-nonce' );

	if ( ! current_user_can( 'edit_theme_options' ) ) {
		wp_die( -1 );
	}

	require_once ABSPATH . 'wp-admin/includes/nav-menu.php';

	/*
	 * For performance reasons, we omit some object properties from the checklist.
	 * The following is a hacky way to restore them when adding non-custom items.
	 */
	$menu_items_data = array();

	foreach ( (array) $_POST['menu-item'] as $menu_item_data ) {
		if (
			! empty( $menu_item_data['menu-item-type'] ) &&
			'custom' !== $menu_item_data['menu-item-type'] &&
			! empty( $menu_item_data['menu-item-object-id'] )
		) {
			switch ( $menu_item_data['menu-item-type'] ) {
				case 'post_type':
					$_object = get_post( $menu_item_data['menu-item-object-id'] );
					break;

				case 'post_type_archive':
					$_object = get_post_type_object( $menu_item_data['menu-item-object'] );
					break;

				case 'taxonomy':
					$_object = get_term( $menu_item_data['menu-item-object-id'], $menu_item_data['menu-item-object'] );
					break;
			}

			$_menu_items = array_map( 'wp_setup_nav_menu_item', array( $_object ) );
			$_menu_item  = reset( $_menu_items );

			// Restore the missing menu item properties.
			$menu_item_data['menu-item-description'] = $_menu_item->description;
		}

		$menu_items_data[] = $menu_item_data;
	}

	$item_ids = wp_save_nav_menu_items( 0, $menu_items_data );
	if ( is_wp_error( $item_ids ) ) {
		wp_die( 0 );
	}

	$menu_items = array();

	foreach ( (array) $item_ids as $menu_item_id ) {
		$menu_obj = get_post( $menu_item_id );

		if ( ! empty( $menu_obj->ID ) ) {
			$menu_obj        = wp_setup_nav_menu_item( $menu_obj );
			$menu_obj->title = empty( $menu_obj->title ) ? __( 'Menu Item' ) : $menu_obj->title;
			$menu_obj->label = $menu_obj->title; // Don't show "(pending)" in ajax-added items.
			$menu_items[]    = $menu_obj;
		}
	}

	/** This filter is documented in wp-admin/includes/nav-menu.php */
	$walker_class_name = apply_filters( 'wp_edit_nav_menu_walker', 'Walker_Nav_Menu_Edit', $_POST['menu'] );

	if ( ! class_exists( $walker_class_name ) ) {
		wp_die( 0 );
	}

	if ( ! empty( $menu_items ) ) {
		$args = array(
			'after'       => '',
			'before'      => '',
			'link_after'  => '',
			'link_before' => '',
			'walker'      => new $walker_class_name(),
		);

		echo walk_nav_menu_tree( $menu_items, 0, (object) $args );
	}

	wp_die();
}

/**
 * Handles adding meta via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_add_meta() {
	check_ajax_referer( 'add-meta', '_ajax_nonce-add-meta' );
	$c    = 0;
	$pid  = (int) $_POST['post_id'];
	$post = get_post( $pid );

	if ( isset( $_POST['metakeyselect'] ) || isset( $_POST['metakeyinput'] ) ) {
		if ( ! current_user_can( 'edit_post', $pid ) ) {
			wp_die( -1 );
		}

		if ( isset( $_POST['metakeyselect'] ) && '#NONE#' === $_POST['metakeyselect'] && empty( $_POST['metakeyinput'] ) ) {
			wp_die( 1 );
		}

		// If the post is an autodraft, save the post as a draft and then attempt to save the meta.
		if ( 'auto-draft' === $post->post_status ) {
			$post_data                = array();
			$post_data['action']      = 'draft'; // Warning fix.
			$post_data['post_ID']     = $pid;
			$post_data['post_type']   = $post->post_type;
			$post_data['post_status'] = 'draft';
			$now                      = time();

			$post_data['post_title'] = sprintf(
				/* translators: 1: Post creation date, 2: Post creation time. */
				__( 'Draft created on %1$s at %2$s' ),
				gmdate( __( 'F j, Y' ), $now ),
				gmdate( __( 'g:i a' ), $now )
			);

			$pid = edit_post( $post_data );

			if ( $pid ) {
				if ( is_wp_error( $pid ) ) {
					$x = new WP_Ajax_Response(
						array(
							'what' => 'meta',
							'data' => $pid,
						)
					);
					$x->send();
				}

				$mid = add_meta( $pid );
				if ( ! $mid ) {
					wp_die( __( 'Please provide a custom field value.' ) );
				}
			} else {
				wp_die( 0 );
			}
		} else {
			$mid = add_meta( $pid );
			if ( ! $mid ) {
				wp_die( __( 'Please provide a custom field value.' ) );
			}
		}

		$meta = get_metadata_by_mid( 'post', $mid );
		$pid  = (int) $meta->post_id;
		$meta = get_object_vars( $meta );

		$x = new WP_Ajax_Response(
			array(
				'what'         => 'meta',
				'id'           => $mid,
				'data'         => _list_meta_row( $meta, $c ),
				'position'     => 1,
				'supplemental' => array( 'postid' => $pid ),
			)
		);
	} else { // Update?
		$mid   = (int) key( $_POST['meta'] );
		$key   = wp_unslash( $_POST['meta'][ $mid ]['key'] );
		$value = wp_unslash( $_POST['meta'][ $mid ]['value'] );

		if ( '' === trim( $key ) ) {
			wp_die( __( 'Please provide a custom field name.' ) );
		}

		$meta = get_metadata_by_mid( 'post', $mid );

		if ( ! $meta ) {
			wp_die( 0 ); // If meta doesn't exist.
		}

		if (
			is_protected_meta( $meta->meta_key, 'post' ) || is_protected_meta( $key, 'post' ) ||
			! current_user_can( 'edit_post_meta', $meta->post_id, $meta->meta_key ) ||
			! current_user_can( 'edit_post_meta', $meta->post_id, $key )
		) {
			wp_die( -1 );
		}

		if ( $meta->meta_value !== $value || $meta->meta_key !== $key ) {
			$u = update_metadata_by_mid( 'post', $mid, $value, $key );
			if ( ! $u ) {
				wp_die( 0 ); // We know meta exists; we also know it's unchanged (or DB error, in which case there are bigger problems).
			}
		}

		$x = new WP_Ajax_Response(
			array(
				'what'         => 'meta',
				'id'           => $mid,
				'old_id'       => $mid,
				'data'         => _list_meta_row(
					array(
						'meta_key'   => $key,
						'meta_value' => $value,
						'meta_id'    => $mid,
					),
					$c
				),
				'position'     => 0,
				'supplemental' => array( 'postid' => $meta->post_id ),
			)
		);
	}
	$x->send();
}

/**
 * Handles adding a user via AJAX.
 *
 * @since 3.1.0
 *
 * @param string $action Action to perform.
 */
function wp_ajax_add_user( $action ) {
	if ( empty( $action ) ) {
		$action = 'add-user';
	}

	check_ajax_referer( $action );

	if ( ! current_user_can( 'create_users' ) ) {
		wp_die( -1 );
	}

	$user_id = edit_user();

	if ( ! $user_id ) {
		wp_die( 0 );
	} elseif ( is_wp_error( $user_id ) ) {
		$x = new WP_Ajax_Response(
			array(
				'what' => 'user',
				'id'   => $user_id,
			)
		);
		$x->send();
	}

	$user_object   = get_userdata( $user_id );
	$wp_list_table = _get_list_table( 'WP_Users_List_Table' );

	$role = current( $user_object->roles );

	$x = new WP_Ajax_Response(
		array(
			'what'         => 'user',
			'id'           => $user_id,
			'data'         => $wp_list_table->single_row( $user_object, '', $role ),
			'supplemental' => array(
				'show-link' => sprintf(
					/* translators: %s: The new user. */
					__( 'User %s added' ),
					'<a href="#user-' . $user_id . '">' . $user_object->user_login . '</a>'
				),
				'role'      => $role,
			),
		)
	);
	$x->send();
}

/**
 * Handles closed post boxes via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_closed_postboxes() {
	check_ajax_referer( 'closedpostboxes', 'closedpostboxesnonce' );
	$closed = isset( $_POST['closed'] ) ? explode( ',', $_POST['closed'] ) : array();
	$closed = array_filter( $closed );

	$hidden = isset( $_POST['hidden'] ) ? explode( ',', $_POST['hidden'] ) : array();
	$hidden = array_filter( $hidden );

	$page = isset( $_POST['page'] ) ? $_POST['page'] : '';

	if ( sanitize_key( $page ) !== $page ) {
		wp_die( 0 );
	}

	$user = wp_get_current_user();
	if ( ! $user ) {
		wp_die( -1 );
	}

	if ( is_array( $closed ) ) {
		update_user_meta( $user->ID, "closedpostboxes_$page", $closed );
	}

	if ( is_array( $hidden ) ) {
		// Postboxes that are always shown.
		$hidden = array_diff( $hidden, array( 'submitdiv', 'linksubmitdiv', 'manage-menu', 'create-menu' ) );
		update_user_meta( $user->ID, "metaboxhidden_$page", $hidden );
	}

	wp_die( 1 );
}

/**
 * Handles hidden columns via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_hidden_columns() {
	check_ajax_referer( 'screen-options-nonce', 'screenoptionnonce' );
	$page = isset( $_POST['page'] ) ? $_POST['page'] : '';

	if ( sanitize_key( $page ) !== $page ) {
		wp_die( 0 );
	}

	$user = wp_get_current_user();
	if ( ! $user ) {
		wp_die( -1 );
	}

	$hidden = ! empty( $_POST['hidden'] ) ? explode( ',', $_POST['hidden'] ) : array();
	update_user_meta( $user->ID, "manage{$page}columnshidden", $hidden );

	wp_die( 1 );
}

/**
 * Handles updating whether to display the welcome panel via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_update_welcome_panel() {
	check_ajax_referer( 'welcome-panel-nonce', 'welcomepanelnonce' );

	if ( ! current_user_can( 'edit_theme_options' ) ) {
		wp_die( -1 );
	}

	update_user_meta( get_current_user_id(), 'show_welcome_panel', empty( $_POST['visible'] ) ? 0 : 1 );

	wp_die( 1 );
}

/**
 * Handles for retrieving menu meta boxes via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_menu_get_metabox() {
	if ( ! current_user_can( 'edit_theme_options' ) ) {
		wp_die( -1 );
	}

	require_once ABSPATH . 'wp-admin/includes/nav-menu.php';

	if ( isset( $_POST['item-type'] ) && 'post_type' === $_POST['item-type'] ) {
		$type     = 'posttype';
		$callback = 'wp_nav_menu_item_post_type_meta_box';
		$items    = (array) get_post_types( array( 'show_in_nav_menus' => true ), 'object' );
	} elseif ( isset( $_POST['item-type'] ) && 'taxonomy' === $_POST['item-type'] ) {
		$type     = 'taxonomy';
		$callback = 'wp_nav_menu_item_taxonomy_meta_box';
		$items    = (array) get_taxonomies( array( 'show_ui' => true ), 'object' );
	}

	if ( ! empty( $_POST['item-object'] ) && isset( $items[ $_POST['item-object'] ] ) ) {
		$menus_meta_box_object = $items[ $_POST['item-object'] ];

		/** This filter is documented in wp-admin/includes/nav-menu.php */
		$item = apply_filters( 'nav_menu_meta_box_object', $menus_meta_box_object );

		$box_args = array(
			'id'       => 'add-' . $item->name,
			'title'    => $item->labels->name,
			'callback' => $callback,
			'args'     => $item,
		);

		ob_start();
		$callback( null, $box_args );

		$markup = ob_get_clean();

		echo wp_json_encode(
			array(
				'replace-id' => $type . '-' . $item->name,
				'markup'     => $markup,
			)
		);
	}

	wp_die();
}

/**
 * Handles internal linking via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_wp_link_ajax() {
	check_ajax_referer( 'internal-linking', '_ajax_linking_nonce' );

	$args = array();

	if ( isset( $_POST['search'] ) ) {
		$args['s'] = wp_unslash( $_POST['search'] );
	}

	if ( isset( $_POST['term'] ) ) {
		$args['s'] = wp_unslash( $_POST['term'] );
	}

	$args['pagenum'] = ! empty( $_POST['page'] ) ? absint( $_POST['page'] ) : 1;

	if ( ! class_exists( '_WP_Editors', false ) ) {
		require ABSPATH . WPINC . '/class-wp-editor.php';
	}

	$results = _WP_Editors::wp_link_query( $args );

	if ( ! isset( $results ) ) {
		wp_die( 0 );
	}

	echo wp_json_encode( $results );
	echo "\n";

	wp_die();
}

/**
 * Handles saving menu locations via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_menu_locations_save() {
	if ( ! current_user_can( 'edit_theme_options' ) ) {
		wp_die( -1 );
	}

	check_ajax_referer( 'add-menu_item', 'menu-settings-column-nonce' );

	if ( ! isset( $_POST['menu-locations'] ) ) {
		wp_die( 0 );
	}

	set_theme_mod( 'nav_menu_locations', array_map( 'absint', $_POST['menu-locations'] ) );
	wp_die( 1 );
}

/**
 * Handles saving the meta box order via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_meta_box_order() {
	check_ajax_referer( 'meta-box-order' );
	$order        = isset( $_POST['order'] ) ? (array) $_POST['order'] : false;
	$page_columns = isset( $_POST['page_columns'] ) ? $_POST['page_columns'] : 'auto';

	if ( 'auto' !== $page_columns ) {
		$page_columns = (int) $page_columns;
	}

	$page = isset( $_POST['page'] ) ? $_POST['page'] : '';

	if ( sanitize_key( $page ) !== $page ) {
		wp_die( 0 );
	}

	$user = wp_get_current_user();
	if ( ! $user ) {
		wp_die( -1 );
	}

	if ( $order ) {
		update_user_meta( $user->ID, "meta-box-order_$page", $order );
	}

	if ( $page_columns ) {
		update_user_meta( $user->ID, "screen_layout_$page", $page_columns );
	}

	wp_send_json_success();
}

/**
 * Handles menu quick searching via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_menu_quick_search() {
	if ( ! current_user_can( 'edit_theme_options' ) ) {
		wp_die( -1 );
	}

	require_once ABSPATH . 'wp-admin/includes/nav-menu.php';

	_wp_ajax_menu_quick_search( $_POST );

	wp_die();
}

/**
 * Handles retrieving a permalink via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_get_permalink() {
	check_ajax_referer( 'getpermalink', 'getpermalinknonce' );
	$post_id = isset( $_POST['post_id'] ) ? (int) $_POST['post_id'] : 0;
	wp_die( get_preview_post_link( $post_id ) );
}

/**
 * Handles retrieving a sample permalink via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_sample_permalink() {
	check_ajax_referer( 'samplepermalink', 'samplepermalinknonce' );
	$post_id = isset( $_POST['post_id'] ) ? (int) $_POST['post_id'] : 0;
	$title   = isset( $_POST['new_title'] ) ? $_POST['new_title'] : '';
	$slug    = isset( $_POST['new_slug'] ) ? $_POST['new_slug'] : null;
	wp_die( get_sample_permalink_html( $post_id, $title, $slug ) );
}

/**
 * Handles Quick Edit saving a post from a list table via AJAX.
 *
 * @since 3.1.0
 *
 * @global string $mode List table view mode.
 */
function wp_ajax_inline_save() {
	global $mode;

	check_ajax_referer( 'inlineeditnonce', '_inline_edit' );

	if ( ! isset( $_POST['post_ID'] ) || ! (int) $_POST['post_ID'] ) {
		wp_die();
	}

	$post_id = (int) $_POST['post_ID'];

	if ( 'page' === $_POST['post_type'] ) {
		if ( ! current_user_can( 'edit_page', $post_id ) ) {
			wp_die( __( 'Sorry, you are not allowed to edit this page.' ) );
		}
	} else {
		if ( ! current_user_can( 'edit_post', $post_id ) ) {
			wp_die( __( 'Sorry, you are not allowed to edit this post.' ) );
		}
	}

	$last = wp_check_post_lock( $post_id );
	if ( $last ) {
		$last_user      = get_userdata( $last );
		$last_user_name = $last_user ? $last_user->display_name : __( 'Someone' );

		/* translators: %s: User's display name. */
		$msg_template = __( 'Saving is disabled: %s is currently editing this post.' );

		if ( 'page' === $_POST['post_type'] ) {
			/* translators: %s: User's display name. */
			$msg_template = __( 'Saving is disabled: %s is currently editing this page.' );
		}

		printf( $msg_template, esc_html( $last_user_name ) );
		wp_die();
	}

	$data = &$_POST;

	$post = get_post( $post_id, ARRAY_A );

	// Since it's coming from the database.
	$post = wp_slash( $post );

	$data['content'] = $post['post_content'];
	$data['excerpt'] = $post['post_excerpt'];

	// Rename.
	$data['user_ID'] = get_current_user_id();

	if ( isset( $data['post_parent'] ) ) {
		$data['parent_id'] = $data['post_parent'];
	}

	// Status.
	if ( isset( $data['keep_private'] ) && 'private' === $data['keep_private'] ) {
		$data['visibility']  = 'private';
		$data['post_status'] = 'private';
	} else {
		$data['post_status'] = $data['_status'];
	}

	if ( empty( $data['comment_status'] ) ) {
		$data['comment_status'] = 'closed';
	}

	if ( empty( $data['ping_status'] ) ) {
		$data['ping_status'] = 'closed';
	}

	// Exclude terms from taxonomies that are not supposed to appear in Quick Edit.
	if ( ! empty( $data['tax_input'] ) ) {
		foreach ( $data['tax_input'] as $taxonomy => $terms ) {
			$tax_object = get_taxonomy( $taxonomy );
			/** This filter is documented in wp-admin/includes/class-wp-posts-list-table.php */
			if ( ! apply_filters( 'quick_edit_show_taxonomy', $tax_object->show_in_quick_edit, $taxonomy, $post['post_type'] ) ) {
				unset( $data['tax_input'][ $taxonomy ] );
			}
		}
	}

	// Hack: wp_unique_post_slug() doesn't work for drafts, so we will fake that our post is published.
	if ( ! empty( $data['post_name'] ) && in_array( $post['post_status'], array( 'draft', 'pending' ), true ) ) {
		$post['post_status'] = 'publish';
		$data['post_name']   = wp_unique_post_slug( $data['post_name'], $post['ID'], $post['post_status'], $post['post_type'], $post['post_parent'] );
	}

	// Update the post.
	edit_post();

	$wp_list_table = _get_list_table( 'WP_Posts_List_Table', array( 'screen' => $_POST['screen'] ) );

	$mode = 'excerpt' === $_POST['post_view'] ? 'excerpt' : 'list';

	$level = 0;
	if ( is_post_type_hierarchical( $wp_list_table->screen->post_type ) ) {
		$request_post = array( get_post( $_POST['post_ID'] ) );
		$parent       = $request_post[0]->post_parent;

		while ( $parent > 0 ) {
			$parent_post = get_post( $parent );
			$parent      = $parent_post->post_parent;
			++$level;
		}
	}

	$wp_list_table->display_rows( array( get_post( $_POST['post_ID'] ) ), $level );

	wp_die();
}

/**
 * Handles Quick Edit saving for a term via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_inline_save_tax() {
	check_ajax_referer( 'taxinlineeditnonce', '_inline_edit' );

	$taxonomy        = sanitize_key( $_POST['taxonomy'] );
	$taxonomy_object = get_taxonomy( $taxonomy );

	if ( ! $taxonomy_object ) {
		wp_die( 0 );
	}

	if ( ! isset( $_POST['tax_ID'] ) || ! (int) $_POST['tax_ID'] ) {
		wp_die( -1 );
	}

	$id = (int) $_POST['tax_ID'];

	if ( ! current_user_can( 'edit_term', $id ) ) {
		wp_die( -1 );
	}

	$wp_list_table = _get_list_table( 'WP_Terms_List_Table', array( 'screen' => 'edit-' . $taxonomy ) );

	$tag                  = get_term( $id, $taxonomy );
	$_POST['description'] = $tag->description;

	$updated = wp_update_term( $id, $taxonomy, $_POST );

	if ( $updated && ! is_wp_error( $updated ) ) {
		$tag = get_term( $updated['term_id'], $taxonomy );
		if ( ! $tag || is_wp_error( $tag ) ) {
			if ( is_wp_error( $tag ) && $tag->get_error_message() ) {
				wp_die( $tag->get_error_message() );
			}
			wp_die( __( 'Item not updated.' ) );
		}
	} else {
		if ( is_wp_error( $updated ) && $updated->get_error_message() ) {
			wp_die( $updated->get_error_message() );
		}
		wp_die( __( 'Item not updated.' ) );
	}

	$level  = 0;
	$parent = $tag->parent;

	while ( $parent > 0 ) {
		$parent_tag = get_term( $parent, $taxonomy );
		$parent     = $parent_tag->parent;
		++$level;
	}

	$wp_list_table->single_row( $tag, $level );
	wp_die();
}

/**
 * Handles querying posts for the Find Posts modal via AJAX.
 *
 * @see window.findPosts
 *
 * @since 3.1.0
 */
function wp_ajax_find_posts() {
	check_ajax_referer( 'find-posts' );

	$post_types = get_post_types( array( 'public' => true ), 'objects' );
	unset( $post_types['attachment'] );

	$args = array(
		'post_type'      => array_keys( $post_types ),
		'post_status'    => 'any',
		'posts_per_page' => 50,
	);

	$search = wp_unslash( $_POST['ps'] );

	if ( '' !== $search ) {
		$args['s'] = $search;
	}

	$posts = get_posts( $args );

	if ( ! $posts ) {
		wp_send_json_error( __( 'No items found.' ) );
	}

	$html = '<table class="widefat"><thead><tr><th class="found-radio"><br /></th><th>' . __( 'Title' ) . '</th><th class="no-break">' . __( 'Type' ) . '</th><th class="no-break">' . __( 'Date' ) . '</th><th class="no-break">' . __( 'Status' ) . '</th></tr></thead><tbody>';
	$alt  = '';
	foreach ( $posts as $post ) {
		$title = trim( $post->post_title ) ? $post->post_title : __( '(no title)' );
		$alt   = ( 'alternate' === $alt ) ? '' : 'alternate';

		switch ( $post->post_status ) {
			case 'publish':
			case 'private':
				$stat = __( 'Published' );
				break;
			case 'future':
				$stat = __( 'Scheduled' );
				break;
			case 'pending':
				$stat = __( 'Pending Review' );
				break;
			case 'draft':
				$stat = __( 'Draft' );
				break;
		}

		if ( '0000-00-00 00:00:00' === $post->post_date ) {
			$time = '';
		} else {
			/* translators: Date format in table columns, see https://www.php.net/manual/datetime.format.php */
			$time = mysql2date( __( 'Y/m/d' ), $post->post_date );
		}

		$html .= '<tr class="' . trim( 'found-posts ' . $alt ) . '"><td class="found-radio"><input type="radio" id="found-' . $post->ID . '" name="found_post_id" value="' . esc_attr( $post->ID ) . '"></td>';
		$html .= '<td><label for="found-' . $post->ID . '">' . esc_html( $title ) . '</label></td><td class="no-break">' . esc_html( $post_types[ $post->post_type ]->labels->singular_name ) . '</td><td class="no-break">' . esc_html( $time ) . '</td><td class="no-break">' . esc_html( $stat ) . ' </td></tr>' . "\n\n";
	}

	$html .= '</tbody></table>';

	wp_send_json_success( $html );
}

/**
 * Handles saving the widgets order via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_widgets_order() {
	check_ajax_referer( 'save-sidebar-widgets', 'savewidgets' );

	if ( ! current_user_can( 'edit_theme_options' ) ) {
		wp_die( -1 );
	}

	unset( $_POST['savewidgets'], $_POST['action'] );

	// Save widgets order for all sidebars.
	if ( is_array( $_POST['sidebars'] ) ) {
		$sidebars = array();

		foreach ( wp_unslash( $_POST['sidebars'] ) as $key => $val ) {
			$sb = array();

			if ( ! empty( $val ) ) {
				$val = explode( ',', $val );

				foreach ( $val as $k => $v ) {
					if ( ! str_contains( $v, 'widget-' ) ) {
						continue;
					}

					$sb[ $k ] = substr( $v, strpos( $v, '_' ) + 1 );
				}
			}
			$sidebars[ $key ] = $sb;
		}

		wp_set_sidebars_widgets( $sidebars );
		wp_die( 1 );
	}

	wp_die( -1 );
}

/**
 * Handles saving a widget via AJAX.
 *
 * @since 3.1.0
 *
 * @global array $wp_registered_widgets
 * @global array $wp_registered_widget_controls
 * @global array $wp_registered_widget_updates
 */
function wp_ajax_save_widget() {
	global $wp_registered_widgets, $wp_registered_widget_controls, $wp_registered_widget_updates;

	check_ajax_referer( 'save-sidebar-widgets', 'savewidgets' );

	if ( ! current_user_can( 'edit_theme_options' ) || ! isset( $_POST['id_base'] ) ) {
		wp_die( -1 );
	}

	unset( $_POST['savewidgets'], $_POST['action'] );

	/**
	 * Fires early when editing the widgets displayed in sidebars.
	 *
	 * @since 2.8.0
	 */
	do_action( 'load-widgets.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores

	/**
	 * Fires early when editing the widgets displayed in sidebars.
	 *
	 * @since 2.8.0
	 */
	do_action( 'widgets.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores

	/** This action is documented in wp-admin/widgets.php */
	do_action( 'sidebar_admin_setup' );

	$id_base      = wp_unslash( $_POST['id_base'] );
	$widget_id    = wp_unslash( $_POST['widget-id'] );
	$sidebar_id   = $_POST['sidebar'];
	$multi_number = ! empty( $_POST['multi_number'] ) ? (int) $_POST['multi_number'] : 0;
	$settings     = isset( $_POST[ 'widget-' . $id_base ] ) && is_array( $_POST[ 'widget-' . $id_base ] ) ? $_POST[ 'widget-' . $id_base ] : false;
	$error        = '<p>' . __( 'An error has occurred. Please reload the page and try again.' ) . '</p>';

	$sidebars = wp_get_sidebars_widgets();
	$sidebar  = isset( $sidebars[ $sidebar_id ] ) ? $sidebars[ $sidebar_id ] : array();

	// Delete.
	if ( isset( $_POST['delete_widget'] ) && $_POST['delete_widget'] ) {

		if ( ! isset( $wp_registered_widgets[ $widget_id ] ) ) {
			wp_die( $error );
		}

		$sidebar = array_diff( $sidebar, array( $widget_id ) );
		$_POST   = array(
			'sidebar'            => $sidebar_id,
			'widget-' . $id_base => array(),
			'the-widget-id'      => $widget_id,
			'delete_widget'      => '1',
		);

		/** This action is documented in wp-admin/widgets.php */
		do_action( 'delete_widget', $widget_id, $sidebar_id, $id_base );

	} elseif ( $settings && preg_match( '/__i__|%i%/', key( $settings ) ) ) {
		if ( ! $multi_number ) {
			wp_die( $error );
		}

		$_POST[ 'widget-' . $id_base ] = array( $multi_number => reset( $settings ) );
		$widget_id                     = $id_base . '-' . $multi_number;
		$sidebar[]                     = $widget_id;
	}
	$_POST['widget-id'] = $sidebar;

	foreach ( (array) $wp_registered_widget_updates as $name => $control ) {

		if ( $name === $id_base ) {
			if ( ! is_callable( $control['callback'] ) ) {
				continue;
			}

			ob_start();
				call_user_func_array( $control['callback'], $control['params'] );
			ob_end_clean();
			break;
		}
	}

	if ( isset( $_POST['delete_widget'] ) && $_POST['delete_widget'] ) {
		$sidebars[ $sidebar_id ] = $sidebar;
		wp_set_sidebars_widgets( $sidebars );
		echo "deleted:$widget_id";
		wp_die();
	}

	if ( ! empty( $_POST['add_new'] ) ) {
		wp_die();
	}

	$form = $wp_registered_widget_controls[ $widget_id ];
	if ( $form ) {
		call_user_func_array( $form['callback'], $form['params'] );
	}

	wp_die();
}

/**
 * Handles updating a widget via AJAX.
 *
 * @since 3.9.0
 *
 * @global WP_Customize_Manager $wp_customize
 */
function wp_ajax_update_widget() {
	global $wp_customize;
	$wp_customize->widgets->wp_ajax_update_widget();
}

/**
 * Handles removing inactive widgets via AJAX.
 *
 * @since 4.4.0
 */
function wp_ajax_delete_inactive_widgets() {
	check_ajax_referer( 'remove-inactive-widgets', 'removeinactivewidgets' );

	if ( ! current_user_can( 'edit_theme_options' ) ) {
		wp_die( -1 );
	}

	unset( $_POST['removeinactivewidgets'], $_POST['action'] );
	/** This action is documented in wp-admin/includes/ajax-actions.php */
	do_action( 'load-widgets.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
	/** This action is documented in wp-admin/includes/ajax-actions.php */
	do_action( 'widgets.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
	/** This action is documented in wp-admin/widgets.php */
	do_action( 'sidebar_admin_setup' );

	$sidebars_widgets = wp_get_sidebars_widgets();

	foreach ( $sidebars_widgets['wp_inactive_widgets'] as $key => $widget_id ) {
		$pieces       = explode( '-', $widget_id );
		$multi_number = array_pop( $pieces );
		$id_base      = implode( '-', $pieces );
		$widget       = get_option( 'widget_' . $id_base );
		unset( $widget[ $multi_number ] );
		update_option( 'widget_' . $id_base, $widget );
		unset( $sidebars_widgets['wp_inactive_widgets'][ $key ] );
	}

	wp_set_sidebars_widgets( $sidebars_widgets );

	wp_die();
}

/**
 * Handles creating missing image sub-sizes for just uploaded images via AJAX.
 *
 * @since 5.3.0
 */
function wp_ajax_media_create_image_subsizes() {
	check_ajax_referer( 'media-form' );

	if ( ! current_user_can( 'upload_files' ) ) {
		wp_send_json_error( array( 'message' => __( 'Sorry, you are not allowed to upload files.' ) ) );
	}

	if ( empty( $_POST['attachment_id'] ) ) {
		wp_send_json_error( array( 'message' => __( 'Upload failed. Please reload and try again.' ) ) );
	}

	$attachment_id = (int) $_POST['attachment_id'];

	if ( ! empty( $_POST['_wp_upload_failed_cleanup'] ) ) {
		// Upload failed. Cleanup.
		if ( wp_attachment_is_image( $attachment_id ) && current_user_can( 'delete_post', $attachment_id ) ) {
			$attachment = get_post( $attachment_id );

			// Created at most 10 min ago.
			if ( $attachment && ( time() - strtotime( $attachment->post_date_gmt ) < 600 ) ) {
				wp_delete_attachment( $attachment_id, true );
				wp_send_json_success();
			}
		}
	}

	/*
	 * Set a custom header with the attachment_id.
	 * Used by the browser/client to resume creating image sub-sizes after a PHP fatal error.
	 */
	if ( ! headers_sent() ) {
		header( 'X-WP-Upload-Attachment-ID: ' . $attachment_id );
	}

	/*
	 * This can still be pretty slow and cause timeout or out of memory errors.
	 * The js that handles the response would need to also handle HTTP 500 errors.
	 */
	wp_update_image_subsizes( $attachment_id );

	if ( ! empty( $_POST['_legacy_support'] ) ) {
		// The old (inline) uploader. Only needs the attachment_id.
		$response = array( 'id' => $attachment_id );
	} else {
		// Media modal and Media Library grid view.
		$response = wp_prepare_attachment_for_js( $attachment_id );

		if ( ! $response ) {
			wp_send_json_error( array( 'message' => __( 'Upload failed.' ) ) );
		}
	}

	// At this point the image has been uploaded successfully.
	wp_send_json_success( $response );
}

/**
 * Handles uploading attachments via AJAX.
 *
 * @since 3.3.0
 */
function wp_ajax_upload_attachment() {
	check_ajax_referer( 'media-form' );
	/*
	 * This function does not use wp_send_json_success() / wp_send_json_error()
	 * as the html4 Plupload handler requires a text/html Content-Type for older IE.
	 * See https://core.trac.wordpress.org/ticket/31037
	 */

	if ( ! current_user_can( 'upload_files' ) ) {
		echo wp_json_encode(
			array(
				'success' => false,
				'data'    => array(
					'message'  => __( 'Sorry, you are not allowed to upload files.' ),
					'filename' => esc_html( $_FILES['async-upload']['name'] ),
				),
			)
		);

		wp_die();
	}

	if ( isset( $_REQUEST['post_id'] ) ) {
		$post_id = $_REQUEST['post_id'];

		if ( ! current_user_can( 'edit_post', $post_id ) ) {
			echo wp_json_encode(
				array(
					'success' => false,
					'data'    => array(
						'message'  => __( 'Sorry, you are not allowed to attach files to this post.' ),
						'filename' => esc_html( $_FILES['async-upload']['name'] ),
					),
				)
			);

			wp_die();
		}
	} else {
		$post_id = null;
	}

	$post_data = ! empty( $_REQUEST['post_data'] ) ? _wp_get_allowed_postdata( _wp_translate_postdata( false, (array) $_REQUEST['post_data'] ) ) : array();

	if ( is_wp_error( $post_data ) ) {
		wp_die( $post_data->get_error_message() );
	}

	// If the context is custom header or background, make sure the uploaded file is an image.
	if ( isset( $post_data['context'] ) && in_array( $post_data['context'], array( 'custom-header', 'custom-background' ), true ) ) {
		$wp_filetype = wp_check_filetype_and_ext( $_FILES['async-upload']['tmp_name'], $_FILES['async-upload']['name'] );

		if ( ! wp_match_mime_types( 'image', $wp_filetype['type'] ) ) {
			echo wp_json_encode(
				array(
					'success' => false,
					'data'    => array(
						'message'  => __( 'The uploaded file is not a valid image. Please try again.' ),
						'filename' => esc_html( $_FILES['async-upload']['name'] ),
					),
				)
			);

			wp_die();
		}
	}

	$attachment_id = media_handle_upload( 'async-upload', $post_id, $post_data );

	if ( is_wp_error( $attachment_id ) ) {
		echo wp_json_encode(
			array(
				'success' => false,
				'data'    => array(
					'message'  => $attachment_id->get_error_message(),
					'filename' => esc_html( $_FILES['async-upload']['name'] ),
				),
			)
		);

		wp_die();
	}

	if ( isset( $post_data['context'] ) && isset( $post_data['theme'] ) ) {
		if ( 'custom-background' === $post_data['context'] ) {
			update_post_meta( $attachment_id, '_wp_attachment_is_custom_background', $post_data['theme'] );
		}

		if ( 'custom-header' === $post_data['context'] ) {
			update_post_meta( $attachment_id, '_wp_attachment_is_custom_header', $post_data['theme'] );
		}
	}

	$attachment = wp_prepare_attachment_for_js( $attachment_id );
	if ( ! $attachment ) {
		wp_die();
	}

	echo wp_json_encode(
		array(
			'success' => true,
			'data'    => $attachment,
		)
	);

	wp_die();
}

/**
 * Handles image editing via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_image_editor() {
	$attachment_id = (int) $_POST['postid'];

	if ( empty( $attachment_id ) || ! current_user_can( 'edit_post', $attachment_id ) ) {
		wp_die( -1 );
	}

	check_ajax_referer( "image_editor-$attachment_id" );
	require_once ABSPATH . 'wp-admin/includes/image-edit.php';

	$msg = false;

	switch ( $_POST['do'] ) {
		case 'save':
			$msg = wp_save_image( $attachment_id );
			if ( ! empty( $msg->error ) ) {
				wp_send_json_error( $msg );
			}

			wp_send_json_success( $msg );
			break;
		case 'scale':
			$msg = wp_save_image( $attachment_id );
			break;
		case 'restore':
			$msg = wp_restore_image( $attachment_id );
			break;
	}

	ob_start();
	wp_image_editor( $attachment_id, $msg );
	$html = ob_get_clean();

	if ( ! empty( $msg->error ) ) {
		wp_send_json_error(
			array(
				'message' => $msg,
				'html'    => $html,
			)
		);
	}

	wp_send_json_success(
		array(
			'message' => $msg,
			'html'    => $html,
		)
	);
}

/**
 * Handles setting the featured image via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_set_post_thumbnail() {
	$json = ! empty( $_REQUEST['json'] ); // New-style request.

	$post_id = (int) $_POST['post_id'];
	if ( ! current_user_can( 'edit_post', $post_id ) ) {
		wp_die( -1 );
	}

	$thumbnail_id = (int) $_POST['thumbnail_id'];

	if ( $json ) {
		check_ajax_referer( "update-post_$post_id" );
	} else {
		check_ajax_referer( "set_post_thumbnail-$post_id" );
	}

	if ( -1 === $thumbnail_id ) {
		if ( delete_post_thumbnail( $post_id ) ) {
			$return = _wp_post_thumbnail_html( null, $post_id );
			$json ? wp_send_json_success( $return ) : wp_die( $return );
		} else {
			wp_die( 0 );
		}
	}

	if ( set_post_thumbnail( $post_id, $thumbnail_id ) ) {
		$return = _wp_post_thumbnail_html( $thumbnail_id, $post_id );
		$json ? wp_send_json_success( $return ) : wp_die( $return );
	}

	wp_die( 0 );
}

/**
 * Handles retrieving HTML for the featured image via AJAX.
 *
 * @since 4.6.0
 */
function wp_ajax_get_post_thumbnail_html() {
	$post_id = (int) $_POST['post_id'];

	check_ajax_referer( "update-post_$post_id" );

	if ( ! current_user_can( 'edit_post', $post_id ) ) {
		wp_die( -1 );
	}

	$thumbnail_id = (int) $_POST['thumbnail_id'];

	// For backward compatibility, -1 refers to no featured image.
	if ( -1 === $thumbnail_id ) {
		$thumbnail_id = null;
	}

	$return = _wp_post_thumbnail_html( $thumbnail_id, $post_id );
	wp_send_json_success( $return );
}

/**
 * Handles setting the featured image for an attachment via AJAX.
 *
 * @since 4.0.0
 *
 * @see set_post_thumbnail()
 */
function wp_ajax_set_attachment_thumbnail() {
	if ( empty( $_POST['urls'] ) || ! is_array( $_POST['urls'] ) ) {
		wp_send_json_error();
	}

	$thumbnail_id = (int) $_POST['thumbnail_id'];
	if ( empty( $thumbnail_id ) ) {
		wp_send_json_error();
	}

	if ( false === check_ajax_referer( 'set-attachment-thumbnail', '_ajax_nonce', false ) ) {
		wp_send_json_error();
	}

	$post_ids = array();
	// For each URL, try to find its corresponding post ID.
	foreach ( $_POST['urls'] as $url ) {
		$post_id = attachment_url_to_postid( $url );
		if ( ! empty( $post_id ) ) {
			$post_ids[] = $post_id;
		}
	}

	if ( empty( $post_ids ) ) {
		wp_send_json_error();
	}

	$success = 0;
	// For each found attachment, set its thumbnail.
	foreach ( $post_ids as $post_id ) {
		if ( ! current_user_can( 'edit_post', $post_id ) ) {
			continue;
		}

		if ( set_post_thumbnail( $post_id, $thumbnail_id ) ) {
			++$success;
		}
	}

	if ( 0 === $success ) {
		wp_send_json_error();
	} else {
		wp_send_json_success();
	}

	wp_send_json_error();
}

/**
 * Handles formatting a date via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_date_format() {
	wp_die( date_i18n( sanitize_option( 'date_format', wp_unslash( $_POST['date'] ) ) ) );
}

/**
 * Handles formatting a time via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_time_format() {
	wp_die( date_i18n( sanitize_option( 'time_format', wp_unslash( $_POST['date'] ) ) ) );
}

/**
 * Handles saving posts from the fullscreen editor via AJAX.
 *
 * @since 3.1.0
 * @deprecated 4.3.0
 */
function wp_ajax_wp_fullscreen_save_post() {
	$post_id = isset( $_POST['post_ID'] ) ? (int) $_POST['post_ID'] : 0;

	$post = null;

	if ( $post_id ) {
		$post = get_post( $post_id );
	}

	check_ajax_referer( 'update-post_' . $post_id, '_wpnonce' );

	$post_id = edit_post();

	if ( is_wp_error( $post_id ) ) {
		wp_send_json_error();
	}

	if ( $post ) {
		$last_date = mysql2date( __( 'F j, Y' ), $post->post_modified );
		$last_time = mysql2date( __( 'g:i a' ), $post->post_modified );
	} else {
		$last_date = date_i18n( __( 'F j, Y' ) );
		$last_time = date_i18n( __( 'g:i a' ) );
	}

	$last_id = get_post_meta( $post_id, '_edit_last', true );
	if ( $last_id ) {
		$last_user = get_userdata( $last_id );
		/* translators: 1: User's display name, 2: Date of last edit, 3: Time of last edit. */
		$last_edited = sprintf( __( 'Last edited by %1$s on %2$s at %3$s' ), esc_html( $last_user->display_name ), $last_date, $last_time );
	} else {
		/* translators: 1: Date of last edit, 2: Time of last edit. */
		$last_edited = sprintf( __( 'Last edited on %1$s at %2$s' ), $last_date, $last_time );
	}

	wp_send_json_success( array( 'last_edited' => $last_edited ) );
}

/**
 * Handles removing a post lock via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_wp_remove_post_lock() {
	if ( empty( $_POST['post_ID'] ) || empty( $_POST['active_post_lock'] ) ) {
		wp_die( 0 );
	}

	$post_id = (int) $_POST['post_ID'];
	$post    = get_post( $post_id );

	if ( ! $post ) {
		wp_die( 0 );
	}

	check_ajax_referer( 'update-post_' . $post_id );

	if ( ! current_user_can( 'edit_post', $post_id ) ) {
		wp_die( -1 );
	}

	$active_lock = array_map( 'absint', explode( ':', $_POST['active_post_lock'] ) );

	if ( get_current_user_id() !== $active_lock[1] ) {
		wp_die( 0 );
	}

	/**
	 * Filters the post lock window duration.
	 *
	 * @since 3.3.0
	 *
	 * @param int $interval The interval in seconds the post lock duration
	 *                      should last, plus 5 seconds. Default 150.
	 */
	$new_lock = ( time() - apply_filters( 'wp_check_post_lock_window', 150 ) + 5 ) . ':' . $active_lock[1];
	update_post_meta( $post_id, '_edit_lock', $new_lock, implode( ':', $active_lock ) );
	wp_die( 1 );
}

/**
 * Handles dismissing a WordPress pointer via AJAX.
 *
 * @since 3.1.0
 */
function wp_ajax_dismiss_wp_pointer() {
	$pointer = $_POST['pointer'];

	if ( sanitize_key( $pointer ) !== $pointer ) {
		wp_die( 0 );
	}

	//  check_ajax_referer( 'dismiss-pointer_' . $pointer );

	$dismissed = array_filter( explode( ',', (string) get_user_meta( get_current_user_id(), 'dismissed_wp_pointers', true ) ) );

	if ( in_array( $pointer, $dismissed, true ) ) {
		wp_die( 0 );
	}

	$dismissed[] = $pointer;
	$dismissed   = implode( ',', $dismissed );

	update_user_meta( get_current_user_id(), 'dismissed_wp_pointers', $dismissed );
	wp_die( 1 );
}

/**
 * Handles getting an attachment via AJAX.
 *
 * @since 3.5.0
 */
function wp_ajax_get_attachment() {
	if ( ! isset( $_REQUEST['id'] ) ) {
		wp_send_json_error();
	}

	$id = absint( $_REQUEST['id'] );
	if ( ! $id ) {
		wp_send_json_error();
	}

	$post = get_post( $id );
	if ( ! $post ) {
		wp_send_json_error();
	}

	if ( 'attachment' !== $post->post_type ) {
		wp_send_json_error();
	}

	if ( ! current_user_can( 'upload_files' ) ) {
		wp_send_json_error();
	}

	$attachment = wp_prepare_attachment_for_js( $id );
	if ( ! $attachment ) {
		wp_send_json_error();
	}

	wp_send_json_success( $attachment );
}

/**
 * Handles querying attachments via AJAX.
 *
 * @since 3.5.0
 */
function wp_ajax_query_attachments() {
	if ( ! current_user_can( 'upload_files' ) ) {
		wp_send_json_error();
	}

	$query = isset( $_REQUEST['query'] ) ? (array) $_REQUEST['query'] : array();
	$keys  = array(
		's',
		'order',
		'orderby',
		'posts_per_page',
		'paged',
		'post_mime_type',
		'post_parent',
		'author',
		'post__in',
		'post__not_in',
		'year',
		'monthnum',
	);

	foreach ( get_taxonomies_for_attachments( 'objects' ) as $t ) {
		if ( $t->query_var && isset( $query[ $t->query_var ] ) ) {
			$keys[] = $t->query_var;
		}
	}

	$query              = array_intersect_key( $query, array_flip( $keys ) );
	$query['post_type'] = 'attachment';

	if (
		MEDIA_TRASH &&
		! empty( $_REQUEST['query']['post_status'] ) &&
		'trash' === $_REQUEST['query']['post_status']
	) {
		$query['post_status'] = 'trash';
	} else {
		$query['post_status'] = 'inherit';
	}

	if ( current_user_can( get_post_type_object( 'attachment' )->cap->read_private_posts ) ) {
		$query['post_status'] .= ',private';
	}

	// Filter query clauses to include filenames.
	if ( isset( $query['s'] ) ) {
		add_filter( 'wp_allow_query_attachment_by_filename', '__return_true' );
	}

	/**
	 * Filters the arguments passed to WP_Query during an Ajax
	 * call for querying attachments.
	 *
	 * @since 3.7.0
	 *
	 * @see WP_Query::parse_query()
	 *
	 * @param array $query An array of query variables.
	 */
	$query             = apply_filters( 'ajax_query_attachments_args', $query );
	$attachments_query = new WP_Query( $query );
	update_post_parent_caches( $attachments_query->posts );

	$posts       = array_map( 'wp_prepare_attachment_for_js', $attachments_query->posts );
	$posts       = array_filter( $posts );
	$total_posts = $attachments_query->found_posts;

	if ( $total_posts < 1 ) {
		// Out-of-bounds, run the query again without LIMIT for total count.
		unset( $query['paged'] );

		$count_query = new WP_Query();
		$count_query->query( $query );
		$total_posts = $count_query->found_posts;
	}

	$posts_per_page = (int) $attachments_query->get( 'posts_per_page' );

	$max_pages = $posts_per_page ? (int) ceil( $total_posts / $posts_per_page ) : 0;

	header( 'X-WP-Total: ' . (int) $total_posts );
	header( 'X-WP-TotalPages: ' . $max_pages );

	wp_send_json_success( $posts );
}

/**
 * Handles updating attachment attributes via AJAX.
 *
 * @since 3.5.0
 */
function wp_ajax_save_attachment() {
	if ( ! isset( $_REQUEST['id'] ) || ! isset( $_REQUEST['changes'] ) ) {
		wp_send_json_error();
	}

	$id = absint( $_REQUEST['id'] );
	if ( ! $id ) {
		wp_send_json_error();
	}

	check_ajax_referer( 'update-post_' . $id, 'nonce' );

	if ( ! current_user_can( 'edit_post', $id ) ) {
		wp_send_json_error();
	}

	$changes = $_REQUEST['changes'];
	$post    = get_post( $id, ARRAY_A );

	if ( 'attachment' !== $post['post_type'] ) {
		wp_send_json_error();
	}

	if ( isset( $changes['parent'] ) ) {
		$post['post_parent'] = $changes['parent'];
	}

	if ( isset( $changes['title'] ) ) {
		$post['post_title'] = $changes['title'];
	}

	if ( isset( $changes['caption'] ) ) {
		$post['post_excerpt'] = $changes['caption'];
	}

	if ( isset( $changes['description'] ) ) {
		$post['post_content'] = $changes['description'];
	}

	if ( MEDIA_TRASH && isset( $changes['status'] ) ) {
		$post['post_status'] = $changes['status'];
	}

	if ( isset( $changes['alt'] ) ) {
		$alt = wp_unslash( $changes['alt'] );
		if ( get_post_meta( $id, '_wp_attachment_image_alt', true ) !== $alt ) {
			$alt = wp_strip_all_tags( $alt, true );
			update_post_meta( $id, '_wp_attachment_image_alt', wp_slash( $alt ) );
		}
	}

	if ( wp_attachment_is( 'audio', $post['ID'] ) ) {
		$changed = false;
		$id3data = wp_get_attachment_metadata( $post['ID'] );

		if ( ! is_array( $id3data ) ) {
			$changed = true;
			$id3data = array();
		}

		foreach ( wp_get_attachment_id3_keys( (object) $post, 'edit' ) as $key => $label ) {
			if ( isset( $changes[ $key ] ) ) {
				$changed         = true;
				$id3data[ $key ] = sanitize_text_field( wp_unslash( $changes[ $key ] ) );
			}
		}

		if ( $changed ) {
			wp_update_attachment_metadata( $id, $id3data );
		}
	}

	if ( MEDIA_TRASH && isset( $changes['status'] ) && 'trash' === $changes['status'] ) {
		wp_delete_post( $id );
	} else {
		wp_update_post( $post );
	}

	wp_send_json_success();
}

/**
 * Handles saving backward compatible attachment attributes via AJAX.
 *
 * @since 3.5.0
 */
function wp_ajax_save_attachment_compat() {
	if ( ! isset( $_REQUEST['id'] ) ) {
		wp_send_json_error();
	}

	$id = absint( $_REQUEST['id'] );
	if ( ! $id ) {
		wp_send_json_error();
	}

	if ( empty( $_REQUEST['attachments'] ) || empty( $_REQUEST['attachments'][ $id ] ) ) {
		wp_send_json_error();
	}

	$attachment_data = $_REQUEST['attachments'][ $id ];

	check_ajax_referer( 'update-post_' . $id, 'nonce' );

	if ( ! current_user_can( 'edit_post', $id ) ) {
		wp_send_json_error();
	}

	$post = get_post( $id, ARRAY_A );

	if ( 'attachment' !== $post['post_type'] ) {
		wp_send_json_error();
	}

	/** This filter is documented in wp-admin/includes/media.php */
	$post = apply_filters( 'attachment_fields_to_save', $post, $attachment_data );

	if ( isset( $post['errors'] ) ) {
		$errors = $post['errors']; // @todo return me and display me!
		unset( $post['errors'] );
	}

	wp_update_post( $post );

	foreach ( get_attachment_taxonomies( $post ) as $taxonomy ) {
		if ( isset( $attachment_data[ $taxonomy ] ) ) {
			wp_set_object_terms( $id, array_map( 'trim', preg_split( '/,+/', $attachment_data[ $taxonomy ] ) ), $taxonomy, false );
		}
	}

	$attachment = wp_prepare_attachment_for_js( $id );

	if ( ! $attachment ) {
		wp_send_json_error();
	}

	wp_send_json_success( $attachment );
}

/**
 * Handles saving the attachment order via AJAX.
 *
 * @since 3.5.0
 */
function wp_ajax_save_attachment_order() {
	if ( ! isset( $_REQUEST['post_id'] ) ) {
		wp_send_json_error();
	}

	$post_id = absint( $_REQUEST['post_id'] );
	if ( ! $post_id ) {
		wp_send_json_error();
	}

	if ( empty( $_REQUEST['attachments'] ) ) {
		wp_send_json_error();
	}

	check_ajax_referer( 'update-post_' . $post_id, 'nonce' );

	$attachments = $_REQUEST['attachments'];

	if ( ! current_user_can( 'edit_post', $post_id ) ) {
		wp_send_json_error();
	}

	foreach ( $attachments as $attachment_id => $menu_order ) {
		if ( ! current_user_can( 'edit_post', $attachment_id ) ) {
			continue;
		}

		$attachment = get_post( $attachment_id );

		if ( ! $attachment ) {
			continue;
		}

		if ( 'attachment' !== $attachment->post_type ) {
			continue;
		}

		wp_update_post(
			array(
				'ID'         => $attachment_id,
				'menu_order' => $menu_order,
			)
		);
	}

	wp_send_json_success();
}

/**
 * Handles sending an attachment to the editor via AJAX.
 *
 * Generates the HTML to send an attachment to the editor.
 * Backward compatible with the {@see 'media_send_to_editor'} filter
 * and the chain of filters that follow.
 *
 * @since 3.5.0
 */
function wp_ajax_send_attachment_to_editor() {
	check_ajax_referer( 'media-send-to-editor', 'nonce' );

	$attachment = wp_unslash( $_POST['attachment'] );

	$id = (int) $attachment['id'];

	$post = get_post( $id );
	if ( ! $post ) {
		wp_send_json_error();
	}

	if ( 'attachment' !== $post->post_type ) {
		wp_send_json_error();
	}

	if ( current_user_can( 'edit_post', $id ) ) {
		// If this attachment is unattached, attach it. Primarily a back compat thing.
		$insert_into_post_id = (int) $_POST['post_id'];

		if ( 0 === $post->post_parent && $insert_into_post_id ) {
			wp_update_post(
				array(
					'ID'          => $id,
					'post_parent' => $insert_into_post_id,
				)
			);
		}
	}

	$url = empty( $attachment['url'] ) ? '' : $attachment['url'];
	$rel = ( str_contains( $url, 'attachment_id' ) || get_attachment_link( $id ) === $url );

	remove_filter( 'media_send_to_editor', 'image_media_send_to_editor' );

	if ( str_starts_with( $post->post_mime_type, 'image' ) ) {
		$align = isset( $attachment['align'] ) ? $attachment['align'] : 'none';
		$size  = isset( $attachment['image-size'] ) ? $attachment['image-size'] : 'medium';
		$alt   = isset( $attachment['image_alt'] ) ? $attachment['image_alt'] : '';

		// No whitespace-only captions.
		$caption = isset( $attachment['post_excerpt'] ) ? $attachment['post_excerpt'] : '';
		if ( '' === trim( $caption ) ) {
			$caption = '';
		}

		$title = ''; // We no longer insert title tags into <img> tags, as they are redundant.
		$html  = get_image_send_to_editor( $id, $caption, $title, $align, $url, $rel, $size, $alt );
	} elseif ( wp_attachment_is( 'video', $post ) || wp_attachment_is( 'audio', $post ) ) {
		$html = stripslashes_deep( $_POST['html'] );
	} else {
		$html = isset( $attachment['post_title'] ) ? $attachment['post_title'] : '';
		$rel  = $rel ? ' rel="attachment wp-att-' . $id . '"' : ''; // Hard-coded string, $id is already sanitized.

		if ( ! empty( $url ) ) {
			$html = '<a href="' . esc_url( $url ) . '"' . $rel . '>' . $html . '</a>';
		}
	}

	/** This filter is documented in wp-admin/includes/media.php */
	$html = apply_filters( 'media_send_to_editor', $html, $id, $attachment );

	wp_send_json_success( $html );
}

/**
 * Handles sending a link to the editor via AJAX.
 *
 * Generates the HTML to send a non-image embed link to the editor.
 *
 * Backward compatible with the following filters:
 * - file_send_to_editor_url
 * - audio_send_to_editor_url
 * - video_send_to_editor_url
 *
 * @since 3.5.0
 *
 * @global WP_Post  $post     Global post object.
 * @global WP_Embed $wp_embed WordPress Embed object.
 */
function wp_ajax_send_link_to_editor() {
	global $post, $wp_embed;

	check_ajax_referer( 'media-send-to-editor', 'nonce' );

	$src = wp_unslash( $_POST['src'] );
	if ( ! $src ) {
		wp_send_json_error();
	}

	if ( ! strpos( $src, '://' ) ) {
		$src = 'http://' . $src;
	}

	$src = sanitize_url( $src );
	if ( ! $src ) {
		wp_send_json_error();
	}

	$link_text = trim( wp_unslash( $_POST['link_text'] ) );
	if ( ! $link_text ) {
		$link_text = wp_basename( $src );
	}

	$post = get_post( isset( $_POST['post_id'] ) ? $_POST['post_id'] : 0 );

	// Ping WordPress for an embed.
	$check_embed = $wp_embed->run_shortcode( '[embed]' . $src . '[/embed]' );

	// Fallback that WordPress creates when no oEmbed was found.
	$fallback = $wp_embed->maybe_make_link( $src );

	if ( $check_embed !== $fallback ) {
		// TinyMCE view for [embed] will parse this.
		$html = '[embed]' . $src . '[/embed]';
	} elseif ( $link_text ) {
		$html = '<a href="' . esc_url( $src ) . '">' . $link_text . '</a>';
	} else {
		$html = '';
	}

	// Figure out what filter to run:
	$type = 'file';
	$ext  = preg_replace( '/^.+?\.([^.]+)$/', '$1', $src );
	if ( $ext ) {
		$ext_type = wp_ext2type( $ext );
		if ( 'audio' === $ext_type || 'video' === $ext_type ) {
			$type = $ext_type;
		}
	}

	/** This filter is documented in wp-admin/includes/media.php */
	$html = apply_filters( "{$type}_send_to_editor_url", $html, $src, $link_text );

	wp_send_json_success( $html );
}

/**
 * Handles the Heartbeat API via AJAX.
 *
 * Runs when the user is logged in.
 *
 * @since 3.6.0
 */
function wp_ajax_heartbeat() {
	if ( empty( $_POST['_nonce'] ) ) {
		wp_send_json_error();
	}

	$response    = array();
	$data        = array();
	$nonce_state = wp_verify_nonce( $_POST['_nonce'], 'heartbeat-nonce' );

	// 'screen_id' is the same as $current_screen->id and the JS global 'pagenow'.
	if ( ! empty( $_POST['screen_id'] ) ) {
		$screen_id = sanitize_key( $_POST['screen_id'] );
	} else {
		$screen_id = 'front';
	}

	if ( ! empty( $_POST['data'] ) ) {
		$data = wp_unslash( (array) $_POST['data'] );
	}

	if ( 1 !== $nonce_state ) {
		/**
		 * Filters the nonces to send to the New/Edit Post screen.
		 *
		 * @since 4.3.0
		 *
		 * @param array  $response  The Heartbeat response.
		 * @param array  $data      The $_POST data sent.
		 * @param string $screen_id The screen ID.
		 */
		$response = apply_filters( 'wp_refresh_nonces', $response, $data, $screen_id );

		if ( false === $nonce_state ) {
			// User is logged in but nonces have expired.
			$response['nonces_expired'] = true;
			wp_send_json( $response );
		}
	}

	if ( ! empty( $data ) ) {
		/**
		 * Filters the Heartbeat response received.
		 *
		 * @since 3.6.0
		 *
		 * @param array  $response  The Heartbeat response.
		 * @param array  $data      The $_POST data sent.
		 * @param string $screen_id The screen ID.
		 */
		$response = apply_filters( 'heartbeat_received', $response, $data, $screen_id );
	}

	/**
	 * Filters the Heartbeat response sent.
	 *
	 * @since 3.6.0
	 *
	 * @param array  $response  The Heartbeat response.
	 * @param string $screen_id The screen ID.
	 */
	$response = apply_filters( 'heartbeat_send', $response, $screen_id );

	/**
	 * Fires when Heartbeat ticks in logged-in environments.
	 *
	 * Allows the transport to be easily replaced with long-polling.
	 *
	 * @since 3.6.0
	 *
	 * @param array  $response  The Heartbeat response.
	 * @param string $screen_id The screen ID.
	 */
	do_action( 'heartbeat_tick', $response, $screen_id );

	// Send the current time according to the server.
	$response['server_time'] = time();

	wp_send_json( $response );
}

/**
 * Handles getting revision diffs via AJAX.
 *
 * @since 3.6.0
 */
function wp_ajax_get_revision_diffs() {
	require ABSPATH . 'wp-admin/includes/revision.php';

	$post = get_post( (int) $_REQUEST['post_id'] );
	if ( ! $post ) {
		wp_send_json_error();
	}

	if ( ! current_user_can( 'edit_post', $post->ID ) ) {
		wp_send_json_error();
	}

	// Really just pre-loading the cache here.
	$revisions = wp_get_post_revisions( $post->ID, array( 'check_enabled' => false ) );
	if ( ! $revisions ) {
		wp_send_json_error();
	}

	$return = array();

	// Increase the script timeout limit to allow ample time for diff UI setup.
	if ( function_exists( 'set_time_limit' ) ) {
		set_time_limit( 5 * MINUTE_IN_SECONDS );
	}

	foreach ( $_REQUEST['compare'] as $compare_key ) {
		list( $compare_from, $compare_to ) = explode( ':', $compare_key ); // from:to

		$return[] = array(
			'id'     => $compare_key,
			'fields' => wp_get_revision_ui_diff( $post, $compare_from, $compare_to ),
		);
	}
	wp_send_json_success( $return );
}

/**
 * Handles auto-saving the selected color scheme for
 * a user's own profile via AJAX.
 *
 * @since 3.8.0
 *
 * @global array $_wp_admin_css_colors
 */
function wp_ajax_save_user_color_scheme() {
	global $_wp_admin_css_colors;

	check_ajax_referer( 'save-color-scheme', 'nonce' );

	$color_scheme = sanitize_key( $_POST['color_scheme'] );

	if ( ! isset( $_wp_admin_css_colors[ $color_scheme ] ) ) {
		wp_send_json_error();
	}

	$previous_color_scheme = get_user_meta( get_current_user_id(), 'admin_color', true );
	update_user_meta( get_current_user_id(), 'admin_color', $color_scheme );

	wp_send_json_success(
		array(
			'previousScheme' => 'admin-color-' . $previous_color_scheme,
			'currentScheme'  => 'admin-color-' . $color_scheme,
		)
	);
}

/**
 * Handles getting themes from themes_api() via AJAX.
 *
 * @since 3.9.0
 *
 * @global array $themes_allowedtags
 * @global array $theme_field_defaults
 */
function wp_ajax_query_themes() {
	global $themes_allowedtags, $theme_field_defaults;

	if ( ! current_user_can( 'install_themes' ) ) {
		wp_send_json_error();
	}

	$args = wp_parse_args(
		wp_unslash( $_REQUEST['request'] ),
		array(
			'per_page' => 20,
			'fields'   => array_merge(
				(array) $theme_field_defaults,
				array(
					'reviews_url' => true, // Explicitly request the reviews URL to be linked from the Add Themes screen.
				)
			),
		)
	);

	if ( isset( $args['browse'] ) && 'favorites' === $args['browse'] && ! isset( $args['user'] ) ) {
		$user = get_user_option( 'wporg_favorites' );
		if ( $user ) {
			$args['user'] = $user;
		}
	}

	$old_filter = isset( $args['browse'] ) ? $args['browse'] : 'search';

	/** This filter is documented in wp-admin/includes/class-wp-theme-install-list-table.php */
	$args = apply_filters( 'install_themes_table_api_args_' . $old_filter, $args );

	$api = themes_api( 'query_themes', $args );

	if ( is_wp_error( $api ) ) {
		wp_send_json_error();
	}

	$update_php = network_admin_url( 'update.php?action=install-theme' );

	$installed_themes = search_theme_directories();

	if ( false === $installed_themes ) {
		$installed_themes = array();
	}

	foreach ( $installed_themes as $theme_slug => $theme_data ) {
		// Ignore child themes.
		if ( str_contains( $theme_slug, '/' ) ) {
			unset( $installed_themes[ $theme_slug ] );
		}
	}

	foreach ( $api->themes as &$theme ) {
		$theme->install_url = add_query_arg(
			array(
				'theme'    => $theme->slug,
				'_wpnonce' => wp_create_nonce( 'install-theme_' . $theme->slug ),
			),
			$update_php
		);

		if ( current_user_can( 'switch_themes' ) ) {
			if ( is_multisite() ) {
				$theme->activate_url = add_query_arg(
					array(
						'action'   => 'enable',
						'_wpnonce' => wp_create_nonce( 'enable-theme_' . $theme->slug ),
						'theme'    => $theme->slug,
					),
					network_admin_url( 'themes.php' )
				);
			} else {
				$theme->activate_url = add_query_arg(
					array(
						'action'     => 'activate',
						'_wpnonce'   => wp_create_nonce( 'switch-theme_' . $theme->slug ),
						'stylesheet' => $theme->slug,
					),
					admin_url( 'themes.php' )
				);
			}
		}

		$is_theme_installed = array_key_exists( $theme->slug, $installed_themes );

		// We only care about installed themes.
		$theme->block_theme = $is_theme_installed && wp_get_theme( $theme->slug )->is_block_theme();

		if ( ! is_multisite() && current_user_can( 'edit_theme_options' ) && current_user_can( 'customize' ) ) {
			$customize_url = $theme->block_theme ? admin_url( 'site-editor.php' ) : wp_customize_url( $theme->slug );

			$theme->customize_url = add_query_arg(
				array(
					'return' => urlencode( network_admin_url( 'theme-install.php', 'relative' ) ),
				),
				$customize_url
			);
		}

		$theme->name        = wp_kses( $theme->name, $themes_allowedtags );
		$theme->author      = wp_kses( $theme->author['display_name'], $themes_allowedtags );
		$theme->version     = wp_kses( $theme->version, $themes_allowedtags );
		$theme->description = wp_kses( $theme->description, $themes_allowedtags );

		$theme->stars = wp_star_rating(
			array(
				'rating' => $theme->rating,
				'type'   => 'percent',
				'number' => $theme->num_ratings,
				'echo'   => false,
			)
		);

		$theme->num_ratings    = number_format_i18n( $theme->num_ratings );
		$theme->preview_url    = set_url_scheme( $theme->preview_url );
		$theme->compatible_wp  = is_wp_version_compatible( $theme->requires );
		$theme->compatible_php = is_php_version_compatible( $theme->requires_php );
	}

	wp_send_json_success( $api );
}

/**
 * Applies [embed] Ajax handlers to a string.
 *
 * @since 4.0.0
 *
 * @global WP_Post    $post          Global post object.
 * @global WP_Embed   $wp_embed      WordPress Embed object.
 * @global WP_Scripts $wp_scripts
 * @global int        $content_width
 */
function wp_ajax_parse_embed() {
	global $post, $wp_embed, $content_width;

	if ( empty( $_POST['shortcode'] ) ) {
		wp_send_json_error();
	}

	$post_id = isset( $_POST['post_ID'] ) ? (int) $_POST['post_ID'] : 0;

	if ( $post_id > 0 ) {
		$post = get_post( $post_id );

		if ( ! $post || ! current_user_can( 'edit_post', $post->ID ) ) {
			wp_send_json_error();
		}
		setup_postdata( $post );
	} elseif ( ! current_user_can( 'edit_posts' ) ) { // See WP_oEmbed_Controller::get_proxy_item_permissions_check().
		wp_send_json_error();
	}

	$shortcode = wp_unslash( $_POST['shortcode'] );

	preg_match( '/' . get_shortcode_regex() . '/s', $shortcode, $matches );
	$atts = shortcode_parse_atts( $matches[3] );

	if ( ! empty( $matches[5] ) ) {
		$url = $matches[5];
	} elseif ( ! empty( $atts['src'] ) ) {
		$url = $atts['src'];
	} else {
		$url = '';
	}

	$parsed                         = false;
	$wp_embed->return_false_on_fail = true;

	if ( 0 === $post_id ) {
		/*
		 * Refresh oEmbeds cached outside of posts that are past their TTL.
		 * Posts are excluded because they have separate logic for refreshing
		 * their post meta caches. See WP_Embed::cache_oembed().
		 */
		$wp_embed->usecache = false;
	}

	if ( is_ssl() && str_starts_with( $url, 'http://' ) ) {
		/*
		 * Admin is ssl and the user pasted non-ssl URL.
		 * Check if the provider supports ssl embeds and use that for the preview.
		 */
		$ssl_shortcode = preg_replace( '%^(\\[embed[^\\]]*\\])http://%i', '$1https://', $shortcode );
		$parsed        = $wp_embed->run_shortcode( $ssl_shortcode );

		if ( ! $parsed ) {
			$no_ssl_support = true;
		}
	}

	// Set $content_width so any embeds fit in the destination iframe.
	if ( isset( $_POST['maxwidth'] ) && is_numeric( $_POST['maxwidth'] ) && $_POST['maxwidth'] > 0 ) {
		if ( ! isset( $content_width ) ) {
			$content_width = (int) $_POST['maxwidth'];
		} else {
			$content_width = min( $content_width, (int) $_POST['maxwidth'] );
		}
	}

	if ( $url && ! $parsed ) {
		$parsed = $wp_embed->run_shortcode( $shortcode );
	}

	if ( ! $parsed ) {
		wp_send_json_error(
			array(
				'type'    => 'not-embeddable',
				/* translators: %s: URL that could not be embedded. */
				'message' => sprintf( __( '%s failed to embed.' ), '<code>' . esc_html( $url ) . '</code>' ),
			)
		);
	}

	if ( has_shortcode( $parsed, 'audio' ) || has_shortcode( $parsed, 'video' ) ) {
		$styles     = '';
		$mce_styles = wpview_media_sandbox_styles();

		foreach ( $mce_styles as $style ) {
			$styles .= sprintf( '<link rel="stylesheet" href="%s" />', $style );
		}

		$html = do_shortcode( $parsed );

		global $wp_scripts;

		if ( ! empty( $wp_scripts ) ) {
			$wp_scripts->done = array();
		}

		ob_start();
		wp_print_scripts( array( 'mediaelement-vimeo', 'wp-mediaelement' ) );
		$scripts = ob_get_clean();

		$parsed = $styles . $html . $scripts;
	}

	if ( ! empty( $no_ssl_support ) || ( is_ssl() && ( preg_match( '%<(iframe|script|embed) [^>]*src="http://%', $parsed ) ||
		preg_match( '%<link [^>]*href="http://%', $parsed ) ) ) ) {
		// Admin is ssl and the embed is not. Iframes, scripts, and other "active content" will be blocked.
		wp_send_json_error(
			array(
				'type'    => 'not-ssl',
				'message' => __( 'This preview is unavailable in the editor.' ),
			)
		);
	}

	$return = array(
		'body' => $parsed,
		'attr' => $wp_embed->last_attr,
	);

	if ( str_contains( $parsed, 'class="wp-embedded-content' ) ) {
		if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) {
			$script_src = includes_url( 'js/wp-embed.js' );
		} else {
			$script_src = includes_url( 'js/wp-embed.min.js' );
		}

		$return['head']    = '<script src="' . $script_src . '"></script>';
		$return['sandbox'] = true;
	}

	wp_send_json_success( $return );
}

/**
 * @since 4.0.0
 *
 * @global WP_Post    $post       Global post object.
 * @global WP_Scripts $wp_scripts
 */
function wp_ajax_parse_media_shortcode() {
	global $post, $wp_scripts;

	if ( empty( $_POST['shortcode'] ) ) {
		wp_send_json_error();
	}

	$shortcode = wp_unslash( $_POST['shortcode'] );

	// Only process previews for media related shortcodes:
	$found_shortcodes = get_shortcode_tags_in_content( $shortcode );
	$media_shortcodes = array(
		'audio',
		'embed',
		'playlist',
		'video',
		'gallery',
	);

	$other_shortcodes = array_diff( $found_shortcodes, $media_shortcodes );

	if ( ! empty( $other_shortcodes ) ) {
		wp_send_json_error();
	}

	if ( ! empty( $_POST['post_ID'] ) ) {
		$post = get_post( (int) $_POST['post_ID'] );
	}

	// The embed shortcode requires a post.
	if ( ! $post || ! current_user_can( 'edit_post', $post->ID ) ) {
		if ( in_array( 'embed', $found_shortcodes, true ) ) {
			wp_send_json_error();
		}
	} else {
		setup_postdata( $post );
	}

	$parsed = do_shortcode( $shortcode );

	if ( empty( $parsed ) ) {
		wp_send_json_error(
			array(
				'type'    => 'no-items',
				'message' => __( 'No items found.' ),
			)
		);
	}

	$head   = '';
	$styles = wpview_media_sandbox_styles();

	foreach ( $styles as $style ) {
		$head .= '<link type="text/css" rel="stylesheet" href="' . $style . '">';
	}

	if ( ! empty( $wp_scripts ) ) {
		$wp_scripts->done = array();
	}

	ob_start();

	echo $parsed;

	if ( 'playlist' === $_REQUEST['type'] ) {
		wp_underscore_playlist_templates();

		wp_print_scripts( 'wp-playlist' );
	} else {
		wp_print_scripts( array( 'mediaelement-vimeo', 'wp-mediaelement' ) );
	}

	wp_send_json_success(
		array(
			'head' => $head,
			'body' => ob_get_clean(),
		)
	);
}

/**
 * Handles destroying multiple open sessions for a user via AJAX.
 *
 * @since 4.1.0
 */
function wp_ajax_destroy_sessions() {
	$user = get_userdata( (int) $_POST['user_id'] );

	if ( $user ) {
		if ( ! current_user_can( 'edit_user', $user->ID ) ) {
			$user = false;
		} elseif ( ! wp_verify_nonce( $_POST['nonce'], 'update-user_' . $user->ID ) ) {
			$user = false;
		}
	}

	if ( ! $user ) {
		wp_send_json_error(
			array(
				'message' => __( 'Could not log out user sessions. Please try again.' ),
			)
		);
	}

	$sessions = WP_Session_Tokens::get_instance( $user->ID );

	if ( get_current_user_id() === $user->ID ) {
		$sessions->destroy_others( wp_get_session_token() );
		$message = __( 'You are now logged out everywhere else.' );
	} else {
		$sessions->destroy_all();
		/* translators: %s: User's display name. */
		$message = sprintf( __( '%s has been logged out.' ), $user->display_name );
	}

	wp_send_json_success( array( 'message' => $message ) );
}

/**
 * Handles cropping an image via AJAX.
 *
 * @since 4.3.0
 */
function wp_ajax_crop_image() {
	$attachment_id = absint( $_POST['id'] );

	check_ajax_referer( 'image_editor-' . $attachment_id, 'nonce' );

	if ( empty( $attachment_id ) || ! current_user_can( 'edit_post', $attachment_id ) ) {
		wp_send_json_error();
	}

	$context = str_replace( '_', '-', $_POST['context'] );
	$data    = array_map( 'absint', $_POST['cropDetails'] );
	$cropped = wp_crop_image( $attachment_id, $data['x1'], $data['y1'], $data['width'], $data['height'], $data['dst_width'], $data['dst_height'] );

	if ( ! $cropped || is_wp_error( $cropped ) ) {
		wp_send_json_error( array( 'message' => __( 'Image could not be processed.' ) ) );
	}

	switch ( $context ) {
		case 'site-icon':
			require_once ABSPATH . 'wp-admin/includes/class-wp-site-icon.php';
			$wp_site_icon = new WP_Site_Icon();

			// Skip creating a new attachment if the attachment is a Site Icon.
			if ( get_post_meta( $attachment_id, '_wp_attachment_context', true ) === $context ) {

				// Delete the temporary cropped file, we don't need it.
				wp_delete_file( $cropped );

				// Additional sizes in wp_prepare_attachment_for_js().
				add_filter( 'image_size_names_choose', array( $wp_site_icon, 'additional_sizes' ) );
				break;
			}

			/** This filter is documented in wp-admin/includes/class-custom-image-header.php */
			$cropped = apply_filters( 'wp_create_file_in_uploads', $cropped, $attachment_id ); // For replication.

			// Copy attachment properties.
			$attachment = wp_copy_parent_attachment_properties( $cropped, $attachment_id, $context );

			// Update the attachment.
			add_filter( 'intermediate_image_sizes_advanced', array( $wp_site_icon, 'additional_sizes' ) );
			$attachment_id = $wp_site_icon->insert_attachment( $attachment, $cropped );
			remove_filter( 'intermediate_image_sizes_advanced', array( $wp_site_icon, 'additional_sizes' ) );

			// Additional sizes in wp_prepare_attachment_for_js().
			add_filter( 'image_size_names_choose', array( $wp_site_icon, 'additional_sizes' ) );
			break;

		default:
			/**
			 * Fires before a cropped image is saved.
			 *
			 * Allows to add filters to modify the way a cropped image is saved.
			 *
			 * @since 4.3.0
			 *
			 * @param string $context       The Customizer control requesting the cropped image.
			 * @param int    $attachment_id The attachment ID of the original image.
			 * @param string $cropped       Path to the cropped image file.
			 */
			do_action( 'wp_ajax_crop_image_pre_save', $context, $attachment_id, $cropped );

			/** This filter is documented in wp-admin/includes/class-custom-image-header.php */
			$cropped = apply_filters( 'wp_create_file_in_uploads', $cropped, $attachment_id ); // For replication.

			// Copy attachment properties.
			$attachment = wp_copy_parent_attachment_properties( $cropped, $attachment_id, $context );

			$attachment_id = wp_insert_attachment( $attachment, $cropped );
			$metadata      = wp_generate_attachment_metadata( $attachment_id, $cropped );

			/**
			 * Filters the cropped image attachment metadata.
			 *
			 * @since 4.3.0
			 *
			 * @see wp_generate_attachment_metadata()
			 *
			 * @param array $metadata Attachment metadata.
			 */
			$metadata = apply_filters( 'wp_ajax_cropped_attachment_metadata', $metadata );
			wp_update_attachment_metadata( $attachment_id, $metadata );

			/**
			 * Filters the attachment ID for a cropped image.
			 *
			 * @since 4.3.0
			 *
			 * @param int    $attachment_id The attachment ID of the cropped image.
			 * @param string $context       The Customizer control requesting the cropped image.
			 */
			$attachment_id = apply_filters( 'wp_ajax_cropped_attachment_id', $attachment_id, $context );
	}

	wp_send_json_success( wp_prepare_attachment_for_js( $attachment_id ) );
}

/**
 * Handles generating a password via AJAX.
 *
 * @since 4.4.0
 */
function wp_ajax_generate_password() {
	wp_send_json_success( wp_generate_password( 24 ) );
}

/**
 * Handles generating a password in the no-privilege context via AJAX.
 *
 * @since 5.7.0
 */
function wp_ajax_nopriv_generate_password() {
	wp_send_json_success( wp_generate_password( 24 ) );
}

/**
 * Handles saving the user's WordPress.org username via AJAX.
 *
 * @since 4.4.0
 */
function wp_ajax_save_wporg_username() {
	if ( ! current_user_can( 'install_themes' ) && ! current_user_can( 'install_plugins' ) ) {
		wp_send_json_error();
	}

	check_ajax_referer( 'save_wporg_username_' . get_current_user_id() );

	$username = isset( $_REQUEST['username'] ) ? wp_unslash( $_REQUEST['username'] ) : false;

	if ( ! $username ) {
		wp_send_json_error();
	}

	wp_send_json_success( update_user_meta( get_current_user_id(), 'wporg_favorites', $username ) );
}

/**
 * Handles installing a theme via AJAX.
 *
 * @since 4.6.0
 *
 * @see Theme_Upgrader
 *
 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
 */
function wp_ajax_install_theme() {
	check_ajax_referer( 'updates' );

	if ( empty( $_POST['slug'] ) ) {
		wp_send_json_error(
			array(
				'slug'         => '',
				'errorCode'    => 'no_theme_specified',
				'errorMessage' => __( 'No theme specified.' ),
			)
		);
	}

	$slug = sanitize_key( wp_unslash( $_POST['slug'] ) );

	$status = array(
		'install' => 'theme',
		'slug'    => $slug,
	);

	if ( ! current_user_can( 'install_themes' ) ) {
		$status['errorMessage'] = __( 'Sorry, you are not allowed to install themes on this site.' );
		wp_send_json_error( $status );
	}

	require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
	require_once ABSPATH . 'wp-admin/includes/theme.php';

	$api = themes_api(
		'theme_information',
		array(
			'slug'   => $slug,
			'fields' => array( 'sections' => false ),
		)
	);

	if ( is_wp_error( $api ) ) {
		$status['errorMessage'] = $api->get_error_message();
		wp_send_json_error( $status );
	}

	$skin     = new WP_Ajax_Upgrader_Skin();
	$upgrader = new Theme_Upgrader( $skin );
	$result   = $upgrader->install( $api->download_link );

	if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
		$status['debug'] = $skin->get_upgrade_messages();
	}

	if ( is_wp_error( $result ) ) {
		$status['errorCode']    = $result->get_error_code();
		$status['errorMessage'] = $result->get_error_message();
		wp_send_json_error( $status );
	} elseif ( is_wp_error( $skin->result ) ) {
		$status['errorCode']    = $skin->result->get_error_code();
		$status['errorMessage'] = $skin->result->get_error_message();
		wp_send_json_error( $status );
	} elseif ( $skin->get_errors()->has_errors() ) {
		$status['errorMessage'] = $skin->get_error_messages();
		wp_send_json_error( $status );
	} elseif ( is_null( $result ) ) {
		global $wp_filesystem;

		$status['errorCode']    = 'unable_to_connect_to_filesystem';
		$status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' );

		// Pass through the error from WP_Filesystem if one was raised.
		if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
			$status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() );
		}

		wp_send_json_error( $status );
	}

	$status['themeName'] = wp_get_theme( $slug )->get( 'Name' );

	if ( current_user_can( 'switch_themes' ) ) {
		if ( is_multisite() ) {
			$status['activateUrl'] = add_query_arg(
				array(
					'action'   => 'enable',
					'_wpnonce' => wp_create_nonce( 'enable-theme_' . $slug ),
					'theme'    => $slug,
				),
				network_admin_url( 'themes.php' )
			);
		} else {
			$status['activateUrl'] = add_query_arg(
				array(
					'action'     => 'activate',
					'_wpnonce'   => wp_create_nonce( 'switch-theme_' . $slug ),
					'stylesheet' => $slug,
				),
				admin_url( 'themes.php' )
			);
		}
	}

	$theme                = wp_get_theme( $slug );
	$status['blockTheme'] = $theme->is_block_theme();

	if ( ! is_multisite() && current_user_can( 'edit_theme_options' ) && current_user_can( 'customize' ) ) {
		$status['customizeUrl'] = add_query_arg(
			array(
				'return' => urlencode( network_admin_url( 'theme-install.php', 'relative' ) ),
			),
			wp_customize_url( $slug )
		);
	}

	/*
	 * See WP_Theme_Install_List_Table::_get_theme_status() if we wanted to check
	 * on post-installation status.
	 */
	wp_send_json_success( $status );
}

/**
 * Handles updating a theme via AJAX.
 *
 * @since 4.6.0
 *
 * @see Theme_Upgrader
 *
 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
 */
function wp_ajax_update_theme() {
	check_ajax_referer( 'updates' );

	if ( empty( $_POST['slug'] ) ) {
		wp_send_json_error(
			array(
				'slug'         => '',
				'errorCode'    => 'no_theme_specified',
				'errorMessage' => __( 'No theme specified.' ),
			)
		);
	}

	$stylesheet = preg_replace( '/[^A-z0-9_\-]/', '', wp_unslash( $_POST['slug'] ) );
	$status     = array(
		'update'     => 'theme',
		'slug'       => $stylesheet,
		'oldVersion' => '',
		'newVersion' => '',
	);

	if ( ! current_user_can( 'update_themes' ) ) {
		$status['errorMessage'] = __( 'Sorry, you are not allowed to update themes for this site.' );
		wp_send_json_error( $status );
	}

	$theme = wp_get_theme( $stylesheet );
	if ( $theme->exists() ) {
		$status['oldVersion'] = $theme->get( 'Version' );
	}

	require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';

	$current = get_site_transient( 'update_themes' );
	if ( empty( $current ) ) {
		wp_update_themes();
	}

	$skin     = new WP_Ajax_Upgrader_Skin();
	$upgrader = new Theme_Upgrader( $skin );
	$result   = $upgrader->bulk_upgrade( array( $stylesheet ) );

	if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
		$status['debug'] = $skin->get_upgrade_messages();
	}

	if ( is_wp_error( $skin->result ) ) {
		$status['errorCode']    = $skin->result->get_error_code();
		$status['errorMessage'] = $skin->result->get_error_message();
		wp_send_json_error( $status );
	} elseif ( $skin->get_errors()->has_errors() ) {
		$status['errorMessage'] = $skin->get_error_messages();
		wp_send_json_error( $status );
	} elseif ( is_array( $result ) && ! empty( $result[ $stylesheet ] ) ) {

		// Theme is already at the latest version.
		if ( true === $result[ $stylesheet ] ) {
			$status['errorMessage'] = $upgrader->strings['up_to_date'];
			wp_send_json_error( $status );
		}

		$theme = wp_get_theme( $stylesheet );
		if ( $theme->exists() ) {
			$status['newVersion'] = $theme->get( 'Version' );
		}

		wp_send_json_success( $status );
	} elseif ( false === $result ) {
		global $wp_filesystem;

		$status['errorCode']    = 'unable_to_connect_to_filesystem';
		$status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' );

		// Pass through the error from WP_Filesystem if one was raised.
		if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
			$status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() );
		}

		wp_send_json_error( $status );
	}

	// An unhandled error occurred.
	$status['errorMessage'] = __( 'Theme update failed.' );
	wp_send_json_error( $status );
}

/**
 * Handles deleting a theme via AJAX.
 *
 * @since 4.6.0
 *
 * @see delete_theme()
 *
 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
 */
function wp_ajax_delete_theme() {
	check_ajax_referer( 'updates' );

	if ( empty( $_POST['slug'] ) ) {
		wp_send_json_error(
			array(
				'slug'         => '',
				'errorCode'    => 'no_theme_specified',
				'errorMessage' => __( 'No theme specified.' ),
			)
		);
	}

	$stylesheet = preg_replace( '/[^A-z0-9_\-]/', '', wp_unslash( $_POST['slug'] ) );
	$status     = array(
		'delete' => 'theme',
		'slug'   => $stylesheet,
	);

	if ( ! current_user_can( 'delete_themes' ) ) {
		$status['errorMessage'] = __( 'Sorry, you are not allowed to delete themes on this site.' );
		wp_send_json_error( $status );
	}

	if ( ! wp_get_theme( $stylesheet )->exists() ) {
		$status['errorMessage'] = __( 'The requested theme does not exist.' );
		wp_send_json_error( $status );
	}

	// Check filesystem credentials. `delete_theme()` will bail otherwise.
	$url = wp_nonce_url( 'themes.php?action=delete&stylesheet=' . urlencode( $stylesheet ), 'delete-theme_' . $stylesheet );

	ob_start();
	$credentials = request_filesystem_credentials( $url );
	ob_end_clean();

	if ( false === $credentials || ! WP_Filesystem( $credentials ) ) {
		global $wp_filesystem;

		$status['errorCode']    = 'unable_to_connect_to_filesystem';
		$status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' );

		// Pass through the error from WP_Filesystem if one was raised.
		if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
			$status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() );
		}

		wp_send_json_error( $status );
	}

	require_once ABSPATH . 'wp-admin/includes/theme.php';

	$result = delete_theme( $stylesheet );

	if ( is_wp_error( $result ) ) {
		$status['errorMessage'] = $result->get_error_message();
		wp_send_json_error( $status );
	} elseif ( false === $result ) {
		$status['errorMessage'] = __( 'Theme could not be deleted.' );
		wp_send_json_error( $status );
	}

	wp_send_json_success( $status );
}

/**
 * Handles installing a plugin via AJAX.
 *
 * @since 4.6.0
 *
 * @see Plugin_Upgrader
 *
 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
 */
function wp_ajax_install_plugin() {
	check_ajax_referer( 'updates' );

	if ( empty( $_POST['slug'] ) ) {
		wp_send_json_error(
			array(
				'slug'         => '',
				'errorCode'    => 'no_plugin_specified',
				'errorMessage' => __( 'No plugin specified.' ),
			)
		);
	}

	$status = array(
		'install' => 'plugin',
		'slug'    => sanitize_key( wp_unslash( $_POST['slug'] ) ),
	);

	if ( ! current_user_can( 'install_plugins' ) ) {
		$status['errorMessage'] = __( 'Sorry, you are not allowed to install plugins on this site.' );
		wp_send_json_error( $status );
	}

	require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
	require_once ABSPATH . 'wp-admin/includes/plugin-install.php';

	$api = plugins_api(
		'plugin_information',
		array(
			'slug'   => sanitize_key( wp_unslash( $_POST['slug'] ) ),
			'fields' => array(
				'sections' => false,
			),
		)
	);

	if ( is_wp_error( $api ) ) {
		$status['errorMessage'] = $api->get_error_message();
		wp_send_json_error( $status );
	}

	$status['pluginName'] = $api->name;

	$skin     = new WP_Ajax_Upgrader_Skin();
	$upgrader = new Plugin_Upgrader( $skin );
	$result   = $upgrader->install( $api->download_link );

	if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
		$status['debug'] = $skin->get_upgrade_messages();
	}

	if ( is_wp_error( $result ) ) {
		$status['errorCode']    = $result->get_error_code();
		$status['errorMessage'] = $result->get_error_message();
		wp_send_json_error( $status );
	} elseif ( is_wp_error( $skin->result ) ) {
		$status['errorCode']    = $skin->result->get_error_code();
		$status['errorMessage'] = $skin->result->get_error_message();
		wp_send_json_error( $status );
	} elseif ( $skin->get_errors()->has_errors() ) {
		$status['errorMessage'] = $skin->get_error_messages();
		wp_send_json_error( $status );
	} elseif ( is_null( $result ) ) {
		global $wp_filesystem;

		$status['errorCode']    = 'unable_to_connect_to_filesystem';
		$status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' );

		// Pass through the error from WP_Filesystem if one was raised.
		if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
			$status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() );
		}

		wp_send_json_error( $status );
	}

	$install_status = install_plugin_install_status( $api );
	$pagenow        = isset( $_POST['pagenow'] ) ? sanitize_key( $_POST['pagenow'] ) : '';

	// If installation request is coming from import page, do not return network activation link.
	$plugins_url = ( 'import' === $pagenow ) ? admin_url( 'plugins.php' ) : network_admin_url( 'plugins.php' );

	if ( current_user_can( 'activate_plugin', $install_status['file'] ) && is_plugin_inactive( $install_status['file'] ) ) {
		$status['activateUrl'] = add_query_arg(
			array(
				'_wpnonce' => wp_create_nonce( 'activate-plugin_' . $install_status['file'] ),
				'action'   => 'activate',
				'plugin'   => $install_status['file'],
			),
			$plugins_url
		);
	}

	if ( is_multisite() && current_user_can( 'manage_network_plugins' ) && 'import' !== $pagenow ) {
		$status['activateUrl'] = add_query_arg( array( 'networkwide' => 1 ), $status['activateUrl'] );
	}

	wp_send_json_success( $status );
}

/**
 * Handles activating a plugin via AJAX.
 *
 * @since 6.5.0
 */
function wp_ajax_activate_plugin() {
	check_ajax_referer( 'updates' );

	if ( empty( $_POST['name'] ) || empty( $_POST['slug'] ) || empty( $_POST['plugin'] ) ) {
		wp_send_json_error(
			array(
				'slug'         => '',
				'pluginName'   => '',
				'plugin'       => '',
				'errorCode'    => 'no_plugin_specified',
				'errorMessage' => __( 'No plugin specified.' ),
			)
		);
	}

	$status = array(
		'activate'   => 'plugin',
		'slug'       => wp_unslash( $_POST['slug'] ),
		'pluginName' => wp_unslash( $_POST['name'] ),
		'plugin'     => wp_unslash( $_POST['plugin'] ),
	);

	if ( ! current_user_can( 'activate_plugin', $status['plugin'] ) ) {
		$status['errorMessage'] = __( 'Sorry, you are not allowed to activate plugins on this site.' );
		wp_send_json_error( $status );
	}

	if ( is_plugin_active( $status['plugin'] ) ) {
		$status['errorMessage'] = sprintf(
			/* translators: %s: Plugin name. */
			__( '%s is already active.' ),
			$status['pluginName']
		);
	}

	$activated = activate_plugin( $status['plugin'] );

	if ( is_wp_error( $activated ) ) {
		$status['errorMessage'] = $activated->get_error_message();
		wp_send_json_error( $status );
	}

	wp_send_json_success( $status );
}

/**
 * Handles updating a plugin via AJAX.
 *
 * @since 4.2.0
 *
 * @see Plugin_Upgrader
 *
 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
 */
function wp_ajax_update_plugin() {
	check_ajax_referer( 'updates' );

	if ( empty( $_POST['plugin'] ) || empty( $_POST['slug'] ) ) {
		wp_send_json_error(
			array(
				'slug'         => '',
				'errorCode'    => 'no_plugin_specified',
				'errorMessage' => __( 'No plugin specified.' ),
			)
		);
	}

	$plugin = plugin_basename( sanitize_text_field( wp_unslash( $_POST['plugin'] ) ) );

	$status = array(
		'update'     => 'plugin',
		'slug'       => sanitize_key( wp_unslash( $_POST['slug'] ) ),
		'oldVersion' => '',
		'newVersion' => '',
	);

	if ( ! current_user_can( 'update_plugins' ) || 0 !== validate_file( $plugin ) ) {
		$status['errorMessage'] = __( 'Sorry, you are not allowed to update plugins for this site.' );
		wp_send_json_error( $status );
	}

	$plugin_data          = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin );
	$status['plugin']     = $plugin;
	$status['pluginName'] = $plugin_data['Name'];

	if ( $plugin_data['Version'] ) {
		/* translators: %s: Plugin version. */
		$status['oldVersion'] = sprintf( __( 'Version %s' ), $plugin_data['Version'] );
	}

	require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';

	wp_update_plugins();

	$skin     = new WP_Ajax_Upgrader_Skin();
	$upgrader = new Plugin_Upgrader( $skin );
	$result   = $upgrader->bulk_upgrade( array( $plugin ) );

	if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
		$status['debug'] = $skin->get_upgrade_messages();
	}

	if ( is_wp_error( $skin->result ) ) {
		$status['errorCode']    = $skin->result->get_error_code();
		$status['errorMessage'] = $skin->result->get_error_message();
		wp_send_json_error( $status );
	} elseif ( $skin->get_errors()->has_errors() ) {
		$status['errorMessage'] = $skin->get_error_messages();
		wp_send_json_error( $status );
	} elseif ( is_array( $result ) && ! empty( $result[ $plugin ] ) ) {

		/*
		 * Plugin is already at the latest version.
		 *
		 * This may also be the return value if the `update_plugins` site transient is empty,
		 * e.g. when you update two plugins in quick succession before the transient repopulates.
		 *
		 * Preferably something can be done to ensure `update_plugins` isn't empty.
		 * For now, surface some sort of error here.
		 */
		if ( true === $result[ $plugin ] ) {
			$status['errorMessage'] = $upgrader->strings['up_to_date'];
			wp_send_json_error( $status );
		}

		$plugin_data = get_plugins( '/' . $result[ $plugin ]['destination_name'] );
		$plugin_data = reset( $plugin_data );

		if ( $plugin_data['Version'] ) {
			/* translators: %s: Plugin version. */
			$status['newVersion'] = sprintf( __( 'Version %s' ), $plugin_data['Version'] );
		}

		wp_send_json_success( $status );
	} elseif ( false === $result ) {
		global $wp_filesystem;

		$status['errorCode']    = 'unable_to_connect_to_filesystem';
		$status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' );

		// Pass through the error from WP_Filesystem if one was raised.
		if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
			$status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() );
		}

		wp_send_json_error( $status );
	}

	// An unhandled error occurred.
	$status['errorMessage'] = __( 'Plugin update failed.' );
	wp_send_json_error( $status );
}

/**
 * Handles deleting a plugin via AJAX.
 *
 * @since 4.6.0
 *
 * @see delete_plugins()
 *
 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
 */
function wp_ajax_delete_plugin() {
	check_ajax_referer( 'updates' );

	if ( empty( $_POST['slug'] ) || empty( $_POST['plugin'] ) ) {
		wp_send_json_error(
			array(
				'slug'         => '',
				'errorCode'    => 'no_plugin_specified',
				'errorMessage' => __( 'No plugin specified.' ),
			)
		);
	}

	$plugin = plugin_basename( sanitize_text_field( wp_unslash( $_POST['plugin'] ) ) );

	$status = array(
		'delete' => 'plugin',
		'slug'   => sanitize_key( wp_unslash( $_POST['slug'] ) ),
	);

	if ( ! current_user_can( 'delete_plugins' ) || 0 !== validate_file( $plugin ) ) {
		$status['errorMessage'] = __( 'Sorry, you are not allowed to delete plugins for this site.' );
		wp_send_json_error( $status );
	}

	$plugin_data          = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin );
	$status['plugin']     = $plugin;
	$status['pluginName'] = $plugin_data['Name'];

	if ( is_plugin_active( $plugin ) ) {
		$status['errorMessage'] = __( 'You cannot delete a plugin while it is active on the main site.' );
		wp_send_json_error( $status );
	}

	// Check filesystem credentials. `delete_plugins()` will bail otherwise.
	$url = wp_nonce_url( 'plugins.php?action=delete-selected&verify-delete=1&checked[]=' . $plugin, 'bulk-plugins' );

	ob_start();
	$credentials = request_filesystem_credentials( $url );
	ob_end_clean();

	if ( false === $credentials || ! WP_Filesystem( $credentials ) ) {
		global $wp_filesystem;

		$status['errorCode']    = 'unable_to_connect_to_filesystem';
		$status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' );

		// Pass through the error from WP_Filesystem if one was raised.
		if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
			$status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() );
		}

		wp_send_json_error( $status );
	}

	$result = delete_plugins( array( $plugin ) );

	if ( is_wp_error( $result ) ) {
		$status['errorMessage'] = $result->get_error_message();
		wp_send_json_error( $status );
	} elseif ( false === $result ) {
		$status['errorMessage'] = __( 'Plugin could not be deleted.' );
		wp_send_json_error( $status );
	}

	wp_send_json_success( $status );
}

/**
 * Handles searching plugins via AJAX.
 *
 * @since 4.6.0
 *
 * @global string $s Search term.
 */
function wp_ajax_search_plugins() {
	check_ajax_referer( 'updates' );

	// Ensure after_plugin_row_{$plugin_file} gets hooked.
	wp_plugin_update_rows();

	$pagenow = isset( $_POST['pagenow'] ) ? sanitize_key( $_POST['pagenow'] ) : '';
	if ( 'plugins-network' === $pagenow || 'plugins' === $pagenow ) {
		set_current_screen( $pagenow );
	}

	/** @var WP_Plugins_List_Table $wp_list_table */
	$wp_list_table = _get_list_table(
		'WP_Plugins_List_Table',
		array(
			'screen' => get_current_screen(),
		)
	);

	$status = array();

	if ( ! $wp_list_table->ajax_user_can() ) {
		$status['errorMessage'] = __( 'Sorry, you are not allowed to manage plugins for this site.' );
		wp_send_json_error( $status );
	}

	// Set the correct requester, so pagination works.
	$_SERVER['REQUEST_URI'] = add_query_arg(
		array_diff_key(
			$_POST,
			array(
				'_ajax_nonce' => null,
				'action'      => null,
			)
		),
		network_admin_url( 'plugins.php', 'relative' )
	);

	$GLOBALS['s'] = wp_unslash( $_POST['s'] );

	$wp_list_table->prepare_items();

	ob_start();
	$wp_list_table->display();
	$status['count'] = count( $wp_list_table->items );
	$status['items'] = ob_get_clean();

	wp_send_json_success( $status );
}

/**
 * Handles searching plugins to install via AJAX.
 *
 * @since 4.6.0
 */
function wp_ajax_search_install_plugins() {
	check_ajax_referer( 'updates' );

	$pagenow = isset( $_POST['pagenow'] ) ? sanitize_key( $_POST['pagenow'] ) : '';
	if ( 'plugin-install-network' === $pagenow || 'plugin-install' === $pagenow ) {
		set_current_screen( $pagenow );
	}

	/** @var WP_Plugin_Install_List_Table $wp_list_table */
	$wp_list_table = _get_list_table(
		'WP_Plugin_Install_List_Table',
		array(
			'screen' => get_current_screen(),
		)
	);

	$status = array();

	if ( ! $wp_list_table->ajax_user_can() ) {
		$status['errorMessage'] = __( 'Sorry, you are not allowed to manage plugins for this site.' );
		wp_send_json_error( $status );
	}

	// Set the correct requester, so pagination works.
	$_SERVER['REQUEST_URI'] = add_query_arg(
		array_diff_key(
			$_POST,
			array(
				'_ajax_nonce' => null,
				'action'      => null,
			)
		),
		network_admin_url( 'plugin-install.php', 'relative' )
	);

	$wp_list_table->prepare_items();

	ob_start();
	$wp_list_table->display();
	$status['count'] = (int) $wp_list_table->get_pagination_arg( 'total_items' );
	$status['items'] = ob_get_clean();

	wp_send_json_success( $status );
}

/**
 * Handles editing a theme or plugin file via AJAX.
 *
 * @since 4.9.0
 *
 * @see wp_edit_theme_plugin_file()
 */
function wp_ajax_edit_theme_plugin_file() {
	$r = wp_edit_theme_plugin_file( wp_unslash( $_POST ) ); // Validation of args is done in wp_edit_theme_plugin_file().

	if ( is_wp_error( $r ) ) {
		wp_send_json_error(
			array_merge(
				array(
					'code'    => $r->get_error_code(),
					'message' => $r->get_error_message(),
				),
				(array) $r->get_error_data()
			)
		);
	} else {
		wp_send_json_success(
			array(
				'message' => __( 'File edited successfully.' ),
			)
		);
	}
}

/**
 * Handles exporting a user's personal data via AJAX.
 *
 * @since 4.9.6
 */
function wp_ajax_wp_privacy_export_personal_data() {

	if ( empty( $_POST['id'] ) ) {
		wp_send_json_error( __( 'Missing request ID.' ) );
	}

	$request_id = (int) $_POST['id'];

	if ( $request_id < 1 ) {
		wp_send_json_error( __( 'Invalid request ID.' ) );
	}

	if ( ! current_user_can( 'export_others_personal_data' ) ) {
		wp_send_json_error( __( 'Sorry, you are not allowed to perform this action.' ) );
	}

	check_ajax_referer( 'wp-privacy-export-personal-data-' . $request_id, 'security' );

	// Get the request.
	$request = wp_get_user_request( $request_id );

	if ( ! $request || 'export_personal_data' !== $request->action_name ) {
		wp_send_json_error( __( 'Invalid request type.' ) );
	}

	$email_address = $request->email;
	if ( ! is_email( $email_address ) ) {
		wp_send_json_error( __( 'A valid email address must be given.' ) );
	}

	if ( ! isset( $_POST['exporter'] ) ) {
		wp_send_json_error( __( 'Missing exporter index.' ) );
	}

	$exporter_index = (int) $_POST['exporter'];

	if ( ! isset( $_POST['page'] ) ) {
		wp_send_json_error( __( 'Missing page index.' ) );
	}

	$page = (int) $_POST['page'];

	$send_as_email = isset( $_POST['sendAsEmail'] ) ? ( 'true' === $_POST['sendAsEmail'] ) : false;

	/**
	 * Filters the array of exporter callbacks.
	 *
	 * @since 4.9.6
	 *
	 * @param array $args {
	 *     An array of callable exporters of personal data. Default empty array.
	 *
	 *     @type array ...$0 {
	 *         Array of personal data exporters.
	 *
	 *         @type callable $callback               Callable exporter function that accepts an
	 *                                                email address and a page number and returns an
	 *                                                array of name => value pairs of personal data.
	 *         @type string   $exporter_friendly_name Translated user facing friendly name for the
	 *                                                exporter.
	 *     }
	 * }
	 */
	$exporters = apply_filters( 'wp_privacy_personal_data_exporters', array() );

	if ( ! is_array( $exporters ) ) {
		wp_send_json_error( __( 'An exporter has improperly used the registration filter.' ) );
	}

	// Do we have any registered exporters?
	if ( 0 < count( $exporters ) ) {
		if ( $exporter_index < 1 ) {
			wp_send_json_error( __( 'Exporter index cannot be negative.' ) );
		}

		if ( $exporter_index > count( $exporters ) ) {
			wp_send_json_error( __( 'Exporter index is out of range.' ) );
		}

		if ( $page < 1 ) {
			wp_send_json_error( __( 'Page index cannot be less than one.' ) );
		}

		$exporter_keys = array_keys( $exporters );
		$exporter_key  = $exporter_keys[ $exporter_index - 1 ];
		$exporter      = $exporters[ $exporter_key ];

		if ( ! is_array( $exporter ) ) {
			wp_send_json_error(
				/* translators: %s: Exporter array index. */
				sprintf( __( 'Expected an array describing the exporter at index %s.' ), $exporter_key )
			);
		}

		if ( ! array_key_exists( 'exporter_friendly_name', $exporter ) ) {
			wp_send_json_error(
				/* translators: %s: Exporter array index. */
				sprintf( __( 'Exporter array at index %s does not include a friendly name.' ), $exporter_key )
			);
		}

		$exporter_friendly_name = $exporter['exporter_friendly_name'];

		if ( ! array_key_exists( 'callback', $exporter ) ) {
			wp_send_json_error(
				/* translators: %s: Exporter friendly name. */
				sprintf( __( 'Exporter does not include a callback: %s.' ), esc_html( $exporter_friendly_name ) )
			);
		}

		if ( ! is_callable( $exporter['callback'] ) ) {
			wp_send_json_error(
				/* translators: %s: Exporter friendly name. */
				sprintf( __( 'Exporter callback is not a valid callback: %s.' ), esc_html( $exporter_friendly_name ) )
			);
		}

		$callback = $exporter['callback'];
		$response = call_user_func( $callback, $email_address, $page );

		if ( is_wp_error( $response ) ) {
			wp_send_json_error( $response );
		}

		if ( ! is_array( $response ) ) {
			wp_send_json_error(
				/* translators: %s: Exporter friendly name. */
				sprintf( __( 'Expected response as an array from exporter: %s.' ), esc_html( $exporter_friendly_name ) )
			);
		}

		if ( ! array_key_exists( 'data', $response ) ) {
			wp_send_json_error(
				/* translators: %s: Exporter friendly name. */
				sprintf( __( 'Expected data in response array from exporter: %s.' ), esc_html( $exporter_friendly_name ) )
			);
		}

		if ( ! is_array( $response['data'] ) ) {
			wp_send_json_error(
				/* translators: %s: Exporter friendly name. */
				sprintf( __( 'Expected data array in response array from exporter: %s.' ), esc_html( $exporter_friendly_name ) )
			);
		}

		if ( ! array_key_exists( 'done', $response ) ) {
			wp_send_json_error(
				/* translators: %s: Exporter friendly name. */
				sprintf( __( 'Expected done (boolean) in response array from exporter: %s.' ), esc_html( $exporter_friendly_name ) )
			);
		}
	} else {
		// No exporters, so we're done.
		$exporter_key = '';

		$response = array(
			'data' => array(),
			'done' => true,
		);
	}

	/**
	 * Filters a page of personal data exporter data. Used to build the export report.
	 *
	 * Allows the export response to be consumed by destinations in addition to Ajax.
	 *
	 * @since 4.9.6
	 *
	 * @param array  $response        The personal data for the given exporter and page number.
	 * @param int    $exporter_index  The index of the exporter that provided this data.
	 * @param string $email_address   The email address associated with this personal data.
	 * @param int    $page            The page number for this response.
	 * @param int    $request_id      The privacy request post ID associated with this request.
	 * @param bool   $send_as_email   Whether the final results of the export should be emailed to the user.
	 * @param string $exporter_key    The key (slug) of the exporter that provided this data.
	 */
	$response = apply_filters( 'wp_privacy_personal_data_export_page', $response, $exporter_index, $email_address, $page, $request_id, $send_as_email, $exporter_key );

	if ( is_wp_error( $response ) ) {
		wp_send_json_error( $response );
	}

	wp_send_json_success( $response );
}

/**
 * Handles erasing personal data via AJAX.
 *
 * @since 4.9.6
 */
function wp_ajax_wp_privacy_erase_personal_data() {

	if ( empty( $_POST['id'] ) ) {
		wp_send_json_error( __( 'Missing request ID.' ) );
	}

	$request_id = (int) $_POST['id'];

	if ( $request_id < 1 ) {
		wp_send_json_error( __( 'Invalid request ID.' ) );
	}

	// Both capabilities are required to avoid confusion, see `_wp_personal_data_removal_page()`.
	if ( ! current_user_can( 'erase_others_personal_data' ) || ! current_user_can( 'delete_users' ) ) {
		wp_send_json_error( __( 'Sorry, you are not allowed to perform this action.' ) );
	}

	check_ajax_referer( 'wp-privacy-erase-personal-data-' . $request_id, 'security' );

	// Get the request.
	$request = wp_get_user_request( $request_id );

	if ( ! $request || 'remove_personal_data' !== $request->action_name ) {
		wp_send_json_error( __( 'Invalid request type.' ) );
	}

	$email_address = $request->email;

	if ( ! is_email( $email_address ) ) {
		wp_send_json_error( __( 'Invalid email address in request.' ) );
	}

	if ( ! isset( $_POST['eraser'] ) ) {
		wp_send_json_error( __( 'Missing eraser index.' ) );
	}

	$eraser_index = (int) $_POST['eraser'];

	if ( ! isset( $_POST['page'] ) ) {
		wp_send_json_error( __( 'Missing page index.' ) );
	}

	$page = (int) $_POST['page'];

	/**
	 * Filters the array of personal data eraser callbacks.
	 *
	 * @since 4.9.6
	 *
	 * @param array $args {
	 *     An array of callable erasers of personal data. Default empty array.
	 *
	 *     @type array ...$0 {
	 *         Array of personal data exporters.
	 *
	 *         @type callable $callback               Callable eraser that accepts an email address and a page
	 *                                                number, and returns an array with boolean values for
	 *                                                whether items were removed or retained and any messages
	 *                                                from the eraser, as well as if additional pages are
	 *                                                available.
	 *         @type string   $exporter_friendly_name Translated user facing friendly name for the eraser.
	 *     }
	 * }
	 */
	$erasers = apply_filters( 'wp_privacy_personal_data_erasers', array() );

	// Do we have any registered erasers?
	if ( 0 < count( $erasers ) ) {

		if ( $eraser_index < 1 ) {
			wp_send_json_error( __( 'Eraser index cannot be less than one.' ) );
		}

		if ( $eraser_index > count( $erasers ) ) {
			wp_send_json_error( __( 'Eraser index is out of range.' ) );
		}

		if ( $page < 1 ) {
			wp_send_json_error( __( 'Page index cannot be less than one.' ) );
		}

		$eraser_keys = array_keys( $erasers );
		$eraser_key  = $eraser_keys[ $eraser_index - 1 ];
		$eraser      = $erasers[ $eraser_key ];

		if ( ! is_array( $eraser ) ) {
			/* translators: %d: Eraser array index. */
			wp_send_json_error( sprintf( __( 'Expected an array describing the eraser at index %d.' ), $eraser_index ) );
		}

		if ( ! array_key_exists( 'eraser_friendly_name', $eraser ) ) {
			/* translators: %d: Eraser array index. */
			wp_send_json_error( sprintf( __( 'Eraser array at index %d does not include a friendly name.' ), $eraser_index ) );
		}

		$eraser_friendly_name = $eraser['eraser_friendly_name'];

		if ( ! array_key_exists( 'callback', $eraser ) ) {
			wp_send_json_error(
				sprintf(
					/* translators: %s: Eraser friendly name. */
					__( 'Eraser does not include a callback: %s.' ),
					esc_html( $eraser_friendly_name )
				)
			);
		}

		if ( ! is_callable( $eraser['callback'] ) ) {
			wp_send_json_error(
				sprintf(
					/* translators: %s: Eraser friendly name. */
					__( 'Eraser callback is not valid: %s.' ),
					esc_html( $eraser_friendly_name )
				)
			);
		}

		$callback = $eraser['callback'];
		$response = call_user_func( $callback, $email_address, $page );

		if ( is_wp_error( $response ) ) {
			wp_send_json_error( $response );
		}

		if ( ! is_array( $response ) ) {
			wp_send_json_error(
				sprintf(
					/* translators: 1: Eraser friendly name, 2: Eraser array index. */
					__( 'Did not receive array from %1$s eraser (index %2$d).' ),
					esc_html( $eraser_friendly_name ),
					$eraser_index
				)
			);
		}

		if ( ! array_key_exists( 'items_removed', $response ) ) {
			wp_send_json_error(
				sprintf(
					/* translators: 1: Eraser friendly name, 2: Eraser array index. */
					__( 'Expected items_removed key in response array from %1$s eraser (index %2$d).' ),
					esc_html( $eraser_friendly_name ),
					$eraser_index
				)
			);
		}

		if ( ! array_key_exists( 'items_retained', $response ) ) {
			wp_send_json_error(
				sprintf(
					/* translators: 1: Eraser friendly name, 2: Eraser array index. */
					__( 'Expected items_retained key in response array from %1$s eraser (index %2$d).' ),
					esc_html( $eraser_friendly_name ),
					$eraser_index
				)
			);
		}

		if ( ! array_key_exists( 'messages', $response ) ) {
			wp_send_json_error(
				sprintf(
					/* translators: 1: Eraser friendly name, 2: Eraser array index. */
					__( 'Expected messages key in response array from %1$s eraser (index %2$d).' ),
					esc_html( $eraser_friendly_name ),
					$eraser_index
				)
			);
		}

		if ( ! is_array( $response['messages'] ) ) {
			wp_send_json_error(
				sprintf(
					/* translators: 1: Eraser friendly name, 2: Eraser array index. */
					__( 'Expected messages key to reference an array in response array from %1$s eraser (index %2$d).' ),
					esc_html( $eraser_friendly_name ),
					$eraser_index
				)
			);
		}

		if ( ! array_key_exists( 'done', $response ) ) {
			wp_send_json_error(
				sprintf(
					/* translators: 1: Eraser friendly name, 2: Eraser array index. */
					__( 'Expected done flag in response array from %1$s eraser (index %2$d).' ),
					esc_html( $eraser_friendly_name ),
					$eraser_index
				)
			);
		}
	} else {
		// No erasers, so we're done.
		$eraser_key = '';

		$response = array(
			'items_removed'  => false,
			'items_retained' => false,
			'messages'       => array(),
			'done'           => true,
		);
	}

	/**
	 * Filters a page of personal data eraser data.
	 *
	 * Allows the erasure response to be consumed by destinations in addition to Ajax.
	 *
	 * @since 4.9.6
	 *
	 * @param array  $response        {
	 *     The personal data for the given exporter and page number.
	 *
	 *     @type bool     $items_removed  Whether items were actually removed or not.
	 *     @type bool     $items_retained Whether items were retained or not.
	 *     @type string[] $messages       An array of messages to add to the personal data export file.
	 *     @type bool     $done           Whether the eraser is finished or not.
	 * }
	 * @param int    $eraser_index    The index of the eraser that provided this data.
	 * @param string $email_address   The email address associated with this personal data.
	 * @param int    $page            The page number for this response.
	 * @param int    $request_id      The privacy request post ID associated with this request.
	 * @param string $eraser_key      The key (slug) of the eraser that provided this data.
	 */
	$response = apply_filters( 'wp_privacy_personal_data_erasure_page', $response, $eraser_index, $email_address, $page, $request_id, $eraser_key );

	if ( is_wp_error( $response ) ) {
		wp_send_json_error( $response );
	}

	wp_send_json_success( $response );
}

/**
 * Handles site health checks on server communication via AJAX.
 *
 * @since 5.2.0
 * @deprecated 5.6.0 Use WP_REST_Site_Health_Controller::test_dotorg_communication()
 * @see WP_REST_Site_Health_Controller::test_dotorg_communication()
 */
function wp_ajax_health_check_dotorg_communication() {
	_doing_it_wrong(
		'wp_ajax_health_check_dotorg_communication',
		sprintf(
		// translators: 1: The Site Health action that is no longer used by core. 2: The new function that replaces it.
			__( 'The Site Health check for %1$s has been replaced with %2$s.' ),
			'wp_ajax_health_check_dotorg_communication',
			'WP_REST_Site_Health_Controller::test_dotorg_communication'
		),
		'5.6.0'
	);

	check_ajax_referer( 'health-check-site-status' );

	if ( ! current_user_can( 'view_site_health_checks' ) ) {
		wp_send_json_error();
	}

	if ( ! class_exists( 'WP_Site_Health' ) ) {
		require_once ABSPATH . 'wp-admin/includes/class-wp-site-health.php';
	}

	$site_health = WP_Site_Health::get_instance();
	wp_send_json_success( $site_health->get_test_dotorg_communication() );
}

/**
 * Handles site health checks on background updates via AJAX.
 *
 * @since 5.2.0
 * @deprecated 5.6.0 Use WP_REST_Site_Health_Controller::test_background_updates()
 * @see WP_REST_Site_Health_Controller::test_background_updates()
 */
function wp_ajax_health_check_background_updates() {
	_doing_it_wrong(
		'wp_ajax_health_check_background_updates',
		sprintf(
		// translators: 1: The Site Health action that is no longer used by core. 2: The new function that replaces it.
			__( 'The Site Health check for %1$s has been replaced with %2$s.' ),
			'wp_ajax_health_check_background_updates',
			'WP_REST_Site_Health_Controller::test_background_updates'
		),
		'5.6.0'
	);

	check_ajax_referer( 'health-check-site-status' );

	if ( ! current_user_can( 'view_site_health_checks' ) ) {
		wp_send_json_error();
	}

	if ( ! class_exists( 'WP_Site_Health' ) ) {
		require_once ABSPATH . 'wp-admin/includes/class-wp-site-health.php';
	}

	$site_health = WP_Site_Health::get_instance();
	wp_send_json_success( $site_health->get_test_background_updates() );
}

/**
 * Handles site health checks on loopback requests via AJAX.
 *
 * @since 5.2.0
 * @deprecated 5.6.0 Use WP_REST_Site_Health_Controller::test_loopback_requests()
 * @see WP_REST_Site_Health_Controller::test_loopback_requests()
 */
function wp_ajax_health_check_loopback_requests() {
	_doing_it_wrong(
		'wp_ajax_health_check_loopback_requests',
		sprintf(
		// translators: 1: The Site Health action that is no longer used by core. 2: The new function that replaces it.
			__( 'The Site Health check for %1$s has been replaced with %2$s.' ),
			'wp_ajax_health_check_loopback_requests',
			'WP_REST_Site_Health_Controller::test_loopback_requests'
		),
		'5.6.0'
	);

	check_ajax_referer( 'health-check-site-status' );

	if ( ! current_user_can( 'view_site_health_checks' ) ) {
		wp_send_json_error();
	}

	if ( ! class_exists( 'WP_Site_Health' ) ) {
		require_once ABSPATH . 'wp-admin/includes/class-wp-site-health.php';
	}

	$site_health = WP_Site_Health::get_instance();
	wp_send_json_success( $site_health->get_test_loopback_requests() );
}

/**
 * Handles site health check to update the result status via AJAX.
 *
 * @since 5.2.0
 */
function wp_ajax_health_check_site_status_result() {
	check_ajax_referer( 'health-check-site-status-result' );

	if ( ! current_user_can( 'view_site_health_checks' ) ) {
		wp_send_json_error();
	}

	set_transient( 'health-check-site-status-result', wp_json_encode( $_POST['counts'] ) );

	wp_send_json_success();
}

/**
 * Handles site health check to get directories and database sizes via AJAX.
 *
 * @since 5.2.0
 * @deprecated 5.6.0 Use WP_REST_Site_Health_Controller::get_directory_sizes()
 * @see WP_REST_Site_Health_Controller::get_directory_sizes()
 */
function wp_ajax_health_check_get_sizes() {
	_doing_it_wrong(
		'wp_ajax_health_check_get_sizes',
		sprintf(
		// translators: 1: The Site Health action that is no longer used by core. 2: The new function that replaces it.
			__( 'The Site Health check for %1$s has been replaced with %2$s.' ),
			'wp_ajax_health_check_get_sizes',
			'WP_REST_Site_Health_Controller::get_directory_sizes'
		),
		'5.6.0'
	);

	check_ajax_referer( 'health-check-site-status-result' );

	if ( ! current_user_can( 'view_site_health_checks' ) || is_multisite() ) {
		wp_send_json_error();
	}

	if ( ! class_exists( 'WP_Debug_Data' ) ) {
		require_once ABSPATH . 'wp-admin/includes/class-wp-debug-data.php';
	}

	$sizes_data = WP_Debug_Data::get_sizes();
	$all_sizes  = array( 'raw' => 0 );

	foreach ( $sizes_data as $name => $value ) {
		$name = sanitize_text_field( $name );
		$data = array();

		if ( isset( $value['size'] ) ) {
			if ( is_string( $value['size'] ) ) {
				$data['size'] = sanitize_text_field( $value['size'] );
			} else {
				$data['size'] = (int) $value['size'];
			}
		}

		if ( isset( $value['debug'] ) ) {
			if ( is_string( $value['debug'] ) ) {
				$data['debug'] = sanitize_text_field( $value['debug'] );
			} else {
				$data['debug'] = (int) $value['debug'];
			}
		}

		if ( ! empty( $value['raw'] ) ) {
			$data['raw'] = (int) $value['raw'];
		}

		$all_sizes[ $name ] = $data;
	}

	if ( isset( $all_sizes['total_size']['debug'] ) && 'not available' === $all_sizes['total_size']['debug'] ) {
		wp_send_json_error( $all_sizes );
	}

	wp_send_json_success( $all_sizes );
}

/**
 * Handles renewing the REST API nonce via AJAX.
 *
 * @since 5.3.0
 */
function wp_ajax_rest_nonce() {
	exit( wp_create_nonce( 'wp_rest' ) );
}

/**
 * Handles enabling or disable plugin and theme auto-updates via AJAX.
 *
 * @since 5.5.0
 */
function wp_ajax_toggle_auto_updates() {
	check_ajax_referer( 'updates' );

	if ( empty( $_POST['type'] ) || empty( $_POST['asset'] ) || empty( $_POST['state'] ) ) {
		wp_send_json_error( array( 'error' => __( 'Invalid data. No selected item.' ) ) );
	}

	$asset = sanitize_text_field( urldecode( $_POST['asset'] ) );

	if ( 'enable' !== $_POST['state'] && 'disable' !== $_POST['state'] ) {
		wp_send_json_error( array( 'error' => __( 'Invalid data. Unknown state.' ) ) );
	}
	$state = $_POST['state'];

	if ( 'plugin' !== $_POST['type'] && 'theme' !== $_POST['type'] ) {
		wp_send_json_error( array( 'error' => __( 'Invalid data. Unknown type.' ) ) );
	}
	$type = $_POST['type'];

	switch ( $type ) {
		case 'plugin':
			if ( ! current_user_can( 'update_plugins' ) ) {
				$error_message = __( 'Sorry, you are not allowed to modify plugins.' );
				wp_send_json_error( array( 'error' => $error_message ) );
			}

			$option = 'auto_update_plugins';
			/** This filter is documented in wp-admin/includes/class-wp-plugins-list-table.php */
			$all_items = apply_filters( 'all_plugins', get_plugins() );
			break;
		case 'theme':
			if ( ! current_user_can( 'update_themes' ) ) {
				$error_message = __( 'Sorry, you are not allowed to modify themes.' );
				wp_send_json_error( array( 'error' => $error_message ) );
			}

			$option    = 'auto_update_themes';
			$all_items = wp_get_themes();
			break;
		default:
			wp_send_json_error( array( 'error' => __( 'Invalid data. Unknown type.' ) ) );
	}

	if ( ! array_key_exists( $asset, $all_items ) ) {
		$error_message = __( 'Invalid data. The item does not exist.' );
		wp_send_json_error( array( 'error' => $error_message ) );
	}

	$auto_updates = (array) get_site_option( $option, array() );

	if ( 'disable' === $state ) {
		$auto_updates = array_diff( $auto_updates, array( $asset ) );
	} else {
		$auto_updates[] = $asset;
		$auto_updates   = array_unique( $auto_updates );
	}

	// Remove items that have been deleted since the site option was last updated.
	$auto_updates = array_intersect( $auto_updates, array_keys( $all_items ) );

	update_site_option( $option, $auto_updates );

	wp_send_json_success();
}

/**
 * Handles sending a password reset link via AJAX.
 *
 * @since 5.7.0
 */
function wp_ajax_send_password_reset() {

	// Validate the nonce for this action.
	$user_id = isset( $_POST['user_id'] ) ? (int) $_POST['user_id'] : 0;
	check_ajax_referer( 'reset-password-for-' . $user_id, 'nonce' );

	// Verify user capabilities.
	if ( ! current_user_can( 'edit_user', $user_id ) ) {
		wp_send_json_error( __( 'Cannot send password reset, permission denied.' ) );
	}

	// Send the password reset link.
	$user    = get_userdata( $user_id );
	$results = retrieve_password( $user->user_login );

	if ( true === $results ) {
		wp_send_json_success(
			/* translators: %s: User's display name. */
			sprintf( __( 'A password reset link was emailed to %s.' ), $user->display_name )
		);
	} else {
		wp_send_json_error( $results->get_error_message() );
	}
}
network.php000064400000064564151327343360006774 0ustar00<?php
/**
 * WordPress Network Administration API.
 *
 * @package WordPress
 * @subpackage Administration
 * @since 4.4.0
 */

/**
 * Check for an existing network.
 *
 * @since 3.0.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @return string|false Base domain if network exists, otherwise false.
 */
function network_domain_check() {
	global $wpdb;

	$sql = $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $wpdb->site ) );
	if ( $wpdb->get_var( $sql ) ) {
		return $wpdb->get_var( "SELECT domain FROM $wpdb->site ORDER BY id ASC LIMIT 1" );
	}
	return false;
}

/**
 * Allow subdomain installation
 *
 * @since 3.0.0
 * @return bool Whether subdomain installation is allowed
 */
function allow_subdomain_install() {
	$home   = get_option( 'home' );
	$domain = parse_url( $home, PHP_URL_HOST );
	if ( parse_url( $home, PHP_URL_PATH ) || 'localhost' === $domain || preg_match( '|^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$|', $domain ) ) {
		return false;
	}

	return true;
}

/**
 * Allow subdirectory installation.
 *
 * @since 3.0.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @return bool Whether subdirectory installation is allowed
 */
function allow_subdirectory_install() {
	global $wpdb;

	/**
	 * Filters whether to enable the subdirectory installation feature in Multisite.
	 *
	 * @since 3.0.0
	 *
	 * @param bool $allow Whether to enable the subdirectory installation feature in Multisite.
	 *                    Default false.
	 */
	if ( apply_filters( 'allow_subdirectory_install', false ) ) {
		return true;
	}

	if ( defined( 'ALLOW_SUBDIRECTORY_INSTALL' ) && ALLOW_SUBDIRECTORY_INSTALL ) {
		return true;
	}

	$post = $wpdb->get_row( "SELECT ID FROM $wpdb->posts WHERE post_date < DATE_SUB(NOW(), INTERVAL 1 MONTH) AND post_status = 'publish'" );
	if ( empty( $post ) ) {
		return true;
	}

	return false;
}

/**
 * Get base domain of network.
 *
 * @since 3.0.0
 * @return string Base domain.
 */
function get_clean_basedomain() {
	$existing_domain = network_domain_check();
	if ( $existing_domain ) {
		return $existing_domain;
	}
	$domain = preg_replace( '|https?://|', '', get_option( 'siteurl' ) );
	$slash  = strpos( $domain, '/' );
	if ( $slash ) {
		$domain = substr( $domain, 0, $slash );
	}
	return $domain;
}

/**
 * Prints step 1 for Network installation process.
 *
 * @todo Realistically, step 1 should be a welcome screen explaining what a Network is and such.
 *       Navigating to Tools > Network should not be a sudden "Welcome to a new install process!
 *       Fill this out and click here." See also contextual help todo.
 *
 * @since 3.0.0
 *
 * @global bool $is_apache
 *
 * @param false|WP_Error $errors Optional. Error object. Default false.
 */
function network_step1( $errors = false ) {
	global $is_apache;

	if ( defined( 'DO_NOT_UPGRADE_GLOBAL_TABLES' ) ) {
		$cannot_define_constant_message  = '<strong>' . __( 'Error:' ) . '</strong> ';
		$cannot_define_constant_message .= sprintf(
			/* translators: %s: DO_NOT_UPGRADE_GLOBAL_TABLES */
			__( 'The constant %s cannot be defined when creating a network.' ),
			'<code>DO_NOT_UPGRADE_GLOBAL_TABLES</code>'
		);

		wp_admin_notice(
			$cannot_define_constant_message,
			array(
				'additional_classes' => array( 'error' ),
			)
		);

		echo '</div>';
		require_once ABSPATH . 'wp-admin/admin-footer.php';
		die();
	}

	$active_plugins = get_option( 'active_plugins' );
	if ( ! empty( $active_plugins ) ) {
		wp_admin_notice(
			'<strong>' . __( 'Warning:' ) . '</strong> ' . sprintf(
				/* translators: %s: URL to Plugins screen. */
				__( 'Please <a href="%s">deactivate your plugins</a> before enabling the Network feature.' ),
				admin_url( 'plugins.php?plugin_status=active' )
			),
			array( 'type' => 'warning' )
		);
		echo '<p>' . __( 'Once the network is created, you may reactivate your plugins.' ) . '</p>';
		echo '</div>';
		require_once ABSPATH . 'wp-admin/admin-footer.php';
		die();
	}

	// Strip standard port from hostname.
	$hostname = preg_replace( '/(?::80|:443)$/', '', get_clean_basedomain() );

	echo '<form method="post">';

	wp_nonce_field( 'install-network-1' );

	$error_codes = array();
	if ( is_wp_error( $errors ) ) {
		$network_created_error_message = '<p><strong>' . __( 'Error:' ) . '</strong> ' . __( 'The network could not be created.' ) . '</p>';
		foreach ( $errors->get_error_messages() as $error ) {
			$network_created_error_message .= "<p>$error</p>";
		}
		wp_admin_notice(
			$network_created_error_message,
			array(
				'additional_classes' => array( 'error' ),
				'paragraph_wrap'     => false,
			)
		);
		$error_codes = $errors->get_error_codes();
	}

	if ( ! empty( $_POST['sitename'] ) && ! in_array( 'empty_sitename', $error_codes, true ) ) {
		$site_name = $_POST['sitename'];
	} else {
		/* translators: %s: Default network title. */
		$site_name = sprintf( __( '%s Sites' ), get_option( 'blogname' ) );
	}

	if ( ! empty( $_POST['email'] ) && ! in_array( 'invalid_email', $error_codes, true ) ) {
		$admin_email = $_POST['email'];
	} else {
		$admin_email = get_option( 'admin_email' );
	}
	?>
	<p><?php _e( 'Welcome to the Network installation process!' ); ?></p>
	<p><?php _e( 'Fill in the information below and you&#8217;ll be on your way to creating a network of WordPress sites. Configuration files will be created in the next step.' ); ?></p>
	<?php

	if ( isset( $_POST['subdomain_install'] ) ) {
		$subdomain_install = (bool) $_POST['subdomain_install'];
	} elseif ( apache_mod_loaded( 'mod_rewrite' ) ) { // Assume nothing.
		$subdomain_install = true;
	} elseif ( ! allow_subdirectory_install() ) {
		$subdomain_install = true;
	} else {
		$subdomain_install = false;
		$got_mod_rewrite   = got_mod_rewrite();
		if ( $got_mod_rewrite ) { // Dangerous assumptions.
			$message_class = 'updated';
			$message       = '<p><strong>' . __( 'Warning:' ) . '</strong> ';
			$message      .= '<p>' . sprintf(
				/* translators: %s: mod_rewrite */
				__( 'Please make sure the Apache %s module is installed as it will be used at the end of this installation.' ),
				'<code>mod_rewrite</code>'
			) . '</p>';
		} elseif ( $is_apache ) {
			$message_class = 'error';
			$message       = '<p><strong>' . __( 'Warning:' ) . '</strong> ';
			$message      .= sprintf(
				/* translators: %s: mod_rewrite */
				__( 'It looks like the Apache %s module is not installed.' ),
				'<code>mod_rewrite</code>'
			) . '</p>';
		}

		if ( $got_mod_rewrite || $is_apache ) { // Protect against mod_rewrite mimicry (but ! Apache).
			$message .= '<p>' . sprintf(
				/* translators: 1: mod_rewrite, 2: mod_rewrite documentation URL, 3: Google search for mod_rewrite. */
				__( 'If %1$s is disabled, ask your administrator to enable that module, or look at the <a href="%2$s">Apache documentation</a> or <a href="%3$s">elsewhere</a> for help setting it up.' ),
				'<code>mod_rewrite</code>',
				'https://httpd.apache.org/docs/mod/mod_rewrite.html',
				'https://www.google.com/search?q=apache+mod_rewrite'
			) . '</p>';

			wp_admin_notice(
				$message,
				array(
					'additional_classes' => array( $message_class, 'inline' ),
					'paragraph_wrap'     => false,
				)
			);
		}
	}

	if ( allow_subdomain_install() && allow_subdirectory_install() ) :
		?>
		<h3><?php esc_html_e( 'Addresses of Sites in your Network' ); ?></h3>
		<p><?php _e( 'Please choose whether you would like sites in your WordPress network to use sub-domains or sub-directories.' ); ?>
			<strong><?php _e( 'You cannot change this later.' ); ?></strong></p>
		<p><?php _e( 'You will need a wildcard DNS record if you are going to use the virtual host (sub-domain) functionality.' ); ?></p>
		<?php // @todo Link to an MS readme? ?>
		<table class="form-table" role="presentation">
			<tr>
				<th><label><input type="radio" name="subdomain_install" value="1"<?php checked( $subdomain_install ); ?> /> <?php _e( 'Sub-domains' ); ?></label></th>
				<td>
				<?php
				printf(
					/* translators: 1: Host name. */
					_x( 'like <code>site1.%1$s</code> and <code>site2.%1$s</code>', 'subdomain examples' ),
					$hostname
				);
				?>
				</td>
			</tr>
			<tr>
				<th><label><input type="radio" name="subdomain_install" value="0"<?php checked( ! $subdomain_install ); ?> /> <?php _e( 'Sub-directories' ); ?></label></th>
				<td>
				<?php
				printf(
					/* translators: 1: Host name. */
					_x( 'like <code>%1$s/site1</code> and <code>%1$s/site2</code>', 'subdirectory examples' ),
					$hostname
				);
				?>
				</td>
			</tr>
		</table>

		<?php
	endif;

	if ( WP_CONTENT_DIR !== ABSPATH . 'wp-content' && ( allow_subdirectory_install() || ! allow_subdomain_install() ) ) {
		$subdirectory_warning_message  = '<strong>' . __( 'Warning:' ) . '</strong> ';
		$subdirectory_warning_message .= __( 'Subdirectory networks may not be fully compatible with custom wp-content directories.' );
		wp_admin_notice(
			$subdirectory_warning_message,
			array(
				'additional_classes' => array( 'error', 'inline' ),
			)
		);
	}

	$is_www = str_starts_with( $hostname, 'www.' );
	if ( $is_www ) :
		?>
		<h3><?php esc_html_e( 'Server Address' ); ?></h3>
		<p>
		<?php
		printf(
			/* translators: 1: Site URL, 2: Host name, 3: www. */
			__( 'You should consider changing your site domain to %1$s before enabling the network feature. It will still be possible to visit your site using the %3$s prefix with an address like %2$s but any links will not have the %3$s prefix.' ),
			'<code>' . substr( $hostname, 4 ) . '</code>',
			'<code>' . $hostname . '</code>',
			'<code>www</code>'
		);
		?>
		</p>
		<table class="form-table" role="presentation">
			<tr>
			<th scope='row'><?php esc_html_e( 'Server Address' ); ?></th>
			<td>
				<?php
					printf(
						/* translators: %s: Host name. */
						__( 'The internet address of your network will be %s.' ),
						'<code>' . $hostname . '</code>'
					);
				?>
				</td>
			</tr>
		</table>
		<?php endif; ?>

		<h3><?php esc_html_e( 'Network Details' ); ?></h3>
		<table class="form-table" role="presentation">
		<?php if ( 'localhost' === $hostname ) : ?>
			<tr>
				<th scope="row"><?php esc_html_e( 'Sub-directory Installation' ); ?></th>
				<td>
				<?php
					printf(
						/* translators: 1: localhost, 2: localhost.localdomain */
						__( 'Because you are using %1$s, the sites in your WordPress network must use sub-directories. Consider using %2$s if you wish to use sub-domains.' ),
						'<code>localhost</code>',
						'<code>localhost.localdomain</code>'
					);
					// Uh oh:
				if ( ! allow_subdirectory_install() ) {
					echo ' <strong>' . __( 'Warning:' ) . ' ' . __( 'The main site in a sub-directory installation will need to use a modified permalink structure, potentially breaking existing links.' ) . '</strong>';
				}
				?>
				</td>
			</tr>
		<?php elseif ( ! allow_subdomain_install() ) : ?>
			<tr>
				<th scope="row"><?php esc_html_e( 'Sub-directory Installation' ); ?></th>
				<td>
				<?php
					_e( 'Because your installation is in a directory, the sites in your WordPress network must use sub-directories.' );
					// Uh oh:
				if ( ! allow_subdirectory_install() ) {
					echo ' <strong>' . __( 'Warning:' ) . ' ' . __( 'The main site in a sub-directory installation will need to use a modified permalink structure, potentially breaking existing links.' ) . '</strong>';
				}
				?>
				</td>
			</tr>
		<?php elseif ( ! allow_subdirectory_install() ) : ?>
			<tr>
				<th scope="row"><?php esc_html_e( 'Sub-domain Installation' ); ?></th>
				<td>
				<?php
				_e( 'Because your installation is not new, the sites in your WordPress network must use sub-domains.' );
					echo ' <strong>' . __( 'The main site in a sub-directory installation will need to use a modified permalink structure, potentially breaking existing links.' ) . '</strong>';
				?>
				</td>
			</tr>
		<?php endif; ?>
		<?php if ( ! $is_www ) : ?>
			<tr>
				<th scope='row'><?php esc_html_e( 'Server Address' ); ?></th>
				<td>
					<?php
					printf(
						/* translators: %s: Host name. */
						__( 'The internet address of your network will be %s.' ),
						'<code>' . $hostname . '</code>'
					);
					?>
				</td>
			</tr>
		<?php endif; ?>
			<tr>
				<th scope='row'><label for="sitename"><?php esc_html_e( 'Network Title' ); ?></label></th>
				<td>
					<input name='sitename' id='sitename' type='text' size='45' value='<?php echo esc_attr( $site_name ); ?>' />
					<p class="description">
						<?php _e( 'What would you like to call your network?' ); ?>
					</p>
				</td>
			</tr>
			<tr>
				<th scope='row'><label for="email"><?php esc_html_e( 'Network Admin Email' ); ?></label></th>
				<td>
					<input name='email' id='email' type='text' size='45' value='<?php echo esc_attr( $admin_email ); ?>' />
					<p class="description">
						<?php _e( 'Your email address.' ); ?>
					</p>
				</td>
			</tr>
		</table>
		<?php submit_button( __( 'Install' ), 'primary', 'submit' ); ?>
	</form>
	<?php
}

/**
 * Prints step 2 for Network installation process.
 *
 * @since 3.0.0
 *
 * @global wpdb $wpdb     WordPress database abstraction object.
 * @global bool $is_nginx Whether the server software is Nginx or something else.
 *
 * @param false|WP_Error $errors Optional. Error object. Default false.
 */
function network_step2( $errors = false ) {
	global $wpdb, $is_nginx;

	$hostname          = get_clean_basedomain();
	$slashed_home      = trailingslashit( get_option( 'home' ) );
	$base              = parse_url( $slashed_home, PHP_URL_PATH );
	$document_root_fix = str_replace( '\\', '/', realpath( $_SERVER['DOCUMENT_ROOT'] ) );
	$abspath_fix       = str_replace( '\\', '/', ABSPATH );
	$home_path         = str_starts_with( $abspath_fix, $document_root_fix ) ? $document_root_fix . $base : get_home_path();
	$wp_siteurl_subdir = preg_replace( '#^' . preg_quote( $home_path, '#' ) . '#', '', $abspath_fix );
	$rewrite_base      = ! empty( $wp_siteurl_subdir ) ? ltrim( trailingslashit( $wp_siteurl_subdir ), '/' ) : '';

	$location_of_wp_config = $abspath_fix;
	if ( ! file_exists( ABSPATH . 'wp-config.php' ) && file_exists( dirname( ABSPATH ) . '/wp-config.php' ) ) {
		$location_of_wp_config = dirname( $abspath_fix );
	}
	$location_of_wp_config = trailingslashit( $location_of_wp_config );

	// Wildcard DNS message.
	if ( is_wp_error( $errors ) ) {
		wp_admin_notice(
			$errors->get_error_message(),
			array(
				'additional_classes' => array( 'error' ),
			)
		);
	}

	if ( $_POST ) {
		if ( allow_subdomain_install() ) {
			$subdomain_install = allow_subdirectory_install() ? ! empty( $_POST['subdomain_install'] ) : true;
		} else {
			$subdomain_install = false;
		}
	} else {
		if ( is_multisite() ) {
			$subdomain_install = is_subdomain_install();
			?>
	<p><?php _e( 'The original configuration steps are shown here for reference.' ); ?></p>
			<?php
		} else {
			$subdomain_install = (bool) $wpdb->get_var( "SELECT meta_value FROM $wpdb->sitemeta WHERE site_id = 1 AND meta_key = 'subdomain_install'" );

			wp_admin_notice(
				'<strong>' . __( 'Warning:' ) . '</strong> ' . __( 'An existing WordPress network was detected.' ),
				array(
					'additional_classes' => array( 'error' ),
				)
			);
			?>
	<p><?php _e( 'Please complete the configuration steps. To create a new network, you will need to empty or remove the network database tables.' ); ?></p>
			<?php
		}
	}

	$subdir_match          = $subdomain_install ? '' : '([_0-9a-zA-Z-]+/)?';
	$subdir_replacement_01 = $subdomain_install ? '' : '$1';
	$subdir_replacement_12 = $subdomain_install ? '$1' : '$2';

	if ( $_POST || ! is_multisite() ) {
		?>
		<h3><?php esc_html_e( 'Enabling the Network' ); ?></h3>
		<p><?php _e( 'Complete the following steps to enable the features for creating a network of sites.' ); ?></p>
		<?php
		$notice_message = '<strong>' . __( 'Caution:' ) . '</strong> ';
		$notice_args    = array(
			'type'               => 'warning',
			'additional_classes' => array( 'inline' ),
		);

		if ( file_exists( $home_path . '.htaccess' ) ) {
			$notice_message .= sprintf(
				/* translators: 1: wp-config.php, 2: .htaccess */
				__( 'You should back up your existing %1$s and %2$s files.' ),
				'<code>wp-config.php</code>',
				'<code>.htaccess</code>'
			);
		} elseif ( file_exists( $home_path . 'web.config' ) ) {
			$notice_message .= sprintf(
				/* translators: 1: wp-config.php, 2: web.config */
				__( 'You should back up your existing %1$s and %2$s files.' ),
				'<code>wp-config.php</code>',
				'<code>web.config</code>'
			);
		} else {
			$notice_message .= sprintf(
				/* translators: %s: wp-config.php */
				__( 'You should back up your existing %s file.' ),
				'<code>wp-config.php</code>'
			);
		}

		wp_admin_notice( $notice_message, $notice_args );
	}
	?>
	<ol>
		<li><p id="network-wpconfig-rules-description">
		<?php
		printf(
			/* translators: 1: wp-config.php, 2: Location of wp-config file, 3: Translated version of "That's all, stop editing! Happy publishing." */
			__( 'Add the following to your %1$s file in %2$s <strong>above</strong> the line reading %3$s:' ),
			'<code>wp-config.php</code>',
			'<code>' . $location_of_wp_config . '</code>',
			/*
			 * translators: This string should only be translated if wp-config-sample.php is localized.
			 * You can check the localized release package or
			 * https://i18n.svn.wordpress.org/<locale code>/branches/<wp version>/dist/wp-config-sample.php
			 */
			'<code>/* ' . __( 'That&#8217;s all, stop editing! Happy publishing.' ) . ' */</code>'
		);
		?>
		</p>
		<p class="configuration-rules-label"><label for="network-wpconfig-rules">
			<?php
			printf(
				/* translators: %s: File name (wp-config.php, .htaccess or web.config). */
				__( 'Network configuration rules for %s' ),
				'<code>wp-config.php</code>'
			);
			?>
		</label></p>
		<textarea id="network-wpconfig-rules" class="code" readonly="readonly" cols="100" rows="7" aria-describedby="network-wpconfig-rules-description">
define( 'MULTISITE', true );
define( 'SUBDOMAIN_INSTALL', <?php echo $subdomain_install ? 'true' : 'false'; ?> );
define( 'DOMAIN_CURRENT_SITE', '<?php echo $hostname; ?>' );
define( 'PATH_CURRENT_SITE', '<?php echo $base; ?>' );
define( 'SITE_ID_CURRENT_SITE', 1 );
define( 'BLOG_ID_CURRENT_SITE', 1 );
</textarea>
		<?php
		$keys_salts = array(
			'AUTH_KEY'         => '',
			'SECURE_AUTH_KEY'  => '',
			'LOGGED_IN_KEY'    => '',
			'NONCE_KEY'        => '',
			'AUTH_SALT'        => '',
			'SECURE_AUTH_SALT' => '',
			'LOGGED_IN_SALT'   => '',
			'NONCE_SALT'       => '',
		);
		foreach ( $keys_salts as $c => $v ) {
			if ( defined( $c ) ) {
				unset( $keys_salts[ $c ] );
			}
		}

		if ( ! empty( $keys_salts ) ) {
			$keys_salts_str = '';
			$from_api       = wp_remote_get( 'https://api.wordpress.org/secret-key/1.1/salt/' );
			if ( is_wp_error( $from_api ) ) {
				foreach ( $keys_salts as $c => $v ) {
					$keys_salts_str .= "\ndefine( '$c', '" . wp_generate_password( 64, true, true ) . "' );";
				}
			} else {
				$from_api = explode( "\n", wp_remote_retrieve_body( $from_api ) );
				foreach ( $keys_salts as $c => $v ) {
					$keys_salts_str .= "\ndefine( '$c', '" . substr( array_shift( $from_api ), 28, 64 ) . "' );";
				}
			}
			$num_keys_salts = count( $keys_salts );
			?>
		<p id="network-wpconfig-authentication-description">
			<?php
			if ( 1 === $num_keys_salts ) {
				printf(
					/* translators: %s: wp-config.php */
					__( 'This unique authentication key is also missing from your %s file.' ),
					'<code>wp-config.php</code>'
				);
			} else {
				printf(
					/* translators: %s: wp-config.php */
					__( 'These unique authentication keys are also missing from your %s file.' ),
					'<code>wp-config.php</code>'
				);
			}
			?>
			<?php _e( 'To make your installation more secure, you should also add:' ); ?>
		</p>
		<p class="configuration-rules-label"><label for="network-wpconfig-authentication"><?php _e( 'Network configuration authentication keys' ); ?></label></p>
		<textarea id="network-wpconfig-authentication" class="code" readonly="readonly" cols="100" rows="<?php echo $num_keys_salts; ?>" aria-describedby="network-wpconfig-authentication-description"><?php echo esc_textarea( $keys_salts_str ); ?></textarea>
			<?php
		}
		?>
		</li>
	<?php
	if ( iis7_supports_permalinks() ) :
		// IIS doesn't support RewriteBase, all your RewriteBase are belong to us.
		$iis_subdir_match       = ltrim( $base, '/' ) . $subdir_match;
		$iis_rewrite_base       = ltrim( $base, '/' ) . $rewrite_base;
		$iis_subdir_replacement = $subdomain_install ? '' : '{R:1}';

		$web_config_file = '<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <rewrite>
            <rules>
                <rule name="WordPress Rule 1" stopProcessing="true">
                    <match url="^index\.php$" ignoreCase="false" />
                    <action type="None" />
                </rule>';
		if ( is_multisite() && get_site_option( 'ms_files_rewriting' ) ) {
			$web_config_file .= '
                <rule name="WordPress Rule for Files" stopProcessing="true">
                    <match url="^' . $iis_subdir_match . 'files/(.+)" ignoreCase="false" />
                    <action type="Rewrite" url="' . $iis_rewrite_base . WPINC . '/ms-files.php?file={R:1}" appendQueryString="false" />
                </rule>';
		}
			$web_config_file .= '
                <rule name="WordPress Rule 2" stopProcessing="true">
                    <match url="^' . $iis_subdir_match . 'wp-admin$" ignoreCase="false" />
                    <action type="Redirect" url="' . $iis_subdir_replacement . 'wp-admin/" redirectType="Permanent" />
                </rule>
                <rule name="WordPress Rule 3" stopProcessing="true">
                    <match url="^" ignoreCase="false" />
                    <conditions logicalGrouping="MatchAny">
                        <add input="{REQUEST_FILENAME}" matchType="IsFile" ignoreCase="false" />
                        <add input="{REQUEST_FILENAME}" matchType="IsDirectory" ignoreCase="false" />
                    </conditions>
                    <action type="None" />
                </rule>
                <rule name="WordPress Rule 4" stopProcessing="true">
                    <match url="^' . $iis_subdir_match . '(wp-(content|admin|includes).*)" ignoreCase="false" />
                    <action type="Rewrite" url="' . $iis_rewrite_base . '{R:1}" />
                </rule>
                <rule name="WordPress Rule 5" stopProcessing="true">
                    <match url="^' . $iis_subdir_match . '([_0-9a-zA-Z-]+/)?(.*\.php)$" ignoreCase="false" />
                    <action type="Rewrite" url="' . $iis_rewrite_base . '{R:2}" />
                </rule>
                <rule name="WordPress Rule 6" stopProcessing="true">
                    <match url="." ignoreCase="false" />
                    <action type="Rewrite" url="index.php" />
                </rule>
            </rules>
        </rewrite>
    </system.webServer>
</configuration>
';

			echo '<li><p id="network-webconfig-rules-description">';
			printf(
				/* translators: 1: File name (.htaccess or web.config), 2: File path. */
				__( 'Add the following to your %1$s file in %2$s, <strong>replacing</strong> other WordPress rules:' ),
				'<code>web.config</code>',
				'<code>' . $home_path . '</code>'
			);
		echo '</p>';
		if ( ! $subdomain_install && WP_CONTENT_DIR !== ABSPATH . 'wp-content' ) {
			echo '<p><strong>' . __( 'Warning:' ) . ' ' . __( 'Subdirectory networks may not be fully compatible with custom wp-content directories.' ) . '</strong></p>';
		}
		?>
			<p class="configuration-rules-label"><label for="network-webconfig-rules">
				<?php
				printf(
					/* translators: %s: File name (wp-config.php, .htaccess or web.config). */
					__( 'Network configuration rules for %s' ),
					'<code>web.config</code>'
				);
				?>
			</label></p>
			<textarea id="network-webconfig-rules" class="code" readonly="readonly" cols="100" rows="20" aria-describedby="network-webconfig-rules-description"><?php echo esc_textarea( $web_config_file ); ?></textarea>
		</li>
	</ol>

		<?php
	elseif ( $is_nginx ) : // End iis7_supports_permalinks(). Link to Nginx documentation instead:

		echo '<li><p>';
		printf(
			/* translators: %s: Documentation URL. */
			__( 'It seems your network is running with Nginx web server. <a href="%s">Learn more about further configuration</a>.' ),
			__( 'https://developer.wordpress.org/advanced-administration/server/web-server/nginx/' )
		);
		echo '</p></li>';

	else : // End $is_nginx. Construct an .htaccess file instead:

		$ms_files_rewriting = '';
		if ( is_multisite() && get_site_option( 'ms_files_rewriting' ) ) {
			$ms_files_rewriting  = "\n# uploaded files\nRewriteRule ^";
			$ms_files_rewriting .= $subdir_match . "files/(.+) {$rewrite_base}" . WPINC . "/ms-files.php?file={$subdir_replacement_12} [L]" . "\n";
		}

		$htaccess_file = <<<EOF
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase {$base}
RewriteRule ^index\.php$ - [L]
{$ms_files_rewriting}
# add a trailing slash to /wp-admin
RewriteRule ^{$subdir_match}wp-admin$ {$subdir_replacement_01}wp-admin/ [R=301,L]

RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
RewriteRule ^{$subdir_match}(wp-(content|admin|includes).*) {$rewrite_base}{$subdir_replacement_12} [L]
RewriteRule ^{$subdir_match}(.*\.php)$ {$rewrite_base}$subdir_replacement_12 [L]
RewriteRule . index.php [L]

EOF;

		echo '<li><p id="network-htaccess-rules-description">';
		printf(
			/* translators: 1: File name (.htaccess or web.config), 2: File path. */
			__( 'Add the following to your %1$s file in %2$s, <strong>replacing</strong> other WordPress rules:' ),
			'<code>.htaccess</code>',
			'<code>' . $home_path . '</code>'
		);
		echo '</p>';
		if ( ! $subdomain_install && WP_CONTENT_DIR !== ABSPATH . 'wp-content' ) {
			echo '<p><strong>' . __( 'Warning:' ) . ' ' . __( 'Subdirectory networks may not be fully compatible with custom wp-content directories.' ) . '</strong></p>';
		}
		?>
			<p class="configuration-rules-label"><label for="network-htaccess-rules">
				<?php
				printf(
					/* translators: %s: File name (wp-config.php, .htaccess or web.config). */
					__( 'Network configuration rules for %s' ),
					'<code>.htaccess</code>'
				);
				?>
			</label></p>
			<textarea id="network-htaccess-rules" class="code" readonly="readonly" cols="100" rows="<?php echo substr_count( $htaccess_file, "\n" ) + 1; ?>" aria-describedby="network-htaccess-rules-description"><?php echo esc_textarea( $htaccess_file ); ?></textarea>
		</li>
	</ol>

		<?php
	endif; // End IIS/Nginx/Apache code branches.

	if ( ! is_multisite() ) {
		?>
		<p><?php _e( 'Once you complete these steps, your network is enabled and configured. You will have to log in again.' ); ?> <a href="<?php echo esc_url( wp_login_url() ); ?>"><?php _e( 'Log In' ); ?></a></p>
		<?php
	}
}
taxonomy.php000060400000020350151327343360007136 0ustar00<?php
/**
 * WordPress Taxonomy Administration API.
 *
 * @package WordPress
 * @subpackage Administration
 */

//
// Category.
//

/**
 * Checks whether a category exists.
 *
 * @since 2.0.0
 *
 * @see term_exists()
 *
 * @param int|string $cat_name        Category name.
 * @param int        $category_parent Optional. ID of parent category.
 * @return string|null Returns the category ID as a numeric string if the pairing exists, null if not.
 */
function category_exists( $cat_name, $category_parent = null ) {
	$id = term_exists( $cat_name, 'category', $category_parent );
	if ( is_array( $id ) ) {
		$id = $id['term_id'];
	}
	return $id;
}

/**
 * Gets category object for given ID and 'edit' filter context.
 *
 * @since 2.0.0
 *
 * @param int $id
 * @return object
 */
function get_category_to_edit( $id ) {
	$category = get_term( $id, 'category', OBJECT, 'edit' );
	_make_cat_compat( $category );
	return $category;
}

/**
 * Adds a new category to the database if it does not already exist.
 *
 * @since 2.0.0
 *
 * @param int|string $cat_name        Category name.
 * @param int        $category_parent Optional. ID of parent category.
 * @return int|WP_Error
 */
function wp_create_category( $cat_name, $category_parent = 0 ) {
	$id = category_exists( $cat_name, $category_parent );
	if ( $id ) {
		return $id;
	}

	return wp_insert_category(
		array(
			'cat_name'        => $cat_name,
			'category_parent' => $category_parent,
		)
	);
}

/**
 * Creates categories for the given post.
 *
 * @since 2.0.0
 *
 * @param string[] $categories Array of category names to create.
 * @param int      $post_id    Optional. The post ID. Default empty.
 * @return int[] Array of IDs of categories assigned to the given post.
 */
function wp_create_categories( $categories, $post_id = '' ) {
	$cat_ids = array();
	foreach ( $categories as $category ) {
		$id = category_exists( $category );
		if ( $id ) {
			$cat_ids[] = $id;
		} else {
			$id = wp_create_category( $category );
			if ( $id ) {
				$cat_ids[] = $id;
			}
		}
	}

	if ( $post_id ) {
		wp_set_post_categories( $post_id, $cat_ids );
	}

	return $cat_ids;
}

/**
 * Updates an existing Category or creates a new Category.
 *
 * @since 2.0.0
 * @since 2.5.0 $wp_error parameter was added.
 * @since 3.0.0 The 'taxonomy' argument was added.
 *
 * @param array $catarr {
 *     Array of arguments for inserting a new category.
 *
 *     @type int        $cat_ID               Category ID. A non-zero value updates an existing category.
 *                                            Default 0.
 *     @type string     $taxonomy             Taxonomy slug. Default 'category'.
 *     @type string     $cat_name             Category name. Default empty.
 *     @type string     $category_description Category description. Default empty.
 *     @type string     $category_nicename    Category nice (display) name. Default empty.
 *     @type int|string $category_parent      Category parent ID. Default empty.
 * }
 * @param bool  $wp_error Optional. Default false.
 * @return int|WP_Error The ID number of the new or updated Category on success. Zero or a WP_Error on failure,
 *                      depending on param `$wp_error`.
 */
function wp_insert_category( $catarr, $wp_error = false ) {
	$cat_defaults = array(
		'cat_ID'               => 0,
		'taxonomy'             => 'category',
		'cat_name'             => '',
		'category_description' => '',
		'category_nicename'    => '',
		'category_parent'      => '',
	);
	$catarr       = wp_parse_args( $catarr, $cat_defaults );

	if ( '' === trim( $catarr['cat_name'] ) ) {
		if ( ! $wp_error ) {
			return 0;
		} else {
			return new WP_Error( 'cat_name', __( 'You did not enter a category name.' ) );
		}
	}

	$catarr['cat_ID'] = (int) $catarr['cat_ID'];

	// Are we updating or creating?
	$update = ! empty( $catarr['cat_ID'] );

	$name        = $catarr['cat_name'];
	$description = $catarr['category_description'];
	$slug        = $catarr['category_nicename'];
	$parent      = (int) $catarr['category_parent'];
	if ( $parent < 0 ) {
		$parent = 0;
	}

	if ( empty( $parent )
		|| ! term_exists( $parent, $catarr['taxonomy'] )
		|| ( $catarr['cat_ID'] && term_is_ancestor_of( $catarr['cat_ID'], $parent, $catarr['taxonomy'] ) ) ) {
		$parent = 0;
	}

	$args = compact( 'name', 'slug', 'parent', 'description' );

	if ( $update ) {
		$catarr['cat_ID'] = wp_update_term( $catarr['cat_ID'], $catarr['taxonomy'], $args );
	} else {
		$catarr['cat_ID'] = wp_insert_term( $catarr['cat_name'], $catarr['taxonomy'], $args );
	}

	if ( is_wp_error( $catarr['cat_ID'] ) ) {
		if ( $wp_error ) {
			return $catarr['cat_ID'];
		} else {
			return 0;
		}
	}
	return $catarr['cat_ID']['term_id'];
}

/**
 * Aliases wp_insert_category() with minimal args.
 *
 * If you want to update only some fields of an existing category, call this
 * function with only the new values set inside $catarr.
 *
 * @since 2.0.0
 *
 * @param array $catarr The 'cat_ID' value is required. All other keys are optional.
 * @return int|false The ID number of the new or updated Category on success. Zero or FALSE on failure.
 */
function wp_update_category( $catarr ) {
	$cat_id = (int) $catarr['cat_ID'];

	if ( isset( $catarr['category_parent'] ) && ( $cat_id === (int) $catarr['category_parent'] ) ) {
		return false;
	}

	// First, get all of the original fields.
	$category = get_term( $cat_id, 'category', ARRAY_A );
	_make_cat_compat( $category );

	// Escape data pulled from DB.
	$category = wp_slash( $category );

	// Merge old and new fields with new fields overwriting old ones.
	$catarr = array_merge( $category, $catarr );

	return wp_insert_category( $catarr );
}

//
// Tags.
//

/**
 * Checks whether a post tag with a given name exists.
 *
 * @since 2.3.0
 *
 * @param int|string $tag_name
 * @return mixed Returns null if the term does not exist.
 *               Returns an array of the term ID and the term taxonomy ID if the pairing exists.
 *               Returns 0 if term ID 0 is passed to the function.
 */
function tag_exists( $tag_name ) {
	return term_exists( $tag_name, 'post_tag' );
}

/**
 * Adds a new tag to the database if it does not already exist.
 *
 * @since 2.3.0
 *
 * @param int|string $tag_name
 * @return array|WP_Error
 */
function wp_create_tag( $tag_name ) {
	return wp_create_term( $tag_name, 'post_tag' );
}

/**
 * Gets comma-separated list of tags available to edit.
 *
 * @since 2.3.0
 *
 * @param int    $post_id
 * @param string $taxonomy Optional. The taxonomy for which to retrieve terms. Default 'post_tag'.
 * @return string|false|WP_Error
 */
function get_tags_to_edit( $post_id, $taxonomy = 'post_tag' ) {
	return get_terms_to_edit( $post_id, $taxonomy );
}

/**
 * Gets comma-separated list of terms available to edit for the given post ID.
 *
 * @since 2.8.0
 *
 * @param int    $post_id
 * @param string $taxonomy Optional. The taxonomy for which to retrieve terms. Default 'post_tag'.
 * @return string|false|WP_Error
 */
function get_terms_to_edit( $post_id, $taxonomy = 'post_tag' ) {
	$post_id = (int) $post_id;
	if ( ! $post_id ) {
		return false;
	}

	$terms = get_object_term_cache( $post_id, $taxonomy );
	if ( false === $terms ) {
		$terms = wp_get_object_terms( $post_id, $taxonomy );
		wp_cache_add( $post_id, wp_list_pluck( $terms, 'term_id' ), $taxonomy . '_relationships' );
	}

	if ( ! $terms ) {
		return false;
	}
	if ( is_wp_error( $terms ) ) {
		return $terms;
	}
	$term_names = array();
	foreach ( $terms as $term ) {
		$term_names[] = $term->name;
	}

	$terms_to_edit = esc_attr( implode( ',', $term_names ) );

	/**
	 * Filters the comma-separated list of terms available to edit.
	 *
	 * @since 2.8.0
	 *
	 * @see get_terms_to_edit()
	 *
	 * @param string $terms_to_edit A comma-separated list of term names.
	 * @param string $taxonomy      The taxonomy name for which to retrieve terms.
	 */
	$terms_to_edit = apply_filters( 'terms_to_edit', $terms_to_edit, $taxonomy );

	return $terms_to_edit;
}

/**
 * Adds a new term to the database if it does not already exist.
 *
 * @since 2.8.0
 *
 * @param string $tag_name The term name.
 * @param string $taxonomy Optional. The taxonomy within which to create the term. Default 'post_tag'.
 * @return array|WP_Error
 */
function wp_create_term( $tag_name, $taxonomy = 'post_tag' ) {
	$id = term_exists( $tag_name, $taxonomy );
	if ( $id ) {
		return $id;
	}

	return wp_insert_term( $tag_name, $taxonomy );
}
class-language-pack-upgrader.php000060400000036311151327343360012675 0ustar00<?php
/**
 * Upgrade API: Language_Pack_Upgrader class
 *
 * @package WordPress
 * @subpackage Upgrader
 * @since 4.6.0
 */

/**
 * Core class used for updating/installing language packs (translations)
 * for plugins, themes, and core.
 *
 * @since 3.7.0
 * @since 4.6.0 Moved to its own file from wp-admin/includes/class-wp-upgrader.php.
 *
 * @see WP_Upgrader
 */
class Language_Pack_Upgrader extends WP_Upgrader {

	/**
	 * Result of the language pack upgrade.
	 *
	 * @since 3.7.0
	 * @var array|WP_Error $result
	 * @see WP_Upgrader::$result
	 */
	public $result;

	/**
	 * Whether a bulk upgrade/installation is being performed.
	 *
	 * @since 3.7.0
	 * @var bool $bulk
	 */
	public $bulk = true;

	/**
	 * Asynchronously upgrades language packs after other upgrades have been made.
	 *
	 * Hooked to the {@see 'upgrader_process_complete'} action by default.
	 *
	 * @since 3.7.0
	 *
	 * @param false|WP_Upgrader $upgrader Optional. WP_Upgrader instance or false. If `$upgrader` is
	 *                                    a Language_Pack_Upgrader instance, the method will bail to
	 *                                    avoid recursion. Otherwise unused. Default false.
	 */
	public static function async_upgrade( $upgrader = false ) {
		// Avoid recursion.
		if ( $upgrader && $upgrader instanceof Language_Pack_Upgrader ) {
			return;
		}

		// Nothing to do?
		$language_updates = wp_get_translation_updates();
		if ( ! $language_updates ) {
			return;
		}

		/*
		 * Avoid messing with VCS installations, at least for now.
		 * Noted: this is not the ideal way to accomplish this.
		 */
		$check_vcs = new WP_Automatic_Updater();
		if ( $check_vcs->is_vcs_checkout( WP_CONTENT_DIR ) ) {
			return;
		}

		foreach ( $language_updates as $key => $language_update ) {
			$update = ! empty( $language_update->autoupdate );

			/**
			 * Filters whether to asynchronously update translation for core, a plugin, or a theme.
			 *
			 * @since 4.0.0
			 *
			 * @param bool   $update          Whether to update.
			 * @param object $language_update The update offer.
			 */
			$update = apply_filters( 'async_update_translation', $update, $language_update );

			if ( ! $update ) {
				unset( $language_updates[ $key ] );
			}
		}

		if ( empty( $language_updates ) ) {
			return;
		}

		// Re-use the automatic upgrader skin if the parent upgrader is using it.
		if ( $upgrader && $upgrader->skin instanceof Automatic_Upgrader_Skin ) {
			$skin = $upgrader->skin;
		} else {
			$skin = new Language_Pack_Upgrader_Skin(
				array(
					'skip_header_footer' => true,
				)
			);
		}

		$lp_upgrader = new Language_Pack_Upgrader( $skin );
		$lp_upgrader->bulk_upgrade( $language_updates );
	}

	/**
	 * Initializes the upgrade strings.
	 *
	 * @since 3.7.0
	 */
	public function upgrade_strings() {
		$this->strings['starting_upgrade'] = __( 'Some of your translations need updating. Sit tight for a few more seconds while they are updated as well.' );
		$this->strings['up_to_date']       = __( 'Your translations are all up to date.' );
		$this->strings['no_package']       = __( 'Update package not available.' );
		/* translators: %s: Package URL. */
		$this->strings['downloading_package'] = sprintf( __( 'Downloading translation from %s&#8230;' ), '<span class="code pre">%s</span>' );
		$this->strings['unpack_package']      = __( 'Unpacking the update&#8230;' );
		$this->strings['process_failed']      = __( 'Translation update failed.' );
		$this->strings['process_success']     = __( 'Translation updated successfully.' );
		$this->strings['remove_old']          = __( 'Removing the old version of the translation&#8230;' );
		$this->strings['remove_old_failed']   = __( 'Could not remove the old translation.' );
	}

	/**
	 * Upgrades a language pack.
	 *
	 * @since 3.7.0
	 *
	 * @param string|false $update Optional. Whether an update offer is available. Default false.
	 * @param array        $args   Optional. Other optional arguments, see
	 *                             Language_Pack_Upgrader::bulk_upgrade(). Default empty array.
	 * @return array|bool|WP_Error The result of the upgrade, or a WP_Error object instead.
	 */
	public function upgrade( $update = false, $args = array() ) {
		if ( $update ) {
			$update = array( $update );
		}

		$results = $this->bulk_upgrade( $update, $args );

		if ( ! is_array( $results ) ) {
			return $results;
		}

		return $results[0];
	}

	/**
	 * Upgrades several language packs at once.
	 *
	 * @since 3.7.0
	 *
	 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
	 *
	 * @param object[] $language_updates Optional. Array of language packs to update. See {@see wp_get_translation_updates()}.
	 *                                   Default empty array.
	 * @param array    $args {
	 *     Other arguments for upgrading multiple language packs. Default empty array.
	 *
	 *     @type bool $clear_update_cache Whether to clear the update cache when done.
	 *                                    Default true.
	 * }
	 * @return array|bool|WP_Error Will return an array of results, or true if there are no updates,
	 *                             false or WP_Error for initial errors.
	 */
	public function bulk_upgrade( $language_updates = array(), $args = array() ) {
		global $wp_filesystem;

		$defaults    = array(
			'clear_update_cache' => true,
		);
		$parsed_args = wp_parse_args( $args, $defaults );

		$this->init();
		$this->upgrade_strings();

		if ( ! $language_updates ) {
			$language_updates = wp_get_translation_updates();
		}

		if ( empty( $language_updates ) ) {
			$this->skin->header();
			$this->skin->set_result( true );
			$this->skin->feedback( 'up_to_date' );
			$this->skin->bulk_footer();
			$this->skin->footer();
			return true;
		}

		if ( 'upgrader_process_complete' === current_filter() ) {
			$this->skin->feedback( 'starting_upgrade' );
		}

		// Remove any existing upgrade filters from the plugin/theme upgraders #WP29425 & #WP29230.
		remove_all_filters( 'upgrader_pre_install' );
		remove_all_filters( 'upgrader_clear_destination' );
		remove_all_filters( 'upgrader_post_install' );
		remove_all_filters( 'upgrader_source_selection' );

		add_filter( 'upgrader_source_selection', array( $this, 'check_package' ), 10, 2 );

		$this->skin->header();

		// Connect to the filesystem first.
		$res = $this->fs_connect( array( WP_CONTENT_DIR, WP_LANG_DIR ) );
		if ( ! $res ) {
			$this->skin->footer();
			return false;
		}

		$results = array();

		$this->update_count   = count( $language_updates );
		$this->update_current = 0;

		/*
		 * The filesystem's mkdir() is not recursive. Make sure WP_LANG_DIR exists,
		 * as we then may need to create a /plugins or /themes directory inside of it.
		 */
		$remote_destination = $wp_filesystem->find_folder( WP_LANG_DIR );
		if ( ! $wp_filesystem->exists( $remote_destination ) ) {
			if ( ! $wp_filesystem->mkdir( $remote_destination, FS_CHMOD_DIR ) ) {
				return new WP_Error( 'mkdir_failed_lang_dir', $this->strings['mkdir_failed'], $remote_destination );
			}
		}

		$language_updates_results = array();

		foreach ( $language_updates as $language_update ) {

			$this->skin->language_update = $language_update;

			$destination = WP_LANG_DIR;
			if ( 'plugin' === $language_update->type ) {
				$destination .= '/plugins';
			} elseif ( 'theme' === $language_update->type ) {
				$destination .= '/themes';
			}

			++$this->update_current;

			$options = array(
				'package'                     => $language_update->package,
				'destination'                 => $destination,
				'clear_destination'           => true,
				'abort_if_destination_exists' => false, // We expect the destination to exist.
				'clear_working'               => true,
				'is_multi'                    => true,
				'hook_extra'                  => array(
					'language_update_type' => $language_update->type,
					'language_update'      => $language_update,
				),
			);

			$result = $this->run( $options );

			$results[] = $this->result;

			// Prevent credentials auth screen from displaying multiple times.
			if ( false === $result ) {
				break;
			}

			$language_updates_results[] = array(
				'language' => $language_update->language,
				'type'     => $language_update->type,
				'slug'     => isset( $language_update->slug ) ? $language_update->slug : 'default',
				'version'  => $language_update->version,
			);
		}

		// Remove upgrade hooks which are not required for translation updates.
		remove_action( 'upgrader_process_complete', array( 'Language_Pack_Upgrader', 'async_upgrade' ), 20 );
		remove_action( 'upgrader_process_complete', 'wp_version_check' );
		remove_action( 'upgrader_process_complete', 'wp_update_plugins' );
		remove_action( 'upgrader_process_complete', 'wp_update_themes' );

		/** This action is documented in wp-admin/includes/class-wp-upgrader.php */
		do_action(
			'upgrader_process_complete',
			$this,
			array(
				'action'       => 'update',
				'type'         => 'translation',
				'bulk'         => true,
				'translations' => $language_updates_results,
			)
		);

		// Re-add upgrade hooks.
		add_action( 'upgrader_process_complete', array( 'Language_Pack_Upgrader', 'async_upgrade' ), 20 );
		add_action( 'upgrader_process_complete', 'wp_version_check', 10, 0 );
		add_action( 'upgrader_process_complete', 'wp_update_plugins', 10, 0 );
		add_action( 'upgrader_process_complete', 'wp_update_themes', 10, 0 );

		$this->skin->bulk_footer();

		$this->skin->footer();

		// Clean up our hooks, in case something else does an upgrade on this connection.
		remove_filter( 'upgrader_source_selection', array( $this, 'check_package' ) );

		if ( $parsed_args['clear_update_cache'] ) {
			wp_clean_update_cache();
		}

		return $results;
	}

	/**
	 * Checks that the package source contains .mo and .po files.
	 *
	 * Hooked to the {@see 'upgrader_source_selection'} filter by
	 * Language_Pack_Upgrader::bulk_upgrade().
	 *
	 * @since 3.7.0
	 *
	 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
	 *
	 * @param string|WP_Error $source        The path to the downloaded package source.
	 * @param string          $remote_source Remote file source location.
	 * @return string|WP_Error The source as passed, or a WP_Error object on failure.
	 */
	public function check_package( $source, $remote_source ) {
		global $wp_filesystem;

		if ( is_wp_error( $source ) ) {
			return $source;
		}

		// Check that the folder contains a valid language.
		$files = $wp_filesystem->dirlist( $remote_source );

		// Check to see if the expected files exist in the folder.
		$po  = false;
		$mo  = false;
		$php = false;
		foreach ( (array) $files as $file => $filedata ) {
			if ( str_ends_with( $file, '.po' ) ) {
				$po = true;
			} elseif ( str_ends_with( $file, '.mo' ) ) {
				$mo = true;
			} elseif ( str_ends_with( $file, '.l10n.php' ) ) {
				$php = true;
			}
		}

		if ( $php ) {
			return $source;
		}

		if ( ! $mo || ! $po ) {
			return new WP_Error(
				'incompatible_archive_pomo',
				$this->strings['incompatible_archive'],
				sprintf(
					/* translators: 1: .po, 2: .mo, 3: .l10n.php */
					__( 'The language pack is missing either the %1$s, %2$s, or %3$s files.' ),
					'<code>.po</code>',
					'<code>.mo</code>',
					'<code>.l10n.php</code>'
				)
			);
		}

		return $source;
	}

	/**
	 * Gets the name of an item being updated.
	 *
	 * @since 3.7.0
	 *
	 * @param object $update The data for an update.
	 * @return string The name of the item being updated.
	 */
	public function get_name_for_update( $update ) {
		switch ( $update->type ) {
			case 'core':
				return 'WordPress'; // Not translated.

			case 'theme':
				$theme = wp_get_theme( $update->slug );
				if ( $theme->exists() ) {
					return $theme->Get( 'Name' );
				}
				break;
			case 'plugin':
				$plugin_data = get_plugins( '/' . $update->slug );
				$plugin_data = reset( $plugin_data );
				if ( $plugin_data ) {
					return $plugin_data['Name'];
				}
				break;
		}
		return '';
	}

	/**
	 * Clears existing translations where this item is going to be installed into.
	 *
	 * @since 5.1.0
	 *
	 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
	 *
	 * @param string $remote_destination The location on the remote filesystem to be cleared.
	 * @return bool|WP_Error True upon success, WP_Error on failure.
	 */
	public function clear_destination( $remote_destination ) {
		global $wp_filesystem;

		$language_update    = $this->skin->language_update;
		$language_directory = WP_LANG_DIR . '/'; // Local path for use with glob().

		if ( 'core' === $language_update->type ) {
			$files = array(
				$remote_destination . $language_update->language . '.po',
				$remote_destination . $language_update->language . '.mo',
				$remote_destination . $language_update->language . '.l10n.php',
				$remote_destination . 'admin-' . $language_update->language . '.po',
				$remote_destination . 'admin-' . $language_update->language . '.mo',
				$remote_destination . 'admin-' . $language_update->language . '.l10n.php',
				$remote_destination . 'admin-network-' . $language_update->language . '.po',
				$remote_destination . 'admin-network-' . $language_update->language . '.mo',
				$remote_destination . 'admin-network-' . $language_update->language . '.l10n.php',
				$remote_destination . 'continents-cities-' . $language_update->language . '.po',
				$remote_destination . 'continents-cities-' . $language_update->language . '.mo',
				$remote_destination . 'continents-cities-' . $language_update->language . '.l10n.php',
			);

			$json_translation_files = glob( $language_directory . $language_update->language . '-*.json' );
			if ( $json_translation_files ) {
				foreach ( $json_translation_files as $json_translation_file ) {
					$files[] = str_replace( $language_directory, $remote_destination, $json_translation_file );
				}
			}
		} else {
			$files = array(
				$remote_destination . $language_update->slug . '-' . $language_update->language . '.po',
				$remote_destination . $language_update->slug . '-' . $language_update->language . '.mo',
				$remote_destination . $language_update->slug . '-' . $language_update->language . '.l10n.php',
			);

			$language_directory     = $language_directory . $language_update->type . 's/';
			$json_translation_files = glob( $language_directory . $language_update->slug . '-' . $language_update->language . '-*.json' );
			if ( $json_translation_files ) {
				foreach ( $json_translation_files as $json_translation_file ) {
					$files[] = str_replace( $language_directory, $remote_destination, $json_translation_file );
				}
			}
		}

		$files = array_filter( $files, array( $wp_filesystem, 'exists' ) );

		// No files to delete.
		if ( ! $files ) {
			return true;
		}

		// Check all files are writable before attempting to clear the destination.
		$unwritable_files = array();

		// Check writability.
		foreach ( $files as $file ) {
			if ( ! $wp_filesystem->is_writable( $file ) ) {
				// Attempt to alter permissions to allow writes and try again.
				$wp_filesystem->chmod( $file, FS_CHMOD_FILE );
				if ( ! $wp_filesystem->is_writable( $file ) ) {
					$unwritable_files[] = $file;
				}
			}
		}

		if ( ! empty( $unwritable_files ) ) {
			return new WP_Error( 'files_not_writable', $this->strings['files_not_writable'], implode( ', ', $unwritable_files ) );
		}

		foreach ( $files as $file ) {
			if ( ! $wp_filesystem->delete( $file ) ) {
				return new WP_Error( 'remove_old_failed', $this->strings['remove_old_failed'] );
			}
		}

		return true;
	}
}
class-theme-upgrader-skin.php000060400000010120151327343360012230 0ustar00<?php
/**
 * Upgrader API: Theme_Upgrader_Skin class
 *
 * @package WordPress
 * @subpackage Upgrader
 * @since 4.6.0
 */

/**
 * Theme Upgrader Skin for WordPress Theme Upgrades.
 *
 * @since 2.8.0
 * @since 4.6.0 Moved to its own file from wp-admin/includes/class-wp-upgrader-skins.php.
 *
 * @see WP_Upgrader_Skin
 */
class Theme_Upgrader_Skin extends WP_Upgrader_Skin {

	/**
	 * Holds the theme slug in the Theme Directory.
	 *
	 * @since 2.8.0
	 *
	 * @var string
	 */
	public $theme = '';

	/**
	 * Constructor.
	 *
	 * Sets up the theme upgrader skin.
	 *
	 * @since 2.8.0
	 *
	 * @param array $args Optional. The theme upgrader skin arguments to
	 *                    override default options. Default empty array.
	 */
	public function __construct( $args = array() ) {
		$defaults = array(
			'url'   => '',
			'theme' => '',
			'nonce' => '',
			'title' => __( 'Update Theme' ),
		);
		$args     = wp_parse_args( $args, $defaults );

		$this->theme = $args['theme'];

		parent::__construct( $args );
	}

	/**
	 * Performs an action following a single theme update.
	 *
	 * @since 2.8.0
	 */
	public function after() {
		$this->decrement_update_count( 'theme' );

		$update_actions = array();
		$theme_info     = $this->upgrader->theme_info();
		if ( $theme_info ) {
			$name       = $theme_info->display( 'Name' );
			$stylesheet = $this->upgrader->result['destination_name'];
			$template   = $theme_info->get_template();

			$activate_link = add_query_arg(
				array(
					'action'     => 'activate',
					'template'   => urlencode( $template ),
					'stylesheet' => urlencode( $stylesheet ),
				),
				admin_url( 'themes.php' )
			);
			$activate_link = wp_nonce_url( $activate_link, 'switch-theme_' . $stylesheet );

			$customize_url = add_query_arg(
				array(
					'theme'  => urlencode( $stylesheet ),
					'return' => urlencode( admin_url( 'themes.php' ) ),
				),
				admin_url( 'customize.php' )
			);

			if ( get_stylesheet() === $stylesheet ) {
				if ( current_user_can( 'edit_theme_options' ) && current_user_can( 'customize' ) ) {
					$update_actions['preview'] = sprintf(
						'<a href="%s" class="hide-if-no-customize load-customize">' .
						'<span aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></a>',
						esc_url( $customize_url ),
						__( 'Customize' ),
						/* translators: Hidden accessibility text. %s: Theme name. */
						sprintf( __( 'Customize &#8220;%s&#8221;' ), $name )
					);
				}
			} elseif ( current_user_can( 'switch_themes' ) ) {
				if ( current_user_can( 'edit_theme_options' ) && current_user_can( 'customize' ) ) {
					$update_actions['preview'] = sprintf(
						'<a href="%s" class="hide-if-no-customize load-customize">' .
						'<span aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></a>',
						esc_url( $customize_url ),
						__( 'Live Preview' ),
						/* translators: Hidden accessibility text. %s: Theme name. */
						sprintf( __( 'Live Preview &#8220;%s&#8221;' ), $name )
					);
				}

				$update_actions['activate'] = sprintf(
					'<a href="%s" class="activatelink">' .
					'<span aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></a>',
					esc_url( $activate_link ),
					_x( 'Activate', 'theme' ),
					/* translators: Hidden accessibility text. %s: Theme name. */
					sprintf( _x( 'Activate &#8220;%s&#8221;', 'theme' ), $name )
				);
			}

			if ( ! $this->result || is_wp_error( $this->result ) || is_network_admin() ) {
				unset( $update_actions['preview'], $update_actions['activate'] );
			}
		}

		$update_actions['themes_page'] = sprintf(
			'<a href="%s" target="_parent">%s</a>',
			self_admin_url( 'themes.php' ),
			__( 'Go to Themes page' )
		);

		/**
		 * Filters the list of action links available following a single theme update.
		 *
		 * @since 2.8.0
		 *
		 * @param string[] $update_actions Array of theme action links.
		 * @param string   $theme          Theme directory name.
		 */
		$update_actions = apply_filters( 'update_theme_complete_actions', $update_actions, $this->theme );

		if ( ! empty( $update_actions ) ) {
			$this->feedback( implode( ' | ', (array) $update_actions ) );
		}
	}
}
class-wp-ajax-upgrader-skin.php000060400000010141151327343360012500 0ustar00<?php
/**
 * Upgrader API: WP_Ajax_Upgrader_Skin class
 *
 * @package WordPress
 * @subpackage Upgrader
 * @since 4.6.0
 */

/**
 * Upgrader Skin for Ajax WordPress upgrades.
 *
 * This skin is designed to be used for Ajax updates.
 *
 * @since 4.6.0
 *
 * @see Automatic_Upgrader_Skin
 */
class WP_Ajax_Upgrader_Skin extends Automatic_Upgrader_Skin {

	/**
	 * Plugin info.
	 *
	 * The Plugin_Upgrader::bulk_upgrade() method will fill this in
	 * with info retrieved from the get_plugin_data() function.
	 *
	 * @var array Plugin data. Values will be empty if not supplied by the plugin.
	 */
	public $plugin_info = array();

	/**
	 * Theme info.
	 *
	 * The Theme_Upgrader::bulk_upgrade() method will fill this in
	 * with info retrieved from the Theme_Upgrader::theme_info() method,
	 * which in turn calls the wp_get_theme() function.
	 *
	 * @var WP_Theme|false The theme's info object, or false.
	 */
	public $theme_info = false;

	/**
	 * Holds the WP_Error object.
	 *
	 * @since 4.6.0
	 *
	 * @var null|WP_Error
	 */
	protected $errors = null;

	/**
	 * Constructor.
	 *
	 * Sets up the WordPress Ajax upgrader skin.
	 *
	 * @since 4.6.0
	 *
	 * @see WP_Upgrader_Skin::__construct()
	 *
	 * @param array $args Optional. The WordPress Ajax upgrader skin arguments to
	 *                    override default options. See WP_Upgrader_Skin::__construct().
	 *                    Default empty array.
	 */
	public function __construct( $args = array() ) {
		parent::__construct( $args );

		$this->errors = new WP_Error();
	}

	/**
	 * Retrieves the list of errors.
	 *
	 * @since 4.6.0
	 *
	 * @return WP_Error Errors during an upgrade.
	 */
	public function get_errors() {
		return $this->errors;
	}

	/**
	 * Retrieves a string for error messages.
	 *
	 * @since 4.6.0
	 *
	 * @return string Error messages during an upgrade.
	 */
	public function get_error_messages() {
		$messages = array();

		foreach ( $this->errors->get_error_codes() as $error_code ) {
			$error_data = $this->errors->get_error_data( $error_code );

			if ( $error_data && is_string( $error_data ) ) {
				$messages[] = $this->errors->get_error_message( $error_code ) . ' ' . esc_html( strip_tags( $error_data ) );
			} else {
				$messages[] = $this->errors->get_error_message( $error_code );
			}
		}

		return implode( ', ', $messages );
	}

	/**
	 * Stores an error message about the upgrade.
	 *
	 * @since 4.6.0
	 * @since 5.3.0 Formalized the existing `...$args` parameter by adding it
	 *              to the function signature.
	 *
	 * @param string|WP_Error $errors  Errors.
	 * @param mixed           ...$args Optional text replacements.
	 */
	public function error( $errors, ...$args ) {
		if ( is_string( $errors ) ) {
			$string = $errors;
			if ( ! empty( $this->upgrader->strings[ $string ] ) ) {
				$string = $this->upgrader->strings[ $string ];
			}

			if ( str_contains( $string, '%' ) ) {
				if ( ! empty( $args ) ) {
					$string = vsprintf( $string, $args );
				}
			}

			// Count existing errors to generate a unique error code.
			$errors_count = count( $this->errors->get_error_codes() );
			$this->errors->add( 'unknown_upgrade_error_' . ( $errors_count + 1 ), $string );
		} elseif ( is_wp_error( $errors ) ) {
			foreach ( $errors->get_error_codes() as $error_code ) {
				$this->errors->add( $error_code, $errors->get_error_message( $error_code ), $errors->get_error_data( $error_code ) );
			}
		}

		parent::error( $errors, ...$args );
	}

	/**
	 * Stores a message about the upgrade.
	 *
	 * @since 4.6.0
	 * @since 5.3.0 Formalized the existing `...$args` parameter by adding it
	 *              to the function signature.
	 * @since 5.9.0 Renamed `$data` to `$feedback` for PHP 8 named parameter support.
	 *
	 * @param string|array|WP_Error $feedback Message data.
	 * @param mixed                 ...$args  Optional text replacements.
	 */
	public function feedback( $feedback, ...$args ) {
		if ( is_wp_error( $feedback ) ) {
			foreach ( $feedback->get_error_codes() as $error_code ) {
				$this->errors->add( $error_code, $feedback->get_error_message( $error_code ), $feedback->get_error_data( $error_code ) );
			}
		}

		parent::feedback( $feedback, ...$args );
	}
}
class-wp-filesystem-base.php000060400000057532151327343360012117 0ustar00<?php
/**
 * Base WordPress Filesystem
 *
 * @package WordPress
 * @subpackage Filesystem
 */

/**
 * Base WordPress Filesystem class which Filesystem implementations extend.
 *
 * @since 2.5.0
 */
#[AllowDynamicProperties]
class WP_Filesystem_Base {

	/**
	 * Whether to display debug data for the connection.
	 *
	 * @since 2.5.0
	 * @var bool
	 */
	public $verbose = false;

	/**
	 * Cached list of local filepaths to mapped remote filepaths.
	 *
	 * @since 2.7.0
	 * @var array
	 */
	public $cache = array();

	/**
	 * The Access method of the current connection, Set automatically.
	 *
	 * @since 2.5.0
	 * @var string
	 */
	public $method = '';

	/**
	 * @var WP_Error
	 */
	public $errors = null;

	/**
	 */
	public $options = array();

	/**
	 * Returns the path on the remote filesystem of ABSPATH.
	 *
	 * @since 2.7.0
	 *
	 * @return string The location of the remote path.
	 */
	public function abspath() {
		$folder = $this->find_folder( ABSPATH );

		/*
		 * Perhaps the FTP folder is rooted at the WordPress install.
		 * Check for wp-includes folder in root. Could have some false positives, but rare.
		 */
		if ( ! $folder && $this->is_dir( '/' . WPINC ) ) {
			$folder = '/';
		}

		return $folder;
	}

	/**
	 * Returns the path on the remote filesystem of WP_CONTENT_DIR.
	 *
	 * @since 2.7.0
	 *
	 * @return string The location of the remote path.
	 */
	public function wp_content_dir() {
		return $this->find_folder( WP_CONTENT_DIR );
	}

	/**
	 * Returns the path on the remote filesystem of WP_PLUGIN_DIR.
	 *
	 * @since 2.7.0
	 *
	 * @return string The location of the remote path.
	 */
	public function wp_plugins_dir() {
		return $this->find_folder( WP_PLUGIN_DIR );
	}

	/**
	 * Returns the path on the remote filesystem of the Themes Directory.
	 *
	 * @since 2.7.0
	 *
	 * @param string|false $theme Optional. The theme stylesheet or template for the directory.
	 *                            Default false.
	 * @return string The location of the remote path.
	 */
	public function wp_themes_dir( $theme = false ) {
		$theme_root = get_theme_root( $theme );

		// Account for relative theme roots.
		if ( '/themes' === $theme_root || ! is_dir( $theme_root ) ) {
			$theme_root = WP_CONTENT_DIR . $theme_root;
		}

		return $this->find_folder( $theme_root );
	}

	/**
	 * Returns the path on the remote filesystem of WP_LANG_DIR.
	 *
	 * @since 3.2.0
	 *
	 * @return string The location of the remote path.
	 */
	public function wp_lang_dir() {
		return $this->find_folder( WP_LANG_DIR );
	}

	/**
	 * Locates a folder on the remote filesystem.
	 *
	 * @since 2.5.0
	 * @deprecated 2.7.0 use WP_Filesystem_Base::abspath() or WP_Filesystem_Base::wp_*_dir() instead.
	 * @see WP_Filesystem_Base::abspath()
	 * @see WP_Filesystem_Base::wp_content_dir()
	 * @see WP_Filesystem_Base::wp_plugins_dir()
	 * @see WP_Filesystem_Base::wp_themes_dir()
	 * @see WP_Filesystem_Base::wp_lang_dir()
	 *
	 * @param string $base    Optional. The folder to start searching from. Default '.'.
	 * @param bool   $verbose Optional. True to display debug information. Default false.
	 * @return string The location of the remote path.
	 */
	public function find_base_dir( $base = '.', $verbose = false ) {
		_deprecated_function( __FUNCTION__, '2.7.0', 'WP_Filesystem_Base::abspath() or WP_Filesystem_Base::wp_*_dir()' );
		$this->verbose = $verbose;
		return $this->abspath();
	}

	/**
	 * Locates a folder on the remote filesystem.
	 *
	 * @since 2.5.0
	 * @deprecated 2.7.0 use WP_Filesystem_Base::abspath() or WP_Filesystem_Base::wp_*_dir() methods instead.
	 * @see WP_Filesystem_Base::abspath()
	 * @see WP_Filesystem_Base::wp_content_dir()
	 * @see WP_Filesystem_Base::wp_plugins_dir()
	 * @see WP_Filesystem_Base::wp_themes_dir()
	 * @see WP_Filesystem_Base::wp_lang_dir()
	 *
	 * @param string $base    Optional. The folder to start searching from. Default '.'.
	 * @param bool   $verbose Optional. True to display debug information. Default false.
	 * @return string The location of the remote path.
	 */
	public function get_base_dir( $base = '.', $verbose = false ) {
		_deprecated_function( __FUNCTION__, '2.7.0', 'WP_Filesystem_Base::abspath() or WP_Filesystem_Base::wp_*_dir()' );
		$this->verbose = $verbose;
		return $this->abspath();
	}

	/**
	 * Locates a folder on the remote filesystem.
	 *
	 * Assumes that on Windows systems, Stripping off the Drive
	 * letter is OK Sanitizes \\ to / in Windows filepaths.
	 *
	 * @since 2.7.0
	 *
	 * @param string $folder the folder to locate.
	 * @return string|false The location of the remote path, false on failure.
	 */
	public function find_folder( $folder ) {
		if ( isset( $this->cache[ $folder ] ) ) {
			return $this->cache[ $folder ];
		}

		if ( stripos( $this->method, 'ftp' ) !== false ) {
			$constant_overrides = array(
				'FTP_BASE'        => ABSPATH,
				'FTP_CONTENT_DIR' => WP_CONTENT_DIR,
				'FTP_PLUGIN_DIR'  => WP_PLUGIN_DIR,
				'FTP_LANG_DIR'    => WP_LANG_DIR,
			);

			// Direct matches ( folder = CONSTANT/ ).
			foreach ( $constant_overrides as $constant => $dir ) {
				if ( ! defined( $constant ) ) {
					continue;
				}

				if ( $folder === $dir ) {
					return trailingslashit( constant( $constant ) );
				}
			}

			// Prefix matches ( folder = CONSTANT/subdir ),
			foreach ( $constant_overrides as $constant => $dir ) {
				if ( ! defined( $constant ) ) {
					continue;
				}

				if ( 0 === stripos( $folder, $dir ) ) { // $folder starts with $dir.
					$potential_folder = preg_replace( '#^' . preg_quote( $dir, '#' ) . '/#i', trailingslashit( constant( $constant ) ), $folder );
					$potential_folder = trailingslashit( $potential_folder );

					if ( $this->is_dir( $potential_folder ) ) {
						$this->cache[ $folder ] = $potential_folder;

						return $potential_folder;
					}
				}
			}
		} elseif ( 'direct' === $this->method ) {
			$folder = str_replace( '\\', '/', $folder ); // Windows path sanitization.

			return trailingslashit( $folder );
		}

		$folder = preg_replace( '|^([a-z]{1}):|i', '', $folder ); // Strip out Windows drive letter if it's there.
		$folder = str_replace( '\\', '/', $folder ); // Windows path sanitization.

		if ( isset( $this->cache[ $folder ] ) ) {
			return $this->cache[ $folder ];
		}

		if ( $this->exists( $folder ) ) { // Folder exists at that absolute path.
			$folder                 = trailingslashit( $folder );
			$this->cache[ $folder ] = $folder;

			return $folder;
		}

		$return = $this->search_for_folder( $folder );

		if ( $return ) {
			$this->cache[ $folder ] = $return;
		}

		return $return;
	}

	/**
	 * Locates a folder on the remote filesystem.
	 *
	 * Expects Windows sanitized path.
	 *
	 * @since 2.7.0
	 *
	 * @param string $folder The folder to locate.
	 * @param string $base   The folder to start searching from.
	 * @param bool   $loop   If the function has recursed. Internal use only.
	 * @return string|false The location of the remote path, false to cease looping.
	 */
	public function search_for_folder( $folder, $base = '.', $loop = false ) {
		if ( empty( $base ) || '.' === $base ) {
			$base = trailingslashit( $this->cwd() );
		}

		$folder = untrailingslashit( $folder );

		if ( $this->verbose ) {
			/* translators: 1: Folder to locate, 2: Folder to start searching from. */
			printf( "\n" . __( 'Looking for %1$s in %2$s' ) . "<br />\n", $folder, $base );
		}

		$folder_parts     = explode( '/', $folder );
		$folder_part_keys = array_keys( $folder_parts );
		$last_index       = array_pop( $folder_part_keys );
		$last_path        = $folder_parts[ $last_index ];

		$files = $this->dirlist( $base );

		foreach ( $folder_parts as $index => $key ) {
			if ( $index === $last_index ) {
				continue; // We want this to be caught by the next code block.
			}

			/*
			 * Working from /home/ to /user/ to /wordpress/ see if that file exists within
			 * the current folder, If it's found, change into it and follow through looking
			 * for it. If it can't find WordPress down that route, it'll continue onto the next
			 * folder level, and see if that matches, and so on. If it reaches the end, and still
			 * can't find it, it'll return false for the entire function.
			 */
			if ( isset( $files[ $key ] ) ) {

				// Let's try that folder:
				$newdir = trailingslashit( path_join( $base, $key ) );

				if ( $this->verbose ) {
					/* translators: %s: Directory name. */
					printf( "\n" . __( 'Changing to %s' ) . "<br />\n", $newdir );
				}

				// Only search for the remaining path tokens in the directory, not the full path again.
				$newfolder = implode( '/', array_slice( $folder_parts, $index + 1 ) );
				$ret       = $this->search_for_folder( $newfolder, $newdir, $loop );

				if ( $ret ) {
					return $ret;
				}
			}
		}

		/*
		 * Only check this as a last resort, to prevent locating the incorrect install.
		 * All above procedures will fail quickly if this is the right branch to take.
		 */
		if ( isset( $files[ $last_path ] ) ) {
			if ( $this->verbose ) {
				/* translators: %s: Directory name. */
				printf( "\n" . __( 'Found %s' ) . "<br />\n", $base . $last_path );
			}

			return trailingslashit( $base . $last_path );
		}

		/*
		 * Prevent this function from looping again.
		 * No need to proceed if we've just searched in `/`.
		 */
		if ( $loop || '/' === $base ) {
			return false;
		}

		/*
		 * As an extra last resort, Change back to / if the folder wasn't found.
		 * This comes into effect when the CWD is /home/user/ but WP is at /var/www/....
		 */
		return $this->search_for_folder( $folder, '/', true );
	}

	/**
	 * Returns the *nix-style file permissions for a file.
	 *
	 * From the PHP documentation page for fileperms().
	 *
	 * @link https://www.php.net/manual/en/function.fileperms.php
	 *
	 * @since 2.5.0
	 *
	 * @param string $file String filename.
	 * @return string The *nix-style representation of permissions.
	 */
	public function gethchmod( $file ) {
		$perms = intval( $this->getchmod( $file ), 8 );

		if ( ( $perms & 0xC000 ) === 0xC000 ) { // Socket.
			$info = 's';
		} elseif ( ( $perms & 0xA000 ) === 0xA000 ) { // Symbolic Link.
			$info = 'l';
		} elseif ( ( $perms & 0x8000 ) === 0x8000 ) { // Regular.
			$info = '-';
		} elseif ( ( $perms & 0x6000 ) === 0x6000 ) { // Block special.
			$info = 'b';
		} elseif ( ( $perms & 0x4000 ) === 0x4000 ) { // Directory.
			$info = 'd';
		} elseif ( ( $perms & 0x2000 ) === 0x2000 ) { // Character special.
			$info = 'c';
		} elseif ( ( $perms & 0x1000 ) === 0x1000 ) { // FIFO pipe.
			$info = 'p';
		} else { // Unknown.
			$info = 'u';
		}

		// Owner.
		$info .= ( ( $perms & 0x0100 ) ? 'r' : '-' );
		$info .= ( ( $perms & 0x0080 ) ? 'w' : '-' );
		$info .= ( ( $perms & 0x0040 ) ?
					( ( $perms & 0x0800 ) ? 's' : 'x' ) :
					( ( $perms & 0x0800 ) ? 'S' : '-' ) );

		// Group.
		$info .= ( ( $perms & 0x0020 ) ? 'r' : '-' );
		$info .= ( ( $perms & 0x0010 ) ? 'w' : '-' );
		$info .= ( ( $perms & 0x0008 ) ?
					( ( $perms & 0x0400 ) ? 's' : 'x' ) :
					( ( $perms & 0x0400 ) ? 'S' : '-' ) );

		// World.
		$info .= ( ( $perms & 0x0004 ) ? 'r' : '-' );
		$info .= ( ( $perms & 0x0002 ) ? 'w' : '-' );
		$info .= ( ( $perms & 0x0001 ) ?
					( ( $perms & 0x0200 ) ? 't' : 'x' ) :
					( ( $perms & 0x0200 ) ? 'T' : '-' ) );

		return $info;
	}

	/**
	 * Gets the permissions of the specified file or filepath in their octal format.
	 *
	 * @since 2.5.0
	 *
	 * @param string $file Path to the file.
	 * @return string Mode of the file (the last 3 digits).
	 */
	public function getchmod( $file ) {
		return '777';
	}

	/**
	 * Converts *nix-style file permissions to an octal number.
	 *
	 * Converts '-rw-r--r--' to 0644
	 * From "info at rvgate dot nl"'s comment on the PHP documentation for chmod()
	 *
	 * @link https://www.php.net/manual/en/function.chmod.php#49614
	 *
	 * @since 2.5.0
	 *
	 * @param string $mode string The *nix-style file permissions.
	 * @return string Octal representation of permissions.
	 */
	public function getnumchmodfromh( $mode ) {
		$realmode = '';
		$legal    = array( '', 'w', 'r', 'x', '-' );
		$attarray = preg_split( '//', $mode );

		for ( $i = 0, $c = count( $attarray ); $i < $c; $i++ ) {
			$key = array_search( $attarray[ $i ], $legal, true );

			if ( $key ) {
				$realmode .= $legal[ $key ];
			}
		}

		$mode  = str_pad( $realmode, 10, '-', STR_PAD_LEFT );
		$trans = array(
			'-' => '0',
			'r' => '4',
			'w' => '2',
			'x' => '1',
		);
		$mode  = strtr( $mode, $trans );

		$newmode  = $mode[0];
		$newmode .= $mode[1] + $mode[2] + $mode[3];
		$newmode .= $mode[4] + $mode[5] + $mode[6];
		$newmode .= $mode[7] + $mode[8] + $mode[9];

		return $newmode;
	}

	/**
	 * Determines if the string provided contains binary characters.
	 *
	 * @since 2.7.0
	 *
	 * @param string $text String to test against.
	 * @return bool True if string is binary, false otherwise.
	 */
	public function is_binary( $text ) {
		return (bool) preg_match( '|[^\x20-\x7E]|', $text ); // chr(32)..chr(127)
	}

	/**
	 * Changes the owner of a file or directory.
	 *
	 * Default behavior is to do nothing, override this in your subclass, if desired.
	 *
	 * @since 2.5.0
	 *
	 * @param string     $file      Path to the file or directory.
	 * @param string|int $owner     A user name or number.
	 * @param bool       $recursive Optional. If set to true, changes file owner recursively.
	 *                              Default false.
	 * @return bool True on success, false on failure.
	 */
	public function chown( $file, $owner, $recursive = false ) {
		return false;
	}

	/**
	 * Connects filesystem.
	 *
	 * @since 2.5.0
	 * @abstract
	 *
	 * @return bool True on success, false on failure (always true for WP_Filesystem_Direct).
	 */
	public function connect() {
		return true;
	}

	/**
	 * Reads entire file into a string.
	 *
	 * @since 2.5.0
	 * @abstract
	 *
	 * @param string $file Name of the file to read.
	 * @return string|false Read data on success, false on failure.
	 */
	public function get_contents( $file ) {
		return false;
	}

	/**
	 * Reads entire file into an array.
	 *
	 * @since 2.5.0
	 * @abstract
	 *
	 * @param string $file Path to the file.
	 * @return array|false File contents in an array on success, false on failure.
	 */
	public function get_contents_array( $file ) {
		return false;
	}

	/**
	 * Writes a string to a file.
	 *
	 * @since 2.5.0
	 * @abstract
	 *
	 * @param string    $file     Remote path to the file where to write the data.
	 * @param string    $contents The data to write.
	 * @param int|false $mode     Optional. The file permissions as octal number, usually 0644.
	 *                            Default false.
	 * @return bool True on success, false on failure.
	 */
	public function put_contents( $file, $contents, $mode = false ) {
		return false;
	}

	/**
	 * Gets the current working directory.
	 *
	 * @since 2.5.0
	 * @abstract
	 *
	 * @return string|false The current working directory on success, false on failure.
	 */
	public function cwd() {
		return false;
	}

	/**
	 * Changes current directory.
	 *
	 * @since 2.5.0
	 * @abstract
	 *
	 * @param string $dir The new current directory.
	 * @return bool True on success, false on failure.
	 */
	public function chdir( $dir ) {
		return false;
	}

	/**
	 * Changes the file group.
	 *
	 * @since 2.5.0
	 * @abstract
	 *
	 * @param string     $file      Path to the file.
	 * @param string|int $group     A group name or number.
	 * @param bool       $recursive Optional. If set to true, changes file group recursively.
	 *                              Default false.
	 * @return bool True on success, false on failure.
	 */
	public function chgrp( $file, $group, $recursive = false ) {
		return false;
	}

	/**
	 * Changes filesystem permissions.
	 *
	 * @since 2.5.0
	 * @abstract
	 *
	 * @param string    $file      Path to the file.
	 * @param int|false $mode      Optional. The permissions as octal number, usually 0644 for files,
	 *                             0755 for directories. Default false.
	 * @param bool      $recursive Optional. If set to true, changes file permissions recursively.
	 *                             Default false.
	 * @return bool True on success, false on failure.
	 */
	public function chmod( $file, $mode = false, $recursive = false ) {
		return false;
	}

	/**
	 * Gets the file owner.
	 *
	 * @since 2.5.0
	 * @abstract
	 *
	 * @param string $file Path to the file.
	 * @return string|false Username of the owner on success, false on failure.
	 */
	public function owner( $file ) {
		return false;
	}

	/**
	 * Gets the file's group.
	 *
	 * @since 2.5.0
	 * @abstract
	 *
	 * @param string $file Path to the file.
	 * @return string|false The group on success, false on failure.
	 */
	public function group( $file ) {
		return false;
	}

	/**
	 * Copies a file.
	 *
	 * @since 2.5.0
	 * @abstract
	 *
	 * @param string    $source      Path to the source file.
	 * @param string    $destination Path to the destination file.
	 * @param bool      $overwrite   Optional. Whether to overwrite the destination file if it exists.
	 *                               Default false.
	 * @param int|false $mode        Optional. The permissions as octal number, usually 0644 for files,
	 *                               0755 for dirs. Default false.
	 * @return bool True on success, false on failure.
	 */
	public function copy( $source, $destination, $overwrite = false, $mode = false ) {
		return false;
	}

	/**
	 * Moves a file.
	 *
	 * @since 2.5.0
	 * @abstract
	 *
	 * @param string $source      Path to the source file.
	 * @param string $destination Path to the destination file.
	 * @param bool   $overwrite   Optional. Whether to overwrite the destination file if it exists.
	 *                            Default false.
	 * @return bool True on success, false on failure.
	 */
	public function move( $source, $destination, $overwrite = false ) {
		return false;
	}

	/**
	 * Deletes a file or directory.
	 *
	 * @since 2.5.0
	 * @abstract
	 *
	 * @param string       $file      Path to the file or directory.
	 * @param bool         $recursive Optional. If set to true, deletes files and folders recursively.
	 *                                Default false.
	 * @param string|false $type      Type of resource. 'f' for file, 'd' for directory.
	 *                                Default false.
	 * @return bool True on success, false on failure.
	 */
	public function delete( $file, $recursive = false, $type = false ) {
		return false;
	}

	/**
	 * Checks if a file or directory exists.
	 *
	 * @since 2.5.0
	 * @abstract
	 *
	 * @param string $path Path to file or directory.
	 * @return bool Whether $path exists or not.
	 */
	public function exists( $path ) {
		return false;
	}

	/**
	 * Checks if resource is a file.
	 *
	 * @since 2.5.0
	 * @abstract
	 *
	 * @param string $file File path.
	 * @return bool Whether $file is a file.
	 */
	public function is_file( $file ) {
		return false;
	}

	/**
	 * Checks if resource is a directory.
	 *
	 * @since 2.5.0
	 * @abstract
	 *
	 * @param string $path Directory path.
	 * @return bool Whether $path is a directory.
	 */
	public function is_dir( $path ) {
		return false;
	}

	/**
	 * Checks if a file is readable.
	 *
	 * @since 2.5.0
	 * @abstract
	 *
	 * @param string $file Path to file.
	 * @return bool Whether $file is readable.
	 */
	public function is_readable( $file ) {
		return false;
	}

	/**
	 * Checks if a file or directory is writable.
	 *
	 * @since 2.5.0
	 * @abstract
	 *
	 * @param string $path Path to file or directory.
	 * @return bool Whether $path is writable.
	 */
	public function is_writable( $path ) {
		return false;
	}

	/**
	 * Gets the file's last access time.
	 *
	 * @since 2.5.0
	 * @abstract
	 *
	 * @param string $file Path to file.
	 * @return int|false Unix timestamp representing last access time, false on failure.
	 */
	public function atime( $file ) {
		return false;
	}

	/**
	 * Gets the file modification time.
	 *
	 * @since 2.5.0
	 * @abstract
	 *
	 * @param string $file Path to file.
	 * @return int|false Unix timestamp representing modification time, false on failure.
	 */
	public function mtime( $file ) {
		return false;
	}

	/**
	 * Gets the file size (in bytes).
	 *
	 * @since 2.5.0
	 * @abstract
	 *
	 * @param string $file Path to file.
	 * @return int|false Size of the file in bytes on success, false on failure.
	 */
	public function size( $file ) {
		return false;
	}

	/**
	 * Sets the access and modification times of a file.
	 *
	 * Note: If $file doesn't exist, it will be created.
	 *
	 * @since 2.5.0
	 * @abstract
	 *
	 * @param string $file  Path to file.
	 * @param int    $time  Optional. Modified time to set for file.
	 *                      Default 0.
	 * @param int    $atime Optional. Access time to set for file.
	 *                      Default 0.
	 * @return bool True on success, false on failure.
	 */
	public function touch( $file, $time = 0, $atime = 0 ) {
		return false;
	}

	/**
	 * Creates a directory.
	 *
	 * @since 2.5.0
	 * @abstract
	 *
	 * @param string           $path  Path for new directory.
	 * @param int|false        $chmod Optional. The permissions as octal number (or false to skip chmod).
	 *                                Default false.
	 * @param string|int|false $chown Optional. A user name or number (or false to skip chown).
	 *                                Default false.
	 * @param string|int|false $chgrp Optional. A group name or number (or false to skip chgrp).
	 *                                Default false.
	 * @return bool True on success, false on failure.
	 */
	public function mkdir( $path, $chmod = false, $chown = false, $chgrp = false ) {
		return false;
	}

	/**
	 * Deletes a directory.
	 *
	 * @since 2.5.0
	 * @abstract
	 *
	 * @param string $path      Path to directory.
	 * @param bool   $recursive Optional. Whether to recursively remove files/directories.
	 *                          Default false.
	 * @return bool True on success, false on failure.
	 */
	public function rmdir( $path, $recursive = false ) {
		return false;
	}

	/**
	 * Gets details for files in a directory or a specific file.
	 *
	 * @since 2.5.0
	 * @abstract
	 *
	 * @param string $path           Path to directory or file.
	 * @param bool   $include_hidden Optional. Whether to include details of hidden ("." prefixed) files.
	 *                               Default true.
	 * @param bool   $recursive      Optional. Whether to recursively include file details in nested directories.
	 *                               Default false.
	 * @return array|false {
	 *     Array of arrays containing file information. False if unable to list directory contents.
	 *
	 *     @type array ...$0 {
	 *         Array of file information. Note that some elements may not be available on all filesystems.
	 *
	 *         @type string           $name        Name of the file or directory.
	 *         @type string           $perms       *nix representation of permissions.
	 *         @type string           $permsn      Octal representation of permissions.
	 *         @type int|string|false $number      File number. May be a numeric string. False if not available.
	 *         @type string|false     $owner       Owner name or ID, or false if not available.
	 *         @type string|false     $group       File permissions group, or false if not available.
	 *         @type int|string|false $size        Size of file in bytes. May be a numeric string.
	 *                                             False if not available.
	 *         @type int|string|false $lastmodunix Last modified unix timestamp. May be a numeric string.
	 *                                             False if not available.
	 *         @type string|false     $lastmod     Last modified month (3 letters) and day (without leading 0), or
	 *                                             false if not available.
	 *         @type string|false     $time        Last modified time, or false if not available.
	 *         @type string           $type        Type of resource. 'f' for file, 'd' for directory, 'l' for link.
	 *         @type array|false      $files       If a directory and `$recursive` is true, contains another array of
	 *                                             files. False if unable to list directory contents.
	 *     }
	 * }
	 */
	public function dirlist( $path, $include_hidden = true, $recursive = false ) {
		return false;
	}
}
class-yith-wcwl-exception.php000064400000001421151327635660012317 0ustar00<?php
/**
 * Wishlist Exception class
 *
 * @author YITH
 * @package YITH\Wishlist\Classes
 * @version 3.0.0
 */

if ( ! defined( 'YITH_WCWL' ) ) {
	exit;
} // Exit if accessed directly

if ( ! class_exists( 'YITH_WCWL_Exception' ) ) {
	/**
	 * WooCommerce Wishlist Exception
	 *
	 * @since 1.0.0
	 */
	class YITH_WCWL_Exception extends Exception {
		/**
		 * Available error codes
		 *
		 * @var array
		 */
		private $error_codes = array(
			0 => 'error',
			1 => 'exists',
		);

		/**
		 * Returns textual code for the error
		 *
		 * @return string Textual code of the error.
		 */
		public function getTextualCode() {
			$code = $this->getCode();

			if ( array_key_exists( $code, $this->error_codes ) ) {
				return $this->error_codes[ $code ];
			}

			return 'error';
		}
	}
}
class-yith-wcwl.php000064400000065001151327635660010327 0ustar00<?php
/**
 * Main class
 *
 * @author YITH
 * @package YITH\Wishlist\Classes
 * @version 3.0.0
 */

if ( ! defined( 'YITH_WCWL' ) ) {
	exit;
} // Exit if accessed directly

if ( ! class_exists( 'YITH_WCWL' ) ) {
	/**
	 * WooCommerce Wishlist
	 *
	 * @since 1.0.0
	 */
	class YITH_WCWL {
		/**
		 * Single instance of the class
		 *
		 * @var \YITH_WCWL
		 * @since 2.0.0
		 */
		protected static $instance;

		/**
		 * Last operation token
		 *
		 * @var string
		 * @since 2.0.0
		 */
		public $last_operation_token;

		/**
		 * Query string parameter used to generate Wishlist urls
		 *
		 * @var string
		 * @since 2.1.2
		 */
		public $wishlist_param = 'wishlist-action';

		/**
		 * Returns single instance of the class
		 *
		 * @return \YITH_WCWL
		 * @since 2.0.0
		 */
		public static function get_instance() {
			if ( is_null( self::$instance ) ) {
				self::$instance = new self();
			}

			return self::$instance;
		}

		/**
		 * Constructor.
		 *
		 * @return \YITH_WCWL
		 * @since 1.0.0
		 */
		public function __construct() {
			// register data stores.
			add_filter( 'woocommerce_data_stores', array( $this, 'register_data_stores' ) );

			// init frontend class.
			$this->wcwl_frontend = YITH_WCWL_Frontend();

			// init crons.
			$this->wcwl_cron = YITH_WCWL_Cron();

			// init session.
			$this->wcwl_session = YITH_WCWL_Session();

			// init admin handling.
			if ( is_admin() ) {
				$this->wcwl_admin = YITH_WCWL_Admin();
			}

			// load plugin-fw.
			add_action( 'plugins_loaded', array( $this, 'plugin_fw_loader' ), 15 );
			add_action( 'plugins_loaded', array( $this, 'privacy_loader' ), 20 );

			// add rewrite rule.
			add_action( 'init', array( $this, 'add_rewrite_rules' ), 0 );
			add_filter( 'query_vars', array( $this, 'add_public_query_var' ) );

			// Polylang integration.
			add_filter( 'pll_translation_url', array( $this, 'get_pll_wishlist_url' ), 10, 1 );
		}

		/* === PLUGIN FW LOADER === */

		/**
		 * Loads plugin fw, if not yet created
		 *
		 * @return void
		 * @since 2.0.0
		 */
		public function plugin_fw_loader() {
			if ( ! defined( 'YIT_CORE_PLUGIN' ) ) {
				global $plugin_fw_data;
				if ( ! empty( $plugin_fw_data ) ) {
					$plugin_fw_file = array_shift( $plugin_fw_data );
					require_once $plugin_fw_file;
				}
			}
		}

		/* === PRIVACY LOADER === */

		/**
		 * Loads privacy class
		 *
		 * @return void
		 * @since 2.0.0
		 */
		public function privacy_loader() {
			if ( class_exists( 'YITH_Privacy_Plugin_Abstract' ) ) {
				require_once YITH_WCWL_INC . 'class-yith-wcwl-privacy.php';
				new YITH_WCWL_Privacy();
			}
		}

		/* === ITEMS METHODS === */

		/**
		 * Add a product in the wishlist.
		 *
		 * @param array $atts Array of parameters; when not passed, params will be searched in $_REQUEST.
		 * @return void
		 * @throws YITH_WCWL_Exception When an error occurs with Add to Wishlist operation.
		 *
		 * @since 1.0.0
		 */
		public function add( $atts = array() ) {
			$defaults = array(
				'add_to_wishlist'     => 0,
				'wishlist_id'         => 0,
				'quantity'            => 1,
				'user_id'             => false,
				'dateadded'           => '',
				'wishlist_name'       => '',
				'wishlist_visibility' => 0,
			);

			$atts = empty( $atts ) && ! empty( $this->details ) ? $this->details : $atts;
			$atts = ! empty( $atts ) ? $atts : $_REQUEST; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$atts = wp_parse_args( $atts, $defaults );

			// filtering params.
			$prod_id     = apply_filters( 'yith_wcwl_adding_to_wishlist_prod_id', intval( $atts['add_to_wishlist'] ) );
			$wishlist_id = apply_filters( 'yith_wcwl_adding_to_wishlist_wishlist_id', $atts['wishlist_id'] );
			$quantity    = apply_filters( 'yith_wcwl_adding_to_wishlist_quantity', intval( $atts['quantity'] ) );
			$user_id     = apply_filters( 'yith_wcwl_adding_to_wishlist_user_id', intval( $atts['user_id'] ) );
			$dateadded   = apply_filters( 'yith_wcwl_adding_to_wishlist_dateadded', $atts['dateadded'] );

			do_action( 'yith_wcwl_adding_to_wishlist', $prod_id, $wishlist_id, $user_id );

			if ( ! $this->can_user_add_to_wishlist() ) {
				throw new YITH_WCWL_Exception( apply_filters( 'yith_wcwl_user_cannot_add_to_wishlist_message', __( 'The item cannot be added to this wishlist', 'yith-woocommerce-wishlist' ) ), 1 );
			}

			if ( ! $prod_id ) {
				throw new YITH_WCWL_Exception( __( 'An error occurred while adding the products to the wishlist.', 'yith-woocommerce-wishlist' ), 0 );
			}

			$wishlist = 'new' === $wishlist_id ? $this->add_wishlist( $atts ) : YITH_WCWL_Wishlist_Factory::get_wishlist( $wishlist_id, 'edit' );

			if ( ! $wishlist instanceof YITH_WCWL_Wishlist || ! $wishlist->current_user_can( 'add_to_wishlist' ) ) {
				throw new YITH_WCWL_Exception( __( 'An error occurred while adding the products to the wishlist.', 'yith-woocommerce-wishlist' ), 0 );
			}

			$this->last_operation_token = $wishlist->get_token();

			if ( $wishlist->has_product( $prod_id ) ) {
				throw new YITH_WCWL_Exception( apply_filters( 'yith_wcwl_product_already_in_wishlist_message', get_option( 'yith_wcwl_already_in_wishlist_text' ) ), 1 );
			}

			$item = new YITH_WCWL_Wishlist_Item();

			$item->set_product_id( $prod_id );
			$item->set_quantity( $quantity );
			$item->set_wishlist_id( $wishlist->get_id() );
			$item->set_user_id( $wishlist->get_user_id() );

			if ( $dateadded ) {
				$item->set_date_added( $dateadded );
			}

			$wishlist->add_item( $item );
			$wishlist->save();

			wp_cache_delete( 'wishlist-count-' . $wishlist->get_token(), 'wishlists' );

			$user_id = $wishlist->get_user_id();

			if ( $user_id ) {
				wp_cache_delete( 'wishlist-user-total-count-' . $user_id, 'wishlists' );
			}

			do_action( 'yith_wcwl_added_to_wishlist', $prod_id, $item->get_wishlist_id(), $item->get_user_id() );
		}

		/**
		 * Remove an entry from the wishlist.
		 *
		 * @param array $atts Array of parameters; when not passed, parameters will be retrieved from $_REQUEST.
		 *
		 * @return void
		 * @throws YITH_WCWL_Exception When something was wrong with removal.
		 * @since 1.0.0
		 */
		public function remove( $atts = array() ) {
			$defaults = array(
				'remove_from_wishlist' => 0,
				'wishlist_id'          => 0,
				'user_id'              => false,
			);

			$atts = empty( $atts ) && ! empty( $this->details ) ? $this->details : $atts;
			$atts = ! empty( $atts ) ? $atts : $_REQUEST; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$atts = wp_parse_args( $atts, $defaults );

			$prod_id     = intval( $atts['remove_from_wishlist'] );
			$wishlist_id = intval( $atts['wishlist_id'] );
			$user_id     = intval( $atts['user_id'] );

			do_action( 'yith_wcwl_removing_from_wishlist', $prod_id, $wishlist_id, $user_id );

			if ( ! $prod_id ) {
				throw new YITH_WCWL_Exception( apply_filters( 'yith_wcwl_unable_to_remove_product_message', __( 'Error. Unable to remove the product from the wishlist.', 'yith-woocommerce-wishlist' ) ), 0 );
			}

			$wishlist = apply_filters( 'yith_wcwl_get_wishlist_on_remove', YITH_WCWL_Wishlist_Factory::get_wishlist( $wishlist_id ), $atts );

			if ( apply_filters( 'yith_wcwl_allow_remove_after_add_to_cart', ! $wishlist instanceof YITH_WCWL_Wishlist || ! $wishlist->current_user_can( 'remove_from_wishlist' ), $wishlist ) ) {
				throw new YITH_WCWL_Exception( apply_filters( 'yith_wcwl_unable_to_remove_product_message', __( 'Error. Unable to remove the product from the wishlist.', 'yith-woocommerce-wishlist' ) ), 0 );
			}

			$wishlist->remove_product( $prod_id );
			$wishlist->save();

			wp_cache_delete( 'wishlist-count-' . $wishlist->get_token(), 'wishlists' );

			$user_id = $wishlist->get_user_id();

			if ( $user_id ) {
				wp_cache_delete( 'wishlist-user-total-count-' . $user_id );
			}

			do_action( 'yith_wcwl_removed_from_wishlist', $prod_id, $wishlist->get_id(), $wishlist->get_user_id() );
		}

		/**
		 * Check if the product exists in the wishlist.
		 *
		 * @param int      $product_id  Product id to check.
		 * @param int|bool $wishlist_id Wishlist where to search (use false to search in default wishlist).
		 * @return bool
		 * @since 1.0.0
		 */
		public function is_product_in_wishlist( $product_id, $wishlist_id = false ) {
			$wishlist = YITH_WCWL_Wishlist_Factory::get_wishlist( $wishlist_id );

			if ( ! $wishlist ) {
				return false;
			}

			return apply_filters( 'yith_wcwl_is_product_in_wishlist', $wishlist->has_product( $product_id ), $product_id, $wishlist_id );
		}

		/**
		 * Retrieve elements of the wishlist for a specific user
		 *
		 * @param array $args Arguments array; it may contains any of the following:<br/>
		 * [<br/>
		 *     'user_id'             // Owner of the wishlist; default to current user logged in (if any), or false for cookie wishlist<br/>
		 *     'product_id'          // Product to search in the wishlist<br/>
		 *     'wishlist_id'         // wishlist_id for a specific wishlist, false for default, or all for any wishlist<br/>
		 *     'wishlist_token'      // wishlist token, or false as default<br/>
		 *     'wishlist_visibility' // all, visible, public, shared, private<br/>
		 *     'is_default' =>       // whether searched wishlist should be default one <br/>
		 *     'id' => false,        // only for table select<br/>
		 *     'limit' => false,     // pagination param; number of items per page. 0 to get all items<br/>
		 *     'offset' => 0         // pagination param; offset for the current set. 0 to start from the first item<br/>
		 * ].
		 *
		 * @return YITH_WCWL_Wishlist_Item[]|bool
		 * @since 2.0.0
		 */
		public function get_products( $args = array() ) {
			return YITH_WCWL_Wishlist_Factory::get_wishlist_items( $args );
		}

		/**
		 * Retrieve the number of products in the wishlist.
		 *
		 * @param string|bool $wishlist_token Wishlist token if any; false for default wishlist.
		 *
		 * @return int
		 * @since 1.0.0
		 */
		public function count_products( $wishlist_token = false ) {
			$wishlist = YITH_WCWL_Wishlist_Factory::get_wishlist( $wishlist_token );

			if ( ! $wishlist ) {
				return 0;
			}

			$count = wp_cache_get( 'wishlist-count-' . $wishlist->get_token(), 'wishlists' );

			if ( false === $count ) {
				$count = $wishlist->count_items();
				wp_cache_set( 'wishlist-count-' . $wishlist->get_token(), $count, 'wishlists' );
			}

			return $count;
		}

		/**
		 * Count all user items in wishlists
		 *
		 * @return int Count of items added all over wishlist from current user
		 * @since 2.0.12
		 */
		public function count_all_products() {
			$args = array(
				'wishlist_id' => 'all',
			);

			if ( is_user_logged_in() ) {
				$id              = get_current_user_id();
				$args['user_id'] = $id;
			} elseif ( YITH_WCWL_Session()->has_session() ) {
				$id                 = YITH_WCWL_Session()->get_session_id();
				$args['session_id'] = $id;
			}

			if ( ! isset( $id ) ) {
				return 0;
			}

			$count = wp_cache_get( 'wishlist-user-total-count-' . $id, 'wishlists' );

			if ( false === $count ) {
				$count = YITH_WCWL_Wishlist_Factory::get_wishlist_items_count( $args );
				wp_cache_set( 'wishlist-user-total-count-' . $id, $count, 'wishlists' );
			}

			return $count;
		}

		/**
		 * Count number of times a product was added to users wishlists
		 *
		 * @param int|bool $product_id Product id; false will force method to use global product.
		 *
		 * @return int Number of times the product was added to wishlist
		 * @since 2.0.13
		 */
		public function count_add_to_wishlist( $product_id = false ) {
			global $product;

			$product_id = ! ( $product_id ) ? yit_get_product_id( $product ) : $product_id;

			if ( ! $product_id ) {
				return 0;
			}

			$count = YITH_WCWL_Wishlist_Factory::get_times_added_count( $product_id );

			return $count;
		}

		/**
		 * Count product occurrences in users wishlists
		 *
		 * @param int|bool $product_id Product id; false will force method to use global product.
		 *
		 * @return int
		 * @since 2.0.0
		 */
		public function count_product_occurrences( $product_id = false ) {
			global $product;

			$product_id = ! ( $product_id ) ? yit_get_product_id( $product ) : $product_id;

			if ( ! $product_id ) {
				return 0;
			}

			$count = YITH_WCWL_Wishlist_Factory::get_wishlist_items_count(
				array(
					'product_id'  => $product_id,
					'user_id'     => false,
					'session_id'  => false,
					'wishlist_id' => 'all',
				)
			);

			return $count;
		}

		/**
		 * Retrieve details of a product in the wishlist.
		 *
		 * @param int      $product_id  Product id.
		 * @param int|bool $wishlist_id Wishlist id, or false when default should be applied.
		 * @return YITH_WCWL_Wishlist_Item|bool
		 * @since 1.0.0
		 */
		public function get_product_details( $product_id, $wishlist_id = false ) {
			$product = $this->get_products(
				array(
					'prod_id'     => $product_id,
					'wishlist_id' => $wishlist_id,
				)
			);

			if ( empty( $product ) ) {
				return false;
			}

			return array_shift( $product );
		}

		/* === WISHLISTS METHODS === */

		/**
		 * Add a new wishlist for the user.
		 *
		 * @param array $atts Array of params for wishlist creation.
		 * @return int Id of the wishlist created
		 * @since 2.0.0
		 */
		public function add_wishlist( $atts = array() ) {
			$defaults = array(
				'user_id' => false,
			);

			$atts = empty( $atts ) && ! empty( $this->details ) ? $this->details : $atts;
			$atts = ! empty( $atts ) ? $atts : $_REQUEST; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$atts = wp_parse_args( $atts, $defaults );

			$user_id = ( ! empty( $atts['user_id'] ) ) ? $atts['user_id'] : false;

			return $this->generate_default_wishlist( $user_id );
		}

		/**
		 * Update wishlist with arguments passed as second parameter
		 *
		 * @param int   $wishlist_id Wishlist id.
		 * @param array $args Array of parameters to use in update process.
		 * @return void
		 * @since 2.0.0
		 */
		public function update_wishlist( $wishlist_id, $args = array() ) {}

		/**
		 * Delete indicated wishlist
		 *
		 * @param int $wishlist_id Wishlist id.
		 * @return void
		 * @since 2.0.0
		 */
		public function remove_wishlist( $wishlist_id ) {}

		/**
		 * Retrieve all the wishlist matching specified arguments
		 *
		 * @param array $args Array of valid arguments<br/>
		 * [<br/>
		 *     'id'                  // Wishlist id to search, if any<br/>
		 *     'user_id'             // User owner<br/>
		 *     'wishlist_slug'       // Slug of the wishlist to search<br/>
		 *     'wishlist_name'       // Name of the wishlist to search<br/>
		 *     'wishlist_token'      // Token of the wishlist to search<br/>
		 *     'wishlist_visibility' // Wishlist visibility: all, visible, public, shared, private<br/>
		 *     'user_search'         // String to match against first name / last name or email of the wishlist owner<br/>
		 *     'is_default'          // Whether wishlist should be default or not<br/>
		 *     'orderby'             // Column used to sort final result (could be any wishlist lists column)<br/>
		 *     'order'               // Sorting order<br/>
		 *     'limit'               // Pagination param: maximum number of elements in the set. 0 to retrieve all elements<br/>
		 *     'offset'              // Pagination param: offset for the current set. 0 to start from the first item<br/>
		 *     'show_empty'          // Whether to show empty lists os not<br/>
		 * ].
		 *
		 * @return YITH_WCWL_Wishlist[]
		 * @since 2.0.0
		 */
		public function get_wishlists( $args = array() ) {
			return YITH_WCWL_Wishlist_Factory::get_wishlists( $args );
		}

		/**
		 * Wrapper for \YITH_WCWL::get_wishlists, will return wishlists for current user
		 *
		 * @return YITH_WCWL_Wishlist[]
		 * @since 2.0.0
		 */
		public function get_current_user_wishlists() {
			$id = is_user_logged_in() ? get_current_user_id() : YITH_WCWL_Session()->maybe_get_session_id();

			if ( ! $id ) {
				return array();
			}

			$lists = wp_cache_get( 'user-wishlists-' . $id, 'wishlists' );

			if ( ! $lists ) {
				$lists = YITH_WCWL_Wishlist_Factory::get_wishlists(
					array(
						'orderby' => 'dateadded',
						'order'   => 'ASC',
					)
				);

				wp_cache_set( 'user-wishlists-' . $id, $lists, 'wishlists' );
			}

			return $lists;
		}

		/**
		 * Returns details of a wishlist, searching it by wishlist id
		 *
		 * @param int $wishlist_id Wishlist id.
		 * @return YITH_WCWL_Wishlist
		 * @since 2.0.0
		 */
		public function get_wishlist_detail( $wishlist_id ) {
			return YITH_WCWL_Wishlist_Factory::get_wishlist( $wishlist_id );
		}

		/**
		 * Returns details of a wishlist, searching it by wishlist token
		 *
		 * @param string $wishlist_token Wishlist token.
		 * @return YITH_WCWL_Wishlist
		 * @since 2.0.0
		 */
		public function get_wishlist_detail_by_token( $wishlist_token ) {
			return YITH_WCWL_Wishlist_Factory::get_wishlist( $wishlist_token );
		}

		/**
		 * Generate default wishlist for current user or session
		 *
		 * @param int|bool $id User or session id; false if you want to use current user/session.
		 *
		 * @return int Default wishlist id
		 * @since 2.0.0
		 */
		public function generate_default_wishlist( $id = false ) {
			$wishlist = YITH_WCWL_Wishlist_Factory::generate_default_wishlist( $id );

			if ( $wishlist ) {
				return $wishlist->get_id();
			}

			return false;
		}

		/**
		 * Generate a token to visit wishlist
		 *
		 * @return string token
		 * @since 2.0.0
		 */
		public function generate_wishlist_token() {
			return YITH_WCWL_Wishlist_Factory::generate_wishlist_token();
		}

		/**
		 * Returns an array of users that created and populated a public wishlist
		 *
		 * @param array $args Array of valid arguments<br/>
		 * [<br/>
		 *     'search' // String to match against first name / last name / user login or user email of wishlist owner<br/>
		 *     'limit'  // Pagination param: number of items to show in one page. 0 to show all items<br/>
		 *     'offset' // Pagination param: offset for the current set. 0 to start from the first item<br/>
		 * ].
		 * @return array
		 * @since 2.0.0
		 */
		public function get_users_with_wishlist( $args = array() ) {
			return YITH_WCWL_Wishlist_Factory::get_wishlist_users( $args );
		}

		/**
		 * Count users that have public wishlists
		 *
		 * @param string $search Search string.
		 * @return int
		 * @since 2.0.0
		 */
		public function count_users_with_wishlists( $search ) {
			return count( $this->get_users_with_wishlist( array( 'search' => $search ) ) );
		}

		/* === GENERAL METHODS === */

		/**
		 * Checks whether current user can add to the wishlist
		 *
		 * TODO: merge this into \YITH_WCWL_Wishlist::current_user_can
		 *
		 * @param int|bool $user_id User id to test; false to use current user id.
		 * @return bool Whether current user can add to wishlist
		 * @since 3.0.0
		 */
		public function can_user_add_to_wishlist( $user_id = false ) {
			$user_id                                    = $user_id ? $user_id : get_current_user_id();
			$disable_wishlist_for_unauthenticated_users = get_option( 'yith_wcwl_disable_wishlist_for_unauthenticated_users' );
			$return                                     = true;

			if ( 'yes' === $disable_wishlist_for_unauthenticated_users && ! $user_id ) {
				$return = false;
			}

			return apply_filters( 'yith_wcwl_can_user_add_to_wishlist', $return, $user_id );
		}

		/**
		 * Register custom plugin Data Stores classes
		 *
		 * @param array $data_stores Array of registered data stores.
		 * @return array Array of filtered data store
		 */
		public function register_data_stores( $data_stores ) {
			$data_stores['wishlist']      = 'YITH_WCWL_Wishlist_Data_Store';
			$data_stores['wishlist-item'] = 'YITH_WCWL_Wishlist_Item_Data_Store';

			return $data_stores;
		}

		/**
		 * Add rewrite rules for wishlist
		 *
		 * @return void
		 * @since 2.0.0
		 */
		public function add_rewrite_rules() {
			global $wp_query;

			// filter wishlist param.
			$this->wishlist_param = apply_filters( 'yith_wcwl_wishlist_param', $this->wishlist_param );

			$wishlist_page_id = get_option( 'yith_wcwl_wishlist_page_id' );
			$wishlist_page_id = yith_wcwl_object_id( $wishlist_page_id, 'page', true, 'default' );

			if ( empty( $wishlist_page_id ) ) {
				return;
			}

			$wishlist_page      = get_post( $wishlist_page_id );
			$wishlist_page_slug = $wishlist_page ? $wishlist_page->post_name : false;

			if ( empty( $wishlist_page_slug ) ) {
				return;
			}

			if ( defined( 'POLYLANG_VERSION' ) || defined( 'ICL_PLUGIN_PATH' ) ) {
				return;
			}

			$regex_paged  = '(([^/]+/)*' . urldecode( $wishlist_page_slug ) . ')(/(.*))?/page/([0-9]{1,})/?$';
			$regex_simple = '(([^/]+/)*' . urldecode( $wishlist_page_slug ) . ')(/(.*))?/?$';

			add_rewrite_rule( $regex_paged, 'index.php?pagename=$matches[1]&' . $this->wishlist_param . '=$matches[4]&paged=$matches[5]', 'top' );
			add_rewrite_rule( $regex_simple, 'index.php?pagename=$matches[1]&' . $this->wishlist_param . '=$matches[4]', 'top' );

			$rewrite_rules = get_option( 'rewrite_rules' );

			if ( ! is_array( $rewrite_rules ) || ! array_key_exists( $regex_paged, $rewrite_rules ) || ! array_key_exists( $regex_simple, $rewrite_rules ) ) {
				flush_rewrite_rules();
			}
		}

		/**
		 * Adds public query var for wishlist
		 *
		 * @param array $public_var Array of available query vars.
		 * @return array
		 * @since 2.0.0
		 */
		public function add_public_query_var( $public_var ) {
			$public_var[] = $this->wishlist_param;
			$public_var[] = 'wishlist_id';

			return $public_var;
		}

		/**
		 * Return wishlist page id, if any
		 *
		 * @return int Wishlist page id.
		 */
		public function get_wishlist_page_id() {
			$wishlist_page_id = get_option( 'yith_wcwl_wishlist_page_id' );
			$wishlist_page_id = yith_wcwl_object_id( $wishlist_page_id );

			return (int) apply_filters( 'yith_wcwl_wishlist_page_id', $wishlist_page_id );
		}

		/**
		 * Build wishlist page URL.
		 *
		 * @param string $action Action string to use in the url.
		 *
		 * @return string
		 * @since 1.0.0
		 */
		public function get_wishlist_url( $action = '' ) {
			global $sitepress;
			$wishlist_page_id   = $this->get_wishlist_page_id();
			$wishlist_permalink = get_the_permalink( $wishlist_page_id );

			$action_params = explode( '/', $action );
			$view          = $action_params[0];
			$data          = isset( $action_params[1] ) ? $action_params[1] : '';

			if ( 'view' === $action && empty( $data ) ) {
				return $wishlist_permalink;
			}

			if ( get_option( 'permalink_structure' ) && ! defined( 'ICL_PLUGIN_PATH' ) && ! defined( 'POLYLANG_VERSION' ) ) {
				$wishlist_permalink = trailingslashit( $wishlist_permalink );
				$base_url           = trailingslashit( $wishlist_permalink . $action );
			} else {
				$base_url = $wishlist_permalink;
				$params   = array();

				if ( ! empty( $data ) ) {
					$params[ $this->wishlist_param ] = $view;

					if ( 'view' === $view ) {
						$params['wishlist_id'] = $data;
					} elseif ( 'user' === $view ) {
						$params['user_id'] = $data;
					}
				} else {
					$params[ $this->wishlist_param ] = $view;
				}

				$base_url = add_query_arg( $params, $base_url );
			}

			if ( defined( 'ICL_PLUGIN_PATH' ) && $sitepress->get_current_language() !== $sitepress->get_default_language() ) {
				$base_url = add_query_arg( 'lang', $sitepress->get_current_language(), $base_url );
			}

			return apply_filters( 'yith_wcwl_wishlist_page_url', esc_url_raw( $base_url ), $action );
		}

		/**
		 * Retrieve url for the wishlist that was affected by last operation
		 *
		 * @return string Url to view last operation wishlist
		 */
		public function get_last_operation_url() {
			$action = 'view';

			if ( ! empty( $this->last_operation_token ) ) {
				$action .= "/{$this->last_operation_token}";
			}

			return $this->get_wishlist_url( $action );
		}

		/**
		 * Generates Add to Wishlist url, to use when customer do not have js enabled
		 *
		 * @param int   $product_id Product id to add to wishlist.
		 * @param array $args       Any of the following parameters
		 * [
		 *     'base_url' => ''
		 *     'wishlist_id' => 0,
		 *     'quantity' => 1,
		 *     'user_id' => false,
		 *     'dateadded' => '',
		 *     'wishlist_name' => '',
		 *     'wishlist_visibility' => 0
		 * ].
		 * @return string Add to wishlist url
		 */
		public function get_add_to_wishlist_url( $product_id, $args = array() ) {
			$args = array_merge(
				array(
					'add_to_wishlist' => $product_id,
				),
				$args
			);

			if ( isset( $args['base_url'] ) ) {
				$base_url = $args['base_url'];
				unset( $args['base_url'] );

				$url = add_query_arg( $args, $base_url );
			} else {
				$url = add_query_arg( $args );
			}

			return apply_filters( 'yith_wcwl_add_to_wishlist_url', esc_url_raw( wp_nonce_url( $url, 'add_to_wishlist' ) ), $product_id, $args );
		}

		/**
		 * Build the URL used to remove an item from the wishlist.
		 *
		 * @param int $item_id Id of the item to remove.
		 * @return string
		 * @since 1.0.0
		 */
		public function get_remove_url( $item_id ) {
			return esc_url( wp_nonce_url( add_query_arg( 'remove_from_wishlist', $item_id ), 'remove_from_wishlist' ) );
		}

		/**
		 * Returns available views for wishlist page
		 *
		 * @return string[]
		 * @since 3.0.0
		 */
		public function get_available_views() {
			$available_views = apply_filters( 'yith_wcwl_available_wishlist_views', array( 'view', 'user' ) );
			return $available_views;
		}

		/**
		 * Checks whether multi-wishlist feature is enabled for current user
		 *
		 * @return bool Whether feature is enabled or not
		 */
		public function is_multi_wishlist_enabled() {
			return false;
		}

		/* === POLYLANG INTEGRATION === */

		/**
		 * Filters translation url for the wishlist page, when PolyLang is enabled
		 *
		 * @param  string $url Translation url.
		 * @return string Filtered translation url for current page/post.
		 */
		public function get_pll_wishlist_url( $url ) {
			// phpcs:disable WordPress.Security.NonceVerification.Recommended
			if ( yith_wcwl_is_wishlist_page() && isset( $_GET[ $this->wishlist_param ] ) ) {
				$wishlist_action = sanitize_text_field( wp_unslash( $_GET[ $this->wishlist_param ] ) );
				$user_id         = isset( $_GET['user_id'] ) ? sanitize_text_field( wp_unslash( $_GET['user_id'] ) ) : '';
				$wishlist_id     = isset( $_GET['wishlist_id'] ) ? sanitize_text_field( wp_unslash( $_GET['wishlist_id'] ) ) : '';

				$params = array_filter(
					array(
						$this->wishlist_param => $wishlist_action,
						'user_id'             => $user_id,
						'wishlist_id'         => $wishlist_id,
					)
				);

				$url = add_query_arg( $params, $url );
			}
			// phpcs:enable WordPress.Security.NonceVerification.Recommended

			return $url;
		}
	}
}

/**
 * Unique access to instance of YITH_WCWL class
 *
 * @return \YITH_WCWL|\YITH_WCWL_Premium
 * @since 2.0.0
 */
function YITH_WCWL() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid
	return defined( 'YITH_WCWL_PREMIUM' ) ? YITH_WCWL_Premium::get_instance() : YITH_WCWL::get_instance();
}
class-yith-wcwl-frontend.php000064400000132526151327635660012153 0ustar00<?php
/**
 * Init class
 *
 * @author YITH
 * @package YITH\Wishlist\Classes
 * @version 3.0.0
 */

if ( ! defined( 'YITH_WCWL' ) ) {
	exit;
} // Exit if accessed directly

if ( ! class_exists( 'YITH_WCWL_Frontend' ) ) {
	/**
	 * Initiator class. Install the plugin database and load all needed stuffs.
	 *
	 * @since 1.0.0
	 */
	class YITH_WCWL_Frontend {

		/**
		 * Single instance of the class
		 *
		 * @var \YITH_WCWL_Frontend
		 * @since 2.0.0
		 */
		protected static $instance;

		/**
		 * Plugin version
		 *
		 * @var string
		 * @since 1.0.0
		 */
		public $version = '3.3.0';

		/**
		 * Plugin database version
		 *
		 * @var string
		 * @since 1.0.0
		 */
		public $db_version = '3.0.0';

		/**
		 * Store class yith_WCWL_Install.
		 *
		 * @var object
		 * @access private
		 * @since 1.0.0
		 */
		protected $yith_wcwl_install;

		/**
		 * Returns single instance of the class
		 *
		 * @return \YITH_WCWL_Frontend
		 * @since 2.0.0
		 */
		public static function get_instance() {
			if ( is_null( self::$instance ) ) {
				self::$instance = new self();
			}

			return self::$instance;
		}

		/**
		 * Constructor
		 *
		 * @since 1.0.0
		 */
		public function __construct() {
			define( 'YITH_WCWL_VERSION', $this->version );
			define( 'YITH_WCWL_DB_VERSION', $this->db_version );

			$this->yith_wcwl_install = YITH_WCWL_Install();

			// add filter for font-awesome compatibility.
			add_filter( 'option_yith_wcwl_add_to_wishlist_icon', array( $this, 'update_font_awesome_classes' ) );
			add_filter( 'option_yith_wcwl_add_to_cart_icon', array( $this, 'update_font_awesome_classes' ) );

			// init class.
			add_action( 'init', array( $this, 'init' ), 0 );

			// templates.
			add_action( 'init', array( $this, 'add_button' ) );
			add_filter( 'body_class', array( $this, 'add_body_class' ) );
			add_action( 'template_redirect', array( $this, 'add_nocache_headers' ) );
			add_action( 'wp_head', array( $this, 'add_noindex_header' ) );
			add_filter( 'wp_robots', array( $this, 'add_noindex_robots' ) );
			add_action( 'yith_wcwl_before_wishlist_title', array( $this, 'print_notices' ) );
			add_action( 'yith_wcwl_wishlist_before_wishlist_content', array( $this, 'wishlist_header' ), 10, 1 );
			add_action( 'yith_wcwl_wishlist_main_wishlist_content', array( $this, 'main_wishlist_content' ), 10, 1 );
			add_action( 'yith_wcwl_wishlist_after_wishlist_content', array( $this, 'wishlist_footer' ), 10, 1 );

			// template modifications.
			add_filter( 'woocommerce_post_class', array( $this, 'add_products_class_on_loop' ) );

			// scripts.
			add_action( 'wp_head', array( $this, 'detect_javascript' ), 0 );
			add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_styles_and_stuffs' ) );

			// add YITH WooCommerce Frequently Bought Together Premium shortcode.
			add_action( 'yith_wcwl_after_wishlist_form', array( $this, 'yith_wcfbt_shortcode' ), 10, 1 );
			add_filter( 'woocommerce_add_to_cart_redirect', array( $this, 'yith_wfbt_redirect_after_add_to_cart' ), 10, 1 );

			// YITH WCWL Loaded.
			do_action( 'yith_wcwl_loaded' );
		}

		/**
		 * Initiator method.
		 *
		 * @return void
		 * @since 1.0.0
		 */
		public function init() {
			// update cookie from old version to new one.
			$this->update_cookies();
			$this->destroy_serialized_cookies();
			$this->convert_cookies_to_session();

			// register assets.
			$this->register_styles();
			$this->register_scripts();
		}

		/* === ADD TO WISHLIST */

		/**
		 * Add the "Add to Wishlist" button. Needed to use in wp_head hook.
		 *
		 * @return void
		 * @since 1.0.0
		 */
		public function add_button() {
			$positions = apply_filters(
				'yith_wcwl_positions',
				array(
					'after_add_to_cart' => array(
						'hook'     => 'woocommerce_single_product_summary',
						'priority' => 31,
					),
					'add-to-cart'       => array(
						'hook'     => 'woocommerce_single_product_summary',
						'priority' => 31,
					),
					'thumbnails'        => array(
						'hook'     => 'woocommerce_product_thumbnails',
						'priority' => 21,
					),
					'summary'           => array(
						'hook'     => 'woocommerce_after_single_product_summary',
						'priority' => 11,
					),
				)
			);

			// Add the link "Add to wishlist".
			$position = get_option( 'yith_wcwl_button_position', 'add-to-cart' );

			if ( 'shortcode' !== $position && isset( $positions[ $position ] ) ) {
				add_action( $positions[ $position ]['hook'], array( $this, 'print_button' ), $positions[ $position ]['priority'] );
			}

			// check if Add to wishlist button is enabled for loop.
			$enabled_on_loop = 'yes' === get_option( 'yith_wcwl_show_on_loop', 'no' );

			if ( ! $enabled_on_loop ) {
				return;
			}

			$positions = apply_filters(
				'yith_wcwl_loop_positions',
				array(
					'before_image'       => array(
						'hook'     => 'woocommerce_before_shop_loop_item',
						'priority' => 5,
					),
					'before_add_to_cart' => array(
						'hook'     => 'woocommerce_after_shop_loop_item',
						'priority' => 7,
					),
					'after_add_to_cart'  => array(
						'hook'     => 'woocommerce_after_shop_loop_item',
						'priority' => 15,
					),
				)
			);

			// Add the link "Add to wishlist".
			$position = get_option( 'yith_wcwl_loop_position', 'after_add_to_cart' );

			if ( 'shortcode' !== $position && isset( $positions[ $position ] ) ) {
				add_action( $positions[ $position ]['hook'], array( $this, 'print_button' ), $positions[ $position ]['priority'] );
			}
		}

		/**
		 * Print "Add to Wishlist" shortcode
		 *
		 * @return void
		 * @since 2.2.2
		 */
		public function print_button() {
			/**
			 * Developers can use this filter to remove ATW button selectively from specific pages or products
			 * You can use global $product or $post to execute checks
			 *
			 * @since 3.0.7
			 */
			if ( ! apply_filters( 'yith_wcwl_show_add_to_wishlist', true ) ) {
				return;
			}

			echo do_shortcode( '[yith_wcwl_add_to_wishlist]' );
		}

		/* === WISHLIST PAGE === */

		/**
		 * Prints wc notice for wishlist pages
		 *
		 * @return void
		 * @since 2.0.5
		 */
		public function print_notices() {
			if ( function_exists( 'wc_print_notices' ) ) {
				wc_print_notices();
			}
		}

		/**
		 * Add specific body class when the Wishlist page is opened
		 *
		 * @param array $classes Existing boy classes.
		 *
		 * @return array
		 * @since 1.0.0
		 */
		public function add_body_class( $classes ) {
			$wishlist_page_id = YITH_WCWL()->get_wishlist_page_id();

			if ( ! empty( $wishlist_page_id ) && is_page( $wishlist_page_id ) ) {
				$classes[] = 'woocommerce-wishlist';
				$classes[] = 'woocommerce';
				$classes[] = 'woocommerce-page';
			}

			return $classes;
		}

		/**
		 * Send nocache headers on wishlist page
		 *
		 * @return void
		 * @since 3.0.0
		 */
		public function add_nocache_headers() {
			if ( ! headers_sent() && yith_wcwl_is_wishlist_page() ) {
				wc_nocache_headers();
			}
		}

		/**
		 * Send noindex header on Add To Wishlist url (?add_to_wishlist=12345)
		 * Deprecated since version 5.7 of WordPress.
		 *
		 * @return void
		 * @since 3.0.20
		 */
		public function add_noindex_header() {
			if ( function_exists( 'wp_robots_no_robots' ) || ! isset( $_GET['add_to_wishlist'] ) || apply_filters( 'yith_wcwl_skip_noindex_headers', false ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
				return;
			}

			wp_no_robots();
		}

		/**
		 * Disable search engines indexing for Add to Wishlist url.
		 * Uses "wp_robots" filter introduced in WP 5.7.
		 *
		 * @since 3.0.20
		 * @param array $robots Associative array of robots directives.
		 * @return array Filtered robots directives.
		 */
		public function add_noindex_robots( $robots ) {
			if ( ! isset( $_GET['add_to_wishlist'] ) || apply_filters( 'yith_wcwl_skip_noindex_headers', false ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
				return $robots;
			}

			return wp_robots_no_robots( $robots );
		}

		/* === SCRIPTS AND ASSETS === */

		/**
		 * Register styles required by the plugin
		 *
		 * @return void
		 */
		public function register_styles() {
			$woocommerce_base = WC()->template_path();
			$assets_path      = str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/';

			// register dependencies.
			wp_register_style( 'jquery-selectBox', YITH_WCWL_URL . 'assets/css/jquery.selectBox.css', array(), '1.2.0' );
			wp_register_style( 'yith-wcwl-font-awesome', YITH_WCWL_URL . 'assets/css/font-awesome.css', array(), '4.7.0' );
			wp_register_style( 'woocommerce_prettyPhoto_css', $assets_path . 'css/prettyPhoto.css', array(), '3.1.6' );

			$deps = apply_filters( 'yith_wcwl_main_style_deps', array( 'jquery-selectBox', 'yith-wcwl-font-awesome', 'woocommerce_prettyPhoto_css' ) );

			// register main style.
			$located = locate_template(
				array(
					$woocommerce_base . 'wishlist.css',
					'wishlist.css',
				)
			);

			if ( ! $located ) {
				wp_register_style( 'yith-wcwl-main', YITH_WCWL_URL . 'assets/css/style.css', $deps, $this->version );
			} else {
				$stylesheet_directory     = get_stylesheet_directory();
				$stylesheet_directory_uri = get_stylesheet_directory_uri();
				$template_directory       = get_template_directory();
				$template_directory_uri   = get_template_directory_uri();

				$style_url = ( strpos( $located, $stylesheet_directory ) !== false ) ? str_replace( $stylesheet_directory, $stylesheet_directory_uri, $located ) : str_replace( $template_directory, $template_directory_uri, $located );

				wp_register_style( 'yith-wcwl-user-main', $style_url, $deps, $this->version );
			}

			// theme specific assets.
			$current_theme = wp_get_theme();

			if ( $current_theme->exists() ) {
				$theme_slug = $current_theme->Template; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase

				if ( file_exists( YITH_WCWL_DIR . 'assets/css/themes/' . $theme_slug . '.css' ) ) {
					wp_register_style( 'yith-wcwl-theme', YITH_WCWL_URL . 'assets/css/themes/' . $theme_slug . '.css', array( $located ? 'yith-wcwl-user-main' : 'yith-wcwl-main' ), $this->version );
				}
			}
		}

		/**
		 * Register scripts required by the plugin
		 *
		 * @return void
		 */
		public function register_scripts() {
			$woocommerce_base = WC()->template_path();
			$assets_path      = str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/';
			$suffix           = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
			$prefix           = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? 'unminified/' : '';

			// register dependencies.
			wp_register_script( 'prettyPhoto', $assets_path . 'js/prettyPhoto/jquery.prettyPhoto' . $suffix . '.js', array( 'jquery' ), '3.1.6', true );
			wp_register_script( 'jquery-selectBox', YITH_WCWL_URL . 'assets/js/jquery.selectBox.min.js', array( 'jquery' ), '1.2.0', true );

			$deps = apply_filters( 'yith_wcwl_main_script_deps', array( 'jquery', 'jquery-selectBox', 'prettyPhoto' ) );

			// get localized variables.
			$yith_wcwl_l10n = $this->get_localize();

			// register main script.
			$located = locate_template(
				array(
					$woocommerce_base . 'wishlist.js',
					'wishlist.js',
				)
			);

			if ( ! $located ) {
				wp_register_script( 'jquery-yith-wcwl', YITH_WCWL_URL . 'assets/js/' . $prefix . 'jquery.yith-wcwl' . $suffix . '.js', $deps, $this->version, true );
				wp_localize_script( 'jquery-yith-wcwl', 'yith_wcwl_l10n', $yith_wcwl_l10n );
			} else {
				wp_register_script( 'jquery-yith-wcwl-user', str_replace( get_stylesheet_directory(), get_stylesheet_directory_uri(), $located ), $deps, $this->version, true );
				wp_localize_script( 'jquery-yith-wcwl-user', 'yith_wcwl_l10n', $yith_wcwl_l10n );
			}
		}

		/**
		 * Enqueue styles, scripts and other stuffs needed in the <head>.
		 *
		 * @return void
		 * @since 1.0.0
		 */
		public function enqueue_styles_and_stuffs() {
			// main plugin style.
			if ( ! wp_style_is( 'yith-wcwl-user-main', 'registered' ) ) {
				wp_enqueue_style( 'yith-wcwl-main' );
			} else {
				wp_enqueue_style( 'yith-wcwl-user-main' );
			}

			// theme specific style.
			if ( wp_style_is( 'yith-wcwl-theme', 'registered' ) ) {
				wp_enqueue_style( 'yith-wcwl-theme' );
			}

			// custom style.
			$this->enqueue_custom_style();
		}

		/**
		 * Enqueue style dynamically generated by the plugin
		 *
		 * @return void
		 */
		public function enqueue_custom_style() {
			$custom_css = $this->build_custom_css();

			if ( $custom_css ) {
				$handle = wp_script_is( 'yith-wcwl-user-main' ) ? 'yith-wcwl-user-main' : 'yith-wcwl-main';

				wp_add_inline_style( $handle, $custom_css );
			}
		}

		/**
		 * Enqueue plugin scripts.
		 *
		 * @return void
		 * @since 1.0.0
		 */
		public function enqueue_scripts() {
			if ( ! wp_script_is( 'jquery-yith-wcwl-user', 'registered' ) ) {
				wp_enqueue_script( 'jquery-yith-wcwl' );
			} else {
				wp_enqueue_script( 'jquery-yith-wcwl-user' );
			}
		}

		/**
		 * Return localize array
		 *
		 * @return array Array with variables to be localized inside js
		 * @since 2.2.3
		 */
		public function get_localize() {
			return apply_filters(
				'yith_wcwl_localize_script',
				array(
					'ajax_url'                  => admin_url( 'admin-ajax.php', 'relative' ),
					'redirect_to_cart'          => get_option( 'yith_wcwl_redirect_cart' ),
					'multi_wishlist'            => false,
					'hide_add_button'           => apply_filters( 'yith_wcwl_hide_add_button', true ),
					'enable_ajax_loading'       => 'yes' === get_option( 'yith_wcwl_ajax_enable', 'no' ),
					'ajax_loader_url'           => YITH_WCWL_URL . 'assets/images/ajax-loader-alt.svg',
					'remove_from_wishlist_after_add_to_cart' => 'yes' === get_option( 'yith_wcwl_remove_after_add_to_cart' ),
					'is_wishlist_responsive'    => apply_filters( 'yith_wcwl_is_wishlist_responsive', true ),
					'time_to_close_prettyphoto' => apply_filters( 'yith_wcwl_time_to_close_prettyphoto', 3000 ),
					'fragments_index_glue'      => apply_filters( 'yith_wcwl_fragments_index_glue', '.' ),
					'reload_on_found_variation' => apply_filters( 'yith_wcwl_reload_on_found_variation', true ),
					'mobile_media_query'        => apply_filters( 'yith_wcwl_mobile_media_query', 768 ),
					'labels'                    => array(
						'cookie_disabled'       => __( 'We are sorry, but this feature is available only if cookies on your browser are enabled.', 'yith-woocommerce-wishlist' ),
						'added_to_cart_message' => sprintf( '<div class="woocommerce-notices-wrapper"><div class="woocommerce-message" role="alert">%s</div></div>', apply_filters( 'yith_wcwl_added_to_cart_message', __( 'Product added to cart successfully', 'yith-woocommerce-wishlist' ) ) ),
					),
					'actions'                   => array(
						'add_to_wishlist_action'      => 'add_to_wishlist',
						'remove_from_wishlist_action' => 'remove_from_wishlist',
						'reload_wishlist_and_adding_elem_action' => 'reload_wishlist_and_adding_elem',
						'load_mobile_action'          => 'load_mobile',
						'delete_item_action'          => 'delete_item',
						'save_title_action'           => 'save_title',
						'save_privacy_action'         => 'save_privacy',
						'load_fragments'              => 'load_fragments',
					),
					'nonce'                     => array(
						'add_to_wishlist_nonce'      => wp_create_nonce( 'add_to_wishlist' ),
						'remove_from_wishlist_nonce' => wp_create_nonce( 'remove_from_wishlist' ),
						'reload_wishlist_and_adding_elem_nonce' => wp_create_nonce( 'reload_wishlist_and_adding_elem' ),
						'load_mobile_nonce'          => wp_create_nonce( 'load_mobile' ),
						'delete_item_nonce'          => wp_create_nonce( 'delete_item' ),
						'save_title_nonce'           => wp_create_nonce( 'save_title' ),
						'save_privacy_nonce'         => wp_create_nonce( 'save_privacy' ),
						'load_fragments_nonce'       => wp_create_nonce( 'load_fragments' ),
					),
				)
			);
		}

		/**
		 * Remove the class no-js when javascript is activated
		 *
		 * We add the action at the start of head, to do this operation immediatly, without gap of all libraries loading
		 *
		 * @return void
		 * @since 1.0.0
		 */
		public function detect_javascript() {
			if ( ! defined( 'YIT' ) ) :
				?>
				<script>document.documentElement.className = document.documentElement.className + ' yes-js js_active js'</script>
				<?php
			endif;
		}

		/* === TEMPLATES === */

		/**
		 * Include main wishlist template
		 *
		 * @param array $var Array of variables to pass to the template.
		 *
		 * @var $var array Array of parameters for current view
		 * @return void
		 */
		public function main_wishlist_content( $var ) {
			$template = isset( $var['template_part'] ) ? $var['template_part'] : 'view';
			$layout   = ! empty( $var['layout'] ) ? $var['layout'] : '';

			yith_wcwl_get_template_part( $template, '', $layout, $var );
		}

		/**
		 * Include wishlist header template
		 *
		 * @param array $var Array of variables to pass to the template.
		 *
		 * @var $var array Array of parameters for current view
		 * @return void
		 */
		public function wishlist_header( $var ) {
			$template = isset( $var['template_part'] ) ? $var['template_part'] : 'view';
			$layout   = ! empty( $var['layout'] ) ? $var['layout'] : '';

			yith_wcwl_get_template_part( $template, 'header', $layout, $var );
		}

		/**
		 * Include wishlist footer template
		 *
		 * @param array $var Array of variables to pass to the template.
		 *
		 * @var $var array Array of parameters for current view
		 * @return void
		 */
		public function wishlist_footer( $var ) {
			$template = isset( $var['template_part'] ) ? $var['template_part'] : 'view';
			$layout   = ! empty( $var['layout'] ) ? $var['layout'] : '';

			yith_wcwl_get_template_part( $template, 'footer', $layout, $var );
		}

		/* === TEMPLATE MODIFICATIONS === */

		/**
		 * Add class to products when Add to Wishlist is shown on loop
		 *
		 * @param array $classes Array of available classes for the product.
		 * @return array Array of filtered classes for the product
		 * @since 3.0.0
		 */
		public function add_products_class_on_loop( $classes ) {
			if ( yith_wcwl_is_single() ) {
				return $classes;
			}

			$enabled_on_loop = 'yes' === get_option( 'yith_wcwl_show_on_loop', 'no' );

			if ( ! $enabled_on_loop ) {
				return $classes;
			}

			$position = get_option( 'yith_wcwl_loop_position', 'after_add_to_cart' );

			if ( 'shortcode' === $position ) {
				return $classes;
			}

			$classes[] = "add-to-wishlist-$position";

			return $classes;
		}

		/* === UTILS === */

		/**
		 * Format options that will sent through AJAX calls to refresh arguments
		 *
		 * @param array  $options Array of options.
		 * @param string $context Widget/Shortcode that will use the options.
		 * @return array Array of formatted options
		 * @since 3.0.0
		 */
		public function format_fragment_options( $options, $context = '' ) {
			// removes unusable values, and changes options common for all fragments.
			if ( ! empty( $options ) ) {
				foreach ( $options as $id => $value ) {
					if ( is_object( $value ) || is_array( $value ) ) {
						// remove item if type is not supported.
						unset( $options[ $id ] );
					} elseif ( 'ajax_loading' === $id ) {
						$options['ajax_loading'] = false;
					}
				}
			}

			// applies context specific changes.
			if ( ! empty( $context ) ) {
				$options['item'] = $context;

				switch ( $context ) {
					case 'add_to_wishlist':
						unset( $options['template_part'] );
						unset( $options['label'] );
						unset( $options['exists'] );
						unset( $options['icon'] );
						unset( $options['link_classes'] );
						unset( $options['link_popup_classes'] );
						unset( $options['container_classes'] );
						unset( $options['found_in_list'] );
						unset( $options['found_item'] );
						unset( $options['popup_title'] );
						unset( $options['wishlist_url'] );
						break;
				}
			}

			return $options;
		}

		/**
		 * Decode options that comes from the fragment
		 *
		 * @param array $options Options for the fragments.
		 * @return array Filtered options for the fragment
		 */
		public function decode_fragment_options( $options ) {
			if ( ! empty( $options ) ) {
				foreach ( $options as $id => $value ) {
					if ( 'true' === $value ) {
						$options[ $id ] = true;
					} elseif ( 'false' === $value ) {
						$options[ $id ] = false;
					} else {
						$options[ $id ] = sanitize_text_field( wp_unslash( $value ) );
					}
				}
			}

			return $options;
		}

		/**
		 * Alter add to cart button when on wishlist page
		 *
		 * @return void
		 * @since 2.0.0
		 * @version 3.0.0
		 */
		public function alter_add_to_cart_button() {
			add_filter( 'woocommerce_loop_add_to_cart_args', array( $this, 'alter_add_to_cart_args' ) );
			add_filter( 'woocommerce_product_add_to_cart_text', array( $this, 'alter_add_to_cart_text' ), 10, 2 );
			add_filter( 'woocommerce_product_add_to_cart_url', array( $this, 'alter_add_to_cart_url' ), 10, 2 );
		}

		/**
		 * Restore default Add to Cart button, after wishlist handling
		 *
		 * @return void
		 * @since 3.0.0
		 */
		public function restore_add_to_cart_button() {
			remove_filter( 'woocommerce_loop_add_to_cart_args', array( $this, 'alter_add_to_cart_args' ) );
			remove_filter( 'woocommerce_product_add_to_cart_text', array( $this, 'alter_add_to_cart_text' ) );
			remove_filter( 'woocommerce_product_add_to_cart_url', array( $this, 'alter_add_to_cart_url' ) );
		}

		/**
		 * Changes arguments used to print Add to Cart button on wishlist (classes and attributes)
		 *
		 * @param array $args Array of arguments.
		 * @return array Array of filtered arguments
		 * @since 3.0.0
		 */
		public function alter_add_to_cart_args( $args ) {
			$use_custom_button = get_option( 'yith_wcwl_add_to_cart_style' );
			$button_class      = in_array( $use_custom_button, array( 'button_custom', 'button_default' ), true );
			$icon              = get_option( 'yith_wcwl_add_to_cart_icon' );
			$custom_icon       = get_option( 'yith_wcwl_add_to_cart_custom_icon' );
			$classes           = isset( $args['class'] ) ? explode( ' ', $args['class'] ) : array();
			$pos               = array_search( 'button', $classes, true );

			if ( ! $button_class && false !== $pos ) {
				unset( $classes[ $pos ] );
			} elseif ( $button_class ) {
				$classes[] = 'button';
			}

			$classes[] = 'add_to_cart';
			$classes[] = 'alt';

			$args['class'] = implode( ' ', $classes );

			if ( 'button_custom' === $use_custom_button && 'none' !== $icon ) {
				if ( ! isset( $args['attributes'] ) ) {
					$args['attributes'] = array();
				}

				if ( 'custom' !== $icon ) {
					$args['attributes']['data-icon'] = $icon;
				} elseif ( $custom_icon ) {
					$args['attributes']['data-icon'] = $custom_icon;
				}
			}

			return $args;
		}

		/**
		 * Filter Add to Cart button label on wishlist page
		 *
		 * @param string      $text Button label.
		 * @param \WC_Product $product Current product.
		 * @return string Filtered label
		 */
		public function alter_add_to_cart_text( $text, $product ) {
			$label_option = get_option( 'yith_wcwl_add_to_cart_text', __( 'Add to cart', 'yith-woocommerce-wishlist' ) );
			$label        = $product->is_type( 'variable' ) ? $text : apply_filters( 'yith_wcwl_add_to_cart_label', $label_option );

			return $label;
		}

		/**
		 * Filter Add to Cart button url on wishlist page
		 *
		 * @param string      $url Url to the Add to Cart.
		 * @param \WC_Product $product Current product.
		 * @return string Filtered url
		 */
		public function alter_add_to_cart_url( $url, $product ) {
			global $yith_wcwl_wishlist_token;

			if ( $yith_wcwl_wishlist_token ) {
				$wishlist = yith_wcwl_get_wishlist( $yith_wcwl_wishlist_token );

				if ( ! $wishlist ) {
					return $url;
				}

				$wishlist_id = $wishlist->get_id();
				$item        = $wishlist->get_product( $product->get_id() );

				if ( wp_doing_ajax() ) {
					$url = add_query_arg( 'add-to-cart', $product->get_id(), YITH_WCWL()->get_wishlist_url( 'view/' . $yith_wcwl_wishlist_token ) );
				}

				if ( $product->is_type( array( 'simple', 'variation' ) ) && 'yes' === get_option( 'yith_wcwl_redirect_cart' ) ) {
					$url = add_query_arg( 'add-to-cart', $product->get_id(), wc_get_cart_url() );
				}

				if ( ! $product->is_type( 'external' ) && 'yes' === get_option( 'yith_wcwl_remove_after_add_to_cart' ) ) {
					$url = add_query_arg(
						array(
							'remove_from_wishlist_after_add_to_cart' => $product->get_id(),
							'wishlist_id'    => $wishlist_id,
							'wishlist_token' => $yith_wcwl_wishlist_token,
						),
						$url
					);
				}

				if ( $item && 'yes' === get_option( 'yith_wcwl_quantity_show' ) ) {
					$url = add_query_arg( 'quantity', $item->get_quantity(), $url );
				}
			}

			return apply_filters( 'yit_wcwl_add_to_cart_redirect_url', esc_url_raw( $url ), $url, $product );
		}

		/**
		 * Modernize font-awesome class, for old wishlist users
		 *
		 * @param string $class Original font-awesome class.
		 * @return string Filtered font-awesome class
		 * @since 2.0.2
		 */
		public function update_font_awesome_classes( $class ) {
			$exceptions = array(
				'icon-envelope'           => 'fa-envelope-o',
				'icon-star-empty'         => 'fa-star-o',
				'icon-ok'                 => 'fa-check',
				'icon-zoom-in'            => 'fa-search-plus',
				'icon-zoom-out'           => 'fa-search-minus',
				'icon-off'                => 'fa-power-off',
				'icon-trash'              => 'fa-trash-o',
				'icon-share'              => 'fa-share-square-o',
				'icon-check'              => 'fa-check-square-o',
				'icon-move'               => 'fa-arrows',
				'icon-file'               => 'fa-file-o',
				'icon-time'               => 'fa-clock-o',
				'icon-download-alt'       => 'fa-download',
				'icon-download'           => 'fa-arrow-circle-o-down',
				'icon-upload'             => 'fa-arrow-circle-o-up',
				'icon-play-circle'        => 'fa-play-circle-o',
				'icon-indent-left'        => 'fa-dedent',
				'icon-indent-right'       => 'fa-indent',
				'icon-facetime-video'     => 'fa-video-camera',
				'icon-picture'            => 'fa-picture-o',
				'icon-plus-sign'          => 'fa-plus-circle',
				'icon-minus-sign'         => 'fa-minus-circle',
				'icon-remove-sign'        => 'fa-times-circle',
				'icon-ok-sign'            => 'fa-check-circle',
				'icon-question-sign'      => 'fa-question-circle',
				'icon-info-sign'          => 'fa-info-circle',
				'icon-screenshot'         => 'fa-crosshairs',
				'icon-remove-circle'      => 'fa-times-circle-o',
				'icon-ok-circle'          => 'fa-check-circle-o',
				'icon-ban-circle'         => 'fa-ban',
				'icon-share-alt'          => 'fa-share',
				'icon-resize-full'        => 'fa-expand',
				'icon-resize-small'       => 'fa-compress',
				'icon-exclamation-sign'   => 'fa-exclamation-circle',
				'icon-eye-open'           => 'fa-eye',
				'icon-eye-close'          => 'fa-eye-slash',
				'icon-warning-sign'       => 'fa-warning',
				'icon-folder-close'       => 'fa-folder',
				'icon-resize-vertical'    => 'fa-arrows-v',
				'icon-resize-horizontal'  => 'fa-arrows-h',
				'icon-twitter-sign'       => 'fa-twitter-square',
				'icon-facebook-sign'      => 'fa-facebook-square',
				'icon-thumbs-up'          => 'fa-thumbs-o-up',
				'icon-thumbs-down'        => 'fa-thumbs-o-down',
				'icon-heart-empty'        => 'fa-heart-o',
				'icon-signout'            => 'fa-sign-out',
				'icon-linkedin-sign'      => 'fa-linkedin-square',
				'icon-pushpin'            => 'fa-thumb-tack',
				'icon-signin'             => 'fa-sign-in',
				'icon-github-sign'        => 'fa-github-square',
				'icon-upload-alt'         => 'fa-upload',
				'icon-lemon'              => 'fa-lemon-o',
				'icon-check-empty'        => 'fa-square-o',
				'icon-bookmark-empty'     => 'fa-bookmark-o',
				'icon-phone-sign'         => 'fa-phone-square',
				'icon-hdd'                => 'fa-hdd-o',
				'icon-hand-right'         => 'fa-hand-o-right',
				'icon-hand-left'          => 'fa-hand-o-left',
				'icon-hand-up'            => 'fa-hand-o-up',
				'icon-hand-down'          => 'fa-hand-o-down',
				'icon-circle-arrow-left'  => 'fa-arrow-circle-left',
				'icon-circle-arrow-right' => 'fa-arrow-circle-right',
				'icon-circle-arrow-up'    => 'fa-arrow-circle-up',
				'icon-circle-arrow-down'  => 'fa-arrow-circle-down',
				'icon-fullscreen'         => 'fa-arrows-alt',
				'icon-beaker'             => 'fa-flask',
				'icon-paper-clip'         => 'fa-paperclip',
				'icon-sign-blank'         => 'fa-square',
				'icon-pinterest-sign'     => 'fa-pinterest-square',
				'icon-google-plus-sign'   => 'fa-google-plus-square',
				'icon-envelope-alt'       => 'fa-envelope',
				'icon-comment-alt'        => 'fa-comment-o',
				'icon-comments-alt'       => 'fa-comments-o',
			);

			if ( in_array( $class, array_keys( $exceptions ), true ) ) {
				$class = $exceptions[ $class ];
			}

			$class = str_replace( 'icon-', 'fa-', $class );

			return $class;
		}

		/**
		 * Add Frequently Bought Together shortcode to wishlist page
		 *
		 * @param mixed $meta Meta.
		 * @author Francesco Licandro
		 */
		public function yith_wcfbt_shortcode( $meta ) {

			if ( ! ( defined( 'YITH_WFBT' ) && YITH_WFBT ) || 'no' === get_option( 'yith_wfbt_enable_integration' ) ) {
				return;
			}

			$products = YITH_WCWL()->get_products(
				array(
					'wishlist_id' => is_user_logged_in() ? $meta['ID'] : '',
				)
			);

			$ids = array();
			// take id of products in wishlist.
			foreach ( $products as $product ) {
				$ids[] = $product['prod_id'];
			}

			if ( empty( $ids ) ) {
				return;
			}

			do_shortcode( '[yith_wfbt products="' . implode( ',', $ids ) . '"]' );
		}

		/**
		 * Redirect after add to cart from YITH WooCommerce Frequently Bought Together Premium shortcode
		 *
		 * @param string $url Redirect url.
		 *
		 * @since 2.0.0
		 */
		public function yith_wfbt_redirect_after_add_to_cart( $url ) {
			if ( ! isset( $_REQUEST['yith_wfbt_shortcode'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
				return $url;
			}

			return 'yes' === get_option( 'yith_wcwl_redirect_cart' ) ? wc_get_cart_url() : YITH_WCWL()->get_wishlist_url();
		}

		/**
		 * Generate CSS code to append to each page, to apply custom style to wishlist elements
		 *
		 * @param array $rules Array of additional rules to add to default ones.
		 * @return string Generated CSS code
		 */
		protected function build_custom_css( $rules = array() ) {
			$generated_code = '';
			$rules          = apply_filters(
				'yith_wcwl_custom_css_rules',
				array_merge(
					array(
						'color_add_to_wishlist'    => array(
							'selector' => '.woocommerce a.add_to_wishlist.button.alt',
							'rules'    => array(
								'background'       => array(
									'rule'    => 'background-color: %1$s; background: %1$s',
									'default' => '#333333',
								),
								'text'             => array(
									'rule'    => 'color: %s',
									'default' => '#ffffff',
								),
								'border'           => array(
									'rule'    => 'border-color: %s',
									'default' => '#333333',
								),
								'background_hover' => array(
									'rule'    => 'background-color: %1$s; background: %1$s',
									'default' => '#4F4F4F',
									'status'  => ':hover',
								),
								'text_hover'       => array(
									'rule'    => 'color: %s',
									'default' => '#ffffff',
									'status'  => ':hover',
								),
								'border_hover'     => array(
									'rule'    => 'border-color: %s',
									'default' => '#4F4F4F',
									'status'  => ':hover',
								),
							),
							'deps'     => array(
								'yith_wcwl_add_to_wishlist_style' => 'button_custom',
							),
						),
						'rounded_corners_radius'   => array(
							'selector' => '.woocommerce a.add_to_wishlist.button.alt',
							'rules'    => array(
								'rule'    => 'border-radius: %dpx',
								'default' => 16,
							),
							'deps'     => array(
								'yith_wcwl_add_to_wishlist_style' => 'button_custom',
							),
						),
						'color_add_to_cart'        => array(
							'selector' => '.woocommerce .wishlist_table a.add_to_cart.button.alt',
							'rules'    => array(
								'background'       => array(
									'rule'    => 'background: %1$s; background-color: %1$s;',
									'default' => '#333333',
								),
								'text'             => array(
									'rule'    => 'color: %s',
									'default' => '#ffffff',
								),
								'border'           => array(
									'rule'    => 'border-color: %s',
									'default' => '#333333',
								),
								'background_hover' => array(
									'rule'    => 'background: %1$s; background-color: %1$s;',
									'default' => '#4F4F4F',
									'status'  => ':hover',
								),
								'text_hover'       => array(
									'rule'    => 'color: %s',
									'default' => '#ffffff',
									'status'  => ':hover',
								),
								'border_hover'     => array(
									'rule'    => 'border-color: %s',
									'default' => '#4F4F4F',
									'status'  => ':hover',
								),
							),
							'deps'     => array(
								'yith_wcwl_add_to_cart_style' => 'button_custom',
							),
						),
						'add_to_cart_rounded_corners_radius' => array(
							'selector' => '.woocommerce .wishlist_table a.add_to_cart.button.alt',
							'rules'    => array(
								'rule'    => 'border-radius: %dpx',
								'default' => 16,
							),
							'deps'     => array(
								'yith_wcwl_add_to_cart_style' => 'button_custom',
							),
						),
						'color_button_style_1'     => array(
							'selector' => '.woocommerce .hidden-title-form button,
								   .yith-wcwl-wishlist-new .create-wishlist-button,
								   .wishlist_manage_table tfoot button.submit-wishlist-changes,
								   .yith-wcwl-wishlist-search-form button.wishlist-search-button',
							'rules'    => array(
								'background'       => array(
									'rule'    => 'background: %1$s; background-color: %1$s;',
									'default' => '#333333',
								),
								'text'             => array(
									'rule'    => 'color: %s',
									'default' => '#ffffff',
								),
								'border'           => array(
									'rule'    => 'border-color: %s',
									'default' => '#333333',
								),
								'background_hover' => array(
									'rule'    => 'background: %1$s; background-color: %1$s;',
									'default' => '#333333',
									'status'  => ':hover',
								),
								'text_hover'       => array(
									'rule'    => 'color: %s',
									'default' => '#ffffff',
									'status'  => ':hover',
								),
								'border_hover'     => array(
									'rule'    => 'border-color: %s',
									'default' => '#333333',
									'status'  => ':hover',
								),
							),
							'deps'     => array(
								'yith_wcwl_add_to_cart_style' => 'button_custom',
							),
						),
						'color_button_style_2'     => array(
							'selector' => '.woocommerce .wishlist-title a.show-title-form,
								   .woocommerce .hidden-title-form a.hide-title-form,
								   .wishlist_manage_table tfoot a.create-new-wishlist',
							'rules'    => array(
								'background'       => array(
									'rule'    => 'background: %1$s; background-color: %1$s;',
									'default' => '#333333',
								),
								'text'             => array(
									'rule'    => 'color: %s',
									'default' => '#ffffff',
								),
								'border'           => array(
									'rule'    => 'border-color: %s',
									'default' => '#333333',
								),
								'background_hover' => array(
									'rule'    => 'background: %1$s; background-color: %1$s;',
									'default' => '#333333',
									'status'  => ':hover',
								),
								'text_hover'       => array(
									'rule'    => 'color: %s',
									'default' => '#ffffff',
									'status'  => ':hover',
								),
								'border_hover'     => array(
									'rule'    => 'border-color: %s',
									'default' => '#333333',
									'status'  => ':hover',
								),
							),
							'deps'     => array(
								'yith_wcwl_add_to_cart_style' => 'button_custom',
							),
						),
						'color_wishlist_table'     => array(
							'selector' => '.woocommerce table.shop_table.wishlist_table tr td',
							'rules'    => array(
								'background' => array(
									'rule'    => 'background: %1$s; background-color: %1$s;',
									'default' => '#FFFFFF',
								),
								'text'       => array(
									'rule'    => 'color: %s',
									'default' => '#6D6C6C',
								),
								'border'     => array(
									'rule'    => 'border-color: %s;',
									'default' => '#FFFFFF',
								),
							),
							'deps'     => array(
								'yith_wcwl_add_to_cart_style' => 'button_custom',
							),
						),
						'color_headers_background' => array(
							'selector' => '.wishlist_table thead tr th,
								   .wishlist_table tfoot td td,
								   .widget_yith-wcwl-lists ul.dropdown li.current a,
								   .widget_yith-wcwl-lists ul.dropdown li a:hover,
								   .selectBox-dropdown-menu.selectBox-options li.selectBox-selected a,
								   .selectBox-dropdown-menu.selectBox-options li.selectBox-hover a',
							'rules'    => array(
								'rule'    => 'background: %1$s; background-color: %1$s;',
								'default' => '#F4F4F4',
							),
							'deps'     => array(
								'yith_wcwl_add_to_cart_style' => 'button_custom',
							),
						),
						'color_share_button'       => array(
							'selector' => '.yith-wcwl-share li a',
							'rules'    => array(
								'color'       => array(
									'rule'    => 'color: %s;',
									'default' => '#FFFFFF',
								),
								'color_hover' => array(
									'rule'    => 'color: %s;',
									'status'  => ':hover',
									'default' => '#FFFFFF',
								),
							),
							'deps'     => array(
								'yith_wcwl_enable_share' => 'yes',
							),
						),
						'color_fb_button'          => array(
							'selector' => '.yith-wcwl-share a.facebook',
							'rules'    => array(
								'background'       => array(
									'rule'    => 'background: %1$s; background-color: %1$s;',
									'default' => '#39599E',
								),
								'background_hover' => array(
									'rule'    => 'background: %1$s; background-color: %1$s;',
									'status'  => ':hover',
									'default' => '#39599E',
								),
							),
							'deps'     => array(
								'yith_wcwl_enable_share' => 'yes',
								'yith_wcwl_share_fb'     => 'yes',
							),
						),
						'color_tw_button'          => array(
							'selector' => '.yith-wcwl-share a.twitter',
							'rules'    => array(
								'background'       => array(
									'rule'    => 'background: %1$s; background-color: %1$s;',
									'default' => '#45AFE2',
								),
								'background_hover' => array(
									'rule'    => 'background: %1$s; background-color: %1$s;',
									'status'  => ':hover',
									'default' => '#39599E',
								),
							),
							'deps'     => array(
								'yith_wcwl_enable_share'  => 'yes',
								'yith_wcwl_share_twitter' => 'yes',
							),
						),
						'color_pr_button'          => array(
							'selector' => '.yith-wcwl-share a.pinterest',
							'rules'    => array(
								'background'       => array(
									'rule'    => 'background: %1$s; background-color: %1$s;',
									'default' => '#AB2E31',
								),
								'background_hover' => array(
									'rule'    => 'background: %1$s; background-color: %1$s;',
									'status'  => ':hover',
									'default' => '#39599E',
								),
							),
							'deps'     => array(
								'yith_wcwl_enable_share' => 'yes',
								'yith_wcwl_share_pinterest' => 'yes',
							),
						),
						'color_em_button'          => array(
							'selector' => '.yith-wcwl-share a.email',
							'rules'    => array(
								'background'       => array(
									'rule'    => 'background: %1$s; background-color: %1$s;',
									'default' => '#FBB102',
								),
								'background_hover' => array(
									'rule'    => 'background: %1$s; background-color: %1$s;',
									'status'  => ':hover',
									'default' => '#39599E',
								),
							),
							'deps'     => array(
								'yith_wcwl_enable_share' => 'yes',
								'yith_wcwl_share_email'  => 'yes',
							),
						),
						'color_wa_button'          => array(
							'selector' => '.yith-wcwl-share a.whatsapp',
							'rules'    => array(
								'background'       => array(
									'rule'    => 'background: %1$s; background-color: %1$s;',
									'default' => '#00A901',
								),
								'background_hover' => array(
									'rule'    => 'background: %1$s; background-color: %1$s;',
									'status'  => ':hover',
									'default' => '#39599E',
								),
							),
							'deps'     => array(
								'yith_wcwl_enable_share'   => 'yes',
								'yith_wcwl_share_whatsapp' => 'yes',
							),
						),
					),
					$rules
				)
			);

			if ( empty( $rules ) ) {
				return $generated_code;
			}

			// retrieve dependencies.
			$deps_list    = wp_list_pluck( $rules, 'deps' );
			$dependencies = array();

			if ( ! empty( $deps_list ) ) {
				foreach ( $deps_list as $rule => $deps ) {
					foreach ( $deps as $dep_rule => $dep_value ) {
						if ( ! isset( $dependencies[ $dep_rule ] ) ) {
							$dependencies[ $dep_rule ] = get_option( $dep_rule );
						}
					}
				}
			}

			foreach ( $rules as $id => $rule ) {
				// check dependencies first.
				if ( ! empty( $rule['deps'] ) ) {
					foreach ( $rule['deps'] as $dep_rule => $dep_value ) {
						if ( ! isset( $dependencies[ $dep_rule ] ) || $dependencies[ $dep_rule ] !== $dep_value ) {
							continue 2;
						}
					}
				}

				// retrieve values from db.
				$values     = get_option( "yith_wcwl_{$id}" );
				$new_rules  = array();
				$rules_code = '';

				if ( isset( $rule['rules']['rule'] ) ) {
					// if we have a single-valued option, just search for the rule to apply.
					$status = isset( $rule['rules']['status'] ) ? $rule['rules']['status'] : '';

					if ( ! isset( $new_rules[ $status ] ) ) {
						$new_rules[ $status ] = array();
					}

					$new_rules[ $status ][] = $this->build_css_rule( $rule['rules']['rule'], $values, $rule['rules']['default'] );
				} else {
					// otherwise cycle through rules, and generate CSS code.
					foreach ( $rule['rules'] as $property => $css ) {
						$status = isset( $css['status'] ) ? $css['status'] : '';

						if ( ! isset( $new_rules[ $status ] ) ) {
							$new_rules[ $status ] = array();
						}

						$new_rules[ $status ][] = $this->build_css_rule( $css['rule'], isset( $values[ $property ] ) ? $values[ $property ] : false, $css['default'] );
					}
				}

				// if code was generated, prepend selector.
				if ( ! empty( $new_rules ) ) {
					foreach ( $new_rules as $status => $rules ) {
						$selector = $rule['selector'];

						if ( ! empty( $status ) ) {
							$updated_selector = array();
							$split_selectors  = explode( ',', $rule['selector'] );

							foreach ( $split_selectors as $split_selector ) {
								$updated_selector[] = $split_selector . $status;
							}

							$selector = implode( ',', $updated_selector );
						}

						$rules_code .= $selector . '{' . implode( '', $rules ) . '}';
					}
				}

				// append new rule to generated CSS.
				$generated_code .= $rules_code;
			}

			$custom_css = get_option( 'yith_wcwl_custom_css' );

			if ( $custom_css ) {
				$generated_code .= $custom_css;
			}

			return $generated_code;
		}

		/**
		 * Generate each single CSS rule that will be included in custom plugin CSS
		 *
		 * @param string $rule    Rule to use; placeholders may be applied to be replaced with value {@see sprintf}.
		 * @param string $value   Value to inject inside rule, replacing placeholders.
		 * @param string $default Default value, to be used instead of value when it is empty.
		 *
		 * @return string Formatted CSS rule
		 */
		protected function build_css_rule( $rule, $value, $default = '' ) {
			$value = ( '0' === $value || ( ! empty( $value ) && ! is_array( $value ) ) ) ? $value : $default;

			return sprintf( rtrim( $rule, ';' ) . ';', $value );
		}

		/**
		 * Destroy serialize cookies, to prevent major vulnerability
		 *
		 * @return void
		 * @since 2.0.7
		 */
		protected function destroy_serialized_cookies() {
			$name = 'yith_wcwl_products';

			if ( isset( $_COOKIE[ $name ] ) && is_serialized( sanitize_text_field( wp_unslash( $_COOKIE[ $name ] ) ) ) ) {
				$_COOKIE[ $name ] = wp_json_encode( array() );
				yith_destroycookie( $name );
			}
		}

		/**
		 * Update old wishlist cookies
		 *
		 * @return void
		 * @since 2.0.0
		 */
		protected function update_cookies() {
			$cookie     = yith_getcookie( 'yith_wcwl_products' );
			$new_cookie = array();

			if ( ! empty( $cookie ) ) {
				foreach ( $cookie as $item ) {
					if ( ! isset( $item['add-to-wishlist'] ) ) {
						return;
					}

					$new_cookie[] = array(
						'prod_id'     => $item['add-to-wishlist'],
						'quantity'    => isset( $item['quantity'] ) ? $item['quantity'] : 1,
						'wishlist_id' => false,
					);
				}

				yith_setcookie( 'yith_wcwl_products', $new_cookie );
			}
		}

		/**
		 * Convert wishlist stored into cookies into
		 */
		protected function convert_cookies_to_session() {
			$cookie = yith_getcookie( 'yith_wcwl_products' );

			if ( ! empty( $cookie ) ) {

				$default_list = YITH_WCWL_Wishlist_Factory::get_default_wishlist();

				if ( ! $default_list ) {
					return false;
				}

				foreach ( $cookie as $item ) {
					if ( $default_list->has_product( $item['prod_id'] ) ) {
						continue;
					}

					$new_item = new YITH_WCWL_Wishlist_Item();

					$new_item->set_product_id( $item['prod_id'] );
					$new_item->set_quantity( $item['quantity'] );

					if ( isset( $item['dateadded'] ) ) {
						$new_item->set_date_added( $item['dateadded'] );
					}

					$default_list->add_item( $new_item );
				}

				$default_list->save();

				yith_destroycookie( 'yith_wcwl_products' );
			}
		}
	}
}

/**
 * Unique access to instance of YITH_WCWL_Frontend class
 *
 * @return \YITH_WCWL_Frontend|\YITH_WCWL_Frontend_Premium
 * @since 2.0.0
 */
function YITH_WCWL_Frontend() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid
	return defined( 'YITH_WCWL_PREMIUM' ) ? YITH_WCWL_Frontend_Premium::get_instance() : YITH_WCWL_Frontend::get_instance();
}
class-yith-wcwl-form-handler.php000064400000013024151327635660012701 0ustar00<?php
/**
 * Static class that will handle all form submission from customer
 *
 * @author YITH
 * @package YITH\Wishlist\Classes
 * @version 3.0.0
 */

if ( ! defined( 'YITH_WCWL' ) ) {
	exit;
} // Exit if accessed directly

if ( ! class_exists( 'YITH_WCWL_Form_Handler' ) ) {
	/**
	 * WooCommerce Wishlist Form Handler
	 *
	 * @since 3.0.0
	 */
	class YITH_WCWL_Form_Handler {
		/**
		 * Performs all required add_actions to handle forms
		 *
		 * @return void
		 */
		public static function init() {
			/**
			 * This check was added to prevent bots from accidentaly executing wishlist code
			 *
			 * @since 3.0.10
			 */
			if ( ! self::process_form_handling() ) {
				return;
			}

			// add to wishlist when js is disabled.
			add_action( 'init', array( 'YITH_WCWL_Form_Handler', 'add_to_wishlist' ) );

			// remove from wishlist when js is disabled.
			add_action( 'init', array( 'YITH_WCWL_Form_Handler', 'remove_from_wishlist' ) );

			// remove from wishlist after add to cart.
			add_action( 'woocommerce_add_to_cart', array( 'YITH_WCWL_Form_Handler', 'remove_from_wishlist_after_add_to_cart' ) );

			// change wishlist title.
			add_action( 'init', array( 'YITH_WCWL_Form_Handler', 'change_wishlist_title' ) );
		}

		/**
		 * Return true if system can process request; false otherwise
		 *
		 * @return bool
		 */
		public static function process_form_handling() {
			$user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : false;

			if ( $user_agent && apply_filters( 'yith_wcwl_block_user_agent', preg_match( '/bot|crawl|slurp|spider|wordpress/i', $user_agent ), $user_agent ) ) {
				return false;
			}

			return true;
		}

		/**
		 * Adds a product to wishlist when js is disabled
		 *
		 * @return void
		 */
		public static function add_to_wishlist() {
			// add item to wishlist when javascript is not enabled.
			if ( isset( $_GET['add_to_wishlist'] ) && isset( $_GET['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'add_to_wishlist' ) ) {
				try {
					YITH_WCWL()->add();

					yith_wcwl_add_notice( apply_filters( 'yith_wcwl_product_added_to_wishlist_message', get_option( 'yith_wcwl_product_added_text' ) ), 'success' );
				} catch ( Exception $e ) {
					yith_wcwl_add_notice( apply_filters( 'yith_wcwl_error_adding_to_wishlist_message', $e->getMessage() ), 'error' );
				}
			}
		}

		/**
		 * Removes from wishlist when js is disabled
		 *
		 * @return void
		 */
		public static function remove_from_wishlist() {
			// remove item from wishlist when javascript is not enabled.
			if ( isset( $_GET['remove_from_wishlist'] ) && isset( $_GET['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'remove_from_wishlist' ) ) {
				try {
					YITH_WCWL()->remove();
				} catch ( Exception $e ) {
					yith_wcwl_add_notice( $e->getMessage(), 'error' );
				}
			}
		}

		/**
		 * Remove from wishlist after adding to cart
		 *
		 * @return void
		 */
		public static function remove_from_wishlist_after_add_to_cart() {
			if ( 'yes' !== get_option( 'yith_wcwl_remove_after_add_to_cart' ) ) {
				return;
			}

			$args = array();

			// phpcs:disable WordPress.Security.NonceVerification.Recommended
			if ( isset( $_REQUEST['remove_from_wishlist_after_add_to_cart'] ) ) {

				$args['remove_from_wishlist'] = intval( $_REQUEST['remove_from_wishlist_after_add_to_cart'] );

				if ( isset( $_REQUEST['wishlist_id'] ) ) {
					$args['wishlist_id'] = sanitize_text_field( wp_unslash( $_REQUEST['wishlist_id'] ) );
				}
			} elseif ( yith_wcwl_is_wishlist() && isset( $_REQUEST['add-to-cart'] ) ) {
				$args['remove_from_wishlist'] = intval( $_REQUEST['add-to-cart'] );

				if ( isset( $_REQUEST['wishlist_id'] ) ) {
					$args['wishlist_id'] = sanitize_text_field( wp_unslash( $_REQUEST['wishlist_id'] ) );
				}
			}
			// phpcs:enable WordPress.Security.NonceVerification.Recommended

			if ( ! empty( $args['wishlist_id'] ) ) {
				$wishlist = yith_wcwl_get_wishlist( $args['wishlist_id'] );

				if ( apply_filters( 'yith_wcwl_remove_after_add_to_cart', $wishlist && $wishlist->is_current_user_owner(), $wishlist ) ) {
					try {
						YITH_WCWL()->remove( $args );
					} catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
						// we were unable to remove item from the wishlist; no follow up is provided.
					}
				}
			}
		}

		/**
		 * Change wishlist title
		 *
		 * @return void
		 * @since 2.0.0
		 */
		public static function change_wishlist_title() {
			if ( ! isset( $_POST['yith_wcwl_edit_wishlist'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['yith_wcwl_edit_wishlist'] ) ), 'yith_wcwl_edit_wishlist_action' ) || ! isset( $_POST['save_title'] ) || empty( $_POST['wishlist_name'] ) ) {
				return;
			}

			$wishlist_name = isset( $_POST['wishlist_name'] ) ? sanitize_text_field( wp_unslash( $_POST['wishlist_name'] ) ) : false;
			$wishlist_id   = isset( $_POST['wishlist_id'] ) ? sanitize_text_field( wp_unslash( $_POST['wishlist_id'] ) ) : false;
			$wishlist      = yith_wcwl_get_wishlist( $wishlist_id );

			if ( ! $wishlist_name || strlen( $wishlist_name ) >= 65535 ) {
				yith_wcwl_add_notice( __( 'Please, make sure to enter a valid title', 'yith-woocommerce-wishlist' ), 'error' );
			} else {
				$wishlist->set_name( $wishlist_name );
				$wishlist->save();
			}

			$redirect_url = isset( $_REQUEST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_REQUEST['redirect_to'] ) ) : $wishlist->get_url();

			wp_safe_redirect( $redirect_url );
			die;
		}
	}
}

YITH_WCWL_Form_Handler::init();
class-yith-wcwl-wishlist-item.php000064400000037562151327635660013142 0ustar00<?php
/**
 * Wishlist Item class
 *
 * @author YITH
 * @package YITH\Wishlist\Classes\Wishlists
 * @version 3.0.0
 */

if ( ! defined( 'YITH_WCWL' ) ) {
	exit;
} // Exit if accessed directly

if ( ! class_exists( 'YITH_WCWL_Wishlist_Item' ) ) {
	/**
	 * This class describes Wishlist Item object, and it is meant to be created by YITH_WCWL_Wishlist class, via
	 * get_items method
	 *
	 * @since 3.0.0
	 */
	class YITH_WCWL_Wishlist_Item extends WC_Data implements ArrayAccess {

		/**
		 * Item Data array
		 *
		 * @since 3.0.0
		 * @var array
		 */
		protected $data = array(
			'wishlist_id'       => 0,
			'product_id'        => 0,
			'quantity'          => 1,
			'user_id'           => 0,
			'date_added'        => '',
			'position'          => 0,
			'original_price'    => 0,
			'original_currency' => '',
			'on_sale'           => 0,
		);

		/**
		 * Register product to avoid retrieving it more than once
		 *
		 * @var \WC_Product
		 */
		protected $product = null;

		/**
		 * Register origin wishlist ID;
		 * if item is moved to another wishlist, we can then clear origin wishlist cache
		 *
		 * @var int
		 */
		protected $origin_wishlist_id = 0;

		/**
		 * Stores meta in cache for future reads.
		 * A group must be set to to enable caching.
		 *
		 * @var string
		 */
		protected $cache_group = 'wishlist-items';

		/**
		 * Constructor.
		 *
		 * @param int|object|array $item ID to load from the DB, or YITH_WCWL_Wishlist_Item object.
		 * @throws Exception When cannot loading correct Data Store object.
		 */
		public function __construct( $item = 0 ) {
			parent::__construct( $item );

			if ( $item instanceof YITH_WCWL_Wishlist_Item ) {
				$this->set_id( $item->get_id() );
			} elseif ( is_numeric( $item ) && $item > 0 ) {
				$this->set_id( $item );
			} else {
				$this->set_object_read( true );
			}

			$this->data_store = WC_Data_Store::load( 'wishlist-item' );

			if ( $this->get_id() > 0 ) {
				$this->data_store->read( $this );
			}

			if ( $this->get_object_read() ) {
				$this->origin_wishlist_id = $this->get_wishlist_id();
			}
		}

		/* === GETTERS === */

		/**
		 * Get wishlist ID for current item
		 *
		 * @param string $context Context.
		 * @return int Wishlist ID
		 */
		public function get_wishlist_id( $context = 'view' ) {
			return (int) $this->get_prop( 'wishlist_id', $context );
		}

		/**
		 * Get origin wishlist ID for current item
		 *
		 * @return int Wishlist ID
		 */
		public function get_origin_wishlist_id() {
			return (int) $this->origin_wishlist_id;
		}

		/**
		 * Get origin product ID for current item (no WPML filtering)
		 *
		 * @param string $context Context.
		 *
		 * @return int Wishlist ID
		 */
		public function get_original_product_id( $context = 'view' ) {
			return (int) $this->get_prop( 'product_id', $context );
		}

		/**
		 * Get product ID for current item
		 *
		 * @param string $context Context.
		 * @return int Product ID
		 */
		public function get_product_id( $context = 'view' ) {
			return yit_wpml_object_id( $this->get_original_product_id( $context ), 'product', true );
		}

		/**
		 * Return product object related to current item
		 *
		 * @param string $context Context.
		 * @return \WC_Product Product
		 */
		public function get_product( $context = 'view' ) {
			if ( empty( $this->product ) ) {
				$product = wc_get_product( $this->get_product_id( $context ) );

				if ( $product ) {
					$this->product = $product;
				}
			}

			return $this->product;
		}

		/**
		 * Return price of the produce related to current item
		 *
		 * @param string $context Context.
		 * @return float
		 */
		public function get_product_price( $context = 'view' ) {
			$product = $this->get_product( $context );

			if ( ! $product ) {
				return 0;
			}

			switch ( $product->get_type() ) {
				case 'variable':
					/**
					 * Product used is a variation; we can then retrieve minimum variation price
					 *
					 * @var $product \WC_Product_Variable
					 */
					return (float) $product->get_variation_price( 'min' );
				default:
					$sale_price = $product->get_sale_price();
					return $sale_price ? (float) $sale_price : (float) $product->get_price();
			}
		}

		/**
		 * Retrieve formatted price for current item
		 *
		 * @param string $context Context.
		 * @return string Formatter price
		 */
		public function get_formatted_product_price( $context = 'view' ) {
			$product = $this->get_product( $context );

			$base_price      = $product->is_type( 'variable' ) ? $product->get_variation_regular_price( 'max' ) : $product->get_price();
			$formatted_price = $base_price ? $product->get_price_html() : apply_filters( 'yith_free_text', __( 'Free!', 'yith-woocommerce-wishlist' ), $product );

			return apply_filters( 'yith_wcwl_item_formatted_price', $formatted_price, $base_price, $product );
		}

		/**
		 * Return formatted product name
		 *
		 * @param string $context Context.
		 * @return string Formatted name; empty string on failure
		 */
		public function get_formatted_product_name( $context = 'view' ) {
			$product = $this->get_product( $context );

			if ( ! $product ) {
				return '';
			}

			return $product->get_formatted_name();
		}

		/**
		 * Get quantity for current item
		 *
		 * @param string $context Context.
		 * @return int Quantity
		 */
		public function get_quantity( $context = 'view' ) {
			return max( 1, (int) $this->get_prop( 'quantity', $context ) );
		}

		/**
		 * Get user ID for current item
		 *
		 * @param string $context Context.
		 * @return int User ID
		 */
		public function get_user_id( $context = 'view' ) {
			return (int) $this->get_prop( 'user_id', $context );
		}

		/**
		 * Get user for current item
		 *
		 * @param string $context Context.
		 * @return \WP_User|bool User
		 */
		public function get_user( $context = 'view' ) {
			$user_id = (int) $this->get_prop( 'user_id', $context );

			if ( ! $user_id ) {
				return false;
			}

			return get_user_by( 'id', $user_id );
		}

		/**
		 * Get wishlist date added
		 *
		 * @param string $context Context.
		 * @return \WC_DateTime|string Wishlist date of creation
		 */
		public function get_date_added( $context = 'view' ) {
			$date_added = $this->get_prop( 'date_added', $context );

			if ( $date_added && 'view' === $context ) {
				return $date_added->date_i18n( 'Y-m-d H:i:s' );
			} else {
				return $date_added;
			}
		}

		/**
		 * Get formatted wishlist date added
		 *
		 * @param string $format Date format (if empty, WP date format will be applied).
		 * @return string Wishlist date of creation
		 */
		public function get_date_added_formatted( $format = '' ) {
			$date_added = $this->get_date_added( 'edit' );

			if ( $date_added ) {
				$format = $format ? $format : get_option( 'date_format' );
				return $date_added->date_i18n( $format );
			}

			return '';
		}

		/**
		 * Get related wishlist
		 *
		 * @return \YITH_WCWL_Wishlist|bool Wishlist object, or false on failure
		 */
		public function get_wishlist() {
			$wishlist_id = $this->get_wishlist_id();

			if ( ! $wishlist_id ) {
				return false;
			}

			return YITH_WCWL_Wishlist_Factory::get_wishlist( $wishlist_id );
		}

		/**
		 * Get related wishlist slug
		 *
		 * @return string|bool Wishlist slug, or false on failure
		 */
		public function get_wishlist_slug() {
			$wishlist = $this->get_wishlist();

			if ( ! $wishlist ) {
				return false;
			}

			return $wishlist->get_slug();
		}

		/**
		 * Get related wishlist name
		 *
		 * @return string|bool Wishlist name, or false on failure
		 */
		public function get_wishlist_name() {
			$wishlist = $this->get_wishlist();

			if ( ! $wishlist ) {
				return false;
			}

			return $wishlist->get_name();
		}

		/**
		 * Get related wishlist token
		 *
		 * @return string|bool Wishlist token, or false on failure
		 */
		public function get_wishlist_token() {
			$wishlist = $this->get_wishlist();

			if ( ! $wishlist ) {
				return false;
			}

			return $wishlist->get_token();
		}

		/**
		 * Return item position inside the list
		 *
		 * @param string $context Context.
		 * @return int Position
		 */
		public function get_position( $context = 'view' ) {
			return (int) $this->get_prop( 'position', $context );
		}

		/**
		 * Return original price
		 *
		 * @param string $context Context.
		 * @return string Original price
		 */
		public function get_original_price( $context = 'view' ) {
			$price = $this->get_prop( 'original_price', 'edit' );

			if ( 'view' === $context ) {
				return wc_price(
					$price,
					array(
						'currency' => $this->get_original_currency(),
					)
				);
			}

			return $price;
		}

		/**
		 * Return original currency
		 *
		 * @param string $context Context.
		 * @return string Original price
		 */
		public function get_original_currency( $context = 'view' ) {
			$currency = $this->get_prop( 'original_currency', 'edit' );

			if ( 'view' === $context && ! $currency ) {
				$currency = get_woocommerce_currency();
			}

			return $currency;
		}

		/**
		 * Returns a formatted HTML template for the "Price variation" label
		 *
		 * @return string HTML for the template, or empty string if price variation is not applicable to current item
		 */
		public function get_price_variation() {
			$original_currency = $this->get_original_currency( 'edit' );

			// if currency changed, makes no sense to make comparisons.
			if ( get_woocommerce_currency() !== $original_currency ) {
				return '';
			}

			$original_price = $this->get_original_price( 'edit' );

			// original price wasn't stored in the wishlist.
			if ( ! $original_price ) {
				return '';
			}

			$product       = $this->get_product();
			$current_price = $this->get_product_price();

			if ( ! is_numeric( $current_price ) ) {
				return '';
			}

			$difference = $original_price - $current_price;

			if ( $difference <= 0 && apply_filters( 'yith_wcwl_hide_price_increase', true, $product, $original_price, $original_currency ) ) {
				return '';
			}

			$percentage_difference = -1 * round( $difference / $original_price * 100, 2 );
			$class                 = $percentage_difference > 0 ? 'increase' : 'decrease';

			$template = apply_filters(
				'yith_wcwl_price_variation_template',
				sprintf(
					'<small class="price-variation %s"><span class="variation-rate">%s</span><span class="old-price">%s</span></small>',
					$class,
					// translators: 1. % of reduction/increase in price.
					_x( 'Price is %1$s%%', 'Part of the template that shows price variation since addition to list; placeholder will be replaced with a percentage', 'yith-woocommerce-wishlist' ),
					// translators: 2: original product price.
					_x( '(Was %2$s when added  in list)', 'Part of the template that shows price variation since addition to list; placeholder will be replaced with a price', 'yith-woocommerce-wishlist' )
				),
				$class,
				$percentage_difference,
				$original_price,
				$original_currency
			);
			$template = sprintf( $template, $percentage_difference, wc_price( $original_price, array( 'currency' => $original_currency ) ) );

			return $template;
		}

		/**
		 * Return state of on_sale flag
		 * Important: this flag is used for email campaigns, and doesn't necessarily represent
		 * current on_sale status for the product
		 * Plugins checks every day to find on_sale products, and to schedule email sending
		 *
		 * @param string $context Context.
		 * @return bool Whether product was on sale during last check that plugin performed
		 */
		public function is_on_sale( $context = 'view' ) {
			return (bool) $this->get_prop( 'on_sale', $context );
		}

		/**
		 * Returns url to remove item from wishlist
		 *
		 * @return string Remove url.
		 */
		public function get_remove_url() {
			$base_url = $this->get_wishlist()->get_url();

			return apply_filters( 'yith_wcwl_wishlist_item_remove_url', wp_nonce_url( add_query_arg( 'remove_from_wishlist', $this->get_product_id(), $base_url ), 'remove_from_wishlist' ), $this );
		}

		/* === SETTERS === */

		/**
		 * Set wishlist ID for current item
		 *
		 * @param int $wishlist_id Wishlist ID.
		 */
		public function set_wishlist_id( $wishlist_id ) {
			$this->set_prop( 'wishlist_id', $wishlist_id );

			$wishlist = yith_wcwl_get_wishlist( $wishlist_id );

			if ( $wishlist && $this->get_user_id() !== $wishlist->get_user_id() ) {
				$this->set_user_id( $wishlist->get_user_id() );
			}
		}

		/**
		 * Set product ID for current item
		 *
		 * @param int $product_id Product ID.
		 */
		public function set_product_id( $product_id ) {
			$product_id = yith_wcwl_object_id( $product_id, 'product', true, 'default' );

			if ( ! empty( $this->product ) ) {
				$this->product = null;
			}

			$this->set_prop( 'product_id', $product_id );
		}

		/**
		 * Set quantity for current item
		 *
		 * @param int $quantity Quantity.
		 */
		public function set_quantity( $quantity ) {
			$this->set_prop( 'quantity', $quantity );
		}

		/**
		 * Set user ID for current item
		 *
		 * @param int $user_id User ID.
		 */
		public function set_user_id( $user_id ) {
			$this->set_prop( 'user_id', $user_id );
		}

		/**
		 * Set date added for current item
		 *
		 * @param int $date_added Date added.
		 */
		public function set_date_added( $date_added ) {
			$this->set_date_prop( 'date_added', $date_added );
		}

		/**
		 * Set position in wishlist for current item
		 *
		 * @param int $position Position.
		 */
		public function set_position( $position ) {
			$this->set_prop( 'position', (int) $position );
		}

		/**
		 * Set original price
		 *
		 * @param double $original_price Price.
		 */
		public function set_original_price( $original_price ) {
			$this->set_prop( 'original_price', $original_price );
		}

		/**
		 * Set original currency
		 *
		 * @param string $original_currency Currency.
		 */
		public function set_original_currency( $original_currency ) {
			$this->set_prop( 'original_currency', $original_currency );
		}

		/**
		 * Set on sale value
		 *
		 * @param bool $on_sale Whether product was found as on sale.
		 * @return void
		 */
		public function set_on_sale( $on_sale ) {
			if ( $this->get_object_read() && $on_sale && $this->is_on_sale() !== $on_sale ) {
				do_action( 'yith_wcwl_item_is_on_sale', $this );
			}

			$this->set_prop( 'on_sale', $on_sale );
		}

		/* === ARRAY ACCESS METHODS === */

		/**
		 * OffsetSet for ArrayAccess.
		 *
		 * @param string $offset Offset.
		 * @param mixed  $value  Value.
		 */
		public function offsetSet( $offset, $value ) {
			$offset = $this->map_legacy_offsets( $offset );

			if ( array_key_exists( $offset, $this->data ) ) {
				$setter = "set_$offset";
				if ( is_callable( array( $this, $setter ) ) ) {
					$this->$setter( $value );
				}
			}
		}

		/**
		 * OffsetUnset for ArrayAccess.
		 *
		 * @param string $offset Offset.
		 */
		public function offsetUnset( $offset ) {
			$offset = $this->map_legacy_offsets( $offset );

			if ( array_key_exists( $offset, $this->data ) ) {
				unset( $this->data[ $offset ] );
			}

			if ( array_key_exists( $offset, $this->changes ) ) {
				unset( $this->changes[ $offset ] );
			}
		}

		/**
		 * OffsetExists for ArrayAccess.
		 *
		 * @param string $offset Offset.
		 * @return bool
		 */
		public function offsetExists( $offset ) {
			$offset = $this->map_legacy_offsets( $offset );

			$getter = "get_$offset";
			if ( is_callable( array( $this, $getter ) ) ) {
				return true;
			}

			return false;
		}

		/**
		 * OffsetGet for ArrayAccess.
		 *
		 * @param string $offset Offset.
		 * @return mixed
		 */
		public function offsetGet( $offset ) {
			$offset = $this->map_legacy_offsets( $offset );

			$getter = "get_$offset";
			if ( is_callable( array( $this, $getter ) ) ) {
				return $this->$getter();
			}

			return null;
		}

		/**
		 * Map legacy indexes to new properties, for ArrayAccess
		 *
		 * @param string $offset Offset to search.
		 * @return string Mapped offset
		 */
		protected function map_legacy_offsets( $offset ) {
			$legacy_offset = $offset;

			if ( 'prod_id' === $offset ) {
				$offset = 'product_id';
			} elseif ( 'dateadded' === $offset ) {
				$offset = 'date_added';
			}

			return apply_filters( 'yith_wcwl_wishlist_item_map_legacy_offsets', $offset, $legacy_offset );
		}
	}
}
class-yith-wcwl-wishlist.php000064400000054651151327635660012204 0ustar00<?php
/**
 * Wishlist class
 *
 * @author YITH
 * @package YITH\Wishlist\Classes\Wishlists
 * @version 3.0.0
 */

if ( ! defined( 'YITH_WCWL' ) ) {
	exit;
} // Exit if accessed directly

if ( ! class_exists( 'YITH_WCWL_Wishlist' ) ) {
	/**
	 * This class describes Wishlist object, and it is meant to be used whenever a wishlist needs to be retrieved
	 * It is an abstract class to account for different type of wishlists; system will determine wishlist type basing
	 * on user visiting the site
	 *
	 * @since 3.0.0
	 */
	class YITH_WCWL_Wishlist extends WC_Data implements ArrayAccess {

		/**
		 * Wishlist token (Unique identifier)
		 *
		 * @var string
		 */
		protected $token = '';

		/**
		 * Wishlist Data array
		 *
		 * @var array
		 */
		protected $data;

		/**
		 * Wishlist items will be stored here, sometimes before they persist in the DB.
		 *
		 * @since 3.0.0
		 * @var array
		 */
		protected $items = array();

		/**
		 * Wishlist items that need deleting are stored here.
		 *
		 * @since 3.0.0
		 * @var array
		 */
		protected $items_to_delete = array();

		/**
		 * Stores meta in cache for future reads.
		 *
		 * A group must be set to to enable caching.
		 *
		 * @var string
		 */
		protected $cache_group = 'wishlists';

		/* === MAGIC METHODS === */

		/**
		 * Constructor
		 *
		 * @param int|string|\YITH_WCWL_Wishlist $wishlist Wishlist identifier.
		 *
		 * @throws Exception When not able to load Data Store class.
		 */
		public function __construct( $wishlist = 0 ) {
			// set default values.
			$this->data = array(
				'privacy'    => apply_filters( 'yith_wcwl_default_wishlist_privacy', 0 ),
				'user_id'    => 0,
				'session_id' => '',
				'name'       => apply_filters( 'yith_wcwl_default_wishlist_name', '' ),
				'slug'       => apply_filters( 'yith_wcwl_default_wishlist_slug', '' ),
				'token'      => '',
				'is_default' => 0,
				'date_added' => '',
				'expiration' => '',
			);

			parent::__construct();

			if ( is_numeric( $wishlist ) && $wishlist > 0 ) {
				$this->set_id( $wishlist );
			} elseif ( $wishlist instanceof self ) {
				$this->set_id( $wishlist->get_id() );
			} elseif ( is_string( $wishlist ) ) {
				$this->set_token( $wishlist );
			} else {
				$this->set_object_read( true );
			}

			$this->data_store = WC_Data_Store::load( 'wishlist' );

			if ( $this->get_id() > 0 || ! empty( $this->get_token() ) ) {
				$this->data_store->read( $this );
			}
		}

		/* === HELPERS === */

		/**
		 * Return number of items for current wishlist
		 *
		 * @return int Count of items
		 */
		public function count_items() {
			return count( $this->get_items() );
		}

		/**
		 * Check whether wishlist was created for unautheticated user
		 *
		 * @return bool
		 */
		public function is_session_based() {
			return (bool) $this->get_session_id();
		}

		/**
		 * Returns true when wishlist is default
		 *
		 * @return bool Whether wishlist is default or not
		 */
		public function is_default() {
			return $this->get_is_default();
		}

		/**
		 * Check whether wishlist was created for authenticated user
		 *
		 * @return bool
		 */
		public function has_owner() {
			return (bool) $this->get_user_id();
		}

		/**
		 * Check if current user is owner of this wishlist (works both for authenticated users & guests)
		 *
		 * @param string|int|bool $current_user Optional user identifier, in the form of a User ID or session id; false for default.
		 * @return bool
		 */
		public function is_current_user_owner( $current_user = false ) {
			$user_id    = $this->get_user_id();
			$session_id = $this->get_session_id();

			if ( $current_user && ( (int) $current_user === $user_id || $current_user === $session_id ) ) {
				return true;
			}

			if ( $this->has_owner() && is_user_logged_in() && get_current_user_id() === $user_id ) {
				return true;
			}

			if ( $this->is_session_based() && YITH_WCWL_Session()->maybe_get_session_id() === $session_id ) {
				return true;
			}

			return false;
		}

		/**
		 * Check whether current user can perform a specific action on wishlist
		 *
		 * Accepted capabilities:
		 * * view
		 * * update_wishlist
		 * * add_to_wishlist
		 * * remove_from_wishlist
		 * * move_to_another_wishlist
		 * * ask_an_estimate
		 * * sort_items
		 * * update_quantity
		 * * download_pdf
		 *
		 * @param string          $capability Capability to check; default "view".
		 * @param string|int|bool $current_user Optional user identifier, in the form of a User ID or session id; false for default.
		 * @return bool
		 */
		public function current_user_can( $capability = 'view', $current_user = false ) {
			// admin can do anything by default.
			if ( is_user_logged_in() && current_user_can( 'manage_woocommerce' ) && apply_filters( 'yith_wcwl_admin_can', true, $capability, $current_user, $this ) ) {
				return true;
			}

			// for other users, perform checks over capability required.
			switch ( $capability ) {
				case 'view':
					$can = $this->is_current_user_owner( $current_user );

					if ( ! $can && $this->has_privacy( array( 'public', 'shared' ) ) ) {
						$can = true;
					}
					break;
				default:
					$can = $this->is_current_user_owner( $current_user );
					break;
			}

			return apply_filters( 'yith_wcwl_current_user_can', $can, $capability, $current_user, $this );
		}

		/* === GETTERS === */

		/**
		 * Get wishlist token
		 *
		 * @return string Wishlist unique token
		 */
		public function get_token() {
			return $this->token;
		}

		/**
		 * Get privacy visibility
		 *
		 * @param string $context Context.
		 * @return int Wishlist visibility (0 => public, 1 => shared, 2 => private)
		 */
		public function get_privacy( $context = 'view' ) {
			return (int) $this->get_prop( 'privacy', $context );
		}

		/**
		 * Get formatted privacy name
		 *
		 * @param string $context Context.
		 * @return string Formatted privacy value
		 */
		public function get_formatted_privacy( $context = 'view' ) {
			$privacy           = $this->get_privacy( $context );
			$formatted_privacy = yith_wcwl_get_privacy_label( $privacy );

			return apply_filters( 'yith_wcwl_wishlist_formatted_privacy', $formatted_privacy, $privacy, $this, $context );
		}

		/**
		 * Checks if current wishlist has a specific privacy value
		 * Method will accept both numeric privacy values and privacy labels
		 *
		 * @param int|string|array $privacy Privacy value (0|1|2) or label (public|shared|private), or array of acceptable values.
		 * @return bool Whether wishlist matched privacy test
		 */
		public function has_privacy( $privacy ) {
			$wishlist_privacy = $this->get_privacy( 'edit' );
			$has_privacy      = false;

			if ( is_array( $privacy ) && ! empty( $privacy ) ) {
				foreach ( $privacy as $test_value ) {
					// return true if wishlist has any of the privacy value submitted.
					if ( $this->has_privacy( $test_value ) ) {
						return true;
					}
				}
			} elseif ( is_string( $privacy ) ) {
				$has_privacy = yith_wcwl_get_privacy_value( $privacy ) === $wishlist_privacy;
			} else {
				$has_privacy = $privacy === $wishlist_privacy;
			}

			return $has_privacy;
		}

		/**
		 * Get owner id
		 *
		 * @param string $context Context.
		 * @return int Wishlist owner id
		 */
		public function get_user_id( $context = 'view' ) {
			return (int) $this->get_prop( 'user_id', $context );
		}

		/**
		 * Get session id
		 *
		 * @param string $context Context.
		 * @return int Wishlist owner id
		 */
		public function get_session_id( $context = 'view' ) {
			return $this->get_prop( 'session_id', $context );
		}

		/**
		 * Get wishlist name
		 *
		 * @param string $context Context.
		 * @return string Wishlist name
		 */
		public function get_name( $context = 'view' ) {
			return wc_clean( stripslashes( $this->get_prop( 'name', $context ) ) );
		}

		/**
		 * Get wishlist formatted name
		 *
		 * @param string $context Context.
		 * @return string Formatted name
		 */
		public function get_formatted_name( $context = 'view' ) {
			$name = $this->get_name( $context );

			if ( $this->is_default() && ! $name ) {
				$name = apply_filters( 'yith_wcwl_default_wishlist_formatted_title', get_option( 'yith_wcwl_wishlist_title' ) );
			}

			return apply_filters( 'yith_wcwl_wishlist_formatted_title', $name );
		}

		/**
		 * Get wishlist slug
		 *
		 * @param string $context Context.
		 * @return string Wishlist slug
		 */
		public function get_slug( $context = 'view' ) {
			return $this->get_prop( 'slug', $context );
		}

		/**
		 * Check if wishlist is default one for the user
		 *
		 * @param string $context Context.
		 * @return bool Whether wishlist is default one or not
		 */
		public function get_is_default( $context = 'view' ) {
			return (bool) $this->get_prop( 'is_default', $context );
		}

		/**
		 * Get wishlist date added
		 *
		 * @param string $context Context.
		 * @return \WC_DateTime|string Wishlist date of creation
		 */
		public function get_date_added( $context = 'view' ) {
			$date_added = $this->get_prop( 'date_added', $context );

			if ( $date_added && 'view' === $context ) {
				return $date_added->date_i18n( 'Y-m-d H:i:s' );
			}

			return $date_added;
		}

		/**
		 * Get formatted wishlist date added
		 *
		 * @param string $format Date format (if empty, WP date format will be applied).
		 * @return string Wishlist date of creation
		 */
		public function get_date_added_formatted( $format = '' ) {
			$date_added = $this->get_date_added( 'edit' );

			if ( $date_added ) {
				$format = $format ? $format : get_option( 'date_format' );
				return $date_added->date_i18n( $format );
			}

			return '';
		}

		/**
		 * Get wishlist date added
		 *
		 * @param string $context Context.
		 * @return \WC_DateTime|string Wishlist date of creation
		 */
		public function get_expiration( $context = 'view' ) {
			$expiration = $this->get_prop( 'expiration', $context );

			if ( $expiration && 'view' === $context ) {
				return $expiration->date_i18n( 'Y-m-d H:i:s' );
			}

			return $expiration;
		}

		/**
		 * Get formatted wishlist expiration added
		 *
		 * @param string $format Date format (if empty, WP date format will be applied).
		 * @return string Wishlist date of expiration
		 */
		public function get_expiration_formatted( $format = '' ) {
			$expiration = $this->get_expiration( 'edit' );

			if ( $expiration ) {
				$format = $format ? $format : get_option( 'date_format' );
				return $expiration->date_i18n( $format );
			}

			return '';
		}

		/**
		 * Retrieve email of the owner of the wishlist (if any)
		 *
		 * @return string|bool Email of wishlist owner (or false, if no registered owner)
		 */
		public function get_user_email() {
			$user_id = $this->get_user_id();

			if ( ! $user_id ) {
				return false;
			}

			$user = get_userdata( $user_id );
			return $user->user_email;
		}

		/**
		 * Retrieve first name of the owner of the wishlist (if any)
		 *
		 * @return string|bool First name of wishlist owner (or false, if no registered owner)
		 */
		public function get_user_first_name() {
			$user_id = $this->get_user_id();

			if ( ! $user_id ) {
				return false;
			}

			$user = get_userdata( $user_id );
			return $user->first_name;
		}

		/**
		 * Retrieve last name of the owner of the wishlist (if any)
		 *
		 * @return string|bool Last name of wishlist owner (or false, if no registered owner)
		 */
		public function get_user_last_name() {
			$user_id = $this->get_user_id();

			if ( ! $user_id ) {
				return false;
			}

			$user = get_userdata( $user_id );
			return $user->last_name;
		}

		/**
		 * Return owner formatted name
		 *
		 * @return string User formatted name
		 */
		public function get_user_formatted_name() {
			$user_id = $this->get_user_id();

			if ( ! $user_id ) {
				return false;
			}

			$user       = get_userdata( $user_id );
			$first_name = $user->first_name;
			$last_name  = $user->last_name;
			$email      = $user->user_email;

			$formatted_name = $email;

			if ( ! empty( $first_name ) || ! empty( $last_name ) ) {
				$formatted_name .= " <{$first_name} {$last_name}>";
			}

			return $formatted_name;
		}

		/**
		 * Return url to visit wishlist
		 *
		 * @return string Url to the wishlist
		 */
		public function get_url() {
			return YITH_WCWL()->get_wishlist_url( 'view/' . $this->get_token() );
		}

		/**
		 * Get url to download the wishlist as PDF file
		 *
		 * @return string Url to download
		 */
		public function get_download_url() {
			return apply_filters( 'yith_wcwl_wishlist_download_url', wp_nonce_url( add_query_arg( 'download_wishlist', $this->get_id() ), 'download_wishlist', 'download_nonce' ), $this );
		}

		/**
		 * Get url to delete the wishlist
		 *
		 * @return string Url to delete the wishlist
		 */
		public function get_delete_url() {
			return apply_filters( 'yith_wcwl_wishlist_delete_url', wp_nonce_url( add_query_arg( 'wishlist_id', $this->get_id(), YITH_WCWL()->get_wishlist_url( 'manage' ) ), 'yith_wcwl_delete_action', 'yith_wcwl_delete' ), $this );
		}

		/* === SETTERS === */

		/**
		 * Set wishlist token
		 *
		 * @param string $token Wishlist unique token.
		 */
		public function set_token( $token ) {
			$this->token = (string) $token;
		}

		/**
		 * Set privacy visibility
		 *
		 * @param int $privacy Wishlist visibility (0 => public, 1 => shared, 2 => private).
		 */
		public function set_privacy( $privacy ) {
			$this->set_prop( 'privacy', $privacy );
		}

		/**
		 * Set owner id
		 *
		 * @param int $user_id Wishlist owner id.
		 */
		public function set_user_id( $user_id ) {
			$this->set_prop( 'user_id', $user_id );
		}

		/**
		 * Set session id
		 *
		 * @param int $session_id Wishlist session.
		 */
		public function set_session_id( $session_id ) {
			$this->set_prop( 'session_id', $session_id );
		}

		/**
		 * Set wishlist name
		 *
		 * @param string $name Wishlist name.
		 */
		public function set_name( $name ) {
			$this->set_prop( 'name', $name );
		}

		/**
		 * Set wishlist slug
		 *
		 * @param string $slug Wishlist slug.
		 */
		public function set_slug( $slug ) {
			$this->set_prop( 'slug', substr( $slug, 0, 200 ) );
		}

		/**
		 * Set if wishlist is default one for the user
		 *
		 * @param bool $is_default Whether wishlist is default one or not.
		 */
		public function set_is_default( $is_default ) {
			$this->set_prop( 'is_default', $is_default );
		}

		/**
		 * Set wishlist date added
		 *
		 * @param int|string $date_added Wishlist date of creation (timestamp or date).
		 */
		public function set_date_added( $date_added ) {
			$this->set_date_prop( 'date_added', $date_added );
		}

		/**
		 * Set wishlist date added
		 *
		 * @param int|string $expiration Wishlist date of creation (timestamp or date).
		 */
		public function set_expiration( $expiration ) {
			$this->set_date_prop( 'expiration', $expiration );
		}

		/**
		 * Sets a prop for a setter method.
		 *
		 * This stores changes in a special array so we can track what needs saving
		 * the the DB later.
		 *
		 * @since 3.0.0
		 * @param string $prop Name of prop to set.
		 * @param mixed  $value Value of the prop.
		 */
		protected function set_prop( $prop, $value ) {
			parent::set_prop( $prop, $value );

			if ( 'name' === $prop ) {
				$this->set_slug( sanitize_title_with_dashes( $this->get_name() ) );
			}
		}

		/* === CRUD METHODS === */

		/**
		 * Save data to the database.
		 *
		 * @since 3.0.0
		 * @return int order ID
		 */
		public function save() {
			if ( $this->data_store ) {
				// Trigger action before saving to the DB. Allows you to adjust object props before save.
				do_action( 'woocommerce_before_' . $this->object_type . '_object_save', $this, $this->data_store );

				if ( $this->get_id() ) {
					$this->data_store->update( $this );
				} else {
					$this->data_store->create( $this );
				}
			}
			$this->save_items();
			return $this->get_id();
		}

		/* === ITEM METHODS === */

		/**
		 * Returns true when wishlist is non-empty
		 *
		 * @return bool Whether wishlist is empty or not
		 */
		public function has_items() {
			$items = $this->get_items();

			return ! empty( $items );
		}

		/**
		 * Return an array of items/products within this wishlist.
		 *
		 * @param int $limit  When differs from 0, method will return at most this number of items.
		 * @param int $offset When @see $limit is set, this will be used as offset to retrieve items.
		 *
		 * @return YITH_WCWL_Wishlist_Item[]
		 */
		public function get_items( $limit = 0, $offset = 0 ) {
			if ( ! $this->items ) {
				$this->items = array_filter( $this->data_store->read_items( $this ) );
			}

			$items = apply_filters( 'yith_wcwl_wishlist_get_items', $this->items, $this );

			if ( $limit ) {
				$items = array_slice( $items, $offset, $limit );
			}

			return $items;
		}

		/**
		 * Save all wishlist items which are part of this wishlist.
		 *
		 * @return void
		 */
		protected function save_items() {
			foreach ( $this->items_to_delete as $item ) {
				$item->delete();
			}
			$this->items_to_delete = array();

			// Add/save items.
			foreach ( $this->items as $product_id => $item ) {
				if ( $item->get_wishlist_id() !== $this->get_id() ) {
					$item->set_wishlist_id( $this->get_id() );
				}

				$item->save();
			}
		}

		/**
		 * Check whether a product is already in list
		 *
		 * @param int $product_id Product id.
		 * @return bool Whether product is already in list
		 */
		public function has_product( $product_id ) {
			$product_id = yith_wcwl_object_id( $product_id, 'product', true, 'default' );

			return array_key_exists( $product_id, $this->get_items() );
		}

		/**
		 * Retrieves a product from the list (if set)
		 *
		 * @param int $product_id Product ID.
		 * @return \YITH_WCWL_Wishlist_Item|bool Item on success, false on failure
		 */
		public function get_product( $product_id ) {
			$product_id = yith_wcwl_object_id( $product_id, 'product', true, 'default' );

			if ( ! $this->has_product( $product_id ) ) {
				return false;
			}

			$items = $this->get_items();
			return $items[ $product_id ];
		}

		/**
		 * Add a product to the list
		 *
		 * @param int $product_id Product id.
		 *
		 * @return \YITH_WCWL_Wishlist_Item|bool Item on success; false on failure
		 */
		public function add_product( $product_id ) {
			$product_id = yith_wcwl_object_id( $product_id, 'product', true, 'default' );

			$product = wc_get_product( $product_id );

			if ( ! $product || $this->has_product( $product_id ) ) {
				return false;
			}

			try {
				$item = new YITH_WCWL_Wishlist_Item();
				$item->set_product_id( $product_id );
				$item->set_wishlist_id( $this->get_id() );
				$item->set_user_id( $this->get_user_id() );

				$this->items[ $product_id ] = $item;

				return $item;
			} catch ( Exception $e ) {
				return false;
			}
		}

		/**
		 * Remove product from the list
		 *
		 * @param int $product_id Product id.
		 * @return bool Status of the operation
		 */
		public function remove_product( $product_id ) {
			$product_id = yith_wcwl_object_id( $product_id, 'product', true, 'default' );

			if ( ! $this->has_product( $product_id ) ) {
				return false;
			}

			$this->items_to_delete[] = $this->items[ $product_id ];
			unset( $this->items[ $product_id ] );

			return true;
		}

		/**
		 * Check whether an item is already in list (by item id)
		 *
		 * @param int $item_id Item id.
		 * @return bool Whether item is already in list
		 */
		public function has_item( $item_id ) {
			return in_array( (int) $item_id, array_column( $this->get_items(), 'id' ), true );
		}

		/**
		 * Retrieve a specific item of the list, by item id
		 *
		 * @param int $item_id Item id.
		 * @return \YITH_WCWL_Wishlist_Item|bool Item to retrieve, or false on error
		 */
		public function get_item( $item_id ) {
			if ( ! $this->has_item( $item_id ) ) {
				return false;
			}

			$items = array_combine( array_column( $this->get_items(), 'id' ), $this->get_items() );
			return $items[ $item_id ];
		}

		/**
		 * Add new item to the list
		 *
		 * @param \YITH_WCWL_Wishlist_Item $item Wishlist item.
		 * @return \YITH_WCWL_Wishlist_Item|bool Item on success; false on failure
		 */
		public function add_item( $item ) {
			if ( ! $item->get_product_id() || $this->has_item( $item->get_id() ) ) {
				return false;
			}

			$item->set_wishlist_id( $this->get_id() );
			$item->set_user_id( $this->get_user_id() );

			$this->items[ $item->get_product_id() ] = $item;
			return $item;
		}

		/**
		 * Remove item from the list
		 *
		 * @param int $item_id Item id.
		 * @return bool status of the operation
		 */
		public function remove_item( $item_id ) {
			if ( ! $this->has_item( $item_id ) ) {
				return false;
			}

			$item = $this->get_item( $item_id );

			$this->items_to_delete[] = $item;
			unset( $this->items[ $item->get_product_id() ] );

			return true;
		}

		/* === ARRAY ACCESS METHODS === */

		/**
		 * OffsetSet for ArrayAccess.
		 *
		 * @param string $offset Offset.
		 * @param mixed  $value  Value.
		 */
		public function offsetSet( $offset, $value ) {
			$offset = $this->map_legacy_offsets( $offset );

			if ( array_key_exists( $offset, $this->data ) ) {
				$setter = "set_$offset";
				if ( is_callable( array( $this, $setter ) ) ) {
					$this->$setter( $value );
				}
			}
		}

		/**
		 * OffsetUnset for ArrayAccess.
		 *
		 * @param string $offset Offset.
		 */
		public function offsetUnset( $offset ) {
			$offset = $this->map_legacy_offsets( $offset );

			if ( array_key_exists( $offset, $this->data ) ) {
				unset( $this->data[ $offset ] );
			}

			if ( array_key_exists( $offset, $this->changes ) ) {
				unset( $this->changes[ $offset ] );
			}
		}

		/**
		 * OffsetExists for ArrayAccess.
		 *
		 * @param string $offset Offset.
		 * @return bool
		 */
		public function offsetExists( $offset ) {
			$offset = $this->map_legacy_offsets( $offset );

			$getter = "get_$offset";
			if ( is_callable( array( $this, $getter ) ) ) {
				return true;
			}

			return false;
		}

		/**
		 * OffsetGet for ArrayAccess.
		 *
		 * @param string $offset Offset.
		 * @return mixed
		 */
		public function offsetGet( $offset ) {
			$offset = $this->map_legacy_offsets( $offset );

			$getter = "get_$offset";
			if ( is_callable( array( $this, $getter ) ) ) {
				return $this->$getter();
			}

			return null;
		}

		/**
		 * Map legacy indexes to new properties, for ArrayAccess
		 *
		 * @param string $offset Offset to search.
		 * @return string Mapped offset
		 */
		protected function map_legacy_offsets( $offset ) {
			$legacy_offset = $offset;

			if ( false !== strpos( $offset, 'wishlist_' ) ) {
				$offset = str_replace( 'wishlist_', '', $offset );
			}

			if ( 'dateadded' === $offset ) {
				$offset = 'date_added';
			}

			if ( 'first_name' === $offset ) {
				$offset = 'user_first_name';
			}

			if ( 'last_name' === $offset ) {
				$offset = 'user_last_name';
			}

			return apply_filters( 'yith_wcwl_wishlist_map_legacy_offsets', $offset, $legacy_offset );
		}
	}
}
legacy/functions-yith-wcwl-legacy.php000064400000001733151327635660013742 0ustar00<?php
/**
 * Legacy Functions
 *
 * @author YITH
 * @package YITH\Wishlist\Classes\Legacy
 * @version 3.0.0
 */

if ( ! defined( 'YITH_WCWL' ) ) {
	exit;
} // Exit if accessed directly

if ( ! function_exists( 'YITH_WCWL_Admin_Init' ) ) {
	/**
	 * Deprecated function that used to return admin class single instance
	 *
	 * @return YITH_WCWL_Admin
	 * @since 2.0.0
	 */
	function YITH_WCWL_Admin_Init() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid
		_deprecated_function( __FUNCTION__, '3.0.0', 'YITH_WCWL_Admin' );
		return YITH_WCWL_Admin();
	}
}

if ( ! function_exists( 'YITH_WCWL_Init' ) ) {
	/**
	 * Deprecated function that used to return init class single instance
	 *
	 * @return YITH_WCWL_Frontend
	 * @since 2.0.0
	 */
	function YITH_WCWL_Init() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid
		_deprecated_function( __FUNCTION__, '3.0.0', 'YITH_WCWL_Frontend' );
		return YITH_WCWL_Frontend();
	}
}
class-yith-wcwl-cron.php000064400000004541151327635660011270 0ustar00<?php
/**
 * Wishlist Cron Handler
 *
 * @author YITH
 * @package YITH\Wishlist\Classes
 * @version 3.0.0
 */

if ( ! defined( 'YITH_WCWL' ) ) {
	exit;
} // Exit if accessed directly

if ( ! class_exists( 'YITH_WCWL_Cron' ) ) {
	/**
	 * This class handles cron for wishlist plugin
	 *
	 * @since 3.0.0
	 */
	class YITH_WCWL_Cron {
		/**
		 * Array of events to schedule
		 *
		 * @var array
		 */
		protected $crons = array();

		/**
		 * Single instance of the class
		 *
		 * @var \YITH_WCWL_Cron
		 * @since 3.0.0
		 */
		protected static $instance;

		/**
		 * Constructor
		 *
		 * @return void
		 */
		public function __construct() {
			add_action( 'init', array( $this, 'schedule' ) );
		}

		/**
		 * Returns registered crons
		 *
		 * @return array Array of registered crons ans callbacks
		 */
		public function get_crons() {
			if ( empty( $this->crons ) ) {
				$this->crons = array(
					'yith_wcwl_delete_expired_wishlists' => array(
						'schedule' => 'daily',
						'callback' => array( $this, 'delete_expired_wishlists' ),
					),
				);
			}

			return apply_filters( 'yith_wcwl_crons', $this->crons );
		}

		/**
		 * Schedule events not scheduled yet; register callbacks for each event
		 *
		 * @return void
		 */
		public function schedule() {
			$crons = $this->get_crons();

			if ( ! empty( $crons ) ) {
				foreach ( $crons as $hook => $data ) {

					add_action( $hook, $data['callback'] );

					if ( ! wp_next_scheduled( $hook ) ) {
						wp_schedule_event( time() + MINUTE_IN_SECONDS, $data['schedule'], $hook );
					}
				}
			}
		}

		/**
		 * Delete expired session wishlist
		 *
		 * @return void
		 */
		public function delete_expired_wishlists() {
			try {
				WC_Data_Store::load( 'wishlist' )->delete_expired();
			} catch ( Exception $e ) {
				return;
			}
		}

		/**
		 * Returns single instance of the class
		 *
		 * @return \YITH_WCWL_Cron
		 * @since 3.0.0
		 */
		public static function get_instance() {
			if ( is_null( self::$instance ) ) {
				self::$instance = new self();
			}

			return self::$instance;
		}
	}
}

/**
 * Unique access to instance of YITH_WCWL_Cron class
 *
 * @return \YITH_WCWL_Cron
 * @since 3.0.0
 */
function YITH_WCWL_Cron() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid
	return defined( 'YITH_WCWL_PREMIUM' ) ? YITH_WCWL_Cron_Premium::get_instance() : YITH_WCWL_Cron::get_instance();
}
data-stores/class-yith-wcwl-wishlist-item-data-store.php000064400000053442151327635660017424 0ustar00<?php
/**
 * Wishlist data store
 *
 * @author YITH
 * @package YITH\Wishlist\Classes\DataStores
 * @version 3.0.0
 */

if ( ! defined( 'YITH_WCWL' ) ) {
	exit;
} // Exit if accessed directly

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

if ( ! class_exists( 'YITH_WCWL_Wishlist_Item_Data_Store' ) ) {
	/**
	 * This class implements CRUD methods for wishlists' items
	 *
	 * @since 3.0.0
	 */
	class YITH_WCWL_Wishlist_Item_Data_Store {

		/**
		 * Create a new wishlist item in the database.
		 *
		 * @since 3.0.0
		 * @param \YITH_WCWL_Wishlist_Item $item Wishlist item object.
		 */
		public function create( &$item ) {
			global $wpdb;

			$product_id  = $item->get_original_product_id();
			$wishlist_id = $item->get_wishlist_id();

			if ( ! $product_id || ! $wishlist_id ) {
				return;
			}

			$item_id = YITH_WCWL_Wishlist_Factory::get_wishlist_item_by_product_id( $wishlist_id, $product_id );

			if ( $item_id ) {
				$item->set_id( $item_id );

				$this->update( $item );
				return;
			}

			$columns = array(
				'prod_id'           => '%d',
				'quantity'          => '%d',
				'wishlist_id'       => '%d',
				'position'          => '%d',
				'original_price'    => '%f',
				'original_currency' => '%s',
				'on_sale'           => '%d',
			);
			$values  = array(
				apply_filters( 'yith_wcwl_adding_to_wishlist_product_id', $product_id ),
				apply_filters( 'yith_wcwl_adding_to_wishlist_quantity', $item->get_quantity() ),
				apply_filters( 'yith_wcwl_adding_to_wishlist_wishlist_id', $wishlist_id ),
				apply_filters( 'yith_wcwl_adding_to_wishlist_position', $item->get_position() ),
				apply_filters( 'yith_wcwl_adding_to_wishlist_original_price', $item->get_product_price() ),
				apply_filters( 'yith_wcwl_adding_to_wishlist_original_currency', $item->get_original_currency() ),
				apply_filters( 'yith_wcwl_adding_to_wishlist_on_sale', $item->is_on_sale() ),
			);

			$user_id = $item->get_user_id();

			if ( $user_id ) {
				$columns['user_id'] = '%d';
				$values[]           = apply_filters( 'yith_wcwl_adding_to_wishlist_user_id', $user_id );
			}

			$date_added = $item->get_date_added( 'edit' );

			if ( $date_added ) {
				$columns['dateadded'] = 'FROM_UNIXTIME( %d )';
				$values[]             = apply_filters( 'yith_wcwl_adding_to_wishlist_date_added', $date_added->getTimestamp() );
			}

			$query_columns = implode( ', ', array_map( 'esc_sql', array_keys( $columns ) ) );
			$query_values  = implode( ', ', array_values( $columns ) );
			$query         = "INSERT INTO {$wpdb->yith_wcwl_items} ( {$query_columns} ) VALUES ( {$query_values} ) ";

			$res = $wpdb->query( $wpdb->prepare( $query, $values ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

			if ( $res ) {
				$item->set_id( $wpdb->insert_id );
				$item->apply_changes();
				$this->clear_cache( $item );

				do_action( 'yith_wcwl_new_wishlist_item', $item->get_id(), $item, $item->get_wishlist_id() );
			}
		}

		/**
		 * Read/populate data properties specific to this order item.
		 *
		 * @param WC_Order_Item_Product $item Product order item object.
		 *
		 * @throws Exception When wishlist item is not found.
		 * @since 3.0.0
		 */
		public function read( &$item ) {
			global $wpdb;

			$item->set_defaults();

			// Get from cache if available.
			$data = wp_cache_get( 'item-' . $item->get_id(), 'wishlist-items' );

			if ( false === $data ) {
				$data = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->yith_wcwl_items} WHERE ID = %d LIMIT 1;", $item->get_id() ) );
				wp_cache_set( 'item-' . $item->get_id(), $data, 'wishlist-items' );
			}

			if ( ! $data ) {
				throw new Exception( __( 'Invalid wishlist item.', 'yith-woocommerce-wishlist' ) );
			}

			$item->set_props(
				array(
					'wishlist_id'       => $data->wishlist_id,
					'product_id'        => $data->prod_id,
					'user_id'           => $data->user_id,
					'quantity'          => $data->quantity,
					'date_added'        => $data->dateadded,
					'position'          => $data->position,
					'original_price'    => $data->original_price,
					'original_currency' => $data->original_currency,
					'on_sale'           => $data->on_sale,
				)
			);
			$item->set_object_read( true );
		}

		/**
		 * Update a wishlist item in the database.
		 *
		 * @since 3.0.0
		 * @param YITH_WCWL_Wishlist_Item $item Wishlist item object.
		 */
		public function update( &$item ) {
			if ( ! $item->get_id() ) {
				return;
			}

			$changes = $item->get_changes();

			if ( array_intersect( array( 'quantity', 'wishlist_id', 'product_id', 'user_id', 'position', 'on_sale', 'date_added' ), array_keys( $changes ) ) ) {
				$columns = array(
					'quantity'    => '%d',
					'wishlist_id' => '%d',
					'prod_id'     => '%d',
					'position'    => '%d',
					'on_sale'     => '%d',
					'dateadded'   => 'FROM_UNIXTIME( %d )',
					'user_id'     => $item->get_user_id() ? '%d' : 'NULL',
				);
				$values  = array(
					$item->get_quantity(),
					$item->get_wishlist_id(),
					$item->get_original_product_id(),
					$item->get_position(),
					$item->is_on_sale(),
					$item->get_date_added( 'edit' ) ? $item->get_date_added( 'edit' )->getTimestamp() : time(),
				);

				$user_id = $item->get_user_id();

				if ( $user_id ) {
					$values[] = $user_id;
				}

				$this->update_raw( $columns, $values, array( 'ID' => '%d' ), array( $item->get_id() ) );
			}

			$item->apply_changes();
			$this->clear_cache( $item );

			do_action( 'yith_wcwl_update_wishlist_item', $item->get_id(), $item, $item->get_wishlist_id() );
		}

		/**
		 * Remove a wishlist item from the database.
		 *
		 * @since 3.0.0
		 * @param \YITH_WCWL_Wishlist_Item $item Wishlist item object.
		 */
		public function delete( &$item ) {
			global $wpdb;

			$id = $item->get_id();

			if ( ! $id ) {
				return;
			}

			do_action( 'yith_wcwl_before_delete_wishlist_item', $item->get_id() );

			$wpdb->delete( $wpdb->yith_wcwl_items, array( 'ID' => $item->get_id() ) );

			do_action( 'yith_wcwl_delete_wishlist_item', $item->get_id() );

			$item->set_id( 0 );
			$this->clear_cache( $item );
		}

		/**
		 * Retrieves wishlist items that match a set of conditions
		 *
		 * @param array $args Arguments array; it may contains any of the following:<br/>
		 * [<br/>
		 *     'user_id'             // Owner of the wishlist; default to current user logged in (if any), or false for cookie wishlist<br/>
		 *     'product_id'          // Product to search in the wishlist<br/>
		 *     'wishlist_id'         // wishlist_id for a specific wishlist, false for default, or all for any wishlist<br/>
		 *     'wishlist_token'      // wishlist token, or false as default<br/>
		 *     'wishlist_visibility' // all, visible, public, shared, private<br/>
		 *     'is_default' =>       // whether searched wishlist should be default one <br/>
		 *     'id' => false,        // only for table select<br/>
		 *     'limit' => false,     // pagination param; number of items per page. 0 to get all items<br/>
		 *     'offset' => 0         // pagination param; offset for the current set. 0 to start from the first item<br/>
		 * ].
		 *
		 * @return YITH_WCWL_Wishlist_Item[]
		 */
		public function query( $args = array() ) {
			global $wpdb;

			$default = array(
				'user_id'             => ( is_user_logged_in() ) ? get_current_user_id() : false,
				'session_id'          => ( ! is_user_logged_in() ) ? YITH_WCWL_Session()->maybe_get_session_id() : false,
				'product_id'          => false,
				'wishlist_id'         => false, // wishlist_id for a specific wishlist, false for default, or all for any wishlist.
				'wishlist_token'      => false,
				'wishlist_visibility' => apply_filters( 'yith_wcwl_wishlist_visibility_string_value', 'all' ), // all | visible | public | shared | private.
				'is_default'          => false,
				'on_sale'             => false,
				'id'                  => false, // only for table select..
				'limit'               => false,
				'offset'              => 0,
				'orderby'             => '',
				'order'               => 'DESC',
			);

			// if there is no current wishlist, and user was asking for current one, short-circuit query, as pointless.
			if ( ! is_user_logged_in() && ! YITH_WCWL_Session()->has_session() && ! isset( $args['user_id'] ) && ! isset( $args['session_id'] ) ) {
				return array();
			}

			$args = wp_parse_args( $args, $default );
			extract( $args ); // phpcs:ignore WordPress.PHP.DontExtract

			$sql = "SELECT SQL_CALC_FOUND_ROWS i.*
                    FROM `{$wpdb->yith_wcwl_items}` AS i
                    LEFT JOIN {$wpdb->yith_wcwl_wishlists} AS l ON l.`ID` = i.`wishlist_id`
                    INNER JOIN {$wpdb->posts} AS p ON p.ID = i.prod_id 
                    WHERE 1 AND p.post_type IN ( %s, %s ) AND p.post_status = %s";

			// remove hidden products from result.
			$hidden_products = yith_wcwl_get_hidden_products();

			if ( ! empty( $hidden_products ) && apply_filters( 'yith_wcwl_remove_hidden_products_via_query', true ) ) {
				$sql .= ' AND p.ID NOT IN ( ' . implode( ', ', array_filter( $hidden_products, 'esc_sql' ) ) . ' )';
			}

			$sql_args = array(
				'product',
				'product_variation',
				'publish',
			);

			if ( ! empty( $user_id ) ) {
				$sql       .= ' AND i.`user_id` = %d';
				$sql_args[] = $user_id;
			}

			if ( ! empty( $session_id ) ) {
				$sql       .= ' AND l.`session_id` = %s AND l.`expiration` > NOW()';
				$sql_args[] = $session_id;
			}

			if ( ! empty( $product_id ) ) {
				$product_id = yith_wcwl_object_id( $product_id, 'product', true, 'default' );

				$sql       .= ' AND i.`prod_id` = %d';
				$sql_args[] = $product_id;
			}

			if ( ! empty( $wishlist_id ) && 'all' !== $wishlist_id ) {
				$sql       .= ' AND i.`wishlist_id` = %d';
				$sql_args[] = $wishlist_id;
			} elseif ( ( empty( $wishlist_id ) ) && empty( $wishlist_token ) && empty( $is_default ) ) {
				$sql .= ' AND i.`wishlist_id` IS NULL';
			}

			if ( ! empty( $wishlist_token ) ) {
				$sql       .= ' AND l.`wishlist_token` = %s';
				$sql_args[] = $wishlist_token;
			}

			if ( ! empty( $wishlist_visibility ) && 'all' !== $wishlist_visibility ) {
				switch ( $wishlist_visibility ) {
					case 'visible':
						$sql       .= ' AND ( l.`wishlist_privacy` = %d OR l.`wishlist_privacy` = %d )';
						$sql_args[] = 0;
						$sql_args[] = 1;
						break;
					case 'shared':
						$sql       .= ' AND l.`wishlist_privacy` = %d';
						$sql_args[] = 1;
						break;
					case 'private':
						$sql       .= ' AND l.`wishlist_privacy` = %d';
						$sql_args[] = 2;
						break;
					case 'public':
					default:
						$sql       .= ' AND l.`wishlist_privacy` = %d';
						$sql_args[] = 0;
						break;
				}
			}

			if ( ! empty( $is_default ) ) {
				YITH_WCWL_Wishlist_Factory::generate_default_wishlist();

				$sql       .= ' AND l.`is_default` = %d';
				$sql_args[] = $is_default;
			}

			if ( isset( $on_sale ) && false !== $on_sale ) {
				$sql       .= ' AND i.`on_sale` = %d';
				$sql_args[] = $on_sale;
			}

			if ( ! empty( $id ) ) {
				$sql       .= ' AND `i.ID` = %d';
				$sql_args[] = $id;
			}

			$sql .= ' GROUP BY i.prod_id, l.ID';

			if ( ! empty( $orderby ) ) {
				$order = ! empty( $order ) ? $order : 'DESC';
				$sql  .= ' ORDER BY i.' . esc_sql( $orderby ) . ' ' . esc_sql( $order ) . ', i.position ASC';
			} else {
				$sql .= ' ORDER BY i.position ASC, i.ID DESC';
			}

			if ( ! empty( $limit ) && isset( $offset ) ) {
				$sql       .= ' LIMIT %d, %d';
				$sql_args[] = $offset;
				$sql_args[] = $limit;
			}

			$items = $wpdb->get_results( $wpdb->prepare( $sql, $sql_args ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

			/**
			 * This filter was added to allow developer remove hidden products using a foreach loop, instead of the query
			 * It is required when the store contains a huge number of hidden products, and the resulting query would fail
			 * to be submitted to DBMS because of its size
			 *
			 * This code requires reasonable amount of products in the wishlist
			 * A great number of products retrieved from the main query could easily degrade performance of the overall system
			 *
			 * @since 3.0.7
			 */
			if ( ! empty( $hidden_products ) && ! empty( $items ) && ! apply_filters( 'yith_wcwl_remove_hidden_products_via_query', true ) ) {
				foreach ( $items as $item_id => $item ) {
					if ( ! in_array( $item->prod_id, $hidden_products, true ) ) {
						continue;
					}

					unset( $items[ $item_id ] );
				}
			}

			if ( ! empty( $items ) ) {
				$items = array_map( array( 'YITH_WCWL_Wishlist_Factory', 'get_wishlist_item' ), $items );
			} else {
				$items = array();
			}

			return apply_filters( 'yith_wcwl_get_products', $items, $args );
		}

		/**
		 * Counts items that matches
		 *
		 * @param array $args Same parameters allowed for {@see query} method.
		 * @return int Count of items
		 */
		public function count( $args = array() ) {
			return count( $this->query( $args ) );
		}

		/**
		 * Query items table to retrieve distinct products added to wishlist, with count of occurrences
		 *
		 * @param array $args Arguments array; it may contains any of the following:<br/>
		 * [<br/>
		 *     'product_id'          // Product to search in the wishlist<br/>
		 *     'search' => '',       // search string; will be matched against product name<br/>
		 *     'interval' => '',     // Interval of dates; this should be an associative array, that may contain start_date or end_date<br/>
		 *     'orderby' => 'ID',    // order param; a valid column in the result set<br/>
		 *     'order' => 'desc',    // order param; asc or desc<br/>
		 *     'limit' => false,     // pagination param; number of items per page. 0 to get all items<br/>
		 *     'offset' => 0         // pagination param; offset for the current set. 0 to start from the first item<br/>
		 * ].
		 * @return mixed Result set
		 */
		public function query_products( $args ) {
			global $wpdb;

			$default = array(
				'product_id' => '',
				'search'     => '',
				'interval'   => array(),
				'limit'      => false,
				'offset'     => 0,
				'orderby'    => 'ID',
				'order'      => 'DESC',
			);

			$args = wp_parse_args( $args, $default );
			extract( $args ); // phpcs:ignore WordPress.PHP.DontExtract

			$sql = "SELECT
		            DISTINCT i.prod_id AS id,
		            p.post_title AS post_title,
		            i2.wishlist_count AS wishlist_count
		            FROM {$wpdb->yith_wcwl_items} AS i
		            INNER JOIN {$wpdb->posts} AS p ON p.ID = i.prod_id
		            LEFT JOIN ( 
		                SELECT 
		                COUNT( DISTINCT ID ) AS wishlist_count, 
                        prod_id 
		                FROM {$wpdb->yith_wcwl_items} 
		                GROUP BY prod_id 
	                ) AS i2 ON p.ID = i2.prod_id
		            WHERE 1=1 AND p.post_status = %s";

			$sql_args = array( 'publish' );

			if ( ! empty( $product_id ) ) {
				$sql       .= ' AND i.prod_id = %d';
				$sql_args[] = $product_id;
			}

			if ( ! empty( $search ) ) {
				$sql       .= ' AND p.post_title LIKE %s';
				$sql_args[] = '%' . $search . '%';
			}

			if ( ! empty( $args['interval'] ) && is_array( $args['interval'] ) && ( isset( $args['interval']['start_date'] ) || isset( $args['interval']['end_date'] ) ) ) {
				if ( ! empty( $args['interval']['start_date'] ) ) {
					$sql       .= ' AND i.dateadded >= %s';
					$sql_args[] = $args['interval']['start_date'];
				}

				if ( ! empty( $args['interval']['end_date'] ) ) {
					$sql       .= ' AND i.dateadded <= %s';
					$sql_args[] = $args['interval']['end_date'];
				}
			}

			if ( ! empty( $orderby ) ) {
				$order = ! empty( $order ) ? $order : 'DESC';
				$sql  .= ' ORDER BY ' . esc_sql( $orderby ) . ' ' . esc_sql( $order );
			}

			if ( ! empty( $limit ) && isset( $offset ) ) {
				$sql       .= ' LIMIT %d, %d';
				$sql_args[] = $offset;
				$sql_args[] = $limit;
			}

			$items = $wpdb->get_results( $wpdb->prepare( $sql, $sql_args ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

			return $items;
		}

		/**
		 * Counts total number of distinct products added to wishlist
		 *
		 * @param array $args Same parameters allowed for {@see query_products} method.
		 * @return int Count of items
		 */
		public function count_products( $args ) {
			return count( $this->query_products( $args ) );
		}

		/**
		 * Counts how many distinct users added a product in wishlist
		 *
		 * @param int        $product_id Product id.
		 * @param string|int $user User to use in query; it could be a user id, a session id or 'current' (for current user/session).
		 * @return int Count of times product was added to wishlist
		 */
		public function count_times_added( $product_id, $user = false ) {
			global $wpdb;

			$query_args     = array();
			$user_condition = '';

			if ( $user ) {
				if ( 'current' === $user ) {
					if ( is_user_logged_in() ) {
						$user_condition = ' AND l.`user_id` = %d';
						$query_args[]   = get_current_user_id();
					} elseif ( YITH_WCWL_Session()->has_session() ) {
						$user_condition = ' AND l.`session_id` = %s';
						$query_args[]   = YITH_WCWL_Session()->get_session_id();
					} else {
						return 0;
					}
				} elseif ( is_int( $user ) ) {
					$user_condition = ' AND l.`user_id` = %d';
					$query_args[]   = $user;
				} elseif ( is_string( $user ) ) {
					$user_condition = ' AND l.`session_id` = %s';
					$query_args[]   = $user;
				}
			}

			$query = "SELECT 
       				      COUNT( DISTINCT( v.`u_id` ) ) 
					  FROM ( 
					      SELECT 
					          ( CASE WHEN l.`user_id` IS NULL THEN l.`session_id` ELSE l.`user_id` END) AS u_id, 
					          l.`ID` as wishlist_id 
					      FROM {$wpdb->yith_wcwl_wishlists} AS l 
					      WHERE ( l.`expiration` > NOW() OR l.`expiration` IS NULL ) {$user_condition}
				      ) as v 
				      LEFT JOIN {$wpdb->yith_wcwl_items} AS i USING( wishlist_id ) 
					  WHERE i.`prod_id` = %d";

			$query_args[] = $product_id;

			$res = $wpdb->get_var( $wpdb->prepare( $query, $query_args ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

			return (int) $res;
		}

		/**
		 * Raw update method; useful when it is needed to update a bunch of items
		 *
		 * @param array $columns Array of columns to update, in the following format: 'column_id' => 'column_type'.
		 * @param array $column_values Array of values to apply to the query; must have same number of elements of columns, and they must respect defined tpe.
		 * @param array $conditions Array of where conditions, in the following format: 'column_id' => 'columns_type'.
		 * @param array $conditions_values Array of values to apply to where condition; must have same number of elements of columns, and they must respect defined type.
		 * @param bool  $clear_caches Whether system should clear caches (this is optional since other methods may want to run more optimized clear).
		 *
		 * @return void
		 */
		public function update_raw( $columns, $column_values, $conditions = array(), $conditions_values = array(), $clear_caches = false ) {
			global $wpdb;

			// calculate where statement.
			$query_where = '';

			if ( ! empty( $conditions ) ) {
				$query_where = array();

				foreach ( $conditions as $column => $value ) {
					$query_where[] = $column . '=' . $value;
				}

				$query_where = ' WHERE ' . implode( ' AND ', $query_where );
			}

			// retrieves wishlists that will be affected by the changes.
			if ( $clear_caches ) {
				$query = "SELECT ID FROM {$wpdb->yith_wcwl_items} {$query_where}";
				$query = $conditions ? $wpdb->prepare( $query, $conditions_values ) : $query; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
				$ids   = $wpdb->get_col( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			}

			// calculate set statement.
			$query_columns = array();

			foreach ( $columns as $column => $value ) {
				$query_columns[] = $column . '=' . $value;
			}

			$query_columns = implode( ', ', $query_columns );

			// build query, and execute it.
			$query  = "UPDATE {$wpdb->yith_wcwl_items} SET {$query_columns} {$query_where}";
			$values = $conditions ? array_merge( $column_values, $conditions_values ) : $column_values;

			$wpdb->query( $wpdb->prepare( $query, $values ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

			// clear cache for updated items.
			if ( $clear_caches && $ids ) {
				foreach ( $ids as $id ) {
					$this->clear_cache( $id );
				}
			}
		}

		/**
		 * Clear meta cache.
		 *
		 * @param YITH_WCWL_Wishlist_Item|int $item Wishlist item object, or id of the item.
		 */
		public function clear_cache( &$item ) {
			if ( ! $item instanceof YITH_WCWL_Wishlist_Item ) {
				$item = YITH_WCWL_Wishlist_Factory::get_wishlist_item( $item );
			}

			wp_cache_delete( 'item-' . $item->get_id(), 'wishlist-items' );
			wp_cache_delete( 'wishlist-items-' . $item->get_wishlist_id(), 'wishlists' );
			wp_cache_delete( 'wishlist-items-' . $item->get_origin_wishlist_id(), 'wishlists' );
		}

		/* === MISC === */

		/**
		 * Here we collected all methods related to db implementation of the items
		 * They can be used without creating an instance of the Data Store, and are
		 * listed here just for
		 */

		/**
		 * Alter join section of the query, for ordering purpose
		 *
		 * @param string $join Join statement of the query.
		 * @return string
		 * @since 2.0.0
		 */
		public static function filter_join_for_wishlist_count( $join ) {
			global $wpdb;
			$join .= " LEFT JOIN ( SELECT COUNT(*) AS wishlist_count, prod_id FROM {$wpdb->yith_wcwl_items} GROUP BY prod_id ) AS i ON ID = i.prod_id";
			return $join;
		}

		/**
		 * Alter orderby section of the query, for ordering purpose
		 *
		 * @param string $orderby Orderby statement of the query.
		 * @return string
		 * @since 2.0.0
		 */
		public static function filter_orderby_for_wishlist_count( $orderby ) {
			$order   = isset( $_REQUEST['order'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['order'] ) ) : 'ASC'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$orderby = 'i.wishlist_count ' . $order;

			return $orderby;
		}
	}
}

// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
data-stores/class-yith-wcwl-wishlist-data-store.php000064400000105722151327635660016467 0ustar00<?php
/**
 * Wishlist data store
 *
 * @author YITH
 * @package YITH\Wishlist\Classes\DataStores
 * @version 3.0.0
 */

if ( ! defined( 'YITH_WCWL' ) ) {
	exit;
} // Exit if accessed directly

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

if ( ! class_exists( 'YITH_WCWL_Wishlist_Data_Store' ) ) {
	/**
	 * This class implements CRUD methods for wishlists
	 *
	 * @since 3.0.0
	 */
	class YITH_WCWL_Wishlist_Data_Store {

		/**
		 * Create a new wishlist and stores it on DB
		 *
		 * @param \YITH_WCWL_Wishlist $wishlist Wishlist to create.
		 */
		public function create( &$wishlist ) {
			global $wpdb;

			// set token if missing.
			if ( ! $wishlist->get_token() ) {
				$wishlist->set_token( $this->generate_token() );
			}

			// set slug if missing.
			$wishlist_slug = $wishlist->get_slug();

			if ( ! $wishlist_slug ) {
				$wishlist_slug = sanitize_title_with_dashes( $wishlist->get_name() );
				$wishlist->set_slug( $wishlist_slug );
			}

			// set date added if missing.
			if ( ! $wishlist->get_date_added() ) {
				$wishlist->set_date_added( gmdate( 'Y-m-d H:i:s' ) );
			}

			// set default, if needed.
			if ( $this->should_be_default() ) {
				$wishlist->set_is_default( 1 );
			}

			// set always at least an owner.
			if ( ! $wishlist->get_session_id() && ! $wishlist->get_user_id() ) {
				if ( is_user_logged_in() ) {
					$user_id = get_current_user_id();
					$wishlist->set_user_id( apply_filters( 'yith_wcwl_add_wishlist_user_id', $user_id ) );
				} else {
					$session_id = YITH_WCWL_Session()->get_session_id();
					$wishlist->set_session_id( apply_filters( 'yith_wcwl_add_wishlist_session_id', $session_id ) );
				}
			}

			// avoid slug duplicate, adding -n to the end of the string.
			$wishlist->set_slug( $this->generate_slug( $wishlist_slug ) );

			$columns = array(
				'wishlist_privacy' => '%d',
				'wishlist_name'    => '%s',
				'wishlist_slug'    => '%s',
				'wishlist_token'   => '%s',
				'is_default'       => '%d',
			);
			$values  = array(
				apply_filters( 'yith_wcwl_add_wishlist_privacy', $wishlist->get_privacy() ),
				apply_filters( 'yith_wcwl_add_wishlist_name', $wishlist->get_name() ),
				apply_filters( 'yith_wcwl_add_wishlist_slug', $wishlist->get_slug() ),
				apply_filters( 'yith_wcwl_add_wishlist_token', $wishlist->get_token() ),
				apply_filters( 'yith_wcwl_add_wishlist_is_default', $wishlist->get_is_default() ),
			);

			$session_id = $wishlist->get_session_id();

			if ( $session_id ) {
				$columns['session_id'] = '%s';
				$values[]              = apply_filters( 'yith_wcwl_add_wishlist_session_id', $session_id );
			}

			$user_id = $wishlist->get_user_id();

			if ( $user_id ) {
				$columns['user_id'] = '%d';
				$values[]           = apply_filters( 'yith_wcwl_add_wishlist_user_id', $user_id );
			}

			$date_added = $wishlist->get_date_added( 'edit' );

			if ( $date_added ) {
				$columns['dateadded'] = 'FROM_UNIXTIME( %d )';
				$values[]             = apply_filters( 'yith_wcwl_add_wishlist_date_added', $date_added->getTimestamp() );
			}

			$expiration = $wishlist->get_expiration( 'edit' );

			if ( $expiration ) {
				$columns['expiration'] = 'FROM_UNIXTIME( %d )';
				$values[]              = apply_filters( 'yith_wcwl_add_wishlist_expiration', $expiration->getTimestamp() );
			}

			// if session wishlist, set always an expiration.
			$session_expiration = YITH_WCWL_Session()->get_session_expiration();

			if ( isset( $columns['session_id'] ) && ! $expiration && $session_expiration ) {
				$columns['expiration'] = 'FROM_UNIXTIME( %d )';
				$values[]              = apply_filters( 'yith_wcwl_add_wishlist_expiration', $session_expiration );
			}

			$query_columns = implode( ', ', array_map( 'esc_sql', array_keys( $columns ) ) );
			$query_values  = implode( ', ', array_values( $columns ) );
			$query         = "INSERT INTO {$wpdb->yith_wcwl_wishlists} ( {$query_columns} ) VALUES ( {$query_values} ) ";

			$res = $wpdb->query( $wpdb->prepare( $query, $values ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

			if ( $res ) {
				$id = apply_filters( 'yith_wcwl_wishlist_correctly_created', intval( $wpdb->insert_id ) );

				$wishlist->set_id( $id );
				$wishlist->apply_changes();
				$this->clear_caches( $wishlist );

				do_action( 'yith_wcwl_new_wishlist', $wishlist->get_id(), $wishlist );
			}
		}

		/**
		 * Read data from DB for a specific wishlist
		 *
		 * @param \YITH_WCWL_Wishlist $wishlist Wishlist object.
		 * @throws Exception When cannot retrieve specified wishlist.
		 */
		public function read( &$wishlist ) {
			global $wpdb;

			$wishlist->set_defaults();

			$id    = $wishlist->get_id();
			$token = $wishlist->get_token();

			if ( ! $id && ! $token ) {
				throw new Exception( __( 'Invalid wishlist.', 'yith-woocommerce-wishlist' ) );
			}

			$wishlist_data = $wishlist->get_id() ? wp_cache_get( 'wishlist-id-' . $wishlist->get_id(), 'wishlists' ) : wp_cache_get( 'wishlist-token-' . $wishlist->get_token(), 'wishlists' );

			if ( ! $wishlist_data ) {
				// format query to retrieve wishlist.
				$query = false;
				if ( $id ) {
					$query = $wpdb->prepare( "SELECT * FROM {$wpdb->yith_wcwl_wishlists} WHERE ID = %d", $id );
				} elseif ( $token ) {
					$query = $wpdb->prepare( "SELECT * FROM {$wpdb->yith_wcwl_wishlists} WHERE wishlist_token = %s", $token );
				}

				// retrieve wishlist data.
				$wishlist_data = $wpdb->get_row( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

				wp_cache_set( 'wishlist-id-' . $wishlist->get_id(), $wishlist_data, 'wishlists' );
				wp_cache_set( 'wishlist-token-' . $wishlist->get_token(), $wishlist_data, 'wishlists' );
			}

			if ( ! $wishlist_data ) {
				throw new Exception( __( 'Invalid wishlist.', 'yith-woocommerce-wishlist' ) );
			}

			// set wishlist props.
			$wishlist->set_props(
				array(
					'id'         => $wishlist_data->ID,
					'privacy'    => $wishlist_data->wishlist_privacy,
					'user_id'    => $wishlist_data->user_id,
					'session_id' => isset( $wishlist_data->session_id ) ? $wishlist_data->session_id : '',
					'name'       => wc_clean( stripslashes( $wishlist_data->wishlist_name ) ),
					'slug'       => $wishlist_data->wishlist_slug,
					'token'      => $wishlist_data->wishlist_token,
					'is_default' => $wishlist_data->is_default,
					'date_added' => $wishlist_data->dateadded,
					'expiration' => isset( $wishlist_data->expiration ) ? $wishlist_data->expiration : '',
				)
			);
			$wishlist->set_object_read( true );
		}

		/**
		 * Update wishlist data on DB
		 *
		 * @param \YITH_WCWL_Wishlist $wishlist Wishlist to save on db, with $changes property.
		 */
		public function update( &$wishlist ) {
			global $wpdb;

			if ( ! $wishlist->get_id() ) {
				return;
			}

			$data    = $wishlist->get_data();
			$changes = $wishlist->get_changes();

			if ( array_intersect( array( 'user_id', 'session_id', 'slug', 'name', 'token', 'privacy', 'expiration', 'date_added', 'is_default' ), array_keys( $changes ) ) ) {
				$columns = array(
					'wishlist_privacy' => '%d',
					'wishlist_name'    => '%s',
					'wishlist_token'   => '%s',
					'is_default'       => '%d',
					'dateadded'        => 'FROM_UNIXTIME( %d )',
				);
				$values  = array(
					$wishlist->get_privacy(),
					$wishlist->get_name(),
					$wishlist->get_token(),
					$wishlist->get_is_default(),
					$wishlist->get_date_added( 'edit' ) ? $wishlist->get_date_added( 'edit' )->getTimestamp() : time(),
				);

				$session_id = $wishlist->get_session_id();

				if ( $session_id ) {
					$columns['session_id'] = '%s';
					$values[]              = apply_filters( 'yith_wcwl_update_wishlist_session_id', $session_id );
				} else {
					$columns['session_id'] = 'NULL';
				}

				$user_id = $wishlist->get_user_id();

				if ( $user_id ) {
					$columns['user_id'] = '%d';
					$values[]           = apply_filters( 'yith_wcwl_update_wishlist_user_id', $user_id );
				} else {
					$columns['user_id'] = 'NULL';
				}

				$expiration = $wishlist->get_expiration( 'edit' );

				if ( $expiration ) {
					$columns['expiration'] = 'FROM_UNIXTIME( %d )';
					$values[]              = apply_filters( 'yith_wcwl_update_wishlist_expiration', $expiration->getTimestamp() );
				} else {
					$columns['expiration'] = 'NULL';
				}

				$wishlist_slug = $wishlist->get_slug();

				if ( isset( $changes['slug'] ) && $wishlist_slug !== $data['slug'] ) {
					$columns['wishlist_slug'] = '%s';
					$values[]                 = $this->generate_slug( $wishlist_slug );
				}

				$this->update_raw( $columns, $values, array( 'ID' => '%d' ), array( $wishlist->get_id() ) );
			}

			$wishlist->apply_changes();
			$this->clear_caches( $wishlist );

			do_action( 'yith_wcwl_update_wishlist', $wishlist->get_id(), $wishlist );
		}

		/**
		 * Delete a wishlist from DB
		 *
		 * @param \YITH_WCWL_Wishlist $wishlist Wishlist to delete.
		 */
		public function delete( &$wishlist ) {
			global $wpdb;

			$id         = $wishlist->get_id();
			$is_default = $wishlist->is_default();
			$user_id    = $wishlist->get_user_id();
			$session_id = $wishlist->get_session_id();

			if ( ! $id ) {
				return;
			}

			do_action( 'yith_wcwl_before_delete_wishlist', $wishlist->get_id() );

			$this->clear_caches( $wishlist );

			// delete wishlist and all its items.
			$wpdb->delete( $wpdb->yith_wcwl_items, array( 'wishlist_id' => $id ) );
			$wpdb->delete( $wpdb->yith_wcwl_wishlists, array( 'ID' => $id ) );

			do_action( 'yith_wcwl_delete_wishlist', $wishlist->get_id() );

			$wishlist->set_id( 0 );

			do_action( 'yith_wcwl_deleted_wishlist', $id );

			if ( $is_default && ( $user_id || $session_id ) ) {
				// retrieve other lists for the same user.
				$other_lists = $this->query(
					array_merge(
						array(
							'orderby' => 'dateadded',
							'order'   => 'asc',
						),
						$user_id ? array( 'user_id' => $user_id ) : array(),
						$session_id ? array( 'session_id' => $session_id ) : array()
					)
				);

				if ( ! empty( $other_lists ) ) {
					$new_default = $other_lists[0];

					$new_default->set_is_default( 1 );
					$new_default->save();
				}
			}
		}

		/**
		 * Delete expired session wishlist from DB
		 *
		 * @return void
		 */
		public function delete_expired() {
			global $wpdb;

			$wpdb->query( "DELETE FROM {$wpdb->yith_wcwl_items} WHERE wishlist_id IN ( SELECT ID FROM {$wpdb->yith_wcwl_wishlists} WHERE expiration < NOW() and user_id IS NULL )" );
			$wpdb->query( "DELETE FROM {$wpdb->yith_wcwl_wishlists} WHERE expiration < NOW() and user_id IS NULL" );
		}

		/**
		 * Query database to search
		 *
		 * @param array $args Array of parameters used for the query:<br/>
		 * [<br/>
		 *   'id'                   // Wishlist id<br/>
		 *   'user_id'              // User id<br/>
		 *   'session_id'           // Session id<br/>
		 *   'wishlist_slug'        // Wishlist slug, exact match<br/>
		 *   'wishlist_name'        // Wishlist name, like<br/>
		 *   'wishlist_token'       // Wishlist token, exact match<br/>
		 *   'wishlist_visibility'  // all, visible, public, shared, private<br/>
		 *   'user_search'          // String to search within user fields<br/>
		 *   's'                    // String to search within wishlist fields<br/>
		 *   'is_default'           // Whether searched wishlist is default<br/>
		 *   'orderby'              // Any of the table columns<br/>
		 *   'order'                // ASC, DESC<br/>
		 *   'limit'                // Limit of items to retrieve<br/>
		 *   'offset'               // Offset of items to retrieve<br/>
		 *   'show_empty'           // Whether to show empty wishlists<br/>
		 * ].
		 *
		 * @return \YITH_WCWL_Wishlist[] Array of matched wishlists.
		 */
		public function query( $args = array() ) {
			global $wpdb;

			$default = array(
				'id'                  => false,
				'user_id'             => ( is_user_logged_in() ) ? get_current_user_id() : false,
				'session_id'          => ( ! is_user_logged_in() ) ? YITH_WCWL_Session()->maybe_get_session_id() : false,
				'wishlist_slug'       => false,
				'wishlist_name'       => false,
				'wishlist_token'      => false,
				'wishlist_visibility' => apply_filters( 'yith_wcwl_wishlist_visibility_string_value', 'all' ), // all | visible | public | shared | private.
				'user_search'         => false,
				's'                   => false,
				'is_default'          => false,
				'orderby'             => '',
				'order'               => 'DESC',
				'limit'               => false,
				'offset'              => 0,
				'show_empty'          => true,
			);

			// if there is no current wishlist, and user was asking for current one, short-circuit query, as pointless.
			if ( ! is_user_logged_in() && ! YITH_WCWL_Session()->has_session() && ! isset( $args['user_id'] ) && ! isset( $args['session_id'] ) ) {
				return array();
			}

			$args = wp_parse_args( $args, $default );
			extract( $args ); // phpcs:ignore WordPress.PHP.DontExtract

			$sql  = 'SELECT SQL_CALC_FOUND_ROWS l.ID';
			$sql .= " FROM `{$wpdb->yith_wcwl_wishlists}` AS l";

			if ( ! empty( $user_search ) || ! empty( $s ) || ( ! empty( $orderby ) && 'user_login' === $orderby ) ) {
				$sql .= " LEFT JOIN `{$wpdb->users}` AS u ON l.`user_id` = u.ID";
			}

			if ( ! empty( $user_search ) || ! empty( $s ) ) {
				$sql .= " LEFT JOIN `{$wpdb->usermeta}` AS umn ON umn.`user_id` = u.`ID`";
				$sql .= " LEFT JOIN `{$wpdb->usermeta}` AS ums ON ums.`user_id` = u.`ID`";
			}

			$sql     .= ' WHERE 1';
			$sql_args = array();

			if ( ! empty( $user_id ) ) {
				$sql .= ' AND l.`user_id` = %d';

				$sql_args[] = $user_id;
			}

			if ( ! empty( $session_id ) ) {
				$sql .= ' AND l.`session_id` = %s AND l.`expiration` > NOW()';

				$sql_args[] = $session_id;
			}

			if ( ! empty( $user_search ) && empty( $s ) ) {
				$sql .= ' AND (
							umn.`meta_key` = %s AND
							ums.`meta_key` = %s AND
							(
								u.`user_email` LIKE %s OR
								umn.`meta_value` LIKE %s OR
								ums.`meta_value` LIKE %s
							)
						)';

				$search_value = '%' . esc_sql( $user_search ) . '%';

				$sql_args[] = 'first_name';
				$sql_args[] = 'last_name';
				$sql_args[] = $search_value;
				$sql_args[] = $search_value;
				$sql_args[] = $search_value;
			}

			if ( ! empty( $s ) ) {
				$sql .= ' AND ( 
							( 
								umn.`meta_key` = %s AND 
								ums.`meta_key` = %s AND 
								( 
									u.`user_email` LIKE %s OR
									u.`user_login` LIKE %s OR
									umn.`meta_value` LIKE %s OR
									ums.`meta_value` LIKE %s
								) 
							) OR 
							l.wishlist_name LIKE %s OR 
							l.wishlist_slug LIKE %s OR 
							l.wishlist_token LIKE %s 
						)';

				$search_value = '%' . esc_sql( $s ) . '%';

				$sql_args[] = 'first_name';
				$sql_args[] = 'last_name';
				$sql_args[] = $search_value;
				$sql_args[] = $search_value;
				$sql_args[] = $search_value;
				$sql_args[] = $search_value;
				$sql_args[] = $search_value;
				$sql_args[] = $search_value;
				$sql_args[] = $search_value;
			}

			if ( ! empty( $is_default ) ) {
				$sql       .= ' AND l.`is_default` = %d';
				$sql_args[] = $is_default;
			}

			if ( ! empty( $id ) ) {
				$sql       .= ' AND l.`ID` = %d';
				$sql_args[] = $id;
			}

			if ( isset( $wishlist_slug ) && false !== $wishlist_slug ) {
				$sql       .= ' AND l.`wishlist_slug` = %s';
				$sql_args[] = sanitize_title_with_dashes( $wishlist_slug );
			}

			if ( ! empty( $wishlist_token ) ) {
				$sql       .= ' AND l.`wishlist_token` = %s';
				$sql_args[] = $wishlist_token;
			}

			if ( ! empty( $wishlist_name ) ) {
				$sql       .= ' AND l.`wishlist_name` LIKE %s';
				$sql_args[] = '%' . esc_sql( $wishlist_name ) . '%';
			}

			if ( isset( $wishlist_visibility ) && 'all' !== $wishlist_visibility ) {
				if ( ! is_int( $wishlist_visibility ) ) {
					$wishlist_visibility = yith_wcwl_get_privacy_value( $wishlist_visibility );
				}

				$sql       .= ' AND l.`wishlist_privacy` = %d';
				$sql_args[] = $wishlist_visibility;
			}

			if ( empty( $show_empty ) ) {
				$sql .= " AND l.`ID` IN ( SELECT wishlist_id FROM {$wpdb->yith_wcwl_items} )";
			}

			$sql .= ' GROUP BY l.ID';
			$sql .= ' ORDER BY';

			if ( ! empty( $orderby ) && isset( $order ) ) {
				$sql .= ' ' . esc_sql( $orderby ) . ' ' . esc_sql( $order ) . ', ';
			}

			$sql .= ' is_default DESC';

			if ( ! empty( $limit ) && isset( $offset ) ) {
				$sql       .= ' LIMIT %d, %d';
				$sql_args[] = $offset;
				$sql_args[] = $limit;
			}

			if ( ! empty( $sql_args ) ) {
				$sql = $wpdb->prepare( $sql, $sql_args ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			}

			$lists = $wpdb->get_col( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

			if ( ! empty( $lists ) ) {
				$lists = array_map( array( 'YITH_WCWL_Wishlist_Factory', 'get_wishlist' ), $lists );
			} else {
				$lists = array();
			}

			return apply_filters( 'yith_wcwl_get_wishlists', $lists, $args );
		}

		/**
		 * Counts items that matches
		 *
		 * @param array $args Same parameters allowed for {@see query} method.
		 * @return int Count of items
		 */
		public function count( $args = array() ) {
			// retrieve number of items found.
			return count( $this->query( $args ) );
		}

		/**
		 * Search user ids whose wishlists match passed parameters
		 * NOTE: this will only retrieve wishlists for a logged in user, while guests wishlist will be ignored
		 *
		 * @param mixed $args Array of valid arguments<br/>
		 * [<br/>
		 *     'search' // String to match against first name / last name / user login or user email of wishlist owner<br/>
		 *     'limit'  // Pagination param: number of items to show in one page. 0 to show all items<br/>
		 *     'offset' // Pagination param: offset for the current set. 0 to start from the first item<br/>
		 * ].
		 * @return int[] Array of user ids
		 */
		public function search_users( $args = array() ) {
			global $wpdb;

			$default = array(
				'search' => false,
				'limit'  => false,
				'offset' => 0,
			);

			$args = wp_parse_args( $args, $default );
			extract( $args ); // phpcs:ignore WordPress.PHP.DontExtract

			$sql = "SELECT DISTINCT i.user_id
                    FROM {$wpdb->yith_wcwl_items} AS i
                    LEFT JOIN {$wpdb->yith_wcwl_wishlists} AS l ON i.wishlist_id = l.ID";

			if ( ! empty( $search ) ) {
				$sql .= " LEFT JOIN `{$wpdb->users}` AS u ON l.`user_id` = u.ID";
				$sql .= " LEFT JOIN `{$wpdb->usermeta}` AS umn ON umn.`user_id` = u.`ID`";
				$sql .= " LEFT JOIN `{$wpdb->usermeta}` AS ums ON ums.`user_id` = u.`ID`";
			}

			$sql     .= ' WHERE l.wishlist_privacy = %d';
			$sql_args = array( 0 );

			if ( ! empty( $search ) ) {
				$sql .= ' AND ( 
							umn.`meta_key` = %s AND 
							ums.`meta_key` = %s AND 
							( 
								u.`user_email` LIKE %s OR 
								u.`user_login` LIKE %s OR 
								umn.`meta_value` LIKE %s OR 
								ums.`meta_value` LIKE %s
							)
						)';

				$search_string = '%' . esc_sql( $search ) . '%';

				$sql_args[] = 'first_name';
				$sql_args[] = 'last_name';
				$sql_args[] = $search_string;
				$sql_args[] = $search_string;
				$sql_args[] = $search_string;
				$sql_args[] = $search_string;
			}

			if ( ! empty( $limit ) && isset( $offset ) ) {
				$sql .= " LIMIT {$offset}, {$limit}";
			}

			$res = $wpdb->get_col( $wpdb->prepare( $sql, $sql_args ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			return $res;
		}

		/**
		 * Raw update method; useful when it is needed to update a bunch of wishlists
		 *
		 * @param array $columns Array of columns to update, in the following format: 'column_id' => 'column_type'.
		 * @param array $column_values Array of values to apply to the query; must have same number of elements of columns, and they must respect defined tpe.
		 * @param array $conditions Array of where conditions, in the following format: 'column_id' => 'columns_type'.
		 * @param array $conditions_values Array of values to apply to where condition; must have same number of elements of columns, and they must respect defined tpe.
		 * @param bool  $clear_caches Whether to clear stored value before operation or not.
		 * @pram $clear_caches bool Whether system should clear caches (this is optional since other methods may want to run more optimized clear)
		 *
		 * @return void
		 */
		public function update_raw( $columns, $column_values, $conditions = array(), $conditions_values = array(), $clear_caches = false ) {
			global $wpdb;

			// calculate where statement.
			$query_where = '';

			if ( ! empty( $conditions ) ) {
				$query_where = array();

				foreach ( $conditions as $column => $value ) {
					$query_where[] = $column . '=' . $value;
				}

				$query_where = ' WHERE ' . implode( ' AND ', $query_where );
			}

			// retrieves wishlists that will be affected by the changes.
			if ( $clear_caches ) {
				$query = "SELECT ID FROM {$wpdb->yith_wcwl_wishlists} {$query_where}";
				$query = $conditions ? $wpdb->prepare( $query, $conditions_values ) : $query; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
				$ids   = $wpdb->get_col( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			}

			// calculate set statement.
			$query_columns = array();

			foreach ( $columns as $column => $value ) {
				$query_columns[] = $column . '=' . $value;
			}

			$query_columns = implode( ', ', $query_columns );

			// build query, and execute it.
			$query  = "UPDATE {$wpdb->yith_wcwl_wishlists} SET {$query_columns} {$query_where}";
			$values = $conditions ? array_merge( $column_values, $conditions_values ) : $column_values;

			$wpdb->query( $wpdb->prepare( $query, $values ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

			// clear cache for updated items.
			if ( $clear_caches && $ids ) {
				foreach ( $ids as $id ) {
					$this->clear_caches( $id );
				}
			}
		}

		/**
		 * Retrieve all items for the wishlist
		 *
		 * @param \YITH_WCWL_Wishlist $wishlist Wishlist object.
		 * @return \YITH_WCWL_Wishlist_Item[] Array or Wishlist items for the wishlist
		 */
		public function read_items( $wishlist ) {
			global $wpdb;

			// Get from cache if available.
			$items = 0 < $wishlist->get_id() ? wp_cache_get( 'wishlist-items-' . $wishlist->get_id(), 'wishlists' ) : false;

			if ( false === $items ) {
				$query = "SELECT i.* FROM {$wpdb->yith_wcwl_items} as i INNER JOIN {$wpdb->posts} as p on i.prod_id = p.ID WHERE wishlist_id = %d AND p.post_type IN ( %s, %s ) AND p.post_status = %s";

				// remove hidden products from result.
				$hidden_products = yith_wcwl_get_hidden_products();

				if ( ! empty( $hidden_products ) && apply_filters( 'yith_wcwl_remove_hidden_products_via_query', true ) ) {
					$query .= ' AND prod_id NOT IN ( ' . implode( ', ', array_filter( $hidden_products, 'esc_sql' ) ) . ' )';
				}

				// order by statement.
				$query .= ' ORDER BY position ASC, ID DESC;';

				$items = $wpdb->get_results(
					$wpdb->prepare(
						$query, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
						array(
							$wishlist->get_id(),
							'product',
							'product_variation',
							'publish',
						)
					)
				);

				/**
				 * This filter was added to allow developer remove hidden products using a foreach loop, instead of the query
				 * It is required when the store contains a huge number of hidden products, and the resulting query would fail
				 * to be submitted to DBMS because of its size
				 *
				 * This code requires reasonable amount of products in the wishlist
				 * A great number of products retrieved from the main query could easily degrade performance of the overall system
				 *
				 * @since 3.0.7
				 */
				if ( ! empty( $hidden_products ) && ! empty( $items ) && ! apply_filters( 'yith_wcwl_remove_hidden_products_via_query', true ) ) {
					foreach ( $items as $item_id => $item ) {
						if ( ! in_array( $item->prod_id, $hidden_products, true ) ) {
							continue;
						}

						unset( $items[ $item_id ] );
					}
				}

				foreach ( $items as $item ) {
					wp_cache_set( 'item-' . $item->ID, $item, 'wishlist-items' );
				}

				if ( 0 < $wishlist->get_id() ) {
					wp_cache_set( 'wishlist-items-' . $wishlist->get_id(), $items, 'wishlists' );
				}
			}

			if ( ! empty( $items ) ) {
				$items = array_map( array( 'YITH_WCWL_Wishlist_Factory', 'get_wishlist_item' ), array_combine( wp_list_pluck( $items, 'prod_id' ), $items ) );
			} else {
				$items = array();
			}

			return apply_filters( 'yith_wcwl_get_products', $items, array( 'wishlist_id' => $wishlist->get_id() ) );
		}

		/**
		 * Delete all items from the wishist
		 *
		 * @param \YITH_WCWL_Wishlist $wishlist Wishlist object.
		 * @return void
		 */
		public function delete_items( $wishlist ) {
			global $wpdb;

			$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->yith_wcwl_items} WHERE wishlist_id = %d", $wishlist->get_id() ) );

			$this->clear_caches( $wishlist );
		}

		/**
		 * Generate default token for the wishlist
		 *
		 * @return string Wishlist token
		 */
		public function generate_token() {
			global $wpdb;

			$sql = "SELECT COUNT(*) FROM `{$wpdb->yith_wcwl_wishlists}` WHERE `wishlist_token` = %s";

			do {
				$dictionary = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
				$nchars     = 12;
				$token      = '';

				for ( $i = 0; $i <= $nchars - 1; $i++ ) {
					$token .= $dictionary[ wp_rand( 0, strlen( $dictionary ) - 1 ) ];
				}

				$count = $wpdb->get_var( $wpdb->prepare( $sql, $token ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			} while ( $count );

			return $token;
		}

		/**
		 * When a session is finalized, all session wishlists will be converted to user wishlists
		 * This method takes also care of allowing just one default per time after finalization
		 *
		 * @param string $session_id Session id.
		 * @param int    $user_id User id.
		 *
		 * @return void
		 */
		public function assign_to_user( $session_id, $user_id ) {
			global $wpdb;

			// update any item that is assigned to the list.
			$items = $wpdb->get_col( $wpdb->prepare( "SELECT i.ID FROM {$wpdb->yith_wcwl_items} AS i LEFT JOIN {$wpdb->yith_wcwl_wishlists} AS l ON l.ID = i.wishlist_id WHERE l.session_id = %s", $session_id ) );

			if ( ! empty( $items ) ) {
				$items_string = implode( ',', array_map( 'esc_sql', $items ) );
				$wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->yith_wcwl_items} SET user_id = %d WHERE ID IN ({$items_string})", $user_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			}

			// set user id for any session wishlist, and remove session data.
			$this->update_raw(
				array(
					'session_id' => 'NULL',
					'expiration' => 'NULL',
					'user_id'    => '%d',
				),
				array( $user_id ),
				array( 'session_id' => '%s' ),
				array( $session_id )
			);

			// retrieves default wishlist ids.
			$default_ids = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM {$wpdb->yith_wcwl_wishlists} WHERE is_default = %d AND user_id = %d ORDER BY dateadded ASC", 1, $user_id ) );

			// if we find more than one default list, fix data in db.
			if ( count( $default_ids ) > 1 ) {

				// search for master default wishlist.
				$master_default_wishlist = array_shift( $default_ids );
				$where_statement         = implode( ', ', array_map( 'esc_sql', $default_ids ) );

				try {
					if ( apply_filters( 'yith_wcwl_merge_default_wishlists', true ) ) {
						// by default we merge all default wishlists into oldest one (master default wishlist).

						// change wishlist id to master default id.
						$wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->yith_wcwl_items} SET wishlist_id = %d WHERE wishlist_id IN ({$where_statement})", $master_default_wishlist ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared

						// delete slave default wishlists.
						$wpdb->query( "DELETE FROM {$wpdb->yith_wcwl_wishlists} WHERE ID IN ({$where_statement})" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
					} else {
						// otherwise, we just leave all the wishlists as they are, but we remove default flag from latest.

						// remove default flag.
						$wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->yith_wcwl_wishlists} SET is_default = %d WHERE ID IN ({$where_statement})", 0 ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared

						// set name where it is missing.
						$default_title = apply_filters( 'yith_wcwl_default_wishlist_formatted_title', get_option( 'yith_wcwl_wishlist_title' ) );
						$wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->yith_wcwl_wishlists} SET wishlist_name = %s WHERE ID IN ({$where_statement}) AND wishlist_name = ''", $default_title ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
					}
				} catch ( Exception $e ) {
					return;
				}
			}
		}

		/**
		 * Retrieve default wishlist for current user/session; if none is found, generate it
		 *
		 * @param string|int|bool $id       Pass this param when you want to retrieve a wishlist for a specific user/session.
		 * @param string          $context  Context; when on edit context, wishlist will be created, if not exists.
		 * @return \YITH_WCWL_Wishlist|bool Default wishlist for current user/session, or false on failure
		 */
		public function get_default_wishlist( $id = false, $context = 'read' ) {
			global $wpdb;

			$wishlist_id = false;
			$cache_key   = false;

			$user_id    = get_current_user_id();
			$session_id = YITH_WCWL_Session()->maybe_get_session_id();

			if ( ! empty( $id ) && is_int( $id ) ) {
				$cache_key   = 'wishlist-default-' . $id;
				$wishlist_id = wp_cache_get( $cache_key, 'wishlists' );
				$wishlist_id = false !== $wishlist_id ? $wishlist_id : $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->yith_wcwl_wishlists} WHERE user_id = %d AND is_default = 1", $id ) );
			} elseif ( ! empty( $id ) && is_string( $id ) ) {
				$cache_key   = 'wishlist-default-' . $id;
				$wishlist_id = wp_cache_get( $cache_key, 'wishlists' );
				$wishlist_id = false !== $wishlist_id ? $wishlist_id : $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->yith_wcwl_wishlists} WHERE session_id = %s AND expiration > NOW() AND is_default = 1", $id ) );
			} elseif ( $user_id ) {
				$cache_key   = 'wishlist-default-' . $user_id;
				$wishlist_id = wp_cache_get( $cache_key, 'wishlists' );
				$wishlist_id = false !== $wishlist_id ? $wishlist_id : $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->yith_wcwl_wishlists} WHERE user_id = %d AND is_default = 1", $user_id ) );
			} elseif ( $session_id ) {
				$cache_key   = 'wishlist-default-' . $session_id;
				$wishlist_id = wp_cache_get( $cache_key, 'wishlists' );
				$wishlist_id = false !== $wishlist_id ? $wishlist_id : $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->yith_wcwl_wishlists} WHERE session_id = %s AND expiration > NOW() AND is_default = 1", $session_id ) );
			}

			if ( $wishlist_id ) {
				if ( $cache_key ) {
					wp_cache_set( $cache_key, $wishlist_id, 'wishlists' );
				}

				return YITH_WCWL_Wishlist_Factory::get_wishlist( $wishlist_id );
			} elseif ( 'edit' === $context ) {
				$wishlist = $this->generate_default_wishlist( $id );

				if ( $cache_key ) {
					wp_cache_set( $cache_key, $wishlist->get_id(), 'wishlists' );
				}

				return $wishlist;
			} else {
				/**
				 * If no default wishlist was found, register null as cache value
				 * This will be used until someone tries to edit the list (entering previous elseif),
				 * causing a new default wishlist to be automatically generated and stored in cache, replacing null
				 *
				 * @since 3.0.6
				 */
				if ( $cache_key ) {
					wp_cache_set( $cache_key, null, 'wishlists' );
				}

				return false;
			}
		}

		/**
		 * Generate a new default wishlist
		 *
		 * @param string|int|bool $id Pass this param when you want to create a wishlist for a specific user/session.
		 * @return YITH_WCWL_Wishlist|bool Brand new default wishlist, or false on failure
		 */
		public function generate_default_wishlist( $id ) {
			try {
				$default_wishlist = new YITH_WCWL_Wishlist();

				if ( ! empty( $id ) && is_int( $id ) ) {
					$default_wishlist->set_user_id( $id );
				} elseif ( ! empty( $id ) && is_string( $id ) ) {
					$default_wishlist->set_session_id( $id );
				}

				$default_wishlist->save();

				/**
				 * Let developers perform processing when default wishlist is created
				 *
				 * @since 3.0.10
				 */
				do_action( 'yith_wcwl_generated_default_wishlist', $default_wishlist, $id );
			} catch ( Exception $e ) {
				return false;
			}

			return $default_wishlist;
		}

		/**
		 * Generate unique slug for the wishlisst
		 *
		 * @param string $slug Original slug assigned to the wishlist (it cuold be custom assigned, or generated from the title).
		 * @return string Unique slug, derived from original one adding ordinal number when necessary
		 */
		public function generate_slug( $slug ) {
			if ( empty( $slug ) ) {
				return '';
			}

			while ( $this->slug_exists( $slug ) ) {
				$match = array();

				if ( ! preg_match( '/([a-z-]+)-([0-9]+)/', $slug, $match ) ) {
					$i = 2;
				} else {
					$i    = intval( $match[2] ) + 1;
					$slug = $match[1];
				}

				$suffix = '-' . $i;
				$slug   = substr( $slug, 0, 200 - strlen( $suffix ) ) . $suffix;
			}

			return $slug;
		}

		/**
		 * Checks if a slug already exists
		 *
		 * @param string $slug Slug to check on db.
		 *
		 * @return bool Whether slug already exists for current session or not
		 */
		public function slug_exists( $slug ) {
			global $wpdb;

			$res = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(ID) FROM {$wpdb->yith_wcwl_wishlists} WHERE wishlist_slug = %s", $slug ) );

			return (bool) $res;
		}

		/**
		 * Check if we're registering first wishlist for the user/session
		 *
		 * @return bool Whether current wishlist should be default
		 */
		protected function should_be_default() {
			global $wpdb;

			$user_id     = get_current_user_id();
			$customer_id = YITH_WCWL_Session()->maybe_get_session_id();

			if ( $user_id ) {
				$wishlists = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( ID ) FROM {$wpdb->yith_wcwl_wishlists} WHERE user_id = %d AND is_default = %d", $user_id, 1 ) );

				return ! (bool) $wishlists;
			}

			if ( $customer_id ) {
				$wishlists = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( ID ) FROM {$wpdb->yith_wcwl_wishlists} WHERE session_id = %s AND expiration > NOW() AND is_default = %d", $customer_id, 1 ) );

				return ! (bool) $wishlists;
			}

			return true;
		}

		/**
		 * Clear wishlist related caches
		 *
		 * @param \YITH_WCWL_Wishlist|int|string $wishlist Wishlist object.
		 * @return void
		 */
		protected function clear_caches( &$wishlist ) {
			if ( $wishlist instanceof YITH_WCWL_Wishlist ) {
				$id    = $wishlist->get_id();
				$token = $wishlist->get_token();
			} elseif ( intval( $wishlist ) ) {
				$id       = $wishlist;
				$wishlist = yith_wcwl_get_wishlist( $wishlist );
				$token    = $wishlist ? $wishlist->get_token() : false;
			} else {
				$token    = $wishlist;
				$wishlist = yith_wcwl_get_wishlist( $wishlist );
				$id       = $wishlist ? $wishlist->get_id() : false;
			}

			$user_id    = $wishlist ? $wishlist->get_user_id() : false;
			$session_id = $wishlist ? $wishlist->get_session_id() : false;

			wp_cache_delete( 'wishlist-items-' . $id, 'wishlists' );
			wp_cache_delete( 'wishlist-id-' . $id, 'wishlists' );
			wp_cache_delete( 'wishlist-token-' . $token, 'wishlists' );

			if ( $user_id ) {
				wp_cache_delete( 'user-wishlists-' . $user_id, 'wishlists' );
			}

			if ( $session_id ) {
				wp_cache_delete( 'user-wishlists-' . $session_id, 'wishlists' );
			}
		}
	}
}

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
class-yith-wcwl-ajax-handler.php000064400000030432151327635660012663 0ustar00<?php
/**
 * Static class that will handle all ajax calls for the list
 *
 * @author YITH
 * @package YITH\Wishlist\Classes
 * @version 3.0.0
 */

if ( ! defined( 'YITH_WCWL' ) ) {
	exit;
} // Exit if accessed directly

if ( ! class_exists( 'YITH_WCWL_Ajax_Handler' ) ) {
	/**
	 * WooCommerce Wishlist Ajax Handler
	 *
	 * @since 3.0.0
	 */
	class YITH_WCWL_Ajax_Handler {
		/**
		 * Performs all required add_actions to handle forms
		 *
		 * @return void
		 */
		public static function init() {
			// add to wishlist.
			add_action( 'wp_ajax_add_to_wishlist', array( 'YITH_WCWL_Ajax_Handler', 'add_to_wishlist' ) );
			add_action( 'wp_ajax_nopriv_add_to_wishlist', array( 'YITH_WCWL_Ajax_Handler', 'add_to_wishlist' ) );

			// remove from wishlist.
			add_action( 'wp_ajax_remove_from_wishlist', array( 'YITH_WCWL_Ajax_Handler', 'remove_from_wishlist' ) );
			add_action( 'wp_ajax_nopriv_remove_from_wishlist', array( 'YITH_WCWL_Ajax_Handler', 'remove_from_wishlist' ) );

			// remove from wishlist (button).
			add_action( 'wp_ajax_delete_item', array( 'YITH_WCWL_Ajax_Handler', 'delete_item' ) );
			add_action( 'wp_ajax_nopriv_delete_item', array( 'YITH_WCWL_Ajax_Handler', 'delete_item' ) );

			// save title.
			add_action( 'wp_ajax_save_title', array( 'YITH_WCWL_Ajax_Handler', 'save_title' ) );
			add_action( 'wp_ajax_nopriv_save_title', array( 'YITH_WCWL_Ajax_Handler', 'save_title' ) );

			// load mobile templates.
			add_action( 'wp_ajax_load_mobile', array( 'YITH_WCWL_Ajax_Handler', 'load_mobile' ) );
			add_action( 'wp_ajax_nopriv_load_mobile', array( 'YITH_WCWL_Ajax_Handler', 'load_mobile' ) );

			// add to wishlist and reload.
			add_action( 'wp_ajax_reload_wishlist_and_adding_elem', array( 'YITH_WCWL_Ajax_Handler', 'reload_wishlist_and_adding_elem' ) );
			add_action( 'wp_ajax_nopriv_reload_wishlist_and_adding_elem', array( 'YITH_WCWL_Ajax_Handler', 'reload_wishlist_and_adding_elem' ) );

			// load fragments.
			add_action( 'wp_ajax_load_fragments', array( 'YITH_WCWL_Ajax_Handler', 'load_fragments' ) );
			add_action( 'wp_ajax_nopriv_load_fragments', array( 'YITH_WCWL_Ajax_Handler', 'load_fragments' ) );
		}

		/**
		 * Add to wishlist from ajax call
		 *
		 * @return void
		 */
		public static function add_to_wishlist() {
			if ( ! isset( $_REQUEST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ), 'add_to_wishlist' ) ) {
				wp_send_json( array( 'result' => false ) );
			}

			try {
				YITH_WCWL()->add();

				$return  = 'true';
				$message = apply_filters( 'yith_wcwl_product_added_to_wishlist_message', get_option( 'yith_wcwl_product_added_text' ) );

				// append view and close links.
				if ( apply_filters( 'yith_wcwl_show_popup_links', YITH_WCWL()->is_multi_wishlist_enabled() ) ) {
					$message .= '<p class="after-links">
					<a href="' . YITH_WCWL()->get_last_operation_url() . '">' . __( 'View &rsaquo;', 'yith-woocommerce-wishlist' ) . '</a>
					<span class="separator">' . __( 'or', 'yith-woocommerce-wishlist' ) . '</span>
					<a href="#" class="close-popup">' . __( 'Close', 'yith-woocommerce-wishlist' ) . '</a>
					</p>';
				}
			} catch ( YITH_WCWL_Exception $e ) {
				$return  = $e->getTextualCode();
				$message = apply_filters( 'yith_wcwl_error_adding_to_wishlist_message', $e->getMessage() );
			} catch ( Exception $e ) {
				$return  = 'error';
				$message = apply_filters( 'yith_wcwl_error_adding_to_wishlist_message', $e->getMessage() );
			}

			$product_id   = isset( $_REQUEST['add_to_wishlist'] ) ? intval( $_REQUEST['add_to_wishlist'] ) : false;
			$fragments    = isset( $_REQUEST['fragments'] ) ? wc_clean( $_REQUEST['fragments'] ) : false; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
			$wishlist_url = YITH_WCWL()->get_last_operation_url();

			$wishlists = YITH_WCWL_Wishlist_Factory::get_wishlists();

			$wishlists_to_prompt = array();

			foreach ( $wishlists as $wishlist ) {
				$wishlists_to_prompt[] = array(
					'id'                       => $wishlist->get_id(),
					'wishlist_name'            => $wishlist->get_formatted_name(),
					'default'                  => $wishlist->is_default(),
					'add_to_this_wishlist_url' => $product_id ? wp_nonce_url(
						add_query_arg(
							array(
								'add_to_wishlist' => $product_id,
								'wishlist_id'     => $wishlist->get_id(),
							),
							$wishlist->get_url()
						),
						'add_to_wishlist'
					) : '',
				);
			}

			if ( in_array( $return, array( 'exists', 'true' ), true ) ) {
				// search for related fragments.
				if ( ! empty( $fragments ) && ! empty( $product_id ) ) {
					foreach ( $fragments as $id => $options ) {
						if ( strpos( $id, 'add-to-wishlist-' . $product_id ) ) {
							$fragments[ $id ]['wishlist_url']      = $wishlist_url;
							$fragments[ $id ]['added_to_wishlist'] = 'true' === $return;
						}
					}
				}
			}

			wp_send_json(
				apply_filters(
					'yith_wcwl_ajax_add_return_params',
					array(
						'prod_id'        => $product_id,
						'result'         => $return,
						'message'        => $message,
						'fragments'      => self::refresh_fragments( $fragments ),
						'user_wishlists' => $wishlists_to_prompt,
						'wishlist_url'   => $wishlist_url,
					)
				)
			);
		}

		/**
		 * Remove from wishlist from ajax call
		 *
		 * @return void
		 */
		public static function remove_from_wishlist() {
			if ( ! isset( $_REQUEST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ), 'remove_from_wishlist' ) ) {
				wp_send_json( array( 'fragments' => array() ) );
			}

			$fragments = isset( $_REQUEST['fragments'] ) ? wc_clean( $_REQUEST['fragments'] ) : false; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput

			try {
				YITH_WCWL()->remove();
				$message = apply_filters( 'yith_wcwl_product_removed_text', __( 'Product successfully removed.', 'yith-woocommerce-wishlist' ) );
			} catch ( Exception $e ) {
				$message = $e->getMessage();
			}

			yith_wcwl_add_notice( $message );

			wp_send_json(
				array(
					'fragments' => self::refresh_fragments( $fragments ),
				)
			);
		}

		/**
		 * Remove item from a wishlist
		 * Differs from remove from wishlist, since this accepts item id instead of product id
		 *
		 * @return void
		 * @since 3.0.0
		 */
		public static function delete_item() {
			if ( ! isset( $_REQUEST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ), 'delete_item' ) ) {
				wp_send_json( array( 'result' => false ) );
			}

			$item_id   = isset( $_POST['item_id'] ) ? intval( $_POST['item_id'] ) : false;
			$fragments = isset( $_REQUEST['fragments'] ) ? wc_clean( $_REQUEST['fragments'] ) : false; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
			$return    = array(
				'result' => false,
			);

			if ( $item_id ) {
				$item = YITH_WCWL_Wishlist_Factory::get_wishlist_item( $item_id );

				if ( $item ) {
					$item->delete();

					$return = array(
						'result'    => true,
						'message'   => apply_filters( 'yith_wcwl_product_removed_text', __( 'Product successfully removed.', 'yith-woocommerce-wishlist' ) ),
						'fragments' => self::refresh_fragments( $fragments ),
					);
				}
			}

			wp_send_json( $return );
		}

		/**
		 * Save new wishlist privacy
		 *
		 * @return void
		 * @since 3.0.7
		 */
		public static function save_title() {
			if ( ! isset( $_REQUEST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ), 'save_title' ) ) {
				wp_send_json( array( 'result' => false ) );
			}

			$wishlist_id   = isset( $_POST['wishlist_id'] ) ? sanitize_text_field( wp_unslash( $_POST['wishlist_id'] ) ) : false;
			$wishlist_name = isset( $_POST['title'] ) ? sanitize_text_field( wp_unslash( $_POST['title'] ) ) : false;
			$fragments     = isset( $_REQUEST['fragments'] ) ? wc_clean( $_REQUEST['fragments'] ) : false; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
			$wishlist      = $wishlist_id ? yith_wcwl_get_wishlist( $wishlist_id ) : false;

			if ( ! $wishlist_id || ! $wishlist ) {
				wp_send_json(
					array(
						'result' => false,
					)
				);
			}

			if ( ! $wishlist_name || strlen( $wishlist_name ) >= 65535 ) {
				wp_send_json(
					array(
						'result' => false,
					)
				);
			}

			$wishlist->set_name( $wishlist_name );
			$wishlist->save();

			$return = array(
				'result'    => true,
				'fragments' => self::refresh_fragments( $fragments ),
			);

			wp_send_json( $return );
		}

		/**
		 * Generated fragments to replace in the the page
		 *
		 * @return void
		 * @since 3.0.0
		 */
		public static function load_fragments() {
			if ( ! isset( $_REQUEST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ), 'load_fragments' ) ) {
				wp_send_json( array( 'result' => false ) );
			}

			$fragments = isset( $_POST['fragments'] ) ? wc_clean( $_POST['fragments'] ) : false; // phpcs:ignore WordPress.Security

			wp_send_json(
				array(
					'fragments' => self::refresh_fragments( $fragments ),
				)
			);
		}

		/**
		 * Reload wishlist and adding elem action
		 *
		 * @return void
		 * @since 1.0.0
		 */
		public static function reload_wishlist_and_adding_elem() {
			if ( ! isset( $_REQUEST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ), 'reload_wishlist_and_adding_elem' ) ) {
				wp_send_json( array( 'result' => false ) );
			}

			$type_msg = 'success';

			try {
				YITH_WCWL()->add();
				$message = apply_filters( 'yith_wcwl_product_added_to_wishlist_message', get_option( 'yith_wcwl_product_added_text' ) );
			} catch ( YITH_WCWL_Exception $e ) {
				$message  = $e->getMessage();
				$type_msg = $e->getTextualCode();
			} catch ( Exception $e ) {
				$message  = $e->getMessage();
				$type_msg = 'error';
			}

			$wishlist_token = isset( $_REQUEST['wishlist_token'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['wishlist_token'] ) ) : false;
			$atts           = array( 'wishlist_id' => $wishlist_token );

			if ( isset( $_REQUEST['pagination'] ) ) {
				$atts['pagination'] = sanitize_text_field( wp_unslash( $_REQUEST['pagination'] ) );
			}

			if ( isset( $_REQUEST['per_page'] ) ) {
				$atts['per_page'] = intval( $_REQUEST['per_page'] );
			}

			yith_wcwl_add_notice( $message, $type_msg );

			?>
			<div>
				<?php echo YITH_WCWL_Shortcode::wishlist( $atts ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
			</div>
			<?php

			die();

		}

		/**
		 * Reloads fragments, returning mobile version when available
		 *
		 * @return void
		 * @since 3.0.0
		 */
		public static function load_mobile() {
			global $yith_wcwl_is_mobile;

			if ( ! isset( $_REQUEST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ), 'load_mobile' ) ) {
				wp_send_json( array( 'fragments' => array() ) );
			}

			$fragments = isset( $_POST['fragments'] ) ? wc_clean( $_POST['fragments'] ) : false; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
			$result    = array();

			if ( ! empty( $fragments ) ) {
				foreach ( $fragments as $id => $options ) {
					$yith_wcwl_is_mobile = isset( $options['is_mobile'] ) ? 'yes' === $options['is_mobile'] : false;

					$result = array_merge( $result, self::refresh_fragments( array( $id => $options ) ) );
				}
			}

			wp_send_json(
				array(
					'fragments' => $result,
				)
			);
		}

		/**
		 * Generate fragments for the templates that needs to be refreshed after ajax
		 *
		 * @param array $fragments Array of fragments to refresh.
		 * @return array Array of templates to be replaced on the page
		 */
		public static function refresh_fragments( $fragments ) {
			$result = array();

			if ( ! empty( $fragments ) ) {
				foreach ( $fragments as $id => $options ) {
					$id      = sanitize_text_field( $id );
					$options = YITH_WCWL_Frontend()->decode_fragment_options( $options );
					$item    = isset( $options['item'] ) ? $options['item'] : false;

					if ( ! $item ) {
						continue;
					}

					switch ( $item ) {
						case 'add_to_wishlist':
						case 'wishlist':
							$result[ $id ] = YITH_WCWL_Shortcode::$item( $options );
							break;
						case 'YITH_WCWL_Widget':
						case 'YITH_WCWL_Items_Widget':
							ob_start();
							the_widget( $item, $options );
							$result[ $id ] = ob_get_clean();
							break;
						default:
							$result[ $id ] = apply_filters( 'yith_wcwl_fragment_output', '', $id, $options );
							break;
					}
				}
			}

			return $result;
		}
	}
}
YITH_WCWL_Ajax_Handler::init();
class-yith-wcwl-session.php000064400000021542151327635660012012 0ustar00<?php
/**
 * Wishlist Session Handler
 *
 * @author YITH
 * @package YITH\Wishlist\Classes
 * @version 3.0.0
 */

if ( ! defined( 'YITH_WCWL' ) ) {
	exit;
} // Exit if accessed directly

if ( ! class_exists( 'YITH_WCWL_Session' ) ) {
	/**
	 * This class implements Session handler for wishlist
	 * Unique session id is assigned to any new customer, and registered in a cookie
	 *
	 * Expiration is set accordingly to plugin options
	 *
	 * @since 3.0.0
	 */
	class YITH_WCWL_Session {
		/**
		 * Single instance of the class
		 *
		 * @var \YITH_WCWL_Session
		 * @since 3.0.0
		 */
		protected static $instance;

		/**
		 * Session ID.
		 *
		 * @var int $session_id Session ID.
		 */
		protected $session_id;

		/**
		 * Cookie name used for the session.
		 *
		 * @var string cookie name
		 */
		protected $cookie_name;

		/**
		 * Cookie content.
		 *
		 * @var array cookie content.
		 */
		protected $cookie;

		/**
		 * Stores session expiry.
		 *
		 * @var string session due to expire timestamp
		 */
		protected $session_expiring;

		/**
		 * Stores session expiration.
		 *
		 * @var string session expiration timestamp
		 */
		protected $session_expiration;

		/**
		 * True when the cookie exists.
		 *
		 * @var bool Based on whether a cookie exists.
		 */
		protected $has_cookie = false;

		/**
		 * Construct session class
		 */
		public function __construct() {
			// prefetch session cookie.
			add_action( 'init', array( $this, 'get_session_cookie' ), 5 );

			// add action to finalize session.
			add_action( 'init', array( $this, 'finalize_session' ) );
		}

		/**
		 * Setup cookie and customer ID.
		 *
		 * @since 3.0.0
		 */
		public function init_session_cookie() {
			$cookie = $this->get_session_cookie();

			if ( is_array( $cookie ) && ! empty( $cookie['session_id'] ) && ! empty( $cookie['session_expiration'] ) ) {
				if ( is_user_logged_in() ) {
					// If the user logs in, forget session.
					/**
					 * Once customer logs in, we can permanently register wishlists for his account
					 */
					$this->finalize_session();
				} elseif ( time() > $this->session_expiring ) {
					// Update session if its close to expiring.
					$this->set_session_expiration();
					$this->update_session_timestamp( $this->session_id, $this->session_expiration );
				}
			} else {
				$this->set_session_expiration();
				$this->session_id = $this->session_id ? $this->session_id : $this->generate_session_id();
			}

			if ( ! $this->has_cookie ) {
				$this->set_session_cookie();
			}
		}

		/**
		 * Sets the session cookie on-demand
		 *
		 * @return void
		 */
		public function set_session_cookie() {
			if ( headers_sent() || ! apply_filters( 'yith_wcwl_set_session_cookie', true ) ) {
				return;
			}

			$to_hash = $this->session_id . '|' . $this->session_expiration;
			$hash    = hash_hmac( 'md5', $to_hash, wp_hash( $to_hash ) );

			$cookie_value = array(
				'session_id'         => $this->session_id,
				'session_expiration' => $this->session_expiration,
				'session_expiring'   => $this->session_expiring,
				'cookie_hash'        => $hash,
			);
			yith_setcookie( $this->get_session_cookie_name(), $cookie_value, $this->session_expiration, $this->use_secure_cookie(), true );

			// cookie has been set.
			$this->cookie     = $cookie_value;
			$this->has_cookie = true;
		}

		/**
		 * Get the session cookie, if set. Otherwise return false.
		 *
		 * Session cookies without a customer ID are invalid.
		 *
		 * @return bool|array
		 */
		public function get_session_cookie() {
			if ( ! empty( $this->cookie ) ) {
				return $this->cookie;
			}

			$cookie_value = yith_getcookie( $this->get_session_cookie_name() ); // @codingStandardsIgnoreLine.

			if ( empty( $cookie_value ) || ! is_array( $cookie_value ) ) {
				return false;
			}

			if ( empty( $cookie_value['session_id'] ) ) {
				return false;
			}

			// Validate hash.
			$to_hash = $cookie_value['session_id'] . '|' . $cookie_value['session_expiration'];
			$hash    = hash_hmac( 'md5', $to_hash, wp_hash( $to_hash ) );

			if ( empty( $cookie_value['cookie_hash'] ) || ! hash_equals( $hash, $cookie_value['cookie_hash'] ) ) {
				return false;
			}

			$this->cookie     = $cookie_value;
			$this->has_cookie = true;

			$this->session_id         = $cookie_value['session_id'];
			$this->session_expiration = $cookie_value['session_expiration'];
			$this->session_expiring   = $cookie_value['session_expiring'];

			return $cookie_value;
		}

		/**
		 * Returns true if system should use HTTPS only cookies
		 *
		 * @return bool
		 */
		public function use_secure_cookie() {
			return apply_filters( 'yith_wcwl_session_use_secure_cookie', wc_site_is_https() && is_ssl() );
		}

		/**
		 * Returns name for the session cookie
		 *
		 * @return string
		 * @since 3.0.3
		 */
		public function get_session_cookie_name() {
			if ( empty( $this->cookie_name ) ) {
				$this->cookie_name = apply_filters( 'yith_wcwl_session_cookie', 'yith_wcwl_session_' . COOKIEHASH );
			}

			return $this->cookie_name;
		}

		/**
		 * Returns current session expiration; if session doesn't exist, creates it; if user is logged in, return false
		 *
		 * @return string Current customer id
		 */
		public function get_session_expiration() {
			$session_id = $this->get_session_id();

			if ( $session_id ) {
				return $this->session_expiration;
			}

			return false;
		}

		/**
		 * Set session expiration.
		 */
		public function set_session_expiration() {
			$this->session_expiring   = time() + yith_wcwl_get_cookie_expiration() - HOUR_IN_SECONDS;
			$this->session_expiration = time() + yith_wcwl_get_cookie_expiration();
		}

		/**
		 * Return true if the current user has an active session, i.e. a cookie to retrieve values.
		 *
		 * @return bool
		 */
		public function has_session() {
			return $this->has_cookie; // @codingStandardsIgnoreLine.
		}

		/**
		 * Returns current session id; if session doesn't exist, creates it; if user is logged in, return false
		 *
		 * @return string Current customer id
		 */
		public function get_session_id() {
			if ( $this->has_session() ) {
				return $this->session_id;
			} elseif ( ! is_user_logged_in() ) {
				$this->init_session_cookie();

				return $this->session_id;
			}

			return false;
		}

		/**
		 * Returns current session id, if any; false otherwise (won't create a session)
		 *
		 * @return string|bool Current customer id, or false, if none
		 */
		public function maybe_get_session_id() {
			if ( $this->has_session() ) {
				return $this->session_id;
			}

			return false;
		}

		/**
		 * Generate a unique customer ID for guests, or return false if logged in.
		 *
		 * Uses Portable PHP password hashing framework to generate a unique cryptographically strong ID.
		 *
		 * @return string|bool
		 */
		public function generate_session_id() {
			$session_id = '';

			if ( is_user_logged_in() ) {
				return false;
			}

			require_once ABSPATH . 'wp-includes/class-phpass.php';
			$hasher     = new PasswordHash( 8, false );
			$session_id = md5( $hasher->get_random_bytes( 32 ) );

			return $session_id;
		}

		/**
		 * Converts session to stable database items
		 *
		 * @return void
		 */
		public function finalize_session() {
			if ( ! is_user_logged_in() ) {
				return;
			}

			$cookie = $this->get_session_cookie();

			if ( ! $cookie ) {
				return;
			}

			if ( empty( $cookie['session_id'] ) ) {
				return;
			}

			$user_id    = get_current_user_id();
			$session_id = $cookie['session_id'];

			try {
				WC_Data_Store::load( 'wishlist' )->assign_to_user( $session_id, $user_id );
			} catch ( Exception $e ) {
				wc_caught_exception( $e, __FUNCTION__, func_get_args() );
				return;
			}

			$this->forget_session();
		}

		/**
		 * Update the session expiry timestamp.
		 *
		 * @param string $session_id Session ID.
		 * @param int    $timestamp Timestamp to expire the cookie.
		 */
		public function update_session_timestamp( $session_id, $timestamp ) {
			try {
				WC_Data_Store::load( 'wishlist' )->update_raw(
					array( 'expiration' => 'FROM_UNIXTIME(%d)' ),
					array( $timestamp ),
					array( 'session_id' => '%s' ),
					array( $session_id )
				);
			} catch ( Exception $e ) {
				wc_caught_exception( $e, __FUNCTION__, func_get_args() );
				return;
			}
		}

		/**
		 * Forget all session data without destroying it.
		 */
		public function forget_session() {
			yith_destroycookie( $this->get_session_cookie_name() );

			$this->session_id = $this->generate_session_id();
			$this->cookie     = null;
		}

		/**
		 * Returns single instance of the class
		 *
		 * @return \YITH_WCWL_Session
		 * @since 3.0.0
		 */
		public static function get_instance() {
			if ( is_null( self::$instance ) ) {
				self::$instance = new self();
			}

			return self::$instance;
		}
	}
}

/**
 * Unique access to instance of YITH_WCWL_Session class
 *
 * @return \YITH_WCWL_Session
 * @since 3.0.0
 */
function YITH_WCWL_Session() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid
	return YITH_WCWL_Session::get_instance();
}
widgets/elementor/class-yith-wcwl-elementor-add-to-wishlist.php000064400000013163151327635660020773 0ustar00<?php
/**
 * Add to Wishlist widget for Elementor
 *
 * @author YITH
 * @package YITH\Wishlist\Classes\Elementor
 * @version 3.0.7
 */

if ( ! defined( 'YITH_WCWL' ) ) {
	exit;
} // Exit if accessed directly

if ( ! class_exists( 'YITH_WCWL_Elementor_Add_To_Wishlist' ) ) {
	/**
	 * Add to Wishlist Elementor block
	 *
	 * @since 3.0.0
	 */
	class YITH_WCWL_Elementor_Add_To_Wishlist extends \Elementor\Widget_Base {

		/**
		 * Get widget name.
		 *
		 * Retrieve YITH_WCWL_Elementor_Add_to_Wishlist widget name.
		 *
		 * @return string Widget name.
		 * @since  1.0.0
		 * @access public
		 */
		public function get_name() {
			return 'yith_wcwl_add_to_wishlist';
		}

		/**
		 * Get widget title.
		 *
		 * Retrieve YITH_WCWL_Elementor_Add_to_Wishlist widget title.
		 *
		 * @return string Widget title.
		 * @since  1.0.0
		 * @access public
		 */
		public function get_title() {
			return _x( 'YITH Wishlist Add button', 'Elementor widget name', 'yith-woocommerce-wishlist' );
		}

		/**
		 * Get widget icon.
		 *
		 * Retrieve YITH_WCWL_Elementor_Add_to_Wishlist widget icon.
		 *
		 * @return string Widget icon.
		 * @since  1.0.0
		 * @access public
		 */
		public function get_icon() {
			return 'eicon-button';
		}

		/**
		 * Get widget categories.
		 *
		 * Retrieve the list of categories the YITH_WCWL_Elementor_Add_to_Wishlist widget belongs to.
		 *
		 * @return array Widget categories.
		 * @since  1.0.0
		 * @access public
		 */
		public function get_categories() {
			return array( 'general', 'yith' );
		}

		/**
		 * Register YITH_WCWL_Elementor_Add_to_Wishlist widget controls.
		 *
		 * Adds different input fields to allow the user to change and customize the widget settings.
		 *
		 * @since  1.0.0
		 * @access protected
		 */
		protected function _register_controls() { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore

			$this->start_controls_section(
				'product_section',
				array(
					'label' => _x( 'Product', 'Elementor section title', 'yith-woocommerce-wishlist' ),
					'tab'   => \Elementor\Controls_Manager::TAB_CONTENT,
				)
			);

			$this->add_control(
				'product_id',
				array(
					'label'       => _x( 'Product ID', 'Elementor control label', 'yith-woocommerce-wishlist' ),
					'type'        => \Elementor\Controls_Manager::NUMBER,
					'input_type'  => 'text',
					'placeholder' => '123',
				)
			);

			$this->end_controls_section();

			$this->start_controls_section(
				'labels_section',
				array(
					'label' => _x( 'Labels', 'Elementor section title', 'yith-woocommerce-wishlist' ),
					'tab'   => \Elementor\Controls_Manager::TAB_CONTENT,
				)
			);

			$this->add_control(
				'label',
				array(
					'label'       => _x( 'Button label', 'Elementor control label', 'yith-woocommerce-wishlist' ),
					'type'        => \Elementor\Controls_Manager::TEXT,
					'input_type'  => 'text',
					'placeholder' => __( 'Add to wishlist', 'yith-woocommerce-wishlist' ),
				)
			);

			$this->add_control(
				'browse_wishlist_text',
				array(
					'label'       => _x( '"Browse wishlist" label', 'Elementor control label', 'yith-woocommerce-wishlist' ),
					'type'        => \Elementor\Controls_Manager::TEXT,
					'input_type'  => 'text',
					'placeholder' => __( 'Browse wishlist', 'yith-woocommerce-wishlist' ),
				)
			);

			$this->add_control(
				'already_in_wishslist_text',
				array(
					'label'       => _x( '"Product already in wishlist" label', 'Elementor control label', 'yith-woocommerce-wishlist' ),
					'type'        => \Elementor\Controls_Manager::TEXT,
					'input_type'  => 'text',
					'placeholder' => __( 'Product already in wishlist', 'yith-woocommerce-wishlist' ),
				)
			);

			$this->add_control(
				'product_added_text',
				array(
					'label'       => _x( '"Product added to wishlist" label', 'Elementor control label', 'yith-woocommerce-wishlist' ),
					'type'        => \Elementor\Controls_Manager::TEXT,
					'input_type'  => 'text',
					'placeholder' => __( 'Product added to wishlist', 'yith-woocommerce-wishlist' ),
				)
			);

			$this->end_controls_section();

			$this->start_controls_section(
				'advanced_section',
				array(
					'label' => _x( 'Advanced', 'Elementor section title', 'yith-woocommerce-wishlist' ),
					'tab'   => \Elementor\Controls_Manager::TAB_CONTENT,
				)
			);

			$this->add_control(
				'wishlist_url',
				array(
					'label'       => _x( 'URL of the wishlist page', 'Elementor control label', 'yith-woocommerce-wishlist' ),
					'type'        => \Elementor\Controls_Manager::TEXT,
					'input_type'  => 'url',
					'placeholder' => '',
				)
			);

			$this->add_control(
				'icon',
				array(
					'label'       => _x( 'Icon for the button', 'Elementor control label', 'yith-woocommerce-wishlist' ),
					'type'        => \Elementor\Controls_Manager::TEXT,
					'input_type'  => 'text',
					'placeholder' => '',
				)
			);

			$this->add_control(
				'link_classes',
				array(
					'label'       => _x( 'Additional CSS classes for the button', 'Elementor control label', 'yith-woocommerce-wishlist' ),
					'type'        => \Elementor\Controls_Manager::TEXT,
					'input_type'  => 'text',
					'placeholder' => '',
				)
			);

			$this->end_controls_section();

		}

		/**
		 * Render YITH_WCWL_Elementor_Add_to_Wishlist widget output on the frontend.
		 *
		 * @since  1.0.0
		 * @access protected
		 */
		protected function render() {

			$attribute_string = '';
			$settings         = $this->get_settings_for_display();

			foreach ( $settings as $key => $value ) {
				if ( empty( $value ) || ! is_scalar( $value ) ) {
					continue;
				}
				$attribute_string .= " {$key}=\"{$value}\"";
			}

			echo do_shortcode( "[yith_wcwl_add_to_wishlist {$attribute_string}]" );
		}

	}
}
widgets/elementor/class-yith-wcwl-elementor-wishlist.php000064400000007270151327635660017627 0ustar00<?php
/**
 * Wishlist widget for Elementor
 *
 * @author YITH
 * @package YITH\Wishlist\Classes\Elementor
 * @version 3.0.7
 */

if ( ! defined( 'YITH_WCWL' ) ) {
	exit;
} // Exit if accessed directly

if ( ! class_exists( 'YITH_WCWL_Elementor_Wishlist' ) ) {
	/**
	 * Wishlist Elementor block
	 *
	 * @since 3.0.0
	 */
	class YITH_WCWL_Elementor_Wishlist extends \Elementor\Widget_Base {

		/**
		 * Get widget name.
		 *
		 * Retrieve YITH_WCWL_Elementor_Wishlist widget name.
		 *
		 * @return string Widget name.
		 * @since  1.0.0
		 * @access public
		 */
		public function get_name() {
			return 'yith_wcwl_wishlist';
		}

		/**
		 * Get widget title.
		 *
		 * Retrieve YITH_WCWL_Elementor_Wishlist widget title.
		 *
		 * @return string Widget title.
		 * @since  1.0.0
		 * @access public
		 */
		public function get_title() {
			return _x( 'YITH Wishlist', 'Elementor widget name', 'yith-woocommerce-wishlist' );
		}

		/**
		 * Get widget icon.
		 *
		 * Retrieve YITH_WCWL_Elementor_Wishlist widget icon.
		 *
		 * @return string Widget icon.
		 * @since  1.0.0
		 * @access public
		 */
		public function get_icon() {
			return 'eicon-table';
		}

		/**
		 * Get widget categories.
		 *
		 * Retrieve the list of categories the YITH_WCWL_Elementor_Wishlist widget belongs to.
		 *
		 * @return array Widget categories.
		 * @since  1.0.0
		 * @access public
		 */
		public function get_categories() {
			return array( 'general', 'yith' );
		}

		/**
		 * Register YITH_WCWL_Elementor_Wishlist widget controls.
		 *
		 * Adds different input fields to allow the user to change and customize the widget settings.
		 *
		 * @since  1.0.0
		 * @access protected
		 */
		protected function _register_controls() { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore

			$this->start_controls_section(
				'product_section',
				array(
					'label' => _x( 'Wishlist', 'Elementor section title', 'yith-woocommerce-wishlist' ),
					'tab'   => \Elementor\Controls_Manager::TAB_CONTENT,
				)
			);

			$this->add_control(
				'wishlist_id',
				array(
					'label'       => _x( 'Wishlist ID', 'Elementor control label', 'yith-woocommerce-wishlist' ),
					'type'        => \Elementor\Controls_Manager::TEXT,
					'input_type'  => 'text',
					'placeholder' => 'K6EOWXB888ZD',
				)
			);

			$this->end_controls_section();

			$this->start_controls_section(
				'labels_section',
				array(
					'label' => _x( 'Pagination', 'Elementor section title', 'yith-woocommerce-wishlist' ),
					'tab'   => \Elementor\Controls_Manager::TAB_CONTENT,
				)
			);

			$this->add_control(
				'pagination',
				array(
					'label'   => _x( 'Paginate items', 'Elementor control label', 'yith-woocommerce-wishlist' ),
					'type'    => \Elementor\Controls_Manager::SELECT,
					'options' => array(
						'yes' => __( 'Paginate', 'yith-woocommerce-wishlist' ),
						'no'  => __( 'Do not paginate', 'yith-woocommerce-wishlist' ),
					),
					'default' => 'no',
				)
			);

			$this->add_control(
				'per_page',
				array(
					'label'       => _x( 'Items per page', 'yith-woocommerce-wishlist' ),
					'type'        => \Elementor\Controls_Manager::NUMBER,
					'input_type'  => 'number',
					'placeholder' => 5,
				)
			);

			$this->end_controls_section();

		}

		/**
		 * Render YITH_WCWL_Elementor_Wishlist widget output on the frontend.
		 *
		 * @since  1.0.0
		 * @access protected
		 */
		protected function render() {

			$attribute_string = '';
			$settings         = $this->get_settings_for_display();

			foreach ( $settings as $key => $value ) {
				if ( empty( $value ) || ! is_scalar( $value ) ) {
					continue;
				}
				$attribute_string .= " {$key}=\"{$value}\"";
			}

			echo do_shortcode( "[yith_wcwl_wishlist {$attribute_string}]" );
		}

	}
}
class-yith-wcwl-install.php000064400000022426151327635660011777 0ustar00<?php
/**
 * Install file
 *
 * @author YITH
 * @package YITH\Wishlist\Classes
 * @version 3.0.0
 */

if ( ! defined( 'YITH_WCWL' ) ) {
	exit;
} // Exit if accessed directly

if ( ! class_exists( 'YITH_WCWL_Install' ) ) {
	/**
	 * Install plugin table and create the wishlist page
	 *
	 * @since 1.0.0
	 */
	class YITH_WCWL_Install {

		/**
		 * Single instance of the class
		 *
		 * @var \YITH_WCWL_Install
		 * @since 2.0.0
		 */
		protected static $instance;

		/**
		 * Items table name
		 *
		 * @var string
		 * @access private
		 * @since 1.0.0
		 */
		private $table_items;

		/**
		 * Items table name
		 *
		 * @var string
		 * @access private
		 * @since 1.0.0
		 */
		private $table_wishlists;

		/**
		 * Returns single instance of the class
		 *
		 * @return \YITH_WCWL_Install
		 * @since 2.0.0
		 */
		public static function get_instance() {
			if ( is_null( self::$instance ) ) {
				self::$instance = new self();
			}

			return self::$instance;
		}

		/**
		 * Constructor.
		 *
		 * @since 1.0.0
		 */
		public function __construct() {
			global $wpdb;

			// define local private attribute.
			$this->table_items     = $wpdb->prefix . 'yith_wcwl';
			$this->table_wishlists = $wpdb->prefix . 'yith_wcwl_lists';

			// add custom field to global $wpdb.
			$wpdb->yith_wcwl_items     = $this->table_items;
			$wpdb->yith_wcwl_wishlists = $this->table_wishlists;

			// define constant to use allover the application.
			define( 'YITH_WCWL_ITEMS_TABLE', $this->table_items );
			define( 'YITH_WCWL_WISHLISTS_TABLE', $this->table_wishlists );

			/**
			 * Deprecated constant
			 *
			 * @deprecated
			 */
			define( 'YITH_WCWL_TABLE', $this->table_items );
		}

		/**
		 * Init db structure of the plugin
		 *
		 * @since 1.0.0
		 */
		public function init() {
			$this->add_tables();
			$this->add_pages();

			$this->register_current_version();
		}

		/**
		 * Update db structure of the plugin
		 *
		 * @param string $current_version Version from which we're updating.
		 *
		 * @ince 3.0.0
		 */
		public function update( $current_version ) {
			if ( version_compare( $current_version, '1.0.0', '<' ) ) {
				$this->update_100();
			}

			if ( version_compare( $current_version, '2.0.0', '<' ) ) {
				$this->update_200();
			}

			if ( version_compare( $current_version, '3.0.0', '<' ) ) {
				$this->update_300();
			}

			// TODO (3.1): _update_310() should call ->_add_tables(), to update db structure and size of external id columns.

			$this->register_current_version();
		}

		/**
		 * Register current version of plugin and database sctructure
		 *
		 * @since 3.0.0
		 */
		public function register_current_version() {
			delete_option( 'yith_wcwl_version' );
			update_option( 'yith_wcwl_version', YITH_WCWL_VERSION );

			delete_option( 'yith_wcwl_db_version' );
			update_option( 'yith_wcwl_db_version', YITH_WCWL_DB_VERSION );
		}

		/**
		 * Check if the table of the plugin already exists.
		 *
		 * @return bool
		 * @since 1.0.0
		 */
		public function is_installed() {
			global $wpdb;
			$number_of_tables = $wpdb->query( $wpdb->prepare( 'SHOW TABLES LIKE %s', "{$this->table_items}%" ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery

			return (bool) ( 2 === (int) $number_of_tables );
		}

		/**
		 * Update from 0.x to 1.0
		 */
		private function update_100() {
			flush_rewrite_rules();
		}

		/**
		 * Update from version 1.0 to 2.0
		 *
		 * @since 2.0.0
		 */
		private function update_200() {
			// update tables.
			$this->add_tables();
		}

		/**
		 * Update from version 2.0 to 3.0
		 *
		 * @since 3.0.0
		 */
		private function update_300() {
			// update tables.
			$this->add_tables();

			// update color options.
			$options = array(
				'color_add_to_wishlist',
				'color_add_to_cart',
				'color_button_style_1',
				'color_button_style_2',
				'color_wishlist_table',
			);

			foreach ( $options as $option ) {
				$base_option_name = "yith_wcwl_{$option}";

				$background = get_option( "{$base_option_name}_background" );
				$color      = get_option( "{$base_option_name}_color" );
				$border     = get_option( "{$base_option_name}_border_color" );

				if ( 'color_wishlist_table' !== $option ) {
					$background_hover = get_option( "{$base_option_name}_hover_background" );
					$color_hover      = get_option( "{$base_option_name}_hover_color" );
					$border_hover     = get_option( "{$base_option_name}_hover_border_color" );
				}

				update_option(
					$base_option_name,
					array_merge(
						! empty( $background ) ? array( 'background' => $background ) : array(),
						! empty( $color ) ? array( 'text' => $color ) : array(),
						! empty( $border ) ? array( 'border' => $border ) : array(),
						! empty( $background_hover ) ? array( 'background_hover' => $background_hover ) : array(),
						! empty( $color_hover ) ? array( 'text_hover' => $color_hover ) : array(),
						! empty( $border_hover ) ? array( 'border_hover' => $border_hover ) : array()
					)
				);
			}

			// duplicate options.
			$options = array(
				'yith_wcwl_color_button_style_1'       => array(
					'yith_wcwl_color_ask_an_estimate',
				),
				'yith_wcwl_color_button_style_1_hover' => array(
					'yith_wcwl_color_ask_an_estimate_hover',
				),
				'woocommerce_promotion_mail_settings'  => array(
					'woocommerce_yith_wcwl_promotion_mail_settings',
				),
			);

			foreach ( $options as $original_option => $destinations ) {
				$option_value = get_option( $option );

				if ( $option_value ) {
					foreach ( $destinations as $destination ) {
						update_option( $destination, $option_value );
					}
				}
			}

			// button style options.
			$use_buttons     = get_option( 'yith_wcwl_use_button' );
			$use_theme_style = get_option( 'yith_wcwl_frontend_css' );

			if ( 'yes' === $use_buttons && 'no' === $use_theme_style ) {
				$destination_value = 'button_custom';
			} elseif ( 'yes' === $use_buttons ) {
				$destination_value = 'button_default';
			} else {
				$destination_value = 'link';
			}

			update_option( 'yith_wcwl_add_to_wishlist_style', $destination_value );
			update_option( 'yith_wcwl_add_to_cart_style', $destination_value );
			update_option( 'yith_wcwl_ask_an_estimate_style', $destination_value );

			// rounded corners options.
			$rounded_corners = get_option( 'yith_wcwl_rounded_corners' );
			$radius_value    = 'yes' === $rounded_corners ? 16 : 0;

			update_option( 'yith_wcwl_rounded_corners_radius', $radius_value );
			update_option( 'yith_wcwl_add_to_cart_rounded_corners_radius', $radius_value );
			update_option( 'yith_wcwl_ask_an_estimate_rounded_corners_radius', $radius_value );
		}

		/**
		 * Add tables for a fresh installation
		 *
		 * @return void
		 * @access private
		 * @since 1.0.0
		 */
		private function add_tables() {
			$this->add_wishlists_table();
			$this->add_items_table();
		}

		/**
		 * Add the wishlists table to the database.
		 *
		 * @return void
		 * @access private
		 * @since 1.0.0
		 */
		private function add_wishlists_table() {
			if ( ! $this->is_installed() || version_compare( get_option( 'yith_wcwl_db_version' ), '3.0.0', '<' ) ) {
				$sql = "CREATE TABLE {$this->table_wishlists} (
							ID BIGINT( 20 ) NOT NULL AUTO_INCREMENT,
							user_id BIGINT( 20 ) NULL DEFAULT NULL,
							session_id VARCHAR( 255 ) DEFAULT NULL,
							wishlist_slug VARCHAR( 200 ) NOT NULL,
							wishlist_name TEXT,
							wishlist_token VARCHAR( 64 ) NOT NULL UNIQUE,
							wishlist_privacy TINYINT( 1 ) NOT NULL DEFAULT 0,
							is_default TINYINT( 1 ) NOT NULL DEFAULT 0,
							dateadded timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
							expiration timestamp NULL DEFAULT NULL,
							PRIMARY KEY  ( ID ),
							KEY wishlist_slug ( wishlist_slug )
						) DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;";

				require_once ABSPATH . 'wp-admin/includes/upgrade.php';
				dbDelta( $sql );
			}
		}

		/**
		 * Add the items table to the database.
		 *
		 * @return void
		 * @access private
		 * @since 1.0.0
		 */
		private function add_items_table() {
			if ( ! $this->is_installed() || version_compare( get_option( 'yith_wcwl_db_version' ), '3.0.0', '<' ) ) {
				$sql = "CREATE TABLE {$this->table_items} (
							ID BIGINT( 20 ) NOT NULL AUTO_INCREMENT,
							prod_id BIGINT( 20 ) NOT NULL,
							quantity INT( 11 ) NOT NULL,
							user_id BIGINT( 20 ) NULL DEFAULT NULL,
							wishlist_id BIGINT( 20 ) NULL,
							position INT( 11 ) DEFAULT 0,
							original_price DECIMAL( 9,3 ) NULL DEFAULT NULL,
							original_currency CHAR( 3 ) NULL DEFAULT NULL,
							dateadded timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
							on_sale tinyint NOT NULL DEFAULT 0,
							PRIMARY KEY  ( ID ),
							KEY prod_id ( prod_id )
						) DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;";

				require_once ABSPATH . 'wp-admin/includes/upgrade.php';
				dbDelta( $sql );
			}
		}

		/**
		 * Add a page "Wishlist".
		 *
		 * @return void
		 * @since 1.0.0
		 */
		private function add_pages() {
			wc_create_page(
				sanitize_title_with_dashes( _x( 'wishlist', 'page_slug', 'yith-woocommerce-wishlist' ) ),
				'yith_wcwl_wishlist_page_id',
				__( 'Wishlist', 'yith-woocommerce-wishlist' ),
				'<!-- wp:shortcode -->[yith_wcwl_wishlist]<!-- /wp:shortcode -->'
			);
		}
	}
}

/**
 * Unique access to instance of YITH_WCWL_Install class
 *
 * @return \YITH_WCWL_Install
 * @since 2.0.0
 */
function YITH_WCWL_Install() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid
	return YITH_WCWL_Install::get_instance();
}
class-yith-wcwl-admin.php000064400000024150151327635660011415 0ustar00<?php
/**
 * Admin init class
 *
 * @author YITH
 * @package YITH\Wishlist\Classes
 * @version 3.0.0
 */

if ( ! defined( 'YITH_WCWL' ) ) {
	exit;
} // Exit if accessed directly

if ( ! class_exists( 'YITH_WCWL_Admin' ) ) {
	/**
	 * Initiator class. Create and populate admin views.
	 *
	 * @since 1.0.0
	 */
	class YITH_WCWL_Admin {

		/**
		 * Single instance of the class
		 *
		 * @var \YITH_WCWL_Admin
		 * @since 2.0.0
		 */
		protected static $instance;

		/**
		 * Wishlist panel
		 *
		 * @var string Panel hookname
		 * @since 2.0.0
		 */
		protected $panel = null;

		/**
		 * Link to landing page on yithemes.com
		 *
		 * @var string
		 * @since 2.0.0
		 */
		public $premium_landing_url = 'https://yithemes.com/themes/plugins/yith-woocommerce-wishlist/';

		/**
		 * Tab name
		 *
		 * @var string
		 * @since 1.0.0
		 */
		public $tab;

		/**
		 * Plugin options
		 *
		 * @var array
		 * @since 1.0.0
		 */
		public $options;

		/**
		 * List of available tab for wishlist panel
		 *
		 * @var array
		 * @access public
		 * @since 2.0.0
		 */
		public $available_tabs = array();

		/**
		 * Returns single instance of the class
		 *
		 * @return \YITH_WCWL_Admin
		 * @since 2.0.0
		 */
		public static function get_instance() {
			if ( is_null( self::$instance ) ) {
				self::$instance = new self();
			}

			return self::$instance;
		}

		/**
		 * Constructor of the class
		 *
		 * @return \YITH_WCWL_Admin
		 * @since 2.0.0
		 */
		public function __construct() {
			// install plugin, or update from older versions.
			add_action( 'init', array( $this, 'install' ) );

			// init admin processing.
			add_action( 'init', array( $this, 'init' ) );

			// enqueue scripts.
			add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ), 20 );

			// plugin panel options.
			add_filter( 'yith_plugin_fw_panel_wc_extra_row_classes', array( $this, 'mark_options_disabled' ), 10, 23 );

			// add plugin links.
			add_filter( 'plugin_action_links_' . plugin_basename( YITH_WCWL_DIR . 'init.php' ), array( $this, 'action_links' ) );
			add_filter( 'yith_show_plugin_row_meta', array( $this, 'add_plugin_meta' ), 10, 5 );

			// register wishlist panel.
			add_action( 'admin_menu', array( $this, 'register_panel' ), 5 );
			add_action( 'yith_wcwl_premium_tab', array( $this, 'print_premium_tab' ) );

			// add a post display state for special WC pages.
			add_filter( 'display_post_states', array( $this, 'add_display_post_states' ), 10, 2 );
		}

		/* === ADMIN GENERAL === */

		/**
		 * Add a post display state for special WC pages in the page list table.
		 *
		 * @param array   $post_states An array of post display states.
		 * @param WP_Post $post        The current post object.
		 */
		public function add_display_post_states( $post_states, $post ) {
			if ( (int) get_option( 'yith_wcwl_wishlist_page_id' ) === $post->ID ) {
				$post_states['yith_wcwl_page_for_wishlist'] = __( 'Wishlist Page', 'yith-woocommerce-wishlist' );
			}

			return $post_states;
		}

		/* === INITIALIZATION SECTION === */

		/**
		 * Initiator method. Initiate properties.
		 *
		 * @return void
		 * @access private
		 * @since 1.0.0
		 */
		public function init() {
			$prefix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? 'unminified/' : '';
			$suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';

			$this->available_tabs = apply_filters(
				'yith_wcwl_available_admin_tabs',
				array(
					'settings'        => __( 'General settings', 'yith-woocommerce-wishlist' ),
					'add_to_wishlist' => __( 'Add to wishlist options', 'yith-woocommerce-wishlist' ),
					'wishlist_page'   => __( 'Wishlist page options', 'yith-woocommerce-wishlist' ),
					'premium'         => __( 'Premium Version', 'yith-woocommerce-wishlist' ),
				)
			);

			wp_register_style( 'yith-wcwl-font-awesome', YITH_WCWL_URL . 'assets/css/font-awesome.min.css', array(), '4.7.0' );
			wp_register_style( 'yith-wcwl-material-icons', 'https://fonts.googleapis.com/icon?family=Material+Icons', array(), '3.0.1' );
			wp_register_style( 'yith-wcwl-admin', YITH_WCWL_URL . 'assets/css/admin.css', array( 'yith-wcwl-font-awesome' ), YITH_WCWL_Frontend()->version );
			wp_register_script( 'yith-wcwl-admin', YITH_WCWL_URL . 'assets/js/' . $prefix . 'admin/yith-wcwl' . $suffix . '.js', array( 'jquery', 'wc-backbone-modal', 'jquery-blockui' ), YITH_WCWL_Frontend()->version, true );

			wp_localize_script(
				'yith-wcwl-admin',
				'yith_wcwl',
				array(
					'nonce' => array(
						'preview_promotion_email'             => wp_create_nonce( 'preview_promotion_email' ),
						'calculate_promotion_email_receivers' => wp_create_nonce( 'calculate_promotion_email_receivers' ),
					),
				)
			);
		}

		/**
		 * Run the installation
		 *
		 * @return void
		 * @since 1.0.0
		 */
		public function install() {
			if ( wp_doing_ajax() ) {
				return;
			}

			$stored_db_version = get_option( 'yith_wcwl_db_version' );

			if ( ! $stored_db_version || ! YITH_WCWL_Install()->is_installed() ) {
				// fresh installation.
				YITH_WCWL_Install()->init();
			} elseif ( version_compare( $stored_db_version, YITH_WCWL_DB_VERSION, '<' ) ) {
				// update database.
				YITH_WCWL_Install()->update( $stored_db_version );
				do_action( 'yith_wcwl_updated' );
			}

			// Plugin installed.
			do_action( 'yith_wcwl_installed' );
		}

		/**
		 * Adds plugin actions link
		 *
		 * @param mixed $links Available action links.
		 * @return array
		 */
		public function action_links( $links ) {
			$links = yith_add_action_links( $links, 'yith_wcwl_panel', defined( 'YITH_WCWL_PREMIUM' ), YITH_WCWL_SLUG );
			return $links;
		}

		/**
		 * Adds plugin row meta
		 *
		 * @param array  $new_row_meta_args Array of meta for current plugin.
		 * @param array  $plugin_meta Not in use.
		 * @param string $plugin_file Current plugin iit file path.
		 * @param array  $plugin_data Plugin info.
		 * @param string $status Plugin status.
		 * @param string $init_file Wishlist plugin init file.
		 * @return array
		 * @since 2.0.0
		 */
		public function add_plugin_meta( $new_row_meta_args, $plugin_meta, $plugin_file, $plugin_data, $status, $init_file = 'YITH_WCWL_INIT' ) {
			if ( defined( $init_file ) && constant( $init_file ) === $plugin_file ) {
				$new_row_meta_args['slug'] = 'yith-woocommerce-wishlist';

			}

			if ( defined( 'YITH_WCWL_PREMIUM' ) ) {
				$new_row_meta_args['is_premium'] = true;

			}

			return $new_row_meta_args;
		}

		/* === WISHLIST SUBPANEL SECTION === */

		/**
		 * Register wishlist panel
		 *
		 * @return void
		 * @since 2.0.0
		 */
		public function register_panel() {

			$args = array(
				'create_menu_page'   => true,
				'parent_slug'        => '',
				'page_title'         => 'WooCommerce Wishlist',
				'menu_title'         => __( 'Wishlist', 'yith-woocommerce-wishlist' ),
				'plugin_slug'        => YITH_WCWL_SLUG,
				'plugin_description' => __( 'Allows your customers to create and share lists of products that they want to purchase on your e-commerce.', 'yith-woocommerce-wishlist' ),
				'capability'         => apply_filters( 'yith_wcwl_settings_panel_capability', 'manage_options' ),
				'parent'             => '',
				'class'              => function_exists( 'yith_set_wrapper_class' ) ? yith_set_wrapper_class() : '',
				'parent_page'        => 'yith_plugin_panel',
				'page'               => 'yith_wcwl_panel',
				'admin-tabs'         => $this->available_tabs,
				'options-path'       => YITH_WCWL_DIR . 'plugin-options',
				'help_tab'           => array(
					'main_video' => array(
						'desc' => _x( 'Check this video to learn how to <b>configure wishlist and customize options:</b>', '[HELP TAB] Video title', 'yith-woocommerce-ajax-navigation' ),
						'url'  => array(
							'it' => 'https://www.youtube.com/embed/9hM9PgBVNTg',
							'es' => 'https://www.youtube.com/embed/GwQkNrrHFs4',
						),
					),
					'hc_url'    => 'https://support.yithemes.com/hc/en-us/categories/360003468437-YITH-WOOCOMMERCE-WISHLIST',
				),
			);

			/* === Fixed: not updated theme  === */
			if ( ! class_exists( 'YIT_Plugin_Panel_WooCommerce' ) ) {
				require_once YITH_WCWL_DIR . 'plugin-fw/lib/yit-plugin-panel-wc.php';
			}

			$this->panel = new YIT_Plugin_Panel_WooCommerce( $args );
		}

		/**
		 * Adds yith-disabled class
		 * Adds class to fields when required, and when disabled state cannot be achieved any other way (eg. by dependencies)
		 *
		 * @param array $classes Array of field extra classes.
		 * @param array $field   Array of field data.
		 *
		 * @return array Filtered array of extra classes
		 */
		public function mark_options_disabled( $classes, $field ) {
			if ( isset( $field['id'] ) && 'yith_wfbt_enable_integration' === $field['id'] && ! ( defined( 'YITH_WFBT' ) && YITH_WFBT ) ) {
				$classes[] = 'yith-disabled';
			}

			return $classes;
		}

		/**
		 * Load admin style.
		 *
		 * @return void
		 * @since 1.0.0
		 */
		public function enqueue() {
			global $woocommerce, $pagenow;

			if ( 'admin.php' === $pagenow && isset( $_GET['page'] ) && 'yith_wcwl_panel' === $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
				wp_enqueue_style( 'yith-wcwl-admin' );
				wp_enqueue_script( 'yith-wcwl-admin' );

				if ( isset( $_GET['tab'] ) && 'popular' === $_GET['tab'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
					wp_enqueue_style( 'yith-wcwl-material-icons' );
					wp_enqueue_editor();
				}
			}
		}

		/**
		 * Prints tab premium of the plugin
		 *
		 * @return void
		 * @since 2.0.0
		 */
		public function print_premium_tab() {
			$premium_tab = YITH_WCWL_DIR . 'templates/admin/wishlist-panel-premium.php';

			if ( file_exists( $premium_tab ) ) {
				include $premium_tab;
			}
		}

		/**
		 * Get the premium landing uri
		 *
		 * @since   1.0.0
		 * @author  Andrea Grillo <andrea.grillo@yithemes.com>
		 * @return  string The premium landing link
		 */
		public function get_premium_landing_uri() {
			return $this->premium_landing_url;
		}
	}
}

/**
 * Unique access to instance of YITH_WCWL_Admin class
 *
 * @return \YITH_WCWL_Admin
 * @since 2.0.0
 */
function YITH_WCWL_Admin() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid
	return defined( 'YITH_WCWL_PREMIUM' ) ? YITH_WCWL_Admin_Premium::get_instance() : YITH_WCWL_Admin::get_instance();
}
class-yith-wcwl-privacy.php000064400000022320151327635660011777 0ustar00<?php
/**
 * Privacy class; added to let customer export personal data
 *
 * @author YITH
 * @package YITH\Wishlist\Classes
 * @version 3.0.0
 */

if ( ! defined( 'YITH_WCWL' ) ) {
	exit;
} // Exit if accessed directly

if ( ! class_exists( 'YITH_WCWL_Privacy' ) ) {
	/**
	 * YITH WCWL Exporter
	 *
	 * @since 2.2.2
	 */
	class YITH_WCWL_Privacy extends YITH_Privacy_Plugin_Abstract {

		/**
		 * Constructor method
		 *
		 * @return \YITH_WCWL_Privacy
		 * @since 2.2.2
		 */
		public function __construct() {

			parent::__construct( 'YITH WooCommerce Wishlist' );

			// set up wishlist data exporter.
			add_filter( 'wp_privacy_personal_data_exporters', array( $this, 'register_exporter' ) );

			// set up wishlist data eraser.
			add_filter( 'wp_privacy_personal_data_erasers', array( $this, 'register_eraser' ) );
		}

		/**
		 * Retrieves privacy example text for wishlist plugin
		 *
		 * @param string $section Section of the message to retrieve.
		 *
		 * @return string Privacy message
		 * @since 2.2.2
		 */
		public function get_privacy_message( $section ) {
			$content = '';

			switch ( $section ) {
				case 'collect_and_store':
					$content = '<p>' . __( 'While you visit our site, we’ll track:', 'yith-woocommerce-wishlist' ) . '</p>' .
								'<ul>' .
								'<li>' . __( 'Products you’ve added to the wishlist: we’ll use this to show you and other users your favourite products, and to create targeted email campaigns.', 'yith-woocommerce-wishlist' ) . '</li>' .
								'<li>' . __( 'Wishlists you’ve created: we’ll keep track of the wishlists you create, and make them visible to the store staff', 'yith-woocommerce-wishlist' ) . '</li>' .
								'</ul>' .
								'<p>' . __( 'We’ll also use cookies to keep track of wishlist contents while you’re browsing our site.', 'yith-woocommerce-wishlist' ) . '</p>';
					break;
				case 'has_access':
					$content = '<p>' . __( 'Members of our team have access to the information you provide us with. For example, both Administrators and Shop Managers can access:', 'yith-woocommerce-wishlist' ) . '</p>' .
								'<ul>' .
								'<li>' . __( 'Wishlist details, such as products added, date of addition, name and privacy settings of your wishlists', 'yith-woocommerce-wishlist' ) . '</li>' .
								'</ul>' .
								'<p>' . __( 'Our team members have access to this information to offer you better deals for the products you love.', 'yith-woocommerce-wishlist' ) . '</p>';
					break;
				case 'share':
				case 'payments':
				default:
					break;
			}

			return apply_filters( 'yith_wcwl_privacy_policy_content', $content, $section );
		}

		/**
		 * Register exporters for wishlist plugin
		 *
		 * @param array $exporters Array of currently registered exporters.
		 * @return array Array of filtered exporters
		 * @since 2.2.2
		 */
		public function register_exporter( $exporters ) {
			$exporters['yith_wcwl_exporter'] = array(
				'exporter_friendly_name' => __( 'Customer wishlists', 'yith-woocommerce-wishlist' ),
				'callback'               => array( $this, 'wishlist_data_exporter' ),
			);

			return $exporters;
		}

		/**
		 * Register eraser for wishlist plugin
		 *
		 * @param array $erasers Array of currently registered erasers.
		 * @return array Array of filtered erasers
		 * @since 2.2.2
		 */
		public function register_eraser( $erasers ) {
			$erasers['yith_wcwl_eraser'] = array(
				'eraser_friendly_name' => __( 'Customer wishlists', 'yith-woocommerce-wishlist' ),
				'callback'             => array( $this, 'wishlist_data_eraser' ),
			);

			return $erasers;
		}

		/**
		 * Export user wishlists (only available for authenticated users' wishlist)
		 *
		 * @param string $email_address Email of the users that requested export.
		 * @param int    $page Current page processed.
		 * @return array Array of data to export
		 * @since 2.2.2
		 */
		public function wishlist_data_exporter( $email_address, $page ) {
			$done           = true;
			$page           = (int) $page;
			$offset         = 10 * ( $page - 1 );
			$user           = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data.
			$data_to_export = array();

			if ( $user instanceof WP_User ) {
				$wishlists = YITH_WCWL()->get_wishlists(
					array(
						'limit'   => 10,
						'offset'  => $offset,
						'user_id' => $user->ID,
						'orderby' => 'ID',
						'order'   => 'ASC',
					)
				);

				if ( 0 < count( $wishlists ) ) {
					foreach ( $wishlists as $wishlist ) {
						$data_to_export[] = array(
							'group_id'    => 'yith_wcwl_wishlist',
							'group_label' => __( 'Wishlists', 'yith-woocommerce-wishlist' ),
							'item_id'     => 'wishlist-' . $wishlist->get_id(),
							'data'        => $this->get_wishlist_personal_data( $wishlist ),
						);
					}
					$done = 10 > count( $wishlists );
				} else {
					$done = true;
				}
			}

			return array(
				'data' => $data_to_export,
				'done' => $done,
			);
		}

		/**
		 * Deletes user wishlists (only available for authenticated users' wishlist)
		 *
		 * @param string $email_address Email of the users that requested export.
		 * @param int    $page Current page processed.
		 * @return array Result of the operation
		 * @since 2.2.2
		 */
		public function wishlist_data_eraser( $email_address, $page ) {
			global $wpdb;

			$page     = (int) $page;
			$offset   = 10 * ( $page - 1 );
			$user     = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data.
			$response = array(
				'items_removed'  => false,
				'items_retained' => false,
				'messages'       => array(),
				'done'           => true,
			);

			if ( ! $user instanceof WP_User ) {
				return $response;
			}

			$wishlists = YITH_WCWL()->get_wishlists(
				array(
					'limit'   => 10,
					'offset'  => $offset,
					'user_id' => $user->ID,
					'orderby' => 'ID',
					'order'   => 'ASC',
				)
			);

			if ( 0 < count( $wishlists ) ) {
				foreach ( $wishlists as $wishlist ) {
					if ( apply_filters( 'yith_wcwl_privacy_erase_wishlist_personal_data', true, $wishlist ) ) {
						do_action( 'yith_wcwl_privacy_before_remove_wishlist_personal_data', $wishlist );

						$wishlist->delete();

						do_action( 'yith_wcwl_privacy_remove_wishlist_personal_data', $wishlist );

						/* Translators: %s Order number. */
						$response['messages'][]    = sprintf( __( 'Removed wishlist %s.', 'yith-woocommerce-wishlist' ), $wishlist->get_token() );
						$response['items_removed'] = true;
					} else {
						/* Translators: %s Order number. */
						$response['messages'][]     = sprintf( __( 'Wishlist %s has been retained.', 'yith-woocommerce-wishlist' ), $wishlist->get_token() );
						$response['items_retained'] = true;
					}
				}
				$response['done'] = 10 > count( $wishlists );
			} else {
				$response['done'] = true;
			}

			return $response;
		}

		/**
		 * Retrieves data to export for each user's wishlist
		 *
		 * @param \YITH_WCWL_Wishlist $wishlist Wishlist.
		 * @return array Data to export
		 * @since 2.2.2
		 */
		protected function get_wishlist_personal_data( $wishlist ) {
			$personal_data   = array();
			$props_to_export = apply_filters(
				'yith_wcwl_privacy_export_wishlist_personal_data_props',
				array(
					'wishlist_token'   => __( 'Token', 'yith-woocommerce-wishlist' ),
					'wishlist_url'     => __( 'Wishlist URL', 'yith-woocommerce-wishlist' ),
					'wishlist_name'    => __( 'Title', 'yith-woocommerce-wishlist' ),
					'dateadded'        => _x( 'Created on', 'date when wishlist was created', 'yith-woocommerce-wishlist' ),
					'wishlist_privacy' => __( 'Visibility', 'yith-woocommerce-wishlist' ),
					'items'            => __( 'Items added', 'yith-woocommerce-wishlist' ),
				),
				$wishlist
			);

			foreach ( $props_to_export as $prop => $name ) {
				$value = '';

				switch ( $prop ) {
					case 'items':
						$item_names = array();
						$items      = $wishlist->get_items();

						foreach ( $items as $item ) {
							$product = $item->get_product();

							if ( ! $product ) {
								continue;
							}

							$item_name = $product->get_name() . ' x ' . $item['quantity'];

							if ( $item->get_date_added() ) {
								$item_name .= ' (on: ' . $item->get_date_added() . ')';
							}

							$item_names[] = $item_name;
						}

						$value = implode( ', ', $item_names );
						break;
					case 'wishlist_url':
						$wishlist_url = $wishlist->get_url();

						$value = sprintf( '<a href="%1$s">%1$s</a>', $wishlist_url );
						break;
					case 'wishlist_name':
						$wishlist_name = $wishlist->get_formatted_name();

						$value = $wishlist_name ? $wishlist_name : get_option( 'yith_wcwl_wishlist_title' );
						break;
					case 'dateadded':
						$value = $wishlist->get_date_added();
						break;
					case 'wishlist_privacy':
						$value = $wishlist->get_formatted_privacy();
						break;
					default:
						if ( isset( $wishlist[ $prop ] ) ) {
							$value = $wishlist[ $prop ];
						}
						break;
				}

				$value = apply_filters( 'yith_wcwl_privacy_export_wishlist_personal_data_prop', $value, $prop, $wishlist );

				if ( $value ) {
					$personal_data[] = array(
						'name'  => $name,
						'value' => $value,
					);
				}
			}

			$personal_data = apply_filters( 'yith_wcwl_privacy_export_wishlist_personal_data', $personal_data, $wishlist );

			return $personal_data;
		}
	}
}
class-yith-wcwl-shortcode.php000064400000074417151327635660012332 0ustar00<?php
/**
 * Shortcodes class
 *
 * @author YITH
 * @package YITH\Wishlist\Classes
 * @version 3.0.0
 */

if ( ! defined( 'YITH_WCWL' ) ) {
	exit;
} // Exit if accessed directly

if ( ! class_exists( 'YITH_WCWL_Shortcode' ) ) {
	/**
	 * YITH WCWL Shortcodes
	 *
	 * @since 1.0.0
	 */
	class YITH_WCWL_Shortcode {

		/**
		 * Init shortcodes available for the plugin
		 *
		 * @return void
		 */
		public static function init() {
			// register shortcodes.
			add_shortcode( 'yith_wcwl_wishlist', array( 'YITH_WCWL_Shortcode', 'wishlist' ) );
			add_shortcode( 'yith_wcwl_add_to_wishlist', array( 'YITH_WCWL_Shortcode', 'add_to_wishlist' ) );

			// register gutenberg blocks.
			add_action( 'init', array( 'YITH_WCWL_Shortcode', 'register_gutenberg_blocks' ) );
			add_action( 'yith_plugin_fw_gutenberg_before_do_shortcode', array( 'YITH_WCWL_Shortcode', 'fix_for_gutenberg_blocks' ), 10, 1 );

			// register elementor widgets.
			add_action( 'init', array( 'YITH_WCWL_Shortcode', 'init_elementor_widgets' ) );
		}

		/* === GUTENBERG BLOCKS === */

		/**
		 * Register available gutenberg blocks
		 *
		 * @return void
		 */
		public static function register_gutenberg_blocks() {
			$blocks = array(
				'yith-wcwl-add-to-wishlist' => array(
					'style'          => 'yith-wcwl-main',
					'script'         => 'jquery-yith-wcwl',
					'title'          => _x( 'YITH Add to wishlist', '[gutenberg]: block name', 'yith-woocommerce-wishlist' ),
					'description'    => _x( 'Shows Add to wishlist button', '[gutenberg]: block description', 'yith-woocommerce-wishlist' ),
					'shortcode_name' => 'yith_wcwl_add_to_wishlist',
					'attributes'     => array(
						'product_id'                => array(
							'type'    => 'text',
							'label'   => __( 'ID of the product to add to the wishlist (leave empty to use the global product)', 'yith-woocommerce-wishlist' ),
							'default' => '',
						),
						'wishlist_url'              => array(
							'type'    => 'text',
							'label'   => __( 'URL of the wishlist page (leave empty to use the default settings)', 'yith-woocommerce-wishlist' ),
							'default' => '',
						),
						'label'                     => array(
							'type'    => 'text',
							'label'   => __( 'Button label (leave empty to use the default settings)', 'yith-woocommerce-wishlist' ),
							'default' => '',
						),
						'browse_wishlist_text'      => array(
							'type'    => 'text',
							'label'   => __( '"Browse wishlist" label (leave empty to use the default settings)', 'yith-woocommerce-wishlist' ),
							'default' => '',
						),
						'already_in_wishslist_text' => array(
							'type'    => 'text',
							'label'   => __( '"Product already in wishlist" label (leave empty to use the default settings)', 'yith-woocommerce-wishlist' ),
							'default' => '',
						),
						'product_added_text'        => array(
							'type'    => 'text',
							'label'   => __( '"Product added to wishlist" label (leave empty to use the default settings)', 'yith-woocommerce-wishlist' ),
							'default' => '',
						),
						'icon'                      => array(
							'type'    => 'text',
							'label'   => __( 'Icon for the button (use any FontAwesome valid class, or leave empty to use the default settings)', 'yith-woocommerce-wishlist' ),
							'default' => '',
						),
						'link_classes'              => array(
							'type'    => 'text',
							'label'   => __( 'Additional CSS classes for the button (leave empty to use the default settings)', 'yith-woocommerce-wishlist' ),
							'default' => '',
						),
					),
				),
				'yith-wcwl-wishlist'        => array(
					'style'          => 'yith-wcwl-main',
					'script'         => 'jquery-yith-wcwl',
					'title'          => _x( 'YITH Wishlist', '[gutenberg]: block name', 'yith-woocommerce-wishlist' ),
					'description'    => _x( 'Shows a list of products in wishlist', '[gutenberg]: block description', 'yith-woocommerce-wishlist' ),
					'shortcode_name' => 'yith_wcwl_wishlist',
					'attributes'     => array(
						'pagination'  => array(
							'type'    => 'select',
							'label'   => __( 'Choose whether to paginate items in the wishlist or show them all', 'yith-woocommerce-wishlist' ),
							'default' => 'no',
							'options' => array(
								'yes' => __( 'Paginate', 'yith-woocommerce-wishlist' ),
								'no'  => __( 'Do not paginate', 'yith-woocommerce-wishlist' ),
							),
						),
						'per_page'    => array(
							'type'    => 'number',
							'label'   => __( 'Number of items to show per page', 'yith-woocommerce-wishlist' ),
							'default' => '5',
						),
						'wishlist_id' => array(
							'type'    => 'text',
							'label'   => __( 'ID of the wishlist to show (e.g. K6EOWXB888ZD)', 'yith-woocommerce-wishlist' ),
							'default' => '',
						),
					),
				),
			);

			yith_plugin_fw_gutenberg_add_blocks( $blocks );
		}

		/**
		 * Fix preview of Gutenberg blocks at backend
		 *
		 * @param string $shortcode Shortcode to render.
		 * @return void
		 */
		public static function fix_for_gutenberg_blocks( $shortcode ) {
			if ( strpos( $shortcode, '[yith_wcwl_add_to_wishlist' ) !== false ) {
				if ( strpos( $shortcode, 'product_id=""' ) !== false ) {
					$products = wc_get_products(
						array(
							'type'  => 'simple',
							'limit' => 1,
						)
					);

					if ( ! empty( $products ) ) {
						global $product;
						$product = array_shift( $products );
					}
				}
			}
		}

		/* === ELEMENTOR WIDGETS === */

		/**
		 * Register custom widgets for Elementor
		 *
		 * @return void
		 */
		public static function init_elementor_widgets() {
			// check if elementor is active.
			if ( ! defined( 'ELEMENTOR_VERSION' ) ) {
				return;
			}

			// include widgets.
			include_once YITH_WCWL_INC . 'widgets/elementor/class-yith-wcwl-elementor-add-to-wishlist.php';
			include_once YITH_WCWL_INC . 'widgets/elementor/class-yith-wcwl-elementor-wishlist.php';

			// register widgets.
			add_action( 'elementor/widgets/widgets_registered', array( 'YITH_WCWL_Shortcode', 'register_elementor_widgets' ) );
		}

		/**
		 * Register Elementor Widgets
		 *
		 * @return void
		 */
		public static function register_elementor_widgets() {
			\Elementor\Plugin::instance()->widgets_manager->register_widget_type( new YITH_WCWL_Elementor_Add_To_Wishlist() );
			\Elementor\Plugin::instance()->widgets_manager->register_widget_type( new YITH_WCWL_Elementor_Wishlist() );
		}

		/* === SHORTCODES == */

		/**
		 * Print the wishlist HTML.
		 *
		 * @param array  $atts    Array of attributes for the shortcode.
		 * @param string $content Shortcode content (none expected).
		 * @return string Rendered shortcode
		 *
		 * @since 1.0.0
		 */
		public static function wishlist( $atts, $content = null ) {
			global $yith_wcwl_is_wishlist, $yith_wcwl_wishlist_token;

			$atts = shortcode_atts(
				array(
					'per_page'        => 5,
					'current_page'    => 1,
					'pagination'      => 'no',
					'wishlist_id'     => get_query_var( 'wishlist_id', false ),
					'action_params'   => get_query_var( YITH_WCWL()->wishlist_param, false ),
					'no_interactions' => 'no',
					'layout'          => '',
				),
				$atts
			);

			/**
			 * Extracted variables:
			 *
			 * @var $per_page int
			 * @var $current_page int
			 * @var $pagination string
			 * @var $wishlist_id int
			 * @var $action_params array
			 * @var $no_interactions string
			 * @var $layout string
			 */
			extract( $atts ); // phpcs:ignore

			// retrieve options from query string.
			$action_params = explode( '/', apply_filters( 'yith_wcwl_current_wishlist_view_params', $action_params ) );
			$action        = ( isset( $action_params[0] ) ) ? $action_params[0] : 'view';

			// retrieve options from db.
			$default_wishlist_title = get_option( 'yith_wcwl_wishlist_title' );
			$show_price             = 'yes' === get_option( 'yith_wcwl_price_show' );
			$show_stock             = 'yes' === get_option( 'yith_wcwl_stock_show' );
			$show_date_added        = 'yes' === get_option( 'yith_wcwl_show_dateadded' );
			$show_add_to_cart       = 'yes' === get_option( 'yith_wcwl_add_to_cart_show' );
			$show_remove_product    = 'yes' === get_option( 'yith_wcwl_show_remove', 'yes' );
			$show_variation         = 'yes' === get_option( 'yith_wcwl_variation_show' );
			$repeat_remove_button   = 'yes' === get_option( 'yith_wcwl_repeat_remove_button' );
			$add_to_cart_label      = get_option( 'yith_wcwl_add_to_cart_text', __( 'Add to cart', 'yith-woocommerce-wishlist' ) );
			$price_excluding_tax    = 'excl' === get_option( 'woocommerce_tax_display_cart' );
			$ajax_loading           = 'yes' === get_option( 'yith_wcwl_ajax_enable', 'no' );

			// icons.
			$icon = get_option( 'yith_wcwl_add_to_wishlist_icon' );

			if ( 'custom' === $icon ) {
				$custom_icon       = get_option( 'yith_wcwl_add_to_wishlist_custom_icon' );
				$custom_icon_alt   = apply_filters( 'yith_wcwl_custom_icon_alt', '' );
				$custom_icon_width = apply_filters( 'yith_wcwl_custom_width', '32' );

				$heading_icon = '<img src="' . esc_url( $custom_icon ) . '" alt="' . esc_attr( $custom_icon_alt ) . '" width="' . esc_attr( $custom_icon_width ) . '" />';
			} else {
				$heading_icon = ! empty( $icon ) ? '<i class="fa ' . esc_attr( $icon ) . '"></i>' : '';
			}

			// init params needed to load correct template.
			$template_part     = 'view';
			$no_interactions   = 'yes' === $no_interactions;
			$additional_params = array(
				// wishlist data.
				'wishlist'                 => false,
				'is_default'               => true, // @deprecated since 3.0.7
				'is_custom_list'           => false,
				'wishlist_token'           => '',
				'wishlist_id'              => false,
				'is_private'               => false,

				// wishlist items.
				'count'                    => 0,
				'wishlist_items'           => array(),

				// page data.
				'page_title'               => $default_wishlist_title,
				'default_wishlsit_title'   => $default_wishlist_title,
				'current_page'             => $current_page,
				'page_links'               => false,
				'layout'                   => $layout,

				// user data.
				'is_user_logged_in'        => is_user_logged_in(),
				'is_user_owner'            => true,
				'can_user_edit_title'      => false,

				// view data.
				'no_interactions'          => $no_interactions,
				'show_price'               => $show_price,
				'show_dateadded'           => $show_date_added,
				'show_stock_status'        => $show_stock,
				'show_add_to_cart'         => $show_add_to_cart && ! $no_interactions,
				'show_remove_product'      => $show_remove_product && ! $no_interactions,
				'add_to_cart_text'         => $add_to_cart_label,
				'show_ask_estimate_button' => false,
				'ask_estimate_url'         => '',
				'price_excl_tax'           => $price_excluding_tax,
				'show_cb'                  => false,
				'show_quantity'            => false,
				'show_variation'           => $show_variation,
				'show_price_variations'    => false,
				'show_update'              => false,
				'enable_drag_n_drop'       => false,
				'enable_add_all_to_cart'   => false,
				'move_to_another_wishlist' => false,
				'repeat_remove_button'     => $repeat_remove_button && ! $no_interactions,
				'show_last_column'         => $show_date_added || ( $show_add_to_cart && ! $no_interactions ) || ( $repeat_remove_button && ! $no_interactions ),

				// wishlist icon.
				'heading_icon'             => $heading_icon,

				// share data.
				'share_enabled'            => false,

				// template data.
				'template_part'            => $template_part,
				'additional_info'          => false,
				'available_multi_wishlist' => false,
				'users_wishlists'          => array(),
				'form_action'              => esc_url( YITH_WCWL()->get_wishlist_url( 'view' ) ),
			);

			$wishlist = YITH_WCWL_Wishlist_Factory::get_current_wishlist( $atts );

			if ( $wishlist && $wishlist->current_user_can( 'view' ) ) {
				// set global wishlist token.
				$yith_wcwl_wishlist_token = $wishlist->get_token();

				// retrieve wishlist params.
				$is_user_owner = $wishlist->is_current_user_owner();
				$count         = $wishlist->count_items();
				$offset        = 0;

				// sets current page, number of pages and element offset.
				$queried_page = get_query_var( 'paged' );
				$current_page = max( 1, $queried_page ? $queried_page : $current_page );

				// sets variables for pagination, if shortcode atts is set to yes.
				if ( 'yes' === $pagination && ! $no_interactions && $count > 1 ) {
					$pages = ceil( $count / $per_page );

					if ( $current_page > $pages ) {
						$current_page = $pages;
					}

					$offset = ( $current_page - 1 ) * $per_page;

					if ( $pages > 1 ) {
						$page_links = paginate_links(
							array(
								'base'     => esc_url( add_query_arg( array( 'paged' => '%#%' ), $wishlist->get_url() ) ),
								'format'   => '?paged=%#%',
								'current'  => $current_page,
								'total'    => $pages,
								'show_all' => true,
							)
						);
					}
				} else {
					$per_page = 0;
				}

				// retrieve items to print.
				$wishlist_items = $wishlist->get_items( $per_page, $offset );

				// retrieve wishlist information.
				$is_default     = $wishlist->get_is_default();
				$wishlist_token = $wishlist->get_token();
				$wishlist_title = $wishlist->get_formatted_name();

				$additional_params = wp_parse_args(
					array(
						// wishlist items.
						'count'                => $count,
						'wishlist_items'       => $wishlist_items,

						// wishlist data.
						'wishlist'             => $wishlist,
						'is_default'           => $is_default,
						'is_custom_list'       => $is_user_owner && ! $no_interactions, // @deprecated since 3.0.7
						'wishlist_token'       => $wishlist_token,
						'wishlist_id'          => $wishlist->get_id(),
						'is_private'           => $wishlist->has_privacy( 'private' ),
						'ajax_loading'         => $ajax_loading,

						// page data.
						'page_title'           => $wishlist_title,
						'current_page'         => $current_page,
						'page_links'           => isset( $page_links ) && ! $no_interactions ? $page_links : false,

						// user data.
						'is_user_owner'        => $is_user_owner,
						'can_user_edit_title'  => $wishlist->current_user_can( 'update_wishlist' ) && ! $no_interactions,

						// view data.
						'show_remove_product'  => $show_remove_product && $wishlist->current_user_can( 'remove_from_wishlist' ) && ! $no_interactions,
						'repeat_remove_button' => $repeat_remove_button && $wishlist->current_user_can( 'remove_from_wishlist' ) && ! $no_interactions,

						// template data.
						'form_action'          => $wishlist->get_url(),
					),
					$additional_params
				);

				// share options.
				$enable_share            = 'yes' === get_option( 'yith_wcwl_enable_share' ) && ! $wishlist->has_privacy( 'private' );
				$share_facebook_enabled  = 'yes' === get_option( 'yith_wcwl_share_fb' );
				$share_twitter_enabled   = 'yes' === get_option( 'yith_wcwl_share_twitter' );
				$share_pinterest_enabled = 'yes' === get_option( 'yith_wcwl_share_pinterest' );
				$share_email_enabled     = 'yes' === get_option( 'yith_wcwl_share_email' );
				$share_whatsapp_enabled  = 'yes' === get_option( 'yith_wcwl_share_whatsapp' );
				$share_url_enabled       = 'yes' === get_option( 'yith_wcwl_share_url' );

				// we want spaces to be encoded as + instead of %20, so we use urlencode instead of rawurlencode.
				// phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.urlencode_urlencode
				if ( ! $no_interactions && $enable_share && ( $share_facebook_enabled || $share_twitter_enabled || $share_pinterest_enabled || $share_email_enabled || $share_whatsapp_enabled || $share_url_enabled ) ) {
					$share_title      = apply_filters( 'yith_wcwl_socials_share_title', __( 'Share on:', 'yith-woocommerce-wishlist' ) );
					$share_link_url   = apply_filters( 'yith_wcwl_shortcode_share_link_url', $wishlist->get_url(), $wishlist );
					$share_link_title = apply_filters( 'plugin_text', urlencode( get_option( 'yith_wcwl_socials_title' ) ) );
					$share_summary    = urlencode( str_replace( '%wishlist_url%', $share_link_url, get_option( 'yith_wcwl_socials_text' ) ) );

					$share_atts = array(
						'share_facebook_enabled'  => $share_facebook_enabled,
						'share_twitter_enabled'   => $share_twitter_enabled,
						'share_pinterest_enabled' => $share_pinterest_enabled,
						'share_email_enabled'     => $share_email_enabled,
						'share_whatsapp_enabled'  => $share_whatsapp_enabled,
						'share_url_enabled'       => $share_url_enabled,
						'share_title'             => $share_title,
						'share_link_url'          => $share_link_url,
						'share_link_title'        => $share_link_title,
					);

					if ( $share_facebook_enabled ) {
						$share_facebook_icon        = get_option( 'yith_wcwl_fb_button_icon', 'fa-facebook' );
						$share_facebook_custom_icon = get_option( 'yith_wcwl_fb_button_custom_icon' );

						if ( ! in_array( $share_facebook_icon, array( 'none', 'custom' ), true ) ) {
							$share_atts['share_facebook_icon'] = "<i class='fa {$share_facebook_icon}'></i>";
						} elseif ( 'custom' === $share_facebook_icon && $share_facebook_custom_icon ) {
							$alt_text                          = __( 'Share on Facebook', 'yith-woocommerce-wishlist' );
							$share_atts['share_facebook_icon'] = "<img src='{$share_facebook_custom_icon}' alt='{$alt_text}'/>";
						} else {
							$share_atts['share_facebook_icon'] = '';
						}
					}

					if ( $share_twitter_enabled ) {
						$share_twitter_summary     = urlencode( str_replace( '%wishlist_url%', '', get_option( 'yith_wcwl_socials_text' ) ) );
						$share_twitter_icon        = get_option( 'yith_wcwl_tw_button_icon', 'fa-twitter' );
						$share_twitter_custom_icon = get_option( 'yith_wcwl_tw_button_custom_icon' );

						$share_atts['share_twitter_summary'] = $share_twitter_summary;

						if ( ! in_array( $share_twitter_icon, array( 'none', 'custom' ), true ) ) {
							$share_atts['share_twitter_icon'] = "<i class='fa {$share_twitter_icon}'></i>";
						} elseif ( 'custom' === $share_twitter_icon && $share_twitter_custom_icon ) {
							$alt_text                         = __( 'Tweet on Twitter', 'yith-woocommerce-wishlist' );
							$share_atts['share_twitter_icon'] = "<img src='{$share_twitter_custom_icon}' alt='{$alt_text}'/>";
						} else {
							$share_atts['share_twitter_icon'] = '';
						}
					}

					if ( $share_pinterest_enabled ) {
						$share_image_url             = urlencode( get_option( 'yith_wcwl_socials_image_url' ) );
						$share_pinterest_icon        = get_option( 'yith_wcwl_pr_button_icon', 'fa-pinterest' );
						$share_pinterest_custom_icon = get_option( 'yith_wcwl_pr_button_custom_icon' );

						$share_atts['share_summary']   = $share_summary;
						$share_atts['share_image_url'] = $share_image_url;

						if ( ! in_array( $share_pinterest_icon, array( 'none', 'custom' ), true ) ) {
							$share_atts['share_pinterest_icon'] = "<i class='fa {$share_pinterest_icon}'></i>";
						} elseif ( 'custom' === $share_pinterest_icon && $share_pinterest_custom_icon ) {
							$alt_text                           = __( 'Pin on Pinterest', 'yith-woocommerce-wishlist' );
							$share_atts['share_pinterest_icon'] = "<img src='{$share_pinterest_custom_icon}' alt='{$alt_text}'/>";
						} else {
							$share_atts['share_pinterest_icon'] = '';
						}
					}

					if ( $share_email_enabled ) {
						$share_email_icon        = get_option( 'yith_wcwl_em_button_icon', 'fa-email' );
						$share_email_custom_icon = get_option( 'yith_wcwl_em_button_custom_icon' );

						if ( ! in_array( $share_email_icon, array( 'none', 'custom' ), true ) ) {
							$share_atts['share_email_icon'] = "<i class='fa {$share_email_icon}'></i>";
						} elseif ( 'custom' === $share_email_icon && $share_email_custom_icon ) {
							$alt_text                       = __( 'Share via email', 'yith-woocommerce-wishlist' );
							$share_atts['share_email_icon'] = "<img src='{$share_email_custom_icon}' alt='{$alt_text}'/>";
						} else {
							$share_atts['share_email_icon'] = '';
						}
					}

					if ( $share_whatsapp_enabled ) {
						$share_whatsapp_icon        = get_option( 'yith_wcwl_wa_button_icon', 'fa-whatsapp' );
						$share_whatsapp_custom_icon = get_option( 'yith_wcwl_wa_button_custom_icon' );
						$share_whatsapp_url         = '';

						if ( wp_is_mobile() ) {
							$share_whatsapp_url = 'whatsapp://send?text=' . $share_link_title . ' - ' . urlencode( $share_link_url );
						} else {
							$share_whatsapp_url = 'https://web.whatsapp.com/send?text=' . $share_link_title . ' - ' . urlencode( $share_link_url );
						}

						$share_atts['share_whatsapp_url'] = $share_whatsapp_url;

						if ( ! in_array( $share_whatsapp_icon, array( 'none', 'custom' ), true ) ) {
							$share_atts['share_whatsapp_icon'] = "<i class='fa {$share_whatsapp_icon}'></i>";
						} elseif ( 'custom' === $share_whatsapp_icon && $share_whatsapp_custom_icon ) {
							$alt_text                          = __( 'Share on WhatsApp', 'yith-woocommerce-wishlist' );
							$share_atts['share_whatsapp_icon'] = "<img src='{$share_whatsapp_custom_icon}' alt='{$alt_text}'/>";
						} else {
							$share_atts['share_whatsapp_icon'] = '';
						}
					}

					$additional_params['share_enabled'] = true;
					$additional_params['share_atts']    = $share_atts;
				}
				// phpcs:enable WordPress.PHP.DiscouragedPHPFunctions.urlencode_urlencode
			}

			// filter params.
			$additional_params = apply_filters( 'yith_wcwl_wishlist_params', $additional_params, $action, $action_params, $pagination, $per_page, $atts );

			$atts = array_merge(
				$atts,
				$additional_params
			);

			$atts['fragment_options'] = YITH_WCWL_Frontend()->format_fragment_options( $atts, 'wishlist' );

			// apply filters for add to cart buttons.
			YITH_WCWL_Frontend()->alter_add_to_cart_button();

			// sets that we're in the wishlist template.
			$yith_wcwl_is_wishlist = true;

			$template = yith_wcwl_get_template( 'wishlist.php', $atts, true );

			// we're not in wishlist template anymore.
			$yith_wcwl_is_wishlist    = false;
			$yith_wcwl_wishlist_token = null;

			// remove filters for add to cart buttons.
			YITH_WCWL_Frontend()->restore_add_to_cart_button();

			// enqueue scripts.
			YITH_WCWL_Frontend()->enqueue_scripts();

			return apply_filters( 'yith_wcwl_wishlisth_html', $template, array(), true );
		}

		/**
		 * Return "Add to Wishlist" button.
		 *
		 * @param array  $atts Array of parameters for the shortcode.
		 * @param string $content Shortcode content (usually empty).
		 *
		 * @return string Template of the shortcode.
		 *
		 * @since 1.0.0
		 */
		public static function add_to_wishlist( $atts, $content = null ) {
			global $product;

			// product object.
			$current_product = ( isset( $atts['product_id'] ) ) ? wc_get_product( $atts['product_id'] ) : false;
			$current_product = $current_product ? $current_product : $product;

			if ( ! $current_product || ! $current_product instanceof WC_Product ) {
				return '';
			}

			$current_product_id = yit_get_product_id( $current_product );

			// product parent.
			$current_product_parent = $current_product->get_parent_id();

			// labels & icons settings.
			$label_option        = get_option( 'yith_wcwl_add_to_wishlist_text' );
			$icon_option         = get_option( 'yith_wcwl_add_to_wishlist_icon' );
			$custom_icon         = 'none' !== $icon_option ? get_option( 'yith_wcwl_add_to_wishlist_custom_icon' ) : '';
			$custom_icon_alt     = apply_filters( 'yith_wcwl_custom_icon_alt', '' );
			$custom_icon_width   = apply_filters( 'yith_wcwl_custom_width', '32' );
			$added_icon_option   = get_option( 'yith_wcwl_added_to_wishlist_icon' );
			$custom_added_icon   = 'none' !== $added_icon_option ? get_option( 'yith_wcwl_added_to_wishlist_custom_icon' ) : '';
			$browse_wishlist     = get_option( 'yith_wcwl_browse_wishlist_text' );
			$already_in_wishlist = get_option( 'yith_wcwl_already_in_wishlist_text' );
			$product_added       = get_option( 'yith_wcwl_product_added_text' );
			$loop_position       = get_option( 'yith_wcwl_loop_position' );

			// button label.
			$label = apply_filters( 'yith_wcwl_button_label', $label_option );

			// button icon.
			$icon       = apply_filters( 'yith_wcwl_button_icon', 'none' !== $icon_option ? $icon_option : '' );
			$added_icon = apply_filters( 'yith_wcwl_button_added_icon', 'none' !== $added_icon_option ? $added_icon_option : '' );

			// button class.
			$is_single         = isset( $atts['is_single'] ) ? $atts['is_single'] : yith_wcwl_is_single();
			$use_custom_button = get_option( 'yith_wcwl_add_to_wishlist_style' );
			$classes           = apply_filters( 'yith_wcwl_add_to_wishlist_button_classes', in_array( $use_custom_button, array( 'button_custom', 'button_default' ), true ) ? 'add_to_wishlist single_add_to_wishlist button alt' : 'add_to_wishlist single_add_to_wishlist' );

			// check if product is already in wishlist.
			$exists                      = YITH_WCWL()->is_product_in_wishlist( $current_product_id );
			$added_to_wishlist_behaviour = get_option( 'yith_wcwl_after_add_to_wishlist_behaviour', 'view' );
			$container_classes           = $exists ? 'exists' : false;
			$found_in_list               = $exists ? yith_wcwl_get_wishlist( false ) : false;
			$found_item                  = $found_in_list ? $found_in_list->get_product( $current_product_id ) : false;

			$template_part = $exists && 'add' !== $added_to_wishlist_behaviour ? 'browse' : 'button';
			$template_part = isset( $atts['added_to_wishlist'] ) ? ( $atts['added_to_wishlist'] ? 'added' : 'browse' ) : $template_part;

			if ( $found_in_list && in_array( $template_part, array( 'browse', 'added' ), true ) && 'remove' === $added_to_wishlist_behaviour ) {
				$template_part = 'remove';
			}

			if ( 'remove' === $template_part ) {
				$classes = str_replace( array( 'single_add_to_wishlist', 'add_to_wishlist' ), '', $classes );
				$label   = apply_filters( 'yith_wcwl_remove_from_wishlist_label', __( 'Remove from list', 'yith-woocommerce-wishlist' ) );
			}

			// forcefully add icon when showing button over image, if no one is set.
			if ( ! $is_single && 'before_image' === get_option( 'yith_wcwl_loop_position' ) ) {
				$classes = str_replace( 'button', '', $classes );
			}

			$ajax_loading = 'yes' === get_option( 'yith_wcwl_ajax_enable', 'no' );

			// get wishlist url.
			$wishlist_url = YITH_WCWL()->get_wishlist_url();

			// get product type.
			$product_type = $current_product->get_type();

			$additional_params = array(
				'base_url'                  => yith_wcwl_get_current_url(),
				'wishlist_url'              => $wishlist_url,
				'in_default_wishlist'       => $exists,
				'exists'                    => $exists,
				'container_classes'         => $container_classes,
				'is_single'                 => $is_single,
				'show_exists'               => false,
				'found_in_list'             => $found_in_list,
				'found_item'                => $found_item,
				'product_id'                => $current_product_id,
				'parent_product_id'         => $current_product_parent ? $current_product_parent : $current_product_id,
				'product_type'              => $product_type,
				'label'                     => $label,
				'show_view'                 => yith_wcwl_is_single(),
				'browse_wishlist_text'      => apply_filters( 'yith_wcwl_browse_wishlist_label', $browse_wishlist ),
				'already_in_wishslist_text' => apply_filters( 'yith_wcwl_product_already_in_wishlist_text_button', $already_in_wishlist ),
				'product_added_text'        => apply_filters( 'yith_wcwl_product_added_to_wishlist_message_button', $product_added ),
				'icon'                      => $icon,
				'heading_icon'              => $icon,
				'link_classes'              => $classes,
				'available_multi_wishlist'  => false,
				'disable_wishlist'          => false,
				'show_count'                => false,
				'ajax_loading'              => $ajax_loading,
				'loop_position'             => $loop_position,
				'template_part'             => $template_part,
			);
			// let third party developer filter options.
			$additional_params = apply_filters( 'yith_wcwl_add_to_wishlist_params', $additional_params, $atts );

			$atts = shortcode_atts(
				$additional_params,
				$atts
			);

			// set icon when missing, when on top of image (icon only, icon required).
			if ( ! $is_single && 'before_image' === get_option( 'yith_wcwl_loop_position' ) && ( ! $atts['icon'] || 'custom' === $atts['icon'] ) ) {
				if ( ! $atts['icon'] ) {
					$atts['icon'] = 'fa-heart-o';
				} elseif ( 'custom' === $atts['icon'] && ! $custom_icon && ! $custom_added_icon ) {
					$atts['icon'] = 'fa-heart-o';
				}
			}

			// change icon when item exists in wishlist.
			if ( $atts['exists'] ) {
				if ( $added_icon && ( 'custom' !== $added_icon || $custom_added_icon || $custom_icon ) ) {
					$atts['icon'] = $added_icon;
				} elseif ( strpos( $atts['icon'], '-o' ) !== false ) {
					$atts['icon'] = str_replace( '-o', '', $atts['icon'] );
				}
			}

			if ( 'custom' === $atts['icon'] && $atts['exists'] && $custom_added_icon ) {
				$icon_html         = '<img class="yith-wcwl-icon" src="' . esc_url( $custom_added_icon ) . '" alt="' . esc_attr( $custom_icon_alt ) . '" width="' . esc_attr( $custom_icon_width ) . '" />';
				$heading_icon_html = ! empty( $custom_icon ) ? '<img class="yith-wcwl-icon" src="' . esc_url( $custom_icon ) . '" alt="' . esc_attr( $custom_icon_alt ) . '" width="' . esc_attr( $custom_icon_width ) . '" />' : '';
			} elseif ( 'custom' === $atts['icon'] && $custom_icon ) {
				$icon_html         = '<img class="yith-wcwl-icon" src="' . esc_url( $custom_icon ) . '" alt="' . esc_attr( $custom_icon_alt ) . '" width="' . esc_attr( $custom_icon_width ) . '" />';
				$heading_icon_html = $icon_html;
			} elseif ( 'custom' !== $atts['icon'] ) {
				$icon_html         = ! empty( $atts['icon'] ) ? '<i class="yith-wcwl-icon fa ' . $atts['icon'] . '"></i>' : '';
				$heading_icon_html = ! empty( $atts['heading_icon'] ) ? '<i class="yith-wcwl-icon fa ' . $atts['heading_icon'] . '"></i>' : '';
			} else {
				$icon_html         = '';
				$heading_icon_html = '';
			}

			// set fragment options.
			$atts['fragment_options'] = YITH_WCWL_Frontend()->format_fragment_options( $atts, 'add_to_wishlist' );
			$atts['icon']             = apply_filters( 'yith_wcwl_add_to_wishlist_icon_html', $icon_html, $atts );
			$atts['heading_icon']     = apply_filters( 'yith_wcwl_add_to_wishlist_heading_icon_html', $heading_icon_html, $atts );

			$template = yith_wcwl_get_template( 'add-to-wishlist.php', $atts, true );

			// enqueue scripts.
			YITH_WCWL_Frontend()->enqueue_scripts();

			return apply_filters( 'yith_wcwl_add_to_wishlisth_button_html', $template, $wishlist_url, $product_type, $exists, $atts );
		}

	}
}

YITH_WCWL_Shortcode::init();
class-yith-wcwl-wishlist-factory.php000064400000031260151327635660013640 0ustar00<?php
/**
 * Wishlist Factory class
 *
 * @author YITH
 * @package YITH\Wishlist\Classes\Wishlists
 * @version 3.0.0
 */

if ( ! defined( 'YITH_WCWL' ) ) {
	exit;
} // Exit if accessed directly

if ( ! class_exists( 'YITH_WCWL_Wishlist_Factory' ) ) {
	/**
	 * This class is used to create all Wishlist object required by the plugin
	 *
	 * @since 3.0.0
	 */
	class YITH_WCWL_Wishlist_Factory {
		/**
		 * Retrieve a specific wishlist from ID or token
		 *
		 * @param string|int|bool $wishlist_id Wishlist id or token or false, when you want to retrieve default.
		 * @param string          $context Context; when on edit context, and no wishlist matches selection, default wishlist will be created and returned.
		 * @return \YITH_WCWL_Wishlist|bool Wishlist object or false on failure
		 */
		public static function get_wishlist( $wishlist_id = false, $context = 'view' ) {
			if ( ! $wishlist_id ) {
				return self::get_default_wishlist( false, $context );
			}

			try {
				return new YITH_WCWL_Wishlist( $wishlist_id );
			} catch ( Exception $e ) {
				wc_caught_exception( $e, __FUNCTION__, func_get_args() );
				return false;
			}
		}

		/**
		 * Query database to search for wishlists that matches specific parameters
		 *
		 * @param array $args Array of valid arguments<br/>
		 *              [<br/>
		 *              'id'                  // Wishlist id to search, if any<br/>
		 *              'user_id'             // User owner<br/>
		 *              'wishlist_slug'       // Slug of the wishlist to search<br/>
		 *              'wishlist_name'       // Name of the wishlist to search<br/>
		 *              'wishlist_token'      // Token of the wishlist to search<br/>
		 *              'wishlist_visibility' // Wishlist visibility: all, visible, public, shared, private<br/>
		 *              'user_search'         // String to match against first name / last name or email of the wishlist owner<br/>
		 *              'is_default'          // Whether wishlist should be default or not<br/>
		 *              'orderby'             // Column used to sort final result (could be any wishlist lists column)<br/>
		 *              'order'               // Sorting order<br/>
		 *              'limit'               // Pagination param: maximum number of elements in the set. 0 to retrieve all elements<br/>
		 *              'offset'              // Pagination param: offset for the current set. 0 to start from the first item<br/>
		 *              'show_empty'          // Whether to show empty lists os not<br/>
		 *              ].
		 *
		 * @return \YITH_WCWL_Wishlist[]|bool A list of matching wishlists or false on failure
		 */
		public static function get_wishlists( $args = array() ) {
			$args = apply_filters( 'yith_wcwl_wishlist_query_args', $args );

			try {
				$results = WC_Data_Store::load( 'wishlist' )->query( $args );
				return apply_filters( 'yith_wcwl_wishlist_query', $results, $args );
			} catch ( Exception $e ) {
				wc_caught_exception( $e, __FUNCTION__, func_get_args() );
				return false;
			}
		}

		/**
		 * Query database to count wishlists that matches specific parameters
		 *
		 * @param array $args Same parameters allowed for {@see get_wishlists}.
		 * @return int Count
		 */
		public static function get_wishlists_count( $args = array() ) {
			$args = apply_filters( 'yith_wcwl_wishlists_count_query_args', $args );

			try {
				$result = WC_Data_Store::load( 'wishlist' )->count( $args );
				return apply_filters( 'yith_wcwl_wishlist_count_query', $result, $args );
			} catch ( Exception $e ) {
				wc_caught_exception( $e, __FUNCTION__, func_get_args() );
				return 0;
			}
		}

		/**
		 * Search user ids whose wishlists match passed parameters
		 * NOTE: this will only retrieve wishlists for a logged in user, while guests wishlist will be ignored
		 *
		 * @param array $args Array of valid arguments<br/>
		 * [<br/>
		 *     'search' // String to match against first name / last name / user login or user email of wishlist owner<br/>
		 *     'limit'  // Pagination param: number of items to show in one page. 0 to show all items<br/>
		 *     'offset' // Pagination param: offset for the current set. 0 to start from the first item<br/>
		 * ].
		 * @return int[]|bool Array of user ids, or false on failure
		 */
		public static function get_wishlist_users( $args = array() ) {
			$args = apply_filters( 'yith_wcwl_wishlist_users_query_args', $args );

			try {
				$results = WC_Data_Store::load( 'wishlist' )->search_users( $args );
				return apply_filters( 'yith_wcwl_wishlist_user_query', $results, $args );
			} catch ( Exception $e ) {
				wc_caught_exception( $e, __FUNCTION__, func_get_args() );
				return false;
			}
		}

		/**
		 * Retrieve current wishlist, basing on query string parameters, user or session
		 *
		 * @param array $args Array of arguments<br/>
		 *              [<br/>
		 *              'action_params' // query string parameters
		 *              'user_id'       // user we need to retrieve wishlist for
		 *              'wishlist_id'   // id of the wishlist we need to retrieve
		 *              ].
		 * @return YITH_WCWL_Wishlist|bool
		 */
		public static function get_current_wishlist( $args = array() ) {
			$defaults = array(
				'action_params' => get_query_var( YITH_WCWL()->wishlist_param, false ),
				'user_id'       => isset( $_GET['user_id'] ) ? intval( $_GET['user_id'] ) : false, // phpcs:ignore WordPress.Security.NonceVerification.Recommended
				'wishlist_id'   => false,
			);

			/**
			 * Extracted variables:
			 *
			 * @var $action_params
			 * @var $user_id
			 * @var $wishlist_id
			 */
			$args = wp_parse_args( $args, $defaults );
			extract( $args ); // phpcs:ignore WordPress.PHP.DontExtract

			// retrieve options from query string.
			$action_params = explode( '/', apply_filters( 'yith_wcwl_current_wishlist_view_params', $action_params ) );

			$action = ( isset( $action_params[0] ) ) ? $action_params[0] : 'view';
			$value  = ( isset( $action_params[1] ) ) ? $action_params[1] : '';

			if ( ! empty( $wishlist_id ) ) {
				return self::get_wishlist( $wishlist_id );
			}

			if ( ! empty( $user_id ) ) {
				return self::get_default_wishlist( $user_id );
			}

			if (
				empty( $action ) ||
				! in_array( $action, YITH_WCWL()->get_available_views(), true ) ||
				in_array( $action, array( 'view', 'user' ), true ) ||
				( in_array( $action, array( 'manage', 'create' ), true ) && ! YITH_WCWL()->is_multi_wishlist_enabled() )
			) {
				switch ( $action ) {
					case 'user':
						$user_id = $value;
						$user_id = ( ! $user_id ) ? get_query_var( $user_id, false ) : $user_id;

						return self::get_default_wishlist( intval( $user_id ) );
					case 'view':
					default:
						return self::get_wishlist( sanitize_text_field( $value ) );
				}
			}

			return false;
		}

		/**
		 * Retrieve default wishlist for current user (or current session)
		 *
		 * @param string|int|bool $id      Customer or session id; false if you want to use current customer or session.
		 * @param string          $context Context; when on edit context, wishlist will be created, if not exists.
		 *
		 * @return \YITH_WCWL_Wishlist|bool Wishlist object or false on failure
		 */
		public static function get_default_wishlist( $id = false, $context = 'read' ) {
			try {
				$default_wishlist = WC_Data_Store::load( 'wishlist' )->get_default_wishlist( $id, $context );
				return apply_filters( 'yith_wcwl_default_wishlist', $default_wishlist, $id, $context );
			} catch ( Exception $e ) {
				wc_caught_exception( $e, __FUNCTION__, func_get_args() );
				return false;
			}
		}

		/**
		 * Retrieve default wishlist for current user (or current session)
		 *
		 * @param string|int|bool $id Customer or session id; false if you want to use current customer or session.
		 * @return \YITH_WCWL_Wishlist|bool Wishlist object or false on failure
		 */
		public static function generate_default_wishlist( $id = false ) {
			return self::get_default_wishlist( $id );
		}

		/**
		 * Generate new token for a wishlist
		 *
		 * @return string|bool Brand new token, or false on failure
		 */
		public static function generate_wishlist_token() {
			try {
				$token = WC_Data_Store::load( 'wishlist' )->generate_token();
				return $token;
			} catch ( Exception $e ) {
				wc_caught_exception( $e, __FUNCTION__, func_get_args() );
				return false;
			}
		}

		/**
		 * Retrieve a specific wishlist item from ID
		 *
		 * @param int|\YITH_WCWL_Wishlist_Item|stdClass $item_id Item identifier, or item itself.
		 * @return \YITH_WCWL_Wishlist_Item|bool Wishlist item, or false on failure
		 */
		public static function get_wishlist_item( $item_id = 0 ) {
			if ( is_numeric( $item_id ) ) {
				$id = $item_id;
			} elseif ( $item_id instanceof YITH_WCWL_Wishlist_Item ) {
				$id = $item_id->get_id();
			} elseif ( is_object( $item_id ) && ! empty( $item_id->ID ) ) {
				$id = $item_id->ID;
			} else {
				$id = false;
			}

			if ( $id ) {
				try {
					return new YITH_WCWL_Wishlist_Item( $id );
				} catch ( Exception $e ) {
					return false;
				}
			}
			return false;
		}

		/**
		 * Retrieve item from a wishlist by product id
		 *
		 * @param int|string $wishlist_id Wishlist id or token.
		 * @param int        $product_id  Product ID.
		 * @return YITH_WCWL_Wishlist_Item|bool Item, or false when no item found
		 */
		public static function get_wishlist_item_by_product_id( $wishlist_id, $product_id ) {
			$wishlist = self::get_wishlist( $wishlist_id );

			if ( $wishlist ) {
				return $wishlist->get_product( $product_id );
			}

			return false;
		}

		/**
		 * Query database to search for wishlist items that matches specific parameters
		 *
		 * @param array $args Arguments array; it may contains any of the following:<br/>
		 * [<br/>
		 *     'user_id'             // Owner of the wishlist; default to current user logged in (if any), or false for cookie wishlist<br/>
		 *     'product_id'          // Product to search in the wishlist<br/>
		 *     'wishlist_id'         // wishlist_id for a specific wishlist, false for default, or all for any wishlist<br/>
		 *     'wishlist_token'      // wishlist token, or false as default<br/>
		 *     'wishlist_visibility' // all, visible, public, shared, private<br/>
		 *     'is_default' =>       // whether searched wishlist should be default one <br/>
		 *     'id' => false,        // only for table select<br/>
		 *     'limit' => false,     // pagination param; number of items per page. 0 to get all items<br/>
		 *     'offset' => 0         // pagination param; offset for the current set. 0 to start from the first item<br/>
		 * ].
		 *
		 * @return \YITH_WCWL_Wishlist_Item[]|bool A list of matching items or false on failure
		 */
		public static function get_wishlist_items( $args = array() ) {
			$args = apply_filters( 'yith_wcwl_wishlist_items_query_args', $args );

			try {
				$results = WC_Data_Store::load( 'wishlist-item' )->query( $args );
				return apply_filters( 'yith_wcwl_wishlist_item_query', $results, $args );
			} catch ( Exception $e ) {
				wc_caught_exception( $e, __FUNCTION__, func_get_args() );
				return false;
			}
		}

		/**
		 * Query database to count wishlist items that matches specific parameters
		 *
		 * @param array $args Same parameters allowed for {@see get_wishlist_items}.
		 * @return int Count
		 */
		public static function get_wishlist_items_count( $args = array() ) {
			$args = apply_filters( 'yith_wcwl_wishlist_items_count_query_args', $args );

			try {
				$result = WC_Data_Store::load( 'wishlist-item' )->count( $args );
				return apply_filters( 'yith_wcwl_wishlist_item_count_query', $result, $args );
			} catch ( Exception $e ) {
				wc_caught_exception( $e, __FUNCTION__, func_get_args() );
				return 0;
			}
		}

		/**
		 * Count how many times a specific product was added to wishlist
		 *
		 * @param int $product_id Product id.
		 * @return int Count of times product was added to cart
		 */
		public static function get_times_added_count( $product_id ) {
			try {
				$result = WC_Data_Store::load( 'wishlist-item' )->count_times_added( $product_id );
				return (int) apply_filters( 'yith_wcwl_wishlist_times_added_count_query', $result, $product_id );
			} catch ( Exception $e ) {
				wc_caught_exception( $e, __FUNCTION__, func_get_args() );
				return 0;
			}
		}

		/**
		 * Count how many times a specific product was added to wishlist by the current user
		 *
		 * @param int $product_id Product id.
		 * @return int Count of times product was added to cart
		 */
		public static function get_times_current_user_added_count( $product_id ) {
			try {
				$result = WC_Data_Store::load( 'wishlist-item' )->count_times_added( $product_id, 'current' );
				return (int) apply_filters( 'yith_wcwl_wishlist_times_current_user_added_count_query', $result, $product_id );
			} catch ( Exception $e ) {
				wc_caught_exception( $e, __FUNCTION__, func_get_args() );
				return 0;
			}
		}
	}
}
functions-yith-wcwl.php000064400000052120151327635660011230 0ustar00<?php
/**
 * Functions file
 *
 * @author YITH
 * @package YITH\Wishlist\Functions
 * @version 3.0.0
 */

if ( ! defined( 'YITH_WCWL' ) ) {
	exit;
} // Exit if accessed directly

/* === TESTER FUNCTIONS === */

if ( ! function_exists( 'yith_wcwl_is_wishlist' ) ) {
	/**
	 * Check if we're printing wishlist shortcode
	 *
	 * @return bool
	 * @since 2.0.0
	 */
	function yith_wcwl_is_wishlist() {
		global $yith_wcwl_is_wishlist;

		return $yith_wcwl_is_wishlist;
	}
}

if ( ! function_exists( 'yith_wcwl_is_wishlist_page' ) ) {
	/**
	 * Check if current page is wishlist
	 *
	 * @return bool
	 * @since 2.0.13
	 */
	function yith_wcwl_is_wishlist_page() {
		$wishlist_page_id = YITH_WCWL()->get_wishlist_page_id();

		if ( ! $wishlist_page_id ) {
			return false;
		}

		return apply_filters( 'yith_wcwl_is_wishlist_page', is_page( $wishlist_page_id ) );
	}
}

if ( ! function_exists( 'yith_wcwl_is_single' ) ) {
	/**
	 * Returns true if it finds that you're printing a single product
	 * Should return false in any loop (including the ones inside single product page)
	 *
	 * @return bool Whether you're currently on single product template
	 * @since 3.0.0
	 */
	function yith_wcwl_is_single() {
		return apply_filters( 'yith_wcwl_is_single', is_product() && ! in_array( wc_get_loop_prop( 'name' ), array( 'related', 'up-sells' ), true ) && ! wc_get_loop_prop( 'is_shortcode' ) );
	}
}

if ( ! function_exists( 'yith_wcwl_is_mobile' ) ) {
	/**
	 * Returns true if we're currently on mobile view
	 *
	 * @return bool Whether you're currently on mobile view
	 * @since 3.0.0
	 */
	function yith_wcwl_is_mobile() {
		global $yith_wcwl_is_mobile;

		return apply_filters( 'yith_wcwl_is_wishlist_responsive', true ) && ( wp_is_mobile() || $yith_wcwl_is_mobile );
	}
}

/* === TEMPLATE FUNCTIONS === */

if ( ! function_exists( 'yith_wcwl_locate_template' ) ) {
	/**
	 * Locate the templates and return the path of the file found
	 *
	 * @param string $path Path to locate.
	 * @param array  $var  Unused.
	 *
	 * @return string
	 * @since 1.0.0
	 */
	function yith_wcwl_locate_template( $path, $var = null ) {
		$woocommerce_base = WC()->template_path();

		$template_woocommerce_path = $woocommerce_base . $path;
		$template_path             = '/' . $path;
		$plugin_path               = YITH_WCWL_DIR . 'templates/' . $path;

		$located = locate_template(
			array(
				$template_woocommerce_path, // Search in <theme>/woocommerce/.
				$template_path,             // Search in <theme>/.
			)
		);

		if ( ! $located && file_exists( $plugin_path ) ) {
			return apply_filters( 'yith_wcwl_locate_template', $plugin_path, $path );
		}

		return apply_filters( 'yith_wcwl_locate_template', $located, $path );
	}
}

if ( ! function_exists( 'yith_wcwl_get_template' ) ) {
	/**
	 * Retrieve a template file.
	 *
	 * @param string $path   Path to get.
	 * @param mixed  $var    Variables to send to template.
	 * @param bool   $return Whether to return or print the template.
	 *
	 * @return string|void
	 * @since 1.0.0
	 */
	function yith_wcwl_get_template( $path, $var = null, $return = false ) {
		$located = yith_wcwl_locate_template( $path, $var );

		if ( $var && is_array( $var ) ) {
			$atts = $var;
			extract( $var ); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract
		}

		if ( $return ) {
			ob_start();
		}

		// include file located.
		include $located;

		if ( $return ) {
			return ob_get_clean();
		}
	}
}

if ( ! function_exists( 'yith_wcwl_get_template_part' ) ) {
	/**
	 * Search and include a template part
	 *
	 * @param string $template        Template to include.
	 * @param string $template_part   Template part.
	 * @param string $template_layout Template variation.
	 * @param array  $var             Array of variables to be passed to template.
	 * @param bool   $return          Whether to return template or print it.
	 *
	 * @return string|null
	 */
	function yith_wcwl_get_template_part( $template = '', $template_part = '', $template_layout = '', $var = array(), $return = false ) {
		if ( ! empty( $template_part ) ) {
			$template_part = '-' . $template_part;
		}

		if ( ! empty( $template_layout ) ) {
			$template_layout = '-' . $template_layout;
		}

		$template_hierarchy = apply_filters(
			'yith_wcwl_template_part_hierarchy',
			array_merge(
				! yith_wcwl_is_mobile() ? array() : array(
					"wishlist-{$template}{$template_layout}{$template_part}-mobile.php",
					"wishlist-{$template}{$template_part}-mobile.php",
				),
				array(
					"wishlist-{$template}{$template_layout}{$template_part}.php",
					"wishlist-{$template}{$template_part}.php",
				)
			),
			$template,
			$template_part,
			$template_layout,
			$var
		);

		foreach ( $template_hierarchy as $filename ) {
			$located = yith_wcwl_locate_template( $filename );

			if ( $located ) {
				return yith_wcwl_get_template( $filename, $var, $return );
			}
		}
	}
}

/* === COUNT FUNCTIONS === */

if ( ! function_exists( 'yith_wcwl_count_products' ) ) {
	/**
	 * Retrieve the number of products in the wishlist.
	 *
	 * @param string|bool $wishlist_token Optional wishlist token.
	 *
	 * @return int
	 * @since 1.0.0
	 */
	function yith_wcwl_count_products( $wishlist_token = false ) {
		return YITH_WCWL()->count_products( $wishlist_token );
	}
}

if ( ! function_exists( 'yith_wcwl_count_all_products' ) ) {
	/**
	 * Retrieve the number of products in all the wishlists.
	 *
	 * @return int
	 * @since 2.0.13
	 */
	function yith_wcwl_count_all_products() {
		return YITH_WCWL()->count_all_products();
	}
}

if ( ! function_exists( 'yith_wcwl_count_add_to_wishlist' ) ) {
	/**
	 * Count number of times a product was added to users wishlists
	 *
	 * @param int|bool $product_id Product id.
	 *
	 * @return int Number of times the product was added to wishlists
	 * @since 2.0.13
	 */
	function yith_wcwl_count_add_to_wishlist( $product_id = false ) {
		return YITH_WCWL()->count_add_to_wishlist( $product_id );
	}
}

if ( ! function_exists( 'yith_wcwl_get_count_text' ) ) {
	/**
	 * Returns the label that states how many users added a specific product to wishlist
	 *
	 * @param int|bool $product_id Product id or false, when you want to use global product as reference.
	 *
	 * @return string Label with count of items
	 */
	function yith_wcwl_get_count_text( $product_id = false ) {
		$count              = yith_wcwl_count_add_to_wishlist( $product_id );
		$current_user_count = $count ? YITH_WCWL_Wishlist_Factory::get_times_current_user_added_count( $product_id ) : 0;

		// if no user added to wishlist, return empty string.
		if ( ! $count ) {
			return apply_filters( 'yith_wcwl_count_text_empty', '', $product_id );
		} elseif ( ! $current_user_count ) {
			// translators: 1. Number of users.
			$count_text = sprintf( _n( '%d user', '%d users', $count, 'yith-woocommerce-wishlist' ), $count );
			$text       = _n( 'has this item in wishlist', 'have this item in wishlist', $count, 'yith-woocommerce-wishlist' );
		} elseif ( $count === $current_user_count ) {
			$count_text = __( 'You\'re the first', 'yith-woocommerce-wishlist' );
			$text       = __( 'to add this item in wishlist', 'yith-woocommerce-wishlist' );
		} else {
			$other_count = $count - $current_user_count;
			// translators: 1. Count of users when many, or "another" when only one.
			$count_text = sprintf( _n( 'You and %s user', 'You and %d users', $other_count, 'yith-woocommerce-wishlist' ), 1 === $other_count ? __( 'another', 'yith-woocommerce-wishlist' ) : $other_count ); // phpcs:ignore WordPress.WP.I18n.MismatchedPlaceholders
			$text       = __( 'have this item in wishlist', 'yith-woocommerce-wishlist' );
		}

		$label = sprintf( '<div class="count-add-to-wishlist"><span class="count">%s</span> %s</div>', $count_text, $text );

		return apply_filters( 'yith_wcwl_count_text', $label, $product_id, $current_user_count, $count );
	}
}

/* === COOKIE FUNCTIONS === */

if ( ! function_exists( 'yith_wcwl_get_cookie_expiration' ) ) {
	/**
	 * Returns default expiration for wishlist cookie
	 *
	 * @return int Number of seconds the cookie should last.
	 */
	function yith_wcwl_get_cookie_expiration() {
		return intval( apply_filters( 'yith_wcwl_cookie_expiration', 60 * 60 * 24 * 30 ) );
	}
}

if ( ! function_exists( 'yith_setcookie' ) ) {
	/**
	 * Create a cookie.
	 *
	 * @param string $name     Cookie name.
	 * @param mixed  $value    Cookie value.
	 * @param int    $time     Cookie expiration time.
	 * @param bool   $secure   Whether cookie should be available to secured connection only.
	 * @param bool   $httponly Whether cookie should be available to HTTP request only (no js handling).
	 *
	 * @return bool
	 * @since 1.0.0
	 */
	function yith_setcookie( $name, $value = array(), $time = null, $secure = false, $httponly = false ) {
		if ( ! apply_filters( 'yith_wcwl_set_cookie', true ) || empty( $name ) ) {
			return false;
		}

		$time = ! empty( $time ) ? $time : time() + yith_wcwl_get_cookie_expiration();

		$value      = wp_json_encode( stripslashes_deep( $value ) );
		$expiration = apply_filters( 'yith_wcwl_cookie_expiration_time', $time ); // Default 30 days.

		$_COOKIE[ $name ] = $value;
		wc_setcookie( $name, $value, $expiration, $secure, $httponly );

		return true;
	}
}

if ( ! function_exists( 'yith_getcookie' ) ) {
	/**
	 * Retrieve the value of a cookie.
	 *
	 * @param string $name Cookie name.
	 *
	 * @return mixed
	 * @since 1.0.0
	 */
	function yith_getcookie( $name ) {
		if ( isset( $_COOKIE[ $name ] ) ) {
			return json_decode( sanitize_text_field( wp_unslash( $_COOKIE[ $name ] ) ), true );
		}

		return array();
	}
}

if ( ! function_exists( 'yith_destroycookie' ) ) {
	/**
	 * Destroy a cookie.
	 *
	 * @param string $name Cookie name.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	function yith_destroycookie( $name ) {
		yith_setcookie( $name, array(), time() - 3600 );
	}
}

/* === GET FUNCTIONS === */

if ( ! function_exists( 'yith_wcwl_get_hidden_products' ) ) {
	/**
	 * Retrieves a list of hidden products, whatever WC version is running
	 *
	 * WC switched from meta _visibility to product_visibility taxonomy since version 3.0.0,
	 * forcing a split handling (Thank you, WC!)
	 *
	 * @return array List of hidden product ids
	 * @since 2.1.1
	 */
	function yith_wcwl_get_hidden_products() {
		$hidden_products = get_transient( 'yith_wcwl_hidden_products' );

		if ( false === $hidden_products ) {
			if ( version_compare( WC()->version, '3.0.0', '<' ) ) {
				// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_query
				$hidden_products = get_posts(
					array(
						'post_type'      => 'product',
						'post_status'    => 'publish',
						'posts_per_page' => - 1,
						'fields'         => 'ids',
						'meta_query'     => array(
							array(
								'key'   => '_visibility',
								'value' => 'visible',
							),
						),
					)
				);
				// phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_query
			} else {
				$hidden_products = wc_get_products(
					array(
						'limit'      => - 1,
						'status'     => 'publish',
						'return'     => 'ids',
						'visibility' => 'hidden',
					)
				);
			}

			/**
			 * Array_filter was added to prevent errors when previous query returns for some reason just 0 index.
			 *
			 * @since 2.2.6
			 */
			$hidden_products = array_filter( $hidden_products );

			set_transient( 'yith_wcwl_hidden_products', $hidden_products, 30 * DAY_IN_SECONDS );
		}

		return apply_filters( 'yith_wcwl_hidden_products', $hidden_products );
	}
}

if ( ! function_exists( 'yith_wcwl_get_wishlist' ) ) {
	/**
	 * Retrieves wishlist by ID
	 *
	 * @param int|string $wishlist_id Wishlist ID or Wishlist Token.
	 *
	 * @return \YITH_WCWL_Wishlist|bool Wishlist object; false on error
	 */
	function yith_wcwl_get_wishlist( $wishlist_id ) {
		return YITH_WCWL_Wishlist_Factory::get_wishlist( $wishlist_id );
	}
}

if ( ! function_exists( 'yith_wcwl_get_plugin_icons' ) ) {
	/**
	 * Return array of available icons
	 *
	 * @param string $none_label   Label to use for none option.
	 * @param string $custom_label Label to use for custom option.
	 *
	 * @return array Array of available icons, in class => name format
	 */
	function yith_wcwl_get_plugin_icons( $none_label = '', $custom_label = '' ) {
		$icons = json_decode( file_get_contents( YITH_WCWL_DIR . 'assets/js/admin/yith-wcwl-icons.json' ), true ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents

		$icons['none']   = $none_label ? $none_label : __( 'None', 'yith-woocommerce-wishlist' );
		$icons['custom'] = $custom_label ? $custom_label : __( 'Custom', 'yith-woocommerce-wishlist' );

		return apply_filters( 'yith_wcwl_plugin_icons', $icons, $none_label, $custom_label );
	}
}

if ( ! function_exists( 'yith_wcwl_get_privacy_label' ) ) {
	/**
	 * Returns privacy label
	 *
	 * @param int  $privacy  Privacy value.
	 * @param bool $extended Whether to show extended or simplified label.
	 *
	 * @return string Privacy label
	 * @since 3.0.0
	 */
	function yith_wcwl_get_privacy_label( $privacy, $extended = false ) {

		switch ( $privacy ) {
			case 1:
				$privacy_label = 'shared';
				$privacy_text  = __( 'Shared', 'yith-woocommerce-wishlist' );

				if ( $extended ) {
					$privacy_text  = '<b>' . $privacy_text . '</b> - ';
					$privacy_text .= __( 'Only people with a link to this list can see it', 'yith-woocommerce-wishlist' );
				}

				break;
			case 2:
				$privacy_label = 'private';
				$privacy_text  = __( 'Private', 'yith-woocommerce-wishlist' );

				if ( $extended ) {
					$privacy_text  = '<b>' . $privacy_text . '</b> - ';
					$privacy_text .= __( 'Only you can see this list', 'yith-woocommerce-wishlist' );
				}

				break;
			default:
				$privacy_label = 'public';
				$privacy_text  = __( 'Public', 'yith-woocommerce-wishlist' );

				if ( $extended ) {
					$privacy_text  = '<b>' . $privacy_text . '</b> - ';
					$privacy_text .= __( 'Anyone can search for and see this list', 'yith-woocommerce-wishlist' );
				}

				break;
		}

		return apply_filters( "yith_wcwl_{$privacy_label}_wishlist_visibility", $privacy_text, $extended, $privacy );
	}
}

if ( ! function_exists( 'yith_wcwl_get_privacy_value' ) ) {
	/**
	 * Returns privacy numeric value
	 *
	 * @param string $privacy_label Privacy label.
	 *
	 * @return int Privacy value
	 * @since 3.0.0
	 */
	function yith_wcwl_get_privacy_value( $privacy_label ) {

		switch ( $privacy_label ) {
			case 'shared':
				$privacy_value = 1;
				break;
			case 'private':
				$privacy_value = 2;
				break;
			default:
				$privacy_value = 0;
				break;
		}

		return apply_filters( 'yith_wcwl_privacy_value', $privacy_value, $privacy_label );
	}
}

if ( ! function_exists( 'yith_wcwl_get_current_url' ) ) {
	/**
	 * Retrieves current url
	 *
	 * @return string Current url
	 * @since 3.0.0
	 */
	function yith_wcwl_get_current_url() {
		global $wp;

		/**
		 * Returns empty string by default, to avoid problems with unexpected redirects
		 * Added filter to change default behaviour, passing what we think is current page url
		 *
		 * @since 3.0.12
		 */
		return apply_filters( 'yith_wcwl_current_url', '', add_query_arg( $wp->query_vars, home_url( $wp->request ) ) );
	}
}

/* === UTILITY FUNCTIONS === */

if ( ! function_exists( 'yith_wcwl_merge_in_array' ) ) {
	/**
	 * Merges an array of items into a specific position of an array
	 *
	 * @param array  $array    Origin array.
	 * @param array  $element  Elements to merge.
	 * @param string $pivot    Index to use as pivot.
	 * @param string $position Where elements should be merged (before or after the pivot).
	 *
	 * @return array Result of the merge
	 */
	function yith_wcwl_merge_in_array( $array, $element, $pivot, $position = 'after' ) {
		// search for the pivot inside array.
		$pos = array_search( $pivot, array_keys( $array ), true );

		if ( false === $pos ) {
			return $array;
		}

		// separate array into chunks.
		$i      = 'after' === $position ? 1 : 0;
		$part_1 = array_slice( $array, 0, $pos + $i );
		$part_2 = array_slice( $array, $pos + $i );

		return array_merge( $part_1, $element, $part_2 );
	}
}

if ( ! function_exists( 'yith_wcwl_maybe_format_field_array' ) ) {
	/**
	 * Take a field structure from plugin saved data, and format it as required by WC to print fields
	 *
	 * @param array $field_structure Array of fields as saved on db.
	 *
	 * @return array Array of fields as required by WC
	 */
	function yith_wcwl_maybe_format_field_array( $field_structure ) {
		$fields = array();

		if ( empty( $field_structure ) ) {
			return array();
		}

		foreach ( $field_structure as $field ) {
			if ( isset( $field['active'] ) && 'yes' !== $field['active'] ) {
				continue;
			}

			if ( empty( $field['label'] ) ) {
				continue;
			}

			// format type.
			$field_id = sanitize_title_with_dashes( $field['label'] );

			// format options, if needed.
			if ( ! empty( $field['options'] ) ) {
				$options     = array();
				$raw_options = explode( '|', $field['options'] );

				if ( ! empty( $raw_options ) ) {
					foreach ( $raw_options as $raw_option ) {
						if ( strpos( $raw_option, '::' ) === false ) {
							continue;
						}

						list( $id, $value ) = explode( '::', $raw_option );
						$options[ $id ]     = $value;
					}
				}

				$field['options'] = $options;
			}

			// format class.
			$field['class'] = array( 'form-row-' . $field['position'] );

			// format requires.
			$field['required'] = isset( $field['required'] ) && 'yes' === $field['required'];

			// set custom attributes when field is required.
			if ( $field['required'] ) {
				$field['custom_attributes'] = array(
					'required' => 'required',
				);
			}

			// if type requires options, but no options was defined, skip field printing.
			if ( in_array( $field['type'], array( 'select', 'radio' ), true ) && empty( $field['options'] ) ) {
				continue;
			}

			$fields[ $field_id ] = $field;
		}

		return $fields;
	}
}

if ( ! function_exists( 'yith_wcwl_add_notice' ) ) {
	/**
	 * Calls wc_add_notice, when it exists
	 *
	 * @param string $message     Message to print.
	 * @param string $notice_type Notice type (succcess|error|notice).
	 * @param array  $data        Optional notice data.
	 *
	 * @since 3.0.10
	 */
	function yith_wcwl_add_notice( $message, $notice_type = 'success', $data = array() ) {
		function_exists( 'wc_add_notice' ) && wc_add_notice( $message, $notice_type, $data );
	}
}

if ( ! function_exists( 'yith_wcwl_object_id' ) ) {
	/**
	 * Retrieve translated object id, if a translation plugin is active
	 *
	 * @param int    $id              Original object id.
	 * @param string $type            Object type.
	 * @param bool   $return_original Whether to return original object if no translation is found.
	 * @param string $lang            Language to use for translation ().
	 *
	 * @return int Translation id
	 * @since 1.0.0
	 */
	function yith_wcwl_object_id( $id, $type = 'page', $return_original = true, $lang = null ) {

		// process special value for $lang.
		if ( 'default' === $lang ) {
			if ( defined( 'ICL_SITEPRESS_VERSION' ) ) { // wpml default language.
				global $sitepress;
				$lang = $sitepress->get_default_language();
			} elseif ( function_exists( 'pll_default_language' ) ) { // polylang default language.
				$lang = pll_default_language( 'locale' );
			} else { // cannot determine default language.
				$lang = null;
			}
		}

		// Should work with WPML and PolyLang.
		$id = apply_filters( 'wpml_object_id', $id, $type, $return_original, $lang );

		// Space for additional translations.
		$id = apply_filters( 'yith_wcwl_object_id', $id, $type, $return_original, $lang );

		return $id;
	}
}

if ( ! function_exists( 'yith_wcwl_kses_icon' ) ) {
	/**
	 * Escape output of wishlist icon
	 *
	 * @param string $data Data to escape.
	 * @return string Escaped data
	 */
	function yith_wcwl_kses_icon( $data ) {
		$allowed_icon_html = apply_filters(
			'yith_wcwl_allowed_icon_html',
			array(
				'i'   => array(
					'class' => true,
				),
				'img' => array(
					'src'    => true,
					'alt'    => true,
					'width'  => true,
					'height' => true,
				),
			)
		);

		return wp_kses( $data, $allowed_icon_html );
	}
}

/* === DEPRECATED FUNCTIONS === */

if ( ! function_exists( 'yith_frontend_css_color_picker' ) ) {
	/**
	 * Output a colour picker input box.
	 *
	 * This function is not of the plugin YITH WCWL. It is from WooCommerce.
	 * We redeclare it only because it is needed in the tab "Styles" where it is not available.
	 * The original function name is woocommerce_frontend_css_colorpicker and it is declared in
	 * wp-content/plugins/woocommerce/admin/settings/settings-frontend-styles.php
	 *
	 * @access public
	 *
	 * @param mixed  $name  Name for the input field.
	 * @param mixed  $id    Id for the input field.
	 * @param mixed  $value Value for the input field.
	 * @param string $desc  Description to show under input field (default '').
	 *
	 * @return void
	 * @deprecated
	 */
	function yith_frontend_css_color_picker( $name, $id, $value, $desc = '' ) {
		_deprecated_function( 'yith_frontend_css_color_picker', '3.0.0' );

		$value = ! empty( $value ) ? $value : '#ffffff';

		echo '<div  class="color_box">
				  <table><tr><td>
				  <strong>' . esc_html( $name ) . '</strong>
				  <input name="' . esc_attr( $id ) . '" id="' . esc_attr( $id ) . '" type="text" value="' . esc_attr( $value ) . '" class="colorpick colorpickpreview" style="background-color: ' . esc_attr( $value ) . '" /> <div id="colorPickerDiv_' . esc_attr( $id ) . '" class="colorpickdiv"></div>
				  </td></tr></table>
			  </div>';

	}
}
pocket-holder.php000064400000000505151330370000010005 0ustar00<?php

/**
 * Handy trait provides methods to handle dynamic properties.
 */
trait WPCF7_PocketHolder {

	protected $pocket = array();

	public function pull( $key ) {
		if ( isset( $this->pocket[$key] ) ) {
			return $this->pocket[$key];
		}
	}

	public function push( $key, $value ) {
		$this->pocket[$key] = $value;
	}

}
form-tags-manager.php000064400000034354151330370000010565 0ustar00<?php

/**
 * Wrapper function of WPCF7_FormTagsManager::add().
 */
function wpcf7_add_form_tag( $tag_types, $callback, $features = '' ) {
	$manager = WPCF7_FormTagsManager::get_instance();

	return $manager->add( $tag_types, $callback, $features );
}


/**
 * Wrapper function of WPCF7_FormTagsManager::remove().
 */
function wpcf7_remove_form_tag( $tag_type ) {
	$manager = WPCF7_FormTagsManager::get_instance();

	return $manager->remove( $tag_type );
}


/**
 * Wrapper function of WPCF7_FormTagsManager::replace_all().
 */
function wpcf7_replace_all_form_tags( $content ) {
	$manager = WPCF7_FormTagsManager::get_instance();

	return $manager->replace_all( $content );
}


/**
 * Wrapper function of WPCF7_ContactForm::scan_form_tags().
 */
function wpcf7_scan_form_tags( $cond = null ) {
	$contact_form = WPCF7_ContactForm::get_current();

	if ( $contact_form ) {
		return $contact_form->scan_form_tags( $cond );
	}

	return array();
}


/**
 * Wrapper function of WPCF7_FormTagsManager::tag_type_supports().
 */
function wpcf7_form_tag_supports( $tag_type, $feature ) {
	$manager = WPCF7_FormTagsManager::get_instance();

	return $manager->tag_type_supports( $tag_type, $feature );
}


/**
 * The singleton instance of this class manages the collection of form-tags.
 */
class WPCF7_FormTagsManager {

	private static $instance;

	private $tag_types = array();
	private $scanned_tags = null; // Tags scanned at the last time of scan()
	private $placeholders = array();

	private function __construct() {}


	/**
	 * Returns the singleton instance.
	 *
	 * @return WPCF7_FormTagsManager The singleton manager.
	 */
	public static function get_instance() {
		if ( empty( self::$instance ) ) {
			self::$instance = new self;
		}

		return self::$instance;
	}


	/**
	 * Returns scanned form-tags.
	 *
	 * @return array Array of WPCF7_FormTag objects.
	 */
	public function get_scanned_tags() {
		return $this->scanned_tags;
	}


	/**
	 * Registers form-tag types to the manager.
	 *
	 * @param string|array $tag_types The name of the form-tag type or
	 *                     an array of the names.
	 * @param callable $callback The callback to generates a form control HTML
	 *                 for a form-tag in this type.
	 * @param string|array $features Optional. Features a form-tag
	 *                     in this type supports.
	 */
	public function add( $tag_types, $callback, $features = '' ) {
		if ( ! is_callable( $callback ) ) {
			return;
		}

		if ( true === $features ) { // for back-compat
			$features = array( 'name-attr' => true );
		}

		$features = wp_parse_args( $features, array() );

		$tag_types = array_filter( array_unique( (array) $tag_types ) );

		foreach ( $tag_types as $tag_type ) {
			$tag_type = $this->sanitize_tag_type( $tag_type );

			if ( ! $this->tag_type_exists( $tag_type ) ) {
				$this->tag_types[$tag_type] = array(
					'function' => $callback,
					'features' => $features,
				);
			}
		}
	}


	/**
	 * Returns true if the given tag type exists.
	 */
	public function tag_type_exists( $tag_type ) {
		return isset( $this->tag_types[$tag_type] );
	}


	/**
	 * Returns true if the tag type supports the features.
	 *
	 * @param string $tag_type The name of the form-tag type.
	 * @param array|string $features The feature to check or an array of features.
	 * @return bool True if the form-tag type supports at least one of
	 *              the given features, false otherwise.
	 */
	public function tag_type_supports( $tag_type, $features ) {
		$features = array_filter( (array) $features );

		if ( isset( $this->tag_types[$tag_type]['features'] ) ) {
			return (bool) array_intersect(
				array_keys( array_filter( $this->tag_types[$tag_type]['features'] ) ),
				$features
			);
		}

		return false;
	}


	/**
	 * Returns form-tag types that support the given features.
	 *
	 * @param array|string $features Optional. The feature to check or
	 *                     an array of features. Default empty array.
	 * @param bool $invert Optional. If this value is true, returns form-tag
	 *             types that do not support the given features. Default false.
	 * @return array An array of form-tag types. If the $features param is empty,
	 *               returns all form-tag types that have been registered.
	 */
	public function collect_tag_types( $features = array(), $invert = false ) {
		$tag_types = array_keys( $this->tag_types );

		if ( empty( $features ) ) {
			return $tag_types;
		}

		$output = array();

		foreach ( $tag_types as $tag_type ) {
			if ( ! $invert && $this->tag_type_supports( $tag_type, $features )
			|| $invert && ! $this->tag_type_supports( $tag_type, $features ) ) {
				$output[] = $tag_type;
			}
		}

		return $output;
	}


	/**
	 * Sanitizes the form-tag type name.
	 */
	private function sanitize_tag_type( $tag_type ) {
		$tag_type = preg_replace( '/[^a-zA-Z0-9_*]+/', '_', $tag_type );
		$tag_type = rtrim( $tag_type, '_' );
		$tag_type = strtolower( $tag_type );
		return $tag_type;
	}


	/**
	 * Deregisters the form-tag type.
	 */
	public function remove( $tag_type ) {
		unset( $this->tag_types[$tag_type] );
	}


	/**
	 * Normalizes the text content that includes form-tags.
	 */
	public function normalize( $content ) {
		if ( empty( $this->tag_types ) ) {
			return $content;
		}

		$content = preg_replace_callback(
			'/' . $this->tag_regex() . '/s',
			array( $this, 'normalize_callback' ),
			$content
		);

		return $content;
	}


	/**
	 * The callback function used within normalize().
	 */
	private function normalize_callback( $matches ) {
		// allow [[foo]] syntax for escaping a tag
		if ( $matches[1] == '['
		and $matches[6] == ']' ) {
			return $matches[0];
		}

		$tag = $matches[2];

		$attr = trim( preg_replace( '/[\r\n\t ]+/', ' ', $matches[3] ) );
		$attr = strtr( $attr, array( '<' => '&lt;', '>' => '&gt;' ) );

		$content = trim( $matches[5] );
		$content = str_replace( "\n", '<WPPreserveNewline />', $content );

		$result = $matches[1] . '[' . $tag
			. ( $attr ? ' ' . $attr : '' )
			. ( $matches[4] ? ' ' . $matches[4] : '' )
			. ']'
			. ( $content ? $content . '[/' . $tag . ']' : '' )
			. $matches[6];

		return $result;
	}


	/**
	 * Replace all form-tags in the given text with placeholders.
	 */
	public function replace_with_placeholders( $content ) {
		if ( empty( $this->tag_types ) ) {
			return $content;
		}

		$this->placeholders = array();

		$callback = function ( $matches ) {
			// Allow [[foo]] syntax for escaping a tag.
			if ( '[' === $matches[1] and ']' === $matches[6] ) {
				return $matches[0];
			}

			$tag = $matches[0];
			$tag_type = $matches[2];

			$block_or_hidden = $this->tag_type_supports(
				$tag_type,
				array( 'display-block', 'display-hidden' )
			);

			if ( $block_or_hidden ) {
				$placeholder_tag_name = WPCF7_HTMLFormatter::placeholder_block;
			} else {
				$placeholder_tag_name = WPCF7_HTMLFormatter::placeholder_inline;
			}

			$placeholder = sprintf(
				'<%1$s id="%2$s" />',
				$placeholder_tag_name,
				sha1( $tag )
			);

			list( $placeholder ) =
				WPCF7_HTMLFormatter::normalize_start_tag( $placeholder );

			$this->placeholders[$placeholder] = $tag;

			return $placeholder;
		};

		return preg_replace_callback(
			'/' . $this->tag_regex() . '/s',
			$callback,
			$content
		);
	}


	/**
	 * Replace placeholders in the given text with original form-tags.
	 */
	public function restore_from_placeholders( $content ) {
		return str_replace(
			array_keys( $this->placeholders ),
			array_values( $this->placeholders ),
			$content
		);
	}


	/**
	 * Replaces all form-tags in the text content.
	 *
	 * @param string $content The text content including form-tags.
	 * @return string The result of replacements.
	 */
	public function replace_all( $content ) {
		return $this->scan( $content, true );
	}


	/**
	 * Scans form-tags in the text content.
	 *
	 * @param string $content The text content including form-tags.
	 * @param bool $replace Optional. Whether scanned form-tags will be
	 *             replaced. Default false.
	 * @return array|string An array of scanned form-tags if $replace is false.
	 *                      Otherwise text that scanned form-tags are replaced.
	 */
	public function scan( $content, $replace = false ) {
		$this->scanned_tags = array();

		if ( empty( $this->tag_types ) ) {
			if ( $replace ) {
				return $content;
			} else {
				return $this->scanned_tags;
			}
		}

		if ( $replace ) {
			$content = preg_replace_callback(
				'/' . $this->tag_regex() . '/s',
				array( $this, 'replace_callback' ),
				$content
			);

			return $content;
		} else {
			preg_replace_callback(
				'/' . $this->tag_regex() . '/s',
				array( $this, 'scan_callback' ),
				$content
			);

			return $this->scanned_tags;
		}
	}


	/**
	 * Filters form-tags based on a condition array argument.
	 *
	 * @param array|string $input The original form-tags collection.
	 *                     If it is a string, scans form-tags from it.
	 * @param array $cond The conditions that filtering will be based on.
	 * @return array The filtered form-tags collection.
	 */
	public function filter( $input, $cond ) {
		if ( is_array( $input ) ) {
			$tags = $input;
		} elseif ( is_string( $input ) ) {
			$tags = $this->scan( $input );
		} else {
			$tags = $this->scanned_tags;
		}

		$cond = wp_parse_args( $cond, array(
			'type' => array(),
			'basetype' => array(),
			'name' => array(),
			'feature' => array(),
		) );

		$cond = array_map( static function ( $c ) {
			return array_filter( array_map( 'trim', (array) $c ) );
		}, $cond );

		$tags = array_filter(
			(array) $tags,
			function ( $tag ) use ( $cond ) {
				$tag = new WPCF7_FormTag( $tag );

				if ( $cond['type']
				and ! in_array( $tag->type, $cond['type'], true ) ) {
					return false;
				}

				if ( $cond['basetype']
				and ! in_array( $tag->basetype, $cond['basetype'], true ) ) {
					return false;
				}

				if ( $cond['name']
				and ! in_array( $tag->name, $cond['name'], true ) ) {
					return false;
				}

				foreach ( $cond['feature'] as $feature ) {
					if ( '!' === substr( $feature, 0, 1 ) ) { // Negation
						$feature = trim( substr( $feature, 1 ) );

						if ( $this->tag_type_supports( $tag->type, $feature ) ) {
							return false;
						}
					} else {
						if ( ! $this->tag_type_supports( $tag->type, $feature ) ) {
							return false;
						}
					}
				}

				return true;
			}
		);

		return array_values( $tags );
	}


	/**
	 * Returns the regular expression for a form-tag.
	 */
	private function tag_regex() {
		$tagnames = array_keys( $this->tag_types );
		$tagregexp = implode( '|', array_map( 'preg_quote', $tagnames ) );

		return '(\[?)'
			. '\[(' . $tagregexp . ')(?:[\r\n\t ](.*?))?(?:[\r\n\t ](\/))?\]'
			. '(?:([^[]*?)\[\/\2\])?'
			. '(\]?)';
	}


	/**
	 * The callback function for the form-tag replacement.
	 */
	private function replace_callback( $matches ) {
		return $this->scan_callback( $matches, true );
	}


	/**
	 * The callback function for the form-tag scanning.
	 */
	private function scan_callback( $matches, $replace = false ) {
		// allow [[foo]] syntax for escaping a tag
		if ( $matches[1] == '['
		and $matches[6] == ']' ) {
			return substr( $matches[0], 1, -1 );
		}

		$tag_type = $matches[2];
		$tag_basetype = trim( $tag_type, '*' );
		$attr = $this->parse_atts( $matches[3] );

		$scanned_tag = array(
			'type' => $tag_type,
			'basetype' => $tag_basetype,
			'raw_name' => '',
			'name' => '',
			'options' => array(),
			'raw_values' => array(),
			'values' => array(),
			'pipes' => null,
			'labels' => array(),
			'attr' => '',
			'content' => '',
		);

		if ( $this->tag_type_supports( $tag_type, 'singular' ) ) {
			$tags_in_same_basetype = $this->filter(
				$this->scanned_tags,
				array( 'basetype' => $tag_basetype )
			);

			if ( $tags_in_same_basetype ) {
				// Another tag in the same base type already exists. Ignore this one.
				return $matches[0];
			}
		}

		if ( $this->tag_type_supports( $tag_type, 'name-attr' ) ) {
			if ( ! is_array( $attr ) ) {
				return $matches[0]; // Invalid form-tag.
			}

			$scanned_tag['raw_name'] = (string) array_shift( $attr['options'] );

			if ( ! wpcf7_is_name( $scanned_tag['raw_name'] ) ) {
				return $matches[0]; // Invalid name is used. Ignore this tag.
			}

			$scanned_tag['name'] = strtr( $scanned_tag['raw_name'], '.', '_' );
		}

		if ( is_array( $attr ) ) {
			$scanned_tag['options'] = (array) $attr['options'];
			$scanned_tag['raw_values'] = (array) $attr['values'];

			if ( WPCF7_USE_PIPE ) {
				$pipes = new WPCF7_Pipes( $scanned_tag['raw_values'] );
				$scanned_tag['values'] = $pipes->collect_befores();
				$scanned_tag['pipes'] = $pipes;
			} else {
				$scanned_tag['values'] = $scanned_tag['raw_values'];
			}

			$scanned_tag['labels'] = $scanned_tag['values'];

		} else {
			$scanned_tag['attr'] = $attr;
		}

		$scanned_tag['values'] = array_map( 'trim', $scanned_tag['values'] );
		$scanned_tag['labels'] = array_map( 'trim', $scanned_tag['labels'] );

		$content = trim( $matches[5] );
		$content = preg_replace( "/<br[\r\n\t ]*\/?>$/m", '', $content );
		$scanned_tag['content'] = $content;

		$scanned_tag = apply_filters( 'wpcf7_form_tag', $scanned_tag, $replace );

		$scanned_tag = new WPCF7_FormTag( $scanned_tag );

		$this->scanned_tags[] = $scanned_tag;

		if ( $replace ) {
			$callback = $this->tag_types[$tag_type]['function'];
			return $matches[1] . call_user_func( $callback, $scanned_tag ) . $matches[6];
		} else {
			return $matches[0];
		}
	}


	/**
	 * Parses the attributes of a form-tag to extract the name,
	 * options, and values.
	 *
	 * @param string $text Attributes of a form-tag.
	 * @return array|string An associative array of the options and values
	 *                      if the input is in the correct syntax,
	 *                      otherwise the input text itself.
	 */
	private function parse_atts( $text ) {
		$atts = array( 'options' => array(), 'values' => array() );
		$text = preg_replace( "/[\x{00a0}\x{200b}]+/u", " ", $text );
		$text = trim( $text );

		$pattern = '%^([-+*=0-9a-zA-Z:.!?#$&@_/|\%\r\n\t ]*?)((?:[\r\n\t ]*"[^"]*"|[\r\n\t ]*\'[^\']*\')*)$%';

		if ( preg_match( $pattern, $text, $matches ) ) {
			if ( ! empty( $matches[1] ) ) {
				$atts['options'] = preg_split( '/[\r\n\t ]+/', trim( $matches[1] ) );
			}

			if ( ! empty( $matches[2] ) ) {
				preg_match_all( '/"[^"]*"|\'[^\']*\'/', $matches[2], $matched_values );
				$atts['values'] = wpcf7_strip_quote_deep( $matched_values[0] );
			}
		} else {
			$atts = $text;
		}

		return $atts;
	}

}
js/html5-fallback.js000064400000001203151330370000010270 0ustar00( function( $ ) {
	$( function() {
		var supportHtml5 = ( function() {
			var features = {};
			var input = document.createElement( 'input' );
			var inputTypes = [ 'date' ];

			$.each( inputTypes, function( index, value ) {
				input.setAttribute( 'type', value );
				features[ value ] = input.type !== 'text';
			} );

			return features;
		} )();

		if ( ! supportHtml5.date ) {
			$( 'input.wpcf7-date[type="date"]' ).each( function() {
				$( this ).datepicker( {
					dateFormat: 'yy-mm-dd',
					minDate: new Date( $( this ).attr( 'min' ) ),
					maxDate: new Date( $( this ).attr( 'max' ) )
				} );
			} );
		}
	} );
} )( jQuery );
js/jquery-ui/themes/smoothness/images/ui-icons_454545_256x240.png000064400000015662151330370000020150 0ustar00�PNG


IHDR�E�r@gAMA���a cHRMz&�����u0�`:�p��Q<bKGDE�;�-tIME�
%��'E�IDATx��k�%Gu�m;���!�^�pl0�[�HXȹ�H��:w8;�������g1s����]�&��<�����������Clfq؅!,"�·�Wu׫��{�v�Gwn�>U�UuN=�ΩS��x4g
:�������6!�Agb3� �6+�J�D��0���&�s؇N�_�Bs&&�ڏ>~=��
c{v��j%�_�D����Y�E��F
qܸ�G���
�R�SơC���E�ہ!�'�NP)~��tϷ=;��wהme�BH{s�
S���&�S��6MCP6�z�̐!(�hnE�6�VjնcO��6�y7��iʶ~fK����jڬT0��<�2�����6+��^;�4~%����pxh8�4^/
�������:���2�n6eL�h`~��=u���p����r�n0s��Os횶����Wr��G��h �_
�C��mz�-��=�)w���Fi��#�@u-cթP���O{S�s˛^|B��A�'m	TQ���n`.[��ܫ�
|�Եt�7^�p�����@������pxh8�4^�s���C8�U��k���N��}��A� �栺*�s%�����~�aJYT}�-����1�pT��V�P%�%�?p(�>~�PA.E
�����]�1�^��(��[#��\���ֱWRЃ>4j"m�h��I�@I�>]���y�����ҙ�A6�������E:��q7����n�05A���uP��y
L�8�Pj���K�9
[��E�Z���.�Ȫ����.�L��u֛���L��;X�t�0Ua���2�9�@�às���_!�_	l8�4^/
����@����7���R�?�~¥\4����F�>�R�7F��{�V�C��l9w����T�.[�)�?}ڕk ��Po5o�t�h��z'���\C���V�g�s�kۥb�%��@���6��[�\ܩ۶`�6��Y��s�K���ݞs7��j�A��A�n�X�2G�?1�R6+�u
����W@P"�[�U��|��~��m�m`�3�` ��{H+���"Ȥ�N=��Mϯ�4�U������ `z~S=����n0�o�粤=���	����̙�hJ#[4�ٗ�v4�`5�@b�t
n0�w59��-���̩B���(X'�[����6�7i3�S�P�@	� ���Ĥ�����/7^/
����@�����:��6��U.Eo��w��sX����E{Ls�U�Xc��a��u7W�����v��R҅�l���~���٭��N�v]h���;І���i;M�GC'�{I7�9����J��*F���ͦ�>B\u}A���窞o��<�ȟ�Va���CHP"nLu�m߼]݅�����=e���MF�d�U�f;I��l�;:�D�C�ߪ�iŪ'Ƚǐ�_V�L�:p�s/5V��.�~�{3R[�I����AH�]$��fCT5����Pہ�(Ku��^([ݬ�N4]&��E@���.��`C�5o�J�����@������pxh8�4�'��Җ�:���6�lB'�.D��
��Р��xw�5��%��1\U��)F��:���Jm^�[u��R�d�T�wOt�I��&~|Uͩz��q;j~đ
��T�s�D�_S>�ܵ���	��������T��G4������m�������5'W�:m��" �VH�#t|dM‚��nz����:�um���?bG��A�-<[ez#�S/\���х��yD�V{���+��
@�����u6y�#�L����<"�<]5��d�	��l�j&�˺'Y�%i��F��b��g�J�AH�}������P�=o2�O౩��pxh8�4�$��J�l_ҿ����A�wp(��
UN�Q��߆�'$k��L�e>��Wƞ�%��ײ�:��Rf���Q�k����b½�?�.�u���x8��L���Pr�07j�N���װ�����OfX˼I��\�9���9T�n?���S�#��Z֙��kk@�g��?m�b�i��ʘ,������k�ċr�]��=
JO��E�*We~� 4�	-�E,dN�9�wuʬq�w>�c�3��e|�q���Ew��Kځ3%�,y ]�Y����$p��)C�Ǣ�����yR�<KۿK}�AHD`���!�@}En��evp�7��/
4shV��TʉX������J`ג�	~\#gᅑ./T����!�I�j|��*��<^�T���@+�]��uƜ��O�	��e��ә�"�O>������<Y\��U�����s�h&(� 8���0h'yW����ǣ��)����:�)yMj�m�v/	��୅{��Gy�N�h�/�(�?_��i�Rr5^��z�,b��\�S
�,Oqx�YM`�/���*�6�x���g2��
%sT2iW��6�B�E!S�r4�G-2-ՕAE�~�5Ν���f�Ӆ�^�f%��ؑ�VU� ڹ<��b2��a@`�F�^3�ڦ��D2�7	�\����9�F�U��K4�s�׼2t����;_E�!��|�P�D�N'W:�$n�|�XN�r`�b=b��?L���K3���ԍ�E
��2u�����sXf�0	��@0J��
��pxh8�4^F�e�N���1[��7�0�Ԩ�et�ŷ�C��_޲�H�~D�ܬ�|����O'��P����Xb���G�5�E�9`^a�����Gy�3�T�>�^�(/檜���}cݾ~�[��"�����ɯ�����x�·�z&�$u7@�L�&����i��r=G���g��jw)ZLZ�sܗ�~�M+�@�>�7p�Z��O�I��'y#�GSA��S�^z��U����K�!�;4�Og�0͂��_��]%=��e`�d
�ԣ��=K?RP��z��1�2�Ӎ�-���x9u��J�Z�[�Ԛ�[����@b�yvp#��a�i.�\�w���iC���#Y�.�ǵꚸ�M*�R��6ȩ�-~�����fZ���>�Ohզ:�as�|3����|��*��f��0S}a?���~�o�V�J�]@l,5���}c×%������E�SԸ��ՙ�_T����	�!co�����`_Xp����j�9�C�>|"qO�ꁾ�K������t��͝��i��'����
�t #{�P��&re���j�5���hv�^�M�8����G�E�����h��&��]@���X֔��Af����a���J�ɂH�*����_�VnX3�u�g��<}��h���r������PPv!�o��[@�[�D�F- 2��)�o���C��O�SP���{y��#\��_�.1�C���|ܼ���]�,�ҥKY단K���ss��ͅ�d
��w�2L)�߰O˾��e+������'HL�$�1�
�?��Ѽ�z>��!�0�r>��:x���Gt����b�*���h8�.����pxh8�4��6V��tU��9Oyw莇�!+�IT
���.Z��v��Z���F�
"�p������S�΃ѯ��?���!�p!�B|����5Cޞ�H��(zz����i8��[97Q{���
��z_b�:L��XTO>��٭�E� {�R\)|er}�o+�BO�����#/������}W�H6����gvs!��Pf^��3d"#Qg���h��8��la��0`X�J&�ɂ�`7�;a�	E
E���O�8��SҿL�D�_���:Z�Q�z�2��*T5�5�XQ{8!�Wf���S~����!�a�8�9�'J�HSle'�@��&3�Y�E,��_�ɷx�1��O�	�WP�1E�#��u�3j�k�E��y�g\iQ�bp��-���;��;�ť\���{�ǔ����U5e�)�6t����i,�U��J6���os&��X!���؉N���3�˔��A�!�
:�)��OFH*V��3��9g��[yT��,�FU���
&_C�<����ҫ�(_��?(����W1p�?����OW	�p�!�3J8Ҩ��o���W�d��'�`�9�MYa,���3�˳|�Cl�S�Ŧ���.׳wU!�ܟ��z�Bļ�:���b ؑ�r@j�����vx���_����	E�4�X�3|��8Wi�5gq�qp/�+�3v+��s�+Q7�]��.v��)����g�p
(6G���,(6G�?�Xd�� �o����u�'p����0M�:��n���C��f�F�/�?>m�O�΅ؑ�T^�r���R[M)<#���5Ќݤ�>;rZs��[�{u���Y����0c?��]��y�Um-��~��/d�|d�7�?�C
��e_>Dѡ���Wg���>�i�K��ݑX�8ƫ
"��J�fqvq�xh8��������p���_�]|-Y��w
:�����o\��+
�>%����W��7:������r.�\����+t�W��#��*�Oԃؗ��q!�TFQu�~rÁ���	&(�_�����=��q~�}�����`���_�Ƴ���CN%�E�Hs���
�h�k�Q��EϬ����6����\P�O6/���I
m!���ƹC�lg>��҅ 1�O 6X}G��.M�.��W�@@,��T�K�D���6���k,s?ofR"����vޣM�t�2'zlY�=@'�������Ӥ>�U�?R�ω;}�]q?k�o���)\LXf�Ӝ�4��X� ��j��JĽ�A���onLٮg��{�S�a���D���Vf���2��n�2���td�cj�˚k��$��O��ng��y���v'����C�4��6ާ�~4�=MI�3������d��F9�ݑ��W=	dH!�R����
�u�}�8�o�=�-�p`$��^���/O�e4V<F^�pxh8�4^/
��<:F�@Ǫ1�b�
@ȆEԻ#!�'x�H�m:VK�-������
OK�B��H��a� B��QЏH)�DP��<�wW���~��-���`���*�d�ބC)^H~M_T^th�S<ې�����@_.���UvFm��Ȋ�Z@}�M 6���B鯍�yXR�	�>��F/X������[�I�I
V=z�d1a�b�����X�C
:��oF��E,(�����`�p��4�l�bZ�D�'OW��A]4��RCC�-yЍ�i���s'/��U�S�$�C��C#\r=��h�Lݒ���ï6^/
����@��[�D*->
���M�k_��>��zlI�Т�������3,r���uX(�+d��<��d���B�qv�%^I�g��P�Y��=�:�-�*��%���]~�E�#g�r�bo��n�c���LE8����ȝ�6ΰ��O;�㚌�u5�����>�z�A��C���&�C�&����g��I����ZP�
�b�s�b/�p.�����o'G�+���o^�_��n\!�	�噻/�������fvq�=���fJ�>y�O���eĢH���ES|f���D<�`g�iz�I��>�7p5SZت!�{��y{�~�=4�@���#��˹���5�"p@y�U8ZS���q�	�?*6�6x{���ϖ/
����@Ñ
��<�����п�8����~�o���>U8/���yu�67s����|��W�ٿpo.P��<]ץ�Ɵ�/�D��oz��7hz
�! ����6�*�Uy�jr��6~`�oK?�B?���o˿9�iic�.���ͅ��%�w�vms%��]���\7}����U�]��7��%~��T�����_|���JOvh���務����Ѹ[��鄚�����{ [�@+r.����~�ٳ�u��ĭ����O"��2��#��f�`Ͽ=�MM���v�����7Q}�����WL�ZnJߔ����B��� &�����+�$��U��=	;T�=�����(�r����/K�j���+�o�د~jI8&e �1�Uyz;���6~h�oK?�B?���o˿)~�yUY: u��R��XX�
^���fN��K��)F�i޲i�~�o�����8�K���X�/�pu�97���:dس�~�o����!
��6^/
����@�����*�l��;ݣdm�xr��]��1t(�X�nM�^�
j��!!/6��n���t�1P��;FXQ��A^��qƍt�C������Tx�Dq���ۜB��k ���3|�|���9/`����/
����@������pl]h��^@���l!���=�mV$!���3h�Z�`�꠳9
�`�x_� akݞ�=�k�.|�!,��!ثØ�~��A+���l[�������������U?�d0���ğrB0_�S<ҥ�I_���~?������C�W��S|3ȇɲ_��>����N���ք�(�azX�>E����=�<�Y�=��ku��o��������	��U�X��0�%tEXtdate:create2016-09-14T13:32:39-04:00z��%tEXtdate:modify2015-06-01T10:37:21-04:00(An�tEXtSoftwareAdobe ImageReadyq�e<IEND�B`�js/jquery-ui/themes/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png000064400000000606151330370000021324 0ustar00�PNG


IHDR���A�gAMA���a cHRMz&�����u0�`:�p��Q<bKGD������	X��tIME�	
"��!�IDATH��ϱ
a����\!V��J#X���ЋD}�
.f�>���>��P�կx���x���q��жuɚq���f+��6���[��\�‡����כW�T4r��6:]V:�,
(�Ŵ�8�yG-(d��	H%tEXtdate:create2016-09-14T13:34:08-04:00_Dn�%tEXtdate:modify2016-09-14T13:34:08-04:00.�IEND�B`�js/jquery-ui/themes/smoothness/images/ui-bg_glass_75_dadada_1x400.png000064400000000503151330370000021363 0ustar00�PNG


IHDR���DgAMA���a cHRMz&�����u0�`:�p��Q<bKGD���1�tIME�	
"��!KIDAT8�cx����a�"*���n{11�abgb4a����)���&�v�����1101�01�fb\�� 3�EC� _>o%tEXtdate:create2016-09-14T13:34:08-04:00_Dn�%tEXtdate:modify2016-09-14T13:34:08-04:00.�IEND�B`�js/jquery-ui/themes/smoothness/images/ui-icons_2e83ff_256x240.png000064400000011104151330370000020356 0ustar00�PNG


IHDR��IJ�gAMA���a cHRMz&�����u0�`:�p��Q<,PLTE.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��.��)�:wctRNSXG|"2���wf�����ZNz�@@ef�S�F��ccaM�hmz�s��}�����䁎]����������b�p�
Ιi�8*�y�㧁Xȓ�د�͐����ǫ���bKGD�HtIME�
%��'E�IDATx��]c�޻�$,�'$h	��)$uZ7I_�B"H1i���R����kgg���d�l����>�ٽ��=Y���� �d�]ز��s:w/��	f@e�,!�A�$���HW�	��O�|��FM�Z ' ��1��d���9� ��v<�AAr�k�eN���
f����s��an�l�줰.�`�����vb���V��L�tm�)�u\vG(""""""��8sOp�~P¢5��{BK��F���|�lAH־QJ,`�4�.&�J�'�T�o2b���ؓlYQI<�E`	
6��-X�lֈ�VXH��|#j�`E��q��>B�:�qџ�C7�?���xt��v*��ݹ�8�P�6�_��d�Y���J�7��%�߾�A,�U�4��IGb�(��W�
��pB� �k.��b*X�&kVfvZ��?k�	y��Ya�R�o��ڨ�3D�O�HnoH��6ݩ�~
�q��c2""""""Ⲣ��ԎnFb9������ͷ�J
��y��j�Jxsj�Nev'`���dU�۷ķT_�`�����'yo-��/����jf>��%������p}��b�%g�l�:`O�2Lj��-6�.�`=��3��`�����f7ֽu�C�l�!�Q�8s0���x��	��v��I���i)��C۷�#��W	��3�{=`���DDDDDD�g��
yU�Rb��E�:h���������Z���~9јf��vv�_�촸�;�{������%0��}z	{�]	p��]$y>���a�W������,�����:�t�X�a�uO<��m��@R���HAb�!w�(xǰ�6� �=�b%����wzRS�	,:3�﷤�6�1!��!!»�\�a�)yc$������G]���������?�,n����7�s���V�u�����n ��R�(D�6C��e���j
)
��.��t~a�g�e��^��z���@I��R�@��l�s�R"S�Oa��е�b}+X�o��(	rX=�P�����l 9�+4��:X���u�	�pL�(jW,�)l�2[0��hw��3Ї��tE�#"""".2�'d��2�����
e�FC_�7�씮�KŌ&g~>��1�X\!��R��&��H�Z%���!r���
B
�h�O?���pj����lgk�J�{=�`����	z��*�&�� -��!dd�e���A��z_�q����~wY��p�7�|0���������TG$!	&��I�|��|�����@4z</��5o��_\�v��w��%@�.&�4)���6��5xM�i�Ju��u�����U?Zb+���!p�:3!o���Ȋ��I���w�$䂼�$�	
a���=��d@�	(�5� "�*}�f}᠜�:�'���!�����
‡P�����M�Y@�?�oz����Յd�Ƿ`��BCH^�U�P�0,`����T�Qbr��j�~��1�;cL���S`\�?vZ' 4pz9�M憀�l�����
�����X����*2���p`��?�����\�Y6-���c��K8;�Z���]��=�����������w���;�X�M�=��IS�tMvW���5�O�}��hkN.��6�?�4#�E���ّ]��4����L_�S��5���i2=��rW���h�+g.m���v���]��D�-���7o�_z:�9f~#��p�2y�3��������8����X
cC����m$��O��B��Wrmp��dg�i��+Mt�	(W�n�p�&Z6ʤ}g�?^~�_�
*ן�8�����O�V�|jo�v-�y`{S�A)�T�z��"�������-م-������tH,�X�������F�!G@9I5�-i��R���`�k(��&���>�_v�+O�B��T�������Ą1��-"��b�J|� ���R�C�~�9��`(F�H��D��D�I�~rmt�o�*y��nw5yj�=��@%c��]-+�o�;�yA���АE�;U��S@1�!��;��/�?�S�������&_�o��?��g�N�$j.��E~��eo"�>�'O���1(�C]:����7��``,�!���EDDDDlC�0�\98dy�_�ha��}�A����ߗ��|��|�����Ξҿ����
�wnܪ���
sYbHdz�	ި�4��p��`���b!Le]cm�d`I������.���)n�t#6e/YW���#]�L;zs�L��r����rZ�g�\-TB�^�7
@�7����P@>��'��FꝦb1
xHE�T�j_;S�1��j�7�T����%W����{ڢ����O�P�nS��L!w�@�Y��E@�2��&|^}|r��KM��_�%0�8
�"1�
 ��;X����<m2�Q�~��_��ld$z}�`��	5>Q���j��3S��xY`aׄA��|>7��l�c�X��p�xH<,�s�ɉx|2��8	�9��j�P��F8zK�w��/��1�*:@�^&}T�#�����	�`�cp�O�g���i趜L􂕜�T3Ə���}*�(�`��	���.�ŽV��3��+��I�f��u�?��o�c]D1kT� SKzY}`���}<~����*}�0F;l<#""":c�OG��NM1߽�v�6��/�z�~����+u�W‹򉣾k����ř�;y6+��u�hU�TTm�4���m�����m=�T��z�ϼ��!��
_G�S׃�'�rm��-R�^���ksw�5�I��B�F�f&xc��_o/�����t��{-��62̶H@^���M@N�<�q�Ӱ�6R�d���#�6�`6�m���
�	�e��RwcR�l��(_�)tx�	x�2�
pz�9`�����k�>�D
~:?���8�
�=8��S�#������V��Բ,<�a�}��jEDDDDD�#�xրqdVzC�vv���Ϟ i)ΐ�R��ȠB�Z�����mN|�%C�C�AL
Lʴ������8�@�E�`���00%�X��:Y��>W0#�Na�
q�?`%6h+�a��@��9`�O�ޱ�@�]? """"�r��EG�_?>Dg7��rt�>'W�G
?��}�q���E���?h+�K��$@��Ɋ�c��Y��뷓�����1�m��BF��Z��| �5Ƿan�gW2��@}
z��fչ�#}��SP�����ן�	$��P��K��J�W���PX��1��j^X���J4&�R�F�3p�μ�`t����7	�����_.G��.���Ky^�˷�#"""":����!��r���!�B2=F�Vn�^W9x����Udz���ܶ��r�W��Z>U��i%�Gi���T��W:���}�tu������w�t��u%��
>=Żá�<���[�t�{f�	F�O?�$�q���wz����øC,�`
������|R�>��3�,�~�Q(ؾ�B|p"J�W& <ԧo�1�����!%H~*KN��A�,�Z=	�I�NR@e��j�$i�t�5�k~\��Ħc]�m�06��tu���alڕ��J��#""""""<��3�{���$�_XA�'@�?z�o�I��7�L��]�-�����)�n$�������Aׄ�}�>#���@���I�uX�\�oZ@q���r�콙���v�j���;^�~l����ϯ�]-����)����h�Ϗ
-�
�������T�&�!�.%tEXtdate:create2016-09-14T13:32:39-04:00z��%tEXtdate:modify2015-06-01T10:37:21-04:00(An�tEXtSoftwareAdobe ImageReadyq�e<IEND�B`�js/jquery-ui/themes/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png000064400000000504151330370000021166 0ustar00�PNG


IHDR���DgAMA���a cHRMz&�����u0�`:�p��Q<bKGD���1�tIME�	
"��!LIDAT8�cx���0�Fuógύ��ab���x���+�7&F&FN&�oL�_�3|dfbX��I��Ax�<���0:�� �%tEXtdate:create2016-09-14T13:34:08-04:00_Dn�%tEXtdate:modify2016-09-14T13:34:08-04:00.�IEND�B`�js/jquery-ui/themes/smoothness/images/ui-icons_888888_256x240.png000064400000015707151330370000020175 0ustar00�PNG


IHDR�E�r@gAMA���a cHRMz&�����u0�`:�p��Q<bKGD�I�( tIME�
%��'E�IDATx��{�eE}�?H(bX�!�²[R�`����J�H��!w��M�b�ݱ�F�Ѩh����������ڥ�XT�#�D
T�;"ba�Rqw}�����G��)b����}N��9�νsO���sϯ�Ow�~�8�����<��3����@��@F����3��� ��
��$����	+�\���$��������	�����Ook�؞�0�Z	"���"`Jy��Qz�B7i��Q'.�����q���I�v�C���T���2��m�k��5e[7��������G�tͩ�k��!HL�^?3dJ��[��
f�Z���S0�Ms��}`�����T8�t��US�˴�����
��@1�a�`��fe��k���ï6^/
����@������px{�|�A��Q���"���i��O����=�]�}����=���\�z��\��mc�-��ܷ>���(�DЯ�'�L�6=�����;�����㴓l���������ۧ�=�����M/>��ڠ̓6�(�ڬ��0�����$%GI_�km��+�$�.q��
�W7~!����pxh8�4^/
�����ƐQG8�U��k���V��}��A� �栺*�s%�����~�aJYT}�-��c*v�ۭ.�J�=C
`+~�P}����\��=.�4���3���%4Ph�
�F,�_\���ֱWRЃ>4j"m�h��i�@I��zJҼ�y
ts�Y���@�ee'��C뱅r��jm܍��C�&��[�LM�%m{�e^�.�%��f��lN��źuѶd{��$�*�"n�K9(��u��f��c/ӯ<��-�!LU��i���{�&��0�l��W��W/
����@������pxh8�͡�������p��h�x��ѮO)�L��Q.�ޫU@�P--[��j��j6�ƵKǎ�ɟ���5�r	�7���p�h�ۊ{�]Zn�!�w��3ڹ��R���z�z[�t�a.��m[�uD�,i�9Хcw�nϹ�Cz�Ơ�� G�m,o}�R�f��NaÔ�
J�uˁ�
]�o�ۯ�-�M�u`f�]�g�J(��U@�E ��N=��Mϯ�,�U������ `z~S=����n0�o�粤=���	����̙�hJC,���K���J��s �/��&����޸咲C��9Uhua��v�^u9���&m�t�jc�(��0�H�@�ᗂ/
����@������kR�]y{L�*���k����,��CV��=�	&���*a����T��������W;�u�M��B��-����b��5���߮�`���b�p�]��Ӵy4t���tC�����lh�b���ٔއA���/(qW�\���;���Ȋ���G�'�+? ��U�@���낢�s�B����d *
�Ȱ�$�����=�����U!zӊUO�{���'�!`5͔��:���K�U�=�����Œ�@mRa��l�{IB%�b��5}�Z��ƀ��T7	셲��j��D�e�!��0��14]�F���h��4^/
����@�1|��E�f /n�l��&t��B��nȫؠ� 
ʹ�wS�L��O�DHZ�pU]�g���@�*�y�;i�-�JӅR�޽��HCt5�jN��ާ��r�	��L�s���_S>�ܵG]�������ϗ-
򩸹�h_�Y��=^�4Y	����kN��ջ��N�c��>BG��A"I՚L�@<4A��G����<�!s�"=;�a[@�ȼ��U�w0b?��X	=��0�H�j��r~z�V�(��U��&�v$��>�R�@P��骁�lO$���H!�`;p�dvY�$��4m����^�U��_��B����`ڑyP���0l'�z&m0�Oౡ��pxh8�4�$��J�l_ҿ����A�w��`\q����cM�
!OJ�e�8�|z=��=[K<.�i��Ǹ���"0k�ޘ�W�=������r)��0O�1%�^���q�&�l���
;���GX�)����[�ʝυ�c	��CU�rH�=Ž��1�e�q�3<�qm
����LZ��P�(DV�2�K~�,�y)���xQ@�e��z��RB���c�"~���߲D��ɔ��"2'���Z��o���|�8�8ۀW�a�5p��T�a�,i�1�Y�"@������)�?�ӆ��߻�!�b/�$y
rH��C*s��Y�+r����(󰍋x�!�i��{@�:>P�RN���������%���F���./T��D�UC��|��'ȔՇx� �;���w1w�s:�?r�^f��O��E�1�~6	���Y\��U��������h&(� 8��0h'yW����'�w(����:�*yMj�m�v/	����{��p�n�x�/�(�?_��i�Rz5^��z�,f�\��
�,Osx�YM`�/���*�6;x���g2Ë%s�W2i���al!ަ�)�9ϣ��ՕAE�~�5�]�f�"�G"�z
���^`[�[UE0C���!f �,h���5��c5C�m:�X@`�I��b�Sd���s�h���ye�Q�w���B~���+`$�;�\�l�<�A�����r�z$" ��D�/~?U�����e�����!���$�(�<*�@������px-<.�3� @�ac�.'o�aV�Q�t�ŷ����_޲�p�~X�\Q[��t�/X꧓[_(���f,��R�C|
l�e�W���x�1�L)չϰ8�K�*���w�D�/����n��-
}ߑ���B|W}|�·�z&�$u7@�}L�&����i��p=��
��g��jw)^LZ���/
@�H�V��=��5eO��~�7�)�����T�,�9�TA�'n�T=����~�E<�6���:�L����W8�~GIO��,Y�	�H�n�J�#\�x�A�X������N�S癫�E�ҥ���X�'9��!f�g7�\���b�˅|�K��6T��>c�#��%��V]���I%uB�5�9��ů9���\�
@��L��V���I��T�5lΜo�av�Y~�/�z�q�,�b�/쇗�/�&��o�MޮP��H����:`�oaLc��$1>���a_�~�EvQ�5��y��K
��wہ]�V�\MbL}��n�{�7_�$_f�@M�?g|vhЇO��iT=�wx9���6��M�R���D�?�P�d�3�����ddco�/����\��=��Zy����*N�֫�Ig/7��؜�(���8��6���~���bC2XS��ɇ��v>�G�[����UBu���S����f�������%&�?g��.n^�[$CA�9D�`}�O���&R5j�Q|O� c�]z@hd�}����
�vn�A�sw����$�S&�q�N��w��8BHH�.al�/�.uS�?�-¯[
��Q�ߍ�0���~�-��ӗ�쏌���}��dK���@�3>����Lb�,�㺪��k,Ox�AJL��
Q�
��4^/
����y��U�4�T���t�U����m`:���!��E�0ݮu\k��_A>�>��^�Ż���
1��y(�u.pn��f>Nȭ�J�ǹ���"o͐�g(�m!��dz��5p����휝�=���Nن~Y�/1��YP,�DO>��٩�E� {�Q\)|uz}��(�BO�U����#/������}W�H6����gvs!��Pf^��3dB����3u�B�J|��y���e0	,p%��dAN�ؙ���"���o��ԧ�E~��)�_�F�xq��/�O^�_��V=g�Ow���	V�.�+��"�����8<g''�d颹)c���RDz�I�Z�X��E�|�7�����F�(S�@;fqYw\�Sc^s��g�qƕ-v��E��}t�a;��K3�!yo`�1���0wUM�cʢ
���ò�Z���]��!��N�p�⿖����tz[8]�L.I��H⯀H�1E�����U���?~Ιl�6�ngs���9;��u�W0�������QK���|#�h'�_�i�D�u�����5F4x�V����F��t?��@T��C��7Cl����n6�p6��U��vE�9�V���9�rf��	1�k	���`[6����"���Av�E��'xx�h�Q������Js�9����9��8�_����Sq/�\��A��Lv����I�l��_D���N���3t�&�����c,���{��o����u�'p����2M�1>J��Z˿�5���F�-�?>o��ʅؖ�T^�r ZJ�����j�hF�n���m9��zޭν:���y����0c?��]��y�Um-��~�C/d�|l�7�?��
��e_>Dѡ���W���_ִ�%��X�Z�5��,�.�/
G�t�4^���������_��Agܣ7�����ˁ�sE!ܧ���W�*��Agޣ>���p9r��Nٟ�нQ��$�=f��?qb_fƅ�����d�����	&(�_�����5��q^��ϒ���`���_�Ƴ���CN��E�Ds���	@��X��d�������6����\�h�l^�͍��B,1��9�M�2�^�G
���-�W�C��f[X�.��I���!�n?����.3�_P�pǹ��@��}d�@'�������3d>�U�?R��I:}�]�k<`���\LXf�S����X� Y
� E���㶎��ho~sc�v=��C`�x+��!bc�;y�6�Si�qRN��, ����:���W�qUw;�9~�8�c�S�_�\�w'�^x��q'c�����:QFf�����߷�~����,%}ϠW'�;��[=���Z���6^�$�!E$'�-vs+�s�U�(�t�g�M���Á�,��Q�h<1c��X�Ixm`������pxh8�4^��X��o26��nQ��H�A�I�4�;F[��Ւa�A����*jd��Ҧ�a=�4tX/��,~���RJ*�o-O�]�U���ŪiS �+8³�*�d�ބC)^H~M?����f{�lC
��]�GG ���%П֡��Ψ��YT�O��	���Bٯ��yXR�	�>��F/X�����כ�I�,*��YS�/
�����(������`}��D,(��K�Xg�p��4���bZ�D�'OW��h�+RCC�MyЍ�Y���s'!����nJ�C��C#\r=��h�Lݔ���ï6^/
����@Ñ��[���9�3\�;4[C#=��G=6!h�Mݏ^NW��9��:,���s��o0�C�d!�8;�2�&�s\K�t��Žl�:�-�*��%���A$-�����G9�4?�B��8�=��:�5�F�T���cw�[8͖�?�kx�k��j��/~}�c���n"]�����o��.0x�+��>`/]ޭ5�.`+g(�"F�e�r�����s�"����b��ۍ+"xx�p���=����N�K���L�B���g�[y� E���?���Ga�*�J$s�u������̟2$J�LqW3�����[���7�7�C�D��0~ķx%�>1 �X�)Ϲ
G�`j�!�g�����F����o�p��r������pxh82��P����R�������
�^�[����~F:�.��f���~�o���8����\�#�u]��y��
�K�	����|���@4d���mS�Q\���WӣeW��C|[����V~[���OI�t�Mt1�R�D�U���͕l��w؃zwp��]���W��v���`��
�鑫���K�~����-�ְ|1�t4�0w��q>�Psߜ���Id�hE�%u]�\�1{�Θ���M`OpV�$� �/���#����=���65ك��|���S��~X�B�!�~��_1�j]�)}S���/��G" ��C@4	���F_&���D�xv��{\����(�r���ݯH�j�$�+�o�د~jIDpT�@���+yz;���6~h�oK?�B?���o˿)~�yUY: u��R�D����"GYN��K��.F�޶a�~�o�����s8�K�GY�/�pu�����}��l���4�2�AH�ᵁ
����@������pxh8De��t�a�{T��
O��)Cץ{�C@=���P���S��q�HO�3�`c�Ηw�����C�������鑇=B��_�nP�Q�!`�B*rls
�گM�<JBd����[@P漀a�{T�_j8�4^/
����@ñy���zY꯳��2�|�Y�@xT�,{�Ϡak��W��Q�,+$��	[���!\{��v�Sa�O���z1L���=�l��*+ʶ��O9��oOؿ�H_�s�^AS�'eM�)'�;�#]�B���e��A��@d��{���=@}؏p*��&�G)�[@�j�)B�~?����mVie�Ɲ~�ۣ&���c��<z��6���H�~�%tEXtdate:create2016-09-14T13:32:39-04:00z��%tEXtdate:modify2015-06-01T10:37:21-04:00(An�tEXtSoftwareAdobe ImageReadyq�e<IEND�B`�js/jquery-ui/themes/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png000064400000000611151330370000021323 0ustar00�PNG


IHDR���A�gAMA���a cHRMz&�����u0�`:�p��Q<bKGD������	X��tIME�	
"��!�IDATH���!
A���bl����A1{�V�Y0i�x��x�v����D�K_��O��9�a�Ք��}��^�JaȌ�0b�vBA�$,�Q���"_44���=�Sqc�yE��I�W
<kA���i�0��<a$S��y.%tEXtdate:create2016-09-14T13:34:08-04:00_Dn�%tEXtdate:modify2016-09-14T13:34:08-04:00.�IEND�B`�js/jquery-ui/themes/smoothness/images/ui-bg_glass_65_ffffff_1x400.png000064400000000411151330370000021405 0ustar00�PNG


IHDR�G#7vgAMA���a cHRMz&�����u0�`:�p��Q<bKGD݊�tIME�	
"��!IDAT(�ch`��p��h��4�i%tEXtdate:create2016-09-14T13:34:08-04:00_Dn�%tEXtdate:modify2016-09-14T13:34:08-04:00.�IEND�B`�js/jquery-ui/themes/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png000064400000000505151330370000023174 0ustar00�PNG


IHDRd2��gAMA���a cHRMz&�����u0�`:�p��Q<bKGD���1�tIME�	
"��!MIDATӽΡ
�@џ���n�jn5�$@���(��'G��
i���D�7\��ƍ;N���p��
-ʜu���&����%tEXtdate:create2016-09-14T13:34:08-04:00_Dn�%tEXtdate:modify2016-09-14T13:34:08-04:00.�IEND�B`�js/jquery-ui/themes/smoothness/images/ui-icons_cd0a0a_256x240.png000064400000011104151330370000020411 0ustar00�PNG


IHDR��IJ�gAMA���a cHRMz&�����u0�`:�p��Q<,PLTE�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�

�J�ctRNSXG|"2���wf�����ZNz�@@ef�S�F��ccaM�hmz�s��}�����䁎]����������b�p�
Ιi�8*�y�㧁Xȓ�د�͐����ǫ���bKGD�HtIME�
%��'E�IDATx��]c�޻�$,�'$h	��)$uZ7I_�B"H1i���R����kgg���d�l����>�ٽ��=Y���� �d�]ز��s:w/��	f@e�,!�A�$���HW�	��O�|��FM�Z ' ��1��d���9� ��v<�AAr�k�eN���
f����s��an�l�줰.�`�����vb���V��L�tm�)�u\vG(""""""��8sOp�~P¢5��{BK��F���|�lAH־QJ,`�4�.&�J�'�T�o2b���ؓlYQI<�E`	
6��-X�lֈ�VXH��|#j�`E��q��>B�:�qџ�C7�?���xt��v*��ݹ�8�P�6�_��d�Y���J�7��%�߾�A,�U�4��IGb�(��W�
��pB� �k.��b*X�&kVfvZ��?k�	y��Ya�R�o��ڨ�3D�O�HnoH��6ݩ�~
�q��c2""""""Ⲣ��ԎnFb9������ͷ�J
��y��j�Jxsj�Nev'`���dU�۷ķT_�`�����'yo-��/����jf>��%������p}��b�%g�l�:`O�2Lj��-6�.�`=��3��`�����f7ֽu�C�l�!�Q�8s0���x��	��v��I���i)��C۷�#��W	��3�{=`���DDDDDD�g��
yU�Rb��E�:h���������Z���~9јf��vv�_�촸�;�{������%0��}z	{�]	p��]$y>���a�W������,�����:�t�X�a�uO<��m��@R���HAb�!w�(xǰ�6� �=�b%����wzRS�	,:3�﷤�6�1!��!!»�\�a�)yc$������G]���������?�,n����7�s���V�u�����n ��R�(D�6C��e���j
)
��.��t~a�g�e��^��z���@I��R�@��l�s�R"S�Oa��е�b}+X�o��(	rX=�P�����l 9�+4��:X���u�	�pL�(jW,�)l�2[0��hw��3Ї��tE�#"""".2�'d��2�����
e�FC_�7�씮�KŌ&g~>��1�X\!��R��&��H�Z%���!r���
B
�h�O?���pj����lgk�J�{=�`����	z��*�&�� -��!dd�e���A��z_�q����~wY��p�7�|0���������TG$!	&��I�|��|�����@4z</��5o��_\�v��w��%@�.&�4)���6��5xM�i�Ju��u�����U?Zb+���!p�:3!o���Ȋ��I���w�$䂼�$�	
a���=��d@�	(�5� "�*}�f}᠜�:�'���!�����
‡P�����M�Y@�?�oz����Յd�Ƿ`��BCH^�U�P�0,`����T�Qbr��j�~��1�;cL���S`\�?vZ' 4pz9�M憀�l�����
�����X����*2���p`��?�����\�Y6-���c��K8;�Z���]��=�����������w���;�X�M�=��IS�tMvW���5�O�}��hkN.��6�?�4#�E���ّ]��4����L_�S��5���i2=��rW���h�+g.m���v���]��D�-���7o�_z:�9f~#��p�2y�3��������8����X
cC����m$��O��B��Wrmp��dg�i��+Mt�	(W�n�p�&Z6ʤ}g�?^~�_�
*ן�8�����O�V�|jo�v-�y`{S�A)�T�z��"�������-م-������tH,�X�������F�!G@9I5�-i��R���`�k(��&���>�_v�+O�B��T�������Ą1��-"��b�J|� ���R�C�~�9��`(F�H��D��D�I�~rmt�o�*y��nw5yj�=��@%c��]-+�o�;�yA���АE�;U��S@1�!��;��/�?�S�������&_�o��?��g�N�$j.��E~��eo"�>�'O���1(�C]:����7��``,�!���EDDDDlC�0�\98dy�_�ha��}�A����ߗ��|��|�����Ξҿ����
�wnܪ���
sYbHdz�	ި�4��p��`���b!Le]cm�d`I������.���)n�t#6e/YW���#]�L;zs�L��r����rZ�g�\-TB�^�7
@�7����P@>��'��FꝦb1
xHE�T�j_;S�1��j�7�T����%W����{ڢ����O�P�nS��L!w�@�Y��E@�2��&|^}|r��KM��_�%0�8
�"1�
 ��;X����<m2�Q�~��_��ld$z}�`��	5>Q���j��3S��xY`aׄA��|>7��l�c�X��p�xH<,�s�ɉx|2��8	�9��j�P��F8zK�w��/��1�*:@�^&}T�#�����	�`�cp�O�g���i趜L􂕜�T3Ə���}*�(�`��	���.�ŽV��3��+��I�f��u�?��o�c]D1kT� SKzY}`���}<~����*}�0F;l<#""":c�OG��NM1߽�v�6��/�z�~����+u�W‹򉣾k����ř�;y6+��u�hU�TTm�4���m�����m=�T��z�ϼ��!��
_G�S׃�'�rm��-R�^���ksw�5�I��B�F�f&xc��_o/�����t��{-��62̶H@^���M@N�<�q�Ӱ�6R�d���#�6�`6�m���
�	�e��RwcR�l��(_�)tx�	x�2�
pz�9`�����k�>�D
~:?���8�
�=8��S�#������V��Բ,<�a�}��jEDDDDD�#�xրqdVzC�vv���Ϟ i)ΐ�R��ȠB�Z�����mN|�%C�C�AL
Lʴ������8�@�E�`���00%�X��:Y��>W0#�Na�
q�?`%6h+�a��@��9`�O�ޱ�@�]? """"�r��EG�_?>Dg7��rt�>'W�G
?��}�q���E���?h+�K��$@��Ɋ�c��Y��뷓�����1�m��BF��Z��| �5Ƿan�gW2��@}
z��fչ�#}��SP�����ן�	$��P��K��J�W���PX��1��j^X���J4&�R�F�3p�μ�`t����7	�����_.G��.���Ky^�˷�#"""":����!��r���!�B2=F�Vn�^W9x����Udz���ܶ��r�W��Z>U��i%�Gi���T��W:���}�tu������w�t��u%��
>=Żá�<���[�t�{f�	F�O?�$�q���wz����øC,�`
������|R�>��3�,�~�Q(ؾ�B|p"J�W& <ԧo�1�����!%H~*KN��A�,�Z=	�I�NR@e��j�$i�t�5�k~\��Ħc]�m�06��tu���alڕ��J��#""""""<��3�{���$�_XA�'@�?z�o�I��7�L��]�-�����)�n$�������Aׄ�}�>#���@���I�uX�\�oZ@q���r�콙���v�j���;^�~l����ϯ�]-����)����h�Ϗ
-�
�������T�&�!�.%tEXtdate:create2016-09-14T13:32:39-04:00z��%tEXtdate:modify2015-06-01T10:37:21-04:00(An�tEXtSoftwareAdobe ImageReadyq�e<IEND�B`�js/jquery-ui/themes/smoothness/images/ui-icons_222222_256x240.png000064400000015561151330370000020127 0ustar00�PNG


IHDR�E�r@gAMA���a cHRMz&�����u0�`:�p��Q<bKGD"�b�tIME�
%��'ETIDATx��m�%Uy�
$!,h ,�%�&���@�|I��EM�r�ݱ�D���2;�f��܉A^$5�Ƣj^�K�@U���-wW���D`�|�q��"�t>������ν3}�S���~�9}�<�>��<�yNp	M�)����`�	�px�h�t%��y�Y��D�^`�O��r�%`:
$%�{��0L�L��?��a|��6��܉�]A�����7P�'@/:�]5JH�&�ay���2���:�8t�y����@����#*'��?��t緝;�Ywגm׸������{�>|Dg7��\J�{�4�K�����3���tf�Z�ޱ�`�7�u7��Yɶ~f]�<��4m�*�<%%�{�^`����FR��
��	l8<O������h8<O������k�2�n6cL��`>�]=u������$��,`�&�+h��k�6��J_*|�K�0��?@�lx��do��m�]�L�3�ݞߌ0.;���T�E,;��4�����t���MO���mP�L�U�A&����e��+��`�8u�]�5�77~"���h8<O������h8<��]��C8�Y��s���Z��}i�A� �֠�)ȞsW.!m�.��
���z[�#��R�䨚��
\R��{���v�����
�r	����R�� ?˝#��[�|�f���`��������y��)�A-��Q�N
�4w���Ϯ:Kr{���河�3��ZP�!0t�{l�\�Y�wSn�P�IA.�Vm�-�R��
$y����sI����@;��eغX�.�v����Yv���B
��n]g������Wc�Vn�*�ôyT�=M�hpt
��+�3�
�'@��	�px4�
�'@��	�p��C�`���=x?��.�j�lk�]�R
��Q.�ޫ5@�P�,[��Z�j6�ƵK��ɟ�ڕ[ ,�P/5/�t�h��zG]Yn�!�wk�9څ��R��+��@���Ktťa.��mK�uD�<i�5Еc�n��[@z��@VmP���g�Q�O���c��:�Ӕ%�{%��܄.緯�ןߖ�F{�h'��@�{����"ȕ�.=>M篂��U�!Lg�K�\A�t�$���Vr�����X˒��`L�o���Me�/��R����+X.�X.]�L�]N��q�%e�7w��"�<
���ּ��p�뻴�ʩ����@	 ��uǤ
O���O7�
�'@��	�px4��� ��P���d�r��vM{w�Q�v�e�{Ȯ]�0�d1w�%L,v���Qwq�yyx`]�jW�.���!��W��W�l�/������D3�,v�܁6]����iZ<:IݯtM�~�9��U��P�i!�s�nS�!�����Q�yU緯lJ�Ț����!$(�7��-��/ޮB¤F[�^�)F�#���*U��$`7���\��!�OU���Ū3��ǐ�_��J�:𨃏ܽ�X6��X���!��|�.��!}t�$ePR�OQա�/P[Cm�b,�=�����X�A��!�th��o14]�Z���h��=O������h8<��#@k��eE�ٲM&��I�KѢ�&�b���04(.�%�t�h��tIH�2���>���D�g�ͳ��]ݢ��@�N����E]�h���ɟ|�T=��m5���@�.#���B���W��2w�Q׿¨a�lrv��E��b)n�#��ය݄��F���/N���6�%�t���]���KX���"�nz��"�t.���'��`[��A-<�d�#�]/\����#
��sDbVG��ӫ��
@�����u>y�-�L��T�i 	rgW=�����)�l�j&�˺;Y��e��F��,��6��C�G_0l;���y�1|��5�'@��	�px4�S�L�T_ʿ���ߐ�:�AD����V��\�cK�
!��e�8�L�}F�{�=.�y��0�������~w,ݭ9�^n�	���gH���<�s�ٌ�g�d���}ܠ):�$:�N��"��<�S!,��qg
駹��\���v��'�g��{;W3�p�I��m���bn��i�;J�����)��s<.$_�.�[5�{�I�V\ⷸ"�[$@d	Ke�%b!���Ѯ���F���n�0#l�§��8o���($���%�f)@6���[L�9�(?�K��o�?wjSl2�^�9�E�L���
�VH)0ͭ�!k@}Cn��U�:l�|�4���
2s���P�R�Q,Q�g�d�p6صp�Ki�*���B�?O�_1�?Ʒ����d��'%d�;��§\��k:�?r7p#��'�r�eL3��[[$꿟3D
�N��_��8�״)��<��z����'�i�>�]Y�,b���}H!��0L�a�4-�U��ڣ$�R|��c��r�Q�x�/a��_��s�e�R�mD��z�$V��\��
��3<ɔ�0㗹1�VD��<�n&�S	��L�C�4�*��5�O���`K�A����`�5�D$�n��}�5����Bf�"�GP��M	rU/���j"�$���!f+��{/��fjt�k�P�t쇘�S��ӹ�r�#��q>#��a��=���5���!*�,6�~ ���^#J�t
Wg{ȃ�s�d,�o9�X1�	��?������*u�u�B��,=�`�޻�'���1l$[�Gx4�
�'@��	���DΞ��-���z���)�E}:g˖���	���gÁ����yk��>��fi�Na~������ɏ�k`�.���7���3P���ȸҜ�;����+
�Zp�Ml�b�in��F�ݬ��L�����پȓ4� ��:�a�.0J���N�.p�k8�NP��x���\lh݅x2i�ǹ?O�H��5M�@�.K4p�\a���/�>a������y�%�^��z�U��������2�5�O'�0��V�Os�M��R�4�"0V���ܞ�����Fq�y�Y����E��L��0])PK~I��rx/��Q�s�*SL1�fn��>��������6U4�>i�#�����\��}cJ��j�rjo�_u8�n�,���JF�g����yFk6�a[0�y�|���k�S�\6����+�����)L*g�	E�D�خq|Y_���'-?Z";�<�W�ϼ=��
��[��\I�L}���‹�7Oq��X�������
���4<��z�7�=~����CP�6w��'ƟL~*?7ȳg����%�n�pM��B��C{��
�L�^�O������2;C!�<��n��$7��B5F&XQ�?̧��>��-��c%D�P]y���_�VaX1�uҗ���E�c��_��t'�[���PP>�`���������4B�� "�O�7��Ց�F���3
�����<�5��ϝ|B�.0�#���b�b���]�,ҥK{�/QW�����ܯ���d������0���~�.�����ꏜ���}��eK�
��Y@�3>?�_��"��X,�u5OԘ��(��x�sG4DI�
��4�
�'@��	�p4���X�N�IM�s(�,�ѡ�ކ<&���R=>@]���ZG�^
_���H�k����|��p7�rF��p��,�/�^>G�-�B���+�7��(z3�d�-��A���^'����qzj����
��z_`�I:L0��T��|��a��Ʉ�܇<S���I�U̅囌3I'.G���Ó��Ґl�{U���R��̌"�g�h.Gj�����,�N�Ui��0`��rƀ1�G�lK�TQ��zy��"?���or�>���'���/CT��S̤�
U��
�(�.����2����Rr ?<
aG�B�Ks�RƔ[�I���lc��yl�U��_�ɷx����O�	�QH1N�c��Ӻ#�?5f4ߣ��3lg��GA��<�aW�9�w@�I6����8��F�߮��U3���hC'��aQ�?m��*�w���\�����9$�;`[���6q��5e�(�|�E��"�8��ODHF+���~��S��m<���*��M�ʖ
._S�9<�ΐ�ҫ�Q�w �ʼn�W)p�?����
gW,
�
'�!pF�Q�8�_/�_z������{!��X�Z7&9�W�&�E��f���!��GU)�ŸY�:�l���{����`s6�������Ãl��d��,Gyx��IC�
_�k���Jw�iK��s8��8�����Mq,y��
��S��V�{�ycP��I�#ి��űo���os�:�*�i�@Z���8]�^��e�xt7�)�%��-T,)�O�\N�K��Q���|�0w�f���Kl.X�����ګ�=_���;fl⧅��;ϱ�m�� ]/�O�B����.y�S���W�/�)���)�xsp;�+��<��.p�ƴjq��I�CE��[���kO���y��
�'@�Q$��6^�=|;���6�������o\
|�ˤt_���⽃��G}�{����K����T���{���q�8b�
�� vK���"�kK���/n8�'@e��6e^���j��Ԫ�p؀�����l)l�㩻���7h��kdC�]�H�4�nnF�[:b�	l�[�ꑬ�'n�(���|[�Ƹ����4%�t�]�BF�h�%Z`%>�nj��Ԅ&����"\r'V�Z�v�D)y�kC���bW�;�I[‰�k�!�����fui�Q����*t�W�s�N_�~W<�
��g�)�'8}ږjHz��D6���6Dkx��3���?��x����Fy
�z��no���O+�v,��bcx���sLw���xt�n�G��ĝl�Nn2�ھ�I�O�ΐl��Ɵw�
�gkг��=�ޜl��Al)l��[�k��;�)"W2:��-�.H�٥��a�q�6���6$\0��o����qnj����cC�[O������h8<O�":F�@�j1Xg� d�b�ݖ��3<c�w��
�'ú��la��jd�iiK�[:�J�w@!? ����~iy��
�0\���]������#cY9%��&
�B�s�Q�@�6[�sJ8�NI���W�߭C'�QۥdFPMP�8a#$��Q(��+K	:��e�B�zi�X|�N�~<Ɯ��G�c̥��5P�\|t+ss(A[����P}��
���|-V���ܘ `�UZLh��v��$0���sƼyihH�� ��?K��Q`�a��#d�*���A��S#\j=��i�,]������6�
�'@��	�px4E���zl@dh���!M����U��c]"!@�n~�R�J%O2�!�E�a��h�=P�~`��$AG��S����r5�2r���b&ATZ�-J��C��-����W8�?�<��8ʽ��*[4�F�T�jG�p�8�&)�v�U<�U���j�i,~�|���?�=�lW��j�˻�RR�$�3�}�n�|TK5..�NQ�E���\�)�k������)ʿxS,��qE����������i�!�{q}�g�.��r
�g�U����Sq����s=W2^��)�.��[�AB�h��W�.��y�wȼ�=�}��0���6�O�8A���6�ϫ�
����O�
�'@��	�px4l�ԕ��T�
ž�����-���-��@]�-�\���A�+����õ��2���{]���$g�������-��h�"��M�M�-��|9�ZvY�?0䷕_,��m�o��9�	aa�.�I�����K�*V�J6[���B�:�n�.����a��P/0K�s����Զ_r�S�FOVh�	 �(��ɸ�����5���@Y����	P�}��x5y��o����E��Z���=����UJȯ|��?t�k3�=h���vn'SO��!`�Շ��]��~r	պpS�n=���ҝ/���A=�p�/c�����Nء�葜�&�(G)�JG�$6y�I�/�,�MP���%�P����(��i���CC~[�����v������W��B篾ꒈ�V���$˹�}��工�(�k&�
Z^�8�S�8>z�E�LHWW�����+�wx0�������-���pxk`��	�px4�
�'@��	�p�A�ܣDk�H��2u]���A��zwnP�E�x��Fy�A�u���m�	0F�0b�G:�1;L�6|�C�GI�C�H�R����ݿ6y��~˘�{�o}@���]�Q~"���h8<O������h8�/�~B�	P�-d����׻͒£"D�
��;R�򠫹 `�d]� a����{� �-�+�P�S#R������3@O z-�̒��
��¿r�vJ���Q��z��`W���_9�HG�x��\���E��A����2͌r��͠�&�~U�����>�w�S[�Σ��- `9�'C�~���C@�e�Cٹ�q�_��	��X�����z���N�%tEXtdate:create2016-09-14T13:32:39-04:00z��%tEXtdate:modify2015-06-01T10:37:21-04:00(An�tEXtSoftwareAdobe ImageReadyq�e<IEND�B`�js/jquery-ui/themes/smoothness/theme.css000064400000042732151330370000014415 0ustar00/*!
 * jQuery UI CSS Framework 1.12.1
 * http://jqueryui.com
 *
 * Copyright jQuery Foundation and other contributors
 * Released under the MIT license.
 * http://jquery.org/license
 *
 * http://api.jqueryui.com/category/theming/
 *
 * To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana%2CArial%2Csans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=highlight_soft&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=flat&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=glass&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=glass&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=glass&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=glass&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px
 */


/* Component containers
----------------------------------*/
.ui-widget {
	font-family: Verdana,Arial,sans-serif;
	font-size: 1.1em;
}
.ui-widget .ui-widget {
	font-size: 1em;
}
.ui-widget input,
.ui-widget select,
.ui-widget textarea,
.ui-widget button {
	font-family: Verdana,Arial,sans-serif;
	font-size: 1em;
}
.ui-widget.ui-widget-content {
	border: 1px solid #d3d3d3;
}
.ui-widget-content {
	border: 1px solid #aaaaaa;
	background: #ffffff;
	color: #222222;
}
.ui-widget-content a {
	color: #222222;
}
.ui-widget-header {
	border: 1px solid #aaaaaa;
	background: #cccccc url("images/ui-bg_highlight-soft_75_cccccc_1x100.png") 50% 50% repeat-x;
	color: #222222;
	font-weight: bold;
}
.ui-widget-header a {
	color: #222222;
}

/* Interaction states
----------------------------------*/
.ui-state-default,
.ui-widget-content .ui-state-default,
.ui-widget-header .ui-state-default,
.ui-button,

/* We use html here because we need a greater specificity to make sure disabled
works properly when clicked or hovered */
html .ui-button.ui-state-disabled:hover,
html .ui-button.ui-state-disabled:active {
	border: 1px solid #d3d3d3;
	background: #e6e6e6 url("images/ui-bg_glass_75_e6e6e6_1x400.png") 50% 50% repeat-x;
	font-weight: normal;
	color: #555555;
}
.ui-state-default a,
.ui-state-default a:link,
.ui-state-default a:visited,
a.ui-button,
a:link.ui-button,
a:visited.ui-button,
.ui-button {
	color: #555555;
	text-decoration: none;
}
.ui-state-hover,
.ui-widget-content .ui-state-hover,
.ui-widget-header .ui-state-hover,
.ui-state-focus,
.ui-widget-content .ui-state-focus,
.ui-widget-header .ui-state-focus,
.ui-button:hover,
.ui-button:focus {
	border: 1px solid #999999;
	background: #dadada url("images/ui-bg_glass_75_dadada_1x400.png") 50% 50% repeat-x;
	font-weight: normal;
	color: #212121;
}
.ui-state-hover a,
.ui-state-hover a:hover,
.ui-state-hover a:link,
.ui-state-hover a:visited,
.ui-state-focus a,
.ui-state-focus a:hover,
.ui-state-focus a:link,
.ui-state-focus a:visited,
a.ui-button:hover,
a.ui-button:focus {
	color: #212121;
	text-decoration: none;
}

.ui-visual-focus {
	box-shadow: 0 0 3px 1px rgb(94, 158, 214);
}
.ui-state-active,
.ui-widget-content .ui-state-active,
.ui-widget-header .ui-state-active,
a.ui-button:active,
.ui-button:active,
.ui-button.ui-state-active:hover {
	border: 1px solid #aaaaaa;
	background: #ffffff url("images/ui-bg_glass_65_ffffff_1x400.png") 50% 50% repeat-x;
	font-weight: normal;
	color: #212121;
}
.ui-icon-background,
.ui-state-active .ui-icon-background {
	border: #aaaaaa;
	background-color: #212121;
}
.ui-state-active a,
.ui-state-active a:link,
.ui-state-active a:visited {
	color: #212121;
	text-decoration: none;
}

/* Interaction Cues
----------------------------------*/
.ui-state-highlight,
.ui-widget-content .ui-state-highlight,
.ui-widget-header .ui-state-highlight {
	border: 1px solid #fcefa1;
	background: #fbf9ee url("images/ui-bg_glass_55_fbf9ee_1x400.png") 50% 50% repeat-x;
	color: #363636;
}
.ui-state-checked {
	border: 1px solid #fcefa1;
	background: #fbf9ee;
}
.ui-state-highlight a,
.ui-widget-content .ui-state-highlight a,
.ui-widget-header .ui-state-highlight a {
	color: #363636;
}
.ui-state-error,
.ui-widget-content .ui-state-error,
.ui-widget-header .ui-state-error {
	border: 1px solid #cd0a0a;
	background: #fef1ec url("images/ui-bg_glass_95_fef1ec_1x400.png") 50% 50% repeat-x;
	color: #cd0a0a;
}
.ui-state-error a,
.ui-widget-content .ui-state-error a,
.ui-widget-header .ui-state-error a {
	color: #cd0a0a;
}
.ui-state-error-text,
.ui-widget-content .ui-state-error-text,
.ui-widget-header .ui-state-error-text {
	color: #cd0a0a;
}
.ui-priority-primary,
.ui-widget-content .ui-priority-primary,
.ui-widget-header .ui-priority-primary {
	font-weight: bold;
}
.ui-priority-secondary,
.ui-widget-content .ui-priority-secondary,
.ui-widget-header .ui-priority-secondary {
	opacity: .7;
	filter:Alpha(Opacity=70); /* support: IE8 */
	font-weight: normal;
}
.ui-state-disabled,
.ui-widget-content .ui-state-disabled,
.ui-widget-header .ui-state-disabled {
	opacity: .35;
	filter:Alpha(Opacity=35); /* support: IE8 */
	background-image: none;
}
.ui-state-disabled .ui-icon {
	filter:Alpha(Opacity=35); /* support: IE8 - See #6059 */
}

/* Icons
----------------------------------*/

/* states and images */
.ui-icon {
	width: 16px;
	height: 16px;
}
.ui-icon,
.ui-widget-content .ui-icon {
	background-image: url("images/ui-icons_222222_256x240.png");
}
.ui-widget-header .ui-icon {
	background-image: url("images/ui-icons_222222_256x240.png");
}
.ui-state-hover .ui-icon,
.ui-state-focus .ui-icon,
.ui-button:hover .ui-icon,
.ui-button:focus .ui-icon {
	background-image: url("images/ui-icons_454545_256x240.png");
}
.ui-state-active .ui-icon,
.ui-button:active .ui-icon {
	background-image: url("images/ui-icons_454545_256x240.png");
}
.ui-state-highlight .ui-icon,
.ui-button .ui-state-highlight.ui-icon {
	background-image: url("images/ui-icons_2e83ff_256x240.png");
}
.ui-state-error .ui-icon,
.ui-state-error-text .ui-icon {
	background-image: url("images/ui-icons_cd0a0a_256x240.png");
}
.ui-button .ui-icon {
	background-image: url("images/ui-icons_888888_256x240.png");
}

/* positioning */
.ui-icon-blank { background-position: 16px 16px; }
.ui-icon-caret-1-n { background-position: 0 0; }
.ui-icon-caret-1-ne { background-position: -16px 0; }
.ui-icon-caret-1-e { background-position: -32px 0; }
.ui-icon-caret-1-se { background-position: -48px 0; }
.ui-icon-caret-1-s { background-position: -65px 0; }
.ui-icon-caret-1-sw { background-position: -80px 0; }
.ui-icon-caret-1-w { background-position: -96px 0; }
.ui-icon-caret-1-nw { background-position: -112px 0; }
.ui-icon-caret-2-n-s { background-position: -128px 0; }
.ui-icon-caret-2-e-w { background-position: -144px 0; }
.ui-icon-triangle-1-n { background-position: 0 -16px; }
.ui-icon-triangle-1-ne { background-position: -16px -16px; }
.ui-icon-triangle-1-e { background-position: -32px -16px; }
.ui-icon-triangle-1-se { background-position: -48px -16px; }
.ui-icon-triangle-1-s { background-position: -65px -16px; }
.ui-icon-triangle-1-sw { background-position: -80px -16px; }
.ui-icon-triangle-1-w { background-position: -96px -16px; }
.ui-icon-triangle-1-nw { background-position: -112px -16px; }
.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
.ui-icon-arrow-1-n { background-position: 0 -32px; }
.ui-icon-arrow-1-ne { background-position: -16px -32px; }
.ui-icon-arrow-1-e { background-position: -32px -32px; }
.ui-icon-arrow-1-se { background-position: -48px -32px; }
.ui-icon-arrow-1-s { background-position: -65px -32px; }
.ui-icon-arrow-1-sw { background-position: -80px -32px; }
.ui-icon-arrow-1-w { background-position: -96px -32px; }
.ui-icon-arrow-1-nw { background-position: -112px -32px; }
.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
.ui-icon-arrowthick-1-n { background-position: 1px -48px; }
.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
.ui-icon-arrow-4 { background-position: 0 -80px; }
.ui-icon-arrow-4-diag { background-position: -16px -80px; }
.ui-icon-extlink { background-position: -32px -80px; }
.ui-icon-newwin { background-position: -48px -80px; }
.ui-icon-refresh { background-position: -64px -80px; }
.ui-icon-shuffle { background-position: -80px -80px; }
.ui-icon-transfer-e-w { background-position: -96px -80px; }
.ui-icon-transferthick-e-w { background-position: -112px -80px; }
.ui-icon-folder-collapsed { background-position: 0 -96px; }
.ui-icon-folder-open { background-position: -16px -96px; }
.ui-icon-document { background-position: -32px -96px; }
.ui-icon-document-b { background-position: -48px -96px; }
.ui-icon-note { background-position: -64px -96px; }
.ui-icon-mail-closed { background-position: -80px -96px; }
.ui-icon-mail-open { background-position: -96px -96px; }
.ui-icon-suitcase { background-position: -112px -96px; }
.ui-icon-comment { background-position: -128px -96px; }
.ui-icon-person { background-position: -144px -96px; }
.ui-icon-print { background-position: -160px -96px; }
.ui-icon-trash { background-position: -176px -96px; }
.ui-icon-locked { background-position: -192px -96px; }
.ui-icon-unlocked { background-position: -208px -96px; }
.ui-icon-bookmark { background-position: -224px -96px; }
.ui-icon-tag { background-position: -240px -96px; }
.ui-icon-home { background-position: 0 -112px; }
.ui-icon-flag { background-position: -16px -112px; }
.ui-icon-calendar { background-position: -32px -112px; }
.ui-icon-cart { background-position: -48px -112px; }
.ui-icon-pencil { background-position: -64px -112px; }
.ui-icon-clock { background-position: -80px -112px; }
.ui-icon-disk { background-position: -96px -112px; }
.ui-icon-calculator { background-position: -112px -112px; }
.ui-icon-zoomin { background-position: -128px -112px; }
.ui-icon-zoomout { background-position: -144px -112px; }
.ui-icon-search { background-position: -160px -112px; }
.ui-icon-wrench { background-position: -176px -112px; }
.ui-icon-gear { background-position: -192px -112px; }
.ui-icon-heart { background-position: -208px -112px; }
.ui-icon-star { background-position: -224px -112px; }
.ui-icon-link { background-position: -240px -112px; }
.ui-icon-cancel { background-position: 0 -128px; }
.ui-icon-plus { background-position: -16px -128px; }
.ui-icon-plusthick { background-position: -32px -128px; }
.ui-icon-minus { background-position: -48px -128px; }
.ui-icon-minusthick { background-position: -64px -128px; }
.ui-icon-close { background-position: -80px -128px; }
.ui-icon-closethick { background-position: -96px -128px; }
.ui-icon-key { background-position: -112px -128px; }
.ui-icon-lightbulb { background-position: -128px -128px; }
.ui-icon-scissors { background-position: -144px -128px; }
.ui-icon-clipboard { background-position: -160px -128px; }
.ui-icon-copy { background-position: -176px -128px; }
.ui-icon-contact { background-position: -192px -128px; }
.ui-icon-image { background-position: -208px -128px; }
.ui-icon-video { background-position: -224px -128px; }
.ui-icon-script { background-position: -240px -128px; }
.ui-icon-alert { background-position: 0 -144px; }
.ui-icon-info { background-position: -16px -144px; }
.ui-icon-notice { background-position: -32px -144px; }
.ui-icon-help { background-position: -48px -144px; }
.ui-icon-check { background-position: -64px -144px; }
.ui-icon-bullet { background-position: -80px -144px; }
.ui-icon-radio-on { background-position: -96px -144px; }
.ui-icon-radio-off { background-position: -112px -144px; }
.ui-icon-pin-w { background-position: -128px -144px; }
.ui-icon-pin-s { background-position: -144px -144px; }
.ui-icon-play { background-position: 0 -160px; }
.ui-icon-pause { background-position: -16px -160px; }
.ui-icon-seek-next { background-position: -32px -160px; }
.ui-icon-seek-prev { background-position: -48px -160px; }
.ui-icon-seek-end { background-position: -64px -160px; }
.ui-icon-seek-start { background-position: -80px -160px; }
/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */
.ui-icon-seek-first { background-position: -80px -160px; }
.ui-icon-stop { background-position: -96px -160px; }
.ui-icon-eject { background-position: -112px -160px; }
.ui-icon-volume-off { background-position: -128px -160px; }
.ui-icon-volume-on { background-position: -144px -160px; }
.ui-icon-power { background-position: 0 -176px; }
.ui-icon-signal-diag { background-position: -16px -176px; }
.ui-icon-signal { background-position: -32px -176px; }
.ui-icon-battery-0 { background-position: -48px -176px; }
.ui-icon-battery-1 { background-position: -64px -176px; }
.ui-icon-battery-2 { background-position: -80px -176px; }
.ui-icon-battery-3 { background-position: -96px -176px; }
.ui-icon-circle-plus { background-position: 0 -192px; }
.ui-icon-circle-minus { background-position: -16px -192px; }
.ui-icon-circle-close { background-position: -32px -192px; }
.ui-icon-circle-triangle-e { background-position: -48px -192px; }
.ui-icon-circle-triangle-s { background-position: -64px -192px; }
.ui-icon-circle-triangle-w { background-position: -80px -192px; }
.ui-icon-circle-triangle-n { background-position: -96px -192px; }
.ui-icon-circle-arrow-e { background-position: -112px -192px; }
.ui-icon-circle-arrow-s { background-position: -128px -192px; }
.ui-icon-circle-arrow-w { background-position: -144px -192px; }
.ui-icon-circle-arrow-n { background-position: -160px -192px; }
.ui-icon-circle-zoomin { background-position: -176px -192px; }
.ui-icon-circle-zoomout { background-position: -192px -192px; }
.ui-icon-circle-check { background-position: -208px -192px; }
.ui-icon-circlesmall-plus { background-position: 0 -208px; }
.ui-icon-circlesmall-minus { background-position: -16px -208px; }
.ui-icon-circlesmall-close { background-position: -32px -208px; }
.ui-icon-squaresmall-plus { background-position: -48px -208px; }
.ui-icon-squaresmall-minus { background-position: -64px -208px; }
.ui-icon-squaresmall-close { background-position: -80px -208px; }
.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
.ui-icon-grip-diagonal-se { background-position: -80px -224px; }


/* Misc visuals
----------------------------------*/

/* Corner radius */
.ui-corner-all,
.ui-corner-top,
.ui-corner-left,
.ui-corner-tl {
	border-top-left-radius: 4px;
}
.ui-corner-all,
.ui-corner-top,
.ui-corner-right,
.ui-corner-tr {
	border-top-right-radius: 4px;
}
.ui-corner-all,
.ui-corner-bottom,
.ui-corner-left,
.ui-corner-bl {
	border-bottom-left-radius: 4px;
}
.ui-corner-all,
.ui-corner-bottom,
.ui-corner-right,
.ui-corner-br {
	border-bottom-right-radius: 4px;
}

/* Overlays */
.ui-widget-overlay {
	background: #aaaaaa;
	opacity: .3;
	filter: Alpha(Opacity=30); /* support: IE8 */
}
.ui-widget-shadow {
	-webkit-box-shadow: -8px -8px 8px #aaaaaa;
	box-shadow: -8px -8px 8px #aaaaaa;
}
js/jquery-ui/themes/smoothness/jquery-ui.css000064400000107270151330370000015244 0ustar00/*! jQuery UI - v1.12.1 - 2016-09-14
* http://jqueryui.com
* Includes: core.css, accordion.css, autocomplete.css, menu.css, button.css, controlgroup.css, checkboxradio.css, datepicker.css, dialog.css, draggable.css, resizable.css, progressbar.css, selectable.css, selectmenu.css, slider.css, sortable.css, spinner.css, tabs.css, tooltip.css, theme.css
* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana%2CArial%2Csans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=highlight_soft&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=flat&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=glass&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=glass&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=glass&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=glass&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px
* Copyright jQuery Foundation and other contributors; Licensed MIT */

/* Layout helpers
----------------------------------*/
.ui-helper-hidden {
	display: none;
}
.ui-helper-hidden-accessible {
	border: 0;
	clip: rect(0 0 0 0);
	height: 1px;
	margin: -1px;
	overflow: hidden;
	padding: 0;
	position: absolute;
	width: 1px;
}
.ui-helper-reset {
	margin: 0;
	padding: 0;
	border: 0;
	outline: 0;
	line-height: 1.3;
	text-decoration: none;
	font-size: 100%;
	list-style: none;
}
.ui-helper-clearfix:before,
.ui-helper-clearfix:after {
	content: "";
	display: table;
	border-collapse: collapse;
}
.ui-helper-clearfix:after {
	clear: both;
}
.ui-helper-zfix {
	width: 100%;
	height: 100%;
	top: 0;
	left: 0;
	position: absolute;
	opacity: 0;
	filter:Alpha(Opacity=0); /* support: IE8 */
}

.ui-front {
	z-index: 100;
}


/* Interaction Cues
----------------------------------*/
.ui-state-disabled {
	cursor: default !important;
	pointer-events: none;
}


/* Icons
----------------------------------*/
.ui-icon {
	display: inline-block;
	vertical-align: middle;
	margin-top: -.25em;
	position: relative;
	text-indent: -99999px;
	overflow: hidden;
	background-repeat: no-repeat;
}

.ui-widget-icon-block {
	left: 50%;
	margin-left: -8px;
	display: block;
}

/* Misc visuals
----------------------------------*/

/* Overlays */
.ui-widget-overlay {
	position: fixed;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
}
.ui-accordion .ui-accordion-header {
	display: block;
	cursor: pointer;
	position: relative;
	margin: 2px 0 0 0;
	padding: .5em .5em .5em .7em;
	font-size: 100%;
}
.ui-accordion .ui-accordion-content {
	padding: 1em 2.2em;
	border-top: 0;
	overflow: auto;
}
.ui-autocomplete {
	position: absolute;
	top: 0;
	left: 0;
	cursor: default;
}
.ui-menu {
	list-style: none;
	padding: 0;
	margin: 0;
	display: block;
	outline: 0;
}
.ui-menu .ui-menu {
	position: absolute;
}
.ui-menu .ui-menu-item {
	margin: 0;
	cursor: pointer;
	/* support: IE10, see #8844 */
	list-style-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");
}
.ui-menu .ui-menu-item-wrapper {
	position: relative;
	padding: 3px 1em 3px .4em;
}
.ui-menu .ui-menu-divider {
	margin: 5px 0;
	height: 0;
	font-size: 0;
	line-height: 0;
	border-width: 1px 0 0 0;
}
.ui-menu .ui-state-focus,
.ui-menu .ui-state-active {
	margin: -1px;
}

/* icon support */
.ui-menu-icons {
	position: relative;
}
.ui-menu-icons .ui-menu-item-wrapper {
	padding-left: 2em;
}

/* left-aligned */
.ui-menu .ui-icon {
	position: absolute;
	top: 0;
	bottom: 0;
	left: .2em;
	margin: auto 0;
}

/* right-aligned */
.ui-menu .ui-menu-icon {
	left: auto;
	right: 0;
}
.ui-button {
	padding: .4em 1em;
	display: inline-block;
	position: relative;
	line-height: normal;
	margin-right: .1em;
	cursor: pointer;
	vertical-align: middle;
	text-align: center;
	-webkit-user-select: none;
	-moz-user-select: none;
	-ms-user-select: none;
	user-select: none;

	/* Support: IE <= 11 */
	overflow: visible;
}

.ui-button,
.ui-button:link,
.ui-button:visited,
.ui-button:hover,
.ui-button:active {
	text-decoration: none;
}

/* to make room for the icon, a width needs to be set here */
.ui-button-icon-only {
	width: 2em;
	box-sizing: border-box;
	text-indent: -9999px;
	white-space: nowrap;
}

/* no icon support for input elements */
input.ui-button.ui-button-icon-only {
	text-indent: 0;
}

/* button icon element(s) */
.ui-button-icon-only .ui-icon {
	position: absolute;
	top: 50%;
	left: 50%;
	margin-top: -8px;
	margin-left: -8px;
}

.ui-button.ui-icon-notext .ui-icon {
	padding: 0;
	width: 2.1em;
	height: 2.1em;
	text-indent: -9999px;
	white-space: nowrap;

}

input.ui-button.ui-icon-notext .ui-icon {
	width: auto;
	height: auto;
	text-indent: 0;
	white-space: normal;
	padding: .4em 1em;
}

/* workarounds */
/* Support: Firefox 5 - 40 */
input.ui-button::-moz-focus-inner,
button.ui-button::-moz-focus-inner {
	border: 0;
	padding: 0;
}
.ui-controlgroup {
	vertical-align: middle;
	display: inline-block;
}
.ui-controlgroup > .ui-controlgroup-item {
	float: left;
	margin-left: 0;
	margin-right: 0;
}
.ui-controlgroup > .ui-controlgroup-item:focus,
.ui-controlgroup > .ui-controlgroup-item.ui-visual-focus {
	z-index: 9999;
}
.ui-controlgroup-vertical > .ui-controlgroup-item {
	display: block;
	float: none;
	width: 100%;
	margin-top: 0;
	margin-bottom: 0;
	text-align: left;
}
.ui-controlgroup-vertical .ui-controlgroup-item {
	box-sizing: border-box;
}
.ui-controlgroup .ui-controlgroup-label {
	padding: .4em 1em;
}
.ui-controlgroup .ui-controlgroup-label span {
	font-size: 80%;
}
.ui-controlgroup-horizontal .ui-controlgroup-label + .ui-controlgroup-item {
	border-left: none;
}
.ui-controlgroup-vertical .ui-controlgroup-label + .ui-controlgroup-item {
	border-top: none;
}
.ui-controlgroup-horizontal .ui-controlgroup-label.ui-widget-content {
	border-right: none;
}
.ui-controlgroup-vertical .ui-controlgroup-label.ui-widget-content {
	border-bottom: none;
}

/* Spinner specific style fixes */
.ui-controlgroup-vertical .ui-spinner-input {

	/* Support: IE8 only, Android < 4.4 only */
	width: 75%;
	width: calc( 100% - 2.4em );
}
.ui-controlgroup-vertical .ui-spinner .ui-spinner-up {
	border-top-style: solid;
}

.ui-checkboxradio-label .ui-icon-background {
	box-shadow: inset 1px 1px 1px #ccc;
	border-radius: .12em;
	border: none;
}
.ui-checkboxradio-radio-label .ui-icon-background {
	width: 16px;
	height: 16px;
	border-radius: 1em;
	overflow: visible;
	border: none;
}
.ui-checkboxradio-radio-label.ui-checkboxradio-checked .ui-icon,
.ui-checkboxradio-radio-label.ui-checkboxradio-checked:hover .ui-icon {
	background-image: none;
	width: 8px;
	height: 8px;
	border-width: 4px;
	border-style: solid;
}
.ui-checkboxradio-disabled {
	pointer-events: none;
}
.ui-datepicker {
	width: 17em;
	padding: .2em .2em 0;
	display: none;
}
.ui-datepicker .ui-datepicker-header {
	position: relative;
	padding: .2em 0;
}
.ui-datepicker .ui-datepicker-prev,
.ui-datepicker .ui-datepicker-next {
	position: absolute;
	top: 2px;
	width: 1.8em;
	height: 1.8em;
}
.ui-datepicker .ui-datepicker-prev-hover,
.ui-datepicker .ui-datepicker-next-hover {
	top: 1px;
}
.ui-datepicker .ui-datepicker-prev {
	left: 2px;
}
.ui-datepicker .ui-datepicker-next {
	right: 2px;
}
.ui-datepicker .ui-datepicker-prev-hover {
	left: 1px;
}
.ui-datepicker .ui-datepicker-next-hover {
	right: 1px;
}
.ui-datepicker .ui-datepicker-prev span,
.ui-datepicker .ui-datepicker-next span {
	display: block;
	position: absolute;
	left: 50%;
	margin-left: -8px;
	top: 50%;
	margin-top: -8px;
}
.ui-datepicker .ui-datepicker-title {
	margin: 0 2.3em;
	line-height: 1.8em;
	text-align: center;
}
.ui-datepicker .ui-datepicker-title select {
	font-size: 1em;
	margin: 1px 0;
}
.ui-datepicker select.ui-datepicker-month,
.ui-datepicker select.ui-datepicker-year {
	width: 45%;
}
.ui-datepicker table {
	width: 100%;
	font-size: .9em;
	border-collapse: collapse;
	margin: 0 0 .4em;
}
.ui-datepicker th {
	padding: .7em .3em;
	text-align: center;
	font-weight: bold;
	border: 0;
}
.ui-datepicker td {
	border: 0;
	padding: 1px;
}
.ui-datepicker td span,
.ui-datepicker td a {
	display: block;
	padding: .2em;
	text-align: right;
	text-decoration: none;
}
.ui-datepicker .ui-datepicker-buttonpane {
	background-image: none;
	margin: .7em 0 0 0;
	padding: 0 .2em;
	border-left: 0;
	border-right: 0;
	border-bottom: 0;
}
.ui-datepicker .ui-datepicker-buttonpane button {
	float: right;
	margin: .5em .2em .4em;
	cursor: pointer;
	padding: .2em .6em .3em .6em;
	width: auto;
	overflow: visible;
}
.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current {
	float: left;
}

/* with multiple calendars */
.ui-datepicker.ui-datepicker-multi {
	width: auto;
}
.ui-datepicker-multi .ui-datepicker-group {
	float: left;
}
.ui-datepicker-multi .ui-datepicker-group table {
	width: 95%;
	margin: 0 auto .4em;
}
.ui-datepicker-multi-2 .ui-datepicker-group {
	width: 50%;
}
.ui-datepicker-multi-3 .ui-datepicker-group {
	width: 33.3%;
}
.ui-datepicker-multi-4 .ui-datepicker-group {
	width: 25%;
}
.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,
.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header {
	border-left-width: 0;
}
.ui-datepicker-multi .ui-datepicker-buttonpane {
	clear: left;
}
.ui-datepicker-row-break {
	clear: both;
	width: 100%;
	font-size: 0;
}

/* RTL support */
.ui-datepicker-rtl {
	direction: rtl;
}
.ui-datepicker-rtl .ui-datepicker-prev {
	right: 2px;
	left: auto;
}
.ui-datepicker-rtl .ui-datepicker-next {
	left: 2px;
	right: auto;
}
.ui-datepicker-rtl .ui-datepicker-prev:hover {
	right: 1px;
	left: auto;
}
.ui-datepicker-rtl .ui-datepicker-next:hover {
	left: 1px;
	right: auto;
}
.ui-datepicker-rtl .ui-datepicker-buttonpane {
	clear: right;
}
.ui-datepicker-rtl .ui-datepicker-buttonpane button {
	float: left;
}
.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,
.ui-datepicker-rtl .ui-datepicker-group {
	float: right;
}
.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,
.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header {
	border-right-width: 0;
	border-left-width: 1px;
}

/* Icons */
.ui-datepicker .ui-icon {
	display: block;
	text-indent: -99999px;
	overflow: hidden;
	background-repeat: no-repeat;
	left: .5em;
	top: .3em;
}
.ui-dialog {
	position: absolute;
	top: 0;
	left: 0;
	padding: .2em;
	outline: 0;
}
.ui-dialog .ui-dialog-titlebar {
	padding: .4em 1em;
	position: relative;
}
.ui-dialog .ui-dialog-title {
	float: left;
	margin: .1em 0;
	white-space: nowrap;
	width: 90%;
	overflow: hidden;
	text-overflow: ellipsis;
}
.ui-dialog .ui-dialog-titlebar-close {
	position: absolute;
	right: .3em;
	top: 50%;
	width: 20px;
	margin: -10px 0 0 0;
	padding: 1px;
	height: 20px;
}
.ui-dialog .ui-dialog-content {
	position: relative;
	border: 0;
	padding: .5em 1em;
	background: none;
	overflow: auto;
}
.ui-dialog .ui-dialog-buttonpane {
	text-align: left;
	border-width: 1px 0 0 0;
	background-image: none;
	margin-top: .5em;
	padding: .3em 1em .5em .4em;
}
.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset {
	float: right;
}
.ui-dialog .ui-dialog-buttonpane button {
	margin: .5em .4em .5em 0;
	cursor: pointer;
}
.ui-dialog .ui-resizable-n {
	height: 2px;
	top: 0;
}
.ui-dialog .ui-resizable-e {
	width: 2px;
	right: 0;
}
.ui-dialog .ui-resizable-s {
	height: 2px;
	bottom: 0;
}
.ui-dialog .ui-resizable-w {
	width: 2px;
	left: 0;
}
.ui-dialog .ui-resizable-se,
.ui-dialog .ui-resizable-sw,
.ui-dialog .ui-resizable-ne,
.ui-dialog .ui-resizable-nw {
	width: 7px;
	height: 7px;
}
.ui-dialog .ui-resizable-se {
	right: 0;
	bottom: 0;
}
.ui-dialog .ui-resizable-sw {
	left: 0;
	bottom: 0;
}
.ui-dialog .ui-resizable-ne {
	right: 0;
	top: 0;
}
.ui-dialog .ui-resizable-nw {
	left: 0;
	top: 0;
}
.ui-draggable .ui-dialog-titlebar {
	cursor: move;
}
.ui-draggable-handle {
	-ms-touch-action: none;
	touch-action: none;
}
.ui-resizable {
	position: relative;
}
.ui-resizable-handle {
	position: absolute;
	font-size: 0.1px;
	display: block;
	-ms-touch-action: none;
	touch-action: none;
}
.ui-resizable-disabled .ui-resizable-handle,
.ui-resizable-autohide .ui-resizable-handle {
	display: none;
}
.ui-resizable-n {
	cursor: n-resize;
	height: 7px;
	width: 100%;
	top: -5px;
	left: 0;
}
.ui-resizable-s {
	cursor: s-resize;
	height: 7px;
	width: 100%;
	bottom: -5px;
	left: 0;
}
.ui-resizable-e {
	cursor: e-resize;
	width: 7px;
	right: -5px;
	top: 0;
	height: 100%;
}
.ui-resizable-w {
	cursor: w-resize;
	width: 7px;
	left: -5px;
	top: 0;
	height: 100%;
}
.ui-resizable-se {
	cursor: se-resize;
	width: 12px;
	height: 12px;
	right: 1px;
	bottom: 1px;
}
.ui-resizable-sw {
	cursor: sw-resize;
	width: 9px;
	height: 9px;
	left: -5px;
	bottom: -5px;
}
.ui-resizable-nw {
	cursor: nw-resize;
	width: 9px;
	height: 9px;
	left: -5px;
	top: -5px;
}
.ui-resizable-ne {
	cursor: ne-resize;
	width: 9px;
	height: 9px;
	right: -5px;
	top: -5px;
}
.ui-progressbar {
	height: 2em;
	text-align: left;
	overflow: hidden;
}
.ui-progressbar .ui-progressbar-value {
	margin: -1px;
	height: 100%;
}
.ui-progressbar .ui-progressbar-overlay {
	background: url("data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw==");
	height: 100%;
	filter: alpha(opacity=25); /* support: IE8 */
	opacity: 0.25;
}
.ui-progressbar-indeterminate .ui-progressbar-value {
	background-image: none;
}
.ui-selectable {
	-ms-touch-action: none;
	touch-action: none;
}
.ui-selectable-helper {
	position: absolute;
	z-index: 100;
	border: 1px dotted black;
}
.ui-selectmenu-menu {
	padding: 0;
	margin: 0;
	position: absolute;
	top: 0;
	left: 0;
	display: none;
}
.ui-selectmenu-menu .ui-menu {
	overflow: auto;
	overflow-x: hidden;
	padding-bottom: 1px;
}
.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup {
	font-size: 1em;
	font-weight: bold;
	line-height: 1.5;
	padding: 2px 0.4em;
	margin: 0.5em 0 0 0;
	height: auto;
	border: 0;
}
.ui-selectmenu-open {
	display: block;
}
.ui-selectmenu-text {
	display: block;
	margin-right: 20px;
	overflow: hidden;
	text-overflow: ellipsis;
}
.ui-selectmenu-button.ui-button {
	text-align: left;
	white-space: nowrap;
	width: 14em;
}
.ui-selectmenu-icon.ui-icon {
	float: right;
	margin-top: 0;
}
.ui-slider {
	position: relative;
	text-align: left;
}
.ui-slider .ui-slider-handle {
	position: absolute;
	z-index: 2;
	width: 1.2em;
	height: 1.2em;
	cursor: default;
	-ms-touch-action: none;
	touch-action: none;
}
.ui-slider .ui-slider-range {
	position: absolute;
	z-index: 1;
	font-size: .7em;
	display: block;
	border: 0;
	background-position: 0 0;
}

/* support: IE8 - See #6727 */
.ui-slider.ui-state-disabled .ui-slider-handle,
.ui-slider.ui-state-disabled .ui-slider-range {
	filter: inherit;
}

.ui-slider-horizontal {
	height: .8em;
}
.ui-slider-horizontal .ui-slider-handle {
	top: -.3em;
	margin-left: -.6em;
}
.ui-slider-horizontal .ui-slider-range {
	top: 0;
	height: 100%;
}
.ui-slider-horizontal .ui-slider-range-min {
	left: 0;
}
.ui-slider-horizontal .ui-slider-range-max {
	right: 0;
}

.ui-slider-vertical {
	width: .8em;
	height: 100px;
}
.ui-slider-vertical .ui-slider-handle {
	left: -.3em;
	margin-left: 0;
	margin-bottom: -.6em;
}
.ui-slider-vertical .ui-slider-range {
	left: 0;
	width: 100%;
}
.ui-slider-vertical .ui-slider-range-min {
	bottom: 0;
}
.ui-slider-vertical .ui-slider-range-max {
	top: 0;
}
.ui-sortable-handle {
	-ms-touch-action: none;
	touch-action: none;
}
.ui-spinner {
	position: relative;
	display: inline-block;
	overflow: hidden;
	padding: 0;
	vertical-align: middle;
}
.ui-spinner-input {
	border: none;
	background: none;
	color: inherit;
	padding: .222em 0;
	margin: .2em 0;
	vertical-align: middle;
	margin-left: .4em;
	margin-right: 2em;
}
.ui-spinner-button {
	width: 1.6em;
	height: 50%;
	font-size: .5em;
	padding: 0;
	margin: 0;
	text-align: center;
	position: absolute;
	cursor: default;
	display: block;
	overflow: hidden;
	right: 0;
}
/* more specificity required here to override default borders */
.ui-spinner a.ui-spinner-button {
	border-top-style: none;
	border-bottom-style: none;
	border-right-style: none;
}
.ui-spinner-up {
	top: 0;
}
.ui-spinner-down {
	bottom: 0;
}
.ui-tabs {
	position: relative;/* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */
	padding: .2em;
}
.ui-tabs .ui-tabs-nav {
	margin: 0;
	padding: .2em .2em 0;
}
.ui-tabs .ui-tabs-nav li {
	list-style: none;
	float: left;
	position: relative;
	top: 0;
	margin: 1px .2em 0 0;
	border-bottom-width: 0;
	padding: 0;
	white-space: nowrap;
}
.ui-tabs .ui-tabs-nav .ui-tabs-anchor {
	float: left;
	padding: .5em 1em;
	text-decoration: none;
}
.ui-tabs .ui-tabs-nav li.ui-tabs-active {
	margin-bottom: -1px;
	padding-bottom: 1px;
}
.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,
.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,
.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor {
	cursor: text;
}
.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor {
	cursor: pointer;
}
.ui-tabs .ui-tabs-panel {
	display: block;
	border-width: 0;
	padding: 1em 1.4em;
	background: none;
}
.ui-tooltip {
	padding: 8px;
	position: absolute;
	z-index: 9999;
	max-width: 300px;
}
body .ui-tooltip {
	border-width: 2px;
}
/* Component containers
----------------------------------*/
.ui-widget {
	font-family: Verdana,Arial,sans-serif;
	font-size: 1.1em;
}
.ui-widget .ui-widget {
	font-size: 1em;
}
.ui-widget input,
.ui-widget select,
.ui-widget textarea,
.ui-widget button {
	font-family: Verdana,Arial,sans-serif;
	font-size: 1em;
}
.ui-widget.ui-widget-content {
	border: 1px solid #d3d3d3;
}
.ui-widget-content {
	border: 1px solid #aaaaaa;
	background: #ffffff;
	color: #222222;
}
.ui-widget-content a {
	color: #222222;
}
.ui-widget-header {
	border: 1px solid #aaaaaa;
	background: #cccccc url("images/ui-bg_highlight-soft_75_cccccc_1x100.png") 50% 50% repeat-x;
	color: #222222;
	font-weight: bold;
}
.ui-widget-header a {
	color: #222222;
}

/* Interaction states
----------------------------------*/
.ui-state-default,
.ui-widget-content .ui-state-default,
.ui-widget-header .ui-state-default,
.ui-button,

/* We use html here because we need a greater specificity to make sure disabled
works properly when clicked or hovered */
html .ui-button.ui-state-disabled:hover,
html .ui-button.ui-state-disabled:active {
	border: 1px solid #d3d3d3;
	background: #e6e6e6 url("images/ui-bg_glass_75_e6e6e6_1x400.png") 50% 50% repeat-x;
	font-weight: normal;
	color: #555555;
}
.ui-state-default a,
.ui-state-default a:link,
.ui-state-default a:visited,
a.ui-button,
a:link.ui-button,
a:visited.ui-button,
.ui-button {
	color: #555555;
	text-decoration: none;
}
.ui-state-hover,
.ui-widget-content .ui-state-hover,
.ui-widget-header .ui-state-hover,
.ui-state-focus,
.ui-widget-content .ui-state-focus,
.ui-widget-header .ui-state-focus,
.ui-button:hover,
.ui-button:focus {
	border: 1px solid #999999;
	background: #dadada url("images/ui-bg_glass_75_dadada_1x400.png") 50% 50% repeat-x;
	font-weight: normal;
	color: #212121;
}
.ui-state-hover a,
.ui-state-hover a:hover,
.ui-state-hover a:link,
.ui-state-hover a:visited,
.ui-state-focus a,
.ui-state-focus a:hover,
.ui-state-focus a:link,
.ui-state-focus a:visited,
a.ui-button:hover,
a.ui-button:focus {
	color: #212121;
	text-decoration: none;
}

.ui-visual-focus {
	box-shadow: 0 0 3px 1px rgb(94, 158, 214);
}
.ui-state-active,
.ui-widget-content .ui-state-active,
.ui-widget-header .ui-state-active,
a.ui-button:active,
.ui-button:active,
.ui-button.ui-state-active:hover {
	border: 1px solid #aaaaaa;
	background: #ffffff url("images/ui-bg_glass_65_ffffff_1x400.png") 50% 50% repeat-x;
	font-weight: normal;
	color: #212121;
}
.ui-icon-background,
.ui-state-active .ui-icon-background {
	border: #aaaaaa;
	background-color: #212121;
}
.ui-state-active a,
.ui-state-active a:link,
.ui-state-active a:visited {
	color: #212121;
	text-decoration: none;
}

/* Interaction Cues
----------------------------------*/
.ui-state-highlight,
.ui-widget-content .ui-state-highlight,
.ui-widget-header .ui-state-highlight {
	border: 1px solid #fcefa1;
	background: #fbf9ee url("images/ui-bg_glass_55_fbf9ee_1x400.png") 50% 50% repeat-x;
	color: #363636;
}
.ui-state-checked {
	border: 1px solid #fcefa1;
	background: #fbf9ee;
}
.ui-state-highlight a,
.ui-widget-content .ui-state-highlight a,
.ui-widget-header .ui-state-highlight a {
	color: #363636;
}
.ui-state-error,
.ui-widget-content .ui-state-error,
.ui-widget-header .ui-state-error {
	border: 1px solid #cd0a0a;
	background: #fef1ec url("images/ui-bg_glass_95_fef1ec_1x400.png") 50% 50% repeat-x;
	color: #cd0a0a;
}
.ui-state-error a,
.ui-widget-content .ui-state-error a,
.ui-widget-header .ui-state-error a {
	color: #cd0a0a;
}
.ui-state-error-text,
.ui-widget-content .ui-state-error-text,
.ui-widget-header .ui-state-error-text {
	color: #cd0a0a;
}
.ui-priority-primary,
.ui-widget-content .ui-priority-primary,
.ui-widget-header .ui-priority-primary {
	font-weight: bold;
}
.ui-priority-secondary,
.ui-widget-content .ui-priority-secondary,
.ui-widget-header .ui-priority-secondary {
	opacity: .7;
	filter:Alpha(Opacity=70); /* support: IE8 */
	font-weight: normal;
}
.ui-state-disabled,
.ui-widget-content .ui-state-disabled,
.ui-widget-header .ui-state-disabled {
	opacity: .35;
	filter:Alpha(Opacity=35); /* support: IE8 */
	background-image: none;
}
.ui-state-disabled .ui-icon {
	filter:Alpha(Opacity=35); /* support: IE8 - See #6059 */
}

/* Icons
----------------------------------*/

/* states and images */
.ui-icon {
	width: 16px;
	height: 16px;
}
.ui-icon,
.ui-widget-content .ui-icon {
	background-image: url("images/ui-icons_222222_256x240.png");
}
.ui-widget-header .ui-icon {
	background-image: url("images/ui-icons_222222_256x240.png");
}
.ui-state-hover .ui-icon,
.ui-state-focus .ui-icon,
.ui-button:hover .ui-icon,
.ui-button:focus .ui-icon {
	background-image: url("images/ui-icons_454545_256x240.png");
}
.ui-state-active .ui-icon,
.ui-button:active .ui-icon {
	background-image: url("images/ui-icons_454545_256x240.png");
}
.ui-state-highlight .ui-icon,
.ui-button .ui-state-highlight.ui-icon {
	background-image: url("images/ui-icons_2e83ff_256x240.png");
}
.ui-state-error .ui-icon,
.ui-state-error-text .ui-icon {
	background-image: url("images/ui-icons_cd0a0a_256x240.png");
}
.ui-button .ui-icon {
	background-image: url("images/ui-icons_888888_256x240.png");
}

/* positioning */
.ui-icon-blank { background-position: 16px 16px; }
.ui-icon-caret-1-n { background-position: 0 0; }
.ui-icon-caret-1-ne { background-position: -16px 0; }
.ui-icon-caret-1-e { background-position: -32px 0; }
.ui-icon-caret-1-se { background-position: -48px 0; }
.ui-icon-caret-1-s { background-position: -65px 0; }
.ui-icon-caret-1-sw { background-position: -80px 0; }
.ui-icon-caret-1-w { background-position: -96px 0; }
.ui-icon-caret-1-nw { background-position: -112px 0; }
.ui-icon-caret-2-n-s { background-position: -128px 0; }
.ui-icon-caret-2-e-w { background-position: -144px 0; }
.ui-icon-triangle-1-n { background-position: 0 -16px; }
.ui-icon-triangle-1-ne { background-position: -16px -16px; }
.ui-icon-triangle-1-e { background-position: -32px -16px; }
.ui-icon-triangle-1-se { background-position: -48px -16px; }
.ui-icon-triangle-1-s { background-position: -65px -16px; }
.ui-icon-triangle-1-sw { background-position: -80px -16px; }
.ui-icon-triangle-1-w { background-position: -96px -16px; }
.ui-icon-triangle-1-nw { background-position: -112px -16px; }
.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
.ui-icon-arrow-1-n { background-position: 0 -32px; }
.ui-icon-arrow-1-ne { background-position: -16px -32px; }
.ui-icon-arrow-1-e { background-position: -32px -32px; }
.ui-icon-arrow-1-se { background-position: -48px -32px; }
.ui-icon-arrow-1-s { background-position: -65px -32px; }
.ui-icon-arrow-1-sw { background-position: -80px -32px; }
.ui-icon-arrow-1-w { background-position: -96px -32px; }
.ui-icon-arrow-1-nw { background-position: -112px -32px; }
.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
.ui-icon-arrowthick-1-n { background-position: 1px -48px; }
.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
.ui-icon-arrow-4 { background-position: 0 -80px; }
.ui-icon-arrow-4-diag { background-position: -16px -80px; }
.ui-icon-extlink { background-position: -32px -80px; }
.ui-icon-newwin { background-position: -48px -80px; }
.ui-icon-refresh { background-position: -64px -80px; }
.ui-icon-shuffle { background-position: -80px -80px; }
.ui-icon-transfer-e-w { background-position: -96px -80px; }
.ui-icon-transferthick-e-w { background-position: -112px -80px; }
.ui-icon-folder-collapsed { background-position: 0 -96px; }
.ui-icon-folder-open { background-position: -16px -96px; }
.ui-icon-document { background-position: -32px -96px; }
.ui-icon-document-b { background-position: -48px -96px; }
.ui-icon-note { background-position: -64px -96px; }
.ui-icon-mail-closed { background-position: -80px -96px; }
.ui-icon-mail-open { background-position: -96px -96px; }
.ui-icon-suitcase { background-position: -112px -96px; }
.ui-icon-comment { background-position: -128px -96px; }
.ui-icon-person { background-position: -144px -96px; }
.ui-icon-print { background-position: -160px -96px; }
.ui-icon-trash { background-position: -176px -96px; }
.ui-icon-locked { background-position: -192px -96px; }
.ui-icon-unlocked { background-position: -208px -96px; }
.ui-icon-bookmark { background-position: -224px -96px; }
.ui-icon-tag { background-position: -240px -96px; }
.ui-icon-home { background-position: 0 -112px; }
.ui-icon-flag { background-position: -16px -112px; }
.ui-icon-calendar { background-position: -32px -112px; }
.ui-icon-cart { background-position: -48px -112px; }
.ui-icon-pencil { background-position: -64px -112px; }
.ui-icon-clock { background-position: -80px -112px; }
.ui-icon-disk { background-position: -96px -112px; }
.ui-icon-calculator { background-position: -112px -112px; }
.ui-icon-zoomin { background-position: -128px -112px; }
.ui-icon-zoomout { background-position: -144px -112px; }
.ui-icon-search { background-position: -160px -112px; }
.ui-icon-wrench { background-position: -176px -112px; }
.ui-icon-gear { background-position: -192px -112px; }
.ui-icon-heart { background-position: -208px -112px; }
.ui-icon-star { background-position: -224px -112px; }
.ui-icon-link { background-position: -240px -112px; }
.ui-icon-cancel { background-position: 0 -128px; }
.ui-icon-plus { background-position: -16px -128px; }
.ui-icon-plusthick { background-position: -32px -128px; }
.ui-icon-minus { background-position: -48px -128px; }
.ui-icon-minusthick { background-position: -64px -128px; }
.ui-icon-close { background-position: -80px -128px; }
.ui-icon-closethick { background-position: -96px -128px; }
.ui-icon-key { background-position: -112px -128px; }
.ui-icon-lightbulb { background-position: -128px -128px; }
.ui-icon-scissors { background-position: -144px -128px; }
.ui-icon-clipboard { background-position: -160px -128px; }
.ui-icon-copy { background-position: -176px -128px; }
.ui-icon-contact { background-position: -192px -128px; }
.ui-icon-image { background-position: -208px -128px; }
.ui-icon-video { background-position: -224px -128px; }
.ui-icon-script { background-position: -240px -128px; }
.ui-icon-alert { background-position: 0 -144px; }
.ui-icon-info { background-position: -16px -144px; }
.ui-icon-notice { background-position: -32px -144px; }
.ui-icon-help { background-position: -48px -144px; }
.ui-icon-check { background-position: -64px -144px; }
.ui-icon-bullet { background-position: -80px -144px; }
.ui-icon-radio-on { background-position: -96px -144px; }
.ui-icon-radio-off { background-position: -112px -144px; }
.ui-icon-pin-w { background-position: -128px -144px; }
.ui-icon-pin-s { background-position: -144px -144px; }
.ui-icon-play { background-position: 0 -160px; }
.ui-icon-pause { background-position: -16px -160px; }
.ui-icon-seek-next { background-position: -32px -160px; }
.ui-icon-seek-prev { background-position: -48px -160px; }
.ui-icon-seek-end { background-position: -64px -160px; }
.ui-icon-seek-start { background-position: -80px -160px; }
/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */
.ui-icon-seek-first { background-position: -80px -160px; }
.ui-icon-stop { background-position: -96px -160px; }
.ui-icon-eject { background-position: -112px -160px; }
.ui-icon-volume-off { background-position: -128px -160px; }
.ui-icon-volume-on { background-position: -144px -160px; }
.ui-icon-power { background-position: 0 -176px; }
.ui-icon-signal-diag { background-position: -16px -176px; }
.ui-icon-signal { background-position: -32px -176px; }
.ui-icon-battery-0 { background-position: -48px -176px; }
.ui-icon-battery-1 { background-position: -64px -176px; }
.ui-icon-battery-2 { background-position: -80px -176px; }
.ui-icon-battery-3 { background-position: -96px -176px; }
.ui-icon-circle-plus { background-position: 0 -192px; }
.ui-icon-circle-minus { background-position: -16px -192px; }
.ui-icon-circle-close { background-position: -32px -192px; }
.ui-icon-circle-triangle-e { background-position: -48px -192px; }
.ui-icon-circle-triangle-s { background-position: -64px -192px; }
.ui-icon-circle-triangle-w { background-position: -80px -192px; }
.ui-icon-circle-triangle-n { background-position: -96px -192px; }
.ui-icon-circle-arrow-e { background-position: -112px -192px; }
.ui-icon-circle-arrow-s { background-position: -128px -192px; }
.ui-icon-circle-arrow-w { background-position: -144px -192px; }
.ui-icon-circle-arrow-n { background-position: -160px -192px; }
.ui-icon-circle-zoomin { background-position: -176px -192px; }
.ui-icon-circle-zoomout { background-position: -192px -192px; }
.ui-icon-circle-check { background-position: -208px -192px; }
.ui-icon-circlesmall-plus { background-position: 0 -208px; }
.ui-icon-circlesmall-minus { background-position: -16px -208px; }
.ui-icon-circlesmall-close { background-position: -32px -208px; }
.ui-icon-squaresmall-plus { background-position: -48px -208px; }
.ui-icon-squaresmall-minus { background-position: -64px -208px; }
.ui-icon-squaresmall-close { background-position: -80px -208px; }
.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
.ui-icon-grip-diagonal-se { background-position: -80px -224px; }


/* Misc visuals
----------------------------------*/

/* Corner radius */
.ui-corner-all,
.ui-corner-top,
.ui-corner-left,
.ui-corner-tl {
	border-top-left-radius: 4px;
}
.ui-corner-all,
.ui-corner-top,
.ui-corner-right,
.ui-corner-tr {
	border-top-right-radius: 4px;
}
.ui-corner-all,
.ui-corner-bottom,
.ui-corner-left,
.ui-corner-bl {
	border-bottom-left-radius: 4px;
}
.ui-corner-all,
.ui-corner-bottom,
.ui-corner-right,
.ui-corner-br {
	border-bottom-right-radius: 4px;
}

/* Overlays */
.ui-widget-overlay {
	background: #aaaaaa;
	opacity: .3;
	filter: Alpha(Opacity=30); /* support: IE8 */
}
.ui-widget-shadow {
	-webkit-box-shadow: -8px -8px 8px #aaaaaa;
	box-shadow: -8px -8px 8px #aaaaaa;
}
js/jquery-ui/themes/smoothness/jquery-ui.min.css000064400000075066151330370000016035 0ustar00/*! jQuery UI - v1.12.1 - 2016-09-14
* http://jqueryui.com
* Includes: core.css, accordion.css, autocomplete.css, menu.css, button.css, controlgroup.css, checkboxradio.css, datepicker.css, dialog.css, draggable.css, resizable.css, progressbar.css, selectable.css, selectmenu.css, slider.css, sortable.css, spinner.css, tabs.css, tooltip.css, theme.css
* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana%2CArial%2Csans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=highlight_soft&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=flat&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=glass&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=glass&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=glass&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=glass&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px
* Copyright jQuery Foundation and other contributors; Licensed MIT */

.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important;pointer-events:none}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-accordion .ui-accordion-header{display:block;cursor:pointer;position:relative;margin:2px 0 0 0;padding:.5em .5em .5em .7em;font-size:100%}.ui-accordion .ui-accordion-content{padding:1em 2.2em;border-top:0;overflow:auto}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-menu{list-style:none;padding:0;margin:0;display:block;outline:0}.ui-menu .ui-menu{position:absolute}.ui-menu .ui-menu-item{margin:0;cursor:pointer;list-style-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")}.ui-menu .ui-menu-item-wrapper{position:relative;padding:3px 1em 3px .4em}.ui-menu .ui-menu-divider{margin:5px 0;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-state-focus,.ui-menu .ui-state-active{margin:-1px}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item-wrapper{padding-left:2em}.ui-menu .ui-icon{position:absolute;top:0;bottom:0;left:.2em;margin:auto 0}.ui-menu .ui-menu-icon{left:auto;right:0}.ui-button{padding:.4em 1em;display:inline-block;position:relative;line-height:normal;margin-right:.1em;cursor:pointer;vertical-align:middle;text-align:center;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;overflow:visible}.ui-button,.ui-button:link,.ui-button:visited,.ui-button:hover,.ui-button:active{text-decoration:none}.ui-button-icon-only{width:2em;box-sizing:border-box;text-indent:-9999px;white-space:nowrap}input.ui-button.ui-button-icon-only{text-indent:0}.ui-button-icon-only .ui-icon{position:absolute;top:50%;left:50%;margin-top:-8px;margin-left:-8px}.ui-button.ui-icon-notext .ui-icon{padding:0;width:2.1em;height:2.1em;text-indent:-9999px;white-space:nowrap}input.ui-button.ui-icon-notext .ui-icon{width:auto;height:auto;text-indent:0;white-space:normal;padding:.4em 1em}input.ui-button::-moz-focus-inner,button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-controlgroup{vertical-align:middle;display:inline-block}.ui-controlgroup > .ui-controlgroup-item{float:left;margin-left:0;margin-right:0}.ui-controlgroup > .ui-controlgroup-item:focus,.ui-controlgroup > .ui-controlgroup-item.ui-visual-focus{z-index:9999}.ui-controlgroup-vertical > .ui-controlgroup-item{display:block;float:none;width:100%;margin-top:0;margin-bottom:0;text-align:left}.ui-controlgroup-vertical .ui-controlgroup-item{box-sizing:border-box}.ui-controlgroup .ui-controlgroup-label{padding:.4em 1em}.ui-controlgroup .ui-controlgroup-label span{font-size:80%}.ui-controlgroup-horizontal .ui-controlgroup-label + .ui-controlgroup-item{border-left:none}.ui-controlgroup-vertical .ui-controlgroup-label + .ui-controlgroup-item{border-top:none}.ui-controlgroup-horizontal .ui-controlgroup-label.ui-widget-content{border-right:none}.ui-controlgroup-vertical .ui-controlgroup-label.ui-widget-content{border-bottom:none}.ui-controlgroup-vertical .ui-spinner-input{width:75%;width:calc( 100% - 2.4em )}.ui-controlgroup-vertical .ui-spinner .ui-spinner-up{border-top-style:solid}.ui-checkboxradio-label .ui-icon-background{box-shadow:inset 1px 1px 1px #ccc;border-radius:.12em;border:none}.ui-checkboxradio-radio-label .ui-icon-background{width:16px;height:16px;border-radius:1em;overflow:visible;border:none}.ui-checkboxradio-radio-label.ui-checkboxradio-checked .ui-icon,.ui-checkboxradio-radio-label.ui-checkboxradio-checked:hover .ui-icon{background-image:none;width:8px;height:8px;border-width:4px;border-style:solid}.ui-checkboxradio-disabled{pointer-events:none}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev-hover,.ui-datepicker .ui-datepicker-next-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:45%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:bold;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-datepicker .ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat;left:.5em;top:.3em}.ui-dialog{position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:20px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:none;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-n{height:2px;top:0}.ui-dialog .ui-resizable-e{width:2px;right:0}.ui-dialog .ui-resizable-s{height:2px;bottom:0}.ui-dialog .ui-resizable-w{width:2px;left:0}.ui-dialog .ui-resizable-se,.ui-dialog .ui-resizable-sw,.ui-dialog .ui-resizable-ne,.ui-dialog .ui-resizable-nw{width:7px;height:7px}.ui-dialog .ui-resizable-se{right:0;bottom:0}.ui-dialog .ui-resizable-sw{left:0;bottom:0}.ui-dialog .ui-resizable-ne{right:0;top:0}.ui-dialog .ui-resizable-nw{left:0;top:0}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-draggable-handle{-ms-touch-action:none;touch-action:none}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-progressbar{height:2em;text-align:left;overflow:hidden}.ui-progressbar .ui-progressbar-value{margin:-1px;height:100%}.ui-progressbar .ui-progressbar-overlay{background:url("data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw==");height:100%;filter:alpha(opacity=25);opacity:0.25}.ui-progressbar-indeterminate .ui-progressbar-value{background-image:none}.ui-selectable{-ms-touch-action:none;touch-action:none}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-selectmenu-menu{padding:0;margin:0;position:absolute;top:0;left:0;display:none}.ui-selectmenu-menu .ui-menu{overflow:auto;overflow-x:hidden;padding-bottom:1px}.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup{font-size:1em;font-weight:bold;line-height:1.5;padding:2px 0.4em;margin:0.5em 0 0 0;height:auto;border:0}.ui-selectmenu-open{display:block}.ui-selectmenu-text{display:block;margin-right:20px;overflow:hidden;text-overflow:ellipsis}.ui-selectmenu-button.ui-button{text-align:left;white-space:nowrap;width:14em}.ui-selectmenu-icon.ui-icon{float:right;margin-top:0}.ui-slider{position:relative;text-align:left}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:default;-ms-touch-action:none;touch-action:none}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider.ui-state-disabled .ui-slider-handle,.ui-slider.ui-state-disabled .ui-slider-range{filter:inherit}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-left:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{left:0}.ui-slider-horizontal .ui-slider-range-max{right:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{left:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0}.ui-sortable-handle{-ms-touch-action:none;touch-action:none}.ui-spinner{position:relative;display:inline-block;overflow:hidden;padding:0;vertical-align:middle}.ui-spinner-input{border:none;background:none;color:inherit;padding:.222em 0;margin:.2em 0;vertical-align:middle;margin-left:.4em;margin-right:2em}.ui-spinner-button{width:1.6em;height:50%;font-size:.5em;padding:0;margin:0;text-align:center;position:absolute;cursor:default;display:block;overflow:hidden;right:0}.ui-spinner a.ui-spinner-button{border-top-style:none;border-bottom-style:none;border-right-style:none}.ui-spinner-up{top:0}.ui-spinner-down{bottom:0}.ui-tabs{position:relative;padding:.2em}.ui-tabs .ui-tabs-nav{margin:0;padding:.2em .2em 0}.ui-tabs .ui-tabs-nav li{list-style:none;float:left;position:relative;top:0;margin:1px .2em 0 0;border-bottom-width:0;padding:0;white-space:nowrap}.ui-tabs .ui-tabs-nav .ui-tabs-anchor{float:left;padding:.5em 1em;text-decoration:none}.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:-1px;padding-bottom:1px}.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor{cursor:text}.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor{cursor:pointer}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:none}.ui-tooltip{padding:8px;position:absolute;z-index:9999;max-width:300px}body .ui-tooltip{border-width:2px}.ui-widget{font-family:Verdana,Arial,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Verdana,Arial,sans-serif;font-size:1em}.ui-widget.ui-widget-content{border:1px solid #d3d3d3}.ui-widget-content{border:1px solid #aaa;background:#fff;color:#222}.ui-widget-content a{color:#222}.ui-widget-header{border:1px solid #aaa;background:#ccc url("images/ui-bg_highlight-soft_75_cccccc_1x100.png") 50% 50% repeat-x;color:#222;font-weight:bold}.ui-widget-header a{color:#222}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default,.ui-button,html .ui-button.ui-state-disabled:hover,html .ui-button.ui-state-disabled:active{border:1px solid #d3d3d3;background:#e6e6e6 url("images/ui-bg_glass_75_e6e6e6_1x400.png") 50% 50% repeat-x;font-weight:normal;color:#555}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited,a.ui-button,a:link.ui-button,a:visited.ui-button,.ui-button{color:#555;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus,.ui-button:hover,.ui-button:focus{border:1px solid #999;background:#dadada url("images/ui-bg_glass_75_dadada_1x400.png") 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited,a.ui-button:hover,a.ui-button:focus{color:#212121;text-decoration:none}.ui-visual-focus{box-shadow:0 0 3px 1px rgb(94,158,214)}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active,a.ui-button:active,.ui-button:active,.ui-button.ui-state-active:hover{border:1px solid #aaa;background:#fff url("images/ui-bg_glass_65_ffffff_1x400.png") 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-icon-background,.ui-state-active .ui-icon-background{border:#aaa;background-color:#212121}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#212121;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #fcefa1;background:#fbf9ee url("images/ui-bg_glass_55_fbf9ee_1x400.png") 50% 50% repeat-x;color:#363636}.ui-state-checked{border:1px solid #fcefa1;background:#fbf9ee}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #cd0a0a;background:#fef1ec url("images/ui-bg_glass_95_fef1ec_1x400.png") 50% 50% repeat-x;color:#cd0a0a}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#cd0a0a}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#cd0a0a}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("images/ui-icons_222222_256x240.png")}.ui-widget-header .ui-icon{background-image:url("images/ui-icons_222222_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon,.ui-button:hover .ui-icon,.ui-button:focus .ui-icon{background-image:url("images/ui-icons_454545_256x240.png")}.ui-state-active .ui-icon,.ui-button:active .ui-icon{background-image:url("images/ui-icons_454545_256x240.png")}.ui-state-highlight .ui-icon,.ui-button .ui-state-highlight.ui-icon{background-image:url("images/ui-icons_2e83ff_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("images/ui-icons_cd0a0a_256x240.png")}.ui-button .ui-icon{background-image:url("images/ui-icons_888888_256x240.png")}.ui-icon-blank{background-position:16px 16px}.ui-icon-caret-1-n{background-position:0 0}.ui-icon-caret-1-ne{background-position:-16px 0}.ui-icon-caret-1-e{background-position:-32px 0}.ui-icon-caret-1-se{background-position:-48px 0}.ui-icon-caret-1-s{background-position:-65px 0}.ui-icon-caret-1-sw{background-position:-80px 0}.ui-icon-caret-1-w{background-position:-96px 0}.ui-icon-caret-1-nw{background-position:-112px 0}.ui-icon-caret-2-n-s{background-position:-128px 0}.ui-icon-caret-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-65px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-65px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:1px -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:4px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:4px}.ui-widget-overlay{background:#aaa;opacity:.3;filter:Alpha(Opacity=30)}.ui-widget-shadow{-webkit-box-shadow:-8px -8px 8px #aaa;box-shadow:-8px -8px 8px #aaa}js/index.js000064400000031217151330370000006621 0ustar00(()=>{"use strict";const e=e=>Math.abs(parseInt(e,10)),t=(e,t)=>{const a=new Map([["init","init"],["validation_failed","invalid"],["acceptance_missing","unaccepted"],["spam","spam"],["aborted","aborted"],["mail_sent","sent"],["mail_failed","failed"],["submitting","submitting"],["resetting","resetting"],["validating","validating"],["payment_required","payment-required"]]);a.has(t)&&(t=a.get(t)),Array.from(a.values()).includes(t)||(t=`custom-${t=(t=t.replace(/[^0-9a-z]+/i," ").trim()).replace(/\s+/,"-")}`);const n=e.getAttribute("data-status");return e.wpcf7.status=t,e.setAttribute("data-status",t),e.classList.add(t),n&&n!==t&&e.classList.remove(n),t},a=(e,t,a)=>{const n=new CustomEvent(`wpcf7${t}`,{bubbles:!0,detail:a});"string"==typeof e&&(e=document.querySelector(e)),e.dispatchEvent(n)},n=e=>{const{root:t,namespace:a="contact-form-7/v1"}=wpcf7.api;return r.reduceRight(((e,t)=>a=>t(a,e)),(e=>{let n,r,{url:o,path:c,endpoint:s,headers:i,body:l,data:d,...p}=e;"string"==typeof s&&(n=a.replace(/^\/|\/$/g,""),r=s.replace(/^\//,""),c=r?n+"/"+r:n),"string"==typeof c&&(-1!==t.indexOf("?")&&(c=c.replace("?","&")),c=c.replace(/^\//,""),o=t+c),i={Accept:"application/json, */*;q=0.1",...i},delete i["X-WP-Nonce"],d&&(l=JSON.stringify(d),i["Content-Type"]="application/json");const u={code:"fetch_error",message:"You are probably offline."},f={code:"invalid_json",message:"The response is not a valid JSON response."};return window.fetch(o||c||window.location.href,{...p,headers:i,body:l}).then((e=>Promise.resolve(e).then((e=>{if(e.status>=200&&e.status<300)return e;throw e})).then((e=>{if(204===e.status)return null;if(e&&e.json)return e.json().catch((()=>{throw f}));throw f}))),(()=>{throw u}))}))(e)},r=[];function o(e){var a,n;let r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const{target:o,scope:l=e,...d}=r;if(void 0===(null===(a=e.wpcf7)||void 0===a?void 0:a.schema))return;const p={...e.wpcf7.schema};if(void 0!==o){if(!e.contains(o))return;if(!o.closest(".wpcf7-form-control-wrap[data-name]"))return;if(o.closest(".novalidate"))return}const u=new FormData,f=[];for(const e of l.querySelectorAll(".wpcf7-form-control-wrap"))if(!e.closest(".novalidate")&&(e.querySelectorAll(":where( input, textarea, select ):enabled").forEach((e=>{if(e.name)switch(e.type){case"button":case"image":case"reset":case"submit":break;case"checkbox":case"radio":e.checked&&u.append(e.name,e.value);break;case"select-multiple":for(const t of e.selectedOptions)u.append(e.name,t.value);break;case"file":for(const t of e.files)u.append(e.name,t);break;default:u.append(e.name,e.value)}})),e.dataset.name&&(f.push(e.dataset.name),e.setAttribute("data-under-validation","1"),e.contains(o))))break;p.rules=(null!==(n=p.rules)&&void 0!==n?n:[]).filter((e=>{let{field:t}=e;return f.includes(t)}));const m=e.getAttribute("data-status");Promise.resolve(t(e,"validating")).then((t=>{if(void 0!==swv){const t=swv.validate(p,u,r);for(const[a,{error:n,validInputs:r}]of t)s(e,a),void 0!==n&&c(e,a,n,{scope:l}),i(e,a,null!=r?r:[])}})).finally((()=>{t(e,m),e.querySelectorAll(".wpcf7-form-control-wrap[data-under-validation]").forEach((e=>{e.removeAttribute("data-under-validation")}))}))}n.use=e=>{r.unshift(e)};const c=(e,t,a,n)=>{var r;const{scope:o=e,...c}=null!=n?n:{},s=`${null===(r=e.wpcf7)||void 0===r?void 0:r.unitTag}-ve-${t}`.replaceAll(/[^0-9a-z_-]+/gi,""),i=e.querySelector(`.wpcf7-form-control-wrap[data-name="${t}"] .wpcf7-form-control`);(()=>{const t=document.createElement("li");t.setAttribute("id",s),i&&i.id?t.insertAdjacentHTML("beforeend",`<a href="#${i.id}">${a}</a>`):t.insertAdjacentText("beforeend",a),e.wpcf7.parent.querySelector(".screen-reader-response ul").appendChild(t)})(),o.querySelectorAll(`.wpcf7-form-control-wrap[data-name="${t}"]`).forEach((t=>{if("validating"===e.getAttribute("data-status")&&!t.dataset.underValidation)return;const n=document.createElement("span");n.classList.add("wpcf7-not-valid-tip"),n.setAttribute("aria-hidden","true"),n.insertAdjacentText("beforeend",a),t.appendChild(n),t.querySelectorAll("[aria-invalid]").forEach((e=>{e.setAttribute("aria-invalid","true")})),t.querySelectorAll(".wpcf7-form-control").forEach((e=>{e.classList.add("wpcf7-not-valid"),e.setAttribute("aria-describedby",s),"function"==typeof e.setCustomValidity&&e.setCustomValidity(a),e.closest(".use-floating-validation-tip")&&(e.addEventListener("focus",(e=>{n.setAttribute("style","display: none")})),n.addEventListener("click",(e=>{n.setAttribute("style","display: none")})))}))}))},s=(e,t)=>{var a,n;const r=`${null===(a=e.wpcf7)||void 0===a?void 0:a.unitTag}-ve-${t}`.replaceAll(/[^0-9a-z_-]+/gi,"");null===(n=e.wpcf7.parent.querySelector(`.screen-reader-response ul li#${r}`))||void 0===n||n.remove(),e.querySelectorAll(`.wpcf7-form-control-wrap[data-name="${t}"]`).forEach((e=>{var t;null===(t=e.querySelector(".wpcf7-not-valid-tip"))||void 0===t||t.remove(),e.querySelectorAll("[aria-invalid]").forEach((e=>{e.setAttribute("aria-invalid","false")})),e.querySelectorAll(".wpcf7-form-control").forEach((e=>{e.removeAttribute("aria-describedby"),e.classList.remove("wpcf7-not-valid"),"function"==typeof e.setCustomValidity&&e.setCustomValidity("")}))}))},i=(e,t,a)=>{e.querySelectorAll(`[data-reflection-of="${t}"]`).forEach((e=>{if("output"===e.tagName.toLowerCase()){const t=e;0===a.length&&a.push(t.dataset.default),a.slice(0,1).forEach((e=>{e instanceof File&&(e=e.name),t.textContent=e}))}else e.querySelectorAll("output").forEach((e=>{e.hasAttribute("data-default")?0===a.length?e.removeAttribute("hidden"):e.setAttribute("hidden","hidden"):e.remove()})),a.forEach((a=>{a instanceof File&&(a=a.name);const n=document.createElement("output");n.setAttribute("name",t),n.textContent=a,e.appendChild(n)}))}))};function l(e){let r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if(wpcf7.blocked)return d(e),void t(e,"submitting");const o=new FormData(e);r.submitter&&r.submitter.name&&o.append(r.submitter.name,r.submitter.value);const s={contactFormId:e.wpcf7.id,pluginVersion:e.wpcf7.pluginVersion,contactFormLocale:e.wpcf7.locale,unitTag:e.wpcf7.unitTag,containerPostId:e.wpcf7.containerPost,status:e.wpcf7.status,inputs:Array.from(o,(e=>{const t=e[0],a=e[1];return!t.match(/^_/)&&{name:t,value:a}})).filter((e=>!1!==e)),formData:o};n({endpoint:`contact-forms/${e.wpcf7.id}/feedback`,method:"POST",body:o,wpcf7:{endpoint:"feedback",form:e,detail:s}}).then((n=>{const r=t(e,n.status);return s.status=n.status,s.apiResponse=n,["invalid","unaccepted","spam","aborted"].includes(r)?a(e,r,s):["sent","failed"].includes(r)&&a(e,`mail${r}`,s),a(e,"submit",s),n})).then((t=>{t.posted_data_hash&&(e.querySelector('input[name="_wpcf7_posted_data_hash"]').value=t.posted_data_hash),"mail_sent"===t.status&&(e.reset(),e.wpcf7.resetOnMailSent=!0),t.invalid_fields&&t.invalid_fields.forEach((t=>{c(e,t.field,t.message)})),e.wpcf7.parent.querySelector('.screen-reader-response [role="status"]').insertAdjacentText("beforeend",t.message),e.querySelectorAll(".wpcf7-response-output").forEach((e=>{e.innerText=t.message}))})).catch((e=>console.error(e)))}n.use(((e,n)=>{if(e.wpcf7&&"feedback"===e.wpcf7.endpoint){const{form:n,detail:r}=e.wpcf7;d(n),a(n,"beforesubmit",r),t(n,"submitting")}return n(e)}));const d=e=>{e.querySelectorAll(".wpcf7-form-control-wrap").forEach((t=>{t.dataset.name&&s(e,t.dataset.name)})),e.wpcf7.parent.querySelector('.screen-reader-response [role="status"]').innerText="",e.querySelectorAll(".wpcf7-response-output").forEach((e=>{e.innerText=""}))};function p(e){const r=new FormData(e),o={contactFormId:e.wpcf7.id,pluginVersion:e.wpcf7.pluginVersion,contactFormLocale:e.wpcf7.locale,unitTag:e.wpcf7.unitTag,containerPostId:e.wpcf7.containerPost,status:e.wpcf7.status,inputs:Array.from(r,(e=>{const t=e[0],a=e[1];return!t.match(/^_/)&&{name:t,value:a}})).filter((e=>!1!==e)),formData:r};n({endpoint:`contact-forms/${e.wpcf7.id}/refill`,method:"GET",wpcf7:{endpoint:"refill",form:e,detail:o}}).then((n=>{e.wpcf7.resetOnMailSent?(delete e.wpcf7.resetOnMailSent,t(e,"mail_sent")):t(e,"init"),o.apiResponse=n,a(e,"reset",o)})).catch((e=>console.error(e)))}n.use(((e,a)=>{if(e.wpcf7&&"refill"===e.wpcf7.endpoint){const{form:a,detail:n}=e.wpcf7;d(a),t(a,"resetting")}return a(e)}));const u=(e,t)=>{for(const a in t){const n=t[a];e.querySelectorAll(`input[name="${a}"]`).forEach((e=>{e.value=""})),e.querySelectorAll(`img.wpcf7-captcha-${a.replaceAll(":","")}`).forEach((e=>{e.setAttribute("src",n)}));const r=/([0-9]+)\.(png|gif|jpeg)$/.exec(n);r&&e.querySelectorAll(`input[name="_wpcf7_captcha_challenge_${a}"]`).forEach((e=>{e.value=r[1]}))}},f=(e,t)=>{for(const a in t){const n=t[a][0],r=t[a][1];e.querySelectorAll(`.wpcf7-form-control-wrap[data-name="${a}"]`).forEach((e=>{e.querySelector(`input[name="${a}"]`).value="",e.querySelector(".wpcf7-quiz-label").textContent=n,e.querySelector(`input[name="_wpcf7_quiz_answer_${a}"]`).value=r}))}};function m(t){const a=new FormData(t);t.wpcf7={id:e(a.get("_wpcf7")),status:t.getAttribute("data-status"),pluginVersion:a.get("_wpcf7_version"),locale:a.get("_wpcf7_locale"),unitTag:a.get("_wpcf7_unit_tag"),containerPost:e(a.get("_wpcf7_container_post")),parent:t.closest(".wpcf7"),schema:void 0},t.querySelectorAll(".has-spinner").forEach((e=>{e.insertAdjacentHTML("afterend",'<span class="wpcf7-spinner"></span>')})),(e=>{e.querySelectorAll(".wpcf7-exclusive-checkbox").forEach((t=>{t.addEventListener("change",(t=>{const a=t.target.getAttribute("name");e.querySelectorAll(`input[type="checkbox"][name="${a}"]`).forEach((e=>{e!==t.target&&(e.checked=!1)}))}))}))})(t),(e=>{e.querySelectorAll(".has-free-text").forEach((t=>{const a=t.querySelector("input.wpcf7-free-text"),n=t.querySelector('input[type="checkbox"], input[type="radio"]');a.disabled=!n.checked,e.addEventListener("change",(e=>{a.disabled=!n.checked,e.target===n&&n.checked&&a.focus()}))}))})(t),(e=>{e.querySelectorAll(".wpcf7-validates-as-url").forEach((e=>{e.addEventListener("change",(t=>{let a=e.value.trim();a&&!a.match(/^[a-z][a-z0-9.+-]*:/i)&&-1!==a.indexOf(".")&&(a=a.replace(/^\/+/,""),a="http://"+a),e.value=a}))}))})(t),(e=>{if(!e.querySelector(".wpcf7-acceptance")||e.classList.contains("wpcf7-acceptance-as-validation"))return;const t=()=>{let t=!0;e.querySelectorAll(".wpcf7-acceptance").forEach((e=>{if(!t||e.classList.contains("optional"))return;const a=e.querySelector('input[type="checkbox"]');(e.classList.contains("invert")&&a.checked||!e.classList.contains("invert")&&!a.checked)&&(t=!1)})),e.querySelectorAll(".wpcf7-submit").forEach((e=>{e.disabled=!t}))};t(),e.addEventListener("change",(e=>{t()})),e.addEventListener("wpcf7reset",(e=>{t()}))})(t),(t=>{const a=(t,a)=>{const n=e(t.getAttribute("data-starting-value")),r=e(t.getAttribute("data-maximum-value")),o=e(t.getAttribute("data-minimum-value")),c=t.classList.contains("down")?n-a.value.length:a.value.length;t.setAttribute("data-current-value",c),t.innerText=c,r&&r<a.value.length?t.classList.add("too-long"):t.classList.remove("too-long"),o&&a.value.length<o?t.classList.add("too-short"):t.classList.remove("too-short")},n=e=>{e={init:!1,...e},t.querySelectorAll(".wpcf7-character-count").forEach((n=>{const r=n.getAttribute("data-target-name"),o=t.querySelector(`[name="${r}"]`);o&&(o.value=o.defaultValue,a(n,o),e.init&&o.addEventListener("keyup",(e=>{a(n,o)})))}))};n({init:!0}),t.addEventListener("wpcf7reset",(e=>{n()}))})(t),window.addEventListener("load",(e=>{wpcf7.cached&&t.reset()})),t.addEventListener("reset",(e=>{wpcf7.reset(t)})),t.addEventListener("submit",(e=>{wpcf7.submit(t,{submitter:e.submitter}),e.preventDefault()})),t.addEventListener("wpcf7submit",(e=>{e.detail.apiResponse.captcha&&u(t,e.detail.apiResponse.captcha),e.detail.apiResponse.quiz&&f(t,e.detail.apiResponse.quiz)})),t.addEventListener("wpcf7reset",(e=>{e.detail.apiResponse.captcha&&u(t,e.detail.apiResponse.captcha),e.detail.apiResponse.quiz&&f(t,e.detail.apiResponse.quiz)})),n({endpoint:`contact-forms/${t.wpcf7.id}/feedback/schema`,method:"GET"}).then((e=>{t.wpcf7.schema=e})),t.addEventListener("change",(e=>{e.target.closest(".wpcf7-form-control")&&wpcf7.validate(t,{target:e.target})}))}document.addEventListener("DOMContentLoaded",(e=>{var t;"undefined"!=typeof wpcf7?void 0!==wpcf7.api?"function"==typeof window.fetch?"function"==typeof window.FormData?"function"==typeof NodeList.prototype.forEach?"function"==typeof String.prototype.replaceAll?(wpcf7={init:m,submit:l,reset:p,validate:o,...null!==(t=wpcf7)&&void 0!==t?t:{}},document.querySelectorAll(".wpcf7 > form").forEach((e=>{wpcf7.init(e),e.closest(".wpcf7").classList.replace("no-js","js")}))):console.error("Your browser does not support String.replaceAll()."):console.error("Your browser does not support NodeList.forEach()."):console.error("Your browser does not support window.FormData()."):console.error("Your browser does not support window.fetch()."):console.error("wpcf7.api is not defined."):console.error("wpcf7 is not defined.")}))})();js/index.asset.php000064400000000121151330370000010100 0ustar00<?php

return array(
	'dependencies' => array(),
	'version' => WPCF7_VERSION,
);
html-formatter.php000064400000040503151330370000010214 0ustar00<?php

/**
 * Contact Form 7's class used for formatting HTML fragments.
 */
class WPCF7_HTMLFormatter {

	// HTML component types.
	const text = 0;
	const start_tag = 1;
	const end_tag = 2;
	const comment = 3;

	/**
	 * Tag name reserved for a custom HTML element used as a block placeholder.
	 */
	const placeholder_block = 'placeholder:block';

	/**
	 * Tag name reserved for a custom HTML element used as an inline placeholder.
	 */
	const placeholder_inline = 'placeholder:inline';

	/**
	 * The void elements in HTML.
	 *
	 * @link https://developer.mozilla.org/en-US/docs/Glossary/Void_element
	 */
	const void_elements = array(
		'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
		'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr',
		self::placeholder_block, self::placeholder_inline,
	);

	/**
	 * HTML elements that can contain flow content.
	 */
	const p_parent_elements = array(
		'address', 'article', 'aside', 'blockquote', 'body', 'caption',
		'dd', 'details', 'dialog', 'div', 'dt', 'fieldset', 'figcaption',
		'figure', 'footer', 'form', 'header', 'li', 'main', 'nav',
		'section', 'td', 'th',
	);

	/**
	 * HTML elements that can be neither the parent nor a child of
	 * a paragraph element.
	 */
	const p_nonparent_elements = array(
		'colgroup', 'dl', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head',
		'hgroup', 'html', 'legend', 'menu', 'ol', 'pre', 'style', 'summary',
		'table', 'tbody', 'template', 'tfoot', 'thead', 'title', 'tr', 'ul',
	);

	/**
	 * HTML elements in the phrasing content category, plus non-phrasing
	 * content elements that can be grandchildren of a paragraph element.
	 */
	const p_child_elements = array(
		'a', 'abbr', 'area', 'audio', 'b', 'bdi', 'bdo', 'br', 'button',
		'canvas', 'cite', 'code', 'data', 'datalist', 'del', 'dfn',
		'em', 'embed', 'i', 'iframe', 'img', 'input', 'ins', 'kbd',
		'keygen', 'label', 'link', 'map', 'mark', 'meta',
		'meter', 'noscript', 'object', 'output', 'picture', 'progress',
		'q', 'ruby', 's', 'samp', 'script', 'select', 'slot', 'small',
		'span', 'strong', 'sub', 'sup', 'textarea',
		'time', 'u', 'var', 'video', 'wbr',
		'optgroup', 'option', 'rp', 'rt', // non-phrasing grandchildren
		self::placeholder_inline,
	);

	/**
	 * HTML elements that can contain phrasing content.
	 */
	const br_parent_elements = array(
		'a', 'abbr', 'address', 'article', 'aside', 'audio', 'b', 'bdi',
		'bdo', 'blockquote', 'button', 'canvas', 'caption', 'cite', 'code',
		'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'div',
		'dt', 'em', 'fieldset', 'figcaption', 'figure', 'footer', 'form',
		'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'i', 'ins', 'kbd',
		'label', 'legend', 'li', 'main', 'map', 'mark', 'meter', 'nav',
		'noscript', 'object', 'output', 'p', 'progress', 'q', 'rt',
		'ruby', 's', 'samp', 'section', 'slot', 'small', 'span', 'strong',
		'sub', 'summary', 'sup', 'td', 'th', 'time', 'u', 'var',
		'video',
	);


	// Properties.
	private $options = array();
	private $stacked_elements = array();
	private $output = '';


	/**
	 * Constructor.
	 */
	public function __construct( $args = '' ) {
		$this->options = wp_parse_args( $args, array(
			'auto_br' => true,
			'auto_indent' => true,
		) );
	}


	/**
	 * Separates the given text into chunks of HTML. Each chunk must be an
	 * associative array that includes 'position', 'type', and 'content' keys.
	 *
	 * @param string $input Text to be separated into chunks.
	 * @return iterable Iterable of chunks.
	 */
	public function separate_into_chunks( $input ) {
		$input_bytelength = strlen( $input );
		$position = 0;

		while ( $position < $input_bytelength ) {
			$next_tag = preg_match(
				'/(?:<!--.*?-->|<(?:\/?)[a-z].*?>)/is',
				$input,
				$matches,
				PREG_OFFSET_CAPTURE,
				$position
			);

			if ( ! $next_tag ) {
				yield array(
					'position' => $position,
					'type' => self::text,
					'content' => substr( $input, $position ),
				);

				break;
			}

			$next_tag = $matches[0][0];
			$next_tag_position = $matches[0][1];

			if ( $position < $next_tag_position ) {
				yield array(
					'position' => $position,
					'type' => self::text,
					'content' => substr(
						$input,
						$position,
						$next_tag_position - $position
					),
				);
			}

			if ( '<!' === substr( $next_tag, 0, 2 ) ) {
				$next_tag_type = self::comment;
			} elseif ( '</' === substr( $next_tag, 0, 2 ) ) {
				$next_tag_type = self::end_tag;
			} else {
				$next_tag_type = self::start_tag;
			}

			yield array(
				'position' => $next_tag_position,
				'type' => $next_tag_type,
				'content' => substr(
					$input,
					$next_tag_position,
					strlen( $next_tag )
				),
			);

			$position = $next_tag_position + strlen( $next_tag );
		}
	}


	/**
	 * Normalizes content in each chunk. This may change the type and position
	 * of the chunk.
	 *
	 * @param iterable $chunks The original chunks.
	 * @return iterable Normalized chunks.
	 */
	public function pre_format( $chunks ) {
		$position = 0;

		foreach ( $chunks as $chunk ) {
			$chunk['position'] = $position;

			// Standardize newline characters to "\n".
			$chunk['content'] = str_replace(
				array( "\r\n", "\r" ), "\n", $chunk['content']
			);

			if ( $chunk['type'] === self::start_tag ) {
				list( $chunk['content'] ) =
					self::normalize_start_tag( $chunk['content'] );

				// Replace <br /> by a line break.
				if (
					$this->options['auto_br'] and
					preg_match( '/^<br\s*\/?>$/i', $chunk['content'] )
				) {
					$chunk['type'] = self::text;
					$chunk['content'] = "\n";
				}
			}

			yield $chunk;
			$position = self::calc_next_position( $chunk );
		}
	}


	/**
	 * Concatenates neighboring text chunks to create a single chunk.
	 *
	 * @param iterable $chunks The original chunks.
	 * @return iterable Processed chunks.
	 */
	public function concatenate_texts( $chunks ) {
		$position = 0;
		$text_left = null;

		foreach ( $chunks as $chunk ) {
			$chunk['position'] = $position;

			if ( $chunk['type'] === self::text ) {
				if ( isset( $text_left ) ) {
					$text_left['content'] .= $chunk['content'];
				} else {
					$text_left = $chunk;
				}

				continue;
			}

			if ( isset( $text_left ) ) {
				yield $text_left;
				$chunk['position'] = self::calc_next_position( $text_left );
				$text_left = null;
			}

			yield $chunk;
			$position = self::calc_next_position( $chunk );
		}

		if ( isset( $text_left ) ) {
			yield $text_left;
		}
	}


	/**
	 * Outputs formatted HTML based on the given chunks.
	 *
	 * @param iterable $chunks The original chunks.
	 * @return string Formatted HTML.
	 */
	public function format( $chunks ) {
		$chunks = $this->pre_format( $chunks );
		$chunks = $this->concatenate_texts( $chunks );

		$this->output = '';
		$this->stacked_elements = array();

		foreach ( $chunks as $chunk ) {

			if ( $chunk['type'] === self::text ) {
				$this->append_text( $chunk['content'] );
			}

			if ( $chunk['type'] === self::start_tag ) {
				$this->start_tag( $chunk['content'] );
			}

			if ( $chunk['type'] === self::end_tag ) {
				$this->end_tag( $chunk['content'] );
			}

			if ( $chunk['type'] === self::comment ) {
				$this->append_comment( $chunk['content'] );
			}
		}

		// Close all remaining tags.
		$this->close_all_tags();

		return $this->output;
	}


	/**
	 * Appends a text node content to the output property.
	 *
	 * @param string $content Text node content.
	 */
	public function append_text( $content ) {
		if ( $this->is_inside( array( 'pre', 'template' ) ) ) {
			$this->output .= $content;
			return;
		}

		if (
			empty( $this->stacked_elements ) or
			$this->has_parent( 'p' ) or
			$this->has_parent( self::p_parent_elements )
		) {
			// Close <p> if the content starts with multiple line breaks.
			if ( preg_match( '/^\s*\n\s*\n\s*/', $content ) ) {
				$this->end_tag( 'p' );
			}

			// Split up the contents into paragraphs, separated by double line breaks.
			$paragraphs = preg_split( '/\s*\n\s*\n\s*/', $content );

			$paragraphs = array_filter( $paragraphs, static function ( $paragraph ) {
				return '' !== trim( $paragraph );
			} );

			$paragraphs = array_values( $paragraphs );

			if ( $paragraphs ) {
				if ( $this->is_inside( 'p' ) ) {
					$paragraph = array_shift( $paragraphs );

					$paragraph = self::normalize_paragraph(
						$paragraph,
						$this->options['auto_br']
					);

					$this->output .= $paragraph;
				}

				foreach ( $paragraphs as $paragraph ) {
					$this->start_tag( 'p' );

					$paragraph = ltrim( $paragraph );

					$paragraph = self::normalize_paragraph(
						$paragraph,
						$this->options['auto_br']
					);

					$this->output .= $paragraph;
				}
			}

			// Close <p> if the content ends with multiple line breaks.
			if ( preg_match( '/\s*\n\s*\n\s*$/', $content ) ) {
				$this->end_tag( 'p' );
			}

			// Cases where the content is a single line break.
			if ( preg_match( '/^\s*\n\s*$/', $content ) ) {
				$auto_br = $this->options['auto_br'] && $this->is_inside( 'p' );

				$content = self::normalize_paragraph( $content, $auto_br );

				$this->output .= $content;
			}
		} else {
			$auto_br = $this->options['auto_br'] &&
				$this->has_parent( self::br_parent_elements );

			$content = self::normalize_paragraph( $content, $auto_br );

			$this->output .= $content;
		}
	}


	/**
	 * Appends a start tag to the output property.
	 *
	 * @param string $tag A start tag.
	 */
	public function start_tag( $tag ) {
		list( $tag, $tag_name ) = self::normalize_start_tag( $tag );

		if ( in_array( $tag_name, self::p_child_elements ) ) {
			if (
				! $this->is_inside( 'p' ) and
				! $this->is_inside( self::p_child_elements ) and
				! $this->has_parent( self::p_nonparent_elements )
			) {
				// Open <p> if it does not exist.
				$this->start_tag( 'p' );
			}
		} elseif (
			'p' === $tag_name or
			in_array( $tag_name, self::p_parent_elements ) or
			in_array( $tag_name, self::p_nonparent_elements )
		) {
			// Close <p> if it exists.
			$this->end_tag( 'p' );
		}

		if ( 'dd' === $tag_name or 'dt' === $tag_name ) {
			// Close <dd> and <dt> if closing tag is omitted.
			$this->end_tag( 'dd' );
			$this->end_tag( 'dt' );
		}

		if ( 'li' === $tag_name ) {
			// Close <li> if closing tag is omitted.
			$this->end_tag( 'li' );
		}

		if ( 'optgroup' === $tag_name ) {
			// Close <option> and <optgroup> if closing tag is omitted.
			$this->end_tag( 'option' );
			$this->end_tag( 'optgroup' );
		}

		if ( 'option' === $tag_name ) {
			// Close <option> if closing tag is omitted.
			$this->end_tag( 'option' );
		}

		if ( 'rp' === $tag_name or 'rt' === $tag_name ) {
			// Close <rp> and <rt> if closing tag is omitted.
			$this->end_tag( 'rp' );
			$this->end_tag( 'rt' );
		}

		if ( 'td' === $tag_name or 'th' === $tag_name ) {
			// Close <td> and <th> if closing tag is omitted.
			$this->end_tag( 'td' );
			$this->end_tag( 'th' );
		}

		if ( 'tr' === $tag_name ) {
			// Close <tr> if closing tag is omitted.
			$this->end_tag( 'tr' );
		}

		if ( 'tbody' === $tag_name or 'tfoot' === $tag_name ) {
			// Close <thead> if closing tag is omitted.
			$this->end_tag( 'thead' );
		}

		if ( 'tfoot' === $tag_name ) {
			// Close <tbody> if closing tag is omitted.
			$this->end_tag( 'tbody' );
		}

		if ( ! in_array( $tag_name, self::void_elements ) ) {
			array_unshift( $this->stacked_elements, $tag_name );
		}

		if ( ! in_array( $tag_name, self::p_child_elements ) ) {
			if ( '' !== $this->output ) {
				$this->output = rtrim( $this->output ) . "\n";
			}

			if ( $this->options['auto_indent'] ) {
				$this->output .= self::indent( count( $this->stacked_elements ) - 1 );
			}
		}

		$this->output .= $tag;
	}


	/**
	 * Closes an element and its open descendants at a time.
	 *
	 * @param string $tag An end tag.
	 */
	public function end_tag( $tag ) {
		if ( preg_match( '/<\/(.+?)(?:\s|>)/', $tag, $matches ) ) {
			$tag_name = strtolower( $matches[1] );
		} else {
			$tag_name = strtolower( $tag );
		}

		$stacked_elements = array_values( $this->stacked_elements );

		$tag_position = array_search( $tag_name, $stacked_elements );

		if ( false === $tag_position ) {
			return;
		}

		// Element groups that make up an indirect nesting structure.
		// Descendant can contain ancestors.
		static $nesting_families = array(
			array(
				'ancestors' => array( 'dl', ),
				'descendants' => array( 'dd', 'dt', ),
			),
			array(
				'ancestors' => array( 'ol', 'ul', 'menu', ),
				'descendants' => array( 'li', ),
			),
			array(
				'ancestors' => array( 'table', ),
				'descendants' => array( 'td', 'th', 'tr', 'thead', 'tbody', 'tfoot', ),
			),
		);

		foreach ( $nesting_families as $family ) {
			$ancestors = (array) $family['ancestors'];
			$descendants = (array) $family['descendants'];

			if ( in_array( $tag_name, $descendants ) ) {
				$intersect = array_intersect(
					$ancestors,
					array_slice( $stacked_elements, 0, $tag_position )
				);

				if ( $intersect ) { // Ancestor appears after descendant.
					return;
				}
			}
		}

		while ( $element = array_shift( $this->stacked_elements ) ) {
			$this->append_end_tag( $element );

			if ( $element === $tag_name ) {
				break;
			}
		}
	}


	/**
	 * Closes all open tags.
	 */
	public function close_all_tags() {
		while ( $element = array_shift( $this->stacked_elements ) ) {
			$this->append_end_tag( $element );
		}
	}


	/**
	 * Appends an end tag to the output property.
	 *
	 * @param string $tag_name Tag name.
	 */
	public function append_end_tag( $tag_name ) {
		if ( ! in_array( $tag_name, self::p_child_elements ) ) {
			// Remove unnecessary <br />.
			$this->output = preg_replace( '/\s*<br \/>\s*$/', '', $this->output );

			$this->output = rtrim( $this->output ) . "\n";

			if ( $this->options['auto_indent'] ) {
				$this->output .= self::indent( count( $this->stacked_elements ) );
			}
		}

		$this->output .= sprintf( '</%s>', $tag_name );

		// Remove trailing <p></p>.
		$this->output = preg_replace( '/<p>\s*<\/p>$/', '', $this->output );
	}


	/**
	 * Appends an HTML comment to the output property.
	 *
	 * @param string $tag An HTML comment.
	 */
	public function append_comment( $tag ) {
		$this->output .= $tag;
	}


	/**
	 * Returns true if it is currently inside one of HTML elements specified
	 * by tag names.
	 *
	 * @param string|array $tag_names A tag name or an array of tag names.
	 */
	public function is_inside( $tag_names ) {
		$tag_names = (array) $tag_names;

		foreach ( $this->stacked_elements as $element ) {
			if ( in_array( $element, $tag_names ) ) {
				return true;
			}
		}

		return false;
	}


	/**
	 * Returns true if the parent node is one of HTML elements specified
	 * by tag names.
	 *
	 * @param string|array $tag_names A tag name or an array of tag names.
	 */
	public function has_parent( $tag_names ) {
		$tag_names = (array) $tag_names;

		$parent = reset( $this->stacked_elements );

		if ( false === $parent ) {
			return false;
		}

		return in_array( $parent, $tag_names );
	}


	/**
	 * Calculates the position of the next chunk based on the position and
	 * length of the current chunk.
	 *
	 * @param array $chunk An associative array of the current chunk.
	 * @return int The position of the next chunk.
	 */
	public static function calc_next_position( $chunk ) {
		return $chunk['position'] + strlen( $chunk['content'] );
	}


	/**
	 * Outputs a set of tabs to indent.
	 *
	 * @param int $level Indentation level.
	 * @return string A series of tabs.
	 */
	public static function indent( $level ) {
		$level = (int) $level;

		if ( 0 < $level ) {
			return str_repeat( "\t", $level );
		}

		return '';
	}


	/**
	 * Normalizes a start tag.
	 *
	 * @param string $tag A start tag or a tag name.
	 * @return array An array includes the normalized start tag and tag name.
	 */
	public static function normalize_start_tag( $tag ) {
		if ( preg_match( '/<(.+?)[\s\/>]/', $tag, $matches ) ) {
			$tag_name = strtolower( $matches[1] );
		} else {
			$tag_name = strtolower( $tag );
			$tag = sprintf( '<%s>', $tag_name );
		}

		if ( in_array( $tag_name, self::void_elements ) ) {
			// Normalize void element.
			$tag = preg_replace( '/\s*\/?>/', ' />', $tag );
		}

		return array( $tag, $tag_name );
	}


	/**
	 * Normalizes a paragraph of text.
	 *
	 * @param string $paragraph A paragraph of text.
	 * @param bool $auto_br Optional. If true, line breaks will be replaced
	 *             by a br element.
	 * @return string The normalized paragraph.
	 */
	public static function normalize_paragraph( $paragraph, $auto_br = false ) {
		if ( $auto_br ) {
			$paragraph = preg_replace( '/\s*\n\s*/', "<br />\n", $paragraph );
		}

		$paragraph = preg_replace( '/[ ]+/', " ", $paragraph );

		return $paragraph;
	}

}
css/styles-rtl.css000064400000000230151330370000010153 0ustar00.wpcf7-not-valid-tip {
	direction: rtl;
}

.use-floating-validation-tip .wpcf7-not-valid-tip {
	right: 1em;
}

.wpcf7-list-item {
	margin: 0 1em 0 0;
}
css/styles.css000064400000005453151330370000007370 0ustar00.wpcf7 .screen-reader-response {
	position: absolute;
	overflow: hidden;
	clip: rect(1px, 1px, 1px, 1px);
	clip-path: inset(50%);
	height: 1px;
	width: 1px;
	margin: -1px;
	padding: 0;
	border: 0;
	word-wrap: normal !important;
}

.wpcf7 form .wpcf7-response-output {
	margin: 2em 0.5em 1em;
	padding: 0.2em 1em;
	border: 2px solid #00a0d2; /* Blue */
}

.wpcf7 form.init .wpcf7-response-output,
.wpcf7 form.resetting .wpcf7-response-output,
.wpcf7 form.submitting .wpcf7-response-output {
	display: none;
}

.wpcf7 form.sent .wpcf7-response-output {
	border-color: #46b450; /* Green */
}

.wpcf7 form.failed .wpcf7-response-output,
.wpcf7 form.aborted .wpcf7-response-output {
	border-color: #dc3232; /* Red */
}

.wpcf7 form.spam .wpcf7-response-output {
	border-color: #f56e28; /* Orange */
}

.wpcf7 form.invalid .wpcf7-response-output,
.wpcf7 form.unaccepted .wpcf7-response-output,
.wpcf7 form.payment-required .wpcf7-response-output {
	border-color: #ffb900; /* Yellow */
}

.wpcf7-form-control-wrap {
	position: relative;
}

.wpcf7-not-valid-tip {
	color: #dc3232; /* Red */
	font-size: 1em;
	font-weight: normal;
	display: block;
}

.use-floating-validation-tip .wpcf7-not-valid-tip {
	position: relative;
	top: -2ex;
	left: 1em;
	z-index: 100;
	border: 1px solid #dc3232;
	background: #fff;
	padding: .2em .8em;
	width: 24em;
}

.wpcf7-list-item {
	display: inline-block;
	margin: 0 0 0 1em;
}

.wpcf7-list-item-label::before,
.wpcf7-list-item-label::after {
	content: " ";
}

.wpcf7-spinner {
	visibility: hidden;
	display: inline-block;
	background-color: #23282d; /* Dark Gray 800 */
	opacity: 0.75;
	width: 24px;
	height: 24px;
	border: none;
	border-radius: 100%;
	padding: 0;
	margin: 0 24px;
	position: relative;
}

form.submitting .wpcf7-spinner {
	visibility: visible;
}

.wpcf7-spinner::before {
	content: '';
	position: absolute;
	background-color: #fbfbfc; /* Light Gray 100 */
	top: 4px;
	left: 4px;
	width: 6px;
	height: 6px;
	border: none;
	border-radius: 100%;
	transform-origin: 8px 8px;
	animation-name: spin;
	animation-duration: 1000ms;
	animation-timing-function: linear;
	animation-iteration-count: infinite;
}

@media (prefers-reduced-motion: reduce) {
	.wpcf7-spinner::before {
		animation-name: blink;
		animation-duration: 2000ms;
	}
}

@keyframes spin {
	from {
		transform: rotate(0deg);
	}

	to {
		transform: rotate(360deg);
	}
}

@keyframes blink {
	from {
		opacity: 0;
	}

	50% {
		opacity: 1;
	}

	to {
		opacity: 0;
	}
}

.wpcf7 input[type="file"] {
	cursor: pointer;
}

.wpcf7 input[type="file"]:disabled {
	cursor: default;
}

.wpcf7 .wpcf7-submit:disabled {
	cursor: not-allowed;
}

.wpcf7 input[type="url"],
.wpcf7 input[type="email"],
.wpcf7 input[type="tel"] {
	direction: ltr;
}

.wpcf7-reflection > output {
	display: list-item;
	list-style: none;
}

.wpcf7-reflection > output[hidden] {
	display: none;
}
formatting.php000064400000030155151330370000007423 0ustar00<?php

/**
 * Replaces double line breaks with paragraph elements.
 *
 * @param string $input The text which has to be formatted.
 * @param bool $br Optional. If set, this will convert all remaining
 *             line breaks after paragraphing. Default true.
 * @return string Text which has been converted into correct paragraph tags.
 */
function wpcf7_autop( $input, $br = true ) {
	$placeholders = array();

	// Replace non-HTML embedded elements with placeholders.
	$input = preg_replace_callback(
		'/<(math|svg).*?<\/\1>/is',
		static function ( $matches ) use ( &$placeholders ) {
			$placeholder = sprintf(
				'<%1$s id="%2$s" />',
				WPCF7_HTMLFormatter::placeholder_inline,
				sha1( $matches[0] )
			);

			list( $placeholder ) =
				WPCF7_HTMLFormatter::normalize_start_tag( $placeholder );

			$placeholders[$placeholder] = $matches[0];

			return $placeholder;
		},
		$input
	);

	$formatter = new WPCF7_HTMLFormatter( array(
		'auto_br' => $br,
	) );

	$chunks = $formatter->separate_into_chunks( $input );

	$output = $formatter->format( $chunks );

	// Restore from placeholders.
	$output = str_replace(
		array_keys( $placeholders ),
		array_values( $placeholders ),
		$output
	);

	return $output;
}


/**
 * Newline preservation help function for wpcf7_autop().
 *
 * @deprecated 5.7 Unnecessary to use any more.
 *
 * @param array $matches preg_replace_callback() matches array.
 * @return string Text including newline placeholders.
 */
function wpcf7_autop_preserve_newline_callback( $matches ) {
	return str_replace( "\n", '<WPPreserveNewline />', $matches[0] );
}


/**
 * Sanitizes the query variables.
 *
 * @param string $text Query variable.
 * @return string Text sanitized.
 */
function wpcf7_sanitize_query_var( $text ) {
	$text = wp_unslash( $text );
	$text = wp_check_invalid_utf8( $text );

	if ( false !== strpos( $text, '<' ) ) {
		$text = wp_pre_kses_less_than( $text );
		$text = wp_strip_all_tags( $text );
	}

	$text = preg_replace( '/%[a-f0-9]{2}/i', '', $text );
	$text = preg_replace( '/ +/', ' ', $text );
	$text = trim( $text, ' ' );

	return $text;
}


/**
 * Strips quote characters surrounding the input.
 *
 * @param string $text Input text.
 * @return string Processed output.
 */
function wpcf7_strip_quote( $text ) {
	$text = trim( $text );

	if ( preg_match( '/^"(.*)"$/s', $text, $matches ) ) {
		$text = $matches[1];
	} elseif ( preg_match( "/^'(.*)'$/s", $text, $matches ) ) {
		$text = $matches[1];
	}

	return $text;
}


/**
 * Navigates through an array, object, or scalar, and
 * strips quote characters surrounding the each value.
 *
 * @param mixed $input The array or string to be processed.
 * @return mixed Processed value.
 */
function wpcf7_strip_quote_deep( $input ) {
	if ( is_string( $input ) ) {
		return wpcf7_strip_quote( $input );
	}

	if ( is_array( $input ) ) {
		$result = array();

		foreach ( $input as $key => $text ) {
			$result[$key] = wpcf7_strip_quote_deep( $text );
		}

		return $result;
	}
}


/**
 * Normalizes newline characters.
 *
 * @param string $text Input text.
 * @param string $to Optional. The newline character that is used in the output.
 * @return string Normalized text.
 */
function wpcf7_normalize_newline( $text, $to = "\n" ) {
	if ( ! is_string( $text ) ) {
		return $text;
	}

	$nls = array( "\r\n", "\r", "\n" );

	if ( ! in_array( $to, $nls ) ) {
		return $text;
	}

	return str_replace( $nls, $to, $text );
}


/**
 * Navigates through an array, object, or scalar, and
 * normalizes newline characters in the each value.
 *
 * @param mixed $input The array or string to be processed.
 * @param string $to Optional. The newline character that is used in the output.
 * @return mixed Processed value.
 */
function wpcf7_normalize_newline_deep( $input, $to = "\n" ) {
	if ( is_array( $input ) ) {
		$result = array();

		foreach ( $input as $key => $text ) {
			$result[$key] = wpcf7_normalize_newline_deep( $text, $to );
		}

		return $result;
	}

	return wpcf7_normalize_newline( $input, $to );
}


/**
 * Strips newline characters.
 *
 * @param string $text Input text.
 * @return string Processed one-line text.
 */
function wpcf7_strip_newline( $text ) {
	$text = (string) $text;
	$text = str_replace( array( "\r", "\n" ), '', $text );
	return trim( $text );
}


/**
 * Canonicalizes text.
 *
 * @param string $text Input text.
 * @param string|array|object $args Options.
 * @return string Canonicalized text.
 */
function wpcf7_canonicalize( $text, $args = '' ) {
	// for back-compat
	if ( is_string( $args ) and '' !== $args
	and false === strpos( $args, '=' ) ) {
		$args = array(
			'strto' => $args,
		);
	}

	$args = wp_parse_args( $args, array(
		'strto' => 'lower',
		'strip_separators' => false,
	) );

	static $charset = null;

	if ( ! isset( $charset ) ) {
		$charset = get_option( 'blog_charset' );

		$is_utf8 = in_array(
			$charset,
			array( 'utf8', 'utf-8', 'UTF8', 'UTF-8' )
		);

		if ( $is_utf8 ) {
			$charset = 'UTF-8';
		}
	}

	$text = html_entity_decode( $text, ENT_QUOTES | ENT_HTML5, $charset );

	if ( function_exists( 'mb_convert_kana' ) ) {
		$text = mb_convert_kana( $text, 'asKV', $charset );
	}

	if ( $args['strip_separators'] ) {
		$text = preg_replace( '/[\r\n\t ]+/', '', $text );
	} else {
		$text = preg_replace( '/[\r\n\t ]+/', ' ', $text );
	}

	if ( 'lower' == $args['strto'] ) {
		if ( function_exists( 'mb_strtolower' ) ) {
			$text = mb_strtolower( $text, $charset );
		} else {
			$text = strtolower( $text );
		}
	} elseif ( 'upper' == $args['strto'] ) {
		if ( function_exists( 'mb_strtoupper' ) ) {
			$text = mb_strtoupper( $text, $charset );
		} else {
			$text = strtoupper( $text );
		}
	}

	$text = trim( $text );
	return $text;
}


/**
 * Sanitizes Contact Form 7's form unit-tag.
 *
 * @param string $tag Unit-tag.
 * @return string Sanitized unit-tag.
 */
function wpcf7_sanitize_unit_tag( $tag ) {
	$tag = preg_replace( '/[^A-Za-z0-9_-]/', '', $tag );
	return $tag;
}


/**
 * Converts a file name to one that is not executable as a script.
 *
 * @param string $filename File name.
 * @return string Converted file name.
 */
function wpcf7_antiscript_file_name( $filename ) {
	$filename = wp_basename( $filename );

	$filename = preg_replace( '/[\r\n\t -]+/', '-', $filename );
	$filename = preg_replace( '/[\pC\pZ]+/iu', '', $filename );

	$parts = explode( '.', $filename );

	if ( count( $parts ) < 2 ) {
		return $filename;
	}

	$script_pattern = '/^(php|phtml|pl|py|rb|cgi|asp|aspx)\d?$/i';

	$filename = array_shift( $parts );
	$extension = array_pop( $parts );

	foreach ( (array) $parts as $part ) {
		if ( preg_match( $script_pattern, $part ) ) {
			$filename .= '.' . $part . '_';
		} else {
			$filename .= '.' . $part;
		}
	}

	if ( preg_match( $script_pattern, $extension ) ) {
		$filename .= '.' . $extension . '_.txt';
	} else {
		$filename .= '.' . $extension;
	}

	return $filename;
}


/**
 * Masks a password with asterisks (*).
 *
 * @param int $right Length of right-hand unmasked text. Default 0.
 * @param int $left Length of left-hand unmasked text. Default 0.
 * @return string Text of masked password.
 */
function wpcf7_mask_password( $text, $right = 0, $left = 0 ) {
	$length = strlen( $text );

	$right = absint( $right );
	$left = absint( $left );

	if ( $length < $right + $left ) {
		$right = $left = 0;
	}

	if ( $length <= 48 ) {
		$masked = str_repeat( '*', $length - ( $right + $left ) );
	} elseif ( $right + $left < 48 ) {
		$masked = str_repeat( '*', 48 - ( $right + $left ) );
	} else {
		$masked = '****';
	}

	$left_unmasked = $left ? substr( $text, 0, $left ) : '';
	$right_unmasked = $right ? substr( $text, -1 * $right ) : '';

	$text = $left_unmasked . $masked . $right_unmasked;

	return $text;
}


/**
 * Returns an array of allowed HTML tags and attributes for a given context.
 *
 * @param string $context Context used to decide allowed tags and attributes.
 * @return array Array of allowed HTML tags and their allowed attributes.
 */
function wpcf7_kses_allowed_html( $context = 'form' ) {
	static $allowed_tags = array();

	if ( isset( $allowed_tags[$context] ) ) {
		return apply_filters(
			'wpcf7_kses_allowed_html',
			$allowed_tags[$context],
			$context
		);
	}

	$allowed_tags[$context] = wp_kses_allowed_html( 'post' );

	if ( 'form' === $context ) {
		$additional_tags_for_form = array(
			'button' => array(
				'disabled' => true,
				'name' => true,
				'type' => true,
				'value' => true,
			),
			'datalist' => array(),
			'fieldset' => array(
				'disabled' => true,
				'name' => true,
			),
			'input' => array(
				'accept' => true,
				'alt' => true,
				'capture' => true,
				'checked' => true,
				'disabled' => true,
				'list' => true,
				'max' => true,
				'maxlength' => true,
				'min' => true,
				'minlength' => true,
				'multiple' => true,
				'name' => true,
				'placeholder' => true,
				'readonly' => true,
				'size' => true,
				'step' => true,
				'type' => true,
				'value' => true,
			),
			'label' => array(
				'for' => true,
			),
			'legend' => array(),
			'meter' => array(
				'value' => true,
				'min' => true,
				'max' => true,
				'low' => true,
				'high' => true,
				'optimum' => true,
			),
			'optgroup' => array(
				'disabled' => true,
				'label' => true,
			),
			'option' => array(
				'disabled' => true,
				'label' => true,
				'selected' => true,
				'value' => true,
			),
			'output' => array(
				'for' => true,
				'name' => true,
			),
			'progress' => array(
				'max' => true,
				'value' => true,
			),
			'select' => array(
				'disabled' => true,
				'multiple' => true,
				'name' => true,
				'size' => true,
			),
			'textarea' => array(
				'cols' => true,
				'disabled' => true,
				'maxlength' => true,
				'minlength' => true,
				'name' => true,
				'placeholder' => true,
				'readonly' => true,
				'rows' => true,
				'spellcheck' => true,
				'wrap' => true,
			),
		);

		$additional_tags_for_form = array_map(
			static function ( $elm ) {
				$global_attributes = array(
					'aria-atomic' => true,
					'aria-checked' => true,
					'aria-describedby' => true,
					'aria-details' => true,
					'aria-disabled' => true,
					'aria-hidden' => true,
					'aria-invalid' => true,
					'aria-label' => true,
					'aria-labelledby' => true,
					'aria-live' => true,
					'aria-relevant' => true,
					'aria-required' => true,
					'aria-selected' => true,
					'class' => true,
					'data-*' => true,
					'id' => true,
					'inputmode' => true,
					'role' => true,
					'style' => true,
					'tabindex' => true,
					'title' => true,
				);

				return array_merge( $global_attributes, (array) $elm );
			},
			$additional_tags_for_form
		);

		$allowed_tags[$context] = array_merge(
			$allowed_tags[$context],
			$additional_tags_for_form
		);
	}

	return apply_filters(
		'wpcf7_kses_allowed_html',
		$allowed_tags[$context],
		$context
	);
}


/**
 * Sanitizes content for allowed HTML tags for the specified context.
 *
 * @param string $input Content to filter.
 * @param string $context Context used to decide allowed tags and attributes.
 * @return string Filtered text with allowed HTML tags and attributes intact.
 */
function wpcf7_kses( $input, $context = 'form' ) {
	$output = wp_kses(
		$input,
		wpcf7_kses_allowed_html( $context )
	);

	return $output;
}


/**
 * Returns a formatted string of HTML attributes.
 *
 * @param array $atts Associative array of attribute name and value pairs.
 * @return string Formatted HTML attributes.
 */
function wpcf7_format_atts( $atts ) {
	$atts_filtered = array();

	foreach ( $atts as $name => $value ) {
		$name = strtolower( trim( $name ) );

		if ( ! preg_match( '/^[a-z_:][a-z_:.0-9-]*$/', $name ) ) {
			continue;
		}

		static $boolean_attributes = array(
			'checked', 'disabled', 'multiple', 'readonly', 'required', 'selected',
		);

		if ( in_array( $name, $boolean_attributes ) and '' === $value ) {
			$value = false;
		}

		if ( is_numeric( $value ) ) {
			$value = (string) $value;
		}

		if ( null === $value or false === $value ) {
			unset( $atts_filtered[$name] );
		} elseif ( true === $value ) {
			$atts_filtered[$name] = $name; // boolean attribute
		} elseif ( is_string( $value ) ) {
			$atts_filtered[$name] = trim( $value );
		}
	}

	$output = '';

	foreach ( $atts_filtered as $name => $value ) {
		$output .= sprintf( ' %1$s="%2$s"', $name, esc_attr( $value ) );
	}

	return trim( $output );
}
submission.php000064400000053060151330370000007444 0ustar00<?php

/**
 * Class representing contact form submission.
 */
class WPCF7_Submission {

	use WPCF7_PocketHolder;

	private static $instance;

	private $contact_form;
	private $status = 'init';
	private $posted_data = array();
	private $posted_data_hash = null;
	private $skip_spam_check = false;
	private $uploaded_files = array();
	private $extra_attachments = array();
	private $skip_mail = false;
	private $response = '';
	private $invalid_fields = array();
	private $meta = array();
	private $consent = array();
	private $spam_log = array();
	private $result_props = array();


	/**
	 * Returns the singleton instance of this class.
	 */
	public static function get_instance( $contact_form = null, $args = '' ) {
		if ( $contact_form instanceof WPCF7_ContactForm ) {
			if ( empty( self::$instance ) ) {
				self::$instance = new self( $contact_form, $args );
				self::$instance->proceed();
				return self::$instance;
			} else {
				return null;
			}
		} else {
			if ( empty( self::$instance ) ) {
				return null;
			} else {
				return self::$instance;
			}
		}
	}


	/**
	 * Returns true if this submission is created via WP REST API.
	 */
	public static function is_restful() {
		return defined( 'REST_REQUEST' ) && REST_REQUEST;
	}


	/**
	 * Constructor.
	 */
	private function __construct( WPCF7_ContactForm $contact_form, $args = '' ) {
		$args = wp_parse_args( $args, array(
			'skip_mail' => false,
		) );

		$this->contact_form = $contact_form;
		$this->skip_mail = (bool) $args['skip_mail'];
	}


	/**
	 * The main logic of submission.
	 */
	private function proceed() {

		$callback = function () {
			$contact_form = $this->contact_form;

			$this->setup_meta_data();
			$this->setup_posted_data();

			if ( $this->is( 'init' ) and ! $this->validate() ) {
				$this->set_status( 'validation_failed' );
				$this->set_response( $contact_form->message( 'validation_error' ) );
			}

			if ( $this->is( 'init' ) and ! $this->accepted() ) {
				$this->set_status( 'acceptance_missing' );
				$this->set_response( $contact_form->message( 'accept_terms' ) );
			}

			if ( $this->is( 'init' ) and $this->spam() ) {
				$this->set_status( 'spam' );
				$this->set_response( $contact_form->message( 'spam' ) );
			}

			if ( $this->is( 'init' ) and ! $this->unship_uploaded_files() ) {
				$this->set_status( 'validation_failed' );
				$this->set_response( $contact_form->message( 'validation_error' ) );
			}

			if ( $this->is( 'init' ) ) {
				$abort = ! $this->before_send_mail();

				if ( $abort ) {
					if ( $this->is( 'init' ) ) {
						$this->set_status( 'aborted' );
					}

					if ( '' === $this->get_response() ) {
						$this->set_response( $contact_form->filter_message(
							__( "Sending mail has been aborted.", 'contact-form-7' ) )
						);
					}
				} elseif ( $this->mail() ) {
					$this->set_status( 'mail_sent' );
					$this->set_response( $contact_form->message( 'mail_sent_ok' ) );

					do_action( 'wpcf7_mail_sent', $contact_form );
				} else {
					$this->set_status( 'mail_failed' );
					$this->set_response( $contact_form->message( 'mail_sent_ng' ) );

					do_action( 'wpcf7_mail_failed', $contact_form );
				}
			}

			$this->remove_uploaded_files();
		};

		wpcf7_switch_locale( $this->contact_form->locale(), $callback );
	}


	/**
	 * Returns the current status property.
	 */
	public function get_status() {
		return $this->status;
	}


	/**
	 * Sets the status property.
	 *
	 * @param string $status The status.
	 */
	public function set_status( $status ) {
		if ( preg_match( '/^[a-z][0-9a-z_]+$/', $status ) ) {
			$this->status = $status;
			return true;
		}

		return false;
	}


	/**
	 * Returns true if the specified status is identical to the current
	 * status property.
	 *
	 * @param string $status The status to compare.
	 */
	public function is( $status ) {
		return $this->status === $status;
	}


	/**
	 * Returns an associative array of submission result properties.
	 *
	 * @return array Submission result properties.
	 */
	public function get_result() {
		$result = array_merge( $this->result_props, array(
			'status' => $this->get_status(),
			'message' => $this->get_response(),
		) );

		if ( $this->is( 'validation_failed' ) ) {
			$result['invalid_fields'] = $this->get_invalid_fields();
		}

		switch ( $this->get_status() ) {
			case 'init':
			case 'validation_failed':
			case 'acceptance_missing':
			case 'spam':
				$result['posted_data_hash'] = '';
				break;
			default:
				$result['posted_data_hash'] = $this->get_posted_data_hash();
				break;
		}

		$result = apply_filters( 'wpcf7_submission_result', $result, $this );

		return $result;
	}


	/**
	 * Adds items to the array of submission result properties.
	 *
	 * @param string|array|object $args Value to add to result properties.
	 * @return array Added result properties.
	 */
	public function add_result_props( $args = '' ) {
		$args = wp_parse_args( $args, array() );

		$this->result_props = array_merge( $this->result_props, $args );

		return $args;
	}


	/**
	 * Retrieves the response property.
	 *
	 * @return string The current response property value.
	 */
	public function get_response() {
		return $this->response;
	}


	/**
	 * Sets the response property.
	 *
	 * @param string $response New response property value.
	 */
	public function set_response( $response ) {
		$this->response = $response;
		return true;
	}


	/**
	 * Retrieves the contact form property.
	 *
	 * @return WPCF7_ContactForm A contact form object.
	 */
	public function get_contact_form() {
		return $this->contact_form;
	}


	/**
	 * Search an invalid field by field name.
	 *
	 * @param string $name The field name.
	 * @return array|bool An associative array of validation error
	 *                    or false when no invalid field.
	 */
	public function get_invalid_field( $name ) {
		if ( isset( $this->invalid_fields[$name] ) ) {
			return $this->invalid_fields[$name];
		} else {
			return false;
		}
	}


	/**
	 * Retrieves all invalid fields.
	 *
	 * @return array Invalid fields.
	 */
	public function get_invalid_fields() {
		return $this->invalid_fields;
	}


	/**
	 * Retrieves meta information.
	 *
	 * @param string $name Name of the meta information.
	 * @return string|null The meta information of the given name if it exists,
	 *                     null otherwise.
	 */
	public function get_meta( $name ) {
		if ( isset( $this->meta[$name] ) ) {
			return $this->meta[$name];
		}
	}


	/**
	 * Collects meta information about this submission.
	 */
	private function setup_meta_data() {
		$timestamp = time();

		$remote_ip = $this->get_remote_ip_addr();

		$remote_port = isset( $_SERVER['REMOTE_PORT'] )
			? (int) $_SERVER['REMOTE_PORT'] : '';

		$user_agent = isset( $_SERVER['HTTP_USER_AGENT'] )
			? substr( $_SERVER['HTTP_USER_AGENT'], 0, 254 ) : '';

		$url = $this->get_request_url();

		$unit_tag = isset( $_POST['_wpcf7_unit_tag'] )
			? wpcf7_sanitize_unit_tag( $_POST['_wpcf7_unit_tag'] ) : '';

		$container_post_id = isset( $_POST['_wpcf7_container_post'] )
			? (int) $_POST['_wpcf7_container_post'] : 0;

		$current_user_id = get_current_user_id();

		$do_not_store = $this->contact_form->is_true( 'do_not_store' );

		$this->meta = array(
			'timestamp' => $timestamp,
			'remote_ip' => $remote_ip,
			'remote_port' => $remote_port,
			'user_agent' => $user_agent,
			'url' => $url,
			'unit_tag' => $unit_tag,
			'container_post_id' => $container_post_id,
			'current_user_id' => $current_user_id,
			'do_not_store' => $do_not_store,
		);

		return $this->meta;
	}


	/**
	 * Retrieves user input data through this submission.
	 *
	 * @param string $name Optional field name.
	 * @return string|array|null The user input of the field, or array of all
	 *                           fields values if no field name specified.
	 */
	public function get_posted_data( $name = '' ) {
		if ( ! empty( $name ) ) {
			if ( isset( $this->posted_data[$name] ) ) {
				return $this->posted_data[$name];
			} else {
				return null;
			}
		}

		return $this->posted_data;
	}


	/**
	 * Retrieves a user input string value through the specified field.
	 *
	 * @param string $name Field name.
	 * @return string The user input. If the input is an array,
	 *                the first item in the array.
	 */
	public function get_posted_string( $name ) {
		$data = $this->get_posted_data( $name );
		$data = wpcf7_array_flatten( $data );

		if ( empty( $data ) ) {
			return '';
		}

		// Returns the first array item.
		return trim( reset( $data ) );
	}


	/**
	 * Constructs posted data property based on user input values.
	 */
	private function setup_posted_data() {
		$posted_data = array_filter( (array) $_POST, static function ( $key ) {
			return '_' !== substr( $key, 0, 1 );
		}, ARRAY_FILTER_USE_KEY );

		$posted_data = wp_unslash( $posted_data );
		$posted_data = $this->sanitize_posted_data( $posted_data );

		$tags = $this->contact_form->scan_form_tags();

		foreach ( (array) $tags as $tag ) {
			if ( empty( $tag->name ) ) {
				continue;
			}

			$type = $tag->type;
			$name = $tag->name;
			$pipes = $tag->pipes;

			$value_orig = $value = '';

			if ( isset( $posted_data[$name] ) ) {
				$value_orig = $value = $posted_data[$name];
			}

			if ( WPCF7_USE_PIPE
			and $pipes instanceof WPCF7_Pipes
			and ! $pipes->zero() ) {
				if ( is_array( $value_orig ) ) {
					$value = array();

					foreach ( $value_orig as $v ) {
						$value[] = $pipes->do_pipe( $v );
					}
				} else {
					$value = $pipes->do_pipe( $value_orig );
				}
			}

			if ( wpcf7_form_tag_supports( $type, 'selectable-values' ) ) {
				$value = (array) $value;

				if ( $tag->has_option( 'free_text' )
				and isset( $posted_data[$name . '_free_text'] ) ) {
					$last_val = array_pop( $value );

					list( $tied_item ) = array_slice(
						WPCF7_USE_PIPE ? $tag->pipes->collect_afters() : $tag->values,
						-1, 1
					);

					list( $last_val, $tied_item ) = array_map(
						static function ( $item ) {
							return wpcf7_canonicalize( $item, array(
								'strto' => 'as-is',
							) );
						},
						array( $last_val, $tied_item )
					);

					if ( $last_val === $tied_item ) {
						$value[] = sprintf( '%s %s',
							$last_val,
							$posted_data[$name . '_free_text']
						);
					} else {
						$value[] = $last_val;
					}

					unset( $posted_data[$name . '_free_text'] );
				}
			}

			$value = apply_filters( "wpcf7_posted_data_{$type}", $value,
				$value_orig, $tag
			);

			$posted_data[$name] = $value;

			if ( $tag->has_option( 'consent_for:storage' )
			and empty( $posted_data[$name] ) ) {
				$this->meta['do_not_store'] = true;
			}
		}

		$this->posted_data = apply_filters( 'wpcf7_posted_data', $posted_data );

		$this->posted_data_hash = $this->create_posted_data_hash();

		return $this->posted_data;
	}


	/**
	 * Sanitizes user input data.
	 */
	private function sanitize_posted_data( $value ) {
		if ( is_array( $value ) ) {
			$value = array_map( array( $this, 'sanitize_posted_data' ), $value );
		} elseif ( is_string( $value ) ) {
			$value = wp_check_invalid_utf8( $value );
			$value = wp_kses_no_null( $value );
		}

		return $value;
	}


	/**
	 * Returns the time-dependent variable for hash creation.
	 *
	 * @return float Float value rounded up to the next highest integer.
	 */
	private function posted_data_hash_tick() {
		return ceil( time() / ( HOUR_IN_SECONDS / 2 ) );
	}


	/**
	 * Creates a hash string based on posted data, the remote IP address,
	 * contact form location, and window of time.
	 *
	 * @param string $tick Optional. If not specified, result of
	 *               posted_data_hash_tick() will be used.
	 * @return string The hash.
	 */
	private function create_posted_data_hash( $tick = '' ) {
		if ( '' === $tick ) {
			$tick = $this->posted_data_hash_tick();
		}

		$hash = wp_hash(
			wpcf7_flat_join( array_merge(
				array(
					$tick,
					$this->get_meta( 'remote_ip' ),
					$this->get_meta( 'unit_tag' ),
				),
				$this->posted_data
			) ),
			'wpcf7_submission'
		);

		return $hash;
	}


	/**
	 * Returns the hash string created for this submission.
	 *
	 * @return string The current hash for the submission.
	 */
	public function get_posted_data_hash() {
		return $this->posted_data_hash;
	}


	/**
	 * Verifies that the given string is equivalent to the posted data hash.
	 *
	 * @param string $hash Optional. This value will be compared to the
	 *               current posted data hash for the submission. If not
	 *               specified, the value of $_POST['_wpcf7_posted_data_hash']
	 *               will be used.
	 * @return int|bool 1 if $hash is created 0-30 minutes ago,
	 *                  2 if $hash is created 30-60 minutes ago,
	 *                  false if $hash is invalid.
	 */
	public function verify_posted_data_hash( $hash = '' ) {
		if ( '' === $hash and ! empty( $_POST['_wpcf7_posted_data_hash'] ) ) {
			$hash = trim( $_POST['_wpcf7_posted_data_hash'] );
		}

		if ( '' === $hash ) {
			return false;
		}

		$tick = $this->posted_data_hash_tick();

		// Hash created 0-30 minutes ago.
		$expected_1 = $this->create_posted_data_hash( $tick );

		if ( hash_equals( $expected_1, $hash ) ) {
			return 1;
		}

		// Hash created 30-60 minutes ago.
		$expected_2 = $this->create_posted_data_hash( $tick - 1 );

		if ( hash_equals( $expected_2, $hash ) ) {
			return 2;
		}

		return false;
	}


	/**
	 * Retrieves the remote IP address of this submission.
	 */
	private function get_remote_ip_addr() {
		$ip_addr = '';

		if ( isset( $_SERVER['REMOTE_ADDR'] )
		and WP_Http::is_ip_address( $_SERVER['REMOTE_ADDR'] ) ) {
			$ip_addr = $_SERVER['REMOTE_ADDR'];
		}

		return apply_filters( 'wpcf7_remote_ip_addr', $ip_addr );
	}


	/**
	 * Retrieves the request URL of this submission.
	 */
	private function get_request_url() {
		$home_url = untrailingslashit( home_url() );

		if ( self::is_restful() ) {
			$referer = isset( $_SERVER['HTTP_REFERER'] )
				? trim( $_SERVER['HTTP_REFERER'] ) : '';

			if ( $referer
			and 0 === strpos( $referer, $home_url ) ) {
				return sanitize_url( $referer );
			}
		}

		$url = preg_replace( '%(?<!:|/)/.*$%', '', $home_url )
			. wpcf7_get_request_uri();

		return $url;
	}


	/**
	 * Runs user input validation.
	 *
	 * @return bool True if no invalid field is found.
	 */
	private function validate() {
		if ( $this->invalid_fields ) {
			return false;
		}

		$result = new WPCF7_Validation();

		$this->contact_form->validate_schema(
			array(
				'text' => true,
				'file' => false,
				'field' => array(),
			),
			$result
		);

		$tags = $this->contact_form->scan_form_tags( array(
		  'feature' => '! file-uploading',
		) );

		foreach ( $tags as $tag ) {
			$type = $tag->type;
			$result = apply_filters( "wpcf7_validate_{$type}", $result, $tag );
		}

		$result = apply_filters( 'wpcf7_validate', $result, $tags );

		$this->invalid_fields = $result->get_invalid_fields();

		return $result->is_valid();
	}


	/**
	 * Returns true if user consent is obtained.
	 */
	private function accepted() {
		return apply_filters( 'wpcf7_acceptance', true, $this );
	}


	/**
	 * Adds user consent data to this submission.
	 *
	 * @param string $name Field name.
	 * @param string $conditions Conditions of consent.
	 */
	public function add_consent( $name, $conditions ) {
		$this->consent[$name] = $conditions;
		return true;
	}


	/**
	 * Collects user consent data.
	 *
	 * @return array User consent data.
	 */
	public function collect_consent() {
		return (array) $this->consent;
	}


	/**
	 * Executes spam protections.
	 *
	 * @return bool True if spam captured.
	 */
	private function spam() {
		$spam = false;

		$skip_spam_check = apply_filters( 'wpcf7_skip_spam_check',
			$this->skip_spam_check,
			$this
		);

		if ( $skip_spam_check ) {
			return $spam;
		}

		if ( $this->contact_form->is_true( 'subscribers_only' )
		and current_user_can( 'wpcf7_submit', $this->contact_form->id() ) ) {
			return $spam;
		}

		$user_agent = (string) $this->get_meta( 'user_agent' );

		if ( strlen( $user_agent ) < 2 ) {
			$spam = true;

			$this->add_spam_log( array(
				'agent' => 'wpcf7',
				'reason' => __( "User-Agent string is unnaturally short.", 'contact-form-7' ),
			) );
		}

		if ( ! $this->verify_nonce() ) {
			$spam = true;

			$this->add_spam_log( array(
				'agent' => 'wpcf7',
				'reason' => __( "Submitted nonce is invalid.", 'contact-form-7' ),
			) );
		}

		return apply_filters( 'wpcf7_spam', $spam, $this );
	}


	/**
	 * Adds a spam log.
	 *
	 * @link https://contactform7.com/2019/05/31/why-is-this-message-marked-spam/
	 */
	public function add_spam_log( $args = '' ) {
		$args = wp_parse_args( $args, array(
			'agent' => '',
			'reason' => '',
		) );

		$this->spam_log[] = $args;
	}


	/**
	 * Retrieves the spam logging data.
	 *
	 * @return array Spam logging data.
	 */
	public function get_spam_log() {
		return $this->spam_log;
	}


	/**
	 * Verifies that a correct security nonce was used.
	 */
	private function verify_nonce() {
		if ( ! $this->contact_form->nonce_is_active() or ! is_user_logged_in() ) {
			return true;
		}

		$nonce = isset( $_POST['_wpnonce'] ) ? $_POST['_wpnonce'] : '';

		return wpcf7_verify_nonce( $nonce );
	}


	/**
	 * Function called just before sending email.
	 */
	private function before_send_mail() {
		$abort = false;

		do_action_ref_array( 'wpcf7_before_send_mail', array(
			$this->contact_form,
			&$abort,
			$this,
		) );

		return ! $abort;
	}


	/**
	 * Sends emails based on user input values and contact form email templates.
	 */
	private function mail() {
		$contact_form = $this->contact_form;

		$skip_mail = apply_filters( 'wpcf7_skip_mail',
			$this->skip_mail, $contact_form
		);

		if ( $skip_mail ) {
			return true;
		}

		$result = WPCF7_Mail::send( $contact_form->prop( 'mail' ), 'mail' );

		if ( $result ) {
			$additional_mail = array();

			if ( $mail_2 = $contact_form->prop( 'mail_2' )
			and $mail_2['active'] ) {
				$additional_mail['mail_2'] = $mail_2;
			}

			$additional_mail = apply_filters( 'wpcf7_additional_mail',
				$additional_mail, $contact_form
			);

			foreach ( $additional_mail as $name => $template ) {
				WPCF7_Mail::send( $template, $name );
			}

			return true;
		}

		return false;
	}


	/**
	 * Retrieves files uploaded through this submission.
	 */
	public function uploaded_files() {
		return $this->uploaded_files;
	}


	/**
	 * Adds a file to the uploaded files array.
	 *
	 * @param string $name Field name.
	 * @param string|array $file_path File path or array of file paths.
	 */
	private function add_uploaded_file( $name, $file_path ) {
		if ( ! wpcf7_is_name( $name ) ) {
			return false;
		}

		$paths = (array) $file_path;
		$uploaded_files = array();
		$hash_strings = array();

		foreach ( $paths as $path ) {
			if ( @is_file( $path ) and @is_readable( $path ) ) {
				$uploaded_files[] = $path;
				$hash_strings[] = md5_file( $path );
			}
		}

		$this->uploaded_files[$name] = $uploaded_files;

		if ( empty( $this->posted_data[$name] ) ) {
			$this->posted_data[$name] = implode( ' ', $hash_strings );
		}
	}


	/**
	 * Removes uploaded files.
	 */
	private function remove_uploaded_files() {
		foreach ( (array) $this->uploaded_files as $file_path ) {
			$paths = (array) $file_path;

			foreach ( $paths as $path ) {
				wpcf7_rmdir_p( $path );

				if ( $dir = dirname( $path )
				and false !== ( $files = scandir( $dir ) )
				and ! array_diff( $files, array( '.', '..' ) ) ) {
					// remove parent dir if it's empty.
					rmdir( $dir );
				}
			}
		}
	}


	/**
	 * Moves uploaded files to the tmp directory and validates them.
	 *
	 * @return bool True if no invalid file is found.
	 */
	private function unship_uploaded_files() {
		$result = new WPCF7_Validation();

		$tags = $this->contact_form->scan_form_tags( array(
			'feature' => 'file-uploading',
		) );

		foreach ( $tags as $tag ) {
			if ( empty( $_FILES[$tag->name] ) ) {
				continue;
			}

			$file = $_FILES[$tag->name];

			$args = array(
				'tag' => $tag,
				'name' => $tag->name,
				'required' => $tag->is_required(),
				'filetypes' => $tag->get_option( 'filetypes' ),
				'limit' => $tag->get_limit_option(),
				'schema' => $this->contact_form->get_schema(),
			);

			$new_files = wpcf7_unship_uploaded_file( $file, $args );

			if ( is_wp_error( $new_files ) ) {
				$result->invalidate( $tag, $new_files );
			} else {
				$this->add_uploaded_file( $tag->name, $new_files );
			}

			$result = apply_filters(
				"wpcf7_validate_{$tag->type}",
				$result, $tag,
				array(
					'uploaded_files' => $new_files,
				)
			);
		}

		$this->invalid_fields = $result->get_invalid_fields();

		return $result->is_valid();
	}


	/**
	 * Adds extra email attachment files that are independent from form fields.
	 *
	 * @param string|array $file_path A file path or an array of file paths.
	 * @param string $template Optional. The name of the template to which
	 *                         the files are attached.
	 * @return bool True if it succeeds to attach a file at least,
	 *              or false otherwise.
	 */
	public function add_extra_attachments( $file_path, $template = 'mail' ) {
		if ( ! did_action( 'wpcf7_before_send_mail' ) ) {
			return false;
		}

		$extra_attachments = array();

		foreach ( (array) $file_path as $path ) {
			$path = path_join( WP_CONTENT_DIR, $path );

			if ( file_exists( $path ) ) {
				$extra_attachments[] = $path;
			}
		}

		if ( empty( $extra_attachments ) ) {
			return false;
		}

		if ( ! isset( $this->extra_attachments[$template] ) ) {
			$this->extra_attachments[$template] = array();
		}

		$this->extra_attachments[$template] = array_merge(
			$this->extra_attachments[$template],
			$extra_attachments
		);

		return true;
	}


	/**
	 * Returns extra email attachment files.
	 *
	 * @param string $template An email template name.
	 * @return array Array of file paths.
	 */
	public function extra_attachments( $template ) {
		if ( isset( $this->extra_attachments[$template] ) ) {
			return (array) $this->extra_attachments[$template];
		}

		return array();
	}

}
config-validator.php000064400000061156151330370000010506 0ustar00<?php

/**
 * Configuration validator.
 *
 * @link https://contactform7.com/configuration-errors/
 */
class WPCF7_ConfigValidator {

	/**
	 * The plugin version in which important updates happened last time.
	 */
	const last_important_update = '5.6.1';

	const error = 100;
	const error_maybe_empty = 101;
	const error_invalid_mailbox_syntax = 102;
	const error_email_not_in_site_domain = 103;
	const error_html_in_message = 104;
	const error_multiple_controls_in_label = 105;
	const error_file_not_found = 106;
	const error_unavailable_names = 107;
	const error_invalid_mail_header = 108;
	const error_deprecated_settings = 109;
	const error_file_not_in_content_dir = 110;
	const error_unavailable_html_elements = 111;
	const error_attachments_overweight = 112;
	const error_dots_in_names = 113;
	const error_colons_in_names = 114;
	const error_upload_filesize_overlimit = 115;


	/**
	 * Returns a URL linking to the documentation page for the error type.
	 */
	public static function get_doc_link( $error_code = '' ) {
		$url = __( 'https://contactform7.com/configuration-errors/',
			'contact-form-7'
		);

		if ( '' !== $error_code ) {
			$error_code = strtr( $error_code, '_', '-' );

			$url = sprintf( '%s/%s', untrailingslashit( $url ), $error_code );
		}

		return esc_url( $url );
	}


	private $contact_form;
	private $errors = array();

	public function __construct( WPCF7_ContactForm $contact_form ) {
		$this->contact_form = $contact_form;
	}


	/**
	 * Returns the contact form object that is tied to this validator.
	 */
	public function contact_form() {
		return $this->contact_form;
	}


	/**
	 * Returns true if no error has been detected.
	 */
	public function is_valid() {
		return ! $this->count_errors();
	}


	/**
	 * Counts detected errors.
	 */
	public function count_errors( $args = '' ) {
		$args = wp_parse_args( $args, array(
			'section' => '',
			'code' => '',
		) );

		$count = 0;

		foreach ( $this->errors as $key => $errors ) {
			if ( preg_match( '/^mail_[0-9]+\.(.*)$/', $key, $matches ) ) {
				$key = sprintf( 'mail.%s', $matches[1] );
			}

			if ( $args['section']
			and $key != $args['section']
			and preg_replace( '/\..*$/', '', $key, 1 ) != $args['section'] ) {
				continue;
			}

			foreach ( $errors as $error ) {
				if ( empty( $error ) ) {
					continue;
				}

				if ( $args['code'] and $error['code'] != $args['code'] ) {
					continue;
				}

				$count += 1;
			}
		}

		return $count;
	}


	/**
	 * Collects messages for detected errors.
	 */
	public function collect_error_messages() {
		$error_messages = array();

		foreach ( $this->errors as $section => $errors ) {
			$error_messages[$section] = array();

			foreach ( $errors as $error ) {
				if ( empty( $error['args']['message'] ) ) {
					$message = $this->get_default_message( $error['code'] );
				} elseif ( empty( $error['args']['params'] ) ) {
					$message = $error['args']['message'];
				} else {
					$message = $this->build_message(
						$error['args']['message'],
						$error['args']['params'] );
				}

				$link = '';

				if ( ! empty( $error['args']['link'] ) ) {
					$link = $error['args']['link'];
				}

				$error_messages[$section][] = array(
					'message' => $message,
					'link' => esc_url( $link ),
				);
			}
		}

		return $error_messages;
	}


	/**
	 * Builds an error message by replacing placeholders.
	 */
	public function build_message( $message, $params = '' ) {
		$params = wp_parse_args( $params, array() );

		foreach ( $params as $key => $val ) {
			if ( ! preg_match( '/^[0-9A-Za-z_]+$/', $key ) ) { // invalid key
				continue;
			}

			$placeholder = '%' . $key . '%';

			if ( false !== stripos( $message, $placeholder ) ) {
				$message = str_ireplace( $placeholder, $val, $message );
			}
		}

		return $message;
	}


	/**
	 * Returns a default message that is used when the message for the error
	 * is not specified.
	 */
	public function get_default_message( $code ) {
		switch ( $code ) {
			case self::error_maybe_empty:
				return __( "There is a possible empty field.", 'contact-form-7' );
			case self::error_invalid_mailbox_syntax:
				return __( "Invalid mailbox syntax is used.", 'contact-form-7' );
			case self::error_email_not_in_site_domain:
				return __( "Sender email address does not belong to the site domain.", 'contact-form-7' );
			case self::error_html_in_message:
				return __( "HTML tags are used in a message.", 'contact-form-7' );
			case self::error_multiple_controls_in_label:
				return __( "Multiple form controls are in a single label element.", 'contact-form-7' );
			case self::error_invalid_mail_header:
				return __( "There are invalid mail header fields.", 'contact-form-7' );
			case self::error_deprecated_settings:
				return __( "Deprecated settings are used.", 'contact-form-7' );
			default:
				return '';
		}
	}


	/**
	 * Adds a validation error.
	 *
	 * @param string $section The section where the error detected.
	 * @param int $code The unique code of the error.
	 *            This must be one of the class constants.
	 * @param string|array $args Optional options for the error.
	 */
	public function add_error( $section, $code, $args = '' ) {
		$args = wp_parse_args( $args, array(
			'message' => '',
			'params' => array(),
		) );

		if ( ! isset( $this->errors[$section] ) ) {
			$this->errors[$section] = array();
		}

		$this->errors[$section][] = array( 'code' => $code, 'args' => $args );

		return true;
	}


	/**
	 * Removes an error.
	 */
	public function remove_error( $section, $code ) {
		if ( empty( $this->errors[$section] ) ) {
			return;
		}

		foreach ( (array) $this->errors[$section] as $key => $error ) {
			if ( isset( $error['code'] )
			and $error['code'] == $code ) {
				unset( $this->errors[$section][$key] );
			}
		}

		if ( empty( $this->errors[$section] ) ) {
			unset( $this->errors[$section] );
		}
	}


	/**
	 * The main validation runner.
	 *
	 * @return bool True if there is no error detected.
	 */
	public function validate() {
		$this->errors = array();

		$this->validate_form();
		$this->validate_mail( 'mail' );
		$this->validate_mail( 'mail_2' );
		$this->validate_messages();
		$this->validate_additional_settings();

		do_action( 'wpcf7_config_validator_validate', $this );

		return $this->is_valid();
	}


	/**
	 * Saves detected errors as a post meta data.
	 */
	public function save() {
		if ( $this->contact_form->initial() ) {
			return;
		}

		delete_post_meta( $this->contact_form->id(), '_config_errors' );

		if ( $this->errors ) {
			update_post_meta(
				$this->contact_form->id(), '_config_errors', $this->errors
			);
		}
	}


	/**
	 * Restore errors from the database.
	 */
	public function restore() {
		$config_errors = get_post_meta(
			$this->contact_form->id(), '_config_errors', true
		);

		foreach ( (array) $config_errors as $section => $errors ) {
			if ( empty( $errors ) ) {
				continue;
			}

			if ( ! is_array( $errors ) ) { // for back-compat
				$code = $errors;
				$this->add_error( $section, $code );
			} else {
				foreach ( (array) $errors as $error ) {
					if ( ! empty( $error['code'] ) ) {
						$code = $error['code'];
						$args = isset( $error['args'] ) ? $error['args'] : '';
						$this->add_error( $section, $code, $args );
					}
				}
			}
		}
	}


	/**
	 * Callback function for WPCF7_MailTaggedText. Replaces mail-tags with
	 * the most conservative inputs.
	 */
	public function replace_mail_tags_with_minimum_input( $matches ) {
		// allow [[foo]] syntax for escaping a tag
		if ( $matches[1] == '[' && $matches[4] == ']' ) {
			return substr( $matches[0], 1, -1 );
		}

		$tag = $matches[0];
		$tagname = $matches[2];
		$values = $matches[3];

		$mail_tag = new WPCF7_MailTag( $tag, $tagname, $values );
		$field_name = $mail_tag->field_name();

		$example_email = 'example@example.com';
		$example_text = 'example';
		$example_blank = '';

		$form_tags = $this->contact_form->scan_form_tags(
			array( 'name' => $field_name )
		);

		if ( $form_tags ) {
			$form_tag = new WPCF7_FormTag( $form_tags[0] );

			$is_required = ( $form_tag->is_required() || 'radio' == $form_tag->type );

			if ( ! $is_required ) {
				return $example_blank;
			}

			if ( wpcf7_form_tag_supports( $form_tag->type, 'selectable-values' ) ) {
				if ( $form_tag->pipes instanceof WPCF7_Pipes ) {
					if ( $mail_tag->get_option( 'do_not_heat' ) ) {
						$before_pipes = $form_tag->pipes->collect_befores();
						$last_item = array_pop( $before_pipes );
					} else {
						$after_pipes = $form_tag->pipes->collect_afters();
						$last_item = array_pop( $after_pipes );
					}
				} else {
					$last_item = array_pop( $form_tag->values );
				}

				if ( $last_item and wpcf7_is_mailbox_list( $last_item ) ) {
					return $example_email;
				} else {
					return $example_text;
				}
			}

			if ( 'email' == $form_tag->basetype ) {
				return $example_email;
			} else {
				return $example_text;
			}

		} else { // maybe special mail tag
			// for back-compat
			$field_name = preg_replace( '/^wpcf7\./', '_', $field_name );

			if ( '_site_admin_email' == $field_name ) {
				return get_bloginfo( 'admin_email', 'raw' );

			} elseif ( '_user_agent' == $field_name ) {
				return $example_text;

			} elseif ( '_user_email' == $field_name ) {
				return $this->contact_form->is_true( 'subscribers_only' )
					? $example_email
					: $example_blank;

			} elseif ( '_user_' == substr( $field_name, 0, 6 ) ) {
				return $this->contact_form->is_true( 'subscribers_only' )
					? $example_text
					: $example_blank;

			} elseif ( '_' == substr( $field_name, 0, 1 ) ) {
				return '_email' == substr( $field_name, -6 )
					? $example_email
					: $example_text;

			}
		}

		return $tag;
	}


	/**
	 * Runs error detection for the form section.
	 */
	public function validate_form() {
		$section = 'form.body';
		$form = $this->contact_form->prop( 'form' );
		$this->detect_multiple_controls_in_label( $section, $form );
		$this->detect_unavailable_names( $section, $form );
		$this->detect_unavailable_html_elements( $section, $form );
		$this->detect_dots_in_names( $section, $form );
		$this->detect_colons_in_names( $section, $form );
		$this->detect_upload_filesize_overlimit( $section, $form );
	}


	/**
	 * Detects errors of multiple form controls in a single label.
	 *
	 * @link https://contactform7.com/configuration-errors/multiple-controls-in-label/
	 */
	public function detect_multiple_controls_in_label( $section, $content ) {
		$pattern = '%<label(?:[ \t\n]+.*?)?>(.+?)</label>%s';

		if ( preg_match_all( $pattern, $content, $matches ) ) {
			$form_tags_manager = WPCF7_FormTagsManager::get_instance();

			foreach ( $matches[1] as $insidelabel ) {
				$tags = $form_tags_manager->scan( $insidelabel );
				$fields_count = 0;

				foreach ( $tags as $tag ) {
					$is_multiple_controls_container = wpcf7_form_tag_supports(
						$tag->type, 'multiple-controls-container'
					);

					$is_zero_controls_container = wpcf7_form_tag_supports(
						$tag->type, 'zero-controls-container'
					);

					if ( $is_multiple_controls_container ) {
						$fields_count += count( $tag->values );

						if ( $tag->has_option( 'free_text' ) ) {
							$fields_count += 1;
						}
					} elseif ( $is_zero_controls_container ) {
						$fields_count += 0;
					} elseif ( ! empty( $tag->name ) ) {
						$fields_count += 1;
					}

					if ( 1 < $fields_count ) {
						return $this->add_error( $section,
							self::error_multiple_controls_in_label, array(
								'link' => self::get_doc_link( 'multiple_controls_in_label' ),
							)
						);
					}
				}
			}
		}

		return false;
	}


	/**
	 * Detects errors of unavailable form-tag names.
	 *
	 * @link https://contactform7.com/configuration-errors/unavailable-names/
	 */
	public function detect_unavailable_names( $section, $content ) {
		$public_query_vars = array( 'm', 'p', 'posts', 'w', 'cat',
			'withcomments', 'withoutcomments', 's', 'search', 'exact', 'sentence',
			'calendar', 'page', 'paged', 'more', 'tb', 'pb', 'author', 'order',
			'orderby', 'year', 'monthnum', 'day', 'hour', 'minute', 'second',
			'name', 'category_name', 'tag', 'feed', 'author_name', 'static',
			'pagename', 'page_id', 'error', 'attachment', 'attachment_id',
			'subpost', 'subpost_id', 'preview', 'robots', 'taxonomy', 'term',
			'cpage', 'post_type', 'embed',
		);

		$form_tags_manager = WPCF7_FormTagsManager::get_instance();

		$ng_named_tags = $form_tags_manager->filter( $content, array(
			'name' => $public_query_vars,
		) );

		$ng_names = array();

		foreach ( $ng_named_tags as $tag ) {
			$ng_names[] = sprintf( '"%s"', $tag->name );
		}

		if ( $ng_names ) {
			$ng_names = array_unique( $ng_names );

			return $this->add_error( $section,
				self::error_unavailable_names,
				array(
					'message' =>
						/* translators: %names%: a list of form control names */
						__( "Unavailable names (%names%) are used for form controls.", 'contact-form-7' ),
					'params' => array( 'names' => implode( ', ', $ng_names ) ),
					'link' => self::get_doc_link( 'unavailable_names' ),
				)
			);
		}

		return false;
	}


	/**
	 * Detects errors of unavailable HTML elements.
	 *
	 * @link https://contactform7.com/configuration-errors/unavailable-html-elements/
	 */
	public function detect_unavailable_html_elements( $section, $content ) {
		$pattern = '%(?:<form[\s\t>]|</form>)%i';

		if ( preg_match( $pattern, $content ) ) {
			return $this->add_error( $section,
				self::error_unavailable_html_elements,
				array(
					'message' => __( "Unavailable HTML elements are used in the form template.", 'contact-form-7' ),
					'link' => self::get_doc_link( 'unavailable_html_elements' ),
				)
			);
		}

		return false;
	}


	/**
	 * Detects errors of dots in form-tag names.
	 *
	 * @link https://contactform7.com/configuration-errors/dots-in-names/
	 */
	public function detect_dots_in_names( $section, $content ) {
		$form_tags_manager = WPCF7_FormTagsManager::get_instance();

		$tags = $form_tags_manager->filter( $content, array(
			'feature' => 'name-attr',
		) );

		foreach ( $tags as $tag ) {
			if ( false !== strpos( $tag->raw_name, '.' ) ) {
				return $this->add_error( $section,
					self::error_dots_in_names,
					array(
						'message' => __( "Dots are used in form-tag names.", 'contact-form-7' ),
						'link' => self::get_doc_link( 'dots_in_names' ),
					)
				);
			}
		}

		return false;
	}


	/**
	 * Detects errors of colons in form-tag names.
	 *
	 * @link https://contactform7.com/configuration-errors/colons-in-names/
	 */
	public function detect_colons_in_names( $section, $content ) {
		$form_tags_manager = WPCF7_FormTagsManager::get_instance();

		$tags = $form_tags_manager->filter( $content, array(
			'feature' => 'name-attr',
		) );

		foreach ( $tags as $tag ) {
			if ( false !== strpos( $tag->raw_name, ':' ) ) {
				return $this->add_error( $section,
					self::error_colons_in_names,
					array(
						'message' => __( "Colons are used in form-tag names.", 'contact-form-7' ),
						'link' => self::get_doc_link( 'colons_in_names' ),
					)
				);
			}
		}

		return false;
	}


	/**
	 * Detects errors of uploadable file size overlimit.
	 *
	 * @link https://contactform7.com/configuration-errors/upload-filesize-overlimit
	 */
	public function detect_upload_filesize_overlimit( $section, $content ) {
		$upload_max_filesize = ini_get( 'upload_max_filesize' );

		if ( ! $upload_max_filesize ) {
			return false;
		}

		$upload_max_filesize = strtolower( $upload_max_filesize );
		$upload_max_filesize = trim( $upload_max_filesize );

		if ( ! preg_match( '/^(\d+)([kmg]?)$/', $upload_max_filesize, $matches ) ) {
			return false;
		}

		if ( 'k' === $matches[2] ) {
			$upload_max_filesize = (int) $matches[1] * KB_IN_BYTES;
		} elseif ( 'm' === $matches[2] ) {
			$upload_max_filesize = (int) $matches[1] * MB_IN_BYTES;
		} elseif ( 'g' === $matches[2] ) {
			$upload_max_filesize = (int) $matches[1] * GB_IN_BYTES;
		} else {
			$upload_max_filesize = (int) $matches[1];
		}

		$form_tags_manager = WPCF7_FormTagsManager::get_instance();

		$tags = $form_tags_manager->filter( $content, array(
			'basetype' => 'file',
		) );

		foreach ( $tags as $tag ) {
			if ( $upload_max_filesize < $tag->get_limit_option() ) {
				return $this->add_error( $section,
					self::error_upload_filesize_overlimit,
					array(
						'message' => __( "Uploadable file size exceeds PHP’s maximum acceptable size.", 'contact-form-7' ),
						'link' => self::get_doc_link( 'upload_filesize_overlimit' ),
					)
				);
			}
		}

		return false;
	}


	/**
	 * Runs error detection for the mail sections.
	 */
	public function validate_mail( $template = 'mail' ) {
		if (
			$this->contact_form->is_true( 'demo_mode' ) or
			$this->contact_form->is_true( 'skip_mail' )
		) {
			return;
		}

		$components = (array) $this->contact_form->prop( $template );

		if ( ! $components ) {
			return;
		}

		if ( 'mail' !== $template and empty( $components['active'] ) ) {
			return;
		}

		$components = wp_parse_args( $components, array(
			'subject' => '',
			'sender' => '',
			'recipient' => '',
			'additional_headers' => '',
			'body' => '',
			'attachments' => '',
		) );

		$callback = array( $this, 'replace_mail_tags_with_minimum_input' );

		$subject = new WPCF7_MailTaggedText(
			$components['subject'],
			array( 'callback' => $callback )
		);

		$subject = $subject->replace_tags();
		$subject = wpcf7_strip_newline( $subject );

		$this->detect_maybe_empty( sprintf( '%s.subject', $template ), $subject );

		$sender = new WPCF7_MailTaggedText(
			$components['sender'],
			array( 'callback' => $callback )
		);

		$sender = $sender->replace_tags();
		$sender = wpcf7_strip_newline( $sender );

		$invalid_mailbox = $this->detect_invalid_mailbox_syntax(
			sprintf( '%s.sender', $template ),
			$sender
		);

		if ( ! $invalid_mailbox and ! wpcf7_is_email_in_site_domain( $sender ) ) {
			$this->add_error( sprintf( '%s.sender', $template ),
				self::error_email_not_in_site_domain, array(
					'link' => self::get_doc_link( 'email_not_in_site_domain' ),
				)
			);
		}

		$recipient = new WPCF7_MailTaggedText(
			$components['recipient'],
			array( 'callback' => $callback )
		);

		$recipient = $recipient->replace_tags();
		$recipient = wpcf7_strip_newline( $recipient );

		$this->detect_invalid_mailbox_syntax(
			sprintf( '%s.recipient', $template ),
			$recipient
		);

		$additional_headers = new WPCF7_MailTaggedText(
			$components['additional_headers'],
			array( 'callback' => $callback )
		);

		$additional_headers = $additional_headers->replace_tags();
		$additional_headers = explode( "\n", $additional_headers );
		$mailbox_header_types = array( 'reply-to', 'cc', 'bcc' );
		$invalid_mail_header_exists = false;

		foreach ( $additional_headers as $header ) {
			$header = trim( $header );

			if ( '' === $header ) {
				continue;
			}

			if ( ! preg_match( '/^([0-9A-Za-z-]+):(.*)$/', $header, $matches ) ) {
				$invalid_mail_header_exists = true;
			} else {
				$header_name = $matches[1];
				$header_value = trim( $matches[2] );

				if ( in_array( strtolower( $header_name ), $mailbox_header_types )
				and '' !== $header_value ) {
					$this->detect_invalid_mailbox_syntax(
						sprintf( '%s.additional_headers', $template ),
						$header_value,
						array(
							'message' =>
								__( "Invalid mailbox syntax is used in the %name% field.", 'contact-form-7' ),
							'params' => array( 'name' => $header_name )
						)
					);
				}
			}
		}

		if ( $invalid_mail_header_exists ) {
			$this->add_error( sprintf( '%s.additional_headers', $template ),
				self::error_invalid_mail_header, array(
					'link' => self::get_doc_link( 'invalid_mail_header' ),
				)
			);
		}

		$body = new WPCF7_MailTaggedText(
			$components['body'],
			array( 'callback' => $callback )
		);

		$body = $body->replace_tags();

		$this->detect_maybe_empty( sprintf( '%s.body', $template ), $body );

		if ( '' !== $components['attachments'] ) {
			$attachables = array();

			$tags = $this->contact_form->scan_form_tags(
				array( 'type' => array( 'file', 'file*' ) )
			);

			foreach ( $tags as $tag ) {
				$name = $tag->name;

				if ( false === strpos( $components['attachments'], "[{$name}]" ) ) {
					continue;
				}

				$limit = (int) $tag->get_limit_option();

				if ( empty( $attachables[$name] )
				or $attachables[$name] < $limit ) {
					$attachables[$name] = $limit;
				}
			}

			$total_size = array_sum( $attachables );

			$has_file_not_found = false;
			$has_file_not_in_content_dir = false;

			foreach ( explode( "\n", $components['attachments'] ) as $line ) {
				$line = trim( $line );

				if ( '' === $line or '[' == substr( $line, 0, 1 ) ) {
					continue;
				}

				$has_file_not_found = $this->detect_file_not_found(
					sprintf( '%s.attachments', $template ), $line
				);

				if ( ! $has_file_not_found and ! $has_file_not_in_content_dir ) {
					$has_file_not_in_content_dir = $this->detect_file_not_in_content_dir(
						sprintf( '%s.attachments', $template ), $line
					);
				}

				if ( ! $has_file_not_found ) {
					$path = path_join( WP_CONTENT_DIR, $line );
					$total_size += (int) @filesize( $path );
				}
			}

			$max = 25 * MB_IN_BYTES; // 25 MB

			if ( $max < $total_size ) {
				$this->add_error( sprintf( '%s.attachments', $template ),
					self::error_attachments_overweight,
					array(
						'message' => __( "The total size of attachment files is too large.", 'contact-form-7' ),
						'link' => self::get_doc_link( 'attachments_overweight' ),
					)
				);
			}
		}
	}


	/**
	 * Detects errors of invalid mailbox syntax.
	 *
	 * @link https://contactform7.com/configuration-errors/invalid-mailbox-syntax/
	 */
	public function detect_invalid_mailbox_syntax( $section, $content, $args = '' ) {
		$args = wp_parse_args( $args, array(
			'link' => self::get_doc_link( 'invalid_mailbox_syntax' ),
			'message' => '',
			'params' => array(),
		) );

		if ( ! wpcf7_is_mailbox_list( $content ) ) {
			return $this->add_error( $section,
				self::error_invalid_mailbox_syntax, $args
			);
		}

		return false;
	}


	/**
	 * Detects errors of empty message fields.
	 *
	 * @link https://contactform7.com/configuration-errors/maybe-empty/
	 */
	public function detect_maybe_empty( $section, $content ) {
		if ( '' === $content ) {
			return $this->add_error( $section,
				self::error_maybe_empty, array(
					'link' => self::get_doc_link( 'maybe_empty' ),
				)
			);
		}

		return false;
	}


	/**
	 * Detects errors of nonexistent attachment files.
	 *
	 * @link https://contactform7.com/configuration-errors/file-not-found/
	 */
	public function detect_file_not_found( $section, $content ) {
		$path = path_join( WP_CONTENT_DIR, $content );

		if ( ! is_readable( $path ) or ! is_file( $path ) ) {
			return $this->add_error( $section,
				self::error_file_not_found,
				array(
					'message' =>
						__( "Attachment file does not exist at %path%.", 'contact-form-7' ),
					'params' => array( 'path' => $content ),
					'link' => self::get_doc_link( 'file_not_found' ),
				)
			);
		}

		return false;
	}


	/**
	 * Detects errors of attachment files out of the content directory.
	 *
	 * @link https://contactform7.com/configuration-errors/file-not-in-content-dir/
	 */
	public function detect_file_not_in_content_dir( $section, $content ) {
		$path = path_join( WP_CONTENT_DIR, $content );

		if ( ! wpcf7_is_file_path_in_content_dir( $path ) ) {
			return $this->add_error( $section,
				self::error_file_not_in_content_dir,
				array(
					'message' =>
						__( "It is not allowed to use files outside the wp-content directory.", 'contact-form-7' ),
					'link' => self::get_doc_link( 'file_not_in_content_dir' ),
				)
			);
		}

		return false;
	}


	/**
	 * Runs error detection for the messages section.
	 */
	public function validate_messages() {
		$messages = (array) $this->contact_form->prop( 'messages' );

		if ( ! $messages ) {
			return;
		}

		if ( isset( $messages['captcha_not_match'] )
		and ! wpcf7_use_really_simple_captcha() ) {
			unset( $messages['captcha_not_match'] );
		}

		foreach ( $messages as $key => $message ) {
			$section = sprintf( 'messages.%s', $key );
			$this->detect_html_in_message( $section, $message );
		}
	}


	/**
	 * Detects errors of HTML uses in a message.
	 *
	 * @link https://contactform7.com/configuration-errors/html-in-message/
	 */
	public function detect_html_in_message( $section, $content ) {
		$stripped = wp_strip_all_tags( $content );

		if ( $stripped != $content ) {
			return $this->add_error( $section,
				self::error_html_in_message,
				array(
					'link' => self::get_doc_link( 'html_in_message' ),
				)
			);
		}

		return false;
	}


	/**
	 * Runs error detection for the additional settings section.
	 */
	public function validate_additional_settings() {
		$deprecated_settings_used =
			$this->contact_form->additional_setting( 'on_sent_ok' ) ||
			$this->contact_form->additional_setting( 'on_submit' );

		if ( $deprecated_settings_used ) {
			return $this->add_error( 'additional_settings.body',
				self::error_deprecated_settings,
				array(
					'link' => self::get_doc_link( 'deprecated_settings' ),
				)
			);
		}
	}

}
contact-form.php000064400000072677151330370000007664 0ustar00<?php

class WPCF7_ContactForm {

	use WPCF7_SWV_SchemaHolder;

	const post_type = 'wpcf7_contact_form';

	private static $found_items = 0;
	private static $current = null;

	private $id;
	private $name;
	private $title;
	private $locale;
	private $properties = array();
	private $unit_tag;
	private $responses_count = 0;
	private $scanned_form_tags;
	private $shortcode_atts = array();


	/**
	 * Returns count of contact forms found by the previous retrieval.
	 *
	 * @return int Count of contact forms.
	 */
	public static function count() {
		return self::$found_items;
	}


	/**
	 * Returns the contact form that is currently processed.
	 *
	 * @return WPCF7_ContactForm|null Current contact form object. Null if unset.
	 */
	public static function get_current() {
		return self::$current;
	}


	/**
	 * Registers the post type for contact forms.
	 */
	public static function register_post_type() {
		register_post_type( self::post_type, array(
			'labels' => array(
				'name' => __( 'Contact Forms', 'contact-form-7' ),
				'singular_name' => __( 'Contact Form', 'contact-form-7' ),
			),
			'rewrite' => false,
			'query_var' => false,
			'public' => false,
			'capability_type' => 'page',
			'capabilities' => array(
				'edit_post' => 'wpcf7_edit_contact_form',
				'read_post' => 'wpcf7_read_contact_form',
				'delete_post' => 'wpcf7_delete_contact_form',
				'edit_posts' => 'wpcf7_edit_contact_forms',
				'edit_others_posts' => 'wpcf7_edit_contact_forms',
				'publish_posts' => 'wpcf7_edit_contact_forms',
				'read_private_posts' => 'wpcf7_edit_contact_forms',
			),
		) );
	}


	/**
	 * Retrieves contact form data that match given conditions.
	 *
	 * @param string|array $args Optional. Arguments to be passed to WP_Query.
	 * @return array Array of WPCF7_ContactForm objects.
	 */
	public static function find( $args = '' ) {
		$defaults = array(
			'post_status' => 'any',
			'posts_per_page' => -1,
			'offset' => 0,
			'orderby' => 'ID',
			'order' => 'ASC',
		);

		$args = wp_parse_args( $args, $defaults );

		$args['post_type'] = self::post_type;

		$q = new WP_Query();
		$posts = $q->query( $args );

		self::$found_items = $q->found_posts;

		$objs = array();

		foreach ( (array) $posts as $post ) {
			$objs[] = new self( $post );
		}

		return $objs;
	}


	/**
	 * Returns a contact form data filled by default template contents.
	 *
	 * @param string|array $args Optional. Contact form options.
	 * @return WPCF7_ContactForm A new contact form object.
	 */
	public static function get_template( $args = '' ) {
		$args = wp_parse_args( $args, array(
			'locale' => null,
			'title' => __( 'Untitled', 'contact-form-7' ),
		) );

		if ( ! isset( $args['locale'] ) ) {
			$args['locale'] = determine_locale();
		}

		$callback = static function ( $args ) {
			$contact_form = new self;
			$contact_form->title = $args['title'];
			$contact_form->locale = $args['locale'];

			$properties = $contact_form->get_properties();

			foreach ( $properties as $key => $value ) {
				$default_template = WPCF7_ContactFormTemplate::get_default( $key );

				if ( isset( $default_template ) ) {
					$properties[$key] = $default_template;
				}
			}

			$contact_form->properties = $properties;

			return $contact_form;
		};

		$contact_form = wpcf7_switch_locale(
			$args['locale'],
			$callback,
			$args
		);

		self::$current = apply_filters( 'wpcf7_contact_form_default_pack',
			$contact_form, $args
		);

		return self::$current;
	}


	/**
	 * Creates a WPCF7_ContactForm object and sets it as the current instance.
	 *
	 * @param WPCF7_ContactForm|WP_Post|int $post Object or post ID.
	 * @return WPCF7_ContactForm|null Contact form object. Null if unset.
	 */
	public static function get_instance( $post ) {
		$contact_form = null;

		if ( $post instanceof self ) {
			$contact_form = $post;
		} elseif ( ! empty( $post ) ) {
			$post = get_post( $post );

			if ( isset( $post ) and self::post_type === get_post_type( $post ) ) {
				$contact_form = new self( $post );
			}
		}

		return self::$current = $contact_form;
	}


	/**
	 * Generates a "unit-tag" for the given contact form ID.
	 *
	 * @return string Unit-tag.
	 */
	private static function generate_unit_tag( $id = 0 ) {
		static $global_count = 0;

		$global_count += 1;

		if ( in_the_loop() ) {
			$unit_tag = sprintf( 'wpcf7-f%1$d-p%2$d-o%3$d',
				absint( $id ),
				get_the_ID(),
				$global_count
			);
		} else {
			$unit_tag = sprintf( 'wpcf7-f%1$d-o%2$d',
				absint( $id ),
				$global_count
			);
		}

		return $unit_tag;
	}


	/**
	 * Constructor.
	 */
	private function __construct( $post = null ) {
		$post = get_post( $post );

		if ( $post
		and self::post_type == get_post_type( $post ) ) {
			$this->id = $post->ID;
			$this->name = $post->post_name;
			$this->title = $post->post_title;
			$this->locale = get_post_meta( $post->ID, '_locale', true );

			$this->construct_properties( $post );
			$this->upgrade();
		} else {
			$this->construct_properties();
		}

		do_action( 'wpcf7_contact_form', $this );
	}


	/**
	 * Magic method for property overloading.
	 */
	public function __get( $name ) {
		$message = __( '<code>%1$s</code> property of a <code>WPCF7_ContactForm</code> object is <strong>no longer accessible</strong>. Use <code>%2$s</code> method instead.', 'contact-form-7' );

		if ( 'id' == $name ) {
			if ( WP_DEBUG ) {
				trigger_error(
					sprintf( $message, 'id', 'id()' ),
					E_USER_DEPRECATED
				);
			}

			return $this->id;
		} elseif ( 'title' == $name ) {
			if ( WP_DEBUG ) {
				trigger_error(
					sprintf( $message, 'title', 'title()' ),
					E_USER_DEPRECATED
				);
			}

			return $this->title;
		} elseif ( $prop = $this->prop( $name ) ) {
			if ( WP_DEBUG ) {
				trigger_error(
					sprintf( $message, $name, 'prop(\'' . $name . '\')' ),
					E_USER_DEPRECATED
				);
			}

			return $prop;
		}
	}


	/**
	 * Returns true if this contact form is not yet saved to the database.
	 */
	public function initial() {
		return empty( $this->id );
	}


	/**
	 * Constructs contact form properties. This is called only once
	 * from the constructor.
	 */
	private function construct_properties( $post = null ) {
		$builtin_properties = array(
			'form' => '',
			'mail' => array(),
			'mail_2' => array(),
			'messages' => array(),
			'additional_settings' => '',
		);

		$properties = apply_filters(
			'wpcf7_pre_construct_contact_form_properties',
			$builtin_properties, $this
		);

		// Filtering out properties with invalid name
		$properties = array_filter(
			$properties,
			static function ( $key ) {
				$sanitized_key = sanitize_key( $key );
				return $key === $sanitized_key;
			},
			ARRAY_FILTER_USE_KEY
		);

		foreach ( $properties as $name => $val ) {
			$prop = $this->retrieve_property( $name );

			if ( isset( $prop ) ) {
				$properties[$name] = $prop;
			}
		}

		$this->properties = $properties;

		foreach ( $properties as $name => $val ) {
			$properties[$name] = apply_filters(
				"wpcf7_contact_form_property_{$name}",
				$val, $this
			);
		}

		$this->properties = $properties;

		$properties = (array) apply_filters(
			'wpcf7_contact_form_properties',
			$properties, $this
		);

		$this->properties = $properties;
	}


	/**
	 * Retrieves contact form property of the specified name from the database.
	 *
	 * @param string $name Property name.
	 * @return array|string|null Property value. Null if property does not exist.
	 */
	private function retrieve_property( $name ) {
		$property = null;

		if ( ! $this->initial() ) {
			$post_id = $this->id;

			if ( metadata_exists( 'post', $post_id, '_' . $name ) ) {
				$property = get_post_meta( $post_id, '_' . $name, true );
			} elseif ( metadata_exists( 'post', $post_id, $name ) ) {
				$property = get_post_meta( $post_id, $name, true );
			}
		}

		return $property;
	}


	/**
	 * Returns the value for the given property name.
	 *
	 * @param string $name Property name.
	 * @return array|string|null Property value. Null if property does not exist.
	 */
	public function prop( $name ) {
		$props = $this->get_properties();
		return isset( $props[$name] ) ? $props[$name] : null;
	}


	/**
	 * Returns all the properties.
	 *
	 * @return array This contact form's properties.
	 */
	public function get_properties() {
		return (array) $this->properties;
	}


	/**
	 * Updates properties.
	 *
	 * @param array $properties New properties.
	 */
	public function set_properties( $properties ) {
		$defaults = $this->get_properties();

		$properties = wp_parse_args( $properties, $defaults );
		$properties = array_intersect_key( $properties, $defaults );

		$this->properties = $properties;
	}


	/**
	 * Returns ID of this contact form.
	 *
	 * @return int The ID.
	 */
	public function id() {
		return $this->id;
	}


	/**
	 * Returns unit-tag for this contact form.
	 *
	 * @return string Unit-tag.
	 */
	public function unit_tag() {
		return $this->unit_tag;
	}


	/**
	 * Returns name (slug) of this contact form.
	 *
	 * @return string Name.
	 */
	public function name() {
		return $this->name;
	}


	/**
	 * Returns title of this contact form.
	 *
	 * @return string Title.
	 */
	public function title() {
		return $this->title;
	}


	/**
	 * Set a title for this contact form.
	 *
	 * @param string $title Title.
	 */
	public function set_title( $title ) {
		$title = strip_tags( $title );
		$title = trim( $title );

		if ( '' === $title ) {
			$title = __( 'Untitled', 'contact-form-7' );
		}

		$this->title = $title;
	}


	/**
	 * Returns the locale code of this contact form.
	 *
	 * @return string Locale code. Empty string if no valid locale is set.
	 */
	public function locale() {
		if ( wpcf7_is_valid_locale( $this->locale ) ) {
			return $this->locale;
		} else {
			return '';
		}
	}


	/**
	 * Sets a locale for this contact form.
	 *
	 * @param string $locale Locale code.
	 */
	public function set_locale( $locale ) {
		$locale = trim( $locale );

		if ( wpcf7_is_valid_locale( $locale ) ) {
			$this->locale = $locale;
		} else {
			$this->locale = 'en_US';
		}
	}


	/**
	 * Returns the specified shortcode attribute value.
	 *
	 * @param string $name Shortcode attribute name.
	 * @return string|null Attribute value. Null if the attribute does not exist.
	 */
	public function shortcode_attr( $name ) {
		if ( isset( $this->shortcode_atts[$name] ) ) {
			return (string) $this->shortcode_atts[$name];
		}
	}


	/**
	 * Returns true if this contact form is identical to the submitted one.
	 */
	public function is_posted() {
		if ( ! WPCF7_Submission::get_instance() ) {
			return false;
		}

		if ( empty( $_POST['_wpcf7_unit_tag'] ) ) {
			return false;
		}

		return $this->unit_tag() === $_POST['_wpcf7_unit_tag'];
	}


	/**
	 * Generates HTML that represents a form.
	 *
	 * @param string|array $args Optional. Form options.
	 * @return string HTML output.
	 */
	public function form_html( $args = '' ) {
		$args = wp_parse_args( $args, array(
			'html_id' => '',
			'html_name' => '',
			'html_title' => '',
			'html_class' => '',
			'output' => 'form',
		) );

		$this->shortcode_atts = $args;

		if ( 'raw_form' == $args['output'] ) {
			return sprintf(
				'<pre class="wpcf7-raw-form"><code>%s</code></pre>',
				esc_html( $this->prop( 'form' ) )
			);
		}

		if ( $this->is_true( 'subscribers_only' )
		and ! current_user_can( 'wpcf7_submit', $this->id() ) ) {
			$notice = __(
				"This contact form is available only for logged in users.",
				'contact-form-7'
			);

			$notice = sprintf(
				'<p class="wpcf7-subscribers-only">%s</p>',
				esc_html( $notice )
			);

			return apply_filters( 'wpcf7_subscribers_only_notice', $notice, $this );
		}

		$this->unit_tag = self::generate_unit_tag( $this->id );

		$lang_tag = str_replace( '_', '-', $this->locale );

		if ( preg_match( '/^([a-z]+-[a-z]+)-/i', $lang_tag, $matches ) ) {
			$lang_tag = $matches[1];
		}

		$html = "\n" . sprintf( '<div %s>',
			wpcf7_format_atts( array(
				'class' => 'wpcf7 no-js',
				'id' => $this->unit_tag(),
				( get_option( 'html_type' ) == 'text/html' ) ? 'lang' : 'xml:lang'
					=> $lang_tag,
				'dir' => wpcf7_is_rtl( $this->locale ) ? 'rtl' : 'ltr',
			) )
		);

		$html .= "\n" . $this->screen_reader_response() . "\n";

		$url = wpcf7_get_request_uri();

		if ( $frag = strstr( $url, '#' ) ) {
			$url = substr( $url, 0, -strlen( $frag ) );
		}

		$url .= '#' . $this->unit_tag();

		$url = apply_filters( 'wpcf7_form_action_url', $url );

		$id_attr = apply_filters( 'wpcf7_form_id_attr',
			preg_replace( '/[^A-Za-z0-9:._-]/', '', $args['html_id'] )
		);

		$name_attr = apply_filters( 'wpcf7_form_name_attr',
			preg_replace( '/[^A-Za-z0-9:._-]/', '', $args['html_name'] )
		);

		$title_attr = apply_filters( 'wpcf7_form_title_attr', $args['html_title'] );

		$class = 'wpcf7-form';

		if ( $this->is_posted() ) {
			$submission = WPCF7_Submission::get_instance();

			$data_status_attr = $this->form_status_class_name(
				$submission->get_status()
			);

			$class .= sprintf( ' %s', $data_status_attr );
		} else {
			$data_status_attr = 'init';
			$class .= ' init';
		}

		if ( $args['html_class'] ) {
			$class .= ' ' . $args['html_class'];
		}

		if ( $this->in_demo_mode() ) {
			$class .= ' demo';
		}

		$class = explode( ' ', $class );
		$class = array_map( 'sanitize_html_class', $class );
		$class = array_filter( $class );
		$class = array_unique( $class );
		$class = implode( ' ', $class );
		$class = apply_filters( 'wpcf7_form_class_attr', $class );

		$enctype = wpcf7_enctype_value( apply_filters( 'wpcf7_form_enctype', '' ) );
		$autocomplete = apply_filters( 'wpcf7_form_autocomplete', '' );

		$atts = array(
			'action' => esc_url( $url ),
			'method' => 'post',
			'class' => ( '' !== $class ) ? $class : null,
			'id' => ( '' !== $id_attr ) ? $id_attr : null,
			'name' => ( '' !== $name_attr ) ? $name_attr : null,
			'aria-label' => ( '' !== $title_attr )
				? $title_attr : __( 'Contact form', 'contact-form-7' ),
			'enctype' => ( '' !== $enctype ) ? $enctype : null,
			'autocomplete' => ( '' !== $autocomplete ) ? $autocomplete : null,
			'novalidate' => true,
			'data-status' => $data_status_attr,
		);

		$atts = wpcf7_format_atts( $atts );

		$html .= sprintf( '<form %s>', $atts ) . "\n";
		$html .= $this->form_hidden_fields();
		$html .= $this->form_elements();

		if ( ! $this->responses_count ) {
			$html .= $this->form_response_output();
		}

		$html .= "\n" . '</form>';
		$html .= "\n" . '</div>';

		return $html . "\n";
	}


	/**
	 * Returns the class name that matches the given form status.
	 */
	private function form_status_class_name( $status ) {
		switch ( $status ) {
			case 'init':
				$class = 'init';
				break;
			case 'validation_failed':
				$class = 'invalid';
				break;
			case 'acceptance_missing':
				$class = 'unaccepted';
				break;
			case 'spam':
				$class = 'spam';
				break;
			case 'aborted':
				$class = 'aborted';
				break;
			case 'mail_sent':
				$class = 'sent';
				break;
			case 'mail_failed':
				$class = 'failed';
				break;
			default:
				$class = sprintf(
					'custom-%s',
					preg_replace( '/[^0-9a-z]+/i', '-', $status )
				);
		}

		return $class;
	}


	/**
	 * Returns a set of hidden fields.
	 */
	private function form_hidden_fields() {
		$hidden_fields = array(
			'_wpcf7' => $this->id(),
			'_wpcf7_version' => WPCF7_VERSION,
			'_wpcf7_locale' => $this->locale(),
			'_wpcf7_unit_tag' => $this->unit_tag(),
			'_wpcf7_container_post' => 0,
			'_wpcf7_posted_data_hash' => '',
		);

		if ( in_the_loop() ) {
			$hidden_fields['_wpcf7_container_post'] = (int) get_the_ID();
		}

		if ( $this->nonce_is_active() and is_user_logged_in() ) {
			$hidden_fields['_wpnonce'] = wpcf7_create_nonce();
		}

		$hidden_fields += (array) apply_filters(
			'wpcf7_form_hidden_fields', array()
		);

		$content = '';

		foreach ( $hidden_fields as $name => $value ) {
			$content .= sprintf(
				'<input type="hidden" name="%1$s" value="%2$s" />',
				esc_attr( $name ),
				esc_attr( $value )
			) . "\n";
		}

		return '<div style="display: none;">' . "\n" . $content . '</div>' . "\n";
	}


	/**
	 * Returns the visible response output for a form submission.
	 */
	public function form_response_output() {
		$status = 'init';
		$class = 'wpcf7-response-output';
		$content = '';

		if ( $this->is_posted() ) { // Post response output for non-AJAX
			$submission = WPCF7_Submission::get_instance();
			$status = $submission->get_status();
			$content = $submission->get_response();
		}

		$atts = array(
			'class' => trim( $class ),
			'aria-hidden' => 'true',
		);

		$output = sprintf( '<div %1$s>%2$s</div>',
			wpcf7_format_atts( $atts ),
			esc_html( $content )
		);

		$output = apply_filters( 'wpcf7_form_response_output',
			$output, $class, $content, $this, $status
		);

		$this->responses_count += 1;

		return $output;
	}


	/**
	 * Returns the response output that is only accessible from screen readers.
	 */
	public function screen_reader_response() {
		$primary_response = '';
		$validation_errors = array();

		if ( $this->is_posted() ) { // Post response output for non-AJAX
			$submission = WPCF7_Submission::get_instance();
			$primary_response = $submission->get_response();

			if ( $invalid_fields = $submission->get_invalid_fields() ) {
				foreach ( (array) $invalid_fields as $name => $field ) {
					$list_item = esc_html( $field['reason'] );

					if ( $field['idref'] ) {
						$list_item = sprintf(
							'<a href="#%1$s">%2$s</a>',
							esc_attr( $field['idref'] ),
							$list_item
						);
					}

					$validation_error_id = wpcf7_get_validation_error_reference(
						$name,
						$this->unit_tag()
					);

					if ( $validation_error_id ) {
						$list_item = sprintf(
							'<li id="%1$s">%2$s</li>',
							esc_attr( $validation_error_id ),
							$list_item
						);

						$validation_errors[] = $list_item;
					}
				}
			}
		}

		$primary_response = sprintf(
			'<p role="status" aria-live="polite" aria-atomic="true">%s</p>',
			esc_html( $primary_response )
		);

		$validation_errors = sprintf(
			'<ul>%s</ul>',
			implode( "\n", $validation_errors )
		);

		$output = sprintf(
			'<div class="screen-reader-response">%1$s %2$s</div>',
			$primary_response,
			$validation_errors
		);

		return $output;
	}


	/**
	 * Returns a validation error for the specified input field.
	 *
	 * @param string $name Input field name.
	 */
	public function validation_error( $name ) {
		$error = '';

		if ( $this->is_posted() ) {
			$submission = WPCF7_Submission::get_instance();

			if ( $invalid_field = $submission->get_invalid_field( $name ) ) {
				$error = trim( $invalid_field['reason'] );
			}
		}

		if ( ! $error ) {
			return $error;
		}

		$atts = array(
			'class' => 'wpcf7-not-valid-tip',
			'aria-hidden' => 'true',
		);

		$error = sprintf(
			'<span %1$s>%2$s</span>',
			wpcf7_format_atts( $atts ),
			esc_html( $error )
		);

		return apply_filters( 'wpcf7_validation_error', $error, $name, $this );
	}


	/**
	 * Replaces all form-tags in the form template with corresponding HTML.
	 *
	 * @return string Replaced form content.
	 */
	public function replace_all_form_tags() {
		$manager = WPCF7_FormTagsManager::get_instance();
		$form = $this->prop( 'form' );

		if ( wpcf7_autop_or_not() ) {
			$form = $manager->replace_with_placeholders( $form );
			$form = wpcf7_autop( $form );
			$form = $manager->restore_from_placeholders( $form );
		}

		$form = $manager->replace_all( $form );
		$this->scanned_form_tags = $manager->get_scanned_tags();

		return $form;
	}


	/**
	 * Replaces all form-tags in the form template with corresponding HTML.
	 *
	 * @deprecated 4.6 Use replace_all_form_tags()
	 *
	 * @return string Replaced form content.
	 */
	public function form_do_shortcode() {
		wpcf7_deprecated_function( __METHOD__, '4.6',
			'WPCF7_ContactForm::replace_all_form_tags'
		);

		return $this->replace_all_form_tags();
	}


	/**
	 * Scans form-tags from the form template.
	 *
	 * @param string|array|null $cond Optional. Filters. Default null.
	 * @return array Form-tags matching the given filter conditions.
	 */
	public function scan_form_tags( $cond = null ) {
		$manager = WPCF7_FormTagsManager::get_instance();

		if ( empty( $this->scanned_form_tags ) ) {
			$this->scanned_form_tags = $manager->scan( $this->prop( 'form' ) );
		}

		$tags = $this->scanned_form_tags;

		return $manager->filter( $tags, $cond );
	}


	/**
	 * Scans form-tags from the form template.
	 *
	 * @deprecated 4.6 Use scan_form_tags()
	 *
	 * @param string|array|null $cond Optional. Filters. Default null.
	 * @return array Form-tags matching the given filter conditions.
	 */
	public function form_scan_shortcode( $cond = null ) {
		wpcf7_deprecated_function( __METHOD__, '4.6',
			'WPCF7_ContactForm::scan_form_tags'
		);

		return $this->scan_form_tags( $cond );
	}


	/**
	 * Replaces all form-tags in the form template with corresponding HTML.
	 *
	 * @return string Replaced form content. wpcf7_form_elements filters applied.
	 */
	public function form_elements() {
		return apply_filters( 'wpcf7_form_elements',
			$this->replace_all_form_tags()
		);
	}


	/**
	 * Collects mail-tags available for this contact form.
	 *
	 * @param string|array $args Optional. Search options.
	 * @return array Mail-tag names.
	 */
	public function collect_mail_tags( $args = '' ) {
		$manager = WPCF7_FormTagsManager::get_instance();

		$args = wp_parse_args( $args, array(
			'include' => array(),
			'exclude' => $manager->collect_tag_types( 'not-for-mail' ),
		) );

		$tags = $this->scan_form_tags();
		$mailtags = array();

		foreach ( (array) $tags as $tag ) {
			$type = $tag->basetype;

			if ( empty( $type ) ) {
				continue;
			} elseif ( ! empty( $args['include'] ) ) {
				if ( ! in_array( $type, $args['include'] ) ) {
					continue;
				}
			} elseif ( ! empty( $args['exclude'] ) ) {
				if ( in_array( $type, $args['exclude'] ) ) {
					continue;
				}
			}

			$mailtags[] = $tag->name;
		}

		$mailtags = array_unique( $mailtags );
		$mailtags = array_filter( $mailtags );
		$mailtags = array_values( $mailtags );

		return apply_filters( 'wpcf7_collect_mail_tags', $mailtags, $args, $this );
	}


	/**
	 * Prints a mail-tag suggestion list.
	 *
	 * @param string $template_name Optional. Mail template name. Default 'mail'.
	 */
	public function suggest_mail_tags( $template_name = 'mail' ) {
		$mail = wp_parse_args( $this->prop( $template_name ),
			array(
				'active' => false,
				'recipient' => '',
				'sender' => '',
				'subject' => '',
				'body' => '',
				'additional_headers' => '',
				'attachments' => '',
				'use_html' => false,
				'exclude_blank' => false,
			)
		);

		$mail = array_filter( $mail );

		foreach ( (array) $this->collect_mail_tags() as $mail_tag ) {
			$pattern = sprintf(
				'/\[(_[a-z]+_)?%s([ \t]+[^]]+)?\]/',
				preg_quote( $mail_tag, '/' )
			);

			$used = preg_grep( $pattern, $mail );

			echo sprintf(
				'<span class="%1$s">[%2$s]</span>',
				'mailtag code ' . ( $used ? 'used' : 'unused' ),
				esc_html( $mail_tag )
			);
		}
	}


	/**
	 * Submits this contact form.
	 *
	 * @param string|array $args Optional. Submission options. Default empty.
	 * @return array Result of submission.
	 */
	public function submit( $args = '' ) {
		$args = wp_parse_args( $args, array(
			'skip_mail' =>
				( $this->in_demo_mode()
				|| $this->is_true( 'skip_mail' )
				|| ! empty( $this->skip_mail ) ),
		) );

		if ( $this->is_true( 'subscribers_only' )
		and ! current_user_can( 'wpcf7_submit', $this->id() ) ) {
			$result = array(
				'contact_form_id' => $this->id(),
				'status' => 'error',
				'message' => __(
					"This contact form is available only for logged in users.",
					'contact-form-7'
				),
			);

			return $result;
		}

		$submission = WPCF7_Submission::get_instance( $this, array(
			'skip_mail' => $args['skip_mail'],
		) );

		$result = array(
			'contact_form_id' => $this->id(),
		);

		$result += $submission->get_result();

		if ( $this->in_demo_mode() ) {
			$result['demo_mode'] = true;
		}

		do_action( 'wpcf7_submit', $this, $result );

		return $result;
	}


	/**
	 * Returns message used for given status.
	 *
	 * @param string $status Status.
	 * @param bool $filter Optional. Whether filters are applied. Default true.
	 * @return string Message.
	 */
	public function message( $status, $filter = true ) {
		$messages = $this->prop( 'messages' );
		$message = isset( $messages[$status] ) ? $messages[$status] : '';

		if ( $filter ) {
			$message = $this->filter_message( $message, $status );
		}

		return $message;
	}


	/**
	 * Filters a message.
	 *
	 * @param string $message Message to filter.
	 * @param string $status Optional. Status. Default empty.
	 * @return string Filtered message.
	 */
	public function filter_message( $message, $status = '' ) {
		$message = wpcf7_mail_replace_tags( $message );
		$message = apply_filters( 'wpcf7_display_message', $message, $status );
		$message = wp_strip_all_tags( $message );

		return $message;
	}


	/**
	 * Returns the additional setting value searched by name.
	 *
	 * @param string $name Name of setting.
	 * @return string Additional setting value.
	 */
	public function pref( $name ) {
		$settings = $this->additional_setting( $name );

		if ( $settings ) {
			return $settings[0];
		}
	}


	/**
	 * Returns additional setting values searched by name.
	 *
	 * @param string $name Name of setting.
	 * @param int $max Maximum result item count.
	 * @return array Additional setting values.
	 */
	public function additional_setting( $name, $max = 1 ) {
		$settings = (array) explode( "\n", $this->prop( 'additional_settings' ) );

		$pattern = '/^([a-zA-Z0-9_]+)[\t ]*:(.*)$/';
		$count = 0;
		$values = array();

		foreach ( $settings as $setting ) {
			if ( preg_match( $pattern, $setting, $matches ) ) {
				if ( $matches[1] != $name ) {
					continue;
				}

				if ( ! $max or $count < (int) $max ) {
					$values[] = trim( $matches[2] );
					$count += 1;
				}
			}
		}

		return $values;
	}


	/**
	 * Returns true if the specified setting has a truthy string value.
	 *
	 * @param string $name Name of setting.
	 * @return bool True if the setting value is 'on', 'true', or '1'.
	 */
	public function is_true( $name ) {
		return in_array(
			$this->pref( $name ),
			array( 'on', 'true', '1' ),
			true
		);
	}


	/**
	 * Returns true if this contact form is in the demo mode.
	 */
	public function in_demo_mode() {
		return $this->is_true( 'demo_mode' );
	}


	/**
	 * Returns true if nonce is active for this contact form.
	 */
	public function nonce_is_active() {
		$is_active = WPCF7_VERIFY_NONCE;

		if ( $this->is_true( 'subscribers_only' ) ) {
			$is_active = true;
		}

		return (bool) apply_filters( 'wpcf7_verify_nonce', $is_active, $this );
	}


	/**
	 * Returns true if the specified setting has a falsey string value.
	 *
	 * @param string $name Name of setting.
	 * @return bool True if the setting value is 'off', 'false', or '0'.
	 */
	public function is_false( $name ) {
		return in_array(
			$this->pref( $name ),
			array( 'off', 'false', '0' ),
			true
		);
	}


	/**
	 * Upgrades this contact form properties.
	 */
	private function upgrade() {
		$mail = $this->prop( 'mail' );

		if ( is_array( $mail )
		and ! isset( $mail['recipient'] ) ) {
			$mail['recipient'] = get_option( 'admin_email' );
		}

		$this->properties['mail'] = $mail;

		$messages = $this->prop( 'messages' );

		if ( is_array( $messages ) ) {
			foreach ( wpcf7_messages() as $key => $arr ) {
				if ( ! isset( $messages[$key] ) ) {
					$messages[$key] = $arr['default'];
				}
			}
		}

		$this->properties['messages'] = $messages;
	}


	/**
	 * Stores this contact form properties to the database.
	 *
	 * @return int The post ID on success. The value 0 on failure.
	 */
	public function save() {
		$title = wp_slash( $this->title );
		$props = wp_slash( $this->get_properties() );

		$post_content = implode( "\n", wpcf7_array_flatten( $props ) );

		if ( $this->initial() ) {
			$post_id = wp_insert_post( array(
				'post_type' => self::post_type,
				'post_status' => 'publish',
				'post_title' => $title,
				'post_content' => trim( $post_content ),
			) );
		} else {
			$post_id = wp_update_post( array(
				'ID' => (int) $this->id,
				'post_status' => 'publish',
				'post_title' => $title,
				'post_content' => trim( $post_content ),
			) );
		}

		if ( $post_id ) {
			foreach ( $props as $prop => $value ) {
				update_post_meta( $post_id, '_' . $prop,
					wpcf7_normalize_newline_deep( $value )
				);
			}

			if ( wpcf7_is_valid_locale( $this->locale ) ) {
				update_post_meta( $post_id, '_locale', $this->locale );
			}

			if ( $this->initial() ) {
				$this->id = $post_id;
				do_action( 'wpcf7_after_create', $this );
			} else {
				do_action( 'wpcf7_after_update', $this );
			}

			do_action( 'wpcf7_after_save', $this );
		}

		return $post_id;
	}


	/**
	 * Makes a copy of this contact form.
	 *
	 * @return WPCF7_ContactForm New contact form object.
	 */
	public function copy() {
		$new = new self;
		$new->title = $this->title . '_copy';
		$new->locale = $this->locale;
		$new->properties = $this->properties;

		return apply_filters( 'wpcf7_copy', $new, $this );
	}


	/**
	 * Deletes this contact form.
	 */
	public function delete() {
		if ( $this->initial() ) {
			return;
		}

		if ( wp_delete_post( $this->id, true ) ) {
			$this->id = 0;
			return true;
		}

		return false;
	}


	/**
	 * Returns a WordPress shortcode for this contact form.
	 */
	public function shortcode( $args = '' ) {
		$args = wp_parse_args( $args, array(
			'use_old_format' => false
		) );

		$title = str_replace( array( '"', '[', ']' ), '', $this->title );

		if ( $args['use_old_format'] ) {
			$old_unit_id = (int) get_post_meta( $this->id, '_old_cf7_unit_id', true );

			if ( $old_unit_id ) {
				$shortcode = sprintf(
					'[contact-form %1$d "%2$s"]',
					$old_unit_id,
					$title
				);
			} else {
				$shortcode = '';
			}
		} else {
			$shortcode = sprintf(
				'[contact-form-7 id="%1$d" title="%2$s"]',
				$this->id,
				$title
			);
		}

		return apply_filters( 'wpcf7_contact_form_shortcode',
			$shortcode, $args, $this
		);
	}
}
l10n.php000064400000006576151330370000006035 0ustar00<?php

/**
 * Retrieves an associative array of languages to which
 * this plugin is translated.
 *
 * @return array Array of languages.
 */
function wpcf7_l10n() {
	static $l10n = array();

	if ( ! empty( $l10n ) ) {
		return $l10n;
	}

	if ( ! is_admin() ) {
		return $l10n;
	}

	require_once( ABSPATH . 'wp-admin/includes/translation-install.php' );

	$api = translations_api( 'plugins', array(
		'slug' => 'contact-form-7',
		'version' => WPCF7_VERSION,
	) );

	if ( is_wp_error( $api )
	or empty( $api['translations'] ) ) {
		return $l10n;
	}

	foreach ( (array) $api['translations'] as $translation ) {
		if ( ! empty( $translation['language'] )
		and ! empty( $translation['english_name'] ) ) {
			$l10n[$translation['language']] = $translation['english_name'];
		}
	}

	return $l10n;
}


/**
 * Returns true if the given locale code looks valid.
 *
 * @param string $locale Locale code.
 */
function wpcf7_is_valid_locale( $locale ) {
	if ( ! is_string( $locale ) ) {
		return false;
	}

	$pattern = '/^[a-z]{2,3}(?:_[a-zA-Z_]{2,})?$/';
	return (bool) preg_match( $pattern, $locale );
}


/**
 * Returns true if the given locale is an RTL language.
 */
function wpcf7_is_rtl( $locale = '' ) {
	static $rtl_locales = array(
		'ar' => 'Arabic',
		'ary' => 'Moroccan Arabic',
		'azb' => 'South Azerbaijani',
		'fa_IR' => 'Persian',
		'haz' => 'Hazaragi',
		'he_IL' => 'Hebrew',
		'ps' => 'Pashto',
		'ug_CN' => 'Uighur',
	);

	if ( empty( $locale )
	and function_exists( 'is_rtl' ) ) {
		return is_rtl();
	}

	if ( empty( $locale ) ) {
		$locale = determine_locale();
	}

	return isset( $rtl_locales[$locale] );
}


/**
 * Loads a translation file into the plugin's text domain.
 *
 * @param string $locale Locale code.
 * @return bool True on success, false on failure.
 */
function wpcf7_load_textdomain( $locale = '' ) {
	$mofile = path_join(
		WP_LANG_DIR . '/plugins/',
		sprintf( '%s-%s.mo', WPCF7_TEXT_DOMAIN, $locale )
	);

	return load_textdomain( WPCF7_TEXT_DOMAIN, $mofile, $locale );
}


/**
 * Unloads translations for the plugin's text domain.
 *
 * @param bool $reloadable Whether the text domain can be loaded
 *             just-in-time again.
 * @return bool True on success, false on failure.
 */
function wpcf7_unload_textdomain( $reloadable = false ) {
	return unload_textdomain( WPCF7_TEXT_DOMAIN, $reloadable );
}


/**
 * Switches translation locale, calls the callback, then switches back
 * to the original locale.
 *
 * @param string $locale Locale code.
 * @param callable $callback The callable to be called.
 * @param mixed $args Parameters to be passed to the callback.
 * @return mixed The return value of the callback.
 */
function wpcf7_switch_locale( $locale, callable $callback, ...$args ) {
	static $available_locales = null;

	if ( ! isset( $available_locales ) ) {
		$available_locales = array_merge(
			array( 'en_US' ),
			get_available_languages()
		);
	}

	$previous_locale = determine_locale();

	$do_switch_locale = (
		$locale !== $previous_locale &&
		in_array( $locale, $available_locales, true ) &&
		in_array( $previous_locale, $available_locales, true )
	);

	if ( $do_switch_locale ) {
		wpcf7_unload_textdomain();
		switch_to_locale( $locale );
		wpcf7_load_textdomain( $locale );
	}

	$result = call_user_func( $callback, ...$args );

	if ( $do_switch_locale ) {
		wpcf7_unload_textdomain( true );
		restore_previous_locale();
		wpcf7_load_textdomain( $previous_locale );
	}

	return $result;
}
validation.php000064400000006341151330370000007403 0ustar00<?php

/**
 * Server-side user input validation manager.
 */
class WPCF7_Validation implements ArrayAccess {
	private $invalid_fields = array();
	private $container = array();

	public function __construct() {
		$this->container = array(
			'valid' => true,
			'reason' => array(),
			'idref' => array(),
		);
	}


	/**
	 * Marks a form control as an invalid field.
	 *
	 * @param WPCF7_FormTag|array|string $context Context representing the
	 *                                   target field.
	 * @param WP_Error|string $error The error of the field.
	 */
	public function invalidate( $context, $error ) {
		if ( $context instanceof WPCF7_FormTag ) {
			$tag = $context;
		} elseif ( is_array( $context ) ) {
			$tag = new WPCF7_FormTag( $context );
		} elseif ( is_string( $context ) ) {
			$tags = wpcf7_scan_form_tags( array( 'name' => trim( $context ) ) );
			$tag = $tags ? new WPCF7_FormTag( $tags[0] ) : null;
		}

		$name = ! empty( $tag ) ? $tag->name : null;

		if ( empty( $name )
		or ! wpcf7_is_name( $name ) ) {
			return;
		}

		if ( is_wp_error( $error ) ) {
			$message = $error->get_error_message();
		} else {
			$message = $error;
		}

		if ( $this->is_valid( $name ) ) {
			$id = $tag->get_id_option();

			if ( empty( $id )
			or ! wpcf7_is_name( $id ) ) {
				$id = null;
			}

			$this->invalid_fields[$name] = array(
				'reason' => (string) $message,
				'idref' => $id,
			);
		}
	}


	/**
	 * Returns true if the target field is valid.
	 *
	 * @param string|null $name Optional. If specified, this is the name of
	 *                    the target field. Default null.
	 * @return bool True if the target field has no error. If no target is
	 *              specified, returns true if all fields are valid.
	 *              Otherwise false.
	 */
	public function is_valid( $name = null ) {
		if ( ! empty( $name ) ) {
			return ! isset( $this->invalid_fields[$name] );
		} else {
			return empty( $this->invalid_fields );
		}
	}


	/**
	 * Retrieves an associative array of invalid fields.
	 *
	 * @return array The associative array of invalid fields.
	 */
	public function get_invalid_fields() {
		return $this->invalid_fields;
	}


	/**
	 * Assigns a value to the specified offset.
	 *
	 * @link https://www.php.net/manual/en/arrayaccess.offsetset.php
	 */
	#[ReturnTypeWillChange]
	public function offsetSet( $offset, $value ) {
		if ( isset( $this->container[$offset] ) ) {
			$this->container[$offset] = $value;
		}

		if ( 'reason' == $offset
		and is_array( $value ) ) {
			foreach ( $value as $k => $v ) {
				$this->invalidate( $k, $v );
			}
		}
	}


	/**
	 * Returns the value at specified offset.
	 *
	 * @link https://www.php.net/manual/en/arrayaccess.offsetget.php
	 */
	#[ReturnTypeWillChange]
	public function offsetGet( $offset ) {
		if ( isset( $this->container[$offset] ) ) {
			return $this->container[$offset];
		}
	}


	/**
	 * Returns true if the specified offset exists.
	 *
	 * @link https://www.php.net/manual/en/arrayaccess.offsetexists.php
	 */
	#[ReturnTypeWillChange]
	public function offsetExists( $offset ) {
		return isset( $this->container[$offset] );
	}


	/**
	 * Unsets an offset.
	 *
	 * @link https://www.php.net/manual/en/arrayaccess.offsetunset.php
	 */
	#[ReturnTypeWillChange]
	public function offsetUnset( $offset ) {
	}

}
controller.php000064400000006036151330370000007435 0ustar00<?php
/**
 * Controller for front-end requests, scripts, and styles
 */


add_action(
	'parse_request',
	'wpcf7_control_init',
	20, 0
);

/**
 * Handles a submission in non-Ajax mode.
 */
function wpcf7_control_init() {
	if ( WPCF7_Submission::is_restful() ) {
		return;
	}

	if ( isset( $_POST['_wpcf7'] ) ) {
		$contact_form = wpcf7_contact_form( (int) $_POST['_wpcf7'] );

		if ( $contact_form ) {
			$contact_form->submit();
		}
	}
}


/**
 * Registers main scripts and styles.
 */
add_action(
	'wp_enqueue_scripts',
	static function () {
		$assets = array();
		$asset_file = wpcf7_plugin_path( 'includes/js/index.asset.php' );

		if ( file_exists( $asset_file ) ) {
			$assets = include( $asset_file );
		}

		$assets = wp_parse_args( $assets, array(
			'dependencies' => array(),
			'version' => WPCF7_VERSION,
		) );

		wp_register_script(
			'contact-form-7',
			wpcf7_plugin_url( 'includes/js/index.js' ),
			array_merge(
				$assets['dependencies'],
				array( 'swv' )
			),
			$assets['version'],
			true
		);

		wp_register_script(
			'contact-form-7-html5-fallback',
			wpcf7_plugin_url( 'includes/js/html5-fallback.js' ),
			array( 'jquery-ui-datepicker' ),
			WPCF7_VERSION,
			true
		);

		if ( wpcf7_load_js() ) {
			wpcf7_enqueue_scripts();
		}

		wp_register_style(
			'contact-form-7',
			wpcf7_plugin_url( 'includes/css/styles.css' ),
			array(),
			WPCF7_VERSION,
			'all'
		);

		wp_register_style(
			'contact-form-7-rtl',
			wpcf7_plugin_url( 'includes/css/styles-rtl.css' ),
			array( 'contact-form-7' ),
			WPCF7_VERSION,
			'all'
		);

		wp_register_style(
			'jquery-ui-smoothness',
			wpcf7_plugin_url(
				'includes/js/jquery-ui/themes/smoothness/jquery-ui.min.css'
			),
			array(),
			'1.12.1',
			'screen'
		);

		if ( wpcf7_load_css() ) {
			wpcf7_enqueue_styles();
		}
	},
	10, 0
);


/**
 * Enqueues scripts.
 */
function wpcf7_enqueue_scripts() {
	wp_enqueue_script( 'contact-form-7' );

	$wpcf7 = array(
		'api' => array(
			'root' => sanitize_url( get_rest_url() ),
			'namespace' => 'contact-form-7/v1',
		),
	);

	if ( defined( 'WP_CACHE' ) and WP_CACHE ) {
		$wpcf7['cached'] = 1;
	}

	wp_localize_script( 'contact-form-7', 'wpcf7', $wpcf7 );

	do_action( 'wpcf7_enqueue_scripts' );
}


/**
 * Returns true if the main script is enqueued.
 */
function wpcf7_script_is() {
	return wp_script_is( 'contact-form-7' );
}


/**
 * Enqueues styles.
 */
function wpcf7_enqueue_styles() {
	wp_enqueue_style( 'contact-form-7' );

	if ( wpcf7_is_rtl() ) {
		wp_enqueue_style( 'contact-form-7-rtl' );
	}

	do_action( 'wpcf7_enqueue_styles' );
}


/**
 * Returns true if the main stylesheet is enqueued.
 */
function wpcf7_style_is() {
	return wp_style_is( 'contact-form-7' );
}


add_action(
	'wp_enqueue_scripts',
	'wpcf7_html5_fallback',
	20, 0
);

/**
 * Enqueues scripts and styles for the HTML5 fallback.
 */
function wpcf7_html5_fallback() {
	if ( ! wpcf7_support_html5_fallback() ) {
		return;
	}

	if ( wpcf7_script_is() ) {
		wp_enqueue_script( 'contact-form-7-html5-fallback' );
	}

	if ( wpcf7_style_is() ) {
		wp_enqueue_style( 'jquery-ui-smoothness' );
	}
}
pipe.php000064400000003576151330370000006215 0ustar00<?php
/**
 * Pipe-related classes.
 *
 * @link https://contactform7.com/selectable-recipient-with-pipes/
 */


/**
 * Class representing a pair of pipe.
 */
class WPCF7_Pipe {

	public $before = '';
	public $after = '';

	public function __construct( $text ) {
		$text = (string) $text;

		$pipe_pos = strpos( $text, '|' );

		if ( false === $pipe_pos ) {
			$this->before = $this->after = trim( $text );
		} else {
			$this->before = trim( substr( $text, 0, $pipe_pos ) );
			$this->after = trim( substr( $text, $pipe_pos + 1 ) );
		}
	}
}


/**
 * Class representing a list of pipes.
 */
class WPCF7_Pipes {

	private $pipes = array();

	public function __construct( array $texts ) {
		foreach ( $texts as $text ) {
			$this->add_pipe( $text );
		}
	}

	private function add_pipe( $text ) {
		$pipe = new WPCF7_Pipe( $text );
		$this->pipes[] = $pipe;
	}

	public function do_pipe( $input ) {
		$input_canonical = wpcf7_canonicalize( $input, array(
			'strto' => 'as-is',
		) );

		foreach ( $this->pipes as $pipe ) {
			$before_canonical = wpcf7_canonicalize( $pipe->before, array(
				'strto' => 'as-is',
			) );

			if ( $input_canonical === $before_canonical ) {
				return $pipe->after;
			}
		}

		return $input;
	}

	public function collect_befores() {
		$befores = array();

		foreach ( $this->pipes as $pipe ) {
			$befores[] = $pipe->before;
		}

		return $befores;
	}

	public function collect_afters() {
		$afters = array();

		foreach ( $this->pipes as $pipe ) {
			$afters[] = $pipe->after;
		}

		return $afters;
	}

	public function zero() {
		return empty( $this->pipes );
	}

	public function random_pipe() {
		if ( $this->zero() ) {
			return null;
		}

		return $this->pipes[array_rand( $this->pipes )];
	}

	public function to_array() {
		return array_map(
			static function ( WPCF7_Pipe $pipe ) {
				return array(
					$pipe->before,
					$pipe->after,
				);
			},
			$this->pipes
		);
	}
}
shortcodes.php000064400000005226151330370000007427 0ustar00<?php
/**
 * All the functions and classes in this file are deprecated.
 * You should not use them. The functions and classes will be
 * removed in a later version.
 */

function wpcf7_add_shortcode( $tag, $callback, $has_name = false ) {
	wpcf7_deprecated_function( __FUNCTION__, '4.6', 'wpcf7_add_form_tag' );

	return wpcf7_add_form_tag( $tag, $callback, $has_name );
}

function wpcf7_remove_shortcode( $tag ) {
	wpcf7_deprecated_function( __FUNCTION__, '4.6', 'wpcf7_remove_form_tag' );

	return wpcf7_remove_form_tag( $tag );
}

function wpcf7_do_shortcode( $content ) {
	wpcf7_deprecated_function( __FUNCTION__, '4.6',
		'wpcf7_replace_all_form_tags' );

	return wpcf7_replace_all_form_tags( $content );
}

function wpcf7_scan_shortcode( $cond = null ) {
	wpcf7_deprecated_function( __FUNCTION__, '4.6', 'wpcf7_scan_form_tags' );

	return wpcf7_scan_form_tags( $cond );
}

class WPCF7_ShortcodeManager {

	private static $form_tags_manager;

	private function __construct() {}

	public static function get_instance() {
		wpcf7_deprecated_function( __METHOD__, '4.6',
			'WPCF7_FormTagsManager::get_instance' );

		self::$form_tags_manager = WPCF7_FormTagsManager::get_instance();
		return new self;
	}

	public function get_scanned_tags() {
		wpcf7_deprecated_function( __METHOD__, '4.6',
			'WPCF7_FormTagsManager::get_scanned_tags' );

		return self::$form_tags_manager->get_scanned_tags();
	}

	public function add_shortcode( $tag, $callback, $has_name = false ) {
		wpcf7_deprecated_function( __METHOD__, '4.6',
			'WPCF7_FormTagsManager::add' );

		return self::$form_tags_manager->add( $tag, $callback, $has_name );
	}

	public function remove_shortcode( $tag ) {
		wpcf7_deprecated_function( __METHOD__, '4.6',
			'WPCF7_FormTagsManager::remove' );

		return self::$form_tags_manager->remove( $tag );
	}

	public function normalize_shortcode( $content ) {
		wpcf7_deprecated_function( __METHOD__, '4.6',
			'WPCF7_FormTagsManager::normalize' );

		return self::$form_tags_manager->normalize( $content );
	}

	public function do_shortcode( $content, $exec = true ) {
		wpcf7_deprecated_function( __METHOD__, '4.6',
			'WPCF7_FormTagsManager::replace_all' );

		if ( $exec ) {
			return self::$form_tags_manager->replace_all( $content );
		} else {
			return self::$form_tags_manager->scan( $content );
		}
	}

	public function scan_shortcode( $content ) {
		wpcf7_deprecated_function( __METHOD__, '4.6',
			'WPCF7_FormTagsManager::scan' );

		return self::$form_tags_manager->scan( $content );
	}
}

class WPCF7_Shortcode extends WPCF7_FormTag {

	public function __construct( $tag ) {
		wpcf7_deprecated_function( 'WPCF7_Shortcode', '4.6', 'WPCF7_FormTag' );

		parent::__construct( $tag );
	}
}
swv/script-loader.php000064400000000774151330370000010644 0ustar00<?php

add_action(
	'wp_enqueue_scripts',
	static function () {
		$assets = array();
		$asset_file = wpcf7_plugin_path( 'includes/swv/js/index.asset.php' );

		if ( file_exists( $asset_file ) ) {
			$assets = include( $asset_file );
		}

		$assets = wp_parse_args( $assets, array(
			'dependencies' => array(),
			'version' => WPCF7_VERSION,
		) );

		wp_register_script( 'swv',
			wpcf7_plugin_url( 'includes/swv/js/index.js' ),
			$assets['dependencies'],
			$assets['version'],
			true
		);
	},
	10, 0
);
swv/schema-holder.php000064400000002340151330370000010576 0ustar00<?php

trait WPCF7_SWV_SchemaHolder {

	protected $schema;


	/**
	 * Retrieves SWV schema for this holder object (contact form).
	 *
	 * @return WPCF7_SWV_Schema The schema object.
	 */
	public function get_schema() {
		if ( isset( $this->schema ) ) {
			return $this->schema;
		}

		$schema = new WPCF7_SWV_Schema( array(
			'locale' => isset( $this->locale ) ? $this->locale : '',
		) );

		do_action( 'wpcf7_swv_create_schema', $schema, $this );

		return $this->schema = $schema;
	}


	/**
	 * Validates form inputs based on the schema and given context.
	 */
	public function validate_schema( $context, WPCF7_Validation $validity ) {
		$callback = static function ( $rule ) use ( &$callback, $context, $validity ) {
			if ( ! $rule->matches( $context ) ) {
				return;
			}

			if ( $rule instanceof WPCF7_SWV_CompositeRule ) {
				foreach ( $rule->rules() as $child_rule ) {
					call_user_func( $callback, $child_rule );
				}
			} else {
				$field = $rule->get_property( 'field' );

				if ( $validity->is_valid( $field ) ) {
					$result = $rule->validate( $context );

					if ( is_wp_error( $result ) ) {
						$validity->invalidate( $field, $result );
					}
				}
			}
		};

		call_user_func( $callback, $this->get_schema() );
	}

}
swv/rules/minnumber.php000064400000001675151330370000011223 0ustar00<?php

class WPCF7_SWV_MinNumberRule extends WPCF7_SWV_Rule {

	const rule_name = 'minnumber';

	public function matches( $context ) {
		if ( false === parent::matches( $context ) ) {
			return false;
		}

		if ( empty( $context['text'] ) ) {
			return false;
		}

		return true;
	}

	public function validate( $context ) {
		$field = $this->get_property( 'field' );
		$input = isset( $_POST[$field] ) ? $_POST[$field] : '';
		$input = wpcf7_array_flatten( $input );
		$input = wpcf7_exclude_blank( $input );

		$threshold = $this->get_property( 'threshold' );

		if ( ! wpcf7_is_number( $threshold ) ) {
			return true;
		}

		foreach ( $input as $i ) {
			if ( wpcf7_is_number( $i ) and (float) $i < (float) $threshold ) {
				return new WP_Error( 'wpcf7_invalid_minnumber',
					$this->get_property( 'error' )
				);
			}
		}

		return true;
	}

	public function to_array() {
		return array( 'rule' => self::rule_name ) + (array) $this->properties;
	}
}
swv/rules/maxnumber.php000064400000001675151330370000011225 0ustar00<?php

class WPCF7_SWV_MaxNumberRule extends WPCF7_SWV_Rule {

	const rule_name = 'maxnumber';

	public function matches( $context ) {
		if ( false === parent::matches( $context ) ) {
			return false;
		}

		if ( empty( $context['text'] ) ) {
			return false;
		}

		return true;
	}

	public function validate( $context ) {
		$field = $this->get_property( 'field' );
		$input = isset( $_POST[$field] ) ? $_POST[$field] : '';
		$input = wpcf7_array_flatten( $input );
		$input = wpcf7_exclude_blank( $input );

		$threshold = $this->get_property( 'threshold' );

		if ( ! wpcf7_is_number( $threshold ) ) {
			return true;
		}

		foreach ( $input as $i ) {
			if ( wpcf7_is_number( $i ) and (float) $threshold < (float) $i ) {
				return new WP_Error( 'wpcf7_invalid_maxnumber',
					$this->get_property( 'error' )
				);
			}
		}

		return true;
	}

	public function to_array() {
		return array( 'rule' => self::rule_name ) + (array) $this->properties;
	}
}
swv/rules/requiredfile.php000064400000001441151330370000011676 0ustar00<?php

class WPCF7_SWV_RequiredFileRule extends WPCF7_SWV_Rule {

	const rule_name = 'requiredfile';

	public function matches( $context ) {
		if ( false === parent::matches( $context ) ) {
			return false;
		}

		if ( empty( $context['file'] ) ) {
			return false;
		}

		return true;
	}

	public function validate( $context ) {
		$field = $this->get_property( 'field' );

		$input = isset( $_FILES[$field]['tmp_name'] )
			? $_FILES[$field]['tmp_name'] : '';

		$input = wpcf7_array_flatten( $input );
		$input = wpcf7_exclude_blank( $input );

		if ( empty( $input ) ) {
			return new WP_Error( 'wpcf7_invalid_requiredfile',
				$this->get_property( 'error' )
			);
		}

		return true;
	}

	public function to_array() {
		return array( 'rule' => self::rule_name ) + (array) $this->properties;
	}
}
swv/rules/required.php000064400000001370151330370000011037 0ustar00<?php

class WPCF7_SWV_RequiredRule extends WPCF7_SWV_Rule {

	const rule_name = 'required';

	public function matches( $context ) {
		if ( false === parent::matches( $context ) ) {
			return false;
		}

		if ( empty( $context['text'] ) ) {
			return false;
		}

		return true;
	}

	public function validate( $context ) {
		$field = $this->get_property( 'field' );

		$input = isset( $_POST[$field] ) ? $_POST[$field] : '';

		$input = wpcf7_array_flatten( $input );
		$input = wpcf7_exclude_blank( $input );

		if ( empty( $input ) ) {
			return new WP_Error( 'wpcf7_invalid_required',
				$this->get_property( 'error' )
			);
		}

		return true;
	}

	public function to_array() {
		return array( 'rule' => self::rule_name ) + (array) $this->properties;
	}
}
swv/rules/minitems.php000064400000001575151330370000011053 0ustar00<?php

class WPCF7_SWV_MinItemsRule extends WPCF7_SWV_Rule {

	const rule_name = 'minitems';

	public function matches( $context ) {
		if ( false === parent::matches( $context ) ) {
			return false;
		}

		if ( empty( $context['text'] ) ) {
			return false;
		}

		return true;
	}

	public function validate( $context ) {
		$field = $this->get_property( 'field' );
		$input = isset( $_POST[$field] ) ? $_POST[$field] : '';
		$input = wpcf7_array_flatten( $input );
		$input = wpcf7_exclude_blank( $input );

		$threshold = $this->get_property( 'threshold' );

		if ( ! wpcf7_is_number( $threshold ) ) {
			return true;
		}

		if ( count( $input ) < (int) $threshold ) {
			return new WP_Error( 'wpcf7_invalid_minitems',
				$this->get_property( 'error' )
			);
		}

		return true;
	}

	public function to_array() {
		return array( 'rule' => self::rule_name ) + (array) $this->properties;
	}
}
swv/rules/tel.php000064400000001422151330370000010001 0ustar00<?php

class WPCF7_SWV_TelRule extends WPCF7_SWV_Rule {

	const rule_name = 'tel';

	public function matches( $context ) {
		if ( false === parent::matches( $context ) ) {
			return false;
		}

		if ( empty( $context['text'] ) ) {
			return false;
		}

		return true;
	}

	public function validate( $context ) {
		$field = $this->get_property( 'field' );
		$input = isset( $_POST[$field] ) ? $_POST[$field] : '';
		$input = wpcf7_array_flatten( $input );
		$input = wpcf7_exclude_blank( $input );

		foreach ( $input as $i ) {
			if ( ! wpcf7_is_tel( $i ) ) {
				return new WP_Error( 'wpcf7_invalid_tel',
					$this->get_property( 'error' )
				);
			}
		}

		return true;
	}

	public function to_array() {
		return array( 'rule' => self::rule_name ) + (array) $this->properties;
	}
}
swv/rules/enum.php000064400000002046151330370000010164 0ustar00<?php

class WPCF7_SWV_EnumRule extends WPCF7_SWV_Rule {

	const rule_name = 'enum';

	public function matches( $context ) {
		if ( false === parent::matches( $context ) ) {
			return false;
		}

		if ( empty( $context['text'] ) ) {
			return false;
		}

		return true;
	}

	public function validate( $context ) {
		$field = $this->get_property( 'field' );

		$input = isset( $_POST[$field] ) ? $_POST[$field] : '';

		$input = wpcf7_array_flatten( $input );
		$input = wpcf7_exclude_blank( $input );

		$acceptable_values = (array) $this->get_property( 'accept' );
		$acceptable_values = array_map( 'strval', $acceptable_values );
		$acceptable_values = array_filter( $acceptable_values );
		$acceptable_values = array_unique( $acceptable_values );

		foreach ( $input as $i ) {
			if ( ! in_array( $i, $acceptable_values, true ) ) {
				return new WP_Error( 'wpcf7_invalid_enum',
					$this->get_property( 'error' )
				);
			}
		}

		return true;
	}

	public function to_array() {
		return array( 'rule' => self::rule_name ) + (array) $this->properties;
	}
}
swv/rules/mindate.php000064400000001643151330370000010643 0ustar00<?php

class WPCF7_SWV_MinDateRule extends WPCF7_SWV_Rule {

	const rule_name = 'mindate';

	public function matches( $context ) {
		if ( false === parent::matches( $context ) ) {
			return false;
		}

		if ( empty( $context['text'] ) ) {
			return false;
		}

		return true;
	}

	public function validate( $context ) {
		$field = $this->get_property( 'field' );
		$input = isset( $_POST[$field] ) ? $_POST[$field] : '';
		$input = wpcf7_array_flatten( $input );
		$input = wpcf7_exclude_blank( $input );

		$threshold = $this->get_property( 'threshold' );

		if ( ! wpcf7_is_date( $threshold ) ) {
			return true;
		}

		foreach ( $input as $i ) {
			if ( wpcf7_is_date( $i ) and $i < $threshold ) {
				return new WP_Error( 'wpcf7_invalid_mindate',
					$this->get_property( 'error' )
				);
			}
		}

		return true;
	}

	public function to_array() {
		return array( 'rule' => self::rule_name ) + (array) $this->properties;
	}
}
swv/rules/maxitems.php000064400000001575151330370000011055 0ustar00<?php

class WPCF7_SWV_MaxItemsRule extends WPCF7_SWV_Rule {

	const rule_name = 'maxitems';

	public function matches( $context ) {
		if ( false === parent::matches( $context ) ) {
			return false;
		}

		if ( empty( $context['text'] ) ) {
			return false;
		}

		return true;
	}

	public function validate( $context ) {
		$field = $this->get_property( 'field' );
		$input = isset( $_POST[$field] ) ? $_POST[$field] : '';
		$input = wpcf7_array_flatten( $input );
		$input = wpcf7_exclude_blank( $input );

		$threshold = $this->get_property( 'threshold' );

		if ( ! wpcf7_is_number( $threshold ) ) {
			return true;
		}

		if ( (int) $threshold < count( $input ) ) {
			return new WP_Error( 'wpcf7_invalid_maxitems',
				$this->get_property( 'error' )
			);
		}

		return true;
	}

	public function to_array() {
		return array( 'rule' => self::rule_name ) + (array) $this->properties;
	}
}
swv/rules/minfilesize.php000064400000001606151330370000011537 0ustar00<?php

class WPCF7_SWV_MinFileSizeRule extends WPCF7_SWV_Rule {

	const rule_name = 'minfilesize';

	public function matches( $context ) {
		if ( false === parent::matches( $context ) ) {
			return false;
		}

		if ( empty( $context['file'] ) ) {
			return false;
		}

		return true;
	}

	public function validate( $context ) {
		$field = $this->get_property( 'field' );
		$input = isset( $_FILES[$field]['size'] ) ? $_FILES[$field]['size'] : '';
		$input = wpcf7_array_flatten( $input );
		$input = wpcf7_exclude_blank( $input );

		if ( empty( $input ) ) {
			return true;
		}

		$threshold = $this->get_property( 'threshold' );

		if ( array_sum( $input ) < $threshold ) {
			return new WP_Error( 'wpcf7_invalid_minfilesize',
				$this->get_property( 'error' )
			);
		}

		return true;
	}

	public function to_array() {
		return array( 'rule' => self::rule_name ) + (array) $this->properties;
	}
}
swv/rules/file.php000064400000003012151330370000010131 0ustar00<?php

class WPCF7_SWV_FileRule extends WPCF7_SWV_Rule {

	const rule_name = 'file';

	public function matches( $context ) {
		if ( false === parent::matches( $context ) ) {
			return false;
		}

		if ( empty( $context['file'] ) ) {
			return false;
		}

		return true;
	}

	public function validate( $context ) {
		$field = $this->get_property( 'field' );
		$input = isset( $_FILES[$field]['name'] ) ? $_FILES[$field]['name'] : '';
		$input = wpcf7_array_flatten( $input );
		$input = wpcf7_exclude_blank( $input );

		$acceptable_filetypes = array();

		foreach ( (array) $this->get_property( 'accept' ) as $accept ) {
			if ( false === strpos( $accept, '/' ) ) {
				$acceptable_filetypes[] = strtolower( $accept );
			} else {
				foreach ( wpcf7_convert_mime_to_ext( $accept ) as $ext ) {
					$acceptable_filetypes[] = sprintf(
						'.%s',
						strtolower( trim( $ext, ' .' ) )
					);
				}
			}
		}

		$acceptable_filetypes = array_unique( $acceptable_filetypes );

		foreach ( $input as $i ) {
			$last_period_pos = strrpos( $i, '.' );

			if ( false === $last_period_pos ) { // no period
				return new WP_Error( 'wpcf7_invalid_file',
					$this->get_property( 'error' )
				);
			}

			$suffix = strtolower( substr( $i, $last_period_pos ) );

			if ( ! in_array( $suffix, $acceptable_filetypes, true ) ) {
				return new WP_Error( 'wpcf7_invalid_file',
					$this->get_property( 'error' )
				);
			}
		}

		return true;
	}

	public function to_array() {
		return array( 'rule' => self::rule_name ) + (array) $this->properties;
	}
}
swv/rules/minlength.php000064400000001717151330370000011211 0ustar00<?php

class WPCF7_SWV_MinLengthRule extends WPCF7_SWV_Rule {

	const rule_name = 'minlength';

	public function matches( $context ) {
		if ( false === parent::matches( $context ) ) {
			return false;
		}

		if ( empty( $context['text'] ) ) {
			return false;
		}

		return true;
	}

	public function validate( $context ) {
		$field = $this->get_property( 'field' );
		$input = isset( $_POST[$field] ) ? $_POST[$field] : '';
		$input = wpcf7_array_flatten( $input );
		$input = wpcf7_exclude_blank( $input );

		if ( empty( $input ) ) {
			return true;
		}

		$total = 0;

		foreach ( $input as $i ) {
			$total += wpcf7_count_code_units( $i );
		}

		$threshold = (int) $this->get_property( 'threshold' );

		if ( $threshold <= $total ) {
			return true;
		} else {
			return new WP_Error( 'wpcf7_invalid_minlength',
				$this->get_property( 'error' )
			);
		}
	}

	public function to_array() {
		return array( 'rule' => self::rule_name ) + (array) $this->properties;
	}
}
swv/rules/email.php000064400000001432151330370000010305 0ustar00<?php

class WPCF7_SWV_EmailRule extends WPCF7_SWV_Rule {

	const rule_name = 'email';

	public function matches( $context ) {
		if ( false === parent::matches( $context ) ) {
			return false;
		}

		if ( empty( $context['text'] ) ) {
			return false;
		}

		return true;
	}

	public function validate( $context ) {
		$field = $this->get_property( 'field' );
		$input = isset( $_POST[$field] ) ? $_POST[$field] : '';
		$input = wpcf7_array_flatten( $input );
		$input = wpcf7_exclude_blank( $input );

		foreach ( $input as $i ) {
			if ( ! wpcf7_is_email( $i ) ) {
				return new WP_Error( 'wpcf7_invalid_email',
					$this->get_property( 'error' )
				);
			}
		}

		return true;
	}

	public function to_array() {
		return array( 'rule' => self::rule_name ) + (array) $this->properties;
	}
}
swv/rules/maxlength.php000064400000001717151330370000011213 0ustar00<?php

class WPCF7_SWV_MaxLengthRule extends WPCF7_SWV_Rule {

	const rule_name = 'maxlength';

	public function matches( $context ) {
		if ( false === parent::matches( $context ) ) {
			return false;
		}

		if ( empty( $context['text'] ) ) {
			return false;
		}

		return true;
	}

	public function validate( $context ) {
		$field = $this->get_property( 'field' );
		$input = isset( $_POST[$field] ) ? $_POST[$field] : '';
		$input = wpcf7_array_flatten( $input );
		$input = wpcf7_exclude_blank( $input );

		if ( empty( $input ) ) {
			return true;
		}

		$total = 0;

		foreach ( $input as $i ) {
			$total += wpcf7_count_code_units( $i );
		}

		$threshold = (int) $this->get_property( 'threshold' );

		if ( $total <= $threshold ) {
			return true;
		} else {
			return new WP_Error( 'wpcf7_invalid_maxlength',
				$this->get_property( 'error' )
			);
		}
	}

	public function to_array() {
		return array( 'rule' => self::rule_name ) + (array) $this->properties;
	}
}
swv/rules/maxfilesize.php000064400000001606151330370000011541 0ustar00<?php

class WPCF7_SWV_MaxFileSizeRule extends WPCF7_SWV_Rule {

	const rule_name = 'maxfilesize';

	public function matches( $context ) {
		if ( false === parent::matches( $context ) ) {
			return false;
		}

		if ( empty( $context['file'] ) ) {
			return false;
		}

		return true;
	}

	public function validate( $context ) {
		$field = $this->get_property( 'field' );
		$input = isset( $_FILES[$field]['size'] ) ? $_FILES[$field]['size'] : '';
		$input = wpcf7_array_flatten( $input );
		$input = wpcf7_exclude_blank( $input );

		if ( empty( $input ) ) {
			return true;
		}

		$threshold = $this->get_property( 'threshold' );

		if ( $threshold < array_sum( $input ) ) {
			return new WP_Error( 'wpcf7_invalid_maxfilesize',
				$this->get_property( 'error' )
			);
		}

		return true;
	}

	public function to_array() {
		return array( 'rule' => self::rule_name ) + (array) $this->properties;
	}
}
swv/rules/date.php000064400000001426151330370000010136 0ustar00<?php

class WPCF7_SWV_DateRule extends WPCF7_SWV_Rule {

	const rule_name = 'date';

	public function matches( $context ) {
		if ( false === parent::matches( $context ) ) {
			return false;
		}

		if ( empty( $context['text'] ) ) {
			return false;
		}

		return true;
	}

	public function validate( $context ) {
		$field = $this->get_property( 'field' );
		$input = isset( $_POST[$field] ) ? $_POST[$field] : '';
		$input = wpcf7_array_flatten( $input );
		$input = wpcf7_exclude_blank( $input );

		foreach ( $input as $i ) {
			if ( ! wpcf7_is_date( $i ) ) {
				return new WP_Error( 'wpcf7_invalid_date',
					$this->get_property( 'error' )
				);
			}
		}

		return true;
	}

	public function to_array() {
		return array( 'rule' => self::rule_name ) + (array) $this->properties;
	}
}
swv/rules/maxdate.php000064400000001643151330370000010645 0ustar00<?php

class WPCF7_SWV_MaxDateRule extends WPCF7_SWV_Rule {

	const rule_name = 'maxdate';

	public function matches( $context ) {
		if ( false === parent::matches( $context ) ) {
			return false;
		}

		if ( empty( $context['text'] ) ) {
			return false;
		}

		return true;
	}

	public function validate( $context ) {
		$field = $this->get_property( 'field' );
		$input = isset( $_POST[$field] ) ? $_POST[$field] : '';
		$input = wpcf7_array_flatten( $input );
		$input = wpcf7_exclude_blank( $input );

		$threshold = $this->get_property( 'threshold' );

		if ( ! wpcf7_is_date( $threshold ) ) {
			return true;
		}

		foreach ( $input as $i ) {
			if ( wpcf7_is_date( $i ) and $threshold < $i ) {
				return new WP_Error( 'wpcf7_invalid_maxdate',
					$this->get_property( 'error' )
				);
			}
		}

		return true;
	}

	public function to_array() {
		return array( 'rule' => self::rule_name ) + (array) $this->properties;
	}
}
swv/rules/number.php000064400000001436151330370000010512 0ustar00<?php

class WPCF7_SWV_NumberRule extends WPCF7_SWV_Rule {

	const rule_name = 'number';

	public function matches( $context ) {
		if ( false === parent::matches( $context ) ) {
			return false;
		}

		if ( empty( $context['text'] ) ) {
			return false;
		}

		return true;
	}

	public function validate( $context ) {
		$field = $this->get_property( 'field' );
		$input = isset( $_POST[$field] ) ? $_POST[$field] : '';
		$input = wpcf7_array_flatten( $input );
		$input = wpcf7_exclude_blank( $input );

		foreach ( $input as $i ) {
			if ( ! wpcf7_is_number( $i ) ) {
				return new WP_Error( 'wpcf7_invalid_number',
					$this->get_property( 'error' )
				);
			}
		}

		return true;
	}

	public function to_array() {
		return array( 'rule' => self::rule_name ) + (array) $this->properties;
	}
}
swv/rules/url.php000064400000001422151330370000010017 0ustar00<?php

class WPCF7_SWV_URLRule extends WPCF7_SWV_Rule {

	const rule_name = 'url';

	public function matches( $context ) {
		if ( false === parent::matches( $context ) ) {
			return false;
		}

		if ( empty( $context['text'] ) ) {
			return false;
		}

		return true;
	}

	public function validate( $context ) {
		$field = $this->get_property( 'field' );
		$input = isset( $_POST[$field] ) ? $_POST[$field] : '';
		$input = wpcf7_array_flatten( $input );
		$input = wpcf7_exclude_blank( $input );

		foreach ( $input as $i ) {
			if ( ! wpcf7_is_url( $i ) ) {
				return new WP_Error( 'wpcf7_invalid_url',
					$this->get_property( 'error' )
				);
			}
		}

		return true;
	}

	public function to_array() {
		return array( 'rule' => self::rule_name ) + (array) $this->properties;
	}
}
swv/js/index.asset.php000064400000000121151330370000010717 0ustar00<?php

return array(
	'dependencies' => array(),
	'version' => WPCF7_VERSION,
);
swv/js/index.js000064400000024001151330370000007431 0ustar00(()=>{"use strict";var t={d:(e,s)=>{for(var i in s)t.o(s,i)&&!t.o(e,i)&&Object.defineProperty(e,i,{enumerable:!0,get:s[i]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},e={};function s(t){if(this.formData={},this.tree={},!(t instanceof FormData))return this;this.formData=t;const e=()=>{const t=new Map;return t.largestIndex=0,t.set=function(e,s){""===e?e=t.largestIndex++:/^[0-9]+$/.test(e)&&(e=parseInt(e),t.largestIndex<=e&&(t.largestIndex=e+1)),Map.prototype.set.call(t,e,s)},t};this.tree=e();const s=/^(?<name>[a-z][-a-z0-9_:]*)(?<array>(?:\[(?:[a-z][-a-z0-9_:]*|[0-9]*)\])*)/i;for(const[t,i]of this.formData){const o=t.match(s);if(o)if(""===o.groups.array)this.tree.set(o.groups.name,i);else{const t=[...o.groups.array.matchAll(/\[([a-z][-a-z0-9_:]*|[0-9]*)\]/gi)].map((([t,e])=>e));t.unshift(o.groups.name);const s=t.pop();t.reduce(((t,s)=>{if(/^[0-9]+$/.test(s)&&(s=parseInt(s)),t.get(s)instanceof Map)return t.get(s);const i=e();return t.set(s,i),i}),this.tree).set(s,i)}}}t.r(e),t.d(e,{date:()=>d,email:()=>r,enum:()=>f,file:()=>m,maxdate:()=>b,maxfilesize:()=>z,maxitems:()=>u,maxlength:()=>v,maxnumber:()=>x,mindate:()=>y,minfilesize:()=>A,minitems:()=>h,minlength:()=>g,minnumber:()=>w,number:()=>c,required:()=>n,requiredfile:()=>a,tel:()=>l,url:()=>p}),s.prototype.entries=function(){return this.tree.entries()},s.prototype.get=function(t){return this.tree.get(t)},s.prototype.getAll=function(t){if(!this.has(t))return[];const e=t=>{const s=[];if(t instanceof Map)for(const[i,o]of t)s.push(...e(o));else""!==t&&s.push(t);return s};return e(this.get(t))},s.prototype.has=function(t){return this.tree.has(t)},s.prototype.keys=function(){return this.tree.keys()},s.prototype.values=function(){return this.tree.values()};const i=s;function o({rule:t,field:e,error:s,...i}){this.rule=t,this.field=e,this.error=s,this.properties=i}const n=function(t){if(0===t.getAll(this.field).length)throw new o(this)},a=function(t){if(0===t.getAll(this.field).length)throw new o(this)},r=function(t){if(!t.getAll(this.field).every((t=>{if((t=t.trim()).length<6)return!1;if(-1===t.indexOf("@",1))return!1;if(t.indexOf("@")!==t.lastIndexOf("@"))return!1;const[e,s]=t.split("@",2);if(!/^[a-zA-Z0-9!#$%&\'*+\/=?^_`{|}~\.-]+$/.test(e))return!1;if(/\.{2,}/.test(s))return!1;if(/(?:^[ \t\n\r\0\x0B.]|[ \t\n\r\0\x0B.]$)/.test(s))return!1;const i=s.split(".");if(i.length<2)return!1;for(const t of i){if(/(?:^[ \t\n\r\0\x0B-]|[ \t\n\r\0\x0B-]$)/.test(t))return!1;if(!/^[a-z0-9-]+$/i.test(t))return!1}return!0})))throw new o(this)},p=function(t){const e=t.getAll(this.field);if(!e.every((t=>{if(""===(t=t.trim()))return!1;try{return(t=>-1!==["http","https","ftp","ftps","mailto","news","irc","irc6","ircs","gopher","nntp","feed","telnet","mms","rtsp","sms","svn","tel","fax","xmpp","webcal","urn"].indexOf(t))(new URL(t).protocol.replace(/:$/,""))}catch{return!1}})))throw new o(this)},l=function(t){if(!t.getAll(this.field).every((t=>(t=(t=t.trim()).replaceAll(/[()/.*#\s-]+/g,""),/^[+]?[0-9]+$/.test(t)))))throw new o(this)},c=function(t){if(!t.getAll(this.field).every((t=>(t=t.trim(),!!/^[-]?[0-9]+(?:[eE][+-]?[0-9]+)?$/.test(t)||!!/^[-]?(?:[0-9]+)?[.][0-9]+(?:[eE][+-]?[0-9]+)?$/.test(t)))))throw new o(this)},d=function(t){if(!t.getAll(this.field).every((t=>/^[0-9]{4,}-[0-9]{2}-[0-9]{2}$/.test(t.trim()))))throw new o(this)},m=function(t){if(!t.getAll(this.field).every((t=>t instanceof File&&this.accept?.some((e=>/^\.[a-z0-9]+$/i.test(e)?t.name.toLowerCase().endsWith(e.toLowerCase()):(t=>{const e=[],s=t.match(/^(?<toplevel>[a-z]+)\/(?<sub>[*]|[a-z0-9.+-]+)$/i);if(s){const t=s.groups.toplevel.toLowerCase(),i=s.groups.sub.toLowerCase();for(const[o,n]of(()=>{const t=new Map;return t.set("jpg|jpeg|jpe","image/jpeg"),t.set("gif","image/gif"),t.set("png","image/png"),t.set("bmp","image/bmp"),t.set("tiff|tif","image/tiff"),t.set("webp","image/webp"),t.set("ico","image/x-icon"),t.set("heic","image/heic"),t.set("asf|asx","video/x-ms-asf"),t.set("wmv","video/x-ms-wmv"),t.set("wmx","video/x-ms-wmx"),t.set("wm","video/x-ms-wm"),t.set("avi","video/avi"),t.set("divx","video/divx"),t.set("flv","video/x-flv"),t.set("mov|qt","video/quicktime"),t.set("mpeg|mpg|mpe","video/mpeg"),t.set("mp4|m4v","video/mp4"),t.set("ogv","video/ogg"),t.set("webm","video/webm"),t.set("mkv","video/x-matroska"),t.set("3gp|3gpp","video/3gpp"),t.set("3g2|3gp2","video/3gpp2"),t.set("txt|asc|c|cc|h|srt","text/plain"),t.set("csv","text/csv"),t.set("tsv","text/tab-separated-values"),t.set("ics","text/calendar"),t.set("rtx","text/richtext"),t.set("css","text/css"),t.set("htm|html","text/html"),t.set("vtt","text/vtt"),t.set("dfxp","application/ttaf+xml"),t.set("mp3|m4a|m4b","audio/mpeg"),t.set("aac","audio/aac"),t.set("ra|ram","audio/x-realaudio"),t.set("wav","audio/wav"),t.set("ogg|oga","audio/ogg"),t.set("flac","audio/flac"),t.set("mid|midi","audio/midi"),t.set("wma","audio/x-ms-wma"),t.set("wax","audio/x-ms-wax"),t.set("mka","audio/x-matroska"),t.set("rtf","application/rtf"),t.set("js","application/javascript"),t.set("pdf","application/pdf"),t.set("swf","application/x-shockwave-flash"),t.set("class","application/java"),t.set("tar","application/x-tar"),t.set("zip","application/zip"),t.set("gz|gzip","application/x-gzip"),t.set("rar","application/rar"),t.set("7z","application/x-7z-compressed"),t.set("exe","application/x-msdownload"),t.set("psd","application/octet-stream"),t.set("xcf","application/octet-stream"),t.set("doc","application/msword"),t.set("pot|pps|ppt","application/vnd.ms-powerpoint"),t.set("wri","application/vnd.ms-write"),t.set("xla|xls|xlt|xlw","application/vnd.ms-excel"),t.set("mdb","application/vnd.ms-access"),t.set("mpp","application/vnd.ms-project"),t.set("docx","application/vnd.openxmlformats-officedocument.wordprocessingml.document"),t.set("docm","application/vnd.ms-word.document.macroEnabled.12"),t.set("dotx","application/vnd.openxmlformats-officedocument.wordprocessingml.template"),t.set("dotm","application/vnd.ms-word.template.macroEnabled.12"),t.set("xlsx","application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),t.set("xlsm","application/vnd.ms-excel.sheet.macroEnabled.12"),t.set("xlsb","application/vnd.ms-excel.sheet.binary.macroEnabled.12"),t.set("xltx","application/vnd.openxmlformats-officedocument.spreadsheetml.template"),t.set("xltm","application/vnd.ms-excel.template.macroEnabled.12"),t.set("xlam","application/vnd.ms-excel.addin.macroEnabled.12"),t.set("pptx","application/vnd.openxmlformats-officedocument.presentationml.presentation"),t.set("pptm","application/vnd.ms-powerpoint.presentation.macroEnabled.12"),t.set("ppsx","application/vnd.openxmlformats-officedocument.presentationml.slideshow"),t.set("ppsm","application/vnd.ms-powerpoint.slideshow.macroEnabled.12"),t.set("potx","application/vnd.openxmlformats-officedocument.presentationml.template"),t.set("potm","application/vnd.ms-powerpoint.template.macroEnabled.12"),t.set("ppam","application/vnd.ms-powerpoint.addin.macroEnabled.12"),t.set("sldx","application/vnd.openxmlformats-officedocument.presentationml.slide"),t.set("sldm","application/vnd.ms-powerpoint.slide.macroEnabled.12"),t.set("onetoc|onetoc2|onetmp|onepkg","application/onenote"),t.set("oxps","application/oxps"),t.set("xps","application/vnd.ms-xpsdocument"),t.set("odt","application/vnd.oasis.opendocument.text"),t.set("odp","application/vnd.oasis.opendocument.presentation"),t.set("ods","application/vnd.oasis.opendocument.spreadsheet"),t.set("odg","application/vnd.oasis.opendocument.graphics"),t.set("odc","application/vnd.oasis.opendocument.chart"),t.set("odb","application/vnd.oasis.opendocument.database"),t.set("odf","application/vnd.oasis.opendocument.formula"),t.set("wp|wpd","application/wordperfect"),t.set("key","application/vnd.apple.keynote"),t.set("numbers","application/vnd.apple.numbers"),t.set("pages","application/vnd.apple.pages"),t})())("*"===i&&n.startsWith(t+"/")||n===s[0])&&e.push(...o.split("|"))}return e})(e).some((e=>(e="."+e.trim(),t.name.toLowerCase().endsWith(e.toLowerCase())))))))))throw new o(this)},f=function(t){if(!t.getAll(this.field).every((t=>this.accept?.some((e=>t===String(e))))))throw new o(this)},h=function(t){if(t.getAll(this.field).length<parseInt(this.threshold))throw new o(this)},u=function(t){const e=t.getAll(this.field);if(parseInt(this.threshold)<e.length)throw new o(this)},g=function(t){const e=t.getAll(this.field);let s=0;if(e.forEach((t=>{"string"==typeof t&&(s+=t.length)})),0!==s&&s<parseInt(this.threshold))throw new o(this)},v=function(t){const e=t.getAll(this.field);let s=0;if(e.forEach((t=>{"string"==typeof t&&(s+=t.length)})),parseInt(this.threshold)<s)throw new o(this)},w=function(t){if(!t.getAll(this.field).every((t=>!(parseFloat(t)<parseFloat(this.threshold)))))throw new o(this)},x=function(t){if(!t.getAll(this.field).every((t=>!(parseFloat(this.threshold)<parseFloat(t)))))throw new o(this)},y=function(t){if(!t.getAll(this.field).every((t=>(t=t.trim(),!(/^[0-9]{4,}-[0-9]{2}-[0-9]{2}$/.test(t)&&/^[0-9]{4,}-[0-9]{2}-[0-9]{2}$/.test(this.threshold)&&t<this.threshold)))))throw new o(this)},b=function(t){if(!t.getAll(this.field).every((t=>(t=t.trim(),!(/^[0-9]{4,}-[0-9]{2}-[0-9]{2}$/.test(t)&&/^[0-9]{4,}-[0-9]{2}-[0-9]{2}$/.test(this.threshold)&&this.threshold<t)))))throw new o(this)},A=function(t){const e=t.getAll(this.field);let s=0;if(e.forEach((t=>{t instanceof File&&(s+=t.size)})),s<parseInt(this.threshold))throw new o(this)},z=function(t){const e=t.getAll(this.field);let s=0;if(e.forEach((t=>{t instanceof File&&(s+=t.size)})),parseInt(this.threshold)<s)throw new o(this)};var $;window.swv={validators:e,validate:(t,s,n={})=>{const a=(t.rules??[]).filter((({rule:t,...s})=>"function"==typeof e[t]&&("function"!=typeof e[t].matches||e[t].matches(s,n))));if(!a.length)return new Map;const r=new i(s),p=a.reduce(((t,s)=>{const{rule:i,...n}=s;if(t.get(n.field)?.error)return t;try{e[i].call({rule:i,...n},r)}catch(e){if(e instanceof o)return t.set(n.field,e)}return t}),new Map);for(const t of r.keys())p.has(t)||p.set(t,{validInputs:r.getAll(t)});return p},...null!==($=window.swv)&&void 0!==$?$:{}}})();swv/swv.php000064400000013376151330370000006715 0ustar00<?php
/**
 * Schema-Woven Validation API
 */

require_once WPCF7_PLUGIN_DIR . '/includes/swv/schema-holder.php';
require_once WPCF7_PLUGIN_DIR . '/includes/swv/script-loader.php';


/**
 * Returns an associative array of SWV rules.
 */
function wpcf7_swv_available_rules() {
	$rules = array(
		'required' => 'WPCF7_SWV_RequiredRule',
		'requiredfile' => 'WPCF7_SWV_RequiredFileRule',
		'email' => 'WPCF7_SWV_EmailRule',
		'url' => 'WPCF7_SWV_URLRule',
		'tel' => 'WPCF7_SWV_TelRule',
		'number' => 'WPCF7_SWV_NumberRule',
		'date' => 'WPCF7_SWV_DateRule',
		'file' => 'WPCF7_SWV_FileRule',
		'enum' => 'WPCF7_SWV_EnumRule',
		'minitems' => 'WPCF7_SWV_MinItemsRule',
		'maxitems' => 'WPCF7_SWV_MaxItemsRule',
		'minlength' => 'WPCF7_SWV_MinLengthRule',
		'maxlength' => 'WPCF7_SWV_MaxLengthRule',
		'minnumber' => 'WPCF7_SWV_MinNumberRule',
		'maxnumber' => 'WPCF7_SWV_MaxNumberRule',
		'mindate' => 'WPCF7_SWV_MinDateRule',
		'maxdate' => 'WPCF7_SWV_MaxDateRule',
		'minfilesize' => 'WPCF7_SWV_MinFileSizeRule',
		'maxfilesize' => 'WPCF7_SWV_MaxFileSizeRule',
	);

	return apply_filters( 'wpcf7_swv_available_rules', $rules );
}


add_action( 'wpcf7_init', 'wpcf7_swv_load_rules', 10, 0 );

/**
 * Loads SWV fules.
 */
function wpcf7_swv_load_rules() {
	$rules = wpcf7_swv_available_rules();

	foreach ( array_keys( $rules ) as $rule ) {
		$file = sprintf( '%s.php', $rule );
		$path = path_join( WPCF7_PLUGIN_DIR . '/includes/swv/rules', $file );

		if ( file_exists( $path ) ) {
			include_once $path;
		}
	}
}


/**
 * Creates an SWV rule object.
 *
 * @param string $rule_name Rule name.
 * @param string|array $properties Optional. Rule properties.
 * @return WPCF7_SWV_Rule|null The rule object, or null if it failed.
 */
function wpcf7_swv_create_rule( $rule_name, $properties = '' ) {
	$rules = wpcf7_swv_available_rules();

	if ( isset( $rules[$rule_name] ) ) {
		return new $rules[$rule_name]( $properties );
	}
}


/**
 * Returns an associative array of JSON Schema for Contact Form 7 SWV.
 */
function wpcf7_swv_get_meta_schema() {
	return array(
		'$schema' => 'https://json-schema.org/draft/2020-12/schema',
		'title' => 'Contact Form 7 SWV',
		'description' => 'Contact Form 7 SWV meta-schema',
		'type' => 'object',
		'properties' => array(
			'version' => array(
				'type' => 'string',
			),
			'locale' => array(
				'type' => 'string',
			),
			'rules' => array(
				'type' => 'array',
				'items' => array(
					'type' => 'object',
					'properties' => array(
						'rule' => array(
							'type' => 'string',
							'enum' => array_keys( wpcf7_swv_available_rules() ),
						),
						'field' => array(
							'type' => 'string',
							'pattern' => '^[A-Za-z][-A-Za-z0-9_:]*$',
						),
						'error' => array(
							'type' => 'string',
						),
						'accept' => array(
							'type' => 'array',
							'items' => array(
								'type' => 'string',
							),
						),
						'threshold' => array(
							'type' => 'string',
						),
					),
					'required' => array( 'rule' ),
				),
			),
		),
	);
}


/**
 * The base class of SWV rules.
 */
abstract class WPCF7_SWV_Rule {

	protected $properties = array();

	public function __construct( $properties = '' ) {
		$this->properties = wp_parse_args( $properties, array() );
	}


	/**
	 * Returns true if this rule matches the given context.
	 *
	 * @param array $context Context.
	 */
	public function matches( $context ) {
		$field = $this->get_property( 'field' );

		if ( ! empty( $context['field'] ) ) {
			if ( $field and ! in_array( $field, (array) $context['field'], true ) ) {
				return false;
			}
		}

		return true;
	}


	/**
	 * Validates with this rule's logic.
	 *
	 * @param array $context Context.
	 */
	public function validate( $context ) {
		return true;
	}


	/**
	 * Converts the properties to an array.
	 *
	 * @return array Array of properties.
	 */
	public function to_array() {
		return (array) $this->properties;
	}


	/**
	 * Returns the property value specified by the given property name.
	 *
	 * @param string $name Property name.
	 * @return mixed Property value.
	 */
	public function get_property( $name ) {
		if ( isset( $this->properties[$name] ) ) {
			return $this->properties[$name];
		}
	}

}


/**
 * The base class of SWV composite rules.
 */
abstract class WPCF7_SWV_CompositeRule extends WPCF7_SWV_Rule {

	protected $rules = array();


	/**
	 * Adds a sub-rule to this composite rule.
	 *
	 * @param WPCF7_SWV_Rule $rule Sub-rule to be added.
	 */
	public function add_rule( $rule ) {
		if ( $rule instanceof WPCF7_SWV_Rule ) {
			$this->rules[] = $rule;
		}
	}


	/**
	 * Returns an iterator of sub-rules.
	 */
	public function rules() {
		foreach ( $this->rules as $rule ) {
			yield $rule;
		}
	}


	/**
	 * Returns true if this rule matches the given context.
	 *
	 * @param array $context Context.
	 */
	public function matches( $context ) {
		return true;
	}


	/**
	 * Validates with this rule's logic.
	 *
	 * @param array $context Context.
	 */
	public function validate( $context ) {
		foreach ( $this->rules() as $rule ) {
			if ( $rule->matches( $context ) ) {
				$result = $rule->validate( $context );

				if ( is_wp_error( $result ) ) {
					return $result;
				}
			}
		}

		return true;
	}


	/**
	 * Converts the properties to an array.
	 *
	 * @return array Array of properties.
	 */
	public function to_array() {
		$rules_arrays = array_map(
			static function ( $rule ) {
				return $rule->to_array();
			},
			$this->rules
		);

		return array_merge(
			parent::to_array(),
			array(
				'rules' => $rules_arrays,
			)
		);
	}

}


/**
 * The schema class as a composite rule.
 */
class WPCF7_SWV_Schema extends WPCF7_SWV_CompositeRule {

	const version = 'Contact Form 7 SWV Schema 2022-10';

	public function __construct( $properties = '' ) {
		$this->properties = wp_parse_args( $properties, array(
			'version' => self::version,
		) );
	}

}
rest-api.php000064400000031270151330370000006774 0ustar00<?php

add_action(
	'rest_api_init',
	static function () {
		$controller = new WPCF7_REST_Controller;
		$controller->register_routes();
	},
	10, 0
);


class WPCF7_REST_Controller {

	const route_namespace = 'contact-form-7/v1';

	public function register_routes() {

		register_rest_route( self::route_namespace,
			'/contact-forms',
			array(
				array(
					'methods' => WP_REST_Server::READABLE,
					'callback' => array( $this, 'get_contact_forms' ),
					'permission_callback' => static function () {
						if ( current_user_can( 'wpcf7_read_contact_forms' ) ) {
							return true;
						} else {
							return new WP_Error( 'wpcf7_forbidden',
								__( "You are not allowed to access contact forms.", 'contact-form-7' ),
								array( 'status' => 403 )
							);
						}
					},
				),
				array(
					'methods' => WP_REST_Server::CREATABLE,
					'callback' => array( $this, 'create_contact_form' ),
					'permission_callback' => static function () {
						if ( current_user_can( 'wpcf7_edit_contact_forms' ) ) {
							return true;
						} else {
							return new WP_Error( 'wpcf7_forbidden',
								__( "You are not allowed to create a contact form.", 'contact-form-7' ),
								array( 'status' => 403 )
							);
						}
					},
				),
			)
		);

		register_rest_route( self::route_namespace,
			'/contact-forms/(?P<id>\d+)',
			array(
				array(
					'methods' => WP_REST_Server::READABLE,
					'callback' => array( $this, 'get_contact_form' ),
					'permission_callback' => static function ( WP_REST_Request $request ) {
						$id = (int) $request->get_param( 'id' );

						if ( current_user_can( 'wpcf7_edit_contact_form', $id ) ) {
							return true;
						} else {
							return new WP_Error( 'wpcf7_forbidden',
								__( "You are not allowed to access the requested contact form.", 'contact-form-7' ),
								array( 'status' => 403 )
							);
						}
					},
				),
				array(
					'methods' => WP_REST_Server::EDITABLE,
					'callback' => array( $this, 'update_contact_form' ),
					'permission_callback' => static function ( WP_REST_Request $request ) {
						$id = (int) $request->get_param( 'id' );

						if ( current_user_can( 'wpcf7_edit_contact_form', $id ) ) {
							return true;
						} else {
							return new WP_Error( 'wpcf7_forbidden',
								__( "You are not allowed to access the requested contact form.", 'contact-form-7' ),
								array( 'status' => 403 )
							);
						}
					},
				),
				array(
					'methods' => WP_REST_Server::DELETABLE,
					'callback' => array( $this, 'delete_contact_form' ),
					'permission_callback' => static function ( WP_REST_Request $request ) {
						$id = (int) $request->get_param( 'id' );

						if ( current_user_can( 'wpcf7_delete_contact_form', $id ) ) {
							return true;
						} else {
							return new WP_Error( 'wpcf7_forbidden',
								__( "You are not allowed to access the requested contact form.", 'contact-form-7' ),
								array( 'status' => 403 )
							);
						}
					},
				),
			)
		);

		register_rest_route( self::route_namespace,
			'/contact-forms/(?P<id>\d+)/feedback',
			array(
				array(
					'methods' => WP_REST_Server::CREATABLE,
					'callback' => array( $this, 'create_feedback' ),
					'permission_callback' => '__return_true',
				),
			)
		);

		register_rest_route( self::route_namespace,
			'/contact-forms/(?P<id>\d+)/feedback/schema',
			array(
				array(
					'methods' => WP_REST_Server::READABLE,
					'callback' => array( $this, 'get_schema' ),
					'permission_callback' => '__return_true',
				),
				'schema' => 'wpcf7_swv_get_meta_schema',
			)
		);

		register_rest_route( self::route_namespace,
			'/contact-forms/(?P<id>\d+)/refill',
			array(
				array(
					'methods' => WP_REST_Server::READABLE,
					'callback' => array( $this, 'get_refill' ),
					'permission_callback' => '__return_true',
				),
			)
		);
	}

	public function get_contact_forms( WP_REST_Request $request ) {
		$args = array();

		$per_page = $request->get_param( 'per_page' );

		if ( null !== $per_page ) {
			$args['posts_per_page'] = (int) $per_page;
		}

		$offset = $request->get_param( 'offset' );

		if ( null !== $offset ) {
			$args['offset'] = (int) $offset;
		}

		$order = $request->get_param( 'order' );

		if ( null !== $order ) {
			$args['order'] = (string) $order;
		}

		$orderby = $request->get_param( 'orderby' );

		if ( null !== $orderby ) {
			$args['orderby'] = (string) $orderby;
		}

		$search = $request->get_param( 'search' );

		if ( null !== $search ) {
			$args['s'] = (string) $search;
		}

		$items = WPCF7_ContactForm::find( $args );

		$response = array();

		foreach ( $items as $item ) {
			$response[] = array(
				'id' => $item->id(),
				'slug' => $item->name(),
				'title' => $item->title(),
				'locale' => $item->locale(),
			);
		}

		return rest_ensure_response( $response );
	}

	public function create_contact_form( WP_REST_Request $request ) {
		$id = (int) $request->get_param( 'id' );

		if ( $id ) {
			return new WP_Error( 'wpcf7_post_exists',
				__( "Cannot create existing contact form.", 'contact-form-7' ),
				array( 'status' => 400 )
			);
		}

		$args = $request->get_params();
		$args['id'] = -1; // Create
		$context = $request->get_param( 'context' );
		$item = wpcf7_save_contact_form( $args, $context );

		if ( ! $item ) {
			return new WP_Error( 'wpcf7_cannot_save',
				__( "There was an error saving the contact form.", 'contact-form-7' ),
				array( 'status' => 500 )
			);
		}

		$response = array(
			'id' => $item->id(),
			'slug' => $item->name(),
			'title' => $item->title(),
			'locale' => $item->locale(),
			'properties' => $this->get_properties( $item ),
			'config_errors' => array(),
		);

		if ( wpcf7_validate_configuration() ) {
			$config_validator = new WPCF7_ConfigValidator( $item );
			$config_validator->validate();

			$response['config_errors'] = $config_validator->collect_error_messages();

			if ( 'save' == $context ) {
				$config_validator->save();
			}
		}

		return rest_ensure_response( $response );
	}

	public function get_contact_form( WP_REST_Request $request ) {
		$id = (int) $request->get_param( 'id' );
		$item = wpcf7_contact_form( $id );

		if ( ! $item ) {
			return new WP_Error( 'wpcf7_not_found',
				__( "The requested contact form was not found.", 'contact-form-7' ),
				array( 'status' => 404 )
			);
		}

		$response = array(
			'id' => $item->id(),
			'slug' => $item->name(),
			'title' => $item->title(),
			'locale' => $item->locale(),
			'properties' => $this->get_properties( $item ),
		);

		return rest_ensure_response( $response );
	}

	public function update_contact_form( WP_REST_Request $request ) {
		$id = (int) $request->get_param( 'id' );
		$item = wpcf7_contact_form( $id );

		if ( ! $item ) {
			return new WP_Error( 'wpcf7_not_found',
				__( "The requested contact form was not found.", 'contact-form-7' ),
				array( 'status' => 404 )
			);
		}

		$args = $request->get_params();
		$context = $request->get_param( 'context' );
		$item = wpcf7_save_contact_form( $args, $context );

		if ( ! $item ) {
			return new WP_Error( 'wpcf7_cannot_save',
				__( "There was an error saving the contact form.", 'contact-form-7' ),
				array( 'status' => 500 )
			);
		}

		$response = array(
			'id' => $item->id(),
			'slug' => $item->name(),
			'title' => $item->title(),
			'locale' => $item->locale(),
			'properties' => $this->get_properties( $item ),
			'config_errors' => array(),
		);

		if ( wpcf7_validate_configuration() ) {
			$config_validator = new WPCF7_ConfigValidator( $item );
			$config_validator->validate();

			$response['config_errors'] = $config_validator->collect_error_messages();

			if ( 'save' == $context ) {
				$config_validator->save();
			}
		}

		return rest_ensure_response( $response );
	}

	public function delete_contact_form( WP_REST_Request $request ) {
		$id = (int) $request->get_param( 'id' );
		$item = wpcf7_contact_form( $id );

		if ( ! $item ) {
			return new WP_Error( 'wpcf7_not_found',
				__( "The requested contact form was not found.", 'contact-form-7' ),
				array( 'status' => 404 )
			);
		}

		$result = $item->delete();

		if ( ! $result ) {
			return new WP_Error( 'wpcf7_cannot_delete',
				__( "There was an error deleting the contact form.", 'contact-form-7' ),
				array( 'status' => 500 )
			);
		}

		$response = array( 'deleted' => true );

		return rest_ensure_response( $response );
	}

	public function create_feedback( WP_REST_Request $request ) {
		$content_type = $request->get_header( 'Content-Type' );

		if ( ! str_starts_with( $content_type, 'multipart/form-data' ) ) {
			return new WP_Error( 'wpcf7_unsupported_media_type',
				__( "The request payload format is not supported.", 'contact-form-7' ),
				array( 'status' => 415 )
			);
		}

		$url_params = $request->get_url_params();

		$item = null;

		if ( ! empty( $url_params['id'] ) ) {
			$item = wpcf7_contact_form( $url_params['id'] );
		}

		if ( ! $item ) {
			return new WP_Error( 'wpcf7_not_found',
				__( "The requested contact form was not found.", 'contact-form-7' ),
				array( 'status' => 404 )
			);
		}

		$unit_tag = wpcf7_sanitize_unit_tag(
			$request->get_param( '_wpcf7_unit_tag' )
		);

		$result = $item->submit();

		$response = array_merge( $result, array(
			'into' => sprintf( '#%s', $unit_tag ),
			'invalid_fields' => array(),
		) );

		if ( ! empty( $result['invalid_fields'] ) ) {
			$invalid_fields = array();

			foreach ( (array) $result['invalid_fields'] as $name => $field ) {
				if ( ! wpcf7_is_name( $name ) ) {
					continue;
				}

				$name = strtr( $name, '.', '_' );

				$invalid_fields[] = array(
					'field' => $name,
					'message' => $field['reason'],
					'idref' => $field['idref'],
					'error_id' => sprintf(
						'%1$s-ve-%2$s',
						$unit_tag,
						$name
					),
				);
			}

			$response['invalid_fields'] = $invalid_fields;
		}

		$response = wpcf7_apply_filters_deprecated(
			'wpcf7_ajax_json_echo',
			array( $response, $result ),
			'5.2',
			'wpcf7_feedback_response'
		);

		$response = apply_filters( 'wpcf7_feedback_response', $response, $result );

		return rest_ensure_response( $response );
	}


	public function get_schema( WP_REST_Request $request ) {
		$url_params = $request->get_url_params();

		$item = null;

		if ( ! empty( $url_params['id'] ) ) {
			$item = wpcf7_contact_form( $url_params['id'] );
		}

		if ( ! $item ) {
			return new WP_Error( 'wpcf7_not_found',
				__( "The requested contact form was not found.", 'contact-form-7' ),
				array( 'status' => 404 )
			);
		}

		$schema = $item->get_schema();

		$response = isset( $schema ) ? $schema->to_array() : array();

		return rest_ensure_response( $response );
	}


	public function get_refill( WP_REST_Request $request ) {
		$id = (int) $request->get_param( 'id' );
		$item = wpcf7_contact_form( $id );

		if ( ! $item ) {
			return new WP_Error( 'wpcf7_not_found',
				__( "The requested contact form was not found.", 'contact-form-7' ),
				array( 'status' => 404 )
			);
		}

		$response = wpcf7_apply_filters_deprecated(
			'wpcf7_ajax_onload',
			array( array() ),
			'5.2',
			'wpcf7_refill_response'
		);

		$response = apply_filters( 'wpcf7_refill_response', array() );

		return rest_ensure_response( $response );
	}

	private function get_properties( WPCF7_ContactForm $contact_form ) {
		$properties = $contact_form->get_properties();

		$properties['form'] = array(
			'content' => (string) $properties['form'],
			'fields' => array_map(
				static function ( WPCF7_FormTag $form_tag ) {
					return array(
						'type' => $form_tag->type,
						'basetype' => $form_tag->basetype,
						'name' => $form_tag->name,
						'options' => $form_tag->options,
						'raw_values' => $form_tag->raw_values,
						'labels' => $form_tag->labels,
						'values' => $form_tag->values,
						'pipes' => $form_tag->pipes instanceof WPCF7_Pipes
							? $form_tag->pipes->to_array()
							: $form_tag->pipes,
						'content' => $form_tag->content,
					);
				},
				$contact_form->scan_form_tags()
			),
		);

		$properties['additional_settings'] = array(
			'content' => (string) $properties['additional_settings'],
			'settings' => array_filter( array_map(
				static function ( $setting ) {
					$pattern = '/^([a-zA-Z0-9_]+)[\t ]*:(.*)$/';

					if ( preg_match( $pattern, $setting, $matches ) ) {
						$name = trim( $matches[1] );
						$value = trim( $matches[2] );

						if ( in_array( $value, array( 'on', 'true' ), true ) ) {
							$value = true;
						} elseif ( in_array( $value, array( 'off', 'false' ), true ) ) {
							$value = false;
						}

						return array( $name, $value );
					}

					return false;
				},
				explode( "\n", $properties['additional_settings'] )
			) ),
		);

		return $properties;
	}

	private function get_argument_schema() {
		return array(
			'id' => array(
				'description' => __( "Unique identifier for the contact form.", 'contact-form-7' ),
				'type' => 'integer',
				'required' => true,
			),
		);
	}

}
validation-functions.php000064400000012424151330370000011410 0ustar00<?php

/**
 * Checks whether a string is a valid NAME token.
 *
 * ID and NAME tokens must begin with a letter ([A-Za-z])
 * and may be followed by any number of letters, digits ([0-9]),
 * hyphens ("-"), underscores ("_"), colons (":"), and periods (".").
 *
 * @link http://www.w3.org/TR/html401/types.html#h-6.2
 *
 * @return bool True if it is a valid name, false if not.
 */
function wpcf7_is_name( $text ) {
	return preg_match( '/^[A-Za-z][-A-Za-z0-9_:.]*$/', $text );
}


/**
 * Checks whether the given text is a well-formed email address.
 */
function wpcf7_is_email( $text ) {
	$result = is_email( $text );
	return apply_filters( 'wpcf7_is_email', $result, $text );
}


/**
 * Checks whether the given text is a well-formed URL.
 */
function wpcf7_is_url( $text ) {
	$scheme = wp_parse_url( $text, PHP_URL_SCHEME );
	$result = $scheme && in_array( $scheme, wp_allowed_protocols(), true );
	return apply_filters( 'wpcf7_is_url', $result, $text );
}


/**
 * Checks whether the given text is a well-formed telephone number.
 */
function wpcf7_is_tel( $text ) {
	$text = preg_replace( '%[()/.*#\s-]+%', '', $text );
	$result = preg_match( '/^[+]?[0-9]+$/', $text );
	return apply_filters( 'wpcf7_is_tel', $result, $text );
}


/**
 * Checks whether the given text is a well-formed number.
 *
 * @link https://html.spec.whatwg.org/multipage/input.html#number-state-(type=number)
 */
function wpcf7_is_number( $text ) {
	$result = false;

	$patterns = array(
		'/^[-]?[0-9]+(?:[eE][+-]?[0-9]+)?$/',
		'/^[-]?(?:[0-9]+)?[.][0-9]+(?:[eE][+-]?[0-9]+)?$/',
	);

	foreach ( $patterns as $pattern ) {
		if ( preg_match( $pattern, $text ) ) {
			$result = true;
			break;
		}
	}

	return apply_filters( 'wpcf7_is_number', $result, $text );
}


/**
 * Checks whether the given text is a valid date.
 *
 * @link https://html.spec.whatwg.org/multipage/input.html#date-state-(type=date)
 */
function wpcf7_is_date( $text ) {
	$result = preg_match( '/^([0-9]{4,})-([0-9]{2})-([0-9]{2})$/', $text, $matches );

	if ( $result ) {
		$result = checkdate( $matches[2], $matches[3], $matches[1] );
	}

	return apply_filters( 'wpcf7_is_date', $result, $text );
}


/**
 * Checks whether the given text is a well-formed mailbox list.
 *
 * @param string|array $mailbox_list The subject to be checked.
 *                     Comma-separated string or an array of mailboxes.
 * @return array|bool Array of email addresses if all items are well-formed
 *                    mailbox, false if not.
 */
function wpcf7_is_mailbox_list( $mailbox_list ) {
	if ( ! is_array( $mailbox_list ) ) {
		$mailbox_text = (string) $mailbox_list;

		$mailbox_text = preg_replace(
			'/\\\\(?:\"|\')/',
			'esc-quote',
			$mailbox_text
		);

		$mailbox_text = preg_replace(
			'/(?:\".*?\"|\'.*?\')/',
			'quoted-string',
			$mailbox_text
		);

		$mailbox_list = explode( ',', $mailbox_text );
	}

	$addresses = array();

	foreach ( $mailbox_list as $mailbox ) {
		if ( ! is_string( $mailbox ) ) {
			return false;
		}

		$mailbox = trim( $mailbox );

		if ( '' === $mailbox ) {
			continue;
		}

		if ( preg_match( '/<(.+)>$/', $mailbox, $matches ) ) {
			$addr_spec = $matches[1];
		} else {
			$addr_spec = $mailbox;
		}

		if ( ! wpcf7_is_email( $addr_spec ) ) {
			return false;
		}

		$addresses[] = $addr_spec;
	}

	return $addresses;
}


/**
 * Checks whether an email address belongs to a domain.
 *
 * @param string $email A mailbox or a comma-separated list of mailboxes.
 * @param string $domain Internet domain name.
 * @return bool True if all of the email addresses belong to the domain,
 *              false if not.
 */
function wpcf7_is_email_in_domain( $email, $domain ) {
	$email_list = wpcf7_is_mailbox_list( $email );

	if ( false === $email_list ) {
		return false;
	}

	$domain = strtolower( $domain );

	foreach ( $email_list as $email ) {
		$email_domain = substr( $email, strrpos( $email, '@' ) + 1 );
		$email_domain = strtolower( $email_domain );
		$domain_parts = explode( '.', $domain );

		do {
			$site_domain = implode( '.', $domain_parts );

			if ( $site_domain == $email_domain ) {
				continue 2;
			}

			array_shift( $domain_parts );
		} while ( $domain_parts );

		return false;
	}

	return true;
}


/**
 * Checks whether an email address belongs to the site domain.
 */
function wpcf7_is_email_in_site_domain( $email ) {
	if ( wpcf7_is_localhost() ) {
		return true;
	}

	$homes = array(
		home_url(),
		network_home_url(),
	);

	$homes = array_unique( $homes );

	foreach ( $homes as $home ) {
		$sitename = wp_parse_url( $home, PHP_URL_HOST );

		if ( WP_Http::is_ip_address( $sitename ) ) {
			return true;
		}

		if ( wpcf7_is_email_in_domain( $email, $sitename ) ) {
			return true;
		}
	}

	return false;
}


/**
 * Verifies that a given file path is under the directories that WordPress
 * manages for user contents.
 *
 * Returns false if the file at the given path does not exist yet.
 *
 * @param string $path A file path.
 * @return bool True if the path is under the content directories,
 *              false otherwise.
 */
function wpcf7_is_file_path_in_content_dir( $path ) {
	if ( $real_path = realpath( $path ) ) {
		$path = $real_path;
	} else {
		return false;
	}

	if ( 0 === strpos( $path, realpath( WP_CONTENT_DIR ) ) ) {
		return true;
	}

	if ( defined( 'UPLOADS' )
	and 0 === strpos( $path, realpath( ABSPATH . UPLOADS ) ) ) {
		return true;
	}

	return false;
}
block-editor/index.asset.php000064400000000305151330370000012046 0ustar00<?php

return array(
	'dependencies' => array(
		'wp-api-fetch',
		'wp-block-editor',
		'wp-blocks',
		'wp-components',
		'wp-element',
		'wp-i18n',
		'wp-url',
	),
	'version' => WPCF7_VERSION,
);
block-editor/block.json000064400000001253151330370000011100 0ustar00{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 2,
	"name": "contact-form-7/contact-form-selector",
	"title": "Contact Form 7",
	"category": "widgets",
	"description": "Insert a contact form you have created with Contact Form 7.",
	"keywords": [ "form" ],
	"textdomain": "contact-form-7",
	"attributes": {
		"id": {
			"type": "integer"
		},
		"title": {
			"type": "string"
		},
		"htmlId": {
			"type": "string"
		},
		"htmlName": {
			"type": "string"
		},
		"htmlTitle": {
			"type": "string"
		},
		"htmlClass": {
			"type": "string"
		},
		"output": {
			"enum": [ "form", "raw_form" ],
			"default": "form"
		}
	},
	"editorScript": "file:./index.js"
}
block-editor/index.js000064400000012511151330370000010557 0ustar00(()=>{"use strict";var e={n:t=>{var l=t&&t.__esModule?()=>t.default:()=>t;return e.d(l,{a:l}),l},d:(t,l)=>{for(var a in l)e.o(l,a)&&!e.o(t,a)&&Object.defineProperty(t,a,{enumerable:!0,get:l[a]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};const t=window.wp.element,l=window.wp.i18n,a=window.wp.blocks,r=window.wp.blockEditor,o=(0,t.createElement)("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 242.5 239.46"},(0,t.createElement)("defs",null,(0,t.createElement)("clipPath",{id:"clip-path",transform:"translate(1.72)"},(0,t.createElement)("circle",{className:"cls-1",cx:"119.73",cy:"119.73",r:"116.15",fill:"none"}))),(0,t.createElement)("g",{id:"Layer_2","data-name":"Layer 2"},(0,t.createElement)("g",{id:"Layer_1","data-name":"Layer 1"},(0,t.createElement)("g",{className:"cls-2",clipPath:"url(#clip-path)"},(0,t.createElement)("circle",{className:"cls-3",cx:"121.45",cy:"119.73",r:"116.15",fill:"#33c6f4"}),(0,t.createElement)("path",{className:"cls-4",d:"M239.32,167.79c-53.41-24-108.37-91.46-113-94.55s-10.84.77-10.84.77c-3.87-6.19-10.06.77-10.06.77C76.77,123.55.14,170.11.14,170.11S36.94,237.79,122,237.79C208.48,237.79,239.32,167.79,239.32,167.79Z",transform:"translate(1.72)",fill:"#1b447e"}),(0,t.createElement)("path",{className:"cls-5",d:"M67.48,116.58s15.48-7,12.38,4.65-15.48,28.64-11.61,29.41S83,140.58,86.06,142.12s5.42.78,3.87,6.2-3.1,9.29,0,9.29,5.42-7,9.29-13.94,10.06-3.87,12.38-1.55,9.29,15.49,14.71,13.94,8.51-8.52,6.19-24,1.55-20.12,1.55-20.12,4.64-2.32,13.16,8.51,24,27.09,26.31,26.32-10.83-17.8-7.74-19.35,15.48,2.32,21.68,7.74c0,0,2.12,8.87,2.12.36L126.31,73.24,115.47,74l-10.06.77S80.64,111.94,67.48,116.58Z",transform:"translate(1.72)",fill:"#fff"}),(0,t.createElement)("path",{className:"cls-6",d:"M239.32,170.11c-53.41-24-108.37-93.78-113-96.87s-10.84.77-10.84.77c-3.87-6.19-10.06.77-10.06.77C76.77,123.55.14,170.11.14,170.11",transform:"translate(1.72)",fill:"none",stroke:"#221e1f",strokeMiterlimit:"10",strokeWidth:"8px"})),(0,t.createElement)("circle",{className:"cls-6",cx:"121.45",cy:"119.73",r:"116.15",fill:"none",stroke:"#1b447e",strokeMiterlimit:"10",strokeWidth:"8px"})))),n=window.wp.components,c=window.wp.apiFetch;var s=e.n(c);const m=window.wp.url,i=e=>{let t="[contact-form-7]";return e.id&&(t=t.replace(/\]$/,` id="${e.id}"]`)),e.title&&(t=t.replace(/\]$/,` title="${e.title}"]`)),e.htmlId&&(t=t.replace(/\]$/,` html_id="${e.htmlId}"]`)),e.htmlName&&(t=t.replace(/\]$/,` html_name="${e.htmlName}"]`)),e.htmlTitle&&(t=t.replace(/\]$/,` html_title="${e.htmlTitle}"]`)),e.htmlClass&&(t=t.replace(/\]$/,` html_class="${e.htmlClass}"]`)),"raw_form"===e.output&&(t=t.replace(/\]$/,` output="${e.output}"]`)),t},d=e=>{const t=ajaxurl.replace(/\/admin-ajax\.php$/,"/admin.php");return(0,m.addQueryArgs)(t,{page:"wpcf7",post:e.id,action:"edit"})},p={from:[{type:"shortcode",tag:"contact-form-7",attributes:{id:{type:"integer",shortcode:e=>{let{named:{id:t}}=e;return parseInt(t)}},title:{type:"string",shortcode:e=>{let{named:{title:t}}=e;return t}}}}],to:[{type:"block",blocks:["core/shortcode"],transform:e=>{const t=i(e);return(0,a.createBlock)("core/shortcode",{text:t})}}]};var h;window.wpcf7=null!==(h=window.wpcf7)&&void 0!==h?h:{contactForms:[]},(0,a.registerBlockType)("contact-form-7/contact-form-selector",{icon:o,transforms:p,edit:function(e){let{attributes:a,setAttributes:o}=e;const c=e=>e.reduce(((e,t)=>e.set(t.id,t)),new Map),[i,p]=(0,t.useState)((()=>{var e;return c(null!==(e=window.wpcf7.contactForms)&&void 0!==e?e:[])}));return(0,t.createElement)(t.Fragment,null,(0,t.createElement)(r.InspectorControls,null,a.id&&(0,t.createElement)(n.PanelBody,{title:a.title},(0,t.createElement)(n.ExternalLink,{href:d(a)},(0,l.__)("Edit this contact form","contact-form-7"))),a.id&&(0,t.createElement)(n.PanelBody,{title:(0,l.__)("Form attributes","contact-form-7"),initialOpen:!1},(0,t.createElement)(n.TextControl,{label:(0,l.__)("ID","contact-form-7"),value:a.htmlId,onChange:e=>o({htmlId:e}),help:(0,l.__)("Used for the id attribute value of the form element.","contact-form-7")}),(0,t.createElement)(n.TextControl,{label:(0,l.__)("Name","contact-form-7"),value:a.htmlName,onChange:e=>o({htmlName:e}),help:(0,l.__)("Used for the name attribute value of the form element.","contact-form-7")}),(0,t.createElement)(n.TextControl,{label:(0,l.__)("Title","contact-form-7"),value:a.htmlTitle,onChange:e=>o({htmlTitle:e}),help:(0,l.__)("Used for the aria-label attribute value of the form element.","contact-form-7")}),(0,t.createElement)(n.TextControl,{label:(0,l.__)("Class","contact-form-7"),value:a.htmlClass,onChange:e=>o({htmlClass:e}),help:(0,l.__)("Used for the class attribute value of the form element.","contact-form-7")}))),(0,t.createElement)("div",(0,r.useBlockProps)({className:"components-placeholder",style:{marginTop:"28px",marginBottom:"28px"}}),(0,t.createElement)(n.ComboboxControl,{label:(0,l.__)("Select a contact form:","contact-form-7"),options:(e=>{const t=[];for(const[l,a]of e)t.push({value:l,label:a.title});return t})(i),value:a.id,onChange:e=>{var t;return o({id:parseInt(e),title:null===(t=i.get(parseInt(e)))||void 0===t?void 0:t.title})},onFilterValueChange:e=>{(async e=>s()({path:(0,m.addQueryArgs)("/contact-form-7/v1/contact-forms",{posts_per_page:20,orderby:"modified",order:"DESC",...e})}).then((e=>e)))({search:e}).then((e=>{p(c(e))}))}})))},save:e=>{let{attributes:l}=e;const a=i(l);return(0,t.createElement)("div",r.useBlockProps.save(),a)}})})();block-editor/block-editor.php000064400000003067151330370000012207 0ustar00<?php

add_action(
	'init',
	'wpcf7_init_block_editor_assets',
	10, 0
);

function wpcf7_init_block_editor_assets() {
	$assets = array();

	$asset_file = wpcf7_plugin_path(
		'includes/block-editor/index.asset.php'
	);

	if ( file_exists( $asset_file ) ) {
		$assets = include( $asset_file );
	}

	$assets = wp_parse_args( $assets, array(
		'dependencies' => array(
			'wp-api-fetch',
			'wp-block-editor',
			'wp-blocks',
			'wp-components',
			'wp-element',
			'wp-i18n',
			'wp-url',
		),
		'version' => WPCF7_VERSION,
	) );

	wp_register_script(
		'contact-form-7-block-editor',
		wpcf7_plugin_url( 'includes/block-editor/index.js' ),
		$assets['dependencies'],
		$assets['version']
	);

	wp_set_script_translations(
		'contact-form-7-block-editor',
		'contact-form-7'
	);

	register_block_type(
		wpcf7_plugin_path( 'includes/block-editor' ),
		array(
			'editor_script' => 'contact-form-7-block-editor',
		)
	);
}


add_action(
	'enqueue_block_editor_assets',
	'wpcf7_enqueue_block_editor_assets',
	10, 0
);

function wpcf7_enqueue_block_editor_assets() {
	$contact_forms = array_map(
		static function ( $contact_form ) {
			return array(
				'id' => $contact_form->id(),
				'slug' => $contact_form->name(),
				'title' => $contact_form->title(),
				'locale' => $contact_form->locale(),
			);
		},
		WPCF7_ContactForm::find( array(
			'posts_per_page' => 20,
			'orderby' => 'modified',
			'order' => 'DESC',
		) )
	);

	wp_add_inline_script(
		'contact-form-7-block-editor',
		sprintf(
			'window.wpcf7 = {contactForms:%s};',
			json_encode( $contact_forms )
		),
		'before'
	);
}
mail.php000064400000036023151330370000006173 0ustar00<?php

/**
 * Class that represents an attempt to compose and send email.
 */
class WPCF7_Mail {

	private static $current = null;

	private $name = '';
	private $locale = '';
	private $template = array();
	private $use_html = false;
	private $exclude_blank = false;


	/**
	 * Returns the singleton instance of this class.
	 */
	public static function get_current() {
		return self::$current;
	}


	/**
	 * Composes and sends email based on the specified template.
	 *
	 * @param array $template Array of email template.
	 * @param string $name Optional name of the template, such as
	 *               'mail' or 'mail_2'. Default empty string.
	 * @return bool Whether the email was sent successfully.
	 */
	public static function send( $template, $name = '' ) {
		self::$current = new self( $name, $template );
		return self::$current->compose();
	}


	/**
	 * The constructor method.
	 *
	 * @param string $name The name of the email template.
	 *               Such as 'mail' or 'mail_2'.
	 * @param array $template Array of email template.
	 */
	private function __construct( $name, $template ) {
		$this->name = trim( $name );
		$this->use_html = ! empty( $template['use_html'] );
		$this->exclude_blank = ! empty( $template['exclude_blank'] );

		$this->template = wp_parse_args( $template, array(
			'subject' => '',
			'sender' => '',
			'body' => '',
			'recipient' => '',
			'additional_headers' => '',
			'attachments' => '',
		) );

		if ( $submission = WPCF7_Submission::get_instance() ) {
			$contact_form = $submission->get_contact_form();
			$this->locale = $contact_form->locale();
		}
	}


	/**
	 * Returns the name of the email template.
	 */
	public function name() {
		return $this->name;
	}


	/**
	 * Retrieves a component from the email template.
	 *
	 * @param string $component The name of the component.
	 * @param bool $replace_tags Whether to replace mail-tags
	 *             within the component.
	 * @return string The text representation of the email component.
	 */
	public function get( $component, $replace_tags = false ) {
		$use_html = ( $this->use_html && 'body' == $component );
		$exclude_blank = ( $this->exclude_blank && 'body' == $component );

		$template = $this->template;
		$component = isset( $template[$component] ) ? $template[$component] : '';

		if ( $replace_tags ) {
			$component = $this->replace_tags( $component, array(
				'html' => $use_html,
				'exclude_blank' => $exclude_blank,
			) );

			if ( $use_html ) {
				// Convert <example@example.com> to &lt;example@example.com&gt;.
				$component = preg_replace_callback(
					'/<(.*?)>/',
					static function ( $matches ) {
						if ( is_email( $matches[1] ) ) {
							return sprintf( '&lt;%s&gt;', $matches[1] );
						} else {
							return $matches[0];
						}
					},
					$component
				);

				if ( ! preg_match( '%<html[>\s].*</html>%is', $component ) ) {
					$component = $this->htmlize( $component );
				}
			}
		}

		return $component;
	}


	/**
	 * Creates HTML message body by adding the header and footer.
	 *
	 * @param string $body The body part of HTML.
	 * @return string Formatted HTML.
	 */
	private function htmlize( $body ) {
		if ( $this->locale ) {
			$lang_atts = sprintf( ' %s',
				wpcf7_format_atts( array(
					'dir' => wpcf7_is_rtl( $this->locale ) ? 'rtl' : 'ltr',
					'lang' => str_replace( '_', '-', $this->locale ),
				) )
			);
		} else {
			$lang_atts = '';
		}

		$header = apply_filters( 'wpcf7_mail_html_header',
			'<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml"' . $lang_atts . '>
<head>
<title>' . esc_html( $this->get( 'subject', true ) ) . '</title>
</head>
<body>
', $this );

		$footer = apply_filters( 'wpcf7_mail_html_footer',
			'</body>
</html>', $this );

		$html = $header . wpcf7_autop( $body ) . $footer;
		return $html;
	}


	/**
	 * Composes an email message and attempts to send it.
	 *
	 * @param bool $send Whether to attempt to send email. Default true.
	 */
	private function compose( $send = true ) {
		$components = array(
			'subject' => $this->get( 'subject', true ),
			'sender' => $this->get( 'sender', true ),
			'body' => $this->get( 'body', true ),
			'recipient' => $this->get( 'recipient', true ),
			'additional_headers' => $this->get( 'additional_headers', true ),
			'attachments' => $this->attachments(),
		);

		$components = apply_filters( 'wpcf7_mail_components',
			$components, wpcf7_get_current_contact_form(), $this
		);

		if ( ! $send ) {
			return $components;
		}

		$subject = wpcf7_strip_newline( $components['subject'] );
		$sender = wpcf7_strip_newline( $components['sender'] );
		$recipient = wpcf7_strip_newline( $components['recipient'] );
		$body = $components['body'];
		$additional_headers = trim( $components['additional_headers'] );

		$headers = "From: $sender\n";

		if ( $this->use_html ) {
			$headers .= "Content-Type: text/html\n";
			$headers .= "X-WPCF7-Content-Type: text/html\n";
		} else {
			$headers .= "X-WPCF7-Content-Type: text/plain\n";
		}

		if ( $additional_headers ) {
			$headers .= $additional_headers . "\n";
		}

		$attachments = array_filter(
			(array) $components['attachments'],
			function ( $attachment ) {
				$path = path_join( WP_CONTENT_DIR, $attachment );

				if ( ! wpcf7_is_file_path_in_content_dir( $path ) ) {
					if ( WP_DEBUG ) {
						trigger_error(
							sprintf(
								/* translators: %s: Attachment file path. */
								__( 'Failed to attach a file. %s is not in the allowed directory.', 'contact-form-7' ),
								$path
							),
							E_USER_NOTICE
						);
					}

					return false;
				}

				if ( ! is_readable( $path ) or ! is_file( $path ) ) {
					if ( WP_DEBUG ) {
						trigger_error(
							sprintf(
								/* translators: %s: Attachment file path. */
								__( 'Failed to attach a file. %s is not a readable file.', 'contact-form-7' ),
								$path
							),
							E_USER_NOTICE
						);
					}

					return false;
				}

				static $total_size = array();

				if ( ! isset( $total_size[$this->name] ) ) {
					$total_size[$this->name] = 0;
				}

				$file_size = (int) @filesize( $path );

				if ( 25 * MB_IN_BYTES < $total_size[$this->name] + $file_size ) {
					if ( WP_DEBUG ) {
						trigger_error(
							__( 'Failed to attach a file. The total file size exceeds the limit of 25 megabytes.', 'contact-form-7' ),
							E_USER_NOTICE
						);
					}

					return false;
				}

				$total_size[$this->name] += $file_size;

				return true;
			}
		);

		return wp_mail( $recipient, $subject, $body, $headers, $attachments );
	}


	/**
	 * Replaces mail-tags within the given text.
	 */
	public function replace_tags( $content, $args = '' ) {
		if ( true === $args ) {
			$args = array( 'html' => true );
		}

		$args = wp_parse_args( $args, array(
			'html' => false,
			'exclude_blank' => false,
		) );

		return wpcf7_mail_replace_tags( $content, $args );
	}


	/**
	 * Creates an array of attachments based on uploaded files and local files.
	 */
	private function attachments( $template = null ) {
		if ( ! $template ) {
			$template = $this->get( 'attachments' );
		}

		$attachments = array();

		if ( $submission = WPCF7_Submission::get_instance() ) {
			$uploaded_files = $submission->uploaded_files();

			foreach ( (array) $uploaded_files as $name => $paths ) {
				if ( false !== strpos( $template, "[{$name}]" ) ) {
					$attachments = array_merge( $attachments, (array) $paths );
				}
			}
		}

		foreach ( explode( "\n", $template ) as $line ) {
			$line = trim( $line );

			if ( '' === $line or '[' == substr( $line, 0, 1 ) ) {
				continue;
			}

			$attachments[] = path_join( WP_CONTENT_DIR, $line );
		}

		if ( $submission = WPCF7_Submission::get_instance() ) {
			$attachments = array_merge(
				$attachments,
				(array) $submission->extra_attachments( $this->name )
			);
		}

		return $attachments;
	}
}


/**
 * Replaces all mail-tags within the given text content.
 *
 * @param string $content Text including mail-tags.
 * @param string|array $args Optional. Output options.
 * @return string Result of replacement.
 */
function wpcf7_mail_replace_tags( $content, $args = '' ) {
	$args = wp_parse_args( $args, array(
		'html' => false,
		'exclude_blank' => false,
	) );

	if ( is_array( $content ) ) {
		foreach ( $content as $key => $value ) {
			$content[$key] = wpcf7_mail_replace_tags( $value, $args );
		}

		return $content;
	}

	$content = explode( "\n", $content );

	foreach ( $content as $num => $line ) {
		$line = new WPCF7_MailTaggedText( $line, $args );
		$replaced = $line->replace_tags();

		if ( $args['exclude_blank'] ) {
			$replaced_tags = $line->get_replaced_tags();

			if ( empty( $replaced_tags )
			or array_filter( $replaced_tags, 'strlen' ) ) {
				$content[$num] = $replaced;
			} else {
				unset( $content[$num] ); // Remove a line.
			}
		} else {
			$content[$num] = $replaced;
		}
	}

	$content = implode( "\n", $content );

	return $content;
}


add_action( 'phpmailer_init', 'wpcf7_phpmailer_init', 10, 1 );

/**
 * Adds custom properties to the PHPMailer object.
 */
function wpcf7_phpmailer_init( $phpmailer ) {
	$custom_headers = $phpmailer->getCustomHeaders();
	$phpmailer->clearCustomHeaders();
	$wpcf7_content_type = false;

	foreach ( (array) $custom_headers as $custom_header ) {
		$name = $custom_header[0];
		$value = $custom_header[1];

		if ( 'X-WPCF7-Content-Type' === $name ) {
			$wpcf7_content_type = trim( $value );
		} else {
			$phpmailer->addCustomHeader( $name, $value );
		}
	}

	if ( 'text/html' === $wpcf7_content_type ) {
		$phpmailer->msgHTML( $phpmailer->Body );
	} elseif ( 'text/plain' === $wpcf7_content_type ) {
		$phpmailer->AltBody = '';
	}
}


/**
 * Class that represents a single-line text including mail-tags.
 */
class WPCF7_MailTaggedText {

	private $html = false;
	private $callback = null;
	private $content = '';
	private $replaced_tags = array();


	/**
	 * The constructor method.
	 */
	public function __construct( $content, $args = '' ) {
		$args = wp_parse_args( $args, array(
			'html' => false,
			'callback' => null,
		) );

		$this->html = (bool) $args['html'];

		if ( null !== $args['callback']
		and is_callable( $args['callback'] ) ) {
			$this->callback = $args['callback'];
		} elseif ( $this->html ) {
			$this->callback = array( $this, 'replace_tags_callback_html' );
		} else {
			$this->callback = array( $this, 'replace_tags_callback' );
		}

		$this->content = $content;
	}


	/**
	 * Retrieves mail-tags that have been replaced by this instance.
	 *
	 * @return array List of mail-tags replaced.
	 */
	public function get_replaced_tags() {
		return $this->replaced_tags;
	}


	/**
	 * Replaces mail-tags based on regexp.
	 */
	public function replace_tags() {
		$regex = '/(\[?)\[[\t ]*'
			. '([a-zA-Z_][0-9a-zA-Z:._-]*)' // [2] = name
			. '((?:[\t ]+"[^"]*"|[\t ]+\'[^\']*\')*)' // [3] = values
			. '[\t ]*\](\]?)/';

		return preg_replace_callback( $regex, $this->callback, $this->content );
	}


	/**
	 * Callback function for replacement. For HTML message body.
	 */
	private function replace_tags_callback_html( $matches ) {
		return $this->replace_tags_callback( $matches, true );
	}


	/**
	 * Callback function for replacement.
	 */
	private function replace_tags_callback( $matches, $html = false ) {
		// allow [[foo]] syntax for escaping a tag
		if ( $matches[1] == '['
		and $matches[4] == ']' ) {
			return substr( $matches[0], 1, -1 );
		}

		$tag = $matches[0];
		$tagname = $matches[2];
		$values = $matches[3];

		$mail_tag = new WPCF7_MailTag( $tag, $tagname, $values );
		$field_name = $mail_tag->field_name();

		$submission = WPCF7_Submission::get_instance();
		$submitted = $submission
			? $submission->get_posted_data( $field_name )
			: null;

		if ( $mail_tag->get_option( 'do_not_heat' ) ) {
			$submitted = isset( $_POST[$field_name] )
				? wp_unslash( $_POST[$field_name] )
				: '';
		}

		$replaced = $submitted;

		if ( null !== $replaced ) {
			if ( $format = $mail_tag->get_option( 'format' ) ) {
				$replaced = $this->format( $replaced, $format );
			}

			$replaced = wpcf7_flat_join( $replaced, array(
				'separator' => wp_get_list_item_separator(),
			) );

			if ( $html ) {
				$replaced = esc_html( $replaced );
				$replaced = wptexturize( $replaced );
			}
		}

		if ( $form_tag = $mail_tag->corresponding_form_tag() ) {
			$type = $form_tag->type;

			$replaced = apply_filters(
				"wpcf7_mail_tag_replaced_{$type}", $replaced,
				$submitted, $html, $mail_tag
			);
		}

		$replaced = apply_filters(
			'wpcf7_mail_tag_replaced', $replaced,
			$submitted, $html, $mail_tag
		);

		if ( null !== $replaced ) {
			$replaced = trim( $replaced );

			$this->replaced_tags[$tag] = $replaced;
			return $replaced;
		}

		$special = apply_filters( 'wpcf7_special_mail_tags', null,
			$mail_tag->tag_name(), $html, $mail_tag
		);

		if ( null !== $special ) {
			$this->replaced_tags[$tag] = $special;
			return $special;
		}

		return $tag;
	}


	/**
	 * Formats string based on the formatting option in the form-tag.
	 */
	public function format( $original, $format ) {
		$original = (array) $original;

		foreach ( $original as $key => $value ) {
			if ( preg_match( '/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/', $value ) ) {
				$datetime = date_create( $value, wp_timezone() );

				if ( false !== $datetime ) {
					$original[$key] = wp_date( $format, $datetime->getTimestamp() );
				}
			}
		}

		return $original;
	}

}


/**
 * Class that represents a mail-tag.
 */
class WPCF7_MailTag {

	private $tag;
	private $tagname = '';
	private $name = '';
	private $options = array();
	private $values = array();
	private $form_tag = null;


	/**
	 * The constructor method.
	 */
	public function __construct( $tag, $tagname, $values ) {
		$this->tag = $tag;
		$this->name = $this->tagname = $tagname;

		$this->options = array(
			'do_not_heat' => false,
			'format' => '',
		);

		if ( ! empty( $values ) ) {
			preg_match_all( '/"[^"]*"|\'[^\']*\'/', $values, $matches );
			$this->values = wpcf7_strip_quote_deep( $matches[0] );
		}

		if ( preg_match( '/^_raw_(.+)$/', $tagname, $matches ) ) {
			$this->name = trim( $matches[1] );
			$this->options['do_not_heat'] = true;
		}

		if ( preg_match( '/^_format_(.+)$/', $tagname, $matches ) ) {
			$this->name = trim( $matches[1] );
			$this->options['format'] = $this->values[0];
		}
	}


	/**
	 * Returns the name part of this mail-tag.
	 */
	public function tag_name() {
		return $this->tagname;
	}


	/**
	 * Returns the form field name corresponding to this mail-tag.
	 */
	public function field_name() {
		return strtr( $this->name, '.', '_' );
	}


	/**
	 * Returns the value of the specified option.
	 */
	public function get_option( $option ) {
		return $this->options[$option];
	}


	/**
	 * Returns the values part of this mail-tag.
	 */
	public function values() {
		return $this->values;
	}


	/**
	 * Retrieves the WPCF7_FormTag object that corresponds to this mail-tag.
	 */
	public function corresponding_form_tag() {
		if ( $this->form_tag instanceof WPCF7_FormTag ) {
			return $this->form_tag;
		}

		if ( $submission = WPCF7_Submission::get_instance() ) {
			$contact_form = $submission->get_contact_form();
			$tags = $contact_form->scan_form_tags( array(
				'name' => $this->field_name(),
				'feature' => '! zero-controls-container',
			) );

			if ( $tags ) {
				$this->form_tag = $tags[0];
			}
		}

		return $this->form_tag;
	}

}
form-tag.php000064400000032245151330370000006767 0ustar00<?php

/**
 * A form-tag.
 *
 * @link https://contactform7.com/tag-syntax/#form_tag
 */
class WPCF7_FormTag implements ArrayAccess {

	public $type;
	public $basetype;
	public $raw_name = '';
	public $name = '';
	public $options = array();
	public $raw_values = array();
	public $values = array();
	public $pipes;
	public $labels = array();
	public $attr = '';
	public $content = '';

	public function __construct( $tag = array() ) {
		if ( is_array( $tag )
		or $tag instanceof self ) {
			foreach ( $tag as $key => $value ) {
				if ( property_exists( __CLASS__, $key ) ) {
					$this->{$key} = $value;
				}
			}
		}
	}


	/**
	 * Returns true if the type has a trailing asterisk.
	 */
	public function is_required() {
		return ( '*' === substr( $this->type, -1 ) );
	}


	/**
	 * Returns true if the form-tag has a specified option.
	 */
	public function has_option( $option_name ) {
		$pattern = sprintf( '/^%s(:.+)?$/i', preg_quote( $option_name, '/' ) );
		return (bool) preg_grep( $pattern, $this->options );
	}


	/**
	 * Retrieves option values with the specified option name.
	 *
	 * @param string $option_name Option name.
	 * @param string $pattern Optional. A regular expression pattern or one of
	 *               the keys of preset patterns. If specified, only options
	 *               whose value part matches this pattern will be returned.
	 * @param bool $single Optional. If true, only the first matching option
	 *             will be returned. Default false.
	 * @return string|array|bool The option value or an array of option values.
	 *                           False if there is no option matches the pattern.
	 */
	public function get_option( $option_name, $pattern = '', $single = false ) {
		$preset_patterns = array(
			'date' => '[0-9]{4}-[0-9]{2}-[0-9]{2}',
			'int' => '[0-9]+',
			'signed_int' => '[-]?[0-9]+',
			'num' => '(?:[0-9]+|(?:[0-9]+)?[.][0-9]+)',
			'signed_num' => '[-]?(?:[0-9]+|(?:[0-9]+)?[.][0-9]+)',
			'class' => '[-0-9a-zA-Z_]+',
			'id' => '[-0-9a-zA-Z_]+',
		);

		if ( isset( $preset_patterns[$pattern] ) ) {
			$pattern = $preset_patterns[$pattern];
		}

		if ( '' == $pattern ) {
			$pattern = '.+';
		}

		$pattern = sprintf(
			'/^%s:%s$/i',
			preg_quote( $option_name, '/' ),
			$pattern
		);

		if ( $single ) {
			$matches = $this->get_first_match_option( $pattern );

			if ( ! $matches ) {
				return false;
			}

			return substr( $matches[0], strlen( $option_name ) + 1 );
		} else {
			$matches_a = $this->get_all_match_options( $pattern );

			if ( ! $matches_a ) {
				return false;
			}

			$results = array();

			foreach ( $matches_a as $matches ) {
				$results[] = substr( $matches[0], strlen( $option_name ) + 1 );
			}

			return $results;
		}
	}


	/**
	 * Retrieves the id option value from the form-tag.
	 */
	public function get_id_option() {
		return $this->get_option( 'id', 'id', true );
	}


	/**
	 * Retrieves the class option value from the form-tag.
	 *
	 * @param string|array $default_classes Optional. Preset classes as an array
	 *                     or a whitespace-separated list. Default empty string.
	 * @return string|bool A whitespace-separated list of classes.
	 *                     False if there is no class to return.
	 */
	public function get_class_option( $default_classes = '' ) {
		if ( is_string( $default_classes ) ) {
			$default_classes = explode( ' ', $default_classes );
		}

		$options = array_merge(
			(array) $default_classes,
			(array) $this->get_option( 'class', 'class' )
		);

		$options = array_filter( array_unique( $options ) );

		if ( empty( $options ) ) {
			return false;
		}

		return implode( ' ', $options );
	}


	/**
	 * Retrieves the size option value from the form-tag.
	 *
	 * @param string $default_value Optional default value.
	 * @return string The option value.
	 */
	public function get_size_option( $default_value = false ) {
		$option = $this->get_option( 'size', 'int', true );

		if ( $option ) {
			return $option;
		}

		$matches_a = $this->get_all_match_options( '%^([0-9]*)/[0-9]*$%' );

		foreach ( (array) $matches_a as $matches ) {
			if ( isset( $matches[1] ) and '' !== $matches[1] ) {
				return $matches[1];
			}
		}

		return $default_value;
	}


	/**
	 * Retrieves the maxlength option value from the form-tag.
	 *
	 * @param string $default_value Optional default value.
	 * @return string The option value.
	 */
	public function get_maxlength_option( $default_value = false ) {
		$option = $this->get_option( 'maxlength', 'int', true );

		if ( $option ) {
			return $option;
		}

		$matches_a = $this->get_all_match_options(
			'%^(?:[0-9]*x?[0-9]*)?/([0-9]+)$%'
		);

		foreach ( (array) $matches_a as $matches ) {
			if ( isset( $matches[1] ) and '' !== $matches[1] ) {
				return $matches[1];
			}
		}

		return $default_value;
	}


	/**
	 * Retrieves the minlength option value from the form-tag.
	 *
	 * @param string $default_value Optional default value.
	 * @return string The option value.
	 */
	public function get_minlength_option( $default_value = false ) {
		$option = $this->get_option( 'minlength', 'int', true );

		if ( $option ) {
			return $option;
		} else {
			return $default_value;
		}
	}


	/**
	 * Retrieves the cols option value from the form-tag.
	 *
	 * @param string $default_value Optional default value.
	 * @return string The option value.
	 */
	public function get_cols_option( $default_value = false ) {
		$option = $this->get_option( 'cols', 'int', true );

		if ( $option ) {
			return $option;
		}

		$matches_a = $this->get_all_match_options(
			'%^([0-9]*)x([0-9]*)(?:/[0-9]+)?$%'
		);

		foreach ( (array) $matches_a as $matches ) {
			if ( isset( $matches[1] ) and '' !== $matches[1] ) {
				return $matches[1];
			}
		}

		return $default_value;
	}


	/**
	 * Retrieves the rows option value from the form-tag.
	 *
	 * @param string $default_value Optional default value.
	 * @return string The option value.
	 */
	public function get_rows_option( $default_value = false ) {
		$option = $this->get_option( 'rows', 'int', true );

		if ( $option ) {
			return $option;
		}

		$matches_a = $this->get_all_match_options(
			'%^([0-9]*)x([0-9]*)(?:/[0-9]+)?$%'
		);

		foreach ( (array) $matches_a as $matches ) {
			if ( isset( $matches[2] ) and '' !== $matches[2] ) {
				return $matches[2];
			}
		}

		return $default_value;
	}


	/**
	 * Retrieves a date-type option value from the form-tag.
	 *
	 * @param string $option_name A date-type option name, such as 'min' or 'max'.
	 * @return string|bool The option value in YYYY-MM-DD format. False if the
	 *                     option does not exist or the date value is invalid.
	 */
	public function get_date_option( $option_name ) {
		$option_value = $this->get_option( $option_name, '', true );

		if ( empty( $option_value ) ) {
			return false;
		}

		$date = apply_filters( 'wpcf7_form_tag_date_option',
			null,
			array( $option_name => $option_value )
		);

		if ( $date ) {
			$date_pattern = '/^([0-9]{4})-([0-9]{2})-([0-9]{2})$/';

			if ( preg_match( $date_pattern, $date, $matches )
			and checkdate( $matches[2], $matches[3], $matches[1] ) ) {
				return $date;
			}
		} else {
			$datetime_obj = date_create_immutable(
				preg_replace( '/[_]+/', ' ', $option_value ),
				wp_timezone()
			);

			if ( $datetime_obj ) {
				return $datetime_obj->format( 'Y-m-d' );
			}
		}

		return false;
	}


	/**
	 * Retrieves the default option value from the form-tag.
	 *
	 * @param string|array $default_value Optional default value.
	 * @param string|array $args Optional options for the option value retrieval.
	 * @return string|array The option value. If the multiple option is enabled,
	 *                      an array of option values.
	 */
	public function get_default_option( $default_value = '', $args = '' ) {
		$args = wp_parse_args( $args, array(
			'multiple' => false,
			'shifted' => false,
		) );

		$options = (array) $this->get_option( 'default' );
		$values = array();

		if ( empty( $options ) ) {
			return $args['multiple'] ? $values : $default_value;
		}

		foreach ( $options as $opt ) {
			$opt = sanitize_key( $opt );

			if ( 'user_' == substr( $opt, 0, 5 ) and is_user_logged_in() ) {
				$primary_props = array( 'user_login', 'user_email', 'user_url' );
				$opt = in_array( $opt, $primary_props ) ? $opt : substr( $opt, 5 );

				$user = wp_get_current_user();
				$user_prop = $user->get( $opt );

				if ( ! empty( $user_prop ) ) {
					if ( $args['multiple'] ) {
						$values[] = $user_prop;
					} else {
						return $user_prop;
					}
				}

			} elseif ( 'post_meta' === $opt and in_the_loop() ) {
				if ( $args['multiple'] ) {
					$values = array_merge( $values,
						get_post_meta( get_the_ID(), $this->name )
					);
				} else {
					$val = (string) get_post_meta( get_the_ID(), $this->name, true );

					if ( strlen( $val ) ) {
						return $val;
					}
				}

			} elseif ( 'get' === $opt and isset( $_GET[$this->name] ) ) {
				$vals = (array) $_GET[$this->name];
				$vals = array_map( 'wpcf7_sanitize_query_var', $vals );

				if ( $args['multiple'] ) {
					$values = array_merge( $values, $vals );
				} else {
					$val = isset( $vals[0] ) ? (string) $vals[0] : '';

					if ( strlen( $val ) ) {
						return $val;
					}
				}

			} elseif ( 'post' === $opt and isset( $_POST[$this->name] ) ) {
				$vals = (array) $_POST[$this->name];
				$vals = array_map( 'wpcf7_sanitize_query_var', $vals );

				if ( $args['multiple'] ) {
					$values = array_merge( $values, $vals );
				} else {
					$val = isset( $vals[0] ) ? (string) $vals[0] : '';

					if ( strlen( $val ) ) {
						return $val;
					}
				}

			} elseif ( 'shortcode_attr' === $opt ) {
				if ( $contact_form = WPCF7_ContactForm::get_current() ) {
					$val = $contact_form->shortcode_attr( $this->name );

					if ( strlen( $val ) ) {
						if ( $args['multiple'] ) {
							$values[] = $val;
						} else {
							return $val;
						}
					}
				}

			} elseif ( preg_match( '/^[0-9_]+$/', $opt ) ) {
				$nums = explode( '_', $opt );

				foreach ( $nums as $num ) {
					$num = absint( $num );
					$num = $args['shifted'] ? $num : $num - 1;

					if ( isset( $this->values[$num] ) ) {
						if ( $args['multiple'] ) {
							$values[] = $this->values[$num];
						} else {
							return $this->values[$num];
						}
					}
				}
			}
		}

		if ( $args['multiple'] ) {
			$values = array_unique( $values );
			return $values;
		} else {
			return $default_value;
		}
	}


	/**
	 * Retrieves the data option value from the form-tag.
	 *
	 * @param string|array $args Optional options for the option value retrieval.
	 * @return mixed The option value.
	 */
	public function get_data_option( $args = '' ) {
		$options = (array) $this->get_option( 'data' );

		return apply_filters( 'wpcf7_form_tag_data_option', null, $options, $args );
	}


	/**
	 * Retrieves the limit option value from the form-tag.
	 *
	 * @param int $default_value Optional default value. Default 1048576.
	 * @return int The option value.
	 */
	public function get_limit_option( $default_value = MB_IN_BYTES ) {
		$pattern = '/^limit:([1-9][0-9]*)([kKmM]?[bB])?$/';

		$matches = $this->get_first_match_option( $pattern );

		if ( $matches ) {
			$size = (int) $matches[1];

			if ( ! empty( $matches[2] ) ) {
				$kbmb = strtolower( $matches[2] );

				if ( 'kb' === $kbmb ) {
					$size *= KB_IN_BYTES;
				} elseif ( 'mb' === $kbmb ) {
					$size *= MB_IN_BYTES;
				}
			}

			return $size;
		}

		return (int) $default_value;
	}


	/**
	 * Retrieves the value of the first option matches the given
	 * regular expression pattern.
	 *
	 * @param string $pattern Regular expression pattern.
	 * @return array|bool Option value as an array of matched strings.
	 *                    False if there is no option matches the pattern.
	 */
	public function get_first_match_option( $pattern ) {
		foreach( (array) $this->options as $option ) {
			if ( preg_match( $pattern, $option, $matches ) ) {
				return $matches;
			}
		}

		return false;
	}


	/**
	 * Retrieves values of options that match the given
	 * regular expression pattern.
	 *
	 * @param string $pattern Regular expression pattern.
	 * @return array Array of arrays of strings that match the pattern.
	 */
	public function get_all_match_options( $pattern ) {
		$result = array();

		foreach( (array) $this->options as $option ) {
			if ( preg_match( $pattern, $option, $matches ) ) {
				$result[] = $matches;
			}
		}

		return $result;
	}


	/**
	 * Assigns a value to the specified offset.
	 *
	 * @link https://www.php.net/manual/en/arrayaccess.offsetset.php
	 */
	#[ReturnTypeWillChange]
	public function offsetSet( $offset, $value ) {
		if ( property_exists( __CLASS__, $offset ) ) {
			$this->{$offset} = $value;
		}
	}


	/**
	 * Returns the value at specified offset.
	 *
	 * @link https://www.php.net/manual/en/arrayaccess.offsetget.php
	 */
	#[ReturnTypeWillChange]
	public function offsetGet( $offset ) {
		if ( property_exists( __CLASS__, $offset ) ) {
			return $this->{$offset};
		}

		return null;
	}


	/**
	 * Returns true if the specified offset exists.
	 *
	 * @link https://www.php.net/manual/en/arrayaccess.offsetexists.php
	 */
	#[ReturnTypeWillChange]
	public function offsetExists( $offset ) {
		return property_exists( __CLASS__, $offset );
	}


	/**
	 * Unsets an offset.
	 *
	 * @link https://www.php.net/manual/en/arrayaccess.offsetunset.php
	 */
	#[ReturnTypeWillChange]
	public function offsetUnset( $offset ) {
	}

}
contact-form-functions.php000064400000022333151330370000011652 0ustar00<?php
/**
 * Contact form helper functions
 */


/**
 * Wrapper function of WPCF7_ContactForm::get_instance().
 *
 * @param WPCF7_ContactForm|WP_Post|int $post Object or post ID.
 * @return WPCF7_ContactForm|null Contact form object. Null if unset.
 */
function wpcf7_contact_form( $post ) {
	return WPCF7_ContactForm::get_instance( $post );
}


/**
 * Searches for a contact form by an old unit ID.
 *
 * @param int $old_id Old unit ID.
 * @return WPCF7_ContactForm Contact form object.
 */
function wpcf7_get_contact_form_by_old_id( $old_id ) {
	global $wpdb;

	$q = "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_old_cf7_unit_id'"
		. $wpdb->prepare( " AND meta_value = %d", $old_id );

	if ( $new_id = $wpdb->get_var( $q ) ) {
		return wpcf7_contact_form( $new_id );
	}
}


/**
 * Searches for a contact form by title.
 *
 * @param string $title Title of contact form.
 * @return WPCF7_ContactForm|null Contact form object if found, null otherwise.
 */
function wpcf7_get_contact_form_by_title( $title ) {
	if ( ! is_string( $title ) or '' === $title ) {
		return null;
	}

	$contact_forms = WPCF7_ContactForm::find( array(
		'title' => $title,
		'posts_per_page' => 1,
	) );

	if ( $contact_forms ) {
		return wpcf7_contact_form( reset( $contact_forms ) );
	}
}


/**
 * Wrapper function of WPCF7_ContactForm::get_current().
 *
 * @return WPCF7_ContactForm Contact form object.
 */
function wpcf7_get_current_contact_form() {
	if ( $current = WPCF7_ContactForm::get_current() ) {
		return $current;
	}
}


/**
 * Returns true if it is in the state that a non-Ajax submission is accepted.
 */
function wpcf7_is_posted() {
	if ( ! $contact_form = wpcf7_get_current_contact_form() ) {
		return false;
	}

	return $contact_form->is_posted();
}


/**
 * Retrieves the user input value through a non-Ajax submission.
 *
 * @param string $name Name of form control.
 * @param string $default_value Optional default value.
 * @return string The user input value through the form-control.
 */
function wpcf7_get_hangover( $name, $default_value = null ) {
	if ( ! wpcf7_is_posted() ) {
		return $default_value;
	}

	$submission = WPCF7_Submission::get_instance();

	if ( ! $submission
	or $submission->is( 'mail_sent' ) ) {
		return $default_value;
	}

	return isset( $_POST[$name] ) ? wp_unslash( $_POST[$name] ) : $default_value;
}


/**
 * Retrieves an HTML snippet of validation error on the given form control.
 *
 * @param string $name Name of form control.
 * @return string Validation error message in a form of HTML snippet.
 */
function wpcf7_get_validation_error( $name ) {
	if ( ! $contact_form = wpcf7_get_current_contact_form() ) {
		return '';
	}

	return $contact_form->validation_error( $name );
}


/**
 * Returns a reference key to a validation error message.
 *
 * @param string $name Name of form control.
 * @param string $unit_tag Optional. Unit tag of the contact form.
 * @return string Reference key code.
 */
function wpcf7_get_validation_error_reference( $name, $unit_tag = '' ) {
	if ( '' === $unit_tag ) {
		$contact_form = wpcf7_get_current_contact_form();

		if ( $contact_form and $contact_form->validation_error( $name ) ) {
			$unit_tag = $contact_form->unit_tag();
		} else {
			return null;
		}
	}

	return preg_replace( '/[^0-9a-z_-]+/i', '',
		sprintf(
			'%1$s-ve-%2$s',
			$unit_tag,
			$name
		)
	);
}


/**
 * Retrieves a message for the given status.
 */
function wpcf7_get_message( $status ) {
	if ( ! $contact_form = wpcf7_get_current_contact_form() ) {
		return '';
	}

	return $contact_form->message( $status );
}


/**
 * Returns a class names list for a form-tag of the specified type.
 *
 * @param string $type Form-tag type.
 * @param string $default_classes Optional default classes.
 * @return string Whitespace-separated list of class names.
 */
function wpcf7_form_controls_class( $type, $default_classes = '' ) {
	$type = trim( $type );
	$default_classes = array_filter( explode( ' ', $default_classes ) );

	$classes = array_merge( array( 'wpcf7-form-control' ), $default_classes );

	$typebase = rtrim( $type, '*' );
	$required = ( '*' == substr( $type, -1 ) );

	$classes[] = 'wpcf7-' . $typebase;

	if ( $required ) {
		$classes[] = 'wpcf7-validates-as-required';
	}

	$classes = array_unique( $classes );

	return implode( ' ', $classes );
}


/**
 * Callback function for the contact-form-7 shortcode.
 */
function wpcf7_contact_form_tag_func( $atts, $content = null, $code = '' ) {
	if ( is_feed() ) {
		return '[contact-form-7]';
	}

	if ( 'contact-form-7' == $code ) {
		$atts = shortcode_atts(
			array(
				'id' => 0,
				'title' => '',
				'html_id' => '',
				'html_name' => '',
				'html_title' => '',
				'html_class' => '',
				'output' => 'form',
			),
			$atts, 'wpcf7'
		);

		$id = (int) $atts['id'];
		$title = trim( $atts['title'] );

		if ( ! $contact_form = wpcf7_contact_form( $id ) ) {
			$contact_form = wpcf7_get_contact_form_by_title( $title );
		}

	} else {
		if ( is_string( $atts ) ) {
			$atts = explode( ' ', $atts, 2 );
		}

		$id = (int) array_shift( $atts );
		$contact_form = wpcf7_get_contact_form_by_old_id( $id );
	}

	if ( ! $contact_form ) {
		return sprintf(
			'<p class="wpcf7-contact-form-not-found"><strong>%1$s</strong> %2$s</p>',
			esc_html( __( 'Error:', 'contact-form-7' ) ),
			esc_html( __( "Contact form not found.", 'contact-form-7' ) )
		);
	}

	$callback = static function ( $contact_form, $atts ) {
		return $contact_form->form_html( $atts );
	};

	return wpcf7_switch_locale(
		$contact_form->locale(),
		$callback,
		$contact_form, $atts
	);
}


/**
 * Saves the contact form data.
 */
function wpcf7_save_contact_form( $args = '', $context = 'save' ) {
	$args = wp_parse_args( $args, array(
		'id' => -1,
		'title' => null,
		'locale' => null,
		'form' => null,
		'mail' => null,
		'mail_2' => null,
		'messages' => null,
		'additional_settings' => null,
	) );

	$args = wp_unslash( $args );

	$args['id'] = (int) $args['id'];

	if ( -1 == $args['id'] ) {
		$contact_form = WPCF7_ContactForm::get_template();
	} else {
		$contact_form = wpcf7_contact_form( $args['id'] );
	}

	if ( empty( $contact_form ) ) {
		return false;
	}

	if ( null !== $args['title'] ) {
		$contact_form->set_title( $args['title'] );
	}

	if ( null !== $args['locale'] ) {
		$contact_form->set_locale( $args['locale'] );
	}

	$properties = array();

	if ( null !== $args['form'] ) {
		$properties['form'] = wpcf7_sanitize_form( $args['form'] );
	}

	if ( null !== $args['mail'] ) {
		$properties['mail'] = wpcf7_sanitize_mail( $args['mail'] );
		$properties['mail']['active'] = true;
	}

	if ( null !== $args['mail_2'] ) {
		$properties['mail_2'] = wpcf7_sanitize_mail( $args['mail_2'] );
	}

	if ( null !== $args['messages'] ) {
		$properties['messages'] = wpcf7_sanitize_messages( $args['messages'] );
	}

	if ( null !== $args['additional_settings'] ) {
		$properties['additional_settings'] = wpcf7_sanitize_additional_settings(
			$args['additional_settings']
		);
	}

	$contact_form->set_properties( $properties );

	do_action( 'wpcf7_save_contact_form', $contact_form, $args, $context );

	if ( 'save' == $context ) {
		$contact_form->save();
	}

	return $contact_form;
}


/**
 * Sanitizes the form property data.
 */
function wpcf7_sanitize_form( $input, $default_template = '' ) {
	if ( null === $input ) {
		return $default_template;
	}

	$output = trim( $input );

	if ( ! current_user_can( 'unfiltered_html' ) ) {
		$output = wpcf7_kses( $output, 'form' );
	}

	return $output;
}


/**
 * Sanitizes the mail property data.
 */
function wpcf7_sanitize_mail( $input, $defaults = array() ) {
	$input = wp_parse_args( $input, array(
		'active' => false,
		'subject' => '',
		'sender' => '',
		'recipient' => '',
		'body' => '',
		'additional_headers' => '',
		'attachments' => '',
		'use_html' => false,
		'exclude_blank' => false,
	) );

	$input = wp_parse_args( $input, $defaults );

	$output = array();
	$output['active'] = (bool) $input['active'];
	$output['subject'] = trim( $input['subject'] );
	$output['sender'] = trim( $input['sender'] );
	$output['recipient'] = trim( $input['recipient'] );
	$output['body'] = trim( $input['body'] );

	if ( ! current_user_can( 'unfiltered_html' ) ) {
		$output['body'] = wpcf7_kses( $output['body'], 'mail' );
	}

	$output['additional_headers'] = '';

	$headers = str_replace( "\r\n", "\n", $input['additional_headers'] );
	$headers = explode( "\n", $headers );

	foreach ( $headers as $header ) {
		$header = trim( $header );

		if ( '' !== $header ) {
			$output['additional_headers'] .= $header . "\n";
		}
	}

	$output['additional_headers'] = trim( $output['additional_headers'] );
	$output['attachments'] = trim( $input['attachments'] );
	$output['use_html'] = (bool) $input['use_html'];
	$output['exclude_blank'] = (bool) $input['exclude_blank'];

	return $output;
}


/**
 * Sanitizes the messages property data.
 */
function wpcf7_sanitize_messages( $input, $defaults = array() ) {
	$output = array();

	foreach ( wpcf7_messages() as $key => $val ) {
		if ( isset( $input[$key] ) ) {
			$output[$key] = trim( $input[$key] );
		} elseif ( isset( $defaults[$key] ) ) {
			$output[$key] = $defaults[$key];
		}
	}

	return $output;
}


/**
 * Sanitizes the additional settings property data.
 */
function wpcf7_sanitize_additional_settings( $input, $default_template = '' ) {
	if ( null === $input ) {
		return $default_template;
	}

	$output = trim( $input );
	return $output;
}
functions.php000064400000036604151330370000007266 0ustar00<?php

/**
 * Returns path to a plugin file.
 *
 * @param string $path File path relative to the plugin root directory.
 * @return string Absolute file path.
 */
function wpcf7_plugin_path( $path = '' ) {
	return path_join( WPCF7_PLUGIN_DIR, trim( $path, '/' ) );
}


/**
 * Returns the URL to a plugin file.
 *
 * @param string $path File path relative to the plugin root directory.
 * @return string URL.
 */
function wpcf7_plugin_url( $path = '' ) {
	$url = plugins_url( $path, WPCF7_PLUGIN );

	if ( is_ssl()
	and 'http:' == substr( $url, 0, 5 ) ) {
		$url = 'https:' . substr( $url, 5 );
	}

	return $url;
}


/**
 * Include a file under WPCF7_PLUGIN_MODULES_DIR.
 *
 * @param string $path File path relative to the module dir.
 * @return bool True on success, false on failure.
 */
function wpcf7_include_module_file( $path ) {
	$dir = WPCF7_PLUGIN_MODULES_DIR;

	if ( empty( $dir ) or ! is_dir( $dir ) ) {
		return false;
	}

	$path = path_join( $dir, ltrim( $path, '/' ) );

	if ( file_exists( $path ) ) {
		include_once $path;
		return true;
	}

	return false;
}


/**
 * Retrieves uploads directory information.
 *
 * @param string|bool $type Optional. Type of output. Default false.
 * @return array|string Information about the upload directory.
 */
function wpcf7_upload_dir( $type = false ) {
	$uploads = wp_get_upload_dir();

	$uploads = apply_filters( 'wpcf7_upload_dir', array(
		'dir' => $uploads['basedir'],
		'url' => $uploads['baseurl'],
	) );

	if ( 'dir' == $type ) {
		return $uploads['dir'];
	} if ( 'url' == $type ) {
		return $uploads['url'];
	}

	return $uploads;
}


/**
 * Verifies that a correct security nonce was used with time limit.
 *
 * @param string $nonce Nonce value that was used for verification.
 * @param string $action Optional. Context to what is taking place.
 *                       Default 'wp_rest'.
 * @return int|bool 1 if the nonce is generated between 0-12 hours ago,
 *                  2 if the nonce is generated between 12-24 hours ago.
 *                  False if the nonce is invalid.
 */
function wpcf7_verify_nonce( $nonce, $action = 'wp_rest' ) {
	return wp_verify_nonce( $nonce, $action );
}


/**
 * Creates a cryptographic token tied to a specific action, user, user session,
 * and window of time.
 *
 * @param string $action Optional. Context to what is taking place.
 *                       Default 'wp_rest'.
 * @return string The token.
 */
function wpcf7_create_nonce( $action = 'wp_rest' ) {
	return wp_create_nonce( $action );
}


/**
 * Converts multi-dimensional array to a flat array.
 *
 * @param mixed $input Array or item of array.
 * @return array Flatten array.
 */
function wpcf7_array_flatten( $input ) {
	if ( ! is_array( $input ) ) {
		return array( $input );
	}

	$output = array();

	foreach ( $input as $value ) {
		$output = array_merge( $output, wpcf7_array_flatten( $value ) );
	}

	return $output;
}


/**
 * Excludes unset or blank text values from the given array.
 *
 * @param array $input The array.
 * @return array Array without blank text values.
 */
function wpcf7_exclude_blank( $input ) {
	$output = array_filter( $input,
		static function ( $i ) {
			return isset( $i ) && '' !== $i;
		}
	);

	return array_values( $output );
}


/**
 * Creates a comma-separated list from a multi-dimensional array.
 *
 * @param mixed $input Array or item of array.
 * @param string|array $args Optional. Output options.
 * @return string Comma-separated list.
 */
function wpcf7_flat_join( $input, $args = '' ) {
	$args = wp_parse_args( $args, array(
		'separator' => ', ',
	) );

	$input = wpcf7_array_flatten( $input );
	$output = array();

	foreach ( (array) $input as $value ) {
		if ( is_scalar( $value ) ) {
			$output[] = trim( (string) $value );
		}
	}

	return implode( $args['separator'], $output );
}


/**
 * Returns true if HTML5 is supported.
 */
function wpcf7_support_html5() {
	return (bool) wpcf7_apply_filters_deprecated(
		'wpcf7_support_html5',
		array( true ),
		'5.6',
		''
	);
}


/**
 * Returns true if HTML5 fallback is active.
 */
function wpcf7_support_html5_fallback() {
	return (bool) apply_filters( 'wpcf7_support_html5_fallback', false );
}


/**
 * Returns true if the Really Simple CAPTCHA plugin is used for contact forms.
 */
function wpcf7_use_really_simple_captcha() {
	return apply_filters( 'wpcf7_use_really_simple_captcha',
		WPCF7_USE_REALLY_SIMPLE_CAPTCHA
	);
}


/**
 * Returns true if config validation is active.
 */
function wpcf7_validate_configuration() {
	return apply_filters( 'wpcf7_validate_configuration',
		WPCF7_VALIDATE_CONFIGURATION
	);
}


/**
 * Returns true if wpcf7_autop() is applied to form content.
 */
function wpcf7_autop_or_not() {
	return (bool) apply_filters( 'wpcf7_autop_or_not', WPCF7_AUTOP );
}


/**
 * Returns true if JavaScript for this plugin is loaded.
 */
function wpcf7_load_js() {
	return apply_filters( 'wpcf7_load_js', WPCF7_LOAD_JS );
}


/**
 * Returns true if CSS for this plugin is loaded.
 */
function wpcf7_load_css() {
	return apply_filters( 'wpcf7_load_css', WPCF7_LOAD_CSS );
}


/**
 * Builds an HTML anchor element.
 *
 * @param string $url Link URL.
 * @param string $anchor_text Anchor label text.
 * @param string|array $args Optional. Link options.
 * @return string Formatted anchor element.
 */
function wpcf7_link( $url, $anchor_text, $args = '' ) {
	$args = wp_parse_args( $args, array(
		'id' => null,
		'class' => null,
	) );

	$atts = array_merge( $args, array(
		'href' => esc_url( $url ),
	) );

	return sprintf(
		'<a %1$s>%2$s</a>',
		wpcf7_format_atts( $atts ),
		esc_html( $anchor_text )
	);
}


/**
 * Returns the current request URL.
 */
function wpcf7_get_request_uri() {
	static $request_uri = '';

	if ( empty( $request_uri ) ) {
		$request_uri = add_query_arg( array() );
	}

	return sanitize_url( $request_uri );
}


/**
 * Registers post types used for this plugin.
 */
function wpcf7_register_post_types() {
	if ( class_exists( 'WPCF7_ContactForm' ) ) {
		WPCF7_ContactForm::register_post_type();
		return true;
	} else {
		return false;
	}
}


/**
 * Returns the version string of this plugin.
 *
 * @param string|array $args Optional. Output options.
 * @return string Version string.
 */
function wpcf7_version( $args = '' ) {
	$defaults = array(
		'limit' => -1,
		'only_major' => false,
	);

	$args = wp_parse_args( $args, $defaults );

	if ( $args['only_major'] ) {
		$args['limit'] = 2;
	}

	$args['limit'] = (int) $args['limit'];

	$ver = WPCF7_VERSION;
	$ver = strtr( $ver, '_-+', '...' );
	$ver = preg_replace( '/[^0-9.]+/', ".$0.", $ver );
	$ver = preg_replace( '/[.]+/', ".", $ver );
	$ver = trim( $ver, '.' );
	$ver = explode( '.', $ver );

	if ( -1 < $args['limit'] ) {
		$ver = array_slice( $ver, 0, $args['limit'] );
	}

	$ver = implode( '.', $ver );

	return $ver;
}


/**
 * Returns array entries that match the given version.
 *
 * @param string $version The version to search for.
 * @param array $input Search target array.
 * @return array|bool Array of matched entries. False on failure.
 */
function wpcf7_version_grep( $version, array $input ) {
	$pattern = '/^' . preg_quote( (string) $version, '/' ) . '(?:\.|$)/';

	return preg_grep( $pattern, $input );
}


/**
 * Returns an enctype attribute value.
 *
 * @param string $enctype Enctype value.
 * @return string Enctype value. Empty if not a valid enctype.
 */
function wpcf7_enctype_value( $enctype ) {
	$enctype = trim( $enctype );

	if ( empty( $enctype ) ) {
		return '';
	}

	$valid_enctypes = array(
		'application/x-www-form-urlencoded',
		'multipart/form-data',
		'text/plain',
	);

	if ( in_array( $enctype, $valid_enctypes ) ) {
		return $enctype;
	}

	$pattern = '%^enctype="(' . implode( '|', $valid_enctypes ) . ')"$%';

	if ( preg_match( $pattern, $enctype, $matches ) ) {
		return $matches[1]; // for back-compat
	}

	return '';
}


/**
 * Removes directory recursively.
 *
 * @param string $dir Directory path.
 * @return bool True on success, false on failure.
 */
function wpcf7_rmdir_p( $dir ) {
	if ( is_file( $dir ) ) {
		$file = $dir;

		if ( @unlink( $file ) ) {
			return true;
		}

		$stat = stat( $file );

		if ( @chmod( $file, $stat['mode'] | 0200 ) ) { // add write for owner
			if ( @unlink( $file ) ) {
				return true;
			}

			@chmod( $file, $stat['mode'] );
		}

		return false;
	}

	if ( ! is_dir( $dir ) ) {
		return false;
	}

	if ( $handle = opendir( $dir ) ) {
		while ( false !== ( $file = readdir( $handle ) ) ) {
			if ( $file == "."
			or $file == ".." ) {
				continue;
			}

			wpcf7_rmdir_p( path_join( $dir, $file ) );
		}

		closedir( $handle );
	}

	if ( false !== ( $files = scandir( $dir ) )
	and ! array_diff( $files, array( '.', '..' ) ) ) {
		return rmdir( $dir );
	}

	return false;
}


/**
 * Builds a URL-encoded query string.
 *
 * @link https://developer.wordpress.org/reference/functions/_http_build_query/
 *
 * @param array $args URL query parameters.
 * @param string $key Optional. If specified, used to prefix key name.
 * @return string Query string.
 */
function wpcf7_build_query( $args, $key = '' ) {
	$sep = '&';
	$ret = array();

	foreach ( (array) $args as $k => $v ) {
		$k = urlencode( $k );

		if ( ! empty( $key ) ) {
			$k = $key . '%5B' . $k . '%5D';
		}

		if ( null === $v ) {
			continue;
		} elseif ( false === $v ) {
			$v = '0';
		}

		if ( is_array( $v ) or is_object( $v ) ) {
			array_push( $ret, wpcf7_build_query( $v, $k ) );
		} else {
			array_push( $ret, $k . '=' . urlencode( $v ) );
		}
	}

	return implode( $sep, $ret );
}


/**
 * Returns the number of code units in a string.
 *
 * @link http://www.w3.org/TR/html5/infrastructure.html#code-unit-length
 *
 * @param string $text Input string.
 * @return int|bool The number of code units, or false if
 *                  mb_convert_encoding is not available.
 */
function wpcf7_count_code_units( $text ) {
	static $use_mb = null;

	if ( is_null( $use_mb ) ) {
		$use_mb = function_exists( 'mb_convert_encoding' );
	}

	if ( ! $use_mb ) {
		return false;
	}

	$text = (string) $text;
	$text = str_replace( "\r\n", "\n", $text );

	$encoding = mb_detect_encoding( $text, mb_detect_order(), true );

	if ( $encoding ) {
		$text = mb_convert_encoding( $text, 'UTF-16', $encoding );
	} else {
		$text = mb_convert_encoding( $text, 'UTF-16', 'UTF-8' );
	}

	$byte_count = mb_strlen( $text, '8bit' );

	return floor( $byte_count / 2 );
}


/**
 * Returns true if WordPress is running on the localhost.
 */
function wpcf7_is_localhost() {
	$sitename = wp_parse_url( network_home_url(), PHP_URL_HOST );
	return in_array( strtolower( $sitename ), array( 'localhost', '127.0.0.1' ) );
}


/**
 * Marks a function as deprecated and informs when it has been used.
 *
 * @param string $function_name The function that was called.
 * @param string $version The version of Contact Form 7 that deprecated
 *                        the function.
 * @param string $replacement The function that should have been called.
 */
function wpcf7_deprecated_function( $function_name, $version, $replacement ) {
	if ( WP_DEBUG ) {
		if ( function_exists( '__' ) ) {
			trigger_error(
				sprintf(
					/* translators: 1: PHP function name, 2: version number, 3: alternative function name */
					__( 'Function %1$s is <strong>deprecated</strong> since Contact Form 7 version %2$s! Use %3$s instead.', 'contact-form-7' ),
					$function_name, $version, $replacement
				),
				E_USER_DEPRECATED
			);
		} else {
			trigger_error(
				sprintf(
					'Function %1$s is <strong>deprecated</strong> since Contact Form 7 version %2$s! Use %3$s instead.',
					$function_name, $version, $replacement
				),
				E_USER_DEPRECATED
			);
		}
	}
}


/**
 * Fires functions attached to a deprecated filter hook.
 *
 * @param string $hook_name The name of the filter hook.
 * @param array $args Array of additional function arguments to be
 *                    passed to apply_filters().
 * @param string $version The version of Contact Form 7 that deprecated
 *                        the hook.
 * @param string $replacement The hook that should have been used.
 */
function wpcf7_apply_filters_deprecated( $hook_name, $args, $version, $replacement = '' ) {
	if ( ! has_filter( $hook_name ) ) {
		return $args[0];
	}

	if ( WP_DEBUG and apply_filters( 'deprecated_hook_trigger_error', true ) ) {
		if ( $replacement ) {
			trigger_error(
				sprintf(
					/* translators: 1: WordPress hook name, 2: version number, 3: alternative hook name */
					__( 'Hook %1$s is <strong>deprecated</strong> since Contact Form 7 version %2$s! Use %3$s instead.', 'contact-form-7' ),
					$hook_name,
					$version,
					$replacement
				),
				E_USER_DEPRECATED
			);
		} else {
			trigger_error(
				sprintf(
					/* translators: 1: WordPress hook name, 2: version number */
					__( 'Hook %1$s is <strong>deprecated</strong> since Contact Form 7 version %2$s with no alternative available.', 'contact-form-7' ),
					$hook_name,
					$version
				),
				E_USER_DEPRECATED
			);
		}
	}

	return apply_filters_ref_array( $hook_name, $args );
}


/**
 * Marks something as being incorrectly called.
 *
 * @param string $function_name The function that was called.
 * @param string $message A message explaining what has been done incorrectly.
 * @param string $version The version of Contact Form 7 where the message
 *                        was added.
 */
function wpcf7_doing_it_wrong( $function_name, $message, $version ) {
	if ( WP_DEBUG ) {
		if ( function_exists( '__' ) ) {
			if ( $version ) {
				$version = sprintf(
					/* translators: %s: Contact Form 7 version number. */
					__( '(This message was added in Contact Form 7 version %s.)', 'contact-form-7' ),
					$version
				);
			}

			trigger_error(
				sprintf(
					/* translators: Developer debugging message. 1: PHP function name, 2: Explanatory message, 3: Contact Form 7 version number. */
					__( 'Function %1$s was called incorrectly. %2$s %3$s', 'contact-form-7' ),
					$function_name,
					$message,
					$version
				),
				E_USER_NOTICE
			);
		} else {
			if ( $version ) {
				$version = sprintf(
					'(This message was added in Contact Form 7 version %s.)',
					$version
				);
			}

			trigger_error(
				sprintf(
					'Function %1$s was called incorrectly. %2$s %3$s',
					$function_name,
					$message,
					$version
				),
				E_USER_NOTICE
			);
		}
	}
}


/**
 * Triggers an error about a remote HTTP request and response.
 *
 * @param string $url The resource URL.
 * @param array $request Request arguments.
 * @param array|WP_Error $response The response or WP_Error on failure.
 */
function wpcf7_log_remote_request( $url, $request, $response ) {
	$log = sprintf(
		/* translators: 1: response code, 2: message, 3: body, 4: URL */
		__( 'HTTP Response: %1$s %2$s %3$s from %4$s', 'contact-form-7' ),
		(int) wp_remote_retrieve_response_code( $response ),
		wp_remote_retrieve_response_message( $response ),
		wp_remote_retrieve_body( $response ),
		$url
	);

	$log = apply_filters( 'wpcf7_log_remote_request',
		$log, $url, $request, $response
	);

	if ( $log ) {
		trigger_error( $log );
	}
}


/**
 * Anonymizes an IP address by masking local part.
 *
 * @param string $ip_addr The original IP address.
 * @return string|bool Anonymized IP address, or false on failure.
 */
function wpcf7_anonymize_ip_addr( $ip_addr ) {
	if ( ! function_exists( 'inet_ntop' )
	or ! function_exists( 'inet_pton' ) ) {
		return $ip_addr;
	}

	$packed = inet_pton( $ip_addr );

	if ( false === $packed ) {
		return $ip_addr;
	}

	if ( 4 == strlen( $packed ) ) { // IPv4
		$mask = '255.255.255.0';
	} elseif ( 16 == strlen( $packed ) ) { // IPv6
		$mask = 'ffff:ffff:ffff:0000:0000:0000:0000:0000';
	} else {
		return $ip_addr;
	}

	return inet_ntop( $packed & inet_pton( $mask ) );
}
integration.php000064400000021360151330370000007572 0ustar00<?php
/**
 * Integration API
 *
 * @link https://contactform7.com/integration-with-external-apis/
 */

class WPCF7_Integration {

	private static $instance;

	private $services = array();
	private $categories = array();

	private function __construct() {}


	/**
	 * Returns initially supported service categories.
	 *
	 * @return array Service categories.
	 */
	public static function get_builtin_categories() {
		return array(
			'spam_protection' => __( 'Spam protection', 'contact-form-7' ),
			'email_marketing' => __( 'Email marketing', 'contact-form-7' ),
			'payments' => __( 'Payments', 'contact-form-7' ),
		);
	}


	/**
	 * Returns the singleton instance of this class.
	 *
	 * @return WPCF7_Integration The instance.
	 */
	public static function get_instance() {
		if ( empty( self::$instance ) ) {
			self::$instance = new self;
			self::$instance->categories = self::get_builtin_categories();
		}

		return self::$instance;
	}


	/**
	 * Adds a service to the services list.
	 */
	public function add_service( $name, WPCF7_Service $service ) {
		$name = sanitize_key( $name );

		if ( empty( $name )
		or isset( $this->services[$name] ) ) {
			return false;
		}

		$this->services[$name] = $service;
	}


	/**
	 * Adds a service category to the categories list.
	 */
	public function add_category( $name, $title ) {
		$name = sanitize_key( $name );

		if ( empty( $name )
		or isset( $this->categories[$name] ) ) {
			return false;
		}

		$this->categories[$name] = $title;
	}


	/**
	 * Returns true if a service with the name exists in the services list.
	 *
	 * @param string $name The name of service to search.
	 */
	public function service_exists( $name = '' ) {
		if ( '' == $name ) {
			return (bool) count( $this->services );
		} else {
			return isset( $this->services[$name] );
		}
	}


	/**
	 * Returns a service object with the name.
	 *
	 * @param string $name The name of service.
	 * @return WPCF7_Service|bool The service object if it exists,
	 *                            false otherwise.
	 */
	public function get_service( $name ) {
		if ( $this->service_exists( $name ) ) {
			return $this->services[$name];
		} else {
			return false;
		}
	}


	/**
	 * Prints services list.
	 */
	public function list_services( $args = '' ) {
		$args = wp_parse_args( $args, array(
			'include' => array(),
		) );

		$singular = false;
		$services = (array) $this->services;

		if ( ! empty( $args['include'] ) ) {
			$services = array_intersect_key( $services,
				array_flip( (array) $args['include'] )
			);

			if ( 1 == count( $services ) ) {
				$singular = true;
			}
		}

		if ( empty( $services ) ) {
			return;
		}

		$action = wpcf7_current_action();

		foreach ( $services as $name => $service ) {
			$cats = array_intersect_key( $this->categories,
				array_flip( $service->get_categories() )
			);
?>
<div class="card<?php echo $service->is_active() ? ' active' : ''; ?>" id="<?php echo esc_attr( $name ); ?>">
<?php $service->icon(); ?>
<h2 class="title"><?php echo esc_html( $service->get_title() ); ?></h2>
<div class="infobox">
<?php echo esc_html( implode( ', ', $cats ) ); ?>
</div>
<br class="clear" />

<div class="inside">
<?php
			if ( $singular ) {
				$service->display( $action );
			} else {
				$service->display();
			}
?>
</div>
</div>
<?php
		}
	}

}


/**
 * Abstract class for services.
 *
 * Only instances of this class's subclasses are allowed to be
 * listed on the Integration page.
 */
abstract class WPCF7_Service {

	abstract public function get_title();
	abstract public function is_active();


	public function get_categories() {
		return array();
	}


	public function icon() {
		return '';
	}


	public function link() {
		return '';
	}


	public function load( $action = '' ) {
	}


	public function display( $action = '' ) {
	}


	public function admin_notice( $message = '' ) {
	}

}


/**
 * Class for services that use OAuth.
 *
 * While this is not an abstract class, subclassing this class for
 * your aim is advised.
 */
class WPCF7_Service_OAuth2 extends WPCF7_Service {

	protected $client_id = '';
	protected $client_secret = '';
	protected $access_token = '';
	protected $refresh_token = '';
	protected $authorization_endpoint = 'https://example.com/authorization';
	protected $token_endpoint = 'https://example.com/token';


	public function get_title() {
		return '';
	}


	public function is_active() {
		return ! empty( $this->refresh_token );
	}


	protected function save_data() {
	}


	protected function reset_data() {
	}


	protected function get_redirect_uri() {
		return admin_url();
	}


	protected function menu_page_url( $args = '' ) {
		return menu_page_url( 'wpcf7-integration', false );
	}


	public function load( $action = '' ) {
		if ( 'auth_redirect' == $action ) {
			$code = isset( $_GET['code'] ) ? $_GET['code'] : '';

			if ( $code ) {
				$this->request_token( $code );
			}

			if ( ! empty( $this->access_token ) ) {
				$message = 'success';
			} else {
				$message = 'failed';
			}

			wp_safe_redirect( $this->menu_page_url(
				array(
					'action' => 'setup',
					'message' => $message,
				)
			) );

			exit();
		}
	}


	protected function authorize( $scope = '' ) {
		$endpoint = add_query_arg(
			array(
				'response_type' => 'code',
				'client_id' => $this->client_id,
				'redirect_uri' => urlencode( $this->get_redirect_uri() ),
				'scope' => $scope,
			),
			$this->authorization_endpoint
		);

		if ( wp_redirect( sanitize_url( $endpoint ) ) ) {
			exit();
		}
	}


	protected function get_http_authorization_header( $scheme = 'basic' ) {
		$scheme = strtolower( trim( $scheme ) );

		switch ( $scheme ) {
			case 'bearer':
				return sprintf( 'Bearer %s', $this->access_token );
			case 'basic':
			default:
				return sprintf( 'Basic %s',
					base64_encode( $this->client_id . ':' . $this->client_secret )
				);
		}
	}


	protected function request_token( $authorization_code ) {
		$endpoint = add_query_arg(
			array(
				'code' => $authorization_code,
				'redirect_uri' => urlencode( $this->get_redirect_uri() ),
				'grant_type' => 'authorization_code',
			),
			$this->token_endpoint
		);

		$request = array(
			'headers' => array(
				'Authorization' => $this->get_http_authorization_header( 'basic' ),
			),
		);

		$response = wp_remote_post( sanitize_url( $endpoint ), $request );
		$response_code = (int) wp_remote_retrieve_response_code( $response );
		$response_body = wp_remote_retrieve_body( $response );
		$response_body = json_decode( $response_body, true );

		if ( WP_DEBUG and 400 <= $response_code ) {
			$this->log( $endpoint, $request, $response );
		}

		if ( 401 == $response_code ) { // Unauthorized
			$this->access_token = null;
			$this->refresh_token = null;
		} else {
			if ( isset( $response_body['access_token'] ) ) {
				$this->access_token = $response_body['access_token'];
			} else {
				$this->access_token = null;
			}

			if ( isset( $response_body['refresh_token'] ) ) {
				$this->refresh_token = $response_body['refresh_token'];
			} else {
				$this->refresh_token = null;
			}
		}

		$this->save_data();

		return $response;
	}


	protected function refresh_token() {
		$endpoint = add_query_arg(
			array(
				'refresh_token' => $this->refresh_token,
				'grant_type' => 'refresh_token',
			),
			$this->token_endpoint
		);

		$request = array(
			'headers' => array(
				'Authorization' => $this->get_http_authorization_header( 'basic' ),
			),
		);

		$response = wp_remote_post( sanitize_url( $endpoint ), $request );
		$response_code = (int) wp_remote_retrieve_response_code( $response );
		$response_body = wp_remote_retrieve_body( $response );
		$response_body = json_decode( $response_body, true );

		if ( WP_DEBUG and 400 <= $response_code ) {
			$this->log( $endpoint, $request, $response );
		}

		if ( 401 == $response_code ) { // Unauthorized
			$this->access_token = null;
			$this->refresh_token = null;
		} else {
			if ( isset( $response_body['access_token'] ) ) {
				$this->access_token = $response_body['access_token'];
			} else {
				$this->access_token = null;
			}

			if ( isset( $response_body['refresh_token'] ) ) {
				$this->refresh_token = $response_body['refresh_token'];
			}
		}

		$this->save_data();

		return $response;
	}


	protected function remote_request( $url, $request = array() ) {
		static $refreshed = false;

		$request = wp_parse_args( $request, array() );

		$request['headers'] = array_merge(
			$request['headers'],
			array(
				'Authorization' => $this->get_http_authorization_header( 'bearer' ),
			)
		);

		$response = wp_remote_request( sanitize_url( $url ), $request );

		if ( 401 === wp_remote_retrieve_response_code( $response )
		and ! $refreshed ) {
			$this->refresh_token();
			$refreshed = true;

			$response = $this->remote_request( $url, $request );
		}

		return $response;
	}


	protected function log( $url, $request, $response ) {
		wpcf7_log_remote_request( $url, $request, $response );
	}

}
capabilities.php000064400000001502151330370000007674 0ustar00<?php

add_filter( 'map_meta_cap', 'wpcf7_map_meta_cap', 10, 4 );

function wpcf7_map_meta_cap( $caps, $cap, $user_id, $args ) {
	$meta_caps = array(
		'wpcf7_edit_contact_form' => WPCF7_ADMIN_READ_WRITE_CAPABILITY,
		'wpcf7_edit_contact_forms' => WPCF7_ADMIN_READ_WRITE_CAPABILITY,
		'wpcf7_read_contact_form' => WPCF7_ADMIN_READ_CAPABILITY,
		'wpcf7_read_contact_forms' => WPCF7_ADMIN_READ_CAPABILITY,
		'wpcf7_delete_contact_form' => WPCF7_ADMIN_READ_WRITE_CAPABILITY,
		'wpcf7_delete_contact_forms' => WPCF7_ADMIN_READ_WRITE_CAPABILITY,
		'wpcf7_manage_integration' => 'manage_options',
		'wpcf7_submit' => 'read',
	);

	$meta_caps = apply_filters( 'wpcf7_map_meta_cap', $meta_caps );

	$caps = array_diff( $caps, array_keys( $meta_caps ) );

	if ( isset( $meta_caps[$cap] ) ) {
		$caps[] = $meta_caps[$cap];
	}

	return $caps;
}
special-mail-tags.php000064400000014055151330370000010546 0ustar00<?php
/**
** Special Mail Tags
** https://contactform7.com/special-mail-tags/
**/

add_filter( 'wpcf7_special_mail_tags', 'wpcf7_special_mail_tag', 10, 4 );

/**
 * Returns output string of a special mail-tag.
 *
 * @param string $output The string to be output.
 * @param string $name The tag name of the special mail-tag.
 * @param bool $html Whether the mail-tag is used in an HTML content.
 * @param WPCF7_MailTag $mail_tag An object representation of the mail-tag.
 * @return string Output of the given special mail-tag.
 */
function wpcf7_special_mail_tag( $output, $name, $html, $mail_tag = null ) {
	if ( ! $mail_tag instanceof WPCF7_MailTag ) {
		wpcf7_doing_it_wrong(
			sprintf( '%s()', __FUNCTION__ ),
			__( 'The fourth parameter ($mail_tag) must be an instance of the WPCF7_MailTag class.', 'contact-form-7' ),
			'5.2.2'
		);
	}

	$name = preg_replace( '/^wpcf7\./', '_', $name ); // for back-compat

	$submission = WPCF7_Submission::get_instance();

	if ( ! $submission ) {
		return $output;
	}

	if ( '_remote_ip' == $name ) {
		if ( $remote_ip = $submission->get_meta( 'remote_ip' ) ) {
			return $remote_ip;
		} else {
			return '';
		}
	}

	if ( '_user_agent' == $name ) {
		if ( $user_agent = $submission->get_meta( 'user_agent' ) ) {
			return $html ? esc_html( $user_agent ) : $user_agent;
		} else {
			return '';
		}
	}

	if ( '_url' == $name ) {
		if ( $url = $submission->get_meta( 'url' ) ) {
			return $url;
		} else {
			return '';
		}
	}

	if ( '_date' == $name
	or '_time' == $name ) {
		if ( $timestamp = $submission->get_meta( 'timestamp' ) ) {
			if ( '_date' == $name ) {
				return wp_date( get_option( 'date_format' ), $timestamp );
			}

			if ( '_time' == $name ) {
				return wp_date( get_option( 'time_format' ), $timestamp );
			}
		}

		return '';
	}

	if ( '_invalid_fields' == $name ) {
		return count( $submission->get_invalid_fields() );
	}

	return $output;
}


add_filter( 'wpcf7_special_mail_tags', 'wpcf7_post_related_smt', 10, 4 );

/**
 * Returns output string of a special mail-tag.
 *
 * @param string $output The string to be output.
 * @param string $name The tag name of the special mail-tag.
 * @param bool $html Whether the mail-tag is used in an HTML content.
 * @param WPCF7_MailTag $mail_tag An object representation of the mail-tag.
 * @return string Output of the given special mail-tag.
 */
function wpcf7_post_related_smt( $output, $name, $html, $mail_tag = null ) {
	if ( ! $mail_tag instanceof WPCF7_MailTag ) {
		wpcf7_doing_it_wrong(
			sprintf( '%s()', __FUNCTION__ ),
			__( 'The fourth parameter ($mail_tag) must be an instance of the WPCF7_MailTag class.', 'contact-form-7' ),
			'5.2.2'
		);
	}

	if ( '_post_' != substr( $name, 0, 6 ) ) {
		return $output;
	}

	$submission = WPCF7_Submission::get_instance();

	if ( ! $submission ) {
		return $output;
	}

	$post_id = (int) $submission->get_meta( 'container_post_id' );

	if ( ! $post_id
	or ! $post = get_post( $post_id ) ) {
		return '';
	}

	if ( '_post_id' == $name ) {
		return (string) $post->ID;
	}

	if ( '_post_name' == $name ) {
		return $post->post_name;
	}

	if ( '_post_title' == $name ) {
		return $html ? esc_html( $post->post_title ) : $post->post_title;
	}

	if ( '_post_url' == $name ) {
		return get_permalink( $post->ID );
	}

	$user = new WP_User( $post->post_author );

	if ( '_post_author' == $name ) {
		return $user->display_name;
	}

	if ( '_post_author_email' == $name ) {
		return $user->user_email;
	}

	return $output;
}


add_filter( 'wpcf7_special_mail_tags', 'wpcf7_site_related_smt', 10, 4 );

/**
 * Returns output string of a special mail-tag.
 *
 * @param string $output The string to be output.
 * @param string $name The tag name of the special mail-tag.
 * @param bool $html Whether the mail-tag is used in an HTML content.
 * @param WPCF7_MailTag $mail_tag An object representation of the mail-tag.
 * @return string Output of the given special mail-tag.
 */
function wpcf7_site_related_smt( $output, $name, $html, $mail_tag = null ) {
	if ( ! $mail_tag instanceof WPCF7_MailTag ) {
		wpcf7_doing_it_wrong(
			sprintf( '%s()', __FUNCTION__ ),
			__( 'The fourth parameter ($mail_tag) must be an instance of the WPCF7_MailTag class.', 'contact-form-7' ),
			'5.2.2'
		);
	}

	$filter = $html ? 'display' : 'raw';

	if ( '_site_title' == $name ) {
		$output = get_bloginfo( 'name', $filter );

		if ( ! $html ) {
			$output = wp_specialchars_decode( $output, ENT_QUOTES );
		}

		return $output;
	}

	if ( '_site_description' == $name ) {
		$output = get_bloginfo( 'description', $filter );

		if ( ! $html ) {
			$output = wp_specialchars_decode( $output, ENT_QUOTES );
		}

		return $output;
	}

	if ( '_site_url' == $name ) {
		return get_bloginfo( 'url', $filter );
	}

	if ( '_site_admin_email' == $name ) {
		return get_bloginfo( 'admin_email', $filter );
	}

	return $output;
}


add_filter( 'wpcf7_special_mail_tags', 'wpcf7_user_related_smt', 10, 4 );

/**
 * Returns output string of a special mail-tag.
 *
 * @param string $output The string to be output.
 * @param string $name The tag name of the special mail-tag.
 * @param bool $html Whether the mail-tag is used in an HTML content.
 * @param WPCF7_MailTag $mail_tag An object representation of the mail-tag.
 * @return string Output of the given special mail-tag.
 */
function wpcf7_user_related_smt( $output, $name, $html, $mail_tag = null ) {
	if ( ! $mail_tag instanceof WPCF7_MailTag ) {
		wpcf7_doing_it_wrong(
			sprintf( '%s()', __FUNCTION__ ),
			__( 'The fourth parameter ($mail_tag) must be an instance of the WPCF7_MailTag class.', 'contact-form-7' ),
			'5.2.2'
		);
	}

	if ( '_user_' != substr( $name, 0, 6 )
	or '_user_agent' == $name ) {
		return $output;
	}

	$submission = WPCF7_Submission::get_instance();

	if ( ! $submission ) {
		return $output;
	}

	$user_id = (int) $submission->get_meta( 'current_user_id' );

	if ( ! $user_id ) {
		return '';
	}

	$primary_props = array( 'user_login', 'user_email', 'user_url' );
	$opt = ltrim( $name, '_' );
	$opt = in_array( $opt, $primary_props ) ? $opt : substr( $opt, 5 );

	$user = new WP_User( $user_id );

	if ( $user->has_prop( $opt ) ) {
		return $user->get( $opt );
	}

	return '';
}
contact-form-template.php000064400000013061151330370000011453 0ustar00<?php

class WPCF7_ContactFormTemplate {

	public static function get_default( $prop = 'form' ) {
		if ( 'form' == $prop ) {
			$template = self::form();
		} elseif ( 'mail' == $prop ) {
			$template = self::mail();
		} elseif ( 'mail_2' == $prop ) {
			$template = self::mail_2();
		} elseif ( 'messages' == $prop ) {
			$template = self::messages();
		} else {
			$template = null;
		}

		return apply_filters( 'wpcf7_default_template', $template, $prop );
	}

	public static function form() {
		$template = sprintf(
			'
<label> %2$s
    [text* your-name autocomplete:name] </label>

<label> %3$s
    [email* your-email autocomplete:email] </label>

<label> %4$s
    [text* your-subject] </label>

<label> %5$s %1$s
    [textarea your-message] </label>

[submit "%6$s"]',
			__( '(optional)', 'contact-form-7' ),
			__( 'Your name', 'contact-form-7' ),
			__( 'Your email', 'contact-form-7' ),
			__( 'Subject', 'contact-form-7' ),
			__( 'Your message', 'contact-form-7' ),
			__( 'Submit', 'contact-form-7' )
		);

		return trim( $template );
	}

	public static function mail() {
		$template = array(
			'subject' => sprintf(
				/* translators: 1: blog name, 2: [your-subject] */
				_x( '%1$s "%2$s"', 'mail subject', 'contact-form-7' ),
				'[_site_title]',
				'[your-subject]'
			),
			'sender' => sprintf(
				'%s <%s>',
				'[_site_title]',
				self::from_email()
			),
			'body' =>
				sprintf(
					/* translators: %s: [your-name] [your-email] */
					__( 'From: %s', 'contact-form-7' ),
					'[your-name] [your-email]'
				) . "\n"
				. sprintf(
					/* translators: %s: [your-subject] */
					__( 'Subject: %s', 'contact-form-7' ),
					'[your-subject]'
				) . "\n\n"
				. __( 'Message Body:', 'contact-form-7' )
				. "\n" . '[your-message]' . "\n\n"
				. '-- ' . "\n"
				. sprintf(
					/* translators: 1: blog name, 2: blog URL */
					__( 'This e-mail was sent from a contact form on %1$s (%2$s)', 'contact-form-7' ),
					'[_site_title]',
					'[_site_url]'
				),
			'recipient' => '[_site_admin_email]',
			'additional_headers' => 'Reply-To: [your-email]',
			'attachments' => '',
			'use_html' => 0,
			'exclude_blank' => 0,
		);

		return $template;
	}

	public static function mail_2() {
		$template = array(
			'active' => false,
			'subject' => sprintf(
				/* translators: 1: blog name, 2: [your-subject] */
				_x( '%1$s "%2$s"', 'mail subject', 'contact-form-7' ),
				'[_site_title]',
				'[your-subject]'
			),
			'sender' => sprintf(
				'%s <%s>',
				'[_site_title]',
				self::from_email()
			),
			'body' =>
				__( 'Message Body:', 'contact-form-7' )
				. "\n" . '[your-message]' . "\n\n"
				. '-- ' . "\n"
				. sprintf(
					/* translators: 1: blog name, 2: blog URL */
					__( 'This e-mail was sent from a contact form on %1$s (%2$s)', 'contact-form-7' ),
					'[_site_title]',
					'[_site_url]'
				),
			'recipient' => '[your-email]',
			'additional_headers' => sprintf(
				'Reply-To: %s',
				'[_site_admin_email]'
			),
			'attachments' => '',
			'use_html' => 0,
			'exclude_blank' => 0,
		);

		return $template;
	}

	public static function from_email() {
		$admin_email = get_option( 'admin_email' );

		if ( wpcf7_is_localhost() ) {
			return $admin_email;
		}

		$sitename = wp_parse_url( network_home_url(), PHP_URL_HOST );
		$sitename = strtolower( $sitename );

		if ( 'www.' === substr( $sitename, 0, 4 ) ) {
			$sitename = substr( $sitename, 4 );
		}

		if ( strpbrk( $admin_email, '@' ) === '@' . $sitename ) {
			return $admin_email;
		}

		return 'wordpress@' . $sitename;
	}

	public static function messages() {
		$messages = array();

		foreach ( wpcf7_messages() as $key => $arr ) {
			$messages[$key] = $arr['default'];
		}

		return $messages;
	}
}

function wpcf7_messages() {
	$messages = array(
		'mail_sent_ok' => array(
			'description'
				=> __( "Sender's message was sent successfully", 'contact-form-7' ),
			'default'
				=> __( "Thank you for your message. It has been sent.", 'contact-form-7' ),
		),

		'mail_sent_ng' => array(
			'description'
				=> __( "Sender's message failed to send", 'contact-form-7' ),
			'default'
				=> __( "There was an error trying to send your message. Please try again later.", 'contact-form-7' ),
		),

		'validation_error' => array(
			'description'
				=> __( "Validation errors occurred", 'contact-form-7' ),
			'default'
				=> __( "One or more fields have an error. Please check and try again.", 'contact-form-7' ),
		),

		'spam' => array(
			'description'
				=> __( "Submission was referred to as spam", 'contact-form-7' ),
			'default'
				=> __( "There was an error trying to send your message. Please try again later.", 'contact-form-7' ),
		),

		'accept_terms' => array(
			'description'
				=> __( "There are terms that the sender must accept", 'contact-form-7' ),
			'default'
				=> __( "You must accept the terms and conditions before sending your message.", 'contact-form-7' ),
		),

		'invalid_required' => array(
			'description'
				=> __( "There is a field that the sender must fill in", 'contact-form-7' ),
			'default'
				=> __( "Please fill out this field.", 'contact-form-7' ),
		),

		'invalid_too_long' => array(
			'description'
				=> __( "There is a field with input that is longer than the maximum allowed length", 'contact-form-7' ),
			'default'
				=> __( "This field has a too long input.", 'contact-form-7' ),
		),

		'invalid_too_short' => array(
			'description'
				=> __( "There is a field with input that is shorter than the minimum allowed length", 'contact-form-7' ),
			'default'
				=> __( "This field has a too short input.", 'contact-form-7' ),
		),
	);

	return apply_filters( 'wpcf7_messages', $messages );
}
ms.php000064400000103016151331232430005672 0ustar00<?php
/**
 * Multisite administration functions.
 *
 * @package WordPress
 * @subpackage Multisite
 * @since 3.0.0
 */

/**
 * Determines whether uploaded file exceeds space quota.
 *
 * @since 3.0.0
 *
 * @param array $file An element from the `$_FILES` array for a given file.
 * @return array The `$_FILES` array element with 'error' key set if file exceeds quota. 'error' is empty otherwise.
 */
function check_upload_size( $file ) {
	if ( get_site_option( 'upload_space_check_disabled' ) ) {
		return $file;
	}

	if ( $file['error'] > 0 ) { // There's already an error.
		return $file;
	}

	if ( defined( 'WP_IMPORTING' ) ) {
		return $file;
	}

	$space_left = get_upload_space_available();

	$file_size = filesize( $file['tmp_name'] );
	if ( $space_left < $file_size ) {
		/* translators: %s: Required disk space in kilobytes. */
		$file['error'] = sprintf( __( 'Not enough space to upload. %s KB needed.' ), number_format( ( $file_size - $space_left ) / KB_IN_BYTES ) );
	}

	if ( $file_size > ( KB_IN_BYTES * get_site_option( 'fileupload_maxk', 1500 ) ) ) {
		/* translators: %s: Maximum allowed file size in kilobytes. */
		$file['error'] = sprintf( __( 'This file is too big. Files must be less than %s KB in size.' ), get_site_option( 'fileupload_maxk', 1500 ) );
	}

	if ( upload_is_user_over_quota( false ) ) {
		$file['error'] = __( 'You have used your space quota. Please delete files before uploading.' );
	}

	if ( $file['error'] > 0 && ! isset( $_POST['html-upload'] ) && ! wp_doing_ajax() ) {
		wp_die( $file['error'] . ' <a href="javascript:history.go(-1)">' . __( 'Back' ) . '</a>' );
	}

	return $file;
}

/**
 * Deletes a site.
 *
 * @since 3.0.0
 * @since 5.1.0 Use wp_delete_site() internally to delete the site row from the database.
 *
 * @param int  $blog_id Site ID.
 * @param bool $drop    True if site's database tables should be dropped. Default false.
 */
function wpmu_delete_blog( $blog_id, $drop = false ) {
	$blog_id = (int) $blog_id;

	$switch = false;
	if ( get_current_blog_id() !== $blog_id ) {
		$switch = true;
		switch_to_blog( $blog_id );
	}

	$blog = get_site( $blog_id );

	$current_network = get_network();

	// If a full blog object is not available, do not destroy anything.
	if ( $drop && ! $blog ) {
		$drop = false;
	}

	// Don't destroy the initial, main, or root blog.
	if ( $drop
		&& ( 1 === $blog_id || is_main_site( $blog_id )
			|| ( $blog->path === $current_network->path && $blog->domain === $current_network->domain ) )
	) {
		$drop = false;
	}

	$upload_path = trim( get_option( 'upload_path' ) );

	// If ms_files_rewriting is enabled and upload_path is empty, wp_upload_dir is not reliable.
	if ( $drop && get_site_option( 'ms_files_rewriting' ) && empty( $upload_path ) ) {
		$drop = false;
	}

	if ( $drop ) {
		wp_delete_site( $blog_id );
	} else {
		/** This action is documented in wp-includes/ms-blogs.php */
		do_action_deprecated( 'delete_blog', array( $blog_id, false ), '5.1.0' );

		$users = get_users(
			array(
				'blog_id' => $blog_id,
				'fields'  => 'ids',
			)
		);

		// Remove users from this blog.
		if ( ! empty( $users ) ) {
			foreach ( $users as $user_id ) {
				remove_user_from_blog( $user_id, $blog_id );
			}
		}

		update_blog_status( $blog_id, 'deleted', 1 );

		/** This action is documented in wp-includes/ms-blogs.php */
		do_action_deprecated( 'deleted_blog', array( $blog_id, false ), '5.1.0' );
	}

	if ( $switch ) {
		restore_current_blog();
	}
}

/**
 * Deletes a user and all of their posts from the network.
 *
 * This function:
 *
 * - Deletes all posts (of all post types) authored by the user on all sites on the network
 * - Deletes all links owned by the user on all sites on the network
 * - Removes the user from all sites on the network
 * - Deletes the user from the database
 *
 * @since 3.0.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param int $id The user ID.
 * @return bool True if the user was deleted, false otherwise.
 */
function wpmu_delete_user( $id ) {
	global $wpdb;

	if ( ! is_numeric( $id ) ) {
		return false;
	}

	$id   = (int) $id;
	$user = new WP_User( $id );

	if ( ! $user->exists() ) {
		return false;
	}

	// Global super-administrators are protected, and cannot be deleted.
	$_super_admins = get_super_admins();
	if ( in_array( $user->user_login, $_super_admins, true ) ) {
		return false;
	}

	/**
	 * Fires before a user is deleted from the network.
	 *
	 * @since MU (3.0.0)
	 * @since 5.5.0 Added the `$user` parameter.
	 *
	 * @param int     $id   ID of the user about to be deleted from the network.
	 * @param WP_User $user WP_User object of the user about to be deleted from the network.
	 */
	do_action( 'wpmu_delete_user', $id, $user );

	$blogs = get_blogs_of_user( $id );

	if ( ! empty( $blogs ) ) {
		foreach ( $blogs as $blog ) {
			switch_to_blog( $blog->userblog_id );
			remove_user_from_blog( $id, $blog->userblog_id );

			$post_ids = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_author = %d", $id ) );
			foreach ( (array) $post_ids as $post_id ) {
				wp_delete_post( $post_id );
			}

			// Clean links.
			$link_ids = $wpdb->get_col( $wpdb->prepare( "SELECT link_id FROM $wpdb->links WHERE link_owner = %d", $id ) );

			if ( $link_ids ) {
				foreach ( $link_ids as $link_id ) {
					wp_delete_link( $link_id );
				}
			}

			restore_current_blog();
		}
	}

	$meta = $wpdb->get_col( $wpdb->prepare( "SELECT umeta_id FROM $wpdb->usermeta WHERE user_id = %d", $id ) );
	foreach ( $meta as $mid ) {
		delete_metadata_by_mid( 'user', $mid );
	}

	$wpdb->delete( $wpdb->users, array( 'ID' => $id ) );

	clean_user_cache( $user );

	/** This action is documented in wp-admin/includes/user.php */
	do_action( 'deleted_user', $id, null, $user );

	return true;
}

/**
 * Checks whether a site has used its allotted upload space.
 *
 * @since MU (3.0.0)
 *
 * @param bool $display_message Optional. If set to true and the quota is exceeded,
 *                              a warning message is displayed. Default true.
 * @return bool True if user is over upload space quota, otherwise false.
 */
function upload_is_user_over_quota( $display_message = true ) {
	if ( get_site_option( 'upload_space_check_disabled' ) ) {
		return false;
	}

	$space_allowed = get_space_allowed();
	if ( ! is_numeric( $space_allowed ) ) {
		$space_allowed = 10; // Default space allowed is 10 MB.
	}
	$space_used = get_space_used();

	if ( ( $space_allowed - $space_used ) < 0 ) {
		if ( $display_message ) {
			printf(
				/* translators: %s: Allowed space allocation. */
				__( 'Sorry, you have used your space allocation of %s. Please delete some files to upload more files.' ),
				size_format( $space_allowed * MB_IN_BYTES )
			);
		}
		return true;
	} else {
		return false;
	}
}

/**
 * Displays the amount of disk space used by the current site. Not used in core.
 *
 * @since MU (3.0.0)
 */
function display_space_usage() {
	$space_allowed = get_space_allowed();
	$space_used    = get_space_used();

	$percent_used = ( $space_used / $space_allowed ) * 100;

	$space = size_format( $space_allowed * MB_IN_BYTES );
	?>
	<strong>
	<?php
		/* translators: Storage space that's been used. 1: Percentage of used space, 2: Total space allowed in megabytes or gigabytes. */
		printf( __( 'Used: %1$s%% of %2$s' ), number_format( $percent_used ), $space );
	?>
	</strong>
	<?php
}

/**
 * Gets the remaining upload space for this site.
 *
 * @since MU (3.0.0)
 *
 * @param int $size Current max size in bytes.
 * @return int Max size in bytes.
 */
function fix_import_form_size( $size ) {
	if ( upload_is_user_over_quota( false ) ) {
		return 0;
	}
	$available = get_upload_space_available();
	return min( $size, $available );
}

/**
 * Displays the site upload space quota setting form on the Edit Site Settings screen.
 *
 * @since 3.0.0
 *
 * @param int $id The ID of the site to display the setting for.
 */
function upload_space_setting( $id ) {
	switch_to_blog( $id );
	$quota = get_option( 'blog_upload_space' );
	restore_current_blog();

	if ( ! $quota ) {
		$quota = '';
	}

	?>
	<tr>
		<th><label for="blog-upload-space-number"><?php _e( 'Site Upload Space Quota' ); ?></label></th>
		<td>
			<input type="number" step="1" min="0" style="width: 100px"
				name="option[blog_upload_space]" id="blog-upload-space-number"
				aria-describedby="blog-upload-space-desc" value="<?php echo esc_attr( $quota ); ?>" />
			<span id="blog-upload-space-desc"><span class="screen-reader-text">
				<?php
				/* translators: Hidden accessibility text. */
				_e( 'Size in megabytes' );
				?>
			</span> <?php _e( 'MB (Leave blank for network default)' ); ?></span>
		</td>
	</tr>
	<?php
}

/**
 * Cleans the user cache for a specific user.
 *
 * @since 3.0.0
 *
 * @param int $id The user ID.
 * @return int|false The ID of the refreshed user or false if the user does not exist.
 */
function refresh_user_details( $id ) {
	$id = (int) $id;

	$user = get_userdata( $id );
	if ( ! $user ) {
		return false;
	}

	clean_user_cache( $user );

	return $id;
}

/**
 * Returns the language for a language code.
 *
 * @since 3.0.0
 *
 * @param string $code Optional. The two-letter language code. Default empty.
 * @return string The language corresponding to $code if it exists. If it does not exist,
 *                then the first two letters of $code is returned.
 */
function format_code_lang( $code = '' ) {
	$code       = strtolower( substr( $code, 0, 2 ) );
	$lang_codes = array(
		'aa' => 'Afar',
		'ab' => 'Abkhazian',
		'af' => 'Afrikaans',
		'ak' => 'Akan',
		'sq' => 'Albanian',
		'am' => 'Amharic',
		'ar' => 'Arabic',
		'an' => 'Aragonese',
		'hy' => 'Armenian',
		'as' => 'Assamese',
		'av' => 'Avaric',
		'ae' => 'Avestan',
		'ay' => 'Aymara',
		'az' => 'Azerbaijani',
		'ba' => 'Bashkir',
		'bm' => 'Bambara',
		'eu' => 'Basque',
		'be' => 'Belarusian',
		'bn' => 'Bengali',
		'bh' => 'Bihari',
		'bi' => 'Bislama',
		'bs' => 'Bosnian',
		'br' => 'Breton',
		'bg' => 'Bulgarian',
		'my' => 'Burmese',
		'ca' => 'Catalan; Valencian',
		'ch' => 'Chamorro',
		'ce' => 'Chechen',
		'zh' => 'Chinese',
		'cu' => 'Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic',
		'cv' => 'Chuvash',
		'kw' => 'Cornish',
		'co' => 'Corsican',
		'cr' => 'Cree',
		'cs' => 'Czech',
		'da' => 'Danish',
		'dv' => 'Divehi; Dhivehi; Maldivian',
		'nl' => 'Dutch; Flemish',
		'dz' => 'Dzongkha',
		'en' => 'English',
		'eo' => 'Esperanto',
		'et' => 'Estonian',
		'ee' => 'Ewe',
		'fo' => 'Faroese',
		'fj' => 'Fijjian',
		'fi' => 'Finnish',
		'fr' => 'French',
		'fy' => 'Western Frisian',
		'ff' => 'Fulah',
		'ka' => 'Georgian',
		'de' => 'German',
		'gd' => 'Gaelic; Scottish Gaelic',
		'ga' => 'Irish',
		'gl' => 'Galician',
		'gv' => 'Manx',
		'el' => 'Greek, Modern',
		'gn' => 'Guarani',
		'gu' => 'Gujarati',
		'ht' => 'Haitian; Haitian Creole',
		'ha' => 'Hausa',
		'he' => 'Hebrew',
		'hz' => 'Herero',
		'hi' => 'Hindi',
		'ho' => 'Hiri Motu',
		'hu' => 'Hungarian',
		'ig' => 'Igbo',
		'is' => 'Icelandic',
		'io' => 'Ido',
		'ii' => 'Sichuan Yi',
		'iu' => 'Inuktitut',
		'ie' => 'Interlingue',
		'ia' => 'Interlingua (International Auxiliary Language Association)',
		'id' => 'Indonesian',
		'ik' => 'Inupiaq',
		'it' => 'Italian',
		'jv' => 'Javanese',
		'ja' => 'Japanese',
		'kl' => 'Kalaallisut; Greenlandic',
		'kn' => 'Kannada',
		'ks' => 'Kashmiri',
		'kr' => 'Kanuri',
		'kk' => 'Kazakh',
		'km' => 'Central Khmer',
		'ki' => 'Kikuyu; Gikuyu',
		'rw' => 'Kinyarwanda',
		'ky' => 'Kirghiz; Kyrgyz',
		'kv' => 'Komi',
		'kg' => 'Kongo',
		'ko' => 'Korean',
		'kj' => 'Kuanyama; Kwanyama',
		'ku' => 'Kurdish',
		'lo' => 'Lao',
		'la' => 'Latin',
		'lv' => 'Latvian',
		'li' => 'Limburgan; Limburger; Limburgish',
		'ln' => 'Lingala',
		'lt' => 'Lithuanian',
		'lb' => 'Luxembourgish; Letzeburgesch',
		'lu' => 'Luba-Katanga',
		'lg' => 'Ganda',
		'mk' => 'Macedonian',
		'mh' => 'Marshallese',
		'ml' => 'Malayalam',
		'mi' => 'Maori',
		'mr' => 'Marathi',
		'ms' => 'Malay',
		'mg' => 'Malagasy',
		'mt' => 'Maltese',
		'mo' => 'Moldavian',
		'mn' => 'Mongolian',
		'na' => 'Nauru',
		'nv' => 'Navajo; Navaho',
		'nr' => 'Ndebele, South; South Ndebele',
		'nd' => 'Ndebele, North; North Ndebele',
		'ng' => 'Ndonga',
		'ne' => 'Nepali',
		'nn' => 'Norwegian Nynorsk; Nynorsk, Norwegian',
		'nb' => 'Bokmål, Norwegian, Norwegian Bokmål',
		'no' => 'Norwegian',
		'ny' => 'Chichewa; Chewa; Nyanja',
		'oc' => 'Occitan, Provençal',
		'oj' => 'Ojibwa',
		'or' => 'Oriya',
		'om' => 'Oromo',
		'os' => 'Ossetian; Ossetic',
		'pa' => 'Panjabi; Punjabi',
		'fa' => 'Persian',
		'pi' => 'Pali',
		'pl' => 'Polish',
		'pt' => 'Portuguese',
		'ps' => 'Pushto',
		'qu' => 'Quechua',
		'rm' => 'Romansh',
		'ro' => 'Romanian',
		'rn' => 'Rundi',
		'ru' => 'Russian',
		'sg' => 'Sango',
		'sa' => 'Sanskrit',
		'sr' => 'Serbian',
		'hr' => 'Croatian',
		'si' => 'Sinhala; Sinhalese',
		'sk' => 'Slovak',
		'sl' => 'Slovenian',
		'se' => 'Northern Sami',
		'sm' => 'Samoan',
		'sn' => 'Shona',
		'sd' => 'Sindhi',
		'so' => 'Somali',
		'st' => 'Sotho, Southern',
		'es' => 'Spanish; Castilian',
		'sc' => 'Sardinian',
		'ss' => 'Swati',
		'su' => 'Sundanese',
		'sw' => 'Swahili',
		'sv' => 'Swedish',
		'ty' => 'Tahitian',
		'ta' => 'Tamil',
		'tt' => 'Tatar',
		'te' => 'Telugu',
		'tg' => 'Tajik',
		'tl' => 'Tagalog',
		'th' => 'Thai',
		'bo' => 'Tibetan',
		'ti' => 'Tigrinya',
		'to' => 'Tonga (Tonga Islands)',
		'tn' => 'Tswana',
		'ts' => 'Tsonga',
		'tk' => 'Turkmen',
		'tr' => 'Turkish',
		'tw' => 'Twi',
		'ug' => 'Uighur; Uyghur',
		'uk' => 'Ukrainian',
		'ur' => 'Urdu',
		'uz' => 'Uzbek',
		've' => 'Venda',
		'vi' => 'Vietnamese',
		'vo' => 'Volapük',
		'cy' => 'Welsh',
		'wa' => 'Walloon',
		'wo' => 'Wolof',
		'xh' => 'Xhosa',
		'yi' => 'Yiddish',
		'yo' => 'Yoruba',
		'za' => 'Zhuang; Chuang',
		'zu' => 'Zulu',
	);

	/**
	 * Filters the language codes.
	 *
	 * @since MU (3.0.0)
	 *
	 * @param string[] $lang_codes Array of key/value pairs of language codes where key is the short version.
	 * @param string   $code       A two-letter designation of the language.
	 */
	$lang_codes = apply_filters( 'lang_codes', $lang_codes, $code );
	return strtr( $code, $lang_codes );
}

/**
 * Displays an access denied message when a user tries to view a site's dashboard they
 * do not have access to.
 *
 * @since 3.2.0
 * @access private
 */
function _access_denied_splash() {
	if ( ! is_user_logged_in() || is_network_admin() ) {
		return;
	}

	$blogs = get_blogs_of_user( get_current_user_id() );

	if ( wp_list_filter( $blogs, array( 'userblog_id' => get_current_blog_id() ) ) ) {
		return;
	}

	$blog_name = get_bloginfo( 'name' );

	if ( empty( $blogs ) ) {
		wp_die(
			sprintf(
				/* translators: 1: Site title. */
				__( 'You attempted to access the "%1$s" dashboard, but you do not currently have privileges on this site. If you believe you should be able to access the "%1$s" dashboard, please contact your network administrator.' ),
				$blog_name
			),
			403
		);
	}

	$output = '<p>' . sprintf(
		/* translators: 1: Site title. */
		__( 'You attempted to access the "%1$s" dashboard, but you do not currently have privileges on this site. If you believe you should be able to access the "%1$s" dashboard, please contact your network administrator.' ),
		$blog_name
	) . '</p>';
	$output .= '<p>' . __( 'If you reached this screen by accident and meant to visit one of your own sites, here are some shortcuts to help you find your way.' ) . '</p>';

	$output .= '<h3>' . __( 'Your Sites' ) . '</h3>';
	$output .= '<table>';

	foreach ( $blogs as $blog ) {
		$output .= '<tr>';
		$output .= "<td>{$blog->blogname}</td>";
		$output .= '<td><a href="' . esc_url( get_admin_url( $blog->userblog_id ) ) . '">' . __( 'Visit Dashboard' ) . '</a> | ' .
			'<a href="' . esc_url( get_home_url( $blog->userblog_id ) ) . '">' . __( 'View Site' ) . '</a></td>';
		$output .= '</tr>';
	}

	$output .= '</table>';

	wp_die( $output, 403 );
}

/**
 * Checks if the current user has permissions to import new users.
 *
 * @since 3.0.0
 *
 * @param string $permission A permission to be checked. Currently not used.
 * @return bool True if the user has proper permissions, false if they do not.
 */
function check_import_new_users( $permission ) {
	if ( ! current_user_can( 'manage_network_users' ) ) {
		return false;
	}

	return true;
}
// See "import_allow_fetch_attachments" and "import_attachment_size_limit" filters too.

/**
 * Generates and displays a drop-down of available languages.
 *
 * @since 3.0.0
 *
 * @param string[] $lang_files Optional. An array of the language files. Default empty array.
 * @param string   $current    Optional. The current language code. Default empty.
 */
function mu_dropdown_languages( $lang_files = array(), $current = '' ) {
	$flag   = false;
	$output = array();

	foreach ( (array) $lang_files as $val ) {
		$code_lang = basename( $val, '.mo' );

		if ( 'en_US' === $code_lang ) { // American English.
			$flag          = true;
			$ae            = __( 'American English' );
			$output[ $ae ] = '<option value="' . esc_attr( $code_lang ) . '"' . selected( $current, $code_lang, false ) . '> ' . $ae . '</option>';
		} elseif ( 'en_GB' === $code_lang ) { // British English.
			$flag          = true;
			$be            = __( 'British English' );
			$output[ $be ] = '<option value="' . esc_attr( $code_lang ) . '"' . selected( $current, $code_lang, false ) . '> ' . $be . '</option>';
		} else {
			$translated            = format_code_lang( $code_lang );
			$output[ $translated ] = '<option value="' . esc_attr( $code_lang ) . '"' . selected( $current, $code_lang, false ) . '> ' . esc_html( $translated ) . '</option>';
		}
	}

	if ( false === $flag ) { // WordPress English.
		$output[] = '<option value=""' . selected( $current, '', false ) . '>' . __( 'English' ) . '</option>';
	}

	// Order by name.
	uksort( $output, 'strnatcasecmp' );

	/**
	 * Filters the languages available in the dropdown.
	 *
	 * @since MU (3.0.0)
	 *
	 * @param string[] $output     Array of HTML output for the dropdown.
	 * @param string[] $lang_files Array of available language files.
	 * @param string   $current    The current language code.
	 */
	$output = apply_filters( 'mu_dropdown_languages', $output, $lang_files, $current );

	echo implode( "\n\t", $output );
}

/**
 * Displays an admin notice to upgrade all sites after a core upgrade.
 *
 * @since 3.0.0
 *
 * @global int    $wp_db_version WordPress database version.
 * @global string $pagenow       The filename of the current screen.
 *
 * @return void|false Void on success. False if the current user is not a super admin.
 */
function site_admin_notice() {
	global $wp_db_version, $pagenow;

	if ( ! current_user_can( 'upgrade_network' ) ) {
		return false;
	}

	if ( 'upgrade.php' === $pagenow ) {
		return;
	}

	if ( (int) get_site_option( 'wpmu_upgrade_site' ) !== $wp_db_version ) {
		$upgrade_network_message = sprintf(
			/* translators: %s: URL to Upgrade Network screen. */
			__( 'Thank you for Updating! Please visit the <a href="%s">Upgrade Network</a> page to update all your sites.' ),
			esc_url( network_admin_url( 'upgrade.php' ) )
		);

		wp_admin_notice(
			$upgrade_network_message,
			array(
				'type'               => 'warning',
				'additional_classes' => array( 'update-nag', 'inline' ),
				'paragraph_wrap'     => false,
			)
		);
	}
}

/**
 * Avoids a collision between a site slug and a permalink slug.
 *
 * In a subdirectory installation this will make sure that a site and a post do not use the
 * same subdirectory by checking for a site with the same name as a new post.
 *
 * @since 3.0.0
 *
 * @param array $data    An array of post data.
 * @param array $postarr An array of posts. Not currently used.
 * @return array The new array of post data after checking for collisions.
 */
function avoid_blog_page_permalink_collision( $data, $postarr ) {
	if ( is_subdomain_install() ) {
		return $data;
	}
	if ( 'page' !== $data['post_type'] ) {
		return $data;
	}
	if ( ! isset( $data['post_name'] ) || '' === $data['post_name'] ) {
		return $data;
	}
	if ( ! is_main_site() ) {
		return $data;
	}
	if ( isset( $data['post_parent'] ) && $data['post_parent'] ) {
		return $data;
	}

	$post_name = $data['post_name'];
	$c         = 0;

	while ( $c < 10 && get_id_from_blogname( $post_name ) ) {
		$post_name .= mt_rand( 1, 10 );
		++$c;
	}

	if ( $post_name !== $data['post_name'] ) {
		$data['post_name'] = $post_name;
	}

	return $data;
}

/**
 * Handles the display of choosing a user's primary site.
 *
 * This displays the user's primary site and allows the user to choose
 * which site is primary.
 *
 * @since 3.0.0
 */
function choose_primary_blog() {
	?>
	<table class="form-table" role="presentation">
	<tr>
	<?php /* translators: My Sites label. */ ?>
		<th scope="row"><label for="primary_blog"><?php _e( 'Primary Site' ); ?></label></th>
		<td>
		<?php
		$all_blogs    = get_blogs_of_user( get_current_user_id() );
		$primary_blog = (int) get_user_meta( get_current_user_id(), 'primary_blog', true );
		if ( count( $all_blogs ) > 1 ) {
			$found = false;
			?>
			<select name="primary_blog" id="primary_blog">
				<?php
				foreach ( (array) $all_blogs as $blog ) {
					if ( $blog->userblog_id === $primary_blog ) {
						$found = true;
					}
					?>
					<option value="<?php echo $blog->userblog_id; ?>"<?php selected( $primary_blog, $blog->userblog_id ); ?>><?php echo esc_url( get_home_url( $blog->userblog_id ) ); ?></option>
					<?php
				}
				?>
			</select>
			<?php
			if ( ! $found ) {
				$blog = reset( $all_blogs );
				update_user_meta( get_current_user_id(), 'primary_blog', $blog->userblog_id );
			}
		} elseif ( 1 === count( $all_blogs ) ) {
			$blog = reset( $all_blogs );
			echo esc_url( get_home_url( $blog->userblog_id ) );
			if ( $blog->userblog_id !== $primary_blog ) { // Set the primary blog again if it's out of sync with blog list.
				update_user_meta( get_current_user_id(), 'primary_blog', $blog->userblog_id );
			}
		} else {
			_e( 'Not available' );
		}
		?>
		</td>
	</tr>
	</table>
	<?php
}

/**
 * Determines whether or not this network from this page can be edited.
 *
 * By default editing of network is restricted to the Network Admin for that `$network_id`.
 * This function allows for this to be overridden.
 *
 * @since 3.1.0
 *
 * @param int $network_id The network ID to check.
 * @return bool True if network can be edited, false otherwise.
 */
function can_edit_network( $network_id ) {
	if ( get_current_network_id() === (int) $network_id ) {
		$result = true;
	} else {
		$result = false;
	}

	/**
	 * Filters whether this network can be edited from this page.
	 *
	 * @since 3.1.0
	 *
	 * @param bool $result     Whether the network can be edited from this page.
	 * @param int  $network_id The network ID to check.
	 */
	return apply_filters( 'can_edit_network', $result, $network_id );
}

/**
 * Prints thickbox image paths for Network Admin.
 *
 * @since 3.1.0
 *
 * @access private
 */
function _thickbox_path_admin_subfolder() {
	?>
<script type="text/javascript">
var tb_pathToImage = "<?php echo esc_js( includes_url( 'js/thickbox/loadingAnimation.gif', 'relative' ) ); ?>";
</script>
	<?php
}

/**
 * @param array $users
 * @return bool
 */
function confirm_delete_users( $users ) {
	$current_user = wp_get_current_user();
	if ( ! is_array( $users ) || empty( $users ) ) {
		return false;
	}
	?>
	<h1><?php esc_html_e( 'Users' ); ?></h1>

	<?php if ( 1 === count( $users ) ) : ?>
		<p><?php _e( 'You have chosen to delete the user from all networks and sites.' ); ?></p>
	<?php else : ?>
		<p><?php _e( 'You have chosen to delete the following users from all networks and sites.' ); ?></p>
	<?php endif; ?>

	<form action="users.php?action=dodelete" method="post">
	<input type="hidden" name="dodelete" />
	<?php
	wp_nonce_field( 'ms-users-delete' );
	$site_admins = get_super_admins();
	$admin_out   = '<option value="' . esc_attr( $current_user->ID ) . '">' . $current_user->user_login . '</option>';
	?>
	<table class="form-table" role="presentation">
	<?php
	$allusers = (array) $_POST['allusers'];
	foreach ( $allusers as $user_id ) {
		if ( '' !== $user_id && '0' !== $user_id ) {
			$delete_user = get_userdata( $user_id );

			if ( ! current_user_can( 'delete_user', $delete_user->ID ) ) {
				wp_die(
					sprintf(
						/* translators: %s: User login. */
						__( 'Warning! User %s cannot be deleted.' ),
						$delete_user->user_login
					)
				);
			}

			if ( in_array( $delete_user->user_login, $site_admins, true ) ) {
				wp_die(
					sprintf(
						/* translators: %s: User login. */
						__( 'Warning! User cannot be deleted. The user %s is a network administrator.' ),
						'<em>' . $delete_user->user_login . '</em>'
					)
				);
			}
			?>
			<tr>
				<th scope="row"><?php echo $delete_user->user_login; ?>
					<?php echo '<input type="hidden" name="user[]" value="' . esc_attr( $user_id ) . '" />' . "\n"; ?>
				</th>
			<?php
			$blogs = get_blogs_of_user( $user_id, true );

			if ( ! empty( $blogs ) ) {
				?>
				<td><fieldset><p><legend>
				<?php
				printf(
					/* translators: %s: User login. */
					__( 'What should be done with content owned by %s?' ),
					'<em>' . $delete_user->user_login . '</em>'
				);
				?>
				</legend></p>
				<?php
				foreach ( (array) $blogs as $key => $details ) {
					$blog_users = get_users(
						array(
							'blog_id' => $details->userblog_id,
							'fields'  => array( 'ID', 'user_login' ),
						)
					);

					if ( is_array( $blog_users ) && ! empty( $blog_users ) ) {
						$user_site     = "<a href='" . esc_url( get_home_url( $details->userblog_id ) ) . "'>{$details->blogname}</a>";
						$user_dropdown = '<label for="reassign_user" class="screen-reader-text">' .
								/* translators: Hidden accessibility text. */
								__( 'Select a user' ) .
							'</label>';
						$user_dropdown .= "<select name='blog[$user_id][$key]' id='reassign_user'>";
						$user_list      = '';

						foreach ( $blog_users as $user ) {
							if ( ! in_array( (int) $user->ID, $allusers, true ) ) {
								$user_list .= "<option value='{$user->ID}'>{$user->user_login}</option>";
							}
						}

						if ( '' === $user_list ) {
							$user_list = $admin_out;
						}

						$user_dropdown .= $user_list;
						$user_dropdown .= "</select>\n";
						?>
						<ul style="list-style:none;">
							<li>
								<?php
								/* translators: %s: Link to user's site. */
								printf( __( 'Site: %s' ), $user_site );
								?>
							</li>
							<li><label><input type="radio" id="delete_option0" name="delete[<?php echo $details->userblog_id . '][' . $delete_user->ID; ?>]" value="delete" checked="checked" />
							<?php _e( 'Delete all content.' ); ?></label></li>
							<li><label><input type="radio" id="delete_option1" name="delete[<?php echo $details->userblog_id . '][' . $delete_user->ID; ?>]" value="reassign" />
							<?php _e( 'Attribute all content to:' ); ?></label>
							<?php echo $user_dropdown; ?></li>
						</ul>
						<?php
					}
				}
				echo '</fieldset></td></tr>';
			} else {
				?>
				<td><p><?php _e( 'User has no sites or content and will be deleted.' ); ?></p></td>
			<?php } ?>
			</tr>
			<?php
		}
	}

	?>
	</table>
	<?php
	/** This action is documented in wp-admin/users.php */
	do_action( 'delete_user_form', $current_user, $allusers );

	if ( 1 === count( $users ) ) :
		?>
		<p><?php _e( 'Once you hit &#8220;Confirm Deletion&#8221;, the user will be permanently removed.' ); ?></p>
	<?php else : ?>
		<p><?php _e( 'Once you hit &#8220;Confirm Deletion&#8221;, these users will be permanently removed.' ); ?></p>
		<?php
	endif;

	submit_button( __( 'Confirm Deletion' ), 'primary' );
	?>
	</form>
	<?php
	return true;
}

/**
 * Prints JavaScript in the header on the Network Settings screen.
 *
 * @since 4.1.0
 */
function network_settings_add_js() {
	?>
<script type="text/javascript">
jQuery( function($) {
	var languageSelect = $( '#WPLANG' );
	$( 'form' ).on( 'submit', function() {
		/*
		 * Don't show a spinner for English and installed languages,
		 * as there is nothing to download.
		 */
		if ( ! languageSelect.find( 'option:selected' ).data( 'installed' ) ) {
			$( '#submit', this ).after( '<span class="spinner language-install-spinner is-active" />' );
		}
	});
} );
</script>
	<?php
}

/**
 * Outputs the HTML for a network's "Edit Site" tabular interface.
 *
 * @since 4.6.0
 *
 * @global string $pagenow The filename of the current screen.
 *
 * @param array $args {
 *     Optional. Array or string of Query parameters. Default empty array.
 *
 *     @type int    $blog_id  The site ID. Default is the current site.
 *     @type array  $links    The tabs to include with (label|url|cap) keys.
 *     @type string $selected The ID of the selected link.
 * }
 */
function network_edit_site_nav( $args = array() ) {

	/**
	 * Filters the links that appear on site-editing network pages.
	 *
	 * Default links: 'site-info', 'site-users', 'site-themes', and 'site-settings'.
	 *
	 * @since 4.6.0
	 *
	 * @param array $links {
	 *     An array of link data representing individual network admin pages.
	 *
	 *     @type array $link_slug {
	 *         An array of information about the individual link to a page.
	 *
	 *         $type string $label Label to use for the link.
	 *         $type string $url   URL, relative to `network_admin_url()` to use for the link.
	 *         $type string $cap   Capability required to see the link.
	 *     }
	 * }
	 */
	$links = apply_filters(
		'network_edit_site_nav_links',
		array(
			'site-info'     => array(
				'label' => __( 'Info' ),
				'url'   => 'site-info.php',
				'cap'   => 'manage_sites',
			),
			'site-users'    => array(
				'label' => __( 'Users' ),
				'url'   => 'site-users.php',
				'cap'   => 'manage_sites',
			),
			'site-themes'   => array(
				'label' => __( 'Themes' ),
				'url'   => 'site-themes.php',
				'cap'   => 'manage_sites',
			),
			'site-settings' => array(
				'label' => __( 'Settings' ),
				'url'   => 'site-settings.php',
				'cap'   => 'manage_sites',
			),
		)
	);

	// Parse arguments.
	$parsed_args = wp_parse_args(
		$args,
		array(
			'blog_id'  => isset( $_GET['blog_id'] ) ? (int) $_GET['blog_id'] : 0,
			'links'    => $links,
			'selected' => 'site-info',
		)
	);

	// Setup the links array.
	$screen_links = array();

	// Loop through tabs.
	foreach ( $parsed_args['links'] as $link_id => $link ) {

		// Skip link if user can't access.
		if ( ! current_user_can( $link['cap'], $parsed_args['blog_id'] ) ) {
			continue;
		}

		// Link classes.
		$classes = array( 'nav-tab' );

		// Aria-current attribute.
		$aria_current = '';

		// Selected is set by the parent OR assumed by the $pagenow global.
		if ( $parsed_args['selected'] === $link_id || $link['url'] === $GLOBALS['pagenow'] ) {
			$classes[]    = 'nav-tab-active';
			$aria_current = ' aria-current="page"';
		}

		// Escape each class.
		$esc_classes = implode( ' ', $classes );

		// Get the URL for this link.
		$url = add_query_arg( array( 'id' => $parsed_args['blog_id'] ), network_admin_url( $link['url'] ) );

		// Add link to nav links.
		$screen_links[ $link_id ] = '<a href="' . esc_url( $url ) . '" id="' . esc_attr( $link_id ) . '" class="' . $esc_classes . '"' . $aria_current . '>' . esc_html( $link['label'] ) . '</a>';
	}

	// All done!
	echo '<nav class="nav-tab-wrapper wp-clearfix" aria-label="' . esc_attr__( 'Secondary menu' ) . '">';
	echo implode( '', $screen_links );
	echo '</nav>';
}

/**
 * Returns the arguments for the help tab on the Edit Site screens.
 *
 * @since 4.9.0
 *
 * @return array Help tab arguments.
 */
function get_site_screen_help_tab_args() {
	return array(
		'id'      => 'overview',
		'title'   => __( 'Overview' ),
		'content' =>
			'<p>' . __( 'The menu is for editing information specific to individual sites, particularly if the admin area of a site is unavailable.' ) . '</p>' .
			'<p>' . __( '<strong>Info</strong> &mdash; The site URL is rarely edited as this can cause the site to not work properly. The Registered date and Last Updated date are displayed. Network admins can mark a site as archived, spam, deleted and mature, to remove from public listings or disable.' ) . '</p>' .
			'<p>' . __( '<strong>Users</strong> &mdash; This displays the users associated with this site. You can also change their role, reset their password, or remove them from the site. Removing the user from the site does not remove the user from the network.' ) . '</p>' .
			'<p>' . sprintf(
				/* translators: %s: URL to Network Themes screen. */
				__( '<strong>Themes</strong> &mdash; This area shows themes that are not already enabled across the network. Enabling a theme in this menu makes it accessible to this site. It does not activate the theme, but allows it to show in the site&#8217;s Appearance menu. To enable a theme for the entire network, see the <a href="%s">Network Themes</a> screen.' ),
				network_admin_url( 'themes.php' )
			) . '</p>' .
			'<p>' . __( '<strong>Settings</strong> &mdash; This page shows a list of all settings associated with this site. Some are created by WordPress and others are created by plugins you activate. Note that some fields are grayed out and say Serialized Data. You cannot modify these values due to the way the setting is stored in the database.' ) . '</p>',
	);
}

/**
 * Returns the content for the help sidebar on the Edit Site screens.
 *
 * @since 4.9.0
 *
 * @return string Help sidebar content.
 */
function get_site_screen_help_sidebar_content() {
	return '<p><strong>' . __( 'For more information:' ) . '</strong></p>' .
		'<p>' . __( '<a href="https://developer.wordpress.org/advanced-administration/multisite/admin/#network-admin-sites-screen">Documentation on Site Management</a>' ) . '</p>' .
		'<p>' . __( '<a href="https://wordpress.org/support/forum/multisite/">Support forums</a>' ) . '</p>';
}

/**
 * Stop execution if the role can not be assigned by the current user.
 *
 * @since 6.8.0
 *
 * @param string $role Role the user is attempting to assign.
 */
function wp_ensure_editable_role( $role ) {
	$roles = get_editable_roles();
	if ( ! isset( $roles[ $role ] ) ) {
		wp_die( __( 'Sorry, you are not allowed to give users that role.' ), 403 );
	}
}
class-walker-nav-menu-checklist.php000060400000012774151331232430013344 0ustar00<?php
/**
 * Navigation Menu API: Walker_Nav_Menu_Checklist class
 *
 * @package WordPress
 * @subpackage Administration
 * @since 4.4.0
 */

/**
 * Create HTML list of nav menu input items.
 *
 * @since 3.0.0
 * @uses Walker_Nav_Menu
 */
class Walker_Nav_Menu_Checklist extends Walker_Nav_Menu {
	/**
	 * @param array|false $fields Database fields to use.
	 */
	public function __construct( $fields = false ) {
		if ( $fields ) {
			$this->db_fields = $fields;
		}
	}

	/**
	 * Starts the list before the elements are added.
	 *
	 * @see Walker_Nav_Menu::start_lvl()
	 *
	 * @since 3.0.0
	 *
	 * @param string   $output Used to append additional content (passed by reference).
	 * @param int      $depth  Depth of page. Used for padding.
	 * @param stdClass $args   Not used.
	 */
	public function start_lvl( &$output, $depth = 0, $args = null ) {
		$indent  = str_repeat( "\t", $depth );
		$output .= "\n$indent<ul class='children'>\n";
	}

	/**
	 * Ends the list of after the elements are added.
	 *
	 * @see Walker_Nav_Menu::end_lvl()
	 *
	 * @since 3.0.0
	 *
	 * @param string   $output Used to append additional content (passed by reference).
	 * @param int      $depth  Depth of page. Used for padding.
	 * @param stdClass $args   Not used.
	 */
	public function end_lvl( &$output, $depth = 0, $args = null ) {
		$indent  = str_repeat( "\t", $depth );
		$output .= "\n$indent</ul>";
	}

	/**
	 * Start the element output.
	 *
	 * @see Walker_Nav_Menu::start_el()
	 *
	 * @since 3.0.0
	 * @since 5.9.0 Renamed `$item` to `$data_object` and `$id` to `$current_object_id`
	 *              to match parent class for PHP 8 named parameter support.
	 *
	 * @global int        $_nav_menu_placeholder
	 * @global int|string $nav_menu_selected_id
	 *
	 * @param string   $output            Used to append additional content (passed by reference).
	 * @param WP_Post  $data_object       Menu item data object.
	 * @param int      $depth             Depth of menu item. Used for padding.
	 * @param stdClass $args              Not used.
	 * @param int      $current_object_id Optional. ID of the current menu item. Default 0.
	 */
	public function start_el( &$output, $data_object, $depth = 0, $args = null, $current_object_id = 0 ) {
		global $_nav_menu_placeholder, $nav_menu_selected_id;

		// Restores the more descriptive, specific name for use within this method.
		$menu_item = $data_object;

		$_nav_menu_placeholder = ( 0 > $_nav_menu_placeholder ) ? (int) $_nav_menu_placeholder - 1 : -1;
		$possible_object_id    = isset( $menu_item->post_type ) && 'nav_menu_item' === $menu_item->post_type ? $menu_item->object_id : $_nav_menu_placeholder;
		$possible_db_id        = ( ! empty( $menu_item->ID ) ) && ( 0 < $possible_object_id ) ? (int) $menu_item->ID : 0;

		$indent = ( $depth ) ? str_repeat( "\t", $depth ) : '';

		$output .= $indent . '<li>';
		$output .= '<label class="menu-item-title">';
		$output .= '<input type="checkbox"' . wp_nav_menu_disabled_check( $nav_menu_selected_id, false ) . ' class="menu-item-checkbox';

		if ( ! empty( $menu_item->front_or_home ) ) {
			$output .= ' add-to-top';
		}

		$output .= '" name="menu-item[' . $possible_object_id . '][menu-item-object-id]" value="' . esc_attr( $menu_item->object_id ) . '" /> ';

		if ( ! empty( $menu_item->label ) ) {
			$title = $menu_item->label;
		} elseif ( isset( $menu_item->post_type ) ) {
			/** This filter is documented in wp-includes/post-template.php */
			$title = apply_filters( 'the_title', $menu_item->post_title, $menu_item->ID );
		}

		$output .= isset( $title ) ? esc_html( $title ) : esc_html( $menu_item->title );

		if ( empty( $menu_item->label ) && isset( $menu_item->post_type ) && 'page' === $menu_item->post_type ) {
			// Append post states.
			$output .= _post_states( $menu_item, false );
		}

		$output .= '</label>';

		// Menu item hidden fields.
		$output .= '<input type="hidden" class="menu-item-db-id" name="menu-item[' . $possible_object_id . '][menu-item-db-id]" value="' . $possible_db_id . '" />';
		$output .= '<input type="hidden" class="menu-item-object" name="menu-item[' . $possible_object_id . '][menu-item-object]" value="' . esc_attr( $menu_item->object ) . '" />';
		$output .= '<input type="hidden" class="menu-item-parent-id" name="menu-item[' . $possible_object_id . '][menu-item-parent-id]" value="' . esc_attr( $menu_item->menu_item_parent ) . '" />';
		$output .= '<input type="hidden" class="menu-item-type" name="menu-item[' . $possible_object_id . '][menu-item-type]" value="' . esc_attr( $menu_item->type ) . '" />';
		$output .= '<input type="hidden" class="menu-item-title" name="menu-item[' . $possible_object_id . '][menu-item-title]" value="' . esc_attr( $menu_item->title ) . '" />';
		$output .= '<input type="hidden" class="menu-item-url" name="menu-item[' . $possible_object_id . '][menu-item-url]" value="' . esc_attr( $menu_item->url ) . '" />';
		$output .= '<input type="hidden" class="menu-item-target" name="menu-item[' . $possible_object_id . '][menu-item-target]" value="' . esc_attr( $menu_item->target ) . '" />';
		$output .= '<input type="hidden" class="menu-item-attr-title" name="menu-item[' . $possible_object_id . '][menu-item-attr-title]" value="' . esc_attr( $menu_item->attr_title ) . '" />';
		$output .= '<input type="hidden" class="menu-item-classes" name="menu-item[' . $possible_object_id . '][menu-item-classes]" value="' . esc_attr( implode( ' ', $menu_item->classes ) ) . '" />';
		$output .= '<input type="hidden" class="menu-item-xfn" name="menu-item[' . $possible_object_id . '][menu-item-xfn]" value="' . esc_attr( $menu_item->xfn ) . '" />';
	}
}
misc.php000064400000131327151331232430006214 0ustar00<?php
/**
 * Misc WordPress Administration API.
 *
 * @package WordPress
 * @subpackage Administration
 */

/**
 * Returns whether the server is running Apache with the mod_rewrite module loaded.
 *
 * @since 2.0.0
 *
 * @return bool Whether the server is running Apache with the mod_rewrite module loaded.
 */
function got_mod_rewrite() {
	$got_rewrite = apache_mod_loaded( 'mod_rewrite', true );

	/**
	 * Filters whether Apache and mod_rewrite are present.
	 *
	 * This filter was previously used to force URL rewriting for other servers,
	 * like nginx. Use the {@see 'got_url_rewrite'} filter in got_url_rewrite() instead.
	 *
	 * @since 2.5.0
	 *
	 * @see got_url_rewrite()
	 *
	 * @param bool $got_rewrite Whether Apache and mod_rewrite are present.
	 */
	return apply_filters( 'got_rewrite', $got_rewrite );
}

/**
 * Returns whether the server supports URL rewriting.
 *
 * Detects Apache's mod_rewrite, IIS 7.0+ permalink support, and nginx.
 *
 * @since 3.7.0
 *
 * @global bool $is_nginx
 * @global bool $is_caddy
 *
 * @return bool Whether the server supports URL rewriting.
 */
function got_url_rewrite() {
	$got_url_rewrite = ( got_mod_rewrite() || $GLOBALS['is_nginx'] || $GLOBALS['is_caddy'] || iis7_supports_permalinks() );

	/**
	 * Filters whether URL rewriting is available.
	 *
	 * @since 3.7.0
	 *
	 * @param bool $got_url_rewrite Whether URL rewriting is available.
	 */
	return apply_filters( 'got_url_rewrite', $got_url_rewrite );
}

/**
 * Extracts strings from between the BEGIN and END markers in the .htaccess file.
 *
 * @since 1.5.0
 *
 * @param string $filename Filename to extract the strings from.
 * @param string $marker   The marker to extract the strings from.
 * @return string[] An array of strings from a file (.htaccess) from between BEGIN and END markers.
 */
function extract_from_markers( $filename, $marker ) {
	$result = array();

	if ( ! file_exists( $filename ) ) {
		return $result;
	}

	$markerdata = explode( "\n", implode( '', file( $filename ) ) );

	$state = false;

	foreach ( $markerdata as $markerline ) {
		if ( str_contains( $markerline, '# END ' . $marker ) ) {
			$state = false;
		}

		if ( $state ) {
			if ( str_starts_with( $markerline, '#' ) ) {
				continue;
			}

			$result[] = $markerline;
		}

		if ( str_contains( $markerline, '# BEGIN ' . $marker ) ) {
			$state = true;
		}
	}

	return $result;
}

/**
 * Inserts an array of strings into a file (.htaccess), placing it between
 * BEGIN and END markers.
 *
 * Replaces existing marked info. Retains surrounding
 * data. Creates file if none exists.
 *
 * @since 1.5.0
 *
 * @param string       $filename  Filename to alter.
 * @param string       $marker    The marker to alter.
 * @param array|string $insertion The new content to insert.
 * @return bool True on write success, false on failure.
 */
function insert_with_markers( $filename, $marker, $insertion ) {
	if ( ! file_exists( $filename ) ) {
		if ( ! is_writable( dirname( $filename ) ) ) {
			return false;
		}

		if ( ! touch( $filename ) ) {
			return false;
		}

		// Make sure the file is created with a minimum set of permissions.
		$perms = fileperms( $filename );

		if ( $perms ) {
			chmod( $filename, $perms | 0644 );
		}
	} elseif ( ! is_writable( $filename ) ) {
		return false;
	}

	if ( ! is_array( $insertion ) ) {
		$insertion = explode( "\n", $insertion );
	}

	$switched_locale = switch_to_locale( get_locale() );

	$instructions = sprintf(
		/* translators: 1: Marker. */
		__(
			'The directives (lines) between "BEGIN %1$s" and "END %1$s" are
dynamically generated, and should only be modified via WordPress filters.
Any changes to the directives between these markers will be overwritten.'
		),
		$marker
	);

	$instructions = explode( "\n", $instructions );

	foreach ( $instructions as $line => $text ) {
		$instructions[ $line ] = '# ' . $text;
	}

	/**
	 * Filters the inline instructions inserted before the dynamically generated content.
	 *
	 * @since 5.3.0
	 *
	 * @param string[] $instructions Array of lines with inline instructions.
	 * @param string   $marker       The marker being inserted.
	 */
	$instructions = apply_filters( 'insert_with_markers_inline_instructions', $instructions, $marker );

	if ( $switched_locale ) {
		restore_previous_locale();
	}

	$insertion = array_merge( $instructions, $insertion );

	$start_marker = "# BEGIN {$marker}";
	$end_marker   = "# END {$marker}";

	$fp = fopen( $filename, 'r+' );

	if ( ! $fp ) {
		return false;
	}

	// Attempt to get a lock. If the filesystem supports locking, this will block until the lock is acquired.
	flock( $fp, LOCK_EX );

	$lines = array();

	while ( ! feof( $fp ) ) {
		$lines[] = rtrim( fgets( $fp ), "\r\n" );
	}

	// Split out the existing file into the preceding lines, and those that appear after the marker.
	$pre_lines        = array();
	$post_lines       = array();
	$existing_lines   = array();
	$found_marker     = false;
	$found_end_marker = false;

	foreach ( $lines as $line ) {
		if ( ! $found_marker && str_contains( $line, $start_marker ) ) {
			$found_marker = true;
			continue;
		} elseif ( ! $found_end_marker && str_contains( $line, $end_marker ) ) {
			$found_end_marker = true;
			continue;
		}

		if ( ! $found_marker ) {
			$pre_lines[] = $line;
		} elseif ( $found_marker && $found_end_marker ) {
			$post_lines[] = $line;
		} else {
			$existing_lines[] = $line;
		}
	}

	// Check to see if there was a change.
	if ( $existing_lines === $insertion ) {
		flock( $fp, LOCK_UN );
		fclose( $fp );

		return true;
	}

	// Generate the new file data.
	$new_file_data = implode(
		"\n",
		array_merge(
			$pre_lines,
			array( $start_marker ),
			$insertion,
			array( $end_marker ),
			$post_lines
		)
	);

	// Write to the start of the file, and truncate it to that length.
	fseek( $fp, 0 );
	$bytes = fwrite( $fp, $new_file_data );

	if ( $bytes ) {
		ftruncate( $fp, ftell( $fp ) );
	}

	fflush( $fp );
	flock( $fp, LOCK_UN );
	fclose( $fp );

	return (bool) $bytes;
}

/**
 * Updates the htaccess file with the current rules if it is writable.
 *
 * Always writes to the file if it exists and is writable to ensure that we
 * blank out old rules.
 *
 * @since 1.5.0
 *
 * @global WP_Rewrite $wp_rewrite WordPress rewrite component.
 *
 * @return bool|null True on write success, false on failure. Null in multisite.
 */
function save_mod_rewrite_rules() {
	global $wp_rewrite;

	if ( is_multisite() ) {
		return;
	}

	// Ensure get_home_path() is declared.
	require_once ABSPATH . 'wp-admin/includes/file.php';

	$home_path     = get_home_path();
	$htaccess_file = $home_path . '.htaccess';

	/*
	 * If the file doesn't already exist check for write access to the directory
	 * and whether we have some rules. Else check for write access to the file.
	 */
	if ( ! file_exists( $htaccess_file ) && is_writable( $home_path ) && $wp_rewrite->using_mod_rewrite_permalinks()
		|| is_writable( $htaccess_file )
	) {
		if ( got_mod_rewrite() ) {
			$rules = explode( "\n", $wp_rewrite->mod_rewrite_rules() );

			return insert_with_markers( $htaccess_file, 'WordPress', $rules );
		}
	}

	return false;
}

/**
 * Updates the IIS web.config file with the current rules if it is writable.
 * If the permalinks do not require rewrite rules then the rules are deleted from the web.config file.
 *
 * @since 2.8.0
 *
 * @global WP_Rewrite $wp_rewrite WordPress rewrite component.
 *
 * @return bool|null True on write success, false on failure. Null in multisite.
 */
function iis7_save_url_rewrite_rules() {
	global $wp_rewrite;

	if ( is_multisite() ) {
		return;
	}

	// Ensure get_home_path() is declared.
	require_once ABSPATH . 'wp-admin/includes/file.php';

	$home_path       = get_home_path();
	$web_config_file = $home_path . 'web.config';

	// Using win_is_writable() instead of is_writable() because of a bug in Windows PHP.
	if ( iis7_supports_permalinks()
		&& ( ! file_exists( $web_config_file ) && win_is_writable( $home_path ) && $wp_rewrite->using_mod_rewrite_permalinks()
			|| win_is_writable( $web_config_file ) )
	) {
		$rule = $wp_rewrite->iis7_url_rewrite_rules( false );

		if ( ! empty( $rule ) ) {
			return iis7_add_rewrite_rule( $web_config_file, $rule );
		} else {
			return iis7_delete_rewrite_rule( $web_config_file );
		}
	}

	return false;
}

/**
 * Updates the "recently-edited" file for the plugin or theme file editor.
 *
 * @since 1.5.0
 *
 * @param string $file
 */
function update_recently_edited( $file ) {
	$oldfiles = (array) get_option( 'recently_edited' );

	if ( $oldfiles ) {
		$oldfiles   = array_reverse( $oldfiles );
		$oldfiles[] = $file;
		$oldfiles   = array_reverse( $oldfiles );
		$oldfiles   = array_unique( $oldfiles );

		if ( 5 < count( $oldfiles ) ) {
			array_pop( $oldfiles );
		}
	} else {
		$oldfiles[] = $file;
	}

	update_option( 'recently_edited', $oldfiles );
}

/**
 * Makes a tree structure for the theme file editor's file list.
 *
 * @since 4.9.0
 * @access private
 *
 * @param array $allowed_files List of theme file paths.
 * @return array Tree structure for listing theme files.
 */
function wp_make_theme_file_tree( $allowed_files ) {
	$tree_list = array();

	foreach ( $allowed_files as $file_name => $absolute_filename ) {
		$list     = explode( '/', $file_name );
		$last_dir = &$tree_list;

		foreach ( $list as $dir ) {
			$last_dir =& $last_dir[ $dir ];
		}

		$last_dir = $file_name;
	}

	return $tree_list;
}

/**
 * Outputs the formatted file list for the theme file editor.
 *
 * @since 4.9.0
 * @access private
 *
 * @global string $relative_file Name of the file being edited relative to the
 *                               theme directory.
 * @global string $stylesheet    The stylesheet name of the theme being edited.
 *
 * @param array|string $tree  List of file/folder paths, or filename.
 * @param int          $level The aria-level for the current iteration.
 * @param int          $size  The aria-setsize for the current iteration.
 * @param int          $index The aria-posinset for the current iteration.
 */
function wp_print_theme_file_tree( $tree, $level = 2, $size = 1, $index = 1 ) {
	global $relative_file, $stylesheet;

	if ( is_array( $tree ) ) {
		$index = 0;
		$size  = count( $tree );

		foreach ( $tree as $label => $theme_file ) :
			++$index;

			if ( ! is_array( $theme_file ) ) {
				wp_print_theme_file_tree( $theme_file, $level, $index, $size );
				continue;
			}
			?>
			<li role="treeitem" aria-expanded="true" tabindex="-1"
				aria-level="<?php echo esc_attr( $level ); ?>"
				aria-setsize="<?php echo esc_attr( $size ); ?>"
				aria-posinset="<?php echo esc_attr( $index ); ?>">
				<span class="folder-label"><?php echo esc_html( $label ); ?> <span class="screen-reader-text">
					<?php
					/* translators: Hidden accessibility text. */
					_e( 'folder' );
					?>
				</span><span aria-hidden="true" class="icon"></span></span>
				<ul role="group" class="tree-folder"><?php wp_print_theme_file_tree( $theme_file, $level + 1, $index, $size ); ?></ul>
			</li>
			<?php
		endforeach;
	} else {
		$filename = $tree;
		$url      = add_query_arg(
			array(
				'file'  => rawurlencode( $tree ),
				'theme' => rawurlencode( $stylesheet ),
			),
			self_admin_url( 'theme-editor.php' )
		);
		?>
		<li role="none" class="<?php echo esc_attr( $relative_file === $filename ? 'current-file' : '' ); ?>">
			<a role="treeitem" tabindex="<?php echo esc_attr( $relative_file === $filename ? '0' : '-1' ); ?>"
				href="<?php echo esc_url( $url ); ?>"
				aria-level="<?php echo esc_attr( $level ); ?>"
				aria-setsize="<?php echo esc_attr( $size ); ?>"
				aria-posinset="<?php echo esc_attr( $index ); ?>">
				<?php
				$file_description = esc_html( get_file_description( $filename ) );

				if ( $file_description !== $filename && wp_basename( $filename ) !== $file_description ) {
					$file_description .= '<br /><span class="nonessential">(' . esc_html( $filename ) . ')</span>';
				}

				if ( $relative_file === $filename ) {
					echo '<span class="notice notice-info">' . $file_description . '</span>';
				} else {
					echo $file_description;
				}
				?>
			</a>
		</li>
		<?php
	}
}

/**
 * Makes a tree structure for the plugin file editor's file list.
 *
 * @since 4.9.0
 * @access private
 *
 * @param array $plugin_editable_files List of plugin file paths.
 * @return array Tree structure for listing plugin files.
 */
function wp_make_plugin_file_tree( $plugin_editable_files ) {
	$tree_list = array();

	foreach ( $plugin_editable_files as $plugin_file ) {
		$list     = explode( '/', preg_replace( '#^.+?/#', '', $plugin_file ) );
		$last_dir = &$tree_list;

		foreach ( $list as $dir ) {
			$last_dir =& $last_dir[ $dir ];
		}

		$last_dir = $plugin_file;
	}

	return $tree_list;
}

/**
 * Outputs the formatted file list for the plugin file editor.
 *
 * @since 4.9.0
 * @access private
 *
 * @param array|string $tree  List of file/folder paths, or filename.
 * @param string       $label Name of file or folder to print.
 * @param int          $level The aria-level for the current iteration.
 * @param int          $size  The aria-setsize for the current iteration.
 * @param int          $index The aria-posinset for the current iteration.
 */
function wp_print_plugin_file_tree( $tree, $label = '', $level = 2, $size = 1, $index = 1 ) {
	global $file, $plugin;

	if ( is_array( $tree ) ) {
		$index = 0;
		$size  = count( $tree );

		foreach ( $tree as $label => $plugin_file ) :
			++$index;

			if ( ! is_array( $plugin_file ) ) {
				wp_print_plugin_file_tree( $plugin_file, $label, $level, $index, $size );
				continue;
			}
			?>
			<li role="treeitem" aria-expanded="true" tabindex="-1"
				aria-level="<?php echo esc_attr( $level ); ?>"
				aria-setsize="<?php echo esc_attr( $size ); ?>"
				aria-posinset="<?php echo esc_attr( $index ); ?>">
				<span class="folder-label"><?php echo esc_html( $label ); ?> <span class="screen-reader-text">
					<?php
					/* translators: Hidden accessibility text. */
					_e( 'folder' );
					?>
				</span><span aria-hidden="true" class="icon"></span></span>
				<ul role="group" class="tree-folder"><?php wp_print_plugin_file_tree( $plugin_file, '', $level + 1, $index, $size ); ?></ul>
			</li>
			<?php
		endforeach;
	} else {
		$url = add_query_arg(
			array(
				'file'   => rawurlencode( $tree ),
				'plugin' => rawurlencode( $plugin ),
			),
			self_admin_url( 'plugin-editor.php' )
		);
		?>
		<li role="none" class="<?php echo esc_attr( $file === $tree ? 'current-file' : '' ); ?>">
			<a role="treeitem" tabindex="<?php echo esc_attr( $file === $tree ? '0' : '-1' ); ?>"
				href="<?php echo esc_url( $url ); ?>"
				aria-level="<?php echo esc_attr( $level ); ?>"
				aria-setsize="<?php echo esc_attr( $size ); ?>"
				aria-posinset="<?php echo esc_attr( $index ); ?>">
				<?php
				if ( $file === $tree ) {
					echo '<span class="notice notice-info">' . esc_html( $label ) . '</span>';
				} else {
					echo esc_html( $label );
				}
				?>
			</a>
		</li>
		<?php
	}
}

/**
 * Flushes rewrite rules if `siteurl`, `home` or `page_on_front` changed.
 *
 * @since 2.1.0
 *
 * @param string $old_value
 * @param string $value
 */
function update_home_siteurl( $old_value, $value ) {
	if ( wp_installing() ) {
		return;
	}

	if ( is_multisite() && ms_is_switched() ) {
		delete_option( 'rewrite_rules' );
	} else {
		flush_rewrite_rules();
	}
}

/**
 * Resets global variables based on `$_GET` and `$_POST`.
 *
 * This function resets global variables based on the names passed
 * in the `$vars` array to the value of `$_POST[$var]` or `$_GET[$var]` or an
 * empty string if neither is defined.
 *
 * @since 2.0.0
 *
 * @param array $vars An array of globals to reset.
 */
function wp_reset_vars( $vars ) {
	foreach ( $vars as $var ) {
		if ( empty( $_POST[ $var ] ) ) {
			if ( empty( $_GET[ $var ] ) ) {
				$GLOBALS[ $var ] = '';
			} else {
				$GLOBALS[ $var ] = $_GET[ $var ];
			}
		} else {
			$GLOBALS[ $var ] = $_POST[ $var ];
		}
	}
}

/**
 * Displays the given administration message.
 *
 * @since 2.1.0
 *
 * @param string|WP_Error $message
 */
function show_message( $message ) {
	if ( is_wp_error( $message ) ) {
		if ( $message->get_error_data() && is_string( $message->get_error_data() ) ) {
			$message = $message->get_error_message() . ': ' . $message->get_error_data();
		} else {
			$message = $message->get_error_message();
		}
	}

	echo "<p>$message</p>\n";
	wp_ob_end_flush_all();
	flush();
}

/**
 * @since 2.8.0
 *
 * @param string $content
 * @return string[] Array of function names.
 */
function wp_doc_link_parse( $content ) {
	if ( ! is_string( $content ) || empty( $content ) ) {
		return array();
	}

	if ( ! function_exists( 'token_get_all' ) ) {
		return array();
	}

	$tokens           = token_get_all( $content );
	$count            = count( $tokens );
	$functions        = array();
	$ignore_functions = array();

	for ( $t = 0; $t < $count - 2; $t++ ) {
		if ( ! is_array( $tokens[ $t ] ) ) {
			continue;
		}

		if ( T_STRING === $tokens[ $t ][0] && ( '(' === $tokens[ $t + 1 ] || '(' === $tokens[ $t + 2 ] ) ) {
			// If it's a function or class defined locally, there's not going to be any docs available.
			if ( ( isset( $tokens[ $t - 2 ][1] ) && in_array( $tokens[ $t - 2 ][1], array( 'function', 'class' ), true ) )
				|| ( isset( $tokens[ $t - 2 ][0] ) && T_OBJECT_OPERATOR === $tokens[ $t - 1 ][0] )
			) {
				$ignore_functions[] = $tokens[ $t ][1];
			}

			// Add this to our stack of unique references.
			$functions[] = $tokens[ $t ][1];
		}
	}

	$functions = array_unique( $functions );
	sort( $functions );

	/**
	 * Filters the list of functions and classes to be ignored from the documentation lookup.
	 *
	 * @since 2.8.0
	 *
	 * @param string[] $ignore_functions Array of names of functions and classes to be ignored.
	 */
	$ignore_functions = apply_filters( 'documentation_ignore_functions', $ignore_functions );

	$ignore_functions = array_unique( $ignore_functions );

	$output = array();

	foreach ( $functions as $function ) {
		if ( in_array( $function, $ignore_functions, true ) ) {
			continue;
		}

		$output[] = $function;
	}

	return $output;
}

/**
 * Saves option for number of rows when listing posts, pages, comments, etc.
 *
 * @since 2.8.0
 */
function set_screen_options() {
	if ( ! isset( $_POST['wp_screen_options'] ) || ! is_array( $_POST['wp_screen_options'] ) ) {
		return;
	}

	check_admin_referer( 'screen-options-nonce', 'screenoptionnonce' );

	$user = wp_get_current_user();

	if ( ! $user ) {
		return;
	}

	$option = $_POST['wp_screen_options']['option'];
	$value  = $_POST['wp_screen_options']['value'];

	if ( sanitize_key( $option ) !== $option ) {
		return;
	}

	$map_option = $option;
	$type       = str_replace( 'edit_', '', $map_option );
	$type       = str_replace( '_per_page', '', $type );

	if ( in_array( $type, get_taxonomies(), true ) ) {
		$map_option = 'edit_tags_per_page';
	} elseif ( in_array( $type, get_post_types(), true ) ) {
		$map_option = 'edit_per_page';
	} else {
		$option = str_replace( '-', '_', $option );
	}

	switch ( $map_option ) {
		case 'edit_per_page':
		case 'users_per_page':
		case 'edit_comments_per_page':
		case 'upload_per_page':
		case 'edit_tags_per_page':
		case 'plugins_per_page':
		case 'export_personal_data_requests_per_page':
		case 'remove_personal_data_requests_per_page':
			// Network admin.
		case 'sites_network_per_page':
		case 'users_network_per_page':
		case 'site_users_network_per_page':
		case 'plugins_network_per_page':
		case 'themes_network_per_page':
		case 'site_themes_network_per_page':
			$value = (int) $value;

			if ( $value < 1 || $value > 999 ) {
				return;
			}

			break;

		default:
			$screen_option = false;

			if ( str_ends_with( $option, '_page' ) || 'layout_columns' === $option ) {
				/**
				 * Filters a screen option value before it is set.
				 *
				 * The filter can also be used to modify non-standard `[items]_per_page`
				 * settings. See the parent function for a full list of standard options.
				 *
				 * Returning false from the filter will skip saving the current option.
				 *
				 * @since 2.8.0
				 * @since 5.4.2 Only applied to options ending with '_page',
				 *              or the 'layout_columns' option.
				 *
				 * @see set_screen_options()
				 *
				 * @param mixed  $screen_option The value to save instead of the option value.
				 *                              Default false (to skip saving the current option).
				 * @param string $option        The option name.
				 * @param int    $value         The option value.
				 */
				$screen_option = apply_filters( 'set-screen-option', $screen_option, $option, $value ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
			}

			/**
			 * Filters a screen option value before it is set.
			 *
			 * The dynamic portion of the hook name, `$option`, refers to the option name.
			 *
			 * Returning false from the filter will skip saving the current option.
			 *
			 * @since 5.4.2
			 *
			 * @see set_screen_options()
			 *
			 * @param mixed   $screen_option The value to save instead of the option value.
			 *                               Default false (to skip saving the current option).
			 * @param string  $option        The option name.
			 * @param int     $value         The option value.
			 */
			$value = apply_filters( "set_screen_option_{$option}", $screen_option, $option, $value );

			if ( false === $value ) {
				return;
			}

			break;
	}

	update_user_meta( $user->ID, $option, $value );

	$url = remove_query_arg( array( 'pagenum', 'apage', 'paged' ), wp_get_referer() );

	if ( isset( $_POST['mode'] ) ) {
		$url = add_query_arg( array( 'mode' => $_POST['mode'] ), $url );
	}

	wp_safe_redirect( $url );
	exit;
}

/**
 * Checks if rewrite rule for WordPress already exists in the IIS 7+ configuration file.
 *
 * @since 2.8.0
 *
 * @param string $filename The file path to the configuration file.
 * @return bool
 */
function iis7_rewrite_rule_exists( $filename ) {
	if ( ! file_exists( $filename ) ) {
		return false;
	}

	if ( ! class_exists( 'DOMDocument', false ) ) {
		return false;
	}

	$doc = new DOMDocument();

	if ( $doc->load( $filename ) === false ) {
		return false;
	}

	$xpath = new DOMXPath( $doc );
	$rules = $xpath->query( '/configuration/system.webServer/rewrite/rules/rule[starts-with(@name,\'wordpress\')] | /configuration/system.webServer/rewrite/rules/rule[starts-with(@name,\'WordPress\')]' );

	if ( 0 === $rules->length ) {
		return false;
	}

	return true;
}

/**
 * Deletes WordPress rewrite rule from web.config file if it exists there.
 *
 * @since 2.8.0
 *
 * @param string $filename Name of the configuration file.
 * @return bool
 */
function iis7_delete_rewrite_rule( $filename ) {
	// If configuration file does not exist then rules also do not exist, so there is nothing to delete.
	if ( ! file_exists( $filename ) ) {
		return true;
	}

	if ( ! class_exists( 'DOMDocument', false ) ) {
		return false;
	}

	$doc                     = new DOMDocument();
	$doc->preserveWhiteSpace = false;

	if ( $doc->load( $filename ) === false ) {
		return false;
	}

	$xpath = new DOMXPath( $doc );
	$rules = $xpath->query( '/configuration/system.webServer/rewrite/rules/rule[starts-with(@name,\'wordpress\')] | /configuration/system.webServer/rewrite/rules/rule[starts-with(@name,\'WordPress\')]' );

	if ( $rules->length > 0 ) {
		$child  = $rules->item( 0 );
		$parent = $child->parentNode;
		$parent->removeChild( $child );
		$doc->formatOutput = true;
		saveDomDocument( $doc, $filename );
	}

	return true;
}

/**
 * Adds WordPress rewrite rule to the IIS 7+ configuration file.
 *
 * @since 2.8.0
 *
 * @param string $filename     The file path to the configuration file.
 * @param string $rewrite_rule The XML fragment with URL Rewrite rule.
 * @return bool
 */
function iis7_add_rewrite_rule( $filename, $rewrite_rule ) {
	if ( ! class_exists( 'DOMDocument', false ) ) {
		return false;
	}

	// If configuration file does not exist then we create one.
	if ( ! file_exists( $filename ) ) {
		$fp = fopen( $filename, 'w' );
		fwrite( $fp, '<configuration/>' );
		fclose( $fp );
	}

	$doc                     = new DOMDocument();
	$doc->preserveWhiteSpace = false;

	if ( $doc->load( $filename ) === false ) {
		return false;
	}

	$xpath = new DOMXPath( $doc );

	// First check if the rule already exists as in that case there is no need to re-add it.
	$wordpress_rules = $xpath->query( '/configuration/system.webServer/rewrite/rules/rule[starts-with(@name,\'wordpress\')] | /configuration/system.webServer/rewrite/rules/rule[starts-with(@name,\'WordPress\')]' );

	if ( $wordpress_rules->length > 0 ) {
		return true;
	}

	// Check the XPath to the rewrite rule and create XML nodes if they do not exist.
	$xml_nodes = $xpath->query( '/configuration/system.webServer/rewrite/rules' );

	if ( $xml_nodes->length > 0 ) {
		$rules_node = $xml_nodes->item( 0 );
	} else {
		$rules_node = $doc->createElement( 'rules' );

		$xml_nodes = $xpath->query( '/configuration/system.webServer/rewrite' );

		if ( $xml_nodes->length > 0 ) {
			$rewrite_node = $xml_nodes->item( 0 );
			$rewrite_node->appendChild( $rules_node );
		} else {
			$rewrite_node = $doc->createElement( 'rewrite' );
			$rewrite_node->appendChild( $rules_node );

			$xml_nodes = $xpath->query( '/configuration/system.webServer' );

			if ( $xml_nodes->length > 0 ) {
				$system_web_server_node = $xml_nodes->item( 0 );
				$system_web_server_node->appendChild( $rewrite_node );
			} else {
				$system_web_server_node = $doc->createElement( 'system.webServer' );
				$system_web_server_node->appendChild( $rewrite_node );

				$xml_nodes = $xpath->query( '/configuration' );

				if ( $xml_nodes->length > 0 ) {
					$config_node = $xml_nodes->item( 0 );
					$config_node->appendChild( $system_web_server_node );
				} else {
					$config_node = $doc->createElement( 'configuration' );
					$doc->appendChild( $config_node );
					$config_node->appendChild( $system_web_server_node );
				}
			}
		}
	}

	$rule_fragment = $doc->createDocumentFragment();
	$rule_fragment->appendXML( $rewrite_rule );
	$rules_node->appendChild( $rule_fragment );

	$doc->encoding     = 'UTF-8';
	$doc->formatOutput = true;
	saveDomDocument( $doc, $filename );

	return true;
}

/**
 * Saves the XML document into a file.
 *
 * @since 2.8.0
 *
 * @param DOMDocument $doc
 * @param string      $filename
 */
function saveDomDocument( $doc, $filename ) { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid
	$config = $doc->saveXML();
	$config = preg_replace( "/([^\r])\n/", "$1\r\n", $config );

	$fp = fopen( $filename, 'w' );
	fwrite( $fp, $config );
	fclose( $fp );
}

/**
 * Displays the default admin color scheme picker (Used in user-edit.php).
 *
 * @since 3.0.0
 *
 * @global array $_wp_admin_css_colors
 *
 * @param int $user_id User ID.
 */
function admin_color_scheme_picker( $user_id ) {
	global $_wp_admin_css_colors;

	ksort( $_wp_admin_css_colors );

	if ( isset( $_wp_admin_css_colors['fresh'] ) ) {
		// Set Default ('fresh') and Light should go first.
		$_wp_admin_css_colors = array_filter(
			array_merge(
				array(
					'fresh'  => '',
					'light'  => '',
					'modern' => '',
				),
				$_wp_admin_css_colors
			)
		);
	}

	$current_color = get_user_option( 'admin_color', $user_id );

	if ( empty( $current_color ) || ! isset( $_wp_admin_css_colors[ $current_color ] ) ) {
		$current_color = 'fresh';
	}
	?>
	<fieldset id="color-picker" class="scheme-list">
		<legend class="screen-reader-text"><span>
			<?php
			/* translators: Hidden accessibility text. */
			_e( 'Admin Color Scheme' );
			?>
		</span></legend>
		<?php
		wp_nonce_field( 'save-color-scheme', 'color-nonce', false );
		foreach ( $_wp_admin_css_colors as $color => $color_info ) :

			?>
			<div class="color-option <?php echo ( $color === $current_color ) ? 'selected' : ''; ?>">
				<input name="admin_color" id="admin_color_<?php echo esc_attr( $color ); ?>" type="radio" value="<?php echo esc_attr( $color ); ?>" class="tog" <?php checked( $color, $current_color ); ?> />
				<input type="hidden" class="css_url" value="<?php echo esc_url( $color_info->url ); ?>" />
				<input type="hidden" class="icon_colors" value="<?php echo esc_attr( wp_json_encode( array( 'icons' => $color_info->icon_colors ) ) ); ?>" />
				<label for="admin_color_<?php echo esc_attr( $color ); ?>"><?php echo esc_html( $color_info->name ); ?></label>
				<div class="color-palette">
				<?php
				foreach ( $color_info->colors as $html_color ) {
					?>
					<div class="color-palette-shade" style="background-color: <?php echo esc_attr( $html_color ); ?>">&nbsp;</div>
					<?php
				}
				?>
				</div>
			</div>
			<?php

		endforeach;
		?>
	</fieldset>
	<?php
}

/**
 *
 * @global array $_wp_admin_css_colors
 */
function wp_color_scheme_settings() {
	global $_wp_admin_css_colors;

	$color_scheme = get_user_option( 'admin_color' );

	// It's possible to have a color scheme set that is no longer registered.
	if ( empty( $_wp_admin_css_colors[ $color_scheme ] ) ) {
		$color_scheme = 'fresh';
	}

	if ( ! empty( $_wp_admin_css_colors[ $color_scheme ]->icon_colors ) ) {
		$icon_colors = $_wp_admin_css_colors[ $color_scheme ]->icon_colors;
	} elseif ( ! empty( $_wp_admin_css_colors['fresh']->icon_colors ) ) {
		$icon_colors = $_wp_admin_css_colors['fresh']->icon_colors;
	} else {
		// Fall back to the default set of icon colors if the default scheme is missing.
		$icon_colors = array(
			'base'    => '#a7aaad',
			'focus'   => '#72aee6',
			'current' => '#fff',
		);
	}

	echo '<script type="text/javascript">var _wpColorScheme = ' . wp_json_encode( array( 'icons' => $icon_colors ) ) . ";</script>\n";
}

/**
 * Displays the viewport meta in the admin.
 *
 * @since 5.5.0
 */
function wp_admin_viewport_meta() {
	/**
	 * Filters the viewport meta in the admin.
	 *
	 * @since 5.5.0
	 *
	 * @param string $viewport_meta The viewport meta.
	 */
	$viewport_meta = apply_filters( 'admin_viewport_meta', 'width=device-width,initial-scale=1.0' );

	if ( empty( $viewport_meta ) ) {
		return;
	}

	echo '<meta name="viewport" content="' . esc_attr( $viewport_meta ) . '">';
}

/**
 * Adds viewport meta for mobile in Customizer.
 *
 * Hooked to the {@see 'admin_viewport_meta'} filter.
 *
 * @since 5.5.0
 *
 * @param string $viewport_meta The viewport meta.
 * @return string Filtered viewport meta.
 */
function _customizer_mobile_viewport_meta( $viewport_meta ) {
	return trim( $viewport_meta, ',' ) . ',minimum-scale=0.5,maximum-scale=1.2';
}

/**
 * Checks lock status for posts displayed on the Posts screen.
 *
 * @since 3.6.0
 *
 * @param array  $response  The Heartbeat response.
 * @param array  $data      The $_POST data sent.
 * @param string $screen_id The screen ID.
 * @return array The Heartbeat response.
 */
function wp_check_locked_posts( $response, $data, $screen_id ) {
	$checked = array();

	if ( array_key_exists( 'wp-check-locked-posts', $data ) && is_array( $data['wp-check-locked-posts'] ) ) {
		foreach ( $data['wp-check-locked-posts'] as $key ) {
			$post_id = absint( substr( $key, 5 ) );

			if ( ! $post_id ) {
				continue;
			}

			$user_id = wp_check_post_lock( $post_id );

			if ( $user_id ) {
				$user = get_userdata( $user_id );

				if ( $user && current_user_can( 'edit_post', $post_id ) ) {
					$send = array(
						'name' => $user->display_name,
						/* translators: %s: User's display name. */
						'text' => sprintf( __( '%s is currently editing' ), $user->display_name ),
					);

					if ( get_option( 'show_avatars' ) ) {
						$send['avatar_src']    = get_avatar_url( $user->ID, array( 'size' => 18 ) );
						$send['avatar_src_2x'] = get_avatar_url( $user->ID, array( 'size' => 36 ) );
					}

					$checked[ $key ] = $send;
				}
			}
		}
	}

	if ( ! empty( $checked ) ) {
		$response['wp-check-locked-posts'] = $checked;
	}

	return $response;
}

/**
 * Checks lock status on the New/Edit Post screen and refresh the lock.
 *
 * @since 3.6.0
 *
 * @param array  $response  The Heartbeat response.
 * @param array  $data      The $_POST data sent.
 * @param string $screen_id The screen ID.
 * @return array The Heartbeat response.
 */
function wp_refresh_post_lock( $response, $data, $screen_id ) {
	if ( array_key_exists( 'wp-refresh-post-lock', $data ) ) {
		$received = $data['wp-refresh-post-lock'];
		$send     = array();

		$post_id = absint( $received['post_id'] );

		if ( ! $post_id ) {
			return $response;
		}

		if ( ! current_user_can( 'edit_post', $post_id ) ) {
			return $response;
		}

		$user_id = wp_check_post_lock( $post_id );
		$user    = get_userdata( $user_id );

		if ( $user ) {
			$error = array(
				'name' => $user->display_name,
				/* translators: %s: User's display name. */
				'text' => sprintf( __( '%s has taken over and is currently editing.' ), $user->display_name ),
			);

			if ( get_option( 'show_avatars' ) ) {
				$error['avatar_src']    = get_avatar_url( $user->ID, array( 'size' => 64 ) );
				$error['avatar_src_2x'] = get_avatar_url( $user->ID, array( 'size' => 128 ) );
			}

			$send['lock_error'] = $error;
		} else {
			$new_lock = wp_set_post_lock( $post_id );

			if ( $new_lock ) {
				$send['new_lock'] = implode( ':', $new_lock );
			}
		}

		$response['wp-refresh-post-lock'] = $send;
	}

	return $response;
}

/**
 * Checks nonce expiration on the New/Edit Post screen and refresh if needed.
 *
 * @since 3.6.0
 *
 * @param array  $response  The Heartbeat response.
 * @param array  $data      The $_POST data sent.
 * @param string $screen_id The screen ID.
 * @return array The Heartbeat response.
 */
function wp_refresh_post_nonces( $response, $data, $screen_id ) {
	if ( array_key_exists( 'wp-refresh-post-nonces', $data ) ) {
		$received = $data['wp-refresh-post-nonces'];

		$response['wp-refresh-post-nonces'] = array( 'check' => 1 );

		$post_id = absint( $received['post_id'] );

		if ( ! $post_id ) {
			return $response;
		}

		if ( ! current_user_can( 'edit_post', $post_id ) ) {
			return $response;
		}

		$response['wp-refresh-post-nonces'] = array(
			'replace' => array(
				'getpermalinknonce'    => wp_create_nonce( 'getpermalink' ),
				'samplepermalinknonce' => wp_create_nonce( 'samplepermalink' ),
				'closedpostboxesnonce' => wp_create_nonce( 'closedpostboxes' ),
				'_ajax_linking_nonce'  => wp_create_nonce( 'internal-linking' ),
				'_wpnonce'             => wp_create_nonce( 'update-post_' . $post_id ),
			),
		);
	}

	return $response;
}

/**
 * Refresh nonces used with meta boxes in the block editor.
 *
 * @since 6.1.0
 *
 * @param array  $response  The Heartbeat response.
 * @param array  $data      The $_POST data sent.
 * @return array The Heartbeat response.
 */
function wp_refresh_metabox_loader_nonces( $response, $data ) {
	if ( empty( $data['wp-refresh-metabox-loader-nonces'] ) ) {
		return $response;
	}

	$received = $data['wp-refresh-metabox-loader-nonces'];
	$post_id  = (int) $received['post_id'];

	if ( ! $post_id ) {
		return $response;
	}

	if ( ! current_user_can( 'edit_post', $post_id ) ) {
		return $response;
	}

	$response['wp-refresh-metabox-loader-nonces'] = array(
		'replace' => array(
			'metabox_loader_nonce' => wp_create_nonce( 'meta-box-loader' ),
			'_wpnonce'             => wp_create_nonce( 'update-post_' . $post_id ),
		),
	);

	return $response;
}

/**
 * Adds the latest Heartbeat and REST API nonce to the Heartbeat response.
 *
 * @since 5.0.0
 *
 * @param array $response The Heartbeat response.
 * @return array The Heartbeat response.
 */
function wp_refresh_heartbeat_nonces( $response ) {
	// Refresh the Rest API nonce.
	$response['rest_nonce'] = wp_create_nonce( 'wp_rest' );

	// Refresh the Heartbeat nonce.
	$response['heartbeat_nonce'] = wp_create_nonce( 'heartbeat-nonce' );

	return $response;
}

/**
 * Disables suspension of Heartbeat on the Add/Edit Post screens.
 *
 * @since 3.8.0
 *
 * @global string $pagenow The filename of the current screen.
 *
 * @param array $settings An array of Heartbeat settings.
 * @return array Filtered Heartbeat settings.
 */
function wp_heartbeat_set_suspension( $settings ) {
	global $pagenow;

	if ( 'post.php' === $pagenow || 'post-new.php' === $pagenow ) {
		$settings['suspension'] = 'disable';
	}

	return $settings;
}

/**
 * Performs autosave with heartbeat.
 *
 * @since 3.9.0
 *
 * @param array $response The Heartbeat response.
 * @param array $data     The $_POST data sent.
 * @return array The Heartbeat response.
 */
function heartbeat_autosave( $response, $data ) {
	if ( ! empty( $data['wp_autosave'] ) ) {
		$saved = wp_autosave( $data['wp_autosave'] );

		if ( is_wp_error( $saved ) ) {
			$response['wp_autosave'] = array(
				'success' => false,
				'message' => $saved->get_error_message(),
			);
		} elseif ( empty( $saved ) ) {
			$response['wp_autosave'] = array(
				'success' => false,
				'message' => __( 'Error while saving.' ),
			);
		} else {
			/* translators: Draft saved date format, see https://www.php.net/manual/datetime.format.php */
			$draft_saved_date_format = __( 'g:i:s a' );
			$response['wp_autosave'] = array(
				'success' => true,
				/* translators: %s: Date and time. */
				'message' => sprintf( __( 'Draft saved at %s.' ), date_i18n( $draft_saved_date_format ) ),
			);
		}
	}

	return $response;
}

/**
 * Removes single-use URL parameters and create canonical link based on new URL.
 *
 * Removes specific query string parameters from a URL, create the canonical link,
 * put it in the admin header, and change the current URL to match.
 *
 * @since 4.2.0
 */
function wp_admin_canonical_url() {
	$removable_query_args = wp_removable_query_args();

	if ( empty( $removable_query_args ) ) {
		return;
	}

	// Ensure we're using an absolute URL.
	$current_url  = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
	$filtered_url = remove_query_arg( $removable_query_args, $current_url );

	/**
	 * Filters the admin canonical URL value.
	 *
	 * @since 6.5.0
	 *
	 * @param string $filtered_url The admin canonical URL value.
	 */
	$filtered_url = apply_filters( 'wp_admin_canonical_url', $filtered_url );
	?>
	<link id="wp-admin-canonical" rel="canonical" href="<?php echo esc_url( $filtered_url ); ?>" />
	<script>
		if ( window.history.replaceState ) {
			window.history.replaceState( null, null, document.getElementById( 'wp-admin-canonical' ).href + window.location.hash );
		}
	</script>
	<?php
}

/**
 * Outputs JS that reloads the page if the user navigated to it with the Back or Forward button.
 *
 * Used on the Edit Post and Add New Post screens. Needed to ensure the page is not loaded from browser cache,
 * so the post title and editor content are the last saved versions. Ideally this script should run first in the head.
 *
 * @since 4.6.0
 */
function wp_page_reload_on_back_button_js() {
	?>
	<script>
		if ( typeof performance !== 'undefined' && performance.navigation && performance.navigation.type === 2 ) {
			document.location.reload( true );
		}
	</script>
	<?php
}

/**
 * Sends a confirmation request email when a change of site admin email address is attempted.
 *
 * The new site admin address will not become active until confirmed.
 *
 * @since 3.0.0
 * @since 4.9.0 This function was moved from wp-admin/includes/ms.php so it's no longer Multisite specific.
 *
 * @param string $old_value The old site admin email address.
 * @param string $value     The proposed new site admin email address.
 */
function update_option_new_admin_email( $old_value, $value ) {
	if ( get_option( 'admin_email' ) === $value || ! is_email( $value ) ) {
		return;
	}

	$hash            = md5( $value . time() . wp_rand() );
	$new_admin_email = array(
		'hash'     => $hash,
		'newemail' => $value,
	);
	update_option( 'adminhash', $new_admin_email, false );

	$switched_locale = switch_to_user_locale( get_current_user_id() );

	/* translators: Do not translate USERNAME, ADMIN_URL, EMAIL, SITENAME, SITEURL: those are placeholders. */
	$email_text = __(
		'Howdy,

A site administrator (###USERNAME###) recently requested to have the
administration email address changed on this site:
###SITEURL###

To confirm this change, please click on the following link:
###ADMIN_URL###

You can safely ignore and delete this email if you do not want to
take this action.

This email has been sent to ###EMAIL###

Regards,
All at ###SITENAME###
###SITEURL###'
	);

	/**
	 * Filters the text of the email sent when a change of site admin email address is attempted.
	 *
	 * The following strings have a special meaning and will get replaced dynamically:
	 *  - ###USERNAME###  The current user's username.
	 *  - ###ADMIN_URL### The link to click on to confirm the email change.
	 *  - ###EMAIL###     The proposed new site admin email address.
	 *  - ###SITENAME###  The name of the site.
	 *  - ###SITEURL###   The URL to the site.
	 *
	 * @since MU (3.0.0)
	 * @since 4.9.0 This filter is no longer Multisite specific.
	 *
	 * @param string $email_text      Text in the email.
	 * @param array  $new_admin_email {
	 *     Data relating to the new site admin email address.
	 *
	 *     @type string $hash     The secure hash used in the confirmation link URL.
	 *     @type string $newemail The proposed new site admin email address.
	 * }
	 */
	$content = apply_filters( 'new_admin_email_content', $email_text, $new_admin_email );

	$current_user = wp_get_current_user();
	$content      = str_replace( '###USERNAME###', $current_user->user_login, $content );
	$content      = str_replace( '###ADMIN_URL###', esc_url( self_admin_url( 'options.php?adminhash=' . $hash ) ), $content );
	$content      = str_replace( '###EMAIL###', $value, $content );
	$content      = str_replace( '###SITENAME###', wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ), $content );
	$content      = str_replace( '###SITEURL###', home_url(), $content );

	if ( '' !== get_option( 'blogname' ) ) {
		$site_title = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
	} else {
		$site_title = parse_url( home_url(), PHP_URL_HOST );
	}

	$subject = sprintf(
		/* translators: New admin email address notification email subject. %s: Site title. */
		__( '[%s] New Admin Email Address' ),
		$site_title
	);

	/**
	 * Filters the subject of the email sent when a change of site admin email address is attempted.
	 *
	 * @since 6.5.0
	 *
	 * @param string $subject Subject of the email.
	 */
	$subject = apply_filters( 'new_admin_email_subject', $subject );

	wp_mail( $value, $subject, $content );

	if ( $switched_locale ) {
		restore_previous_locale();
	}
}

/**
 * Appends '(Draft)' to draft page titles in the privacy page dropdown
 * so that unpublished content is obvious.
 *
 * @since 4.9.8
 * @access private
 *
 * @param string  $title Page title.
 * @param WP_Post $page  Page data object.
 * @return string Page title.
 */
function _wp_privacy_settings_filter_draft_page_titles( $title, $page ) {
	if ( 'draft' === $page->post_status && 'privacy' === get_current_screen()->id ) {
		/* translators: %s: Page title. */
		$title = sprintf( __( '%s (Draft)' ), $title );
	}

	return $title;
}

/**
 * Checks if the user needs to update PHP.
 *
 * @since 5.1.0
 * @since 5.1.1 Added the {@see 'wp_is_php_version_acceptable'} filter.
 *
 * @return array|false {
 *     Array of PHP version data. False on failure.
 *
 *     @type string $recommended_version The PHP version recommended by WordPress.
 *     @type string $minimum_version     The minimum required PHP version.
 *     @type bool   $is_supported        Whether the PHP version is actively supported.
 *     @type bool   $is_secure           Whether the PHP version receives security updates.
 *     @type bool   $is_acceptable       Whether the PHP version is still acceptable or warnings
 *                                       should be shown and an update recommended.
 * }
 */
function wp_check_php_version() {
	$version = PHP_VERSION;
	$key     = md5( $version );

	$response = get_site_transient( 'php_check_' . $key );

	if ( false === $response ) {
		$url = 'http://api.wordpress.org/core/serve-happy/1.0/';

		if ( wp_http_supports( array( 'ssl' ) ) ) {
			$url = set_url_scheme( $url, 'https' );
		}

		$url = add_query_arg( 'php_version', $version, $url );

		$response = wp_remote_get( $url );

		if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
			return false;
		}

		$response = json_decode( wp_remote_retrieve_body( $response ), true );

		if ( ! is_array( $response ) ) {
			return false;
		}

		set_site_transient( 'php_check_' . $key, $response, WEEK_IN_SECONDS );
	}

	if ( isset( $response['is_acceptable'] ) && $response['is_acceptable'] ) {
		/**
		 * Filters whether the active PHP version is considered acceptable by WordPress.
		 *
		 * Returning false will trigger a PHP version warning to show up in the admin dashboard to administrators.
		 *
		 * This filter is only run if the wordpress.org Serve Happy API considers the PHP version acceptable, ensuring
		 * that this filter can only make this check stricter, but not loosen it.
		 *
		 * @since 5.1.1
		 *
		 * @param bool   $is_acceptable Whether the PHP version is considered acceptable. Default true.
		 * @param string $version       PHP version checked.
		 */
		$response['is_acceptable'] = (bool) apply_filters( 'wp_is_php_version_acceptable', true, $version );
	}

	$response['is_lower_than_future_minimum'] = false;

	// The minimum supported PHP version will be updated to 7.4 in the future. Check if the current version is lower.
	if ( version_compare( $version, '7.4', '<' ) ) {
		$response['is_lower_than_future_minimum'] = true;

		// Force showing of warnings.
		$response['is_acceptable'] = false;
	}

	return $response;
}
class-wp-community-events.php000060400000044521151331232430012331 0ustar00<?php
/**
 * Administration: Community Events class.
 *
 * @package WordPress
 * @subpackage Administration
 * @since 4.8.0
 */

/**
 * Class WP_Community_Events.
 *
 * A client for api.wordpress.org/events.
 *
 * @since 4.8.0
 */
#[AllowDynamicProperties]
class WP_Community_Events {
	/**
	 * ID for a WordPress user account.
	 *
	 * @since 4.8.0
	 *
	 * @var int
	 */
	protected $user_id = 0;

	/**
	 * Stores location data for the user.
	 *
	 * @since 4.8.0
	 *
	 * @var false|array
	 */
	protected $user_location = false;

	/**
	 * Constructor for WP_Community_Events.
	 *
	 * @since 4.8.0
	 *
	 * @param int        $user_id       WP user ID.
	 * @param false|array $user_location {
	 *     Stored location data for the user. false to pass no location.
	 *
	 *     @type string $description The name of the location
	 *     @type string $latitude    The latitude in decimal degrees notation, without the degree
	 *                               symbol. e.g.: 47.615200.
	 *     @type string $longitude   The longitude in decimal degrees notation, without the degree
	 *                               symbol. e.g.: -122.341100.
	 *     @type string $country     The ISO 3166-1 alpha-2 country code. e.g.: BR
	 * }
	 */
	public function __construct( $user_id, $user_location = false ) {
		$this->user_id       = absint( $user_id );
		$this->user_location = $user_location;
	}

	/**
	 * Gets data about events near a particular location.
	 *
	 * Cached events will be immediately returned if the `user_location` property
	 * is set for the current user, and cached events exist for that location.
	 *
	 * Otherwise, this method sends a request to the w.org Events API with location
	 * data. The API will send back a recognized location based on the data, along
	 * with nearby events.
	 *
	 * The browser's request for events is proxied with this method, rather
	 * than having the browser make the request directly to api.wordpress.org,
	 * because it allows results to be cached server-side and shared with other
	 * users and sites in the network. This makes the process more efficient,
	 * since increasing the number of visits that get cached data means users
	 * don't have to wait as often; if the user's browser made the request
	 * directly, it would also need to make a second request to WP in order to
	 * pass the data for caching. Having WP make the request also introduces
	 * the opportunity to anonymize the IP before sending it to w.org, which
	 * mitigates possible privacy concerns.
	 *
	 * @since 4.8.0
	 * @since 5.5.2 Response no longer contains formatted date field. They're added
	 *              in `wp.communityEvents.populateDynamicEventFields()` now.
	 *
	 * @param string $location_search Optional. City name to help determine the location.
	 *                                e.g., "Seattle". Default empty string.
	 * @param string $timezone        Optional. Timezone to help determine the location.
	 *                                Default empty string.
	 * @return array|WP_Error A WP_Error on failure; an array with location and events on
	 *                        success.
	 */
	public function get_events( $location_search = '', $timezone = '' ) {
		$cached_events = $this->get_cached_events();

		if ( ! $location_search && $cached_events ) {
			return $cached_events;
		}

		// Include an unmodified $wp_version.
		require ABSPATH . WPINC . '/version.php';

		$api_url                    = 'http://api.wordpress.org/events/1.0/';
		$request_args               = $this->get_request_args( $location_search, $timezone );
		$request_args['user-agent'] = 'WordPress/' . $wp_version . '; ' . home_url( '/' );

		if ( wp_http_supports( array( 'ssl' ) ) ) {
			$api_url = set_url_scheme( $api_url, 'https' );
		}

		$response       = wp_remote_get( $api_url, $request_args );
		$response_code  = wp_remote_retrieve_response_code( $response );
		$response_body  = json_decode( wp_remote_retrieve_body( $response ), true );
		$response_error = null;

		if ( is_wp_error( $response ) ) {
			$response_error = $response;
		} elseif ( 200 !== $response_code ) {
			$response_error = new WP_Error(
				'api-error',
				/* translators: %d: Numeric HTTP status code, e.g. 400, 403, 500, 504, etc. */
				sprintf( __( 'Invalid API response code (%d).' ), $response_code )
			);
		} elseif ( ! isset( $response_body['location'], $response_body['events'] ) ) {
			$response_error = new WP_Error(
				'api-invalid-response',
				isset( $response_body['error'] ) ? $response_body['error'] : __( 'Unknown API error.' )
			);
		}

		if ( is_wp_error( $response_error ) ) {
			return $response_error;
		} else {
			$expiration = false;

			if ( isset( $response_body['ttl'] ) ) {
				$expiration = $response_body['ttl'];
				unset( $response_body['ttl'] );
			}

			/*
			 * The IP in the response is usually the same as the one that was sent
			 * in the request, but in some cases it is different. In those cases,
			 * it's important to reset it back to the IP from the request.
			 *
			 * For example, if the IP sent in the request is private (e.g., 192.168.1.100),
			 * then the API will ignore that and use the corresponding public IP instead,
			 * and the public IP will get returned. If the public IP were saved, though,
			 * then get_cached_events() would always return `false`, because the transient
			 * would be generated based on the public IP when saving the cache, but generated
			 * based on the private IP when retrieving the cache.
			 */
			if ( ! empty( $response_body['location']['ip'] ) ) {
				$response_body['location']['ip'] = $request_args['body']['ip'];
			}

			/*
			 * The API doesn't return a description for latitude/longitude requests,
			 * but the description is already saved in the user location, so that
			 * one can be used instead.
			 */
			if ( $this->coordinates_match( $request_args['body'], $response_body['location'] ) && empty( $response_body['location']['description'] ) ) {
				$response_body['location']['description'] = $this->user_location['description'];
			}

			/*
			 * Store the raw response, because events will expire before the cache does.
			 * The response will need to be processed every page load.
			 */
			$this->cache_events( $response_body, $expiration );

			$response_body['events'] = $this->trim_events( $response_body['events'] );

			return $response_body;
		}
	}

	/**
	 * Builds an array of args to use in an HTTP request to the w.org Events API.
	 *
	 * @since 4.8.0
	 *
	 * @param string $search   Optional. City search string. Default empty string.
	 * @param string $timezone Optional. Timezone string. Default empty string.
	 * @return array The request args.
	 */
	protected function get_request_args( $search = '', $timezone = '' ) {
		$args = array(
			'number' => 5, // Get more than three in case some get trimmed out.
			'ip'     => self::get_unsafe_client_ip(),
		);

		/*
		 * Include the minimal set of necessary arguments, in order to increase the
		 * chances of a cache-hit on the API side.
		 */
		if ( empty( $search ) && isset( $this->user_location['latitude'], $this->user_location['longitude'] ) ) {
			$args['latitude']  = $this->user_location['latitude'];
			$args['longitude'] = $this->user_location['longitude'];
		} else {
			$args['locale'] = get_user_locale( $this->user_id );

			if ( $timezone ) {
				$args['timezone'] = $timezone;
			}

			if ( $search ) {
				$args['location'] = $search;
			}
		}

		// Wrap the args in an array compatible with the second parameter of `wp_remote_get()`.
		return array(
			'body' => $args,
		);
	}

	/**
	 * Determines the user's actual IP address and attempts to partially
	 * anonymize an IP address by converting it to a network ID.
	 *
	 * Geolocating the network ID usually returns a similar location as the
	 * actual IP, but provides some privacy for the user.
	 *
	 * $_SERVER['REMOTE_ADDR'] cannot be used in all cases, such as when the user
	 * is making their request through a proxy, or when the web server is behind
	 * a proxy. In those cases, $_SERVER['REMOTE_ADDR'] is set to the proxy address rather
	 * than the user's actual address.
	 *
	 * Modified from https://stackoverflow.com/a/2031935/450127, MIT license.
	 * Modified from https://github.com/geertw/php-ip-anonymizer, MIT license.
	 *
	 * SECURITY WARNING: This function is _NOT_ intended to be used in
	 * circumstances where the authenticity of the IP address matters. This does
	 * _NOT_ guarantee that the returned address is valid or accurate, and it can
	 * be easily spoofed.
	 *
	 * @since 4.8.0
	 *
	 * @return string|false The anonymized address on success; the given address
	 *                      or false on failure.
	 */
	public static function get_unsafe_client_ip() {
		$client_ip = false;

		// In order of preference, with the best ones for this purpose first.
		$address_headers = array(
			'HTTP_CLIENT_IP',
			'HTTP_X_FORWARDED_FOR',
			'HTTP_X_FORWARDED',
			'HTTP_X_CLUSTER_CLIENT_IP',
			'HTTP_FORWARDED_FOR',
			'HTTP_FORWARDED',
			'REMOTE_ADDR',
		);

		foreach ( $address_headers as $header ) {
			if ( array_key_exists( $header, $_SERVER ) ) {
				/*
				 * HTTP_X_FORWARDED_FOR can contain a chain of comma-separated
				 * addresses. The first one is the original client. It can't be
				 * trusted for authenticity, but we don't need to for this purpose.
				 */
				$address_chain = explode( ',', $_SERVER[ $header ] );
				$client_ip     = trim( $address_chain[0] );

				break;
			}
		}

		if ( ! $client_ip ) {
			return false;
		}

		$anon_ip = wp_privacy_anonymize_ip( $client_ip, true );

		if ( '0.0.0.0' === $anon_ip || '::' === $anon_ip ) {
			return false;
		}

		return $anon_ip;
	}

	/**
	 * Test if two pairs of latitude/longitude coordinates match each other.
	 *
	 * @since 4.8.0
	 *
	 * @param array $a The first pair, with indexes 'latitude' and 'longitude'.
	 * @param array $b The second pair, with indexes 'latitude' and 'longitude'.
	 * @return bool True if they match, false if they don't.
	 */
	protected function coordinates_match( $a, $b ) {
		if ( ! isset( $a['latitude'], $a['longitude'], $b['latitude'], $b['longitude'] ) ) {
			return false;
		}

		return $a['latitude'] === $b['latitude'] && $a['longitude'] === $b['longitude'];
	}

	/**
	 * Generates a transient key based on user location.
	 *
	 * This could be reduced to a one-liner in the calling functions, but it's
	 * intentionally a separate function because it's called from multiple
	 * functions, and having it abstracted keeps the logic consistent and DRY,
	 * which is less prone to errors.
	 *
	 * @since 4.8.0
	 *
	 * @param array $location Should contain 'latitude' and 'longitude' indexes.
	 * @return string|false Transient key on success, false on failure.
	 */
	protected function get_events_transient_key( $location ) {
		$key = false;

		if ( isset( $location['ip'] ) ) {
			$key = 'community-events-' . md5( $location['ip'] );
		} elseif ( isset( $location['latitude'], $location['longitude'] ) ) {
			$key = 'community-events-' . md5( $location['latitude'] . $location['longitude'] );
		}

		return $key;
	}

	/**
	 * Caches an array of events data from the Events API.
	 *
	 * @since 4.8.0
	 *
	 * @param array     $events     Response body from the API request.
	 * @param int|false $expiration Optional. Amount of time to cache the events. Defaults to false.
	 * @return bool true if events were cached; false if not.
	 */
	protected function cache_events( $events, $expiration = false ) {
		$set              = false;
		$transient_key    = $this->get_events_transient_key( $events['location'] );
		$cache_expiration = $expiration ? absint( $expiration ) : HOUR_IN_SECONDS * 12;

		if ( $transient_key ) {
			$set = set_site_transient( $transient_key, $events, $cache_expiration );
		}

		return $set;
	}

	/**
	 * Gets cached events.
	 *
	 * @since 4.8.0
	 * @since 5.5.2 Response no longer contains formatted date field. They're added
	 *              in `wp.communityEvents.populateDynamicEventFields()` now.
	 *
	 * @return array|false An array containing `location` and `events` items
	 *                     on success, false on failure.
	 */
	public function get_cached_events() {
		$transient_key = $this->get_events_transient_key( $this->user_location );
		if ( ! $transient_key ) {
			return false;
		}

		$cached_response = get_site_transient( $transient_key );
		if ( isset( $cached_response['events'] ) ) {
			$cached_response['events'] = $this->trim_events( $cached_response['events'] );
		}

		return $cached_response;
	}

	/**
	 * Adds formatted date and time items for each event in an API response.
	 *
	 * This has to be called after the data is pulled from the cache, because
	 * the cached events are shared by all users. If it was called before storing
	 * the cache, then all users would see the events in the localized data/time
	 * of the user who triggered the cache refresh, rather than their own.
	 *
	 * @since 4.8.0
	 * @deprecated 5.6.0 No longer used in core.
	 *
	 * @param array $response_body The response which contains the events.
	 * @return array The response with dates and times formatted.
	 */
	protected function format_event_data_time( $response_body ) {
		_deprecated_function(
			__METHOD__,
			'5.5.2',
			'This is no longer used by core, and only kept for backward compatibility.'
		);

		if ( isset( $response_body['events'] ) ) {
			foreach ( $response_body['events'] as $key => $event ) {
				$timestamp = strtotime( $event['date'] );

				/*
				 * The `date_format` option is not used because it's important
				 * in this context to keep the day of the week in the formatted date,
				 * so that users can tell at a glance if the event is on a day they
				 * are available, without having to open the link.
				 */
				/* translators: Date format for upcoming events on the dashboard. Include the day of the week. See https://www.php.net/manual/datetime.format.php */
				$formatted_date = date_i18n( __( 'l, M j, Y' ), $timestamp );
				$formatted_time = date_i18n( get_option( 'time_format' ), $timestamp );

				if ( isset( $event['end_date'] ) ) {
					$end_timestamp      = strtotime( $event['end_date'] );
					$formatted_end_date = date_i18n( __( 'l, M j, Y' ), $end_timestamp );

					if ( 'meetup' !== $event['type'] && $formatted_end_date !== $formatted_date ) {
						/* translators: Upcoming events month format. See https://www.php.net/manual/datetime.format.php */
						$start_month = date_i18n( _x( 'F', 'upcoming events month format' ), $timestamp );
						$end_month   = date_i18n( _x( 'F', 'upcoming events month format' ), $end_timestamp );

						if ( $start_month === $end_month ) {
							$formatted_date = sprintf(
								/* translators: Date string for upcoming events. 1: Month, 2: Starting day, 3: Ending day, 4: Year. */
								__( '%1$s %2$d–%3$d, %4$d' ),
								$start_month,
								/* translators: Upcoming events day format. See https://www.php.net/manual/datetime.format.php */
								date_i18n( _x( 'j', 'upcoming events day format' ), $timestamp ),
								date_i18n( _x( 'j', 'upcoming events day format' ), $end_timestamp ),
								/* translators: Upcoming events year format. See https://www.php.net/manual/datetime.format.php */
								date_i18n( _x( 'Y', 'upcoming events year format' ), $timestamp )
							);
						} else {
							$formatted_date = sprintf(
								/* translators: Date string for upcoming events. 1: Starting month, 2: Starting day, 3: Ending month, 4: Ending day, 5: Year. */
								__( '%1$s %2$d – %3$s %4$d, %5$d' ),
								$start_month,
								date_i18n( _x( 'j', 'upcoming events day format' ), $timestamp ),
								$end_month,
								date_i18n( _x( 'j', 'upcoming events day format' ), $end_timestamp ),
								date_i18n( _x( 'Y', 'upcoming events year format' ), $timestamp )
							);
						}

						$formatted_date = wp_maybe_decline_date( $formatted_date, 'F j, Y' );
					}
				}

				$response_body['events'][ $key ]['formatted_date'] = $formatted_date;
				$response_body['events'][ $key ]['formatted_time'] = $formatted_time;
			}
		}

		return $response_body;
	}

	/**
	 * Prepares the event list for presentation.
	 *
	 * Discards expired events, and makes WordCamps "sticky." Attendees need more
	 * advanced notice about WordCamps than they do for meetups, so camps should
	 * appear in the list sooner. If a WordCamp is coming up, the API will "stick"
	 * it in the response, even if it wouldn't otherwise appear. When that happens,
	 * the event will be at the end of the list, and will need to be moved into a
	 * higher position, so that it doesn't get trimmed off.
	 *
	 * @since 4.8.0
	 * @since 4.9.7 Stick a WordCamp to the final list.
	 * @since 5.5.2 Accepts and returns only the events, rather than an entire HTTP response.
	 * @since 6.0.0 Decode HTML entities from the event title.
	 *
	 * @param array $events The events that will be prepared.
	 * @return array The response body with events trimmed.
	 */
	protected function trim_events( array $events ) {
		$future_events = array();

		foreach ( $events as $event ) {
			/*
			 * The API's `date` and `end_date` fields are in the _event's_ local timezone, but UTC is needed so
			 * it can be converted to the _user's_ local time.
			 */
			$end_time = (int) $event['end_unix_timestamp'];

			if ( time() < $end_time ) {
				// Decode HTML entities from the event title.
				$event['title'] = html_entity_decode( $event['title'], ENT_QUOTES, 'UTF-8' );

				array_push( $future_events, $event );
			}
		}

		$future_wordcamps = array_filter(
			$future_events,
			static function ( $wordcamp ) {
				return 'wordcamp' === $wordcamp['type'];
			}
		);

		$future_wordcamps    = array_values( $future_wordcamps ); // Remove gaps in indices.
		$trimmed_events      = array_slice( $future_events, 0, 3 );
		$trimmed_event_types = wp_list_pluck( $trimmed_events, 'type' );

		// Make sure the soonest upcoming WordCamp is pinned in the list.
		if ( $future_wordcamps && ! in_array( 'wordcamp', $trimmed_event_types, true ) ) {
			array_pop( $trimmed_events );
			array_push( $trimmed_events, $future_wordcamps[0] );
		}

		return $trimmed_events;
	}

	/**
	 * Logs responses to Events API requests.
	 *
	 * @since 4.8.0
	 * @deprecated 4.9.0 Use a plugin instead. See #41217 for an example.
	 *
	 * @param string $message A description of what occurred.
	 * @param array  $details Details that provide more context for the
	 *                        log entry.
	 */
	protected function maybe_log_events_response( $message, $details ) {
		_deprecated_function( __METHOD__, '4.9.0' );

		if ( ! WP_DEBUG_LOG ) {
			return;
		}

		error_log(
			sprintf(
				'%s: %s. Details: %s',
				__METHOD__,
				trim( $message, '.' ),
				wp_json_encode( $details )
			)
		);
	}
}
screen.php000060400000014352151331232430006532 0ustar00<?php
/**
 * WordPress Administration Screen API.
 *
 * @package WordPress
 * @subpackage Administration
 */

/**
 * Get the column headers for a screen
 *
 * @since 2.7.0
 *
 * @param string|WP_Screen $screen The screen you want the headers for
 * @return string[] The column header labels keyed by column ID.
 */
function get_column_headers( $screen ) {
	static $column_headers = array();

	if ( is_string( $screen ) ) {
		$screen = convert_to_screen( $screen );
	}

	if ( ! isset( $column_headers[ $screen->id ] ) ) {
		/**
		 * Filters the column headers for a list table on a specific screen.
		 *
		 * The dynamic portion of the hook name, `$screen->id`, refers to the
		 * ID of a specific screen. For example, the screen ID for the Posts
		 * list table is edit-post, so the filter for that screen would be
		 * manage_edit-post_columns.
		 *
		 * @since 3.0.0
		 *
		 * @param string[] $columns The column header labels keyed by column ID.
		 */
		$column_headers[ $screen->id ] = apply_filters( "manage_{$screen->id}_columns", array() );
	}

	return $column_headers[ $screen->id ];
}

/**
 * Get a list of hidden columns.
 *
 * @since 2.7.0
 *
 * @param string|WP_Screen $screen The screen you want the hidden columns for
 * @return string[] Array of IDs of hidden columns.
 */
function get_hidden_columns( $screen ) {
	if ( is_string( $screen ) ) {
		$screen = convert_to_screen( $screen );
	}

	$hidden = get_user_option( 'manage' . $screen->id . 'columnshidden' );

	$use_defaults = ! is_array( $hidden );

	if ( $use_defaults ) {
		$hidden = array();

		/**
		 * Filters the default list of hidden columns.
		 *
		 * @since 4.4.0
		 *
		 * @param string[]  $hidden Array of IDs of columns hidden by default.
		 * @param WP_Screen $screen WP_Screen object of the current screen.
		 */
		$hidden = apply_filters( 'default_hidden_columns', $hidden, $screen );
	}

	/**
	 * Filters the list of hidden columns.
	 *
	 * @since 4.4.0
	 * @since 4.4.1 Added the `use_defaults` parameter.
	 *
	 * @param string[]  $hidden       Array of IDs of hidden columns.
	 * @param WP_Screen $screen       WP_Screen object of the current screen.
	 * @param bool      $use_defaults Whether to show the default columns.
	 */
	return apply_filters( 'hidden_columns', $hidden, $screen, $use_defaults );
}

/**
 * Prints the meta box preferences for screen meta.
 *
 * @since 2.7.0
 *
 * @global array $wp_meta_boxes Global meta box state.
 *
 * @param WP_Screen $screen
 */
function meta_box_prefs( $screen ) {
	global $wp_meta_boxes;

	if ( is_string( $screen ) ) {
		$screen = convert_to_screen( $screen );
	}

	if ( empty( $wp_meta_boxes[ $screen->id ] ) ) {
		return;
	}

	$hidden = get_hidden_meta_boxes( $screen );

	foreach ( array_keys( $wp_meta_boxes[ $screen->id ] ) as $context ) {
		foreach ( array( 'high', 'core', 'default', 'low' ) as $priority ) {
			if ( ! isset( $wp_meta_boxes[ $screen->id ][ $context ][ $priority ] ) ) {
				continue;
			}

			foreach ( $wp_meta_boxes[ $screen->id ][ $context ][ $priority ] as $box ) {
				if ( false === $box || ! $box['title'] ) {
					continue;
				}

				// Submit box cannot be hidden.
				if ( 'submitdiv' === $box['id'] || 'linksubmitdiv' === $box['id'] ) {
					continue;
				}

				$widget_title = $box['title'];

				if ( is_array( $box['args'] ) && isset( $box['args']['__widget_basename'] ) ) {
					$widget_title = $box['args']['__widget_basename'];
				}

				$is_hidden = in_array( $box['id'], $hidden, true );

				printf(
					'<label for="%1$s-hide"><input class="hide-postbox-tog" name="%1$s-hide" type="checkbox" id="%1$s-hide" value="%1$s" %2$s />%3$s</label>',
					esc_attr( $box['id'] ),
					checked( $is_hidden, false, false ),
					$widget_title
				);
			}
		}
	}
}

/**
 * Gets an array of IDs of hidden meta boxes.
 *
 * @since 2.7.0
 *
 * @param string|WP_Screen $screen Screen identifier
 * @return string[] IDs of hidden meta boxes.
 */
function get_hidden_meta_boxes( $screen ) {
	if ( is_string( $screen ) ) {
		$screen = convert_to_screen( $screen );
	}

	$hidden = get_user_option( "metaboxhidden_{$screen->id}" );

	$use_defaults = ! is_array( $hidden );

	// Hide slug boxes by default.
	if ( $use_defaults ) {
		$hidden = array();

		if ( 'post' === $screen->base ) {
			if ( in_array( $screen->post_type, array( 'post', 'page', 'attachment' ), true ) ) {
				$hidden = array( 'slugdiv', 'trackbacksdiv', 'postcustom', 'postexcerpt', 'commentstatusdiv', 'commentsdiv', 'authordiv', 'revisionsdiv' );
			} else {
				$hidden = array( 'slugdiv' );
			}
		}

		/**
		 * Filters the default list of hidden meta boxes.
		 *
		 * @since 3.1.0
		 *
		 * @param string[]  $hidden An array of IDs of meta boxes hidden by default.
		 * @param WP_Screen $screen WP_Screen object of the current screen.
		 */
		$hidden = apply_filters( 'default_hidden_meta_boxes', $hidden, $screen );
	}

	/**
	 * Filters the list of hidden meta boxes.
	 *
	 * @since 3.3.0
	 *
	 * @param string[]  $hidden       An array of IDs of hidden meta boxes.
	 * @param WP_Screen $screen       WP_Screen object of the current screen.
	 * @param bool      $use_defaults Whether to show the default meta boxes.
	 *                                Default true.
	 */
	return apply_filters( 'hidden_meta_boxes', $hidden, $screen, $use_defaults );
}

/**
 * Register and configure an admin screen option
 *
 * @since 3.1.0
 *
 * @param string $option An option name.
 * @param mixed  $args   Option-dependent arguments.
 */
function add_screen_option( $option, $args = array() ) {
	$current_screen = get_current_screen();

	if ( ! $current_screen ) {
		return;
	}

	$current_screen->add_option( $option, $args );
}

/**
 * Get the current screen object
 *
 * @since 3.1.0
 *
 * @global WP_Screen $current_screen WordPress current screen object.
 *
 * @return WP_Screen|null Current screen object or null when screen not defined.
 */
function get_current_screen() {
	global $current_screen;

	if ( ! isset( $current_screen ) ) {
		return null;
	}

	return $current_screen;
}

/**
 * Set the current screen object
 *
 * @since 3.0.0
 *
 * @param string|WP_Screen $hook_name Optional. The hook name (also known as the hook suffix) used to determine the screen,
 *                                    or an existing screen object.
 */
function set_current_screen( $hook_name = '' ) {
	WP_Screen::get( $hook_name )->set_current_screen();
}
theme.php000060400000135176151331232430006365 0ustar00<?php
/**
 * WordPress Theme Administration API
 *
 * @package WordPress
 * @subpackage Administration
 */

/**
 * Removes a theme.
 *
 * @since 2.8.0
 *
 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
 *
 * @param string $stylesheet Stylesheet of the theme to delete.
 * @param string $redirect   Redirect to page when complete.
 * @return bool|null|WP_Error True on success, false if `$stylesheet` is empty, WP_Error on failure.
 *                            Null if filesystem credentials are required to proceed.
 */
function delete_theme( $stylesheet, $redirect = '' ) {
	global $wp_filesystem;

	if ( empty( $stylesheet ) ) {
		return false;
	}

	if ( empty( $redirect ) ) {
		$redirect = wp_nonce_url( 'themes.php?action=delete&stylesheet=' . urlencode( $stylesheet ), 'delete-theme_' . $stylesheet );
	}

	ob_start();
	$credentials = request_filesystem_credentials( $redirect );
	$data        = ob_get_clean();

	if ( false === $credentials ) {
		if ( ! empty( $data ) ) {
			require_once ABSPATH . 'wp-admin/admin-header.php';
			echo $data;
			require_once ABSPATH . 'wp-admin/admin-footer.php';
			exit;
		}
		return;
	}

	if ( ! WP_Filesystem( $credentials ) ) {
		ob_start();
		// Failed to connect. Error and request again.
		request_filesystem_credentials( $redirect, '', true );
		$data = ob_get_clean();

		if ( ! empty( $data ) ) {
			require_once ABSPATH . 'wp-admin/admin-header.php';
			echo $data;
			require_once ABSPATH . 'wp-admin/admin-footer.php';
			exit;
		}
		return;
	}

	if ( ! is_object( $wp_filesystem ) ) {
		return new WP_Error( 'fs_unavailable', __( 'Could not access filesystem.' ) );
	}

	if ( is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
		return new WP_Error( 'fs_error', __( 'Filesystem error.' ), $wp_filesystem->errors );
	}

	// Get the base theme folder.
	$themes_dir = $wp_filesystem->wp_themes_dir();
	if ( empty( $themes_dir ) ) {
		return new WP_Error( 'fs_no_themes_dir', __( 'Unable to locate WordPress theme directory.' ) );
	}

	/**
	 * Fires immediately before a theme deletion attempt.
	 *
	 * @since 5.8.0
	 *
	 * @param string $stylesheet Stylesheet of the theme to delete.
	 */
	do_action( 'delete_theme', $stylesheet );

	$theme = wp_get_theme( $stylesheet );

	$themes_dir = trailingslashit( $themes_dir );
	$theme_dir  = trailingslashit( $themes_dir . $stylesheet );
	$deleted    = $wp_filesystem->delete( $theme_dir, true );

	/**
	 * Fires immediately after a theme deletion attempt.
	 *
	 * @since 5.8.0
	 *
	 * @param string $stylesheet Stylesheet of the theme to delete.
	 * @param bool   $deleted    Whether the theme deletion was successful.
	 */
	do_action( 'deleted_theme', $stylesheet, $deleted );

	if ( ! $deleted ) {
		return new WP_Error(
			'could_not_remove_theme',
			/* translators: %s: Theme name. */
			sprintf( __( 'Could not fully remove the theme %s.' ), $stylesheet )
		);
	}

	$theme_translations = wp_get_installed_translations( 'themes' );

	// Remove language files, silently.
	if ( ! empty( $theme_translations[ $stylesheet ] ) ) {
		$translations = $theme_translations[ $stylesheet ];

		foreach ( $translations as $translation => $data ) {
			$wp_filesystem->delete( WP_LANG_DIR . '/themes/' . $stylesheet . '-' . $translation . '.po' );
			$wp_filesystem->delete( WP_LANG_DIR . '/themes/' . $stylesheet . '-' . $translation . '.mo' );
			$wp_filesystem->delete( WP_LANG_DIR . '/themes/' . $stylesheet . '-' . $translation . '.l10n.php' );

			$json_translation_files = glob( WP_LANG_DIR . '/themes/' . $stylesheet . '-' . $translation . '-*.json' );
			if ( $json_translation_files ) {
				array_map( array( $wp_filesystem, 'delete' ), $json_translation_files );
			}
		}
	}

	// Remove the theme from allowed themes on the network.
	if ( is_multisite() ) {
		WP_Theme::network_disable_theme( $stylesheet );
	}

	// Clear theme caches.
	$theme->cache_delete();

	// Force refresh of theme update information.
	delete_site_transient( 'update_themes' );

	return true;
}

/**
 * Gets the page templates available in this theme.
 *
 * @since 1.5.0
 * @since 4.7.0 Added the `$post_type` parameter.
 *
 * @param WP_Post|null $post      Optional. The post being edited, provided for context.
 * @param string       $post_type Optional. Post type to get the templates for. Default 'page'.
 * @return string[] Array of template file names keyed by the template header name.
 */
function get_page_templates( $post = null, $post_type = 'page' ) {
	return array_flip( wp_get_theme()->get_page_templates( $post, $post_type ) );
}

/**
 * Tidies a filename for url display by the theme file editor.
 *
 * @since 2.9.0
 * @access private
 *
 * @param string $fullpath Full path to the theme file
 * @param string $containingfolder Path of the theme parent folder
 * @return string
 */
function _get_template_edit_filename( $fullpath, $containingfolder ) {
	return str_replace( dirname( $containingfolder, 2 ), '', $fullpath );
}

/**
 * Check if there is an update for a theme available.
 *
 * Will display link, if there is an update available.
 *
 * @since 2.7.0
 *
 * @see get_theme_update_available()
 *
 * @param WP_Theme $theme Theme data object.
 */
function theme_update_available( $theme ) {
	echo get_theme_update_available( $theme );
}

/**
 * Retrieves the update link if there is a theme update available.
 *
 * Will return a link if there is an update available.
 *
 * @since 3.8.0
 *
 * @param WP_Theme $theme WP_Theme object.
 * @return string|false HTML for the update link, or false if invalid info was passed.
 */
function get_theme_update_available( $theme ) {
	static $themes_update = null;

	if ( ! current_user_can( 'update_themes' ) ) {
		return false;
	}

	if ( ! isset( $themes_update ) ) {
		$themes_update = get_site_transient( 'update_themes' );
	}

	if ( ! ( $theme instanceof WP_Theme ) ) {
		return false;
	}

	$stylesheet = $theme->get_stylesheet();

	$html = '';

	if ( isset( $themes_update->response[ $stylesheet ] ) ) {
		$update      = $themes_update->response[ $stylesheet ];
		$theme_name  = $theme->display( 'Name' );
		$details_url = add_query_arg(
			array(
				'TB_iframe' => 'true',
				'width'     => 1024,
				'height'    => 800,
			),
			$update['url']
		); // Theme browser inside WP? Replace this. Also, theme preview JS will override this on the available list.
		$update_url  = wp_nonce_url( admin_url( 'update.php?action=upgrade-theme&amp;theme=' . urlencode( $stylesheet ) ), 'upgrade-theme_' . $stylesheet );

		if ( ! is_multisite() ) {
			if ( ! current_user_can( 'update_themes' ) ) {
				$html = sprintf(
					/* translators: 1: Theme name, 2: Theme details URL, 3: Additional link attributes, 4: Version number. */
					'<p><strong>' . __( 'There is a new version of %1$s available. <a href="%2$s" %3$s>View version %4$s details</a>.' ) . '</strong></p>',
					$theme_name,
					esc_url( $details_url ),
					sprintf(
						'class="thickbox open-plugin-details-modal" aria-label="%s"',
						/* translators: 1: Theme name, 2: Version number. */
						esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $theme_name, $update['new_version'] ) )
					),
					$update['new_version']
				);
			} elseif ( empty( $update['package'] ) ) {
				$html = sprintf(
					/* translators: 1: Theme name, 2: Theme details URL, 3: Additional link attributes, 4: Version number. */
					'<p><strong>' . __( 'There is a new version of %1$s available. <a href="%2$s" %3$s>View version %4$s details</a>. <em>Automatic update is unavailable for this theme.</em>' ) . '</strong></p>',
					$theme_name,
					esc_url( $details_url ),
					sprintf(
						'class="thickbox open-plugin-details-modal" aria-label="%s"',
						/* translators: 1: Theme name, 2: Version number. */
						esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $theme_name, $update['new_version'] ) )
					),
					$update['new_version']
				);
			} else {
				$html = sprintf(
					/* translators: 1: Theme name, 2: Theme details URL, 3: Additional link attributes, 4: Version number, 5: Update URL, 6: Additional link attributes. */
					'<p><strong>' . __( 'There is a new version of %1$s available. <a href="%2$s" %3$s>View version %4$s details</a> or <a href="%5$s" %6$s>update now</a>.' ) . '</strong></p>',
					$theme_name,
					esc_url( $details_url ),
					sprintf(
						'class="thickbox open-plugin-details-modal" aria-label="%s"',
						/* translators: 1: Theme name, 2: Version number. */
						esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $theme_name, $update['new_version'] ) )
					),
					$update['new_version'],
					$update_url,
					sprintf(
						'aria-label="%s" id="update-theme" data-slug="%s"',
						/* translators: %s: Theme name. */
						esc_attr( sprintf( _x( 'Update %s now', 'theme' ), $theme_name ) ),
						$stylesheet
					)
				);
			}
		}
	}

	return $html;
}

/**
 * Retrieves list of WordPress theme features (aka theme tags).
 *
 * @since 3.1.0
 * @since 3.2.0 Added 'Gray' color and 'Featured Image Header', 'Featured Images',
 *              'Full Width Template', and 'Post Formats' features.
 * @since 3.5.0 Added 'Flexible Header' feature.
 * @since 3.8.0 Renamed 'Width' filter to 'Layout'.
 * @since 3.8.0 Renamed 'Fixed Width' and 'Flexible Width' options
 *              to 'Fixed Layout' and 'Fluid Layout'.
 * @since 3.8.0 Added 'Accessibility Ready' feature and 'Responsive Layout' option.
 * @since 3.9.0 Combined 'Layout' and 'Columns' filters.
 * @since 4.6.0 Removed 'Colors' filter.
 * @since 4.6.0 Added 'Grid Layout' option.
 *              Removed 'Fixed Layout', 'Fluid Layout', and 'Responsive Layout' options.
 * @since 4.6.0 Added 'Custom Logo' and 'Footer Widgets' features.
 *              Removed 'Blavatar' feature.
 * @since 4.6.0 Added 'Blog', 'E-Commerce', 'Education', 'Entertainment', 'Food & Drink',
 *              'Holiday', 'News', 'Photography', and 'Portfolio' subjects.
 *              Removed 'Photoblogging' and 'Seasonal' subjects.
 * @since 4.9.0 Reordered the filters from 'Layout', 'Features', 'Subject'
 *              to 'Subject', 'Features', 'Layout'.
 * @since 4.9.0 Removed 'BuddyPress', 'Custom Menu', 'Flexible Header',
 *              'Front Page Posting', 'Microformats', 'RTL Language Support',
 *              'Threaded Comments', and 'Translation Ready' features.
 * @since 5.5.0 Added 'Block Editor Patterns', 'Block Editor Styles',
 *              and 'Full Site Editing' features.
 * @since 5.5.0 Added 'Wide Blocks' layout option.
 * @since 5.8.1 Added 'Template Editing' feature.
 * @since 6.1.1 Replaced 'Full Site Editing' feature name with 'Site Editor'.
 * @since 6.2.0 Added 'Style Variations' feature.
 *
 * @param bool $api Optional. Whether try to fetch tags from the WordPress.org API. Defaults to true.
 * @return array Array of features keyed by category with translations keyed by slug.
 */
function get_theme_feature_list( $api = true ) {
	// Hard-coded list is used if API is not accessible.
	$features = array(

		__( 'Subject' )  => array(
			'blog'           => __( 'Blog' ),
			'e-commerce'     => __( 'E-Commerce' ),
			'education'      => __( 'Education' ),
			'entertainment'  => __( 'Entertainment' ),
			'food-and-drink' => __( 'Food & Drink' ),
			'holiday'        => __( 'Holiday' ),
			'news'           => __( 'News' ),
			'photography'    => __( 'Photography' ),
			'portfolio'      => __( 'Portfolio' ),
		),

		__( 'Features' ) => array(
			'accessibility-ready'   => __( 'Accessibility Ready' ),
			'block-patterns'        => __( 'Block Editor Patterns' ),
			'block-styles'          => __( 'Block Editor Styles' ),
			'custom-background'     => __( 'Custom Background' ),
			'custom-colors'         => __( 'Custom Colors' ),
			'custom-header'         => __( 'Custom Header' ),
			'custom-logo'           => __( 'Custom Logo' ),
			'editor-style'          => __( 'Editor Style' ),
			'featured-image-header' => __( 'Featured Image Header' ),
			'featured-images'       => __( 'Featured Images' ),
			'footer-widgets'        => __( 'Footer Widgets' ),
			'full-site-editing'     => __( 'Site Editor' ),
			'full-width-template'   => __( 'Full Width Template' ),
			'post-formats'          => __( 'Post Formats' ),
			'sticky-post'           => __( 'Sticky Post' ),
			'style-variations'      => __( 'Style Variations' ),
			'template-editing'      => __( 'Template Editing' ),
			'theme-options'         => __( 'Theme Options' ),
		),

		__( 'Layout' )   => array(
			'grid-layout'   => __( 'Grid Layout' ),
			'one-column'    => __( 'One Column' ),
			'two-columns'   => __( 'Two Columns' ),
			'three-columns' => __( 'Three Columns' ),
			'four-columns'  => __( 'Four Columns' ),
			'left-sidebar'  => __( 'Left Sidebar' ),
			'right-sidebar' => __( 'Right Sidebar' ),
			'wide-blocks'   => __( 'Wide Blocks' ),
		),

	);

	if ( ! $api || ! current_user_can( 'install_themes' ) ) {
		return $features;
	}

	$feature_list = get_site_transient( 'wporg_theme_feature_list' );
	if ( ! $feature_list ) {
		set_site_transient( 'wporg_theme_feature_list', array(), 3 * HOUR_IN_SECONDS );
	}

	if ( ! $feature_list ) {
		$feature_list = themes_api( 'feature_list', array() );
		if ( is_wp_error( $feature_list ) ) {
			return $features;
		}
	}

	if ( ! $feature_list ) {
		return $features;
	}

	set_site_transient( 'wporg_theme_feature_list', $feature_list, 3 * HOUR_IN_SECONDS );

	$category_translations = array(
		'Layout'   => __( 'Layout' ),
		'Features' => __( 'Features' ),
		'Subject'  => __( 'Subject' ),
	);

	$wporg_features = array();

	// Loop over the wp.org canonical list and apply translations.
	foreach ( (array) $feature_list as $feature_category => $feature_items ) {
		if ( isset( $category_translations[ $feature_category ] ) ) {
			$feature_category = $category_translations[ $feature_category ];
		}

		$wporg_features[ $feature_category ] = array();

		foreach ( $feature_items as $feature ) {
			if ( isset( $features[ $feature_category ][ $feature ] ) ) {
				$wporg_features[ $feature_category ][ $feature ] = $features[ $feature_category ][ $feature ];
			} else {
				$wporg_features[ $feature_category ][ $feature ] = $feature;
			}
		}
	}

	return $wporg_features;
}

/**
 * Retrieves theme installer pages from the WordPress.org Themes API.
 *
 * It is possible for a theme to override the Themes API result with three
 * filters. Assume this is for themes, which can extend on the Theme Info to
 * offer more choices. This is very powerful and must be used with care, when
 * overriding the filters.
 *
 * The first filter, {@see 'themes_api_args'}, is for the args and gives the action
 * as the second parameter. The hook for {@see 'themes_api_args'} must ensure that
 * an object is returned.
 *
 * The second filter, {@see 'themes_api'}, allows a plugin to override the WordPress.org
 * Theme API entirely. If `$action` is 'query_themes', 'theme_information', or 'feature_list',
 * an object MUST be passed. If `$action` is 'hot_tags', an array should be passed.
 *
 * Finally, the third filter, {@see 'themes_api_result'}, makes it possible to filter the
 * response object or array, depending on the `$action` type.
 *
 * Supported arguments per action:
 *
 * | Argument Name      | 'query_themes' | 'theme_information' | 'hot_tags' | 'feature_list'   |
 * | -------------------| :------------: | :-----------------: | :--------: | :--------------: |
 * | `$slug`            | No             |  Yes                | No         | No               |
 * | `$per_page`        | Yes            |  No                 | No         | No               |
 * | `$page`            | Yes            |  No                 | No         | No               |
 * | `$number`          | No             |  No                 | Yes        | No               |
 * | `$search`          | Yes            |  No                 | No         | No               |
 * | `$tag`             | Yes            |  No                 | No         | No               |
 * | `$author`          | Yes            |  No                 | No         | No               |
 * | `$user`            | Yes            |  No                 | No         | No               |
 * | `$browse`          | Yes            |  No                 | No         | No               |
 * | `$locale`          | Yes            |  Yes                | No         | No               |
 * | `$fields`          | Yes            |  Yes                | No         | No               |
 *
 * @since 2.8.0
 *
 * @param string       $action API action to perform: Accepts 'query_themes', 'theme_information',
 *                             'hot_tags' or 'feature_list'.
 * @param array|object $args   {
 *     Optional. Array or object of arguments to serialize for the Themes API. Default empty array.
 *
 *     @type string  $slug     The theme slug. Default empty.
 *     @type int     $per_page Number of themes per page. Default 24.
 *     @type int     $page     Number of current page. Default 1.
 *     @type int     $number   Number of tags to be queried.
 *     @type string  $search   A search term. Default empty.
 *     @type string  $tag      Tag to filter themes. Default empty.
 *     @type string  $author   Username of an author to filter themes. Default empty.
 *     @type string  $user     Username to query for their favorites. Default empty.
 *     @type string  $browse   Browse view: 'featured', 'popular', 'updated', 'favorites'.
 *     @type string  $locale   Locale to provide context-sensitive results. Default is the value of get_locale().
 *     @type array   $fields   {
 *         Array of fields which should or should not be returned.
 *
 *         @type bool $description        Whether to return the theme full description. Default false.
 *         @type bool $sections           Whether to return the theme readme sections: description, installation,
 *                                        FAQ, screenshots, other notes, and changelog. Default false.
 *         @type bool $rating             Whether to return the rating in percent and total number of ratings.
 *                                        Default false.
 *         @type bool $ratings            Whether to return the number of rating for each star (1-5). Default false.
 *         @type bool $downloaded         Whether to return the download count. Default false.
 *         @type bool $downloadlink       Whether to return the download link for the package. Default false.
 *         @type bool $last_updated       Whether to return the date of the last update. Default false.
 *         @type bool $tags               Whether to return the assigned tags. Default false.
 *         @type bool $homepage           Whether to return the theme homepage link. Default false.
 *         @type bool $screenshots        Whether to return the screenshots. Default false.
 *         @type int  $screenshot_count   Number of screenshots to return. Default 1.
 *         @type bool $screenshot_url     Whether to return the URL of the first screenshot. Default false.
 *         @type bool $photon_screenshots Whether to return the screenshots via Photon. Default false.
 *         @type bool $template           Whether to return the slug of the parent theme. Default false.
 *         @type bool $parent             Whether to return the slug, name and homepage of the parent theme. Default false.
 *         @type bool $versions           Whether to return the list of all available versions. Default false.
 *         @type bool $theme_url          Whether to return theme's URL. Default false.
 *         @type bool $extended_author    Whether to return nicename or nicename and display name. Default false.
 *     }
 * }
 * @return object|array|WP_Error Response object or array on success, WP_Error on failure. See the
 *         {@link https://developer.wordpress.org/reference/functions/themes_api/ function reference article}
 *         for more information on the make-up of possible return objects depending on the value of `$action`.
 */
function themes_api( $action, $args = array() ) {
	if ( is_array( $args ) ) {
		$args = (object) $args;
	}

	if ( 'query_themes' === $action ) {
		if ( ! isset( $args->per_page ) ) {
			$args->per_page = 24;
		}
	}

	if ( ! isset( $args->locale ) ) {
		$args->locale = get_user_locale();
	}

	if ( ! isset( $args->wp_version ) ) {
		$args->wp_version = substr( wp_get_wp_version(), 0, 3 ); // x.y
	}

	/**
	 * Filters arguments used to query for installer pages from the WordPress.org Themes API.
	 *
	 * Important: An object MUST be returned to this filter.
	 *
	 * @since 2.8.0
	 *
	 * @param object $args   Arguments used to query for installer pages from the WordPress.org Themes API.
	 * @param string $action Requested action. Likely values are 'theme_information',
	 *                       'feature_list', or 'query_themes'.
	 */
	$args = apply_filters( 'themes_api_args', $args, $action );

	/**
	 * Filters whether to override the WordPress.org Themes API.
	 *
	 * Returning a non-false value will effectively short-circuit the WordPress.org API request.
	 *
	 * If `$action` is 'query_themes', 'theme_information', or 'feature_list', an object MUST
	 * be passed. If `$action` is 'hot_tags', an array should be passed.
	 *
	 * @since 2.8.0
	 *
	 * @param false|object|array $override Whether to override the WordPress.org Themes API. Default false.
	 * @param string             $action   Requested action. Likely values are 'theme_information',
	 *                                    'feature_list', or 'query_themes'.
	 * @param object             $args     Arguments used to query for installer pages from the Themes API.
	 */
	$res = apply_filters( 'themes_api', false, $action, $args );

	if ( ! $res ) {
		$url = 'http://api.wordpress.org/themes/info/1.2/';
		$url = add_query_arg(
			array(
				'action'  => $action,
				'request' => $args,
			),
			$url
		);

		$http_url = $url;
		$ssl      = wp_http_supports( array( 'ssl' ) );
		if ( $ssl ) {
			$url = set_url_scheme( $url, 'https' );
		}

		$http_args = array(
			'timeout'    => 15,
			'user-agent' => 'WordPress/' . wp_get_wp_version() . '; ' . home_url( '/' ),
		);
		$request   = wp_remote_get( $url, $http_args );

		if ( $ssl && is_wp_error( $request ) ) {
			if ( ! wp_doing_ajax() ) {
				wp_trigger_error(
					__FUNCTION__,
					sprintf(
						/* translators: %s: Support forums URL. */
						__( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server&#8217;s configuration. If you continue to have problems, please try the <a href="%s">support forums</a>.' ),
						__( 'https://wordpress.org/support/forums/' )
					) . ' ' . __( '(WordPress could not establish a secure connection to WordPress.org. Please contact your server administrator.)' ),
					headers_sent() || WP_DEBUG ? E_USER_WARNING : E_USER_NOTICE
				);
			}
			$request = wp_remote_get( $http_url, $http_args );
		}

		if ( is_wp_error( $request ) ) {
			$res = new WP_Error(
				'themes_api_failed',
				sprintf(
					/* translators: %s: Support forums URL. */
					__( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server&#8217;s configuration. If you continue to have problems, please try the <a href="%s">support forums</a>.' ),
					__( 'https://wordpress.org/support/forums/' )
				),
				$request->get_error_message()
			);
		} else {
			$res = json_decode( wp_remote_retrieve_body( $request ), true );
			if ( is_array( $res ) ) {
				// Object casting is required in order to match the info/1.0 format.
				$res = (object) $res;
			} elseif ( null === $res ) {
				$res = new WP_Error(
					'themes_api_failed',
					sprintf(
						/* translators: %s: Support forums URL. */
						__( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server&#8217;s configuration. If you continue to have problems, please try the <a href="%s">support forums</a>.' ),
						__( 'https://wordpress.org/support/forums/' )
					),
					wp_remote_retrieve_body( $request )
				);
			}

			if ( isset( $res->error ) ) {
				$res = new WP_Error( 'themes_api_failed', $res->error );
			}
		}

		if ( ! is_wp_error( $res ) ) {
			// Back-compat for info/1.2 API, upgrade the theme objects in query_themes to objects.
			if ( 'query_themes' === $action ) {
				foreach ( $res->themes as $i => $theme ) {
					$res->themes[ $i ] = (object) $theme;
				}
			}

			// Back-compat for info/1.2 API, downgrade the feature_list result back to an array.
			if ( 'feature_list' === $action ) {
				$res = (array) $res;
			}
		}
	}

	/**
	 * Filters the returned WordPress.org Themes API response.
	 *
	 * @since 2.8.0
	 *
	 * @param array|stdClass|WP_Error $res    WordPress.org Themes API response.
	 * @param string                  $action Requested action. Likely values are 'theme_information',
	 *                                        'feature_list', or 'query_themes'.
	 * @param stdClass                $args   Arguments used to query for installer pages from the WordPress.org Themes API.
	 */
	return apply_filters( 'themes_api_result', $res, $action, $args );
}

/**
 * Prepares themes for JavaScript.
 *
 * @since 3.8.0
 *
 * @param WP_Theme[] $themes Optional. Array of theme objects to prepare.
 *                           Defaults to all allowed themes.
 *
 * @return array An associative array of theme data, sorted by name.
 */
function wp_prepare_themes_for_js( $themes = null ) {
	$current_theme = get_stylesheet();

	/**
	 * Filters theme data before it is prepared for JavaScript.
	 *
	 * Passing a non-empty array will result in wp_prepare_themes_for_js() returning
	 * early with that value instead.
	 *
	 * @since 4.2.0
	 *
	 * @param array           $prepared_themes An associative array of theme data. Default empty array.
	 * @param WP_Theme[]|null $themes          An array of theme objects to prepare, if any.
	 * @param string          $current_theme   The active theme slug.
	 */
	$prepared_themes = (array) apply_filters( 'pre_prepare_themes_for_js', array(), $themes, $current_theme );

	if ( ! empty( $prepared_themes ) ) {
		return $prepared_themes;
	}

	// Make sure the active theme is listed first.
	$prepared_themes[ $current_theme ] = array();

	if ( null === $themes ) {
		$themes = wp_get_themes( array( 'allowed' => true ) );
		if ( ! isset( $themes[ $current_theme ] ) ) {
			$themes[ $current_theme ] = wp_get_theme();
		}
	}

	$updates    = array();
	$no_updates = array();
	if ( ! is_multisite() && current_user_can( 'update_themes' ) ) {
		$updates_transient = get_site_transient( 'update_themes' );
		if ( isset( $updates_transient->response ) ) {
			$updates = $updates_transient->response;
		}
		if ( isset( $updates_transient->no_update ) ) {
			$no_updates = $updates_transient->no_update;
		}
	}

	WP_Theme::sort_by_name( $themes );

	$parents = array();

	$auto_updates = (array) get_site_option( 'auto_update_themes', array() );

	foreach ( $themes as $theme ) {
		$slug         = $theme->get_stylesheet();
		$encoded_slug = urlencode( $slug );

		$parent = false;
		if ( $theme->parent() ) {
			$parent           = $theme->parent();
			$parents[ $slug ] = $parent->get_stylesheet();
			$parent           = $parent->display( 'Name' );
		}

		$customize_action = null;

		$can_edit_theme_options = current_user_can( 'edit_theme_options' );
		$can_customize          = current_user_can( 'customize' );
		$is_block_theme         = $theme->is_block_theme();

		if ( $is_block_theme && $can_edit_theme_options ) {
			$customize_action = admin_url( 'site-editor.php' );
			if ( $current_theme !== $slug ) {
				$customize_action = add_query_arg( 'wp_theme_preview', $slug, $customize_action );
			}
		} elseif ( ! $is_block_theme && $can_customize && $can_edit_theme_options ) {
			$customize_action = wp_customize_url( $slug );
		}
		if ( null !== $customize_action ) {
			$customize_action = add_query_arg(
				array(
					'return' => urlencode( sanitize_url( remove_query_arg( wp_removable_query_args(), wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) ),
				),
				$customize_action
			);
			$customize_action = esc_url( $customize_action );
		}

		$update_requires_wp  = isset( $updates[ $slug ]['requires'] ) ? $updates[ $slug ]['requires'] : null;
		$update_requires_php = isset( $updates[ $slug ]['requires_php'] ) ? $updates[ $slug ]['requires_php'] : null;

		$auto_update        = in_array( $slug, $auto_updates, true );
		$auto_update_action = $auto_update ? 'disable-auto-update' : 'enable-auto-update';

		if ( isset( $updates[ $slug ] ) ) {
			$auto_update_supported      = true;
			$auto_update_filter_payload = (object) $updates[ $slug ];
		} elseif ( isset( $no_updates[ $slug ] ) ) {
			$auto_update_supported      = true;
			$auto_update_filter_payload = (object) $no_updates[ $slug ];
		} else {
			$auto_update_supported = false;
			/*
			 * Create the expected payload for the auto_update_theme filter, this is the same data
			 * as contained within $updates or $no_updates but used when the Theme is not known.
			 */
			$auto_update_filter_payload = (object) array(
				'theme'        => $slug,
				'new_version'  => $theme->get( 'Version' ),
				'url'          => '',
				'package'      => '',
				'requires'     => $theme->get( 'RequiresWP' ),
				'requires_php' => $theme->get( 'RequiresPHP' ),
			);
		}

		$auto_update_forced = wp_is_auto_update_forced_for_item( 'theme', null, $auto_update_filter_payload );

		$prepared_themes[ $slug ] = array(
			'id'             => $slug,
			'name'           => $theme->display( 'Name' ),
			'screenshot'     => array( $theme->get_screenshot() ), // @todo Multiple screenshots.
			'description'    => $theme->display( 'Description' ),
			'author'         => $theme->display( 'Author', false, true ),
			'authorAndUri'   => $theme->display( 'Author' ),
			'tags'           => $theme->display( 'Tags' ),
			'version'        => $theme->get( 'Version' ),
			'compatibleWP'   => is_wp_version_compatible( $theme->get( 'RequiresWP' ) ),
			'compatiblePHP'  => is_php_version_compatible( $theme->get( 'RequiresPHP' ) ),
			'updateResponse' => array(
				'compatibleWP'  => is_wp_version_compatible( $update_requires_wp ),
				'compatiblePHP' => is_php_version_compatible( $update_requires_php ),
			),
			'parent'         => $parent,
			'active'         => $slug === $current_theme,
			'hasUpdate'      => isset( $updates[ $slug ] ),
			'hasPackage'     => isset( $updates[ $slug ] ) && ! empty( $updates[ $slug ]['package'] ),
			'update'         => get_theme_update_available( $theme ),
			'autoupdate'     => array(
				'enabled'   => $auto_update || $auto_update_forced,
				'supported' => $auto_update_supported,
				'forced'    => $auto_update_forced,
			),
			'actions'        => array(
				'activate'   => current_user_can( 'switch_themes' ) ? wp_nonce_url( admin_url( 'themes.php?action=activate&amp;stylesheet=' . $encoded_slug ), 'switch-theme_' . $slug ) : null,
				'customize'  => $customize_action,
				'delete'     => ( ! is_multisite() && current_user_can( 'delete_themes' ) ) ? wp_nonce_url( admin_url( 'themes.php?action=delete&amp;stylesheet=' . $encoded_slug ), 'delete-theme_' . $slug ) : null,
				'autoupdate' => wp_is_auto_update_enabled_for_type( 'theme' ) && ! is_multisite() && current_user_can( 'update_themes' )
					? wp_nonce_url( admin_url( 'themes.php?action=' . $auto_update_action . '&amp;stylesheet=' . $encoded_slug ), 'updates' )
					: null,
			),
			'blockTheme'     => $theme->is_block_theme(),
		);
	}

	// Remove 'delete' action if theme has an active child.
	if ( ! empty( $parents ) && array_key_exists( $current_theme, $parents ) ) {
		unset( $prepared_themes[ $parents[ $current_theme ] ]['actions']['delete'] );
	}

	/**
	 * Filters the themes prepared for JavaScript, for themes.php.
	 *
	 * Could be useful for changing the order, which is by name by default.
	 *
	 * @since 3.8.0
	 *
	 * @param array $prepared_themes Array of theme data.
	 */
	$prepared_themes = apply_filters( 'wp_prepare_themes_for_js', $prepared_themes );
	$prepared_themes = array_values( $prepared_themes );
	return array_filter( $prepared_themes );
}

/**
 * Prints JS templates for the theme-browsing UI in the Customizer.
 *
 * @since 4.2.0
 */
function customize_themes_print_templates() {
	?>
	<script type="text/html" id="tmpl-customize-themes-details-view">
		<div class="theme-backdrop"></div>
		<div class="theme-wrap wp-clearfix" role="document">
			<div class="theme-header">
				<button type="button" class="left dashicons dashicons-no"><span class="screen-reader-text">
					<?php
					/* translators: Hidden accessibility text. */
					_e( 'Show previous theme' );
					?>
				</span></button>
				<button type="button" class="right dashicons dashicons-no"><span class="screen-reader-text">
					<?php
					/* translators: Hidden accessibility text. */
					_e( 'Show next theme' );
					?>
				</span></button>
				<button type="button" class="close dashicons dashicons-no"><span class="screen-reader-text">
					<?php
					/* translators: Hidden accessibility text. */
					_e( 'Close details dialog' );
					?>
				</span></button>
			</div>
			<div class="theme-about wp-clearfix">
				<div class="theme-screenshots">
				<# if ( data.screenshot && data.screenshot[0] ) { #>
					<div class="screenshot"><img src="{{ data.screenshot[0] }}?ver={{ data.version }}" alt="" /></div>
				<# } else { #>
					<div class="screenshot blank"></div>
				<# } #>
				</div>

				<div class="theme-info">
					<# if ( data.active ) { #>
						<span class="current-label"><?php _e( 'Active Theme' ); ?></span>
					<# } #>
					<h2 class="theme-name">{{{ data.name }}}<span class="theme-version">
						<?php
						/* translators: %s: Theme version. */
						printf( __( 'Version: %s' ), '{{ data.version }}' );
						?>
					</span></h2>
					<h3 class="theme-author">
						<?php
						/* translators: %s: Theme author link. */
						printf( __( 'By %s' ), '{{{ data.authorAndUri }}}' );
						?>
					</h3>

					<# if ( data.stars && 0 != data.num_ratings ) { #>
						<div class="theme-rating">
							{{{ data.stars }}}
							<a class="num-ratings" target="_blank" href="{{ data.reviews_url }}">
								<?php
								printf(
									'%1$s <span class="screen-reader-text">%2$s</span>',
									/* translators: %s: Number of ratings. */
									sprintf( __( '(%s ratings)' ), '{{ data.num_ratings }}' ),
									/* translators: Hidden accessibility text. */
									__( '(opens in a new tab)' )
								);
								?>
							</a>
						</div>
					<# } #>

					<# if ( data.hasUpdate ) { #>
						<# if ( data.updateResponse.compatibleWP && data.updateResponse.compatiblePHP ) { #>
							<div class="notice notice-warning notice-alt notice-large" data-slug="{{ data.id }}">
								<h3 class="notice-title"><?php _e( 'Update Available' ); ?></h3>
								{{{ data.update }}}
							</div>
						<# } else { #>
							<div class="notice notice-error notice-alt notice-large" data-slug="{{ data.id }}">
								<h3 class="notice-title"><?php _e( 'Update Incompatible' ); ?></h3>
								<p>
									<# if ( ! data.updateResponse.compatibleWP && ! data.updateResponse.compatiblePHP ) { #>
										<?php
										printf(
											/* translators: %s: Theme name. */
											__( 'There is a new version of %s available, but it does not work with your versions of WordPress and PHP.' ),
											'{{{ data.name }}}'
										);
										if ( current_user_can( 'update_core' ) && current_user_can( 'update_php' ) ) {
											printf(
												/* translators: 1: URL to WordPress Updates screen, 2: URL to Update PHP page. */
												' ' . __( '<a href="%1$s">Please update WordPress</a>, and then <a href="%2$s">learn more about updating PHP</a>.' ),
												self_admin_url( 'update-core.php' ),
												esc_url( wp_get_update_php_url() )
											);
											wp_update_php_annotation( '</p><p><em>', '</em>' );
										} elseif ( current_user_can( 'update_core' ) ) {
											printf(
												/* translators: %s: URL to WordPress Updates screen. */
												' ' . __( '<a href="%s">Please update WordPress</a>.' ),
												self_admin_url( 'update-core.php' )
											);
										} elseif ( current_user_can( 'update_php' ) ) {
											printf(
												/* translators: %s: URL to Update PHP page. */
												' ' . __( '<a href="%s">Learn more about updating PHP</a>.' ),
												esc_url( wp_get_update_php_url() )
											);
											wp_update_php_annotation( '</p><p><em>', '</em>' );
										}
										?>
									<# } else if ( ! data.updateResponse.compatibleWP ) { #>
										<?php
										printf(
											/* translators: %s: Theme name. */
											__( 'There is a new version of %s available, but it does not work with your version of WordPress.' ),
											'{{{ data.name }}}'
										);
										if ( current_user_can( 'update_core' ) ) {
											printf(
												/* translators: %s: URL to WordPress Updates screen. */
												' ' . __( '<a href="%s">Please update WordPress</a>.' ),
												self_admin_url( 'update-core.php' )
											);
										}
										?>
									<# } else if ( ! data.updateResponse.compatiblePHP ) { #>
										<?php
										printf(
											/* translators: %s: Theme name. */
											__( 'There is a new version of %s available, but it does not work with your version of PHP.' ),
											'{{{ data.name }}}'
										);
										if ( current_user_can( 'update_php' ) ) {
											printf(
												/* translators: %s: URL to Update PHP page. */
												' ' . __( '<a href="%s">Learn more about updating PHP</a>.' ),
												esc_url( wp_get_update_php_url() )
											);
											wp_update_php_annotation( '</p><p><em>', '</em>' );
										}
										?>
									<# } #>
								</p>
							</div>
						<# } #>
					<# } #>

					<# if ( data.parent ) { #>
						<p class="parent-theme">
							<?php
							printf(
								/* translators: %s: Theme name. */
								__( 'This is a child theme of %s.' ),
								'<strong>{{{ data.parent }}}</strong>'
							);
							?>
						</p>
					<# } #>

					<# if ( ! data.compatibleWP || ! data.compatiblePHP ) { #>
						<div class="notice notice-error notice-alt notice-large"><p>
							<# if ( ! data.compatibleWP && ! data.compatiblePHP ) { #>
								<?php
								_e( 'This theme does not work with your versions of WordPress and PHP.' );
								if ( current_user_can( 'update_core' ) && current_user_can( 'update_php' ) ) {
									printf(
										/* translators: 1: URL to WordPress Updates screen, 2: URL to Update PHP page. */
										' ' . __( '<a href="%1$s">Please update WordPress</a>, and then <a href="%2$s">learn more about updating PHP</a>.' ),
										self_admin_url( 'update-core.php' ),
										esc_url( wp_get_update_php_url() )
									);
									wp_update_php_annotation( '</p><p><em>', '</em>' );
								} elseif ( current_user_can( 'update_core' ) ) {
									printf(
										/* translators: %s: URL to WordPress Updates screen. */
										' ' . __( '<a href="%s">Please update WordPress</a>.' ),
										self_admin_url( 'update-core.php' )
									);
								} elseif ( current_user_can( 'update_php' ) ) {
									printf(
										/* translators: %s: URL to Update PHP page. */
										' ' . __( '<a href="%s">Learn more about updating PHP</a>.' ),
										esc_url( wp_get_update_php_url() )
									);
									wp_update_php_annotation( '</p><p><em>', '</em>' );
								}
								?>
							<# } else if ( ! data.compatibleWP ) { #>
								<?php
								_e( 'This theme does not work with your version of WordPress.' );
								if ( current_user_can( 'update_core' ) ) {
									printf(
										/* translators: %s: URL to WordPress Updates screen. */
										' ' . __( '<a href="%s">Please update WordPress</a>.' ),
										self_admin_url( 'update-core.php' )
									);
								}
								?>
							<# } else if ( ! data.compatiblePHP ) { #>
								<?php
								_e( 'This theme does not work with your version of PHP.' );
								if ( current_user_can( 'update_php' ) ) {
									printf(
										/* translators: %s: URL to Update PHP page. */
										' ' . __( '<a href="%s">Learn more about updating PHP</a>.' ),
										esc_url( wp_get_update_php_url() )
									);
									wp_update_php_annotation( '</p><p><em>', '</em>' );
								}
								?>
							<# } #>
						</p></div>
					<# } else if ( ! data.active && data.blockTheme ) { #>
						<div class="notice notice-error notice-alt notice-large"><p>
						<?php
							_e( 'This theme doesn\'t support Customizer.' );
						?>
						<# if ( data.actions.activate ) { #>
							<?php
							printf(
								/* translators: %s: URL to the themes page (also it activates the theme). */
								' ' . __( 'However, you can still <a href="%s">activate this theme</a>, and use the Site Editor to customize it.' ),
								'{{{ data.actions.activate }}}'
							);
							?>
						<# } #>
						</p></div>
					<# } #>

					<p class="theme-description">{{{ data.description }}}</p>

					<# if ( data.tags ) { #>
						<p class="theme-tags"><span><?php _e( 'Tags:' ); ?></span> {{{ data.tags }}}</p>
					<# } #>
				</div>
			</div>

			<div class="theme-actions">
				<# if ( data.active ) { #>
					<button type="button" class="button button-primary customize-theme"><?php _e( 'Customize' ); ?></button>
				<# } else if ( 'installed' === data.type ) { #>
					<div class="theme-inactive-actions">
					<# if ( data.blockTheme ) { #>
						<?php
							/* translators: %s: Theme name. */
							$aria_label = sprintf( _x( 'Activate %s', 'theme' ), '{{ data.name }}' );
						?>
						<# if ( data.compatibleWP && data.compatiblePHP && data.actions.activate ) { #>
							<a href="{{{ data.actions.activate }}}" class="button button-primary activate" aria-label="<?php echo esc_attr( $aria_label ); ?>"><?php _e( 'Activate' ); ?></a>
						<# } #>
					<# } else { #>
						<# if ( data.compatibleWP && data.compatiblePHP ) { #>
							<button type="button" class="button button-primary preview-theme" data-slug="{{ data.id }}"><?php _e( 'Live Preview' ); ?></button>
						<# } else { #>
							<button class="button button-primary disabled"><?php _e( 'Live Preview' ); ?></button>
						<# } #>
					<# } #>
					</div>
					<?php if ( current_user_can( 'delete_themes' ) ) { ?>
						<# if ( data.actions && data.actions['delete'] ) { #>
							<a href="{{{ data.actions['delete'] }}}" data-slug="{{ data.id }}" class="button button-secondary delete-theme"><?php _e( 'Delete' ); ?></a>
						<# } #>
					<?php } ?>
				<# } else { #>
					<# if ( data.compatibleWP && data.compatiblePHP ) { #>
						<button type="button" class="button theme-install" data-slug="{{ data.id }}"><?php _e( 'Install' ); ?></button>
						<button type="button" class="button button-primary theme-install preview" data-slug="{{ data.id }}"><?php _e( 'Install &amp; Preview' ); ?></button>
					<# } else { #>
						<button type="button" class="button disabled"><?php _ex( 'Cannot Install', 'theme' ); ?></button>
						<button type="button" class="button button-primary disabled"><?php _e( 'Install &amp; Preview' ); ?></button>
					<# } #>
				<# } #>
			</div>
		</div>
	</script>
	<?php
}

/**
 * Determines whether a theme is technically active but was paused while
 * loading.
 *
 * For more information on this and similar theme functions, check out
 * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/
 * Conditional Tags} article in the Theme Developer Handbook.
 *
 * @since 5.2.0
 *
 * @global WP_Paused_Extensions_Storage $_paused_themes
 *
 * @param string $theme Path to the theme directory relative to the themes directory.
 * @return bool True, if in the list of paused themes. False, not in the list.
 */
function is_theme_paused( $theme ) {
	if ( ! isset( $GLOBALS['_paused_themes'] ) ) {
		return false;
	}

	if ( get_stylesheet() !== $theme && get_template() !== $theme ) {
		return false;
	}

	return array_key_exists( $theme, $GLOBALS['_paused_themes'] );
}

/**
 * Gets the error that was recorded for a paused theme.
 *
 * @since 5.2.0
 *
 * @global WP_Paused_Extensions_Storage $_paused_themes
 *
 * @param string $theme Path to the theme directory relative to the themes
 *                      directory.
 * @return array|false Array of error information as it was returned by
 *                     `error_get_last()`, or false if none was recorded.
 */
function wp_get_theme_error( $theme ) {
	if ( ! isset( $GLOBALS['_paused_themes'] ) ) {
		return false;
	}

	if ( ! array_key_exists( $theme, $GLOBALS['_paused_themes'] ) ) {
		return false;
	}

	return $GLOBALS['_paused_themes'][ $theme ];
}

/**
 * Tries to resume a single theme.
 *
 * If a redirect was provided and a functions.php file was found, we first ensure that
 * functions.php file does not throw fatal errors anymore.
 *
 * The way it works is by setting the redirection to the error before trying to
 * include the file. If the theme fails, then the redirection will not be overwritten
 * with the success message and the theme will not be resumed.
 *
 * @since 5.2.0
 *
 * @global string $wp_stylesheet_path Path to current theme's stylesheet directory.
 * @global string $wp_template_path   Path to current theme's template directory.
 *
 * @param string $theme    Single theme to resume.
 * @param string $redirect Optional. URL to redirect to. Default empty string.
 * @return bool|WP_Error True on success, false if `$theme` was not paused,
 *                       `WP_Error` on failure.
 */
function resume_theme( $theme, $redirect = '' ) {
	global $wp_stylesheet_path, $wp_template_path;

	list( $extension ) = explode( '/', $theme );

	/*
	 * We'll override this later if the theme could be resumed without
	 * creating a fatal error.
	 */
	if ( ! empty( $redirect ) ) {
		$functions_path = '';
		if ( str_contains( $wp_stylesheet_path, $extension ) ) {
			$functions_path = $wp_stylesheet_path . '/functions.php';
		} elseif ( str_contains( $wp_template_path, $extension ) ) {
			$functions_path = $wp_template_path . '/functions.php';
		}

		if ( ! empty( $functions_path ) ) {
			wp_redirect(
				add_query_arg(
					'_error_nonce',
					wp_create_nonce( 'theme-resume-error_' . $theme ),
					$redirect
				)
			);

			// Load the theme's functions.php to test whether it throws a fatal error.
			ob_start();
			if ( ! defined( 'WP_SANDBOX_SCRAPING' ) ) {
				define( 'WP_SANDBOX_SCRAPING', true );
			}
			include $functions_path;
			ob_clean();
		}
	}

	$result = wp_paused_themes()->delete( $extension );

	if ( ! $result ) {
		return new WP_Error(
			'could_not_resume_theme',
			__( 'Could not resume the theme.' )
		);
	}

	return true;
}

/**
 * Renders an admin notice in case some themes have been paused due to errors.
 *
 * @since 5.2.0
 *
 * @global string                       $pagenow        The filename of the current screen.
 * @global WP_Paused_Extensions_Storage $_paused_themes
 */
function paused_themes_notice() {
	if ( 'themes.php' === $GLOBALS['pagenow'] ) {
		return;
	}

	if ( ! current_user_can( 'resume_themes' ) ) {
		return;
	}

	if ( ! isset( $GLOBALS['_paused_themes'] ) || empty( $GLOBALS['_paused_themes'] ) ) {
		return;
	}

	$message = sprintf(
		'<p><strong>%s</strong><br>%s</p><p><a href="%s">%s</a></p>',
		__( 'One or more themes failed to load properly.' ),
		__( 'You can find more details and make changes on the Themes screen.' ),
		esc_url( admin_url( 'themes.php' ) ),
		__( 'Go to the Themes screen' )
	);
	wp_admin_notice(
		$message,
		array(
			'type'           => 'error',
			'paragraph_wrap' => false,
		)
	);
}
meta-boxes.php000064400000201034151331232430007316 0ustar00<?php
/**
 * WordPress Administration Meta Boxes API.
 *
 * @package WordPress
 * @subpackage Administration
 */

//
// Post-related Meta Boxes.
//

/**
 * Displays post submit form fields.
 *
 * @since 2.7.0
 *
 * @global string $action
 *
 * @param WP_Post $post Current post object.
 * @param array   $args {
 *     Array of arguments for building the post submit meta box.
 *
 *     @type string   $id       Meta box 'id' attribute.
 *     @type string   $title    Meta box title.
 *     @type callable $callback Meta box display callback.
 *     @type array    $args     Extra meta box arguments.
 * }
 */
function post_submit_meta_box( $post, $args = array() ) {
	global $action;

	$post_id          = (int) $post->ID;
	$post_type        = $post->post_type;
	$post_type_object = get_post_type_object( $post_type );
	$can_publish      = current_user_can( $post_type_object->cap->publish_posts );
	?>
<div class="submitbox" id="submitpost">

<div id="minor-publishing">

	<?php // Hidden submit button early on so that the browser chooses the right button when form is submitted with Return key. ?>
	<div style="display:none;">
		<?php submit_button( __( 'Save' ), '', 'save' ); ?>
	</div>

	<div id="minor-publishing-actions">
		<div id="save-action">
			<?php
			if ( ! in_array( $post->post_status, array( 'publish', 'future', 'pending' ), true ) ) {
				$private_style = '';
				if ( 'private' === $post->post_status ) {
					$private_style = 'style="display:none"';
				}
				?>
				<input <?php echo $private_style; ?> type="submit" name="save" id="save-post" value="<?php esc_attr_e( 'Save Draft' ); ?>" class="button" />
				<span class="spinner"></span>
			<?php } elseif ( 'pending' === $post->post_status && $can_publish ) { ?>
				<input type="submit" name="save" id="save-post" value="<?php esc_attr_e( 'Save as Pending' ); ?>" class="button" />
				<span class="spinner"></span>
			<?php } ?>
		</div>

		<?php
		if ( is_post_type_viewable( $post_type_object ) ) :
			?>
			<div id="preview-action">
				<?php
				$preview_link = esc_url( get_preview_post_link( $post ) );
				if ( 'publish' === $post->post_status ) {
					$preview_button_text = __( 'Preview Changes' );
				} else {
					$preview_button_text = __( 'Preview' );
				}

				$preview_button = sprintf(
					'%1$s<span class="screen-reader-text"> %2$s</span>',
					$preview_button_text,
					/* translators: Hidden accessibility text. */
					__( '(opens in a new tab)' )
				);
				?>
				<a class="preview button" href="<?php echo $preview_link; ?>" target="wp-preview-<?php echo $post_id; ?>" id="post-preview"><?php echo $preview_button; ?></a>
				<input type="hidden" name="wp-preview" id="wp-preview" value="" />
			</div>
			<?php
		endif;

		/**
		 * Fires after the Save Draft (or Save as Pending) and Preview (or Preview Changes) buttons
		 * in the Publish meta box.
		 *
		 * @since 4.4.0
		 *
		 * @param WP_Post $post WP_Post object for the current post.
		 */
		do_action( 'post_submitbox_minor_actions', $post );
		?>
		<div class="clear"></div>
	</div>

	<div id="misc-publishing-actions">
		<div class="misc-pub-section misc-pub-post-status">
			<?php _e( 'Status:' ); ?>
			<span id="post-status-display">
				<?php
				switch ( $post->post_status ) {
					case 'private':
						_e( 'Privately Published' );
						break;
					case 'publish':
						_e( 'Published' );
						break;
					case 'future':
						_e( 'Scheduled' );
						break;
					case 'pending':
						_e( 'Pending Review' );
						break;
					case 'draft':
					case 'auto-draft':
						_e( 'Draft' );
						break;
				}
				?>
			</span>

			<?php
			if ( 'publish' === $post->post_status || 'private' === $post->post_status || $can_publish ) {
				$private_style = '';
				if ( 'private' === $post->post_status ) {
					$private_style = 'style="display:none"';
				}
				?>
				<a href="#post_status" <?php echo $private_style; ?> class="edit-post-status hide-if-no-js" role="button"><span aria-hidden="true"><?php _e( 'Edit' ); ?></span> <span class="screen-reader-text">
					<?php
					/* translators: Hidden accessibility text. */
					_e( 'Edit status' );
					?>
				</span></a>

				<div id="post-status-select" class="hide-if-js">
					<input type="hidden" name="hidden_post_status" id="hidden_post_status" value="<?php echo esc_attr( ( 'auto-draft' === $post->post_status ) ? 'draft' : $post->post_status ); ?>" />
					<label for="post_status" class="screen-reader-text">
						<?php
						/* translators: Hidden accessibility text. */
						_e( 'Set status' );
						?>
					</label>
					<select name="post_status" id="post_status">
						<?php if ( 'publish' === $post->post_status ) : ?>
							<option<?php selected( $post->post_status, 'publish' ); ?> value='publish'><?php _e( 'Published' ); ?></option>
						<?php elseif ( 'private' === $post->post_status ) : ?>
							<option<?php selected( $post->post_status, 'private' ); ?> value='publish'><?php _e( 'Privately Published' ); ?></option>
						<?php elseif ( 'future' === $post->post_status ) : ?>
							<option<?php selected( $post->post_status, 'future' ); ?> value='future'><?php _e( 'Scheduled' ); ?></option>
						<?php endif; ?>
							<option<?php selected( $post->post_status, 'pending' ); ?> value='pending'><?php _e( 'Pending Review' ); ?></option>
						<?php if ( 'auto-draft' === $post->post_status ) : ?>
							<option<?php selected( $post->post_status, 'auto-draft' ); ?> value='draft'><?php _e( 'Draft' ); ?></option>
						<?php else : ?>
							<option<?php selected( $post->post_status, 'draft' ); ?> value='draft'><?php _e( 'Draft' ); ?></option>
						<?php endif; ?>
					</select>
					<a href="#post_status" class="save-post-status hide-if-no-js button"><?php _e( 'OK' ); ?></a>
					<a href="#post_status" class="cancel-post-status hide-if-no-js button-cancel"><?php _e( 'Cancel' ); ?></a>
				</div>
				<?php
			}
			?>
		</div>

		<div class="misc-pub-section misc-pub-visibility" id="visibility">
			<?php _e( 'Visibility:' ); ?>
			<span id="post-visibility-display">
				<?php
				if ( 'private' === $post->post_status ) {
					$post->post_password = '';
					$visibility          = 'private';
					$visibility_trans    = __( 'Private' );
				} elseif ( ! empty( $post->post_password ) ) {
					$visibility       = 'password';
					$visibility_trans = __( 'Password protected' );
				} elseif ( 'post' === $post_type && is_sticky( $post_id ) ) {
					$visibility       = 'public';
					$visibility_trans = __( 'Public, Sticky' );
				} else {
					$visibility       = 'public';
					$visibility_trans = __( 'Public' );
				}

				echo esc_html( $visibility_trans );
				?>
			</span>

			<?php if ( $can_publish ) { ?>
				<a href="#visibility" class="edit-visibility hide-if-no-js" role="button"><span aria-hidden="true"><?php _e( 'Edit' ); ?></span> <span class="screen-reader-text">
					<?php
					/* translators: Hidden accessibility text. */
					_e( 'Edit visibility' );
					?>
				</span></a>

				<div id="post-visibility-select" class="hide-if-js">
					<input type="hidden" name="hidden_post_password" id="hidden-post-password" value="<?php echo esc_attr( $post->post_password ); ?>" />
					<?php if ( 'post' === $post_type ) : ?>
						<input type="checkbox" style="display:none" name="hidden_post_sticky" id="hidden-post-sticky" value="sticky" <?php checked( is_sticky( $post_id ) ); ?> />
					<?php endif; ?>

					<input type="hidden" name="hidden_post_visibility" id="hidden-post-visibility" value="<?php echo esc_attr( $visibility ); ?>" />
					<input type="radio" name="visibility" id="visibility-radio-public" value="public" <?php checked( $visibility, 'public' ); ?> /> <label for="visibility-radio-public" class="selectit"><?php _e( 'Public' ); ?></label><br />

					<?php if ( 'post' === $post_type && current_user_can( 'edit_others_posts' ) ) : ?>
						<span id="sticky-span"><input id="sticky" name="sticky" type="checkbox" value="sticky" <?php checked( is_sticky( $post_id ) ); ?> /> <label for="sticky" class="selectit"><?php _e( 'Stick this post to the front page' ); ?></label><br /></span>
					<?php endif; ?>

					<input type="radio" name="visibility" id="visibility-radio-password" value="password" <?php checked( $visibility, 'password' ); ?> /> <label for="visibility-radio-password" class="selectit"><?php _e( 'Password protected' ); ?></label><br />
					<span id="password-span"><label for="post_password"><?php _e( 'Password:' ); ?></label> <input type="text" name="post_password" id="post_password" value="<?php echo esc_attr( $post->post_password ); ?>"  maxlength="255" /><br /></span>

					<input type="radio" name="visibility" id="visibility-radio-private" value="private" <?php checked( $visibility, 'private' ); ?> /> <label for="visibility-radio-private" class="selectit"><?php _e( 'Private' ); ?></label><br />

					<p>
						<a href="#visibility" class="save-post-visibility hide-if-no-js button"><?php _e( 'OK' ); ?></a>
						<a href="#visibility" class="cancel-post-visibility hide-if-no-js button-cancel"><?php _e( 'Cancel' ); ?></a>
					</p>
				</div>
			<?php } ?>
		</div>

		<?php
		/* translators: Publish box date string. 1: Date, 2: Time. See https://www.php.net/manual/datetime.format.php */
		$date_string = __( '%1$s at %2$s' );
		/* translators: Publish box date format, see https://www.php.net/manual/datetime.format.php */
		$date_format = _x( 'M j, Y', 'publish box date format' );
		/* translators: Publish box time format, see https://www.php.net/manual/datetime.format.php */
		$time_format = _x( 'H:i', 'publish box time format' );

		if ( 0 !== $post_id ) {
			if ( 'future' === $post->post_status ) { // Scheduled for publishing at a future date.
				/* translators: Post date information. %s: Date on which the post is currently scheduled to be published. */
				$stamp = __( 'Scheduled for: %s' );
			} elseif ( 'publish' === $post->post_status || 'private' === $post->post_status ) { // Already published.
				/* translators: Post date information. %s: Date on which the post was published. */
				$stamp = __( 'Published on: %s' );
			} elseif ( '0000-00-00 00:00:00' === $post->post_date_gmt ) { // Draft, 1 or more saves, no date specified.
				$stamp = __( 'Publish <b>immediately</b>' );
			} elseif ( time() < strtotime( $post->post_date_gmt . ' +0000' ) ) { // Draft, 1 or more saves, future date specified.
				/* translators: Post date information. %s: Date on which the post is to be published. */
				$stamp = __( 'Schedule for: %s' );
			} else { // Draft, 1 or more saves, date specified.
				/* translators: Post date information. %s: Date on which the post is to be published. */
				$stamp = __( 'Publish on: %s' );
			}
			$date = sprintf(
				$date_string,
				date_i18n( $date_format, strtotime( $post->post_date ) ),
				date_i18n( $time_format, strtotime( $post->post_date ) )
			);
		} else { // Draft (no saves, and thus no date specified).
			$stamp = __( 'Publish <b>immediately</b>' );
			$date  = sprintf(
				$date_string,
				date_i18n( $date_format, strtotime( current_time( 'mysql' ) ) ),
				date_i18n( $time_format, strtotime( current_time( 'mysql' ) ) )
			);
		}

		if ( ! empty( $args['args']['revisions_count'] ) ) :
			?>
			<div class="misc-pub-section misc-pub-revisions">
				<?php
				/* translators: Post revisions heading. %s: The number of available revisions. */
				printf( __( 'Revisions: %s' ), '<b>' . number_format_i18n( $args['args']['revisions_count'] ) . '</b>' );
				?>
				<a class="hide-if-no-js" href="<?php echo esc_url( get_edit_post_link( $args['args']['revision_id'] ) ); ?>"><span aria-hidden="true"><?php _ex( 'Browse', 'revisions' ); ?></span> <span class="screen-reader-text">
					<?php
					/* translators: Hidden accessibility text. */
					_e( 'Browse revisions' );
					?>
				</span></a>
			</div>
			<?php
		endif;

		if ( $can_publish ) : // Contributors don't get to choose the date of publish.
			?>
			<div class="misc-pub-section curtime misc-pub-curtime">
				<span id="timestamp">
					<?php printf( $stamp, '<b>' . $date . '</b>' ); ?>
				</span>
				<a href="#edit_timestamp" class="edit-timestamp hide-if-no-js" role="button">
					<span aria-hidden="true"><?php _e( 'Edit' ); ?></span>
					<span class="screen-reader-text">
						<?php
						/* translators: Hidden accessibility text. */
						_e( 'Edit date and time' );
						?>
					</span>
				</a>
				<fieldset id="timestampdiv" class="hide-if-js">
					<legend class="screen-reader-text">
						<?php
						/* translators: Hidden accessibility text. */
						_e( 'Date and time' );
						?>
					</legend>
					<?php touch_time( ( 'edit' === $action ), 1 ); ?>
				</fieldset>
			</div>
			<?php
		endif;

		if ( 'draft' === $post->post_status && get_post_meta( $post_id, '_customize_changeset_uuid', true ) ) :
			$message = sprintf(
				/* translators: %s: URL to the Customizer. */
				__( 'This draft comes from your <a href="%s">unpublished customization changes</a>. You can edit, but there is no need to publish now. It will be published automatically with those changes.' ),
				esc_url(
					add_query_arg(
						'changeset_uuid',
						rawurlencode( get_post_meta( $post_id, '_customize_changeset_uuid', true ) ),
						admin_url( 'customize.php' )
					)
				)
			);
			wp_admin_notice(
				$message,
				array(
					'type'               => 'info',
					'additional_classes' => array( 'notice-alt', 'inline' ),
				)
			);
		endif;

		/**
		 * Fires after the post time/date setting in the Publish meta box.
		 *
		 * @since 2.9.0
		 * @since 4.4.0 Added the `$post` parameter.
		 *
		 * @param WP_Post $post WP_Post object for the current post.
		 */
		do_action( 'post_submitbox_misc_actions', $post );
		?>
	</div>
	<div class="clear"></div>
</div>

<div id="major-publishing-actions">
	<?php
	/**
	 * Fires at the beginning of the publishing actions section of the Publish meta box.
	 *
	 * @since 2.7.0
	 * @since 4.9.0 Added the `$post` parameter.
	 *
	 * @param WP_Post|null $post WP_Post object for the current post on Edit Post screen,
	 *                           null on Edit Link screen.
	 */
	do_action( 'post_submitbox_start', $post );
	?>
	<div id="delete-action">
		<?php
		if ( current_user_can( 'delete_post', $post_id ) ) {
			if ( ! EMPTY_TRASH_DAYS ) {
				$delete_text = __( 'Delete permanently' );
			} else {
				$delete_text = __( 'Move to Trash' );
			}
			?>
			<a class="submitdelete deletion" href="<?php echo get_delete_post_link( $post_id ); ?>"><?php echo $delete_text; ?></a>
			<?php
		}
		?>
	</div>

	<div id="publishing-action">
		<span class="spinner"></span>
		<?php
		if ( ! in_array( $post->post_status, array( 'publish', 'future', 'private' ), true ) || 0 === $post_id ) {
			if ( $can_publish ) :
				if ( ! empty( $post->post_date_gmt ) && time() < strtotime( $post->post_date_gmt . ' +0000' ) ) :
					?>
					<input name="original_publish" type="hidden" id="original_publish" value="<?php echo esc_attr_x( 'Schedule', 'post action/button label' ); ?>" />
					<?php submit_button( _x( 'Schedule', 'post action/button label' ), 'primary large', 'publish', false ); ?>
					<?php
				else :
					?>
					<input name="original_publish" type="hidden" id="original_publish" value="<?php esc_attr_e( 'Publish' ); ?>" />
					<?php submit_button( __( 'Publish' ), 'primary large', 'publish', false ); ?>
					<?php
				endif;
			else :
				?>
				<input name="original_publish" type="hidden" id="original_publish" value="<?php esc_attr_e( 'Submit for Review' ); ?>" />
				<?php submit_button( __( 'Submit for Review' ), 'primary large', 'publish', false ); ?>
				<?php
			endif;
		} else {
			?>
			<input name="original_publish" type="hidden" id="original_publish" value="<?php esc_attr_e( 'Update' ); ?>" />
			<?php submit_button( __( 'Update' ), 'primary large', 'save', false, array( 'id' => 'publish' ) ); ?>
			<?php
		}
		?>
	</div>
	<div class="clear"></div>
</div>

</div>
	<?php
}

/**
 * Displays attachment submit form fields.
 *
 * @since 3.5.0
 *
 * @param WP_Post $post Current post object.
 */
function attachment_submit_meta_box( $post ) {
	?>
<div class="submitbox" id="submitpost">

<div id="minor-publishing">

	<?php // Hidden submit button early on so that the browser chooses the right button when form is submitted with Return key. ?>
<div style="display:none;">
	<?php submit_button( __( 'Save' ), '', 'save' ); ?>
</div>


<div id="misc-publishing-actions">
	<div class="misc-pub-section curtime misc-pub-curtime">
		<span id="timestamp">
			<?php
			$uploaded_on = sprintf(
				/* translators: Publish box date string. 1: Date, 2: Time. */
				__( '%1$s at %2$s' ),
				/* translators: Publish box date format, see https://www.php.net/manual/datetime.format.php */
				date_i18n( _x( 'M j, Y', 'publish box date format' ), strtotime( $post->post_date ) ),
				/* translators: Publish box time format, see https://www.php.net/manual/datetime.format.php */
				date_i18n( _x( 'H:i', 'publish box time format' ), strtotime( $post->post_date ) )
			);
			/* translators: Attachment information. %s: Date the attachment was uploaded. */
			printf( __( 'Uploaded on: %s' ), '<b>' . $uploaded_on . '</b>' );
			?>
		</span>
	</div><!-- .misc-pub-section -->

	<?php
	/**
	 * Fires after the 'Uploaded on' section of the Save meta box
	 * in the attachment editing screen.
	 *
	 * @since 3.5.0
	 * @since 4.9.0 Added the `$post` parameter.
	 *
	 * @param WP_Post $post WP_Post object for the current attachment.
	 */
	do_action( 'attachment_submitbox_misc_actions', $post );
	?>
</div><!-- #misc-publishing-actions -->
<div class="clear"></div>
</div><!-- #minor-publishing -->

<div id="major-publishing-actions">
	<div id="delete-action">
	<?php
	if ( current_user_can( 'delete_post', $post->ID ) ) {
		if ( EMPTY_TRASH_DAYS && MEDIA_TRASH ) {
			printf(
				'<a class="submitdelete deletion" href="%1$s">%2$s</a>',
				get_delete_post_link( $post->ID ),
				__( 'Move to Trash' )
			);
		} else {
			$show_confirmation = ! MEDIA_TRASH ? " onclick='return showNotice.warn();'" : '';

			printf(
				'<a class="submitdelete deletion"%1$s href="%2$s">%3$s</a>',
				$show_confirmation,
				get_delete_post_link( $post->ID, '', true ),
				__( 'Delete permanently' )
			);
		}
	}
	?>
	</div>

	<div id="publishing-action">
		<span class="spinner"></span>
		<input name="original_publish" type="hidden" id="original_publish" value="<?php esc_attr_e( 'Update' ); ?>" />
		<input name="save" type="submit" class="button button-primary button-large" id="publish" value="<?php esc_attr_e( 'Update' ); ?>" />
	</div>
	<div class="clear"></div>
</div><!-- #major-publishing-actions -->

</div>

	<?php
}

/**
 * Displays post format form elements.
 *
 * @since 3.1.0
 *
 * @param WP_Post $post Current post object.
 * @param array   $box {
 *     Post formats meta box arguments.
 *
 *     @type string   $id       Meta box 'id' attribute.
 *     @type string   $title    Meta box title.
 *     @type callable $callback Meta box display callback.
 *     @type array    $args     Extra meta box arguments.
 * }
 */
function post_format_meta_box( $post, $box ) {
	if ( current_theme_supports( 'post-formats' ) && post_type_supports( $post->post_type, 'post-formats' ) ) :
		$post_formats = get_theme_support( 'post-formats' );

		if ( is_array( $post_formats[0] ) ) :
			$post_format = get_post_format( $post->ID );
			if ( ! $post_format ) {
				$post_format = '0';
			}
			// Add in the current one if it isn't there yet, in case the active theme doesn't support it.
			if ( $post_format && ! in_array( $post_format, $post_formats[0], true ) ) {
				$post_formats[0][] = $post_format;
			}
			?>
		<div id="post-formats-select">
		<fieldset>
			<legend class="screen-reader-text">
				<?php
				/* translators: Hidden accessibility text. */
				_e( 'Post Formats' );
				?>
			</legend>
			<input type="radio" name="post_format" class="post-format" id="post-format-0" value="0" <?php checked( $post_format, '0' ); ?> /> <label for="post-format-0" class="post-format-icon post-format-standard"><?php echo get_post_format_string( 'standard' ); ?></label>
			<?php foreach ( $post_formats[0] as $format ) : ?>
			<br /><input type="radio" name="post_format" class="post-format" id="post-format-<?php echo esc_attr( $format ); ?>" value="<?php echo esc_attr( $format ); ?>" <?php checked( $post_format, $format ); ?> /> <label for="post-format-<?php echo esc_attr( $format ); ?>" class="post-format-icon post-format-<?php echo esc_attr( $format ); ?>"><?php echo esc_html( get_post_format_string( $format ) ); ?></label>
			<?php endforeach; ?>
		</fieldset>
	</div>
			<?php
	endif;
endif;
}

/**
 * Displays post tags form fields.
 *
 * @since 2.6.0
 *
 * @todo Create taxonomy-agnostic wrapper for this.
 *
 * @param WP_Post $post Current post object.
 * @param array   $box {
 *     Tags meta box arguments.
 *
 *     @type string   $id       Meta box 'id' attribute.
 *     @type string   $title    Meta box title.
 *     @type callable $callback Meta box display callback.
 *     @type array    $args {
 *         Extra meta box arguments.
 *
 *         @type string $taxonomy Taxonomy. Default 'post_tag'.
 *     }
 * }
 */
function post_tags_meta_box( $post, $box ) {
	$defaults = array( 'taxonomy' => 'post_tag' );
	if ( ! isset( $box['args'] ) || ! is_array( $box['args'] ) ) {
		$args = array();
	} else {
		$args = $box['args'];
	}
	$parsed_args           = wp_parse_args( $args, $defaults );
	$tax_name              = esc_attr( $parsed_args['taxonomy'] );
	$taxonomy              = get_taxonomy( $parsed_args['taxonomy'] );
	$user_can_assign_terms = current_user_can( $taxonomy->cap->assign_terms );
	$comma                 = _x( ',', 'tag delimiter' );
	$terms_to_edit         = get_terms_to_edit( $post->ID, $tax_name );
	if ( ! is_string( $terms_to_edit ) ) {
		$terms_to_edit = '';
	}
	?>
<div class="tagsdiv" id="<?php echo $tax_name; ?>">
	<div class="jaxtag">
	<div class="nojs-tags hide-if-js">
		<label for="tax-input-<?php echo $tax_name; ?>"><?php echo $taxonomy->labels->add_or_remove_items; ?></label>
		<p><textarea name="<?php echo "tax_input[$tax_name]"; ?>" rows="3" cols="20" class="the-tags" id="tax-input-<?php echo $tax_name; ?>" <?php disabled( ! $user_can_assign_terms ); ?> aria-describedby="new-tag-<?php echo $tax_name; ?>-desc"><?php echo str_replace( ',', $comma . ' ', $terms_to_edit ); // textarea_escaped by esc_attr() ?></textarea></p>
	</div>
	<?php if ( $user_can_assign_terms ) : ?>
	<div class="ajaxtag hide-if-no-js">
		<label class="screen-reader-text" for="new-tag-<?php echo $tax_name; ?>"><?php echo $taxonomy->labels->add_new_item; ?></label>
		<input data-wp-taxonomy="<?php echo $tax_name; ?>" type="text" id="new-tag-<?php echo $tax_name; ?>" name="newtag[<?php echo $tax_name; ?>]" class="newtag form-input-tip" size="16" autocomplete="off" aria-describedby="new-tag-<?php echo $tax_name; ?>-desc" value="" />
		<input type="button" class="button tagadd" value="<?php esc_attr_e( 'Add' ); ?>" />
	</div>
	<p class="howto" id="new-tag-<?php echo $tax_name; ?>-desc"><?php echo $taxonomy->labels->separate_items_with_commas; ?></p>
	<?php elseif ( empty( $terms_to_edit ) ) : ?>
		<p><?php echo $taxonomy->labels->no_terms; ?></p>
	<?php endif; ?>
	</div>
	<ul class="tagchecklist" role="list"></ul>
</div>
	<?php if ( $user_can_assign_terms ) : ?>
<p class="hide-if-no-js"><button type="button" class="button-link tagcloud-link" id="link-<?php echo $tax_name; ?>" aria-expanded="false"><?php echo $taxonomy->labels->choose_from_most_used; ?></button></p>
<?php endif; ?>
	<?php
}

/**
 * Displays post categories form fields.
 *
 * @since 2.6.0
 *
 * @todo Create taxonomy-agnostic wrapper for this.
 *
 * @param WP_Post $post Current post object.
 * @param array   $box {
 *     Categories meta box arguments.
 *
 *     @type string   $id       Meta box 'id' attribute.
 *     @type string   $title    Meta box title.
 *     @type callable $callback Meta box display callback.
 *     @type array    $args {
 *         Extra meta box arguments.
 *
 *         @type string $taxonomy Taxonomy. Default 'category'.
 *     }
 * }
 */
function post_categories_meta_box( $post, $box ) {
	$defaults = array( 'taxonomy' => 'category' );
	if ( ! isset( $box['args'] ) || ! is_array( $box['args'] ) ) {
		$args = array();
	} else {
		$args = $box['args'];
	}
	$parsed_args = wp_parse_args( $args, $defaults );
	$tax_name    = esc_attr( $parsed_args['taxonomy'] );
	$taxonomy    = get_taxonomy( $parsed_args['taxonomy'] );
	?>
	<div id="taxonomy-<?php echo $tax_name; ?>" class="categorydiv">
		<ul id="<?php echo $tax_name; ?>-tabs" class="category-tabs">
			<li class="tabs"><a href="#<?php echo $tax_name; ?>-all"><?php echo $taxonomy->labels->all_items; ?></a></li>
			<li class="hide-if-no-js"><a href="#<?php echo $tax_name; ?>-pop"><?php echo esc_html( $taxonomy->labels->most_used ); ?></a></li>
		</ul>

		<div id="<?php echo $tax_name; ?>-pop" class="tabs-panel" style="display: none;">
			<ul id="<?php echo $tax_name; ?>checklist-pop" class="categorychecklist form-no-clear" >
				<?php $popular_ids = wp_popular_terms_checklist( $tax_name ); ?>
			</ul>
		</div>

		<div id="<?php echo $tax_name; ?>-all" class="tabs-panel">
			<?php
			$name = ( 'category' === $tax_name ) ? 'post_category' : 'tax_input[' . $tax_name . ']';
			// Allows for an empty term set to be sent. 0 is an invalid term ID and will be ignored by empty() checks.
			echo "<input type='hidden' name='{$name}[]' value='0' />";
			?>
			<ul id="<?php echo $tax_name; ?>checklist" data-wp-lists="list:<?php echo $tax_name; ?>" class="categorychecklist form-no-clear">
				<?php
				wp_terms_checklist(
					$post->ID,
					array(
						'taxonomy'     => $tax_name,
						'popular_cats' => $popular_ids,
					)
				);
				?>
			</ul>
		</div>
	<?php if ( current_user_can( $taxonomy->cap->edit_terms ) ) : ?>
			<div id="<?php echo $tax_name; ?>-adder" class="wp-hidden-children">
				<a id="<?php echo $tax_name; ?>-add-toggle" href="#<?php echo $tax_name; ?>-add" class="hide-if-no-js taxonomy-add-new">
					<?php
						/* translators: %s: Add New taxonomy label. */
						printf( __( '+ %s' ), $taxonomy->labels->add_new_item );
					?>
				</a>
				<p id="<?php echo $tax_name; ?>-add" class="category-add wp-hidden-child">
					<label class="screen-reader-text" for="new<?php echo $tax_name; ?>"><?php echo $taxonomy->labels->add_new_item; ?></label>
					<input type="text" name="new<?php echo $tax_name; ?>" id="new<?php echo $tax_name; ?>" class="form-required form-input-tip" value="<?php echo esc_attr( $taxonomy->labels->new_item_name ); ?>" aria-required="true" />
					<label class="screen-reader-text" for="new<?php echo $tax_name; ?>_parent">
						<?php echo $taxonomy->labels->parent_item_colon; ?>
					</label>
					<?php
					$parent_dropdown_args = array(
						'taxonomy'         => $tax_name,
						'hide_empty'       => 0,
						'name'             => 'new' . $tax_name . '_parent',
						'orderby'          => 'name',
						'hierarchical'     => 1,
						'show_option_none' => '&mdash; ' . $taxonomy->labels->parent_item . ' &mdash;',
					);

					/**
					 * Filters the arguments for the taxonomy parent dropdown on the Post Edit page.
					 *
					 * @since 4.4.0
					 *
					 * @param array $parent_dropdown_args {
					 *     Optional. Array of arguments to generate parent dropdown.
					 *
					 *     @type string   $taxonomy         Name of the taxonomy to retrieve.
					 *     @type bool     $hide_if_empty    True to skip generating markup if no
					 *                                      categories are found. Default 0.
					 *     @type string   $name             Value for the 'name' attribute
					 *                                      of the select element.
					 *                                      Default "new{$tax_name}_parent".
					 *     @type string   $orderby          Which column to use for ordering
					 *                                      terms. Default 'name'.
					 *     @type bool|int $hierarchical     Whether to traverse the taxonomy
					 *                                      hierarchy. Default 1.
					 *     @type string   $show_option_none Text to display for the "none" option.
					 *                                      Default "&mdash; {$parent} &mdash;",
					 *                                      where `$parent` is 'parent_item'
					 *                                      taxonomy label.
					 * }
					 */
					$parent_dropdown_args = apply_filters( 'post_edit_category_parent_dropdown_args', $parent_dropdown_args );

					wp_dropdown_categories( $parent_dropdown_args );
					?>
					<input type="button" id="<?php echo $tax_name; ?>-add-submit" data-wp-lists="add:<?php echo $tax_name; ?>checklist:<?php echo $tax_name; ?>-add" class="button category-add-submit" value="<?php echo esc_attr( $taxonomy->labels->add_new_item ); ?>" />
					<?php wp_nonce_field( 'add-' . $tax_name, '_ajax_nonce-add-' . $tax_name, false ); ?>
					<span id="<?php echo $tax_name; ?>-ajax-response"></span>
				</p>
			</div>
		<?php endif; ?>
	</div>
	<?php
}

/**
 * Displays post excerpt form fields.
 *
 * @since 2.6.0
 *
 * @param WP_Post $post Current post object.
 */
function post_excerpt_meta_box( $post ) {
	?>
<label class="screen-reader-text" for="excerpt">
	<?php
	/* translators: Hidden accessibility text. */
	_e( 'Excerpt' );
	?>
</label><textarea rows="1" cols="40" name="excerpt" id="excerpt"><?php echo $post->post_excerpt; // textarea_escaped ?></textarea>
<p>
	<?php
	printf(
		/* translators: %s: Documentation URL. */
		__( 'Excerpts are optional hand-crafted summaries of your content that can be used in your theme. <a href="%s">Learn more about manual excerpts</a>.' ),
		__( 'https://wordpress.org/documentation/article/what-is-an-excerpt-classic-editor/' )
	);
	?>
</p>
	<?php
}

/**
 * Displays trackback links form fields.
 *
 * @since 2.6.0
 *
 * @param WP_Post $post Current post object.
 */
function post_trackback_meta_box( $post ) {
	$form_trackback = '<input type="text" name="trackback_url" id="trackback_url" class="code" value="' .
		esc_attr( str_replace( "\n", ' ', $post->to_ping ) ) . '" aria-describedby="trackback-url-desc" />';

	if ( '' !== $post->pinged ) {
		$pings          = '<p>' . __( 'Already pinged:' ) . '</p><ul>';
		$already_pinged = explode( "\n", trim( $post->pinged ) );
		foreach ( $already_pinged as $pinged_url ) {
			$pings .= "\n\t<li>" . esc_html( $pinged_url ) . '</li>';
		}
		$pings .= '</ul>';
	}

	?>
<p>
	<label for="trackback_url"><?php _e( 'Send trackbacks to:' ); ?></label>
	<?php echo $form_trackback; ?>
</p>
<p id="trackback-url-desc" class="howto"><?php _e( 'Separate multiple URLs with spaces' ); ?></p>
<p>
	<?php
	printf(
		/* translators: %s: Documentation URL. */
		__( 'Trackbacks are a way to notify legacy blog systems that you&#8217;ve linked to them. If you link other WordPress sites, they&#8217;ll be notified automatically using <a href="%s">pingbacks</a>, no other action necessary.' ),
		__( 'https://wordpress.org/documentation/article/introduction-to-blogging/#comments' )
	);
	?>
</p>
	<?php
	if ( ! empty( $pings ) ) {
		echo $pings;
	}
}

/**
 * Displays custom fields form fields.
 *
 * @since 2.6.0
 *
 * @param WP_Post $post Current post object.
 */
function post_custom_meta_box( $post ) {
	?>
<div id="postcustomstuff">
<div id="ajax-response"></div>
	<?php
	$metadata = has_meta( $post->ID );
	foreach ( $metadata as $key => $value ) {
		if ( is_protected_meta( $metadata[ $key ]['meta_key'], 'post' ) || ! current_user_can( 'edit_post_meta', $post->ID, $metadata[ $key ]['meta_key'] ) ) {
			unset( $metadata[ $key ] );
		}
	}
	list_meta( $metadata );
	meta_form( $post );
	?>
</div>
<p>
	<?php
	printf(
		/* translators: %s: Documentation URL. */
		__( 'Custom fields can be used to add extra metadata to a post that you can <a href="%s">use in your theme</a>.' ),
		__( 'https://wordpress.org/documentation/article/assign-custom-fields/' )
	);
	?>
</p>
	<?php
}

/**
 * Displays comments status form fields.
 *
 * @since 2.6.0
 *
 * @param WP_Post $post Current post object.
 */
function post_comment_status_meta_box( $post ) {
	?>
<input name="advanced_view" type="hidden" value="1" />
<p class="meta-options">
	<label for="comment_status" class="selectit"><input name="comment_status" type="checkbox" id="comment_status" value="open" <?php checked( $post->comment_status, 'open' ); ?> /> <?php _e( 'Allow comments' ); ?></label><br />
	<label for="ping_status" class="selectit"><input name="ping_status" type="checkbox" id="ping_status" value="open" <?php checked( $post->ping_status, 'open' ); ?> />
		<?php
		printf(
			/* translators: %s: Documentation URL. */
			__( 'Allow <a href="%s">trackbacks and pingbacks</a>' ),
			__( 'https://wordpress.org/documentation/article/introduction-to-blogging/#managing-comments' )
		);
		?>
	</label>
	<?php
	/**
	 * Fires at the end of the Discussion meta box on the post editing screen.
	 *
	 * @since 3.1.0
	 *
	 * @param WP_Post $post WP_Post object for the current post.
	 */
	do_action( 'post_comment_status_meta_box-options', $post ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
	?>
</p>
	<?php
}

/**
 * Displays comments for post table header
 *
 * @since 3.0.0
 *
 * @param array $result Table header rows.
 * @return array
 */
function post_comment_meta_box_thead( $result ) {
	unset( $result['cb'], $result['response'] );
	return $result;
}

/**
 * Displays comments for post.
 *
 * @since 2.8.0
 *
 * @param WP_Post $post Current post object.
 */
function post_comment_meta_box( $post ) {
	wp_nonce_field( 'get-comments', 'add_comment_nonce', false );
	?>
	<p class="hide-if-no-js" id="add-new-comment"><button type="button" class="button" onclick="window.commentReply && commentReply.addcomment(<?php echo $post->ID; ?>);"><?php _e( 'Add Comment' ); ?></button></p>
	<?php

	$total         = get_comments(
		array(
			'post_id' => $post->ID,
			'count'   => true,
			'orderby' => 'none',
		)
	);
	$wp_list_table = _get_list_table( 'WP_Post_Comments_List_Table' );
	$wp_list_table->display( true );

	if ( 1 > $total ) {
		echo '<p id="no-comments">' . __( 'No comments yet.' ) . '</p>';
	} else {
		$hidden = get_hidden_meta_boxes( get_current_screen() );
		if ( ! in_array( 'commentsdiv', $hidden, true ) ) {
			?>
			<script type="text/javascript">jQuery(function(){commentsBox.get(<?php echo $total; ?>, 10);});</script>
			<?php
		}

		?>
		<p class="hide-if-no-js" id="show-comments"><a href="#commentstatusdiv" onclick="commentsBox.load(<?php echo $total; ?>);return false;"><?php _e( 'Show comments' ); ?></a> <span class="spinner"></span></p>
		<?php
	}

	wp_comment_trashnotice();
}

/**
 * Displays slug form fields.
 *
 * @since 2.6.0
 *
 * @param WP_Post $post Current post object.
 */
function post_slug_meta_box( $post ) {
	/** This filter is documented in wp-admin/edit-tag-form.php */
	$editable_slug = apply_filters( 'editable_slug', $post->post_name, $post );
	?>
<label class="screen-reader-text" for="post_name">
	<?php
	/* translators: Hidden accessibility text. */
	_e( 'Slug' );
	?>
</label><input name="post_name" type="text" class="large-text" id="post_name" value="<?php echo esc_attr( $editable_slug ); ?>" />
	<?php
}

/**
 * Displays form field with list of authors.
 *
 * @since 2.6.0
 *
 * @global int $user_ID
 *
 * @param WP_Post $post Current post object.
 */
function post_author_meta_box( $post ) {
	global $user_ID;

	$post_type_object = get_post_type_object( $post->post_type );
	?>
<label class="screen-reader-text" for="post_author_override">
	<?php
	/* translators: Hidden accessibility text. */
	_e( 'Author' );
	?>
</label>
	<?php
	wp_dropdown_users(
		array(
			'capability'       => array( $post_type_object->cap->edit_posts ),
			'name'             => 'post_author_override',
			'selected'         => empty( $post->ID ) ? $user_ID : $post->post_author,
			'include_selected' => true,
			'show'             => 'display_name_with_login',
		)
	);
}

/**
 * Displays list of revisions.
 *
 * @since 2.6.0
 *
 * @param WP_Post $post Current post object.
 */
function post_revisions_meta_box( $post ) {
	wp_list_post_revisions( $post );
}

//
// Page-related Meta Boxes.
//

/**
 * Displays page attributes form fields.
 *
 * @since 2.7.0
 *
 * @param WP_Post $post Current post object.
 */
function page_attributes_meta_box( $post ) {
	if ( is_post_type_hierarchical( $post->post_type ) ) :
		$dropdown_args = array(
			'post_type'        => $post->post_type,
			'exclude_tree'     => $post->ID,
			'selected'         => $post->post_parent,
			'name'             => 'parent_id',
			'show_option_none' => __( '(no parent)' ),
			'sort_column'      => 'menu_order, post_title',
			'echo'             => 0,
		);

		/**
		 * Filters the arguments used to generate a Pages drop-down element.
		 *
		 * @since 3.3.0
		 *
		 * @see wp_dropdown_pages()
		 *
		 * @param array   $dropdown_args Array of arguments used to generate the pages drop-down.
		 * @param WP_Post $post          The current post.
		 */
		$dropdown_args = apply_filters( 'page_attributes_dropdown_pages_args', $dropdown_args, $post );
		$pages         = wp_dropdown_pages( $dropdown_args );
		if ( ! empty( $pages ) ) :
			?>
<p class="post-attributes-label-wrapper parent-id-label-wrapper"><label class="post-attributes-label" for="parent_id"><?php _e( 'Parent' ); ?></label></p>
			<?php echo $pages; ?>
			<?php
		endif; // End empty pages check.
	endif;  // End hierarchical check.

	if ( count( get_page_templates( $post ) ) > 0 && (int) get_option( 'page_for_posts' ) !== $post->ID ) :
		$template = ! empty( $post->page_template ) ? $post->page_template : false;
		?>
<p class="post-attributes-label-wrapper page-template-label-wrapper"><label class="post-attributes-label" for="page_template"><?php _e( 'Template' ); ?></label>
		<?php
		/**
		 * Fires immediately after the label inside the 'Template' section
		 * of the 'Page Attributes' meta box.
		 *
		 * @since 4.4.0
		 *
		 * @param string|false $template The template used for the current post.
		 * @param WP_Post      $post     The current post.
		 */
		do_action( 'page_attributes_meta_box_template', $template, $post );
		?>
</p>
<select name="page_template" id="page_template">
		<?php
		/**
		 * Filters the title of the default page template displayed in the drop-down.
		 *
		 * @since 4.1.0
		 *
		 * @param string $label   The display value for the default page template title.
		 * @param string $context Where the option label is displayed. Possible values
		 *                        include 'meta-box' or 'quick-edit'.
		 */
		$default_title = apply_filters( 'default_page_template_title', __( 'Default template' ), 'meta-box' );
		?>
<option value="default"><?php echo esc_html( $default_title ); ?></option>
		<?php page_template_dropdown( $template, $post->post_type ); ?>
</select>
<?php endif; ?>
	<?php if ( post_type_supports( $post->post_type, 'page-attributes' ) ) : ?>
<p class="post-attributes-label-wrapper menu-order-label-wrapper"><label class="post-attributes-label" for="menu_order"><?php _e( 'Order' ); ?></label></p>
<input name="menu_order" type="text" size="4" id="menu_order" value="<?php echo esc_attr( $post->menu_order ); ?>" />
		<?php
		/**
		 * Fires before the help hint text in the 'Page Attributes' meta box.
		 *
		 * @since 4.9.0
		 *
		 * @param WP_Post $post The current post.
		 */
		do_action( 'page_attributes_misc_attributes', $post );
		?>
		<?php if ( 'page' === $post->post_type && get_current_screen()->get_help_tabs() ) : ?>
<p class="post-attributes-help-text"><?php _e( 'Need help? Use the Help tab above the screen title.' ); ?></p>
			<?php
	endif;
	endif;
}

//
// Link-related Meta Boxes.
//

/**
 * Displays link create form fields.
 *
 * @since 2.7.0
 *
 * @param object $link Current link object.
 */
function link_submit_meta_box( $link ) {
	?>
<div class="submitbox" id="submitlink">

<div id="minor-publishing">

	<?php // Hidden submit button early on so that the browser chooses the right button when form is submitted with Return key. ?>
<div style="display:none;">
	<?php submit_button( __( 'Save' ), '', 'save', false ); ?>
</div>

<div id="minor-publishing-actions">
<div id="preview-action">
	<?php if ( ! empty( $link->link_id ) ) { ?>
	<a class="preview button" href="<?php echo $link->link_url; ?>" target="_blank"><?php _e( 'Visit Link' ); ?></a>
<?php } ?>
</div>
<div class="clear"></div>
</div>

<div id="misc-publishing-actions">
<div class="misc-pub-section misc-pub-private">
	<label for="link_private" class="selectit"><input id="link_private" name="link_visible" type="checkbox" value="N" <?php checked( $link->link_visible, 'N' ); ?> /> <?php _e( 'Keep this link private' ); ?></label>
</div>
</div>

</div>

<div id="major-publishing-actions">
	<?php
	/** This action is documented in wp-admin/includes/meta-boxes.php */
	do_action( 'post_submitbox_start', null );
	?>
<div id="delete-action">
	<?php
	if ( ! empty( $_GET['action'] ) && 'edit' === $_GET['action'] && current_user_can( 'manage_links' ) ) {
		printf(
			'<a class="submitdelete deletion" href="%s" onclick="return confirm( \'%s\' );">%s</a>',
			wp_nonce_url( "link.php?action=delete&amp;link_id=$link->link_id", 'delete-bookmark_' . $link->link_id ),
			/* translators: %s: Link name. */
			esc_js( sprintf( __( "You are about to delete this link '%s'\n  'Cancel' to stop, 'OK' to delete." ), $link->link_name ) ),
			__( 'Delete' )
		);
	}
	?>
</div>

<div id="publishing-action">
	<?php if ( ! empty( $link->link_id ) ) { ?>
	<input name="save" type="submit" class="button button-primary button-large" id="publish" value="<?php esc_attr_e( 'Update Link' ); ?>" />
<?php } else { ?>
	<input name="save" type="submit" class="button button-primary button-large" id="publish" value="<?php esc_attr_e( 'Add Link' ); ?>" />
<?php } ?>
</div>
<div class="clear"></div>
</div>
	<?php
	/**
	 * Fires at the end of the Publish box in the Link editing screen.
	 *
	 * @since 2.5.0
	 */
	do_action( 'submitlink_box' );
	?>
<div class="clear"></div>
</div>
	<?php
}

/**
 * Displays link categories form fields.
 *
 * @since 2.6.0
 *
 * @param object $link Current link object.
 */
function link_categories_meta_box( $link ) {
	?>
<div id="taxonomy-linkcategory" class="categorydiv">
	<ul id="category-tabs" class="category-tabs">
		<li class="tabs"><a href="#categories-all"><?php _e( 'All categories' ); ?></a></li>
		<li class="hide-if-no-js"><a href="#categories-pop"><?php _ex( 'Most Used', 'categories' ); ?></a></li>
	</ul>

	<div id="categories-all" class="tabs-panel">
		<ul id="categorychecklist" data-wp-lists="list:category" class="categorychecklist form-no-clear">
			<?php
			if ( isset( $link->link_id ) ) {
				wp_link_category_checklist( $link->link_id );
			} else {
				wp_link_category_checklist();
			}
			?>
		</ul>
	</div>

	<div id="categories-pop" class="tabs-panel" style="display: none;">
		<ul id="categorychecklist-pop" class="categorychecklist form-no-clear">
			<?php wp_popular_terms_checklist( 'link_category' ); ?>
		</ul>
	</div>

	<div id="category-adder" class="wp-hidden-children">
		<a id="category-add-toggle" href="#category-add" class="taxonomy-add-new"><?php _e( '+ Add Category' ); ?></a>
		<p id="link-category-add" class="wp-hidden-child">
			<label class="screen-reader-text" for="newcat">
				<?php
				/* translators: Hidden accessibility text. */
				_e( '+ Add Category' );
				?>
			</label>
			<input type="text" name="newcat" id="newcat" class="form-required form-input-tip" value="<?php esc_attr_e( 'New category name' ); ?>" aria-required="true" />
			<input type="button" id="link-category-add-submit" data-wp-lists="add:categorychecklist:link-category-add" class="button" value="<?php esc_attr_e( 'Add' ); ?>" />
			<?php wp_nonce_field( 'add-link-category', '_ajax_nonce', false ); ?>
			<span id="category-ajax-response"></span>
		</p>
	</div>
</div>
	<?php
}

/**
 * Displays form fields for changing link target.
 *
 * @since 2.6.0
 *
 * @param object $link Current link object.
 */
function link_target_meta_box( $link ) {

	?>
<fieldset><legend class="screen-reader-text"><span>
	<?php
	/* translators: Hidden accessibility text. */
	_e( 'Target' );
	?>
</span></legend>
<p><label for="link_target_blank" class="selectit">
<input id="link_target_blank" type="radio" name="link_target" value="_blank" <?php echo ( isset( $link->link_target ) && ( '_blank' === $link->link_target ) ? 'checked="checked"' : '' ); ?> />
	<?php _e( '<code>_blank</code> &mdash; new window or tab.' ); ?></label></p>
<p><label for="link_target_top" class="selectit">
<input id="link_target_top" type="radio" name="link_target" value="_top" <?php echo ( isset( $link->link_target ) && ( '_top' === $link->link_target ) ? 'checked="checked"' : '' ); ?> />
	<?php _e( '<code>_top</code> &mdash; current window or tab, with no frames.' ); ?></label></p>
<p><label for="link_target_none" class="selectit">
<input id="link_target_none" type="radio" name="link_target" value="" <?php echo ( isset( $link->link_target ) && ( '' === $link->link_target ) ? 'checked="checked"' : '' ); ?> />
	<?php _e( '<code>_none</code> &mdash; same window or tab.' ); ?></label></p>
</fieldset>
<p><?php _e( 'Choose the target frame for your link.' ); ?></p>
	<?php
}

/**
 * Displays 'checked' checkboxes attribute for XFN microformat options.
 *
 * @since 1.0.1
 *
 * @global object $link Current link object.
 *
 * @param string $xfn_relationship XFN relationship category. Possible values are:
 *                                 'friendship', 'physical', 'professional',
 *                                 'geographical', 'family', 'romantic', 'identity'.
 * @param string $xfn_value        Optional. The XFN value to mark as checked
 *                                 if it matches the current link's relationship.
 *                                 Default empty string.
 * @param mixed  $deprecated       Deprecated. Not used.
 */
function xfn_check( $xfn_relationship, $xfn_value = '', $deprecated = '' ) {
	global $link;

	if ( ! empty( $deprecated ) ) {
		_deprecated_argument( __FUNCTION__, '2.5.0' ); // Never implemented.
	}

	$link_rel  = isset( $link->link_rel ) ? $link->link_rel : '';
	$link_rels = preg_split( '/\s+/', $link_rel );

	// Mark the specified value as checked if it matches the current link's relationship.
	if ( '' !== $xfn_value && in_array( $xfn_value, $link_rels, true ) ) {
		echo ' checked="checked"';
	}

	if ( '' === $xfn_value ) {
		// Mark the 'none' value as checked if the current link does not match the specified relationship.
		if ( 'family' === $xfn_relationship
			&& ! array_intersect( $link_rels, array( 'child', 'parent', 'sibling', 'spouse', 'kin' ) )
		) {
			echo ' checked="checked"';
		}

		if ( 'friendship' === $xfn_relationship
			&& ! array_intersect( $link_rels, array( 'friend', 'acquaintance', 'contact' ) )
		) {
			echo ' checked="checked"';
		}

		if ( 'geographical' === $xfn_relationship
			&& ! array_intersect( $link_rels, array( 'co-resident', 'neighbor' ) )
		) {
			echo ' checked="checked"';
		}

		// Mark the 'me' value as checked if it matches the current link's relationship.
		if ( 'identity' === $xfn_relationship
			&& in_array( 'me', $link_rels, true )
		) {
			echo ' checked="checked"';
		}
	}
}

/**
 * Displays XFN form fields.
 *
 * @since 2.6.0
 *
 * @param object $link Current link object.
 */
function link_xfn_meta_box( $link ) {
	?>
<table class="links-table">
	<tr>
		<th scope="row"><label for="link_rel"><?php /* translators: xfn: https://gmpg.org/xfn/ */ _e( 'rel:' ); ?></label></th>
		<td><input type="text" name="link_rel" id="link_rel" value="<?php echo ( isset( $link->link_rel ) ? esc_attr( $link->link_rel ) : '' ); ?>" /></td>
	</tr>
	<tr>
		<th scope="row"><?php /* translators: xfn: https://gmpg.org/xfn/ */ _e( 'identity' ); ?></th>
		<td><fieldset>
			<legend class="screen-reader-text"><span>
				<?php
				/* translators: Hidden accessibility text. xfn: https://gmpg.org/xfn/ */
				_e( 'identity' );
				?>
			</span></legend>
			<label for="me">
			<input type="checkbox" name="identity" value="me" id="me" <?php xfn_check( 'identity', 'me' ); ?> />
			<?php _e( 'another web address of mine' ); ?></label>
		</fieldset></td>
	</tr>
	<tr>
		<th scope="row"><?php /* translators: xfn: https://gmpg.org/xfn/ */ _e( 'friendship' ); ?></th>
		<td><fieldset>
			<legend class="screen-reader-text"><span>
				<?php
				/* translators: Hidden accessibility text. xfn: https://gmpg.org/xfn/ */
				_e( 'friendship' );
				?>
			</span></legend>
			<label for="contact">
			<input class="valinp" type="radio" name="friendship" value="contact" id="contact" <?php xfn_check( 'friendship', 'contact' ); ?> />&nbsp;<?php /* translators: xfn: https://gmpg.org/xfn/ */ _e( 'contact' ); ?>
			</label>
			<label for="acquaintance">
			<input class="valinp" type="radio" name="friendship" value="acquaintance" id="acquaintance" <?php xfn_check( 'friendship', 'acquaintance' ); ?> />&nbsp;<?php /* translators: xfn: https://gmpg.org/xfn/ */ _e( 'acquaintance' ); ?>
			</label>
			<label for="friend">
			<input class="valinp" type="radio" name="friendship" value="friend" id="friend" <?php xfn_check( 'friendship', 'friend' ); ?> />&nbsp;<?php /* translators: xfn: https://gmpg.org/xfn/ */ _e( 'friend' ); ?>
			</label>
			<label for="friendship">
			<input name="friendship" type="radio" class="valinp" value="" id="friendship" <?php xfn_check( 'friendship' ); ?> />&nbsp;<?php /* translators: xfn (friendship relation): http://gmpg.org/xfn/ */ _ex( 'none', 'Type of relation' ); ?>
			</label>
		</fieldset></td>
	</tr>
	<tr>
		<th scope="row"> <?php /* translators: xfn: https://gmpg.org/xfn/ */ _e( 'physical' ); ?> </th>
		<td><fieldset>
			<legend class="screen-reader-text"><span>
				<?php
				/* translators: Hidden accessibility text. xfn: https://gmpg.org/xfn/ */
				_e( 'physical' );
				?>
			</span></legend>
			<label for="met">
			<input class="valinp" type="checkbox" name="physical" value="met" id="met" <?php xfn_check( 'physical', 'met' ); ?> />&nbsp;<?php /* translators: xfn: https://gmpg.org/xfn/ */ _e( 'met' ); ?>
			</label>
		</fieldset></td>
	</tr>
	<tr>
		<th scope="row"> <?php /* translators: xfn: https://gmpg.org/xfn/ */ _e( 'professional' ); ?> </th>
		<td><fieldset>
			<legend class="screen-reader-text"><span>
				<?php
				/* translators: Hidden accessibility text. xfn: https://gmpg.org/xfn/ */
				_e( 'professional' );
				?>
			</span></legend>
			<label for="co-worker">
			<input class="valinp" type="checkbox" name="professional" value="co-worker" id="co-worker" <?php xfn_check( 'professional', 'co-worker' ); ?> />&nbsp;<?php /* translators: xfn: https://gmpg.org/xfn/ */ _e( 'co-worker' ); ?>
			</label>
			<label for="colleague">
			<input class="valinp" type="checkbox" name="professional" value="colleague" id="colleague" <?php xfn_check( 'professional', 'colleague' ); ?> />&nbsp;<?php /* translators: xfn: https://gmpg.org/xfn/ */ _e( 'colleague' ); ?>
			</label>
		</fieldset></td>
	</tr>
	<tr>
		<th scope="row"><?php /* translators: xfn: https://gmpg.org/xfn/ */ _e( 'geographical' ); ?></th>
		<td><fieldset>
			<legend class="screen-reader-text"><span>
				<?php
				/* translators: Hidden accessibility text. xfn: https://gmpg.org/xfn/ */
				_e( 'geographical' );
				?>
			</span></legend>
			<label for="co-resident">
			<input class="valinp" type="radio" name="geographical" value="co-resident" id="co-resident" <?php xfn_check( 'geographical', 'co-resident' ); ?> />&nbsp;<?php /* translators: xfn: https://gmpg.org/xfn/ */ _e( 'co-resident' ); ?>
			</label>
			<label for="neighbor">
			<input class="valinp" type="radio" name="geographical" value="neighbor" id="neighbor" <?php xfn_check( 'geographical', 'neighbor' ); ?> />&nbsp;<?php /* translators: xfn: https://gmpg.org/xfn/ */ _e( 'neighbor' ); ?>
			</label>
			<label for="geographical">
			<input class="valinp" type="radio" name="geographical" value="" id="geographical" <?php xfn_check( 'geographical' ); ?> />&nbsp;<?php /* translators: xfn (geographical relation): http://gmpg.org/xfn/ */ _ex( 'none', 'Type of relation' ); ?>
			</label>
		</fieldset></td>
	</tr>
	<tr>
		<th scope="row"><?php /* translators: xfn: https://gmpg.org/xfn/ */ _e( 'family' ); ?></th>
		<td><fieldset>
			<legend class="screen-reader-text"><span>
				<?php
				/* translators: Hidden accessibility text. xfn: https://gmpg.org/xfn/ */
				_e( 'family' );
				?>
			</span></legend>
			<label for="child">
			<input class="valinp" type="radio" name="family" value="child" id="child" <?php xfn_check( 'family', 'child' ); ?> />&nbsp;<?php /* translators: xfn: https://gmpg.org/xfn/ */ _e( 'child' ); ?>
			</label>
			<label for="kin">
			<input class="valinp" type="radio" name="family" value="kin" id="kin" <?php xfn_check( 'family', 'kin' ); ?> />&nbsp;<?php /* translators: xfn: https://gmpg.org/xfn/ */ _e( 'kin' ); ?>
			</label>
			<label for="parent">
			<input class="valinp" type="radio" name="family" value="parent" id="parent" <?php xfn_check( 'family', 'parent' ); ?> />&nbsp;<?php /* translators: xfn: https://gmpg.org/xfn/ */ _e( 'parent' ); ?>
			</label>
			<label for="sibling">
			<input class="valinp" type="radio" name="family" value="sibling" id="sibling" <?php xfn_check( 'family', 'sibling' ); ?> />&nbsp;<?php /* translators: xfn: https://gmpg.org/xfn/ */ _e( 'sibling' ); ?>
			</label>
			<label for="spouse">
			<input class="valinp" type="radio" name="family" value="spouse" id="spouse" <?php xfn_check( 'family', 'spouse' ); ?> />&nbsp;<?php /* translators: xfn: https://gmpg.org/xfn/ */ _e( 'spouse' ); ?>
			</label>
			<label for="family">
			<input class="valinp" type="radio" name="family" value="" id="family" <?php xfn_check( 'family' ); ?> />&nbsp;<?php /* translators: xfn (family relation): http://gmpg.org/xfn/ */ _ex( 'none', 'Type of relation' ); ?>
			</label>
		</fieldset></td>
	</tr>
	<tr>
		<th scope="row"><?php /* translators: xfn: https://gmpg.org/xfn/ */ _e( 'romantic' ); ?></th>
		<td><fieldset>
			<legend class="screen-reader-text"><span>
				<?php
				/* translators: Hidden accessibility text. xfn: https://gmpg.org/xfn/ */
				_e( 'romantic' );
				?>
			</span></legend>
			<label for="muse">
			<input class="valinp" type="checkbox" name="romantic" value="muse" id="muse" <?php xfn_check( 'romantic', 'muse' ); ?> />&nbsp;<?php /* translators: xfn: https://gmpg.org/xfn/ */ _e( 'muse' ); ?>
			</label>
			<label for="crush">
			<input class="valinp" type="checkbox" name="romantic" value="crush" id="crush" <?php xfn_check( 'romantic', 'crush' ); ?> />&nbsp;<?php /* translators: xfn: https://gmpg.org/xfn/ */ _e( 'crush' ); ?>
			</label>
			<label for="date">
			<input class="valinp" type="checkbox" name="romantic" value="date" id="date" <?php xfn_check( 'romantic', 'date' ); ?> />&nbsp;<?php /* translators: xfn: https://gmpg.org/xfn/ */ _e( 'date' ); ?>
			</label>
			<label for="romantic">
			<input class="valinp" type="checkbox" name="romantic" value="sweetheart" id="romantic" <?php xfn_check( 'romantic', 'sweetheart' ); ?> />&nbsp;<?php /* translators: xfn: https://gmpg.org/xfn/ */ _e( 'sweetheart' ); ?>
			</label>
		</fieldset></td>
	</tr>

</table>
<p><?php _e( 'If the link is to a person, you can specify your relationship with them using the above form. If you would like to learn more about the idea check out <a href="https://gmpg.org/xfn/">XFN</a>.' ); ?></p>
	<?php
}

/**
 * Displays advanced link options form fields.
 *
 * @since 2.6.0
 *
 * @param object $link Current link object.
 */
function link_advanced_meta_box( $link ) {
	?>
<table class="links-table" cellpadding="0">
	<tr>
		<th scope="row"><label for="link_image"><?php _e( 'Image Address' ); ?></label></th>
		<td><input type="text" name="link_image" class="code" id="link_image" maxlength="255" value="<?php echo ( isset( $link->link_image ) ? esc_attr( $link->link_image ) : '' ); ?>" /></td>
	</tr>
	<tr>
		<th scope="row"><label for="rss_uri"><?php _e( 'RSS Address' ); ?></label></th>
		<td><input name="link_rss" class="code" type="text" id="rss_uri" maxlength="255" value="<?php echo ( isset( $link->link_rss ) ? esc_attr( $link->link_rss ) : '' ); ?>" /></td>
	</tr>
	<tr>
		<th scope="row"><label for="link_notes"><?php _e( 'Notes' ); ?></label></th>
		<td><textarea name="link_notes" id="link_notes" rows="10"><?php echo ( isset( $link->link_notes ) ? $link->link_notes : '' ); // textarea_escaped ?></textarea></td>
	</tr>
	<tr>
		<th scope="row"><label for="link_rating"><?php _e( 'Rating' ); ?></label></th>
		<td><select name="link_rating" id="link_rating" size="1">
		<?php
		for ( $rating = 0; $rating <= 10; $rating++ ) {
			echo '<option value="' . $rating . '"';
			if ( isset( $link->link_rating ) && $link->link_rating === $rating ) {
				echo ' selected="selected"';
			}
			echo '>' . $rating . '</option>';
		}
		?>
		</select>&nbsp;<?php _e( '(Leave at 0 for no rating.)' ); ?>
		</td>
	</tr>
</table>
	<?php
}

/**
 * Displays post thumbnail meta box.
 *
 * @since 2.9.0
 *
 * @param WP_Post $post Current post object.
 */
function post_thumbnail_meta_box( $post ) {
	$thumbnail_id = get_post_meta( $post->ID, '_thumbnail_id', true );
	echo _wp_post_thumbnail_html( $thumbnail_id, $post->ID );
}

/**
 * Displays fields for ID3 data.
 *
 * @since 3.9.0
 *
 * @param WP_Post $post Current post object.
 */
function attachment_id3_data_meta_box( $post ) {
	$meta = array();
	if ( ! empty( $post->ID ) ) {
		$meta = wp_get_attachment_metadata( $post->ID );
	}

	foreach ( wp_get_attachment_id3_keys( $post, 'edit' ) as $key => $label ) :
		$value = '';
		if ( ! empty( $meta[ $key ] ) ) {
			$value = $meta[ $key ];
		}
		?>
	<p>
		<label for="title"><?php echo $label; ?></label><br />
		<input type="text" name="id3_<?php echo esc_attr( $key ); ?>" id="id3_<?php echo esc_attr( $key ); ?>" class="large-text" value="<?php echo esc_attr( $value ); ?>" />
	</p>
		<?php
	endforeach;
}

/**
 * Registers the default post meta boxes, and runs the `do_meta_boxes` actions.
 *
 * @since 5.0.0
 *
 * @param WP_Post $post The post object that these meta boxes are being generated for.
 */
function register_and_do_post_meta_boxes( $post ) {
	$post_type        = $post->post_type;
	$post_type_object = get_post_type_object( $post_type );

	$thumbnail_support = current_theme_supports( 'post-thumbnails', $post_type ) && post_type_supports( $post_type, 'thumbnail' );
	if ( ! $thumbnail_support && 'attachment' === $post_type && $post->post_mime_type ) {
		if ( wp_attachment_is( 'audio', $post ) ) {
			$thumbnail_support = post_type_supports( 'attachment:audio', 'thumbnail' ) || current_theme_supports( 'post-thumbnails', 'attachment:audio' );
		} elseif ( wp_attachment_is( 'video', $post ) ) {
			$thumbnail_support = post_type_supports( 'attachment:video', 'thumbnail' ) || current_theme_supports( 'post-thumbnails', 'attachment:video' );
		}
	}

	$publish_callback_args = array( '__back_compat_meta_box' => true );

	if ( post_type_supports( $post_type, 'revisions' ) && 'auto-draft' !== $post->post_status ) {
		$revisions = wp_get_latest_revision_id_and_total_count( $post->ID );

		// We should aim to show the revisions meta box only when there are revisions.
		if ( ! is_wp_error( $revisions ) && $revisions['count'] > 1 ) {
			$publish_callback_args = array(
				'revisions_count'        => $revisions['count'],
				'revision_id'            => $revisions['latest_id'],
				'__back_compat_meta_box' => true,
			);

			add_meta_box( 'revisionsdiv', __( 'Revisions' ), 'post_revisions_meta_box', null, 'normal', 'core', array( '__back_compat_meta_box' => true ) );
		}
	}

	if ( 'attachment' === $post_type ) {
		wp_enqueue_script( 'image-edit' );
		wp_enqueue_style( 'imgareaselect' );
		add_meta_box( 'submitdiv', __( 'Save' ), 'attachment_submit_meta_box', null, 'side', 'core', array( '__back_compat_meta_box' => true ) );
		add_action( 'edit_form_after_title', 'edit_form_image_editor' );

		if ( wp_attachment_is( 'audio', $post ) ) {
			add_meta_box( 'attachment-id3', __( 'Metadata' ), 'attachment_id3_data_meta_box', null, 'normal', 'core', array( '__back_compat_meta_box' => true ) );
		}
	} else {
		add_meta_box( 'submitdiv', __( 'Publish' ), 'post_submit_meta_box', null, 'side', 'core', $publish_callback_args );
	}

	if ( current_theme_supports( 'post-formats' ) && post_type_supports( $post_type, 'post-formats' ) ) {
		add_meta_box( 'formatdiv', _x( 'Format', 'post format' ), 'post_format_meta_box', null, 'side', 'core', array( '__back_compat_meta_box' => true ) );
	}

	// All taxonomies.
	foreach ( get_object_taxonomies( $post ) as $tax_name ) {
		$taxonomy = get_taxonomy( $tax_name );
		if ( ! $taxonomy->show_ui || false === $taxonomy->meta_box_cb ) {
			continue;
		}

		$label = $taxonomy->labels->name;

		if ( ! is_taxonomy_hierarchical( $tax_name ) ) {
			$tax_meta_box_id = 'tagsdiv-' . $tax_name;
		} else {
			$tax_meta_box_id = $tax_name . 'div';
		}

		add_meta_box(
			$tax_meta_box_id,
			$label,
			$taxonomy->meta_box_cb,
			null,
			'side',
			'core',
			array(
				'taxonomy'               => $tax_name,
				'__back_compat_meta_box' => true,
			)
		);
	}

	if ( post_type_supports( $post_type, 'page-attributes' ) || count( get_page_templates( $post ) ) > 0 ) {
		add_meta_box( 'pageparentdiv', $post_type_object->labels->attributes, 'page_attributes_meta_box', null, 'side', 'core', array( '__back_compat_meta_box' => true ) );
	}

	if ( $thumbnail_support && current_user_can( 'upload_files' ) ) {
		add_meta_box( 'postimagediv', esc_html( $post_type_object->labels->featured_image ), 'post_thumbnail_meta_box', null, 'side', 'low', array( '__back_compat_meta_box' => true ) );
	}

	if ( post_type_supports( $post_type, 'excerpt' ) ) {
		add_meta_box( 'postexcerpt', __( 'Excerpt' ), 'post_excerpt_meta_box', null, 'normal', 'core', array( '__back_compat_meta_box' => true ) );
	}

	if ( post_type_supports( $post_type, 'trackbacks' ) ) {
		add_meta_box( 'trackbacksdiv', __( 'Send Trackbacks' ), 'post_trackback_meta_box', null, 'normal', 'core', array( '__back_compat_meta_box' => true ) );
	}

	if ( post_type_supports( $post_type, 'custom-fields' ) ) {
		add_meta_box(
			'postcustom',
			__( 'Custom Fields' ),
			'post_custom_meta_box',
			null,
			'normal',
			'core',
			array(
				'__back_compat_meta_box'             => ! (bool) get_user_meta( get_current_user_id(), 'enable_custom_fields', true ),
				'__block_editor_compatible_meta_box' => true,
			)
		);
	}

	/**
	 * Fires in the middle of built-in meta box registration.
	 *
	 * @since 2.1.0
	 * @deprecated 3.7.0 Use {@see 'add_meta_boxes'} instead.
	 *
	 * @param WP_Post $post Post object.
	 */
	do_action_deprecated( 'dbx_post_advanced', array( $post ), '3.7.0', 'add_meta_boxes' );

	/*
	 * Allow the Discussion meta box to show up if the post type supports comments,
	 * or if comments or pings are open.
	 */
	if ( comments_open( $post ) || pings_open( $post ) || post_type_supports( $post_type, 'comments' ) ) {
		add_meta_box( 'commentstatusdiv', __( 'Discussion' ), 'post_comment_status_meta_box', null, 'normal', 'core', array( '__back_compat_meta_box' => true ) );
	}

	$statuses = get_post_stati( array( 'public' => true ) );

	if ( empty( $statuses ) ) {
		$statuses = array( 'publish' );
	}

	$statuses[] = 'private';

	if ( in_array( get_post_status( $post ), $statuses, true ) ) {
		/*
		 * If the post type support comments, or the post has comments,
		 * allow the Comments meta box.
		 */
		if ( comments_open( $post ) || pings_open( $post ) || $post->comment_count > 0 || post_type_supports( $post_type, 'comments' ) ) {
			add_meta_box( 'commentsdiv', __( 'Comments' ), 'post_comment_meta_box', null, 'normal', 'core', array( '__back_compat_meta_box' => true ) );
		}
	}

	if ( ! ( 'pending' === get_post_status( $post ) && ! current_user_can( $post_type_object->cap->publish_posts ) ) ) {
		add_meta_box( 'slugdiv', __( 'Slug' ), 'post_slug_meta_box', null, 'normal', 'core', array( '__back_compat_meta_box' => true ) );
	}

	if ( post_type_supports( $post_type, 'author' ) && current_user_can( $post_type_object->cap->edit_others_posts ) ) {
		add_meta_box( 'authordiv', __( 'Author' ), 'post_author_meta_box', null, 'normal', 'core', array( '__back_compat_meta_box' => true ) );
	}

	/**
	 * Fires after all built-in meta boxes have been added.
	 *
	 * @since 3.0.0
	 *
	 * @param string  $post_type Post type.
	 * @param WP_Post $post      Post object.
	 */
	do_action( 'add_meta_boxes', $post_type, $post );

	/**
	 * Fires after all built-in meta boxes have been added, contextually for the given post type.
	 *
	 * The dynamic portion of the hook name, `$post_type`, refers to the post type of the post.
	 *
	 * Possible hook names include:
	 *
	 *  - `add_meta_boxes_post`
	 *  - `add_meta_boxes_page`
	 *  - `add_meta_boxes_attachment`
	 *
	 * @since 3.0.0
	 *
	 * @param WP_Post $post Post object.
	 */
	do_action( "add_meta_boxes_{$post_type}", $post );

	/**
	 * Fires after meta boxes have been added.
	 *
	 * Fires once for each of the default meta box contexts: normal, advanced, and side.
	 *
	 * @since 3.0.0
	 *
	 * @param string                $post_type Post type of the post on Edit Post screen, 'link' on Edit Link screen,
	 *                                         'dashboard' on Dashboard screen.
	 * @param string                $context   Meta box context. Possible values include 'normal', 'advanced', 'side'.
	 * @param WP_Post|object|string $post      Post object on Edit Post screen, link object on Edit Link screen,
	 *                                         an empty string on Dashboard screen.
	 */
	do_action( 'do_meta_boxes', $post_type, 'normal', $post );
	/** This action is documented in wp-admin/includes/meta-boxes.php */
	do_action( 'do_meta_boxes', $post_type, 'advanced', $post );
	/** This action is documented in wp-admin/includes/meta-boxes.php */
	do_action( 'do_meta_boxes', $post_type, 'side', $post );
}
class-theme-installer-skin.php000064400000030707151334405730012433 0ustar00<?php
/**
 * Upgrader API: Theme_Installer_Skin class
 *
 * @package WordPress
 * @subpackage Upgrader
 * @since 4.6.0
 */

/**
 * Theme Installer Skin for the WordPress Theme Installer.
 *
 * @since 2.8.0
 * @since 4.6.0 Moved to its own file from wp-admin/includes/class-wp-upgrader-skins.php.
 *
 * @see WP_Upgrader_Skin
 */
class Theme_Installer_Skin extends WP_Upgrader_Skin {
	public $api;
	public $type;
	public $url;
	public $overwrite;

	private $is_downgrading = false;

	/**
	 * Constructor.
	 *
	 * Sets up the theme installer skin.
	 *
	 * @since 2.8.0
	 *
	 * @param array $args
	 */
	public function __construct( $args = array() ) {
		$defaults = array(
			'type'      => 'web',
			'url'       => '',
			'theme'     => '',
			'nonce'     => '',
			'title'     => '',
			'overwrite' => '',
		);
		$args     = wp_parse_args( $args, $defaults );

		$this->type      = $args['type'];
		$this->url       = $args['url'];
		$this->api       = isset( $args['api'] ) ? $args['api'] : array();
		$this->overwrite = $args['overwrite'];

		parent::__construct( $args );
	}

	/**
	 * Performs an action before installing a theme.
	 *
	 * @since 2.8.0
	 */
	public function before() {
		if ( ! empty( $this->api ) ) {
			$this->upgrader->strings['process_success'] = sprintf(
				$this->upgrader->strings['process_success_specific'],
				$this->api->name,
				$this->api->version
			);
		}
	}

	/**
	 * Hides the `process_failed` error when updating a theme by uploading a zip file.
	 *
	 * @since 5.5.0
	 *
	 * @param WP_Error $wp_error WP_Error object.
	 * @return bool True if the error should be hidden, false otherwise.
	 */
	public function hide_process_failed( $wp_error ) {
		if (
			'upload' === $this->type &&
			'' === $this->overwrite &&
			$wp_error->get_error_code() === 'folder_exists'
		) {
			return true;
		}

		return false;
	}

	/**
	 * Performs an action following a single theme install.
	 *
	 * @since 2.8.0
	 */
	public function after() {
		if ( $this->do_overwrite() ) {
			return;
		}

		if ( empty( $this->upgrader->result['destination_name'] ) ) {
			return;
		}

		$theme_info = $this->upgrader->theme_info();
		if ( empty( $theme_info ) ) {
			return;
		}

		$name       = $theme_info->display( 'Name' );
		$stylesheet = $this->upgrader->result['destination_name'];
		$template   = $theme_info->get_template();

		$activate_link = add_query_arg(
			array(
				'action'     => 'activate',
				'template'   => urlencode( $template ),
				'stylesheet' => urlencode( $stylesheet ),
			),
			admin_url( 'themes.php' )
		);
		$activate_link = wp_nonce_url( $activate_link, 'switch-theme_' . $stylesheet );

		$install_actions = array();

		if ( current_user_can( 'edit_theme_options' ) && current_user_can( 'customize' ) && ! $theme_info->is_block_theme() ) {
			$customize_url = add_query_arg(
				array(
					'theme'  => urlencode( $stylesheet ),
					'return' => urlencode( admin_url( 'web' === $this->type ? 'theme-install.php' : 'themes.php' ) ),
				),
				admin_url( 'customize.php' )
			);

			$install_actions['preview'] = sprintf(
				'<a href="%s" class="hide-if-no-customize load-customize">' .
				'<span aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></a>',
				esc_url( $customize_url ),
				__( 'Live Preview' ),
				/* translators: Hidden accessibility text. %s: Theme name. */
				sprintf( __( 'Live Preview &#8220;%s&#8221;' ), $name )
			);
		}

		$install_actions['activate'] = sprintf(
			'<a href="%s" class="activatelink">' .
			'<span aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></a>',
			esc_url( $activate_link ),
			_x( 'Activate', 'theme' ),
			/* translators: Hidden accessibility text. %s: Theme name. */
			sprintf( _x( 'Activate &#8220;%s&#8221;', 'theme' ), $name )
		);

		if ( is_network_admin() && current_user_can( 'manage_network_themes' ) ) {
			$install_actions['network_enable'] = sprintf(
				'<a href="%s" target="_parent">%s</a>',
				esc_url( wp_nonce_url( 'themes.php?action=enable&amp;theme=' . urlencode( $stylesheet ), 'enable-theme_' . $stylesheet ) ),
				__( 'Network Enable' )
			);
		}

		if ( 'web' === $this->type ) {
			$install_actions['themes_page'] = sprintf(
				'<a href="%s" target="_parent">%s</a>',
				self_admin_url( 'theme-install.php' ),
				__( 'Go to Theme Installer' )
			);
		} elseif ( current_user_can( 'switch_themes' ) || current_user_can( 'edit_theme_options' ) ) {
			$install_actions['themes_page'] = sprintf(
				'<a href="%s" target="_parent">%s</a>',
				self_admin_url( 'themes.php' ),
				__( 'Go to Themes page' )
			);
		}

		if ( ! $this->result || is_wp_error( $this->result ) || is_network_admin() || ! current_user_can( 'switch_themes' ) ) {
			unset( $install_actions['activate'], $install_actions['preview'] );
		} elseif ( get_option( 'template' ) === $stylesheet ) {
			unset( $install_actions['activate'] );
		}

		/**
		 * Filters the list of action links available following a single theme installation.
		 *
		 * @since 2.8.0
		 *
		 * @param string[] $install_actions Array of theme action links.
		 * @param object   $api             Object containing WordPress.org API theme data.
		 * @param string   $stylesheet      Theme directory name.
		 * @param WP_Theme $theme_info      Theme object.
		 */
		$install_actions = apply_filters( 'install_theme_complete_actions', $install_actions, $this->api, $stylesheet, $theme_info );
		if ( ! empty( $install_actions ) ) {
			$this->feedback( implode( ' | ', (array) $install_actions ) );
		}
	}

	/**
	 * Checks if the theme can be overwritten and outputs the HTML for overwriting a theme on upload.
	 *
	 * @since 5.5.0
	 *
	 * @return bool Whether the theme can be overwritten and HTML was outputted.
	 */
	private function do_overwrite() {
		if ( 'upload' !== $this->type || ! is_wp_error( $this->result ) || 'folder_exists' !== $this->result->get_error_code() ) {
			return false;
		}

		$folder = $this->result->get_error_data( 'folder_exists' );
		$folder = rtrim( $folder, '/' );

		$current_theme_data = false;
		$all_themes         = wp_get_themes( array( 'errors' => null ) );

		foreach ( $all_themes as $theme ) {
			$stylesheet_dir = wp_normalize_path( $theme->get_stylesheet_directory() );

			if ( rtrim( $stylesheet_dir, '/' ) !== $folder ) {
				continue;
			}

			$current_theme_data = $theme;
		}

		$new_theme_data = $this->upgrader->new_theme_data;

		if ( ! $current_theme_data || ! $new_theme_data ) {
			return false;
		}

		echo '<h2 class="update-from-upload-heading">' . esc_html__( 'This theme is already installed.' ) . '</h2>';

		// Check errors for active theme.
		if ( is_wp_error( $current_theme_data->errors() ) ) {
			$this->feedback( 'current_theme_has_errors', $current_theme_data->errors()->get_error_message() );
		}

		$this->is_downgrading = version_compare( $current_theme_data['Version'], $new_theme_data['Version'], '>' );

		$is_invalid_parent = false;
		if ( ! empty( $new_theme_data['Template'] ) ) {
			$is_invalid_parent = ! in_array( $new_theme_data['Template'], array_keys( $all_themes ), true );
		}

		$rows = array(
			'Name'        => __( 'Theme name' ),
			'Version'     => __( 'Version' ),
			'Author'      => __( 'Author' ),
			'RequiresWP'  => __( 'Required WordPress version' ),
			'RequiresPHP' => __( 'Required PHP version' ),
			'Template'    => __( 'Parent theme' ),
		);

		$table  = '<table class="update-from-upload-comparison"><tbody>';
		$table .= '<tr><th></th><th>' . esc_html_x( 'Installed', 'theme' ) . '</th><th>' . esc_html_x( 'Uploaded', 'theme' ) . '</th></tr>';

		$is_same_theme = true; // Let's consider only these rows.

		foreach ( $rows as $field => $label ) {
			$old_value = $current_theme_data->display( $field, false );
			$old_value = $old_value ? (string) $old_value : '-';

			$new_value = ! empty( $new_theme_data[ $field ] ) ? (string) $new_theme_data[ $field ] : '-';

			if ( $old_value === $new_value && '-' === $new_value && 'Template' === $field ) {
				continue;
			}

			$is_same_theme = $is_same_theme && ( $old_value === $new_value );

			$diff_field     = ( 'Version' !== $field && $new_value !== $old_value );
			$diff_version   = ( 'Version' === $field && $this->is_downgrading );
			$invalid_parent = false;

			if ( 'Template' === $field && $is_invalid_parent ) {
				$invalid_parent = true;
				$new_value     .= ' ' . __( '(not found)' );
			}

			$table .= '<tr><td class="name-label">' . $label . '</td><td>' . wp_strip_all_tags( $old_value ) . '</td>';
			$table .= ( $diff_field || $diff_version || $invalid_parent ) ? '<td class="warning">' : '<td>';
			$table .= wp_strip_all_tags( $new_value ) . '</td></tr>';
		}

		$table .= '</tbody></table>';

		/**
		 * Filters the compare table output for overwriting a theme package on upload.
		 *
		 * @since 5.5.0
		 *
		 * @param string   $table              The output table with Name, Version, Author, RequiresWP, and RequiresPHP info.
		 * @param WP_Theme $current_theme_data Active theme data.
		 * @param array    $new_theme_data     Array with uploaded theme data.
		 */
		echo apply_filters( 'install_theme_overwrite_comparison', $table, $current_theme_data, $new_theme_data );

		$install_actions = array();
		$can_update      = true;

		$blocked_message  = '<p>' . esc_html__( 'The theme cannot be updated due to the following:' ) . '</p>';
		$blocked_message .= '<ul class="ul-disc">';

		$requires_php = isset( $new_theme_data['RequiresPHP'] ) ? $new_theme_data['RequiresPHP'] : null;
		$requires_wp  = isset( $new_theme_data['RequiresWP'] ) ? $new_theme_data['RequiresWP'] : null;

		if ( ! is_php_version_compatible( $requires_php ) ) {
			$error = sprintf(
				/* translators: 1: Current PHP version, 2: Version required by the uploaded theme. */
				__( 'The PHP version on your server is %1$s, however the uploaded theme requires %2$s.' ),
				PHP_VERSION,
				$requires_php
			);

			$blocked_message .= '<li>' . esc_html( $error ) . '</li>';
			$can_update       = false;
		}

		if ( ! is_wp_version_compatible( $requires_wp ) ) {
			$error = sprintf(
				/* translators: 1: Current WordPress version, 2: Version required by the uploaded theme. */
				__( 'Your WordPress version is %1$s, however the uploaded theme requires %2$s.' ),
				esc_html( wp_get_wp_version() ),
				$requires_wp
			);

			$blocked_message .= '<li>' . esc_html( $error ) . '</li>';
			$can_update       = false;
		}

		$blocked_message .= '</ul>';

		if ( $can_update ) {
			if ( $this->is_downgrading ) {
				$warning = sprintf(
					/* translators: %s: Documentation URL. */
					__( 'You are uploading an older version of the installed theme. You can continue to install the older version, but be sure to <a href="%s">back up your database and files</a> first.' ),
					__( 'https://developer.wordpress.org/advanced-administration/security/backup/' )
				);
			} else {
				$warning = sprintf(
					/* translators: %s: Documentation URL. */
					__( 'You are updating a theme. Be sure to <a href="%s">back up your database and files</a> first.' ),
					__( 'https://developer.wordpress.org/advanced-administration/security/backup/' )
				);
			}

			echo '<p class="update-from-upload-notice">' . $warning . '</p>';

			$overwrite = $this->is_downgrading ? 'downgrade-theme' : 'update-theme';

			$install_actions['overwrite_theme'] = sprintf(
				'<a class="button button-primary update-from-upload-overwrite" href="%s" target="_parent">%s</a>',
				wp_nonce_url( add_query_arg( 'overwrite', $overwrite, $this->url ), 'theme-upload' ),
				_x( 'Replace installed with uploaded', 'theme' )
			);
		} else {
			echo $blocked_message;
		}

		$cancel_url = add_query_arg( 'action', 'upload-theme-cancel-overwrite', $this->url );

		$install_actions['themes_page'] = sprintf(
			'<a class="button" href="%s" target="_parent">%s</a>',
			wp_nonce_url( $cancel_url, 'theme-upload-cancel-overwrite' ),
			__( 'Cancel and go back' )
		);

		/**
		 * Filters the list of action links available following a single theme installation failure
		 * when overwriting is allowed.
		 *
		 * @since 5.5.0
		 *
		 * @param string[] $install_actions Array of theme action links.
		 * @param object   $api             Object containing WordPress.org API theme data.
		 * @param array    $new_theme_data  Array with uploaded theme data.
		 */
		$install_actions = apply_filters( 'install_theme_overwrite_actions', $install_actions, $this->api, $new_theme_data );

		if ( ! empty( $install_actions ) ) {
			printf(
				'<p class="update-from-upload-expired hidden">%s</p>',
				__( 'The uploaded file has expired. Please go back and upload it again.' )
			);
			echo '<p class="update-from-upload-actions">' . implode( ' ', (array) $install_actions ) . '</p>';
		}

		return true;
	}
}
payment-tokens/class-wc-payment-token-cc.php000064400000010715151336044760015140 0ustar00<?php
/**
 * Class WC_Payment_Token_CC file.
 *
 * @package WooCommerce\PaymentTokens
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * WooCommerce Credit Card Payment Token.
 *
 * Representation of a payment token for credit cards.
 *
 * @class       WC_Payment_Token_CC
 * @version     3.0.0
 * @since       2.6.0
 * @package     WooCommerce\PaymentTokens
 */
class WC_Payment_Token_CC extends WC_Payment_Token {

	/**
	 * Token Type String.
	 *
	 * @var string
	 */
	protected $type = 'CC';

	/**
	 * Stores Credit Card payment token data.
	 *
	 * @var array
	 */
	protected $extra_data = array(
		'last4'        => '',
		'expiry_year'  => '',
		'expiry_month' => '',
		'card_type'    => '',
	);

	/**
	 * Get type to display to user.
	 *
	 * @since  2.6.0
	 * @param  string $deprecated Deprecated since WooCommerce 3.0.
	 * @return string
	 */
	public function get_display_name( $deprecated = '' ) {
		$display = sprintf(
			/* translators: 1: credit card type 2: last 4 digits 3: expiry month 4: expiry year */
			__( '%1$s ending in %2$s (expires %3$s/%4$s)', 'woocommerce' ),
			wc_get_credit_card_type_label( $this->get_card_type() ),
			$this->get_last4(),
			$this->get_expiry_month(),
			substr( $this->get_expiry_year(), 2 )
		);
		return $display;
	}

	/**
	 * Hook prefix
	 *
	 * @since 3.0.0
	 */
	protected function get_hook_prefix() {
		return 'woocommerce_payment_token_cc_get_';
	}

	/**
	 * Validate credit card payment tokens.
	 *
	 * These fields are required by all credit card payment tokens:
	 * expiry_month  - string Expiration date (MM) for the card
	 * expiry_year   - string Expiration date (YYYY) for the card
	 * last4         - string Last 4 digits of the card
	 * card_type     - string Card type (visa, mastercard, etc)
	 *
	 * @since 2.6.0
	 * @return boolean True if the passed data is valid
	 */
	public function validate() {
		if ( false === parent::validate() ) {
			return false;
		}

		if ( ! $this->get_last4( 'edit' ) ) {
			return false;
		}

		if ( ! $this->get_expiry_year( 'edit' ) ) {
			return false;
		}

		if ( ! $this->get_expiry_month( 'edit' ) ) {
			return false;
		}

		if ( ! $this->get_card_type( 'edit' ) ) {
			return false;
		}

		if ( 4 !== strlen( $this->get_expiry_year( 'edit' ) ) ) {
			return false;
		}

		if ( 2 !== strlen( $this->get_expiry_month( 'edit' ) ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Returns the card type (mastercard, visa, ...).
	 *
	 * @since  2.6.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string Card type
	 */
	public function get_card_type( $context = 'view' ) {
		return $this->get_prop( 'card_type', $context );
	}

	/**
	 * Set the card type (mastercard, visa, ...).
	 *
	 * @since 2.6.0
	 * @param string $type Credit card type (mastercard, visa, ...).
	 */
	public function set_card_type( $type ) {
		$this->set_prop( 'card_type', $type );
	}

	/**
	 * Returns the card expiration year (YYYY).
	 *
	 * @since  2.6.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string Expiration year
	 */
	public function get_expiry_year( $context = 'view' ) {
		return $this->get_prop( 'expiry_year', $context );
	}

	/**
	 * Set the expiration year for the card (YYYY format).
	 *
	 * @since 2.6.0
	 * @param string $year Credit card expiration year.
	 */
	public function set_expiry_year( $year ) {
		$this->set_prop( 'expiry_year', $year );
	}

	/**
	 * Returns the card expiration month (MM).
	 *
	 * @since  2.6.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string Expiration month
	 */
	public function get_expiry_month( $context = 'view' ) {
		return $this->get_prop( 'expiry_month', $context );
	}

	/**
	 * Set the expiration month for the card (formats into MM format).
	 *
	 * @since 2.6.0
	 * @param string $month Credit card expiration month.
	 */
	public function set_expiry_month( $month ) {
		$this->set_prop( 'expiry_month', str_pad( $month, 2, '0', STR_PAD_LEFT ) );
	}

	/**
	 * Returns the last four digits.
	 *
	 * @since  2.6.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string Last 4 digits
	 */
	public function get_last4( $context = 'view' ) {
		return $this->get_prop( 'last4', $context );
	}

	/**
	 * Set the last four digits.
	 *
	 * @since 2.6.0
	 * @param string $last4 Credit card last four digits.
	 */
	public function set_last4( $last4 ) {
		$this->set_prop( 'last4', $last4 );
	}
}
payment-tokens/class-wc-payment-token-echeck.php000064400000003771151336044760016001 0ustar00<?php
/**
 * Class WC_Payment_Token_eCheck file.
 *
 * @package WooCommerce\PaymentTokens
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * WooCommerce eCheck Payment Token.
 *
 * Representation of a payment token for eChecks.
 *
 * @class       WC_Payment_Token_ECheck
 * @version     3.0.0
 * @since       2.6.0
 * @package     WooCommerce\PaymentTokens
 */
class WC_Payment_Token_ECheck extends WC_Payment_Token {

	/**
	 * Token Type String.
	 *
	 * @var string
	 */
	protected $type = 'eCheck';

	/**
	 * Stores eCheck payment token data.
	 *
	 * @var array
	 */
	protected $extra_data = array(
		'last4' => '',
	);

	/**
	 * Get type to display to user.
	 *
	 * @since  2.6.0
	 * @param  string $deprecated Deprecated since WooCommerce 3.0.
	 * @return string
	 */
	public function get_display_name( $deprecated = '' ) {
		$display = sprintf(
			/* translators: 1: last 4 digits */
			__( 'eCheck ending in %1$s', 'woocommerce' ),
			$this->get_last4()
		);
		return $display;
	}

	/**
	 * Hook prefix
	 *
	 * @since 3.0.0
	 */
	protected function get_hook_prefix() {
		return 'woocommerce_payment_token_echeck_get_';
	}

	/**
	 * Validate eCheck payment tokens.
	 *
	 * These fields are required by all eCheck payment tokens:
	 * last4  - string Last 4 digits of the check
	 *
	 * @since 2.6.0
	 * @return boolean True if the passed data is valid
	 */
	public function validate() {
		if ( false === parent::validate() ) {
			return false;
		}

		if ( ! $this->get_last4( 'edit' ) ) {
			return false;
		}
		return true;
	}

	/**
	 * Returns the last four digits.
	 *
	 * @since  2.6.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string Last 4 digits
	 */
	public function get_last4( $context = 'view' ) {
		return $this->get_prop( 'last4', $context );
	}

	/**
	 * Set the last four digits.
	 *
	 * @since 2.6.0
	 * @param string $last4 eCheck last four digits.
	 */
	public function set_last4( $last4 ) {
		$this->set_prop( 'last4', $last4 );
	}
}
class-wc-product-variable.php000064400000053006151336044760012247 0ustar00<?php
/**
 * Variable Product
 *
 * The WooCommerce product class handles individual product data.
 *
 * @version 3.0.0
 * @package WooCommerce\Classes\Products
 */

defined( 'ABSPATH' ) || exit;

/**
 * Variable product class.
 */
class WC_Product_Variable extends WC_Product {

	/**
	 * Array of children variation IDs. Determined by children.
	 *
	 * @var array
	 */
	protected $children = null;

	/**
	 * Array of visible children variation IDs. Determined by children.
	 *
	 * @var array
	 */
	protected $visible_children = null;

	/**
	 * Array of variation attributes IDs. Determined by children.
	 *
	 * @var array
	 */
	protected $variation_attributes = null;

	/**
	 * Get internal type.
	 *
	 * @return string
	 */
	public function get_type() {
		return 'variable';
	}

	/*
	|--------------------------------------------------------------------------
	| Getters
	|--------------------------------------------------------------------------
	*/

	/**
	 * Get the add to cart button text.
	 *
	 * @return string
	 */
	public function add_to_cart_text() {
		return apply_filters( 'woocommerce_product_add_to_cart_text', $this->is_purchasable() ? __( 'Select options', 'woocommerce' ) : __( 'Read more', 'woocommerce' ), $this );
	}

	/**
	 * Get the add to cart button text description - used in aria tags.
	 *
	 * @since 3.3.0
	 * @return string
	 */
	public function add_to_cart_description() {
		/* translators: %s: Product title */
		return apply_filters( 'woocommerce_product_add_to_cart_description', sprintf( __( 'Select options for &ldquo;%s&rdquo;', 'woocommerce' ), $this->get_name() ), $this );
	}

	/**
	 * Get an array of all sale and regular prices from all variations. This is used for example when displaying the price range at variable product level or seeing if the variable product is on sale.
	 *
	 * @param  bool $for_display If true, prices will be adapted for display based on the `woocommerce_tax_display_shop` setting (including or excluding taxes).
	 * @return array Array of RAW prices, regular prices, and sale prices with keys set to variation ID.
	 */
	public function get_variation_prices( $for_display = false ) {
		$prices = $this->data_store->read_price_data( $this, $for_display );

		foreach ( $prices as $price_key => $variation_prices ) {
			$prices[ $price_key ] = $this->sort_variation_prices( $variation_prices );
		}

		return $prices;
	}

	/**
	 * Get the min or max variation regular price.
	 *
	 * @param  string  $min_or_max Min or max price.
	 * @param  boolean $for_display If true, prices will be adapted for display based on the `woocommerce_tax_display_shop` setting (including or excluding taxes).
	 * @return string
	 */
	public function get_variation_regular_price( $min_or_max = 'min', $for_display = false ) {
		$prices = $this->get_variation_prices( $for_display );
		$price  = 'min' === $min_or_max ? current( $prices['regular_price'] ) : end( $prices['regular_price'] );

		return apply_filters( 'woocommerce_get_variation_regular_price', $price, $this, $min_or_max, $for_display );
	}

	/**
	 * Get the min or max variation sale price.
	 *
	 * @param  string  $min_or_max Min or max price.
	 * @param  boolean $for_display If true, prices will be adapted for display based on the `woocommerce_tax_display_shop` setting (including or excluding taxes).
	 * @return string
	 */
	public function get_variation_sale_price( $min_or_max = 'min', $for_display = false ) {
		$prices = $this->get_variation_prices( $for_display );
		$price  = 'min' === $min_or_max ? current( $prices['sale_price'] ) : end( $prices['sale_price'] );

		return apply_filters( 'woocommerce_get_variation_sale_price', $price, $this, $min_or_max, $for_display );
	}

	/**
	 * Get the min or max variation (active) price.
	 *
	 * @param  string  $min_or_max Min or max price.
	 * @param  boolean $for_display If true, prices will be adapted for display based on the `woocommerce_tax_display_shop` setting (including or excluding taxes).
	 * @return string
	 */
	public function get_variation_price( $min_or_max = 'min', $for_display = false ) {
		$prices = $this->get_variation_prices( $for_display );
		$price  = 'min' === $min_or_max ? current( $prices['price'] ) : end( $prices['price'] );

		return apply_filters( 'woocommerce_get_variation_price', $price, $this, $min_or_max, $for_display );
	}

	/**
	 * Returns the price in html format.
	 *
	 * Note: Variable prices do not show suffixes like other product types. This
	 * is due to some things like tax classes being set at variation level which
	 * could differ from the parent price. The only way to show accurate prices
	 * would be to load the variation and get it's price, which adds extra
	 * overhead and still has edge cases where the values would be inaccurate.
	 *
	 * Additionally, ranges of prices no longer show 'striked out' sale prices
	 * due to the strings being very long and unclear/confusing. A single range
	 * is shown instead.
	 *
	 * @param string $price Price (default: '').
	 * @return string
	 */
	public function get_price_html( $price = '' ) {
		$prices = $this->get_variation_prices( true );

		if ( empty( $prices['price'] ) ) {
			$price = apply_filters( 'woocommerce_variable_empty_price_html', '', $this );
		} else {
			$min_price     = current( $prices['price'] );
			$max_price     = end( $prices['price'] );
			$min_reg_price = current( $prices['regular_price'] );
			$max_reg_price = end( $prices['regular_price'] );

			if ( $min_price !== $max_price ) {
				$price = wc_format_price_range( $min_price, $max_price );
			} elseif ( $this->is_on_sale() && $min_reg_price === $max_reg_price ) {
				$price = wc_format_sale_price( wc_price( $max_reg_price ), wc_price( $min_price ) );
			} else {
				$price = wc_price( $min_price );
			}

			$price = apply_filters( 'woocommerce_variable_price_html', $price . $this->get_price_suffix(), $this );
		}

		return apply_filters( 'woocommerce_get_price_html', $price, $this );
	}

	/**
	 * Get the suffix to display after prices > 0.
	 *
	 * This is skipped if the suffix
	 * has dynamic values such as {price_excluding_tax} for variable products.
	 *
	 * @see get_price_html for an explanation as to why.
	 * @param  string  $price Price to calculate, left blank to just use get_price().
	 * @param  integer $qty   Quantity passed on to get_price_including_tax() or get_price_excluding_tax().
	 * @return string
	 */
	public function get_price_suffix( $price = '', $qty = 1 ) {
		$suffix = get_option( 'woocommerce_price_display_suffix' );

		if ( strstr( $suffix, '{' ) ) {
			return apply_filters( 'woocommerce_get_price_suffix', '', $this, $price, $qty );
		} else {
			return parent::get_price_suffix( $price, $qty );
		}
	}

	/**
	 * Return a products child ids.
	 *
	 * This is lazy loaded as it's not used often and does require several queries.
	 *
	 * @param bool|string $visible_only Visible only.
	 * @return array Children ids
	 */
	public function get_children( $visible_only = '' ) {
		if ( is_bool( $visible_only ) ) {
			wc_deprecated_argument( 'visible_only', '3.0', 'WC_Product_Variable::get_visible_children' );

			return $visible_only ? $this->get_visible_children() : $this->get_children();
		}

		if ( null === $this->children ) {
			$children = $this->data_store->read_children( $this );
			$this->set_children( $children['all'] );
			$this->set_visible_children( $children['visible'] );
		}

		return apply_filters( 'woocommerce_get_children', $this->children, $this, false );
	}

	/**
	 * Return a products child ids - visible only.
	 *
	 * This is lazy loaded as it's not used often and does require several queries.
	 *
	 * @since 3.0.0
	 * @return array Children ids
	 */
	public function get_visible_children() {
		if ( null === $this->visible_children ) {
			$children = $this->data_store->read_children( $this );
			$this->set_children( $children['all'] );
			$this->set_visible_children( $children['visible'] );
		}
		return apply_filters( 'woocommerce_get_children', $this->visible_children, $this, true );
	}

	/**
	 * Return an array of attributes used for variations, as well as their possible values.
	 *
	 * This is lazy loaded as it's not used often and does require several queries.
	 *
	 * @return array Attributes and their available values
	 */
	public function get_variation_attributes() {
		if ( null === $this->variation_attributes ) {
			$this->variation_attributes = $this->data_store->read_variation_attributes( $this );
		}
		return $this->variation_attributes;
	}

	/**
	 * If set, get the default attributes for a variable product.
	 *
	 * @param string $attribute_name Attribute name.
	 * @return string
	 */
	public function get_variation_default_attribute( $attribute_name ) {
		$defaults       = $this->get_default_attributes();
		$attribute_name = sanitize_title( $attribute_name );

		return isset( $defaults[ $attribute_name ] ) ? $defaults[ $attribute_name ] : '';
	}

	/**
	 * Variable products themselves cannot be downloadable.
	 *
	 * @param string $context What the value is for. Valid values are view and edit.
	 * @return bool
	 */
	public function get_downloadable( $context = 'view' ) {
		return false;
	}

	/**
	 * Variable products themselves cannot be virtual.
	 *
	 * @param string $context What the value is for. Valid values are view and edit.
	 * @return bool
	 */
	public function get_virtual( $context = 'view' ) {
		return false;
	}

	/**
	 * Get an array of available variations for the current product.
	 *
	 * @param string $return Optional. The format to return the results in. Can be 'array' to return an array of variation data or 'objects' for the product objects. Default 'array'.
	 *
	 * @return array[]|WC_Product_Variation[]
	 */
	public function get_available_variations( $return = 'array' ) {
		$variation_ids        = $this->get_children();
		$available_variations = array();

		if ( is_callable( '_prime_post_caches' ) ) {
			_prime_post_caches( $variation_ids );
		}

		foreach ( $variation_ids as $variation_id ) {

			$variation = wc_get_product( $variation_id );

			// Hide out of stock variations if 'Hide out of stock items from the catalog' is checked.
			if ( ! $variation || ! $variation->exists() || ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && ! $variation->is_in_stock() ) ) {
				continue;
			}

			// Filter 'woocommerce_hide_invisible_variations' to optionally hide invisible variations (disabled variations and variations with empty price).
			if ( apply_filters( 'woocommerce_hide_invisible_variations', true, $this->get_id(), $variation ) && ! $variation->variation_is_visible() ) {
				continue;
			}

			if ( 'array' === $return ) {
				$available_variations[] = $this->get_available_variation( $variation );
			} else {
				$available_variations[] = $variation;
			}
		}

		if ( 'array' === $return ) {
			$available_variations = array_values( array_filter( $available_variations ) );
		}

		return $available_variations;
	}

	/**
	 * Check if a given variation is currently available.
	 *
	 * @param WC_Product_Variation $variation Variation to check.
	 *
	 * @return bool True if the variation is available, false otherwise.
	 */
	private function variation_is_available( WC_Product_Variation $variation ) {
		// Hide out of stock variations if 'Hide out of stock items from the catalog' is checked.
		if ( ! $variation || ! $variation->exists() || ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && ! $variation->is_in_stock() ) ) {
			return false;
		}

		// Filter 'woocommerce_hide_invisible_variations' to optionally hide invisible variations (disabled variations and variations with empty price).
		if ( apply_filters( 'woocommerce_hide_invisible_variations', true, $this->get_id(), $variation ) && ! $variation->variation_is_visible() ) {
			return false;
		}

		return true;
	}

	/**
	 * Returns an array of data for a variation. Used in the add to cart form.
	 *
	 * @since  2.4.0
	 * @param  WC_Product $variation Variation product object or ID.
	 * @return array|bool
	 */
	public function get_available_variation( $variation ) {
		if ( is_numeric( $variation ) ) {
			$variation = wc_get_product( $variation );
		}
		if ( ! $variation instanceof WC_Product_Variation ) {
			return false;
		}
		// See if prices should be shown for each variation after selection.
		$show_variation_price = apply_filters( 'woocommerce_show_variation_price', $variation->get_price() === '' || $this->get_variation_sale_price( 'min' ) !== $this->get_variation_sale_price( 'max' ) || $this->get_variation_regular_price( 'min' ) !== $this->get_variation_regular_price( 'max' ), $this, $variation );

		return apply_filters(
			'woocommerce_available_variation',
			array(
				'attributes'            => $variation->get_variation_attributes(),
				'availability_html'     => wc_get_stock_html( $variation ),
				'backorders_allowed'    => $variation->backorders_allowed(),
				'dimensions'            => $variation->get_dimensions( false ),
				'dimensions_html'       => wc_format_dimensions( $variation->get_dimensions( false ) ),
				'display_price'         => wc_get_price_to_display( $variation ),
				'display_regular_price' => wc_get_price_to_display( $variation, array( 'price' => $variation->get_regular_price() ) ),
				'image'                 => wc_get_product_attachment_props( $variation->get_image_id() ),
				'image_id'              => $variation->get_image_id(),
				'is_downloadable'       => $variation->is_downloadable(),
				'is_in_stock'           => $variation->is_in_stock(),
				'is_purchasable'        => $variation->is_purchasable(),
				'is_sold_individually'  => $variation->is_sold_individually() ? 'yes' : 'no',
				'is_virtual'            => $variation->is_virtual(),
				'max_qty'               => 0 < $variation->get_max_purchase_quantity() ? $variation->get_max_purchase_quantity() : '',
				'min_qty'               => $variation->get_min_purchase_quantity(),
				'price_html'            => $show_variation_price ? '<span class="price">' . $variation->get_price_html() . '</span>' : '',
				'sku'                   => $variation->get_sku(),
				'variation_description' => wc_format_content( $variation->get_description() ),
				'variation_id'          => $variation->get_id(),
				'variation_is_active'   => $variation->variation_is_active(),
				'variation_is_visible'  => $variation->variation_is_visible(),
				'weight'                => $variation->get_weight(),
				'weight_html'           => wc_format_weight( $variation->get_weight() ),
			),
			$this,
			$variation
		);
	}

	/*
	|--------------------------------------------------------------------------
	| Setters
	|--------------------------------------------------------------------------
	*/

	/**
	 * Sets an array of variation attributes.
	 *
	 * @since 3.0.0
	 * @param array $variation_attributes Attributes list.
	 */
	public function set_variation_attributes( $variation_attributes ) {
		$this->variation_attributes = $variation_attributes;
	}

	/**
	 * Sets an array of children for the product.
	 *
	 * @since 3.0.0
	 * @param array $children Children products.
	 */
	public function set_children( $children ) {
		$this->children = array_filter( wp_parse_id_list( (array) $children ) );
	}

	/**
	 * Sets an array of visible children only.
	 *
	 * @since 3.0.0
	 * @param array $visible_children List of visible children products.
	 */
	public function set_visible_children( $visible_children ) {
		$this->visible_children = array_filter( wp_parse_id_list( (array) $visible_children ) );
	}

	/*
	|--------------------------------------------------------------------------
	| CRUD methods
	|--------------------------------------------------------------------------
	*/

	/**
	 * Ensure properties are set correctly before save.
	 *
	 * @since 3.0.0
	 */
	public function validate_props() {
		parent::validate_props();

		if ( ! $this->get_manage_stock() ) {
			$this->data_store->sync_stock_status( $this );
		}
	}

	/**
	 * Do any extra processing needed before the actual product save
	 * (but after triggering the 'woocommerce_before_..._object_save' action)
	 *
	 * @return mixed A state value that will be passed to after_data_store_save_or_update.
	 */
	protected function before_data_store_save_or_update() {
		// Get names before save.
		$previous_name = $this->data['name'];
		$new_name      = $this->get_name( 'edit' );

		return array(
			'previous_name' => $previous_name,
			'new_name'      => $new_name,
		);
	}

	/**
	 * Do any extra processing needed after the actual product save
	 * (but before triggering the 'woocommerce_after_..._object_save' action)
	 *
	 * @param mixed $state The state object that was returned by before_data_store_save_or_update.
	 */
	protected function after_data_store_save_or_update( $state ) {
		$this->data_store->sync_variation_names( $this, $state['previous_name'], $state['new_name'] );
		$this->data_store->sync_managed_variation_stock_status( $this );
	}

	/*
	|--------------------------------------------------------------------------
	| Conditionals
	|--------------------------------------------------------------------------
	*/

	/**
	 * Returns whether or not the product is on sale.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit. What the value is for. Valid values are view and edit.
	 * @return bool
	 */
	public function is_on_sale( $context = 'view' ) {
		$prices  = $this->get_variation_prices();
		$on_sale = $prices['regular_price'] !== $prices['sale_price'] && $prices['sale_price'] === $prices['price'];

		return 'view' === $context ? apply_filters( 'woocommerce_product_is_on_sale', $on_sale, $this ) : $on_sale;
	}

	/**
	 * Is a child in stock?
	 *
	 * @return boolean
	 */
	public function child_is_in_stock() {
		return $this->data_store->child_is_in_stock( $this );
	}

	/**
	 * Is a child on backorder?
	 *
	 * @since 3.3.0
	 * @return boolean
	 */
	public function child_is_on_backorder() {
		return $this->data_store->child_has_stock_status( $this, 'onbackorder' );
	}

	/**
	 * Does a child have a weight set?
	 *
	 * @return boolean
	 */
	public function child_has_weight() {
		$transient_name = 'wc_child_has_weight_' . $this->get_id();
		$has_weight     = get_transient( $transient_name );

		if ( false === $has_weight ) {
			$has_weight = $this->data_store->child_has_weight( $this );
			set_transient( $transient_name, (int) $has_weight, DAY_IN_SECONDS * 30 );
		}

		return (bool) $has_weight;
	}

	/**
	 * Does a child have dimensions set?
	 *
	 * @return boolean
	 */
	public function child_has_dimensions() {
		$transient_name = 'wc_child_has_dimensions_' . $this->get_id();
		$has_dimension  = get_transient( $transient_name );

		if ( false === $has_dimension ) {
			$has_dimension = $this->data_store->child_has_dimensions( $this );
			set_transient( $transient_name, (int) $has_dimension, DAY_IN_SECONDS * 30 );
		}

		return (bool) $has_dimension;
	}

	/**
	 * Returns whether or not the product has dimensions set.
	 *
	 * @return bool
	 */
	public function has_dimensions() {
		return parent::has_dimensions() || $this->child_has_dimensions();
	}

	/**
	 * Returns whether or not the product has weight set.
	 *
	 * @return bool
	 */
	public function has_weight() {
		return parent::has_weight() || $this->child_has_weight();
	}

	/**
	 * Returns whether or not the product has additional options that need
	 * selecting before adding to cart.
	 *
	 * @since  3.0.0
	 * @return boolean
	 */
	public function has_options() {
		return apply_filters( 'woocommerce_product_has_options', true, $this );
	}


	/*
	|--------------------------------------------------------------------------
	| Sync with child variations.
	|--------------------------------------------------------------------------
	*/

	/**
	 * Sync a variable product with it's children. These sync functions sync
	 * upwards (from child to parent) when the variation is saved.
	 *
	 * @param WC_Product|int $product Product object or ID for which you wish to sync.
	 * @param bool           $save If true, the product object will be saved to the DB before returning it.
	 * @return WC_Product Synced product object.
	 */
	public static function sync( $product, $save = true ) {
		if ( ! is_a( $product, 'WC_Product' ) ) {
			$product = wc_get_product( $product );
		}
		if ( is_a( $product, 'WC_Product_Variable' ) ) {
			$data_store = WC_Data_Store::load( 'product-' . $product->get_type() );
			$data_store->sync_price( $product );
			$data_store->sync_stock_status( $product );
			self::sync_attributes( $product ); // Legacy update of attributes.

			do_action( 'woocommerce_variable_product_sync_data', $product );

			if ( $save ) {
				$product->save();
			}

			wc_do_deprecated_action(
				'woocommerce_variable_product_sync',
				array(
					$product->get_id(),
					$product->get_visible_children(),
				),
				'3.0',
				'woocommerce_variable_product_sync_data, woocommerce_new_product or woocommerce_update_product'
			);
		}

		return $product;
	}

	/**
	 * Sync parent stock status with the status of all children and save.
	 *
	 * @param WC_Product|int $product Product object or ID for which you wish to sync.
	 * @param bool           $save If true, the product object will be saved to the DB before returning it.
	 * @return WC_Product Synced product object.
	 */
	public static function sync_stock_status( $product, $save = true ) {
		if ( ! is_a( $product, 'WC_Product' ) ) {
			$product = wc_get_product( $product );
		}
		if ( is_a( $product, 'WC_Product_Variable' ) ) {
			$data_store = WC_Data_Store::load( 'product-' . $product->get_type() );
			$data_store->sync_stock_status( $product );

			if ( $save ) {
				$product->save();
			}
		}

		return $product;
	}

	/**
	 * Sort an associative array of $variation_id => $price pairs in order of min and max prices.
	 *
	 * @param array $prices associative array of $variation_id => $price pairs.
	 * @return array
	 */
	protected function sort_variation_prices( $prices ) {
		asort( $prices );

		return $prices;
	}
}
class-woocommerce.php000064400000104142151336044760010712 0ustar00<?php
/**
 * WooCommerce setup
 *
 * @package WooCommerce
 * @since   3.2.0
 */

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Internal\AssignDefaultCategory;
use Automattic\WooCommerce\Internal\DownloadPermissionsAdjuster;
use Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator;
use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore;
use Automattic\WooCommerce\Internal\RestockRefundedItemsAdjuster;
use Automattic\WooCommerce\Proxies\LegacyProxy;

/**
 * Main WooCommerce Class.
 *
 * @class WooCommerce
 */
final class WooCommerce {

	/**
	 * WooCommerce version.
	 *
	 * @var string
	 */
	public $version = '5.9.1';

	/**
	 * WooCommerce Schema version.
	 *
	 * @since 4.3 started with version string 430.
	 *
	 * @var string
	 */
	public $db_version = '430';

	/**
	 * The single instance of the class.
	 *
	 * @var WooCommerce
	 * @since 2.1
	 */
	protected static $_instance = null;

	/**
	 * Session instance.
	 *
	 * @var WC_Session|WC_Session_Handler
	 */
	public $session = null;

	/**
	 * Query instance.
	 *
	 * @var WC_Query
	 */
	public $query = null;

	/**
	 * Product factory instance.
	 *
	 * @var WC_Product_Factory
	 */
	public $product_factory = null;

	/**
	 * Countries instance.
	 *
	 * @var WC_Countries
	 */
	public $countries = null;

	/**
	 * Integrations instance.
	 *
	 * @var WC_Integrations
	 */
	public $integrations = null;

	/**
	 * Cart instance.
	 *
	 * @var WC_Cart
	 */
	public $cart = null;

	/**
	 * Customer instance.
	 *
	 * @var WC_Customer
	 */
	public $customer = null;

	/**
	 * Order factory instance.
	 *
	 * @var WC_Order_Factory
	 */
	public $order_factory = null;

	/**
	 * Structured data instance.
	 *
	 * @var WC_Structured_Data
	 */
	public $structured_data = null;

	/**
	 * Array of deprecated hook handlers.
	 *
	 * @var array of WC_Deprecated_Hooks
	 */
	public $deprecated_hook_handlers = array();

	/**
	 * Main WooCommerce Instance.
	 *
	 * Ensures only one instance of WooCommerce is loaded or can be loaded.
	 *
	 * @since 2.1
	 * @static
	 * @see WC()
	 * @return WooCommerce - Main instance.
	 */
	public static function instance() {
		if ( is_null( self::$_instance ) ) {
			self::$_instance = new self();
		}
		return self::$_instance;
	}

	/**
	 * Cloning is forbidden.
	 *
	 * @since 2.1
	 */
	public function __clone() {
		wc_doing_it_wrong( __FUNCTION__, __( 'Cloning is forbidden.', 'woocommerce' ), '2.1' );
	}

	/**
	 * Unserializing instances of this class is forbidden.
	 *
	 * @since 2.1
	 */
	public function __wakeup() {
		wc_doing_it_wrong( __FUNCTION__, __( 'Unserializing instances of this class is forbidden.', 'woocommerce' ), '2.1' );
	}

	/**
	 * Auto-load in-accessible properties on demand.
	 *
	 * @param mixed $key Key name.
	 * @return mixed
	 */
	public function __get( $key ) {
		if ( in_array( $key, array( 'payment_gateways', 'shipping', 'mailer', 'checkout' ), true ) ) {
			return $this->$key();
		}
	}

	/**
	 * WooCommerce Constructor.
	 */
	public function __construct() {
		$this->define_constants();
		$this->define_tables();
		$this->includes();
		$this->init_hooks();
	}

	/**
	 * When WP has loaded all plugins, trigger the `woocommerce_loaded` hook.
	 *
	 * This ensures `woocommerce_loaded` is called only after all other plugins
	 * are loaded, to avoid issues caused by plugin directory naming changing
	 * the load order. See #21524 for details.
	 *
	 * @since 3.6.0
	 */
	public function on_plugins_loaded() {
		do_action( 'woocommerce_loaded' );
	}

	/**
	 * Hook into actions and filters.
	 *
	 * @since 2.3
	 */
	private function init_hooks() {
		register_activation_hook( WC_PLUGIN_FILE, array( 'WC_Install', 'install' ) );
		register_shutdown_function( array( $this, 'log_errors' ) );

		add_action( 'plugins_loaded', array( $this, 'on_plugins_loaded' ), -1 );
		add_action( 'admin_notices', array( $this, 'build_dependencies_notice' ) );
		add_action( 'after_setup_theme', array( $this, 'setup_environment' ) );
		add_action( 'after_setup_theme', array( $this, 'include_template_functions' ), 11 );
		add_action( 'init', array( $this, 'init' ), 0 );
		add_action( 'init', array( 'WC_Shortcodes', 'init' ) );
		add_action( 'init', array( 'WC_Emails', 'init_transactional_emails' ) );
		add_action( 'init', array( $this, 'add_image_sizes' ) );
		add_action( 'init', array( $this, 'load_rest_api' ) );
		add_action( 'switch_blog', array( $this, 'wpdb_table_fix' ), 0 );
		add_action( 'activated_plugin', array( $this, 'activated_plugin' ) );
		add_action( 'deactivated_plugin', array( $this, 'deactivated_plugin' ) );
		add_action( 'woocommerce_installed', array( $this, 'add_woocommerce_inbox_variant' ) );
		add_action( 'woocommerce_updated', array( $this, 'add_woocommerce_inbox_variant' ) );

		// These classes set up hooks on instantiation.
		wc_get_container()->get( DownloadPermissionsAdjuster::class );
		wc_get_container()->get( AssignDefaultCategory::class );
		wc_get_container()->get( DataRegenerator::class );
		wc_get_container()->get( LookupDataStore::class );
		wc_get_container()->get( RestockRefundedItemsAdjuster::class );
	}

	/**
	 * Add woocommerce_inbox_variant for the Remote Inbox Notification.
	 *
	 * P2 post can be found at https://wp.me/paJDYF-1uJ.
	 */
	public function add_woocommerce_inbox_variant() {
		$config_name = 'woocommerce_inbox_variant_assignment';
		if ( false === get_option( $config_name, false ) ) {
			update_option( $config_name, wp_rand( 1, 12 ) );
		}
	}
	/**
	 * Ensures fatal errors are logged so they can be picked up in the status report.
	 *
	 * @since 3.2.0
	 */
	public function log_errors() {
		$error = error_get_last();
		if ( $error && in_array( $error['type'], array( E_ERROR, E_PARSE, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR ), true ) ) {
			$logger = wc_get_logger();
			$logger->critical(
				/* translators: 1: error message 2: file name and path 3: line number */
				sprintf( __( '%1$s in %2$s on line %3$s', 'woocommerce' ), $error['message'], $error['file'], $error['line'] ) . PHP_EOL,
				array(
					'source' => 'fatal-errors',
				)
			);
			do_action( 'woocommerce_shutdown_error', $error );
		}
	}

	/**
	 * Define WC Constants.
	 */
	private function define_constants() {
		$upload_dir = wp_upload_dir( null, false );

		$this->define( 'WC_ABSPATH', dirname( WC_PLUGIN_FILE ) . '/' );
		$this->define( 'WC_PLUGIN_BASENAME', plugin_basename( WC_PLUGIN_FILE ) );
		$this->define( 'WC_VERSION', $this->version );
		$this->define( 'WOOCOMMERCE_VERSION', $this->version );
		$this->define( 'WC_ROUNDING_PRECISION', 6 );
		$this->define( 'WC_DISCOUNT_ROUNDING_MODE', 2 );
		$this->define( 'WC_TAX_ROUNDING_MODE', 'yes' === get_option( 'woocommerce_prices_include_tax', 'no' ) ? 2 : 1 );
		$this->define( 'WC_DELIMITER', '|' );
		$this->define( 'WC_LOG_DIR', $upload_dir['basedir'] . '/wc-logs/' );
		$this->define( 'WC_SESSION_CACHE_GROUP', 'wc_session_id' );
		$this->define( 'WC_TEMPLATE_DEBUG_MODE', false );
		$this->define( 'WC_NOTICE_MIN_PHP_VERSION', '7.2' );
		$this->define( 'WC_NOTICE_MIN_WP_VERSION', '5.2' );
		$this->define( 'WC_PHP_MIN_REQUIREMENTS_NOTICE', 'wp_php_min_requirements_' . WC_NOTICE_MIN_PHP_VERSION . '_' . WC_NOTICE_MIN_WP_VERSION );
		/** Define if we're checking against major, minor or no versions in the following places:
		 *   - plugin screen in WP Admin (displaying extra warning when updating to new major versions)
		 *   - System Status Report ('Installed version not tested with active version of WooCommerce' warning)
		 *   - core update screen in WP Admin (displaying extra warning when updating to new major versions)
		 *   - enable/disable automated updates in the plugin screen in WP Admin (if there are any plugins
		 *      that don't declare compatibility, the auto-update is disabled)
		 *
		 * We dropped SemVer before WC 5.0, so all versions are backwards compatible now, thus no more check needed.
		 * The SSR in the name is preserved for bw compatibility, as this was initially used in System Status Report.
		 */
		$this->define( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE', 'none' );

	}

	/**
	 * Register custom tables within $wpdb object.
	 */
	private function define_tables() {
		global $wpdb;

		// List of tables without prefixes.
		$tables = array(
			'payment_tokenmeta'      => 'woocommerce_payment_tokenmeta',
			'order_itemmeta'         => 'woocommerce_order_itemmeta',
			'wc_product_meta_lookup' => 'wc_product_meta_lookup',
			'wc_tax_rate_classes'    => 'wc_tax_rate_classes',
			'wc_reserved_stock'      => 'wc_reserved_stock',
		);

		foreach ( $tables as $name => $table ) {
			$wpdb->$name    = $wpdb->prefix . $table;
			$wpdb->tables[] = $table;
		}
	}

	/**
	 * Define constant if not already set.
	 *
	 * @param string      $name  Constant name.
	 * @param string|bool $value Constant value.
	 */
	private function define( $name, $value ) {
		if ( ! defined( $name ) ) {
			define( $name, $value );
		}
	}

	/**
	 * Returns true if the request is a non-legacy REST API request.
	 *
	 * Legacy REST requests should still run some extra code for backwards compatibility.
	 *
	 * @todo: replace this function once core WP function is available: https://core.trac.wordpress.org/ticket/42061.
	 *
	 * @return bool
	 */
	public function is_rest_api_request() {
		if ( empty( $_SERVER['REQUEST_URI'] ) ) {
			return false;
		}

		$rest_prefix         = trailingslashit( rest_get_url_prefix() );
		$is_rest_api_request = ( false !== strpos( $_SERVER['REQUEST_URI'], $rest_prefix ) ); // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

		return apply_filters( 'woocommerce_is_rest_api_request', $is_rest_api_request );
	}

	/**
	 * Load REST API.
	 */
	public function load_rest_api() {
		\Automattic\WooCommerce\RestApi\Server::instance()->init();
	}

	/**
	 * What type of request is this?
	 *
	 * @param  string $type admin, ajax, cron or frontend.
	 * @return bool
	 */
	private function is_request( $type ) {
		switch ( $type ) {
			case 'admin':
				return is_admin();
			case 'ajax':
				return defined( 'DOING_AJAX' );
			case 'cron':
				return defined( 'DOING_CRON' );
			case 'frontend':
				return ( ! is_admin() || defined( 'DOING_AJAX' ) ) && ! defined( 'DOING_CRON' ) && ! $this->is_rest_api_request();
		}
	}

	/**
	 * Include required core files used in admin and on the frontend.
	 */
	public function includes() {
		/**
		 * Class autoloader.
		 */
		include_once WC_ABSPATH . 'includes/class-wc-autoloader.php';

		/**
		 * Interfaces.
		 */
		include_once WC_ABSPATH . 'includes/interfaces/class-wc-abstract-order-data-store-interface.php';
		include_once WC_ABSPATH . 'includes/interfaces/class-wc-coupon-data-store-interface.php';
		include_once WC_ABSPATH . 'includes/interfaces/class-wc-customer-data-store-interface.php';
		include_once WC_ABSPATH . 'includes/interfaces/class-wc-customer-download-data-store-interface.php';
		include_once WC_ABSPATH . 'includes/interfaces/class-wc-customer-download-log-data-store-interface.php';
		include_once WC_ABSPATH . 'includes/interfaces/class-wc-object-data-store-interface.php';
		include_once WC_ABSPATH . 'includes/interfaces/class-wc-order-data-store-interface.php';
		include_once WC_ABSPATH . 'includes/interfaces/class-wc-order-item-data-store-interface.php';
		include_once WC_ABSPATH . 'includes/interfaces/class-wc-order-item-product-data-store-interface.php';
		include_once WC_ABSPATH . 'includes/interfaces/class-wc-order-item-type-data-store-interface.php';
		include_once WC_ABSPATH . 'includes/interfaces/class-wc-order-refund-data-store-interface.php';
		include_once WC_ABSPATH . 'includes/interfaces/class-wc-payment-token-data-store-interface.php';
		include_once WC_ABSPATH . 'includes/interfaces/class-wc-product-data-store-interface.php';
		include_once WC_ABSPATH . 'includes/interfaces/class-wc-product-variable-data-store-interface.php';
		include_once WC_ABSPATH . 'includes/interfaces/class-wc-shipping-zone-data-store-interface.php';
		include_once WC_ABSPATH . 'includes/interfaces/class-wc-logger-interface.php';
		include_once WC_ABSPATH . 'includes/interfaces/class-wc-log-handler-interface.php';
		include_once WC_ABSPATH . 'includes/interfaces/class-wc-webhooks-data-store-interface.php';
		include_once WC_ABSPATH . 'includes/interfaces/class-wc-queue-interface.php';

		/**
		 * Core traits.
		 */
		include_once WC_ABSPATH . 'includes/traits/trait-wc-item-totals.php';

		/**
		 * Abstract classes.
		 */
		include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-data.php';
		include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-object-query.php';
		include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-payment-token.php';
		include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-product.php';
		include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-order.php';
		include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-settings-api.php';
		include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-shipping-method.php';
		include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-payment-gateway.php';
		include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-integration.php';
		include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-log-handler.php';
		include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-deprecated-hooks.php';
		include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-session.php';
		include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-privacy.php';

		/**
		 * Core classes.
		 */
		include_once WC_ABSPATH . 'includes/wc-core-functions.php';
		include_once WC_ABSPATH . 'includes/class-wc-datetime.php';
		include_once WC_ABSPATH . 'includes/class-wc-post-types.php';
		include_once WC_ABSPATH . 'includes/class-wc-install.php';
		include_once WC_ABSPATH . 'includes/class-wc-geolocation.php';
		include_once WC_ABSPATH . 'includes/class-wc-download-handler.php';
		include_once WC_ABSPATH . 'includes/class-wc-comments.php';
		include_once WC_ABSPATH . 'includes/class-wc-post-data.php';
		include_once WC_ABSPATH . 'includes/class-wc-ajax.php';
		include_once WC_ABSPATH . 'includes/class-wc-emails.php';
		include_once WC_ABSPATH . 'includes/class-wc-data-exception.php';
		include_once WC_ABSPATH . 'includes/class-wc-query.php';
		include_once WC_ABSPATH . 'includes/class-wc-meta-data.php';
		include_once WC_ABSPATH . 'includes/class-wc-order-factory.php';
		include_once WC_ABSPATH . 'includes/class-wc-order-query.php';
		include_once WC_ABSPATH . 'includes/class-wc-product-factory.php';
		include_once WC_ABSPATH . 'includes/class-wc-product-query.php';
		include_once WC_ABSPATH . 'includes/class-wc-payment-tokens.php';
		include_once WC_ABSPATH . 'includes/class-wc-shipping-zone.php';
		include_once WC_ABSPATH . 'includes/gateways/class-wc-payment-gateway-cc.php';
		include_once WC_ABSPATH . 'includes/gateways/class-wc-payment-gateway-echeck.php';
		include_once WC_ABSPATH . 'includes/class-wc-countries.php';
		include_once WC_ABSPATH . 'includes/class-wc-integrations.php';
		include_once WC_ABSPATH . 'includes/class-wc-cache-helper.php';
		include_once WC_ABSPATH . 'includes/class-wc-https.php';
		include_once WC_ABSPATH . 'includes/class-wc-deprecated-action-hooks.php';
		include_once WC_ABSPATH . 'includes/class-wc-deprecated-filter-hooks.php';
		include_once WC_ABSPATH . 'includes/class-wc-background-emailer.php';
		include_once WC_ABSPATH . 'includes/class-wc-discounts.php';
		include_once WC_ABSPATH . 'includes/class-wc-cart-totals.php';
		include_once WC_ABSPATH . 'includes/customizer/class-wc-shop-customizer.php';
		include_once WC_ABSPATH . 'includes/class-wc-regenerate-images.php';
		include_once WC_ABSPATH . 'includes/class-wc-privacy.php';
		include_once WC_ABSPATH . 'includes/class-wc-structured-data.php';
		include_once WC_ABSPATH . 'includes/class-wc-shortcodes.php';
		include_once WC_ABSPATH . 'includes/class-wc-logger.php';
		include_once WC_ABSPATH . 'includes/queue/class-wc-action-queue.php';
		include_once WC_ABSPATH . 'includes/queue/class-wc-queue.php';
		include_once WC_ABSPATH . 'includes/admin/marketplace-suggestions/class-wc-marketplace-updater.php';
		include_once WC_ABSPATH . 'includes/blocks/class-wc-blocks-utils.php';

		/**
		 * Data stores - used to store and retrieve CRUD object data from the database.
		 */
		include_once WC_ABSPATH . 'includes/class-wc-data-store.php';
		include_once WC_ABSPATH . 'includes/data-stores/class-wc-data-store-wp.php';
		include_once WC_ABSPATH . 'includes/data-stores/class-wc-coupon-data-store-cpt.php';
		include_once WC_ABSPATH . 'includes/data-stores/class-wc-product-data-store-cpt.php';
		include_once WC_ABSPATH . 'includes/data-stores/class-wc-product-grouped-data-store-cpt.php';
		include_once WC_ABSPATH . 'includes/data-stores/class-wc-product-variable-data-store-cpt.php';
		include_once WC_ABSPATH . 'includes/data-stores/class-wc-product-variation-data-store-cpt.php';
		include_once WC_ABSPATH . 'includes/data-stores/abstract-wc-order-item-type-data-store.php';
		include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-data-store.php';
		include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-coupon-data-store.php';
		include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-fee-data-store.php';
		include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-product-data-store.php';
		include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-shipping-data-store.php';
		include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-tax-data-store.php';
		include_once WC_ABSPATH . 'includes/data-stores/class-wc-payment-token-data-store.php';
		include_once WC_ABSPATH . 'includes/data-stores/class-wc-customer-data-store.php';
		include_once WC_ABSPATH . 'includes/data-stores/class-wc-customer-data-store-session.php';
		include_once WC_ABSPATH . 'includes/data-stores/class-wc-customer-download-data-store.php';
		include_once WC_ABSPATH . 'includes/data-stores/class-wc-customer-download-log-data-store.php';
		include_once WC_ABSPATH . 'includes/data-stores/class-wc-shipping-zone-data-store.php';
		include_once WC_ABSPATH . 'includes/data-stores/abstract-wc-order-data-store-cpt.php';
		include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-data-store-cpt.php';
		include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-refund-data-store-cpt.php';
		include_once WC_ABSPATH . 'includes/data-stores/class-wc-webhook-data-store.php';

		/**
		 * REST API.
		 */
		include_once WC_ABSPATH . 'includes/legacy/class-wc-legacy-api.php';
		include_once WC_ABSPATH . 'includes/class-wc-api.php';
		include_once WC_ABSPATH . 'includes/class-wc-rest-authentication.php';
		include_once WC_ABSPATH . 'includes/class-wc-rest-exception.php';
		include_once WC_ABSPATH . 'includes/class-wc-auth.php';
		include_once WC_ABSPATH . 'includes/class-wc-register-wp-admin-settings.php';

		/**
		 * WCCOM Site.
		 */
		include_once WC_ABSPATH . 'includes/wccom-site/class-wc-wccom-site.php';

		/**
		 * Libraries and packages.
		 */
		include_once WC_ABSPATH . 'packages/action-scheduler/action-scheduler.php';

		if ( defined( 'WP_CLI' ) && WP_CLI ) {
			include_once WC_ABSPATH . 'includes/class-wc-cli.php';
		}

		if ( $this->is_request( 'admin' ) ) {
			include_once WC_ABSPATH . 'includes/admin/class-wc-admin.php';
		}

		if ( $this->is_request( 'frontend' ) ) {
			$this->frontend_includes();
		}

		if ( $this->is_request( 'cron' ) && 'yes' === get_option( 'woocommerce_allow_tracking', 'no' ) ) {
			include_once WC_ABSPATH . 'includes/class-wc-tracker.php';
		}

		$this->theme_support_includes();
		$this->query = new WC_Query();
		$this->api   = new WC_API();
		$this->api->init();
	}

	/**
	 * Include classes for theme support.
	 *
	 * @since 3.3.0
	 */
	private function theme_support_includes() {
		if ( wc_is_wp_default_theme_active() ) {
			switch ( get_template() ) {
				case 'twentyten':
					include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-ten.php';
					break;
				case 'twentyeleven':
					include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-eleven.php';
					break;
				case 'twentytwelve':
					include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-twelve.php';
					break;
				case 'twentythirteen':
					include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-thirteen.php';
					break;
				case 'twentyfourteen':
					include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-fourteen.php';
					break;
				case 'twentyfifteen':
					include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-fifteen.php';
					break;
				case 'twentysixteen':
					include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-sixteen.php';
					break;
				case 'twentyseventeen':
					include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-seventeen.php';
					break;
				case 'twentynineteen':
					include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-nineteen.php';
					break;
				case 'twentytwenty':
					include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-twenty.php';
					break;
				case 'twentytwentyone':
					include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-twenty-one.php';
					break;
			}
		}
	}

	/**
	 * Include required frontend files.
	 */
	public function frontend_includes() {
		include_once WC_ABSPATH . 'includes/wc-cart-functions.php';
		include_once WC_ABSPATH . 'includes/wc-notice-functions.php';
		include_once WC_ABSPATH . 'includes/wc-template-hooks.php';
		include_once WC_ABSPATH . 'includes/class-wc-template-loader.php';
		include_once WC_ABSPATH . 'includes/class-wc-frontend-scripts.php';
		include_once WC_ABSPATH . 'includes/class-wc-form-handler.php';
		include_once WC_ABSPATH . 'includes/class-wc-cart.php';
		include_once WC_ABSPATH . 'includes/class-wc-tax.php';
		include_once WC_ABSPATH . 'includes/class-wc-shipping-zones.php';
		include_once WC_ABSPATH . 'includes/class-wc-customer.php';
		include_once WC_ABSPATH . 'includes/class-wc-embed.php';
		include_once WC_ABSPATH . 'includes/class-wc-session-handler.php';
	}

	/**
	 * Function used to Init WooCommerce Template Functions - This makes them pluggable by plugins and themes.
	 */
	public function include_template_functions() {
		include_once WC_ABSPATH . 'includes/wc-template-functions.php';
	}

	/**
	 * Init WooCommerce when WordPress Initialises.
	 */
	public function init() {
		// Before init action.
		do_action( 'before_woocommerce_init' );

		// Set up localisation.
		$this->load_plugin_textdomain();

		// Load class instances.
		$this->product_factory                     = new WC_Product_Factory();
		$this->order_factory                       = new WC_Order_Factory();
		$this->countries                           = new WC_Countries();
		$this->integrations                        = new WC_Integrations();
		$this->structured_data                     = new WC_Structured_Data();
		$this->deprecated_hook_handlers['actions'] = new WC_Deprecated_Action_Hooks();
		$this->deprecated_hook_handlers['filters'] = new WC_Deprecated_Filter_Hooks();

		// Classes/actions loaded for the frontend and for ajax requests.
		if ( $this->is_request( 'frontend' ) ) {
			wc_load_cart();
		}

		$this->load_webhooks();

		// Init action.
		do_action( 'woocommerce_init' );
	}

	/**
	 * Load Localisation files.
	 *
	 * Note: the first-loaded translation file overrides any following ones if the same translation is present.
	 *
	 * Locales found in:
	 *      - WP_LANG_DIR/woocommerce/woocommerce-LOCALE.mo
	 *      - WP_LANG_DIR/plugins/woocommerce-LOCALE.mo
	 */
	public function load_plugin_textdomain() {
		$locale = determine_locale();
		$locale = apply_filters( 'plugin_locale', $locale, 'woocommerce' );

		unload_textdomain( 'woocommerce' );
		load_textdomain( 'woocommerce', WP_LANG_DIR . '/woocommerce/woocommerce-' . $locale . '.mo' );
		load_plugin_textdomain( 'woocommerce', false, plugin_basename( dirname( WC_PLUGIN_FILE ) ) . '/i18n/languages' );
	}

	/**
	 * Ensure theme and server variable compatibility and setup image sizes.
	 */
	public function setup_environment() {
		/**
		 * WC_TEMPLATE_PATH constant.
		 *
		 * @deprecated 2.2 Use WC()->template_path() instead.
		 */
		$this->define( 'WC_TEMPLATE_PATH', $this->template_path() );

		$this->add_thumbnail_support();
	}

	/**
	 * Ensure post thumbnail support is turned on.
	 */
	private function add_thumbnail_support() {
		if ( ! current_theme_supports( 'post-thumbnails' ) ) {
			add_theme_support( 'post-thumbnails' );
		}
		add_post_type_support( 'product', 'thumbnail' );
	}

	/**
	 * Add WC Image sizes to WP.
	 *
	 * As of 3.3, image sizes can be registered via themes using add_theme_support for woocommerce
	 * and defining an array of args. If these are not defined, we will use defaults. This is
	 * handled in wc_get_image_size function.
	 *
	 * 3.3 sizes:
	 *
	 * woocommerce_thumbnail - Used in product listings. We assume these work for a 3 column grid layout.
	 * woocommerce_single - Used on single product pages for the main image.
	 *
	 * @since 2.3
	 */
	public function add_image_sizes() {
		$thumbnail         = wc_get_image_size( 'thumbnail' );
		$single            = wc_get_image_size( 'single' );
		$gallery_thumbnail = wc_get_image_size( 'gallery_thumbnail' );

		add_image_size( 'woocommerce_thumbnail', $thumbnail['width'], $thumbnail['height'], $thumbnail['crop'] );
		add_image_size( 'woocommerce_single', $single['width'], $single['height'], $single['crop'] );
		add_image_size( 'woocommerce_gallery_thumbnail', $gallery_thumbnail['width'], $gallery_thumbnail['height'], $gallery_thumbnail['crop'] );

		/**
		 * Legacy image sizes.
		 *
		 * @deprecated 3.3.0 These sizes will be removed in 4.6.0.
		 */
		add_image_size( 'shop_catalog', $thumbnail['width'], $thumbnail['height'], $thumbnail['crop'] );
		add_image_size( 'shop_single', $single['width'], $single['height'], $single['crop'] );
		add_image_size( 'shop_thumbnail', $gallery_thumbnail['width'], $gallery_thumbnail['height'], $gallery_thumbnail['crop'] );
	}

	/**
	 * Get the plugin url.
	 *
	 * @return string
	 */
	public function plugin_url() {
		return untrailingslashit( plugins_url( '/', WC_PLUGIN_FILE ) );
	}

	/**
	 * Get the plugin path.
	 *
	 * @return string
	 */
	public function plugin_path() {
		return untrailingslashit( plugin_dir_path( WC_PLUGIN_FILE ) );
	}

	/**
	 * Get the template path.
	 *
	 * @return string
	 */
	public function template_path() {
		return apply_filters( 'woocommerce_template_path', 'woocommerce/' );
	}

	/**
	 * Get Ajax URL.
	 *
	 * @return string
	 */
	public function ajax_url() {
		return admin_url( 'admin-ajax.php', 'relative' );
	}

	/**
	 * Return the WC API URL for a given request.
	 *
	 * @param string    $request Requested endpoint.
	 * @param bool|null $ssl     If should use SSL, null if should auto detect. Default: null.
	 * @return string
	 */
	public function api_request_url( $request, $ssl = null ) {
		if ( is_null( $ssl ) ) {
			$scheme = wp_parse_url( home_url(), PHP_URL_SCHEME );
		} elseif ( $ssl ) {
			$scheme = 'https';
		} else {
			$scheme = 'http';
		}

		if ( strstr( get_option( 'permalink_structure' ), '/index.php/' ) ) {
			$api_request_url = trailingslashit( home_url( '/index.php/wc-api/' . $request, $scheme ) );
		} elseif ( get_option( 'permalink_structure' ) ) {
			$api_request_url = trailingslashit( home_url( '/wc-api/' . $request, $scheme ) );
		} else {
			$api_request_url = add_query_arg( 'wc-api', $request, trailingslashit( home_url( '', $scheme ) ) );
		}

		return esc_url_raw( apply_filters( 'woocommerce_api_request_url', $api_request_url, $request, $ssl ) );
	}

	/**
	 * Load & enqueue active webhooks.
	 *
	 * @since 2.2
	 */
	private function load_webhooks() {

		if ( ! is_blog_installed() ) {
			return;
		}

		/**
		 * Hook: woocommerce_load_webhooks_limit.
		 *
		 * @since 3.6.0
		 * @param int $limit Used to limit how many webhooks are loaded. Default: no limit.
		 */
		$limit = apply_filters( 'woocommerce_load_webhooks_limit', null );

		wc_load_webhooks( 'active', $limit );
	}

	/**
	 * Initialize the customer and cart objects and setup customer saving on shutdown.
	 *
	 * @since 3.6.4
	 * @return void
	 */
	public function initialize_cart() {
		// Cart needs customer info.
		if ( is_null( $this->customer ) || ! $this->customer instanceof WC_Customer ) {
			$this->customer = new WC_Customer( get_current_user_id(), true );
			// Customer should be saved during shutdown.
			add_action( 'shutdown', array( $this->customer, 'save' ), 10 );
		}
		if ( is_null( $this->cart ) || ! $this->cart instanceof WC_Cart ) {
			$this->cart = new WC_Cart();
		}
	}

	/**
	 * Initialize the session class.
	 *
	 * @since 3.6.4
	 * @return void
	 */
	public function initialize_session() {
		// Session class, handles session data for users - can be overwritten if custom handler is needed.
		$session_class = apply_filters( 'woocommerce_session_handler', 'WC_Session_Handler' );
		if ( is_null( $this->session ) || ! $this->session instanceof $session_class ) {
			$this->session = new $session_class();
			$this->session->init();
		}
	}

	/**
	 * Set tablenames inside WPDB object.
	 */
	public function wpdb_table_fix() {
		$this->define_tables();
	}

	/**
	 * Ran when any plugin is activated.
	 *
	 * @since 3.6.0
	 * @param string $filename The filename of the activated plugin.
	 */
	public function activated_plugin( $filename ) {
		include_once dirname( __FILE__ ) . '/admin/helper/class-wc-helper.php';

		if ( '/woocommerce.php' === substr( $filename, -16 ) ) {
			set_transient( 'woocommerce_activated_plugin', $filename );
		}

		WC_Helper::activated_plugin( $filename );
	}

	/**
	 * Ran when any plugin is deactivated.
	 *
	 * @since 3.6.0
	 * @param string $filename The filename of the deactivated plugin.
	 */
	public function deactivated_plugin( $filename ) {
		include_once dirname( __FILE__ ) . '/admin/helper/class-wc-helper.php';

		WC_Helper::deactivated_plugin( $filename );
	}

	/**
	 * Get queue instance.
	 *
	 * @return WC_Queue_Interface
	 */
	public function queue() {
		return WC_Queue::instance();
	}

	/**
	 * Get Checkout Class.
	 *
	 * @return WC_Checkout
	 */
	public function checkout() {
		return WC_Checkout::instance();
	}

	/**
	 * Get gateways class.
	 *
	 * @return WC_Payment_Gateways
	 */
	public function payment_gateways() {
		return WC_Payment_Gateways::instance();
	}

	/**
	 * Get shipping class.
	 *
	 * @return WC_Shipping
	 */
	public function shipping() {
		return WC_Shipping::instance();
	}

	/**
	 * Email Class.
	 *
	 * @return WC_Emails
	 */
	public function mailer() {
		return WC_Emails::instance();
	}

	/**
	 * Check if plugin assets are built and minified
	 *
	 * @return bool
	 */
	public function build_dependencies_satisfied() {
		// Check if we have compiled CSS.
		if ( ! file_exists( WC()->plugin_path() . '/assets/css/admin.css' ) ) {
			return false;
		}

		// Check if we have minified JS.
		if ( ! file_exists( WC()->plugin_path() . '/assets/js/admin/woocommerce_admin.min.js' ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Output a admin notice when build dependencies not met.
	 *
	 * @return void
	 */
	public function build_dependencies_notice() {
		if ( $this->build_dependencies_satisfied() ) {
			return;
		}

		$message_one = __( 'You have installed a development version of WooCommerce which requires files to be built and minified. From the plugin directory, run <code>grunt assets</code> to build and minify assets.', 'woocommerce' );
		$message_two = sprintf(
			/* translators: 1: URL of WordPress.org Repository 2: URL of the GitHub Repository release page */
			__( 'Or you can download a pre-built version of the plugin from the <a href="%1$s">WordPress.org repository</a> or by visiting <a href="%2$s">the releases page in the GitHub repository</a>.', 'woocommerce' ),
			'https://wordpress.org/plugins/woocommerce/',
			'https://github.com/woocommerce/woocommerce/releases'
		);
		printf( '<div class="error"><p>%s %s</p></div>', $message_one, $message_two ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
	}

	/**
	 * Is the WooCommerce Admin actively included in the WooCommerce core?
	 * Based on presence of a basic WC Admin function.
	 *
	 * @return boolean
	 */
	public function is_wc_admin_active() {
		return function_exists( 'wc_admin_url' );
	}

	/**
	 * Call a user function. This should be used to execute any non-idempotent function, especially
	 * those in the `includes` directory or provided by WordPress.
	 *
	 * This method can be useful for unit tests, since functions called using this method
	 * can be easily mocked by using WC_Unit_Test_Case::register_legacy_proxy_function_mocks.
	 *
	 * @param string $function_name The function to execute.
	 * @param mixed  ...$parameters The parameters to pass to the function.
	 *
	 * @return mixed The result from the function.
	 *
	 * @since 4.4
	 */
	public function call_function( $function_name, ...$parameters ) {
		return wc_get_container()->get( LegacyProxy::class )->call_function( $function_name, ...$parameters );
	}

	/**
	 * Call a static method in a class. This should be used to execute any non-idempotent method in classes
	 * from the `includes` directory.
	 *
	 * This method can be useful for unit tests, since methods called using this method
	 * can be easily mocked by using WC_Unit_Test_Case::register_legacy_proxy_static_mocks.
	 *
	 * @param string $class_name The name of the class containing the method.
	 * @param string $method_name The name of the method.
	 * @param mixed  ...$parameters The parameters to pass to the method.
	 *
	 * @return mixed The result from the method.
	 *
	 * @since 4.4
	 */
	public function call_static( $class_name, $method_name, ...$parameters ) {
		return wc_get_container()->get( LegacyProxy::class )->call_static( $class_name, $method_name, ...$parameters );
	}

	/**
	 * Gets an instance of a given legacy class.
	 * This must not be used to get instances of classes in the `src` directory.
	 *
	 * This method can be useful for unit tests, since objects obtained using this method
	 * can be easily mocked by using WC_Unit_Test_Case::register_legacy_proxy_class_mocks.
	 *
	 * @param string $class_name The name of the class to get an instance for.
	 * @param mixed  ...$args Parameters to be passed to the class constructor or to the appropriate internal 'get_instance_of_' method.
	 *
	 * @return object The instance of the class.
	 * @throws \Exception The requested class belongs to the `src` directory, or there was an error creating an instance of the class.
	 *
	 * @since 4.4
	 */
	public function get_instance_of( string $class_name, ...$args ) {
		return wc_get_container()->get( LegacyProxy::class )->get_instance_of( $class_name, ...$args );
	}
}
cli/class-wc-cli-tracker-command.php000064400000002353151336044760013366 0ustar00<?php
/**
 * WC_CLI_Tracker_Command class file.
 *
 * @package WooCommerce\CLI
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Allows access to tracker snapshot for transparency and debugging.
 *
 * @since 5.5.0
 * @package WooCommerce
 */
class WC_CLI_Tracker_Command {

	/**
	 * Registers a command for showing WooCommerce Tracker snapshot data.
	 */
	public static function register_commands() {
		WP_CLI::add_command( 'wc tracker snapshot', array( 'WC_CLI_Tracker_Command', 'show_tracker_snapshot' ) );
	}

	/**
	 * Dump tracker snapshot data to screen.
	 *
	 * ## EXAMPLES
	 *
	 * wp wc tracker snapshot --format=yaml
	 * wp wc tracker snapshot --format=json
	 *
	 * ## OPTIONS
	 *
	 * [--format=<format>]
	 * : Render output in a particular format, see WP_CLI\Formatter for details.
	 *
	 * @see \WP_CLI\Formatter
	 * @see WC_Tracker::get_tracking_data()
	 * @param array $args WP-CLI positional arguments.
	 * @param array $assoc_args WP-CLI associative arguments.
	 */
	public static function show_tracker_snapshot( $args, $assoc_args ) {
		$snapshot_data = WC_Tracker::get_tracking_data();

		$formatter = new \WP_CLI\Formatter(
			$assoc_args,
			array_keys( $snapshot_data )
		);

		$formatter->display_items( array( $snapshot_data ) );
	}
}
cli/class-wc-cli-tool-command.php000064400000005546151336044760012717 0ustar00<?php
/**
 * WC_CLI_Tool_Command class file.
 *
 * @package WooCommerce\CLI
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Hooks up our system status tools to the CLI.
 *
 * Forked from wp-cli/restful (by Daniel Bachhuber, released under the MIT license https://opensource.org/licenses/MIT).
 * https://github.com/wp-cli/restful
 *
 * @version 3.0.0
 * @package WooCommerce
 */
class WC_CLI_Tool_Command {

	/**
	 * Registers just a 'list' and 'run' command to the WC CLI
	 * since we only want to enable certain actions on the system status
	 * tools endpoints.
	 */
	public static function register_commands() {
		global $wp_rest_server;

		$request       = new WP_REST_Request( 'OPTIONS', '/wc/v2/system_status/tools' );
		$response      = $wp_rest_server->dispatch( $request );
		$response_data = $response->get_data();
		if ( empty( $response_data ) ) {
			return;
		}

		$parent             = 'wc tool';
		$supported_commands = array( 'list', 'run' );
		foreach ( $supported_commands as $command ) {
			$synopsis = array();
			if ( 'run' === $command ) {
				$synopsis[] = array(
					'name'        => 'id',
					'type'        => 'positional',
					'description' => __( 'The id for the resource.', 'woocommerce' ),
					'optional'    => false,
				);
				$method     = 'update_item';
				$route      = '/wc/v2/system_status/tools/(?P<id>[\w-]+)';
			} elseif ( 'list' === $command ) {
				$synopsis[] = array(
					'name'        => 'fields',
					'type'        => 'assoc',
					'description' => __( 'Limit response to specific fields. Defaults to all fields.', 'woocommerce' ),
					'optional'    => true,
				);
				$synopsis[] = array(
					'name'        => 'field',
					'type'        => 'assoc',
					'description' => __( 'Get the value of an individual field.', 'woocommerce' ),
					'optional'    => true,
				);
				$synopsis[] = array(
					'name'        => 'format',
					'type'        => 'assoc',
					'description' => __( 'Render response in a particular format.', 'woocommerce' ),
					'optional'    => true,
					'default'     => 'table',
					'options'     => array(
						'table',
						'json',
						'csv',
						'ids',
						'yaml',
						'count',
						'headers',
						'body',
						'envelope',
					),
				);
				$method     = 'list_items';
				$route      = '/wc/v2/system_status/tools';
			}

			$before_invoke = null;
			if ( empty( $command_args['when'] ) && WP_CLI::get_config( 'debug' ) ) {
				$before_invoke = function() {
					wc_maybe_define_constant( 'SAVEQUERIES', true );
				};
			}

			$rest_command = new WC_CLI_REST_Command( 'system_status_tool', $route, $response_data['schema'] );

			WP_CLI::add_command(
				"{$parent} {$command}",
				array( $rest_command, $method ),
				array(
					'synopsis'      => $synopsis,
					'when'          => ! empty( $command_args['when'] ) ? $command_args['when'] : '',
					'before_invoke' => $before_invoke,
				)
			);
		}
	}

}
cli/class-wc-cli-rest-command.php000064400000032267151336044760012717 0ustar00<?php
/**
 * WP_CLI_Rest_Command class file.
 *
 * @package WooCommerce\CLI
 */

use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Utilities\NumberUtil;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Main Command for WooCommere CLI.
 *
 * Since a lot of WC operations can be handled via the REST API, we base our CLI
 * off of Restful to generate commands for each WooCommerce REST API endpoint
 * so most of the logic is shared.
 *
 * Forked from wp-cli/restful (by Daniel Bachhuber, released under the MIT license https://opensource.org/licenses/MIT).
 * https://github.com/wp-cli/restful
 *
 * @version 3.0.0
 * @package WooCommerce
 */
class WC_CLI_REST_Command {
	/**
	 * Endpoints that have a parent ID.
	 * Ex: Product reviews, which has a product ID and a review ID.
	 *
	 * @var array
	 */
	protected $routes_with_parent_id = array(
		'customer_download',
		'product_review',
		'order_note',
		'shop_order_refund',
	);

	/**
	 * Name of command/endpoint object.
	 *
	 * @var string
	 */
	private $name;

	/**
	 * Endpoint route.
	 *
	 * @var string
	 */
	private $route;

	/**
	 * Main resource ID.
	 *
	 * @var int
	 */
	private $resource_identifier;

	/**
	 * Schema for command.
	 *
	 * @var array
	 */
	private $schema;

	/**
	 * List of supported IDs and their description (name => desc).
	 *
	 * @var array
	 */
	private $supported_ids = array();

	/**
	 * Sets up REST Command.
	 *
	 * @param string $name   Name of endpoint object (comes from schema).
	 * @param string $route  Path to route of this endpoint.
	 * @param array  $schema Schema object.
	 */
	public function __construct( $name, $route, $schema ) {
		$this->name = $name;

		preg_match_all( '#\([^\)]+\)#', $route, $matches );
		$first_match  = $matches[0];
		$resource_id  = ! empty( $matches[0] ) ? array_pop( $matches[0] ) : null;
		$this->route  = rtrim( $route );
		$this->schema = $schema;

		$this->resource_identifier = $resource_id;
		if ( in_array( $name, $this->routes_with_parent_id, true ) ) {
			$is_singular = substr( $this->route, - strlen( $resource_id ) ) === $resource_id;
			if ( ! $is_singular ) {
				$this->resource_identifier = $first_match[0];
			}
		}
	}

	/**
	 * Passes supported ID arguments (things like product_id, order_id, etc) that we should look for in addition to id.
	 *
	 * @param array $supported_ids List of supported IDs.
	 */
	public function set_supported_ids( $supported_ids = array() ) {
		$this->supported_ids = $supported_ids;
	}

	/**
	 * Returns an ID of supported ID arguments (things like product_id, order_id, etc) that we should look for in addition to id.
	 *
	 * @return array
	 */
	public function get_supported_ids() {
		return $this->supported_ids;
	}

	/**
	 * Create a new item.
	 *
	 * @subcommand create
	 *
	 * @param array $args WP-CLI positional arguments.
	 * @param array $assoc_args WP-CLI associative arguments.
	 */
	public function create_item( $args, $assoc_args ) {
		$assoc_args            = self::decode_json( $assoc_args );
		list( $status, $body ) = $this->do_request( 'POST', $this->get_filled_route( $args ), $assoc_args );
		if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'porcelain' ) ) {
			WP_CLI::line( $body['id'] );
		} else {
			WP_CLI::success( "Created {$this->name} {$body['id']}." );
		}
	}

	/**
	 * Delete an existing item.
	 *
	 * @subcommand delete
	 *
	 * @param array $args WP-CLI positional arguments.
	 * @param array $assoc_args WP-CLI associative arguments.
	 */
	public function delete_item( $args, $assoc_args ) {
		list( $status, $body ) = $this->do_request( 'DELETE', $this->get_filled_route( $args ), $assoc_args );
		$object_id = isset( $body['id'] ) ? $body['id'] : '';
		if ( ! $object_id && isset( $body['slug'] ) ) {
			$object_id = $body['slug'];
		}

		if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'porcelain' ) ) {
			WP_CLI::line( $object_id );
		} else {
			if ( empty( $assoc_args['force'] ) ) {
				WP_CLI::success( __( 'Trashed', 'woocommerce' ) . " {$this->name} {$object_id}" );
			} else {
				WP_CLI::success( __( 'Deleted', 'woocommerce' ) . " {$this->name} {$object_id}." );
			}
		}
	}

	/**
	 * Get a single item.
	 *
	 * @subcommand get
	 *
	 * @param array $args WP-CLI positional arguments.
	 * @param array $assoc_args WP-CLI associative arguments.
	 */
	public function get_item( $args, $assoc_args ) {
		$route                           = $this->get_filled_route( $args );
		list( $status, $body, $headers ) = $this->do_request( 'GET', $route, $assoc_args );

		if ( ! empty( $assoc_args['fields'] ) ) {
			$body = self::limit_item_to_fields( $body, $assoc_args['fields'] );
		}

		if ( empty( $assoc_args['format'] ) ) {
			$assoc_args['format'] = 'table';
		}

		if ( 'headers' === $assoc_args['format'] ) {
			echo wp_json_encode( $headers );
		} elseif ( 'body' === $assoc_args['format'] ) {
			echo wp_json_encode( $body );
		} elseif ( 'envelope' === $assoc_args['format'] ) {
			echo wp_json_encode(
				array(
					'body'    => $body,
					'headers' => $headers,
					'status'  => $status,
				)
			);
		} else {
			$formatter = $this->get_formatter( $assoc_args );
			$formatter->display_item( $body );
		}
	}

	/**
	 * List all items.
	 *
	 * @subcommand list
	 *
	 * @param array $args WP-CLI positional arguments.
	 * @param array $assoc_args WP-CLI associative arguments.
	 */
	public function list_items( $args, $assoc_args ) {
		if ( ! empty( $assoc_args['format'] ) && 'count' === $assoc_args['format'] ) {
			$method = 'HEAD';
		} else {
			$method = 'GET';
		}

		if ( ! isset( $assoc_args['per_page'] ) || empty( $assoc_args['per_page'] ) ) {
			$assoc_args['per_page'] = '100';
		}

		list( $status, $body, $headers ) = $this->do_request( $method, $this->get_filled_route( $args ), $assoc_args );
		if ( ! empty( $assoc_args['format'] ) && 'ids' === $assoc_args['format'] ) {
			$items = array_column( $body, 'id' );
		} else {
			$items = $body;
		}

		if ( ! empty( $assoc_args['fields'] ) ) {
			foreach ( $items as $key => $item ) {
				$items[ $key ] = self::limit_item_to_fields( $item, $assoc_args['fields'] );
			}
		}

		if ( empty( $assoc_args['format'] ) ) {
			$assoc_args['format'] = 'table';
		}

		if ( ! empty( $assoc_args['format'] ) && 'count' === $assoc_args['format'] ) {
			echo (int) $headers['X-WP-Total'];
		} elseif ( 'headers' === $assoc_args['format'] ) {
			echo wp_json_encode( $headers );
		} elseif ( 'body' === $assoc_args['format'] ) {
			echo wp_json_encode( $body );
		} elseif ( 'envelope' === $assoc_args['format'] ) {
			echo wp_json_encode(
				array(
					'body'    => $body,
					'headers' => $headers,
					'status'  => $status,
					'api_url' => $this->api_url,
				)
			);
		} else {
			$formatter = $this->get_formatter( $assoc_args );
			$formatter->display_items( $items );
		}
	}

	/**
	 * Update an existing item.
	 *
	 * @subcommand update
	 *
	 * @param array $args WP-CLI positional arguments.
	 * @param array $assoc_args WP-CLI associative arguments.
	 */
	public function update_item( $args, $assoc_args ) {
		$assoc_args            = self::decode_json( $assoc_args );
		list( $status, $body ) = $this->do_request( 'POST', $this->get_filled_route( $args ), $assoc_args );
		if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'porcelain' ) ) {
			WP_CLI::line( $body['id'] );
		} else {
			WP_CLI::success( __( 'Updated', 'woocommerce' ) . " {$this->name} {$body['id']}." );
		}
	}

	/**
	 * Do a REST Request
	 *
	 * @param string $method Request method. Examples: 'POST', 'PUT', 'DELETE' or 'GET'.
	 * @param string $route Resource route.
	 * @param array  $assoc_args Associative arguments passed to the originating WP-CLI command.
	 *
	 * @return array
	 */
	private function do_request( $method, $route, $assoc_args ) {
		wc_maybe_define_constant( 'REST_REQUEST', true );

		$request = new WP_REST_Request( $method, $route );
		if ( in_array( $method, array( 'POST', 'PUT' ), true ) ) {
			$request->set_body_params( $assoc_args );
		} else {
			foreach ( $assoc_args as $key => $value ) {
				$request->set_param( $key, $value );
			}
		}
		if ( Constants::is_true( 'SAVEQUERIES' ) ) {
			$original_queries = is_array( $GLOBALS['wpdb']->queries ) ? array_keys( $GLOBALS['wpdb']->queries ) : array();
		}
		$response = rest_do_request( $request );
		if ( Constants::is_true( 'SAVEQUERIES' ) ) {
			$performed_queries = array();
			foreach ( (array) $GLOBALS['wpdb']->queries as $key => $query ) {
				if ( in_array( $key, $original_queries, true ) ) {
					continue;
				}
				$performed_queries[] = $query;
			}
			usort(
				$performed_queries,
				function( $a, $b ) {
					if ( $a[1] === $b[1] ) {
						return 0;
					}
					return ( $a[1] > $b[1] ) ? -1 : 1;
				}
			);

			$query_count      = count( $performed_queries );
			$query_total_time = 0;
			foreach ( $performed_queries as $query ) {
				$query_total_time += $query[1];
			}
			$slow_query_message = '';
			if ( $performed_queries && 'wc' === WP_CLI::get_config( 'debug' ) ) {
				$slow_query_message .= '. Ordered by slowness, the queries are:' . PHP_EOL;
				foreach ( $performed_queries as $i => $query ) {
					$i++;
					$bits                = explode( ', ', $query[2] );
					$backtrace           = implode( ', ', array_slice( $bits, 13 ) );
					$seconds             = NumberUtil::round( $query[1], 6 );
					$slow_query_message .= <<<EOT
{$i}:
- {$seconds} seconds
- {$backtrace}
- {$query[0]}
EOT;
					$slow_query_message .= PHP_EOL;
				}
			} elseif ( 'wc' !== WP_CLI::get_config( 'debug' ) ) {
				$slow_query_message = '. Use --debug=wc to see all queries.';
			}
			$query_total_time = NumberUtil::round( $query_total_time, 6 );
			WP_CLI::debug( "wc command executed {$query_count} queries in {$query_total_time} seconds{$slow_query_message}", 'wc' );
		}

		$error = $response->as_error();

		if ( $error ) {
			// For authentication errors (status 401), include a reminder to set the --user flag.
			// WP_CLI::error will only return the first message from WP_Error, so we will pass a string containing both instead.
			if ( 401 === $response->get_status() ) {
				$errors   = $error->get_error_messages();
				$errors[] = __( 'Make sure to include the --user flag with an account that has permissions for this action.', 'woocommerce' ) . ' {"status":401}';
				$error    = implode( "\n", $errors );
			}
			WP_CLI::error( $error );
		}
		return array( $response->get_status(), $response->get_data(), $response->get_headers() );
	}

	/**
	 * Get Formatter object based on supplied parameters.
	 *
	 * @param array $assoc_args Parameters passed to command. Determines formatting.
	 * @return \WP_CLI\Formatter
	 */
	protected function get_formatter( &$assoc_args ) {
		if ( ! empty( $assoc_args['fields'] ) ) {
			if ( is_string( $assoc_args['fields'] ) ) {
				$fields = explode( ',', $assoc_args['fields'] );
			} else {
				$fields = $assoc_args['fields'];
			}
		} else {
			if ( ! empty( $assoc_args['context'] ) ) {
				$fields = $this->get_context_fields( $assoc_args['context'] );
			} else {
				$fields = $this->get_context_fields( 'view' );
			}
		}
		return new \WP_CLI\Formatter( $assoc_args, $fields );
	}

	/**
	 * Get a list of fields present in a given context
	 *
	 * @param string $context Scope under which the request is made. Determines fields present in response.
	 * @return array
	 */
	private function get_context_fields( $context ) {
		$fields = array();
		foreach ( $this->schema['properties'] as $key => $args ) {
			if ( empty( $args['context'] ) || in_array( $context, $args['context'], true ) ) {
				$fields[] = $key;
			}
		}
		return $fields;
	}

	/**
	 * Get the route for this resource
	 *
	 * @param  array $args Positional arguments passed to the originating WP-CLI command.
	 * @return string
	 */
	private function get_filled_route( $args = array() ) {
		$supported_id_matched = false;
		$route                = $this->route;

		foreach ( $this->get_supported_ids() as $id_name => $id_desc ) {
			if ( 'id' !== $id_name && strpos( $route, '<' . $id_name . '>' ) !== false && ! empty( $args ) ) {
				$route                = str_replace( array( '(?P<' . $id_name . '>[\d]+)', '(?P<' . $id_name . '>\w[\w\s\-]*)' ), $args[0], $route );
				$supported_id_matched = true;
			}
		}

		if ( ! empty( $args ) ) {
			$id_replacement = $supported_id_matched && ! empty( $args[1] ) ? $args[1] : $args[0];
			$route          = str_replace( array( '(?P<id>[\d]+)', '(?P<id>[\w-]+)' ), $id_replacement, $route );
		}

		return rtrim( $route );
	}

	/**
	 * Reduce an item to specific fields.
	 *
	 * @param  array $item Item to reduce.
	 * @param  array $fields Fields to keep.
	 * @return array
	 */
	private static function limit_item_to_fields( $item, $fields ) {
		if ( empty( $fields ) ) {
			return $item;
		}
		if ( is_string( $fields ) ) {
			$fields = explode( ',', $fields );
		}
		foreach ( $item as $i => $field ) {
			if ( ! in_array( $i, $fields, true ) ) {
				unset( $item[ $i ] );
			}
		}
		return $item;
	}

	/**
	 * JSON can be passed in some more complicated objects, like the payment gateway settings array.
	 * This function decodes the json (if present) and tries to get it's value.
	 *
	 * @param array $arr Array that will be scanned for JSON encoded values.
	 *
	 * @return array
	 */
	protected function decode_json( $arr ) {
		foreach ( $arr as $key => $value ) {
			if ( '[' === substr( $value, 0, 1 ) || '{' === substr( $value, 0, 1 ) ) {
				$arr[ $key ] = json_decode( $value, true );
			} else {
				continue;
			}
		}
		return $arr;
	}

}
cli/class-wc-cli-update-command.php000064400000004716151336044760013222 0ustar00<?php
/**
 * WC_CLI_Update_Command class file.
 *
 * @package WooCommerce\CLI
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Allows updates via CLI.
 *
 * @version 3.0.0
 * @package WooCommerce
 */
class WC_CLI_Update_Command {

	/**
	 * Registers the update command.
	 */
	public static function register_commands() {
		WP_CLI::add_command( 'wc update', array( 'WC_CLI_Update_Command', 'update' ) );
	}

	/**
	 * Runs all pending WooCommerce database updates.
	 */
	public static function update() {
		global $wpdb;

		$wpdb->hide_errors();

		include_once WC_ABSPATH . 'includes/class-wc-install.php';
		include_once WC_ABSPATH . 'includes/wc-update-functions.php';

		$current_db_version = get_option( 'woocommerce_db_version' );
		$update_count       = 0;
		$callbacks          = WC_Install::get_db_update_callbacks();
		$callbacks_to_run   = array();

		foreach ( $callbacks as $version => $update_callbacks ) {
			if ( version_compare( $current_db_version, $version, '<' ) ) {
				foreach ( $update_callbacks as $update_callback ) {
					$callbacks_to_run[] = $update_callback;
				}
			}
		}

		if ( empty( $callbacks_to_run ) ) {
			// Ensure DB version is set to the current WC version to match WP-Admin update routine.
			WC_Install::update_db_version();
			/* translators: %s Database version number */
			WP_CLI::success( sprintf( __( 'No updates required. Database version is %s', 'woocommerce' ), get_option( 'woocommerce_db_version' ) ) );
			return;
		}

		/* translators: 1: Number of database updates 2: List of update callbacks */
		WP_CLI::log( sprintf( __( 'Found %1$d updates (%2$s)', 'woocommerce' ), count( $callbacks_to_run ), implode( ', ', $callbacks_to_run ) ) );

		$progress = \WP_CLI\Utils\make_progress_bar( __( 'Updating database', 'woocommerce' ), count( $callbacks_to_run ) ); // phpcs:ignore PHPCompatibility.LanguageConstructs.NewLanguageConstructs.t_ns_separatorFound

		foreach ( $callbacks_to_run as $update_callback ) {
			call_user_func( $update_callback );
			$result = false;
			while ( $result ) {
				$result = (bool) call_user_func( $update_callback );
			}
			$update_count ++;
			$progress->tick();
		}

		$progress->finish();

		WC_Admin_Notices::remove_notice( 'update', true );

		/* translators: 1: Number of database updates performed 2: Database version number */
		WP_CLI::success( sprintf( __( '%1$d update functions completed. Database version is %2$s', 'woocommerce' ), absint( $update_count ), get_option( 'woocommerce_db_version' ) ) );
	}
}
cli/class-wc-cli-runner.php000064400000020354151336044760011631 0ustar00<?php
/**
 * WP_CLI_Runner class file.
 *
 * @package WooCommerce\CLI
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC API to WC CLI Bridge.
 *
 * Hooks into the REST API, figures out which endpoints come from WC,
 * and registers them as CLI commands.
 *
 * Forked from wp-cli/restful (by Daniel Bachhuber, released under the MIT license https://opensource.org/licenses/MIT).
 * https://github.com/wp-cli/restful
 *
 * @version 3.0.0
 * @package WooCommerce
 */
class WC_CLI_Runner {
	/**
	 * Endpoints to disable (meaning they will not be available as CLI commands).
	 * Some of these can either be done via WP already, or are offered with
	 * some other changes (like tools).
	 *
	 * @var array
	 */
	private static $disabled_endpoints = array(
		'settings',
		'settings/(?P<group_id>[\w-]+)',
		'settings/(?P<group_id>[\w-]+)/batch',
		'settings/(?P<group_id>[\w-]+)/(?P<id>[\w-]+)',
		'system_status',
		'system_status/tools',
		'system_status/tools/(?P<id>[\w-]+)',
		'reports',
		'reports/sales',
		'reports/top_sellers',
	);

	/**
	 * The version of the REST API we should target to
	 * generate commands.
	 *
	 * @var string
	 */
	private static $target_rest_version = 'v2';

	/**
	 * Register's all endpoints as commands once WP and WC have all loaded.
	 */
	public static function after_wp_load() {
		global $wp_rest_server;
		$wp_rest_server = new WP_REST_Server();
		do_action( 'rest_api_init', $wp_rest_server );

		$request = new WP_REST_Request( 'GET', '/' );
		$request->set_param( 'context', 'help' );
		$response      = $wp_rest_server->dispatch( $request );
		$response_data = $response->get_data();
		if ( empty( $response_data ) ) {
			return;
		}

		// Loop through all of our endpoints and register any valid WC endpoints.
		foreach ( $response_data['routes'] as $route => $route_data ) {
			// Only register endpoints for WC and our target version.
			if ( substr( $route, 0, 4 + strlen( self::$target_rest_version ) ) !== '/wc/' . self::$target_rest_version ) {
				continue;
			}

			// Only register endpoints with schemas.
			if ( empty( $route_data['schema']['title'] ) ) {
				/* translators: %s: Route to a given WC-API endpoint */
				WP_CLI::debug( sprintf( __( 'No schema title found for %s, skipping REST command registration.', 'woocommerce' ), $route ), 'wc' );
				continue;
			}
			// Ignore batch endpoints.
			if ( 'batch' === $route_data['schema']['title'] ) {
				continue;
			}
			// Disable specific endpoints.
			$route_pieces   = explode( '/', $route );
			$endpoint_piece = str_replace( '/wc/' . $route_pieces[2] . '/', '', $route );
			if ( in_array( $endpoint_piece, self::$disabled_endpoints, true ) ) {
				continue;
			}

			self::register_route_commands( new WC_CLI_REST_Command( $route_data['schema']['title'], $route, $route_data['schema'] ), $route, $route_data );
		}
	}

	/**
	 * Generates command information and tells WP CLI about all
	 * commands available from a route.
	 *
	 * @param string $rest_command WC-API command.
	 * @param string $route Path to route endpoint.
	 * @param array  $route_data Command data.
	 * @param array  $command_args WP-CLI command arguments.
	 */
	private static function register_route_commands( $rest_command, $route, $route_data, $command_args = array() ) {
		// Define IDs that we are looking for in the routes (in addition to id)
		// so that we can pass it to the rest command, and use it here to generate documentation.
		$supported_ids = array(
			'product_id'   => __( 'Product ID.', 'woocommerce' ),
			'customer_id'  => __( 'Customer ID.', 'woocommerce' ),
			'order_id'     => __( 'Order ID.', 'woocommerce' ),
			'refund_id'    => __( 'Refund ID.', 'woocommerce' ),
			'attribute_id' => __( 'Attribute ID.', 'woocommerce' ),
			'zone_id'      => __( 'Zone ID.', 'woocommerce' ),
			'instance_id'  => __( 'Instance ID.', 'woocommerce' ),
			'id'           => __( 'The ID for the resource.', 'woocommerce' ),
			'slug'         => __( 'The slug for the resource.', 'woocommerce' ),
		);
		$rest_command->set_supported_ids( $supported_ids );
		$positional_args = array_keys( $supported_ids );
		$parent             = "wc {$route_data['schema']['title']}";
		$supported_commands = array();

		// Get a list of supported commands for each route.
		foreach ( $route_data['endpoints'] as $endpoint ) {
			preg_match_all( '#\([^\)]+\)#', $route, $matches );
			$resource_id   = ! empty( $matches[0] ) ? array_pop( $matches[0] ) : null;
			$trimmed_route = rtrim( $route );
			$is_singular   = substr( $trimmed_route, - strlen( $resource_id ) ) === $resource_id;

			// List a collection.
			if ( array( 'GET' ) === $endpoint['methods'] && ! $is_singular ) {
				$supported_commands['list'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array();
			}
			// Create a specific resource.
			if ( array( 'POST' ) === $endpoint['methods'] && ! $is_singular ) {
				$supported_commands['create'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array();
			}
			// Get a specific resource.
			if ( array( 'GET' ) === $endpoint['methods'] && $is_singular ) {
				$supported_commands['get'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array();
			}
			// Update a specific resource.
			if ( in_array( 'POST', $endpoint['methods'], true ) && $is_singular ) {
				$supported_commands['update'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array();
			}
			// Delete a specific resource.
			if ( array( 'DELETE' ) === $endpoint['methods'] && $is_singular ) {
				$supported_commands['delete'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array();
			}
		}

		foreach ( $supported_commands as $command => $endpoint_args ) {
			$synopsis = array();
			$arg_regs = array();
			$ids      = array();

			foreach ( $supported_ids as $id_name => $id_desc ) {
				if ( strpos( $route, '<' . $id_name . '>' ) !== false ) {
					$synopsis[] = array(
						'name'        => $id_name,
						'type'        => 'positional',
						'description' => $id_desc,
						'optional'    => false,
					);
					$ids[]      = $id_name;
				}
			}

			foreach ( $endpoint_args as $name => $args ) {
				if ( ! in_array( $name, $positional_args, true ) || strpos( $route, '<' . $id_name . '>' ) === false ) {
					$arg_regs[] = array(
						'name'        => $name,
						'type'        => 'assoc',
						'description' => ! empty( $args['description'] ) ? $args['description'] : '',
						'optional'    => empty( $args['required'] ),
					);
				}
			}

			foreach ( $arg_regs as $arg_reg ) {
				$synopsis[] = $arg_reg;
			}

			if ( in_array( $command, array( 'list', 'get' ), true ) ) {
				$synopsis[] = array(
					'name'        => 'fields',
					'type'        => 'assoc',
					'description' => __( 'Limit response to specific fields. Defaults to all fields.', 'woocommerce' ),
					'optional'    => true,
				);
				$synopsis[] = array(
					'name'        => 'field',
					'type'        => 'assoc',
					'description' => __( 'Get the value of an individual field.', 'woocommerce' ),
					'optional'    => true,
				);
				$synopsis[] = array(
					'name'        => 'format',
					'type'        => 'assoc',
					'description' => __( 'Render response in a particular format.', 'woocommerce' ),
					'optional'    => true,
					'default'     => 'table',
					'options'     => array(
						'table',
						'json',
						'csv',
						'ids',
						'yaml',
						'count',
						'headers',
						'body',
						'envelope',
					),
				);
			}

			if ( in_array( $command, array( 'create', 'update', 'delete' ), true ) ) {
				$synopsis[] = array(
					'name'        => 'porcelain',
					'type'        => 'flag',
					'description' => __( 'Output just the id when the operation is successful.', 'woocommerce' ),
					'optional'    => true,
				);
			}

			$methods = array(
				'list'   => 'list_items',
				'create' => 'create_item',
				'delete' => 'delete_item',
				'get'    => 'get_item',
				'update' => 'update_item',
			);

			$before_invoke = null;
			if ( empty( $command_args['when'] ) && \WP_CLI::get_config( 'debug' ) ) {
				$before_invoke = function() {
					wc_maybe_define_constant( 'SAVEQUERIES', true );
				};
			}

			WP_CLI::add_command(
				"{$parent} {$command}",
				array( $rest_command, $methods[ $command ] ),
				array(
					'synopsis'      => $synopsis,
					'when'          => ! empty( $command_args['when'] ) ? $command_args['when'] : '',
					'before_invoke' => $before_invoke,
				)
			);
		}
	}
}
class-wc-product-grouped.php000064400000012345151336044760012130 0ustar00<?php
/**
 * Grouped Product
 *
 * Grouped products cannot be purchased - they are wrappers for other products.
 *
 * @package WooCommerce\Classes\Products
 * @version 3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Product grouped class.
 */
class WC_Product_Grouped extends WC_Product {

	/**
	 * Stores product data.
	 *
	 * @var array
	 */
	protected $extra_data = array(
		'children' => array(),
	);

	/**
	 * Get internal type.
	 *
	 * @return string
	 */
	public function get_type() {
		return 'grouped';
	}

	/**
	 * Get the add to cart button text.
	 *
	 * @return string
	 */
	public function add_to_cart_text() {
		return apply_filters( 'woocommerce_product_add_to_cart_text', __( 'View products', 'woocommerce' ), $this );
	}

	/**
	 * Get the add to cart button text description - used in aria tags.
	 *
	 * @since 3.3.0
	 * @return string
	 */
	public function add_to_cart_description() {
		/* translators: %s: Product title */
		return apply_filters( 'woocommerce_product_add_to_cart_description', sprintf( __( 'View products in the &ldquo;%s&rdquo; group', 'woocommerce' ), $this->get_name() ), $this );
	}

	/**
	 * Returns whether or not the product is on sale.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return bool
	 */
	public function is_on_sale( $context = 'view' ) {
		$children = array_filter( array_map( 'wc_get_product', $this->get_children( $context ) ), 'wc_products_array_filter_visible_grouped' );
		$on_sale  = false;

		foreach ( $children as $child ) {
			if ( $child->is_purchasable() && ! $child->has_child() && $child->is_on_sale() ) {
				$on_sale = true;
				break;
			}
		}

		return 'view' === $context ? apply_filters( 'woocommerce_product_is_on_sale', $on_sale, $this ) : $on_sale;
	}

	/**
	 * Returns false if the product cannot be bought.
	 *
	 * @return bool
	 */
	public function is_purchasable() {
		return apply_filters( 'woocommerce_is_purchasable', false, $this );
	}

	/**
	 * Returns the price in html format.
	 *
	 * @param string $price (default: '').
	 * @return string
	 */
	public function get_price_html( $price = '' ) {
		$tax_display_mode = get_option( 'woocommerce_tax_display_shop' );
		$child_prices     = array();
		$children         = array_filter( array_map( 'wc_get_product', $this->get_children() ), 'wc_products_array_filter_visible_grouped' );

		foreach ( $children as $child ) {
			if ( '' !== $child->get_price() ) {
				$child_prices[] = 'incl' === $tax_display_mode ? wc_get_price_including_tax( $child ) : wc_get_price_excluding_tax( $child );
			}
		}

		if ( ! empty( $child_prices ) ) {
			$min_price = min( $child_prices );
			$max_price = max( $child_prices );
		} else {
			$min_price = '';
			$max_price = '';
		}

		if ( '' !== $min_price ) {
			if ( $min_price !== $max_price ) {
				$price = wc_format_price_range( $min_price, $max_price );
			} else {
				$price = wc_price( $min_price );
			}

			$is_free = 0 === $min_price && 0 === $max_price;

			if ( $is_free ) {
				$price = apply_filters( 'woocommerce_grouped_free_price_html', __( 'Free!', 'woocommerce' ), $this );
			} else {
				$price = apply_filters( 'woocommerce_grouped_price_html', $price . $this->get_price_suffix(), $this, $child_prices );
			}
		} else {
			$price = apply_filters( 'woocommerce_grouped_empty_price_html', '', $this );
		}

		return apply_filters( 'woocommerce_get_price_html', $price, $this );
	}

	/*
	|--------------------------------------------------------------------------
	| Getters
	|--------------------------------------------------------------------------
	|
	| Methods for getting data from the product object.
	*/

	/**
	 * Return the children of this product.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return array
	 */
	public function get_children( $context = 'view' ) {
		return $this->get_prop( 'children', $context );
	}

	/*
	|--------------------------------------------------------------------------
	| Setters
	|--------------------------------------------------------------------------
	|
	| Methods for getting data from the product object.
	*/

	/**
	 * Return the children of this product.
	 *
	 * @param array $children List of product children.
	 */
	public function set_children( $children ) {
		$this->set_prop( 'children', array_filter( wp_parse_id_list( (array) $children ) ) );
	}

	/*
	|--------------------------------------------------------------------------
	| Sync with children.
	|--------------------------------------------------------------------------
	*/

	/**
	 * Sync a grouped product with it's children. These sync functions sync
	 * upwards (from child to parent) when the variation is saved.
	 *
	 * @param WC_Product|int $product Product object or ID for which you wish to sync.
	 * @param bool           $save If true, the product object will be saved to the DB before returning it.
	 * @return WC_Product Synced product object.
	 */
	public static function sync( $product, $save = true ) {
		if ( ! is_a( $product, 'WC_Product' ) ) {
			$product = wc_get_product( $product );
		}
		if ( is_a( $product, 'WC_Product_Grouped' ) ) {
			$data_store = WC_Data_Store::load( 'product-' . $product->get_type() );
			$data_store->sync_price( $product );
			if ( $save ) {
				$product->save();
			}
		}
		return $product;
	}
}
class-wc-product-simple.php000064400000003622151336044760011752 0ustar00<?php
/**
 * Simple Product Class.
 *
 * The default product type kinda product.
 *
 * @package WooCommerce\Classes\Products
 */

defined( 'ABSPATH' ) || exit;

/**
 * Simple product class.
 */
class WC_Product_Simple extends WC_Product {

	/**
	 * Initialize simple product.
	 *
	 * @param WC_Product|int $product Product instance or ID.
	 */
	public function __construct( $product = 0 ) {
		$this->supports[] = 'ajax_add_to_cart';
		parent::__construct( $product );
	}

	/**
	 * Get internal type.
	 *
	 * @return string
	 */
	public function get_type() {
		return 'simple';
	}

	/**
	 * Get the add to url used mainly in loops.
	 *
	 * @return string
	 */
	public function add_to_cart_url() {
		$url = $this->is_purchasable() && $this->is_in_stock() ? remove_query_arg(
			'added-to-cart',
			add_query_arg(
				array(
					'add-to-cart' => $this->get_id(),
				),
				( function_exists( 'is_feed' ) && is_feed() ) || ( function_exists( 'is_404' ) && is_404() ) ? $this->get_permalink() : ''
			)
		) : $this->get_permalink();
		return apply_filters( 'woocommerce_product_add_to_cart_url', $url, $this );
	}

	/**
	 * Get the add to cart button text.
	 *
	 * @return string
	 */
	public function add_to_cart_text() {
		$text = $this->is_purchasable() && $this->is_in_stock() ? __( 'Add to cart', 'woocommerce' ) : __( 'Read more', 'woocommerce' );

		return apply_filters( 'woocommerce_product_add_to_cart_text', $text, $this );
	}

	/**
	 * Get the add to cart button text description - used in aria tags.
	 *
	 * @since 3.3.0
	 * @return string
	 */
	public function add_to_cart_description() {
		/* translators: %s: Product title */
		$text = $this->is_purchasable() && $this->is_in_stock() ? __( 'Add &ldquo;%s&rdquo; to your cart', 'woocommerce' ) : __( 'Read more about &ldquo;%s&rdquo;', 'woocommerce' );

		return apply_filters( 'woocommerce_product_add_to_cart_description', sprintf( $text, $this->get_name() ), $this );
	}
}
class-wc-customer-download.php000064400000024553151336044760012457 0ustar00<?php
/**
 * Class for customer download permissions.
 *
 * @package WooCommerce\Classes
 * @version 3.0.0
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Customer download class.
 */
class WC_Customer_Download extends WC_Data implements ArrayAccess {

	/**
	 * This is the name of this object type.
	 *
	 * @var string
	 */
	protected $object_type = 'customer_download';

	/**
	 * Download Data array.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $data = array(
		'download_id'         => '',
		'product_id'          => 0,
		'user_id'             => 0,
		'user_email'          => '',
		'order_id'            => 0,
		'order_key'           => '',
		'downloads_remaining' => '',
		'access_granted'      => null,
		'access_expires'      => null,
		'download_count'      => 0,
	);

	/**
	 * Constructor.
	 *
	 * @param int|object|array $download Download ID, instance or data.
	 */
	public function __construct( $download = 0 ) {
		parent::__construct( $download );

		if ( is_numeric( $download ) && $download > 0 ) {
			$this->set_id( $download );
		} elseif ( $download instanceof self ) {
			$this->set_id( $download->get_id() );
		} elseif ( is_object( $download ) && ! empty( $download->permission_id ) ) {
			$this->set_id( $download->permission_id );
			$this->set_props( (array) $download );
			$this->set_object_read( true );
		} else {
			$this->set_object_read( true );
		}

		$this->data_store = WC_Data_Store::load( 'customer-download' );

		if ( $this->get_id() > 0 ) {
			$this->data_store->read( $this );
		}
	}

	/*
	|--------------------------------------------------------------------------
	| Getters
	|--------------------------------------------------------------------------
	*/

	/**
	 * Get download id.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_download_id( $context = 'view' ) {
		return $this->get_prop( 'download_id', $context );
	}

	/**
	 * Get product id.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return integer
	 */
	public function get_product_id( $context = 'view' ) {
		return $this->get_prop( 'product_id', $context );
	}

	/**
	 * Get user id.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return integer
	 */
	public function get_user_id( $context = 'view' ) {
		return $this->get_prop( 'user_id', $context );
	}

	/**
	 * Get user_email.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_user_email( $context = 'view' ) {
		return $this->get_prop( 'user_email', $context );
	}

	/**
	 * Get order_id.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return integer
	 */
	public function get_order_id( $context = 'view' ) {
		return $this->get_prop( 'order_id', $context );
	}

	/**
	 * Get order_key.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_order_key( $context = 'view' ) {
		return $this->get_prop( 'order_key', $context );
	}

	/**
	 * Get downloads_remaining.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return integer|string
	 */
	public function get_downloads_remaining( $context = 'view' ) {
		return $this->get_prop( 'downloads_remaining', $context );
	}

	/**
	 * Get access_granted.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return WC_DateTime|null Object if the date is set or null if there is no date.
	 */
	public function get_access_granted( $context = 'view' ) {
		return $this->get_prop( 'access_granted', $context );
	}

	/**
	 * Get access_expires.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return WC_DateTime|null Object if the date is set or null if there is no date.
	 */
	public function get_access_expires( $context = 'view' ) {
		return $this->get_prop( 'access_expires', $context );
	}

	/**
	 * Get download_count.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return integer
	 */
	public function get_download_count( $context = 'view' ) {
		// Check for count of download logs.
		$data_store       = WC_Data_Store::load( 'customer-download-log' );
		$download_log_ids = $data_store->get_download_logs_for_permission( $this->get_id() );

		$download_log_count = 0;
		if ( ! empty( $download_log_ids ) ) {
			$download_log_count = count( $download_log_ids );
		}

		// Check download count in prop.
		$download_count_prop = $this->get_prop( 'download_count', $context );

		// Return the larger of the two in case they differ.
		// If logs are removed for some reason, we should still respect the
		// count stored in the prop.
		return max( $download_log_count, $download_count_prop );
	}

	/*
	|--------------------------------------------------------------------------
	| Setters
	|--------------------------------------------------------------------------
	*/

	/**
	 * Set download id.
	 *
	 * @param string $value Download ID.
	 */
	public function set_download_id( $value ) {
		$this->set_prop( 'download_id', $value );
	}
	/**
	 * Set product id.
	 *
	 * @param int $value Product ID.
	 */
	public function set_product_id( $value ) {
		$this->set_prop( 'product_id', absint( $value ) );
	}

	/**
	 * Set user id.
	 *
	 * @param int $value User ID.
	 */
	public function set_user_id( $value ) {
		$this->set_prop( 'user_id', absint( $value ) );
	}

	/**
	 * Set user_email.
	 *
	 * @param int $value User email.
	 */
	public function set_user_email( $value ) {
		$this->set_prop( 'user_email', sanitize_email( $value ) );
	}

	/**
	 * Set order_id.
	 *
	 * @param int $value Order ID.
	 */
	public function set_order_id( $value ) {
		$this->set_prop( 'order_id', absint( $value ) );
	}

	/**
	 * Set order_key.
	 *
	 * @param string $value Order key.
	 */
	public function set_order_key( $value ) {
		$this->set_prop( 'order_key', $value );
	}

	/**
	 * Set downloads_remaining.
	 *
	 * @param integer|string $value Amount of downloads remaining.
	 */
	public function set_downloads_remaining( $value ) {
		$this->set_prop( 'downloads_remaining', '' === $value ? '' : absint( $value ) );
	}

	/**
	 * Set access_granted.
	 *
	 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date.
	 */
	public function set_access_granted( $date = null ) {
		$this->set_date_prop( 'access_granted', $date );
	}

	/**
	 * Set access_expires.
	 *
	 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date.
	 */
	public function set_access_expires( $date = null ) {
		$this->set_date_prop( 'access_expires', $date );
	}

	/**
	 * Set download_count.
	 *
	 * @param int $value Download count.
	 */
	public function set_download_count( $value ) {
		$this->set_prop( 'download_count', absint( $value ) );
	}

	/**
	 * Track a download on this permission.
	 *
	 * @since 3.3.0
	 * @throws Exception When permission ID is invalid.
	 * @param int    $user_id         Id of the user performing the download.
	 * @param string $user_ip_address IP Address of the user performing the download.
	 */
	public function track_download( $user_id = null, $user_ip_address = null ) {
		global $wpdb;

		// Must have a permission_id to track download log.
		if ( ! ( $this->get_id() > 0 ) ) {
			throw new Exception( __( 'Invalid permission ID.', 'woocommerce' ) );
		}

		// Increment download count, and decrement downloads remaining.
		// Use SQL to avoid possible issues with downloads in quick succession.
		// If downloads_remaining is blank, leave it blank (unlimited).
		// Also, ensure downloads_remaining doesn't drop below zero.
		$query = $wpdb->prepare(
			"
UPDATE {$wpdb->prefix}woocommerce_downloadable_product_permissions
SET download_count = download_count + 1,
downloads_remaining = IF( downloads_remaining = '', '', GREATEST( 0, downloads_remaining - 1 ) )
WHERE permission_id = %d",
			$this->get_id()
		);
		$wpdb->query( $query ); // WPCS: unprepared SQL ok.

		// Re-read this download from the data store to pull updated counts.
		$this->data_store->read( $this );

		// Track download in download log.
		$download_log = new WC_Customer_Download_Log();
		$download_log->set_timestamp( current_time( 'timestamp', true ) );
		$download_log->set_permission_id( $this->get_id() );

		if ( ! is_null( $user_id ) ) {
			$download_log->set_user_id( $user_id );
		}

		if ( ! is_null( $user_ip_address ) ) {
			$download_log->set_user_ip_address( $user_ip_address );
		}

		$download_log->save();
	}

	/*
	|--------------------------------------------------------------------------
	| ArrayAccess/Backwards compatibility.
	|--------------------------------------------------------------------------
	*/

	/**
	 * OffsetGet.
	 *
	 * @param string $offset Offset.
	 * @return mixed
	 */
	public function offsetGet( $offset ) {
		if ( is_callable( array( $this, "get_$offset" ) ) ) {
			return $this->{"get_$offset"}();
		}
	}

	/**
	 * OffsetSet.
	 *
	 * @param string $offset Offset.
	 * @param mixed  $value  Value.
	 */
	public function offsetSet( $offset, $value ) {
		if ( is_callable( array( $this, "set_$offset" ) ) ) {
			$this->{"set_$offset"}( $value );
		}
	}

	/**
	 * OffsetUnset
	 *
	 * @param string $offset Offset.
	 */
	public function offsetUnset( $offset ) {
		if ( is_callable( array( $this, "set_$offset" ) ) ) {
			$this->{"set_$offset"}( '' );
		}
	}

	/**
	 * OffsetExists.
	 *
	 * @param string $offset Offset.
	 * @return bool
	 */
	public function offsetExists( $offset ) {
		return in_array( $offset, array_keys( $this->data ), true );
	}

	/**
	 * Magic __isset method for backwards compatibility. Legacy properties which could be accessed directly in the past.
	 *
	 * @param  string $key Key name.
	 * @return bool
	 */
	public function __isset( $key ) {
		return in_array( $key, array_keys( $this->data ), true );
	}

	/**
	 * Magic __get method for backwards compatibility. Maps legacy vars to new getters.
	 *
	 * @param  string $key Key name.
	 * @return mixed
	 */
	public function __get( $key ) {
		if ( is_callable( array( $this, "get_$key" ) ) ) {
			return $this->{"get_$key"}( '' );
		}
	}
}
blocks/class-wc-blocks-utils.php000064400000004112151336044760012666 0ustar00<?php
/**
 * Blocks Utils
 *
 * Used by core components that need to work with blocks.
 *
 * @package WooCommerce\Blocks\Utils
 * @version 5.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Blocks Utility class.
 */
class WC_Blocks_Utils {

	/**
	 * Get blocks from a woocommerce page.
	 *
	 * @param string $woo_page_name A woocommerce page e.g. `checkout` or `cart`.
	 * @return array Array of blocks as returned by parse_blocks().
	 */
	private static function get_all_blocks_from_page( $woo_page_name ) {
		$page_id = wc_get_page_id( $woo_page_name );

		$page = get_post( $page_id );
		if ( ! $page ) {
			return array();
		}

		$blocks = parse_blocks( $page->post_content );
		if ( ! $blocks ) {
			return array();
		}

		return $blocks;
	}

	/**
	 * Get all instances of the specified block on a specific woo page
	 * (e.g. `cart` or `checkout` page).
	 *
	 * @param string $block_name The name (id) of a block, e.g. `woocommerce/cart`.
	 * @param string $woo_page_name The woo page to search, e.g. `cart`.
	 * @return array Array of blocks as returned by parse_blocks().
	 */
	public static function get_blocks_from_page( $block_name, $woo_page_name ) {
		$page_blocks = self::get_all_blocks_from_page( $woo_page_name );

		// Get any instances of the specified block.
		return array_values(
			array_filter(
				$page_blocks,
				function ( $block ) use ( $block_name ) {
					return ( $block_name === $block['blockName'] );
				}
			)
		);
	}

	/**
	 * Check if a given page contains a particular block.
	 *
	 * @param int|WP_Post $page Page post ID or post object.
	 * @param string      $block_name The name (id) of a block, e.g. `woocommerce/cart`.
	 * @return bool Boolean value if the page contains the block or not. Null in case the page does not exist.
	 */
	public static function has_block_in_page( $page, $block_name ) {
		$page_to_check = get_post( $page );
		if ( null === $page_to_check ) {
			return false;
		}

		$blocks = parse_blocks( $page_to_check->post_content );
		foreach ( $blocks as $block ) {
			if ( $block_name === $block['blockName'] ) {
				return true;
			}
		}

		return false;
	}
}
class-wc-breadcrumb.php000064400000022772151336044760011120 0ustar00<?php
/**
 * WC_Breadcrumb class.
 *
 * @package WooCommerce\Classes
 * @version 2.3.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Breadcrumb class.
 */
class WC_Breadcrumb {

	/**
	 * Breadcrumb trail.
	 *
	 * @var array
	 */
	protected $crumbs = array();

	/**
	 * Add a crumb so we don't get lost.
	 *
	 * @param string $name Name.
	 * @param string $link Link.
	 */
	public function add_crumb( $name, $link = '' ) {
		$this->crumbs[] = array(
			wp_strip_all_tags( $name ),
			$link,
		);
	}

	/**
	 * Reset crumbs.
	 */
	public function reset() {
		$this->crumbs = array();
	}

	/**
	 * Get the breadcrumb.
	 *
	 * @return array
	 */
	public function get_breadcrumb() {
		return apply_filters( 'woocommerce_get_breadcrumb', $this->crumbs, $this );
	}

	/**
	 * Generate breadcrumb trail.
	 *
	 * @return array of breadcrumbs
	 */
	public function generate() {
		$conditionals = array(
			'is_home',
			'is_404',
			'is_attachment',
			'is_single',
			'is_product_category',
			'is_product_tag',
			'is_shop',
			'is_page',
			'is_post_type_archive',
			'is_category',
			'is_tag',
			'is_author',
			'is_date',
			'is_tax',
		);

		if ( ( ! is_front_page() && ! ( is_post_type_archive() && intval( get_option( 'page_on_front' ) ) === wc_get_page_id( 'shop' ) ) ) || is_paged() ) {
			foreach ( $conditionals as $conditional ) {
				if ( call_user_func( $conditional ) ) {
					call_user_func( array( $this, 'add_crumbs_' . substr( $conditional, 3 ) ) );
					break;
				}
			}

			$this->search_trail();
			$this->paged_trail();

			return $this->get_breadcrumb();
		}

		return array();
	}

	/**
	 * Prepend the shop page to shop breadcrumbs.
	 */
	protected function prepend_shop_page() {
		$permalinks   = wc_get_permalink_structure();
		$shop_page_id = wc_get_page_id( 'shop' );
		$shop_page    = get_post( $shop_page_id );

		// If permalinks contain the shop page in the URI prepend the breadcrumb with shop.
		if ( $shop_page_id && $shop_page && isset( $permalinks['product_base'] ) && strstr( $permalinks['product_base'], '/' . $shop_page->post_name ) && intval( get_option( 'page_on_front' ) ) !== $shop_page_id ) {
			$this->add_crumb( get_the_title( $shop_page ), get_permalink( $shop_page ) );
		}
	}

	/**
	 * Is home trail..
	 */
	protected function add_crumbs_home() {
		$this->add_crumb( single_post_title( '', false ) );
	}

	/**
	 * 404 trail.
	 */
	protected function add_crumbs_404() {
		$this->add_crumb( __( 'Error 404', 'woocommerce' ) );
	}

	/**
	 * Attachment trail.
	 */
	protected function add_crumbs_attachment() {
		global $post;

		$this->add_crumbs_single( $post->post_parent, get_permalink( $post->post_parent ) );
		$this->add_crumb( get_the_title(), get_permalink() );
	}

	/**
	 * Single post trail.
	 *
	 * @param int    $post_id   Post ID.
	 * @param string $permalink Post permalink.
	 */
	protected function add_crumbs_single( $post_id = 0, $permalink = '' ) {
		if ( ! $post_id ) {
			global $post;
		} else {
			$post = get_post( $post_id ); // WPCS: override ok.
		}

		if ( ! $permalink ) {
			$permalink = get_permalink( $post );
		}

		if ( 'product' === get_post_type( $post ) ) {
			$this->prepend_shop_page();

			$terms = wc_get_product_terms(
				$post->ID,
				'product_cat',
				apply_filters(
					'woocommerce_breadcrumb_product_terms_args',
					array(
						'orderby' => 'parent',
						'order'   => 'DESC',
					)
				)
			);

			if ( $terms ) {
				$main_term = apply_filters( 'woocommerce_breadcrumb_main_term', $terms[0], $terms );
				$this->term_ancestors( $main_term->term_id, 'product_cat' );
				$this->add_crumb( $main_term->name, get_term_link( $main_term ) );
			}
		} elseif ( 'post' !== get_post_type( $post ) ) {
			$post_type = get_post_type_object( get_post_type( $post ) );

			if ( ! empty( $post_type->has_archive ) ) {
				$this->add_crumb( $post_type->labels->singular_name, get_post_type_archive_link( get_post_type( $post ) ) );
			}
		} else {
			$cat = current( get_the_category( $post ) );
			if ( $cat ) {
				$this->term_ancestors( $cat->term_id, 'category' );
				$this->add_crumb( $cat->name, get_term_link( $cat ) );
			}
		}

		$this->add_crumb( get_the_title( $post ), $permalink );
	}

	/**
	 * Page trail.
	 */
	protected function add_crumbs_page() {
		global $post;

		if ( $post->post_parent ) {
			$parent_crumbs = array();
			$parent_id     = $post->post_parent;

			while ( $parent_id ) {
				$page            = get_post( $parent_id );
				$parent_id       = $page->post_parent;
				$parent_crumbs[] = array( get_the_title( $page->ID ), get_permalink( $page->ID ) );
			}

			$parent_crumbs = array_reverse( $parent_crumbs );

			foreach ( $parent_crumbs as $crumb ) {
				$this->add_crumb( $crumb[0], $crumb[1] );
			}
		}

		$this->add_crumb( get_the_title(), get_permalink() );
		$this->endpoint_trail();
	}

	/**
	 * Product category trail.
	 */
	protected function add_crumbs_product_category() {
		$current_term = $GLOBALS['wp_query']->get_queried_object();

		$this->prepend_shop_page();
		$this->term_ancestors( $current_term->term_id, 'product_cat' );
		$this->add_crumb( $current_term->name, get_term_link( $current_term, 'product_cat' ) );
	}

	/**
	 * Product tag trail.
	 */
	protected function add_crumbs_product_tag() {
		$current_term = $GLOBALS['wp_query']->get_queried_object();

		$this->prepend_shop_page();

		/* translators: %s: product tag */
		$this->add_crumb( sprintf( __( 'Products tagged &ldquo;%s&rdquo;', 'woocommerce' ), $current_term->name ), get_term_link( $current_term, 'product_tag' ) );
	}

	/**
	 * Shop breadcrumb.
	 */
	protected function add_crumbs_shop() {
		if ( intval( get_option( 'page_on_front' ) ) === wc_get_page_id( 'shop' ) ) {
			return;
		}

		$_name = wc_get_page_id( 'shop' ) ? get_the_title( wc_get_page_id( 'shop' ) ) : '';

		if ( ! $_name ) {
			$product_post_type = get_post_type_object( 'product' );
			$_name             = $product_post_type->labels->name;
		}

		$this->add_crumb( $_name, get_post_type_archive_link( 'product' ) );
	}

	/**
	 * Post type archive trail.
	 */
	protected function add_crumbs_post_type_archive() {
		$post_type = get_post_type_object( get_post_type() );

		if ( $post_type ) {
			$this->add_crumb( $post_type->labels->name, get_post_type_archive_link( get_post_type() ) );
		}
	}

	/**
	 * Category trail.
	 */
	protected function add_crumbs_category() {
		$this_category = get_category( $GLOBALS['wp_query']->get_queried_object() );

		if ( 0 !== intval( $this_category->parent ) ) {
			$this->term_ancestors( $this_category->term_id, 'category' );
		}

		$this->add_crumb( single_cat_title( '', false ), get_category_link( $this_category->term_id ) );
	}

	/**
	 * Tag trail.
	 */
	protected function add_crumbs_tag() {
		$queried_object = $GLOBALS['wp_query']->get_queried_object();

		/* translators: %s: tag name */
		$this->add_crumb( sprintf( __( 'Posts tagged &ldquo;%s&rdquo;', 'woocommerce' ), single_tag_title( '', false ) ), get_tag_link( $queried_object->term_id ) );
	}

	/**
	 * Add crumbs for date based archives.
	 */
	protected function add_crumbs_date() {
		if ( is_year() || is_month() || is_day() ) {
			$this->add_crumb( get_the_time( 'Y' ), get_year_link( get_the_time( 'Y' ) ) );
		}
		if ( is_month() || is_day() ) {
			$this->add_crumb( get_the_time( 'F' ), get_month_link( get_the_time( 'Y' ), get_the_time( 'm' ) ) );
		}
		if ( is_day() ) {
			$this->add_crumb( get_the_time( 'd' ) );
		}
	}

	/**
	 * Add crumbs for taxonomies
	 */
	protected function add_crumbs_tax() {
		$this_term = $GLOBALS['wp_query']->get_queried_object();
		$taxonomy  = get_taxonomy( $this_term->taxonomy );

		$this->add_crumb( $taxonomy->labels->name );

		if ( 0 !== intval( $this_term->parent ) ) {
			$this->term_ancestors( $this_term->term_id, $this_term->taxonomy );
		}

		$this->add_crumb( single_term_title( '', false ), get_term_link( $this_term->term_id, $this_term->taxonomy ) );
	}

	/**
	 * Add a breadcrumb for author archives.
	 */
	protected function add_crumbs_author() {
		global $author;

		$userdata = get_userdata( $author );

		/* translators: %s: author name */
		$this->add_crumb( sprintf( __( 'Author: %s', 'woocommerce' ), $userdata->display_name ) );
	}

	/**
	 * Add crumbs for a term.
	 *
	 * @param int    $term_id  Term ID.
	 * @param string $taxonomy Taxonomy.
	 */
	protected function term_ancestors( $term_id, $taxonomy ) {
		$ancestors = get_ancestors( $term_id, $taxonomy );
		$ancestors = array_reverse( $ancestors );

		foreach ( $ancestors as $ancestor ) {
			$ancestor = get_term( $ancestor, $taxonomy );

			if ( ! is_wp_error( $ancestor ) && $ancestor ) {
				$this->add_crumb( $ancestor->name, get_term_link( $ancestor ) );
			}
		}
	}

	/**
	 * Endpoints.
	 */
	protected function endpoint_trail() {
		$action         = isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : '';
		$endpoint       = is_wc_endpoint_url() ? WC()->query->get_current_endpoint() : '';
		$endpoint_title = $endpoint ? WC()->query->get_endpoint_title( $endpoint, $action ) : '';

		if ( $endpoint_title ) {
			$this->add_crumb( $endpoint_title );
		}
	}

	/**
	 * Add a breadcrumb for search results.
	 */
	protected function search_trail() {
		if ( is_search() ) {
			/* translators: %s: search term */
			$this->add_crumb( sprintf( __( 'Search results for &ldquo;%s&rdquo;', 'woocommerce' ), get_search_query() ), remove_query_arg( 'paged' ) );
		}
	}

	/**
	 * Add a breadcrumb for pagination.
	 */
	protected function paged_trail() {
		if ( get_query_var( 'paged' ) && 'subcategories' !== woocommerce_get_loop_display_mode() ) {
			/* translators: %d: page number */
			$this->add_crumb( sprintf( __( 'Page %d', 'woocommerce' ), get_query_var( 'paged' ) ) );
		}
	}
}
rest-api/Controllers/Version3/class-wc-rest-products-controller.php000064400000133621151336044760021547 0ustar00<?php
/**
 * REST API Products controller
 *
 * Handles requests to the /products endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Products controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Products_V2_Controller
 */
class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';

	/**
	 * Get the images for a product or product variation.
	 *
	 * @param WC_Product|WC_Product_Variation $product Product instance.
	 * @return array
	 */
	protected function get_images( $product ) {
		$images         = array();
		$attachment_ids = array();

		// Add featured image.
		if ( $product->get_image_id() ) {
			$attachment_ids[] = $product->get_image_id();
		}

		// Add gallery images.
		$attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() );

		// Build image data.
		foreach ( $attachment_ids as $attachment_id ) {
			$attachment_post = get_post( $attachment_id );
			if ( is_null( $attachment_post ) ) {
				continue;
			}

			$attachment = wp_get_attachment_image_src( $attachment_id, 'full' );
			if ( ! is_array( $attachment ) ) {
				continue;
			}

			$images[] = array(
				'id'                => (int) $attachment_id,
				'date_created'      => wc_rest_prepare_date_response( $attachment_post->post_date, false ),
				'date_created_gmt'  => wc_rest_prepare_date_response( strtotime( $attachment_post->post_date_gmt ) ),
				'date_modified'     => wc_rest_prepare_date_response( $attachment_post->post_modified, false ),
				'date_modified_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_modified_gmt ) ),
				'src'               => current( $attachment ),
				'name'              => get_the_title( $attachment_id ),
				'alt'               => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ),
			);
		}

		return $images;
	}

	/**
	 * Make extra product orderby features supported by WooCommerce available to the WC API.
	 * This includes 'price', 'popularity', and 'rating'.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return array
	 */
	protected function prepare_objects_query( $request ) {
		$args = WC_REST_CRUD_Controller::prepare_objects_query( $request );

		// Set post_status.
		$args['post_status'] = $request['status'];

		// Taxonomy query to filter products by type, category,
		// tag, shipping class, and attribute.
		$tax_query = array();

		// Map between taxonomy name and arg's key.
		$taxonomies = array(
			'product_cat'            => 'category',
			'product_tag'            => 'tag',
			'product_shipping_class' => 'shipping_class',
		);

		// Set tax_query for each passed arg.
		foreach ( $taxonomies as $taxonomy => $key ) {
			if ( ! empty( $request[ $key ] ) ) {
				$tax_query[] = array(
					'taxonomy' => $taxonomy,
					'field'    => 'term_id',
					'terms'    => $request[ $key ],
				);
			}
		}

		// Filter product type by slug.
		if ( ! empty( $request['type'] ) ) {
			$tax_query[] = array(
				'taxonomy' => 'product_type',
				'field'    => 'slug',
				'terms'    => $request['type'],
			);
		}

		// Filter by attribute and term.
		if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) {
			if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names(), true ) ) {
				$tax_query[] = array(
					'taxonomy' => $request['attribute'],
					'field'    => 'term_id',
					'terms'    => $request['attribute_term'],
				);
			}
		}

		// Build tax_query if taxonomies are set.
		if ( ! empty( $tax_query ) ) {
			if ( ! empty( $args['tax_query'] ) ) {
				$args['tax_query'] = array_merge( $tax_query, $args['tax_query'] ); // WPCS: slow query ok.
			} else {
				$args['tax_query'] = $tax_query; // WPCS: slow query ok.
			}
		}

		// Filter featured.
		if ( is_bool( $request['featured'] ) ) {
			$args['tax_query'][] = array(
				'taxonomy' => 'product_visibility',
				'field'    => 'name',
				'terms'    => 'featured',
				'operator' => true === $request['featured'] ? 'IN' : 'NOT IN',
			);
		}

		// Filter by sku.
		if ( ! empty( $request['sku'] ) ) {
			$skus = explode( ',', $request['sku'] );
			// Include the current string as a SKU too.
			if ( 1 < count( $skus ) ) {
				$skus[] = $request['sku'];
			}

			$args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok.
				$args,
				array(
					'key'     => '_sku',
					'value'   => $skus,
					'compare' => 'IN',
				)
			);
		}

		// Filter by tax class.
		if ( ! empty( $request['tax_class'] ) ) {
			$args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok.
				$args,
				array(
					'key'   => '_tax_class',
					'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '',
				)
			);
		}

		// Price filter.
		if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) {
			$args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) );  // WPCS: slow query ok.
		}

		// Filter product by stock_status.
		if ( ! empty( $request['stock_status'] ) ) {
			$args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok.
				$args,
				array(
					'key'   => '_stock_status',
					'value' => $request['stock_status'],
				)
			);
		}

		// Filter by on sale products.
		if ( is_bool( $request['on_sale'] ) ) {
			$on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in';
			$on_sale_ids = wc_get_product_ids_on_sale();

			// Use 0 when there's no on sale products to avoid return all products.
			$on_sale_ids = empty( $on_sale_ids ) ? array( 0 ) : $on_sale_ids;

			$args[ $on_sale_key ] += $on_sale_ids;
		}

		// Force the post_type argument, since it's not a user input variable.
		if ( ! empty( $request['sku'] ) ) {
			$args['post_type'] = array( 'product', 'product_variation' );
		} else {
			$args['post_type'] = $this->post_type;
		}

		$orderby = $request->get_param( 'orderby' );
		$order   = $request->get_param( 'order' );

		$ordering_args   = WC()->query->get_catalog_ordering_args( $orderby, $order );
		$args['orderby'] = $ordering_args['orderby'];
		$args['order']   = $ordering_args['order'];
		if ( $ordering_args['meta_key'] ) {
			$args['meta_key'] = $ordering_args['meta_key']; // WPCS: slow query ok.
		}

		return $args;
	}

	/**
	 * Set product images.
	 *
	 * @throws WC_REST_Exception REST API exceptions.
	 * @param WC_Product $product Product instance.
	 * @param array      $images  Images data.
	 * @return WC_Product
	 */
	protected function set_product_images( $product, $images ) {
		$images = is_array( $images ) ? array_filter( $images ) : array();

		if ( ! empty( $images ) ) {
			$gallery = array();

			foreach ( $images as $index => $image ) {
				$attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0;

				if ( 0 === $attachment_id && isset( $image['src'] ) ) {
					$upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) );

					if ( is_wp_error( $upload ) ) {
						if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $product->get_id(), $images ) ) {
							throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 );
						} else {
							continue;
						}
					}

					$attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->get_id() );
				}

				if ( ! wp_attachment_is_image( $attachment_id ) ) {
					/* translators: %s: image ID */
					throw new WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 );
				}

				$featured_image = $product->get_image_id();

				if ( 0 === $index ) {
					$product->set_image_id( $attachment_id );
				} else {
					$gallery[] = $attachment_id;
				}

				// Set the image alt if present.
				if ( ! empty( $image['alt'] ) ) {
					update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) );
				}

				// Set the image name if present.
				if ( ! empty( $image['name'] ) ) {
					wp_update_post(
						array(
							'ID'         => $attachment_id,
							'post_title' => $image['name'],
						)
					);
				}
			}

			$product->set_gallery_image_ids( $gallery );
		} else {
			$product->set_image_id( '' );
			$product->set_gallery_image_ids( array() );
		}

		return $product;
	}

	/**
	 * Prepare a single product for create or update.
	 *
	 * @param  WP_REST_Request $request Request object.
	 * @param  bool            $creating If is creating a new object.
	 * @return WP_Error|WC_Data
	 */
	protected function prepare_object_for_database( $request, $creating = false ) {
		$id = isset( $request['id'] ) ? absint( $request['id'] ) : 0;

		// Type is the most important part here because we need to be using the correct class and methods.
		if ( isset( $request['type'] ) ) {
			$classname = WC_Product_Factory::get_classname_from_product_type( $request['type'] );

			if ( ! class_exists( $classname ) ) {
				$classname = 'WC_Product_Simple';
			}

			$product = new $classname( $id );
		} elseif ( isset( $request['id'] ) ) {
			$product = wc_get_product( $id );
		} else {
			$product = new WC_Product_Simple();
		}

		if ( 'variation' === $product->get_type() ) {
			return new WP_Error(
				"woocommerce_rest_invalid_{$this->post_type}_id",
				__( 'To manipulate product variations you should use the /products/&lt;product_id&gt;/variations/&lt;id&gt; endpoint.', 'woocommerce' ),
				array(
					'status' => 404,
				)
			);
		}

		// Post title.
		if ( isset( $request['name'] ) ) {
			$product->set_name( wp_filter_post_kses( $request['name'] ) );
		}

		// Post content.
		if ( isset( $request['description'] ) ) {
			$product->set_description( wp_filter_post_kses( $request['description'] ) );
		}

		// Post excerpt.
		if ( isset( $request['short_description'] ) ) {
			$product->set_short_description( wp_filter_post_kses( $request['short_description'] ) );
		}

		// Post status.
		if ( isset( $request['status'] ) ) {
			$product->set_status( get_post_status_object( $request['status'] ) ? $request['status'] : 'draft' );
		}

		// Post slug.
		if ( isset( $request['slug'] ) ) {
			$product->set_slug( $request['slug'] );
		}

		// Menu order.
		if ( isset( $request['menu_order'] ) ) {
			$product->set_menu_order( $request['menu_order'] );
		}

		// Comment status.
		if ( isset( $request['reviews_allowed'] ) ) {
			$product->set_reviews_allowed( $request['reviews_allowed'] );
		}

		// Virtual.
		if ( isset( $request['virtual'] ) ) {
			$product->set_virtual( $request['virtual'] );
		}

		// Tax status.
		if ( isset( $request['tax_status'] ) ) {
			$product->set_tax_status( $request['tax_status'] );
		}

		// Tax Class.
		if ( isset( $request['tax_class'] ) ) {
			$product->set_tax_class( $request['tax_class'] );
		}

		// Catalog Visibility.
		if ( isset( $request['catalog_visibility'] ) ) {
			$product->set_catalog_visibility( $request['catalog_visibility'] );
		}

		// Purchase Note.
		if ( isset( $request['purchase_note'] ) ) {
			$product->set_purchase_note( wp_kses_post( wp_unslash( $request['purchase_note'] ) ) );
		}

		// Featured Product.
		if ( isset( $request['featured'] ) ) {
			$product->set_featured( $request['featured'] );
		}

		// Shipping data.
		$product = $this->save_product_shipping_data( $product, $request );

		// SKU.
		if ( isset( $request['sku'] ) ) {
			$product->set_sku( wc_clean( $request['sku'] ) );
		}

		// Attributes.
		if ( isset( $request['attributes'] ) ) {
			$attributes = array();

			foreach ( $request['attributes'] as $attribute ) {
				$attribute_id   = 0;
				$attribute_name = '';

				// Check ID for global attributes or name for product attributes.
				if ( ! empty( $attribute['id'] ) ) {
					$attribute_id   = absint( $attribute['id'] );
					$attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
				} elseif ( ! empty( $attribute['name'] ) ) {
					$attribute_name = wc_clean( $attribute['name'] );
				}

				if ( ! $attribute_id && ! $attribute_name ) {
					continue;
				}

				if ( $attribute_id ) {

					if ( isset( $attribute['options'] ) ) {
						$options = $attribute['options'];

						if ( ! is_array( $attribute['options'] ) ) {
							// Text based attributes - Posted values are term names.
							$options = explode( WC_DELIMITER, $options );
						}

						$values = array_map( 'wc_sanitize_term_text_based', $options );
						$values = array_filter( $values, 'strlen' );
					} else {
						$values = array();
					}

					if ( ! empty( $values ) ) {
						// Add attribute to array, but don't set values.
						$attribute_object = new WC_Product_Attribute();
						$attribute_object->set_id( $attribute_id );
						$attribute_object->set_name( $attribute_name );
						$attribute_object->set_options( $values );
						$attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' );
						$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
						$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
						$attributes[] = $attribute_object;
					}
				} elseif ( isset( $attribute['options'] ) ) {
					// Custom attribute - Add attribute to array and set the values.
					if ( is_array( $attribute['options'] ) ) {
						$values = $attribute['options'];
					} else {
						$values = explode( WC_DELIMITER, $attribute['options'] );
					}
					$attribute_object = new WC_Product_Attribute();
					$attribute_object->set_name( $attribute_name );
					$attribute_object->set_options( $values );
					$attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' );
					$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
					$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
					$attributes[] = $attribute_object;
				}
			}
			$product->set_attributes( $attributes );
		}

		// Sales and prices.
		if ( in_array( $product->get_type(), array( 'variable', 'grouped' ), true ) ) {
			$product->set_regular_price( '' );
			$product->set_sale_price( '' );
			$product->set_date_on_sale_to( '' );
			$product->set_date_on_sale_from( '' );
			$product->set_price( '' );
		} else {
			// Regular Price.
			if ( isset( $request['regular_price'] ) ) {
				$product->set_regular_price( $request['regular_price'] );
			}

			// Sale Price.
			if ( isset( $request['sale_price'] ) ) {
				$product->set_sale_price( $request['sale_price'] );
			}

			if ( isset( $request['date_on_sale_from'] ) ) {
				$product->set_date_on_sale_from( $request['date_on_sale_from'] );
			}

			if ( isset( $request['date_on_sale_from_gmt'] ) ) {
				$product->set_date_on_sale_from( $request['date_on_sale_from_gmt'] ? strtotime( $request['date_on_sale_from_gmt'] ) : null );
			}

			if ( isset( $request['date_on_sale_to'] ) ) {
				$product->set_date_on_sale_to( $request['date_on_sale_to'] );
			}

			if ( isset( $request['date_on_sale_to_gmt'] ) ) {
				$product->set_date_on_sale_to( $request['date_on_sale_to_gmt'] ? strtotime( $request['date_on_sale_to_gmt'] ) : null );
			}
		}

		// Product parent ID.
		if ( isset( $request['parent_id'] ) ) {
			$product->set_parent_id( $request['parent_id'] );
		}

		// Sold individually.
		if ( isset( $request['sold_individually'] ) ) {
			$product->set_sold_individually( $request['sold_individually'] );
		}

		// Stock status; stock_status has priority over in_stock.
		if ( isset( $request['stock_status'] ) ) {
			$stock_status = $request['stock_status'];
		} else {
			$stock_status = $product->get_stock_status();
		}

		// Stock data.
		if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
			// Manage stock.
			if ( isset( $request['manage_stock'] ) ) {
				$product->set_manage_stock( $request['manage_stock'] );
			}

			// Backorders.
			if ( isset( $request['backorders'] ) ) {
				$product->set_backorders( $request['backorders'] );
			}

			if ( $product->is_type( 'grouped' ) ) {
				$product->set_manage_stock( 'no' );
				$product->set_backorders( 'no' );
				$product->set_stock_quantity( '' );
				$product->set_stock_status( $stock_status );
			} elseif ( $product->is_type( 'external' ) ) {
				$product->set_manage_stock( 'no' );
				$product->set_backorders( 'no' );
				$product->set_stock_quantity( '' );
				$product->set_stock_status( 'instock' );
			} elseif ( $product->get_manage_stock() ) {
				// Stock status is always determined by children so sync later.
				if ( ! $product->is_type( 'variable' ) ) {
					$product->set_stock_status( $stock_status );
				}

				// Stock quantity.
				if ( isset( $request['stock_quantity'] ) ) {
					$product->set_stock_quantity( wc_stock_amount( $request['stock_quantity'] ) );
				} elseif ( isset( $request['inventory_delta'] ) ) {
					$stock_quantity  = wc_stock_amount( $product->get_stock_quantity() );
					$stock_quantity += wc_stock_amount( $request['inventory_delta'] );
					$product->set_stock_quantity( wc_stock_amount( $stock_quantity ) );
				}

				// Low stock amount.
				// isset() returns false for value null, thus we need to check whether the value has been sent by the request.
				if ( array_key_exists( 'low_stock_amount', $request->get_params() ) ) {
					if ( null === $request['low_stock_amount'] ) {
						$product->set_low_stock_amount( '' );
					} else {
						$product->set_low_stock_amount( wc_stock_amount( $request['low_stock_amount'] ) );
					}
				}
			} else {
				// Don't manage stock.
				$product->set_manage_stock( 'no' );
				$product->set_stock_quantity( '' );
				$product->set_stock_status( $stock_status );
				$product->set_low_stock_amount( '' );
			}
		} elseif ( ! $product->is_type( 'variable' ) ) {
			$product->set_stock_status( $stock_status );
		}

		// Upsells.
		if ( isset( $request['upsell_ids'] ) ) {
			$upsells = array();
			$ids     = $request['upsell_ids'];

			if ( ! empty( $ids ) ) {
				foreach ( $ids as $id ) {
					if ( $id && $id > 0 ) {
						$upsells[] = $id;
					}
				}
			}

			$product->set_upsell_ids( $upsells );
		}

		// Cross sells.
		if ( isset( $request['cross_sell_ids'] ) ) {
			$crosssells = array();
			$ids        = $request['cross_sell_ids'];

			if ( ! empty( $ids ) ) {
				foreach ( $ids as $id ) {
					if ( $id && $id > 0 ) {
						$crosssells[] = $id;
					}
				}
			}

			$product->set_cross_sell_ids( $crosssells );
		}

		// Product categories.
		if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) {
			$product = $this->save_taxonomy_terms( $product, $request['categories'] );
		}

		// Product tags.
		if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) {
			$new_tags = array();

			foreach ( $request['tags'] as $tag ) {
				if ( ! isset( $tag['name'] ) ) {
					$new_tags[] = $tag;
					continue;
				}

				if ( ! term_exists( $tag['name'], 'product_tag' ) ) {
					// Create the tag if it doesn't exist.
					$term = wp_insert_term( $tag['name'], 'product_tag' );

					if ( ! is_wp_error( $term ) ) {
						$new_tags[] = array(
							'id' => $term['term_id'],
						);

						continue;
					}
				} else {
					// Tag exists, assume user wants to set the product with this tag.
					$new_tags[] = array(
						'id' => get_term_by( 'name', $tag['name'], 'product_tag' )->term_id,
					);
				}
			}

			$product = $this->save_taxonomy_terms( $product, $new_tags, 'tag' );
		}

		// Downloadable.
		if ( isset( $request['downloadable'] ) ) {
			$product->set_downloadable( $request['downloadable'] );
		}

		// Downloadable options.
		if ( $product->get_downloadable() ) {

			// Downloadable files.
			if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) {
				$product = $this->save_downloadable_files( $product, $request['downloads'] );
			}

			// Download limit.
			if ( isset( $request['download_limit'] ) ) {
				$product->set_download_limit( $request['download_limit'] );
			}

			// Download expiry.
			if ( isset( $request['download_expiry'] ) ) {
				$product->set_download_expiry( $request['download_expiry'] );
			}
		}

		// Product url and button text for external products.
		if ( $product->is_type( 'external' ) ) {
			if ( isset( $request['external_url'] ) ) {
				$product->set_product_url( $request['external_url'] );
			}

			if ( isset( $request['button_text'] ) ) {
				$product->set_button_text( $request['button_text'] );
			}
		}

		// Save default attributes for variable products.
		if ( $product->is_type( 'variable' ) ) {
			$product = $this->save_default_attributes( $product, $request );
		}

		// Set children for a grouped product.
		if ( $product->is_type( 'grouped' ) && isset( $request['grouped_products'] ) ) {
			$product->set_children( $request['grouped_products'] );
		}

		// Check for featured/gallery images, upload it and set it.
		if ( isset( $request['images'] ) ) {
			$product = $this->set_product_images( $product, $request['images'] );
		}

		// Allow set meta_data.
		if ( is_array( $request['meta_data'] ) ) {
			foreach ( $request['meta_data'] as $meta ) {
				$product->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
			}
		}

		if ( ! empty( $request['date_created'] ) ) {
			$date = rest_parse_date( $request['date_created'] );

			if ( $date ) {
				$product->set_date_created( $date );
			}
		}

		if ( ! empty( $request['date_created_gmt'] ) ) {
			$date = rest_parse_date( $request['date_created_gmt'], true );

			if ( $date ) {
				$product->set_date_created( $date );
			}
		}

		/**
		 * Filters an object before it is inserted via the REST API.
		 *
		 * The dynamic portion of the hook name, `$this->post_type`,
		 * refers to the object type slug.
		 *
		 * @param WC_Data         $product  Object object.
		 * @param WP_REST_Request $request  Request object.
		 * @param bool            $creating If is creating a new object.
		 */
		return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $product, $request, $creating );
	}

	/**
	 * Get the Product's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$weight_unit    = get_option( 'woocommerce_weight_unit' );
		$dimension_unit = get_option( 'woocommerce_dimension_unit' );
		$schema         = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => $this->post_type,
			'type'       => 'object',
			'properties' => array(
				'id'                    => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'name'                  => array(
					'description' => __( 'Product name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'slug'                  => array(
					'description' => __( 'Product slug.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'permalink'             => array(
					'description' => __( 'Product URL.', 'woocommerce' ),
					'type'        => 'string',
					'format'      => 'uri',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_created'          => array(
					'description' => __( "The date the product was created, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'date_created_gmt'      => array(
					'description' => __( 'The date the product was created, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'date_modified'         => array(
					'description' => __( "The date the product was last modified, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_modified_gmt'     => array(
					'description' => __( 'The date the product was last modified, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'type'                  => array(
					'description' => __( 'Product type.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'simple',
					'enum'        => array_keys( wc_get_product_types() ),
					'context'     => array( 'view', 'edit' ),
				),
				'status'                => array(
					'description' => __( 'Product status (post status).', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'publish',
					'enum'        => array_merge( array_keys( get_post_statuses() ), array( 'future' ) ),
					'context'     => array( 'view', 'edit' ),
				),
				'featured'              => array(
					'description' => __( 'Featured product.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'catalog_visibility'    => array(
					'description' => __( 'Catalog visibility.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'visible',
					'enum'        => array( 'visible', 'catalog', 'search', 'hidden' ),
					'context'     => array( 'view', 'edit' ),
				),
				'description'           => array(
					'description' => __( 'Product description.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'short_description'     => array(
					'description' => __( 'Product short description.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'sku'                   => array(
					'description' => __( 'Unique identifier.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'price'                 => array(
					'description' => __( 'Current product price.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'regular_price'         => array(
					'description' => __( 'Product regular price.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'sale_price'            => array(
					'description' => __( 'Product sale price.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'date_on_sale_from'     => array(
					'description' => __( "Start date of sale price, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'date_on_sale_from_gmt' => array(
					'description' => __( 'Start date of sale price, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'date_on_sale_to'       => array(
					'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'date_on_sale_to_gmt'   => array(
					'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'price_html'            => array(
					'description' => __( 'Price formatted in HTML.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'on_sale'               => array(
					'description' => __( 'Shows if the product is on sale.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'purchasable'           => array(
					'description' => __( 'Shows if the product can be bought.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'total_sales'           => array(
					'description' => __( 'Amount of sales.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'virtual'               => array(
					'description' => __( 'If the product is virtual.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'downloadable'          => array(
					'description' => __( 'If the product is downloadable.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'downloads'             => array(
					'description' => __( 'List of downloadable files.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'   => array(
								'description' => __( 'File ID.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'name' => array(
								'description' => __( 'File name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'file' => array(
								'description' => __( 'File URL.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
						),
					),
				),
				'download_limit'        => array(
					'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ),
					'type'        => 'integer',
					'default'     => -1,
					'context'     => array( 'view', 'edit' ),
				),
				'download_expiry'       => array(
					'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ),
					'type'        => 'integer',
					'default'     => -1,
					'context'     => array( 'view', 'edit' ),
				),
				'external_url'          => array(
					'description' => __( 'Product external URL. Only for external products.', 'woocommerce' ),
					'type'        => 'string',
					'format'      => 'uri',
					'context'     => array( 'view', 'edit' ),
				),
				'button_text'           => array(
					'description' => __( 'Product external button text. Only for external products.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'tax_status'            => array(
					'description' => __( 'Tax status.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'taxable',
					'enum'        => array( 'taxable', 'shipping', 'none' ),
					'context'     => array( 'view', 'edit' ),
				),
				'tax_class'             => array(
					'description' => __( 'Tax class.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'manage_stock'          => array(
					'description' => __( 'Stock management at product level.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'stock_quantity'        => array(
					'description' => __( 'Stock quantity.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'stock_status'          => array(
					'description' => __( 'Controls the stock status of the product.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'instock',
					'enum'        => array_keys( wc_get_product_stock_status_options() ),
					'context'     => array( 'view', 'edit' ),
				),
				'backorders'            => array(
					'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'no',
					'enum'        => array( 'no', 'notify', 'yes' ),
					'context'     => array( 'view', 'edit' ),
				),
				'backorders_allowed'    => array(
					'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'backordered'           => array(
					'description' => __( 'Shows if the product is on backordered.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'low_stock_amount'       => array(
					'description' => __( 'Low Stock amount for the product.', 'woocommerce' ),
					'type'        => array( 'integer', 'null' ),
					'context'     => array( 'view', 'edit' ),
				),
				'sold_individually'     => array(
					'description' => __( 'Allow one item to be bought in a single order.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'weight'                => array(
					/* translators: %s: weight unit */
					'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), $weight_unit ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'dimensions'            => array(
					'description' => __( 'Product dimensions.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'properties'  => array(
						'length' => array(
							/* translators: %s: dimension unit */
							'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), $dimension_unit ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'width'  => array(
							/* translators: %s: dimension unit */
							'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), $dimension_unit ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'height' => array(
							/* translators: %s: dimension unit */
							'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), $dimension_unit ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
					),
				),
				'shipping_required'     => array(
					'description' => __( 'Shows if the product need to be shipped.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'shipping_taxable'      => array(
					'description' => __( 'Shows whether or not the product shipping is taxable.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'shipping_class'        => array(
					'description' => __( 'Shipping class slug.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'shipping_class_id'     => array(
					'description' => __( 'Shipping class ID.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'reviews_allowed'       => array(
					'description' => __( 'Allow reviews.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => true,
					'context'     => array( 'view', 'edit' ),
				),
				'average_rating'        => array(
					'description' => __( 'Reviews average rating.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'rating_count'          => array(
					'description' => __( 'Amount of reviews that the product have.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'related_ids'           => array(
					'description' => __( 'List of related products IDs.', 'woocommerce' ),
					'type'        => 'array',
					'items'       => array(
						'type' => 'integer',
					),
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'upsell_ids'            => array(
					'description' => __( 'List of up-sell products IDs.', 'woocommerce' ),
					'type'        => 'array',
					'items'       => array(
						'type' => 'integer',
					),
					'context'     => array( 'view', 'edit' ),
				),
				'cross_sell_ids'        => array(
					'description' => __( 'List of cross-sell products IDs.', 'woocommerce' ),
					'type'        => 'array',
					'items'       => array(
						'type' => 'integer',
					),
					'context'     => array( 'view', 'edit' ),
				),
				'parent_id'             => array(
					'description' => __( 'Product parent ID.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'purchase_note'         => array(
					'description' => __( 'Optional note to send the customer after purchase.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'categories'            => array(
					'description' => __( 'List of categories.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'   => array(
								'description' => __( 'Category ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'name' => array(
								'description' => __( 'Category name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'slug' => array(
								'description' => __( 'Category slug.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
						),
					),
				),
				'tags'                  => array(
					'description' => __( 'List of tags.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'   => array(
								'description' => __( 'Tag ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'name' => array(
								'description' => __( 'Tag name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'slug' => array(
								'description' => __( 'Tag slug.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
						),
					),
				),
				'images'                => array(
					'description' => __( 'List of images.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'                => array(
								'description' => __( 'Image ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'date_created'      => array(
								'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_created_gmt'  => array(
								'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_modified'     => array(
								'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_modified_gmt' => array(
								'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'src'               => array(
								'description' => __( 'Image URL.', 'woocommerce' ),
								'type'        => 'string',
								'format'      => 'uri',
								'context'     => array( 'view', 'edit' ),
							),
							'name'              => array(
								'description' => __( 'Image name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'alt'               => array(
								'description' => __( 'Image alternative text.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
						),
					),
				),
				'attributes'            => array(
					'description' => __( 'List of attributes.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'        => array(
								'description' => __( 'Attribute ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'name'      => array(
								'description' => __( 'Attribute name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'position'  => array(
								'description' => __( 'Attribute position.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'visible'   => array(
								'description' => __( "Define if the attribute is visible on the \"Additional information\" tab in the product's page.", 'woocommerce' ),
								'type'        => 'boolean',
								'default'     => false,
								'context'     => array( 'view', 'edit' ),
							),
							'variation' => array(
								'description' => __( 'Define if the attribute can be used as variation.', 'woocommerce' ),
								'type'        => 'boolean',
								'default'     => false,
								'context'     => array( 'view', 'edit' ),
							),
							'options'   => array(
								'description' => __( 'List of available term names of the attribute.', 'woocommerce' ),
								'type'        => 'array',
								'items'       => array(
									'type' => 'string',
								),
								'context'     => array( 'view', 'edit' ),
							),
						),
					),
				),
				'default_attributes'    => array(
					'description' => __( 'Defaults variation attributes.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'     => array(
								'description' => __( 'Attribute ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'name'   => array(
								'description' => __( 'Attribute name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'option' => array(
								'description' => __( 'Selected attribute term name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
						),
					),
				),
				'variations'            => array(
					'description' => __( 'List of variations IDs.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type' => 'integer',
					),
					'readonly'    => true,
				),
				'grouped_products'      => array(
					'description' => __( 'List of grouped products ID.', 'woocommerce' ),
					'type'        => 'array',
					'items'       => array(
						'type' => 'integer',
					),
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'menu_order'            => array(
					'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'meta_data'             => array(
					'description' => __( 'Meta data.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'    => array(
								'description' => __( 'Meta ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'key'   => array(
								'description' => __( 'Meta key.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'value' => array(
								'description' => __( 'Meta value.', 'woocommerce' ),
								'type'        => 'mixed',
								'context'     => array( 'view', 'edit' ),
							),
						),
					),
				),
			),
		);
		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Add new options for 'orderby' to the collection params.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params                    = parent::get_collection_params();
		$params['orderby']['enum'] = array_merge( $params['orderby']['enum'], array( 'price', 'popularity', 'rating' ) );

		unset( $params['in_stock'] );
		$params['stock_status'] = array(
			'description'       => __( 'Limit result set to products with specified stock status.', 'woocommerce' ),
			'type'              => 'string',
			'enum'              => array_keys( wc_get_product_stock_status_options() ),
			'sanitize_callback' => 'sanitize_text_field',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}

	/**
	 * Get product data.
	 *
	 * @param WC_Product $product Product instance.
	 * @param string     $context Request context. Options: 'view' and 'edit'.
	 *
	 * @return array
	 */
	protected function get_product_data( $product, $context = 'view' ) {
		$data = parent::get_product_data( ...func_get_args() );
		// Add stock_status if needed.
		if ( isset( $this->request ) ) {
			$fields = $this->get_fields_for_response( $this->request );
			if ( in_array( 'stock_status', $fields ) ) {
				$data['stock_status'] = $product->get_stock_status( $context );
			}
		}
		return $data;
	}
}
rest-api/Controllers/Version3/class-wc-rest-shipping-zone-methods-controller.php000064400000002100151336044760024122 0ustar00<?php
/**
 * REST API Shipping Zone Methods controller
 *
 * Handles requests to the /shipping/zones/<id>/methods endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Shipping Zone Methods class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Shipping_Zone_Methods_V2_Controller
 */
class WC_REST_Shipping_Zone_Methods_Controller extends WC_REST_Shipping_Zone_Methods_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';

	/**
	 * Get the settings schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		// Get parent schema to append additional supported settings types for shipping zone method.
		$schema = parent::get_item_schema();

		// Append additional settings supported types (class, order).
		$schema['properties']['settings']['properties']['type']['enum'][] = 'class';
		$schema['properties']['settings']['properties']['type']['enum'][] = 'order';

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version3/class-wc-rest-network-orders-controller.php000064400000000772151336044760022671 0ustar00<?php
/**
 * REST API Network Orders controller
 *
 * Handles requests to the /orders/network endpoint
 *
 * @package WooCommerce\RestApi
 * @since   3.4.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Network Orders controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Network_Orders_V2_Controller
 */
class WC_REST_Network_Orders_Controller extends WC_REST_Network_Orders_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';
}
rest-api/Controllers/Version3/class-wc-rest-data-controller.php000064400000011343151336044760020611 0ustar00<?php
/**
 * REST API Data controller.
 *
 * Handles requests to the /data endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   3.5.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Data controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Controller
 */
class WC_REST_Data_Controller extends WC_REST_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'data';

	/**
	 * Register routes.
	 *
	 * @since 3.5.0
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace, '/' . $this->rest_base, array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Check whether a given request has permission to read site data.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_items_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check whether a given request has permission to read site settings.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_item_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Return the list of data resources.
	 *
	 * @since  3.5.0
	 * @param  WP_REST_Request $request Request data.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_items( $request ) {
		$data      = array();
		$resources = array(
			array(
				'slug'        => 'continents',
				'description' => __( 'List of supported continents, countries, and states.', 'woocommerce' ),
			),
			array(
				'slug'        => 'countries',
				'description' => __( 'List of supported states in a given country.', 'woocommerce' ),
			),
			array(
				'slug'        => 'currencies',
				'description' => __( 'List of supported currencies.', 'woocommerce' ),
			),
		);

		foreach ( $resources as $resource ) {
			$item   = $this->prepare_item_for_response( (object) $resource, $request );
			$data[] = $this->prepare_response_for_collection( $item );
		}

		return rest_ensure_response( $data );
	}

	/**
	 * Prepare a data resource object for serialization.
	 *
	 * @param stdClass        $resource Resource data.
	 * @param WP_REST_Request $request  Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $resource, $request ) {
		$data = array(
			'slug'        => $resource->slug,
			'description' => $resource->description,
		);

		$data = $this->add_additional_fields_to_object( $data, $request );
		$data = $this->filter_response_by_context( $data, 'view' );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );
		$response->add_links( $this->prepare_links( $resource ) );

		return $response;
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param object $item Data object.
	 * @return array Links for the given country.
	 */
	protected function prepare_links( $item ) {
		$links = array(
			'self'       => array(
				'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $item->slug ) ),
			),
			'collection' => array(
				'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
			),
		);

		return $links;
	}

	/**
	 * Get the data index schema, conforming to JSON Schema.
	 *
	 * @since  3.5.0
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'data_index',
			'type'       => 'object',
			'properties' => array(
				'slug'        => array(
					'description' => __( 'Data resource ID.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'description' => array(
					'description' => __( 'Data resource description.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version3/class-wc-rest-posts-controller.php000064400000056526151336044760021064 0ustar00<?php
/**
 * Abstract Rest Posts Controller Class
 *
 * @class WC_REST_Posts_Controller
 * @package WooCommerce\RestApi
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC_REST_Posts_Controller
 *
 * @package WooCommerce\RestApi
 * @version  2.6.0
 */
abstract class WC_REST_Posts_Controller extends WC_REST_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v1';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = '';

	/**
	 * Post type.
	 *
	 * @var string
	 */
	protected $post_type = '';

	/**
	 * Controls visibility on frontend.
	 *
	 * @var string
	 */
	protected $public = false;

	/**
	 * Check if a given request has access to read items.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_items_permissions_check( $request ) {
		if ( ! wc_rest_check_post_permissions( $this->post_type, 'read' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access to create an item.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function create_item_permissions_check( $request ) {
		if ( ! wc_rest_check_post_permissions( $this->post_type, 'create' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access to read an item.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_item_permissions_check( $request ) {
		$post = get_post( (int) $request['id'] );

		if ( $post && ! wc_rest_check_post_permissions( $this->post_type, 'read', $post->ID ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access to update an item.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function update_item_permissions_check( $request ) {
		$post = get_post( (int) $request['id'] );

		if ( $post && ! wc_rest_check_post_permissions( $this->post_type, 'edit', $post->ID ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access to delete an item.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return bool|WP_Error
	 */
	public function delete_item_permissions_check( $request ) {
		$post = get_post( (int) $request['id'] );

		if ( $post && ! wc_rest_check_post_permissions( $this->post_type, 'delete', $post->ID ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access batch create, update and delete items.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 *
	 * @return boolean|WP_Error
	 */
	public function batch_items_permissions_check( $request ) {
		if ( ! wc_rest_check_post_permissions( $this->post_type, 'batch' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Get a single item.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_item( $request ) {
		$id   = (int) $request['id'];
		$post = get_post( $id );

		if ( ! empty( $post->post_type ) && 'product_variation' === $post->post_type && 'product' === $this->post_type ) {
			return new WP_Error( "woocommerce_rest_invalid_{$this->post_type}_id", __( 'To manipulate product variations you should use the /products/&lt;product_id&gt;/variations/&lt;id&gt; endpoint.', 'woocommerce' ), array( 'status' => 404 ) );
		} elseif ( empty( $id ) || empty( $post->ID ) || $post->post_type !== $this->post_type ) {
			return new WP_Error( "woocommerce_rest_invalid_{$this->post_type}_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$data = $this->prepare_item_for_response( $post, $request );
		$response = rest_ensure_response( $data );

		if ( $this->public ) {
			$response->link_header( 'alternate', get_permalink( $id ), array( 'type' => 'text/html' ) );
		}

		return $response;
	}

	/**
	 * Create a single item.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function create_item( $request ) {
		if ( ! empty( $request['id'] ) ) {
			/* translators: %s: post type */
			return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) );
		}

		$post = $this->prepare_item_for_database( $request );
		if ( is_wp_error( $post ) ) {
			return $post;
		}

		$post->post_type = $this->post_type;
		$post_id         = wp_insert_post( $post, true );

		if ( is_wp_error( $post_id ) ) {

			if ( in_array( $post_id->get_error_code(), array( 'db_insert_error' ) ) ) {
				$post_id->add_data( array( 'status' => 500 ) );
			} else {
				$post_id->add_data( array( 'status' => 400 ) );
			}
			return $post_id;
		}
		$post->ID = $post_id;
		$post     = get_post( $post_id );

		$this->update_additional_fields_for_object( $post, $request );

		// Add meta fields.
		$meta_fields = $this->add_post_meta_fields( $post, $request );
		if ( is_wp_error( $meta_fields ) ) {
			// Remove post.
			$this->delete_post( $post );

			return $meta_fields;
		}

		/**
		 * Fires after a single item is created or updated via the REST API.
		 *
		 * @param WP_Post         $post      Post object.
		 * @param WP_REST_Request $request   Request object.
		 * @param boolean         $creating  True when creating item, false when updating.
		 */
		do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true );

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_item_for_response( $post, $request );
		$response = rest_ensure_response( $response );
		$response->set_status( 201 );
		$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post_id ) ) );

		return $response;
	}

	/**
	 * Add post meta fields.
	 *
	 * @param WP_Post         $post Post Object.
	 * @param WP_REST_Request $request WP_REST_Request Object.
	 * @return bool|WP_Error
	 */
	protected function add_post_meta_fields( $post, $request ) {
		return true;
	}

	/**
	 * Delete post.
	 *
	 * @param WP_Post $post Post object.
	 */
	protected function delete_post( $post ) {
		wp_delete_post( $post->ID, true );
	}

	/**
	 * Update a single post.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function update_item( $request ) {
		$id   = (int) $request['id'];
		$post = get_post( $id );

		if ( ! empty( $post->post_type ) && 'product_variation' === $post->post_type && 'product' === $this->post_type ) {
			return new WP_Error( "woocommerce_rest_invalid_{$this->post_type}_id", __( 'To manipulate product variations you should use the /products/&lt;product_id&gt;/variations/&lt;id&gt; endpoint.', 'woocommerce' ), array( 'status' => 404 ) );
		} elseif ( empty( $id ) || empty( $post->ID ) || $post->post_type !== $this->post_type ) {
			return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) );
		}

		$post = $this->prepare_item_for_database( $request );
		if ( is_wp_error( $post ) ) {
			return $post;
		}
		// Convert the post object to an array, otherwise wp_update_post will expect non-escaped input.
		$post_id = wp_update_post( (array) $post, true );
		if ( is_wp_error( $post_id ) ) {
			if ( in_array( $post_id->get_error_code(), array( 'db_update_error' ) ) ) {
				$post_id->add_data( array( 'status' => 500 ) );
			} else {
				$post_id->add_data( array( 'status' => 400 ) );
			}
			return $post_id;
		}

		$post = get_post( $post_id );
		$this->update_additional_fields_for_object( $post, $request );

		// Update meta fields.
		$meta_fields = $this->update_post_meta_fields( $post, $request );
		if ( is_wp_error( $meta_fields ) ) {
			return $meta_fields;
		}

		/**
		 * Fires after a single item is created or updated via the REST API.
		 *
		 * @param WP_Post         $post      Post object.
		 * @param WP_REST_Request $request   Request object.
		 * @param boolean         $creating  True when creating item, false when updating.
		 */
		do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, false );

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_item_for_response( $post, $request );
		return rest_ensure_response( $response );
	}

	/**
	 * Get a collection of posts.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_items( $request ) {
		$args                         = array();
		$args['offset']               = $request['offset'];
		$args['order']                = $request['order'];
		$args['orderby']              = $request['orderby'];
		$args['paged']                = $request['page'];
		$args['post__in']             = $request['include'];
		$args['post__not_in']         = $request['exclude'];
		$args['posts_per_page']       = $request['per_page'];
		$args['name']                 = $request['slug'];
		$args['post_parent__in']      = $request['parent'];
		$args['post_parent__not_in']  = $request['parent_exclude'];
		$args['s']                    = $request['search'];

		$args['date_query'] = array();
		// Set before into date query. Date query must be specified as an array of an array.
		if ( isset( $request['before'] ) ) {
			$args['date_query'][0]['before'] = $request['before'];
		}

		// Set after into date query. Date query must be specified as an array of an array.
		if ( isset( $request['after'] ) ) {
			$args['date_query'][0]['after'] = $request['after'];
		}

		if ( 'wc/v1' === $this->namespace ) {
			if ( is_array( $request['filter'] ) ) {
				$args = array_merge( $args, $request['filter'] );
				unset( $args['filter'] );
			}
		}

		// Force the post_type argument, since it's not a user input variable.
		$args['post_type'] = $this->post_type;

		/**
		 * Filter the query arguments for a request.
		 *
		 * Enables adding extra arguments or setting defaults for a post
		 * collection request.
		 *
		 * @param array           $args    Key value array of query var to query value.
		 * @param WP_REST_Request $request The request used.
		 */
		$args = apply_filters( "woocommerce_rest_{$this->post_type}_query", $args, $request );
		$query_args = $this->prepare_items_query( $args, $request );

		$posts_query = new WP_Query();
		$query_result = $posts_query->query( $query_args );

		$posts = array();
		foreach ( $query_result as $post ) {
			if ( ! wc_rest_check_post_permissions( $this->post_type, 'read', $post->ID ) ) {
				continue;
			}

			$data = $this->prepare_item_for_response( $post, $request );
			$posts[] = $this->prepare_response_for_collection( $data );
		}

		$page = (int) $query_args['paged'];
		$total_posts = $posts_query->found_posts;

		if ( $total_posts < 1 ) {
			// Out-of-bounds, run the query again without LIMIT for total count.
			unset( $query_args['paged'] );
			$count_query = new WP_Query();
			$count_query->query( $query_args );
			$total_posts = $count_query->found_posts;
		}

		$max_pages = ceil( $total_posts / (int) $query_args['posts_per_page'] );

		$response = rest_ensure_response( $posts );
		$response->header( 'X-WP-Total', (int) $total_posts );
		$response->header( 'X-WP-TotalPages', (int) $max_pages );

		$request_params = $request->get_query_params();
		if ( ! empty( $request_params['filter'] ) ) {
			// Normalize the pagination params.
			unset( $request_params['filter']['posts_per_page'] );
			unset( $request_params['filter']['paged'] );
		}
		$base = add_query_arg( $request_params, rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );

		if ( $page > 1 ) {
			$prev_page = $page - 1;
			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}
			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}
		if ( $max_pages > $page ) {
			$next_page = $page + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );
			$response->link_header( 'next', $next_link );
		}

		return $response;
	}

	/**
	 * Delete a single item.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error
	 */
	public function delete_item( $request ) {
		$id    = (int) $request['id'];
		$force = (bool) $request['force'];
		$post  = get_post( $id );

		if ( empty( $id ) || empty( $post->ID ) || $post->post_type !== $this->post_type ) {
			return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$supports_trash = EMPTY_TRASH_DAYS > 0;

		/**
		 * Filter whether an item is trashable.
		 *
		 * Return false to disable trash support for the item.
		 *
		 * @param boolean $supports_trash Whether the item type support trashing.
		 * @param WP_Post $post           The Post object being considered for trashing support.
		 */
		$supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_trashable", $supports_trash, $post );

		if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $post->ID ) ) {
			/* translators: %s: post type */
			return new WP_Error( "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) );
		}

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_item_for_response( $post, $request );

		// If we're forcing, then delete permanently.
		if ( $force ) {
			$result = wp_delete_post( $id, true );
		} else {
			// If we don't support trashing for this type, error out.
			if ( ! $supports_trash ) {
				/* translators: %s: post type */
				return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array( 'status' => 501 ) );
			}

			// Otherwise, only trash if we haven't already.
			if ( 'trash' === $post->post_status ) {
				/* translators: %s: post type */
				return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410 ) );
			}

			// (Note that internally this falls through to `wp_delete_post` if
			// the trash is disabled.)
			$result = wp_trash_post( $id );
		}

		if ( ! $result ) {
			/* translators: %s: post type */
			return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) );
		}

		/**
		 * Fires after a single item is deleted or trashed via the REST API.
		 *
		 * @param object           $post     The deleted or trashed item.
		 * @param WP_REST_Response $response The response data.
		 * @param WP_REST_Request  $request  The request sent to the API.
		 */
		do_action( "woocommerce_rest_delete_{$this->post_type}", $post, $response, $request );

		return $response;
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param WP_Post         $post Post object.
	 * @param WP_REST_Request $request Request object.
	 * @return array Links for the given post.
	 */
	protected function prepare_links( $post, $request ) {
		$links = array(
			'self' => array(
				'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ),
			),
			'collection' => array(
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
			),
		);

		return $links;
	}

	/**
	 * Determine the allowed query_vars for a get_items() response and
	 * prepare for WP_Query.
	 *
	 * @param array           $prepared_args Prepared arguments.
	 * @param WP_REST_Request $request Request object.
	 * @return array          $query_args
	 */
	protected function prepare_items_query( $prepared_args = array(), $request = null ) {

		$valid_vars = array_flip( $this->get_allowed_query_vars() );
		$query_args = array();
		foreach ( $valid_vars as $var => $index ) {
			if ( isset( $prepared_args[ $var ] ) ) {
				/**
				 * Filter the query_vars used in `get_items` for the constructed query.
				 *
				 * The dynamic portion of the hook name, $var, refers to the query_var key.
				 *
				 * @param mixed $prepared_args[ $var ] The query_var value.
				 */
				$query_args[ $var ] = apply_filters( "woocommerce_rest_query_var-{$var}", $prepared_args[ $var ] );
			}
		}

		$query_args['ignore_sticky_posts'] = true;

		if ( 'include' === $query_args['orderby'] ) {
			$query_args['orderby'] = 'post__in';
		} elseif ( 'id' === $query_args['orderby'] ) {
			$query_args['orderby'] = 'ID'; // ID must be capitalized.
		} elseif ( 'slug' === $query_args['orderby'] ) {
			$query_args['orderby'] = 'name';
		}

		return $query_args;
	}

	/**
	 * Get all the WP Query vars that are allowed for the API request.
	 *
	 * @return array
	 */
	protected function get_allowed_query_vars() {
		global $wp;

		/**
		 * Filter the publicly allowed query vars.
		 *
		 * Allows adjusting of the default query vars that are made public.
		 *
		 * @param array  Array of allowed WP_Query query vars.
		 */
		$valid_vars = apply_filters( 'query_vars', $wp->public_query_vars );

		$post_type_obj = get_post_type_object( $this->post_type );
		if ( current_user_can( $post_type_obj->cap->edit_posts ) ) {
			/**
			 * Filter the allowed 'private' query vars for authorized users.
			 *
			 * If the user has the `edit_posts` capability, we also allow use of
			 * private query parameters, which are only undesirable on the
			 * frontend, but are safe for use in query strings.
			 *
			 * To disable anyway, use
			 * `add_filter( 'woocommerce_rest_private_query_vars', '__return_empty_array' );`
			 *
			 * @param array $private_query_vars Array of allowed query vars for authorized users.
			 * }
			 */
			$private = apply_filters( 'woocommerce_rest_private_query_vars', $wp->private_query_vars );
			$valid_vars = array_merge( $valid_vars, $private );
		}
		// Define our own in addition to WP's normal vars.
		$rest_valid = array(
			'date_query',
			'ignore_sticky_posts',
			'offset',
			'post__in',
			'post__not_in',
			'post_parent',
			'post_parent__in',
			'post_parent__not_in',
			'posts_per_page',
			'meta_query',
			'tax_query',
			'meta_key',
			'meta_value',
			'meta_compare',
			'meta_value_num',
		);
		$valid_vars = array_merge( $valid_vars, $rest_valid );

		/**
		 * Filter allowed query vars for the REST API.
		 *
		 * This filter allows you to add or remove query vars from the final allowed
		 * list for all requests, including unauthenticated ones. To alter the
		 * vars for editors only.
		 *
		 * @param array {
		 *    Array of allowed WP_Query query vars.
		 *
		 *    @param string $allowed_query_var The query var to allow.
		 * }
		 */
		$valid_vars = apply_filters( 'woocommerce_rest_query_vars', $valid_vars );

		return $valid_vars;
	}

	/**
	 * Get the query params for collections of attachments.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params = parent::get_collection_params();

		$params['context']['default'] = 'view';

		$params['after'] = array(
			'description'        => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
			'type'               => 'string',
			'format'             => 'date-time',
			'validate_callback'  => 'rest_validate_request_arg',
		);
		$params['before'] = array(
			'description'        => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
			'type'               => 'string',
			'format'             => 'date-time',
			'validate_callback'  => 'rest_validate_request_arg',
		);
		$params['exclude'] = array(
			'description'       => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type'          => 'integer',
			),
			'default'           => array(),
			'sanitize_callback' => 'wp_parse_id_list',
		);
		$params['include'] = array(
			'description'       => __( 'Limit result set to specific ids.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type'          => 'integer',
			),
			'default'           => array(),
			'sanitize_callback' => 'wp_parse_id_list',
		);
		$params['offset'] = array(
			'description'        => __( 'Offset the result set by a specific number of items.', 'woocommerce' ),
			'type'               => 'integer',
			'sanitize_callback'  => 'absint',
			'validate_callback'  => 'rest_validate_request_arg',
		);
		$params['order'] = array(
			'description'        => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
			'type'               => 'string',
			'default'            => 'desc',
			'enum'               => array( 'asc', 'desc' ),
			'validate_callback'  => 'rest_validate_request_arg',
		);
		$params['orderby'] = array(
			'description'        => __( 'Sort collection by object attribute.', 'woocommerce' ),
			'type'               => 'string',
			'default'            => 'date',
			'enum'               => array(
				'date',
				'id',
				'include',
				'title',
				'slug',
				'modified',
			),
			'validate_callback'  => 'rest_validate_request_arg',
		);

		$post_type_obj = get_post_type_object( $this->post_type );

		if ( isset( $post_type_obj->hierarchical ) && $post_type_obj->hierarchical ) {
			$params['parent'] = array(
				'description'       => __( 'Limit result set to those of particular parent IDs.', 'woocommerce' ),
				'type'              => 'array',
				'items'             => array(
					'type'          => 'integer',
				),
				'sanitize_callback' => 'wp_parse_id_list',
				'default'           => array(),
			);
			$params['parent_exclude'] = array(
				'description'       => __( 'Limit result set to all items except those of a particular parent ID.', 'woocommerce' ),
				'type'              => 'array',
				'items'             => array(
					'type'          => 'integer',
				),
				'sanitize_callback' => 'wp_parse_id_list',
				'default'           => array(),
			);
		}

		if ( 'wc/v1' === $this->namespace ) {
			$params['filter'] = array(
				'type'        => 'object',
				'description' => __( 'Use WP Query arguments to modify the response; private query vars require appropriate authorization.', 'woocommerce' ),
			);
		}

		return $params;
	}

	/**
	 * Update post meta fields.
	 *
	 * @param WP_Post         $post Post object.
	 * @param WP_REST_Request $request Request object.
	 * @return bool|WP_Error
	 */
	protected function update_post_meta_fields( $post, $request ) {
		return true;
	}
}
rest-api/Controllers/Version3/class-wc-rest-tax-classes-controller.php000064400000000753151336044760022132 0ustar00<?php
/**
 * REST API Tax Classes controller
 *
 * Handles requests to the /taxes/classes endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Tax Classes controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Tax_Classes_V2_Controller
 */
class WC_REST_Tax_Classes_Controller extends WC_REST_Tax_Classes_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';
}
rest-api/Controllers/Version3/class-wc-rest-product-tags-controller.php000064400000000757151336044760022323 0ustar00<?php
/**
 * REST API Product Tags controller
 *
 * Handles requests to the products/tags endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Product Tags controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Product_Tags_V2_Controller
 */
class WC_REST_Product_Tags_Controller extends WC_REST_Product_Tags_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';
}
rest-api/Controllers/Version3/class-wc-rest-product-shipping-classes-controller.php000064400000001067151336044760024634 0ustar00<?php
/**
 * REST API Product Shipping Classes controller
 *
 * Handles requests to the products/shipping_classes endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Product Shipping Classes controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Product_Shipping_Classes_V2_Controller
 */
class WC_REST_Product_Shipping_Classes_Controller extends WC_REST_Product_Shipping_Classes_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';
}
rest-api/Controllers/Version3/class-wc-rest-order-notes-controller.php000064400000013614151336044760022144 0ustar00<?php
/**
 * REST API Order Notes controller
 *
 * Handles requests to the /orders/<order_id>/notes endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Order Notes controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Order_Notes_V2_Controller
 */
class WC_REST_Order_Notes_Controller extends WC_REST_Order_Notes_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';

	/**
	 * Prepare a single order note output for response.
	 *
	 * @param WP_Comment      $note    Order note object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $note, $request ) {
		$data = array(
			'id'               => (int) $note->comment_ID,
			'author'           => __( 'woocommerce', 'woocommerce' ) === $note->comment_author ? 'system' : $note->comment_author,
			'date_created'     => wc_rest_prepare_date_response( $note->comment_date ),
			'date_created_gmt' => wc_rest_prepare_date_response( $note->comment_date_gmt ),
			'note'             => $note->comment_content,
			'customer_note'    => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ),
		);

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $note ) );

		/**
		 * Filter order note object returned from the REST API.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param WP_Comment       $note     Order note object used to create response.
		 * @param WP_REST_Request  $request  Request object.
		 */
		return apply_filters( 'woocommerce_rest_prepare_order_note', $response, $note, $request );
	}

	/**
	 * Create a single order note.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function create_item( $request ) {
		if ( ! empty( $request['id'] ) ) {
			/* translators: %s: post type */
			return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) );
		}

		$order = wc_get_order( (int) $request['order_id'] );

		if ( ! $order || $this->post_type !== $order->get_type() ) {
			return new WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		// Create the note.
		$note_id = $order->add_order_note( $request['note'], $request['customer_note'], $request['added_by_user'] );

		if ( ! $note_id ) {
			return new WP_Error( 'woocommerce_api_cannot_create_order_note', __( 'Cannot create order note, please try again.', 'woocommerce' ), array( 'status' => 500 ) );
		}

		$note = get_comment( $note_id );
		$this->update_additional_fields_for_object( $note, $request );

		/**
		 * Fires after a order note is created or updated via the REST API.
		 *
		 * @param WP_Comment      $note      New order note object.
		 * @param WP_REST_Request $request   Request object.
		 * @param boolean         $creating  True when creating item, false when updating.
		 */
		do_action( 'woocommerce_rest_insert_order_note', $note, $request, true );

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_item_for_response( $note, $request );
		$response = rest_ensure_response( $response );
		$response->set_status( 201 );
		$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, str_replace( '(?P<order_id>[\d]+)', $order->get_id(), $this->rest_base ), $note_id ) ) );

		return $response;
	}

	/**
	 * Get the Order Notes schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'order_note',
			'type'       => 'object',
			'properties' => array(
				'id'               => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'author'           => array(
					'description' => __( 'Order note author.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_created'     => array(
					'description' => __( "The date the order note was created, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_created_gmt' => array(
					'description' => __( 'The date the order note was created, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'note'             => array(
					'description' => __( 'Order note content.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'customer_note'    => array(
					'description' => __( 'If true, the note will be shown to customers and they will be notified. If false, the note will be for admin reference only.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'added_by_user'    => array(
					'description' => __( 'If true, this note will be attributed to the current user. If false, the note will be attributed to the system.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'edit' ),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller.php000064400000000760151336044760022656 0ustar00<?php
/**
 * REST API Shipping Zones controller
 *
 * Handles requests to the /shipping/zones endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Shipping Zones class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Shipping_Zones_V2_Controller
 */
class WC_REST_Shipping_Zones_Controller extends WC_REST_Shipping_Zones_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';
}
rest-api/Controllers/Version3/class-wc-rest-report-products-totals-controller.php000064400000006356151336044760024370 0ustar00<?php
/**
 * REST API Reports Products Totals controller
 *
 * Handles requests to the /reports/products/count endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   3.5.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Reports Products Totals controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Reports_Controller
 */
class WC_REST_Report_Products_Totals_Controller extends WC_REST_Reports_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'reports/products/totals';

	/**
	 * Get reports list.
	 *
	 * @since 3.5.0
	 * @return array
	 */
	protected function get_reports() {
		$types = wc_get_product_types();
		$terms = get_terms(
			array(
				'taxonomy'   => 'product_type',
				'hide_empty' => false,
			)
		);
		$data  = array();

		foreach ( $terms as $product_type ) {
			if ( ! isset( $types[ $product_type->name ] ) ) {
				continue;
			}

			$data[] = array(
				'slug'  => $product_type->name,
				'name'  => $types[ $product_type->name ],
				'total' => (int) $product_type->count,
			);
		}

		return $data;
	}

	/**
	 * Prepare a report object for serialization.
	 *
	 * @param  stdClass        $report Report data.
	 * @param  WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $report, $request ) {
		$data = array(
			'slug'  => $report->slug,
			'name'  => $report->name,
			'total' => $report->total,
		);

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		/**
		 * Filter a report returned from the API.
		 *
		 * Allows modification of the report data right before it is returned.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param object           $report   The original report object.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_report_products_count', $response, $report, $request );
	}

	/**
	 * Get the Report's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'report_product_total',
			'type'       => 'object',
			'properties' => array(
				'slug'  => array(
					'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'name'  => array(
					'description' => __( 'Product type name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'total' => array(
					'description' => __( 'Amount of products.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version3/class-wc-rest-order-refunds-controller.php000064400000007335151336044760022465 0ustar00<?php
/**
 * REST API Order Refunds controller
 *
 * Handles requests to the /orders/<order_id>/refunds endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Internal\RestApiUtil;

/**
 * REST API Order Refunds controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Order_Refunds_V2_Controller
 */
class WC_REST_Order_Refunds_Controller extends WC_REST_Order_Refunds_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';

	/**
	 * Prepares one object for create or update operation.
	 *
	 * @since  3.0.0
	 * @param  WP_REST_Request $request Request object.
	 * @param  bool            $creating If is creating a new object.
	 * @return WP_Error|WC_Data The prepared item, or WP_Error object on failure.
	 */
	protected function prepare_object_for_database( $request, $creating = false ) {
		RestApiUtil::adjust_create_refund_request_parameters( $request );

		$order = wc_get_order( (int) $request['order_id'] );

		if ( ! $order ) {
			return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), 404 );
		}

		if ( 0 > $request['amount'] ) {
			return new WP_Error( 'woocommerce_rest_invalid_order_refund', __( 'Refund amount must be greater than zero.', 'woocommerce' ), 400 );
		}

		// Create the refund.
		$refund = wc_create_refund(
			array(
				'order_id'       => $order->get_id(),
				'amount'         => $request['amount'],
				'reason'         => $request['reason'],
				'line_items'     => $request['line_items'],
				'refund_payment' => $request['api_refund'],
				'restock_items'  => $request['api_restock'],
			)
		);

		if ( is_wp_error( $refund ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', $refund->get_error_message(), 500 );
		}

		if ( ! $refund ) {
			return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 );
		}

		if ( ! empty( $request['meta_data'] ) && is_array( $request['meta_data'] ) ) {
			foreach ( $request['meta_data'] as $meta ) {
				$refund->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
			}
			$refund->save_meta_data();
		}

		/**
		 * Filters an object before it is inserted via the REST API.
		 *
		 * The dynamic portion of the hook name, `$this->post_type`,
		 * refers to the object type slug.
		 *
		 * @param WC_Data         $coupon   Object object.
		 * @param WP_REST_Request $request  Request object.
		 * @param bool            $creating If is creating a new object.
		 */
		return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $refund, $request, $creating );
	}

	/**
	 * Get the refund schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = parent::get_item_schema();

		$schema['properties']['line_items']['items']['properties']['refund_total'] = array(
			'description' => __( 'Amount that will be refunded for this line item (excluding taxes).', 'woocommerce' ),
			'type'        => 'number',
			'context'     => array( 'edit' ),
			'readonly'    => true,
		);

		$schema['properties']['line_items']['items']['properties']['taxes']['items']['properties']['refund_total'] = array(
			'description' => __( 'Amount that will be refunded for this tax.', 'woocommerce' ),
			'type'        => 'number',
			'context'     => array( 'edit' ),
			'readonly'    => true,
		);

		$schema['properties']['api_restock'] = array(
			'description' => __( 'When true, refunded items are restocked.', 'woocommerce' ),
			'type'        => 'boolean',
			'context'     => array( 'edit' ),
			'default'     => true,
		);

		return $schema;
	}
}
rest-api/Controllers/Version3/class-wc-rest-system-status-controller.php000064400000000757151336044760022554 0ustar00<?php
/**
 * REST API WC System Status controller
 *
 * Handles requests to the /system_status endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * System status controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_System_Status_V2_Controller
 */
class WC_REST_System_Status_Controller extends WC_REST_System_Status_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';
}
rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller-base.php000064400000007254151336044760023573 0ustar00<?php
/**
 * REST API Shipping Zones Controller base
 *
 * Houses common functionality between Shipping Zones and Locations.
 *
 * @package WooCommerce\RestApi
 * @since    3.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * REST API Shipping Zones base class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Controller
 */
abstract class WC_REST_Shipping_Zones_Controller_Base extends WC_REST_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'shipping/zones';

	/**
	 * Retrieve a Shipping Zone by it's ID.
	 *
	 * @param int $zone_id Shipping Zone ID.
	 * @return WC_Shipping_Zone|WP_Error
	 */
	protected function get_zone( $zone_id ) {
		$zone = WC_Shipping_Zones::get_zone_by( 'zone_id', $zone_id );

		if ( false === $zone ) {
			return new WP_Error( 'woocommerce_rest_shipping_zone_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		return $zone;
	}

	/**
	 * Check whether a given request has permission to read Shipping Zones.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_items_permissions_check( $request ) {
		if ( ! wc_shipping_enabled() ) {
			return new WP_Error( 'rest_no_route', __( 'Shipping is disabled.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access to create Shipping Zones.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function create_item_permissions_check( $request ) {
		if ( ! wc_shipping_enabled() ) {
			return new WP_Error( 'rest_no_route', __( 'Shipping is disabled.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check whether a given request has permission to edit Shipping Zones.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function update_items_permissions_check( $request ) {
		if ( ! wc_shipping_enabled() ) {
			return new WP_Error( 'rest_no_route', __( 'Shipping is disabled.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check whether a given request has permission to delete Shipping Zones.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function delete_items_permissions_check( $request ) {
		if ( ! wc_shipping_enabled() ) {
			return new WP_Error( 'rest_no_route', __( 'Shipping is disabled.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		if ( ! wc_rest_check_manager_permissions( 'settings', 'delete' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

}
rest-api/Controllers/Version3/class-wc-rest-payment-gateways-controller.php000064400000017437151336044760023211 0ustar00<?php
/**
 * REST API WC Payment gateways controller
 *
 * Handles requests to the /payment_gateways endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Paymenga gateways controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Payment_Gateways_V2_Controller
 */
class WC_REST_Payment_Gateways_Controller extends WC_REST_Payment_Gateways_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';

	/**
	 * Prepare a payment gateway for response.
	 *
	 * @param  WC_Payment_Gateway $gateway    Payment gateway object.
	 * @param  WP_REST_Request    $request    Request object.
	 * @return WP_REST_Response   $response   Response data.
	 */
	public function prepare_item_for_response( $gateway, $request ) {
		$order = (array) get_option( 'woocommerce_gateway_order' );
		$item  = array(
			'id'                 => $gateway->id,
			'title'              => $gateway->title,
			'description'        => $gateway->description,
			'order'              => isset( $order[ $gateway->id ] ) ? $order[ $gateway->id ] : '',
			'enabled'            => ( 'yes' === $gateway->enabled ),
			'method_title'       => $gateway->get_method_title(),
			'method_description' => $gateway->get_method_description(),
			'method_supports'    => $gateway->supports,
			'settings'           => $this->get_settings( $gateway ),
		);

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $item, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		$response = rest_ensure_response( $data );
		$response->add_links( $this->prepare_links( $gateway, $request ) );

		/**
		 * Filter payment gateway objects returned from the REST API.
		 *
		 * @param WP_REST_Response   $response The response object.
		 * @param WC_Payment_Gateway $gateway  Payment gateway object.
		 * @param WP_REST_Request    $request  Request object.
		 */
		return apply_filters( 'woocommerce_rest_prepare_payment_gateway', $response, $gateway, $request );
	}

	/**
	 * Return settings associated with this payment gateway.
	 *
	 * @param WC_Payment_Gateway $gateway Gateway instance.
	 *
	 * @return array
	 */
	public function get_settings( $gateway ) {
		$settings = array();
		$gateway->init_form_fields();
		foreach ( $gateway->form_fields as $id => $field ) {
			// Make sure we at least have a title and type.
			if ( empty( $field['title'] ) || empty( $field['type'] ) ) {
				continue;
			}

			// Ignore 'enabled' and 'description' which get included elsewhere.
			if ( in_array( $id, array( 'enabled', 'description' ), true ) ) {
				continue;
			}

			$data = array(
				'id'          => $id,
				'label'       => empty( $field['label'] ) ? $field['title'] : $field['label'],
				'description' => empty( $field['description'] ) ? '' : $field['description'],
				'type'        => $field['type'],
				'value'       => empty( $gateway->settings[ $id ] ) ? '' : $gateway->settings[ $id ],
				'default'     => empty( $field['default'] ) ? '' : $field['default'],
				'tip'         => empty( $field['description'] ) ? '' : $field['description'],
				'placeholder' => empty( $field['placeholder'] ) ? '' : $field['placeholder'],
			);
			if ( ! empty( $field['options'] ) ) {
				$data['options'] = $field['options'];
			}
			$settings[ $id ] = $data;
		}
		return $settings;
	}

	/**
	 * Get the payment gateway schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'payment_gateway',
			'type'       => 'object',
			'properties' => array(
				'id'                 => array(
					'description' => __( 'Payment gateway ID.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'title'              => array(
					'description' => __( 'Payment gateway title on checkout.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'description'        => array(
					'description' => __( 'Payment gateway description on checkout.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'order'              => array(
					'description' => __( 'Payment gateway sort order.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'absint',
					),
				),
				'enabled'            => array(
					'description' => __( 'Payment gateway enabled status.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
				),
				'method_title'       => array(
					'description' => __( 'Payment gateway method title.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'method_description' => array(
					'description' => __( 'Payment gateway method description.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'method_supports'    => array(
					'description' => __( 'Supported features for this payment gateway.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
					'items'       => array(
						'type' => 'string',
					),
				),
				'settings'           => array(
					'description' => __( 'Payment gateway settings.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'properties'  => array(
						'id'          => array(
							'description' => __( 'A unique identifier for the setting.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'label'       => array(
							'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'description' => array(
							'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'type'        => array(
							'description' => __( 'Type of setting.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
							'enum'        => array( 'text', 'email', 'number', 'color', 'password', 'textarea', 'select', 'multiselect', 'radio', 'image_width', 'checkbox' ),
							'readonly'    => true,
						),
						'value'       => array(
							'description' => __( 'Setting value.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'default'     => array(
							'description' => __( 'Default value for the setting.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'tip'         => array(
							'description' => __( 'Additional help text shown to the user about the setting.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'placeholder' => array(
							'description' => __( 'Placeholder text to be displayed in text inputs.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
					),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version3/class-wc-rest-shipping-zone-locations-controller.php000064400000001054151336044760024461 0ustar00<?php
/**
 * REST API Shipping Zone Locations controller
 *
 * Handles requests to the /shipping/zones/<id>/locations endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Shipping Zone Locations class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Shipping_Zone_Locations_V2_Controller
 */
class WC_REST_Shipping_Zone_Locations_Controller extends WC_REST_Shipping_Zone_Locations_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';
}
rest-api/Controllers/Version3/class-wc-rest-report-sales-controller.php000064400000000752151336044760022322 0ustar00<?php
/**
 * REST API Reports controller
 *
 * Handles requests to the reports/sales endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Report Sales controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Report_Sales_V2_Controller
 */
class WC_REST_Report_Sales_Controller extends WC_REST_Report_Sales_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';
}
rest-api/Controllers/Version3/class-wc-rest-setting-options-controller.php000064400000016522151336044760023052 0ustar00<?php
/**
 * REST API Setting Options controller
 *
 * Handles requests to the /settings/$group/$setting endpoints.
 *
 * @package WooCommerce\RestApi
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Setting Options controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Setting_Options_V2_Controller
 */
class WC_REST_Setting_Options_Controller extends WC_REST_Setting_Options_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';

	/**
	 * Get setting data.
	 *
	 * @param string $group_id Group ID.
	 * @param string $setting_id Setting ID.
	 * @return stdClass|WP_Error
	 */
	public function get_setting( $group_id, $setting_id ) {
		$setting = parent::get_setting( $group_id, $setting_id );
		if ( is_wp_error( $setting ) ) {
			return $setting;
		}
		$setting['group_id'] = $group_id;
		return $setting;
	}

	/**
	 * Callback for allowed keys for each setting response.
	 *
	 * @param  string $key Key to check.
	 * @return boolean
	 */
	public function allowed_setting_keys( $key ) {
		return in_array(
			$key, array(
				'id',
				'group_id',
				'label',
				'description',
				'default',
				'tip',
				'placeholder',
				'type',
				'options',
				'value',
				'option_key',
			), true
		);
	}

	/**
	 * Get all settings in a group.
	 *
	 * @param string $group_id Group ID.
	 * @return array|WP_Error
	 */
	public function get_group_settings( $group_id ) {
		if ( empty( $group_id ) ) {
			return new WP_Error( 'rest_setting_setting_group_invalid', __( 'Invalid setting group.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$settings = apply_filters( 'woocommerce_settings-' . $group_id, array() ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores

		if ( empty( $settings ) ) {
			return new WP_Error( 'rest_setting_setting_group_invalid', __( 'Invalid setting group.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$filtered_settings = array();
		foreach ( $settings as $setting ) {
			$option_key = $setting['option_key'];
			$setting    = $this->filter_setting( $setting );
			$default    = isset( $setting['default'] ) ? $setting['default'] : '';
			// Get the option value.
			if ( is_array( $option_key ) ) {
				$option           = get_option( $option_key[0] );
				$setting['value'] = isset( $option[ $option_key[1] ] ) ? $option[ $option_key[1] ] : $default;
			} else {
				$admin_setting_value = WC_Admin_Settings::get_option( $option_key, $default );
				$setting['value']    = $admin_setting_value;
			}

			if ( 'multi_select_countries' === $setting['type'] ) {
				$setting['options'] = WC()->countries->get_countries();
				$setting['type']    = 'multiselect';
			} elseif ( 'single_select_country' === $setting['type'] ) {
				$setting['type']    = 'select';
				$setting['options'] = $this->get_countries_and_states();
			} elseif ( 'single_select_page' === $setting['type'] ) {
				$pages   = get_pages(
					array(
						'sort_column'  => 'menu_order',
						'sort_order'   => 'ASC',
						'hierarchical' => 0,
					)
				);
				$options = array();
				foreach ( $pages as $page ) {
					$options[ $page->ID ] = ! empty( $page->post_title ) ? $page->post_title : '#' . $page->ID;
				}
				$setting['type']    = 'select';
				$setting['options'] = $options;
			}

			$filtered_settings[] = $setting;
		}

		return $filtered_settings;
	}

	/**
	 * Returns a list of countries and states for use in the base location setting.
	 *
	 * @since  3.0.7
	 * @return array Array of states and countries.
	 */
	private function get_countries_and_states() {
		$countries = WC()->countries->get_countries();
		if ( ! $countries ) {
			return array();
		}
		$output = array();
		foreach ( $countries as $key => $value ) {
			$states = WC()->countries->get_states( $key );

			if ( $states ) {
				foreach ( $states as $state_key => $state_value ) {
					$output[ $key . ':' . $state_key ] = $value . ' - ' . $state_value;
				}
			} else {
				$output[ $key ] = $value;
			}
		}
		return $output;
	}

	/**
	 * Get the settings schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'setting',
			'type'       => 'object',
			'properties' => array(
				'id'          => array(
					'description' => __( 'A unique identifier for the setting.', 'woocommerce' ),
					'type'        => 'string',
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_title',
					),
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'group_id'    => array(
					'description' => __( 'An identifier for the group this setting belongs to.', 'woocommerce' ),
					'type'        => 'string',
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_title',
					),
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'label'       => array(
					'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ),
					'type'        => 'string',
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'description' => array(
					'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ),
					'type'        => 'string',
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'value'       => array(
					'description' => __( 'Setting value.', 'woocommerce' ),
					'type'        => 'mixed',
					'context'     => array( 'view', 'edit' ),
				),
				'default'     => array(
					'description' => __( 'Default value for the setting.', 'woocommerce' ),
					'type'        => 'mixed',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'tip'         => array(
					'description' => __( 'Additional help text shown to the user about the setting.', 'woocommerce' ),
					'type'        => 'string',
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'placeholder' => array(
					'description' => __( 'Placeholder text to be displayed in text inputs.', 'woocommerce' ),
					'type'        => 'string',
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'type'        => array(
					'description' => __( 'Type of setting.', 'woocommerce' ),
					'type'        => 'string',
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
					'context'     => array( 'view', 'edit' ),
					'enum'        => array( 'text', 'email', 'number', 'color', 'password', 'textarea', 'select', 'multiselect', 'radio', 'image_width', 'checkbox' ),
					'readonly'    => true,
				),
				'options'     => array(
					'description' => __( 'Array of options (key value pairs) for inputs such as select, multiselect, and radio buttons.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version3/class-wc-rest-terms-controller.php000064400000062731151336044760021041 0ustar00<?php
/**
 * Abstract Rest Terms Controller
 *
 * @package WooCommerce\RestApi
 * @version  3.3.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use Automattic\WooCommerce\Internal\AssignDefaultCategory;

/**
 * Terms controller class.
 */
abstract class WC_REST_Terms_Controller extends WC_REST_Controller {

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = '';

	/**
	 * Taxonomy.
	 *
	 * @var string
	 */
	protected $taxonomy = '';

	/**
	 * Cached taxonomies by attribute id.
	 *
	 * @var array
	 */
	protected $taxonomies_by_id = array();

	/**
	 * Register the routes for terms.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_item' ),
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
					'args'                => array_merge(
						$this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
						array(
							'name' => array(
								'type'        => 'string',
								'description' => __( 'Name for the resource.', 'woocommerce' ),
								'required'    => true,
							),
						)
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\d]+)',
			array(
				'args'   => array(
					'id' => array(
						'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => array(
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_item' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_item' ),
					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
					'args'                => array(
						'force' => array(
							'default'     => false,
							'type'        => 'boolean',
							'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
						),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/batch',
			array(
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'batch_items' ),
					'permission_callback' => array( $this, 'batch_items_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				'schema' => array( $this, 'get_public_batch_schema' ),
			)
		);
	}

	/**
	 * Check if a given request has access to read the terms.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_items_permissions_check( $request ) {
		$permissions = $this->check_permissions( $request, 'read' );
		if ( is_wp_error( $permissions ) ) {
			return $permissions;
		}

		if ( ! $permissions ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access to create a term.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function create_item_permissions_check( $request ) {
		$permissions = $this->check_permissions( $request, 'create' );
		if ( is_wp_error( $permissions ) ) {
			return $permissions;
		}

		if ( ! $permissions ) {
			return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access to read a term.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_item_permissions_check( $request ) {
		$permissions = $this->check_permissions( $request, 'read' );
		if ( is_wp_error( $permissions ) ) {
			return $permissions;
		}

		if ( ! $permissions ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access to update a term.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function update_item_permissions_check( $request ) {
		$permissions = $this->check_permissions( $request, 'edit' );
		if ( is_wp_error( $permissions ) ) {
			return $permissions;
		}

		if ( ! $permissions ) {
			return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access to delete a term.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function delete_item_permissions_check( $request ) {
		$permissions = $this->check_permissions( $request, 'delete' );
		if ( is_wp_error( $permissions ) ) {
			return $permissions;
		}

		if ( ! $permissions ) {
			return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access batch create, update and delete items.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return boolean|WP_Error
	 */
	public function batch_items_permissions_check( $request ) {
		$permissions = $this->check_permissions( $request, 'batch' );
		if ( is_wp_error( $permissions ) ) {
			return $permissions;
		}

		if ( ! $permissions ) {
			return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check permissions.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @param string          $context Request context.
	 * @return bool|WP_Error
	 */
	protected function check_permissions( $request, $context = 'read' ) {
		// Get taxonomy.
		$taxonomy = $this->get_taxonomy( $request );
		if ( ! $taxonomy || ! taxonomy_exists( $taxonomy ) ) {
			return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( 'Taxonomy does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		// Check permissions for a single term.
		$id = intval( $request['id'] );
		if ( $id ) {
			$term = get_term( $id, $taxonomy );

			if ( is_wp_error( $term ) || ! $term || $term->taxonomy !== $taxonomy ) {
				return new WP_Error( 'woocommerce_rest_term_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
			}

			return wc_rest_check_product_term_permissions( $taxonomy, $context, $term->term_id );
		}

		return wc_rest_check_product_term_permissions( $taxonomy, $context );
	}

	/**
	 * Get terms associated with a taxonomy.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error
	 */
	public function get_items( $request ) {
		$taxonomy      = $this->get_taxonomy( $request );
		$prepared_args = array(
			'exclude'    => $request['exclude'],
			'include'    => $request['include'],
			'order'      => $request['order'],
			'orderby'    => $request['orderby'],
			'product'    => $request['product'],
			'hide_empty' => $request['hide_empty'],
			'number'     => $request['per_page'],
			'search'     => $request['search'],
			'slug'       => $request['slug'],
		);

		if ( ! empty( $request['offset'] ) ) {
			$prepared_args['offset'] = $request['offset'];
		} else {
			$prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number'];
		}

		$taxonomy_obj = get_taxonomy( $taxonomy );

		if ( $taxonomy_obj->hierarchical && isset( $request['parent'] ) ) {
			if ( 0 === $request['parent'] ) {
				// Only query top-level terms.
				$prepared_args['parent'] = 0;
			} else {
				if ( $request['parent'] ) {
					$prepared_args['parent'] = $request['parent'];
				}
			}
		}

		/**
		 * Filter the query arguments, before passing them to `get_terms()`.
		 *
		 * Enables adding extra arguments or setting defaults for a terms
		 * collection request.
		 *
		 * @see https://developer.wordpress.org/reference/functions/get_terms/
		 *
		 * @param array           $prepared_args Array of arguments to be
		 *                                       passed to get_terms.
		 * @param WP_REST_Request $request       The current request.
		 */
		$prepared_args = apply_filters( "woocommerce_rest_{$taxonomy}_query", $prepared_args, $request );

		if ( ! empty( $prepared_args['product'] ) ) {
			$query_result = $this->get_terms_for_product( $prepared_args, $request );
			$total_terms  = $this->total_terms;
		} else {
			$query_result = get_terms( $taxonomy, $prepared_args );

			$count_args = $prepared_args;
			unset( $count_args['number'] );
			unset( $count_args['offset'] );
			$total_terms = wp_count_terms( $taxonomy, $count_args );

			// Ensure we don't return results when offset is out of bounds.
			// See https://core.trac.wordpress.org/ticket/35935.
			if ( $prepared_args['offset'] && $prepared_args['offset'] >= $total_terms ) {
				$query_result = array();
			}

			// wp_count_terms can return a falsy value when the term has no children.
			if ( ! $total_terms ) {
				$total_terms = 0;
			}
		}
		$response = array();
		foreach ( $query_result as $term ) {
			$data       = $this->prepare_item_for_response( $term, $request );
			$response[] = $this->prepare_response_for_collection( $data );
		}

		$response = rest_ensure_response( $response );

		// Store pagination values for headers then unset for count query.
		$per_page = (int) $prepared_args['number'];
		$page     = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 );

		$response->header( 'X-WP-Total', (int) $total_terms );
		$max_pages = ceil( $total_terms / $per_page );
		$response->header( 'X-WP-TotalPages', (int) $max_pages );

		$base = str_replace( '(?P<attribute_id>[\d]+)', $request['attribute_id'], $this->rest_base );
		$base = add_query_arg( $request->get_query_params(), rest_url( '/' . $this->namespace . '/' . $base ) );
		if ( $page > 1 ) {
			$prev_page = $page - 1;
			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}
			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}
		if ( $max_pages > $page ) {
			$next_page = $page + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );
			$response->link_header( 'next', $next_link );
		}

		return $response;
	}

	/**
	 * Create a single term for a taxonomy.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Request|WP_Error
	 */
	public function create_item( $request ) {
		$taxonomy = $this->get_taxonomy( $request );
		$name     = $request['name'];
		$args     = array();
		$schema   = $this->get_item_schema();

		if ( ! empty( $schema['properties']['description'] ) && isset( $request['description'] ) ) {
			$args['description'] = $request['description'];
		}
		if ( isset( $request['slug'] ) ) {
			$args['slug'] = $request['slug'];
		}
		if ( isset( $request['parent'] ) ) {
			if ( ! is_taxonomy_hierarchical( $taxonomy ) ) {
				return new WP_Error( 'woocommerce_rest_taxonomy_not_hierarchical', __( 'Can not set resource parent, taxonomy is not hierarchical.', 'woocommerce' ), array( 'status' => 400 ) );
			}
			$args['parent'] = $request['parent'];
		}

		$term = wp_insert_term( $name, $taxonomy, $args );
		if ( is_wp_error( $term ) ) {
			$error_data = array( 'status' => 400 );

			// If we're going to inform the client that the term exists,
			// give them the identifier they can actually use.
			$term_id = $term->get_error_data( 'term_exists' );
			if ( $term_id ) {
				$error_data['resource_id'] = $term_id;
			}

			return new WP_Error( $term->get_error_code(), $term->get_error_message(), $error_data );
		}

		$term = get_term( $term['term_id'], $taxonomy );

		$this->update_additional_fields_for_object( $term, $request );

		// Add term data.
		$meta_fields = $this->update_term_meta_fields( $term, $request );
		if ( is_wp_error( $meta_fields ) ) {
			wp_delete_term( $term->term_id, $taxonomy );

			return $meta_fields;
		}

		/**
		 * Fires after a single term is created or updated via the REST API.
		 *
		 * @param WP_Term         $term      Inserted Term object.
		 * @param WP_REST_Request $request   Request object.
		 * @param boolean         $creating  True when creating term, false when updating.
		 */
		do_action( "woocommerce_rest_insert_{$taxonomy}", $term, $request, true );

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_item_for_response( $term, $request );
		$response = rest_ensure_response( $response );
		$response->set_status( 201 );

		$base = '/' . $this->namespace . '/' . $this->rest_base;
		if ( ! empty( $request['attribute_id'] ) ) {
			$base = str_replace( '(?P<attribute_id>[\d]+)', (int) $request['attribute_id'], $base );
		}

		$response->header( 'Location', rest_url( $base . '/' . $term->term_id ) );

		return $response;
	}

	/**
	 * Get a single term from a taxonomy.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Request|WP_Error
	 */
	public function get_item( $request ) {
		$taxonomy = $this->get_taxonomy( $request );
		$term     = get_term( (int) $request['id'], $taxonomy );

		if ( is_wp_error( $term ) ) {
			return $term;
		}

		$response = $this->prepare_item_for_response( $term, $request );

		return rest_ensure_response( $response );
	}

	/**
	 * Update a single term from a taxonomy.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Request|WP_Error
	 */
	public function update_item( $request ) {
		$taxonomy      = $this->get_taxonomy( $request );
		$term          = get_term( (int) $request['id'], $taxonomy );
		$schema        = $this->get_item_schema();
		$prepared_args = array();

		if ( isset( $request['name'] ) ) {
			$prepared_args['name'] = $request['name'];
		}
		if ( ! empty( $schema['properties']['description'] ) && isset( $request['description'] ) ) {
			$prepared_args['description'] = $request['description'];
		}
		if ( isset( $request['slug'] ) ) {
			$prepared_args['slug'] = $request['slug'];
		}
		if ( isset( $request['parent'] ) ) {
			if ( ! is_taxonomy_hierarchical( $taxonomy ) ) {
				return new WP_Error( 'woocommerce_rest_taxonomy_not_hierarchical', __( 'Can not set resource parent, taxonomy is not hierarchical.', 'woocommerce' ), array( 'status' => 400 ) );
			}
			$prepared_args['parent'] = $request['parent'];
		}

		// Only update the term if we haz something to update.
		if ( ! empty( $prepared_args ) ) {
			$update = wp_update_term( $term->term_id, $term->taxonomy, $prepared_args );
			if ( is_wp_error( $update ) ) {
				return $update;
			}
		}

		$term = get_term( (int) $request['id'], $taxonomy );

		$this->update_additional_fields_for_object( $term, $request );

		// Update term data.
		$meta_fields = $this->update_term_meta_fields( $term, $request );
		if ( is_wp_error( $meta_fields ) ) {
			return $meta_fields;
		}

		/**
		 * Fires after a single term is created or updated via the REST API.
		 *
		 * @param WP_Term         $term      Inserted Term object.
		 * @param WP_REST_Request $request   Request object.
		 * @param boolean         $creating  True when creating term, false when updating.
		 */
		do_action( "woocommerce_rest_insert_{$taxonomy}", $term, $request, false );

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_item_for_response( $term, $request );
		return rest_ensure_response( $response );
	}

	/**
	 * Delete a single term from a taxonomy.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error
	 */
	public function delete_item( $request ) {
		$taxonomy = $this->get_taxonomy( $request );
		$force    = isset( $request['force'] ) ? (bool) $request['force'] : false;

		// We don't support trashing for this type, error out.
		if ( ! $force ) {
			return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Resource does not support trashing.', 'woocommerce' ), array( 'status' => 501 ) );
		}

		$term = get_term( (int) $request['id'], $taxonomy );
		// Get default category id.
		$default_category_id = absint( get_option( 'default_product_cat', 0 ) );

		// Prevent deleting the default product category.
		if ( $default_category_id === (int) $request['id'] ) {
			return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Default product category cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) );
		}

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_item_for_response( $term, $request );

		$retval = wp_delete_term( $term->term_id, $term->taxonomy );
		if ( ! $retval ) {
			return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) );
		}

		// Schedule action to assign default category.
		wc_get_container()->get( AssignDefaultCategory::class )->schedule_action();

		/**
		 * Fires after a single term is deleted via the REST API.
		 *
		 * @param WP_Term          $term     The deleted term.
		 * @param WP_REST_Response $response The response data.
		 * @param WP_REST_Request  $request  The request sent to the API.
		 */
		do_action( "woocommerce_rest_delete_{$taxonomy}", $term, $response, $request );

		return $response;
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param object          $term   Term object.
	 * @param WP_REST_Request $request Full details about the request.
	 * @return array Links for the given term.
	 */
	protected function prepare_links( $term, $request ) {
		$base = '/' . $this->namespace . '/' . $this->rest_base;

		if ( ! empty( $request['attribute_id'] ) ) {
			$base = str_replace( '(?P<attribute_id>[\d]+)', (int) $request['attribute_id'], $base );
		}

		$links = array(
			'self'       => array(
				'href' => rest_url( trailingslashit( $base ) . $term->term_id ),
			),
			'collection' => array(
				'href' => rest_url( $base ),
			),
		);

		if ( $term->parent ) {
			$parent_term = get_term( (int) $term->parent, $term->taxonomy );
			if ( $parent_term ) {
				$links['up'] = array(
					'href' => rest_url( trailingslashit( $base ) . $parent_term->term_id ),
				);
			}
		}

		return $links;
	}

	/**
	 * Update term meta fields.
	 *
	 * @param WP_Term         $term    Term object.
	 * @param WP_REST_Request $request Full details about the request.
	 * @return bool|WP_Error
	 */
	protected function update_term_meta_fields( $term, $request ) {
		return true;
	}

	/**
	 * Get the terms attached to a product.
	 *
	 * This is an alternative to `get_terms()` that uses `get_the_terms()`
	 * instead, which hits the object cache. There are a few things not
	 * supported, notably `include`, `exclude`. In `self::get_items()` these
	 * are instead treated as a full query.
	 *
	 * @param array           $prepared_args Arguments for `get_terms()`.
	 * @param WP_REST_Request $request       Full details about the request.
	 * @return array List of term objects. (Total count in `$this->total_terms`).
	 */
	protected function get_terms_for_product( $prepared_args, $request ) {
		$taxonomy = $this->get_taxonomy( $request );

		$query_result = get_the_terms( $prepared_args['product'], $taxonomy );
		if ( empty( $query_result ) ) {
			$this->total_terms = 0;
			return array();
		}

		// get_items() verifies that we don't have `include` set, and default.
		// ordering is by `name`.
		if ( ! in_array( $prepared_args['orderby'], array( 'name', 'none', 'include' ), true ) ) {
			switch ( $prepared_args['orderby'] ) {
				case 'id':
					$this->sort_column = 'term_id';
					break;
				case 'slug':
				case 'term_group':
				case 'description':
				case 'count':
					$this->sort_column = $prepared_args['orderby'];
					break;
			}
			usort( $query_result, array( $this, 'compare_terms' ) );
		}
		if ( strtolower( $prepared_args['order'] ) !== 'asc' ) {
			$query_result = array_reverse( $query_result );
		}

		// Pagination.
		$this->total_terms = count( $query_result );
		$query_result      = array_slice( $query_result, $prepared_args['offset'], $prepared_args['number'] );

		return $query_result;
	}

	/**
	 * Comparison function for sorting terms by a column.
	 *
	 * Uses `$this->sort_column` to determine field to sort by.
	 *
	 * @param stdClass $left Term object.
	 * @param stdClass $right Term object.
	 * @return int <0 if left is higher "priority" than right, 0 if equal, >0 if right is higher "priority" than left.
	 */
	protected function compare_terms( $left, $right ) {
		$col       = $this->sort_column;
		$left_val  = $left->$col;
		$right_val = $right->$col;

		if ( is_int( $left_val ) && is_int( $right_val ) ) {
			return $left_val - $right_val;
		}

		return strcmp( $left_val, $right_val );
	}

	/**
	 * Get the query params for collections
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params = parent::get_collection_params();

		$params['context']['default'] = 'view';

		$params['exclude']    = array(
			'description'       => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'sanitize_callback' => 'wp_parse_id_list',
		);
		$params['include']    = array(
			'description'       => __( 'Limit result set to specific ids.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'sanitize_callback' => 'wp_parse_id_list',
		);
		$params['offset']     = array(
			'description'       => __( 'Offset the result set by a specific number of items. Applies to hierarchical taxonomies only.', 'woocommerce' ),
			'type'              => 'integer',
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['order']      = array(
			'description'       => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
			'type'              => 'string',
			'sanitize_callback' => 'sanitize_key',
			'default'           => 'asc',
			'enum'              => array(
				'asc',
				'desc',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['orderby']    = array(
			'description'       => __( 'Sort collection by resource attribute.', 'woocommerce' ),
			'type'              => 'string',
			'sanitize_callback' => 'sanitize_key',
			'default'           => 'name',
			'enum'              => array(
				'id',
				'include',
				'name',
				'slug',
				'term_group',
				'description',
				'count',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['hide_empty'] = array(
			'description'       => __( 'Whether to hide resources not assigned to any products.', 'woocommerce' ),
			'type'              => 'boolean',
			'default'           => false,
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['parent']     = array(
			'description'       => __( 'Limit result set to resources assigned to a specific parent. Applies to hierarchical taxonomies only.', 'woocommerce' ),
			'type'              => 'integer',
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['product']    = array(
			'description'       => __( 'Limit result set to resources assigned to a specific product.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => null,
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['slug']       = array(
			'description'       => __( 'Limit result set to resources with a specific slug.', 'woocommerce' ),
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}

	/**
	 * Get taxonomy.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return int|WP_Error
	 */
	protected function get_taxonomy( $request ) {
		$attribute_id = $request['attribute_id'];

		if ( empty( $attribute_id ) ) {
			return $this->taxonomy;
		}

		if ( isset( $this->taxonomies_by_id[ $attribute_id ] ) ) {
			return $this->taxonomies_by_id[ $attribute_id ];
		}

		$taxonomy = WC()->call_function( 'wc_attribute_taxonomy_name_by_id', (int) $request['attribute_id'] );
		if ( ! empty( $taxonomy ) ) {
			$this->taxonomy                          = $taxonomy;
			$this->taxonomies_by_id[ $attribute_id ] = $taxonomy;
		}

		return $taxonomy;
	}
}
rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php000064400000114607151336044760023051 0ustar00<?php
/**
 * REST API Product Reviews Controller
 *
 * Handles requests to /products/reviews.
 *
 * @package WooCommerce\RestApi
 * @since   3.5.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Product Reviews Controller Class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Controller
 */
class WC_REST_Product_Reviews_Controller extends WC_REST_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'products/reviews';

	/**
	 * Register the routes for product reviews.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace, '/' . $this->rest_base, array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_item' ),
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
					'args'                => array_merge(
						$this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array(
							'product_id'     => array(
								'required'    => true,
								'description' => __( 'Unique identifier for the product.', 'woocommerce' ),
								'type'        => 'integer',
							),
							'review'         => array(
								'required'    => true,
								'type'        => 'string',
								'description' => __( 'Review content.', 'woocommerce' ),
							),
							'reviewer'       => array(
								'required'    => true,
								'type'        => 'string',
								'description' => __( 'Name of the reviewer.', 'woocommerce' ),
							),
							'reviewer_email' => array(
								'required'    => true,
								'type'        => 'string',
								'description' => __( 'Email of the reviewer.', 'woocommerce' ),
							),
						)
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
				'args'   => array(
					'id' => array(
						'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => array(
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_item' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_item' ),
					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
					'args'                => array(
						'force' => array(
							'default'     => false,
							'type'        => 'boolean',
							'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ),
						),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace, '/' . $this->rest_base . '/batch', array(
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'batch_items' ),
					'permission_callback' => array( $this, 'batch_items_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				'schema' => array( $this, 'get_public_batch_schema' ),
			)
		);
	}

	/**
	 * Check whether a given request has permission to read webhook deliveries.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_items_permissions_check( $request ) {
		if ( ! wc_rest_check_product_reviews_permissions( 'read' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access to read a product review.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_item_permissions_check( $request ) {
		$id     = (int) $request['id'];
		$review = get_comment( $id );

		if ( $review && ! wc_rest_check_product_reviews_permissions( 'read', $review->comment_ID ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access to create a new product review.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function create_item_permissions_check( $request ) {
		if ( ! wc_rest_check_product_reviews_permissions( 'create' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access to update a product review.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function update_item_permissions_check( $request ) {
		$id     = (int) $request['id'];
		$review = get_comment( $id );

		if ( $review && ! wc_rest_check_product_reviews_permissions( 'edit', $review->comment_ID ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access to delete a product review.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function delete_item_permissions_check( $request ) {
		$id     = (int) $request['id'];
		$review = get_comment( $id );

		if ( $review && ! wc_rest_check_product_reviews_permissions( 'delete', $review->comment_ID ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access batch create, update and delete items.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return boolean|WP_Error
	 */
	public function batch_items_permissions_check( $request ) {
		if ( ! wc_rest_check_product_reviews_permissions( 'create' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Get all reviews.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return array|WP_Error
	 */
	public function get_items( $request ) {
		// Retrieve the list of registered collection query parameters.
		$registered = $this->get_collection_params();

		/*
		 * This array defines mappings between public API query parameters whose
		 * values are accepted as-passed, and their internal WP_Query parameter
		 * name equivalents (some are the same). Only values which are also
		 * present in $registered will be set.
		 */
		$parameter_mappings = array(
			'reviewer'         => 'author__in',
			'reviewer_email'   => 'author_email',
			'reviewer_exclude' => 'author__not_in',
			'exclude'          => 'comment__not_in',
			'include'          => 'comment__in',
			'offset'           => 'offset',
			'order'            => 'order',
			'per_page'         => 'number',
			'product'          => 'post__in',
			'search'           => 'search',
			'status'           => 'status',
		);

		$prepared_args = array();

		/*
		 * For each known parameter which is both registered and present in the request,
		 * set the parameter's value on the query $prepared_args.
		 */
		foreach ( $parameter_mappings as $api_param => $wp_param ) {
			if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
				$prepared_args[ $wp_param ] = $request[ $api_param ];
			}
		}

		// Ensure certain parameter values default to empty strings.
		foreach ( array( 'author_email', 'search' ) as $param ) {
			if ( ! isset( $prepared_args[ $param ] ) ) {
				$prepared_args[ $param ] = '';
			}
		}

		if ( isset( $registered['orderby'] ) ) {
			$prepared_args['orderby'] = $this->normalize_query_param( $request['orderby'] );
		}

		if ( isset( $prepared_args['status'] ) ) {
			$prepared_args['status'] = 'approved' === $prepared_args['status'] ? 'approve' : $prepared_args['status'];
		}

		$prepared_args['no_found_rows'] = false;
		$prepared_args['date_query']    = array();

		// Set before into date query. Date query must be specified as an array of an array.
		if ( isset( $registered['before'], $request['before'] ) ) {
			$prepared_args['date_query'][0]['before'] = $request['before'];
		}

		// Set after into date query. Date query must be specified as an array of an array.
		if ( isset( $registered['after'], $request['after'] ) ) {
			$prepared_args['date_query'][0]['after'] = $request['after'];
		}

		if ( isset( $registered['page'] ) && empty( $request['offset'] ) ) {
			$prepared_args['offset'] = $prepared_args['number'] * ( absint( $request['page'] ) - 1 );
		}

		/**
		 * Filters arguments, before passing to WP_Comment_Query, when querying reviews via the REST API.
		 *
		 * @since 3.5.0
		 * @link https://developer.wordpress.org/reference/classes/wp_comment_query/
		 * @param array           $prepared_args Array of arguments for WP_Comment_Query.
		 * @param WP_REST_Request $request       The current request.
		 */
		$prepared_args = apply_filters( 'woocommerce_rest_product_review_query', $prepared_args, $request );

		// Make sure that returns only reviews.
		$prepared_args['type'] = 'review';

		// Query reviews.
		$query        = new WP_Comment_Query();
		$query_result = $query->query( $prepared_args );
		$reviews      = array();

		foreach ( $query_result as $review ) {
			if ( ! wc_rest_check_product_reviews_permissions( 'read', $review->comment_ID ) ) {
				continue;
			}

			$data      = $this->prepare_item_for_response( $review, $request );
			$reviews[] = $this->prepare_response_for_collection( $data );
		}

		$total_reviews = (int) $query->found_comments;
		$max_pages     = (int) $query->max_num_pages;

		if ( $total_reviews < 1 ) {
			// Out-of-bounds, run the query again without LIMIT for total count.
			unset( $prepared_args['number'], $prepared_args['offset'] );

			$query                  = new WP_Comment_Query();
			$prepared_args['count'] = true;

			$total_reviews = $query->query( $prepared_args );
			$max_pages     = ceil( $total_reviews / $request['per_page'] );
		}

		$response = rest_ensure_response( $reviews );
		$response->header( 'X-WP-Total', $total_reviews );
		$response->header( 'X-WP-TotalPages', $max_pages );

		$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) );

		if ( $request['page'] > 1 ) {
			$prev_page = $request['page'] - 1;

			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}

			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}

		if ( $max_pages > $request['page'] ) {
			$next_page = $request['page'] + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );

			$response->link_header( 'next', $next_link );
		}

		return $response;
	}

	/**
	 * Create a single review.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function create_item( $request ) {
		if ( ! empty( $request['id'] ) ) {
			return new WP_Error( 'woocommerce_rest_review_exists', __( 'Cannot create existing product review.', 'woocommerce' ), array( 'status' => 400 ) );
		}

		$product_id = (int) $request['product_id'];

		if ( 'product' !== get_post_type( $product_id ) ) {
			return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$prepared_review = $this->prepare_item_for_database( $request );
		if ( is_wp_error( $prepared_review ) ) {
			return $prepared_review;
		}

		$prepared_review['comment_type'] = 'review';

		/*
		 * Do not allow a comment to be created with missing or empty comment_content. See wp_handle_comment_submission().
		 */
		if ( empty( $prepared_review['comment_content'] ) ) {
			return new WP_Error( 'woocommerce_rest_review_content_invalid', __( 'Invalid review content.', 'woocommerce' ), array( 'status' => 400 ) );
		}

		// Setting remaining values before wp_insert_comment so we can use wp_allow_comment().
		if ( ! isset( $prepared_review['comment_date_gmt'] ) ) {
			$prepared_review['comment_date_gmt'] = current_time( 'mysql', true );
		}

		if ( ! empty( $_SERVER['REMOTE_ADDR'] ) && rest_is_ip_address( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) ) { // WPCS: input var ok, sanitization ok.
			$prepared_review['comment_author_IP'] = wc_clean( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ); // WPCS: input var ok.
		} else {
			$prepared_review['comment_author_IP'] = '127.0.0.1';
		}

		if ( ! empty( $request['author_user_agent'] ) ) {
			$prepared_review['comment_agent'] = $request['author_user_agent'];
		} elseif ( $request->get_header( 'user_agent' ) ) {
			$prepared_review['comment_agent'] = $request->get_header( 'user_agent' );
		} else {
			$prepared_review['comment_agent'] = '';
		}

		$check_comment_lengths = wp_check_comment_data_max_lengths( $prepared_review );
		if ( is_wp_error( $check_comment_lengths ) ) {
			$error_code = str_replace( array( 'comment_author', 'comment_content' ), array( 'reviewer', 'review_content' ), $check_comment_lengths->get_error_code() );
			return new WP_Error( 'woocommerce_rest_' . $error_code, __( 'Product review field exceeds maximum length allowed.', 'woocommerce' ), array( 'status' => 400 ) );
		}

		$prepared_review['comment_parent']     = 0;
		$prepared_review['comment_author_url'] = '';
		$prepared_review['comment_approved']   = wp_allow_comment( $prepared_review, true );

		if ( is_wp_error( $prepared_review['comment_approved'] ) ) {
			$error_code    = $prepared_review['comment_approved']->get_error_code();
			$error_message = $prepared_review['comment_approved']->get_error_message();

			if ( 'comment_duplicate' === $error_code ) {
				return new WP_Error( 'woocommerce_rest_' . $error_code, $error_message, array( 'status' => 409 ) );
			}

			if ( 'comment_flood' === $error_code ) {
				return new WP_Error( 'woocommerce_rest_' . $error_code, $error_message, array( 'status' => 400 ) );
			}

			return $prepared_review['comment_approved'];
		}

		/**
		 * Filters a review before it is inserted via the REST API.
		 *
		 * Allows modification of the review right before it is inserted via wp_insert_comment().
		 * Returning a WP_Error value from the filter will shortcircuit insertion and allow
		 * skipping further processing.
		 *
		 * @since 3.5.0
		 * @param array|WP_Error  $prepared_review The prepared review data for wp_insert_comment().
		 * @param WP_REST_Request $request          Request used to insert the review.
		 */
		$prepared_review = apply_filters( 'woocommerce_rest_pre_insert_product_review', $prepared_review, $request );
		if ( is_wp_error( $prepared_review ) ) {
			return $prepared_review;
		}

		$review_id = wp_insert_comment( wp_filter_comment( wp_slash( (array) $prepared_review ) ) );

		if ( ! $review_id ) {
			return new WP_Error( 'woocommerce_rest_review_failed_create', __( 'Creating product review failed.', 'woocommerce' ), array( 'status' => 500 ) );
		}

		if ( isset( $request['status'] ) ) {
			$this->handle_status_param( $request['status'], $review_id );
		}

		update_comment_meta( $review_id, 'rating', ! empty( $request['rating'] ) ? $request['rating'] : '0' );

		$review = get_comment( $review_id );

		/**
		 * Fires after a comment is created or updated via the REST API.
		 *
		 * @param WP_Comment      $review   Inserted or updated comment object.
		 * @param WP_REST_Request $request  Request object.
		 * @param bool            $creating True when creating a comment, false when updating.
		 */
		do_action( 'woocommerce_rest_insert_product_review', $review, $request, true );

		$fields_update = $this->update_additional_fields_for_object( $review, $request );
		if ( is_wp_error( $fields_update ) ) {
			return $fields_update;
		}

		$context = current_user_can( 'moderate_comments' ) ? 'edit' : 'view';
		$request->set_param( 'context', $context );

		$response = $this->prepare_item_for_response( $review, $request );
		$response = rest_ensure_response( $response );

		$response->set_status( 201 );
		$response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $review_id ) ) );

		return $response;
	}

	/**
	 * Get a single product review.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_item( $request ) {
		$review = $this->get_review( $request['id'] );
		if ( is_wp_error( $review ) ) {
			return $review;
		}

		$data     = $this->prepare_item_for_response( $review, $request );
		$response = rest_ensure_response( $data );

		return $response;
	}

	/**
	 * Updates a review.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response Response object on success, or error object on failure.
	 */
	public function update_item( $request ) {
		$review = $this->get_review( $request['id'] );
		if ( is_wp_error( $review ) ) {
			return $review;
		}

		$id = (int) $review->comment_ID;

		if ( isset( $request['type'] ) && 'review' !== get_comment_type( $id ) ) {
			return new WP_Error( 'woocommerce_rest_review_invalid_type', __( 'Sorry, you are not allowed to change the comment type.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$prepared_args = $this->prepare_item_for_database( $request );
		if ( is_wp_error( $prepared_args ) ) {
			return $prepared_args;
		}

		if ( ! empty( $prepared_args['comment_post_ID'] ) ) {
			if ( 'product' !== get_post_type( (int) $prepared_args['comment_post_ID'] ) ) {
				return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
			}
		}

		if ( empty( $prepared_args ) && isset( $request['status'] ) ) {
			// Only the comment status is being changed.
			$change = $this->handle_status_param( $request['status'], $id );

			if ( ! $change ) {
				return new WP_Error( 'woocommerce_rest_review_failed_edit', __( 'Updating review status failed.', 'woocommerce' ), array( 'status' => 500 ) );
			}
		} elseif ( ! empty( $prepared_args ) ) {
			if ( is_wp_error( $prepared_args ) ) {
				return $prepared_args;
			}

			if ( isset( $prepared_args['comment_content'] ) && empty( $prepared_args['comment_content'] ) ) {
				return new WP_Error( 'woocommerce_rest_review_content_invalid', __( 'Invalid review content.', 'woocommerce' ), array( 'status' => 400 ) );
			}

			$prepared_args['comment_ID'] = $id;

			$check_comment_lengths = wp_check_comment_data_max_lengths( $prepared_args );
			if ( is_wp_error( $check_comment_lengths ) ) {
				$error_code = str_replace( array( 'comment_author', 'comment_content' ), array( 'reviewer', 'review_content' ), $check_comment_lengths->get_error_code() );
				return new WP_Error( 'woocommerce_rest_' . $error_code, __( 'Product review field exceeds maximum length allowed.', 'woocommerce' ), array( 'status' => 400 ) );
			}

			$updated = wp_update_comment( wp_slash( (array) $prepared_args ) );

			if ( false === $updated ) {
				return new WP_Error( 'woocommerce_rest_comment_failed_edit', __( 'Updating review failed.', 'woocommerce' ), array( 'status' => 500 ) );
			}

			if ( isset( $request['status'] ) ) {
				$this->handle_status_param( $request['status'], $id );
			}
		}

		if ( ! empty( $request['rating'] ) ) {
			update_comment_meta( $id, 'rating', $request['rating'] );
		}

		$review = get_comment( $id );

		/** This action is documented in includes/api/class-wc-rest-product-reviews-controller.php */
		do_action( 'woocommerce_rest_insert_product_review', $review, $request, false );

		$fields_update = $this->update_additional_fields_for_object( $review, $request );

		if ( is_wp_error( $fields_update ) ) {
			return $fields_update;
		}

		$request->set_param( 'context', 'edit' );

		$response = $this->prepare_item_for_response( $review, $request );

		return rest_ensure_response( $response );
	}

	/**
	 * Deletes a review.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response Response object on success, or error object on failure.
	 */
	public function delete_item( $request ) {
		$review = $this->get_review( $request['id'] );
		if ( is_wp_error( $review ) ) {
			return $review;
		}

		$force = isset( $request['force'] ) ? (bool) $request['force'] : false;

		/**
		 * Filters whether a review can be trashed.
		 *
		 * Return false to disable trash support for the post.
		 *
		 * @since 3.5.0
		 * @param bool       $supports_trash Whether the post type support trashing.
		 * @param WP_Comment $review         The review object being considered for trashing support.
		 */
		$supports_trash = apply_filters( 'woocommerce_rest_product_review_trashable', ( EMPTY_TRASH_DAYS > 0 ), $review );

		$request->set_param( 'context', 'edit' );

		if ( $force ) {
			$previous = $this->prepare_item_for_response( $review, $request );
			$result   = wp_delete_comment( $review->comment_ID, true );
			$response = new WP_REST_Response();
			$response->set_data(
				array(
					'deleted'  => true,
					'previous' => $previous->get_data(),
				)
			);
		} else {
			// If this type doesn't support trashing, error out.
			if ( ! $supports_trash ) {
				/* translators: %s: force=true */
				return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( "The object does not support trashing. Set '%s' to delete.", 'woocommerce' ), 'force=true' ), array( 'status' => 501 ) );
			}

			if ( 'trash' === $review->comment_approved ) {
				return new WP_Error( 'woocommerce_rest_already_trashed', __( 'The object has already been trashed.', 'woocommerce' ), array( 'status' => 410 ) );
			}

			$result   = wp_trash_comment( $review->comment_ID );
			$review   = get_comment( $review->comment_ID );
			$response = $this->prepare_item_for_response( $review, $request );
		}

		if ( ! $result ) {
			return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The object cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) );
		}

		/**
		 * Fires after a review is deleted via the REST API.
		 *
		 * @param WP_Comment       $review   The deleted review data.
		 * @param WP_REST_Response $response The response returned from the API.
		 * @param WP_REST_Request  $request  The request sent to the API.
		 */
		do_action( 'woocommerce_rest_delete_review', $review, $response, $request );

		return $response;
	}

	/**
	 * Prepare a single product review output for response.
	 *
	 * @param WP_Comment      $review Product review object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $review, $request ) {
		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$fields  = $this->get_fields_for_response( $request );
		$data    = array();

		if ( in_array( 'id', $fields, true ) ) {
			$data['id'] = (int) $review->comment_ID;
		}
		if ( in_array( 'date_created', $fields, true ) ) {
			$data['date_created'] = wc_rest_prepare_date_response( $review->comment_date );
		}
		if ( in_array( 'date_created_gmt', $fields, true ) ) {
			$data['date_created_gmt'] = wc_rest_prepare_date_response( $review->comment_date_gmt );
		}
		if ( in_array( 'product_id', $fields, true ) ) {
			$data['product_id'] = (int) $review->comment_post_ID;
		}
		if ( in_array( 'status', $fields, true ) ) {
			$data['status'] = $this->prepare_status_response( (string) $review->comment_approved );
		}
		if ( in_array( 'reviewer', $fields, true ) ) {
			$data['reviewer'] = $review->comment_author;
		}
		if ( in_array( 'reviewer_email', $fields, true ) ) {
			$data['reviewer_email'] = $review->comment_author_email;
		}
		if ( in_array( 'review', $fields, true ) ) {
			$data['review'] = 'view' === $context ? wpautop( $review->comment_content ) : $review->comment_content;
		}
		if ( in_array( 'rating', $fields, true ) ) {
			$data['rating'] = (int) get_comment_meta( $review->comment_ID, 'rating', true );
		}
		if ( in_array( 'verified', $fields, true ) ) {
			$data['verified'] = wc_review_is_from_verified_owner( $review->comment_ID );
		}
		if ( in_array( 'reviewer_avatar_urls', $fields, true ) ) {
			$data['reviewer_avatar_urls'] = rest_get_avatar_urls( $review->comment_author_email );
		}

		$data = $this->add_additional_fields_to_object( $data, $request );
		$data = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $review ) );

		/**
		 * Filter product reviews object returned from the REST API.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param WP_Comment       $review   Product review object used to create response.
		 * @param WP_REST_Request  $request  Request object.
		 */
		return apply_filters( 'woocommerce_rest_prepare_product_review', $response, $review, $request );
	}

	/**
	 * Prepare a single product review to be inserted into the database.
	 *
	 * @param  WP_REST_Request $request Request object.
	 * @return array|WP_Error  $prepared_review
	 */
	protected function prepare_item_for_database( $request ) {
		if ( isset( $request['id'] ) ) {
			$prepared_review['comment_ID'] = (int) $request['id'];
		}

		if ( isset( $request['review'] ) ) {
			$prepared_review['comment_content'] = $request['review'];
		}

		if ( isset( $request['product_id'] ) ) {
			$prepared_review['comment_post_ID'] = (int) $request['product_id'];
		}

		if ( isset( $request['reviewer'] ) ) {
			$prepared_review['comment_author'] = $request['reviewer'];
		}

		if ( isset( $request['reviewer_email'] ) ) {
			$prepared_review['comment_author_email'] = $request['reviewer_email'];
		}

		if ( ! empty( $request['date_created'] ) ) {
			$date_data = rest_get_date_with_gmt( $request['date_created'] );

			if ( ! empty( $date_data ) ) {
				list( $prepared_review['comment_date'], $prepared_review['comment_date_gmt'] ) = $date_data;
			}
		} elseif ( ! empty( $request['date_created_gmt'] ) ) {
			$date_data = rest_get_date_with_gmt( $request['date_created_gmt'], true );

			if ( ! empty( $date_data ) ) {
				list( $prepared_review['comment_date'], $prepared_review['comment_date_gmt'] ) = $date_data;
			}
		}

		/**
		 * Filters a review after it is prepared for the database.
		 *
		 * Allows modification of the review right after it is prepared for the database.
		 *
		 * @since 3.5.0
		 * @param array           $prepared_review The prepared review data for `wp_insert_comment`.
		 * @param WP_REST_Request $request         The current request.
		 */
		return apply_filters( 'woocommerce_rest_preprocess_product_review', $prepared_review, $request );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param WP_Comment $review Product review object.
	 * @return array Links for the given product review.
	 */
	protected function prepare_links( $review ) {
		$links = array(
			'self'       => array(
				'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $review->comment_ID ) ),
			),
			'collection' => array(
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
			),
		);

		if ( 0 !== (int) $review->comment_post_ID ) {
			$links['up'] = array(
				'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $review->comment_post_ID ) ),
			);
		}

		if ( 0 !== (int) $review->user_id ) {
			$links['reviewer'] = array(
				'href'       => rest_url( 'wp/v2/users/' . $review->user_id ),
				'embeddable' => true,
			);
		}

		return $links;
	}

	/**
	 * Get the Product Review's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'product_review',
			'type'       => 'object',
			'properties' => array(
				'id'               => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_created'     => array(
					'description' => __( "The date the review was created, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_created_gmt' => array(
					'description' => __( 'The date the review was created, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'product_id'       => array(
					'description' => __( 'Unique identifier for the product that the review belongs to.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'status'           => array(
					'description' => __( 'Status of the review.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'approved',
					'enum'        => array( 'approved', 'hold', 'spam', 'unspam', 'trash', 'untrash' ),
					'context'     => array( 'view', 'edit' ),
				),
				'reviewer'         => array(
					'description' => __( 'Reviewer name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'reviewer_email'   => array(
					'description' => __( 'Reviewer email.', 'woocommerce' ),
					'type'        => 'string',
					'format'      => 'email',
					'context'     => array( 'view', 'edit' ),
				),
				'review'           => array(
					'description' => __( 'The content of the review.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'wp_filter_post_kses',
					),
				),
				'rating'           => array(
					'description' => __( 'Review rating (0 to 5).', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'verified'         => array(
					'description' => __( 'Shows if the reviewer bought the product or not.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		if ( get_option( 'show_avatars' ) ) {
			$avatar_properties = array();
			$avatar_sizes      = rest_get_avatar_sizes();

			foreach ( $avatar_sizes as $size ) {
				$avatar_properties[ $size ] = array(
					/* translators: %d: avatar image size in pixels */
					'description' => sprintf( __( 'Avatar URL with image size of %d pixels.', 'woocommerce' ), $size ),
					'type'        => 'string',
					'format'      => 'uri',
					'context'     => array( 'embed', 'view', 'edit' ),
				);
			}
			$schema['properties']['reviewer_avatar_urls'] = array(
				'description' => __( 'Avatar URLs for the object reviewer.', 'woocommerce' ),
				'type'        => 'object',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'properties'  => $avatar_properties,
			);
		}

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params = parent::get_collection_params();

		$params['context']['default'] = 'view';

		$params['after']            = array(
			'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
			'type'        => 'string',
			'format'      => 'date-time',
		);
		$params['before']           = array(
			'description' => __( 'Limit response to reviews published before a given ISO8601 compliant date.', 'woocommerce' ),
			'type'        => 'string',
			'format'      => 'date-time',
		);
		$params['exclude']          = array(
			'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'integer',
			),
			'default'     => array(),
		);
		$params['include']          = array(
			'description' => __( 'Limit result set to specific IDs.', 'woocommerce' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'integer',
			),
			'default'     => array(),
		);
		$params['offset']           = array(
			'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ),
			'type'        => 'integer',
		);
		$params['order']            = array(
			'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
			'type'        => 'string',
			'default'     => 'desc',
			'enum'        => array(
				'asc',
				'desc',
			),
		);
		$params['orderby']          = array(
			'description' => __( 'Sort collection by object attribute.', 'woocommerce' ),
			'type'        => 'string',
			'default'     => 'date_gmt',
			'enum'        => array(
				'date',
				'date_gmt',
				'id',
				'include',
				'product',
			),
		);
		$params['reviewer']         = array(
			'description' => __( 'Limit result set to reviews assigned to specific user IDs.', 'woocommerce' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'integer',
			),
		);
		$params['reviewer_exclude'] = array(
			'description' => __( 'Ensure result set excludes reviews assigned to specific user IDs.', 'woocommerce' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'integer',
			),
		);
		$params['reviewer_email']   = array(
			'default'     => null,
			'description' => __( 'Limit result set to that from a specific author email.', 'woocommerce' ),
			'format'      => 'email',
			'type'        => 'string',
		);
		$params['product']          = array(
			'default'     => array(),
			'description' => __( 'Limit result set to reviews assigned to specific product IDs.', 'woocommerce' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'integer',
			),
		);
		$params['status']           = array(
			'default'           => 'approved',
			'description'       => __( 'Limit result set to reviews assigned a specific status.', 'woocommerce' ),
			'sanitize_callback' => 'sanitize_key',
			'type'              => 'string',
			'enum'              => array(
				'all',
				'hold',
				'approved',
				'spam',
				'trash',
			),
		);

		/**
		 * Filter collection parameters for the reviews controller.
		 *
		 * This filter registers the collection parameter, but does not map the
		 * collection parameter to an internal WP_Comment_Query parameter. Use the
		 * `wc_rest_review_query` filter to set WP_Comment_Query parameters.
		 *
		 * @since 3.5.0
		 * @param array $params JSON Schema-formatted collection parameters.
		 */
		return apply_filters( 'woocommerce_rest_product_review_collection_params', $params );
	}

	/**
	 * Get the reivew, if the ID is valid.
	 *
	 * @since 3.5.0
	 * @param int $id Supplied ID.
	 * @return WP_Comment|WP_Error Comment object if ID is valid, WP_Error otherwise.
	 */
	protected function get_review( $id ) {
		$id    = (int) $id;
		$error = new WP_Error( 'woocommerce_rest_review_invalid_id', __( 'Invalid review ID.', 'woocommerce' ), array( 'status' => 404 ) );

		if ( 0 >= $id ) {
			return $error;
		}

		$review = get_comment( $id );
		if ( empty( $review ) ) {
			return $error;
		}

		if ( ! empty( $review->comment_post_ID ) ) {
			$post = get_post( (int) $review->comment_post_ID );

			if ( 'product' !== get_post_type( (int) $review->comment_post_ID ) ) {
				return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
			}
		}

		return $review;
	}

	/**
	 * Prepends internal property prefix to query parameters to match our response fields.
	 *
	 * @since 3.5.0
	 * @param string $query_param Query parameter.
	 * @return string
	 */
	protected function normalize_query_param( $query_param ) {
		$prefix = 'comment_';

		switch ( $query_param ) {
			case 'id':
				$normalized = $prefix . 'ID';
				break;
			case 'product':
				$normalized = $prefix . 'post_ID';
				break;
			case 'include':
				$normalized = 'comment__in';
				break;
			default:
				$normalized = $prefix . $query_param;
				break;
		}

		return $normalized;
	}

	/**
	 * Checks comment_approved to set comment status for single comment output.
	 *
	 * @since 3.5.0
	 * @param string|int $comment_approved comment status.
	 * @return string Comment status.
	 */
	protected function prepare_status_response( $comment_approved ) {
		switch ( $comment_approved ) {
			case 'hold':
			case '0':
				$status = 'hold';
				break;
			case 'approve':
			case '1':
				$status = 'approved';
				break;
			case 'spam':
			case 'trash':
			default:
				$status = $comment_approved;
				break;
		}

		return $status;
	}

	/**
	 * Sets the comment_status of a given review object when creating or updating a review.
	 *
	 * @since 3.5.0
	 * @param string|int $new_status New review status.
	 * @param int        $id         Review ID.
	 * @return bool Whether the status was changed.
	 */
	protected function handle_status_param( $new_status, $id ) {
		$old_status = wp_get_comment_status( $id );

		if ( $new_status === $old_status ) {
			return false;
		}

		switch ( $new_status ) {
			case 'approved':
			case 'approve':
			case '1':
				$changed = wp_set_comment_status( $id, 'approve' );
				break;
			case 'hold':
			case '0':
				$changed = wp_set_comment_status( $id, 'hold' );
				break;
			case 'spam':
				$changed = wp_spam_comment( $id );
				break;
			case 'unspam':
				$changed = wp_unspam_comment( $id );
				break;
			case 'trash':
				$changed = wp_trash_comment( $id );
				break;
			case 'untrash':
				$changed = wp_untrash_comment( $id );
				break;
			default:
				$changed = false;
				break;
		}

		return $changed;
	}
}
rest-api/Controllers/Version3/class-wc-rest-customer-downloads-controller.php000064400000001031151336044760023522 0ustar00<?php
/**
 * REST API Customer Downloads controller
 *
 * Handles requests to the /customers/<customer_id>/downloads endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Customers controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Customer_Downloads_V2_Controller
 */
class WC_REST_Customer_Downloads_Controller extends WC_REST_Customer_Downloads_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';
}
rest-api/Controllers/Version3/class-wc-rest-data-currencies-controller.php000064400000014164151336044760022755 0ustar00<?php
/**
 * REST API Data currencies controller.
 *
 * Handles requests to the /data/currencies endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   3.5.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Data Currencies controller class.
 *
 * @package WooCommerce\RestApi
 */
class WC_REST_Data_Currencies_Controller extends WC_REST_Data_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'data/currencies';

	/**
	 * Register routes.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/current',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_current_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<currency>[\w-]{3})',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => array(
						'location' => array(
							'description' => __( 'ISO4217 currency code.', 'woocommerce' ),
							'type'        => 'string',
						),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Get currency information.
	 *
	 * @param  string          $code    Currency code.
	 * @param  WP_REST_Request $request Request data.
	 * @return array|mixed Response data, ready for insertion into collection data.
	 */
	public function get_currency( $code, $request ) {
		$currencies = get_woocommerce_currencies();
		$data       = array();

		if ( ! array_key_exists( $code, $currencies ) ) {
			return false;
		}

		$currency = array(
			'code'   => $code,
			'name'   => $currencies[ $code ],
			'symbol' => get_woocommerce_currency_symbol( $code ),
		);

		return $currency;
	}

	/**
	 * Return the list of currencies.
	 *
	 * @param  WP_REST_Request $request Request data.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_items( $request ) {
		$currencies = get_woocommerce_currencies();
		foreach ( array_keys( $currencies ) as $code ) {
			$currency = $this->get_currency( $code, $request );
			$response = $this->prepare_item_for_response( $currency, $request );
			$data[]   = $this->prepare_response_for_collection( $response );
		}

		return rest_ensure_response( $data );
	}

	/**
	 * Return information for a specific currency.
	 *
	 * @param  WP_REST_Request $request Request data.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_item( $request ) {
		$data = $this->get_currency( strtoupper( $request['currency'] ), $request );
		if ( empty( $data ) ) {
			return new WP_Error( 'woocommerce_rest_data_invalid_currency', __( 'There are no currencies matching these parameters.', 'woocommerce' ), array( 'status' => 404 ) );
		}
		return $this->prepare_item_for_response( $data, $request );
	}

	/**
	 * Return information for the current site currency.
	 *
	 * @param  WP_REST_Request $request Request data.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_current_item( $request ) {
		$currency = get_option( 'woocommerce_currency' );
		return $this->prepare_item_for_response( $this->get_currency( $currency, $request ), $request );
	}

	/**
	 * Prepare the data object for response.
	 *
	 * @param object          $item Data object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $item, $request ) {
		$data     = $this->add_additional_fields_to_object( $item, $request );
		$data     = $this->filter_response_by_context( $data, 'view' );
		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $item ) );

		/**
		 * Filter currency returned from the API.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param array            $item     Currency data.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_data_currency', $response, $item, $request );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param object $item Data object.
	 * @return array Links for the given currency.
	 */
	protected function prepare_links( $item ) {
		$code  = strtoupper( $item['code'] );
		$links = array(
			'self'       => array(
				'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $code ) ),
			),
			'collection' => array(
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
			),
		);

		return $links;
	}


	/**
	 * Get the currency schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'data_currencies',
			'type'       => 'object',
			'properties' => array(
				'code'   => array(
					'type'        => 'string',
					'description' => __( 'ISO4217 currency code.', 'woocommerce' ),
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'name'   => array(
					'type'        => 'string',
					'description' => __( 'Full name of currency.', 'woocommerce' ),
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'symbol' => array(
					'type'        => 'string',
					'description' => __( 'Currency symbol.', 'woocommerce' ),
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php000064400000075154151336044760023547 0ustar00<?php
/**
 * REST API variations controller
 *
 * Handles requests to the /products/<product_id>/variations endpoints.
 *
 * @package WooCommerce\RestApi
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API variations controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Product_Variations_V2_Controller
 */
class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';

	/**
	 * Prepare a single variation output for response.
	 *
	 * @param  WC_Data         $object  Object data.
	 * @param  WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_object_for_response( $object, $request ) {
		$data = array(
			'id'                    => $object->get_id(),
			'date_created'          => wc_rest_prepare_date_response( $object->get_date_created(), false ),
			'date_created_gmt'      => wc_rest_prepare_date_response( $object->get_date_created() ),
			'date_modified'         => wc_rest_prepare_date_response( $object->get_date_modified(), false ),
			'date_modified_gmt'     => wc_rest_prepare_date_response( $object->get_date_modified() ),
			'description'           => wc_format_content( $object->get_description() ),
			'permalink'             => $object->get_permalink(),
			'sku'                   => $object->get_sku(),
			'price'                 => $object->get_price(),
			'regular_price'         => $object->get_regular_price(),
			'sale_price'            => $object->get_sale_price(),
			'date_on_sale_from'     => wc_rest_prepare_date_response( $object->get_date_on_sale_from(), false ),
			'date_on_sale_from_gmt' => wc_rest_prepare_date_response( $object->get_date_on_sale_from() ),
			'date_on_sale_to'       => wc_rest_prepare_date_response( $object->get_date_on_sale_to(), false ),
			'date_on_sale_to_gmt'   => wc_rest_prepare_date_response( $object->get_date_on_sale_to() ),
			'on_sale'               => $object->is_on_sale(),
			'status'                => $object->get_status(),
			'purchasable'           => $object->is_purchasable(),
			'virtual'               => $object->is_virtual(),
			'downloadable'          => $object->is_downloadable(),
			'downloads'             => $this->get_downloads( $object ),
			'download_limit'        => '' !== $object->get_download_limit() ? (int) $object->get_download_limit() : -1,
			'download_expiry'       => '' !== $object->get_download_expiry() ? (int) $object->get_download_expiry() : -1,
			'tax_status'            => $object->get_tax_status(),
			'tax_class'             => $object->get_tax_class(),
			'manage_stock'          => $object->managing_stock(),
			'stock_quantity'        => $object->get_stock_quantity(),
			'stock_status'          => $object->get_stock_status(),
			'backorders'            => $object->get_backorders(),
			'backorders_allowed'    => $object->backorders_allowed(),
			'backordered'           => $object->is_on_backorder(),
			'low_stock_amount'      => '' === $object->get_low_stock_amount() ? null : $object->get_low_stock_amount(),
			'weight'                => $object->get_weight(),
			'dimensions'            => array(
				'length' => $object->get_length(),
				'width'  => $object->get_width(),
				'height' => $object->get_height(),
			),
			'shipping_class'        => $object->get_shipping_class(),
			'shipping_class_id'     => $object->get_shipping_class_id(),
			'image'                 => $this->get_image( $object ),
			'attributes'            => $this->get_attributes( $object ),
			'menu_order'            => $object->get_menu_order(),
			'meta_data'             => $object->get_meta_data(),
		);

		$context  = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data     = $this->add_additional_fields_to_object( $data, $request );
		$data     = $this->filter_response_by_context( $data, $context );
		$response = rest_ensure_response( $data );
		$response->add_links( $this->prepare_links( $object, $request ) );

		/**
		 * Filter the data for a response.
		 *
		 * The dynamic portion of the hook name, $this->post_type,
		 * refers to object type being prepared for the response.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param WC_Data          $object   Object data.
		 * @param WP_REST_Request  $request  Request object.
		 */
		return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request );
	}

	/**
	 * Prepare a single variation for create or update.
	 *
	 * @param  WP_REST_Request $request Request object.
	 * @param  bool            $creating If is creating a new object.
	 * @return WP_Error|WC_Data
	 */
	protected function prepare_object_for_database( $request, $creating = false ) {
		if ( isset( $request['id'] ) ) {
			$variation = wc_get_product( absint( $request['id'] ) );
		} else {
			$variation = new WC_Product_Variation();
		}

		$variation->set_parent_id( absint( $request['product_id'] ) );

		// Status.
		if ( isset( $request['status'] ) ) {
			$variation->set_status( get_post_status_object( $request['status'] ) ? $request['status'] : 'draft' );
		}

		// SKU.
		if ( isset( $request['sku'] ) ) {
			$variation->set_sku( wc_clean( $request['sku'] ) );
		}

		// Thumbnail.
		if ( isset( $request['image'] ) ) {
			if ( is_array( $request['image'] ) ) {
				$variation = $this->set_variation_image( $variation, $request['image'] );
			} else {
				$variation->set_image_id( '' );
			}
		}

		// Virtual variation.
		if ( isset( $request['virtual'] ) ) {
			$variation->set_virtual( $request['virtual'] );
		}

		// Downloadable variation.
		if ( isset( $request['downloadable'] ) ) {
			$variation->set_downloadable( $request['downloadable'] );
		}

		// Downloads.
		if ( $variation->get_downloadable() ) {
			// Downloadable files.
			if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) {
				$variation = $this->save_downloadable_files( $variation, $request['downloads'] );
			}

			// Download limit.
			if ( isset( $request['download_limit'] ) ) {
				$variation->set_download_limit( $request['download_limit'] );
			}

			// Download expiry.
			if ( isset( $request['download_expiry'] ) ) {
				$variation->set_download_expiry( $request['download_expiry'] );
			}
		}

		// Shipping data.
		$variation = $this->save_product_shipping_data( $variation, $request );

		// Stock handling.
		if ( isset( $request['manage_stock'] ) ) {
			$variation->set_manage_stock( $request['manage_stock'] );
		}

		if ( isset( $request['stock_status'] ) ) {
			$variation->set_stock_status( $request['stock_status'] );
		}

		if ( isset( $request['backorders'] ) ) {
			$variation->set_backorders( $request['backorders'] );
		}

		if ( $variation->get_manage_stock() ) {
			if ( isset( $request['stock_quantity'] ) ) {
				$variation->set_stock_quantity( $request['stock_quantity'] );
			} elseif ( isset( $request['inventory_delta'] ) ) {
				$stock_quantity  = wc_stock_amount( $variation->get_stock_quantity() );
				$stock_quantity += wc_stock_amount( $request['inventory_delta'] );
				$variation->set_stock_quantity( $stock_quantity );
			}
			// isset() returns false for value null, thus we need to check whether the value has been sent by the request.
			if ( array_key_exists( 'low_stock_amount', $request->get_params() ) ) {
				if ( null === $request['low_stock_amount'] ) {
					$variation->set_low_stock_amount( '' );
				} else {
					$variation->set_low_stock_amount( wc_stock_amount( $request['low_stock_amount'] ) );
				}
			}
		} else {
			$variation->set_backorders( 'no' );
			$variation->set_stock_quantity( '' );
			$variation->set_low_stock_amount( '' );
		}

		// Regular Price.
		if ( isset( $request['regular_price'] ) ) {
			$variation->set_regular_price( $request['regular_price'] );
		}

		// Sale Price.
		if ( isset( $request['sale_price'] ) ) {
			$variation->set_sale_price( $request['sale_price'] );
		}

		if ( isset( $request['date_on_sale_from'] ) ) {
			$variation->set_date_on_sale_from( $request['date_on_sale_from'] );
		}

		if ( isset( $request['date_on_sale_from_gmt'] ) ) {
			$variation->set_date_on_sale_from( $request['date_on_sale_from_gmt'] ? strtotime( $request['date_on_sale_from_gmt'] ) : null );
		}

		if ( isset( $request['date_on_sale_to'] ) ) {
			$variation->set_date_on_sale_to( $request['date_on_sale_to'] );
		}

		if ( isset( $request['date_on_sale_to_gmt'] ) ) {
			$variation->set_date_on_sale_to( $request['date_on_sale_to_gmt'] ? strtotime( $request['date_on_sale_to_gmt'] ) : null );
		}

		// Tax class.
		if ( isset( $request['tax_class'] ) ) {
			$variation->set_tax_class( $request['tax_class'] );
		}

		// Description.
		if ( isset( $request['description'] ) ) {
			$variation->set_description( wp_kses_post( $request['description'] ) );
		}

		// Update taxonomies.
		if ( isset( $request['attributes'] ) ) {
			$attributes = array();
			$parent     = wc_get_product( $variation->get_parent_id() );

			if ( ! $parent ) {
				return new WP_Error(
					// Translators: %d parent ID.
					"woocommerce_rest_{$this->post_type}_invalid_parent",
					__( 'Cannot set attributes due to invalid parent product.', 'woocommerce' ),
					array( 'status' => 404 )
				);
			}

			$parent_attributes = $parent->get_attributes();

			foreach ( $request['attributes'] as $attribute ) {
				$attribute_id   = 0;
				$attribute_name = '';

				// Check ID for global attributes or name for product attributes.
				if ( ! empty( $attribute['id'] ) ) {
					$attribute_id   = absint( $attribute['id'] );
					$attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
				} elseif ( ! empty( $attribute['name'] ) ) {
					$attribute_name = sanitize_title( $attribute['name'] );
				}

				if ( ! $attribute_id && ! $attribute_name ) {
					continue;
				}

				if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) {
					continue;
				}

				$attribute_key   = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() );
				$attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : '';

				if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) {
					// If dealing with a taxonomy, we need to get the slug from the name posted to the API.
					$term = get_term_by( 'name', $attribute_value, $attribute_name );

					if ( $term && ! is_wp_error( $term ) ) {
						$attribute_value = $term->slug;
					} else {
						$attribute_value = sanitize_title( $attribute_value );
					}
				}

				$attributes[ $attribute_key ] = $attribute_value;
			}

			$variation->set_attributes( $attributes );
		}

		// Menu order.
		if ( $request['menu_order'] ) {
			$variation->set_menu_order( $request['menu_order'] );
		}

		// Meta data.
		if ( is_array( $request['meta_data'] ) ) {
			foreach ( $request['meta_data'] as $meta ) {
				$variation->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
			}
		}

		/**
		 * Filters an object before it is inserted via the REST API.
		 *
		 * The dynamic portion of the hook name, `$this->post_type`,
		 * refers to the object type slug.
		 *
		 * @param WC_Data         $variation Object object.
		 * @param WP_REST_Request $request   Request object.
		 * @param bool            $creating  If is creating a new object.
		 */
		return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $variation, $request, $creating );
	}

	/**
	 * Get the image for a product variation.
	 *
	 * @param WC_Product_Variation $variation Variation data.
	 * @return array
	 */
	protected function get_image( $variation ) {
		if ( ! $variation->get_image_id() ) {
			return;
		}

		$attachment_id   = $variation->get_image_id();
		$attachment_post = get_post( $attachment_id );
		if ( is_null( $attachment_post ) ) {
			return;
		}

		$attachment = wp_get_attachment_image_src( $attachment_id, 'full' );
		if ( ! is_array( $attachment ) ) {
			return;
		}

		if ( ! isset( $image ) ) {
			return array(
				'id'                => (int) $attachment_id,
				'date_created'      => wc_rest_prepare_date_response( $attachment_post->post_date, false ),
				'date_created_gmt'  => wc_rest_prepare_date_response( strtotime( $attachment_post->post_date_gmt ) ),
				'date_modified'     => wc_rest_prepare_date_response( $attachment_post->post_modified, false ),
				'date_modified_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_modified_gmt ) ),
				'src'               => current( $attachment ),
				'name'              => get_the_title( $attachment_id ),
				'alt'               => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ),
			);
		}
	}

	/**
	 * Set variation image.
	 *
	 * @throws WC_REST_Exception REST API exceptions.
	 * @param  WC_Product_Variation $variation Variation instance.
	 * @param  array                $image    Image data.
	 * @return WC_Product_Variation
	 */
	protected function set_variation_image( $variation, $image ) {
		$attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0;

		if ( 0 === $attachment_id ) {
			if ( isset( $image['src'] ) ) {
				$upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) );

				if ( is_wp_error( $upload ) ) {
					if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $variation->get_id(), array( $image ) ) ) {
						throw new WC_REST_Exception( 'woocommerce_variation_image_upload_error', $upload->get_error_message(), 400 );
					}
				}

				$attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $variation->get_id() );
			} else {
				$variation->set_image_id( '' );
				return $variation;
			}
		}

		if ( ! wp_attachment_is_image( $attachment_id ) ) {
			/* translators: %s: attachment ID */
			throw new WC_REST_Exception( 'woocommerce_variation_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 );
		}

		$variation->set_image_id( $attachment_id );

		// Set the image alt if present.
		if ( ! empty( $image['alt'] ) ) {
			update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) );
		}

		// Set the image name if present.
		if ( ! empty( $image['name'] ) ) {
			wp_update_post(
				array(
					'ID'         => $attachment_id,
					'post_title' => $image['name'],
				)
			);
		}

		return $variation;
	}

	/**
	 * Get the Variation's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$weight_unit    = get_option( 'woocommerce_weight_unit' );
		$dimension_unit = get_option( 'woocommerce_dimension_unit' );
		$schema         = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => $this->post_type,
			'type'       => 'object',
			'properties' => array(
				'id'                    => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_created'          => array(
					'description' => __( "The date the variation was created, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_modified'         => array(
					'description' => __( "The date the variation was last modified, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'description'           => array(
					'description' => __( 'Variation description.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'permalink'             => array(
					'description' => __( 'Variation URL.', 'woocommerce' ),
					'type'        => 'string',
					'format'      => 'uri',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'sku'                   => array(
					'description' => __( 'Unique identifier.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'price'                 => array(
					'description' => __( 'Current variation price.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'regular_price'         => array(
					'description' => __( 'Variation regular price.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'sale_price'            => array(
					'description' => __( 'Variation sale price.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'date_on_sale_from'     => array(
					'description' => __( "Start date of sale price, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'date_on_sale_from_gmt' => array(
					'description' => __( 'Start date of sale price, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'date_on_sale_to'       => array(
					'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'date_on_sale_to_gmt'   => array(
					'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'on_sale'               => array(
					'description' => __( 'Shows if the variation is on sale.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'status'                => array(
					'description' => __( 'Variation status.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'publish',
					'enum'        => array_keys( get_post_statuses() ),
					'context'     => array( 'view', 'edit' ),
				),
				'purchasable'           => array(
					'description' => __( 'Shows if the variation can be bought.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'virtual'               => array(
					'description' => __( 'If the variation is virtual.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'downloadable'          => array(
					'description' => __( 'If the variation is downloadable.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'downloads'             => array(
					'description' => __( 'List of downloadable files.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'   => array(
								'description' => __( 'File ID.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'name' => array(
								'description' => __( 'File name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'file' => array(
								'description' => __( 'File URL.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
						),
					),
				),
				'download_limit'        => array(
					'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ),
					'type'        => 'integer',
					'default'     => -1,
					'context'     => array( 'view', 'edit' ),
				),
				'download_expiry'       => array(
					'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ),
					'type'        => 'integer',
					'default'     => -1,
					'context'     => array( 'view', 'edit' ),
				),
				'tax_status'            => array(
					'description' => __( 'Tax status.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'taxable',
					'enum'        => array( 'taxable', 'shipping', 'none' ),
					'context'     => array( 'view', 'edit' ),
				),
				'tax_class'             => array(
					'description' => __( 'Tax class.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'manage_stock'          => array(
					'description' => __( 'Stock management at variation level.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'stock_quantity'        => array(
					'description' => __( 'Stock quantity.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'stock_status'          => array(
					'description' => __( 'Controls the stock status of the product.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'instock',
					'enum'        => array_keys( wc_get_product_stock_status_options() ),
					'context'     => array( 'view', 'edit' ),
				),
				'backorders'            => array(
					'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'no',
					'enum'        => array( 'no', 'notify', 'yes' ),
					'context'     => array( 'view', 'edit' ),
				),
				'backorders_allowed'    => array(
					'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'backordered'           => array(
					'description' => __( 'Shows if the variation is on backordered.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'low_stock_amount'       => array(
					'description' => __( 'Low Stock amount for the variation.', 'woocommerce' ),
					'type'        => array( 'integer', 'null' ),
					'context'     => array( 'view', 'edit' ),
				),
				'weight'                => array(
					/* translators: %s: weight unit */
					'description' => sprintf( __( 'Variation weight (%s).', 'woocommerce' ), $weight_unit ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'dimensions'            => array(
					'description' => __( 'Variation dimensions.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'properties'  => array(
						'length' => array(
							/* translators: %s: dimension unit */
							'description' => sprintf( __( 'Variation length (%s).', 'woocommerce' ), $dimension_unit ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'width'  => array(
							/* translators: %s: dimension unit */
							'description' => sprintf( __( 'Variation width (%s).', 'woocommerce' ), $dimension_unit ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'height' => array(
							/* translators: %s: dimension unit */
							'description' => sprintf( __( 'Variation height (%s).', 'woocommerce' ), $dimension_unit ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
					),
				),
				'shipping_class'        => array(
					'description' => __( 'Shipping class slug.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'shipping_class_id'     => array(
					'description' => __( 'Shipping class ID.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'image'                 => array(
					'description' => __( 'Variation image data.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'properties'  => array(
						'id'                => array(
							'description' => __( 'Image ID.', 'woocommerce' ),
							'type'        => 'integer',
							'context'     => array( 'view', 'edit' ),
						),
						'date_created'      => array(
							'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ),
							'type'        => 'date-time',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'date_created_gmt'  => array(
							'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ),
							'type'        => 'date-time',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'date_modified'     => array(
							'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ),
							'type'        => 'date-time',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'date_modified_gmt' => array(
							'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ),
							'type'        => 'date-time',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'src'               => array(
							'description' => __( 'Image URL.', 'woocommerce' ),
							'type'        => 'string',
							'format'      => 'uri',
							'context'     => array( 'view', 'edit' ),
						),
						'name'              => array(
							'description' => __( 'Image name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'alt'               => array(
							'description' => __( 'Image alternative text.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
					),
				),
				'attributes'            => array(
					'description' => __( 'List of attributes.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'     => array(
								'description' => __( 'Attribute ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'name'   => array(
								'description' => __( 'Attribute name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'option' => array(
								'description' => __( 'Selected attribute term name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
						),
					),
				),
				'menu_order'            => array(
					'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'meta_data'             => array(
					'description' => __( 'Meta data.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'    => array(
								'description' => __( 'Meta ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'key'   => array(
								'description' => __( 'Meta key.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'value' => array(
								'description' => __( 'Meta value.', 'woocommerce' ),
								'type'        => 'mixed',
								'context'     => array( 'view', 'edit' ),
							),
						),
					),
				),
			),
		);
		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Prepare objects query.
	 *
	 * @since  3.0.0
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return array
	 */
	protected function prepare_objects_query( $request ) {
		$args = WC_REST_CRUD_Controller::prepare_objects_query( $request );

		// Set post_status.
		$args['post_status'] = $request['status'];

		// Filter by sku.
		if ( ! empty( $request['sku'] ) ) {
			$skus = explode( ',', $request['sku'] );
			// Include the current string as a SKU too.
			if ( 1 < count( $skus ) ) {
				$skus[] = $request['sku'];
			}

			$args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok.
				$args,
				array(
					'key'     => '_sku',
					'value'   => $skus,
					'compare' => 'IN',
				)
			);
		}

		// Filter by tax class.
		if ( ! empty( $request['tax_class'] ) ) {
			$args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok.
				$args,
				array(
					'key'   => '_tax_class',
					'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '',
				)
			);
		}

		// Price filter.
		if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) {
			$args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) );  // WPCS: slow query ok.
		}

		// Filter product based on stock_status.
		if ( ! empty( $request['stock_status'] ) ) {
			$args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok.
				$args,
				array(
					'key'   => '_stock_status',
					'value' => $request['stock_status'],
				)
			);
		}

		// Filter by on sale products.
		if ( is_bool( $request['on_sale'] ) ) {
			$on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in';
			$on_sale_ids = wc_get_product_ids_on_sale();

			// Use 0 when there's no on sale products to avoid return all products.
			$on_sale_ids = empty( $on_sale_ids ) ? array( 0 ) : $on_sale_ids;

			$args[ $on_sale_key ] += $on_sale_ids;
		}

		// Force the post_type argument, since it's not a user input variable.
		if ( ! empty( $request['sku'] ) ) {
			$args['post_type'] = array( 'product', 'product_variation' );
		} else {
			$args['post_type'] = $this->post_type;
		}

		$args['post_parent'] = $request['product_id'];

		return $args;
	}

	/**
	 * Get the query params for collections of attachments.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params = parent::get_collection_params();

		unset(
			$params['in_stock'],
			$params['type'],
			$params['featured'],
			$params['category'],
			$params['tag'],
			$params['shipping_class'],
			$params['attribute'],
			$params['attribute_term']
		);

		$params['stock_status'] = array(
			'description'       => __( 'Limit result set to products with specified stock status.', 'woocommerce' ),
			'type'              => 'string',
			'enum'              => array_keys( wc_get_product_stock_status_options() ),
			'sanitize_callback' => 'sanitize_text_field',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}
}
rest-api/Controllers/Version3/class-wc-rest-controller.php000064400000040450151336044760017703 0ustar00<?php
/**
 * REST Controller
 *
 * This class extend `WP_REST_Controller` in order to include /batch endpoint
 * for almost all endpoints in WooCommerce REST API.
 *
 * It's required to follow "Controller Classes" guide before extending this class:
 * <https://developer.wordpress.org/rest-api/extending-the-rest-api/controller-classes/>
 *
 * NOTE THAT ONLY CODE RELEVANT FOR MOST ENDPOINTS SHOULD BE INCLUDED INTO THIS CLASS.
 * If necessary extend this class and create new abstract classes like `WC_REST_CRUD_Controller` or `WC_REST_Terms_Controller`.
 *
 * @class   WC_REST_Controller
 * @package WooCommerce\RestApi
 * @see     https://developer.wordpress.org/rest-api/extending-the-rest-api/controller-classes/
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Abstract Rest Controller Class
 *
 * @package WooCommerce\RestApi
 * @extends  WP_REST_Controller
 * @version  2.6.0
 */
abstract class WC_REST_Controller extends WP_REST_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v1';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = '';

	/**
	 * Used to cache computed return fields.
	 *
	 * @var null|array
	 */
	private $_fields = null;

	/**
	 * Used to verify if cached fields are for correct request object.
	 *
	 * @var null|WP_REST_Request
	 */
	private $_request = null;

	/**
	 * Add the schema from additional fields to an schema array.
	 *
	 * The type of object is inferred from the passed schema.
	 *
	 * @param array $schema Schema array.
	 *
	 * @return array
	 */
	protected function add_additional_fields_schema( $schema ) {
		if ( empty( $schema['title'] ) ) {
			return $schema;
		}

		/**
		 * Can't use $this->get_object_type otherwise we cause an inf loop.
		 */
		$object_type = $schema['title'];

		$additional_fields = $this->get_additional_fields( $object_type );

		foreach ( $additional_fields as $field_name => $field_options ) {
			if ( ! $field_options['schema'] ) {
				continue;
			}

			$schema['properties'][ $field_name ] = $field_options['schema'];
		}

		$schema['properties'] = apply_filters( 'woocommerce_rest_' . $object_type . '_schema', $schema['properties'] );

		return $schema;
	}

	/**
	 * Compatibility functions for WP 5.5, since custom types are not supported anymore.
	 * See @link https://core.trac.wordpress.org/changeset/48306
	 *
	 * @param string $method Optional. HTTP method of the request.
	 *
	 * @return array Endpoint arguments.
	 */
	public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) {

		$endpoint_args = parent::get_endpoint_args_for_item_schema( $method );

		if ( false === strpos( WP_REST_Server::EDITABLE, $method ) ) {
			return $endpoint_args;
		}

		$endpoint_args = $this->adjust_wp_5_5_datatype_compatibility( $endpoint_args );

		return $endpoint_args;
	}

	/**
	 * Change datatypes `date-time` to string, and `mixed` to composite of all built in types. This is required for maintaining forward compatibility with WP 5.5 since custom post types are not supported anymore.
	 *
	 * See @link https://core.trac.wordpress.org/changeset/48306
	 *
	 * We still use the 'mixed' type, since if we convert to composite type everywhere, it won't work in 5.4 anymore because they require to define the full schema.
	 *
	 * @param array $endpoint_args Schema with datatypes to convert.

	 * @return mixed Schema with converted datatype.
	 */
	protected function adjust_wp_5_5_datatype_compatibility( $endpoint_args ) {
		if ( version_compare( get_bloginfo( 'version' ), '5.5', '<' ) ) {
			return $endpoint_args;
		}

		foreach ( $endpoint_args as $field_id => $params ) {

			if ( ! isset( $params['type'] ) ) {
				continue;
			}

			/**
			 * Custom types are not supported as of WP 5.5, this translates type => 'date-time' to type => 'string'.
			 */
			if ( 'date-time' === $params['type'] ) {
				$params['type'] = array( 'null', 'string' );
			}

			/**
			 * WARNING: Order of fields here is important, types of fields are ordered from most specific to least specific as perceived by core's built-in type validation methods.
			 */
			if ( 'mixed' === $params['type'] ) {
				$params['type'] = array( 'null', 'object', 'string', 'number', 'boolean', 'integer', 'array' );
			}

			if ( isset( $params['properties'] ) ) {
				$params['properties'] = $this->adjust_wp_5_5_datatype_compatibility( $params['properties'] );
			}

			if ( isset( $params['items'] ) && isset( $params['items']['properties'] ) ) {
				$params['items']['properties'] = $this->adjust_wp_5_5_datatype_compatibility( $params['items']['properties'] );
			}

			$endpoint_args[ $field_id ] = $params;
		}
		return $endpoint_args;
	}

	/**
	 * Get normalized rest base.
	 *
	 * @return string
	 */
	protected function get_normalized_rest_base() {
		return preg_replace( '/\(.*\)\//i', '', $this->rest_base );
	}

	/**
	 * Check batch limit.
	 *
	 * @param array $items Request items.
	 * @return bool|WP_Error
	 */
	protected function check_batch_limit( $items ) {
		$limit = apply_filters( 'woocommerce_rest_batch_items_limit', 100, $this->get_normalized_rest_base() );
		$total = 0;

		if ( ! empty( $items['create'] ) ) {
			$total += count( $items['create'] );
		}

		if ( ! empty( $items['update'] ) ) {
			$total += count( $items['update'] );
		}

		if ( ! empty( $items['delete'] ) ) {
			$total += count( $items['delete'] );
		}

		if ( $total > $limit ) {
			/* translators: %s: items limit */
			return new WP_Error( 'woocommerce_rest_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), array( 'status' => 413 ) );
		}

		return true;
	}

	/**
	 * Bulk create, update and delete items.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return array Of WP_Error or WP_REST_Response.
	 */
	public function batch_items( $request ) {
		/**
		 * REST Server
		 *
		 * @var WP_REST_Server $wp_rest_server
		 */
		global $wp_rest_server;

		// Get the request params.
		$items    = array_filter( $request->get_params() );
		$query    = $request->get_query_params();
		$response = array();

		// Check batch limit.
		$limit = $this->check_batch_limit( $items );
		if ( is_wp_error( $limit ) ) {
			return $limit;
		}

		if ( ! empty( $items['create'] ) ) {
			foreach ( $items['create'] as $item ) {
				$_item = new WP_REST_Request( 'POST' );

				// Default parameters.
				$defaults = array();
				$schema   = $this->get_public_item_schema();
				foreach ( $schema['properties'] as $arg => $options ) {
					if ( isset( $options['default'] ) ) {
						$defaults[ $arg ] = $options['default'];
					}
				}
				$_item->set_default_params( $defaults );

				// Set request parameters.
				$_item->set_body_params( $item );

				// Set query (GET) parameters.
				$_item->set_query_params( $query );

				$_response = $this->create_item( $_item );

				if ( is_wp_error( $_response ) ) {
					$response['create'][] = array(
						'id'    => 0,
						'error' => array(
							'code'    => $_response->get_error_code(),
							'message' => $_response->get_error_message(),
							'data'    => $_response->get_error_data(),
						),
					);
				} else {
					$response['create'][] = $wp_rest_server->response_to_data( $_response, '' );
				}
			}
		}

		if ( ! empty( $items['update'] ) ) {
			foreach ( $items['update'] as $item ) {
				$_item = new WP_REST_Request( 'PUT' );
				$_item->set_body_params( $item );
				$_response = $this->update_item( $_item );

				if ( is_wp_error( $_response ) ) {
					$response['update'][] = array(
						'id'    => $item['id'],
						'error' => array(
							'code'    => $_response->get_error_code(),
							'message' => $_response->get_error_message(),
							'data'    => $_response->get_error_data(),
						),
					);
				} else {
					$response['update'][] = $wp_rest_server->response_to_data( $_response, '' );
				}
			}
		}

		if ( ! empty( $items['delete'] ) ) {
			foreach ( $items['delete'] as $id ) {
				$id = (int) $id;

				if ( 0 === $id ) {
					continue;
				}

				$_item = new WP_REST_Request( 'DELETE' );
				$_item->set_query_params(
					array(
						'id'    => $id,
						'force' => true,
					)
				);
				$_response = $this->delete_item( $_item );

				if ( is_wp_error( $_response ) ) {
					$response['delete'][] = array(
						'id'    => $id,
						'error' => array(
							'code'    => $_response->get_error_code(),
							'message' => $_response->get_error_message(),
							'data'    => $_response->get_error_data(),
						),
					);
				} else {
					$response['delete'][] = $wp_rest_server->response_to_data( $_response, '' );
				}
			}
		}

		return $response;
	}

	/**
	 * Validate a text value for a text based setting.
	 *
	 * @since 3.0.0
	 * @param string $value Value.
	 * @param array  $setting Setting.
	 * @return string
	 */
	public function validate_setting_text_field( $value, $setting ) {
		$value = is_null( $value ) ? '' : $value;
		return wp_kses_post( trim( stripslashes( $value ) ) );
	}

	/**
	 * Validate select based settings.
	 *
	 * @since 3.0.0
	 * @param string $value Value.
	 * @param array  $setting Setting.
	 * @return string|WP_Error
	 */
	public function validate_setting_select_field( $value, $setting ) {
		if ( array_key_exists( $value, $setting['options'] ) ) {
			return $value;
		} else {
			return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) );
		}
	}

	/**
	 * Validate multiselect based settings.
	 *
	 * @since 3.0.0
	 * @param array $values Values.
	 * @param array $setting Setting.
	 * @return array|WP_Error
	 */
	public function validate_setting_multiselect_field( $values, $setting ) {
		if ( empty( $values ) ) {
			return array();
		}

		if ( ! is_array( $values ) ) {
			return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) );
		}

		$final_values = array();
		foreach ( $values as $value ) {
			if ( array_key_exists( $value, $setting['options'] ) ) {
				$final_values[] = $value;
			}
		}

		return $final_values;
	}

	/**
	 * Validate image_width based settings.
	 *
	 * @since 3.0.0
	 * @param array $values Values.
	 * @param array $setting Setting.
	 * @return string|WP_Error
	 */
	public function validate_setting_image_width_field( $values, $setting ) {
		if ( ! is_array( $values ) ) {
			return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) );
		}

		$current = $setting['value'];
		if ( isset( $values['width'] ) ) {
			$current['width'] = intval( $values['width'] );
		}
		if ( isset( $values['height'] ) ) {
			$current['height'] = intval( $values['height'] );
		}
		if ( isset( $values['crop'] ) ) {
			$current['crop'] = (bool) $values['crop'];
		}
		return $current;
	}

	/**
	 * Validate radio based settings.
	 *
	 * @since 3.0.0
	 * @param string $value Value.
	 * @param array  $setting Setting.
	 * @return string|WP_Error
	 */
	public function validate_setting_radio_field( $value, $setting ) {
		return $this->validate_setting_select_field( $value, $setting );
	}

	/**
	 * Validate checkbox based settings.
	 *
	 * @since 3.0.0
	 * @param string $value Value.
	 * @param array  $setting Setting.
	 * @return string|WP_Error
	 */
	public function validate_setting_checkbox_field( $value, $setting ) {
		if ( in_array( $value, array( 'yes', 'no' ) ) ) {
			return $value;
		} elseif ( empty( $value ) ) {
			$value = isset( $setting['default'] ) ? $setting['default'] : 'no';
			return $value;
		} else {
			return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) );
		}
	}

	/**
	 * Validate textarea based settings.
	 *
	 * @since 3.0.0
	 * @param string $value Value.
	 * @param array  $setting Setting.
	 * @return string
	 */
	public function validate_setting_textarea_field( $value, $setting ) {
		$value = is_null( $value ) ? '' : $value;
		return wp_kses(
			trim( stripslashes( $value ) ),
			array_merge(
				array(
					'iframe' => array(
						'src'   => true,
						'style' => true,
						'id'    => true,
						'class' => true,
					),
				),
				wp_kses_allowed_html( 'post' )
			)
		);
	}

	/**
	 * Add meta query.
	 *
	 * @since 3.0.0
	 * @param array $args       Query args.
	 * @param array $meta_query Meta query.
	 * @return array
	 */
	protected function add_meta_query( $args, $meta_query ) {
		if ( empty( $args['meta_query'] ) ) {
			$args['meta_query'] = array();
		}

		$args['meta_query'][] = $meta_query;

		return $args['meta_query'];
	}

	/**
	 * Get the batch schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_public_batch_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'batch',
			'type'       => 'object',
			'properties' => array(
				'create' => array(
					'description' => __( 'List of created resources.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'    => 'object',
					),
				),
				'update' => array(
					'description' => __( 'List of updated resources.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'    => 'object',
					),
				),
				'delete' => array(
					'description' => __( 'List of delete resources.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'    => 'integer',
					),
				),
			),
		);

		return $schema;
	}

	/**
	 * Gets an array of fields to be included on the response.
	 *
	 * Included fields are based on item schema and `_fields=` request argument.
	 * Updated from WordPress 5.3, included into this class to support old versions.
	 *
	 * @since 3.5.0
	 * @param WP_REST_Request $request Full details about the request.
	 * @return array Fields to be included in the response.
	 */
	public function get_fields_for_response( $request ) {
		// From xdebug profiling, this method could take upto 25% of request time in index calls.
		// Cache it and make sure _fields was cached on current request object!
		// TODO: Submit this caching behavior in core.
		if ( isset( $this->_fields ) && is_array( $this->_fields ) && $request === $this->_request ) {
			return $this->_fields;
		}
		$this->_request = $request;

		$schema     = $this->get_item_schema();
		$properties = isset( $schema['properties'] ) ? $schema['properties'] : array();

		$additional_fields = $this->get_additional_fields();

		foreach ( $additional_fields as $field_name => $field_options ) {
			// For back-compat, include any field with an empty schema
			// because it won't be present in $this->get_item_schema().
			if ( is_null( $field_options['schema'] ) ) {
				$properties[ $field_name ] = $field_options;
			}
		}

		// Exclude fields that specify a different context than the request context.
		$context = $request['context'];
		if ( $context ) {
			foreach ( $properties as $name => $options ) {
				if ( ! empty( $options['context'] ) && ! in_array( $context, $options['context'], true ) ) {
					unset( $properties[ $name ] );
				}
			}
		}

		$fields = array_keys( $properties );

		if ( ! isset( $request['_fields'] ) ) {
			$this->_fields = $fields;
			return $fields;
		}
		$requested_fields = wp_parse_list( $request['_fields'] );
		if ( 0 === count( $requested_fields ) ) {
			$this->_fields = $fields;
			return $fields;
		}
		// Trim off outside whitespace from the comma delimited list.
		$requested_fields = array_map( 'trim', $requested_fields );
		// Always persist 'id', because it can be needed for add_additional_fields_to_object().
		if ( in_array( 'id', $fields, true ) ) {
			$requested_fields[] = 'id';
		}
		// Return the list of all requested fields which appear in the schema.
		$this->_fields = array_reduce(
			$requested_fields,
			function( $response_fields, $field ) use ( $fields ) {
				if ( in_array( $field, $fields, true ) ) {
					$response_fields[] = $field;
					return $response_fields;
				}
				// Check for nested fields if $field is not a direct match.
				$nested_fields = explode( '.', $field );
				// A nested field is included so long as its top-level property
				// is present in the schema.
				if ( in_array( $nested_fields[0], $fields, true ) ) {
					$response_fields[] = $field;
				}
				return $response_fields;
			},
			array()
		);
		return $this->_fields;
	}
}
rest-api/Controllers/Version3/class-wc-rest-reports-controller.php000064400000003152151336044760021375 0ustar00<?php
/**
 * REST API Reports controller
 *
 * Handles requests to the reports endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Reports controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Reports_V2_Controller
 */
class WC_REST_Reports_Controller extends WC_REST_Reports_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';

	/**
	 * Get reports list.
	 *
	 * @since 3.5.0
	 * @return array
	 */
	protected function get_reports() {
		$reports = parent::get_reports();

		$reports[] = array(
			'slug'        => 'orders/totals',
			'description' => __( 'Orders totals.', 'woocommerce' ),
		);
		$reports[] = array(
			'slug'        => 'products/totals',
			'description' => __( 'Products totals.', 'woocommerce' ),
		);
		$reports[] = array(
			'slug'        => 'customers/totals',
			'description' => __( 'Customers totals.', 'woocommerce' ),
		);
		$reports[] = array(
			'slug'        => 'coupons/totals',
			'description' => __( 'Coupons totals.', 'woocommerce' ),
		);
		$reports[] = array(
			'slug'        => 'reviews/totals',
			'description' => __( 'Reviews totals.', 'woocommerce' ),
		);
		$reports[] = array(
			'slug'        => 'categories/totals',
			'description' => __( 'Categories totals.', 'woocommerce' ),
		);
		$reports[] = array(
			'slug'        => 'tags/totals',
			'description' => __( 'Tags totals.', 'woocommerce' ),
		);
		$reports[] = array(
			'slug'        => 'attributes/totals',
			'description' => __( 'Attributes totals.', 'woocommerce' ),
		);

		return $reports;
	}
}
rest-api/Controllers/Version3/class-wc-rest-webhooks-controller.php000064400000001172151336044760021520 0ustar00<?php
/**
 * REST API Webhooks controller
 *
 * Handles requests to the /webhooks endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Webhooks controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Webhooks_V2_Controller
 */
class WC_REST_Webhooks_Controller extends WC_REST_Webhooks_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';

	/**
	 * Get the default REST API version.
	 *
	 * @since  3.0.0
	 * @return string
	 */
	protected function get_default_api_version() {
		return 'wp_api_v3';
	}
}
rest-api/Controllers/Version3/class-wc-rest-coupons-controller.php000064400000000721151336044760021364 0ustar00<?php
/**
 * REST API Coupons controller
 *
 * Handles requests to the /coupons endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Coupons controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Coupons_V2_Controller
 */
class WC_REST_Coupons_Controller extends WC_REST_Coupons_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';
}
rest-api/Controllers/Version3/class-wc-rest-customers-controller.php000064400000024746151336044760021737 0ustar00<?php
/**
 * REST API Customers controller
 *
 * Handles requests to the /customers endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Customers controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Customers_V2_Controller
 */
class WC_REST_Customers_Controller extends WC_REST_Customers_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';

	/**
	 * Get formatted item data.
	 *
	 * @param WC_Data $object WC_Data instance.
	 *
	 * @since  3.0.0
	 * @return array
	 */
	protected function get_formatted_item_data( $object ) {
		$data        = $object->get_data();
		$format_date = array( 'date_created', 'date_modified' );

		// Format date values.
		foreach ( $format_date as $key ) {
			// Date created is stored UTC, date modified is stored WP local time.
			$datetime              = 'date_created' === $key ? get_date_from_gmt( gmdate( 'Y-m-d H:i:s', $data[ $key ]->getTimestamp() ) ) : $data[ $key ];
			$data[ $key ]          = wc_rest_prepare_date_response( $datetime, false );
			$data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime );
		}

		return array(
			'id'                 => $object->get_id(),
			'date_created'       => $data['date_created'],
			'date_created_gmt'   => $data['date_created_gmt'],
			'date_modified'      => $data['date_modified'],
			'date_modified_gmt'  => $data['date_modified_gmt'],
			'email'              => $data['email'],
			'first_name'         => $data['first_name'],
			'last_name'          => $data['last_name'],
			'role'               => $data['role'],
			'username'           => $data['username'],
			'billing'            => $data['billing'],
			'shipping'           => $data['shipping'],
			'is_paying_customer' => $data['is_paying_customer'],
			'avatar_url'         => $object->get_avatar_url(),
			'meta_data'          => $data['meta_data'],
		);
	}

	/**
	 * Get the Customer's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'customer',
			'type'       => 'object',
			'properties' => array(
				'id'                 => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_created'       => array(
					'description' => __( "The date the customer was created, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_created_gmt'   => array(
					'description' => __( 'The date the customer was created, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_modified'      => array(
					'description' => __( "The date the customer was last modified, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_modified_gmt'  => array(
					'description' => __( 'The date the customer was last modified, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'email'              => array(
					'description' => __( 'The email address for the customer.', 'woocommerce' ),
					'type'        => 'string',
					'format'      => 'email',
					'context'     => array( 'view', 'edit' ),
				),
				'first_name'         => array(
					'description' => __( 'Customer first name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				'last_name'          => array(
					'description' => __( 'Customer last name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				'role'               => array(
					'description' => __( 'Customer role.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'username'           => array(
					'description' => __( 'Customer login name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_user',
					),
				),
				'password'           => array(
					'description' => __( 'Customer password.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'edit' ),
				),
				'billing'            => array(
					'description' => __( 'List of billing address data.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'properties'  => array(
						'first_name' => array(
							'description' => __( 'First name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'last_name'  => array(
							'description' => __( 'Last name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'company'    => array(
							'description' => __( 'Company name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'address_1'  => array(
							'description' => __( 'Address line 1', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'address_2'  => array(
							'description' => __( 'Address line 2', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'city'       => array(
							'description' => __( 'City name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'state'      => array(
							'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'postcode'   => array(
							'description' => __( 'Postal code.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'country'    => array(
							'description' => __( 'ISO code of the country.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'email'      => array(
							'description' => __( 'Email address.', 'woocommerce' ),
							'type'        => 'string',
							'format'      => 'email',
							'context'     => array( 'view', 'edit' ),
						),
						'phone'      => array(
							'description' => __( 'Phone number.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
					),
				),
				'shipping'           => array(
					'description' => __( 'List of shipping address data.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'properties'  => array(
						'first_name' => array(
							'description' => __( 'First name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'last_name'  => array(
							'description' => __( 'Last name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'company'    => array(
							'description' => __( 'Company name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'address_1'  => array(
							'description' => __( 'Address line 1', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'address_2'  => array(
							'description' => __( 'Address line 2', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'city'       => array(
							'description' => __( 'City name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'state'      => array(
							'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'postcode'   => array(
							'description' => __( 'Postal code.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'country'    => array(
							'description' => __( 'ISO code of the country.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'phone'      => array(
							'description' => __( 'Phone number.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
					),
				),
				'is_paying_customer' => array(
					'description' => __( 'Is the customer a paying customer?', 'woocommerce' ),
					'type'        => 'bool',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'avatar_url'         => array(
					'description' => __( 'Avatar URL.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'meta_data'          => array(
					'description' => __( 'Meta data.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'    => array(
								'description' => __( 'Meta ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'key'   => array(
								'description' => __( 'Meta key.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'value' => array(
								'description' => __( 'Meta value.', 'woocommerce' ),
								'type'        => 'mixed',
								'context'     => array( 'view', 'edit' ),
							),
						),
					),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version3/class-wc-rest-product-attributes-controller.php000064400000001023151336044760023536 0ustar00<?php
/**
 * REST API Product Attributes controller
 *
 * Handles requests to the products/attributes endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Product Attributes controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Product_Attributes_V2_Controller
 */
class WC_REST_Product_Attributes_Controller extends WC_REST_Product_Attributes_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';
}
rest-api/Controllers/Version3/class-wc-rest-report-customers-totals-controller.php000064400000007451151336044760024546 0ustar00<?php
/**
 * REST API Reports Customers Totals controller
 *
 * Handles requests to the /reports/customers/count endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   3.5.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Reports Customers Totals controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Reports_Controller
 */
class WC_REST_Report_Customers_Totals_Controller extends WC_REST_Reports_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'reports/customers/totals';

	/**
	 * Get reports list.
	 *
	 * @since 3.5.0
	 * @return array
	 */
	protected function get_reports() {
		$users_count     = count_users();
		$total_customers = 0;

		foreach ( $users_count['avail_roles'] as $role => $total ) {
			if ( in_array( $role, array( 'administrator', 'shop_manager' ), true ) ) {
				continue;
			}

			$total_customers += (int) $total;
		}

		$customers_query = new WP_User_Query(
			array(
				'role__not_in' => array( 'administrator', 'shop_manager' ),
				'number'       => 0,
				'fields'       => 'ID',
				'count_total'  => true,
				'meta_query'   => array( // WPCS: slow query ok.
					array(
						'key'     => 'paying_customer',
						'value'   => 1,
						'compare' => '=',
					),
				),
			)
		);

		$total_paying = (int) $customers_query->get_total();

		$data = array(
			array(
				'slug'  => 'paying',
				'name'  => __( 'Paying customer', 'woocommerce' ),
				'total' => $total_paying,
			),
			array(
				'slug'  => 'non_paying',
				'name'  => __( 'Non-paying customer', 'woocommerce' ),
				'total' => $total_customers - $total_paying,
			),
		);

		return $data;
	}

	/**
	 * Prepare a report object for serialization.
	 *
	 * @param  stdClass        $report Report data.
	 * @param  WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $report, $request ) {
		$data = array(
			'slug'  => $report->slug,
			'name'  => $report->name,
			'total' => $report->total,
		);

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		/**
		 * Filter a report returned from the API.
		 *
		 * Allows modification of the report data right before it is returned.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param object           $report   The original report object.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_report_customers_count', $response, $report, $request );
	}

	/**
	 * Get the Report's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'report_customer_total',
			'type'       => 'object',
			'properties' => array(
				'slug'  => array(
					'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'name'  => array(
					'description' => __( 'Customer type name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'total' => array(
					'description' => __( 'Amount of customers.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version3/class-wc-rest-product-attribute-terms-controller.php000064400000001101151336044760024500 0ustar00<?php
/**
 * REST API Product Attribute Terms controller
 *
 * Handles requests to the products/attributes/<attribute_id>/terms endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Product Attribute Terms controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Product_Attribute_Terms_V2_Controller
 */
class WC_REST_Product_Attribute_Terms_Controller extends WC_REST_Product_Attribute_Terms_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';
}
rest-api/Controllers/Version3/class-wc-rest-settings-controller.php000064400000006172151336044760021544 0ustar00<?php
/**
 * REST API Settings controller
 *
 * Handles requests to the /settings endpoints.
 *
 * @package WooCommerce\RestApi
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Settings controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Settings_V2_Controller
 */
class WC_REST_Settings_Controller extends WC_REST_Settings_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';

	/**
	 * Register routes.
	 */
	public function register_routes() {
		parent::register_routes();
		register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array(
			array(
				'methods'             => WP_REST_Server::EDITABLE,
				'callback'            => array( $this, 'batch_items' ),
				'permission_callback' => array( $this, 'update_items_permissions_check' ),
			),
			'schema' => array( $this, 'get_public_batch_schema' ),
		) );
	}

	/**
	 * Makes sure the current user has access to WRITE the settings APIs.
	 *
	 * @param WP_REST_Request $request Full data about the request.
	 * @return WP_Error|bool
	 */
	public function update_items_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Update a setting.
	 *
	 * @param  WP_REST_Request $request Request data.
	 * @return WP_Error|WP_REST_Response
	 */
	public function update_item( $request ) {
		$options_controller = new WC_REST_Setting_Options_Controller();
		$response           = $options_controller->update_item( $request );

		return $response;
	}

	/**
	 * Get the groups schema, conforming to JSON Schema.
	 *
	 * @since  3.0.0
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'setting_group',
			'type'       => 'object',
			'properties' => array(
				'id'          => array(
					'description' => __( 'A unique identifier that can be used to link settings together.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'label'       => array(
					'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'description' => array(
					'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'parent_id'   => array(
					'description' => __( 'ID of parent grouping.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'sub_groups'  => array(
					'description' => __( 'IDs for settings sub groups.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version3/class-wc-rest-data-continents-controller.php000064400000025527151336044760023004 0ustar00<?php
/**
 * REST API Data continents controller.
 *
 * Handles requests to the /data/continents endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   3.5.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Data continents controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Controller
 */
class WC_REST_Data_Continents_Controller extends WC_REST_Data_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'data/continents';

	/**
	 * Register routes.
	 *
	 * @since 3.5.0
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<location>[\w-]+)',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => array(
						'continent' => array(
							'description' => __( '2 character continent code.', 'woocommerce' ),
							'type'        => 'string',
						),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Return the list of countries and states for a given continent.
	 *
	 * @since  3.5.0
	 * @param  string          $continent_code Continent code.
	 * @param  WP_REST_Request $request        Request data.
	 * @return array|mixed Response data, ready for insertion into collection data.
	 */
	public function get_continent( $continent_code, $request ) {
		$continents  = WC()->countries->get_continents();
		$countries   = WC()->countries->get_countries();
		$states      = WC()->countries->get_states();
		$locale_info = include WC()->plugin_path() . '/i18n/locale-info.php';
		$data        = array();

		if ( ! array_key_exists( $continent_code, $continents ) ) {
			return false;
		}

		$continent_list = $continents[ $continent_code ];

		$continent = array(
			'code' => $continent_code,
			'name' => $continent_list['name'],
		);

		$local_countries = array();
		foreach ( $continent_list['countries'] as $country_code ) {
			if ( isset( $countries[ $country_code ] ) ) {
				$country = array(
					'code' => $country_code,
					'name' => $countries[ $country_code ],
				);

				// If we have detailed locale information include that in the response.
				if ( array_key_exists( $country_code, $locale_info ) ) {
					// Defensive programming against unexpected changes in locale-info.php.
					$country_data = wp_parse_args(
						$locale_info[ $country_code ],
						array(
							'currency_code'  => 'USD',
							'currency_pos'   => 'left',
							'decimal_sep'    => '.',
							'dimension_unit' => 'in',
							'num_decimals'   => 2,
							'thousand_sep'   => ',',
							'weight_unit'    => 'lbs',
						)
					);

					$country = array_merge( $country, $country_data );
				}

				$local_states = array();
				if ( isset( $states[ $country_code ] ) ) {
					foreach ( $states[ $country_code ] as $state_code => $state_name ) {
						$local_states[] = array(
							'code' => $state_code,
							'name' => $state_name,
						);
					}
				}
				$country['states'] = $local_states;

				// Allow only desired keys (e.g. filter out tax rates).
				$allowed = array(
					'code',
					'currency_code',
					'currency_pos',
					'decimal_sep',
					'dimension_unit',
					'name',
					'num_decimals',
					'states',
					'thousand_sep',
					'weight_unit',
				);
				$country = array_intersect_key( $country, array_flip( $allowed ) );

				$local_countries[] = $country;
			}
		}

		$continent['countries'] = $local_countries;
		return $continent;
	}

	/**
	 * Return the list of states for all continents.
	 *
	 * @since  3.5.0
	 * @param  WP_REST_Request $request Request data.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_items( $request ) {
		$continents = WC()->countries->get_continents();
		$data       = array();

		foreach ( array_keys( $continents ) as $continent_code ) {
			$continent = $this->get_continent( $continent_code, $request );
			$response  = $this->prepare_item_for_response( $continent, $request );
			$data[]    = $this->prepare_response_for_collection( $response );
		}

		return rest_ensure_response( $data );
	}

	/**
	 * Return the list of locations for a given continent.
	 *
	 * @since  3.5.0
	 * @param  WP_REST_Request $request Request data.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_item( $request ) {
		$data = $this->get_continent( strtoupper( $request['location'] ), $request );
		if ( empty( $data ) ) {
			return new WP_Error( 'woocommerce_rest_data_invalid_location', __( 'There are no locations matching these parameters.', 'woocommerce' ), array( 'status' => 404 ) );
		}
		return $this->prepare_item_for_response( $data, $request );
	}

	/**
	 * Prepare the data object for response.
	 *
	 * @since  3.5.0
	 * @param object          $item Data object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $item, $request ) {
		$data     = $this->add_additional_fields_to_object( $item, $request );
		$data     = $this->filter_response_by_context( $data, 'view' );
		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $item ) );

		/**
		 * Filter the location list returned from the API.
		 *
		 * Allows modification of the loction data right before it is returned.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param array            $item     The original list of continent(s), countries, and states.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_data_continent', $response, $item, $request );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param object $item Data object.
	 * @return array Links for the given continent.
	 */
	protected function prepare_links( $item ) {
		$continent_code = strtolower( $item['code'] );
		$links          = array(
			'self'       => array(
				'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $continent_code ) ),
			),
			'collection' => array(
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
			),
		);
		return $links;
	}

	/**
	 * Get the location schema, conforming to JSON Schema.
	 *
	 * @since  3.5.0
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'data_continents',
			'type'       => 'object',
			'properties' => array(
				'code'      => array(
					'type'        => 'string',
					'description' => __( '2 character continent code.', 'woocommerce' ),
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'name'      => array(
					'type'        => 'string',
					'description' => __( 'Full name of continent.', 'woocommerce' ),
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'countries' => array(
					'type'        => 'array',
					'description' => __( 'List of countries on this continent.', 'woocommerce' ),
					'context'     => array( 'view' ),
					'readonly'    => true,
					'items'       => array(
						'type'       => 'object',
						'context'    => array( 'view' ),
						'readonly'   => true,
						'properties' => array(
							'code'           => array(
								'type'        => 'string',
								'description' => __( 'ISO3166 alpha-2 country code.', 'woocommerce' ),
								'context'     => array( 'view' ),
								'readonly'    => true,
							),
							'currency_code'  => array(
								'type'        => 'string',
								'description' => __( 'Default ISO4127 alpha-3 currency code for the country.', 'woocommerce' ),
								'context'     => array( 'view' ),
								'readonly'    => true,
							),
							'currency_pos'   => array(
								'type'        => 'string',
								'description' => __( 'Currency symbol position for this country.', 'woocommerce' ),
								'context'     => array( 'view' ),
								'readonly'    => true,
							),
							'decimal_sep'    => array(
								'type'        => 'string',
								'description' => __( 'Decimal separator for displayed prices for this country.', 'woocommerce' ),
								'context'     => array( 'view' ),
								'readonly'    => true,
							),
							'dimension_unit' => array(
								'type'        => 'string',
								'description' => __( 'The unit lengths are defined in for this country.', 'woocommerce' ),
								'context'     => array( 'view' ),
								'readonly'    => true,
							),
							'name'           => array(
								'type'        => 'string',
								'description' => __( 'Full name of country.', 'woocommerce' ),
								'context'     => array( 'view' ),
								'readonly'    => true,
							),
							'num_decimals'   => array(
								'type'        => 'integer',
								'description' => __( 'Number of decimal points shown in displayed prices for this country.', 'woocommerce' ),
								'context'     => array( 'view' ),
								'readonly'    => true,
							),
							'states'         => array(
								'type'        => 'array',
								'description' => __( 'List of states in this country.', 'woocommerce' ),
								'context'     => array( 'view' ),
								'readonly'    => true,
								'items'       => array(
									'type'       => 'object',
									'context'    => array( 'view' ),
									'readonly'   => true,
									'properties' => array(
										'code' => array(
											'type'        => 'string',
											'description' => __( 'State code.', 'woocommerce' ),
											'context'     => array( 'view' ),
											'readonly'    => true,
										),
										'name' => array(
											'type'        => 'string',
											'description' => __( 'Full name of state.', 'woocommerce' ),
											'context'     => array( 'view' ),
											'readonly'    => true,
										),
									),
								),
							),
							'thousand_sep'   => array(
								'type'        => 'string',
								'description' => __( 'Thousands separator for displayed prices in this country.', 'woocommerce' ),
								'context'     => array( 'view' ),
								'readonly'    => true,
							),
							'weight_unit'    => array(
								'type'        => 'string',
								'description' => __( 'The unit weights are defined in for this country.', 'woocommerce' ),
								'context'     => array( 'view' ),
								'readonly'    => true,
							),
						),
					),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version3/class-wc-rest-taxes-controller.php000064400000007053151336044760021027 0ustar00<?php
/**
 * REST API Taxes controller
 *
 * Handles requests to the /taxes endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Taxes controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Taxes_V2_Controller
 */
class WC_REST_Taxes_Controller extends WC_REST_Taxes_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';

	/**
	 * Add tax rate locales to the response array.
	 *
	 * @param array    $data Response data.
	 * @param stdClass $tax  Tax object.
	 *
	 * @return array
	 */
	protected function add_tax_rate_locales( $data, $tax ) {
		global $wpdb;

		$data              = parent::add_tax_rate_locales( $data, $tax );
		$data['postcodes'] = array();
		$data['cities']    = array();

		// Get locales from a tax rate.
		$locales = $wpdb->get_results(
			$wpdb->prepare(
				"
				SELECT location_code, location_type
				FROM {$wpdb->prefix}woocommerce_tax_rate_locations
				WHERE tax_rate_id = %d
				",
				$tax->tax_rate_id
			)
		);

		if ( ! is_wp_error( $tax ) && ! is_null( $tax ) ) {
			foreach ( $locales as $locale ) {
				if ( 'postcode' === $locale->location_type ) {
					$data['postcodes'][] = $locale->location_code;
				} elseif ( 'city' === $locale->location_type ) {
					$data['cities'][] = $locale->location_code;
				}
			}
		}

		return $data;
	}

	/**
	 * Get the taxes schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = parent::get_item_schema();

		$schema['properties']['postcodes'] = array(
			'description' => __( 'List of postcodes / ZIPs. Introduced in WooCommerce 5.3.', 'woocommerce' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'string',
			),
			'context'     => array( 'view', 'edit' ),
		);

		$schema['properties']['cities'] = array(
			'description' => __( 'List of city names. Introduced in WooCommerce 5.3.', 'woocommerce' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'string',
			),
			'context'     => array( 'view', 'edit' ),
		);

		$schema['properties']['postcode']['description'] =
			__( "Postcode/ZIP, it doesn't support multiple values. Deprecated as of WooCommerce 5.3, 'postcodes' should be used instead.", 'woocommerce' );

		$schema['properties']['city']['description'] =
			__( "City name, it doesn't support multiple values. Deprecated as of WooCommerce 5.3, 'cities' should be used instead.", 'woocommerce' );

		return $schema;
	}

	/**
	 * Create a single tax.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response The response, or an error.
	 */
	public function create_item( $request ) {
		$this->adjust_cities_and_postcodes( $request );

		return parent::create_item( $request );
	}

	/**
	 * Update a single tax.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response The response, or an error.
	 */
	public function update_item( $request ) {
		$this->adjust_cities_and_postcodes( $request );

		return parent::update_item( $request );
	}

	/**
	 * Convert array "cities" and "postcodes" parameters
	 * into semicolon-separated strings "city" and "postcode".
	 *
	 * @param WP_REST_Request $request The request to adjust.
	 */
	private function adjust_cities_and_postcodes( &$request ) {
		if ( isset( $request['cities'] ) ) {
			$request['city'] = join( ';', $request['cities'] );
		}
		if ( isset( $request['postcodes'] ) ) {
			$request['postcode'] = join( ';', $request['postcodes'] );
		}
	}
}
rest-api/Controllers/Version3/class-wc-rest-report-orders-totals-controller.php000064400000006160151336044760024014 0ustar00<?php
/**
 * REST API Reports Orders Totals controller
 *
 * Handles requests to the /reports/orders/count endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   3.5.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Reports Orders Totals controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Reports_Controller
 */
class WC_REST_Report_Orders_Totals_Controller extends WC_REST_Reports_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'reports/orders/totals';

	/**
	 * Get reports list.
	 *
	 * @since 3.5.0
	 * @return array
	 */
	protected function get_reports() {
		$totals = wp_count_posts( 'shop_order' );
		$data   = array();

		foreach ( wc_get_order_statuses() as $slug => $name ) {
			if ( ! isset( $totals->$slug ) ) {
				continue;
			}

			$data[] = array(
				'slug'  => str_replace( 'wc-', '', $slug ),
				'name'  => $name,
				'total' => (int) $totals->$slug,
			);
		}

		return $data;
	}

	/**
	 * Prepare a report object for serialization.
	 *
	 * @param  stdClass        $report Report data.
	 * @param  WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $report, $request ) {
		$data = array(
			'slug'  => $report->slug,
			'name'  => $report->name,
			'total' => $report->total,
		);

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		/**
		 * Filter a report returned from the API.
		 *
		 * Allows modification of the report data right before it is returned.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param object           $report   The original report object.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_report_orders_count', $response, $report, $request );
	}

	/**
	 * Get the Report's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'report_order_total',
			'type'       => 'object',
			'properties' => array(
				'slug'  => array(
					'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'name'  => array(
					'description' => __( 'Order status name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'total' => array(
					'description' => __( 'Amount of orders.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version3/class-wc-rest-product-categories-controller.php000064400000021612151336044760023503 0ustar00<?php
/**
 * REST API Product Categories controller
 *
 * Handles requests to the products/categories endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Product Categories controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Product_Categories_V2_Controller
 */
class WC_REST_Product_Categories_Controller extends WC_REST_Product_Categories_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';

	/**
	 * Prepare a single product category output for response.
	 *
	 * @param WP_Term         $item    Term object.
	 * @param WP_REST_Request $request Request instance.
	 * @return WP_REST_Response
	 */
	public function prepare_item_for_response( $item, $request ) {
		// Get category display type.
		$display_type = get_term_meta( $item->term_id, 'display_type', true );

		// Get category order.
		$menu_order = get_term_meta( $item->term_id, 'order', true );

		$data = array(
			'id'          => (int) $item->term_id,
			'name'        => $item->name,
			'slug'        => $item->slug,
			'parent'      => (int) $item->parent,
			'description' => $item->description,
			'display'     => $display_type ? $display_type : 'default',
			'image'       => null,
			'menu_order'  => (int) $menu_order,
			'count'       => (int) $item->count,
		);

		// Get category image.
		$image_id = get_term_meta( $item->term_id, 'thumbnail_id', true );
		if ( $image_id ) {
			$attachment = get_post( $image_id );

			$data['image'] = array(
				'id'                => (int) $image_id,
				'date_created'      => wc_rest_prepare_date_response( $attachment->post_date ),
				'date_created_gmt'  => wc_rest_prepare_date_response( $attachment->post_date_gmt ),
				'date_modified'     => wc_rest_prepare_date_response( $attachment->post_modified ),
				'date_modified_gmt' => wc_rest_prepare_date_response( $attachment->post_modified_gmt ),
				'src'               => wp_get_attachment_url( $image_id ),
				'name'              => get_the_title( $attachment ),
				'alt'               => get_post_meta( $image_id, '_wp_attachment_image_alt', true ),
			);
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $item, $request ) );

		/**
		 * Filter a term item returned from the API.
		 *
		 * Allows modification of the term data right before it is returned.
		 *
		 * @param WP_REST_Response  $response  The response object.
		 * @param object            $item      The original term object.
		 * @param WP_REST_Request   $request   Request used to generate the response.
		 */
		return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request );
	}

	/**
	 * Get the Category schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => $this->taxonomy,
			'type'       => 'object',
			'properties' => array(
				'id'          => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'name'        => array(
					'description' => __( 'Category name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				'slug'        => array(
					'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_title',
					),
				),
				'parent'      => array(
					'description' => __( 'The ID for the parent of the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'description' => array(
					'description' => __( 'HTML description of the resource.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'wp_filter_post_kses',
					),
				),
				'display'     => array(
					'description' => __( 'Category archive display type.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'default',
					'enum'        => array( 'default', 'products', 'subcategories', 'both' ),
					'context'     => array( 'view', 'edit' ),
				),
				'image'       => array(
					'description' => __( 'Image data.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'properties'  => array(
						'id'                => array(
							'description' => __( 'Image ID.', 'woocommerce' ),
							'type'        => 'integer',
							'context'     => array( 'view', 'edit' ),
						),
						'date_created'      => array(
							'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ),
							'type'        => 'date-time',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'date_created_gmt'  => array(
							'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ),
							'type'        => 'date-time',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'date_modified'     => array(
							'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ),
							'type'        => 'date-time',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'date_modified_gmt' => array(
							'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ),
							'type'        => 'date-time',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'src'               => array(
							'description' => __( 'Image URL.', 'woocommerce' ),
							'type'        => 'string',
							'format'      => 'uri',
							'context'     => array( 'view', 'edit' ),
						),
						'name'              => array(
							'description' => __( 'Image name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'alt'               => array(
							'description' => __( 'Image alternative text.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
					),
				),
				'menu_order'  => array(
					'description' => __( 'Menu order, used to custom sort the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'count'       => array(
					'description' => __( 'Number of published products for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Update term meta fields.
	 *
	 * @param WP_Term         $term    Term object.
	 * @param WP_REST_Request $request Request instance.
	 * @return bool|WP_Error
	 *
	 * @since 3.5.5
	 */
	protected function update_term_meta_fields( $term, $request ) {
		$id = (int) $term->term_id;

		if ( isset( $request['display'] ) ) {
			update_term_meta( $id, 'display_type', 'default' === $request['display'] ? '' : $request['display'] );
		}

		if ( isset( $request['menu_order'] ) ) {
			update_term_meta( $id, 'order', $request['menu_order'] );
		}

		if ( isset( $request['image'] ) ) {
			if ( empty( $request['image']['id'] ) && ! empty( $request['image']['src'] ) ) {
				$upload = wc_rest_upload_image_from_url( esc_url_raw( $request['image']['src'] ) );

				if ( is_wp_error( $upload ) ) {
					return $upload;
				}

				$image_id = wc_rest_set_uploaded_image_as_attachment( $upload );
			} else {
				$image_id = isset( $request['image']['id'] ) ? absint( $request['image']['id'] ) : 0;
			}

			// Check if image_id is a valid image attachment before updating the term meta.
			if ( $image_id && wp_attachment_is_image( $image_id ) ) {
				update_term_meta( $id, 'thumbnail_id', $image_id );

				// Set the image alt.
				if ( ! empty( $request['image']['alt'] ) ) {
					update_post_meta( $image_id, '_wp_attachment_image_alt', wc_clean( $request['image']['alt'] ) );
				}

				// Set the image title.
				if ( ! empty( $request['image']['name'] ) ) {
					wp_update_post(
						array(
							'ID'         => $image_id,
							'post_title' => wc_clean( $request['image']['name'] ),
						)
					);
				}
			} else {
				delete_term_meta( $id, 'thumbnail_id' );
			}
		}

		return true;
	}
}
rest-api/Controllers/Version3/class-wc-rest-report-reviews-totals-controller.php000064400000006470151336044760024206 0ustar00<?php
/**
 * REST API Reports Reviews Totals controller
 *
 * Handles requests to the /reports/reviews/count endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   3.5.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Reports Reviews Totals controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Reports_Controller
 */
class WC_REST_Report_Reviews_Totals_Controller extends WC_REST_Reports_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'reports/reviews/totals';

	/**
	 * Get reports list.
	 *
	 * @since 3.5.0
	 * @return array
	 */
	protected function get_reports() {
		$data = array();

		$query_data = array(
			'count'      => true,
			'post_type'  => 'product',
			'meta_key'   => 'rating', // WPCS: slow query ok.
			'meta_value' => '', // WPCS: slow query ok.
		);

		for ( $i = 1; $i <= 5; $i++ ) {
			$query_data['meta_value'] = $i;

			$data[] = array(
				'slug'  => 'rated_' . $i . '_out_of_5',
				/* translators: %s: average rating */
				'name'  => sprintf( __( 'Rated %s out of 5', 'woocommerce' ), $i ),
				'total' => (int) get_comments( $query_data ),
			);
		}

		return $data;
	}

	/**
	 * Prepare a report object for serialization.
	 *
	 * @param  stdClass        $report Report data.
	 * @param  WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $report, $request ) {
		$data = array(
			'slug'  => $report->slug,
			'name'  => $report->name,
			'total' => $report->total,
		);

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		/**
		 * Filter a report returned from the API.
		 *
		 * Allows modification of the report data right before it is returned.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param object           $report   The original report object.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_report_reviews_count', $response, $report, $request );
	}

	/**
	 * Get the Report's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'report_review_total',
			'type'       => 'object',
			'properties' => array(
				'slug'  => array(
					'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'name'  => array(
					'description' => __( 'Review type name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'total' => array(
					'description' => __( 'Amount of reviews.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version3/class-wc-rest-orders-controller.php000064400000022015151336044760021174 0ustar00<?php
/**
 * REST API Orders controller
 *
 * Handles requests to the /orders endpoint.
 *
 * @package WooCommerce\RestApi
 * @since    2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Orders controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Orders_V2_Controller
 */
class WC_REST_Orders_Controller extends WC_REST_Orders_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';

	/**
	 * Calculate coupons.
	 *
	 * @throws WC_REST_Exception When fails to set any item.
	 * @param WP_REST_Request $request Request object.
	 * @param WC_Order        $order   Order data.
	 * @return bool
	 */
	protected function calculate_coupons( $request, $order ) {
		if ( ! isset( $request['coupon_lines'] ) ) {
			return false;
		}

		// Validate input and at the same time store the processed coupon codes to apply.

		$coupon_codes = array();
		$discounts    = new WC_Discounts( $order );

		$current_order_coupons      = array_values( $order->get_coupons() );
		$current_order_coupon_codes = array_map(
			function( $coupon ) {
				return $coupon->get_code();
			},
			$current_order_coupons
		);

		foreach ( $request['coupon_lines'] as $item ) {
			if ( ! empty( $item['id'] ) ) {
				throw new WC_REST_Exception( 'woocommerce_rest_coupon_item_id_readonly', __( 'Coupon item ID is readonly.', 'woocommerce' ), 400 );
			}

			if ( empty( $item['code'] ) ) {
				throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 );
			}

			$coupon_code = wc_format_coupon_code( wc_clean( $item['code'] ) );
			$coupon      = new WC_Coupon( $coupon_code );

			// Skip check if the coupon is already applied to the order, as this could wrongly throw an error for single-use coupons.
			if ( ! in_array( $coupon_code, $current_order_coupon_codes, true ) ) {
				$check_result = $discounts->is_coupon_valid( $coupon );
				if ( is_wp_error( $check_result ) ) {
					throw new WC_REST_Exception( 'woocommerce_rest_' . $check_result->get_error_code(), $check_result->get_error_message(), 400 );
				}
			}

			$coupon_codes[] = $coupon_code;
		}

		// Remove all coupons first to ensure calculation is correct.
		foreach ( $order->get_items( 'coupon' ) as $existing_coupon ) {
			$order->remove_coupon( $existing_coupon->get_code() );
		}

		// Apply the coupons.
		foreach ( $coupon_codes as $new_coupon ) {
			$results = $order->apply_coupon( $new_coupon );

			if ( is_wp_error( $results ) ) {
				throw new WC_REST_Exception( 'woocommerce_rest_' . $results->get_error_code(), $results->get_error_message(), 400 );
			}
		}

		return true;
	}

	/**
	 * Prepare a single order for create or update.
	 *
	 * @throws WC_REST_Exception When fails to set any item.
	 * @param  WP_REST_Request $request Request object.
	 * @param  bool            $creating If is creating a new object.
	 * @return WP_Error|WC_Data
	 */
	protected function prepare_object_for_database( $request, $creating = false ) {
		$id        = isset( $request['id'] ) ? absint( $request['id'] ) : 0;
		$order     = new WC_Order( $id );
		$schema    = $this->get_item_schema();
		$data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) );

		// Handle all writable props.
		foreach ( $data_keys as $key ) {
			$value = $request[ $key ];

			if ( ! is_null( $value ) ) {
				switch ( $key ) {
					case 'coupon_lines':
					case 'status':
						// Change should be done later so transitions have new data.
						break;
					case 'billing':
					case 'shipping':
						$this->update_address( $order, $value, $key );
						break;
					case 'line_items':
					case 'shipping_lines':
					case 'fee_lines':
						if ( is_array( $value ) ) {
							foreach ( $value as $item ) {
								if ( is_array( $item ) ) {
									if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) {
										$order->remove_item( $item['id'] );
									} else {
										$this->set_item( $order, $key, $item );
									}
								}
							}
						}
						break;
					case 'meta_data':
						if ( is_array( $value ) ) {
							foreach ( $value as $meta ) {
								$order->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
							}
						}
						break;
					default:
						if ( is_callable( array( $order, "set_{$key}" ) ) ) {
							$order->{"set_{$key}"}( $value );
						}
						break;
				}
			}
		}

		/**
		 * Filters an object before it is inserted via the REST API.
		 *
		 * The dynamic portion of the hook name, `$this->post_type`,
		 * refers to the object type slug.
		 *
		 * @param WC_Data         $order    Object object.
		 * @param WP_REST_Request $request  Request object.
		 * @param bool            $creating If is creating a new object.
		 */
		return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $order, $request, $creating );
	}

	/**
	 * Save an object data.
	 *
	 * @since  3.0.0
	 * @throws WC_REST_Exception But all errors are validated before returning any data.
	 * @param  WP_REST_Request $request  Full details about the request.
	 * @param  bool            $creating If is creating a new object.
	 * @return WC_Data|WP_Error
	 */
	protected function save_object( $request, $creating = false ) {
		try {
			$object = $this->prepare_object_for_database( $request, $creating );

			if ( is_wp_error( $object ) ) {
				return $object;
			}

			// Make sure gateways are loaded so hooks from gateways fire on save/create.
			WC()->payment_gateways();

			if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] ) {
				// Make sure customer exists.
				if ( false === get_user_by( 'id', $request['customer_id'] ) ) {
					throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 );
				}

				// Make sure customer is part of blog.
				if ( is_multisite() && ! is_user_member_of_blog( $request['customer_id'] ) ) {
					add_user_to_blog( get_current_blog_id(), $request['customer_id'], 'customer' );
				}
			}

			if ( $creating ) {
				$object->set_created_via( 'rest-api' );
				$object->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) );
				$object->calculate_totals();
			} else {
				// If items have changed, recalculate order totals.
				if ( isset( $request['billing'] ) || isset( $request['shipping'] ) || isset( $request['line_items'] ) || isset( $request['shipping_lines'] ) || isset( $request['fee_lines'] ) || isset( $request['coupon_lines'] ) ) {
					$object->calculate_totals( true );
				}
			}

			// Set coupons.
			$this->calculate_coupons( $request, $object );

			// Set status.
			if ( ! empty( $request['status'] ) ) {
				$object->set_status( $request['status'] );
			}

			$object->save();

			// Actions for after the order is saved.
			if ( true === $request['set_paid'] ) {
				if ( $creating || $object->needs_payment() ) {
					$object->payment_complete( $request['transaction_id'] );
				}
			}

			return $this->get_object( $object->get_id() );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
		} catch ( WC_REST_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Prepare objects query.
	 *
	 * @since  3.0.0
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return array
	 */
	protected function prepare_objects_query( $request ) {
		// This is needed to get around an array to string notice in WC_REST_Orders_V2_Controller::prepare_objects_query.
		$statuses = $request['status'];
		unset( $request['status'] );
		$args = parent::prepare_objects_query( $request );

		$args['post_status'] = array();
		foreach ( $statuses as $status ) {
			if ( in_array( $status, $this->get_order_statuses(), true ) ) {
				$args['post_status'][] = 'wc-' . $status;
			} elseif ( 'any' === $status ) {
				// Set status to "any" and short-circuit out.
				$args['post_status'] = 'any';
				break;
			} else {
				$args['post_status'][] = $status;
			}
		}

		// Put the statuses back for further processing (next/prev links, etc).
		$request['status'] = $statuses;

		return $args;
	}

	/**
	 * Get the Order's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = parent::get_item_schema();

		$schema['properties']['coupon_lines']['items']['properties']['discount']['readonly'] = true;

		return $schema;
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params = parent::get_collection_params();

		$params['status'] = array(
			'default'           => 'any',
			'description'       => __( 'Limit result set to orders which have specific statuses.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'string',
				'enum' => array_merge( array( 'any', 'trash' ), $this->get_order_statuses() ),
			),
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}
}
rest-api/Controllers/Version3/class-wc-rest-crud-controller.php000064400000056664151336044760020654 0ustar00<?php
/**
 * Abstract Rest CRUD Controller Class
 *
 * @class    WC_REST_CRUD_Controller
 * @package WooCommerce\RestApi
 * @version  3.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC_REST_CRUD_Controller class.
 *
 * @extends WC_REST_Posts_Controller
 */
abstract class WC_REST_CRUD_Controller extends WC_REST_Posts_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';

	/**
	 * If object is hierarchical.
	 *
	 * @var bool
	 */
	protected $hierarchical = false;

	/**
	 * Get object.
	 *
	 * @param  int $id Object ID.
	 * @return object WC_Data object or WP_Error object.
	 */
	protected function get_object( $id ) {
		// translators: %s: Class method name.
		return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'woocommerce' ), __METHOD__ ), array( 'status' => 405 ) );
	}

	/**
	 * Check if a given request has access to read an item.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_item_permissions_check( $request ) {
		$object = $this->get_object( (int) $request['id'] );

		if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'read', $object->get_id() ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access to update an item.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function update_item_permissions_check( $request ) {
		$object = $this->get_object( (int) $request['id'] );

		if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'edit', $object->get_id() ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access to delete an item.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return bool|WP_Error
	 */
	public function delete_item_permissions_check( $request ) {
		$object = $this->get_object( (int) $request['id'] );

		if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Get object permalink.
	 *
	 * @param  object $object Object.
	 * @return string
	 */
	protected function get_permalink( $object ) {
		return '';
	}

	/**
	 * Prepares the object for the REST response.
	 *
	 * @since  3.0.0
	 * @param  WC_Data         $object  Object data.
	 * @param  WP_REST_Request $request Request object.
	 * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
	 */
	protected function prepare_object_for_response( $object, $request ) {
		// translators: %s: Class method name.
		return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'woocommerce' ), __METHOD__ ), array( 'status' => 405 ) );
	}

	/**
	 * Prepares one object for create or update operation.
	 *
	 * @since  3.0.0
	 * @param  WP_REST_Request $request Request object.
	 * @param  bool            $creating If is creating a new object.
	 * @return WP_Error|WC_Data The prepared item, or WP_Error object on failure.
	 */
	protected function prepare_object_for_database( $request, $creating = false ) {
		// translators: %s: Class method name.
		return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'woocommerce' ), __METHOD__ ), array( 'status' => 405 ) );
	}

	/**
	 * Get a single item.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_item( $request ) {
		$object = $this->get_object( (int) $request['id'] );

		if ( ! $object || 0 === $object->get_id() ) {
			return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$data     = $this->prepare_object_for_response( $object, $request );
		$response = rest_ensure_response( $data );

		if ( $this->public ) {
			$response->link_header( 'alternate', $this->get_permalink( $object ), array( 'type' => 'text/html' ) );
		}

		return $response;
	}

	/**
	 * Save an object data.
	 *
	 * @since  3.0.0
	 * @param  WP_REST_Request $request  Full details about the request.
	 * @param  bool            $creating If is creating a new object.
	 * @return WC_Data|WP_Error
	 */
	protected function save_object( $request, $creating = false ) {
		try {
			$object = $this->prepare_object_for_database( $request, $creating );

			if ( is_wp_error( $object ) ) {
				return $object;
			}

			$object->save();

			return $this->get_object( $object->get_id() );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
		} catch ( WC_REST_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create a single item.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function create_item( $request ) {
		if ( ! empty( $request['id'] ) ) {
			/* translators: %s: post type */
			return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) );
		}

		$object = $this->save_object( $request, true );

		if ( is_wp_error( $object ) ) {
			return $object;
		}

		try {
			$this->update_additional_fields_for_object( $object, $request );

			/**
			 * Fires after a single object is created or updated via the REST API.
			 *
			 * @param WC_Data         $object    Inserted object.
			 * @param WP_REST_Request $request   Request object.
			 * @param boolean         $creating  True when creating object, false when updating.
			 */
			do_action( "woocommerce_rest_insert_{$this->post_type}_object", $object, $request, true );
		} catch ( WC_Data_Exception $e ) {
			$object->delete();
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
		} catch ( WC_REST_Exception $e ) {
			$object->delete();
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_object_for_response( $object, $request );
		$response = rest_ensure_response( $response );
		$response->set_status( 201 );
		$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ) );

		return $response;
	}

	/**
	 * Update a single post.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function update_item( $request ) {
		$object = $this->get_object( (int) $request['id'] );

		if ( ! $object || 0 === $object->get_id() ) {
			return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 400 ) );
		}

		$object = $this->save_object( $request, false );

		if ( is_wp_error( $object ) ) {
			return $object;
		}

		try {
			$this->update_additional_fields_for_object( $object, $request );

			/**
			 * Fires after a single object is created or updated via the REST API.
			 *
			 * @param WC_Data         $object    Inserted object.
			 * @param WP_REST_Request $request   Request object.
			 * @param boolean         $creating  True when creating object, false when updating.
			 */
			do_action( "woocommerce_rest_insert_{$this->post_type}_object", $object, $request, false );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
		} catch ( WC_REST_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_object_for_response( $object, $request );
		return rest_ensure_response( $response );
	}

	/**
	 * Prepare objects query.
	 *
	 * @since  3.0.0
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return array
	 */
	protected function prepare_objects_query( $request ) {
		$args                        = array();
		$args['offset']              = $request['offset'];
		$args['order']               = $request['order'];
		$args['orderby']             = $request['orderby'];
		$args['paged']               = $request['page'];
		$args['post__in']            = $request['include'];
		$args['post__not_in']        = $request['exclude'];
		$args['posts_per_page']      = $request['per_page'];
		$args['name']                = $request['slug'];
		$args['post_parent__in']     = $request['parent'];
		$args['post_parent__not_in'] = $request['parent_exclude'];
		$args['s']                   = $request['search'];
		$args['fields']              = $this->get_fields_for_response( $request );

		if ( 'date' === $args['orderby'] ) {
			$args['orderby'] = 'date ID';
		}

		$date_query = array();
		$use_gmt    = $request['dates_are_gmt'];

		if ( isset( $request['before'] ) ) {
			$date_query[] = array(
				'column' => $use_gmt ? 'post_date_gmt' : 'post_date',
				'before' => $request['before'],
			);
		}

		if ( isset( $request['after'] ) ) {
			$date_query[] = array(
				'column' => $use_gmt ? 'post_date_gmt' : 'post_date',
				'after'  => $request['after'],
			);
		}

		if ( isset( $request['modified_before'] ) ) {
			$date_query[] = array(
				'column' => $use_gmt ? 'post_modified_gmt' : 'post_modified',
				'before' => $request['modified_before'],
			);
		}

		if ( isset( $request['modified_after'] ) ) {
			$date_query[] = array(
				'column' => $use_gmt ? 'post_modified_gmt' : 'post_modified',
				'after'  => $request['modified_after'],
			);
		}

		if ( ! empty( $date_query ) ) {
			$date_query['relation'] = 'AND';
			$args['date_query']     = $date_query;
		}

		// Force the post_type argument, since it's not a user input variable.
		$args['post_type'] = $this->post_type;

		/**
		 * Filter the query arguments for a request.
		 *
		 * Enables adding extra arguments or setting defaults for a post
		 * collection request.
		 *
		 * @param array           $args    Key value array of query var to query value.
		 * @param WP_REST_Request $request The request used.
		 */
		$args = apply_filters( "woocommerce_rest_{$this->post_type}_object_query", $args, $request );

		return $this->prepare_items_query( $args, $request );
	}

	/**
	 * Get objects.
	 *
	 * @since  3.0.0
	 * @param  array $query_args Query args.
	 * @return array
	 */
	protected function get_objects( $query_args ) {
		$query  = new WP_Query();
		$result = $query->query( $query_args );

		$total_posts = $query->found_posts;
		if ( $total_posts < 1 ) {
			// Out-of-bounds, run the query again without LIMIT for total count.
			unset( $query_args['paged'] );
			$count_query = new WP_Query();
			$count_query->query( $query_args );
			$total_posts = $count_query->found_posts;
		}

		return array(
			'objects' => array_filter( array_map( array( $this, 'get_object' ), $result ) ),
			'total'   => (int) $total_posts,
			'pages'   => (int) ceil( $total_posts / (int) $query->query_vars['posts_per_page'] ),
		);
	}

	/**
	 * Get a collection of posts.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_items( $request ) {
		$query_args = $this->prepare_objects_query( $request );
		if ( is_wp_error( current( $query_args ) ) ) {
			return current( $query_args );
		}
		$query_results = $this->get_objects( $query_args );

		$objects = array();
		foreach ( $query_results['objects'] as $object ) {
			if ( ! wc_rest_check_post_permissions( $this->post_type, 'read', $object->get_id() ) ) {
				continue;
			}

			$data      = $this->prepare_object_for_response( $object, $request );
			$objects[] = $this->prepare_response_for_collection( $data );
		}

		$page      = (int) $query_args['paged'];
		$max_pages = $query_results['pages'];

		$response = rest_ensure_response( $objects );
		$response->header( 'X-WP-Total', $query_results['total'] );
		$response->header( 'X-WP-TotalPages', (int) $max_pages );

		$base          = $this->rest_base;
		$attrib_prefix = '(?P<';
		if ( strpos( $base, $attrib_prefix ) !== false ) {
			$attrib_names = array();
			preg_match( '/\(\?P<[^>]+>.*\)/', $base, $attrib_names, PREG_OFFSET_CAPTURE );
			foreach ( $attrib_names as $attrib_name_match ) {
				$beginning_offset = strlen( $attrib_prefix );
				$attrib_name_end  = strpos( $attrib_name_match[0], '>', $attrib_name_match[1] );
				$attrib_name      = substr( $attrib_name_match[0], $beginning_offset, $attrib_name_end - $beginning_offset );
				if ( isset( $request[ $attrib_name ] ) ) {
					$base = str_replace( "(?P<$attrib_name>[\d]+)", $request[ $attrib_name ], $base );
				}
			}
		}
		$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ) );

		if ( $page > 1 ) {
			$prev_page = $page - 1;
			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}
			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}
		if ( $max_pages > $page ) {
			$next_page = $page + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );
			$response->link_header( 'next', $next_link );
		}

		return $response;
	}

	/**
	 * Delete a single item.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error
	 */
	public function delete_item( $request ) {
		$force  = (bool) $request['force'];
		$object = $this->get_object( (int) $request['id'] );
		$result = false;

		if ( ! $object || 0 === $object->get_id() ) {
			return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( array( $object, 'get_status' ) );

		/**
		 * Filter whether an object is trashable.
		 *
		 * Return false to disable trash support for the object.
		 *
		 * @param boolean $supports_trash Whether the object type support trashing.
		 * @param WC_Data $object         The object being considered for trashing support.
		 */
		$supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_object_trashable", $supports_trash, $object );

		if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) {
			/* translators: %s: post type */
			return new WP_Error( "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) );
		}

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_object_for_response( $object, $request );

		// If we're forcing, then delete permanently.
		if ( $force ) {
			$object->delete( true );
			$result = 0 === $object->get_id();
		} else {
			// If we don't support trashing for this type, error out.
			if ( ! $supports_trash ) {
				/* translators: %s: post type */
				return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array( 'status' => 501 ) );
			}

			// Otherwise, only trash if we haven't already.
			if ( is_callable( array( $object, 'get_status' ) ) ) {
				if ( 'trash' === $object->get_status() ) {
					/* translators: %s: post type */
					return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410 ) );
				}

				$object->delete();
				$result = 'trash' === $object->get_status();
			}
		}

		if ( ! $result ) {
			/* translators: %s: post type */
			return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) );
		}

		/**
		 * Fires after a single object is deleted or trashed via the REST API.
		 *
		 * @param WC_Data          $object   The deleted or trashed object.
		 * @param WP_REST_Response $response The response data.
		 * @param WP_REST_Request  $request  The request sent to the API.
		 */
		do_action( "woocommerce_rest_delete_{$this->post_type}_object", $object, $response, $request );

		return $response;
	}

	/**
	 * Get fields for an object if getter is defined.
	 *
	 * @param object $object  Object we are fetching response for.
	 * @param string $context Context of the request. Can be `view` or `edit`.
	 * @param array  $fields  List of fields to fetch.
	 * @return array Data fetched from getters.
	 */
	public function fetch_fields_using_getters( $object, $context, $fields ) {
		$data = array();
		foreach ( $fields as $field ) {
			if ( method_exists( $this, "api_get_$field" ) ) {
				$data[ $field ] = $this->{"api_get_$field"}( $object, $context );
			}
		}
		return $data;
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param WC_Data         $object  Object data.
	 * @param WP_REST_Request $request Request object.
	 * @return array                   Links for the given post.
	 */
	protected function prepare_links( $object, $request ) {
		$links = array(
			'self'       => array(
				'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ),
			),
			'collection' => array(
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
			),
		);

		return $links;
	}

	/**
	 * Get the query params for collections of attachments.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params                       = array();
		$params['context']            = $this->get_context_param();
		$params['context']['default'] = 'view';

		$params['page']            = array(
			'description'       => __( 'Current page of the collection.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 1,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
			'minimum'           => 1,
		);
		$params['per_page']        = array(
			'description'       => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 10,
			'minimum'           => 1,
			'maximum'           => 100,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['search']          = array(
			'description'       => __( 'Limit results to those matching a string.', 'woocommerce' ),
			'type'              => 'string',
			'sanitize_callback' => 'sanitize_text_field',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['after']           = array(
			'description'       => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['before']          = array(
			'description'       => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['modified_after']  = array(
			'description'       => __( 'Limit response to resources modified after a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['modified_before'] = array(
			'description'       => __( 'Limit response to resources modified before a given ISO8601 compliant date.', 'woocommerce' ),
			'type'              => 'string',
			'format'            => 'date-time',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['dates_are_gmt']   = array(
			'description'       => __( 'Whether to consider GMT post dates when limiting response by published or modified date.', 'woocommerce' ),
			'type'              => 'boolean',
			'default'           => false,
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['exclude']         = array(
			'description'       => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'sanitize_callback' => 'wp_parse_id_list',
		);
		$params['include']         = array(
			'description'       => __( 'Limit result set to specific ids.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'sanitize_callback' => 'wp_parse_id_list',
		);
		$params['offset']          = array(
			'description'       => __( 'Offset the result set by a specific number of items.', 'woocommerce' ),
			'type'              => 'integer',
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['order']           = array(
			'description'       => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'desc',
			'enum'              => array( 'asc', 'desc' ),
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['orderby']         = array(
			'description'       => __( 'Sort collection by object attribute.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'date',
			'enum'              => array(
				'date',
				'id',
				'include',
				'title',
				'slug',
				'modified',
			),
			'validate_callback' => 'rest_validate_request_arg',
		);

		if ( $this->hierarchical ) {
			$params['parent']         = array(
				'description'       => __( 'Limit result set to those of particular parent IDs.', 'woocommerce' ),
				'type'              => 'array',
				'items'             => array(
					'type' => 'integer',
				),
				'sanitize_callback' => 'wp_parse_id_list',
				'default'           => array(),
			);
			$params['parent_exclude'] = array(
				'description'       => __( 'Limit result set to all items except those of a particular parent ID.', 'woocommerce' ),
				'type'              => 'array',
				'items'             => array(
					'type' => 'integer',
				),
				'sanitize_callback' => 'wp_parse_id_list',
				'default'           => array(),
			);
		}

		/**
		 * Filter collection parameters for the posts controller.
		 *
		 * The dynamic part of the filter `$this->post_type` refers to the post
		 * type slug for the controller.
		 *
		 * This filter registers the collection parameter, but does not map the
		 * collection parameter to an internal WP_Query parameter. Use the
		 * `rest_{$this->post_type}_query` filter to set WP_Query parameters.
		 *
		 * @param array        $query_params JSON Schema-formatted collection parameters.
		 * @param WP_Post_Type $post_type    Post type object.
		 */
		return apply_filters( "rest_{$this->post_type}_collection_params", $params, $this->post_type );
	}
}
rest-api/Controllers/Version3/class-wc-rest-shipping-methods-controller.php000064400000001001151336044760023150 0ustar00<?php
/**
 * REST API WC Shipping Methods controller
 *
 * Handles requests to the /shipping_methods endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Shipping methods controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Shipping_Methods_V2_Controller
 */
class WC_REST_Shipping_Methods_Controller extends WC_REST_Shipping_Methods_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';
}
rest-api/Controllers/Version3/class-wc-rest-report-coupons-totals-controller.php000064400000006715151336044760024212 0ustar00<?php
/**
 * REST API Reports Coupons Totals controller
 *
 * Handles requests to the /reports/coupons/count endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   3.5.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Reports Coupons Totals controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Reports_Controller
 */
class WC_REST_Report_Coupons_Totals_Controller extends WC_REST_Reports_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'reports/coupons/totals';

	/**
	 * Get reports list.
	 *
	 * @since 3.5.0
	 * @return array
	 */
	protected function get_reports() {
		global $wpdb;

		$data = get_transient( 'rest_api_coupons_type_count' );
		if ( false !== $data ) {
			return $data;
		}

		$types = wc_get_coupon_types();
		$data  = array();

		foreach ( $types as $slug => $name ) {
			$results = $wpdb->get_results(
				$wpdb->prepare( "
					SELECT count(meta_id) AS total
					FROM $wpdb->postmeta
					WHERE meta_key = 'discount_type'
					AND meta_value = %s
				", $slug )
			);

			$total = isset( $results[0] ) ? (int) $results[0]->total : 0;

			$data[] = array(
				'slug'  => $slug,
				'name'  => $name,
				'total' => $total,
			);
		}

		set_transient( 'rest_api_coupons_type_count', $data, YEAR_IN_SECONDS );

		return $data;
	}

	/**
	 * Prepare a report object for serialization.
	 *
	 * @param  stdClass        $report Report data.
	 * @param  WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $report, $request ) {
		$data = array(
			'slug'  => $report->slug,
			'name'  => $report->name,
			'total' => $report->total,
		);

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		/**
		 * Filter a report returned from the API.
		 *
		 * Allows modification of the report data right before it is returned.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param object           $report   The original report object.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_report_coupons_count', $response, $report, $request );
	}

	/**
	 * Get the Report's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'report_coupon_total',
			'type'       => 'object',
			'properties' => array(
				'slug'  => array(
					'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'name'  => array(
					'description' => __( 'Coupon type name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'total' => array(
					'description' => __( 'Amount of coupons.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version3/class-wc-rest-system-status-tools-controller.php000064400000001020151336044760023672 0ustar00<?php
/**
 * REST API WC System Status Tools Controller
 *
 * Handles requests to the /system_status/tools/* endpoints.
 *
 * @package WooCommerce\RestApi
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * System status tools controller.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_System_Status_Tools_V2_Controller
 */
class WC_REST_System_Status_Tools_Controller extends WC_REST_System_Status_Tools_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';
}
rest-api/Controllers/Version3/class-wc-rest-data-countries-controller.php000064400000015157151336044760022631 0ustar00<?php
/**
 * REST API Data countries controller.
 *
 * Handles requests to the /data/countries endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   3.5.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Data countries controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Controller
 */
class WC_REST_Data_Countries_Controller extends WC_REST_Data_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'data/countries';

	/**
	 * Register routes.
	 *
	 * @since 3.5.0
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<location>[\w-]+)',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => array(
						'location' => array(
							'description' => __( 'ISO3166 alpha-2 country code.', 'woocommerce' ),
							'type'        => 'string',
						),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Get a list of countries and states.
	 *
	 * @param  string          $country_code Country code.
	 * @param  WP_REST_Request $request      Request data.
	 * @return array|mixed Response data, ready for insertion into collection data.
	 */
	public function get_country( $country_code, $request ) {
		$countries = WC()->countries->get_countries();
		$states    = WC()->countries->get_states();
		$data      = array();

		if ( ! array_key_exists( $country_code, $countries ) ) {
			return false;
		}

		$country = array(
			'code' => $country_code,
			'name' => $countries[ $country_code ],
		);

		$local_states = array();
		if ( isset( $states[ $country_code ] ) ) {
			foreach ( $states[ $country_code ] as $state_code => $state_name ) {
				$local_states[] = array(
					'code' => $state_code,
					'name' => $state_name,
				);
			}
		}
		$country['states'] = $local_states;
		return $country;
	}

	/**
	 * Return the list of states for all countries.
	 *
	 * @since  3.5.0
	 * @param  WP_REST_Request $request Request data.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_items( $request ) {
		$countries = WC()->countries->get_countries();
		$data      = array();

		foreach ( array_keys( $countries ) as $country_code ) {
			$country  = $this->get_country( $country_code, $request );
			$response = $this->prepare_item_for_response( $country, $request );
			$data[]   = $this->prepare_response_for_collection( $response );
		}

		return rest_ensure_response( $data );
	}

	/**
	 * Return the list of states for a given country.
	 *
	 * @since  3.5.0
	 * @param  WP_REST_Request $request Request data.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_item( $request ) {
		$data = $this->get_country( strtoupper( $request['location'] ), $request );
		if ( empty( $data ) ) {
			return new WP_Error( 'woocommerce_rest_data_invalid_location', __( 'There are no locations matching these parameters.', 'woocommerce' ), array( 'status' => 404 ) );
		}
		return $this->prepare_item_for_response( $data, $request );
	}

	/**
	 * Prepare the data object for response.
	 *
	 * @since  3.5.0
	 * @param object          $item Data object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $item, $request ) {
		$data     = $this->add_additional_fields_to_object( $item, $request );
		$data     = $this->filter_response_by_context( $data, 'view' );
		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $item ) );

		/**
		 * Filter the states list for a country returned from the API.
		 *
		 * Allows modification of the loction data right before it is returned.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param array            $data     The original country's states list.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_data_country', $response, $item, $request );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param object $item Data object.
	 * @return array Links for the given country.
	 */
	protected function prepare_links( $item ) {
		$country_code = strtolower( $item['code'] );
		$links        = array(
			'self'       => array(
				'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $country_code ) ),
			),
			'collection' => array(
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
			),
		);

		return $links;
	}


	/**
	 * Get the location schema, conforming to JSON Schema.
	 *
	 * @since  3.5.0
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'data_countries',
			'type'       => 'object',
			'properties' => array(
				'code'   => array(
					'type'        => 'string',
					'description' => __( 'ISO3166 alpha-2 country code.', 'woocommerce' ),
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'name'   => array(
					'type'        => 'string',
					'description' => __( 'Full name of country.', 'woocommerce' ),
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'states' => array(
					'type'        => 'array',
					'description' => __( 'List of states in this country.', 'woocommerce' ),
					'context'     => array( 'view' ),
					'readonly'    => true,
					'items'       => array(
						'type'       => 'object',
						'context'    => array( 'view' ),
						'readonly'   => true,
						'properties' => array(
							'code' => array(
								'type'        => 'string',
								'description' => __( 'State code.', 'woocommerce' ),
								'context'     => array( 'view' ),
								'readonly'    => true,
							),
							'name' => array(
								'type'        => 'string',
								'description' => __( 'Full name of state.', 'woocommerce' ),
								'context'     => array( 'view' ),
								'readonly'    => true,
							),
						),
					),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version3/class-wc-rest-report-top-sellers-controller.php000064400000001010151336044760023450 0ustar00<?php
/**
 * REST API Reports controller
 *
 * Handles requests to the reports/top_sellers endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Report Top Sellers controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Report_Top_Sellers_V2_Controller
 */
class WC_REST_Report_Top_Sellers_Controller extends WC_REST_Report_Top_Sellers_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v3';
}
rest-api/Controllers/Version2/class-wc-rest-product-shipping-classes-v2-controller.php000064400000001072151336044760025154 0ustar00<?php
/**
 * REST API Product Shipping Classes controller
 *
 * Handles requests to the products/shipping_classes endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Product Shipping Classes controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Product_Shipping_Classes_V1_Controller
 */
class WC_REST_Product_Shipping_Classes_V2_Controller extends WC_REST_Product_Shipping_Classes_V1_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';
}
rest-api/Controllers/Version2/class-wc-rest-network-orders-v2-controller.php000064400000012222151336044760023206 0ustar00<?php
/**
 * REST API Network Orders controller
 *
 * Handles requests to the /orders/network endpoint
 *
 * @package WooCommerce\RestApi
 * @since    3.4.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Network Orders controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Orders_V2_Controller
 */
class WC_REST_Network_Orders_V2_Controller extends WC_REST_Orders_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';

	/**
	 * Register the routes for network orders.
	 */
	public function register_routes() {
		if ( is_multisite() ) {
			register_rest_route(
				$this->namespace,
				'/' . $this->rest_base . '/network',
				array(
					array(
						'methods'             => WP_REST_Server::READABLE,
						'callback'            => array( $this, 'network_orders' ),
						'permission_callback' => array( $this, 'network_orders_permissions_check' ),
						'args'                => $this->get_collection_params(),
					),
					'schema' => array( $this, 'get_public_item_schema' ),
				)
			);
		}
	}

	/**
	 * Retrieves the item's schema for display / public consumption purposes.
	 *
	 * @return array Public item schema data.
	 */
	public function get_public_item_schema() {
		$schema = parent::get_public_item_schema();

		$schema['properties']['blog']              = array(
			'description' => __( 'Blog id of the record on the multisite.', 'woocommerce' ),
			'type'        => 'integer',
			'context'     => array( 'view' ),
			'readonly'    => true,
		);
		$schema['properties']['edit_url']          = array(
			'description' => __( 'URL to edit the order', 'woocommerce' ),
			'type'        => 'string',
			'context'     => array( 'view' ),
			'readonly'    => true,
		);
		$schema['properties']['customer'][]        = array(
			'description' => __( 'Name of the customer for the order', 'woocommerce' ),
			'type'        => 'string',
			'context'     => array( 'view' ),
			'readonly'    => true,
		);
		$schema['properties']['status_name'][]     = array(
			'description' => __( 'Order Status', 'woocommerce' ),
			'type'        => 'string',
			'context'     => array( 'view' ),
			'readonly'    => true,
		);
		$schema['properties']['formatted_total'][] = array(
			'description' => __( 'Order total formatted for locale', 'woocommerce' ),
			'type'        => 'string',
			'context'     => array( 'view' ),
			'readonly'    => true,
		);

		return $schema;
	}

	/**
	 * Does a permissions check for the proper requested blog
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 *
	 * @return bool $permission
	 */
	public function network_orders_permissions_check( $request ) {
		$blog_id = $request->get_param( 'blog_id' );
		$blog_id = ! empty( $blog_id ) ? $blog_id : get_current_blog_id();

		switch_to_blog( $blog_id );

		$permission = $this->get_items_permissions_check( $request );

		restore_current_blog();

		return $permission;
	}

	/**
	 * Get a collection of orders from the requested blog id
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 *
	 * @return WP_REST_Response
	 */
	public function network_orders( $request ) {
		$blog_id = $request->get_param( 'blog_id' );
		$blog_id = ! empty( $blog_id ) ? $blog_id : get_current_blog_id();
		$active_plugins = get_blog_option( $blog_id, 'active_plugins', array() );
		$network_active_plugins = array_keys( get_site_option( 'active_sitewide_plugins', array() ) );

		$plugins = array_merge( $active_plugins, $network_active_plugins );
		$wc_active = false;
		foreach ( $plugins as $plugin ) {
			if ( substr_compare( $plugin, '/woocommerce.php', strlen( $plugin ) - strlen( '/woocommerce.php' ), strlen( '/woocommerce.php' ) ) === 0 ) {
				$wc_active = true;
			}
		}

		// If WooCommerce not active for site, return an empty response.
		if ( ! $wc_active ) {
			$response = rest_ensure_response( array() );
			return $response;
		}

		switch_to_blog( $blog_id );
		add_filter( 'woocommerce_rest_orders_prepare_object_query', array( $this, 'network_orders_filter_args' ) );
		$items = $this->get_items( $request );
		remove_filter( 'woocommerce_rest_orders_prepare_object_query', array( $this, 'network_orders_filter_args' ) );

		foreach ( $items->data as &$current_order ) {
			$order = wc_get_order( $current_order['id'] );

			$current_order['blog']     = get_blog_details( get_current_blog_id() );
			$current_order['edit_url'] = get_admin_url( $blog_id, 'post.php?post=' . absint( $order->get_id() ) . '&action=edit' );
			/* translators: 1: first name 2: last name */
			$current_order['customer']        = trim( sprintf( _x( '%1$s %2$s', 'full name', 'woocommerce' ), $order->get_billing_first_name(), $order->get_billing_last_name() ) );
			$current_order['status_name']     = wc_get_order_status_name( $order->get_status() );
			$current_order['formatted_total'] = $order->get_formatted_order_total();
		}

		restore_current_blog();

		return $items;
	}

	/**
	 * Filters the post statuses to on hold and processing for the network order query.
	 *
	 * @param array $args Query args.
	 *
	 * @return array
	 */
	public function network_orders_filter_args( $args ) {
		$args['post_status'] = array(
			'wc-on-hold',
			'wc-processing',
		);

		return $args;
	}
}
rest-api/Controllers/Version2/class-wc-rest-settings-v2-controller.php000064400000014204151336044760022063 0ustar00<?php
/**
 * REST API Settings controller
 *
 * Handles requests to the /settings endpoints.
 *
 * @package WooCommerce\RestApi
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Settings controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Controller
 */
class WC_REST_Settings_V2_Controller extends WC_REST_Controller {

	/**
	 * WP REST API namespace/version.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'settings';

	/**
	 * Register routes.
	 *
	 * @since 3.0.0
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace, '/' . $this->rest_base, array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Get all settings groups items.
	 *
	 * @since  3.0.0
	 * @param  WP_REST_Request $request Request data.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_items( $request ) {
		$groups = apply_filters( 'woocommerce_settings_groups', array() );
		if ( empty( $groups ) ) {
			return new WP_Error( 'rest_setting_groups_empty', __( 'No setting groups have been registered.', 'woocommerce' ), array( 'status' => 500 ) );
		}

		$defaults        = $this->group_defaults();
		$filtered_groups = array();
		foreach ( $groups as $group ) {
			$sub_groups = array();
			foreach ( $groups as $_group ) {
				if ( ! empty( $_group['parent_id'] ) && $group['id'] === $_group['parent_id'] ) {
					$sub_groups[] = $_group['id'];
				}
			}
			$group['sub_groups'] = $sub_groups;

			$group = wp_parse_args( $group, $defaults );
			if ( ! is_null( $group['id'] ) && ! is_null( $group['label'] ) ) {
				$group_obj  = $this->filter_group( $group );
				$group_data = $this->prepare_item_for_response( $group_obj, $request );
				$group_data = $this->prepare_response_for_collection( $group_data );

				$filtered_groups[] = $group_data;
			}
		}

		$response = rest_ensure_response( $filtered_groups );
		return $response;
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param string $group_id Group ID.
	 * @return array Links for the given group.
	 */
	protected function prepare_links( $group_id ) {
		$base  = '/' . $this->namespace . '/' . $this->rest_base;
		$links = array(
			'options' => array(
				'href' => rest_url( trailingslashit( $base ) . $group_id ),
			),
		);

		return $links;
	}

	/**
	 * Prepare a report sales object for serialization.
	 *
	 * @since  3.0.0
	 * @param array           $item Group object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $item, $request ) {
		$context = empty( $request['context'] ) ? 'view' : $request['context'];
		$data    = $this->add_additional_fields_to_object( $item, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $item['id'] ) );

		return $response;
	}

	/**
	 * Filters out bad values from the groups array/filter so we
	 * only return known values via the API.
	 *
	 * @since 3.0.0
	 * @param  array $group Group.
	 * @return array
	 */
	public function filter_group( $group ) {
		return array_intersect_key(
			$group,
			array_flip( array_filter( array_keys( $group ), array( $this, 'allowed_group_keys' ) ) )
		);
	}

	/**
	 * Callback for allowed keys for each group response.
	 *
	 * @since  3.0.0
	 * @param  string $key Key to check.
	 * @return boolean
	 */
	public function allowed_group_keys( $key ) {
		return in_array( $key, array( 'id', 'label', 'description', 'parent_id', 'sub_groups' ) );
	}

	/**
	 * Returns default settings for groups. null means the field is required.
	 *
	 * @since  3.0.0
	 * @return array
	 */
	protected function group_defaults() {
		return array(
			'id'          => null,
			'label'       => null,
			'description' => '',
			'parent_id'   => '',
			'sub_groups'  => array(),
		);
	}

	/**
	 * Makes sure the current user has access to READ the settings APIs.
	 *
	 * @since  3.0.0
	 * @param WP_REST_Request $request Full data about the request.
	 * @return WP_Error|boolean
	 */
	public function get_items_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Get the groups schema, conforming to JSON Schema.
	 *
	 * @since  3.0.0
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'setting_group',
			'type'       => 'object',
			'properties' => array(
				'id'          => array(
					'description' => __( 'A unique identifier that can be used to link settings together.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'label'       => array(
					'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'description' => array(
					'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'parent_id'   => array(
					'description' => __( 'ID of parent grouping.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'sub_groups'  => array(
					'description' => __( 'IDs for settings sub groups.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version2/class-wc-rest-webhooks-v2-controller.php000064400000014216151336044760022047 0ustar00<?php
/**
 * REST API Webhooks controller
 *
 * Handles requests to the /webhooks endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Webhooks controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Webhooks_V1_Controller
 */
class WC_REST_Webhooks_V2_Controller extends WC_REST_Webhooks_V1_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';

	/**
	 * Prepare a single webhook output for response.
	 *
	 * @param int             $id       Webhook ID.
	 * @param WP_REST_Request $request  Request object.
	 * @return WP_REST_Response $response
	 */
	public function prepare_item_for_response( $id, $request ) {
		$webhook = wc_get_webhook( $id );

		if ( empty( $webhook ) || is_null( $webhook ) ) {
			return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) );
		}

		$data = array(
			'id'                => $webhook->get_id(),
			'name'              => $webhook->get_name(),
			'status'            => $webhook->get_status(),
			'topic'             => $webhook->get_topic(),
			'resource'          => $webhook->get_resource(),
			'event'             => $webhook->get_event(),
			'hooks'             => $webhook->get_hooks(),
			'delivery_url'      => $webhook->get_delivery_url(),
			'date_created'      => wc_rest_prepare_date_response( $webhook->get_date_created(), false ),
			'date_created_gmt'  => wc_rest_prepare_date_response( $webhook->get_date_created() ),
			'date_modified'     => wc_rest_prepare_date_response( $webhook->get_date_modified(), false ),
			'date_modified_gmt' => wc_rest_prepare_date_response( $webhook->get_date_modified() ),
		);

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $webhook->get_id(), $request ) );

		/**
		 * Filter webhook object returned from the REST API.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param WC_Webhook       $webhook  Webhook object used to create response.
		 * @param WP_REST_Request  $request  Request object.
		 */
		return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $webhook, $request );
	}

	/**
	 * Get the default REST API version.
	 *
	 * @since  3.0.0
	 * @return string
	 */
	protected function get_default_api_version() {
		return 'wp_api_v2';
	}

	/**
	 * Get the Webhook's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'webhook',
			'type'       => 'object',
			'properties' => array(
				'id'                => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'name'              => array(
					'description' => __( 'A friendly name for the webhook.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'status'            => array(
					'description' => __( 'Webhook status.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'active',
					'enum'        => array_keys( wc_get_webhook_statuses() ),
					'context'     => array( 'view', 'edit' ),
				),
				'topic'             => array(
					'description' => __( 'Webhook topic.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'resource'          => array(
					'description' => __( 'Webhook resource.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'event'             => array(
					'description' => __( 'Webhook event.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'hooks'             => array(
					'description' => __( 'WooCommerce action names associated with the webhook.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
					'items'       => array(
						'type' => 'string',
					),
				),
				'delivery_url'      => array(
					'description' => __( 'The URL where the webhook payload is delivered.', 'woocommerce' ),
					'type'        => 'string',
					'format'      => 'uri',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'secret'            => array(
					'description' => __( "Secret key used to generate a hash of the delivered webhook and provided in the request headers. This will default to a MD5 hash from the current user's ID|username if not provided.", 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'edit' ),
				),
				'date_created'      => array(
					'description' => __( "The date the webhook was created, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_created_gmt'  => array(
					'description' => __( 'The date the webhook was created, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_modified'     => array(
					'description' => __( "The date the webhook was last modified, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_modified_gmt' => array(
					'description' => __( 'The date the webhook was last modified, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version2/class-wc-rest-customers-v2-controller.php000064400000030627151336044760022256 0ustar00<?php
/**
 * REST API Customers controller
 *
 * Handles requests to the /customers endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Customers controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Customers_V1_Controller
 */
class WC_REST_Customers_V2_Controller extends WC_REST_Customers_V1_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';

	/**
	 * Get formatted item data.
	 *
	 * @since  3.0.0
	 * @param  WC_Data $object WC_Data instance.
	 * @return array
	 */
	protected function get_formatted_item_data( $object ) {
		$data        = $object->get_data();
		$format_date = array( 'date_created', 'date_modified' );

		// Format date values.
		foreach ( $format_date as $key ) {
			$datetime              = 'date_created' === $key ? get_date_from_gmt( gmdate( 'Y-m-d H:i:s', $data[ $key ]->getTimestamp() ) ) : $data[ $key ];
			$data[ $key ]          = wc_rest_prepare_date_response( $datetime, false );
			$data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime );
		}

		return array(
			'id'                 => $object->get_id(),
			'date_created'       => $data['date_created'],
			'date_created_gmt'   => $data['date_created_gmt'],
			'date_modified'      => $data['date_modified'],
			'date_modified_gmt'  => $data['date_modified_gmt'],
			'email'              => $data['email'],
			'first_name'         => $data['first_name'],
			'last_name'          => $data['last_name'],
			'role'               => $data['role'],
			'username'           => $data['username'],
			'billing'            => $data['billing'],
			'shipping'           => $data['shipping'],
			'is_paying_customer' => $data['is_paying_customer'],
			'orders_count'       => $object->get_order_count(),
			'total_spent'        => $object->get_total_spent(),
			'avatar_url'         => $object->get_avatar_url(),
			'meta_data'          => $data['meta_data'],
		);
	}

	/**
	 * Prepare a single customer output for response.
	 *
	 * @param  WP_User         $user_data User object.
	 * @param  WP_REST_Request $request   Request object.
	 * @return WP_REST_Response $response  Response data.
	 */
	public function prepare_item_for_response( $user_data, $request ) {
		$customer = new WC_Customer( $user_data->ID );
		$data     = $this->get_formatted_item_data( $customer );
		$context  = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data     = $this->add_additional_fields_to_object( $data, $request );
		$data     = $this->filter_response_by_context( $data, $context );
		$response = rest_ensure_response( $data );
		$response->add_links( $this->prepare_links( $user_data ) );

		/**
		 * Filter customer data returned from the REST API.
		 *
		 * @param WP_REST_Response $response   The response object.
		 * @param WP_User          $user_data  User object used to create response.
		 * @param WP_REST_Request  $request    Request object.
		 */
		return apply_filters( 'woocommerce_rest_prepare_customer', $response, $user_data, $request );
	}

	/**
	 * Update customer meta fields.
	 *
	 * @param WC_Customer     $customer Customer data.
	 * @param WP_REST_Request $request  Request data.
	 */
	protected function update_customer_meta_fields( $customer, $request ) {
		parent::update_customer_meta_fields( $customer, $request );

		// Meta data.
		if ( isset( $request['meta_data'] ) ) {
			if ( is_array( $request['meta_data'] ) ) {
				foreach ( $request['meta_data'] as $meta ) {
					$customer->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
				}
			}
		}
	}

	/**
	 * Get the Customer's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'customer',
			'type'       => 'object',
			'properties' => array(
				'id'                 => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_created'       => array(
					'description' => __( "The date the customer was created, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_created_gmt'   => array(
					'description' => __( 'The date the customer was created, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_modified'      => array(
					'description' => __( "The date the customer was last modified, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_modified_gmt'  => array(
					'description' => __( 'The date the customer was last modified, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'email'              => array(
					'description' => __( 'The email address for the customer.', 'woocommerce' ),
					'type'        => 'string',
					'format'      => 'email',
					'context'     => array( 'view', 'edit' ),
				),
				'first_name'         => array(
					'description' => __( 'Customer first name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				'last_name'          => array(
					'description' => __( 'Customer last name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				'role'               => array(
					'description' => __( 'Customer role.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'username'           => array(
					'description' => __( 'Customer login name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_user',
					),
				),
				'password'           => array(
					'description' => __( 'Customer password.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'edit' ),
				),
				'billing'            => array(
					'description' => __( 'List of billing address data.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'properties'  => array(
						'first_name' => array(
							'description' => __( 'First name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'last_name'  => array(
							'description' => __( 'Last name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'company'    => array(
							'description' => __( 'Company name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'address_1'  => array(
							'description' => __( 'Address line 1', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'address_2'  => array(
							'description' => __( 'Address line 2', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'city'       => array(
							'description' => __( 'City name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'state'      => array(
							'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'postcode'   => array(
							'description' => __( 'Postal code.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'country'    => array(
							'description' => __( 'ISO code of the country.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'email'      => array(
							'description' => __( 'Email address.', 'woocommerce' ),
							'type'        => 'string',
							'format'      => 'email',
							'context'     => array( 'view', 'edit' ),
						),
						'phone'      => array(
							'description' => __( 'Phone number.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
					),
				),
				'shipping'           => array(
					'description' => __( 'List of shipping address data.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'properties'  => array(
						'first_name' => array(
							'description' => __( 'First name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'last_name'  => array(
							'description' => __( 'Last name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'company'    => array(
							'description' => __( 'Company name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'address_1'  => array(
							'description' => __( 'Address line 1', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'address_2'  => array(
							'description' => __( 'Address line 2', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'city'       => array(
							'description' => __( 'City name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'state'      => array(
							'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'postcode'   => array(
							'description' => __( 'Postal code.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'country'    => array(
							'description' => __( 'ISO code of the country.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
					),
				),
				'is_paying_customer' => array(
					'description' => __( 'Is the customer a paying customer?', 'woocommerce' ),
					'type'        => 'bool',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'orders_count'       => array(
					'description' => __( 'Quantity of orders made by the customer.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'total_spent'        => array(
					'description' => __( 'Total amount spent.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'avatar_url'         => array(
					'description' => __( 'Avatar URL.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'meta_data'          => array(
					'description' => __( 'Meta data.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'    => array(
								'description' => __( 'Meta ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'key'   => array(
								'description' => __( 'Meta key.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'value' => array(
								'description' => __( 'Meta value.', 'woocommerce' ),
								'type'        => 'mixed',
								'context'     => array( 'view', 'edit' ),
							),
						),
					),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version2/class-wc-rest-system-status-tools-v2-controller.php000064400000053503151336044760024233 0ustar00<?php
/**
 * REST API WC System Status Tools Controller
 *
 * Handles requests to the /system_status/tools/* endpoints.
 *
 * @package WooCommerce\RestApi
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * System status tools controller.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Controller
 */
class WC_REST_System_Status_Tools_V2_Controller extends WC_REST_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'system_status/tools';

	/**
	 * Register the routes for /system_status/tools/*.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\w-]+)',
			array(
				'args'   => array(
					'id' => array(
						'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
						'type'        => 'string',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_item' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Check whether a given request has permission to view system status tools.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_items_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'system_status', 'read' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}
		return true;
	}

	/**
	 * Check whether a given request has permission to view a specific system status tool.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_item_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'system_status', 'read' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}
		return true;
	}

	/**
	 * Check whether a given request has permission to execute a specific system status tool.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function update_item_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'system_status', 'edit' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot update resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}
		return true;
	}

	/**
	 * A list of available tools for use in the system status section.
	 * 'button' becomes 'action' in the API.
	 *
	 * @return array
	 */
	public function get_tools() {
		$tools = array(
			'clear_transients'                   => array(
				'name'   => __( 'WooCommerce transients', 'woocommerce' ),
				'button' => __( 'Clear transients', 'woocommerce' ),
				'desc'   => __( 'This tool will clear the product/shop transients cache.', 'woocommerce' ),
			),
			'clear_expired_transients'           => array(
				'name'   => __( 'Expired transients', 'woocommerce' ),
				'button' => __( 'Clear transients', 'woocommerce' ),
				'desc'   => __( 'This tool will clear ALL expired transients from WordPress.', 'woocommerce' ),
			),
			'delete_orphaned_variations'         => array(
				'name'   => __( 'Orphaned variations', 'woocommerce' ),
				'button' => __( 'Delete orphaned variations', 'woocommerce' ),
				'desc'   => __( 'This tool will delete all variations which have no parent.', 'woocommerce' ),
			),
			'clear_expired_download_permissions' => array(
				'name'   => __( 'Used-up download permissions', 'woocommerce' ),
				'button' => __( 'Clean up download permissions', 'woocommerce' ),
				'desc'   => __( 'This tool will delete expired download permissions and permissions with 0 remaining downloads.', 'woocommerce' ),
			),
			'regenerate_product_lookup_tables'   => array(
				'name'   => __( 'Product lookup tables', 'woocommerce' ),
				'button' => __( 'Regenerate', 'woocommerce' ),
				'desc'   => __( 'This tool will regenerate product lookup table data. This process may take a while.', 'woocommerce' ),
			),
			'recount_terms'                      => array(
				'name'   => __( 'Term counts', 'woocommerce' ),
				'button' => __( 'Recount terms', 'woocommerce' ),
				'desc'   => __( 'This tool will recount product terms - useful when changing your settings in a way which hides products from the catalog.', 'woocommerce' ),
			),
			'reset_roles'                        => array(
				'name'   => __( 'Capabilities', 'woocommerce' ),
				'button' => __( 'Reset capabilities', 'woocommerce' ),
				'desc'   => __( 'This tool will reset the admin, customer and shop_manager roles to default. Use this if your users cannot access all of the WooCommerce admin pages.', 'woocommerce' ),
			),
			'clear_sessions'                     => array(
				'name'   => __( 'Clear customer sessions', 'woocommerce' ),
				'button' => __( 'Clear', 'woocommerce' ),
				'desc'   => sprintf(
					'<strong class="red">%1$s</strong> %2$s',
					__( 'Note:', 'woocommerce' ),
					__( 'This tool will delete all customer session data from the database, including current carts and saved carts in the database.', 'woocommerce' )
				),
			),
			'clear_template_cache'               => array(
				'name'   => __( 'Clear template cache', 'woocommerce' ),
				'button' => __( 'Clear', 'woocommerce' ),
				'desc'   => sprintf(
					'<strong class="red">%1$s</strong> %2$s',
					__( 'Note:', 'woocommerce' ),
					__( 'This tool will empty the template cache.', 'woocommerce' )
				),
			),
			'install_pages'                      => array(
				'name'   => __( 'Create default WooCommerce pages', 'woocommerce' ),
				'button' => __( 'Create pages', 'woocommerce' ),
				'desc'   => sprintf(
					'<strong class="red">%1$s</strong> %2$s',
					__( 'Note:', 'woocommerce' ),
					__( 'This tool will install all the missing WooCommerce pages. Pages already defined and set up will not be replaced.', 'woocommerce' )
				),
			),
			'delete_taxes'                       => array(
				'name'   => __( 'Delete WooCommerce tax rates', 'woocommerce' ),
				'button' => __( 'Delete tax rates', 'woocommerce' ),
				'desc'   => sprintf(
					'<strong class="red">%1$s</strong> %2$s',
					__( 'Note:', 'woocommerce' ),
					__( 'This option will delete ALL of your tax rates, use with caution. This action cannot be reversed.', 'woocommerce' )
				),
			),
			'regenerate_thumbnails'              => array(
				'name'   => __( 'Regenerate shop thumbnails', 'woocommerce' ),
				'button' => __( 'Regenerate', 'woocommerce' ),
				'desc'   => __( 'This will regenerate all shop thumbnails to match your theme and/or image settings.', 'woocommerce' ),
			),
			'db_update_routine'                  => array(
				'name'   => __( 'Update database', 'woocommerce' ),
				'button' => __( 'Update database', 'woocommerce' ),
				'desc'   => sprintf(
					'<strong class="red">%1$s</strong> %2$s',
					__( 'Note:', 'woocommerce' ),
					__( 'This tool will update your WooCommerce database to the latest version. Please ensure you make sufficient backups before proceeding.', 'woocommerce' )
				),
			),
		);
		if ( method_exists( 'WC_Install', 'verify_base_tables' ) ) {
			$tools['verify_db_tables'] = array(
				'name'   => __( 'Verify base database tables', 'woocommerce' ),
				'button' => __( 'Verify database', 'woocommerce' ),
				'desc'   => sprintf(
					__( 'Verify if all base database tables are present.', 'woocommerce' )
				),
			);
		}

		// Jetpack does the image resizing heavy lifting so you don't have to.
		if ( ( class_exists( 'Jetpack' ) && Jetpack::is_module_active( 'photon' ) ) || ! apply_filters( 'woocommerce_background_image_regeneration', true ) ) {
			unset( $tools['regenerate_thumbnails'] );
		}

		if ( ! function_exists( 'wc_clear_template_cache' ) ) {
			unset( $tools['clear_template_cache'] );
		}

		return apply_filters( 'woocommerce_debug_tools', $tools );
	}

	/**
	 * Get a list of system status tools.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_items( $request ) {
		$tools = array();
		foreach ( $this->get_tools() as $id => $tool ) {
			$tools[] = $this->prepare_response_for_collection(
				$this->prepare_item_for_response(
					array(
						'id'          => $id,
						'name'        => $tool['name'],
						'action'      => $tool['button'],
						'description' => $tool['desc'],
					),
					$request
				)
			);
		}

		$response = rest_ensure_response( $tools );
		return $response;
	}

	/**
	 * Return a single tool.
	 *
	 * @param  WP_REST_Request $request Request data.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_item( $request ) {
		$tools = $this->get_tools();
		if ( empty( $tools[ $request['id'] ] ) ) {
			return new WP_Error( 'woocommerce_rest_system_status_tool_invalid_id', __( 'Invalid tool ID.', 'woocommerce' ), array( 'status' => 404 ) );
		}
		$tool = $tools[ $request['id'] ];
		return rest_ensure_response(
			$this->prepare_item_for_response(
				array(
					'id'          => $request['id'],
					'name'        => $tool['name'],
					'action'      => $tool['button'],
					'description' => $tool['desc'],
				),
				$request
			)
		);
	}

	/**
	 * Update (execute) a tool.
	 *
	 * @param  WP_REST_Request $request Request data.
	 * @return WP_Error|WP_REST_Response
	 */
	public function update_item( $request ) {
		$tools = $this->get_tools();
		if ( empty( $tools[ $request['id'] ] ) ) {
			return new WP_Error( 'woocommerce_rest_system_status_tool_invalid_id', __( 'Invalid tool ID.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$tool = $tools[ $request['id'] ];
		$tool = array(
			'id'          => $request['id'],
			'name'        => $tool['name'],
			'action'      => $tool['button'],
			'description' => $tool['desc'],
		);

		$execute_return = $this->execute_tool( $request['id'] );
		$tool           = array_merge( $tool, $execute_return );

		/**
		 * Fires after a WooCommerce REST system status tool has been executed.
		 *
		 * @param array           $tool    Details about the tool that has been executed.
		 * @param WP_REST_Request $request The current WP_REST_Request object.
		 */
		do_action( 'woocommerce_rest_insert_system_status_tool', $tool, $request );

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_item_for_response( $tool, $request );
		return rest_ensure_response( $response );
	}

	/**
	 * Prepare a tool item for serialization.
	 *
	 * @param  array           $item     Object.
	 * @param  WP_REST_Request $request  Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $item, $request ) {
		$context = empty( $request['context'] ) ? 'view' : $request['context'];
		$data    = $this->add_additional_fields_to_object( $item, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $item['id'] ) );

		return $response;
	}

	/**
	 * Get the system status tools schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'system_status_tool',
			'type'       => 'object',
			'properties' => array(
				'id'          => array(
					'description' => __( 'A unique identifier for the tool.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_title',
					),
				),
				'name'        => array(
					'description' => __( 'Tool name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				'action'      => array(
					'description' => __( 'What running the tool will do.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				'description' => array(
					'description' => __( 'Tool description.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				'success'     => array(
					'description' => __( 'Did the tool run successfully?', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'edit' ),
				),
				'message'     => array(
					'description' => __( 'Tool return message.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param string $id ID.
	 * @return array
	 */
	protected function prepare_links( $id ) {
		$base  = '/' . $this->namespace . '/' . $this->rest_base;
		$links = array(
			'item' => array(
				'href'       => rest_url( trailingslashit( $base ) . $id ),
				'embeddable' => true,
			),
		);

		return $links;
	}

	/**
	 * Get any query params needed.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		return array(
			'context' => $this->get_context_param( array( 'default' => 'view' ) ),
		);
	}

	/**
	 * Actually executes a tool.
	 *
	 * @param  string $tool Tool.
	 * @return array
	 */
	public function execute_tool( $tool ) {
		global $wpdb;
		$ran = true;
		switch ( $tool ) {
			case 'clear_transients':
				wc_delete_product_transients();
				wc_delete_shop_order_transients();
				delete_transient( 'wc_count_comments' );
				delete_transient( 'as_comment_count' );

				$attribute_taxonomies = wc_get_attribute_taxonomies();

				if ( $attribute_taxonomies ) {
					foreach ( $attribute_taxonomies as $attribute ) {
						delete_transient( 'wc_layered_nav_counts_pa_' . $attribute->attribute_name );
					}
				}

				WC_Cache_Helper::get_transient_version( 'shipping', true );
				$message = __( 'Product transients cleared', 'woocommerce' );
				break;

			case 'clear_expired_transients':
				/* translators: %d: amount of expired transients */
				$message = sprintf( __( '%d transients rows cleared', 'woocommerce' ), wc_delete_expired_transients() );
				break;

			case 'delete_orphaned_variations':
				// Delete orphans.
				$result = absint(
					$wpdb->query(
						"DELETE products
					FROM {$wpdb->posts} products
					LEFT JOIN {$wpdb->posts} wp ON wp.ID = products.post_parent
					WHERE wp.ID IS NULL AND products.post_type = 'product_variation';"
					)
				);
				/* translators: %d: amount of orphaned variations */
				$message = sprintf( __( '%d orphaned variations deleted', 'woocommerce' ), $result );
				break;

			case 'clear_expired_download_permissions':
				// Delete expired download permissions and ones with 0 downloads remaining.
				$result = absint(
					$wpdb->query(
						$wpdb->prepare(
							"DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions
							WHERE ( downloads_remaining != '' AND downloads_remaining = 0 ) OR ( access_expires IS NOT NULL AND access_expires < %s )",
							gmdate( 'Y-m-d', current_time( 'timestamp' ) )
						)
					)
				);
				/* translators: %d: amount of permissions */
				$message = sprintf( __( '%d permissions deleted', 'woocommerce' ), $result );
				break;

			case 'regenerate_product_lookup_tables':
				if ( ! wc_update_product_lookup_tables_is_running() ) {
					wc_update_product_lookup_tables();
				}
				$message = __( 'Lookup tables are regenerating', 'woocommerce' );
				break;
			case 'reset_roles':
				// Remove then re-add caps and roles.
				WC_Install::remove_roles();
				WC_Install::create_roles();
				$message = __( 'Roles successfully reset', 'woocommerce' );
				break;

			case 'recount_terms':
				wc_recount_all_terms();
				$message = __( 'Terms successfully recounted', 'woocommerce' );
				break;

			case 'clear_sessions':
				$wpdb->query( "TRUNCATE {$wpdb->prefix}woocommerce_sessions" );
				$result = absint( $wpdb->query( "DELETE FROM {$wpdb->usermeta} WHERE meta_key='_woocommerce_persistent_cart_" . get_current_blog_id() . "';" ) ); // WPCS: unprepared SQL ok.
				wp_cache_flush();
				/* translators: %d: amount of sessions */
				$message = sprintf( __( 'Deleted all active sessions, and %d saved carts.', 'woocommerce' ), absint( $result ) );
				break;

			case 'install_pages':
				WC_Install::create_pages();
				$message = __( 'All missing WooCommerce pages successfully installed', 'woocommerce' );
				break;

			case 'delete_taxes':
				$wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}woocommerce_tax_rates;" );
				$wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}woocommerce_tax_rate_locations;" );

				if ( method_exists( 'WC_Cache_Helper', 'invalidate_cache_group' ) ) {
					WC_Cache_Helper::invalidate_cache_group( 'taxes' );
				} else {
					WC_Cache_Helper::incr_cache_prefix( 'taxes' );
				}
				$message = __( 'Tax rates successfully deleted', 'woocommerce' );
				break;

			case 'regenerate_thumbnails':
				WC_Regenerate_Images::queue_image_regeneration();
				$message = __( 'Thumbnail regeneration has been scheduled to run in the background.', 'woocommerce' );
				break;

			case 'db_update_routine':
				$blog_id = get_current_blog_id();
				// Used to fire an action added in WP_Background_Process::_construct() that calls WP_Background_Process::handle_cron_healthcheck().
				// This method will make sure the database updates are executed even if cron is disabled. Nothing will happen if the updates are already running.
				do_action( 'wp_' . $blog_id . '_wc_updater_cron' );
				$message = __( 'Database upgrade routine has been scheduled to run in the background.', 'woocommerce' );
				break;

			case 'clear_template_cache':
				if ( function_exists( 'wc_clear_template_cache' ) ) {
					wc_clear_template_cache();
					$message = __( 'Template cache cleared.', 'woocommerce' );
				} else {
					$message = __( 'The active version of WooCommerce does not support template cache clearing.', 'woocommerce' );
					$ran     = false;
				}
				break;

			case 'verify_db_tables':
				if ( ! method_exists( 'WC_Install', 'verify_base_tables' ) ) {
					$message = __( 'You need WooCommerce 4.2 or newer to run this tool.', 'woocommerce' );
					$ran     = false;
					break;
				}
				// Try to manually create table again.
				$missing_tables = WC_Install::verify_base_tables( true, true );
				if ( 0 === count( $missing_tables ) ) {
					$message = __( 'Database verified successfully.', 'woocommerce' );
				} else {
					$message  = __( 'Verifying database... One or more tables are still missing: ', 'woocommerce' );
					$message .= implode( ', ', $missing_tables );
					$ran      = false;
				}
				break;

			default:
				$tools = $this->get_tools();
				if ( isset( $tools[ $tool ]['callback'] ) ) {
					$callback = $tools[ $tool ]['callback'];
					try {
						$return = call_user_func( $callback );
					} catch ( Exception $exception ) {
						$return = $exception;
					}
					if ( is_a( $return, Exception::class ) ) {
						$callback_string = $this->get_printable_callback_name( $callback, $tool );
						$ran             = false;
						/* translators: %1$s: callback string, %2$s: error message */
						$message = sprintf( __( 'There was an error calling %1$s: %2$s', 'woocommerce' ), $callback_string, $return->getMessage() );

						$logger = wc_get_logger();
						$logger->error(
							sprintf(
								'Error running debug tool %s: %s',
								$tool,
								$return->getMessage()
							),
							array(
								'source'   => 'run-debug-tool',
								'tool'     => $tool,
								'callback' => $callback,
								'error'    => $return,
							)
						);
					} elseif ( is_string( $return ) ) {
						$message = $return;
					} elseif ( false === $return ) {
						$callback_string = $this->get_printable_callback_name( $callback, $tool );
						$ran             = false;
						/* translators: %s: callback string */
						$message = sprintf( __( 'There was an error calling %s', 'woocommerce' ), $callback_string );
					} else {
						$message = __( 'Tool ran.', 'woocommerce' );
					}
				} else {
					$ran     = false;
					$message = __( 'There was an error calling this tool. There is no callback present.', 'woocommerce' );
				}
				break;
		}

		return array(
			'success' => $ran,
			'message' => $message,
		);
	}

	/**
	 * Get a printable name for a callback.
	 *
	 * @param mixed  $callback The callback to get a name for.
	 * @param string $default The default name, to be returned when the callback is an inline function.
	 * @return string A printable name for the callback.
	 */
	private function get_printable_callback_name( $callback, $default ) {
		if ( is_array( $callback ) ) {
			return get_class( $callback[0] ) . '::' . $callback[1];
		}
		if ( is_string( $callback ) ) {
			return $callback;
		}

		return $default;
	}
}
rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php000064400000171610151336044760021526 0ustar00<?php
/**
 * REST API Orders controller
 *
 * Handles requests to the /orders endpoint.
 *
 * @package WooCommerce\RestApi
 * @since    2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Orders controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_CRUD_Controller
 */
class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'orders';

	/**
	 * Post type.
	 *
	 * @var string
	 */
	protected $post_type = 'shop_order';

	/**
	 * If object is hierarchical.
	 *
	 * @var bool
	 */
	protected $hierarchical = true;

	/**
	 * Stores the request.
	 *
	 * @var array
	 */
	protected $request = array();

	/**
	 * Register the routes for orders.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_item' ),
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\d]+)',
			array(
				'args'   => array(
					'id' => array(
						'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => array(
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_item' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_item' ),
					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
					'args'                => array(
						'force' => array(
							'default'     => false,
							'type'        => 'boolean',
							'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ),
						),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/batch',
			array(
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'batch_items' ),
					'permission_callback' => array( $this, 'batch_items_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				'schema' => array( $this, 'get_public_batch_schema' ),
			)
		);
	}

	/**
	 * Get object. Return false if object is not of required type.
	 *
	 * @since  3.0.0
	 * @param  int $id Object ID.
	 * @return WC_Data|bool
	 */
	protected function get_object( $id ) {
		$order = wc_get_order( $id );
		// In case id is a refund's id (or it's not an order at all), don't expose it via /orders/ path.
		if ( ! $order || 'shop_order_refund' === $order->get_type() ) {
			return false;
		}

		return $order;
	}

	/**
	 * Expands an order item to get its data.
	 *
	 * @param WC_Order_item $item Order item data.
	 * @return array
	 */
	protected function get_order_item_data( $item ) {
		$data           = $item->get_data();
		$format_decimal = array( 'subtotal', 'subtotal_tax', 'total', 'total_tax', 'tax_total', 'shipping_tax_total' );

		// Format decimal values.
		foreach ( $format_decimal as $key ) {
			if ( isset( $data[ $key ] ) ) {
				$data[ $key ] = wc_format_decimal( $data[ $key ], $this->request['dp'] );
			}
		}

		// Add SKU and PRICE to products.
		if ( is_callable( array( $item, 'get_product' ) ) ) {
			$data['sku']   = $item->get_product() ? $item->get_product()->get_sku() : null;
			$data['price'] = $item->get_quantity() ? $item->get_total() / $item->get_quantity() : 0;
		}

		// Add parent_name if the product is a variation.
		if ( is_callable( array( $item, 'get_product' ) ) ) {
			$product = $item->get_product();

			if ( is_callable( array( $product, 'get_parent_data' ) ) ) {
				$data['parent_name'] = $product->get_title();
			} else {
				$data['parent_name'] = null;
			}
		}

		// Format taxes.
		if ( ! empty( $data['taxes']['total'] ) ) {
			$taxes = array();

			foreach ( $data['taxes']['total'] as $tax_rate_id => $tax ) {
				$taxes[] = array(
					'id'       => $tax_rate_id,
					'total'    => $tax,
					'subtotal' => isset( $data['taxes']['subtotal'][ $tax_rate_id ] ) ? $data['taxes']['subtotal'][ $tax_rate_id ] : '',
				);
			}
			$data['taxes'] = $taxes;
		} elseif ( isset( $data['taxes'] ) ) {
			$data['taxes'] = array();
		}

		// Remove names for coupons, taxes and shipping.
		if ( isset( $data['code'] ) || isset( $data['rate_code'] ) || isset( $data['method_title'] ) ) {
			unset( $data['name'] );
		}

		// Remove props we don't want to expose.
		unset( $data['order_id'] );
		unset( $data['type'] );

		// Expand meta_data to include user-friendly values.
		$formatted_meta_data = $item->get_formatted_meta_data( null, true );
		$data['meta_data'] = array_map(
			array( $this, 'merge_meta_item_with_formatted_meta_display_attributes' ),
			$data['meta_data'],
			array_fill( 0, count( $data['meta_data'] ), $formatted_meta_data )
		);

		return $data;
	}

	/**
	 * Merge the `$formatted_meta_data` `display_key` and `display_value` attribute values into the corresponding
	 * {@link WC_Meta_Data}. Returns the merged array.
	 *
	 * @param WC_Meta_Data $meta_item           An object from {@link WC_Order_Item::get_meta_data()}.
	 * @param array        $formatted_meta_data An object result from {@link WC_Order_Item::get_formatted_meta_data}.
	 * The keys are the IDs of {@link WC_Meta_Data}.
	 *
	 * @return array
	 */
	private function merge_meta_item_with_formatted_meta_display_attributes( $meta_item, $formatted_meta_data ) {
		$result = array(
			'id'            => $meta_item->id,
			'key'           => $meta_item->key,
			'value'         => $meta_item->value,
			'display_key'   => $meta_item->key,   // Default to original key, in case a formatted key is not available.
			'display_value' => $meta_item->value, // Default to original value, in case a formatted value is not available.
		);

		if ( array_key_exists( $meta_item->id, $formatted_meta_data ) ) {
			$formatted_meta_item = $formatted_meta_data[ $meta_item->id ];

			$result['display_key'] = wc_clean( $formatted_meta_item->display_key );
			$result['display_value'] = wc_clean( $formatted_meta_item->display_value );
		}

		return $result;
	}

	/**
	 * Get formatted item data.
	 *
	 * @since 3.0.0
	 * @param WC_Order $order WC_Data instance.
	 *
	 * @return array
	 */
	protected function get_formatted_item_data( $order ) {
		$extra_fields      = array( 'meta_data', 'line_items', 'tax_lines', 'shipping_lines', 'fee_lines', 'coupon_lines', 'refunds' );
		$format_decimal    = array( 'discount_total', 'discount_tax', 'shipping_total', 'shipping_tax', 'shipping_total', 'shipping_tax', 'cart_tax', 'total', 'total_tax' );
		$format_date       = array( 'date_created', 'date_modified', 'date_completed', 'date_paid' );
		// These fields are dependent on other fields.
		$dependent_fields = array(
			'date_created_gmt'   => 'date_created',
			'date_modified_gmt'  => 'date_modified',
			'date_completed_gmt' => 'date_completed',
			'date_paid_gmt'      => 'date_paid',
		);

		$format_line_items = array( 'line_items', 'tax_lines', 'shipping_lines', 'fee_lines', 'coupon_lines' );

		// Only fetch fields that we need.
		$fields = $this->get_fields_for_response( $this->request );
		foreach ( $dependent_fields as $field_key => $dependency ) {
			if ( in_array( $field_key, $fields ) && ! in_array( $dependency, $fields ) ) {
				$fields[] = $dependency;
			}
		}

		$extra_fields      = array_intersect( $extra_fields, $fields );
		$format_decimal    = array_intersect( $format_decimal, $fields );
		$format_date       = array_intersect( $format_date, $fields );

		$format_line_items = array_intersect( $format_line_items, $fields );

		$data = $order->get_base_data();

		// Add extra data as necessary.
		foreach ( $extra_fields as $field ) {
			switch ( $field ) {
				case 'meta_data':
					$data['meta_data'] = $order->get_meta_data();
					break;
				case 'line_items':
					$data['line_items'] = $order->get_items( 'line_item' );
					break;
				case 'tax_lines':
					$data['tax_lines'] = $order->get_items( 'tax' );
					break;
				case 'shipping_lines':
					$data['shipping_lines'] = $order->get_items( 'shipping' );
					break;
				case 'fee_lines':
					$data['fee_lines'] = $order->get_items( 'fee' );
					break;
				case 'coupon_lines':
					$data['coupon_lines'] = $order->get_items( 'coupon' );
					break;
				case 'refunds':
					$data['refunds'] = array();
					foreach ( $order->get_refunds() as $refund ) {
						$data['refunds'][] = array(
							'id'     => $refund->get_id(),
							'reason' => $refund->get_reason() ? $refund->get_reason() : '',
							'total'  => '-' . wc_format_decimal( $refund->get_amount(), $this->request['dp'] ),
						);
					}
					break;
			}
		}

		// Format decimal values.
		foreach ( $format_decimal as $key ) {
			$data[ $key ] = wc_format_decimal( $data[ $key ], $this->request['dp'] );
		}

		// Format date values.
		foreach ( $format_date as $key ) {
			$datetime              = $data[ $key ];
			$data[ $key ]          = wc_rest_prepare_date_response( $datetime, false );
			$data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime );
		}

		// Format the order status.
		$data['status'] = 'wc-' === substr( $data['status'], 0, 3 ) ? substr( $data['status'], 3 ) : $data['status'];

		// Format line items.
		foreach ( $format_line_items as $key ) {
			$data[ $key ] = array_values( array_map( array( $this, 'get_order_item_data' ), $data[ $key ] ) );
		}

		$allowed_fields = array(
			'id',
			'parent_id',
			'number',
			'order_key',
			'created_via',
			'version',
			'status',
			'currency',
			'date_created',
			'date_created_gmt',
			'date_modified',
			'date_modified_gmt',
			'discount_total',
			'discount_tax',
			'shipping_total',
			'shipping_tax',
			'cart_tax',
			'total',
			'total_tax',
			'prices_include_tax',
			'customer_id',
			'customer_ip_address',
			'customer_user_agent',
			'customer_note',
			'billing',
			'shipping',
			'payment_method',
			'payment_method_title',
			'transaction_id',
			'date_paid',
			'date_paid_gmt',
			'date_completed',
			'date_completed_gmt',
			'cart_hash',
			'meta_data',
			'line_items',
			'tax_lines',
			'shipping_lines',
			'fee_lines',
			'coupon_lines',
			'refunds',
		);

		$data = array_intersect_key( $data, array_flip( $allowed_fields ) );

		return $data;
	}

	/**
	 * Prepare a single order output for response.
	 *
	 * @since  3.0.0
	 * @param  WC_Data         $object  Object data.
	 * @param  WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_object_for_response( $object, $request ) {
		$this->request       = $request;
		$this->request['dp'] = is_null( $this->request['dp'] ) ? wc_get_price_decimals() : absint( $this->request['dp'] );
		$request['context']  = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data                = $this->get_formatted_item_data( $object );
		$data                = $this->add_additional_fields_to_object( $data, $request );
		$data                = $this->filter_response_by_context( $data, $request['context'] );
		$response            = rest_ensure_response( $data );
		$response->add_links( $this->prepare_links( $object, $request ) );

		/**
		 * Filter the data for a response.
		 *
		 * The dynamic portion of the hook name, $this->post_type,
		 * refers to object type being prepared for the response.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param WC_Data          $object   Object data.
		 * @param WP_REST_Request  $request  Request object.
		 */
		return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param WC_Data         $object  Object data.
	 * @param WP_REST_Request $request Request object.
	 * @return array                   Links for the given post.
	 */
	protected function prepare_links( $object, $request ) {
		$links = array(
			'self'       => array(
				'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ),
			),
			'collection' => array(
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
			),
		);

		if ( 0 !== (int) $object->get_customer_id() ) {
			$links['customer'] = array(
				'href' => rest_url( sprintf( '/%s/customers/%d', $this->namespace, $object->get_customer_id() ) ),
			);
		}

		if ( 0 !== (int) $object->get_parent_id() ) {
			$links['up'] = array(
				'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $object->get_parent_id() ) ),
			);
		}

		return $links;
	}

	/**
	 * Prepare objects query.
	 *
	 * @since  3.0.0
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return array
	 */
	protected function prepare_objects_query( $request ) {
		global $wpdb;

		$args = parent::prepare_objects_query( $request );

		// Set post_status.
		if ( in_array( $request['status'], $this->get_order_statuses(), true ) ) {
			$args['post_status'] = 'wc-' . $request['status'];
		} elseif ( 'any' === $request['status'] ) {
			$args['post_status'] = 'any';
		} else {
			$args['post_status'] = $request['status'];
		}

		if ( isset( $request['customer'] ) ) {
			if ( ! empty( $args['meta_query'] ) ) {
				$args['meta_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
			}

			$args['meta_query'][] = array(
				'key'   => '_customer_user',
				'value' => $request['customer'],
				'type'  => 'NUMERIC',
			);
		}

		// Search by product.
		if ( ! empty( $request['product'] ) ) {
			$order_ids = $wpdb->get_col(
				$wpdb->prepare(
					"SELECT order_id
					FROM {$wpdb->prefix}woocommerce_order_items
					WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d )
					AND order_item_type = 'line_item'",
					$request['product']
				)
			);

			// Force WP_Query return empty if don't found any order.
			$order_ids = ! empty( $order_ids ) ? $order_ids : array( 0 );

			$args['post__in'] = $order_ids;
		}

		// Search.
		if ( ! empty( $args['s'] ) ) {
			$order_ids = wc_order_search( $args['s'] );

			if ( ! empty( $order_ids ) ) {
				unset( $args['s'] );
				$args['post__in'] = array_merge( $order_ids, array( 0 ) );
			}
		}

		/**
		 * Filter the query arguments for a request.
		 *
		 * Enables adding extra arguments or setting defaults for an order collection request.
		 *
		 * @param array           $args    Key value array of query var to query value.
		 * @param WP_REST_Request $request The request used.
		 */
		$args = apply_filters( 'woocommerce_rest_orders_prepare_object_query', $args, $request );

		return $args;
	}

	/**
	 * Only return writable props from schema.
	 *
	 * @param  array $schema Schema.
	 * @return bool
	 */
	protected function filter_writable_props( $schema ) {
		return empty( $schema['readonly'] );
	}

	/**
	 * Prepare a single order for create or update.
	 *
	 * @param  WP_REST_Request $request Request object.
	 * @param  bool            $creating If is creating a new object.
	 * @return WP_Error|WC_Data
	 */
	protected function prepare_object_for_database( $request, $creating = false ) {
		$id        = isset( $request['id'] ) ? absint( $request['id'] ) : 0;
		$order     = new WC_Order( $id );
		$schema    = $this->get_item_schema();
		$data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) );

		// Handle all writable props.
		foreach ( $data_keys as $key ) {
			$value = $request[ $key ];

			if ( ! is_null( $value ) ) {
				switch ( $key ) {
					case 'status':
						// Status change should be done later so transitions have new data.
						break;
					case 'billing':
					case 'shipping':
						$this->update_address( $order, $value, $key );
						break;
					case 'line_items':
					case 'shipping_lines':
					case 'fee_lines':
					case 'coupon_lines':
						if ( is_array( $value ) ) {
							foreach ( $value as $item ) {
								if ( is_array( $item ) ) {
									if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) {
										$order->remove_item( $item['id'] );
									} else {
										$this->set_item( $order, $key, $item );
									}
								}
							}
						}
						break;
					case 'meta_data':
						if ( is_array( $value ) ) {
							foreach ( $value as $meta ) {
								$order->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
							}
						}
						break;
					default:
						if ( is_callable( array( $order, "set_{$key}" ) ) ) {
							$order->{"set_{$key}"}( $value );
						}
						break;
				}
			}
		}

		/**
		 * Filters an object before it is inserted via the REST API.
		 *
		 * The dynamic portion of the hook name, `$this->post_type`,
		 * refers to the object type slug.
		 *
		 * @param WC_Data         $order    Object object.
		 * @param WP_REST_Request $request  Request object.
		 * @param bool            $creating If is creating a new object.
		 */
		return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $order, $request, $creating );
	}

	/**
	 * Save an object data.
	 *
	 * @since  3.0.0
	 * @throws WC_REST_Exception But all errors are validated before returning any data.
	 * @param  WP_REST_Request $request  Full details about the request.
	 * @param  bool            $creating If is creating a new object.
	 * @return WC_Data|WP_Error
	 */
	protected function save_object( $request, $creating = false ) {
		try {
			$object = $this->prepare_object_for_database( $request, $creating );

			if ( is_wp_error( $object ) ) {
				return $object;
			}

			// Make sure gateways are loaded so hooks from gateways fire on save/create.
			WC()->payment_gateways();

			if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] ) {
				// Make sure customer exists.
				if ( false === get_user_by( 'id', $request['customer_id'] ) ) {
					throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 );
				}

				// Make sure customer is part of blog.
				if ( is_multisite() && ! is_user_member_of_blog( $request['customer_id'] ) ) {
					add_user_to_blog( get_current_blog_id(), $request['customer_id'], 'customer' );
				}
			}

			if ( $creating ) {
				$object->set_created_via( 'rest-api' );
				$object->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) );
				$object->calculate_totals();
			} else {
				// If items have changed, recalculate order totals.
				if ( isset( $request['billing'] ) || isset( $request['shipping'] ) || isset( $request['line_items'] ) || isset( $request['shipping_lines'] ) || isset( $request['fee_lines'] ) || isset( $request['coupon_lines'] ) ) {
					$object->calculate_totals( true );
				}
			}

			// Set status.
			if ( ! empty( $request['status'] ) ) {
				$object->set_status( $request['status'] );
			}

			$object->save();

			// Actions for after the order is saved.
			if ( true === $request['set_paid'] ) {
				if ( $creating || $object->needs_payment() ) {
					$object->payment_complete( $request['transaction_id'] );
				}
			}

			return $this->get_object( $object->get_id() );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
		} catch ( WC_REST_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Update address.
	 *
	 * @param WC_Order $order  Order data.
	 * @param array    $posted Posted data.
	 * @param string   $type   Address type.
	 */
	protected function update_address( $order, $posted, $type = 'billing' ) {
		foreach ( $posted as $key => $value ) {
			if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) {
				$order->{"set_{$type}_{$key}"}( $value );
			}
		}
	}

	/**
	 * Gets the product ID from the SKU or posted ID.
	 *
	 * @throws WC_REST_Exception When SKU or ID is not valid.
	 * @param array  $posted Request data.
	 * @param string $action 'create' to add line item or 'update' to update it.
	 * @return int
	 */
	protected function get_product_id( $posted, $action = 'create' ) {
		if ( ! empty( $posted['sku'] ) ) {
			$product_id = (int) wc_get_product_id_by_sku( $posted['sku'] );
		} elseif ( ! empty( $posted['product_id'] ) && empty( $posted['variation_id'] ) ) {
			$product_id = (int) $posted['product_id'];
		} elseif ( ! empty( $posted['variation_id'] ) ) {
			$product_id = (int) $posted['variation_id'];
		} elseif ( 'update' === $action ) {
			$product_id = 0;
		} else {
			throw new WC_REST_Exception( 'woocommerce_rest_required_product_reference', __( 'Product ID or SKU is required.', 'woocommerce' ), 400 );
		}
		return $product_id;
	}

	/**
	 * Maybe set an item prop if the value was posted.
	 *
	 * @param WC_Order_Item $item   Order item.
	 * @param string        $prop   Order property.
	 * @param array         $posted Request data.
	 */
	protected function maybe_set_item_prop( $item, $prop, $posted ) {
		if ( isset( $posted[ $prop ] ) ) {
			$item->{"set_$prop"}( $posted[ $prop ] );
		}
	}

	/**
	 * Maybe set item props if the values were posted.
	 *
	 * @param WC_Order_Item $item   Order item data.
	 * @param string[]      $props  Properties.
	 * @param array         $posted Request data.
	 */
	protected function maybe_set_item_props( $item, $props, $posted ) {
		foreach ( $props as $prop ) {
			$this->maybe_set_item_prop( $item, $prop, $posted );
		}
	}

	/**
	 * Maybe set item meta if posted.
	 *
	 * @param WC_Order_Item $item   Order item data.
	 * @param array         $posted Request data.
	 */
	protected function maybe_set_item_meta_data( $item, $posted ) {
		if ( ! empty( $posted['meta_data'] ) && is_array( $posted['meta_data'] ) ) {
			foreach ( $posted['meta_data'] as $meta ) {
				if ( isset( $meta['key'] ) ) {
					$value = isset( $meta['value'] ) ? $meta['value'] : null;
					$item->update_meta_data( $meta['key'], $value, isset( $meta['id'] ) ? $meta['id'] : '' );
				}
			}
		}
	}

	/**
	 * Create or update a line item.
	 *
	 * @param array  $posted Line item data.
	 * @param string $action 'create' to add line item or 'update' to update it.
	 * @param object $item Passed when updating an item. Null during creation.
	 * @return WC_Order_Item_Product
	 * @throws WC_REST_Exception Invalid data, server error.
	 */
	protected function prepare_line_items( $posted, $action = 'create', $item = null ) {
		$item    = is_null( $item ) ? new WC_Order_Item_Product( ! empty( $posted['id'] ) ? $posted['id'] : '' ) : $item;
		$product = wc_get_product( $this->get_product_id( $posted, $action ) );

		if ( $product && $product !== $item->get_product() ) {
			$item->set_product( $product );

			if ( 'create' === $action ) {
				$quantity = isset( $posted['quantity'] ) ? $posted['quantity'] : 1;
				$total    = wc_get_price_excluding_tax( $product, array( 'qty' => $quantity ) );
				$item->set_total( $total );
				$item->set_subtotal( $total );
			}
		}

		$this->maybe_set_item_props( $item, array( 'name', 'quantity', 'total', 'subtotal', 'tax_class' ), $posted );
		$this->maybe_set_item_meta_data( $item, $posted );

		return $item;
	}

	/**
	 * Create or update an order shipping method.
	 *
	 * @param array  $posted $shipping Item data.
	 * @param string $action 'create' to add shipping or 'update' to update it.
	 * @param object $item Passed when updating an item. Null during creation.
	 * @return WC_Order_Item_Shipping
	 * @throws WC_REST_Exception Invalid data, server error.
	 */
	protected function prepare_shipping_lines( $posted, $action = 'create', $item = null ) {
		$item = is_null( $item ) ? new WC_Order_Item_Shipping( ! empty( $posted['id'] ) ? $posted['id'] : '' ) : $item;

		if ( 'create' === $action ) {
			if ( empty( $posted['method_id'] ) ) {
				throw new WC_REST_Exception( 'woocommerce_rest_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 );
			}
		}

		$this->maybe_set_item_props( $item, array( 'method_id', 'method_title', 'total', 'instance_id' ), $posted );
		$this->maybe_set_item_meta_data( $item, $posted );

		return $item;
	}

	/**
	 * Create or update an order fee.
	 *
	 * @param array  $posted Item data.
	 * @param string $action 'create' to add fee or 'update' to update it.
	 * @param object $item Passed when updating an item. Null during creation.
	 * @return WC_Order_Item_Fee
	 * @throws WC_REST_Exception Invalid data, server error.
	 */
	protected function prepare_fee_lines( $posted, $action = 'create', $item = null ) {
		$item = is_null( $item ) ? new WC_Order_Item_Fee( ! empty( $posted['id'] ) ? $posted['id'] : '' ) : $item;

		if ( 'create' === $action ) {
			if ( empty( $posted['name'] ) ) {
				throw new WC_REST_Exception( 'woocommerce_rest_invalid_fee_item', __( 'Fee name is required.', 'woocommerce' ), 400 );
			}
		}

		$this->maybe_set_item_props( $item, array( 'name', 'tax_class', 'tax_status', 'total' ), $posted );
		$this->maybe_set_item_meta_data( $item, $posted );

		return $item;
	}

	/**
	 * Create or update an order coupon.
	 *
	 * @param array  $posted Item data.
	 * @param string $action 'create' to add coupon or 'update' to update it.
	 * @param object $item Passed when updating an item. Null during creation.
	 * @return WC_Order_Item_Coupon
	 * @throws WC_REST_Exception Invalid data, server error.
	 */
	protected function prepare_coupon_lines( $posted, $action = 'create', $item = null ) {
		$item = is_null( $item ) ? new WC_Order_Item_Coupon( ! empty( $posted['id'] ) ? $posted['id'] : '' ) : $item;

		if ( 'create' === $action ) {
			if ( empty( $posted['code'] ) ) {
				throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 );
			}
		}

		$this->maybe_set_item_props( $item, array( 'code', 'discount' ), $posted );
		$this->maybe_set_item_meta_data( $item, $posted );

		return $item;
	}

	/**
	 * Wrapper method to create/update order items.
	 * When updating, the item ID provided is checked to ensure it is associated
	 * with the order.
	 *
	 * @param WC_Order $order order object.
	 * @param string   $item_type The item type.
	 * @param array    $posted item provided in the request body.
	 * @throws WC_REST_Exception If item ID is not associated with order.
	 */
	protected function set_item( $order, $item_type, $posted ) {
		global $wpdb;

		if ( ! empty( $posted['id'] ) ) {
			$action = 'update';
		} else {
			$action = 'create';
		}

		$method = 'prepare_' . $item_type;
		$item   = null;

		// Verify provided line item ID is associated with order.
		if ( 'update' === $action ) {
			$item = $order->get_item( absint( $posted['id'] ), false );

			if ( ! $item ) {
				throw new WC_REST_Exception( 'woocommerce_rest_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 );
			}
		}

		// Prepare item data.
		$item = $this->$method( $posted, $action, $item );

		do_action( 'woocommerce_rest_set_order_item', $item, $posted );

		// If creating the order, add the item to it.
		if ( 'create' === $action ) {
			$order->add_item( $item );
		} else {
			$item->save();
		}
	}

	/**
	 * Helper method to check if the resource ID associated with the provided item is null.
	 * Items can be deleted by setting the resource ID to null.
	 *
	 * @param array $item Item provided in the request body.
	 * @return bool True if the item resource ID is null, false otherwise.
	 */
	protected function item_is_null( $item ) {
		$keys = array( 'product_id', 'method_id', 'method_title', 'name', 'code' );

		foreach ( $keys as $key ) {
			if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Get order statuses without prefixes.
	 *
	 * @return array
	 */
	protected function get_order_statuses() {
		$order_statuses = array();

		foreach ( array_keys( wc_get_order_statuses() ) as $status ) {
			$order_statuses[] = str_replace( 'wc-', '', $status );
		}

		return $order_statuses;
	}

	/**
	 * Get the Order's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => $this->post_type,
			'type'       => 'object',
			'properties' => array(
				'id'                   => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'parent_id'            => array(
					'description' => __( 'Parent order ID.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'number'               => array(
					'description' => __( 'Order number.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'order_key'            => array(
					'description' => __( 'Order key.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'created_via'          => array(
					'description' => __( 'Shows where the order was created.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'version'              => array(
					'description' => __( 'Version of WooCommerce which last updated the order.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'status'               => array(
					'description' => __( 'Order status.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'pending',
					'enum'        => $this->get_order_statuses(),
					'context'     => array( 'view', 'edit' ),
				),
				'currency'             => array(
					'description' => __( 'Currency the order was created with, in ISO format.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => get_woocommerce_currency(),
					'enum'        => array_keys( get_woocommerce_currencies() ),
					'context'     => array( 'view', 'edit' ),
				),
				'date_created'         => array(
					'description' => __( "The date the order was created, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_created_gmt'     => array(
					'description' => __( 'The date the order was created, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_modified'        => array(
					'description' => __( "The date the order was last modified, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_modified_gmt'    => array(
					'description' => __( 'The date the order was last modified, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'discount_total'       => array(
					'description' => __( 'Total discount amount for the order.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'discount_tax'         => array(
					'description' => __( 'Total discount tax amount for the order.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'shipping_total'       => array(
					'description' => __( 'Total shipping amount for the order.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'shipping_tax'         => array(
					'description' => __( 'Total shipping tax amount for the order.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'cart_tax'             => array(
					'description' => __( 'Sum of line item taxes only.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'total'                => array(
					'description' => __( 'Grand total.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'total_tax'            => array(
					'description' => __( 'Sum of all taxes.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'prices_include_tax'   => array(
					'description' => __( 'True the prices included tax during checkout.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'customer_id'          => array(
					'description' => __( 'User ID who owns the order. 0 for guests.', 'woocommerce' ),
					'type'        => 'integer',
					'default'     => 0,
					'context'     => array( 'view', 'edit' ),
				),
				'customer_ip_address'  => array(
					'description' => __( "Customer's IP address.", 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'customer_user_agent'  => array(
					'description' => __( 'User agent of the customer.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'customer_note'        => array(
					'description' => __( 'Note left by customer during checkout.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'billing'              => array(
					'description' => __( 'Billing address.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'properties'  => array(
						'first_name' => array(
							'description' => __( 'First name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'last_name'  => array(
							'description' => __( 'Last name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'company'    => array(
							'description' => __( 'Company name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'address_1'  => array(
							'description' => __( 'Address line 1', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'address_2'  => array(
							'description' => __( 'Address line 2', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'city'       => array(
							'description' => __( 'City name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'state'      => array(
							'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'postcode'   => array(
							'description' => __( 'Postal code.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'country'    => array(
							'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'email'      => array(
							'description' => __( 'Email address.', 'woocommerce' ),
							'type'        => array( 'string', 'null' ),
							'format'      => 'email',
							'context'     => array( 'view', 'edit' ),
						),
						'phone'      => array(
							'description' => __( 'Phone number.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
					),
				),
				'shipping'             => array(
					'description' => __( 'Shipping address.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'properties'  => array(
						'first_name' => array(
							'description' => __( 'First name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'last_name'  => array(
							'description' => __( 'Last name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'company'    => array(
							'description' => __( 'Company name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'address_1'  => array(
							'description' => __( 'Address line 1', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'address_2'  => array(
							'description' => __( 'Address line 2', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'city'       => array(
							'description' => __( 'City name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'state'      => array(
							'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'postcode'   => array(
							'description' => __( 'Postal code.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'country'    => array(
							'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
					),
				),
				'payment_method'       => array(
					'description' => __( 'Payment method ID.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'payment_method_title' => array(
					'description' => __( 'Payment method title.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				'transaction_id'       => array(
					'description' => __( 'Unique transaction ID.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'date_paid'            => array(
					'description' => __( "The date the order was paid, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_paid_gmt'        => array(
					'description' => __( 'The date the order was paid, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_completed'       => array(
					'description' => __( "The date the order was completed, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_completed_gmt'   => array(
					'description' => __( 'The date the order was completed, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'cart_hash'            => array(
					'description' => __( 'MD5 hash of cart items to ensure orders are not modified.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'meta_data'            => array(
					'description' => __( 'Meta data.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'    => array(
								'description' => __( 'Meta ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'key'   => array(
								'description' => __( 'Meta key.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'value' => array(
								'description' => __( 'Meta value.', 'woocommerce' ),
								'type'        => 'mixed',
								'context'     => array( 'view', 'edit' ),
							),
						),
					),
				),
				'line_items'           => array(
					'description' => __( 'Line items data.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'           => array(
								'description' => __( 'Item ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'name'         => array(
								'description' => __( 'Product name.', 'woocommerce' ),
								'type'        => 'mixed',
								'context'     => array( 'view', 'edit' ),
							),
							'parent_name'  => array(
								'description' => __( 'Parent product name if the product is a variation.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'product_id'   => array(
								'description' => __( 'Product ID.', 'woocommerce' ),
								'type'        => 'mixed',
								'context'     => array( 'view', 'edit' ),
							),
							'variation_id' => array(
								'description' => __( 'Variation ID, if applicable.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'quantity'     => array(
								'description' => __( 'Quantity ordered.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'tax_class'    => array(
								'description' => __( 'Tax class of product.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'subtotal'     => array(
								'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'subtotal_tax' => array(
								'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'total'        => array(
								'description' => __( 'Line total (after discounts).', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'total_tax'    => array(
								'description' => __( 'Line total tax (after discounts).', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'taxes'        => array(
								'description' => __( 'Line taxes.', 'woocommerce' ),
								'type'        => 'array',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
								'items'       => array(
									'type'       => 'object',
									'properties' => array(
										'id'       => array(
											'description' => __( 'Tax rate ID.', 'woocommerce' ),
											'type'        => 'integer',
											'context'     => array( 'view', 'edit' ),
										),
										'total'    => array(
											'description' => __( 'Tax total.', 'woocommerce' ),
											'type'        => 'string',
											'context'     => array( 'view', 'edit' ),
										),
										'subtotal' => array(
											'description' => __( 'Tax subtotal.', 'woocommerce' ),
											'type'        => 'string',
											'context'     => array( 'view', 'edit' ),
										),
									),
								),
							),
							'meta_data'    => array(
								'description' => __( 'Meta data.', 'woocommerce' ),
								'type'        => 'array',
								'context'     => array( 'view', 'edit' ),
								'items'       => array(
									'type'       => 'object',
									'properties' => array(
										'id'            => array(
											'description' => __( 'Meta ID.', 'woocommerce' ),
											'type'        => 'integer',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
										'key'           => array(
											'description' => __( 'Meta key.', 'woocommerce' ),
											'type'        => 'string',
											'context'     => array( 'view', 'edit' ),
										),
										'value'         => array(
											'description' => __( 'Meta value.', 'woocommerce' ),
											'type'        => 'mixed',
											'context'     => array( 'view', 'edit' ),
										),
										'display_key'   => array(
											'description' => __( 'Meta key for UI display.', 'woocommerce' ),
											'type'        => 'string',
											'context'     => array( 'view', 'edit' ),
										),
										'display_value' => array(
											'description' => __( 'Meta value for UI display.', 'woocommerce' ),
											'type'        => 'string',
											'context'     => array( 'view', 'edit' ),
										),
									),
								),
							),
							'sku'          => array(
								'description' => __( 'Product SKU.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'price'        => array(
								'description' => __( 'Product price.', 'woocommerce' ),
								'type'        => 'number',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
						),
					),
				),
				'tax_lines'            => array(
					'description' => __( 'Tax lines data.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'                 => array(
								'description' => __( 'Item ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'rate_code'          => array(
								'description' => __( 'Tax rate code.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'rate_id'            => array(
								'description' => __( 'Tax rate ID.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'label'              => array(
								'description' => __( 'Tax rate label.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'compound'           => array(
								'description' => __( 'Show if is a compound tax rate.', 'woocommerce' ),
								'type'        => 'boolean',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'tax_total'          => array(
								'description' => __( 'Tax total (not including shipping taxes).', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'shipping_tax_total' => array(
								'description' => __( 'Shipping tax total.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'meta_data'          => array(
								'description' => __( 'Meta data.', 'woocommerce' ),
								'type'        => 'array',
								'context'     => array( 'view', 'edit' ),
								'items'       => array(
									'type'       => 'object',
									'properties' => array(
										'id'    => array(
											'description' => __( 'Meta ID.', 'woocommerce' ),
											'type'        => 'integer',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
										'key'   => array(
											'description' => __( 'Meta key.', 'woocommerce' ),
											'type'        => 'string',
											'context'     => array( 'view', 'edit' ),
										),
										'value' => array(
											'description' => __( 'Meta value.', 'woocommerce' ),
											'type'        => 'mixed',
											'context'     => array( 'view', 'edit' ),
										),
									),
								),
							),
						),
					),
				),
				'shipping_lines'       => array(
					'description' => __( 'Shipping lines data.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'           => array(
								'description' => __( 'Item ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'method_title' => array(
								'description' => __( 'Shipping method name.', 'woocommerce' ),
								'type'        => 'mixed',
								'context'     => array( 'view', 'edit' ),
							),
							'method_id'    => array(
								'description' => __( 'Shipping method ID.', 'woocommerce' ),
								'type'        => 'mixed',
								'context'     => array( 'view', 'edit' ),
							),
							'instance_id'  => array(
								'description' => __( 'Shipping instance ID.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'total'        => array(
								'description' => __( 'Line total (after discounts).', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'total_tax'    => array(
								'description' => __( 'Line total tax (after discounts).', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'taxes'        => array(
								'description' => __( 'Line taxes.', 'woocommerce' ),
								'type'        => 'array',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
								'items'       => array(
									'type'       => 'object',
									'properties' => array(
										'id'    => array(
											'description' => __( 'Tax rate ID.', 'woocommerce' ),
											'type'        => 'integer',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
										'total' => array(
											'description' => __( 'Tax total.', 'woocommerce' ),
											'type'        => 'string',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
									),
								),
							),
							'meta_data'    => array(
								'description' => __( 'Meta data.', 'woocommerce' ),
								'type'        => 'array',
								'context'     => array( 'view', 'edit' ),
								'items'       => array(
									'type'       => 'object',
									'properties' => array(
										'id'    => array(
											'description' => __( 'Meta ID.', 'woocommerce' ),
											'type'        => 'integer',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
										'key'   => array(
											'description' => __( 'Meta key.', 'woocommerce' ),
											'type'        => 'string',
											'context'     => array( 'view', 'edit' ),
										),
										'value' => array(
											'description' => __( 'Meta value.', 'woocommerce' ),
											'type'        => 'mixed',
											'context'     => array( 'view', 'edit' ),
										),
									),
								),
							),
						),
					),
				),
				'fee_lines'            => array(
					'description' => __( 'Fee lines data.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'         => array(
								'description' => __( 'Item ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'name'       => array(
								'description' => __( 'Fee name.', 'woocommerce' ),
								'type'        => 'mixed',
								'context'     => array( 'view', 'edit' ),
							),
							'tax_class'  => array(
								'description' => __( 'Tax class of fee.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'tax_status' => array(
								'description' => __( 'Tax status of fee.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'enum'        => array( 'taxable', 'none' ),
							),
							'total'      => array(
								'description' => __( 'Line total (after discounts).', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'total_tax'  => array(
								'description' => __( 'Line total tax (after discounts).', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'taxes'      => array(
								'description' => __( 'Line taxes.', 'woocommerce' ),
								'type'        => 'array',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
								'items'       => array(
									'type'       => 'object',
									'properties' => array(
										'id'       => array(
											'description' => __( 'Tax rate ID.', 'woocommerce' ),
											'type'        => 'integer',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
										'total'    => array(
											'description' => __( 'Tax total.', 'woocommerce' ),
											'type'        => 'string',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
										'subtotal' => array(
											'description' => __( 'Tax subtotal.', 'woocommerce' ),
											'type'        => 'string',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
									),
								),
							),
							'meta_data'  => array(
								'description' => __( 'Meta data.', 'woocommerce' ),
								'type'        => 'array',
								'context'     => array( 'view', 'edit' ),
								'items'       => array(
									'type'       => 'object',
									'properties' => array(
										'id'    => array(
											'description' => __( 'Meta ID.', 'woocommerce' ),
											'type'        => 'integer',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
										'key'   => array(
											'description' => __( 'Meta key.', 'woocommerce' ),
											'type'        => 'string',
											'context'     => array( 'view', 'edit' ),
										),
										'value' => array(
											'description' => __( 'Meta value.', 'woocommerce' ),
											'type'        => 'mixed',
											'context'     => array( 'view', 'edit' ),
										),
									),
								),
							),
						),
					),
				),
				'coupon_lines'         => array(
					'description' => __( 'Coupons line data.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'           => array(
								'description' => __( 'Item ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'code'         => array(
								'description' => __( 'Coupon code.', 'woocommerce' ),
								'type'        => 'mixed',
								'context'     => array( 'view', 'edit' ),
							),
							'discount'     => array(
								'description' => __( 'Discount total.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'discount_tax' => array(
								'description' => __( 'Discount total tax.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'meta_data'    => array(
								'description' => __( 'Meta data.', 'woocommerce' ),
								'type'        => 'array',
								'context'     => array( 'view', 'edit' ),
								'items'       => array(
									'type'       => 'object',
									'properties' => array(
										'id'    => array(
											'description' => __( 'Meta ID.', 'woocommerce' ),
											'type'        => 'integer',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
										'key'   => array(
											'description' => __( 'Meta key.', 'woocommerce' ),
											'type'        => 'string',
											'context'     => array( 'view', 'edit' ),
										),
										'value' => array(
											'description' => __( 'Meta value.', 'woocommerce' ),
											'type'        => 'mixed',
											'context'     => array( 'view', 'edit' ),
										),
									),
								),
							),
						),
					),
				),
				'refunds'              => array(
					'description' => __( 'List of refunds.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'     => array(
								'description' => __( 'Refund ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'reason' => array(
								'description' => __( 'Refund reason.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'total'  => array(
								'description' => __( 'Refund total.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
						),
					),
				),
				'set_paid'             => array(
					'description' => __( 'Define if the order is paid. It will set the status to processing and reduce stock items.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'edit' ),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params = parent::get_collection_params();

		$params['status']   = array(
			'default'           => 'any',
			'description'       => __( 'Limit result set to orders assigned a specific status.', 'woocommerce' ),
			'type'              => 'string',
			'enum'              => array_merge( array( 'any', 'trash' ), $this->get_order_statuses() ),
			'sanitize_callback' => 'sanitize_key',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['customer'] = array(
			'description'       => __( 'Limit result set to orders assigned a specific customer.', 'woocommerce' ),
			'type'              => 'integer',
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['product']  = array(
			'description'       => __( 'Limit result set to orders assigned a specific product.', 'woocommerce' ),
			'type'              => 'integer',
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['dp']       = array(
			'default'           => wc_get_price_decimals(),
			'description'       => __( 'Number of decimal points to use in each resource.', 'woocommerce' ),
			'type'              => 'integer',
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}
}
rest-api/Controllers/Version2/class-wc-rest-tax-classes-v2-controller.php000064400000005656151336044760022465 0ustar00<?php
/**
 * REST API Tax Classes controller
 *
 * Handles requests to the /taxes/classes endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Tax Classes controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Tax_Classes_V1_Controller
 */
class WC_REST_Tax_Classes_V2_Controller extends WC_REST_Tax_Classes_V1_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';

	/**
	 * Register the routes for tax classes.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_item' ),
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<slug>\w[\w\s\-]*)',
			array(
				'args' => array(
					'slug' => array(
						'description' => __( 'Unique slug for the resource.', 'woocommerce' ),
						'type'        => 'string',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_item' ),
					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
					'args'                => array(
						'force' => array(
							'default'     => false,
							'type'        => 'boolean',
							'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
						),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Get one tax class.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return array
	 */
	public function get_item( $request ) {
		if ( 'standard' === $request['slug'] ) {
			$tax_class = array(
				'slug' => 'standard',
				'name' => __( 'Standard rate', 'woocommerce' ),
			);
		} else {
			$tax_class = WC_Tax::get_tax_class_by( 'slug', sanitize_title( $request['slug'] ) );
		}

		$data = array();
		if ( $tax_class ) {
			$class  = $this->prepare_item_for_response( $tax_class, $request );
			$class  = $this->prepare_response_for_collection( $class );
			$data[] = $class;
		}

		return rest_ensure_response( $data );
	}
}
rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php000064400000232721151336044760022074 0ustar00<?php
/**
 * REST API Products controller
 *
 * Handles requests to the /products endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Products controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_CRUD_Controller
 */
class WC_REST_Products_V2_Controller extends WC_REST_CRUD_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'products';

	/**
	 * Post type.
	 *
	 * @var string
	 */
	protected $post_type = 'product';

	/**
	 * If object is hierarchical.
	 *
	 * @var bool
	 */
	protected $hierarchical = true;

	/**
	 * Initialize product actions.
	 */
	public function __construct() {
		add_action( "woocommerce_rest_insert_{$this->post_type}_object", array( $this, 'clear_transients' ) );
	}

	/**
	 * Register the routes for products.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_item' ),
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\d]+)',
			array(
				'args'   => array(
					'id' => array(
						'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => array(
						'context' => $this->get_context_param(
							array(
								'default' => 'view',
							)
						),
					),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_item' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_item' ),
					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
					'args'                => array(
						'force' => array(
							'default'     => false,
							'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ),
							'type'        => 'boolean',
						),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/batch',
			array(
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'batch_items' ),
					'permission_callback' => array( $this, 'batch_items_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				'schema' => array( $this, 'get_public_batch_schema' ),
			)
		);
	}

	/**
	 * Get object.
	 *
	 * @param int $id Object ID.
	 *
	 * @since  3.0.0
	 * @return WC_Data
	 */
	protected function get_object( $id ) {
		return wc_get_product( $id );
	}

	/**
	 * Prepare a single product output for response.
	 *
	 * @param WC_Data         $object  Object data.
	 * @param WP_REST_Request $request Request object.
	 *
	 * @since  3.0.0
	 * @return WP_REST_Response
	 */
	public function prepare_object_for_response( $object, $request ) {
		$context       = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$this->request = $request;
		$data          = $this->get_product_data( $object, $context, $request );

		// Add variations to variable products.
		if ( $object->is_type( 'variable' ) && $object->has_child() ) {
			$data['variations'] = $object->get_children();
		}

		// Add grouped products data.
		if ( $object->is_type( 'grouped' ) && $object->has_child() ) {
			$data['grouped_products'] = $object->get_children();
		}

		$data     = $this->add_additional_fields_to_object( $data, $request );
		$data     = $this->filter_response_by_context( $data, $context );
		$response = rest_ensure_response( $data );
		$response->add_links( $this->prepare_links( $object, $request ) );

		/**
		 * Filter the data for a response.
		 *
		 * The dynamic portion of the hook name, $this->post_type,
		 * refers to object type being prepared for the response.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param WC_Data          $object   Object data.
		 * @param WP_REST_Request  $request  Request object.
		 */
		return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request );
	}

	/**
	 * Prepare objects query.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 *
	 * @since  3.0.0
	 * @return array
	 */
	protected function prepare_objects_query( $request ) {
		$args = parent::prepare_objects_query( $request );

		// Set post_status.
		$args['post_status'] = $request['status'];

		// Taxonomy query to filter products by type, category,
		// tag, shipping class, and attribute.
		$tax_query = array();

		// Map between taxonomy name and arg's key.
		$taxonomies = array(
			'product_cat'            => 'category',
			'product_tag'            => 'tag',
			'product_shipping_class' => 'shipping_class',
		);

		// Set tax_query for each passed arg.
		foreach ( $taxonomies as $taxonomy => $key ) {
			if ( ! empty( $request[ $key ] ) ) {
				$tax_query[] = array(
					'taxonomy' => $taxonomy,
					'field'    => 'term_id',
					'terms'    => $request[ $key ],
				);
			}
		}

		// Filter product type by slug.
		if ( ! empty( $request['type'] ) ) {
			$tax_query[] = array(
				'taxonomy' => 'product_type',
				'field'    => 'slug',
				'terms'    => $request['type'],
			);
		}

		// Filter by attribute and term.
		if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) {
			if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names(), true ) ) {
				$tax_query[] = array(
					'taxonomy' => $request['attribute'],
					'field'    => 'term_id',
					'terms'    => $request['attribute_term'],
				);
			}
		}

		if ( ! empty( $tax_query ) ) {
			$args['tax_query'] = $tax_query; // WPCS: slow query ok.
		}

		// Filter featured.
		if ( is_bool( $request['featured'] ) ) {
			$args['tax_query'][] = array(
				'taxonomy' => 'product_visibility',
				'field'    => 'name',
				'terms'    => 'featured',
				'operator' => true === $request['featured'] ? 'IN' : 'NOT IN',
			);
		}

		// Filter by sku.
		if ( ! empty( $request['sku'] ) ) {
			$skus = explode( ',', $request['sku'] );
			// Include the current string as a SKU too.
			if ( 1 < count( $skus ) ) {
				$skus[] = $request['sku'];
			}

			$args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok.
				$args,
				array(
					'key'     => '_sku',
					'value'   => $skus,
					'compare' => 'IN',
				)
			);
		}

		// Filter by tax class.
		if ( ! empty( $request['tax_class'] ) ) {
			$args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok.
				$args,
				array(
					'key'   => '_tax_class',
					'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '',
				)
			);
		}

		// Price filter.
		if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) {
			$args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) );  // WPCS: slow query ok.
		}

		// Filter product in stock or out of stock.
		if ( is_bool( $request['in_stock'] ) ) {
			$args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok.
				$args,
				array(
					'key'   => '_stock_status',
					'value' => true === $request['in_stock'] ? 'instock' : 'outofstock',
				)
			);
		}

		// Filter by on sale products.
		if ( is_bool( $request['on_sale'] ) ) {
			$on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in';
			$on_sale_ids = wc_get_product_ids_on_sale();

			// Use 0 when there's no on sale products to avoid return all products.
			$on_sale_ids = empty( $on_sale_ids ) ? array( 0 ) : $on_sale_ids;

			$args[ $on_sale_key ] += $on_sale_ids;
		}

		// Force the post_type argument, since it's not a user input variable.
		if ( ! empty( $request['sku'] ) ) {
			$args['post_type'] = array( 'product', 'product_variation' );
		} else {
			$args['post_type'] = $this->post_type;
		}

		return $args;
	}

	/**
	 * Get the downloads for a product or product variation.
	 *
	 * @param WC_Product|WC_Product_Variation $product Product instance.
	 *
	 * @return array
	 */
	protected function get_downloads( $product ) {
		$downloads = array();

		if ( $product->is_downloadable() ) {
			foreach ( $product->get_downloads() as $file_id => $file ) {
				$downloads[] = array(
					'id'   => $file_id, // MD5 hash.
					'name' => $file['name'],
					'file' => $file['file'],
				);
			}
		}

		return $downloads;
	}

	/**
	 * Get taxonomy terms.
	 *
	 * @param WC_Product $product  Product instance.
	 * @param string     $taxonomy Taxonomy slug.
	 *
	 * @return array
	 */
	protected function get_taxonomy_terms( $product, $taxonomy = 'cat' ) {
		$terms = array();

		foreach ( wc_get_object_terms( $product->get_id(), 'product_' . $taxonomy ) as $term ) {
			$terms[] = array(
				'id'   => $term->term_id,
				'name' => $term->name,
				'slug' => $term->slug,
			);
		}

		return $terms;
	}

	/**
	 * Get the images for a product or product variation.
	 *
	 * @param WC_Product|WC_Product_Variation $product Product instance.
	 *
	 * @return array
	 */
	protected function get_images( $product ) {
		$images         = array();
		$attachment_ids = array();

		// Add featured image.
		if ( $product->get_image_id() ) {
			$attachment_ids[] = $product->get_image_id();
		}

		// Add gallery images.
		$attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() );

		// Build image data.
		foreach ( $attachment_ids as $position => $attachment_id ) {
			$attachment_post = get_post( $attachment_id );
			if ( is_null( $attachment_post ) ) {
				continue;
			}

			$attachment = wp_get_attachment_image_src( $attachment_id, 'full' );
			if ( ! is_array( $attachment ) ) {
				continue;
			}

			$images[] = array(
				'id'                => (int) $attachment_id,
				'date_created'      => wc_rest_prepare_date_response( $attachment_post->post_date, false ),
				'date_created_gmt'  => wc_rest_prepare_date_response( strtotime( $attachment_post->post_date_gmt ) ),
				'date_modified'     => wc_rest_prepare_date_response( $attachment_post->post_modified, false ),
				'date_modified_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_modified_gmt ) ),
				'src'               => current( $attachment ),
				'name'              => get_the_title( $attachment_id ),
				'alt'               => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ),
				'position'          => (int) $position,
			);
		}

		// Set a placeholder image if the product has no images set.
		if ( empty( $images ) ) {
			$images[] = array(
				'id'                => 0,
				'date_created'      => wc_rest_prepare_date_response( current_time( 'mysql' ), false ), // Default to now.
				'date_created_gmt'  => wc_rest_prepare_date_response( time() ), // Default to now.
				'date_modified'     => wc_rest_prepare_date_response( current_time( 'mysql' ), false ),
				'date_modified_gmt' => wc_rest_prepare_date_response( time() ),
				'src'               => wc_placeholder_img_src(),
				'name'              => __( 'Placeholder', 'woocommerce' ),
				'alt'               => __( 'Placeholder', 'woocommerce' ),
				'position'          => 0,
			);
		}

		return $images;
	}

	/**
	 * Get attribute taxonomy label.
	 *
	 * @param string $name Taxonomy name.
	 *
	 * @deprecated 3.0.0
	 * @return     string
	 */
	protected function get_attribute_taxonomy_label( $name ) {
		$tax    = get_taxonomy( $name );
		$labels = get_taxonomy_labels( $tax );

		return $labels->singular_name;
	}

	/**
	 * Get product attribute taxonomy name.
	 *
	 * @param string     $slug    Taxonomy name.
	 * @param WC_Product $product Product data.
	 *
	 * @since  3.0.0
	 * @return string
	 */
	protected function get_attribute_taxonomy_name( $slug, $product ) {
		// Format slug so it matches attributes of the product.
		$slug       = wc_attribute_taxonomy_slug( $slug );
		$attributes = $product->get_attributes();
		$attribute  = false;

		// pa_ attributes.
		if ( isset( $attributes[ wc_attribute_taxonomy_name( $slug ) ] ) ) {
			$attribute = $attributes[ wc_attribute_taxonomy_name( $slug ) ];
		} elseif ( isset( $attributes[ $slug ] ) ) {
			$attribute = $attributes[ $slug ];
		}

		if ( ! $attribute ) {
			return $slug;
		}

		// Taxonomy attribute name.
		if ( $attribute->is_taxonomy() ) {
			$taxonomy = $attribute->get_taxonomy_object();
			return $taxonomy->attribute_label;
		}

		// Custom product attribute name.
		return $attribute->get_name();
	}

	/**
	 * Get default attributes.
	 *
	 * @param WC_Product $product Product instance.
	 *
	 * @return array
	 */
	protected function get_default_attributes( $product ) {
		$default = array();

		if ( $product->is_type( 'variable' ) ) {
			foreach ( array_filter( (array) $product->get_default_attributes(), 'strlen' ) as $key => $value ) {
				if ( 0 === strpos( $key, 'pa_' ) ) {
					$default[] = array(
						'id'     => wc_attribute_taxonomy_id_by_name( $key ),
						'name'   => $this->get_attribute_taxonomy_name( $key, $product ),
						'option' => $value,
					);
				} else {
					$default[] = array(
						'id'     => 0,
						'name'   => $this->get_attribute_taxonomy_name( $key, $product ),
						'option' => $value,
					);
				}
			}
		}

		return $default;
	}

	/**
	 * Get attribute options.
	 *
	 * @param int   $product_id Product ID.
	 * @param array $attribute  Attribute data.
	 *
	 * @return array
	 */
	protected function get_attribute_options( $product_id, $attribute ) {
		if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) {
			return wc_get_product_terms(
				$product_id,
				$attribute['name'],
				array(
					'fields' => 'names',
				)
			);
		} elseif ( isset( $attribute['value'] ) ) {
			return array_map( 'trim', explode( '|', $attribute['value'] ) );
		}

		return array();
	}

	/**
	 * Get the attributes for a product or product variation.
	 *
	 * @param WC_Product|WC_Product_Variation $product Product instance.
	 *
	 * @return array
	 */
	protected function get_attributes( $product ) {
		$attributes = array();

		if ( $product->is_type( 'variation' ) ) {
			$_product = wc_get_product( $product->get_parent_id() );
			foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) {
				$name = str_replace( 'attribute_', '', $attribute_name );

				if ( empty( $attribute ) && '0' !== $attribute ) {
					continue;
				}

				// Taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_`.
				if ( 0 === strpos( $attribute_name, 'attribute_pa_' ) ) {
					$option_term  = get_term_by( 'slug', $attribute, $name );
					$attributes[] = array(
						'id'     => wc_attribute_taxonomy_id_by_name( $name ),
						'name'   => $this->get_attribute_taxonomy_name( $name, $_product ),
						'option' => $option_term && ! is_wp_error( $option_term ) ? $option_term->name : $attribute,
					);
				} else {
					$attributes[] = array(
						'id'     => 0,
						'name'   => $this->get_attribute_taxonomy_name( $name, $_product ),
						'option' => $attribute,
					);
				}
			}
		} else {
			foreach ( $product->get_attributes() as $attribute ) {
				$attributes[] = array(
					'id'        => $attribute['is_taxonomy'] ? wc_attribute_taxonomy_id_by_name( $attribute['name'] ) : 0,
					'name'      => $this->get_attribute_taxonomy_name( $attribute['name'], $product ),
					'position'  => (int) $attribute['position'],
					'visible'   => (bool) $attribute['is_visible'],
					'variation' => (bool) $attribute['is_variation'],
					'options'   => $this->get_attribute_options( $product->get_id(), $attribute ),
				);
			}
		}

		return $attributes;
	}

	/**
	 * Fetch price HTML.
	 *
	 * @param WC_Product $product Product object.
	 * @param string     $context Context of request, can be `view` or `edit`.
	 *
	 * @return string
	 */
	protected function api_get_price_html( $product, $context ) {
		return $product->get_price_html();
	}

	/**
	 * Fetch related IDs.
	 *
	 * @param WC_Product $product Product object.
	 * @param string     $context Context of request, can be `view` or `edit`.
	 *
	 * @return array
	 */
	protected function api_get_related_ids( $product, $context ) {
		return array_map( 'absint', array_values( wc_get_related_products( $product->get_id() ) ) );
	}

	/**
	 * Fetch meta data.
	 *
	 * @param WC_Product $product Product object.
	 * @param string     $context Context of request, can be `view` or `edit`.
	 *
	 * @return array
	 */
	protected function api_get_meta_data( $product, $context ) {
		return $product->get_meta_data();
	}

	/**
	 * Get product data.
	 *
	 * @param WC_Product $product Product instance.
	 * @param string     $context Request context. Options: 'view' and 'edit'.
	 *
	 * @return array
	 */
	protected function get_product_data( $product, $context = 'view' ) {
		/*
		 * @param WP_REST_Request $request Current request object. For backward compatibility, we pass this argument silently.
		 *
		 *  TODO: Refactor to fix this behavior when DI gets included to make it obvious and clean.
		*/
		$request = func_num_args() >= 3 ? func_get_arg( 2 ) : new WP_REST_Request( '', '', array( 'context' => $context ) );
		$fields  = $this->get_fields_for_response( $request );

		$base_data = array();
		foreach ( $fields as $field ) {
			switch ( $field ) {
				case 'id':
					$base_data['id'] = $product->get_id();
					break;
				case 'name':
					$base_data['name'] = $product->get_name( $context );
					break;
				case 'slug':
					$base_data['slug'] = $product->get_slug( $context );
					break;
				case 'permalink':
					$base_data['permalink'] = $product->get_permalink();
					break;
				case 'date_created':
					$base_data['date_created'] = wc_rest_prepare_date_response( $product->get_date_created( $context ), false );
					break;
				case 'date_created_gmt':
					$base_data['date_created_gmt'] = wc_rest_prepare_date_response( $product->get_date_created( $context ) );
					break;
				case 'date_modified':
					$base_data['date_modified'] = wc_rest_prepare_date_response( $product->get_date_modified( $context ), false );
					break;
				case 'date_modified_gmt':
					$base_data['date_modified_gmt'] = wc_rest_prepare_date_response( $product->get_date_modified( $context ) );
					break;
				case 'type':
					$base_data['type'] = $product->get_type();
					break;
				case 'status':
					$base_data['status'] = $product->get_status( $context );
					break;
				case 'featured':
					$base_data['featured'] = $product->is_featured();
					break;
				case 'catalog_visibility':
					$base_data['catalog_visibility'] = $product->get_catalog_visibility( $context );
					break;
				case 'description':
					$base_data['description'] = 'view' === $context ? wpautop( do_shortcode( $product->get_description() ) ) : $product->get_description( $context );
					break;
				case 'short_description':
					$base_data['short_description'] = 'view' === $context ? apply_filters( 'woocommerce_short_description', $product->get_short_description() ) : $product->get_short_description( $context );
					break;
				case 'sku':
					$base_data['sku'] = $product->get_sku( $context );
					break;
				case 'price':
					$base_data['price'] = $product->get_price( $context );
					break;
				case 'regular_price':
					$base_data['regular_price'] = $product->get_regular_price( $context );
					break;
				case 'sale_price':
					$base_data['sale_price'] = $product->get_sale_price( $context ) ? $product->get_sale_price( $context ) : '';
					break;
				case 'date_on_sale_from':
					$base_data['date_on_sale_from'] = wc_rest_prepare_date_response( $product->get_date_on_sale_from( $context ), false );
					break;
				case 'date_on_sale_from_gmt':
					$base_data['date_on_sale_from_gmt'] = wc_rest_prepare_date_response( $product->get_date_on_sale_from( $context ) );
					break;
				case 'date_on_sale_to':
					$base_data['date_on_sale_to'] = wc_rest_prepare_date_response( $product->get_date_on_sale_to( $context ), false );
					break;
				case 'date_on_sale_to_gmt':
					$base_data['date_on_sale_to_gmt'] = wc_rest_prepare_date_response( $product->get_date_on_sale_to( $context ) );
					break;
				case 'on_sale':
					$base_data['on_sale'] = $product->is_on_sale( $context );
					break;
				case 'purchasable':
					$base_data['purchasable'] = $product->is_purchasable();
					break;
				case 'total_sales':
					$base_data['total_sales'] = $product->get_total_sales( $context );
					break;
				case 'virtual':
					$base_data['virtual'] = $product->is_virtual();
					break;
				case 'downloadable':
					$base_data['downloadable'] = $product->is_downloadable();
					break;
				case 'downloads':
					$base_data['downloads'] = $this->get_downloads( $product );
					break;
				case 'download_limit':
					$base_data['download_limit'] = $product->get_download_limit( $context );
					break;
				case 'download_expiry':
					$base_data['download_expiry'] = $product->get_download_expiry( $context );
					break;
				case 'external_url':
					$base_data['external_url'] = $product->is_type( 'external' ) ? $product->get_product_url( $context ) : '';
					break;
				case 'button_text':
					$base_data['button_text'] = $product->is_type( 'external' ) ? $product->get_button_text( $context ) : '';
					break;
				case 'tax_status':
					$base_data['tax_status'] = $product->get_tax_status( $context );
					break;
				case 'tax_class':
					$base_data['tax_class'] = $product->get_tax_class( $context );
					break;
				case 'manage_stock':
					$base_data['manage_stock'] = $product->managing_stock();
					break;
				case 'stock_quantity':
					$base_data['stock_quantity'] = $product->get_stock_quantity( $context );
					break;
				case 'in_stock':
					$base_data['in_stock'] = $product->is_in_stock();
					break;
				case 'backorders':
					$base_data['backorders'] = $product->get_backorders( $context );
					break;
				case 'backorders_allowed':
					$base_data['backorders_allowed'] = $product->backorders_allowed();
					break;
				case 'backordered':
					$base_data['backordered'] = $product->is_on_backorder();
					break;
				case 'low_stock_amount':
					$base_data['low_stock_amount'] = '' === $product->get_low_stock_amount() ? null : $product->get_low_stock_amount();
					break;
				case 'sold_individually':
					$base_data['sold_individually'] = $product->is_sold_individually();
					break;
				case 'weight':
					$base_data['weight'] = $product->get_weight( $context );
					break;
				case 'dimensions':
					$base_data['dimensions'] = array(
						'length' => $product->get_length( $context ),
						'width'  => $product->get_width( $context ),
						'height' => $product->get_height( $context ),
					);
					break;
				case 'shipping_required':
					$base_data['shipping_required'] = $product->needs_shipping();
					break;
				case 'shipping_taxable':
					$base_data['shipping_taxable'] = $product->is_shipping_taxable();
					break;
				case 'shipping_class':
					$base_data['shipping_class'] = $product->get_shipping_class();
					break;
				case 'shipping_class_id':
					$base_data['shipping_class_id'] = $product->get_shipping_class_id( $context );
					break;
				case 'reviews_allowed':
					$base_data['reviews_allowed'] = $product->get_reviews_allowed( $context );
					break;
				case 'average_rating':
					$base_data['average_rating'] = 'view' === $context ? wc_format_decimal( $product->get_average_rating(), 2 ) : $product->get_average_rating( $context );
					break;
				case 'rating_count':
					$base_data['rating_count'] = $product->get_rating_count();
					break;
				case 'upsell_ids':
					$base_data['upsell_ids'] = array_map( 'absint', $product->get_upsell_ids( $context ) );
					break;
				case 'cross_sell_ids':
					$base_data['cross_sell_ids'] = array_map( 'absint', $product->get_cross_sell_ids( $context ) );
					break;
				case 'parent_id':
					$base_data['parent_id'] = $product->get_parent_id( $context );
					break;
				case 'purchase_note':
					$base_data['purchase_note'] = 'view' === $context ? wpautop( do_shortcode( wp_kses_post( $product->get_purchase_note() ) ) ) : $product->get_purchase_note( $context );
					break;
				case 'categories':
					$base_data['categories'] = $this->get_taxonomy_terms( $product );
					break;
				case 'tags':
					$base_data['tags'] = $this->get_taxonomy_terms( $product, 'tag' );
					break;
				case 'images':
					$base_data['images'] = $this->get_images( $product );
					break;
				case 'attributes':
					$base_data['attributes'] = $this->get_attributes( $product );
					break;
				case 'default_attributes':
					$base_data['default_attributes'] = $this->get_default_attributes( $product );
					break;
				case 'variations':
					$base_data['variations'] = array();
					break;
				case 'grouped_products':
					$base_data['grouped_products'] = array();
					break;
				case 'menu_order':
					$base_data['menu_order'] = $product->get_menu_order( $context );
					break;
			}
		}

		$data = array_merge(
			$base_data,
			$this->fetch_fields_using_getters( $product, $context, $fields )
		);

		return $data;
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param WC_Data         $object  Object data.
	 * @param WP_REST_Request $request Request object.
	 *
	 * @return array Links for the given post.
	 */
	protected function prepare_links( $object, $request ) {
		$links = array(
			'self'       => array(
				'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ),  // @codingStandardsIgnoreLine.
			),
			'collection' => array(
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),  // @codingStandardsIgnoreLine.
			),
		);

		if ( $object->get_parent_id() ) {
			$links['up'] = array(
				'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $object->get_parent_id() ) ),  // @codingStandardsIgnoreLine.
			);
		}

		return $links;
	}

	/**
	 * Prepare a single product for create or update.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @param bool            $creating If is creating a new object.
	 *
	 * @return WP_Error|WC_Data
	 */
	protected function prepare_object_for_database( $request, $creating = false ) {
		$id = isset( $request['id'] ) ? absint( $request['id'] ) : 0;

		// Type is the most important part here because we need to be using the correct class and methods.
		if ( isset( $request['type'] ) ) {
			$classname = WC_Product_Factory::get_classname_from_product_type( $request['type'] );

			if ( ! class_exists( $classname ) ) {
				$classname = 'WC_Product_Simple';
			}

			$product = new $classname( $id );
		} elseif ( isset( $request['id'] ) ) {
			$product = wc_get_product( $id );
		} else {
			$product = new WC_Product_Simple();
		}

		if ( 'variation' === $product->get_type() ) {
			return new WP_Error(
				"woocommerce_rest_invalid_{$this->post_type}_id",
				__( 'To manipulate product variations you should use the /products/&lt;product_id&gt;/variations/&lt;id&gt; endpoint.', 'woocommerce' ),
				array(
					'status' => 404,
				)
			);
		}

		// Post title.
		if ( isset( $request['name'] ) ) {
			$product->set_name( wp_filter_post_kses( $request['name'] ) );
		}

		// Post content.
		if ( isset( $request['description'] ) ) {
			$product->set_description( wp_filter_post_kses( $request['description'] ) );
		}

		// Post excerpt.
		if ( isset( $request['short_description'] ) ) {
			$product->set_short_description( wp_filter_post_kses( $request['short_description'] ) );
		}

		// Post status.
		if ( isset( $request['status'] ) ) {
			$product->set_status( get_post_status_object( $request['status'] ) ? $request['status'] : 'draft' );
		}

		// Post slug.
		if ( isset( $request['slug'] ) ) {
			$product->set_slug( $request['slug'] );
		}

		// Menu order.
		if ( isset( $request['menu_order'] ) ) {
			$product->set_menu_order( $request['menu_order'] );
		}

		// Comment status.
		if ( isset( $request['reviews_allowed'] ) ) {
			$product->set_reviews_allowed( $request['reviews_allowed'] );
		}

		// Virtual.
		if ( isset( $request['virtual'] ) ) {
			$product->set_virtual( $request['virtual'] );
		}

		// Tax status.
		if ( isset( $request['tax_status'] ) ) {
			$product->set_tax_status( $request['tax_status'] );
		}

		// Tax Class.
		if ( isset( $request['tax_class'] ) ) {
			$product->set_tax_class( $request['tax_class'] );
		}

		// Catalog Visibility.
		if ( isset( $request['catalog_visibility'] ) ) {
			$product->set_catalog_visibility( $request['catalog_visibility'] );
		}

		// Purchase Note.
		if ( isset( $request['purchase_note'] ) ) {
			$product->set_purchase_note( wp_kses_post( wp_unslash( $request['purchase_note'] ) ) );
		}

		// Featured Product.
		if ( isset( $request['featured'] ) ) {
			$product->set_featured( $request['featured'] );
		}

		// Shipping data.
		$product = $this->save_product_shipping_data( $product, $request );

		// SKU.
		if ( isset( $request['sku'] ) ) {
			$product->set_sku( wc_clean( $request['sku'] ) );
		}

		// Attributes.
		if ( isset( $request['attributes'] ) ) {
			$attributes = array();

			foreach ( $request['attributes'] as $attribute ) {
				$attribute_id   = 0;
				$attribute_name = '';

				// Check ID for global attributes or name for product attributes.
				if ( ! empty( $attribute['id'] ) ) {
					$attribute_id   = absint( $attribute['id'] );
					$attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
				} elseif ( ! empty( $attribute['name'] ) ) {
					$attribute_name = wc_clean( $attribute['name'] );
				}

				if ( ! $attribute_id && ! $attribute_name ) {
					continue;
				}

				if ( $attribute_id ) {

					if ( isset( $attribute['options'] ) ) {
						$options = $attribute['options'];

						if ( ! is_array( $attribute['options'] ) ) {
							// Text based attributes - Posted values are term names.
							$options = explode( WC_DELIMITER, $options );
						}

						$values = array_map( 'wc_sanitize_term_text_based', $options );
						$values = array_filter( $values, 'strlen' );
					} else {
						$values = array();
					}

					if ( ! empty( $values ) ) {
						// Add attribute to array, but don't set values.
						$attribute_object = new WC_Product_Attribute();
						$attribute_object->set_id( $attribute_id );
						$attribute_object->set_name( $attribute_name );
						$attribute_object->set_options( $values );
						$attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' );
						$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
						$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
						$attributes[] = $attribute_object;
					}
				} elseif ( isset( $attribute['options'] ) ) {
					// Custom attribute - Add attribute to array and set the values.
					if ( is_array( $attribute['options'] ) ) {
						$values = $attribute['options'];
					} else {
						$values = explode( WC_DELIMITER, $attribute['options'] );
					}
					$attribute_object = new WC_Product_Attribute();
					$attribute_object->set_name( $attribute_name );
					$attribute_object->set_options( $values );
					$attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' );
					$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
					$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
					$attributes[] = $attribute_object;
				}
			}
			$product->set_attributes( $attributes );
		}

		// Sales and prices.
		if ( in_array( $product->get_type(), array( 'variable', 'grouped' ), true ) ) {
			$product->set_regular_price( '' );
			$product->set_sale_price( '' );
			$product->set_date_on_sale_to( '' );
			$product->set_date_on_sale_from( '' );
			$product->set_price( '' );
		} else {
			// Regular Price.
			if ( isset( $request['regular_price'] ) ) {
				$product->set_regular_price( $request['regular_price'] );
			}

			// Sale Price.
			if ( isset( $request['sale_price'] ) ) {
				$product->set_sale_price( $request['sale_price'] );
			}

			if ( isset( $request['date_on_sale_from'] ) ) {
				$product->set_date_on_sale_from( $request['date_on_sale_from'] );
			}

			if ( isset( $request['date_on_sale_from_gmt'] ) ) {
				$product->set_date_on_sale_from( $request['date_on_sale_from_gmt'] ? strtotime( $request['date_on_sale_from_gmt'] ) : null );
			}

			if ( isset( $request['date_on_sale_to'] ) ) {
				$product->set_date_on_sale_to( $request['date_on_sale_to'] );
			}

			if ( isset( $request['date_on_sale_to_gmt'] ) ) {
				$product->set_date_on_sale_to( $request['date_on_sale_to_gmt'] ? strtotime( $request['date_on_sale_to_gmt'] ) : null );
			}
		}

		// Product parent ID.
		if ( isset( $request['parent_id'] ) ) {
			$product->set_parent_id( $request['parent_id'] );
		}

		// Sold individually.
		if ( isset( $request['sold_individually'] ) ) {
			$product->set_sold_individually( $request['sold_individually'] );
		}

		// Stock status.
		if ( isset( $request['in_stock'] ) ) {
			$stock_status = true === $request['in_stock'] ? 'instock' : 'outofstock';
		} else {
			$stock_status = $product->get_stock_status();
		}

		// Stock data.
		if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
			// Manage stock.
			if ( isset( $request['manage_stock'] ) ) {
				$product->set_manage_stock( $request['manage_stock'] );
			}

			// Backorders.
			if ( isset( $request['backorders'] ) ) {
				$product->set_backorders( $request['backorders'] );
			}

			if ( $product->is_type( 'grouped' ) ) {
				$product->set_manage_stock( 'no' );
				$product->set_backorders( 'no' );
				$product->set_stock_quantity( '' );
				$product->set_stock_status( $stock_status );
			} elseif ( $product->is_type( 'external' ) ) {
				$product->set_manage_stock( 'no' );
				$product->set_backorders( 'no' );
				$product->set_stock_quantity( '' );
				$product->set_stock_status( 'instock' );
			} elseif ( $product->get_manage_stock() ) {
				// Stock status is always determined by children so sync later.
				if ( ! $product->is_type( 'variable' ) ) {
					$product->set_stock_status( $stock_status );
				}

				// Stock quantity.
				if ( isset( $request['stock_quantity'] ) ) {
					$product->set_stock_quantity( wc_stock_amount( $request['stock_quantity'] ) );
				} elseif ( isset( $request['inventory_delta'] ) ) {
					$stock_quantity  = wc_stock_amount( $product->get_stock_quantity() );
					$stock_quantity += wc_stock_amount( $request['inventory_delta'] );
					$product->set_stock_quantity( wc_stock_amount( $stock_quantity ) );
				}
			} else {
				// Don't manage stock.
				$product->set_manage_stock( 'no' );
				$product->set_stock_quantity( '' );
				$product->set_stock_status( $stock_status );
			}
		} elseif ( ! $product->is_type( 'variable' ) ) {
			$product->set_stock_status( $stock_status );
		}

		// Upsells.
		if ( isset( $request['upsell_ids'] ) ) {
			$upsells = array();
			$ids     = $request['upsell_ids'];

			if ( ! empty( $ids ) ) {
				foreach ( $ids as $id ) {
					if ( $id && $id > 0 ) {
						$upsells[] = $id;
					}
				}
			}

			$product->set_upsell_ids( $upsells );
		}

		// Cross sells.
		if ( isset( $request['cross_sell_ids'] ) ) {
			$crosssells = array();
			$ids        = $request['cross_sell_ids'];

			if ( ! empty( $ids ) ) {
				foreach ( $ids as $id ) {
					if ( $id && $id > 0 ) {
						$crosssells[] = $id;
					}
				}
			}

			$product->set_cross_sell_ids( $crosssells );
		}

		// Product categories.
		if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) {
			$product = $this->save_taxonomy_terms( $product, $request['categories'] );
		}

		// Product tags.
		if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) {
			$product = $this->save_taxonomy_terms( $product, $request['tags'], 'tag' );
		}

		// Downloadable.
		if ( isset( $request['downloadable'] ) ) {
			$product->set_downloadable( $request['downloadable'] );
		}

		// Downloadable options.
		if ( $product->get_downloadable() ) {

			// Downloadable files.
			if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) {
				$product = $this->save_downloadable_files( $product, $request['downloads'] );
			}

			// Download limit.
			if ( isset( $request['download_limit'] ) ) {
				$product->set_download_limit( $request['download_limit'] );
			}

			// Download expiry.
			if ( isset( $request['download_expiry'] ) ) {
				$product->set_download_expiry( $request['download_expiry'] );
			}
		}

		// Product url and button text for external products.
		if ( $product->is_type( 'external' ) ) {
			if ( isset( $request['external_url'] ) ) {
				$product->set_product_url( $request['external_url'] );
			}

			if ( isset( $request['button_text'] ) ) {
				$product->set_button_text( $request['button_text'] );
			}
		}

		// Save default attributes for variable products.
		if ( $product->is_type( 'variable' ) ) {
			$product = $this->save_default_attributes( $product, $request );
		}

		// Set children for a grouped product.
		if ( $product->is_type( 'grouped' ) && isset( $request['grouped_products'] ) ) {
			$product->set_children( $request['grouped_products'] );
		}

		// Check for featured/gallery images, upload it and set it.
		if ( isset( $request['images'] ) ) {
			$product = $this->set_product_images( $product, $request['images'] );
		}

		// Allow set meta_data.
		if ( is_array( $request['meta_data'] ) ) {
			foreach ( $request['meta_data'] as $meta ) {
				$product->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
			}
		}

		/**
		 * Filters an object before it is inserted via the REST API.
		 *
		 * The dynamic portion of the hook name, `$this->post_type`,
		 * refers to the object type slug.
		 *
		 * @param WC_Data         $product  Object object.
		 * @param WP_REST_Request $request  Request object.
		 * @param bool            $creating If is creating a new object.
		 */
		return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $product, $request, $creating );
	}

	/**
	 * Set product images.
	 *
	 * @param WC_Product $product Product instance.
	 * @param array      $images  Images data.
	 *
	 * @throws WC_REST_Exception REST API exceptions.
	 * @return WC_Product
	 */
	protected function set_product_images( $product, $images ) {
		$images = is_array( $images ) ? array_filter( $images ) : array();

		if ( ! empty( $images ) ) {
			$gallery_positions = array();

			foreach ( $images as $index => $image ) {
				$attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0;

				if ( 0 === $attachment_id && isset( $image['src'] ) ) {
					$upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) );

					if ( is_wp_error( $upload ) ) {
						if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $product->get_id(), $images ) ) {
							throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 );
						} else {
							continue;
						}
					}

					$attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->get_id() );
				}

				if ( ! wp_attachment_is_image( $attachment_id ) ) {
					/* translators: %s: attachment id */
					throw new WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 );
				}

				$gallery_positions[ $attachment_id ] = absint( isset( $image['position'] ) ? $image['position'] : $index );

				// Set the image alt if present.
				if ( ! empty( $image['alt'] ) ) {
					update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) );
				}

				// Set the image name if present.
				if ( ! empty( $image['name'] ) ) {
					wp_update_post(
						array(
							'ID'         => $attachment_id,
							'post_title' => $image['name'],
						)
					);
				}

				// Set the image source if present, for future reference.
				if ( ! empty( $image['src'] ) ) {
					update_post_meta( $attachment_id, '_wc_attachment_source', esc_url_raw( $image['src'] ) );
				}
			}

			// Sort images and get IDs in correct order.
			asort( $gallery_positions );

			// Get gallery in correct order.
			$gallery = array_keys( $gallery_positions );

			// Featured image is in position 0.
			$image_id = array_shift( $gallery );

			// Set images.
			$product->set_image_id( $image_id );
			$product->set_gallery_image_ids( $gallery );
		} else {
			$product->set_image_id( '' );
			$product->set_gallery_image_ids( array() );
		}

		return $product;
	}

	/**
	 * Save product shipping data.
	 *
	 * @param WC_Product $product Product instance.
	 * @param array      $data    Shipping data.
	 *
	 * @return WC_Product
	 */
	protected function save_product_shipping_data( $product, $data ) {
		// Virtual.
		if ( isset( $data['virtual'] ) && true === $data['virtual'] ) {
			$product->set_weight( '' );
			$product->set_height( '' );
			$product->set_length( '' );
			$product->set_width( '' );
		} else {
			if ( isset( $data['weight'] ) ) {
				$product->set_weight( $data['weight'] );
			}

			// Height.
			if ( isset( $data['dimensions']['height'] ) ) {
				$product->set_height( $data['dimensions']['height'] );
			}

			// Width.
			if ( isset( $data['dimensions']['width'] ) ) {
				$product->set_width( $data['dimensions']['width'] );
			}

			// Length.
			if ( isset( $data['dimensions']['length'] ) ) {
				$product->set_length( $data['dimensions']['length'] );
			}
		}

		// Shipping class.
		if ( isset( $data['shipping_class'] ) ) {
			$data_store        = $product->get_data_store();
			$shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $data['shipping_class'] ) );
			$product->set_shipping_class_id( $shipping_class_id );
		}

		return $product;
	}

	/**
	 * Save downloadable files.
	 *
	 * @param WC_Product $product    Product instance.
	 * @param array      $downloads  Downloads data.
	 * @param int        $deprecated Deprecated since 3.0.
	 *
	 * @return WC_Product
	 */
	protected function save_downloadable_files( $product, $downloads, $deprecated = 0 ) {
		if ( $deprecated ) {
			wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() not requires a variation_id anymore.' );
		}

		$files = array();
		foreach ( $downloads as $key => $file ) {
			if ( empty( $file['file'] ) ) {
				continue;
			}

			$download = new WC_Product_Download();
			$download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() );
			$download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) );
			$download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) );
			$files[] = $download;
		}
		$product->set_downloads( $files );

		return $product;
	}

	/**
	 * Save taxonomy terms.
	 *
	 * @param WC_Product $product  Product instance.
	 * @param array      $terms    Terms data.
	 * @param string     $taxonomy Taxonomy name.
	 *
	 * @return WC_Product
	 */
	protected function save_taxonomy_terms( $product, $terms, $taxonomy = 'cat' ) {
		$term_ids = wp_list_pluck( $terms, 'id' );

		if ( 'cat' === $taxonomy ) {
			$product->set_category_ids( $term_ids );
		} elseif ( 'tag' === $taxonomy ) {
			$product->set_tag_ids( $term_ids );
		}

		return $product;
	}

	/**
	 * Save default attributes.
	 *
	 * @param WC_Product      $product Product instance.
	 * @param WP_REST_Request $request Request data.
	 *
	 * @since  3.0.0
	 * @return WC_Product
	 */
	protected function save_default_attributes( $product, $request ) {
		if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) {

			$attributes         = $product->get_attributes();
			$default_attributes = array();

			foreach ( $request['default_attributes'] as $attribute ) {
				$attribute_id   = 0;
				$attribute_name = '';

				// Check ID for global attributes or name for product attributes.
				if ( ! empty( $attribute['id'] ) ) {
					$attribute_id   = absint( $attribute['id'] );
					$attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
				} elseif ( ! empty( $attribute['name'] ) ) {
					$attribute_name = sanitize_title( $attribute['name'] );
				}

				if ( ! $attribute_id && ! $attribute_name ) {
					continue;
				}

				if ( isset( $attributes[ $attribute_name ] ) ) {
					$_attribute = $attributes[ $attribute_name ];

					if ( $_attribute['is_variation'] ) {
						$value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : '';

						if ( ! empty( $_attribute['is_taxonomy'] ) ) {
							// If dealing with a taxonomy, we need to get the slug from the name posted to the API.
							$term = get_term_by( 'name', $value, $attribute_name );

							if ( $term && ! is_wp_error( $term ) ) {
								$value = $term->slug;
							} else {
								$value = sanitize_title( $value );
							}
						}

						if ( $value ) {
							$default_attributes[ $attribute_name ] = $value;
						}
					}
				}
			}

			$product->set_default_attributes( $default_attributes );
		}

		return $product;
	}

	/**
	 * Clear caches here so in sync with any new variations/children.
	 *
	 * @param WC_Data $object Object data.
	 */
	public function clear_transients( $object ) {
		wc_delete_product_transients( $object->get_id() );
		wp_cache_delete( 'product-' . $object->get_id(), 'products' );
	}

	/**
	 * Delete a single item.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 *
	 * @return WP_REST_Response|WP_Error
	 */
	public function delete_item( $request ) {
		$id     = (int) $request['id'];
		$force  = (bool) $request['force'];
		$object = $this->get_object( (int) $request['id'] );
		$result = false;

		if ( ! $object || 0 === $object->get_id() ) {
			return new WP_Error(
				"woocommerce_rest_{$this->post_type}_invalid_id",
				__( 'Invalid ID.', 'woocommerce' ),
				array(
					'status' => 404,
				)
			);
		}

		if ( 'variation' === $object->get_type() ) {
			return new WP_Error(
				"woocommerce_rest_invalid_{$this->post_type}_id",
				__( 'To manipulate product variations you should use the /products/&lt;product_id&gt;/variations/&lt;id&gt; endpoint.', 'woocommerce' ),
				array(
					'status' => 404,
				)
			);
		}

		$supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( array( $object, 'get_status' ) );

		/**
		 * Filter whether an object is trashable.
		 *
		 * Return false to disable trash support for the object.
		 *
		 * @param boolean $supports_trash Whether the object type support trashing.
		 * @param WC_Data $object         The object being considered for trashing support.
		 */
		$supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_object_trashable", $supports_trash, $object );

		if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) {
			return new WP_Error(
				"woocommerce_rest_user_cannot_delete_{$this->post_type}",
				/* translators: %s: post type */
				sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ),
				array(
					'status' => rest_authorization_required_code(),
				)
			);
		}

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_object_for_response( $object, $request );

		// If we're forcing, then delete permanently.
		if ( $force ) {
			if ( $object->is_type( 'variable' ) ) {
				foreach ( $object->get_children() as $child_id ) {
					$child = wc_get_product( $child_id );
					if ( ! empty( $child ) ) {
						$child->delete( true );
					}
				}
			} else {
				// For other product types, if the product has children, remove the relationship.
				foreach ( $object->get_children() as $child_id ) {
					$child = wc_get_product( $child_id );
					if ( ! empty( $child ) ) {
						$child->set_parent_id( 0 );
						$child->save();
					}
				}
			}

			$object->delete( true );
			$result = 0 === $object->get_id();
		} else {
			// If we don't support trashing for this type, error out.
			if ( ! $supports_trash ) {
				return new WP_Error(
					'woocommerce_rest_trash_not_supported',
					/* translators: %s: post type */
					sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ),
					array(
						'status' => 501,
					)
				);
			}

			// Otherwise, only trash if we haven't already.
			if ( is_callable( array( $object, 'get_status' ) ) ) {
				if ( 'trash' === $object->get_status() ) {
					return new WP_Error(
						'woocommerce_rest_already_trashed',
						/* translators: %s: post type */
						sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ),
						array(
							'status' => 410,
						)
					);
				}

				$object->delete();
				$result = 'trash' === $object->get_status();
			}
		}

		if ( ! $result ) {
			return new WP_Error(
				'woocommerce_rest_cannot_delete',
				/* translators: %s: post type */
				sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ),
				array(
					'status' => 500,
				)
			);
		}

		// Delete parent product transients.
		if ( 0 !== $object->get_parent_id() ) {
			wc_delete_product_transients( $object->get_parent_id() );
		}

		/**
		 * Fires after a single object is deleted or trashed via the REST API.
		 *
		 * @param WC_Data          $object   The deleted or trashed object.
		 * @param WP_REST_Response $response The response data.
		 * @param WP_REST_Request  $request  The request sent to the API.
		 */
		do_action( "woocommerce_rest_delete_{$this->post_type}_object", $object, $response, $request );

		return $response;
	}

	/**
	 * Get the Product's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$weight_unit    = get_option( 'woocommerce_weight_unit' );
		$dimension_unit = get_option( 'woocommerce_dimension_unit' );
		$schema         = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => $this->post_type,
			'type'       => 'object',
			'properties' => array(
				'id'                    => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'name'                  => array(
					'description' => __( 'Product name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'slug'                  => array(
					'description' => __( 'Product slug.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'permalink'             => array(
					'description' => __( 'Product URL.', 'woocommerce' ),
					'type'        => 'string',
					'format'      => 'uri',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_created'          => array(
					'description' => __( "The date the product was created, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_created_gmt'      => array(
					'description' => __( 'The date the product was created, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_modified'         => array(
					'description' => __( "The date the product was last modified, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_modified_gmt'     => array(
					'description' => __( 'The date the product was last modified, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'type'                  => array(
					'description' => __( 'Product type.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'simple',
					'enum'        => array_keys( wc_get_product_types() ),
					'context'     => array( 'view', 'edit' ),
				),
				'status'                => array(
					'description' => __( 'Product status (post status).', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'publish',
					'enum'        => array_merge( array_keys( get_post_statuses() ), array( 'future' ) ),
					'context'     => array( 'view', 'edit' ),
				),
				'featured'              => array(
					'description' => __( 'Featured product.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'catalog_visibility'    => array(
					'description' => __( 'Catalog visibility.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'visible',
					'enum'        => array( 'visible', 'catalog', 'search', 'hidden' ),
					'context'     => array( 'view', 'edit' ),
				),
				'description'           => array(
					'description' => __( 'Product description.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'short_description'     => array(
					'description' => __( 'Product short description.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'sku'                   => array(
					'description' => __( 'Unique identifier.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'price'                 => array(
					'description' => __( 'Current product price.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'regular_price'         => array(
					'description' => __( 'Product regular price.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'sale_price'            => array(
					'description' => __( 'Product sale price.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'date_on_sale_from'     => array(
					'description' => __( "Start date of sale price, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'date_on_sale_from_gmt' => array(
					'description' => __( 'Start date of sale price, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'date_on_sale_to'       => array(
					'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'date_on_sale_to_gmt'   => array(
					'description' => __( 'End date of sale price, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'price_html'            => array(
					'description' => __( 'Price formatted in HTML.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'on_sale'               => array(
					'description' => __( 'Shows if the product is on sale.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'purchasable'           => array(
					'description' => __( 'Shows if the product can be bought.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'total_sales'           => array(
					'description' => __( 'Amount of sales.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'virtual'               => array(
					'description' => __( 'If the product is virtual.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'downloadable'          => array(
					'description' => __( 'If the product is downloadable.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'downloads'             => array(
					'description' => __( 'List of downloadable files.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'   => array(
								'description' => __( 'File ID.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'name' => array(
								'description' => __( 'File name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'file' => array(
								'description' => __( 'File URL.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
						),
					),
				),
				'download_limit'        => array(
					'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ),
					'type'        => 'integer',
					'default'     => -1,
					'context'     => array( 'view', 'edit' ),
				),
				'download_expiry'       => array(
					'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ),
					'type'        => 'integer',
					'default'     => -1,
					'context'     => array( 'view', 'edit' ),
				),
				'external_url'          => array(
					'description' => __( 'Product external URL. Only for external products.', 'woocommerce' ),
					'type'        => 'string',
					'format'      => 'uri',
					'context'     => array( 'view', 'edit' ),
				),
				'button_text'           => array(
					'description' => __( 'Product external button text. Only for external products.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'tax_status'            => array(
					'description' => __( 'Tax status.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'taxable',
					'enum'        => array( 'taxable', 'shipping', 'none' ),
					'context'     => array( 'view', 'edit' ),
				),
				'tax_class'             => array(
					'description' => __( 'Tax class.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'manage_stock'          => array(
					'description' => __( 'Stock management at product level.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'stock_quantity'        => array(
					'description' => __( 'Stock quantity.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'in_stock'              => array(
					'description' => __( 'Controls whether or not the product is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => true,
					'context'     => array( 'view', 'edit' ),
				),
				'backorders'            => array(
					'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'no',
					'enum'        => array( 'no', 'notify', 'yes' ),
					'context'     => array( 'view', 'edit' ),
				),
				'backorders_allowed'    => array(
					'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'backordered'           => array(
					'description' => __( 'Shows if the product is on backordered.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'sold_individually'     => array(
					'description' => __( 'Allow one item to be bought in a single order.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'weight'                => array(
					/* translators: %s: weight unit */
					'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), $weight_unit ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'dimensions'            => array(
					'description' => __( 'Product dimensions.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'properties'  => array(
						'length' => array(
							/* translators: %s: dimension unit */
							'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), $dimension_unit ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'width'  => array(
							/* translators: %s: dimension unit */
							'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), $dimension_unit ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'height' => array(
							/* translators: %s: dimension unit */
							'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), $dimension_unit ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
					),
				),
				'shipping_required'     => array(
					'description' => __( 'Shows if the product need to be shipped.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'shipping_taxable'      => array(
					'description' => __( 'Shows whether or not the product shipping is taxable.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'shipping_class'        => array(
					'description' => __( 'Shipping class slug.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'shipping_class_id'     => array(
					'description' => __( 'Shipping class ID.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'reviews_allowed'       => array(
					'description' => __( 'Allow reviews.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => true,
					'context'     => array( 'view', 'edit' ),
				),
				'average_rating'        => array(
					'description' => __( 'Reviews average rating.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'rating_count'          => array(
					'description' => __( 'Amount of reviews that the product have.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'related_ids'           => array(
					'description' => __( 'List of related products IDs.', 'woocommerce' ),
					'type'        => 'array',
					'items'       => array(
						'type' => 'integer',
					),
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'upsell_ids'            => array(
					'description' => __( 'List of up-sell products IDs.', 'woocommerce' ),
					'type'        => 'array',
					'items'       => array(
						'type' => 'integer',
					),
					'context'     => array( 'view', 'edit' ),
				),
				'cross_sell_ids'        => array(
					'description' => __( 'List of cross-sell products IDs.', 'woocommerce' ),
					'type'        => 'array',
					'items'       => array(
						'type' => 'integer',
					),
					'context'     => array( 'view', 'edit' ),
				),
				'parent_id'             => array(
					'description' => __( 'Product parent ID.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'purchase_note'         => array(
					'description' => __( 'Optional note to send the customer after purchase.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'categories'            => array(
					'description' => __( 'List of categories.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'   => array(
								'description' => __( 'Category ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'name' => array(
								'description' => __( 'Category name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'slug' => array(
								'description' => __( 'Category slug.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
						),
					),
				),
				'tags'                  => array(
					'description' => __( 'List of tags.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'   => array(
								'description' => __( 'Tag ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'name' => array(
								'description' => __( 'Tag name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'slug' => array(
								'description' => __( 'Tag slug.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
						),
					),
				),
				'images'                => array(
					'description' => __( 'List of images.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'                => array(
								'description' => __( 'Image ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'date_created'      => array(
								'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_created_gmt'  => array(
								'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_modified'     => array(
								'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_modified_gmt' => array(
								'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'src'               => array(
								'description' => __( 'Image URL.', 'woocommerce' ),
								'type'        => 'string',
								'format'      => 'uri',
								'context'     => array( 'view', 'edit' ),
							),
							'name'              => array(
								'description' => __( 'Image name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'alt'               => array(
								'description' => __( 'Image alternative text.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'position'          => array(
								'description' => __( 'Image position. 0 means that the image is featured.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
						),
					),
				),
				'attributes'            => array(
					'description' => __( 'List of attributes.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'        => array(
								'description' => __( 'Attribute ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'name'      => array(
								'description' => __( 'Attribute name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'position'  => array(
								'description' => __( 'Attribute position.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'visible'   => array(
								'description' => __( "Define if the attribute is visible on the \"Additional information\" tab in the product's page.", 'woocommerce' ),
								'type'        => 'boolean',
								'default'     => false,
								'context'     => array( 'view', 'edit' ),
							),
							'variation' => array(
								'description' => __( 'Define if the attribute can be used as variation.', 'woocommerce' ),
								'type'        => 'boolean',
								'default'     => false,
								'context'     => array( 'view', 'edit' ),
							),
							'options'   => array(
								'description' => __( 'List of available term names of the attribute.', 'woocommerce' ),
								'type'        => 'array',
								'context'     => array( 'view', 'edit' ),
								'items'       => array(
									'type' => 'string',
								),
							),
						),
					),
				),
				'default_attributes'    => array(
					'description' => __( 'Defaults variation attributes.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'     => array(
								'description' => __( 'Attribute ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'name'   => array(
								'description' => __( 'Attribute name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'option' => array(
								'description' => __( 'Selected attribute term name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
						),
					),
				),
				'variations'            => array(
					'description' => __( 'List of variations IDs.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type' => 'integer',
					),
					'readonly'    => true,
				),
				'grouped_products'      => array(
					'description' => __( 'List of grouped products ID.', 'woocommerce' ),
					'type'        => 'array',
					'items'       => array(
						'type' => 'integer',
					),
					'context'     => array( 'view', 'edit' ),
				),
				'menu_order'            => array(
					'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'meta_data'             => array(
					'description' => __( 'Meta data.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'    => array(
								'description' => __( 'Meta ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'key'   => array(
								'description' => __( 'Meta key.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'value' => array(
								'description' => __( 'Meta value.', 'woocommerce' ),
								'type'        => 'mixed',
								'context'     => array( 'view', 'edit' ),
							),
						),
					),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections of attachments.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params = parent::get_collection_params();

		$params['orderby']['enum'] = array_merge( $params['orderby']['enum'], array( 'menu_order' ) );

		$params['slug']           = array(
			'description'       => __( 'Limit result set to products with a specific slug.', 'woocommerce' ),
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['status']         = array(
			'default'           => 'any',
			'description'       => __( 'Limit result set to products assigned a specific status.', 'woocommerce' ),
			'type'              => 'string',
			'enum'              => array_merge( array( 'any', 'future', 'trash' ), array_keys( get_post_statuses() ) ),
			'sanitize_callback' => 'sanitize_key',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['type']           = array(
			'description'       => __( 'Limit result set to products assigned a specific type.', 'woocommerce' ),
			'type'              => 'string',
			'enum'              => array_keys( wc_get_product_types() ),
			'sanitize_callback' => 'sanitize_key',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['sku']            = array(
			'description'       => __( 'Limit result set to products with specific SKU(s). Use commas to separate.', 'woocommerce' ),
			'type'              => 'string',
			'sanitize_callback' => 'sanitize_text_field',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['featured']       = array(
			'description'       => __( 'Limit result set to featured products.', 'woocommerce' ),
			'type'              => 'boolean',
			'sanitize_callback' => 'wc_string_to_bool',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['category']       = array(
			'description'       => __( 'Limit result set to products assigned a specific category ID.', 'woocommerce' ),
			'type'              => 'string',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['tag']            = array(
			'description'       => __( 'Limit result set to products assigned a specific tag ID.', 'woocommerce' ),
			'type'              => 'string',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['shipping_class'] = array(
			'description'       => __( 'Limit result set to products assigned a specific shipping class ID.', 'woocommerce' ),
			'type'              => 'string',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['attribute']      = array(
			'description'       => __( 'Limit result set to products with a specific attribute. Use the taxonomy name/attribute slug.', 'woocommerce' ),
			'type'              => 'string',
			'sanitize_callback' => 'sanitize_text_field',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['attribute_term'] = array(
			'description'       => __( 'Limit result set to products with a specific attribute term ID (required an assigned attribute).', 'woocommerce' ),
			'type'              => 'string',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
		);

		if ( wc_tax_enabled() ) {
			$params['tax_class'] = array(
				'description'       => __( 'Limit result set to products with a specific tax class.', 'woocommerce' ),
				'type'              => 'string',
				'enum'              => array_merge( array( 'standard' ), WC_Tax::get_tax_class_slugs() ),
				'sanitize_callback' => 'sanitize_text_field',
				'validate_callback' => 'rest_validate_request_arg',
			);
		}

		$params['in_stock']  = array(
			'description'       => __( 'Limit result set to products in stock or out of stock.', 'woocommerce' ),
			'type'              => 'boolean',
			'sanitize_callback' => 'wc_string_to_bool',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['on_sale']   = array(
			'description'       => __( 'Limit result set to products on sale.', 'woocommerce' ),
			'type'              => 'boolean',
			'sanitize_callback' => 'wc_string_to_bool',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['min_price'] = array(
			'description'       => __( 'Limit result set to products based on a minimum price.', 'woocommerce' ),
			'type'              => 'string',
			'sanitize_callback' => 'sanitize_text_field',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['max_price'] = array(
			'description'       => __( 'Limit result set to products based on a maximum price.', 'woocommerce' ),
			'type'              => 'string',
			'sanitize_callback' => 'sanitize_text_field',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}
}
rest-api/Controllers/Version2/class-wc-rest-shipping-zones-v2-controller.php000064400000021046151336044760023202 0ustar00<?php
/**
 * REST API Shipping Zones controller
 *
 * Handles requests to the /shipping/zones endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Shipping Zones class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Shipping_Zones_Controller_Base
 */
class WC_REST_Shipping_Zones_V2_Controller extends WC_REST_Shipping_Zones_Controller_Base {

	/**
	 * Register the routes for Shipping Zones.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace, '/' . $this->rest_base, array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_item' ),
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
					'args'                => array_merge(
						$this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array(
							'name' => array(
								'required'    => true,
								'type'        => 'string',
								'description' => __( 'Shipping zone name.', 'woocommerce' ),
							),
						)
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
				'args'   => array(
					'id' => array(
						'description' => __( 'Unique ID for the resource.', 'woocommerce' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_item' ),
					'permission_callback' => array( $this, 'update_items_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_item' ),
					'permission_callback' => array( $this, 'delete_items_permissions_check' ),
					'args'                => array(
						'force' => array(
							'default'     => false,
							'type'        => 'boolean',
							'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ),
						),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Get a single Shipping Zone.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return WP_REST_Response|WP_Error
	 */
	public function get_item( $request ) {
		$zone = $this->get_zone( $request->get_param( 'id' ) );

		if ( is_wp_error( $zone ) ) {
			return $zone;
		}

		$data = $zone->get_data();
		$data = $this->prepare_item_for_response( $data, $request );
		$data = $this->prepare_response_for_collection( $data );

		return rest_ensure_response( $data );
	}

	/**
	 * Get all Shipping Zones.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return WP_REST_Response
	 */
	public function get_items( $request ) {
		$rest_of_the_world = WC_Shipping_Zones::get_zone_by( 'zone_id', 0 );

		$zones = WC_Shipping_Zones::get_zones();
		array_unshift( $zones, $rest_of_the_world->get_data() );
		$data = array();

		foreach ( $zones as $zone_obj ) {
			$zone   = $this->prepare_item_for_response( $zone_obj, $request );
			$zone   = $this->prepare_response_for_collection( $zone );
			$data[] = $zone;
		}

		return rest_ensure_response( $data );
	}

	/**
	 * Create a single Shipping Zone.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Request|WP_Error
	 */
	public function create_item( $request ) {
		$zone = new WC_Shipping_Zone( null );

		if ( ! is_null( $request->get_param( 'name' ) ) ) {
			$zone->set_zone_name( $request->get_param( 'name' ) );
		}

		if ( ! is_null( $request->get_param( 'order' ) ) ) {
			$zone->set_zone_order( $request->get_param( 'order' ) );
		}

		$zone->save();

		if ( $zone->get_id() !== 0 ) {
			$request->set_param( 'id', $zone->get_id() );
			$response = $this->get_item( $request );
			$response->set_status( 201 );
			$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $zone->get_id() ) ) );
			return $response;
		} else {
			return new WP_Error( 'woocommerce_rest_shipping_zone_not_created', __( "Resource cannot be created. Check to make sure 'order' and 'name' are present.", 'woocommerce' ), array( 'status' => 500 ) );
		}
	}

	/**
	 * Update a single Shipping Zone.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Request|WP_Error
	 */
	public function update_item( $request ) {
		$zone = $this->get_zone( $request->get_param( 'id' ) );

		if ( is_wp_error( $zone ) ) {
			return $zone;
		}

		if ( 0 === $zone->get_id() ) {
			return new WP_Error( 'woocommerce_rest_shipping_zone_invalid_zone', __( 'The "locations not covered by your other zones" zone cannot be updated.', 'woocommerce' ), array( 'status' => 403 ) );
		}

		$zone_changed = false;

		if ( ! is_null( $request->get_param( 'name' ) ) ) {
			$zone->set_zone_name( $request->get_param( 'name' ) );
			$zone_changed = true;
		}

		if ( ! is_null( $request->get_param( 'order' ) ) ) {
			$zone->set_zone_order( $request->get_param( 'order' ) );
			$zone_changed = true;
		}

		if ( $zone_changed ) {
			$zone->save();
		}

		return $this->get_item( $request );
	}

	/**
	 * Delete a single Shipping Zone.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Request|WP_Error
	 */
	public function delete_item( $request ) {
		$zone = $this->get_zone( $request->get_param( 'id' ) );

		if ( is_wp_error( $zone ) ) {
			return $zone;
		}

		$force = $request['force'];

		$response = $this->get_item( $request );

		if ( $force ) {
			$zone->delete();
		} else {
			return new WP_Error( 'rest_trash_not_supported', __( 'Shipping zones do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) );
		}

		return $response;
	}

	/**
	 * Prepare the Shipping Zone for the REST response.
	 *
	 * @param array           $item Shipping Zone.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response
	 */
	public function prepare_item_for_response( $item, $request ) {
		$data = array(
			'id'    => (int) $item['id'],
			'name'  => $item['zone_name'],
			'order' => (int) $item['zone_order'],
		);

		$context = empty( $request['context'] ) ? 'view' : $request['context'];
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $data['id'] ) );

		return $response;
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param int $zone_id Given Shipping Zone ID.
	 * @return array Links for the given Shipping Zone.
	 */
	protected function prepare_links( $zone_id ) {
		$base  = '/' . $this->namespace . '/' . $this->rest_base;
		$links = array(
			'self'        => array(
				'href' => rest_url( trailingslashit( $base ) . $zone_id ),
			),
			'collection'  => array(
				'href' => rest_url( $base ),
			),
			'describedby' => array(
				'href' => rest_url( trailingslashit( $base ) . $zone_id . '/locations' ),
			),
		);

		return $links;
	}

	/**
	 * Get the Shipping Zones schema, conforming to JSON Schema
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'shipping_zone',
			'type'       => 'object',
			'properties' => array(
				'id'    => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'name'  => array(
					'description' => __( 'Shipping zone name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				'order' => array(
					'description' => __( 'Shipping zone order.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version2/class-wc-rest-order-notes-v2-controller.php000064400000012764151336044760022475 0ustar00<?php
/**
 * REST API Order Notes controller
 *
 * Handles requests to the /orders/<order_id>/notes endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Order Notes controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Order_Notes_V1_Controller
 */
class WC_REST_Order_Notes_V2_Controller extends WC_REST_Order_Notes_V1_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';

	/**
	 * Get order notes from an order.
	 *
	 * @param WP_REST_Request $request Request data.
	 *
	 * @return array|WP_Error
	 */
	public function get_items( $request ) {
		$order = wc_get_order( (int) $request['order_id'] );

		if ( ! $order || $this->post_type !== $order->get_type() ) {
			return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$args = array(
			'post_id' => $order->get_id(),
			'approve' => 'approve',
			'type'    => 'order_note',
		);

		// Allow filter by order note type.
		if ( 'customer' === $request['type'] ) {
			$args['meta_query'] = array( // WPCS: slow query ok.
				array(
					'key'     => 'is_customer_note',
					'value'   => 1,
					'compare' => '=',
				),
			);
		} elseif ( 'internal' === $request['type'] ) {
			$args['meta_query'] = array( // WPCS: slow query ok.
				array(
					'key'     => 'is_customer_note',
					'compare' => 'NOT EXISTS',
				),
			);
		}

		remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );

		$notes = get_comments( $args );

		add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );

		$data = array();
		foreach ( $notes as $note ) {
			$order_note = $this->prepare_item_for_response( $note, $request );
			$order_note = $this->prepare_response_for_collection( $order_note );
			$data[]     = $order_note;
		}

		return rest_ensure_response( $data );
	}

	/**
	 * Prepare a single order note output for response.
	 *
	 * @param WP_Comment      $note Order note object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $note, $request ) {
		$data = array(
			'id'               => (int) $note->comment_ID,
			'date_created'     => wc_rest_prepare_date_response( $note->comment_date ),
			'date_created_gmt' => wc_rest_prepare_date_response( $note->comment_date_gmt ),
			'note'             => $note->comment_content,
			'customer_note'    => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ),
		);

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $note ) );

		/**
		 * Filter order note object returned from the REST API.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param WP_Comment       $note     Order note object used to create response.
		 * @param WP_REST_Request  $request  Request object.
		 */
		return apply_filters( 'woocommerce_rest_prepare_order_note', $response, $note, $request );
	}

	/**
	 * Get the Order Notes schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'order_note',
			'type'       => 'object',
			'properties' => array(
				'id'               => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_created'     => array(
					'description' => __( "The date the order note was created, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_created_gmt' => array(
					'description' => __( 'The date the order note was created, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'note'             => array(
					'description' => __( 'Order note content.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'customer_note'    => array(
					'description' => __( 'If true, the note will be shown to customers and they will be notified. If false, the note will be for admin reference only.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params            = array();
		$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
		$params['type']    = array(
			'default'           => 'any',
			'description'       => __( 'Limit result to customers or internal notes.', 'woocommerce' ),
			'type'              => 'string',
			'enum'              => array( 'any', 'customer', 'internal' ),
			'sanitize_callback' => 'sanitize_key',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}
}
rest-api/Controllers/Version2/class-wc-rest-product-reviews-v2-controller.php000064400000014613151336044760023371 0ustar00<?php
/**
 * REST API Product Reviews Controller
 *
 * Handles requests to /products/<product_id>/reviews.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Product Reviews Controller Class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Product_Reviews_V1_Controller
 */
class WC_REST_Product_Reviews_V2_Controller extends WC_REST_Product_Reviews_V1_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'products/(?P<product_id>[\d]+)/reviews';

	/**
	 * Register the routes for product reviews.
	 */
	public function register_routes() {
		parent::register_routes();

		register_rest_route(
			$this->namespace, '/' . $this->rest_base . '/batch', array(
				'args'   => array(
					'product_id' => array(
						'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'batch_items' ),
					'permission_callback' => array( $this, 'batch_items_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				'schema' => array( $this, 'get_public_batch_schema' ),
			)
		);
	}

	/**
	 * Check if a given request has access to batch manage product reviews.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function batch_items_permissions_check( $request ) {
		if ( ! wc_rest_check_post_permissions( 'product', 'batch' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}
		return true;
	}

	/**
	 * Prepare a single product review output for response.
	 *
	 * @param WP_Comment      $review Product review object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $review, $request ) {
		$data = array(
			'id'               => (int) $review->comment_ID,
			'date_created'     => wc_rest_prepare_date_response( $review->comment_date ),
			'date_created_gmt' => wc_rest_prepare_date_response( $review->comment_date_gmt ),
			'review'           => $review->comment_content,
			'rating'           => (int) get_comment_meta( $review->comment_ID, 'rating', true ),
			'name'             => $review->comment_author,
			'email'            => $review->comment_author_email,
			'verified'         => wc_review_is_from_verified_owner( $review->comment_ID ),
		);

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $review, $request ) );

		/**
		 * Filter product reviews object returned from the REST API.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param WP_Comment       $review   Product review object used to create response.
		 * @param WP_REST_Request  $request  Request object.
		 */
		return apply_filters( 'woocommerce_rest_prepare_product_review', $response, $review, $request );
	}


	/**
	 * Bulk create, update and delete items.
	 *
	 * @since  3.0.0
	 * @param WP_REST_Request $request Full details about the request.
	 * @return array Of WP_Error or WP_REST_Response.
	 */
	public function batch_items( $request ) {
		$items       = array_filter( $request->get_params() );
		$params      = $request->get_url_params();
		$product_id  = $params['product_id'];
		$body_params = array();

		foreach ( array( 'update', 'create', 'delete' ) as $batch_type ) {
			if ( ! empty( $items[ $batch_type ] ) ) {
				$injected_items = array();
				foreach ( $items[ $batch_type ] as $item ) {
					$injected_items[] = is_array( $item ) ? array_merge( array( 'product_id' => $product_id ), $item ) : $item;
				}
				$body_params[ $batch_type ] = $injected_items;
			}
		}

		$request = new WP_REST_Request( $request->get_method() );
		$request->set_body_params( $body_params );

		return parent::batch_items( $request );
	}

	/**
	 * Get the Product Review's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'product_review',
			'type'       => 'object',
			'properties' => array(
				'id'               => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'review'           => array(
					'description' => __( 'The content of the review.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'date_created'     => array(
					'description' => __( "The date the review was created, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'date_created_gmt' => array(
					'description' => __( 'The date the review was created, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'rating'           => array(
					'description' => __( 'Review rating (0 to 5).', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'name'             => array(
					'description' => __( 'Reviewer name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'email'            => array(
					'description' => __( 'Reviewer email.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'verified'         => array(
					'description' => __( 'Shows if the reviewer bought the product or not.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version2/class-wc-rest-report-top-sellers-v2-controller.php000064400000001013151336044760023777 0ustar00<?php
/**
 * REST API Reports controller
 *
 * Handles requests to the reports/top_sellers endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Report Top Sellers controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Report_Top_Sellers_V1_Controller
 */
class WC_REST_Report_Top_Sellers_V2_Controller extends WC_REST_Report_Top_Sellers_V1_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';
}
rest-api/Controllers/Version2/class-wc-rest-shipping-methods-v2-controller.php000064400000015432151336044760023511 0ustar00<?php
/**
 * REST API WC Shipping Methods controller
 *
 * Handles requests to the /shipping_methods endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Shipping methods controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Controller
 */
class WC_REST_Shipping_Methods_V2_Controller extends WC_REST_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'shipping_methods';

	/**
	 * Register the route for /shipping_methods and /shipping_methods/<method>
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace, '/' . $this->rest_base, array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
		register_rest_route(
			$this->namespace, '/' . $this->rest_base . '/(?P<id>[\w-]+)', array(
				'args'   => array(
					'id' => array(
						'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
						'type'        => 'string',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => array(
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Check whether a given request has permission to view shipping methods.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_items_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'shipping_methods', 'read' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}
		return true;
	}

	/**
	 * Check if a given request has access to read a shipping method.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_item_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'shipping_methods', 'read' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}
		return true;
	}

	/**
	 * Get shipping methods.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_items( $request ) {
		$wc_shipping = WC_Shipping::instance();
		$response    = array();
		foreach ( $wc_shipping->get_shipping_methods() as $id => $shipping_method ) {
			$method     = $this->prepare_item_for_response( $shipping_method, $request );
			$method     = $this->prepare_response_for_collection( $method );
			$response[] = $method;
		}
		return rest_ensure_response( $response );
	}

	/**
	 * Get a single Shipping Method.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return WP_REST_Response|WP_Error
	 */
	public function get_item( $request ) {
		$wc_shipping = WC_Shipping::instance();
		$methods     = $wc_shipping->get_shipping_methods();
		if ( empty( $methods[ $request['id'] ] ) ) {
			return new WP_Error( 'woocommerce_rest_shipping_method_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$method   = $methods[ $request['id'] ];
		$response = $this->prepare_item_for_response( $method, $request );

		return rest_ensure_response( $response );
	}

	/**
	 * Prepare a shipping method for response.
	 *
	 * @param  WC_Shipping_Method $method   Shipping method object.
	 * @param  WP_REST_Request    $request  Request object.
	 * @return WP_REST_Response   $response Response data.
	 */
	public function prepare_item_for_response( $method, $request ) {
		$data = array(
			'id'          => $method->id,
			'title'       => $method->method_title,
			'description' => $method->method_description,
		);

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $method, $request ) );

		/**
		 * Filter shipping methods object returned from the REST API.
		 *
		 * @param WP_REST_Response   $response The response object.
		 * @param WC_Shipping_Method $method   Shipping method object used to create response.
		 * @param WP_REST_Request    $request  Request object.
		 */
		return apply_filters( 'woocommerce_rest_prepare_shipping_method', $response, $method, $request );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param WC_Shipping_Method $method Shipping method object.
	 * @param WP_REST_Request    $request Request object.
	 * @return array
	 */
	protected function prepare_links( $method, $request ) {
		$links = array(
			'self'       => array(
				'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $method->id ) ),
			),
			'collection' => array(
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
			),
		);

		return $links;
	}

	/**
	 * Get the shipping method schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'shipping_method',
			'type'       => 'object',
			'properties' => array(
				'id'          => array(
					'description' => __( 'Method ID.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'title'       => array(
					'description' => __( 'Shipping method title.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'description' => array(
					'description' => __( 'Shipping method description.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get any query params needed.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		return array(
			'context' => $this->get_context_param( array( 'default' => 'view' ) ),
		);
	}
}
rest-api/Controllers/Version2/class-wc-rest-setting-options-v2-controller.php000064400000042367151336044760023404 0ustar00<?php
/**
 * REST API Setting Options controller
 *
 * Handles requests to the /settings/$group/$setting endpoints.
 *
 * @package WooCommerce\RestApi
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Setting Options controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Controller
 */
class WC_REST_Setting_Options_V2_Controller extends WC_REST_Controller {

	/**
	 * WP REST API namespace/version.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'settings/(?P<group_id>[\w-]+)';

	/**
	 * Register routes.
	 *
	 * @since 3.0.0
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace, '/' . $this->rest_base, array(
				'args'   => array(
					'group' => array(
						'description' => __( 'Settings group ID.', 'woocommerce' ),
						'type'        => 'string',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace, '/' . $this->rest_base . '/batch', array(
				'args'   => array(
					'group' => array(
						'description' => __( 'Settings group ID.', 'woocommerce' ),
						'type'        => 'string',
					),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'batch_items' ),
					'permission_callback' => array( $this, 'update_items_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				'schema' => array( $this, 'get_public_batch_schema' ),
			)
		);

		register_rest_route(
			$this->namespace, '/' . $this->rest_base . '/(?P<id>[\w-]+)', array(
				'args'   => array(
					'group' => array(
						'description' => __( 'Settings group ID.', 'woocommerce' ),
						'type'        => 'string',
					),
					'id'    => array(
						'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
						'type'        => 'string',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_item' ),
					'permission_callback' => array( $this, 'update_items_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Return a single setting.
	 *
	 * @since  3.0.0
	 * @param  WP_REST_Request $request Request data.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_item( $request ) {
		$setting = $this->get_setting( $request['group_id'], $request['id'] );

		if ( is_wp_error( $setting ) ) {
			return $setting;
		}

		$response = $this->prepare_item_for_response( $setting, $request );

		return rest_ensure_response( $response );
	}

	/**
	 * Return all settings in a group.
	 *
	 * @since  3.0.0
	 * @param  WP_REST_Request $request Request data.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_items( $request ) {
		$settings = $this->get_group_settings( $request['group_id'] );

		if ( is_wp_error( $settings ) ) {
			return $settings;
		}

		$data = array();

		foreach ( $settings as $setting_obj ) {
			$setting = $this->prepare_item_for_response( $setting_obj, $request );
			$setting = $this->prepare_response_for_collection( $setting );
			if ( $this->is_setting_type_valid( $setting['type'] ) ) {
				$data[] = $setting;
			}
		}

		return rest_ensure_response( $data );
	}

	/**
	 * Get all settings in a group.
	 *
	 * @since  3.0.0
	 * @param string $group_id Group ID.
	 * @return array|WP_Error
	 */
	public function get_group_settings( $group_id ) {
		if ( empty( $group_id ) ) {
			return new WP_Error( 'rest_setting_setting_group_invalid', __( 'Invalid setting group.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$settings = apply_filters( 'woocommerce_settings-' . $group_id, array() );

		if ( empty( $settings ) ) {
			return new WP_Error( 'rest_setting_setting_group_invalid', __( 'Invalid setting group.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$filtered_settings = array();
		foreach ( $settings as $setting ) {
			$option_key = $setting['option_key'];
			$setting    = $this->filter_setting( $setting );
			$default    = isset( $setting['default'] ) ? $setting['default'] : '';
			// Get the option value.
			if ( is_array( $option_key ) ) {
				$option           = get_option( $option_key[0] );
				$setting['value'] = isset( $option[ $option_key[1] ] ) ? $option[ $option_key[1] ] : $default;
			} else {
				$admin_setting_value = WC_Admin_Settings::get_option( $option_key, $default );
				$setting['value']    = $admin_setting_value;
			}

			if ( 'multi_select_countries' === $setting['type'] ) {
				$setting['options'] = WC()->countries->get_countries();
				$setting['type']    = 'multiselect';
			} elseif ( 'single_select_country' === $setting['type'] ) {
				$setting['type']    = 'select';
				$setting['options'] = $this->get_countries_and_states();
			}

			$filtered_settings[] = $setting;
		}

		return $filtered_settings;
	}

	/**
	 * Returns a list of countries and states for use in the base location setting.
	 *
	 * @since  3.0.7
	 * @return array Array of states and countries.
	 */
	private function get_countries_and_states() {
		$countries = WC()->countries->get_countries();
		if ( ! $countries ) {
			return array();
		}

		$output = array();

		foreach ( $countries as $key => $value ) {
			$states = WC()->countries->get_states( $key );
			if ( $states ) {
				foreach ( $states as $state_key => $state_value ) {
					$output[ $key . ':' . $state_key ] = $value . ' - ' . $state_value;
				}
			} else {
				$output[ $key ] = $value;
			}
		}

		return $output;
	}

	/**
	 * Get setting data.
	 *
	 * @since  3.0.0
	 * @param string $group_id Group ID.
	 * @param string $setting_id Setting ID.
	 * @return stdClass|WP_Error
	 */
	public function get_setting( $group_id, $setting_id ) {
		if ( empty( $setting_id ) ) {
			return new WP_Error( 'rest_setting_setting_invalid', __( 'Invalid setting.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$settings = $this->get_group_settings( $group_id );

		if ( is_wp_error( $settings ) ) {
			return $settings;
		}

		$array_key = array_keys( wp_list_pluck( $settings, 'id' ), $setting_id );

		if ( empty( $array_key ) ) {
			return new WP_Error( 'rest_setting_setting_invalid', __( 'Invalid setting.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$setting = $settings[ $array_key[0] ];

		if ( ! $this->is_setting_type_valid( $setting['type'] ) ) {
			return new WP_Error( 'rest_setting_setting_invalid', __( 'Invalid setting.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		return $setting;
	}

	/**
	 * Bulk create, update and delete items.
	 *
	 * @since  3.0.0
	 * @param WP_REST_Request $request Full details about the request.
	 * @return array Of WP_Error or WP_REST_Response.
	 */
	public function batch_items( $request ) {
		// Get the request params.
		$items = array_filter( $request->get_params() );

		/*
		 * Since our batch settings update is group-specific and matches based on the route,
		 * we inject the URL parameters (containing group) into the batch items
		 */
		if ( ! empty( $items['update'] ) ) {
			$to_update = array();
			foreach ( $items['update'] as $item ) {
				$to_update[] = array_merge( $request->get_url_params(), $item );
			}
			$request = new WP_REST_Request( $request->get_method() );
			$request->set_body_params( array( 'update' => $to_update ) );
		}

		return parent::batch_items( $request );
	}

	/**
	 * Update a single setting in a group.
	 *
	 * @since  3.0.0
	 * @param  WP_REST_Request $request Request data.
	 * @return WP_Error|WP_REST_Response
	 */
	public function update_item( $request ) {
		$setting = $this->get_setting( $request['group_id'], $request['id'] );

		if ( is_wp_error( $setting ) ) {
			return $setting;
		}

		if ( is_callable( array( $this, 'validate_setting_' . $setting['type'] . '_field' ) ) ) {
			$value = $this->{'validate_setting_' . $setting['type'] . '_field'}( $request['value'], $setting );
		} else {
			$value = $this->validate_setting_text_field( $request['value'], $setting );
		}

		if ( is_wp_error( $value ) ) {
			return $value;
		}

		if ( is_array( $setting['option_key'] ) ) {
			$setting['value']       = $value;
			$option_key             = $setting['option_key'];
			$prev                   = get_option( $option_key[0] );
			$prev[ $option_key[1] ] = $request['value'];
			update_option( $option_key[0], $prev );
		} else {
			$update_data                           = array();
			$update_data[ $setting['option_key'] ] = $value;
			$setting['value']                      = $value;
			WC_Admin_Settings::save_fields( array( $setting ), $update_data );
		}

		$response = $this->prepare_item_for_response( $setting, $request );

		return rest_ensure_response( $response );
	}

	/**
	 * Prepare a single setting object for response.
	 *
	 * @since  3.0.0
	 * @param object          $item Setting object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $item, $request ) {
		unset( $item['option_key'] );
		$data     = $this->filter_setting( $item );
		$data     = $this->add_additional_fields_to_object( $data, $request );
		$data     = $this->filter_response_by_context( $data, empty( $request['context'] ) ? 'view' : $request['context'] );
		$response = rest_ensure_response( $data );
		$response->add_links( $this->prepare_links( $data['id'], $request['group_id'] ) );
		return $response;
	}

	/**
	 * Prepare links for the request.
	 *
	 * @since  3.0.0
	 * @param string $setting_id Setting ID.
	 * @param string $group_id Group ID.
	 * @return array Links for the given setting.
	 */
	protected function prepare_links( $setting_id, $group_id ) {
		$base  = str_replace( '(?P<group_id>[\w-]+)', $group_id, $this->rest_base );
		$links = array(
			'self'       => array(
				'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $base, $setting_id ) ),
			),
			'collection' => array(
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ),
			),
		);

		return $links;
	}

	/**
	 * Makes sure the current user has access to READ the settings APIs.
	 *
	 * @since  3.0.0
	 * @param WP_REST_Request $request Full data about the request.
	 * @return WP_Error|boolean
	 */
	public function get_items_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Makes sure the current user has access to WRITE the settings APIs.
	 *
	 * @since  3.0.0
	 * @param WP_REST_Request $request Full data about the request.
	 * @return WP_Error|boolean
	 */
	public function update_items_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Filters out bad values from the settings array/filter so we
	 * only return known values via the API.
	 *
	 * @since 3.0.0
	 * @param  array $setting Settings.
	 * @return array
	 */
	public function filter_setting( $setting ) {
		$setting = array_intersect_key(
			$setting,
			array_flip( array_filter( array_keys( $setting ), array( $this, 'allowed_setting_keys' ) ) )
		);

		if ( empty( $setting['options'] ) ) {
			unset( $setting['options'] );
		}

		if ( 'image_width' === $setting['type'] ) {
			$setting = $this->cast_image_width( $setting );
		}

		return $setting;
	}

	/**
	 * For image_width, Crop can return "0" instead of false -- so we want
	 * to make sure we return these consistently the same we accept them.
	 *
	 * @todo remove in 4.0
	 * @since 3.0.0
	 * @param  array $setting Settings.
	 * @return array
	 */
	public function cast_image_width( $setting ) {
		foreach ( array( 'default', 'value' ) as $key ) {
			if ( isset( $setting[ $key ] ) ) {
				$setting[ $key ]['width']  = intval( $setting[ $key ]['width'] );
				$setting[ $key ]['height'] = intval( $setting[ $key ]['height'] );
				$setting[ $key ]['crop']   = (bool) $setting[ $key ]['crop'];
			}
		}
		return $setting;
	}

	/**
	 * Callback for allowed keys for each setting response.
	 *
	 * @since  3.0.0
	 * @param  string $key Key to check.
	 * @return boolean
	 */
	public function allowed_setting_keys( $key ) {
		return in_array(
			$key, array(
				'id',
				'label',
				'description',
				'default',
				'tip',
				'placeholder',
				'type',
				'options',
				'value',
				'option_key',
			)
		);
	}

	/**
	 * Boolean for if a setting type is a valid supported setting type.
	 *
	 * @since  3.0.0
	 * @param  string $type Type.
	 * @return bool
	 */
	public function is_setting_type_valid( $type ) {
		return in_array(
			$type, array(
				'text',         // Validates with validate_setting_text_field.
				'email',        // Validates with validate_setting_text_field.
				'number',       // Validates with validate_setting_text_field.
				'color',        // Validates with validate_setting_text_field.
				'password',     // Validates with validate_setting_text_field.
				'textarea',     // Validates with validate_setting_textarea_field.
				'select',       // Validates with validate_setting_select_field.
				'multiselect',  // Validates with validate_setting_multiselect_field.
				'radio',        // Validates with validate_setting_radio_field (-> validate_setting_select_field).
				'checkbox',     // Validates with validate_setting_checkbox_field.
				'image_width',  // Validates with validate_setting_image_width_field.
				'thumbnail_cropping', // Validates with validate_setting_text_field.
			)
		);
	}

	/**
	 * Get the settings schema, conforming to JSON Schema.
	 *
	 * @since 3.0.0
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'setting',
			'type'       => 'object',
			'properties' => array(
				'id'          => array(
					'description' => __( 'A unique identifier for the setting.', 'woocommerce' ),
					'type'        => 'string',
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_title',
					),
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'label'       => array(
					'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ),
					'type'        => 'string',
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'description' => array(
					'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ),
					'type'        => 'string',
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'value'       => array(
					'description' => __( 'Setting value.', 'woocommerce' ),
					'type'        => 'mixed',
					'context'     => array( 'view', 'edit' ),
				),
				'default'     => array(
					'description' => __( 'Default value for the setting.', 'woocommerce' ),
					'type'        => 'mixed',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'tip'         => array(
					'description' => __( 'Additional help text shown to the user about the setting.', 'woocommerce' ),
					'type'        => 'string',
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'placeholder' => array(
					'description' => __( 'Placeholder text to be displayed in text inputs.', 'woocommerce' ),
					'type'        => 'string',
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'type'        => array(
					'description' => __( 'Type of setting.', 'woocommerce' ),
					'type'        => 'string',
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
					'context'     => array( 'view', 'edit' ),
					'enum'        => array( 'text', 'email', 'number', 'color', 'password', 'textarea', 'select', 'multiselect', 'radio', 'image_width', 'checkbox', 'thumbnail_cropping' ),
					'readonly'    => true,
				),
				'options'     => array(
					'description' => __( 'Array of options (key value pairs) for inputs such as select, multiselect, and radio buttons.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version2/class-wc-rest-shipping-zone-locations-v2-controller.php000064400000012247151336044760025013 0ustar00<?php
/**
 * REST API Shipping Zone Locations controller
 *
 * Handles requests to the /shipping/zones/<id>/locations endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Shipping Zone Locations class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Shipping_Zones_Controller_Base
 */
class WC_REST_Shipping_Zone_Locations_V2_Controller extends WC_REST_Shipping_Zones_Controller_Base {

	/**
	 * Register the routes for Shipping Zone Locations.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)/locations', array(
				'args'   => array(
					'id' => array(
						'description' => __( 'Unique ID for the resource.', 'woocommerce' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_items' ),
					'permission_callback' => array( $this, 'update_items_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Get all Shipping Zone Locations.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return WP_REST_Response|WP_Error
	 */
	public function get_items( $request ) {
		$zone = $this->get_zone( (int) $request['id'] );

		if ( is_wp_error( $zone ) ) {
			return $zone;
		}

		$locations = $zone->get_zone_locations();
		$data      = array();

		foreach ( $locations as $location_obj ) {
			$location = $this->prepare_item_for_response( $location_obj, $request );
			$location = $this->prepare_response_for_collection( $location );
			$data[]   = $location;
		}

		return rest_ensure_response( $data );
	}

	/**
	 * Update all Shipping Zone Locations.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return WP_REST_Response|WP_Error
	 */
	public function update_items( $request ) {
		$zone = $this->get_zone( (int) $request['id'] );

		if ( is_wp_error( $zone ) ) {
			return $zone;
		}

		if ( 0 === $zone->get_id() ) {
			return new WP_Error( 'woocommerce_rest_shipping_zone_locations_invalid_zone', __( 'The "locations not covered by your other zones" zone cannot be updated.', 'woocommerce' ), array( 'status' => 403 ) );
		}

		$raw_locations = $request->get_json_params();
		$locations     = array();

		foreach ( (array) $raw_locations as $raw_location ) {
			if ( empty( $raw_location['code'] ) ) {
				continue;
			}

			$type = ! empty( $raw_location['type'] ) ? sanitize_text_field( $raw_location['type'] ) : 'country';

			if ( ! in_array( $type, array( 'postcode', 'state', 'country', 'continent' ), true ) ) {
				continue;
			}

			$locations[] = array(
				'code' => sanitize_text_field( $raw_location['code'] ),
				'type' => sanitize_text_field( $type ),
			);
		}

		$zone->set_locations( $locations );
		$zone->save();

		return $this->get_items( $request );
	}

	/**
	 * Prepare the Shipping Zone Location for the REST response.
	 *
	 * @param array           $item Shipping Zone Location.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response
	 */
	public function prepare_item_for_response( $item, $request ) {
		$context = empty( $request['context'] ) ? 'view' : $request['context'];
		$data    = $this->add_additional_fields_to_object( $item, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( (int) $request['id'] ) );

		return $response;
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param int $zone_id Given Shipping Zone ID.
	 * @return array Links for the given Shipping Zone Location.
	 */
	protected function prepare_links( $zone_id ) {
		$base  = '/' . $this->namespace . '/' . $this->rest_base . '/' . $zone_id;
		$links = array(
			'collection' => array(
				'href' => rest_url( $base . '/locations' ),
			),
			'describes'  => array(
				'href' => rest_url( $base ),
			),
		);

		return $links;
	}

	/**
	 * Get the Shipping Zone Locations schema, conforming to JSON Schema
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'shipping_zone_location',
			'type'       => 'object',
			'properties' => array(
				'code' => array(
					'description' => __( 'Shipping zone location code.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'type' => array(
					'description' => __( 'Shipping zone location type.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'country',
					'enum'        => array(
						'postcode',
						'state',
						'country',
						'continent',
					),
					'context'     => array( 'view', 'edit' ),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version2/class-wc-rest-product-attribute-terms-v2-controller.php000064400000001104151336044760025027 0ustar00<?php
/**
 * REST API Product Attribute Terms controller
 *
 * Handles requests to the products/attributes/<attribute_id>/terms endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Product Attribute Terms controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Product_Attribute_Terms_V1_Controller
 */
class WC_REST_Product_Attribute_Terms_V2_Controller extends WC_REST_Product_Attribute_Terms_V1_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';
}
rest-api/Controllers/Version2/class-wc-rest-payment-gateways-v2-controller.php000064400000036731151336044760023533 0ustar00<?php
/**
 * REST API WC Payment gateways controller
 *
 * Handles requests to the /payment_gateways endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Paymenga gateways controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Controller
 */
class WC_REST_Payment_Gateways_V2_Controller extends WC_REST_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'payment_gateways';

	/**
	 * Register the route for /payment_gateways and /payment_gateways/<id>
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace, '/' . $this->rest_base, array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
		register_rest_route(
			$this->namespace, '/' . $this->rest_base . '/(?P<id>[\w-]+)', array(
				'args'   => array(
					'id' => array(
						'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
						'type'        => 'string',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => array(
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_item' ),
					'permission_callback' => array( $this, 'update_items_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Check whether a given request has permission to view payment gateways.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_items_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'payment_gateways', 'read' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}
		return true;
	}

	/**
	 * Check if a given request has access to read a payment gateway.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_item_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'payment_gateways', 'read' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}
		return true;
	}

	/**
	 * Check whether a given request has permission to edit payment gateways.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function update_items_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'payment_gateways', 'edit' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}
		return true;
	}

	/**
	 * Get payment gateways.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_items( $request ) {
		$payment_gateways = WC()->payment_gateways->payment_gateways();
		$response         = array();
		foreach ( $payment_gateways as $payment_gateway_id => $payment_gateway ) {
			$payment_gateway->id = $payment_gateway_id;
			$gateway             = $this->prepare_item_for_response( $payment_gateway, $request );
			$gateway             = $this->prepare_response_for_collection( $gateway );
			$response[]          = $gateway;
		}
		return rest_ensure_response( $response );
	}

	/**
	 * Get a single payment gateway.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return WP_REST_Response|WP_Error
	 */
	public function get_item( $request ) {
		$gateway = $this->get_gateway( $request );

		if ( is_null( $gateway ) ) {
			return new WP_Error( 'woocommerce_rest_payment_gateway_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$gateway = $this->prepare_item_for_response( $gateway, $request );
		return rest_ensure_response( $gateway );
	}

	/**
	 * Update A Single Payment Method.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return WP_REST_Response|WP_Error
	 */
	public function update_item( $request ) {
		$gateway = $this->get_gateway( $request );

		if ( is_null( $gateway ) ) {
			return new WP_Error( 'woocommerce_rest_payment_gateway_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		// Get settings.
		$gateway->init_form_fields();
		$settings = $gateway->settings;

		// Update settings.
		if ( isset( $request['settings'] ) ) {
			$errors_found = false;
			foreach ( $gateway->form_fields as $key => $field ) {
				if ( isset( $request['settings'][ $key ] ) ) {
					if ( is_callable( array( $this, 'validate_setting_' . $field['type'] . '_field' ) ) ) {
						$value = $this->{'validate_setting_' . $field['type'] . '_field'}( $request['settings'][ $key ], $field );
					} else {
						$value = $this->validate_setting_text_field( $request['settings'][ $key ], $field );
					}
					if ( is_wp_error( $value ) ) {
						$errors_found = true;
						break;
					}
					$settings[ $key ] = $value;
				}
			}

			if ( $errors_found ) {
				return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) );
			}
		}

		// Update if this method is enabled or not.
		if ( isset( $request['enabled'] ) ) {
			$settings['enabled'] = wc_bool_to_string( $request['enabled'] );
			$gateway->enabled    = $settings['enabled'];
		}

		// Update title.
		if ( isset( $request['title'] ) ) {
			$settings['title'] = $request['title'];
			$gateway->title    = $settings['title'];
		}

		// Update description.
		if ( isset( $request['description'] ) ) {
			$settings['description'] = $request['description'];
			$gateway->description    = $settings['description'];
		}

		// Update options.
		$gateway->settings = $settings;
		update_option( $gateway->get_option_key(), apply_filters( 'woocommerce_gateway_' . $gateway->id . '_settings_values', $settings, $gateway ) );

		// Update order.
		if ( isset( $request['order'] ) ) {
			$order                 = (array) get_option( 'woocommerce_gateway_order' );
			$order[ $gateway->id ] = $request['order'];
			update_option( 'woocommerce_gateway_order', $order );
			$gateway->order = absint( $request['order'] );
		}

		$gateway = $this->prepare_item_for_response( $gateway, $request );
		return rest_ensure_response( $gateway );
	}

	/**
	 * Get a gateway based on the current request object.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return WP_REST_Response|null
	 */
	public function get_gateway( $request ) {
		$gateway          = null;
		$payment_gateways = WC()->payment_gateways->payment_gateways();
		foreach ( $payment_gateways as $payment_gateway_id => $payment_gateway ) {
			if ( $request['id'] !== $payment_gateway_id ) {
				continue;
			}
			$payment_gateway->id = $payment_gateway_id;
			$gateway             = $payment_gateway;
		}
		return $gateway;
	}

	/**
	 * Prepare a payment gateway for response.
	 *
	 * @param  WC_Payment_Gateway $gateway    Payment gateway object.
	 * @param  WP_REST_Request    $request    Request object.
	 * @return WP_REST_Response   $response   Response data.
	 */
	public function prepare_item_for_response( $gateway, $request ) {
		$order = (array) get_option( 'woocommerce_gateway_order' );
		$item  = array(
			'id'                 => $gateway->id,
			'title'              => $gateway->title,
			'description'        => $gateway->description,
			'order'              => isset( $order[ $gateway->id ] ) ? $order[ $gateway->id ] : '',
			'enabled'            => ( 'yes' === $gateway->enabled ),
			'method_title'       => $gateway->get_method_title(),
			'method_description' => $gateway->get_method_description(),
			'settings'           => $this->get_settings( $gateway ),
		);

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $item, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		$response = rest_ensure_response( $data );
		$response->add_links( $this->prepare_links( $gateway, $request ) );

		/**
		 * Filter payment gateway objects returned from the REST API.
		 *
		 * @param WP_REST_Response   $response The response object.
		 * @param WC_Payment_Gateway $gateway  Payment gateway object.
		 * @param WP_REST_Request    $request  Request object.
		 */
		return apply_filters( 'woocommerce_rest_prepare_payment_gateway', $response, $gateway, $request );
	}

	/**
	 * Return settings associated with this payment gateway.
	 *
	 * @param WC_Payment_Gateway $gateway Gateway data.
	 *
	 * @return array
	 */
	public function get_settings( $gateway ) {
		$settings = array();
		$gateway->init_form_fields();
		foreach ( $gateway->form_fields as $id => $field ) {
			// Make sure we at least have a title and type.
			if ( empty( $field['title'] ) || empty( $field['type'] ) ) {
				continue;
			}
			// Ignore 'title' settings/fields -- they are UI only.
			if ( 'title' === $field['type'] ) {
				continue;
			}
			// Ignore 'enabled' and 'description' which get included elsewhere.
			if ( in_array( $id, array( 'enabled', 'description' ), true ) ) {
				continue;
			}
			$data = array(
				'id'          => $id,
				'label'       => empty( $field['label'] ) ? $field['title'] : $field['label'],
				'description' => empty( $field['description'] ) ? '' : $field['description'],
				'type'        => $field['type'],
				'value'       => empty( $gateway->settings[ $id ] ) ? '' : $gateway->settings[ $id ],
				'default'     => empty( $field['default'] ) ? '' : $field['default'],
				'tip'         => empty( $field['description'] ) ? '' : $field['description'],
				'placeholder' => empty( $field['placeholder'] ) ? '' : $field['placeholder'],
			);
			if ( ! empty( $field['options'] ) ) {
				$data['options'] = $field['options'];
			}
			$settings[ $id ] = $data;
		}
		return $settings;
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param  WC_Payment_Gateway $gateway    Payment gateway object.
	 * @param  WP_REST_Request    $request    Request object.
	 * @return array
	 */
	protected function prepare_links( $gateway, $request ) {
		$links = array(
			'self'       => array(
				'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $gateway->id ) ),
			),
			'collection' => array(
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
			),
		);

		return $links;
	}

	/**
	 * Get the payment gateway schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'payment_gateway',
			'type'       => 'object',
			'properties' => array(
				'id'                 => array(
					'description' => __( 'Payment gateway ID.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'title'              => array(
					'description' => __( 'Payment gateway title on checkout.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'description'        => array(
					'description' => __( 'Payment gateway description on checkout.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'order'              => array(
					'description' => __( 'Payment gateway sort order.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'absint',
					),
				),
				'enabled'            => array(
					'description' => __( 'Payment gateway enabled status.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
				),
				'method_title'       => array(
					'description' => __( 'Payment gateway method title.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'method_description' => array(
					'description' => __( 'Payment gateway method description.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'settings'           => array(
					'description' => __( 'Payment gateway settings.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'properties'  => array(
						'id'          => array(
							'description' => __( 'A unique identifier for the setting.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'label'       => array(
							'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'description' => array(
							'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'type'        => array(
							'description' => __( 'Type of setting.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
							'enum'        => array( 'text', 'email', 'number', 'color', 'password', 'textarea', 'select', 'multiselect', 'radio', 'image_width', 'checkbox' ),
							'readonly'    => true,
						),
						'value'       => array(
							'description' => __( 'Setting value.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'default'     => array(
							'description' => __( 'Default value for the setting.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'tip'         => array(
							'description' => __( 'Additional help text shown to the user about the setting.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'placeholder' => array(
							'description' => __( 'Placeholder text to be displayed in text inputs.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
					),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get any query params needed.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		return array(
			'context' => $this->get_context_param( array( 'default' => 'view' ) ),
		);
	}

}
rest-api/Controllers/Version2/class-wc-rest-product-attributes-v2-controller.php000064400000001026151336044760024065 0ustar00<?php
/**
 * REST API Product Attributes controller
 *
 * Handles requests to the products/attributes endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Product Attributes controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Product_Attributes_V1_Controller
 */
class WC_REST_Product_Attributes_V2_Controller extends WC_REST_Product_Attributes_V1_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';
}
rest-api/Controllers/Version2/class-wc-rest-order-refunds-v2-controller.php000064400000045375151336044760023017 0ustar00<?php
/**
 * REST API Order Refunds controller
 *
 * Handles requests to the /orders/<order_id>/refunds endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Order Refunds controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Orders_V2_Controller
 */
class WC_REST_Order_Refunds_V2_Controller extends WC_REST_Orders_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'orders/(?P<order_id>[\d]+)/refunds';

	/**
	 * Post type.
	 *
	 * @var string
	 */
	protected $post_type = 'shop_order_refund';

	/**
	 * Stores the request.
	 *
	 * @var array
	 */
	protected $request = array();

	/**
	 * Order refunds actions.
	 */
	public function __construct() {
		add_filter( "woocommerce_rest_{$this->post_type}_object_trashable", '__return_false' );
	}

	/**
	 * Register the routes for order refunds.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				'args'   => array(
					'order_id' => array(
						'description' => __( 'The order ID.', 'woocommerce' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_item' ),
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\d]+)',
			array(
				'args'   => array(
					'order_id' => array(
						'description' => __( 'The order ID.', 'woocommerce' ),
						'type'        => 'integer',
					),
					'id'       => array(
						'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => array(
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_item' ),
					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
					'args'                => array(
						'force' => array(
							'default'     => true,
							'type'        => 'boolean',
							'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
						),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Get object.
	 *
	 * @since  3.0.0
	 * @param  int $id Object ID.
	 * @return WC_Data
	 */
	protected function get_object( $id ) {
		return wc_get_order( $id );
	}

	/**
	 * Get formatted item data.
	 *
	 * @since  3.0.0
	 * @param  WC_Data $object WC_Data instance.
	 * @return array
	 */
	protected function get_formatted_item_data( $object ) {
		$data              = $object->get_data();
		$format_decimal    = array( 'amount' );
		$format_date       = array( 'date_created' );
		$format_line_items = array( 'line_items', 'shipping_lines', 'tax_lines', 'fee_lines' );

		// Format decimal values.
		foreach ( $format_decimal as $key ) {
			$data[ $key ] = wc_format_decimal( $data[ $key ], $this->request['dp'] );
		}

		// Format date values.
		foreach ( $format_date as $key ) {
			$datetime              = $data[ $key ];
			$data[ $key ]          = wc_rest_prepare_date_response( $datetime, false );
			$data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime );
		}

		// Format line items.
		foreach ( $format_line_items as $key ) {
			$data[ $key ] = array_values( array_map( array( $this, 'get_order_item_data' ), $data[ $key ] ) );
		}

		return array(
			'id'               => $object->get_id(),
			'date_created'     => $data['date_created'],
			'date_created_gmt' => $data['date_created_gmt'],
			'amount'           => $data['amount'],
			'reason'           => $data['reason'],
			'refunded_by'      => $data['refunded_by'],
			'refunded_payment' => $data['refunded_payment'],
			'meta_data'        => $data['meta_data'],
			'line_items'       => $data['line_items'],
			'shipping_lines'   => $data['shipping_lines'],
			'tax_lines'        => $data['tax_lines'],
			'fee_lines'        => $data['fee_lines'],
		);
	}

	/**
	 * Prepare a single order output for response.
	 *
	 * @since  3.0.0
	 *
	 * @param  WC_Data         $object  Object data.
	 * @param  WP_REST_Request $request Request object.
	 *
	 * @return WP_Error|WP_REST_Response
	 */
	public function prepare_object_for_response( $object, $request ) {
		$this->request       = $request;
		$this->request['dp'] = is_null( $this->request['dp'] ) ? wc_get_price_decimals() : absint( $this->request['dp'] );
		$order               = wc_get_order( (int) $request['order_id'] );

		if ( ! $order ) {
			return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), 404 );
		}

		if ( ! $object || $object->get_parent_id() !== $order->get_id() ) {
			return new WP_Error( 'woocommerce_rest_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 404 );
		}

		$data    = $this->get_formatted_item_data( $object );
		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $object, $request ) );

		/**
		 * Filter the data for a response.
		 *
		 * The dynamic portion of the hook name, $this->post_type,
		 * refers to object type being prepared for the response.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param WC_Data          $object   Object data.
		 * @param WP_REST_Request  $request  Request object.
		 */
		return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param WC_Data         $object  Object data.
	 * @param WP_REST_Request $request Request object.
	 * @return array                   Links for the given post.
	 */
	protected function prepare_links( $object, $request ) {
		$base  = str_replace( '(?P<order_id>[\d]+)', $object->get_parent_id(), $this->rest_base );
		$links = array(
			'self'       => array(
				'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $object->get_id() ) ),
			),
			'collection' => array(
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ),
			),
			'up'         => array(
				'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $object->get_parent_id() ) ),
			),
		);

		return $links;
	}

	/**
	 * Prepare objects query.
	 *
	 * @since  3.0.0
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return array
	 */
	protected function prepare_objects_query( $request ) {
		$args = parent::prepare_objects_query( $request );

		$args['post_status']     = array_keys( wc_get_order_statuses() );
		$args['post_parent__in'] = array( absint( $request['order_id'] ) );

		return $args;
	}

	/**
	 * Prepares one object for create or update operation.
	 *
	 * @since  3.0.0
	 * @param  WP_REST_Request $request Request object.
	 * @param  bool            $creating If is creating a new object.
	 * @return WP_Error|WC_Data The prepared item, or WP_Error object on failure.
	 */
	protected function prepare_object_for_database( $request, $creating = false ) {
		$order = wc_get_order( (int) $request['order_id'] );

		if ( ! $order ) {
			return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), 404 );
		}

		if ( 0 > $request['amount'] ) {
			return new WP_Error( 'woocommerce_rest_invalid_order_refund', __( 'Refund amount must be greater than zero.', 'woocommerce' ), 400 );
		}

		// Create the refund.
		$refund = wc_create_refund(
			array(
				'order_id'       => $order->get_id(),
				'amount'         => $request['amount'],
				'reason'         => empty( $request['reason'] ) ? null : $request['reason'],
				'refund_payment' => is_bool( $request['api_refund'] ) ? $request['api_refund'] : true,
				'restock_items'  => true,
			)
		);

		if ( is_wp_error( $refund ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', $refund->get_error_message(), 500 );
		}

		if ( ! $refund ) {
			return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 );
		}

		if ( ! empty( $request['meta_data'] ) && is_array( $request['meta_data'] ) ) {
			foreach ( $request['meta_data'] as $meta ) {
				$refund->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
			}
			$refund->save_meta_data();
		}

		/**
		 * Filters an object before it is inserted via the REST API.
		 *
		 * The dynamic portion of the hook name, `$this->post_type`,
		 * refers to the object type slug.
		 *
		 * @param WC_Data         $coupon   Object object.
		 * @param WP_REST_Request $request  Request object.
		 * @param bool            $creating If is creating a new object.
		 */
		return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $refund, $request, $creating );
	}

	/**
	 * Save an object data.
	 *
	 * @since  3.0.0
	 * @param  WP_REST_Request $request  Full details about the request.
	 * @param  bool            $creating If is creating a new object.
	 * @return WC_Data|WP_Error
	 */
	protected function save_object( $request, $creating = false ) {
		try {
			$object = $this->prepare_object_for_database( $request, $creating );

			if ( is_wp_error( $object ) ) {
				return $object;
			}

			return $this->get_object( $object->get_id() );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
		} catch ( WC_REST_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the refund schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => $this->post_type,
			'type'       => 'object',
			'properties' => array(
				'id'               => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_created'     => array(
					'description' => __( "The date the order refund was created, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_created_gmt' => array(
					'description' => __( 'The date the order refund was created, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'amount'           => array(
					'description' => __( 'Refund amount.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'reason'           => array(
					'description' => __( 'Reason for refund.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'refunded_by'      => array(
					'description' => __( 'User ID of user who created the refund.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'refunded_payment' => array(
					'description' => __( 'If the payment was refunded via the API.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'meta_data'        => array(
					'description' => __( 'Meta data.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'    => array(
								'description' => __( 'Meta ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'key'   => array(
								'description' => __( 'Meta key.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'value' => array(
								'description' => __( 'Meta value.', 'woocommerce' ),
								'type'        => 'mixed',
								'context'     => array( 'view', 'edit' ),
							),
						),
					),
				),
				'line_items'       => array(
					'description' => __( 'Line items data.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'           => array(
								'description' => __( 'Item ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'name'         => array(
								'description' => __( 'Product name.', 'woocommerce' ),
								'type'        => 'mixed',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'product_id'   => array(
								'description' => __( 'Product ID.', 'woocommerce' ),
								'type'        => 'mixed',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'variation_id' => array(
								'description' => __( 'Variation ID, if applicable.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'quantity'     => array(
								'description' => __( 'Quantity ordered.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'tax_class'    => array(
								'description' => __( 'Tax class of product.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'subtotal'     => array(
								'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'subtotal_tax' => array(
								'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'total'        => array(
								'description' => __( 'Line total (after discounts).', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'total_tax'    => array(
								'description' => __( 'Line total tax (after discounts).', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'taxes'        => array(
								'description' => __( 'Line taxes.', 'woocommerce' ),
								'type'        => 'array',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
								'items'       => array(
									'type'       => 'object',
									'properties' => array(
										'id'       => array(
											'description' => __( 'Tax rate ID.', 'woocommerce' ),
											'type'        => 'integer',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
										'total'    => array(
											'description' => __( 'Tax total.', 'woocommerce' ),
											'type'        => 'string',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
										'subtotal' => array(
											'description' => __( 'Tax subtotal.', 'woocommerce' ),
											'type'        => 'string',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
									),
								),
							),
							'meta_data'    => array(
								'description' => __( 'Meta data.', 'woocommerce' ),
								'type'        => 'array',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
								'items'       => array(
									'type'       => 'object',
									'properties' => array(
										'id'    => array(
											'description' => __( 'Meta ID.', 'woocommerce' ),
											'type'        => 'integer',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
										'key'   => array(
											'description' => __( 'Meta key.', 'woocommerce' ),
											'type'        => 'string',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
										'value' => array(
											'description' => __( 'Meta value.', 'woocommerce' ),
											'type'        => 'mixed',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
									),
								),
							),
							'sku'          => array(
								'description' => __( 'Product SKU.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'price'        => array(
								'description' => __( 'Product price.', 'woocommerce' ),
								'type'        => 'number',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
						),
					),
				),
				'api_refund'       => array(
					'description' => __( 'When true, the payment gateway API is used to generate the refund.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'edit' ),
					'default'     => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params = parent::get_collection_params();

		unset( $params['status'], $params['customer'], $params['product'] );

		return $params;
	}
}
rest-api/Controllers/Version2/class-wc-rest-taxes-v2-controller.php000064400000000710151336044760021344 0ustar00<?php
/**
 * REST API Taxes controller
 *
 * Handles requests to the /taxes endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Taxes controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Taxes_V1_Controller
 */
class WC_REST_Taxes_V2_Controller extends WC_REST_Taxes_V1_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';
}
rest-api/Controllers/Version2/class-wc-rest-coupons-v2-controller.php000064400000045157151336044760021724 0ustar00<?php
/**
 * REST API Coupons controller
 *
 * Handles requests to the /coupons endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Coupons controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_CRUD_Controller
 */
class WC_REST_Coupons_V2_Controller extends WC_REST_CRUD_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'coupons';

	/**
	 * Post type.
	 *
	 * @var string
	 */
	protected $post_type = 'shop_coupon';

	/**
	 * Register the routes for coupons.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace, '/' . $this->rest_base, array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_item' ),
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
					'args'                => array_merge(
						$this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array(
							'code' => array(
								'description' => __( 'Coupon code.', 'woocommerce' ),
								'required'    => true,
								'type'        => 'string',
							),
						)
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
				'args'   => array(
					'id' => array(
						'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => array(
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_item' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_item' ),
					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
					'args'                => array(
						'force' => array(
							'default'     => false,
							'type'        => 'boolean',
							'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ),
						),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace, '/' . $this->rest_base . '/batch', array(
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'batch_items' ),
					'permission_callback' => array( $this, 'batch_items_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				'schema' => array( $this, 'get_public_batch_schema' ),
			)
		);
	}

	/**
	 * Get object.
	 *
	 * @since  3.0.0
	 * @param  int $id Object ID.
	 * @return WC_Data
	 */
	protected function get_object( $id ) {
		return new WC_Coupon( $id );
	}

	/**
	 * Get formatted item data.
	 *
	 * @since  3.0.0
	 * @param  WC_Data $object WC_Data instance.
	 * @return array
	 */
	protected function get_formatted_item_data( $object ) {
		$data = $object->get_data();

		$format_decimal = array( 'amount', 'minimum_amount', 'maximum_amount' );
		$format_date    = array( 'date_created', 'date_modified', 'date_expires' );
		$format_null    = array( 'usage_limit', 'usage_limit_per_user', 'limit_usage_to_x_items' );

		// Format decimal values.
		foreach ( $format_decimal as $key ) {
			$data[ $key ] = wc_format_decimal( $data[ $key ], 2 );
		}

		// Format date values.
		foreach ( $format_date as $key ) {
			$datetime              = $data[ $key ];
			$data[ $key ]          = wc_rest_prepare_date_response( $datetime, false );
			$data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime );
		}

		// Format null values.
		foreach ( $format_null as $key ) {
			$data[ $key ] = $data[ $key ] ? $data[ $key ] : null;
		}

		return array(
			'id'                          => $object->get_id(),
			'code'                        => $data['code'],
			'amount'                      => $data['amount'],
			'date_created'                => $data['date_created'],
			'date_created_gmt'            => $data['date_created_gmt'],
			'date_modified'               => $data['date_modified'],
			'date_modified_gmt'           => $data['date_modified_gmt'],
			'discount_type'               => $data['discount_type'],
			'description'                 => $data['description'],
			'date_expires'                => $data['date_expires'],
			'date_expires_gmt'            => $data['date_expires_gmt'],
			'usage_count'                 => $data['usage_count'],
			'individual_use'              => $data['individual_use'],
			'product_ids'                 => $data['product_ids'],
			'excluded_product_ids'        => $data['excluded_product_ids'],
			'usage_limit'                 => $data['usage_limit'],
			'usage_limit_per_user'        => $data['usage_limit_per_user'],
			'limit_usage_to_x_items'      => $data['limit_usage_to_x_items'],
			'free_shipping'               => $data['free_shipping'],
			'product_categories'          => $data['product_categories'],
			'excluded_product_categories' => $data['excluded_product_categories'],
			'exclude_sale_items'          => $data['exclude_sale_items'],
			'minimum_amount'              => $data['minimum_amount'],
			'maximum_amount'              => $data['maximum_amount'],
			'email_restrictions'          => $data['email_restrictions'],
			'used_by'                     => $data['used_by'],
			'meta_data'                   => $data['meta_data'],
		);
	}

	/**
	 * Prepare a single coupon output for response.
	 *
	 * @since  3.0.0
	 * @param  WC_Data         $object  Object data.
	 * @param  WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_object_for_response( $object, $request ) {
		$data     = $this->get_formatted_item_data( $object );
		$context  = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data     = $this->add_additional_fields_to_object( $data, $request );
		$data     = $this->filter_response_by_context( $data, $context );
		$response = rest_ensure_response( $data );
		$response->add_links( $this->prepare_links( $object, $request ) );

		/**
		 * Filter the data for a response.
		 *
		 * The dynamic portion of the hook name, $this->post_type,
		 * refers to object type being prepared for the response.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param WC_Data          $object   Object data.
		 * @param WP_REST_Request  $request  Request object.
		 */
		return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request );
	}

	/**
	 * Prepare objects query.
	 *
	 * @since  3.0.0
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return array
	 */
	protected function prepare_objects_query( $request ) {
		$args = parent::prepare_objects_query( $request );

		if ( ! empty( $request['code'] ) ) {
			$id               = wc_get_coupon_id_by_code( $request['code'] );
			$args['post__in'] = array( $id );
		}

		// Get only ids.
		$args['fields'] = 'ids';

		return $args;
	}

	/**
	 * Only return writable props from schema.
	 *
	 * @param  array $schema Schema.
	 * @return bool
	 */
	protected function filter_writable_props( $schema ) {
		return empty( $schema['readonly'] );
	}

	/**
	 * Prepare a single coupon for create or update.
	 *
	 * @param  WP_REST_Request $request Request object.
	 * @param  bool            $creating If is creating a new object.
	 * @return WP_Error|WC_Data
	 */
	protected function prepare_object_for_database( $request, $creating = false ) {
		$id        = isset( $request['id'] ) ? absint( $request['id'] ) : 0;
		$coupon    = new WC_Coupon( $id );
		$schema    = $this->get_item_schema();
		$data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) );

		// Validate required POST fields.
		if ( $creating && empty( $request['code'] ) ) {
			return new WP_Error( 'woocommerce_rest_empty_coupon_code', sprintf( __( 'The coupon code cannot be empty.', 'woocommerce' ), 'code' ), array( 'status' => 400 ) );
		}

		// Handle all writable props.
		foreach ( $data_keys as $key ) {
			$value = $request[ $key ];

			if ( ! is_null( $value ) ) {
				switch ( $key ) {
					case 'code':
						$coupon_code  = wc_format_coupon_code( $value );
						$id           = $coupon->get_id() ? $coupon->get_id() : 0;
						$id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id );

						if ( $id_from_code ) {
							return new WP_Error( 'woocommerce_rest_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), array( 'status' => 400 ) );
						}

						$coupon->set_code( $coupon_code );
						break;
					case 'meta_data':
						if ( is_array( $value ) ) {
							foreach ( $value as $meta ) {
								$coupon->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
							}
						}
						break;
					case 'description':
						$coupon->set_description( wp_filter_post_kses( $value ) );
						break;
					default:
						if ( is_callable( array( $coupon, "set_{$key}" ) ) ) {
							$coupon->{"set_{$key}"}( $value );
						}
						break;
				}
			}
		}

		/**
		 * Filters an object before it is inserted via the REST API.
		 *
		 * The dynamic portion of the hook name, `$this->post_type`,
		 * refers to the object type slug.
		 *
		 * @param WC_Data         $coupon   Object object.
		 * @param WP_REST_Request $request  Request object.
		 * @param bool            $creating If is creating a new object.
		 */
		return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $coupon, $request, $creating );
	}

	/**
	 * Get the Coupon's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => $this->post_type,
			'type'       => 'object',
			'properties' => array(
				'id'                          => array(
					'description' => __( 'Unique identifier for the object.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'code'                        => array(
					'description' => __( 'Coupon code.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'amount'                      => array(
					'description' => __( 'The amount of discount. Should always be numeric, even if setting a percentage.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'date_created'                => array(
					'description' => __( "The date the coupon was created, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_created_gmt'            => array(
					'description' => __( 'The date the coupon was created, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_modified'               => array(
					'description' => __( "The date the coupon was last modified, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_modified_gmt'           => array(
					'description' => __( 'The date the coupon was last modified, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'discount_type'               => array(
					'description' => __( 'Determines the type of discount that will be applied.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'fixed_cart',
					'enum'        => array_keys( wc_get_coupon_types() ),
					'context'     => array( 'view', 'edit' ),
				),
				'description'                 => array(
					'description' => __( 'Coupon description.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'date_expires'                => array(
					'description' => __( "The date the coupon expires, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'date_expires_gmt'            => array(
					'description' => __( 'The date the coupon expires, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'usage_count'                 => array(
					'description' => __( 'Number of times the coupon has been used already.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'individual_use'              => array(
					'description' => __( 'If true, the coupon can only be used individually. Other applied coupons will be removed from the cart.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'product_ids'                 => array(
					'description' => __( 'List of product IDs the coupon can be used on.', 'woocommerce' ),
					'type'        => 'array',
					'items'       => array(
						'type' => 'integer',
					),
					'context'     => array( 'view', 'edit' ),
				),
				'excluded_product_ids'        => array(
					'description' => __( 'List of product IDs the coupon cannot be used on.', 'woocommerce' ),
					'type'        => 'array',
					'items'       => array(
						'type' => 'integer',
					),
					'context'     => array( 'view', 'edit' ),
				),
				'usage_limit'                 => array(
					'description' => __( 'How many times the coupon can be used in total.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'usage_limit_per_user'        => array(
					'description' => __( 'How many times the coupon can be used per customer.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'limit_usage_to_x_items'      => array(
					'description' => __( 'Max number of items in the cart the coupon can be applied to.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'free_shipping'               => array(
					'description' => __( 'If true and if the free shipping method requires a coupon, this coupon will enable free shipping.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'product_categories'          => array(
					'description' => __( 'List of category IDs the coupon applies to.', 'woocommerce' ),
					'type'        => 'array',
					'items'       => array(
						'type' => 'integer',
					),
					'context'     => array( 'view', 'edit' ),
				),
				'excluded_product_categories' => array(
					'description' => __( 'List of category IDs the coupon does not apply to.', 'woocommerce' ),
					'type'        => 'array',
					'items'       => array(
						'type' => 'integer',
					),
					'context'     => array( 'view', 'edit' ),
				),
				'exclude_sale_items'          => array(
					'description' => __( 'If true, this coupon will not be applied to items that have sale prices.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'minimum_amount'              => array(
					'description' => __( 'Minimum order amount that needs to be in the cart before coupon applies.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'maximum_amount'              => array(
					'description' => __( 'Maximum order amount allowed when using the coupon.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'email_restrictions'          => array(
					'description' => __( 'List of email addresses that can use this coupon.', 'woocommerce' ),
					'type'        => 'array',
					'items'       => array(
						'type' => 'string',
					),
					'context'     => array( 'view', 'edit' ),
				),
				'used_by'                     => array(
					'description' => __( 'List of user IDs (or guest email addresses) that have used the coupon.', 'woocommerce' ),
					'type'        => 'array',
					'items'       => array(
						'type' => 'integer',
					),
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'meta_data'                   => array(
					'description' => __( 'Meta data.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'    => array(
								'description' => __( 'Meta ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'key'   => array(
								'description' => __( 'Meta key.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'value' => array(
								'description' => __( 'Meta value.', 'woocommerce' ),
								'type'        => 'mixed',
								'context'     => array( 'view', 'edit' ),
							),
						),
					),
				),
			),
		);
		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections of attachments.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params = parent::get_collection_params();

		$params['code'] = array(
			'description'       => __( 'Limit result set to resources with a specific code.', 'woocommerce' ),
			'type'              => 'string',
			'sanitize_callback' => 'sanitize_text_field',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}
}
rest-api/Controllers/Version2/class-wc-rest-shipping-zone-methods-v2-controller.php000064400000042503151336044760024461 0ustar00<?php
/**
 * REST API Shipping Zone Methods controller
 *
 * Handles requests to the /shipping/zones/<id>/methods endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Shipping Zone Methods class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Shipping_Zones_Controller_Base
 */
class WC_REST_Shipping_Zone_Methods_V2_Controller extends WC_REST_Shipping_Zones_Controller_Base {

	/**
	 * Register the routes for Shipping Zone Methods.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace, '/' . $this->rest_base . '/(?P<zone_id>[\d]+)/methods', array(
				'args'   => array(
					'zone_id' => array(
						'description' => __( 'Unique ID for the zone.', 'woocommerce' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_item' ),
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
					'args'                => array_merge(
						$this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array(
							'method_id' => array(
								'required'    => true,
								'readonly'    => false,
								'description' => __( 'Shipping method ID.', 'woocommerce' ),
							),
						)
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace, '/' . $this->rest_base . '/(?P<zone_id>[\d]+)/methods/(?P<instance_id>[\d]+)', array(
				'args'   => array(
					'zone_id'     => array(
						'description' => __( 'Unique ID for the zone.', 'woocommerce' ),
						'type'        => 'integer',
					),
					'instance_id' => array(
						'description' => __( 'Unique ID for the instance.', 'woocommerce' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_item' ),
					'permission_callback' => array( $this, 'update_items_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_item' ),
					'permission_callback' => array( $this, 'delete_items_permissions_check' ),
					'args'                => array(
						'force' => array(
							'default'     => false,
							'type'        => 'boolean',
							'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ),
						),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Get a single Shipping Zone Method.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return WP_REST_Response|WP_Error
	 */
	public function get_item( $request ) {
		$zone = $this->get_zone( $request['zone_id'] );

		if ( is_wp_error( $zone ) ) {
			return $zone;
		}

		$instance_id = (int) $request['instance_id'];
		$methods     = $zone->get_shipping_methods();
		$method      = false;

		foreach ( $methods as $method_obj ) {
			if ( $instance_id === $method_obj->instance_id ) {
				$method = $method_obj;
				break;
			}
		}

		if ( false === $method ) {
			return new WP_Error( 'woocommerce_rest_shipping_zone_method_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$data = $this->prepare_item_for_response( $method, $request );

		return rest_ensure_response( $data );
	}

	/**
	 * Get all Shipping Zone Methods.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return WP_REST_Response|WP_Error
	 */
	public function get_items( $request ) {
		$zone = $this->get_zone( $request['zone_id'] );

		if ( is_wp_error( $zone ) ) {
			return $zone;
		}

		$methods = $zone->get_shipping_methods();
		$data    = array();

		foreach ( $methods as $method_obj ) {
			$method = $this->prepare_item_for_response( $method_obj, $request );
			$data[] = $method;
		}

		return rest_ensure_response( $data );
	}

	/**
	 * Create a new shipping zone method instance.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Request|WP_Error
	 */
	public function create_item( $request ) {
		$method_id = $request['method_id'];
		$zone      = $this->get_zone( $request['zone_id'] );
		if ( is_wp_error( $zone ) ) {
			return $zone;
		}

		$instance_id = $zone->add_shipping_method( $method_id );
		$methods     = $zone->get_shipping_methods();
		$method      = false;
		foreach ( $methods as $method_obj ) {
			if ( $instance_id === $method_obj->instance_id ) {
				$method = $method_obj;
				break;
			}
		}

		if ( false === $method ) {
			return new WP_Error( 'woocommerce_rest_shipping_zone_not_created', __( 'Resource cannot be created.', 'woocommerce' ), array( 'status' => 500 ) );
		}

		$method = $this->update_fields( $instance_id, $method, $request );
		if ( is_wp_error( $method ) ) {
			return $method;
		}

		$data = $this->prepare_item_for_response( $method, $request );
		return rest_ensure_response( $data );
	}

	/**
	 * Delete a shipping method instance.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function delete_item( $request ) {
		$zone = $this->get_zone( $request['zone_id'] );
		if ( is_wp_error( $zone ) ) {
			return $zone;
		}

		$instance_id = (int) $request['instance_id'];
		$force       = $request['force'];

		$methods = $zone->get_shipping_methods();
		$method  = false;

		foreach ( $methods as $method_obj ) {
			if ( $instance_id === $method_obj->instance_id ) {
				$method = $method_obj;
				break;
			}
		}

		if ( false === $method ) {
			return new WP_Error( 'woocommerce_rest_shipping_zone_method_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$method = $this->update_fields( $instance_id, $method, $request );
		if ( is_wp_error( $method ) ) {
			return $method;
		}

		$request->set_param( 'context', 'view' );
		$response = $this->prepare_item_for_response( $method, $request );

		// Actually delete.
		if ( $force ) {
			$zone->delete_shipping_method( $instance_id );
		} else {
			return new WP_Error( 'rest_trash_not_supported', __( 'Shipping methods do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) );
		}

		/**
		 * Fires after a product review is deleted via the REST API.
		 *
		 * @param object           $method
		 * @param WP_REST_Response $response        The response data.
		 * @param WP_REST_Request  $request         The request sent to the API.
		 */
		do_action( 'rest_delete_product_review', $method, $response, $request );

		return $response;
	}

	/**
	 * Update A Single Shipping Zone Method.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return WP_REST_Response|WP_Error
	 */
	public function update_item( $request ) {
		$zone = $this->get_zone( $request['zone_id'] );
		if ( is_wp_error( $zone ) ) {
			return $zone;
		}

		$instance_id = (int) $request['instance_id'];
		$methods     = $zone->get_shipping_methods();
		$method      = false;

		foreach ( $methods as $method_obj ) {
			if ( $instance_id === $method_obj->instance_id ) {
				$method = $method_obj;
				break;
			}
		}

		if ( false === $method ) {
			return new WP_Error( 'woocommerce_rest_shipping_zone_method_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$method = $this->update_fields( $instance_id, $method, $request );
		if ( is_wp_error( $method ) ) {
			return $method;
		}

		$data = $this->prepare_item_for_response( $method, $request );
		return rest_ensure_response( $data );
	}

	/**
	 * Updates settings, order, and enabled status on create.
	 *
	 * @param int                $instance_id Instance ID.
	 * @param WC_Shipping_Method $method      Shipping method data.
	 * @param WP_REST_Request    $request     Request data.
	 *
	 * @return WC_Shipping_Method
	 */
	public function update_fields( $instance_id, $method, $request ) {
		global $wpdb;

		// Update settings if present.
		if ( isset( $request['settings'] ) ) {
			$method->init_instance_settings();
			$instance_settings = $method->instance_settings;
			$errors_found      = false;
			foreach ( $method->get_instance_form_fields() as $key => $field ) {
				if ( isset( $request['settings'][ $key ] ) ) {
					if ( is_callable( array( $this, 'validate_setting_' . $field['type'] . '_field' ) ) ) {
						$value = $this->{'validate_setting_' . $field['type'] . '_field'}( $request['settings'][ $key ], $field );
					} else {
						$value = $this->validate_setting_text_field( $request['settings'][ $key ], $field );
					}
					if ( is_wp_error( $value ) ) {
						$errors_found = true;
						break;
					}
					$instance_settings[ $key ] = $value;
				}
			}

			if ( $errors_found ) {
				return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) );
			}

			update_option( $method->get_instance_option_key(), apply_filters( 'woocommerce_shipping_' . $method->id . '_instance_settings_values', $instance_settings, $method ) );
		}

		// Update order.
		if ( isset( $request['order'] ) ) {
			$wpdb->update( "{$wpdb->prefix}woocommerce_shipping_zone_methods", array( 'method_order' => absint( $request['order'] ) ), array( 'instance_id' => absint( $instance_id ) ) );
			$method->method_order = absint( $request['order'] );
		}

		// Update if this method is enabled or not.
		if ( isset( $request['enabled'] ) ) {
			if ( $wpdb->update( "{$wpdb->prefix}woocommerce_shipping_zone_methods", array( 'is_enabled' => $request['enabled'] ), array( 'instance_id' => absint( $instance_id ) ) ) ) {
				do_action( 'woocommerce_shipping_zone_method_status_toggled', $instance_id, $method->id, $request['zone_id'], $request['enabled'] );
				$method->enabled = ( true === $request['enabled'] ? 'yes' : 'no' );
			}
		}

		return $method;
	}

	/**
	 * Prepare the Shipping Zone Method for the REST response.
	 *
	 * @param array           $item Shipping Zone Method.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response
	 */
	public function prepare_item_for_response( $item, $request ) {
		$method = array(
			'id'                 => $item->instance_id,
			'instance_id'        => $item->instance_id,
			'title'              => $item->instance_settings['title'],
			'order'              => $item->method_order,
			'enabled'            => ( 'yes' === $item->enabled ),
			'method_id'          => $item->id,
			'method_title'       => $item->method_title,
			'method_description' => $item->method_description,
			'settings'           => $this->get_settings( $item ),
		);

		$context = empty( $request['context'] ) ? 'view' : $request['context'];
		$data    = $this->add_additional_fields_to_object( $method, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $request['zone_id'], $item->instance_id ) );

		$response = $this->prepare_response_for_collection( $response );

		return $response;
	}

	/**
	 * Return settings associated with this shipping zone method instance.
	 *
	 * @param WC_Shipping_Method $item Shipping method data.
	 *
	 * @return array
	 */
	public function get_settings( $item ) {
		$item->init_instance_settings();
		$settings = array();
		foreach ( $item->get_instance_form_fields() as $id => $field ) {
			$data = array(
				'id'          => $id,
				'label'       => $field['title'],
				'description' => empty( $field['description'] ) ? '' : $field['description'],
				'type'        => $field['type'],
				'value'       => $item->instance_settings[ $id ],
				'default'     => empty( $field['default'] ) ? '' : $field['default'],
				'tip'         => empty( $field['description'] ) ? '' : $field['description'],
				'placeholder' => empty( $field['placeholder'] ) ? '' : $field['placeholder'],
			);
			if ( ! empty( $field['options'] ) ) {
				$data['options'] = $field['options'];
			}
			$settings[ $id ] = $data;
		}
		return $settings;
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param int $zone_id Given Shipping Zone ID.
	 * @param int $instance_id Given Shipping Zone Method Instance ID.
	 * @return array Links for the given Shipping Zone Method.
	 */
	protected function prepare_links( $zone_id, $instance_id ) {
		$base  = '/' . $this->namespace . '/' . $this->rest_base . '/' . $zone_id;
		$links = array(
			'self'       => array(
				'href' => rest_url( $base . '/methods/' . $instance_id ),
			),
			'collection' => array(
				'href' => rest_url( $base . '/methods' ),
			),
			'describes'  => array(
				'href' => rest_url( $base ),
			),
		);

		return $links;
	}

	/**
	 * Get the Shipping Zone Methods schema, conforming to JSON Schema
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'shipping_zone_method',
			'type'       => 'object',
			'properties' => array(
				'id'                 => array(
					'description' => __( 'Shipping method instance ID.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'instance_id'        => array(
					'description' => __( 'Shipping method instance ID.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'title'              => array(
					'description' => __( 'Shipping method customer facing title.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'order'              => array(
					'description' => __( 'Shipping method sort order.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'enabled'            => array(
					'description' => __( 'Shipping method enabled status.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
				),
				'method_id'          => array(
					'description' => __( 'Shipping method ID.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'method_title'       => array(
					'description' => __( 'Shipping method title.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'method_description' => array(
					'description' => __( 'Shipping method description.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'settings'           => array(
					'description' => __( 'Shipping method settings.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'properties'  => array(
						'id'          => array(
							'description' => __( 'A unique identifier for the setting.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'label'       => array(
							'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'description' => array(
							'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'type'        => array(
							'description' => __( 'Type of setting.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
							'enum'        => array( 'text', 'email', 'number', 'color', 'password', 'textarea', 'select', 'multiselect', 'radio', 'image_width', 'checkbox' ),
							'readonly'    => true,
						),
						'value'       => array(
							'description' => __( 'Setting value.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'default'     => array(
							'description' => __( 'Default value for the setting.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'tip'         => array(
							'description' => __( 'Additional help text shown to the user about the setting.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'placeholder' => array(
							'description' => __( 'Placeholder text to be displayed in text inputs.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
					),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version2/class-wc-rest-customer-downloads-v2-controller.php000064400000012675151336044760024066 0ustar00<?php
/**
 * REST API Customer Downloads controller
 *
 * Handles requests to the /customers/<customer_id>/downloads endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Customers controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Customer_Downloads_V1_Controller
 */
class WC_REST_Customer_Downloads_V2_Controller extends WC_REST_Customer_Downloads_V1_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';

	/**
	 * Prepare a single download output for response.
	 *
	 * @param stdClass        $download Download object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $download, $request ) {
		$data = array(
			'download_id'         => $download->download_id,
			'download_url'        => $download->download_url,
			'product_id'          => $download->product_id,
			'product_name'        => $download->product_name,
			'download_name'       => $download->download_name,
			'order_id'            => $download->order_id,
			'order_key'           => $download->order_key,
			'downloads_remaining' => '' === $download->downloads_remaining ? 'unlimited' : $download->downloads_remaining,
			'access_expires'      => $download->access_expires ? wc_rest_prepare_date_response( $download->access_expires ) : 'never',
			'access_expires_gmt'  => $download->access_expires ? wc_rest_prepare_date_response( get_gmt_from_date( $download->access_expires ) ) : 'never',
			'file'                => $download->file,
		);

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $download, $request ) );

		/**
		 * Filter customer download data returned from the REST API.
		 *
		 * @param WP_REST_Response $response  The response object.
		 * @param stdClass         $download  Download object used to create response.
		 * @param WP_REST_Request  $request   Request object.
		 */
		return apply_filters( 'woocommerce_rest_prepare_customer_download', $response, $download, $request );
	}

	/**
	 * Get the Customer Download's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'customer_download',
			'type'       => 'object',
			'properties' => array(
				'download_id'         => array(
					'description' => __( 'Download ID.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'download_url'        => array(
					'description' => __( 'Download file URL.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'product_id'          => array(
					'description' => __( 'Downloadable product ID.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'product_name'        => array(
					'description' => __( 'Product name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'download_name'       => array(
					'description' => __( 'Downloadable file name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'order_id'            => array(
					'description' => __( 'Order ID.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'order_key'           => array(
					'description' => __( 'Order key.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'downloads_remaining' => array(
					'description' => __( 'Number of downloads remaining.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'access_expires'      => array(
					'description' => __( "The date when download access expires, in the site's timezone.", 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'access_expires_gmt'  => array(
					'description' => __( 'The date when download access expires, as GMT.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'file'                => array(
					'description' => __( 'File details.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view' ),
					'readonly'    => true,
					'properties'  => array(
						'name' => array(
							'description' => __( 'File name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'file' => array(
							'description' => __( 'File URL.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
					),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version2/class-wc-rest-report-sales-v2-controller.php000064400000000755151336044760022651 0ustar00<?php
/**
 * REST API Reports controller
 *
 * Handles requests to the reports/sales endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Report Sales controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Report_Sales_V1_Controller
 */
class WC_REST_Report_Sales_V2_Controller extends WC_REST_Report_Sales_V1_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';
}
rest-api/Controllers/Version2/class-wc-rest-product-tags-v2-controller.php000064400000000762151336044760022643 0ustar00<?php
/**
 * REST API Product Tags controller
 *
 * Handles requests to the products/tags endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Product Tags controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Product_Tags_V1_Controller
 */
class WC_REST_Product_Tags_V2_Controller extends WC_REST_Product_Tags_V1_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';
}
rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php000064400000105277151336044760024073 0ustar00<?php
/**
 * REST API variations controller
 *
 * Handles requests to the /products/<product_id>/variations endpoints.
 *
 * @package WooCommerce\RestApi
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API variations controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Products_V2_Controller
 */
class WC_REST_Product_Variations_V2_Controller extends WC_REST_Products_V2_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'products/(?P<product_id>[\d]+)/variations';

	/**
	 * Post type.
	 *
	 * @var string
	 */
	protected $post_type = 'product_variation';

	/**
	 * Register the routes for products.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace, '/' . $this->rest_base, array(
				'args'   => array(
					'product_id' => array(
						'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_item' ),
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
		register_rest_route(
			$this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
				'args'   => array(
					'product_id' => array(
						'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ),
						'type'        => 'integer',
					),
					'id'         => array(
						'description' => __( 'Unique identifier for the variation.', 'woocommerce' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => array(
						'context' => $this->get_context_param(
							array(
								'default' => 'view',
							)
						),
					),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_item' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_item' ),
					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
					'args'                => array(
						'force' => array(
							'default'     => false,
							'type'        => 'boolean',
							'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ),
						),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
		register_rest_route(
			$this->namespace, '/' . $this->rest_base . '/batch', array(
				'args'   => array(
					'product_id' => array(
						'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'batch_items' ),
					'permission_callback' => array( $this, 'batch_items_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				'schema' => array( $this, 'get_public_batch_schema' ),
			)
		);
	}

	/**
	 * Get object.
	 *
	 * @since  3.0.0
	 * @param  int $id Object ID.
	 * @return WC_Data
	 */
	protected function get_object( $id ) {
		return wc_get_product( $id );
	}

	/**
	 * Check if a given request has access to update an item.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function update_item_permissions_check( $request ) {
		$object = $this->get_object( (int) $request['id'] );

		if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'edit', $object->get_id() ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		// Check if variation belongs to the correct parent product.
		if ( $object && 0 !== $object->get_parent_id() && absint( $request['product_id'] ) !== $object->get_parent_id() ) {
			return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Parent product does not match current variation.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Prepare a single variation output for response.
	 *
	 * @since  3.0.0
	 * @param  WC_Data         $object  Object data.
	 * @param  WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_object_for_response( $object, $request ) {
		$data = array(
			'id'                    => $object->get_id(),
			'date_created'          => wc_rest_prepare_date_response( $object->get_date_created(), false ),
			'date_created_gmt'      => wc_rest_prepare_date_response( $object->get_date_created() ),
			'date_modified'         => wc_rest_prepare_date_response( $object->get_date_modified(), false ),
			'date_modified_gmt'     => wc_rest_prepare_date_response( $object->get_date_modified() ),
			'description'           => wc_format_content( $object->get_description() ),
			'permalink'             => $object->get_permalink(),
			'sku'                   => $object->get_sku(),
			'price'                 => $object->get_price(),
			'regular_price'         => $object->get_regular_price(),
			'sale_price'            => $object->get_sale_price(),
			'date_on_sale_from'     => wc_rest_prepare_date_response( $object->get_date_on_sale_from(), false ),
			'date_on_sale_from_gmt' => wc_rest_prepare_date_response( $object->get_date_on_sale_from() ),
			'date_on_sale_to'       => wc_rest_prepare_date_response( $object->get_date_on_sale_to(), false ),
			'date_on_sale_to_gmt'   => wc_rest_prepare_date_response( $object->get_date_on_sale_to() ),
			'on_sale'               => $object->is_on_sale(),
			'visible'               => $object->is_visible(),
			'purchasable'           => $object->is_purchasable(),
			'virtual'               => $object->is_virtual(),
			'downloadable'          => $object->is_downloadable(),
			'downloads'             => $this->get_downloads( $object ),
			'download_limit'        => '' !== $object->get_download_limit() ? (int) $object->get_download_limit() : -1,
			'download_expiry'       => '' !== $object->get_download_expiry() ? (int) $object->get_download_expiry() : -1,
			'tax_status'            => $object->get_tax_status(),
			'tax_class'             => $object->get_tax_class(),
			'manage_stock'          => $object->managing_stock(),
			'stock_quantity'        => $object->get_stock_quantity(),
			'in_stock'              => $object->is_in_stock(),
			'backorders'            => $object->get_backorders(),
			'backorders_allowed'    => $object->backorders_allowed(),
			'backordered'           => $object->is_on_backorder(),
			'weight'                => $object->get_weight(),
			'dimensions'            => array(
				'length' => $object->get_length(),
				'width'  => $object->get_width(),
				'height' => $object->get_height(),
			),
			'shipping_class'        => $object->get_shipping_class(),
			'shipping_class_id'     => $object->get_shipping_class_id(),
			'image'                 => current( $this->get_images( $object ) ),
			'attributes'            => $this->get_attributes( $object ),
			'menu_order'            => $object->get_menu_order(),
			'meta_data'             => $object->get_meta_data(),
		);

		$context  = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data     = $this->add_additional_fields_to_object( $data, $request );
		$data     = $this->filter_response_by_context( $data, $context );
		$response = rest_ensure_response( $data );
		$response->add_links( $this->prepare_links( $object, $request ) );

		/**
		 * Filter the data for a response.
		 *
		 * The dynamic portion of the hook name, $this->post_type,
		 * refers to object type being prepared for the response.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param WC_Data          $object   Object data.
		 * @param WP_REST_Request  $request  Request object.
		 */
		return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request );
	}

	/**
	 * Prepare objects query.
	 *
	 * @since  3.0.0
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return array
	 */
	protected function prepare_objects_query( $request ) {
		$args = parent::prepare_objects_query( $request );

		$args['post_parent'] = $request['product_id'];

		return $args;
	}

	/**
	 * Prepare a single variation for create or update.
	 *
	 * @param  WP_REST_Request $request Request object.
	 * @param  bool            $creating If is creating a new object.
	 * @return WP_Error|WC_Data
	 */
	protected function prepare_object_for_database( $request, $creating = false ) {
		if ( isset( $request['id'] ) ) {
			$variation = wc_get_product( absint( $request['id'] ) );
		} else {
			$variation = new WC_Product_Variation();
		}

		// Update parent ID just once.
		if ( 0 === $variation->get_parent_id() ) {
			$variation->set_parent_id( absint( $request['product_id'] ) );
		}

		// Status.
		if ( isset( $request['visible'] ) ) {
			$variation->set_status( false === $request['visible'] ? 'private' : 'publish' );
		}

		// SKU.
		if ( isset( $request['sku'] ) ) {
			$variation->set_sku( wc_clean( $request['sku'] ) );
		}

		// Thumbnail.
		if ( isset( $request['image'] ) ) {
			if ( is_array( $request['image'] ) && ! empty( $request['image'] ) ) {
				$image = $request['image'];
				if ( is_array( $image ) ) {
					$image['position'] = 0;
				}

				$variation = $this->set_product_images( $variation, array( $image ) );
			} else {
				$variation->set_image_id( '' );
			}
		}

		// Virtual variation.
		if ( isset( $request['virtual'] ) ) {
			$variation->set_virtual( $request['virtual'] );
		}

		// Downloadable variation.
		if ( isset( $request['downloadable'] ) ) {
			$variation->set_downloadable( $request['downloadable'] );
		}

		// Downloads.
		if ( $variation->get_downloadable() ) {
			// Downloadable files.
			if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) {
				$variation = $this->save_downloadable_files( $variation, $request['downloads'] );
			}

			// Download limit.
			if ( isset( $request['download_limit'] ) ) {
				$variation->set_download_limit( $request['download_limit'] );
			}

			// Download expiry.
			if ( isset( $request['download_expiry'] ) ) {
				$variation->set_download_expiry( $request['download_expiry'] );
			}
		}

		// Shipping data.
		$variation = $this->save_product_shipping_data( $variation, $request );

		// Stock handling.
		if ( isset( $request['manage_stock'] ) ) {
			if ( 'parent' === $request['manage_stock'] ) {
				$variation->set_manage_stock( false ); // This just indicates the variation does not manage stock, but the parent does.
			} else {
				$variation->set_manage_stock( wc_string_to_bool( $request['manage_stock'] ) );
			}
		}

		if ( isset( $request['in_stock'] ) ) {
			$variation->set_stock_status( true === $request['in_stock'] ? 'instock' : 'outofstock' );
		}

		if ( isset( $request['backorders'] ) ) {
			$variation->set_backorders( $request['backorders'] );
		}

		if ( $variation->get_manage_stock() ) {
			if ( isset( $request['stock_quantity'] ) ) {
				$variation->set_stock_quantity( $request['stock_quantity'] );
			} elseif ( isset( $request['inventory_delta'] ) ) {
				$stock_quantity  = wc_stock_amount( $variation->get_stock_quantity() );
				$stock_quantity += wc_stock_amount( $request['inventory_delta'] );
				$variation->set_stock_quantity( $stock_quantity );
			}
		} else {
			$variation->set_backorders( 'no' );
			$variation->set_stock_quantity( '' );
		}

		// Regular Price.
		if ( isset( $request['regular_price'] ) ) {
			$variation->set_regular_price( $request['regular_price'] );
		}

		// Sale Price.
		if ( isset( $request['sale_price'] ) ) {
			$variation->set_sale_price( $request['sale_price'] );
		}

		if ( isset( $request['date_on_sale_from'] ) ) {
			$variation->set_date_on_sale_from( $request['date_on_sale_from'] );
		}

		if ( isset( $request['date_on_sale_from_gmt'] ) ) {
			$variation->set_date_on_sale_from( $request['date_on_sale_from_gmt'] ? strtotime( $request['date_on_sale_from_gmt'] ) : null );
		}

		if ( isset( $request['date_on_sale_to'] ) ) {
			$variation->set_date_on_sale_to( $request['date_on_sale_to'] );
		}

		if ( isset( $request['date_on_sale_to_gmt'] ) ) {
			$variation->set_date_on_sale_to( $request['date_on_sale_to_gmt'] ? strtotime( $request['date_on_sale_to_gmt'] ) : null );
		}

		// Tax class.
		if ( isset( $request['tax_class'] ) ) {
			$variation->set_tax_class( $request['tax_class'] );
		}

		// Description.
		if ( isset( $request['description'] ) ) {
			$variation->set_description( wp_kses_post( $request['description'] ) );
		}

		// Update taxonomies.
		if ( isset( $request['attributes'] ) ) {
			$attributes        = array();
			$parent            = wc_get_product( $variation->get_parent_id() );
			$parent_attributes = $parent->get_attributes();

			foreach ( $request['attributes'] as $attribute ) {
				$attribute_id   = 0;
				$attribute_name = '';

				// Check ID for global attributes or name for product attributes.
				if ( ! empty( $attribute['id'] ) ) {
					$attribute_id       = absint( $attribute['id'] );
					$raw_attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
				} elseif ( ! empty( $attribute['name'] ) ) {
					$raw_attribute_name = sanitize_title( $attribute['name'] );
				}

				if ( ! $attribute_id && ! $raw_attribute_name ) {
					continue;
				}

				$attribute_name = sanitize_title( $raw_attribute_name );

				if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) {
					continue;
				}

				$attribute_key   = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() );
				$attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : '';

				if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) {
					// If dealing with a taxonomy, we need to get the slug from the name posted to the API.
					$term = get_term_by( 'name', $attribute_value, $raw_attribute_name ); // @codingStandardsIgnoreLine

					if ( $term && ! is_wp_error( $term ) ) {
						$attribute_value = $term->slug;
					} else {
						$attribute_value = sanitize_title( $attribute_value );
					}
				}

				$attributes[ $attribute_key ] = $attribute_value;
			}

			$variation->set_attributes( $attributes );
		}

		// Menu order.
		if ( $request['menu_order'] ) {
			$variation->set_menu_order( $request['menu_order'] );
		}

		// Meta data.
		if ( is_array( $request['meta_data'] ) ) {
			foreach ( $request['meta_data'] as $meta ) {
				$variation->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
			}
		}

		/**
		 * Filters an object before it is inserted via the REST API.
		 *
		 * The dynamic portion of the hook name, `$this->post_type`,
		 * refers to the object type slug.
		 *
		 * @param WC_Data         $variation Object object.
		 * @param WP_REST_Request $request   Request object.
		 * @param bool            $creating  If is creating a new object.
		 */
		return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $variation, $request, $creating );
	}

	/**
	 * Clear caches here so in sync with any new variations.
	 *
	 * @param WC_Data $object Object data.
	 */
	public function clear_transients( $object ) {
		wc_delete_product_transients( $object->get_parent_id() );
		wp_cache_delete( 'product-' . $object->get_parent_id(), 'products' );
	}

	/**
	 * Delete a variation.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 *
	 * @return bool|WP_Error|WP_REST_Response
	 */
	public function delete_item( $request ) {
		$force  = (bool) $request['force'];
		$object = $this->get_object( (int) $request['id'] );
		$result = false;

		if ( ! $object || 0 === $object->get_id() ) {
			return new WP_Error(
				"woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array(
					'status' => 404,
				)
			);
		}

		$supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( array( $object, 'get_status' ) );

		/**
		 * Filter whether an object is trashable.
		 *
		 * Return false to disable trash support for the object.
		 *
		 * @param boolean $supports_trash Whether the object type support trashing.
		 * @param WC_Data $object         The object being considered for trashing support.
		 */
		$supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_object_trashable", $supports_trash, $object );

		if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) {
			return new WP_Error(
				/* translators: %s: post type */
				"woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array(
					'status' => rest_authorization_required_code(),
				)
			);
		}

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_object_for_response( $object, $request );

		// If we're forcing, then delete permanently.
		if ( $force ) {
			$object->delete( true );
			$result = 0 === $object->get_id();
		} else {
			// If we don't support trashing for this type, error out.
			if ( ! $supports_trash ) {
				return new WP_Error(
					/* translators: %s: post type */
					'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array(
						'status' => 501,
					)
				);
			}

			// Otherwise, only trash if we haven't already.
			if ( is_callable( array( $object, 'get_status' ) ) ) {
				if ( 'trash' === $object->get_status() ) {
					return new WP_Error(
						/* translators: %s: post type */
						'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array(
							'status' => 410,
						)
					);
				}

				$object->delete();
				$result = 'trash' === $object->get_status();
			}
		}

		if ( ! $result ) {
			return new WP_Error(
				/* translators: %s: post type */
				'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array(
					'status' => 500,
				)
			);
		}

		// Delete parent product transients.
		if ( 0 !== $object->get_parent_id() ) {
			wc_delete_product_transients( $object->get_parent_id() );
		}

		/**
		 * Fires after a single object is deleted or trashed via the REST API.
		 *
		 * @param WC_Data          $object   The deleted or trashed object.
		 * @param WP_REST_Response $response The response data.
		 * @param WP_REST_Request  $request  The request sent to the API.
		 */
		do_action( "woocommerce_rest_delete_{$this->post_type}_object", $object, $response, $request );

		return $response;
	}

	/**
	 * Bulk create, update and delete items.
	 *
	 * @since  3.0.0
	 * @param WP_REST_Request $request Full details about the request.
	 * @return array Of WP_Error or WP_REST_Response.
	 */
	public function batch_items( $request ) {
		$items       = array_filter( $request->get_params() );
		$params      = $request->get_url_params();
		$query       = $request->get_query_params();
		$product_id  = $params['product_id'];
		$body_params = array();

		foreach ( array( 'update', 'create', 'delete' ) as $batch_type ) {
			if ( ! empty( $items[ $batch_type ] ) ) {
				$injected_items = array();
				foreach ( $items[ $batch_type ] as $item ) {
					$injected_items[] = is_array( $item ) ? array_merge(
						array(
							'product_id' => $product_id,
						), $item
					) : $item;
				}
				$body_params[ $batch_type ] = $injected_items;
			}
		}

		$request = new WP_REST_Request( $request->get_method() );
		$request->set_body_params( $body_params );
		$request->set_query_params( $query );

		return parent::batch_items( $request );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param WC_Data         $object  Object data.
	 * @param WP_REST_Request $request Request object.
	 * @return array                   Links for the given post.
	 */
	protected function prepare_links( $object, $request ) {
		$product_id = (int) $request['product_id'];
		$base       = str_replace( '(?P<product_id>[\d]+)', $product_id, $this->rest_base );
		$links      = array(
			'self'       => array(
				'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $object->get_id() ) ),
			),
			'collection' => array(
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ),
			),
			'up'         => array(
				'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product_id ) ),
			),
		);
		return $links;
	}

	/**
	 * Get the Variation's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$weight_unit    = get_option( 'woocommerce_weight_unit' );
		$dimension_unit = get_option( 'woocommerce_dimension_unit' );
		$schema         = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => $this->post_type,
			'type'       => 'object',
			'properties' => array(
				'id'                    => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_created'          => array(
					'description' => __( "The date the variation was created, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_modified'         => array(
					'description' => __( "The date the variation was last modified, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'description'           => array(
					'description' => __( 'Variation description.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'permalink'             => array(
					'description' => __( 'Variation URL.', 'woocommerce' ),
					'type'        => 'string',
					'format'      => 'uri',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'sku'                   => array(
					'description' => __( 'Unique identifier.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'price'                 => array(
					'description' => __( 'Current variation price.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'regular_price'         => array(
					'description' => __( 'Variation regular price.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'sale_price'            => array(
					'description' => __( 'Variation sale price.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'date_on_sale_from'     => array(
					'description' => __( "Start date of sale price, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'date_on_sale_from_gmt' => array(
					'description' => __( 'Start date of sale price, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'date_on_sale_to'       => array(
					'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'date_on_sale_to_gmt'   => array(
					'description' => __( 'End date of sale price, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'on_sale'               => array(
					'description' => __( 'Shows if the variation is on sale.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'visible'               => array(
					'description' => __( "Define if the variation is visible on the product's page.", 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => true,
					'context'     => array( 'view', 'edit' ),
				),
				'purchasable'           => array(
					'description' => __( 'Shows if the variation can be bought.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'virtual'               => array(
					'description' => __( 'If the variation is virtual.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'downloadable'          => array(
					'description' => __( 'If the variation is downloadable.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'downloads'             => array(
					'description' => __( 'List of downloadable files.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'   => array(
								'description' => __( 'File ID.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'name' => array(
								'description' => __( 'File name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'file' => array(
								'description' => __( 'File URL.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
						),
					),
				),
				'download_limit'        => array(
					'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ),
					'type'        => 'integer',
					'default'     => -1,
					'context'     => array( 'view', 'edit' ),
				),
				'download_expiry'       => array(
					'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ),
					'type'        => 'integer',
					'default'     => -1,
					'context'     => array( 'view', 'edit' ),
				),
				'tax_status'            => array(
					'description' => __( 'Tax status.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'taxable',
					'enum'        => array( 'taxable', 'shipping', 'none' ),
					'context'     => array( 'view', 'edit' ),
				),
				'tax_class'             => array(
					'description' => __( 'Tax class.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'manage_stock'          => array(
					'description' => __( 'Stock management at variation level.', 'woocommerce' ),
					'type'        => 'mixed',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'stock_quantity'        => array(
					'description' => __( 'Stock quantity.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'in_stock'              => array(
					'description' => __( 'Controls whether or not the variation is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => true,
					'context'     => array( 'view', 'edit' ),
				),
				'backorders'            => array(
					'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'no',
					'enum'        => array( 'no', 'notify', 'yes' ),
					'context'     => array( 'view', 'edit' ),
				),
				'backorders_allowed'    => array(
					'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'backordered'           => array(
					'description' => __( 'Shows if the variation is on backordered.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'weight'                => array(
					/* translators: %s: weight unit */
					'description' => sprintf( __( 'Variation weight (%s).', 'woocommerce' ), $weight_unit ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'dimensions'            => array(
					'description' => __( 'Variation dimensions.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'properties'  => array(
						'length' => array(
							/* translators: %s: dimension unit */
							'description' => sprintf( __( 'Variation length (%s).', 'woocommerce' ), $dimension_unit ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'width'  => array(
							/* translators: %s: dimension unit */
							'description' => sprintf( __( 'Variation width (%s).', 'woocommerce' ), $dimension_unit ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'height' => array(
							/* translators: %s: dimension unit */
							'description' => sprintf( __( 'Variation height (%s).', 'woocommerce' ), $dimension_unit ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
					),
				),
				'shipping_class'        => array(
					'description' => __( 'Shipping class slug.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'shipping_class_id'     => array(
					'description' => __( 'Shipping class ID.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'image'                 => array(
					'description' => __( 'Variation image data.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'properties'  => array(
						'id'                => array(
							'description' => __( 'Image ID.', 'woocommerce' ),
							'type'        => 'integer',
							'context'     => array( 'view', 'edit' ),
						),
						'date_created'      => array(
							'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ),
							'type'        => 'date-time',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'date_created_gmt'  => array(
							'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ),
							'type'        => 'date-time',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'date_modified'     => array(
							'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ),
							'type'        => 'date-time',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'date_modified_gmt' => array(
							'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ),
							'type'        => 'date-time',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'src'               => array(
							'description' => __( 'Image URL.', 'woocommerce' ),
							'type'        => 'string',
							'format'      => 'uri',
							'context'     => array( 'view', 'edit' ),
						),
						'name'              => array(
							'description' => __( 'Image name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'alt'               => array(
							'description' => __( 'Image alternative text.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'position'          => array(
							'description' => __( 'Image position. 0 means that the image is featured.', 'woocommerce' ),
							'type'        => 'integer',
							'context'     => array( 'view', 'edit' ),
						),
					),
				),
				'attributes'            => array(
					'description' => __( 'List of attributes.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'     => array(
								'description' => __( 'Attribute ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'name'   => array(
								'description' => __( 'Attribute name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'option' => array(
								'description' => __( 'Selected attribute term name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
						),
					),
				),
				'menu_order'            => array(
					'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'meta_data'             => array(
					'description' => __( 'Meta data.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'    => array(
								'description' => __( 'Meta ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'key'   => array(
								'description' => __( 'Meta key.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'value' => array(
								'description' => __( 'Meta value.', 'woocommerce' ),
								'type'        => 'mixed',
								'context'     => array( 'view', 'edit' ),
							),
						),
					),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php000064400000127520151336044760023076 0ustar00<?php
/**
 * REST API WC System Status controller
 *
 * Handles requests to the /system_status endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * System status controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Controller
 */
class WC_REST_System_Status_V2_Controller extends WC_REST_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'system_status';

	/**
	 * Register the route for /system_status
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Check whether a given request has permission to view system status.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_items_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'system_status', 'read' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}
		return true;
	}

	/**
	 * Get a system status info, by section.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_items( $request ) {
		$fields   = $this->get_fields_for_response( $request );
		$mappings = $this->get_item_mappings_per_fields( $fields );
		$response = $this->prepare_item_for_response( $mappings, $request );

		return rest_ensure_response( $response );
	}

	/**
	 * Get the system status schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'system_status',
			'type'       => 'object',
			'properties' => array(
				'environment'        => array(
					'description' => __( 'Environment.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view' ),
					'readonly'    => true,
					'properties'  => array(
						'home_url'                  => array(
							'description' => __( 'Home URL.', 'woocommerce' ),
							'type'        => 'string',
							'format'      => 'uri',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'site_url'                  => array(
							'description' => __( 'Site URL.', 'woocommerce' ),
							'type'        => 'string',
							'format'      => 'uri',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'version'                   => array(
							'description' => __( 'WooCommerce version.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'log_directory'             => array(
							'description' => __( 'Log directory.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'log_directory_writable'    => array(
							'description' => __( 'Is log directory writable?', 'woocommerce' ),
							'type'        => 'boolean',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'wp_version'                => array(
							'description' => __( 'WordPress version.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'wp_multisite'              => array(
							'description' => __( 'Is WordPress multisite?', 'woocommerce' ),
							'type'        => 'boolean',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'wp_memory_limit'           => array(
							'description' => __( 'WordPress memory limit.', 'woocommerce' ),
							'type'        => 'integer',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'wp_debug_mode'             => array(
							'description' => __( 'Is WordPress debug mode active?', 'woocommerce' ),
							'type'        => 'boolean',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'wp_cron'                   => array(
							'description' => __( 'Are WordPress cron jobs enabled?', 'woocommerce' ),
							'type'        => 'boolean',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'language'                  => array(
							'description' => __( 'WordPress language.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'server_info'               => array(
							'description' => __( 'Server info.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'php_version'               => array(
							'description' => __( 'PHP version.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'php_post_max_size'         => array(
							'description' => __( 'PHP post max size.', 'woocommerce' ),
							'type'        => 'integer',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'php_max_execution_time'    => array(
							'description' => __( 'PHP max execution time.', 'woocommerce' ),
							'type'        => 'integer',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'php_max_input_vars'        => array(
							'description' => __( 'PHP max input vars.', 'woocommerce' ),
							'type'        => 'integer',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'curl_version'              => array(
							'description' => __( 'cURL version.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'suhosin_installed'         => array(
							'description' => __( 'Is SUHOSIN installed?', 'woocommerce' ),
							'type'        => 'boolean',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'max_upload_size'           => array(
							'description' => __( 'Max upload size.', 'woocommerce' ),
							'type'        => 'integer',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'mysql_version'             => array(
							'description' => __( 'MySQL version.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'mysql_version_string'      => array(
							'description' => __( 'MySQL version string.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'default_timezone'          => array(
							'description' => __( 'Default timezone.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'fsockopen_or_curl_enabled' => array(
							'description' => __( 'Is fsockopen/cURL enabled?', 'woocommerce' ),
							'type'        => 'boolean',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'soapclient_enabled'        => array(
							'description' => __( 'Is SoapClient class enabled?', 'woocommerce' ),
							'type'        => 'boolean',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'domdocument_enabled'       => array(
							'description' => __( 'Is DomDocument class enabled?', 'woocommerce' ),
							'type'        => 'boolean',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'gzip_enabled'              => array(
							'description' => __( 'Is GZip enabled?', 'woocommerce' ),
							'type'        => 'boolean',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'mbstring_enabled'          => array(
							'description' => __( 'Is mbstring enabled?', 'woocommerce' ),
							'type'        => 'boolean',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'remote_post_successful'    => array(
							'description' => __( 'Remote POST successful?', 'woocommerce' ),
							'type'        => 'boolean',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'remote_post_response'      => array(
							'description' => __( 'Remote POST response.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'remote_get_successful'     => array(
							'description' => __( 'Remote GET successful?', 'woocommerce' ),
							'type'        => 'boolean',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'remote_get_response'       => array(
							'description' => __( 'Remote GET response.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
					),
				),
				'database'           => array(
					'description' => __( 'Database.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view' ),
					'readonly'    => true,
					'properties'  => array(
						'wc_database_version'    => array(
							'description' => __( 'WC database version.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'database_prefix'        => array(
							'description' => __( 'Database prefix.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'maxmind_geoip_database' => array(
							'description' => __( 'MaxMind GeoIP database.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'database_tables'        => array(
							'description' => __( 'Database tables.', 'woocommerce' ),
							'type'        => 'array',
							'context'     => array( 'view' ),
							'readonly'    => true,
							'items'       => array(
								'type' => 'string',
							),
						),
					),
				),
				'active_plugins'     => array(
					'description' => __( 'Active plugins.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view' ),
					'readonly'    => true,
					'items'       => array(
						'type' => 'string',
					),
				),
				'inactive_plugins'   => array(
					'description' => __( 'Inactive plugins.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view' ),
					'readonly'    => true,
					'items'       => array(
						'type' => 'string',
					),
				),
				'dropins_mu_plugins' => array(
					'description' => __( 'Dropins & MU plugins.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view' ),
					'readonly'    => true,
					'items'       => array(
						'type' => 'string',
					),
				),
				'theme'              => array(
					'description' => __( 'Theme.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view' ),
					'readonly'    => true,
					'properties'  => array(
						'name'                    => array(
							'description' => __( 'Theme name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'version'                 => array(
							'description' => __( 'Theme version.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'version_latest'          => array(
							'description' => __( 'Latest version of theme.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'author_url'              => array(
							'description' => __( 'Theme author URL.', 'woocommerce' ),
							'type'        => 'string',
							'format'      => 'uri',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'is_child_theme'          => array(
							'description' => __( 'Is this theme a child theme?', 'woocommerce' ),
							'type'        => 'boolean',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'has_woocommerce_support' => array(
							'description' => __( 'Does the theme declare WooCommerce support?', 'woocommerce' ),
							'type'        => 'boolean',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'has_woocommerce_file'    => array(
							'description' => __( 'Does the theme have a woocommerce.php file?', 'woocommerce' ),
							'type'        => 'boolean',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'has_outdated_templates'  => array(
							'description' => __( 'Does this theme have outdated templates?', 'woocommerce' ),
							'type'        => 'boolean',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'overrides'               => array(
							'description' => __( 'Template overrides.', 'woocommerce' ),
							'type'        => 'array',
							'context'     => array( 'view' ),
							'readonly'    => true,
							'items'       => array(
								'type' => 'string',
							),
						),
						'parent_name'             => array(
							'description' => __( 'Parent theme name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'parent_version'          => array(
							'description' => __( 'Parent theme version.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'parent_author_url'       => array(
							'description' => __( 'Parent theme author URL.', 'woocommerce' ),
							'type'        => 'string',
							'format'      => 'uri',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
					),
				),
				'settings'           => array(
					'description' => __( 'Settings.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view' ),
					'readonly'    => true,
					'properties'  => array(
						'api_enabled'              => array(
							'description' => __( 'REST API enabled?', 'woocommerce' ),
							'type'        => 'boolean',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'force_ssl'                => array(
							'description' => __( 'SSL forced?', 'woocommerce' ),
							'type'        => 'boolean',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'currency'                 => array(
							'description' => __( 'Currency.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'currency_symbol'          => array(
							'description' => __( 'Currency symbol.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'currency_position'        => array(
							'description' => __( 'Currency position.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'thousand_separator'       => array(
							'description' => __( 'Thousand separator.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'decimal_separator'        => array(
							'description' => __( 'Decimal separator.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'number_of_decimals'       => array(
							'description' => __( 'Number of decimals.', 'woocommerce' ),
							'type'        => 'integer',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'geolocation_enabled'      => array(
							'description' => __( 'Geolocation enabled?', 'woocommerce' ),
							'type'        => 'boolean',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'taxonomies'               => array(
							'description' => __( 'Taxonomy terms for product/order statuses.', 'woocommerce' ),
							'type'        => 'array',
							'context'     => array( 'view' ),
							'readonly'    => true,
							'items'       => array(
								'type' => 'string',
							),
						),
						'product_visibility_terms' => array(
							'description' => __( 'Terms in the product visibility taxonomy.', 'woocommerce' ),
							'type'        => 'array',
							'context'     => array( 'view' ),
							'readonly'    => true,
							'items'       => array(
								'type' => 'string',
							),
						),
					),
				),
				'security'           => array(
					'description' => __( 'Security.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view' ),
					'readonly'    => true,
					'properties'  => array(
						'secure_connection' => array(
							'description' => __( 'Is the connection to your store secure?', 'woocommerce' ),
							'type'        => 'boolean',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'hide_errors'       => array(
							'description' => __( 'Hide errors from visitors?', 'woocommerce' ),
							'type'        => 'boolean',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
					),
				),
				'pages'              => array(
					'description' => __( 'WooCommerce pages.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view' ),
					'readonly'    => true,
					'items'       => array(
						'type' => 'string',
					),
				),
				'post_type_counts'   => array(
					'description' => __( 'Total post count.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view' ),
					'readonly'    => true,
					'items'       => array(
						'type' => 'string',
					),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Return an array of sections and the data associated with each.
	 *
	 * @deprecated 3.9.0
	 * @return array
	 */
	public function get_item_mappings() {
		return array(
			'environment'        => $this->get_environment_info(),
			'database'           => $this->get_database_info(),
			'active_plugins'     => $this->get_active_plugins(),
			'inactive_plugins'   => $this->get_inactive_plugins(),
			'dropins_mu_plugins' => $this->get_dropins_mu_plugins(),
			'theme'              => $this->get_theme_info(),
			'settings'           => $this->get_settings(),
			'security'           => $this->get_security_info(),
			'pages'              => $this->get_pages(),
			'post_type_counts'   => $this->get_post_type_counts(),
		);
	}

	/**
	 * Return an array of sections and the data associated with each.
	 *
	 * @since 3.9.0
	 * @param array $fields List of fields to be included on the response.
	 * @return array
	 */
	public function get_item_mappings_per_fields( $fields ) {
		return array(
			'environment'        => $this->get_environment_info_per_fields( $fields ),
			'database'           => $this->get_database_info(),
			'active_plugins'     => $this->get_active_plugins(),
			'inactive_plugins'   => $this->get_inactive_plugins(),
			'dropins_mu_plugins' => $this->get_dropins_mu_plugins(),
			'theme'              => $this->get_theme_info(),
			'settings'           => $this->get_settings(),
			'security'           => $this->get_security_info(),
			'pages'              => $this->get_pages(),
			'post_type_counts'   => $this->get_post_type_counts(),
		);
	}

	/**
	 * Get array of environment information. Includes thing like software
	 * versions, and various server settings.
	 *
	 * @deprecated 3.9.0
	 * @return array
	 */
	public function get_environment_info() {
		return $this->get_environment_info_per_fields( array( 'environment' ) );
	}

	/**
	 * Check if field item exists.
	 *
	 * @since 3.9.0
	 * @param string $section Fields section.
	 * @param array  $items List of items to check for.
	 * @param array  $fields List of fields to be included on the response.
	 * @return bool
	 */
	private function check_if_field_item_exists( $section, $items, $fields ) {
		if ( ! in_array( $section, $fields, true ) ) {
			return false;
		}

		$exclude = array();
		foreach ( $fields as $field ) {
			$values = explode( '.', $field );

			if ( $section !== $values[0] || empty( $values[1] ) ) {
				continue;
			}

			$exclude[] = $values[1];
		}

		return 0 <= count( array_intersect( $items, $exclude ) );
	}

	/**
	 * Get array of environment information. Includes thing like software
	 * versions, and various server settings.
	 *
	 * @param array $fields List of fields to be included on the response.
	 * @return array
	 */
	public function get_environment_info_per_fields( $fields ) {
		global $wpdb;

		$enable_remote_post = $this->check_if_field_item_exists( 'environment', array( 'remote_post_successful', 'remote_post_response' ), $fields );
		$enable_remote_get  = $this->check_if_field_item_exists( 'environment', array( 'remote_get_successful', 'remote_get_response' ), $fields );

		// Figure out cURL version, if installed.
		$curl_version = '';
		if ( function_exists( 'curl_version' ) ) {
			$curl_version = curl_version();
			$curl_version = $curl_version['version'] . ', ' . $curl_version['ssl_version'];
		} elseif ( extension_loaded( 'curl' ) ) {
			$curl_version = __( 'cURL installed but unable to retrieve version.', 'woocommerce' );
		}

		// WP memory limit.
		$wp_memory_limit = wc_let_to_num( WP_MEMORY_LIMIT );
		if ( function_exists( 'memory_get_usage' ) ) {
			$wp_memory_limit = max( $wp_memory_limit, wc_let_to_num( @ini_get( 'memory_limit' ) ) ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
		}

		// Test POST requests.
		$post_response_successful = null;
		$post_response_code       = null;
		if ( $enable_remote_post ) {
			$post_response_code = get_transient( 'woocommerce_test_remote_post' );

			if ( false === $post_response_code || is_wp_error( $post_response_code ) ) {
				$response = wp_safe_remote_post(
					'https://www.paypal.com/cgi-bin/webscr',
					array(
						'timeout'     => 10,
						'user-agent'  => 'WooCommerce/' . WC()->version,
						'httpversion' => '1.1',
						'body'        => array(
							'cmd' => '_notify-validate',
						),
					)
				);
				if ( ! is_wp_error( $response ) ) {
					$post_response_code = $response['response']['code'];
				}
				set_transient( 'woocommerce_test_remote_post', $post_response_code, HOUR_IN_SECONDS );
			}

			$post_response_successful = ! is_wp_error( $post_response_code ) && $post_response_code >= 200 && $post_response_code < 300;
		}

		// Test GET requests.
		$get_response_successful = null;
		$get_response_code       = null;
		if ( $enable_remote_get ) {
			$get_response_code = get_transient( 'woocommerce_test_remote_get' );

			if ( false === $get_response_code || is_wp_error( $get_response_code ) ) {
				$response = wp_safe_remote_get( 'https://woocommerce.com/wc-api/product-key-api?request=ping&network=' . ( is_multisite() ? '1' : '0' ) );
				if ( ! is_wp_error( $response ) ) {
					$get_response_code = $response['response']['code'];
				}
				set_transient( 'woocommerce_test_remote_get', $get_response_code, HOUR_IN_SECONDS );
			}

			$get_response_successful = ! is_wp_error( $get_response_code ) && $get_response_code >= 200 && $get_response_code < 300;
		}

		$database_version = wc_get_server_database_version();

		// Return all environment info. Described by JSON Schema.
		return array(
			'home_url'                  => get_option( 'home' ),
			'site_url'                  => get_option( 'siteurl' ),
			'version'                   => WC()->version,
			'log_directory'             => WC_LOG_DIR,
			'log_directory_writable'    => (bool) @fopen( WC_LOG_DIR . 'test-log.log', 'a' ), // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_fopen
			'wp_version'                => get_bloginfo( 'version' ),
			'wp_multisite'              => is_multisite(),
			'wp_memory_limit'           => $wp_memory_limit,
			'wp_debug_mode'             => ( defined( 'WP_DEBUG' ) && WP_DEBUG ),
			'wp_cron'                   => ! ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ),
			'language'                  => get_locale(),
			'external_object_cache'     => wp_using_ext_object_cache(),
			'server_info'               => isset( $_SERVER['SERVER_SOFTWARE'] ) ? wc_clean( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : '',
			'php_version'               => phpversion(),
			'php_post_max_size'         => wc_let_to_num( ini_get( 'post_max_size' ) ),
			'php_max_execution_time'    => (int) ini_get( 'max_execution_time' ),
			'php_max_input_vars'        => (int) ini_get( 'max_input_vars' ),
			'curl_version'              => $curl_version,
			'suhosin_installed'         => extension_loaded( 'suhosin' ),
			'max_upload_size'           => wp_max_upload_size(),
			'mysql_version'             => $database_version['number'],
			'mysql_version_string'      => $database_version['string'],
			'default_timezone'          => date_default_timezone_get(),
			'fsockopen_or_curl_enabled' => ( function_exists( 'fsockopen' ) || function_exists( 'curl_init' ) ),
			'soapclient_enabled'        => class_exists( 'SoapClient' ),
			'domdocument_enabled'       => class_exists( 'DOMDocument' ),
			'gzip_enabled'              => is_callable( 'gzopen' ),
			'mbstring_enabled'          => extension_loaded( 'mbstring' ),
			'remote_post_successful'    => $post_response_successful,
			'remote_post_response'      => is_wp_error( $post_response_code ) ? $post_response_code->get_error_message() : $post_response_code,
			'remote_get_successful'     => $get_response_successful,
			'remote_get_response'       => is_wp_error( $get_response_code ) ? $get_response_code->get_error_message() : $get_response_code,
		);
	}

	/**
	 * Add prefix to table.
	 *
	 * @param string $table Table name.
	 * @return stromg
	 */
	protected function add_db_table_prefix( $table ) {
		global $wpdb;
		return $wpdb->prefix . $table;
	}

	/**
	 * Get array of database information. Version, prefix, and table existence.
	 *
	 * @return array
	 */
	public function get_database_info() {
		global $wpdb;

		$tables        = array();
		$database_size = array();

		// It is not possible to get the database name from some classes that replace wpdb (e.g., HyperDB)
		// and that is why this if condition is needed.
		if ( defined( 'DB_NAME' ) ) {
			$database_table_information = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT
					    table_name AS 'name',
						engine AS 'engine',
					    round( ( data_length / 1024 / 1024 ), 2 ) 'data',
					    round( ( index_length / 1024 / 1024 ), 2 ) 'index'
					FROM information_schema.TABLES
					WHERE table_schema = %s
					ORDER BY name ASC;",
					DB_NAME
				)
			);

			// WC Core tables to check existence of.
			$core_tables = apply_filters(
				'woocommerce_database_tables',
				array(
					'woocommerce_sessions',
					'woocommerce_api_keys',
					'woocommerce_attribute_taxonomies',
					'woocommerce_downloadable_product_permissions',
					'woocommerce_order_items',
					'woocommerce_order_itemmeta',
					'woocommerce_tax_rates',
					'woocommerce_tax_rate_locations',
					'woocommerce_shipping_zones',
					'woocommerce_shipping_zone_locations',
					'woocommerce_shipping_zone_methods',
					'woocommerce_payment_tokens',
					'woocommerce_payment_tokenmeta',
					'woocommerce_log',
				)
			);

			/**
			 * Adding the prefix to the tables array, for backwards compatibility.
			 *
			 * If we changed the tables above to include the prefix, then any filters against that table could break.
			 */
			$core_tables = array_map( array( $this, 'add_db_table_prefix' ), $core_tables );

			/**
			 * Organize WooCommerce and non-WooCommerce tables separately for display purposes later.
			 *
			 * To ensure we include all WC tables, even if they do not exist, pre-populate the WC array with all the tables.
			 */
			$tables = array(
				'woocommerce' => array_fill_keys( $core_tables, false ),
				'other'       => array(),
			);

			$database_size = array(
				'data'  => 0,
				'index' => 0,
			);

			$site_tables_prefix = $wpdb->get_blog_prefix( get_current_blog_id() );
			$global_tables      = $wpdb->tables( 'global', true );
			foreach ( $database_table_information as $table ) {
				// Only include tables matching the prefix of the current site, this is to prevent displaying all tables on a MS install not relating to the current.
				if ( is_multisite() && 0 !== strpos( $table->name, $site_tables_prefix ) && ! in_array( $table->name, $global_tables, true ) ) {
					continue;
				}
				$table_type = in_array( $table->name, $core_tables, true ) ? 'woocommerce' : 'other';

				$tables[ $table_type ][ $table->name ] = array(
					'data'   => $table->data,
					'index'  => $table->index,
					'engine' => $table->engine,
				);

				$database_size['data']  += $table->data;
				$database_size['index'] += $table->index;
			}
		}

		// Return all database info. Described by JSON Schema.
		return array(
			'wc_database_version'    => get_option( 'woocommerce_db_version' ),
			'database_prefix'        => $wpdb->prefix,
			'maxmind_geoip_database' => '',
			'database_tables'        => $tables,
			'database_size'          => $database_size,
		);
	}

	/**
	 * Get array of counts of objects. Orders, products, etc.
	 *
	 * @return array
	 */
	public function get_post_type_counts() {
		global $wpdb;

		$post_type_counts = $wpdb->get_results( "SELECT post_type AS 'type', count(1) AS 'count' FROM {$wpdb->posts} GROUP BY post_type;" );

		return is_array( $post_type_counts ) ? $post_type_counts : array();
	}

	/**
	 * Get a list of plugins active on the site.
	 *
	 * @return array
	 */
	public function get_active_plugins() {
		require_once ABSPATH . 'wp-admin/includes/plugin.php';

		if ( ! function_exists( 'get_plugin_data' ) ) {
			return array();
		}

		$active_plugins = (array) get_option( 'active_plugins', array() );
		if ( is_multisite() ) {
			$network_activated_plugins = array_keys( get_site_option( 'active_sitewide_plugins', array() ) );
			$active_plugins            = array_merge( $active_plugins, $network_activated_plugins );
		}

		$active_plugins_data = array();

		foreach ( $active_plugins as $plugin ) {
			$data                  = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin );
			$active_plugins_data[] = $this->format_plugin_data( $plugin, $data );
		}

		return $active_plugins_data;
	}

	/**
	 * Get a list of inplugins active on the site.
	 *
	 * @return array
	 */
	public function get_inactive_plugins() {
		require_once ABSPATH . 'wp-admin/includes/plugin.php';

		if ( ! function_exists( 'get_plugins' ) ) {
			return array();
		}

		$plugins        = get_plugins();
		$active_plugins = (array) get_option( 'active_plugins', array() );

		if ( is_multisite() ) {
			$network_activated_plugins = array_keys( get_site_option( 'active_sitewide_plugins', array() ) );
			$active_plugins            = array_merge( $active_plugins, $network_activated_plugins );
		}

		$plugins_data = array();

		foreach ( $plugins as $plugin => $data ) {
			if ( in_array( $plugin, $active_plugins, true ) ) {
				continue;
			}
			$plugins_data[] = $this->format_plugin_data( $plugin, $data );
		}

		return $plugins_data;
	}

	/**
	 * Format plugin data, including data on updates, into a standard format.
	 *
	 * @since 3.6.0
	 * @param string $plugin Plugin directory/file.
	 * @param array  $data Plugin data from WP.
	 * @return array Formatted data.
	 */
	protected function format_plugin_data( $plugin, $data ) {
		require_once ABSPATH . 'wp-admin/includes/update.php';

		if ( ! function_exists( 'get_plugin_updates' ) ) {
			return array();
		}

		// Use WP API to lookup latest updates for plugins. WC_Helper injects updates for premium plugins.
		if ( empty( $this->available_updates ) ) {
			$this->available_updates = get_plugin_updates();
		}

		$version_latest = $data['Version'];

		// Find latest version.
		if ( isset( $this->available_updates[ $plugin ]->update->new_version ) ) {
			$version_latest = $this->available_updates[ $plugin ]->update->new_version;
		}

		return array(
			'plugin'            => $plugin,
			'name'              => $data['Name'],
			'version'           => $data['Version'],
			'version_latest'    => $version_latest,
			'url'               => $data['PluginURI'],
			'author_name'       => $data['AuthorName'],
			'author_url'        => esc_url_raw( $data['AuthorURI'] ),
			'network_activated' => $data['Network'],
		);
	}

	/**
	 * Get a list of Dropins and MU plugins.
	 *
	 * @since 3.6.0
	 * @return array
	 */
	public function get_dropins_mu_plugins() {
		$dropins = get_dropins();
		$plugins = array(
			'dropins'    => array(),
			'mu_plugins' => array(),
		);
		foreach ( $dropins as $key => $dropin ) {
			$plugins['dropins'][] = array(
				'plugin' => $key,
				'name'   => $dropin['Name'],
			);
		}

		$mu_plugins = get_mu_plugins();
		foreach ( $mu_plugins as $plugin => $mu_plugin ) {
			$plugins['mu_plugins'][] = array(
				'plugin'      => $plugin,
				'name'        => $mu_plugin['Name'],
				'version'     => $mu_plugin['Version'],
				'url'         => $mu_plugin['PluginURI'],
				'author_name' => $mu_plugin['AuthorName'],
				'author_url'  => esc_url_raw( $mu_plugin['AuthorURI'] ),
			);
		}
		return $plugins;
	}

	/**
	 * Get info on the current active theme, info on parent theme (if presnet)
	 * and a list of template overrides.
	 *
	 * @return array
	 */
	public function get_theme_info() {
		$active_theme = wp_get_theme();

		// Get parent theme info if this theme is a child theme, otherwise
		// pass empty info in the response.
		if ( is_child_theme() ) {
			$parent_theme      = wp_get_theme( $active_theme->template );
			$parent_theme_info = array(
				'parent_name'           => $parent_theme->name,
				'parent_version'        => $parent_theme->version,
				'parent_version_latest' => WC_Admin_Status::get_latest_theme_version( $parent_theme ),
				'parent_author_url'     => $parent_theme->{'Author URI'},
			);
		} else {
			$parent_theme_info = array(
				'parent_name'           => '',
				'parent_version'        => '',
				'parent_version_latest' => '',
				'parent_author_url'     => '',
			);
		}

		/**
		 * Scan the theme directory for all WC templates to see if our theme
		 * overrides any of them.
		 */
		$override_files     = array();
		$outdated_templates = false;
		$scan_files         = WC_Admin_Status::scan_template_files( WC()->plugin_path() . '/templates/' );

		// Include *-product_<cat|tag> templates for backwards compatibility.
		$scan_files[] = 'content-product_cat.php';
		$scan_files[] = 'taxonomy-product_cat.php';
		$scan_files[] = 'taxonomy-product_tag.php';

		foreach ( $scan_files as $file ) {
			$located = apply_filters( 'wc_get_template', $file, $file, array(), WC()->template_path(), WC()->plugin_path() . '/templates/' );

			if ( file_exists( $located ) ) {
				$theme_file = $located;
			} elseif ( file_exists( get_stylesheet_directory() . '/' . $file ) ) {
				$theme_file = get_stylesheet_directory() . '/' . $file;
			} elseif ( file_exists( get_stylesheet_directory() . '/' . WC()->template_path() . $file ) ) {
				$theme_file = get_stylesheet_directory() . '/' . WC()->template_path() . $file;
			} elseif ( file_exists( get_template_directory() . '/' . $file ) ) {
				$theme_file = get_template_directory() . '/' . $file;
			} elseif ( file_exists( get_template_directory() . '/' . WC()->template_path() . $file ) ) {
				$theme_file = get_template_directory() . '/' . WC()->template_path() . $file;
			} else {
				$theme_file = false;
			}

			if ( ! empty( $theme_file ) ) {
				$core_file = $file;

				// Update *-product_<cat|tag> template name before searching in core.
				if ( false !== strpos( $core_file, '-product_cat' ) || false !== strpos( $core_file, '-product_tag' ) ) {
					$core_file = str_replace( '_', '-', $core_file );
				}

				$core_version  = WC_Admin_Status::get_file_version( WC()->plugin_path() . '/templates/' . $core_file );
				$theme_version = WC_Admin_Status::get_file_version( $theme_file );
				if ( $core_version && ( empty( $theme_version ) || version_compare( $theme_version, $core_version, '<' ) ) ) {
					if ( ! $outdated_templates ) {
						$outdated_templates = true;
					}
				}
				$override_files[] = array(
					'file'         => str_replace( WP_CONTENT_DIR . '/themes/', '', $theme_file ),
					'version'      => $theme_version,
					'core_version' => $core_version,
				);
			}
		}

		$active_theme_info = array(
			'name'                    => $active_theme->name,
			'version'                 => $active_theme->version,
			'version_latest'          => WC_Admin_Status::get_latest_theme_version( $active_theme ),
			'author_url'              => esc_url_raw( $active_theme->{'Author URI'} ),
			'is_child_theme'          => is_child_theme(),
			'has_woocommerce_support' => current_theme_supports( 'woocommerce' ),
			'has_woocommerce_file'    => ( file_exists( get_stylesheet_directory() . '/woocommerce.php' ) || file_exists( get_template_directory() . '/woocommerce.php' ) ),
			'has_outdated_templates'  => $outdated_templates,
			'overrides'               => $override_files,
		);

		return array_merge( $active_theme_info, $parent_theme_info );
	}

	/**
	 * Get some setting values for the site that are useful for debugging
	 * purposes. For full settings access, use the settings api.
	 *
	 * @return array
	 */
	public function get_settings() {
		// Get a list of terms used for product/order taxonomies.
		$term_response = array();
		$terms         = get_terms( 'product_type', array( 'hide_empty' => 0 ) );
		foreach ( $terms as $term ) {
			$term_response[ $term->slug ] = strtolower( $term->name );
		}

		// Get a list of terms used for product visibility.
		$product_visibility_terms = array();
		$terms                    = get_terms( 'product_visibility', array( 'hide_empty' => 0 ) );
		foreach ( $terms as $term ) {
			$product_visibility_terms[ $term->slug ] = strtolower( $term->name );
		}

		// Check if WooCommerce.com account is connected.
		$woo_com_connected = 'no';
		$helper_options    = get_option( 'woocommerce_helper_data', array() );
		if ( array_key_exists( 'auth', $helper_options ) && ! empty( $helper_options['auth'] ) ) {
			$woo_com_connected = 'yes';
		}

		// Return array of useful settings for debugging.
		return array(
			'api_enabled'               => 'yes' === get_option( 'woocommerce_api_enabled' ),
			'force_ssl'                 => 'yes' === get_option( 'woocommerce_force_ssl_checkout' ),
			'currency'                  => get_woocommerce_currency(),
			'currency_symbol'           => get_woocommerce_currency_symbol(),
			'currency_position'         => get_option( 'woocommerce_currency_pos' ),
			'thousand_separator'        => wc_get_price_thousand_separator(),
			'decimal_separator'         => wc_get_price_decimal_separator(),
			'number_of_decimals'        => wc_get_price_decimals(),
			'geolocation_enabled'       => in_array( get_option( 'woocommerce_default_customer_address' ), array( 'geolocation_ajax', 'geolocation' ), true ),
			'taxonomies'                => $term_response,
			'product_visibility_terms'  => $product_visibility_terms,
			'woocommerce_com_connected' => $woo_com_connected,
		);
	}

	/**
	 * Returns security tips.
	 *
	 * @return array
	 */
	public function get_security_info() {
		$check_page = wc_get_page_permalink( 'shop' );
		return array(
			'secure_connection' => 'https' === substr( $check_page, 0, 5 ),
			'hide_errors'       => ! ( defined( 'WP_DEBUG' ) && defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG && WP_DEBUG_DISPLAY ) || 0 === intval( ini_get( 'display_errors' ) ),
		);
	}

	/**
	 * Returns a mini-report on WC pages and if they are configured correctly:
	 * Present, visible, and including the correct shortcode or block.
	 *
	 * @return array
	 */
	public function get_pages() {
		// WC pages to check against.
		$check_pages = array(
			_x( 'Shop base', 'Page setting', 'woocommerce' ) => array(
				'option'    => 'woocommerce_shop_page_id',
				'shortcode' => '',
				'block'     => '',
			),
			_x( 'Cart', 'Page setting', 'woocommerce' ) => array(
				'option'    => 'woocommerce_cart_page_id',
				'shortcode' => '[' . apply_filters( 'woocommerce_cart_shortcode_tag', 'woocommerce_cart' ) . ']',
				'block'     => 'woocommerce/cart',
			),
			_x( 'Checkout', 'Page setting', 'woocommerce' ) => array(
				'option'    => 'woocommerce_checkout_page_id',
				'shortcode' => '[' . apply_filters( 'woocommerce_checkout_shortcode_tag', 'woocommerce_checkout' ) . ']',
				'block'     => 'woocommerce/checkout',
			),
			_x( 'My account', 'Page setting', 'woocommerce' ) => array(
				'option'    => 'woocommerce_myaccount_page_id',
				'shortcode' => '[' . apply_filters( 'woocommerce_my_account_shortcode_tag', 'woocommerce_my_account' ) . ']',
				'block'     => '',
			),
			_x( 'Terms and conditions', 'Page setting', 'woocommerce' ) => array(
				'option'    => 'woocommerce_terms_page_id',
				'shortcode' => '',
				'block'     => '',
			),
		);

		$pages_output = array();
		foreach ( $check_pages as $page_name => $values ) {
			$page_id            = get_option( $values['option'] );
			$page_set           = false;
			$page_exists        = false;
			$page_visible       = false;
			$shortcode_present  = false;
			$shortcode_required = false;
			$block_present      = false;
			$block_required     = false;

			// Page checks.
			if ( $page_id ) {
				$page_set = true;
			}
			if ( get_post( $page_id ) ) {
				$page_exists = true;
			}
			if ( 'publish' === get_post_status( $page_id ) ) {
				$page_visible = true;
			}

			// Shortcode checks.
			if ( $values['shortcode'] && get_post( $page_id ) ) {
				$shortcode_required = true;
				$page               = get_post( $page_id );
				if ( strstr( $page->post_content, $values['shortcode'] ) ) {
					$shortcode_present = true;
				}
			}

			// Block checks.
			if ( $values['block'] && get_post( $page_id ) ) {
				$block_required = true;
				$block_present = WC_Blocks_Utils::has_block_in_page( $page_id, $values['block'] );
			}

			// Wrap up our findings into an output array.
			$pages_output[] = array(
				'page_name'          => $page_name,
				'page_id'            => $page_id,
				'page_set'           => $page_set,
				'page_exists'        => $page_exists,
				'page_visible'       => $page_visible,
				'shortcode'          => $values['shortcode'],
				'block'              => $values['block'],
				'shortcode_required' => $shortcode_required,
				'shortcode_present'  => $shortcode_present,
				'block_present'      => $block_present,
				'block_required'     => $block_required,
			);
		}

		return $pages_output;
	}

	/**
	 * Get any query params needed.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		return array(
			'context' => $this->get_context_param( array( 'default' => 'view' ) ),
		);
	}

	/**
	 * Prepare the system status response
	 *
	 * @param  array           $system_status System status data.
	 * @param  WP_REST_Request $request       Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_item_for_response( $system_status, $request ) {
		$data = $this->add_additional_fields_to_object( $system_status, $request );
		$data = $this->filter_response_by_context( $data, 'view' );

		$response = rest_ensure_response( $data );

		/**
		 * Filter the system status returned from the REST API.
		 *
		 * @param WP_REST_Response   $response The response object.
		 * @param mixed              $system_status System status
		 * @param WP_REST_Request    $request  Request object.
		 */
		return apply_filters( 'woocommerce_rest_prepare_system_status', $response, $system_status, $request );
	}
}
rest-api/Controllers/Version2/class-wc-rest-product-categories-v2-controller.php000064400000016347151336044760024040 0ustar00<?php
/**
 * REST API Product Categories controller
 *
 * Handles requests to the products/categories endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Product Categories controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Product_Categories_V1_Controller
 */
class WC_REST_Product_Categories_V2_Controller extends WC_REST_Product_Categories_V1_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';

	/**
	 * Prepare a single product category output for response.
	 *
	 * @param WP_Term         $item    Term object.
	 * @param WP_REST_Request $request Request instance.
	 * @return WP_REST_Response
	 */
	public function prepare_item_for_response( $item, $request ) {
		// Get category display type.
		$display_type = get_term_meta( $item->term_id, 'display_type', true );

		// Get category order.
		$menu_order = get_term_meta( $item->term_id, 'order', true );

		$data = array(
			'id'          => (int) $item->term_id,
			'name'        => $item->name,
			'slug'        => $item->slug,
			'parent'      => (int) $item->parent,
			'description' => $item->description,
			'display'     => $display_type ? $display_type : 'default',
			'image'       => null,
			'menu_order'  => (int) $menu_order,
			'count'       => (int) $item->count,
		);

		// Get category image.
		$image_id = get_term_meta( $item->term_id, 'thumbnail_id', true );
		if ( $image_id ) {
			$attachment = get_post( $image_id );

			$data['image'] = array(
				'id'                => (int) $image_id,
				'date_created'      => wc_rest_prepare_date_response( $attachment->post_date ),
				'date_created_gmt'  => wc_rest_prepare_date_response( $attachment->post_date_gmt ),
				'date_modified'     => wc_rest_prepare_date_response( $attachment->post_modified ),
				'date_modified_gmt' => wc_rest_prepare_date_response( $attachment->post_modified_gmt ),
				'src'               => wp_get_attachment_url( $image_id ),
				'title'             => get_the_title( $attachment ),
				'alt'               => get_post_meta( $image_id, '_wp_attachment_image_alt', true ),
			);
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $item, $request ) );

		/**
		 * Filter a term item returned from the API.
		 *
		 * Allows modification of the term data right before it is returned.
		 *
		 * @param WP_REST_Response  $response  The response object.
		 * @param object            $item      The original term object.
		 * @param WP_REST_Request   $request   Request used to generate the response.
		 */
		return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request );
	}

	/**
	 * Get the Category schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => $this->taxonomy,
			'type'       => 'object',
			'properties' => array(
				'id'          => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'name'        => array(
					'description' => __( 'Category name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				'slug'        => array(
					'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_title',
					),
				),
				'parent'      => array(
					'description' => __( 'The ID for the parent of the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'description' => array(
					'description' => __( 'HTML description of the resource.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'wp_filter_post_kses',
					),
				),
				'display'     => array(
					'description' => __( 'Category archive display type.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'default',
					'enum'        => array( 'default', 'products', 'subcategories', 'both' ),
					'context'     => array( 'view', 'edit' ),
				),
				'image'       => array(
					'description' => __( 'Image data.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'properties'  => array(
						'id'                => array(
							'description' => __( 'Image ID.', 'woocommerce' ),
							'type'        => 'integer',
							'context'     => array( 'view', 'edit' ),
						),
						'date_created'      => array(
							'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ),
							'type'        => 'date-time',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'date_created_gmt'  => array(
							'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ),
							'type'        => 'date-time',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'date_modified'     => array(
							'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ),
							'type'        => 'date-time',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'date_modified_gmt' => array(
							'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ),
							'type'        => 'date-time',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'src'               => array(
							'description' => __( 'Image URL.', 'woocommerce' ),
							'type'        => 'string',
							'format'      => 'uri',
							'context'     => array( 'view', 'edit' ),
						),
						'title'             => array(
							'description' => __( 'Image name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'alt'               => array(
							'description' => __( 'Image alternative text.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
					),
				),
				'menu_order'  => array(
					'description' => __( 'Menu order, used to custom sort the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'count'       => array(
					'description' => __( 'Number of published products for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version2/class-wc-rest-webhook-deliveries-v2-controller.php000064400000011552151336044760024015 0ustar00<?php
/**
 * REST API Webhooks controller
 *
 * Handles requests to the /webhooks/<webhook_id>/deliveries endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Webhook Deliveries controller class.
 *
 * @deprecated 3.3.0 Webhooks deliveries logs now uses logging system.
 * @package WooCommerce\RestApi
 * @extends WC_REST_Webhook_Deliveries_V1_Controller
 */
class WC_REST_Webhook_Deliveries_V2_Controller extends WC_REST_Webhook_Deliveries_V1_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';

	/**
	 * Prepare a single webhook delivery output for response.
	 *
	 * @param  stdClass        $log Delivery log object.
	 * @param  WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_item_for_response( $log, $request ) {
		$data = (array) $log;

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $log ) );

		/**
		 * Filter webhook delivery object returned from the REST API.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param stdClass         $log      Delivery log object used to create response.
		 * @param WP_REST_Request  $request  Request object.
		 */
		return apply_filters( 'woocommerce_rest_prepare_webhook_delivery', $response, $log, $request );
	}

	/**
	 * Get the Webhook's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'webhook_delivery',
			'type'       => 'object',
			'properties' => array(
				'id'               => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'duration'         => array(
					'description' => __( 'The delivery duration, in seconds.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'summary'          => array(
					'description' => __( 'A friendly summary of the response including the HTTP response code, message, and body.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'request_url'      => array(
					'description' => __( 'The URL where the webhook was delivered.', 'woocommerce' ),
					'type'        => 'string',
					'format'      => 'uri',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'request_headers'  => array(
					'description' => __( 'Request headers.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view' ),
					'readonly'    => true,
					'items'       => array(
						'type' => 'string',
					),
				),
				'request_body'     => array(
					'description' => __( 'Request body.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'response_code'    => array(
					'description' => __( 'The HTTP response code from the receiving server.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'response_message' => array(
					'description' => __( 'The HTTP response message from the receiving server.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'response_headers' => array(
					'description' => __( 'Array of the response headers from the receiving server.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view' ),
					'readonly'    => true,
					'items'       => array(
						'type' => 'string',
					),
				),
				'response_body'    => array(
					'description' => __( 'The response body from the receiving server.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'date_created'     => array(
					'description' => __( "The date the webhook delivery was logged, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_created_gmt' => array(
					'description' => __( 'The date the webhook delivery was logged, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version2/class-wc-rest-reports-v2-controller.php000064400000000723151336044760021722 0ustar00<?php
/**
 * REST API Reports controller
 *
 * Handles requests to the reports endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API Reports controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Reports_V1_Controller
 */
class WC_REST_Reports_V2_Controller extends WC_REST_Reports_V1_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';
}
rest-api/Controllers/Telemetry/class-wc-rest-telemetry-controller.php000064400000006516151336044760022162 0ustar00<?php
/**
 * REST API WC Telemetry controller
 *
 * Handles requests to the /wc-telemetry endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Telemetry controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Controller
 */
class WC_REST_Telemetry_Controller extends WC_REST_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-telemetry';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'tracker';

	/**
	 * Register the route for /tracker
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'record_usage_data' ),
					'permission_callback' => array( $this, 'telemetry_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Check whether a given request has permission to post telemetry data
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function telemetry_permissions_check( $request ) {
		if ( ! is_user_logged_in() ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you post telemetry data.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}
		return true;
	}

	/**
	 * Record WCTracker Data
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 */
	public function record_usage_data( $request ) {
		$new = $this->get_usage_data( $request );
		if ( ! $new || ! $new['platform'] ) {
			return;
		}

		$data = get_option( 'woocommerce_mobile_app_usage' );
		if ( ! $data ) {
			$data = array();
		}

		$platform = $new['platform'];
		if ( ! $data[ $platform ] || version_compare( $new['version'], $data[ $platform ]['version'], '>=' ) ) {
			$data[ $platform ] = $new;
		}

		update_option( 'woocommerce_mobile_app_usage', $data );
	}

	/**
	 * Get usage data from current request
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return Array
	 */
	public function get_usage_data( $request ) {
		$platform = strtolower( $request->get_param( 'platform' ) );
		switch ( $platform ) {
			case 'ios':
			case 'android':
				break;
			default:
				return;
		}

		$version = $request->get_param( 'version' );
		if ( ! $version ) {
			return;
		}

		return array(
			'platform'  => sanitize_text_field( $platform ),
			'version'   => sanitize_text_field( $version ),
			'last_used' => gmdate( 'c' ),
		);
	}

	/**
	 * Get any query params needed.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		return array(
			'platform' => array(
				'description'       => __( 'Platform to track.', 'woocommerce' ),
				'required'          => true,
				'type'              => 'string',
				'sanitize_callback' => 'sanitize_text_field',
				'validate_callback' => 'rest_validate_request_arg',
			),
			'version'  => array(
				'description'       => __( 'Platform version to track.', 'woocommerce' ),
				'required'          => true,
				'type'              => 'string',
				'sanitize_callback' => 'sanitize_text_field',
				'validate_callback' => 'rest_validate_request_arg',
			),
		);
	}
}
rest-api/Controllers/Version1/class-wc-rest-product-attributes-v1-controller.php000064400000046752151336044760024102 0ustar00<?php
/**
 * REST API Product Attributes controller
 *
 * Handles requests to the products/attributes endpoint.
 *
 * @package WooCommerce\RestApi
 * @since    3.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * REST API Product Attributes controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Controller
 */
class WC_REST_Product_Attributes_V1_Controller extends WC_REST_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v1';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'products/attributes';

	/**
	 * Attribute name.
	 *
	 * @var string
	 */
	protected $attribute = '';

	/**
	 * Cached taxonomies by attribute id.
	 *
	 * @var array
	 */
	protected $taxonomies_by_id = array();

	/**
	 * Register the routes for product attributes.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_item' ),
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
					'args'                => array_merge(
						$this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
						array(
							'name' => array(
								'description' => __( 'Name for the resource.', 'woocommerce' ),
								'type'        => 'string',
								'required'    => true,
							),
						)
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\d]+)',
			array(
				'args'   => array(
					'id' => array(
						'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => array(
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_item' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_item' ),
					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
					'args'                => array(
						'force' => array(
							'default'     => true,
							'type'        => 'boolean',
							'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
						),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/batch',
			array(
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'batch_items' ),
					'permission_callback' => array( $this, 'batch_items_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				'schema' => array( $this, 'get_public_batch_schema' ),
			)
		);
	}

	/**
	 * Check if a given request has access to read the attributes.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_items_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'attributes', 'read' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access to create a attribute.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function create_item_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'attributes', 'create' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you cannot create new resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access to read a attribute.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_item_permissions_check( $request ) {
		if ( ! $this->get_taxonomy( $request ) ) {
			return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		if ( ! wc_rest_check_manager_permissions( 'attributes', 'read' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access to update a attribute.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function update_item_permissions_check( $request ) {
		if ( ! $this->get_taxonomy( $request ) ) {
			return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		if ( ! wc_rest_check_manager_permissions( 'attributes', 'edit' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot update resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access to delete a attribute.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function delete_item_permissions_check( $request ) {
		if ( ! $this->get_taxonomy( $request ) ) {
			return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		if ( ! wc_rest_check_manager_permissions( 'attributes', 'delete' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access batch create, update and delete items.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 *
	 * @return bool|WP_Error
	 */
	public function batch_items_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'attributes', 'batch' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Get all attributes.
	 *
	 * @param WP_REST_Request $request The request to get the attributes from.
	 * @return array
	 */
	public function get_items( $request ) {
		$attributes = wc_get_attribute_taxonomies();
		$data       = array();
		foreach ( $attributes as $attribute_obj ) {
			$attribute = $this->prepare_item_for_response( $attribute_obj, $request );
			$attribute = $this->prepare_response_for_collection( $attribute );
			$data[]    = $attribute;
		}

		$response = rest_ensure_response( $data );

		// This API call always returns all product attributes due to retrieval from the object cache.
		$response->header( 'X-WP-Total', count( $data ) );
		$response->header( 'X-WP-TotalPages', 1 );

		return $response;
	}

	/**
	 * Create a single attribute.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Request|WP_Error
	 */
	public function create_item( $request ) {
		global $wpdb;

		$id = wc_create_attribute(
			array(
				'name'         => $request['name'],
				'slug'         => wc_sanitize_taxonomy_name( stripslashes( $request['slug'] ) ),
				'type'         => ! empty( $request['type'] ) ? $request['type'] : 'select',
				'order_by'     => ! empty( $request['order_by'] ) ? $request['order_by'] : 'menu_order',
				'has_archives' => true === $request['has_archives'],
			)
		);

		// Checks for errors.
		if ( is_wp_error( $id ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_create', $id->get_error_message(), array( 'status' => 400 ) );
		}

		$attribute = $this->get_attribute( $id );

		if ( is_wp_error( $attribute ) ) {
			return $attribute;
		}

		$this->update_additional_fields_for_object( $attribute, $request );

		/**
		 * Fires after a single product attribute is created or updated via the REST API.
		 *
		 * @param stdObject       $attribute Inserted attribute object.
		 * @param WP_REST_Request $request   Request object.
		 * @param boolean         $creating  True when creating attribute, false when updating.
		 */
		do_action( 'woocommerce_rest_insert_product_attribute', $attribute, $request, true );

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_item_for_response( $attribute, $request );
		$response = rest_ensure_response( $response );
		$response->set_status( 201 );
		$response->header( 'Location', rest_url( '/' . $this->namespace . '/' . $this->rest_base . '/' . $attribute->attribute_id ) );

		return $response;
	}

	/**
	 * Get a single attribute.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Request|WP_Error
	 */
	public function get_item( $request ) {
		$attribute = $this->get_attribute( (int) $request['id'] );

		if ( is_wp_error( $attribute ) ) {
			return $attribute;
		}

		$response = $this->prepare_item_for_response( $attribute, $request );

		return rest_ensure_response( $response );
	}

	/**
	 * Update a single term from a taxonomy.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Request|WP_Error
	 */
	public function update_item( $request ) {
		global $wpdb;

		$id     = (int) $request['id'];
		$edited = wc_update_attribute(
			$id,
			array(
				'name'         => $request['name'],
				'slug'         => wc_sanitize_taxonomy_name( stripslashes( $request['slug'] ) ),
				'type'         => $request['type'],
				'order_by'     => $request['order_by'],
				'has_archives' => $request['has_archives'],
			)
		);

		// Checks for errors.
		if ( is_wp_error( $edited ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_edit', $edited->get_error_message(), array( 'status' => 400 ) );
		}

		$attribute = $this->get_attribute( $id );

		if ( is_wp_error( $attribute ) ) {
			return $attribute;
		}

		$this->update_additional_fields_for_object( $attribute, $request );

		/**
		 * Fires after a single product attribute is created or updated via the REST API.
		 *
		 * @param stdObject       $attribute Inserted attribute object.
		 * @param WP_REST_Request $request   Request object.
		 * @param boolean         $creating  True when creating attribute, false when updating.
		 */
		do_action( 'woocommerce_rest_insert_product_attribute', $attribute, $request, false );

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_item_for_response( $attribute, $request );

		return rest_ensure_response( $response );
	}

	/**
	 * Delete a single attribute.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error
	 */
	public function delete_item( $request ) {
		$force = isset( $request['force'] ) ? (bool) $request['force'] : false;

		// We don't support trashing for this type, error out.
		if ( ! $force ) {
			return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Resource does not support trashing.', 'woocommerce' ), array( 'status' => 501 ) );
		}

		$attribute = $this->get_attribute( (int) $request['id'] );

		if ( is_wp_error( $attribute ) ) {
			return $attribute;
		}

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_item_for_response( $attribute, $request );

		$deleted = wc_delete_attribute( $attribute->attribute_id );

		if ( false === $deleted ) {
			return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) );
		}

		/**
		 * Fires after a single attribute is deleted via the REST API.
		 *
		 * @param stdObject        $attribute     The deleted attribute.
		 * @param WP_REST_Response $response The response data.
		 * @param WP_REST_Request  $request  The request sent to the API.
		 */
		do_action( 'woocommerce_rest_delete_product_attribute', $attribute, $response, $request );

		return $response;
	}

	/**
	 * Prepare a single product attribute output for response.
	 *
	 * @param obj             $item Term object.
	 * @param WP_REST_Request $request The request to process.
	 * @return WP_REST_Response
	 */
	public function prepare_item_for_response( $item, $request ) {
		$data = array(
			'id'           => (int) $item->attribute_id,
			'name'         => $item->attribute_label,
			'slug'         => wc_attribute_taxonomy_name( $item->attribute_name ),
			'type'         => $item->attribute_type,
			'order_by'     => $item->attribute_orderby,
			'has_archives' => (bool) $item->attribute_public,
		);

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $item ) );

		/**
		 * Filter a attribute item returned from the API.
		 *
		 * Allows modification of the product attribute data right before it is returned.
		 *
		 * @param WP_REST_Response  $response  The response object.
		 * @param object            $item      The original attribute object.
		 * @param WP_REST_Request   $request   Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_product_attribute', $response, $item, $request );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param object $attribute Attribute object.
	 * @return array Links for the given attribute.
	 */
	protected function prepare_links( $attribute ) {
		$base  = '/' . $this->namespace . '/' . $this->rest_base;
		$links = array(
			'self'       => array(
				'href' => rest_url( trailingslashit( $base ) . $attribute->attribute_id ),
			),
			'collection' => array(
				'href' => rest_url( $base ),
			),
		);

		return $links;
	}

	/**
	 * Get the Attribute's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'product_attribute',
			'type'       => 'object',
			'properties' => array(
				'id'           => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'name'         => array(
					'description' => __( 'Attribute name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				'slug'         => array(
					'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_title',
					),
				),
				'type'         => array(
					'description' => __( 'Type of attribute.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'select',
					'enum'        => array_keys( wc_get_attribute_types() ),
					'context'     => array( 'view', 'edit' ),
				),
				'order_by'     => array(
					'description' => __( 'Default sort order.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'menu_order',
					'enum'        => array( 'menu_order', 'name', 'name_num', 'id' ),
					'context'     => array( 'view', 'edit' ),
				),
				'has_archives' => array(
					'description' => __( 'Enable/Disable attribute archives.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params            = array();
		$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );

		return $params;
	}

	/**
	 * Get attribute name.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return string
	 */
	protected function get_taxonomy( $request ) {
		$attribute_id = $request['id'];

		if ( empty( $attribute_id ) ) {
			return '';
		}

		if ( isset( $this->taxonomies_by_id[ $attribute_id ] ) ) {
			return $this->taxonomies_by_id[ $attribute_id ];
		}

		$taxonomy = WC()->call_function( 'wc_attribute_taxonomy_name_by_id', (int) $request['id'] );
		if ( ! empty( $taxonomy ) ) {
			$this->taxonomies_by_id[ $attribute_id ] = $taxonomy;
		}

		return $taxonomy;
	}

	/**
	 * Get attribute data.
	 *
	 * @param int $id Attribute ID.
	 * @return stdClass|WP_Error
	 */
	protected function get_attribute( $id ) {
		global $wpdb;

		$attribute = $wpdb->get_row(
			$wpdb->prepare(
				"
			SELECT *
			FROM {$wpdb->prefix}woocommerce_attribute_taxonomies
			WHERE attribute_id = %d
		 ",
				$id
			)
		);

		if ( is_wp_error( $attribute ) || is_null( $attribute ) ) {
			return new WP_Error( 'woocommerce_rest_attribute_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		return $attribute;
	}

	/**
	 * Validate attribute slug.
	 *
	 * @deprecated 3.2.0
	 * @param string $slug The slug to validate.
	 * @param bool   $new_data If we are creating new data.
	 * @return bool|WP_Error
	 */
	protected function validate_attribute_slug( $slug, $new_data = true ) {
		if ( strlen( $slug ) >= 28 ) {
			/* translators: %s: slug being validated */
			return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) );
		} elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) {
			/* translators: %s: slug being validated */
			return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) );
		} elseif ( $new_data && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) {
			/* translators: %s: slug being validated */
			return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) );
		}

		return true;
	}

	/**
	 * Schedule to flush rewrite rules.
	 *
	 * @deprecated 3.2.0
	 * @since 3.0.0
	 */
	protected function flush_rewrite_rules() {
		wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' );
	}
}
rest-api/Controllers/Version1/class-wc-rest-tax-classes-v1-controller.php000064400000023010151336044760022443 0ustar00<?php
/**
 * REST API Tax Classes controller
 *
 * Handles requests to the /taxes/classes endpoint.
 *
 * @package WooCommerce\RestApi
 * @since    3.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * REST API Tax Classes controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Controller
 */
class WC_REST_Tax_Classes_V1_Controller extends WC_REST_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v1';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'taxes/classes';

	/**
	 * Register the routes for tax classes.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_item' ),
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<slug>\w[\w\s\-]*)',
			array(
				'args' => array(
					'slug' => array(
						'description' => __( 'Unique slug for the resource.', 'woocommerce' ),
						'type'        => 'string',
					),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_item' ),
					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
					'args'                => array(
						'force' => array(
							'default'     => false,
							'type'        => 'boolean',
							'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
						),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Check whether a given request has permission to read tax classes.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_items_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access create tax classes.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 *
	 * @return bool|WP_Error
	 */
	public function create_item_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'settings', 'create' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access delete a tax.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 *
	 * @return bool|WP_Error
	 */
	public function delete_item_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'settings', 'delete' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Get all tax classes.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return array
	 */
	public function get_items( $request ) {
		$tax_classes = array();

		// Add standard class.
		$tax_classes[] = array(
			'slug' => 'standard',
			'name' => __( 'Standard rate', 'woocommerce' ),
		);

		$classes = WC_Tax::get_tax_classes();

		foreach ( $classes as $class ) {
			$tax_classes[] = array(
				'slug' => sanitize_title( $class ),
				'name' => $class,
			);
		}

		$data = array();
		foreach ( $tax_classes as $tax_class ) {
			$class  = $this->prepare_item_for_response( $tax_class, $request );
			$class  = $this->prepare_response_for_collection( $class );
			$data[] = $class;
		}

		return rest_ensure_response( $data );
	}

	/**
	 * Create a single tax class.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function create_item( $request ) {
		$tax_class = WC_Tax::create_tax_class( $request['name'] );

		if ( is_wp_error( $tax_class ) ) {
			return new WP_Error( 'woocommerce_rest_' . $tax_class->get_error_code(), $tax_class->get_error_message(), array( 'status' => 400 ) );
		}

		$this->update_additional_fields_for_object( $tax_class, $request );

		/**
		 * Fires after a tax class is created or updated via the REST API.
		 *
		 * @param stdClass        $tax_class Data used to create the tax class.
		 * @param WP_REST_Request $request   Request object.
		 * @param boolean         $creating  True when creating tax class, false when updating tax class.
		 */
		do_action( 'woocommerce_rest_insert_tax_class', (object) $tax_class, $request, true );

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_item_for_response( $tax_class, $request );
		$response = rest_ensure_response( $response );
		$response->set_status( 201 );
		$response->header( 'Location', rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $tax_class['slug'] ) ) );

		return $response;
	}

	/**
	 * Delete a single tax class.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function delete_item( $request ) {
		global $wpdb;

		$force = isset( $request['force'] ) ? (bool) $request['force'] : false;

		// We don't support trashing for this type, error out.
		if ( ! $force ) {
			return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Taxes do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) );
		}

		$tax_class = WC_Tax::get_tax_class_by( 'slug', sanitize_title( $request['slug'] ) );
		$deleted   = WC_Tax::delete_tax_class_by( 'slug', sanitize_title( $request['slug'] ) );

		if ( ! $deleted ) {
			return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) );
		}

		if ( is_wp_error( $deleted ) ) {
			return new WP_Error( 'woocommerce_rest_' . $deleted->get_error_code(), $deleted->get_error_message(), array( 'status' => 400 ) );
		}

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_item_for_response( $tax_class, $request );

		/**
		 * Fires after a tax class is deleted via the REST API.
		 *
		 * @param stdClass         $tax_class The tax data.
		 * @param WP_REST_Response $response  The response returned from the API.
		 * @param WP_REST_Request  $request   The request sent to the API.
		 */
		do_action( 'woocommerce_rest_delete_tax', (object) $tax_class, $response, $request );

		return $response;
	}

	/**
	 * Prepare a single tax class output for response.
	 *
	 * @param array           $tax_class Tax class data.
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $tax_class, $request ) {
		$data = $tax_class;

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links() );

		/**
		 * Filter tax object returned from the REST API.
		 *
		 * @param WP_REST_Response $response  The response object.
		 * @param stdClass         $tax_class Tax object used to create response.
		 * @param WP_REST_Request  $request   Request object.
		 */
		return apply_filters( 'woocommerce_rest_prepare_tax', $response, (object) $tax_class, $request );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @return array Links for the given tax class.
	 */
	protected function prepare_links() {
		$links = array(
			'collection' => array(
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
			),
		);

		return $links;
	}

	/**
	 * Get the Tax Classes schema, conforming to JSON Schema
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'tax_class',
			'type'       => 'object',
			'properties' => array(
				'slug' => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'name' => array(
					'description' => __( 'Tax class name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'required'    => true,
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		return array(
			'context' => $this->get_context_param( array( 'default' => 'view' ) ),
		);
	}
}
rest-api/Controllers/Version1/class-wc-rest-product-tags-v1-controller.php000064400000007051151336044760022637 0ustar00<?php
/**
 * REST API Product Tags controller
 *
 * Handles requests to the products/tags endpoint.
 *
 * @author   WooThemes
 * @category API
 * @package WooCommerce\RestApi
 * @since    3.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * REST API Product Tags controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Terms_Controller
 */
class WC_REST_Product_Tags_V1_Controller extends WC_REST_Terms_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v1';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'products/tags';

	/**
	 * Taxonomy.
	 *
	 * @var string
	 */
	protected $taxonomy = 'product_tag';

	/**
	 * Prepare a single product tag output for response.
	 *
	 * @param obj $item Term object.
	 * @param WP_REST_Request $request
	 * @return WP_REST_Response $response
	 */
	public function prepare_item_for_response( $item, $request ) {
		$data = array(
			'id'          => (int) $item->term_id,
			'name'        => $item->name,
			'slug'        => $item->slug,
			'description' => $item->description,
			'count'       => (int) $item->count,
		);

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $item, $request ) );

		/**
		 * Filter a term item returned from the API.
		 *
		 * Allows modification of the term data right before it is returned.
		 *
		 * @param WP_REST_Response  $response  The response object.
		 * @param object            $item      The original term object.
		 * @param WP_REST_Request   $request   Request used to generate the response.
		 */
		return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request );
	}

	/**
	 * Get the Tag's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'              => 'http://json-schema.org/draft-04/schema#',
			'title'                => $this->taxonomy,
			'type'                 => 'object',
			'properties'           => array(
				'id' => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'name' => array(
					'description' => __( 'Tag name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				'slug' => array(
					'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_title',
					),
				),
				'description' => array(
					'description' => __( 'HTML description of the resource.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'wp_filter_post_kses',
					),
				),
				'count' => array(
					'description' => __( 'Number of published products for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version1/class-wc-rest-report-sales-v1-controller.php000064400000030713151336044760022644 0ustar00<?php
/**
 * REST API Reports controller
 *
 * Handles requests to the reports/sales endpoint.
 *
 * @author   WooThemes
 * @category API
 * @package WooCommerce\RestApi
 * @since    3.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * REST API Report Sales controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Controller
 */
class WC_REST_Report_Sales_V1_Controller extends WC_REST_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v1';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'reports/sales';

	/**
	 * Report instance.
	 *
	 * @var WC_Admin_Report
	 */
	protected $report;

	/**
	 * Register the routes for sales reports.
	 */
	public function register_routes() {
		register_rest_route( $this->namespace, '/' . $this->rest_base, array(
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_items' ),
				'permission_callback' => array( $this, 'get_items_permissions_check' ),
				'args'                => $this->get_collection_params(),
			),
			'schema' => array( $this, 'get_public_item_schema' ),
		) );
	}

	/**
	 * Check whether a given request has permission to read report.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_items_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'reports', 'read' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Get sales reports.
	 *
	 * @param WP_REST_Request $request
	 * @return array|WP_Error
	 */
	public function get_items( $request ) {
		$data   = array();
		$item   = $this->prepare_item_for_response( null, $request );
		$data[] = $this->prepare_response_for_collection( $item );

		return rest_ensure_response( $data );
	}

	/**
	 * Prepare a report sales object for serialization.
	 *
	 * @param null $_
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $_, $request ) {
		// Set date filtering.
		$filter = array(
			'period'   => $request['period'],
			'date_min' => $request['date_min'],
			'date_max' => $request['date_max'],
		);
		$this->setup_report( $filter );

		// New customers.
		$users_query = new WP_User_Query(
			array(
				'fields' => array( 'user_registered' ),
				'role'   => 'customer',
			)
		);

		$customers = $users_query->get_results();

		foreach ( $customers as $key => $customer ) {
			if ( strtotime( $customer->user_registered ) < $this->report->start_date || strtotime( $customer->user_registered ) > $this->report->end_date ) {
				unset( $customers[ $key ] );
			}
		}

		$total_customers = count( $customers );
		$report_data     = $this->report->get_report_data();
		$period_totals   = array();

		// Setup period totals by ensuring each period in the interval has data.
		for ( $i = 0; $i <= $this->report->chart_interval; $i++ ) {

			switch ( $this->report->chart_groupby ) {
				case 'day' :
					$time = date( 'Y-m-d', strtotime( "+{$i} DAY", $this->report->start_date ) );
					break;
				default :
					$time = date( 'Y-m', strtotime( "+{$i} MONTH", $this->report->start_date ) );
					break;
			}

			// Set the customer signups for each period.
			$customer_count = 0;
			foreach ( $customers as $customer ) {
				if ( date( ( 'day' == $this->report->chart_groupby ) ? 'Y-m-d' : 'Y-m', strtotime( $customer->user_registered ) ) == $time ) {
					$customer_count++;
				}
 			}

			$period_totals[ $time ] = array(
				'sales'     => wc_format_decimal( 0.00, 2 ),
				'orders'    => 0,
				'items'     => 0,
				'tax'       => wc_format_decimal( 0.00, 2 ),
				'shipping'  => wc_format_decimal( 0.00, 2 ),
				'discount'  => wc_format_decimal( 0.00, 2 ),
				'customers' => $customer_count,
			);
		}

		// add total sales, total order count, total tax and total shipping for each period
		foreach ( $report_data->orders as $order ) {
			$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) );

			if ( ! isset( $period_totals[ $time ] ) ) {
				continue;
			}

			$period_totals[ $time ]['sales']    = wc_format_decimal( $order->total_sales, 2 );
			$period_totals[ $time ]['tax']      = wc_format_decimal( $order->total_tax + $order->total_shipping_tax, 2 );
			$period_totals[ $time ]['shipping'] = wc_format_decimal( $order->total_shipping, 2 );
		}

		foreach ( $report_data->order_counts as $order ) {
			$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) );

			if ( ! isset( $period_totals[ $time ] ) ) {
				continue;
			}

			$period_totals[ $time ]['orders']   = (int) $order->count;
		}

		// Add total order items for each period.
		foreach ( $report_data->order_items as $order_item ) {
			$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order_item->post_date ) ) : date( 'Y-m', strtotime( $order_item->post_date ) );

			if ( ! isset( $period_totals[ $time ] ) ) {
				continue;
			}

			$period_totals[ $time ]['items'] = (int) $order_item->order_item_count;
		}

		// Add total discount for each period.
		foreach ( $report_data->coupons as $discount ) {
			$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $discount->post_date ) ) : date( 'Y-m', strtotime( $discount->post_date ) );

			if ( ! isset( $period_totals[ $time ] ) ) {
				continue;
			}

			$period_totals[ $time ]['discount'] = wc_format_decimal( $discount->discount_amount, 2 );
		}

		$sales_data = array(
			'total_sales'       => $report_data->total_sales,
			'net_sales'         => $report_data->net_sales,
			'average_sales'     => $report_data->average_sales,
			'total_orders'      => $report_data->total_orders,
			'total_items'       => $report_data->total_items,
			'total_tax'         => wc_format_decimal( $report_data->total_tax + $report_data->total_shipping_tax, 2 ),
			'total_shipping'    => $report_data->total_shipping,
			'total_refunds'     => $report_data->total_refunds,
			'total_discount'    => $report_data->total_coupons,
			'totals_grouped_by' => $this->report->chart_groupby,
			'totals'            => $period_totals,
			'total_customers'   => $total_customers,
		);

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $sales_data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );
		$response->add_links( array(
			'about' => array(
				'href' => rest_url( sprintf( '%s/reports', $this->namespace ) ),
			),
		) );

		/**
		 * Filter a report sales returned from the API.
		 *
		 * Allows modification of the report sales data right before it is returned.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param stdClass         $data     The original report object.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_report_sales', $response, (object) $sales_data, $request );
	}

	/**
	 * Setup the report object and parse any date filtering.
	 *
	 * @param array $filter date filtering
	 */
	protected function setup_report( $filter ) {
		include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' );
		include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-report-sales-by-date.php' );

		$this->report = new WC_Report_Sales_By_Date();

		if ( empty( $filter['period'] ) ) {
			// Custom date range.
			$filter['period'] = 'custom';

			if ( ! empty( $filter['date_min'] ) || ! empty( $filter['date_max'] ) ) {

				// Overwrite _GET to make use of WC_Admin_Report::calculate_current_range() for custom date ranges.
				$_GET['start_date'] = $filter['date_min'];
				$_GET['end_date'] = isset( $filter['date_max'] ) ? $filter['date_max'] : null;

			} else {

				// Default custom range to today.
				$_GET['start_date'] = $_GET['end_date'] = date( 'Y-m-d', current_time( 'timestamp' ) );
			}
		} else {
			$filter['period'] = empty( $filter['period'] ) ? 'week' : $filter['period'];

			// Change "week" period to "7day".
			if ( 'week' === $filter['period'] ) {
				$filter['period'] = '7day';
			}
		}

		$this->report->calculate_current_range( $filter['period'] );
	}

	/**
	 * Get the Report's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'sales_report',
			'type'       => 'object',
			'properties' => array(
				'total_sales' => array(
					'description' => __( 'Gross sales in the period.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'net_sales' => array(
					'description' => __( 'Net sales in the period.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'average_sales' => array(
					'description' => __( 'Average net daily sales.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'total_orders' => array(
					'description' => __( 'Total of orders placed.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'total_items' => array(
					'description' => __( 'Total of items purchased.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'total_tax' => array(
					'description' => __( 'Total charged for taxes.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'total_shipping' => array(
					'description' => __( 'Total charged for shipping.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'total_refunds' => array(
					'description' => __( 'Total of refunded orders.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'total_discount' => array(
					'description' => __( 'Total of coupons used.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'totals_grouped_by' => array(
					'description' => __( 'Group type.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'totals' => array(
					'description' => __( 'Totals.', 'woocommerce' ),
					'type'        => 'array',
					'items'       => array(
						'type'    => 'array',
					),
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		return array(
			'context' => $this->get_context_param( array( 'default' => 'view' ) ),
			'period' => array(
				'description'       => __( 'Report period.', 'woocommerce' ),
				'type'              => 'string',
				'enum'              => array( 'week', 'month', 'last_month', 'year' ),
				'validate_callback' => 'rest_validate_request_arg',
				'sanitize_callback' => 'sanitize_text_field',
			),
			'date_min' => array(
				/* translators: %s: date format */
				'description'       => sprintf( __( 'Return sales for a specific start date, the date need to be in the %s format.', 'woocommerce' ), 'YYYY-MM-DD' ),
				'type'              => 'string',
				'format'            => 'date',
				'validate_callback' => 'wc_rest_validate_reports_request_arg',
				'sanitize_callback' => 'sanitize_text_field',
			),
			'date_max' => array(
				/* translators: %s: date format */
				'description'       => sprintf( __( 'Return sales for a specific end date, the date need to be in the %s format.', 'woocommerce' ), 'YYYY-MM-DD' ),
				'type'              => 'string',
				'format'            => 'date',
				'validate_callback' => 'wc_rest_validate_reports_request_arg',
				'sanitize_callback' => 'sanitize_text_field',
			),
		);
	}
}
rest-api/Controllers/Version1/class-wc-rest-webhook-deliveries-v1-controller.php000064400000023020151336044760024004 0ustar00<?php
/**
 * REST API Webhooks controller
 *
 * Handles requests to the /webhooks/<webhook_id>/deliveries endpoint.
 *
 * @author   WooThemes
 * @category API
 * @package WooCommerce\RestApi
 * @since    3.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * REST API Webhook Deliveries controller class.
 *
 * @deprecated 3.3.0 Webhooks deliveries logs now uses logging system.
 * @package WooCommerce\RestApi
 * @extends WC_REST_Controller
 */
class WC_REST_Webhook_Deliveries_V1_Controller extends WC_REST_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v1';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'webhooks/(?P<webhook_id>[\d]+)/deliveries';

	/**
	 * Register the routes for webhook deliveries.
	 */
	public function register_routes() {
		register_rest_route( $this->namespace, '/' . $this->rest_base, array(
			'args' => array(
				'webhook_id' => array(
					'description' => __( 'Unique identifier for the webhook.', 'woocommerce' ),
					'type'        => 'integer',
				),
			),
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_items' ),
				'permission_callback' => array( $this, 'get_items_permissions_check' ),
				'args'                => $this->get_collection_params(),
			),
			'schema' => array( $this, 'get_public_item_schema' ),
		) );

		register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
			'args' => array(
				'webhook_id' => array(
					'description' => __( 'Unique identifier for the webhook.', 'woocommerce' ),
					'type'        => 'integer',
				),
				'id' => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
				),
			),
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_item' ),
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
				'args'                => array(
					'context' => $this->get_context_param( array( 'default' => 'view' ) ),
				),
			),
			'schema' => array( $this, 'get_public_item_schema' ),
		) );
	}

	/**
	 * Check whether a given request has permission to read taxes.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_items_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'webhooks', 'read' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access to read a tax.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_item_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'webhooks', 'read' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Get all webhook deliveries.
	 *
	 * @param WP_REST_Request $request
	 *
	 * @return array|WP_Error
	 */
	public function get_items( $request ) {
		$webhook = wc_get_webhook( (int) $request['webhook_id'] );

		if ( empty( $webhook ) || is_null( $webhook ) ) {
			return new WP_Error( 'woocommerce_rest_webhook_invalid_id', __( 'Invalid webhook ID.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$logs = array();
		$data = array();
		foreach ( $logs as $log ) {
			$delivery = $this->prepare_item_for_response( (object) $log, $request );
			$delivery = $this->prepare_response_for_collection( $delivery );
			$data[]   = $delivery;
		}

		return rest_ensure_response( $data );
	}

	/**
	 * Get a single webhook delivery.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_item( $request ) {
		$id      = (int) $request['id'];
		$webhook = wc_get_webhook( (int) $request['webhook_id'] );

		if ( empty( $webhook ) || is_null( $webhook ) ) {
			return new WP_Error( 'woocommerce_rest_webhook_invalid_id', __( 'Invalid webhook ID.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$log = array();

		if ( empty( $id ) || empty( $log ) ) {
			return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$delivery = $this->prepare_item_for_response( (object) $log, $request );
		$response = rest_ensure_response( $delivery );

		return $response;
	}

	/**
	 * Prepare a single webhook delivery output for response.
	 *
	 * @param stdClass $log Delivery log object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $log, $request ) {
		$data    = (array) $log;
		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $log ) );

		/**
		 * Filter webhook delivery object returned from the REST API.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param stdClass         $log      Delivery log object used to create response.
		 * @param WP_REST_Request  $request  Request object.
		 */
		return apply_filters( 'woocommerce_rest_prepare_webhook_delivery', $response, $log, $request );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param stdClass $log Delivery log object.
	 * @return array Links for the given webhook delivery.
	 */
	protected function prepare_links( $log ) {
		$webhook_id = (int) $log->request_headers['X-WC-Webhook-ID'];
		$base       = str_replace( '(?P<webhook_id>[\d]+)', $webhook_id, $this->rest_base );
		$links      = array(
			'self' => array(
				'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $log->id ) ),
			),
			'collection' => array(
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ),
			),
			'up' => array(
				'href' => rest_url( sprintf( '/%s/webhooks/%d', $this->namespace, $webhook_id ) ),
			),
		);

		return $links;
	}

	/**
	 * Get the Webhook's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'webhook_delivery',
			'type'       => 'object',
			'properties' => array(
				'id' => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'duration' => array(
					'description' => __( 'The delivery duration, in seconds.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'summary' => array(
					'description' => __( 'A friendly summary of the response including the HTTP response code, message, and body.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'request_url' => array(
					'description' => __( 'The URL where the webhook was delivered.', 'woocommerce' ),
					'type'        => 'string',
					'format'      => 'uri',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'request_headers' => array(
					'description' => __( 'Request headers.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view' ),
					'readonly'    => true,
					'items'       => array(
						'type'    => 'string',
					),
				),
				'request_body' => array(
					'description' => __( 'Request body.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'response_code' => array(
					'description' => __( 'The HTTP response code from the receiving server.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'response_message' => array(
					'description' => __( 'The HTTP response message from the receiving server.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'response_headers' => array(
					'description' => __( 'Array of the response headers from the receiving server.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view' ),
					'readonly'    => true,
					'items'       => array(
						'type'    => 'string',
					),
				),
				'response_body' => array(
					'description' => __( 'The response body from the receiving server.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'date_created' => array(
					'description' => __( "The date the webhook delivery was logged, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		return array(
			'context' => $this->get_context_param( array( 'default' => 'view' ) ),
		);
	}
}
rest-api/Controllers/Version1/class-wc-rest-order-notes-v1-controller.php000064400000034147151336044760022472 0ustar00<?php
/**
 * REST API Order Notes controller
 *
 * Handles requests to the /orders/<order_id>/notes endpoint.
 *
 * @author   WooThemes
 * @category API
 * @package WooCommerce\RestApi
 * @since    3.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * REST API Order Notes controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Controller
 */
class WC_REST_Order_Notes_V1_Controller extends WC_REST_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v1';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'orders/(?P<order_id>[\d]+)/notes';

	/**
	 * Post type.
	 *
	 * @var string
	 */
	protected $post_type = 'shop_order';

	/**
	 * Register the routes for order notes.
	 */
	public function register_routes() {
		register_rest_route( $this->namespace, '/' . $this->rest_base, array(
			'args' => array(
				'order_id'  => array(
					'description' => __( 'The order ID.', 'woocommerce' ),
					'type'        => 'integer',
				),
			),
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_items' ),
				'permission_callback' => array( $this, 'get_items_permissions_check' ),
				'args'                => $this->get_collection_params(),
			),
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'create_item' ),
				'permission_callback' => array( $this, 'create_item_permissions_check' ),
				'args'                => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array(
					'note' => array(
						'type'        => 'string',
						'description' => __( 'Order note content.', 'woocommerce' ),
						'required'    => true,
					),
				) ),
			),
			'schema' => array( $this, 'get_public_item_schema' ),
		) );

		register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
			'args' => array(
				'id' => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
				),
				'order_id'  => array(
					'description' => __( 'The order ID.', 'woocommerce' ),
					'type'        => 'integer',
				),
			),
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_item' ),
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
				'args'                => array(
					'context' => $this->get_context_param( array( 'default' => 'view' ) ),
				),
			),
			array(
				'methods'             => WP_REST_Server::DELETABLE,
				'callback'            => array( $this, 'delete_item' ),
				'permission_callback' => array( $this, 'delete_item_permissions_check' ),
				'args'                => array(
					'force' => array(
						'default'     => false,
						'type'        => 'boolean',
						'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
					),
				),
			),
			'schema' => array( $this, 'get_public_item_schema' ),
		) );
	}

	/**
	 * Check whether a given request has permission to read order notes.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_items_permissions_check( $request ) {
		if ( ! wc_rest_check_post_permissions( $this->post_type, 'read' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access create order notes.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 *
	 * @return bool|WP_Error
	 */
	public function create_item_permissions_check( $request ) {
		if ( ! wc_rest_check_post_permissions( $this->post_type, 'create' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access to read a order note.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_item_permissions_check( $request ) {
		$order = wc_get_order( (int) $request['order_id'] );

		if ( $order && ! wc_rest_check_post_permissions( $this->post_type, 'read', $order->get_id() ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access delete a order note.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 *
	 * @return bool|WP_Error
	 */
	public function delete_item_permissions_check( $request ) {
		$order = wc_get_order( (int) $request['order_id'] );

		if ( $order && ! wc_rest_check_post_permissions( $this->post_type, 'delete', $order->get_id() ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Get order notes from an order.
	 *
	 * @param WP_REST_Request $request
	 *
	 * @return array|WP_Error
	 */
	public function get_items( $request ) {
		$order = wc_get_order( (int) $request['order_id'] );

		if ( ! $order || $this->post_type !== $order->get_type() ) {
			return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$args = array(
			'post_id' => $order->get_id(),
			'approve' => 'approve',
			'type'    => 'order_note',
		);

		remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );

		$notes = get_comments( $args );

		add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );

		$data = array();
		foreach ( $notes as $note ) {
			$order_note = $this->prepare_item_for_response( $note, $request );
			$order_note = $this->prepare_response_for_collection( $order_note );
			$data[]     = $order_note;
		}

		return rest_ensure_response( $data );
	}

	/**
	 * Create a single order note.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function create_item( $request ) {
		if ( ! empty( $request['id'] ) ) {
			/* translators: %s: post type */
			return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) );
		}

		$order = wc_get_order( (int) $request['order_id'] );

		if ( ! $order || $this->post_type !== $order->get_type() ) {
			return new WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		// Create the note.
		$note_id = $order->add_order_note( $request['note'], $request['customer_note'] );

		if ( ! $note_id ) {
			return new WP_Error( 'woocommerce_api_cannot_create_order_note', __( 'Cannot create order note, please try again.', 'woocommerce' ), array( 'status' => 500 ) );
		}

		$note = get_comment( $note_id );
		$this->update_additional_fields_for_object( $note, $request );

		/**
		 * Fires after a order note is created or updated via the REST API.
		 *
		 * @param WP_Comment      $note      New order note object.
		 * @param WP_REST_Request $request   Request object.
		 * @param boolean         $creating  True when creating item, false when updating.
		 */
		do_action( 'woocommerce_rest_insert_order_note', $note, $request, true );

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_item_for_response( $note, $request );
		$response = rest_ensure_response( $response );
		$response->set_status( 201 );
		$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, str_replace( '(?P<order_id>[\d]+)', $order->get_id(), $this->rest_base ), $note_id ) ) );

		return $response;
	}

	/**
	 * Get a single order note.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_item( $request ) {
		$id    = (int) $request['id'];
		$order = wc_get_order( (int) $request['order_id'] );

		if ( ! $order || $this->post_type !== $order->get_type() ) {
			return new WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$note = get_comment( $id );

		if ( empty( $id ) || empty( $note ) || intval( $note->comment_post_ID ) !== intval( $order->get_id() ) ) {
			return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$order_note = $this->prepare_item_for_response( $note, $request );
		$response   = rest_ensure_response( $order_note );

		return $response;
	}

	/**
	 * Delete a single order note.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error
	 */
	public function delete_item( $request ) {
		$id    = (int) $request['id'];
		$force = isset( $request['force'] ) ? (bool) $request['force'] : false;

		// We don't support trashing for this type, error out.
		if ( ! $force ) {
			return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Webhooks do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) );
		}

		$order = wc_get_order( (int) $request['order_id'] );

		if ( ! $order || $this->post_type !== $order->get_type() ) {
			return new WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$note = get_comment( $id );

		if ( empty( $id ) || empty( $note ) || intval( $note->comment_post_ID ) !== intval( $order->get_id() ) ) {
			return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_item_for_response( $note, $request );

		$result = wc_delete_order_note( $note->comment_ID );

		if ( ! $result ) {
			return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), 'order_note' ), array( 'status' => 500 ) );
		}

		/**
		 * Fires after a order note is deleted or trashed via the REST API.
		 *
		 * @param WP_Comment       $note     The deleted or trashed order note.
		 * @param WP_REST_Response $response The response data.
		 * @param WP_REST_Request  $request  The request sent to the API.
		 */
		do_action( 'woocommerce_rest_delete_order_note', $note, $response, $request );

		return $response;
	}

	/**
	 * Prepare a single order note output for response.
	 *
	 * @param WP_Comment $note Order note object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $note, $request ) {
		$data = array(
			'id'            => (int) $note->comment_ID,
			'date_created'  => wc_rest_prepare_date_response( $note->comment_date_gmt ),
			'note'          => $note->comment_content,
			'customer_note' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ),
		);

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $note ) );

		/**
		 * Filter order note object returned from the REST API.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param WP_Comment       $note     Order note object used to create response.
		 * @param WP_REST_Request  $request  Request object.
		 */
		return apply_filters( 'woocommerce_rest_prepare_order_note', $response, $note, $request );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param WP_Comment $note Delivery order_note object.
	 * @return array Links for the given order note.
	 */
	protected function prepare_links( $note ) {
		$order_id = (int) $note->comment_post_ID;
		$base     = str_replace( '(?P<order_id>[\d]+)', $order_id, $this->rest_base );
		$links    = array(
			'self' => array(
				'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $note->comment_ID ) ),
			),
			'collection' => array(
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ),
			),
			'up' => array(
				'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order_id ) ),
			),
		);

		return $links;
	}

	/**
	 * Get the Order Notes schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'order_note',
			'type'       => 'object',
			'properties' => array(
				'id' => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_created' => array(
					'description' => __( "The date the order note was created, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'note' => array(
					'description' => __( 'Order note.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'customer_note' => array(
					'description' => __( 'Shows/define if the note is only for reference or for the customer (the user will be notified).', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		return array(
			'context' => $this->get_context_param( array( 'default' => 'view' ) ),
		);
	}
}
rest-api/Controllers/Version1/class-wc-rest-product-categories-v1-controller.php000064400000020460151336044760024025 0ustar00<?php
/**
 * REST API Product Categories controller
 *
 * Handles requests to the products/categories endpoint.
 *
 * @author   WooThemes
 * @category API
 * @package WooCommerce\RestApi
 * @since    3.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * REST API Product Categories controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Terms_Controller
 */
class WC_REST_Product_Categories_V1_Controller extends WC_REST_Terms_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v1';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'products/categories';

	/**
	 * Taxonomy.
	 *
	 * @var string
	 */
	protected $taxonomy = 'product_cat';

	/**
	 * Prepare a single product category output for response.
	 *
	 * @param WP_Term         $item    Term object.
	 * @param WP_REST_Request $request Request instance.
	 * @return WP_REST_Response
	 */
	public function prepare_item_for_response( $item, $request ) {
		// Get category display type.
		$display_type = get_term_meta( $item->term_id, 'display_type', true );

		// Get category order.
		$menu_order = get_term_meta( $item->term_id, 'order', true );

		$data = array(
			'id'          => (int) $item->term_id,
			'name'        => $item->name,
			'slug'        => $item->slug,
			'parent'      => (int) $item->parent,
			'description' => $item->description,
			'display'     => $display_type ? $display_type : 'default',
			'image'       => null,
			'menu_order'  => (int) $menu_order,
			'count'       => (int) $item->count,
		);

		// Get category image.
		$image_id = get_term_meta( $item->term_id, 'thumbnail_id', true );
		if ( $image_id ) {
			$attachment = get_post( $image_id );

			$data['image'] = array(
				'id'            => (int) $image_id,
				'date_created'  => wc_rest_prepare_date_response( $attachment->post_date_gmt ),
				'date_modified' => wc_rest_prepare_date_response( $attachment->post_modified_gmt ),
				'src'           => wp_get_attachment_url( $image_id ),
				'title'         => get_the_title( $attachment ),
				'alt'           => get_post_meta( $image_id, '_wp_attachment_image_alt', true ),
			);
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $item, $request ) );

		/**
		 * Filter a term item returned from the API.
		 *
		 * Allows modification of the term data right before it is returned.
		 *
		 * @param WP_REST_Response  $response  The response object.
		 * @param object            $item      The original term object.
		 * @param WP_REST_Request   $request   Request used to generate the response.
		 */
		return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request );
	}

	/**
	 * Update term meta fields.
	 *
	 * @param WP_Term         $term    Term object.
	 * @param WP_REST_Request $request Request instance.
	 * @return bool|WP_Error
	 */
	protected function update_term_meta_fields( $term, $request ) {
		$id = (int) $term->term_id;

		if ( isset( $request['display'] ) ) {
			update_term_meta( $id, 'display_type', 'default' === $request['display'] ? '' : $request['display'] );
		}

		if ( isset( $request['menu_order'] ) ) {
			update_term_meta( $id, 'order', $request['menu_order'] );
		}

		if ( isset( $request['image'] ) ) {
			if ( empty( $request['image']['id'] ) && ! empty( $request['image']['src'] ) ) {
				$upload = wc_rest_upload_image_from_url( esc_url_raw( $request['image']['src'] ) );

				if ( is_wp_error( $upload ) ) {
					return $upload;
				}

				$image_id = wc_rest_set_uploaded_image_as_attachment( $upload );
			} else {
				$image_id = isset( $request['image']['id'] ) ? absint( $request['image']['id'] ) : 0;
			}

			// Check if image_id is a valid image attachment before updating the term meta.
			if ( $image_id && wp_attachment_is_image( $image_id ) ) {
				update_term_meta( $id, 'thumbnail_id', $image_id );

				// Set the image alt.
				if ( ! empty( $request['image']['alt'] ) ) {
					update_post_meta( $image_id, '_wp_attachment_image_alt', wc_clean( $request['image']['alt'] ) );
				}

				// Set the image title.
				if ( ! empty( $request['image']['title'] ) ) {
					wp_update_post( array(
						'ID'         => $image_id,
						'post_title' => wc_clean( $request['image']['title'] ),
					) );
				}
			} else {
				delete_term_meta( $id, 'thumbnail_id' );
			}
		}

		return true;
	}

	/**
	 * Get the Category schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'              => 'http://json-schema.org/draft-04/schema#',
			'title'                => $this->taxonomy,
			'type'                 => 'object',
			'properties'           => array(
				'id' => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'name' => array(
					'description' => __( 'Category name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				'slug' => array(
					'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_title',
					),
				),
				'parent' => array(
					'description' => __( 'The ID for the parent of the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'description' => array(
					'description' => __( 'HTML description of the resource.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'wp_filter_post_kses',
					),
				),
				'display' => array(
					'description' => __( 'Category archive display type.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'default',
					'enum'        => array( 'default', 'products', 'subcategories', 'both' ),
					'context'     => array( 'view', 'edit' ),
				),
				'image' => array(
					'description' => __( 'Image data.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'properties'  => array(
						'id' => array(
							'description' => __( 'Image ID.', 'woocommerce' ),
							'type'        => 'integer',
							'context'     => array( 'view', 'edit' ),
						),
						'date_created' => array(
							'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ),
							'type'        => 'date-time',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'date_modified' => array(
							'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ),
							'type'        => 'date-time',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'src' => array(
							'description' => __( 'Image URL.', 'woocommerce' ),
							'type'        => 'string',
							'format'      => 'uri',
							'context'     => array( 'view', 'edit' ),
						),
						'title' => array(
							'description' => __( 'Image name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'alt' => array(
							'description' => __( 'Image alternative text.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
					),
				),
				'menu_order' => array(
					'description' => __( 'Menu order, used to custom sort the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'count' => array(
					'description' => __( 'Number of published products for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version1/class-wc-rest-customer-downloads-v1-controller.php000064400000016707151336044760024064 0ustar00<?php
/**
 * REST API Customer Downloads controller
 *
 * Handles requests to the /customers/<customer_id>/downloads endpoint.
 *
 * @author   WooThemes
 * @category API
 * @package WooCommerce\RestApi
 * @since    3.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * REST API Customers controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Controller
 */
class WC_REST_Customer_Downloads_V1_Controller extends WC_REST_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v1';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'customers/(?P<customer_id>[\d]+)/downloads';

	/**
	 * Register the routes for customers.
	 */
	public function register_routes() {
		register_rest_route( $this->namespace, '/' . $this->rest_base, array(
			'args' => array(
				'customer_id' => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
				),
			),
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_items' ),
				'permission_callback' => array( $this, 'get_items_permissions_check' ),
				'args'                => $this->get_collection_params(),
			),
			'schema' => array( $this, 'get_public_item_schema' ),
		) );
	}

	/**
	 * Check whether a given request has permission to read customers.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_items_permissions_check( $request ) {
		$customer = get_user_by( 'id', (int) $request['customer_id'] );

		if ( ! $customer ) {
			return new WP_Error( 'woocommerce_rest_customer_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		if ( ! wc_rest_check_user_permissions( 'read', $customer->get_id() ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Get all customer downloads.
	 *
	 * @param WP_REST_Request $request
	 * @return array
	 */
	public function get_items( $request ) {
		$downloads = wc_get_customer_available_downloads( (int) $request['customer_id'] );

		$data = array();
		foreach ( $downloads as $download_data ) {
			$download = $this->prepare_item_for_response( (object) $download_data, $request );
			$download = $this->prepare_response_for_collection( $download );
			$data[]   = $download;
		}

		return rest_ensure_response( $data );
	}

	/**
	 * Prepare a single download output for response.
	 *
	 * @param stdObject $download Download object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $download, $request ) {
		$data = (array) $download;
		$data['access_expires']      = $data['access_expires'] ? wc_rest_prepare_date_response( $data['access_expires'] ) : 'never';
		$data['downloads_remaining'] = '' === $data['downloads_remaining'] ? 'unlimited' : $data['downloads_remaining'];

		// Remove "product_name" since it's new in 3.0.
		unset( $data['product_name'] );

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $download, $request ) );

		/**
		 * Filter customer download data returned from the REST API.
		 *
		 * @param WP_REST_Response $response  The response object.
		 * @param stdObject        $download  Download object used to create response.
		 * @param WP_REST_Request  $request   Request object.
		 */
		return apply_filters( 'woocommerce_rest_prepare_customer_download', $response, $download, $request );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param stdClass $download Download object.
	 * @param WP_REST_Request $request Request object.
	 * @return array Links for the given customer download.
	 */
	protected function prepare_links( $download, $request ) {
		$base  = str_replace( '(?P<customer_id>[\d]+)', $request['customer_id'], $this->rest_base );
		$links = array(
			'collection' => array(
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ),
			),
			'product' => array(
				'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $download->product_id ) ),
			),
			'order' => array(
				'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $download->order_id ) ),
			),
		);

		return $links;
	}

	/**
	 * Get the Customer Download's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'customer_download',
			'type'       => 'object',
			'properties' => array(
				'download_url' => array(
					'description' => __( 'Download file URL.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'download_id' => array(
					'description' => __( 'Download ID (MD5).', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'product_id' => array(
					'description' => __( 'Downloadable product ID.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'download_name' => array(
					'description' => __( 'Downloadable file name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'order_id' => array(
					'description' => __( 'Order ID.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'order_key' => array(
					'description' => __( 'Order key.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'downloads_remaining' => array(
					'description' => __( 'Number of downloads remaining.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'access_expires' => array(
					'description' => __( "The date when download access expires, in the site's timezone.", 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'file' => array(
					'description' => __( 'File details.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view' ),
					'readonly'    => true,
					'properties' => array(
						'name' => array(
							'description' => __( 'File name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
						'file' => array(
							'description' => __( 'File URL.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view' ),
							'readonly'    => true,
						),
					),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		return array(
			'context' => $this->get_context_param( array( 'default' => 'view' ) ),
		);
	}
}
rest-api/Controllers/Version1/class-wc-rest-product-attribute-terms-v1-controller.php000064400000017117151336044760025040 0ustar00<?php
/**
 * REST API Product Attribute Terms controller
 *
 * Handles requests to the products/attributes/<attribute_id>/terms endpoint.
 *
 * @author   WooThemes
 * @category API
 * @package WooCommerce\RestApi
 * @since    3.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * REST API Product Attribute Terms controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Terms_Controller
 */
class WC_REST_Product_Attribute_Terms_V1_Controller extends WC_REST_Terms_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v1';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'products/attributes/(?P<attribute_id>[\d]+)/terms';

	/**
	 * Register the routes for terms.
	 */
	public function register_routes() {
		register_rest_route( $this->namespace, '/' . $this->rest_base,
		array(
			'args' => array(
				'attribute_id' => array(
					'description' => __( 'Unique identifier for the attribute of the terms.', 'woocommerce' ),
					'type'        => 'integer',
				),
			),
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_items' ),
				'permission_callback' => array( $this, 'get_items_permissions_check' ),
				'args'                => $this->get_collection_params(),
			),
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'create_item' ),
				'permission_callback' => array( $this, 'create_item_permissions_check' ),
				'args'                => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array(
					'name' => array(
						'type'        => 'string',
						'description' => __( 'Name for the resource.', 'woocommerce' ),
						'required'    => true,
					),
				) ),
			),
			'schema' => array( $this, 'get_public_item_schema' ),
		));

		register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
			'args' => array(
				'id' => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
				),
				'attribute_id' => array(
					'description' => __( 'Unique identifier for the attribute of the terms.', 'woocommerce' ),
					'type'        => 'integer',
				),
			),
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_item' ),
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
				'args'                => array(
					'context'         => $this->get_context_param( array( 'default' => 'view' ) ),
				),
			),
			array(
				'methods'             => WP_REST_Server::EDITABLE,
				'callback'            => array( $this, 'update_item' ),
				'permission_callback' => array( $this, 'update_item_permissions_check' ),
				'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
			),
			array(
				'methods'             => WP_REST_Server::DELETABLE,
				'callback'            => array( $this, 'delete_item' ),
				'permission_callback' => array( $this, 'delete_item_permissions_check' ),
				'args'                => array(
					'force' => array(
						'default'     => false,
						'type'        => 'boolean',
						'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
					),
				),
			),
			'schema' => array( $this, 'get_public_item_schema' ),
		) );

		register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array(
			'args' => array(
				'attribute_id' => array(
					'description' => __( 'Unique identifier for the attribute of the terms.', 'woocommerce' ),
					'type'        => 'integer',
				),
			),
			array(
				'methods'             => WP_REST_Server::EDITABLE,
				'callback'            => array( $this, 'batch_items' ),
				'permission_callback' => array( $this, 'batch_items_permissions_check' ),
				'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
			),
			'schema' => array( $this, 'get_public_batch_schema' ),
		) );
	}

	/**
	 * Prepare a single product attribute term output for response.
	 *
	 * @param WP_Term $item Term object.
	 * @param WP_REST_Request $request
	 * @return WP_REST_Response $response
	 */
	public function prepare_item_for_response( $item, $request ) {
		// Get term order.
		$menu_order = get_term_meta( $item->term_id, 'order_' . $this->taxonomy, true );

		$data = array(
			'id'          => (int) $item->term_id,
			'name'        => $item->name,
			'slug'        => $item->slug,
			'description' => $item->description,
			'menu_order'  => (int) $menu_order,
			'count'       => (int) $item->count,
		);

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $item, $request ) );

		/**
		 * Filter a term item returned from the API.
		 *
		 * Allows modification of the term data right before it is returned.
		 *
		 * @param WP_REST_Response  $response  The response object.
		 * @param object            $item      The original term object.
		 * @param WP_REST_Request   $request   Request used to generate the response.
		 */
		return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request );
	}

	/**
	 * Update term meta fields.
	 *
	 * @param WP_Term $term
	 * @param WP_REST_Request $request
	 * @return bool|WP_Error
	 */
	protected function update_term_meta_fields( $term, $request ) {
		$id = (int) $term->term_id;

		update_term_meta( $id, 'order_' . $this->taxonomy, $request['menu_order'] );

		return true;
	}

	/**
	 * Get the Attribute Term's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'              => 'http://json-schema.org/draft-04/schema#',
			'title'                => 'product_attribute_term',
			'type'                 => 'object',
			'properties'           => array(
				'id' => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'name' => array(
					'description' => __( 'Term name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				'slug' => array(
					'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_title',
					),
				),
				'description' => array(
					'description' => __( 'HTML description of the resource.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'wp_filter_post_kses',
					),
				),
				'menu_order' => array(
					'description' => __( 'Menu order, used to custom sort the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'count' => array(
					'description' => __( 'Number of published products for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php000064400000261642151336044760022076 0ustar00<?php
/**
 * REST API Products controller
 *
 * Handles requests to the /products endpoint.
 *
 * @author   WooThemes
 * @category API
 * @package WooCommerce\RestApi
 * @since    3.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * REST API Products controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Posts_Controller
 */
class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v1';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'products';

	/**
	 * Post type.
	 *
	 * @var string
	 */
	protected $post_type = 'product';

	/**
	 * Initialize product actions.
	 */
	public function __construct() {
		add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 );
		add_action( "woocommerce_rest_insert_{$this->post_type}", array( $this, 'clear_transients' ) );
	}

	/**
	 * Register the routes for products.
	 */
	public function register_routes() {
		register_rest_route( $this->namespace, '/' . $this->rest_base, array(
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_items' ),
				'permission_callback' => array( $this, 'get_items_permissions_check' ),
				'args'                => $this->get_collection_params(),
			),
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'create_item' ),
				'permission_callback' => array( $this, 'create_item_permissions_check' ),
				'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
			),
			'schema' => array( $this, 'get_public_item_schema' ),
		) );

		register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
			'args' => array(
				'id' => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
				),
			),
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_item' ),
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
				'args'                => array(
					'context' => $this->get_context_param( array( 'default' => 'view' ) ),
				),
			),
			array(
				'methods'             => WP_REST_Server::EDITABLE,
				'callback'            => array( $this, 'update_item' ),
				'permission_callback' => array( $this, 'update_item_permissions_check' ),
				'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
			),
			array(
				'methods'             => WP_REST_Server::DELETABLE,
				'callback'            => array( $this, 'delete_item' ),
				'permission_callback' => array( $this, 'delete_item_permissions_check' ),
				'args'                => array(
					'force' => array(
						'default'     => false,
						'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ),
						'type'        => 'boolean',
					),
				),
			),
			'schema' => array( $this, 'get_public_item_schema' ),
		) );

		register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array(
			array(
				'methods'             => WP_REST_Server::EDITABLE,
				'callback'            => array( $this, 'batch_items' ),
				'permission_callback' => array( $this, 'batch_items_permissions_check' ),
				'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
			),
			'schema' => array( $this, 'get_public_batch_schema' ),
		) );
	}

	/**
	 * Get post types.
	 *
	 * @return array
	 */
	protected function get_post_types() {
		return array( 'product', 'product_variation' );
	}

	/**
	 * Query args.
	 *
	 * @param array           $args    Request args.
	 * @param WP_REST_Request $request Request data.
	 * @return array
	 */
	public function query_args( $args, $request ) {
		// Set post_status.
		$args['post_status'] = $request['status'];

		// Taxonomy query to filter products by type, category,
		// tag, shipping class, and attribute.
		$tax_query = array();

		// Map between taxonomy name and arg's key.
		$taxonomies = array(
			'product_cat'            => 'category',
			'product_tag'            => 'tag',
			'product_shipping_class' => 'shipping_class',
		);

		// Set tax_query for each passed arg.
		foreach ( $taxonomies as $taxonomy => $key ) {
			if ( ! empty( $request[ $key ] ) && is_array( $request[ $key ] ) ) {
				$request[ $key ] = array_filter( $request[ $key ] );
			}

			if ( ! empty( $request[ $key ] ) ) {
				$tax_query[] = array(
					'taxonomy' => $taxonomy,
					'field'    => 'term_id',
					'terms'    => $request[ $key ],
				);
			}
		}

		// Filter product type by slug.
		if ( ! empty( $request['type'] ) ) {
			$tax_query[] = array(
				'taxonomy' => 'product_type',
				'field'    => 'slug',
				'terms'    => $request['type'],
			);
		}

		// Filter by attribute and term.
		if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) {
			if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names(), true ) ) {
				$tax_query[] = array(
					'taxonomy' => $request['attribute'],
					'field'    => 'term_id',
					'terms'    => $request['attribute_term'],
				);
			}
		}

		if ( ! empty( $tax_query ) ) {
			$args['tax_query'] = $tax_query;
		}

		// Filter by sku.
		if ( ! empty( $request['sku'] ) ) {
			$skus = explode( ',', $request['sku'] );
			// Include the current string as a SKU too.
			if ( 1 < count( $skus ) ) {
				$skus[] = $request['sku'];
			}

			$args['meta_query'] = $this->add_meta_query( $args, array(
				'key'     => '_sku',
				'value'   => $skus,
				'compare' => 'IN',
			) );
		}

		// Apply all WP_Query filters again.
		if ( is_array( $request['filter'] ) ) {
			$args = array_merge( $args, $request['filter'] );
			unset( $args['filter'] );
		}

		// Force the post_type argument, since it's not a user input variable.
		if ( ! empty( $request['sku'] ) ) {
			$args['post_type'] = array( 'product', 'product_variation' );
		} else {
			$args['post_type'] = $this->post_type;
		}

		return $args;
	}

	/**
	 * Get the downloads for a product or product variation.
	 *
	 * @param WC_Product|WC_Product_Variation $product Product instance.
	 * @return array
	 */
	protected function get_downloads( $product ) {
		$downloads = array();

		if ( $product->is_downloadable() ) {
			foreach ( $product->get_downloads() as $file_id => $file ) {
				$downloads[] = array(
					'id'   => $file_id, // MD5 hash.
					'name' => $file['name'],
					'file' => $file['file'],
				);
			}
		}

		return $downloads;
	}

	/**
	 * Get taxonomy terms.
	 *
	 * @param WC_Product $product  Product instance.
	 * @param string     $taxonomy Taxonomy slug.
	 * @return array
	 */
	protected function get_taxonomy_terms( $product, $taxonomy = 'cat' ) {
		$terms = array();

		foreach ( wc_get_object_terms( $product->get_id(), 'product_' . $taxonomy ) as $term ) {
			$terms[] = array(
				'id'   => $term->term_id,
				'name' => $term->name,
				'slug' => $term->slug,
			);
		}

		return $terms;
	}

	/**
	 * Get the images for a product or product variation.
	 *
	 * @param WC_Product|WC_Product_Variation $product Product instance.
	 * @return array
	 */
	protected function get_images( $product ) {
		$images = array();
		$attachment_ids = array();

		// Add featured image.
		if ( $product->get_image_id() ) {
			$attachment_ids[] = $product->get_image_id();
		}

		// Add gallery images.
		$attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() );

		// Build image data.
		foreach ( $attachment_ids as $position => $attachment_id ) {
			$attachment_post = get_post( $attachment_id );
			if ( is_null( $attachment_post ) ) {
				continue;
			}

			$attachment = wp_get_attachment_image_src( $attachment_id, 'full' );
			if ( ! is_array( $attachment ) ) {
				continue;
			}

			$images[] = array(
				'id'            => (int) $attachment_id,
				'date_created'  => wc_rest_prepare_date_response( $attachment_post->post_date_gmt ),
				'date_modified' => wc_rest_prepare_date_response( $attachment_post->post_modified_gmt ),
				'src'           => current( $attachment ),
				'name'          => get_the_title( $attachment_id ),
				'alt'           => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ),
				'position'      => (int) $position,
			);
		}

		// Set a placeholder image if the product has no images set.
		if ( empty( $images ) ) {
			$images[] = array(
				'id'            => 0,
				'date_created'  => wc_rest_prepare_date_response( current_time( 'mysql' ) ), // Default to now.
				'date_modified' => wc_rest_prepare_date_response( current_time( 'mysql' ) ),
				'src'           => wc_placeholder_img_src(),
				'name'          => __( 'Placeholder', 'woocommerce' ),
				'alt'           => __( 'Placeholder', 'woocommerce' ),
				'position'      => 0,
			);
		}

		return $images;
	}

	/**
	 * Get attribute taxonomy label.
	 *
	 * @param  string $name Taxonomy name.
	 * @return string
	 */
	protected function get_attribute_taxonomy_label( $name ) {
		$tax    = get_taxonomy( $name );
		$labels = get_taxonomy_labels( $tax );

		return $labels->singular_name;
	}

	/**
	 * Get default attributes.
	 *
	 * @param WC_Product $product Product instance.
	 * @return array
	 */
	protected function get_default_attributes( $product ) {
		$default = array();

		if ( $product->is_type( 'variable' ) ) {
			foreach ( array_filter( (array) $product->get_default_attributes(), 'strlen' ) as $key => $value ) {
				if ( 0 === strpos( $key, 'pa_' ) ) {
					$default[] = array(
						'id'     => wc_attribute_taxonomy_id_by_name( $key ),
						'name'   => $this->get_attribute_taxonomy_label( $key ),
						'option' => $value,
					);
				} else {
					$default[] = array(
						'id'     => 0,
						'name'   => wc_attribute_taxonomy_slug( $key ),
						'option' => $value,
					);
				}
			}
		}

		return $default;
	}

	/**
	 * Get attribute options.
	 *
	 * @param int   $product_id Product ID.
	 * @param array $attribute  Attribute data.
	 * @return array
	 */
	protected function get_attribute_options( $product_id, $attribute ) {
		if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) {
			return wc_get_product_terms( $product_id, $attribute['name'], array( 'fields' => 'names' ) );
		} elseif ( isset( $attribute['value'] ) ) {
			return array_map( 'trim', explode( '|', $attribute['value'] ) );
		}

		return array();
	}

	/**
	 * Get the attributes for a product or product variation.
	 *
	 * @param WC_Product|WC_Product_Variation $product Product instance.
	 * @return array
	 */
	protected function get_attributes( $product ) {
		$attributes = array();

		if ( $product->is_type( 'variation' ) ) {
			// Variation attributes.
			foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) {
				$name = str_replace( 'attribute_', '', $attribute_name );

				if ( ! $attribute ) {
					continue;
				}

				// Taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_`.
				if ( 0 === strpos( $attribute_name, 'attribute_pa_' ) ) {
					$option_term = get_term_by( 'slug', $attribute, $name );
					$attributes[] = array(
						'id'     => wc_attribute_taxonomy_id_by_name( $name ),
						'name'   => $this->get_attribute_taxonomy_label( $name ),
						'option' => $option_term && ! is_wp_error( $option_term ) ? $option_term->name : $attribute,
					);
				} else {
					$attributes[] = array(
						'id'     => 0,
						'name'   => $name,
						'option' => $attribute,
					);
				}
			}
		} else {
			foreach ( $product->get_attributes() as $attribute ) {
				if ( $attribute['is_taxonomy'] ) {
					$attributes[] = array(
						'id'        => wc_attribute_taxonomy_id_by_name( $attribute['name'] ),
						'name'      => $this->get_attribute_taxonomy_label( $attribute['name'] ),
						'position'  => (int) $attribute['position'],
						'visible'   => (bool) $attribute['is_visible'],
						'variation' => (bool) $attribute['is_variation'],
						'options'   => $this->get_attribute_options( $product->get_id(), $attribute ),
					);
				} else {
					$attributes[] = array(
						'id'        => 0,
						'name'      => $attribute['name'],
						'position'  => (int) $attribute['position'],
						'visible'   => (bool) $attribute['is_visible'],
						'variation' => (bool) $attribute['is_variation'],
						'options'   => $this->get_attribute_options( $product->get_id(), $attribute ),
					);
				}
			}
		}

		return $attributes;
	}

	/**
	 * Get product menu order.
	 *
	 * @deprecated 3.0.0
	 * @param WC_Product $product Product instance.
	 * @return int
	 */
	protected function get_product_menu_order( $product ) {
		return $product->get_menu_order();
	}

	/**
	 * Get product data.
	 *
	 * @param WC_Product $product Product instance.
	 * @return array
	 */
	protected function get_product_data( $product ) {
		$data = array(
			'id'                    => $product->get_id(),
			'name'                  => $product->get_name(),
			'slug'                  => $product->get_slug(),
			'permalink'             => $product->get_permalink(),
			'date_created'          => wc_rest_prepare_date_response( $product->get_date_created() ),
			'date_modified'         => wc_rest_prepare_date_response( $product->get_date_modified() ),
			'type'                  => $product->get_type(),
			'status'                => $product->get_status(),
			'featured'              => $product->is_featured(),
			'catalog_visibility'    => $product->get_catalog_visibility(),
			'description'           => wpautop( do_shortcode( $product->get_description() ) ),
			'short_description'     => apply_filters( 'woocommerce_short_description', $product->get_short_description() ),
			'sku'                   => $product->get_sku(),
			'price'                 => $product->get_price(),
			'regular_price'         => $product->get_regular_price(),
			'sale_price'            => $product->get_sale_price() ? $product->get_sale_price() : '',
			'date_on_sale_from'     => $product->get_date_on_sale_from() ? date( 'Y-m-d', $product->get_date_on_sale_from()->getTimestamp() ) : '',
			'date_on_sale_to'       => $product->get_date_on_sale_to() ? date( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() ) : '',
			'price_html'            => $product->get_price_html(),
			'on_sale'               => $product->is_on_sale(),
			'purchasable'           => $product->is_purchasable(),
			'total_sales'           => $product->get_total_sales(),
			'virtual'               => $product->is_virtual(),
			'downloadable'          => $product->is_downloadable(),
			'downloads'             => $this->get_downloads( $product ),
			'download_limit'        => $product->get_download_limit(),
			'download_expiry'       => $product->get_download_expiry(),
			'download_type'         => 'standard',
			'external_url'          => $product->is_type( 'external' ) ? $product->get_product_url() : '',
			'button_text'           => $product->is_type( 'external' ) ? $product->get_button_text() : '',
			'tax_status'            => $product->get_tax_status(),
			'tax_class'             => $product->get_tax_class(),
			'manage_stock'          => $product->managing_stock(),
			'stock_quantity'        => $product->get_stock_quantity(),
			'in_stock'              => $product->is_in_stock(),
			'backorders'            => $product->get_backorders(),
			'backorders_allowed'    => $product->backorders_allowed(),
			'backordered'           => $product->is_on_backorder(),
			'sold_individually'     => $product->is_sold_individually(),
			'weight'                => $product->get_weight(),
			'dimensions'            => array(
				'length' => $product->get_length(),
				'width'  => $product->get_width(),
				'height' => $product->get_height(),
			),
			'shipping_required'     => $product->needs_shipping(),
			'shipping_taxable'      => $product->is_shipping_taxable(),
			'shipping_class'        => $product->get_shipping_class(),
			'shipping_class_id'     => $product->get_shipping_class_id(),
			'reviews_allowed'       => $product->get_reviews_allowed(),
			'average_rating'        => wc_format_decimal( $product->get_average_rating(), 2 ),
			'rating_count'          => $product->get_rating_count(),
			'related_ids'           => array_map( 'absint', array_values( wc_get_related_products( $product->get_id() ) ) ),
			'upsell_ids'            => array_map( 'absint', $product->get_upsell_ids() ),
			'cross_sell_ids'        => array_map( 'absint', $product->get_cross_sell_ids() ),
			'parent_id'             => $product->get_parent_id(),
			'purchase_note'         => wpautop( do_shortcode( wp_kses_post( $product->get_purchase_note() ) ) ),
			'categories'            => $this->get_taxonomy_terms( $product ),
			'tags'                  => $this->get_taxonomy_terms( $product, 'tag' ),
			'images'                => $this->get_images( $product ),
			'attributes'            => $this->get_attributes( $product ),
			'default_attributes'    => $this->get_default_attributes( $product ),
			'variations'            => array(),
			'grouped_products'      => array(),
			'menu_order'            => $product->get_menu_order(),
		);

		return $data;
	}

	/**
	 * Get an individual variation's data.
	 *
	 * @param WC_Product $product Product instance.
	 * @return array
	 */
	protected function get_variation_data( $product ) {
		$variations = array();

		foreach ( $product->get_children() as $child_id ) {
			$variation = wc_get_product( $child_id );
			if ( ! $variation || ! $variation->exists() ) {
				continue;
			}

			$variations[] = array(
				'id'                 => $variation->get_id(),
				'date_created'       => wc_rest_prepare_date_response( $variation->get_date_created() ),
				'date_modified'      => wc_rest_prepare_date_response( $variation->get_date_modified() ),
				'permalink'          => $variation->get_permalink(),
				'sku'                => $variation->get_sku(),
				'price'              => $variation->get_price(),
				'regular_price'      => $variation->get_regular_price(),
				'sale_price'         => $variation->get_sale_price(),
				'date_on_sale_from'  => $variation->get_date_on_sale_from() ? date( 'Y-m-d', $variation->get_date_on_sale_from()->getTimestamp() ) : '',
				'date_on_sale_to'    => $variation->get_date_on_sale_to() ? date( 'Y-m-d', $variation->get_date_on_sale_to()->getTimestamp() ) : '',
				'on_sale'            => $variation->is_on_sale(),
				'purchasable'        => $variation->is_purchasable(),
				'visible'            => $variation->is_visible(),
				'virtual'            => $variation->is_virtual(),
				'downloadable'       => $variation->is_downloadable(),
				'downloads'          => $this->get_downloads( $variation ),
				'download_limit'     => '' !== $variation->get_download_limit() ? (int) $variation->get_download_limit() : -1,
				'download_expiry'    => '' !== $variation->get_download_expiry() ? (int) $variation->get_download_expiry() : -1,
				'tax_status'         => $variation->get_tax_status(),
				'tax_class'          => $variation->get_tax_class(),
				'manage_stock'       => $variation->managing_stock(),
				'stock_quantity'     => $variation->get_stock_quantity(),
				'in_stock'           => $variation->is_in_stock(),
				'backorders'         => $variation->get_backorders(),
				'backorders_allowed' => $variation->backorders_allowed(),
				'backordered'        => $variation->is_on_backorder(),
				'weight'             => $variation->get_weight(),
				'dimensions'         => array(
					'length' => $variation->get_length(),
					'width'  => $variation->get_width(),
					'height' => $variation->get_height(),
				),
				'shipping_class'     => $variation->get_shipping_class(),
				'shipping_class_id'  => $variation->get_shipping_class_id(),
				'image'              => $this->get_images( $variation ),
				'attributes'         => $this->get_attributes( $variation ),
			);
		}

		return $variations;
	}

	/**
	 * Prepare a single product output for response.
	 *
	 * @param WP_Post         $post    Post object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_item_for_response( $post, $request ) {
		$product = wc_get_product( $post );
		$data    = $this->get_product_data( $product );

		// Add variations to variable products.
		if ( $product->is_type( 'variable' ) && $product->has_child() ) {
			$data['variations'] = $this->get_variation_data( $product );
		}

		// Add grouped products data.
		if ( $product->is_type( 'grouped' ) && $product->has_child() ) {
			$data['grouped_products'] = $product->get_children();
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $product, $request ) );

		/**
		 * Filter the data for a response.
		 *
		 * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
		 * prepared for the response.
		 *
		 * @param WP_REST_Response   $response   The response object.
		 * @param WP_Post            $post       Post object.
		 * @param WP_REST_Request    $request    Request object.
		 */
		return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param WC_Product      $product Product object.
	 * @param WP_REST_Request $request Request object.
	 * @return array Links for the given product.
	 */
	protected function prepare_links( $product, $request ) {
		$links = array(
			'self' => array(
				'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $product->get_id() ) ),
			),
			'collection' => array(
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
			),
		);

		if ( $product->get_parent_id() ) {
			$links['up'] = array(
				'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product->get_parent_id() ) ),
			);
		}

		return $links;
	}

	/**
	 * Prepare a single product for create or update.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_Error|stdClass $data Post object.
	 */
	protected function prepare_item_for_database( $request ) {
		$id = isset( $request['id'] ) ? absint( $request['id'] ) : 0;

		// Type is the most important part here because we need to be using the correct class and methods.
		if ( isset( $request['type'] ) ) {
			$classname = WC_Product_Factory::get_classname_from_product_type( $request['type'] );

			if ( ! class_exists( $classname ) ) {
				$classname = 'WC_Product_Simple';
			}

			$product = new $classname( $id );
		} elseif ( isset( $request['id'] ) ) {
			$product = wc_get_product( $id );
		} else {
			$product = new WC_Product_Simple();
		}

		// Post title.
		if ( isset( $request['name'] ) ) {
			$product->set_name( wp_filter_post_kses( $request['name'] ) );
		}

		// Post content.
		if ( isset( $request['description'] ) ) {
			$product->set_description( wp_filter_post_kses( $request['description'] ) );
		}

		// Post excerpt.
		if ( isset( $request['short_description'] ) ) {
			$product->set_short_description( wp_filter_post_kses( $request['short_description'] ) );
		}

		// Post status.
		if ( isset( $request['status'] ) ) {
			$product->set_status( get_post_status_object( $request['status'] ) ? $request['status'] : 'draft' );
		}

		// Post slug.
		if ( isset( $request['slug'] ) ) {
			$product->set_slug( $request['slug'] );
		}

		// Menu order.
		if ( isset( $request['menu_order'] ) ) {
			$product->set_menu_order( $request['menu_order'] );
		}

		// Comment status.
		if ( isset( $request['reviews_allowed'] ) ) {
			$product->set_reviews_allowed( $request['reviews_allowed'] );
		}

		/**
		 * Filter the query_vars used in `get_items` for the constructed query.
		 *
		 * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
		 * prepared for insertion.
		 *
		 * @param WC_Product       $product An object representing a single item prepared
		 *                                       for inserting or updating the database.
		 * @param WP_REST_Request $request       Request object.
		 */
		return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $product, $request );
	}

	/**
	 * Create a single product.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function create_item( $request ) {
		if ( ! empty( $request['id'] ) ) {
			return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) );
		}

		$product_id = 0;

		try {
			$product_id = $this->save_product( $request );
			$post       = get_post( $product_id );
			$this->update_additional_fields_for_object( $post, $request );
			$this->update_post_meta_fields( $post, $request );

			/**
			 * Fires after a single item is created or updated via the REST API.
			 *
			 * @param WP_Post         $post      Post data.
			 * @param WP_REST_Request $request   Request object.
			 * @param boolean         $creating  True when creating item, false when updating.
			 */
			do_action( 'woocommerce_rest_insert_product', $post, $request, true );
			$request->set_param( 'context', 'edit' );
			$response = $this->prepare_item_for_response( $post, $request );
			$response = rest_ensure_response( $response );
			$response->set_status( 201 );
			$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) );

			return $response;
		} catch ( WC_Data_Exception $e ) {
			$this->delete_post( $product_id );
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
		} catch ( WC_REST_Exception $e ) {
			$this->delete_post( $product_id );
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Update a single product.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function update_item( $request ) {
		$post_id = (int) $request['id'];

		if ( empty( $post_id ) || get_post_type( $post_id ) !== $this->post_type ) {
			return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) );
		}

		try {
			$product_id = $this->save_product( $request );
			$post       = get_post( $product_id );
			$this->update_additional_fields_for_object( $post, $request );
			$this->update_post_meta_fields( $post, $request );

			/**
			 * Fires after a single item is created or updated via the REST API.
			 *
			 * @param WP_Post         $post      Post data.
			 * @param WP_REST_Request $request   Request object.
			 * @param boolean         $creating  True when creating item, false when updating.
			 */
			do_action( 'woocommerce_rest_insert_product', $post, $request, false );
			$request->set_param( 'context', 'edit' );
			$response = $this->prepare_item_for_response( $post, $request );

			return rest_ensure_response( $response );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
		} catch ( WC_REST_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Saves a product to the database.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return int
	 */
	public function save_product( $request ) {
		$product = $this->prepare_item_for_database( $request );
		return $product->save();
	}

	/**
	 * Save product images.
	 *
	 * @deprecated 3.0.0
	 * @param int $product_id
	 * @param array $images
	 * @throws WC_REST_Exception
	 */
	protected function save_product_images( $product_id, $images ) {
		$product = wc_get_product( $product_id );

		return set_product_images( $product, $images );
	}

	/**
	 * Set product images.
	 *
	 * @throws WC_REST_Exception REST API exceptions.
	 * @param WC_Product $product Product instance.
	 * @param array      $images  Images data.
	 * @return WC_Product
	 */
	protected function set_product_images( $product, $images ) {
		if ( is_array( $images ) ) {
			$gallery = array();

			foreach ( $images as $image ) {
				$attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0;

				if ( 0 === $attachment_id && isset( $image['src'] ) ) {
					$upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) );

					if ( is_wp_error( $upload ) ) {
						if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $product->get_id(), $images ) ) {
							throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 );
						} else {
							continue;
						}
					}

					$attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->get_id() );
				}

				if ( ! wp_attachment_is_image( $attachment_id ) ) {
					throw new WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 );
				}

				if ( isset( $image['position'] ) && 0 === absint( $image['position'] ) ) {
					$product->set_image_id( $attachment_id );
				} else {
					$gallery[] = $attachment_id;
				}

				// Set the image alt if present.
				if ( ! empty( $image['alt'] ) ) {
					update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) );
				}

				// Set the image name if present.
				if ( ! empty( $image['name'] ) ) {
					wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['name'] ) );
				}
			}

			if ( ! empty( $gallery ) ) {
				$product->set_gallery_image_ids( $gallery );
			}
		} else {
			$product->set_image_id( '' );
			$product->set_gallery_image_ids( array() );
		}

		return $product;
	}

	/**
	 * Save product shipping data.
	 *
	 * @param WC_Product $product Product instance.
	 * @param array      $data    Shipping data.
	 * @return WC_Product
	 */
	protected function save_product_shipping_data( $product, $data ) {
		// Virtual.
		if ( isset( $data['virtual'] ) && true === $data['virtual'] ) {
			$product->set_weight( '' );
			$product->set_height( '' );
			$product->set_length( '' );
			$product->set_width( '' );
		} else {
			if ( isset( $data['weight'] ) ) {
				$product->set_weight( $data['weight'] );
			}

			// Height.
			if ( isset( $data['dimensions']['height'] ) ) {
				$product->set_height( $data['dimensions']['height'] );
			}

			// Width.
			if ( isset( $data['dimensions']['width'] ) ) {
				$product->set_width( $data['dimensions']['width'] );
			}

			// Length.
			if ( isset( $data['dimensions']['length'] ) ) {
				$product->set_length( $data['dimensions']['length'] );
			}
		}

		// Shipping class.
		if ( isset( $data['shipping_class'] ) ) {
			$data_store        = $product->get_data_store();
			$shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $data['shipping_class'] ) );
			$product->set_shipping_class_id( $shipping_class_id );
		}

		return $product;
	}

	/**
	 * Save downloadable files.
	 *
	 * @param WC_Product $product    Product instance.
	 * @param array      $downloads  Downloads data.
	 * @param int        $deprecated Deprecated since 3.0.
	 * @return WC_Product
	 */
	protected function save_downloadable_files( $product, $downloads, $deprecated = 0 ) {
		if ( $deprecated ) {
			wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() not requires a variation_id anymore.' );
		}

		$files = array();
		foreach ( $downloads as $key => $file ) {
			if ( empty( $file['file'] ) ) {
				continue;
			}

			$download = new WC_Product_Download();
			$download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() );
			$download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) );
			$download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) );
			$files[]  = $download;
		}
		$product->set_downloads( $files );

		return $product;
	}

	/**
	 * Save taxonomy terms.
	 *
	 * @param WC_Product $product  Product instance.
	 * @param array      $terms    Terms data.
	 * @param string     $taxonomy Taxonomy name.
	 * @return WC_Product
	 */
	protected function save_taxonomy_terms( $product, $terms, $taxonomy = 'cat' ) {
		$term_ids = wp_list_pluck( $terms, 'id' );

		if ( 'cat' === $taxonomy ) {
			$product->set_category_ids( $term_ids );
		} elseif ( 'tag' === $taxonomy ) {
			$product->set_tag_ids( $term_ids );
		}

		return $product;
	}

	/**
	 * Save default attributes.
	 *
	 * @since 3.0.0
	 *
	 * @param WC_Product      $product Product instance.
	 * @param WP_REST_Request $request Request data.
	 * @return WC_Product
	 */
	protected function save_default_attributes( $product, $request ) {
		if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) {
			$attributes         = $product->get_attributes();
			$default_attributes = array();

			foreach ( $request['default_attributes'] as $attribute ) {
				$attribute_id   = 0;
				$attribute_name = '';

				// Check ID for global attributes or name for product attributes.
				if ( ! empty( $attribute['id'] ) ) {
					$attribute_id   = absint( $attribute['id'] );
					$attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
				} elseif ( ! empty( $attribute['name'] ) ) {
					$attribute_name = sanitize_title( $attribute['name'] );
				}

				if ( ! $attribute_id && ! $attribute_name ) {
					continue;
				}

				if ( isset( $attributes[ $attribute_name ] ) ) {
					$_attribute = $attributes[ $attribute_name ];

					if ( $_attribute['is_variation'] ) {
						$value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : '';

						if ( ! empty( $_attribute['is_taxonomy'] ) ) {
							// If dealing with a taxonomy, we need to get the slug from the name posted to the API.
							$term = get_term_by( 'name', $value, $attribute_name );

							if ( $term && ! is_wp_error( $term ) ) {
								$value = $term->slug;
							} else {
								$value = sanitize_title( $value );
							}
						}

						if ( $value ) {
							$default_attributes[ $attribute_name ] = $value;
						}
					}
				}
			}

			$product->set_default_attributes( $default_attributes );
		}

		return $product;
	}

	/**
	 * Save product meta.
	 *
	 * @deprecated 3.0.0
	 * @param WC_Product $product
	 * @param WP_REST_Request $request
	 * @return bool
	 * @throws WC_REST_Exception
	 */
	protected function save_product_meta( $product, $request ) {
		$product = $this->set_product_meta( $product, $request );
		$product->save();

		return true;
	}

	/**
	 * Set product meta.
	 *
	 * @throws WC_REST_Exception REST API exceptions.
	 * @param WC_Product      $product Product instance.
	 * @param WP_REST_Request $request Request data.
	 * @return WC_Product
	 */
	protected function set_product_meta( $product, $request ) {
		// Virtual.
		if ( isset( $request['virtual'] ) ) {
			$product->set_virtual( $request['virtual'] );
		}

		// Tax status.
		if ( isset( $request['tax_status'] ) ) {
			$product->set_tax_status( $request['tax_status'] );
		}

		// Tax Class.
		if ( isset( $request['tax_class'] ) ) {
			$product->set_tax_class( $request['tax_class'] );
		}

		// Catalog Visibility.
		if ( isset( $request['catalog_visibility'] ) ) {
			$product->set_catalog_visibility( $request['catalog_visibility'] );
		}

		// Purchase Note.
		if ( isset( $request['purchase_note'] ) ) {
			$product->set_purchase_note( wp_kses_post( wp_unslash( $request['purchase_note'] ) ) );
		}

		// Featured Product.
		if ( isset( $request['featured'] ) ) {
			$product->set_featured( $request['featured'] );
		}

		// Shipping data.
		$product = $this->save_product_shipping_data( $product, $request );

		// SKU.
		if ( isset( $request['sku'] ) ) {
			$product->set_sku( wc_clean( $request['sku'] ) );
		}

		// Attributes.
		if ( isset( $request['attributes'] ) ) {
			$attributes = array();

			foreach ( $request['attributes'] as $attribute ) {
				$attribute_id   = 0;
				$attribute_name = '';

				// Check ID for global attributes or name for product attributes.
				if ( ! empty( $attribute['id'] ) ) {
					$attribute_id   = absint( $attribute['id'] );
					$attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
				} elseif ( ! empty( $attribute['name'] ) ) {
					$attribute_name = wc_clean( $attribute['name'] );
				}

				if ( ! $attribute_id && ! $attribute_name ) {
					continue;
				}

				if ( $attribute_id ) {

					if ( isset( $attribute['options'] ) ) {
						$options = $attribute['options'];

						if ( ! is_array( $attribute['options'] ) ) {
							// Text based attributes - Posted values are term names.
							$options = explode( WC_DELIMITER, $options );
						}

						$values = array_map( 'wc_sanitize_term_text_based', $options );
						$values = array_filter( $values, 'strlen' );
					} else {
						$values = array();
					}

					if ( ! empty( $values ) ) {
						// Add attribute to array, but don't set values.
						$attribute_object = new WC_Product_Attribute();
						$attribute_object->set_id( $attribute_id );
						$attribute_object->set_name( $attribute_name );
						$attribute_object->set_options( $values );
						$attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' );
						$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
						$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
						$attributes[] = $attribute_object;
					}
				} elseif ( isset( $attribute['options'] ) ) {
					// Custom attribute - Add attribute to array and set the values.
					if ( is_array( $attribute['options'] ) ) {
						$values = $attribute['options'];
					} else {
						$values = explode( WC_DELIMITER, $attribute['options'] );
					}
					$attribute_object = new WC_Product_Attribute();
					$attribute_object->set_name( $attribute_name );
					$attribute_object->set_options( $values );
					$attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' );
					$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
					$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
					$attributes[] = $attribute_object;
				}
			}
			$product->set_attributes( $attributes );
		}

		// Sales and prices.
		if ( in_array( $product->get_type(), array( 'variable', 'grouped' ), true ) ) {
			$product->set_regular_price( '' );
			$product->set_sale_price( '' );
			$product->set_date_on_sale_to( '' );
			$product->set_date_on_sale_from( '' );
			$product->set_price( '' );
		} else {
			// Regular Price.
			if ( isset( $request['regular_price'] ) ) {
				$product->set_regular_price( $request['regular_price'] );
			}

			// Sale Price.
			if ( isset( $request['sale_price'] ) ) {
				$product->set_sale_price( $request['sale_price'] );
			}

			if ( isset( $request['date_on_sale_from'] ) ) {
				$product->set_date_on_sale_from( $request['date_on_sale_from'] );
			}

			if ( isset( $request['date_on_sale_to'] ) ) {
				$product->set_date_on_sale_to( $request['date_on_sale_to'] );
			}
		}

		// Product parent ID for groups.
		if ( isset( $request['parent_id'] ) ) {
			$product->set_parent_id( $request['parent_id'] );
		}

		// Sold individually.
		if ( isset( $request['sold_individually'] ) ) {
			$product->set_sold_individually( $request['sold_individually'] );
		}

		// Stock status.
		if ( isset( $request['in_stock'] ) ) {
			$stock_status = true === $request['in_stock'] ? 'instock' : 'outofstock';
		} else {
			$stock_status = $product->get_stock_status();
		}

		// Stock data.
		if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
			// Manage stock.
			if ( isset( $request['manage_stock'] ) ) {
				$product->set_manage_stock( $request['manage_stock'] );
			}

			// Backorders.
			if ( isset( $request['backorders'] ) ) {
				$product->set_backorders( $request['backorders'] );
			}

			if ( $product->is_type( 'grouped' ) ) {
				$product->set_manage_stock( 'no' );
				$product->set_backorders( 'no' );
				$product->set_stock_quantity( '' );
				$product->set_stock_status( $stock_status );
			} elseif ( $product->is_type( 'external' ) ) {
				$product->set_manage_stock( 'no' );
				$product->set_backorders( 'no' );
				$product->set_stock_quantity( '' );
				$product->set_stock_status( 'instock' );
			} elseif ( $product->get_manage_stock() ) {
				// Stock status is always determined by children so sync later.
				if ( ! $product->is_type( 'variable' ) ) {
					$product->set_stock_status( $stock_status );
				}

				// Stock quantity.
				if ( isset( $request['stock_quantity'] ) ) {
					$product->set_stock_quantity( wc_stock_amount( $request['stock_quantity'] ) );
				} elseif ( isset( $request['inventory_delta'] ) ) {
					$stock_quantity  = wc_stock_amount( $product->get_stock_quantity() );
					$stock_quantity += wc_stock_amount( $request['inventory_delta'] );
					$product->set_stock_quantity( wc_stock_amount( $stock_quantity ) );
				}
			} else {
				// Don't manage stock.
				$product->set_manage_stock( 'no' );
				$product->set_stock_quantity( '' );
				$product->set_stock_status( $stock_status );
			}
		} elseif ( ! $product->is_type( 'variable' ) ) {
			$product->set_stock_status( $stock_status );
		}

		// Upsells.
		if ( isset( $request['upsell_ids'] ) ) {
			$upsells = array();
			$ids     = $request['upsell_ids'];

			if ( ! empty( $ids ) ) {
				foreach ( $ids as $id ) {
					if ( $id && $id > 0 ) {
						$upsells[] = $id;
					}
				}
			}

			$product->set_upsell_ids( $upsells );
		}

		// Cross sells.
		if ( isset( $request['cross_sell_ids'] ) ) {
			$crosssells = array();
			$ids        = $request['cross_sell_ids'];

			if ( ! empty( $ids ) ) {
				foreach ( $ids as $id ) {
					if ( $id && $id > 0 ) {
						$crosssells[] = $id;
					}
				}
			}

			$product->set_cross_sell_ids( $crosssells );
		}

		// Product categories.
		if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) {
			$product = $this->save_taxonomy_terms( $product, $request['categories'] );
		}

		// Product tags.
		if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) {
			$product = $this->save_taxonomy_terms( $product, $request['tags'], 'tag' );
		}

		// Downloadable.
		if ( isset( $request['downloadable'] ) ) {
			$product->set_downloadable( $request['downloadable'] );
		}

		// Downloadable options.
		if ( $product->get_downloadable() ) {

			// Downloadable files.
			if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) {
				$product = $this->save_downloadable_files( $product, $request['downloads'] );
			}

			// Download limit.
			if ( isset( $request['download_limit'] ) ) {
				$product->set_download_limit( $request['download_limit'] );
			}

			// Download expiry.
			if ( isset( $request['download_expiry'] ) ) {
				$product->set_download_expiry( $request['download_expiry'] );
			}
		}

		// Product url and button text for external products.
		if ( $product->is_type( 'external' ) ) {
			if ( isset( $request['external_url'] ) ) {
				$product->set_product_url( $request['external_url'] );
			}

			if ( isset( $request['button_text'] ) ) {
				$product->set_button_text( $request['button_text'] );
			}
		}

		// Save default attributes for variable products.
		if ( $product->is_type( 'variable' ) ) {
			$product = $this->save_default_attributes( $product, $request );
		}

		return $product;
	}

	/**
	 * Save variations.
	 *
	 * @throws WC_REST_Exception REST API exceptions.
	 * @param WC_Product      $product          Product instance.
	 * @param WP_REST_Request $request          Request data.
	 * @return bool
	 */
	protected function save_variations_data( $product, $request ) {
		foreach ( $request['variations'] as $menu_order => $data ) {
			$variation = new WC_Product_Variation( isset( $data['id'] ) ? absint( $data['id'] ) : 0 );

			// Create initial name and status.
			if ( ! $variation->get_slug() ) {
				/* translators: 1: variation id 2: product name */
				$variation->set_name( sprintf( __( 'Variation #%1$s of %2$s', 'woocommerce' ), $variation->get_id(), $product->get_name() ) );
				$variation->set_status( isset( $data['visible'] ) && false === $data['visible'] ? 'private' : 'publish' );
			}

			// Parent ID.
			$variation->set_parent_id( $product->get_id() );

			// Menu order.
			$variation->set_menu_order( $menu_order );

			// Status.
			if ( isset( $data['visible'] ) ) {
				$variation->set_status( false === $data['visible'] ? 'private' : 'publish' );
			}

			// SKU.
			if ( isset( $data['sku'] ) ) {
				$variation->set_sku( wc_clean( $data['sku'] ) );
			}

			// Thumbnail.
			if ( isset( $data['image'] ) && is_array( $data['image'] ) ) {
				$image = $data['image'];
				$image = current( $image );
				if ( is_array( $image ) ) {
					$image['position'] = 0;
				}

				$variation = $this->set_product_images( $variation, array( $image ) );
			}

			// Virtual variation.
			if ( isset( $data['virtual'] ) ) {
				$variation->set_virtual( $data['virtual'] );
			}

			// Downloadable variation.
			if ( isset( $data['downloadable'] ) ) {
				$variation->set_downloadable( $data['downloadable'] );
			}

			// Downloads.
			if ( $variation->get_downloadable() ) {
				// Downloadable files.
				if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) {
					$variation = $this->save_downloadable_files( $variation, $data['downloads'] );
				}

				// Download limit.
				if ( isset( $data['download_limit'] ) ) {
					$variation->set_download_limit( $data['download_limit'] );
				}

				// Download expiry.
				if ( isset( $data['download_expiry'] ) ) {
					$variation->set_download_expiry( $data['download_expiry'] );
				}
			}

			// Shipping data.
			$variation = $this->save_product_shipping_data( $variation, $data );

			// Stock handling.
			if ( isset( $data['manage_stock'] ) ) {
				$variation->set_manage_stock( $data['manage_stock'] );
			}

			if ( isset( $data['in_stock'] ) ) {
				$variation->set_stock_status( true === $data['in_stock'] ? 'instock' : 'outofstock' );
			}

			if ( isset( $data['backorders'] ) ) {
				$variation->set_backorders( $data['backorders'] );
			}

			if ( $variation->get_manage_stock() ) {
				if ( isset( $data['stock_quantity'] ) ) {
					$variation->set_stock_quantity( $data['stock_quantity'] );
				} elseif ( isset( $data['inventory_delta'] ) ) {
					$stock_quantity  = wc_stock_amount( $variation->get_stock_quantity() );
					$stock_quantity += wc_stock_amount( $data['inventory_delta'] );
					$variation->set_stock_quantity( $stock_quantity );
				}
			} else {
				$variation->set_backorders( 'no' );
				$variation->set_stock_quantity( '' );
			}

			// Regular Price.
			if ( isset( $data['regular_price'] ) ) {
				$variation->set_regular_price( $data['regular_price'] );
			}

			// Sale Price.
			if ( isset( $data['sale_price'] ) ) {
				$variation->set_sale_price( $data['sale_price'] );
			}

			if ( isset( $data['date_on_sale_from'] ) ) {
				$variation->set_date_on_sale_from( $data['date_on_sale_from'] );
			}

			if ( isset( $data['date_on_sale_to'] ) ) {
				$variation->set_date_on_sale_to( $data['date_on_sale_to'] );
			}

			// Tax class.
			if ( isset( $data['tax_class'] ) ) {
				$variation->set_tax_class( $data['tax_class'] );
			}

			// Description.
			if ( isset( $data['description'] ) ) {
				$variation->set_description( wp_kses_post( $data['description'] ) );
			}

			// Update taxonomies.
			if ( isset( $data['attributes'] ) ) {
				$attributes = array();
				$parent_attributes = $product->get_attributes();

				foreach ( $data['attributes'] as $attribute ) {
					$attribute_id   = 0;
					$attribute_name = '';

					// Check ID for global attributes or name for product attributes.
					if ( ! empty( $attribute['id'] ) ) {
						$attribute_id   = absint( $attribute['id'] );
						$attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
					} elseif ( ! empty( $attribute['name'] ) ) {
						$attribute_name = sanitize_title( $attribute['name'] );
					}

					if ( ! $attribute_id && ! $attribute_name ) {
						continue;
					}

					if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) {
						continue;
					}

					$attribute_key   = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() );
					$attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : '';

					if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) {
						// If dealing with a taxonomy, we need to get the slug from the name posted to the API.
						$term = get_term_by( 'name', $attribute_value, $attribute_name );

						if ( $term && ! is_wp_error( $term ) ) {
							$attribute_value = $term->slug;
						} else {
							$attribute_value = sanitize_title( $attribute_value );
						}
					}

					$attributes[ $attribute_key ] = $attribute_value;
				}

				$variation->set_attributes( $attributes );
			}

			$variation->save();

			do_action( 'woocommerce_rest_save_product_variation', $variation->get_id(), $menu_order, $data );
		}

		return true;
	}

	/**
	 * Add post meta fields.
	 *
	 * @param WP_Post         $post    Post data.
	 * @param WP_REST_Request $request Request data.
	 * @return bool|WP_Error
	 */
	protected function add_post_meta_fields( $post, $request ) {
		return $this->update_post_meta_fields( $post, $request );
	}

	/**
	 * Update post meta fields.
	 *
	 * @param WP_Post         $post    Post data.
	 * @param WP_REST_Request $request Request data.
	 * @return bool|WP_Error
	 */
	protected function update_post_meta_fields( $post, $request ) {
		$product = wc_get_product( $post );

		// Check for featured/gallery images, upload it and set it.
		if ( isset( $request['images'] ) ) {
			$product = $this->set_product_images( $product, $request['images'] );
		}

		// Save product meta fields.
		$product = $this->set_product_meta( $product, $request );

		// Save the product data.
		$product->save();

		// Save variations.
		if ( $product->is_type( 'variable' ) ) {
			if ( isset( $request['variations'] ) && is_array( $request['variations'] ) ) {
				$this->save_variations_data( $product, $request );
			}
		}

		// Clear caches here so in sync with any new variations/children.
		wc_delete_product_transients( $product->get_id() );
		wp_cache_delete( 'product-' . $product->get_id(), 'products' );

		return true;
	}

	/**
	 * Clear cache/transients.
	 *
	 * @param WP_Post $post Post data.
	 */
	public function clear_transients( $post ) {
		wc_delete_product_transients( $post->ID );
	}

	/**
	 * Delete post.
	 *
	 * @param int|WP_Post $id Post ID or WP_Post instance.
	 */
	protected function delete_post( $id ) {
		if ( ! empty( $id->ID ) ) {
			$id = $id->ID;
		} elseif ( ! is_numeric( $id ) || 0 >= $id ) {
			return;
		}

		// Delete product attachments.
		$attachments = get_posts( array(
			'post_parent' => $id,
			'post_status' => 'any',
			'post_type'   => 'attachment',
		) );

		foreach ( (array) $attachments as $attachment ) {
			wp_delete_attachment( $attachment->ID, true );
		}

		// Delete product.
		$product = wc_get_product( $id );
		$product->delete( true );
	}

	/**
	 * Delete a single item.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error
	 */
	public function delete_item( $request ) {
		$id      = (int) $request['id'];
		$force   = (bool) $request['force'];
		$post    = get_post( $id );
		$product = wc_get_product( $id );

		if ( ! empty( $post->post_type ) && 'product_variation' === $post->post_type && 'product' === $this->post_type ) {
			return new WP_Error( "woocommerce_rest_invalid_{$this->post_type}_id", __( 'To manipulate product variations you should use the /products/&lt;product_id&gt;/variations/&lt;id&gt; endpoint.', 'woocommerce' ), array( 'status' => 404 ) );
		} elseif ( empty( $id ) || empty( $post->ID ) || $post->post_type !== $this->post_type ) {
			return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid post ID.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$supports_trash = EMPTY_TRASH_DAYS > 0;

		/**
		 * Filter whether an item is trashable.
		 *
		 * Return false to disable trash support for the item.
		 *
		 * @param boolean $supports_trash Whether the item type support trashing.
		 * @param WP_Post $post           The Post object being considered for trashing support.
		 */
		$supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_trashable", $supports_trash, $post );

		if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $post->ID ) ) {
			/* translators: %s: post type */
			return new WP_Error( "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) );
		}

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_item_for_response( $post, $request );

		// If we're forcing, then delete permanently.
		if ( $force ) {
			if ( $product->is_type( 'variable' ) ) {
				foreach ( $product->get_children() as $child_id ) {
					$child = wc_get_product( $child_id );
					if ( ! empty( $child ) ) {
						$child->delete( true );
					}
				}
			} else {
				// For other product types, if the product has children, remove the relationship.
				foreach ( $product->get_children() as $child_id ) {
					$child = wc_get_product( $child_id );
					if ( ! empty( $child ) ) {
						$child->set_parent_id( 0 );
						$child->save();
					}
				}
			}

			$product->delete( true );
			$result = ! ( $product->get_id() > 0 );
		} else {
			// If we don't support trashing for this type, error out.
			if ( ! $supports_trash ) {
				/* translators: %s: post type */
				return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array( 'status' => 501 ) );
			}

			// Otherwise, only trash if we haven't already.
			if ( 'trash' === $post->post_status ) {
				/* translators: %s: post type */
				return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410 ) );
			}

			// (Note that internally this falls through to `wp_delete_post` if
			// the trash is disabled.)
			$product->delete();
			$result = 'trash' === $product->get_status();
		}

		if ( ! $result ) {
			/* translators: %s: post type */
			return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) );
		}

		// Delete parent product transients.
		if ( $parent_id = wp_get_post_parent_id( $id ) ) {
			wc_delete_product_transients( $parent_id );
		}

		/**
		 * Fires after a single item is deleted or trashed via the REST API.
		 *
		 * @param object           $post     The deleted or trashed item.
		 * @param WP_REST_Response $response The response data.
		 * @param WP_REST_Request  $request  The request sent to the API.
		 */
		do_action( "woocommerce_rest_delete_{$this->post_type}", $post, $response, $request );

		return $response;
	}

	/**
	 * Get the Product's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$weight_unit    = get_option( 'woocommerce_weight_unit' );
		$dimension_unit = get_option( 'woocommerce_dimension_unit' );
		$schema         = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => $this->post_type,
			'type'       => 'object',
			'properties' => array(
				'id' => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'name' => array(
					'description' => __( 'Product name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'slug' => array(
					'description' => __( 'Product slug.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'permalink' => array(
					'description' => __( 'Product URL.', 'woocommerce' ),
					'type'        => 'string',
					'format'      => 'uri',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_created' => array(
					'description' => __( "The date the product was created, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_modified' => array(
					'description' => __( "The date the product was last modified, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'type' => array(
					'description' => __( 'Product type.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'simple',
					'enum'        => array_keys( wc_get_product_types() ),
					'context'     => array( 'view', 'edit' ),
				),
				'status' => array(
					'description' => __( 'Product status (post status).', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'publish',
					'enum'        => array_merge( array_keys( get_post_statuses() ), array( 'future' ) ),
					'context'     => array( 'view', 'edit' ),
				),
				'featured' => array(
					'description' => __( 'Featured product.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'catalog_visibility' => array(
					'description' => __( 'Catalog visibility.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'visible',
					'enum'        => array( 'visible', 'catalog', 'search', 'hidden' ),
					'context'     => array( 'view', 'edit' ),
				),
				'description' => array(
					'description' => __( 'Product description.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'short_description' => array(
					'description' => __( 'Product short description.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'sku' => array(
					'description' => __( 'Unique identifier.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'price' => array(
					'description' => __( 'Current product price.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'regular_price' => array(
					'description' => __( 'Product regular price.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'sale_price' => array(
					'description' => __( 'Product sale price.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'date_on_sale_from' => array(
					'description' => __( 'Start date of sale price.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'date_on_sale_to' => array(
					'description' => __( 'End date of sale price.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'price_html' => array(
					'description' => __( 'Price formatted in HTML.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'on_sale' => array(
					'description' => __( 'Shows if the product is on sale.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'purchasable' => array(
					'description' => __( 'Shows if the product can be bought.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'total_sales' => array(
					'description' => __( 'Amount of sales.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'virtual' => array(
					'description' => __( 'If the product is virtual.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'downloadable' => array(
					'description' => __( 'If the product is downloadable.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'downloads' => array(
					'description' => __( 'List of downloadable files.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id' => array(
								'description' => __( 'File ID.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'name' => array(
								'description' => __( 'File name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'file' => array(
								'description' => __( 'File URL.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
						),
					),
				),
				'download_limit' => array(
					'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ),
					'type'        => 'integer',
					'default'     => -1,
					'context'     => array( 'view', 'edit' ),
				),
				'download_expiry' => array(
					'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ),
					'type'        => 'integer',
					'default'     => -1,
					'context'     => array( 'view', 'edit' ),
				),
				'download_type' => array(
					'description' => __( 'Download type, this controls the schema on the front-end.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'standard',
					'enum'        => array( 'standard' ),
					'context'     => array( 'view', 'edit' ),
				),
				'external_url' => array(
					'description' => __( 'Product external URL. Only for external products.', 'woocommerce' ),
					'type'        => 'string',
					'format'      => 'uri',
					'context'     => array( 'view', 'edit' ),
				),
				'button_text' => array(
					'description' => __( 'Product external button text. Only for external products.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'tax_status' => array(
					'description' => __( 'Tax status.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'taxable',
					'enum'        => array( 'taxable', 'shipping', 'none' ),
					'context'     => array( 'view', 'edit' ),
				),
				'tax_class' => array(
					'description' => __( 'Tax class.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'manage_stock' => array(
					'description' => __( 'Stock management at product level.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'stock_quantity' => array(
					'description' => __( 'Stock quantity.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'in_stock' => array(
					'description' => __( 'Controls whether or not the product is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => true,
					'context'     => array( 'view', 'edit' ),
				),
				'backorders' => array(
					'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'no',
					'enum'        => array( 'no', 'notify', 'yes' ),
					'context'     => array( 'view', 'edit' ),
				),
				'backorders_allowed' => array(
					'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'backordered' => array(
					'description' => __( 'Shows if the product is on backordered.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'sold_individually' => array(
					'description' => __( 'Allow one item to be bought in a single order.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'weight' => array(
					/* translators: %s: weight unit */
					'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), $weight_unit ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'dimensions' => array(
					'description' => __( 'Product dimensions.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'properties'  => array(
						'length' => array(
							/* translators: %s: dimension unit */
							'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), $dimension_unit ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'width' => array(
							/* translators: %s: dimension unit */
							'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), $dimension_unit ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'height' => array(
							/* translators: %s: dimension unit */
							'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), $dimension_unit ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
					),
				),
				'shipping_required' => array(
					'description' => __( 'Shows if the product need to be shipped.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'shipping_taxable' => array(
					'description' => __( 'Shows whether or not the product shipping is taxable.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'shipping_class' => array(
					'description' => __( 'Shipping class slug.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'shipping_class_id' => array(
					'description' => __( 'Shipping class ID.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'reviews_allowed' => array(
					'description' => __( 'Allow reviews.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => true,
					'context'     => array( 'view', 'edit' ),
				),
				'average_rating' => array(
					'description' => __( 'Reviews average rating.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'rating_count' => array(
					'description' => __( 'Amount of reviews that the product have.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'related_ids' => array(
					'description' => __( 'List of related products IDs.', 'woocommerce' ),
					'type'        => 'array',
					'items'       => array(
						'type'    => 'integer',
					),
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'upsell_ids' => array(
					'description' => __( 'List of upsell products IDs.', 'woocommerce' ),
					'type'        => 'array',
					'items'       => array(
						'type'    => 'integer',
					),
					'context'     => array( 'view', 'edit' ),
				),
				'cross_sell_ids' => array(
					'description' => __( 'List of cross-sell products IDs.', 'woocommerce' ),
					'type'        => 'array',
					'items'       => array(
						'type'    => 'integer',
					),
					'context'     => array( 'view', 'edit' ),
				),
				'parent_id' => array(
					'description' => __( 'Product parent ID.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'purchase_note' => array(
					'description' => __( 'Optional note to send the customer after purchase.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'categories' => array(
					'description' => __( 'List of categories.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id' => array(
								'description' => __( 'Category ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'name' => array(
								'description' => __( 'Category name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'slug' => array(
								'description' => __( 'Category slug.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
						),
					),
				),
				'tags' => array(
					'description' => __( 'List of tags.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id' => array(
								'description' => __( 'Tag ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'name' => array(
								'description' => __( 'Tag name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'slug' => array(
								'description' => __( 'Tag slug.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
						),
					),
				),
				'images' => array(
					'description' => __( 'List of images.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id' => array(
								'description' => __( 'Image ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'date_created' => array(
								'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_modified' => array(
								'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'src' => array(
								'description' => __( 'Image URL.', 'woocommerce' ),
								'type'        => 'string',
								'format'      => 'uri',
								'context'     => array( 'view', 'edit' ),
							),
							'name' => array(
								'description' => __( 'Image name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'alt' => array(
								'description' => __( 'Image alternative text.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'position' => array(
								'description' => __( 'Image position. 0 means that the image is featured.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
						),
					),
				),
				'attributes' => array(
					'description' => __( 'List of attributes.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id' => array(
								'description' => __( 'Attribute ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'name' => array(
								'description' => __( 'Attribute name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'position' => array(
								'description' => __( 'Attribute position.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'visible' => array(
								'description' => __( "Define if the attribute is visible on the \"Additional information\" tab in the product's page.", 'woocommerce' ),
								'type'        => 'boolean',
								'default'     => false,
								'context'     => array( 'view', 'edit' ),
							),
							'variation' => array(
								'description' => __( 'Define if the attribute can be used as variation.', 'woocommerce' ),
								'type'        => 'boolean',
								'default'     => false,
								'context'     => array( 'view', 'edit' ),
							),
							'options' => array(
								'description' => __( 'List of available term names of the attribute.', 'woocommerce' ),
								'type'        => 'array',
								'context'     => array( 'view', 'edit' ),
							),
						),
					),
				),
				'default_attributes' => array(
					'description' => __( 'Defaults variation attributes.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id' => array(
								'description' => __( 'Attribute ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'name' => array(
								'description' => __( 'Attribute name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'option' => array(
								'description' => __( 'Selected attribute term name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
						),
					),
				),
				'variations' => array(
					'description' => __( 'List of variations.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id' => array(
								'description' => __( 'Variation ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_created' => array(
								'description' => __( "The date the variation was created, in the site's timezone.", 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_modified' => array(
								'description' => __( "The date the variation was last modified, in the site's timezone.", 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'permalink' => array(
								'description' => __( 'Variation URL.', 'woocommerce' ),
								'type'        => 'string',
								'format'      => 'uri',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'sku' => array(
								'description' => __( 'Unique identifier.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'price' => array(
								'description' => __( 'Current variation price.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'regular_price' => array(
								'description' => __( 'Variation regular price.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'sale_price' => array(
								'description' => __( 'Variation sale price.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'date_on_sale_from' => array(
								'description' => __( 'Start date of sale price.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'date_on_sale_to' => array(
								'description' => __( 'End date of sale price.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'on_sale' => array(
								'description' => __( 'Shows if the variation is on sale.', 'woocommerce' ),
								'type'        => 'boolean',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'purchasable' => array(
								'description' => __( 'Shows if the variation can be bought.', 'woocommerce' ),
								'type'        => 'boolean',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'visible' => array(
								'description' => __( 'If the variation is visible.', 'woocommerce' ),
								'type'        => 'boolean',
								'context'     => array( 'view', 'edit' ),
							),
							'virtual' => array(
								'description' => __( 'If the variation is virtual.', 'woocommerce' ),
								'type'        => 'boolean',
								'default'     => false,
								'context'     => array( 'view', 'edit' ),
							),
							'downloadable' => array(
								'description' => __( 'If the variation is downloadable.', 'woocommerce' ),
								'type'        => 'boolean',
								'default'     => false,
								'context'     => array( 'view', 'edit' ),
							),
							'downloads' => array(
								'description' => __( 'List of downloadable files.', 'woocommerce' ),
								'type'        => 'array',
								'context'     => array( 'view', 'edit' ),
								'items'       => array(
									'type'       => 'object',
									'properties' => array(
										'id' => array(
											'description' => __( 'File ID.', 'woocommerce' ),
											'type'        => 'string',
											'context'     => array( 'view', 'edit' ),
										),
										'name' => array(
											'description' => __( 'File name.', 'woocommerce' ),
											'type'        => 'string',
											'context'     => array( 'view', 'edit' ),
										),
										'file' => array(
											'description' => __( 'File URL.', 'woocommerce' ),
											'type'        => 'string',
											'context'     => array( 'view', 'edit' ),
										),
									),
								),
							),
							'download_limit' => array(
								'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ),
								'type'        => 'integer',
								'default'     => null,
								'context'     => array( 'view', 'edit' ),
							),
							'download_expiry' => array(
								'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ),
								'type'        => 'integer',
								'default'     => null,
								'context'     => array( 'view', 'edit' ),
							),
							'tax_status' => array(
								'description' => __( 'Tax status.', 'woocommerce' ),
								'type'        => 'string',
								'default'     => 'taxable',
								'enum'        => array( 'taxable', 'shipping', 'none' ),
								'context'     => array( 'view', 'edit' ),
							),
							'tax_class' => array(
								'description' => __( 'Tax class.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'manage_stock' => array(
								'description' => __( 'Stock management at variation level.', 'woocommerce' ),
								'type'        => 'boolean',
								'default'     => false,
								'context'     => array( 'view', 'edit' ),
							),
							'stock_quantity' => array(
								'description' => __( 'Stock quantity.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'in_stock' => array(
								'description' => __( 'Controls whether or not the variation is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ),
								'type'        => 'boolean',
								'default'     => true,
								'context'     => array( 'view', 'edit' ),
							),
							'backorders' => array(
								'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ),
								'type'        => 'string',
								'default'     => 'no',
								'enum'        => array( 'no', 'notify', 'yes' ),
								'context'     => array( 'view', 'edit' ),
							),
							'backorders_allowed' => array(
								'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ),
								'type'        => 'boolean',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'backordered' => array(
								'description' => __( 'Shows if the variation is on backordered.', 'woocommerce' ),
								'type'        => 'boolean',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'weight' => array(
								/* translators: %s: weight unit */
								'description' => sprintf( __( 'Variation weight (%s).', 'woocommerce' ), $weight_unit ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'dimensions' => array(
								'description' => __( 'Variation dimensions.', 'woocommerce' ),
								'type'        => 'object',
								'context'     => array( 'view', 'edit' ),
								'properties'  => array(
									'length' => array(
										/* translators: %s: dimension unit */
										'description' => sprintf( __( 'Variation length (%s).', 'woocommerce' ), $dimension_unit ),
										'type'        => 'string',
										'context'     => array( 'view', 'edit' ),
									),
									'width' => array(
										/* translators: %s: dimension unit */
										'description' => sprintf( __( 'Variation width (%s).', 'woocommerce' ), $dimension_unit ),
										'type'        => 'string',
										'context'     => array( 'view', 'edit' ),
									),
									'height' => array(
										/* translators: %s: dimension unit */
										'description' => sprintf( __( 'Variation height (%s).', 'woocommerce' ), $dimension_unit ),
										'type'        => 'string',
										'context'     => array( 'view', 'edit' ),
									),
								),
							),
							'shipping_class' => array(
								'description' => __( 'Shipping class slug.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'shipping_class_id' => array(
								'description' => __( 'Shipping class ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'image' => array(
								'description' => __( 'Variation image data.', 'woocommerce' ),
								'type'        => 'object',
								'context'     => array( 'view', 'edit' ),
								'properties'  => array(
									'id' => array(
										'description' => __( 'Image ID.', 'woocommerce' ),
										'type'        => 'integer',
										'context'     => array( 'view', 'edit' ),
									),
									'date_created' => array(
										'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ),
										'type'        => 'date-time',
										'context'     => array( 'view', 'edit' ),
										'readonly'    => true,
									),
									'date_modified' => array(
										'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ),
										'type'        => 'date-time',
										'context'     => array( 'view', 'edit' ),
										'readonly'    => true,
									),
									'src' => array(
										'description' => __( 'Image URL.', 'woocommerce' ),
										'type'        => 'string',
										'format'      => 'uri',
										'context'     => array( 'view', 'edit' ),
									),
									'name' => array(
										'description' => __( 'Image name.', 'woocommerce' ),
										'type'        => 'string',
										'context'     => array( 'view', 'edit' ),
									),
									'alt' => array(
										'description' => __( 'Image alternative text.', 'woocommerce' ),
										'type'        => 'string',
										'context'     => array( 'view', 'edit' ),
									),
									'position' => array(
										'description' => __( 'Image position. 0 means that the image is featured.', 'woocommerce' ),
										'type'        => 'integer',
										'context'     => array( 'view', 'edit' ),
									),
								),
							),
							'attributes' => array(
								'description' => __( 'List of attributes.', 'woocommerce' ),
								'type'        => 'array',
								'context'     => array( 'view', 'edit' ),
								'items'       => array(
									'type'       => 'object',
									'properties' => array(
										'id' => array(
											'description' => __( 'Attribute ID.', 'woocommerce' ),
											'type'        => 'integer',
											'context'     => array( 'view', 'edit' ),
										),
										'name' => array(
											'description' => __( 'Attribute name.', 'woocommerce' ),
											'type'        => 'string',
											'context'     => array( 'view', 'edit' ),
										),
										'option' => array(
											'description' => __( 'Selected attribute term name.', 'woocommerce' ),
											'type'        => 'string',
											'context'     => array( 'view', 'edit' ),
										),
									),
								),
							),
						),
					),
				),
				'grouped_products' => array(
					'description' => __( 'List of grouped products ID.', 'woocommerce' ),
					'type'        => 'array',
					'items'       => array(
						'type'    => 'integer',
					),
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'menu_order' => array(
					'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections of attachments.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params = parent::get_collection_params();

		$params['slug'] = array(
			'description'       => __( 'Limit result set to products with a specific slug.', 'woocommerce' ),
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['status'] = array(
			'default'           => 'any',
			'description'       => __( 'Limit result set to products assigned a specific status.', 'woocommerce' ),
			'type'              => 'string',
			'enum'              => array_merge( array( 'any', 'future' ), array_keys( get_post_statuses() ) ),
			'sanitize_callback' => 'sanitize_key',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['type'] = array(
			'description'       => __( 'Limit result set to products assigned a specific type.', 'woocommerce' ),
			'type'              => 'string',
			'enum'              => array_keys( wc_get_product_types() ),
			'sanitize_callback' => 'sanitize_key',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['category'] = array(
			'description'       => __( 'Limit result set to products assigned a specific category ID.', 'woocommerce' ),
			'type'              => 'string',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['tag'] = array(
			'description'       => __( 'Limit result set to products assigned a specific tag ID.', 'woocommerce' ),
			'type'              => 'string',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['shipping_class'] = array(
			'description'       => __( 'Limit result set to products assigned a specific shipping class ID.', 'woocommerce' ),
			'type'              => 'string',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['attribute'] = array(
			'description'       => __( 'Limit result set to products with a specific attribute.', 'woocommerce' ),
			'type'              => 'string',
			'sanitize_callback' => 'sanitize_text_field',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['attribute_term'] = array(
			'description'       => __( 'Limit result set to products with a specific attribute term ID (required an assigned attribute).', 'woocommerce' ),
			'type'              => 'string',
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['sku'] = array(
			'description'       => __( 'Limit result set to products with a specific SKU.', 'woocommerce' ),
			'type'              => 'string',
			'sanitize_callback' => 'sanitize_text_field',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}
}
rest-api/Controllers/Version1/class-wc-rest-order-refunds-v1-controller.php000064400000042102151336044760022776 0ustar00<?php
/**
 * REST API Order Refunds controller
 *
 * Handles requests to the /orders/<order_id>/refunds endpoint.
 *
 * @author   WooThemes
 * @category API
 * @package WooCommerce\RestApi
 * @since    2.6.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * REST API Order Refunds controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Orders_V1_Controller
 */
class WC_REST_Order_Refunds_V1_Controller extends WC_REST_Orders_V1_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v1';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'orders/(?P<order_id>[\d]+)/refunds';

	/**
	 * Post type.
	 *
	 * @var string
	 */
	protected $post_type = 'shop_order_refund';

	/**
	 * Order refunds actions.
	 */
	public function __construct() {
		add_filter( "woocommerce_rest_{$this->post_type}_trashable", '__return_false' );
		add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 );
	}

	/**
	 * Register the routes for order refunds.
	 */
	public function register_routes() {
		register_rest_route( $this->namespace, '/' . $this->rest_base, array(
			'args' => array(
				'order_id'  => array(
					'description' => __( 'The order ID.', 'woocommerce' ),
					'type'        => 'integer',
				),
			),
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_items' ),
				'permission_callback' => array( $this, 'get_items_permissions_check' ),
				'args'                => $this->get_collection_params(),
			),
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'create_item' ),
				'permission_callback' => array( $this, 'create_item_permissions_check' ),
				'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
			),
			'schema' => array( $this, 'get_public_item_schema' ),
		) );

		register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
			'args' => array(
				'order_id'  => array(
					'description' => __( 'The order ID.', 'woocommerce' ),
					'type'        => 'integer',
				),
				'id' => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
				),
			),
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_item' ),
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
				'args'                => array(
					'context' => $this->get_context_param( array( 'default' => 'view' ) ),
				),
			),
			array(
				'methods'             => WP_REST_Server::DELETABLE,
				'callback'            => array( $this, 'delete_item' ),
				'permission_callback' => array( $this, 'delete_item_permissions_check' ),
				'args'                => array(
					'force' => array(
						'default'     => true,
						'type'        => 'boolean',
						'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
					),
				),
			),
			'schema' => array( $this, 'get_public_item_schema' ),
		) );
	}

	/**
	 * Prepare a single order refund output for response.
	 *
	 * @param WP_Post $post Post object.
	 * @param WP_REST_Request $request Request object.
	 *
	 * @return WP_Error|WP_REST_Response
	 */
	public function prepare_item_for_response( $post, $request ) {
		$order = wc_get_order( (int) $request['order_id'] );

		if ( ! $order ) {
			return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), 404 );
		}

		$refund = wc_get_order( $post );

		if ( ! $refund || $refund->get_parent_id() !== $order->get_id() ) {
			return new WP_Error( 'woocommerce_rest_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 404 );
		}

		$dp = is_null( $request['dp'] ) ? wc_get_price_decimals() : absint( $request['dp'] );

		$data = array(
			'id'           => $refund->get_id(),
			'date_created' => wc_rest_prepare_date_response( $refund->get_date_created() ),
			'amount'       => wc_format_decimal( $refund->get_amount(), $dp ),
			'reason'       => $refund->get_reason(),
			'line_items'   => array(),
		);

		// Add line items.
		foreach ( $refund->get_items() as $item_id => $item ) {
			$product      = $item->get_product();
			$product_id   = 0;
			$variation_id = 0;
			$product_sku  = null;

			// Check if the product exists.
			if ( is_object( $product ) ) {
				$product_id   = $item->get_product_id();
				$variation_id = $item->get_variation_id();
				$product_sku  = $product->get_sku();
			}

			$item_meta = array();

			$hideprefix = 'true' === $request['all_item_meta'] ? null : '_';

			foreach ( $item->get_formatted_meta_data( $hideprefix, true ) as $meta_key => $formatted_meta ) {
				$item_meta[] = array(
					'key'   => $formatted_meta->key,
					'label' => $formatted_meta->display_key,
					'value' => wc_clean( $formatted_meta->display_value ),
				);
			}

			$line_item = array(
				'id'           => $item_id,
				'name'         => $item['name'],
				'sku'          => $product_sku,
				'product_id'   => (int) $product_id,
				'variation_id' => (int) $variation_id,
				'quantity'     => wc_stock_amount( $item['qty'] ),
				'tax_class'    => ! empty( $item['tax_class'] ) ? $item['tax_class'] : '',
				'price'        => wc_format_decimal( $refund->get_item_total( $item, false, false ), $dp ),
				'subtotal'     => wc_format_decimal( $refund->get_line_subtotal( $item, false, false ), $dp ),
				'subtotal_tax' => wc_format_decimal( $item['line_subtotal_tax'], $dp ),
				'total'        => wc_format_decimal( $refund->get_line_total( $item, false, false ), $dp ),
				'total_tax'    => wc_format_decimal( $item['line_tax'], $dp ),
				'taxes'        => array(),
				'meta'         => $item_meta,
			);

			$item_line_taxes = maybe_unserialize( $item['line_tax_data'] );
			if ( isset( $item_line_taxes['total'] ) ) {
				$line_tax = array();

				foreach ( $item_line_taxes['total'] as $tax_rate_id => $tax ) {
					$line_tax[ $tax_rate_id ] = array(
						'id'       => $tax_rate_id,
						'total'    => $tax,
						'subtotal' => '',
					);
				}

				foreach ( $item_line_taxes['subtotal'] as $tax_rate_id => $tax ) {
					$line_tax[ $tax_rate_id ]['subtotal'] = $tax;
				}

				$line_item['taxes'] = array_values( $line_tax );
			}

			$data['line_items'][] = $line_item;
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $refund, $request ) );

		/**
		 * Filter the data for a response.
		 *
		 * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
		 * prepared for the response.
		 *
		 * @param WP_REST_Response   $response   The response object.
		 * @param WP_Post            $post       Post object.
		 * @param WP_REST_Request    $request    Request object.
		 */
		return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param WC_Order_Refund $refund Comment object.
	 * @param WP_REST_Request $request Request object.
	 * @return array Links for the given order refund.
	 */
	protected function prepare_links( $refund, $request ) {
		$order_id = $refund->get_parent_id();
		$base     = str_replace( '(?P<order_id>[\d]+)', $order_id, $this->rest_base );
		$links    = array(
			'self' => array(
				'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $refund->get_id() ) ),
			),
			'collection' => array(
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ),
			),
			'up' => array(
				'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order_id ) ),
			),
		);

		return $links;
	}

	/**
	 * Query args.
	 *
	 * @param array           $args    Request args.
	 * @param WP_REST_Request $request Request object.
	 * @return array
	 */
	public function query_args( $args, $request ) {
		$args['post_status']     = array_keys( wc_get_order_statuses() );
		$args['post_parent__in'] = array( absint( $request['order_id'] ) );

		return $args;
	}

	/**
	 * Create a single item.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function create_item( $request ) {
		if ( ! empty( $request['id'] ) ) {
			/* translators: %s: post type */
			return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) );
		}

		$order_data = get_post( (int) $request['order_id'] );

		if ( empty( $order_data ) ) {
			return new WP_Error( 'woocommerce_rest_invalid_order', __( 'Order is invalid', 'woocommerce' ), 400 );
		}

		if ( 0 > $request['amount'] ) {
			return new WP_Error( 'woocommerce_rest_invalid_order_refund', __( 'Refund amount must be greater than zero.', 'woocommerce' ), 400 );
		}

		// Create the refund.
		$refund = wc_create_refund( array(
			'order_id'       => $order_data->ID,
			'amount'         => $request['amount'],
			'reason'         => empty( $request['reason'] ) ? null : $request['reason'],
			'refund_payment' => is_bool( $request['api_refund'] ) ? $request['api_refund'] : true,
			'restock_items'  => true,
		) );

		if ( is_wp_error( $refund ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', $refund->get_error_message(), 500 );
		}

		if ( ! $refund ) {
			return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 );
		}

		$post = get_post( $refund->get_id() );
		$this->update_additional_fields_for_object( $post, $request );

		/**
		 * Fires after a single item is created or updated via the REST API.
		 *
		 * @param WP_Post         $post      Post object.
		 * @param WP_REST_Request $request   Request object.
		 * @param boolean         $creating  True when creating item, false when updating.
		 */
		do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true );

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_item_for_response( $post, $request );
		$response = rest_ensure_response( $response );
		$response->set_status( 201 );
		$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) );

		return $response;
	}

	/**
	 * Get the Order's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => $this->post_type,
			'type'       => 'object',
			'properties' => array(
				'id' => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_created' => array(
					'description' => __( "The date the order refund was created, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'amount' => array(
					'description' => __( 'Refund amount.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'reason' => array(
					'description' => __( 'Reason for refund.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'line_items' => array(
					'description' => __( 'Line items data.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id' => array(
								'description' => __( 'Item ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'name' => array(
								'description' => __( 'Product name.', 'woocommerce' ),
								'type'        => 'mixed',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'sku' => array(
								'description' => __( 'Product SKU.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'product_id' => array(
								'description' => __( 'Product ID.', 'woocommerce' ),
								'type'        => 'mixed',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'variation_id' => array(
								'description' => __( 'Variation ID, if applicable.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'quantity' => array(
								'description' => __( 'Quantity ordered.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'tax_class' => array(
								'description' => __( 'Tax class of product.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'price' => array(
								'description' => __( 'Product price.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'subtotal' => array(
								'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'subtotal_tax' => array(
								'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'total' => array(
								'description' => __( 'Line total (after discounts).', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'total_tax' => array(
								'description' => __( 'Line total tax (after discounts).', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'taxes' => array(
								'description' => __( 'Line taxes.', 'woocommerce' ),
								'type'        => 'array',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
								'items'       => array(
									'type'       => 'object',
									'properties' => array(
										'id' => array(
											'description' => __( 'Tax rate ID.', 'woocommerce' ),
											'type'        => 'integer',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
										'total' => array(
											'description' => __( 'Tax total.', 'woocommerce' ),
											'type'        => 'string',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
										'subtotal' => array(
											'description' => __( 'Tax subtotal.', 'woocommerce' ),
											'type'        => 'string',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
									),
								),
							),
							'meta' => array(
								'description' => __( 'Line item meta data.', 'woocommerce' ),
								'type'        => 'array',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
								'items'       => array(
									'type'       => 'object',
									'properties' => array(
										'key' => array(
											'description' => __( 'Meta key.', 'woocommerce' ),
											'type'        => 'string',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
										'label' => array(
											'description' => __( 'Meta label.', 'woocommerce' ),
											'type'        => 'string',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
										'value' => array(
											'description' => __( 'Meta value.', 'woocommerce' ),
											'type'        => 'mixed',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
									),
								),
							),
						),
					),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params = parent::get_collection_params();

		$params['dp'] = array(
			'default'           => wc_get_price_decimals(),
			'description'       => __( 'Number of decimal points to use in each resource.', 'woocommerce' ),
			'type'              => 'integer',
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}
}
rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php000064400000047475151336044760023403 0ustar00<?php
/**
 * REST API Product Reviews Controller
 *
 * Handles requests to /products/<product_id>/reviews.
 *
 * @author   WooThemes
 * @category API
 * @package WooCommerce\RestApi
 * @since    3.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * REST API Product Reviews Controller Class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Controller
 */
class WC_REST_Product_Reviews_V1_Controller extends WC_REST_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v1';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'products/(?P<product_id>[\d]+)/reviews';

	/**
	 * Register the routes for product reviews.
	 */
	public function register_routes() {
		register_rest_route( $this->namespace, '/' . $this->rest_base, array(
			'args' => array(
				'product_id' => array(
					'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ),
					'type'        => 'integer',
				),
				'id' => array(
					'description' => __( 'Unique identifier for the variation.', 'woocommerce' ),
					'type'        => 'integer',
				),
			),
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_items' ),
				'permission_callback' => array( $this, 'get_items_permissions_check' ),
				'args'                => $this->get_collection_params(),
			),
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'create_item' ),
				'permission_callback' => array( $this, 'create_item_permissions_check' ),
				'args'                => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array(
					'review' => array(
						'required'    => true,
						'type'        => 'string',
						'description' => __( 'Review content.', 'woocommerce' ),
					),
					'name' => array(
						'required'    => true,
						'type'        => 'string',
						'description' => __( 'Name of the reviewer.', 'woocommerce' ),
					),
					'email' => array(
						'required'    => true,
						'type'        => 'string',
						'description' => __( 'Email of the reviewer.', 'woocommerce' ),
					),
				) ),
			),
			'schema' => array( $this, 'get_public_item_schema' ),
		) );

		register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
			'args' => array(
				'product_id' => array(
					'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ),
					'type'        => 'integer',
				),
				'id' => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
				),
			),
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_item' ),
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
				'args'                => array(
					'context' => $this->get_context_param( array( 'default' => 'view' ) ),
				),
			),
			array(
				'methods'             => WP_REST_Server::EDITABLE,
				'callback'            => array( $this, 'update_item' ),
				'permission_callback' => array( $this, 'update_item_permissions_check' ),
				'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
			),
			array(
				'methods'             => WP_REST_Server::DELETABLE,
				'callback'            => array( $this, 'delete_item' ),
				'permission_callback' => array( $this, 'delete_item_permissions_check' ),
				'args'                => array(
					'force' => array(
						'default'     => false,
						'type'        => 'boolean',
						'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ),
					),
				),
			),
			'schema' => array( $this, 'get_public_item_schema' ),
		) );
	}

	/**
	 * Check whether a given request has permission to read webhook deliveries.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_items_permissions_check( $request ) {
		if ( ! wc_rest_check_post_permissions( 'product', 'read' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access to read a product review.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_item_permissions_check( $request ) {
		$post = get_post( (int) $request['product_id'] );

		if ( $post && ! wc_rest_check_post_permissions( 'product', 'read', $post->ID ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access to create a new product review.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function create_item_permissions_check( $request ) {
		$post = get_post( (int) $request['product_id'] );
		if ( $post && ! wc_rest_check_post_permissions( 'product', 'create', $post->ID ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}
		return true;
	}

	/**
	 * Check if a given request has access to update a product review.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function update_item_permissions_check( $request ) {
		$post = get_post( (int) $request['product_id'] );
		if ( $post && ! wc_rest_check_post_permissions( 'product', 'edit', $post->ID ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}
		return true;
	}

	/**
	 * Check if a given request has access to delete a product review.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function delete_item_permissions_check( $request ) {
		$post = get_post( (int) $request['product_id'] );
		if ( $post && ! wc_rest_check_post_permissions( 'product', 'delete', $post->ID ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}
		return true;
	}

	/**
	 * Get all reviews from a product.
	 *
	 * @param WP_REST_Request $request
	 *
	 * @return array|WP_Error
	 */
	public function get_items( $request ) {
		$product_id = (int) $request['product_id'];

		if ( 'product' !== get_post_type( $product_id ) ) {
			return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$reviews = get_approved_comments( $product_id );
		$data    = array();
		foreach ( $reviews as $review_data ) {
			$review = $this->prepare_item_for_response( $review_data, $request );
			$review = $this->prepare_response_for_collection( $review );
			$data[] = $review;
		}

		return rest_ensure_response( $data );
	}

	/**
	 * Get a single product review.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_item( $request ) {
		$id         = (int) $request['id'];
		$product_id = (int) $request['product_id'];

		if ( 'product' !== get_post_type( $product_id ) ) {
			return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$review = get_comment( $id );

		if ( empty( $id ) || empty( $review ) || intval( $review->comment_post_ID ) !== $product_id ) {
			return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$delivery = $this->prepare_item_for_response( $review, $request );
		$response = rest_ensure_response( $delivery );

		return $response;
	}


	/**
	 * Create a product review.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function create_item( $request ) {
		$product_id = (int) $request['product_id'];

		if ( 'product' !== get_post_type( $product_id ) ) {
			return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$prepared_review = $this->prepare_item_for_database( $request );

		/**
		 * Filter a product review (comment) before it is inserted via the REST API.
		 *
		 * Allows modification of the comment right before it is inserted via `wp_insert_comment`.
		 *
		 * @param array           $prepared_review The prepared comment data for `wp_insert_comment`.
		 * @param WP_REST_Request $request          Request used to insert the comment.
		 */
		$prepared_review = apply_filters( 'rest_pre_insert_product_review', $prepared_review, $request );

		$product_review_id = wp_insert_comment( $prepared_review );
		if ( ! $product_review_id ) {
			return new WP_Error( 'rest_product_review_failed_create', __( 'Creating product review failed.', 'woocommerce' ), array( 'status' => 500 ) );
		}

		update_comment_meta( $product_review_id, 'rating', ( ! empty( $request['rating'] ) ? $request['rating'] : '0' ) );

		$product_review = get_comment( $product_review_id );
		$this->update_additional_fields_for_object( $product_review, $request );

		/**
		 * Fires after a single item is created or updated via the REST API.
		 *
		 * @param WP_Comment      $product_review Inserted object.
		 * @param WP_REST_Request $request        Request object.
		 * @param boolean         $creating       True when creating item, false when updating.
		 */
		do_action( "woocommerce_rest_insert_product_review", $product_review, $request, true );

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_item_for_response( $product_review, $request );
		$response = rest_ensure_response( $response );
		$response->set_status( 201 );
		$base = str_replace( '(?P<product_id>[\d]+)', $product_id, $this->rest_base );
		$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $product_review_id ) ) );

		return $response;
	}

	/**
	 * Update a single product review.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function update_item( $request ) {
		$product_review_id = (int) $request['id'];
		$product_id        = (int) $request['product_id'];

		if ( 'product' !== get_post_type( $product_id ) ) {
			return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$review = get_comment( $product_review_id );

		if ( empty( $product_review_id ) || empty( $review ) || intval( $review->comment_post_ID ) !== $product_id ) {
			return new WP_Error( 'woocommerce_rest_product_review_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$prepared_review = $this->prepare_item_for_database( $request );

		$updated = wp_update_comment( $prepared_review );
		if ( 0 === $updated ) {
			return new WP_Error( 'rest_product_review_failed_edit', __( 'Updating product review failed.', 'woocommerce' ), array( 'status' => 500 ) );
		}

		if ( ! empty( $request['rating'] ) ) {
			update_comment_meta( $product_review_id, 'rating', $request['rating'] );
		}

		$product_review = get_comment( $product_review_id );
		$this->update_additional_fields_for_object( $product_review, $request );

		/**
		 * Fires after a single item is created or updated via the REST API.
		 *
		 * @param WP_Comment         $comment      Inserted object.
		 * @param WP_REST_Request $request   Request object.
		 * @param boolean         $creating  True when creating item, false when updating.
		 */
		do_action( "woocommerce_rest_insert_product_review", $product_review, $request, true );

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_item_for_response( $product_review, $request );

		return rest_ensure_response( $response );
	}

	/**
	 * Delete a product review.
	 *
	 * @param WP_REST_Request $request Full details about the request
	 *
	 * @return bool|WP_Error|WP_REST_Response
	 */
	public function delete_item( $request ) {
		$product_review_id = absint( is_array( $request['id'] ) ? $request['id']['id'] : $request['id'] );
		$force             = isset( $request['force'] ) ? (bool) $request['force']     : false;

		$product_review = get_comment( $product_review_id );
		if ( empty( $product_review_id ) || empty( $product_review->comment_ID ) || empty( $product_review->comment_post_ID ) ) {
			return new WP_Error( 'woocommerce_rest_product_review_invalid_id', __( 'Invalid product review ID.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		/**
		 * Filter whether a product review is trashable.
		 *
		 * Return false to disable trash support for the product review.
		 *
		 * @param boolean $supports_trash        Whether the object supports trashing.
		 * @param WP_Post $product_review        The object being considered for trashing support.
		 */
		$supports_trash = apply_filters( 'rest_product_review_trashable', ( EMPTY_TRASH_DAYS > 0 ), $product_review );

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_item_for_response( $product_review, $request );

		if ( $force ) {
			$result = wp_delete_comment( $product_review_id, true );
		} else {
			if ( ! $supports_trash ) {
				return new WP_Error( 'rest_trash_not_supported', __( 'The product review does not support trashing.', 'woocommerce' ), array( 'status' => 501 ) );
			}

			if ( 'trash' === $product_review->comment_approved ) {
				return new WP_Error( 'rest_already_trashed', __( 'The comment has already been trashed.', 'woocommerce' ), array( 'status' => 410 ) );
			}

			$result = wp_trash_comment( $product_review->comment_ID );
		}

		if ( ! $result ) {
			return new WP_Error( 'rest_cannot_delete', __( 'The product review cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) );
		}

		/**
		 * Fires after a product review is deleted via the REST API.
		 *
		 * @param object           $product_review  The deleted item.
		 * @param WP_REST_Response $response        The response data.
		 * @param WP_REST_Request  $request         The request sent to the API.
		 */
		do_action( 'rest_delete_product_review', $product_review, $response, $request );

		return $response;
	}

	/**
	 * Prepare a single product review output for response.
	 *
	 * @param WP_Comment $review Product review object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $review, $request ) {
		$data = array(
			'id'           => (int) $review->comment_ID,
			'date_created' => wc_rest_prepare_date_response( $review->comment_date_gmt ),
			'review'       => $review->comment_content,
			'rating'       => (int) get_comment_meta( $review->comment_ID, 'rating', true ),
			'name'         => $review->comment_author,
			'email'        => $review->comment_author_email,
			'verified'     => wc_review_is_from_verified_owner( $review->comment_ID ),
		);

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $review, $request ) );

		/**
		 * Filter product reviews object returned from the REST API.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param WP_Comment       $review   Product review object used to create response.
		 * @param WP_REST_Request  $request  Request object.
		 */
		return apply_filters( 'woocommerce_rest_prepare_product_review', $response, $review, $request );
	}

	/**
	 * Prepare a single product review to be inserted into the database.
	 *
	 * @param  WP_REST_Request $request Request object.
	 * @return array|WP_Error  $prepared_review
	 */
	protected function prepare_item_for_database( $request ) {
		$prepared_review = array( 'comment_approved' => 1, 'comment_type' => 'review' );

		if ( isset( $request['id'] ) ) {
			$prepared_review['comment_ID'] = (int) $request['id'];
		}

		if ( isset( $request['review'] ) ) {
			$prepared_review['comment_content'] = $request['review'];
		}

		if ( isset( $request['product_id'] ) ) {
			$prepared_review['comment_post_ID'] = (int) $request['product_id'];
		}

		if ( isset( $request['name'] ) ) {
			$prepared_review['comment_author'] = $request['name'];
		}

		if ( isset( $request['email'] ) ) {
			$prepared_review['comment_author_email'] = $request['email'];
		}

		if ( isset( $request['date_created'] ) ) {
			$prepared_review['comment_date'] = $request['date_created'];
		}

		if ( isset( $request['date_created_gmt'] ) ) {
			$prepared_review['comment_date_gmt'] = $request['date_created_gmt'];
		}

		return apply_filters( 'rest_preprocess_product_review', $prepared_review, $request );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param WP_Comment $review Product review object.
	 * @param WP_REST_Request $request Request object.
	 * @return array Links for the given product review.
	 */
	protected function prepare_links( $review, $request ) {
		$product_id = (int) $request['product_id'];
		$base       = str_replace( '(?P<product_id>[\d]+)', $product_id, $this->rest_base );
		$links      = array(
			'self' => array(
				'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $review->comment_ID ) ),
			),
			'collection' => array(
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ),
			),
			'up' => array(
				'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product_id ) ),
			),
		);

		return $links;
	}

	/**
	 * Get the Product Review's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'product_review',
			'type'       => 'object',
			'properties' => array(
				'id' => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'review' => array(
					'description' => __( 'The content of the review.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'date_created' => array(
					'description' => __( "The date the review was created, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'rating' => array(
					'description' => __( 'Review rating (0 to 5).', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'name' => array(
					'description' => __( 'Reviewer name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'email' => array(
					'description' => __( 'Reviewer email.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'verified' => array(
					'description' => __( 'Shows if the reviewer bought the product or not.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		return array(
			'context' => $this->get_context_param( array( 'default' => 'view' ) ),
		);
	}
}
rest-api/Controllers/Version1/class-wc-rest-customers-v1-controller.php000064400000076003151336044760022252 0ustar00<?php
/**
 * REST API Customers controller
 *
 * Handles requests to the /customers endpoint.
 *
 * @author   WooThemes
 * @category API
 * @package WooCommerce\RestApi
 * @since    3.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * REST API Customers controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Controller
 */
class WC_REST_Customers_V1_Controller extends WC_REST_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v1';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'customers';

	/**
	 * Register the routes for customers.
	 */
	public function register_routes() {
		register_rest_route( $this->namespace, '/' . $this->rest_base, array(
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_items' ),
				'permission_callback' => array( $this, 'get_items_permissions_check' ),
				'args'                => $this->get_collection_params(),
			),
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'create_item' ),
				'permission_callback' => array( $this, 'create_item_permissions_check' ),
				'args'                => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array(
					'email' => array(
						'required' => true,
						'type'     => 'string',
						'description' => __( 'New user email address.', 'woocommerce' ),
					),
					'username' => array(
						'required' => 'no' === get_option( 'woocommerce_registration_generate_username', 'yes' ),
						'description' => __( 'New user username.', 'woocommerce' ),
						'type'     => 'string',
					),
					'password' => array(
						'required' => 'no' === get_option( 'woocommerce_registration_generate_password', 'no' ),
						'description' => __( 'New user password.', 'woocommerce' ),
						'type'     => 'string',
					),
				) ),
			),
			'schema' => array( $this, 'get_public_item_schema' ),
		) );

		register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
			'args' => array(
				'id' => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
				),
			),
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_item' ),
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
				'args'                => array(
					'context' => $this->get_context_param( array( 'default' => 'view' ) ),
				),
			),
			array(
				'methods'             => WP_REST_Server::EDITABLE,
				'callback'            => array( $this, 'update_item' ),
				'permission_callback' => array( $this, 'update_item_permissions_check' ),
				'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
			),
			array(
				'methods'             => WP_REST_Server::DELETABLE,
				'callback'            => array( $this, 'delete_item' ),
				'permission_callback' => array( $this, 'delete_item_permissions_check' ),
				'args'                => array(
					'force' => array(
						'default'     => false,
						'type'        => 'boolean',
						'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
					),
					'reassign' => array(
						'default'     => 0,
						'type'        => 'integer',
						'description' => __( 'ID to reassign posts to.', 'woocommerce' ),
					),
				),
			),
			'schema' => array( $this, 'get_public_item_schema' ),
		) );

		register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array(
			array(
				'methods'             => WP_REST_Server::EDITABLE,
				'callback'            => array( $this, 'batch_items' ),
				'permission_callback' => array( $this, 'batch_items_permissions_check' ),
				'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
			),
			'schema' => array( $this, 'get_public_batch_schema' ),
		) );
	}

	/**
	 * Check whether a given request has permission to read customers.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_items_permissions_check( $request ) {
		if ( ! wc_rest_check_user_permissions( 'read' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access create customers.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 *
	 * @return bool|WP_Error
	 */
	public function create_item_permissions_check( $request ) {
		if ( ! wc_rest_check_user_permissions( 'create' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access to read a customer.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_item_permissions_check( $request ) {
		$id = (int) $request['id'];

		if ( ! wc_rest_check_user_permissions( 'read', $id ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access update a customer.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 *
	 * @return bool|WP_Error
	 */
	public function update_item_permissions_check( $request ) {
		$id = (int) $request['id'];

		if ( ! wc_rest_check_user_permissions( 'edit', $id ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access delete a customer.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 *
	 * @return bool|WP_Error
	 */
	public function delete_item_permissions_check( $request ) {
		$id = (int) $request['id'];

		if ( ! wc_rest_check_user_permissions( 'delete', $id ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access batch create, update and delete items.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 *
	 * @return bool|WP_Error
	 */
	public function batch_items_permissions_check( $request ) {
		if ( ! wc_rest_check_user_permissions( 'batch' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Get all customers.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_items( $request ) {
		$prepared_args = array();
		$prepared_args['exclude'] = $request['exclude'];
		$prepared_args['include'] = $request['include'];
		$prepared_args['order']   = $request['order'];
		$prepared_args['number']  = $request['per_page'];
		if ( ! empty( $request['offset'] ) ) {
			$prepared_args['offset'] = $request['offset'];
		} else {
			$prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number'];
		}
		$orderby_possibles = array(
			'id'              => 'ID',
			'include'         => 'include',
			'name'            => 'display_name',
			'registered_date' => 'registered',
		);
		$prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ];
		$prepared_args['search']  = $request['search'];

		if ( '' !== $prepared_args['search'] ) {
			$prepared_args['search'] = '*' . $prepared_args['search'] . '*';
		}

		// Filter by email.
		if ( ! empty( $request['email'] ) ) {
			$prepared_args['search']         = $request['email'];
			$prepared_args['search_columns'] = array( 'user_email' );
		}

		// Filter by role.
		if ( 'all' !== $request['role'] ) {
			$prepared_args['role'] = $request['role'];
		}

		/**
		 * Filter arguments, before passing to WP_User_Query, when querying users via the REST API.
		 *
		 * @see https://developer.wordpress.org/reference/classes/wp_user_query/
		 *
		 * @param array           $prepared_args Array of arguments for WP_User_Query.
		 * @param WP_REST_Request $request       The current request.
		 */
		$prepared_args = apply_filters( 'woocommerce_rest_customer_query', $prepared_args, $request );

		$query = new WP_User_Query( $prepared_args );

		$users = array();
		foreach ( $query->results as $user ) {
			$data = $this->prepare_item_for_response( $user, $request );
			$users[] = $this->prepare_response_for_collection( $data );
		}

		$response = rest_ensure_response( $users );

		// Store pagination values for headers then unset for count query.
		$per_page = (int) $prepared_args['number'];
		$page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 );

		$prepared_args['fields'] = 'ID';

		$total_users = $query->get_total();
		if ( $total_users < 1 ) {
			// Out-of-bounds, run the query again without LIMIT for total count.
			unset( $prepared_args['number'] );
			unset( $prepared_args['offset'] );
			$count_query = new WP_User_Query( $prepared_args );
			$total_users = $count_query->get_total();
		}
		$response->header( 'X-WP-Total', (int) $total_users );
		$max_pages = ceil( $total_users / $per_page );
		$response->header( 'X-WP-TotalPages', (int) $max_pages );

		$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
		if ( $page > 1 ) {
			$prev_page = $page - 1;
			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}
			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}
		if ( $max_pages > $page ) {
			$next_page = $page + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );
			$response->link_header( 'next', $next_link );
		}

		return $response;
	}

	/**
	 * Create a single customer.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function create_item( $request ) {
		try {
			if ( ! empty( $request['id'] ) ) {
				throw new WC_REST_Exception( 'woocommerce_rest_customer_exists', __( 'Cannot create existing resource.', 'woocommerce' ), 400 );
			}

			// Sets the username.
			$request['username'] = ! empty( $request['username'] ) ? $request['username'] : '';

			// Sets the password.
			$request['password'] = ! empty( $request['password'] ) ? $request['password'] : '';

			// Create customer.
			$customer = new WC_Customer;
			$customer->set_username( $request['username'] );
			$customer->set_password( $request['password'] );
			$customer->set_email( $request['email'] );
			$this->update_customer_meta_fields( $customer, $request );
			$customer->save();

			if ( ! $customer->get_id() ) {
				throw new WC_REST_Exception( 'woocommerce_rest_cannot_create', __( 'This resource cannot be created.', 'woocommerce' ), 400 );
			}

			$user_data = get_userdata( $customer->get_id() );
			$this->update_additional_fields_for_object( $user_data, $request );

			/**
			 * Fires after a customer is created or updated via the REST API.
			 *
			 * @param WP_User         $user_data Data used to create the customer.
			 * @param WP_REST_Request $request   Request object.
			 * @param boolean         $creating  True when creating customer, false when updating customer.
			 */
			do_action( 'woocommerce_rest_insert_customer', $user_data, $request, true );

			$request->set_param( 'context', 'edit' );
			$response = $this->prepare_item_for_response( $user_data, $request );
			$response = rest_ensure_response( $response );
			$response->set_status( 201 );
			$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $customer->get_id() ) ) );

			return $response;
		} catch ( Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get a single customer.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_item( $request ) {
		$id        = (int) $request['id'];
		$user_data = get_userdata( $id );

		if ( empty( $id ) || empty( $user_data->ID ) ) {
			return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$customer = $this->prepare_item_for_response( $user_data, $request );
		$response = rest_ensure_response( $customer );

		return $response;
	}

	/**
	 * Update a single user.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function update_item( $request ) {
		try {
			$id       = (int) $request['id'];
			$customer = new WC_Customer( $id );

			if ( ! $customer->get_id() ) {
				throw new WC_REST_Exception( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), 400 );
			}

			if ( ! empty( $request['email'] ) && email_exists( $request['email'] ) && $request['email'] !== $customer->get_email() ) {
				throw new WC_REST_Exception( 'woocommerce_rest_customer_invalid_email', __( 'Email address is invalid.', 'woocommerce' ), 400 );
			}

			if ( ! empty( $request['username'] ) && $request['username'] !== $customer->get_username() ) {
				throw new WC_REST_Exception( 'woocommerce_rest_customer_invalid_argument', __( "Username isn't editable.", 'woocommerce' ), 400 );
			}

			// Customer email.
			if ( isset( $request['email'] ) ) {
				$customer->set_email( sanitize_email( $request['email'] ) );
			}

			// Customer password.
			if ( isset( $request['password'] ) ) {
				$customer->set_password( $request['password'] );
			}

			$this->update_customer_meta_fields( $customer, $request );
			$customer->save();

			$user_data = get_userdata( $customer->get_id() );
			$this->update_additional_fields_for_object( $user_data, $request );

			if ( ! is_user_member_of_blog( $user_data->ID ) ) {
				$user_data->add_role( 'customer' );
			}

			/**
			 * Fires after a customer is created or updated via the REST API.
			 *
			 * @param WP_User         $customer  Data used to create the customer.
			 * @param WP_REST_Request $request   Request object.
			 * @param boolean         $creating  True when creating customer, false when updating customer.
			 */
			do_action( 'woocommerce_rest_insert_customer', $user_data, $request, false );

			$request->set_param( 'context', 'edit' );
			$response = $this->prepare_item_for_response( $user_data, $request );
			$response = rest_ensure_response( $response );
			return $response;
		} catch ( Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete a single customer.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function delete_item( $request ) {
		$id       = (int) $request['id'];
		$reassign = isset( $request['reassign'] ) ? absint( $request['reassign'] ) : null;
		$force    = isset( $request['force'] ) ? (bool) $request['force'] : false;

		// We don't support trashing for this type, error out.
		if ( ! $force ) {
			return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Customers do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) );
		}

		$user_data = get_userdata( $id );
		if ( ! $user_data ) {
			return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) );
		}

		if ( ! empty( $reassign ) ) {
			if ( $reassign === $id || ! get_userdata( $reassign ) ) {
				return new WP_Error( 'woocommerce_rest_customer_invalid_reassign', __( 'Invalid resource id for reassignment.', 'woocommerce' ), array( 'status' => 400 ) );
			}
		}

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_item_for_response( $user_data, $request );

		/** Include admin customer functions to get access to wp_delete_user() */
		require_once ABSPATH . 'wp-admin/includes/user.php';

		$customer = new WC_Customer( $id );

		if ( ! is_null( $reassign ) ) {
			$result = $customer->delete_and_reassign( $reassign );
		} else {
			$result = $customer->delete();
		}

		if ( ! $result ) {
			return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) );
		}

		/**
		 * Fires after a customer is deleted via the REST API.
		 *
		 * @param WP_User          $user_data User data.
		 * @param WP_REST_Response $response  The response returned from the API.
		 * @param WP_REST_Request  $request   The request sent to the API.
		 */
		do_action( 'woocommerce_rest_delete_customer', $user_data, $response, $request );

		return $response;
	}

	/**
	 * Prepare a single customer output for response.
	 *
	 * @param  WP_User          $user_data User object.
	 * @param  WP_REST_Request  $request Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $user_data, $request ) {
		$customer    = new WC_Customer( $user_data->ID );
		$_data       = $customer->get_data();
		$last_order  = wc_get_customer_last_order( $customer->get_id() );
		$format_date = array( 'date_created', 'date_modified' );

		// Format date values.
		foreach ( $format_date as $key ) {
			$_data[ $key ] = $_data[ $key ] ? wc_rest_prepare_date_response( $_data[ $key ] ) : null; // v1 API used UTC.
		}

		$data = array(
			'id'            => $_data['id'],
			'date_created'  => $_data['date_created'],
			'date_modified' => $_data['date_modified'],
			'email'         => $_data['email'],
			'first_name'    => $_data['first_name'],
			'last_name'     => $_data['last_name'],
			'username'      => $_data['username'],
			'last_order'    => array(
				'id'   => is_object( $last_order ) ? $last_order->get_id() : null,
				'date' => is_object( $last_order ) ? wc_rest_prepare_date_response( $last_order->get_date_created() ) : null, // v1 API used UTC.
			),
			'orders_count'  => $customer->get_order_count(),
			'total_spent'   => $customer->get_total_spent(),
			'avatar_url'    => $customer->get_avatar_url(),
			'billing'       => $_data['billing'],
			'shipping'      => $_data['shipping'],
		);

		$context  = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data     = $this->add_additional_fields_to_object( $data, $request );
		$data     = $this->filter_response_by_context( $data, $context );
		$response = rest_ensure_response( $data );
		$response->add_links( $this->prepare_links( $user_data ) );

		/**
		 * Filter customer data returned from the REST API.
		 *
		 * @param WP_REST_Response $response   The response object.
		 * @param WP_User          $user_data  User object used to create response.
		 * @param WP_REST_Request  $request    Request object.
		 */
		return apply_filters( 'woocommerce_rest_prepare_customer', $response, $user_data, $request );
	}

	/**
	 * Update customer meta fields.
	 *
	 * @param WC_Customer $customer
	 * @param WP_REST_Request $request
	 */
	protected function update_customer_meta_fields( $customer, $request ) {
		$schema = $this->get_item_schema();

		// Customer first name.
		if ( isset( $request['first_name'] ) ) {
			$customer->set_first_name( wc_clean( $request['first_name'] ) );
		}

		// Customer last name.
		if ( isset( $request['last_name'] ) ) {
			$customer->set_last_name( wc_clean( $request['last_name'] ) );
		}

		// Customer billing address.
		if ( isset( $request['billing'] ) ) {
			foreach ( array_keys( $schema['properties']['billing']['properties'] ) as $field ) {
				if ( isset( $request['billing'][ $field ] ) && is_callable( array( $customer, "set_billing_{$field}" ) ) ) {
					$customer->{"set_billing_{$field}"}( $request['billing'][ $field ] );
				}
			}
		}

		// Customer shipping address.
		if ( isset( $request['shipping'] ) ) {
			foreach ( array_keys( $schema['properties']['shipping']['properties'] ) as $field ) {
				if ( isset( $request['shipping'][ $field ] ) && is_callable( array( $customer, "set_shipping_{$field}" ) ) ) {
					$customer->{"set_shipping_{$field}"}( $request['shipping'][ $field ] );
				}
			}
		}
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param WP_User $customer Customer object.
	 * @return array Links for the given customer.
	 */
	protected function prepare_links( $customer ) {
		$links = array(
			'self' => array(
				'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $customer->ID ) ),
			),
			'collection' => array(
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
			),
		);

		return $links;
	}

	/**
	 * Get the Customer's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'customer',
			'type'       => 'object',
			'properties' => array(
				'id' => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_created' => array(
					'description' => __( 'The date the customer was created, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_modified' => array(
					'description' => __( 'The date the customer was last modified, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'email' => array(
					'description' => __( 'The email address for the customer.', 'woocommerce' ),
					'type'        => 'string',
					'format'      => 'email',
					'context'     => array( 'view', 'edit' ),
				),
				'first_name' => array(
					'description' => __( 'Customer first name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				'last_name' => array(
					'description' => __( 'Customer last name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				'username' => array(
					'description' => __( 'Customer login name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_user',
					),
				),
				'password' => array(
					'description' => __( 'Customer password.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'edit' ),
				),
				'last_order' => array(
					'description' => __( 'Last order data.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
					'properties'  => array(
						'id' => array(
							'description' => __( 'Last order ID.', 'woocommerce' ),
							'type'        => 'integer',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
						'date' => array(
							'description' => __( 'The date of the customer last order, as GMT.', 'woocommerce' ),
							'type'        => 'date-time',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
					),
				),
				'orders_count' => array(
					'description' => __( 'Quantity of orders made by the customer.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'total_spent' => array(
					'description' => __( 'Total amount spent.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'avatar_url' => array(
					'description' => __( 'Avatar URL.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'billing' => array(
					'description' => __( 'List of billing address data.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'properties' => array(
						'first_name' => array(
							'description' => __( 'First name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'last_name' => array(
							'description' => __( 'Last name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'company' => array(
							'description' => __( 'Company name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'address_1' => array(
							'description' => __( 'Address line 1.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'address_2' => array(
							'description' => __( 'Address line 2.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'city' => array(
							'description' => __( 'City name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'state' => array(
							'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'postcode' => array(
							'description' => __( 'Postal code.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'country' => array(
							'description' => __( 'ISO code of the country.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'email' => array(
							'description' => __( 'Email address.', 'woocommerce' ),
							'type'        => 'string',
							'format'      => 'email',
							'context'     => array( 'view', 'edit' ),
						),
						'phone' => array(
							'description' => __( 'Phone number.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
					),
				),
				'shipping' => array(
					'description' => __( 'List of shipping address data.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'properties' => array(
						'first_name' => array(
							'description' => __( 'First name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'last_name' => array(
							'description' => __( 'Last name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'company' => array(
							'description' => __( 'Company name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'address_1' => array(
							'description' => __( 'Address line 1.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'address_2' => array(
							'description' => __( 'Address line 2.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'city' => array(
							'description' => __( 'City name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'state' => array(
							'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'postcode' => array(
							'description' => __( 'Postal code.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'country' => array(
							'description' => __( 'ISO code of the country.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
					),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get role names.
	 *
	 * @return array
	 */
	protected function get_role_names() {
		global $wp_roles;

		return array_keys( $wp_roles->role_names );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params = parent::get_collection_params();

		$params['context']['default'] = 'view';

		$params['exclude'] = array(
			'description'       => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type'          => 'integer',
			),
			'default'           => array(),
			'sanitize_callback' => 'wp_parse_id_list',
		);
		$params['include'] = array(
			'description'       => __( 'Limit result set to specific IDs.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type'          => 'integer',
			),
			'default'           => array(),
			'sanitize_callback' => 'wp_parse_id_list',
		);
		$params['offset'] = array(
			'description'        => __( 'Offset the result set by a specific number of items.', 'woocommerce' ),
			'type'               => 'integer',
			'sanitize_callback'  => 'absint',
			'validate_callback'  => 'rest_validate_request_arg',
		);
		$params['order'] = array(
			'default'            => 'asc',
			'description'        => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
			'enum'               => array( 'asc', 'desc' ),
			'sanitize_callback'  => 'sanitize_key',
			'type'               => 'string',
			'validate_callback'  => 'rest_validate_request_arg',
		);
		$params['orderby'] = array(
			'default'            => 'name',
			'description'        => __( 'Sort collection by object attribute.', 'woocommerce' ),
			'enum'               => array(
				'id',
				'include',
				'name',
				'registered_date',
			),
			'sanitize_callback'  => 'sanitize_key',
			'type'               => 'string',
			'validate_callback'  => 'rest_validate_request_arg',
		);
		$params['email'] = array(
			'description'        => __( 'Limit result set to resources with a specific email.', 'woocommerce' ),
			'type'               => 'string',
			'format'             => 'email',
			'validate_callback'  => 'rest_validate_request_arg',
		);
		$params['role'] = array(
			'description'        => __( 'Limit result set to resources with a specific role.', 'woocommerce' ),
			'type'               => 'string',
			'default'            => 'customer',
			'enum'               => array_merge( array( 'all' ), $this->get_role_names() ),
			'validate_callback'  => 'rest_validate_request_arg',
		);
		return $params;
	}
}
rest-api/Controllers/Version1/class-wc-rest-coupons-v1-controller.php000064400000047153151336044760021720 0ustar00<?php
/**
 * REST API Coupons controller
 *
 * Handles requests to the /coupons endpoint.
 *
 * @author   WooThemes
 * @category API
 * @package WooCommerce\RestApi
 * @since    3.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * REST API Coupons controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Posts_Controller
 */
class WC_REST_Coupons_V1_Controller extends WC_REST_Posts_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v1';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'coupons';

	/**
	 * Post type.
	 *
	 * @var string
	 */
	protected $post_type = 'shop_coupon';

	/**
	 * Coupons actions.
	 */
	public function __construct() {
		add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 );
	}

	/**
	 * Register the routes for coupons.
	 */
	public function register_routes() {
		register_rest_route( $this->namespace, '/' . $this->rest_base, array(
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_items' ),
				'permission_callback' => array( $this, 'get_items_permissions_check' ),
				'args'                => $this->get_collection_params(),
			),
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'create_item' ),
				'permission_callback' => array( $this, 'create_item_permissions_check' ),
				'args'                => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array(
					'code' => array(
						'description' => __( 'Coupon code.', 'woocommerce' ),
						'required'    => true,
						'type'        => 'string',
					),
				) ),
			),
			'schema' => array( $this, 'get_public_item_schema' ),
		) );

		register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
			'args' => array(
				'id' => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
				),
			),
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_item' ),
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
				'args'                => array(
					'context'         => $this->get_context_param( array( 'default' => 'view' ) ),
				),
			),
			array(
				'methods'         => WP_REST_Server::EDITABLE,
				'callback'        => array( $this, 'update_item' ),
				'permission_callback' => array( $this, 'update_item_permissions_check' ),
				'args'            => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
			),
			array(
				'methods'             => WP_REST_Server::DELETABLE,
				'callback'            => array( $this, 'delete_item' ),
				'permission_callback' => array( $this, 'delete_item_permissions_check' ),
				'args'                => array(
					'force' => array(
						'default'     => false,
						'type'        => 'boolean',
						'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ),
					),
				),
			),
			'schema' => array( $this, 'get_public_item_schema' ),
		) );

		register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array(
			array(
				'methods'             => WP_REST_Server::EDITABLE,
				'callback'            => array( $this, 'batch_items' ),
				'permission_callback' => array( $this, 'batch_items_permissions_check' ),
				'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
			),
			'schema' => array( $this, 'get_public_batch_schema' ),
		) );
	}

	/**
	 * Query args.
	 *
	 * @param array $args Query args
	 * @param WP_REST_Request $request Request data.
	 * @return array
	 */
	public function query_args( $args, $request ) {
		if ( ! empty( $request['code'] ) ) {
			$id = wc_get_coupon_id_by_code( $request['code'] );
			$args['post__in'] = array( $id );
		}

		return $args;
	}

	/**
	 * Prepare a single coupon output for response.
	 *
	 * @param WP_Post $post Post object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $data
	 */
	public function prepare_item_for_response( $post, $request ) {
		$coupon = new WC_Coupon( (int) $post->ID );
		$_data  = $coupon->get_data();

		$format_decimal  = array( 'amount', 'minimum_amount', 'maximum_amount' );
		$format_date     = array( 'date_created', 'date_modified' );
		$format_date_utc = array( 'date_expires' );
		$format_null     = array( 'usage_limit', 'usage_limit_per_user' );

		// Format decimal values.
		foreach ( $format_decimal as $key ) {
			$_data[ $key ] = wc_format_decimal( $_data[ $key ], 2 );
		}

		// Format date values.
		foreach ( $format_date as $key ) {
			$_data[ $key ] = $_data[ $key ] ? wc_rest_prepare_date_response( $_data[ $key ], false ) : null;
		}
		foreach ( $format_date_utc as $key ) {
			$_data[ $key ] = $_data[ $key ] ? wc_rest_prepare_date_response( $_data[ $key ] ) : null;
		}

		// Format null values.
		foreach ( $format_null as $key ) {
			$_data[ $key ] = $_data[ $key ] ? $_data[ $key ] : null;
		}

		$data = array(
			'id'                          => $_data['id'],
			'code'                        => $_data['code'],
			'date_created'                => $_data['date_created'],
			'date_modified'               => $_data['date_modified'],
			'discount_type'               => $_data['discount_type'],
			'description'                 => $_data['description'],
			'amount'                      => $_data['amount'],
			'expiry_date'                 => $_data['date_expires'],
			'usage_count'                 => $_data['usage_count'],
			'individual_use'              => $_data['individual_use'],
			'product_ids'                 => $_data['product_ids'],
			'exclude_product_ids'         => $_data['excluded_product_ids'],
			'usage_limit'                 => $_data['usage_limit'],
			'usage_limit_per_user'        => $_data['usage_limit_per_user'],
			'limit_usage_to_x_items'      => $_data['limit_usage_to_x_items'],
			'free_shipping'               => $_data['free_shipping'],
			'product_categories'          => $_data['product_categories'],
			'excluded_product_categories' => $_data['excluded_product_categories'],
			'exclude_sale_items'          => $_data['exclude_sale_items'],
			'minimum_amount'              => $_data['minimum_amount'],
			'maximum_amount'              => $_data['maximum_amount'],
			'email_restrictions'          => $_data['email_restrictions'],
			'used_by'                     => $_data['used_by'],
		);

		$context  = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data     = $this->add_additional_fields_to_object( $data, $request );
		$data     = $this->filter_response_by_context( $data, $context );
		$response = rest_ensure_response( $data );
		$response->add_links( $this->prepare_links( $post, $request ) );

		/**
		 * Filter the data for a response.
		 *
		 * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
		 * prepared for the response.
		 *
		 * @param WP_REST_Response   $response   The response object.
		 * @param WP_Post            $post       Post object.
		 * @param WP_REST_Request    $request    Request object.
		 */
		return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request );
	}

	/**
	 * Only return writable props from schema.
	 * @param  array $schema
	 * @return bool
	 */
	protected function filter_writable_props( $schema ) {
		return empty( $schema['readonly'] );
	}

	/**
	 * Prepare a single coupon for create or update.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_Error|stdClass $data Post object.
	 */
	protected function prepare_item_for_database( $request ) {
		$id        = isset( $request['id'] ) ? absint( $request['id'] ) : 0;
		$coupon    = new WC_Coupon( $id );
		$schema    = $this->get_item_schema();
		$data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) );

		// Update to schema to make compatible with CRUD schema.
		if ( $request['exclude_product_ids'] ) {
			$request['excluded_product_ids'] = $request['exclude_product_ids'];
		}
		if ( $request['expiry_date'] ) {
			$request['date_expires'] = $request['expiry_date'];
		}

		// Validate required POST fields.
		if ( 'POST' === $request->get_method() && 0 === $coupon->get_id() ) {
			if ( empty( $request['code'] ) ) {
				return new WP_Error( 'woocommerce_rest_empty_coupon_code', sprintf( __( 'The coupon code cannot be empty.', 'woocommerce' ), 'code' ), array( 'status' => 400 ) );
			}
		}

		// Handle all writable props.
		foreach ( $data_keys as $key ) {
			$value = $request[ $key ];

			if ( ! is_null( $value ) ) {
				switch ( $key ) {
					case 'code' :
						$coupon_code = wc_format_coupon_code( $value );
						$id          = $coupon->get_id() ? $coupon->get_id() : 0;
						$id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id );

						if ( $id_from_code ) {
							return new WP_Error( 'woocommerce_rest_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), array( 'status' => 400 ) );
						}

						$coupon->set_code( $coupon_code );
						break;
					case 'description' :
						$coupon->set_description( wp_filter_post_kses( $value ) );
						break;
					case 'expiry_date' :
						$coupon->set_date_expires( $value );
						break;
					default :
						if ( is_callable( array( $coupon, "set_{$key}" ) ) ) {
							$coupon->{"set_{$key}"}( $value );
						}
						break;
				}
			}
		}

		/**
		 * Filter the query_vars used in `get_items` for the constructed query.
		 *
		 * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
		 * prepared for insertion.
		 *
		 * @param WC_Coupon       $coupon        The coupon object.
		 * @param WP_REST_Request $request       Request object.
		 */
		return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $coupon, $request );
	}

	/**
	 * Create a single item.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function create_item( $request ) {
		if ( ! empty( $request['id'] ) ) {
			/* translators: %s: post type */
			return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) );
		}

		$coupon_id = $this->save_coupon( $request );
		if ( is_wp_error( $coupon_id ) ) {
			return $coupon_id;
		}

		$post = get_post( $coupon_id );
		$this->update_additional_fields_for_object( $post, $request );

		$this->add_post_meta_fields( $post, $request );

		/**
		 * Fires after a single item is created or updated via the REST API.
		 *
		 * @param WP_Post         $post      Post object.
		 * @param WP_REST_Request $request   Request object.
		 * @param boolean         $creating  True when creating item, false when updating.
		 */
		do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true );
		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_item_for_response( $post, $request );
		$response = rest_ensure_response( $response );
		$response->set_status( 201 );
		$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) );

		return $response;
	}

	/**
	 * Update a single coupon.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function update_item( $request ) {
		try {
			$post_id = (int) $request['id'];

			if ( empty( $post_id ) || get_post_type( $post_id ) !== $this->post_type ) {
				return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) );
			}

			$coupon_id = $this->save_coupon( $request );
			if ( is_wp_error( $coupon_id ) ) {
				return $coupon_id;
			}

			$post = get_post( $coupon_id );
			$this->update_additional_fields_for_object( $post, $request );

			/**
			 * Fires after a single item is created or updated via the REST API.
			 *
			 * @param WP_Post         $post      Post object.
			 * @param WP_REST_Request $request   Request object.
			 * @param boolean         $creating  True when creating item, false when updating.
			 */
			do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, false );
			$request->set_param( 'context', 'edit' );
			$response = $this->prepare_item_for_response( $post, $request );
			return rest_ensure_response( $response );

		} catch ( Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Saves a coupon to the database.
	 *
	 * @since 3.0.0
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|int
	 */
	protected function save_coupon( $request ) {
		try {
			$coupon = $this->prepare_item_for_database( $request );

			if ( is_wp_error( $coupon ) ) {
				return $coupon;
			}

			$coupon->save();
			return $coupon->get_id();
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
		} catch ( WC_REST_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the Coupon's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => $this->post_type,
			'type'       => 'object',
			'properties' => array(
				'id' => array(
					'description' => __( 'Unique identifier for the object.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'code' => array(
					'description' => __( 'Coupon code.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'date_created' => array(
					'description' => __( "The date the coupon was created, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_modified' => array(
					'description' => __( "The date the coupon was last modified, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'description' => array(
					'description' => __( 'Coupon description.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'discount_type' => array(
					'description' => __( 'Determines the type of discount that will be applied.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'fixed_cart',
					'enum'        => array_keys( wc_get_coupon_types() ),
					'context'     => array( 'view', 'edit' ),
				),
				'amount' => array(
					'description' => __( 'The amount of discount. Should always be numeric, even if setting a percentage.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'expiry_date' => array(
					'description' => __( 'UTC DateTime when the coupon expires.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'usage_count' => array(
					'description' => __( 'Number of times the coupon has been used already.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'individual_use' => array(
					'description' => __( 'If true, the coupon can only be used individually. Other applied coupons will be removed from the cart.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'product_ids' => array(
					'description' => __( "List of product IDs the coupon can be used on.", 'woocommerce' ),
					'type'        => 'array',
					'items'       => array(
						'type'    => 'integer',
					),
					'context'     => array( 'view', 'edit' ),
				),
				'exclude_product_ids' => array(
					'description' => __( "List of product IDs the coupon cannot be used on.", 'woocommerce' ),
					'type'        => 'array',
					'items'       => array(
						'type'    => 'integer',
					),
					'context'     => array( 'view', 'edit' ),
				),
				'usage_limit' => array(
					'description' => __( 'How many times the coupon can be used in total.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'usage_limit_per_user' => array(
					'description' => __( 'How many times the coupon can be used per customer.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'limit_usage_to_x_items' => array(
					'description' => __( 'Max number of items in the cart the coupon can be applied to.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'free_shipping' => array(
					'description' => __( 'If true and if the free shipping method requires a coupon, this coupon will enable free shipping.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'product_categories' => array(
					'description' => __( "List of category IDs the coupon applies to.", 'woocommerce' ),
					'type'        => 'array',
					'items'       => array(
						'type'    => 'integer',
					),
					'context'     => array( 'view', 'edit' ),
				),
				'excluded_product_categories' => array(
					'description' => __( "List of category IDs the coupon does not apply to.", 'woocommerce' ),
					'type'        => 'array',
					'items'       => array(
						'type'    => 'integer',
					),
					'context'     => array( 'view', 'edit' ),
				),
				'exclude_sale_items' => array(
					'description' => __( 'If true, this coupon will not be applied to items that have sale prices.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'minimum_amount' => array(
					'description' => __( 'Minimum order amount that needs to be in the cart before coupon applies.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'maximum_amount' => array(
					'description' => __( 'Maximum order amount allowed when using the coupon.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'email_restrictions' => array(
					'description' => __( 'List of email addresses that can use this coupon.', 'woocommerce' ),
					'type'        => 'array',
					'items'       => array(
						'type'    => 'string',
					),
					'context'     => array( 'view', 'edit' ),
				),
				'used_by' => array(
					'description' => __( 'List of user IDs (or guest email addresses) that have used the coupon.', 'woocommerce' ),
					'type'        => 'array',
					'items'       => array(
						'type'    => 'integer',
					),
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections of attachments.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params = parent::get_collection_params();

		$params['code'] = array(
			'description'       => __( 'Limit result set to resources with a specific code.', 'woocommerce' ),
			'type'              => 'string',
			'sanitize_callback' => 'sanitize_text_field',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}
}
rest-api/Controllers/Version1/class-wc-rest-webhooks-v1-controller.php000064400000063356151336044760022056 0ustar00<?php
/**
 * REST API Webhooks controller
 *
 * Handles requests to the /webhooks endpoint.
 *
 * @package WooCommerce\RestApi
 * @since    3.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * REST API Webhooks controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Controller
 */
class WC_REST_Webhooks_V1_Controller extends WC_REST_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v1';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'webhooks';

	/**
	 * Post type.
	 *
	 * @var string
	 */
	protected $post_type = 'shop_webhook';

	/**
	 * Register the routes for webhooks.
	 */
	public function register_routes() {
		register_rest_route( $this->namespace, '/' . $this->rest_base, array(
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_items' ),
				'permission_callback' => array( $this, 'get_items_permissions_check' ),
				'args'                => $this->get_collection_params(),
			),
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'create_item' ),
				'permission_callback' => array( $this, 'create_item_permissions_check' ),
				'args'                => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array(
					'topic' => array(
						'required'    => true,
						'type'        => 'string',
						'description' => __( 'Webhook topic.', 'woocommerce' ),
					),
					'delivery_url' => array(
						'required'    => true,
						'type'        => 'string',
						'description' => __( 'Webhook delivery URL.', 'woocommerce' ),
					),
				) ),
			),
			'schema' => array( $this, 'get_public_item_schema' ),
		) );

		register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
			'args' => array(
				'id' => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
				),
			),
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_item' ),
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
				'args'                => array(
					'context' => $this->get_context_param( array( 'default' => 'view' ) ),
				),
			),
			array(
				'methods'             => WP_REST_Server::EDITABLE,
				'callback'            => array( $this, 'update_item' ),
				'permission_callback' => array( $this, 'update_item_permissions_check' ),
				'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
			),
			array(
				'methods'             => WP_REST_Server::DELETABLE,
				'callback'            => array( $this, 'delete_item' ),
				'permission_callback' => array( $this, 'delete_item_permissions_check' ),
				'args'                => array(
					'force' => array(
						'default'     => false,
						'type'        => 'boolean',
						'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
					),
				),
			),
			'schema' => array( $this, 'get_public_item_schema' ),
		) );

		register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array(
			array(
				'methods'             => WP_REST_Server::EDITABLE,
				'callback'            => array( $this, 'batch_items' ),
				'permission_callback' => array( $this, 'batch_items_permissions_check' ),
				'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
			),
			'schema' => array( $this, 'get_public_batch_schema' ),
		) );
	}

	/**
	 * Check whether a given request has permission to read webhooks.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_items_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'webhooks', 'read' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access create webhooks.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 *
	 * @return bool|WP_Error
	 */
	public function create_item_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'webhooks', 'create' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access to read a webhook.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_item_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'webhooks', 'read' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access update a webhook.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 *
	 * @return bool|WP_Error
	 */
	public function update_item_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'webhooks', 'edit' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access delete a webhook.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 *
	 * @return bool|WP_Error
	 */
	public function delete_item_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'webhooks', 'delete' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access batch create, update and delete items.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 *
	 * @return bool|WP_Error
	 */
	public function batch_items_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'webhooks', 'batch' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Get the default REST API version.
	 *
	 * @since  3.0.0
	 * @return string
	 */
	protected function get_default_api_version() {
		return 'wp_api_v1';
	}

	/**
	 * Get all webhooks.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_items( $request ) {
		$args            = array();
		$args['order']   = $request['order'];
		$args['orderby'] = $request['orderby'];
		$args['status']  = 'all' === $request['status'] ? '' : $request['status'];
		$args['include'] = implode( ',', $request['include'] );
		$args['exclude'] = implode( ',', $request['exclude'] );
		$args['limit']   = $request['per_page'];
		$args['search']  = $request['search'];
		$args['before']  = $request['before'];
		$args['after']   = $request['after'];

		if ( empty( $request['offset'] ) ) {
			$args['offset'] = 1 < $request['page'] ? ( $request['page'] - 1 ) * $args['limit'] : 0;
		}

		/**
		 * Filter arguments, before passing to WC_Webhook_Data_Store->search_webhooks, when querying webhooks via the REST API.
		 *
		 * @param array           $args    Array of arguments for $wpdb->get_results().
		 * @param WP_REST_Request $request The current request.
		 */
		$prepared_args = apply_filters( 'woocommerce_rest_webhook_query', $args, $request );
		unset( $prepared_args['page'] );
		$prepared_args['paginate'] = true;

		// Get the webhooks.
		$webhooks       = array();
		$data_store     = WC_Data_Store::load( 'webhook' );
		$results        = $data_store->search_webhooks( $prepared_args );
		$webhook_ids    = $results->webhooks;

		foreach ( $webhook_ids as $webhook_id ) {
			$data = $this->prepare_item_for_response( $webhook_id, $request );
			$webhooks[] = $this->prepare_response_for_collection( $data );
		}

		$response       = rest_ensure_response( $webhooks );
		$per_page       = (int) $prepared_args['limit'];
		$page           = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 );
		$total_webhooks = $results->total;
		$max_pages      = $results->max_num_pages;
		$base           = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );

		$response->header( 'X-WP-Total', $total_webhooks );
		$response->header( 'X-WP-TotalPages', $max_pages );

		if ( $page > 1 ) {
			$prev_page = $page - 1;
			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}
			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}
		if ( $max_pages > $page ) {
			$next_page = $page + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );
			$response->link_header( 'next', $next_link );
		}

		return $response;
	}

	/**
	 * Get a single item.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_item( $request ) {
		$id = (int) $request['id'];

		if ( empty( $id ) ) {
			return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$data     = $this->prepare_item_for_response( $id, $request );
		$response = rest_ensure_response( $data );

		return $response;
	}

	/**
	 * Create a single webhook.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function create_item( $request ) {
		if ( ! empty( $request['id'] ) ) {
			/* translators: %s: post type */
			return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) );
		}

		// Validate topic.
		if ( empty( $request['topic'] ) || ! wc_is_webhook_valid_topic( strtolower( $request['topic'] ) ) ) {
			return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_topic", __( 'Webhook topic is required and must be valid.', 'woocommerce' ), array( 'status' => 400 ) );
		}

		// Validate delivery URL.
		if ( empty( $request['delivery_url'] ) || ! wc_is_valid_url( $request['delivery_url'] ) ) {
			return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_delivery_url", __( 'Webhook delivery URL must be a valid URL starting with http:// or https://.', 'woocommerce' ), array( 'status' => 400 ) );
		}

		$post = $this->prepare_item_for_database( $request );
		if ( is_wp_error( $post ) ) {
			return $post;
		}

		$webhook = new WC_Webhook();
		$webhook->set_name( $post->post_title );
		$webhook->set_user_id( $post->post_author );
		$webhook->set_status( 'publish' === $post->post_status ? 'active' : 'disabled' );
		$webhook->set_topic( $request['topic'] );
		$webhook->set_delivery_url( $request['delivery_url'] );
		$webhook->set_secret( ! empty( $request['secret'] ) ? $request['secret'] : wp_generate_password( 50, true, true ) );
		$webhook->set_api_version( $this->get_default_api_version() );
		$webhook->save();

		$this->update_additional_fields_for_object( $webhook, $request );

		/**
		 * Fires after a single item is created or updated via the REST API.
		 *
		 * @param WC_Webhook      $webhook  Webhook data.
		 * @param WP_REST_Request $request  Request object.
		 * @param bool            $creating True when creating item, false when updating.
		 */
		do_action( "woocommerce_rest_insert_webhook_object", $webhook, $request, true );

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_item_for_response( $webhook->get_id(), $request );
		$response = rest_ensure_response( $response );
		$response->set_status( 201 );
		$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $webhook->get_id() ) ) );

		// Send ping.
		$webhook->deliver_ping();

		return $response;
	}

	/**
	 * Update a single webhook.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function update_item( $request ) {
		$id      = (int) $request['id'];
		$webhook = wc_get_webhook( $id );

		if ( empty( $webhook ) || is_null( $webhook ) ) {
			return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) );
		}

		// Update topic.
		if ( ! empty( $request['topic'] ) ) {
			if ( wc_is_webhook_valid_topic( strtolower( $request['topic'] ) ) ) {
				$webhook->set_topic( $request['topic'] );
			} else {
				return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_topic", __( 'Webhook topic must be valid.', 'woocommerce' ), array( 'status' => 400 ) );
			}
		}

		// Update delivery URL.
		if ( ! empty( $request['delivery_url'] ) ) {
			if ( wc_is_valid_url( $request['delivery_url'] ) ) {
				$webhook->set_delivery_url( $request['delivery_url'] );
			} else {
				return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_delivery_url", __( 'Webhook delivery URL must be a valid URL starting with http:// or https://.', 'woocommerce' ), array( 'status' => 400 ) );
			}
		}

		// Update secret.
		if ( ! empty( $request['secret'] ) ) {
			$webhook->set_secret( $request['secret'] );
		}

		// Update status.
		if ( ! empty( $request['status'] ) ) {
			if ( wc_is_webhook_valid_status( strtolower( $request['status'] ) ) ) {
				$webhook->set_status( $request['status'] );
			} else {
				return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_status", __( 'Webhook status must be valid.', 'woocommerce' ), array( 'status' => 400 ) );
			}
		}

		$post = $this->prepare_item_for_database( $request );
		if ( is_wp_error( $post ) ) {
			return $post;
		}

		if ( isset( $post->post_title ) ) {
			$webhook->set_name( $post->post_title );
		}

		$webhook->save();

		$this->update_additional_fields_for_object( $webhook, $request );

		/**
		 * Fires after a single item is created or updated via the REST API.
		 *
		 * @param WC_Webhook      $webhook  Webhook data.
		 * @param WP_REST_Request $request  Request object.
		 * @param bool            $creating True when creating item, false when updating.
		 */
		do_action( "woocommerce_rest_insert_webhook_object", $webhook, $request, false );

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_item_for_response( $webhook->get_id(), $request );

		return rest_ensure_response( $response );
	}

	/**
	 * Delete a single webhook.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error
	 */
	public function delete_item( $request ) {
		$id    = (int) $request['id'];
		$force = isset( $request['force'] ) ? (bool) $request['force'] : false;

		// We don't support trashing for this type, error out.
		if ( ! $force ) {
			return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Webhooks do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) );
		}

		$webhook = wc_get_webhook( $id );

		if ( empty( $webhook ) || is_null( $webhook ) ) {
			return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_item_for_response( $webhook, $request );
		$result   = $webhook->delete( true );

		if ( ! $result ) {
			/* translators: %s: post type */
			return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) );
		}

		/**
		 * Fires after a single item is deleted or trashed via the REST API.
		 *
		 * @param WC_Webhook       $webhook     The deleted or trashed item.
		 * @param WP_REST_Response $response The response data.
		 * @param WP_REST_Request  $request  The request sent to the API.
		 */
		do_action( "woocommerce_rest_delete_webhook_object", $webhook, $response, $request );

		return $response;
	}

	/**
	 * Prepare a single webhook for create or update.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_Error|stdClass $data Post object.
	 */
	protected function prepare_item_for_database( $request ) {
		$data = new stdClass;

		// Post ID.
		if ( isset( $request['id'] ) ) {
			$data->ID = absint( $request['id'] );
		}

		// Validate required POST fields.
		if ( 'POST' === $request->get_method() && empty( $data->ID ) ) {
			$data->post_title = ! empty( $request['name'] ) ? $request['name'] : sprintf( __( 'Webhook created on %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Webhook created on date parsed by strftime', 'woocommerce' ) ) ); // @codingStandardsIgnoreLine

			// Post author.
			$data->post_author = get_current_user_id();

			// Post password.
			$data->post_password = 'webhook_' . wp_generate_password();

			// Post status.
			$data->post_status = 'publish';
		} else {

			// Allow edit post title.
			if ( ! empty( $request['name'] ) ) {
				$data->post_title = $request['name'];
			}
		}

		// Comment status.
		$data->comment_status = 'closed';

		// Ping status.
		$data->ping_status = 'closed';

		/**
		 * Filter the query_vars used in `get_items` for the constructed query.
		 *
		 * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
		 * prepared for insertion.
		 *
		 * @param stdClass        $data An object representing a single item prepared
		 *                                       for inserting or updating the database.
		 * @param WP_REST_Request $request       Request object.
		 */
		return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $data, $request );
	}

	/**
	 * Prepare a single webhook output for response.
	 *
	 * @param int               $id       Webhook ID or object.
	 * @param WP_REST_Request   $request  Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $id, $request ) {
		$webhook = wc_get_webhook( $id );

		if ( empty( $webhook ) || is_null( $webhook ) ) {
			return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) );
		}

		$data    = array(
			'id'            => $webhook->get_id(),
			'name'          => $webhook->get_name(),
			'status'        => $webhook->get_status(),
			'topic'         => $webhook->get_topic(),
			'resource'      => $webhook->get_resource(),
			'event'         => $webhook->get_event(),
			'hooks'         => $webhook->get_hooks(),
			'delivery_url'  => $webhook->get_delivery_url(),
			'date_created'  => wc_rest_prepare_date_response( $webhook->get_date_created() ),
			'date_modified' => wc_rest_prepare_date_response( $webhook->get_date_modified() ),
		);

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $webhook->get_id() ) );

		/**
		 * Filter webhook object returned from the REST API.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param WC_Webhook       $webhook  Webhook object used to create response.
		 * @param WP_REST_Request  $request  Request object.
		 */
		return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $webhook, $request );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param int $id Webhook ID.
	 * @return array
	 */
	protected function prepare_links( $id ) {
		$links = array(
			'self' => array(
				'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $id ) ),
			),
			'collection' => array(
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
			),
		);

		return $links;
	}

	/**
	 * Get the Webhook's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'webhook',
			'type'       => 'object',
			'properties' => array(
				'id' => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'name' => array(
					'description' => __( 'A friendly name for the webhook.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'status' => array(
					'description' => __( 'Webhook status.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'active',
					'enum'        => array_keys( wc_get_webhook_statuses() ),
					'context'     => array( 'view', 'edit' ),
				),
				'topic' => array(
					'description' => __( 'Webhook topic.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'resource' => array(
					'description' => __( 'Webhook resource.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'event' => array(
					'description' => __( 'Webhook event.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'hooks' => array(
					'description' => __( 'WooCommerce action names associated with the webhook.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
					'items'       => array(
						'type'    => 'string',
					),
				),
				'delivery_url' => array(
					'description' => __( 'The URL where the webhook payload is delivered.', 'woocommerce' ),
					'type'        => 'string',
					'format'      => 'uri',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'secret' => array(
					'description' => __( "Secret key used to generate a hash of the delivered webhook and provided in the request headers. This will default to a MD5 hash from the current user's ID|username if not provided.", 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'edit' ),
				),
				'date_created' => array(
					'description' => __( "The date the webhook was created, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_modified' => array(
					'description' => __( "The date the webhook was last modified, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections of attachments.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params = parent::get_collection_params();

		$params['context']['default'] = 'view';

		$params['after'] = array(
			'description'        => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
			'type'               => 'string',
			'format'             => 'date-time',
			'validate_callback'  => 'rest_validate_request_arg',
		);
		$params['before'] = array(
			'description'        => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
			'type'               => 'string',
			'format'             => 'date-time',
			'validate_callback'  => 'rest_validate_request_arg',
		);
		$params['exclude'] = array(
			'description'       => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type'          => 'integer',
			),
			'default'           => array(),
			'sanitize_callback' => 'wp_parse_id_list',
		);
		$params['include'] = array(
			'description'       => __( 'Limit result set to specific ids.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type'          => 'integer',
			),
			'default'           => array(),
			'sanitize_callback' => 'wp_parse_id_list',
		);
		$params['offset'] = array(
			'description'        => __( 'Offset the result set by a specific number of items.', 'woocommerce' ),
			'type'               => 'integer',
			'sanitize_callback'  => 'absint',
			'validate_callback'  => 'rest_validate_request_arg',
		);
		$params['order'] = array(
			'description'        => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
			'type'               => 'string',
			'default'            => 'desc',
			'enum'               => array( 'asc', 'desc' ),
			'validate_callback'  => 'rest_validate_request_arg',
		);
		$params['orderby'] = array(
			'description'        => __( 'Sort collection by object attribute.', 'woocommerce' ),
			'type'               => 'string',
			'default'            => 'date',
			'enum'               => array(
				'date',
				'id',
				'title',
			),
			'validate_callback'  => 'rest_validate_request_arg',
		);
		$params['status'] = array(
			'default'           => 'all',
			'description'       => __( 'Limit result set to webhooks assigned a specific status.', 'woocommerce' ),
			'type'              => 'string',
			'enum'              => array( 'all', 'active', 'paused', 'disabled' ),
			'sanitize_callback' => 'sanitize_key',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}
}
rest-api/Controllers/Version1/class-wc-rest-product-shipping-classes-v1-controller.php000064400000007217151336044760025161 0ustar00<?php
/**
 * REST API Product Shipping Classes controller
 *
 * Handles requests to the products/shipping_classes endpoint.
 *
 * @author   WooThemes
 * @category API
 * @package WooCommerce\RestApi
 * @since    3.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * REST API Product Shipping Classes controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Terms_Controller
 */
class WC_REST_Product_Shipping_Classes_V1_Controller extends WC_REST_Terms_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v1';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'products/shipping_classes';

	/**
	 * Taxonomy.
	 *
	 * @var string
	 */
	protected $taxonomy = 'product_shipping_class';

	/**
	 * Prepare a single product shipping class output for response.
	 *
	 * @param obj $item Term object.
	 * @param WP_REST_Request $request
	 * @return WP_REST_Response $response
	 */
	public function prepare_item_for_response( $item, $request ) {
		$data = array(
			'id'          => (int) $item->term_id,
			'name'        => $item->name,
			'slug'        => $item->slug,
			'description' => $item->description,
			'count'       => (int) $item->count,
		);

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $item, $request ) );

		/**
		 * Filter a term item returned from the API.
		 *
		 * Allows modification of the term data right before it is returned.
		 *
		 * @param WP_REST_Response  $response  The response object.
		 * @param object            $item      The original term object.
		 * @param WP_REST_Request   $request   Request used to generate the response.
		 */
		return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request );
	}

	/**
	 * Get the Shipping Class schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'              => 'http://json-schema.org/draft-04/schema#',
			'title'                => $this->taxonomy,
			'type'                 => 'object',
			'properties'           => array(
				'id' => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'name' => array(
					'description' => __( 'Shipping class name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				'slug' => array(
					'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_title',
					),
				),
				'description' => array(
					'description' => __( 'HTML description of the resource.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'wp_filter_post_kses',
					),
				),
				'count' => array(
					'description' => __( 'Number of published products for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php000064400000056633151336044760021361 0ustar00<?php
/**
 * REST API Taxes controller
 *
 * Handles requests to the /taxes endpoint.
 *
 * @package WooCommerce\RestApi
 * @since    3.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * REST API Taxes controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Controller
 */
class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v1';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'taxes';

	/**
	 * Register the routes for taxes.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_item' ),
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\d]+)',
			array(
				'args'   => array(
					'id' => array(
						'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => array(
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_item' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_item' ),
					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
					'args'                => array(
						'force' => array(
							'default'     => false,
							'type'        => 'boolean',
							'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
						),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/batch',
			array(
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'batch_items' ),
					'permission_callback' => array( $this, 'batch_items_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				'schema' => array( $this, 'get_public_batch_schema' ),
			)
		);
	}

	/**
	 * Check whether a given request has permission to read taxes.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_items_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access create taxes.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 *
	 * @return bool|WP_Error
	 */
	public function create_item_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'settings', 'create' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access to read a tax.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_item_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access update a tax.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 *
	 * @return bool|WP_Error
	 */
	public function update_item_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access delete a tax.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 *
	 * @return bool|WP_Error
	 */
	public function delete_item_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'settings', 'delete' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Check if a given request has access batch create, update and delete items.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 *
	 * @return bool|WP_Error
	 */
	public function batch_items_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'settings', 'batch' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Get all taxes.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_items( $request ) {
		global $wpdb;

		$prepared_args           = array();
		$prepared_args['order']  = $request['order'];
		$prepared_args['number'] = $request['per_page'];
		if ( ! empty( $request['offset'] ) ) {
			$prepared_args['offset'] = $request['offset'];
		} else {
			$prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number'];
		}
		$orderby_possibles        = array(
			'id'       => 'tax_rate_id',
			'order'    => 'tax_rate_order',
			'priority' => 'tax_rate_priority',
		);
		$prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ];
		$prepared_args['class']   = $request['class'];

		/**
		 * Filter arguments, before passing to $wpdb->get_results(), when querying taxes via the REST API.
		 *
		 * @param array           $prepared_args Array of arguments for $wpdb->get_results().
		 * @param WP_REST_Request $request       The current request.
		 */
		$prepared_args = apply_filters( 'woocommerce_rest_tax_query', $prepared_args, $request );

		$orderby = sanitize_key( $prepared_args['orderby'] ) . ' ' . sanitize_key( $prepared_args['order'] );
		$query   = "
			SELECT *
			FROM {$wpdb->prefix}woocommerce_tax_rates
			%s
			ORDER BY {$orderby}
			LIMIT %%d, %%d
		";

		$wpdb_prepare_args = array(
			$prepared_args['offset'],
			$prepared_args['number'],
		);

		// Filter by tax class.
		if ( empty( $prepared_args['class'] ) ) {
			$query = sprintf( $query, '' );
		} else {
			$class = 'standard' !== $prepared_args['class'] ? sanitize_title( $prepared_args['class'] ) : '';
			array_unshift( $wpdb_prepare_args, $class );
			$query = sprintf( $query, 'WHERE tax_rate_class = %s' );
		}

		// Query taxes.
		// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
		$results = $wpdb->get_results(
			$wpdb->prepare(
				$query,
				$wpdb_prepare_args
			)
		);
		// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared

		$taxes = array();
		foreach ( $results as $tax ) {
			$data    = $this->prepare_item_for_response( $tax, $request );
			$taxes[] = $this->prepare_response_for_collection( $data );
		}

		$response = rest_ensure_response( $taxes );

		// Store pagination values for headers then unset for count query.
		$per_page = (int) $prepared_args['number'];
		$page     = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 );

		// Query only for ids.
		// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
		$query = str_replace( 'SELECT *', 'SELECT tax_rate_id', $query );
		$wpdb->get_results(
			$wpdb->prepare(
				$query,
				$wpdb_prepare_args
			)
		);
		// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared

		// Calculate totals.
		$total_taxes = (int) $wpdb->num_rows;
		$response->header( 'X-WP-Total', (int) $total_taxes );
		$max_pages = ceil( $total_taxes / $per_page );
		$response->header( 'X-WP-TotalPages', (int) $max_pages );

		$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
		if ( $page > 1 ) {
			$prev_page = $page - 1;
			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}
			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}
		if ( $max_pages > $page ) {
			$next_page = $page + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );
			$response->link_header( 'next', $next_link );
		}

		return $response;
	}

	/**
	 * Take tax data from the request and return the updated or newly created rate.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @param stdClass|null   $current Existing tax object.
	 * @return object
	 */
	protected function create_or_update_tax( $request, $current = null ) {
		$id     = absint( isset( $request['id'] ) ? $request['id'] : 0 );
		$data   = array();
		$fields = array(
			'tax_rate_country',
			'tax_rate_state',
			'tax_rate',
			'tax_rate_name',
			'tax_rate_priority',
			'tax_rate_compound',
			'tax_rate_shipping',
			'tax_rate_order',
			'tax_rate_class',
		);

		foreach ( $fields as $field ) {
			// Keys via API differ from the stored names returned by _get_tax_rate.
			$key = 'tax_rate' === $field ? 'rate' : str_replace( 'tax_rate_', '', $field );

			// Remove data that was not posted.
			if ( ! isset( $request[ $key ] ) ) {
				continue;
			}

			// Test new data against current data.
			if ( $current && $current->$field === $request[ $key ] ) {
				continue;
			}

			// Add to data array.
			switch ( $key ) {
				case 'tax_rate_priority':
				case 'tax_rate_compound':
				case 'tax_rate_shipping':
				case 'tax_rate_order':
					$data[ $field ] = absint( $request[ $key ] );
					break;
				case 'tax_rate_class':
					$data[ $field ] = 'standard' !== $request['tax_rate_class'] ? $request['tax_rate_class'] : '';
					break;
				default:
					$data[ $field ] = wc_clean( $request[ $key ] );
					break;
			}
		}

		if ( ! $id ) {
			$id = WC_Tax::_insert_tax_rate( $data );
		} elseif ( $data ) {
			WC_Tax::_update_tax_rate( $id, $data );
		}

		// Add locales.
		if ( ! empty( $request['postcode'] ) ) {
			WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $request['postcode'] ) );
		}
		if ( ! empty( $request['city'] ) ) {
			WC_Tax::_update_tax_rate_cities( $id, wc_clean( $request['city'] ) );
		}

		return WC_Tax::_get_tax_rate( $id, OBJECT );
	}

	/**
	 * Create a single tax.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function create_item( $request ) {
		if ( ! empty( $request['id'] ) ) {
			return new WP_Error( 'woocommerce_rest_tax_exists', __( 'Cannot create existing resource.', 'woocommerce' ), array( 'status' => 400 ) );
		}

		$tax = $this->create_or_update_tax( $request );

		$this->update_additional_fields_for_object( $tax, $request );

		/**
		 * Fires after a tax is created or updated via the REST API.
		 *
		 * @param stdClass        $tax       Data used to create the tax.
		 * @param WP_REST_Request $request   Request object.
		 * @param boolean         $creating  True when creating tax, false when updating tax.
		 */
		do_action( 'woocommerce_rest_insert_tax', $tax, $request, true );

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_item_for_response( $tax, $request );
		$response = rest_ensure_response( $response );
		$response->set_status( 201 );
		$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $tax->tax_rate_id ) ) );

		return $response;
	}

	/**
	 * Get a single tax.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_item( $request ) {
		$id      = (int) $request['id'];
		$tax_obj = WC_Tax::_get_tax_rate( $id, OBJECT );

		if ( empty( $id ) || empty( $tax_obj ) ) {
			return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$tax      = $this->prepare_item_for_response( $tax_obj, $request );
		$response = rest_ensure_response( $tax );

		return $response;
	}

	/**
	 * Update a single tax.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function update_item( $request ) {
		$id      = (int) $request['id'];
		$tax_obj = WC_Tax::_get_tax_rate( $id, OBJECT );

		if ( empty( $id ) || empty( $tax_obj ) ) {
			return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		$tax = $this->create_or_update_tax( $request, $tax_obj );

		$this->update_additional_fields_for_object( $tax, $request );

		/**
		 * Fires after a tax is created or updated via the REST API.
		 *
		 * @param stdClass        $tax       Data used to create the tax.
		 * @param WP_REST_Request $request   Request object.
		 * @param boolean         $creating  True when creating tax, false when updating tax.
		 */
		do_action( 'woocommerce_rest_insert_tax', $tax, $request, false );

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_item_for_response( $tax, $request );
		$response = rest_ensure_response( $response );

		return $response;
	}

	/**
	 * Delete a single tax.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function delete_item( $request ) {
		global $wpdb;

		$id    = (int) $request['id'];
		$force = isset( $request['force'] ) ? (bool) $request['force'] : false;

		// We don't support trashing for this type, error out.
		if ( ! $force ) {
			return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Taxes do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) );
		}

		$tax = WC_Tax::_get_tax_rate( $id, OBJECT );

		if ( empty( $id ) || empty( $tax ) ) {
			return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 400 ) );
		}

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_item_for_response( $tax, $request );

		WC_Tax::_delete_tax_rate( $id );

		if ( 0 === $wpdb->rows_affected ) {
			return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) );
		}

		/**
		 * Fires after a tax is deleted via the REST API.
		 *
		 * @param stdClass         $tax      The tax data.
		 * @param WP_REST_Response $response The response returned from the API.
		 * @param WP_REST_Request  $request  The request sent to the API.
		 */
		do_action( 'woocommerce_rest_delete_tax', $tax, $response, $request );

		return $response;
	}

	/**
	 * Prepare a single tax output for response.
	 *
	 * @param stdClass        $tax     Tax object.
	 * @param WP_REST_Request $request Request object.
	 *
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $tax, $request ) {
		$id   = (int) $tax->tax_rate_id;
		$data = array(
			'id'       => $id,
			'country'  => $tax->tax_rate_country,
			'state'    => $tax->tax_rate_state,
			'postcode' => '',
			'city'     => '',
			'rate'     => $tax->tax_rate,
			'name'     => $tax->tax_rate_name,
			'priority' => (int) $tax->tax_rate_priority,
			'compound' => (bool) $tax->tax_rate_compound,
			'shipping' => (bool) $tax->tax_rate_shipping,
			'order'    => (int) $tax->tax_rate_order,
			'class'    => $tax->tax_rate_class ? $tax->tax_rate_class : 'standard',
		);

		$data = $this->add_tax_rate_locales( $data, $tax );

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $tax ) );

		/**
		 * Filter tax object returned from the REST API.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param stdClass         $tax      Tax object used to create response.
		 * @param WP_REST_Request  $request  Request object.
		 */
		return apply_filters( 'woocommerce_rest_prepare_tax', $response, $tax, $request );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param stdClass $tax Tax object.
	 * @return array Links for the given tax.
	 */
	protected function prepare_links( $tax ) {
		$links = array(
			'self'       => array(
				'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $tax->tax_rate_id ) ),
			),
			'collection' => array(
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
			),
		);

		return $links;
	}

	/**
	 * Add tax rate locales to the response array.
	 *
	 * @param array    $data Response data.
	 * @param stdClass $tax  Tax object.
	 *
	 * @return array
	 */
	protected function add_tax_rate_locales( $data, $tax ) {
		global $wpdb;

		// Get locales from a tax rate.
		$locales = $wpdb->get_results(
			$wpdb->prepare(
				"
				SELECT location_code, location_type
				FROM {$wpdb->prefix}woocommerce_tax_rate_locations
				WHERE tax_rate_id = %d
				",
				$tax->tax_rate_id
			)
		);

		if ( ! is_wp_error( $tax ) && ! is_null( $tax ) ) {
			foreach ( $locales as $locale ) {
				$data[ $locale->location_type ] = $locale->location_code;
			}
		}

		return $data;
	}

	/**
	 * Get the Taxes schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'tax',
			'type'       => 'object',
			'properties' => array(
				'id'       => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'country'  => array(
					'description' => __( 'Country ISO 3166 code.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'state'    => array(
					'description' => __( 'State code.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'postcode' => array(
					'description' => __( 'Postcode / ZIP.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'city'     => array(
					'description' => __( 'City name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'rate'     => array(
					'description' => __( 'Tax rate.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'name'     => array(
					'description' => __( 'Tax rate name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'priority' => array(
					'description' => __( 'Tax priority.', 'woocommerce' ),
					'type'        => 'integer',
					'default'     => 1,
					'context'     => array( 'view', 'edit' ),
				),
				'compound' => array(
					'description' => __( 'Whether or not this is a compound rate.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'shipping' => array(
					'description' => __( 'Whether or not this tax rate also gets applied to shipping.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => true,
					'context'     => array( 'view', 'edit' ),
				),
				'order'    => array(
					'description' => __( 'Indicates the order that will appear in queries.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'class'    => array(
					'description' => __( 'Tax class.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'standard',
					'enum'        => array_merge( array( 'standard' ), WC_Tax::get_tax_class_slugs() ),
					'context'     => array( 'view', 'edit' ),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params                       = array();
		$params['context']            = $this->get_context_param();
		$params['context']['default'] = 'view';

		$params['page']     = array(
			'description'       => __( 'Current page of the collection.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 1,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
			'minimum'           => 1,
		);
		$params['per_page'] = array(
			'description'       => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 10,
			'minimum'           => 1,
			'maximum'           => 100,
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['offset']   = array(
			'description'       => __( 'Offset the result set by a specific number of items.', 'woocommerce' ),
			'type'              => 'integer',
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['order']    = array(
			'default'           => 'asc',
			'description'       => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
			'enum'              => array( 'asc', 'desc' ),
			'sanitize_callback' => 'sanitize_key',
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['orderby']  = array(
			'default'           => 'order',
			'description'       => __( 'Sort collection by object attribute.', 'woocommerce' ),
			'enum'              => array(
				'id',
				'order',
				'priority',
			),
			'sanitize_callback' => 'sanitize_key',
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['class']    = array(
			'description'       => __( 'Sort by tax class.', 'woocommerce' ),
			'enum'              => array_merge( array( 'standard' ), WC_Tax::get_tax_class_slugs() ),
			'sanitize_callback' => 'sanitize_title',
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}
}
rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller.php000064400000154403151336044760021525 0ustar00<?php
/**
 * REST API Orders controller
 *
 * Handles requests to the /orders endpoint.
 *
 * @author   WooThemes
 * @category API
 * @package WooCommerce\RestApi
 * @since    3.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * REST API Orders controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Posts_Controller
 */
class WC_REST_Orders_V1_Controller extends WC_REST_Posts_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v1';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'orders';

	/**
	 * Post type.
	 *
	 * @var string
	 */
	protected $post_type = 'shop_order';

	/**
	 * Initialize orders actions.
	 */
	public function __construct() {
		add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 );
	}

	/**
	 * Register the routes for orders.
	 */
	public function register_routes() {
		register_rest_route( $this->namespace, '/' . $this->rest_base, array(
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_items' ),
				'permission_callback' => array( $this, 'get_items_permissions_check' ),
				'args'                => $this->get_collection_params(),
			),
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'create_item' ),
				'permission_callback' => array( $this, 'create_item_permissions_check' ),
				'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
			),
			'schema' => array( $this, 'get_public_item_schema' ),
		) );

		register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
			'args' => array(
				'id' => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
				),
			),
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_item' ),
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
				'args'                => array(
					'context' => $this->get_context_param( array( 'default' => 'view' ) ),
				),
			),
			array(
				'methods'             => WP_REST_Server::EDITABLE,
				'callback'            => array( $this, 'update_item' ),
				'permission_callback' => array( $this, 'update_item_permissions_check' ),
				'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
			),
			array(
				'methods'             => WP_REST_Server::DELETABLE,
				'callback'            => array( $this, 'delete_item' ),
				'permission_callback' => array( $this, 'delete_item_permissions_check' ),
				'args'                => array(
					'force' => array(
						'default'     => false,
						'type'        => 'boolean',
						'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ),
					),
				),
			),
			'schema' => array( $this, 'get_public_item_schema' ),
		) );

		register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array(
			array(
				'methods'             => WP_REST_Server::EDITABLE,
				'callback'            => array( $this, 'batch_items' ),
				'permission_callback' => array( $this, 'batch_items_permissions_check' ),
				'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
			),
			'schema' => array( $this, 'get_public_batch_schema' ),
		) );
	}

	/**
	 * Prepare a single order output for response.
	 *
	 * @param WP_Post $post Post object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $data
	 */
	public function prepare_item_for_response( $post, $request ) {
		$order = wc_get_order( $post );
		$dp    = is_null( $request['dp'] ) ? wc_get_price_decimals() : absint( $request['dp'] );

		$data = array(
			'id'                   => $order->get_id(),
			'parent_id'            => $order->get_parent_id(),
			'status'               => $order->get_status(),
			'order_key'            => $order->get_order_key(),
			'number'               => $order->get_order_number(),
			'currency'             => $order->get_currency(),
			'version'              => $order->get_version(),
			'prices_include_tax'   => $order->get_prices_include_tax(),
			'date_created'         => wc_rest_prepare_date_response( $order->get_date_created() ),  // v1 API used UTC.
			'date_modified'        => wc_rest_prepare_date_response( $order->get_date_modified() ), // v1 API used UTC.
			'customer_id'          => $order->get_customer_id(),
			'discount_total'       => wc_format_decimal( $order->get_total_discount(), $dp ),
			'discount_tax'         => wc_format_decimal( $order->get_discount_tax(), $dp ),
			'shipping_total'       => wc_format_decimal( $order->get_shipping_total(), $dp ),
			'shipping_tax'         => wc_format_decimal( $order->get_shipping_tax(), $dp ),
			'cart_tax'             => wc_format_decimal( $order->get_cart_tax(), $dp ),
			'total'                => wc_format_decimal( $order->get_total(), $dp ),
			'total_tax'            => wc_format_decimal( $order->get_total_tax(), $dp ),
			'billing'              => array(),
			'shipping'             => array(),
			'payment_method'       => $order->get_payment_method(),
			'payment_method_title' => $order->get_payment_method_title(),
			'transaction_id'       => $order->get_transaction_id(),
			'customer_ip_address'  => $order->get_customer_ip_address(),
			'customer_user_agent'  => $order->get_customer_user_agent(),
			'created_via'          => $order->get_created_via(),
			'customer_note'        => $order->get_customer_note(),
			'date_completed'       => wc_rest_prepare_date_response( $order->get_date_completed(), false ), // v1 API used local time.
			'date_paid'            => wc_rest_prepare_date_response( $order->get_date_paid(), false ), // v1 API used local time.
			'cart_hash'            => $order->get_cart_hash(),
			'line_items'           => array(),
			'tax_lines'            => array(),
			'shipping_lines'       => array(),
			'fee_lines'            => array(),
			'coupon_lines'         => array(),
			'refunds'              => array(),
		);

		// Add addresses.
		$data['billing']  = $order->get_address( 'billing' );
		$data['shipping'] = $order->get_address( 'shipping' );

		// Add line items.
		foreach ( $order->get_items() as $item_id => $item ) {
			$product      = $item->get_product();
			$product_id   = 0;
			$variation_id = 0;
			$product_sku  = null;

			// Check if the product exists.
			if ( is_object( $product ) ) {
				$product_id   = $item->get_product_id();
				$variation_id = $item->get_variation_id();
				$product_sku  = $product->get_sku();
			}

			$item_meta = array();

			$hideprefix = 'true' === $request['all_item_meta'] ? null : '_';

			foreach ( $item->get_formatted_meta_data( $hideprefix, true ) as $meta_key => $formatted_meta ) {
				$item_meta[] = array(
					'key'   => $formatted_meta->key,
					'label' => $formatted_meta->display_key,
					'value' => wc_clean( $formatted_meta->display_value ),
				);
			}

			$line_item = array(
				'id'           => $item_id,
				'name'         => $item['name'],
				'sku'          => $product_sku,
				'product_id'   => (int) $product_id,
				'variation_id' => (int) $variation_id,
				'quantity'     => wc_stock_amount( $item['qty'] ),
				'tax_class'    => ! empty( $item['tax_class'] ) ? $item['tax_class'] : '',
				'price'        => wc_format_decimal( $order->get_item_total( $item, false, false ), $dp ),
				'subtotal'     => wc_format_decimal( $order->get_line_subtotal( $item, false, false ), $dp ),
				'subtotal_tax' => wc_format_decimal( $item['line_subtotal_tax'], $dp ),
				'total'        => wc_format_decimal( $order->get_line_total( $item, false, false ), $dp ),
				'total_tax'    => wc_format_decimal( $item['line_tax'], $dp ),
				'taxes'        => array(),
				'meta'         => $item_meta,
			);

			$item_line_taxes = maybe_unserialize( $item['line_tax_data'] );
			if ( isset( $item_line_taxes['total'] ) ) {
				$line_tax = array();

				foreach ( $item_line_taxes['total'] as $tax_rate_id => $tax ) {
					$line_tax[ $tax_rate_id ] = array(
						'id'       => $tax_rate_id,
						'total'    => $tax,
						'subtotal' => '',
					);
				}

				foreach ( $item_line_taxes['subtotal'] as $tax_rate_id => $tax ) {
					$line_tax[ $tax_rate_id ]['subtotal'] = $tax;
				}

				$line_item['taxes'] = array_values( $line_tax );
			}

			$data['line_items'][] = $line_item;
		}

		// Add taxes.
		foreach ( $order->get_items( 'tax' ) as $key => $tax ) {
			$tax_line = array(
				'id'                 => $key,
				'rate_code'          => $tax['name'],
				'rate_id'            => $tax['rate_id'],
				'label'              => isset( $tax['label'] ) ? $tax['label'] : $tax['name'],
				'compound'           => (bool) $tax['compound'],
				'tax_total'          => wc_format_decimal( $tax['tax_amount'], $dp ),
				'shipping_tax_total' => wc_format_decimal( $tax['shipping_tax_amount'], $dp ),
			);

			$data['tax_lines'][] = $tax_line;
		}

		// Add shipping.
		foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) {
			$shipping_line = array(
				'id'           => $shipping_item_id,
				'method_title' => $shipping_item['name'],
				'method_id'    => $shipping_item['method_id'],
				'total'        => wc_format_decimal( $shipping_item['cost'], $dp ),
				'total_tax'    => wc_format_decimal( '', $dp ),
				'taxes'        => array(),
			);

			$shipping_taxes = $shipping_item->get_taxes();

			if ( ! empty( $shipping_taxes['total'] ) ) {
				$shipping_line['total_tax'] = wc_format_decimal( array_sum( $shipping_taxes['total'] ), $dp );

				foreach ( $shipping_taxes['total'] as $tax_rate_id => $tax ) {
					$shipping_line['taxes'][] = array(
						'id'       => $tax_rate_id,
						'total'    => $tax,
					);
				}
			}

			$data['shipping_lines'][] = $shipping_line;
		}

		// Add fees.
		foreach ( $order->get_fees() as $fee_item_id => $fee_item ) {
			$fee_line = array(
				'id'         => $fee_item_id,
				'name'       => $fee_item['name'],
				'tax_class'  => ! empty( $fee_item['tax_class'] ) ? $fee_item['tax_class'] : '',
				'tax_status' => 'taxable',
				'total'      => wc_format_decimal( $order->get_line_total( $fee_item ), $dp ),
				'total_tax'  => wc_format_decimal( $order->get_line_tax( $fee_item ), $dp ),
				'taxes'      => array(),
			);

			$fee_line_taxes = maybe_unserialize( $fee_item['line_tax_data'] );
			if ( isset( $fee_line_taxes['total'] ) ) {
				$fee_tax = array();

				foreach ( $fee_line_taxes['total'] as $tax_rate_id => $tax ) {
					$fee_tax[ $tax_rate_id ] = array(
						'id'       => $tax_rate_id,
						'total'    => $tax,
						'subtotal' => '',
					);
				}

				if ( isset( $fee_line_taxes['subtotal'] ) ) {
					foreach ( $fee_line_taxes['subtotal'] as $tax_rate_id => $tax ) {
						$fee_tax[ $tax_rate_id ]['subtotal'] = $tax;
					}
				}

				$fee_line['taxes'] = array_values( $fee_tax );
			}

			$data['fee_lines'][] = $fee_line;
		}

		// Add coupons.
		foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) {
			$coupon_line = array(
				'id'           => $coupon_item_id,
				'code'         => $coupon_item['name'],
				'discount'     => wc_format_decimal( $coupon_item['discount_amount'], $dp ),
				'discount_tax' => wc_format_decimal( $coupon_item['discount_amount_tax'], $dp ),
			);

			$data['coupon_lines'][] = $coupon_line;
		}

		// Add refunds.
		foreach ( $order->get_refunds() as $refund ) {
			$data['refunds'][] = array(
				'id'     => $refund->get_id(),
				'refund' => $refund->get_reason() ? $refund->get_reason() : '',
				'total'  => '-' . wc_format_decimal( $refund->get_amount(), $dp ),
			);
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $order, $request ) );

		/**
		 * Filter the data for a response.
		 *
		 * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
		 * prepared for the response.
		 *
		 * @param WP_REST_Response   $response   The response object.
		 * @param WP_Post            $post       Post object.
		 * @param WP_REST_Request    $request    Request object.
		 */
		return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param WC_Order $order Order object.
	 * @param WP_REST_Request $request Request object.
	 * @return array Links for the given order.
	 */
	protected function prepare_links( $order, $request ) {
		$links = array(
			'self' => array(
				'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $order->get_id() ) ),
			),
			'collection' => array(
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
			),
		);
		if ( 0 !== (int) $order->get_user_id() ) {
			$links['customer'] = array(
				'href' => rest_url( sprintf( '/%s/customers/%d', $this->namespace, $order->get_user_id() ) ),
			);
		}
		if ( 0 !== (int) $order->get_parent_id() ) {
			$links['up'] = array(
				'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order->get_parent_id() ) ),
			);
		}
		return $links;
	}

	/**
	 * Query args.
	 *
	 * @param array $args
	 * @param WP_REST_Request $request
	 * @return array
	 */
	public function query_args( $args, $request ) {
		global $wpdb;

		// Set post_status.
		if ( 'any' !== $request['status'] ) {
			$args['post_status'] = 'wc-' . $request['status'];
		} else {
			$args['post_status'] = 'any';
		}

		if ( isset( $request['customer'] ) ) {
			if ( ! empty( $args['meta_query'] ) ) {
				$args['meta_query'] = array();
			}

			$args['meta_query'][] = array(
				'key'   => '_customer_user',
				'value' => $request['customer'],
				'type'  => 'NUMERIC',
			);
		}

		// Search by product.
		if ( ! empty( $request['product'] ) ) {
			$order_ids = $wpdb->get_col( $wpdb->prepare( "
				SELECT order_id
				FROM {$wpdb->prefix}woocommerce_order_items
				WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d )
				AND order_item_type = 'line_item'
			 ", $request['product'] ) );

			// Force WP_Query return empty if don't found any order.
			$order_ids = ! empty( $order_ids ) ? $order_ids : array( 0 );

			$args['post__in'] = $order_ids;
		}

		// Search.
		if ( ! empty( $args['s'] ) ) {
			$order_ids = wc_order_search( $args['s'] );

			if ( ! empty( $order_ids ) ) {
				unset( $args['s'] );
				$args['post__in'] = array_merge( $order_ids, array( 0 ) );
			}
		}

		return $args;
	}

	/**
	 * Prepare a single order for create.
	 *
	 * @param  WP_REST_Request $request Request object.
	 * @return WP_Error|WC_Order $data Object.
	 */
	protected function prepare_item_for_database( $request ) {
		$id        = isset( $request['id'] ) ? absint( $request['id'] ) : 0;
		$order     = new WC_Order( $id );
		$schema    = $this->get_item_schema();
		$data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) );

		// Handle all writable props
		foreach ( $data_keys as $key ) {
			$value = $request[ $key ];

			if ( ! is_null( $value ) ) {
				switch ( $key ) {
					case 'billing' :
					case 'shipping' :
						$this->update_address( $order, $value, $key );
						break;
					case 'line_items' :
					case 'shipping_lines' :
					case 'fee_lines' :
					case 'coupon_lines' :
						if ( is_array( $value ) ) {
							foreach ( $value as $item ) {
								if ( is_array( $item ) ) {
									if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) {
										$order->remove_item( $item['id'] );
									} else {
										$this->set_item( $order, $key, $item );
									}
								}
							}
						}
						break;
					default :
						if ( is_callable( array( $order, "set_{$key}" ) ) ) {
							$order->{"set_{$key}"}( $value );
						}
						break;
				}
			}
		}

		/**
		 * Filter the data for the insert.
		 *
		 * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
		 * prepared for the response.
		 *
		 * @param WC_Order           $order      The order object.
		 * @param WP_REST_Request    $request    Request object.
		 */
		return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $order, $request );
	}

	/**
	 * Create base WC Order object.
	 * @deprecated 3.0.0
	 * @param array $data
	 * @return WC_Order
	 */
	protected function create_base_order( $data ) {
		return wc_create_order( $data );
	}

	/**
	 * Only return writable props from schema.
	 * @param  array $schema
	 * @return bool
	 */
	protected function filter_writable_props( $schema ) {
		return empty( $schema['readonly'] );
	}

	/**
	 * Create order.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return int|WP_Error
	 */
	protected function create_order( $request ) {
		try {
			// Make sure customer exists.
			if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] && false === get_user_by( 'id', $request['customer_id'] ) ) {
				throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id',__( 'Customer ID is invalid.', 'woocommerce' ), 400 );
			}

			// Make sure customer is part of blog.
			if ( is_multisite() && ! is_user_member_of_blog( $request['customer_id'] ) ) {
				add_user_to_blog( get_current_blog_id(), $request['customer_id'], 'customer' );
			}

			$order = $this->prepare_item_for_database( $request );
			$order->set_created_via( 'rest-api' );
			$order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) );
			$order->calculate_totals();
			$order->save();

			// Handle set paid.
			if ( true === $request['set_paid'] ) {
				$order->payment_complete( $request['transaction_id'] );
			}

			return $order->get_id();
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
		} catch ( WC_REST_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Update order.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return int|WP_Error
	 */
	protected function update_order( $request ) {
		try {
			$order = $this->prepare_item_for_database( $request );
			$order->save();

			// Handle set paid.
			if ( $order->needs_payment() && true === $request['set_paid'] ) {
				$order->payment_complete( $request['transaction_id'] );
			}

			// If items have changed, recalculate order totals.
			if ( isset( $request['billing'] ) || isset( $request['shipping'] ) || isset( $request['line_items'] ) || isset( $request['shipping_lines'] ) || isset( $request['fee_lines'] ) || isset( $request['coupon_lines'] ) ) {
				$order->calculate_totals( true );
			}

			return $order->get_id();
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
		} catch ( WC_REST_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Update address.
	 *
	 * @param WC_Order $order
	 * @param array $posted
	 * @param string $type
	 */
	protected function update_address( $order, $posted, $type = 'billing' ) {
		foreach ( $posted as $key => $value ) {
			if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) {
				$order->{"set_{$type}_{$key}"}( $value );
			}
		}
	}

	/**
	 * Gets the product ID from the SKU or posted ID.
	 *
	 * @throws WC_REST_Exception When SKU or ID is not valid.
	 * @param array           $posted Request data.
	 * @param string          $action 'create' to add line item or 'update' to update it.
	 * @return int
	 */
	protected function get_product_id( $posted, $action = 'create' ) {
		if ( ! empty( $posted['sku'] ) ) {
			$product_id = (int) wc_get_product_id_by_sku( $posted['sku'] );
		} elseif ( ! empty( $posted['product_id'] ) && empty( $posted['variation_id'] ) ) {
			$product_id = (int) $posted['product_id'];
		} elseif ( ! empty( $posted['variation_id'] ) ) {
			$product_id = (int) $posted['variation_id'];
		} elseif ( 'update' === $action ) {
			$product_id = 0;
		} else {
			throw new WC_REST_Exception( 'woocommerce_rest_required_product_reference', __( 'Product ID or SKU is required.', 'woocommerce' ), 400 );
		}
		return $product_id;
	}

	/**
	 * Maybe set an item prop if the value was posted.
	 * @param WC_Order_Item $item
	 * @param string $prop
	 * @param array $posted Request data.
	 */
	protected function maybe_set_item_prop( $item, $prop, $posted ) {
		if ( isset( $posted[ $prop ] ) ) {
			$item->{"set_$prop"}( $posted[ $prop ] );
		}
	}

	/**
	 * Maybe set item props if the values were posted.
	 * @param WC_Order_Item $item
	 * @param string[] $props
	 * @param array $posted Request data.
	 */
	protected function maybe_set_item_props( $item, $props, $posted ) {
		foreach ( $props as $prop ) {
			$this->maybe_set_item_prop( $item, $prop, $posted );
		}
	}

	/**
	 * Create or update a line item.
	 *
	 * @param array $posted Line item data.
	 * @param string $action 'create' to add line item or 'update' to update it.
	 *
	 * @return WC_Order_Item_Product
	 * @throws WC_REST_Exception Invalid data, server error.
	 */
	protected function prepare_line_items( $posted, $action = 'create' ) {
		$item    = new WC_Order_Item_Product( ! empty( $posted['id'] ) ? $posted['id'] : '' );
		$product = wc_get_product( $this->get_product_id( $posted, $action ) );

		if ( $product && $product !== $item->get_product() ) {
			$item->set_product( $product );

			if ( 'create' === $action ) {
				$quantity = isset( $posted['quantity'] ) ? $posted['quantity'] : 1;
				$total    = wc_get_price_excluding_tax( $product, array( 'qty' => $quantity ) );
				$item->set_total( $total );
				$item->set_subtotal( $total );
			}
		}

		$this->maybe_set_item_props( $item, array( 'name', 'quantity', 'total', 'subtotal', 'tax_class' ), $posted );

		return $item;
	}

	/**
	 * Create or update an order shipping method.
	 *
	 * @param $posted $shipping Item data.
	 * @param string $action 'create' to add shipping or 'update' to update it.
	 *
	 * @return WC_Order_Item_Shipping
	 * @throws WC_REST_Exception Invalid data, server error.
	 */
	protected function prepare_shipping_lines( $posted, $action ) {
		$item = new WC_Order_Item_Shipping( ! empty( $posted['id'] ) ? $posted['id'] : '' );

		if ( 'create' === $action ) {
			if ( empty( $posted['method_id'] ) ) {
				throw new WC_REST_Exception( 'woocommerce_rest_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 );
			}
		}

		$this->maybe_set_item_props( $item, array( 'method_id', 'method_title', 'total' ), $posted );

		return $item;
	}

	/**
	 * Create or update an order fee.
	 *
	 * @param array $posted Item data.
	 * @param string $action 'create' to add fee or 'update' to update it.
	 *
	 * @return WC_Order_Item_Fee
	 * @throws WC_REST_Exception Invalid data, server error.
	 */
	protected function prepare_fee_lines( $posted, $action ) {
		$item = new WC_Order_Item_Fee( ! empty( $posted['id'] ) ? $posted['id'] : '' );

		if ( 'create' === $action ) {
			if ( empty( $posted['name'] ) ) {
				throw new WC_REST_Exception( 'woocommerce_rest_invalid_fee_item', __( 'Fee name is required.', 'woocommerce' ), 400 );
			}
		}

		$this->maybe_set_item_props( $item, array( 'name', 'tax_class', 'tax_status', 'total' ), $posted );

		return $item;
	}

	/**
	 * Create or update an order coupon.
	 *
	 * @param array $posted Item data.
	 * @param string $action 'create' to add coupon or 'update' to update it.
	 *
	 * @return WC_Order_Item_Coupon
	 * @throws WC_REST_Exception Invalid data, server error.
	 */
	protected function prepare_coupon_lines( $posted, $action ) {
		$item = new WC_Order_Item_Coupon( ! empty( $posted['id'] ) ? $posted['id'] : '' );

		if ( 'create' === $action ) {
			if ( empty( $posted['code'] ) ) {
				throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 );
			}
		}

		$this->maybe_set_item_props( $item, array( 'code', 'discount' ), $posted );

		return $item;
	}

	/**
	 * Wrapper method to create/update order items.
	 * When updating, the item ID provided is checked to ensure it is associated
	 * with the order.
	 *
	 * @param WC_Order $order order
	 * @param string $item_type
	 * @param array $posted item provided in the request body
	 * @throws WC_REST_Exception If item ID is not associated with order
	 */
	protected function set_item( $order, $item_type, $posted ) {
		global $wpdb;

		if ( ! empty( $posted['id'] ) ) {
			$action = 'update';
		} else {
			$action = 'create';
		}

		$method = 'prepare_' . $item_type;

		// Verify provided line item ID is associated with order.
		if ( 'update' === $action ) {
			$result = $wpdb->get_row(
				$wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d AND order_id = %d",
				absint( $posted['id'] ),
				absint( $order->get_id() )
			) );
			if ( is_null( $result ) ) {
				throw new WC_REST_Exception( 'woocommerce_rest_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 );
			}
		}

		// Prepare item data
		$item = $this->$method( $posted, $action );

		/**
		 * Action hook to adjust item before save.
		 * @since 3.0.0
		 */
		do_action( 'woocommerce_rest_set_order_item', $item, $posted );

		// Save or add to order
		if ( 'create' === $action ) {
			$order->add_item( $item );
		} else {
			$item->save();
		}
	}

	/**
	 * Helper method to check if the resource ID associated with the provided item is null.
	 * Items can be deleted by setting the resource ID to null.
	 *
	 * @param array $item Item provided in the request body.
	 * @return bool True if the item resource ID is null, false otherwise.
	 */
	protected function item_is_null( $item ) {
		$keys = array( 'product_id', 'method_id', 'method_title', 'name', 'code' );

		foreach ( $keys as $key ) {
			if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Create a single item.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function create_item( $request ) {
		if ( ! empty( $request['id'] ) ) {
			/* translators: %s: post type */
			return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) );
		}

		$order_id = $this->create_order( $request );
		if ( is_wp_error( $order_id ) ) {
			return $order_id;
		}

		$post = get_post( $order_id );
		$this->update_additional_fields_for_object( $post, $request );

		/**
		 * Fires after a single item is created or updated via the REST API.
		 *
		 * @param WP_Post         $post      Post object.
		 * @param WP_REST_Request $request   Request object.
		 * @param boolean         $creating  True when creating item, false when updating.
		 */
		do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true );
		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_item_for_response( $post, $request );
		$response = rest_ensure_response( $response );
		$response->set_status( 201 );
		$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) );

		return $response;
	}

	/**
	 * Update a single order.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function update_item( $request ) {
		try {
			$post_id = (int) $request['id'];

			if ( empty( $post_id ) || get_post_type( $post_id ) !== $this->post_type ) {
				return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) );
			}

			$order_id = $this->update_order( $request );
			if ( is_wp_error( $order_id ) ) {
				return $order_id;
			}

			$post = get_post( $order_id );
			$this->update_additional_fields_for_object( $post, $request );

			/**
			 * Fires after a single item is created or updated via the REST API.
			 *
			 * @param WP_Post         $post      Post object.
			 * @param WP_REST_Request $request   Request object.
			 * @param boolean         $creating  True when creating item, false when updating.
			 */
			do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, false );
			$request->set_param( 'context', 'edit' );
			$response = $this->prepare_item_for_response( $post, $request );
			return rest_ensure_response( $response );

		} catch ( Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get order statuses without prefixes.
	 * @return array
	 */
	protected function get_order_statuses() {
		$order_statuses = array();

		foreach ( array_keys( wc_get_order_statuses() ) as $status ) {
			$order_statuses[] = str_replace( 'wc-', '', $status );
		}

		return $order_statuses;
	}

	/**
	 * Get the Order's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => $this->post_type,
			'type'       => 'object',
			'properties' => array(
				'id' => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'parent_id' => array(
					'description' => __( 'Parent order ID.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'status' => array(
					'description' => __( 'Order status.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'pending',
					'enum'        => $this->get_order_statuses(),
					'context'     => array( 'view', 'edit' ),
				),
				'order_key' => array(
					'description' => __( 'Order key.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'number' => array(
					'description' => __( 'Order number.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'currency' => array(
					'description' => __( 'Currency the order was created with, in ISO format.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => get_woocommerce_currency(),
					'enum'        => array_keys( get_woocommerce_currencies() ),
					'context'     => array( 'view', 'edit' ),
				),
				'version' => array(
					'description' => __( 'Version of WooCommerce which last updated the order.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'prices_include_tax' => array(
					'description' => __( 'True the prices included tax during checkout.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_created' => array(
					'description' => __( "The date the order was created, as GMT.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_modified' => array(
					'description' => __( "The date the order was last modified, as GMT.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'customer_id' => array(
					'description' => __( 'User ID who owns the order. 0 for guests.', 'woocommerce' ),
					'type'        => 'integer',
					'default'     => 0,
					'context'     => array( 'view', 'edit' ),
				),
				'discount_total' => array(
					'description' => __( 'Total discount amount for the order.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'discount_tax' => array(
					'description' => __( 'Total discount tax amount for the order.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'shipping_total' => array(
					'description' => __( 'Total shipping amount for the order.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'shipping_tax' => array(
					'description' => __( 'Total shipping tax amount for the order.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'cart_tax' => array(
					'description' => __( 'Sum of line item taxes only.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'total' => array(
					'description' => __( 'Grand total.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'total_tax' => array(
					'description' => __( 'Sum of all taxes.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'billing' => array(
					'description' => __( 'Billing address.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'properties'  => array(
						'first_name' => array(
							'description' => __( 'First name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'last_name' => array(
							'description' => __( 'Last name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'company' => array(
							'description' => __( 'Company name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'address_1' => array(
							'description' => __( 'Address line 1.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'address_2' => array(
							'description' => __( 'Address line 2.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'city' => array(
							'description' => __( 'City name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'state' => array(
							'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'postcode' => array(
							'description' => __( 'Postal code.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'country' => array(
							'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'email' => array(
							'description' => __( 'Email address.', 'woocommerce' ),
							'type'        => 'string',
							'format'      => 'email',
							'context'     => array( 'view', 'edit' ),
						),
						'phone' => array(
							'description' => __( 'Phone number.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
					),
				),
				'shipping' => array(
					'description' => __( 'Shipping address.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'properties'  => array(
						'first_name' => array(
							'description' => __( 'First name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'last_name' => array(
							'description' => __( 'Last name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'company' => array(
							'description' => __( 'Company name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'address_1' => array(
							'description' => __( 'Address line 1.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'address_2' => array(
							'description' => __( 'Address line 2.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'city' => array(
							'description' => __( 'City name.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'state' => array(
							'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'postcode' => array(
							'description' => __( 'Postal code.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'country' => array(
							'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
					),
				),
				'payment_method' => array(
					'description' => __( 'Payment method ID.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'payment_method_title' => array(
					'description' => __( 'Payment method title.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				'set_paid' => array(
					'description' => __( 'Define if the order is paid. It will set the status to processing and reduce stock items.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'edit' ),
				),
				'transaction_id' => array(
					'description' => __( 'Unique transaction ID.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'customer_ip_address' => array(
					'description' => __( "Customer's IP address.", 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'customer_user_agent' => array(
					'description' => __( 'User agent of the customer.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'created_via' => array(
					'description' => __( 'Shows where the order was created.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'customer_note' => array(
					'description' => __( 'Note left by customer during checkout.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'date_completed' => array(
					'description' => __( "The date the order was completed, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_paid' => array(
					'description' => __( "The date the order was paid, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'cart_hash' => array(
					'description' => __( 'MD5 hash of cart items to ensure orders are not modified.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'line_items' => array(
					'description' => __( 'Line items data.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id' => array(
								'description' => __( 'Item ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'name' => array(
								'description' => __( 'Product name.', 'woocommerce' ),
								'type'        => 'mixed',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'sku' => array(
								'description' => __( 'Product SKU.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'product_id' => array(
								'description' => __( 'Product ID.', 'woocommerce' ),
								'type'        => 'mixed',
								'context'     => array( 'view', 'edit' ),
							),
							'variation_id' => array(
								'description' => __( 'Variation ID, if applicable.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'quantity' => array(
								'description' => __( 'Quantity ordered.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'tax_class' => array(
								'description' => __( 'Tax class of product.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'price' => array(
								'description' => __( 'Product price.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'subtotal' => array(
								'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'subtotal_tax' => array(
								'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'total' => array(
								'description' => __( 'Line total (after discounts).', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'total_tax' => array(
								'description' => __( 'Line total tax (after discounts).', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'taxes' => array(
								'description' => __( 'Line taxes.', 'woocommerce' ),
								'type'        => 'array',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
								'items'       => array(
									'type'       => 'object',
									'properties' => array(
										'id' => array(
											'description' => __( 'Tax rate ID.', 'woocommerce' ),
											'type'        => 'integer',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
										'total' => array(
											'description' => __( 'Tax total.', 'woocommerce' ),
											'type'        => 'string',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
										'subtotal' => array(
											'description' => __( 'Tax subtotal.', 'woocommerce' ),
											'type'        => 'string',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
									),
								),
							),
							'meta' => array(
								'description' => __( 'Line item meta data.', 'woocommerce' ),
								'type'        => 'array',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
								'items'       => array(
									'type'       => 'object',
									'properties' => array(
										'key' => array(
											'description' => __( 'Meta key.', 'woocommerce' ),
											'type'        => 'string',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
										'label' => array(
											'description' => __( 'Meta label.', 'woocommerce' ),
											'type'        => 'string',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
										'value' => array(
											'description' => __( 'Meta value.', 'woocommerce' ),
											'type'        => 'mixed',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
									),
								),
							),
						),
					),
				),
				'tax_lines' => array(
					'description' => __( 'Tax lines data.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id' => array(
								'description' => __( 'Item ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'rate_code' => array(
								'description' => __( 'Tax rate code.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'rate_id' => array(
								'description' => __( 'Tax rate ID.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'label' => array(
								'description' => __( 'Tax rate label.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'compound' => array(
								'description' => __( 'Show if is a compound tax rate.', 'woocommerce' ),
								'type'        => 'boolean',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'tax_total' => array(
								'description' => __( 'Tax total (not including shipping taxes).', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'shipping_tax_total' => array(
								'description' => __( 'Shipping tax total.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
						),
					),
				),
				'shipping_lines' => array(
					'description' => __( 'Shipping lines data.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id' => array(
								'description' => __( 'Item ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'method_title' => array(
								'description' => __( 'Shipping method name.', 'woocommerce' ),
								'type'        => 'mixed',
								'context'     => array( 'view', 'edit' ),
							),
							'method_id' => array(
								'description' => __( 'Shipping method ID.', 'woocommerce' ),
								'type'        => 'mixed',
								'context'     => array( 'view', 'edit' ),
							),
							'total' => array(
								'description' => __( 'Line total (after discounts).', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'total_tax' => array(
								'description' => __( 'Line total tax (after discounts).', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'taxes' => array(
								'description' => __( 'Line taxes.', 'woocommerce' ),
								'type'        => 'array',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
								'items'       => array(
									'type'       => 'object',
									'properties' => array(
										'id' => array(
											'description' => __( 'Tax rate ID.', 'woocommerce' ),
											'type'        => 'integer',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
										'total' => array(
											'description' => __( 'Tax total.', 'woocommerce' ),
											'type'        => 'string',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
									),
								),
							),
						),
					),
				),
				'fee_lines' => array(
					'description' => __( 'Fee lines data.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id' => array(
								'description' => __( 'Item ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'name' => array(
								'description' => __( 'Fee name.', 'woocommerce' ),
								'type'        => 'mixed',
								'context'     => array( 'view', 'edit' ),
							),
							'tax_class' => array(
								'description' => __( 'Tax class of fee.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'tax_status' => array(
								'description' => __( 'Tax status of fee.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'enum'        => array( 'taxable', 'none' ),
							),
							'total' => array(
								'description' => __( 'Line total (after discounts).', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'total_tax' => array(
								'description' => __( 'Line total tax (after discounts).', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'taxes' => array(
								'description' => __( 'Line taxes.', 'woocommerce' ),
								'type'        => 'array',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
								'items'       => array(
									'type'       => 'object',
									'properties' => array(
										'id' => array(
											'description' => __( 'Tax rate ID.', 'woocommerce' ),
											'type'        => 'integer',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
										'total' => array(
											'description' => __( 'Tax total.', 'woocommerce' ),
											'type'        => 'string',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
										'subtotal' => array(
											'description' => __( 'Tax subtotal.', 'woocommerce' ),
											'type'        => 'string',
											'context'     => array( 'view', 'edit' ),
											'readonly'    => true,
										),
									),
								),
							),
						),
					),
				),
				'coupon_lines' => array(
					'description' => __( 'Coupons line data.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id' => array(
								'description' => __( 'Item ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'code' => array(
								'description' => __( 'Coupon code.', 'woocommerce' ),
								'type'        => 'mixed',
								'context'     => array( 'view', 'edit' ),
							),
							'discount' => array(
								'description' => __( 'Discount total.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'discount_tax' => array(
								'description' => __( 'Discount total tax.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
						),
					),
				),
				'refunds' => array(
					'description' => __( 'List of refunds.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id' => array(
								'description' => __( 'Refund ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'reason' => array(
								'description' => __( 'Refund reason.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'total' => array(
								'description' => __( 'Refund total.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
						),
					),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params = parent::get_collection_params();

		$params['status'] = array(
			'default'           => 'any',
			'description'       => __( 'Limit result set to orders assigned a specific status.', 'woocommerce' ),
			'type'              => 'string',
			'enum'              => array_merge( array( 'any' ), $this->get_order_statuses() ),
			'sanitize_callback' => 'sanitize_key',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['customer'] = array(
			'description'       => __( 'Limit result set to orders assigned a specific customer.', 'woocommerce' ),
			'type'              => 'integer',
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['product'] = array(
			'description'       => __( 'Limit result set to orders assigned a specific product.', 'woocommerce' ),
			'type'              => 'integer',
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['dp'] = array(
			'default'           => wc_get_price_decimals(),
			'description'       => __( 'Number of decimal points to use in each resource.', 'woocommerce' ),
			'type'              => 'integer',
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}
}
rest-api/Controllers/Version1/class-wc-rest-report-top-sellers-v1-controller.php000064400000011153151336044760024003 0ustar00<?php
/**
 * REST API Reports controller
 *
 * Handles requests to the reports/top_sellers endpoint.
 *
 * @author   WooThemes
 * @category API
 * @package WooCommerce\RestApi
 * @since    3.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * REST API Report Top Sellers controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Report_Sales_V1_Controller
 */
class WC_REST_Report_Top_Sellers_V1_Controller extends WC_REST_Report_Sales_V1_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v1';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'reports/top_sellers';

	/**
	 * Get sales reports.
	 *
	 * @param WP_REST_Request $request
	 * @return array|WP_Error
	 */
	public function get_items( $request ) {
		// Set date filtering.
		$filter = array(
			'period'   => $request['period'],
			'date_min' => $request['date_min'],
			'date_max' => $request['date_max'],
		);
		$this->setup_report( $filter );

		$report_data = $this->report->get_order_report_data( array(
			'data' => array(
				'_product_id' => array(
					'type'            => 'order_item_meta',
					'order_item_type' => 'line_item',
					'function'        => '',
					'name'            => 'product_id',
				),
				'_qty' => array(
					'type'            => 'order_item_meta',
					'order_item_type' => 'line_item',
					'function'        => 'SUM',
					'name'            => 'order_item_qty',
				),
			),
			'order_by'     => 'order_item_qty DESC',
			'group_by'     => 'product_id',
			'limit'        => isset( $filter['limit'] ) ? absint( $filter['limit'] ) : 12,
			'query_type'   => 'get_results',
			'filter_range' => true,
		) );

		$top_sellers = array();

		foreach ( $report_data as $item ) {
			$product = wc_get_product( $item->product_id );

			if ( $product ) {
				$top_sellers[] = array(
					'name'      => $product->get_name(),
					'product_id' => (int) $item->product_id,
					'quantity'   => wc_stock_amount( $item->order_item_qty ),
				);
			}
		}

		$data = array();
		foreach ( $top_sellers as $top_seller ) {
			$item   = $this->prepare_item_for_response( (object) $top_seller, $request );
			$data[] = $this->prepare_response_for_collection( $item );
		}

		return rest_ensure_response( $data );
	}

	/**
	 * Prepare a report sales object for serialization.
	 *
	 * @param stdClass $top_seller
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $top_seller, $request ) {
		$data = array(
			'name'       => $top_seller->name,
			'product_id' => $top_seller->product_id,
			'quantity'   => $top_seller->quantity,
		);

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );
		$response->add_links( array(
			'about' => array(
				'href' => rest_url( sprintf( '%s/reports', $this->namespace ) ),
			),
			'product' => array(
				'href' => rest_url( sprintf( '/%s/products/%s', $this->namespace, $top_seller->product_id ) ),
			),
		) );

		/**
		 * Filter a report top sellers returned from the API.
		 *
		 * Allows modification of the report top sellers data right before it is returned.
		 *
		 * @param WP_REST_Response $response   The response object.
		 * @param stdClass         $top_seller The original report object.
		 * @param WP_REST_Request  $request    Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_report_top_sellers', $response, $top_seller, $request );
	}

	/**
	 * Get the Report's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'top_sellers_report',
			'type'       => 'object',
			'properties' => array(
				'name' => array(
					'description' => __( 'Product name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'product_id' => array(
					'description' => __( 'Product ID.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'quantity' => array(
					'description' => __( 'Total number of purchases.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}
rest-api/Controllers/Version1/class-wc-rest-reports-v1-controller.php000064400000011251151336044760021716 0ustar00<?php
/**
 * REST API Reports controller
 *
 * Handles requests to the reports endpoint.
 *
 * @author   WooThemes
 * @category API
 * @package WooCommerce\RestApi
 * @since    3.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * REST API Reports controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_Controller
 */
class WC_REST_Reports_V1_Controller extends WC_REST_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v1';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'reports';

	/**
	 * Register the routes for reports.
	 */
	public function register_routes() {
		register_rest_route( $this->namespace, '/' . $this->rest_base, array(
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_items' ),
				'permission_callback' => array( $this, 'get_items_permissions_check' ),
				'args'                => $this->get_collection_params(),
			),
			'schema' => array( $this, 'get_public_item_schema' ),
		) );
	}

	/**
	 * Check whether a given request has permission to read reports.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function get_items_permissions_check( $request ) {
		if ( ! wc_rest_check_manager_permissions( 'reports', 'read' ) ) {
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
		}

		return true;
	}

	/**
	 * Get reports list.
	 *
	 * @since 3.5.0
	 * @return array
	 */
	protected function get_reports() {
		return array(
			array(
				'slug'        => 'sales',
				'description' => __( 'List of sales reports.', 'woocommerce' ),
			),
			array(
				'slug'        => 'top_sellers',
				'description' => __( 'List of top sellers products.', 'woocommerce' ),
			),
		);
	}

	/**
	 * Get all reports.
	 *
	 * @param WP_REST_Request $request
	 * @return array|WP_Error
	 */
	public function get_items( $request ) {
		$data    = array();
		$reports = $this->get_reports();

		foreach ( $reports as $report ) {
			$item   = $this->prepare_item_for_response( (object) $report, $request );
			$data[] = $this->prepare_response_for_collection( $item );
		}

		return rest_ensure_response( $data );
	}

	/**
	 * Prepare a report object for serialization.
	 *
	 * @param stdClass $report Report data.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response Response data.
	 */
	public function prepare_item_for_response( $report, $request ) {
		$data = array(
			'slug'        => $report->slug,
			'description' => $report->description,
		);

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data = $this->add_additional_fields_to_object( $data, $request );
		$data = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );
		$response->add_links( array(
			'self' => array(
				'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $report->slug ) ),
			),
			'collection' => array(
				'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
			),
		) );

		/**
		 * Filter a report returned from the API.
		 *
		 * Allows modification of the report data right before it is returned.
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param object           $report   The original report object.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'woocommerce_rest_prepare_report', $response, $report, $request );
	}

	/**
	 * Get the Report's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'report',
			'type'       => 'object',
			'properties' => array(
				'slug' => array(
					'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
				'description' => array(
					'description' => __( 'A human-readable description of the resource.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for collections.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		return array(
			'context' => $this->get_context_param( array( 'default' => 'view' ) ),
		);
	}
}
rest-api/Server.php000064400000021126151336044760010262 0ustar00<?php
/**
 * Initialize this version of the REST API.
 *
 * @package WooCommerce\RestApi
 */

namespace Automattic\WooCommerce\RestApi;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\RestApi\Utilities\SingletonTrait;

/**
 * Class responsible for loading the REST API and all REST API namespaces.
 */
class Server {
	use SingletonTrait;

	/**
	 * REST API namespaces and endpoints.
	 *
	 * @var array
	 */
	protected $controllers = array();

	/**
	 * Hook into WordPress ready to init the REST API as needed.
	 */
	public function init() {
		add_action( 'rest_api_init', array( $this, 'register_rest_routes' ), 10 );
	}

	/**
	 * Register REST API routes.
	 */
	public function register_rest_routes() {
		foreach ( $this->get_rest_namespaces() as $namespace => $controllers ) {
			foreach ( $controllers as $controller_name => $controller_class ) {
				$this->controllers[ $namespace ][ $controller_name ] = new $controller_class();
				$this->controllers[ $namespace ][ $controller_name ]->register_routes();
			}
		}
	}

	/**
	 * Get API namespaces - new namespaces should be registered here.
	 *
	 * @return array List of Namespaces and Main controller classes.
	 */
	protected function get_rest_namespaces() {
		return apply_filters(
			'woocommerce_rest_api_get_rest_namespaces',
			array(
				'wc/v1'        => $this->get_v1_controllers(),
				'wc/v2'        => $this->get_v2_controllers(),
				'wc/v3'        => $this->get_v3_controllers(),
				'wc-telemetry' => $this->get_telemetry_controllers(),
			)
		);
	}

	/**
	 * List of controllers in the wc/v1 namespace.
	 *
	 * @return array
	 */
	protected function get_v1_controllers() {
		return array(
			'coupons'                  => 'WC_REST_Coupons_V1_Controller',
			'customer-downloads'       => 'WC_REST_Customer_Downloads_V1_Controller',
			'customers'                => 'WC_REST_Customers_V1_Controller',
			'order-notes'              => 'WC_REST_Order_Notes_V1_Controller',
			'order-refunds'            => 'WC_REST_Order_Refunds_V1_Controller',
			'orders'                   => 'WC_REST_Orders_V1_Controller',
			'product-attribute-terms'  => 'WC_REST_Product_Attribute_Terms_V1_Controller',
			'product-attributes'       => 'WC_REST_Product_Attributes_V1_Controller',
			'product-categories'       => 'WC_REST_Product_Categories_V1_Controller',
			'product-reviews'          => 'WC_REST_Product_Reviews_V1_Controller',
			'product-shipping-classes' => 'WC_REST_Product_Shipping_Classes_V1_Controller',
			'product-tags'             => 'WC_REST_Product_Tags_V1_Controller',
			'products'                 => 'WC_REST_Products_V1_Controller',
			'reports-sales'            => 'WC_REST_Report_Sales_V1_Controller',
			'reports-top-sellers'      => 'WC_REST_Report_Top_Sellers_V1_Controller',
			'reports'                  => 'WC_REST_Reports_V1_Controller',
			'tax-classes'              => 'WC_REST_Tax_Classes_V1_Controller',
			'taxes'                    => 'WC_REST_Taxes_V1_Controller',
			'webhooks'                 => 'WC_REST_Webhooks_V1_Controller',
			'webhook-deliveries'       => 'WC_REST_Webhook_Deliveries_V1_Controller',
		);
	}

	/**
	 * List of controllers in the wc/v2 namespace.
	 *
	 * @return array
	 */
	protected function get_v2_controllers() {
		return array(
			'coupons'                  => 'WC_REST_Coupons_V2_Controller',
			'customer-downloads'       => 'WC_REST_Customer_Downloads_V2_Controller',
			'customers'                => 'WC_REST_Customers_V2_Controller',
			'network-orders'           => 'WC_REST_Network_Orders_V2_Controller',
			'order-notes'              => 'WC_REST_Order_Notes_V2_Controller',
			'order-refunds'            => 'WC_REST_Order_Refunds_V2_Controller',
			'orders'                   => 'WC_REST_Orders_V2_Controller',
			'product-attribute-terms'  => 'WC_REST_Product_Attribute_Terms_V2_Controller',
			'product-attributes'       => 'WC_REST_Product_Attributes_V2_Controller',
			'product-categories'       => 'WC_REST_Product_Categories_V2_Controller',
			'product-reviews'          => 'WC_REST_Product_Reviews_V2_Controller',
			'product-shipping-classes' => 'WC_REST_Product_Shipping_Classes_V2_Controller',
			'product-tags'             => 'WC_REST_Product_Tags_V2_Controller',
			'products'                 => 'WC_REST_Products_V2_Controller',
			'product-variations'       => 'WC_REST_Product_Variations_V2_Controller',
			'reports-sales'            => 'WC_REST_Report_Sales_V2_Controller',
			'reports-top-sellers'      => 'WC_REST_Report_Top_Sellers_V2_Controller',
			'reports'                  => 'WC_REST_Reports_V2_Controller',
			'settings'                 => 'WC_REST_Settings_V2_Controller',
			'settings-options'         => 'WC_REST_Setting_Options_V2_Controller',
			'shipping-zones'           => 'WC_REST_Shipping_Zones_V2_Controller',
			'shipping-zone-locations'  => 'WC_REST_Shipping_Zone_Locations_V2_Controller',
			'shipping-zone-methods'    => 'WC_REST_Shipping_Zone_Methods_V2_Controller',
			'tax-classes'              => 'WC_REST_Tax_Classes_V2_Controller',
			'taxes'                    => 'WC_REST_Taxes_V2_Controller',
			'webhooks'                 => 'WC_REST_Webhooks_V2_Controller',
			'webhook-deliveries'       => 'WC_REST_Webhook_Deliveries_V2_Controller',
			'system-status'            => 'WC_REST_System_Status_V2_Controller',
			'system-status-tools'      => 'WC_REST_System_Status_Tools_V2_Controller',
			'shipping-methods'         => 'WC_REST_Shipping_Methods_V2_Controller',
			'payment-gateways'         => 'WC_REST_Payment_Gateways_V2_Controller',
		);
	}

	/**
	 * List of controllers in the wc/v3 namespace.
	 *
	 * @return array
	 */
	protected function get_v3_controllers() {
		return array(
			'coupons'                  => 'WC_REST_Coupons_Controller',
			'customer-downloads'       => 'WC_REST_Customer_Downloads_Controller',
			'customers'                => 'WC_REST_Customers_Controller',
			'network-orders'           => 'WC_REST_Network_Orders_Controller',
			'order-notes'              => 'WC_REST_Order_Notes_Controller',
			'order-refunds'            => 'WC_REST_Order_Refunds_Controller',
			'orders'                   => 'WC_REST_Orders_Controller',
			'product-attribute-terms'  => 'WC_REST_Product_Attribute_Terms_Controller',
			'product-attributes'       => 'WC_REST_Product_Attributes_Controller',
			'product-categories'       => 'WC_REST_Product_Categories_Controller',
			'product-reviews'          => 'WC_REST_Product_Reviews_Controller',
			'product-shipping-classes' => 'WC_REST_Product_Shipping_Classes_Controller',
			'product-tags'             => 'WC_REST_Product_Tags_Controller',
			'products'                 => 'WC_REST_Products_Controller',
			'product-variations'       => 'WC_REST_Product_Variations_Controller',
			'reports-sales'            => 'WC_REST_Report_Sales_Controller',
			'reports-top-sellers'      => 'WC_REST_Report_Top_Sellers_Controller',
			'reports-orders-totals'    => 'WC_REST_Report_Orders_Totals_Controller',
			'reports-products-totals'  => 'WC_REST_Report_Products_Totals_Controller',
			'reports-customers-totals' => 'WC_REST_Report_Customers_Totals_Controller',
			'reports-coupons-totals'   => 'WC_REST_Report_Coupons_Totals_Controller',
			'reports-reviews-totals'   => 'WC_REST_Report_Reviews_Totals_Controller',
			'reports'                  => 'WC_REST_Reports_Controller',
			'settings'                 => 'WC_REST_Settings_Controller',
			'settings-options'         => 'WC_REST_Setting_Options_Controller',
			'shipping-zones'           => 'WC_REST_Shipping_Zones_Controller',
			'shipping-zone-locations'  => 'WC_REST_Shipping_Zone_Locations_Controller',
			'shipping-zone-methods'    => 'WC_REST_Shipping_Zone_Methods_Controller',
			'tax-classes'              => 'WC_REST_Tax_Classes_Controller',
			'taxes'                    => 'WC_REST_Taxes_Controller',
			'webhooks'                 => 'WC_REST_Webhooks_Controller',
			'system-status'            => 'WC_REST_System_Status_Controller',
			'system-status-tools'      => 'WC_REST_System_Status_Tools_Controller',
			'shipping-methods'         => 'WC_REST_Shipping_Methods_Controller',
			'payment-gateways'         => 'WC_REST_Payment_Gateways_Controller',
			'data'                     => 'WC_REST_Data_Controller',
			'data-continents'          => 'WC_REST_Data_Continents_Controller',
			'data-countries'           => 'WC_REST_Data_Countries_Controller',
			'data-currencies'          => 'WC_REST_Data_Currencies_Controller',
		);
	}

	/**
	 * List of controllers in the telemetry namespace.
	 *
	 * @return array
	 */
	protected function get_telemetry_controllers() {
		return array(
			'tracker' => 'WC_REST_Telemetry_Controller',
		);
	}

	/**
	 * Return the path to the package.
	 *
	 * @return string
	 */
	public static function get_path() {
		return dirname( __DIR__ );
	}
}
rest-api/Utilities/SingletonTrait.php000064400000001537151336044760013741 0ustar00<?php
/**
 * Singleton class trait.
 *
 * @package WooCommerce\Utilities
 */

namespace Automattic\WooCommerce\RestApi\Utilities;

/**
 * Singleton trait.
 */
trait SingletonTrait {
	/**
	 * The single instance of the class.
	 *
	 * @var object
	 */
	protected static $instance = null;

	/**
	 * Constructor
	 *
	 * @return void
	 */
	protected function __construct() {}

	/**
	 * Get class instance.
	 *
	 * @return object Instance.
	 */
	final public static function instance() {
		if ( null === static::$instance ) {
			static::$instance = new static();
		}
		return static::$instance;
	}

	/**
	 * Prevent cloning.
	 */
	private function __clone() {}

	/**
	 * Prevent unserializing.
	 */
	final public function __wakeup() {
		wc_doing_it_wrong( __FUNCTION__, __( 'Unserializing instances of this class is forbidden.', 'woocommerce' ), '4.6' );
		die();
	}
}
rest-api/Utilities/ImageAttachment.php000064400000003705151336044760014025 0ustar00<?php
/**
 * Helper to upload files via the REST API.
 *
 * @package WooCommerce\Utilities
 */

namespace Automattic\WooCommerce\RestApi\Utilities;

/**
 * ImageAttachment class.
 */
class ImageAttachment {

	/**
	 * Attachment ID.
	 *
	 * @var integer
	 */
	public $id = 0;

	/**
	 * Object attached to.
	 *
	 * @var integer
	 */
	public $object_id = 0;

	/**
	 * Constructor.
	 *
	 * @param integer $id Attachment ID.
	 * @param integer $object_id Object ID.
	 */
	public function __construct( $id = 0, $object_id = 0 ) {
		$this->id        = (int) $id;
		$this->object_id = (int) $object_id;
	}

	/**
	 * Upload an attachment file.
	 *
	 * @throws \WC_REST_Exception REST API exceptions.
	 * @param string $src URL to file.
	 */
	public function upload_image_from_src( $src ) {
		$upload = wc_rest_upload_image_from_url( esc_url_raw( $src ) );

		if ( is_wp_error( $upload ) ) {
			if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $this->object_id, $images ) ) {
				throw new \WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 );
			} else {
				return;
			}
		}

		$this->id = wc_rest_set_uploaded_image_as_attachment( $upload, $this->object_id );

		if ( ! wp_attachment_is_image( $this->id ) ) {
			/* translators: %s: image ID */
			throw new \WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $this->id ), 400 );
		}
	}

	/**
	 * Update attachment alt text.
	 *
	 * @param string $text Text to set.
	 */
	public function update_alt_text( $text ) {
		if ( ! $this->id ) {
			return;
		}
		update_post_meta( $this->id, '_wp_attachment_image_alt', wc_clean( $text ) );
	}

	/**
	 * Update attachment name.
	 *
	 * @param string $text Text to set.
	 */
	public function update_name( $text ) {
		if ( ! $this->id ) {
			return;
		}
		wp_update_post(
			array(
				'ID'         => $this->id,
				'post_title' => $text,
			)
		);
	}
}
rest-api/Package.php000064400000003027151336044760010347 0ustar00<?php
/**
 * Deprecated notice: This class is deprecated as of version 4.5.0. WooCommerce API is now part of core and not packaged seperately.
 *
 * Returns information about the package and handles init.
 *
 * @package WooCommerce\RestApi
 */

namespace Automattic\WooCommerce\RestApi;

defined( 'ABSPATH' ) || exit;

/**
 * Main package class.
 *
 * @deprecated Use \Automattic\WooCommerce\RestApi\Server directly.
 */
class Package {

	/**
	 * Version.
	 *
	 * @deprecated since 4.5.0. This tracks WooCommerce version now.
	 * @var string
	 */
	const VERSION = WC_VERSION;

	/**
	 * Init the package - load the REST API Server class.
	 *
	 * @deprecated since 4.5.0. Directly call Automattic\WooCommerce\RestApi\Server::instance()->init()
	 */
	public static function init() {
		wc_deprecated_function( 'Automattic\WooCommerce\RestApi\Server::instance()->init()', '4.5.0' );
		\Automattic\WooCommerce\RestApi\Server::instance()->init();
	}

	/**
	 * Return the version of the package.
	 *
	 * @deprecated since 4.5.0. This tracks WooCommerce version now.
	 * @return string
	 */
	public static function get_version() {
		wc_deprecated_function( 'WC()->version', '4.5.0' );
		return WC()->version;
	}

	/**
	 * Return the path to the package.
	 *
	 * @deprecated since 4.5.0. Directly call Automattic\WooCommerce\RestApi\Server::get_path()
	 * @return string
	 */
	public static function get_path() {
		wc_deprecated_function( 'Automattic\WooCommerce\RestApi\Server::get_path()', '4.5.0' );
		return \Automattic\WooCommerce\RestApi\Server::get_path();
	}
}
wc-term-functions.php000064400000051163151336044760010660 0ustar00<?php
/**
 * WooCommerce Terms
 *
 * Functions for handling terms/term meta.
 *
 * @package WooCommerce\Functions
 * @version 2.1.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Change get terms defaults for attributes to order by the sorting setting, or default to menu_order for sortable taxonomies.
 *
 * @since 3.6.0 Sorting options are now set as the default automatically, so you no longer have to request to orderby menu_order.
 *
 * @param array $defaults   An array of default get_terms() arguments.
 * @param array $taxonomies An array of taxonomies.
 * @return array
 */
function wc_change_get_terms_defaults( $defaults, $taxonomies ) {
	if ( is_array( $taxonomies ) && 1 < count( $taxonomies ) ) {
		return $defaults;
	}
	$taxonomy = is_array( $taxonomies ) ? (string) current( $taxonomies ) : $taxonomies;
	$orderby  = 'name';

	if ( taxonomy_is_product_attribute( $taxonomy ) ) {
		$orderby = wc_attribute_orderby( $taxonomy );
	} elseif ( in_array( $taxonomy, apply_filters( 'woocommerce_sortable_taxonomies', array( 'product_cat' ) ), true ) ) {
		$orderby = 'menu_order';
	}

	// Change defaults. Invalid values will be changed later @see wc_change_pre_get_terms.
	// These are in place so we know if a specific order was requested.
	switch ( $orderby ) {
		case 'menu_order':
		case 'name_num':
		case 'parent':
			$defaults['orderby'] = $orderby;
			break;
	}

	return $defaults;
}
add_filter( 'get_terms_defaults', 'wc_change_get_terms_defaults', 10, 2 );

/**
 * Adds support to get_terms for menu_order argument.
 *
 * @since 3.6.0
 * @param WP_Term_Query $terms_query Instance of WP_Term_Query.
 */
function wc_change_pre_get_terms( $terms_query ) {
	$args = &$terms_query->query_vars;

	// Put back valid orderby values.
	if ( 'menu_order' === $args['orderby'] ) {
		$args['orderby']               = 'name';
		$args['force_menu_order_sort'] = true;
	}

	if ( 'name_num' === $args['orderby'] ) {
		$args['orderby']            = 'name';
		$args['force_numeric_name'] = true;
	}

	// When COUNTING, disable custom sorting.
	if ( 'count' === $args['fields'] ) {
		return;
	}

	// Support menu_order arg used in previous versions.
	if ( ! empty( $args['menu_order'] ) ) {
		$args['order']                 = 'DESC' === strtoupper( $args['menu_order'] ) ? 'DESC' : 'ASC';
		$args['force_menu_order_sort'] = true;
	}

	if ( ! empty( $args['force_menu_order_sort'] ) ) {
		$args['orderby']  = 'meta_value_num';
		$args['meta_key'] = 'order'; // phpcs:ignore
		$terms_query->meta_query->parse_query_vars( $args );
	}
}
add_action( 'pre_get_terms', 'wc_change_pre_get_terms', 10, 1 );

/**
 * Adjust term query to handle custom sorting parameters.
 *
 * @param array $clauses    Clauses.
 * @param array $taxonomies Taxonomies.
 * @param array $args       Arguments.
 * @return array
 */
function wc_terms_clauses( $clauses, $taxonomies, $args ) {
	global $wpdb;

	// No need to filter when counting.
	if ( strpos( $clauses['fields'], 'COUNT(*)' ) !== false ) {
		return $clauses;
	}

	// Force numeric sort if using name_num custom sorting param.
	if ( ! empty( $args['force_numeric_name'] ) ) {
		$clauses['orderby'] = str_replace( 'ORDER BY t.name', 'ORDER BY t.name+0', $clauses['orderby'] );
	}

	// For sorting, force left join in case order meta is missing.
	if ( ! empty( $args['force_menu_order_sort'] ) ) {
		$clauses['join']    = str_replace( "INNER JOIN {$wpdb->termmeta} ON ( t.term_id = {$wpdb->termmeta}.term_id )", "LEFT JOIN {$wpdb->termmeta} ON ( t.term_id = {$wpdb->termmeta}.term_id AND {$wpdb->termmeta}.meta_key='order')", $clauses['join'] );
		$clauses['where']   = str_replace( "{$wpdb->termmeta}.meta_key = 'order'", "( {$wpdb->termmeta}.meta_key = 'order' OR {$wpdb->termmeta}.meta_key IS NULL )", $clauses['where'] );
		$clauses['orderby'] = 'DESC' === $args['order'] ? str_replace( 'meta_value+0', 'meta_value+0 DESC, t.name', $clauses['orderby'] ) : str_replace( 'meta_value+0', 'meta_value+0 ASC, t.name', $clauses['orderby'] );
	}

	return $clauses;
}
add_filter( 'terms_clauses', 'wc_terms_clauses', 99, 3 );

/**
 * Helper to get cached object terms and filter by field using wp_list_pluck().
 * Works as a cached alternative for wp_get_post_terms() and wp_get_object_terms().
 *
 * @since  3.0.0
 * @param  int    $object_id Object ID.
 * @param  string $taxonomy  Taxonomy slug.
 * @param  string $field     Field name.
 * @param  string $index_key Index key name.
 * @return array
 */
function wc_get_object_terms( $object_id, $taxonomy, $field = null, $index_key = null ) {
	// Test if terms exists. get_the_terms() return false when it finds no terms.
	$terms = get_the_terms( $object_id, $taxonomy );

	if ( ! $terms || is_wp_error( $terms ) ) {
		return array();
	}

	return is_null( $field ) ? $terms : wp_list_pluck( $terms, $field, $index_key );
}

/**
 * Cached version of wp_get_post_terms().
 * This is a private function (internal use ONLY).
 *
 * @since  3.0.0
 * @param  int    $product_id Product ID.
 * @param  string $taxonomy   Taxonomy slug.
 * @param  array  $args       Query arguments.
 * @return array
 */
function _wc_get_cached_product_terms( $product_id, $taxonomy, $args = array() ) {
	$cache_key   = 'wc_' . $taxonomy . md5( wp_json_encode( $args ) );
	$cache_group = WC_Cache_Helper::get_cache_prefix( 'product_' . $product_id ) . $product_id;
	$terms       = wp_cache_get( $cache_key, $cache_group );

	if ( false !== $terms ) {
		return $terms;
	}

	$terms = wp_get_post_terms( $product_id, $taxonomy, $args );

	wp_cache_add( $cache_key, $terms, $cache_group );

	return $terms;
}

/**
 * Wrapper used to get terms for a product.
 *
 * @param  int    $product_id Product ID.
 * @param  string $taxonomy   Taxonomy slug.
 * @param  array  $args       Query arguments.
 * @return array
 */
function wc_get_product_terms( $product_id, $taxonomy, $args = array() ) {
	if ( ! taxonomy_exists( $taxonomy ) ) {
		return array();
	}

	return apply_filters( 'woocommerce_get_product_terms', _wc_get_cached_product_terms( $product_id, $taxonomy, $args ), $product_id, $taxonomy, $args );
}

/**
 * Sort by name (numeric).
 *
 * @param  WP_Post $a First item to compare.
 * @param  WP_Post $b Second item to compare.
 * @return int
 */
function _wc_get_product_terms_name_num_usort_callback( $a, $b ) {
	$a_name = (float) $a->name;
	$b_name = (float) $b->name;

	if ( abs( $a_name - $b_name ) < 0.001 ) {
		return 0;
	}

	return ( $a_name < $b_name ) ? -1 : 1;
}

/**
 * Sort by parent.
 *
 * @param  WP_Post $a First item to compare.
 * @param  WP_Post $b Second item to compare.
 * @return int
 */
function _wc_get_product_terms_parent_usort_callback( $a, $b ) {
	if ( $a->parent === $b->parent ) {
		return 0;
	}
	return ( $a->parent < $b->parent ) ? 1 : -1;
}

/**
 * WooCommerce Dropdown categories.
 *
 * @param array $args Args to control display of dropdown.
 */
function wc_product_dropdown_categories( $args = array() ) {
	global $wp_query;

	$args = wp_parse_args(
		$args,
		array(
			'pad_counts'         => 1,
			'show_count'         => 1,
			'hierarchical'       => 1,
			'hide_empty'         => 1,
			'show_uncategorized' => 1,
			'orderby'            => 'name',
			'selected'           => isset( $wp_query->query_vars['product_cat'] ) ? $wp_query->query_vars['product_cat'] : '',
			'show_option_none'   => __( 'Select a category', 'woocommerce' ),
			'option_none_value'  => '',
			'value_field'        => 'slug',
			'taxonomy'           => 'product_cat',
			'name'               => 'product_cat',
			'class'              => 'dropdown_product_cat',
		)
	);

	if ( 'order' === $args['orderby'] ) {
		$args['orderby']  = 'meta_value_num';
		$args['meta_key'] = 'order'; // phpcs:ignore
	}

	wp_dropdown_categories( $args );
}

/**
 * Custom walker for Product Categories.
 *
 * Previously used by wc_product_dropdown_categories, but wp_dropdown_categories has been fixed in core.
 *
 * @param mixed ...$args Variable number of parameters to be passed to the walker.
 * @return mixed
 */
function wc_walk_category_dropdown_tree( ...$args ) {
	if ( ! class_exists( 'WC_Product_Cat_Dropdown_Walker', false ) ) {
		include_once WC()->plugin_path() . '/includes/walkers/class-wc-product-cat-dropdown-walker.php';
	}

	// The user's options are the third parameter.
	if ( empty( $args[2]['walker'] ) || ! is_a( $args[2]['walker'], 'Walker' ) ) {
		$walker = new WC_Product_Cat_Dropdown_Walker();
	} else {
		$walker = $args[2]['walker'];
	}

	return $walker->walk( ...$args );
}

/**
 * Migrate data from WC term meta to WP term meta.
 *
 * When the database is updated to support term meta, migrate WC term meta data across.
 * We do this when the new version is >= 34370, and the old version is < 34370 (34370 is when term meta table was added).
 *
 * @param string $wp_db_version The new $wp_db_version.
 * @param string $wp_current_db_version The old (current) $wp_db_version.
 */
function wc_taxonomy_metadata_migrate_data( $wp_db_version, $wp_current_db_version ) {
	if ( $wp_db_version >= 34370 && $wp_current_db_version < 34370 ) {
		global $wpdb;
		if ( $wpdb->query( "INSERT INTO {$wpdb->termmeta} ( term_id, meta_key, meta_value ) SELECT woocommerce_term_id, meta_key, meta_value FROM {$wpdb->prefix}woocommerce_termmeta;" ) ) {
			$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}woocommerce_termmeta" );
		}
	}
}
add_action( 'wp_upgrade', 'wc_taxonomy_metadata_migrate_data', 10, 2 );

/**
 * Move a term before the a given element of its hierarchy level.
 *
 * @param int    $the_term Term ID.
 * @param int    $next_id  The id of the next sibling element in save hierarchy level.
 * @param string $taxonomy Taxnomy.
 * @param int    $index    Term index (default: 0).
 * @param mixed  $terms    List of terms. (default: null).
 * @return int
 */
function wc_reorder_terms( $the_term, $next_id, $taxonomy, $index = 0, $terms = null ) {
	if ( ! $terms ) {
		$terms = get_terms( $taxonomy, 'hide_empty=0&parent=0&menu_order=ASC' );
	}
	if ( empty( $terms ) ) {
		return $index;
	}

	$id = intval( $the_term->term_id );

	$term_in_level = false; // Flag: is our term to order in this level of terms.

	foreach ( $terms as $term ) {
		$term_id = intval( $term->term_id );

		if ( $term_id === $id ) { // Our term to order, we skip.
			$term_in_level = true;
			continue; // Our term to order, we skip.
		}
		// the nextid of our term to order, lets move our term here.
		if ( null !== $next_id && $term_id === $next_id ) {
			$index++;
			$index = wc_set_term_order( $id, $index, $taxonomy, true );
		}

		// Set order.
		$index++;
		$index = wc_set_term_order( $term_id, $index, $taxonomy );

		/**
		 * After a term has had it's order set.
		*/
		do_action( 'woocommerce_after_set_term_order', $term, $index, $taxonomy );

		// If that term has children we walk through them.
		$children = get_terms( $taxonomy, "parent={$term_id}&hide_empty=0&menu_order=ASC" );
		if ( ! empty( $children ) ) {
			$index = wc_reorder_terms( $the_term, $next_id, $taxonomy, $index, $children );
		}
	}

	// No nextid meaning our term is in last position.
	if ( $term_in_level && null === $next_id ) {
		$index = wc_set_term_order( $id, $index + 1, $taxonomy, true );
	}

	return $index;
}

/**
 * Set the sort order of a term.
 *
 * @param int    $term_id   Term ID.
 * @param int    $index     Index.
 * @param string $taxonomy  Taxonomy.
 * @param bool   $recursive Recursive (default: false).
 * @return int
 */
function wc_set_term_order( $term_id, $index, $taxonomy, $recursive = false ) {

	$term_id = (int) $term_id;
	$index   = (int) $index;

	update_term_meta( $term_id, 'order', $index );

	if ( ! $recursive ) {
		return $index;
	}

	$children = get_terms( $taxonomy, "parent=$term_id&hide_empty=0&menu_order=ASC" );

	foreach ( $children as $term ) {
		$index++;
		$index = wc_set_term_order( $term->term_id, $index, $taxonomy, true );
	}

	clean_term_cache( $term_id, $taxonomy );

	return $index;
}

/**
 * Function for recounting product terms, ignoring hidden products.
 *
 * @param array  $terms                       List of terms.
 * @param object $taxonomy                    Taxonomy.
 * @param bool   $callback                    Callback.
 * @param bool   $terms_are_term_taxonomy_ids If terms are from term_taxonomy_id column.
 */
function _wc_term_recount( $terms, $taxonomy, $callback = true, $terms_are_term_taxonomy_ids = true ) {
	global $wpdb;

	/**
	 * Filter to allow/prevent recounting of terms as it could be expensive.
	 * A likely scenario for this is when bulk importing products. We could
	 * then prevent it from recounting per product but instead recount it once
	 * when import is done. Of course this means the import logic has to support this.
	 *
	 * @since 5.2
	 * @param bool
	 */
	if ( ! apply_filters( 'woocommerce_product_recount_terms', '__return_true' ) ) {
		return;
	}

	// Standard callback.
	if ( $callback ) {
		_update_post_term_count( $terms, $taxonomy );
	}

	$exclude_term_ids            = array();
	$product_visibility_term_ids = wc_get_product_visibility_term_ids();

	if ( $product_visibility_term_ids['exclude-from-catalog'] ) {
		$exclude_term_ids[] = $product_visibility_term_ids['exclude-from-catalog'];
	}

	if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && $product_visibility_term_ids['outofstock'] ) {
		$exclude_term_ids[] = $product_visibility_term_ids['outofstock'];
	}

	$query = array(
		'fields' => "
			SELECT COUNT( DISTINCT ID ) FROM {$wpdb->posts} p
		",
		'join'   => '',
		'where'  => "
			WHERE 1=1
			AND p.post_status = 'publish'
			AND p.post_type = 'product'

		",
	);

	if ( count( $exclude_term_ids ) ) {
		$query['join']  .= " LEFT JOIN ( SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( " . implode( ',', array_map( 'absint', $exclude_term_ids ) ) . ' ) ) AS exclude_join ON exclude_join.object_id = p.ID';
		$query['where'] .= ' AND exclude_join.object_id IS NULL';
	}

	// Pre-process term taxonomy ids.
	if ( ! $terms_are_term_taxonomy_ids ) {
		// We passed in an array of TERMS in format id=>parent.
		$terms = array_filter( (array) array_keys( $terms ) );
	} else {
		// If we have term taxonomy IDs we need to get the term ID.
		$term_taxonomy_ids = $terms;
		$terms             = array();
		foreach ( $term_taxonomy_ids as $term_taxonomy_id ) {
			$term    = get_term_by( 'term_taxonomy_id', $term_taxonomy_id, $taxonomy->name );
			$terms[] = $term->term_id;
		}
	}

	// Exit if we have no terms to count.
	if ( empty( $terms ) ) {
		return;
	}

	// Ancestors need counting.
	if ( is_taxonomy_hierarchical( $taxonomy->name ) ) {
		foreach ( $terms as $term_id ) {
			$terms = array_merge( $terms, get_ancestors( $term_id, $taxonomy->name ) );
		}
	}

	// Unique terms only.
	$terms = array_unique( $terms );

	// Count the terms.
	foreach ( $terms as $term_id ) {
		$terms_to_count = array( absint( $term_id ) );

		if ( is_taxonomy_hierarchical( $taxonomy->name ) ) {
			// We need to get the $term's hierarchy so we can count its children too.
			$children = get_term_children( $term_id, $taxonomy->name );

			if ( $children && ! is_wp_error( $children ) ) {
				$terms_to_count = array_unique( array_map( 'absint', array_merge( $terms_to_count, $children ) ) );
			}
		}

		// Generate term query.
		$term_query          = $query;
		$term_query['join'] .= " INNER JOIN ( SELECT object_id FROM {$wpdb->term_relationships} INNER JOIN {$wpdb->term_taxonomy} using( term_taxonomy_id ) WHERE term_id IN ( " . implode( ',', array_map( 'absint', $terms_to_count ) ) . ' ) ) AS include_join ON include_join.object_id = p.ID';

		// Get the count.
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$count = $wpdb->get_var( implode( ' ', $term_query ) );

		// Update the count.
		update_term_meta( $term_id, 'product_count_' . $taxonomy->name, absint( $count ) );
	}

	delete_transient( 'wc_term_counts' );
}

/**
 * Recount terms after the stock amount changes.
 *
 * @param int $product_id Product ID.
 */
function wc_recount_after_stock_change( $product_id ) {
	if ( 'yes' !== get_option( 'woocommerce_hide_out_of_stock_items' ) ) {
		return;
	}

	_wc_recount_terms_by_product( $product_id );
}
add_action( 'woocommerce_product_set_stock_status', 'wc_recount_after_stock_change' );


/**
 * Overrides the original term count for product categories and tags with the product count.
 * that takes catalog visibility into account.
 *
 * @param array        $terms      List of terms.
 * @param string|array $taxonomies Single taxonomy or list of taxonomies.
 * @return array
 */
function wc_change_term_counts( $terms, $taxonomies ) {
	if ( is_admin() || is_ajax() ) {
		return $terms;
	}

	if ( ! isset( $taxonomies[0] ) || ! in_array( $taxonomies[0], apply_filters( 'woocommerce_change_term_counts', array( 'product_cat', 'product_tag' ) ), true ) ) {
		return $terms;
	}

	$o_term_counts = get_transient( 'wc_term_counts' );
	$term_counts   = $o_term_counts;

	foreach ( $terms as &$term ) {
		if ( is_object( $term ) ) {
			$term_counts[ $term->term_id ] = isset( $term_counts[ $term->term_id ] ) ? $term_counts[ $term->term_id ] : get_term_meta( $term->term_id, 'product_count_' . $taxonomies[0], true );

			if ( '' !== $term_counts[ $term->term_id ] ) {
				$term->count = absint( $term_counts[ $term->term_id ] );
			}
		}
	}

	// Update transient.
	if ( $term_counts !== $o_term_counts ) {
		set_transient( 'wc_term_counts', $term_counts, DAY_IN_SECONDS * 30 );
	}

	return $terms;
}
add_filter( 'get_terms', 'wc_change_term_counts', 10, 2 );

/**
 * Return products in a given term, and cache value.
 *
 * To keep in sync, product_count will be cleared on "set_object_terms".
 *
 * @param int    $term_id  Term ID.
 * @param string $taxonomy Taxonomy.
 * @return array
 */
function wc_get_term_product_ids( $term_id, $taxonomy ) {
	$product_ids = get_term_meta( $term_id, 'product_ids', true );

	if ( false === $product_ids || ! is_array( $product_ids ) ) {
		$product_ids = get_objects_in_term( $term_id, $taxonomy );
		update_term_meta( $term_id, 'product_ids', $product_ids );
	}

	return $product_ids;
}

/**
 * When a post is updated and terms recounted (called by _update_post_term_count), clear the ids.
 *
 * @param int    $object_id  Object ID.
 * @param array  $terms      An array of object terms.
 * @param array  $tt_ids     An array of term taxonomy IDs.
 * @param string $taxonomy   Taxonomy slug.
 * @param bool   $append     Whether to append new terms to the old terms.
 * @param array  $old_tt_ids Old array of term taxonomy IDs.
 */
function wc_clear_term_product_ids( $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) {
	foreach ( $old_tt_ids as $term_id ) {
		delete_term_meta( $term_id, 'product_ids' );
	}
	foreach ( $tt_ids as $term_id ) {
		delete_term_meta( $term_id, 'product_ids' );
	}
}
add_action( 'set_object_terms', 'wc_clear_term_product_ids', 10, 6 );

/**
 * Get full list of product visibilty term ids.
 *
 * @since  3.0.0
 * @return int[]
 */
function wc_get_product_visibility_term_ids() {
	if ( ! taxonomy_exists( 'product_visibility' ) ) {
		wc_doing_it_wrong( __FUNCTION__, 'wc_get_product_visibility_term_ids should not be called before taxonomies are registered (woocommerce_after_register_post_type action).', '3.1' );
		return array();
	}
	return array_map(
		'absint',
		wp_parse_args(
			wp_list_pluck(
				get_terms(
					array(
						'taxonomy'   => 'product_visibility',
						'hide_empty' => false,
					)
				),
				'term_taxonomy_id',
				'name'
			),
			array(
				'exclude-from-catalog' => 0,
				'exclude-from-search'  => 0,
				'featured'             => 0,
				'outofstock'           => 0,
				'rated-1'              => 0,
				'rated-2'              => 0,
				'rated-3'              => 0,
				'rated-4'              => 0,
				'rated-5'              => 0,
			)
		)
	);
}

/**
 * Recounts all terms.
 *
 * @since 5.2
 * @return void
 */
function wc_recount_all_terms() {
	$product_cats = get_terms(
		'product_cat',
		array(
			'hide_empty' => false,
			'fields'     => 'id=>parent',
		)
	);
	_wc_term_recount( $product_cats, get_taxonomy( 'product_cat' ), true, false );
	$product_tags = get_terms(
		'product_tag',
		array(
			'hide_empty' => false,
			'fields'     => 'id=>parent',
		)
	);
	_wc_term_recount( $product_tags, get_taxonomy( 'product_tag' ), true, false );
}

/**
 * Recounts terms by product.
 *
 * @since 5.2
 * @param int $product_id The ID of the product.
 * @return void
 */
function _wc_recount_terms_by_product( $product_id = '' ) {
	if ( empty( $product_id ) ) {
		return;
	}

	$product_terms = get_the_terms( $product_id, 'product_cat' );

	if ( $product_terms ) {
		$product_cats = array();

		foreach ( $product_terms as $term ) {
			$product_cats[ $term->term_id ] = $term->parent;
		}

		_wc_term_recount( $product_cats, get_taxonomy( 'product_cat' ), false, false );
	}

	$product_terms = get_the_terms( $product_id, 'product_tag' );

	if ( $product_terms ) {
		$product_tags = array();

		foreach ( $product_terms as $term ) {
			$product_tags[ $term->term_id ] = $term->parent;
		}

		_wc_term_recount( $product_tags, get_taxonomy( 'product_tag' ), false, false );
	}
}
class-wc-privacy-exporters.php000064400000035304151336044760012513 0ustar00<?php
/**
 * Personal data exporters.
 *
 * @since 3.4.0
 * @package WooCommerce\Classes
 */

defined( 'ABSPATH' ) || exit;

/**
 * WC_Privacy_Exporters Class.
 */
class WC_Privacy_Exporters {
	/**
	 * Finds and exports customer data by email address.
	 *
	 * @since 3.4.0
	 * @param string $email_address The user email address.
	 * @return array An array of personal data in name value pairs
	 */
	public static function customer_data_exporter( $email_address ) {
		$user           = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data.
		$data_to_export = array();

		if ( $user instanceof WP_User ) {
			$customer_personal_data = self::get_customer_personal_data( $user );
			if ( ! empty( $customer_personal_data ) ) {
				$data_to_export[] = array(
					'group_id'          => 'woocommerce_customer',
					'group_label'       => __( 'Customer Data', 'woocommerce' ),
					'group_description' => __( 'User&#8217;s WooCommerce customer data.', 'woocommerce' ),
					'item_id'           => 'user',
					'data'              => $customer_personal_data,
				);
			}
		}

		return array(
			'data' => $data_to_export,
			'done' => true,
		);
	}

	/**
	 * Finds and exports data which could be used to identify a person from WooCommerce data associated with an email address.
	 *
	 * Orders are exported in blocks of 10 to avoid timeouts.
	 *
	 * @since 3.4.0
	 * @param string $email_address The user email address.
	 * @param int    $page  Page.
	 * @return array An array of personal data in name value pairs
	 */
	public static function order_data_exporter( $email_address, $page ) {
		$done           = true;
		$page           = (int) $page;
		$user           = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data.
		$data_to_export = array();
		$order_query    = array(
			'limit'    => 10,
			'page'     => $page,
			'customer' => array( $email_address ),
		);

		if ( $user instanceof WP_User ) {
			$order_query['customer'][] = (int) $user->ID;
		}

		$orders = wc_get_orders( $order_query );

		if ( 0 < count( $orders ) ) {
			foreach ( $orders as $order ) {
				$data_to_export[] = array(
					'group_id'          => 'woocommerce_orders',
					'group_label'       => __( 'Orders', 'woocommerce' ),
					'group_description' => __( 'User&#8217;s WooCommerce orders data.', 'woocommerce' ),
					'item_id'           => 'order-' . $order->get_id(),
					'data'              => self::get_order_personal_data( $order ),
				);
			}
			$done = 10 > count( $orders );
		}

		return array(
			'data' => $data_to_export,
			'done' => $done,
		);
	}

	/**
	 * Finds and exports customer download logs by email address.
	 *
	 * @since 3.4.0
	 * @param string $email_address The user email address.
	 * @param int    $page  Page.
	 * @throws Exception When WC_Data_Store validation fails.
	 * @return array An array of personal data in name value pairs
	 */
	public static function download_data_exporter( $email_address, $page ) {
		$done            = true;
		$page            = (int) $page;
		$user            = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data.
		$data_to_export  = array();
		$downloads_query = array(
			'limit' => 10,
			'page'  => $page,
		);

		if ( $user instanceof WP_User ) {
			$downloads_query['user_id'] = (int) $user->ID;
		} else {
			$downloads_query['user_email'] = $email_address;
		}

		$customer_download_data_store     = WC_Data_Store::load( 'customer-download' );
		$customer_download_log_data_store = WC_Data_Store::load( 'customer-download-log' );
		$downloads                        = $customer_download_data_store->get_downloads( $downloads_query );

		if ( 0 < count( $downloads ) ) {
			foreach ( $downloads as $download ) {
				$data_to_export[] = array(
					'group_id'          => 'woocommerce_downloads',
					/* translators: This is the headline for a list of downloads purchased from the store for a given user. */
					'group_label'       => __( 'Purchased Downloads', 'woocommerce' ),
					'group_description' => __( 'User&#8217;s WooCommerce purchased downloads data.', 'woocommerce' ),
					'item_id'           => 'download-' . $download->get_id(),
					'data'              => self::get_download_personal_data( $download ),
				);

				$download_logs = $customer_download_log_data_store->get_download_logs_for_permission( $download->get_id() );

				foreach ( $download_logs as $download_log ) {
					$data_to_export[] = array(
						'group_id'          => 'woocommerce_download_logs',
						/* translators: This is the headline for a list of access logs for downloads purchased from the store for a given user. */
						'group_label'       => __( 'Access to Purchased Downloads', 'woocommerce' ),
						'group_description' => __( 'User&#8217;s WooCommerce access to purchased downloads data.', 'woocommerce' ),
						'item_id'           => 'download-log-' . $download_log->get_id(),
						'data'              => array(
							array(
								'name'  => __( 'Download ID', 'woocommerce' ),
								'value' => $download_log->get_permission_id(),
							),
							array(
								'name'  => __( 'Timestamp', 'woocommerce' ),
								'value' => $download_log->get_timestamp(),
							),
							array(
								'name'  => __( 'IP Address', 'woocommerce' ),
								'value' => $download_log->get_user_ip_address(),
							),
						),
					);
				}
			}
			$done = 10 > count( $downloads );
		}

		return array(
			'data' => $data_to_export,
			'done' => $done,
		);
	}

	/**
	 * Get personal data (key/value pairs) for a user object.
	 *
	 * @since 3.4.0
	 * @param WP_User $user user object.
	 * @throws Exception If customer cannot be read/found and $data is set to WC_Customer class.
	 * @return array
	 */
	protected static function get_customer_personal_data( $user ) {
		$personal_data = array();
		$customer      = new WC_Customer( $user->ID );

		if ( ! $customer ) {
			return array();
		}

		$props_to_export = apply_filters(
			'woocommerce_privacy_export_customer_personal_data_props',
			array(
				'billing_first_name'  => __( 'Billing First Name', 'woocommerce' ),
				'billing_last_name'   => __( 'Billing Last Name', 'woocommerce' ),
				'billing_company'     => __( 'Billing Company', 'woocommerce' ),
				'billing_address_1'   => __( 'Billing Address 1', 'woocommerce' ),
				'billing_address_2'   => __( 'Billing Address 2', 'woocommerce' ),
				'billing_city'        => __( 'Billing City', 'woocommerce' ),
				'billing_postcode'    => __( 'Billing Postal/Zip Code', 'woocommerce' ),
				'billing_state'       => __( 'Billing State', 'woocommerce' ),
				'billing_country'     => __( 'Billing Country / Region', 'woocommerce' ),
				'billing_phone'       => __( 'Billing Phone Number', 'woocommerce' ),
				'billing_email'       => __( 'Email Address', 'woocommerce' ),
				'shipping_first_name' => __( 'Shipping First Name', 'woocommerce' ),
				'shipping_last_name'  => __( 'Shipping Last Name', 'woocommerce' ),
				'shipping_company'    => __( 'Shipping Company', 'woocommerce' ),
				'shipping_address_1'  => __( 'Shipping Address 1', 'woocommerce' ),
				'shipping_address_2'  => __( 'Shipping Address 2', 'woocommerce' ),
				'shipping_city'       => __( 'Shipping City', 'woocommerce' ),
				'shipping_postcode'   => __( 'Shipping Postal/Zip Code', 'woocommerce' ),
				'shipping_state'      => __( 'Shipping State', 'woocommerce' ),
				'shipping_country'    => __( 'Shipping Country / Region', 'woocommerce' ),
				'shipping_phone'      => __( 'Shipping Phone Number', 'woocommerce' ),
			),
			$customer
		);

		foreach ( $props_to_export as $prop => $description ) {
			$value = '';

			if ( is_callable( array( $customer, 'get_' . $prop ) ) ) {
				$value = $customer->{"get_$prop"}( 'edit' );
			}

			$value = apply_filters( 'woocommerce_privacy_export_customer_personal_data_prop_value', $value, $prop, $customer );

			if ( $value ) {
				$personal_data[] = array(
					'name'  => $description,
					'value' => $value,
				);
			}
		}

		/**
		 * Allow extensions to register their own personal data for this customer for the export.
		 *
		 * @since 3.4.0
		 * @param array    $personal_data Array of name value pairs.
		 * @param WC_Order $order A customer object.
		 */
		$personal_data = apply_filters( 'woocommerce_privacy_export_customer_personal_data', $personal_data, $customer );

		return $personal_data;
	}

	/**
	 * Get personal data (key/value pairs) for an order object.
	 *
	 * @since 3.4.0
	 * @param WC_Order $order Order object.
	 * @return array
	 */
	protected static function get_order_personal_data( $order ) {
		$personal_data   = array();
		$props_to_export = apply_filters(
			'woocommerce_privacy_export_order_personal_data_props',
			array(
				'order_number'               => __( 'Order Number', 'woocommerce' ),
				'date_created'               => __( 'Order Date', 'woocommerce' ),
				'total'                      => __( 'Order Total', 'woocommerce' ),
				'items'                      => __( 'Items Purchased', 'woocommerce' ),
				'customer_ip_address'        => __( 'IP Address', 'woocommerce' ),
				'customer_user_agent'        => __( 'Browser User Agent', 'woocommerce' ),
				'formatted_billing_address'  => __( 'Billing Address', 'woocommerce' ),
				'formatted_shipping_address' => __( 'Shipping Address', 'woocommerce' ),
				'billing_phone'              => __( 'Phone Number', 'woocommerce' ),
				'billing_email'              => __( 'Email Address', 'woocommerce' ),
				'shipping_phone'             => __( 'Shipping Phone Number', 'woocommerce' ),
			),
			$order
		);

		foreach ( $props_to_export as $prop => $name ) {
			$value = '';

			switch ( $prop ) {
				case 'items':
					$item_names = array();
					foreach ( $order->get_items() as $item ) {
						$item_names[] = $item->get_name() . ' x ' . $item->get_quantity();
					}
					$value = implode( ', ', $item_names );
					break;
				case 'date_created':
					$value = wc_format_datetime( $order->get_date_created(), get_option( 'date_format' ) . ', ' . get_option( 'time_format' ) );
					break;
				case 'formatted_billing_address':
				case 'formatted_shipping_address':
					$value = preg_replace( '#<br\s*/?>#i', ', ', $order->{"get_$prop"}() );
					break;
				default:
					if ( is_callable( array( $order, 'get_' . $prop ) ) ) {
						$value = $order->{"get_$prop"}();
					}
					break;
			}

			$value = apply_filters( 'woocommerce_privacy_export_order_personal_data_prop', $value, $prop, $order );

			if ( $value ) {
				$personal_data[] = array(
					'name'  => $name,
					'value' => $value,
				);
			}
		}

		// Export meta data.
		$meta_to_export = apply_filters(
			'woocommerce_privacy_export_order_personal_data_meta',
			array(
				'Payer first name'     => __( 'Payer first name', 'woocommerce' ),
				'Payer last name'      => __( 'Payer last name', 'woocommerce' ),
				'Payer PayPal address' => __( 'Payer PayPal address', 'woocommerce' ),
				'Transaction ID'       => __( 'Transaction ID', 'woocommerce' ),
			)
		);

		if ( ! empty( $meta_to_export ) && is_array( $meta_to_export ) ) {
			foreach ( $meta_to_export as $meta_key => $name ) {
				$value = apply_filters( 'woocommerce_privacy_export_order_personal_data_meta_value', $order->get_meta( $meta_key ), $meta_key, $order );

				if ( $value ) {
					$personal_data[] = array(
						'name'  => $name,
						'value' => $value,
					);
				}
			}
		}

		/**
		 * Allow extensions to register their own personal data for this order for the export.
		 *
		 * @since 3.4.0
		 * @param array    $personal_data Array of name value pairs to expose in the export.
		 * @param WC_Order $order An order object.
		 */
		$personal_data = apply_filters( 'woocommerce_privacy_export_order_personal_data', $personal_data, $order );

		return $personal_data;
	}

	/**
	 * Get personal data (key/value pairs) for a download object.
	 *
	 * @since 3.4.0
	 * @param WC_Order $download Download object.
	 * @return array
	 */
	protected static function get_download_personal_data( $download ) {
		$personal_data = array(
			array(
				'name'  => __( 'Download ID', 'woocommerce' ),
				'value' => $download->get_id(),
			),
			array(
				'name'  => __( 'Order ID', 'woocommerce' ),
				'value' => $download->get_order_id(),
			),
			array(
				'name'  => __( 'Product', 'woocommerce' ),
				'value' => get_the_title( $download->get_product_id() ),
			),
			array(
				'name'  => __( 'User email', 'woocommerce' ),
				'value' => $download->get_user_email(),
			),
			array(
				'name'  => __( 'Downloads remaining', 'woocommerce' ),
				'value' => $download->get_downloads_remaining(),
			),
			array(
				'name'  => __( 'Download count', 'woocommerce' ),
				'value' => $download->get_download_count(),
			),
			array(
				'name'  => __( 'Access granted', 'woocommerce' ),
				'value' => gmdate( 'Y-m-d', $download->get_access_granted( 'edit' )->getTimestamp() ),
			),
			array(
				'name'  => __( 'Access expires', 'woocommerce' ),
				'value' => ! is_null( $download->get_access_expires( 'edit' ) ) ? gmdate( 'Y-m-d', $download->get_access_expires( 'edit' )->getTimestamp() ) : null,
			),
		);

		/**
		 * Allow extensions to register their own personal data for this download for the export.
		 *
		 * @since 3.4.0
		 * @param array    $personal_data Array of name value pairs to expose in the export.
		 * @param WC_Order $order An order object.
		 */
		$personal_data = apply_filters( 'woocommerce_privacy_export_download_personal_data', $personal_data, $download );

		return $personal_data;
	}

	/**
	 * Finds and exports payment tokens by email address for a customer.
	 *
	 * @since 3.4.0
	 * @param string $email_address The user email address.
	 * @param int    $page  Page.
	 * @return array An array of personal data in name value pairs
	 */
	public static function customer_tokens_exporter( $email_address, $page ) {
		$user           = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data.
		$data_to_export = array();

		if ( ! $user instanceof WP_User ) {
			return array(
				'data' => $data_to_export,
				'done' => true,
			);
		}

		$tokens = WC_Payment_Tokens::get_tokens(
			array(
				'user_id' => $user->ID,
				'limit'   => 10,
				'page'    => $page,
			)
		);

		if ( 0 < count( $tokens ) ) {
			foreach ( $tokens as $token ) {
				$data_to_export[] = array(
					'group_id'          => 'woocommerce_tokens',
					'group_label'       => __( 'Payment Tokens', 'woocommerce' ),
					'group_description' => __( 'User&#8217;s WooCommerce payment tokens data.', 'woocommerce' ),
					'item_id'           => 'token-' . $token->get_id(),
					'data'              => array(
						array(
							'name'  => __( 'Token', 'woocommerce' ),
							'value' => $token->get_display_name(),
						),
					),
				);
			}
			$done = 10 > count( $tokens );
		} else {
			$done = true;
		}

		return array(
			'data' => $data_to_export,
			'done' => $done,
		);
	}
}
class-wc-payment-tokens.php000064400000013634151336044760011765 0ustar00<?php
/**
 * WooCommerce Payment Tokens
 *
 * An API for storing and managing tokens for gateways and customers.
 *
 * @package WooCommerce\Classes
 * @version 3.0.0
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Payment tokens class.
 */
class WC_Payment_Tokens {

	/**
	 * Gets valid tokens from the database based on user defined criteria.
	 *
	 * @since  2.6.0
	 * @param  array $args Query arguments {
	 *     Array of query parameters.
	 *
	 *     @type string $token_id   Token ID.
	 *     @type string $user_id    User ID.
	 *     @type string $gateway_id Gateway ID.
	 *     @type string $type       Token type.
	 * }
	 * @return WC_Payment_Token[]
	 */
	public static function get_tokens( $args ) {
		$args = wp_parse_args(
			$args,
			array(
				'token_id'   => '',
				'user_id'    => '',
				'gateway_id' => '',
				'type'       => '',
			)
		);

		$data_store    = WC_Data_Store::load( 'payment-token' );
		$token_results = $data_store->get_tokens( $args );
		$tokens        = array();

		if ( ! empty( $token_results ) ) {
			foreach ( $token_results as $token_result ) {
				$_token = self::get( $token_result->token_id, $token_result );
				if ( ! empty( $_token ) ) {
					$tokens[ $token_result->token_id ] = $_token;
				}
			}
		}

		return $tokens;
	}

	/**
	 * Returns an array of payment token objects associated with the passed customer ID.
	 *
	 * @since 2.6.0
	 * @param  int    $customer_id Customer ID.
	 * @param  string $gateway_id  Optional Gateway ID for getting tokens for a specific gateway.
	 * @return WC_Payment_Token[]  Array of token objects.
	 */
	public static function get_customer_tokens( $customer_id, $gateway_id = '' ) {
		if ( $customer_id < 1 ) {
			return array();
		}

		$tokens = self::get_tokens(
			array(
				'user_id'    => $customer_id,
				'gateway_id' => $gateway_id,
			)
		);

		return apply_filters( 'woocommerce_get_customer_payment_tokens', $tokens, $customer_id, $gateway_id );
	}

	/**
	 * Returns a customers default token or NULL if there is no default token.
	 *
	 * @since  2.6.0
	 * @param  int $customer_id Customer ID.
	 * @return WC_Payment_Token|null
	 */
	public static function get_customer_default_token( $customer_id ) {
		if ( $customer_id < 1 ) {
			return null;
		}

		$data_store = WC_Data_Store::load( 'payment-token' );
		$token      = $data_store->get_users_default_token( $customer_id );

		if ( $token ) {
			return self::get( $token->token_id, $token );
		} else {
			return null;
		}
	}

	/**
	 * Returns an array of payment token objects associated with the passed order ID.
	 *
	 * @since 2.6.0
	 * @param int $order_id       Order ID.
	 * @return WC_Payment_Token[] Array of token objects.
	 */
	public static function get_order_tokens( $order_id ) {
		$order = wc_get_order( $order_id );

		if ( ! $order ) {
			return array();
		}

		$token_ids = $order->get_payment_tokens();

		if ( empty( $token_ids ) ) {
			return array();
		}

		$tokens = self::get_tokens(
			array(
				'token_id' => $token_ids,
			)
		);

		return apply_filters( 'woocommerce_get_order_payment_tokens', $tokens, $order_id );
	}

	/**
	 * Get a token object by ID.
	 *
	 * @since 2.6.0
	 *
	 * @param int    $token_id Token ID.
	 * @param object $token_result Token result.
	 * @return null|WC_Payment_Token Returns a valid payment token or null if no token can be found.
	 */
	public static function get( $token_id, $token_result = null ) {
		$data_store = WC_Data_Store::load( 'payment-token' );

		if ( is_null( $token_result ) ) {
			$token_result = $data_store->get_token_by_id( $token_id );
			// Still empty? Token doesn't exist? Don't continue.
			if ( empty( $token_result ) ) {
				return null;
			}
		}

		$token_class = self::get_token_classname( $token_result->type );

		if ( class_exists( $token_class ) ) {
			$meta        = $data_store->get_metadata( $token_id );
			$passed_meta = array();
			if ( ! empty( $meta ) ) {
				foreach ( $meta as $meta_key => $meta_value ) {
					$passed_meta[ $meta_key ] = $meta_value[0];
				}
			}
			return new $token_class( $token_id, (array) $token_result, $passed_meta );
		}

		return null;
	}

	/**
	 * Remove a payment token from the database by ID.
	 *
	 * @since 2.6.0
	 * @param int $token_id Token ID.
	 */
	public static function delete( $token_id ) {
		$type = self::get_token_type_by_id( $token_id );
		if ( ! empty( $type ) ) {
			$class = self::get_token_classname( $type );
			$token = new $class( $token_id );
			$token->delete();
		}
	}

	/**
	 * Loops through all of a users payment tokens and sets is_default to false for all but a specific token.
	 *
	 * @since 2.6.0
	 * @param int $user_id  User to set a default for.
	 * @param int $token_id The ID of the token that should be default.
	 */
	public static function set_users_default( $user_id, $token_id ) {
		$data_store   = WC_Data_Store::load( 'payment-token' );
		$users_tokens = self::get_customer_tokens( $user_id );
		foreach ( $users_tokens as $token ) {
			if ( $token_id === $token->get_id() ) {
				$data_store->set_default_status( $token->get_id(), true );
				do_action( 'woocommerce_payment_token_set_default', $token_id, $token );
			} else {
				$data_store->set_default_status( $token->get_id(), false );
			}
		}
	}

	/**
	 * Returns what type (credit card, echeck, etc) of token a token is by ID.
	 *
	 * @since  2.6.0
	 * @param  int $token_id Token ID.
	 * @return string        Type.
	 */
	public static function get_token_type_by_id( $token_id ) {
		$data_store = WC_Data_Store::load( 'payment-token' );
		return $data_store->get_token_type_by_id( $token_id );
	}

	/**
	 * Get classname based on token type.
	 *
	 * @since 3.8.0
	 * @param string $type Token type.
	 * @return string
	 */
	protected static function get_token_classname( $type ) {
		/**
		 * Filter payment token class per type.
		 *
		 * @since 3.8.0
		 * @param string $class Payment token class.
		 * @param string $type Token type.
		 */
		return apply_filters( 'woocommerce_payment_token_class', 'WC_Payment_Token_' . $type, $type );
	}
}
wc-cart-functions.php000064400000042240151336044760010636 0ustar00<?php
/**
 * WooCommerce Cart Functions
 *
 * Functions for cart specific things.
 *
 * @package WooCommerce\Functions
 * @version 2.5.0
 */

use Automattic\Jetpack\Constants;

defined( 'ABSPATH' ) || exit;

/**
 * Prevent password protected products being added to the cart.
 *
 * @param  bool $passed     Validation.
 * @param  int  $product_id Product ID.
 * @return bool
 */
function wc_protected_product_add_to_cart( $passed, $product_id ) {
	if ( post_password_required( $product_id ) ) {
		$passed = false;
		wc_add_notice( __( 'This product is protected and cannot be purchased.', 'woocommerce' ), 'error' );
	}
	return $passed;
}
add_filter( 'woocommerce_add_to_cart_validation', 'wc_protected_product_add_to_cart', 10, 2 );

/**
 * Clears the cart session when called.
 */
function wc_empty_cart() {
	if ( ! isset( WC()->cart ) || '' === WC()->cart ) {
		WC()->cart = new WC_Cart();
	}
	WC()->cart->empty_cart( false );
}

/**
 * Load the persistent cart.
 *
 * @param string  $user_login User login.
 * @param WP_User $user       User data.
 * @deprecated 2.3
 */
function wc_load_persistent_cart( $user_login, $user ) {
	if ( ! $user || ! apply_filters( 'woocommerce_persistent_cart_enabled', true ) ) {
		return;
	}

	$saved_cart = get_user_meta( $user->ID, '_woocommerce_persistent_cart_' . get_current_blog_id(), true );

	if ( ! $saved_cart ) {
		return;
	}

	$cart = WC()->session->cart;

	if ( empty( $cart ) || ! is_array( $cart ) || 0 === count( $cart ) ) {
		WC()->session->cart = $saved_cart['cart'];
	}
}

/**
 * Retrieves unvalidated referer from '_wp_http_referer' or HTTP referer.
 *
 * Do not use for redirects, use {@see wp_get_referer()} instead.
 *
 * @since 2.6.1
 * @return string|false Referer URL on success, false on failure.
 */
function wc_get_raw_referer() {
	if ( function_exists( 'wp_get_raw_referer' ) ) {
		return wp_get_raw_referer();
	}

	if ( ! empty( $_REQUEST['_wp_http_referer'] ) ) { // WPCS: input var ok, CSRF ok.
		return wp_unslash( $_REQUEST['_wp_http_referer'] ); // WPCS: input var ok, CSRF ok, sanitization ok.
	} elseif ( ! empty( $_SERVER['HTTP_REFERER'] ) ) { // WPCS: input var ok, CSRF ok.
		return wp_unslash( $_SERVER['HTTP_REFERER'] ); // WPCS: input var ok, CSRF ok, sanitization ok.
	}

	return false;
}

/**
 * Add to cart messages.
 *
 * @param int|array $products Product ID list or single product ID.
 * @param bool      $show_qty Should qty's be shown? Added in 2.6.0.
 * @param bool      $return   Return message rather than add it.
 *
 * @return mixed
 */
function wc_add_to_cart_message( $products, $show_qty = false, $return = false ) {
	$titles = array();
	$count  = 0;

	if ( ! is_array( $products ) ) {
		$products = array( $products => 1 );
		$show_qty = false;
	}

	if ( ! $show_qty ) {
		$products = array_fill_keys( array_keys( $products ), 1 );
	}

	foreach ( $products as $product_id => $qty ) {
		/* translators: %s: product name */
		$titles[] = apply_filters( 'woocommerce_add_to_cart_qty_html', ( $qty > 1 ? absint( $qty ) . ' &times; ' : '' ), $product_id ) . apply_filters( 'woocommerce_add_to_cart_item_name_in_quotes', sprintf( _x( '&ldquo;%s&rdquo;', 'Item name in quotes', 'woocommerce' ), strip_tags( get_the_title( $product_id ) ) ), $product_id );
		$count   += $qty;
	}

	$titles = array_filter( $titles );
	/* translators: %s: product name */
	$added_text = sprintf( _n( '%s has been added to your cart.', '%s have been added to your cart.', $count, 'woocommerce' ), wc_format_list_of_items( $titles ) );

	// Output success messages.
	if ( 'yes' === get_option( 'woocommerce_cart_redirect_after_add' ) ) {
		$return_to = apply_filters( 'woocommerce_continue_shopping_redirect', wc_get_raw_referer() ? wp_validate_redirect( wc_get_raw_referer(), false ) : wc_get_page_permalink( 'shop' ) );
		$message   = sprintf( '<a href="%s" tabindex="1" class="button wc-forward">%s</a> %s', esc_url( $return_to ), esc_html__( 'Continue shopping', 'woocommerce' ), esc_html( $added_text ) );
	} else {
		$message = sprintf( '<a href="%s" tabindex="1" class="button wc-forward">%s</a> %s', esc_url( wc_get_cart_url() ), esc_html__( 'View cart', 'woocommerce' ), esc_html( $added_text ) );
	}

	if ( has_filter( 'wc_add_to_cart_message' ) ) {
		wc_deprecated_function( 'The wc_add_to_cart_message filter', '3.0', 'wc_add_to_cart_message_html' );
		$message = apply_filters( 'wc_add_to_cart_message', $message, $product_id );
	}

	$message = apply_filters( 'wc_add_to_cart_message_html', $message, $products, $show_qty );

	if ( $return ) {
		return $message;
	} else {
		wc_add_notice( $message, apply_filters( 'woocommerce_add_to_cart_notice_type', 'success' ) );
	}
}

/**
 * Comma separate a list of item names, and replace final comma with 'and'.
 *
 * @param  array $items Cart items.
 * @return string
 */
function wc_format_list_of_items( $items ) {
	$item_string = '';

	foreach ( $items as $key => $item ) {
		$item_string .= $item;

		if ( count( $items ) === $key + 2 ) {
			$item_string .= ' ' . __( 'and', 'woocommerce' ) . ' ';
		} elseif ( count( $items ) !== $key + 1 ) {
			$item_string .= ', ';
		}
	}

	return $item_string;
}

/**
 * Clear cart after payment.
 */
function wc_clear_cart_after_payment() {
	global $wp;

	if ( ! empty( $wp->query_vars['order-received'] ) ) {

		$order_id  = absint( $wp->query_vars['order-received'] );
		$order_key = isset( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : ''; // WPCS: input var ok, CSRF ok.

		if ( $order_id > 0 ) {
			$order = wc_get_order( $order_id );

			if ( $order && hash_equals( $order->get_order_key(), $order_key ) ) {
				WC()->cart->empty_cart();
			}
		}
	}

	if ( WC()->session->order_awaiting_payment > 0 ) {
		$order = wc_get_order( WC()->session->order_awaiting_payment );

		if ( $order && $order->get_id() > 0 ) {
			// If the order has not failed, or is not pending, the order must have gone through.
			if ( ! $order->has_status( array( 'failed', 'pending', 'cancelled' ) ) ) {
				WC()->cart->empty_cart();
			}
		}
	}
}
add_action( 'get_header', 'wc_clear_cart_after_payment' );

/**
 * Get the subtotal.
 */
function wc_cart_totals_subtotal_html() {
	echo WC()->cart->get_cart_subtotal(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}

/**
 * Get shipping methods.
 */
function wc_cart_totals_shipping_html() {
	$packages = WC()->shipping()->get_packages();
	$first    = true;

	foreach ( $packages as $i => $package ) {
		$chosen_method = isset( WC()->session->chosen_shipping_methods[ $i ] ) ? WC()->session->chosen_shipping_methods[ $i ] : '';
		$product_names = array();

		if ( count( $packages ) > 1 ) {
			foreach ( $package['contents'] as $item_id => $values ) {
				$product_names[ $item_id ] = $values['data']->get_name() . ' &times;' . $values['quantity'];
			}
			$product_names = apply_filters( 'woocommerce_shipping_package_details_array', $product_names, $package );
		}

		wc_get_template(
			'cart/cart-shipping.php',
			array(
				'package'                  => $package,
				'available_methods'        => $package['rates'],
				'show_package_details'     => count( $packages ) > 1,
				'show_shipping_calculator' => is_cart() && apply_filters( 'woocommerce_shipping_show_shipping_calculator', $first, $i, $package ),
				'package_details'          => implode( ', ', $product_names ),
				/* translators: %d: shipping package number */
				'package_name'             => apply_filters( 'woocommerce_shipping_package_name', ( ( $i + 1 ) > 1 ) ? sprintf( _x( 'Shipping %d', 'shipping packages', 'woocommerce' ), ( $i + 1 ) ) : _x( 'Shipping', 'shipping packages', 'woocommerce' ), $i, $package ),
				'index'                    => $i,
				'chosen_method'            => $chosen_method,
				'formatted_destination'    => WC()->countries->get_formatted_address( $package['destination'], ', ' ),
				'has_calculated_shipping'  => WC()->customer->has_calculated_shipping(),
			)
		);

		$first = false;
	}
}

/**
 * Get taxes total.
 */
function wc_cart_totals_taxes_total_html() {
	echo apply_filters( 'woocommerce_cart_totals_taxes_total_html', wc_price( WC()->cart->get_taxes_total() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}

/**
 * Get a coupon label.
 *
 * @param string|WC_Coupon $coupon Coupon data or code.
 * @param bool             $echo   Echo or return.
 *
 * @return string
 */
function wc_cart_totals_coupon_label( $coupon, $echo = true ) {
	if ( is_string( $coupon ) ) {
		$coupon = new WC_Coupon( $coupon );
	}

	/* translators: %s: coupon code */
	$label = apply_filters( 'woocommerce_cart_totals_coupon_label', sprintf( esc_html__( 'Coupon: %s', 'woocommerce' ), $coupon->get_code() ), $coupon );

	if ( $echo ) {
		echo $label; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
	} else {
		return $label;
	}
}

/**
 * Get coupon display HTML.
 *
 * @param string|WC_Coupon $coupon Coupon data or code.
 */
function wc_cart_totals_coupon_html( $coupon ) {
	if ( is_string( $coupon ) ) {
		$coupon = new WC_Coupon( $coupon );
	}

	$discount_amount_html = '';

	$amount               = WC()->cart->get_coupon_discount_amount( $coupon->get_code(), WC()->cart->display_cart_ex_tax );
	$discount_amount_html = '-' . wc_price( $amount );

	if ( $coupon->get_free_shipping() && empty( $amount ) ) {
		$discount_amount_html = __( 'Free shipping coupon', 'woocommerce' );
	}

	$discount_amount_html = apply_filters( 'woocommerce_coupon_discount_amount_html', $discount_amount_html, $coupon );
	$coupon_html          = $discount_amount_html . ' <a href="' . esc_url( add_query_arg( 'remove_coupon', rawurlencode( $coupon->get_code() ), Constants::is_defined( 'WOOCOMMERCE_CHECKOUT' ) ? wc_get_checkout_url() : wc_get_cart_url() ) ) . '" class="woocommerce-remove-coupon" data-coupon="' . esc_attr( $coupon->get_code() ) . '">' . __( '[Remove]', 'woocommerce' ) . '</a>';

	echo wp_kses( apply_filters( 'woocommerce_cart_totals_coupon_html', $coupon_html, $coupon, $discount_amount_html ), array_replace_recursive( wp_kses_allowed_html( 'post' ), array( 'a' => array( 'data-coupon' => true ) ) ) ); // phpcs:ignore PHPCompatibility.PHP.NewFunctions.array_replace_recursiveFound
}

/**
 * Get order total html including inc tax if needed.
 */
function wc_cart_totals_order_total_html() {
	$value = '<strong>' . WC()->cart->get_total() . '</strong> ';

	// If prices are tax inclusive, show taxes here.
	if ( wc_tax_enabled() && WC()->cart->display_prices_including_tax() ) {
		$tax_string_array = array();
		$cart_tax_totals  = WC()->cart->get_tax_totals();

		if ( get_option( 'woocommerce_tax_total_display' ) === 'itemized' ) {
			foreach ( $cart_tax_totals as $code => $tax ) {
				$tax_string_array[] = sprintf( '%s %s', $tax->formatted_amount, $tax->label );
			}
		} elseif ( ! empty( $cart_tax_totals ) ) {
			$tax_string_array[] = sprintf( '%s %s', wc_price( WC()->cart->get_taxes_total( true, true ) ), WC()->countries->tax_or_vat() );
		}

		if ( ! empty( $tax_string_array ) ) {
			$taxable_address = WC()->customer->get_taxable_address();
			if ( WC()->customer->is_customer_outside_base() && ! WC()->customer->has_calculated_shipping() ) {
				$country = WC()->countries->estimated_for_prefix( $taxable_address[0] ) . WC()->countries->countries[ $taxable_address[0] ];
				/* translators: 1: tax amount 2: country name */
				$tax_text = wp_kses_post( sprintf( __( '(includes %1$s estimated for %2$s)', 'woocommerce' ), implode( ', ', $tax_string_array ), $country ) );
			} else {
				/* translators: %s: tax amount */
				$tax_text = wp_kses_post( sprintf( __( '(includes %s)', 'woocommerce' ), implode( ', ', $tax_string_array ) ) );
			}

			$value .= '<small class="includes_tax">' . $tax_text . '</small>';
		}
	}

	echo apply_filters( 'woocommerce_cart_totals_order_total_html', $value ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}

/**
 * Get the fee value.
 *
 * @param object $fee Fee data.
 */
function wc_cart_totals_fee_html( $fee ) {
	$cart_totals_fee_html = WC()->cart->display_prices_including_tax() ? wc_price( $fee->total + $fee->tax ) : wc_price( $fee->total );

	echo apply_filters( 'woocommerce_cart_totals_fee_html', $cart_totals_fee_html, $fee ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}

/**
 * Get a shipping methods full label including price.
 *
 * @param  WC_Shipping_Rate $method Shipping method rate data.
 * @return string
 */
function wc_cart_totals_shipping_method_label( $method ) {
	$label     = $method->get_label();
	$has_cost  = 0 < $method->cost;
	$hide_cost = ! $has_cost && in_array( $method->get_method_id(), array( 'free_shipping', 'local_pickup' ), true );

	if ( $has_cost && ! $hide_cost ) {
		if ( WC()->cart->display_prices_including_tax() ) {
			$label .= ': ' . wc_price( $method->cost + $method->get_shipping_tax() );
			if ( $method->get_shipping_tax() > 0 && ! wc_prices_include_tax() ) {
				$label .= ' <small class="tax_label">' . WC()->countries->inc_tax_or_vat() . '</small>';
			}
		} else {
			$label .= ': ' . wc_price( $method->cost );
			if ( $method->get_shipping_tax() > 0 && wc_prices_include_tax() ) {
				$label .= ' <small class="tax_label">' . WC()->countries->ex_tax_or_vat() . '</small>';
			}
		}
	}

	return apply_filters( 'woocommerce_cart_shipping_method_full_label', $label, $method );
}

/**
 * Round discount.
 *
 * @param  double $value Amount to round.
 * @param  int    $precision DP to round.
 * @return float
 */
function wc_cart_round_discount( $value, $precision ) {
	return wc_round_discount( $value, $precision );
}

/**
 * Gets chosen shipping method IDs from chosen_shipping_methods session, without instance IDs.
 *
 * @since  2.6.2
 * @return string[]
 */
function wc_get_chosen_shipping_method_ids() {
	$method_ids     = array();
	$chosen_methods = WC()->session->get( 'chosen_shipping_methods', array() );
	foreach ( $chosen_methods as $chosen_method ) {
		$chosen_method = explode( ':', $chosen_method );
		$method_ids[]  = current( $chosen_method );
	}
	return $method_ids;
}

/**
 * Get chosen method for package from session.
 *
 * @since  3.2.0
 * @param  int   $key Key of package.
 * @param  array $package Package data array.
 * @return string|bool
 */
function wc_get_chosen_shipping_method_for_package( $key, $package ) {
	$chosen_methods = WC()->session->get( 'chosen_shipping_methods' );
	$chosen_method  = isset( $chosen_methods[ $key ] ) ? $chosen_methods[ $key ] : false;
	$changed        = wc_shipping_methods_have_changed( $key, $package );

	// This is deprecated but here for BW compat. TODO: Remove in 4.0.0.
	$method_counts = WC()->session->get( 'shipping_method_counts' );

	if ( ! empty( $method_counts[ $key ] ) ) {
		$method_count = absint( $method_counts[ $key ] );
	} else {
		$method_count = 0;
	}

	// If not set, not available, or available methods have changed, set to the DEFAULT option.
	if ( ! $chosen_method || $changed || ! isset( $package['rates'][ $chosen_method ] ) || count( $package['rates'] ) !== $method_count ) {
		$chosen_method          = wc_get_default_shipping_method_for_package( $key, $package, $chosen_method );
		$chosen_methods[ $key ] = $chosen_method;
		$method_counts[ $key ]  = count( $package['rates'] );

		WC()->session->set( 'chosen_shipping_methods', $chosen_methods );
		WC()->session->set( 'shipping_method_counts', $method_counts );

		do_action( 'woocommerce_shipping_method_chosen', $chosen_method );
	}
	return $chosen_method;
}

/**
 * Choose the default method for a package.
 *
 * @since  3.2.0
 * @param  int    $key Key of package.
 * @param  array  $package Package data array.
 * @param  string $chosen_method Chosen method id.
 * @return string
 */
function wc_get_default_shipping_method_for_package( $key, $package, $chosen_method ) {
	$rate_keys = array_keys( $package['rates'] );
	$default   = current( $rate_keys );
	$coupons   = WC()->cart->get_coupons();
	foreach ( $coupons as $coupon ) {
		if ( $coupon->get_free_shipping() ) {
			foreach ( $rate_keys as $rate_key ) {
				if ( 0 === stripos( $rate_key, 'free_shipping' ) ) {
					$default = $rate_key;
					break;
				}
			}
			break;
		}
	}
	return apply_filters( 'woocommerce_shipping_chosen_method', $default, $package['rates'], $chosen_method );
}

/**
 * See if the methods have changed since the last request.
 *
 * @since  3.2.0
 * @param  int   $key Key of package.
 * @param  array $package Package data array.
 * @return bool
 */
function wc_shipping_methods_have_changed( $key, $package ) {
	// Lookup previous methods from session.
	$previous_shipping_methods = WC()->session->get( 'previous_shipping_methods' );
	// Get new and old rates.
	$new_rates  = array_keys( $package['rates'] );
	$prev_rates = isset( $previous_shipping_methods[ $key ] ) ? $previous_shipping_methods[ $key ] : false;
	// Update session.
	$previous_shipping_methods[ $key ] = $new_rates;
	WC()->session->set( 'previous_shipping_methods', $previous_shipping_methods );
	return $new_rates !== $prev_rates;
}

/**
 * Gets a hash of important product data that when changed should cause cart items to be invalidated.
 *
 * The woocommerce_cart_item_data_to_validate filter can be used to add custom properties.
 *
 * @param WC_Product $product Product object.
 * @return string
 */
function wc_get_cart_item_data_hash( $product ) {
	return md5(
		wp_json_encode(
			apply_filters(
				'woocommerce_cart_item_data_to_validate',
				array(
					'type'       => $product->get_type(),
					'attributes' => 'variation' === $product->get_type() ? $product->get_variation_attributes() : '',
				),
				$product
			)
		)
	);
}
wc-product-functions.php000064400000140177151336044760011375 0ustar00<?php
/**
 * WooCommerce Product Functions
 *
 * Functions for product specific things.
 *
 * @package WooCommerce\Functions
 * @version 3.0.0
 */

use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Utilities\ArrayUtil;
use Automattic\WooCommerce\Utilities\NumberUtil;

defined( 'ABSPATH' ) || exit;

/**
 * Standard way of retrieving products based on certain parameters.
 *
 * This function should be used for product retrieval so that we have a data agnostic
 * way to get a list of products.
 *
 * Args and usage: https://github.com/woocommerce/woocommerce/wiki/wc_get_products-and-WC_Product_Query
 *
 * @since  3.0.0
 * @param  array $args Array of args (above).
 * @return array|stdClass Number of pages and an array of product objects if
 *                             paginate is true, or just an array of values.
 */
function wc_get_products( $args ) {
	// Handle some BW compatibility arg names where wp_query args differ in naming.
	$map_legacy = array(
		'numberposts'    => 'limit',
		'post_status'    => 'status',
		'post_parent'    => 'parent',
		'posts_per_page' => 'limit',
		'paged'          => 'page',
	);

	foreach ( $map_legacy as $from => $to ) {
		if ( isset( $args[ $from ] ) ) {
			$args[ $to ] = $args[ $from ];
		}
	}

	$query = new WC_Product_Query( $args );
	return $query->get_products();
}

/**
 * Main function for returning products, uses the WC_Product_Factory class.
 *
 * This function should only be called after 'init' action is finished, as there might be taxonomies that are getting
 * registered during the init action.
 *
 * @since 2.2.0
 *
 * @param mixed $the_product Post object or post ID of the product.
 * @param array $deprecated Previously used to pass arguments to the factory, e.g. to force a type.
 * @return WC_Product|null|false
 */
function wc_get_product( $the_product = false, $deprecated = array() ) {
	if ( ! did_action( 'woocommerce_init' ) || ! did_action( 'woocommerce_after_register_taxonomy' ) || ! did_action( 'woocommerce_after_register_post_type' ) ) {
		/* translators: 1: wc_get_product 2: woocommerce_init 3: woocommerce_after_register_taxonomy 4: woocommerce_after_register_post_type */
		wc_doing_it_wrong( __FUNCTION__, sprintf( __( '%1$s should not be called before the %2$s, %3$s and %4$s actions have finished.', 'woocommerce' ), 'wc_get_product', 'woocommerce_init', 'woocommerce_after_register_taxonomy', 'woocommerce_after_register_post_type' ), '3.9' );
		return false;
	}
	if ( ! empty( $deprecated ) ) {
		wc_deprecated_argument( 'args', '3.0', 'Passing args to wc_get_product is deprecated. If you need to force a type, construct the product class directly.' );
	}
	return WC()->product_factory->get_product( $the_product, $deprecated );
}

/**
 * Get a product object.
 *
 * @see WC_Product_Factory::get_product_classname
 * @since 3.9.0
 * @param string $product_type Product type. If used an invalid type a WC_Product_Simple instance will be returned.
 * @param int    $product_id   Product ID.
 * @return WC_Product
 */
function wc_get_product_object( $product_type, $product_id = 0 ) {
	$classname = WC_Product_Factory::get_product_classname( $product_id, $product_type );

	return new $classname( $product_id );
}

/**
 * Returns whether or not SKUS are enabled.
 *
 * @return bool
 */
function wc_product_sku_enabled() {
	return apply_filters( 'wc_product_sku_enabled', true );
}

/**
 * Returns whether or not product weights are enabled.
 *
 * @return bool
 */
function wc_product_weight_enabled() {
	return apply_filters( 'wc_product_weight_enabled', true );
}

/**
 * Returns whether or not product dimensions (HxWxD) are enabled.
 *
 * @return bool
 */
function wc_product_dimensions_enabled() {
	return apply_filters( 'wc_product_dimensions_enabled', true );
}

/**
 * Clear transient cache for product data.
 *
 * @param int $post_id (default: 0) The product ID.
 */
function wc_delete_product_transients( $post_id = 0 ) {
	// Transient data to clear with a fixed name which may be stale after product updates.
	$transients_to_clear = array(
		'wc_products_onsale',
		'wc_featured_products',
		'wc_outofstock_count',
		'wc_low_stock_count',
	);

	foreach ( $transients_to_clear as $transient ) {
		delete_transient( $transient );
	}

	if ( $post_id > 0 ) {
		// Transient names that include an ID - since they are dynamic they cannot be cleaned in bulk without the ID.
		$post_transient_names = array(
			'wc_product_children_',
			'wc_var_prices_',
			'wc_related_',
			'wc_child_has_weight_',
			'wc_child_has_dimensions_',
		);

		foreach ( $post_transient_names as $transient ) {
			delete_transient( $transient . $post_id );
		}
	}

	// Increments the transient version to invalidate cache.
	WC_Cache_Helper::get_transient_version( 'product', true );

	do_action( 'woocommerce_delete_product_transients', $post_id );
}

/**
 * Function that returns an array containing the IDs of the products that are on sale.
 *
 * @since 2.0
 * @return array
 */
function wc_get_product_ids_on_sale() {
	// Load from cache.
	$product_ids_on_sale = get_transient( 'wc_products_onsale' );

	// Valid cache found.
	if ( false !== $product_ids_on_sale ) {
		return $product_ids_on_sale;
	}

	$data_store          = WC_Data_Store::load( 'product' );
	$on_sale_products    = $data_store->get_on_sale_products();
	$product_ids_on_sale = wp_parse_id_list( array_merge( wp_list_pluck( $on_sale_products, 'id' ), array_diff( wp_list_pluck( $on_sale_products, 'parent_id' ), array( 0 ) ) ) );

	set_transient( 'wc_products_onsale', $product_ids_on_sale, DAY_IN_SECONDS * 30 );

	return $product_ids_on_sale;
}

/**
 * Function that returns an array containing the IDs of the featured products.
 *
 * @since 2.1
 * @return array
 */
function wc_get_featured_product_ids() {
	// Load from cache.
	$featured_product_ids = get_transient( 'wc_featured_products' );

	// Valid cache found.
	if ( false !== $featured_product_ids ) {
		return $featured_product_ids;
	}

	$data_store           = WC_Data_Store::load( 'product' );
	$featured             = $data_store->get_featured_product_ids();
	$product_ids          = array_keys( $featured );
	$parent_ids           = array_values( array_filter( $featured ) );
	$featured_product_ids = array_unique( array_merge( $product_ids, $parent_ids ) );

	set_transient( 'wc_featured_products', $featured_product_ids, DAY_IN_SECONDS * 30 );

	return $featured_product_ids;
}

/**
 * Filter to allow product_cat in the permalinks for products.
 *
 * @param  string  $permalink The existing permalink URL.
 * @param  WP_Post $post WP_Post object.
 * @return string
 */
function wc_product_post_type_link( $permalink, $post ) {
	// Abort if post is not a product.
	if ( 'product' !== $post->post_type ) {
		return $permalink;
	}

	// Abort early if the placeholder rewrite tag isn't in the generated URL.
	if ( false === strpos( $permalink, '%' ) ) {
		return $permalink;
	}

	// Get the custom taxonomy terms in use by this post.
	$terms = get_the_terms( $post->ID, 'product_cat' );

	if ( ! empty( $terms ) ) {
		$terms           = wp_list_sort(
			$terms,
			array(
				'parent'  => 'DESC',
				'term_id' => 'ASC',
			)
		);
		$category_object = apply_filters( 'wc_product_post_type_link_product_cat', $terms[0], $terms, $post );
		$product_cat     = $category_object->slug;

		if ( $category_object->parent ) {
			$ancestors = get_ancestors( $category_object->term_id, 'product_cat' );
			foreach ( $ancestors as $ancestor ) {
				$ancestor_object = get_term( $ancestor, 'product_cat' );
				if ( apply_filters( 'woocommerce_product_post_type_link_parent_category_only', false ) ) {
					$product_cat = $ancestor_object->slug;
				} else {
					$product_cat = $ancestor_object->slug . '/' . $product_cat;
				}
			}
		}
	} else {
		// If no terms are assigned to this post, use a string instead (can't leave the placeholder there).
		$product_cat = _x( 'uncategorized', 'slug', 'woocommerce' );
	}

	$find = array(
		'%year%',
		'%monthnum%',
		'%day%',
		'%hour%',
		'%minute%',
		'%second%',
		'%post_id%',
		'%category%',
		'%product_cat%',
	);

	$replace = array(
		date_i18n( 'Y', strtotime( $post->post_date ) ),
		date_i18n( 'm', strtotime( $post->post_date ) ),
		date_i18n( 'd', strtotime( $post->post_date ) ),
		date_i18n( 'H', strtotime( $post->post_date ) ),
		date_i18n( 'i', strtotime( $post->post_date ) ),
		date_i18n( 's', strtotime( $post->post_date ) ),
		$post->ID,
		$product_cat,
		$product_cat,
	);

	$permalink = str_replace( $find, $replace, $permalink );

	return $permalink;
}
add_filter( 'post_type_link', 'wc_product_post_type_link', 10, 2 );

/**
 * Get the placeholder image URL either from media, or use the fallback image.
 *
 * @param string $size Thumbnail size to use.
 * @return string
 */
function wc_placeholder_img_src( $size = 'woocommerce_thumbnail' ) {
	$src               = WC()->plugin_url() . '/assets/images/placeholder.png';
	$placeholder_image = get_option( 'woocommerce_placeholder_image', 0 );

	if ( ! empty( $placeholder_image ) ) {
		if ( is_numeric( $placeholder_image ) ) {
			$image = wp_get_attachment_image_src( $placeholder_image, $size );

			if ( ! empty( $image[0] ) ) {
				$src = $image[0];
			}
		} else {
			$src = $placeholder_image;
		}
	}

	return apply_filters( 'woocommerce_placeholder_img_src', $src );
}

/**
 * Get the placeholder image.
 *
 * Uses wp_get_attachment_image if using an attachment ID @since 3.6.0 to handle responsiveness.
 *
 * @param string       $size Image size.
 * @param string|array $attr Optional. Attributes for the image markup. Default empty.
 * @return string
 */
function wc_placeholder_img( $size = 'woocommerce_thumbnail', $attr = '' ) {
	$dimensions        = wc_get_image_size( $size );
	$placeholder_image = get_option( 'woocommerce_placeholder_image', 0 );

	$default_attr = array(
		'class' => 'woocommerce-placeholder wp-post-image',
		'alt'   => __( 'Placeholder', 'woocommerce' ),
	);

	$attr = wp_parse_args( $attr, $default_attr );

	if ( wp_attachment_is_image( $placeholder_image ) ) {
		$image_html = wp_get_attachment_image(
			$placeholder_image,
			$size,
			false,
			$attr
		);
	} else {
		$image      = wc_placeholder_img_src( $size );
		$hwstring   = image_hwstring( $dimensions['width'], $dimensions['height'] );
		$attributes = array();

		foreach ( $attr as $name => $value ) {
			$attribute[] = esc_attr( $name ) . '="' . esc_attr( $value ) . '"';
		}

		$image_html = '<img src="' . esc_url( $image ) . '" ' . $hwstring . implode( ' ', $attribute ) . '/>';
	}

	return apply_filters( 'woocommerce_placeholder_img', $image_html, $size, $dimensions );
}

/**
 * Variation Formatting.
 *
 * Gets a formatted version of variation data or item meta.
 *
 * @param array|WC_Product_Variation $variation Variation object.
 * @param bool                       $flat Should this be a flat list or HTML list? (default: false).
 * @param bool                       $include_names include attribute names/labels in the list.
 * @param bool                       $skip_attributes_in_name Do not list attributes already part of the variation name.
 * @return string
 */
function wc_get_formatted_variation( $variation, $flat = false, $include_names = true, $skip_attributes_in_name = false ) {
	$return = '';

	if ( is_a( $variation, 'WC_Product_Variation' ) ) {
		$variation_attributes = $variation->get_attributes();
		$product              = $variation;
		$variation_name       = $variation->get_name();
	} else {
		$product        = false;
		$variation_name = '';
		// Remove attribute_ prefix from names.
		$variation_attributes = array();
		if ( is_array( $variation ) ) {
			foreach ( $variation as $key => $value ) {
				$variation_attributes[ str_replace( 'attribute_', '', $key ) ] = $value;
			}
		}
	}

	$list_type = $include_names ? 'dl' : 'ul';

	if ( is_array( $variation_attributes ) ) {

		if ( ! $flat ) {
			$return = '<' . $list_type . ' class="variation">';
		}

		$variation_list = array();

		foreach ( $variation_attributes as $name => $value ) {
			// If this is a term slug, get the term's nice name.
			if ( taxonomy_exists( $name ) ) {
				$term = get_term_by( 'slug', $value, $name );
				if ( ! is_wp_error( $term ) && ! empty( $term->name ) ) {
					$value = $term->name;
				}
			}

			// Do not list attributes already part of the variation name.
			if ( '' === $value || ( $skip_attributes_in_name && wc_is_attribute_in_product_name( $value, $variation_name ) ) ) {
				continue;
			}

			if ( $include_names ) {
				if ( $flat ) {
					$variation_list[] = wc_attribute_label( $name, $product ) . ': ' . rawurldecode( $value );
				} else {
					$variation_list[] = '<dt>' . wc_attribute_label( $name, $product ) . ':</dt><dd>' . rawurldecode( $value ) . '</dd>';
				}
			} else {
				if ( $flat ) {
					$variation_list[] = rawurldecode( $value );
				} else {
					$variation_list[] = '<li>' . rawurldecode( $value ) . '</li>';
				}
			}
		}

		if ( $flat ) {
			$return .= implode( ', ', $variation_list );
		} else {
			$return .= implode( '', $variation_list );
		}

		if ( ! $flat ) {
			$return .= '</' . $list_type . '>';
		}
	}
	return $return;
}

/**
 * Function which handles the start and end of scheduled sales via cron.
 */
function wc_scheduled_sales() {
	$data_store = WC_Data_Store::load( 'product' );

	// Sales which are due to start.
	$product_ids = $data_store->get_starting_sales();
	if ( $product_ids ) {
		do_action( 'wc_before_products_starting_sales', $product_ids );
		foreach ( $product_ids as $product_id ) {
			$product = wc_get_product( $product_id );

			if ( $product ) {
				$sale_price = $product->get_sale_price();

				if ( $sale_price ) {
					$product->set_price( $sale_price );
					$product->set_date_on_sale_from( '' );
				} else {
					$product->set_date_on_sale_to( '' );
					$product->set_date_on_sale_from( '' );
				}

				$product->save();
			}
		}
		do_action( 'wc_after_products_starting_sales', $product_ids );

		WC_Cache_Helper::get_transient_version( 'product', true );
		delete_transient( 'wc_products_onsale' );
	}

	// Sales which are due to end.
	$product_ids = $data_store->get_ending_sales();
	if ( $product_ids ) {
		do_action( 'wc_before_products_ending_sales', $product_ids );
		foreach ( $product_ids as $product_id ) {
			$product = wc_get_product( $product_id );

			if ( $product ) {
				$regular_price = $product->get_regular_price();
				$product->set_price( $regular_price );
				$product->set_sale_price( '' );
				$product->set_date_on_sale_to( '' );
				$product->set_date_on_sale_from( '' );
				$product->save();
			}
		}
		do_action( 'wc_after_products_ending_sales', $product_ids );

		WC_Cache_Helper::get_transient_version( 'product', true );
		delete_transient( 'wc_products_onsale' );
	}
}
add_action( 'woocommerce_scheduled_sales', 'wc_scheduled_sales' );

/**
 * Get attachment image attributes.
 *
 * @param array $attr Image attributes.
 * @return array
 */
function wc_get_attachment_image_attributes( $attr ) {
	/*
	 * If the user can manage woocommerce, allow them to
	 * see the image content.
	 */
	if ( current_user_can( 'manage_woocommerce' ) ) {
		return $attr;
	}

	/*
	 * If the user does not have the right capabilities,
	 * filter out the image source and replace with placeholder
	 * image.
	 */
	if ( isset( $attr['src'] ) && strstr( $attr['src'], 'woocommerce_uploads/' ) ) {
		$attr['src'] = wc_placeholder_img_src();

		if ( isset( $attr['srcset'] ) ) {
			$attr['srcset'] = '';
		}
	}
	return $attr;
}
add_filter( 'wp_get_attachment_image_attributes', 'wc_get_attachment_image_attributes' );


/**
 * Prepare attachment for JavaScript.
 *
 * @param array $response JS version of a attachment post object.
 * @return array
 */
function wc_prepare_attachment_for_js( $response ) {
	/*
	 * If the user can manage woocommerce, allow them to
	 * see the image content.
	 */
	if ( current_user_can( 'manage_woocommerce' ) ) {
		return $response;
	}

	/*
	 * If the user does not have the right capabilities,
	 * filter out the image source and replace with placeholder
	 * image.
	 */
	if ( isset( $response['url'] ) && strstr( $response['url'], 'woocommerce_uploads/' ) ) {
		$response['full']['url'] = wc_placeholder_img_src();
		if ( isset( $response['sizes'] ) ) {
			foreach ( $response['sizes'] as $size => $value ) {
				$response['sizes'][ $size ]['url'] = wc_placeholder_img_src();
			}
		}
	}

	return $response;
}
add_filter( 'wp_prepare_attachment_for_js', 'wc_prepare_attachment_for_js' );

/**
 * Track product views.
 */
function wc_track_product_view() {
	if ( ! is_singular( 'product' ) || ! is_active_widget( false, false, 'woocommerce_recently_viewed_products', true ) ) {
		return;
	}

	global $post;

	if ( empty( $_COOKIE['woocommerce_recently_viewed'] ) ) { // @codingStandardsIgnoreLine.
		$viewed_products = array();
	} else {
		$viewed_products = wp_parse_id_list( (array) explode( '|', wp_unslash( $_COOKIE['woocommerce_recently_viewed'] ) ) ); // @codingStandardsIgnoreLine.
	}

	// Unset if already in viewed products list.
	$keys = array_flip( $viewed_products );

	if ( isset( $keys[ $post->ID ] ) ) {
		unset( $viewed_products[ $keys[ $post->ID ] ] );
	}

	$viewed_products[] = $post->ID;

	if ( count( $viewed_products ) > 15 ) {
		array_shift( $viewed_products );
	}

	// Store for session only.
	wc_setcookie( 'woocommerce_recently_viewed', implode( '|', $viewed_products ) );
}

add_action( 'template_redirect', 'wc_track_product_view', 20 );

/**
 * Get product types.
 *
 * @since 2.2
 * @return array
 */
function wc_get_product_types() {
	return (array) apply_filters(
		'product_type_selector',
		array(
			'simple'   => __( 'Simple product', 'woocommerce' ),
			'grouped'  => __( 'Grouped product', 'woocommerce' ),
			'external' => __( 'External/Affiliate product', 'woocommerce' ),
			'variable' => __( 'Variable product', 'woocommerce' ),
		)
	);
}

/**
 * Check if product sku is unique.
 *
 * @since 2.2
 * @param int    $product_id Product ID.
 * @param string $sku Product SKU.
 * @return bool
 */
function wc_product_has_unique_sku( $product_id, $sku ) {
	$data_store = WC_Data_Store::load( 'product' );
	$sku_found  = $data_store->is_existing_sku( $product_id, $sku );

	if ( apply_filters( 'wc_product_has_unique_sku', $sku_found, $product_id, $sku ) ) {
		return false;
	}

	return true;
}

/**
 * Force a unique SKU.
 *
 * @since  3.0.0
 * @param  integer $product_id Product ID.
 */
function wc_product_force_unique_sku( $product_id ) {
	$product     = wc_get_product( $product_id );
	$current_sku = $product ? $product->get_sku( 'edit' ) : '';

	if ( $current_sku ) {
		try {
			$new_sku = wc_product_generate_unique_sku( $product_id, $current_sku );

			if ( $current_sku !== $new_sku ) {
				$product->set_sku( $new_sku );
				$product->save();
			}
		} catch ( Exception $e ) {} // @codingStandardsIgnoreLine.
	}
}

/**
 * Recursively appends a suffix until a unique SKU is found.
 *
 * @since  3.0.0
 * @param  integer $product_id Product ID.
 * @param  string  $sku Product SKU.
 * @param  integer $index An optional index that can be added to the product SKU.
 * @return string
 */
function wc_product_generate_unique_sku( $product_id, $sku, $index = 0 ) {
	$generated_sku = 0 < $index ? $sku . '-' . $index : $sku;

	if ( ! wc_product_has_unique_sku( $product_id, $generated_sku ) ) {
		$generated_sku = wc_product_generate_unique_sku( $product_id, $sku, ( $index + 1 ) );
	}

	return $generated_sku;
}

/**
 * Get product ID by SKU.
 *
 * @since  2.3.0
 * @param  string $sku Product SKU.
 * @return int
 */
function wc_get_product_id_by_sku( $sku ) {
	$data_store = WC_Data_Store::load( 'product' );
	return $data_store->get_product_id_by_sku( $sku );
}

/**
 * Get attributes/data for an individual variation from the database and maintain it's integrity.
 *
 * @since  2.4.0
 * @param  int $variation_id Variation ID.
 * @return array
 */
function wc_get_product_variation_attributes( $variation_id ) {
	// Build variation data from meta.
	$all_meta                = get_post_meta( $variation_id );
	$parent_id               = wp_get_post_parent_id( $variation_id );
	$parent_attributes       = array_filter( (array) get_post_meta( $parent_id, '_product_attributes', true ) );
	$found_parent_attributes = array();
	$variation_attributes    = array();

	// Compare to parent variable product attributes and ensure they match.
	foreach ( $parent_attributes as $attribute_name => $options ) {
		if ( ! empty( $options['is_variation'] ) ) {
			$attribute                 = 'attribute_' . sanitize_title( $attribute_name );
			$found_parent_attributes[] = $attribute;
			if ( ! array_key_exists( $attribute, $variation_attributes ) ) {
				$variation_attributes[ $attribute ] = ''; // Add it - 'any' will be asumed.
			}
		}
	}

	// Get the variation attributes from meta.
	foreach ( $all_meta as $name => $value ) {
		// Only look at valid attribute meta, and also compare variation level attributes and remove any which do not exist at parent level.
		if ( 0 !== strpos( $name, 'attribute_' ) || ! in_array( $name, $found_parent_attributes, true ) ) {
			unset( $variation_attributes[ $name ] );
			continue;
		}
		/**
		 * Pre 2.4 handling where 'slugs' were saved instead of the full text attribute.
		 * Attempt to get full version of the text attribute from the parent.
		 */
		if ( sanitize_title( $value[0] ) === $value[0] && version_compare( get_post_meta( $parent_id, '_product_version', true ), '2.4.0', '<' ) ) {
			foreach ( $parent_attributes as $attribute ) {
				if ( 'attribute_' . sanitize_title( $attribute['name'] ) !== $name ) {
					continue;
				}
				$text_attributes = wc_get_text_attributes( $attribute['value'] );

				foreach ( $text_attributes as $text_attribute ) {
					if ( sanitize_title( $text_attribute ) === $value[0] ) {
						$value[0] = $text_attribute;
						break;
					}
				}
			}
		}

		$variation_attributes[ $name ] = $value[0];
	}

	return $variation_attributes;
}

/**
 * Get all product cats for a product by ID, including hierarchy
 *
 * @since  2.5.0
 * @param  int $product_id Product ID.
 * @return array
 */
function wc_get_product_cat_ids( $product_id ) {
	$product_cats = wc_get_product_term_ids( $product_id, 'product_cat' );

	foreach ( $product_cats as $product_cat ) {
		$product_cats = array_merge( $product_cats, get_ancestors( $product_cat, 'product_cat' ) );
	}

	return $product_cats;
}

/**
 * Gets data about an attachment, such as alt text and captions.
 *
 * @since 2.6.0
 *
 * @param int|null        $attachment_id Attachment ID.
 * @param WC_Product|bool $product WC_Product object.
 *
 * @return array
 */
function wc_get_product_attachment_props( $attachment_id = null, $product = false ) {
	$props      = array(
		'title'   => '',
		'caption' => '',
		'url'     => '',
		'alt'     => '',
		'src'     => '',
		'srcset'  => false,
		'sizes'   => false,
	);
	$attachment = get_post( $attachment_id );

	if ( $attachment && 'attachment' === $attachment->post_type ) {
		$props['title']   = wp_strip_all_tags( $attachment->post_title );
		$props['caption'] = wp_strip_all_tags( $attachment->post_excerpt );
		$props['url']     = wp_get_attachment_url( $attachment_id );

		// Alt text.
		$alt_text = array( wp_strip_all_tags( get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ), $props['caption'], wp_strip_all_tags( $attachment->post_title ) );

		if ( $product && $product instanceof WC_Product ) {
			$alt_text[] = wp_strip_all_tags( get_the_title( $product->get_id() ) );
		}

		$alt_text     = array_filter( $alt_text );
		$props['alt'] = isset( $alt_text[0] ) ? $alt_text[0] : '';

		// Large version.
		$full_size           = apply_filters( 'woocommerce_gallery_full_size', apply_filters( 'woocommerce_product_thumbnails_large_size', 'full' ) );
		$src                 = wp_get_attachment_image_src( $attachment_id, $full_size );
		$props['full_src']   = $src[0];
		$props['full_src_w'] = $src[1];
		$props['full_src_h'] = $src[2];

		// Gallery thumbnail.
		$gallery_thumbnail                = wc_get_image_size( 'gallery_thumbnail' );
		$gallery_thumbnail_size           = apply_filters( 'woocommerce_gallery_thumbnail_size', array( $gallery_thumbnail['width'], $gallery_thumbnail['height'] ) );
		$src                              = wp_get_attachment_image_src( $attachment_id, $gallery_thumbnail_size );
		$props['gallery_thumbnail_src']   = $src[0];
		$props['gallery_thumbnail_src_w'] = $src[1];
		$props['gallery_thumbnail_src_h'] = $src[2];

		// Thumbnail version.
		$thumbnail_size       = apply_filters( 'woocommerce_thumbnail_size', 'woocommerce_thumbnail' );
		$src                  = wp_get_attachment_image_src( $attachment_id, $thumbnail_size );
		$props['thumb_src']   = $src[0];
		$props['thumb_src_w'] = $src[1];
		$props['thumb_src_h'] = $src[2];

		// Image source.
		$image_size      = apply_filters( 'woocommerce_gallery_image_size', 'woocommerce_single' );
		$src             = wp_get_attachment_image_src( $attachment_id, $image_size );
		$props['src']    = $src[0];
		$props['src_w']  = $src[1];
		$props['src_h']  = $src[2];
		$props['srcset'] = function_exists( 'wp_get_attachment_image_srcset' ) ? wp_get_attachment_image_srcset( $attachment_id, $image_size ) : false;
		$props['sizes']  = function_exists( 'wp_get_attachment_image_sizes' ) ? wp_get_attachment_image_sizes( $attachment_id, $image_size ) : false;
	}
	return $props;
}

/**
 * Get product visibility options.
 *
 * @since 3.0.0
 * @return array
 */
function wc_get_product_visibility_options() {
	return apply_filters(
		'woocommerce_product_visibility_options',
		array(
			'visible' => __( 'Shop and search results', 'woocommerce' ),
			'catalog' => __( 'Shop only', 'woocommerce' ),
			'search'  => __( 'Search results only', 'woocommerce' ),
			'hidden'  => __( 'Hidden', 'woocommerce' ),
		)
	);
}

/**
 * Get product tax class options.
 *
 * @since 3.0.0
 * @return array
 */
function wc_get_product_tax_class_options() {
	$tax_classes           = WC_Tax::get_tax_classes();
	$tax_class_options     = array();
	$tax_class_options[''] = __( 'Standard', 'woocommerce' );

	if ( ! empty( $tax_classes ) ) {
		foreach ( $tax_classes as $class ) {
			$tax_class_options[ sanitize_title( $class ) ] = $class;
		}
	}
	return $tax_class_options;
}

/**
 * Get stock status options.
 *
 * @since 3.0.0
 * @return array
 */
function wc_get_product_stock_status_options() {
	return apply_filters(
		'woocommerce_product_stock_status_options',
		array(
			'instock'     => __( 'In stock', 'woocommerce' ),
			'outofstock'  => __( 'Out of stock', 'woocommerce' ),
			'onbackorder' => __( 'On backorder', 'woocommerce' ),
		)
	);
}

/**
 * Get backorder options.
 *
 * @since 3.0.0
 * @return array
 */
function wc_get_product_backorder_options() {
	return array(
		'no'     => __( 'Do not allow', 'woocommerce' ),
		'notify' => __( 'Allow, but notify customer', 'woocommerce' ),
		'yes'    => __( 'Allow', 'woocommerce' ),
	);
}

/**
 * Get related products based on product category and tags.
 *
 * @since  3.0.0
 * @param  int   $product_id  Product ID.
 * @param  int   $limit       Limit of results.
 * @param  array $exclude_ids Exclude IDs from the results.
 * @return array
 */
function wc_get_related_products( $product_id, $limit = 5, $exclude_ids = array() ) {

	$product_id     = absint( $product_id );
	$limit          = $limit >= -1 ? $limit : 5;
	$exclude_ids    = array_merge( array( 0, $product_id ), $exclude_ids );
	$transient_name = 'wc_related_' . $product_id;
	$query_args     = http_build_query(
		array(
			'limit'       => $limit,
			'exclude_ids' => $exclude_ids,
		)
	);

	$transient     = get_transient( $transient_name );
	$related_posts = $transient && isset( $transient[ $query_args ] ) ? $transient[ $query_args ] : false;

	// We want to query related posts if they are not cached, or we don't have enough.
	if ( false === $related_posts || count( $related_posts ) < $limit ) {

		$cats_array = apply_filters( 'woocommerce_product_related_posts_relate_by_category', true, $product_id ) ? apply_filters( 'woocommerce_get_related_product_cat_terms', wc_get_product_term_ids( $product_id, 'product_cat' ), $product_id ) : array();
		$tags_array = apply_filters( 'woocommerce_product_related_posts_relate_by_tag', true, $product_id ) ? apply_filters( 'woocommerce_get_related_product_tag_terms', wc_get_product_term_ids( $product_id, 'product_tag' ), $product_id ) : array();

		// Don't bother if none are set, unless woocommerce_product_related_posts_force_display is set to true in which case all products are related.
		if ( empty( $cats_array ) && empty( $tags_array ) && ! apply_filters( 'woocommerce_product_related_posts_force_display', false, $product_id ) ) {
			$related_posts = array();
		} else {
			$data_store    = WC_Data_Store::load( 'product' );
			$related_posts = $data_store->get_related_products( $cats_array, $tags_array, $exclude_ids, $limit + 10, $product_id );
		}

		if ( $transient ) {
			$transient[ $query_args ] = $related_posts;
		} else {
			$transient = array( $query_args => $related_posts );
		}

		set_transient( $transient_name, $transient, DAY_IN_SECONDS );
	}

	$related_posts = apply_filters(
		'woocommerce_related_products',
		$related_posts,
		$product_id,
		array(
			'limit'        => $limit,
			'excluded_ids' => $exclude_ids,
		)
	);

	if ( apply_filters( 'woocommerce_product_related_posts_shuffle', true ) ) {
		shuffle( $related_posts );
	}

	return array_slice( $related_posts, 0, $limit );
}

/**
 * Retrieves product term ids for a taxonomy.
 *
 * @since  3.0.0
 * @param  int    $product_id Product ID.
 * @param  string $taxonomy   Taxonomy slug.
 * @return array
 */
function wc_get_product_term_ids( $product_id, $taxonomy ) {
	$terms = get_the_terms( $product_id, $taxonomy );
	return ( empty( $terms ) || is_wp_error( $terms ) ) ? array() : wp_list_pluck( $terms, 'term_id' );
}

/**
 * For a given product, and optionally price/qty, work out the price with tax included, based on store settings.
 *
 * @since  3.0.0
 * @param  WC_Product $product WC_Product object.
 * @param  array      $args Optional arguments to pass product quantity and price.
 * @return float|string Price with tax included, or an empty string if price calculation failed.
 */
function wc_get_price_including_tax( $product, $args = array() ) {
	$args = wp_parse_args(
		$args,
		array(
			'qty'   => '',
			'price' => '',
		)
	);

	$price = '' !== $args['price'] ? max( 0.0, (float) $args['price'] ) : $product->get_price();
	$qty   = '' !== $args['qty'] ? max( 0.0, (float) $args['qty'] ) : 1;

	if ( '' === $price ) {
		return '';
	} elseif ( empty( $qty ) ) {
		return 0.0;
	}

	$line_price   = $price * $qty;
	$return_price = $line_price;

	if ( $product->is_taxable() ) {
		if ( ! wc_prices_include_tax() ) {
			$tax_rates = WC_Tax::get_rates( $product->get_tax_class() );
			$taxes     = WC_Tax::calc_tax( $line_price, $tax_rates, false );

			if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) {
				$taxes_total = array_sum( $taxes );
			} else {
				$taxes_total = array_sum( array_map( 'wc_round_tax_total', $taxes ) );
			}

			$return_price = NumberUtil::round( $line_price + $taxes_total, wc_get_price_decimals() );
		} else {
			$tax_rates      = WC_Tax::get_rates( $product->get_tax_class() );
			$base_tax_rates = WC_Tax::get_base_tax_rates( $product->get_tax_class( 'unfiltered' ) );

			/**
			 * If the customer is excempt from VAT, remove the taxes here.
			 * Either remove the base or the user taxes depending on woocommerce_adjust_non_base_location_prices setting.
			 */
			if ( ! empty( WC()->customer ) && WC()->customer->get_is_vat_exempt() ) { // @codingStandardsIgnoreLine.
				$remove_taxes = apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ? WC_Tax::calc_tax( $line_price, $base_tax_rates, true ) : WC_Tax::calc_tax( $line_price, $tax_rates, true );

				if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) {
					$remove_taxes_total = array_sum( $remove_taxes );
				} else {
					$remove_taxes_total = array_sum( array_map( 'wc_round_tax_total', $remove_taxes ) );
				}

				$return_price = NumberUtil::round( $line_price - $remove_taxes_total, wc_get_price_decimals() );

				/**
			 * The woocommerce_adjust_non_base_location_prices filter can stop base taxes being taken off when dealing with out of base locations.
			 * e.g. If a product costs 10 including tax, all users will pay 10 regardless of location and taxes.
			 * This feature is experimental @since 2.4.7 and may change in the future. Use at your risk.
			 */
			} elseif ( $tax_rates !== $base_tax_rates && apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) {
				$base_taxes   = WC_Tax::calc_tax( $line_price, $base_tax_rates, true );
				$modded_taxes = WC_Tax::calc_tax( $line_price - array_sum( $base_taxes ), $tax_rates, false );

				if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) {
					$base_taxes_total   = array_sum( $base_taxes );
					$modded_taxes_total = array_sum( $modded_taxes );
				} else {
					$base_taxes_total   = array_sum( array_map( 'wc_round_tax_total', $base_taxes ) );
					$modded_taxes_total = array_sum( array_map( 'wc_round_tax_total', $modded_taxes ) );
				}

				$return_price = NumberUtil::round( $line_price - $base_taxes_total + $modded_taxes_total, wc_get_price_decimals() );
			}
		}
	}
	return apply_filters( 'woocommerce_get_price_including_tax', $return_price, $qty, $product );
}

/**
 * For a given product, and optionally price/qty, work out the price with tax excluded, based on store settings.
 *
 * @since  3.0.0
 * @param  WC_Product $product WC_Product object.
 * @param  array      $args Optional arguments to pass product quantity and price.
 * @return float|string Price with tax excluded, or an empty string if price calculation failed.
 */
function wc_get_price_excluding_tax( $product, $args = array() ) {
	$args = wp_parse_args(
		$args,
		array(
			'qty'   => '',
			'price' => '',
		)
	);

	$price = '' !== $args['price'] ? max( 0.0, (float) $args['price'] ) : $product->get_price();
	$qty   = '' !== $args['qty'] ? max( 0.0, (float) $args['qty'] ) : 1;

	if ( '' === $price ) {
		return '';
	} elseif ( empty( $qty ) ) {
		return 0.0;
	}

	$line_price = $price * $qty;

	if ( $product->is_taxable() && wc_prices_include_tax() ) {
		$order       = ArrayUtil::get_value_or_default( $args, 'order' );
		$customer_id = $order ? $order->get_customer_id() : 0;
		if ( apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) || ! $customer_id ) {
			$tax_rates = WC_Tax::get_base_tax_rates( $product->get_tax_class( 'unfiltered' ) );
		} else {
			$customer  = wc_get_container()->get( LegacyProxy::class )->get_instance_of( WC_Customer::class, $customer_id );
			$tax_rates = WC_Tax::get_rates( $product->get_tax_class(), $customer );
		}
		$remove_taxes = WC_Tax::calc_tax( $line_price, $tax_rates, true );
		$return_price = $line_price - array_sum( $remove_taxes ); // Unrounded since we're dealing with tax inclusive prices. Matches logic in cart-totals class. @see adjust_non_base_location_price.
	} else {
		$return_price = $line_price;
	}

	return apply_filters( 'woocommerce_get_price_excluding_tax', $return_price, $qty, $product );
}

/**
 * Returns the price including or excluding tax, based on the 'woocommerce_tax_display_shop' setting.
 *
 * @since  3.0.0
 * @param  WC_Product $product WC_Product object.
 * @param  array      $args Optional arguments to pass product quantity and price.
 * @return float
 */
function wc_get_price_to_display( $product, $args = array() ) {
	$args = wp_parse_args(
		$args,
		array(
			'qty'   => 1,
			'price' => $product->get_price(),
		)
	);

	$price = $args['price'];
	$qty   = $args['qty'];

	return 'incl' === get_option( 'woocommerce_tax_display_shop' ) ?
		wc_get_price_including_tax(
			$product,
			array(
				'qty'   => $qty,
				'price' => $price,
			)
		) :
		wc_get_price_excluding_tax(
			$product,
			array(
				'qty'   => $qty,
				'price' => $price,
			)
		);
}

/**
 * Returns the product categories in a list.
 *
 * @param int    $product_id Product ID.
 * @param string $sep (default: ', ').
 * @param string $before (default: '').
 * @param string $after (default: '').
 * @return string
 */
function wc_get_product_category_list( $product_id, $sep = ', ', $before = '', $after = '' ) {
	return get_the_term_list( $product_id, 'product_cat', $before, $sep, $after );
}

/**
 * Returns the product tags in a list.
 *
 * @param int    $product_id Product ID.
 * @param string $sep (default: ', ').
 * @param string $before (default: '').
 * @param string $after (default: '').
 * @return string
 */
function wc_get_product_tag_list( $product_id, $sep = ', ', $before = '', $after = '' ) {
	return get_the_term_list( $product_id, 'product_tag', $before, $sep, $after );
}

/**
 * Callback for array filter to get visible only.
 *
 * @since  3.0.0
 * @param  WC_Product $product WC_Product object.
 * @return bool
 */
function wc_products_array_filter_visible( $product ) {
	return $product && is_a( $product, 'WC_Product' ) && $product->is_visible();
}

/**
 * Callback for array filter to get visible grouped products only.
 *
 * @since  3.1.0
 * @param  WC_Product $product WC_Product object.
 * @return bool
 */
function wc_products_array_filter_visible_grouped( $product ) {
	return $product && is_a( $product, 'WC_Product' ) && ( 'publish' === $product->get_status() || current_user_can( 'edit_product', $product->get_id() ) );
}

/**
 * Callback for array filter to get products the user can edit only.
 *
 * @since  3.0.0
 * @param  WC_Product $product WC_Product object.
 * @return bool
 */
function wc_products_array_filter_editable( $product ) {
	return $product && is_a( $product, 'WC_Product' ) && current_user_can( 'edit_product', $product->get_id() );
}

/**
 * Callback for array filter to get products the user can view only.
 *
 * @since  3.4.0
 * @param  WC_Product $product WC_Product object.
 * @return bool
 */
function wc_products_array_filter_readable( $product ) {
	return $product && is_a( $product, 'WC_Product' ) && current_user_can( 'read_product', $product->get_id() );
}

/**
 * Sort an array of products by a value.
 *
 * @since  3.0.0
 *
 * @param array  $products List of products to be ordered.
 * @param string $orderby Optional order criteria.
 * @param string $order Ascending or descending order.
 *
 * @return array
 */
function wc_products_array_orderby( $products, $orderby = 'date', $order = 'desc' ) {
	$orderby = strtolower( $orderby );
	$order   = strtolower( $order );
	switch ( $orderby ) {
		case 'title':
		case 'id':
		case 'date':
		case 'modified':
		case 'menu_order':
		case 'price':
			usort( $products, 'wc_products_array_orderby_' . $orderby );
			break;
		case 'none':
			break;
		default:
			shuffle( $products );
			break;
	}
	if ( 'desc' === $order ) {
		$products = array_reverse( $products );
	}
	return $products;
}

/**
 * Sort by title.
 *
 * @since  3.0.0
 * @param  WC_Product $a First WC_Product object.
 * @param  WC_Product $b Second WC_Product object.
 * @return int
 */
function wc_products_array_orderby_title( $a, $b ) {
	return strcasecmp( $a->get_name(), $b->get_name() );
}

/**
 * Sort by id.
 *
 * @since  3.0.0
 * @param  WC_Product $a First WC_Product object.
 * @param  WC_Product $b Second WC_Product object.
 * @return int
 */
function wc_products_array_orderby_id( $a, $b ) {
	if ( $a->get_id() === $b->get_id() ) {
		return 0;
	}
	return ( $a->get_id() < $b->get_id() ) ? -1 : 1;
}

/**
 * Sort by date.
 *
 * @since  3.0.0
 * @param  WC_Product $a First WC_Product object.
 * @param  WC_Product $b Second WC_Product object.
 * @return int
 */
function wc_products_array_orderby_date( $a, $b ) {
	if ( $a->get_date_created() === $b->get_date_created() ) {
		return 0;
	}
	return ( $a->get_date_created() < $b->get_date_created() ) ? -1 : 1;
}

/**
 * Sort by modified.
 *
 * @since  3.0.0
 * @param  WC_Product $a First WC_Product object.
 * @param  WC_Product $b Second WC_Product object.
 * @return int
 */
function wc_products_array_orderby_modified( $a, $b ) {
	if ( $a->get_date_modified() === $b->get_date_modified() ) {
		return 0;
	}
	return ( $a->get_date_modified() < $b->get_date_modified() ) ? -1 : 1;
}

/**
 * Sort by menu order.
 *
 * @since  3.0.0
 * @param  WC_Product $a First WC_Product object.
 * @param  WC_Product $b Second WC_Product object.
 * @return int
 */
function wc_products_array_orderby_menu_order( $a, $b ) {
	if ( $a->get_menu_order() === $b->get_menu_order() ) {
		return 0;
	}
	return ( $a->get_menu_order() < $b->get_menu_order() ) ? -1 : 1;
}

/**
 * Sort by price low to high.
 *
 * @since  3.0.0
 * @param  WC_Product $a First WC_Product object.
 * @param  WC_Product $b Second WC_Product object.
 * @return int
 */
function wc_products_array_orderby_price( $a, $b ) {
	if ( $a->get_price() === $b->get_price() ) {
		return 0;
	}
	return ( $a->get_price() < $b->get_price() ) ? -1 : 1;
}

/**
 * Queue a product for syncing at the end of the request.
 *
 * @param int $product_id Product ID.
 */
function wc_deferred_product_sync( $product_id ) {
	global $wc_deferred_product_sync;

	if ( empty( $wc_deferred_product_sync ) ) {
		$wc_deferred_product_sync = array();
	}

	$wc_deferred_product_sync[] = $product_id;
}

/**
 * See if the lookup table is being generated already.
 *
 * @since 3.6.0
 * @return bool
 */
function wc_update_product_lookup_tables_is_running() {
	$table_updates_pending = WC()->queue()->search(
		array(
			'status'   => 'pending',
			'group'    => 'wc_update_product_lookup_tables',
			'per_page' => 1,
		)
	);

	return (bool) count( $table_updates_pending );
}

/**
 * Populate lookup table data for products.
 *
 * @since 3.6.0
 */
function wc_update_product_lookup_tables() {
	global $wpdb;

	$is_cli = Constants::is_true( 'WP_CLI' );

	if ( ! $is_cli ) {
		WC_Admin_Notices::add_notice( 'regenerating_lookup_table' );
	}

	// Note that the table is not yet generated.
	update_option( 'woocommerce_product_lookup_table_is_generating', true );

	// Make a row per product in lookup table.
	$wpdb->query(
		"
		INSERT IGNORE INTO {$wpdb->wc_product_meta_lookup} (`product_id`)
		SELECT
			posts.ID
		FROM {$wpdb->posts} posts
		WHERE
			posts.post_type IN ('product', 'product_variation')
		"
	);

	// List of column names in the lookup table we need to populate.
	$columns = array(
		'min_max_price',
		'stock_quantity',
		'sku',
		'stock_status',
		'average_rating',
		'total_sales',
		'downloadable',
		'virtual',
		'onsale',
		'tax_class',
		'tax_status', // When last column is updated, woocommerce_product_lookup_table_is_generating is updated.
	);

	foreach ( $columns as $index => $column ) {
		if ( $is_cli ) {
			wc_update_product_lookup_tables_column( $column );
		} else {
			WC()->queue()->schedule_single(
				time() + $index,
				'wc_update_product_lookup_tables_column',
				array(
					'column' => $column,
				),
				'wc_update_product_lookup_tables'
			);
		}
	}

	// Rating counts are serialised so they have to be unserialised before populating the lookup table.
	if ( $is_cli ) {
		$rating_count_rows = $wpdb->get_results(
			"
			SELECT post_id, meta_value FROM {$wpdb->postmeta}
			WHERE meta_key = '_wc_rating_count'
			AND meta_value != ''
			AND meta_value != 'a:0:{}'
			",
			ARRAY_A
		);
		wc_update_product_lookup_tables_rating_count( $rating_count_rows );
	} else {
		WC()->queue()->schedule_single(
			time() + 10,
			'wc_update_product_lookup_tables_rating_count_batch',
			array(
				'offset' => 0,
				'limit'  => 50,
			),
			'wc_update_product_lookup_tables'
		);
	}
}

/**
 * Populate lookup table column data.
 *
 * @since 3.6.0
 * @param string $column Column name to set.
 */
function wc_update_product_lookup_tables_column( $column ) {
	if ( empty( $column ) ) {
		return;
	}
	global $wpdb;
	switch ( $column ) {
		case 'min_max_price':
			$wpdb->query(
				"
				UPDATE
					{$wpdb->wc_product_meta_lookup} lookup_table
					INNER JOIN (
						SELECT lookup_table.product_id, MIN( meta_value+0 ) as min_price, MAX( meta_value+0 ) as max_price
						FROM {$wpdb->wc_product_meta_lookup} lookup_table
						LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = '_price'
						WHERE
							meta1.meta_value <> ''
						GROUP BY lookup_table.product_id
					) as source on source.product_id = lookup_table.product_id
				SET
					lookup_table.min_price = source.min_price,
					lookup_table.max_price = source.max_price
				"
			);
			break;
		case 'stock_quantity':
			$wpdb->query(
				"
				UPDATE
					{$wpdb->wc_product_meta_lookup} lookup_table
					LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = '_manage_stock'
					LEFT JOIN {$wpdb->postmeta} meta2 ON lookup_table.product_id = meta2.post_id AND meta2.meta_key = '_stock'
				SET
					lookup_table.stock_quantity = meta2.meta_value
				WHERE
					meta1.meta_value = 'yes'
				"
			);
			break;
		case 'sku':
		case 'stock_status':
		case 'average_rating':
		case 'total_sales':
		case 'tax_class':
		case 'tax_status':
			if ( 'total_sales' === $column ) {
				$meta_key = 'total_sales';
			} elseif ( 'average_rating' === $column ) {
				$meta_key = '_wc_average_rating';
			} else {
				$meta_key = '_' . $column;
			}
			$column = esc_sql( $column );
			// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$wpdb->query(
				$wpdb->prepare(
					"
					UPDATE
						{$wpdb->wc_product_meta_lookup} lookup_table
						LEFT JOIN {$wpdb->postmeta} meta ON lookup_table.product_id = meta.post_id AND meta.meta_key = %s
					SET
						lookup_table.`{$column}` = meta.meta_value
					",
					$meta_key
				)
			);
			// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			break;
		case 'downloadable':
		case 'virtual':
			$column   = esc_sql( $column );
			$meta_key = '_' . $column;
			// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$wpdb->query(
				$wpdb->prepare(
					"
					UPDATE
						{$wpdb->wc_product_meta_lookup} lookup_table
						LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = %s
					SET
						lookup_table.`{$column}` = IF ( meta1.meta_value = 'yes', 1, 0 )
					",
					$meta_key
				)
			);
			// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			break;
		case 'onsale':
			$column   = esc_sql( $column );
			$decimals = absint( wc_get_price_decimals() );

			// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$wpdb->query(
				$wpdb->prepare(
					"
					UPDATE
						{$wpdb->wc_product_meta_lookup} lookup_table
						LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = '_price'
						LEFT JOIN {$wpdb->postmeta} meta2 ON lookup_table.product_id = meta2.post_id AND meta2.meta_key = '_sale_price'
					SET
						lookup_table.`{$column}` = IF (
							CAST( meta1.meta_value AS DECIMAL ) >= 0
							AND CAST( meta2.meta_value AS CHAR ) != ''
							AND CAST( meta1.meta_value AS DECIMAL( 10, %d ) ) = CAST( meta2.meta_value AS DECIMAL( 10, %d ) )
						, 1, 0 )
					",
					$decimals,
					$decimals
				)
			);
			// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			break;
	}

	// Final column - mark complete.
	if ( 'tax_status' === $column ) {
		delete_option( 'woocommerce_product_lookup_table_is_generating' );
	}
}
add_action( 'wc_update_product_lookup_tables_column', 'wc_update_product_lookup_tables_column' );

/**
 * Populate rating count lookup table data for products.
 *
 * @since 3.6.0
 * @param array $rows Rows of rating counts to update in lookup table.
 */
function wc_update_product_lookup_tables_rating_count( $rows ) {
	if ( ! $rows || ! is_array( $rows ) ) {
		return;
	}
	global $wpdb;

	foreach ( $rows as $row ) {
		$count = array_sum( (array) maybe_unserialize( $row['meta_value'] ) );
		$wpdb->update(
			$wpdb->wc_product_meta_lookup,
			array(
				'rating_count' => absint( $count ),
			),
			array(
				'product_id' => absint( $row['post_id'] ),
			)
		);
	}
}

/**
 * Populate a batch of rating count lookup table data for products.
 *
 * @since 3.6.2
 * @param array $offset Offset to query.
 * @param array $limit  Limit to query.
 */
function wc_update_product_lookup_tables_rating_count_batch( $offset = 0, $limit = 0 ) {
	global $wpdb;

	if ( ! $limit ) {
		return;
	}

	$rating_count_rows = $wpdb->get_results(
		$wpdb->prepare(
			"
			SELECT post_id, meta_value FROM {$wpdb->postmeta}
			WHERE meta_key = '_wc_rating_count'
			AND meta_value != ''
			AND meta_value != 'a:0:{}'
			ORDER BY post_id ASC
			LIMIT %d, %d
			",
			$offset,
			$limit
		),
		ARRAY_A
	);

	if ( $rating_count_rows ) {
		wc_update_product_lookup_tables_rating_count( $rating_count_rows );
		WC()->queue()->schedule_single(
			time() + 1,
			'wc_update_product_lookup_tables_rating_count_batch',
			array(
				'offset' => $offset + $limit,
				'limit'  => $limit,
			),
			'wc_update_product_lookup_tables'
		);
	}
}
add_action( 'wc_update_product_lookup_tables_rating_count_batch', 'wc_update_product_lookup_tables_rating_count_batch', 10, 2 );
class-wc-order-item-fee.php000064400000021276151336044760011614 0ustar00<?php
/**
 * Order Line Item (fee)
 *
 * Fee is an amount of money charged for a particular piece of work
 * or for a particular right or service, and not supposed to be negative.
 *
 * @package WooCommerce\Classes
 * @version 3.0.0
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Order item fee.
 */
class WC_Order_Item_Fee extends WC_Order_Item {

	/**
	 * Order Data array. This is the core order data exposed in APIs since 3.0.0.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $extra_data = array(
		'tax_class'  => '',
		'tax_status' => 'taxable',
		'amount'     => '',
		'total'      => '',
		'total_tax'  => '',
		'taxes'      => array(
			'total' => array(),
		),
	);

	/**
	 * Get item costs grouped by tax class.
	 *
	 * @since  3.2.0
	 * @param  WC_Order $order Order object.
	 * @return array
	 */
	protected function get_tax_class_costs( $order ) {
		$order_item_tax_classes = $order->get_items_tax_classes();
		$costs                  = array_fill_keys( $order_item_tax_classes, 0 );
		$costs['non-taxable']   = 0;

		foreach ( $order->get_items( array( 'line_item', 'fee', 'shipping' ) ) as $item ) {
			if ( 0 > $item->get_total() ) {
				continue;
			}
			if ( 'taxable' !== $item->get_tax_status() ) {
				$costs['non-taxable'] += $item->get_total();
			} elseif ( 'inherit' === $item->get_tax_class() ) {
				$inherit_class            = reset( $order_item_tax_classes );
				$costs[ $inherit_class ] += $item->get_total();
			} else {
				$costs[ $item->get_tax_class() ] += $item->get_total();
			}
		}

		return array_filter( $costs );
	}
	/**
	 * Calculate item taxes.
	 *
	 * @since  3.2.0
	 * @param  array $calculate_tax_for Location data to get taxes for. Required.
	 * @return bool  True if taxes were calculated.
	 */
	public function calculate_taxes( $calculate_tax_for = array() ) {
		if ( ! isset( $calculate_tax_for['country'], $calculate_tax_for['state'], $calculate_tax_for['postcode'], $calculate_tax_for['city'] ) ) {
			return false;
		}
		// Use regular calculation unless the fee is negative.
		if ( 0 <= $this->get_total() ) {
			return parent::calculate_taxes( $calculate_tax_for );
		}

		if ( wc_tax_enabled() && $this->get_order() ) {
			// Apportion taxes to order items, shipping, and fees.
			$order           = $this->get_order();
			$tax_class_costs = $this->get_tax_class_costs( $order );
			$total_costs     = array_sum( $tax_class_costs );
			$discount_taxes  = array();
			if ( $total_costs ) {
				foreach ( $tax_class_costs as $tax_class => $tax_class_cost ) {
					if ( 'non-taxable' === $tax_class ) {
						continue;
					}
					$proportion                     = $tax_class_cost / $total_costs;
					$cart_discount_proportion       = $this->get_total() * $proportion;
					$calculate_tax_for['tax_class'] = $tax_class;
					$tax_rates                      = WC_Tax::find_rates( $calculate_tax_for );
					$discount_taxes                 = wc_array_merge_recursive_numeric( $discount_taxes, WC_Tax::calc_tax( $cart_discount_proportion, $tax_rates ) );
				}
			}
			$this->set_taxes( array( 'total' => $discount_taxes ) );
		} else {
			$this->set_taxes( false );
		}

		do_action( 'woocommerce_order_item_fee_after_calculate_taxes', $this, $calculate_tax_for );

		return true;
	}

	/*
	|--------------------------------------------------------------------------
	| Setters
	|--------------------------------------------------------------------------
	*/

	/**
	 * Set fee amount.
	 *
	 * @param string $value Amount.
	 */
	public function set_amount( $value ) {
		$this->set_prop( 'amount', wc_format_decimal( $value ) );
	}

	/**
	 * Set tax class.
	 *
	 * @param string $value Tax class.
	 */
	public function set_tax_class( $value ) {
		if ( $value && ! in_array( $value, WC_Tax::get_tax_class_slugs(), true ) ) {
			$this->error( 'order_item_fee_invalid_tax_class', __( 'Invalid tax class', 'woocommerce' ) );
		}
		$this->set_prop( 'tax_class', $value );
	}

	/**
	 * Set tax_status.
	 *
	 * @param string $value Tax status.
	 */
	public function set_tax_status( $value ) {
		if ( in_array( $value, array( 'taxable', 'none' ), true ) ) {
			$this->set_prop( 'tax_status', $value );
		} else {
			$this->set_prop( 'tax_status', 'taxable' );
		}
	}

	/**
	 * Set total.
	 *
	 * @param string $amount Fee amount (do not enter negative amounts).
	 */
	public function set_total( $amount ) {
		$this->set_prop( 'total', wc_format_decimal( $amount ) );
	}

	/**
	 * Set total tax.
	 *
	 * @param string $amount Amount.
	 */
	public function set_total_tax( $amount ) {
		$this->set_prop( 'total_tax', wc_format_decimal( $amount ) );
	}

	/**
	 * Set taxes.
	 *
	 * This is an array of tax ID keys with total amount values.
	 *
	 * @param array $raw_tax_data Raw tax data.
	 */
	public function set_taxes( $raw_tax_data ) {
		$raw_tax_data = maybe_unserialize( $raw_tax_data );
		$tax_data     = array(
			'total' => array(),
		);
		if ( ! empty( $raw_tax_data['total'] ) ) {
			$tax_data['total'] = array_map( 'wc_format_decimal', $raw_tax_data['total'] );
		}
		$this->set_prop( 'taxes', $tax_data );

		if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) {
			$this->set_total_tax( array_sum( $tax_data['total'] ) );
		} else {
			$this->set_total_tax( array_sum( array_map( 'wc_round_tax_total', $tax_data['total'] ) ) );
		}
	}

	/*
	|--------------------------------------------------------------------------
	| Getters
	|--------------------------------------------------------------------------
	*/

	/**
	 * Get fee amount.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_amount( $context = 'view' ) {
		return $this->get_prop( 'amount', $context );
	}

	/**
	 * Get order item name.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_name( $context = 'view' ) {
		$name = $this->get_prop( 'name', $context );
		if ( 'view' === $context ) {
			return $name ? $name : __( 'Fee', 'woocommerce' );
		} else {
			return $name;
		}
	}

	/**
	 * Get order item type.
	 *
	 * @return string
	 */
	public function get_type() {
		return 'fee';
	}

	/**
	 * Get tax class.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_tax_class( $context = 'view' ) {
		return $this->get_prop( 'tax_class', $context );
	}

	/**
	 * Get tax status.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_tax_status( $context = 'view' ) {
		return $this->get_prop( 'tax_status', $context );
	}

	/**
	 * Get total fee.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_total( $context = 'view' ) {
		return $this->get_prop( 'total', $context );
	}

	/**
	 * Get total tax.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_total_tax( $context = 'view' ) {
		return $this->get_prop( 'total_tax', $context );
	}

	/**
	 * Get fee taxes.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return array
	 */
	public function get_taxes( $context = 'view' ) {
		return $this->get_prop( 'taxes', $context );
	}

	/*
	|--------------------------------------------------------------------------
	| Array Access Methods
	|--------------------------------------------------------------------------
	|
	| For backwards compatibility with legacy arrays.
	|
	*/

	/**
	 * OffsetGet for ArrayAccess/Backwards compatibility.
	 *
	 * @param string $offset Offset.
	 * @return mixed
	 */
	public function offsetGet( $offset ) {
		if ( 'line_total' === $offset ) {
			$offset = 'total';
		} elseif ( 'line_tax' === $offset ) {
			$offset = 'total_tax';
		} elseif ( 'line_tax_data' === $offset ) {
			$offset = 'taxes';
		}
		return parent::offsetGet( $offset );
	}

	/**
	 * OffsetSet for ArrayAccess/Backwards compatibility.
	 *
	 * @deprecated 4.4.0
	 * @param string $offset Offset.
	 * @param mixed  $value  Value.
	 */
	public function offsetSet( $offset, $value ) {
		wc_deprecated_function( 'WC_Order_Item_Fee::offsetSet', '4.4.0', '' );
		if ( 'line_total' === $offset ) {
			$offset = 'total';
		} elseif ( 'line_tax' === $offset ) {
			$offset = 'total_tax';
		} elseif ( 'line_tax_data' === $offset ) {
			$offset = 'taxes';
		}
		parent::offsetSet( $offset, $value );
	}

	/**
	 * OffsetExists for ArrayAccess
	 *
	 * @param string $offset Offset.
	 * @return bool
	 */
	public function offsetExists( $offset ) {
		if ( in_array( $offset, array( 'line_total', 'line_tax', 'line_tax_data' ), true ) ) {
			return true;
		}
		return parent::offsetExists( $offset );
	}
}
integrations/maxmind-geolocation/views/html-admin-options.php000064400000001507151336044760020615 0ustar00<?php
/**
 * Admin View: Page - Admin options.
 *
 * @package WooCommerce\Integrations
 */

defined( 'ABSPATH' ) || exit;

?>

<table class="form-table">
	<tr valign="top">
		<th scope="row" class="titledesc">
			<label><?php esc_html_e( 'Database File Path', 'woocommerce' ); ?></label>
		</th>
		<td class="forminp">
			<fieldset>
				<legend class="screen-reader-text"><span><?php esc_html_e( 'Database File Path', 'woocommerce' ); ?></span></legend>
				<input class="input-text regular-input" type="text" value="<?php echo esc_attr( $this->database_service->get_database_path() ); ?>" readonly>
				<p class="description"><?php esc_html_e( 'The location that the MaxMind database should be stored. By default, the integration will automatically save the database here.', 'woocommerce' ); ?></p>
			</fieldset>
		</td>
	</tr>
</table>
integrations/maxmind-geolocation/class-wc-integration-maxmind-geolocation.php000064400000021451151336044760023726 0ustar00<?php
/**
 * MaxMind Geolocation Integration
 *
 * @version 3.9.0
 * @package WooCommerce\Integrations
 */

defined( 'ABSPATH' ) || exit;

require_once __DIR__ . '/class-wc-integration-maxmind-database-service.php';

/**
 * WC Integration MaxMind Geolocation
 *
 * @since 3.9.0
 */
class WC_Integration_MaxMind_Geolocation extends WC_Integration {

	/**
	 * The service responsible for interacting with the MaxMind database.
	 *
	 * @var WC_Integration_MaxMind_Database_Service
	 */
	private $database_service;

	/**
	 * Initialize the integration.
	 */
	public function __construct() {
		$this->id                 = 'maxmind_geolocation';
		$this->method_title       = __( 'MaxMind Geolocation', 'woocommerce' );
		$this->method_description = __( 'An integration for utilizing MaxMind to do Geolocation lookups. Please note that this integration will only do country lookups.', 'woocommerce' );

		/**
		 * Supports overriding the database service to be used.
		 *
		 * @since 3.9.0
		 * @return mixed|null The geolocation database service.
		 */
		$this->database_service = apply_filters( 'woocommerce_maxmind_geolocation_database_service', null );
		if ( null === $this->database_service ) {
			$this->database_service = new WC_Integration_MaxMind_Database_Service( $this->get_database_prefix() );
		}

		$this->init_form_fields();
		$this->init_settings();

		// Bind to the save action for the settings.
		add_action( 'woocommerce_update_options_integration_' . $this->id, array( $this, 'process_admin_options' ) );

		// Trigger notice if license key is missing.
		add_action( 'update_option_woocommerce_default_customer_address', array( $this, 'display_missing_license_key_notice' ), 1000, 2 );

		/**
		 * Allows for the automatic database update to be disabled.
		 *
		 * @deprecated 3.9.0
		 * @return bool Whether or not the database should be updated periodically.
		 */
		$bind_updater = apply_filters_deprecated(
			'woocommerce_geolocation_update_database_periodically',
			array( true ),
			'3.9.0',
			'woocommerce_maxmind_geolocation_update_database_periodically'
		);

		/**
		 * Allows for the automatic database update to be disabled.
		 * Note that MaxMind's TOS requires that the databases be updated or removed periodically.
		 *
		 * @since 3.9.0
		 * @param bool $bind_updater Whether or not the database should be updated periodically.
		 */
		$bind_updater = apply_filters( 'woocommerce_maxmind_geolocation_update_database_periodically', $bind_updater );

		// Bind to the scheduled updater action.
		if ( $bind_updater ) {
			add_action( 'woocommerce_geoip_updater', array( $this, 'update_database' ) );
		}

		// Bind to the geolocation filter for MaxMind database lookups.
		add_filter( 'woocommerce_get_geolocation', array( $this, 'get_geolocation' ), 10, 2 );
	}

	/**
	 * Override the normal options so we can print the database file path to the admin,
	 */
	public function admin_options() {
		parent::admin_options();

		include dirname( __FILE__ ) . '/views/html-admin-options.php';
	}

	/**
	 * Initializes the settings fields.
	 */
	public function init_form_fields() {
		$this->form_fields = array(
			'license_key' => array(
				'title'       => __( 'MaxMind License Key', 'woocommerce' ),
				'type'        => 'password',
				'description' => sprintf(
					/* translators: %1$s: Documentation URL */
					__(
						'The key that will be used when dealing with MaxMind Geolocation services. You can read how to generate one in <a href="%1$s">MaxMind Geolocation Integration documentation</a>.',
						'woocommerce'
					),
					'https://docs.woocommerce.com/document/maxmind-geolocation-integration/'
				),
				'desc_tip'    => false,
				'default'     => '',
			),
		);
	}

	/**
	 * Get database service.
	 *
	 * @return WC_Integration_MaxMind_Database_Service|null
	 */
	public function get_database_service() {
		return $this->database_service;
	}

	/**
	 * Checks to make sure that the license key is valid.
	 *
	 * @param string $key The key of the field.
	 * @param mixed  $value The value of the field.
	 * @return mixed
	 * @throws Exception When the license key is invalid.
	 */
	public function validate_license_key_field( $key, $value ) {
		// Trim whitespaces and strip slashes.
		$value = $this->validate_password_field( $key, $value );

		// Empty license keys have no need test downloading a database.
		if ( empty( $value ) ) {
			return $value;
		}

		// Check the license key by attempting to download the Geolocation database.
		$tmp_database_path = $this->database_service->download_database( $value );
		if ( is_wp_error( $tmp_database_path ) ) {
			WC_Admin_Settings::add_error( $tmp_database_path->get_error_message() );

			// Throw an exception to keep from changing this value. This will prevent
			// users from accidentally losing their license key, which cannot
			// be viewed again after generating.
			throw new Exception( $tmp_database_path->get_error_message() );
		}

		// We may as well put this archive to good use, now that we've downloaded one.
		self::update_database( $tmp_database_path );

		// Remove missing license key notice.
		$this->remove_missing_license_key_notice();

		return $value;
	}

	/**
	 * Updates the database used for geolocation queries.
	 *
	 * @param string|null $new_database_path The path to the new database file. Null will fetch a new archive.
	 */
	public function update_database( $new_database_path = null ) {
		// Allow us to easily interact with the filesystem.
		require_once ABSPATH . 'wp-admin/includes/file.php';
		WP_Filesystem();
		global $wp_filesystem;

		// Remove any existing archives to comply with the MaxMind TOS.
		$target_database_path = $this->database_service->get_database_path();

		// If there's no database path, we can't store the database.
		if ( empty( $target_database_path ) ) {
			return;
		}

		if ( $wp_filesystem->exists( $target_database_path ) ) {
			$wp_filesystem->delete( $target_database_path );
		}

		if ( isset( $new_database_path ) ) {
			$tmp_database_path = $new_database_path;
		} else {
			// We can't download a database if there's no license key configured.
			$license_key = $this->get_option( 'license_key' );
			if ( empty( $license_key ) ) {
				return;
			}

			$tmp_database_path = $this->database_service->download_database( $license_key );
			if ( is_wp_error( $tmp_database_path ) ) {
				wc_get_logger()->notice( $tmp_database_path->get_error_message(), array( 'source' => 'maxmind-geolocation' ) );
				return;
			}
		}

		// Move the new database into position.
		$wp_filesystem->move( $tmp_database_path, $target_database_path, true );
		$wp_filesystem->delete( dirname( $tmp_database_path ) );
	}

	/**
	 * Performs a geolocation lookup against the MaxMind database for the given IP address.
	 *
	 * @param array  $data       Geolocation data.
	 * @param string $ip_address The IP address to geolocate.
	 * @return array Geolocation including country code, state, city and postcode based on an IP address.
	 */
	public function get_geolocation( $data, $ip_address ) {
		// WooCommerce look for headers first, and at this moment could be just enough.
		if ( ! empty( $data['country'] ) ) {
			return $data;
		}

		if ( empty( $ip_address ) ) {
			return $data;
		}

		$country_code = $this->database_service->get_iso_country_code_for_ip( $ip_address );

		return array(
			'country'  => $country_code,
			'state'    => '',
			'city'     => '',
			'postcode' => '',
		);
	}

	/**
	 * Fetches the prefix for the MaxMind database file.
	 *
	 * @return string
	 */
	private function get_database_prefix() {
		$prefix = $this->get_option( 'database_prefix' );
		if ( empty( $prefix ) ) {
			$prefix = wp_generate_password( 32, false );
			$this->update_option( 'database_prefix', $prefix );
		}

		return $prefix;
	}

	/**
	 * Add missing license key notice.
	 */
	private function add_missing_license_key_notice() {
		if ( ! class_exists( 'WC_Admin_Notices' ) ) {
			include_once WC_ABSPATH . 'includes/admin/class-wc-admin-notices.php';
		}
		WC_Admin_Notices::add_notice( 'maxmind_license_key' );
	}

	/**
	 * Remove missing license key notice.
	 */
	private function remove_missing_license_key_notice() {
		if ( ! class_exists( 'WC_Admin_Notices' ) ) {
			include_once WC_ABSPATH . 'includes/admin/class-wc-admin-notices.php';
		}
		WC_Admin_Notices::remove_notice( 'maxmind_license_key' );
	}

	/**
	 * Display notice if license key is missing.
	 *
	 * @param mixed $old_value Option old value.
	 * @param mixed $new_value Current value.
	 */
	public function display_missing_license_key_notice( $old_value, $new_value ) {
		if ( ! apply_filters( 'woocommerce_maxmind_geolocation_display_notices', true ) ) {
			return;
		}

		if ( ! in_array( $new_value, array( 'geolocation', 'geolocation_ajax' ), true ) ) {
			$this->remove_missing_license_key_notice();
			return;
		}

		$license_key = $this->get_option( 'license_key' );
		if ( ! empty( $license_key ) ) {
			return;
		}

		$this->add_missing_license_key_notice();
	}
}
integrations/maxmind-geolocation/class-wc-integration-maxmind-database-service.php000064400000011667151336044760024635 0ustar00<?php
/**
 * The database service class file.
 *
 * @version 3.9.0
 * @package WooCommerce\Integrations
 */

defined( 'ABSPATH' ) || exit;

/**
 * The service class responsible for interacting with MaxMind databases.
 *
 * @since 3.9.0
 */
class WC_Integration_MaxMind_Database_Service {

	/**
	 * The name of the MaxMind database to utilize.
	 */
	const DATABASE = 'GeoLite2-Country';

	/**
	 * The extension for the MaxMind database.
	 */
	const DATABASE_EXTENSION = '.mmdb';

	/**
	 * A prefix for the MaxMind database filename.
	 *
	 * @var string
	 */
	private $database_prefix;

	/**
	 * WC_Integration_MaxMind_Database_Service constructor.
	 *
	 * @param string|null $database_prefix A prefix for the MaxMind database filename.
	 */
	public function __construct( $database_prefix ) {
		$this->database_prefix = $database_prefix;
	}

	/**
	 * Fetches the path that the database should be stored.
	 *
	 * @return string The local database path.
	 */
	public function get_database_path() {
		$uploads_dir = wp_upload_dir();

		$database_path = trailingslashit( $uploads_dir['basedir'] ) . 'woocommerce_uploads/';
		if ( ! empty( $this->database_prefix ) ) {
			$database_path .= $this->database_prefix . '-';
		}
		$database_path .= self::DATABASE . self::DATABASE_EXTENSION;

		/**
		 * Filter the geolocation database storage path.
		 *
		 * @param string $database_path The path to the database.
		 * @param int $version Deprecated since 3.4.0.
		 * @deprecated 3.9.0
		 */
		$database_path = apply_filters_deprecated(
			'woocommerce_geolocation_local_database_path',
			array( $database_path, 2 ),
			'3.9.0',
			'woocommerce_maxmind_geolocation_database_path'
		);

		/**
		 * Filter the geolocation database storage path.
		 *
		 * @since 3.9.0
		 * @param string $database_path The path to the database.
		 */
		return apply_filters( 'woocommerce_maxmind_geolocation_database_path', $database_path );
	}

	/**
	 * Fetches the database from the MaxMind service.
	 *
	 * @param string $license_key The license key to be used when downloading the database.
	 * @return string|WP_Error The path to the database file or an error if invalid.
	 */
	public function download_database( $license_key ) {
		$download_uri = add_query_arg(
			array(
				'edition_id'  => self::DATABASE,
				'license_key' => urlencode( wc_clean( $license_key ) ),
				'suffix'      => 'tar.gz',
			),
			'https://download.maxmind.com/app/geoip_download'
		);

		// Needed for the download_url call right below.
		require_once ABSPATH . 'wp-admin/includes/file.php';

		$tmp_archive_path = download_url( esc_url_raw( $download_uri ) );
		if ( is_wp_error( $tmp_archive_path ) ) {
			// Transform the error into something more informative.
			$error_data = $tmp_archive_path->get_error_data();
			if ( isset( $error_data['code'] ) ) {
				switch ( $error_data['code'] ) {
					case 401:
						return new WP_Error(
							'woocommerce_maxmind_geolocation_database_license_key',
							__( 'The MaxMind license key is invalid. If you have recently created this key, you may need to wait for it to become active.', 'woocommerce' )
						);
				}
			}

			return new WP_Error( 'woocommerce_maxmind_geolocation_database_download', __( 'Failed to download the MaxMind database.', 'woocommerce' ) );
		}

		// Extract the database from the archive.
		try {
			$file = new PharData( $tmp_archive_path );

			$tmp_database_path = trailingslashit( dirname( $tmp_archive_path ) ) . trailingslashit( $file->current()->getFilename() ) . self::DATABASE . self::DATABASE_EXTENSION;

			$file->extractTo(
				dirname( $tmp_archive_path ),
				trailingslashit( $file->current()->getFilename() ) . self::DATABASE . self::DATABASE_EXTENSION,
				true
			);
		} catch ( Exception $exception ) {
			return new WP_Error( 'woocommerce_maxmind_geolocation_database_archive', $exception->getMessage() );
		} finally {
			// Remove the archive since we only care about a single file in it.
			unlink( $tmp_archive_path );
		}

		return $tmp_database_path;
	}

	/**
	 * Fetches the ISO country code associated with an IP address.
	 *
	 * @param string $ip_address The IP address to find the country code for.
	 * @return string The country code for the IP address, or empty if not found.
	 */
	public function get_iso_country_code_for_ip( $ip_address ) {
		$country_code = '';

		if ( ! class_exists( 'MaxMind\Db\Reader' ) ) {
			wc_get_logger()->notice( __( 'Missing MaxMind Reader library!', 'woocommerce' ), array( 'source' => 'maxmind-geolocation' ) );
			return $country_code;
		}

		$database_path = $this->get_database_path();
		if ( ! file_exists( $database_path ) ) {
			return $country_code;
		}

		try {
			$reader = new MaxMind\Db\Reader( $database_path );
			$data   = $reader->get( $ip_address );

			if ( isset( $data['country']['iso_code'] ) ) {
				$country_code = $data['country']['iso_code'];
			}

			$reader->close();
		} catch ( Exception $e ) {
			wc_get_logger()->notice( $e->getMessage(), array( 'source' => 'maxmind-geolocation' ) );
		}

		return $country_code;
	}
}
class-wc-product-factory.php000064400000007124151336044760012131 0ustar00<?php
/**
 * Product Factory
 *
 * The WooCommerce product factory creating the right product object.
 *
 * @package WooCommerce\Classes
 * @version 3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Product factory class.
 */
class WC_Product_Factory {

	/**
	 * Get a product.
	 *
	 * @param mixed $product_id WC_Product|WP_Post|int|bool $product Product instance, post instance, numeric or false to use global $post.
	 * @param array $deprecated Previously used to pass arguments to the factory, e.g. to force a type.
	 * @return WC_Product|bool Product object or false if the product cannot be loaded.
	 */
	public function get_product( $product_id = false, $deprecated = array() ) {
		$product_id = $this->get_product_id( $product_id );

		if ( ! $product_id ) {
			return false;
		}

		$product_type = $this->get_product_type( $product_id );

		// Backwards compatibility.
		if ( ! empty( $deprecated ) ) {
			wc_deprecated_argument( 'args', '3.0', 'Passing args to the product factory is deprecated. If you need to force a type, construct the product class directly.' );

			if ( isset( $deprecated['product_type'] ) ) {
				$product_type = $this->get_classname_from_product_type( $deprecated['product_type'] );
			}
		}

		$classname = $this->get_product_classname( $product_id, $product_type );

		try {
			return new $classname( $product_id, $deprecated );
		} catch ( Exception $e ) {
			return false;
		}
	}

	/**
	 * Gets a product classname and allows filtering. Returns WC_Product_Simple if the class does not exist.
	 *
	 * @since  3.0.0
	 * @param  int    $product_id   Product ID.
	 * @param  string $product_type Product type.
	 * @return string
	 */
	public static function get_product_classname( $product_id, $product_type ) {
		$classname = apply_filters( 'woocommerce_product_class', self::get_classname_from_product_type( $product_type ), $product_type, 'variation' === $product_type ? 'product_variation' : 'product', $product_id );

		if ( ! $classname || ! class_exists( $classname ) ) {
			$classname = 'WC_Product_Simple';
		}

		return $classname;
	}

	/**
	 * Get the product type for a product.
	 *
	 * @since 3.0.0
	 * @param  int $product_id Product ID.
	 * @return string|false
	 */
	public static function get_product_type( $product_id ) {
		// Allow the overriding of the lookup in this function. Return the product type here.
		$override = apply_filters( 'woocommerce_product_type_query', false, $product_id );
		if ( ! $override ) {
			return WC_Data_Store::load( 'product' )->get_product_type( $product_id );
		} else {
			return $override;
		}
	}

	/**
	 * Create a WC coding standards compliant class name e.g. WC_Product_Type_Class instead of WC_Product_type-class.
	 *
	 * @param  string $product_type Product type.
	 * @return string|false
	 */
	public static function get_classname_from_product_type( $product_type ) {
		return $product_type ? 'WC_Product_' . implode( '_', array_map( 'ucfirst', explode( '-', $product_type ) ) ) : false;
	}

	/**
	 * Get the product ID depending on what was passed.
	 *
	 * @since  3.0.0
	 * @param  WC_Product|WP_Post|int|bool $product Product instance, post instance, numeric or false to use global $post.
	 * @return int|bool false on failure
	 */
	private function get_product_id( $product ) {
		global $post;

		if ( false === $product && isset( $post, $post->ID ) && 'product' === get_post_type( $post->ID ) ) {
			return absint( $post->ID );
		} elseif ( is_numeric( $product ) ) {
			return $product;
		} elseif ( $product instanceof WC_Product ) {
			return $product->get_id();
		} elseif ( ! empty( $product->ID ) ) {
			return $product->ID;
		} else {
			return false;
		}
	}
}
class-wc-structured-data.php000064400000042505151336044760012121 0ustar00<?php
/**
 * Structured data's handler and generator using JSON-LD format.
 *
 * @package WooCommerce\Classes
 * @since   3.0.0
 * @version 3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Structured data class.
 */
class WC_Structured_Data {

	/**
	 * Stores the structured data.
	 *
	 * @var array $_data Array of structured data.
	 */
	private $_data = array();

	/**
	 * Constructor.
	 */
	public function __construct() {
		// Generate structured data.
		add_action( 'woocommerce_before_main_content', array( $this, 'generate_website_data' ), 30 );
		add_action( 'woocommerce_breadcrumb', array( $this, 'generate_breadcrumblist_data' ), 10 );
		add_action( 'woocommerce_single_product_summary', array( $this, 'generate_product_data' ), 60 );
		add_action( 'woocommerce_email_order_details', array( $this, 'generate_order_data' ), 20, 3 );

		// Output structured data.
		add_action( 'woocommerce_email_order_details', array( $this, 'output_email_structured_data' ), 30, 3 );
		add_action( 'wp_footer', array( $this, 'output_structured_data' ), 10 );
	}

	/**
	 * Sets data.
	 *
	 * @param  array $data  Structured data.
	 * @param  bool  $reset Unset data (default: false).
	 * @return bool
	 */
	public function set_data( $data, $reset = false ) {
		if ( ! isset( $data['@type'] ) || ! preg_match( '|^[a-zA-Z]{1,20}$|', $data['@type'] ) ) {
			return false;
		}

		if ( $reset && isset( $this->_data ) ) {
			unset( $this->_data );
		}

		$this->_data[] = $data;

		return true;
	}

	/**
	 * Gets data.
	 *
	 * @return array
	 */
	public function get_data() {
		return $this->_data;
	}

	/**
	 * Structures and returns data.
	 *
	 * List of types available by default for specific request:
	 *
	 * 'product',
	 * 'review',
	 * 'breadcrumblist',
	 * 'website',
	 * 'order',
	 *
	 * @param  array $types Structured data types.
	 * @return array
	 */
	public function get_structured_data( $types ) {
		$data = array();

		// Put together the values of same type of structured data.
		foreach ( $this->get_data() as $value ) {
			$data[ strtolower( $value['@type'] ) ][] = $value;
		}

		// Wrap the multiple values of each type inside a graph... Then add context to each type.
		foreach ( $data as $type => $value ) {
			$data[ $type ] = count( $value ) > 1 ? array( '@graph' => $value ) : $value[0];
			$data[ $type ] = apply_filters( 'woocommerce_structured_data_context', array( '@context' => 'https://schema.org/' ), $data, $type, $value ) + $data[ $type ];
		}

		// If requested types, pick them up... Finally change the associative array to an indexed one.
		$data = $types ? array_values( array_intersect_key( $data, array_flip( $types ) ) ) : array_values( $data );

		if ( ! empty( $data ) ) {
			if ( 1 < count( $data ) ) {
				$data = apply_filters( 'woocommerce_structured_data_context', array( '@context' => 'https://schema.org/' ), $data, '', '' ) + array( '@graph' => $data );
			} else {
				$data = $data[0];
			}
		}

		return $data;
	}

	/**
	 * Get data types for pages.
	 *
	 * @return array
	 */
	protected function get_data_type_for_page() {
		$types   = array();
		$types[] = is_shop() || is_product_category() || is_product() ? 'product' : '';
		$types[] = is_shop() && is_front_page() ? 'website' : '';
		$types[] = is_product() ? 'review' : '';
		$types[] = 'breadcrumblist';
		$types[] = 'order';

		return array_filter( apply_filters( 'woocommerce_structured_data_type_for_page', $types ) );
	}

	/**
	 * Makes sure email structured data only outputs on non-plain text versions.
	 *
	 * @param WP_Order $order         Order data.
	 * @param bool     $sent_to_admin Send to admin (default: false).
	 * @param bool     $plain_text    Plain text email (default: false).
	 */
	public function output_email_structured_data( $order, $sent_to_admin = false, $plain_text = false ) {
		if ( $plain_text ) {
			return;
		}
		echo '<div style="display: none; font-size: 0; max-height: 0; line-height: 0; padding: 0; mso-hide: all;">';
		$this->output_structured_data();
		echo '</div>';
	}

	/**
	 * Sanitizes, encodes and outputs structured data.
	 *
	 * Hooked into `wp_footer` action hook.
	 * Hooked into `woocommerce_email_order_details` action hook.
	 */
	public function output_structured_data() {
		$types = $this->get_data_type_for_page();
		$data  = $this->get_structured_data( $types );

		if ( $data ) {
			echo '<script type="application/ld+json">' . wc_esc_json( wp_json_encode( $data ), true ) . '</script>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		}
	}

	/*
	|--------------------------------------------------------------------------
	| Generators
	|--------------------------------------------------------------------------
	|
	| Methods for generating specific structured data types:
	|
	| - Product
	| - Review
	| - BreadcrumbList
	| - WebSite
	| - Order
	|
	| The generated data is stored into `$this->_data`.
	| See the methods above for handling `$this->_data`.
	|
	*/

	/**
	 * Generates Product structured data.
	 *
	 * Hooked into `woocommerce_single_product_summary` action hook.
	 *
	 * @param WC_Product $product Product data (default: null).
	 */
	public function generate_product_data( $product = null ) {
		if ( ! is_object( $product ) ) {
			global $product;
		}

		if ( ! is_a( $product, 'WC_Product' ) ) {
			return;
		}

		$shop_name = get_bloginfo( 'name' );
		$shop_url  = home_url();
		$currency  = get_woocommerce_currency();
		$permalink = get_permalink( $product->get_id() );
		$image     = wp_get_attachment_url( $product->get_image_id() );

		$markup = array(
			'@type'       => 'Product',
			'@id'         => $permalink . '#product', // Append '#product' to differentiate between this @id and the @id generated for the Breadcrumblist.
			'name'        => wp_kses_post( $product->get_name() ),
			'url'         => $permalink,
			'description' => wp_strip_all_tags( do_shortcode( $product->get_short_description() ? $product->get_short_description() : $product->get_description() ) ),
		);

		if ( $image ) {
			$markup['image'] = $image;
		}

		// Declare SKU or fallback to ID.
		if ( $product->get_sku() ) {
			$markup['sku'] = $product->get_sku();
		} else {
			$markup['sku'] = $product->get_id();
		}

		if ( '' !== $product->get_price() ) {
			// Assume prices will be valid until the end of next year, unless on sale and there is an end date.
			$price_valid_until = gmdate( 'Y-12-31', time() + YEAR_IN_SECONDS );

			if ( $product->is_type( 'variable' ) ) {
				$lowest  = $product->get_variation_price( 'min', false );
				$highest = $product->get_variation_price( 'max', false );

				if ( $lowest === $highest ) {
					$markup_offer = array(
						'@type'              => 'Offer',
						'price'              => wc_format_decimal( $lowest, wc_get_price_decimals() ),
						'priceValidUntil'    => $price_valid_until,
						'priceSpecification' => array(
							'price'                 => wc_format_decimal( $lowest, wc_get_price_decimals() ),
							'priceCurrency'         => $currency,
							'valueAddedTaxIncluded' => wc_prices_include_tax() ? 'true' : 'false',
						),
					);
				} else {
					$markup_offer = array(
						'@type'      => 'AggregateOffer',
						'lowPrice'   => wc_format_decimal( $lowest, wc_get_price_decimals() ),
						'highPrice'  => wc_format_decimal( $highest, wc_get_price_decimals() ),
						'offerCount' => count( $product->get_children() ),
					);
				}
			} else {
				if ( $product->is_on_sale() && $product->get_date_on_sale_to() ) {
					$price_valid_until = gmdate( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() );
				}
				$markup_offer = array(
					'@type'              => 'Offer',
					'price'              => wc_format_decimal( $product->get_price(), wc_get_price_decimals() ),
					'priceValidUntil'    => $price_valid_until,
					'priceSpecification' => array(
						'price'                 => wc_format_decimal( $product->get_price(), wc_get_price_decimals() ),
						'priceCurrency'         => $currency,
						'valueAddedTaxIncluded' => wc_prices_include_tax() ? 'true' : 'false',
					),
				);
			}

			$markup_offer += array(
				'priceCurrency' => $currency,
				'availability'  => 'http://schema.org/' . ( $product->is_in_stock() ? 'InStock' : 'OutOfStock' ),
				'url'           => $permalink,
				'seller'        => array(
					'@type' => 'Organization',
					'name'  => $shop_name,
					'url'   => $shop_url,
				),
			);

			$markup['offers'] = array( apply_filters( 'woocommerce_structured_data_product_offer', $markup_offer, $product ) );
		}

		if ( $product->get_rating_count() && wc_review_ratings_enabled() ) {
			$markup['aggregateRating'] = array(
				'@type'       => 'AggregateRating',
				'ratingValue' => $product->get_average_rating(),
				'reviewCount' => $product->get_review_count(),
			);

			// Markup 5 most recent rating/review.
			$comments = get_comments(
				array(
					'number'      => 5,
					'post_id'     => $product->get_id(),
					'status'      => 'approve',
					'post_status' => 'publish',
					'post_type'   => 'product',
					'parent'      => 0,
					'meta_query'  => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
						array(
							'key'     => 'rating',
							'type'    => 'NUMERIC',
							'compare' => '>',
							'value'   => 0,
						),
					),
				)
			);

			if ( $comments ) {
				$markup['review'] = array();
				foreach ( $comments as $comment ) {
					$markup['review'][] = array(
						'@type'         => 'Review',
						'reviewRating'  => array(
							'@type'       => 'Rating',
							'bestRating'  => '5',
							'ratingValue' => get_comment_meta( $comment->comment_ID, 'rating', true ),
							'worstRating' => '1',
						),
						'author'        => array(
							'@type' => 'Person',
							'name'  => get_comment_author( $comment ),
						),
						'reviewBody'    => get_comment_text( $comment ),
						'datePublished' => get_comment_date( 'c', $comment ),
					);
				}
			}
		}

		// Check we have required data.
		if ( empty( $markup['aggregateRating'] ) && empty( $markup['offers'] ) && empty( $markup['review'] ) ) {
			return;
		}

		$this->set_data( apply_filters( 'woocommerce_structured_data_product', $markup, $product ) );
	}

	/**
	 * Generates Review structured data.
	 *
	 * Hooked into `woocommerce_review_meta` action hook.
	 *
	 * @param WP_Comment $comment Comment data.
	 */
	public function generate_review_data( $comment ) {
		$markup                  = array();
		$markup['@type']         = 'Review';
		$markup['@id']           = get_comment_link( $comment->comment_ID );
		$markup['datePublished'] = get_comment_date( 'c', $comment->comment_ID );
		$markup['description']   = get_comment_text( $comment->comment_ID );
		$markup['itemReviewed']  = array(
			'@type' => 'Product',
			'name'  => get_the_title( $comment->comment_post_ID ),
		);

		// Skip replies unless they have a rating.
		$rating = get_comment_meta( $comment->comment_ID, 'rating', true );

		if ( $rating ) {
			$markup['reviewRating'] = array(
				'@type'       => 'Rating',
				'bestRating'  => '5',
				'ratingValue' => $rating,
				'worstRating' => '1',
			);
		} elseif ( $comment->comment_parent ) {
			return;
		}

		$markup['author'] = array(
			'@type' => 'Person',
			'name'  => get_comment_author( $comment->comment_ID ),
		);

		$this->set_data( apply_filters( 'woocommerce_structured_data_review', $markup, $comment ) );
	}

	/**
	 * Generates BreadcrumbList structured data.
	 *
	 * Hooked into `woocommerce_breadcrumb` action hook.
	 *
	 * @param WC_Breadcrumb $breadcrumbs Breadcrumb data.
	 */
	public function generate_breadcrumblist_data( $breadcrumbs ) {
		$crumbs = $breadcrumbs->get_breadcrumb();

		if ( empty( $crumbs ) || ! is_array( $crumbs ) ) {
			return;
		}

		$markup                    = array();
		$markup['@type']           = 'BreadcrumbList';
		$markup['itemListElement'] = array();

		foreach ( $crumbs as $key => $crumb ) {
			$markup['itemListElement'][ $key ] = array(
				'@type'    => 'ListItem',
				'position' => $key + 1,
				'item'     => array(
					'name' => $crumb[0],
				),
			);

			if ( ! empty( $crumb[1] ) ) {
				$markup['itemListElement'][ $key ]['item'] += array( '@id' => $crumb[1] );
			} elseif ( isset( $_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI'] ) ) {
				$current_url = set_url_scheme( 'http://' . wp_unslash( $_SERVER['HTTP_HOST'] ) . wp_unslash( $_SERVER['REQUEST_URI'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

				$markup['itemListElement'][ $key ]['item'] += array( '@id' => $current_url );
			}
		}

		$this->set_data( apply_filters( 'woocommerce_structured_data_breadcrumblist', $markup, $breadcrumbs ) );
	}

	/**
	 * Generates WebSite structured data.
	 *
	 * Hooked into `woocommerce_before_main_content` action hook.
	 */
	public function generate_website_data() {
		$markup                    = array();
		$markup['@type']           = 'WebSite';
		$markup['name']            = get_bloginfo( 'name' );
		$markup['url']             = home_url();
		$markup['potentialAction'] = array(
			'@type'       => 'SearchAction',
			'target'      => home_url( '?s={search_term_string}&post_type=product' ),
			'query-input' => 'required name=search_term_string',
		);

		$this->set_data( apply_filters( 'woocommerce_structured_data_website', $markup ) );
	}

	/**
	 * Generates Order structured data.
	 *
	 * Hooked into `woocommerce_email_order_details` action hook.
	 *
	 * @param WP_Order $order         Order data.
	 * @param bool     $sent_to_admin Send to admin (default: false).
	 * @param bool     $plain_text    Plain text email (default: false).
	 */
	public function generate_order_data( $order, $sent_to_admin = false, $plain_text = false ) {
		if ( $plain_text || ! is_a( $order, 'WC_Order' ) ) {
			return;
		}

		$shop_name      = get_bloginfo( 'name' );
		$shop_url       = home_url();
		$order_url      = $sent_to_admin ? $order->get_edit_order_url() : $order->get_view_order_url();
		$order_statuses = array(
			'pending'    => 'https://schema.org/OrderPaymentDue',
			'processing' => 'https://schema.org/OrderProcessing',
			'on-hold'    => 'https://schema.org/OrderProblem',
			'completed'  => 'https://schema.org/OrderDelivered',
			'cancelled'  => 'https://schema.org/OrderCancelled',
			'refunded'   => 'https://schema.org/OrderReturned',
			'failed'     => 'https://schema.org/OrderProblem',
		);

		$markup_offers = array();
		foreach ( $order->get_items() as $item ) {
			if ( ! apply_filters( 'woocommerce_order_item_visible', true, $item ) ) {
				continue;
			}

			$product        = $item->get_product();
			$product_exists = is_object( $product );
			$is_visible     = $product_exists && $product->is_visible();

			$markup_offers[] = array(
				'@type'              => 'Offer',
				'price'              => $order->get_line_subtotal( $item ),
				'priceCurrency'      => $order->get_currency(),
				'priceSpecification' => array(
					'price'            => $order->get_line_subtotal( $item ),
					'priceCurrency'    => $order->get_currency(),
					'eligibleQuantity' => array(
						'@type' => 'QuantitativeValue',
						'value' => apply_filters( 'woocommerce_email_order_item_quantity', $item->get_quantity(), $item ),
					),
				),
				'itemOffered'        => array(
					'@type' => 'Product',
					'name'  => wp_kses_post( apply_filters( 'woocommerce_order_item_name', $item->get_name(), $item, $is_visible ) ),
					'sku'   => $product_exists ? $product->get_sku() : '',
					'image' => $product_exists ? wp_get_attachment_image_url( $product->get_image_id() ) : '',
					'url'   => $is_visible ? get_permalink( $product->get_id() ) : get_home_url(),
				),
				'seller'             => array(
					'@type' => 'Organization',
					'name'  => $shop_name,
					'url'   => $shop_url,
				),
			);
		}

		$markup                       = array();
		$markup['@type']              = 'Order';
		$markup['url']                = $order_url;
		$markup['orderStatus']        = isset( $order_statuses[ $order->get_status() ] ) ? $order_statuses[ $order->get_status() ] : '';
		$markup['orderNumber']        = $order->get_order_number();
		$markup['orderDate']          = $order->get_date_created()->format( 'c' );
		$markup['acceptedOffer']      = $markup_offers;
		$markup['discount']           = $order->get_total_discount();
		$markup['discountCurrency']   = $order->get_currency();
		$markup['price']              = $order->get_total();
		$markup['priceCurrency']      = $order->get_currency();
		$markup['priceSpecification'] = array(
			'price'                 => $order->get_total(),
			'priceCurrency'         => $order->get_currency(),
			'valueAddedTaxIncluded' => 'true',
		);
		$markup['billingAddress']     = array(
			'@type'           => 'PostalAddress',
			'name'            => $order->get_formatted_billing_full_name(),
			'streetAddress'   => $order->get_billing_address_1(),
			'postalCode'      => $order->get_billing_postcode(),
			'addressLocality' => $order->get_billing_city(),
			'addressRegion'   => $order->get_billing_state(),
			'addressCountry'  => $order->get_billing_country(),
			'email'           => $order->get_billing_email(),
			'telephone'       => $order->get_billing_phone(),
		);
		$markup['customer']           = array(
			'@type' => 'Person',
			'name'  => $order->get_formatted_billing_full_name(),
		);
		$markup['merchant']           = array(
			'@type' => 'Organization',
			'name'  => $shop_name,
			'url'   => $shop_url,
		);
		$markup['potentialAction']    = array(
			'@type'  => 'ViewAction',
			'name'   => 'View Order',
			'url'    => $order_url,
			'target' => $order_url,
		);

		$this->set_data( apply_filters( 'woocommerce_structured_data_order', $markup, $sent_to_admin, $order ), true );
	}
}
class-wc-shipping-rate.php000064400000012466151336044760011563 0ustar00<?php
/**
 * WooCommerce Shipping Rate
 *
 * Simple Class for storing rates.
 *
 * @package WooCommerce\Classes\Shipping
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Shipping rate class.
 */
class WC_Shipping_Rate {

	/**
	 * Stores data for this rate.
	 *
	 * @since 3.2.0
	 * @var   array
	 */
	protected $data = array(
		'id'          => '',
		'method_id'   => '',
		'instance_id' => 0,
		'label'       => '',
		'cost'        => 0,
		'taxes'       => array(),
	);

	/**
	 * Stores meta data for this rate.
	 *
	 * @since 2.6.0
	 * @var   array
	 */
	protected $meta_data = array();

	/**
	 * Constructor.
	 *
	 * @param string  $id          Shipping rate ID.
	 * @param string  $label       Shipping rate label.
	 * @param integer $cost        Cost.
	 * @param array   $taxes       Taxes applied to shipping rate.
	 * @param string  $method_id   Shipping method ID.
	 * @param int     $instance_id Shipping instance ID.
	 */
	public function __construct( $id = '', $label = '', $cost = 0, $taxes = array(), $method_id = '', $instance_id = 0 ) {
		$this->set_id( $id );
		$this->set_label( $label );
		$this->set_cost( $cost );
		$this->set_taxes( $taxes );
		$this->set_method_id( $method_id );
		$this->set_instance_id( $instance_id );
	}

	/**
	 * Magic methods to support direct access to props.
	 *
	 * @since 3.2.0
	 * @param string $key Key.
	 * @return bool
	 */
	public function __isset( $key ) {
		return isset( $this->data[ $key ] );
	}

	/**
	 * Magic methods to support direct access to props.
	 *
	 * @since 3.2.0
	 * @param string $key Key.
	 * @return mixed
	 */
	public function __get( $key ) {
		if ( is_callable( array( $this, "get_{$key}" ) ) ) {
			return $this->{"get_{$key}"}();
		} elseif ( isset( $this->data[ $key ] ) ) {
			return $this->data[ $key ];
		} else {
			return '';
		}
	}

	/**
	 * Magic methods to support direct access to props.
	 *
	 * @since 3.2.0
	 * @param string $key   Key.
	 * @param mixed  $value Value.
	 */
	public function __set( $key, $value ) {
		if ( is_callable( array( $this, "set_{$key}" ) ) ) {
			$this->{"set_{$key}"}( $value );
		} else {
			$this->data[ $key ] = $value;
		}
	}

	/**
	 * Set ID for the rate. This is usually a combination of the method and instance IDs.
	 *
	 * @since 3.2.0
	 * @param string $id Shipping rate ID.
	 */
	public function set_id( $id ) {
		$this->data['id'] = (string) $id;
	}

	/**
	 * Set shipping method ID the rate belongs to.
	 *
	 * @since 3.2.0
	 * @param string $method_id Shipping method ID.
	 */
	public function set_method_id( $method_id ) {
		$this->data['method_id'] = (string) $method_id;
	}

	/**
	 * Set instance ID the rate belongs to.
	 *
	 * @since 3.2.0
	 * @param int $instance_id Instance ID.
	 */
	public function set_instance_id( $instance_id ) {
		$this->data['instance_id'] = absint( $instance_id );
	}

	/**
	 * Set rate label.
	 *
	 * @since 3.2.0
	 * @param string $label Shipping rate label.
	 */
	public function set_label( $label ) {
		$this->data['label'] = (string) $label;
	}

	/**
	 * Set rate cost.
	 *
	 * @todo 4.0 Prevent negative value being set. #19293
	 * @since 3.2.0
	 * @param string $cost Shipping rate cost.
	 */
	public function set_cost( $cost ) {
		$this->data['cost'] = $cost;
	}

	/**
	 * Set rate taxes.
	 *
	 * @since 3.2.0
	 * @param array $taxes List of taxes applied to shipping rate.
	 */
	public function set_taxes( $taxes ) {
		$this->data['taxes'] = ! empty( $taxes ) && is_array( $taxes ) ? $taxes : array();
	}

	/**
	 * Get ID for the rate. This is usually a combination of the method and instance IDs.
	 *
	 * @since 3.2.0
	 * @return string
	 */
	public function get_id() {
		return apply_filters( 'woocommerce_shipping_rate_id', $this->data['id'], $this );
	}

	/**
	 * Get shipping method ID the rate belongs to.
	 *
	 * @since 3.2.0
	 * @return string
	 */
	public function get_method_id() {
		return apply_filters( 'woocommerce_shipping_rate_method_id', $this->data['method_id'], $this );
	}

	/**
	 * Get instance ID the rate belongs to.
	 *
	 * @since 3.2.0
	 * @return int
	 */
	public function get_instance_id() {
		return apply_filters( 'woocommerce_shipping_rate_instance_id', $this->data['instance_id'], $this );
	}

	/**
	 * Get rate label.
	 *
	 * @return string
	 */
	public function get_label() {
		return apply_filters( 'woocommerce_shipping_rate_label', $this->data['label'], $this );
	}

	/**
	 * Get rate cost.
	 *
	 * @since 3.2.0
	 * @return string
	 */
	public function get_cost() {
		return apply_filters( 'woocommerce_shipping_rate_cost', $this->data['cost'], $this );
	}

	/**
	 * Get rate taxes.
	 *
	 * @since 3.2.0
	 * @return array
	 */
	public function get_taxes() {
		return apply_filters( 'woocommerce_shipping_rate_taxes', $this->data['taxes'], $this );
	}

	/**
	 * Get shipping tax.
	 *
	 * @return array
	 */
	public function get_shipping_tax() {
		return apply_filters( 'woocommerce_get_shipping_tax', count( $this->taxes ) > 0 && ! WC()->customer->get_is_vat_exempt() ? array_sum( $this->taxes ) : 0, $this );
	}

	/**
	 * Add some meta data for this rate.
	 *
	 * @since 2.6.0
	 * @param string $key   Key.
	 * @param string $value Value.
	 */
	public function add_meta_data( $key, $value ) {
		$this->meta_data[ wc_clean( $key ) ] = wc_clean( $value );
	}

	/**
	 * Get all meta data for this rate.
	 *
	 * @since 2.6.0
	 * @return array
	 */
	public function get_meta_data() {
		return $this->meta_data;
	}
}
class-wc-privacy-erasers.php000064400000033156151336044760012127 0ustar00<?php
/**
 * Personal data erasers.
 *
 * @since 3.4.0
 * @package WooCommerce\Classes
 */

defined( 'ABSPATH' ) || exit;

/**
 * WC_Privacy_Erasers Class.
 */
class WC_Privacy_Erasers {
	/**
	 * Finds and erases customer data by email address.
	 *
	 * @since 3.4.0
	 * @param string $email_address The user email address.
	 * @param int    $page  Page.
	 * @return array An array of personal data in name value pairs
	 */
	public static function customer_data_eraser( $email_address, $page ) {
		$response = array(
			'items_removed'  => false,
			'items_retained' => false,
			'messages'       => array(),
			'done'           => true,
		);

		$user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data.

		if ( ! $user instanceof WP_User ) {
			return $response;
		}

		$customer = new WC_Customer( $user->ID );

		if ( ! $customer ) {
			return $response;
		}

		$props_to_erase = apply_filters(
			'woocommerce_privacy_erase_customer_personal_data_props',
			array(
				'billing_first_name'  => __( 'Billing First Name', 'woocommerce' ),
				'billing_last_name'   => __( 'Billing Last Name', 'woocommerce' ),
				'billing_company'     => __( 'Billing Company', 'woocommerce' ),
				'billing_address_1'   => __( 'Billing Address 1', 'woocommerce' ),
				'billing_address_2'   => __( 'Billing Address 2', 'woocommerce' ),
				'billing_city'        => __( 'Billing City', 'woocommerce' ),
				'billing_postcode'    => __( 'Billing Postal/Zip Code', 'woocommerce' ),
				'billing_state'       => __( 'Billing State', 'woocommerce' ),
				'billing_country'     => __( 'Billing Country / Region', 'woocommerce' ),
				'billing_phone'       => __( 'Billing Phone Number', 'woocommerce' ),
				'billing_email'       => __( 'Email Address', 'woocommerce' ),
				'shipping_first_name' => __( 'Shipping First Name', 'woocommerce' ),
				'shipping_last_name'  => __( 'Shipping Last Name', 'woocommerce' ),
				'shipping_company'    => __( 'Shipping Company', 'woocommerce' ),
				'shipping_address_1'  => __( 'Shipping Address 1', 'woocommerce' ),
				'shipping_address_2'  => __( 'Shipping Address 2', 'woocommerce' ),
				'shipping_city'       => __( 'Shipping City', 'woocommerce' ),
				'shipping_postcode'   => __( 'Shipping Postal/Zip Code', 'woocommerce' ),
				'shipping_state'      => __( 'Shipping State', 'woocommerce' ),
				'shipping_country'    => __( 'Shipping Country / Region', 'woocommerce' ),
				'shipping_phone'      => __( 'Shipping Phone Number', 'woocommerce' ),
			),
			$customer
		);

		foreach ( $props_to_erase as $prop => $label ) {
			$erased = false;

			if ( is_callable( array( $customer, 'get_' . $prop ) ) && is_callable( array( $customer, 'set_' . $prop ) ) ) {
				$value = $customer->{"get_$prop"}( 'edit' );

				if ( $value ) {
					$customer->{"set_$prop"}( '' );
					$erased = true;
				}
			}

			$erased = apply_filters( 'woocommerce_privacy_erase_customer_personal_data_prop', $erased, $prop, $customer );

			if ( $erased ) {
				/* Translators: %s Prop name. */
				$response['messages'][]    = sprintf( __( 'Removed customer "%s"', 'woocommerce' ), $label );
				$response['items_removed'] = true;
			}
		}

		$customer->save();

		/**
		 * Allow extensions to remove data for this customer and adjust the response.
		 *
		 * @since 3.4.0
		 * @param array    $response Array resonse data. Must include messages, num_items_removed, num_items_retained, done.
		 * @param WC_Order $order A customer object.
		 */
		return apply_filters( 'woocommerce_privacy_erase_personal_data_customer', $response, $customer );
	}

	/**
	 * Finds and erases data which could be used to identify a person from WooCommerce data assocated with an email address.
	 *
	 * Orders are erased in blocks of 10 to avoid timeouts.
	 *
	 * @since 3.4.0
	 * @param string $email_address The user email address.
	 * @param int    $page  Page.
	 * @return array An array of personal data in name value pairs
	 */
	public static function order_data_eraser( $email_address, $page ) {
		$page            = (int) $page;
		$user            = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data.
		$erasure_enabled = wc_string_to_bool( get_option( 'woocommerce_erasure_request_removes_order_data', 'no' ) );
		$response        = array(
			'items_removed'  => false,
			'items_retained' => false,
			'messages'       => array(),
			'done'           => true,
		);

		$order_query = array(
			'limit'    => 10,
			'page'     => $page,
			'customer' => array( $email_address ),
		);

		if ( $user instanceof WP_User ) {
			$order_query['customer'][] = (int) $user->ID;
		}

		$orders = wc_get_orders( $order_query );

		if ( 0 < count( $orders ) ) {
			foreach ( $orders as $order ) {
				if ( apply_filters( 'woocommerce_privacy_erase_order_personal_data', $erasure_enabled, $order ) ) {
					self::remove_order_personal_data( $order );

					/* Translators: %s Order number. */
					$response['messages'][]    = sprintf( __( 'Removed personal data from order %s.', 'woocommerce' ), $order->get_order_number() );
					$response['items_removed'] = true;
				} else {
					/* Translators: %s Order number. */
					$response['messages'][]     = sprintf( __( 'Personal data within order %s has been retained.', 'woocommerce' ), $order->get_order_number() );
					$response['items_retained'] = true;
				}
			}
			$response['done'] = 10 > count( $orders );
		} else {
			$response['done'] = true;
		}

		return $response;
	}

	/**
	 * Finds and removes customer download logs by email address.
	 *
	 * @since 3.4.0
	 * @param string $email_address The user email address.
	 * @param int    $page  Page.
	 * @return array An array of personal data in name value pairs
	 */
	public static function download_data_eraser( $email_address, $page ) {
		$page            = (int) $page;
		$user            = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data.
		$erasure_enabled = wc_string_to_bool( get_option( 'woocommerce_erasure_request_removes_download_data', 'no' ) );
		$response        = array(
			'items_removed'  => false,
			'items_retained' => false,
			'messages'       => array(),
			'done'           => true,
		);

		$downloads_query = array(
			'limit'  => -1,
			'page'   => $page,
			'return' => 'ids',
		);

		if ( $user instanceof WP_User ) {
			$downloads_query['user_id'] = (int) $user->ID;
		} else {
			$downloads_query['user_email'] = $email_address;
		}

		$customer_download_data_store = WC_Data_Store::load( 'customer-download' );

		// Revoke download permissions.
		if ( apply_filters( 'woocommerce_privacy_erase_download_personal_data', $erasure_enabled, $email_address ) ) {
			if ( $user instanceof WP_User ) {
				$result = $customer_download_data_store->delete_by_user_id( (int) $user->ID );
			} else {
				$result = $customer_download_data_store->delete_by_user_email( $email_address );
			}
			if ( $result ) {
				$response['messages'][]    = __( 'Removed access to downloadable files.', 'woocommerce' );
				$response['items_removed'] = true;
			}
		} else {
			$response['messages'][]     = __( 'Customer download permissions have been retained.', 'woocommerce' );
			$response['items_retained'] = true;
		}

		return $response;
	}

	/**
	 * Remove personal data specific to WooCommerce from an order object.
	 *
	 * Note; this will hinder order processing for obvious reasons!
	 *
	 * @param WC_Order $order Order object.
	 */
	public static function remove_order_personal_data( $order ) {
		$anonymized_data = array();

		/**
		 * Allow extensions to remove their own personal data for this order first, so order data is still available.
		 *
		 * @since 3.4.0
		 * @param WC_Order $order A customer object.
		 */
		do_action( 'woocommerce_privacy_before_remove_order_personal_data', $order );

		/**
		 * Expose props and data types we'll be anonymizing.
		 *
		 * @since 3.4.0
		 * @param array    $props Keys are the prop names, values are the data type we'll be passing to wp_privacy_anonymize_data().
		 * @param WC_Order $order A customer object.
		 */
		$props_to_remove = apply_filters(
			'woocommerce_privacy_remove_order_personal_data_props',
			array(
				'customer_ip_address' => 'ip',
				'customer_user_agent' => 'text',
				'billing_first_name'  => 'text',
				'billing_last_name'   => 'text',
				'billing_company'     => 'text',
				'billing_address_1'   => 'text',
				'billing_address_2'   => 'text',
				'billing_city'        => 'text',
				'billing_postcode'    => 'text',
				'billing_state'       => 'address_state',
				'billing_country'     => 'address_country',
				'billing_phone'       => 'phone',
				'billing_email'       => 'email',
				'shipping_first_name' => 'text',
				'shipping_last_name'  => 'text',
				'shipping_company'    => 'text',
				'shipping_address_1'  => 'text',
				'shipping_address_2'  => 'text',
				'shipping_city'       => 'text',
				'shipping_postcode'   => 'text',
				'shipping_state'      => 'address_state',
				'shipping_country'    => 'address_country',
				'shipping_phone'      => 'phone',
				'customer_id'         => 'numeric_id',
				'transaction_id'      => 'numeric_id',
			),
			$order
		);

		if ( ! empty( $props_to_remove ) && is_array( $props_to_remove ) ) {
			foreach ( $props_to_remove as $prop => $data_type ) {
				// Get the current value in edit context.
				$value = $order->{"get_$prop"}( 'edit' );

				// If the value is empty, it does not need to be anonymized.
				if ( empty( $value ) || empty( $data_type ) ) {
					continue;
				}

				$anon_value = function_exists( 'wp_privacy_anonymize_data' ) ? wp_privacy_anonymize_data( $data_type, $value ) : '';

				/**
				 * Expose a way to control the anonymized value of a prop via 3rd party code.
				 *
				 * @since 3.4.0
				 * @param string   $anon_value Value of this prop after anonymization.
				 * @param string   $prop Name of the prop being removed.
				 * @param string   $value Current value of the data.
				 * @param string   $data_type Type of data.
				 * @param WC_Order $order An order object.
				 */
				$anonymized_data[ $prop ] = apply_filters( 'woocommerce_privacy_remove_order_personal_data_prop_value', $anon_value, $prop, $value, $data_type, $order );
			}
		}

		// Set all new props and persist the new data to the database.
		$order->set_props( $anonymized_data );

		// Remove meta data.
		$meta_to_remove = apply_filters(
			'woocommerce_privacy_remove_order_personal_data_meta',
			array(
				'Payer first name'     => 'text',
				'Payer last name'      => 'text',
				'Payer PayPal address' => 'email',
				'Transaction ID'       => 'numeric_id',
			)
		);

		if ( ! empty( $meta_to_remove ) && is_array( $meta_to_remove ) ) {
			foreach ( $meta_to_remove as $meta_key => $data_type ) {
				$value = $order->get_meta( $meta_key );

				// If the value is empty, it does not need to be anonymized.
				if ( empty( $value ) || empty( $data_type ) ) {
					continue;
				}

				$anon_value = function_exists( 'wp_privacy_anonymize_data' ) ? wp_privacy_anonymize_data( $data_type, $value ) : '';

				/**
				 * Expose a way to control the anonymized value of a value via 3rd party code.
				 *
				 * @since 3.4.0
				 * @param string   $anon_value Value of this data after anonymization.
				 * @param string   $prop meta_key key being removed.
				 * @param string   $value Current value of the data.
				 * @param string   $data_type Type of data.
				 * @param WC_Order $order An order object.
				 */
				$anon_value = apply_filters( 'woocommerce_privacy_remove_order_personal_data_meta_value', $anon_value, $meta_key, $value, $data_type, $order );

				if ( $anon_value ) {
					$order->update_meta_data( $meta_key, $anon_value );
				} else {
					$order->delete_meta_data( $meta_key );
				}
			}
		}

		$order->update_meta_data( '_anonymized', 'yes' );
		$order->save();

		// Delete order notes which can contain PII.
		$notes = wc_get_order_notes(
			array(
				'order_id' => $order->get_id(),
			)
		);

		foreach ( $notes as $note ) {
			wc_delete_order_note( $note->id );
		}

		// Add note that this event occured.
		$order->add_order_note( __( 'Personal data removed.', 'woocommerce' ) );

		/**
		 * Allow extensions to remove their own personal data for this order.
		 *
		 * @since 3.4.0
		 * @param WC_Order $order A customer object.
		 */
		do_action( 'woocommerce_privacy_remove_order_personal_data', $order );
	}

	/**
	 * Finds and erases customer tokens by email address.
	 *
	 * @since 3.4.0
	 * @param string $email_address The user email address.
	 * @param int    $page  Page.
	 * @return array An array of personal data in name value pairs
	 */
	public static function customer_tokens_eraser( $email_address, $page ) {
		$response = array(
			'items_removed'  => false,
			'items_retained' => false,
			'messages'       => array(),
			'done'           => true,
		);

		$user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data.

		if ( ! $user instanceof WP_User ) {
			return $response;
		}

		$tokens = WC_Payment_Tokens::get_tokens(
			array(
				'user_id' => $user->ID,
			)
		);

		if ( empty( $tokens ) ) {
			return $response;
		}

		foreach ( $tokens as $token ) {
			WC_Payment_Tokens::delete( $token->get_id() );

			/* Translators: %s Prop name. */
			$response['messages'][]    = sprintf( __( 'Removed payment token "%d"', 'woocommerce' ), $token->get_id() );
			$response['items_removed'] = true;
		}

		/**
		 * Allow extensions to remove data for tokens and adjust the response.
		 *
		 * @since 3.4.0
		 * @param array $response Array resonse data. Must include messages, num_items_removed, num_items_retained, done.
		 * @param array $tokens   Array of tokens.
		 */
		return apply_filters( 'woocommerce_privacy_erase_personal_data_tokens', $response, $tokens );
	}
}
class-wc-product-attribute.php000064400000015575151336044760012476 0ustar00<?php
/**
 * Represents a product attribute
 *
 * Attributes can be global (taxonomy based) or local to the product itself.
 * Uses ArrayAccess to be BW compatible with previous ways of reading attributes.
 *
 * @package WooCommerce\Classes
 * @version 3.0.0
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Product attribute class.
 */
class WC_Product_Attribute implements ArrayAccess {

	/**
	 * Data array.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $data = array(
		'id'        => 0,
		'name'      => '',
		'options'   => array(),
		'position'  => 0,
		'visible'   => false,
		'variation' => false,
	);

	/**
	 * Return if this attribute is a taxonomy.
	 *
	 * @return boolean
	 */
	public function is_taxonomy() {
		return 0 < $this->get_id();
	}

	/**
	 * Get taxonomy name if applicable.
	 *
	 * @return string
	 */
	public function get_taxonomy() {
		return $this->is_taxonomy() ? $this->get_name() : '';
	}

	/**
	 * Get taxonomy object.
	 *
	 * @return array|null
	 */
	public function get_taxonomy_object() {
		global $wc_product_attributes;
		return $this->is_taxonomy() ? $wc_product_attributes[ $this->get_name() ] : null;
	}

	/**
	 * Gets terms from the stored options.
	 *
	 * @return array|null
	 */
	public function get_terms() {
		if ( ! $this->is_taxonomy() || ! taxonomy_exists( $this->get_name() ) ) {
			return null;
		}
		$terms = array();
		foreach ( $this->get_options() as $option ) {
			if ( is_int( $option ) ) {
				$term = get_term_by( 'id', $option, $this->get_name() );
			} else {
				// Term names get escaped in WP. See sanitize_term_field.
				$term = get_term_by( 'name', $option, $this->get_name() );

				if ( ! $term || is_wp_error( $term ) ) {
					$new_term = wp_insert_term( $option, $this->get_name() );
					$term     = is_wp_error( $new_term ) ? false : get_term_by( 'id', $new_term['term_id'], $this->get_name() );
				}
			}
			if ( $term && ! is_wp_error( $term ) ) {
				$terms[] = $term;
			}
		}
		return $terms;
	}

	/**
	 * Gets slugs from the stored options, or just the string if text based.
	 *
	 * @return array
	 */
	public function get_slugs() {
		if ( ! $this->is_taxonomy() || ! taxonomy_exists( $this->get_name() ) ) {
			return $this->get_options();
		}
		$terms = array();
		foreach ( $this->get_options() as $option ) {
			if ( is_int( $option ) ) {
				$term = get_term_by( 'id', $option, $this->get_name() );
			} else {
				$term = get_term_by( 'name', $option, $this->get_name() );

				if ( ! $term || is_wp_error( $term ) ) {
					$new_term = wp_insert_term( $option, $this->get_name() );
					$term     = is_wp_error( $new_term ) ? false : get_term_by( 'id', $new_term['term_id'], $this->get_name() );
				}
			}
			if ( $term && ! is_wp_error( $term ) ) {
				$terms[] = $term->slug;
			}
		}
		return $terms;
	}

	/**
	 * Returns all data for this object.
	 *
	 * @return array
	 */
	public function get_data() {
		return array_merge(
			$this->data,
			array(
				'is_visible'   => $this->get_visible() ? 1 : 0,
				'is_variation' => $this->get_variation() ? 1 : 0,
				'is_taxonomy'  => $this->is_taxonomy() ? 1 : 0,
				'value'        => $this->is_taxonomy() ? '' : wc_implode_text_attributes( $this->get_options() ),
			)
		);
	}

	/*
	|--------------------------------------------------------------------------
	| Setters
	|--------------------------------------------------------------------------
	*/

	/**
	 * Set ID (this is the attribute ID).
	 *
	 * @param int $value Attribute ID.
	 */
	public function set_id( $value ) {
		$this->data['id'] = absint( $value );
	}

	/**
	 * Set name (this is the attribute name or taxonomy).
	 *
	 * @param int $value Attribute name.
	 */
	public function set_name( $value ) {
		$this->data['name'] = $value;
	}

	/**
	 * Set options.
	 *
	 * @param array $value Attribute options.
	 */
	public function set_options( $value ) {
		$this->data['options'] = $value;
	}

	/**
	 * Set position.
	 *
	 * @param int $value Attribute position.
	 */
	public function set_position( $value ) {
		$this->data['position'] = absint( $value );
	}

	/**
	 * Set if visible.
	 *
	 * @param bool $value If is visible on Product's additional info tab.
	 */
	public function set_visible( $value ) {
		$this->data['visible'] = wc_string_to_bool( $value );
	}

	/**
	 * Set if variation.
	 *
	 * @param bool $value If is used for variations.
	 */
	public function set_variation( $value ) {
		$this->data['variation'] = wc_string_to_bool( $value );
	}

	/*
	|--------------------------------------------------------------------------
	| Getters
	|--------------------------------------------------------------------------
	*/

	/**
	 * Get the ID.
	 *
	 * @return int
	 */
	public function get_id() {
		return $this->data['id'];
	}

	/**
	 * Get name.
	 *
	 * @return string
	 */
	public function get_name() {
		return $this->data['name'];
	}

	/**
	 * Get options.
	 *
	 * @return array
	 */
	public function get_options() {
		return $this->data['options'];
	}

	/**
	 * Get position.
	 *
	 * @return int
	 */
	public function get_position() {
		return $this->data['position'];
	}

	/**
	 * Get if visible.
	 *
	 * @return bool
	 */
	public function get_visible() {
		return $this->data['visible'];
	}

	/**
	 * Get if variation.
	 *
	 * @return bool
	 */
	public function get_variation() {
		return $this->data['variation'];
	}

	/*
	|--------------------------------------------------------------------------
	| ArrayAccess/Backwards compatibility.
	|--------------------------------------------------------------------------
	*/

	/**
	 * OffsetGet.
	 *
	 * @param string $offset Offset.
	 * @return mixed
	 */
	public function offsetGet( $offset ) {
		switch ( $offset ) {
			case 'is_variation':
				return $this->get_variation() ? 1 : 0;
			case 'is_visible':
				return $this->get_visible() ? 1 : 0;
			case 'is_taxonomy':
				return $this->is_taxonomy() ? 1 : 0;
			case 'value':
				return $this->is_taxonomy() ? '' : wc_implode_text_attributes( $this->get_options() );
			default:
				if ( is_callable( array( $this, "get_$offset" ) ) ) {
					return $this->{"get_$offset"}();
				}
				break;
		}
		return '';
	}

	/**
	 * OffsetSet.
	 *
	 * @param string $offset Offset.
	 * @param mixed  $value  Value.
	 */
	public function offsetSet( $offset, $value ) {
		switch ( $offset ) {
			case 'is_variation':
				$this->set_variation( $value );
				break;
			case 'is_visible':
				$this->set_visible( $value );
				break;
			case 'value':
				$this->set_options( $value );
				break;
			default:
				if ( is_callable( array( $this, "set_$offset" ) ) ) {
					return $this->{"set_$offset"}( $value );
				}
				break;
		}
	}

	/**
	 * OffsetUnset.
	 *
	 * @param string $offset Offset.
	 */
	public function offsetUnset( $offset ) {}

	/**
	 * OffsetExists.
	 *
	 * @param string $offset Offset.
	 * @return bool
	 */
	public function offsetExists( $offset ) {
		return in_array( $offset, array_merge( array( 'is_variation', 'is_visible', 'is_taxonomy', 'value' ), array_keys( $this->data ) ), true );
	}
}
wc-attribute-functions.php000064400000051130151336044760011706 0ustar00<?php
/**
 * WooCommerce Attribute Functions
 *
 * @package WooCommerce\Functions
 * @version 2.1.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Gets text attributes from a string.
 *
 * @since  2.4
 * @param string $raw_attributes Raw attributes.
 * @return array
 */
function wc_get_text_attributes( $raw_attributes ) {
	return array_filter( array_map( 'trim', explode( WC_DELIMITER, html_entity_decode( $raw_attributes, ENT_QUOTES, get_bloginfo( 'charset' ) ) ) ), 'wc_get_text_attributes_filter_callback' );
}

/**
 * See if an attribute is actually valid.
 *
 * @since  3.0.0
 * @param  string $value Value.
 * @return bool
 */
function wc_get_text_attributes_filter_callback( $value ) {
	return '' !== $value;
}

/**
 * Implode an array of attributes using WC_DELIMITER.
 *
 * @since  3.0.0
 * @param  array $attributes Attributes list.
 * @return string
 */
function wc_implode_text_attributes( $attributes ) {
	return implode( ' ' . WC_DELIMITER . ' ', $attributes );
}

/**
 * Get attribute taxonomies.
 *
 * @return array of objects, @since 3.6.0 these are also indexed by ID.
 */
function wc_get_attribute_taxonomies() {
	$prefix      = WC_Cache_Helper::get_cache_prefix( 'woocommerce-attributes' );
	$cache_key   = $prefix . 'attributes';
	$cache_value = wp_cache_get( $cache_key, 'woocommerce-attributes' );

	if ( false !== $cache_value ) {
		return $cache_value;
	}

	$raw_attribute_taxonomies = get_transient( 'wc_attribute_taxonomies' );

	if ( false === $raw_attribute_taxonomies ) {
		global $wpdb;

		$raw_attribute_taxonomies = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_name != '' ORDER BY attribute_name ASC;" );

		set_transient( 'wc_attribute_taxonomies', $raw_attribute_taxonomies );
	}

	/**
	 * Filter attribute taxonomies.
	 *
	 * @param array $attribute_taxonomies Results of the DB query. Each taxonomy is an object.
	 */
	$raw_attribute_taxonomies = (array) array_filter( apply_filters( 'woocommerce_attribute_taxonomies', $raw_attribute_taxonomies ) );

	// Index by ID for easer lookups.
	$attribute_taxonomies = array();

	foreach ( $raw_attribute_taxonomies as $result ) {
		$attribute_taxonomies[ 'id:' . $result->attribute_id ] = $result;
	}

	wp_cache_set( $cache_key, $attribute_taxonomies, 'woocommerce-attributes' );

	return $attribute_taxonomies;
}

/**
 * Get (cached) attribute taxonomy ID and name pairs.
 *
 * @since 3.6.0
 * @return array
 */
function wc_get_attribute_taxonomy_ids() {
	$prefix      = WC_Cache_Helper::get_cache_prefix( 'woocommerce-attributes' );
	$cache_key   = $prefix . 'ids';
	$cache_value = wp_cache_get( $cache_key, 'woocommerce-attributes' );

	if ( $cache_value ) {
		return $cache_value;
	}

	$taxonomy_ids = array_map( 'absint', wp_list_pluck( wc_get_attribute_taxonomies(), 'attribute_id', 'attribute_name' ) );

	wp_cache_set( $cache_key, $taxonomy_ids, 'woocommerce-attributes' );

	return $taxonomy_ids;
}

/**
 * Get (cached) attribute taxonomy label and name pairs.
 *
 * @since 3.6.0
 * @return array
 */
function wc_get_attribute_taxonomy_labels() {
	$prefix      = WC_Cache_Helper::get_cache_prefix( 'woocommerce-attributes' );
	$cache_key   = $prefix . 'labels';
	$cache_value = wp_cache_get( $cache_key, 'woocommerce-attributes' );

	if ( $cache_value ) {
		return $cache_value;
	}

	$taxonomy_labels = wp_list_pluck( wc_get_attribute_taxonomies(), 'attribute_label', 'attribute_name' );

	wp_cache_set( $cache_key, $taxonomy_labels, 'woocommerce-attributes' );

	return $taxonomy_labels;
}

/**
 * Get a product attribute name.
 *
 * @param string $attribute_name Attribute name.
 * @return string
 */
function wc_attribute_taxonomy_name( $attribute_name ) {
	return $attribute_name ? 'pa_' . wc_sanitize_taxonomy_name( $attribute_name ) : '';
}

/**
 * Get the attribute name used when storing values in post meta.
 *
 * @since 2.6.0
 * @param string $attribute_name Attribute name.
 * @return string
 */
function wc_variation_attribute_name( $attribute_name ) {
	return 'attribute_' . sanitize_title( $attribute_name );
}

/**
 * Get a product attribute name by ID.
 *
 * @since  2.4.0
 * @param int $attribute_id Attribute ID.
 * @return string Return an empty string if attribute doesn't exist.
 */
function wc_attribute_taxonomy_name_by_id( $attribute_id ) {
	$taxonomy_ids   = wc_get_attribute_taxonomy_ids();
	$attribute_name = (string) array_search( $attribute_id, $taxonomy_ids, true );
	return wc_attribute_taxonomy_name( $attribute_name );
}

/**
 * Get a product attribute ID by name.
 *
 * @since  2.6.0
 * @param string $name Attribute name.
 * @return int
 */
function wc_attribute_taxonomy_id_by_name( $name ) {
	$name         = wc_attribute_taxonomy_slug( $name );
	$taxonomy_ids = wc_get_attribute_taxonomy_ids();

	return isset( $taxonomy_ids[ $name ] ) ? $taxonomy_ids[ $name ] : 0;
}

/**
 * Get a product attributes label.
 *
 * @param string     $name    Attribute name.
 * @param WC_Product $product Product data.
 * @return string
 */
function wc_attribute_label( $name, $product = '' ) {
	if ( taxonomy_is_product_attribute( $name ) ) {
		$slug       = wc_attribute_taxonomy_slug( $name );
		$all_labels = wc_get_attribute_taxonomy_labels();
		$label      = isset( $all_labels[ $slug ] ) ? $all_labels[ $slug ] : $slug;
	} elseif ( $product ) {
		if ( $product->is_type( 'variation' ) ) {
			$product = wc_get_product( $product->get_parent_id() );
		}
		$attributes = array();

		if ( false !== $product ) {
			$attributes = $product->get_attributes();
		}

		// Attempt to get label from product, as entered by the user.
		if ( $attributes && isset( $attributes[ sanitize_title( $name ) ] ) ) {
			$label = $attributes[ sanitize_title( $name ) ]->get_name();
		} else {
			$label = $name;
		}
	} else {
		$label = $name;
	}

	return apply_filters( 'woocommerce_attribute_label', $label, $name, $product );
}

/**
 * Get a product attributes orderby setting.
 *
 * @param string $name Attribute name.
 * @return string
 */
function wc_attribute_orderby( $name ) {
	$name       = wc_attribute_taxonomy_slug( $name );
	$id         = wc_attribute_taxonomy_id_by_name( $name );
	$taxonomies = wc_get_attribute_taxonomies();

	return apply_filters( 'woocommerce_attribute_orderby', isset( $taxonomies[ 'id:' . $id ] ) ? $taxonomies[ 'id:' . $id ]->attribute_orderby : 'menu_order', $name );
}

/**
 * Get an array of product attribute taxonomies.
 *
 * @return array
 */
function wc_get_attribute_taxonomy_names() {
	$taxonomy_names       = array();
	$attribute_taxonomies = wc_get_attribute_taxonomies();
	if ( ! empty( $attribute_taxonomies ) ) {
		foreach ( $attribute_taxonomies as $tax ) {
			$taxonomy_names[] = wc_attribute_taxonomy_name( $tax->attribute_name );
		}
	}
	return $taxonomy_names;
}

/**
 * Get attribute types.
 *
 * @since  2.4.0
 * @return array
 */
function wc_get_attribute_types() {
	return (array) apply_filters(
		'product_attributes_type_selector',
		array(
			'select' => __( 'Select', 'woocommerce' ),
		)
	);
}

/**
 * Check if there are custom attribute types.
 *
 * @since  3.3.2
 * @return bool True if there are custom types, otherwise false.
 */
function wc_has_custom_attribute_types() {
	$types = wc_get_attribute_types();

	return 1 < count( $types ) || ! array_key_exists( 'select', $types );
}

/**
 * Get attribute type label.
 *
 * @since  3.0.0
 * @param  string $type Attribute type slug.
 * @return string
 */
function wc_get_attribute_type_label( $type ) {
	$types = wc_get_attribute_types();

	return isset( $types[ $type ] ) ? $types[ $type ] : __( 'Select', 'woocommerce' );
}

/**
 * Check if attribute name is reserved.
 * https://codex.wordpress.org/Function_Reference/register_taxonomy#Reserved_Terms.
 *
 * @since  2.4.0
 * @param  string $attribute_name Attribute name.
 * @return bool
 */
function wc_check_if_attribute_name_is_reserved( $attribute_name ) {
	// Forbidden attribute names.
	$reserved_terms = array(
		'attachment',
		'attachment_id',
		'author',
		'author_name',
		'calendar',
		'cat',
		'category',
		'category__and',
		'category__in',
		'category__not_in',
		'category_name',
		'comments_per_page',
		'comments_popup',
		'cpage',
		'day',
		'debug',
		'error',
		'exact',
		'feed',
		'hour',
		'link_category',
		'm',
		'minute',
		'monthnum',
		'more',
		'name',
		'nav_menu',
		'nopaging',
		'offset',
		'order',
		'orderby',
		'p',
		'page',
		'page_id',
		'paged',
		'pagename',
		'pb',
		'perm',
		'post',
		'post__in',
		'post__not_in',
		'post_format',
		'post_mime_type',
		'post_status',
		'post_tag',
		'post_type',
		'posts',
		'posts_per_archive_page',
		'posts_per_page',
		'preview',
		'robots',
		's',
		'search',
		'second',
		'sentence',
		'showposts',
		'static',
		'subpost',
		'subpost_id',
		'tag',
		'tag__and',
		'tag__in',
		'tag__not_in',
		'tag_id',
		'tag_slug__and',
		'tag_slug__in',
		'taxonomy',
		'tb',
		'term',
		'type',
		'w',
		'withcomments',
		'withoutcomments',
		'year',
	);

	return in_array( $attribute_name, $reserved_terms, true );
}

/**
 * Callback for array filter to get visible only.
 *
 * @since  3.0.0
 * @param  WC_Product_Attribute $attribute Attribute data.
 * @return bool
 */
function wc_attributes_array_filter_visible( $attribute ) {
	return $attribute && is_a( $attribute, 'WC_Product_Attribute' ) && $attribute->get_visible() && ( ! $attribute->is_taxonomy() || taxonomy_exists( $attribute->get_name() ) );
}

/**
 * Callback for array filter to get variation attributes only.
 *
 * @since  3.0.0
 * @param  WC_Product_Attribute $attribute Attribute data.
 * @return bool
 */
function wc_attributes_array_filter_variation( $attribute ) {
	return $attribute && is_a( $attribute, 'WC_Product_Attribute' ) && $attribute->get_variation();
}

/**
 * Check if an attribute is included in the attributes area of a variation name.
 *
 * @since  3.0.2
 * @param  string $attribute Attribute value to check for.
 * @param  string $name      Product name to check in.
 * @return bool
 */
function wc_is_attribute_in_product_name( $attribute, $name ) {
	$is_in_name = stristr( $name, ' ' . $attribute . ',' ) || 0 === stripos( strrev( $name ), strrev( ' ' . $attribute ) );
	return apply_filters( 'woocommerce_is_attribute_in_product_name', $is_in_name, $attribute, $name );
}

/**
 * Callback for array filter to get default attributes.  Will allow for '0' string values, but regard all other
 * class PHP FALSE equivalents normally.
 *
 * @since 3.1.0
 * @param mixed $attribute Attribute being considered for exclusion from parent array.
 * @return bool
 */
function wc_array_filter_default_attributes( $attribute ) {
	return is_scalar( $attribute ) && ( ! empty( $attribute ) || '0' === $attribute );
}

/**
 * Get attribute data by ID.
 *
 * @since  3.2.0
 * @param  int $id Attribute ID.
 * @return stdClass|null
 */
function wc_get_attribute( $id ) {
	$attributes = wc_get_attribute_taxonomies();

	if ( ! isset( $attributes[ 'id:' . $id ] ) ) {
		return null;
	}

	$data                    = $attributes[ 'id:' . $id ];
	$attribute               = new stdClass();
	$attribute->id           = (int) $data->attribute_id;
	$attribute->name         = $data->attribute_label;
	$attribute->slug         = wc_attribute_taxonomy_name( $data->attribute_name );
	$attribute->type         = $data->attribute_type;
	$attribute->order_by     = $data->attribute_orderby;
	$attribute->has_archives = (bool) $data->attribute_public;
	return $attribute;
}

/**
 * Create attribute.
 *
 * @since  3.2.0
 * @param  array $args Attribute arguments {
 *     Array of attribute parameters.
 *
 *     @type int    $id           Unique identifier, used to update an attribute.
 *     @type string $name         Attribute name. Always required.
 *     @type string $slug         Attribute alphanumeric identifier.
 *     @type string $type         Type of attribute.
 *                                Core by default accepts: 'select' and 'text'.
 *                                Default to 'select'.
 *     @type string $order_by     Sort order.
 *                                Accepts: 'menu_order', 'name', 'name_num' and 'id'.
 *                                Default to 'menu_order'.
 *     @type bool   $has_archives Enable or disable attribute archives. False by default.
 * }
 * @return int|WP_Error
 */
function wc_create_attribute( $args ) {
	global $wpdb;

	$args   = wp_unslash( $args );
	$id     = ! empty( $args['id'] ) ? intval( $args['id'] ) : 0;
	$format = array( '%s', '%s', '%s', '%s', '%d' );

	// Name is required.
	if ( empty( $args['name'] ) ) {
		return new WP_Error( 'missing_attribute_name', __( 'Please, provide an attribute name.', 'woocommerce' ), array( 'status' => 400 ) );
	}

	// Set the attribute slug.
	if ( empty( $args['slug'] ) ) {
		$slug = wc_sanitize_taxonomy_name( $args['name'] );
	} else {
		$slug = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( $args['slug'] ) );
	}

	// Validate slug.
	if ( strlen( $slug ) >= 28 ) {
		/* translators: %s: attribute slug */
		return new WP_Error( 'invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) );
	} elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) {
		/* translators: %s: attribute slug */
		return new WP_Error( 'invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) );
	} elseif ( ( 0 === $id && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) || ( isset( $args['old_slug'] ) && $args['old_slug'] !== $slug && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) ) {
		/* translators: %s: attribute slug */
		return new WP_Error( 'invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) );
	}

	// Validate type.
	if ( empty( $args['type'] ) || ! array_key_exists( $args['type'], wc_get_attribute_types() ) ) {
		$args['type'] = 'select';
	}

	// Validate order by.
	if ( empty( $args['order_by'] ) || ! in_array( $args['order_by'], array( 'menu_order', 'name', 'name_num', 'id' ), true ) ) {
		$args['order_by'] = 'menu_order';
	}

	$data = array(
		'attribute_label'   => $args['name'],
		'attribute_name'    => $slug,
		'attribute_type'    => $args['type'],
		'attribute_orderby' => $args['order_by'],
		'attribute_public'  => isset( $args['has_archives'] ) ? (int) $args['has_archives'] : 0,
	);

	// Create or update.
	if ( 0 === $id ) {
		$results = $wpdb->insert(
			$wpdb->prefix . 'woocommerce_attribute_taxonomies',
			$data,
			$format
		);

		if ( is_wp_error( $results ) ) {
			return new WP_Error( 'cannot_create_attribute', $results->get_error_message(), array( 'status' => 400 ) );
		}

		$id = $wpdb->insert_id;

		/**
		 * Attribute added.
		 *
		 * @param int   $id   Added attribute ID.
		 * @param array $data Attribute data.
		 */
		do_action( 'woocommerce_attribute_added', $id, $data );
	} else {
		$results = $wpdb->update(
			$wpdb->prefix . 'woocommerce_attribute_taxonomies',
			$data,
			array( 'attribute_id' => $id ),
			$format,
			array( '%d' )
		);

		if ( false === $results ) {
			return new WP_Error( 'cannot_update_attribute', __( 'Could not update the attribute.', 'woocommerce' ), array( 'status' => 400 ) );
		}

		// Set old slug to check for database changes.
		$old_slug = ! empty( $args['old_slug'] ) ? wc_sanitize_taxonomy_name( $args['old_slug'] ) : $slug;

		/**
		 * Attribute updated.
		 *
		 * @param int    $id       Added attribute ID.
		 * @param array  $data     Attribute data.
		 * @param string $old_slug Attribute old name.
		 */
		do_action( 'woocommerce_attribute_updated', $id, $data, $old_slug );

		if ( $old_slug !== $slug ) {
			// Update taxonomies in the wp term taxonomy table.
			$wpdb->update(
				$wpdb->term_taxonomy,
				array( 'taxonomy' => wc_attribute_taxonomy_name( $data['attribute_name'] ) ),
				array( 'taxonomy' => 'pa_' . $old_slug )
			);

			// Update taxonomy ordering term meta.
			$wpdb->update(
				$wpdb->termmeta,
				array( 'meta_key' => 'order_pa_' . sanitize_title( $data['attribute_name'] ) ), // WPCS: slow query ok.
				array( 'meta_key' => 'order_pa_' . sanitize_title( $old_slug ) ) // WPCS: slow query ok.
			);

			// Update product attributes which use this taxonomy.
			$old_taxonomy_name = 'pa_' . $old_slug;
			$new_taxonomy_name = 'pa_' . $data['attribute_name'];
			$old_attribute_key = sanitize_title( $old_taxonomy_name ); // @see WC_Product::set_attributes().
			$new_attribute_key = sanitize_title( $new_taxonomy_name ); // @see WC_Product::set_attributes().
			$metadatas         = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_product_attributes' AND meta_value LIKE %s",
					'%' . $wpdb->esc_like( $old_taxonomy_name ) . '%'
				),
				ARRAY_A
			);
			foreach ( $metadatas as $metadata ) {
				$product_id        = $metadata['post_id'];
				$unserialized_data = maybe_unserialize( $metadata['meta_value'] );

				if ( ! $unserialized_data || ! is_array( $unserialized_data ) || ! isset( $unserialized_data[ $old_attribute_key ] ) ) {
					continue;
				}

				$unserialized_data[ $new_attribute_key ] = $unserialized_data[ $old_attribute_key ];
				unset( $unserialized_data[ $old_attribute_key ] );
				$unserialized_data[ $new_attribute_key ]['name'] = $new_taxonomy_name;
				update_post_meta( $product_id, '_product_attributes', wp_slash( $unserialized_data ) );
			}

			// Update variations which use this taxonomy.
			$wpdb->update(
				$wpdb->postmeta,
				array( 'meta_key' => 'attribute_pa_' . sanitize_title( $data['attribute_name'] ) ), // WPCS: slow query ok.
				array( 'meta_key' => 'attribute_pa_' . sanitize_title( $old_slug ) ) // WPCS: slow query ok.
			);
		}
	}

	// Clear cache and flush rewrite rules.
	wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' );
	delete_transient( 'wc_attribute_taxonomies' );
	WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' );

	return $id;
}

/**
 * Update an attribute.
 *
 * For available args see wc_create_attribute().
 *
 * @since  3.2.0
 * @param  int   $id   Attribute ID.
 * @param  array $args Attribute arguments.
 * @return int|WP_Error
 */
function wc_update_attribute( $id, $args ) {
	global $wpdb;

	$attribute = wc_get_attribute( $id );

	$args['id'] = $attribute ? $attribute->id : 0;

	if ( $args['id'] && empty( $args['name'] ) ) {
		$args['name'] = $attribute->name;
	}

	$args['old_slug'] = $wpdb->get_var(
		$wpdb->prepare(
			"
				SELECT attribute_name
				FROM {$wpdb->prefix}woocommerce_attribute_taxonomies
				WHERE attribute_id = %d
			",
			$args['id']
		)
	);

	return wc_create_attribute( $args );
}

/**
 * Delete attribute by ID.
 *
 * @since  3.2.0
 * @param  int $id Attribute ID.
 * @return bool
 */
function wc_delete_attribute( $id ) {
	global $wpdb;

	$name = $wpdb->get_var(
		$wpdb->prepare(
			"
			SELECT attribute_name
			FROM {$wpdb->prefix}woocommerce_attribute_taxonomies
			WHERE attribute_id = %d
			",
			$id
		)
	);

	$taxonomy = wc_attribute_taxonomy_name( $name );

	/**
	 * Before deleting an attribute.
	 *
	 * @param int    $id       Attribute ID.
	 * @param string $name     Attribute name.
	 * @param string $taxonomy Attribute taxonomy name.
	 */
	do_action( 'woocommerce_before_attribute_delete', $id, $name, $taxonomy );

	if ( $name && $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_id = %d", $id ) ) ) {
		if ( taxonomy_exists( $taxonomy ) ) {
			$terms = get_terms( $taxonomy, 'orderby=name&hide_empty=0' );
			foreach ( $terms as $term ) {
				wp_delete_term( $term->term_id, $taxonomy );
			}
		}

		/**
		 * After deleting an attribute.
		 *
		 * @param int    $id       Attribute ID.
		 * @param string $name     Attribute name.
		 * @param string $taxonomy Attribute taxonomy name.
		 */
		do_action( 'woocommerce_attribute_deleted', $id, $name, $taxonomy );
		wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' );
		delete_transient( 'wc_attribute_taxonomies' );
		WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' );

		return true;
	}

	return false;
}

/**
 * Get an unprefixed product attribute name.
 *
 * @since 3.6.0
 *
 * @param  string $attribute_name Attribute name.
 * @return string
 */
function wc_attribute_taxonomy_slug( $attribute_name ) {
	$prefix      = WC_Cache_Helper::get_cache_prefix( 'woocommerce-attributes' );
	$cache_key   = $prefix . 'slug-' . $attribute_name;
	$cache_value = wp_cache_get( $cache_key, 'woocommerce-attributes' );

	if ( $cache_value ) {
		return $cache_value;
	}

	$attribute_name = wc_sanitize_taxonomy_name( $attribute_name );
	$attribute_slug = 0 === strpos( $attribute_name, 'pa_' ) ? substr( $attribute_name, 3 ) : $attribute_name;
	wp_cache_set( $cache_key, $attribute_slug, 'woocommerce-attributes' );

	return $attribute_slug;
}
wc-template-functions.php000064400000345431151336044760011530 0ustar00<?php
/**
 * WooCommerce Template
 *
 * Functions for the templating system.
 *
 * @package  WooCommerce\Functions
 * @version  2.5.0
 */

use Automattic\Jetpack\Constants;

defined( 'ABSPATH' ) || exit;

/**
 * Handle redirects before content is output - hooked into template_redirect so is_page works.
 */
function wc_template_redirect() {
	global $wp_query, $wp;

	// phpcs:disable WordPress.Security.NonceVerification.Recommended
	// When default permalinks are enabled, redirect shop page to post type archive url.
	if ( ! empty( $_GET['page_id'] ) && '' === get_option( 'permalink_structure' ) && wc_get_page_id( 'shop' ) === absint( $_GET['page_id'] ) && get_post_type_archive_link( 'product' ) ) {
		wp_safe_redirect( get_post_type_archive_link( 'product' ) );
		exit;
	}
	// phpcs:enable WordPress.Security.NonceVerification.Recommended

	// When on the checkout with an empty cart, redirect to cart page.
	if ( is_page( wc_get_page_id( 'checkout' ) ) && wc_get_page_id( 'checkout' ) !== wc_get_page_id( 'cart' ) && WC()->cart->is_empty() && empty( $wp->query_vars['order-pay'] ) && ! isset( $wp->query_vars['order-received'] ) && ! is_customize_preview() && apply_filters( 'woocommerce_checkout_redirect_empty_cart', true ) ) {
		wc_add_notice( __( 'Checkout is not available whilst your cart is empty.', 'woocommerce' ), 'notice' );
		wp_safe_redirect( wc_get_cart_url() );
		exit;

	}

	// Logout.
	if ( isset( $wp->query_vars['customer-logout'] ) && ! empty( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'customer-logout' ) ) {
		wp_safe_redirect( str_replace( '&amp;', '&', wp_logout_url( apply_filters( 'woocommerce_logout_default_redirect_url', wc_get_page_permalink( 'myaccount' ) ) ) ) );
		exit;
	}

	// Redirect to the correct logout endpoint.
	if ( isset( $wp->query_vars['customer-logout'] ) && 'true' === $wp->query_vars['customer-logout'] ) {
		wp_safe_redirect( esc_url_raw( wc_get_account_endpoint_url( 'customer-logout' ) ) );
		exit;
	}

	// Trigger 404 if trying to access an endpoint on wrong page.
	if ( is_wc_endpoint_url() && ! is_account_page() && ! is_checkout() && apply_filters( 'woocommerce_account_endpoint_page_not_found', true ) ) {
		$wp_query->set_404();
		status_header( 404 );
		include get_query_template( '404' );
		exit;
	}

	// Redirect to the product page if we have a single product.
	if ( is_search() && is_post_type_archive( 'product' ) && apply_filters( 'woocommerce_redirect_single_search_result', true ) && 1 === absint( $wp_query->found_posts ) ) {
		$product = wc_get_product( $wp_query->post );

		if ( $product && $product->is_visible() ) {
			wp_safe_redirect( get_permalink( $product->get_id() ), 302 );
			exit;
		}
	}

	// Ensure gateways and shipping methods are loaded early.
	if ( is_add_payment_method_page() || is_checkout() ) {
		// Buffer the checkout page.
		ob_start();

		// Ensure gateways and shipping methods are loaded early.
		WC()->payment_gateways();
		WC()->shipping();
	}
}
add_action( 'template_redirect', 'wc_template_redirect' );

/**
 * When loading sensitive checkout or account pages, send a HTTP header to limit rendering of pages to same origin iframes for security reasons.
 *
 * Can be disabled with: remove_action( 'template_redirect', 'wc_send_frame_options_header' );
 *
 * @since  2.3.10
 */
function wc_send_frame_options_header() {

	if ( ( is_checkout() || is_account_page() ) && ! is_customize_preview() ) {
		send_frame_options_header();
	}
}
add_action( 'template_redirect', 'wc_send_frame_options_header' );

/**
 * No index our endpoints.
 * Prevent indexing pages like order-received.
 *
 * @since 2.5.3
 */
function wc_prevent_endpoint_indexing() {
	// phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.PHP.NoSilencedErrors.Discouraged
	if ( is_wc_endpoint_url() || isset( $_GET['download_file'] ) ) {
		@header( 'X-Robots-Tag: noindex' );
	}
	// phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.PHP.NoSilencedErrors.Discouraged
}
add_action( 'template_redirect', 'wc_prevent_endpoint_indexing' );

/**
 * Remove adjacent_posts_rel_link_wp_head - pointless for products.
 *
 * @since 3.0.0
 */
function wc_prevent_adjacent_posts_rel_link_wp_head() {
	if ( is_singular( 'product' ) ) {
		remove_action( 'wp_head', 'adjacent_posts_rel_link_wp_head', 10, 0 );
	}
}
add_action( 'template_redirect', 'wc_prevent_adjacent_posts_rel_link_wp_head' );

/**
 * Show the gallery if JS is disabled.
 *
 * @since 3.0.6
 */
function wc_gallery_noscript() {
	?>
	<noscript><style>.woocommerce-product-gallery{ opacity: 1 !important; }</style></noscript>
	<?php
}
add_action( 'wp_head', 'wc_gallery_noscript' );

/**
 * When the_post is called, put product data into a global.
 *
 * @param mixed $post Post Object.
 * @return WC_Product
 */
function wc_setup_product_data( $post ) {
	unset( $GLOBALS['product'] );

	if ( is_int( $post ) ) {
		$post = get_post( $post );
	}

	if ( empty( $post->post_type ) || ! in_array( $post->post_type, array( 'product', 'product_variation' ), true ) ) {
		return;
	}

	$GLOBALS['product'] = wc_get_product( $post );

	return $GLOBALS['product'];
}
add_action( 'the_post', 'wc_setup_product_data' );

/**
 * Sets up the woocommerce_loop global from the passed args or from the main query.
 *
 * @since 3.3.0
 * @param array $args Args to pass into the global.
 */
function wc_setup_loop( $args = array() ) {
	$default_args = array(
		'loop'         => 0,
		'columns'      => wc_get_default_products_per_row(),
		'name'         => '',
		'is_shortcode' => false,
		'is_paginated' => true,
		'is_search'    => false,
		'is_filtered'  => false,
		'total'        => 0,
		'total_pages'  => 0,
		'per_page'     => 0,
		'current_page' => 1,
	);

	// If this is a main WC query, use global args as defaults.
	if ( $GLOBALS['wp_query']->get( 'wc_query' ) ) {
		$default_args = array_merge(
			$default_args,
			array(
				'is_search'    => $GLOBALS['wp_query']->is_search(),
				'is_filtered'  => is_filtered(),
				'total'        => $GLOBALS['wp_query']->found_posts,
				'total_pages'  => $GLOBALS['wp_query']->max_num_pages,
				'per_page'     => $GLOBALS['wp_query']->get( 'posts_per_page' ),
				'current_page' => max( 1, $GLOBALS['wp_query']->get( 'paged', 1 ) ),
			)
		);
	}

	// Merge any existing values.
	if ( isset( $GLOBALS['woocommerce_loop'] ) ) {
		$default_args = array_merge( $default_args, $GLOBALS['woocommerce_loop'] );
	}

	$GLOBALS['woocommerce_loop'] = wp_parse_args( $args, $default_args );
}
add_action( 'woocommerce_before_shop_loop', 'wc_setup_loop' );

/**
 * Resets the woocommerce_loop global.
 *
 * @since 3.3.0
 */
function wc_reset_loop() {
	unset( $GLOBALS['woocommerce_loop'] );
}
add_action( 'woocommerce_after_shop_loop', 'woocommerce_reset_loop', 999 );

/**
 * Gets a property from the woocommerce_loop global.
 *
 * @since 3.3.0
 * @param string $prop Prop to get.
 * @param string $default Default if the prop does not exist.
 * @return mixed
 */
function wc_get_loop_prop( $prop, $default = '' ) {
	wc_setup_loop(); // Ensure shop loop is setup.

	return isset( $GLOBALS['woocommerce_loop'], $GLOBALS['woocommerce_loop'][ $prop ] ) ? $GLOBALS['woocommerce_loop'][ $prop ] : $default;
}

/**
 * Sets a property in the woocommerce_loop global.
 *
 * @since 3.3.0
 * @param string $prop Prop to set.
 * @param string $value Value to set.
 */
function wc_set_loop_prop( $prop, $value = '' ) {
	if ( ! isset( $GLOBALS['woocommerce_loop'] ) ) {
		wc_setup_loop();
	}
	$GLOBALS['woocommerce_loop'][ $prop ] = $value;
}

/**
 * Set the current visbility for a product in the woocommerce_loop global.
 *
 * @since 4.4.0
 * @param int  $product_id Product it to cache visbiility for.
 * @param bool $value The poduct visibility value to cache.
 */
function wc_set_loop_product_visibility( $product_id, $value ) {
	wc_set_loop_prop( "product_visibility_$product_id", $value );
}

/**
 * Gets the cached current visibility for a product from the woocommerce_loop global.
 *
 * @since 4.4.0
 * @param int $product_id Product id to get the cached visibility for.
 *
 * @return bool|null The cached product visibility, or null if on visibility has been cached for that product.
 */
function wc_get_loop_product_visibility( $product_id ) {
	return wc_get_loop_prop( "product_visibility_$product_id", null );
}

/**
 * Should the WooCommerce loop be displayed?
 *
 * This will return true if we have posts (products) or if we have subcats to display.
 *
 * @since 3.4.0
 * @return bool
 */
function woocommerce_product_loop() {
	return have_posts() || 'products' !== woocommerce_get_loop_display_mode();
}

/**
 * Output generator tag to aid debugging.
 *
 * @param string $gen Generator.
 * @param string $type Type.
 * @return string
 */
function wc_generator_tag( $gen, $type ) {
	$version = Constants::get_constant( 'WC_VERSION' );

	switch ( $type ) {
		case 'html':
			$gen .= "\n" . '<meta name="generator" content="WooCommerce ' . esc_attr( $version ) . '">';
			break;
		case 'xhtml':
			$gen .= "\n" . '<meta name="generator" content="WooCommerce ' . esc_attr( $version ) . '" />';
			break;
	}
	return $gen;
}

/**
 * Add body classes for WC pages.
 *
 * @param  array $classes Body Classes.
 * @return array
 */
function wc_body_class( $classes ) {
	$classes = (array) $classes;

	if ( is_shop() ) {

		$classes[] = 'woocommerce-shop';

	}

	if ( is_woocommerce() ) {

		$classes[] = 'woocommerce';
		$classes[] = 'woocommerce-page';

	} elseif ( is_checkout() ) {

		$classes[] = 'woocommerce-checkout';
		$classes[] = 'woocommerce-page';

	} elseif ( is_cart() ) {

		$classes[] = 'woocommerce-cart';
		$classes[] = 'woocommerce-page';

	} elseif ( is_account_page() ) {

		$classes[] = 'woocommerce-account';
		$classes[] = 'woocommerce-page';

	}

	if ( is_store_notice_showing() ) {
		$classes[] = 'woocommerce-demo-store';
	}

	foreach ( WC()->query->get_query_vars() as $key => $value ) {
		if ( is_wc_endpoint_url( $key ) ) {
			$classes[] = 'woocommerce-' . sanitize_html_class( $key );
		}
	}

	$classes[] = 'woocommerce-no-js';

	add_action( 'wp_footer', 'wc_no_js' );

	return array_unique( $classes );
}

/**
 * NO JS handling.
 *
 * @since 3.4.0
 */
function wc_no_js() {
	?>
	<script type="text/javascript">
		(function () {
			var c = document.body.className;
			c = c.replace(/woocommerce-no-js/, 'woocommerce-js');
			document.body.className = c;
		})();
	</script>
	<?php
}

/**
 * Display the classes for the product cat div.
 *
 * @since 2.4.0
 * @param string|array $class One or more classes to add to the class list.
 * @param object       $category object Optional.
 */
function wc_product_cat_class( $class = '', $category = null ) {
	// Separates classes with a single space, collates classes for post DIV.
	echo 'class="' . esc_attr( join( ' ', wc_get_product_cat_class( $class, $category ) ) ) . '"';
}

/**
 * Get the default columns setting - this is how many products will be shown per row in loops.
 *
 * @since 3.3.0
 * @return int
 */
function wc_get_default_products_per_row() {
	$columns      = get_option( 'woocommerce_catalog_columns', 4 );
	$product_grid = wc_get_theme_support( 'product_grid' );
	$min_columns  = isset( $product_grid['min_columns'] ) ? absint( $product_grid['min_columns'] ) : 0;
	$max_columns  = isset( $product_grid['max_columns'] ) ? absint( $product_grid['max_columns'] ) : 0;

	if ( $min_columns && $columns < $min_columns ) {
		$columns = $min_columns;
		update_option( 'woocommerce_catalog_columns', $columns );
	} elseif ( $max_columns && $columns > $max_columns ) {
		$columns = $max_columns;
		update_option( 'woocommerce_catalog_columns', $columns );
	}

	if ( has_filter( 'loop_shop_columns' ) ) { // Legacy filter handling.
		$columns = apply_filters( 'loop_shop_columns', $columns );
	}

	$columns = absint( $columns );

	return max( 1, $columns );
}

/**
 * Get the default rows setting - this is how many product rows will be shown in loops.
 *
 * @since 3.3.0
 * @return int
 */
function wc_get_default_product_rows_per_page() {
	$rows         = absint( get_option( 'woocommerce_catalog_rows', 4 ) );
	$product_grid = wc_get_theme_support( 'product_grid' );
	$min_rows     = isset( $product_grid['min_rows'] ) ? absint( $product_grid['min_rows'] ) : 0;
	$max_rows     = isset( $product_grid['max_rows'] ) ? absint( $product_grid['max_rows'] ) : 0;

	if ( $min_rows && $rows < $min_rows ) {
		$rows = $min_rows;
		update_option( 'woocommerce_catalog_rows', $rows );
	} elseif ( $max_rows && $rows > $max_rows ) {
		$rows = $max_rows;
		update_option( 'woocommerce_catalog_rows', $rows );
	}

	return $rows;
}

/**
 * Reset the product grid settings when a new theme is activated.
 *
 * @since 3.3.0
 */
function wc_reset_product_grid_settings() {
	$product_grid = wc_get_theme_support( 'product_grid' );

	if ( ! empty( $product_grid['default_rows'] ) ) {
		update_option( 'woocommerce_catalog_rows', absint( $product_grid['default_rows'] ) );
	}

	if ( ! empty( $product_grid['default_columns'] ) ) {
		update_option( 'woocommerce_catalog_columns', absint( $product_grid['default_columns'] ) );
	}

	wp_cache_flush(); // Flush any caches which could impact settings or templates.
}
add_action( 'after_switch_theme', 'wc_reset_product_grid_settings' );

/**
 * Get classname for woocommerce loops.
 *
 * @since 2.6.0
 * @return string
 */
function wc_get_loop_class() {
	$loop_index = wc_get_loop_prop( 'loop', 0 );
	$columns    = absint( max( 1, wc_get_loop_prop( 'columns', wc_get_default_products_per_row() ) ) );

	$loop_index ++;
	wc_set_loop_prop( 'loop', $loop_index );

	if ( 0 === ( $loop_index - 1 ) % $columns || 1 === $columns ) {
		return 'first';
	}

	if ( 0 === $loop_index % $columns ) {
		return 'last';
	}

	return '';
}


/**
 * Get the classes for the product cat div.
 *
 * @since 2.4.0
 *
 * @param string|array $class One or more classes to add to the class list.
 * @param object       $category object Optional.
 *
 * @return array
 */
function wc_get_product_cat_class( $class = '', $category = null ) {
	$classes   = is_array( $class ) ? $class : array_map( 'trim', explode( ' ', $class ) );
	$classes[] = 'product-category';
	$classes[] = 'product';
	$classes[] = wc_get_loop_class();
	$classes   = apply_filters( 'product_cat_class', $classes, $class, $category );

	return array_unique( array_filter( $classes ) );
}

/**
 * Adds extra post classes for products via the WordPress post_class hook, if used.
 *
 * Note: For performance reasons we instead recommend using wc_product_class/wc_get_product_class instead.
 *
 * @since 2.1.0
 * @param array        $classes Current classes.
 * @param string|array $class Additional class.
 * @param int          $post_id Post ID.
 * @return array
 */
function wc_product_post_class( $classes, $class = '', $post_id = 0 ) {
	if ( ! $post_id || ! in_array( get_post_type( $post_id ), array( 'product', 'product_variation' ), true ) ) {
		return $classes;
	}

	$product = wc_get_product( $post_id );

	if ( ! $product ) {
		return $classes;
	}

	$classes[] = 'product';
	$classes[] = wc_get_loop_class();
	$classes[] = $product->get_stock_status();

	if ( $product->is_on_sale() ) {
		$classes[] = 'sale';
	}
	if ( $product->is_featured() ) {
		$classes[] = 'featured';
	}
	if ( $product->is_downloadable() ) {
		$classes[] = 'downloadable';
	}
	if ( $product->is_virtual() ) {
		$classes[] = 'virtual';
	}
	if ( $product->is_sold_individually() ) {
		$classes[] = 'sold-individually';
	}
	if ( $product->is_taxable() ) {
		$classes[] = 'taxable';
	}
	if ( $product->is_shipping_taxable() ) {
		$classes[] = 'shipping-taxable';
	}
	if ( $product->is_purchasable() ) {
		$classes[] = 'purchasable';
	}
	if ( $product->get_type() ) {
		$classes[] = 'product-type-' . $product->get_type();
	}
	if ( $product->is_type( 'variable' ) && $product->get_default_attributes() ) {
		$classes[] = 'has-default-attributes';
	}

	$key = array_search( 'hentry', $classes, true );
	if ( false !== $key ) {
		unset( $classes[ $key ] );
	}

	return $classes;
}

/**
 * Get product taxonomy HTML classes.
 *
 * @since 3.4.0
 * @param array  $term_ids Array of terms IDs or objects.
 * @param string $taxonomy Taxonomy.
 * @return array
 */
function wc_get_product_taxonomy_class( $term_ids, $taxonomy ) {
	$classes = array();

	foreach ( $term_ids as $term_id ) {
		$term = get_term( $term_id, $taxonomy );

		if ( empty( $term->slug ) ) {
			continue;
		}

		$term_class = sanitize_html_class( $term->slug, $term->term_id );
		if ( is_numeric( $term_class ) || ! trim( $term_class, '-' ) ) {
			$term_class = $term->term_id;
		}

		// 'post_tag' uses the 'tag' prefix for backward compatibility.
		if ( 'post_tag' === $taxonomy ) {
			$classes[] = 'tag-' . $term_class;
		} else {
			$classes[] = sanitize_html_class( $taxonomy . '-' . $term_class, $taxonomy . '-' . $term->term_id );
		}
	}

	return $classes;
}

/**
 * Retrieves the classes for the post div as an array.
 *
 * This method was modified from WordPress's get_post_class() to allow the removal of taxonomies
 * (for performance reasons). Previously wc_product_post_class was hooked into post_class. @since 3.6.0
 *
 * @since 3.4.0
 * @param string|array           $class      One or more classes to add to the class list.
 * @param int|WP_Post|WC_Product $product Product ID or product object.
 * @return array
 */
function wc_get_product_class( $class = '', $product = null ) {
	if ( is_null( $product ) && ! empty( $GLOBALS['product'] ) ) {
		// Product was null so pull from global.
		$product = $GLOBALS['product'];
	}

	if ( $product && ! is_a( $product, 'WC_Product' ) ) {
		// Make sure we have a valid product, or set to false.
		$product = wc_get_product( $product );
	}

	if ( $class ) {
		if ( ! is_array( $class ) ) {
			$class = preg_split( '#\s+#', $class );
		}
	} else {
		$class = array();
	}

	$post_classes = array_map( 'esc_attr', $class );

	if ( ! $product ) {
		return $post_classes;
	}

	// Run through the post_class hook so 3rd parties using this previously can still append classes.
	// Note, to change classes you will need to use the newer woocommerce_post_class filter.
	// @internal This removes the wc_product_post_class filter so classes are not duplicated.
	$filtered = has_filter( 'post_class', 'wc_product_post_class' );

	if ( $filtered ) {
		remove_filter( 'post_class', 'wc_product_post_class', 20 );
	}

	$post_classes = apply_filters( 'post_class', $post_classes, $class, $product->get_id() );

	if ( $filtered ) {
		add_filter( 'post_class', 'wc_product_post_class', 20, 3 );
	}

	$classes = array_merge(
		$post_classes,
		array(
			'product',
			'type-product',
			'post-' . $product->get_id(),
			'status-' . $product->get_status(),
			wc_get_loop_class(),
			$product->get_stock_status(),
		),
		wc_get_product_taxonomy_class( $product->get_category_ids(), 'product_cat' ),
		wc_get_product_taxonomy_class( $product->get_tag_ids(), 'product_tag' )
	);

	if ( $product->get_image_id() ) {
		$classes[] = 'has-post-thumbnail';
	}
	if ( $product->get_post_password() ) {
		$classes[] = post_password_required( $product->get_id() ) ? 'post-password-required' : 'post-password-protected';
	}
	if ( $product->is_on_sale() ) {
		$classes[] = 'sale';
	}
	if ( $product->is_featured() ) {
		$classes[] = 'featured';
	}
	if ( $product->is_downloadable() ) {
		$classes[] = 'downloadable';
	}
	if ( $product->is_virtual() ) {
		$classes[] = 'virtual';
	}
	if ( $product->is_sold_individually() ) {
		$classes[] = 'sold-individually';
	}
	if ( $product->is_taxable() ) {
		$classes[] = 'taxable';
	}
	if ( $product->is_shipping_taxable() ) {
		$classes[] = 'shipping-taxable';
	}
	if ( $product->is_purchasable() ) {
		$classes[] = 'purchasable';
	}
	if ( $product->get_type() ) {
		$classes[] = 'product-type-' . $product->get_type();
	}
	if ( $product->is_type( 'variable' ) && $product->get_default_attributes() ) {
		$classes[] = 'has-default-attributes';
	}

	// Include attributes and any extra taxonomies only if enabled via the hook - this is a performance issue.
	if ( apply_filters( 'woocommerce_get_product_class_include_taxonomies', false ) ) {
		$taxonomies = get_taxonomies( array( 'public' => true ) );
		$type       = 'variation' === $product->get_type() ? 'product_variation' : 'product';
		foreach ( (array) $taxonomies as $taxonomy ) {
			if ( is_object_in_taxonomy( $type, $taxonomy ) && ! in_array( $taxonomy, array( 'product_cat', 'product_tag' ), true ) ) {
				$classes = array_merge( $classes, wc_get_product_taxonomy_class( (array) get_the_terms( $product->get_id(), $taxonomy ), $taxonomy ) );
			}
		}
	}

	/**
	 * WooCommerce Post Class filter.
	 *
	 * @since 3.6.2
	 * @param array      $classes Array of CSS classes.
	 * @param WC_Product $product Product object.
	 */
	$classes = apply_filters( 'woocommerce_post_class', $classes, $product );

	return array_map( 'esc_attr', array_unique( array_filter( $classes ) ) );
}

/**
 * Display the classes for the product div.
 *
 * @since 3.4.0
 * @param string|array           $class      One or more classes to add to the class list.
 * @param int|WP_Post|WC_Product $product_id Product ID or product object.
 */
function wc_product_class( $class = '', $product_id = null ) {
	echo 'class="' . esc_attr( implode( ' ', wc_get_product_class( $class, $product_id ) ) ) . '"';
}

/**
 * Outputs hidden form inputs for each query string variable.
 *
 * @since 3.0.0
 * @param string|array $values Name value pairs, or a URL to parse.
 * @param array        $exclude Keys to exclude.
 * @param string       $current_key Current key we are outputting.
 * @param bool         $return Whether to return.
 * @return string
 */
function wc_query_string_form_fields( $values = null, $exclude = array(), $current_key = '', $return = false ) {
	if ( is_null( $values ) ) {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$values = $_GET;
	} elseif ( is_string( $values ) ) {
		$url_parts = wp_parse_url( $values );
		$values    = array();

		if ( ! empty( $url_parts['query'] ) ) {
			// This is to preserve full-stops, pluses and spaces in the query string when ran through parse_str.
			$replace_chars = array(
				'.' => '{dot}',
				'+' => '{plus}',
			);

			$query_string = str_replace( array_keys( $replace_chars ), array_values( $replace_chars ), $url_parts['query'] );

			// Parse the string.
			parse_str( $query_string, $parsed_query_string );

			// Convert the full-stops, pluses and spaces back and add to values array.
			foreach ( $parsed_query_string as $key => $value ) {
				$new_key            = str_replace( array_values( $replace_chars ), array_keys( $replace_chars ), $key );
				$new_value          = str_replace( array_values( $replace_chars ), array_keys( $replace_chars ), $value );
				$values[ $new_key ] = $new_value;
			}
		}
	}
	$html = '';

	foreach ( $values as $key => $value ) {
		if ( in_array( $key, $exclude, true ) ) {
			continue;
		}
		if ( $current_key ) {
			$key = $current_key . '[' . $key . ']';
		}
		if ( is_array( $value ) ) {
			$html .= wc_query_string_form_fields( $value, $exclude, $key, true );
		} else {
			$html .= '<input type="hidden" name="' . esc_attr( $key ) . '" value="' . esc_attr( wp_unslash( $value ) ) . '" />';
		}
	}

	if ( $return ) {
		return $html;
	}

	echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}

/**
 * Get the terms and conditons page ID.
 *
 * @since 3.4.0
 * @return int
 */
function wc_terms_and_conditions_page_id() {
	$page_id = wc_get_page_id( 'terms' );
	return apply_filters( 'woocommerce_terms_and_conditions_page_id', 0 < $page_id ? absint( $page_id ) : 0 );
}

/**
 * Get the privacy policy page ID.
 *
 * @since 3.4.0
 * @return int
 */
function wc_privacy_policy_page_id() {
	$page_id = get_option( 'wp_page_for_privacy_policy', 0 );
	return apply_filters( 'woocommerce_privacy_policy_page_id', 0 < $page_id ? absint( $page_id ) : 0 );
}

/**
 * See if the checkbox is enabled or not based on the existance of the terms page and checkbox text.
 *
 * @since 3.4.0
 * @return bool
 */
function wc_terms_and_conditions_checkbox_enabled() {
	$page_id = wc_terms_and_conditions_page_id();
	$page    = $page_id ? get_post( $page_id ) : false;
	return $page && wc_get_terms_and_conditions_checkbox_text();
}

/**
 * Get the terms and conditons checkbox text, if set.
 *
 * @since 3.4.0
 * @return string
 */
function wc_get_terms_and_conditions_checkbox_text() {
	/* translators: %s terms and conditions page name and link */
	return trim( apply_filters( 'woocommerce_get_terms_and_conditions_checkbox_text', get_option( 'woocommerce_checkout_terms_and_conditions_checkbox_text', sprintf( __( 'I have read and agree to the website %s', 'woocommerce' ), '[terms]' ) ) ) );
}

/**
 * Get the privacy policy text, if set.
 *
 * @since 3.4.0
 * @param string $type Type of policy to load. Valid values include registration and checkout.
 * @return string
 */
function wc_get_privacy_policy_text( $type = '' ) {
	$text = '';

	switch ( $type ) {
		case 'checkout':
			/* translators: %s privacy policy page name and link */
			$text = get_option( 'woocommerce_checkout_privacy_policy_text', sprintf( __( 'Your personal data will be used to process your order, support your experience throughout this website, and for other purposes described in our %s.', 'woocommerce' ), '[privacy_policy]' ) );
			break;
		case 'registration':
			/* translators: %s privacy policy page name and link */
			$text = get_option( 'woocommerce_registration_privacy_policy_text', sprintf( __( 'Your personal data will be used to support your experience throughout this website, to manage access to your account, and for other purposes described in our %s.', 'woocommerce' ), '[privacy_policy]' ) );
			break;
	}

	return trim( apply_filters( 'woocommerce_get_privacy_policy_text', $text, $type ) );
}

/**
 * Output t&c checkbox text.
 *
 * @since 3.4.0
 */
function wc_terms_and_conditions_checkbox_text() {
	$text = wc_get_terms_and_conditions_checkbox_text();

	if ( ! $text ) {
		return;
	}

	echo wp_kses_post( wc_replace_policy_page_link_placeholders( $text ) );
}

/**
 * Output t&c page's content (if set). The page can be set from checkout settings.
 *
 * @since 3.4.0
 */
function wc_terms_and_conditions_page_content() {
	$terms_page_id = wc_terms_and_conditions_page_id();

	if ( ! $terms_page_id ) {
		return;
	}

	$page = get_post( $terms_page_id );

	if ( $page && 'publish' === $page->post_status && $page->post_content && ! has_shortcode( $page->post_content, 'woocommerce_checkout' ) ) {
		echo '<div class="woocommerce-terms-and-conditions" style="display: none; max-height: 200px; overflow: auto;">' . wc_format_content( wp_kses_post( $page->post_content ) ) . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
	}
}

/**
 * Render privacy policy text on the checkout.
 *
 * @since 3.4.0
 */
function wc_checkout_privacy_policy_text() {
	echo '<div class="woocommerce-privacy-policy-text">';
	wc_privacy_policy_text( 'checkout' );
	echo '</div>';
}

/**
 * Render privacy policy text on the register forms.
 *
 * @since 3.4.0
 */
function wc_registration_privacy_policy_text() {
	echo '<div class="woocommerce-privacy-policy-text">';
	wc_privacy_policy_text( 'registration' );
	echo '</div>';
}

/**
 * Output privacy policy text. This is custom text which can be added via the customizer/privacy settings section.
 *
 * Loads the relevant policy for the current page unless a specific policy text is required.
 *
 * @since 3.4.0
 * @param string $type Type of policy to load. Valid values include registration and checkout.
 */
function wc_privacy_policy_text( $type = 'checkout' ) {
	if ( ! wc_privacy_policy_page_id() ) {
		return;
	}
	echo wp_kses_post( wpautop( wc_replace_policy_page_link_placeholders( wc_get_privacy_policy_text( $type ) ) ) );
}

/**
 * Replaces placeholders with links to WooCommerce policy pages.
 *
 * @since 3.4.0
 * @param string $text Text to find/replace within.
 * @return string
 */
function wc_replace_policy_page_link_placeholders( $text ) {
	$privacy_page_id = wc_privacy_policy_page_id();
	$terms_page_id   = wc_terms_and_conditions_page_id();
	$privacy_link    = $privacy_page_id ? '<a href="' . esc_url( get_permalink( $privacy_page_id ) ) . '" class="woocommerce-privacy-policy-link" target="_blank">' . __( 'privacy policy', 'woocommerce' ) . '</a>' : __( 'privacy policy', 'woocommerce' );
	$terms_link      = $terms_page_id ? '<a href="' . esc_url( get_permalink( $terms_page_id ) ) . '" class="woocommerce-terms-and-conditions-link" target="_blank">' . __( 'terms and conditions', 'woocommerce' ) . '</a>' : __( 'terms and conditions', 'woocommerce' );

	$find_replace = array(
		'[terms]'          => $terms_link,
		'[privacy_policy]' => $privacy_link,
	);

	return str_replace( array_keys( $find_replace ), array_values( $find_replace ), $text );
}

/**
 * Template pages
 */

if ( ! function_exists( 'woocommerce_content' ) ) {

	/**
	 * Output WooCommerce content.
	 *
	 * This function is only used in the optional 'woocommerce.php' template.
	 * which people can add to their themes to add basic woocommerce support.
	 * without hooks or modifying core templates.
	 */
	function woocommerce_content() {

		if ( is_singular( 'product' ) ) {

			while ( have_posts() ) :
				the_post();
				wc_get_template_part( 'content', 'single-product' );
			endwhile;

		} else {
			?>

			<?php if ( apply_filters( 'woocommerce_show_page_title', true ) ) : ?>

				<h1 class="page-title"><?php woocommerce_page_title(); ?></h1>

			<?php endif; ?>

			<?php do_action( 'woocommerce_archive_description' ); ?>

			<?php if ( woocommerce_product_loop() ) : ?>

				<?php do_action( 'woocommerce_before_shop_loop' ); ?>

				<?php woocommerce_product_loop_start(); ?>

				<?php if ( wc_get_loop_prop( 'total' ) ) : ?>
					<?php while ( have_posts() ) : ?>
						<?php the_post(); ?>
						<?php wc_get_template_part( 'content', 'product' ); ?>
					<?php endwhile; ?>
				<?php endif; ?>

				<?php woocommerce_product_loop_end(); ?>

				<?php do_action( 'woocommerce_after_shop_loop' ); ?>

				<?php
			else :
				do_action( 'woocommerce_no_products_found' );
			endif;
		}
	}
}

/**
 * Global
 */

if ( ! function_exists( 'woocommerce_output_content_wrapper' ) ) {

	/**
	 * Output the start of the page wrapper.
	 */
	function woocommerce_output_content_wrapper() {
		wc_get_template( 'global/wrapper-start.php' );
	}
}
if ( ! function_exists( 'woocommerce_output_content_wrapper_end' ) ) {

	/**
	 * Output the end of the page wrapper.
	 */
	function woocommerce_output_content_wrapper_end() {
		wc_get_template( 'global/wrapper-end.php' );
	}
}

if ( ! function_exists( 'woocommerce_get_sidebar' ) ) {

	/**
	 * Get the shop sidebar template.
	 */
	function woocommerce_get_sidebar() {
		wc_get_template( 'global/sidebar.php' );
	}
}

if ( ! function_exists( 'woocommerce_demo_store' ) ) {

	/**
	 * Adds a demo store banner to the site if enabled.
	 */
	function woocommerce_demo_store() {
		if ( ! is_store_notice_showing() ) {
			return;
		}

		$notice = get_option( 'woocommerce_demo_store_notice' );

		if ( empty( $notice ) ) {
			$notice = __( 'This is a demo store for testing purposes &mdash; no orders shall be fulfilled.', 'woocommerce' );
		}

		$notice_id = md5( $notice );

		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		echo apply_filters( 'woocommerce_demo_store', '<p class="woocommerce-store-notice demo_store" data-notice-id="' . esc_attr( $notice_id ) . '" style="display:none;">' . wp_kses_post( $notice ) . ' <a href="#" class="woocommerce-store-notice__dismiss-link">' . esc_html__( 'Dismiss', 'woocommerce' ) . '</a></p>', $notice );
	}
}

/**
 * Loop
 */

if ( ! function_exists( 'woocommerce_page_title' ) ) {

	/**
	 * Page Title function.
	 *
	 * @param  bool $echo Should echo title.
	 * @return string
	 */
	function woocommerce_page_title( $echo = true ) {

		if ( is_search() ) {
			/* translators: %s: search query */
			$page_title = sprintf( __( 'Search results: &ldquo;%s&rdquo;', 'woocommerce' ), get_search_query() );

			if ( get_query_var( 'paged' ) ) {
				/* translators: %s: page number */
				$page_title .= sprintf( __( '&nbsp;&ndash; Page %s', 'woocommerce' ), get_query_var( 'paged' ) );
			}
		} elseif ( is_tax() ) {

			$page_title = single_term_title( '', false );

		} else {

			$shop_page_id = wc_get_page_id( 'shop' );
			$page_title   = get_the_title( $shop_page_id );

		}

		$page_title = apply_filters( 'woocommerce_page_title', $page_title );

		if ( $echo ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			echo $page_title;
		} else {
			return $page_title;
		}
	}
}

if ( ! function_exists( 'woocommerce_product_loop_start' ) ) {

	/**
	 * Output the start of a product loop. By default this is a UL.
	 *
	 * @param bool $echo Should echo?.
	 * @return string
	 */
	function woocommerce_product_loop_start( $echo = true ) {
		ob_start();

		wc_set_loop_prop( 'loop', 0 );

		wc_get_template( 'loop/loop-start.php' );

		$loop_start = apply_filters( 'woocommerce_product_loop_start', ob_get_clean() );

		if ( $echo ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			echo $loop_start;
		} else {
			return $loop_start;
		}
	}
}

if ( ! function_exists( 'woocommerce_product_loop_end' ) ) {

	/**
	 * Output the end of a product loop. By default this is a UL.
	 *
	 * @param bool $echo Should echo?.
	 * @return string
	 */
	function woocommerce_product_loop_end( $echo = true ) {
		ob_start();

		wc_get_template( 'loop/loop-end.php' );

		$loop_end = apply_filters( 'woocommerce_product_loop_end', ob_get_clean() );

		if ( $echo ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			echo $loop_end;
		} else {
			return $loop_end;
		}
	}
}
if ( ! function_exists( 'woocommerce_template_loop_product_title' ) ) {

	/**
	 * Show the product title in the product loop. By default this is an H2.
	 */
	function woocommerce_template_loop_product_title() {
		echo '<h2 class="' . esc_attr( apply_filters( 'woocommerce_product_loop_title_classes', 'woocommerce-loop-product__title' ) ) . '">' . get_the_title() . '</h2>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
	}
}
if ( ! function_exists( 'woocommerce_template_loop_category_title' ) ) {

	/**
	 * Show the subcategory title in the product loop.
	 *
	 * @param object $category Category object.
	 */
	function woocommerce_template_loop_category_title( $category ) {
		?>
		<h2 class="woocommerce-loop-category__title">
			<?php
			echo esc_html( $category->name );

			if ( $category->count > 0 ) {
				// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
				echo apply_filters( 'woocommerce_subcategory_count_html', ' <mark class="count">(' . esc_html( $category->count ) . ')</mark>', $category );
			}
			?>
		</h2>
		<?php
	}
}

if ( ! function_exists( 'woocommerce_template_loop_product_link_open' ) ) {
	/**
	 * Insert the opening anchor tag for products in the loop.
	 */
	function woocommerce_template_loop_product_link_open() {
		global $product;

		$link = apply_filters( 'woocommerce_loop_product_link', get_the_permalink(), $product );

		echo '<a href="' . esc_url( $link ) . '" class="woocommerce-LoopProduct-link woocommerce-loop-product__link">';
	}
}

if ( ! function_exists( 'woocommerce_template_loop_product_link_close' ) ) {
	/**
	 * Insert the closing anchor tag for products in the loop.
	 */
	function woocommerce_template_loop_product_link_close() {
		echo '</a>';
	}
}

if ( ! function_exists( 'woocommerce_template_loop_category_link_open' ) ) {
	/**
	 * Insert the opening anchor tag for categories in the loop.
	 *
	 * @param int|object|string $category Category ID, Object or String.
	 */
	function woocommerce_template_loop_category_link_open( $category ) {
		echo '<a href="' . esc_url( get_term_link( $category, 'product_cat' ) ) . '">';
	}
}

if ( ! function_exists( 'woocommerce_template_loop_category_link_close' ) ) {
	/**
	 * Insert the closing anchor tag for categories in the loop.
	 */
	function woocommerce_template_loop_category_link_close() {
		echo '</a>';
	}
}

if ( ! function_exists( 'woocommerce_taxonomy_archive_description' ) ) {

	/**
	 * Show an archive description on taxonomy archives.
	 */
	function woocommerce_taxonomy_archive_description() {
		if ( is_product_taxonomy() && 0 === absint( get_query_var( 'paged' ) ) ) {
			$term = get_queried_object();

			if ( $term && ! empty( $term->description ) ) {
				echo '<div class="term-description">' . wc_format_content( wp_kses_post( $term->description ) ) . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			}
		}
	}
}
if ( ! function_exists( 'woocommerce_product_archive_description' ) ) {

	/**
	 * Show a shop page description on product archives.
	 */
	function woocommerce_product_archive_description() {
		// Don't display the description on search results page.
		if ( is_search() ) {
			return;
		}

		if ( is_post_type_archive( 'product' ) && in_array( absint( get_query_var( 'paged' ) ), array( 0, 1 ), true ) ) {
			$shop_page = get_post( wc_get_page_id( 'shop' ) );
			if ( $shop_page ) {

				$allowed_html = wp_kses_allowed_html( 'post' );

				// This is needed for the search product block to work.
				$allowed_html = array_merge(
					$allowed_html,
					array(
						'form'   => array(
							'action'         => true,
							'accept'         => true,
							'accept-charset' => true,
							'enctype'        => true,
							'method'         => true,
							'name'           => true,
							'target'         => true,
						),

						'input'  => array(
							'type'        => true,
							'id'          => true,
							'class'       => true,
							'placeholder' => true,
							'name'        => true,
							'value'       => true,
						),

						'button' => array(
							'type'  => true,
							'class' => true,
							'label' => true,
						),

						'svg'    => array(
							'hidden'    => true,
							'role'      => true,
							'focusable' => true,
							'xmlns'     => true,
							'width'     => true,
							'height'    => true,
							'viewbox'   => true,
						),
						'path'   => array(
							'd' => true,
						),
					)
				);

				$description = wc_format_content( wp_kses( $shop_page->post_content, $allowed_html ) );
				if ( $description ) {
					echo '<div class="page-description">' . $description . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
				}
			}
		}
	}
}

if ( ! function_exists( 'woocommerce_template_loop_add_to_cart' ) ) {

	/**
	 * Get the add to cart template for the loop.
	 *
	 * @param array $args Arguments.
	 */
	function woocommerce_template_loop_add_to_cart( $args = array() ) {
		global $product;

		if ( $product ) {
			$defaults = array(
				'quantity'   => 1,
				'class'      => implode(
					' ',
					array_filter(
						array(
							'button',
							'product_type_' . $product->get_type(),
							$product->is_purchasable() && $product->is_in_stock() ? 'add_to_cart_button' : '',
							$product->supports( 'ajax_add_to_cart' ) && $product->is_purchasable() && $product->is_in_stock() ? 'ajax_add_to_cart' : '',
						)
					)
				),
				'attributes' => array(
					'data-product_id'  => $product->get_id(),
					'data-product_sku' => $product->get_sku(),
					'aria-label'       => $product->add_to_cart_description(),
					'rel'              => 'nofollow',
				),
			);

			$args = apply_filters( 'woocommerce_loop_add_to_cart_args', wp_parse_args( $args, $defaults ), $product );

			if ( isset( $args['attributes']['aria-label'] ) ) {
				$args['attributes']['aria-label'] = wp_strip_all_tags( $args['attributes']['aria-label'] );
			}

			wc_get_template( 'loop/add-to-cart.php', $args );
		}
	}
}

if ( ! function_exists( 'woocommerce_template_loop_product_thumbnail' ) ) {

	/**
	 * Get the product thumbnail for the loop.
	 */
	function woocommerce_template_loop_product_thumbnail() {
		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		echo woocommerce_get_product_thumbnail();
	}
}
if ( ! function_exists( 'woocommerce_template_loop_price' ) ) {

	/**
	 * Get the product price for the loop.
	 */
	function woocommerce_template_loop_price() {
		wc_get_template( 'loop/price.php' );
	}
}
if ( ! function_exists( 'woocommerce_template_loop_rating' ) ) {

	/**
	 * Display the average rating in the loop.
	 */
	function woocommerce_template_loop_rating() {
		wc_get_template( 'loop/rating.php' );
	}
}
if ( ! function_exists( 'woocommerce_show_product_loop_sale_flash' ) ) {

	/**
	 * Get the sale flash for the loop.
	 */
	function woocommerce_show_product_loop_sale_flash() {
		wc_get_template( 'loop/sale-flash.php' );
	}
}

if ( ! function_exists( 'woocommerce_get_product_thumbnail' ) ) {

	/**
	 * Get the product thumbnail, or the placeholder if not set.
	 *
	 * @param string $size (default: 'woocommerce_thumbnail').
	 * @param int    $deprecated1 Deprecated since WooCommerce 2.0 (default: 0).
	 * @param int    $deprecated2 Deprecated since WooCommerce 2.0 (default: 0).
	 * @return string
	 */
	function woocommerce_get_product_thumbnail( $size = 'woocommerce_thumbnail', $deprecated1 = 0, $deprecated2 = 0 ) {
		global $product;

		$image_size = apply_filters( 'single_product_archive_thumbnail_size', $size );

		return $product ? $product->get_image( $image_size ) : '';
	}
}

if ( ! function_exists( 'woocommerce_result_count' ) ) {

	/**
	 * Output the result count text (Showing x - x of x results).
	 */
	function woocommerce_result_count() {
		if ( ! wc_get_loop_prop( 'is_paginated' ) || ! woocommerce_products_will_display() ) {
			return;
		}
		$args = array(
			'total'    => wc_get_loop_prop( 'total' ),
			'per_page' => wc_get_loop_prop( 'per_page' ),
			'current'  => wc_get_loop_prop( 'current_page' ),
		);

		wc_get_template( 'loop/result-count.php', $args );
	}
}

if ( ! function_exists( 'woocommerce_catalog_ordering' ) ) {

	/**
	 * Output the product sorting options.
	 */
	function woocommerce_catalog_ordering() {
		if ( ! wc_get_loop_prop( 'is_paginated' ) || ! woocommerce_products_will_display() ) {
			return;
		}
		$show_default_orderby    = 'menu_order' === apply_filters( 'woocommerce_default_catalog_orderby', get_option( 'woocommerce_default_catalog_orderby', 'menu_order' ) );
		$catalog_orderby_options = apply_filters(
			'woocommerce_catalog_orderby',
			array(
				'menu_order' => __( 'Default sorting', 'woocommerce' ),
				'popularity' => __( 'Sort by popularity', 'woocommerce' ),
				'rating'     => __( 'Sort by average rating', 'woocommerce' ),
				'date'       => __( 'Sort by latest', 'woocommerce' ),
				'price'      => __( 'Sort by price: low to high', 'woocommerce' ),
				'price-desc' => __( 'Sort by price: high to low', 'woocommerce' ),
			)
		);

		$default_orderby = wc_get_loop_prop( 'is_search' ) ? 'relevance' : apply_filters( 'woocommerce_default_catalog_orderby', get_option( 'woocommerce_default_catalog_orderby', '' ) );
		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		$orderby = isset( $_GET['orderby'] ) ? wc_clean( wp_unslash( $_GET['orderby'] ) ) : $default_orderby;
		// phpcs:enable WordPress.Security.NonceVerification.Recommended

		if ( wc_get_loop_prop( 'is_search' ) ) {
			$catalog_orderby_options = array_merge( array( 'relevance' => __( 'Relevance', 'woocommerce' ) ), $catalog_orderby_options );

			unset( $catalog_orderby_options['menu_order'] );
		}

		if ( ! $show_default_orderby ) {
			unset( $catalog_orderby_options['menu_order'] );
		}

		if ( ! wc_review_ratings_enabled() ) {
			unset( $catalog_orderby_options['rating'] );
		}

		if ( ! array_key_exists( $orderby, $catalog_orderby_options ) ) {
			$orderby = current( array_keys( $catalog_orderby_options ) );
		}

		wc_get_template(
			'loop/orderby.php',
			array(
				'catalog_orderby_options' => $catalog_orderby_options,
				'orderby'                 => $orderby,
				'show_default_orderby'    => $show_default_orderby,
			)
		);
	}
}

if ( ! function_exists( 'woocommerce_pagination' ) ) {

	/**
	 * Output the pagination.
	 */
	function woocommerce_pagination() {
		if ( ! wc_get_loop_prop( 'is_paginated' ) || ! woocommerce_products_will_display() ) {
			return;
		}

		$args = array(
			'total'   => wc_get_loop_prop( 'total_pages' ),
			'current' => wc_get_loop_prop( 'current_page' ),
			'base'    => esc_url_raw( add_query_arg( 'product-page', '%#%', false ) ),
			'format'  => '?product-page=%#%',
		);

		if ( ! wc_get_loop_prop( 'is_shortcode' ) ) {
			$args['format'] = '';
			$args['base']   = esc_url_raw( str_replace( 999999999, '%#%', remove_query_arg( 'add-to-cart', get_pagenum_link( 999999999, false ) ) ) );
		}

		wc_get_template( 'loop/pagination.php', $args );
	}
}

/**
 * Single Product
 */

if ( ! function_exists( 'woocommerce_show_product_images' ) ) {

	/**
	 * Output the product image before the single product summary.
	 */
	function woocommerce_show_product_images() {
		wc_get_template( 'single-product/product-image.php' );
	}
}
if ( ! function_exists( 'woocommerce_show_product_thumbnails' ) ) {

	/**
	 * Output the product thumbnails.
	 */
	function woocommerce_show_product_thumbnails() {
		wc_get_template( 'single-product/product-thumbnails.php' );
	}
}

/**
 * Get HTML for a gallery image.
 *
 * Hooks: woocommerce_gallery_thumbnail_size, woocommerce_gallery_image_size and woocommerce_gallery_full_size accept name based image sizes, or an array of width/height values.
 *
 * @since 3.3.2
 * @param int  $attachment_id Attachment ID.
 * @param bool $main_image Is this the main image or a thumbnail?.
 * @return string
 */
function wc_get_gallery_image_html( $attachment_id, $main_image = false ) {
	$flexslider        = (bool) apply_filters( 'woocommerce_single_product_flexslider_enabled', get_theme_support( 'wc-product-gallery-slider' ) );
	$gallery_thumbnail = wc_get_image_size( 'gallery_thumbnail' );
	$thumbnail_size    = apply_filters( 'woocommerce_gallery_thumbnail_size', array( $gallery_thumbnail['width'], $gallery_thumbnail['height'] ) );
	$image_size        = apply_filters( 'woocommerce_gallery_image_size', $flexslider || $main_image ? 'woocommerce_single' : $thumbnail_size );
	$full_size         = apply_filters( 'woocommerce_gallery_full_size', apply_filters( 'woocommerce_product_thumbnails_large_size', 'full' ) );
	$thumbnail_src     = wp_get_attachment_image_src( $attachment_id, $thumbnail_size );
	$full_src          = wp_get_attachment_image_src( $attachment_id, $full_size );
	$alt_text          = trim( wp_strip_all_tags( get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ) );
	$image             = wp_get_attachment_image(
		$attachment_id,
		$image_size,
		false,
		apply_filters(
			'woocommerce_gallery_image_html_attachment_image_params',
			array(
				'title'                   => _wp_specialchars( get_post_field( 'post_title', $attachment_id ), ENT_QUOTES, 'UTF-8', true ),
				'data-caption'            => _wp_specialchars( get_post_field( 'post_excerpt', $attachment_id ), ENT_QUOTES, 'UTF-8', true ),
				'data-src'                => esc_url( $full_src[0] ),
				'data-large_image'        => esc_url( $full_src[0] ),
				'data-large_image_width'  => esc_attr( $full_src[1] ),
				'data-large_image_height' => esc_attr( $full_src[2] ),
				'class'                   => esc_attr( $main_image ? 'wp-post-image' : '' ),
			),
			$attachment_id,
			$image_size,
			$main_image
		)
	);

	return '<div data-thumb="' . esc_url( $thumbnail_src[0] ) . '" data-thumb-alt="' . esc_attr( $alt_text ) . '" class="woocommerce-product-gallery__image"><a href="' . esc_url( $full_src[0] ) . '">' . $image . '</a></div>';
}

if ( ! function_exists( 'woocommerce_output_product_data_tabs' ) ) {

	/**
	 * Output the product tabs.
	 */
	function woocommerce_output_product_data_tabs() {
		wc_get_template( 'single-product/tabs/tabs.php' );
	}
}
if ( ! function_exists( 'woocommerce_template_single_title' ) ) {

	/**
	 * Output the product title.
	 */
	function woocommerce_template_single_title() {
		wc_get_template( 'single-product/title.php' );
	}
}
if ( ! function_exists( 'woocommerce_template_single_rating' ) ) {

	/**
	 * Output the product rating.
	 */
	function woocommerce_template_single_rating() {
		if ( post_type_supports( 'product', 'comments' ) ) {
			wc_get_template( 'single-product/rating.php' );
		}
	}
}
if ( ! function_exists( 'woocommerce_template_single_price' ) ) {

	/**
	 * Output the product price.
	 */
	function woocommerce_template_single_price() {
		wc_get_template( 'single-product/price.php' );
	}
}
if ( ! function_exists( 'woocommerce_template_single_excerpt' ) ) {

	/**
	 * Output the product short description (excerpt).
	 */
	function woocommerce_template_single_excerpt() {
		wc_get_template( 'single-product/short-description.php' );
	}
}
if ( ! function_exists( 'woocommerce_template_single_meta' ) ) {

	/**
	 * Output the product meta.
	 */
	function woocommerce_template_single_meta() {
		wc_get_template( 'single-product/meta.php' );
	}
}
if ( ! function_exists( 'woocommerce_template_single_sharing' ) ) {

	/**
	 * Output the product sharing.
	 */
	function woocommerce_template_single_sharing() {
		wc_get_template( 'single-product/share.php' );
	}
}
if ( ! function_exists( 'woocommerce_show_product_sale_flash' ) ) {

	/**
	 * Output the product sale flash.
	 */
	function woocommerce_show_product_sale_flash() {
		wc_get_template( 'single-product/sale-flash.php' );
	}
}

if ( ! function_exists( 'woocommerce_template_single_add_to_cart' ) ) {

	/**
	 * Trigger the single product add to cart action.
	 */
	function woocommerce_template_single_add_to_cart() {
		global $product;
		do_action( 'woocommerce_' . $product->get_type() . '_add_to_cart' );
	}
}
if ( ! function_exists( 'woocommerce_simple_add_to_cart' ) ) {

	/**
	 * Output the simple product add to cart area.
	 */
	function woocommerce_simple_add_to_cart() {
		wc_get_template( 'single-product/add-to-cart/simple.php' );
	}
}
if ( ! function_exists( 'woocommerce_grouped_add_to_cart' ) ) {

	/**
	 * Output the grouped product add to cart area.
	 */
	function woocommerce_grouped_add_to_cart() {
		global $product;

		$products = array_filter( array_map( 'wc_get_product', $product->get_children() ), 'wc_products_array_filter_visible_grouped' );

		if ( $products ) {
			wc_get_template(
				'single-product/add-to-cart/grouped.php',
				array(
					'grouped_product'    => $product,
					'grouped_products'   => $products,
					'quantites_required' => false,
				)
			);
		}
	}
}
if ( ! function_exists( 'woocommerce_variable_add_to_cart' ) ) {

	/**
	 * Output the variable product add to cart area.
	 */
	function woocommerce_variable_add_to_cart() {
		global $product;

		// Enqueue variation scripts.
		wp_enqueue_script( 'wc-add-to-cart-variation' );

		// Get Available variations?
		$get_variations = count( $product->get_children() ) <= apply_filters( 'woocommerce_ajax_variation_threshold', 30, $product );

		// Load the template.
		wc_get_template(
			'single-product/add-to-cart/variable.php',
			array(
				'available_variations' => $get_variations ? $product->get_available_variations() : false,
				'attributes'           => $product->get_variation_attributes(),
				'selected_attributes'  => $product->get_default_attributes(),
			)
		);
	}
}
if ( ! function_exists( 'woocommerce_external_add_to_cart' ) ) {

	/**
	 * Output the external product add to cart area.
	 */
	function woocommerce_external_add_to_cart() {
		global $product;

		if ( ! $product->add_to_cart_url() ) {
			return;
		}

		wc_get_template(
			'single-product/add-to-cart/external.php',
			array(
				'product_url' => $product->add_to_cart_url(),
				'button_text' => $product->single_add_to_cart_text(),
			)
		);
	}
}

if ( ! function_exists( 'woocommerce_quantity_input' ) ) {

	/**
	 * Output the quantity input for add to cart forms.
	 *
	 * @param  array           $args Args for the input.
	 * @param  WC_Product|null $product Product.
	 * @param  boolean         $echo Whether to return or echo|string.
	 *
	 * @return string
	 */
	function woocommerce_quantity_input( $args = array(), $product = null, $echo = true ) {
		if ( is_null( $product ) ) {
			$product = $GLOBALS['product'];
		}

		$defaults = array(
			'input_id'     => uniqid( 'quantity_' ),
			'input_name'   => 'quantity',
			'input_value'  => '1',
			'classes'      => apply_filters( 'woocommerce_quantity_input_classes', array( 'input-text', 'qty', 'text' ), $product ),
			'max_value'    => apply_filters( 'woocommerce_quantity_input_max', -1, $product ),
			'min_value'    => apply_filters( 'woocommerce_quantity_input_min', 0, $product ),
			'step'         => apply_filters( 'woocommerce_quantity_input_step', 1, $product ),
			'pattern'      => apply_filters( 'woocommerce_quantity_input_pattern', has_filter( 'woocommerce_stock_amount', 'intval' ) ? '[0-9]*' : '' ),
			'inputmode'    => apply_filters( 'woocommerce_quantity_input_inputmode', has_filter( 'woocommerce_stock_amount', 'intval' ) ? 'numeric' : '' ),
			'product_name' => $product ? $product->get_title() : '',
			'placeholder'  => apply_filters( 'woocommerce_quantity_input_placeholder', '', $product ),
		);

		$args = apply_filters( 'woocommerce_quantity_input_args', wp_parse_args( $args, $defaults ), $product );

		// Apply sanity to min/max args - min cannot be lower than 0.
		$args['min_value'] = max( $args['min_value'], 0 );
		$args['max_value'] = 0 < $args['max_value'] ? $args['max_value'] : '';

		// Max cannot be lower than min if defined.
		if ( '' !== $args['max_value'] && $args['max_value'] < $args['min_value'] ) {
			$args['max_value'] = $args['min_value'];
		}

		ob_start();

		wc_get_template( 'global/quantity-input.php', $args );

		if ( $echo ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			echo ob_get_clean();
		} else {
			return ob_get_clean();
		}
	}
}

if ( ! function_exists( 'woocommerce_product_description_tab' ) ) {

	/**
	 * Output the description tab content.
	 */
	function woocommerce_product_description_tab() {
		wc_get_template( 'single-product/tabs/description.php' );
	}
}
if ( ! function_exists( 'woocommerce_product_additional_information_tab' ) ) {

	/**
	 * Output the attributes tab content.
	 */
	function woocommerce_product_additional_information_tab() {
		wc_get_template( 'single-product/tabs/additional-information.php' );
	}
}
if ( ! function_exists( 'woocommerce_default_product_tabs' ) ) {

	/**
	 * Add default product tabs to product pages.
	 *
	 * @param array $tabs Array of tabs.
	 * @return array
	 */
	function woocommerce_default_product_tabs( $tabs = array() ) {
		global $product, $post;

		// Description tab - shows product content.
		if ( $post->post_content ) {
			$tabs['description'] = array(
				'title'    => __( 'Description', 'woocommerce' ),
				'priority' => 10,
				'callback' => 'woocommerce_product_description_tab',
			);
		}

		// Additional information tab - shows attributes.
		if ( $product && ( $product->has_attributes() || apply_filters( 'wc_product_enable_dimensions_display', $product->has_weight() || $product->has_dimensions() ) ) ) {
			$tabs['additional_information'] = array(
				'title'    => __( 'Additional information', 'woocommerce' ),
				'priority' => 20,
				'callback' => 'woocommerce_product_additional_information_tab',
			);
		}

		// Reviews tab - shows comments.
		if ( comments_open() ) {
			$tabs['reviews'] = array(
				/* translators: %s: reviews count */
				'title'    => sprintf( __( 'Reviews (%d)', 'woocommerce' ), $product->get_review_count() ),
				'priority' => 30,
				'callback' => 'comments_template',
			);
		}

		return $tabs;
	}
}

if ( ! function_exists( 'woocommerce_sort_product_tabs' ) ) {

	/**
	 * Sort tabs by priority.
	 *
	 * @param array $tabs Array of tabs.
	 * @return array
	 */
	function woocommerce_sort_product_tabs( $tabs = array() ) {

		// Make sure the $tabs parameter is an array.
		if ( ! is_array( $tabs ) ) {
			// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
			trigger_error( 'Function woocommerce_sort_product_tabs() expects an array as the first parameter. Defaulting to empty array.' );
			$tabs = array();
		}

		// Re-order tabs by priority.
		if ( ! function_exists( '_sort_priority_callback' ) ) {
			/**
			 * Sort Priority Callback Function
			 *
			 * @param array $a Comparison A.
			 * @param array $b Comparison B.
			 * @return bool
			 */
			function _sort_priority_callback( $a, $b ) {
				if ( ! isset( $a['priority'], $b['priority'] ) || $a['priority'] === $b['priority'] ) {
					return 0;
				}
				return ( $a['priority'] < $b['priority'] ) ? -1 : 1;
			}
		}

		uasort( $tabs, '_sort_priority_callback' );

		return $tabs;
	}
}

if ( ! function_exists( 'woocommerce_comments' ) ) {

	/**
	 * Output the Review comments template.
	 *
	 * @param WP_Comment $comment Comment object.
	 * @param array      $args Arguments.
	 * @param int        $depth Depth.
	 */
	function woocommerce_comments( $comment, $args, $depth ) {
		// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
		$GLOBALS['comment'] = $comment;
		wc_get_template(
			'single-product/review.php',
			array(
				'comment' => $comment,
				'args'    => $args,
				'depth'   => $depth,
			)
		);
	}
}

if ( ! function_exists( 'woocommerce_review_display_gravatar' ) ) {
	/**
	 * Display the review authors gravatar
	 *
	 * @param array $comment WP_Comment.
	 * @return void
	 */
	function woocommerce_review_display_gravatar( $comment ) {
		echo get_avatar( $comment, apply_filters( 'woocommerce_review_gravatar_size', '60' ), '' );
	}
}

if ( ! function_exists( 'woocommerce_review_display_rating' ) ) {
	/**
	 * Display the reviewers star rating
	 *
	 * @return void
	 */
	function woocommerce_review_display_rating() {
		if ( post_type_supports( 'product', 'comments' ) ) {
			wc_get_template( 'single-product/review-rating.php' );
		}
	}
}

if ( ! function_exists( 'woocommerce_review_display_meta' ) ) {
	/**
	 * Display the review authors meta (name, verified owner, review date)
	 *
	 * @return void
	 */
	function woocommerce_review_display_meta() {
		wc_get_template( 'single-product/review-meta.php' );
	}
}

if ( ! function_exists( 'woocommerce_review_display_comment_text' ) ) {

	/**
	 * Display the review content.
	 */
	function woocommerce_review_display_comment_text() {
		echo '<div class="description">';
		comment_text();
		echo '</div>';
	}
}

if ( ! function_exists( 'woocommerce_output_related_products' ) ) {

	/**
	 * Output the related products.
	 */
	function woocommerce_output_related_products() {

		$args = array(
			'posts_per_page' => 4,
			'columns'        => 4,
			'orderby'        => 'rand', // @codingStandardsIgnoreLine.
		);

		woocommerce_related_products( apply_filters( 'woocommerce_output_related_products_args', $args ) );
	}
}

if ( ! function_exists( 'woocommerce_related_products' ) ) {

	/**
	 * Output the related products.
	 *
	 * @param array $args Provided arguments.
	 */
	function woocommerce_related_products( $args = array() ) {
		global $product;

		if ( ! $product ) {
			return;
		}

		$defaults = array(
			'posts_per_page' => 2,
			'columns'        => 2,
			'orderby'        => 'rand', // @codingStandardsIgnoreLine.
			'order'          => 'desc',
		);

		$args = wp_parse_args( $args, $defaults );

		// Get visible related products then sort them at random.
		$args['related_products'] = array_filter( array_map( 'wc_get_product', wc_get_related_products( $product->get_id(), $args['posts_per_page'], $product->get_upsell_ids() ) ), 'wc_products_array_filter_visible' );

		// Handle orderby.
		$args['related_products'] = wc_products_array_orderby( $args['related_products'], $args['orderby'], $args['order'] );

		// Set global loop values.
		wc_set_loop_prop( 'name', 'related' );
		wc_set_loop_prop( 'columns', apply_filters( 'woocommerce_related_products_columns', $args['columns'] ) );

		wc_get_template( 'single-product/related.php', $args );
	}
}

if ( ! function_exists( 'woocommerce_upsell_display' ) ) {

	/**
	 * Output product up sells.
	 *
	 * @param int    $limit (default: -1).
	 * @param int    $columns (default: 4).
	 * @param string $orderby Supported values - rand, title, ID, date, modified, menu_order, price.
	 * @param string $order Sort direction.
	 */
	function woocommerce_upsell_display( $limit = '-1', $columns = 4, $orderby = 'rand', $order = 'desc' ) {
		global $product;

		if ( ! $product ) {
			return;
		}

		// Handle the legacy filter which controlled posts per page etc.
		$args = apply_filters(
			'woocommerce_upsell_display_args',
			array(
				'posts_per_page' => $limit,
				'orderby'        => $orderby,
				'order'          => $order,
				'columns'        => $columns,
			)
		);
		wc_set_loop_prop( 'name', 'up-sells' );
		wc_set_loop_prop( 'columns', apply_filters( 'woocommerce_upsells_columns', isset( $args['columns'] ) ? $args['columns'] : $columns ) );

		$orderby = apply_filters( 'woocommerce_upsells_orderby', isset( $args['orderby'] ) ? $args['orderby'] : $orderby );
		$order   = apply_filters( 'woocommerce_upsells_order', isset( $args['order'] ) ? $args['order'] : $order );
		$limit   = apply_filters( 'woocommerce_upsells_total', isset( $args['posts_per_page'] ) ? $args['posts_per_page'] : $limit );

		// Get visible upsells then sort them at random, then limit result set.
		$upsells = wc_products_array_orderby( array_filter( array_map( 'wc_get_product', $product->get_upsell_ids() ), 'wc_products_array_filter_visible' ), $orderby, $order );
		$upsells = $limit > 0 ? array_slice( $upsells, 0, $limit ) : $upsells;

		wc_get_template(
			'single-product/up-sells.php',
			array(
				'upsells'        => $upsells,

				// Not used now, but used in previous version of up-sells.php.
				'posts_per_page' => $limit,
				'orderby'        => $orderby,
				'columns'        => $columns,
			)
		);
	}
}

/** Cart */

if ( ! function_exists( 'woocommerce_shipping_calculator' ) ) {

	/**
	 * Output the cart shipping calculator.
	 *
	 * @param string $button_text Text for the shipping calculation toggle.
	 */
	function woocommerce_shipping_calculator( $button_text = '' ) {
		if ( 'no' === get_option( 'woocommerce_enable_shipping_calc' ) || ! WC()->cart->needs_shipping() ) {
			return;
		}
		wp_enqueue_script( 'wc-country-select' );
		wc_get_template(
			'cart/shipping-calculator.php',
			array(
				'button_text' => $button_text,
			)
		);
	}
}

if ( ! function_exists( 'woocommerce_cart_totals' ) ) {

	/**
	 * Output the cart totals.
	 */
	function woocommerce_cart_totals() {
		if ( is_checkout() ) {
			return;
		}
		wc_get_template( 'cart/cart-totals.php' );
	}
}

if ( ! function_exists( 'woocommerce_cross_sell_display' ) ) {

	/**
	 * Output the cart cross-sells.
	 *
	 * @param  int    $limit (default: 2).
	 * @param  int    $columns (default: 2).
	 * @param  string $orderby (default: 'rand').
	 * @param  string $order (default: 'desc').
	 */
	function woocommerce_cross_sell_display( $limit = 2, $columns = 2, $orderby = 'rand', $order = 'desc' ) {
		if ( is_checkout() ) {
			return;
		}
		// Get visible cross sells then sort them at random.
		$cross_sells = array_filter( array_map( 'wc_get_product', WC()->cart->get_cross_sells() ), 'wc_products_array_filter_visible' );

		wc_set_loop_prop( 'name', 'cross-sells' );
		wc_set_loop_prop( 'columns', apply_filters( 'woocommerce_cross_sells_columns', $columns ) );

		// Handle orderby and limit results.
		$orderby     = apply_filters( 'woocommerce_cross_sells_orderby', $orderby );
		$order       = apply_filters( 'woocommerce_cross_sells_order', $order );
		$cross_sells = wc_products_array_orderby( $cross_sells, $orderby, $order );
		$limit       = apply_filters( 'woocommerce_cross_sells_total', $limit );
		$cross_sells = $limit > 0 ? array_slice( $cross_sells, 0, $limit ) : $cross_sells;

		wc_get_template(
			'cart/cross-sells.php',
			array(
				'cross_sells'    => $cross_sells,

				// Not used now, but used in previous version of up-sells.php.
				'posts_per_page' => $limit,
				'orderby'        => $orderby,
				'columns'        => $columns,
			)
		);
	}
}

if ( ! function_exists( 'woocommerce_button_proceed_to_checkout' ) ) {

	/**
	 * Output the proceed to checkout button.
	 */
	function woocommerce_button_proceed_to_checkout() {
		wc_get_template( 'cart/proceed-to-checkout-button.php' );
	}
}

if ( ! function_exists( 'woocommerce_widget_shopping_cart_button_view_cart' ) ) {

	/**
	 * Output the view cart button.
	 */
	function woocommerce_widget_shopping_cart_button_view_cart() {
		echo '<a href="' . esc_url( wc_get_cart_url() ) . '" class="button wc-forward">' . esc_html__( 'View cart', 'woocommerce' ) . '</a>';
	}
}

if ( ! function_exists( 'woocommerce_widget_shopping_cart_proceed_to_checkout' ) ) {

	/**
	 * Output the proceed to checkout button.
	 */
	function woocommerce_widget_shopping_cart_proceed_to_checkout() {
		echo '<a href="' . esc_url( wc_get_checkout_url() ) . '" class="button checkout wc-forward">' . esc_html__( 'Checkout', 'woocommerce' ) . '</a>';
	}
}

if ( ! function_exists( 'woocommerce_widget_shopping_cart_subtotal' ) ) {
	/**
	 * Output to view cart subtotal.
	 *
	 * @since 3.7.0
	 */
	function woocommerce_widget_shopping_cart_subtotal() {
		echo '<strong>' . esc_html__( 'Subtotal:', 'woocommerce' ) . '</strong> ' . WC()->cart->get_cart_subtotal(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
	}
}

/** Mini-Cart */

if ( ! function_exists( 'woocommerce_mini_cart' ) ) {

	/**
	 * Output the Mini-cart - used by cart widget.
	 *
	 * @param array $args Arguments.
	 */
	function woocommerce_mini_cart( $args = array() ) {

		$defaults = array(
			'list_class' => '',
		);

		$args = wp_parse_args( $args, $defaults );

		wc_get_template( 'cart/mini-cart.php', $args );
	}
}

/** Login */

if ( ! function_exists( 'woocommerce_login_form' ) ) {

	/**
	 * Output the WooCommerce Login Form.
	 *
	 * @param array $args Arguments.
	 */
	function woocommerce_login_form( $args = array() ) {

		$defaults = array(
			'message'  => '',
			'redirect' => '',
			'hidden'   => false,
		);

		$args = wp_parse_args( $args, $defaults );

		wc_get_template( 'global/form-login.php', $args );
	}
}

if ( ! function_exists( 'woocommerce_checkout_login_form' ) ) {

	/**
	 * Output the WooCommerce Checkout Login Form.
	 */
	function woocommerce_checkout_login_form() {
		wc_get_template(
			'checkout/form-login.php',
			array(
				'checkout' => WC()->checkout(),
			)
		);
	}
}

if ( ! function_exists( 'woocommerce_breadcrumb' ) ) {

	/**
	 * Output the WooCommerce Breadcrumb.
	 *
	 * @param array $args Arguments.
	 */
	function woocommerce_breadcrumb( $args = array() ) {
		$args = wp_parse_args(
			$args,
			apply_filters(
				'woocommerce_breadcrumb_defaults',
				array(
					'delimiter'   => '&nbsp;&#47;&nbsp;',
					'wrap_before' => '<nav class="woocommerce-breadcrumb">',
					'wrap_after'  => '</nav>',
					'before'      => '',
					'after'       => '',
					'home'        => _x( 'Home', 'breadcrumb', 'woocommerce' ),
				)
			)
		);

		$breadcrumbs = new WC_Breadcrumb();

		if ( ! empty( $args['home'] ) ) {
			$breadcrumbs->add_crumb( $args['home'], apply_filters( 'woocommerce_breadcrumb_home_url', home_url() ) );
		}

		$args['breadcrumb'] = $breadcrumbs->generate();

		/**
		 * WooCommerce Breadcrumb hook
		 *
		 * @hooked WC_Structured_Data::generate_breadcrumblist_data() - 10
		 */
		do_action( 'woocommerce_breadcrumb', $breadcrumbs, $args );

		wc_get_template( 'global/breadcrumb.php', $args );
	}
}

if ( ! function_exists( 'woocommerce_order_review' ) ) {

	/**
	 * Output the Order review table for the checkout.
	 *
	 * @param bool $deprecated Deprecated param.
	 */
	function woocommerce_order_review( $deprecated = false ) {
		wc_get_template(
			'checkout/review-order.php',
			array(
				'checkout' => WC()->checkout(),
			)
		);
	}
}

if ( ! function_exists( 'woocommerce_checkout_payment' ) ) {

	/**
	 * Output the Payment Methods on the checkout.
	 */
	function woocommerce_checkout_payment() {
		if ( WC()->cart->needs_payment() ) {
			$available_gateways = WC()->payment_gateways()->get_available_payment_gateways();
			WC()->payment_gateways()->set_current_gateway( $available_gateways );
		} else {
			$available_gateways = array();
		}

		wc_get_template(
			'checkout/payment.php',
			array(
				'checkout'           => WC()->checkout(),
				'available_gateways' => $available_gateways,
				'order_button_text'  => apply_filters( 'woocommerce_order_button_text', __( 'Place order', 'woocommerce' ) ),
			)
		);
	}
}

if ( ! function_exists( 'woocommerce_checkout_coupon_form' ) ) {

	/**
	 * Output the Coupon form for the checkout.
	 */
	function woocommerce_checkout_coupon_form() {
		if ( is_user_logged_in() || WC()->checkout()->is_registration_enabled() || ! WC()->checkout()->is_registration_required() ) {
			wc_get_template(
				'checkout/form-coupon.php',
				array(
					'checkout' => WC()->checkout(),
				)
			);
		}
	}
}

if ( ! function_exists( 'woocommerce_products_will_display' ) ) {

	/**
	 * Check if we will be showing products or not (and not sub-categories only).
	 *
	 * @return bool
	 */
	function woocommerce_products_will_display() {
		$display_type = woocommerce_get_loop_display_mode();

		return 0 < wc_get_loop_prop( 'total', 0 ) && 'subcategories' !== $display_type;
	}
}

if ( ! function_exists( 'woocommerce_get_loop_display_mode' ) ) {

	/**
	 * See what is going to display in the loop.
	 *
	 * @since 3.3.0
	 * @return string Either products, subcategories, or both, based on current page.
	 */
	function woocommerce_get_loop_display_mode() {
		// Only return products when filtering things.
		if ( wc_get_loop_prop( 'is_search' ) || wc_get_loop_prop( 'is_filtered' ) ) {
			return 'products';
		}

		$parent_id    = 0;
		$display_type = '';

		if ( is_shop() ) {
			$display_type = get_option( 'woocommerce_shop_page_display', '' );
		} elseif ( is_product_category() ) {
			$parent_id    = get_queried_object_id();
			$display_type = get_term_meta( $parent_id, 'display_type', true );
			$display_type = '' === $display_type ? get_option( 'woocommerce_category_archive_display', '' ) : $display_type;
		}

		if ( ( ! is_shop() || 'subcategories' !== $display_type ) && 1 < wc_get_loop_prop( 'current_page' ) ) {
			return 'products';
		}

		// Ensure valid value.
		if ( '' === $display_type || ! in_array( $display_type, array( 'products', 'subcategories', 'both' ), true ) ) {
			$display_type = 'products';
		}

		// If we're showing categories, ensure we actually have something to show.
		if ( in_array( $display_type, array( 'subcategories', 'both' ), true ) ) {
			$subcategories = woocommerce_get_product_subcategories( $parent_id );

			if ( empty( $subcategories ) ) {
				$display_type = 'products';
			}
		}

		return $display_type;
	}
}

if ( ! function_exists( 'woocommerce_maybe_show_product_subcategories' ) ) {

	/**
	 * Maybe display categories before, or instead of, a product loop.
	 *
	 * @since 3.3.0
	 * @param string $loop_html HTML.
	 * @return string
	 */
	function woocommerce_maybe_show_product_subcategories( $loop_html = '' ) {
		if ( wc_get_loop_prop( 'is_shortcode' ) && ! WC_Template_Loader::in_content_filter() ) {
			return $loop_html;
		}

		$display_type = woocommerce_get_loop_display_mode();

		// If displaying categories, append to the loop.
		if ( 'subcategories' === $display_type || 'both' === $display_type ) {
			ob_start();
			woocommerce_output_product_categories(
				array(
					'parent_id' => is_product_category() ? get_queried_object_id() : 0,
				)
			);
			$loop_html .= ob_get_clean();

			if ( 'subcategories' === $display_type ) {
				wc_set_loop_prop( 'total', 0 );

				// This removes pagination and products from display for themes not using wc_get_loop_prop in their product loops.  @todo Remove in future major version.
				global $wp_query;

				if ( $wp_query->is_main_query() ) {
					$wp_query->post_count    = 0;
					$wp_query->max_num_pages = 0;
				}
			}
		}

		return $loop_html;
	}
}

if ( ! function_exists( 'woocommerce_product_subcategories' ) ) {
	/**
	 * This is a legacy function which used to check if we needed to display subcats and then output them. It was called by templates.
	 *
	 * From 3.3 onwards this is all handled via hooks and the woocommerce_maybe_show_product_subcategories function.
	 *
	 * Since some templates have not updated compatibility, to avoid showing incorrect categories this function has been deprecated and will
	 * return nothing. Replace usage with woocommerce_output_product_categories to render the category list manually.
	 *
	 * This is a legacy function which also checks if things should display.
	 * Themes no longer need to call these functions. It's all done via hooks.
	 *
	 * @deprecated 3.3.1 @todo Add a notice in a future version.
	 * @param array $args Arguments.
	 * @return null|boolean
	 */
	function woocommerce_product_subcategories( $args = array() ) {
		$defaults = array(
			'before'        => '',
			'after'         => '',
			'force_display' => false,
		);

		$args = wp_parse_args( $args, $defaults );

		if ( $args['force_display'] ) {
			// We can still render if display is forced.
			woocommerce_output_product_categories(
				array(
					'before'    => $args['before'],
					'after'     => $args['after'],
					'parent_id' => is_product_category() ? get_queried_object_id() : 0,
				)
			);
			return true;
		} else {
			// Output nothing. woocommerce_maybe_show_product_subcategories will handle the output of cats.
			$display_type = woocommerce_get_loop_display_mode();

			if ( 'subcategories' === $display_type ) {
				// This removes pagination and products from display for themes not using wc_get_loop_prop in their product loops. @todo Remove in future major version.
				global $wp_query;

				if ( $wp_query->is_main_query() ) {
					$wp_query->post_count    = 0;
					$wp_query->max_num_pages = 0;
				}
			}

			return 'subcategories' === $display_type || 'both' === $display_type;
		}
	}
}

if ( ! function_exists( 'woocommerce_output_product_categories' ) ) {
	/**
	 * Display product sub categories as thumbnails.
	 *
	 * This is a replacement for woocommerce_product_subcategories which also does some logic
	 * based on the loop. This function however just outputs when called.
	 *
	 * @since 3.3.1
	 * @param array $args Arguments.
	 * @return boolean
	 */
	function woocommerce_output_product_categories( $args = array() ) {
		$args = wp_parse_args(
			$args,
			array(
				'before'    => apply_filters( 'woocommerce_before_output_product_categories', '' ),
				'after'     => apply_filters( 'woocommerce_after_output_product_categories', '' ),
				'parent_id' => 0,
			)
		);

		$product_categories = woocommerce_get_product_subcategories( $args['parent_id'] );

		if ( ! $product_categories ) {
			return false;
		}

		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		echo $args['before'];

		foreach ( $product_categories as $category ) {
			wc_get_template(
				'content-product_cat.php',
				array(
					'category' => $category,
				)
			);
		}

		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		echo $args['after'];

		return true;
	}
}

if ( ! function_exists( 'woocommerce_get_product_subcategories' ) ) {
	/**
	 * Get (and cache) product subcategories.
	 *
	 * @param int $parent_id Get subcategories of this ID.
	 * @return array
	 */
	function woocommerce_get_product_subcategories( $parent_id = 0 ) {
		$parent_id          = absint( $parent_id );
		$cache_key          = apply_filters( 'woocommerce_get_product_subcategories_cache_key', 'product-category-hierarchy-' . $parent_id, $parent_id );
		$product_categories = $cache_key ? wp_cache_get( $cache_key, 'product_cat' ) : false;

		if ( false === $product_categories ) {
			// NOTE: using child_of instead of parent - this is not ideal but due to a WP bug ( https://core.trac.wordpress.org/ticket/15626 ) pad_counts won't work.
			$product_categories = get_categories(
				apply_filters(
					'woocommerce_product_subcategories_args',
					array(
						'parent'       => $parent_id,
						'hide_empty'   => 0,
						'hierarchical' => 1,
						'taxonomy'     => 'product_cat',
						'pad_counts'   => 1,
					)
				)
			);

			if ( $cache_key ) {
				wp_cache_set( $cache_key, $product_categories, 'product_cat' );
			}
		}

		if ( apply_filters( 'woocommerce_product_subcategories_hide_empty', true ) ) {
			$product_categories = wp_list_filter( $product_categories, array( 'count' => 0 ), 'NOT' );
		}

		return $product_categories;
	}
}

if ( ! function_exists( 'woocommerce_subcategory_thumbnail' ) ) {

	/**
	 * Show subcategory thumbnails.
	 *
	 * @param mixed $category Category.
	 */
	function woocommerce_subcategory_thumbnail( $category ) {
		$small_thumbnail_size = apply_filters( 'subcategory_archive_thumbnail_size', 'woocommerce_thumbnail' );
		$dimensions           = wc_get_image_size( $small_thumbnail_size );
		$thumbnail_id         = get_term_meta( $category->term_id, 'thumbnail_id', true );

		if ( $thumbnail_id ) {
			$image        = wp_get_attachment_image_src( $thumbnail_id, $small_thumbnail_size );
			$image        = $image[0];
			$image_srcset = function_exists( 'wp_get_attachment_image_srcset' ) ? wp_get_attachment_image_srcset( $thumbnail_id, $small_thumbnail_size ) : false;
			$image_sizes  = function_exists( 'wp_get_attachment_image_sizes' ) ? wp_get_attachment_image_sizes( $thumbnail_id, $small_thumbnail_size ) : false;
		} else {
			$image        = wc_placeholder_img_src();
			$image_srcset = false;
			$image_sizes  = false;
		}

		if ( $image ) {
			// Prevent esc_url from breaking spaces in urls for image embeds.
			// Ref: https://core.trac.wordpress.org/ticket/23605.
			$image = str_replace( ' ', '%20', $image );

			// Add responsive image markup if available.
			if ( $image_srcset && $image_sizes ) {
				echo '<img src="' . esc_url( $image ) . '" alt="' . esc_attr( $category->name ) . '" width="' . esc_attr( $dimensions['width'] ) . '" height="' . esc_attr( $dimensions['height'] ) . '" srcset="' . esc_attr( $image_srcset ) . '" sizes="' . esc_attr( $image_sizes ) . '" />';
			} else {
				echo '<img src="' . esc_url( $image ) . '" alt="' . esc_attr( $category->name ) . '" width="' . esc_attr( $dimensions['width'] ) . '" height="' . esc_attr( $dimensions['height'] ) . '" />';
			}
		}
	}
}

if ( ! function_exists( 'woocommerce_order_details_table' ) ) {

	/**
	 * Displays order details in a table.
	 *
	 * @param mixed $order_id Order ID.
	 */
	function woocommerce_order_details_table( $order_id ) {
		if ( ! $order_id ) {
			return;
		}

		wc_get_template(
			'order/order-details.php',
			array(
				'order_id' => $order_id,
			)
		);
	}
}

if ( ! function_exists( 'woocommerce_order_downloads_table' ) ) {

	/**
	 * Displays order downloads in a table.
	 *
	 * @since 3.2.0
	 * @param array $downloads Downloads.
	 */
	function woocommerce_order_downloads_table( $downloads ) {
		if ( ! $downloads ) {
			return;
		}
		wc_get_template(
			'order/order-downloads.php',
			array(
				'downloads' => $downloads,
			)
		);
	}
}

if ( ! function_exists( 'woocommerce_order_again_button' ) ) {

	/**
	 * Display an 'order again' button on the view order page.
	 *
	 * @param object $order Order.
	 */
	function woocommerce_order_again_button( $order ) {
		if ( ! $order || ! $order->has_status( apply_filters( 'woocommerce_valid_order_statuses_for_order_again', array( 'completed' ) ) ) || ! is_user_logged_in() ) {
			return;
		}

		wc_get_template(
			'order/order-again.php',
			array(
				'order'           => $order,
				'order_again_url' => wp_nonce_url( add_query_arg( 'order_again', $order->get_id(), wc_get_cart_url() ), 'woocommerce-order_again' ),
			)
		);
	}
}

/** Forms */

if ( ! function_exists( 'woocommerce_form_field' ) ) {

	/**
	 * Outputs a checkout/address form field.
	 *
	 * @param string $key Key.
	 * @param mixed  $args Arguments.
	 * @param string $value (default: null).
	 * @return string
	 */
	function woocommerce_form_field( $key, $args, $value = null ) {
		$defaults = array(
			'type'              => 'text',
			'label'             => '',
			'description'       => '',
			'placeholder'       => '',
			'maxlength'         => false,
			'required'          => false,
			'autocomplete'      => false,
			'id'                => $key,
			'class'             => array(),
			'label_class'       => array(),
			'input_class'       => array(),
			'return'            => false,
			'options'           => array(),
			'custom_attributes' => array(),
			'validate'          => array(),
			'default'           => '',
			'autofocus'         => '',
			'priority'          => '',
		);

		$args = wp_parse_args( $args, $defaults );
		$args = apply_filters( 'woocommerce_form_field_args', $args, $key, $value );

		if ( $args['required'] ) {
			$args['class'][] = 'validate-required';
			$required        = '&nbsp;<abbr class="required" title="' . esc_attr__( 'required', 'woocommerce' ) . '">*</abbr>';
		} else {
			$required = '&nbsp;<span class="optional">(' . esc_html__( 'optional', 'woocommerce' ) . ')</span>';
		}

		if ( is_string( $args['label_class'] ) ) {
			$args['label_class'] = array( $args['label_class'] );
		}

		if ( is_null( $value ) ) {
			$value = $args['default'];
		}

		// Custom attribute handling.
		$custom_attributes         = array();
		$args['custom_attributes'] = array_filter( (array) $args['custom_attributes'], 'strlen' );

		if ( $args['maxlength'] ) {
			$args['custom_attributes']['maxlength'] = absint( $args['maxlength'] );
		}

		if ( ! empty( $args['autocomplete'] ) ) {
			$args['custom_attributes']['autocomplete'] = $args['autocomplete'];
		}

		if ( true === $args['autofocus'] ) {
			$args['custom_attributes']['autofocus'] = 'autofocus';
		}

		if ( $args['description'] ) {
			$args['custom_attributes']['aria-describedby'] = $args['id'] . '-description';
		}

		if ( ! empty( $args['custom_attributes'] ) && is_array( $args['custom_attributes'] ) ) {
			foreach ( $args['custom_attributes'] as $attribute => $attribute_value ) {
				$custom_attributes[] = esc_attr( $attribute ) . '="' . esc_attr( $attribute_value ) . '"';
			}
		}

		if ( ! empty( $args['validate'] ) ) {
			foreach ( $args['validate'] as $validate ) {
				$args['class'][] = 'validate-' . $validate;
			}
		}

		$field           = '';
		$label_id        = $args['id'];
		$sort            = $args['priority'] ? $args['priority'] : '';
		$field_container = '<p class="form-row %1$s" id="%2$s" data-priority="' . esc_attr( $sort ) . '">%3$s</p>';

		switch ( $args['type'] ) {
			case 'country':
				$countries = 'shipping_country' === $key ? WC()->countries->get_shipping_countries() : WC()->countries->get_allowed_countries();

				if ( 1 === count( $countries ) ) {

					$field .= '<strong>' . current( array_values( $countries ) ) . '</strong>';

					$field .= '<input type="hidden" name="' . esc_attr( $key ) . '" id="' . esc_attr( $args['id'] ) . '" value="' . current( array_keys( $countries ) ) . '" ' . implode( ' ', $custom_attributes ) . ' class="country_to_state" readonly="readonly" />';

				} else {
					$data_label = ! empty( $args['label'] ) ? 'data-label="' . esc_attr( $args['label'] ) . '"' : '';

					$field = '<select name="' . esc_attr( $key ) . '" id="' . esc_attr( $args['id'] ) . '" class="country_to_state country_select ' . esc_attr( implode( ' ', $args['input_class'] ) ) . '" ' . implode( ' ', $custom_attributes ) . ' data-placeholder="' . esc_attr( $args['placeholder'] ? $args['placeholder'] : esc_attr__( 'Select a country / region&hellip;', 'woocommerce' ) ) . '" ' . $data_label . '><option value="">' . esc_html__( 'Select a country / region&hellip;', 'woocommerce' ) . '</option>';

					foreach ( $countries as $ckey => $cvalue ) {
						$field .= '<option value="' . esc_attr( $ckey ) . '" ' . selected( $value, $ckey, false ) . '>' . esc_html( $cvalue ) . '</option>';
					}

					$field .= '</select>';

					$field .= '<noscript><button type="submit" name="woocommerce_checkout_update_totals" value="' . esc_attr__( 'Update country / region', 'woocommerce' ) . '">' . esc_html__( 'Update country / region', 'woocommerce' ) . '</button></noscript>';

				}

				break;
			case 'state':
				/* Get country this state field is representing */
				$for_country = isset( $args['country'] ) ? $args['country'] : WC()->checkout->get_value( 'billing_state' === $key ? 'billing_country' : 'shipping_country' );
				$states      = WC()->countries->get_states( $for_country );

				if ( is_array( $states ) && empty( $states ) ) {

					$field_container = '<p class="form-row %1$s" id="%2$s" style="display: none">%3$s</p>';

					$field .= '<input type="hidden" class="hidden" name="' . esc_attr( $key ) . '" id="' . esc_attr( $args['id'] ) . '" value="" ' . implode( ' ', $custom_attributes ) . ' placeholder="' . esc_attr( $args['placeholder'] ) . '" readonly="readonly" data-input-classes="' . esc_attr( implode( ' ', $args['input_class'] ) ) . '"/>';

				} elseif ( ! is_null( $for_country ) && is_array( $states ) ) {
					$data_label = ! empty( $args['label'] ) ? 'data-label="' . esc_attr( $args['label'] ) . '"' : '';

					$field .= '<select name="' . esc_attr( $key ) . '" id="' . esc_attr( $args['id'] ) . '" class="state_select ' . esc_attr( implode( ' ', $args['input_class'] ) ) . '" ' . implode( ' ', $custom_attributes ) . ' data-placeholder="' . esc_attr( $args['placeholder'] ? $args['placeholder'] : esc_html__( 'Select an option&hellip;', 'woocommerce' ) ) . '"  data-input-classes="' . esc_attr( implode( ' ', $args['input_class'] ) ) . '" ' . $data_label . '>
						<option value="">' . esc_html__( 'Select an option&hellip;', 'woocommerce' ) . '</option>';

					foreach ( $states as $ckey => $cvalue ) {
						$field .= '<option value="' . esc_attr( $ckey ) . '" ' . selected( $value, $ckey, false ) . '>' . esc_html( $cvalue ) . '</option>';
					}

					$field .= '</select>';

				} else {

					$field .= '<input type="text" class="input-text ' . esc_attr( implode( ' ', $args['input_class'] ) ) . '" value="' . esc_attr( $value ) . '"  placeholder="' . esc_attr( $args['placeholder'] ) . '" name="' . esc_attr( $key ) . '" id="' . esc_attr( $args['id'] ) . '" ' . implode( ' ', $custom_attributes ) . ' data-input-classes="' . esc_attr( implode( ' ', $args['input_class'] ) ) . '"/>';

				}

				break;
			case 'textarea':
				$field .= '<textarea name="' . esc_attr( $key ) . '" class="input-text ' . esc_attr( implode( ' ', $args['input_class'] ) ) . '" id="' . esc_attr( $args['id'] ) . '" placeholder="' . esc_attr( $args['placeholder'] ) . '" ' . ( empty( $args['custom_attributes']['rows'] ) ? ' rows="2"' : '' ) . ( empty( $args['custom_attributes']['cols'] ) ? ' cols="5"' : '' ) . implode( ' ', $custom_attributes ) . '>' . esc_textarea( $value ) . '</textarea>';

				break;
			case 'checkbox':
				$field = '<label class="checkbox ' . implode( ' ', $args['label_class'] ) . '" ' . implode( ' ', $custom_attributes ) . '>
						<input type="' . esc_attr( $args['type'] ) . '" class="input-checkbox ' . esc_attr( implode( ' ', $args['input_class'] ) ) . '" name="' . esc_attr( $key ) . '" id="' . esc_attr( $args['id'] ) . '" value="1" ' . checked( $value, 1, false ) . ' /> ' . $args['label'] . $required . '</label>';

				break;
			case 'text':
			case 'password':
			case 'datetime':
			case 'datetime-local':
			case 'date':
			case 'month':
			case 'time':
			case 'week':
			case 'number':
			case 'email':
			case 'url':
			case 'tel':
				$field .= '<input type="' . esc_attr( $args['type'] ) . '" class="input-text ' . esc_attr( implode( ' ', $args['input_class'] ) ) . '" name="' . esc_attr( $key ) . '" id="' . esc_attr( $args['id'] ) . '" placeholder="' . esc_attr( $args['placeholder'] ) . '"  value="' . esc_attr( $value ) . '" ' . implode( ' ', $custom_attributes ) . ' />';

				break;
			case 'hidden':
				$field .= '<input type="' . esc_attr( $args['type'] ) . '" class="input-hidden ' . esc_attr( implode( ' ', $args['input_class'] ) ) . '" name="' . esc_attr( $key ) . '" id="' . esc_attr( $args['id'] ) . '" value="' . esc_attr( $value ) . '" ' . implode( ' ', $custom_attributes ) . ' />';

				break;
			case 'select':
				$field   = '';
				$options = '';

				if ( ! empty( $args['options'] ) ) {
					foreach ( $args['options'] as $option_key => $option_text ) {
						if ( '' === $option_key ) {
							// If we have a blank option, select2 needs a placeholder.
							if ( empty( $args['placeholder'] ) ) {
								$args['placeholder'] = $option_text ? $option_text : __( 'Choose an option', 'woocommerce' );
							}
							$custom_attributes[] = 'data-allow_clear="true"';
						}
						$options .= '<option value="' . esc_attr( $option_key ) . '" ' . selected( $value, $option_key, false ) . '>' . esc_html( $option_text ) . '</option>';
					}

					$field .= '<select name="' . esc_attr( $key ) . '" id="' . esc_attr( $args['id'] ) . '" class="select ' . esc_attr( implode( ' ', $args['input_class'] ) ) . '" ' . implode( ' ', $custom_attributes ) . ' data-placeholder="' . esc_attr( $args['placeholder'] ) . '">
							' . $options . '
						</select>';
				}

				break;
			case 'radio':
				$label_id .= '_' . current( array_keys( $args['options'] ) );

				if ( ! empty( $args['options'] ) ) {
					foreach ( $args['options'] as $option_key => $option_text ) {
						$field .= '<input type="radio" class="input-radio ' . esc_attr( implode( ' ', $args['input_class'] ) ) . '" value="' . esc_attr( $option_key ) . '" name="' . esc_attr( $key ) . '" ' . implode( ' ', $custom_attributes ) . ' id="' . esc_attr( $args['id'] ) . '_' . esc_attr( $option_key ) . '"' . checked( $value, $option_key, false ) . ' />';
						$field .= '<label for="' . esc_attr( $args['id'] ) . '_' . esc_attr( $option_key ) . '" class="radio ' . implode( ' ', $args['label_class'] ) . '">' . esc_html( $option_text ) . '</label>';
					}
				}

				break;
		}

		if ( ! empty( $field ) ) {
			$field_html = '';

			if ( $args['label'] && 'checkbox' !== $args['type'] ) {
				$field_html .= '<label for="' . esc_attr( $label_id ) . '" class="' . esc_attr( implode( ' ', $args['label_class'] ) ) . '">' . wp_kses_post( $args['label'] ) . $required . '</label>';
			}

			$field_html .= '<span class="woocommerce-input-wrapper">' . $field;

			if ( $args['description'] ) {
				$field_html .= '<span class="description" id="' . esc_attr( $args['id'] ) . '-description" aria-hidden="true">' . wp_kses_post( $args['description'] ) . '</span>';
			}

			$field_html .= '</span>';

			$container_class = esc_attr( implode( ' ', $args['class'] ) );
			$container_id    = esc_attr( $args['id'] ) . '_field';
			$field           = sprintf( $field_container, $container_class, $container_id, $field_html );
		}

		/**
		 * Filter by type.
		 */
		$field = apply_filters( 'woocommerce_form_field_' . $args['type'], $field, $key, $args, $value );

		/**
		 * General filter on form fields.
		 *
		 * @since 3.4.0
		 */
		$field = apply_filters( 'woocommerce_form_field', $field, $key, $args, $value );

		if ( $args['return'] ) {
			return $field;
		} else {
			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			echo $field;
		}
	}
}

if ( ! function_exists( 'get_product_search_form' ) ) {

	/**
	 * Display product search form.
	 *
	 * Will first attempt to locate the product-searchform.php file in either the child or.
	 * the parent, then load it. If it doesn't exist, then the default search form.
	 * will be displayed.
	 *
	 * The default searchform uses html5.
	 *
	 * @param bool $echo (default: true).
	 * @return string
	 */
	function get_product_search_form( $echo = true ) {
		global $product_search_form_index;

		ob_start();

		if ( empty( $product_search_form_index ) ) {
			$product_search_form_index = 0;
		}

		do_action( 'pre_get_product_search_form' );

		wc_get_template(
			'product-searchform.php',
			array(
				'index' => $product_search_form_index++,
			)
		);

		$form = apply_filters( 'get_product_search_form', ob_get_clean() );

		if ( ! $echo ) {
			return $form;
		}

		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		echo $form;
	}
}

if ( ! function_exists( 'woocommerce_output_auth_header' ) ) {

	/**
	 * Output the Auth header.
	 */
	function woocommerce_output_auth_header() {
		wc_get_template( 'auth/header.php' );
	}
}

if ( ! function_exists( 'woocommerce_output_auth_footer' ) ) {

	/**
	 * Output the Auth footer.
	 */
	function woocommerce_output_auth_footer() {
		wc_get_template( 'auth/footer.php' );
	}
}

if ( ! function_exists( 'woocommerce_single_variation' ) ) {

	/**
	 * Output placeholders for the single variation.
	 */
	function woocommerce_single_variation() {
		echo '<div class="woocommerce-variation single_variation"></div>';
	}
}

if ( ! function_exists( 'woocommerce_single_variation_add_to_cart_button' ) ) {

	/**
	 * Output the add to cart button for variations.
	 */
	function woocommerce_single_variation_add_to_cart_button() {
		wc_get_template( 'single-product/add-to-cart/variation-add-to-cart-button.php' );
	}
}

if ( ! function_exists( 'wc_dropdown_variation_attribute_options' ) ) {

	/**
	 * Output a list of variation attributes for use in the cart forms.
	 *
	 * @param array $args Arguments.
	 * @since 2.4.0
	 */
	function wc_dropdown_variation_attribute_options( $args = array() ) {
		$args = wp_parse_args(
			apply_filters( 'woocommerce_dropdown_variation_attribute_options_args', $args ),
			array(
				'options'          => false,
				'attribute'        => false,
				'product'          => false,
				'selected'         => false,
				'name'             => '',
				'id'               => '',
				'class'            => '',
				'show_option_none' => __( 'Choose an option', 'woocommerce' ),
			)
		);

		// Get selected value.
		if ( false === $args['selected'] && $args['attribute'] && $args['product'] instanceof WC_Product ) {
			$selected_key = 'attribute_' . sanitize_title( $args['attribute'] );
			// phpcs:disable WordPress.Security.NonceVerification.Recommended
			$args['selected'] = isset( $_REQUEST[ $selected_key ] ) ? wc_clean( wp_unslash( $_REQUEST[ $selected_key ] ) ) : $args['product']->get_variation_default_attribute( $args['attribute'] );
			// phpcs:enable WordPress.Security.NonceVerification.Recommended
		}

		$options               = $args['options'];
		$product               = $args['product'];
		$attribute             = $args['attribute'];
		$name                  = $args['name'] ? $args['name'] : 'attribute_' . sanitize_title( $attribute );
		$id                    = $args['id'] ? $args['id'] : sanitize_title( $attribute );
		$class                 = $args['class'];
		$show_option_none      = (bool) $args['show_option_none'];
		$show_option_none_text = $args['show_option_none'] ? $args['show_option_none'] : __( 'Choose an option', 'woocommerce' ); // We'll do our best to hide the placeholder, but we'll need to show something when resetting options.

		if ( empty( $options ) && ! empty( $product ) && ! empty( $attribute ) ) {
			$attributes = $product->get_variation_attributes();
			$options    = $attributes[ $attribute ];
		}

		$html  = '<select id="' . esc_attr( $id ) . '" class="' . esc_attr( $class ) . '" name="' . esc_attr( $name ) . '" data-attribute_name="attribute_' . esc_attr( sanitize_title( $attribute ) ) . '" data-show_option_none="' . ( $show_option_none ? 'yes' : 'no' ) . '">';
		$html .= '<option value="">' . esc_html( $show_option_none_text ) . '</option>';

		if ( ! empty( $options ) ) {
			if ( $product && taxonomy_exists( $attribute ) ) {
				// Get terms if this is a taxonomy - ordered. We need the names too.
				$terms = wc_get_product_terms(
					$product->get_id(),
					$attribute,
					array(
						'fields' => 'all',
					)
				);

				foreach ( $terms as $term ) {
					if ( in_array( $term->slug, $options, true ) ) {
						$html .= '<option value="' . esc_attr( $term->slug ) . '" ' . selected( sanitize_title( $args['selected'] ), $term->slug, false ) . '>' . esc_html( apply_filters( 'woocommerce_variation_option_name', $term->name, $term, $attribute, $product ) ) . '</option>';
					}
				}
			} else {
				foreach ( $options as $option ) {
					// This handles < 2.4.0 bw compatibility where text attributes were not sanitized.
					$selected = sanitize_title( $args['selected'] ) === $args['selected'] ? selected( $args['selected'], sanitize_title( $option ), false ) : selected( $args['selected'], $option, false );
					$html    .= '<option value="' . esc_attr( $option ) . '" ' . $selected . '>' . esc_html( apply_filters( 'woocommerce_variation_option_name', $option, null, $attribute, $product ) ) . '</option>';
				}
			}
		}

		$html .= '</select>';

		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		echo apply_filters( 'woocommerce_dropdown_variation_attribute_options_html', $html, $args );
	}
}

if ( ! function_exists( 'woocommerce_account_content' ) ) {

	/**
	 * My Account content output.
	 */
	function woocommerce_account_content() {
		global $wp;

		if ( ! empty( $wp->query_vars ) ) {
			foreach ( $wp->query_vars as $key => $value ) {
				// Ignore pagename param.
				if ( 'pagename' === $key ) {
					continue;
				}

				if ( has_action( 'woocommerce_account_' . $key . '_endpoint' ) ) {
					do_action( 'woocommerce_account_' . $key . '_endpoint', $value );
					return;
				}
			}
		}

		// No endpoint found? Default to dashboard.
		wc_get_template(
			'myaccount/dashboard.php',
			array(
				'current_user' => get_user_by( 'id', get_current_user_id() ),
			)
		);
	}
}

if ( ! function_exists( 'woocommerce_account_navigation' ) ) {

	/**
	 * My Account navigation template.
	 */
	function woocommerce_account_navigation() {
		wc_get_template( 'myaccount/navigation.php' );
	}
}

if ( ! function_exists( 'woocommerce_account_orders' ) ) {

	/**
	 * My Account > Orders template.
	 *
	 * @param int $current_page Current page number.
	 */
	function woocommerce_account_orders( $current_page ) {
		$current_page    = empty( $current_page ) ? 1 : absint( $current_page );
		$customer_orders = wc_get_orders(
			apply_filters(
				'woocommerce_my_account_my_orders_query',
				array(
					'customer' => get_current_user_id(),
					'page'     => $current_page,
					'paginate' => true,
				)
			)
		);

		wc_get_template(
			'myaccount/orders.php',
			array(
				'current_page'    => absint( $current_page ),
				'customer_orders' => $customer_orders,
				'has_orders'      => 0 < $customer_orders->total,
			)
		);
	}
}

if ( ! function_exists( 'woocommerce_account_view_order' ) ) {

	/**
	 * My Account > View order template.
	 *
	 * @param int $order_id Order ID.
	 */
	function woocommerce_account_view_order( $order_id ) {
		WC_Shortcode_My_Account::view_order( absint( $order_id ) );
	}
}

if ( ! function_exists( 'woocommerce_account_downloads' ) ) {

	/**
	 * My Account > Downloads template.
	 */
	function woocommerce_account_downloads() {
		wc_get_template( 'myaccount/downloads.php' );
	}
}

if ( ! function_exists( 'woocommerce_account_edit_address' ) ) {

	/**
	 * My Account > Edit address template.
	 *
	 * @param string $type Address type.
	 */
	function woocommerce_account_edit_address( $type ) {
		$type = wc_edit_address_i18n( sanitize_title( $type ), true );

		WC_Shortcode_My_Account::edit_address( $type );
	}
}

if ( ! function_exists( 'woocommerce_account_payment_methods' ) ) {

	/**
	 * My Account > Downloads template.
	 */
	function woocommerce_account_payment_methods() {
		wc_get_template( 'myaccount/payment-methods.php' );
	}
}

if ( ! function_exists( 'woocommerce_account_add_payment_method' ) ) {

	/**
	 * My Account > Add payment method template.
	 */
	function woocommerce_account_add_payment_method() {
		WC_Shortcode_My_Account::add_payment_method();
	}
}

if ( ! function_exists( 'woocommerce_account_edit_account' ) ) {

	/**
	 * My Account > Edit account template.
	 */
	function woocommerce_account_edit_account() {
		WC_Shortcode_My_Account::edit_account();
	}
}

if ( ! function_exists( 'wc_no_products_found' ) ) {

	/**
	 * Handles the loop when no products were found/no product exist.
	 */
	function wc_no_products_found() {
		wc_get_template( 'loop/no-products-found.php' );
	}
}


if ( ! function_exists( 'wc_get_email_order_items' ) ) {
	/**
	 * Get HTML for the order items to be shown in emails.
	 *
	 * @param WC_Order $order Order object.
	 * @param array    $args Arguments.
	 *
	 * @since 3.0.0
	 * @return string
	 */
	function wc_get_email_order_items( $order, $args = array() ) {
		ob_start();

		$defaults = array(
			'show_sku'      => false,
			'show_image'    => false,
			'image_size'    => array( 32, 32 ),
			'plain_text'    => false,
			'sent_to_admin' => false,
		);

		$args     = wp_parse_args( $args, $defaults );
		$template = $args['plain_text'] ? 'emails/plain/email-order-items.php' : 'emails/email-order-items.php';

		wc_get_template(
			$template,
			apply_filters(
				'woocommerce_email_order_items_args',
				array(
					'order'               => $order,
					'items'               => $order->get_items(),
					'show_download_links' => $order->is_download_permitted() && ! $args['sent_to_admin'],
					'show_sku'            => $args['show_sku'],
					'show_purchase_note'  => $order->is_paid() && ! $args['sent_to_admin'],
					'show_image'          => $args['show_image'],
					'image_size'          => $args['image_size'],
					'plain_text'          => $args['plain_text'],
					'sent_to_admin'       => $args['sent_to_admin'],
				)
			)
		);

		return apply_filters( 'woocommerce_email_order_items_table', ob_get_clean(), $order );
	}
}

if ( ! function_exists( 'wc_display_item_meta' ) ) {
	/**
	 * Display item meta data.
	 *
	 * @since  3.0.0
	 * @param  WC_Order_Item $item Order Item.
	 * @param  array         $args Arguments.
	 * @return string|void
	 */
	function wc_display_item_meta( $item, $args = array() ) {
		$strings = array();
		$html    = '';
		$args    = wp_parse_args(
			$args,
			array(
				'before'       => '<ul class="wc-item-meta"><li>',
				'after'        => '</li></ul>',
				'separator'    => '</li><li>',
				'echo'         => true,
				'autop'        => false,
				'label_before' => '<strong class="wc-item-meta-label">',
				'label_after'  => ':</strong> ',
			)
		);

		foreach ( $item->get_formatted_meta_data() as $meta_id => $meta ) {
			$value     = $args['autop'] ? wp_kses_post( $meta->display_value ) : wp_kses_post( make_clickable( trim( $meta->display_value ) ) );
			$strings[] = $args['label_before'] . wp_kses_post( $meta->display_key ) . $args['label_after'] . $value;
		}

		if ( $strings ) {
			$html = $args['before'] . implode( $args['separator'], $strings ) . $args['after'];
		}

		$html = apply_filters( 'woocommerce_display_item_meta', $html, $item, $args );

		if ( $args['echo'] ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			echo $html;
		} else {
			return $html;
		}
	}
}

if ( ! function_exists( 'wc_display_item_downloads' ) ) {
	/**
	 * Display item download links.
	 *
	 * @since  3.0.0
	 * @param  WC_Order_Item $item Order Item.
	 * @param  array         $args Arguments.
	 * @return string|void
	 */
	function wc_display_item_downloads( $item, $args = array() ) {
		$strings = array();
		$html    = '';
		$args    = wp_parse_args(
			$args,
			array(
				'before'    => '<ul class ="wc-item-downloads"><li>',
				'after'     => '</li></ul>',
				'separator' => '</li><li>',
				'echo'      => true,
				'show_url'  => false,
			)
		);

		$downloads = is_object( $item ) && $item->is_type( 'line_item' ) ? $item->get_item_downloads() : array();

		if ( $downloads ) {
			$i = 0;
			foreach ( $downloads as $file ) {
				$i ++;

				if ( $args['show_url'] ) {
					$strings[] = '<strong class="wc-item-download-label">' . esc_html( $file['name'] ) . ':</strong> ' . esc_html( $file['download_url'] );
				} else {
					/* translators: %d: downloads count */
					$prefix    = count( $downloads ) > 1 ? sprintf( __( 'Download %d', 'woocommerce' ), $i ) : __( 'Download', 'woocommerce' );
					$strings[] = '<strong class="wc-item-download-label">' . $prefix . ':</strong> <a href="' . esc_url( $file['download_url'] ) . '" target="_blank">' . esc_html( $file['name'] ) . '</a>';
				}
			}
		}

		if ( $strings ) {
			$html = $args['before'] . implode( $args['separator'], $strings ) . $args['after'];
		}

		$html = apply_filters( 'woocommerce_display_item_downloads', $html, $item, $args );

		if ( $args['echo'] ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			echo $html;
		} else {
			return $html;
		}
	}
}

if ( ! function_exists( 'woocommerce_photoswipe' ) ) {

	/**
	 * Get the shop sidebar template.
	 */
	function woocommerce_photoswipe() {
		if ( current_theme_supports( 'wc-product-gallery-lightbox' ) ) {
			wc_get_template( 'single-product/photoswipe.php' );
		}
	}
}

/**
 * Outputs a list of product attributes for a product.
 *
 * @since  3.0.0
 * @param  WC_Product $product Product Object.
 */
function wc_display_product_attributes( $product ) {
	$product_attributes = array();

	// Display weight and dimensions before attribute list.
	$display_dimensions = apply_filters( 'wc_product_enable_dimensions_display', $product->has_weight() || $product->has_dimensions() );

	if ( $display_dimensions && $product->has_weight() ) {
		$product_attributes['weight'] = array(
			'label' => __( 'Weight', 'woocommerce' ),
			'value' => wc_format_weight( $product->get_weight() ),
		);
	}

	if ( $display_dimensions && $product->has_dimensions() ) {
		$product_attributes['dimensions'] = array(
			'label' => __( 'Dimensions', 'woocommerce' ),
			'value' => wc_format_dimensions( $product->get_dimensions( false ) ),
		);
	}

	// Add product attributes to list.
	$attributes = array_filter( $product->get_attributes(), 'wc_attributes_array_filter_visible' );

	foreach ( $attributes as $attribute ) {
		$values = array();

		if ( $attribute->is_taxonomy() ) {
			$attribute_taxonomy = $attribute->get_taxonomy_object();
			$attribute_values   = wc_get_product_terms( $product->get_id(), $attribute->get_name(), array( 'fields' => 'all' ) );

			foreach ( $attribute_values as $attribute_value ) {
				$value_name = esc_html( $attribute_value->name );

				if ( $attribute_taxonomy->attribute_public ) {
					$values[] = '<a href="' . esc_url( get_term_link( $attribute_value->term_id, $attribute->get_name() ) ) . '" rel="tag">' . $value_name . '</a>';
				} else {
					$values[] = $value_name;
				}
			}
		} else {
			$values = $attribute->get_options();

			foreach ( $values as &$value ) {
				$value = make_clickable( esc_html( $value ) );
			}
		}

		$product_attributes[ 'attribute_' . sanitize_title_with_dashes( $attribute->get_name() ) ] = array(
			'label' => wc_attribute_label( $attribute->get_name() ),
			'value' => apply_filters( 'woocommerce_attribute', wpautop( wptexturize( implode( ', ', $values ) ) ), $attribute, $values ),
		);
	}

	/**
	 * Hook: woocommerce_display_product_attributes.
	 *
	 * @since 3.6.0.
	 * @param array $product_attributes Array of atributes to display; label, value.
	 * @param WC_Product $product Showing attributes for this product.
	 */
	$product_attributes = apply_filters( 'woocommerce_display_product_attributes', $product_attributes, $product );

	wc_get_template(
		'single-product/product-attributes.php',
		array(
			'product_attributes' => $product_attributes,
			// Legacy params.
			'product'            => $product,
			'attributes'         => $attributes,
			'display_dimensions' => $display_dimensions,
		)
	);
}

/**
 * Get HTML to show product stock.
 *
 * @since  3.0.0
 * @param  WC_Product $product Product Object.
 * @return string
 */
function wc_get_stock_html( $product ) {
	$html         = '';
	$availability = $product->get_availability();

	if ( ! empty( $availability['availability'] ) ) {
		ob_start();

		wc_get_template(
			'single-product/stock.php',
			array(
				'product'      => $product,
				'class'        => $availability['class'],
				'availability' => $availability['availability'],
			)
		);

		$html = ob_get_clean();
	}

	if ( has_filter( 'woocommerce_stock_html' ) ) {
		wc_deprecated_function( 'The woocommerce_stock_html filter', '', 'woocommerce_get_stock_html' );
		$html = apply_filters( 'woocommerce_stock_html', $html, $availability['availability'], $product );
	}

	return apply_filters( 'woocommerce_get_stock_html', $html, $product );
}

/**
 * Get HTML for ratings.
 *
 * @since  3.0.0
 * @param  float $rating Rating being shown.
 * @param  int   $count  Total number of ratings.
 * @return string
 */
function wc_get_rating_html( $rating, $count = 0 ) {
	$html = '';

	if ( 0 < $rating ) {
		/* translators: %s: rating */
		$label = sprintf( __( 'Rated %s out of 5', 'woocommerce' ), $rating );
		$html  = '<div class="star-rating" role="img" aria-label="' . esc_attr( $label ) . '">' . wc_get_star_rating_html( $rating, $count ) . '</div>';
	}

	return apply_filters( 'woocommerce_product_get_rating_html', $html, $rating, $count );
}

/**
 * Get HTML for star rating.
 *
 * @since  3.1.0
 * @param  float $rating Rating being shown.
 * @param  int   $count  Total number of ratings.
 * @return string
 */
function wc_get_star_rating_html( $rating, $count = 0 ) {
	$html = '<span style="width:' . ( ( $rating / 5 ) * 100 ) . '%">';

	if ( 0 < $count ) {
		/* translators: 1: rating 2: rating count */
		$html .= sprintf( _n( 'Rated %1$s out of 5 based on %2$s customer rating', 'Rated %1$s out of 5 based on %2$s customer ratings', $count, 'woocommerce' ), '<strong class="rating">' . esc_html( $rating ) . '</strong>', '<span class="rating">' . esc_html( $count ) . '</span>' );
	} else {
		/* translators: %s: rating */
		$html .= sprintf( esc_html__( 'Rated %s out of 5', 'woocommerce' ), '<strong class="rating">' . esc_html( $rating ) . '</strong>' );
	}

	$html .= '</span>';

	return apply_filters( 'woocommerce_get_star_rating_html', $html, $rating, $count );
}

/**
 * Returns a 'from' prefix if you want to show where prices start at.
 *
 * @since  3.0.0
 * @return string
 */
function wc_get_price_html_from_text() {
	return apply_filters( 'woocommerce_get_price_html_from_text', '<span class="from">' . _x( 'From:', 'min_price', 'woocommerce' ) . ' </span>' );
}

/**
 * Get logout endpoint.
 *
 * @since  2.6.9
 *
 * @param string $redirect Redirect URL.
 *
 * @return string
 */
function wc_logout_url( $redirect = '' ) {
	$redirect = $redirect ? $redirect : apply_filters( 'woocommerce_logout_default_redirect_url', wc_get_page_permalink( 'myaccount' ) );

	if ( get_option( 'woocommerce_logout_endpoint' ) ) {
		return wp_nonce_url( wc_get_endpoint_url( 'customer-logout', '', $redirect ), 'customer-logout' );
	}

	return wp_logout_url( $redirect );
}

/**
 * Show notice if cart is empty.
 *
 * @since 3.1.0
 */
function wc_empty_cart_message() {
	echo '<p class="cart-empty woocommerce-info">' . wp_kses_post( apply_filters( 'wc_empty_cart_message', __( 'Your cart is currently empty.', 'woocommerce' ) ) ) . '</p>';
}

/**
 * Disable search engines indexing core, dynamic, cart/checkout pages.
 *
 * @todo Deprecated this function after dropping support for WP 5.6.
 * @since 3.2.0
 */
function wc_page_noindex() {
	// wp_no_robots is deprecated since WP 5.7.
	if ( function_exists( 'wp_robots_no_robots' ) ) {
		return;
	}

	if ( is_page( wc_get_page_id( 'cart' ) ) || is_page( wc_get_page_id( 'checkout' ) ) || is_page( wc_get_page_id( 'myaccount' ) ) ) {
		wp_no_robots();
	}
}
add_action( 'wp_head', 'wc_page_noindex' );

/**
 * Disable search engines indexing core, dynamic, cart/checkout pages.
 * Uses "wp_robots" filter introduced in WP 5.7.
 *
 * @since 5.0.0
 * @param array $robots Associative array of robots directives.
 * @return array Filtered robots directives.
 */
function wc_page_no_robots( $robots ) {
	if ( is_page( wc_get_page_id( 'cart' ) ) || is_page( wc_get_page_id( 'checkout' ) ) || is_page( wc_get_page_id( 'myaccount' ) ) ) {
		return wp_robots_no_robots( $robots );
	}

	return $robots;
}
add_filter( 'wp_robots', 'wc_page_no_robots' );

/**
 * Get a slug identifying the current theme.
 *
 * @since 3.3.0
 * @return string
 */
function wc_get_theme_slug_for_templates() {
	return apply_filters( 'woocommerce_theme_slug_for_templates', get_option( 'template' ) );
}

/**
 * Gets and formats a list of cart item data + variations for display on the frontend.
 *
 * @since 3.3.0
 * @param array $cart_item Cart item object.
 * @param bool  $flat Should the data be returned flat or in a list.
 * @return string
 */
function wc_get_formatted_cart_item_data( $cart_item, $flat = false ) {
	$item_data = array();

	// Variation values are shown only if they are not found in the title as of 3.0.
	// This is because variation titles display the attributes.
	if ( $cart_item['data']->is_type( 'variation' ) && is_array( $cart_item['variation'] ) ) {
		foreach ( $cart_item['variation'] as $name => $value ) {
			$taxonomy = wc_attribute_taxonomy_name( str_replace( 'attribute_pa_', '', urldecode( $name ) ) );

			if ( taxonomy_exists( $taxonomy ) ) {
				// If this is a term slug, get the term's nice name.
				$term = get_term_by( 'slug', $value, $taxonomy );
				if ( ! is_wp_error( $term ) && $term && $term->name ) {
					$value = $term->name;
				}
				$label = wc_attribute_label( $taxonomy );
			} else {
				// If this is a custom option slug, get the options name.
				$value = apply_filters( 'woocommerce_variation_option_name', $value, null, $taxonomy, $cart_item['data'] );
				$label = wc_attribute_label( str_replace( 'attribute_', '', $name ), $cart_item['data'] );
			}

			// Check the nicename against the title.
			if ( '' === $value || wc_is_attribute_in_product_name( $value, $cart_item['data']->get_name() ) ) {
				continue;
			}

			$item_data[] = array(
				'key'   => $label,
				'value' => $value,
			);
		}
	}

	// Filter item data to allow 3rd parties to add more to the array.
	$item_data = apply_filters( 'woocommerce_get_item_data', $item_data, $cart_item );

	// Format item data ready to display.
	foreach ( $item_data as $key => $data ) {
		// Set hidden to true to not display meta on cart.
		if ( ! empty( $data['hidden'] ) ) {
			unset( $item_data[ $key ] );
			continue;
		}
		$item_data[ $key ]['key']     = ! empty( $data['key'] ) ? $data['key'] : $data['name'];
		$item_data[ $key ]['display'] = ! empty( $data['display'] ) ? $data['display'] : $data['value'];
	}

	// Output flat or in list format.
	if ( count( $item_data ) > 0 ) {
		ob_start();

		if ( $flat ) {
			foreach ( $item_data as $data ) {
				echo esc_html( $data['key'] ) . ': ' . wp_kses_post( $data['display'] ) . "\n";
			}
		} else {
			wc_get_template( 'cart/cart-item-data.php', array( 'item_data' => $item_data ) );
		}

		return ob_get_clean();
	}

	return '';
}

/**
 * Gets the url to remove an item from the cart.
 *
 * @since 3.3.0
 * @param string $cart_item_key contains the id of the cart item.
 * @return string url to page
 */
function wc_get_cart_remove_url( $cart_item_key ) {
	$cart_page_url = wc_get_cart_url();
	return apply_filters( 'woocommerce_get_remove_url', $cart_page_url ? wp_nonce_url( add_query_arg( 'remove_item', $cart_item_key, $cart_page_url ), 'woocommerce-cart' ) : '' );
}

/**
 * Gets the url to re-add an item into the cart.
 *
 * @since 3.3.0
 * @param  string $cart_item_key Cart item key to undo.
 * @return string url to page
 */
function wc_get_cart_undo_url( $cart_item_key ) {
	$cart_page_url = wc_get_cart_url();

	$query_args = array(
		'undo_item' => $cart_item_key,
	);

	return apply_filters( 'woocommerce_get_undo_url', $cart_page_url ? wp_nonce_url( add_query_arg( $query_args, $cart_page_url ), 'woocommerce-cart' ) : '', $cart_item_key );
}

/**
 * Outputs all queued notices on WC pages.
 *
 * @since 3.5.0
 */
function woocommerce_output_all_notices() {
	echo '<div class="woocommerce-notices-wrapper">';
	wc_print_notices();
	echo '</div>';
}

/**
 * Products RSS Feed.
 *
 * @deprecated 2.6
 */
function wc_products_rss_feed() {
	wc_deprecated_function( 'wc_products_rss_feed', '2.6' );
}

if ( ! function_exists( 'woocommerce_reset_loop' ) ) {

	/**
	 * Reset the loop's index and columns when we're done outputting a product loop.
	 *
	 * @deprecated 3.3
	 */
	function woocommerce_reset_loop() {
		wc_reset_loop();
	}
}

if ( ! function_exists( 'woocommerce_product_reviews_tab' ) ) {
	/**
	 * Output the reviews tab content.
	 *
	 * @deprecated 2.4.0 Unused.
	 */
	function woocommerce_product_reviews_tab() {
		wc_deprecated_function( 'woocommerce_product_reviews_tab', '2.4' );
	}
}

/**
 * Display pay buttons HTML.
 *
 * @since 3.9.0
 */
function wc_get_pay_buttons() {
	$supported_gateways = array();
	$available_gateways = WC()->payment_gateways()->get_available_payment_gateways();

	foreach ( $available_gateways as $gateway ) {
		if ( $gateway->supports( 'pay_button' ) ) {
			$supported_gateways[] = $gateway->get_pay_button_id();
		}
	}

	if ( ! $supported_gateways ) {
		return;
	}

	echo '<div class="woocommerce-pay-buttons">';
	foreach ( $supported_gateways as $pay_button_id ) {
		echo sprintf( '<div class="woocommerce-pay-button__%1$s %1$s" id="%1$s"></div>', esc_attr( $pay_button_id ) );
	}
	echo '</div>';
}

// phpcs:enable Generic.Commenting.Todo.TaskFound
wc-user-functions.php000064400000065152151336044760010672 0ustar00<?php
/**
 * WooCommerce Customer Functions
 *
 * Functions for customers.
 *
 * @package WooCommerce\Functions
 * @version 2.2.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Prevent any user who cannot 'edit_posts' (subscribers, customers etc) from seeing the admin bar.
 *
 * Note: get_option( 'woocommerce_lock_down_admin', true ) is a deprecated option here for backwards compatibility. Defaults to true.
 *
 * @param bool $show_admin_bar If should display admin bar.
 * @return bool
 */
function wc_disable_admin_bar( $show_admin_bar ) {
	if ( apply_filters( 'woocommerce_disable_admin_bar', true ) && ! ( current_user_can( 'edit_posts' ) || current_user_can( 'manage_woocommerce' ) ) ) {
		$show_admin_bar = false;
	}

	return $show_admin_bar;
}
add_filter( 'show_admin_bar', 'wc_disable_admin_bar', 10, 1 ); // phpcs:ignore WordPress.VIP.AdminBarRemoval.RemovalDetected

if ( ! function_exists( 'wc_create_new_customer' ) ) {

	/**
	 * Create a new customer.
	 *
	 * @param  string $email    Customer email.
	 * @param  string $username Customer username.
	 * @param  string $password Customer password.
	 * @param  array  $args     List of arguments to pass to `wp_insert_user()`.
	 * @return int|WP_Error Returns WP_Error on failure, Int (user ID) on success.
	 */
	function wc_create_new_customer( $email, $username = '', $password = '', $args = array() ) {
		if ( empty( $email ) || ! is_email( $email ) ) {
			return new WP_Error( 'registration-error-invalid-email', __( 'Please provide a valid email address.', 'woocommerce' ) );
		}

		if ( email_exists( $email ) ) {
			return new WP_Error( 'registration-error-email-exists', apply_filters( 'woocommerce_registration_error_email_exists', __( 'An account is already registered with your email address. <a href="#" class="showlogin">Please log in.</a>', 'woocommerce' ), $email ) );
		}

		if ( 'yes' === get_option( 'woocommerce_registration_generate_username', 'yes' ) && empty( $username ) ) {
			$username = wc_create_new_customer_username( $email, $args );
		}

		$username = sanitize_user( $username );

		if ( empty( $username ) || ! validate_username( $username ) ) {
			return new WP_Error( 'registration-error-invalid-username', __( 'Please enter a valid account username.', 'woocommerce' ) );
		}

		if ( username_exists( $username ) ) {
			return new WP_Error( 'registration-error-username-exists', __( 'An account is already registered with that username. Please choose another.', 'woocommerce' ) );
		}

		// Handle password creation.
		$password_generated = false;
		if ( 'yes' === get_option( 'woocommerce_registration_generate_password' ) && empty( $password ) ) {
			$password           = wp_generate_password();
			$password_generated = true;
		}

		if ( empty( $password ) ) {
			return new WP_Error( 'registration-error-missing-password', __( 'Please enter an account password.', 'woocommerce' ) );
		}

		// Use WP_Error to handle registration errors.
		$errors = new WP_Error();

		do_action( 'woocommerce_register_post', $username, $email, $errors );

		$errors = apply_filters( 'woocommerce_registration_errors', $errors, $username, $email );

		if ( $errors->get_error_code() ) {
			return $errors;
		}

		$new_customer_data = apply_filters(
			'woocommerce_new_customer_data',
			array_merge(
				$args,
				array(
					'user_login' => $username,
					'user_pass'  => $password,
					'user_email' => $email,
					'role'       => 'customer',
				)
			)
		);

		$customer_id = wp_insert_user( $new_customer_data );

		if ( is_wp_error( $customer_id ) ) {
			return $customer_id;
		}

		do_action( 'woocommerce_created_customer', $customer_id, $new_customer_data, $password_generated );

		return $customer_id;
	}
}

/**
 * Create a unique username for a new customer.
 *
 * @since 3.6.0
 * @param string $email New customer email address.
 * @param array  $new_user_args Array of new user args, maybe including first and last names.
 * @param string $suffix Append string to username to make it unique.
 * @return string Generated username.
 */
function wc_create_new_customer_username( $email, $new_user_args = array(), $suffix = '' ) {
	$username_parts = array();

	if ( isset( $new_user_args['first_name'] ) ) {
		$username_parts[] = sanitize_user( $new_user_args['first_name'], true );
	}

	if ( isset( $new_user_args['last_name'] ) ) {
		$username_parts[] = sanitize_user( $new_user_args['last_name'], true );
	}

	// Remove empty parts.
	$username_parts = array_filter( $username_parts );

	// If there are no parts, e.g. name had unicode chars, or was not provided, fallback to email.
	if ( empty( $username_parts ) ) {
		$email_parts    = explode( '@', $email );
		$email_username = $email_parts[0];

		// Exclude common prefixes.
		if ( in_array(
			$email_username,
			array(
				'sales',
				'hello',
				'mail',
				'contact',
				'info',
			),
			true
		) ) {
			// Get the domain part.
			$email_username = $email_parts[1];
		}

		$username_parts[] = sanitize_user( $email_username, true );
	}

	$username = wc_strtolower( implode( '.', $username_parts ) );

	if ( $suffix ) {
		$username .= $suffix;
	}

	/**
	 * WordPress 4.4 - filters the list of blocked usernames.
	 *
	 * @since 3.7.0
	 * @param array $usernames Array of blocked usernames.
	 */
	$illegal_logins = (array) apply_filters( 'illegal_user_logins', array() );

	// Stop illegal logins and generate a new random username.
	if ( in_array( strtolower( $username ), array_map( 'strtolower', $illegal_logins ), true ) ) {
		$new_args = array();

		/**
		 * Filter generated customer username.
		 *
		 * @since 3.7.0
		 * @param string $username      Generated username.
		 * @param string $email         New customer email address.
		 * @param array  $new_user_args Array of new user args, maybe including first and last names.
		 * @param string $suffix        Append string to username to make it unique.
		 */
		$new_args['first_name'] = apply_filters(
			'woocommerce_generated_customer_username',
			'woo_user_' . zeroise( wp_rand( 0, 9999 ), 4 ),
			$email,
			$new_user_args,
			$suffix
		);

		return wc_create_new_customer_username( $email, $new_args, $suffix );
	}

	if ( username_exists( $username ) ) {
		// Generate something unique to append to the username in case of a conflict with another user.
		$suffix = '-' . zeroise( wp_rand( 0, 9999 ), 4 );
		return wc_create_new_customer_username( $email, $new_user_args, $suffix );
	}

	/**
	 * Filter new customer username.
	 *
	 * @since 3.7.0
	 * @param string $username      Customer username.
	 * @param string $email         New customer email address.
	 * @param array  $new_user_args Array of new user args, maybe including first and last names.
	 * @param string $suffix        Append string to username to make it unique.
	 */
	return apply_filters( 'woocommerce_new_customer_username', $username, $email, $new_user_args, $suffix );
}

/**
 * Login a customer (set auth cookie and set global user object).
 *
 * @param int $customer_id Customer ID.
 */
function wc_set_customer_auth_cookie( $customer_id ) {
	wp_set_current_user( $customer_id );
	wp_set_auth_cookie( $customer_id, true );

	// Update session.
	WC()->session->init_session_cookie();
}

/**
 * Get past orders (by email) and update them.
 *
 * @param  int $customer_id Customer ID.
 * @return int
 */
function wc_update_new_customer_past_orders( $customer_id ) {
	$linked          = 0;
	$complete        = 0;
	$customer        = get_user_by( 'id', absint( $customer_id ) );
	$customer_orders = wc_get_orders(
		array(
			'limit'    => -1,
			'customer' => array( array( 0, $customer->user_email ) ),
			'return'   => 'ids',
		)
	);

	if ( ! empty( $customer_orders ) ) {
		foreach ( $customer_orders as $order_id ) {
			$order = wc_get_order( $order_id );
			if ( ! $order ) {
				continue;
			}

			$order->set_customer_id( $customer->ID );
			$order->save();

			if ( $order->has_downloadable_item() ) {
				$data_store = WC_Data_Store::load( 'customer-download' );
				$data_store->delete_by_order_id( $order->get_id() );
				wc_downloadable_product_permissions( $order->get_id(), true );
			}

			do_action( 'woocommerce_update_new_customer_past_order', $order_id, $customer );

			if ( get_post_status( $order_id ) === 'wc-completed' ) {
				$complete++;
			}

			$linked++;
		}
	}

	if ( $complete ) {
		update_user_meta( $customer_id, 'paying_customer', 1 );
		update_user_meta( $customer_id, '_order_count', '' );
		update_user_meta( $customer_id, '_money_spent', '' );
		delete_user_meta( $customer_id, '_last_order' );
	}

	return $linked;
}

/**
 * Order payment completed - This is a paying customer.
 *
 * @param int $order_id Order ID.
 */
function wc_paying_customer( $order_id ) {
	$order       = wc_get_order( $order_id );
	$customer_id = $order->get_customer_id();

	if ( $customer_id > 0 && 'shop_order_refund' !== $order->get_type() ) {
		$customer = new WC_Customer( $customer_id );

		if ( ! $customer->get_is_paying_customer() ) {
			$customer->set_is_paying_customer( true );
			$customer->save();
		}
	}
}
add_action( 'woocommerce_payment_complete', 'wc_paying_customer' );
add_action( 'woocommerce_order_status_completed', 'wc_paying_customer' );

/**
 * Checks if a user (by email or ID or both) has bought an item.
 *
 * @param string $customer_email Customer email to check.
 * @param int    $user_id User ID to check.
 * @param int    $product_id Product ID to check.
 * @return bool
 */
function wc_customer_bought_product( $customer_email, $user_id, $product_id ) {
	global $wpdb;

	$result = apply_filters( 'woocommerce_pre_customer_bought_product', null, $customer_email, $user_id, $product_id );

	if ( null !== $result ) {
		return $result;
	}

	$transient_name    = 'wc_customer_bought_product_' . md5( $customer_email . $user_id );
	$transient_version = WC_Cache_Helper::get_transient_version( 'orders' );
	$transient_value   = get_transient( $transient_name );

	if ( isset( $transient_value['value'], $transient_value['version'] ) && $transient_value['version'] === $transient_version ) {
		$result = $transient_value['value'];
	} else {
		$customer_data = array( $user_id );

		if ( $user_id ) {
			$user = get_user_by( 'id', $user_id );

			if ( isset( $user->user_email ) ) {
				$customer_data[] = $user->user_email;
			}
		}

		if ( is_email( $customer_email ) ) {
			$customer_data[] = $customer_email;
		}

		$customer_data = array_map( 'esc_sql', array_filter( array_unique( $customer_data ) ) );
		$statuses      = array_map( 'esc_sql', wc_get_is_paid_statuses() );

		if ( count( $customer_data ) === 0 ) {
			return false;
		}

		$result = $wpdb->get_col(
			"
			SELECT im.meta_value FROM {$wpdb->posts} AS p
			INNER JOIN {$wpdb->postmeta} AS pm ON p.ID = pm.post_id
			INNER JOIN {$wpdb->prefix}woocommerce_order_items AS i ON p.ID = i.order_id
			INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS im ON i.order_item_id = im.order_item_id
			WHERE p.post_status IN ( 'wc-" . implode( "','wc-", $statuses ) . "' )
			AND pm.meta_key IN ( '_billing_email', '_customer_user' )
			AND im.meta_key IN ( '_product_id', '_variation_id' )
			AND im.meta_value != 0
			AND pm.meta_value IN ( '" . implode( "','", $customer_data ) . "' )
		"
		); // WPCS: unprepared SQL ok.
		$result = array_map( 'absint', $result );

		$transient_value = array(
			'version' => $transient_version,
			'value'   => $result,
		);

		set_transient( $transient_name, $transient_value, DAY_IN_SECONDS * 30 );
	}
	return in_array( absint( $product_id ), $result, true );
}

/**
 * Checks if the current user has a role.
 *
 * @param string $role The role.
 * @return bool
 */
function wc_current_user_has_role( $role ) {
	return wc_user_has_role( wp_get_current_user(), $role );
}

/**
 * Checks if a user has a role.
 *
 * @param int|\WP_User $user The user.
 * @param string       $role The role.
 * @return bool
 */
function wc_user_has_role( $user, $role ) {
	if ( ! is_object( $user ) ) {
		$user = get_userdata( $user );
	}

	if ( ! $user || ! $user->exists() ) {
		return false;
	}

	return in_array( $role, $user->roles, true );
}

/**
 * Checks if a user has a certain capability.
 *
 * @param array $allcaps All capabilities.
 * @param array $caps    Capabilities.
 * @param array $args    Arguments.
 *
 * @return array The filtered array of all capabilities.
 */
function wc_customer_has_capability( $allcaps, $caps, $args ) {
	if ( isset( $caps[0] ) ) {
		switch ( $caps[0] ) {
			case 'view_order':
				$user_id = intval( $args[1] );
				$order   = wc_get_order( $args[2] );

				if ( $order && $user_id === $order->get_user_id() ) {
					$allcaps['view_order'] = true;
				}
				break;
			case 'pay_for_order':
				$user_id  = intval( $args[1] );
				$order_id = isset( $args[2] ) ? $args[2] : null;

				// When no order ID, we assume it's a new order
				// and thus, customer can pay for it.
				if ( ! $order_id ) {
					$allcaps['pay_for_order'] = true;
					break;
				}

				$order = wc_get_order( $order_id );

				if ( $order && ( $user_id === $order->get_user_id() || ! $order->get_user_id() ) ) {
					$allcaps['pay_for_order'] = true;
				}
				break;
			case 'order_again':
				$user_id = intval( $args[1] );
				$order   = wc_get_order( $args[2] );

				if ( $order && $user_id === $order->get_user_id() ) {
					$allcaps['order_again'] = true;
				}
				break;
			case 'cancel_order':
				$user_id = intval( $args[1] );
				$order   = wc_get_order( $args[2] );

				if ( $order && $user_id === $order->get_user_id() ) {
					$allcaps['cancel_order'] = true;
				}
				break;
			case 'download_file':
				$user_id  = intval( $args[1] );
				$download = $args[2];

				if ( $download && $user_id === $download->get_user_id() ) {
					$allcaps['download_file'] = true;
				}
				break;
		}
	}
	return $allcaps;
}
add_filter( 'user_has_cap', 'wc_customer_has_capability', 10, 3 );

/**
 * Safe way of allowing shop managers restricted capabilities that will remove
 * access to the capabilities if WooCommerce is deactivated.
 *
 * @since 3.5.4
 * @param bool[]   $allcaps Array of key/value pairs where keys represent a capability name and boolean values
 *                          represent whether the user has that capability.
 * @param string[] $caps    Required primitive capabilities for the requested capability.
 * @param array    $args Arguments that accompany the requested capability check.
 * @param WP_User  $user    The user object.
 * @return bool[]
 */
function wc_shop_manager_has_capability( $allcaps, $caps, $args, $user ) {

	if ( wc_user_has_role( $user, 'shop_manager' ) ) {
		// @see wc_modify_map_meta_cap, which limits editing to customers.
		$allcaps['edit_users'] = true;
	}

	return $allcaps;
}
add_filter( 'user_has_cap', 'wc_shop_manager_has_capability', 10, 4 );

/**
 * Modify the list of editable roles to prevent non-admin adding admin users.
 *
 * @param  array $roles Roles.
 * @return array
 */
function wc_modify_editable_roles( $roles ) {
	if ( is_multisite() && is_super_admin() ) {
		return $roles;
	}
	if ( ! wc_current_user_has_role( 'administrator' ) ) {
		unset( $roles['administrator'] );

		if ( wc_current_user_has_role( 'shop_manager' ) ) {
			$shop_manager_editable_roles = apply_filters( 'woocommerce_shop_manager_editable_roles', array( 'customer' ) );
			return array_intersect_key( $roles, array_flip( $shop_manager_editable_roles ) );
		}
	}

	return $roles;
}
add_filter( 'editable_roles', 'wc_modify_editable_roles' );

/**
 * Modify capabilities to prevent non-admin users editing admin users.
 *
 * $args[0] will be the user being edited in this case.
 *
 * @param  array  $caps    Array of caps.
 * @param  string $cap     Name of the cap we are checking.
 * @param  int    $user_id ID of the user being checked against.
 * @param  array  $args    Arguments.
 * @return array
 */
function wc_modify_map_meta_cap( $caps, $cap, $user_id, $args ) {
	if ( is_multisite() && is_super_admin() ) {
		return $caps;
	}
	switch ( $cap ) {
		case 'edit_user':
		case 'remove_user':
		case 'promote_user':
		case 'delete_user':
			if ( ! isset( $args[0] ) || $args[0] === $user_id ) {
				break;
			} else {
				if ( ! wc_current_user_has_role( 'administrator' ) ) {
					if ( wc_user_has_role( $args[0], 'administrator' ) ) {
						$caps[] = 'do_not_allow';
					} elseif ( wc_current_user_has_role( 'shop_manager' ) ) {
						// Shop managers can only edit customer info.
						$userdata                    = get_userdata( $args[0] );
						$shop_manager_editable_roles = apply_filters( 'woocommerce_shop_manager_editable_roles', array( 'customer' ) );
						if ( property_exists( $userdata, 'roles' ) && ! empty( $userdata->roles ) && ! array_intersect( $userdata->roles, $shop_manager_editable_roles ) ) {
							$caps[] = 'do_not_allow';
						}
					}
				}
			}
			break;
	}
	return $caps;
}
add_filter( 'map_meta_cap', 'wc_modify_map_meta_cap', 10, 4 );

/**
 * Get customer download permissions from the database.
 *
 * @param int $customer_id Customer/User ID.
 * @return array
 */
function wc_get_customer_download_permissions( $customer_id ) {
	$data_store = WC_Data_Store::load( 'customer-download' );
	return apply_filters( 'woocommerce_permission_list', $data_store->get_downloads_for_customer( $customer_id ), $customer_id );
}

/**
 * Get customer available downloads.
 *
 * @param int $customer_id Customer/User ID.
 * @return array
 */
function wc_get_customer_available_downloads( $customer_id ) {
	$downloads   = array();
	$_product    = null;
	$order       = null;
	$file_number = 0;

	// Get results from valid orders only.
	$results = wc_get_customer_download_permissions( $customer_id );

	if ( $results ) {
		foreach ( $results as $result ) {
			$order_id = intval( $result->order_id );

			if ( ! $order || $order->get_id() !== $order_id ) {
				// New order.
				$order    = wc_get_order( $order_id );
				$_product = null;
			}

			// Make sure the order exists for this download.
			if ( ! $order ) {
				continue;
			}

			// Check if downloads are permitted.
			if ( ! $order->is_download_permitted() ) {
				continue;
			}

			$product_id = intval( $result->product_id );

			if ( ! $_product || $_product->get_id() !== $product_id ) {
				// New product.
				$file_number = 0;
				$_product    = wc_get_product( $product_id );
			}

			// Check product exists and has the file.
			if ( ! $_product || ! $_product->exists() || ! $_product->has_file( $result->download_id ) ) {
				continue;
			}

			$download_file = $_product->get_file( $result->download_id );

			// Download name will be 'Product Name' for products with a single downloadable file, and 'Product Name - File X' for products with multiple files.
			$download_name = apply_filters(
				'woocommerce_downloadable_product_name',
				$download_file['name'],
				$_product,
				$result->download_id,
				$file_number
			);

			$downloads[] = array(
				'download_url'        => add_query_arg(
					array(
						'download_file' => $product_id,
						'order'         => $result->order_key,
						'email'         => rawurlencode( $result->user_email ),
						'key'           => $result->download_id,
					),
					home_url( '/' )
				),
				'download_id'         => $result->download_id,
				'product_id'          => $_product->get_id(),
				'product_name'        => $_product->get_name(),
				'product_url'         => $_product->is_visible() ? $_product->get_permalink() : '', // Since 3.3.0.
				'download_name'       => $download_name,
				'order_id'            => $order->get_id(),
				'order_key'           => $order->get_order_key(),
				'downloads_remaining' => $result->downloads_remaining,
				'access_expires'      => $result->access_expires,
				'file'                => array(
					'name' => $download_file->get_name(),
					'file' => $download_file->get_file(),
				),
			);

			$file_number++;
		}
	}

	return apply_filters( 'woocommerce_customer_available_downloads', $downloads, $customer_id );
}

/**
 * Get total spent by customer.
 *
 * @param  int $user_id User ID.
 * @return string
 */
function wc_get_customer_total_spent( $user_id ) {
	$customer = new WC_Customer( $user_id );
	return $customer->get_total_spent();
}

/**
 * Get total orders by customer.
 *
 * @param  int $user_id User ID.
 * @return int
 */
function wc_get_customer_order_count( $user_id ) {
	$customer = new WC_Customer( $user_id );
	return $customer->get_order_count();
}

/**
 * Reset _customer_user on orders when a user is deleted.
 *
 * @param int $user_id User ID.
 */
function wc_reset_order_customer_id_on_deleted_user( $user_id ) {
	global $wpdb;

	$wpdb->update(
		$wpdb->postmeta,
		array(
			'meta_value' => 0,
		),
		array(
			'meta_key'   => '_customer_user',
			'meta_value' => $user_id,
		)
	); // WPCS: slow query ok.
}

add_action( 'deleted_user', 'wc_reset_order_customer_id_on_deleted_user' );

/**
 * Get review verification status.
 *
 * @param  int $comment_id Comment ID.
 * @return bool
 */
function wc_review_is_from_verified_owner( $comment_id ) {
	$verified = get_comment_meta( $comment_id, 'verified', true );
	return '' === $verified ? WC_Comments::add_comment_purchase_verification( $comment_id ) : (bool) $verified;
}

/**
 * Disable author archives for customers.
 *
 * @since 2.5.0
 */
function wc_disable_author_archives_for_customers() {
	global $author;

	if ( is_author() ) {
		$user = get_user_by( 'id', $author );

		if ( user_can( $user, 'customer' ) && ! user_can( $user, 'edit_posts' ) ) {
			wp_safe_redirect( wc_get_page_permalink( 'shop' ) );
			exit;
		}
	}
}

add_action( 'template_redirect', 'wc_disable_author_archives_for_customers' );

/**
 * Hooks into the `profile_update` hook to set the user last updated timestamp.
 *
 * @since 2.6.0
 * @param int   $user_id The user that was updated.
 * @param array $old     The profile fields pre-change.
 */
function wc_update_profile_last_update_time( $user_id, $old ) {
	wc_set_user_last_update_time( $user_id );
}

add_action( 'profile_update', 'wc_update_profile_last_update_time', 10, 2 );

/**
 * Hooks into the update user meta function to set the user last updated timestamp.
 *
 * @since 2.6.0
 * @param int    $meta_id     ID of the meta object that was changed.
 * @param int    $user_id     The user that was updated.
 * @param string $meta_key    Name of the meta key that was changed.
 * @param string $_meta_value Value of the meta that was changed.
 */
function wc_meta_update_last_update_time( $meta_id, $user_id, $meta_key, $_meta_value ) {
	$keys_to_track = apply_filters( 'woocommerce_user_last_update_fields', array( 'first_name', 'last_name' ) );

	$update_time = in_array( $meta_key, $keys_to_track, true ) ? true : false;
	$update_time = 'billing_' === substr( $meta_key, 0, 8 ) ? true : $update_time;
	$update_time = 'shipping_' === substr( $meta_key, 0, 9 ) ? true : $update_time;

	if ( $update_time ) {
		wc_set_user_last_update_time( $user_id );
	}
}

add_action( 'update_user_meta', 'wc_meta_update_last_update_time', 10, 4 );

/**
 * Sets a user's "last update" time to the current timestamp.
 *
 * @since 2.6.0
 * @param int $user_id The user to set a timestamp for.
 */
function wc_set_user_last_update_time( $user_id ) {
	update_user_meta( $user_id, 'last_update', gmdate( 'U' ) );
}

/**
 * Get customer saved payment methods list.
 *
 * @since 2.6.0
 * @param int $customer_id Customer ID.
 * @return array
 */
function wc_get_customer_saved_methods_list( $customer_id ) {
	return apply_filters( 'woocommerce_saved_payment_methods_list', array(), $customer_id );
}

/**
 * Get info about customer's last order.
 *
 * @since 2.6.0
 * @param int $customer_id Customer ID.
 * @return WC_Order|bool Order object if successful or false.
 */
function wc_get_customer_last_order( $customer_id ) {
	$customer = new WC_Customer( $customer_id );

	return $customer->get_last_order();
}

/**
 * Add support for searching by display_name.
 *
 * @since 3.2.0
 * @param array $search_columns Column names.
 * @return array
 */
function wc_user_search_columns( $search_columns ) {
	$search_columns[] = 'display_name';
	return $search_columns;
}
add_filter( 'user_search_columns', 'wc_user_search_columns' );

/**
 * When a user is deleted in WordPress, delete corresponding WooCommerce data.
 *
 * @param int $user_id User ID being deleted.
 */
function wc_delete_user_data( $user_id ) {
	global $wpdb;

	// Clean up sessions.
	$wpdb->delete(
		$wpdb->prefix . 'woocommerce_sessions',
		array(
			'session_key' => $user_id,
		)
	);

	// Revoke API keys.
	$wpdb->delete(
		$wpdb->prefix . 'woocommerce_api_keys',
		array(
			'user_id' => $user_id,
		)
	);

	// Clean up payment tokens.
	$payment_tokens = WC_Payment_Tokens::get_customer_tokens( $user_id );

	foreach ( $payment_tokens as $payment_token ) {
		$payment_token->delete();
	}
}
add_action( 'delete_user', 'wc_delete_user_data' );

/**
 * Store user agents. Used for tracker.
 *
 * @since 3.0.0
 * @param string     $user_login User login.
 * @param int|object $user       User.
 */
function wc_maybe_store_user_agent( $user_login, $user ) {
	if ( 'yes' === get_option( 'woocommerce_allow_tracking', 'no' ) && user_can( $user, 'manage_woocommerce' ) ) {
		$admin_user_agents   = array_filter( (array) get_option( 'woocommerce_tracker_ua', array() ) );
		$admin_user_agents[] = wc_get_user_agent();
		update_option( 'woocommerce_tracker_ua', array_unique( $admin_user_agents ) );
	}
}
add_action( 'wp_login', 'wc_maybe_store_user_agent', 10, 2 );

/**
 * Update logic triggered on login.
 *
 * @since 3.4.0
 * @param string $user_login User login.
 * @param object $user       User.
 */
function wc_user_logged_in( $user_login, $user ) {
	wc_update_user_last_active( $user->ID );
	update_user_meta( $user->ID, '_woocommerce_load_saved_cart_after_login', 1 );
}
add_action( 'wp_login', 'wc_user_logged_in', 10, 2 );

/**
 * Update when the user was last active.
 *
 * @since 3.4.0
 */
function wc_current_user_is_active() {
	if ( ! is_user_logged_in() ) {
		return;
	}
	wc_update_user_last_active( get_current_user_id() );
}
add_action( 'wp', 'wc_current_user_is_active', 10 );

/**
 * Set the user last active timestamp to now.
 *
 * @since 3.4.0
 * @param int $user_id User ID to mark active.
 */
function wc_update_user_last_active( $user_id ) {
	if ( ! $user_id ) {
		return;
	}
	update_user_meta( $user_id, 'wc_last_active', (string) strtotime( gmdate( 'Y-m-d', time() ) ) );
}

/**
 * Translate WC roles using the woocommerce textdomain.
 *
 * @since 3.7.0
 * @param string $translation  Translated text.
 * @param string $text         Text to translate.
 * @param string $context      Context information for the translators.
 * @param string $domain       Text domain. Unique identifier for retrieving translated strings.
 * @return string
 */
function wc_translate_user_roles( $translation, $text, $context, $domain ) {
	// translate_user_role() only accepts a second parameter starting in WP 5.2.
	if ( version_compare( get_bloginfo( 'version' ), '5.2', '<' ) ) {
		return $translation;
	}

	if ( 'User role' === $context && 'default' === $domain && in_array( $text, array( 'Shop manager', 'Customer' ), true ) ) {
		return translate_user_role( $text, 'woocommerce' );
	}

	return $translation;
}
add_filter( 'gettext_with_context', 'wc_translate_user_roles', 10, 4 );
class-wc-tracker.php000064400000056516151336044760010450 0ustar00<?php
/**
 * WooCommerce Tracker
 *
 * The WooCommerce tracker class adds functionality to track WooCommerce usage based on if the customer opted in.
 * No personal information is tracked, only general WooCommerce settings, general product, order and user counts and admin email for discount code.
 *
 * @class WC_Tracker
 * @since 2.3.0
 * @package WooCommerce\Classes
 */

use Automattic\Jetpack\Constants;

defined( 'ABSPATH' ) || exit;

/**
 * WooCommerce Tracker Class
 */
class WC_Tracker {

	/**
	 * URL to the WooThemes Tracker API endpoint.
	 *
	 * @var string
	 */
	private static $api_url = 'https://tracking.woocommerce.com/v1/';

	/**
	 * Hook into cron event.
	 */
	public static function init() {
		add_action( 'woocommerce_tracker_send_event', array( __CLASS__, 'send_tracking_data' ) );
	}

	/**
	 * Decide whether to send tracking data or not.
	 *
	 * @param boolean $override Should override?.
	 */
	public static function send_tracking_data( $override = false ) {
		// Don't trigger this on AJAX Requests.
		if ( Constants::is_true( 'DOING_AJAX' ) ) {
			return;
		}

		if ( ! apply_filters( 'woocommerce_tracker_send_override', $override ) ) {
			// Send a maximum of once per week by default.
			$last_send = self::get_last_send_time();
			if ( $last_send && $last_send > apply_filters( 'woocommerce_tracker_last_send_interval', strtotime( '-1 week' ) ) ) {
				return;
			}
		} else {
			// Make sure there is at least a 1 hour delay between override sends, we don't want duplicate calls due to double clicking links.
			$last_send = self::get_last_send_time();
			if ( $last_send && $last_send > strtotime( '-1 hours' ) ) {
				return;
			}
		}

		// Update time first before sending to ensure it is set.
		update_option( 'woocommerce_tracker_last_send', time() );

		$params = self::get_tracking_data();
		wp_safe_remote_post(
			self::$api_url,
			array(
				'method'      => 'POST',
				'timeout'     => 45,
				'redirection' => 5,
				'httpversion' => '1.0',
				'blocking'    => false,
				'headers'     => array( 'user-agent' => 'WooCommerceTracker/' . md5( esc_url_raw( home_url( '/' ) ) ) . ';' ),
				'body'        => wp_json_encode( $params ),
				'cookies'     => array(),
			)
		);
	}

	/**
	 * Get the last time tracking data was sent.
	 *
	 * @return int|bool
	 */
	private static function get_last_send_time() {
		return apply_filters( 'woocommerce_tracker_last_send_time', get_option( 'woocommerce_tracker_last_send', false ) );
	}

	/**
	 * Test whether this site is a staging site according to the Jetpack criteria.
	 *
	 * With Jetpack 8.1+, Jetpack::is_staging_site has been deprecated.
	 * \Automattic\Jetpack\Status::is_staging_site is the replacement.
	 * However, there are version of JP where \Automattic\Jetpack\Status exists, but does *not* contain is_staging_site method,
	 * so with those, code still needs to use the previous check as a fallback.
	 *
	 * @return bool
	 */
	private static function is_jetpack_staging_site() {
		if ( class_exists( '\Automattic\Jetpack\Status' ) ) {
			// Preferred way of checking with Jetpack 8.1+.
			$jp_status = new \Automattic\Jetpack\Status();
			if ( is_callable( array( $jp_status, 'is_staging_site' ) ) ) {
				return $jp_status->is_staging_site();
			}
		}

		return ( class_exists( 'Jetpack' ) && is_callable( 'Jetpack::is_staging_site' ) && Jetpack::is_staging_site() );
	}

	/**
	 * Get all the tracking data.
	 *
	 * @return array
	 */
	public static function get_tracking_data() {
		$data = array();

		// General site info.
		$data['url']   = home_url();
		$data['email'] = apply_filters( 'woocommerce_tracker_admin_email', get_option( 'admin_email' ) );
		$data['theme'] = self::get_theme_info();

		// WordPress Info.
		$data['wp'] = self::get_wordpress_info();

		// Server Info.
		$data['server'] = self::get_server_info();

		// Plugin info.
		$all_plugins              = self::get_all_plugins();
		$data['active_plugins']   = $all_plugins['active_plugins'];
		$data['inactive_plugins'] = $all_plugins['inactive_plugins'];

		// Jetpack & WooCommerce Connect.

		$data['jetpack_version']    = Constants::is_defined( 'JETPACK__VERSION' ) ? Constants::get_constant( 'JETPACK__VERSION' ) : 'none';
		$data['jetpack_connected']  = ( class_exists( 'Jetpack' ) && is_callable( 'Jetpack::is_active' ) && Jetpack::is_active() ) ? 'yes' : 'no';
		$data['jetpack_is_staging'] = self::is_jetpack_staging_site() ? 'yes' : 'no';
		$data['connect_installed']  = class_exists( 'WC_Connect_Loader' ) ? 'yes' : 'no';
		$data['connect_active']     = ( class_exists( 'WC_Connect_Loader' ) && wp_next_scheduled( 'wc_connect_fetch_service_schemas' ) ) ? 'yes' : 'no';
		$data['helper_connected']   = self::get_helper_connected();

		// Store count info.
		$data['users']      = self::get_user_counts();
		$data['products']   = self::get_product_counts();
		$data['orders']     = self::get_orders();
		$data['reviews']    = self::get_review_counts();
		$data['categories'] = self::get_category_counts();

		// Payment gateway info.
		$data['gateways'] = self::get_active_payment_gateways();

		// Shipping method info.
		$data['shipping_methods'] = self::get_active_shipping_methods();

		// Get all WooCommerce options info.
		$data['settings'] = self::get_all_woocommerce_options_values();

		// Template overrides.
		$data['template_overrides'] = self::get_all_template_overrides();

		// Cart & checkout tech (blocks or shortcodes).
		$data['cart_checkout'] = self::get_cart_checkout_info();

		// WooCommerce Admin info.
		$data['wc_admin_disabled'] = apply_filters( 'woocommerce_admin_disabled', false ) ? 'yes' : 'no';

		// Mobile info.
		$data['wc_mobile_usage'] = self::get_woocommerce_mobile_usage();

		return apply_filters( 'woocommerce_tracker_data', $data );
	}

	/**
	 * Get the current theme info, theme name and version.
	 *
	 * @return array
	 */
	public static function get_theme_info() {
		$theme_data        = wp_get_theme();
		$theme_child_theme = wc_bool_to_string( is_child_theme() );
		$theme_wc_support  = wc_bool_to_string( current_theme_supports( 'woocommerce' ) );

		return array(
			'name'        => $theme_data->Name, // @phpcs:ignore
			'version'     => $theme_data->Version, // @phpcs:ignore
			'child_theme' => $theme_child_theme,
			'wc_support'  => $theme_wc_support,
		);
	}

	/**
	 * Get WordPress related data.
	 *
	 * @return array
	 */
	private static function get_wordpress_info() {
		$wp_data = array();

		$memory = wc_let_to_num( WP_MEMORY_LIMIT );

		if ( function_exists( 'memory_get_usage' ) ) {
			$system_memory = wc_let_to_num( @ini_get( 'memory_limit' ) );
			$memory        = max( $memory, $system_memory );
		}

		// WordPress 5.5+ environment type specification.
		// 'production' is the default in WP, thus using it as a default here, too.
		$environment_type = 'production';
		if ( function_exists( 'wp_get_environment_type' ) ) {
			$environment_type = wp_get_environment_type();
		}

		$wp_data['memory_limit'] = size_format( $memory );
		$wp_data['debug_mode']   = ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ? 'Yes' : 'No';
		$wp_data['locale']       = get_locale();
		$wp_data['version']      = get_bloginfo( 'version' );
		$wp_data['multisite']    = is_multisite() ? 'Yes' : 'No';
		$wp_data['env_type']     = $environment_type;

		return $wp_data;
	}

	/**
	 * Get server related info.
	 *
	 * @return array
	 */
	private static function get_server_info() {
		$server_data = array();

		if ( ! empty( $_SERVER['SERVER_SOFTWARE'] ) ) {
			$server_data['software'] = $_SERVER['SERVER_SOFTWARE']; // @phpcs:ignore
		}

		if ( function_exists( 'phpversion' ) ) {
			$server_data['php_version'] = phpversion();
		}

		if ( function_exists( 'ini_get' ) ) {
			$server_data['php_post_max_size']  = size_format( wc_let_to_num( ini_get( 'post_max_size' ) ) );
			$server_data['php_time_limt']      = ini_get( 'max_execution_time' );
			$server_data['php_max_input_vars'] = ini_get( 'max_input_vars' );
			$server_data['php_suhosin']        = extension_loaded( 'suhosin' ) ? 'Yes' : 'No';
		}

		$database_version             = wc_get_server_database_version();
		$server_data['mysql_version'] = $database_version['number'];

		$server_data['php_max_upload_size']  = size_format( wp_max_upload_size() );
		$server_data['php_default_timezone'] = date_default_timezone_get();
		$server_data['php_soap']             = class_exists( 'SoapClient' ) ? 'Yes' : 'No';
		$server_data['php_fsockopen']        = function_exists( 'fsockopen' ) ? 'Yes' : 'No';
		$server_data['php_curl']             = function_exists( 'curl_init' ) ? 'Yes' : 'No';

		return $server_data;
	}

	/**
	 * Get all plugins grouped into activated or not.
	 *
	 * @return array
	 */
	private static function get_all_plugins() {
		// Ensure get_plugins function is loaded.
		if ( ! function_exists( 'get_plugins' ) ) {
			include ABSPATH . '/wp-admin/includes/plugin.php';
		}

		$plugins             = get_plugins();
		$active_plugins_keys = get_option( 'active_plugins', array() );
		$active_plugins      = array();

		foreach ( $plugins as $k => $v ) {
			// Take care of formatting the data how we want it.
			$formatted         = array();
			$formatted['name'] = strip_tags( $v['Name'] );
			if ( isset( $v['Version'] ) ) {
				$formatted['version'] = strip_tags( $v['Version'] );
			}
			if ( isset( $v['Author'] ) ) {
				$formatted['author'] = strip_tags( $v['Author'] );
			}
			if ( isset( $v['Network'] ) ) {
				$formatted['network'] = strip_tags( $v['Network'] );
			}
			if ( isset( $v['PluginURI'] ) ) {
				$formatted['plugin_uri'] = strip_tags( $v['PluginURI'] );
			}
			if ( in_array( $k, $active_plugins_keys ) ) {
				// Remove active plugins from list so we can show active and inactive separately.
				unset( $plugins[ $k ] );
				$active_plugins[ $k ] = $formatted;
			} else {
				$plugins[ $k ] = $formatted;
			}
		}

		return array(
			'active_plugins'   => $active_plugins,
			'inactive_plugins' => $plugins,
		);
	}

	/**
	 * Check to see if the helper is connected to woocommerce.com
	 *
	 * @return string
	 */
	private static function get_helper_connected() {
		if ( class_exists( 'WC_Helper_Options' ) && is_callable( 'WC_Helper_Options::get' ) ) {
			$authenticated = WC_Helper_Options::get( 'auth' );
		} else {
			$authenticated = '';
		}
		return ( ! empty( $authenticated ) ) ? 'yes' : 'no';
	}


	/**
	 * Get user totals based on user role.
	 *
	 * @return array
	 */
	private static function get_user_counts() {
		$user_count          = array();
		$user_count_data     = count_users();
		$user_count['total'] = $user_count_data['total_users'];

		// Get user count based on user role.
		foreach ( $user_count_data['avail_roles'] as $role => $count ) {
			$user_count[ $role ] = $count;
		}

		return $user_count;
	}

	/**
	 * Get product totals based on product type.
	 *
	 * @return array
	 */
	public static function get_product_counts() {
		$product_count          = array();
		$product_count_data     = wp_count_posts( 'product' );
		$product_count['total'] = $product_count_data->publish;

		$product_statuses = get_terms( 'product_type', array( 'hide_empty' => 0 ) );
		foreach ( $product_statuses as $product_status ) {
			$product_count[ $product_status->name ] = $product_status->count;
		}

		return $product_count;
	}

	/**
	 * Get order counts.
	 *
	 * @return array
	 */
	private static function get_order_counts() {
		$order_count      = array();
		$order_count_data = wp_count_posts( 'shop_order' );
		foreach ( wc_get_order_statuses() as $status_slug => $status_name ) {
			$order_count[ $status_slug ] = $order_count_data->{ $status_slug };
		}
		return $order_count;
	}

	/**
	 * Combine all order data.
	 *
	 * @return array
	 */
	private static function get_orders() {
		$order_dates    = self::get_order_dates();
		$order_counts   = self::get_order_counts();
		$order_totals   = self::get_order_totals();
		$order_gateways = self::get_orders_by_gateway();

		return array_merge( $order_dates, $order_counts, $order_totals, $order_gateways );
	}

	/**
	 * Get order totals.
	 *
	 * @since 5.4.0
	 * @return array
	 */
	private static function get_order_totals() {
		global $wpdb;

		$gross_total = $wpdb->get_var(
			"
			SELECT
				SUM( order_meta.meta_value ) AS 'gross_total'
			FROM {$wpdb->prefix}posts AS orders
			LEFT JOIN {$wpdb->prefix}postmeta AS order_meta ON order_meta.post_id = orders.ID
			WHERE order_meta.meta_key =  '_order_total'
				AND orders.post_status in ( 'wc-completed', 'wc-refunded' )
			GROUP BY order_meta.meta_key
		"
		);

		if ( is_null( $gross_total ) ) {
			$gross_total = 0;
		}

		$processing_gross_total = $wpdb->get_var(
			"
			SELECT
				SUM( order_meta.meta_value ) AS 'gross_total'
			FROM {$wpdb->prefix}posts AS orders
			LEFT JOIN {$wpdb->prefix}postmeta AS order_meta ON order_meta.post_id = orders.ID
			WHERE order_meta.meta_key =  '_order_total'
				AND orders.post_status = 'wc-processing'
			GROUP BY order_meta.meta_key
		"
		);

		if ( is_null( $processing_gross_total ) ) {
			$processing_gross_total = 0;
		}

		return array(
			'gross'            => $gross_total,
			'processing_gross' => $processing_gross_total,
		);
	}

	/**
	 * Get last order date.
	 *
	 * @return string
	 */
	private static function get_order_dates() {
		global $wpdb;

		$min_max = $wpdb->get_row(
			"
			SELECT
				MIN( post_date_gmt ) as 'first', MAX( post_date_gmt ) as 'last'
			FROM {$wpdb->prefix}posts
			WHERE post_type = 'shop_order'
			AND post_status = 'wc-completed'
		",
			ARRAY_A
		);

		if ( is_null( $min_max ) ) {
			$min_max = array(
				'first' => '-',
				'last'  => '-',
			);
		}

		$processing_min_max = $wpdb->get_row(
			"
			SELECT
				MIN( post_date_gmt ) as 'processing_first', MAX( post_date_gmt ) as 'processing_last'
			FROM {$wpdb->prefix}posts
			WHERE post_type = 'shop_order'
			AND post_status = 'wc-processing'
		",
			ARRAY_A
		);

		if ( is_null( $processing_min_max ) ) {
			$processing_min_max = array(
				'processing_first' => '-',
				'processing_last'  => '-',
			);
		}

		return array_merge( $min_max, $processing_min_max );
	}

	/**
	 * Get order details by gateway.
	 *
	 * @return array
	 */
	private static function get_orders_by_gateway() {
		global $wpdb;

		$orders_by_gateway = $wpdb->get_results(
			"
			SELECT
				gateway, currency, SUM(total) AS totals, COUNT(order_id) AS counts
			FROM (
				SELECT
					orders.id AS order_id,
					MAX(CASE WHEN meta_key = '_payment_method' THEN meta_value END) gateway,
					MAX(CASE WHEN meta_key = '_order_total' THEN meta_value END) total,
					MAX(CASE WHEN meta_key = '_order_currency' THEN meta_value END) currency
				FROM
					{$wpdb->prefix}posts orders
				LEFT JOIN
					{$wpdb->prefix}postmeta order_meta ON order_meta.post_id = orders.id
				WHERE orders.post_type = 'shop_order'
					AND orders.post_status in ( 'wc-completed', 'wc-processing', 'wc-refunded' )
					AND meta_key in( '_payment_method','_order_total','_order_currency')
				GROUP BY orders.id
			) order_gateways
			GROUP BY gateway, currency
			"
		);

		$orders_by_gateway_currency = array();
		foreach ( $orders_by_gateway as $orders_details ) {
			$gateway  = 'gateway_' . $orders_details->gateway;
			$currency = $orders_details->currency;
			$count    = $gateway . '_' . $currency . '_count';
			$total    = $gateway . '_' . $currency . '_total';

			$orders_by_gateway_currency[ $count ] = $orders_details->counts;
			$orders_by_gateway_currency[ $total ] = $orders_details->totals;
		}

		return $orders_by_gateway_currency;
	}

	/**
	 * Get review counts for different statuses.
	 *
	 * @return array
	 */
	private static function get_review_counts() {
		global $wpdb;
		$review_count = array( 'total' => 0 );
		$status_map   = array(
			'0'     => 'pending',
			'1'     => 'approved',
			'trash' => 'trash',
			'spam'  => 'spam',
		);
		$counts       = $wpdb->get_results(
			"
			SELECT comment_approved, COUNT(*) AS num_reviews
			FROM {$wpdb->comments}
			WHERE comment_type = 'review'
			GROUP BY comment_approved
			",
			ARRAY_A
		);

		if ( ! $counts ) {
			return $review_count;
		}

		foreach ( $counts as $count ) {
			$status = $count['comment_approved'];
			if ( array_key_exists( $status, $status_map ) ) {
				$review_count[ $status_map[ $status ] ] = $count['num_reviews'];
			}
			$review_count['total'] += $count['num_reviews'];
		}

		return $review_count;
	}

	/**
	 * Get the number of product categories.
	 *
	 * @return int
	 */
	private static function get_category_counts() {
		return wp_count_terms( 'product_cat' );
	}

	/**
	 * Get a list of all active payment gateways.
	 *
	 * @return array
	 */
	private static function get_active_payment_gateways() {
		$active_gateways = array();
		$gateways        = WC()->payment_gateways->payment_gateways();
		foreach ( $gateways as $id => $gateway ) {
			if ( isset( $gateway->enabled ) && 'yes' === $gateway->enabled ) {
				$active_gateways[ $id ] = array(
					'title'    => $gateway->title,
					'supports' => $gateway->supports,
				);
			}
		}

		return $active_gateways;
	}

	/**
	 * Get a list of all active shipping methods.
	 *
	 * @return array
	 */
	private static function get_active_shipping_methods() {
		$active_methods   = array();
		$shipping_methods = WC()->shipping()->get_shipping_methods();
		foreach ( $shipping_methods as $id => $shipping_method ) {
			if ( isset( $shipping_method->enabled ) && 'yes' === $shipping_method->enabled ) {
				$active_methods[ $id ] = array(
					'title'      => $shipping_method->title,
					'tax_status' => $shipping_method->tax_status,
				);
			}
		}

		return $active_methods;
	}

	/**
	 * Get all options starting with woocommerce_ prefix.
	 *
	 * @return array
	 */
	private static function get_all_woocommerce_options_values() {
		return array(
			'version'                               => WC()->version,
			'currency'                              => get_woocommerce_currency(),
			'base_location'                         => WC()->countries->get_base_country(),
			'base_state'                            => WC()->countries->get_base_state(),
			'base_postcode'                         => WC()->countries->get_base_postcode(),
			'selling_locations'                     => WC()->countries->get_allowed_countries(),
			'api_enabled'                           => get_option( 'woocommerce_api_enabled' ),
			'weight_unit'                           => get_option( 'woocommerce_weight_unit' ),
			'dimension_unit'                        => get_option( 'woocommerce_dimension_unit' ),
			'download_method'                       => get_option( 'woocommerce_file_download_method' ),
			'download_require_login'                => get_option( 'woocommerce_downloads_require_login' ),
			'calc_taxes'                            => get_option( 'woocommerce_calc_taxes' ),
			'coupons_enabled'                       => get_option( 'woocommerce_enable_coupons' ),
			'guest_checkout'                        => get_option( 'woocommerce_enable_guest_checkout' ),
			'checkout_login_reminder'               => get_option( 'woocommerce_enable_checkout_login_reminder' ),
			'secure_checkout'                       => get_option( 'woocommerce_force_ssl_checkout' ),
			'enable_signup_and_login_from_checkout' => get_option( 'woocommerce_enable_signup_and_login_from_checkout' ),
			'enable_myaccount_registration'         => get_option( 'woocommerce_enable_myaccount_registration' ),
			'registration_generate_username'        => get_option( 'woocommerce_registration_generate_username' ),
			'registration_generate_password'        => get_option( 'woocommerce_registration_generate_password' ),
		);
	}

	/**
	 * Look for any template override and return filenames.
	 *
	 * @return array
	 */
	private static function get_all_template_overrides() {
		$override_data  = array();
		$template_paths = apply_filters( 'woocommerce_template_overrides_scan_paths', array( 'WooCommerce' => WC()->plugin_path() . '/templates/' ) );
		$scanned_files  = array();

		require_once WC()->plugin_path() . '/includes/admin/class-wc-admin-status.php';

		foreach ( $template_paths as $plugin_name => $template_path ) {
			$scanned_files[ $plugin_name ] = WC_Admin_Status::scan_template_files( $template_path );
		}

		foreach ( $scanned_files as $plugin_name => $files ) {
			foreach ( $files as $file ) {
				if ( file_exists( get_stylesheet_directory() . '/' . $file ) ) {
					$theme_file = get_stylesheet_directory() . '/' . $file;
				} elseif ( file_exists( get_stylesheet_directory() . '/' . WC()->template_path() . $file ) ) {
					$theme_file = get_stylesheet_directory() . '/' . WC()->template_path() . $file;
				} elseif ( file_exists( get_template_directory() . '/' . $file ) ) {
					$theme_file = get_template_directory() . '/' . $file;
				} elseif ( file_exists( get_template_directory() . '/' . WC()->template_path() . $file ) ) {
					$theme_file = get_template_directory() . '/' . WC()->template_path() . $file;
				} else {
					$theme_file = false;
				}

				if ( false !== $theme_file ) {
					$override_data[] = basename( $theme_file );
				}
			}
		}
		return $override_data;
	}

	/**
	 * Search a specific post for text content.
	 *
	 * @param integer $post_id The id of the post to search.
	 * @param string  $text    The text to search for.
	 * @return string 'Yes' if post contains $text (otherwise 'No').
	 */
	public static function post_contains_text( $post_id, $text ) {
		global $wpdb;

		// Search for the text anywhere in the post.
		$wildcarded = "%{$text}%";

		$result = $wpdb->get_var(
			$wpdb->prepare(
				"
				SELECT COUNT( * ) FROM {$wpdb->prefix}posts
				WHERE ID=%d
				AND {$wpdb->prefix}posts.post_content LIKE %s
				",
				array( $post_id, $wildcarded )
			)
		);

		return ( '0' !== $result ) ? 'Yes' : 'No';
	}


	/**
	 * Get tracker data for a specific block type on a woocommerce page.
	 *
	 * @param string $block_name The name (id) of a block, e.g. `woocommerce/cart`.
	 * @param string $woo_page_name The woo page to search, e.g. `cart`.
	 * @return array Associative array of tracker data with keys:
	 * - page_contains_block
	 * - block_attributes
	 */
	public static function get_block_tracker_data( $block_name, $woo_page_name ) {
		$blocks = WC_Blocks_Utils::get_blocks_from_page( $block_name, $woo_page_name );

		$block_present = false;
		$attributes    = array();
		if ( $blocks && count( $blocks ) ) {
			// Return any customised attributes from the first block.
			$block_present = true;
			$attributes    = $blocks[0]['attrs'];
		}

		return array(
			'page_contains_block' => $block_present ? 'Yes' : 'No',
			'block_attributes'    => $attributes,
		);
	}

	/**
	 * Get info about the cart & checkout pages.
	 *
	 * @return array
	 */
	public static function get_cart_checkout_info() {
		$cart_page_id     = wc_get_page_id( 'cart' );
		$checkout_page_id = wc_get_page_id( 'checkout' );

		$cart_block_data     = self::get_block_tracker_data( 'woocommerce/cart', 'cart' );
		$checkout_block_data = self::get_block_tracker_data( 'woocommerce/checkout', 'checkout' );

		return array(
			'cart_page_contains_cart_shortcode'         => self::post_contains_text(
				$cart_page_id,
				'[woocommerce_cart]'
			),
			'checkout_page_contains_checkout_shortcode' => self::post_contains_text(
				$checkout_page_id,
				'[woocommerce_checkout]'
			),

			'cart_page_contains_cart_block'             => $cart_block_data['page_contains_block'],
			'cart_block_attributes'                     => $cart_block_data['block_attributes'],
			'checkout_page_contains_checkout_block'     => $checkout_block_data['page_contains_block'],
			'checkout_block_attributes'                 => $checkout_block_data['block_attributes'],
		);
	}

	/**
	 * Get info about WooCommerce Mobile App usage
	 *
	 * @return array
	 */
	public static function get_woocommerce_mobile_usage() {
		return get_option( 'woocommerce_mobile_app_usage' );
	}
}

WC_Tracker::init();
class-wc-order-factory.php000064400000006172151336044760011566 0ustar00<?php
/**
 * Order Factory
 *
 * The WooCommerce order factory creating the right order objects.
 *
 * @version 3.0.0
 * @package WooCommerce\Classes
 */

defined( 'ABSPATH' ) || exit;

/**
 * Order factory class
 */
class WC_Order_Factory {

	/**
	 * Get order.
	 *
	 * @param  mixed $order_id (default: false) Order ID to get.
	 * @return WC_Order|bool
	 */
	public static function get_order( $order_id = false ) {
		$order_id = self::get_order_id( $order_id );

		if ( ! $order_id ) {
			return false;
		}

		$order_type      = WC_Data_Store::load( 'order' )->get_order_type( $order_id );
		$order_type_data = wc_get_order_type( $order_type );
		if ( $order_type_data ) {
			$classname = $order_type_data['class_name'];
		} else {
			$classname = false;
		}

		// Filter classname so that the class can be overridden if extended.
		$classname = apply_filters( 'woocommerce_order_class', $classname, $order_type, $order_id );

		if ( ! class_exists( $classname ) ) {
			return false;
		}

		try {
			return new $classname( $order_id );
		} catch ( Exception $e ) {
			wc_caught_exception( $e, __FUNCTION__, array( $order_id ) );
			return false;
		}
	}

	/**
	 * Get order item.
	 *
	 * @param int $item_id Order item ID to get.
	 * @return WC_Order_Item|false if not found
	 */
	public static function get_order_item( $item_id = 0 ) {
		if ( is_numeric( $item_id ) ) {
			$item_type = WC_Data_Store::load( 'order-item' )->get_order_item_type( $item_id );
			$id        = $item_id;
		} elseif ( $item_id instanceof WC_Order_Item ) {
			$item_type = $item_id->get_type();
			$id        = $item_id->get_id();
		} elseif ( is_object( $item_id ) && ! empty( $item_id->order_item_type ) ) {
			$id        = $item_id->order_item_id;
			$item_type = $item_id->order_item_type;
		} else {
			$item_type = false;
			$id        = false;
		}

		if ( $id && $item_type ) {
			$classname = false;
			switch ( $item_type ) {
				case 'line_item':
				case 'product':
					$classname = 'WC_Order_Item_Product';
					break;
				case 'coupon':
					$classname = 'WC_Order_Item_Coupon';
					break;
				case 'fee':
					$classname = 'WC_Order_Item_Fee';
					break;
				case 'shipping':
					$classname = 'WC_Order_Item_Shipping';
					break;
				case 'tax':
					$classname = 'WC_Order_Item_Tax';
					break;
			}

			$classname = apply_filters( 'woocommerce_get_order_item_classname', $classname, $item_type, $id );

			if ( $classname && class_exists( $classname ) ) {
				try {
					return new $classname( $id );
				} catch ( Exception $e ) {
					return false;
				}
			}
		}
		return false;
	}

	/**
	 * Get the order ID depending on what was passed.
	 *
	 * @since 3.0.0
	 * @param  mixed $order Order data to convert to an ID.
	 * @return int|bool false on failure
	 */
	public static function get_order_id( $order ) {
		global $post;

		if ( false === $order && is_a( $post, 'WP_Post' ) && 'shop_order' === get_post_type( $post ) ) {
			return absint( $post->ID );
		} elseif ( is_numeric( $order ) ) {
			return $order;
		} elseif ( $order instanceof WC_Abstract_Order ) {
			return $order->get_id();
		} elseif ( ! empty( $order->ID ) ) {
			return $order->ID;
		} else {
			return false;
		}
	}
}
class-wc-regenerate-images.php000064400000036445151336044760012400 0ustar00<?php
/**
 * Regenerate Images Functionality
 *
 * All functionality pertaining to regenerating product images in realtime.
 *
 * @package WooCommerce\Classes
 * @version 3.5.0
 * @since   3.3.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Regenerate Images Class
 */
class WC_Regenerate_Images {

	/**
	 * Background process to regenerate all images
	 *
	 * @var WC_Regenerate_Images_Request
	 */
	protected static $background_process;

	/**
	 * Stores size being generated on the fly.
	 *
	 * @var string
	 */
	protected static $regenerate_size;

	/**
	 * Init function
	 */
	public static function init() {
		add_action( 'image_get_intermediate_size', array( __CLASS__, 'filter_image_get_intermediate_size' ), 10, 3 );
		add_filter( 'wp_generate_attachment_metadata', array( __CLASS__, 'add_uncropped_metadata' ) );
		add_filter( 'wp_get_attachment_image_src', array( __CLASS__, 'maybe_resize_image' ), 10, 4 );

		// Not required when Jetpack Photon is in use.
		if ( method_exists( 'Jetpack', 'is_module_active' ) && Jetpack::is_module_active( 'photon' ) ) {
			return;
		}

		if ( apply_filters( 'woocommerce_background_image_regeneration', true ) ) {
			include_once WC_ABSPATH . 'includes/class-wc-regenerate-images-request.php';

			self::$background_process = new WC_Regenerate_Images_Request();

			add_action( 'admin_init', array( __CLASS__, 'regenerating_notice' ) );
			add_action( 'woocommerce_hide_regenerating_thumbnails_notice', array( __CLASS__, 'dismiss_regenerating_notice' ) );

			// Regenerate thumbnails in the background after settings changes. Not ran on multisite to avoid multiple simultanious jobs.
			if ( ! is_multisite() ) {
				add_action( 'customize_save_after', array( __CLASS__, 'maybe_regenerate_images' ) );
				add_action( 'after_switch_theme', array( __CLASS__, 'maybe_regenerate_images' ) );
			}
		}
	}

	/**
	 * If an intermediate size meta differs from the actual image size (settings were changed?) return false so the wrong size is not used.
	 *
	 * @param array  $data Size data.
	 * @param int    $attachment_id Attachment ID.
	 * @param string $size Size name.
	 * @return array
	 */
	public static function filter_image_get_intermediate_size( $data, $attachment_id, $size ) {
		if ( ! is_string( $size ) || ! in_array( $size, apply_filters( 'woocommerce_image_sizes_to_resize', array( 'woocommerce_thumbnail', 'woocommerce_gallery_thumbnail', 'woocommerce_single', 'shop_thumbnail', 'shop_catalog', 'shop_single' ) ), true ) ) {
			return $data;
		}

		// If we don't have sizes, we cannot proceed.
		if ( ! isset( $data['width'], $data['height'] ) ) {
			return $data;
		}

		// See if the image size has changed from our settings.
		if ( ! self::image_size_matches_settings( $data, $size ) ) {
			// If Photon is running we can just return false and let Jetpack handle regeneration.
			if ( method_exists( 'Jetpack', 'is_module_active' ) && Jetpack::is_module_active( 'photon' ) ) {
				return false;
			} else {
				// If we get here, Jetpack is not running and we don't have the correct image sized stored. Try to return closest match.
				$size_data = wc_get_image_size( $size );
				return image_get_intermediate_size( $attachment_id, array( absint( $size_data['width'] ), absint( $size_data['height'] ) ) );
			}
		}
		return $data;
	}

	/**
	 * We need to track if uncropped was on or off when generating the images.
	 *
	 * @param array $meta_data Array of meta data.
	 * @return array
	 */
	public static function add_uncropped_metadata( $meta_data ) {
		$size_data = wc_get_image_size( 'woocommerce_thumbnail' );
		if ( isset( $meta_data['sizes'], $meta_data['sizes']['woocommerce_thumbnail'] ) ) {
			$meta_data['sizes']['woocommerce_thumbnail']['uncropped'] = empty( $size_data['height'] );
		}
		return $meta_data;
	}

	/**
	 * See if an image's dimensions match actual settings.
	 *
	 * @param array  $image Image dimensions array.
	 * @param string $size Named size.
	 * @return bool True if they match. False if they do not (may trigger regen).
	 */
	protected static function image_size_matches_settings( $image, $size ) {
		$target_size = wc_get_image_size( $size );
		$uncropped   = '' === $target_size['width'] || '' === $target_size['height'];

		if ( ! $uncropped ) {
			$ratio_match = wp_image_matches_ratio( $image['width'], $image['height'], $target_size['width'], $target_size['height'] );

			// Size is invalid if the widths or crop setting don't match.
			if ( $ratio_match && $target_size['width'] !== $image['width'] ) {
				return false;
			}

			// Size is invalid if the heights don't match.
			if ( $ratio_match && $target_size['height'] && $target_size['height'] !== $image['height'] ) {
				return false;
			}
		}

		// If cropping mode has changed, regenerate the image.
		if ( $uncropped && empty( $image['uncropped'] ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Show notice when job is running in background.
	 */
	public static function regenerating_notice() {
		if ( ! self::$background_process->is_running() ) {
			WC_Admin_Notices::add_notice( 'regenerating_thumbnails' );
		} else {
			WC_Admin_Notices::remove_notice( 'regenerating_thumbnails' );
		}
	}

	/**
	 * Dismiss notice and cancel jobs.
	 */
	public static function dismiss_regenerating_notice() {
		if ( self::$background_process ) {
			self::$background_process->kill_process();

			$log = wc_get_logger();
			$log->info(
				__( 'Cancelled product image regeneration job.', 'woocommerce' ),
				array(
					'source' => 'wc-image-regeneration',
				)
			);
		}
		WC_Admin_Notices::remove_notice( 'regenerating_thumbnails' );
	}

	/**
	 * Regenerate images if the settings have changed since last re-generation.
	 *
	 * @return void
	 */
	public static function maybe_regenerate_images() {
		$size_hash = md5(
			wp_json_encode(
				array(
					wc_get_image_size( 'thumbnail' ),
					wc_get_image_size( 'single' ),
					wc_get_image_size( 'gallery_thumbnail' ),
				)
			)
		);

		if ( update_option( 'woocommerce_maybe_regenerate_images_hash', $size_hash ) ) {
			// Size settings have changed. Trigger regen.
			self::queue_image_regeneration();
		}
	}

	/**
	 * Check if we should maybe generate a new image size if not already there.
	 *
	 * @param array        $image Properties of the image.
	 * @param int          $attachment_id Attachment ID.
	 * @param string|array $size Image size.
	 * @param bool         $icon If icon or not.
	 * @return array
	 */
	public static function maybe_resize_image( $image, $attachment_id, $size, $icon ) {
		if ( ! apply_filters( 'woocommerce_resize_images', true ) ) {
			return $image;
		}

		// List of sizes we want to resize. Ignore others.
		if ( ! $image || ! in_array( $size, apply_filters( 'woocommerce_image_sizes_to_resize', array( 'woocommerce_thumbnail', 'woocommerce_gallery_thumbnail', 'woocommerce_single', 'shop_thumbnail', 'shop_catalog', 'shop_single' ) ), true ) ) {
			return $image;
		}

		$target_size      = wc_get_image_size( $size );
		$image_width      = $image[1];
		$image_height     = $image[2];
		$ratio_match      = false;
		$target_uncropped = '' === $target_size['width'] || '' === $target_size['height'] || ! $target_size['crop'];

		// If '' is passed to either size, we test ratios against the original file. It's uncropped.
		if ( $target_uncropped ) {
			$full_size = self::get_full_size_image_dimensions( $attachment_id );

			if ( ! $full_size || ! $full_size['width'] || ! $full_size['height'] ) {
				return $image;
			}

			$ratio_match = wp_image_matches_ratio( $image_width, $image_height, $full_size['width'], $full_size['height'] );
		} else {
			$ratio_match = wp_image_matches_ratio( $image_width, $image_height, $target_size['width'], $target_size['height'] );
		}

		if ( ! $ratio_match ) {
			$full_size = self::get_full_size_image_dimensions( $attachment_id );

			if ( ! $full_size ) {
				return $image;
			}

			// Check if the actual image has a larger dimension than the requested image size. Smaller images are not zoom-cropped.
			if ( $image_width === $target_size['width'] && $full_size['height'] < $target_size['height'] ) {
				return $image;
			}

			if ( $image_height === $target_size['height'] && $full_size['width'] < $target_size['width'] ) {
				return $image;
			}

			// If the full size image is smaller both ways, don't scale it up.
			if ( $full_size['height'] < $target_size['height'] && $full_size['width'] < $target_size['width'] ) {
				return $image;
			}

			return self::resize_and_return_image( $attachment_id, $image, $size, $icon );
		}

		return $image;
	}

	/**
	 * Get full size image dimensions.
	 *
	 * @param int $attachment_id Attachment ID of image.
	 * @return array Width and height. Empty array if the dimensions cannot be found.
	 */
	private static function get_full_size_image_dimensions( $attachment_id ) {
		$imagedata = wp_get_attachment_metadata( $attachment_id );

		if ( ! $imagedata ) {
			return array();
		}

		if ( ! isset( $imagedata['file'] ) && isset( $imagedata['sizes']['full'] ) ) {
			$imagedata['height'] = $imagedata['sizes']['full']['height'];
			$imagedata['width']  = $imagedata['sizes']['full']['width'];
		}

		return array(
			'width'  => $imagedata['width'],
			'height' => $imagedata['height'],
		);
	}

	/**
	 * Ensure we are dealing with the correct image attachment
	 *
	 * @param int|WP_Post $attachment Attachment object or ID.
	 * @return boolean
	 */
	public static function is_regeneratable( $attachment ) {
		if ( 'site-icon' === get_post_meta( is_object( $attachment ) ? $attachment->ID : $attachment, '_wp_attachment_context', true ) ) {
			return false;
		}

		if ( wp_attachment_is_image( $attachment ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Only regenerate images for the requested size.
	 *
	 * @param array $sizes Array of image sizes.
	 * @return array
	 */
	public static function adjust_intermediate_image_sizes( $sizes ) {
		return array( self::$regenerate_size );
	}

	/**
	 * Generate the thumbnail filename and dimensions for a given file.
	 *
	 * @param string $fullsizepath Path to full size image.
	 * @param int    $thumbnail_width  The width of the thumbnail.
	 * @param int    $thumbnail_height The height of the thumbnail.
	 * @param bool   $crop             Whether to crop or not.
	 * @return array|false An array of the filename, thumbnail width, and thumbnail height, or false on failure to resize such as the thumbnail being larger than the fullsize image.
	 */
	private static function get_image( $fullsizepath, $thumbnail_width, $thumbnail_height, $crop ) {
		list( $fullsize_width, $fullsize_height ) = getimagesize( $fullsizepath );

		$dimensions = image_resize_dimensions( $fullsize_width, $fullsize_height, $thumbnail_width, $thumbnail_height, $crop );
		$editor     = wp_get_image_editor( $fullsizepath );

		if ( is_wp_error( $editor ) ) {
			return false;
		}

		if ( ! $dimensions || ! is_array( $dimensions ) ) {
			return false;
		}

		list( , , , , $dst_w, $dst_h ) = $dimensions;
		$suffix                        = "{$dst_w}x{$dst_h}";
		$file_ext                      = strtolower( pathinfo( $fullsizepath, PATHINFO_EXTENSION ) );

		return array(
			'filename' => $editor->generate_filename( $suffix, null, $file_ext ),
			'width'    => $dst_w,
			'height'   => $dst_h,
		);
	}

	/**
	 * Regenerate the image according to the required size
	 *
	 * @param int    $attachment_id Attachment ID.
	 * @param array  $image Original Image.
	 * @param string $size Size to return for new URL.
	 * @param bool   $icon If icon or not.
	 * @return string
	 */
	private static function resize_and_return_image( $attachment_id, $image, $size, $icon ) {
		if ( ! self::is_regeneratable( $attachment_id ) ) {
			return $image;
		}

		$fullsizepath = get_attached_file( $attachment_id );

		if ( false === $fullsizepath || is_wp_error( $fullsizepath ) || ! file_exists( $fullsizepath ) ) {
			return $image;
		}

		if ( ! function_exists( 'wp_crop_image' ) ) {
			include ABSPATH . 'wp-admin/includes/image.php';
		}

		self::$regenerate_size = is_customize_preview() ? $size . '_preview' : $size;

		if ( is_customize_preview() ) {
			$image_size = wc_get_image_size( $size );

			// Make sure registered image size matches the size we're requesting.
			add_image_size( self::$regenerate_size, absint( $image_size['width'] ), absint( $image_size['height'] ), $image_size['crop'] );

			$thumbnail = self::get_image( $fullsizepath, absint( $image_size['width'] ), absint( $image_size['height'] ), $image_size['crop'] );

			// If the file is already there perhaps just load it if we're using the customizer. No need to store in meta data.
			if ( $thumbnail && file_exists( $thumbnail['filename'] ) ) {
				$wp_uploads     = wp_upload_dir( null, false );
				$wp_uploads_dir = $wp_uploads['basedir'];
				$wp_uploads_url = $wp_uploads['baseurl'];

				return array(
					0 => str_replace( $wp_uploads_dir, $wp_uploads_url, $thumbnail['filename'] ),
					1 => $thumbnail['width'],
					2 => $thumbnail['height'],
				);
			}
		}

		$metadata = wp_get_attachment_metadata( $attachment_id );

		// Fix for images with no metadata.
		if ( ! is_array( $metadata ) ) {
			$metadata = array();
		}

		// We only want to regen a specific image size.
		add_filter( 'intermediate_image_sizes', array( __CLASS__, 'adjust_intermediate_image_sizes' ) );

		// This function will generate the new image sizes.
		$new_metadata = wp_generate_attachment_metadata( $attachment_id, $fullsizepath );

		// Remove custom filter.
		remove_filter( 'intermediate_image_sizes', array( __CLASS__, 'adjust_intermediate_image_sizes' ) );

		// If something went wrong lets just return the original image.
		if ( is_wp_error( $new_metadata ) || empty( $new_metadata ) ) {
			return $image;
		}

		if ( isset( $new_metadata['sizes'][ self::$regenerate_size ] ) ) {
			$metadata['sizes'][ self::$regenerate_size ] = $new_metadata['sizes'][ self::$regenerate_size ];
			wp_update_attachment_metadata( $attachment_id, $metadata );
		}

		// Now we've done our regen, attempt to return the new size.
		$new_image = self::unfiltered_image_downsize( $attachment_id, self::$regenerate_size );

		return $new_image ? $new_image : $image;
	}

	/**
	 * Image downsize, without this classes filtering on the results.
	 *
	 * @param int    $attachment_id Attachment ID.
	 * @param string $size Size to downsize to.
	 * @return string New image URL.
	 */
	private static function unfiltered_image_downsize( $attachment_id, $size ) {
		remove_action( 'image_get_intermediate_size', array( __CLASS__, 'filter_image_get_intermediate_size' ), 10, 3 );

		$return = image_downsize( $attachment_id, $size );

		add_action( 'image_get_intermediate_size', array( __CLASS__, 'filter_image_get_intermediate_size' ), 10, 3 );

		return $return;
	}

	/**
	 * Get list of images and queue them for regeneration
	 *
	 * @return void
	 */
	public static function queue_image_regeneration() {
		global $wpdb;
		// First lets cancel existing running queue to avoid running it more than once.
		self::$background_process->kill_process();

		// Now lets find all product image attachments IDs and pop them onto the queue.
		$images = $wpdb->get_results( // @codingStandardsIgnoreLine
			"SELECT ID
			FROM $wpdb->posts
			WHERE post_type = 'attachment'
			AND post_mime_type LIKE 'image/%'
			ORDER BY ID DESC"
		);
		foreach ( $images as $image ) {
			self::$background_process->push_to_queue(
				array(
					'attachment_id' => $image->ID,
				)
			);
		}

		// Lets dispatch the queue to start processing.
		self::$background_process->save()->dispatch();
	}
}

add_action( 'init', array( 'WC_Regenerate_Images', 'init' ) );
class-wc-order-item-meta.php000064400000013460151336044760011777 0ustar00<?php
/**
 * Order Item Meta
 *
 * A Simple class for managing order item meta so plugins add it in the correct format.
 *
 * @package     WooCommerce\Classes
 * @deprecated  3.0.0 wc_display_item_meta function is used instead.
 * @version     2.4
 */

defined( 'ABSPATH' ) || exit;

/**
 * Order item meta class.
 */
class WC_Order_Item_Meta {

	/**
	 * For handling backwards compatibility.
	 *
	 * @var bool
	 */
	private $legacy = false;

	/**
	 * Order item
	 *
	 * @var array|null
	 */
	private $item = null;

	/**
	 * Post meta data
	 *
	 * @var array|null
	 */
	public $meta = null;

	/**
	 * Product object.
	 *
	 * @var WC_Product|null
	 */
	public $product = null;

	/**
	 * Constructor.
	 *
	 * @param array       $item defaults to array().
	 * @param \WC_Product $product defaults to null.
	 */
	public function __construct( $item = array(), $product = null ) {
		wc_deprecated_function( 'WC_Order_Item_Meta::__construct', '3.1', 'WC_Order_Item_Product' );

		// Backwards (pre 2.4) compatibility.
		if ( ! isset( $item['item_meta'] ) ) {
			$this->legacy = true;
			$this->meta   = array_filter( (array) $item );
			return;
		}
		$this->item    = $item;
		$this->meta    = array_filter( (array) $item['item_meta'] );
		$this->product = $product;
	}

	/**
	 * Display meta in a formatted list.
	 *
	 * @param bool   $flat       Flat (default: false).
	 * @param bool   $return     Return (default: false).
	 * @param string $hideprefix Hide prefix (default: _).
	 * @param  string $delimiter Delimiter used to separate items when $flat is true.
	 * @return string|void
	 */
	public function display( $flat = false, $return = false, $hideprefix = '_', $delimiter = ", \n" ) {
		$output         = '';
		$formatted_meta = $this->get_formatted( $hideprefix );

		if ( ! empty( $formatted_meta ) ) {
			$meta_list = array();

			foreach ( $formatted_meta as $meta ) {
				if ( $flat ) {
					$meta_list[] = wp_kses_post( $meta['label'] . ': ' . $meta['value'] );
				} else {
					$meta_list[] = '
						<dt class="variation-' . sanitize_html_class( sanitize_text_field( $meta['key'] ) ) . '">' . wp_kses_post( $meta['label'] ) . ':</dt>
						<dd class="variation-' . sanitize_html_class( sanitize_text_field( $meta['key'] ) ) . '">' . wp_kses_post( wpautop( make_clickable( $meta['value'] ) ) ) . '</dd>
					';
				}
			}

			if ( ! empty( $meta_list ) ) {
				if ( $flat ) {
					$output .= implode( $delimiter, $meta_list );
				} else {
					$output .= '<dl class="variation">' . implode( '', $meta_list ) . '</dl>';
				}
			}
		}

		$output = apply_filters( 'woocommerce_order_items_meta_display', $output, $this, $flat );

		if ( $return ) {
			return $output;
		} else {
			echo $output; // WPCS: XSS ok.
		}
	}

	/**
	 * Return an array of formatted item meta in format e.g.
	 *
	 * Returns: array(
	 *   'pa_size' => array(
	 *     'label' => 'Size',
	 *     'value' => 'Medium',
	 *   )
	 * )
	 *
	 * @since 2.4
	 * @param string $hideprefix exclude meta when key is prefixed with this, defaults to '_'.
	 * @return array
	 */
	public function get_formatted( $hideprefix = '_' ) {
		if ( $this->legacy ) {
			return $this->get_formatted_legacy( $hideprefix );
		}

		$formatted_meta = array();

		if ( ! empty( $this->item['item_meta_array'] ) ) {
			foreach ( $this->item['item_meta_array'] as $meta_id => $meta ) {
				if ( '' === $meta->value || is_serialized( $meta->value ) || ( ! empty( $hideprefix ) && substr( $meta->key, 0, 1 ) === $hideprefix ) ) {
					continue;
				}

				$attribute_key = urldecode( str_replace( 'attribute_', '', $meta->key ) );
				$meta_value    = $meta->value;

				// If this is a term slug, get the term's nice name.
				if ( taxonomy_exists( $attribute_key ) ) {
					$term = get_term_by( 'slug', $meta_value, $attribute_key );

					if ( ! is_wp_error( $term ) && is_object( $term ) && $term->name ) {
						$meta_value = $term->name;
					}
				}

				$formatted_meta[ $meta_id ] = array(
					'key'   => $meta->key,
					'label' => wc_attribute_label( $attribute_key, $this->product ),
					'value' => apply_filters( 'woocommerce_order_item_display_meta_value', $meta_value, $meta, $this->item ),
				);
			}
		}

		return apply_filters( 'woocommerce_order_items_meta_get_formatted', $formatted_meta, $this );
	}

	/**
	 * Return an array of formatted item meta in format e.g.
	 * Handles @deprecated args.
	 *
	 * @param string $hideprefix Hide prefix.
	 *
	 * @return array
	 */
	public function get_formatted_legacy( $hideprefix = '_' ) {
		if ( ! is_ajax() ) {
			wc_deprecated_argument( 'WC_Order_Item_Meta::get_formatted', '2.4', 'Item Meta Data is being called with legacy arguments' );
		}

		$formatted_meta = array();

		foreach ( $this->meta as $meta_key => $meta_values ) {
			if ( empty( $meta_values ) || ( ! empty( $hideprefix ) && substr( $meta_key, 0, 1 ) === $hideprefix ) ) {
				continue;
			}
			foreach ( (array) $meta_values as $meta_value ) {
				// Skip serialised meta.
				if ( is_serialized( $meta_value ) ) {
					continue;
				}

				$attribute_key = urldecode( str_replace( 'attribute_', '', $meta_key ) );

				// If this is a term slug, get the term's nice name.
				if ( taxonomy_exists( $attribute_key ) ) {
					$term = get_term_by( 'slug', $meta_value, $attribute_key );
					if ( ! is_wp_error( $term ) && is_object( $term ) && $term->name ) {
						$meta_value = $term->name;
					}
				}

				// Unique key required.
				$formatted_meta_key = $meta_key;
				$loop               = 0;
				while ( isset( $formatted_meta[ $formatted_meta_key ] ) ) {
					$loop ++;
					$formatted_meta_key = $meta_key . '-' . $loop;
				}

				$formatted_meta[ $formatted_meta_key ] = array(
					'key'   => $meta_key,
					'label' => wc_attribute_label( $attribute_key, $this->product ),
					'value' => apply_filters( 'woocommerce_order_item_display_meta_value', $meta_value, $this->meta, $this->item ),
				);
			}
		}

		return $formatted_meta;
	}
}
class-wc-regenerate-images-request.php000064400000020333151336044760014053 0ustar00<?php
/**
 * All functionality to regenerate images in the background when settings change.
 *
 * @package WooCommerce\Classes
 * @version 3.3.0
 * @since   3.3.0
 */

defined( 'ABSPATH' ) || exit;

if ( ! class_exists( 'WC_Background_Process', false ) ) {
	include_once dirname( __FILE__ ) . '/abstracts/class-wc-background-process.php';
}

/**
 * Class that extends WC_Background_Process to process image regeneration in the background.
 */
class WC_Regenerate_Images_Request extends WC_Background_Process {

	/**
	 * Stores the attachment ID being processed.
	 *
	 * @var integer
	 */
	protected $attachment_id = 0;

	/**
	 * Initiate new background process.
	 */
	public function __construct() {
		// Uses unique prefix per blog so each blog has separate queue.
		$this->prefix = 'wp_' . get_current_blog_id();
		$this->action = 'wc_regenerate_images';

		// This is needed to prevent timeouts due to threading. See https://core.trac.wordpress.org/ticket/36534.
		@putenv( 'MAGICK_THREAD_LIMIT=1' ); // @codingStandardsIgnoreLine.

		parent::__construct();
	}

	/**
	 * Is job running?
	 *
	 * @return boolean
	 */
	public function is_running() {
		return $this->is_queue_empty();
	}

	/**
	 * Limit each task ran per batch to 1 for image regen.
	 *
	 * @return bool
	 */
	protected function batch_limit_exceeded() {
		return true;
	}

	/**
	 * Determines whether an attachment can have its thumbnails regenerated.
	 *
	 * Adapted from Regenerate Thumbnails by Alex Mills.
	 *
	 * @param WP_Post $attachment An attachment's post object.
	 * @return bool Whether the given attachment can have its thumbnails regenerated.
	 */
	protected function is_regeneratable( $attachment ) {
		if ( 'site-icon' === get_post_meta( $attachment->ID, '_wp_attachment_context', true ) ) {
			return false;
		}

		if ( wp_attachment_is_image( $attachment ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Code to execute for each item in the queue
	 *
	 * @param mixed $item Queue item to iterate over.
	 * @return bool
	 */
	protected function task( $item ) {
		if ( ! is_array( $item ) && ! isset( $item['attachment_id'] ) ) {
			return false;
		}

		$this->attachment_id = absint( $item['attachment_id'] );
		$attachment          = get_post( $this->attachment_id );

		if ( ! $attachment || 'attachment' !== $attachment->post_type || ! $this->is_regeneratable( $attachment ) ) {
			return false;
		}

		if ( ! function_exists( 'wp_crop_image' ) ) {
			include ABSPATH . 'wp-admin/includes/image.php';
		}

		$log = wc_get_logger();

		$log->info(
			sprintf(
				// translators: %s: ID of the attachment.
				__( 'Regenerating images for attachment ID: %s', 'woocommerce' ),
				$this->attachment_id
			),
			array(
				'source' => 'wc-image-regeneration',
			)
		);

		$fullsizepath = get_attached_file( $this->attachment_id );

		// Check if the file exists, if not just remove item from queue.
		if ( false === $fullsizepath || is_wp_error( $fullsizepath ) || ! file_exists( $fullsizepath ) ) {
			return false;
		}

		$old_metadata = wp_get_attachment_metadata( $this->attachment_id );

		// We only want to regen WC images.
		add_filter( 'intermediate_image_sizes', array( $this, 'adjust_intermediate_image_sizes' ) );

		// We only want to resize images if they do not already exist.
		add_filter( 'intermediate_image_sizes_advanced', array( $this, 'filter_image_sizes_to_only_missing_thumbnails' ), 10, 3 );

		// This function will generate the new image sizes.
		$new_metadata = wp_generate_attachment_metadata( $this->attachment_id, $fullsizepath );

		// Remove custom filters.
		remove_filter( 'intermediate_image_sizes', array( $this, 'adjust_intermediate_image_sizes' ) );
		remove_filter( 'intermediate_image_sizes_advanced', array( $this, 'filter_image_sizes_to_only_missing_thumbnails' ), 10, 3 );

		// If something went wrong lets just remove the item from the queue.
		if ( is_wp_error( $new_metadata ) || empty( $new_metadata ) ) {
			return false;
		}

		if ( ! empty( $old_metadata ) && ! empty( $old_metadata['sizes'] ) && is_array( $old_metadata['sizes'] ) ) {
			foreach ( $old_metadata['sizes'] as $old_size => $old_size_data ) {
				if ( empty( $new_metadata['sizes'][ $old_size ] ) ) {
					$new_metadata['sizes'][ $old_size ] = $old_metadata['sizes'][ $old_size ];
				}
			}
			// Handle legacy sizes.
			if ( isset( $new_metadata['sizes']['shop_thumbnail'], $new_metadata['sizes']['woocommerce_gallery_thumbnail'] ) ) {
				$new_metadata['sizes']['shop_thumbnail'] = $new_metadata['sizes']['woocommerce_gallery_thumbnail'];
			}
			if ( isset( $new_metadata['sizes']['shop_catalog'], $new_metadata['sizes']['woocommerce_thumbnail'] ) ) {
				$new_metadata['sizes']['shop_catalog'] = $new_metadata['sizes']['woocommerce_thumbnail'];
			}
			if ( isset( $new_metadata['sizes']['shop_single'], $new_metadata['sizes']['woocommerce_single'] ) ) {
				$new_metadata['sizes']['shop_single'] = $new_metadata['sizes']['woocommerce_single'];
			}
		}

		// Update the meta data with the new size values.
		wp_update_attachment_metadata( $this->attachment_id, $new_metadata );

		// We made it till the end, now lets remove the item from the queue.
		return false;
	}

	/**
	 * Filters the list of thumbnail sizes to only include those which have missing files.
	 *
	 * @param array $sizes         An associative array of registered thumbnail image sizes.
	 * @param array $metadata      An associative array of fullsize image metadata: width, height, file.
	 * @param int   $attachment_id Attachment ID. Only passed from WP 5.0+.
	 * @return array An associative array of image sizes.
	 */
	public function filter_image_sizes_to_only_missing_thumbnails( $sizes, $metadata, $attachment_id = null ) {
		$attachment_id = is_null( $attachment_id ) ? $this->attachment_id : $attachment_id;

		if ( ! $sizes || ! $attachment_id ) {
			return $sizes;
		}

		$fullsizepath = get_attached_file( $attachment_id );
		$editor       = wp_get_image_editor( $fullsizepath );

		if ( is_wp_error( $editor ) ) {
			return $sizes;
		}

		$metadata = wp_get_attachment_metadata( $attachment_id );

		// This is based on WP_Image_Editor_GD::multi_resize() and others.
		foreach ( $sizes as $size => $size_data ) {
			if ( empty( $metadata['sizes'][ $size ] ) ) {
				continue;
			}
			if ( ! isset( $size_data['width'] ) && ! isset( $size_data['height'] ) ) {
				continue;
			}
			if ( ! isset( $size_data['width'] ) ) {
				$size_data['width'] = null;
			}
			if ( ! isset( $size_data['height'] ) ) {
				$size_data['height'] = null;
			}
			if ( ! isset( $size_data['crop'] ) ) {
				$size_data['crop'] = false;
			}

			$image_sizes = getimagesize( $fullsizepath );
			if ( false === $image_sizes ) {
				continue;
			}
			list( $orig_w, $orig_h ) = $image_sizes;

			$dimensions = image_resize_dimensions( $orig_w, $orig_h, $size_data['width'], $size_data['height'], $size_data['crop'] );

			if ( ! $dimensions || ! is_array( $dimensions ) ) {
				continue;
			}

			$info         = pathinfo( $fullsizepath );
			$ext          = $info['extension'];
			$dst_w        = $dimensions[4];
			$dst_h        = $dimensions[5];
			$suffix       = "{$dst_w}x{$dst_h}";
			$dst_rel_path = str_replace( '.' . $ext, '', $fullsizepath );
			$thumbnail    = "{$dst_rel_path}-{$suffix}.{$ext}";

			if ( $dst_w === $metadata['sizes'][ $size ]['width'] && $dst_h === $metadata['sizes'][ $size ]['height'] && file_exists( $thumbnail ) ) {
				unset( $sizes[ $size ] );
			}
		}

		return $sizes;
	}

	/**
	 * Returns the sizes we want to regenerate.
	 *
	 * @param array $sizes Sizes to generate.
	 * @return array
	 */
	public function adjust_intermediate_image_sizes( $sizes ) {
		// Prevent a filter loop.
		$unfiltered_sizes = array( 'woocommerce_thumbnail', 'woocommerce_gallery_thumbnail', 'woocommerce_single' );
		static $in_filter = false;
		if ( $in_filter ) {
			return $unfiltered_sizes;
		}
		$in_filter      = true;
		$filtered_sizes = apply_filters( 'woocommerce_regenerate_images_intermediate_image_sizes', $unfiltered_sizes );
		$in_filter      = false;
		return $filtered_sizes;
	}

	/**
	 * This runs once the job has completed all items on the queue.
	 *
	 * @return void
	 */
	protected function complete() {
		parent::complete();
		$log = wc_get_logger();
		$log->info(
			__( 'Completed product image regeneration job.', 'woocommerce' ),
			array(
				'source' => 'wc-image-regeneration',
			)
		);
	}
}
class-wc-shipping-zones.php000064400000010012151336044760011747 0ustar00<?php
/**
 * Handles storage and retrieval of shipping zones
 *
 * @package WooCommerce\Classes
 * @version 3.3.0
 * @since   2.6.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Shipping zones class.
 */
class WC_Shipping_Zones {

	/**
	 * Get shipping zones from the database.
	 *
	 * @since 2.6.0
	 * @param string $context Getting shipping methods for what context. Valid values, admin, json.
	 * @return array Array of arrays.
	 */
	public static function get_zones( $context = 'admin' ) {
		$data_store = WC_Data_Store::load( 'shipping-zone' );
		$raw_zones  = $data_store->get_zones();
		$zones      = array();

		foreach ( $raw_zones as $raw_zone ) {
			$zone                                = new WC_Shipping_Zone( $raw_zone );
			$zones[ $zone->get_id() ]            = $zone->get_data();
			$zones[ $zone->get_id() ]['zone_id'] = $zone->get_id();
			$zones[ $zone->get_id() ]['formatted_zone_location'] = $zone->get_formatted_location();
			$zones[ $zone->get_id() ]['shipping_methods']        = $zone->get_shipping_methods( false, $context );
		}

		return $zones;
	}

	/**
	 * Get shipping zone using it's ID
	 *
	 * @since 2.6.0
	 * @param int $zone_id Zone ID.
	 * @return WC_Shipping_Zone|bool
	 */
	public static function get_zone( $zone_id ) {
		return self::get_zone_by( 'zone_id', $zone_id );
	}

	/**
	 * Get shipping zone by an ID.
	 *
	 * @since 2.6.0
	 * @param string $by Get by 'zone_id' or 'instance_id'.
	 * @param int    $id ID.
	 * @return WC_Shipping_Zone|bool
	 */
	public static function get_zone_by( $by = 'zone_id', $id = 0 ) {
		$zone_id = false;

		switch ( $by ) {
			case 'zone_id':
				$zone_id = $id;
				break;
			case 'instance_id':
				$data_store = WC_Data_Store::load( 'shipping-zone' );
				$zone_id    = $data_store->get_zone_id_by_instance_id( $id );
				break;
		}

		if ( false !== $zone_id ) {
			try {
				return new WC_Shipping_Zone( $zone_id );
			} catch ( Exception $e ) {
				return false;
			}
		}

		return false;
	}

	/**
	 * Get shipping zone using it's ID.
	 *
	 * @since 2.6.0
	 * @param int $instance_id Instance ID.
	 * @return bool|WC_Shipping_Method
	 */
	public static function get_shipping_method( $instance_id ) {
		$data_store          = WC_Data_Store::load( 'shipping-zone' );
		$raw_shipping_method = $data_store->get_method( $instance_id );
		$wc_shipping         = WC_Shipping::instance();
		$allowed_classes     = $wc_shipping->get_shipping_method_class_names();

		if ( ! empty( $raw_shipping_method ) && in_array( $raw_shipping_method->method_id, array_keys( $allowed_classes ), true ) ) {
			$class_name = $allowed_classes[ $raw_shipping_method->method_id ];
			if ( is_object( $class_name ) ) {
				$class_name = get_class( $class_name );
			}
			return new $class_name( $raw_shipping_method->instance_id );
		}
		return false;
	}

	/**
	 * Delete a zone using it's ID
	 *
	 * @param int $zone_id Zone ID.
	 * @since 2.6.0
	 */
	public static function delete_zone( $zone_id ) {
		$zone = new WC_Shipping_Zone( $zone_id );
		$zone->delete();
	}

	/**
	 * Find a matching zone for a given package.
	 *
	 * @since  2.6.0
	 * @uses   wc_make_numeric_postcode()
	 * @param  array $package Shipping package.
	 * @return WC_Shipping_Zone
	 */
	public static function get_zone_matching_package( $package ) {
		$country          = strtoupper( wc_clean( $package['destination']['country'] ) );
		$state            = strtoupper( wc_clean( $package['destination']['state'] ) );
		$postcode         = wc_normalize_postcode( wc_clean( $package['destination']['postcode'] ) );
		$cache_key        = WC_Cache_Helper::get_cache_prefix( 'shipping_zones' ) . 'wc_shipping_zone_' . md5( sprintf( '%s+%s+%s', $country, $state, $postcode ) );
		$matching_zone_id = wp_cache_get( $cache_key, 'shipping_zones' );

		if ( false === $matching_zone_id ) {
			$data_store       = WC_Data_Store::load( 'shipping-zone' );
			$matching_zone_id = $data_store->get_zone_id_from_package( $package );
			wp_cache_set( $cache_key, $matching_zone_id, 'shipping_zones' );
		}

		return new WC_Shipping_Zone( $matching_zone_id ? $matching_zone_id : 0 );
	}
}
class-wc-coupon.php000064400000102667151336044760010317 0ustar00<?php
/**
 * WooCommerce coupons.
 *
 * The WooCommerce coupons class gets coupon data from storage and checks coupon validity.
 *
 * @package WooCommerce\Classes
 * @version 3.0.0
 */

use Automattic\WooCommerce\Utilities\NumberUtil;

defined( 'ABSPATH' ) || exit;

require_once dirname( __FILE__ ) . '/legacy/class-wc-legacy-coupon.php';

/**
 * Coupon class.
 */
class WC_Coupon extends WC_Legacy_Coupon {

	/**
	 * Data array, with defaults.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $data = array(
		'code'                        => '',
		'amount'                      => 0,
		'date_created'                => null,
		'date_modified'               => null,
		'date_expires'                => null,
		'discount_type'               => 'fixed_cart',
		'description'                 => '',
		'usage_count'                 => 0,
		'individual_use'              => false,
		'product_ids'                 => array(),
		'excluded_product_ids'        => array(),
		'usage_limit'                 => 0,
		'usage_limit_per_user'        => 0,
		'limit_usage_to_x_items'      => null,
		'free_shipping'               => false,
		'product_categories'          => array(),
		'excluded_product_categories' => array(),
		'exclude_sale_items'          => false,
		'minimum_amount'              => '',
		'maximum_amount'              => '',
		'email_restrictions'          => array(),
		'used_by'                     => array(),
		'virtual'                     => false,
	);

	// Coupon message codes.
	const E_WC_COUPON_INVALID_FILTERED               = 100;
	const E_WC_COUPON_INVALID_REMOVED                = 101;
	const E_WC_COUPON_NOT_YOURS_REMOVED              = 102;
	const E_WC_COUPON_ALREADY_APPLIED                = 103;
	const E_WC_COUPON_ALREADY_APPLIED_INDIV_USE_ONLY = 104;
	const E_WC_COUPON_NOT_EXIST                      = 105;
	const E_WC_COUPON_USAGE_LIMIT_REACHED            = 106;
	const E_WC_COUPON_EXPIRED                        = 107;
	const E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET        = 108;
	const E_WC_COUPON_NOT_APPLICABLE                 = 109;
	const E_WC_COUPON_NOT_VALID_SALE_ITEMS           = 110;
	const E_WC_COUPON_PLEASE_ENTER                   = 111;
	const E_WC_COUPON_MAX_SPEND_LIMIT_MET            = 112;
	const E_WC_COUPON_EXCLUDED_PRODUCTS              = 113;
	const E_WC_COUPON_EXCLUDED_CATEGORIES            = 114;
	const E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK       = 115;
	const E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK_GUEST = 116;
	const WC_COUPON_SUCCESS                          = 200;
	const WC_COUPON_REMOVED                          = 201;

	/**
	 * Cache group.
	 *
	 * @var string
	 */
	protected $cache_group = 'coupons';

	/**
	 * Coupon constructor. Loads coupon data.
	 *
	 * @param mixed $data Coupon data, object, ID or code.
	 */
	public function __construct( $data = '' ) {
		parent::__construct( $data );

		// If we already have a coupon object, read it again.
		if ( $data instanceof WC_Coupon ) {
			$this->set_id( absint( $data->get_id() ) );
			$this->read_object_from_database();
			return;
		}

		// This filter allows custom coupon objects to be created on the fly.
		$coupon = apply_filters( 'woocommerce_get_shop_coupon_data', false, $data, $this );

		if ( $coupon ) {
			$this->read_manual_coupon( $data, $coupon );
			return;
		}

		// Try to load coupon using ID or code.
		if ( is_int( $data ) && 'shop_coupon' === get_post_type( $data ) ) {
			$this->set_id( $data );
		} elseif ( ! empty( $data ) ) {
			$id = wc_get_coupon_id_by_code( $data );
			// Need to support numeric strings for backwards compatibility.
			if ( ! $id && 'shop_coupon' === get_post_type( $data ) ) {
				$this->set_id( $data );
			} else {
				$this->set_id( $id );
				$this->set_code( $data );
			}
		} else {
			$this->set_object_read( true );
		}

		$this->read_object_from_database();
	}

	/**
	 * If the object has an ID, read using the data store.
	 *
	 * @since 3.4.1
	 */
	protected function read_object_from_database() {
		$this->data_store = WC_Data_Store::load( 'coupon' );

		if ( $this->get_id() > 0 ) {
			$this->data_store->read( $this );
		}
	}
	/**
	 * Checks the coupon type.
	 *
	 * @param  string $type Array or string of types.
	 * @return bool
	 */
	public function is_type( $type ) {
		return ( $this->get_discount_type() === $type || ( is_array( $type ) && in_array( $this->get_discount_type(), $type, true ) ) );
	}

	/**
	 * Prefix for action and filter hooks on data.
	 *
	 * @since  3.0.0
	 * @return string
	 */
	protected function get_hook_prefix() {
		return 'woocommerce_coupon_get_';
	}

	/*
	|--------------------------------------------------------------------------
	| Getters
	|--------------------------------------------------------------------------
	|
	| Methods for getting data from the coupon object.
	|
	*/

	/**
	 * Get coupon code.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_code( $context = 'view' ) {
		return $this->get_prop( 'code', $context );
	}

	/**
	 * Get coupon description.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_description( $context = 'view' ) {
		return $this->get_prop( 'description', $context );
	}

	/**
	 * Get discount type.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_discount_type( $context = 'view' ) {
		return $this->get_prop( 'discount_type', $context );
	}

	/**
	 * Get coupon amount.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return float
	 */
	public function get_amount( $context = 'view' ) {
		return wc_format_decimal( $this->get_prop( 'amount', $context ) );
	}

	/**
	 * Get coupon expiration date.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return WC_DateTime|NULL object if the date is set or null if there is no date.
	 */
	public function get_date_expires( $context = 'view' ) {
		return $this->get_prop( 'date_expires', $context );
	}

	/**
	 * Get date_created
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return WC_DateTime|NULL object if the date is set or null if there is no date.
	 */
	public function get_date_created( $context = 'view' ) {
		return $this->get_prop( 'date_created', $context );
	}

	/**
	 * Get date_modified
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return WC_DateTime|NULL object if the date is set or null if there is no date.
	 */
	public function get_date_modified( $context = 'view' ) {
		return $this->get_prop( 'date_modified', $context );
	}

	/**
	 * Get coupon usage count.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return integer
	 */
	public function get_usage_count( $context = 'view' ) {
		return $this->get_prop( 'usage_count', $context );
	}

	/**
	 * Get the "indvidual use" checkbox status.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return bool
	 */
	public function get_individual_use( $context = 'view' ) {
		return $this->get_prop( 'individual_use', $context );
	}

	/**
	 * Get product IDs this coupon can apply to.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return array
	 */
	public function get_product_ids( $context = 'view' ) {
		return $this->get_prop( 'product_ids', $context );
	}

	/**
	 * Get product IDs that this coupon should not apply to.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return array
	 */
	public function get_excluded_product_ids( $context = 'view' ) {
		return $this->get_prop( 'excluded_product_ids', $context );
	}

	/**
	 * Get coupon usage limit.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return integer
	 */
	public function get_usage_limit( $context = 'view' ) {
		return $this->get_prop( 'usage_limit', $context );
	}

	/**
	 * Get coupon usage limit per customer (for a single customer)
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return integer
	 */
	public function get_usage_limit_per_user( $context = 'view' ) {
		return $this->get_prop( 'usage_limit_per_user', $context );
	}

	/**
	 * Usage limited to certain amount of items
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return integer|null
	 */
	public function get_limit_usage_to_x_items( $context = 'view' ) {
		return $this->get_prop( 'limit_usage_to_x_items', $context );
	}

	/**
	 * If this coupon grants free shipping or not.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return bool
	 */
	public function get_free_shipping( $context = 'view' ) {
		return $this->get_prop( 'free_shipping', $context );
	}

	/**
	 * Get product categories this coupon can apply to.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return array
	 */
	public function get_product_categories( $context = 'view' ) {
		return $this->get_prop( 'product_categories', $context );
	}

	/**
	 * Get product categories this coupon cannot not apply to.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return array
	 */
	public function get_excluded_product_categories( $context = 'view' ) {
		return $this->get_prop( 'excluded_product_categories', $context );
	}

	/**
	 * If this coupon should exclude items on sale.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return bool
	 */
	public function get_exclude_sale_items( $context = 'view' ) {
		return $this->get_prop( 'exclude_sale_items', $context );
	}

	/**
	 * Get minimum spend amount.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return float
	 */
	public function get_minimum_amount( $context = 'view' ) {
		return $this->get_prop( 'minimum_amount', $context );
	}
	/**
	 * Get maximum spend amount.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return float
	 */
	public function get_maximum_amount( $context = 'view' ) {
		return $this->get_prop( 'maximum_amount', $context );
	}

	/**
	 * Get emails to check customer usage restrictions.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return array
	 */
	public function get_email_restrictions( $context = 'view' ) {
		return $this->get_prop( 'email_restrictions', $context );
	}

	/**
	 * Get records of all users who have used the current coupon.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return array
	 */
	public function get_used_by( $context = 'view' ) {
		return $this->get_prop( 'used_by', $context );
	}

	/**
	 * If the filter is added through the woocommerce_get_shop_coupon_data filter, it's virtual and not in the DB.
	 *
	 * @since 3.2.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return boolean
	 */
	public function get_virtual( $context = 'view' ) {
		return (bool) $this->get_prop( 'virtual', $context );
	}

	/**
	 * Get discount amount for a cart item.
	 *
	 * @param  float      $discounting_amount Amount the coupon is being applied to.
	 * @param  array|null $cart_item          Cart item being discounted if applicable.
	 * @param  boolean    $single             True if discounting a single qty item, false if its the line.
	 * @return float Amount this coupon has discounted.
	 */
	public function get_discount_amount( $discounting_amount, $cart_item = null, $single = false ) {
		$discount      = 0;
		$cart_item_qty = is_null( $cart_item ) ? 1 : $cart_item['quantity'];

		if ( $this->is_type( array( 'percent' ) ) ) {
			$discount = (float) $this->get_amount() * ( $discounting_amount / 100 );
		} elseif ( $this->is_type( 'fixed_cart' ) && ! is_null( $cart_item ) && WC()->cart->subtotal_ex_tax ) {
			/**
			 * This is the most complex discount - we need to divide the discount between rows based on their price in.
			 * proportion to the subtotal. This is so rows with different tax rates get a fair discount, and so rows.
			 * with no price (free) don't get discounted.
			 *
			 * Get item discount by dividing item cost by subtotal to get a %.
			 *
			 * Uses price inc tax if prices include tax to work around https://github.com/woocommerce/woocommerce/issues/7669 and https://github.com/woocommerce/woocommerce/issues/8074.
			 */
			if ( wc_prices_include_tax() ) {
				$discount_percent = ( wc_get_price_including_tax( $cart_item['data'] ) * $cart_item_qty ) / WC()->cart->subtotal;
			} else {
				$discount_percent = ( wc_get_price_excluding_tax( $cart_item['data'] ) * $cart_item_qty ) / WC()->cart->subtotal_ex_tax;
			}
			$discount = ( (float) $this->get_amount() * $discount_percent ) / $cart_item_qty;

		} elseif ( $this->is_type( 'fixed_product' ) ) {
			$discount = min( $this->get_amount(), $discounting_amount );
			$discount = $single ? $discount : $discount * $cart_item_qty;
		}

		return apply_filters(
			'woocommerce_coupon_get_discount_amount',
			NumberUtil::round( min( $discount, $discounting_amount ), wc_get_rounding_precision() ),
			$discounting_amount,
			$cart_item,
			$single,
			$this
		);
	}

	/*
	|--------------------------------------------------------------------------
	| Setters
	|--------------------------------------------------------------------------
	|
	| Functions for setting coupon data. These should not update anything in the
	| database itself and should only change what is stored in the class
	| object.
	|
	*/

	/**
	 * Set coupon code.
	 *
	 * @since 3.0.0
	 * @param string $code Coupon code.
	 */
	public function set_code( $code ) {
		$this->set_prop( 'code', wc_format_coupon_code( $code ) );
	}

	/**
	 * Set coupon description.
	 *
	 * @since 3.0.0
	 * @param string $description Description.
	 */
	public function set_description( $description ) {
		$this->set_prop( 'description', $description );
	}

	/**
	 * Set discount type.
	 *
	 * @since 3.0.0
	 * @param string $discount_type Discount type.
	 */
	public function set_discount_type( $discount_type ) {
		if ( 'percent_product' === $discount_type ) {
			$discount_type = 'percent'; // Backwards compatibility.
		}
		if ( ! in_array( $discount_type, array_keys( wc_get_coupon_types() ), true ) ) {
			$this->error( 'coupon_invalid_discount_type', __( 'Invalid discount type', 'woocommerce' ) );
		}
		$this->set_prop( 'discount_type', $discount_type );
	}

	/**
	 * Set amount.
	 *
	 * @since 3.0.0
	 * @param float $amount Amount.
	 */
	public function set_amount( $amount ) {
		$amount = wc_format_decimal( $amount );

		if ( ! is_numeric( $amount ) ) {
			$amount = 0;
		}

		if ( $amount < 0 ) {
			$this->error( 'coupon_invalid_amount', __( 'Invalid discount amount', 'woocommerce' ) );
		}

		if ( 'percent' === $this->get_discount_type() && $amount > 100 ) {
			$this->error( 'coupon_invalid_amount', __( 'Invalid discount amount', 'woocommerce' ) );
		}

		$this->set_prop( 'amount', $amount );
	}

	/**
	 * Set expiration date.
	 *
	 * @since  3.0.0
	 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date.
	 */
	public function set_date_expires( $date ) {
		$this->set_date_prop( 'date_expires', $date );
	}

	/**
	 * Set date_created
	 *
	 * @since  3.0.0
	 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date.
	 */
	public function set_date_created( $date ) {
		$this->set_date_prop( 'date_created', $date );
	}

	/**
	 * Set date_modified
	 *
	 * @since  3.0.0
	 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date.
	 */
	public function set_date_modified( $date ) {
		$this->set_date_prop( 'date_modified', $date );
	}

	/**
	 * Set how many times this coupon has been used.
	 *
	 * @since 3.0.0
	 * @param int $usage_count Usage count.
	 */
	public function set_usage_count( $usage_count ) {
		$this->set_prop( 'usage_count', absint( $usage_count ) );
	}

	/**
	 * Set if this coupon can only be used once.
	 *
	 * @since 3.0.0
	 * @param bool $is_individual_use If is for individual use.
	 */
	public function set_individual_use( $is_individual_use ) {
		$this->set_prop( 'individual_use', (bool) $is_individual_use );
	}

	/**
	 * Set the product IDs this coupon can be used with.
	 *
	 * @since 3.0.0
	 * @param array $product_ids Products IDs.
	 */
	public function set_product_ids( $product_ids ) {
		$this->set_prop( 'product_ids', array_filter( wp_parse_id_list( (array) $product_ids ) ) );
	}

	/**
	 * Set the product IDs this coupon cannot be used with.
	 *
	 * @since 3.0.0
	 * @param array $excluded_product_ids Exclude product IDs.
	 */
	public function set_excluded_product_ids( $excluded_product_ids ) {
		$this->set_prop( 'excluded_product_ids', array_filter( wp_parse_id_list( (array) $excluded_product_ids ) ) );
	}

	/**
	 * Set the amount of times this coupon can be used.
	 *
	 * @since 3.0.0
	 * @param int $usage_limit Usage limit.
	 */
	public function set_usage_limit( $usage_limit ) {
		$this->set_prop( 'usage_limit', absint( $usage_limit ) );
	}

	/**
	 * Set the amount of times this coupon can be used per user.
	 *
	 * @since 3.0.0
	 * @param int $usage_limit Usage limit.
	 */
	public function set_usage_limit_per_user( $usage_limit ) {
		$this->set_prop( 'usage_limit_per_user', absint( $usage_limit ) );
	}

	/**
	 * Set usage limit to x number of items.
	 *
	 * @since 3.0.0
	 * @param int|null $limit_usage_to_x_items Limit usage to X items.
	 */
	public function set_limit_usage_to_x_items( $limit_usage_to_x_items ) {
		$this->set_prop( 'limit_usage_to_x_items', is_null( $limit_usage_to_x_items ) ? null : absint( $limit_usage_to_x_items ) );
	}

	/**
	 * Set if this coupon enables free shipping or not.
	 *
	 * @since 3.0.0
	 * @param bool $free_shipping If grant free shipping.
	 */
	public function set_free_shipping( $free_shipping ) {
		$this->set_prop( 'free_shipping', (bool) $free_shipping );
	}

	/**
	 * Set the product category IDs this coupon can be used with.
	 *
	 * @since 3.0.0
	 * @param array $product_categories List of product categories.
	 */
	public function set_product_categories( $product_categories ) {
		$this->set_prop( 'product_categories', array_filter( wp_parse_id_list( (array) $product_categories ) ) );
	}

	/**
	 * Set the product category IDs this coupon cannot be used with.
	 *
	 * @since 3.0.0
	 * @param array $excluded_product_categories List of excluded product categories.
	 */
	public function set_excluded_product_categories( $excluded_product_categories ) {
		$this->set_prop( 'excluded_product_categories', array_filter( wp_parse_id_list( (array) $excluded_product_categories ) ) );
	}

	/**
	 * Set if this coupon should excluded sale items or not.
	 *
	 * @since 3.0.0
	 * @param bool $exclude_sale_items If should exclude sale items.
	 */
	public function set_exclude_sale_items( $exclude_sale_items ) {
		$this->set_prop( 'exclude_sale_items', (bool) $exclude_sale_items );
	}

	/**
	 * Set the minimum spend amount.
	 *
	 * @since 3.0.0
	 * @param float $amount Minium amount.
	 */
	public function set_minimum_amount( $amount ) {
		$this->set_prop( 'minimum_amount', wc_format_decimal( $amount ) );
	}

	/**
	 * Set the maximum spend amount.
	 *
	 * @since 3.0.0
	 * @param float $amount Maximum amount.
	 */
	public function set_maximum_amount( $amount ) {
		$this->set_prop( 'maximum_amount', wc_format_decimal( $amount ) );
	}

	/**
	 * Set email restrictions.
	 *
	 * @since 3.0.0
	 * @param array $emails List of emails.
	 */
	public function set_email_restrictions( $emails = array() ) {
		$emails = array_filter( array_map( 'sanitize_email', array_map( 'strtolower', (array) $emails ) ) );
		foreach ( $emails as $email ) {
			if ( ! is_email( $email ) ) {
				$this->error( 'coupon_invalid_email_address', __( 'Invalid email address restriction', 'woocommerce' ) );
			}
		}
		$this->set_prop( 'email_restrictions', $emails );
	}

	/**
	 * Set which users have used this coupon.
	 *
	 * @since 3.0.0
	 * @param array $used_by List of user IDs.
	 */
	public function set_used_by( $used_by ) {
		$this->set_prop( 'used_by', array_filter( $used_by ) );
	}

	/**
	 * Set coupon virtual state.
	 *
	 * @param boolean $virtual Whether it is virtual or not.
	 * @since 3.2.0
	 */
	public function set_virtual( $virtual ) {
		$this->set_prop( 'virtual', (bool) $virtual );
	}

	/*
	|--------------------------------------------------------------------------
	| Other Actions
	|--------------------------------------------------------------------------
	*/

	/**
	 * Developers can programmatically return coupons. This function will read those values into our WC_Coupon class.
	 *
	 * @since 3.0.0
	 * @param string $code   Coupon code.
	 * @param array  $coupon Array of coupon properties.
	 */
	public function read_manual_coupon( $code, $coupon ) {
		foreach ( $coupon as $key => $value ) {
			switch ( $key ) {
				case 'excluded_product_ids':
				case 'exclude_product_ids':
					if ( ! is_array( $coupon[ $key ] ) ) {
						wc_doing_it_wrong( $key, $key . ' should be an array instead of a string.', '3.0' );
						$coupon['excluded_product_ids'] = wc_string_to_array( $value );
					}
					break;
				case 'exclude_product_categories':
				case 'excluded_product_categories':
					if ( ! is_array( $coupon[ $key ] ) ) {
						wc_doing_it_wrong( $key, $key . ' should be an array instead of a string.', '3.0' );
						$coupon['excluded_product_categories'] = wc_string_to_array( $value );
					}
					break;
				case 'product_ids':
					if ( ! is_array( $coupon[ $key ] ) ) {
						wc_doing_it_wrong( $key, $key . ' should be an array instead of a string.', '3.0' );
						$coupon[ $key ] = wc_string_to_array( $value );
					}
					break;
				case 'individual_use':
				case 'free_shipping':
				case 'exclude_sale_items':
					if ( ! is_bool( $coupon[ $key ] ) ) {
						wc_doing_it_wrong( $key, $key . ' should be true or false instead of yes or no.', '3.0' );
						$coupon[ $key ] = wc_string_to_bool( $value );
					}
					break;
				case 'expiry_date':
					$coupon['date_expires'] = $value;
					break;
			}
		}
		$this->set_props( $coupon );
		$this->set_code( $code );
		$this->set_id( 0 );
		$this->set_virtual( true );
	}

	/**
	 * Increase usage count for current coupon.
	 *
	 * @param string   $used_by  Either user ID or billing email.
	 * @param WC_Order $order  If provided, will clear the coupons held by this order.
	 */
	public function increase_usage_count( $used_by = '', $order = null ) {
		if ( $this->get_id() && $this->data_store ) {
			$new_count = $this->data_store->increase_usage_count( $this, $used_by, $order );

			// Bypass set_prop and remove pending changes since the data store saves the count already.
			$this->data['usage_count'] = $new_count;
			if ( isset( $this->changes['usage_count'] ) ) {
				unset( $this->changes['usage_count'] );
			}
		}
	}

	/**
	 * Decrease usage count for current coupon.
	 *
	 * @param string $used_by Either user ID or billing email.
	 */
	public function decrease_usage_count( $used_by = '' ) {
		if ( $this->get_id() && $this->get_usage_count() > 0 && $this->data_store ) {
			$new_count = $this->data_store->decrease_usage_count( $this, $used_by );

			// Bypass set_prop and remove pending changes since the data store saves the count already.
			$this->data['usage_count'] = $new_count;
			if ( isset( $this->changes['usage_count'] ) ) {
				unset( $this->changes['usage_count'] );
			}
		}
	}

	/*
	|--------------------------------------------------------------------------
	| Validation & Error Handling
	|--------------------------------------------------------------------------
	*/

	/**
	 * Returns the error_message string.

	 * @return string
	 */
	public function get_error_message() {
		return $this->error_message;
	}

	/**
	 * Check if a coupon is valid for the cart.
	 *
	 * @deprecated 3.2.0 In favor of WC_Discounts->is_coupon_valid.
	 * @return bool
	 */
	public function is_valid() {
		$discounts = new WC_Discounts( WC()->cart );
		$valid     = $discounts->is_coupon_valid( $this );

		if ( is_wp_error( $valid ) ) {
			$this->error_message = $valid->get_error_message();
			return false;
		}

		return $valid;
	}

	/**
	 * Check if a coupon is valid.
	 *
	 * @return bool
	 */
	public function is_valid_for_cart() {
		return apply_filters( 'woocommerce_coupon_is_valid_for_cart', $this->is_type( wc_get_cart_coupon_types() ), $this );
	}

	/**
	 * Check if a coupon is valid for a product.
	 *
	 * @param WC_Product $product Product instance.
	 * @param array      $values  Values.
	 * @return bool
	 */
	public function is_valid_for_product( $product, $values = array() ) {
		if ( ! $this->is_type( wc_get_product_coupon_types() ) ) {
			return apply_filters( 'woocommerce_coupon_is_valid_for_product', false, $product, $this, $values );
		}

		$valid        = false;
		$product_cats = wc_get_product_cat_ids( $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id() );
		$product_ids  = array( $product->get_id(), $product->get_parent_id() );

		// Specific products get the discount.
		if ( count( $this->get_product_ids() ) && count( array_intersect( $product_ids, $this->get_product_ids() ) ) ) {
			$valid = true;
		}

		// Category discounts.
		if ( count( $this->get_product_categories() ) && count( array_intersect( $product_cats, $this->get_product_categories() ) ) ) {
			$valid = true;
		}

		// No product ids - all items discounted.
		if ( ! count( $this->get_product_ids() ) && ! count( $this->get_product_categories() ) ) {
			$valid = true;
		}

		// Specific product IDs excluded from the discount.
		if ( count( $this->get_excluded_product_ids() ) && count( array_intersect( $product_ids, $this->get_excluded_product_ids() ) ) ) {
			$valid = false;
		}

		// Specific categories excluded from the discount.
		if ( count( $this->get_excluded_product_categories() ) && count( array_intersect( $product_cats, $this->get_excluded_product_categories() ) ) ) {
			$valid = false;
		}

		// Sale Items excluded from discount.
		if ( $this->get_exclude_sale_items() && $product->is_on_sale() ) {
			$valid = false;
		}

		return apply_filters( 'woocommerce_coupon_is_valid_for_product', $valid, $product, $this, $values );
	}

	/**
	 * Converts one of the WC_Coupon message/error codes to a message string and.
	 * displays the message/error.
	 *
	 * @param int $msg_code Message/error code.
	 */
	public function add_coupon_message( $msg_code ) {
		$msg = $msg_code < 200 ? $this->get_coupon_error( $msg_code ) : $this->get_coupon_message( $msg_code );

		if ( ! $msg ) {
			return;
		}

		if ( $msg_code < 200 ) {
			wc_add_notice( $msg, 'error' );
		} else {
			wc_add_notice( $msg );
		}
	}

	/**
	 * Map one of the WC_Coupon message codes to a message string.
	 *
	 * @param integer $msg_code Message code.
	 * @return string Message/error string.
	 */
	public function get_coupon_message( $msg_code ) {
		switch ( $msg_code ) {
			case self::WC_COUPON_SUCCESS:
				$msg = __( 'Coupon code applied successfully.', 'woocommerce' );
				break;
			case self::WC_COUPON_REMOVED:
				$msg = __( 'Coupon code removed successfully.', 'woocommerce' );
				break;
			default:
				$msg = '';
				break;
		}
		return apply_filters( 'woocommerce_coupon_message', $msg, $msg_code, $this );
	}

	/**
	 * Map one of the WC_Coupon error codes to a message string.
	 *
	 * @param int $err_code Message/error code.
	 * @return string Message/error string
	 */
	public function get_coupon_error( $err_code ) {
		switch ( $err_code ) {
			case self::E_WC_COUPON_INVALID_FILTERED:
				$err = __( 'Coupon is not valid.', 'woocommerce' );
				break;
			case self::E_WC_COUPON_NOT_EXIST:
				/* translators: %s: coupon code */
				$err = sprintf( __( 'Coupon "%s" does not exist!', 'woocommerce' ), esc_html( $this->get_code() ) );
				break;
			case self::E_WC_COUPON_INVALID_REMOVED:
				/* translators: %s: coupon code */
				$err = sprintf( __( 'Sorry, it seems the coupon "%s" is invalid - it has now been removed from your order.', 'woocommerce' ), esc_html( $this->get_code() ) );
				break;
			case self::E_WC_COUPON_NOT_YOURS_REMOVED:
				/* translators: %s: coupon code */
				$err = sprintf( __( 'Sorry, it seems the coupon "%s" is not yours - it has now been removed from your order.', 'woocommerce' ), esc_html( $this->get_code() ) );
				break;
			case self::E_WC_COUPON_ALREADY_APPLIED:
				$err = __( 'Coupon code already applied!', 'woocommerce' );
				break;
			case self::E_WC_COUPON_ALREADY_APPLIED_INDIV_USE_ONLY:
				/* translators: %s: coupon code */
				$err = sprintf( __( 'Sorry, coupon "%s" has already been applied and cannot be used in conjunction with other coupons.', 'woocommerce' ), esc_html( $this->get_code() ) );
				break;
			case self::E_WC_COUPON_USAGE_LIMIT_REACHED:
				$err = __( 'Coupon usage limit has been reached.', 'woocommerce' );
				break;
			case self::E_WC_COUPON_EXPIRED:
				$err = __( 'This coupon has expired.', 'woocommerce' );
				break;
			case self::E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET:
				/* translators: %s: coupon minimum amount */
				$err = sprintf( __( 'The minimum spend for this coupon is %s.', 'woocommerce' ), wc_price( $this->get_minimum_amount() ) );
				break;
			case self::E_WC_COUPON_MAX_SPEND_LIMIT_MET:
				/* translators: %s: coupon maximum amount */
				$err = sprintf( __( 'The maximum spend for this coupon is %s.', 'woocommerce' ), wc_price( $this->get_maximum_amount() ) );
				break;
			case self::E_WC_COUPON_NOT_APPLICABLE:
				$err = __( 'Sorry, this coupon is not applicable to your cart contents.', 'woocommerce' );
				break;
			case self::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK:
				if ( is_user_logged_in() && wc_get_page_id( 'myaccount' ) > 0 ) {
					/* translators: %s: myaccount page link. */
					$err = sprintf( __( 'Coupon usage limit has been reached. If you were using this coupon just now but order was not complete, you can retry or cancel the order by going to the <a href="%s">my account page</a>.', 'woocommerce' ), wc_get_endpoint_url( 'orders', '', wc_get_page_permalink( 'myaccount' ) ) );
				} else {
					$err = $this->get_coupon_error( self::E_WC_COUPON_USAGE_LIMIT_REACHED );
				}
				break;
			case self::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK_GUEST:
				$err = __( 'Coupon usage limit has been reached. Please try again after some time, or contact us for help.', 'woocommerce' );
				break;
			case self::E_WC_COUPON_EXCLUDED_PRODUCTS:
				// Store excluded products that are in cart in $products.
				$products = array();
				if ( ! WC()->cart->is_empty() ) {
					foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
						if ( in_array( intval( $cart_item['product_id'] ), $this->get_excluded_product_ids(), true ) || in_array( intval( $cart_item['variation_id'] ), $this->get_excluded_product_ids(), true ) || in_array( intval( $cart_item['data']->get_parent_id() ), $this->get_excluded_product_ids(), true ) ) {
							$products[] = $cart_item['data']->get_name();
						}
					}
				}

				/* translators: %s: products list */
				$err = sprintf( __( 'Sorry, this coupon is not applicable to the products: %s.', 'woocommerce' ), implode( ', ', $products ) );
				break;
			case self::E_WC_COUPON_EXCLUDED_CATEGORIES:
				// Store excluded categories that are in cart in $categories.
				$categories = array();
				if ( ! WC()->cart->is_empty() ) {
					foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
						$product_cats = wc_get_product_cat_ids( $cart_item['product_id'] );
						$intersect    = array_intersect( $product_cats, $this->get_excluded_product_categories() );

						if ( count( $intersect ) > 0 ) {
							foreach ( $intersect as $cat_id ) {
								$cat          = get_term( $cat_id, 'product_cat' );
								$categories[] = $cat->name;
							}
						}
					}
				}

				/* translators: %s: categories list */
				$err = sprintf( __( 'Sorry, this coupon is not applicable to the categories: %s.', 'woocommerce' ), implode( ', ', array_unique( $categories ) ) );
				break;
			case self::E_WC_COUPON_NOT_VALID_SALE_ITEMS:
				$err = __( 'Sorry, this coupon is not valid for sale items.', 'woocommerce' );
				break;
			default:
				$err = '';
				break;
		}
		return apply_filters( 'woocommerce_coupon_error', $err, $err_code, $this );
	}

	/**
	 * Map one of the WC_Coupon error codes to an error string.
	 * No coupon instance will be available where a coupon does not exist,
	 * so this static method exists.
	 *
	 * @param int $err_code Error code.
	 * @return string Error string.
	 */
	public static function get_generic_coupon_error( $err_code ) {
		switch ( $err_code ) {
			case self::E_WC_COUPON_NOT_EXIST:
				$err = __( 'Coupon does not exist!', 'woocommerce' );
				break;
			case self::E_WC_COUPON_PLEASE_ENTER:
				$err = __( 'Please enter a coupon code.', 'woocommerce' );
				break;
			default:
				$err = '';
				break;
		}
		// When using this static method, there is no $this to pass to filter.
		return apply_filters( 'woocommerce_coupon_error', $err, $err_code, null );
	}
}
class-wc-data-exception.php000064400000002451151336044760011707 0ustar00<?php
/**
 * WooCommerce Data Exception Class
 *
 * Extends Exception to provide additional data.
 *
 * @package WooCommerce\Classes
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Data exception class.
 */
class WC_Data_Exception extends Exception {

	/**
	 * Sanitized error code.
	 *
	 * @var string
	 */
	protected $error_code;

	/**
	 * Error extra data.
	 *
	 * @var array
	 */
	protected $error_data;

	/**
	 * Setup exception.
	 *
	 * @param string $code             Machine-readable error code, e.g `woocommerce_invalid_product_id`.
	 * @param string $message          User-friendly translated error message, e.g. 'Product ID is invalid'.
	 * @param int    $http_status_code Proper HTTP status code to respond with, e.g. 400.
	 * @param array  $data             Extra error data.
	 */
	public function __construct( $code, $message, $http_status_code = 400, $data = array() ) {
		$this->error_code = $code;
		$this->error_data = array_merge( array( 'status' => $http_status_code ), $data );

		parent::__construct( $message, $http_status_code );
	}

	/**
	 * Returns the error code.
	 *
	 * @return string
	 */
	public function getErrorCode() {
		return $this->error_code;
	}

	/**
	 * Returns error data.
	 *
	 * @return array
	 */
	public function getErrorData() {
		return $this->error_data;
	}
}
class-wc-order-query.php000064400000005067151336044760011266 0ustar00<?php
/**
 * Parameter-based Order querying
 * Args and usage: https://github.com/woocommerce/woocommerce/wiki/wc_get_orders-and-WC_Order_Query
 *
 * @package WooCommerce\Classes
 * @version 3.1.0
 * @since   3.1.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Order query class.
 */
class WC_Order_Query extends WC_Object_Query {

	/**
	 * Valid query vars for orders.
	 *
	 * @return array
	 */
	protected function get_default_query_vars() {
		return array_merge(
			parent::get_default_query_vars(),
			array(
				'status'               => array_keys( wc_get_order_statuses() ),
				'type'                 => wc_get_order_types( 'view-orders' ),
				'currency'             => '',
				'version'              => '',
				'prices_include_tax'   => '',
				'date_created'         => '',
				'date_modified'        => '',
				'date_completed'       => '',
				'date_paid'            => '',
				'discount_total'       => '',
				'discount_tax'         => '',
				'shipping_total'       => '',
				'shipping_tax'         => '',
				'cart_tax'             => '',
				'total'                => '',
				'total_tax'            => '',
				'customer'             => '',
				'customer_id'          => '',
				'order_key'            => '',
				'billing_first_name'   => '',
				'billing_last_name'    => '',
				'billing_company'      => '',
				'billing_address_1'    => '',
				'billing_address_2'    => '',
				'billing_city'         => '',
				'billing_state'        => '',
				'billing_postcode'     => '',
				'billing_country'      => '',
				'billing_email'        => '',
				'billing_phone'        => '',
				'shipping_first_name'  => '',
				'shipping_last_name'   => '',
				'shipping_company'     => '',
				'shipping_address_1'   => '',
				'shipping_address_2'   => '',
				'shipping_city'        => '',
				'shipping_state'       => '',
				'shipping_postcode'    => '',
				'shipping_country'     => '',
				'shipping_phone'       => '',
				'payment_method'       => '',
				'payment_method_title' => '',
				'transaction_id'       => '',
				'customer_ip_address'  => '',
				'customer_user_agent'  => '',
				'created_via'          => '',
				'customer_note'        => '',
			)
		);
	}

	/**
	 * Get orders matching the current query vars.
	 *
	 * @return array|object of WC_Order objects
	 *
	 * @throws Exception When WC_Data_Store validation fails.
	 */
	public function get_orders() {
		$args    = apply_filters( 'woocommerce_order_query_args', $this->get_query_vars() );
		$results = WC_Data_Store::load( 'order' )->query( $args );
		return apply_filters( 'woocommerce_order_query', $results, $args );
	}
}
class-wc-shipping-zone.php000064400000032120151336044760011570 0ustar00<?php
/**
 * Represents a single shipping zone
 *
 * @since   2.6.0
 * @version 3.0.0
 * @package WooCommerce\Classes
 */

defined( 'ABSPATH' ) || exit;

require_once __DIR__ . '/legacy/class-wc-legacy-shipping-zone.php';

/**
 * WC_Shipping_Zone class.
 */
class WC_Shipping_Zone extends WC_Legacy_Shipping_Zone {

	/**
	 * Zone ID
	 *
	 * @var int|null
	 */
	protected $id = null;

	/**
	 * This is the name of this object type.
	 *
	 * @var string
	 */
	protected $object_type = 'shipping_zone';

	/**
	 * Zone Data.
	 *
	 * @var array
	 */
	protected $data = array(
		'zone_name'      => '',
		'zone_order'     => 0,
		'zone_locations' => array(),
	);

	/**
	 * Constructor for zones.
	 *
	 * @param int|object $zone Zone ID to load from the DB or zone object.
	 */
	public function __construct( $zone = null ) {
		if ( is_numeric( $zone ) && ! empty( $zone ) ) {
			$this->set_id( $zone );
		} elseif ( is_object( $zone ) ) {
			$this->set_id( $zone->zone_id );
		} elseif ( 0 === $zone || '0' === $zone ) {
			$this->set_id( 0 );
		} else {
			$this->set_object_read( true );
		}

		$this->data_store = WC_Data_Store::load( 'shipping-zone' );
		if ( false === $this->get_object_read() ) {
			$this->data_store->read( $this );
		}
	}

	/**
	 * --------------------------------------------------------------------------
	 * Getters
	 * --------------------------------------------------------------------------
	 */

	/**
	 * Get zone name.
	 *
	 * @param  string $context View or edit context.
	 * @return string
	 */
	public function get_zone_name( $context = 'view' ) {
		return $this->get_prop( 'zone_name', $context );
	}

	/**
	 * Get zone order.
	 *
	 * @param  string $context View or edit context.
	 * @return int
	 */
	public function get_zone_order( $context = 'view' ) {
		return $this->get_prop( 'zone_order', $context );
	}

	/**
	 * Get zone locations.
	 *
	 * @param  string $context View or edit context.
	 * @return array of zone objects
	 */
	public function get_zone_locations( $context = 'view' ) {
		return $this->get_prop( 'zone_locations', $context );
	}

	/**
	 * Return a text string representing what this zone is for.
	 *
	 * @param  int    $max Max locations to return.
	 * @param  string $context View or edit context.
	 * @return string
	 */
	public function get_formatted_location( $max = 10, $context = 'view' ) {
		$location_parts = array();
		$all_continents = WC()->countries->get_continents();
		$all_countries  = WC()->countries->get_countries();
		$all_states     = WC()->countries->get_states();
		$locations      = $this->get_zone_locations( $context );
		$continents     = array_filter( $locations, array( $this, 'location_is_continent' ) );
		$countries      = array_filter( $locations, array( $this, 'location_is_country' ) );
		$states         = array_filter( $locations, array( $this, 'location_is_state' ) );
		$postcodes      = array_filter( $locations, array( $this, 'location_is_postcode' ) );

		foreach ( $continents as $location ) {
			$location_parts[] = $all_continents[ $location->code ]['name'];
		}

		foreach ( $countries as $location ) {
			$location_parts[] = $all_countries[ $location->code ];
		}

		foreach ( $states as $location ) {
			$location_codes   = explode( ':', $location->code );
			$location_parts[] = $all_states[ $location_codes[0] ][ $location_codes[1] ];
		}

		foreach ( $postcodes as $location ) {
			$location_parts[] = $location->code;
		}

		// Fix display of encoded characters.
		$location_parts = array_map( 'html_entity_decode', $location_parts );

		if ( count( $location_parts ) > $max ) {
			$remaining = count( $location_parts ) - $max;
			// @codingStandardsIgnoreStart
			return sprintf( _n( '%s and %d other region', '%s and %d other regions', $remaining, 'woocommerce' ), implode( ', ', array_splice( $location_parts, 0, $max ) ), $remaining );
			// @codingStandardsIgnoreEnd
		} elseif ( ! empty( $location_parts ) ) {
			return implode( ', ', $location_parts );
		} else {
			return __( 'Everywhere', 'woocommerce' );
		}
	}

	/**
	 * Get shipping methods linked to this zone.
	 *
	 * @param bool   $enabled_only Only return enabled methods.
	 * @param string $context Getting shipping methods for what context. Valid values, admin, json.
	 * @return array of objects
	 */
	public function get_shipping_methods( $enabled_only = false, $context = 'admin' ) {
		if ( null === $this->get_id() ) {
			return array();
		}

		$raw_methods     = $this->data_store->get_methods( $this->get_id(), $enabled_only );
		$wc_shipping     = WC_Shipping::instance();
		$allowed_classes = $wc_shipping->get_shipping_method_class_names();
		$methods         = array();

		foreach ( $raw_methods as $raw_method ) {
			if ( in_array( $raw_method->method_id, array_keys( $allowed_classes ), true ) ) {
				$class_name  = $allowed_classes[ $raw_method->method_id ];
				$instance_id = $raw_method->instance_id;

				// The returned array may contain instances of shipping methods, as well
				// as classes. If the "class" is an instance, just use it. If not,
				// create an instance.
				if ( is_object( $class_name ) ) {
					$class_name_of_instance  = get_class( $class_name );
					$methods[ $instance_id ] = new $class_name_of_instance( $instance_id );
				} else {
					// If the class is not an object, it should be a string. It's better
					// to double check, to be sure (a class must be a string, anything)
					// else would be useless.
					if ( is_string( $class_name ) && class_exists( $class_name ) ) {
						$methods[ $instance_id ] = new $class_name( $instance_id );
					}
				}

				// Let's make sure that we have an instance before setting its attributes.
				if ( is_object( $methods[ $instance_id ] ) ) {
					$methods[ $instance_id ]->method_order       = absint( $raw_method->method_order );
					$methods[ $instance_id ]->enabled            = $raw_method->is_enabled ? 'yes' : 'no';
					$methods[ $instance_id ]->has_settings       = $methods[ $instance_id ]->has_settings();
					$methods[ $instance_id ]->settings_html      = $methods[ $instance_id ]->supports( 'instance-settings-modal' ) ? $methods[ $instance_id ]->get_admin_options_html() : false;
					$methods[ $instance_id ]->method_description = wp_kses_post( wpautop( $methods[ $instance_id ]->method_description ) );
				}

				if ( 'json' === $context ) {
					// We don't want the entire object in this context, just the public props.
					$methods[ $instance_id ] = (object) get_object_vars( $methods[ $instance_id ] );
					unset( $methods[ $instance_id ]->instance_form_fields, $methods[ $instance_id ]->form_fields );
				}
			}
		}

		uasort( $methods, 'wc_shipping_zone_method_order_uasort_comparison' );

		return apply_filters( 'woocommerce_shipping_zone_shipping_methods', $methods, $raw_methods, $allowed_classes, $this );
	}

	/**
	 * --------------------------------------------------------------------------
	 * Setters
	 * --------------------------------------------------------------------------
	 */

	/**
	 * Set zone name.
	 *
	 * @param string $set Value to set.
	 */
	public function set_zone_name( $set ) {
		$this->set_prop( 'zone_name', wc_clean( $set ) );
	}

	/**
	 * Set zone order. Value to set.
	 *
	 * @param int $set Value to set.
	 */
	public function set_zone_order( $set ) {
		$this->set_prop( 'zone_order', absint( $set ) );
	}

	/**
	 * Set zone locations.
	 *
	 * @since 3.0.0
	 * @param array $locations Value to set.
	 */
	public function set_zone_locations( $locations ) {
		if ( 0 !== $this->get_id() ) {
			$this->set_prop( 'zone_locations', $locations );
		}
	}

	/**
	 * --------------------------------------------------------------------------
	 * Other
	 * --------------------------------------------------------------------------
	 */

	/**
	 * Save zone data to the database.
	 *
	 * @return int
	 */
	public function save() {
		if ( ! $this->get_zone_name() ) {
			$this->set_zone_name( $this->generate_zone_name() );
		}

		if ( ! $this->data_store ) {
			return $this->get_id();
		}

		/**
		 * Trigger action before saving to the DB. Allows you to adjust object props before save.
		 *
		 * @param WC_Data          $this The object being saved.
		 * @param WC_Data_Store_WP $data_store THe data store persisting the data.
		 */
		do_action( 'woocommerce_before_' . $this->object_type . '_object_save', $this, $this->data_store );

		if ( null !== $this->get_id() ) {
			$this->data_store->update( $this );
		} else {
			$this->data_store->create( $this );
		}

		/**
		 * Trigger action after saving to the DB.
		 *
		 * @param WC_Data          $this The object being saved.
		 * @param WC_Data_Store_WP $data_store THe data store persisting the data.
		 */
		do_action( 'woocommerce_after_' . $this->object_type . '_object_save', $this, $this->data_store );

		return $this->get_id();
	}

	/**
	 * Generate a zone name based on location.
	 *
	 * @return string
	 */
	protected function generate_zone_name() {
		$zone_name = $this->get_formatted_location();

		if ( empty( $zone_name ) ) {
			$zone_name = __( 'Zone', 'woocommerce' );
		}

		return $zone_name;
	}

	/**
	 * Location type detection.
	 *
	 * @param  object $location Location to check.
	 * @return boolean
	 */
	private function location_is_continent( $location ) {
		return 'continent' === $location->type;
	}

	/**
	 * Location type detection.
	 *
	 * @param  object $location Location to check.
	 * @return boolean
	 */
	private function location_is_country( $location ) {
		return 'country' === $location->type;
	}

	/**
	 * Location type detection.
	 *
	 * @param  object $location Location to check.
	 * @return boolean
	 */
	private function location_is_state( $location ) {
		return 'state' === $location->type;
	}

	/**
	 * Location type detection.
	 *
	 * @param  object $location Location to check.
	 * @return boolean
	 */
	private function location_is_postcode( $location ) {
		return 'postcode' === $location->type;
	}

	/**
	 * Is passed location type valid?
	 *
	 * @param  string $type Type to check.
	 * @return boolean
	 */
	public function is_valid_location_type( $type ) {
		return in_array( $type, apply_filters( 'woocommerce_valid_location_types', array( 'postcode', 'state', 'country', 'continent' ) ), true );
	}

	/**
	 * Add location (state or postcode) to a zone.
	 *
	 * @param string $code Location code.
	 * @param string $type state or postcode.
	 */
	public function add_location( $code, $type ) {
		if ( 0 !== $this->get_id() && $this->is_valid_location_type( $type ) ) {
			if ( 'postcode' === $type ) {
				$code = trim( strtoupper( str_replace( chr( 226 ) . chr( 128 ) . chr( 166 ), '...', $code ) ) ); // No normalization - postcodes are matched against both normal and formatted versions to support wildcards.
			}
			$location         = array(
				'code' => wc_clean( $code ),
				'type' => wc_clean( $type ),
			);
			$zone_locations   = $this->get_prop( 'zone_locations', 'edit' );
			$zone_locations[] = (object) $location;
			$this->set_prop( 'zone_locations', $zone_locations );
		}
	}


	/**
	 * Clear all locations for this zone.
	 *
	 * @param array|string $types of location to clear.
	 */
	public function clear_locations( $types = array( 'postcode', 'state', 'country', 'continent' ) ) {
		if ( ! is_array( $types ) ) {
			$types = array( $types );
		}
		$zone_locations = $this->get_prop( 'zone_locations', 'edit' );
		foreach ( $zone_locations as $key => $values ) {
			if ( in_array( $values->type, $types, true ) ) {
				unset( $zone_locations[ $key ] );
			}
		}
		$zone_locations = array_values( $zone_locations ); // reindex.
		$this->set_prop( 'zone_locations', $zone_locations );
	}

	/**
	 * Set locations.
	 *
	 * @param array $locations Array of locations.
	 */
	public function set_locations( $locations = array() ) {
		$this->clear_locations();
		foreach ( $locations as $location ) {
			$this->add_location( $location['code'], $location['type'] );
		}
	}

	/**
	 * Add a shipping method to this zone.
	 *
	 * @param string $type shipping method type.
	 * @return int new instance_id, 0 on failure
	 */
	public function add_shipping_method( $type ) {
		if ( null === $this->get_id() ) {
			$this->save();
		}

		$instance_id     = 0;
		$wc_shipping     = WC_Shipping::instance();
		$allowed_classes = $wc_shipping->get_shipping_method_class_names();
		$count           = $this->data_store->get_method_count( $this->get_id() );

		if ( in_array( $type, array_keys( $allowed_classes ), true ) ) {
			$instance_id = $this->data_store->add_method( $this->get_id(), $type, $count + 1 );
		}

		if ( $instance_id ) {
			do_action( 'woocommerce_shipping_zone_method_added', $instance_id, $type, $this->get_id() );
		}

		WC_Cache_Helper::get_transient_version( 'shipping', true );

		return $instance_id;
	}

	/**
	 * Delete a shipping method from a zone.
	 *
	 * @param int $instance_id Shipping method instance ID.
	 * @return True on success, false on failure
	 */
	public function delete_shipping_method( $instance_id ) {
		if ( null === $this->get_id() ) {
			return false;
		}

		// Get method details.
		$method = $this->data_store->get_method( $instance_id );

		if ( $method ) {
			$this->data_store->delete_method( $instance_id );
			do_action( 'woocommerce_shipping_zone_method_deleted', $instance_id, $method->method_id, $this->get_id() );
		}

		WC_Cache_Helper::get_transient_version( 'shipping', true );

		return true;
	}
}
wc-update-functions.php000064400000206233151336044760011173 0ustar00<?php
/**
 * WooCommerce Updates
 *
 * Functions for updating data, used by the background updater.
 *
 * @package WooCommerce\Functions
 * @version 3.3.0
 */

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Internal\AssignDefaultCategory;

/**
 * Update file paths for 2.0
 *
 * @return void
 */
function wc_update_200_file_paths() {
	global $wpdb;

	// Upgrade old style files paths to support multiple file paths.
	$existing_file_paths = $wpdb->get_results( "SELECT meta_value, meta_id, post_id FROM {$wpdb->postmeta} WHERE meta_key = '_file_path' AND meta_value != '';" );

	if ( $existing_file_paths ) {

		foreach ( $existing_file_paths as $existing_file_path ) {

			$old_file_path = trim( $existing_file_path->meta_value );

			if ( ! empty( $old_file_path ) ) {
				// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
				$file_paths = serialize( array( md5( $old_file_path ) => $old_file_path ) );

				$wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_key = '_file_paths', meta_value = %s WHERE meta_id = %d", $file_paths, $existing_file_path->meta_id ) );

				$wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->prefix}woocommerce_downloadable_product_permissions SET download_id = %s WHERE product_id = %d", md5( $old_file_path ), $existing_file_path->post_id ) );

			}
		}
	}
}

/**
 * Update permalinks for 2.0
 *
 * @return void
 */
function wc_update_200_permalinks() {
	// Setup default permalinks if shop page is defined.
	$permalinks   = get_option( 'woocommerce_permalinks' );
	$shop_page_id = wc_get_page_id( 'shop' );

	if ( empty( $permalinks ) && $shop_page_id > 0 ) {

		$base_slug = $shop_page_id > 0 && get_post( $shop_page_id ) ? get_page_uri( $shop_page_id ) : 'shop';

		$category_base = 'yes' === get_option( 'woocommerce_prepend_shop_page_to_urls' ) ? trailingslashit( $base_slug ) : '';
		$category_slug = get_option( 'woocommerce_product_category_slug' ) ? get_option( 'woocommerce_product_category_slug' ) : _x( 'product-category', 'slug', 'woocommerce' );
		$tag_slug      = get_option( 'woocommerce_product_tag_slug' ) ? get_option( 'woocommerce_product_tag_slug' ) : _x( 'product-tag', 'slug', 'woocommerce' );

		if ( 'yes' === get_option( 'woocommerce_prepend_shop_page_to_products' ) ) {
			$product_base = trailingslashit( $base_slug );
		} else {
			$product_slug = get_option( 'woocommerce_product_slug' );
			if ( false !== $product_slug && ! empty( $product_slug ) ) {
				$product_base = trailingslashit( $product_slug );
			} else {
				$product_base = trailingslashit( _x( 'product', 'slug', 'woocommerce' ) );
			}
		}

		if ( 'yes' === get_option( 'woocommerce_prepend_category_to_products' ) ) {
			$product_base .= trailingslashit( '%product_cat%' );
		}

		$permalinks = array(
			'product_base'   => untrailingslashit( $product_base ),
			'category_base'  => untrailingslashit( $category_base . $category_slug ),
			'attribute_base' => untrailingslashit( $category_base ),
			'tag_base'       => untrailingslashit( $category_base . $tag_slug ),
		);

		update_option( 'woocommerce_permalinks', $permalinks );
	}
}

/**
 * Update sub-category display options for 2.0
 *
 * @return void
 */
function wc_update_200_subcat_display() {
	// Update subcat display settings.
	if ( 'yes' === get_option( 'woocommerce_shop_show_subcategories' ) ) {
		if ( 'yes' === get_option( 'woocommerce_hide_products_when_showing_subcategories' ) ) {
			update_option( 'woocommerce_shop_page_display', 'subcategories' );
		} else {
			update_option( 'woocommerce_shop_page_display', 'both' );
		}
	}

	if ( 'yes' === get_option( 'woocommerce_show_subcategories' ) ) {
		if ( 'yes' === get_option( 'woocommerce_hide_products_when_showing_subcategories' ) ) {
			update_option( 'woocommerce_category_archive_display', 'subcategories' );
		} else {
			update_option( 'woocommerce_category_archive_display', 'both' );
		}
	}
}

/**
 * Update tax rates for 2.0
 *
 * @return void
 */
function wc_update_200_taxrates() {
	global $wpdb;

	// Update tax rates.
	$loop      = 0;
	$tax_rates = get_option( 'woocommerce_tax_rates' );

	if ( $tax_rates ) {
		foreach ( $tax_rates as $tax_rate ) {

			foreach ( $tax_rate['countries'] as $country => $states ) {

				$states = array_reverse( $states );

				foreach ( $states as $state ) {

					if ( '*' === $state ) {
						$state = '';
					}

					$wpdb->insert(
						$wpdb->prefix . 'woocommerce_tax_rates',
						array(
							'tax_rate_country'  => $country,
							'tax_rate_state'    => $state,
							'tax_rate'          => $tax_rate['rate'],
							'tax_rate_name'     => $tax_rate['label'],
							'tax_rate_priority' => 1,
							'tax_rate_compound' => ( 'yes' === $tax_rate['compound'] ) ? 1 : 0,
							'tax_rate_shipping' => ( 'yes' === $tax_rate['shipping'] ) ? 1 : 0,
							'tax_rate_order'    => $loop,
							'tax_rate_class'    => $tax_rate['class'],
						)
					);

					$loop++;
				}
			}
		}
	}

	$local_tax_rates = get_option( 'woocommerce_local_tax_rates' );

	if ( $local_tax_rates ) {
		foreach ( $local_tax_rates as $tax_rate ) {

			$location_type = ( 'postcode' === $tax_rate['location_type'] ) ? 'postcode' : 'city';

			if ( '*' === $tax_rate['state'] ) {
				$tax_rate['state'] = '';
			}

			$wpdb->insert(
				$wpdb->prefix . 'woocommerce_tax_rates',
				array(
					'tax_rate_country'  => $tax_rate['country'],
					'tax_rate_state'    => $tax_rate['state'],
					'tax_rate'          => $tax_rate['rate'],
					'tax_rate_name'     => $tax_rate['label'],
					'tax_rate_priority' => 2,
					'tax_rate_compound' => ( 'yes' === $tax_rate['compound'] ) ? 1 : 0,
					'tax_rate_shipping' => ( 'yes' === $tax_rate['shipping'] ) ? 1 : 0,
					'tax_rate_order'    => $loop,
					'tax_rate_class'    => $tax_rate['class'],
				)
			);

			$tax_rate_id = $wpdb->insert_id;

			if ( $tax_rate['locations'] ) {
				foreach ( $tax_rate['locations'] as $location ) {

					$wpdb->insert(
						$wpdb->prefix . 'woocommerce_tax_rate_locations',
						array(
							'location_code' => $location,
							'tax_rate_id'   => $tax_rate_id,
							'location_type' => $location_type,
						)
					);

				}
			}

			$loop++;
		}
	}

	update_option( 'woocommerce_tax_rates_backup', $tax_rates );
	update_option( 'woocommerce_local_tax_rates_backup', $local_tax_rates );
	delete_option( 'woocommerce_tax_rates' );
	delete_option( 'woocommerce_local_tax_rates' );
}

/**
 * Update order item line items for 2.0
 *
 * @return void
 */
function wc_update_200_line_items() {
	global $wpdb;

	// Now its time for the massive update to line items - move them to the new DB tables.
	// Reverse with UPDATE `wpwc_postmeta` SET meta_key = '_order_items' WHERE meta_key = '_order_items_old'.
	$order_item_rows = $wpdb->get_results(
		"SELECT meta_value, post_id FROM {$wpdb->postmeta} WHERE meta_key = '_order_items'"
	);

	foreach ( $order_item_rows as $order_item_row ) {

		$order_items = (array) maybe_unserialize( $order_item_row->meta_value );

		foreach ( $order_items as $order_item ) {

			if ( ! isset( $order_item['line_total'] ) && isset( $order_item['taxrate'] ) && isset( $order_item['cost'] ) ) {
				$order_item['line_tax']          = number_format( ( $order_item['cost'] * $order_item['qty'] ) * ( $order_item['taxrate'] / 100 ), 2, '.', '' );
				$order_item['line_total']        = $order_item['cost'] * $order_item['qty'];
				$order_item['line_subtotal_tax'] = $order_item['line_tax'];
				$order_item['line_subtotal']     = $order_item['line_total'];
			}

			$order_item['line_tax']          = isset( $order_item['line_tax'] ) ? $order_item['line_tax'] : 0;
			$order_item['line_total']        = isset( $order_item['line_total'] ) ? $order_item['line_total'] : 0;
			$order_item['line_subtotal_tax'] = isset( $order_item['line_subtotal_tax'] ) ? $order_item['line_subtotal_tax'] : 0;
			$order_item['line_subtotal']     = isset( $order_item['line_subtotal'] ) ? $order_item['line_subtotal'] : 0;

			$item_id = wc_add_order_item(
				$order_item_row->post_id,
				array(
					'order_item_name' => $order_item['name'],
					'order_item_type' => 'line_item',
				)
			);

			// Add line item meta.
			if ( $item_id ) {
				wc_add_order_item_meta( $item_id, '_qty', absint( $order_item['qty'] ) );
				wc_add_order_item_meta( $item_id, '_tax_class', $order_item['tax_class'] );
				wc_add_order_item_meta( $item_id, '_product_id', $order_item['id'] );
				wc_add_order_item_meta( $item_id, '_variation_id', $order_item['variation_id'] );
				wc_add_order_item_meta( $item_id, '_line_subtotal', wc_format_decimal( $order_item['line_subtotal'] ) );
				wc_add_order_item_meta( $item_id, '_line_subtotal_tax', wc_format_decimal( $order_item['line_subtotal_tax'] ) );
				wc_add_order_item_meta( $item_id, '_line_total', wc_format_decimal( $order_item['line_total'] ) );
				wc_add_order_item_meta( $item_id, '_line_tax', wc_format_decimal( $order_item['line_tax'] ) );

				$meta_rows = array();

				// Insert meta.
				if ( ! empty( $order_item['item_meta'] ) ) {
					foreach ( $order_item['item_meta'] as $key => $meta ) {
						// Backwards compatibility.
						if ( is_array( $meta ) && isset( $meta['meta_name'] ) ) {
							$meta_rows[] = '(' . $item_id . ',"' . esc_sql( $meta['meta_name'] ) . '","' . esc_sql( $meta['meta_value'] ) . '")';
						} else {
							$meta_rows[] = '(' . $item_id . ',"' . esc_sql( $key ) . '","' . esc_sql( $meta ) . '")';
						}
					}
				}

				// Insert meta rows at once.
				if ( count( $meta_rows ) > 0 ) {
					$wpdb->query(
						$wpdb->prepare(
							"INSERT INTO {$wpdb->prefix}woocommerce_order_itemmeta ( order_item_id, meta_key, meta_value )
							VALUES " . implode( ',', $meta_rows ) . ';', // @codingStandardsIgnoreLine
							$order_item_row->post_id
						)
					);
				}

				// Delete from DB (rename).
				$wpdb->query(
					$wpdb->prepare(
						"UPDATE {$wpdb->postmeta}
						SET meta_key = '_order_items_old'
						WHERE meta_key = '_order_items'
						AND post_id = %d",
						$order_item_row->post_id
					)
				);
			}

			unset( $meta_rows, $item_id, $order_item );
		}
	}

	// Do the same kind of update for order_taxes - move to lines.
	// Reverse with UPDATE `wpwc_postmeta` SET meta_key = '_order_taxes' WHERE meta_key = '_order_taxes_old'.
	$order_tax_rows = $wpdb->get_results(
		"SELECT meta_value, post_id FROM {$wpdb->postmeta}
		WHERE meta_key = '_order_taxes'"
	);

	foreach ( $order_tax_rows as $order_tax_row ) {

		$order_taxes = (array) maybe_unserialize( $order_tax_row->meta_value );

		if ( ! empty( $order_taxes ) ) {
			foreach ( $order_taxes as $order_tax ) {

				if ( ! isset( $order_tax['label'] ) || ! isset( $order_tax['cart_tax'] ) || ! isset( $order_tax['shipping_tax'] ) ) {
					continue;
				}

				$item_id = wc_add_order_item(
					$order_tax_row->post_id,
					array(
						'order_item_name' => $order_tax['label'],
						'order_item_type' => 'tax',
					)
				);

				// Add line item meta.
				if ( $item_id ) {
					wc_add_order_item_meta( $item_id, 'compound', absint( isset( $order_tax['compound'] ) ? $order_tax['compound'] : 0 ) );
					wc_add_order_item_meta( $item_id, 'tax_amount', wc_clean( $order_tax['cart_tax'] ) );
					wc_add_order_item_meta( $item_id, 'shipping_tax_amount', wc_clean( $order_tax['shipping_tax'] ) );
				}

				// Delete from DB (rename).
				$wpdb->query(
					$wpdb->prepare(
						"UPDATE {$wpdb->postmeta}
						SET meta_key = '_order_taxes_old'
						WHERE meta_key = '_order_taxes'
						AND post_id = %d",
						$order_tax_row->post_id
					)
				);

				unset( $tax_amount );
			}
		}
	}
}

/**
 * Update image settings for 2.0
 *
 * @return void
 */
function wc_update_200_images() {
	// Grab the pre 2.0 Image options and use to populate the new image options settings,
	// cleaning up afterwards like nice people do.
	foreach ( array( 'catalog', 'single', 'thumbnail' ) as $value ) {

		$old_settings = array_filter(
			array(
				'width'  => get_option( 'woocommerce_' . $value . '_image_width' ),
				'height' => get_option( 'woocommerce_' . $value . '_image_height' ),
				'crop'   => get_option( 'woocommerce_' . $value . '_image_crop' ),
			)
		);

		if ( ! empty( $old_settings ) && update_option( 'shop_' . $value . '_image_size', $old_settings ) ) {

			delete_option( 'woocommerce_' . $value . '_image_width' );
			delete_option( 'woocommerce_' . $value . '_image_height' );
			delete_option( 'woocommerce_' . $value . '_image_crop' );

		}
	}
}

/**
 * Update DB version for 2.0
 *
 * @return void
 */
function wc_update_200_db_version() {
	WC_Install::update_db_version( '2.0.0' );
}

/**
 * Update Brazilian States for 2.0.9
 *
 * @return void
 */
function wc_update_209_brazillian_state() {
	global $wpdb;

	// phpcs:disable WordPress.DB.SlowDBQuery

	// Update brazillian state codes.
	$wpdb->update(
		$wpdb->postmeta,
		array(
			'meta_value' => 'BA',
		),
		array(
			'meta_key'   => '_billing_state',
			'meta_value' => 'BH',
		)
	);
	$wpdb->update(
		$wpdb->postmeta,
		array(
			'meta_value' => 'BA',
		),
		array(
			'meta_key'   => '_shipping_state',
			'meta_value' => 'BH',
		)
	);
	$wpdb->update(
		$wpdb->usermeta,
		array(
			'meta_value' => 'BA',
		),
		array(
			'meta_key'   => 'billing_state',
			'meta_value' => 'BH',
		)
	);
	$wpdb->update(
		$wpdb->usermeta,
		array(
			'meta_value' => 'BA',
		),
		array(
			'meta_key'   => 'shipping_state',
			'meta_value' => 'BH',
		)
	);

	// phpcs:enable WordPress.DB.SlowDBQuery
}

/**
 * Update DB version for 2.0.9
 *
 * @return void
 */
function wc_update_209_db_version() {
	WC_Install::update_db_version( '2.0.9' );
}

/**
 * Remove pages for 2.1
 *
 * @return void
 */
function wc_update_210_remove_pages() {
	// Pages no longer used.
	wp_trash_post( get_option( 'woocommerce_pay_page_id' ) );
	wp_trash_post( get_option( 'woocommerce_thanks_page_id' ) );
	wp_trash_post( get_option( 'woocommerce_view_order_page_id' ) );
	wp_trash_post( get_option( 'woocommerce_change_password_page_id' ) );
	wp_trash_post( get_option( 'woocommerce_edit_address_page_id' ) );
	wp_trash_post( get_option( 'woocommerce_lost_password_page_id' ) );
}

/**
 * Update file paths to support multiple files for 2.1
 *
 * @return void
 */
function wc_update_210_file_paths() {
	global $wpdb;

	// Upgrade file paths to support multiple file paths + names etc.
	$existing_file_paths = $wpdb->get_results( "SELECT meta_value, meta_id FROM {$wpdb->postmeta} WHERE meta_key = '_file_paths' AND meta_value != '';" );

	if ( $existing_file_paths ) {

		foreach ( $existing_file_paths as $existing_file_path ) {

			$needs_update = false;
			$new_value    = array();
			$value        = maybe_unserialize( trim( $existing_file_path->meta_value ) );

			if ( $value ) {
				foreach ( $value as $key => $file ) {
					if ( ! is_array( $file ) ) {
						$needs_update      = true;
						$new_value[ $key ] = array(
							'file' => $file,
							'name' => wc_get_filename_from_url( $file ),
						);
					} else {
						$new_value[ $key ] = $file;
					}
				}
				if ( $needs_update ) {
					// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
					$new_value = serialize( $new_value );

					$wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_key = %s, meta_value = %s WHERE meta_id = %d", '_downloadable_files', $new_value, $existing_file_path->meta_id ) );
				}
			}
		}
	}
}

/**
 * Update DB version for 2.1
 *
 * @return void
 */
function wc_update_210_db_version() {
	WC_Install::update_db_version( '2.1.0' );
}

/**
 * Update shipping options for 2.2
 *
 * @return void
 */
function wc_update_220_shipping() {
	$woocommerce_ship_to_destination = 'shipping';

	if ( get_option( 'woocommerce_ship_to_billing_address_only' ) === 'yes' ) {
		$woocommerce_ship_to_destination = 'billing_only';
	} elseif ( get_option( 'woocommerce_ship_to_billing' ) === 'yes' ) {
		$woocommerce_ship_to_destination = 'billing';
	}

	add_option( 'woocommerce_ship_to_destination', $woocommerce_ship_to_destination, '', 'no' );
}

/**
 * Update order statuses for 2.2
 *
 * @return void
 */
function wc_update_220_order_status() {
	global $wpdb;
	$wpdb->query(
		"UPDATE {$wpdb->posts} as posts
		LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id
		LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id )
		LEFT JOIN {$wpdb->terms} AS term USING( term_id )
		SET posts.post_status = 'wc-pending'
		WHERE posts.post_type = 'shop_order'
		AND posts.post_status = 'publish'
		AND tax.taxonomy = 'shop_order_status'
		AND	term.slug LIKE 'pending%';"
	);
	$wpdb->query(
		"UPDATE {$wpdb->posts} as posts
		LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id
		LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id )
		LEFT JOIN {$wpdb->terms} AS term USING( term_id )
		SET posts.post_status = 'wc-processing'
		WHERE posts.post_type = 'shop_order'
		AND posts.post_status = 'publish'
		AND tax.taxonomy = 'shop_order_status'
		AND	term.slug LIKE 'processing%';"
	);
	$wpdb->query(
		"UPDATE {$wpdb->posts} as posts
		LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id
		LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id )
		LEFT JOIN {$wpdb->terms} AS term USING( term_id )
		SET posts.post_status = 'wc-on-hold'
		WHERE posts.post_type = 'shop_order'
		AND posts.post_status = 'publish'
		AND tax.taxonomy = 'shop_order_status'
		AND	term.slug LIKE 'on-hold%';"
	);
	$wpdb->query(
		"UPDATE {$wpdb->posts} as posts
		LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id
		LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id )
		LEFT JOIN {$wpdb->terms} AS term USING( term_id )
		SET posts.post_status = 'wc-completed'
		WHERE posts.post_type = 'shop_order'
		AND posts.post_status = 'publish'
		AND tax.taxonomy = 'shop_order_status'
		AND	term.slug LIKE 'completed%';"
	);
	$wpdb->query(
		"UPDATE {$wpdb->posts} as posts
		LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id
		LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id )
		LEFT JOIN {$wpdb->terms} AS term USING( term_id )
		SET posts.post_status = 'wc-cancelled'
		WHERE posts.post_type = 'shop_order'
		AND posts.post_status = 'publish'
		AND tax.taxonomy = 'shop_order_status'
		AND	term.slug LIKE 'cancelled%';"
	);
	$wpdb->query(
		"UPDATE {$wpdb->posts} as posts
		LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id
		LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id )
		LEFT JOIN {$wpdb->terms} AS term USING( term_id )
		SET posts.post_status = 'wc-refunded'
		WHERE posts.post_type = 'shop_order'
		AND posts.post_status = 'publish'
		AND tax.taxonomy = 'shop_order_status'
		AND	term.slug LIKE 'refunded%';"
	);
	$wpdb->query(
		"UPDATE {$wpdb->posts} as posts
		LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id
		LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id )
		LEFT JOIN {$wpdb->terms} AS term USING( term_id )
		SET posts.post_status = 'wc-failed'
		WHERE posts.post_type = 'shop_order'
		AND posts.post_status = 'publish'
		AND tax.taxonomy = 'shop_order_status'
		AND	term.slug LIKE 'failed%';"
	);
}

/**
 * Update variations for 2.2
 *
 * @return void
 */
function wc_update_220_variations() {
	global $wpdb;
	// Update variations which manage stock.
	$update_variations = $wpdb->get_results(
		"SELECT DISTINCT posts.ID AS variation_id, posts.post_parent AS variation_parent FROM {$wpdb->posts} as posts
		LEFT OUTER JOIN {$wpdb->postmeta} AS postmeta ON posts.ID = postmeta.post_id AND postmeta.meta_key = '_stock'
		LEFT OUTER JOIN {$wpdb->postmeta} as postmeta2 ON posts.ID = postmeta2.post_id AND postmeta2.meta_key = '_manage_stock'
		WHERE posts.post_type = 'product_variation'
		AND postmeta.meta_value IS NOT NULL
		AND postmeta.meta_value != ''
		AND postmeta2.meta_value IS NULL"
	);

	foreach ( $update_variations as $variation ) {
		$parent_backorders = get_post_meta( $variation->variation_parent, '_backorders', true );
		add_post_meta( $variation->variation_id, '_manage_stock', 'yes', true );
		add_post_meta( $variation->variation_id, '_backorders', $parent_backorders ? $parent_backorders : 'no', true );
	}
}

/**
 * Update attributes for 2.2
 *
 * @return void
 */
function wc_update_220_attributes() {
	global $wpdb;
	// Update taxonomy names with correct sanitized names.
	$attribute_taxonomies = $wpdb->get_results( 'SELECT attribute_name, attribute_id FROM ' . $wpdb->prefix . 'woocommerce_attribute_taxonomies' );

	foreach ( $attribute_taxonomies as $attribute_taxonomy ) {
		$sanitized_attribute_name = wc_sanitize_taxonomy_name( $attribute_taxonomy->attribute_name );
		if ( $sanitized_attribute_name !== $attribute_taxonomy->attribute_name ) {
			if ( ! $wpdb->get_var( $wpdb->prepare( "SELECT 1=1 FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_name = %s;", $sanitized_attribute_name ) ) ) {
				// Update attribute.
				$wpdb->update(
					"{$wpdb->prefix}woocommerce_attribute_taxonomies",
					array(
						'attribute_name' => $sanitized_attribute_name,
					),
					array(
						'attribute_id' => $attribute_taxonomy->attribute_id,
					)
				);

				// Update terms.
				$wpdb->update(
					$wpdb->term_taxonomy,
					array( 'taxonomy' => wc_attribute_taxonomy_name( $sanitized_attribute_name ) ),
					array( 'taxonomy' => 'pa_' . $attribute_taxonomy->attribute_name )
				);
			}
		}
	}

	delete_transient( 'wc_attribute_taxonomies' );
	WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' );
}

/**
 * Update DB version for 2.2
 *
 * @return void
 */
function wc_update_220_db_version() {
	WC_Install::update_db_version( '2.2.0' );
}

/**
 * Update options for 2.3
 *
 * @return void
 */
function wc_update_230_options() {
	// _money_spent and _order_count may be out of sync - clear them
	delete_metadata( 'user', 0, '_money_spent', '', true );
	delete_metadata( 'user', 0, '_order_count', '', true );
	delete_metadata( 'user', 0, '_last_order', '', true );

	// To prevent taxes being hidden when using a default 'no address' in a store with tax inc prices, set the woocommerce_default_customer_address to use the store base address by default.
	if ( '' === get_option( 'woocommerce_default_customer_address', false ) && wc_prices_include_tax() ) {
		update_option( 'woocommerce_default_customer_address', 'base' );
	}
}

/**
 * Update DB version for 2.3
 *
 * @return void
 */
function wc_update_230_db_version() {
	WC_Install::update_db_version( '2.3.0' );
}

/**
 * Update calc discount options for 2.4
 *
 * @return void
 */
function wc_update_240_options() {
	/**
	 * Coupon discount calculations.
	 * Maintain the old coupon logic for upgrades.
	 */
	update_option( 'woocommerce_calc_discounts_sequentially', 'yes' );
}

/**
 * Update shipping methods for 2.4
 *
 * @return void
 */
function wc_update_240_shipping_methods() {
	/**
	 * Flat Rate Shipping.
	 * Update legacy options to new math based options.
	 */
	$shipping_methods = array(
		'woocommerce_flat_rates'                        => new WC_Shipping_Legacy_Flat_Rate(),
		'woocommerce_international_delivery_flat_rates' => new WC_Shipping_Legacy_International_Delivery(),
	);
	foreach ( $shipping_methods as $flat_rate_option_key => $shipping_method ) {
		// Stop this running more than once if routine is repeated.
		if ( version_compare( $shipping_method->get_option( 'version', 0 ), '2.4.0', '<' ) ) {
			$shipping_classes  = WC()->shipping()->get_shipping_classes();
			$has_classes       = count( $shipping_classes ) > 0;
			$cost_key          = $has_classes ? 'no_class_cost' : 'cost';
			$min_fee           = $shipping_method->get_option( 'minimum_fee' );
			$math_cost_strings = array(
				'cost'          => array(),
				'no_class_cost' => array(),
			);

			$math_cost_strings[ $cost_key ][] = $shipping_method->get_option( 'cost' );
			$fee                              = $shipping_method->get_option( 'fee' );

			if ( $fee ) {
				$math_cost_strings[ $cost_key ][] = strstr( $fee, '%' ) ? '[fee percent="' . str_replace( '%', '', $fee ) . '" min="' . esc_attr( $min_fee ) . '"]' : $fee;
			}

			foreach ( $shipping_classes as $shipping_class ) {
				$rate_key                       = 'class_cost_' . $shipping_class->slug;
				$math_cost_strings[ $rate_key ] = $math_cost_strings['no_class_cost'];
			}

			$flat_rates = array_filter( (array) get_option( $flat_rate_option_key, array() ) );

			if ( $flat_rates ) {
				foreach ( $flat_rates as $shipping_class => $rate ) {
					$rate_key = 'class_cost_' . $shipping_class;
					if ( $rate['cost'] || $rate['fee'] ) {
						$math_cost_strings[ $rate_key ][] = $rate['cost'];
						$math_cost_strings[ $rate_key ][] = strstr( $rate['fee'], '%' ) ? '[fee percent="' . str_replace( '%', '', $rate['fee'] ) . '" min="' . esc_attr( $min_fee ) . '"]' : $rate['fee'];
					}
				}
			}

			if ( 'item' === $shipping_method->type ) {
				foreach ( $math_cost_strings as $key => $math_cost_string ) {
					$math_cost_strings[ $key ] = array_filter( array_map( 'trim', $math_cost_strings[ $key ] ) );
					if ( ! empty( $math_cost_strings[ $key ] ) ) {
						$last_key                                = max( 0, count( $math_cost_strings[ $key ] ) - 1 );
						$math_cost_strings[ $key ][0]            = '( ' . $math_cost_strings[ $key ][0];
						$math_cost_strings[ $key ][ $last_key ] .= ' ) * [qty]';
					}
				}
			}

			$math_cost_strings['cost'][] = $shipping_method->get_option( 'cost_per_order' );

			// Save settings.
			foreach ( $math_cost_strings as $option_id => $math_cost_string ) {
				$shipping_method->settings[ $option_id ] = implode( ' + ', array_filter( $math_cost_string ) );
			}

			$shipping_method->settings['version'] = '2.4.0';
			$shipping_method->settings['type']    = 'item' === $shipping_method->settings['type'] ? 'class' : $shipping_method->settings['type'];

			update_option( $shipping_method->plugin_id . $shipping_method->id . '_settings', $shipping_method->settings );
		}
	}
}

/**
 * Update API keys for 2.4
 *
 * @return void
 */
function wc_update_240_api_keys() {
	global $wpdb;
	/**
	 * Update the old user API keys to the new Apps keys.
	 */
	$api_users = $wpdb->get_results( "SELECT user_id FROM $wpdb->usermeta WHERE meta_key = 'woocommerce_api_consumer_key'" );
	$apps_keys = array();

	// Get user data.
	foreach ( $api_users as $_user ) {
		$user        = get_userdata( $_user->user_id );
		$apps_keys[] = array(
			'user_id'         => $user->ID,
			'permissions'     => $user->woocommerce_api_key_permissions,
			'consumer_key'    => wc_api_hash( $user->woocommerce_api_consumer_key ),
			'consumer_secret' => $user->woocommerce_api_consumer_secret,
			'truncated_key'   => substr( $user->woocommerce_api_consumer_secret, -7 ),
		);
	}

	if ( ! empty( $apps_keys ) ) {
		// Create new apps.
		foreach ( $apps_keys as $app ) {
			$wpdb->insert(
				$wpdb->prefix . 'woocommerce_api_keys',
				$app,
				array(
					'%d',
					'%s',
					'%s',
					'%s',
					'%s',
				)
			);
		}

		// Delete old user keys from usermeta.
		foreach ( $api_users as $_user ) {
			$user_id = intval( $_user->user_id );
			delete_user_meta( $user_id, 'woocommerce_api_consumer_key' );
			delete_user_meta( $user_id, 'woocommerce_api_consumer_secret' );
			delete_user_meta( $user_id, 'woocommerce_api_key_permissions' );
		}
	}
}

/**
 * Update webhooks for 2.4
 *
 * @return void
 */
function wc_update_240_webhooks() {
	// phpcs:disable WordPress.DB.SlowDBQuery

	/**
	 * Webhooks.
	 * Make sure order.update webhooks get the woocommerce_order_edit_status hook.
	 */
	$order_update_webhooks = get_posts(
		array(
			'posts_per_page' => -1,
			'post_type'      => 'shop_webhook',
			'meta_key'       => '_topic',
			'meta_value'     => 'order.updated',
		)
	);
	foreach ( $order_update_webhooks as $order_update_webhook ) {
		$webhook = new WC_Webhook( $order_update_webhook->ID );
		$webhook->set_topic( 'order.updated' );
	}

	// phpcs:enable WordPress.DB.SlowDBQuery
}

/**
 * Update refunds for 2.4
 *
 * @return void
 */
function wc_update_240_refunds() {
	global $wpdb;
	/**
	 * Refunds for full refunded orders.
	 * Update fully refunded orders to ensure they have a refund line item so reports add up.
	 */
	$refunded_orders = get_posts(
		array(
			'posts_per_page' => -1,
			'post_type'      => 'shop_order',
			'post_status'    => array( 'wc-refunded' ),
		)
	);

	// Ensure emails are disabled during this update routine.
	remove_all_actions( 'woocommerce_order_status_refunded_notification' );
	remove_all_actions( 'woocommerce_order_partially_refunded_notification' );
	remove_action( 'woocommerce_order_status_refunded', array( 'WC_Emails', 'send_transactional_email' ) );
	remove_action( 'woocommerce_order_partially_refunded', array( 'WC_Emails', 'send_transactional_email' ) );

	foreach ( $refunded_orders as $refunded_order ) {
		$order_total    = get_post_meta( $refunded_order->ID, '_order_total', true );
		$refunded_total = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT SUM( postmeta.meta_value )
				FROM $wpdb->postmeta AS postmeta
				INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d )
				WHERE postmeta.meta_key = '_refund_amount'
				AND postmeta.post_id = posts.ID",
				$refunded_order->ID
			)
		);

		if ( $order_total > $refunded_total ) {
			wc_create_refund(
				array(
					'amount'     => $order_total - $refunded_total,
					'reason'     => __( 'Order fully refunded', 'woocommerce' ),
					'order_id'   => $refunded_order->ID,
					'line_items' => array(),
					'date'       => $refunded_order->post_modified,
				)
			);
		}
	}

	wc_delete_shop_order_transients();
}

/**
 * Update DB version for 2.4
 *
 * @return void
 */
function wc_update_240_db_version() {
	WC_Install::update_db_version( '2.4.0' );
}

/**
 * Update variations for 2.4.1
 *
 * @return void
 */
function wc_update_241_variations() {
	global $wpdb;

	// Select variations that don't have any _stock_status implemented on WooCommerce 2.2.
	$update_variations = $wpdb->get_results(
		"SELECT DISTINCT posts.ID AS variation_id, posts.post_parent AS variation_parent
		FROM {$wpdb->posts} as posts
		LEFT OUTER JOIN {$wpdb->postmeta} AS postmeta ON posts.ID = postmeta.post_id AND postmeta.meta_key = '_stock_status'
		WHERE posts.post_type = 'product_variation'
		AND postmeta.meta_value IS NULL"
	);

	foreach ( $update_variations as $variation ) {
		// Get the parent _stock_status.
		$parent_stock_status = get_post_meta( $variation->variation_parent, '_stock_status', true );

		// Set the _stock_status.
		add_post_meta( $variation->variation_id, '_stock_status', $parent_stock_status ? $parent_stock_status : 'instock', true );

		// Delete old product children array.
		delete_transient( 'wc_product_children_' . $variation->variation_parent );
	}

	// Invalidate old transients such as wc_var_price.
	WC_Cache_Helper::get_transient_version( 'product', true );
}

/**
 * Update DB version for 2.4.1
 *
 * @return void
 */
function wc_update_241_db_version() {
	WC_Install::update_db_version( '2.4.1' );
}

/**
 * Update currency settings for 2.5
 *
 * @return void
 */
function wc_update_250_currency() {
	global $wpdb;
	// Fix currency settings for LAK currency.
	$current_currency = get_option( 'woocommerce_currency' );

	if ( 'KIP' === $current_currency ) {
		update_option( 'woocommerce_currency', 'LAK' );
	}

	// phpcs:disable WordPress.DB.SlowDBQuery

	// Update LAK currency code.
	$wpdb->update(
		$wpdb->postmeta,
		array(
			'meta_value' => 'LAK',
		),
		array(
			'meta_key'   => '_order_currency',
			'meta_value' => 'KIP',
		)
	);

	// phpcs:enable WordPress.DB.SlowDBQuery
}

/**
 * Update DB version for 2.5
 *
 * @return void
 */
function wc_update_250_db_version() {
	WC_Install::update_db_version( '2.5.0' );
}

/**
 * Update ship to countries options for 2.6
 *
 * @return void
 */
function wc_update_260_options() {
	// woocommerce_calc_shipping option has been removed in 2.6.
	if ( 'no' === get_option( 'woocommerce_calc_shipping' ) ) {
		update_option( 'woocommerce_ship_to_countries', 'disabled' );
	}

	WC_Admin_Notices::add_notice( 'legacy_shipping' );
}

/**
 * Update term meta for 2.6
 *
 * @return void
 */
function wc_update_260_termmeta() {
	global $wpdb;
	/**
	 * Migrate term meta to WordPress tables.
	 */
	if ( get_option( 'db_version' ) >= 34370 && $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->prefix}woocommerce_termmeta';" ) ) {
		if ( $wpdb->query( "INSERT INTO {$wpdb->termmeta} ( term_id, meta_key, meta_value ) SELECT woocommerce_term_id, meta_key, meta_value FROM {$wpdb->prefix}woocommerce_termmeta;" ) ) {
			$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}woocommerce_termmeta" );
			wp_cache_flush();
		}
	}
}

/**
 * Update zones for 2.6
 *
 * @return void
 */
function wc_update_260_zones() {
	global $wpdb;
	/**
	 * Old (table rate) shipping zones to new core shipping zones migration.
	 * zone_enabled and zone_type are no longer used, but it's safe to leave them be.
	 */
	if ( $wpdb->get_var( "SHOW COLUMNS FROM `{$wpdb->prefix}woocommerce_shipping_zones` LIKE 'zone_enabled';" ) ) {
		$wpdb->query( "ALTER TABLE {$wpdb->prefix}woocommerce_shipping_zones CHANGE `zone_type` `zone_type` VARCHAR(40) NOT NULL DEFAULT '';" );
		$wpdb->query( "ALTER TABLE {$wpdb->prefix}woocommerce_shipping_zones CHANGE `zone_enabled` `zone_enabled` INT(1) NOT NULL DEFAULT 1;" );
	}
}

/**
 * Update zone methods for 2.6
 *
 * @return void
 */
function wc_update_260_zone_methods() {
	global $wpdb;

	/**
	 * Shipping zones in WC 2.6.0 use a table named woocommerce_shipping_zone_methods.
	 * Migrate the old data out of woocommerce_shipping_zone_shipping_methods into the new table and port over any known options (used by table rates and flat rate boxes).
	 */
	if ( $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->prefix}woocommerce_shipping_zone_shipping_methods';" ) ) {
		$old_methods = $wpdb->get_results( "SELECT zone_id, shipping_method_type, shipping_method_order, shipping_method_id FROM {$wpdb->prefix}woocommerce_shipping_zone_shipping_methods;" );

		if ( $old_methods ) {
			$max_new_id = $wpdb->get_var( "SELECT MAX(instance_id) FROM {$wpdb->prefix}woocommerce_shipping_zone_methods" );
			$max_old_id = $wpdb->get_var( "SELECT MAX(shipping_method_id) FROM {$wpdb->prefix}woocommerce_shipping_zone_shipping_methods" );

			// Avoid ID conflicts.
			$wpdb->query( $wpdb->prepare( "ALTER TABLE {$wpdb->prefix}woocommerce_shipping_zone_methods AUTO_INCREMENT = %d;", max( $max_new_id, $max_old_id ) + 1 ) );

			// Store changes.
			$changes = array();

			// Move data.
			foreach ( $old_methods as $old_method ) {
				$wpdb->insert(
					$wpdb->prefix . 'woocommerce_shipping_zone_methods',
					array(
						'zone_id'      => $old_method->zone_id,
						'method_id'    => $old_method->shipping_method_type,
						'method_order' => $old_method->shipping_method_order,
					)
				);

				$new_instance_id = $wpdb->insert_id;

				// Move main settings.
				$older_settings_key = 'woocommerce_' . $old_method->shipping_method_type . '-' . $old_method->shipping_method_id . '_settings';
				$old_settings_key   = 'woocommerce_' . $old_method->shipping_method_type . '_' . $old_method->shipping_method_id . '_settings';
				add_option( 'woocommerce_' . $old_method->shipping_method_type . '_' . $new_instance_id . '_settings', get_option( $old_settings_key, get_option( $older_settings_key ) ) );

				// Handling for table rate and flat rate box shipping.
				if ( 'table_rate' === $old_method->shipping_method_type ) {
					// Move priority settings.
					add_option( 'woocommerce_table_rate_default_priority_' . $new_instance_id, get_option( 'woocommerce_table_rate_default_priority_' . $old_method->shipping_method_id ) );
					add_option( 'woocommerce_table_rate_priorities_' . $new_instance_id, get_option( 'woocommerce_table_rate_priorities_' . $old_method->shipping_method_id ) );

					// Move rates.
					$wpdb->update(
						$wpdb->prefix . 'woocommerce_shipping_table_rates',
						array(
							'shipping_method_id' => $new_instance_id,
						),
						array(
							'shipping_method_id' => $old_method->shipping_method_id,
						)
					);
				} elseif ( 'flat_rate_boxes' === $old_method->shipping_method_type ) {
					$wpdb->update(
						$wpdb->prefix . 'woocommerce_shipping_flat_rate_boxes',
						array(
							'shipping_method_id' => $new_instance_id,
						),
						array(
							'shipping_method_id' => $old_method->shipping_method_id,
						)
					);
				}

				$changes[ $old_method->shipping_method_id ] = $new_instance_id;
			}

			// $changes contains keys (old method ids) and values (new instance ids) if extra processing is needed in plugins.
			// Store this to an option so extensions can pick it up later, then fire an action.
			update_option( 'woocommerce_updated_instance_ids', $changes );
			do_action( 'woocommerce_updated_instance_ids', $changes );
		}
	}

	// Change ranges used to ...
	$wpdb->query( "UPDATE {$wpdb->prefix}woocommerce_shipping_zone_locations SET location_code = REPLACE( location_code, '-', '...' );" );
}

/**
 * Update refunds for 2.6
 *
 * @return void
 */
function wc_update_260_refunds() {
	global $wpdb;
	/**
	 * Refund item qty should be negative.
	 */
	$wpdb->query(
		"UPDATE {$wpdb->prefix}woocommerce_order_itemmeta as item_meta
		LEFT JOIN {$wpdb->prefix}woocommerce_order_items as items ON item_meta.order_item_id = items.order_item_id
		LEFT JOIN {$wpdb->posts} as posts ON items.order_id = posts.ID
		SET item_meta.meta_value = item_meta.meta_value * -1
		WHERE item_meta.meta_value > 0 AND item_meta.meta_key = '_qty' AND posts.post_type = 'shop_order_refund'"
	);
}

/**
 * Update DB version for 2.6
 *
 * @return void
 */
function wc_update_260_db_version() {
	WC_Install::update_db_version( '2.6.0' );
}

/**
 * Update webhooks for 3.0
 *
 * @return void
 */
function wc_update_300_webhooks() {
	// phpcs:disable WordPress.DB.SlowDBQuery

	/**
	 * Make sure product.update webhooks get the woocommerce_product_quick_edit_save
	 * and woocommerce_product_bulk_edit_save hooks.
	 */
	$product_update_webhooks = get_posts(
		array(
			'posts_per_page' => -1,
			'post_type'      => 'shop_webhook',
			'meta_key'       => '_topic',
			'meta_value'     => 'product.updated',
		)
	);
	foreach ( $product_update_webhooks as $product_update_webhook ) {
		$webhook = new WC_Webhook( $product_update_webhook->ID );
		$webhook->set_topic( 'product.updated' );
	}

	// phpcs:enable WordPress.DB.SlowDBQuery
}

/**
 * Add an index to the field comment_type to improve the response time of the query
 * used by WC_Comments::wp_count_comments() to get the number of comments by type.
 */
function wc_update_300_comment_type_index() {
	global $wpdb;

	$index_exists = $wpdb->get_row( "SHOW INDEX FROM {$wpdb->comments} WHERE column_name = 'comment_type' and key_name = 'woo_idx_comment_type'" );

	if ( is_null( $index_exists ) ) {
		// Add an index to the field comment_type to improve the response time of the query
		// used by WC_Comments::wp_count_comments() to get the number of comments by type.
		$wpdb->query( "ALTER TABLE {$wpdb->comments} ADD INDEX woo_idx_comment_type (comment_type)" );
	}
}

/**
 * Update grouped products for 3.0
 *
 * @return void
 */
function wc_update_300_grouped_products() {
	global $wpdb;
	$parents = $wpdb->get_col( "SELECT DISTINCT( post_parent ) FROM {$wpdb->posts} WHERE post_parent > 0 AND post_type = 'product';" );
	foreach ( $parents as $parent_id ) {
		$parent = wc_get_product( $parent_id );
		if ( $parent && $parent->is_type( 'grouped' ) ) {
			$children_ids = get_posts(
				array(
					'post_parent'    => $parent_id,
					'posts_per_page' => -1,
					'post_type'      => 'product',
					'fields'         => 'ids',
				)
			);
			update_post_meta( $parent_id, '_children', $children_ids );

			// Update children to remove the parent.
			$wpdb->update(
				$wpdb->posts,
				array(
					'post_parent' => 0,
				),
				array(
					'post_parent' => $parent_id,
				)
			);
		}
	}
}

/**
 * Update shipping tax classes for 3.0
 *
 * @return void
 */
function wc_update_300_settings() {
	$woocommerce_shipping_tax_class = get_option( 'woocommerce_shipping_tax_class' );
	if ( '' === $woocommerce_shipping_tax_class ) {
		update_option( 'woocommerce_shipping_tax_class', 'inherit' );
	} elseif ( 'standard' === $woocommerce_shipping_tax_class ) {
		update_option( 'woocommerce_shipping_tax_class', '' );
	}
}

/**
 * Convert meta values into term for product visibility.
 */
function wc_update_300_product_visibility() {
	global $wpdb;

	WC_Install::create_terms();

	$featured_term = get_term_by( 'name', 'featured', 'product_visibility' );

	if ( $featured_term ) {
		$wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_featured' AND meta_value = 'yes';", $featured_term->term_taxonomy_id ) );
	}

	$exclude_search_term = get_term_by( 'name', 'exclude-from-search', 'product_visibility' );

	if ( $exclude_search_term ) {
		$wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_visibility' AND meta_value IN ('hidden', 'catalog');", $exclude_search_term->term_taxonomy_id ) );
	}

	$exclude_catalog_term = get_term_by( 'name', 'exclude-from-catalog', 'product_visibility' );

	if ( $exclude_catalog_term ) {
		$wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_visibility' AND meta_value IN ('hidden', 'search');", $exclude_catalog_term->term_taxonomy_id ) );
	}

	$outofstock_term = get_term_by( 'name', 'outofstock', 'product_visibility' );

	if ( $outofstock_term ) {
		$wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_stock_status' AND meta_value = 'outofstock';", $outofstock_term->term_taxonomy_id ) );
	}

	$rating_term = get_term_by( 'name', 'rated-1', 'product_visibility' );

	if ( $rating_term ) {
		$wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_wc_average_rating' AND ROUND( meta_value ) = 1;", $rating_term->term_taxonomy_id ) );
	}

	$rating_term = get_term_by( 'name', 'rated-2', 'product_visibility' );

	if ( $rating_term ) {
		$wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_wc_average_rating' AND ROUND( meta_value ) = 2;", $rating_term->term_taxonomy_id ) );
	}

	$rating_term = get_term_by( 'name', 'rated-3', 'product_visibility' );

	if ( $rating_term ) {
		$wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_wc_average_rating' AND ROUND( meta_value ) = 3;", $rating_term->term_taxonomy_id ) );
	}

	$rating_term = get_term_by( 'name', 'rated-4', 'product_visibility' );

	if ( $rating_term ) {
		$wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_wc_average_rating' AND ROUND( meta_value ) = 4;", $rating_term->term_taxonomy_id ) );
	}

	$rating_term = get_term_by( 'name', 'rated-5', 'product_visibility' );

	if ( $rating_term ) {
		$wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_wc_average_rating' AND ROUND( meta_value ) = 5;", $rating_term->term_taxonomy_id ) );
	}
}

/**
 * Update DB Version.
 */
function wc_update_300_db_version() {
	WC_Install::update_db_version( '3.0.0' );
}

/**
 * Add an index to the downloadable product permissions table to improve performance of update_user_by_order_id.
 */
function wc_update_310_downloadable_products() {
	global $wpdb;

	$index_exists = $wpdb->get_row( "SHOW INDEX FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE column_name = 'order_id' and key_name = 'order_id'" );

	if ( is_null( $index_exists ) ) {
		$wpdb->query( "ALTER TABLE {$wpdb->prefix}woocommerce_downloadable_product_permissions ADD INDEX order_id (order_id)" );
	}
}

/**
 * Find old order notes and ensure they have the correct type for exclusion.
 */
function wc_update_310_old_comments() {
	global $wpdb;

	$wpdb->query( "UPDATE $wpdb->comments comments LEFT JOIN $wpdb->posts as posts ON comments.comment_post_ID = posts.ID SET comment_type = 'order_note' WHERE posts.post_type = 'shop_order' AND comment_type = '';" );
}

/**
 * Update DB Version.
 */
function wc_update_310_db_version() {
	WC_Install::update_db_version( '3.1.0' );
}

/**
 * Update shop_manager capabilities.
 */
function wc_update_312_shop_manager_capabilities() {
	$role = get_role( 'shop_manager' );
	$role->remove_cap( 'unfiltered_html' );
}

/**
 * Update DB Version.
 */
function wc_update_312_db_version() {
	WC_Install::update_db_version( '3.1.2' );
}

/**
 * Update state codes for Mexico.
 */
function wc_update_320_mexican_states() {
	global $wpdb;

	$mx_states = array(
		'Distrito Federal'    => 'CMX',
		'Jalisco'             => 'JAL',
		'Nuevo Leon'          => 'NLE',
		'Aguascalientes'      => 'AGS',
		'Baja California'     => 'BCN',
		'Baja California Sur' => 'BCS',
		'Campeche'            => 'CAM',
		'Chiapas'             => 'CHP',
		'Chihuahua'           => 'CHH',
		'Coahuila'            => 'COA',
		'Colima'              => 'COL',
		'Durango'             => 'DGO',
		'Guanajuato'          => 'GTO',
		'Guerrero'            => 'GRO',
		'Hidalgo'             => 'HGO',
		'Estado de Mexico'    => 'MEX',
		'Michoacan'           => 'MIC',
		'Morelos'             => 'MOR',
		'Nayarit'             => 'NAY',
		'Oaxaca'              => 'OAX',
		'Puebla'              => 'PUE',
		'Queretaro'           => 'QRO',
		'Quintana Roo'        => 'ROO',
		'San Luis Potosi'     => 'SLP',
		'Sinaloa'             => 'SIN',
		'Sonora'              => 'SON',
		'Tabasco'             => 'TAB',
		'Tamaulipas'          => 'TMP',
		'Tlaxcala'            => 'TLA',
		'Veracruz'            => 'VER',
		'Yucatan'             => 'YUC',
		'Zacatecas'           => 'ZAC',
	);

	foreach ( $mx_states as $old => $new ) {
		$wpdb->query(
			$wpdb->prepare(
				"UPDATE $wpdb->postmeta
				SET meta_value = %s
				WHERE meta_key IN ( '_billing_state', '_shipping_state' )
				AND meta_value = %s",
				$new,
				$old
			)
		);
		$wpdb->update(
			"{$wpdb->prefix}woocommerce_shipping_zone_locations",
			array(
				'location_code' => 'MX:' . $new,
			),
			array(
				'location_code' => 'MX:' . $old,
			)
		);
		$wpdb->update(
			"{$wpdb->prefix}woocommerce_tax_rates",
			array(
				'tax_rate_state' => strtoupper( $new ),
			),
			array(
				'tax_rate_state' => strtoupper( $old ),
			)
		);
	}
}

/**
 * Update DB Version.
 */
function wc_update_320_db_version() {
	WC_Install::update_db_version( '3.2.0' );
}

/**
 * Update image settings to use new aspect ratios and widths.
 */
function wc_update_330_image_options() {
	$old_thumbnail_size = get_option( 'shop_catalog_image_size', array() );
	$old_single_size    = get_option( 'shop_single_image_size', array() );

	if ( ! empty( $old_thumbnail_size['width'] ) ) {
		$width     = absint( $old_thumbnail_size['width'] );
		$height    = absint( $old_thumbnail_size['height'] );
		$hard_crop = ! empty( $old_thumbnail_size['crop'] );

		if ( ! $width ) {
			$width = 300;
		}

		if ( ! $height ) {
			$height = $width;
		}

		update_option( 'woocommerce_thumbnail_image_width', $width );

		// Calculate cropping mode from old image options.
		if ( ! $hard_crop ) {
			update_option( 'woocommerce_thumbnail_cropping', 'uncropped' );
		} elseif ( $width === $height ) {
			update_option( 'woocommerce_thumbnail_cropping', '1:1' );
		} else {
			$ratio    = $width / $height;
			$fraction = wc_decimal_to_fraction( $ratio );

			if ( $fraction ) {
				update_option( 'woocommerce_thumbnail_cropping', 'custom' );
				update_option( 'woocommerce_thumbnail_cropping_custom_width', $fraction[0] );
				update_option( 'woocommerce_thumbnail_cropping_custom_height', $fraction[1] );
			}
		}
	}

	// Single is uncropped.
	if ( ! empty( $old_single_size['width'] ) ) {
		update_option( 'woocommerce_single_image_width', absint( $old_single_size['width'] ) );
	}
}

/**
 * Migrate webhooks from post type to CRUD.
 */
function wc_update_330_webhooks() {
	register_post_type( 'shop_webhook' );

	// Map statuses from post_type to Webhooks CRUD.
	$statuses = array(
		'publish' => 'active',
		'draft'   => 'paused',
		'pending' => 'disabled',
	);

	$posts = get_posts(
		array(
			'posts_per_page' => -1,
			'post_type'      => 'shop_webhook',
			'post_status'    => 'any',
		)
	);

	foreach ( $posts as $post ) {
		$webhook = new WC_Webhook();
		$webhook->set_name( $post->post_title );
		$webhook->set_status( isset( $statuses[ $post->post_status ] ) ? $statuses[ $post->post_status ] : 'disabled' );
		$webhook->set_delivery_url( get_post_meta( $post->ID, '_delivery_url', true ) );
		$webhook->set_secret( get_post_meta( $post->ID, '_secret', true ) );
		$webhook->set_topic( get_post_meta( $post->ID, '_topic', true ) );
		$webhook->set_api_version( get_post_meta( $post->ID, '_api_version', true ) );
		$webhook->set_user_id( $post->post_author );
		$webhook->set_pending_delivery( false );
		$webhook->save();

		wp_delete_post( $post->ID, true );
	}

	unregister_post_type( 'shop_webhook' );
}

/**
 * Assign default cat to all products with no cats.
 */
function wc_update_330_set_default_product_cat() {
	/*
	 * When a product category is deleted, we need to check
	 * if the product has no categories assigned. Then assign
	 * it a default category.
	 */
	wc_get_container()->get( AssignDefaultCategory::class )->maybe_assign_default_product_cat();
}

/**
 * Update product stock status to use the new onbackorder status.
 */
function wc_update_330_product_stock_status() {
	global $wpdb;

	if ( 'yes' !== get_option( 'woocommerce_manage_stock' ) ) {
		return;
	}

	$min_stock_amount = (int) get_option( 'woocommerce_notify_no_stock_amount', 0 );

	// Get all products that have stock management enabled, stock less than or equal to min stock amount, and backorders enabled.
	$post_ids = $wpdb->get_col(
		$wpdb->prepare(
			"SELECT t1.post_id FROM $wpdb->postmeta t1
			INNER JOIN $wpdb->postmeta t2
				ON t1.post_id = t2.post_id
				AND t1.meta_key = '_manage_stock' AND t1.meta_value = 'yes'
				AND t2.meta_key = '_stock' AND t2.meta_value <= %d
			INNER JOIN $wpdb->postmeta t3
				ON t2.post_id = t3.post_id
				AND t3.meta_key = '_backorders' AND ( t3.meta_value = 'yes' OR t3.meta_value = 'notify' )",
			$min_stock_amount
		)
	);

	if ( empty( $post_ids ) ) {
		return;
	}

	$post_ids = array_map( 'absint', $post_ids );

	// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
	// Set the status to onbackorder for those products.
	$wpdb->query(
		"UPDATE $wpdb->postmeta
		SET meta_value = 'onbackorder'
		WHERE meta_key = '_stock_status' AND post_id IN ( " . implode( ',', $post_ids ) . ' )'
	);
	// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
}

/**
 * Clear addons page transients
 */
function wc_update_330_clear_transients() {
	delete_transient( 'wc_addons_sections' );
	delete_transient( 'wc_addons_featured' );
}

/**
 * Set PayPal's sandbox credentials.
 */
function wc_update_330_set_paypal_sandbox_credentials() {

	$paypal_settings = get_option( 'woocommerce_paypal_settings' );

	if ( isset( $paypal_settings['testmode'] ) && 'yes' === $paypal_settings['testmode'] ) {
		foreach ( array( 'api_username', 'api_password', 'api_signature' ) as $credential ) {
			if ( ! empty( $paypal_settings[ $credential ] ) ) {
				$paypal_settings[ 'sandbox_' . $credential ] = $paypal_settings[ $credential ];
			}
		}

		update_option( 'woocommerce_paypal_settings', $paypal_settings );
	}
}

/**
 * Update DB Version.
 */
function wc_update_330_db_version() {
	WC_Install::update_db_version( '3.3.0' );
}

/**
 * Update state codes for Ireland and BD.
 */
function wc_update_340_states() {
	$country_states = array(
		'IE' => array(
			'CK' => 'CO',
			'DN' => 'D',
			'GY' => 'G',
			'TY' => 'TA',
		),
		'BD' => array(
			'BAG'  => 'BD-05',
			'BAN'  => 'BD-01',
			'BAR'  => 'BD-02',
			'BARI' => 'BD-06',
			'BHO'  => 'BD-07',
			'BOG'  => 'BD-03',
			'BRA'  => 'BD-04',
			'CHA'  => 'BD-09',
			'CHI'  => 'BD-10',
			'CHU'  => 'BD-12',
			'COX'  => 'BD-11',
			'COM'  => 'BD-08',
			'DHA'  => 'BD-13',
			'DIN'  => 'BD-14',
			'FAR'  => 'BD-15',
			'FEN'  => 'BD-16',
			'GAI'  => 'BD-19',
			'GAZI' => 'BD-18',
			'GOP'  => 'BD-17',
			'HAB'  => 'BD-20',
			'JAM'  => 'BD-21',
			'JES'  => 'BD-22',
			'JHA'  => 'BD-25',
			'JHE'  => 'BD-23',
			'JOY'  => 'BD-24',
			'KHA'  => 'BD-29',
			'KHU'  => 'BD-27',
			'KIS'  => 'BD-26',
			'KUR'  => 'BD-28',
			'KUS'  => 'BD-30',
			'LAK'  => 'BD-31',
			'LAL'  => 'BD-32',
			'MAD'  => 'BD-36',
			'MAG'  => 'BD-37',
			'MAN'  => 'BD-33',
			'MEH'  => 'BD-39',
			'MOU'  => 'BD-38',
			'MUN'  => 'BD-35',
			'MYM'  => 'BD-34',
			'NAO'  => 'BD-48',
			'NAR'  => 'BD-43',
			'NARG' => 'BD-40',
			'NARD' => 'BD-42',
			'NAT'  => 'BD-44',
			'NAW'  => 'BD-45',
			'NET'  => 'BD-41',
			'NIL'  => 'BD-46',
			'NOA'  => 'BD-47',
			'PAB'  => 'BD-49',
			'PAN'  => 'BD-52',
			'PAT'  => 'BD-51',
			'PIR'  => 'BD-50',
			'RAJB' => 'BD-53',
			'RAJ'  => 'BD-54',
			'RAN'  => 'BD-56',
			'RANP' => 'BD-55',
			'SAT'  => 'BD-58',
			'SHA'  => 'BD-57',
			'SIR'  => 'BD-59',
			'SUN'  => 'BD-61',
			'SYL'  => 'BD-60',
			'TAN'  => 'BD-63',
			'THA'  => 'BD-64',
		),
	);

	update_option( 'woocommerce_update_340_states', $country_states );
}

/**
 * Update next state in the queue.
 *
 * @return bool True to run again, false if completed.
 */
function wc_update_340_state() {
	global $wpdb;

	$country_states = array_filter( (array) get_option( 'woocommerce_update_340_states', array() ) );

	if ( empty( $country_states ) ) {
		return false;
	}

	foreach ( $country_states as $country => $states ) {
		foreach ( $states as $old => $new ) {
			$wpdb->query(
				$wpdb->prepare(
					"UPDATE $wpdb->postmeta
					SET meta_value = %s
					WHERE meta_key IN ( '_billing_state', '_shipping_state' )
					AND meta_value = %s",
					$new,
					$old
				)
			);
			$wpdb->update(
				"{$wpdb->prefix}woocommerce_shipping_zone_locations",
				array(
					'location_code' => $country . ':' . $new,
				),
				array(
					'location_code' => $country . ':' . $old,
				)
			);
			$wpdb->update(
				"{$wpdb->prefix}woocommerce_tax_rates",
				array(
					'tax_rate_state' => strtoupper( $new ),
				),
				array(
					'tax_rate_state' => strtoupper( $old ),
				)
			);
			unset( $country_states[ $country ][ $old ] );

			if ( empty( $country_states[ $country ] ) ) {
				unset( $country_states[ $country ] );
			}
			break 2;
		}
	}

	if ( ! empty( $country_states ) ) {
		return update_option( 'woocommerce_update_340_states', $country_states );
	}

	delete_option( 'woocommerce_update_340_states' );

	return false;
}

/**
 * Set last active prop for users.
 */
function wc_update_340_last_active() {
	global $wpdb;
	// @codingStandardsIgnoreStart.
	$wpdb->query(
		$wpdb->prepare( "
			INSERT INTO {$wpdb->usermeta} (user_id, meta_key, meta_value)
			SELECT DISTINCT users.ID, 'wc_last_active', %s
			FROM {$wpdb->users} as users
			LEFT OUTER JOIN {$wpdb->usermeta} AS usermeta ON users.ID = usermeta.user_id AND usermeta.meta_key = 'wc_last_active'
			WHERE usermeta.meta_value IS NULL
			",
			(string) strtotime( date( 'Y-m-d', current_time( 'timestamp', true ) ) )
		)
	);
	// @codingStandardsIgnoreEnd.
}

/**
 * Update DB Version.
 */
function wc_update_340_db_version() {
	WC_Install::update_db_version( '3.4.0' );
}

/**
 * Remove duplicate foreign keys
 *
 * @return void
 */
function wc_update_343_cleanup_foreign_keys() {
	global $wpdb;

	$results = $wpdb->get_results(
		"SELECT CONSTRAINT_NAME
		FROM information_schema.TABLE_CONSTRAINTS
		WHERE CONSTRAINT_SCHEMA = '{$wpdb->dbname}'
		AND CONSTRAINT_NAME LIKE '%wc_download_log_ib%'
		AND CONSTRAINT_TYPE = 'FOREIGN KEY'
		AND TABLE_NAME = '{$wpdb->prefix}wc_download_log'"
	);

	if ( $results ) {
		foreach ( $results as $fk ) {
			$wpdb->query( "ALTER TABLE {$wpdb->prefix}wc_download_log DROP FOREIGN KEY {$fk->CONSTRAINT_NAME}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		}
	}
}

/**
 * Update DB version.
 *
 * @return void
 */
function wc_update_343_db_version() {
	WC_Install::update_db_version( '3.4.3' );
}

/**
 * Recreate user roles so existing users will get the new capabilities.
 *
 * @return void
 */
function wc_update_344_recreate_roles() {
	WC_Install::remove_roles();
	WC_Install::create_roles();
}

/**
 * Update DB version.
 *
 * @return void
 */
function wc_update_344_db_version() {
	WC_Install::update_db_version( '3.4.4' );
}

/**
 * Set the comment type to 'review' for product reviews that don't have a comment type.
 */
function wc_update_350_reviews_comment_type() {
	global $wpdb;

	$wpdb->query(
		"UPDATE {$wpdb->prefix}comments JOIN {$wpdb->prefix}posts ON {$wpdb->prefix}posts.ID = {$wpdb->prefix}comments.comment_post_ID AND ( {$wpdb->prefix}posts.post_type = 'product' OR {$wpdb->prefix}posts.post_type = 'product_variation' ) SET {$wpdb->prefix}comments.comment_type = 'review' WHERE {$wpdb->prefix}comments.comment_type = ''"
	);
}

/**
 * Update DB Version.
 */
function wc_update_350_db_version() {
	WC_Install::update_db_version( '3.5.0' );
}

/**
 * Drop the fk_wc_download_log_permission_id FK as we use a new one with the table and blog prefix for MS compatability.
 *
 * @return void
 */
function wc_update_352_drop_download_log_fk() {
	global $wpdb;
	$results = $wpdb->get_results(
		"SELECT CONSTRAINT_NAME
		FROM information_schema.TABLE_CONSTRAINTS
		WHERE CONSTRAINT_SCHEMA = '{$wpdb->dbname}'
		AND CONSTRAINT_NAME = 'fk_wc_download_log_permission_id'
		AND CONSTRAINT_TYPE = 'FOREIGN KEY'
		AND TABLE_NAME = '{$wpdb->prefix}wc_download_log'"
	);

	// We only need to drop the old key as WC_Install::create_tables() takes care of creating the new FK.
	if ( $results ) {
		$wpdb->query( "ALTER TABLE {$wpdb->prefix}wc_download_log DROP FOREIGN KEY fk_wc_download_log_permission_id" ); // phpcs:ignore WordPress.WP.PreparedSQL.NotPrepared
	}
}

/**
 * Remove edit_user capabilities from shop managers and use "translated" capabilities instead.
 * See wc_shop_manager_has_capability function.
 */
function wc_update_354_modify_shop_manager_caps() {
	global $wp_roles;

	if ( ! class_exists( 'WP_Roles' ) ) {
		return;
	}

	if ( ! isset( $wp_roles ) ) {
		$wp_roles = new WP_Roles(); // @codingStandardsIgnoreLine
	}

	$wp_roles->remove_cap( 'shop_manager', 'edit_users' );
}

/**
 * Update DB Version.
 */
function wc_update_354_db_version() {
	WC_Install::update_db_version( '3.5.4' );
}

/**
 * Update product lookup tables in bulk.
 */
function wc_update_360_product_lookup_tables() {
	wc_update_product_lookup_tables();
}

/**
 * Renames ordering meta to be consistent across taxonomies.
 */
function wc_update_360_term_meta() {
	global $wpdb;

	$wpdb->query( "UPDATE {$wpdb->termmeta} SET meta_key = 'order' WHERE meta_key LIKE 'order_pa_%';" );
}

/**
 * Add new user_order_remaining_expires to speed up user download permission fetching.
 *
 * @return void
 */
function wc_update_360_downloadable_product_permissions_index() {
	global $wpdb;

	$index_exists = $wpdb->get_row( "SHOW INDEX FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE key_name = 'user_order_remaining_expires'" );

	if ( is_null( $index_exists ) ) {
		$wpdb->query( "ALTER TABLE {$wpdb->prefix}woocommerce_downloadable_product_permissions ADD INDEX user_order_remaining_expires (user_id,order_id,downloads_remaining,access_expires)" );
	}
}

/**
 * Update DB Version.
 */
function wc_update_360_db_version() {
	WC_Install::update_db_version( '3.6.0' );
}

/**
 * Put tax classes into a DB table.
 *
 * @return void
 */
function wc_update_370_tax_rate_classes() {
	global $wpdb;

	$classes = array_map( 'trim', explode( "\n", get_option( 'woocommerce_tax_classes' ) ) );

	if ( $classes ) {
		foreach ( $classes as $class ) {
			if ( empty( $class ) ) {
				continue;
			}
			WC_Tax::create_tax_class( $class );
		}
	}
	delete_option( 'woocommerce_tax_classes' );
}

/**
 * Update currency settings for 3.7.0
 *
 * @return void
 */
function wc_update_370_mro_std_currency() {
	global $wpdb;

	// Fix currency settings for MRU and STN currency.
	$current_currency = get_option( 'woocommerce_currency' );

	if ( 'MRO' === $current_currency ) {
		update_option( 'woocommerce_currency', 'MRU' );
	}

	if ( 'STD' === $current_currency ) {
		update_option( 'woocommerce_currency', 'STN' );
	}

	// Update MRU currency code.
	$wpdb->update(
		$wpdb->postmeta,
		array(
			'meta_value' => 'MRU', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
		),
		array(
			'meta_key'   => '_order_currency', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
			'meta_value' => 'MRO', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
		)
	);

	// Update STN currency code.
	$wpdb->update(
		$wpdb->postmeta,
		array(
			'meta_value' => 'STN', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
		),
		array(
			'meta_key'   => '_order_currency', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
			'meta_value' => 'STD', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
		)
	);
}

/**
 * Update DB Version.
 */
function wc_update_370_db_version() {
	WC_Install::update_db_version( '3.7.0' );
}

/**
 * We've moved the MaxMind database to a new location, as per the TOS' requirement that the database not
 * be publicly accessible.
 */
function wc_update_390_move_maxmind_database() {
	// Make sure to use all of the correct filters to pull the local database path.
	$old_path = apply_filters( 'woocommerce_geolocation_local_database_path', WP_CONTENT_DIR . '/uploads/GeoLite2-Country.mmdb', 2 );

	// Generate a prefix for the old file and store it in the integration as it would expect it.
	$prefix = wp_generate_password( 32, false );
	update_option( 'woocommerce_maxmind_geolocation_settings', array( 'database_prefix' => $prefix ) );

	// Generate the new path in the same way that the integration will.
	$uploads_dir = wp_upload_dir();
	$new_path    = trailingslashit( $uploads_dir['basedir'] ) . 'woocommerce_uploads/' . $prefix . '-GeoLite2-Country.mmdb';
	$new_path    = apply_filters( 'woocommerce_geolocation_local_database_path', $new_path, 2 );
	$new_path    = apply_filters( 'woocommerce_maxmind_geolocation_database_path', $new_path );

	// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
	@rename( $old_path, $new_path );
}

/**
 * So that we can best meet MaxMind's TOS, the geolocation database update cron should run once per 15 days.
 */
function wc_update_390_change_geolocation_database_update_cron() {
	wp_clear_scheduled_hook( 'woocommerce_geoip_updater' );
	wp_schedule_event( time() + ( DAY_IN_SECONDS * 15 ), 'fifteendays', 'woocommerce_geoip_updater' );
}

/**
 * Update DB version.
 */
function wc_update_390_db_version() {
	WC_Install::update_db_version( '3.9.0' );
}

/**
 * Increase column size
 */
function wc_update_400_increase_size_of_column() {
	global $wpdb;
	$wpdb->query( "ALTER TABLE {$wpdb->prefix}wc_product_meta_lookup MODIFY COLUMN `min_price` decimal(19,4) NULL default NULL" );
	$wpdb->query( "ALTER TABLE {$wpdb->prefix}wc_product_meta_lookup MODIFY COLUMN `max_price` decimal(19,4) NULL default NULL" );
}

/**
 * Reset ActionScheduler migration status. Needs AS >= 3.0 shipped with WC >= 4.0.
 */
function wc_update_400_reset_action_scheduler_migration_status() {
	if (
		class_exists( 'ActionScheduler_DataController' ) &&
		method_exists( 'ActionScheduler_DataController', 'mark_migration_incomplete' )
	) {
		\ActionScheduler_DataController::mark_migration_incomplete();
	}
}

/**
 * Update DB version.
 */
function wc_update_400_db_version() {
	WC_Install::update_db_version( '4.0.0' );
}

/**
 * Register attributes as terms for variable products, in increments of 100 products.
 *
 * This migration was added to support a new mechanism to improve the filtering of
 * variable products by attribute (https://github.com/woocommerce/woocommerce/pull/26260),
 * however that mechanism was later reverted (https://github.com/woocommerce/woocommerce/pull/27625)
 * due to numerous issues found. Thus the migration is no longer needed.
 *
 * @return bool true if the migration needs to be run again.
 */
function wc_update_440_insert_attribute_terms_for_variable_products() {
	return false;
}

/**
 * Update DB version.
 */
function wc_update_440_db_version() {
	WC_Install::update_db_version( '4.4.0' );
}

/**
 * Update DB version to 4.5.0.
 */
function wc_update_450_db_version() {
	WC_Install::update_db_version( '4.5.0' );
}

/**
 * Sanitize all coupons code.
 *
 * @return bool True to run again, false if completed.
 */
function wc_update_450_sanitize_coupons_code() {
	global $wpdb;

	$coupon_id      = 0;
	$last_coupon_id = get_option( 'woocommerce_update_450_last_coupon_id', '0' );

	$coupons = $wpdb->get_results(
		$wpdb->prepare(
			"SELECT ID, post_title FROM $wpdb->posts WHERE ID > %d AND post_type = 'shop_coupon' LIMIT 10",
			$last_coupon_id
		),
		ARRAY_A
	);

	if ( empty( $coupons ) ) {
		delete_option( 'woocommerce_update_450_last_coupon_id' );
		return false;
	}

	foreach ( $coupons as $key => $data ) {
		$coupon_id = intval( $data['ID'] );
		$code      = trim( wp_filter_kses( $data['post_title'] ) );

		if ( ! empty( $code ) && $data['post_title'] !== $code ) {
			$wpdb->update(
				$wpdb->posts,
				array(
					'post_title' => $code,
				),
				array(
					'ID' => $coupon_id,
				),
				array(
					'%s',
				),
				array(
					'%d',
				)
			);

			// Clean cache.
			clean_post_cache( $coupon_id );
			wp_cache_delete( WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $data['post_title'], 'coupons' );
		}
	}

	// Start the run again.
	if ( $coupon_id ) {
		return update_option( 'woocommerce_update_450_last_coupon_id', $coupon_id );
	}

	delete_option( 'woocommerce_update_450_last_coupon_id' );
	return false;
}

/**
 * Fixes product review count that might have been incorrect.
 *
 * See @link https://github.com/woocommerce/woocommerce/issues/27688.
 */
function wc_update_500_fix_product_review_count() {
	global $wpdb;

	$product_id      = 0;
	$last_product_id = get_option( 'woocommerce_update_500_last_product_id', '0' );

	$products_data = $wpdb->get_results(
		$wpdb->prepare(
			"
				SELECT post_id, meta_value
				FROM $wpdb->postmeta
				JOIN $wpdb->posts
					ON $wpdb->postmeta.post_id = $wpdb->posts.ID
				WHERE
					post_type = 'product'
					AND post_status = 'publish'
					AND post_id > %d
					AND meta_key = '_wc_review_count'
				ORDER BY post_id ASC
				LIMIT 10
			",
			$last_product_id
		),
		ARRAY_A
	);

	if ( empty( $products_data ) ) {
		delete_option( 'woocommerce_update_500_last_product_id' );
		return false;
	}

	$product_ids_to_check = array_column( $products_data, 'post_id' );
	$actual_review_counts = WC_Comments::get_review_counts_for_product_ids( $product_ids_to_check );

	foreach ( $products_data as $product_data ) {
		$product_id           = intval( $product_data['post_id'] );
		$current_review_count = intval( $product_data['meta_value'] );

		if ( intval( $actual_review_counts[ $product_id ] ) !== $current_review_count ) {
			WC_Comments::clear_transients( $product_id );
		}
	}

	// Start the run again.
	if ( $product_id ) {
		return update_option( 'woocommerce_update_500_last_product_id', $product_id );
	}

	delete_option( 'woocommerce_update_500_last_product_id' );
	return false;
}

/**
 * Update DB version to 5.0.0.
 */
function wc_update_500_db_version() {
	WC_Install::update_db_version( '5.0.0' );
}

/**
 * Creates the refund and returns policy page.
 *
 * See @link https://github.com/woocommerce/woocommerce/issues/29235.
 */
function wc_update_560_create_refund_returns_page() {
	/**
	 * Filter on the pages created to return what we expect.
	 *
	 * @param array $pages The default WC pages.
	 */
	function filter_created_pages( $pages ) {
		$page_to_create = array( 'refund_returns' );

		return array_intersect_key( $pages, array_flip( $page_to_create ) );
	}

	add_filter( 'woocommerce_create_pages', 'filter_created_pages' );

	WC_Install::create_pages();

	remove_filter( 'woocommerce_create_pages', 'filter_created_pages' );
}

/**
 * Update DB version to 5.6.0.
 */
function wc_update_560_db_version() {
	WC_Install::update_db_version( '5.6.0' );
}
class-wc-product-variation.php000064400000041567151336044760012467 0ustar00<?php
/**
 * Product Variation
 *
 * The WooCommerce product variation class handles product variation data.
 *
 * @package WooCommerce\Classes
 * @version 3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Product variation class.
 */
class WC_Product_Variation extends WC_Product_Simple {

	/**
	 * Post type.
	 *
	 * @var string
	 */
	protected $post_type = 'product_variation';

	/**
	 * Parent data.
	 *
	 * @var array
	 */
	protected $parent_data = array(
		'title'             => '',
		'sku'               => '',
		'manage_stock'      => '',
		'backorders'        => '',
		'stock_quantity'    => '',
		'weight'            => '',
		'length'            => '',
		'width'             => '',
		'height'            => '',
		'tax_class'         => '',
		'shipping_class_id' => '',
		'image_id'          => '',
		'purchase_note'     => '',
	);

	/**
	 * Override the default constructor to set custom defaults.
	 *
	 * @param int|WC_Product|object $product Product to init.
	 */
	public function __construct( $product = 0 ) {
		$this->data['tax_class']         = 'parent';
		$this->data['attribute_summary'] = '';
		parent::__construct( $product );
	}

	/**
	 * Prefix for action and filter hooks on data.
	 *
	 * @since  3.0.0
	 * @return string
	 */
	protected function get_hook_prefix() {
		return 'woocommerce_product_variation_get_';
	}

	/**
	 * Get internal type.
	 *
	 * @return string
	 */
	public function get_type() {
		return 'variation';
	}

	/**
	 * If the stock level comes from another product ID.
	 *
	 * @since  3.0.0
	 * @return int
	 */
	public function get_stock_managed_by_id() {
		return 'parent' === $this->get_manage_stock() ? $this->get_parent_id() : $this->get_id();
	}

	/**
	 * Get the product's title. For variations this is the parent product name.
	 *
	 * @return string
	 */
	public function get_title() {
		return apply_filters( 'woocommerce_product_title', $this->parent_data['title'], $this );
	}

	/**
	 * Get product name with SKU or ID. Used within admin.
	 *
	 * @return string Formatted product name
	 */
	public function get_formatted_name() {
		if ( $this->get_sku() ) {
			$identifier = $this->get_sku();
		} else {
			$identifier = '#' . $this->get_id();
		}

		$formatted_variation_list = wc_get_formatted_variation( $this, true, true, true );

		return sprintf( '%2$s (%1$s)', $identifier, $this->get_name() ) . '<span class="description">' . $formatted_variation_list . '</span>';
	}

	/**
	 * Get variation attribute values. Keys are prefixed with attribute_, as stored, unless $with_prefix is false.
	 *
	 * @param bool $with_prefix Whether keys should be prepended with attribute_ or not, default is true.
	 * @return array of attributes and their values for this variation.
	 */
	public function get_variation_attributes( $with_prefix = true ) {
		$attributes           = $this->get_attributes();
		$variation_attributes = array();
		$prefix               = $with_prefix ? 'attribute_' : '';

		foreach ( $attributes as $key => $value ) {
			$variation_attributes[ $prefix . $key ] = $value;
		}
		return $variation_attributes;
	}

	/**
	 * Returns a single product attribute as a string.
	 *
	 * @param  string $attribute to get.
	 * @return string
	 */
	public function get_attribute( $attribute ) {
		$attributes = $this->get_attributes();
		$attribute  = sanitize_title( $attribute );

		if ( isset( $attributes[ $attribute ] ) ) {
			$value = $attributes[ $attribute ];
			$term  = taxonomy_exists( $attribute ) ? get_term_by( 'slug', $value, $attribute ) : false;
			return ! is_wp_error( $term ) && $term ? $term->name : $value;
		}

		$att_str = 'pa_' . $attribute;
		if ( isset( $attributes[ $att_str ] ) ) {
			$value = $attributes[ $att_str ];
			$term  = taxonomy_exists( $att_str ) ? get_term_by( 'slug', $value, $att_str ) : false;
			return ! is_wp_error( $term ) && $term ? $term->name : $value;
		}

		return '';
	}

	/**
	 * Wrapper for get_permalink. Adds this variations attributes to the URL.
	 *
	 * @param  array|null $item_object item array If a cart or order item is passed, we can get a link containing the exact attributes selected for the variation, rather than the default attributes.
	 * @return string
	 */
	public function get_permalink( $item_object = null ) {
		$url = get_permalink( $this->get_parent_id() );

		if ( ! empty( $item_object['variation'] ) ) {
			$data = $item_object['variation'];
		} elseif ( ! empty( $item_object['item_meta_array'] ) ) {
			$data_keys   = array_map( 'wc_variation_attribute_name', wp_list_pluck( $item_object['item_meta_array'], 'key' ) );
			$data_values = wp_list_pluck( $item_object['item_meta_array'], 'value' );
			$data        = array_intersect_key( array_combine( $data_keys, $data_values ), $this->get_variation_attributes() );
		} else {
			$data = $this->get_variation_attributes();
		}

		$data = array_filter( $data, 'wc_array_filter_default_attributes' );

		if ( empty( $data ) ) {
			return $url;
		}

		// Filter and encode keys and values so this is not broken by add_query_arg.
		$data = array_map( 'urlencode', $data );
		$keys = array_map( 'urlencode', array_keys( $data ) );

		return add_query_arg( array_combine( $keys, $data ), $url );
	}

	/**
	 * Get the add to url used mainly in loops.
	 *
	 * @return string
	 */
	public function add_to_cart_url() {
		$url = $this->is_purchasable() ? remove_query_arg(
			'added-to-cart',
			add_query_arg(
				array(
					'variation_id' => $this->get_id(),
					'add-to-cart'  => $this->get_parent_id(),
				),
				$this->get_permalink()
			)
		) : $this->get_permalink();
		return apply_filters( 'woocommerce_product_add_to_cart_url', $url, $this );
	}

	/**
	 * Get SKU (Stock-keeping unit) - product unique ID.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_sku( $context = 'view' ) {
		$value = $this->get_prop( 'sku', $context );

		// Inherit value from parent.
		if ( 'view' === $context && empty( $value ) ) {
			$value = apply_filters( $this->get_hook_prefix() . 'sku', $this->parent_data['sku'], $this );
		}
		return $value;
	}

	/**
	 * Returns the product's weight.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_weight( $context = 'view' ) {
		$value = $this->get_prop( 'weight', $context );

		// Inherit value from parent.
		if ( 'view' === $context && empty( $value ) ) {
			$value = apply_filters( $this->get_hook_prefix() . 'weight', $this->parent_data['weight'], $this );
		}
		return $value;
	}

	/**
	 * Returns the product length.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_length( $context = 'view' ) {
		$value = $this->get_prop( 'length', $context );

		// Inherit value from parent.
		if ( 'view' === $context && empty( $value ) ) {
			$value = apply_filters( $this->get_hook_prefix() . 'length', $this->parent_data['length'], $this );
		}
		return $value;
	}

	/**
	 * Returns the product width.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_width( $context = 'view' ) {
		$value = $this->get_prop( 'width', $context );

		// Inherit value from parent.
		if ( 'view' === $context && empty( $value ) ) {
			$value = apply_filters( $this->get_hook_prefix() . 'width', $this->parent_data['width'], $this );
		}
		return $value;
	}

	/**
	 * Returns the product height.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_height( $context = 'view' ) {
		$value = $this->get_prop( 'height', $context );

		// Inherit value from parent.
		if ( 'view' === $context && empty( $value ) ) {
			$value = apply_filters( $this->get_hook_prefix() . 'height', $this->parent_data['height'], $this );
		}
		return $value;
	}

	/**
	 * Returns the tax class.
	 *
	 * Does not use get_prop so it can handle 'parent' inheritance correctly.
	 *
	 * @param  string $context view, edit, or unfiltered.
	 * @return string
	 */
	public function get_tax_class( $context = 'view' ) {
		$value = null;

		if ( array_key_exists( 'tax_class', $this->data ) ) {
			$value = array_key_exists( 'tax_class', $this->changes ) ? $this->changes['tax_class'] : $this->data['tax_class'];

			if ( 'edit' !== $context && 'parent' === $value ) {
				$value = $this->parent_data['tax_class'];
			}

			if ( 'view' === $context ) {
				$value = apply_filters( $this->get_hook_prefix() . 'tax_class', $value, $this );
			}
		}
		return $value;
	}

	/**
	 * Return if product manage stock.
	 *
	 * @since 3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return boolean|string true, false, or parent.
	 */
	public function get_manage_stock( $context = 'view' ) {
		$value = $this->get_prop( 'manage_stock', $context );

		// Inherit value from parent.
		if ( 'view' === $context && false === $value && true === wc_string_to_bool( $this->parent_data['manage_stock'] ) ) {
			$value = 'parent';
		}
		return $value;
	}

	/**
	 * Returns number of items available for sale.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return int|null
	 */
	public function get_stock_quantity( $context = 'view' ) {
		$value = $this->get_prop( 'stock_quantity', $context );

		// Inherit value from parent.
		if ( 'view' === $context && 'parent' === $this->get_manage_stock() ) {
			$value = apply_filters( $this->get_hook_prefix() . 'stock_quantity', $this->parent_data['stock_quantity'], $this );
		}
		return $value;
	}

	/**
	 * Get backorders.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @since 3.0.0
	 * @return string yes no or notify
	 */
	public function get_backorders( $context = 'view' ) {
		$value = $this->get_prop( 'backorders', $context );

		// Inherit value from parent.
		if ( 'view' === $context && 'parent' === $this->get_manage_stock() ) {
			$value = apply_filters( $this->get_hook_prefix() . 'backorders', $this->parent_data['backorders'], $this );
		}
		return $value;
	}

	/**
	 * Get main image ID.
	 *
	 * @since 3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_image_id( $context = 'view' ) {
		$image_id = $this->get_prop( 'image_id', $context );

		if ( 'view' === $context && ! $image_id ) {
			$image_id = apply_filters( $this->get_hook_prefix() . 'image_id', $this->parent_data['image_id'], $this );
		}

		return $image_id;
	}

	/**
	 * Get purchase note.
	 *
	 * @since 3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_purchase_note( $context = 'view' ) {
		$value = $this->get_prop( 'purchase_note', $context );

		// Inherit value from parent.
		if ( 'view' === $context && empty( $value ) ) {
			$value = apply_filters( $this->get_hook_prefix() . 'purchase_note', $this->parent_data['purchase_note'], $this );
		}
		return $value;
	}

	/**
	 * Get shipping class ID.
	 *
	 * @since 3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return int
	 */
	public function get_shipping_class_id( $context = 'view' ) {
		$shipping_class_id = $this->get_prop( 'shipping_class_id', $context );

		if ( 'view' === $context && ! $shipping_class_id ) {
			$shipping_class_id = apply_filters( $this->get_hook_prefix() . 'shipping_class_id', $this->parent_data['shipping_class_id'], $this );
		}

		return $shipping_class_id;
	}

	/**
	 * Get catalog visibility.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_catalog_visibility( $context = 'view' ) {
		return apply_filters( $this->get_hook_prefix() . 'catalog_visibility', $this->parent_data['catalog_visibility'], $this );
	}

	/**
	 * Get attribute summary.
	 *
	 * By default, attribute summary contains comma-delimited 'attribute_name: attribute_value' pairs for all attributes.
	 *
	 * @param string $context What the value is for. Valid values are view and edit.
	 *
	 * @since 3.6.0
	 * @return string
	 */
	public function get_attribute_summary( $context = 'view' ) {
		return $this->get_prop( 'attribute_summary', $context );
	}


	/**
	 * Set attribute summary.
	 *
	 * By default, attribute summary contains comma-delimited 'attribute_name: attribute_value' pairs for all attributes.
	 *
	 * @since 3.6.0
	 * @param string $attribute_summary Summary of attribute names and values assigned to the variation.
	 */
	public function set_attribute_summary( $attribute_summary ) {
		$this->set_prop( 'attribute_summary', $attribute_summary );
	}

	/*
	|--------------------------------------------------------------------------
	| CRUD methods
	|--------------------------------------------------------------------------
	*/

	/**
	 * Set the parent data array for this variation.
	 *
	 * @since 3.0.0
	 * @param array $parent_data parent data array for this variation.
	 */
	public function set_parent_data( $parent_data ) {
		$parent_data = wp_parse_args(
			$parent_data,
			array(
				'title'              => '',
				'status'             => '',
				'sku'                => '',
				'manage_stock'       => 'no',
				'backorders'         => 'no',
				'stock_quantity'     => '',
				'weight'             => '',
				'length'             => '',
				'width'              => '',
				'height'             => '',
				'tax_class'          => '',
				'shipping_class_id'  => 0,
				'image_id'           => 0,
				'purchase_note'      => '',
				'catalog_visibility' => 'visible',
			)
		);

		// Normalize tax class.
		$parent_data['tax_class'] = sanitize_title( $parent_data['tax_class'] );
		$parent_data['tax_class'] = 'standard' === $parent_data['tax_class'] ? '' : $parent_data['tax_class'];
		$valid_classes            = $this->get_valid_tax_classes();

		if ( ! in_array( $parent_data['tax_class'], $valid_classes, true ) ) {
			$parent_data['tax_class'] = '';
		}

		$this->parent_data = $parent_data;
	}

	/**
	 * Get the parent data array for this variation.
	 *
	 * @since  3.0.0
	 * @return array
	 */
	public function get_parent_data() {
		return $this->parent_data;
	}

	/**
	 * Set attributes. Unlike the parent product which uses terms, variations are assigned
	 * specific attributes using name value pairs.
	 *
	 * @param array $raw_attributes array of raw attributes.
	 */
	public function set_attributes( $raw_attributes ) {
		$raw_attributes = (array) $raw_attributes;
		$attributes     = array();

		foreach ( $raw_attributes as $key => $value ) {
			// Remove attribute prefix which meta gets stored with.
			if ( 0 === strpos( $key, 'attribute_' ) ) {
				$key = substr( $key, 10 );
			}
			$attributes[ $key ] = $value;
		}
		$this->set_prop( 'attributes', $attributes );
	}

	/**
	 * Returns whether or not the product has any visible attributes.
	 *
	 * Variations are mapped to specific attributes unlike products, and the return
	 * value of ->get_attributes differs. Therefore this returns false.
	 *
	 * @return boolean
	 */
	public function has_attributes() {
		return false;
	}

	/*
	|--------------------------------------------------------------------------
	| Conditionals
	|--------------------------------------------------------------------------
	*/

	/**
	 * Returns false if the product cannot be bought.
	 * Override abstract method so that: i) Disabled variations are not be purchasable by admins. ii) Enabled variations are not purchasable if the parent product is not purchasable.
	 *
	 * @return bool
	 */
	public function is_purchasable() {
		return apply_filters( 'woocommerce_variation_is_purchasable', $this->variation_is_visible() && parent::is_purchasable() && ( 'publish' === $this->parent_data['status'] || current_user_can( 'edit_post', $this->get_parent_id() ) ), $this );
	}

	/**
	 * Controls whether this particular variation will appear greyed-out (inactive) or not (active).
	 * Used by extensions to make incompatible variations appear greyed-out, etc.
	 * Other possible uses: prevent out-of-stock variations from being selected.
	 *
	 * @return bool
	 */
	public function variation_is_active() {
		return apply_filters( 'woocommerce_variation_is_active', true, $this );
	}

	/**
	 * Checks if this particular variation is visible. Invisible variations are enabled and can be selected, but no price / stock info is displayed.
	 * Instead, a suitable 'unavailable' message is displayed.
	 * Invisible by default: Disabled variations and variations with an empty price.
	 *
	 * @return bool
	 */
	public function variation_is_visible() {
		return apply_filters( 'woocommerce_variation_is_visible', 'publish' === get_post_status( $this->get_id() ) && '' !== $this->get_price(), $this->get_id(), $this->get_parent_id(), $this );
	}

	/**
	 * Return valid tax classes. Adds 'parent' to the default list of valid tax classes.
	 *
	 * @return array valid tax classes
	 */
	protected function get_valid_tax_classes() {
		$valid_classes   = WC_Tax::get_tax_class_slugs();
		$valid_classes[] = 'parent';

		return $valid_classes;
	}
}
wc-account-functions.php000064400000031652151336044760011346 0ustar00<?php
/**
 * WooCommerce Account Functions
 *
 * Functions for account specific things.
 *
 * @package WooCommerce\Functions
 * @version 2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Returns the url to the lost password endpoint url.
 *
 * @param  string $default_url Default lost password URL.
 * @return string
 */
function wc_lostpassword_url( $default_url = '' ) {
	// Avoid loading too early.
	if ( ! did_action( 'init' ) ) {
		return $default_url;
	}

	// Don't redirect to the woocommerce endpoint on global network admin lost passwords.
	if ( is_multisite() && isset( $_GET['redirect_to'] ) && false !== strpos( wp_unslash( $_GET['redirect_to'] ), network_admin_url() ) ) { // WPCS: input var ok, sanitization ok, CSRF ok.
		return $default_url;
	}

	$wc_account_page_url    = wc_get_page_permalink( 'myaccount' );
	$wc_account_page_exists = wc_get_page_id( 'myaccount' ) > 0;
	$lost_password_endpoint = get_option( 'woocommerce_myaccount_lost_password_endpoint' );

	if ( $wc_account_page_exists && ! empty( $lost_password_endpoint ) ) {
		return wc_get_endpoint_url( $lost_password_endpoint, '', $wc_account_page_url );
	} else {
		return $default_url;
	}
}

add_filter( 'lostpassword_url', 'wc_lostpassword_url', 10, 1 );

/**
 * Get the link to the edit account details page.
 *
 * @return string
 */
function wc_customer_edit_account_url() {
	$edit_account_url = wc_get_endpoint_url( 'edit-account', '', wc_get_page_permalink( 'myaccount' ) );

	return apply_filters( 'woocommerce_customer_edit_account_url', $edit_account_url );
}

/**
 * Get the edit address slug translation.
 *
 * @param  string $id   Address ID.
 * @param  bool   $flip Flip the array to make it possible to retrieve the values ​​from both sides.
 *
 * @return string       Address slug i18n.
 */
function wc_edit_address_i18n( $id, $flip = false ) {
	$slugs = apply_filters(
		'woocommerce_edit_address_slugs',
		array(
			'billing'  => sanitize_title( _x( 'billing', 'edit-address-slug', 'woocommerce' ) ),
			'shipping' => sanitize_title( _x( 'shipping', 'edit-address-slug', 'woocommerce' ) ),
		)
	);

	if ( $flip ) {
		$slugs = array_flip( $slugs );
	}

	if ( ! isset( $slugs[ $id ] ) ) {
		return $id;
	}

	return $slugs[ $id ];
}

/**
 * Get My Account menu items.
 *
 * @since 2.6.0
 * @return array
 */
function wc_get_account_menu_items() {
	$endpoints = array(
		'orders'          => get_option( 'woocommerce_myaccount_orders_endpoint', 'orders' ),
		'downloads'       => get_option( 'woocommerce_myaccount_downloads_endpoint', 'downloads' ),
		'edit-address'    => get_option( 'woocommerce_myaccount_edit_address_endpoint', 'edit-address' ),
		'payment-methods' => get_option( 'woocommerce_myaccount_payment_methods_endpoint', 'payment-methods' ),
		'edit-account'    => get_option( 'woocommerce_myaccount_edit_account_endpoint', 'edit-account' ),
		'customer-logout' => get_option( 'woocommerce_logout_endpoint', 'customer-logout' ),
	);

	$items = array(
		'dashboard'       => __( 'Dashboard', 'woocommerce' ),
		'orders'          => __( 'Orders', 'woocommerce' ),
		'downloads'       => __( 'Downloads', 'woocommerce' ),
		'edit-address'    => _n( 'Addresses', 'Address', (int) wc_shipping_enabled(), 'woocommerce' ),
		'payment-methods' => __( 'Payment methods', 'woocommerce' ),
		'edit-account'    => __( 'Account details', 'woocommerce' ),
		'customer-logout' => __( 'Logout', 'woocommerce' ),
	);

	// Remove missing endpoints.
	foreach ( $endpoints as $endpoint_id => $endpoint ) {
		if ( empty( $endpoint ) ) {
			unset( $items[ $endpoint_id ] );
		}
	}

	// Check if payment gateways support add new payment methods.
	if ( isset( $items['payment-methods'] ) ) {
		$support_payment_methods = false;
		foreach ( WC()->payment_gateways->get_available_payment_gateways() as $gateway ) {
			if ( $gateway->supports( 'add_payment_method' ) || $gateway->supports( 'tokenization' ) ) {
				$support_payment_methods = true;
				break;
			}
		}

		if ( ! $support_payment_methods ) {
			unset( $items['payment-methods'] );
		}
	}

	return apply_filters( 'woocommerce_account_menu_items', $items, $endpoints );
}

/**
 * Get account menu item classes.
 *
 * @since 2.6.0
 * @param string $endpoint Endpoint.
 * @return string
 */
function wc_get_account_menu_item_classes( $endpoint ) {
	global $wp;

	$classes = array(
		'woocommerce-MyAccount-navigation-link',
		'woocommerce-MyAccount-navigation-link--' . $endpoint,
	);

	// Set current item class.
	$current = isset( $wp->query_vars[ $endpoint ] );
	if ( 'dashboard' === $endpoint && ( isset( $wp->query_vars['page'] ) || empty( $wp->query_vars ) ) ) {
		$current = true; // Dashboard is not an endpoint, so needs a custom check.
	} elseif ( 'orders' === $endpoint && isset( $wp->query_vars['view-order'] ) ) {
		$current = true; // When looking at individual order, highlight Orders list item (to signify where in the menu the user currently is).
	} elseif ( 'payment-methods' === $endpoint && isset( $wp->query_vars['add-payment-method'] ) ) {
		$current = true;
	}

	if ( $current ) {
		$classes[] = 'is-active';
	}

	$classes = apply_filters( 'woocommerce_account_menu_item_classes', $classes, $endpoint );

	return implode( ' ', array_map( 'sanitize_html_class', $classes ) );
}

/**
 * Get account endpoint URL.
 *
 * @since 2.6.0
 * @param string $endpoint Endpoint.
 * @return string
 */
function wc_get_account_endpoint_url( $endpoint ) {
	if ( 'dashboard' === $endpoint ) {
		return wc_get_page_permalink( 'myaccount' );
	}

	if ( 'customer-logout' === $endpoint ) {
		return wc_logout_url();
	}

	return wc_get_endpoint_url( $endpoint, '', wc_get_page_permalink( 'myaccount' ) );
}

/**
 * Get My Account > Orders columns.
 *
 * @since 2.6.0
 * @return array
 */
function wc_get_account_orders_columns() {
	$columns = apply_filters(
		'woocommerce_account_orders_columns',
		array(
			'order-number'  => __( 'Order', 'woocommerce' ),
			'order-date'    => __( 'Date', 'woocommerce' ),
			'order-status'  => __( 'Status', 'woocommerce' ),
			'order-total'   => __( 'Total', 'woocommerce' ),
			'order-actions' => __( 'Actions', 'woocommerce' ),
		)
	);

	// Deprecated filter since 2.6.0.
	return apply_filters( 'woocommerce_my_account_my_orders_columns', $columns );
}

/**
 * Get My Account > Downloads columns.
 *
 * @since 2.6.0
 * @return array
 */
function wc_get_account_downloads_columns() {
	$columns = apply_filters(
		'woocommerce_account_downloads_columns',
		array(
			'download-product'   => __( 'Product', 'woocommerce' ),
			'download-remaining' => __( 'Downloads remaining', 'woocommerce' ),
			'download-expires'   => __( 'Expires', 'woocommerce' ),
			'download-file'      => __( 'Download', 'woocommerce' ),
			'download-actions'   => '&nbsp;',
		)
	);

	if ( ! has_filter( 'woocommerce_account_download_actions' ) ) {
		unset( $columns['download-actions'] );
	}

	return $columns;
}

/**
 * Get My Account > Payment methods columns.
 *
 * @since 2.6.0
 * @return array
 */
function wc_get_account_payment_methods_columns() {
	return apply_filters(
		'woocommerce_account_payment_methods_columns',
		array(
			'method'  => __( 'Method', 'woocommerce' ),
			'expires' => __( 'Expires', 'woocommerce' ),
			'actions' => '&nbsp;',
		)
	);
}

/**
 * Get My Account > Payment methods types
 *
 * @since 2.6.0
 * @return array
 */
function wc_get_account_payment_methods_types() {
	return apply_filters(
		'woocommerce_payment_methods_types',
		array(
			'cc'     => __( 'Credit card', 'woocommerce' ),
			'echeck' => __( 'eCheck', 'woocommerce' ),
		)
	);
}

/**
 * Get account orders actions.
 *
 * @since  3.2.0
 * @param  int|WC_Order $order Order instance or ID.
 * @return array
 */
function wc_get_account_orders_actions( $order ) {
	if ( ! is_object( $order ) ) {
		$order_id = absint( $order );
		$order    = wc_get_order( $order_id );
	}

	$actions = array(
		'pay'    => array(
			'url'  => $order->get_checkout_payment_url(),
			'name' => __( 'Pay', 'woocommerce' ),
		),
		'view'   => array(
			'url'  => $order->get_view_order_url(),
			'name' => __( 'View', 'woocommerce' ),
		),
		'cancel' => array(
			'url'  => $order->get_cancel_order_url( wc_get_page_permalink( 'myaccount' ) ),
			'name' => __( 'Cancel', 'woocommerce' ),
		),
	);

	if ( ! $order->needs_payment() ) {
		unset( $actions['pay'] );
	}

	if ( ! in_array( $order->get_status(), apply_filters( 'woocommerce_valid_order_statuses_for_cancel', array( 'pending', 'failed' ), $order ), true ) ) {
		unset( $actions['cancel'] );
	}

	return apply_filters( 'woocommerce_my_account_my_orders_actions', $actions, $order );
}

/**
 * Get account formatted address.
 *
 * @since  3.2.0
 * @param  string $address_type Address type.
 *                              Accepts: 'billing' or 'shipping'.
 *                              Default to 'billing'.
 * @param  int    $customer_id  Customer ID.
 *                              Default to 0.
 * @return string
 */
function wc_get_account_formatted_address( $address_type = 'billing', $customer_id = 0 ) {
	$getter  = "get_{$address_type}";
	$address = array();

	if ( 0 === $customer_id ) {
		$customer_id = get_current_user_id();
	}

	$customer = new WC_Customer( $customer_id );

	if ( is_callable( array( $customer, $getter ) ) ) {
		$address = $customer->$getter();
		unset( $address['email'], $address['tel'] );
	}

	return WC()->countries->get_formatted_address( apply_filters( 'woocommerce_my_account_my_address_formatted_address', $address, $customer->get_id(), $address_type ) );
}

/**
 * Returns an array of a user's saved payments list for output on the account tab.
 *
 * @since  2.6
 * @param  array $list         List of payment methods passed from wc_get_customer_saved_methods_list().
 * @param  int   $customer_id  The customer to fetch payment methods for.
 * @return array               Filtered list of customers payment methods.
 */
function wc_get_account_saved_payment_methods_list( $list, $customer_id ) {
	$payment_tokens = WC_Payment_Tokens::get_customer_tokens( $customer_id );
	foreach ( $payment_tokens as $payment_token ) {
		$delete_url      = wc_get_endpoint_url( 'delete-payment-method', $payment_token->get_id() );
		$delete_url      = wp_nonce_url( $delete_url, 'delete-payment-method-' . $payment_token->get_id() );
		$set_default_url = wc_get_endpoint_url( 'set-default-payment-method', $payment_token->get_id() );
		$set_default_url = wp_nonce_url( $set_default_url, 'set-default-payment-method-' . $payment_token->get_id() );

		$type            = strtolower( $payment_token->get_type() );
		$list[ $type ][] = array(
			'method'     => array(
				'gateway' => $payment_token->get_gateway_id(),
			),
			'expires'    => esc_html__( 'N/A', 'woocommerce' ),
			'is_default' => $payment_token->is_default(),
			'actions'    => array(
				'delete' => array(
					'url'  => $delete_url,
					'name' => esc_html__( 'Delete', 'woocommerce' ),
				),
			),
		);
		$key             = key( array_slice( $list[ $type ], -1, 1, true ) );

		if ( ! $payment_token->is_default() ) {
			$list[ $type ][ $key ]['actions']['default'] = array(
				'url'  => $set_default_url,
				'name' => esc_html__( 'Make default', 'woocommerce' ),
			);
		}

		$list[ $type ][ $key ] = apply_filters( 'woocommerce_payment_methods_list_item', $list[ $type ][ $key ], $payment_token );
	}
	return $list;
}

add_filter( 'woocommerce_saved_payment_methods_list', 'wc_get_account_saved_payment_methods_list', 10, 2 );

/**
 * Controls the output for credit cards on the my account page.
 *
 * @since 2.6
 * @param  array            $item         Individual list item from woocommerce_saved_payment_methods_list.
 * @param  WC_Payment_Token $payment_token The payment token associated with this method entry.
 * @return array                           Filtered item.
 */
function wc_get_account_saved_payment_methods_list_item_cc( $item, $payment_token ) {
	if ( 'cc' !== strtolower( $payment_token->get_type() ) ) {
		return $item;
	}

	$card_type               = $payment_token->get_card_type();
	$item['method']['last4'] = $payment_token->get_last4();
	$item['method']['brand'] = ( ! empty( $card_type ) ? ucfirst( $card_type ) : esc_html__( 'Credit card', 'woocommerce' ) );
	$item['expires']         = $payment_token->get_expiry_month() . '/' . substr( $payment_token->get_expiry_year(), -2 );

	return $item;
}

add_filter( 'woocommerce_payment_methods_list_item', 'wc_get_account_saved_payment_methods_list_item_cc', 10, 2 );

/**
 * Controls the output for eChecks on the my account page.
 *
 * @since 2.6
 * @param  array            $item         Individual list item from woocommerce_saved_payment_methods_list.
 * @param  WC_Payment_Token $payment_token The payment token associated with this method entry.
 * @return array                           Filtered item.
 */
function wc_get_account_saved_payment_methods_list_item_echeck( $item, $payment_token ) {
	if ( 'echeck' !== strtolower( $payment_token->get_type() ) ) {
		return $item;
	}

	$item['method']['last4'] = $payment_token->get_last4();
	$item['method']['brand'] = esc_html__( 'eCheck', 'woocommerce' );

	return $item;
}

add_filter( 'woocommerce_payment_methods_list_item', 'wc_get_account_saved_payment_methods_list_item_echeck', 10, 2 );
shortcodes/class-wc-shortcode-checkout.php000064400000025004151336044760014753 0ustar00<?php
/**
 * Checkout Shortcode
 *
 * Used on the checkout page, the checkout shortcode displays the checkout process.
 *
 * @package WooCommerce\Shortcodes\Checkout
 * @version 2.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Shortcode checkout class.
 */
class WC_Shortcode_Checkout {

	/**
	 * Get the shortcode content.
	 *
	 * @param array $atts Shortcode attributes.
	 * @return string
	 */
	public static function get( $atts ) {
		return WC_Shortcodes::shortcode_wrapper( array( __CLASS__, 'output' ), $atts );
	}

	/**
	 * Output the shortcode.
	 *
	 * @param array $atts Shortcode attributes.
	 */
	public static function output( $atts ) {
		global $wp;

		// Check cart class is loaded or abort.
		if ( is_null( WC()->cart ) ) {
			return;
		}

		// Backwards compatibility with old pay and thanks link arguments.
		if ( isset( $_GET['order'] ) && isset( $_GET['key'] ) ) { // WPCS: input var ok, CSRF ok.
			wc_deprecated_argument( __CLASS__ . '->' . __FUNCTION__, '2.1', '"order" is no longer used to pass an order ID. Use the order-pay or order-received endpoint instead.' );

			// Get the order to work out what we are showing.
			$order_id = absint( $_GET['order'] ); // WPCS: input var ok.
			$order    = wc_get_order( $order_id );

			if ( $order && $order->has_status( 'pending' ) ) {
				$wp->query_vars['order-pay'] = absint( $_GET['order'] ); // WPCS: input var ok.
			} else {
				$wp->query_vars['order-received'] = absint( $_GET['order'] ); // WPCS: input var ok.
			}
		}

		// Handle checkout actions.
		if ( ! empty( $wp->query_vars['order-pay'] ) ) {

			self::order_pay( $wp->query_vars['order-pay'] );

		} elseif ( isset( $wp->query_vars['order-received'] ) ) {

			self::order_received( $wp->query_vars['order-received'] );

		} else {

			self::checkout();

		}
	}

	/**
	 * Show the pay page.
	 *
	 * @throws Exception When validate fails.
	 * @param int $order_id Order ID.
	 */
	private static function order_pay( $order_id ) {

		do_action( 'before_woocommerce_pay' );

		$order_id = absint( $order_id );

		// Pay for existing order.
		if ( isset( $_GET['pay_for_order'], $_GET['key'] ) && $order_id ) { // WPCS: input var ok, CSRF ok.
			try {
				$order_key = isset( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : ''; // WPCS: input var ok, CSRF ok.
				$order     = wc_get_order( $order_id );

				// Order or payment link is invalid.
				if ( ! $order || $order->get_id() !== $order_id || ! hash_equals( $order->get_order_key(), $order_key ) ) {
					throw new Exception( __( 'Sorry, this order is invalid and cannot be paid for.', 'woocommerce' ) );
				}

				// Logged out customer does not have permission to pay for this order.
				if ( ! current_user_can( 'pay_for_order', $order_id ) && ! is_user_logged_in() ) {
					echo '<div class="woocommerce-info">' . esc_html__( 'Please log in to your account below to continue to the payment form.', 'woocommerce' ) . '</div>';
					woocommerce_login_form(
						array(
							'redirect' => $order->get_checkout_payment_url(),
						)
					);
					return;
				}

				// Add notice if logged in customer is trying to pay for guest order.
				if ( ! $order->get_user_id() && is_user_logged_in() ) {
					// If order has does not have same billing email then current logged in user then show warning.
					if ( $order->get_billing_email() !== wp_get_current_user()->user_email ) {
						wc_print_notice( __( 'You are paying for a guest order. Please continue with payment only if you recognize this order.', 'woocommerce' ), 'error' );
					}
				}

				// Logged in customer trying to pay for someone else's order.
				if ( ! current_user_can( 'pay_for_order', $order_id ) ) {
					throw new Exception( __( 'This order cannot be paid for. Please contact us if you need assistance.', 'woocommerce' ) );
				}

				// Does not need payment.
				if ( ! $order->needs_payment() ) {
					/* translators: %s: order status */
					throw new Exception( sprintf( __( 'This order&rsquo;s status is &ldquo;%s&rdquo;&mdash;it cannot be paid for. Please contact us if you need assistance.', 'woocommerce' ), wc_get_order_status_name( $order->get_status() ) ) );
				}

				// Ensure order items are still stocked if paying for a failed order. Pending orders do not need this check because stock is held.
				if ( ! $order->has_status( wc_get_is_pending_statuses() ) ) {
					$quantities = array();

					foreach ( $order->get_items() as $item_key => $item ) {
						if ( $item && is_callable( array( $item, 'get_product' ) ) ) {
							$product = $item->get_product();

							if ( ! $product ) {
								continue;
							}

							$quantities[ $product->get_stock_managed_by_id() ] = isset( $quantities[ $product->get_stock_managed_by_id() ] ) ? $quantities[ $product->get_stock_managed_by_id() ] + $item->get_quantity() : $item->get_quantity();
						}
					}

					foreach ( $order->get_items() as $item_key => $item ) {
						if ( $item && is_callable( array( $item, 'get_product' ) ) ) {
							$product = $item->get_product();

							if ( ! $product ) {
								continue;
							}

							if ( ! apply_filters( 'woocommerce_pay_order_product_in_stock', $product->is_in_stock(), $product, $order ) ) {
								/* translators: %s: product name */
								throw new Exception( sprintf( __( 'Sorry, "%s" is no longer in stock so this order cannot be paid for. We apologize for any inconvenience caused.', 'woocommerce' ), $product->get_name() ) );
							}

							// We only need to check products managing stock, with a limited stock qty.
							if ( ! $product->managing_stock() || $product->backorders_allowed() ) {
								continue;
							}

							// Check stock based on all items in the cart and consider any held stock within pending orders.
							$held_stock     = wc_get_held_stock_quantity( $product, $order->get_id() );
							$required_stock = $quantities[ $product->get_stock_managed_by_id() ];

							if ( ! apply_filters( 'woocommerce_pay_order_product_has_enough_stock', ( $product->get_stock_quantity() >= ( $held_stock + $required_stock ) ), $product, $order ) ) {
								/* translators: 1: product name 2: quantity in stock */
								throw new Exception( sprintf( __( 'Sorry, we do not have enough "%1$s" in stock to fulfill your order (%2$s available). We apologize for any inconvenience caused.', 'woocommerce' ), $product->get_name(), wc_format_stock_quantity_for_display( $product->get_stock_quantity() - $held_stock, $product ) ) );
							}
						}
					}
				}

				WC()->customer->set_props(
					array(
						'billing_country'  => $order->get_billing_country() ? $order->get_billing_country() : null,
						'billing_state'    => $order->get_billing_state() ? $order->get_billing_state() : null,
						'billing_postcode' => $order->get_billing_postcode() ? $order->get_billing_postcode() : null,
					)
				);
				WC()->customer->save();

				$available_gateways = WC()->payment_gateways->get_available_payment_gateways();

				if ( count( $available_gateways ) ) {
					current( $available_gateways )->set_current();
				}

				wc_get_template(
					'checkout/form-pay.php',
					array(
						'order'              => $order,
						'available_gateways' => $available_gateways,
						'order_button_text'  => apply_filters( 'woocommerce_pay_order_button_text', __( 'Pay for order', 'woocommerce' ) ),
					)
				);

			} catch ( Exception $e ) {
				wc_print_notice( $e->getMessage(), 'error' );
			}
		} elseif ( $order_id ) {

			// Pay for order after checkout step.
			$order_key = isset( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : ''; // WPCS: input var ok, CSRF ok.
			$order     = wc_get_order( $order_id );

			if ( $order && $order->get_id() === $order_id && hash_equals( $order->get_order_key(), $order_key ) ) {

				if ( $order->needs_payment() ) {

					wc_get_template( 'checkout/order-receipt.php', array( 'order' => $order ) );

				} else {
					/* translators: %s: order status */
					wc_print_notice( sprintf( __( 'This order&rsquo;s status is &ldquo;%s&rdquo;&mdash;it cannot be paid for. Please contact us if you need assistance.', 'woocommerce' ), wc_get_order_status_name( $order->get_status() ) ), 'error' );
				}
			} else {
				wc_print_notice( __( 'Sorry, this order is invalid and cannot be paid for.', 'woocommerce' ), 'error' );
			}
		} else {
			wc_print_notice( __( 'Invalid order.', 'woocommerce' ), 'error' );
		}

		do_action( 'after_woocommerce_pay' );
	}

	/**
	 * Show the thanks page.
	 *
	 * @param int $order_id Order ID.
	 */
	private static function order_received( $order_id = 0 ) {
		$order = false;

		// Get the order.
		$order_id  = apply_filters( 'woocommerce_thankyou_order_id', absint( $order_id ) );
		$order_key = apply_filters( 'woocommerce_thankyou_order_key', empty( $_GET['key'] ) ? '' : wc_clean( wp_unslash( $_GET['key'] ) ) ); // WPCS: input var ok, CSRF ok.

		if ( $order_id > 0 ) {
			$order = wc_get_order( $order_id );
			if ( ! $order || ! hash_equals( $order->get_order_key(), $order_key ) ) {
				$order = false;
			}
		}

		// Empty awaiting payment session.
		unset( WC()->session->order_awaiting_payment );

		// In case order is created from admin, but paid by the actual customer, store the ip address of the payer
		// when they visit the payment confirmation page.
		if ( $order && $order->is_created_via( 'admin' ) ) {
			$order->set_customer_ip_address( WC_Geolocation::get_ip_address() );
			$order->save();
		}

		// Empty current cart.
		wc_empty_cart();

		wc_get_template( 'checkout/thankyou.php', array( 'order' => $order ) );
	}

	/**
	 * Show the checkout.
	 */
	private static function checkout() {
		// Show non-cart errors.
		do_action( 'woocommerce_before_checkout_form_cart_notices' );

		// Check cart has contents.
		if ( WC()->cart->is_empty() && ! is_customize_preview() && apply_filters( 'woocommerce_checkout_redirect_empty_cart', true ) ) {
			return;
		}

		// Check cart contents for errors.
		do_action( 'woocommerce_check_cart_items' );

		// Calc totals.
		WC()->cart->calculate_totals();

		// Get checkout object.
		$checkout = WC()->checkout();

		if ( empty( $_POST ) && wc_notice_count( 'error' ) > 0 ) { // WPCS: input var ok, CSRF ok.

			wc_get_template( 'checkout/cart-errors.php', array( 'checkout' => $checkout ) );
			wc_clear_notices();

		} else {

			$non_js_checkout = ! empty( $_POST['woocommerce_checkout_update_totals'] ); // WPCS: input var ok, CSRF ok.

			if ( wc_notice_count( 'error' ) === 0 && $non_js_checkout ) {
				wc_add_notice( __( 'The order totals have been updated. Please confirm your order by pressing the "Place order" button at the bottom of the page.', 'woocommerce' ) );
			}

			wc_get_template( 'checkout/form-checkout.php', array( 'checkout' => $checkout ) );

		}
	}
}
shortcodes/class-wc-shortcode-cart.php000064400000007223151336044760014102 0ustar00<?php
/**
 * Cart Shortcode
 *
 * Used on the cart page, the cart shortcode displays the cart contents and interface for coupon codes and other cart bits and pieces.
 *
 * @package WooCommerce\Shortcodes\Cart
 * @version 2.3.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Shortcode cart class.
 */
class WC_Shortcode_Cart {

	/**
	 * Calculate shipping for the cart.
	 *
	 * @throws Exception When some data is invalid.
	 */
	public static function calculate_shipping() {
		try {
			WC()->shipping()->reset_shipping();

			$address = array();

			$address['country']  = isset( $_POST['calc_shipping_country'] ) ? wc_clean( wp_unslash( $_POST['calc_shipping_country'] ) ) : ''; // WPCS: input var ok, CSRF ok, sanitization ok.
			$address['state']    = isset( $_POST['calc_shipping_state'] ) ? wc_clean( wp_unslash( $_POST['calc_shipping_state'] ) ) : ''; // WPCS: input var ok, CSRF ok, sanitization ok.
			$address['postcode'] = isset( $_POST['calc_shipping_postcode'] ) ? wc_clean( wp_unslash( $_POST['calc_shipping_postcode'] ) ) : ''; // WPCS: input var ok, CSRF ok, sanitization ok.
			$address['city']     = isset( $_POST['calc_shipping_city'] ) ? wc_clean( wp_unslash( $_POST['calc_shipping_city'] ) ) : ''; // WPCS: input var ok, CSRF ok, sanitization ok.

			$address = apply_filters( 'woocommerce_cart_calculate_shipping_address', $address );

			if ( $address['postcode'] && ! WC_Validation::is_postcode( $address['postcode'], $address['country'] ) ) {
				throw new Exception( __( 'Please enter a valid postcode / ZIP.', 'woocommerce' ) );
			} elseif ( $address['postcode'] ) {
				$address['postcode'] = wc_format_postcode( $address['postcode'], $address['country'] );
			}

			if ( $address['country'] ) {
				if ( ! WC()->customer->get_billing_first_name() ) {
					WC()->customer->set_billing_location( $address['country'], $address['state'], $address['postcode'], $address['city'] );
				}
				WC()->customer->set_shipping_location( $address['country'], $address['state'], $address['postcode'], $address['city'] );
			} else {
				WC()->customer->set_billing_address_to_base();
				WC()->customer->set_shipping_address_to_base();
			}

			WC()->customer->set_calculated_shipping( true );
			WC()->customer->save();

			wc_add_notice( __( 'Shipping costs updated.', 'woocommerce' ), 'notice' );

			do_action( 'woocommerce_calculated_shipping' );

		} catch ( Exception $e ) {
			if ( ! empty( $e ) ) {
				wc_add_notice( $e->getMessage(), 'error' );
			}
		}
	}

	/**
	 * Output the cart shortcode.
	 *
	 * @param array $atts Shortcode attributes.
	 */
	public static function output( $atts ) {
		if ( ! apply_filters( 'woocommerce_output_cart_shortcode_content', true ) ) {
			return;
		}

		// Constants.
		wc_maybe_define_constant( 'WOOCOMMERCE_CART', true );

		$atts        = shortcode_atts( array(), $atts, 'woocommerce_cart' );
		$nonce_value = wc_get_var( $_REQUEST['woocommerce-shipping-calculator-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine.

		// Update Shipping. Nonce check uses new value and old value (woocommerce-cart). @todo remove in 4.0.
		if ( ! empty( $_POST['calc_shipping'] ) && ( wp_verify_nonce( $nonce_value, 'woocommerce-shipping-calculator' ) || wp_verify_nonce( $nonce_value, 'woocommerce-cart' ) ) ) { // WPCS: input var ok.
			self::calculate_shipping();

			// Also calc totals before we check items so subtotals etc are up to date.
			WC()->cart->calculate_totals();
		}

		// Check cart items are valid.
		do_action( 'woocommerce_check_cart_items' );

		// Calc totals.
		WC()->cart->calculate_totals();

		if ( WC()->cart->is_empty() ) {
			wc_get_template( 'cart/cart-empty.php' );
		} else {
			wc_get_template( 'cart/cart.php' );
		}
	}
}
shortcodes/class-wc-shortcode-products.php000064400000051121151336044760015010 0ustar00<?php
/**
 * Products shortcode
 *
 * @package  WooCommerce\Shortcodes
 * @version  3.2.4
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Products shortcode class.
 */
class WC_Shortcode_Products {

	/**
	 * Shortcode type.
	 *
	 * @since 3.2.0
	 * @var   string
	 */
	protected $type = 'products';

	/**
	 * Attributes.
	 *
	 * @since 3.2.0
	 * @var   array
	 */
	protected $attributes = array();

	/**
	 * Query args.
	 *
	 * @since 3.2.0
	 * @var   array
	 */
	protected $query_args = array();

	/**
	 * Set custom visibility.
	 *
	 * @since 3.2.0
	 * @var   bool
	 */
	protected $custom_visibility = false;

	/**
	 * Initialize shortcode.
	 *
	 * @since 3.2.0
	 * @param array  $attributes Shortcode attributes.
	 * @param string $type       Shortcode type.
	 */
	public function __construct( $attributes = array(), $type = 'products' ) {
		$this->type       = $type;
		$this->attributes = $this->parse_attributes( $attributes );
		$this->query_args = $this->parse_query_args();
	}

	/**
	 * Get shortcode attributes.
	 *
	 * @since  3.2.0
	 * @return array
	 */
	public function get_attributes() {
		return $this->attributes;
	}

	/**
	 * Get query args.
	 *
	 * @since  3.2.0
	 * @return array
	 */
	public function get_query_args() {
		return $this->query_args;
	}

	/**
	 * Get shortcode type.
	 *
	 * @since  3.2.0
	 * @return string
	 */
	public function get_type() {
		return $this->type;
	}

	/**
	 * Get shortcode content.
	 *
	 * @since  3.2.0
	 * @return string
	 */
	public function get_content() {
		return $this->product_loop();
	}

	/**
	 * Parse attributes.
	 *
	 * @since  3.2.0
	 * @param  array $attributes Shortcode attributes.
	 * @return array
	 */
	protected function parse_attributes( $attributes ) {
		$attributes = $this->parse_legacy_attributes( $attributes );

		$attributes = shortcode_atts(
			array(
				'limit'          => '-1',      // Results limit.
				'columns'        => '',        // Number of columns.
				'rows'           => '',        // Number of rows. If defined, limit will be ignored.
				'orderby'        => '',        // menu_order, title, date, rand, price, popularity, rating, or id.
				'order'          => '',        // ASC or DESC.
				'ids'            => '',        // Comma separated IDs.
				'skus'           => '',        // Comma separated SKUs.
				'category'       => '',        // Comma separated category slugs or ids.
				'cat_operator'   => 'IN',      // Operator to compare categories. Possible values are 'IN', 'NOT IN', 'AND'.
				'attribute'      => '',        // Single attribute slug.
				'terms'          => '',        // Comma separated term slugs or ids.
				'terms_operator' => 'IN',      // Operator to compare terms. Possible values are 'IN', 'NOT IN', 'AND'.
				'tag'            => '',        // Comma separated tag slugs.
				'tag_operator'   => 'IN',      // Operator to compare tags. Possible values are 'IN', 'NOT IN', 'AND'.
				'visibility'     => 'visible', // Product visibility setting. Possible values are 'visible', 'catalog', 'search', 'hidden'.
				'class'          => '',        // HTML class.
				'page'           => 1,         // Page for pagination.
				'paginate'       => false,     // Should results be paginated.
				'cache'          => true,      // Should shortcode output be cached.
			),
			$attributes,
			$this->type
		);

		if ( ! absint( $attributes['columns'] ) ) {
			$attributes['columns'] = wc_get_default_products_per_row();
		}

		return $attributes;
	}

	/**
	 * Parse legacy attributes.
	 *
	 * @since  3.2.0
	 * @param  array $attributes Attributes.
	 * @return array
	 */
	protected function parse_legacy_attributes( $attributes ) {
		$mapping = array(
			'per_page' => 'limit',
			'operator' => 'cat_operator',
			'filter'   => 'terms',
		);

		foreach ( $mapping as $old => $new ) {
			if ( isset( $attributes[ $old ] ) ) {
				$attributes[ $new ] = $attributes[ $old ];
				unset( $attributes[ $old ] );
			}
		}

		return $attributes;
	}

	/**
	 * Parse query args.
	 *
	 * @since  3.2.0
	 * @return array
	 */
	protected function parse_query_args() {
		$query_args = array(
			'post_type'           => 'product',
			'post_status'         => 'publish',
			'ignore_sticky_posts' => true,
			'no_found_rows'       => false === wc_string_to_bool( $this->attributes['paginate'] ),
			'orderby'             => empty( $_GET['orderby'] ) ? $this->attributes['orderby'] : wc_clean( wp_unslash( $_GET['orderby'] ) ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		);

		$orderby_value         = explode( '-', $query_args['orderby'] );
		$orderby               = esc_attr( $orderby_value[0] );
		$order                 = ! empty( $orderby_value[1] ) ? $orderby_value[1] : strtoupper( $this->attributes['order'] );
		$query_args['orderby'] = $orderby;
		$query_args['order']   = $order;

		if ( wc_string_to_bool( $this->attributes['paginate'] ) ) {
			$this->attributes['page'] = absint( empty( $_GET['product-page'] ) ? 1 : $_GET['product-page'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		}

		if ( ! empty( $this->attributes['rows'] ) ) {
			$this->attributes['limit'] = $this->attributes['columns'] * $this->attributes['rows'];
		}

		$ordering_args         = WC()->query->get_catalog_ordering_args( $query_args['orderby'], $query_args['order'] );
		$query_args['orderby'] = $ordering_args['orderby'];
		$query_args['order']   = $ordering_args['order'];
		if ( $ordering_args['meta_key'] ) {
			$query_args['meta_key'] = $ordering_args['meta_key']; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
		}
		$query_args['posts_per_page'] = intval( $this->attributes['limit'] );
		if ( 1 < $this->attributes['page'] ) {
			$query_args['paged'] = absint( $this->attributes['page'] );
		}
		$query_args['meta_query'] = WC()->query->get_meta_query(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
		$query_args['tax_query']  = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query

		// Visibility.
		$this->set_visibility_query_args( $query_args );

		// SKUs.
		$this->set_skus_query_args( $query_args );

		// IDs.
		$this->set_ids_query_args( $query_args );

		// Set specific types query args.
		if ( method_exists( $this, "set_{$this->type}_query_args" ) ) {
			$this->{"set_{$this->type}_query_args"}( $query_args );
		}

		// Attributes.
		$this->set_attributes_query_args( $query_args );

		// Categories.
		$this->set_categories_query_args( $query_args );

		// Tags.
		$this->set_tags_query_args( $query_args );

		$query_args = apply_filters( 'woocommerce_shortcode_products_query', $query_args, $this->attributes, $this->type );

		// Always query only IDs.
		$query_args['fields'] = 'ids';

		return $query_args;
	}

	/**
	 * Set skus query args.
	 *
	 * @since 3.2.0
	 * @param array $query_args Query args.
	 */
	protected function set_skus_query_args( &$query_args ) {
		if ( ! empty( $this->attributes['skus'] ) ) {
			$skus                       = array_map( 'trim', explode( ',', $this->attributes['skus'] ) );
			$query_args['meta_query'][] = array(
				'key'     => '_sku',
				'value'   => 1 === count( $skus ) ? $skus[0] : $skus,
				'compare' => 1 === count( $skus ) ? '=' : 'IN',
			);
		}
	}

	/**
	 * Set ids query args.
	 *
	 * @since 3.2.0
	 * @param array $query_args Query args.
	 */
	protected function set_ids_query_args( &$query_args ) {
		if ( ! empty( $this->attributes['ids'] ) ) {
			$ids = array_map( 'trim', explode( ',', $this->attributes['ids'] ) );

			if ( 1 === count( $ids ) ) {
				$query_args['p'] = $ids[0];
			} else {
				$query_args['post__in'] = $ids;
			}
		}
	}

	/**
	 * Set attributes query args.
	 *
	 * @since 3.2.0
	 * @param array $query_args Query args.
	 */
	protected function set_attributes_query_args( &$query_args ) {
		if ( ! empty( $this->attributes['attribute'] ) || ! empty( $this->attributes['terms'] ) ) {
			$taxonomy = strstr( $this->attributes['attribute'], 'pa_' ) ? sanitize_title( $this->attributes['attribute'] ) : 'pa_' . sanitize_title( $this->attributes['attribute'] );
			$terms    = $this->attributes['terms'] ? array_map( 'sanitize_title', explode( ',', $this->attributes['terms'] ) ) : array();
			$field    = 'slug';

			if ( $terms && is_numeric( $terms[0] ) ) {
				$field = 'term_id';
				$terms = array_map( 'absint', $terms );
				// Check numeric slugs.
				foreach ( $terms as $term ) {
					$the_term = get_term_by( 'slug', $term, $taxonomy );
					if ( false !== $the_term ) {
						$terms[] = $the_term->term_id;
					}
				}
			}

			// If no terms were specified get all products that are in the attribute taxonomy.
			if ( ! $terms ) {
				$terms = get_terms(
					array(
						'taxonomy' => $taxonomy,
						'fields'   => 'ids',
					)
				);
				$field = 'term_id';
			}

			// We always need to search based on the slug as well, this is to accommodate numeric slugs.
			$query_args['tax_query'][] = array(
				'taxonomy' => $taxonomy,
				'terms'    => $terms,
				'field'    => $field,
				'operator' => $this->attributes['terms_operator'],
			);
		}
	}

	/**
	 * Set categories query args.
	 *
	 * @since 3.2.0
	 * @param array $query_args Query args.
	 */
	protected function set_categories_query_args( &$query_args ) {
		if ( ! empty( $this->attributes['category'] ) ) {
			$categories = array_map( 'sanitize_title', explode( ',', $this->attributes['category'] ) );
			$field      = 'slug';

			if ( is_numeric( $categories[0] ) ) {
				$field      = 'term_id';
				$categories = array_map( 'absint', $categories );
				// Check numeric slugs.
				foreach ( $categories as $cat ) {
					$the_cat = get_term_by( 'slug', $cat, 'product_cat' );
					if ( false !== $the_cat ) {
						$categories[] = $the_cat->term_id;
					}
				}
			}

			$query_args['tax_query'][] = array(
				'taxonomy'         => 'product_cat',
				'terms'            => $categories,
				'field'            => $field,
				'operator'         => $this->attributes['cat_operator'],

				/*
				 * When cat_operator is AND, the children categories should be excluded,
				 * as only products belonging to all the children categories would be selected.
				 */
				'include_children' => 'AND' === $this->attributes['cat_operator'] ? false : true,
			);
		}
	}

	/**
	 * Set tags query args.
	 *
	 * @since 3.3.0
	 * @param array $query_args Query args.
	 */
	protected function set_tags_query_args( &$query_args ) {
		if ( ! empty( $this->attributes['tag'] ) ) {
			$query_args['tax_query'][] = array(
				'taxonomy' => 'product_tag',
				'terms'    => array_map( 'sanitize_title', explode( ',', $this->attributes['tag'] ) ),
				'field'    => 'slug',
				'operator' => $this->attributes['tag_operator'],
			);
		}
	}

	/**
	 * Set sale products query args.
	 *
	 * @since 3.2.0
	 * @param array $query_args Query args.
	 */
	protected function set_sale_products_query_args( &$query_args ) {
		$query_args['post__in'] = array_merge( array( 0 ), wc_get_product_ids_on_sale() );
	}

	/**
	 * Set best selling products query args.
	 *
	 * @since 3.2.0
	 * @param array $query_args Query args.
	 */
	protected function set_best_selling_products_query_args( &$query_args ) {
		$query_args['meta_key'] = 'total_sales'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
		$query_args['order']    = 'DESC';
		$query_args['orderby']  = 'meta_value_num';
	}

	/**
	 * Set top rated products query args.
	 *
	 * @since 3.6.5
	 * @param array $query_args Query args.
	 */
	protected function set_top_rated_products_query_args( &$query_args ) {
		$query_args['meta_key'] = '_wc_average_rating'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
		$query_args['order']    = 'DESC';
		$query_args['orderby']  = 'meta_value_num';
	}

	/**
	 * Set visibility as hidden.
	 *
	 * @since 3.2.0
	 * @param array $query_args Query args.
	 */
	protected function set_visibility_hidden_query_args( &$query_args ) {
		$this->custom_visibility   = true;
		$query_args['tax_query'][] = array(
			'taxonomy'         => 'product_visibility',
			'terms'            => array( 'exclude-from-catalog', 'exclude-from-search' ),
			'field'            => 'name',
			'operator'         => 'AND',
			'include_children' => false,
		);
	}

	/**
	 * Set visibility as catalog.
	 *
	 * @since 3.2.0
	 * @param array $query_args Query args.
	 */
	protected function set_visibility_catalog_query_args( &$query_args ) {
		$this->custom_visibility   = true;
		$query_args['tax_query'][] = array(
			'taxonomy'         => 'product_visibility',
			'terms'            => 'exclude-from-search',
			'field'            => 'name',
			'operator'         => 'IN',
			'include_children' => false,
		);
		$query_args['tax_query'][] = array(
			'taxonomy'         => 'product_visibility',
			'terms'            => 'exclude-from-catalog',
			'field'            => 'name',
			'operator'         => 'NOT IN',
			'include_children' => false,
		);
	}

	/**
	 * Set visibility as search.
	 *
	 * @since 3.2.0
	 * @param array $query_args Query args.
	 */
	protected function set_visibility_search_query_args( &$query_args ) {
		$this->custom_visibility   = true;
		$query_args['tax_query'][] = array(
			'taxonomy'         => 'product_visibility',
			'terms'            => 'exclude-from-catalog',
			'field'            => 'name',
			'operator'         => 'IN',
			'include_children' => false,
		);
		$query_args['tax_query'][] = array(
			'taxonomy'         => 'product_visibility',
			'terms'            => 'exclude-from-search',
			'field'            => 'name',
			'operator'         => 'NOT IN',
			'include_children' => false,
		);
	}

	/**
	 * Set visibility as featured.
	 *
	 * @since 3.2.0
	 * @param array $query_args Query args.
	 */
	protected function set_visibility_featured_query_args( &$query_args ) {
		$query_args['tax_query'] = array_merge( $query_args['tax_query'], WC()->query->get_tax_query() ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query

		$query_args['tax_query'][] = array(
			'taxonomy'         => 'product_visibility',
			'terms'            => 'featured',
			'field'            => 'name',
			'operator'         => 'IN',
			'include_children' => false,
		);
	}

	/**
	 * Set visibility query args.
	 *
	 * @since 3.2.0
	 * @param array $query_args Query args.
	 */
	protected function set_visibility_query_args( &$query_args ) {
		if ( method_exists( $this, 'set_visibility_' . $this->attributes['visibility'] . '_query_args' ) ) {
			$this->{'set_visibility_' . $this->attributes['visibility'] . '_query_args'}( $query_args );
		} else {
			$query_args['tax_query'] = array_merge( $query_args['tax_query'], WC()->query->get_tax_query() ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
		}
	}

	/**
	 * Set product as visible when querying for hidden products.
	 *
	 * @since  3.2.0
	 * @param  bool $visibility Product visibility.
	 * @return bool
	 */
	public function set_product_as_visible( $visibility ) {
		return $this->custom_visibility ? true : $visibility;
	}

	/**
	 * Get wrapper classes.
	 *
	 * @since  3.2.0
	 * @param  int $columns Number of columns.
	 * @return array
	 */
	protected function get_wrapper_classes( $columns ) {
		$classes = array( 'woocommerce' );

		if ( 'product' !== $this->type ) {
			$classes[] = 'columns-' . $columns;
		}

		$classes[] = $this->attributes['class'];

		return $classes;
	}

	/**
	 * Generate and return the transient name for this shortcode based on the query args.
	 *
	 * @since 3.3.0
	 * @return string
	 */
	protected function get_transient_name() {
		$transient_name = 'wc_product_loop_' . md5( wp_json_encode( $this->query_args ) . $this->type );

		if ( 'rand' === $this->query_args['orderby'] ) {
			// When using rand, we'll cache a number of random queries and pull those to avoid querying rand on each page load.
			$rand_index      = wp_rand( 0, max( 1, absint( apply_filters( 'woocommerce_product_query_max_rand_cache_count', 5 ) ) ) );
			$transient_name .= $rand_index;
		}

		return $transient_name;
	}

	/**
	 * Run the query and return an array of data, including queried ids and pagination information.
	 *
	 * @since  3.3.0
	 * @return object Object with the following props; ids, per_page, found_posts, max_num_pages, current_page
	 */
	protected function get_query_results() {
		$transient_name    = $this->get_transient_name();
		$transient_version = WC_Cache_Helper::get_transient_version( 'product_query' );
		$cache             = wc_string_to_bool( $this->attributes['cache'] ) === true;
		$transient_value   = $cache ? get_transient( $transient_name ) : false;

		if ( isset( $transient_value['value'], $transient_value['version'] ) && $transient_value['version'] === $transient_version ) {
			$results = $transient_value['value'];
		} else {
			$query = new WP_Query( $this->query_args );

			$paginated = ! $query->get( 'no_found_rows' );

			$results = (object) array(
				'ids'          => wp_parse_id_list( $query->posts ),
				'total'        => $paginated ? (int) $query->found_posts : count( $query->posts ),
				'total_pages'  => $paginated ? (int) $query->max_num_pages : 1,
				'per_page'     => (int) $query->get( 'posts_per_page' ),
				'current_page' => $paginated ? (int) max( 1, $query->get( 'paged', 1 ) ) : 1,
			);

			if ( $cache ) {
				$transient_value = array(
					'version' => $transient_version,
					'value'   => $results,
				);
				set_transient( $transient_name, $transient_value, DAY_IN_SECONDS * 30 );
			}
		}

		// Remove ordering query arguments which may have been added by get_catalog_ordering_args.
		WC()->query->remove_ordering_args();

		/**
		 * Filter shortcode products query results.
		 *
		 * @since 4.0.0
		 * @param stdClass $results Query results.
		 * @param WC_Shortcode_Products $this WC_Shortcode_Products instance.
		 */
		return apply_filters( 'woocommerce_shortcode_products_query_results', $results, $this );
	}

	/**
	 * Loop over found products.
	 *
	 * @since  3.2.0
	 * @return string
	 */
	protected function product_loop() {
		$columns  = absint( $this->attributes['columns'] );
		$classes  = $this->get_wrapper_classes( $columns );
		$products = $this->get_query_results();

		ob_start();

		if ( $products && $products->ids ) {
			// Prime caches to reduce future queries.
			if ( is_callable( '_prime_post_caches' ) ) {
				_prime_post_caches( $products->ids );
			}

			// Setup the loop.
			wc_setup_loop(
				array(
					'columns'      => $columns,
					'name'         => $this->type,
					'is_shortcode' => true,
					'is_search'    => false,
					'is_paginated' => wc_string_to_bool( $this->attributes['paginate'] ),
					'total'        => $products->total,
					'total_pages'  => $products->total_pages,
					'per_page'     => $products->per_page,
					'current_page' => $products->current_page,
				)
			);

			$original_post = $GLOBALS['post'];

			do_action( "woocommerce_shortcode_before_{$this->type}_loop", $this->attributes );

			// Fire standard shop loop hooks when paginating results so we can show result counts and so on.
			if ( wc_string_to_bool( $this->attributes['paginate'] ) ) {
				do_action( 'woocommerce_before_shop_loop' );
			}

			woocommerce_product_loop_start();

			if ( wc_get_loop_prop( 'total' ) ) {
				foreach ( $products->ids as $product_id ) {
					$GLOBALS['post'] = get_post( $product_id ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
					setup_postdata( $GLOBALS['post'] );

					// Set custom product visibility when quering hidden products.
					add_action( 'woocommerce_product_is_visible', array( $this, 'set_product_as_visible' ) );

					// Render product template.
					wc_get_template_part( 'content', 'product' );

					// Restore product visibility.
					remove_action( 'woocommerce_product_is_visible', array( $this, 'set_product_as_visible' ) );
				}
			}

			$GLOBALS['post'] = $original_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
			woocommerce_product_loop_end();

			// Fire standard shop loop hooks when paginating results so we can show result counts and so on.
			if ( wc_string_to_bool( $this->attributes['paginate'] ) ) {
				do_action( 'woocommerce_after_shop_loop' );
			}

			do_action( "woocommerce_shortcode_after_{$this->type}_loop", $this->attributes );

			wp_reset_postdata();
			wc_reset_loop();
		} else {
			do_action( "woocommerce_shortcode_{$this->type}_loop_no_results", $this->attributes );
		}

		return '<div class="' . esc_attr( implode( ' ', $classes ) ) . '">' . ob_get_clean() . '</div>';
	}

	/**
	 * Order by rating.
	 *
	 * @since  3.2.0
	 * @param  array $args Query args.
	 * @return array
	 */
	public static function order_by_rating_post_clauses( $args ) {
		global $wpdb;

		$args['where']  .= " AND $wpdb->commentmeta.meta_key = 'rating' ";
		$args['join']   .= "LEFT JOIN $wpdb->comments ON($wpdb->posts.ID = $wpdb->comments.comment_post_ID) LEFT JOIN $wpdb->commentmeta ON($wpdb->comments.comment_ID = $wpdb->commentmeta.comment_id)";
		$args['orderby'] = "$wpdb->commentmeta.meta_value DESC";
		$args['groupby'] = "$wpdb->posts.ID";

		return $args;
	}
}
shortcodes/class-wc-shortcode-my-account.php000064400000027420151336044760015231 0ustar00<?php
/**
 * My Account Shortcodes
 *
 * Shows the 'my account' section where the customer can view past orders and update their information.
 *
 * @package WooCommerce\Shortcodes\My_Account
 * @version 2.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Shortcode my account class.
 */
class WC_Shortcode_My_Account {

	/**
	 * Get the shortcode content.
	 *
	 * @param array $atts Shortcode attributes.
	 *
	 * @return string
	 */
	public static function get( $atts ) {
		return WC_Shortcodes::shortcode_wrapper( array( __CLASS__, 'output' ), $atts );
	}

	/**
	 * Output the shortcode.
	 *
	 * @param array $atts Shortcode attributes.
	 */
	public static function output( $atts ) {
		global $wp;

		// Check cart class is loaded or abort.
		if ( is_null( WC()->cart ) ) {
			return;
		}

		if ( ! is_user_logged_in() || isset( $wp->query_vars['lost-password'] ) ) {
			$message = apply_filters( 'woocommerce_my_account_message', '' );

			if ( ! empty( $message ) ) {
				wc_add_notice( $message );
			}

			// After password reset, add confirmation message.
			if ( ! empty( $_GET['password-reset'] ) ) { // WPCS: input var ok, CSRF ok.
				wc_add_notice( __( 'Your password has been reset successfully.', 'woocommerce' ) );
			}

			if ( isset( $wp->query_vars['lost-password'] ) ) {
				self::lost_password();
			} else {
				wc_get_template( 'myaccount/form-login.php' );
			}
		} else {
			// Start output buffer since the html may need discarding for BW compatibility.
			ob_start();

			if ( isset( $wp->query_vars['customer-logout'] ) ) {
				/* translators: %s: logout url */
				wc_add_notice( sprintf( __( 'Are you sure you want to log out? <a href="%s">Confirm and log out</a>', 'woocommerce' ), wc_logout_url() ) );
			}

			// Collect notices before output.
			$notices = wc_get_notices();

			// Output the new account page.
			self::my_account( $atts );

			/**
			 * Deprecated my-account.php template handling. This code should be
			 * removed in a future release.
			 *
			 * If woocommerce_account_content did not run, this is an old template
			 * so we need to render the endpoint content again.
			 */
			if ( ! did_action( 'woocommerce_account_content' ) ) {
				if ( ! empty( $wp->query_vars ) ) {
					foreach ( $wp->query_vars as $key => $value ) {
						if ( 'pagename' === $key ) {
							continue;
						}
						if ( has_action( 'woocommerce_account_' . $key . '_endpoint' ) ) {
							ob_clean(); // Clear previous buffer.
							wc_set_notices( $notices );
							wc_print_notices();
							do_action( 'woocommerce_account_' . $key . '_endpoint', $value );
							break;
						}
					}

					wc_deprecated_function( 'Your theme version of my-account.php template', '2.6', 'the latest version, which supports multiple account pages and navigation, from WC 2.6.0' );
				}
			}

			// Send output buffer.
			ob_end_flush();
		}
	}

	/**
	 * My account page.
	 *
	 * @param array $atts Shortcode attributes.
	 */
	private static function my_account( $atts ) {
		$args = shortcode_atts(
			array(
				'order_count' => 15, // @deprecated 2.6.0. Keep for backward compatibility.
			),
			$atts,
			'woocommerce_my_account'
		);

		wc_get_template(
			'myaccount/my-account.php',
			array(
				'current_user' => get_user_by( 'id', get_current_user_id() ),
				'order_count'  => 'all' === $args['order_count'] ? -1 : $args['order_count'],
			)
		);
	}

	/**
	 * View order page.
	 *
	 * @param int $order_id Order ID.
	 */
	public static function view_order( $order_id ) {
		$order = wc_get_order( $order_id );

		if ( ! $order || ! current_user_can( 'view_order', $order_id ) ) {
			echo '<div class="woocommerce-error">' . esc_html__( 'Invalid order.', 'woocommerce' ) . ' <a href="' . esc_url( wc_get_page_permalink( 'myaccount' ) ) . '" class="wc-forward">' . esc_html__( 'My account', 'woocommerce' ) . '</a></div>';

			return;
		}

		// Backwards compatibility.
		$status       = new stdClass();
		$status->name = wc_get_order_status_name( $order->get_status() );

		wc_get_template(
			'myaccount/view-order.php',
			array(
				'status'   => $status, // @deprecated 2.2.
				'order'    => $order,
				'order_id' => $order->get_id(),
			)
		);
	}

	/**
	 * Edit account details page.
	 */
	public static function edit_account() {
		wc_get_template( 'myaccount/form-edit-account.php', array( 'user' => get_user_by( 'id', get_current_user_id() ) ) );
	}

	/**
	 * Edit address page.
	 *
	 * @param string $load_address Type of address to load.
	 */
	public static function edit_address( $load_address = 'billing' ) {
		$current_user = wp_get_current_user();
		$load_address = sanitize_key( $load_address );
		$country      = get_user_meta( get_current_user_id(), $load_address . '_country', true );

		if ( ! $country ) {
			$country = WC()->countries->get_base_country();
		}

		if ( 'billing' === $load_address ) {
			$allowed_countries = WC()->countries->get_allowed_countries();

			if ( ! array_key_exists( $country, $allowed_countries ) ) {
				$country = current( array_keys( $allowed_countries ) );
			}
		}

		if ( 'shipping' === $load_address ) {
			$allowed_countries = WC()->countries->get_shipping_countries();

			if ( ! array_key_exists( $country, $allowed_countries ) ) {
				$country = current( array_keys( $allowed_countries ) );
			}
		}

		$address = WC()->countries->get_address_fields( $country, $load_address . '_' );

		// Enqueue scripts.
		wp_enqueue_script( 'wc-country-select' );
		wp_enqueue_script( 'wc-address-i18n' );

		// Prepare values.
		foreach ( $address as $key => $field ) {

			$value = get_user_meta( get_current_user_id(), $key, true );

			if ( ! $value ) {
				switch ( $key ) {
					case 'billing_email':
					case 'shipping_email':
						$value = $current_user->user_email;
						break;
				}
			}

			$address[ $key ]['value'] = apply_filters( 'woocommerce_my_account_edit_address_field_value', $value, $key, $load_address );
		}

		wc_get_template(
			'myaccount/form-edit-address.php',
			array(
				'load_address' => $load_address,
				'address'      => apply_filters( 'woocommerce_address_to_edit', $address, $load_address ),
			)
		);
	}

	/**
	 * Lost password page handling.
	 */
	public static function lost_password() {
		/**
		 * After sending the reset link, don't show the form again.
		 */
		if ( ! empty( $_GET['reset-link-sent'] ) ) { // WPCS: input var ok, CSRF ok.
			return wc_get_template( 'myaccount/lost-password-confirmation.php' );

			/**
			 * Process reset key / login from email confirmation link
			 */
		} elseif ( ! empty( $_GET['show-reset-form'] ) ) { // WPCS: input var ok, CSRF ok.
			if ( isset( $_COOKIE[ 'wp-resetpass-' . COOKIEHASH ] ) && 0 < strpos( $_COOKIE[ 'wp-resetpass-' . COOKIEHASH ], ':' ) ) {  // @codingStandardsIgnoreLine
				list( $rp_id, $rp_key ) = array_map( 'wc_clean', explode( ':', wp_unslash( $_COOKIE[ 'wp-resetpass-' . COOKIEHASH ] ), 2 ) ); // @codingStandardsIgnoreLine
				$userdata               = get_userdata( absint( $rp_id ) );
				$rp_login               = $userdata ? $userdata->user_login : '';
				$user                   = self::check_password_reset_key( $rp_key, $rp_login );

				// Reset key / login is correct, display reset password form with hidden key / login values.
				if ( is_object( $user ) ) {
					return wc_get_template(
						'myaccount/form-reset-password.php',
						array(
							'key'   => $rp_key,
							'login' => $rp_login,
						)
					);
				}
			}
		}

		// Show lost password form by default.
		wc_get_template(
			'myaccount/form-lost-password.php',
			array(
				'form' => 'lost_password',
			)
		);
	}

	/**
	 * Handles sending password retrieval email to customer.
	 *
	 * Based on retrieve_password() in core wp-login.php.
	 *
	 * @uses $wpdb WordPress Database object
	 * @return bool True: when finish. False: on error
	 */
	public static function retrieve_password() {
		$login = isset( $_POST['user_login'] ) ? sanitize_user( wp_unslash( $_POST['user_login'] ) ) : ''; // WPCS: input var ok, CSRF ok.

		if ( empty( $login ) ) {

			wc_add_notice( __( 'Enter a username or email address.', 'woocommerce' ), 'error' );

			return false;

		} else {
			// Check on username first, as customers can use emails as usernames.
			$user_data = get_user_by( 'login', $login );
		}

		// If no user found, check if it login is email and lookup user based on email.
		if ( ! $user_data && is_email( $login ) && apply_filters( 'woocommerce_get_username_from_email', true ) ) {
			$user_data = get_user_by( 'email', $login );
		}

		$errors = new WP_Error();

		do_action( 'lostpassword_post', $errors, $user_data );

		if ( $errors->get_error_code() ) {
			wc_add_notice( $errors->get_error_message(), 'error' );

			return false;
		}

		if ( ! $user_data ) {
			wc_add_notice( __( 'Invalid username or email.', 'woocommerce' ), 'error' );

			return false;
		}

		if ( is_multisite() && ! is_user_member_of_blog( $user_data->ID, get_current_blog_id() ) ) {
			wc_add_notice( __( 'Invalid username or email.', 'woocommerce' ), 'error' );

			return false;
		}

		// Redefining user_login ensures we return the right case in the email.
		$user_login = $user_data->user_login;

		do_action( 'retrieve_password', $user_login );

		$allow = apply_filters( 'allow_password_reset', true, $user_data->ID );

		if ( ! $allow ) {

			wc_add_notice( __( 'Password reset is not allowed for this user', 'woocommerce' ), 'error' );

			return false;

		} elseif ( is_wp_error( $allow ) ) {

			wc_add_notice( $allow->get_error_message(), 'error' );

			return false;
		}

		// Get password reset key (function introduced in WordPress 4.4).
		$key = get_password_reset_key( $user_data );

		// Send email notification.
		WC()->mailer(); // Load email classes.
		do_action( 'woocommerce_reset_password_notification', $user_login, $key );

		return true;
	}

	/**
	 * Retrieves a user row based on password reset key and login.
	 *
	 * @uses $wpdb WordPress Database object.
	 * @param string $key   Hash to validate sending user's password.
	 * @param string $login The user login.
	 * @return WP_User|bool User's database row on success, false for invalid keys
	 */
	public static function check_password_reset_key( $key, $login ) {
		// Check for the password reset key.
		// Get user data or an error message in case of invalid or expired key.
		$user = check_password_reset_key( $key, $login );

		if ( is_wp_error( $user ) ) {
			wc_add_notice( __( 'This key is invalid or has already been used. Please reset your password again if needed.', 'woocommerce' ), 'error' );
			return false;
		}

		return $user;
	}

	/**
	 * Handles resetting the user's password.
	 *
	 * @param object $user     The user.
	 * @param string $new_pass New password for the user in plaintext.
	 */
	public static function reset_password( $user, $new_pass ) {
		do_action( 'password_reset', $user, $new_pass );

		wp_set_password( $new_pass, $user->ID );
		self::set_reset_password_cookie();

		if ( ! apply_filters( 'woocommerce_disable_password_change_notification', false ) ) {
			wp_password_change_notification( $user );
		}
	}

	/**
	 * Set or unset the cookie.
	 *
	 * @param string $value Cookie value.
	 */
	public static function set_reset_password_cookie( $value = '' ) {
		$rp_cookie = 'wp-resetpass-' . COOKIEHASH;
		$rp_path   = isset( $_SERVER['REQUEST_URI'] ) ? current( explode( '?', wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) : ''; // WPCS: input var ok, sanitization ok.

		if ( $value ) {
			setcookie( $rp_cookie, $value, 0, $rp_path, COOKIE_DOMAIN, is_ssl(), true );
		} else {
			setcookie( $rp_cookie, ' ', time() - YEAR_IN_SECONDS, $rp_path, COOKIE_DOMAIN, is_ssl(), true );
		}
	}

	/**
	 * Show the add payment method page.
	 */
	public static function add_payment_method() {
		if ( ! is_user_logged_in() ) {
			wp_safe_redirect( wc_get_page_permalink( 'myaccount' ) );
			exit();
		} else {
			do_action( 'before_woocommerce_add_payment_method' );

			wc_get_template( 'myaccount/form-add-payment-method.php' );

			do_action( 'after_woocommerce_add_payment_method' );
		}
	}
}
shortcodes/class-wc-shortcode-order-tracking.php000064400000004372151336044760016066 0ustar00<?php
/**
 * Order Tracking Shortcode
 *
 * Lets a user see the status of an order by entering their order details.
 *
 * @package WooCommerce\Shortcodes\Order_Tracking
 * @version 3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Shortcode order tracking class.
 */
class WC_Shortcode_Order_Tracking {

	/**
	 * Get the shortcode content.
	 *
	 * @param array $atts Shortcode attributes.
	 * @return string
	 */
	public static function get( $atts ) {
		return WC_Shortcodes::shortcode_wrapper( array( __CLASS__, 'output' ), $atts );
	}

	/**
	 * Output the shortcode.
	 *
	 * @param array $atts Shortcode attributes.
	 */
	public static function output( $atts ) {
		// Check cart class is loaded or abort.
		if ( is_null( WC()->cart ) ) {
			return;
		}

		$atts        = shortcode_atts( array(), $atts, 'woocommerce_order_tracking' );
		$nonce_value = wc_get_var( $_REQUEST['woocommerce-order-tracking-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine.

		if ( isset( $_REQUEST['orderid'] ) && wp_verify_nonce( $nonce_value, 'woocommerce-order_tracking' ) ) { // WPCS: input var ok.

			$order_id    = empty( $_REQUEST['orderid'] ) ? 0 : ltrim( wc_clean( wp_unslash( $_REQUEST['orderid'] ) ), '#' ); // WPCS: input var ok.
			$order_email = empty( $_REQUEST['order_email'] ) ? '' : sanitize_email( wp_unslash( $_REQUEST['order_email'] ) ); // WPCS: input var ok.

			if ( ! $order_id ) {
				wc_print_notice( __( 'Please enter a valid order ID', 'woocommerce' ), 'error' );
			} elseif ( ! $order_email ) {
				wc_print_notice( __( 'Please enter a valid email address', 'woocommerce' ), 'error' );
			} else {
				$order = wc_get_order( apply_filters( 'woocommerce_shortcode_order_tracking_order_id', $order_id ) );

				if ( $order && $order->get_id() && strtolower( $order->get_billing_email() ) === strtolower( $order_email ) ) {
					do_action( 'woocommerce_track_order', $order->get_id() );
					wc_get_template(
						'order/tracking.php',
						array(
							'order' => $order,
						)
					);
					return;
				} else {
					wc_print_notice( __( 'Sorry, the order could not be found. Please contact us if you are having difficulty finding your order details.', 'woocommerce' ), 'error' );
				}
			}
		}

		wc_get_template( 'order/form-tracking.php' );
	}
}
class-wc-cache-helper.php000064400000027431151336044760011327 0ustar00<?php
/**
 * WC_Cache_Helper class.
 *
 * @package WooCommerce\Classes
 */

defined( 'ABSPATH' ) || exit;

/**
 * WC_Cache_Helper.
 */
class WC_Cache_Helper {

	/**
	 * Transients to delete on shutdown.
	 *
	 * @var array Array of transient keys.
	 */
	private static $delete_transients = array();

	/**
	 * Hook in methods.
	 */
	public static function init() {
		add_filter( 'nocache_headers', array( __CLASS__, 'additional_nocache_headers' ), 10 );
		add_action( 'shutdown', array( __CLASS__, 'delete_transients_on_shutdown' ), 10 );
		add_action( 'template_redirect', array( __CLASS__, 'geolocation_ajax_redirect' ) );
		add_action( 'wc_ajax_update_order_review', array( __CLASS__, 'update_geolocation_hash' ), 5 );
		add_action( 'admin_notices', array( __CLASS__, 'notices' ) );
		add_action( 'delete_version_transients', array( __CLASS__, 'delete_version_transients' ), 10 );
		add_action( 'wp', array( __CLASS__, 'prevent_caching' ) );
		add_action( 'clean_term_cache', array( __CLASS__, 'clean_term_cache' ), 10, 2 );
		add_action( 'edit_terms', array( __CLASS__, 'clean_term_cache' ), 10, 2 );
	}

	/**
	 * Set additional nocache headers.
	 *
	 * @param array $headers Header names and field values.
	 * @since 3.6.0
	 */
	public static function additional_nocache_headers( $headers ) {
		$agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

		$set_cache = false;

		/**
		 * Allow plugins to enable nocache headers. Enabled for Google weblight.
		 *
		 * @param bool $enable_nocache_headers Flag indicating whether to add nocache headers. Default: false.
		 */
		if ( apply_filters( 'woocommerce_enable_nocache_headers', false ) ) {
			$set_cache = true;
		}

		/**
		 * Enabled for Google weblight.
		 *
		 * @see https://support.google.com/webmasters/answer/1061943?hl=en
		 */
		if ( false !== strpos( $agent, 'googleweblight' ) ) {
			// no-transform: Opt-out of Google weblight. https://support.google.com/webmasters/answer/6211428?hl=en.
			$set_cache = true;
		}

		if ( false !== strpos( $agent, 'Chrome' ) && is_cart() ) {
			$set_cache = true;
		}

		if ( $set_cache ) {
			$headers['Cache-Control'] = 'no-transform, no-cache, no-store, must-revalidate';
		}
		return $headers;
	}

	/**
	 * Add a transient to delete on shutdown.
	 *
	 * @since 3.6.0
	 * @param string|array $keys Transient key or keys.
	 */
	public static function queue_delete_transient( $keys ) {
		self::$delete_transients = array_unique( array_merge( is_array( $keys ) ? $keys : array( $keys ), self::$delete_transients ) );
	}

	/**
	 * Transients that don't need to be cleaned right away can be deleted on shutdown to avoid repetition.
	 *
	 * @since 3.6.0
	 */
	public static function delete_transients_on_shutdown() {
		if ( self::$delete_transients ) {
			foreach ( self::$delete_transients as $key ) {
				delete_transient( $key );
			}
			self::$delete_transients = array();
		}
	}

	/**
	 * Used to clear layered nav counts based on passed attribute names.
	 *
	 * @since 3.6.0
	 * @param array $attribute_keys Attribute keys.
	 */
	public static function invalidate_attribute_count( $attribute_keys ) {
		if ( $attribute_keys ) {
			foreach ( $attribute_keys as $attribute_key ) {
				self::queue_delete_transient( 'wc_layered_nav_counts_' . $attribute_key );
			}
		}
	}

	/**
	 * Get prefix for use with wp_cache_set. Allows all cache in a group to be invalidated at once.
	 *
	 * @param  string $group Group of cache to get.
	 * @return string
	 */
	public static function get_cache_prefix( $group ) {
		// Get cache key - uses cache key wc_orders_cache_prefix to invalidate when needed.
		$prefix = wp_cache_get( 'wc_' . $group . '_cache_prefix', $group );

		if ( false === $prefix ) {
			$prefix = microtime();
			wp_cache_set( 'wc_' . $group . '_cache_prefix', $prefix, $group );
		}

		return 'wc_cache_' . $prefix . '_';
	}

	/**
	 * Increment group cache prefix (invalidates cache).
	 *
	 * @param string $group Group of cache to clear.
	 */
	public static function incr_cache_prefix( $group ) {
		wc_deprecated_function( 'WC_Cache_Helper::incr_cache_prefix', '3.9.0', 'WC_Cache_Helper::invalidate_cache_group' );
		self::invalidate_cache_group( $group );
	}

	/**
	 * Invalidate cache group.
	 *
	 * @param string $group Group of cache to clear.
	 * @since 3.9.0
	 */
	public static function invalidate_cache_group( $group ) {
		wp_cache_set( 'wc_' . $group . '_cache_prefix', microtime(), $group );
	}

	/**
	 * Get a hash of the customer location.
	 *
	 * @return string
	 */
	public static function geolocation_ajax_get_location_hash() {
		$customer             = new WC_Customer( 0, true );
		$location             = array();
		$location['country']  = $customer->get_billing_country();
		$location['state']    = $customer->get_billing_state();
		$location['postcode'] = $customer->get_billing_postcode();
		$location['city']     = $customer->get_billing_city();
		return apply_filters( 'woocommerce_geolocation_ajax_get_location_hash', substr( md5( implode( '', $location ) ), 0, 12 ), $location, $customer );
	}

	/**
	 * Prevent caching on certain pages
	 */
	public static function prevent_caching() {
		if ( ! is_blog_installed() ) {
			return;
		}
		$page_ids = array_filter( array( wc_get_page_id( 'cart' ), wc_get_page_id( 'checkout' ), wc_get_page_id( 'myaccount' ) ) );

		if ( is_page( $page_ids ) ) {
			self::set_nocache_constants();
			nocache_headers();
		}
	}

	/**
	 * When using geolocation via ajax, to bust cache, redirect if the location hash does not equal the querystring.
	 *
	 * This prevents caching of the wrong data for this request.
	 */
	public static function geolocation_ajax_redirect() {
		if ( 'geolocation_ajax' === get_option( 'woocommerce_default_customer_address' ) && ! is_checkout() && ! is_cart() && ! is_account_page() && ! is_ajax() && empty( $_POST ) ) { // WPCS: CSRF ok, input var ok.
			$location_hash = self::geolocation_ajax_get_location_hash();
			$current_hash  = isset( $_GET['v'] ) ? wc_clean( wp_unslash( $_GET['v'] ) ) : ''; // WPCS: sanitization ok, input var ok, CSRF ok.
			if ( empty( $current_hash ) || $current_hash !== $location_hash ) {
				global $wp;

				$redirect_url = trailingslashit( home_url( $wp->request ) );

				if ( ! empty( $_SERVER['QUERY_STRING'] ) ) { // WPCS: Input var ok.
					$redirect_url = add_query_arg( wp_unslash( $_SERVER['QUERY_STRING'] ), '', $redirect_url ); // WPCS: sanitization ok, Input var ok.
				}

				if ( ! get_option( 'permalink_structure' ) ) {
					$redirect_url = add_query_arg( $wp->query_string, '', $redirect_url );
				}

				$redirect_url = add_query_arg( 'v', $location_hash, remove_query_arg( 'v', $redirect_url ) );

				wp_safe_redirect( esc_url_raw( $redirect_url ), 307 );
				exit;
			}
		}
	}

	/**
	 * Updates the `woocommerce_geo_hash` cookie, which is used to help ensure we display
	 * the correct pricing etc to customers, according to their billing country.
	 *
	 * Note that:
	 *
	 * A) This only sets the cookie if the default customer address is set to "Geolocate (with
	 *    Page Caching Support)".
	 *
	 * B) It is hooked into the `wc_ajax_update_order_review` action, which has the benefit of
	 *    ensuring we update the cookie any time the billing country is changed.
	 */
	public static function update_geolocation_hash() {
		if ( 'geolocation_ajax' === get_option( 'woocommerce_default_customer_address' ) ) {
			wc_setcookie( 'woocommerce_geo_hash', static::geolocation_ajax_get_location_hash(), time() + HOUR_IN_SECONDS );
		}
	}

	/**
	 * Get transient version.
	 *
	 * When using transients with unpredictable names, e.g. those containing an md5
	 * hash in the name, we need a way to invalidate them all at once.
	 *
	 * When using default WP transients we're able to do this with a DB query to
	 * delete transients manually.
	 *
	 * With external cache however, this isn't possible. Instead, this function is used
	 * to append a unique string (based on time()) to each transient. When transients
	 * are invalidated, the transient version will increment and data will be regenerated.
	 *
	 * Raised in issue https://github.com/woocommerce/woocommerce/issues/5777.
	 * Adapted from ideas in http://tollmanz.com/invalidation-schemes/.
	 *
	 * @param  string  $group   Name for the group of transients we need to invalidate.
	 * @param  boolean $refresh true to force a new version.
	 * @return string transient version based on time(), 10 digits.
	 */
	public static function get_transient_version( $group, $refresh = false ) {
		$transient_name  = $group . '-transient-version';
		$transient_value = get_transient( $transient_name );

		if ( false === $transient_value || true === $refresh ) {
			$transient_value = (string) time();

			set_transient( $transient_name, $transient_value );
		}

		return $transient_value;
	}

	/**
	 * Set constants to prevent caching by some plugins.
	 *
	 * @param  mixed $return Value to return. Previously hooked into a filter.
	 * @return mixed
	 */
	public static function set_nocache_constants( $return = true ) {
		wc_maybe_define_constant( 'DONOTCACHEPAGE', true );
		wc_maybe_define_constant( 'DONOTCACHEOBJECT', true );
		wc_maybe_define_constant( 'DONOTCACHEDB', true );
		return $return;
	}

	/**
	 * Notices function.
	 */
	public static function notices() {
		if ( ! function_exists( 'w3tc_pgcache_flush' ) || ! function_exists( 'w3_instance' ) ) {
			return;
		}

		$config   = w3_instance( 'W3_Config' );
		$enabled  = $config->get_integer( 'dbcache.enabled' );
		$settings = array_map( 'trim', $config->get_array( 'dbcache.reject.sql' ) );

		if ( $enabled && ! in_array( '_wc_session_', $settings, true ) ) {
			?>
			<div class="error">
				<p>
				<?php
				/* translators: 1: key 2: URL */
				echo wp_kses_post( sprintf( __( 'In order for <strong>database caching</strong> to work with WooCommerce you must add %1$s to the "Ignored Query Strings" option in <a href="%2$s">W3 Total Cache settings</a>.', 'woocommerce' ), '<code>_wc_session_</code>', esc_url( admin_url( 'admin.php?page=w3tc_dbcache' ) ) ) );
				?>
				</p>
			</div>
			<?php
		}
	}

	/**
	 * Clean term caches added by WooCommerce.
	 *
	 * @since 3.3.4
	 * @param array|int $ids Array of ids or single ID to clear cache for.
	 * @param string    $taxonomy Taxonomy name.
	 */
	public static function clean_term_cache( $ids, $taxonomy ) {
		if ( 'product_cat' === $taxonomy ) {
			$ids = is_array( $ids ) ? $ids : array( $ids );

			$clear_ids = array( 0 );

			foreach ( $ids as $id ) {
				$clear_ids[] = $id;
				$clear_ids   = array_merge( $clear_ids, get_ancestors( $id, 'product_cat', 'taxonomy' ) );
			}

			$clear_ids = array_unique( $clear_ids );

			foreach ( $clear_ids as $id ) {
				wp_cache_delete( 'product-category-hierarchy-' . $id, 'product_cat' );
			}
		}
	}

	/**
	 * When the transient version increases, this is used to remove all past transients to avoid filling the DB.
	 *
	 * Note; this only works on transients appended with the transient version, and when object caching is not being used.
	 *
	 * @deprecated 3.6.0 Adjusted transient usage to include versions within the transient values, making this cleanup obsolete.
	 * @since  2.3.10
	 * @param string $version Version of the transient to remove.
	 */
	public static function delete_version_transients( $version = '' ) {
		if ( ! wp_using_ext_object_cache() && ! empty( $version ) ) {
			global $wpdb;

			$limit = apply_filters( 'woocommerce_delete_version_transients_limit', 1000 );

			if ( ! $limit ) {
				return;
			}

			$affected = $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s LIMIT %d;", '\_transient\_%' . $version, $limit ) ); // WPCS: cache ok, db call ok.

			// If affected rows is equal to limit, there are more rows to delete. Delete in 30 secs.
			if ( $affected === $limit ) {
				wp_schedule_single_event( time() + 30, 'delete_version_transients', array( $version ) );
			}
		}
	}
}

WC_Cache_Helper::init();
class-wc-logger.php000064400000021417151336044760010264 0ustar00<?php
/**
 * Provides logging capabilities for debugging purposes.
 *
 * @class          WC_Logger
 * @version        2.0.0
 * @package        WooCommerce\Classes
 */

use Automattic\Jetpack\Constants;

defined( 'ABSPATH' ) || exit;

/**
 * WC_Logger class.
 */
class WC_Logger implements WC_Logger_Interface {

	/**
	 * Stores registered log handlers.
	 *
	 * @var array
	 */
	protected $handlers;

	/**
	 * Minimum log level this handler will process.
	 *
	 * @var int Integer representation of minimum log level to handle.
	 */
	protected $threshold;

	/**
	 * Constructor for the logger.
	 *
	 * @param array  $handlers Optional. Array of log handlers. If $handlers is not provided, the filter 'woocommerce_register_log_handlers' will be used to define the handlers. If $handlers is provided, the filter will not be applied and the handlers will be used directly.
	 * @param string $threshold Optional. Define an explicit threshold. May be configured via  WC_LOG_THRESHOLD. By default, all logs will be processed.
	 */
	public function __construct( $handlers = null, $threshold = null ) {
		if ( null === $handlers ) {
			$handlers = apply_filters( 'woocommerce_register_log_handlers', array() );
		}

		$register_handlers = array();

		if ( ! empty( $handlers ) && is_array( $handlers ) ) {
			foreach ( $handlers as $handler ) {
				$implements = class_implements( $handler );
				if ( is_object( $handler ) && is_array( $implements ) && in_array( 'WC_Log_Handler_Interface', $implements, true ) ) {
					$register_handlers[] = $handler;
				} else {
					wc_doing_it_wrong(
						__METHOD__,
						sprintf(
							/* translators: 1: class name 2: WC_Log_Handler_Interface */
							__( 'The provided handler %1$s does not implement %2$s.', 'woocommerce' ),
							'<code>' . esc_html( is_object( $handler ) ? get_class( $handler ) : $handler ) . '</code>',
							'<code>WC_Log_Handler_Interface</code>'
						),
						'3.0'
					);
				}
			}
		}

		// Support the constant as long as a valid log level has been set for it.
		if ( null === $threshold ) {
			$threshold = Constants::get_constant( 'WC_LOG_THRESHOLD' );
			if ( null !== $threshold && ! WC_Log_Levels::is_valid_level( $threshold ) ) {
				$threshold = null;
			}
		}

		if ( null !== $threshold ) {
			$threshold = WC_Log_Levels::get_level_severity( $threshold );
		}

		$this->handlers  = $register_handlers;
		$this->threshold = $threshold;
	}

	/**
	 * Determine whether to handle or ignore log.
	 *
	 * @param string $level emergency|alert|critical|error|warning|notice|info|debug.
	 * @return bool True if the log should be handled.
	 */
	protected function should_handle( $level ) {
		if ( null === $this->threshold ) {
			return true;
		}
		return $this->threshold <= WC_Log_Levels::get_level_severity( $level );
	}

	/**
	 * Add a log entry.
	 *
	 * This is not the preferred method for adding log messages. Please use log() or any one of
	 * the level methods (debug(), info(), etc.). This method may be deprecated in the future.
	 *
	 * @param string $handle File handle.
	 * @param string $message Message to log.
	 * @param string $level Logging level.
	 * @return bool
	 */
	public function add( $handle, $message, $level = WC_Log_Levels::NOTICE ) {
		$message = apply_filters( 'woocommerce_logger_add_message', $message, $handle );
		$this->log(
			$level,
			$message,
			array(
				'source'  => $handle,
				'_legacy' => true,
			)
		);
		wc_do_deprecated_action( 'woocommerce_log_add', array( $handle, $message ), '3.0', 'This action has been deprecated with no alternative.' );
		return true;
	}

	/**
	 * Add a log entry.
	 *
	 * @param string $level One of the following:
	 *     'emergency': System is unusable.
	 *     'alert': Action must be taken immediately.
	 *     'critical': Critical conditions.
	 *     'error': Error conditions.
	 *     'warning': Warning conditions.
	 *     'notice': Normal but significant condition.
	 *     'info': Informational messages.
	 *     'debug': Debug-level messages.
	 * @param string $message Log message.
	 * @param array  $context Optional. Additional information for log handlers.
	 */
	public function log( $level, $message, $context = array() ) {
		if ( ! WC_Log_Levels::is_valid_level( $level ) ) {
			/* translators: 1: WC_Logger::log 2: level */
			wc_doing_it_wrong( __METHOD__, sprintf( __( '%1$s was called with an invalid level "%2$s".', 'woocommerce' ), '<code>WC_Logger::log</code>', $level ), '3.0' );
		}

		if ( $this->should_handle( $level ) ) {
			$timestamp = time();

			foreach ( $this->handlers as $handler ) {
				/**
				 * Filter the logging message. Returning null will prevent logging from occuring since 5.3.
				 *
				 * @since 3.1
				 * @param string $message Log message.
				 * @param string $level   One of: emergency, alert, critical, error, warning, notice, info, or debug.
				 * @param array  $context Additional information for log handlers.
				 * @param object $handler The handler object, such as WC_Log_Handler_File. Available since 5.3.
				 */
				$message = apply_filters( 'woocommerce_logger_log_message', $message, $level, $context, $handler );

				if ( null !== $message ) {
					$handler->handle( $timestamp, $level, $message, $context );
				}
			}
		}
	}

	/**
	 * Adds an emergency level message.
	 *
	 * System is unusable.
	 *
	 * @see WC_Logger::log
	 *
	 * @param string $message Message to log.
	 * @param array  $context Log context.
	 */
	public function emergency( $message, $context = array() ) {
		$this->log( WC_Log_Levels::EMERGENCY, $message, $context );
	}

	/**
	 * Adds an alert level message.
	 *
	 * Action must be taken immediately.
	 * Example: Entire website down, database unavailable, etc.
	 *
	 * @see WC_Logger::log
	 *
	 * @param string $message Message to log.
	 * @param array  $context Log context.
	 */
	public function alert( $message, $context = array() ) {
		$this->log( WC_Log_Levels::ALERT, $message, $context );
	}

	/**
	 * Adds a critical level message.
	 *
	 * Critical conditions.
	 * Example: Application component unavailable, unexpected exception.
	 *
	 * @see WC_Logger::log
	 *
	 * @param string $message Message to log.
	 * @param array  $context Log context.
	 */
	public function critical( $message, $context = array() ) {
		$this->log( WC_Log_Levels::CRITICAL, $message, $context );
	}

	/**
	 * Adds an error level message.
	 *
	 * Runtime errors that do not require immediate action but should typically be logged
	 * and monitored.
	 *
	 * @see WC_Logger::log
	 *
	 * @param string $message Message to log.
	 * @param array  $context Log context.
	 */
	public function error( $message, $context = array() ) {
		$this->log( WC_Log_Levels::ERROR, $message, $context );
	}

	/**
	 * Adds a warning level message.
	 *
	 * Exceptional occurrences that are not errors.
	 *
	 * Example: Use of deprecated APIs, poor use of an API, undesirable things that are not
	 * necessarily wrong.
	 *
	 * @see WC_Logger::log
	 *
	 * @param string $message Message to log.
	 * @param array  $context Log context.
	 */
	public function warning( $message, $context = array() ) {
		$this->log( WC_Log_Levels::WARNING, $message, $context );
	}

	/**
	 * Adds a notice level message.
	 *
	 * Normal but significant events.
	 *
	 * @see WC_Logger::log
	 *
	 * @param string $message Message to log.
	 * @param array  $context Log context.
	 */
	public function notice( $message, $context = array() ) {
		$this->log( WC_Log_Levels::NOTICE, $message, $context );
	}

	/**
	 * Adds a info level message.
	 *
	 * Interesting events.
	 * Example: User logs in, SQL logs.
	 *
	 * @see WC_Logger::log
	 *
	 * @param string $message Message to log.
	 * @param array  $context Log context.
	 */
	public function info( $message, $context = array() ) {
		$this->log( WC_Log_Levels::INFO, $message, $context );
	}

	/**
	 * Adds a debug level message.
	 *
	 * Detailed debug information.
	 *
	 * @see WC_Logger::log
	 *
	 * @param string $message Message to log.
	 * @param array  $context Log context.
	 */
	public function debug( $message, $context = array() ) {
		$this->log( WC_Log_Levels::DEBUG, $message, $context );
	}

	/**
	 * Clear entries for a chosen file/source.
	 *
	 * @param string $source Source/handle to clear.
	 * @return bool
	 */
	public function clear( $source = '' ) {
		if ( ! $source ) {
			return false;
		}
		foreach ( $this->handlers as $handler ) {
			if ( is_callable( array( $handler, 'clear' ) ) ) {
				$handler->clear( $source );
			}
		}
		return true;
	}

	/**
	 * Clear all logs older than a defined number of days. Defaults to 30 days.
	 *
	 * @since 3.4.0
	 */
	public function clear_expired_logs() {
		$days      = absint( apply_filters( 'woocommerce_logger_days_to_retain_logs', 30 ) );
		$timestamp = strtotime( "-{$days} days" );

		foreach ( $this->handlers as $handler ) {
			if ( is_callable( array( $handler, 'delete_logs_before_timestamp' ) ) ) {
				$handler->delete_logs_before_timestamp( $timestamp );
			}
		}
	}
}
wc-core-functions.php000064400000234177151336044760010651 0ustar00<?php
/**
 * WooCommerce Core Functions
 *
 * General core functions available on both the front-end and admin.
 *
 * @package WooCommerce\Functions
 * @version 3.3.0
 */

use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Utilities\NumberUtil;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// Include core functions (available in both admin and frontend).
require WC_ABSPATH . 'includes/wc-conditional-functions.php';
require WC_ABSPATH . 'includes/wc-coupon-functions.php';
require WC_ABSPATH . 'includes/wc-user-functions.php';
require WC_ABSPATH . 'includes/wc-deprecated-functions.php';
require WC_ABSPATH . 'includes/wc-formatting-functions.php';
require WC_ABSPATH . 'includes/wc-order-functions.php';
require WC_ABSPATH . 'includes/wc-order-item-functions.php';
require WC_ABSPATH . 'includes/wc-page-functions.php';
require WC_ABSPATH . 'includes/wc-product-functions.php';
require WC_ABSPATH . 'includes/wc-stock-functions.php';
require WC_ABSPATH . 'includes/wc-account-functions.php';
require WC_ABSPATH . 'includes/wc-term-functions.php';
require WC_ABSPATH . 'includes/wc-attribute-functions.php';
require WC_ABSPATH . 'includes/wc-rest-functions.php';
require WC_ABSPATH . 'includes/wc-widget-functions.php';
require WC_ABSPATH . 'includes/wc-webhook-functions.php';

/**
 * Filters on data used in admin and frontend.
 */
add_filter( 'woocommerce_coupon_code', 'html_entity_decode' );
add_filter( 'woocommerce_coupon_code', 'wc_sanitize_coupon_code' );
add_filter( 'woocommerce_coupon_code', 'wc_strtolower' );
add_filter( 'woocommerce_stock_amount', 'intval' ); // Stock amounts are integers by default.
add_filter( 'woocommerce_shipping_rate_label', 'sanitize_text_field' ); // Shipping rate label.
add_filter( 'woocommerce_attribute_label', 'wp_kses_post', 100 );

/**
 * Short Description (excerpt).
 */
if ( function_exists( 'do_blocks' ) ) {
	add_filter( 'woocommerce_short_description', 'do_blocks', 9 );
}
add_filter( 'woocommerce_short_description', 'wptexturize' );
add_filter( 'woocommerce_short_description', 'convert_smilies' );
add_filter( 'woocommerce_short_description', 'convert_chars' );
add_filter( 'woocommerce_short_description', 'wpautop' );
add_filter( 'woocommerce_short_description', 'shortcode_unautop' );
add_filter( 'woocommerce_short_description', 'prepend_attachment' );
add_filter( 'woocommerce_short_description', 'do_shortcode', 11 ); // After wpautop().
add_filter( 'woocommerce_short_description', 'wc_format_product_short_description', 9999999 );
add_filter( 'woocommerce_short_description', 'wc_do_oembeds' );
add_filter( 'woocommerce_short_description', array( $GLOBALS['wp_embed'], 'run_shortcode' ), 8 ); // Before wpautop().

/**
 * Define a constant if it is not already defined.
 *
 * @since 3.0.0
 * @param string $name  Constant name.
 * @param mixed  $value Value.
 */
function wc_maybe_define_constant( $name, $value ) {
	if ( ! defined( $name ) ) {
		define( $name, $value );
	}
}

/**
 * Create a new order programmatically.
 *
 * Returns a new order object on success which can then be used to add additional data.
 *
 * @param  array $args Order arguments.
 * @return WC_Order|WP_Error
 */
function wc_create_order( $args = array() ) {
	$default_args = array(
		'status'        => null,
		'customer_id'   => null,
		'customer_note' => null,
		'parent'        => null,
		'created_via'   => null,
		'cart_hash'     => null,
		'order_id'      => 0,
	);

	try {
		$args  = wp_parse_args( $args, $default_args );
		$order = new WC_Order( $args['order_id'] );

		// Update props that were set (not null).
		if ( ! is_null( $args['parent'] ) ) {
			$order->set_parent_id( absint( $args['parent'] ) );
		}

		if ( ! is_null( $args['status'] ) ) {
			$order->set_status( $args['status'] );
		}

		if ( ! is_null( $args['customer_note'] ) ) {
			$order->set_customer_note( $args['customer_note'] );
		}

		if ( ! is_null( $args['customer_id'] ) ) {
			$order->set_customer_id( is_numeric( $args['customer_id'] ) ? absint( $args['customer_id'] ) : 0 );
		}

		if ( ! is_null( $args['created_via'] ) ) {
			$order->set_created_via( sanitize_text_field( $args['created_via'] ) );
		}

		if ( ! is_null( $args['cart_hash'] ) ) {
			$order->set_cart_hash( sanitize_text_field( $args['cart_hash'] ) );
		}

		// Set these fields when creating a new order but not when updating an existing order.
		if ( ! $args['order_id'] ) {
			$order->set_currency( get_woocommerce_currency() );
			$order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) );
			$order->set_customer_ip_address( WC_Geolocation::get_ip_address() );
			$order->set_customer_user_agent( wc_get_user_agent() );
		}

		// Update other order props set automatically.
		$order->save();
	} catch ( Exception $e ) {
		return new WP_Error( 'error', $e->getMessage() );
	}

	return $order;
}

/**
 * Update an order. Uses wc_create_order.
 *
 * @param  array $args Order arguments.
 * @return WC_Order|WP_Error
 */
function wc_update_order( $args ) {
	if ( empty( $args['order_id'] ) ) {
		return new WP_Error( __( 'Invalid order ID.', 'woocommerce' ) );
	}
	return wc_create_order( $args );
}

/**
 * Given a path, this will convert any of the subpaths into their corresponding tokens.
 *
 * @since 4.3.0
 * @param string $path The absolute path to tokenize.
 * @param array  $path_tokens An array keyed with the token, containing paths that should be replaced.
 * @return string The tokenized path.
 */
function wc_tokenize_path( $path, $path_tokens ) {
	// Order most to least specific so that the token can encompass as much of the path as possible.
	uasort(
		$path_tokens,
		function ( $a, $b ) {
			$a = strlen( $a );
			$b = strlen( $b );

			if ( $a > $b ) {
				return -1;
			}

			if ( $b > $a ) {
				return 1;
			}

			return 0;
		}
	);

	foreach ( $path_tokens as $token => $token_path ) {
		if ( 0 !== strpos( $path, $token_path ) ) {
			continue;
		}

		$path = str_replace( $token_path, '{{' . $token . '}}', $path );
	}

	return $path;
}

/**
 * Given a tokenized path, this will expand the tokens to their full path.
 *
 * @since 4.3.0
 * @param string $path The absolute path to expand.
 * @param array  $path_tokens An array keyed with the token, containing paths that should be expanded.
 * @return string The absolute path.
 */
function wc_untokenize_path( $path, $path_tokens ) {
	foreach ( $path_tokens as $token => $token_path ) {
		$path = str_replace( '{{' . $token . '}}', $token_path, $path );
	}

	return $path;
}

/**
 * Fetches an array containing all of the configurable path constants to be used in tokenization.
 *
 * @return array The key is the define and the path is the constant.
 */
function wc_get_path_define_tokens() {
	$defines = array(
		'ABSPATH',
		'WP_CONTENT_DIR',
		'WP_PLUGIN_DIR',
		'WPMU_PLUGIN_DIR',
		'PLUGINDIR',
		'WP_THEME_DIR',
	);

	$path_tokens = array();
	foreach ( $defines as $define ) {
		if ( defined( $define ) ) {
			$path_tokens[ $define ] = constant( $define );
		}
	}

	return apply_filters( 'woocommerce_get_path_define_tokens', $path_tokens );
}

/**
 * Get template part (for templates like the shop-loop).
 *
 * WC_TEMPLATE_DEBUG_MODE will prevent overrides in themes from taking priority.
 *
 * @param mixed  $slug Template slug.
 * @param string $name Template name (default: '').
 */
function wc_get_template_part( $slug, $name = '' ) {
	$cache_key = sanitize_key( implode( '-', array( 'template-part', $slug, $name, Constants::get_constant( 'WC_VERSION' ) ) ) );
	$template  = (string) wp_cache_get( $cache_key, 'woocommerce' );

	if ( ! $template ) {
		if ( $name ) {
			$template = WC_TEMPLATE_DEBUG_MODE ? '' : locate_template(
				array(
					"{$slug}-{$name}.php",
					WC()->template_path() . "{$slug}-{$name}.php",
				)
			);

			if ( ! $template ) {
				$fallback = WC()->plugin_path() . "/templates/{$slug}-{$name}.php";
				$template = file_exists( $fallback ) ? $fallback : '';
			}
		}

		if ( ! $template ) {
			// If template file doesn't exist, look in yourtheme/slug.php and yourtheme/woocommerce/slug.php.
			$template = WC_TEMPLATE_DEBUG_MODE ? '' : locate_template(
				array(
					"{$slug}.php",
					WC()->template_path() . "{$slug}.php",
				)
			);
		}

		// Don't cache the absolute path so that it can be shared between web servers with different paths.
		$cache_path = wc_tokenize_path( $template, wc_get_path_define_tokens() );

		wc_set_template_cache( $cache_key, $cache_path );
	} else {
		// Make sure that the absolute path to the template is resolved.
		$template = wc_untokenize_path( $template, wc_get_path_define_tokens() );
	}

	// Allow 3rd party plugins to filter template file from their plugin.
	$template = apply_filters( 'wc_get_template_part', $template, $slug, $name );

	if ( $template ) {
		load_template( $template, false );
	}
}

/**
 * Get other templates (e.g. product attributes) passing attributes and including the file.
 *
 * @param string $template_name Template name.
 * @param array  $args          Arguments. (default: array).
 * @param string $template_path Template path. (default: '').
 * @param string $default_path  Default path. (default: '').
 */
function wc_get_template( $template_name, $args = array(), $template_path = '', $default_path = '' ) {
	$cache_key = sanitize_key( implode( '-', array( 'template', $template_name, $template_path, $default_path, Constants::get_constant( 'WC_VERSION' ) ) ) );
	$template  = (string) wp_cache_get( $cache_key, 'woocommerce' );

	if ( ! $template ) {
		$template = wc_locate_template( $template_name, $template_path, $default_path );

		// Don't cache the absolute path so that it can be shared between web servers with different paths.
		$cache_path = wc_tokenize_path( $template, wc_get_path_define_tokens() );

		wc_set_template_cache( $cache_key, $cache_path );
	} else {
		// Make sure that the absolute path to the template is resolved.
		$template = wc_untokenize_path( $template, wc_get_path_define_tokens() );
	}

	// Allow 3rd party plugin filter template file from their plugin.
	$filter_template = apply_filters( 'wc_get_template', $template, $template_name, $args, $template_path, $default_path );

	if ( $filter_template !== $template ) {
		if ( ! file_exists( $filter_template ) ) {
			/* translators: %s template */
			wc_doing_it_wrong( __FUNCTION__, sprintf( __( '%s does not exist.', 'woocommerce' ), '<code>' . $filter_template . '</code>' ), '2.1' );
			return;
		}
		$template = $filter_template;
	}

	$action_args = array(
		'template_name' => $template_name,
		'template_path' => $template_path,
		'located'       => $template,
		'args'          => $args,
	);

	if ( ! empty( $args ) && is_array( $args ) ) {
		if ( isset( $args['action_args'] ) ) {
			wc_doing_it_wrong(
				__FUNCTION__,
				__( 'action_args should not be overwritten when calling wc_get_template.', 'woocommerce' ),
				'3.6.0'
			);
			unset( $args['action_args'] );
		}
		extract( $args ); // @codingStandardsIgnoreLine
	}

	do_action( 'woocommerce_before_template_part', $action_args['template_name'], $action_args['template_path'], $action_args['located'], $action_args['args'] );

	include $action_args['located'];

	do_action( 'woocommerce_after_template_part', $action_args['template_name'], $action_args['template_path'], $action_args['located'], $action_args['args'] );
}

/**
 * Like wc_get_template, but returns the HTML instead of outputting.
 *
 * @see wc_get_template
 * @since 2.5.0
 * @param string $template_name Template name.
 * @param array  $args          Arguments. (default: array).
 * @param string $template_path Template path. (default: '').
 * @param string $default_path  Default path. (default: '').
 *
 * @return string
 */
function wc_get_template_html( $template_name, $args = array(), $template_path = '', $default_path = '' ) {
	ob_start();
	wc_get_template( $template_name, $args, $template_path, $default_path );
	return ob_get_clean();
}
/**
 * Locate a template and return the path for inclusion.
 *
 * This is the load order:
 *
 * yourtheme/$template_path/$template_name
 * yourtheme/$template_name
 * $default_path/$template_name
 *
 * @param string $template_name Template name.
 * @param string $template_path Template path. (default: '').
 * @param string $default_path  Default path. (default: '').
 * @return string
 */
function wc_locate_template( $template_name, $template_path = '', $default_path = '' ) {
	if ( ! $template_path ) {
		$template_path = WC()->template_path();
	}

	if ( ! $default_path ) {
		$default_path = WC()->plugin_path() . '/templates/';
	}

	// Look within passed path within the theme - this is priority.
	if ( false !== strpos( $template_name, 'product_cat' ) || false !== strpos( $template_name, 'product_tag' ) ) {
		$cs_template = str_replace( '_', '-', $template_name );
		$template    = locate_template(
			array(
				trailingslashit( $template_path ) . $cs_template,
				$cs_template,
			)
		);
	}

	if ( empty( $template ) ) {
		$template = locate_template(
			array(
				trailingslashit( $template_path ) . $template_name,
				$template_name,
			)
		);
	}

	// Get default template/.
	if ( ! $template || WC_TEMPLATE_DEBUG_MODE ) {
		if ( empty( $cs_template ) ) {
			$template = $default_path . $template_name;
		} else {
			$template = $default_path . $cs_template;
		}
	}

	// Return what we found.
	return apply_filters( 'woocommerce_locate_template', $template, $template_name, $template_path );
}

/**
 * Add a template to the template cache.
 *
 * @since 4.3.0
 * @param string $cache_key Object cache key.
 * @param string $template Located template.
 */
function wc_set_template_cache( $cache_key, $template ) {
	wp_cache_set( $cache_key, $template, 'woocommerce' );

	$cached_templates = wp_cache_get( 'cached_templates', 'woocommerce' );
	if ( is_array( $cached_templates ) ) {
		$cached_templates[] = $cache_key;
	} else {
		$cached_templates = array( $cache_key );
	}

	wp_cache_set( 'cached_templates', $cached_templates, 'woocommerce' );
}

/**
 * Clear the template cache.
 *
 * @since 4.3.0
 */
function wc_clear_template_cache() {
	$cached_templates = wp_cache_get( 'cached_templates', 'woocommerce' );
	if ( is_array( $cached_templates ) ) {
		foreach ( $cached_templates as $cache_key ) {
			wp_cache_delete( $cache_key, 'woocommerce' );
		}

		wp_cache_delete( 'cached_templates', 'woocommerce' );
	}
}

/**
 * Get Base Currency Code.
 *
 * @return string
 */
function get_woocommerce_currency() {
	return apply_filters( 'woocommerce_currency', get_option( 'woocommerce_currency' ) );
}

/**
 * Get full list of currency codes.
 *
 * Currency symbols and names should follow the Unicode CLDR recommendation (http://cldr.unicode.org/translation/currency-names)
 *
 * @return array
 */
function get_woocommerce_currencies() {
	static $currencies;

	if ( ! isset( $currencies ) ) {
		$currencies = array_unique(
			apply_filters(
				'woocommerce_currencies',
				array(
					'AED' => __( 'United Arab Emirates dirham', 'woocommerce' ),
					'AFN' => __( 'Afghan afghani', 'woocommerce' ),
					'ALL' => __( 'Albanian lek', 'woocommerce' ),
					'AMD' => __( 'Armenian dram', 'woocommerce' ),
					'ANG' => __( 'Netherlands Antillean guilder', 'woocommerce' ),
					'AOA' => __( 'Angolan kwanza', 'woocommerce' ),
					'ARS' => __( 'Argentine peso', 'woocommerce' ),
					'AUD' => __( 'Australian dollar', 'woocommerce' ),
					'AWG' => __( 'Aruban florin', 'woocommerce' ),
					'AZN' => __( 'Azerbaijani manat', 'woocommerce' ),
					'BAM' => __( 'Bosnia and Herzegovina convertible mark', 'woocommerce' ),
					'BBD' => __( 'Barbadian dollar', 'woocommerce' ),
					'BDT' => __( 'Bangladeshi taka', 'woocommerce' ),
					'BGN' => __( 'Bulgarian lev', 'woocommerce' ),
					'BHD' => __( 'Bahraini dinar', 'woocommerce' ),
					'BIF' => __( 'Burundian franc', 'woocommerce' ),
					'BMD' => __( 'Bermudian dollar', 'woocommerce' ),
					'BND' => __( 'Brunei dollar', 'woocommerce' ),
					'BOB' => __( 'Bolivian boliviano', 'woocommerce' ),
					'BRL' => __( 'Brazilian real', 'woocommerce' ),
					'BSD' => __( 'Bahamian dollar', 'woocommerce' ),
					'BTC' => __( 'Bitcoin', 'woocommerce' ),
					'BTN' => __( 'Bhutanese ngultrum', 'woocommerce' ),
					'BWP' => __( 'Botswana pula', 'woocommerce' ),
					'BYR' => __( 'Belarusian ruble (old)', 'woocommerce' ),
					'BYN' => __( 'Belarusian ruble', 'woocommerce' ),
					'BZD' => __( 'Belize dollar', 'woocommerce' ),
					'CAD' => __( 'Canadian dollar', 'woocommerce' ),
					'CDF' => __( 'Congolese franc', 'woocommerce' ),
					'CHF' => __( 'Swiss franc', 'woocommerce' ),
					'CLP' => __( 'Chilean peso', 'woocommerce' ),
					'CNY' => __( 'Chinese yuan', 'woocommerce' ),
					'COP' => __( 'Colombian peso', 'woocommerce' ),
					'CRC' => __( 'Costa Rican col&oacute;n', 'woocommerce' ),
					'CUC' => __( 'Cuban convertible peso', 'woocommerce' ),
					'CUP' => __( 'Cuban peso', 'woocommerce' ),
					'CVE' => __( 'Cape Verdean escudo', 'woocommerce' ),
					'CZK' => __( 'Czech koruna', 'woocommerce' ),
					'DJF' => __( 'Djiboutian franc', 'woocommerce' ),
					'DKK' => __( 'Danish krone', 'woocommerce' ),
					'DOP' => __( 'Dominican peso', 'woocommerce' ),
					'DZD' => __( 'Algerian dinar', 'woocommerce' ),
					'EGP' => __( 'Egyptian pound', 'woocommerce' ),
					'ERN' => __( 'Eritrean nakfa', 'woocommerce' ),
					'ETB' => __( 'Ethiopian birr', 'woocommerce' ),
					'EUR' => __( 'Euro', 'woocommerce' ),
					'FJD' => __( 'Fijian dollar', 'woocommerce' ),
					'FKP' => __( 'Falkland Islands pound', 'woocommerce' ),
					'GBP' => __( 'Pound sterling', 'woocommerce' ),
					'GEL' => __( 'Georgian lari', 'woocommerce' ),
					'GGP' => __( 'Guernsey pound', 'woocommerce' ),
					'GHS' => __( 'Ghana cedi', 'woocommerce' ),
					'GIP' => __( 'Gibraltar pound', 'woocommerce' ),
					'GMD' => __( 'Gambian dalasi', 'woocommerce' ),
					'GNF' => __( 'Guinean franc', 'woocommerce' ),
					'GTQ' => __( 'Guatemalan quetzal', 'woocommerce' ),
					'GYD' => __( 'Guyanese dollar', 'woocommerce' ),
					'HKD' => __( 'Hong Kong dollar', 'woocommerce' ),
					'HNL' => __( 'Honduran lempira', 'woocommerce' ),
					'HRK' => __( 'Croatian kuna', 'woocommerce' ),
					'HTG' => __( 'Haitian gourde', 'woocommerce' ),
					'HUF' => __( 'Hungarian forint', 'woocommerce' ),
					'IDR' => __( 'Indonesian rupiah', 'woocommerce' ),
					'ILS' => __( 'Israeli new shekel', 'woocommerce' ),
					'IMP' => __( 'Manx pound', 'woocommerce' ),
					'INR' => __( 'Indian rupee', 'woocommerce' ),
					'IQD' => __( 'Iraqi dinar', 'woocommerce' ),
					'IRR' => __( 'Iranian rial', 'woocommerce' ),
					'IRT' => __( 'Iranian toman', 'woocommerce' ),
					'ISK' => __( 'Icelandic kr&oacute;na', 'woocommerce' ),
					'JEP' => __( 'Jersey pound', 'woocommerce' ),
					'JMD' => __( 'Jamaican dollar', 'woocommerce' ),
					'JOD' => __( 'Jordanian dinar', 'woocommerce' ),
					'JPY' => __( 'Japanese yen', 'woocommerce' ),
					'KES' => __( 'Kenyan shilling', 'woocommerce' ),
					'KGS' => __( 'Kyrgyzstani som', 'woocommerce' ),
					'KHR' => __( 'Cambodian riel', 'woocommerce' ),
					'KMF' => __( 'Comorian franc', 'woocommerce' ),
					'KPW' => __( 'North Korean won', 'woocommerce' ),
					'KRW' => __( 'South Korean won', 'woocommerce' ),
					'KWD' => __( 'Kuwaiti dinar', 'woocommerce' ),
					'KYD' => __( 'Cayman Islands dollar', 'woocommerce' ),
					'KZT' => __( 'Kazakhstani tenge', 'woocommerce' ),
					'LAK' => __( 'Lao kip', 'woocommerce' ),
					'LBP' => __( 'Lebanese pound', 'woocommerce' ),
					'LKR' => __( 'Sri Lankan rupee', 'woocommerce' ),
					'LRD' => __( 'Liberian dollar', 'woocommerce' ),
					'LSL' => __( 'Lesotho loti', 'woocommerce' ),
					'LYD' => __( 'Libyan dinar', 'woocommerce' ),
					'MAD' => __( 'Moroccan dirham', 'woocommerce' ),
					'MDL' => __( 'Moldovan leu', 'woocommerce' ),
					'MGA' => __( 'Malagasy ariary', 'woocommerce' ),
					'MKD' => __( 'Macedonian denar', 'woocommerce' ),
					'MMK' => __( 'Burmese kyat', 'woocommerce' ),
					'MNT' => __( 'Mongolian t&ouml;gr&ouml;g', 'woocommerce' ),
					'MOP' => __( 'Macanese pataca', 'woocommerce' ),
					'MRU' => __( 'Mauritanian ouguiya', 'woocommerce' ),
					'MUR' => __( 'Mauritian rupee', 'woocommerce' ),
					'MVR' => __( 'Maldivian rufiyaa', 'woocommerce' ),
					'MWK' => __( 'Malawian kwacha', 'woocommerce' ),
					'MXN' => __( 'Mexican peso', 'woocommerce' ),
					'MYR' => __( 'Malaysian ringgit', 'woocommerce' ),
					'MZN' => __( 'Mozambican metical', 'woocommerce' ),
					'NAD' => __( 'Namibian dollar', 'woocommerce' ),
					'NGN' => __( 'Nigerian naira', 'woocommerce' ),
					'NIO' => __( 'Nicaraguan c&oacute;rdoba', 'woocommerce' ),
					'NOK' => __( 'Norwegian krone', 'woocommerce' ),
					'NPR' => __( 'Nepalese rupee', 'woocommerce' ),
					'NZD' => __( 'New Zealand dollar', 'woocommerce' ),
					'OMR' => __( 'Omani rial', 'woocommerce' ),
					'PAB' => __( 'Panamanian balboa', 'woocommerce' ),
					'PEN' => __( 'Sol', 'woocommerce' ),
					'PGK' => __( 'Papua New Guinean kina', 'woocommerce' ),
					'PHP' => __( 'Philippine peso', 'woocommerce' ),
					'PKR' => __( 'Pakistani rupee', 'woocommerce' ),
					'PLN' => __( 'Polish z&#x142;oty', 'woocommerce' ),
					'PRB' => __( 'Transnistrian ruble', 'woocommerce' ),
					'PYG' => __( 'Paraguayan guaran&iacute;', 'woocommerce' ),
					'QAR' => __( 'Qatari riyal', 'woocommerce' ),
					'RON' => __( 'Romanian leu', 'woocommerce' ),
					'RSD' => __( 'Serbian dinar', 'woocommerce' ),
					'RUB' => __( 'Russian ruble', 'woocommerce' ),
					'RWF' => __( 'Rwandan franc', 'woocommerce' ),
					'SAR' => __( 'Saudi riyal', 'woocommerce' ),
					'SBD' => __( 'Solomon Islands dollar', 'woocommerce' ),
					'SCR' => __( 'Seychellois rupee', 'woocommerce' ),
					'SDG' => __( 'Sudanese pound', 'woocommerce' ),
					'SEK' => __( 'Swedish krona', 'woocommerce' ),
					'SGD' => __( 'Singapore dollar', 'woocommerce' ),
					'SHP' => __( 'Saint Helena pound', 'woocommerce' ),
					'SLL' => __( 'Sierra Leonean leone', 'woocommerce' ),
					'SOS' => __( 'Somali shilling', 'woocommerce' ),
					'SRD' => __( 'Surinamese dollar', 'woocommerce' ),
					'SSP' => __( 'South Sudanese pound', 'woocommerce' ),
					'STN' => __( 'S&atilde;o Tom&eacute; and Pr&iacute;ncipe dobra', 'woocommerce' ),
					'SYP' => __( 'Syrian pound', 'woocommerce' ),
					'SZL' => __( 'Swazi lilangeni', 'woocommerce' ),
					'THB' => __( 'Thai baht', 'woocommerce' ),
					'TJS' => __( 'Tajikistani somoni', 'woocommerce' ),
					'TMT' => __( 'Turkmenistan manat', 'woocommerce' ),
					'TND' => __( 'Tunisian dinar', 'woocommerce' ),
					'TOP' => __( 'Tongan pa&#x2bb;anga', 'woocommerce' ),
					'TRY' => __( 'Turkish lira', 'woocommerce' ),
					'TTD' => __( 'Trinidad and Tobago dollar', 'woocommerce' ),
					'TWD' => __( 'New Taiwan dollar', 'woocommerce' ),
					'TZS' => __( 'Tanzanian shilling', 'woocommerce' ),
					'UAH' => __( 'Ukrainian hryvnia', 'woocommerce' ),
					'UGX' => __( 'Ugandan shilling', 'woocommerce' ),
					'USD' => __( 'United States (US) dollar', 'woocommerce' ),
					'UYU' => __( 'Uruguayan peso', 'woocommerce' ),
					'UZS' => __( 'Uzbekistani som', 'woocommerce' ),
					'VEF' => __( 'Venezuelan bol&iacute;var', 'woocommerce' ),
					'VES' => __( 'Bol&iacute;var soberano', 'woocommerce' ),
					'VND' => __( 'Vietnamese &#x111;&#x1ed3;ng', 'woocommerce' ),
					'VUV' => __( 'Vanuatu vatu', 'woocommerce' ),
					'WST' => __( 'Samoan t&#x101;l&#x101;', 'woocommerce' ),
					'XAF' => __( 'Central African CFA franc', 'woocommerce' ),
					'XCD' => __( 'East Caribbean dollar', 'woocommerce' ),
					'XOF' => __( 'West African CFA franc', 'woocommerce' ),
					'XPF' => __( 'CFP franc', 'woocommerce' ),
					'YER' => __( 'Yemeni rial', 'woocommerce' ),
					'ZAR' => __( 'South African rand', 'woocommerce' ),
					'ZMW' => __( 'Zambian kwacha', 'woocommerce' ),
				)
			)
		);
	}

	return $currencies;
}

/**
 * Get all available Currency symbols.
 *
 * Currency symbols and names should follow the Unicode CLDR recommendation (http://cldr.unicode.org/translation/currency-names)
 *
 * @since 4.1.0
 * @return array
 */
function get_woocommerce_currency_symbols() {

	$symbols = apply_filters(
		'woocommerce_currency_symbols',
		array(
			'AED' => '&#x62f;.&#x625;',
			'AFN' => '&#x60b;',
			'ALL' => 'L',
			'AMD' => 'AMD',
			'ANG' => '&fnof;',
			'AOA' => 'Kz',
			'ARS' => '&#36;',
			'AUD' => '&#36;',
			'AWG' => 'Afl.',
			'AZN' => 'AZN',
			'BAM' => 'KM',
			'BBD' => '&#36;',
			'BDT' => '&#2547;&nbsp;',
			'BGN' => '&#1083;&#1074;.',
			'BHD' => '.&#x62f;.&#x628;',
			'BIF' => 'Fr',
			'BMD' => '&#36;',
			'BND' => '&#36;',
			'BOB' => 'Bs.',
			'BRL' => '&#82;&#36;',
			'BSD' => '&#36;',
			'BTC' => '&#3647;',
			'BTN' => 'Nu.',
			'BWP' => 'P',
			'BYR' => 'Br',
			'BYN' => 'Br',
			'BZD' => '&#36;',
			'CAD' => '&#36;',
			'CDF' => 'Fr',
			'CHF' => '&#67;&#72;&#70;',
			'CLP' => '&#36;',
			'CNY' => '&yen;',
			'COP' => '&#36;',
			'CRC' => '&#x20a1;',
			'CUC' => '&#36;',
			'CUP' => '&#36;',
			'CVE' => '&#36;',
			'CZK' => '&#75;&#269;',
			'DJF' => 'Fr',
			'DKK' => 'DKK',
			'DOP' => 'RD&#36;',
			'DZD' => '&#x62f;.&#x62c;',
			'EGP' => 'EGP',
			'ERN' => 'Nfk',
			'ETB' => 'Br',
			'EUR' => '&euro;',
			'FJD' => '&#36;',
			'FKP' => '&pound;',
			'GBP' => '&pound;',
			'GEL' => '&#x20be;',
			'GGP' => '&pound;',
			'GHS' => '&#x20b5;',
			'GIP' => '&pound;',
			'GMD' => 'D',
			'GNF' => 'Fr',
			'GTQ' => 'Q',
			'GYD' => '&#36;',
			'HKD' => '&#36;',
			'HNL' => 'L',
			'HRK' => 'kn',
			'HTG' => 'G',
			'HUF' => '&#70;&#116;',
			'IDR' => 'Rp',
			'ILS' => '&#8362;',
			'IMP' => '&pound;',
			'INR' => '&#8377;',
			'IQD' => '&#x639;.&#x62f;',
			'IRR' => '&#xfdfc;',
			'IRT' => '&#x062A;&#x0648;&#x0645;&#x0627;&#x0646;',
			'ISK' => 'kr.',
			'JEP' => '&pound;',
			'JMD' => '&#36;',
			'JOD' => '&#x62f;.&#x627;',
			'JPY' => '&yen;',
			'KES' => 'KSh',
			'KGS' => '&#x441;&#x43e;&#x43c;',
			'KHR' => '&#x17db;',
			'KMF' => 'Fr',
			'KPW' => '&#x20a9;',
			'KRW' => '&#8361;',
			'KWD' => '&#x62f;.&#x643;',
			'KYD' => '&#36;',
			'KZT' => '&#8376;',
			'LAK' => '&#8365;',
			'LBP' => '&#x644;.&#x644;',
			'LKR' => '&#xdbb;&#xdd4;',
			'LRD' => '&#36;',
			'LSL' => 'L',
			'LYD' => '&#x644;.&#x62f;',
			'MAD' => '&#x62f;.&#x645;.',
			'MDL' => 'MDL',
			'MGA' => 'Ar',
			'MKD' => '&#x434;&#x435;&#x43d;',
			'MMK' => 'Ks',
			'MNT' => '&#x20ae;',
			'MOP' => 'P',
			'MRU' => 'UM',
			'MUR' => '&#x20a8;',
			'MVR' => '.&#x783;',
			'MWK' => 'MK',
			'MXN' => '&#36;',
			'MYR' => '&#82;&#77;',
			'MZN' => 'MT',
			'NAD' => 'N&#36;',
			'NGN' => '&#8358;',
			'NIO' => 'C&#36;',
			'NOK' => '&#107;&#114;',
			'NPR' => '&#8360;',
			'NZD' => '&#36;',
			'OMR' => '&#x631;.&#x639;.',
			'PAB' => 'B/.',
			'PEN' => 'S/',
			'PGK' => 'K',
			'PHP' => '&#8369;',
			'PKR' => '&#8360;',
			'PLN' => '&#122;&#322;',
			'PRB' => '&#x440;.',
			'PYG' => '&#8370;',
			'QAR' => '&#x631;.&#x642;',
			'RMB' => '&yen;',
			'RON' => 'lei',
			'RSD' => '&#1088;&#1089;&#1076;',
			'RUB' => '&#8381;',
			'RWF' => 'Fr',
			'SAR' => '&#x631;.&#x633;',
			'SBD' => '&#36;',
			'SCR' => '&#x20a8;',
			'SDG' => '&#x62c;.&#x633;.',
			'SEK' => '&#107;&#114;',
			'SGD' => '&#36;',
			'SHP' => '&pound;',
			'SLL' => 'Le',
			'SOS' => 'Sh',
			'SRD' => '&#36;',
			'SSP' => '&pound;',
			'STN' => 'Db',
			'SYP' => '&#x644;.&#x633;',
			'SZL' => 'L',
			'THB' => '&#3647;',
			'TJS' => '&#x405;&#x41c;',
			'TMT' => 'm',
			'TND' => '&#x62f;.&#x62a;',
			'TOP' => 'T&#36;',
			'TRY' => '&#8378;',
			'TTD' => '&#36;',
			'TWD' => '&#78;&#84;&#36;',
			'TZS' => 'Sh',
			'UAH' => '&#8372;',
			'UGX' => 'UGX',
			'USD' => '&#36;',
			'UYU' => '&#36;',
			'UZS' => 'UZS',
			'VEF' => 'Bs F',
			'VES' => 'Bs.S',
			'VND' => '&#8363;',
			'VUV' => 'Vt',
			'WST' => 'T',
			'XAF' => 'CFA',
			'XCD' => '&#36;',
			'XOF' => 'CFA',
			'XPF' => 'Fr',
			'YER' => '&#xfdfc;',
			'ZAR' => '&#82;',
			'ZMW' => 'ZK',
		)
	);

	return $symbols;
}

/**
 * Get Currency symbol.
 *
 * Currency symbols and names should follow the Unicode CLDR recommendation (http://cldr.unicode.org/translation/currency-names)
 *
 * @param string $currency Currency. (default: '').
 * @return string
 */
function get_woocommerce_currency_symbol( $currency = '' ) {
	if ( ! $currency ) {
		$currency = get_woocommerce_currency();
	}

	$symbols = get_woocommerce_currency_symbols();

	$currency_symbol = isset( $symbols[ $currency ] ) ? $symbols[ $currency ] : '';

	return apply_filters( 'woocommerce_currency_symbol', $currency_symbol, $currency );
}

/**
 * Send HTML emails from WooCommerce.
 *
 * @param mixed  $to          Receiver.
 * @param mixed  $subject     Subject.
 * @param mixed  $message     Message.
 * @param string $headers     Headers. (default: "Content-Type: text/html\r\n").
 * @param string $attachments Attachments. (default: "").
 * @return bool
 */
function wc_mail( $to, $subject, $message, $headers = "Content-Type: text/html\r\n", $attachments = '' ) {
	$mailer = WC()->mailer();

	return $mailer->send( $to, $subject, $message, $headers, $attachments );
}

/**
 * Return "theme support" values from the current theme, if set.
 *
 * @since  3.3.0
 * @param  string $prop Name of prop (or key::subkey for arrays of props) if you want a specific value. Leave blank to get all props as an array.
 * @param  mixed  $default Optional value to return if the theme does not declare support for a prop.
 * @return mixed  Value of prop(s).
 */
function wc_get_theme_support( $prop = '', $default = null ) {
	$theme_support = get_theme_support( 'woocommerce' );
	$theme_support = is_array( $theme_support ) ? $theme_support[0] : false;

	if ( ! $theme_support ) {
		return $default;
	}

	if ( $prop ) {
		$prop_stack = explode( '::', $prop );
		$prop_key   = array_shift( $prop_stack );

		if ( isset( $theme_support[ $prop_key ] ) ) {
			$value = $theme_support[ $prop_key ];

			if ( count( $prop_stack ) ) {
				foreach ( $prop_stack as $prop_key ) {
					if ( is_array( $value ) && isset( $value[ $prop_key ] ) ) {
						$value = $value[ $prop_key ];
					} else {
						$value = $default;
						break;
					}
				}
			}
		} else {
			$value = $default;
		}

		return $value;
	}

	return $theme_support;
}

/**
 * Get an image size by name or defined dimensions.
 *
 * The returned variable is filtered by woocommerce_get_image_size_{image_size} filter to
 * allow 3rd party customisation.
 *
 * Sizes defined by the theme take priority over settings. Settings are hidden when a theme
 * defines sizes.
 *
 * @param array|string $image_size Name of the image size to get, or an array of dimensions.
 * @return array Array of dimensions including width, height, and cropping mode. Cropping mode is 0 for no crop, and 1 for hard crop.
 */
function wc_get_image_size( $image_size ) {
	$cache_key = 'size-' . ( is_array( $image_size ) ? implode( '-', $image_size ) : $image_size );
	$size      = wp_cache_get( $cache_key, 'woocommerce' );

	if ( $size ) {
		return $size;
	}

	$size = array(
		'width'  => 600,
		'height' => 600,
		'crop'   => 1,
	);

	if ( is_array( $image_size ) ) {
		$size       = array(
			'width'  => isset( $image_size[0] ) ? absint( $image_size[0] ) : 600,
			'height' => isset( $image_size[1] ) ? absint( $image_size[1] ) : 600,
			'crop'   => isset( $image_size[2] ) ? absint( $image_size[2] ) : 1,
		);
		$image_size = $size['width'] . '_' . $size['height'];
	} else {
		$image_size = str_replace( 'woocommerce_', '', $image_size );

		// Legacy size mapping.
		if ( 'shop_single' === $image_size ) {
			$image_size = 'single';
		} elseif ( 'shop_catalog' === $image_size ) {
			$image_size = 'thumbnail';
		} elseif ( 'shop_thumbnail' === $image_size ) {
			$image_size = 'gallery_thumbnail';
		}

		if ( 'single' === $image_size ) {
			$size['width']  = absint( wc_get_theme_support( 'single_image_width', get_option( 'woocommerce_single_image_width', 600 ) ) );
			$size['height'] = '';
			$size['crop']   = 0;

		} elseif ( 'gallery_thumbnail' === $image_size ) {
			$size['width']  = absint( wc_get_theme_support( 'gallery_thumbnail_image_width', 100 ) );
			$size['height'] = $size['width'];
			$size['crop']   = 1;

		} elseif ( 'thumbnail' === $image_size ) {
			$size['width'] = absint( wc_get_theme_support( 'thumbnail_image_width', get_option( 'woocommerce_thumbnail_image_width', 300 ) ) );
			$cropping      = get_option( 'woocommerce_thumbnail_cropping', '1:1' );

			if ( 'uncropped' === $cropping ) {
				$size['height'] = '';
				$size['crop']   = 0;
			} elseif ( 'custom' === $cropping ) {
				$width          = max( 1, (float) get_option( 'woocommerce_thumbnail_cropping_custom_width', '4' ) );
				$height         = max( 1, (float) get_option( 'woocommerce_thumbnail_cropping_custom_height', '3' ) );
				$size['height'] = absint( NumberUtil::round( ( $size['width'] / $width ) * $height ) );
				$size['crop']   = 1;
			} else {
				$cropping_split = explode( ':', $cropping );
				$width          = max( 1, (float) current( $cropping_split ) );
				$height         = max( 1, (float) end( $cropping_split ) );
				$size['height'] = absint( NumberUtil::round( ( $size['width'] / $width ) * $height ) );
				$size['crop']   = 1;
			}
		}
	}

	$size = apply_filters( 'woocommerce_get_image_size_' . $image_size, $size );

	wp_cache_set( $cache_key, $size, 'woocommerce' );

	return $size;
}

/**
 * Queue some JavaScript code to be output in the footer.
 *
 * @param string $code Code.
 */
function wc_enqueue_js( $code ) {
	global $wc_queued_js;

	if ( empty( $wc_queued_js ) ) {
		$wc_queued_js = '';
	}

	$wc_queued_js .= "\n" . $code . "\n";
}

/**
 * Output any queued javascript code in the footer.
 */
function wc_print_js() {
	global $wc_queued_js;

	if ( ! empty( $wc_queued_js ) ) {
		// Sanitize.
		$wc_queued_js = wp_check_invalid_utf8( $wc_queued_js );
		$wc_queued_js = preg_replace( '/&#(x)?0*(?(1)27|39);?/i', "'", $wc_queued_js );
		$wc_queued_js = str_replace( "\r", '', $wc_queued_js );

		$js = "<!-- WooCommerce JavaScript -->\n<script type=\"text/javascript\">\njQuery(function($) { $wc_queued_js });\n</script>\n";

		/**
		 * Queued jsfilter.
		 *
		 * @since 2.6.0
		 * @param string $js JavaScript code.
		 */
		echo apply_filters( 'woocommerce_queued_js', $js ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped

		unset( $wc_queued_js );
	}
}

/**
 * Set a cookie - wrapper for setcookie using WP constants.
 *
 * @param  string  $name   Name of the cookie being set.
 * @param  string  $value  Value of the cookie.
 * @param  integer $expire Expiry of the cookie.
 * @param  bool    $secure Whether the cookie should be served only over https.
 * @param  bool    $httponly Whether the cookie is only accessible over HTTP, not scripting languages like JavaScript. @since 3.6.0.
 */
function wc_setcookie( $name, $value, $expire = 0, $secure = false, $httponly = false ) {
	if ( ! headers_sent() ) {
		setcookie( $name, $value, $expire, COOKIEPATH ? COOKIEPATH : '/', COOKIE_DOMAIN, $secure, apply_filters( 'woocommerce_cookie_httponly', $httponly, $name, $value, $expire, $secure ) );
	} elseif ( Constants::is_true( 'WP_DEBUG' ) ) {
		headers_sent( $file, $line );
		trigger_error( "{$name} cookie cannot be set - headers already sent by {$file} on line {$line}", E_USER_NOTICE ); // @codingStandardsIgnoreLine
	}
}

/**
 * Get the URL to the WooCommerce REST API.
 *
 * @since 2.1
 * @param string $path an endpoint to include in the URL.
 * @return string the URL.
 */
function get_woocommerce_api_url( $path ) {
	if ( Constants::is_defined( 'WC_API_REQUEST_VERSION' ) ) {
		$version = Constants::get_constant( 'WC_API_REQUEST_VERSION' );
	} else {
		$version = substr( WC_API::VERSION, 0, 1 );
	}

	$url = get_home_url( null, "wc-api/v{$version}/", is_ssl() ? 'https' : 'http' );

	if ( ! empty( $path ) && is_string( $path ) ) {
		$url .= ltrim( $path, '/' );
	}

	return $url;
}

/**
 * Get a log file path.
 *
 * @since 2.2
 *
 * @param string $handle name.
 * @return string the log file path.
 */
function wc_get_log_file_path( $handle ) {
	return WC_Log_Handler_File::get_log_file_path( $handle );
}

/**
 * Get a log file name.
 *
 * @since 3.3
 *
 * @param string $handle Name.
 * @return string The log file name.
 */
function wc_get_log_file_name( $handle ) {
	return WC_Log_Handler_File::get_log_file_name( $handle );
}

/**
 * Recursively get page children.
 *
 * @param  int $page_id Page ID.
 * @return int[]
 */
function wc_get_page_children( $page_id ) {
	$page_ids = get_posts(
		array(
			'post_parent' => $page_id,
			'post_type'   => 'page',
			'numberposts' => -1, // @codingStandardsIgnoreLine
			'post_status' => 'any',
			'fields'      => 'ids',
		)
	);

	if ( ! empty( $page_ids ) ) {
		foreach ( $page_ids as $page_id ) {
			$page_ids = array_merge( $page_ids, wc_get_page_children( $page_id ) );
		}
	}

	return $page_ids;
}

/**
 * Flushes rewrite rules when the shop page (or it's children) gets saved.
 */
function flush_rewrite_rules_on_shop_page_save() {
	$screen    = get_current_screen();
	$screen_id = $screen ? $screen->id : '';

	// Check if this is the edit page.
	if ( 'page' !== $screen_id ) {
		return;
	}

	// Check if page is edited.
	if ( empty( $_GET['post'] ) || empty( $_GET['action'] ) || ( isset( $_GET['action'] ) && 'edit' !== $_GET['action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		return;
	}

	$post_id      = intval( $_GET['post'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
	$shop_page_id = wc_get_page_id( 'shop' );

	if ( $shop_page_id === $post_id || in_array( $post_id, wc_get_page_children( $shop_page_id ), true ) ) {
		do_action( 'woocommerce_flush_rewrite_rules' );
	}
}
add_action( 'admin_footer', 'flush_rewrite_rules_on_shop_page_save' );

/**
 * Various rewrite rule fixes.
 *
 * @since 2.2
 * @param array $rules Rules.
 * @return array
 */
function wc_fix_rewrite_rules( $rules ) {
	global $wp_rewrite;

	$permalinks = wc_get_permalink_structure();

	// Fix the rewrite rules when the product permalink have %product_cat% flag.
	if ( preg_match( '`/(.+)(/%product_cat%)`', $permalinks['product_rewrite_slug'], $matches ) ) {
		foreach ( $rules as $rule => $rewrite ) {
			if ( preg_match( '`^' . preg_quote( $matches[1], '`' ) . '/\(`', $rule ) && preg_match( '/^(index\.php\?product_cat)(?!(.*product))/', $rewrite ) ) {
				unset( $rules[ $rule ] );
			}
		}
	}

	// If the shop page is used as the base, we need to handle shop page subpages to avoid 404s.
	if ( ! $permalinks['use_verbose_page_rules'] ) {
		return $rules;
	}

	$shop_page_id = wc_get_page_id( 'shop' );
	if ( $shop_page_id ) {
		$page_rewrite_rules = array();
		$subpages           = wc_get_page_children( $shop_page_id );

		// Subpage rules.
		foreach ( $subpages as $subpage ) {
			$uri                                = get_page_uri( $subpage );
			$page_rewrite_rules[ $uri . '/?$' ] = 'index.php?pagename=' . $uri;
			$wp_generated_rewrite_rules         = $wp_rewrite->generate_rewrite_rules( $uri, EP_PAGES, true, true, false, false );
			foreach ( $wp_generated_rewrite_rules as $key => $value ) {
				$wp_generated_rewrite_rules[ $key ] = $value . '&pagename=' . $uri;
			}
			$page_rewrite_rules = array_merge( $page_rewrite_rules, $wp_generated_rewrite_rules );
		}

		// Merge with rules.
		$rules = array_merge( $page_rewrite_rules, $rules );
	}

	return $rules;
}
add_filter( 'rewrite_rules_array', 'wc_fix_rewrite_rules' );

/**
 * Prevent product attachment links from breaking when using complex rewrite structures.
 *
 * @param  string $link    Link.
 * @param  int    $post_id Post ID.
 * @return string
 */
function wc_fix_product_attachment_link( $link, $post_id ) {
	$parent_type = get_post_type( wp_get_post_parent_id( $post_id ) );
	if ( 'product' === $parent_type || 'product_variation' === $parent_type ) {
		$link = home_url( '/?attachment_id=' . $post_id );
	}
	return $link;
}
add_filter( 'attachment_link', 'wc_fix_product_attachment_link', 10, 2 );

/**
 * Protect downloads from ms-files.php in multisite.
 *
 * @param string $rewrite rewrite rules.
 * @return string
 */
function wc_ms_protect_download_rewite_rules( $rewrite ) {
	if ( ! is_multisite() || 'redirect' === get_option( 'woocommerce_file_download_method' ) ) {
		return $rewrite;
	}

	$rule  = "\n# WooCommerce Rules - Protect Files from ms-files.php\n\n";
	$rule .= "<IfModule mod_rewrite.c>\n";
	$rule .= "RewriteEngine On\n";
	$rule .= "RewriteCond %{QUERY_STRING} file=woocommerce_uploads/ [NC]\n";
	$rule .= "RewriteRule /ms-files.php$ - [F]\n";
	$rule .= "</IfModule>\n\n";

	return $rule . $rewrite;
}
add_filter( 'mod_rewrite_rules', 'wc_ms_protect_download_rewite_rules' );

/**
 * Formats a string in the format COUNTRY:STATE into an array.
 *
 * @since 2.3.0
 * @param  string $country_string Country string.
 * @return array
 */
function wc_format_country_state_string( $country_string ) {
	if ( strstr( $country_string, ':' ) ) {
		list( $country, $state ) = explode( ':', $country_string );
	} else {
		$country = $country_string;
		$state   = '';
	}
	return array(
		'country' => $country,
		'state'   => $state,
	);
}

/**
 * Get the store's base location.
 *
 * @since 2.3.0
 * @return array
 */
function wc_get_base_location() {
	$default = apply_filters( 'woocommerce_get_base_location', get_option( 'woocommerce_default_country', 'US:CA' ) );

	return wc_format_country_state_string( $default );
}

/**
 * Get the customer's default location.
 *
 * Filtered, and set to base location or left blank. If cache-busting,
 * this should only be used when 'location' is set in the querystring.
 *
 * @since 2.3.0
 * @return array
 */
function wc_get_customer_default_location() {
	$set_default_location_to = get_option( 'woocommerce_default_customer_address', 'base' );
	$default_location        = '' === $set_default_location_to ? '' : get_option( 'woocommerce_default_country', 'US:CA' );
	$location                = wc_format_country_state_string( apply_filters( 'woocommerce_customer_default_location', $default_location ) );

	// Geolocation takes priority if used and if geolocation is possible.
	if ( 'geolocation' === $set_default_location_to || 'geolocation_ajax' === $set_default_location_to ) {
		$ua = wc_get_user_agent();

		// Exclude common bots from geolocation by user agent.
		if ( ! stristr( $ua, 'bot' ) && ! stristr( $ua, 'spider' ) && ! stristr( $ua, 'crawl' ) ) {
			$geolocation = WC_Geolocation::geolocate_ip( '', true, false );

			if ( ! empty( $geolocation['country'] ) ) {
				$location = $geolocation;
			}
		}
	}

	// Once we have a location, ensure it's valid, otherwise fallback to a valid location.
	$allowed_country_codes = WC()->countries->get_allowed_countries();

	if ( ! empty( $location['country'] ) && ! array_key_exists( $location['country'], $allowed_country_codes ) ) {
		$location['country'] = current( array_keys( $allowed_country_codes ) );
		$location['state']   = '';
	}

	return apply_filters( 'woocommerce_customer_default_location_array', $location );
}

/**
 * Get user agent string.
 *
 * @since  3.0.0
 * @return string
 */
function wc_get_user_agent() {
	return isset( $_SERVER['HTTP_USER_AGENT'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; // @codingStandardsIgnoreLine
}

/**
 * Generate a rand hash.
 *
 * @since  2.4.0
 * @return string
 */
function wc_rand_hash() {
	if ( ! function_exists( 'openssl_random_pseudo_bytes' ) ) {
		return sha1( wp_rand() );
	}

	return bin2hex( openssl_random_pseudo_bytes( 20 ) ); // @codingStandardsIgnoreLine
}

/**
 * WC API - Hash.
 *
 * @since  2.4.0
 * @param  string $data Message to be hashed.
 * @return string
 */
function wc_api_hash( $data ) {
	return hash_hmac( 'sha256', $data, 'wc-api' );
}

/**
 * Find all possible combinations of values from the input array and return in a logical order.
 *
 * @since 2.5.0
 * @param array $input Input.
 * @return array
 */
function wc_array_cartesian( $input ) {
	$input   = array_filter( $input );
	$results = array();
	$indexes = array();
	$index   = 0;

	// Generate indexes from keys and values so we have a logical sort order.
	foreach ( $input as $key => $values ) {
		foreach ( $values as $value ) {
			$indexes[ $key ][ $value ] = $index++;
		}
	}

	// Loop over the 2D array of indexes and generate all combinations.
	foreach ( $indexes as $key => $values ) {
		// When result is empty, fill with the values of the first looped array.
		if ( empty( $results ) ) {
			foreach ( $values as $value ) {
				$results[] = array( $key => $value );
			}
		} else {
			// Second and subsequent input sub-array merging.
			foreach ( $results as $result_key => $result ) {
				foreach ( $values as $value ) {
					// If the key is not set, we can set it.
					if ( ! isset( $results[ $result_key ][ $key ] ) ) {
						$results[ $result_key ][ $key ] = $value;
					} else {
						// If the key is set, we can add a new combination to the results array.
						$new_combination         = $results[ $result_key ];
						$new_combination[ $key ] = $value;
						$results[]               = $new_combination;
					}
				}
			}
		}
	}

	// Sort the indexes.
	arsort( $results );

	// Convert indexes back to values.
	foreach ( $results as $result_key => $result ) {
		$converted_values = array();

		// Sort the values.
		arsort( $results[ $result_key ] );

		// Convert the values.
		foreach ( $results[ $result_key ] as $key => $value ) {
			$converted_values[ $key ] = array_search( $value, $indexes[ $key ], true );
		}

		$results[ $result_key ] = $converted_values;
	}

	return $results;
}

/**
 * Run a MySQL transaction query, if supported.
 *
 * @since 2.5.0
 * @param string $type Types: start (default), commit, rollback.
 * @param bool   $force use of transactions.
 */
function wc_transaction_query( $type = 'start', $force = false ) {
	global $wpdb;

	$wpdb->hide_errors();

	wc_maybe_define_constant( 'WC_USE_TRANSACTIONS', true );

	if ( Constants::is_true( 'WC_USE_TRANSACTIONS' ) || $force ) {
		switch ( $type ) {
			case 'commit':
				$wpdb->query( 'COMMIT' );
				break;
			case 'rollback':
				$wpdb->query( 'ROLLBACK' );
				break;
			default:
				$wpdb->query( 'START TRANSACTION' );
				break;
		}
	}
}

/**
 * Gets the url to the cart page.
 *
 * @since  2.5.0
 *
 * @return string Url to cart page
 */
function wc_get_cart_url() {
	return apply_filters( 'woocommerce_get_cart_url', wc_get_page_permalink( 'cart' ) );
}

/**
 * Gets the url to the checkout page.
 *
 * @since  2.5.0
 *
 * @return string Url to checkout page
 */
function wc_get_checkout_url() {
	$checkout_url = wc_get_page_permalink( 'checkout' );
	if ( $checkout_url ) {
		// Force SSL if needed.
		if ( is_ssl() || 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) ) {
			$checkout_url = str_replace( 'http:', 'https:', $checkout_url );
		}
	}

	return apply_filters( 'woocommerce_get_checkout_url', $checkout_url );
}

/**
 * Register a shipping method.
 *
 * @since 1.5.7
 * @param string|object $shipping_method class name (string) or a class object.
 */
function woocommerce_register_shipping_method( $shipping_method ) {
	WC()->shipping()->register_shipping_method( $shipping_method );
}

if ( ! function_exists( 'wc_get_shipping_zone' ) ) {
	/**
	 * Get the shipping zone matching a given package from the cart.
	 *
	 * @since  2.6.0
	 * @uses   WC_Shipping_Zones::get_zone_matching_package
	 * @param  array $package Shipping package.
	 * @return WC_Shipping_Zone
	 */
	function wc_get_shipping_zone( $package ) {
		return WC_Shipping_Zones::get_zone_matching_package( $package );
	}
}

/**
 * Get a nice name for credit card providers.
 *
 * @since  2.6.0
 * @param  string $type Provider Slug/Type.
 * @return string
 */
function wc_get_credit_card_type_label( $type ) {
	// Normalize.
	$type = strtolower( $type );
	$type = str_replace( '-', ' ', $type );
	$type = str_replace( '_', ' ', $type );

	$labels = apply_filters(
		'woocommerce_credit_card_type_labels',
		array(
			'mastercard'       => __( 'MasterCard', 'woocommerce' ),
			'visa'             => __( 'Visa', 'woocommerce' ),
			'discover'         => __( 'Discover', 'woocommerce' ),
			'american express' => __( 'American Express', 'woocommerce' ),
			'diners'           => __( 'Diners', 'woocommerce' ),
			'jcb'              => __( 'JCB', 'woocommerce' ),
		)
	);

	return apply_filters( 'woocommerce_get_credit_card_type_label', ( array_key_exists( $type, $labels ) ? $labels[ $type ] : ucfirst( $type ) ) );
}

/**
 * Outputs a "back" link so admin screens can easily jump back a page.
 *
 * @param string $label Title of the page to return to.
 * @param string $url   URL of the page to return to.
 */
function wc_back_link( $label, $url ) {
	echo '<small class="wc-admin-breadcrumb"><a href="' . esc_url( $url ) . '" aria-label="' . esc_attr( $label ) . '">&#x2934;</a></small>';
}

/**
 * Display a WooCommerce help tip.
 *
 * @since  2.5.0
 *
 * @param  string $tip        Help tip text.
 * @param  bool   $allow_html Allow sanitized HTML if true or escape.
 * @return string
 */
function wc_help_tip( $tip, $allow_html = false ) {
	if ( $allow_html ) {
		$tip = wc_sanitize_tooltip( $tip );
	} else {
		$tip = esc_attr( $tip );
	}

	return '<span class="woocommerce-help-tip" data-tip="' . $tip . '"></span>';
}

/**
 * Return a list of potential postcodes for wildcard searching.
 *
 * @since 2.6.0
 * @param  string $postcode Postcode.
 * @param  string $country  Country to format postcode for matching.
 * @return string[]
 */
function wc_get_wildcard_postcodes( $postcode, $country = '' ) {
	$formatted_postcode = wc_format_postcode( $postcode, $country );
	$length             = function_exists( 'mb_strlen' ) ? mb_strlen( $formatted_postcode ) : strlen( $formatted_postcode );
	$postcodes          = array(
		$postcode,
		$formatted_postcode,
		$formatted_postcode . '*',
	);

	for ( $i = 0; $i < $length; $i ++ ) {
		$postcodes[] = ( function_exists( 'mb_substr' ) ? mb_substr( $formatted_postcode, 0, ( $i + 1 ) * -1 ) : substr( $formatted_postcode, 0, ( $i + 1 ) * -1 ) ) . '*';
	}

	return $postcodes;
}

/**
 * Used by shipping zones and taxes to compare a given $postcode to stored
 * postcodes to find matches for numerical ranges, and wildcards.
 *
 * @since 2.6.0
 * @param string $postcode           Postcode you want to match against stored postcodes.
 * @param array  $objects            Array of postcode objects from Database.
 * @param string $object_id_key      DB column name for the ID.
 * @param string $object_compare_key DB column name for the value.
 * @param string $country            Country from which this postcode belongs. Allows for formatting.
 * @return array Array of matching object ID and matching values.
 */
function wc_postcode_location_matcher( $postcode, $objects, $object_id_key, $object_compare_key, $country = '' ) {
	$postcode           = wc_normalize_postcode( $postcode );
	$wildcard_postcodes = array_map( 'wc_clean', wc_get_wildcard_postcodes( $postcode, $country ) );
	$matches            = array();

	foreach ( $objects as $object ) {
		$object_id       = $object->$object_id_key;
		$compare_against = $object->$object_compare_key;

		// Handle postcodes containing ranges.
		if ( strstr( $compare_against, '...' ) ) {
			$range = array_map( 'trim', explode( '...', $compare_against ) );

			if ( 2 !== count( $range ) ) {
				continue;
			}

			list( $min, $max ) = $range;

			// If the postcode is non-numeric, make it numeric.
			if ( ! is_numeric( $min ) || ! is_numeric( $max ) ) {
				$compare = wc_make_numeric_postcode( $postcode );
				$min     = str_pad( wc_make_numeric_postcode( $min ), strlen( $compare ), '0' );
				$max     = str_pad( wc_make_numeric_postcode( $max ), strlen( $compare ), '0' );
			} else {
				$compare = $postcode;
			}

			if ( $compare >= $min && $compare <= $max ) {
				$matches[ $object_id ]   = isset( $matches[ $object_id ] ) ? $matches[ $object_id ] : array();
				$matches[ $object_id ][] = $compare_against;
			}
		} elseif ( in_array( $compare_against, $wildcard_postcodes, true ) ) {
			// Wildcard and standard comparison.
			$matches[ $object_id ]   = isset( $matches[ $object_id ] ) ? $matches[ $object_id ] : array();
			$matches[ $object_id ][] = $compare_against;
		}
	}

	return $matches;
}

/**
 * Gets number of shipping methods currently enabled. Used to identify if
 * shipping is configured.
 *
 * @since  2.6.0
 * @param  bool $include_legacy Count legacy shipping methods too.
 * @param  bool $enabled_only   Whether non-legacy shipping methods should be
 *                              restricted to enabled ones. It doesn't affect
 *                              legacy shipping methods. @since 4.3.0.
 * @return int
 */
function wc_get_shipping_method_count( $include_legacy = false, $enabled_only = false ) {
	global $wpdb;

	$transient_name    = $include_legacy ? 'wc_shipping_method_count_legacy' : 'wc_shipping_method_count';
	$transient_version = WC_Cache_Helper::get_transient_version( 'shipping' );
	$transient_value   = get_transient( $transient_name );

	if ( isset( $transient_value['value'], $transient_value['version'] ) && $transient_value['version'] === $transient_version ) {
		return absint( $transient_value['value'] );
	}

	$where_clause = $enabled_only ? 'WHERE is_enabled=1' : '';
	$method_count = absint( $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}woocommerce_shipping_zone_methods ${where_clause}" ) );

	if ( $include_legacy ) {
		// Count activated methods that don't support shipping zones.
		$methods = WC()->shipping()->get_shipping_methods();

		foreach ( $methods as $method ) {
			if ( isset( $method->enabled ) && 'yes' === $method->enabled && ! $method->supports( 'shipping-zones' ) ) {
				$method_count++;
			}
		}
	}

	$transient_value = array(
		'version' => $transient_version,
		'value'   => $method_count,
	);

	set_transient( $transient_name, $transient_value, DAY_IN_SECONDS * 30 );

	return $method_count;
}

/**
 * Wrapper for set_time_limit to see if it is enabled.
 *
 * @since 2.6.0
 * @param int $limit Time limit.
 */
function wc_set_time_limit( $limit = 0 ) {
	if ( function_exists( 'set_time_limit' ) && false === strpos( ini_get( 'disable_functions' ), 'set_time_limit' ) && ! ini_get( 'safe_mode' ) ) { // phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.safe_modeDeprecatedRemoved
		@set_time_limit( $limit ); // @codingStandardsIgnoreLine
	}
}

/**
 * Wrapper for nocache_headers which also disables page caching.
 *
 * @since 3.2.4
 */
function wc_nocache_headers() {
	WC_Cache_Helper::set_nocache_constants();
	nocache_headers();
}

/**
 * Used to sort products attributes with uasort.
 *
 * @since 2.6.0
 * @param array $a First attribute to compare.
 * @param array $b Second attribute to compare.
 * @return int
 */
function wc_product_attribute_uasort_comparison( $a, $b ) {
	$a_position = is_null( $a ) ? null : $a['position'];
	$b_position = is_null( $b ) ? null : $b['position'];
	return wc_uasort_comparison( $a_position, $b_position );
}

/**
 * Used to sort shipping zone methods with uasort.
 *
 * @since 3.0.0
 * @param array $a First shipping zone method to compare.
 * @param array $b Second shipping zone method to compare.
 * @return int
 */
function wc_shipping_zone_method_order_uasort_comparison( $a, $b ) {
	return wc_uasort_comparison( $a->method_order, $b->method_order );
}

/**
 * User to sort checkout fields based on priority with uasort.
 *
 * @since 3.5.1
 * @param array $a First field to compare.
 * @param array $b Second field to compare.
 * @return int
 */
function wc_checkout_fields_uasort_comparison( $a, $b ) {
	/*
	 * We are not guaranteed to get a priority
	 * setting. So don't compare if they don't
	 * exist.
	 */
	if ( ! isset( $a['priority'], $b['priority'] ) ) {
		return 0;
	}

	return wc_uasort_comparison( $a['priority'], $b['priority'] );
}

/**
 * User to sort two values with ausort.
 *
 * @since 3.5.1
 * @param int $a First value to compare.
 * @param int $b Second value to compare.
 * @return int
 */
function wc_uasort_comparison( $a, $b ) {
	if ( $a === $b ) {
		return 0;
	}
	return ( $a < $b ) ? -1 : 1;
}

/**
 * Sort values based on ascii, usefull for special chars in strings.
 *
 * @param string $a First value.
 * @param string $b Second value.
 * @return int
 */
function wc_ascii_uasort_comparison( $a, $b ) {
	// 'setlocale' is required for compatibility with PHP 8.
	// Without it, 'iconv' will return '?'s instead of transliterated characters.
	$prev_locale = setlocale( LC_CTYPE, 0 );
	setlocale( LC_ALL, 'C.UTF-8' );

	// phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
	if ( function_exists( 'iconv' ) && defined( 'ICONV_IMPL' ) && @strcasecmp( ICONV_IMPL, 'unknown' ) !== 0 ) {
		$a = @iconv( 'UTF-8', 'ASCII//TRANSLIT//IGNORE', $a );
		$b = @iconv( 'UTF-8', 'ASCII//TRANSLIT//IGNORE', $b );
	}
	// phpcs:enable WordPress.PHP.NoSilencedErrors.Discouraged

	setlocale( LC_ALL, $prev_locale );
	return strcmp( $a, $b );
}

/**
 * Sort array according to current locale rules and maintaining index association.
 * By default tries to use Collator from PHP Internationalization Functions if available.
 * If PHP Collator class doesn't exists it fallback to removing accepts from a array
 * and by sorting with `uasort( $data, 'strcmp' )` giving support for ASCII values.
 *
 * @since 4.6.0
 * @param array  $data   List of values to sort.
 * @param string $locale Locale.
 * @return array
 */
function wc_asort_by_locale( &$data, $locale = '' ) {
	// Use Collator if PHP Internationalization Functions (php-intl) is available.
	if ( class_exists( 'Collator' ) ) {
		try {
			$locale   = $locale ? $locale : get_locale();
			$collator = new Collator( $locale );
			$collator->asort( $data, Collator::SORT_STRING );
			return $data;
		} catch ( IntlException $e ) {
			/*
			 * Just skip if some error got caused.
			 * It may be caused in installations that doesn't include ICU TZData.
			 */
			if ( Constants::is_true( 'WP_DEBUG' ) ) {
				error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
					sprintf(
						'An unexpected error occurred while trying to use PHP Intl Collator class, it may be caused by an incorrect installation of PHP Intl and ICU, and could be fixed by reinstallaing PHP Intl, see more details about PHP Intl installation: %1$s. Error message: %2$s',
						'https://www.php.net/manual/en/intl.installation.php',
						$e->getMessage()
					)
				);
			}
		}
	}

	$raw_data = $data;

	array_walk(
		$data,
		function ( &$value ) {
			$value = remove_accents( html_entity_decode( $value ) );
		}
	);

	uasort( $data, 'strcmp' );

	foreach ( $data as $key => $val ) {
		$data[ $key ] = $raw_data[ $key ];
	}

	return $data;
}

/**
 * Get rounding mode for internal tax calculations.
 *
 * @since 3.2.4
 * @return int
 */
function wc_get_tax_rounding_mode() {
	$constant = WC_TAX_ROUNDING_MODE;

	if ( 'auto' === $constant ) {
		return 'yes' === get_option( 'woocommerce_prices_include_tax', 'no' ) ? PHP_ROUND_HALF_DOWN : PHP_ROUND_HALF_UP;
	}

	return intval( $constant );
}

/**
 * Get rounding precision for internal WC calculations.
 * Will increase the precision of wc_get_price_decimals by 2 decimals, unless WC_ROUNDING_PRECISION is set to a higher number.
 *
 * @since 2.6.3
 * @return int
 */
function wc_get_rounding_precision() {
	$precision = wc_get_price_decimals() + 2;
	if ( absint( WC_ROUNDING_PRECISION ) > $precision ) {
		$precision = absint( WC_ROUNDING_PRECISION );
	}
	return $precision;
}

/**
 * Add precision to a number and return a number.
 *
 * @since  3.2.0
 * @param  float $value Number to add precision to.
 * @param  bool  $round If should round after adding precision.
 * @return int|float
 */
function wc_add_number_precision( $value, $round = true ) {
	$cent_precision = pow( 10, wc_get_price_decimals() );
	$value          = $value * $cent_precision;
	return $round ? NumberUtil::round( $value, wc_get_rounding_precision() - wc_get_price_decimals() ) : $value;
}

/**
 * Remove precision from a number and return a float.
 *
 * @since  3.2.0
 * @param  float $value Number to add precision to.
 * @return float
 */
function wc_remove_number_precision( $value ) {
	$cent_precision = pow( 10, wc_get_price_decimals() );
	return $value / $cent_precision;
}

/**
 * Add precision to an array of number and return an array of int.
 *
 * @since  3.2.0
 * @param  array $value Number to add precision to.
 * @param  bool  $round Should we round after adding precision?.
 * @return int|array
 */
function wc_add_number_precision_deep( $value, $round = true ) {
	if ( ! is_array( $value ) ) {
		return wc_add_number_precision( $value, $round );
	}

	foreach ( $value as $key => $sub_value ) {
		$value[ $key ] = wc_add_number_precision_deep( $sub_value, $round );
	}

	return $value;
}

/**
 * Remove precision from an array of number and return an array of int.
 *
 * @since  3.2.0
 * @param  array $value Number to add precision to.
 * @return int|array
 */
function wc_remove_number_precision_deep( $value ) {
	if ( ! is_array( $value ) ) {
		return wc_remove_number_precision( $value );
	}

	foreach ( $value as $key => $sub_value ) {
		$value[ $key ] = wc_remove_number_precision_deep( $sub_value );
	}

	return $value;
}

/**
 * Get a shared logger instance.
 *
 * Use the woocommerce_logging_class filter to change the logging class. You may provide one of the following:
 *     - a class name which will be instantiated as `new $class` with no arguments
 *     - an instance which will be used directly as the logger
 * In either case, the class or instance *must* implement WC_Logger_Interface.
 *
 * @see WC_Logger_Interface
 *
 * @return WC_Logger
 */
function wc_get_logger() {
	static $logger = null;

	$class = apply_filters( 'woocommerce_logging_class', 'WC_Logger' );

	if ( null !== $logger && is_string( $class ) && is_a( $logger, $class ) ) {
		return $logger;
	}

	$implements = class_implements( $class );

	if ( is_array( $implements ) && in_array( 'WC_Logger_Interface', $implements, true ) ) {
		$logger = is_object( $class ) ? $class : new $class();
	} else {
		wc_doing_it_wrong(
			__FUNCTION__,
			sprintf(
				/* translators: 1: class name 2: woocommerce_logging_class 3: WC_Logger_Interface */
				__( 'The class %1$s provided by %2$s filter must implement %3$s.', 'woocommerce' ),
				'<code>' . esc_html( is_object( $class ) ? get_class( $class ) : $class ) . '</code>',
				'<code>woocommerce_logging_class</code>',
				'<code>WC_Logger_Interface</code>'
			),
			'3.0'
		);

		$logger = is_a( $logger, 'WC_Logger' ) ? $logger : new WC_Logger();
	}

	return $logger;
}

/**
 * Trigger logging cleanup using the logging class.
 *
 * @since 3.4.0
 */
function wc_cleanup_logs() {
	$logger = wc_get_logger();

	if ( is_callable( array( $logger, 'clear_expired_logs' ) ) ) {
		$logger->clear_expired_logs();
	}
}
add_action( 'woocommerce_cleanup_logs', 'wc_cleanup_logs' );

/**
 * Prints human-readable information about a variable.
 *
 * Some server environments block some debugging functions. This function provides a safe way to
 * turn an expression into a printable, readable form without calling blocked functions.
 *
 * @since 3.0
 *
 * @param mixed $expression The expression to be printed.
 * @param bool  $return     Optional. Default false. Set to true to return the human-readable string.
 * @return string|bool False if expression could not be printed. True if the expression was printed.
 *     If $return is true, a string representation will be returned.
 */
function wc_print_r( $expression, $return = false ) {
	$alternatives = array(
		array(
			'func' => 'print_r',
			'args' => array( $expression, true ),
		),
		array(
			'func' => 'var_export',
			'args' => array( $expression, true ),
		),
		array(
			'func' => 'json_encode',
			'args' => array( $expression ),
		),
		array(
			'func' => 'serialize',
			'args' => array( $expression ),
		),
	);

	$alternatives = apply_filters( 'woocommerce_print_r_alternatives', $alternatives, $expression );

	foreach ( $alternatives as $alternative ) {
		if ( function_exists( $alternative['func'] ) ) {
			$res = $alternative['func']( ...$alternative['args'] );
			if ( $return ) {
				return $res; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			}

			echo $res; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			return true;
		}
	}

	return false;
}

/**
 * Registers the default log handler.
 *
 * @since 3.0
 * @param array $handlers Handlers.
 * @return array
 */
function wc_register_default_log_handler( $handlers ) {
	$handler_class = Constants::get_constant( 'WC_LOG_HANDLER' );
	if ( ! class_exists( $handler_class ) ) {
		$handler_class = WC_Log_Handler_File::class;
	}

	array_push( $handlers, new $handler_class() );

	return $handlers;
}
add_filter( 'woocommerce_register_log_handlers', 'wc_register_default_log_handler' );

/**
 * Based on wp_list_pluck, this calls a method instead of returning a property.
 *
 * @since 3.0.0
 * @param array      $list              List of objects or arrays.
 * @param int|string $callback_or_field Callback method from the object to place instead of the entire object.
 * @param int|string $index_key         Optional. Field from the object to use as keys for the new array.
 *                                      Default null.
 * @return array Array of values.
 */
function wc_list_pluck( $list, $callback_or_field, $index_key = null ) {
	// Use wp_list_pluck if this isn't a callback.
	$first_el = current( $list );
	if ( ! is_object( $first_el ) || ! is_callable( array( $first_el, $callback_or_field ) ) ) {
		return wp_list_pluck( $list, $callback_or_field, $index_key );
	}
	if ( ! $index_key ) {
		/*
		 * This is simple. Could at some point wrap array_column()
		 * if we knew we had an array of arrays.
		 */
		foreach ( $list as $key => $value ) {
			$list[ $key ] = $value->{$callback_or_field}();
		}
		return $list;
	}

	/*
	 * When index_key is not set for a particular item, push the value
	 * to the end of the stack. This is how array_column() behaves.
	 */
	$newlist = array();
	foreach ( $list as $value ) {
		// Get index. @since 3.2.0 this supports a callback.
		if ( is_callable( array( $value, $index_key ) ) ) {
			$newlist[ $value->{$index_key}() ] = $value->{$callback_or_field}();
		} elseif ( isset( $value->$index_key ) ) {
			$newlist[ $value->$index_key ] = $value->{$callback_or_field}();
		} else {
			$newlist[] = $value->{$callback_or_field}();
		}
	}
	return $newlist;
}

/**
 * Get permalink settings for things like products and taxonomies.
 *
 * As of 3.3.0, the permalink settings are stored to the option instead of
 * being blank and inheritting from the locale. This speeds up page loading
 * times by negating the need to switch locales on each page load.
 *
 * This is more inline with WP core behavior which does not localize slugs.
 *
 * @since  3.0.0
 * @return array
 */
function wc_get_permalink_structure() {
	$saved_permalinks = (array) get_option( 'woocommerce_permalinks', array() );
	$permalinks       = wp_parse_args(
		array_filter( $saved_permalinks ),
		array(
			'product_base'           => _x( 'product', 'slug', 'woocommerce' ),
			'category_base'          => _x( 'product-category', 'slug', 'woocommerce' ),
			'tag_base'               => _x( 'product-tag', 'slug', 'woocommerce' ),
			'attribute_base'         => '',
			'use_verbose_page_rules' => false,
		)
	);

	if ( $saved_permalinks !== $permalinks ) {
		update_option( 'woocommerce_permalinks', $permalinks );
	}

	$permalinks['product_rewrite_slug']   = untrailingslashit( $permalinks['product_base'] );
	$permalinks['category_rewrite_slug']  = untrailingslashit( $permalinks['category_base'] );
	$permalinks['tag_rewrite_slug']       = untrailingslashit( $permalinks['tag_base'] );
	$permalinks['attribute_rewrite_slug'] = untrailingslashit( $permalinks['attribute_base'] );

	return $permalinks;
}

/**
 * Switch WooCommerce to site language.
 *
 * @since 3.1.0
 */
function wc_switch_to_site_locale() {
	if ( function_exists( 'switch_to_locale' ) ) {
		switch_to_locale( get_locale() );

		// Filter on plugin_locale so load_plugin_textdomain loads the correct locale.
		add_filter( 'plugin_locale', 'get_locale' );

		// Init WC locale.
		WC()->load_plugin_textdomain();
	}
}

/**
 * Switch WooCommerce language to original.
 *
 * @since 3.1.0
 */
function wc_restore_locale() {
	if ( function_exists( 'restore_previous_locale' ) ) {
		restore_previous_locale();

		// Remove filter.
		remove_filter( 'plugin_locale', 'get_locale' );

		// Init WC locale.
		WC()->load_plugin_textdomain();
	}
}

/**
 * Convert plaintext phone number to clickable phone number.
 *
 * Remove formatting and allow "+".
 * Example and specs: https://developer.mozilla.org/en/docs/Web/HTML/Element/a#Creating_a_phone_link
 *
 * @since 3.1.0
 *
 * @param string $phone Content to convert phone number.
 * @return string Content with converted phone number.
 */
function wc_make_phone_clickable( $phone ) {
	$number = trim( preg_replace( '/[^\d|\+]/', '', $phone ) );

	return $number ? '<a href="tel:' . esc_attr( $number ) . '">' . esc_html( $phone ) . '</a>' : '';
}

/**
 * Get an item of post data if set, otherwise return a default value.
 *
 * @since  3.0.9
 * @param  string $key     Meta key.
 * @param  string $default Default value.
 * @return mixed Value sanitized by wc_clean.
 */
function wc_get_post_data_by_key( $key, $default = '' ) {
	// phpcs:ignore WordPress.Security.ValidatedSanitizedInput, WordPress.Security.NonceVerification.Missing
	return wc_clean( wp_unslash( wc_get_var( $_POST[ $key ], $default ) ) );
}

/**
 * Get data if set, otherwise return a default value or null. Prevents notices when data is not set.
 *
 * @since  3.2.0
 * @param  mixed  $var     Variable.
 * @param  string $default Default value.
 * @return mixed
 */
function wc_get_var( &$var, $default = null ) {
	return isset( $var ) ? $var : $default;
}

/**
 * Read in WooCommerce headers when reading plugin headers.
 *
 * @since 3.2.0
 * @param array $headers Headers.
 * @return array
 */
function wc_enable_wc_plugin_headers( $headers ) {
	if ( ! class_exists( 'WC_Plugin_Updates' ) ) {
		include_once dirname( __FILE__ ) . '/admin/plugin-updates/class-wc-plugin-updates.php';
	}

	// WC requires at least - allows developers to define which version of WooCommerce the plugin requires to run.
	$headers[] = WC_Plugin_Updates::VERSION_REQUIRED_HEADER;

	// WC tested up to - allows developers  to define which version of WooCommerce they have tested up to.
	$headers[] = WC_Plugin_Updates::VERSION_TESTED_HEADER;

	// Woo - This is used in WooCommerce extensions and is picked up by the helper.
	$headers[] = 'Woo';

	return $headers;
}
add_filter( 'extra_theme_headers', 'wc_enable_wc_plugin_headers' );
add_filter( 'extra_plugin_headers', 'wc_enable_wc_plugin_headers' );

/**
 * Prevent auto-updating the WooCommerce plugin on major releases if there are untested extensions active.
 *
 * @since 3.2.0
 * @param  bool   $should_update If should update.
 * @param  object $plugin        Plugin data.
 * @return bool
 */
function wc_prevent_dangerous_auto_updates( $should_update, $plugin ) {
	if ( ! isset( $plugin->plugin, $plugin->new_version ) ) {
		return $should_update;
	}

	if ( 'woocommerce/woocommerce.php' !== $plugin->plugin ) {
		return $should_update;
	}

	if ( ! class_exists( 'WC_Plugin_Updates' ) ) {
		include_once dirname( __FILE__ ) . '/admin/plugin-updates/class-wc-plugin-updates.php';
	}

	$new_version    = wc_clean( $plugin->new_version );
	$plugin_updates = new WC_Plugin_Updates();
	$version_type   = Constants::get_constant( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE' );
	if ( ! is_string( $version_type ) ) {
		$version_type = 'none';
	}
	$untested_plugins = $plugin_updates->get_untested_plugins( $new_version, $version_type );
	if ( ! empty( $untested_plugins ) ) {
		return false;
	}

	return $should_update;
}
add_filter( 'auto_update_plugin', 'wc_prevent_dangerous_auto_updates', 99, 2 );

/**
 * Delete expired transients.
 *
 * Deletes all expired transients. The multi-table delete syntax is used.
 * to delete the transient record from table a, and the corresponding.
 * transient_timeout record from table b.
 *
 * Based on code inside core's upgrade_network() function.
 *
 * @since 3.2.0
 * @return int Number of transients that were cleared.
 */
function wc_delete_expired_transients() {
	global $wpdb;

	// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
	$sql  = "DELETE a, b FROM $wpdb->options a, $wpdb->options b
		WHERE a.option_name LIKE %s
		AND a.option_name NOT LIKE %s
		AND b.option_name = CONCAT( '_transient_timeout_', SUBSTRING( a.option_name, 12 ) )
		AND b.option_value < %d";
	$rows = $wpdb->query( $wpdb->prepare( $sql, $wpdb->esc_like( '_transient_' ) . '%', $wpdb->esc_like( '_transient_timeout_' ) . '%', time() ) );

	$sql   = "DELETE a, b FROM $wpdb->options a, $wpdb->options b
		WHERE a.option_name LIKE %s
		AND a.option_name NOT LIKE %s
		AND b.option_name = CONCAT( '_site_transient_timeout_', SUBSTRING( a.option_name, 17 ) )
		AND b.option_value < %d";
	$rows2 = $wpdb->query( $wpdb->prepare( $sql, $wpdb->esc_like( '_site_transient_' ) . '%', $wpdb->esc_like( '_site_transient_timeout_' ) . '%', time() ) );
	// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared

	return absint( $rows + $rows2 );
}
add_action( 'woocommerce_installed', 'wc_delete_expired_transients' );

/**
 * Make a URL relative, if possible.
 *
 * @since 3.2.0
 * @param string $url URL to make relative.
 * @return string
 */
function wc_get_relative_url( $url ) {
	return wc_is_external_resource( $url ) ? $url : str_replace( array( 'http://', 'https://' ), '//', $url );
}

/**
 * See if a resource is remote.
 *
 * @since 3.2.0
 * @param string $url URL to check.
 * @return bool
 */
function wc_is_external_resource( $url ) {
	$wp_base = str_replace( array( 'http://', 'https://' ), '//', get_home_url( null, '/', 'http' ) );

	return strstr( $url, '://' ) && ! strstr( $url, $wp_base );
}

/**
 * See if theme/s is activate or not.
 *
 * @since 3.3.0
 * @param string|array $theme Theme name or array of theme names to check.
 * @return boolean
 */
function wc_is_active_theme( $theme ) {
	return is_array( $theme ) ? in_array( get_template(), $theme, true ) : get_template() === $theme;
}

/**
 * Is the site using a default WP theme?
 *
 * @return boolean
 */
function wc_is_wp_default_theme_active() {
	return wc_is_active_theme(
		array(
			'twentytwentyone',
			'twentytwenty',
			'twentynineteen',
			'twentyseventeen',
			'twentysixteen',
			'twentyfifteen',
			'twentyfourteen',
			'twentythirteen',
			'twentyeleven',
			'twentytwelve',
			'twentyten',
		)
	);
}

/**
 * Cleans up session data - cron callback.
 *
 * @since 3.3.0
 */
function wc_cleanup_session_data() {
	$session_class = apply_filters( 'woocommerce_session_handler', 'WC_Session_Handler' );
	$session       = new $session_class();

	if ( is_callable( array( $session, 'cleanup_sessions' ) ) ) {
		$session->cleanup_sessions();
	}
}
add_action( 'woocommerce_cleanup_sessions', 'wc_cleanup_session_data' );

/**
 * Convert a decimal (e.g. 3.5) to a fraction (e.g. 7/2).
 * From: https://www.designedbyaturtle.co.uk/2015/converting-a-decimal-to-a-fraction-in-php/
 *
 * @param float $decimal the decimal number.
 * @return array|bool a 1/2 would be [1, 2] array (this can be imploded with '/' to form a string).
 */
function wc_decimal_to_fraction( $decimal ) {
	if ( 0 > $decimal || ! is_numeric( $decimal ) ) {
		// Negative digits need to be passed in as positive numbers and prefixed as negative once the response is imploded.
		return false;
	}

	if ( 0 === $decimal ) {
		return array( 0, 1 );
	}

	$tolerance   = 1.e-4;
	$numerator   = 1;
	$h2          = 0;
	$denominator = 0;
	$k2          = 1;
	$b           = 1 / $decimal;

	do {
		$b           = 1 / $b;
		$a           = floor( $b );
		$aux         = $numerator;
		$numerator   = $a * $numerator + $h2;
		$h2          = $aux;
		$aux         = $denominator;
		$denominator = $a * $denominator + $k2;
		$k2          = $aux;
		$b           = $b - $a;
	} while ( abs( $decimal - $numerator / $denominator ) > $decimal * $tolerance );

	return array( $numerator, $denominator );
}

/**
 * Round discount.
 *
 * @param  double $value Amount to round.
 * @param  int    $precision DP to round.
 * @return float
 */
function wc_round_discount( $value, $precision ) {
	if ( version_compare( PHP_VERSION, '5.3.0', '>=' ) ) {
		return NumberUtil::round( $value, $precision, WC_DISCOUNT_ROUNDING_MODE ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctionParameters.round_modeFound
	}

	if ( PHP_ROUND_HALF_DOWN === WC_DISCOUNT_ROUNDING_MODE ) {
		return wc_legacy_round_half_down( $value, $precision );
	}

	return NumberUtil::round( $value, $precision );
}

/**
 * Return the html selected attribute if stringified $value is found in array of stringified $options
 * or if stringified $value is the same as scalar stringified $options.
 *
 * @param string|int       $value   Value to find within options.
 * @param string|int|array $options Options to go through when looking for value.
 * @return string
 */
function wc_selected( $value, $options ) {
	if ( is_array( $options ) ) {
		$options = array_map( 'strval', $options );
		return selected( in_array( (string) $value, $options, true ), true, false );
	}

	return selected( $value, $options, false );
}

/**
 * Retrieves the MySQL server version. Based on $wpdb.
 *
 * @since 3.4.1
 * @return array Vesion information.
 */
function wc_get_server_database_version() {
	global $wpdb;

	if ( empty( $wpdb->is_mysql ) ) {
		return array(
			'string' => '',
			'number' => '',
		);
	}

	// phpcs:disable WordPress.DB.RestrictedFunctions, PHPCompatibility.Extensions.RemovedExtensions.mysql_DeprecatedRemoved
	if ( $wpdb->use_mysqli ) {
		$server_info = mysqli_get_server_info( $wpdb->dbh );
	} else {
		$server_info = mysql_get_server_info( $wpdb->dbh );
	}
	// phpcs:enable WordPress.DB.RestrictedFunctions, PHPCompatibility.Extensions.RemovedExtensions.mysql_DeprecatedRemoved

	return array(
		'string' => $server_info,
		'number' => preg_replace( '/([^\d.]+).*/', '', $server_info ),
	);
}

/**
 * Initialize and load the cart functionality.
 *
 * @since 3.6.4
 * @return void
 */
function wc_load_cart() {
	if ( ! did_action( 'before_woocommerce_init' ) || doing_action( 'before_woocommerce_init' ) ) {
		/* translators: 1: wc_load_cart 2: woocommerce_init */
		wc_doing_it_wrong( __FUNCTION__, sprintf( __( '%1$s should not be called before the %2$s action.', 'woocommerce' ), 'wc_load_cart', 'woocommerce_init' ), '3.7' );
		return;
	}

	// Ensure dependencies are loaded in all contexts.
	include_once WC_ABSPATH . 'includes/wc-cart-functions.php';
	include_once WC_ABSPATH . 'includes/wc-notice-functions.php';

	WC()->initialize_session();
	WC()->initialize_cart();
}

/**
 * Test whether the context of execution comes from async action scheduler.
 *
 * @since 4.0.0
 * @return bool
 */
function wc_is_running_from_async_action_scheduler() {
	// phpcs:ignore WordPress.Security.NonceVerification.Recommended
	return isset( $_REQUEST['action'] ) && 'as_async_request_queue_runner' === $_REQUEST['action'];
}

/**
 * Polyfill for wp_cache_get_multiple for WP versions before 5.5.
 *
 * @param array  $keys   Array of keys to get from group.
 * @param string $group  Optional. Where the cache contents are grouped. Default empty.
 * @param bool   $force  Optional. Whether to force an update of the local cache from the persistent
 *                            cache. Default false.
 * @return array|bool Array of values.
 */
function wc_cache_get_multiple( $keys, $group = '', $force = false ) {
	if ( function_exists( 'wp_cache_get_multiple' ) ) {
		return wp_cache_get_multiple( $keys, $group, $force );
	}
	$values = array();
	foreach ( $keys as $key ) {
		$values[ $key ] = wp_cache_get( $key, $group, $force );
	}
	return $values;
}
class-wc-template-loader.php000064400000045156151336044760012072 0ustar00<?php
/**
 * Template Loader
 *
 * @package WooCommerce\Classes
 */

defined( 'ABSPATH' ) || exit;

/**
 * Template loader class.
 */
class WC_Template_Loader {

	/**
	 * Store the shop page ID.
	 *
	 * @var integer
	 */
	private static $shop_page_id = 0;

	/**
	 * Store whether we're processing a product inside the_content filter.
	 *
	 * @var boolean
	 */
	private static $in_content_filter = false;

	/**
	 * Is WooCommerce support defined?
	 *
	 * @var boolean
	 */
	private static $theme_support = false;

	/**
	 * Hook in methods.
	 */
	public static function init() {
		self::$theme_support = current_theme_supports( 'woocommerce' );
		self::$shop_page_id  = wc_get_page_id( 'shop' );

		// Supported themes.
		if ( self::$theme_support ) {
			add_filter( 'template_include', array( __CLASS__, 'template_loader' ) );
			add_filter( 'comments_template', array( __CLASS__, 'comments_template_loader' ) );
		} else {
			// Unsupported themes.
			add_action( 'template_redirect', array( __CLASS__, 'unsupported_theme_init' ) );
		}
	}

	/**
	 * Load a template.
	 *
	 * Handles template usage so that we can use our own templates instead of the theme's.
	 *
	 * Templates are in the 'templates' folder. WooCommerce looks for theme
	 * overrides in /theme/woocommerce/ by default.
	 *
	 * For beginners, it also looks for a woocommerce.php template first. If the user adds
	 * this to the theme (containing a woocommerce() inside) this will be used for all
	 * WooCommerce templates.
	 *
	 * @param string $template Template to load.
	 * @return string
	 */
	public static function template_loader( $template ) {
		if ( is_embed() ) {
			return $template;
		}

		$default_file = self::get_template_loader_default_file();

		if ( $default_file ) {
			/**
			 * Filter hook to choose which files to find before WooCommerce does it's own logic.
			 *
			 * @since 3.0.0
			 * @var array
			 */
			$search_files = self::get_template_loader_files( $default_file );
			$template     = locate_template( $search_files );

			if ( ! $template || WC_TEMPLATE_DEBUG_MODE ) {
				if ( false !== strpos( $default_file, 'product_cat' ) || false !== strpos( $default_file, 'product_tag' ) ) {
					$cs_template = str_replace( '_', '-', $default_file );
					$template    = WC()->plugin_path() . '/templates/' . $cs_template;
				} else {
					$template = WC()->plugin_path() . '/templates/' . $default_file;
				}
			}
		}

		return $template;
	}

	/**
	 * Checks whether a block template with that name exists.
	 *
	 * @since  5.5.0
	 * @param string $template_name Template to check.
	 * @return boolean
	 */
	private static function has_block_template( $template_name ) {
		if ( ! $template_name ) {
			return false;
		}

		return is_readable(
			get_stylesheet_directory() . '/block-templates/' . $template_name . '.html'
		);
	}

	/**
	 * Get the default filename for a template except if a block template with
	 * the same name exists.
	 *
	 * @since  3.0.0
	 * @since  5.5.0 If a block template with the same name exists, return an
	 * empty string.
	 * @return string
	 */
	private static function get_template_loader_default_file() {
		if (
			is_singular( 'product' ) &&
			! self::has_block_template( 'single-product' )
		) {
			$default_file = 'single-product.php';
		} elseif ( is_product_taxonomy() ) {
			$object = get_queried_object();

			if ( is_tax( 'product_cat' ) || is_tax( 'product_tag' ) ) {
				if ( self::has_block_template( 'taxonomy-' . $object->taxonomy ) ) {
					$default_file = '';
				} else {
					$default_file = 'taxonomy-' . $object->taxonomy . '.php';
				}
			} elseif ( ! self::has_block_template( 'archive-product' ) ) {
				$default_file = 'archive-product.php';
			}
		} elseif (
			( is_post_type_archive( 'product' ) || is_page( wc_get_page_id( 'shop' ) ) ) &&
			! self::has_block_template( 'archive-product' )
		) {
			$default_file = self::$theme_support ? 'archive-product.php' : '';
		} else {
			$default_file = '';
		}
		return $default_file;
	}

	/**
	 * Get an array of filenames to search for a given template.
	 *
	 * @since  3.0.0
	 * @param  string $default_file The default file name.
	 * @return string[]
	 */
	private static function get_template_loader_files( $default_file ) {
		$templates   = apply_filters( 'woocommerce_template_loader_files', array(), $default_file );
		$templates[] = 'woocommerce.php';

		if ( is_page_template() ) {
			$page_template = get_page_template_slug();

			if ( $page_template ) {
				$validated_file = validate_file( $page_template );
				if ( 0 === $validated_file ) {
					$templates[] = $page_template;
				} else {
					error_log( "WooCommerce: Unable to validate template path: \"$page_template\". Error Code: $validated_file." ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
				}
			}
		}

		if ( is_singular( 'product' ) ) {
			$object       = get_queried_object();
			$name_decoded = urldecode( $object->post_name );
			if ( $name_decoded !== $object->post_name ) {
				$templates[] = "single-product-{$name_decoded}.php";
			}
			$templates[] = "single-product-{$object->post_name}.php";
		}

		if ( is_product_taxonomy() ) {
			$object = get_queried_object();

			$templates[] = 'taxonomy-' . $object->taxonomy . '-' . $object->slug . '.php';
			$templates[] = WC()->template_path() . 'taxonomy-' . $object->taxonomy . '-' . $object->slug . '.php';
			$templates[] = 'taxonomy-' . $object->taxonomy . '.php';
			$templates[] = WC()->template_path() . 'taxonomy-' . $object->taxonomy . '.php';

			if ( is_tax( 'product_cat' ) || is_tax( 'product_tag' ) ) {
				$cs_taxonomy = str_replace( '_', '-', $object->taxonomy );
				$cs_default  = str_replace( '_', '-', $default_file );
				$templates[] = 'taxonomy-' . $object->taxonomy . '-' . $object->slug . '.php';
				$templates[] = WC()->template_path() . 'taxonomy-' . $cs_taxonomy . '-' . $object->slug . '.php';
				$templates[] = 'taxonomy-' . $object->taxonomy . '.php';
				$templates[] = WC()->template_path() . 'taxonomy-' . $cs_taxonomy . '.php';
				$templates[] = $cs_default;
			}
		}

		$templates[] = $default_file;
		if ( isset( $cs_default ) ) {
			$templates[] = WC()->template_path() . $cs_default;
		}
		$templates[] = WC()->template_path() . $default_file;

		return array_unique( $templates );
	}

	/**
	 * Load comments template.
	 *
	 * @param string $template template to load.
	 * @return string
	 */
	public static function comments_template_loader( $template ) {
		if ( get_post_type() !== 'product' ) {
			return $template;
		}

		$check_dirs = array(
			trailingslashit( get_stylesheet_directory() ) . WC()->template_path(),
			trailingslashit( get_template_directory() ) . WC()->template_path(),
			trailingslashit( get_stylesheet_directory() ),
			trailingslashit( get_template_directory() ),
			trailingslashit( WC()->plugin_path() ) . 'templates/',
		);

		if ( WC_TEMPLATE_DEBUG_MODE ) {
			$check_dirs = array( array_pop( $check_dirs ) );
		}

		foreach ( $check_dirs as $dir ) {
			if ( file_exists( trailingslashit( $dir ) . 'single-product-reviews.php' ) ) {
				return trailingslashit( $dir ) . 'single-product-reviews.php';
			}
		}
	}

	/**
	 * Unsupported theme compatibility methods.
	 */

	/**
	 * Hook in methods to enhance the unsupported theme experience on pages.
	 *
	 * @since 3.3.0
	 */
	public static function unsupported_theme_init() {
		if ( 0 < self::$shop_page_id ) {
			if ( is_product_taxonomy() ) {
				self::unsupported_theme_tax_archive_init();
			} elseif ( is_product() ) {
				self::unsupported_theme_product_page_init();
			} else {
				self::unsupported_theme_shop_page_init();
			}
		}
	}

	/**
	 * Hook in methods to enhance the unsupported theme experience on the Shop page.
	 *
	 * @since 3.3.0
	 */
	private static function unsupported_theme_shop_page_init() {
		add_filter( 'the_content', array( __CLASS__, 'unsupported_theme_shop_content_filter' ), 10 );
		add_filter( 'the_title', array( __CLASS__, 'unsupported_theme_title_filter' ), 10, 2 );
		add_filter( 'comments_number', array( __CLASS__, 'unsupported_theme_comments_number_filter' ) );
	}

	/**
	 * Hook in methods to enhance the unsupported theme experience on Product pages.
	 *
	 * @since 3.3.0
	 */
	private static function unsupported_theme_product_page_init() {
		add_filter( 'the_content', array( __CLASS__, 'unsupported_theme_product_content_filter' ), 10 );
		add_filter( 'post_thumbnail_html', array( __CLASS__, 'unsupported_theme_single_featured_image_filter' ) );
		add_filter( 'woocommerce_product_tabs', array( __CLASS__, 'unsupported_theme_remove_review_tab' ) );
		remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper', 10 );
		remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end', 10 );
		add_theme_support( 'wc-product-gallery-zoom' );
		add_theme_support( 'wc-product-gallery-lightbox' );
		add_theme_support( 'wc-product-gallery-slider' );
	}

	/**
	 * Enhance the unsupported theme experience on Product Category and Attribute pages by rendering
	 * those pages using the single template and shortcode-based content. To do this we make a dummy
	 * post and set a shortcode as the post content. This approach is adapted from bbPress.
	 *
	 * @since 3.3.0
	 */
	private static function unsupported_theme_tax_archive_init() {
		global $wp_query, $post;

		$queried_object = get_queried_object();
		$args           = self::get_current_shop_view_args();
		$shortcode_args = array(
			'page'     => $args->page,
			'columns'  => $args->columns,
			'rows'     => $args->rows,
			'orderby'  => '',
			'order'    => '',
			'paginate' => true,
			'cache'    => false,
		);

		if ( is_product_category() ) {
			$shortcode_args['category'] = sanitize_title( $queried_object->slug );
		} elseif ( taxonomy_is_product_attribute( $queried_object->taxonomy ) ) {
			$shortcode_args['attribute'] = sanitize_title( $queried_object->taxonomy );
			$shortcode_args['terms']     = sanitize_title( $queried_object->slug );
		} elseif ( is_product_tag() ) {
			$shortcode_args['tag'] = sanitize_title( $queried_object->slug );
		} else {
			// Default theme archive for all other taxonomies.
			return;
		}

		// Description handling.
		if ( ! empty( $queried_object->description ) && ( empty( $_GET['product-page'] ) || 1 === absint( $_GET['product-page'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$prefix = '<div class="term-description">' . wc_format_content( wp_kses_post( $queried_object->description ) ) . '</div>';
		} else {
			$prefix = '';
		}

		add_filter( 'woocommerce_shortcode_products_query', array( __CLASS__, 'unsupported_archive_layered_nav_compatibility' ) );
		$shortcode = new WC_Shortcode_Products( $shortcode_args );
		remove_filter( 'woocommerce_shortcode_products_query', array( __CLASS__, 'unsupported_archive_layered_nav_compatibility' ) );
		$shop_page = get_post( self::$shop_page_id );

		$dummy_post_properties = array(
			'ID'                    => 0,
			'post_status'           => 'publish',
			'post_author'           => $shop_page->post_author,
			'post_parent'           => 0,
			'post_type'             => 'page',
			'post_date'             => $shop_page->post_date,
			'post_date_gmt'         => $shop_page->post_date_gmt,
			'post_modified'         => $shop_page->post_modified,
			'post_modified_gmt'     => $shop_page->post_modified_gmt,
			'post_content'          => $prefix . $shortcode->get_content(),
			'post_title'            => wc_clean( $queried_object->name ),
			'post_excerpt'          => '',
			'post_content_filtered' => '',
			'post_mime_type'        => '',
			'post_password'         => '',
			'post_name'             => $queried_object->slug,
			'guid'                  => '',
			'menu_order'            => 0,
			'pinged'                => '',
			'to_ping'               => '',
			'ping_status'           => '',
			'comment_status'        => 'closed',
			'comment_count'         => 0,
			'filter'                => 'raw',
		);

		// Set the $post global.
		$post = new WP_Post( (object) $dummy_post_properties ); // @codingStandardsIgnoreLine.

		// Copy the new post global into the main $wp_query.
		$wp_query->post  = $post;
		$wp_query->posts = array( $post );

		// Prevent comments form from appearing.
		$wp_query->post_count    = 1;
		$wp_query->is_404        = false;
		$wp_query->is_page       = true;
		$wp_query->is_single     = true;
		$wp_query->is_archive    = false;
		$wp_query->is_tax        = true;
		$wp_query->max_num_pages = 0;

		// Prepare everything for rendering.
		setup_postdata( $post );
		remove_all_filters( 'the_content' );
		remove_all_filters( 'the_excerpt' );
		add_filter( 'template_include', array( __CLASS__, 'force_single_template_filter' ) );
	}

	/**
	 * Add layered nav args to WP_Query args generated by the 'products' shortcode.
	 *
	 * @since 3.3.4
	 * @param array $query WP_Query args.
	 * @return array
	 */
	public static function unsupported_archive_layered_nav_compatibility( $query ) {
		foreach ( WC()->query->get_layered_nav_chosen_attributes() as $taxonomy => $data ) {
			$query['tax_query'][] = array(
				'taxonomy'         => $taxonomy,
				'field'            => 'slug',
				'terms'            => $data['terms'],
				'operator'         => 'and' === $data['query_type'] ? 'AND' : 'IN',
				'include_children' => false,
			);
		}
		return $query;
	}

	/**
	 * Force the loading of one of the single templates instead of whatever template was about to be loaded.
	 *
	 * @since 3.3.0
	 * @param string $template Path to template.
	 * @return string
	 */
	public static function force_single_template_filter( $template ) {
		$possible_templates = array(
			'page',
			'single',
			'singular',
			'index',
		);

		foreach ( $possible_templates as $possible_template ) {
			$path = get_query_template( $possible_template );
			if ( $path ) {
				return $path;
			}
		}

		return $template;
	}

	/**
	 * Get information about the current shop page view.
	 *
	 * @since 3.3.0
	 * @return array
	 */
	private static function get_current_shop_view_args() {
		return (object) array(
			'page'    => absint( max( 1, absint( get_query_var( 'paged' ) ) ) ),
			'columns' => wc_get_default_products_per_row(),
			'rows'    => wc_get_default_product_rows_per_page(),
		);
	}

	/**
	 * Filter the title and insert WooCommerce content on the shop page.
	 *
	 * For non-WC themes, this will setup the main shop page to be shortcode based to improve default appearance.
	 *
	 * @since 3.3.0
	 * @param string $title Existing title.
	 * @param int    $id ID of the post being filtered.
	 * @return string
	 */
	public static function unsupported_theme_title_filter( $title, $id ) {
		if ( self::$theme_support || ! $id !== self::$shop_page_id ) {
			return $title;
		}

		if ( is_page( self::$shop_page_id ) || ( is_home() && 'page' === get_option( 'show_on_front' ) && absint( get_option( 'page_on_front' ) ) === self::$shop_page_id ) ) {
			$args         = self::get_current_shop_view_args();
			$title_suffix = array();

			if ( $args->page > 1 ) {
				/* translators: %d: Page number. */
				$title_suffix[] = sprintf( esc_html__( 'Page %d', 'woocommerce' ), $args->page );
			}

			if ( $title_suffix ) {
				$title = $title . ' &ndash; ' . implode( ', ', $title_suffix );
			}
		}
		return $title;
	}

	/**
	 * Filter the content and insert WooCommerce content on the shop page.
	 *
	 * For non-WC themes, this will setup the main shop page to be shortcode based to improve default appearance.
	 *
	 * @since 3.3.0
	 * @param string $content Existing post content.
	 * @return string
	 */
	public static function unsupported_theme_shop_content_filter( $content ) {
		global $wp_query;

		if ( self::$theme_support || ! is_main_query() || ! in_the_loop() ) {
			return $content;
		}

		self::$in_content_filter = true;

		// Remove the filter we're in to avoid nested calls.
		remove_filter( 'the_content', array( __CLASS__, 'unsupported_theme_shop_content_filter' ) );

		// Unsupported theme shop page.
		if ( is_page( self::$shop_page_id ) ) {
			$args      = self::get_current_shop_view_args();
			$shortcode = new WC_Shortcode_Products(
				array_merge(
					WC()->query->get_catalog_ordering_args(),
					array(
						'page'     => $args->page,
						'columns'  => $args->columns,
						'rows'     => $args->rows,
						'orderby'  => '',
						'order'    => '',
						'paginate' => true,
						'cache'    => false,
					)
				),
				'products'
			);

			// Allow queries to run e.g. layered nav.
			add_action( 'pre_get_posts', array( WC()->query, 'product_query' ) );

			$content = $content . $shortcode->get_content();

			// Remove actions and self to avoid nested calls.
			remove_action( 'pre_get_posts', array( WC()->query, 'product_query' ) );
			WC()->query->remove_ordering_args();
		}

		self::$in_content_filter = false;

		return $content;
	}

	/**
	 * Filter the content and insert WooCommerce content on the shop page.
	 *
	 * For non-WC themes, this will setup the main shop page to be shortcode based to improve default appearance.
	 *
	 * @since 3.3.0
	 * @param string $content Existing post content.
	 * @return string
	 */
	public static function unsupported_theme_product_content_filter( $content ) {
		global $wp_query;

		if ( self::$theme_support || ! is_main_query() || ! in_the_loop() ) {
			return $content;
		}

		self::$in_content_filter = true;

		// Remove the filter we're in to avoid nested calls.
		remove_filter( 'the_content', array( __CLASS__, 'unsupported_theme_product_content_filter' ) );

		if ( is_product() ) {
			$content = do_shortcode( '[product_page id="' . get_the_ID() . '" show_title=0 status="any"]' );
		}

		self::$in_content_filter = false;

		return $content;
	}

	/**
	 * Suppress the comments number on the Shop page for unsupported themes since there is no commenting on the Shop page.
	 *
	 * @since 3.4.5
	 * @param string $comments_number The comments number text.
	 * @return string
	 */
	public static function unsupported_theme_comments_number_filter( $comments_number ) {
		if ( is_page( self::$shop_page_id ) ) {
			return '';
		}

		return $comments_number;
	}

	/**
	 * Are we filtering content for unsupported themes?
	 *
	 * @since 3.3.2
	 * @return bool
	 */
	public static function in_content_filter() {
		return (bool) self::$in_content_filter;
	}

	/**
	 * Prevent the main featured image on product pages because there will be another featured image
	 * in the gallery.
	 *
	 * @since 3.3.0
	 * @param string $html Img element HTML.
	 * @return string
	 */
	public static function unsupported_theme_single_featured_image_filter( $html ) {
		if ( self::in_content_filter() || ! is_product() || ! is_main_query() ) {
			return $html;
		}

		return '';
	}

	/**
	 * Remove the Review tab and just use the regular comment form.
	 *
	 * @param array $tabs Tab info.
	 * @return array
	 */
	public static function unsupported_theme_remove_review_tab( $tabs ) {
		unset( $tabs['reviews'] );
		return $tabs;
	}
}

add_action( 'init', array( 'WC_Template_Loader', 'init' ) );
class-wc-discounts.php000064400000101243151336044760011014 0ustar00<?php
/**
 * Discount calculation
 *
 * @package WooCommerce\Classes
 * @since   3.2.0
 */

use Automattic\WooCommerce\Utilities\NumberUtil;

defined( 'ABSPATH' ) || exit;

/**
 * Discounts class.
 */
class WC_Discounts {

	/**
	 * Reference to cart or order object.
	 *
	 * @since 3.2.0
	 * @var WC_Cart|WC_Order
	 */
	protected $object;

	/**
	 * An array of items to discount.
	 *
	 * @var array
	 */
	protected $items = array();

	/**
	 * An array of discounts which have been applied to items.
	 *
	 * @var array[] Code => Item Key => Value
	 */
	protected $discounts = array();

	/**
	 * WC_Discounts Constructor.
	 *
	 * @param WC_Cart|WC_Order $object Cart or order object.
	 */
	public function __construct( $object = null ) {
		if ( is_a( $object, 'WC_Cart' ) ) {
			$this->set_items_from_cart( $object );
		} elseif ( is_a( $object, 'WC_Order' ) ) {
			$this->set_items_from_order( $object );
		}
	}

	/**
	 * Set items directly. Used by WC_Cart_Totals.
	 *
	 * @since 3.2.3
	 * @param array $items Items to set.
	 */
	public function set_items( $items ) {
		$this->items     = $items;
		$this->discounts = array();
		uasort( $this->items, array( $this, 'sort_by_price' ) );
	}

	/**
	 * Normalise cart items which will be discounted.
	 *
	 * @since 3.2.0
	 * @param WC_Cart $cart Cart object.
	 */
	public function set_items_from_cart( $cart ) {
		$this->items     = array();
		$this->discounts = array();

		if ( ! is_a( $cart, 'WC_Cart' ) ) {
			return;
		}

		$this->object = $cart;

		foreach ( $cart->get_cart() as $key => $cart_item ) {
			$item                = new stdClass();
			$item->key           = $key;
			$item->object        = $cart_item;
			$item->product       = $cart_item['data'];
			$item->quantity      = $cart_item['quantity'];
			$item->price         = wc_add_number_precision_deep( (float) $item->product->get_price() * (float) $item->quantity );
			$this->items[ $key ] = $item;
		}

		uasort( $this->items, array( $this, 'sort_by_price' ) );
	}

	/**
	 * Normalise order items which will be discounted.
	 *
	 * @since 3.2.0
	 * @param WC_Order $order Order object.
	 */
	public function set_items_from_order( $order ) {
		$this->items     = array();
		$this->discounts = array();

		if ( ! is_a( $order, 'WC_Order' ) ) {
			return;
		}

		$this->object = $order;

		foreach ( $order->get_items() as $order_item ) {
			$item           = new stdClass();
			$item->key      = $order_item->get_id();
			$item->object   = $order_item;
			$item->product  = $order_item->get_product();
			$item->quantity = $order_item->get_quantity();
			$item->price    = wc_add_number_precision_deep( $order_item->get_subtotal() );

			if ( $order->get_prices_include_tax() ) {
				$item->price += wc_add_number_precision_deep( $order_item->get_subtotal_tax() );
			}

			$this->items[ $order_item->get_id() ] = $item;
		}

		uasort( $this->items, array( $this, 'sort_by_price' ) );
	}

	/**
	 * Get the object concerned.
	 *
	 * @since  3.3.2
	 * @return object
	 */
	public function get_object() {
		return $this->object;
	}

	/**
	 * Get items.
	 *
	 * @since  3.2.0
	 * @return object[]
	 */
	public function get_items() {
		return $this->items;
	}

	/**
	 * Get items to validate.
	 *
	 * @since  3.3.2
	 * @return object[]
	 */
	public function get_items_to_validate() {
		return apply_filters( 'woocommerce_coupon_get_items_to_validate', $this->get_items(), $this );
	}

	/**
	 * Get discount by key with or without precision.
	 *
	 * @since  3.2.0
	 * @param  string $key name of discount row to return.
	 * @param  bool   $in_cents Should the totals be returned in cents, or without precision.
	 * @return float
	 */
	public function get_discount( $key, $in_cents = false ) {
		$item_discount_totals = $this->get_discounts_by_item( $in_cents );
		return isset( $item_discount_totals[ $key ] ) ? $item_discount_totals[ $key ] : 0;
	}

	/**
	 * Get all discount totals.
	 *
	 * @since  3.2.0
	 * @param  bool $in_cents Should the totals be returned in cents, or without precision.
	 * @return array
	 */
	public function get_discounts( $in_cents = false ) {
		$discounts = $this->discounts;
		return $in_cents ? $discounts : wc_remove_number_precision_deep( $discounts );
	}

	/**
	 * Get all discount totals per item.
	 *
	 * @since  3.2.0
	 * @param  bool $in_cents Should the totals be returned in cents, or without precision.
	 * @return array
	 */
	public function get_discounts_by_item( $in_cents = false ) {
		$discounts            = $this->discounts;
		$item_discount_totals = (array) array_shift( $discounts );

		foreach ( $discounts as $item_discounts ) {
			foreach ( $item_discounts as $item_key => $item_discount ) {
				$item_discount_totals[ $item_key ] += $item_discount;
			}
		}

		return $in_cents ? $item_discount_totals : wc_remove_number_precision_deep( $item_discount_totals );
	}

	/**
	 * Get all discount totals per coupon.
	 *
	 * @since  3.2.0
	 * @param  bool $in_cents Should the totals be returned in cents, or without precision.
	 * @return array
	 */
	public function get_discounts_by_coupon( $in_cents = false ) {
		$coupon_discount_totals = array_map( 'array_sum', $this->discounts );

		return $in_cents ? $coupon_discount_totals : wc_remove_number_precision_deep( $coupon_discount_totals );
	}

	/**
	 * Get discounted price of an item without precision.
	 *
	 * @since  3.2.0
	 * @param  object $item Get data for this item.
	 * @return float
	 */
	public function get_discounted_price( $item ) {
		return wc_remove_number_precision_deep( $this->get_discounted_price_in_cents( $item ) );
	}

	/**
	 * Get discounted price of an item to precision (in cents).
	 *
	 * @since  3.2.0
	 * @param  object $item Get data for this item.
	 * @return int
	 */
	public function get_discounted_price_in_cents( $item ) {
		return absint( NumberUtil::round( $item->price - $this->get_discount( $item->key, true ) ) );
	}

	/**
	 * Apply a discount to all items using a coupon.
	 *
	 * @since  3.2.0
	 * @param  WC_Coupon $coupon Coupon object being applied to the items.
	 * @param  bool      $validate Set to false to skip coupon validation.
	 * @throws Exception Error message when coupon isn't valid.
	 * @return bool|WP_Error True if applied or WP_Error instance in failure.
	 */
	public function apply_coupon( $coupon, $validate = true ) {
		if ( ! is_a( $coupon, 'WC_Coupon' ) ) {
			return new WP_Error( 'invalid_coupon', __( 'Invalid coupon', 'woocommerce' ) );
		}

		$is_coupon_valid = $validate ? $this->is_coupon_valid( $coupon ) : true;

		if ( is_wp_error( $is_coupon_valid ) ) {
			return $is_coupon_valid;
		}

		if ( ! isset( $this->discounts[ $coupon->get_code() ] ) ) {
			$this->discounts[ $coupon->get_code() ] = array_fill_keys( array_keys( $this->items ), 0 );
		}

		$items_to_apply = $this->get_items_to_apply_coupon( $coupon );

		// Core discounts are handled here as of 3.2.
		switch ( $coupon->get_discount_type() ) {
			case 'percent':
				$this->apply_coupon_percent( $coupon, $items_to_apply );
				break;
			case 'fixed_product':
				$this->apply_coupon_fixed_product( $coupon, $items_to_apply );
				break;
			case 'fixed_cart':
				$this->apply_coupon_fixed_cart( $coupon, $items_to_apply );
				break;
			default:
				$this->apply_coupon_custom( $coupon, $items_to_apply );
				break;
		}

		return true;
	}

	/**
	 * Sort by price.
	 *
	 * @since  3.2.0
	 * @param  array $a First element.
	 * @param  array $b Second element.
	 * @return int
	 */
	protected function sort_by_price( $a, $b ) {
		$price_1 = $a->price * $a->quantity;
		$price_2 = $b->price * $b->quantity;
		if ( $price_1 === $price_2 ) {
			return 0;
		}
		return ( $price_1 < $price_2 ) ? 1 : -1;
	}

	/**
	 * Filter out all products which have been fully discounted to 0.
	 * Used as array_filter callback.
	 *
	 * @since  3.2.0
	 * @param  object $item Get data for this item.
	 * @return bool
	 */
	protected function filter_products_with_price( $item ) {
		return $this->get_discounted_price_in_cents( $item ) > 0;
	}

	/**
	 * Get items which the coupon should be applied to.
	 *
	 * @since  3.2.0
	 * @param  object $coupon Coupon object.
	 * @return array
	 */
	protected function get_items_to_apply_coupon( $coupon ) {
		$items_to_apply = array();

		foreach ( $this->get_items_to_validate() as $item ) {
			$item_to_apply = clone $item; // Clone the item so changes to this item do not affect the originals.

			if ( 0 === $this->get_discounted_price_in_cents( $item_to_apply ) || 0 >= $item_to_apply->quantity ) {
				continue;
			}

			if ( ! $coupon->is_valid_for_product( $item_to_apply->product, $item_to_apply->object ) && ! $coupon->is_valid_for_cart() ) {
				continue;
			}

			$items_to_apply[] = $item_to_apply;
		}
		return $items_to_apply;
	}

	/**
	 * Apply percent discount to items and return an array of discounts granted.
	 *
	 * @since  3.2.0
	 * @param  WC_Coupon $coupon Coupon object. Passed through filters.
	 * @param  array     $items_to_apply Array of items to apply the coupon to.
	 * @return int Total discounted.
	 */
	protected function apply_coupon_percent( $coupon, $items_to_apply ) {
		$total_discount        = 0;
		$cart_total            = 0;
		$limit_usage_qty       = 0;
		$applied_count         = 0;
		$adjust_final_discount = true;

		if ( null !== $coupon->get_limit_usage_to_x_items() ) {
			$limit_usage_qty = $coupon->get_limit_usage_to_x_items();
		}

		$coupon_amount = $coupon->get_amount();

		foreach ( $items_to_apply as $item ) {
			// Find out how much price is available to discount for the item.
			$discounted_price = $this->get_discounted_price_in_cents( $item );

			// Get the price we actually want to discount, based on settings.
			$price_to_discount = ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $discounted_price : NumberUtil::round( $item->price );

			// See how many and what price to apply to.
			$apply_quantity    = $limit_usage_qty && ( $limit_usage_qty - $applied_count ) < $item->quantity ? $limit_usage_qty - $applied_count : $item->quantity;
			$apply_quantity    = max( 0, apply_filters( 'woocommerce_coupon_get_apply_quantity', $apply_quantity, $item, $coupon, $this ) );
			$price_to_discount = ( $price_to_discount / $item->quantity ) * $apply_quantity;

			// Run coupon calculations.
			$discount = floor( $price_to_discount * ( $coupon_amount / 100 ) );

			if ( is_a( $this->object, 'WC_Cart' ) && has_filter( 'woocommerce_coupon_get_discount_amount' ) ) {
				// Send through the legacy filter, but not as cents.
				$filtered_discount = wc_add_number_precision( apply_filters( 'woocommerce_coupon_get_discount_amount', wc_remove_number_precision( $discount ), wc_remove_number_precision( $price_to_discount ), $item->object, false, $coupon ) );

				if ( $filtered_discount !== $discount ) {
					$discount              = $filtered_discount;
					$adjust_final_discount = false;
				}
			}

			$discount       = wc_round_discount( min( $discounted_price, $discount ), 0 );
			$cart_total     = $cart_total + $price_to_discount;
			$total_discount = $total_discount + $discount;
			$applied_count  = $applied_count + $apply_quantity;

			// Store code and discount amount per item.
			$this->discounts[ $coupon->get_code() ][ $item->key ] += $discount;
		}

		// Work out how much discount would have been given to the cart as a whole and compare to what was discounted on all line items.
		$cart_total_discount = wc_round_discount( $cart_total * ( $coupon_amount / 100 ), 0 );

		if ( $total_discount < $cart_total_discount && $adjust_final_discount ) {
			$total_discount += $this->apply_coupon_remainder( $coupon, $items_to_apply, $cart_total_discount - $total_discount );
		}

		return $total_discount;
	}

	/**
	 * Apply fixed product discount to items.
	 *
	 * @since  3.2.0
	 * @param  WC_Coupon $coupon Coupon object. Passed through filters.
	 * @param  array     $items_to_apply Array of items to apply the coupon to.
	 * @param  int       $amount Fixed discount amount to apply in cents. Leave blank to pull from coupon.
	 * @return int Total discounted.
	 */
	protected function apply_coupon_fixed_product( $coupon, $items_to_apply, $amount = null ) {
		$total_discount  = 0;
		$amount          = $amount ? $amount : wc_add_number_precision( $coupon->get_amount() );
		$limit_usage_qty = 0;
		$applied_count   = 0;

		if ( null !== $coupon->get_limit_usage_to_x_items() ) {
			$limit_usage_qty = $coupon->get_limit_usage_to_x_items();
		}

		foreach ( $items_to_apply as $item ) {
			// Find out how much price is available to discount for the item.
			$discounted_price = $this->get_discounted_price_in_cents( $item );

			// Get the price we actually want to discount, based on settings.
			$price_to_discount = ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $discounted_price : $item->price;

			// Run coupon calculations.
			if ( $limit_usage_qty ) {
				$apply_quantity = $limit_usage_qty - $applied_count < $item->quantity ? $limit_usage_qty - $applied_count : $item->quantity;
				$apply_quantity = max( 0, apply_filters( 'woocommerce_coupon_get_apply_quantity', $apply_quantity, $item, $coupon, $this ) );
				$discount       = min( $amount, $item->price / $item->quantity ) * $apply_quantity;
			} else {
				$apply_quantity = apply_filters( 'woocommerce_coupon_get_apply_quantity', $item->quantity, $item, $coupon, $this );
				$discount       = $amount * $apply_quantity;
			}

			if ( is_a( $this->object, 'WC_Cart' ) && has_filter( 'woocommerce_coupon_get_discount_amount' ) ) {
				// Send through the legacy filter, but not as cents.
				$discount = wc_add_number_precision( apply_filters( 'woocommerce_coupon_get_discount_amount', wc_remove_number_precision( $discount ), wc_remove_number_precision( $price_to_discount ), $item->object, false, $coupon ) );
			}

			$discount       = min( $discounted_price, $discount );
			$total_discount = $total_discount + $discount;
			$applied_count  = $applied_count + $apply_quantity;

			// Store code and discount amount per item.
			$this->discounts[ $coupon->get_code() ][ $item->key ] += $discount;
		}
		return $total_discount;
	}

	/**
	 * Apply fixed cart discount to items.
	 *
	 * @since  3.2.0
	 * @param  WC_Coupon $coupon Coupon object. Passed through filters.
	 * @param  array     $items_to_apply Array of items to apply the coupon to.
	 * @param  int       $amount Fixed discount amount to apply in cents. Leave blank to pull from coupon.
	 * @return int Total discounted.
	 */
	protected function apply_coupon_fixed_cart( $coupon, $items_to_apply, $amount = null ) {
		$total_discount = 0;
		$amount         = $amount ? $amount : wc_add_number_precision( $coupon->get_amount() );
		$items_to_apply = array_filter( $items_to_apply, array( $this, 'filter_products_with_price' ) );
		$item_count     = array_sum( wp_list_pluck( $items_to_apply, 'quantity' ) );

		if ( ! $item_count ) {
			return $total_discount;
		}

		if ( ! $amount ) {
			// If there is no amount we still send it through so filters are fired.
			$total_discount = $this->apply_coupon_fixed_product( $coupon, $items_to_apply, 0 );
		} else {
			$per_item_discount = absint( $amount / $item_count ); // round it down to the nearest cent.

			if ( $per_item_discount > 0 ) {
				$total_discount = $this->apply_coupon_fixed_product( $coupon, $items_to_apply, $per_item_discount );

				/**
				 * If there is still discount remaining, repeat the process.
				 */
				if ( $total_discount > 0 && $total_discount < $amount ) {
					$total_discount += $this->apply_coupon_fixed_cart( $coupon, $items_to_apply, $amount - $total_discount );
				}
			} elseif ( $amount > 0 ) {
				$total_discount += $this->apply_coupon_remainder( $coupon, $items_to_apply, $amount );
			}
		}
		return $total_discount;
	}

	/**
	 * Apply custom coupon discount to items.
	 *
	 * @since  3.3
	 * @param  WC_Coupon $coupon Coupon object. Passed through filters.
	 * @param  array     $items_to_apply Array of items to apply the coupon to.
	 * @return int Total discounted.
	 */
	protected function apply_coupon_custom( $coupon, $items_to_apply ) {
		$limit_usage_qty = 0;
		$applied_count   = 0;

		if ( null !== $coupon->get_limit_usage_to_x_items() ) {
			$limit_usage_qty = $coupon->get_limit_usage_to_x_items();
		}

		// Apply the coupon to each item.
		foreach ( $items_to_apply as $item ) {
			// Find out how much price is available to discount for the item.
			$discounted_price = $this->get_discounted_price_in_cents( $item );

			// Get the price we actually want to discount, based on settings.
			$price_to_discount = wc_remove_number_precision( ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $discounted_price : $item->price );

			// See how many and what price to apply to.
			$apply_quantity = $limit_usage_qty && ( $limit_usage_qty - $applied_count ) < $item->quantity ? $limit_usage_qty - $applied_count : $item->quantity;
			$apply_quantity = max( 0, apply_filters( 'woocommerce_coupon_get_apply_quantity', $apply_quantity, $item, $coupon, $this ) );

			// Run coupon calculations.
			$discount      = wc_add_number_precision( $coupon->get_discount_amount( $price_to_discount / $item->quantity, $item->object, true ) ) * $apply_quantity;
			$discount      = wc_round_discount( min( $discounted_price, $discount ), 0 );
			$applied_count = $applied_count + $apply_quantity;

			// Store code and discount amount per item.
			$this->discounts[ $coupon->get_code() ][ $item->key ] += $discount;
		}

		// Allow post-processing for custom coupon types (e.g. calculating discrepancy, etc).
		$this->discounts[ $coupon->get_code() ] = apply_filters( 'woocommerce_coupon_custom_discounts_array', $this->discounts[ $coupon->get_code() ], $coupon );

		return array_sum( $this->discounts[ $coupon->get_code() ] );
	}

	/**
	 * Deal with remaining fractional discounts by splitting it over items
	 * until the amount is expired, discounting 1 cent at a time.
	 *
	 * @since 3.2.0
	 * @param  WC_Coupon $coupon Coupon object if appliable. Passed through filters.
	 * @param  array     $items_to_apply Array of items to apply the coupon to.
	 * @param  int       $amount Fixed discount amount to apply.
	 * @return int Total discounted.
	 */
	protected function apply_coupon_remainder( $coupon, $items_to_apply, $amount ) {
		$total_discount = 0;

		foreach ( $items_to_apply as $item ) {
			for ( $i = 0; $i < $item->quantity; $i ++ ) {
				// Find out how much price is available to discount for the item.
				$price_to_discount = $this->get_discounted_price_in_cents( $item );

				// Run coupon calculations.
				$discount = min( $price_to_discount, 1 );

				// Store totals.
				$total_discount += $discount;

				// Store code and discount amount per item.
				$this->discounts[ $coupon->get_code() ][ $item->key ] += $discount;

				if ( $total_discount >= $amount ) {
					break 2;
				}
			}
			if ( $total_discount >= $amount ) {
				break;
			}
		}
		return $total_discount;
	}

	/**
	 * Ensure coupon exists or throw exception.
	 *
	 * @since  3.2.0
	 * @throws Exception Error message.
	 * @param  WC_Coupon $coupon Coupon data.
	 * @return bool
	 */
	protected function validate_coupon_exists( $coupon ) {
		if ( ! $coupon->get_id() && ! $coupon->get_virtual() ) {
			/* translators: %s: coupon code */
			throw new Exception( sprintf( __( 'Coupon "%s" does not exist!', 'woocommerce' ), esc_html( $coupon->get_code() ) ), 105 );
		}

		return true;
	}

	/**
	 * Ensure coupon usage limit is valid or throw exception.
	 *
	 * @since  3.2.0
	 * @throws Exception Error message.
	 * @param  WC_Coupon $coupon Coupon data.
	 * @return bool
	 */
	protected function validate_coupon_usage_limit( $coupon ) {
		if ( ! $coupon->get_usage_limit() ) {
			return true;
		}
		$usage_count           = $coupon->get_usage_count();
		$data_store            = $coupon->get_data_store();
		$tentative_usage_count = is_callable( array( $data_store, 'get_tentative_usage_count' ) ) ? $data_store->get_tentative_usage_count( $coupon->get_id() ) : 0;
		if ( $usage_count + $tentative_usage_count < $coupon->get_usage_limit() ) {
			// All good.
			return true;
		}
		// Coupon usage limit is reached. Let's show as informative error message as we can.
		if ( 0 === $tentative_usage_count ) {
			// No held coupon, usage limit is indeed reached.
			$error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED;
		} elseif ( is_user_logged_in() ) {
			$recent_pending_orders = wc_get_orders(
				array(
					'limit'       => 1,
					'post_status' => array( 'wc-failed', 'wc-pending' ),
					'customer'    => get_current_user_id(),
					'return'      => 'ids',
				)
			);
			if ( count( $recent_pending_orders ) > 0 ) {
				// User logged in and have a pending order, maybe they are trying to use the coupon.
				$error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK;
			} else {
				$error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED;
			}
		} else {
			// Maybe this user was trying to use the coupon but got stuck. We can't know for sure (performantly). Show a slightly better error message.
			$error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK_GUEST;
		}
		throw new Exception( $coupon->get_coupon_error( $error_code ), $error_code );
	}

	/**
	 * Ensure coupon user usage limit is valid or throw exception.
	 *
	 * Per user usage limit - check here if user is logged in (against user IDs).
	 * Checked again for emails later on in WC_Cart::check_customer_coupons().
	 *
	 * @since  3.2.0
	 * @throws Exception Error message.
	 * @param  WC_Coupon $coupon  Coupon data.
	 * @param  int       $user_id User ID.
	 * @return bool
	 */
	protected function validate_coupon_user_usage_limit( $coupon, $user_id = 0 ) {
		if ( empty( $user_id ) ) {
			if ( $this->object instanceof WC_Order ) {
				$user_id = $this->object->get_customer_id();
			} else {
				$user_id = get_current_user_id();
			}
		}

		if ( $coupon && $user_id && apply_filters( 'woocommerce_coupon_validate_user_usage_limit', $coupon->get_usage_limit_per_user() > 0, $user_id, $coupon, $this ) && $coupon->get_id() && $coupon->get_data_store() ) {
			$data_store  = $coupon->get_data_store();
			$usage_count = $data_store->get_usage_by_user_id( $coupon, $user_id );
			if ( $usage_count >= $coupon->get_usage_limit_per_user() ) {
				if ( $data_store->get_tentative_usages_for_user( $coupon->get_id(), array( $user_id ) ) > 0 ) {
					$error_message = $coupon->get_coupon_error( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK );
					$error_code    = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK;
				} else {
					$error_message = $coupon->get_coupon_error( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED );
					$error_code    = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED;
				}
				throw new Exception( $error_message, $error_code );
			}
		}

		return true;
	}

	/**
	 * Ensure coupon date is valid or throw exception.
	 *
	 * @since  3.2.0
	 * @throws Exception Error message.
	 * @param  WC_Coupon $coupon Coupon data.
	 * @return bool
	 */
	protected function validate_coupon_expiry_date( $coupon ) {
		if ( $coupon->get_date_expires() && apply_filters( 'woocommerce_coupon_validate_expiry_date', time() > $coupon->get_date_expires()->getTimestamp(), $coupon, $this ) ) {
			throw new Exception( __( 'This coupon has expired.', 'woocommerce' ), 107 );
		}

		return true;
	}

	/**
	 * Ensure coupon amount is valid or throw exception.
	 *
	 * @since  3.2.0
	 * @throws Exception Error message.
	 * @param  WC_Coupon $coupon   Coupon data.
	 * @return bool
	 */
	protected function validate_coupon_minimum_amount( $coupon ) {
		$subtotal = wc_remove_number_precision( $this->get_object_subtotal() );

		if ( $coupon->get_minimum_amount() > 0 && apply_filters( 'woocommerce_coupon_validate_minimum_amount', $coupon->get_minimum_amount() > $subtotal, $coupon, $subtotal ) ) {
			/* translators: %s: coupon minimum amount */
			throw new Exception( sprintf( __( 'The minimum spend for this coupon is %s.', 'woocommerce' ), wc_price( $coupon->get_minimum_amount() ) ), 108 );
		}

		return true;
	}

	/**
	 * Ensure coupon amount is valid or throw exception.
	 *
	 * @since  3.2.0
	 * @throws Exception Error message.
	 * @param  WC_Coupon $coupon   Coupon data.
	 * @return bool
	 */
	protected function validate_coupon_maximum_amount( $coupon ) {
		$subtotal = wc_remove_number_precision( $this->get_object_subtotal() );

		if ( $coupon->get_maximum_amount() > 0 && apply_filters( 'woocommerce_coupon_validate_maximum_amount', $coupon->get_maximum_amount() < $subtotal, $coupon ) ) {
			/* translators: %s: coupon maximum amount */
			throw new Exception( sprintf( __( 'The maximum spend for this coupon is %s.', 'woocommerce' ), wc_price( $coupon->get_maximum_amount() ) ), 112 );
		}

		return true;
	}

	/**
	 * Ensure coupon is valid for products in the list is valid or throw exception.
	 *
	 * @since  3.2.0
	 * @throws Exception Error message.
	 * @param  WC_Coupon $coupon Coupon data.
	 * @return bool
	 */
	protected function validate_coupon_product_ids( $coupon ) {
		if ( count( $coupon->get_product_ids() ) > 0 ) {
			$valid = false;

			foreach ( $this->get_items_to_validate() as $item ) {
				if ( $item->product && in_array( $item->product->get_id(), $coupon->get_product_ids(), true ) || in_array( $item->product->get_parent_id(), $coupon->get_product_ids(), true ) ) {
					$valid = true;
					break;
				}
			}

			if ( ! $valid ) {
				throw new Exception( __( 'Sorry, this coupon is not applicable to selected products.', 'woocommerce' ), 109 );
			}
		}

		return true;
	}

	/**
	 * Ensure coupon is valid for product categories in the list is valid or throw exception.
	 *
	 * @since  3.2.0
	 * @throws Exception Error message.
	 * @param  WC_Coupon $coupon Coupon data.
	 * @return bool
	 */
	protected function validate_coupon_product_categories( $coupon ) {
		if ( count( $coupon->get_product_categories() ) > 0 ) {
			$valid = false;

			foreach ( $this->get_items_to_validate() as $item ) {
				if ( $coupon->get_exclude_sale_items() && $item->product && $item->product->is_on_sale() ) {
					continue;
				}

				$product_cats = wc_get_product_cat_ids( $item->product->get_id() );

				if ( $item->product->get_parent_id() ) {
					$product_cats = array_merge( $product_cats, wc_get_product_cat_ids( $item->product->get_parent_id() ) );
				}

				// If we find an item with a cat in our allowed cat list, the coupon is valid.
				if ( count( array_intersect( $product_cats, $coupon->get_product_categories() ) ) > 0 ) {
					$valid = true;
					break;
				}
			}

			if ( ! $valid ) {
				throw new Exception( __( 'Sorry, this coupon is not applicable to selected products.', 'woocommerce' ), 109 );
			}
		}

		return true;
	}

	/**
	 * Ensure coupon is valid for sale items in the list is valid or throw exception.
	 *
	 * @since  3.2.0
	 * @throws Exception Error message.
	 * @param  WC_Coupon $coupon Coupon data.
	 * @return bool
	 */
	protected function validate_coupon_sale_items( $coupon ) {
		if ( $coupon->get_exclude_sale_items() ) {
			$valid = true;

			foreach ( $this->get_items_to_validate() as $item ) {
				if ( $item->product && $item->product->is_on_sale() ) {
					$valid = false;
					break;
				}
			}

			if ( ! $valid ) {
				throw new Exception( __( 'Sorry, this coupon is not valid for sale items.', 'woocommerce' ), 110 );
			}
		}

		return true;
	}

	/**
	 * All exclusion rules must pass at the same time for a product coupon to be valid.
	 *
	 * @since  3.2.0
	 * @throws Exception Error message.
	 * @param  WC_Coupon $coupon Coupon data.
	 * @return bool
	 */
	protected function validate_coupon_excluded_items( $coupon ) {
		$items = $this->get_items_to_validate();
		if ( ! empty( $items ) && $coupon->is_type( wc_get_product_coupon_types() ) ) {
			$valid = false;

			foreach ( $items as $item ) {
				if ( $item->product && $coupon->is_valid_for_product( $item->product, $item->object ) ) {
					$valid = true;
					break;
				}
			}

			if ( ! $valid ) {
				throw new Exception( __( 'Sorry, this coupon is not applicable to selected products.', 'woocommerce' ), 109 );
			}
		}

		return true;
	}

	/**
	 * Cart discounts cannot be added if non-eligible product is found.
	 *
	 * @since  3.2.0
	 * @throws Exception Error message.
	 * @param  WC_Coupon $coupon Coupon data.
	 * @return bool
	 */
	protected function validate_coupon_eligible_items( $coupon ) {
		if ( ! $coupon->is_type( wc_get_product_coupon_types() ) ) {
			$this->validate_coupon_sale_items( $coupon );
			$this->validate_coupon_excluded_product_ids( $coupon );
			$this->validate_coupon_excluded_product_categories( $coupon );
		}

		return true;
	}

	/**
	 * Exclude products.
	 *
	 * @since  3.2.0
	 * @throws Exception Error message.
	 * @param  WC_Coupon $coupon Coupon data.
	 * @return bool
	 */
	protected function validate_coupon_excluded_product_ids( $coupon ) {
		// Exclude Products.
		if ( count( $coupon->get_excluded_product_ids() ) > 0 ) {
			$products = array();

			foreach ( $this->get_items_to_validate() as $item ) {
				if ( $item->product && in_array( $item->product->get_id(), $coupon->get_excluded_product_ids(), true ) || in_array( $item->product->get_parent_id(), $coupon->get_excluded_product_ids(), true ) ) {
					$products[] = $item->product->get_name();
				}
			}

			if ( ! empty( $products ) ) {
				/* translators: %s: products list */
				throw new Exception( sprintf( __( 'Sorry, this coupon is not applicable to the products: %s.', 'woocommerce' ), implode( ', ', $products ) ), 113 );
			}
		}

		return true;
	}

	/**
	 * Exclude categories from product list.
	 *
	 * @since  3.2.0
	 * @throws Exception Error message.
	 * @param  WC_Coupon $coupon Coupon data.
	 * @return bool
	 */
	protected function validate_coupon_excluded_product_categories( $coupon ) {
		if ( count( $coupon->get_excluded_product_categories() ) > 0 ) {
			$categories = array();

			foreach ( $this->get_items_to_validate() as $item ) {
				if ( ! $item->product ) {
					continue;
				}

				$product_cats = wc_get_product_cat_ids( $item->product->get_id() );

				if ( $item->product->get_parent_id() ) {
					$product_cats = array_merge( $product_cats, wc_get_product_cat_ids( $item->product->get_parent_id() ) );
				}

				$cat_id_list = array_intersect( $product_cats, $coupon->get_excluded_product_categories() );
				if ( count( $cat_id_list ) > 0 ) {
					foreach ( $cat_id_list as $cat_id ) {
						$cat          = get_term( $cat_id, 'product_cat' );
						$categories[] = $cat->name;
					}
				}
			}

			if ( ! empty( $categories ) ) {
				/* translators: %s: categories list */
				throw new Exception( sprintf( __( 'Sorry, this coupon is not applicable to the categories: %s.', 'woocommerce' ), implode( ', ', array_unique( $categories ) ) ), 114 );
			}
		}

		return true;
	}

	/**
	 * Get the object subtotal
	 *
	 * @return int
	 */
	protected function get_object_subtotal() {
		if ( is_a( $this->object, 'WC_Cart' ) ) {
			return wc_add_number_precision( $this->object->get_displayed_subtotal() );
		} elseif ( is_a( $this->object, 'WC_Order' ) ) {
			$subtotal = wc_add_number_precision( $this->object->get_subtotal() );

			if ( $this->object->get_prices_include_tax() ) {
				// Add tax to tax-exclusive subtotal.
				$subtotal = $subtotal + wc_add_number_precision( NumberUtil::round( $this->object->get_total_tax(), wc_get_price_decimals() ) );
			}

			return $subtotal;
		} else {
			return array_sum( wp_list_pluck( $this->items, 'price' ) );
		}
	}

	/**
	 * Check if a coupon is valid.
	 *
	 * Error Codes:
	 * - 100: Invalid filtered.
	 * - 101: Invalid removed.
	 * - 102: Not yours removed.
	 * - 103: Already applied.
	 * - 104: Individual use only.
	 * - 105: Not exists.
	 * - 106: Usage limit reached.
	 * - 107: Expired.
	 * - 108: Minimum spend limit not met.
	 * - 109: Not applicable.
	 * - 110: Not valid for sale items.
	 * - 111: Missing coupon code.
	 * - 112: Maximum spend limit met.
	 * - 113: Excluded products.
	 * - 114: Excluded categories.
	 *
	 * @since  3.2.0
	 * @throws Exception Error message.
	 * @param  WC_Coupon $coupon Coupon data.
	 * @return bool|WP_Error
	 */
	public function is_coupon_valid( $coupon ) {
		try {
			$this->validate_coupon_exists( $coupon );
			$this->validate_coupon_usage_limit( $coupon );
			$this->validate_coupon_user_usage_limit( $coupon );
			$this->validate_coupon_expiry_date( $coupon );
			$this->validate_coupon_minimum_amount( $coupon );
			$this->validate_coupon_maximum_amount( $coupon );
			$this->validate_coupon_product_ids( $coupon );
			$this->validate_coupon_product_categories( $coupon );
			$this->validate_coupon_excluded_items( $coupon );
			$this->validate_coupon_eligible_items( $coupon );

			if ( ! apply_filters( 'woocommerce_coupon_is_valid', true, $coupon, $this ) ) {
				throw new Exception( __( 'Coupon is not valid.', 'woocommerce' ), 100 );
			}
		} catch ( Exception $e ) {
			/**
			 * Filter the coupon error message.
			 *
			 * @param string    $error_message Error message.
			 * @param int       $error_code    Error code.
			 * @param WC_Coupon $coupon        Coupon data.
			 */
			$message = apply_filters( 'woocommerce_coupon_error', is_numeric( $e->getMessage() ) ? $coupon->get_coupon_error( $e->getMessage() ) : $e->getMessage(), $e->getCode(), $coupon );

			return new WP_Error(
				'invalid_coupon',
				$message,
				array(
					'status' => 400,
				)
			);
		}
		return true;
	}
}
class-wc-webhook.php000064400000073742151336044760010453 0ustar00<?php
/**
 * Webhook
 *
 * This class handles storing and retrieving webhook data from the associated.
 *
 * Webhooks are enqueued to their associated actions, delivered, and logged.
 *
 * @version  3.2.0
 * @package  WooCommerce\Webhooks
 * @since    2.2.0
 */

use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Utilities\NumberUtil;

defined( 'ABSPATH' ) || exit;

require_once __DIR__ . '/legacy/class-wc-legacy-webhook.php';

/**
 * Webhook class.
 */
class WC_Webhook extends WC_Legacy_Webhook {

	/**
	 * Store which object IDs this webhook has processed (ie scheduled to be delivered)
	 * within the current page request.
	 *
	 * @var array
	 */
	protected $processed = array();

	/**
	 * Stores webhook data.
	 *
	 * @var array
	 */
	protected $data = array(
		'date_created'     => null,
		'date_modified'    => null,
		'status'           => 'disabled',
		'delivery_url'     => '',
		'secret'           => '',
		'name'             => '',
		'topic'            => '',
		'hooks'            => '',
		'resource'         => '',
		'event'            => '',
		'failure_count'    => 0,
		'user_id'          => 0,
		'api_version'      => 3,
		'pending_delivery' => false,
	);

	/**
	 * Load webhook data based on how WC_Webhook is called.
	 *
	 * @param WC_Webhook|int $data Webhook ID or data.
	 * @throws Exception If webhook cannot be read/found and $data is set.
	 */
	public function __construct( $data = 0 ) {
		parent::__construct( $data );

		if ( $data instanceof WC_Webhook ) {
			$this->set_id( absint( $data->get_id() ) );
		} elseif ( is_numeric( $data ) ) {
			$this->set_id( $data );
		}

		$this->data_store = WC_Data_Store::load( 'webhook' );

		// If we have an ID, load the webhook from the DB.
		if ( $this->get_id() ) {
			try {
				$this->data_store->read( $this );
			} catch ( Exception $e ) {
				$this->set_id( 0 );
				$this->set_object_read( true );
			}
		} else {
			$this->set_object_read( true );
		}
	}

	/**
	 * Enqueue the hooks associated with the webhook.
	 *
	 * @since 2.2.0
	 */
	public function enqueue() {
		$hooks = $this->get_hooks();
		$url   = $this->get_delivery_url();

		if ( is_array( $hooks ) && ! empty( $url ) ) {
			foreach ( $hooks as $hook ) {
				add_action( $hook, array( $this, 'process' ) );
			}
		}
	}

	/**
	 * Process the webhook for delivery by verifying that it should be delivered.
	 * and scheduling the delivery (in the background by default, or immediately).
	 *
	 * @since  2.2.0
	 * @param  mixed $arg The first argument provided from the associated hooks.
	 * @return mixed $arg Returns the argument in case the webhook was hooked into a filter.
	 */
	public function process( $arg ) {

		// Verify that webhook should be processed for delivery.
		if ( ! $this->should_deliver( $arg ) ) {
			return;
		}

		// Mark this $arg as processed to ensure it doesn't get processed again within the current request.
		$this->processed[] = $arg;

		/**
		 * Process webhook delivery.
		 *
		 * @since 3.3.0
		 * @hooked wc_webhook_process_delivery - 10
		 */
		do_action( 'woocommerce_webhook_process_delivery', $this, $arg );

		return $arg;
	}

	/**
	 * Helper to check if the webhook should be delivered, as some hooks.
	 * (like `wp_trash_post`) will fire for every post type, not just ours.
	 *
	 * @since  2.2.0
	 * @param  mixed $arg First hook argument.
	 * @return bool       True if webhook should be delivered, false otherwise.
	 */
	private function should_deliver( $arg ) {
		$should_deliver = $this->is_active() && $this->is_valid_topic() && $this->is_valid_action( $arg ) && $this->is_valid_resource( $arg ) && ! $this->is_already_processed( $arg );

		/**
		 * Let other plugins intercept deliver for some messages queue like rabbit/zeromq.
		 *
		 * @param bool       $should_deliver True if the webhook should be sent, or false to not send it.
		 * @param WC_Webhook $this The current webhook class.
		 * @param mixed      $arg First hook argument.
		 */
		return apply_filters( 'woocommerce_webhook_should_deliver', $should_deliver, $this, $arg );
	}

	/**
	 * Returns if webhook is active.
	 *
	 * @since  3.6.0
	 * @return bool  True if validation passes.
	 */
	private function is_active() {
		return 'active' === $this->get_status();
	}

	/**
	 * Returns if topic is valid.
	 *
	 * @since  3.6.0
	 * @return bool  True if validation passes.
	 */
	private function is_valid_topic() {
		return wc_is_webhook_valid_topic( $this->get_topic() );
	}

	/**
	 * Validates the criteria for certain actions.
	 *
	 * @since  3.6.0
	 * @param  mixed $arg First hook argument.
	 * @return bool       True if validation passes.
	 */
	private function is_valid_action( $arg ) {
		$current_action = current_action();
		$return         = true;

		switch ( $current_action ) {
			case 'delete_post':
			case 'wp_trash_post':
			case 'untrashed_post':
				$return = $this->is_valid_post_action( $arg );
				break;
			case 'delete_user':
				$return = $this->is_valid_user_action( $arg );
				break;
		}

		if ( 0 === strpos( $current_action, 'woocommerce_process_shop' ) || 0 === strpos( $current_action, 'woocommerce_process_product' ) ) {
			$return = $this->is_valid_processing_action( $arg );
		}

		return $return;
	}

	/**
	 * Validates post actions.
	 *
	 * @since  3.6.0
	 * @param  mixed $arg First hook argument.
	 * @return bool       True if validation passes.
	 */
	private function is_valid_post_action( $arg ) {
		// Only deliver deleted/restored event for coupons, orders, and products.
		if ( isset( $GLOBALS['post_type'] ) && ! in_array( $GLOBALS['post_type'], array( 'shop_coupon', 'shop_order', 'product' ), true ) ) {
			return false;
		}

		// Check if is delivering for the correct resource.
		if ( isset( $GLOBALS['post_type'] ) && str_replace( 'shop_', '', $GLOBALS['post_type'] ) !== $this->get_resource() ) {
			return false;
		}
		return true;
	}

	/**
	 * Validates user actions.
	 *
	 * @since  3.6.0
	 * @param  mixed $arg First hook argument.
	 * @return bool       True if validation passes.
	 */
	private function is_valid_user_action( $arg ) {
		$user = get_userdata( absint( $arg ) );

		// Only deliver deleted customer event for users with customer role.
		if ( ! $user || ! in_array( 'customer', (array) $user->roles, true ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Validates WC processing actions.
	 *
	 * @since  3.6.0
	 * @param  mixed $arg First hook argument.
	 * @return bool       True if validation passes.
	 */
	private function is_valid_processing_action( $arg ) {
		// The `woocommerce_process_shop_*` and `woocommerce_process_product_*` hooks
		// fire for create and update of products and orders, so check the post
		// creation date to determine the actual event.
		$resource = get_post( absint( $arg ) );

		// Drafts don't have post_date_gmt so calculate it here.
		$gmt_date = get_gmt_from_date( $resource->post_date );

		// A resource is considered created when the hook is executed within 10 seconds of the post creation date.
		$resource_created = ( ( time() - 10 ) <= strtotime( $gmt_date ) );

		if ( 'created' === $this->get_event() && ! $resource_created ) {
			return false;
		} elseif ( 'updated' === $this->get_event() && $resource_created ) {
			return false;
		}
		return true;
	}

	/**
	 * Checks the resource for this webhook is valid e.g. valid post status.
	 *
	 * @since  3.6.0
	 * @param  mixed $arg First hook argument.
	 * @return bool       True if validation passes.
	 */
	private function is_valid_resource( $arg ) {
		$resource = $this->get_resource();

		if ( in_array( $resource, array( 'order', 'product', 'coupon' ), true ) ) {
			$status = get_post_status( absint( $arg ) );

			// Ignore auto drafts for all resources.
			if ( in_array( $status, array( 'auto-draft', 'new' ), true ) ) {
				return false;
			}

			// Ignore standard drafts for orders.
			if ( 'order' === $resource && 'draft' === $status ) {
				return false;
			}

			// Check registered order types for order types args.
			if ( 'order' === $resource && ! in_array( get_post_type( absint( $arg ) ), wc_get_order_types( 'order-webhooks' ), true ) ) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Checks if the specified resource has already been queued for delivery within the current request.
	 *
	 * Helps avoid duplication of data being sent for topics that have more than one hook defined.
	 *
	 * @param mixed $arg First hook argument.
	 *
	 * @return bool
	 */
	protected function is_already_processed( $arg ) {
		return false !== array_search( $arg, $this->processed, true );
	}

	/**
	 * Deliver the webhook payload using wp_safe_remote_request().
	 *
	 * @since 2.2.0
	 * @param mixed $arg First hook argument.
	 */
	public function deliver( $arg ) {
		$start_time = microtime( true );
		$payload    = $this->build_payload( $arg );

		// Setup request args.
		$http_args = array(
			'method'      => 'POST',
			'timeout'     => MINUTE_IN_SECONDS,
			'redirection' => 0,
			'httpversion' => '1.0',
			'blocking'    => true,
			'user-agent'  => sprintf( 'WooCommerce/%s Hookshot (WordPress/%s)', Constants::get_constant( 'WC_VERSION' ), $GLOBALS['wp_version'] ),
			'body'        => trim( wp_json_encode( $payload ) ),
			'headers'     => array(
				'Content-Type' => 'application/json',
			),
			'cookies'     => array(),
		);

		$http_args = apply_filters( 'woocommerce_webhook_http_args', $http_args, $arg, $this->get_id() );

		// Add custom headers.
		$delivery_id                                      = $this->get_new_delivery_id();
		$http_args['headers']['X-WC-Webhook-Source']      = home_url( '/' ); // Since 2.6.0.
		$http_args['headers']['X-WC-Webhook-Topic']       = $this->get_topic();
		$http_args['headers']['X-WC-Webhook-Resource']    = $this->get_resource();
		$http_args['headers']['X-WC-Webhook-Event']       = $this->get_event();
		$http_args['headers']['X-WC-Webhook-Signature']   = $this->generate_signature( $http_args['body'] );
		$http_args['headers']['X-WC-Webhook-ID']          = $this->get_id();
		$http_args['headers']['X-WC-Webhook-Delivery-ID'] = $delivery_id;

		// Webhook away!
		$response = wp_safe_remote_request( $this->get_delivery_url(), $http_args );

		$duration = NumberUtil::round( microtime( true ) - $start_time, 5 );

		$this->log_delivery( $delivery_id, $http_args, $response, $duration );

		do_action( 'woocommerce_webhook_delivery', $http_args, $response, $duration, $arg, $this->get_id() );
	}

	/**
	 * Get Legacy API payload.
	 *
	 * @since  3.0.0
	 * @param  string $resource    Resource type.
	 * @param  int    $resource_id Resource ID.
	 * @param  string $event       Event type.
	 * @return array
	 */
	private function get_legacy_api_payload( $resource, $resource_id, $event ) {
		// Include & load API classes.
		WC()->api->includes();
		WC()->api->register_resources( new WC_API_Server( '/' ) );

		switch ( $resource ) {
			case 'coupon':
				$payload = WC()->api->WC_API_Coupons->get_coupon( $resource_id );
				break;

			case 'customer':
				$payload = WC()->api->WC_API_Customers->get_customer( $resource_id );
				break;

			case 'order':
				$payload = WC()->api->WC_API_Orders->get_order( $resource_id, null, apply_filters( 'woocommerce_webhook_order_payload_filters', array() ) );
				break;

			case 'product':
				// Bulk and quick edit action hooks return a product object instead of an ID.
				if ( 'updated' === $event && is_a( $resource_id, 'WC_Product' ) ) {
					$resource_id = $resource_id->get_id();
				}
				$payload = WC()->api->WC_API_Products->get_product( $resource_id );
				break;

			// Custom topics include the first hook argument.
			case 'action':
				$payload = array(
					'action' => current( $this->get_hooks() ),
					'arg'    => $resource_id,
				);
				break;

			default:
				$payload = array();
				break;
		}

		return $payload;
	}

	/**
	 * Get WP API integration payload.
	 *
	 * @since  3.0.0
	 * @param  string $resource    Resource type.
	 * @param  int    $resource_id Resource ID.
	 * @param  string $event       Event type.
	 * @return array
	 */
	private function get_wp_api_payload( $resource, $resource_id, $event ) {
		switch ( $resource ) {
			case 'coupon':
			case 'customer':
			case 'order':
			case 'product':
				// Bulk and quick edit action hooks return a product object instead of an ID.
				if ( 'product' === $resource && 'updated' === $event && is_a( $resource_id, 'WC_Product' ) ) {
					$resource_id = $resource_id->get_id();
				}

				$version = str_replace( 'wp_api_', '', $this->get_api_version() );
				$payload = wc()->api->get_endpoint_data( "/wc/{$version}/{$resource}s/{$resource_id}" );
				break;

			// Custom topics include the first hook argument.
			case 'action':
				$payload = array(
					'action' => current( $this->get_hooks() ),
					'arg'    => $resource_id,
				);
				break;

			default:
				$payload = array();
				break;
		}

		return $payload;
	}

	/**
	 * Build the payload data for the webhook.
	 *
	 * @since  2.2.0
	 * @param  mixed $resource_id First hook argument, typically the resource ID.
	 * @return mixed              Payload data.
	 */
	public function build_payload( $resource_id ) {
		// Build the payload with the same user context as the user who created
		// the webhook -- this avoids permission errors as background processing
		// runs with no user context.
		$current_user = get_current_user_id();
		wp_set_current_user( $this->get_user_id() );

		$resource = $this->get_resource();
		$event    = $this->get_event();

		// If a resource has been deleted, just include the ID.
		if ( 'deleted' === $event ) {
			$payload = array(
				'id' => $resource_id,
			);
		} else {
			if ( in_array( $this->get_api_version(), wc_get_webhook_rest_api_versions(), true ) ) {
				$payload = $this->get_wp_api_payload( $resource, $resource_id, $event );
			} else {
				$payload = $this->get_legacy_api_payload( $resource, $resource_id, $event );
			}
		}

		// Restore the current user.
		wp_set_current_user( $current_user );

		return apply_filters( 'woocommerce_webhook_payload', $payload, $resource, $resource_id, $this->get_id() );
	}

	/**
	 * Generate a base64-encoded HMAC-SHA256 signature of the payload body so the
	 * recipient can verify the authenticity of the webhook. Note that the signature
	 * is calculated after the body has already been encoded (JSON by default).
	 *
	 * @since  2.2.0
	 * @param  string $payload Payload data to hash.
	 * @return string
	 */
	public function generate_signature( $payload ) {
		$hash_algo = apply_filters( 'woocommerce_webhook_hash_algorithm', 'sha256', $payload, $this->get_id() );

		// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
		return base64_encode( hash_hmac( $hash_algo, $payload, wp_specialchars_decode( $this->get_secret(), ENT_QUOTES ), true ) );
	}

	/**
	 * Generate a new unique hash as a delivery id based on current time and wehbook id.
	 * Return the hash for inclusion in the webhook request.
	 *
	 * @since  2.2.0
	 * @return string
	 */
	public function get_new_delivery_id() {
		// Since we no longer use comments to store delivery logs, we generate a unique hash instead based on current time and webhook ID.
		return wp_hash( $this->get_id() . strtotime( 'now' ) );
	}

	/**
	 * Log the delivery request/response.
	 *
	 * @since 2.2.0
	 * @param string         $delivery_id Previously created hash.
	 * @param array          $request     Request data.
	 * @param array|WP_Error $response    Response data.
	 * @param float          $duration    Request duration.
	 */
	public function log_delivery( $delivery_id, $request, $response, $duration ) {
		$logger  = wc_get_logger();
		$message = array(
			'Webhook Delivery' => array(
				'Delivery ID' => $delivery_id,
				'Date'        => date_i18n( __( 'M j, Y @ G:i', 'woocommerce' ), strtotime( 'now' ), true ),
				'URL'         => $this->get_delivery_url(),
				'Duration'    => $duration,
				'Request'     => array(
					'Method'  => $request['method'],
					'Headers' => array_merge(
						array(
							'User-Agent' => $request['user-agent'],
						),
						$request['headers']
					),
				),
				'Body'        => wp_slash( $request['body'] ),
			),
		);

		// Parse response.
		if ( is_wp_error( $response ) ) {
			$response_code    = $response->get_error_code();
			$response_message = $response->get_error_message();
			$response_headers = array();
			$response_body    = '';
		} else {
			$response_code    = wp_remote_retrieve_response_code( $response );
			$response_message = wp_remote_retrieve_response_message( $response );
			$response_headers = wp_remote_retrieve_headers( $response );
			$response_body    = wp_remote_retrieve_body( $response );
		}

		$message['Webhook Delivery']['Response'] = array(
			'Code'    => $response_code,
			'Message' => $response_message,
			'Headers' => $response_headers,
			'Body'    => $response_body,
		);

		if ( ! Constants::is_true( 'WP_DEBUG' ) ) {
			$message['Webhook Delivery']['Body']             = 'Webhook body is not logged unless WP_DEBUG mode is turned on. This is to avoid the storing of personal data in the logs.';
			$message['Webhook Delivery']['Response']['Body'] = 'Webhook body is not logged unless WP_DEBUG mode is turned on. This is to avoid the storing of personal data in the logs.';
		}

		$logger->info(
			wc_print_r( $message, true ),
			array(
				'source' => 'webhooks-delivery',
			)
		);

		// Track failures.
		// Check for a success, which is a 2xx, 301 or 302 Response Code.
		if ( intval( $response_code ) >= 200 && intval( $response_code ) < 303 ) {
			$this->set_failure_count( 0 );
			$this->save();
		} else {
			$this->failed_delivery();
		}
	}

	/**
	 * Track consecutive delivery failures and automatically disable the webhook.
	 * if more than 5 consecutive failures occur. A failure is defined as a.
	 * non-2xx response.
	 *
	 * @since 2.2.0
	 */
	private function failed_delivery() {
		$failures = $this->get_failure_count();

		if ( $failures > apply_filters( 'woocommerce_max_webhook_delivery_failures', 5 ) ) {
			$this->set_status( 'disabled' );

			do_action( 'woocommerce_webhook_disabled_due_delivery_failures', $this->get_id() );
		} else {
			$this->set_failure_count( ++$failures );
		}

		$this->save();
	}

	/**
	 * Get the delivery logs for this webhook.
	 *
	 * @since  3.3.0
	 * @return string
	 */
	public function get_delivery_logs() {
		return esc_url( add_query_arg( 'log_file', wc_get_log_file_name( 'webhooks-delivery' ), admin_url( 'admin.php?page=wc-status&tab=logs' ) ) );
	}

	/**
	 * Get the delivery log specified by the ID. The delivery log includes:
	 *
	 * + duration
	 * + summary
	 * + request method/url
	 * + request headers/body
	 * + response code/message/headers/body
	 *
	 * @since 2.2
	 * @deprecated 3.3.0
	 * @param int $delivery_id Delivery ID.
	 * @return void
	 */
	public function get_delivery_log( $delivery_id ) {
		wc_deprecated_function( 'WC_Webhook::get_delivery_log', '3.3' );
	}

	/**
	 * Send a test ping to the delivery URL, sent when the webhook is first created.
	 *
	 * @since  2.2.0
	 * @return bool|WP_Error
	 */
	public function deliver_ping() {
		$args = array(
			'user-agent' => sprintf( 'WooCommerce/%s Hookshot (WordPress/%s)', Constants::get_constant( 'WC_VERSION' ), $GLOBALS['wp_version'] ),
			'body'       => 'webhook_id=' . $this->get_id(),
		);

		$test          = wp_safe_remote_post( $this->get_delivery_url(), $args );
		$response_code = wp_remote_retrieve_response_code( $test );

		if ( is_wp_error( $test ) ) {
			/* translators: error message */
			return new WP_Error( 'error', sprintf( __( 'Error: Delivery URL cannot be reached: %s', 'woocommerce' ), $test->get_error_message() ) );
		}

		if ( 200 !== $response_code ) {
			/* translators: error message */
			return new WP_Error( 'error', sprintf( __( 'Error: Delivery URL returned response code: %s', 'woocommerce' ), absint( $response_code ) ) );
		}

		$this->set_pending_delivery( false );
		$this->save();

		return true;
	}

	/*
	|--------------------------------------------------------------------------
	| Getters
	|--------------------------------------------------------------------------
	*/

	/**
	 * Get the friendly name for the webhook.
	 *
	 * @since  2.2.0
	 * @param  string $context What the value is for.
	 *                         Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_name( $context = 'view' ) {
		return apply_filters( 'woocommerce_webhook_name', $this->get_prop( 'name', $context ), $this->get_id() );
	}

	/**
	 * Get the webhook status.
	 *
	 * - 'active' - delivers payload.
	 * - 'paused' - does not deliver payload, paused by admin.
	 * - 'disabled' - does not delivery payload, paused automatically due to consecutive failures.
	 *
	 * @since  2.2.0
	 * @param  string $context What the value is for.
	 *                         Valid values are 'view' and 'edit'.
	 * @return string status
	 */
	public function get_status( $context = 'view' ) {
		return apply_filters( 'woocommerce_webhook_status', $this->get_prop( 'status', $context ), $this->get_id() );
	}

	/**
	 * Get webhook created date.
	 *
	 * @since  3.2.0
	 * @param  string $context  What the value is for.
	 *                          Valid values are 'view' and 'edit'.
	 * @return WC_DateTime|null Object if the date is set or null if there is no date.
	 */
	public function get_date_created( $context = 'view' ) {
		return $this->get_prop( 'date_created', $context );
	}

	/**
	 * Get webhook modified date.
	 *
	 * @since  3.2.0
	 * @param  string $context  What the value is for.
	 *                          Valid values are 'view' and 'edit'.
	 * @return WC_DateTime|null Object if the date is set or null if there is no date.
	 */
	public function get_date_modified( $context = 'view' ) {
		return $this->get_prop( 'date_modified', $context );
	}

	/**
	 * Get the secret used for generating the HMAC-SHA256 signature.
	 *
	 * @since  2.2.0
	 * @param  string $context What the value is for.
	 *                         Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_secret( $context = 'view' ) {
		return apply_filters( 'woocommerce_webhook_secret', $this->get_prop( 'secret', $context ), $this->get_id() );
	}

	/**
	 * Get the webhook topic, e.g. `order.created`.
	 *
	 * @since  2.2.0
	 * @param  string $context What the value is for.
	 *                         Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_topic( $context = 'view' ) {
		return apply_filters( 'woocommerce_webhook_topic', $this->get_prop( 'topic', $context ), $this->get_id() );
	}

	/**
	 * Get the delivery URL.
	 *
	 * @since  2.2.0
	 * @param  string $context What the value is for.
	 *                         Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_delivery_url( $context = 'view' ) {
		return apply_filters( 'woocommerce_webhook_delivery_url', $this->get_prop( 'delivery_url', $context ), $this->get_id() );
	}

	/**
	 * Get the user ID for this webhook.
	 *
	 * @since  2.2.0
	 * @param  string $context What the value is for.
	 *                         Valid values are 'view' and 'edit'.
	 * @return int
	 */
	public function get_user_id( $context = 'view' ) {
		return $this->get_prop( 'user_id', $context );
	}

	/**
	 * API version.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for.
	 *                         Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_api_version( $context = 'view' ) {
		$version = $this->get_prop( 'api_version', $context );

		return 0 < $version ? 'wp_api_v' . $version : 'legacy_v3';
	}

	/**
	 * Get the failure count.
	 *
	 * @since  2.2.0
	 * @param  string $context What the value is for.
	 *                         Valid values are 'view' and 'edit'.
	 * @return int
	 */
	public function get_failure_count( $context = 'view' ) {
		return $this->get_prop( 'failure_count', $context );
	}

	/**
	 * Get pending delivery.
	 *
	 * @since  3.2.0
	 * @param  string $context What the value is for.
	 *                         Valid values are 'view' and 'edit'.
	 * @return bool
	 */
	public function get_pending_delivery( $context = 'view' ) {
		return $this->get_prop( 'pending_delivery', $context );
	}

	/*
	|--------------------------------------------------------------------------
	| Setters
	|--------------------------------------------------------------------------
	 */

	/**
	 * Set webhook name.
	 *
	 * @since 3.2.0
	 * @param string $name Webhook name.
	 */
	public function set_name( $name ) {
		$this->set_prop( 'name', $name );
	}

	/**
	 * Set webhook created date.
	 *
	 * @since 3.2.0
	 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime.
	 *                                  If the DateTime string has no timezone or offset,
	 *                                  WordPress site timezone will be assumed.
	 *                                  Null if their is no date.
	 */
	public function set_date_created( $date = null ) {
		$this->set_date_prop( 'date_created', $date );
	}

	/**
	 * Set webhook modified date.
	 *
	 * @since 3.2.0
	 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime.
	 *                                  If the DateTime string has no timezone or offset,
	 *                                  WordPress site timezone will be assumed.
	 *                                  Null if their is no date.
	 */
	public function set_date_modified( $date = null ) {
		$this->set_date_prop( 'date_modified', $date );
	}

	/**
	 * Set status.
	 *
	 * @since 3.2.0
	 * @param string $status Status.
	 */
	public function set_status( $status ) {
		if ( ! array_key_exists( $status, wc_get_webhook_statuses() ) ) {
			$status = 'disabled';
		}

		$this->set_prop( 'status', $status );
	}

	/**
	 * Set the secret used for generating the HMAC-SHA256 signature.
	 *
	 * @since 2.2.0
	 * @param string $secret Secret.
	 */
	public function set_secret( $secret ) {
		$this->set_prop( 'secret', $secret );
	}

	/**
	 * Set the webhook topic and associated hooks.
	 * The topic resource & event are also saved separately.
	 *
	 * @since 2.2.0
	 * @param string $topic Webhook topic.
	 */
	public function set_topic( $topic ) {
		$topic = wc_clean( $topic );

		if ( ! wc_is_webhook_valid_topic( $topic ) ) {
			$topic = '';
		}

		$this->set_prop( 'topic', $topic );
	}

	/**
	 * Set the delivery URL.
	 *
	 * @since 2.2.0
	 * @param string $url Delivery URL.
	 */
	public function set_delivery_url( $url ) {
		$this->set_prop( 'delivery_url', esc_url_raw( $url, array( 'http', 'https' ) ) );
	}

	/**
	 * Set user ID.
	 *
	 * @since 3.2.0
	 * @param int $user_id User ID.
	 */
	public function set_user_id( $user_id ) {
		$this->set_prop( 'user_id', (int) $user_id );
	}

	/**
	 * Set API version.
	 *
	 * @since 3.0.0
	 * @param int|string $version REST API version.
	 */
	public function set_api_version( $version ) {
		if ( ! is_numeric( $version ) ) {
			$version = $this->data_store->get_api_version_number( $version );
		}

		$this->set_prop( 'api_version', (int) $version );
	}

	/**
	 * Set pending delivery.
	 *
	 * @since 3.2.0
	 * @param bool $pending_delivery Set true if is pending for delivery.
	 */
	public function set_pending_delivery( $pending_delivery ) {
		$this->set_prop( 'pending_delivery', (bool) $pending_delivery );
	}

	/**
	 * Set failure count.
	 *
	 * @since 3.2.0
	 * @param bool $failure_count Total of failures.
	 */
	public function set_failure_count( $failure_count ) {
		$this->set_prop( 'failure_count', intval( $failure_count ) );
	}

	/*
	|--------------------------------------------------------------------------
	| Non-CRUD Getters
	|--------------------------------------------------------------------------
	*/

	/**
	 * Get the associated hook names for a topic.
	 *
	 * @since  2.2.0
	 * @param  string $topic Topic name.
	 * @return array
	 */
	private function get_topic_hooks( $topic ) {
		$topic_hooks = array(
			'coupon.created'   => array(
				'woocommerce_process_shop_coupon_meta',
				'woocommerce_new_coupon',
			),
			'coupon.updated'   => array(
				'woocommerce_process_shop_coupon_meta',
				'woocommerce_update_coupon',
			),
			'coupon.deleted'   => array(
				'wp_trash_post',
			),
			'coupon.restored'  => array(
				'untrashed_post',
			),
			'customer.created' => array(
				'user_register',
				'woocommerce_created_customer',
				'woocommerce_new_customer',
			),
			'customer.updated' => array(
				'profile_update',
				'woocommerce_update_customer',
			),
			'customer.deleted' => array(
				'delete_user',
			),
			'order.created'    => array(
				'woocommerce_new_order',
			),
			'order.updated'    => array(
				'woocommerce_update_order',
				'woocommerce_order_refunded',
			),
			'order.deleted'    => array(
				'wp_trash_post',
			),
			'order.restored'   => array(
				'untrashed_post',
			),
			'product.created'  => array(
				'woocommerce_process_product_meta',
				'woocommerce_new_product',
				'woocommerce_new_product_variation',
			),
			'product.updated'  => array(
				'woocommerce_process_product_meta',
				'woocommerce_update_product',
				'woocommerce_update_product_variation',
			),
			'product.deleted'  => array(
				'wp_trash_post',
			),
			'product.restored' => array(
				'untrashed_post',
			),
		);

		$topic_hooks = apply_filters( 'woocommerce_webhook_topic_hooks', $topic_hooks, $this );

		return isset( $topic_hooks[ $topic ] ) ? $topic_hooks[ $topic ] : array();
	}

	/**
	 * Get the hook names for the webhook.
	 *
	 * @since  2.2.0
	 * @return array
	 */
	public function get_hooks() {
		if ( 'action' === $this->get_resource() ) {
			$hooks = array( $this->get_event() );
		} else {
			$hooks = $this->get_topic_hooks( $this->get_topic() );
		}

		return apply_filters( 'woocommerce_webhook_hooks', $hooks, $this->get_id() );
	}

	/**
	 * Get the resource for the webhook, e.g. `order`.
	 *
	 * @since  2.2.0
	 * @return string
	 */
	public function get_resource() {
		$topic = explode( '.', $this->get_topic() );

		return apply_filters( 'woocommerce_webhook_resource', $topic[0], $this->get_id() );
	}

	/**
	 * Get the event for the webhook, e.g. `created`.
	 *
	 * @since  2.2.0
	 * @return string
	 */
	public function get_event() {
		$topic = explode( '.', $this->get_topic() );

		return apply_filters( 'woocommerce_webhook_event', isset( $topic[1] ) ? $topic[1] : '', $this->get_id() );
	}

	/**
	 * Get the webhook i18n status.
	 *
	 * @return string
	 */
	public function get_i18n_status() {
		$status   = $this->get_status();
		$statuses = wc_get_webhook_statuses();

		return isset( $statuses[ $status ] ) ? $statuses[ $status ] : $status;
	}
}
class-wc-geo-ip.php000064400000074645151336044760010200 0ustar00<?php
/**
 * Geo IP class
 *
 * This class is a fork of GeoIP class from MaxMind LLC.
 *
 * @package    WooCommerce\Classes
 * @version    2.4.0
 * @deprecated 3.4.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC_Geo_IP Class.
 *
 * @deprecated 3.4.0
 */
class WC_Geo_IP {

	const GEOIP_COUNTRY_BEGIN            = 16776960;
	const GEOIP_STATE_BEGIN_REV0         = 16700000;
	const GEOIP_STATE_BEGIN_REV1         = 16000000;
	const GEOIP_MEMORY_CACHE             = 1;
	const GEOIP_SHARED_MEMORY            = 2;
	const STRUCTURE_INFO_MAX_SIZE        = 20;
	const GEOIP_COUNTRY_EDITION          = 1;
	const GEOIP_PROXY_EDITION            = 8;
	const GEOIP_ASNUM_EDITION            = 9;
	const GEOIP_NETSPEED_EDITION         = 10;
	const GEOIP_REGION_EDITION_REV0      = 7;
	const GEOIP_REGION_EDITION_REV1      = 3;
	const GEOIP_CITY_EDITION_REV0        = 6;
	const GEOIP_CITY_EDITION_REV1        = 2;
	const GEOIP_ORG_EDITION              = 5;
	const GEOIP_ISP_EDITION              = 4;
	const SEGMENT_RECORD_LENGTH          = 3;
	const STANDARD_RECORD_LENGTH         = 3;
	const ORG_RECORD_LENGTH              = 4;
	const GEOIP_SHM_KEY                  = 0x4f415401;
	const GEOIP_DOMAIN_EDITION           = 11;
	const GEOIP_COUNTRY_EDITION_V6       = 12;
	const GEOIP_LOCATIONA_EDITION        = 13;
	const GEOIP_ACCURACYRADIUS_EDITION   = 14;
	const GEOIP_CITY_EDITION_REV1_V6     = 30;
	const GEOIP_CITY_EDITION_REV0_V6     = 31;
	const GEOIP_NETSPEED_EDITION_REV1    = 32;
	const GEOIP_NETSPEED_EDITION_REV1_V6 = 33;
	const GEOIP_USERTYPE_EDITION         = 28;
	const GEOIP_USERTYPE_EDITION_V6      = 29;
	const GEOIP_ASNUM_EDITION_V6         = 21;
	const GEOIP_ISP_EDITION_V6           = 22;
	const GEOIP_ORG_EDITION_V6           = 23;
	const GEOIP_DOMAIN_EDITION_V6        = 24;

	/**
	 * Flags.
	 *
	 * @var int
	 */
	public $flags;

	/**
	 * File handler.
	 *
	 * @var resource
	 */
	public $filehandle;

	/**
	 * Memory buffer.
	 *
	 * @var string
	 */
	public $memory_buffer;

	/**
	 * Database type.
	 *
	 * @var int
	 */
	public $databaseType;

	/**
	 * Database segments.
	 *
	 * @var int
	 */
	public $databaseSegments;

	/**
	 * Record length.
	 *
	 * @var int
	 */
	public $record_length;

	/**
	 * Shmid.
	 *
	 * @var string
	 */
	public $shmid;

	/**
	 * Two letters country codes.
	 *
	 * @var array
	 */
	public $GEOIP_COUNTRY_CODES = array(
		'',
		'AP',
		'EU',
		'AD',
		'AE',
		'AF',
		'AG',
		'AI',
		'AL',
		'AM',
		'CW',
		'AO',
		'AQ',
		'AR',
		'AS',
		'AT',
		'AU',
		'AW',
		'AZ',
		'BA',
		'BB',
		'BD',
		'BE',
		'BF',
		'BG',
		'BH',
		'BI',
		'BJ',
		'BM',
		'BN',
		'BO',
		'BR',
		'BS',
		'BT',
		'BV',
		'BW',
		'BY',
		'BZ',
		'CA',
		'CC',
		'CD',
		'CF',
		'CG',
		'CH',
		'CI',
		'CK',
		'CL',
		'CM',
		'CN',
		'CO',
		'CR',
		'CU',
		'CV',
		'CX',
		'CY',
		'CZ',
		'DE',
		'DJ',
		'DK',
		'DM',
		'DO',
		'DZ',
		'EC',
		'EE',
		'EG',
		'EH',
		'ER',
		'ES',
		'ET',
		'FI',
		'FJ',
		'FK',
		'FM',
		'FO',
		'FR',
		'SX',
		'GA',
		'GB',
		'GD',
		'GE',
		'GF',
		'GH',
		'GI',
		'GL',
		'GM',
		'GN',
		'GP',
		'GQ',
		'GR',
		'GS',
		'GT',
		'GU',
		'GW',
		'GY',
		'HK',
		'HM',
		'HN',
		'HR',
		'HT',
		'HU',
		'ID',
		'IE',
		'IL',
		'IN',
		'IO',
		'IQ',
		'IR',
		'IS',
		'IT',
		'JM',
		'JO',
		'JP',
		'KE',
		'KG',
		'KH',
		'KI',
		'KM',
		'KN',
		'KP',
		'KR',
		'KW',
		'KY',
		'KZ',
		'LA',
		'LB',
		'LC',
		'LI',
		'LK',
		'LR',
		'LS',
		'LT',
		'LU',
		'LV',
		'LY',
		'MA',
		'MC',
		'MD',
		'MG',
		'MH',
		'MK',
		'ML',
		'MM',
		'MN',
		'MO',
		'MP',
		'MQ',
		'MR',
		'MS',
		'MT',
		'MU',
		'MV',
		'MW',
		'MX',
		'MY',
		'MZ',
		'NA',
		'NC',
		'NE',
		'NF',
		'NG',
		'NI',
		'NL',
		'NO',
		'NP',
		'NR',
		'NU',
		'NZ',
		'OM',
		'PA',
		'PE',
		'PF',
		'PG',
		'PH',
		'PK',
		'PL',
		'PM',
		'PN',
		'PR',
		'PS',
		'PT',
		'PW',
		'PY',
		'QA',
		'RE',
		'RO',
		'RU',
		'RW',
		'SA',
		'SB',
		'SC',
		'SD',
		'SE',
		'SG',
		'SH',
		'SI',
		'SJ',
		'SK',
		'SL',
		'SM',
		'SN',
		'SO',
		'SR',
		'ST',
		'SV',
		'SY',
		'SZ',
		'TC',
		'TD',
		'TF',
		'TG',
		'TH',
		'TJ',
		'TK',
		'TM',
		'TN',
		'TO',
		'TL',
		'TR',
		'TT',
		'TV',
		'TW',
		'TZ',
		'UA',
		'UG',
		'UM',
		'US',
		'UY',
		'UZ',
		'VA',
		'VC',
		'VE',
		'VG',
		'VI',
		'VN',
		'VU',
		'WF',
		'WS',
		'YE',
		'YT',
		'RS',
		'ZA',
		'ZM',
		'ME',
		'ZW',
		'A1',
		'A2',
		'O1',
		'AX',
		'GG',
		'IM',
		'JE',
		'BL',
		'MF',
		'BQ',
		'SS',
		'O1',
	);

	/**
	 * 3 letters country codes.
	 *
	 * @var array
	 */
	public $GEOIP_COUNTRY_CODES3 = array(
		'',
		'AP',
		'EU',
		'AND',
		'ARE',
		'AFG',
		'ATG',
		'AIA',
		'ALB',
		'ARM',
		'CUW',
		'AGO',
		'ATA',
		'ARG',
		'ASM',
		'AUT',
		'AUS',
		'ABW',
		'AZE',
		'BIH',
		'BRB',
		'BGD',
		'BEL',
		'BFA',
		'BGR',
		'BHR',
		'BDI',
		'BEN',
		'BMU',
		'BRN',
		'BOL',
		'BRA',
		'BHS',
		'BTN',
		'BVT',
		'BWA',
		'BLR',
		'BLZ',
		'CAN',
		'CCK',
		'COD',
		'CAF',
		'COG',
		'CHE',
		'CIV',
		'COK',
		'CHL',
		'CMR',
		'CHN',
		'COL',
		'CRI',
		'CUB',
		'CPV',
		'CXR',
		'CYP',
		'CZE',
		'DEU',
		'DJI',
		'DNK',
		'DMA',
		'DOM',
		'DZA',
		'ECU',
		'EST',
		'EGY',
		'ESH',
		'ERI',
		'ESP',
		'ETH',
		'FIN',
		'FJI',
		'FLK',
		'FSM',
		'FRO',
		'FRA',
		'SXM',
		'GAB',
		'GBR',
		'GRD',
		'GEO',
		'GUF',
		'GHA',
		'GIB',
		'GRL',
		'GMB',
		'GIN',
		'GLP',
		'GNQ',
		'GRC',
		'SGS',
		'GTM',
		'GUM',
		'GNB',
		'GUY',
		'HKG',
		'HMD',
		'HND',
		'HRV',
		'HTI',
		'HUN',
		'IDN',
		'IRL',
		'ISR',
		'IND',
		'IOT',
		'IRQ',
		'IRN',
		'ISL',
		'ITA',
		'JAM',
		'JOR',
		'JPN',
		'KEN',
		'KGZ',
		'KHM',
		'KIR',
		'COM',
		'KNA',
		'PRK',
		'KOR',
		'KWT',
		'CYM',
		'KAZ',
		'LAO',
		'LBN',
		'LCA',
		'LIE',
		'LKA',
		'LBR',
		'LSO',
		'LTU',
		'LUX',
		'LVA',
		'LBY',
		'MAR',
		'MCO',
		'MDA',
		'MDG',
		'MHL',
		'MKD',
		'MLI',
		'MMR',
		'MNG',
		'MAC',
		'MNP',
		'MTQ',
		'MRT',
		'MSR',
		'MLT',
		'MUS',
		'MDV',
		'MWI',
		'MEX',
		'MYS',
		'MOZ',
		'NAM',
		'NCL',
		'NER',
		'NFK',
		'NGA',
		'NIC',
		'NLD',
		'NOR',
		'NPL',
		'NRU',
		'NIU',
		'NZL',
		'OMN',
		'PAN',
		'PER',
		'PYF',
		'PNG',
		'PHL',
		'PAK',
		'POL',
		'SPM',
		'PCN',
		'PRI',
		'PSE',
		'PRT',
		'PLW',
		'PRY',
		'QAT',
		'REU',
		'ROU',
		'RUS',
		'RWA',
		'SAU',
		'SLB',
		'SYC',
		'SDN',
		'SWE',
		'SGP',
		'SHN',
		'SVN',
		'SJM',
		'SVK',
		'SLE',
		'SMR',
		'SEN',
		'SOM',
		'SUR',
		'STP',
		'SLV',
		'SYR',
		'SWZ',
		'TCA',
		'TCD',
		'ATF',
		'TGO',
		'THA',
		'TJK',
		'TKL',
		'TKM',
		'TUN',
		'TON',
		'TLS',
		'TUR',
		'TTO',
		'TUV',
		'TWN',
		'TZA',
		'UKR',
		'UGA',
		'UMI',
		'USA',
		'URY',
		'UZB',
		'VAT',
		'VCT',
		'VEN',
		'VGB',
		'VIR',
		'VNM',
		'VUT',
		'WLF',
		'WSM',
		'YEM',
		'MYT',
		'SRB',
		'ZAF',
		'ZMB',
		'MNE',
		'ZWE',
		'A1',
		'A2',
		'O1',
		'ALA',
		'GGY',
		'IMN',
		'JEY',
		'BLM',
		'MAF',
		'BES',
		'SSD',
		'O1',
	);

	/**
	 * Contry names.
	 *
	 * @var array
	 */
	public $GEOIP_COUNTRY_NAMES = array(
		'',
		'Asia/Pacific Region',
		'Europe',
		'Andorra',
		'United Arab Emirates',
		'Afghanistan',
		'Antigua and Barbuda',
		'Anguilla',
		'Albania',
		'Armenia',
		'Curacao',
		'Angola',
		'Antarctica',
		'Argentina',
		'American Samoa',
		'Austria',
		'Australia',
		'Aruba',
		'Azerbaijan',
		'Bosnia and Herzegovina',
		'Barbados',
		'Bangladesh',
		'Belgium',
		'Burkina Faso',
		'Bulgaria',
		'Bahrain',
		'Burundi',
		'Benin',
		'Bermuda',
		'Brunei Darussalam',
		'Bolivia',
		'Brazil',
		'Bahamas',
		'Bhutan',
		'Bouvet Island',
		'Botswana',
		'Belarus',
		'Belize',
		'Canada',
		'Cocos (Keeling) Islands',
		'Congo, The Democratic Republic of the',
		'Central African Republic',
		'Congo',
		'Switzerland',
		"Cote D'Ivoire",
		'Cook Islands',
		'Chile',
		'Cameroon',
		'China',
		'Colombia',
		'Costa Rica',
		'Cuba',
		'Cape Verde',
		'Christmas Island',
		'Cyprus',
		'Czech Republic',
		'Germany',
		'Djibouti',
		'Denmark',
		'Dominica',
		'Dominican Republic',
		'Algeria',
		'Ecuador',
		'Estonia',
		'Egypt',
		'Western Sahara',
		'Eritrea',
		'Spain',
		'Ethiopia',
		'Finland',
		'Fiji',
		'Falkland Islands (Malvinas)',
		'Micronesia, Federated States of',
		'Faroe Islands',
		'France',
		'Sint Maarten (Dutch part)',
		'Gabon',
		'United Kingdom',
		'Grenada',
		'Georgia',
		'French Guiana',
		'Ghana',
		'Gibraltar',
		'Greenland',
		'Gambia',
		'Guinea',
		'Guadeloupe',
		'Equatorial Guinea',
		'Greece',
		'South Georgia and the South Sandwich Islands',
		'Guatemala',
		'Guam',
		'Guinea-Bissau',
		'Guyana',
		'Hong Kong',
		'Heard Island and McDonald Islands',
		'Honduras',
		'Croatia',
		'Haiti',
		'Hungary',
		'Indonesia',
		'Ireland',
		'Israel',
		'India',
		'British Indian Ocean Territory',
		'Iraq',
		'Iran, Islamic Republic of',
		'Iceland',
		'Italy',
		'Jamaica',
		'Jordan',
		'Japan',
		'Kenya',
		'Kyrgyzstan',
		'Cambodia',
		'Kiribati',
		'Comoros',
		'Saint Kitts and Nevis',
		"Korea, Democratic People's Republic of",
		'Korea, Republic of',
		'Kuwait',
		'Cayman Islands',
		'Kazakhstan',
		"Lao People's Democratic Republic",
		'Lebanon',
		'Saint Lucia',
		'Liechtenstein',
		'Sri Lanka',
		'Liberia',
		'Lesotho',
		'Lithuania',
		'Luxembourg',
		'Latvia',
		'Libya',
		'Morocco',
		'Monaco',
		'Moldova, Republic of',
		'Madagascar',
		'Marshall Islands',
		'Macedonia',
		'Mali',
		'Myanmar',
		'Mongolia',
		'Macau',
		'Northern Mariana Islands',
		'Martinique',
		'Mauritania',
		'Montserrat',
		'Malta',
		'Mauritius',
		'Maldives',
		'Malawi',
		'Mexico',
		'Malaysia',
		'Mozambique',
		'Namibia',
		'New Caledonia',
		'Niger',
		'Norfolk Island',
		'Nigeria',
		'Nicaragua',
		'Netherlands',
		'Norway',
		'Nepal',
		'Nauru',
		'Niue',
		'New Zealand',
		'Oman',
		'Panama',
		'Peru',
		'French Polynesia',
		'Papua New Guinea',
		'Philippines',
		'Pakistan',
		'Poland',
		'Saint Pierre and Miquelon',
		'Pitcairn Islands',
		'Puerto Rico',
		'Palestinian Territory',
		'Portugal',
		'Palau',
		'Paraguay',
		'Qatar',
		'Reunion',
		'Romania',
		'Russian Federation',
		'Rwanda',
		'Saudi Arabia',
		'Solomon Islands',
		'Seychelles',
		'Sudan',
		'Sweden',
		'Singapore',
		'Saint Helena',
		'Slovenia',
		'Svalbard and Jan Mayen',
		'Slovakia',
		'Sierra Leone',
		'San Marino',
		'Senegal',
		'Somalia',
		'Suriname',
		'Sao Tome and Principe',
		'El Salvador',
		'Syrian Arab Republic',
		'Swaziland',
		'Turks and Caicos Islands',
		'Chad',
		'French Southern Territories',
		'Togo',
		'Thailand',
		'Tajikistan',
		'Tokelau',
		'Turkmenistan',
		'Tunisia',
		'Tonga',
		'Timor-Leste',
		'Turkey',
		'Trinidad and Tobago',
		'Tuvalu',
		'Taiwan',
		'Tanzania, United Republic of',
		'Ukraine',
		'Uganda',
		'United States Minor Outlying Islands',
		'United States',
		'Uruguay',
		'Uzbekistan',
		'Holy See (Vatican City State)',
		'Saint Vincent and the Grenadines',
		'Venezuela',
		'Virgin Islands, British',
		'Virgin Islands, U.S.',
		'Vietnam',
		'Vanuatu',
		'Wallis and Futuna',
		'Samoa',
		'Yemen',
		'Mayotte',
		'Serbia',
		'South Africa',
		'Zambia',
		'Montenegro',
		'Zimbabwe',
		'Anonymous Proxy',
		'Satellite Provider',
		'Other',
		'Aland Islands',
		'Guernsey',
		'Isle of Man',
		'Jersey',
		'Saint Barthelemy',
		'Saint Martin',
		'Bonaire, Saint Eustatius and Saba',
		'South Sudan',
		'Other',
	);

	/**
	 * 2 letters continent codes.
	 *
	 * @var array
	 */
	public $GEOIP_CONTINENT_CODES = array(
		'--',
		'AS',
		'EU',
		'EU',
		'AS',
		'AS',
		'NA',
		'NA',
		'EU',
		'AS',
		'NA',
		'AF',
		'AN',
		'SA',
		'OC',
		'EU',
		'OC',
		'NA',
		'AS',
		'EU',
		'NA',
		'AS',
		'EU',
		'AF',
		'EU',
		'AS',
		'AF',
		'AF',
		'NA',
		'AS',
		'SA',
		'SA',
		'NA',
		'AS',
		'AN',
		'AF',
		'EU',
		'NA',
		'NA',
		'AS',
		'AF',
		'AF',
		'AF',
		'EU',
		'AF',
		'OC',
		'SA',
		'AF',
		'AS',
		'SA',
		'NA',
		'NA',
		'AF',
		'AS',
		'AS',
		'EU',
		'EU',
		'AF',
		'EU',
		'NA',
		'NA',
		'AF',
		'SA',
		'EU',
		'AF',
		'AF',
		'AF',
		'EU',
		'AF',
		'EU',
		'OC',
		'SA',
		'OC',
		'EU',
		'EU',
		'NA',
		'AF',
		'EU',
		'NA',
		'AS',
		'SA',
		'AF',
		'EU',
		'NA',
		'AF',
		'AF',
		'NA',
		'AF',
		'EU',
		'AN',
		'NA',
		'OC',
		'AF',
		'SA',
		'AS',
		'AN',
		'NA',
		'EU',
		'NA',
		'EU',
		'AS',
		'EU',
		'AS',
		'AS',
		'AS',
		'AS',
		'AS',
		'EU',
		'EU',
		'NA',
		'AS',
		'AS',
		'AF',
		'AS',
		'AS',
		'OC',
		'AF',
		'NA',
		'AS',
		'AS',
		'AS',
		'NA',
		'AS',
		'AS',
		'AS',
		'NA',
		'EU',
		'AS',
		'AF',
		'AF',
		'EU',
		'EU',
		'EU',
		'AF',
		'AF',
		'EU',
		'EU',
		'AF',
		'OC',
		'EU',
		'AF',
		'AS',
		'AS',
		'AS',
		'OC',
		'NA',
		'AF',
		'NA',
		'EU',
		'AF',
		'AS',
		'AF',
		'NA',
		'AS',
		'AF',
		'AF',
		'OC',
		'AF',
		'OC',
		'AF',
		'NA',
		'EU',
		'EU',
		'AS',
		'OC',
		'OC',
		'OC',
		'AS',
		'NA',
		'SA',
		'OC',
		'OC',
		'AS',
		'AS',
		'EU',
		'NA',
		'OC',
		'NA',
		'AS',
		'EU',
		'OC',
		'SA',
		'AS',
		'AF',
		'EU',
		'EU',
		'AF',
		'AS',
		'OC',
		'AF',
		'AF',
		'EU',
		'AS',
		'AF',
		'EU',
		'EU',
		'EU',
		'AF',
		'EU',
		'AF',
		'AF',
		'SA',
		'AF',
		'NA',
		'AS',
		'AF',
		'NA',
		'AF',
		'AN',
		'AF',
		'AS',
		'AS',
		'OC',
		'AS',
		'AF',
		'OC',
		'AS',
		'EU',
		'NA',
		'OC',
		'AS',
		'AF',
		'EU',
		'AF',
		'OC',
		'NA',
		'SA',
		'AS',
		'EU',
		'NA',
		'SA',
		'NA',
		'NA',
		'AS',
		'OC',
		'OC',
		'OC',
		'AS',
		'AF',
		'EU',
		'AF',
		'AF',
		'EU',
		'AF',
		'--',
		'--',
		'--',
		'EU',
		'EU',
		'EU',
		'EU',
		'NA',
		'NA',
		'NA',
		'AF',
		'--',
	);

	/** @var WC_Logger Logger instance */
	public static $log = false;

	/**
	 * Logging method.
	 *
	 * @param string $message Log message.
	 * @param string $level   Optional. Default 'info'.
	 *     emergency|alert|critical|error|warning|notice|info|debug
	 */
	public static function log( $message, $level = 'info' ) {
		if ( empty( self::$log ) ) {
			self::$log = wc_get_logger();
		}
		self::$log->log( $level, $message, array( 'source' => 'geoip' ) );
	}

	/**
	 * Open geoip file.
	 *
	 * @param string $filename
	 * @param int    $flags
	 */
	public function geoip_open( $filename, $flags ) {
		$this->flags = $flags;
		if ( $this->flags & self::GEOIP_SHARED_MEMORY ) {
			$this->shmid = @shmop_open( self::GEOIP_SHM_KEY, 'a', 0, 0 );
		} else {
			if ( $this->filehandle = fopen( $filename, 'rb' ) ) {
				if ( $this->flags & self::GEOIP_MEMORY_CACHE ) {
					$s_array = fstat( $this->filehandle );
					$this->memory_buffer = fread( $this->filehandle, $s_array['size'] );
				}
			} else {
				$this->log( 'GeoIP API: Can not open ' . $filename, 'error' );
			}
		}

		$this->_setup_segments();
	}

	/**
	 * Setup segments.
	 *
	 * @return WC_Geo_IP instance
	 */
	private function _setup_segments() {
		$this->databaseType  = self::GEOIP_COUNTRY_EDITION;
		$this->record_length = self::STANDARD_RECORD_LENGTH;

		if ( $this->flags & self::GEOIP_SHARED_MEMORY ) {
			$offset = @shmop_size( $this->shmid ) - 3;

			for ( $i = 0; $i < self::STRUCTURE_INFO_MAX_SIZE; $i++ ) {
				$delim   = @shmop_read( $this->shmid, $offset, 3 );
				$offset += 3;

				if ( ( chr( 255 ) . chr( 255 ) . chr( 255 ) ) == $delim ) {
					$this->databaseType = ord( @shmop_read( $this->shmid, $offset, 1 ) );

					if ( $this->databaseType >= 106 ) {
						$this->databaseType -= 105;
					}

					$offset++;

					if ( self::GEOIP_REGION_EDITION_REV0 == $this->databaseType ) {
						$this->databaseSegments = self::GEOIP_STATE_BEGIN_REV0;
					} elseif ( self::GEOIP_REGION_EDITION_REV1 == $this->databaseType ) {
						$this->databaseSegments = self::GEOIP_STATE_BEGIN_REV1;
					} elseif ( ( self::GEOIP_CITY_EDITION_REV0 == $this->databaseType )
						|| ( self::GEOIP_CITY_EDITION_REV1 == $this->databaseType )
						|| ( self::GEOIP_ORG_EDITION == $this->databaseType )
						|| ( self::GEOIP_ORG_EDITION_V6 == $this->databaseType )
						|| ( self::GEOIP_DOMAIN_EDITION == $this->databaseType )
						|| ( self::GEOIP_DOMAIN_EDITION_V6 == $this->databaseType )
						|| ( self::GEOIP_ISP_EDITION == $this->databaseType )
						|| ( self::GEOIP_ISP_EDITION_V6 == $this->databaseType )
						|| ( self::GEOIP_USERTYPE_EDITION == $this->databaseType )
						|| ( self::GEOIP_USERTYPE_EDITION_V6 == $this->databaseType )
						|| ( self::GEOIP_LOCATIONA_EDITION == $this->databaseType )
						|| ( self::GEOIP_ACCURACYRADIUS_EDITION == $this->databaseType )
						|| ( self::GEOIP_CITY_EDITION_REV0_V6 == $this->databaseType )
						|| ( self::GEOIP_CITY_EDITION_REV1_V6 == $this->databaseType )
						|| ( self::GEOIP_NETSPEED_EDITION_REV1 == $this->databaseType )
						|| ( self::GEOIP_NETSPEED_EDITION_REV1_V6 == $this->databaseType )
						|| ( self::GEOIP_ASNUM_EDITION == $this->databaseType )
						|| ( self::GEOIP_ASNUM_EDITION_V6 == $this->databaseType )
					) {
						$this->databaseSegments = 0;
						$buf                    = @shmop_read( $this->shmid, $offset, self::SEGMENT_RECORD_LENGTH );

						for ( $j = 0; $j < self::SEGMENT_RECORD_LENGTH; $j++ ) {
							$this->databaseSegments += ( ord( $buf[ $j ] ) << ( $j * 8 ) );
						}

						if ( ( self::GEOIP_ORG_EDITION == $this->databaseType )
							|| ( self::GEOIP_ORG_EDITION_V6 == $this->databaseType )
							|| ( self::GEOIP_DOMAIN_EDITION == $this->databaseType )
							|| ( self::GEOIP_DOMAIN_EDITION_V6 == $this->databaseType )
							|| ( self::GEOIP_ISP_EDITION == $this->databaseType )
							|| ( self::GEOIP_ISP_EDITION_V6 == $this->databaseType )
						) {
							$this->record_length = self::ORG_RECORD_LENGTH;
						}
					}

					break;
				} else {
					$offset -= 4;
				}
			}
			if ( ( self::GEOIP_COUNTRY_EDITION == $this->databaseType )
				|| ( self::GEOIP_COUNTRY_EDITION_V6 == $this->databaseType )
				|| ( self::GEOIP_PROXY_EDITION == $this->databaseType )
				|| ( self::GEOIP_NETSPEED_EDITION == $this->databaseType )
			) {
				$this->databaseSegments = self::GEOIP_COUNTRY_BEGIN;
			}
		} else {
			$filepos = ftell( $this->filehandle );
			fseek( $this->filehandle, -3, SEEK_END );

			for ( $i = 0; $i < self::STRUCTURE_INFO_MAX_SIZE; $i++ ) {

				$delim = fread( $this->filehandle, 3 );
				if ( ( chr( 255 ) . chr( 255 ) . chr( 255 ) ) == $delim ) {

					$this->databaseType = ord( fread( $this->filehandle, 1 ) );
					if ( $this->databaseType >= 106 ) {
						$this->databaseType -= 105;
					}

					if ( self::GEOIP_REGION_EDITION_REV0 == $this->databaseType ) {
						$this->databaseSegments = self::GEOIP_STATE_BEGIN_REV0;
					} elseif ( self::GEOIP_REGION_EDITION_REV1 == $this->databaseType ) {
						$this->databaseSegments = self::GEOIP_STATE_BEGIN_REV1;
					} elseif ( ( self::GEOIP_CITY_EDITION_REV0 == $this->databaseType )
						|| ( self::GEOIP_CITY_EDITION_REV1 == $this->databaseType )
						|| ( self::GEOIP_CITY_EDITION_REV0_V6 == $this->databaseType )
						|| ( self::GEOIP_CITY_EDITION_REV1_V6 == $this->databaseType )
						|| ( self::GEOIP_ORG_EDITION == $this->databaseType )
						|| ( self::GEOIP_DOMAIN_EDITION == $this->databaseType )
						|| ( self::GEOIP_ISP_EDITION == $this->databaseType )
						|| ( self::GEOIP_ORG_EDITION_V6 == $this->databaseType )
						|| ( self::GEOIP_DOMAIN_EDITION_V6 == $this->databaseType )
						|| ( self::GEOIP_ISP_EDITION_V6 == $this->databaseType )
						|| ( self::GEOIP_LOCATIONA_EDITION == $this->databaseType )
						|| ( self::GEOIP_ACCURACYRADIUS_EDITION == $this->databaseType )
						|| ( self::GEOIP_NETSPEED_EDITION_REV1 == $this->databaseType )
						|| ( self::GEOIP_NETSPEED_EDITION_REV1_V6 == $this->databaseType )
						|| ( self::GEOIP_USERTYPE_EDITION == $this->databaseType )
						|| ( self::GEOIP_USERTYPE_EDITION_V6 == $this->databaseType )
						|| ( self::GEOIP_ASNUM_EDITION == $this->databaseType )
						|| ( self::GEOIP_ASNUM_EDITION_V6 == $this->databaseType )
					) {
						$this->databaseSegments = 0;
						$buf = fread( $this->filehandle, self::SEGMENT_RECORD_LENGTH );

						for ( $j = 0; $j < self::SEGMENT_RECORD_LENGTH; $j++ ) {
							$this->databaseSegments += ( ord( $buf[ $j ] ) << ( $j * 8 ) );
						}

						if ( ( self::GEOIP_ORG_EDITION == $this->databaseType )
							|| ( self::GEOIP_DOMAIN_EDITION == $this->databaseType )
							|| ( self::GEOIP_ISP_EDITION == $this->databaseType )
							|| ( self::GEOIP_ORG_EDITION_V6 == $this->databaseType )
							|| ( self::GEOIP_DOMAIN_EDITION_V6 == $this->databaseType )
							|| ( self::GEOIP_ISP_EDITION_V6 == $this->databaseType )
						) {
							$this->record_length = self::ORG_RECORD_LENGTH;
						}
					}

					break;
				} else {
					fseek( $this->filehandle, -4, SEEK_CUR );
				}
			}

			if ( ( self::GEOIP_COUNTRY_EDITION == $this->databaseType )
				|| ( self::GEOIP_COUNTRY_EDITION_V6 == $this->databaseType )
				|| ( self::GEOIP_PROXY_EDITION == $this->databaseType )
				|| ( self::GEOIP_NETSPEED_EDITION == $this->databaseType )
			) {
				$this->databaseSegments = self::GEOIP_COUNTRY_BEGIN;
			}

			fseek( $this->filehandle, $filepos, SEEK_SET );
		}

		return $this;
	}

	/**
	 * Close geoip file.
	 *
	 * @return bool
	 */
	public function geoip_close() {
		if ( $this->flags & self::GEOIP_SHARED_MEMORY ) {
			return true;
		}

		return fclose( $this->filehandle );
	}

	/**
	 * Common get record.
	 *
	 * @param  string $seek_country
	 * @return WC_Geo_IP_Record instance
	 */
	private function _common_get_record( $seek_country ) {
		// workaround php's broken substr, strpos, etc handling with
		// mbstring.func_overload and mbstring.internal_encoding
		$mbExists = extension_loaded( 'mbstring' );
		if ( $mbExists ) {
			$enc = mb_internal_encoding();
			mb_internal_encoding( 'ISO-8859-1' );
		}

		$record_pointer = $seek_country + ( 2 * $this->record_length - 1 ) * $this->databaseSegments;

		if ( $this->flags & self::GEOIP_MEMORY_CACHE ) {
			$record_buf = substr( $this->memory_buffer, $record_pointer, FULL_RECORD_LENGTH );
		} elseif ( $this->flags & self::GEOIP_SHARED_MEMORY ) {
			$record_buf = @shmop_read( $this->shmid, $record_pointer, FULL_RECORD_LENGTH );
		} else {
			fseek( $this->filehandle, $record_pointer, SEEK_SET );
			$record_buf = fread( $this->filehandle, FULL_RECORD_LENGTH );
		}

		$record                 = new WC_Geo_IP_Record();
		$record_buf_pos         = 0;
		$char                   = ord( substr( $record_buf, $record_buf_pos, 1 ) );
		$record->country_code   = $this->GEOIP_COUNTRY_CODES[ $char ];
		$record->country_code3  = $this->GEOIP_COUNTRY_CODES3[ $char ];
		$record->country_name   = $this->GEOIP_COUNTRY_NAMES[ $char ];
		$record->continent_code = $this->GEOIP_CONTINENT_CODES[ $char ];
		$str_length             = 0;

		$record_buf_pos++;

		// Get region
		$char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) );
		while ( 0 != $char ) {
			$str_length++;
			$char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) );
		}

		if ( $str_length > 0 ) {
			$record->region = substr( $record_buf, $record_buf_pos, $str_length );
		}

		$record_buf_pos += $str_length + 1;
		$str_length      = 0;

		// Get city
		$char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) );
		while ( 0 != $char ) {
			$str_length++;
			$char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) );
		}

		if ( $str_length > 0 ) {
			$record->city = substr( $record_buf, $record_buf_pos, $str_length );
		}

		$record_buf_pos += $str_length + 1;
		$str_length      = 0;

		// Get postal code
		$char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) );
		while ( 0 != $char ) {
			$str_length++;
			$char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) );
		}

		if ( $str_length > 0 ) {
			$record->postal_code = substr( $record_buf, $record_buf_pos, $str_length );
		}

		$record_buf_pos += $str_length + 1;

		// Get latitude and longitude
		$latitude  = 0;
		$longitude = 0;
		for ( $j = 0; $j < 3; ++$j ) {
			$char      = ord( substr( $record_buf, $record_buf_pos++, 1 ) );
			$latitude += ( $char << ( $j * 8 ) );
		}

		$record->latitude = ( $latitude / 10000 ) - 180;

		for ( $j = 0; $j < 3; ++$j ) {
			$char       = ord( substr( $record_buf, $record_buf_pos++, 1 ) );
			$longitude += ( $char << ( $j * 8 ) );
		}

		$record->longitude = ( $longitude / 10000 ) - 180;

		if ( self::GEOIP_CITY_EDITION_REV1 == $this->databaseType ) {
			$metroarea_combo = 0;
			if ( 'US' === $record->country_code ) {
				for ( $j = 0; $j < 3; ++$j ) {
					$char             = ord( substr( $record_buf, $record_buf_pos++, 1 ) );
					$metroarea_combo += ( $char << ( $j * 8 ) );
				}

				$record->metro_code = $record->dma_code = floor( $metroarea_combo / 1000 );
				$record->area_code  = $metroarea_combo % 1000;
			}
		}

		if ( $mbExists ) {
			mb_internal_encoding( $enc );
		}

		return $record;
	}

	/**
	 * Get record.
	 *
	 * @param  int $ipnum
	 * @return WC_Geo_IP_Record instance
	 */
	private function _get_record( $ipnum ) {
		$seek_country = $this->_geoip_seek_country( $ipnum );
		if ( $seek_country == $this->databaseSegments ) {
			return null;
		}

		return $this->_common_get_record( $seek_country );
	}

	/**
	 * Seek country IPv6.
	 *
	 * @param  int $ipnum
	 * @return string
	 */
	public function _geoip_seek_country_v6( $ipnum ) {
		// arrays from unpack start with offset 1
		// yet another php mystery. array_merge work around
		// this broken behaviour
		$v6vec = array_merge( unpack( 'C16', $ipnum ) );

		$offset = 0;
		for ( $depth = 127; $depth >= 0; --$depth ) {
			if ( $this->flags & self::GEOIP_MEMORY_CACHE ) {
				$buf = $this->_safe_substr(
					$this->memory_buffer,
					2 * $this->record_length * $offset,
					2 * $this->record_length
				);
			} elseif ( $this->flags & self::GEOIP_SHARED_MEMORY ) {
				$buf = @shmop_read(
					$this->shmid,
					2 * $this->record_length * $offset,
					2 * $this->record_length
				);
			} else {
				if ( 0 != fseek( $this->filehandle, 2 * $this->record_length * $offset, SEEK_SET ) ) {
					break;
				}

				$buf = fread( $this->filehandle, 2 * $this->record_length );
			}
			$x = array( 0, 0 );
			for ( $i = 0; $i < 2; ++$i ) {
				for ( $j = 0; $j < $this->record_length; ++$j ) {
					$x[ $i ] += ord( $buf[ $this->record_length * $i + $j ] ) << ( $j * 8 );
				}
			}

			$bnum = 127 - $depth;
			$idx = $bnum >> 3;
			$b_mask = 1 << ( $bnum & 7 ^ 7 );
			if ( ( $v6vec[ $idx ] & $b_mask ) > 0 ) {
				if ( $x[1] >= $this->databaseSegments ) {
					return $x[1];
				}
				$offset = $x[1];
			} else {
				if ( $x[0] >= $this->databaseSegments ) {
					return $x[0];
				}
				$offset = $x[0];
			}
		}

		$this->log( 'GeoIP API: Error traversing database - perhaps it is corrupt?', 'error' );

		return false;
	}

	/**
	 * Seek country.
	 *
	 * @param  int $ipnum
	 * @return string
	 */
	private function _geoip_seek_country( $ipnum ) {
		$offset = 0;
		for ( $depth = 31; $depth >= 0; --$depth ) {
			if ( $this->flags & self::GEOIP_MEMORY_CACHE ) {
				$buf = $this->_safe_substr(
					$this->memory_buffer,
					2 * $this->record_length * $offset,
					2 * $this->record_length
				);
			} elseif ( $this->flags & self::GEOIP_SHARED_MEMORY ) {
				$buf = @shmop_read(
					$this->shmid,
					2 * $this->record_length * $offset,
					2 * $this->record_length
				);
			} else {
				if ( 0 != fseek( $this->filehandle, 2 * $this->record_length * $offset, SEEK_SET ) ) {
					break;
				}

				$buf = fread( $this->filehandle, 2 * $this->record_length );
			}

			$x = array( 0, 0 );
			for ( $i = 0; $i < 2; ++$i ) {
				for ( $j = 0; $j < $this->record_length; ++$j ) {
					$x[ $i ] += ord( $buf[ $this->record_length * $i + $j ] ) << ( $j * 8 );
				}
			}
			if ( $ipnum & ( 1 << $depth ) ) {
				if ( $x[1] >= $this->databaseSegments ) {
					return $x[1];
				}

				$offset = $x[1];
			} else {
				if ( $x[0] >= $this->databaseSegments ) {
					return $x[0];
				}

				$offset = $x[0];
			}
		}

		$this->log( 'GeoIP API: Error traversing database - perhaps it is corrupt?', 'error' );

		return false;
	}

	/**
	 * Record by addr.
	 *
	 * @param  string $addr
	 *
	 * @return WC_Geo_IP_Record
	 */
	public function geoip_record_by_addr( $addr ) {
		if ( null == $addr ) {
			return 0;
		}

		$ipnum = ip2long( $addr );
		return $this->_get_record( $ipnum );
	}

	/**
	 * Country ID by addr IPv6.
	 *
	 * @param  string $addr
	 * @return int|bool
	 */
	public function geoip_country_id_by_addr_v6( $addr ) {
		if ( ! defined( 'AF_INET6' ) ) {
			$this->log( 'GEOIP (geoip_country_id_by_addr_v6): PHP was compiled with --disable-ipv6 option' );
			return false;
		}
		$ipnum = inet_pton( $addr );
		return $this->_geoip_seek_country_v6( $ipnum ) - self::GEOIP_COUNTRY_BEGIN;
	}

	/**
	 * Country ID by addr.
	 *
	 * @param  string $addr
	 * @return int
	 */
	public function geoip_country_id_by_addr( $addr ) {
		$ipnum = ip2long( $addr );
		return $this->_geoip_seek_country( $ipnum ) - self::GEOIP_COUNTRY_BEGIN;
	}

	/**
	 * Country code by addr IPv6.
	 *
	 * @param  string $addr
	 * @return string
	 */
	public function geoip_country_code_by_addr_v6( $addr ) {
		$country_id = $this->geoip_country_id_by_addr_v6( $addr );
		if ( false !== $country_id && isset( $this->GEOIP_COUNTRY_CODES[ $country_id ] ) ) {
			return $this->GEOIP_COUNTRY_CODES[ $country_id ];
		}

		return false;
	}

	/**
	 * Country code by addr.
	 *
	 * @param  string $addr
	 * @return string
	 */
	public function geoip_country_code_by_addr( $addr ) {
		if ( self::GEOIP_CITY_EDITION_REV1 == $this->databaseType ) {
			$record = $this->geoip_record_by_addr( $addr );
			if ( false !== $record ) {
				return $record->country_code;
			}
		} else {
			$country_id = $this->geoip_country_id_by_addr( $addr );
			if ( false !== $country_id && isset( $this->GEOIP_COUNTRY_CODES[ $country_id ] ) ) {
				return $this->GEOIP_COUNTRY_CODES[ $country_id ];
			}
		}

		return false;
	}

	/**
	 * Encode string.
	 *
	 * @param  string $string
	 * @param  int    $start
	 * @param  int    $length
	 * @return string
	 */
	private function _safe_substr( $string, $start, $length ) {
		// workaround php's broken substr, strpos, etc handling with
		// mbstring.func_overload and mbstring.internal_encoding
		$mb_exists = extension_loaded( 'mbstring' );

		if ( $mb_exists ) {
			$enc = mb_internal_encoding();
			mb_internal_encoding( 'ISO-8859-1' );
		}

		$buf = substr( $string, $start, $length );

		if ( $mb_exists ) {
			mb_internal_encoding( $enc );
		}

		return $buf;
	}
}

/**
 * Geo IP Record class.
 */
class WC_Geo_IP_Record {

	/**
	 * Country code.
	 *
	 * @var string
	 */
	public $country_code;

	/**
	 * 3 letters country code.
	 *
	 * @var string
	 */
	public $country_code3;

	/**
	 * Country name.
	 *
	 * @var string
	 */
	public $country_name;

	/**
	 * Region.
	 *
	 * @var string
	 */
	public $region;

	/**
	 * City.
	 *
	 * @var string
	 */
	public $city;

	/**
	 * Postal code.
	 *
	 * @var string
	 */
	public $postal_code;

	/**
	 * Latitude
	 *
	 * @var int
	 */
	public $latitude;

	/**
	 * Longitude.
	 *
	 * @var int
	 */
	public $longitude;

	/**
	 * Area code.
	 *
	 * @var int
	 */
	public $area_code;

	/**
	 * DMA Code.
	 *
	 * Metro and DMA code are the same.
	 * Use metro code instead.
	 *
	 * @var float
	 */
	public $dma_code;

	/**
	 * Metro code.
	 *
	 * @var float
	 */
	public $metro_code;

	/**
	 * Continent code.
	 *
	 * @var string
	 */
	public $continent_code;
}
class-wc-cart.php000064400000201721151336044760007734 0ustar00<?php
/**
 * WooCommerce cart
 *
 * The WooCommerce cart class stores cart data and active coupons as well as handling customer sessions and some cart related urls.
 * The cart class also has a price calculation function which calls upon other classes to calculate totals.
 *
 * @package WooCommerce\Classes
 * @version 2.1.0
 */

use Automattic\WooCommerce\Utilities\NumberUtil;

defined( 'ABSPATH' ) || exit;

require_once WC_ABSPATH . 'includes/legacy/class-wc-legacy-cart.php';
require_once WC_ABSPATH . 'includes/class-wc-cart-fees.php';
require_once WC_ABSPATH . 'includes/class-wc-cart-session.php';

/**
 * WC_Cart class.
 */
class WC_Cart extends WC_Legacy_Cart {

	/**
	 * Contains an array of cart items.
	 *
	 * @var array
	 */
	public $cart_contents = array();

	/**
	 * Contains an array of removed cart items so we can restore them if needed.
	 *
	 * @var array
	 */
	public $removed_cart_contents = array();

	/**
	 * Contains an array of coupon codes applied to the cart.
	 *
	 * @var array
	 */
	public $applied_coupons = array();

	/**
	 * This stores the chosen shipping methods for the cart item packages.
	 *
	 * @var array
	 */
	protected $shipping_methods;

	/**
	 * Total defaults used to reset.
	 *
	 * @var array
	 */
	protected $default_totals = array(
		'subtotal'            => 0,
		'subtotal_tax'        => 0,
		'shipping_total'      => 0,
		'shipping_tax'        => 0,
		'shipping_taxes'      => array(),
		'discount_total'      => 0,
		'discount_tax'        => 0,
		'cart_contents_total' => 0,
		'cart_contents_tax'   => 0,
		'cart_contents_taxes' => array(),
		'fee_total'           => 0,
		'fee_tax'             => 0,
		'fee_taxes'           => array(),
		'total'               => 0,
		'total_tax'           => 0,
	);
	/**
	 * Store calculated totals.
	 *
	 * @var array
	 */
	protected $totals = array();

	/**
	 * Reference to the cart session handling class.
	 *
	 * @var WC_Cart_Session
	 */
	protected $session;

	/**
	 * Reference to the cart fees API class.
	 *
	 * @var WC_Cart_Fees
	 */
	protected $fees_api;

	/**
	 * Constructor for the cart class. Loads options and hooks in the init method.
	 */
	public function __construct() {
		$this->session  = new WC_Cart_Session( $this );
		$this->fees_api = new WC_Cart_Fees( $this );

		// Register hooks for the objects.
		$this->session->init();

		add_action( 'woocommerce_add_to_cart', array( $this, 'calculate_totals' ), 20, 0 );
		add_action( 'woocommerce_applied_coupon', array( $this, 'calculate_totals' ), 20, 0 );
		add_action( 'woocommerce_cart_item_removed', array( $this, 'calculate_totals' ), 20, 0 );
		add_action( 'woocommerce_cart_item_restored', array( $this, 'calculate_totals' ), 20, 0 );
		add_action( 'woocommerce_check_cart_items', array( $this, 'check_cart_items' ), 1 );
		add_action( 'woocommerce_check_cart_items', array( $this, 'check_cart_coupons' ), 1 );
		add_action( 'woocommerce_after_checkout_validation', array( $this, 'check_customer_coupons' ), 1, 2 );
	}

	/**
	 * When cloning, ensure object properties are handled.
	 *
	 * These properties store a reference to the cart, so we use new instead of clone.
	 */
	public function __clone() {
		$this->session  = clone $this->session;
		$this->fees_api = clone $this->fees_api;
	}

	/*
	|--------------------------------------------------------------------------
	| Getters.
	|--------------------------------------------------------------------------
	|
	| Methods to retrieve class properties and avoid direct access.
	*/

	/**
	 * Gets cart contents.
	 *
	 * @since 3.2.0
	 * @return array of cart items
	 */
	public function get_cart_contents() {
		return apply_filters( 'woocommerce_get_cart_contents', (array) $this->cart_contents );
	}

	/**
	 * Return items removed from the cart.
	 *
	 * @since 3.2.0
	 * @return array
	 */
	public function get_removed_cart_contents() {
		return (array) $this->removed_cart_contents;
	}

	/**
	 * Gets the array of applied coupon codes.
	 *
	 * @return array of applied coupons
	 */
	public function get_applied_coupons() {
		return (array) $this->applied_coupons;
	}

	/**
	 * Return all calculated coupon totals.
	 *
	 * @since 3.2.0
	 * @return array
	 */
	public function get_coupon_discount_totals() {
		return (array) $this->coupon_discount_totals;
	}
	/**
	 * Return all calculated coupon tax totals.
	 *
	 * @since 3.2.0
	 * @return array
	 */
	public function get_coupon_discount_tax_totals() {
		return (array) $this->coupon_discount_tax_totals;
	}

	/**
	 * Return all calculated totals.
	 *
	 * @since 3.2.0
	 * @return array
	 */
	public function get_totals() {
		return empty( $this->totals ) ? $this->default_totals : $this->totals;
	}

	/**
	 * Get a total.
	 *
	 * @since 3.2.0
	 * @param string $key Key of element in $totals array.
	 * @return mixed
	 */
	protected function get_totals_var( $key ) {
		return isset( $this->totals[ $key ] ) ? $this->totals[ $key ] : $this->default_totals[ $key ];
	}

	/**
	 * Get subtotal.
	 *
	 * @since 3.2.0
	 * @return float
	 */
	public function get_subtotal() {
		return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'subtotal' ) );
	}

	/**
	 * Get subtotal_tax.
	 *
	 * @since 3.2.0
	 * @return float
	 */
	public function get_subtotal_tax() {
		return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'subtotal_tax' ) );
	}

	/**
	 * Get discount_total.
	 *
	 * @since 3.2.0
	 * @return float
	 */
	public function get_discount_total() {
		return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'discount_total' ) );
	}

	/**
	 * Get discount_tax.
	 *
	 * @since 3.2.0
	 * @return float
	 */
	public function get_discount_tax() {
		return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'discount_tax' ) );
	}

	/**
	 * Get shipping_total.
	 *
	 * @since 3.2.0
	 * @return float
	 */
	public function get_shipping_total() {
		return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'shipping_total' ) );
	}

	/**
	 * Get shipping_tax.
	 *
	 * @since 3.2.0
	 * @return float
	 */
	public function get_shipping_tax() {
		return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'shipping_tax' ) );
	}

	/**
	 * Gets cart total. This is the total of items in the cart, but after discounts. Subtotal is before discounts.
	 *
	 * @since 3.2.0
	 * @return float
	 */
	public function get_cart_contents_total() {
		return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'cart_contents_total' ) );
	}

	/**
	 * Gets cart tax amount.
	 *
	 * @since 3.2.0
	 * @return float
	 */
	public function get_cart_contents_tax() {
		return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'cart_contents_tax' ) );
	}

	/**
	 * Gets cart total after calculation.
	 *
	 * @since 3.2.0
	 * @param string $context If the context is view, the value will be formatted for display. This keeps it compatible with pre-3.2 versions.
	 * @return float
	 */
	public function get_total( $context = 'view' ) {
		$total = apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'total' ) );
		return 'view' === $context ? apply_filters( 'woocommerce_cart_total', wc_price( $total ) ) : $total;
	}

	/**
	 * Get total tax amount.
	 *
	 * @since 3.2.0
	 * @return float
	 */
	public function get_total_tax() {
		return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'total_tax' ) );
	}

	/**
	 * Get total fee amount.
	 *
	 * @since 3.2.0
	 * @return float
	 */
	public function get_fee_total() {
		return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'fee_total' ) );
	}

	/**
	 * Get total fee tax amount.
	 *
	 * @since 3.2.0
	 * @return float
	 */
	public function get_fee_tax() {
		return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'fee_tax' ) );
	}

	/**
	 * Get taxes.
	 *
	 * @since 3.2.0
	 */
	public function get_shipping_taxes() {
		return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'shipping_taxes' ) );
	}

	/**
	 * Get taxes.
	 *
	 * @since 3.2.0
	 */
	public function get_cart_contents_taxes() {
		return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'cart_contents_taxes' ) );
	}

	/**
	 * Get taxes.
	 *
	 * @since 3.2.0
	 */
	public function get_fee_taxes() {
		return apply_filters( 'woocommerce_cart_' . __FUNCTION__, $this->get_totals_var( 'fee_taxes' ) );
	}

	/**
	 * Return whether or not the cart is displaying prices including tax, rather than excluding tax.
	 *
	 * @since 3.3.0
	 * @return bool
	 */
	public function display_prices_including_tax() {
		return apply_filters( 'woocommerce_cart_' . __FUNCTION__, 'incl' === $this->get_tax_price_display_mode() );
	}

	/*
	|--------------------------------------------------------------------------
	| Setters.
	|--------------------------------------------------------------------------
	|
	| Methods to set class properties and avoid direct access.
	*/

	/**
	 * Sets the contents of the cart.
	 *
	 * @param array $value Cart array.
	 */
	public function set_cart_contents( $value ) {
		$this->cart_contents = (array) $value;
	}

	/**
	 * Set items removed from the cart.
	 *
	 * @since 3.2.0
	 * @param array $value Item array.
	 */
	public function set_removed_cart_contents( $value = array() ) {
		$this->removed_cart_contents = (array) $value;
	}

	/**
	 * Sets the array of applied coupon codes.
	 *
	 * @param array $value List of applied coupon codes.
	 */
	public function set_applied_coupons( $value = array() ) {
		$this->applied_coupons = (array) $value;
	}

	/**
	 * Sets the array of calculated coupon totals.
	 *
	 * @since 3.2.0
	 * @param array $value Value to set.
	 */
	public function set_coupon_discount_totals( $value = array() ) {
		$this->coupon_discount_totals = (array) $value;
	}
	/**
	 * Sets the array of calculated coupon tax totals.
	 *
	 * @since 3.2.0
	 * @param array $value Value to set.
	 */
	public function set_coupon_discount_tax_totals( $value = array() ) {
		$this->coupon_discount_tax_totals = (array) $value;
	}

	/**
	 * Set all calculated totals.
	 *
	 * @since 3.2.0
	 * @param array $value Value to set.
	 */
	public function set_totals( $value = array() ) {
		$this->totals = wp_parse_args( $value, $this->default_totals );
	}

	/**
	 * Set subtotal.
	 *
	 * @since 3.2.0
	 * @param string $value Value to set.
	 */
	public function set_subtotal( $value ) {
		$this->totals['subtotal'] = wc_format_decimal( $value );
	}

	/**
	 * Set subtotal.
	 *
	 * @since 3.2.0
	 * @param string $value Value to set.
	 */
	public function set_subtotal_tax( $value ) {
		$this->totals['subtotal_tax'] = $value;
	}

	/**
	 * Set discount_total.
	 *
	 * @since 3.2.0
	 * @param string $value Value to set.
	 */
	public function set_discount_total( $value ) {
		$this->totals['discount_total'] = $value;
	}

	/**
	 * Set discount_tax.
	 *
	 * @since 3.2.0
	 * @param string $value Value to set.
	 */
	public function set_discount_tax( $value ) {
		$this->totals['discount_tax'] = $value;
	}

	/**
	 * Set shipping_total.
	 *
	 * @since 3.2.0
	 * @param string $value Value to set.
	 */
	public function set_shipping_total( $value ) {
		$this->totals['shipping_total'] = wc_format_decimal( $value );
	}

	/**
	 * Set shipping_tax.
	 *
	 * @since 3.2.0
	 * @param string $value Value to set.
	 */
	public function set_shipping_tax( $value ) {
		$this->totals['shipping_tax'] = $value;
	}

	/**
	 * Set cart_contents_total.
	 *
	 * @since 3.2.0
	 * @param string $value Value to set.
	 */
	public function set_cart_contents_total( $value ) {
		$this->totals['cart_contents_total'] = wc_format_decimal( $value );
	}

	/**
	 * Set cart tax amount.
	 *
	 * @since 3.2.0
	 * @param string $value Value to set.
	 */
	public function set_cart_contents_tax( $value ) {
		$this->totals['cart_contents_tax'] = $value;
	}

	/**
	 * Set cart total.
	 *
	 * @since 3.2.0
	 * @param string $value Value to set.
	 */
	public function set_total( $value ) {
		$this->totals['total'] = wc_format_decimal( $value, wc_get_price_decimals() );
	}

	/**
	 * Set total tax amount.
	 *
	 * @since 3.2.0
	 * @param string $value Value to set.
	 */
	public function set_total_tax( $value ) {
		// We round here because this is a total entry, as opposed to line items in other setters.
		$this->totals['total_tax'] = wc_round_tax_total( $value );
	}

	/**
	 * Set fee amount.
	 *
	 * @since 3.2.0
	 * @param string $value Value to set.
	 */
	public function set_fee_total( $value ) {
		$this->totals['fee_total'] = wc_format_decimal( $value );
	}

	/**
	 * Set fee tax.
	 *
	 * @since 3.2.0
	 * @param string $value Value to set.
	 */
	public function set_fee_tax( $value ) {
		$this->totals['fee_tax'] = $value;
	}

	/**
	 * Set taxes.
	 *
	 * @since 3.2.0
	 * @param array $value Tax values.
	 */
	public function set_shipping_taxes( $value ) {
		$this->totals['shipping_taxes'] = (array) $value;
	}

	/**
	 * Set taxes.
	 *
	 * @since 3.2.0
	 * @param array $value Tax values.
	 */
	public function set_cart_contents_taxes( $value ) {
		$this->totals['cart_contents_taxes'] = (array) $value;
	}

	/**
	 * Set taxes.
	 *
	 * @since 3.2.0
	 * @param array $value Tax values.
	 */
	public function set_fee_taxes( $value ) {
		$this->totals['fee_taxes'] = (array) $value;
	}

	/*
	|--------------------------------------------------------------------------
	| Helper methods.
	|--------------------------------------------------------------------------
	*/

	/**
	 * Returns the cart and shipping taxes, merged.
	 *
	 * @return array merged taxes
	 */
	public function get_taxes() {
		return apply_filters( 'woocommerce_cart_get_taxes', wc_array_merge_recursive_numeric( $this->get_shipping_taxes(), $this->get_cart_contents_taxes(), $this->get_fee_taxes() ), $this );
	}

	/**
	 * Returns the contents of the cart in an array.
	 *
	 * @return array contents of the cart
	 */
	public function get_cart() {
		if ( ! did_action( 'wp_loaded' ) ) {
			wc_doing_it_wrong( __FUNCTION__, __( 'Get cart should not be called before the wp_loaded action.', 'woocommerce' ), '2.3' );
		}
		if ( ! did_action( 'woocommerce_load_cart_from_session' ) ) {
			$this->session->get_cart_from_session();
		}
		return array_filter( $this->get_cart_contents() );
	}

	/**
	 * Returns a specific item in the cart.
	 *
	 * @param string $item_key Cart item key.
	 * @return array Item data
	 */
	public function get_cart_item( $item_key ) {
		return isset( $this->cart_contents[ $item_key ] ) ? $this->cart_contents[ $item_key ] : array();
	}

	/**
	 * Checks if the cart is empty.
	 *
	 * @return bool
	 */
	public function is_empty() {
		return 0 === count( $this->get_cart() );
	}

	/**
	 * Empties the cart and optionally the persistent cart too.
	 *
	 * @param bool $clear_persistent_cart Should the persistant cart be cleared too. Defaults to true.
	 */
	public function empty_cart( $clear_persistent_cart = true ) {

		do_action( 'woocommerce_before_cart_emptied', $clear_persistent_cart );

		$this->cart_contents              = array();
		$this->removed_cart_contents      = array();
		$this->shipping_methods           = array();
		$this->coupon_discount_totals     = array();
		$this->coupon_discount_tax_totals = array();
		$this->applied_coupons            = array();
		$this->totals                     = $this->default_totals;

		if ( $clear_persistent_cart ) {
			$this->session->persistent_cart_destroy();
		}

		$this->fees_api->remove_all_fees();

		do_action( 'woocommerce_cart_emptied', $clear_persistent_cart );
	}

	/**
	 * Get number of items in the cart.
	 *
	 * @return int
	 */
	public function get_cart_contents_count() {
		return apply_filters( 'woocommerce_cart_contents_count', array_sum( wp_list_pluck( $this->get_cart(), 'quantity' ) ) );
	}

	/**
	 * Get weight of items in the cart.
	 *
	 * @since 2.5.0
	 * @return float
	 */
	public function get_cart_contents_weight() {
		$weight = 0.0;

		foreach ( $this->get_cart() as $cart_item_key => $values ) {
			if ( $values['data']->has_weight() ) {
				$weight += (float) $values['data']->get_weight() * $values['quantity'];
			}
		}

		return apply_filters( 'woocommerce_cart_contents_weight', $weight );
	}

	/**
	 * Get cart items quantities - merged so we can do accurate stock checks on items across multiple lines.
	 *
	 * @return array
	 */
	public function get_cart_item_quantities() {
		$quantities = array();

		foreach ( $this->get_cart() as $cart_item_key => $values ) {
			$product = $values['data'];
			$quantities[ $product->get_stock_managed_by_id() ] = isset( $quantities[ $product->get_stock_managed_by_id() ] ) ? $quantities[ $product->get_stock_managed_by_id() ] + $values['quantity'] : $values['quantity'];
		}

		return $quantities;
	}

	/**
	 * Check all cart items for errors.
	 */
	public function check_cart_items() {
		$return = true;
		$result = $this->check_cart_item_validity();

		if ( is_wp_error( $result ) ) {
			wc_add_notice( $result->get_error_message(), 'error' );
			$return = false;
		}

		$result = $this->check_cart_item_stock();

		if ( is_wp_error( $result ) ) {
			wc_add_notice( $result->get_error_message(), 'error' );
			$return = false;
		}

		return $return;

	}

	/**
	 * Check cart coupons for errors.
	 */
	public function check_cart_coupons() {
		foreach ( $this->get_applied_coupons() as $code ) {
			$coupon = new WC_Coupon( $code );

			if ( ! $coupon->is_valid() ) {
				$coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_INVALID_REMOVED );
				$this->remove_coupon( $code );
			}
		}
	}

	/**
	 * Looks through cart items and checks the posts are not trashed or deleted.
	 *
	 * @return bool|WP_Error
	 */
	public function check_cart_item_validity() {
		$return = true;

		foreach ( $this->get_cart() as $cart_item_key => $values ) {
			$product = $values['data'];

			if ( ! $product || ! $product->exists() || 'trash' === $product->get_status() ) {
				$this->set_quantity( $cart_item_key, 0 );
				$return = new WP_Error( 'invalid', __( 'An item which is no longer available was removed from your cart.', 'woocommerce' ) );
			}
		}

		return $return;
	}

	/**
	 * Looks through the cart to check each item is in stock. If not, add an error.
	 *
	 * @return bool|WP_Error
	 */
	public function check_cart_item_stock() {
		$error                    = new WP_Error();
		$product_qty_in_cart      = $this->get_cart_item_quantities();
		$current_session_order_id = isset( WC()->session->order_awaiting_payment ) ? absint( WC()->session->order_awaiting_payment ) : 0;

		foreach ( $this->get_cart() as $cart_item_key => $values ) {
			$product = $values['data'];

			// Check stock based on stock-status.
			if ( ! $product->is_in_stock() ) {
				/* translators: %s: product name */
				$error->add( 'out-of-stock', sprintf( __( 'Sorry, "%s" is not in stock. Please edit your cart and try again. We apologize for any inconvenience caused.', 'woocommerce' ), $product->get_name() ) );
				return $error;
			}

			// We only need to check products managing stock, with a limited stock qty.
			if ( ! $product->managing_stock() || $product->backorders_allowed() ) {
				continue;
			}

			// Check stock based on all items in the cart and consider any held stock within pending orders.
			$held_stock     = wc_get_held_stock_quantity( $product, $current_session_order_id );
			$required_stock = $product_qty_in_cart[ $product->get_stock_managed_by_id() ];

			/**
			 * Allows filter if product have enough stock to get added to the cart.
			 *
			 * @since 4.6.0
			 * @param bool       $has_stock If have enough stock.
			 * @param WC_Product $product   Product instance.
			 * @param array      $values    Cart item values.
			 */
			if ( apply_filters( 'woocommerce_cart_item_required_stock_is_not_enough', $product->get_stock_quantity() < ( $held_stock + $required_stock ), $product, $values ) ) {
				/* translators: 1: product name 2: quantity in stock */
				$error->add( 'out-of-stock', sprintf( __( 'Sorry, we do not have enough "%1$s" in stock to fulfill your order (%2$s available). We apologize for any inconvenience caused.', 'woocommerce' ), $product->get_name(), wc_format_stock_quantity_for_display( $product->get_stock_quantity() - $held_stock, $product ) ) );
				return $error;
			}
		}

		return true;
	}

	/**
	 * Gets and formats a list of cart item data + variations for display on the frontend.
	 *
	 * @param array $cart_item Cart item object.
	 * @param bool  $flat Should the data be returned flat or in a list.
	 * @return string
	 */
	public function get_item_data( $cart_item, $flat = false ) {
		wc_deprecated_function( 'WC_Cart::get_item_data', '3.3', 'wc_get_formatted_cart_item_data' );

		return wc_get_formatted_cart_item_data( $cart_item, $flat );
	}

	/**
	 * Gets cross sells based on the items in the cart.
	 *
	 * @return array cross_sells (item ids)
	 */
	public function get_cross_sells() {
		$cross_sells = array();
		$in_cart     = array();
		if ( ! $this->is_empty() ) {
			foreach ( $this->get_cart() as $cart_item_key => $values ) {
				if ( $values['quantity'] > 0 ) {
					$cross_sells = array_merge( $values['data']->get_cross_sell_ids(), $cross_sells );
					$in_cart[]   = $values['product_id'];
				}
			}
		}
		$cross_sells = array_diff( $cross_sells, $in_cart );
		return apply_filters( 'woocommerce_cart_crosssell_ids', wp_parse_id_list( $cross_sells ), $this );
	}

	/**
	 * Gets the url to remove an item from the cart.
	 *
	 * @param string $cart_item_key contains the id of the cart item.
	 * @return string url to page
	 */
	public function get_remove_url( $cart_item_key ) {
		wc_deprecated_function( 'WC_Cart::get_remove_url', '3.3', 'wc_get_cart_remove_url' );

		return wc_get_cart_remove_url( $cart_item_key );
	}

	/**
	 * Gets the url to re-add an item into the cart.
	 *
	 * @param  string $cart_item_key Cart item key to undo.
	 * @return string url to page
	 */
	public function get_undo_url( $cart_item_key ) {
		wc_deprecated_function( 'WC_Cart::get_undo_url', '3.3', 'wc_get_cart_undo_url' );

		return wc_get_cart_undo_url( $cart_item_key );
	}

	/**
	 * Get taxes, merged by code, formatted ready for output.
	 *
	 * @return array
	 */
	public function get_tax_totals() {
		$shipping_taxes = $this->get_shipping_taxes(); // Shipping taxes are rounded differently, so we will subtract from all taxes, then round and then add them back.
		$taxes          = $this->get_taxes();
		$tax_totals     = array();

		foreach ( $taxes as $key => $tax ) {
			$code = WC_Tax::get_rate_code( $key );

			if ( $code || apply_filters( 'woocommerce_cart_remove_taxes_zero_rate_id', 'zero-rated' ) === $key ) {
				if ( ! isset( $tax_totals[ $code ] ) ) {
					$tax_totals[ $code ]         = new stdClass();
					$tax_totals[ $code ]->amount = 0;
				}

				$tax_totals[ $code ]->tax_rate_id = $key;
				$tax_totals[ $code ]->is_compound = WC_Tax::is_compound( $key );
				$tax_totals[ $code ]->label       = WC_Tax::get_rate_label( $key );

				if ( isset( $shipping_taxes[ $key ] ) ) {
					$tax -= $shipping_taxes[ $key ];
					$tax  = wc_round_tax_total( $tax );
					$tax += NumberUtil::round( $shipping_taxes[ $key ], wc_get_price_decimals() );
					unset( $shipping_taxes[ $key ] );
				}
				$tax_totals[ $code ]->amount          += wc_round_tax_total( $tax );
				$tax_totals[ $code ]->formatted_amount = wc_price( $tax_totals[ $code ]->amount );
			}
		}

		if ( apply_filters( 'woocommerce_cart_hide_zero_taxes', true ) ) {
			$amounts    = array_filter( wp_list_pluck( $tax_totals, 'amount' ) );
			$tax_totals = array_intersect_key( $tax_totals, $amounts );
		}

		return apply_filters( 'woocommerce_cart_tax_totals', $tax_totals, $this );
	}

	/**
	 * Get all tax classes for items in the cart.
	 *
	 * @return array
	 */
	public function get_cart_item_tax_classes() {
		$found_tax_classes = array();

		foreach ( WC()->cart->get_cart() as $item ) {
			if ( $item['data'] && ( $item['data']->is_taxable() || $item['data']->is_shipping_taxable() ) ) {
				$found_tax_classes[] = $item['data']->get_tax_class();
			}
		}

		return array_unique( $found_tax_classes );
	}

	/**
	 * Get all tax classes for shipping based on the items in the cart.
	 *
	 * @return array
	 */
	public function get_cart_item_tax_classes_for_shipping() {
		$found_tax_classes = array();

		foreach ( WC()->cart->get_cart() as $item ) {
			if ( $item['data'] && ( $item['data']->is_shipping_taxable() ) ) {
				$found_tax_classes[] = $item['data']->get_tax_class();
			}
		}

		return array_unique( $found_tax_classes );
	}

	/**
	 * Determines the value that the customer spent and the subtotal
	 * displayed, used for things like coupon validation.
	 *
	 * Since the coupon lines are displayed based on the TAX DISPLAY value
	 * of cart, this is used to determine the spend.
	 *
	 * If cart totals are shown including tax, use the subtotal.
	 * If cart totals are shown excluding tax, use the subtotal ex tax
	 * (tax is shown after coupons).
	 *
	 * @since 2.6.0
	 * @return string
	 */
	public function get_displayed_subtotal() {
		return $this->display_prices_including_tax() ? $this->get_subtotal() + $this->get_subtotal_tax() : $this->get_subtotal();
	}

	/**
	 * Check if product is in the cart and return cart item key.
	 *
	 * Cart item key will be unique based on the item and its properties, such as variations.
	 *
	 * @param mixed $cart_id id of product to find in the cart.
	 * @return string cart item key
	 */
	public function find_product_in_cart( $cart_id = false ) {
		if ( false !== $cart_id ) {
			if ( is_array( $this->cart_contents ) && isset( $this->cart_contents[ $cart_id ] ) ) {
				return $cart_id;
			}
		}
		return '';
	}

	/**
	 * Generate a unique ID for the cart item being added.
	 *
	 * @param int   $product_id - id of the product the key is being generated for.
	 * @param int   $variation_id of the product the key is being generated for.
	 * @param array $variation data for the cart item.
	 * @param array $cart_item_data other cart item data passed which affects this items uniqueness in the cart.
	 * @return string cart item key
	 */
	public function generate_cart_id( $product_id, $variation_id = 0, $variation = array(), $cart_item_data = array() ) {
		$id_parts = array( $product_id );

		if ( $variation_id && 0 !== $variation_id ) {
			$id_parts[] = $variation_id;
		}

		if ( is_array( $variation ) && ! empty( $variation ) ) {
			$variation_key = '';
			foreach ( $variation as $key => $value ) {
				$variation_key .= trim( $key ) . trim( $value );
			}
			$id_parts[] = $variation_key;
		}

		if ( is_array( $cart_item_data ) && ! empty( $cart_item_data ) ) {
			$cart_item_data_key = '';
			foreach ( $cart_item_data as $key => $value ) {
				if ( is_array( $value ) || is_object( $value ) ) {
					$value = http_build_query( $value );
				}
				$cart_item_data_key .= trim( $key ) . trim( $value );

			}
			$id_parts[] = $cart_item_data_key;
		}

		return apply_filters( 'woocommerce_cart_id', md5( implode( '_', $id_parts ) ), $product_id, $variation_id, $variation, $cart_item_data );
	}

	/**
	 * Add a product to the cart.
	 *
	 * @throws Exception Plugins can throw an exception to prevent adding to cart.
	 * @param int   $product_id contains the id of the product to add to the cart.
	 * @param int   $quantity contains the quantity of the item to add.
	 * @param int   $variation_id ID of the variation being added to the cart.
	 * @param array $variation attribute values.
	 * @param array $cart_item_data extra cart item data we want to pass into the item.
	 * @return string|bool $cart_item_key
	 */
	public function add_to_cart( $product_id = 0, $quantity = 1, $variation_id = 0, $variation = array(), $cart_item_data = array() ) {
		try {
			$product_id   = absint( $product_id );
			$variation_id = absint( $variation_id );

			// Ensure we don't add a variation to the cart directly by variation ID.
			if ( 'product_variation' === get_post_type( $product_id ) ) {
				$variation_id = $product_id;
				$product_id   = wp_get_post_parent_id( $variation_id );
			}

			$product_data = wc_get_product( $variation_id ? $variation_id : $product_id );
			$quantity     = apply_filters( 'woocommerce_add_to_cart_quantity', $quantity, $product_id );

			if ( $quantity <= 0 || ! $product_data || 'trash' === $product_data->get_status() ) {
				return false;
			}

			if ( $product_data->is_type( 'variation' ) ) {
				$missing_attributes = array();
				$parent_data        = wc_get_product( $product_data->get_parent_id() );

				$variation_attributes = $product_data->get_variation_attributes();
				// Filter out 'any' variations, which are empty, as they need to be explicitly specified while adding to cart.
				$variation_attributes = array_filter( $variation_attributes );

				// Gather posted attributes.
				$posted_attributes = array();
				foreach ( $parent_data->get_attributes() as $attribute ) {
					if ( ! $attribute['is_variation'] ) {
						continue;
					}
					$attribute_key = 'attribute_' . sanitize_title( $attribute['name'] );

					if ( isset( $variation[ $attribute_key ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
						if ( $attribute['is_taxonomy'] ) {
							// Don't use wc_clean as it destroys sanitized characters.
							$value = sanitize_title( wp_unslash( $variation[ $attribute_key ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
						} else {
							$value = html_entity_decode( wc_clean( wp_unslash( $variation[ $attribute_key ] ) ), ENT_QUOTES, get_bloginfo( 'charset' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
						}

						// Don't include if it's empty.
						if ( ! empty( $value ) || '0' === $value ) {
							$posted_attributes[ $attribute_key ] = $value;
						}
					}
				}

				// Merge variation attributes and posted attributes.
				$posted_and_variation_attributes = array_merge( $variation_attributes, $posted_attributes );

				// If no variation ID is set, attempt to get a variation ID from posted attributes.
				if ( empty( $variation_id ) ) {
					$data_store   = WC_Data_Store::load( 'product' );
					$variation_id = $data_store->find_matching_product_variation( $parent_data, $posted_attributes );
				}

				// Do we have a variation ID?
				if ( empty( $variation_id ) ) {
					throw new Exception( __( 'Please choose product options&hellip;', 'woocommerce' ) );
				}

				// Check the data we have is valid.
				$variation_data = wc_get_product_variation_attributes( $variation_id );
				$attributes     = array();

				foreach ( $parent_data->get_attributes() as $attribute ) {
					if ( ! $attribute['is_variation'] ) {
						continue;
					}

					// Get valid value from variation data.
					$attribute_key = 'attribute_' . sanitize_title( $attribute['name'] );
					$valid_value   = isset( $variation_data[ $attribute_key ] ) ? $variation_data[ $attribute_key ] : '';

					/**
					 * If the attribute value was posted, check if it's valid.
					 *
					 * If no attribute was posted, only error if the variation has an 'any' attribute which requires a value.
					 */
					if ( isset( $posted_and_variation_attributes[ $attribute_key ] ) ) {
						$value = $posted_and_variation_attributes[ $attribute_key ];

						// Allow if valid or show error.
						if ( $valid_value === $value ) {
							$attributes[ $attribute_key ] = $value;
						} elseif ( '' === $valid_value && in_array( $value, $attribute->get_slugs(), true ) ) {
							// If valid values are empty, this is an 'any' variation so get all possible values.
							$attributes[ $attribute_key ] = $value;
						} else {
							/* translators: %s: Attribute name. */
							throw new Exception( sprintf( __( 'Invalid value posted for %s', 'woocommerce' ), wc_attribute_label( $attribute['name'] ) ) );
						}
					} elseif ( '' === $valid_value ) {
						$missing_attributes[] = wc_attribute_label( $attribute['name'] );
					}

					$variation = $attributes;
				}
				if ( ! empty( $missing_attributes ) ) {
					/* translators: %s: Attribute name. */
					throw new Exception( sprintf( _n( '%s is a required field', '%s are required fields', count( $missing_attributes ), 'woocommerce' ), wc_format_list_of_items( $missing_attributes ) ) );
				}
			}

			// Validate variation ID.
			if (
				0 < $variation_id && // Only check if there's any variation_id.
				(
					! $product_data->is_type( 'variation' ) || // Check if isn't a variation, it suppose to be a variation at this point.
					$product_data->get_parent_id() !== $product_id // Check if belongs to the selected variable product.
				)
			) {
				$product = wc_get_product( $product_id );

				/* translators: 1: product link, 2: product name */
				throw new Exception( sprintf( __( 'The selected product isn\'t a variation of %2$s, please choose product options by visiting <a href="%1$s" title="%2$s">%2$s</a>.', 'woocommerce' ), esc_url( $product->get_permalink() ), esc_html( $product->get_name() ) ) );
			}

			// Load cart item data - may be added by other plugins.
			$cart_item_data = (array) apply_filters( 'woocommerce_add_cart_item_data', $cart_item_data, $product_id, $variation_id, $quantity );

			// Generate a ID based on product ID, variation ID, variation data, and other cart item data.
			$cart_id = $this->generate_cart_id( $product_id, $variation_id, $variation, $cart_item_data );

			// Find the cart item key in the existing cart.
			$cart_item_key = $this->find_product_in_cart( $cart_id );

			// Force quantity to 1 if sold individually and check for existing item in cart.
			if ( $product_data->is_sold_individually() ) {
				$quantity      = apply_filters( 'woocommerce_add_to_cart_sold_individually_quantity', 1, $quantity, $product_id, $variation_id, $cart_item_data );
				$found_in_cart = apply_filters( 'woocommerce_add_to_cart_sold_individually_found_in_cart', $cart_item_key && $this->cart_contents[ $cart_item_key ]['quantity'] > 0, $product_id, $variation_id, $cart_item_data, $cart_id );

				if ( $found_in_cart ) {
					/* translators: %s: product name */
					$message = sprintf( __( 'You cannot add another "%s" to your cart.', 'woocommerce' ), $product_data->get_name() );

					/**
					 * Filters message about more than 1 product being added to cart.
					 *
					 * @since 4.5.0
					 * @param string     $message Message.
					 * @param WC_Product $product_data Product data.
					 */
					$message = apply_filters( 'woocommerce_cart_product_cannot_add_another_message', $message, $product_data );

					throw new Exception( sprintf( '<a href="%s" class="button wc-forward">%s</a> %s', wc_get_cart_url(), __( 'View cart', 'woocommerce' ), $message ) );
				}
			}

			if ( ! $product_data->is_purchasable() ) {
				$message = __( 'Sorry, this product cannot be purchased.', 'woocommerce' );
				/**
				 * Filters message about product unable to be purchased.
				 *
				 * @since 3.8.0
				 * @param string     $message Message.
				 * @param WC_Product $product_data Product data.
				 */
				$message = apply_filters( 'woocommerce_cart_product_cannot_be_purchased_message', $message, $product_data );
				throw new Exception( $message );
			}

			// Stock check - only check if we're managing stock and backorders are not allowed.
			if ( ! $product_data->is_in_stock() ) {
				/* translators: %s: product name */
				$message = sprintf( __( 'You cannot add &quot;%s&quot; to the cart because the product is out of stock.', 'woocommerce' ), $product_data->get_name() );

				/**
				 * Filters message about product being out of stock.
				 *
				 * @since 4.5.0
				 * @param string     $message Message.
				 * @param WC_Product $product_data Product data.
				 */
				$message = apply_filters( 'woocommerce_cart_product_out_of_stock_message', $message, $product_data );
				throw new Exception( $message );
			}

			if ( ! $product_data->has_enough_stock( $quantity ) ) {
				$stock_quantity = $product_data->get_stock_quantity();

				/* translators: 1: product name 2: quantity in stock */
				$message = sprintf( __( 'You cannot add that amount of &quot;%1$s&quot; to the cart because there is not enough stock (%2$s remaining).', 'woocommerce' ), $product_data->get_name(), wc_format_stock_quantity_for_display( $stock_quantity, $product_data ) );

				/**
				 * Filters message about product not having enough stock.
				 *
				 * @since 4.5.0
				 * @param string     $message Message.
				 * @param WC_Product $product_data Product data.
				 * @param int        $stock_quantity Quantity remaining.
				 */
				$message = apply_filters( 'woocommerce_cart_product_not_enough_stock_message', $message, $product_data, $stock_quantity );

				throw new Exception( $message );
			}

			// Stock check - this time accounting for whats already in-cart.
			if ( $product_data->managing_stock() ) {
				$products_qty_in_cart = $this->get_cart_item_quantities();

				if ( isset( $products_qty_in_cart[ $product_data->get_stock_managed_by_id() ] ) && ! $product_data->has_enough_stock( $products_qty_in_cart[ $product_data->get_stock_managed_by_id() ] + $quantity ) ) {
					$stock_quantity         = $product_data->get_stock_quantity();
					$stock_quantity_in_cart = $products_qty_in_cart[ $product_data->get_stock_managed_by_id() ];

					$message = sprintf(
						'<a href="%s" class="button wc-forward">%s</a> %s',
						wc_get_cart_url(),
						__( 'View cart', 'woocommerce' ),
						/* translators: 1: quantity in stock 2: current quantity */
						sprintf( __( 'You cannot add that amount to the cart &mdash; we have %1$s in stock and you already have %2$s in your cart.', 'woocommerce' ), wc_format_stock_quantity_for_display( $stock_quantity, $product_data ), wc_format_stock_quantity_for_display( $stock_quantity_in_cart, $product_data ) )
					);

					/**
					 * Filters message about product not having enough stock accounting for what's already in the cart.
					 *
					 * @param string $message Message.
					 * @param WC_Product $product_data Product data.
					 * @param int $stock_quantity Quantity remaining.
					 * @param int $stock_quantity_in_cart
					 *
					 * @since 5.3.0
					 */
					$message = apply_filters( 'woocommerce_cart_product_not_enough_stock_already_in_cart_message', $message, $product_data, $stock_quantity, $stock_quantity_in_cart );

					throw new Exception( $message );
				}
			}

			// If cart_item_key is set, the item is already in the cart.
			if ( $cart_item_key ) {
				$new_quantity = $quantity + $this->cart_contents[ $cart_item_key ]['quantity'];
				$this->set_quantity( $cart_item_key, $new_quantity, false );
			} else {
				$cart_item_key = $cart_id;

				// Add item after merging with $cart_item_data - hook to allow plugins to modify cart item.
				$this->cart_contents[ $cart_item_key ] = apply_filters(
					'woocommerce_add_cart_item',
					array_merge(
						$cart_item_data,
						array(
							'key'          => $cart_item_key,
							'product_id'   => $product_id,
							'variation_id' => $variation_id,
							'variation'    => $variation,
							'quantity'     => $quantity,
							'data'         => $product_data,
							'data_hash'    => wc_get_cart_item_data_hash( $product_data ),
						)
					),
					$cart_item_key
				);
			}

			$this->cart_contents = apply_filters( 'woocommerce_cart_contents_changed', $this->cart_contents );

			do_action( 'woocommerce_add_to_cart', $cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data );

			return $cart_item_key;

		} catch ( Exception $e ) {
			if ( $e->getMessage() ) {
				wc_add_notice( $e->getMessage(), 'error' );
			}
			return false;
		}
	}

	/**
	 * Remove a cart item.
	 *
	 * @since  2.3.0
	 * @param  string $cart_item_key Cart item key to remove from the cart.
	 * @return bool
	 */
	public function remove_cart_item( $cart_item_key ) {
		if ( isset( $this->cart_contents[ $cart_item_key ] ) ) {
			$this->removed_cart_contents[ $cart_item_key ] = $this->cart_contents[ $cart_item_key ];

			unset( $this->removed_cart_contents[ $cart_item_key ]['data'] );

			do_action( 'woocommerce_remove_cart_item', $cart_item_key, $this );

			unset( $this->cart_contents[ $cart_item_key ] );

			do_action( 'woocommerce_cart_item_removed', $cart_item_key, $this );

			return true;
		}
		return false;
	}

	/**
	 * Restore a cart item.
	 *
	 * @param  string $cart_item_key Cart item key to restore to the cart.
	 * @return bool
	 */
	public function restore_cart_item( $cart_item_key ) {
		if ( isset( $this->removed_cart_contents[ $cart_item_key ] ) ) {
			$restore_item                                  = $this->removed_cart_contents[ $cart_item_key ];
			$this->cart_contents[ $cart_item_key ]         = $restore_item;
			$this->cart_contents[ $cart_item_key ]['data'] = wc_get_product( $restore_item['variation_id'] ? $restore_item['variation_id'] : $restore_item['product_id'] );

			do_action( 'woocommerce_restore_cart_item', $cart_item_key, $this );

			unset( $this->removed_cart_contents[ $cart_item_key ] );

			do_action( 'woocommerce_cart_item_restored', $cart_item_key, $this );

			return true;
		}
		return false;
	}

	/**
	 * Set the quantity for an item in the cart using it's key.
	 *
	 * @param string $cart_item_key contains the id of the cart item.
	 * @param int    $quantity contains the quantity of the item.
	 * @param bool   $refresh_totals whether or not to calculate totals after setting the new qty. Can be used to defer calculations if setting quantities in bulk.
	 * @return bool
	 */
	public function set_quantity( $cart_item_key, $quantity = 1, $refresh_totals = true ) {
		if ( 0 === $quantity || $quantity < 0 ) {
			wc_do_deprecated_action( 'woocommerce_before_cart_item_quantity_zero', array( $cart_item_key, $this ), '3.7.0', 'woocommerce_remove_cart_item' );
			// If we're setting qty to 0 we're removing the item from the cart.
			return $this->remove_cart_item( $cart_item_key );
		}

		// Update qty.
		$old_quantity                                      = $this->cart_contents[ $cart_item_key ]['quantity'];
		$this->cart_contents[ $cart_item_key ]['quantity'] = $quantity;

		do_action( 'woocommerce_after_cart_item_quantity_update', $cart_item_key, $quantity, $old_quantity, $this );

		if ( $refresh_totals ) {
			$this->calculate_totals();
		}

		/**
		 * Fired after qty has been changed.
		 *
		 * @since 3.6.0
		 * @param string  $cart_item_key contains the id of the cart item. This may be empty if the cart item does not exist any more.
		 * @param int     $quantity contains the quantity of the item.
		 * @param WC_Cart $this Cart class.
		 */
		do_action( 'woocommerce_cart_item_set_quantity', $cart_item_key, $quantity, $this );

		return true;
	}

	/**
	 * Get cart's owner.
	 *
	 * @since  3.2.0
	 * @return WC_Customer
	 */
	public function get_customer() {
		return WC()->customer;
	}

	/**
	 * Calculate totals for the items in the cart.
	 *
	 * @uses WC_Cart_Totals
	 */
	public function calculate_totals() {
		$this->reset_totals();

		if ( $this->is_empty() ) {
			$this->session->set_session();
			return;
		}

		do_action( 'woocommerce_before_calculate_totals', $this );

		new WC_Cart_Totals( $this );

		do_action( 'woocommerce_after_calculate_totals', $this );
	}

	/**
	 * Looks at the totals to see if payment is actually required.
	 *
	 * @return bool
	 */
	public function needs_payment() {
		return apply_filters( 'woocommerce_cart_needs_payment', 0 < $this->get_total( 'edit' ), $this );
	}

	/*
	 * Shipping related functions.
	 */

	/**
	 * Uses the shipping class to calculate shipping then gets the totals when its finished.
	 */
	public function calculate_shipping() {
		$this->shipping_methods = $this->needs_shipping() ? $this->get_chosen_shipping_methods( WC()->shipping()->calculate_shipping( $this->get_shipping_packages() ) ) : array();

		$shipping_taxes = wp_list_pluck( $this->shipping_methods, 'taxes' );
		$merged_taxes   = array();
		foreach ( $shipping_taxes as $taxes ) {
			foreach ( $taxes as $tax_id => $tax_amount ) {
				if ( ! isset( $merged_taxes[ $tax_id ] ) ) {
					$merged_taxes[ $tax_id ] = 0;
				}
				$merged_taxes[ $tax_id ] += $tax_amount;
			}
		}

		$this->set_shipping_total( array_sum( wp_list_pluck( $this->shipping_methods, 'cost' ) ) );
		$this->set_shipping_tax( array_sum( $merged_taxes ) );
		$this->set_shipping_taxes( $merged_taxes );

		return $this->shipping_methods;
	}

	/**
	 * Given a set of packages with rates, get the chosen ones only.
	 *
	 * @since 3.2.0
	 * @param array $calculated_shipping_packages Array of packages.
	 * @return array
	 */
	protected function get_chosen_shipping_methods( $calculated_shipping_packages = array() ) {
		$chosen_methods = array();
		// Get chosen methods for each package to get our totals.
		foreach ( $calculated_shipping_packages as $key => $package ) {
			$chosen_method = wc_get_chosen_shipping_method_for_package( $key, $package );
			if ( $chosen_method ) {
				$chosen_methods[ $key ] = $package['rates'][ $chosen_method ];
			}
		}
		return $chosen_methods;
	}

	/**
	 * Filter items needing shipping callback.
	 *
	 * @since  3.0.0
	 * @param  array $item Item to check for shipping.
	 * @return bool
	 */
	protected function filter_items_needing_shipping( $item ) {
		$product = $item['data'];
		return $product && $product->needs_shipping();
	}

	/**
	 * Get only items that need shipping.
	 *
	 * @since  3.0.0
	 * @return array
	 */
	protected function get_items_needing_shipping() {
		return array_filter( $this->get_cart(), array( $this, 'filter_items_needing_shipping' ) );
	}

	/**
	 * Get packages to calculate shipping for.
	 *
	 * This lets us calculate costs for carts that are shipped to multiple locations.
	 *
	 * Shipping methods are responsible for looping through these packages.
	 *
	 * By default we pass the cart itself as a package - plugins can change this.
	 * through the filter and break it up.
	 *
	 * @since 1.5.4
	 * @return array of cart items
	 */
	public function get_shipping_packages() {
		return apply_filters(
			'woocommerce_cart_shipping_packages',
			array(
				array(
					'contents'        => $this->get_items_needing_shipping(),
					'contents_cost'   => array_sum( wp_list_pluck( $this->get_items_needing_shipping(), 'line_total' ) ),
					'applied_coupons' => $this->get_applied_coupons(),
					'user'            => array(
						'ID' => get_current_user_id(),
					),
					'destination'     => array(
						'country'   => $this->get_customer()->get_shipping_country(),
						'state'     => $this->get_customer()->get_shipping_state(),
						'postcode'  => $this->get_customer()->get_shipping_postcode(),
						'city'      => $this->get_customer()->get_shipping_city(),
						'address'   => $this->get_customer()->get_shipping_address(),
						'address_1' => $this->get_customer()->get_shipping_address(), // Provide both address and address_1 for backwards compatibility.
						'address_2' => $this->get_customer()->get_shipping_address_2(),
					),
					'cart_subtotal'   => $this->get_displayed_subtotal(),
				),
			)
		);
	}

	/**
	 * Looks through the cart to see if shipping is actually required.
	 *
	 * @return bool whether or not the cart needs shipping
	 */
	public function needs_shipping() {
		if ( ! wc_shipping_enabled() || 0 === wc_get_shipping_method_count( true ) ) {
			return false;
		}
		$needs_shipping = false;

		foreach ( $this->get_cart_contents() as $cart_item_key => $values ) {
			if ( $values['data']->needs_shipping() ) {
				$needs_shipping = true;
				break;
			}
		}

		return apply_filters( 'woocommerce_cart_needs_shipping', $needs_shipping );
	}

	/**
	 * Should the shipping address form be shown.
	 *
	 * @return bool
	 */
	public function needs_shipping_address() {
		return apply_filters( 'woocommerce_cart_needs_shipping_address', true === $this->needs_shipping() && ! wc_ship_to_billing_address_only() );
	}

	/**
	 * Sees if the customer has entered enough data to calc the shipping yet.
	 *
	 * @return bool
	 */
	public function show_shipping() {
		if ( ! wc_shipping_enabled() || ! $this->get_cart_contents() ) {
			return false;
		}

		if ( 'yes' === get_option( 'woocommerce_shipping_cost_requires_address' ) ) {
			$country = $this->get_customer()->get_shipping_country();
			if ( ! $country ) {
				return false;
			}
			$country_fields = WC()->countries->get_address_fields( $country, 'shipping_' );
			if ( isset( $country_fields['shipping_state'] ) && $country_fields['shipping_state']['required'] && ! $this->get_customer()->get_shipping_state() ) {
				return false;
			}
			if ( isset( $country_fields['shipping_postcode'] ) && $country_fields['shipping_postcode']['required'] && ! $this->get_customer()->get_shipping_postcode() ) {
				return false;
			}
		}

		return apply_filters( 'woocommerce_cart_ready_to_calc_shipping', true );
	}

	/**
	 * Gets the shipping total (after calculation).
	 *
	 * @return string price or string for the shipping total
	 */
	public function get_cart_shipping_total() {

		// Default total assumes Free shipping.
		$total = __( 'Free!', 'woocommerce' );

		if ( 0 < $this->get_shipping_total() ) {

			if ( $this->display_prices_including_tax() ) {
				$total = wc_price( $this->shipping_total + $this->shipping_tax_total );

				if ( $this->shipping_tax_total > 0 && ! wc_prices_include_tax() ) {
					$total .= ' <small class="tax_label">' . WC()->countries->inc_tax_or_vat() . '</small>';
				}
			} else {
				$total = wc_price( $this->shipping_total );

				if ( $this->shipping_tax_total > 0 && wc_prices_include_tax() ) {
					$total .= ' <small class="tax_label">' . WC()->countries->ex_tax_or_vat() . '</small>';
				}
			}
		}
		return apply_filters( 'woocommerce_cart_shipping_total', $total, $this );
	}

	/**
	 * Check for user coupons (now that we have billing email). If a coupon is invalid, add an error.
	 *
	 * Checks two types of coupons:
	 *  1. Where a list of customer emails are set (limits coupon usage to those defined).
	 *  2. Where a usage_limit_per_user is set (limits coupon usage to a number based on user ID and email).
	 *
	 * @param array $posted Post data.
	 */
	public function check_customer_coupons( $posted ) {
		foreach ( $this->get_applied_coupons() as $code ) {
			$coupon = new WC_Coupon( $code );

			if ( $coupon->is_valid() ) {

				// Get user and posted emails to compare.
				$current_user  = wp_get_current_user();
				$billing_email = isset( $posted['billing_email'] ) ? $posted['billing_email'] : '';
				$check_emails  = array_unique(
					array_filter(
						array_map(
							'strtolower',
							array_map(
								'sanitize_email',
								array(
									$billing_email,
									$current_user->user_email,
								)
							)
						)
					)
				);

				// Limit to defined email addresses.
				$restrictions = $coupon->get_email_restrictions();

				if ( is_array( $restrictions ) && 0 < count( $restrictions ) && ! $this->is_coupon_emails_allowed( $check_emails, $restrictions ) ) {
					$coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_NOT_YOURS_REMOVED );
					$this->remove_coupon( $code );
				}

				$coupon_usage_limit = $coupon->get_usage_limit_per_user();
				if ( 0 < $coupon_usage_limit && 0 === get_current_user_id() ) {
					// For guest, usage per user has not been enforced yet. Enforce it now.
					$coupon_data_store = $coupon->get_data_store();
					$billing_email     = strtolower( sanitize_email( $billing_email ) );
					if ( $coupon_data_store && $coupon_data_store->get_usage_by_email( $coupon, $billing_email ) >= $coupon_usage_limit ) {
						if ( $coupon_data_store->get_tentative_usages_for_user( $coupon->get_id(), array( $billing_email ) ) ) {
							$coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK_GUEST );
						} else {
							$coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED );
						}
					}
				}
			}
		}
	}

	/**
	 * Checks if the given email address(es) matches the ones specified on the coupon.
	 *
	 * @param array $check_emails Array of customer email addresses.
	 * @param array $restrictions Array of allowed email addresses.
	 * @return bool
	 */
	public function is_coupon_emails_allowed( $check_emails, $restrictions ) {

		foreach ( $check_emails as $check_email ) {
			// With a direct match we return true.
			if ( in_array( $check_email, $restrictions, true ) ) {
				return true;
			}

			// Go through the allowed emails and return true if the email matches a wildcard.
			foreach ( $restrictions as $restriction ) {
				// Convert to PHP-regex syntax.
				$regex = '/^' . str_replace( '*', '(.+)?', $restriction ) . '$/';
				preg_match( $regex, $check_email, $match );
				if ( ! empty( $match ) ) {
					return true;
				}
			}
		}

		// No matches, this one isn't allowed.
		return false;
	}


	/**
	 * Returns whether or not a discount has been applied.
	 *
	 * @param string $coupon_code Coupon code to check.
	 * @return bool
	 */
	public function has_discount( $coupon_code = '' ) {
		return $coupon_code ? in_array( wc_format_coupon_code( $coupon_code ), $this->applied_coupons, true ) : count( $this->applied_coupons ) > 0;
	}

	/**
	 * Applies a coupon code passed to the method.
	 *
	 * @param string $coupon_code - The code to apply.
	 * @return bool True if the coupon is applied, false if it does not exist or cannot be applied.
	 */
	public function apply_coupon( $coupon_code ) {
		// Coupons are globally disabled.
		if ( ! wc_coupons_enabled() ) {
			return false;
		}

		// Sanitize coupon code.
		$coupon_code = wc_format_coupon_code( $coupon_code );

		// Get the coupon.
		$the_coupon = new WC_Coupon( $coupon_code );

		// Prevent adding coupons by post ID.
		if ( $the_coupon->get_code() !== $coupon_code ) {
			$the_coupon->set_code( $coupon_code );
			$the_coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_NOT_EXIST );
			return false;
		}

		// Check it can be used with cart.
		if ( ! $the_coupon->is_valid() ) {
			wc_add_notice( $the_coupon->get_error_message(), 'error' );
			return false;
		}

		// Check if applied.
		if ( $this->has_discount( $coupon_code ) ) {
			$the_coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_ALREADY_APPLIED );
			return false;
		}

		// If its individual use then remove other coupons.
		if ( $the_coupon->get_individual_use() ) {
			$coupons_to_keep = apply_filters( 'woocommerce_apply_individual_use_coupon', array(), $the_coupon, $this->applied_coupons );

			foreach ( $this->applied_coupons as $applied_coupon ) {
				$keep_key = array_search( $applied_coupon, $coupons_to_keep, true );
				if ( false === $keep_key ) {
					$this->remove_coupon( $applied_coupon );
				} else {
					unset( $coupons_to_keep[ $keep_key ] );
				}
			}

			if ( ! empty( $coupons_to_keep ) ) {
				$this->applied_coupons += $coupons_to_keep;
			}
		}

		// Check to see if an individual use coupon is set.
		if ( $this->applied_coupons ) {
			foreach ( $this->applied_coupons as $code ) {
				$coupon = new WC_Coupon( $code );

				if ( $coupon->get_individual_use() && false === apply_filters( 'woocommerce_apply_with_individual_use_coupon', false, $the_coupon, $coupon, $this->applied_coupons ) ) {

					// Reject new coupon.
					$coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_ALREADY_APPLIED_INDIV_USE_ONLY );

					return false;
				}
			}
		}

		$this->applied_coupons[] = $coupon_code;

		// Choose free shipping.
		if ( $the_coupon->get_free_shipping() ) {
			$packages                = WC()->shipping()->get_packages();
			$chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' );

			foreach ( $packages as $i => $package ) {
				$chosen_shipping_methods[ $i ] = 'free_shipping';
			}

			WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods );
		}

		$the_coupon->add_coupon_message( WC_Coupon::WC_COUPON_SUCCESS );

		do_action( 'woocommerce_applied_coupon', $coupon_code );

		return true;
	}

	/**
	 * Get array of applied coupon objects and codes.
	 *
	 * @param null $deprecated No longer used.
	 * @return array of applied coupons
	 */
	public function get_coupons( $deprecated = null ) {
		$coupons = array();

		if ( 'order' === $deprecated ) {
			return $coupons;
		}

		foreach ( $this->get_applied_coupons() as $code ) {
			$coupon           = new WC_Coupon( $code );
			$coupons[ $code ] = $coupon;
		}

		return $coupons;
	}

	/**
	 * Get the discount amount for a used coupon.
	 *
	 * @param  string $code coupon code.
	 * @param  bool   $ex_tax inc or ex tax.
	 * @return float discount amount
	 */
	public function get_coupon_discount_amount( $code, $ex_tax = true ) {
		$totals          = $this->get_coupon_discount_totals();
		$discount_amount = isset( $totals[ $code ] ) ? $totals[ $code ] : 0;

		if ( ! $ex_tax ) {
			$discount_amount += $this->get_coupon_discount_tax_amount( $code );
		}

		return wc_cart_round_discount( $discount_amount, wc_get_price_decimals() );
	}

	/**
	 * Get the discount tax amount for a used coupon (for tax inclusive prices).
	 *
	 * @param  string $code coupon code.
	 * @return float discount amount
	 */
	public function get_coupon_discount_tax_amount( $code ) {
		$totals = $this->get_coupon_discount_tax_totals();
		return wc_cart_round_discount( isset( $totals[ $code ] ) ? $totals[ $code ] : 0, wc_get_price_decimals() );
	}

	/**
	 * Remove coupons from the cart of a defined type. Type 1 is before tax, type 2 is after tax.
	 *
	 * @param null $deprecated No longer used.
	 */
	public function remove_coupons( $deprecated = null ) {
		$this->set_coupon_discount_totals( array() );
		$this->set_coupon_discount_tax_totals( array() );
		$this->set_applied_coupons( array() );
		$this->session->set_session();
	}

	/**
	 * Remove a single coupon by code.
	 *
	 * @param  string $coupon_code Code of the coupon to remove.
	 * @return bool
	 */
	public function remove_coupon( $coupon_code ) {
		$coupon_code = wc_format_coupon_code( $coupon_code );
		$position    = array_search( $coupon_code, array_map( 'wc_format_coupon_code', $this->get_applied_coupons() ), true );

		if ( false !== $position ) {
			unset( $this->applied_coupons[ $position ] );
		}

		WC()->session->set( 'refresh_totals', true );

		do_action( 'woocommerce_removed_coupon', $coupon_code );

		return true;
	}

	/**
	 * Trigger an action so 3rd parties can add custom fees.
	 *
	 * @since 2.0.0
	 */
	public function calculate_fees() {
		do_action( 'woocommerce_cart_calculate_fees', $this );
	}

	/**
	 * Return reference to fees API.
	 *
	 * @since  3.2.0
	 * @return WC_Cart_Fees
	 */
	public function fees_api() {
		return $this->fees_api;
	}

	/**
	 * Add additional fee to the cart.
	 *
	 * This method should be called on a callback attached to the
	 * woocommerce_cart_calculate_fees action during cart/checkout. Fees do not
	 * persist.
	 *
	 * @uses WC_Cart_Fees::add_fee
	 * @param string $name      Unique name for the fee. Multiple fees of the same name cannot be added.
	 * @param float  $amount    Fee amount (do not enter negative amounts).
	 * @param bool   $taxable   Is the fee taxable? (default: false).
	 * @param string $tax_class The tax class for the fee if taxable. A blank string is standard tax class. (default: '').
	 */
	public function add_fee( $name, $amount, $taxable = false, $tax_class = '' ) {
		$this->fees_api()->add_fee(
			array(
				'name'      => $name,
				'amount'    => (float) $amount,
				'taxable'   => $taxable,
				'tax_class' => $tax_class,
			)
		);
	}

	/**
	 * Return all added fees from the Fees API.
	 *
	 * @uses WC_Cart_Fees::get_fees
	 * @return array
	 */
	public function get_fees() {
		$fees = $this->fees_api()->get_fees();

		if ( property_exists( $this, 'fees' ) ) {
			$fees = $fees + (array) $this->fees;
		}
		return $fees;
	}

	/**
	 * Gets the total excluding taxes.
	 *
	 * @return string formatted price
	 */
	public function get_total_ex_tax() {
		return apply_filters( 'woocommerce_cart_total_ex_tax', wc_price( max( 0, $this->get_total( 'edit' ) - $this->get_total_tax() ) ) );
	}

	/**
	 * Gets the cart contents total (after calculation).
	 *
	 * @return string formatted price
	 */
	public function get_cart_total() {
		return apply_filters( 'woocommerce_cart_contents_total', wc_price( wc_prices_include_tax() ? $this->get_cart_contents_total() + $this->get_cart_contents_tax() : $this->get_cart_contents_total() ) );
	}

	/**
	 * Gets the sub total (after calculation).
	 *
	 * @param bool $compound whether to include compound taxes.
	 * @return string formatted price
	 */
	public function get_cart_subtotal( $compound = false ) {
		/**
		 * If the cart has compound tax, we want to show the subtotal as cart + shipping + non-compound taxes (after discount).
		 */
		if ( $compound ) {
			$cart_subtotal = wc_price( $this->get_cart_contents_total() + $this->get_shipping_total() + $this->get_taxes_total( false, false ) );

		} elseif ( $this->display_prices_including_tax() ) {
			$cart_subtotal = wc_price( $this->get_subtotal() + $this->get_subtotal_tax() );

			if ( $this->get_subtotal_tax() > 0 && ! wc_prices_include_tax() ) {
				$cart_subtotal .= ' <small class="tax_label">' . WC()->countries->inc_tax_or_vat() . '</small>';
			}
		} else {
			$cart_subtotal = wc_price( $this->get_subtotal() );

			if ( $this->get_subtotal_tax() > 0 && wc_prices_include_tax() ) {
				$cart_subtotal .= ' <small class="tax_label">' . WC()->countries->ex_tax_or_vat() . '</small>';
			}
		}

		return apply_filters( 'woocommerce_cart_subtotal', $cart_subtotal, $compound, $this );
	}

	/**
	 * Get the product row price per item.
	 *
	 * @param WC_Product $product Product object.
	 * @return string formatted price
	 */
	public function get_product_price( $product ) {
		if ( $this->display_prices_including_tax() ) {
			$product_price = wc_get_price_including_tax( $product );
		} else {
			$product_price = wc_get_price_excluding_tax( $product );
		}
		return apply_filters( 'woocommerce_cart_product_price', wc_price( $product_price ), $product );
	}

	/**
	 * Get the product row subtotal.
	 *
	 * Gets the tax etc to avoid rounding issues.
	 *
	 * When on the checkout (review order), this will get the subtotal based on the customer's tax rate rather than the base rate.
	 *
	 * @param WC_Product $product Product object.
	 * @param int        $quantity Quantity being purchased.
	 * @return string formatted price
	 */
	public function get_product_subtotal( $product, $quantity ) {
		$price = $product->get_price();

		if ( $product->is_taxable() ) {

			if ( $this->display_prices_including_tax() ) {
				$row_price        = wc_get_price_including_tax( $product, array( 'qty' => $quantity ) );
				$product_subtotal = wc_price( $row_price );

				if ( ! wc_prices_include_tax() && $this->get_subtotal_tax() > 0 ) {
					$product_subtotal .= ' <small class="tax_label">' . WC()->countries->inc_tax_or_vat() . '</small>';
				}
			} else {
				$row_price        = wc_get_price_excluding_tax( $product, array( 'qty' => $quantity ) );
				$product_subtotal = wc_price( $row_price );

				if ( wc_prices_include_tax() && $this->get_subtotal_tax() > 0 ) {
					$product_subtotal .= ' <small class="tax_label">' . WC()->countries->ex_tax_or_vat() . '</small>';
				}
			}
		} else {
			$row_price        = $price * $quantity;
			$product_subtotal = wc_price( $row_price );
		}

		return apply_filters( 'woocommerce_cart_product_subtotal', $product_subtotal, $product, $quantity, $this );
	}

	/**
	 * Gets the cart tax (after calculation).
	 *
	 * @return string formatted price
	 */
	public function get_cart_tax() {
		$cart_total_tax = wc_round_tax_total( $this->get_cart_contents_tax() + $this->get_shipping_tax() + $this->get_fee_tax() );

		return apply_filters( 'woocommerce_get_cart_tax', $cart_total_tax ? wc_price( $cart_total_tax ) : '' );
	}

	/**
	 * Get a tax amount.
	 *
	 * @param  string $tax_rate_id ID of the tax rate to get taxes for.
	 * @return float amount
	 */
	public function get_tax_amount( $tax_rate_id ) {
		$taxes = wc_array_merge_recursive_numeric( $this->get_cart_contents_taxes(), $this->get_fee_taxes() );
		return isset( $taxes[ $tax_rate_id ] ) ? $taxes[ $tax_rate_id ] : 0;
	}

	/**
	 * Get a tax amount.
	 *
	 * @param  string $tax_rate_id ID of the tax rate to get taxes for.
	 * @return float amount
	 */
	public function get_shipping_tax_amount( $tax_rate_id ) {
		$taxes = $this->get_shipping_taxes();
		return isset( $taxes[ $tax_rate_id ] ) ? $taxes[ $tax_rate_id ] : 0;
	}

	/**
	 * Get tax row amounts with or without compound taxes includes.
	 *
	 * @param  bool $compound True if getting compound taxes.
	 * @param  bool $display  True if getting total to display.
	 * @return float price
	 */
	public function get_taxes_total( $compound = true, $display = true ) {
		$total = 0;
		$taxes = $this->get_taxes();
		foreach ( $taxes as $key => $tax ) {
			if ( ! $compound && WC_Tax::is_compound( $key ) ) {
				continue;
			}
			$total += $tax;
		}
		if ( $display ) {
			$total = wc_format_decimal( $total, wc_get_price_decimals() );
		}
		return apply_filters( 'woocommerce_cart_taxes_total', $total, $compound, $display, $this );
	}

	/**
	 * Gets the total discount amount.
	 *
	 * @return mixed formatted price or false if there are none
	 */
	public function get_total_discount() {
		return apply_filters( 'woocommerce_cart_total_discount', $this->get_discount_total() ? wc_price( $this->get_discount_total() ) : false, $this );
	}

	/**
	 * Reset cart totals to the defaults. Useful before running calculations.
	 */
	private function reset_totals() {
		$this->totals = $this->default_totals;
		$this->fees_api->remove_all_fees();
		do_action( 'woocommerce_cart_reset', $this, false );
	}

	/**
	 * Returns 'incl' if tax should be included in cart, otherwise returns 'excl'.
	 *
	 * @return string
	 */
	public function get_tax_price_display_mode() {
		if ( $this->get_customer() && $this->get_customer()->get_is_vat_exempt() ) {
			return 'excl';
		}

		return get_option( 'woocommerce_tax_display_cart' );
	}

	/**
	 * Returns the hash based on cart contents.
	 *
	 * @since 3.6.0
	 * @return string hash for cart content
	 */
	public function get_cart_hash() {
		$cart_session = $this->session->get_cart_for_session();
		$hash         = $cart_session ? md5( wp_json_encode( $cart_session ) . $this->get_total( 'edit' ) ) : '';
		$hash         = apply_filters_deprecated( 'woocommerce_add_to_cart_hash', array( $hash, $cart_session ), '3.6.0', 'woocommerce_cart_hash' );

		return apply_filters( 'woocommerce_cart_hash', $hash, $cart_session );
	}
}
abstracts/abstract-wc-object-query.php000064400000003703151336044760014100 0ustar00<?php
/**
 * Query abstraction layer functionality.
 *
 * @package  WooCommerce\Abstracts
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Abstract WC Object Query Class
 *
 * Extended by classes to provide a query abstraction layer for safe object searching.
 *
 * @version  3.1.0
 * @package  WooCommerce\Abstracts
 */
abstract class WC_Object_Query {

	/**
	 * Stores query data.
	 *
	 * @var array
	 */
	protected $query_vars = array();

	/**
	 * Create a new query.
	 *
	 * @param array $args Criteria to query on in a format similar to WP_Query.
	 */
	public function __construct( $args = array() ) {
		$this->query_vars = wp_parse_args( $args, $this->get_default_query_vars() );
	}

	/**
	 * Get the current query vars.
	 *
	 * @return array
	 */
	public function get_query_vars() {
		return $this->query_vars;
	}

	/**
	 * Get the value of a query variable.
	 *
	 * @param string $query_var Query variable to get value for.
	 * @param mixed  $default Default value if query variable is not set.
	 * @return mixed Query variable value if set, otherwise default.
	 */
	public function get( $query_var, $default = '' ) {
		if ( isset( $this->query_vars[ $query_var ] ) ) {
			return $this->query_vars[ $query_var ];
		}
		return $default;
	}

	/**
	 * Set a query variable.
	 *
	 * @param string $query_var Query variable to set.
	 * @param mixed  $value Value to set for query variable.
	 */
	public function set( $query_var, $value ) {
		$this->query_vars[ $query_var ] = $value;
	}

	/**
	 * Get the default allowed query vars.
	 *
	 * @return array
	 */
	protected function get_default_query_vars() {

		return array(
			'name'           => '',
			'parent'         => '',
			'parent_exclude' => '',
			'exclude'        => '',

			'limit'          => get_option( 'posts_per_page' ),
			'page'           => 1,
			'offset'         => '',
			'paginate'       => false,

			'order'          => 'DESC',
			'orderby'        => 'date',

			'return'         => 'objects',
		);
	}
}
abstracts/abstract-wc-payment-token.php000064400000013263151336044760014264 0ustar00<?php
/**
 * Abstract payment tokens
 *
 * Generic payment tokens functionality which can be extended by individual types of payment tokens.
 *
 * @class WC_Payment_Token
 * @package WooCommerce\Abstracts
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

require_once WC_ABSPATH . 'includes/legacy/abstract-wc-legacy-payment-token.php';

/**
 * WooCommerce Payment Token.
 *
 * Representation of a general payment token to be extended by individuals types of tokens
 * examples: Credit Card, eCheck.
 *
 * @class       WC_Payment_Token
 * @version     3.0.0
 * @since       2.6.0
 * @package     WooCommerce\Abstracts
 */
abstract class WC_Payment_Token extends WC_Legacy_Payment_Token {

	/**
	 * Token Data (stored in the payment_tokens table).
	 *
	 * @var array
	 */
	protected $data = array(
		'gateway_id' => '',
		'token'      => '',
		'is_default' => false,
		'user_id'    => 0,
		'type'       => '',
	);

	/**
	 * Token Type (CC, eCheck, or a custom type added by an extension).
	 * Set by child classes.
	 *
	 * @var string
	 */
	protected $type = '';

	/**
	 * Initialize a payment token.
	 *
	 * These fields are accepted by all payment tokens:
	 * is_default   - boolean Optional - Indicates this is the default payment token for a user
	 * token        - string  Required - The actual token to store
	 * gateway_id   - string  Required - Identifier for the gateway this token is associated with
	 * user_id      - int     Optional - ID for the user this token is associated with. 0 if this token is not associated with a user
	 *
	 * @since 2.6.0
	 * @param mixed $token Token.
	 */
	public function __construct( $token = '' ) {
		parent::__construct( $token );

		if ( is_numeric( $token ) ) {
			$this->set_id( $token );
		} elseif ( is_object( $token ) ) {
			$token_id = $token->get_id();
			if ( ! empty( $token_id ) ) {
				$this->set_id( $token->get_id() );
			}
		} else {
			$this->set_object_read( true );
		}

		$this->data_store = WC_Data_Store::load( 'payment-token' );
		if ( $this->get_id() > 0 ) {
			$this->data_store->read( $this );
		}
	}

	/*
	 *--------------------------------------------------------------------------
	 * Getters
	 *--------------------------------------------------------------------------
	 */

	/**
	 * Returns the raw payment token.
	 *
	 * @since  2.6.0
	 * @param  string $context Context in which to call this.
	 * @return string Raw token
	 */
	public function get_token( $context = 'view' ) {
		return $this->get_prop( 'token', $context );
	}

	/**
	 * Returns the type of this payment token (CC, eCheck, or something else).
	 * Overwritten by child classes.
	 *
	 * @since  2.6.0
	 * @param  string $deprecated Deprecated since WooCommerce 3.0.
	 * @return string Payment Token Type (CC, eCheck)
	 */
	public function get_type( $deprecated = '' ) {
		return $this->type;
	}

	/**
	 * Get type to display to user.
	 * Get's overwritten by child classes.
	 *
	 * @since  2.6.0
	 * @param  string $deprecated Deprecated since WooCommerce 3.0.
	 * @return string
	 */
	public function get_display_name( $deprecated = '' ) {
		return $this->get_type();
	}

	/**
	 * Returns the user ID associated with the token or false if this token is not associated.
	 *
	 * @since 2.6.0
	 * @param  string $context In what context to execute this.
	 * @return int User ID if this token is associated with a user or 0 if no user is associated
	 */
	public function get_user_id( $context = 'view' ) {
		return $this->get_prop( 'user_id', $context );
	}

	/**
	 * Returns the ID of the gateway associated with this payment token.
	 *
	 * @since 2.6.0
	 * @param  string $context In what context to execute this.
	 * @return string Gateway ID
	 */
	public function get_gateway_id( $context = 'view' ) {
		return $this->get_prop( 'gateway_id', $context );
	}

	/**
	 * Returns the ID of the gateway associated with this payment token.
	 *
	 * @since 2.6.0
	 * @param  string $context In what context to execute this.
	 * @return string Gateway ID
	 */
	public function get_is_default( $context = 'view' ) {
		return $this->get_prop( 'is_default', $context );
	}

	/*
	 |--------------------------------------------------------------------------
	 | Setters
	 |--------------------------------------------------------------------------
	 */

	/**
	 * Set the raw payment token.
	 *
	 * @since 2.6.0
	 * @param string $token Payment token.
	 */
	public function set_token( $token ) {
		$this->set_prop( 'token', $token );
	}

	/**
	 * Set the user ID for the user associated with this order.
	 *
	 * @since 2.6.0
	 * @param int $user_id User ID.
	 */
	public function set_user_id( $user_id ) {
		$this->set_prop( 'user_id', absint( $user_id ) );
	}

	/**
	 * Set the gateway ID.
	 *
	 * @since 2.6.0
	 * @param string $gateway_id Gateway ID.
	 */
	public function set_gateway_id( $gateway_id ) {
		$this->set_prop( 'gateway_id', $gateway_id );
	}

	/**
	 * Marks the payment as default or non-default.
	 *
	 * @since 2.6.0
	 * @param boolean $is_default True or false.
	 */
	public function set_default( $is_default ) {
		$this->set_prop( 'is_default', (bool) $is_default );
	}

	/*
	 |--------------------------------------------------------------------------
	 | Other Methods
	 |--------------------------------------------------------------------------
	 */

	/**
	 * Returns if the token is marked as default.
	 *
	 * @since 2.6.0
	 * @return boolean True if the token is default
	 */
	public function is_default() {
		return (bool) $this->get_prop( 'is_default', 'view' );
	}

	/**
	 * Validate basic token info (token and type are required).
	 *
	 * @since 2.6.0
	 * @return boolean True if the passed data is valid
	 */
	public function validate() {
		$token = $this->get_prop( 'token', 'edit' );
		if ( empty( $token ) ) {
			return false;
		}
		return true;
	}

}
abstracts/abstract-wc-log-handler.php000064400000002612151336044760013661 0ustar00<?php
/**
 * Log handling functionality.
 *
 * @class WC_Log_Handler
 * @package WooCommerce\Abstracts
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Abstract WC Log Handler Class
 *
 * @version        1.0.0
 * @package        WooCommerce\Abstracts
 */
abstract class WC_Log_Handler implements WC_Log_Handler_Interface {

	/**
	 * Formats a timestamp for use in log messages.
	 *
	 * @param int $timestamp Log timestamp.
	 * @return string Formatted time for use in log entry.
	 */
	protected static function format_time( $timestamp ) {
		return date( 'c', $timestamp );
	}

	/**
	 * Builds a log entry text from level, timestamp and message.
	 *
	 * @param int    $timestamp Log timestamp.
	 * @param string $level emergency|alert|critical|error|warning|notice|info|debug.
	 * @param string $message Log message.
	 * @param array  $context Additional information for log handlers.
	 *
	 * @return string Formatted log entry.
	 */
	protected static function format_entry( $timestamp, $level, $message, $context ) {
		$time_string  = self::format_time( $timestamp );
		$level_string = strtoupper( $level );
		$entry        = "{$time_string} {$level_string} {$message}";

		return apply_filters(
			'woocommerce_format_log_entry',
			$entry,
			array(
				'timestamp' => $timestamp,
				'level'     => $level,
				'message'   => $message,
				'context'   => $context,
			)
		);
	}
}
abstracts/abstract-wc-product.php000064400000161046151336044760013154 0ustar00<?php
/**
 * WooCommerce product base class.
 *
 * @package WooCommerce\Abstracts
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore as ProductAttributesLookupDataStore;

/**
 * Legacy product contains all deprecated methods for this class and can be
 * removed in the future.
 */
require_once WC_ABSPATH . 'includes/legacy/abstract-wc-legacy-product.php';

/**
 * Abstract Product Class
 *
 * The WooCommerce product class handles individual product data.
 *
 * @version 3.0.0
 * @package WooCommerce\Abstracts
 */
class WC_Product extends WC_Abstract_Legacy_Product {

	/**
	 * This is the name of this object type.
	 *
	 * @var string
	 */
	protected $object_type = 'product';

	/**
	 * Post type.
	 *
	 * @var string
	 */
	protected $post_type = 'product';

	/**
	 * Cache group.
	 *
	 * @var string
	 */
	protected $cache_group = 'products';

	/**
	 * Stores product data.
	 *
	 * @var array
	 */
	protected $data = array(
		'name'               => '',
		'slug'               => '',
		'date_created'       => null,
		'date_modified'      => null,
		'status'             => false,
		'featured'           => false,
		'catalog_visibility' => 'visible',
		'description'        => '',
		'short_description'  => '',
		'sku'                => '',
		'price'              => '',
		'regular_price'      => '',
		'sale_price'         => '',
		'date_on_sale_from'  => null,
		'date_on_sale_to'    => null,
		'total_sales'        => '0',
		'tax_status'         => 'taxable',
		'tax_class'          => '',
		'manage_stock'       => false,
		'stock_quantity'     => null,
		'stock_status'       => 'instock',
		'backorders'         => 'no',
		'low_stock_amount'   => '',
		'sold_individually'  => false,
		'weight'             => '',
		'length'             => '',
		'width'              => '',
		'height'             => '',
		'upsell_ids'         => array(),
		'cross_sell_ids'     => array(),
		'parent_id'          => 0,
		'reviews_allowed'    => true,
		'purchase_note'      => '',
		'attributes'         => array(),
		'default_attributes' => array(),
		'menu_order'         => 0,
		'post_password'      => '',
		'virtual'            => false,
		'downloadable'       => false,
		'category_ids'       => array(),
		'tag_ids'            => array(),
		'shipping_class_id'  => 0,
		'downloads'          => array(),
		'image_id'           => '',
		'gallery_image_ids'  => array(),
		'download_limit'     => -1,
		'download_expiry'    => -1,
		'rating_counts'      => array(),
		'average_rating'     => 0,
		'review_count'       => 0,
	);

	/**
	 * Supported features such as 'ajax_add_to_cart'.
	 *
	 * @var array
	 */
	protected $supports = array();

	/**
	 * Get the product if ID is passed, otherwise the product is new and empty.
	 * This class should NOT be instantiated, but the wc_get_product() function
	 * should be used. It is possible, but the wc_get_product() is preferred.
	 *
	 * @param int|WC_Product|object $product Product to init.
	 */
	public function __construct( $product = 0 ) {
		parent::__construct( $product );
		if ( is_numeric( $product ) && $product > 0 ) {
			$this->set_id( $product );
		} elseif ( $product instanceof self ) {
			$this->set_id( absint( $product->get_id() ) );
		} elseif ( ! empty( $product->ID ) ) {
			$this->set_id( absint( $product->ID ) );
		} else {
			$this->set_object_read( true );
		}

		$this->data_store = WC_Data_Store::load( 'product-' . $this->get_type() );
		if ( $this->get_id() > 0 ) {
			$this->data_store->read( $this );
		}
	}

	/**
	 * Get internal type. Should return string and *should be overridden* by child classes.
	 *
	 * The product_type property is deprecated but is used here for BW compatibility with child classes which may be defining product_type and not have a get_type method.
	 *
	 * @since  3.0.0
	 * @return string
	 */
	public function get_type() {
		return isset( $this->product_type ) ? $this->product_type : 'simple';
	}

	/**
	 * Get product name.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_name( $context = 'view' ) {
		return $this->get_prop( 'name', $context );
	}

	/**
	 * Get product slug.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_slug( $context = 'view' ) {
		return $this->get_prop( 'slug', $context );
	}

	/**
	 * Get product created date.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return WC_DateTime|NULL object if the date is set or null if there is no date.
	 */
	public function get_date_created( $context = 'view' ) {
		return $this->get_prop( 'date_created', $context );
	}

	/**
	 * Get product modified date.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return WC_DateTime|NULL object if the date is set or null if there is no date.
	 */
	public function get_date_modified( $context = 'view' ) {
		return $this->get_prop( 'date_modified', $context );
	}

	/**
	 * Get product status.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_status( $context = 'view' ) {
		return $this->get_prop( 'status', $context );
	}

	/**
	 * If the product is featured.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return boolean
	 */
	public function get_featured( $context = 'view' ) {
		return $this->get_prop( 'featured', $context );
	}

	/**
	 * Get catalog visibility.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_catalog_visibility( $context = 'view' ) {
		return $this->get_prop( 'catalog_visibility', $context );
	}

	/**
	 * Get product description.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_description( $context = 'view' ) {
		return $this->get_prop( 'description', $context );
	}

	/**
	 * Get product short description.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_short_description( $context = 'view' ) {
		return $this->get_prop( 'short_description', $context );
	}

	/**
	 * Get SKU (Stock-keeping unit) - product unique ID.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_sku( $context = 'view' ) {
		return $this->get_prop( 'sku', $context );
	}

	/**
	 * Returns the product's active price.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string price
	 */
	public function get_price( $context = 'view' ) {
		return $this->get_prop( 'price', $context );
	}

	/**
	 * Returns the product's regular price.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string price
	 */
	public function get_regular_price( $context = 'view' ) {
		return $this->get_prop( 'regular_price', $context );
	}

	/**
	 * Returns the product's sale price.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string price
	 */
	public function get_sale_price( $context = 'view' ) {
		return $this->get_prop( 'sale_price', $context );
	}

	/**
	 * Get date on sale from.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return WC_DateTime|NULL object if the date is set or null if there is no date.
	 */
	public function get_date_on_sale_from( $context = 'view' ) {
		return $this->get_prop( 'date_on_sale_from', $context );
	}

	/**
	 * Get date on sale to.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return WC_DateTime|NULL object if the date is set or null if there is no date.
	 */
	public function get_date_on_sale_to( $context = 'view' ) {
		return $this->get_prop( 'date_on_sale_to', $context );
	}

	/**
	 * Get number total of sales.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return int
	 */
	public function get_total_sales( $context = 'view' ) {
		return $this->get_prop( 'total_sales', $context );
	}

	/**
	 * Returns the tax status.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_tax_status( $context = 'view' ) {
		return $this->get_prop( 'tax_status', $context );
	}

	/**
	 * Returns the tax class.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_tax_class( $context = 'view' ) {
		return $this->get_prop( 'tax_class', $context );
	}

	/**
	 * Return if product manage stock.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return boolean
	 */
	public function get_manage_stock( $context = 'view' ) {
		return $this->get_prop( 'manage_stock', $context );
	}

	/**
	 * Returns number of items available for sale.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return int|null
	 */
	public function get_stock_quantity( $context = 'view' ) {
		return $this->get_prop( 'stock_quantity', $context );
	}

	/**
	 * Return the stock status.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @since  3.0.0
	 * @return string
	 */
	public function get_stock_status( $context = 'view' ) {
		return $this->get_prop( 'stock_status', $context );
	}

	/**
	 * Get backorders.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @since  3.0.0
	 * @return string yes no or notify
	 */
	public function get_backorders( $context = 'view' ) {
		return $this->get_prop( 'backorders', $context );
	}

	/**
	 * Get low stock amount.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @since  3.5.0
	 * @return int|string Returns empty string if value not set
	 */
	public function get_low_stock_amount( $context = 'view' ) {
		return $this->get_prop( 'low_stock_amount', $context );
	}

	/**
	 * Return if should be sold individually.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @since  3.0.0
	 * @return boolean
	 */
	public function get_sold_individually( $context = 'view' ) {
		return $this->get_prop( 'sold_individually', $context );
	}

	/**
	 * Returns the product's weight.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_weight( $context = 'view' ) {
		return $this->get_prop( 'weight', $context );
	}

	/**
	 * Returns the product length.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_length( $context = 'view' ) {
		return $this->get_prop( 'length', $context );
	}

	/**
	 * Returns the product width.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_width( $context = 'view' ) {
		return $this->get_prop( 'width', $context );
	}

	/**
	 * Returns the product height.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_height( $context = 'view' ) {
		return $this->get_prop( 'height', $context );
	}

	/**
	 * Returns formatted dimensions.
	 *
	 * @param  bool $formatted True by default for legacy support - will be false/not set in future versions to return the array only. Use wc_format_dimensions for formatted versions instead.
	 * @return string|array
	 */
	public function get_dimensions( $formatted = true ) {
		if ( $formatted ) {
			wc_deprecated_argument( 'WC_Product::get_dimensions', '3.0', 'By default, get_dimensions has an argument set to true so that HTML is returned. This is to support the legacy version of the method. To get HTML dimensions, instead use wc_format_dimensions() function. Pass false to this method to return an array of dimensions. This will be the new default behavior in future versions.' );
			return apply_filters( 'woocommerce_product_dimensions', wc_format_dimensions( $this->get_dimensions( false ) ), $this );
		}
		return array(
			'length' => $this->get_length(),
			'width'  => $this->get_width(),
			'height' => $this->get_height(),
		);
	}

	/**
	 * Get upsell IDs.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return array
	 */
	public function get_upsell_ids( $context = 'view' ) {
		return $this->get_prop( 'upsell_ids', $context );
	}

	/**
	 * Get cross sell IDs.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return array
	 */
	public function get_cross_sell_ids( $context = 'view' ) {
		return $this->get_prop( 'cross_sell_ids', $context );
	}

	/**
	 * Get parent ID.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return int
	 */
	public function get_parent_id( $context = 'view' ) {
		return $this->get_prop( 'parent_id', $context );
	}

	/**
	 * Return if reviews is allowed.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return bool
	 */
	public function get_reviews_allowed( $context = 'view' ) {
		return $this->get_prop( 'reviews_allowed', $context );
	}

	/**
	 * Get purchase note.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_purchase_note( $context = 'view' ) {
		return $this->get_prop( 'purchase_note', $context );
	}

	/**
	 * Returns product attributes.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return array
	 */
	public function get_attributes( $context = 'view' ) {
		return $this->get_prop( 'attributes', $context );
	}

	/**
	 * Get default attributes.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return array
	 */
	public function get_default_attributes( $context = 'view' ) {
		return $this->get_prop( 'default_attributes', $context );
	}

	/**
	 * Get menu order.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return int
	 */
	public function get_menu_order( $context = 'view' ) {
		return $this->get_prop( 'menu_order', $context );
	}

	/**
	 * Get post password.
	 *
	 * @since  3.6.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return int
	 */
	public function get_post_password( $context = 'view' ) {
		return $this->get_prop( 'post_password', $context );
	}

	/**
	 * Get category ids.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return array
	 */
	public function get_category_ids( $context = 'view' ) {
		return $this->get_prop( 'category_ids', $context );
	}

	/**
	 * Get tag ids.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return array
	 */
	public function get_tag_ids( $context = 'view' ) {
		return $this->get_prop( 'tag_ids', $context );
	}

	/**
	 * Get virtual.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return bool
	 */
	public function get_virtual( $context = 'view' ) {
		return $this->get_prop( 'virtual', $context );
	}

	/**
	 * Returns the gallery attachment ids.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return array
	 */
	public function get_gallery_image_ids( $context = 'view' ) {
		return $this->get_prop( 'gallery_image_ids', $context );
	}

	/**
	 * Get shipping class ID.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return int
	 */
	public function get_shipping_class_id( $context = 'view' ) {
		return $this->get_prop( 'shipping_class_id', $context );
	}

	/**
	 * Get downloads.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return array
	 */
	public function get_downloads( $context = 'view' ) {
		return $this->get_prop( 'downloads', $context );
	}

	/**
	 * Get download expiry.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return int
	 */
	public function get_download_expiry( $context = 'view' ) {
		return $this->get_prop( 'download_expiry', $context );
	}

	/**
	 * Get downloadable.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return bool
	 */
	public function get_downloadable( $context = 'view' ) {
		return $this->get_prop( 'downloadable', $context );
	}

	/**
	 * Get download limit.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return int
	 */
	public function get_download_limit( $context = 'view' ) {
		return $this->get_prop( 'download_limit', $context );
	}

	/**
	 * Get main image ID.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_image_id( $context = 'view' ) {
		return $this->get_prop( 'image_id', $context );
	}

	/**
	 * Get rating count.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return array of counts
	 */
	public function get_rating_counts( $context = 'view' ) {
		return $this->get_prop( 'rating_counts', $context );
	}

	/**
	 * Get average rating.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return float
	 */
	public function get_average_rating( $context = 'view' ) {
		return $this->get_prop( 'average_rating', $context );
	}

	/**
	 * Get review count.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return int
	 */
	public function get_review_count( $context = 'view' ) {
		return $this->get_prop( 'review_count', $context );
	}

	/*
	|--------------------------------------------------------------------------
	| Setters
	|--------------------------------------------------------------------------
	|
	| Functions for setting product data. These should not update anything in the
	| database itself and should only change what is stored in the class
	| object.
	*/

	/**
	 * Set product name.
	 *
	 * @since 3.0.0
	 * @param string $name Product name.
	 */
	public function set_name( $name ) {
		$this->set_prop( 'name', $name );
	}

	/**
	 * Set product slug.
	 *
	 * @since 3.0.0
	 * @param string $slug Product slug.
	 */
	public function set_slug( $slug ) {
		$this->set_prop( 'slug', $slug );
	}

	/**
	 * Set product created date.
	 *
	 * @since 3.0.0
	 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date.
	 */
	public function set_date_created( $date = null ) {
		$this->set_date_prop( 'date_created', $date );
	}

	/**
	 * Set product modified date.
	 *
	 * @since 3.0.0
	 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date.
	 */
	public function set_date_modified( $date = null ) {
		$this->set_date_prop( 'date_modified', $date );
	}

	/**
	 * Set product status.
	 *
	 * @since 3.0.0
	 * @param string $status Product status.
	 */
	public function set_status( $status ) {
		$this->set_prop( 'status', $status );
	}

	/**
	 * Set if the product is featured.
	 *
	 * @since 3.0.0
	 * @param bool|string $featured Whether the product is featured or not.
	 */
	public function set_featured( $featured ) {
		$this->set_prop( 'featured', wc_string_to_bool( $featured ) );
	}

	/**
	 * Set catalog visibility.
	 *
	 * @since  3.0.0
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 * @param  string $visibility Options: 'hidden', 'visible', 'search' and 'catalog'.
	 */
	public function set_catalog_visibility( $visibility ) {
		$options = array_keys( wc_get_product_visibility_options() );
		if ( ! in_array( $visibility, $options, true ) ) {
			$this->error( 'product_invalid_catalog_visibility', __( 'Invalid catalog visibility option.', 'woocommerce' ) );
		}
		$this->set_prop( 'catalog_visibility', $visibility );
	}

	/**
	 * Set product description.
	 *
	 * @since 3.0.0
	 * @param string $description Product description.
	 */
	public function set_description( $description ) {
		$this->set_prop( 'description', $description );
	}

	/**
	 * Set product short description.
	 *
	 * @since 3.0.0
	 * @param string $short_description Product short description.
	 */
	public function set_short_description( $short_description ) {
		$this->set_prop( 'short_description', $short_description );
	}

	/**
	 * Set SKU.
	 *
	 * @since  3.0.0
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 * @param  string $sku Product SKU.
	 */
	public function set_sku( $sku ) {
		$sku = (string) $sku;
		if ( $this->get_object_read() && ! empty( $sku ) && ! wc_product_has_unique_sku( $this->get_id(), $sku ) ) {
			$sku_found = wc_get_product_id_by_sku( $sku );

			$this->error( 'product_invalid_sku', __( 'Invalid or duplicated SKU.', 'woocommerce' ), 400, array( 'resource_id' => $sku_found ) );
		}
		$this->set_prop( 'sku', $sku );
	}

	/**
	 * Set the product's active price.
	 *
	 * @param string $price Price.
	 */
	public function set_price( $price ) {
		$this->set_prop( 'price', wc_format_decimal( $price ) );
	}

	/**
	 * Set the product's regular price.
	 *
	 * @since 3.0.0
	 * @param string $price Regular price.
	 */
	public function set_regular_price( $price ) {
		$this->set_prop( 'regular_price', wc_format_decimal( $price ) );
	}

	/**
	 * Set the product's sale price.
	 *
	 * @since 3.0.0
	 * @param string $price sale price.
	 */
	public function set_sale_price( $price ) {
		$this->set_prop( 'sale_price', wc_format_decimal( $price ) );
	}

	/**
	 * Set date on sale from.
	 *
	 * @since 3.0.0
	 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date.
	 */
	public function set_date_on_sale_from( $date = null ) {
		$this->set_date_prop( 'date_on_sale_from', $date );
	}

	/**
	 * Set date on sale to.
	 *
	 * @since 3.0.0
	 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date.
	 */
	public function set_date_on_sale_to( $date = null ) {
		$this->set_date_prop( 'date_on_sale_to', $date );
	}

	/**
	 * Set number total of sales.
	 *
	 * @since 3.0.0
	 * @param int $total Total of sales.
	 */
	public function set_total_sales( $total ) {
		$this->set_prop( 'total_sales', absint( $total ) );
	}

	/**
	 * Set the tax status.
	 *
	 * @since  3.0.0
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 * @param  string $status Tax status.
	 */
	public function set_tax_status( $status ) {
		$options = array(
			'taxable',
			'shipping',
			'none',
		);

		// Set default if empty.
		if ( empty( $status ) ) {
			$status = 'taxable';
		}

		if ( ! in_array( $status, $options, true ) ) {
			$this->error( 'product_invalid_tax_status', __( 'Invalid product tax status.', 'woocommerce' ) );
		}

		$this->set_prop( 'tax_status', $status );
	}

	/**
	 * Set the tax class.
	 *
	 * @since 3.0.0
	 * @param string $class Tax class.
	 */
	public function set_tax_class( $class ) {
		$class         = sanitize_title( $class );
		$class         = 'standard' === $class ? '' : $class;
		$valid_classes = $this->get_valid_tax_classes();

		if ( ! in_array( $class, $valid_classes, true ) ) {
			$class = '';
		}

		$this->set_prop( 'tax_class', $class );
	}

	/**
	 * Return an array of valid tax classes
	 *
	 * @return array valid tax classes
	 */
	protected function get_valid_tax_classes() {
		return WC_Tax::get_tax_class_slugs();
	}

	/**
	 * Set if product manage stock.
	 *
	 * @since 3.0.0
	 * @param bool $manage_stock Whether or not manage stock is enabled.
	 */
	public function set_manage_stock( $manage_stock ) {
		$this->set_prop( 'manage_stock', wc_string_to_bool( $manage_stock ) );
	}

	/**
	 * Set number of items available for sale.
	 *
	 * @since 3.0.0
	 * @param float|null $quantity Stock quantity.
	 */
	public function set_stock_quantity( $quantity ) {
		$this->set_prop( 'stock_quantity', '' !== $quantity ? wc_stock_amount( $quantity ) : null );
	}

	/**
	 * Set stock status.
	 *
	 * @param string $status New status.
	 */
	public function set_stock_status( $status = 'instock' ) {
		$valid_statuses = wc_get_product_stock_status_options();

		if ( isset( $valid_statuses[ $status ] ) ) {
			$this->set_prop( 'stock_status', $status );
		} else {
			$this->set_prop( 'stock_status', 'instock' );
		}
	}

	/**
	 * Set backorders.
	 *
	 * @since 3.0.0
	 * @param string $backorders Options: 'yes', 'no' or 'notify'.
	 */
	public function set_backorders( $backorders ) {
		$this->set_prop( 'backorders', $backorders );
	}

	/**
	 * Set low stock amount.
	 *
	 * @param int|string $amount Empty string if value not set.
	 * @since 3.5.0
	 */
	public function set_low_stock_amount( $amount ) {
		$this->set_prop( 'low_stock_amount', '' === $amount ? '' : absint( $amount ) );
	}

	/**
	 * Set if should be sold individually.
	 *
	 * @since 3.0.0
	 * @param bool $sold_individually Whether or not product is sold individually.
	 */
	public function set_sold_individually( $sold_individually ) {
		$this->set_prop( 'sold_individually', wc_string_to_bool( $sold_individually ) );
	}

	/**
	 * Set the product's weight.
	 *
	 * @since 3.0.0
	 * @param float|string $weight Total weight.
	 */
	public function set_weight( $weight ) {
		$this->set_prop( 'weight', '' === $weight ? '' : wc_format_decimal( $weight ) );
	}

	/**
	 * Set the product length.
	 *
	 * @since 3.0.0
	 * @param float|string $length Total length.
	 */
	public function set_length( $length ) {
		$this->set_prop( 'length', '' === $length ? '' : wc_format_decimal( $length ) );
	}

	/**
	 * Set the product width.
	 *
	 * @since 3.0.0
	 * @param float|string $width Total width.
	 */
	public function set_width( $width ) {
		$this->set_prop( 'width', '' === $width ? '' : wc_format_decimal( $width ) );
	}

	/**
	 * Set the product height.
	 *
	 * @since 3.0.0
	 * @param float|string $height Total height.
	 */
	public function set_height( $height ) {
		$this->set_prop( 'height', '' === $height ? '' : wc_format_decimal( $height ) );
	}

	/**
	 * Set upsell IDs.
	 *
	 * @since 3.0.0
	 * @param array $upsell_ids IDs from the up-sell products.
	 */
	public function set_upsell_ids( $upsell_ids ) {
		$this->set_prop( 'upsell_ids', array_filter( (array) $upsell_ids ) );
	}

	/**
	 * Set crosssell IDs.
	 *
	 * @since 3.0.0
	 * @param array $cross_sell_ids IDs from the cross-sell products.
	 */
	public function set_cross_sell_ids( $cross_sell_ids ) {
		$this->set_prop( 'cross_sell_ids', array_filter( (array) $cross_sell_ids ) );
	}

	/**
	 * Set parent ID.
	 *
	 * @since 3.0.0
	 * @param int $parent_id Product parent ID.
	 */
	public function set_parent_id( $parent_id ) {
		$this->set_prop( 'parent_id', absint( $parent_id ) );
	}

	/**
	 * Set if reviews is allowed.
	 *
	 * @since 3.0.0
	 * @param bool $reviews_allowed Reviews allowed or not.
	 */
	public function set_reviews_allowed( $reviews_allowed ) {
		$this->set_prop( 'reviews_allowed', wc_string_to_bool( $reviews_allowed ) );
	}

	/**
	 * Set purchase note.
	 *
	 * @since 3.0.0
	 * @param string $purchase_note Purchase note.
	 */
	public function set_purchase_note( $purchase_note ) {
		$this->set_prop( 'purchase_note', $purchase_note );
	}

	/**
	 * Set product attributes.
	 *
	 * Attributes are made up of:
	 *     id - 0 for product level attributes. ID for global attributes.
	 *     name - Attribute name.
	 *     options - attribute value or array of term ids/names.
	 *     position - integer sort order.
	 *     visible - If visible on frontend.
	 *     variation - If used for variations.
	 * Indexed by unqiue key to allow clearing old ones after a set.
	 *
	 * @since 3.0.0
	 * @param array $raw_attributes Array of WC_Product_Attribute objects.
	 */
	public function set_attributes( $raw_attributes ) {
		$attributes = array_fill_keys( array_keys( $this->get_attributes( 'edit' ) ), null );
		foreach ( $raw_attributes as $attribute ) {
			if ( is_a( $attribute, 'WC_Product_Attribute' ) ) {
				$attributes[ sanitize_title( $attribute->get_name() ) ] = $attribute;
			}
		}

		uasort( $attributes, 'wc_product_attribute_uasort_comparison' );
		$this->set_prop( 'attributes', $attributes );
	}

	/**
	 * Set default attributes. These will be saved as strings and should map to attribute values.
	 *
	 * @since 3.0.0
	 * @param array $default_attributes List of default attributes.
	 */
	public function set_default_attributes( $default_attributes ) {
		$this->set_prop( 'default_attributes', array_map( 'strval', array_filter( (array) $default_attributes, 'wc_array_filter_default_attributes' ) ) );
	}

	/**
	 * Set menu order.
	 *
	 * @since 3.0.0
	 * @param int $menu_order Menu order.
	 */
	public function set_menu_order( $menu_order ) {
		$this->set_prop( 'menu_order', intval( $menu_order ) );
	}

	/**
	 * Set post password.
	 *
	 * @since 3.6.0
	 * @param int $post_password Post password.
	 */
	public function set_post_password( $post_password ) {
		$this->set_prop( 'post_password', $post_password );
	}

	/**
	 * Set the product categories.
	 *
	 * @since 3.0.0
	 * @param array $term_ids List of terms IDs.
	 */
	public function set_category_ids( $term_ids ) {
		$this->set_prop( 'category_ids', array_unique( array_map( 'intval', $term_ids ) ) );
	}

	/**
	 * Set the product tags.
	 *
	 * @since 3.0.0
	 * @param array $term_ids List of terms IDs.
	 */
	public function set_tag_ids( $term_ids ) {
		$this->set_prop( 'tag_ids', array_unique( array_map( 'intval', $term_ids ) ) );
	}

	/**
	 * Set if the product is virtual.
	 *
	 * @since 3.0.0
	 * @param bool|string $virtual Whether product is virtual or not.
	 */
	public function set_virtual( $virtual ) {
		$this->set_prop( 'virtual', wc_string_to_bool( $virtual ) );
	}

	/**
	 * Set shipping class ID.
	 *
	 * @since 3.0.0
	 * @param int $id Product shipping class id.
	 */
	public function set_shipping_class_id( $id ) {
		$this->set_prop( 'shipping_class_id', absint( $id ) );
	}

	/**
	 * Set if the product is downloadable.
	 *
	 * @since 3.0.0
	 * @param bool|string $downloadable Whether product is downloadable or not.
	 */
	public function set_downloadable( $downloadable ) {
		$this->set_prop( 'downloadable', wc_string_to_bool( $downloadable ) );
	}

	/**
	 * Set downloads.
	 *
	 * @since 3.0.0
	 * @param array $downloads_array Array of WC_Product_Download objects or arrays.
	 */
	public function set_downloads( $downloads_array ) {
		$downloads = array();
		$errors    = array();

		foreach ( $downloads_array as $download ) {
			if ( is_a( $download, 'WC_Product_Download' ) ) {
				$download_object = $download;
			} else {
				$download_object = new WC_Product_Download();

				// If we don't have a previous hash, generate UUID for download.
				if ( empty( $download['download_id'] ) ) {
					$download['download_id'] = wp_generate_uuid4();
				}

				$download_object->set_id( $download['download_id'] );
				$download_object->set_name( $download['name'] );
				$download_object->set_file( $download['file'] );
			}

			// Validate the file extension.
			if ( ! $download_object->is_allowed_filetype() ) {
				if ( $this->get_object_read() ) {
					/* translators: %1$s: Downloadable file */
					$errors[] = sprintf( __( 'The downloadable file %1$s cannot be used as it does not have an allowed file type. Allowed types include: %2$s', 'woocommerce' ), '<code>' . basename( $download_object->get_file() ) . '</code>', '<code>' . implode( ', ', array_keys( $download_object->get_allowed_mime_types() ) ) . '</code>' );
				}
				continue;
			}

			// Validate the file exists.
			if ( ! $download_object->file_exists() ) {
				if ( $this->get_object_read() ) {
					/* translators: %s: Downloadable file */
					$errors[] = sprintf( __( 'The downloadable file %s cannot be used as it does not exist on the server.', 'woocommerce' ), '<code>' . $download_object->get_file() . '</code>' );
				}
				continue;
			}

			$downloads[ $download_object->get_id() ] = $download_object;
		}

		if ( $errors ) {
			$this->error( 'product_invalid_download', $errors[0] );
		}

		$this->set_prop( 'downloads', $downloads );
	}

	/**
	 * Set download limit.
	 *
	 * @since 3.0.0
	 * @param int|string $download_limit Product download limit.
	 */
	public function set_download_limit( $download_limit ) {
		$this->set_prop( 'download_limit', -1 === (int) $download_limit || '' === $download_limit ? -1 : absint( $download_limit ) );
	}

	/**
	 * Set download expiry.
	 *
	 * @since 3.0.0
	 * @param int|string $download_expiry Product download expiry.
	 */
	public function set_download_expiry( $download_expiry ) {
		$this->set_prop( 'download_expiry', -1 === (int) $download_expiry || '' === $download_expiry ? -1 : absint( $download_expiry ) );
	}

	/**
	 * Set gallery attachment ids.
	 *
	 * @since 3.0.0
	 * @param array $image_ids List of image ids.
	 */
	public function set_gallery_image_ids( $image_ids ) {
		$image_ids = wp_parse_id_list( $image_ids );

		$this->set_prop( 'gallery_image_ids', $image_ids );
	}

	/**
	 * Set main image ID.
	 *
	 * @since 3.0.0
	 * @param int|string $image_id Product image id.
	 */
	public function set_image_id( $image_id = '' ) {
		$this->set_prop( 'image_id', $image_id );
	}

	/**
	 * Set rating counts. Read only.
	 *
	 * @param array $counts Product rating counts.
	 */
	public function set_rating_counts( $counts ) {
		$this->set_prop( 'rating_counts', array_filter( array_map( 'absint', (array) $counts ) ) );
	}

	/**
	 * Set average rating. Read only.
	 *
	 * @param float $average Product average rating.
	 */
	public function set_average_rating( $average ) {
		$this->set_prop( 'average_rating', wc_format_decimal( $average ) );
	}

	/**
	 * Set review count. Read only.
	 *
	 * @param int $count Product review count.
	 */
	public function set_review_count( $count ) {
		$this->set_prop( 'review_count', absint( $count ) );
	}

	/*
	|--------------------------------------------------------------------------
	| Other Methods
	|--------------------------------------------------------------------------
	*/

	/**
	 * Ensure properties are set correctly before save.
	 *
	 * @since 3.0.0
	 */
	public function validate_props() {
		// Before updating, ensure stock props are all aligned. Qty, backorders and low stock amount are not needed if not stock managed.
		if ( ! $this->get_manage_stock() ) {
			$this->set_stock_quantity( '' );
			$this->set_backorders( 'no' );
			$this->set_low_stock_amount( '' );
			return;
		}

		$stock_is_above_notification_threshold = ( $this->get_stock_quantity() > get_option( 'woocommerce_notify_no_stock_amount', 0 ) );
		$backorders_are_allowed                = ( 'no' !== $this->get_backorders() );

		if ( $stock_is_above_notification_threshold ) {
			$new_stock_status = 'instock';
		} elseif ( $backorders_are_allowed ) {
			$new_stock_status = 'onbackorder';
		} else {
			$new_stock_status = 'outofstock';
		}

		$this->set_stock_status( $new_stock_status );
	}

	/**
	 * Save data (either create or update depending on if we are working on an existing product).
	 *
	 * @since  3.0.0
	 * @return int
	 */
	public function save() {
		$this->validate_props();

		if ( ! $this->data_store ) {
			return $this->get_id();
		}

		/**
		 * Trigger action before saving to the DB. Allows you to adjust object props before save.
		 *
		 * @param WC_Data          $this The object being saved.
		 * @param WC_Data_Store_WP $data_store THe data store persisting the data.
		 */
		do_action( 'woocommerce_before_' . $this->object_type . '_object_save', $this, $this->data_store );

		$state = $this->before_data_store_save_or_update();

		if ( $this->get_id() ) {
			$changeset = $this->get_changes();
			$this->data_store->update( $this );
		} else {
			$changeset = null;
			$this->data_store->create( $this );
		}

		$this->after_data_store_save_or_update( $state );

		// Update attributes lookup table if the product is new OR it's not but there are actually any changes.
		if ( is_null( $changeset ) || ! empty( $changeset ) ) {
			wc_get_container()->get( ProductAttributesLookupDataStore::class )->on_product_changed( $this, $changeset );
		}

		/**
		 * Trigger action after saving to the DB.
		 *
		 * @param WC_Data          $this The object being saved.
		 * @param WC_Data_Store_WP $data_store THe data store persisting the data.
		 */
		do_action( 'woocommerce_after_' . $this->object_type . '_object_save', $this, $this->data_store );

		return $this->get_id();
	}

	/**
	 * Do any extra processing needed before the actual product save
	 * (but after triggering the 'woocommerce_before_..._object_save' action)
	 *
	 * @return mixed A state value that will be passed to after_data_store_save_or_update.
	 */
	protected function before_data_store_save_or_update() {
	}

	/**
	 * Do any extra processing needed after the actual product save
	 * (but before triggering the 'woocommerce_after_..._object_save' action)
	 *
	 * @param mixed $state The state object that was returned by before_data_store_save_or_update.
	 */
	protected function after_data_store_save_or_update( $state ) {
		$this->maybe_defer_product_sync();
	}

	/**
	 * Delete the product, set its ID to 0, and return result.
	 *
	 * @param  bool $force_delete Should the product be deleted permanently.
	 * @return bool result
	 */
	public function delete( $force_delete = false ) {
		$product_id = $this->get_id();
		$deleted    = parent::delete( $force_delete );

		if ( $deleted ) {
			$this->maybe_defer_product_sync();
			wc_get_container()->get( ProductAttributesLookupDataStore::class )->on_product_deleted( $product_id );
		}

		return $deleted;
	}

	/**
	 * If this is a child product, queue its parent for syncing at the end of the request.
	 */
	protected function maybe_defer_product_sync() {
		$parent_id = $this->get_parent_id();
		if ( $parent_id ) {
			wc_deferred_product_sync( $parent_id );
		}
	}

	/*
	|--------------------------------------------------------------------------
	| Conditionals
	|--------------------------------------------------------------------------
	*/

	/**
	 * Check if a product supports a given feature.
	 *
	 * Product classes should override this to declare support (or lack of support) for a feature.
	 *
	 * @param  string $feature string The name of a feature to test support for.
	 * @return bool True if the product supports the feature, false otherwise.
	 * @since  2.5.0
	 */
	public function supports( $feature ) {
		return apply_filters( 'woocommerce_product_supports', in_array( $feature, $this->supports, true ), $feature, $this );
	}

	/**
	 * Returns whether or not the product post exists.
	 *
	 * @return bool
	 */
	public function exists() {
		return false !== $this->get_status();
	}

	/**
	 * Checks the product type.
	 *
	 * Backwards compatibility with downloadable/virtual.
	 *
	 * @param  string|array $type Array or string of types.
	 * @return bool
	 */
	public function is_type( $type ) {
		return ( $this->get_type() === $type || ( is_array( $type ) && in_array( $this->get_type(), $type, true ) ) );
	}

	/**
	 * Checks if a product is downloadable.
	 *
	 * @return bool
	 */
	public function is_downloadable() {
		return apply_filters( 'woocommerce_is_downloadable', true === $this->get_downloadable(), $this );
	}

	/**
	 * Checks if a product is virtual (has no shipping).
	 *
	 * @return bool
	 */
	public function is_virtual() {
		return apply_filters( 'woocommerce_is_virtual', true === $this->get_virtual(), $this );
	}

	/**
	 * Returns whether or not the product is featured.
	 *
	 * @return bool
	 */
	public function is_featured() {
		return true === $this->get_featured();
	}

	/**
	 * Check if a product is sold individually (no quantities).
	 *
	 * @return bool
	 */
	public function is_sold_individually() {
		return apply_filters( 'woocommerce_is_sold_individually', true === $this->get_sold_individually(), $this );
	}

	/**
	 * Returns whether or not the product is visible in the catalog.
	 *
	 * @return bool
	 */
	public function is_visible() {
		$visible = $this->is_visible_core();
		return apply_filters( 'woocommerce_product_is_visible', $visible, $this->get_id() );
	}

	/**
	 * Returns whether or not the product is visible in the catalog (doesn't trigger filters).
	 *
	 * @return bool
	 */
	protected function is_visible_core() {
		$visible = 'visible' === $this->get_catalog_visibility() || ( is_search() && 'search' === $this->get_catalog_visibility() ) || ( ! is_search() && 'catalog' === $this->get_catalog_visibility() );

		if ( 'trash' === $this->get_status() ) {
			$visible = false;
		} elseif ( 'publish' !== $this->get_status() && ! current_user_can( 'edit_post', $this->get_id() ) ) {
			$visible = false;
		}

		if ( $this->get_parent_id() ) {
			$parent_product = wc_get_product( $this->get_parent_id() );

			if ( $parent_product && 'publish' !== $parent_product->get_status() ) {
				$visible = false;
			}
		}

		if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && ! $this->is_in_stock() ) {
			$visible = false;
		}

		return $visible;
	}

	/**
	 * Returns false if the product cannot be bought.
	 *
	 * @return bool
	 */
	public function is_purchasable() {
		return apply_filters( 'woocommerce_is_purchasable', $this->exists() && ( 'publish' === $this->get_status() || current_user_can( 'edit_post', $this->get_id() ) ) && '' !== $this->get_price(), $this );
	}

	/**
	 * Returns whether or not the product is on sale.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return bool
	 */
	public function is_on_sale( $context = 'view' ) {
		if ( '' !== (string) $this->get_sale_price( $context ) && $this->get_regular_price( $context ) > $this->get_sale_price( $context ) ) {
			$on_sale = true;

			if ( $this->get_date_on_sale_from( $context ) && $this->get_date_on_sale_from( $context )->getTimestamp() > time() ) {
				$on_sale = false;
			}

			if ( $this->get_date_on_sale_to( $context ) && $this->get_date_on_sale_to( $context )->getTimestamp() < time() ) {
				$on_sale = false;
			}
		} else {
			$on_sale = false;
		}
		return 'view' === $context ? apply_filters( 'woocommerce_product_is_on_sale', $on_sale, $this ) : $on_sale;
	}

	/**
	 * Returns whether or not the product has dimensions set.
	 *
	 * @return bool
	 */
	public function has_dimensions() {
		return ( $this->get_length() || $this->get_height() || $this->get_width() ) && ! $this->get_virtual();
	}

	/**
	 * Returns whether or not the product has weight set.
	 *
	 * @return bool
	 */
	public function has_weight() {
		return $this->get_weight() && ! $this->get_virtual();
	}

	/**
	 * Returns whether or not the product can be purchased.
	 * This returns true for 'instock' and 'onbackorder' stock statuses.
	 *
	 * @return bool
	 */
	public function is_in_stock() {
		return apply_filters( 'woocommerce_product_is_in_stock', 'outofstock' !== $this->get_stock_status(), $this );
	}

	/**
	 * Checks if a product needs shipping.
	 *
	 * @return bool
	 */
	public function needs_shipping() {
		return apply_filters( 'woocommerce_product_needs_shipping', ! $this->is_virtual(), $this );
	}

	/**
	 * Returns whether or not the product is taxable.
	 *
	 * @return bool
	 */
	public function is_taxable() {
		return apply_filters( 'woocommerce_product_is_taxable', $this->get_tax_status() === 'taxable' && wc_tax_enabled(), $this );
	}

	/**
	 * Returns whether or not the product shipping is taxable.
	 *
	 * @return bool
	 */
	public function is_shipping_taxable() {
		return $this->needs_shipping() && ( $this->get_tax_status() === 'taxable' || $this->get_tax_status() === 'shipping' );
	}

	/**
	 * Returns whether or not the product is stock managed.
	 *
	 * @return bool
	 */
	public function managing_stock() {
		if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
			return $this->get_manage_stock();
		}
		return false;
	}

	/**
	 * Returns whether or not the product can be backordered.
	 *
	 * @return bool
	 */
	public function backorders_allowed() {
		return apply_filters( 'woocommerce_product_backorders_allowed', ( 'yes' === $this->get_backorders() || 'notify' === $this->get_backorders() ), $this->get_id(), $this );
	}

	/**
	 * Returns whether or not the product needs to notify the customer on backorder.
	 *
	 * @return bool
	 */
	public function backorders_require_notification() {
		return apply_filters( 'woocommerce_product_backorders_require_notification', ( $this->managing_stock() && 'notify' === $this->get_backorders() ), $this );
	}

	/**
	 * Check if a product is on backorder.
	 *
	 * @param  int $qty_in_cart (default: 0).
	 * @return bool
	 */
	public function is_on_backorder( $qty_in_cart = 0 ) {
		if ( 'onbackorder' === $this->get_stock_status() ) {
			return true;
		}

		return $this->managing_stock() && $this->backorders_allowed() && ( $this->get_stock_quantity() - $qty_in_cart ) < 0;
	}

	/**
	 * Returns whether or not the product has enough stock for the order.
	 *
	 * @param  mixed $quantity Quantity of a product added to an order.
	 * @return bool
	 */
	public function has_enough_stock( $quantity ) {
		return ! $this->managing_stock() || $this->backorders_allowed() || $this->get_stock_quantity() >= $quantity;
	}

	/**
	 * Returns whether or not the product has any visible attributes.
	 *
	 * @return boolean
	 */
	public function has_attributes() {
		foreach ( $this->get_attributes() as $attribute ) {
			if ( $attribute->get_visible() ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Returns whether or not the product has any child product.
	 *
	 * @return bool
	 */
	public function has_child() {
		return 0 < count( $this->get_children() );
	}

	/**
	 * Does a child have dimensions?
	 *
	 * @since  3.0.0
	 * @return bool
	 */
	public function child_has_dimensions() {
		return false;
	}

	/**
	 * Does a child have a weight?
	 *
	 * @since  3.0.0
	 * @return boolean
	 */
	public function child_has_weight() {
		return false;
	}

	/**
	 * Check if downloadable product has a file attached.
	 *
	 * @since 1.6.2
	 *
	 * @param  string $download_id file identifier.
	 * @return bool Whether downloadable product has a file attached.
	 */
	public function has_file( $download_id = '' ) {
		return $this->is_downloadable() && $this->get_file( $download_id );
	}

	/**
	 * Returns whether or not the product has additional options that need
	 * selecting before adding to cart.
	 *
	 * @since  3.0.0
	 * @return boolean
	 */
	public function has_options() {
		return apply_filters( 'woocommerce_product_has_options', false, $this );
	}

	/*
	|--------------------------------------------------------------------------
	| Non-CRUD Getters
	|--------------------------------------------------------------------------
	*/

	/**
	 * Get the product's title. For products this is the product name.
	 *
	 * @return string
	 */
	public function get_title() {
		return apply_filters( 'woocommerce_product_title', $this->get_name(), $this );
	}

	/**
	 * Product permalink.
	 *
	 * @return string
	 */
	public function get_permalink() {
		return get_permalink( $this->get_id() );
	}

	/**
	 * Returns the children IDs if applicable. Overridden by child classes.
	 *
	 * @return array of IDs
	 */
	public function get_children() {
		return array();
	}

	/**
	 * If the stock level comes from another product ID, this should be modified.
	 *
	 * @since  3.0.0
	 * @return int
	 */
	public function get_stock_managed_by_id() {
		return $this->get_id();
	}

	/**
	 * Returns the price in html format.
	 *
	 * @param string $deprecated Deprecated param.
	 *
	 * @return string
	 */
	public function get_price_html( $deprecated = '' ) {
		if ( '' === $this->get_price() ) {
			$price = apply_filters( 'woocommerce_empty_price_html', '', $this );
		} elseif ( $this->is_on_sale() ) {
			$price = wc_format_sale_price( wc_get_price_to_display( $this, array( 'price' => $this->get_regular_price() ) ), wc_get_price_to_display( $this ) ) . $this->get_price_suffix();
		} else {
			$price = wc_price( wc_get_price_to_display( $this ) ) . $this->get_price_suffix();
		}

		return apply_filters( 'woocommerce_get_price_html', $price, $this );
	}

	/**
	 * Get product name with SKU or ID. Used within admin.
	 *
	 * @return string Formatted product name
	 */
	public function get_formatted_name() {
		if ( $this->get_sku() ) {
			$identifier = $this->get_sku();
		} else {
			$identifier = '#' . $this->get_id();
		}
		return sprintf( '%2$s (%1$s)', $identifier, $this->get_name() );
	}

	/**
	 * Get min quantity which can be purchased at once.
	 *
	 * @since  3.0.0
	 * @return int
	 */
	public function get_min_purchase_quantity() {
		return 1;
	}

	/**
	 * Get max quantity which can be purchased at once.
	 *
	 * @since  3.0.0
	 * @return int Quantity or -1 if unlimited.
	 */
	public function get_max_purchase_quantity() {
		return $this->is_sold_individually() ? 1 : ( $this->backorders_allowed() || ! $this->managing_stock() ? -1 : $this->get_stock_quantity() );
	}

	/**
	 * Get the add to url used mainly in loops.
	 *
	 * @return string
	 */
	public function add_to_cart_url() {
		return apply_filters( 'woocommerce_product_add_to_cart_url', $this->get_permalink(), $this );
	}

	/**
	 * Get the add to cart button text for the single page.
	 *
	 * @return string
	 */
	public function single_add_to_cart_text() {
		return apply_filters( 'woocommerce_product_single_add_to_cart_text', __( 'Add to cart', 'woocommerce' ), $this );
	}

	/**
	 * Get the add to cart button text.
	 *
	 * @return string
	 */
	public function add_to_cart_text() {
		return apply_filters( 'woocommerce_product_add_to_cart_text', __( 'Read more', 'woocommerce' ), $this );
	}

	/**
	 * Get the add to cart button text description - used in aria tags.
	 *
	 * @since  3.3.0
	 * @return string
	 */
	public function add_to_cart_description() {
		/* translators: %s: Product title */
		return apply_filters( 'woocommerce_product_add_to_cart_description', sprintf( __( 'Read more about &ldquo;%s&rdquo;', 'woocommerce' ), $this->get_name() ), $this );
	}

	/**
	 * Returns the main product image.
	 *
	 * @param  string $size (default: 'woocommerce_thumbnail').
	 * @param  array  $attr Image attributes.
	 * @param  bool   $placeholder True to return $placeholder if no image is found, or false to return an empty string.
	 * @return string
	 */
	public function get_image( $size = 'woocommerce_thumbnail', $attr = array(), $placeholder = true ) {
		$image = '';
		if ( $this->get_image_id() ) {
			$image = wp_get_attachment_image( $this->get_image_id(), $size, false, $attr );
		} elseif ( $this->get_parent_id() ) {
			$parent_product = wc_get_product( $this->get_parent_id() );
			if ( $parent_product ) {
				$image = $parent_product->get_image( $size, $attr, $placeholder );
			}
		}

		if ( ! $image && $placeholder ) {
			$image = wc_placeholder_img( $size, $attr );
		}

		return apply_filters( 'woocommerce_product_get_image', $image, $this, $size, $attr, $placeholder, $image );
	}

	/**
	 * Returns the product shipping class SLUG.
	 *
	 * @return string
	 */
	public function get_shipping_class() {
		$class_id = $this->get_shipping_class_id();
		if ( $class_id ) {
			$term = get_term_by( 'id', $class_id, 'product_shipping_class' );

			if ( $term && ! is_wp_error( $term ) ) {
				return $term->slug;
			}
		}
		return '';
	}

	/**
	 * Returns a single product attribute as a string.
	 *
	 * @param  string $attribute to get.
	 * @return string
	 */
	public function get_attribute( $attribute ) {
		$attributes = $this->get_attributes();
		$attribute  = sanitize_title( $attribute );

		if ( isset( $attributes[ $attribute ] ) ) {
			$attribute_object = $attributes[ $attribute ];
		} elseif ( isset( $attributes[ 'pa_' . $attribute ] ) ) {
			$attribute_object = $attributes[ 'pa_' . $attribute ];
		} else {
			return '';
		}
		return $attribute_object->is_taxonomy() ? implode( ', ', wc_get_product_terms( $this->get_id(), $attribute_object->get_name(), array( 'fields' => 'names' ) ) ) : wc_implode_text_attributes( $attribute_object->get_options() );
	}

	/**
	 * Get the total amount (COUNT) of ratings, or just the count for one rating e.g. number of 5 star ratings.
	 *
	 * @param  int $value Optional. Rating value to get the count for. By default returns the count of all rating values.
	 * @return int
	 */
	public function get_rating_count( $value = null ) {
		$counts = $this->get_rating_counts();

		if ( is_null( $value ) ) {
			return array_sum( $counts );
		} elseif ( isset( $counts[ $value ] ) ) {
			return absint( $counts[ $value ] );
		} else {
			return 0;
		}
	}

	/**
	 * Get a file by $download_id.
	 *
	 * @param  string $download_id file identifier.
	 * @return array|false if not found
	 */
	public function get_file( $download_id = '' ) {
		$files = $this->get_downloads();

		if ( '' === $download_id ) {
			$file = count( $files ) ? current( $files ) : false;
		} elseif ( isset( $files[ $download_id ] ) ) {
			$file = $files[ $download_id ];
		} else {
			$file = false;
		}

		return apply_filters( 'woocommerce_product_file', $file, $this, $download_id );
	}

	/**
	 * Get file download path identified by $download_id.
	 *
	 * @param  string $download_id file identifier.
	 * @return string
	 */
	public function get_file_download_path( $download_id ) {
		$files     = $this->get_downloads();
		$file_path = isset( $files[ $download_id ] ) ? $files[ $download_id ]->get_file() : '';

		// allow overriding based on the particular file being requested.
		return apply_filters( 'woocommerce_product_file_download_path', $file_path, $this, $download_id );
	}

	/**
	 * Get the suffix to display after prices > 0.
	 *
	 * @param  string  $price to calculate, left blank to just use get_price().
	 * @param  integer $qty   passed on to get_price_including_tax() or get_price_excluding_tax().
	 * @return string
	 */
	public function get_price_suffix( $price = '', $qty = 1 ) {
		$html = '';

		$suffix = get_option( 'woocommerce_price_display_suffix' );
		if ( $suffix && wc_tax_enabled() && 'taxable' === $this->get_tax_status() ) {
			if ( '' === $price ) {
				$price = $this->get_price();
			}
			$replacements = array(
				'{price_including_tax}' => wc_price( wc_get_price_including_tax( $this, array( 'qty' => $qty, 'price' => $price ) ) ), // @phpcs:ignore WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine, WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
				'{price_excluding_tax}' => wc_price( wc_get_price_excluding_tax( $this, array( 'qty' => $qty, 'price' => $price ) ) ), // @phpcs:ignore WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
			);
			$html         = str_replace( array_keys( $replacements ), array_values( $replacements ), ' <small class="woocommerce-price-suffix">' . wp_kses_post( $suffix ) . '</small>' );
		}
		return apply_filters( 'woocommerce_get_price_suffix', $html, $this, $price, $qty );
	}

	/**
	 * Returns the availability of the product.
	 *
	 * @return string[]
	 */
	public function get_availability() {
		return apply_filters(
			'woocommerce_get_availability',
			array(
				'availability' => $this->get_availability_text(),
				'class'        => $this->get_availability_class(),
			),
			$this
		);
	}

	/**
	 * Get availability text based on stock status.
	 *
	 * @return string
	 */
	protected function get_availability_text() {
		if ( ! $this->is_in_stock() ) {
			$availability = __( 'Out of stock', 'woocommerce' );
		} elseif ( $this->managing_stock() && $this->is_on_backorder( 1 ) ) {
			$availability = $this->backorders_require_notification() ? __( 'Available on backorder', 'woocommerce' ) : '';
		} elseif ( ! $this->managing_stock() && $this->is_on_backorder( 1 ) ) {
			$availability = __( 'Available on backorder', 'woocommerce' );
		} elseif ( $this->managing_stock() ) {
			$availability = wc_format_stock_for_display( $this );
		} else {
			$availability = '';
		}
		return apply_filters( 'woocommerce_get_availability_text', $availability, $this );
	}

	/**
	 * Get availability classname based on stock status.
	 *
	 * @return string
	 */
	protected function get_availability_class() {
		if ( ! $this->is_in_stock() ) {
			$class = 'out-of-stock';
		} elseif ( ( $this->managing_stock() && $this->is_on_backorder( 1 ) ) || ( ! $this->managing_stock() && $this->is_on_backorder( 1 ) ) ) {
			$class = 'available-on-backorder';
		} else {
			$class = 'in-stock';
		}
		return apply_filters( 'woocommerce_get_availability_class', $class, $this );
	}
}
abstracts/abstract-wc-deprecated-hooks.php000064400000006160151336044760014710 0ustar00<?php
/**
 * Abstract deprecated hooks
 *
 * @package WooCommerce\Abstracts
 * @since   3.0.0
 * @version 3.3.0
 */

use Automattic\Jetpack\Constants;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC_Deprecated_Hooks class maps old actions and filters to new ones. This is the base class for handling those deprecated hooks.
 *
 * Based on the WCS_Hook_Deprecator class by Prospress.
 */
abstract class WC_Deprecated_Hooks {

	/**
	 * Array of deprecated hooks we need to handle.
	 *
	 * @var array
	 */
	protected $deprecated_hooks = array();

	/**
	 * Array of versions on each hook has been deprecated.
	 *
	 * @var array
	 */
	protected $deprecated_version = array();

	/**
	 * Constructor.
	 */
	public function __construct() {
		$new_hooks = array_keys( $this->deprecated_hooks );
		array_walk( $new_hooks, array( $this, 'hook_in' ) );
	}

	/**
	 * Hook into the new hook so we can handle deprecated hooks once fired.
	 *
	 * @param string $hook_name Hook name.
	 */
	abstract public function hook_in( $hook_name );

	/**
	 * Get old hooks to map to new hook.
	 *
	 * @param  string $new_hook New hook name.
	 * @return array
	 */
	public function get_old_hooks( $new_hook ) {
		$old_hooks = isset( $this->deprecated_hooks[ $new_hook ] ) ? $this->deprecated_hooks[ $new_hook ] : array();
		$old_hooks = is_array( $old_hooks ) ? $old_hooks : array( $old_hooks );

		return $old_hooks;
	}

	/**
	 * If the hook is Deprecated, call the old hooks here.
	 */
	public function maybe_handle_deprecated_hook() {
		$new_hook          = current_filter();
		$old_hooks         = $this->get_old_hooks( $new_hook );
		$new_callback_args = func_get_args();
		$return_value      = $new_callback_args[0];

		foreach ( $old_hooks as $old_hook ) {
			$return_value = $this->handle_deprecated_hook( $new_hook, $old_hook, $new_callback_args, $return_value );
		}

		return $return_value;
	}

	/**
	 * If the old hook is in-use, trigger it.
	 *
	 * @param  string $new_hook          New hook name.
	 * @param  string $old_hook          Old hook name.
	 * @param  array  $new_callback_args New callback args.
	 * @param  mixed  $return_value      Returned value.
	 * @return mixed
	 */
	abstract public function handle_deprecated_hook( $new_hook, $old_hook, $new_callback_args, $return_value );

	/**
	 * Get deprecated version.
	 *
	 * @param string $old_hook Old hook name.
	 * @return string
	 */
	protected function get_deprecated_version( $old_hook ) {
		return ! empty( $this->deprecated_version[ $old_hook ] ) ? $this->deprecated_version[ $old_hook ] : Constants::get_constant( 'WC_VERSION' );
	}

	/**
	 * Display a deprecated notice for old hooks.
	 *
	 * @param string $old_hook Old hook.
	 * @param string $new_hook New hook.
	 */
	protected function display_notice( $old_hook, $new_hook ) {
		wc_deprecated_hook( esc_html( $old_hook ), esc_html( $this->get_deprecated_version( $old_hook ) ), esc_html( $new_hook ) );
	}

	/**
	 * Fire off a legacy hook with it's args.
	 *
	 * @param  string $old_hook          Old hook name.
	 * @param  array  $new_callback_args New callback args.
	 * @return mixed
	 */
	abstract protected function trigger_hook( $old_hook, $new_callback_args );
}
abstracts/abstract-wc-data.php000064400000052631151336044760012404 0ustar00<?php
/**
 * Abstract Data.
 *
 * Handles generic data interaction which is implemented by
 * the different data store classes.
 *
 * @class       WC_Data
 * @version     3.0.0
 * @package     WooCommerce\Classes
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Abstract WC Data Class
 *
 * Implemented by classes using the same CRUD(s) pattern.
 *
 * @version  2.6.0
 * @package  WooCommerce\Abstracts
 */
abstract class WC_Data {

	/**
	 * ID for this object.
	 *
	 * @since 3.0.0
	 * @var int
	 */
	protected $id = 0;

	/**
	 * Core data for this object. Name value pairs (name + default value).
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $data = array();

	/**
	 * Core data changes for this object.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $changes = array();

	/**
	 * This is false until the object is read from the DB.
	 *
	 * @since 3.0.0
	 * @var bool
	 */
	protected $object_read = false;

	/**
	 * This is the name of this object type.
	 *
	 * @since 3.0.0
	 * @var string
	 */
	protected $object_type = 'data';

	/**
	 * Extra data for this object. Name value pairs (name + default value).
	 * Used as a standard way for sub classes (like product types) to add
	 * additional information to an inherited class.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $extra_data = array();

	/**
	 * Set to _data on construct so we can track and reset data if needed.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $default_data = array();

	/**
	 * Contains a reference to the data store for this class.
	 *
	 * @since 3.0.0
	 * @var object
	 */
	protected $data_store;

	/**
	 * Stores meta in cache for future reads.
	 * A group must be set to to enable caching.
	 *
	 * @since 3.0.0
	 * @var string
	 */
	protected $cache_group = '';

	/**
	 * Stores additional meta data.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $meta_data = null;

	/**
	 * Default constructor.
	 *
	 * @param int|object|array $read ID to load from the DB (optional) or already queried data.
	 */
	public function __construct( $read = 0 ) {
		$this->data         = array_merge( $this->data, $this->extra_data );
		$this->default_data = $this->data;
	}

	/**
	 * Only store the object ID to avoid serializing the data object instance.
	 *
	 * @return array
	 */
	public function __sleep() {
		return array( 'id' );
	}

	/**
	 * Re-run the constructor with the object ID.
	 *
	 * If the object no longer exists, remove the ID.
	 */
	public function __wakeup() {
		try {
			$this->__construct( absint( $this->id ) );
		} catch ( Exception $e ) {
			$this->set_id( 0 );
			$this->set_object_read( true );
		}
	}

	/**
	 * When the object is cloned, make sure meta is duplicated correctly.
	 *
	 * @since 3.0.2
	 */
	public function __clone() {
		$this->maybe_read_meta_data();
		if ( ! empty( $this->meta_data ) ) {
			foreach ( $this->meta_data as $array_key => $meta ) {
				$this->meta_data[ $array_key ] = clone $meta;
				if ( ! empty( $meta->id ) ) {
					$this->meta_data[ $array_key ]->id = null;
				}
			}
		}
	}

	/**
	 * Get the data store.
	 *
	 * @since  3.0.0
	 * @return object
	 */
	public function get_data_store() {
		return $this->data_store;
	}

	/**
	 * Returns the unique ID for this object.
	 *
	 * @since  2.6.0
	 * @return int
	 */
	public function get_id() {
		return $this->id;
	}

	/**
	 * Delete an object, set the ID to 0, and return result.
	 *
	 * @since  2.6.0
	 * @param  bool $force_delete Should the date be deleted permanently.
	 * @return bool result
	 */
	public function delete( $force_delete = false ) {
		if ( $this->data_store ) {
			$this->data_store->delete( $this, array( 'force_delete' => $force_delete ) );
			$this->set_id( 0 );
			return true;
		}
		return false;
	}

	/**
	 * Save should create or update based on object existence.
	 *
	 * @since  2.6.0
	 * @return int
	 */
	public function save() {
		if ( ! $this->data_store ) {
			return $this->get_id();
		}

		/**
		 * Trigger action before saving to the DB. Allows you to adjust object props before save.
		 *
		 * @param WC_Data          $this The object being saved.
		 * @param WC_Data_Store_WP $data_store THe data store persisting the data.
		 */
		do_action( 'woocommerce_before_' . $this->object_type . '_object_save', $this, $this->data_store );

		if ( $this->get_id() ) {
			$this->data_store->update( $this );
		} else {
			$this->data_store->create( $this );
		}

		/**
		 * Trigger action after saving to the DB.
		 *
		 * @param WC_Data          $this The object being saved.
		 * @param WC_Data_Store_WP $data_store THe data store persisting the data.
		 */
		do_action( 'woocommerce_after_' . $this->object_type . '_object_save', $this, $this->data_store );

		return $this->get_id();
	}

	/**
	 * Change data to JSON format.
	 *
	 * @since  2.6.0
	 * @return string Data in JSON format.
	 */
	public function __toString() {
		return wp_json_encode( $this->get_data() );
	}

	/**
	 * Returns all data for this object.
	 *
	 * @since  2.6.0
	 * @return array
	 */
	public function get_data() {
		return array_merge( array( 'id' => $this->get_id() ), $this->data, array( 'meta_data' => $this->get_meta_data() ) );
	}

	/**
	 * Returns array of expected data keys for this object.
	 *
	 * @since   3.0.0
	 * @return array
	 */
	public function get_data_keys() {
		return array_keys( $this->data );
	}

	/**
	 * Returns all "extra" data keys for an object (for sub objects like product types).
	 *
	 * @since  3.0.0
	 * @return array
	 */
	public function get_extra_data_keys() {
		return array_keys( $this->extra_data );
	}

	/**
	 * Filter null meta values from array.
	 *
	 * @since  3.0.0
	 * @param mixed $meta Meta value to check.
	 * @return bool
	 */
	protected function filter_null_meta( $meta ) {
		return ! is_null( $meta->value );
	}

	/**
	 * Get All Meta Data.
	 *
	 * @since 2.6.0
	 * @return array of objects.
	 */
	public function get_meta_data() {
		$this->maybe_read_meta_data();
		return array_values( array_filter( $this->meta_data, array( $this, 'filter_null_meta' ) ) );
	}

	/**
	 * Check if the key is an internal one.
	 *
	 * @since  3.2.0
	 * @param  string $key Key to check.
	 * @return bool   true if it's an internal key, false otherwise
	 */
	protected function is_internal_meta_key( $key ) {
		$internal_meta_key = ! empty( $key ) && $this->data_store && in_array( $key, $this->data_store->get_internal_meta_keys(), true );

		if ( ! $internal_meta_key ) {
			return false;
		}

		$has_setter_or_getter = is_callable( array( $this, 'set_' . $key ) ) || is_callable( array( $this, 'get_' . $key ) );

		if ( ! $has_setter_or_getter ) {
			return false;
		}
		/* translators: %s: $key Key to check */
		wc_doing_it_wrong( __FUNCTION__, sprintf( __( 'Generic add/update/get meta methods should not be used for internal meta data, including "%s". Use getters and setters.', 'woocommerce' ), $key ), '3.2.0' );

		return true;
	}

	/**
	 * Get Meta Data by Key.
	 *
	 * @since  2.6.0
	 * @param  string $key Meta Key.
	 * @param  bool   $single return first found meta with key, or all with $key.
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return mixed
	 */
	public function get_meta( $key = '', $single = true, $context = 'view' ) {
		if ( $this->is_internal_meta_key( $key ) ) {
			$function = 'get_' . $key;

			if ( is_callable( array( $this, $function ) ) ) {
				return $this->{$function}();
			}
		}

		$this->maybe_read_meta_data();
		$meta_data  = $this->get_meta_data();
		$array_keys = array_keys( wp_list_pluck( $meta_data, 'key' ), $key, true );
		$value      = $single ? '' : array();

		if ( ! empty( $array_keys ) ) {
			// We don't use the $this->meta_data property directly here because we don't want meta with a null value (i.e. meta which has been deleted via $this->delete_meta_data()).
			if ( $single ) {
				$value = $meta_data[ current( $array_keys ) ]->value;
			} else {
				$value = array_intersect_key( $meta_data, array_flip( $array_keys ) );
			}
		}

		if ( 'view' === $context ) {
			$value = apply_filters( $this->get_hook_prefix() . $key, $value, $this );
		}

		return $value;
	}

	/**
	 * See if meta data exists, since get_meta always returns a '' or array().
	 *
	 * @since  3.0.0
	 * @param  string $key Meta Key.
	 * @return boolean
	 */
	public function meta_exists( $key = '' ) {
		$this->maybe_read_meta_data();
		$array_keys = wp_list_pluck( $this->get_meta_data(), 'key' );
		return in_array( $key, $array_keys, true );
	}

	/**
	 * Set all meta data from array.
	 *
	 * @since 2.6.0
	 * @param array $data Key/Value pairs.
	 */
	public function set_meta_data( $data ) {
		if ( ! empty( $data ) && is_array( $data ) ) {
			$this->maybe_read_meta_data();
			foreach ( $data as $meta ) {
				$meta = (array) $meta;
				if ( isset( $meta['key'], $meta['value'], $meta['id'] ) ) {
					$this->meta_data[] = new WC_Meta_Data(
						array(
							'id'    => $meta['id'],
							'key'   => $meta['key'],
							'value' => $meta['value'],
						)
					);
				}
			}
		}
	}

	/**
	 * Add meta data.
	 *
	 * @since 2.6.0
	 *
	 * @param string       $key Meta key.
	 * @param string|array $value Meta value.
	 * @param bool         $unique Should this be a unique key?.
	 */
	public function add_meta_data( $key, $value, $unique = false ) {
		if ( $this->is_internal_meta_key( $key ) ) {
			$function = 'set_' . $key;

			if ( is_callable( array( $this, $function ) ) ) {
				return $this->{$function}( $value );
			}
		}

		$this->maybe_read_meta_data();
		if ( $unique ) {
			$this->delete_meta_data( $key );
		}
		$this->meta_data[] = new WC_Meta_Data(
			array(
				'key'   => $key,
				'value' => $value,
			)
		);
	}

	/**
	 * Update meta data by key or ID, if provided.
	 *
	 * @since  2.6.0
	 *
	 * @param  string       $key Meta key.
	 * @param  string|array $value Meta value.
	 * @param  int          $meta_id Meta ID.
	 */
	public function update_meta_data( $key, $value, $meta_id = 0 ) {
		if ( $this->is_internal_meta_key( $key ) ) {
			$function = 'set_' . $key;

			if ( is_callable( array( $this, $function ) ) ) {
				return $this->{$function}( $value );
			}
		}

		$this->maybe_read_meta_data();

		$array_key = false;

		if ( $meta_id ) {
			$array_keys = array_keys( wp_list_pluck( $this->meta_data, 'id' ), $meta_id, true );
			$array_key  = $array_keys ? current( $array_keys ) : false;
		} else {
			// Find matches by key.
			$matches = array();
			foreach ( $this->meta_data as $meta_data_array_key => $meta ) {
				if ( $meta->key === $key ) {
					$matches[] = $meta_data_array_key;
				}
			}

			if ( ! empty( $matches ) ) {
				// Set matches to null so only one key gets the new value.
				foreach ( $matches as $meta_data_array_key ) {
					$this->meta_data[ $meta_data_array_key ]->value = null;
				}
				$array_key = current( $matches );
			}
		}

		if ( false !== $array_key ) {
			$meta        = $this->meta_data[ $array_key ];
			$meta->key   = $key;
			$meta->value = $value;
		} else {
			$this->add_meta_data( $key, $value, true );
		}
	}

	/**
	 * Delete meta data.
	 *
	 * @since 2.6.0
	 * @param string $key Meta key.
	 */
	public function delete_meta_data( $key ) {
		$this->maybe_read_meta_data();
		$array_keys = array_keys( wp_list_pluck( $this->meta_data, 'key' ), $key, true );

		if ( $array_keys ) {
			foreach ( $array_keys as $array_key ) {
				$this->meta_data[ $array_key ]->value = null;
			}
		}
	}

	/**
	 * Delete meta data.
	 *
	 * @since 2.6.0
	 * @param int $mid Meta ID.
	 */
	public function delete_meta_data_by_mid( $mid ) {
		$this->maybe_read_meta_data();
		$array_keys = array_keys( wp_list_pluck( $this->meta_data, 'id' ), (int) $mid, true );

		if ( $array_keys ) {
			foreach ( $array_keys as $array_key ) {
				$this->meta_data[ $array_key ]->value = null;
			}
		}
	}

	/**
	 * Read meta data if null.
	 *
	 * @since 3.0.0
	 */
	protected function maybe_read_meta_data() {
		if ( is_null( $this->meta_data ) ) {
			$this->read_meta_data();
		}
	}

	/**
	 * Helper method to compute meta cache key. Different from WP Meta cache key in that meta data cached using this key also contains meta_id column.
	 *
	 * @since 4.7.0
	 *
	 * @return string
	 */
	public function get_meta_cache_key() {
		if ( ! $this->get_id() ) {
			wc_doing_it_wrong( 'get_meta_cache_key', 'ID needs to be set before fetching a cache key.', '4.7.0' );
			return false;
		}
		return self::generate_meta_cache_key( $this->get_id(), $this->cache_group );
	}

	/**
	 * Generate cache key from id and group.
	 *
	 * @since 4.7.0
	 *
	 * @param int|string $id          Object ID.
	 * @param string     $cache_group Group name use to store cache. Whole group cache can be invalidated in one go.
	 *
	 * @return string Meta cache key.
	 */
	public static function generate_meta_cache_key( $id, $cache_group ) {
		return WC_Cache_Helper::get_cache_prefix( $cache_group ) . WC_Cache_Helper::get_cache_prefix( 'object_' . $id ) . 'object_meta_' . $id;
	}

	/**
	 * Prime caches for raw meta data. This includes meta_id column as well, which is not included by default in WP meta data.
	 *
	 * @since 4.7.0
	 *
	 * @param array  $raw_meta_data_collection Array of objects of { object_id => array( meta_row_1, meta_row_2, ... }.
	 * @param string $cache_group              Name of cache group.
	 */
	public static function prime_raw_meta_data_cache( $raw_meta_data_collection, $cache_group ) {
		foreach ( $raw_meta_data_collection as $object_id => $raw_meta_data_array ) {
			$cache_key = self::generate_meta_cache_key( $object_id, $cache_group );
			wp_cache_set( $cache_key, $raw_meta_data_array, $cache_group );
		}
	}

	/**
	 * Read Meta Data from the database. Ignore any internal properties.
	 * Uses it's own caches because get_metadata does not provide meta_ids.
	 *
	 * @since 2.6.0
	 * @param bool $force_read True to force a new DB read (and update cache).
	 */
	public function read_meta_data( $force_read = false ) {
		$this->meta_data = array();
		$cache_loaded    = false;

		if ( ! $this->get_id() ) {
			return;
		}

		if ( ! $this->data_store ) {
			return;
		}

		if ( ! empty( $this->cache_group ) ) {
			// Prefix by group allows invalidation by group until https://core.trac.wordpress.org/ticket/4476 is implemented.
			$cache_key = $this->get_meta_cache_key();
		}

		if ( ! $force_read ) {
			if ( ! empty( $this->cache_group ) ) {
				$cached_meta  = wp_cache_get( $cache_key, $this->cache_group );
				$cache_loaded = ! empty( $cached_meta );
			}
		}

		// We filter the raw meta data again when loading from cache, in case we cached in an earlier version where filter conditions were different.
		$raw_meta_data = $cache_loaded ? $this->data_store->filter_raw_meta_data( $this, $cached_meta ) : $this->data_store->read_meta( $this );

		if ( $raw_meta_data ) {
			foreach ( $raw_meta_data as $meta ) {
				$this->meta_data[] = new WC_Meta_Data(
					array(
						'id'    => (int) $meta->meta_id,
						'key'   => $meta->meta_key,
						'value' => maybe_unserialize( $meta->meta_value ),
					)
				);
			}

			if ( ! $cache_loaded && ! empty( $this->cache_group ) ) {
				wp_cache_set( $cache_key, $raw_meta_data, $this->cache_group );
			}
		}
	}

	/**
	 * Update Meta Data in the database.
	 *
	 * @since 2.6.0
	 */
	public function save_meta_data() {
		if ( ! $this->data_store || is_null( $this->meta_data ) ) {
			return;
		}
		foreach ( $this->meta_data as $array_key => $meta ) {
			if ( is_null( $meta->value ) ) {
				if ( ! empty( $meta->id ) ) {
					$this->data_store->delete_meta( $this, $meta );
					unset( $this->meta_data[ $array_key ] );
				}
			} elseif ( empty( $meta->id ) ) {
				$meta->id = $this->data_store->add_meta( $this, $meta );
				$meta->apply_changes();
			} else {
				if ( $meta->get_changes() ) {
					$this->data_store->update_meta( $this, $meta );
					$meta->apply_changes();
				}
			}
		}
		if ( ! empty( $this->cache_group ) ) {
			$cache_key = WC_Cache_Helper::get_cache_prefix( $this->cache_group ) . WC_Cache_Helper::get_cache_prefix( 'object_' . $this->get_id() ) . 'object_meta_' . $this->get_id();
			wp_cache_delete( $cache_key, $this->cache_group );
		}
	}

	/**
	 * Set ID.
	 *
	 * @since 3.0.0
	 * @param int $id ID.
	 */
	public function set_id( $id ) {
		$this->id = absint( $id );
	}

	/**
	 * Set all props to default values.
	 *
	 * @since 3.0.0
	 */
	public function set_defaults() {
		$this->data    = $this->default_data;
		$this->changes = array();
		$this->set_object_read( false );
	}

	/**
	 * Set object read property.
	 *
	 * @since 3.0.0
	 * @param boolean $read Should read?.
	 */
	public function set_object_read( $read = true ) {
		$this->object_read = (bool) $read;
	}

	/**
	 * Get object read property.
	 *
	 * @since  3.0.0
	 * @return boolean
	 */
	public function get_object_read() {
		return (bool) $this->object_read;
	}

	/**
	 * Set a collection of props in one go, collect any errors, and return the result.
	 * Only sets using public methods.
	 *
	 * @since  3.0.0
	 *
	 * @param array  $props Key value pairs to set. Key is the prop and should map to a setter function name.
	 * @param string $context In what context to run this.
	 *
	 * @return bool|WP_Error
	 */
	public function set_props( $props, $context = 'set' ) {
		$errors = false;

		foreach ( $props as $prop => $value ) {
			try {
				/**
				 * Checks if the prop being set is allowed, and the value is not null.
				 */
				if ( is_null( $value ) || in_array( $prop, array( 'prop', 'date_prop', 'meta_data' ), true ) ) {
					continue;
				}
				$setter = "set_$prop";

				if ( is_callable( array( $this, $setter ) ) ) {
					$this->{$setter}( $value );
				}
			} catch ( WC_Data_Exception $e ) {
				if ( ! $errors ) {
					$errors = new WP_Error();
				}
				$errors->add( $e->getErrorCode(), $e->getMessage() );
			}
		}

		return $errors && count( $errors->get_error_codes() ) ? $errors : true;
	}

	/**
	 * Sets a prop for a setter method.
	 *
	 * This stores changes in a special array so we can track what needs saving
	 * the the DB later.
	 *
	 * @since 3.0.0
	 * @param string $prop Name of prop to set.
	 * @param mixed  $value Value of the prop.
	 */
	protected function set_prop( $prop, $value ) {
		if ( array_key_exists( $prop, $this->data ) ) {
			if ( true === $this->object_read ) {
				if ( $value !== $this->data[ $prop ] || array_key_exists( $prop, $this->changes ) ) {
					$this->changes[ $prop ] = $value;
				}
			} else {
				$this->data[ $prop ] = $value;
			}
		}
	}

	/**
	 * Return data changes only.
	 *
	 * @since 3.0.0
	 * @return array
	 */
	public function get_changes() {
		return $this->changes;
	}

	/**
	 * Merge changes with data and clear.
	 *
	 * @since 3.0.0
	 */
	public function apply_changes() {
		$this->data    = array_replace_recursive( $this->data, $this->changes ); // @codingStandardsIgnoreLine
		$this->changes = array();
	}

	/**
	 * Prefix for action and filter hooks on data.
	 *
	 * @since  3.0.0
	 * @return string
	 */
	protected function get_hook_prefix() {
		return 'woocommerce_' . $this->object_type . '_get_';
	}

	/**
	 * Gets a prop for a getter method.
	 *
	 * Gets the value from either current pending changes, or the data itself.
	 * Context controls what happens to the value before it's returned.
	 *
	 * @since  3.0.0
	 * @param  string $prop Name of prop to get.
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return mixed
	 */
	protected function get_prop( $prop, $context = 'view' ) {
		$value = null;

		if ( array_key_exists( $prop, $this->data ) ) {
			$value = array_key_exists( $prop, $this->changes ) ? $this->changes[ $prop ] : $this->data[ $prop ];

			if ( 'view' === $context ) {
				$value = apply_filters( $this->get_hook_prefix() . $prop, $value, $this );
			}
		}

		return $value;
	}

	/**
	 * Sets a date prop whilst handling formatting and datetime objects.
	 *
	 * @since 3.0.0
	 * @param string         $prop Name of prop to set.
	 * @param string|integer $value Value of the prop.
	 */
	protected function set_date_prop( $prop, $value ) {
		try {
			if ( empty( $value ) ) {
				$this->set_prop( $prop, null );
				return;
			}

			if ( is_a( $value, 'WC_DateTime' ) ) {
				$datetime = $value;
			} elseif ( is_numeric( $value ) ) {
				// Timestamps are handled as UTC timestamps in all cases.
				$datetime = new WC_DateTime( "@{$value}", new DateTimeZone( 'UTC' ) );
			} else {
				// Strings are defined in local WP timezone. Convert to UTC.
				if ( 1 === preg_match( '/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(Z|((-|\+)\d{2}:\d{2}))$/', $value, $date_bits ) ) {
					$offset    = ! empty( $date_bits[7] ) ? iso8601_timezone_to_offset( $date_bits[7] ) : wc_timezone_offset();
					$timestamp = gmmktime( $date_bits[4], $date_bits[5], $date_bits[6], $date_bits[2], $date_bits[3], $date_bits[1] ) - $offset;
				} else {
					$timestamp = wc_string_to_timestamp( get_gmt_from_date( gmdate( 'Y-m-d H:i:s', wc_string_to_timestamp( $value ) ) ) );
				}
				$datetime = new WC_DateTime( "@{$timestamp}", new DateTimeZone( 'UTC' ) );
			}

			// Set local timezone or offset.
			if ( get_option( 'timezone_string' ) ) {
				$datetime->setTimezone( new DateTimeZone( wc_timezone_string() ) );
			} else {
				$datetime->set_utc_offset( wc_timezone_offset() );
			}

			$this->set_prop( $prop, $datetime );
		} catch ( Exception $e ) {} // @codingStandardsIgnoreLine.
	}

	/**
	 * When invalid data is found, throw an exception unless reading from the DB.
	 *
	 * @throws WC_Data_Exception Data Exception.
	 * @since 3.0.0
	 * @param string $code             Error code.
	 * @param string $message          Error message.
	 * @param int    $http_status_code HTTP status code.
	 * @param array  $data             Extra error data.
	 */
	protected function error( $code, $message, $http_status_code = 400, $data = array() ) {
		throw new WC_Data_Exception( $code, $message, $http_status_code, $data );
	}
}
abstracts/abstract-wc-order.php000064400000205005151336044760012601 0ustar00<?php
/**
 * Abstract Order
 *
 * Handles generic order data and database interaction which is extended by both
 * WC_Order (regular orders) and WC_Order_Refund (refunds are negative orders).
 *
 * @class       WC_Abstract_Order
 * @version     3.0.0
 * @package     WooCommerce\Classes
 */

use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Utilities\ArrayUtil;
use Automattic\WooCommerce\Utilities\NumberUtil;

defined( 'ABSPATH' ) || exit;

require_once WC_ABSPATH . 'includes/legacy/abstract-wc-legacy-order.php';

/**
 * WC_Abstract_Order class.
 */
abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
	use WC_Item_Totals;

	/**
	 * Order Data array. This is the core order data exposed in APIs since 3.0.0.
	 *
	 * Notes: cart_tax = cart_tax is the new name for the legacy 'order_tax'
	 * which is the tax for items only, not shipping.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $data = array(
		'parent_id'          => 0,
		'status'             => '',
		'currency'           => '',
		'version'            => '',
		'prices_include_tax' => false,
		'date_created'       => null,
		'date_modified'      => null,
		'discount_total'     => 0,
		'discount_tax'       => 0,
		'shipping_total'     => 0,
		'shipping_tax'       => 0,
		'cart_tax'           => 0,
		'total'              => 0,
		'total_tax'          => 0,
	);

	/**
	 * Order items will be stored here, sometimes before they persist in the DB.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $items = array();

	/**
	 * Order items that need deleting are stored here.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $items_to_delete = array();

	/**
	 * Stores meta in cache for future reads.
	 *
	 * A group must be set to to enable caching.
	 *
	 * @var string
	 */
	protected $cache_group = 'orders';

	/**
	 * Which data store to load.
	 *
	 * @var string
	 */
	protected $data_store_name = 'order';

	/**
	 * This is the name of this object type.
	 *
	 * @var string
	 */
	protected $object_type = 'order';

	/**
	 * Get the order if ID is passed, otherwise the order is new and empty.
	 * This class should NOT be instantiated, but the wc_get_order function or new WC_Order_Factory
	 * should be used. It is possible, but the aforementioned are preferred and are the only
	 * methods that will be maintained going forward.
	 *
	 * @param  int|object|WC_Order $order Order to read.
	 */
	public function __construct( $order = 0 ) {
		parent::__construct( $order );

		if ( is_numeric( $order ) && $order > 0 ) {
			$this->set_id( $order );
		} elseif ( $order instanceof self ) {
			$this->set_id( $order->get_id() );
		} elseif ( ! empty( $order->ID ) ) {
			$this->set_id( $order->ID );
		} else {
			$this->set_object_read( true );
		}

		$this->data_store = WC_Data_Store::load( $this->data_store_name );

		if ( $this->get_id() > 0 ) {
			$this->data_store->read( $this );
		}
	}

	/**
	 * Get internal type.
	 *
	 * @return string
	 */
	public function get_type() {
		return 'shop_order';
	}

	/**
	 * Get all class data in array format.
	 *
	 * @since 3.0.0
	 * @return array
	 */
	public function get_data() {
		return array_merge(
			array(
				'id' => $this->get_id(),
			),
			$this->data,
			array(
				'meta_data'      => $this->get_meta_data(),
				'line_items'     => $this->get_items( 'line_item' ),
				'tax_lines'      => $this->get_items( 'tax' ),
				'shipping_lines' => $this->get_items( 'shipping' ),
				'fee_lines'      => $this->get_items( 'fee' ),
				'coupon_lines'   => $this->get_items( 'coupon' ),
			)
		);
	}

	/*
	|--------------------------------------------------------------------------
	| CRUD methods
	|--------------------------------------------------------------------------
	|
	| Methods which create, read, update and delete orders from the database.
	| Written in abstract fashion so that the way orders are stored can be
	| changed more easily in the future.
	|
	| A save method is included for convenience (chooses update or create based
	| on if the order exists yet).
	|
	*/

	/**
	 * Save data to the database.
	 *
	 * @since 3.0.0
	 * @return int order ID
	 */
	public function save() {
		if ( ! $this->data_store ) {
			return $this->get_id();
		}

		try {
			/**
			 * Trigger action before saving to the DB. Allows you to adjust object props before save.
			 *
			 * @param WC_Data          $this The object being saved.
			 * @param WC_Data_Store_WP $data_store THe data store persisting the data.
			 */
			do_action( 'woocommerce_before_' . $this->object_type . '_object_save', $this, $this->data_store );

			if ( $this->get_id() ) {
				$this->data_store->update( $this );
			} else {
				$this->data_store->create( $this );
			}

			$this->save_items();

			/**
			 * Trigger action after saving to the DB.
			 *
			 * @param WC_Data          $this The object being saved.
			 * @param WC_Data_Store_WP $data_store THe data store persisting the data.
			 */
			do_action( 'woocommerce_after_' . $this->object_type . '_object_save', $this, $this->data_store );

		} catch ( Exception $e ) {
			$this->handle_exception( $e, __( 'Error saving order.', 'woocommerce' ) );
		}

		return $this->get_id();
	}

	/**
	 * Log an error about this order is exception is encountered.
	 *
	 * @param Exception $e Exception object.
	 * @param string    $message Message regarding exception thrown.
	 * @since 3.7.0
	 */
	protected function handle_exception( $e, $message = 'Error' ) {
		wc_get_logger()->error(
			$message,
			array(
				'order' => $this,
				'error' => $e,
			)
		);
	}

	/**
	 * Save all order items which are part of this order.
	 */
	protected function save_items() {
		$items_changed = false;

		foreach ( $this->items_to_delete as $item ) {
			$item->delete();
			$items_changed = true;
		}
		$this->items_to_delete = array();

		// Add/save items.
		foreach ( $this->items as $item_group => $items ) {
			if ( is_array( $items ) ) {
				$items = array_filter( $items );
				foreach ( $items as $item_key => $item ) {
					$item->set_order_id( $this->get_id() );

					$item_id = $item->save();

					// If ID changed (new item saved to DB)...
					if ( $item_id !== $item_key ) {
						$this->items[ $item_group ][ $item_id ] = $item;

						unset( $this->items[ $item_group ][ $item_key ] );

						$items_changed = true;
					}
				}
			}
		}

		if ( $items_changed ) {
			delete_transient( 'wc_order_' . $this->get_id() . '_needs_processing' );
		}
	}

	/*
	|--------------------------------------------------------------------------
	| Getters
	|--------------------------------------------------------------------------
	*/

	/**
	 * Get parent order ID.
	 *
	 * @since 3.0.0
	 * @param  string $context View or edit context.
	 * @return integer
	 */
	public function get_parent_id( $context = 'view' ) {
		return $this->get_prop( 'parent_id', $context );
	}

	/**
	 * Gets order currency.
	 *
	 * @param  string $context View or edit context.
	 * @return string
	 */
	public function get_currency( $context = 'view' ) {
		return $this->get_prop( 'currency', $context );
	}

	/**
	 * Get order_version.
	 *
	 * @param  string $context View or edit context.
	 * @return string
	 */
	public function get_version( $context = 'view' ) {
		return $this->get_prop( 'version', $context );
	}

	/**
	 * Get prices_include_tax.
	 *
	 * @param  string $context View or edit context.
	 * @return bool
	 */
	public function get_prices_include_tax( $context = 'view' ) {
		return $this->get_prop( 'prices_include_tax', $context );
	}

	/**
	 * Get date_created.
	 *
	 * @param  string $context View or edit context.
	 * @return WC_DateTime|NULL object if the date is set or null if there is no date.
	 */
	public function get_date_created( $context = 'view' ) {
		return $this->get_prop( 'date_created', $context );
	}

	/**
	 * Get date_modified.
	 *
	 * @param  string $context View or edit context.
	 * @return WC_DateTime|NULL object if the date is set or null if there is no date.
	 */
	public function get_date_modified( $context = 'view' ) {
		return $this->get_prop( 'date_modified', $context );
	}

	/**
	 * Return the order statuses without wc- internal prefix.
	 *
	 * @param  string $context View or edit context.
	 * @return string
	 */
	public function get_status( $context = 'view' ) {
		$status = $this->get_prop( 'status', $context );

		if ( empty( $status ) && 'view' === $context ) {
			// In view context, return the default status if no status has been set.
			$status = apply_filters( 'woocommerce_default_order_status', 'pending' );
		}
		return $status;
	}

	/**
	 * Get discount_total.
	 *
	 * @param  string $context View or edit context.
	 * @return string
	 */
	public function get_discount_total( $context = 'view' ) {
		return $this->get_prop( 'discount_total', $context );
	}

	/**
	 * Get discount_tax.
	 *
	 * @param  string $context View or edit context.
	 * @return string
	 */
	public function get_discount_tax( $context = 'view' ) {
		return $this->get_prop( 'discount_tax', $context );
	}

	/**
	 * Get shipping_total.
	 *
	 * @param  string $context View or edit context.
	 * @return string
	 */
	public function get_shipping_total( $context = 'view' ) {
		return $this->get_prop( 'shipping_total', $context );
	}

	/**
	 * Get shipping_tax.
	 *
	 * @param  string $context View or edit context.
	 * @return string
	 */
	public function get_shipping_tax( $context = 'view' ) {
		return $this->get_prop( 'shipping_tax', $context );
	}

	/**
	 * Gets cart tax amount.
	 *
	 * @param  string $context View or edit context.
	 * @return float
	 */
	public function get_cart_tax( $context = 'view' ) {
		return $this->get_prop( 'cart_tax', $context );
	}

	/**
	 * Gets order grand total. incl. taxes. Used in gateways.
	 *
	 * @param  string $context View or edit context.
	 * @return float
	 */
	public function get_total( $context = 'view' ) {
		return $this->get_prop( 'total', $context );
	}

	/**
	 * Get total tax amount. Alias for get_order_tax().
	 *
	 * @param  string $context View or edit context.
	 * @return float
	 */
	public function get_total_tax( $context = 'view' ) {
		return $this->get_prop( 'total_tax', $context );
	}

	/*
	|--------------------------------------------------------------------------
	| Non-CRUD Getters
	|--------------------------------------------------------------------------
	*/

	/**
	 * Gets the total discount amount.
	 *
	 * @param  bool $ex_tax Show discount excl any tax.
	 * @return float
	 */
	public function get_total_discount( $ex_tax = true ) {
		if ( $ex_tax ) {
			$total_discount = $this->get_discount_total();
		} else {
			$total_discount = $this->get_discount_total() + $this->get_discount_tax();
		}
		return apply_filters( 'woocommerce_order_get_total_discount', NumberUtil::round( $total_discount, WC_ROUNDING_PRECISION ), $this );
	}

	/**
	 * Gets order subtotal.
	 *
	 * @return float
	 */
	public function get_subtotal() {
		$subtotal = NumberUtil::round( $this->get_cart_subtotal_for_order(), wc_get_price_decimals() );
		return apply_filters( 'woocommerce_order_get_subtotal', (float) $subtotal, $this );
	}

	/**
	 * Get taxes, merged by code, formatted ready for output.
	 *
	 * @return array
	 */
	public function get_tax_totals() {
		$tax_totals = array();

		foreach ( $this->get_items( 'tax' ) as $key => $tax ) {
			$code = $tax->get_rate_code();

			if ( ! isset( $tax_totals[ $code ] ) ) {
				$tax_totals[ $code ]         = new stdClass();
				$tax_totals[ $code ]->amount = 0;
			}

			$tax_totals[ $code ]->id               = $key;
			$tax_totals[ $code ]->rate_id          = $tax->get_rate_id();
			$tax_totals[ $code ]->is_compound      = $tax->is_compound();
			$tax_totals[ $code ]->label            = $tax->get_label();
			$tax_totals[ $code ]->amount          += (float) $tax->get_tax_total() + (float) $tax->get_shipping_tax_total();
			$tax_totals[ $code ]->formatted_amount = wc_price( $tax_totals[ $code ]->amount, array( 'currency' => $this->get_currency() ) );
		}

		if ( apply_filters( 'woocommerce_order_hide_zero_taxes', true ) ) {
			$amounts    = array_filter( wp_list_pluck( $tax_totals, 'amount' ) );
			$tax_totals = array_intersect_key( $tax_totals, $amounts );
		}

		return apply_filters( 'woocommerce_order_get_tax_totals', $tax_totals, $this );
	}

	/**
	 * Get all valid statuses for this order
	 *
	 * @since 3.0.0
	 * @return array Internal status keys e.g. 'wc-processing'
	 */
	protected function get_valid_statuses() {
		return array_keys( wc_get_order_statuses() );
	}

	/**
	 * Get user ID. Used by orders, not other order types like refunds.
	 *
	 * @param  string $context View or edit context.
	 * @return int
	 */
	public function get_user_id( $context = 'view' ) {
		return 0;
	}

	/**
	 * Get user. Used by orders, not other order types like refunds.
	 *
	 * @return WP_User|false
	 */
	public function get_user() {
		return false;
	}

	/*
	|--------------------------------------------------------------------------
	| Setters
	|--------------------------------------------------------------------------
	|
	| Functions for setting order data. These should not update anything in the
	| database itself and should only change what is stored in the class
	| object. However, for backwards compatibility pre 3.0.0 some of these
	| setters may handle both.
	*/

	/**
	 * Set parent order ID.
	 *
	 * @since 3.0.0
	 * @param int $value Value to set.
	 * @throws WC_Data_Exception Exception thrown if parent ID does not exist or is invalid.
	 */
	public function set_parent_id( $value ) {
		if ( $value && ( $value === $this->get_id() || ! wc_get_order( $value ) ) ) {
			$this->error( 'order_invalid_parent_id', __( 'Invalid parent ID', 'woocommerce' ) );
		}
		$this->set_prop( 'parent_id', absint( $value ) );
	}

	/**
	 * Set order status.
	 *
	 * @since 3.0.0
	 * @param string $new_status Status to change the order to. No internal wc- prefix is required.
	 * @return array details of change
	 */
	public function set_status( $new_status ) {
		$old_status = $this->get_status();
		$new_status = 'wc-' === substr( $new_status, 0, 3 ) ? substr( $new_status, 3 ) : $new_status;

		// If setting the status, ensure it's set to a valid status.
		if ( true === $this->object_read ) {
			// Only allow valid new status.
			if ( ! in_array( 'wc-' . $new_status, $this->get_valid_statuses(), true ) && 'trash' !== $new_status ) {
				$new_status = 'pending';
			}

			// If the old status is set but unknown (e.g. draft) assume its pending for action usage.
			if ( $old_status && ! in_array( 'wc-' . $old_status, $this->get_valid_statuses(), true ) && 'trash' !== $old_status ) {
				$old_status = 'pending';
			}
		}

		$this->set_prop( 'status', $new_status );

		return array(
			'from' => $old_status,
			'to'   => $new_status,
		);
	}

	/**
	 * Set order_version.
	 *
	 * @param string $value Value to set.
	 * @throws WC_Data_Exception Exception may be thrown if value is invalid.
	 */
	public function set_version( $value ) {
		$this->set_prop( 'version', $value );
	}

	/**
	 * Set order_currency.
	 *
	 * @param string $value Value to set.
	 * @throws WC_Data_Exception Exception may be thrown if value is invalid.
	 */
	public function set_currency( $value ) {
		if ( $value && ! in_array( $value, array_keys( get_woocommerce_currencies() ), true ) ) {
			$this->error( 'order_invalid_currency', __( 'Invalid currency code', 'woocommerce' ) );
		}
		$this->set_prop( 'currency', $value ? $value : get_woocommerce_currency() );
	}

	/**
	 * Set prices_include_tax.
	 *
	 * @param bool $value Value to set.
	 * @throws WC_Data_Exception Exception may be thrown if value is invalid.
	 */
	public function set_prices_include_tax( $value ) {
		$this->set_prop( 'prices_include_tax', (bool) $value );
	}

	/**
	 * Set date_created.
	 *
	 * @param  string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date.
	 * @throws WC_Data_Exception Exception may be thrown if value is invalid.
	 */
	public function set_date_created( $date = null ) {
		$this->set_date_prop( 'date_created', $date );
	}

	/**
	 * Set date_modified.
	 *
	 * @param  string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date.
	 * @throws WC_Data_Exception Exception may be thrown if value is invalid.
	 */
	public function set_date_modified( $date = null ) {
		$this->set_date_prop( 'date_modified', $date );
	}

	/**
	 * Set discount_total.
	 *
	 * @param string $value Value to set.
	 * @throws WC_Data_Exception Exception may be thrown if value is invalid.
	 */
	public function set_discount_total( $value ) {
		$this->set_prop( 'discount_total', wc_format_decimal( $value ) );
	}

	/**
	 * Set discount_tax.
	 *
	 * @param string $value Value to set.
	 * @throws WC_Data_Exception Exception may be thrown if value is invalid.
	 */
	public function set_discount_tax( $value ) {
		$this->set_prop( 'discount_tax', wc_format_decimal( $value ) );
	}

	/**
	 * Set shipping_total.
	 *
	 * @param string $value Value to set.
	 * @throws WC_Data_Exception Exception may be thrown if value is invalid.
	 */
	public function set_shipping_total( $value ) {
		$this->set_prop( 'shipping_total', wc_format_decimal( $value ) );
	}

	/**
	 * Set shipping_tax.
	 *
	 * @param string $value Value to set.
	 * @throws WC_Data_Exception Exception may be thrown if value is invalid.
	 */
	public function set_shipping_tax( $value ) {
		$this->set_prop( 'shipping_tax', wc_format_decimal( $value ) );
		$this->set_total_tax( (float) $this->get_cart_tax() + (float) $this->get_shipping_tax() );
	}

	/**
	 * Set cart tax.
	 *
	 * @param string $value Value to set.
	 * @throws WC_Data_Exception Exception may be thrown if value is invalid.
	 */
	public function set_cart_tax( $value ) {
		$this->set_prop( 'cart_tax', wc_format_decimal( $value ) );
		$this->set_total_tax( (float) $this->get_cart_tax() + (float) $this->get_shipping_tax() );
	}

	/**
	 * Sets order tax (sum of cart and shipping tax). Used internally only.
	 *
	 * @param string $value Value to set.
	 * @throws WC_Data_Exception Exception may be thrown if value is invalid.
	 */
	protected function set_total_tax( $value ) {
		// We round here because this is a total entry, as opposed to line items in other setters.
		$this->set_prop( 'total_tax', wc_format_decimal( NumberUtil::round( $value, wc_get_price_decimals() ) ) );
	}

	/**
	 * Set total.
	 *
	 * @param string $value Value to set.
	 * @param string $deprecated Function used to set different totals based on this.
	 *
	 * @return bool|void
	 * @throws WC_Data_Exception Exception may be thrown if value is invalid.
	 */
	public function set_total( $value, $deprecated = '' ) {
		if ( $deprecated ) {
			wc_deprecated_argument( 'total_type', '3.0', 'Use dedicated total setter methods instead.' );
			return $this->legacy_set_total( $value, $deprecated );
		}
		$this->set_prop( 'total', wc_format_decimal( $value, wc_get_price_decimals() ) );
	}

	/*
	|--------------------------------------------------------------------------
	| Order Item Handling
	|--------------------------------------------------------------------------
	|
	| Order items are used for products, taxes, shipping, and fees within
	| each order.
	*/

	/**
	 * Remove all line items (products, coupons, shipping, taxes) from the order.
	 *
	 * @param string $type Order item type. Default null.
	 */
	public function remove_order_items( $type = null ) {
		if ( ! empty( $type ) ) {
			$this->data_store->delete_items( $this, $type );

			$group = $this->type_to_group( $type );

			if ( $group ) {
				unset( $this->items[ $group ] );
			}
		} else {
			$this->data_store->delete_items( $this );
			$this->items = array();
		}
	}

	/**
	 * Convert a type to a types group.
	 *
	 * @param string $type type to lookup.
	 * @return string
	 */
	protected function type_to_group( $type ) {
		$type_to_group = apply_filters(
			'woocommerce_order_type_to_group',
			array(
				'line_item' => 'line_items',
				'tax'       => 'tax_lines',
				'shipping'  => 'shipping_lines',
				'fee'       => 'fee_lines',
				'coupon'    => 'coupon_lines',
			)
		);
		return isset( $type_to_group[ $type ] ) ? $type_to_group[ $type ] : '';
	}

	/**
	 * Return an array of items/products within this order.
	 *
	 * @param string|array $types Types of line items to get (array or string).
	 * @return WC_Order_Item[]
	 */
	public function get_items( $types = 'line_item' ) {
		$items = array();
		$types = array_filter( (array) $types );

		foreach ( $types as $type ) {
			$group = $this->type_to_group( $type );

			if ( $group ) {
				if ( ! isset( $this->items[ $group ] ) ) {
					$this->items[ $group ] = array_filter( $this->data_store->read_items( $this, $type ) );
				}
				// Don't use array_merge here because keys are numeric.
				$items = $items + $this->items[ $group ];
			}
		}

		return apply_filters( 'woocommerce_order_get_items', $items, $this, $types );
	}

	/**
	 * Return array of values for calculations.
	 *
	 * @param string $field Field name to return.
	 *
	 * @return array Array of values.
	 */
	protected function get_values_for_total( $field ) {
		$items = array_map(
			function ( $item ) use ( $field ) {
				return wc_add_number_precision( $item[ $field ], false );
			},
			array_values( $this->get_items() )
		);
		return $items;
	}

	/**
	 * Return an array of coupons within this order.
	 *
	 * @since  3.7.0
	 * @return WC_Order_Item_Coupon[]
	 */
	public function get_coupons() {
		return $this->get_items( 'coupon' );
	}

	/**
	 * Return an array of fees within this order.
	 *
	 * @return WC_Order_item_Fee[]
	 */
	public function get_fees() {
		return $this->get_items( 'fee' );
	}

	/**
	 * Return an array of taxes within this order.
	 *
	 * @return WC_Order_Item_Tax[]
	 */
	public function get_taxes() {
		return $this->get_items( 'tax' );
	}

	/**
	 * Return an array of shipping costs within this order.
	 *
	 * @return WC_Order_Item_Shipping[]
	 */
	public function get_shipping_methods() {
		return $this->get_items( 'shipping' );
	}

	/**
	 * Gets formatted shipping method title.
	 *
	 * @return string
	 */
	public function get_shipping_method() {
		$names = array();
		foreach ( $this->get_shipping_methods() as $shipping_method ) {
			$names[] = $shipping_method->get_name();
		}
		return apply_filters( 'woocommerce_order_shipping_method', implode( ', ', $names ), $this );
	}

	/**
	 * Get used coupon codes only.
	 *
	 * @since 3.7.0
	 * @return array
	 */
	public function get_coupon_codes() {
		$coupon_codes = array();
		$coupons      = $this->get_items( 'coupon' );

		if ( $coupons ) {
			foreach ( $coupons as $coupon ) {
				$coupon_codes[] = $coupon->get_code();
			}
		}
		return $coupon_codes;
	}

	/**
	 * Gets the count of order items of a certain type.
	 *
	 * @param string $item_type Item type to lookup.
	 * @return int|string
	 */
	public function get_item_count( $item_type = '' ) {
		$items = $this->get_items( empty( $item_type ) ? 'line_item' : $item_type );
		$count = 0;

		foreach ( $items as $item ) {
			$count += $item->get_quantity();
		}

		return apply_filters( 'woocommerce_get_item_count', $count, $item_type, $this );
	}

	/**
	 * Get an order item object, based on its type.
	 *
	 * @since  3.0.0
	 * @param  int  $item_id ID of item to get.
	 * @param  bool $load_from_db Prior to 3.2 this item was loaded direct from WC_Order_Factory, not this object. This param is here for backwards compatility with that. If false, uses the local items variable instead.
	 * @return WC_Order_Item|false
	 */
	public function get_item( $item_id, $load_from_db = true ) {
		if ( $load_from_db ) {
			return WC_Order_Factory::get_order_item( $item_id );
		}

		// Search for item id.
		if ( $this->items ) {
			foreach ( $this->items as $group => $items ) {
				if ( isset( $items[ $item_id ] ) ) {
					return $items[ $item_id ];
				}
			}
		}

		// Load all items of type and cache.
		$type = $this->data_store->get_order_item_type( $this, $item_id );

		if ( ! $type ) {
			return false;
		}

		$items = $this->get_items( $type );

		return ! empty( $items[ $item_id ] ) ? $items[ $item_id ] : false;
	}

	/**
	 * Get key for where a certain item type is stored in _items.
	 *
	 * @since  3.0.0
	 * @param  string $item object Order item (product, shipping, fee, coupon, tax).
	 * @return string
	 */
	protected function get_items_key( $item ) {
		if ( is_a( $item, 'WC_Order_Item_Product' ) ) {
			return 'line_items';
		} elseif ( is_a( $item, 'WC_Order_Item_Fee' ) ) {
			return 'fee_lines';
		} elseif ( is_a( $item, 'WC_Order_Item_Shipping' ) ) {
			return 'shipping_lines';
		} elseif ( is_a( $item, 'WC_Order_Item_Tax' ) ) {
			return 'tax_lines';
		} elseif ( is_a( $item, 'WC_Order_Item_Coupon' ) ) {
			return 'coupon_lines';
		}
		return apply_filters( 'woocommerce_get_items_key', '', $item );
	}

	/**
	 * Remove item from the order.
	 *
	 * @param int $item_id Item ID to delete.
	 * @return false|void
	 */
	public function remove_item( $item_id ) {
		$item      = $this->get_item( $item_id, false );
		$items_key = $item ? $this->get_items_key( $item ) : false;

		if ( ! $items_key ) {
			return false;
		}

		// Unset and remove later.
		$this->items_to_delete[] = $item;
		unset( $this->items[ $items_key ][ $item->get_id() ] );
	}

	/**
	 * Adds an order item to this order. The order item will not persist until save.
	 *
	 * @since 3.0.0
	 * @param WC_Order_Item $item Order item object (product, shipping, fee, coupon, tax).
	 * @return false|void
	 */
	public function add_item( $item ) {
		$items_key = $this->get_items_key( $item );

		if ( ! $items_key ) {
			return false;
		}

		// Make sure existing items are loaded so we can append this new one.
		if ( ! isset( $this->items[ $items_key ] ) ) {
			$this->items[ $items_key ] = $this->get_items( $item->get_type() );
		}

		// Set parent.
		$item->set_order_id( $this->get_id() );

		// Append new row with generated temporary ID.
		$item_id = $item->get_id();

		if ( $item_id ) {
			$this->items[ $items_key ][ $item_id ] = $item;
		} else {
			$this->items[ $items_key ][ 'new:' . $items_key . count( $this->items[ $items_key ] ) ] = $item;
		}
	}

	/**
	 * Check and records coupon usage tentatively so that counts validation is correct. Display an error if coupon usage limit has been reached.
	 *
	 * If you are using this method, make sure to `release_held_coupons` in case an Exception is thrown.
	 *
	 * @throws Exception When not able to apply coupon.
	 *
	 * @param string $billing_email Billing email of order.
	 */
	public function hold_applied_coupons( $billing_email ) {
		$held_keys          = array();
		$held_keys_for_user = array();
		$error              = null;

		try {
			foreach ( WC()->cart->get_applied_coupons() as $code ) {
				$coupon = new WC_Coupon( $code );
				if ( ! $coupon->get_data_store() ) {
					continue;
				}

				// Hold coupon for when global coupon usage limit is present.
				if ( 0 < $coupon->get_usage_limit() ) {
					$held_key = $this->hold_coupon( $coupon );
					if ( $held_key ) {
						$held_keys[ $coupon->get_id() ] = $held_key;
					}
				}

				// Hold coupon for when usage limit per customer is enabled.
				if ( 0 < $coupon->get_usage_limit_per_user() ) {

					if ( ! isset( $user_ids_and_emails ) ) {
						$user_alias          = get_current_user_id() ? wp_get_current_user()->ID : sanitize_email( $billing_email );
						$user_ids_and_emails = $this->get_billing_and_current_user_aliases( $billing_email );
					}

					$held_key_for_user = $this->hold_coupon_for_users( $coupon, $user_ids_and_emails, $user_alias );

					if ( $held_key_for_user ) {
						$held_keys_for_user[ $coupon->get_id() ] = $held_key_for_user;
					}
				}
			}
		} catch ( Exception $e ) {
			$error = $e;
		} finally {
			// Even in case of error, we will save keys for whatever coupons that were held so our data remains accurate.
			// We save them in bulk instead of one by one for performance reasons.
			if ( 0 < count( $held_keys_for_user ) || 0 < count( $held_keys ) ) {
				$this->get_data_store()->set_coupon_held_keys( $this, $held_keys, $held_keys_for_user );
			}
			if ( $error instanceof Exception ) {
				throw $error;
			}
		}
	}


	/**
	 * Hold coupon if a global usage limit is defined.
	 *
	 * @param WC_Coupon $coupon Coupon object.
	 *
	 * @return string    Meta key which indicates held coupon.
	 * @throws Exception When can't be held.
	 */
	private function hold_coupon( $coupon ) {
		$result = $coupon->get_data_store()->check_and_hold_coupon( $coupon );
		if ( false === $result ) {
			// translators: Actual coupon code.
			throw new Exception( sprintf( __( 'An unexpected error happened while applying the Coupon %s.', 'woocommerce' ), esc_html( $coupon->get_code() ) ) );
		} elseif ( 0 === $result ) {
			// translators: Actual coupon code.
			throw new Exception( sprintf( __( 'Coupon %s was used in another transaction during this checkout, and coupon usage limit is reached. Please remove the coupon and try again.', 'woocommerce' ), esc_html( $coupon->get_code() ) ) );
		}
		return $result;
	}

	/**
	 * Hold coupon if usage limit per customer is defined.
	 *
	 * @param WC_Coupon $coupon              Coupon object.
	 * @param array     $user_ids_and_emails Array of user Id and emails to check for usage limit.
	 * @param string    $user_alias          User ID or email to use to record current usage.
	 *
	 * @return string    Meta key which indicates held coupon.
	 * @throws Exception When coupon can't be held.
	 */
	private function hold_coupon_for_users( $coupon, $user_ids_and_emails, $user_alias ) {
		$result = $coupon->get_data_store()->check_and_hold_coupon_for_user( $coupon, $user_ids_and_emails, $user_alias );
		if ( false === $result ) {
			// translators: Actual coupon code.
			throw new Exception( sprintf( __( 'An unexpected error happened while applying the Coupon %s.', 'woocommerce' ), esc_html( $coupon->get_code() ) ) );
		} elseif ( 0 === $result ) {
			// translators: Actual coupon code.
			throw new Exception( sprintf( __( 'You have used this coupon %s in another transaction during this checkout, and coupon usage limit is reached. Please remove the coupon and try again.', 'woocommerce' ), esc_html( $coupon->get_code() ) ) );
		}
		return $result;
	}

	/**
	 * Helper method to get all aliases for current user and provide billing email.
	 *
	 * @param string $billing_email Billing email provided in form.
	 *
	 * @return array     Array of all aliases.
	 * @throws Exception When validation fails.
	 */
	private function get_billing_and_current_user_aliases( $billing_email ) {
		$emails = array( $billing_email );
		if ( get_current_user_id() ) {
			$emails[] = wp_get_current_user()->user_email;
		}
		$emails              = array_unique(
			array_map( 'strtolower', array_map( 'sanitize_email', $emails ) )
		);
		$customer_data_store = WC_Data_Store::load( 'customer' );
		$user_ids            = $customer_data_store->get_user_ids_for_billing_email( $emails );
		return array_merge( $user_ids, $emails );
	}

	/**
	 * Apply a coupon to the order and recalculate totals.
	 *
	 * @since 3.2.0
	 * @param string|WC_Coupon $raw_coupon Coupon code or object.
	 * @return true|WP_Error True if applied, error if not.
	 */
	public function apply_coupon( $raw_coupon ) {
		if ( is_a( $raw_coupon, 'WC_Coupon' ) ) {
			$coupon = $raw_coupon;
		} elseif ( is_string( $raw_coupon ) ) {
			$code   = wc_format_coupon_code( $raw_coupon );
			$coupon = new WC_Coupon( $code );

			if ( $coupon->get_code() !== $code ) {
				return new WP_Error( 'invalid_coupon', __( 'Invalid coupon code', 'woocommerce' ) );
			}
		} else {
			return new WP_Error( 'invalid_coupon', __( 'Invalid coupon', 'woocommerce' ) );
		}

		// Check to make sure coupon is not already applied.
		$applied_coupons = $this->get_items( 'coupon' );
		foreach ( $applied_coupons as $applied_coupon ) {
			if ( $applied_coupon->get_code() === $coupon->get_code() ) {
				return new WP_Error( 'invalid_coupon', __( 'Coupon code already applied!', 'woocommerce' ) );
			}
		}

		$discounts = new WC_Discounts( $this );
		$applied   = $discounts->apply_coupon( $coupon );

		if ( is_wp_error( $applied ) ) {
			return $applied;
		}

		$data_store = $coupon->get_data_store();

		// Check specific for guest checkouts here as well since WC_Cart handles that seperately in check_customer_coupons.
		if ( $data_store && 0 === $this->get_customer_id() ) {
			$usage_count = $data_store->get_usage_by_email( $coupon, $this->get_billing_email() );
			if ( 0 < $coupon->get_usage_limit_per_user() && $usage_count >= $coupon->get_usage_limit_per_user() ) {
				return new WP_Error(
					'invalid_coupon',
					$coupon->get_coupon_error( 106 ),
					array(
						'status' => 400,
					)
				);
			}
		}

		$this->set_coupon_discount_amounts( $discounts );
		$this->save();

		// Recalculate totals and taxes.
		$this->recalculate_coupons();

		// Record usage so counts and validation is correct.
		$used_by = $this->get_user_id();

		if ( ! $used_by ) {
			$used_by = $this->get_billing_email();
		}

		$coupon->increase_usage_count( $used_by );

		return true;
	}

	/**
	 * Remove a coupon from the order and recalculate totals.
	 *
	 * Coupons affect line item totals, but there is no relationship between
	 * coupon and line total, so to remove a coupon we need to work from the
	 * line subtotal (price before discount) and re-apply all coupons in this
	 * order.
	 *
	 * Manual discounts are not affected; those are separate and do not affect
	 * stored line totals.
	 *
	 * @since  3.2.0
	 * @param  string $code Coupon code.
	 * @return void
	 */
	public function remove_coupon( $code ) {
		$coupons = $this->get_items( 'coupon' );

		// Remove the coupon line.
		foreach ( $coupons as $item_id => $coupon ) {
			if ( $coupon->get_code() === $code ) {
				$this->remove_item( $item_id );
				$coupon_object = new WC_Coupon( $code );
				$coupon_object->decrease_usage_count( $this->get_user_id() );
				$this->recalculate_coupons();
				break;
			}
		}
	}

	/**
	 * Apply all coupons in this order again to all line items.
	 * This method is public since WooCommerce 3.8.0.
	 *
	 * @since 3.2.0
	 */
	public function recalculate_coupons() {
		// Reset line item totals.
		foreach ( $this->get_items() as $item ) {
			$item->set_total( $item->get_subtotal() );
			$item->set_total_tax( $item->get_subtotal_tax() );
		}

		$discounts = new WC_Discounts( $this );

		foreach ( $this->get_items( 'coupon' ) as $coupon_item ) {
			$coupon_code = $coupon_item->get_code();
			$coupon_id   = wc_get_coupon_id_by_code( $coupon_code );

			// If we have a coupon ID (loaded via wc_get_coupon_id_by_code) we can simply load the new coupon object using the ID.
			if ( $coupon_id ) {
				$coupon_object = new WC_Coupon( $coupon_id );

			} else {

				// If we do not have a coupon ID (was it virtual? has it been deleted?) we must create a temporary coupon using what data we have stored during checkout.
				$coupon_object = new WC_Coupon();
				$coupon_object->set_props( (array) $coupon_item->get_meta( 'coupon_data', true ) );
				$coupon_object->set_code( $coupon_code );
				$coupon_object->set_virtual( true );

				// If there is no coupon amount (maybe dynamic?), set it to the given **discount** amount so the coupon's same value is applied.
				if ( ! $coupon_object->get_amount() ) {

					// If the order originally had prices including tax, remove the discount + discount tax.
					if ( $this->get_prices_include_tax() ) {
						$coupon_object->set_amount( $coupon_item->get_discount() + $coupon_item->get_discount_tax() );
					} else {
						$coupon_object->set_amount( $coupon_item->get_discount() );
					}
					$coupon_object->set_discount_type( 'fixed_cart' );
				}
			}

			/**
			 * Allow developers to filter this coupon before it get's re-applied to the order.
			 *
			 * @since 3.2.0
			 */
			$coupon_object = apply_filters( 'woocommerce_order_recalculate_coupons_coupon_object', $coupon_object, $coupon_code, $coupon_item, $this );

			if ( $coupon_object ) {
				$discounts->apply_coupon( $coupon_object, false );
			}
		}

		$this->set_coupon_discount_amounts( $discounts );
		$this->set_item_discount_amounts( $discounts );

		// Recalculate totals and taxes.
		$this->calculate_totals( true );
	}

	/**
	 * After applying coupons via the WC_Discounts class, update line items.
	 *
	 * @since 3.2.0
	 * @param WC_Discounts $discounts Discounts class.
	 */
	protected function set_item_discount_amounts( $discounts ) {
		$item_discounts = $discounts->get_discounts_by_item();
		$tax_location   = $this->get_tax_location();
		$tax_location   = array( $tax_location['country'], $tax_location['state'], $tax_location['postcode'], $tax_location['city'] );

		if ( $item_discounts ) {
			foreach ( $item_discounts as $item_id => $amount ) {
				$item = $this->get_item( $item_id, false );

				// If the prices include tax, discounts should be taken off the tax inclusive prices like in the cart.
				if ( $this->get_prices_include_tax() && wc_tax_enabled() && 'taxable' === $item->get_tax_status() ) {
					$taxes = WC_Tax::calc_tax( $amount, $this->get_tax_rates( $item->get_tax_class(), $tax_location ), true );

					// Use unrounded taxes so totals will be re-calculated accurately, like in cart.
					$amount = $amount - array_sum( $taxes );
				}

				$item->set_total( max( 0, $item->get_total() - $amount ) );
			}
		}
	}

	/**
	 * After applying coupons via the WC_Discounts class, update or create coupon items.
	 *
	 * @since 3.2.0
	 * @param WC_Discounts $discounts Discounts class.
	 */
	protected function set_coupon_discount_amounts( $discounts ) {
		$coupons           = $this->get_items( 'coupon' );
		$coupon_code_to_id = wc_list_pluck( $coupons, 'get_id', 'get_code' );
		$all_discounts     = $discounts->get_discounts();
		$coupon_discounts  = $discounts->get_discounts_by_coupon();
		$tax_location      = $this->get_tax_location();
		$tax_location      = array(
			$tax_location['country'],
			$tax_location['state'],
			$tax_location['postcode'],
			$tax_location['city'],
		);

		if ( $coupon_discounts ) {
			foreach ( $coupon_discounts as $coupon_code => $amount ) {
				$item_id = isset( $coupon_code_to_id[ $coupon_code ] ) ? $coupon_code_to_id[ $coupon_code ] : 0;

				if ( ! $item_id ) {
					$coupon_item = new WC_Order_Item_Coupon();
					$coupon_item->set_code( $coupon_code );
				} else {
					$coupon_item = $this->get_item( $item_id, false );
				}

				$discount_tax = 0;

				// Work out how much tax has been removed as a result of the discount from this coupon.
				foreach ( $all_discounts[ $coupon_code ] as $item_id => $item_discount_amount ) {
					$item = $this->get_item( $item_id, false );

					if ( 'taxable' !== $item->get_tax_status() || ! wc_tax_enabled() ) {
						continue;
					}

					$taxes = array_sum( WC_Tax::calc_tax( $item_discount_amount, $this->get_tax_rates( $item->get_tax_class(), $tax_location ), $this->get_prices_include_tax() ) );
					if ( 'yes' !== get_option( 'woocommerce_tax_round_at_subtotal' ) ) {
						$taxes = wc_round_tax_total( $taxes );
					}

					$discount_tax += $taxes;

					if ( $this->get_prices_include_tax() ) {
						$amount = $amount - $taxes;
					}
				}

				$coupon_item->set_discount( $amount );
				$coupon_item->set_discount_tax( $discount_tax );

				$this->add_item( $coupon_item );
			}
		}
	}

	/**
	 * Add a product line item to the order. This is the only line item type with
	 * its own method because it saves looking up order amounts (costs are added up for you).
	 *
	 * @param  WC_Product $product Product object.
	 * @param  int        $qty Quantity to add.
	 * @param  array      $args Args for the added product.
	 * @return int
	 */
	public function add_product( $product, $qty = 1, $args = array() ) {
		if ( $product ) {
			$order = ArrayUtil::get_value_or_default( $args, 'order' );
			$total = wc_get_price_excluding_tax(
				$product,
				array(
					'qty'   => $qty,
					'order' => $order,
				)
			);

			$default_args = array(
				'name'         => $product->get_name(),
				'tax_class'    => $product->get_tax_class(),
				'product_id'   => $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id(),
				'variation_id' => $product->is_type( 'variation' ) ? $product->get_id() : 0,
				'variation'    => $product->is_type( 'variation' ) ? $product->get_attributes() : array(),
				'subtotal'     => $total,
				'total'        => $total,
				'quantity'     => $qty,
			);
		} else {
			$default_args = array(
				'quantity' => $qty,
			);
		}

		$args = wp_parse_args( $args, $default_args );

		// BW compatibility with old args.
		if ( isset( $args['totals'] ) ) {
			foreach ( $args['totals'] as $key => $value ) {
				if ( 'tax' === $key ) {
					$args['total_tax'] = $value;
				} elseif ( 'tax_data' === $key ) {
					$args['taxes'] = $value;
				} else {
					$args[ $key ] = $value;
				}
			}
		}

		$item = wc_get_container()->get( LegacyProxy::class )->get_instance_of( WC_Order_Item_Product::class );
		$item->set_props( $args );
		$item->set_backorder_meta();
		$item->set_order_id( $this->get_id() );
		$item->save();
		$this->add_item( $item );
		wc_do_deprecated_action( 'woocommerce_order_add_product', array( $this->get_id(), $item->get_id(), $product, $qty, $args ), '3.0', 'woocommerce_new_order_item action instead' );
		delete_transient( 'wc_order_' . $this->get_id() . '_needs_processing' );
		return $item->get_id();
	}

	/*
	|--------------------------------------------------------------------------
	| Payment Token Handling
	|--------------------------------------------------------------------------
	|
	| Payment tokens are hashes used to take payments by certain gateways.
	|
	*/

	/**
	 * Add a payment token to an order
	 *
	 * @since 2.6
	 * @param WC_Payment_Token $token Payment token object.
	 * @return boolean|int The new token ID or false if it failed.
	 */
	public function add_payment_token( $token ) {
		if ( empty( $token ) || ! ( $token instanceof WC_Payment_Token ) ) {
			return false;
		}

		$token_ids   = $this->data_store->get_payment_token_ids( $this );
		$token_ids[] = $token->get_id();
		$this->data_store->update_payment_token_ids( $this, $token_ids );

		do_action( 'woocommerce_payment_token_added_to_order', $this->get_id(), $token->get_id(), $token, $token_ids );
		return $token->get_id();
	}

	/**
	 * Returns a list of all payment tokens associated with the current order
	 *
	 * @since 2.6
	 * @return array An array of payment token objects
	 */
	public function get_payment_tokens() {
		return $this->data_store->get_payment_token_ids( $this );
	}

	/*
	|--------------------------------------------------------------------------
	| Calculations.
	|--------------------------------------------------------------------------
	|
	| These methods calculate order totals and taxes based on the current data.
	|
	*/

	/**
	 * Calculate shipping total.
	 *
	 * @since 2.2
	 * @return float
	 */
	public function calculate_shipping() {
		$shipping_total = 0;

		foreach ( $this->get_shipping_methods() as $shipping ) {
			$shipping_total += $shipping->get_total();
		}

		$this->set_shipping_total( $shipping_total );
		$this->save();

		return $this->get_shipping_total();
	}

	/**
	 * Get all tax classes for items in the order.
	 *
	 * @since 2.6.3
	 * @return array
	 */
	public function get_items_tax_classes() {
		$found_tax_classes = array();

		foreach ( $this->get_items() as $item ) {
			if ( is_callable( array( $item, 'get_tax_status' ) ) && in_array( $item->get_tax_status(), array( 'taxable', 'shipping' ), true ) ) {
				$found_tax_classes[] = $item->get_tax_class();
			}
		}

		return array_unique( $found_tax_classes );
	}

	/**
	 * Get tax location for this order.
	 *
	 * @since 3.2.0
	 * @param array $args array Override the location.
	 * @return array
	 */
	protected function get_tax_location( $args = array() ) {
		$tax_based_on = get_option( 'woocommerce_tax_based_on' );

		if ( 'shipping' === $tax_based_on && ! $this->get_shipping_country() ) {
			$tax_based_on = 'billing';
		}

		$args = wp_parse_args(
			$args,
			array(
				'country'  => 'billing' === $tax_based_on ? $this->get_billing_country() : $this->get_shipping_country(),
				'state'    => 'billing' === $tax_based_on ? $this->get_billing_state() : $this->get_shipping_state(),
				'postcode' => 'billing' === $tax_based_on ? $this->get_billing_postcode() : $this->get_shipping_postcode(),
				'city'     => 'billing' === $tax_based_on ? $this->get_billing_city() : $this->get_shipping_city(),
			)
		);

		// Default to base.
		if ( 'base' === $tax_based_on || empty( $args['country'] ) ) {
			$args['country']  = WC()->countries->get_base_country();
			$args['state']    = WC()->countries->get_base_state();
			$args['postcode'] = WC()->countries->get_base_postcode();
			$args['city']     = WC()->countries->get_base_city();
		}

		return apply_filters( 'woocommerce_order_get_tax_location', $args, $this );
	}

	/**
	 * Get tax rates for an order. Use order's shipping or billing address, defaults to base location.
	 *
	 * @param string $tax_class     Tax class to get rates for.
	 * @param array  $location_args Location to compute rates for. Should be in form: array( country, state, postcode, city).
	 * @param object $customer      Only used to maintain backward compatibility for filter `woocommerce-matched_rates`.
	 *
	 * @return mixed|void Tax rates.
	 */
	protected function get_tax_rates( $tax_class, $location_args = array(), $customer = null ) {
		$tax_location = $this->get_tax_location( $location_args );
		$tax_location = array( $tax_location['country'], $tax_location['state'], $tax_location['postcode'], $tax_location['city'] );
		return WC_Tax::get_rates_from_location( $tax_class, $tax_location, $customer );
	}

	/**
	 * Calculate taxes for all line items and shipping, and store the totals and tax rows.
	 *
	 * If by default the taxes are based on the shipping address and the current order doesn't
	 * have any, it would use the billing address rather than using the Shopping base location.
	 *
	 * Will use the base country unless customer addresses are set.
	 *
	 * @param array $args Added in 3.0.0 to pass things like location.
	 */
	public function calculate_taxes( $args = array() ) {
		do_action( 'woocommerce_order_before_calculate_taxes', $args, $this );

		$calculate_tax_for  = $this->get_tax_location( $args );
		$shipping_tax_class = get_option( 'woocommerce_shipping_tax_class' );

		if ( 'inherit' === $shipping_tax_class ) {
			$found_classes      = array_intersect( array_merge( array( '' ), WC_Tax::get_tax_class_slugs() ), $this->get_items_tax_classes() );
			$shipping_tax_class = count( $found_classes ) ? current( $found_classes ) : false;
		}

		$is_vat_exempt = apply_filters( 'woocommerce_order_is_vat_exempt', 'yes' === $this->get_meta( 'is_vat_exempt' ), $this );

		// Trigger tax recalculation for all items.
		foreach ( $this->get_items( array( 'line_item', 'fee' ) ) as $item_id => $item ) {
			if ( ! $is_vat_exempt ) {
				$item->calculate_taxes( $calculate_tax_for );
			} else {
				$item->set_taxes( false );
			}
		}

		foreach ( $this->get_shipping_methods() as $item_id => $item ) {
			if ( false !== $shipping_tax_class && ! $is_vat_exempt ) {
				$item->calculate_taxes( array_merge( $calculate_tax_for, array( 'tax_class' => $shipping_tax_class ) ) );
			} else {
				$item->set_taxes( false );
			}
		}

		$this->update_taxes();
	}

	/**
	 * Calculate fees for all line items.
	 *
	 * @return float Fee total.
	 */
	public function get_total_fees() {
		return array_reduce(
			$this->get_fees(),
			function( $carry, $item ) {
				return $carry + $item->get_total();
			}
		);
	}

	/**
	 * Update tax lines for the order based on the line item taxes themselves.
	 */
	public function update_taxes() {
		$cart_taxes     = array();
		$shipping_taxes = array();
		$existing_taxes = $this->get_taxes();
		$saved_rate_ids = array();

		foreach ( $this->get_items( array( 'line_item', 'fee' ) ) as $item_id => $item ) {
			$taxes = $item->get_taxes();
			foreach ( $taxes['total'] as $tax_rate_id => $tax ) {
				$tax_amount = (float) $this->round_line_tax( $tax, false );

				$cart_taxes[ $tax_rate_id ] = isset( $cart_taxes[ $tax_rate_id ] ) ? (float) $cart_taxes[ $tax_rate_id ] + $tax_amount : $tax_amount;
			}
		}

		foreach ( $this->get_shipping_methods() as $item_id => $item ) {
			$taxes = $item->get_taxes();
			foreach ( $taxes['total'] as $tax_rate_id => $tax ) {
				$tax_amount = (float) $tax;

				if ( 'yes' !== get_option( 'woocommerce_tax_round_at_subtotal' ) ) {
					$tax_amount = wc_round_tax_total( $tax_amount );
				}

				$shipping_taxes[ $tax_rate_id ] = isset( $shipping_taxes[ $tax_rate_id ] ) ? $shipping_taxes[ $tax_rate_id ] + $tax_amount : $tax_amount;
			}
		}

		foreach ( $existing_taxes as $tax ) {
			// Remove taxes which no longer exist for cart/shipping.
			if ( ( ! array_key_exists( $tax->get_rate_id(), $cart_taxes ) && ! array_key_exists( $tax->get_rate_id(), $shipping_taxes ) ) || in_array( $tax->get_rate_id(), $saved_rate_ids, true ) ) {
				$this->remove_item( $tax->get_id() );
				continue;
			}
			$saved_rate_ids[] = $tax->get_rate_id();
			$tax->set_rate( $tax->get_rate_id() );
			$tax->set_tax_total( isset( $cart_taxes[ $tax->get_rate_id() ] ) ? $cart_taxes[ $tax->get_rate_id() ] : 0 );
			$tax->set_label( WC_Tax::get_rate_label( $tax->get_rate_id() ) );
			$tax->set_shipping_tax_total( ! empty( $shipping_taxes[ $tax->get_rate_id() ] ) ? $shipping_taxes[ $tax->get_rate_id() ] : 0 );
			$tax->save();
		}

		$new_rate_ids = wp_parse_id_list( array_diff( array_keys( $cart_taxes + $shipping_taxes ), $saved_rate_ids ) );

		// New taxes.
		foreach ( $new_rate_ids as $tax_rate_id ) {
			$item = new WC_Order_Item_Tax();
			$item->set_rate( $tax_rate_id );
			$item->set_tax_total( isset( $cart_taxes[ $tax_rate_id ] ) ? $cart_taxes[ $tax_rate_id ] : 0 );
			$item->set_shipping_tax_total( ! empty( $shipping_taxes[ $tax_rate_id ] ) ? $shipping_taxes[ $tax_rate_id ] : 0 );
			$this->add_item( $item );
		}

		$this->set_shipping_tax( array_sum( $shipping_taxes ) );
		$this->set_cart_tax( array_sum( $cart_taxes ) );
		$this->save();
	}

	/**
	 * Helper function.
	 * If you add all items in this order in cart again, this would be the cart subtotal (assuming all other settings are same).
	 *
	 * @return float Cart subtotal.
	 */
	protected function get_cart_subtotal_for_order() {
		return wc_remove_number_precision(
			$this->get_rounded_items_total(
				$this->get_values_for_total( 'subtotal' )
			)
		);
	}

	/**
	 * Helper function.
	 * If you add all items in this order in cart again, this would be the cart total (assuming all other settings are same).
	 *
	 * @return float Cart total.
	 */
	protected function get_cart_total_for_order() {
		return wc_remove_number_precision(
			$this->get_rounded_items_total(
				$this->get_values_for_total( 'total' )
			)
		);
	}

	/**
	 * Calculate totals by looking at the contents of the order. Stores the totals and returns the orders final total.
	 *
	 * @since 2.2
	 * @param  bool $and_taxes Calc taxes if true.
	 * @return float calculated grand total.
	 */
	public function calculate_totals( $and_taxes = true ) {
		do_action( 'woocommerce_order_before_calculate_totals', $and_taxes, $this );

		$fees_total        = 0;
		$shipping_total    = 0;
		$cart_subtotal_tax = 0;
		$cart_total_tax    = 0;

		$cart_subtotal = $this->get_cart_subtotal_for_order();
		$cart_total    = $this->get_cart_total_for_order();

		// Sum shipping costs.
		foreach ( $this->get_shipping_methods() as $shipping ) {
			$shipping_total += NumberUtil::round( $shipping->get_total(), wc_get_price_decimals() );
		}

		$this->set_shipping_total( $shipping_total );

		// Sum fee costs.
		foreach ( $this->get_fees() as $item ) {
			$fee_total = $item->get_total();

			if ( 0 > $fee_total ) {
				$max_discount = NumberUtil::round( $cart_total + $fees_total + $shipping_total, wc_get_price_decimals() ) * -1;

				if ( $fee_total < $max_discount && 0 > $max_discount ) {
					$item->set_total( $max_discount );
				}
			}
			$fees_total += $item->get_total();
		}

		// Calculate taxes for items, shipping, discounts. Note; this also triggers save().
		if ( $and_taxes ) {
			$this->calculate_taxes();
		}

		// Sum taxes again so we can work out how much tax was discounted. This uses original values, not those possibly rounded to 2dp.
		foreach ( $this->get_items() as $item ) {
			$taxes = $item->get_taxes();

			foreach ( $taxes['total'] as $tax_rate_id => $tax ) {
				$cart_total_tax += (float) $tax;
			}

			foreach ( $taxes['subtotal'] as $tax_rate_id => $tax ) {
				$cart_subtotal_tax += (float) $tax;
			}
		}

		$this->set_discount_total( NumberUtil::round( $cart_subtotal - $cart_total, wc_get_price_decimals() ) );
		$this->set_discount_tax( wc_round_tax_total( $cart_subtotal_tax - $cart_total_tax ) );
		$this->set_total( NumberUtil::round( $cart_total + $fees_total + $this->get_shipping_total() + $this->get_cart_tax() + $this->get_shipping_tax(), wc_get_price_decimals() ) );

		do_action( 'woocommerce_order_after_calculate_totals', $and_taxes, $this );

		$this->save();

		return $this->get_total();
	}

	/**
	 * Get item subtotal - this is the cost before discount.
	 *
	 * @param object $item Item to get total from.
	 * @param bool   $inc_tax (default: false).
	 * @param bool   $round (default: true).
	 * @return float
	 */
	public function get_item_subtotal( $item, $inc_tax = false, $round = true ) {
		$subtotal = 0;

		if ( is_callable( array( $item, 'get_subtotal' ) ) && $item->get_quantity() ) {
			if ( $inc_tax ) {
				$subtotal = ( $item->get_subtotal() + $item->get_subtotal_tax() ) / $item->get_quantity();
			} else {
				$subtotal = floatval( $item->get_subtotal() ) / $item->get_quantity();
			}

			$subtotal = $round ? number_format( (float) $subtotal, wc_get_price_decimals(), '.', '' ) : $subtotal;
		}

		return apply_filters( 'woocommerce_order_amount_item_subtotal', $subtotal, $this, $item, $inc_tax, $round );
	}

	/**
	 * Get line subtotal - this is the cost before discount.
	 *
	 * @param object $item Item to get total from.
	 * @param bool   $inc_tax (default: false).
	 * @param bool   $round (default: true).
	 * @return float
	 */
	public function get_line_subtotal( $item, $inc_tax = false, $round = true ) {
		$subtotal = 0;

		if ( is_callable( array( $item, 'get_subtotal' ) ) ) {
			if ( $inc_tax ) {
				$subtotal = $item->get_subtotal() + $item->get_subtotal_tax();
			} else {
				$subtotal = $item->get_subtotal();
			}

			$subtotal = $round ? NumberUtil::round( $subtotal, wc_get_price_decimals() ) : $subtotal;
		}

		return apply_filters( 'woocommerce_order_amount_line_subtotal', $subtotal, $this, $item, $inc_tax, $round );
	}

	/**
	 * Calculate item cost - useful for gateways.
	 *
	 * @param object $item Item to get total from.
	 * @param bool   $inc_tax (default: false).
	 * @param bool   $round (default: true).
	 * @return float
	 */
	public function get_item_total( $item, $inc_tax = false, $round = true ) {
		$total = 0;

		if ( is_callable( array( $item, 'get_total' ) ) && $item->get_quantity() ) {
			if ( $inc_tax ) {
				$total = ( $item->get_total() + $item->get_total_tax() ) / $item->get_quantity();
			} else {
				$total = floatval( $item->get_total() ) / $item->get_quantity();
			}

			$total = $round ? NumberUtil::round( $total, wc_get_price_decimals() ) : $total;
		}

		return apply_filters( 'woocommerce_order_amount_item_total', $total, $this, $item, $inc_tax, $round );
	}

	/**
	 * Calculate line total - useful for gateways.
	 *
	 * @param object $item Item to get total from.
	 * @param bool   $inc_tax (default: false).
	 * @param bool   $round (default: true).
	 * @return float
	 */
	public function get_line_total( $item, $inc_tax = false, $round = true ) {
		$total = 0;

		if ( is_callable( array( $item, 'get_total' ) ) ) {
			// Check if we need to add line tax to the line total.
			$total = $inc_tax ? $item->get_total() + $item->get_total_tax() : $item->get_total();

			// Check if we need to round.
			$total = $round ? NumberUtil::round( $total, wc_get_price_decimals() ) : $total;
		}

		return apply_filters( 'woocommerce_order_amount_line_total', $total, $this, $item, $inc_tax, $round );
	}

	/**
	 * Get item tax - useful for gateways.
	 *
	 * @param mixed $item Item to get total from.
	 * @param bool  $round (default: true).
	 * @return float
	 */
	public function get_item_tax( $item, $round = true ) {
		$tax = 0;

		if ( is_callable( array( $item, 'get_total_tax' ) ) && $item->get_quantity() ) {
			$tax = $item->get_total_tax() / $item->get_quantity();
			$tax = $round ? wc_round_tax_total( $tax ) : $tax;
		}

		return apply_filters( 'woocommerce_order_amount_item_tax', $tax, $item, $round, $this );
	}

	/**
	 * Get line tax - useful for gateways.
	 *
	 * @param mixed $item Item to get total from.
	 * @return float
	 */
	public function get_line_tax( $item ) {
		return apply_filters( 'woocommerce_order_amount_line_tax', is_callable( array( $item, 'get_total_tax' ) ) ? wc_round_tax_total( $item->get_total_tax() ) : 0, $item, $this );
	}

	/**
	 * Gets line subtotal - formatted for display.
	 *
	 * @param object $item Item to get total from.
	 * @param string $tax_display Incl or excl tax display mode.
	 * @return string
	 */
	public function get_formatted_line_subtotal( $item, $tax_display = '' ) {
		$tax_display = $tax_display ? $tax_display : get_option( 'woocommerce_tax_display_cart' );

		if ( 'excl' === $tax_display ) {
			$ex_tax_label = $this->get_prices_include_tax() ? 1 : 0;

			$subtotal = wc_price(
				$this->get_line_subtotal( $item ),
				array(
					'ex_tax_label' => $ex_tax_label,
					'currency'     => $this->get_currency(),
				)
			);
		} else {
			$subtotal = wc_price( $this->get_line_subtotal( $item, true ), array( 'currency' => $this->get_currency() ) );
		}

		return apply_filters( 'woocommerce_order_formatted_line_subtotal', $subtotal, $item, $this );
	}

	/**
	 * Gets order total - formatted for display.
	 *
	 * @return string
	 */
	public function get_formatted_order_total() {
		$formatted_total = wc_price( $this->get_total(), array( 'currency' => $this->get_currency() ) );
		return apply_filters( 'woocommerce_get_formatted_order_total', $formatted_total, $this );
	}

	/**
	 * Gets subtotal - subtotal is shown before discounts, but with localised taxes.
	 *
	 * @param bool   $compound (default: false).
	 * @param string $tax_display (default: the tax_display_cart value).
	 * @return string
	 */
	public function get_subtotal_to_display( $compound = false, $tax_display = '' ) {
		$tax_display = $tax_display ? $tax_display : get_option( 'woocommerce_tax_display_cart' );
		$subtotal    = $this->get_cart_subtotal_for_order();

		if ( ! $compound ) {

			if ( 'incl' === $tax_display ) {
				$subtotal_taxes = 0;
				foreach ( $this->get_items() as $item ) {
					$subtotal_taxes += self::round_line_tax( $item->get_subtotal_tax(), false );
				}
				$subtotal += wc_round_tax_total( $subtotal_taxes );
			}

			$subtotal = wc_price( $subtotal, array( 'currency' => $this->get_currency() ) );

			if ( 'excl' === $tax_display && $this->get_prices_include_tax() && wc_tax_enabled() ) {
				$subtotal .= ' <small class="tax_label">' . WC()->countries->ex_tax_or_vat() . '</small>';
			}
		} else {
			if ( 'incl' === $tax_display ) {
				return '';
			}

			// Add Shipping Costs.
			$subtotal += $this->get_shipping_total();

			// Remove non-compound taxes.
			foreach ( $this->get_taxes() as $tax ) {
				if ( $tax->is_compound() ) {
					continue;
				}
				$subtotal = $subtotal + $tax->get_tax_total() + $tax->get_shipping_tax_total();
			}

			// Remove discounts.
			$subtotal = $subtotal - $this->get_total_discount();
			$subtotal = wc_price( $subtotal, array( 'currency' => $this->get_currency() ) );
		}

		return apply_filters( 'woocommerce_order_subtotal_to_display', $subtotal, $compound, $this );
	}

	/**
	 * Gets shipping (formatted).
	 *
	 * @param string $tax_display Excl or incl tax display mode.
	 * @return string
	 */
	public function get_shipping_to_display( $tax_display = '' ) {
		$tax_display = $tax_display ? $tax_display : get_option( 'woocommerce_tax_display_cart' );

		if ( 0 < abs( (float) $this->get_shipping_total() ) ) {

			if ( 'excl' === $tax_display ) {

				// Show shipping excluding tax.
				$shipping = wc_price( $this->get_shipping_total(), array( 'currency' => $this->get_currency() ) );

				if ( (float) $this->get_shipping_tax() > 0 && $this->get_prices_include_tax() ) {
					$shipping .= apply_filters( 'woocommerce_order_shipping_to_display_tax_label', '&nbsp;<small class="tax_label">' . WC()->countries->ex_tax_or_vat() . '</small>', $this, $tax_display );
				}
			} else {

				// Show shipping including tax.
				$shipping = wc_price( $this->get_shipping_total() + $this->get_shipping_tax(), array( 'currency' => $this->get_currency() ) );

				if ( (float) $this->get_shipping_tax() > 0 && ! $this->get_prices_include_tax() ) {
					$shipping .= apply_filters( 'woocommerce_order_shipping_to_display_tax_label', '&nbsp;<small class="tax_label">' . WC()->countries->inc_tax_or_vat() . '</small>', $this, $tax_display );
				}
			}

			/* translators: %s: method */
			$shipping .= apply_filters( 'woocommerce_order_shipping_to_display_shipped_via', '&nbsp;<small class="shipped_via">' . sprintf( __( 'via %s', 'woocommerce' ), $this->get_shipping_method() ) . '</small>', $this );

		} elseif ( $this->get_shipping_method() ) {
			$shipping = $this->get_shipping_method();
		} else {
			$shipping = __( 'Free!', 'woocommerce' );
		}

		return apply_filters( 'woocommerce_order_shipping_to_display', $shipping, $this, $tax_display );
	}

	/**
	 * Get the discount amount (formatted).
	 *
	 * @since  2.3.0
	 * @param string $tax_display Excl or incl tax display mode.
	 * @return string
	 */
	public function get_discount_to_display( $tax_display = '' ) {
		$tax_display = $tax_display ? $tax_display : get_option( 'woocommerce_tax_display_cart' );
		return apply_filters( 'woocommerce_order_discount_to_display', wc_price( $this->get_total_discount( 'excl' === $tax_display && 'excl' === get_option( 'woocommerce_tax_display_cart' ) ), array( 'currency' => $this->get_currency() ) ), $this );
	}

	/**
	 * Add total row for subtotal.
	 *
	 * @param array  $total_rows Reference to total rows array.
	 * @param string $tax_display Excl or incl tax display mode.
	 */
	protected function add_order_item_totals_subtotal_row( &$total_rows, $tax_display ) {
		$subtotal = $this->get_subtotal_to_display( false, $tax_display );

		if ( $subtotal ) {
			$total_rows['cart_subtotal'] = array(
				'label' => __( 'Subtotal:', 'woocommerce' ),
				'value' => $subtotal,
			);
		}
	}

	/**
	 * Add total row for discounts.
	 *
	 * @param array  $total_rows Reference to total rows array.
	 * @param string $tax_display Excl or incl tax display mode.
	 */
	protected function add_order_item_totals_discount_row( &$total_rows, $tax_display ) {
		if ( $this->get_total_discount() > 0 ) {
			$total_rows['discount'] = array(
				'label' => __( 'Discount:', 'woocommerce' ),
				'value' => '-' . $this->get_discount_to_display( $tax_display ),
			);
		}
	}

	/**
	 * Add total row for shipping.
	 *
	 * @param array  $total_rows Reference to total rows array.
	 * @param string $tax_display Excl or incl tax display mode.
	 */
	protected function add_order_item_totals_shipping_row( &$total_rows, $tax_display ) {
		if ( $this->get_shipping_method() ) {
			$total_rows['shipping'] = array(
				'label' => __( 'Shipping:', 'woocommerce' ),
				'value' => $this->get_shipping_to_display( $tax_display ),
			);
		}
	}

	/**
	 * Add total row for fees.
	 *
	 * @param array  $total_rows Reference to total rows array.
	 * @param string $tax_display Excl or incl tax display mode.
	 */
	protected function add_order_item_totals_fee_rows( &$total_rows, $tax_display ) {
		$fees = $this->get_fees();

		if ( $fees ) {
			foreach ( $fees as $id => $fee ) {
				if ( apply_filters( 'woocommerce_get_order_item_totals_excl_free_fees', empty( $fee['line_total'] ) && empty( $fee['line_tax'] ), $id ) ) {
					continue;
				}
				$total_rows[ 'fee_' . $fee->get_id() ] = array(
					'label' => $fee->get_name() . ':',
					'value' => wc_price( 'excl' === $tax_display ? $fee->get_total() : $fee->get_total() + $fee->get_total_tax(), array( 'currency' => $this->get_currency() ) ),
				);
			}
		}
	}

	/**
	 * Add total row for taxes.
	 *
	 * @param array  $total_rows Reference to total rows array.
	 * @param string $tax_display Excl or incl tax display mode.
	 */
	protected function add_order_item_totals_tax_rows( &$total_rows, $tax_display ) {
		// Tax for tax exclusive prices.
		if ( 'excl' === $tax_display && wc_tax_enabled() ) {
			if ( 'itemized' === get_option( 'woocommerce_tax_total_display' ) ) {
				foreach ( $this->get_tax_totals() as $code => $tax ) {
					$total_rows[ sanitize_title( $code ) ] = array(
						'label' => $tax->label . ':',
						'value' => $tax->formatted_amount,
					);
				}
			} else {
				$total_rows['tax'] = array(
					'label' => WC()->countries->tax_or_vat() . ':',
					'value' => wc_price( $this->get_total_tax(), array( 'currency' => $this->get_currency() ) ),
				);
			}
		}
	}

	/**
	 * Add total row for grand total.
	 *
	 * @param array  $total_rows Reference to total rows array.
	 * @param string $tax_display Excl or incl tax display mode.
	 */
	protected function add_order_item_totals_total_row( &$total_rows, $tax_display ) {
		$total_rows['order_total'] = array(
			'label' => __( 'Total:', 'woocommerce' ),
			'value' => $this->get_formatted_order_total( $tax_display ),
		);
	}

	/**
	 * Get totals for display on pages and in emails.
	 *
	 * @param mixed $tax_display Excl or incl tax display mode.
	 * @return array
	 */
	public function get_order_item_totals( $tax_display = '' ) {
		$tax_display = $tax_display ? $tax_display : get_option( 'woocommerce_tax_display_cart' );
		$total_rows  = array();

		$this->add_order_item_totals_subtotal_row( $total_rows, $tax_display );
		$this->add_order_item_totals_discount_row( $total_rows, $tax_display );
		$this->add_order_item_totals_shipping_row( $total_rows, $tax_display );
		$this->add_order_item_totals_fee_rows( $total_rows, $tax_display );
		$this->add_order_item_totals_tax_rows( $total_rows, $tax_display );
		$this->add_order_item_totals_total_row( $total_rows, $tax_display );

		return apply_filters( 'woocommerce_get_order_item_totals', $total_rows, $this, $tax_display );
	}

	/*
	|--------------------------------------------------------------------------
	| Conditionals
	|--------------------------------------------------------------------------
	|
	| Checks if a condition is true or false.
	|
	*/

	/**
	 * Checks the order status against a passed in status.
	 *
	 * @param array|string $status Status to check.
	 * @return bool
	 */
	public function has_status( $status ) {
		return apply_filters( 'woocommerce_order_has_status', ( is_array( $status ) && in_array( $this->get_status(), $status, true ) ) || $this->get_status() === $status, $this, $status );
	}

	/**
	 * Check whether this order has a specific shipping method or not.
	 *
	 * @param string $method_id Method ID to check.
	 * @return bool
	 */
	public function has_shipping_method( $method_id ) {
		foreach ( $this->get_shipping_methods() as $shipping_method ) {
			if ( strpos( $shipping_method->get_method_id(), $method_id ) === 0 ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Returns true if the order contains a free product.
	 *
	 * @since 2.5.0
	 * @return bool
	 */
	public function has_free_item() {
		foreach ( $this->get_items() as $item ) {
			if ( ! $item->get_total() ) {
				return true;
			}
		}
		return false;
	}
}
abstracts/abstract-wc-shipping-method.php000064400000037504151336044760014574 0ustar00<?php
/**
 * Abstract shipping method
 *
 * @class WC_Shipping_Method
 * @package WooCommerce\Abstracts
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WooCommerce Shipping Method Class.
 *
 * Extended by shipping methods to handle shipping calculations etc.
 *
 * @class       WC_Shipping_Method
 * @version     3.0.0
 * @package     WooCommerce\Abstracts
 */
abstract class WC_Shipping_Method extends WC_Settings_API {

	/**
	 * Features this method supports. Possible features used by core:
	 * - shipping-zones Shipping zone functionality + instances
	 * - instance-settings Instance settings screens.
	 * - settings Non-instance settings screens. Enabled by default for BW compatibility with methods before instances existed.
	 * - instance-settings-modal Allows the instance settings to be loaded within a modal in the zones UI.
	 *
	 * @var array
	 */
	public $supports = array( 'settings' );

	/**
	 * Unique ID for the shipping method - must be set.
	 *
	 * @var string
	 */
	public $id = '';

	/**
	 * Method title.
	 *
	 * @var string
	 */
	public $method_title = '';

	/**
	 * Method description.
	 *
	 * @var string
	 */
	public $method_description = '';

	/**
	 * Yes or no based on whether the method is enabled.
	 *
	 * @var string
	 */
	public $enabled = 'yes';

	/**
	 * Shipping method title for the frontend.
	 *
	 * @var string
	 */
	public $title;

	/**
	 * This is an array of rates - methods must populate this array to register shipping costs.
	 *
	 * @var array
	 */
	public $rates = array();

	/**
	 * If 'taxable' tax will be charged for this method (if applicable).
	 *
	 * @var string
	 */
	public $tax_status = 'taxable';

	/**
	 * Fee for the method (if applicable).
	 *
	 * @var string
	 */
	public $fee = null;

	/**
	 * Minimum fee for the method (if applicable).
	 *
	 * @var string
	 */
	public $minimum_fee = null;

	/**
	 * Instance ID if used.
	 *
	 * @var int
	 */
	public $instance_id = 0;

	/**
	 * Instance form fields.
	 *
	 * @var array
	 */
	public $instance_form_fields = array();

	/**
	 * Instance settings.
	 *
	 * @var array
	 */
	public $instance_settings = array();

	/**
	 * Availability - legacy. Used for method Availability.
	 * No longer useful for instance based shipping methods.
	 *
	 * @deprecated 2.6.0
	 * @var string
	 */
	public $availability;

	/**
	 * Availability countries - legacy. Used for method Availability.
	 * No longer useful for instance based shipping methods.
	 *
	 * @deprecated 2.6.0
	 * @var array
	 */
	public $countries = array();

	/**
	 * Constructor.
	 *
	 * @param int $instance_id Instance ID.
	 */
	public function __construct( $instance_id = 0 ) {
		$this->instance_id = absint( $instance_id );
	}

	/**
	 * Check if a shipping method supports a given feature.
	 *
	 * Methods should override this to declare support (or lack of support) for a feature.
	 *
	 * @param string $feature The name of a feature to test support for.
	 * @return bool True if the shipping method supports the feature, false otherwise.
	 */
	public function supports( $feature ) {
		return apply_filters( 'woocommerce_shipping_method_supports', in_array( $feature, $this->supports ), $feature, $this );
	}

	/**
	 * Called to calculate shipping rates for this method. Rates can be added using the add_rate() method.
	 *
	 * @param array $package Package array.
	 */
	public function calculate_shipping( $package = array() ) {}

	/**
	 * Whether or not we need to calculate tax on top of the shipping rate.
	 *
	 * @return boolean
	 */
	public function is_taxable() {
		return wc_tax_enabled() && 'taxable' === $this->tax_status && ( WC()->customer && ! WC()->customer->get_is_vat_exempt() );
	}

	/**
	 * Whether or not this method is enabled in settings.
	 *
	 * @since 2.6.0
	 * @return boolean
	 */
	public function is_enabled() {
		return 'yes' === $this->enabled;
	}

	/**
	 * Return the shipping method instance ID.
	 *
	 * @since 2.6.0
	 * @return int
	 */
	public function get_instance_id() {
		return $this->instance_id;
	}

	/**
	 * Return the shipping method title.
	 *
	 * @since 2.6.0
	 * @return string
	 */
	public function get_method_title() {
		return apply_filters( 'woocommerce_shipping_method_title', $this->method_title, $this );
	}

	/**
	 * Return the shipping method description.
	 *
	 * @since 2.6.0
	 * @return string
	 */
	public function get_method_description() {
		return apply_filters( 'woocommerce_shipping_method_description', $this->method_description, $this );
	}

	/**
	 * Return the shipping title which is user set.
	 *
	 * @return string
	 */
	public function get_title() {
		return apply_filters( 'woocommerce_shipping_method_title', $this->title, $this->id );
	}

	/**
	 * Return calculated rates for a package.
	 *
	 * @since 2.6.0
	 * @param array $package Package array.
	 * @return array
	 */
	public function get_rates_for_package( $package ) {
		$this->rates = array();
		if ( $this->is_available( $package ) && ( empty( $package['ship_via'] ) || in_array( $this->id, $package['ship_via'] ) ) ) {
			$this->calculate_shipping( $package );
		}
		return $this->rates;
	}

	/**
	 * Returns a rate ID based on this methods ID and instance, with an optional
	 * suffix if distinguishing between multiple rates.
	 *
	 * @since 2.6.0
	 * @param string $suffix Suffix.
	 * @return string
	 */
	public function get_rate_id( $suffix = '' ) {
		$rate_id = array( $this->id );

		if ( $this->instance_id ) {
			$rate_id[] = $this->instance_id;
		}

		if ( $suffix ) {
			$rate_id[] = $suffix;
		}

		return implode( ':', $rate_id );
	}

	/**
	 * Add a shipping rate. If taxes are not set they will be calculated based on cost.
	 *
	 * @param array $args Arguments (default: array()).
	 */
	public function add_rate( $args = array() ) {
		$args = apply_filters(
			'woocommerce_shipping_method_add_rate_args',
			wp_parse_args(
				$args,
				array(
					'id'             => $this->get_rate_id(), // ID for the rate. If not passed, this id:instance default will be used.
					'label'          => '', // Label for the rate.
					'cost'           => '0', // Amount or array of costs (per item shipping).
					'taxes'          => '', // Pass taxes, or leave empty to have it calculated for you, or 'false' to disable calculations.
					'calc_tax'       => 'per_order', // Calc tax per_order or per_item. Per item needs an array of costs.
					'meta_data'      => array(), // Array of misc meta data to store along with this rate - key value pairs.
					'package'        => false, // Package array this rate was generated for @since 2.6.0.
					'price_decimals' => wc_get_price_decimals(),
				)
			),
			$this
		);

		// ID and label are required.
		if ( ! $args['id'] || ! $args['label'] ) {
			return;
		}

		// Total up the cost.
		$total_cost = is_array( $args['cost'] ) ? array_sum( $args['cost'] ) : $args['cost'];
		$taxes      = $args['taxes'];

		// Taxes - if not an array and not set to false, calc tax based on cost and passed calc_tax variable. This saves shipping methods having to do complex tax calculations.
		if ( ! is_array( $taxes ) && false !== $taxes && $total_cost > 0 && $this->is_taxable() ) {
			$taxes = 'per_item' === $args['calc_tax'] ? $this->get_taxes_per_item( $args['cost'] ) : WC_Tax::calc_shipping_tax( $total_cost, WC_Tax::get_shipping_tax_rates() );
		}

		// Round the total cost after taxes have been calculated.
		$total_cost = wc_format_decimal( $total_cost, $args['price_decimals'] );

		// Create rate object.
		$rate = new WC_Shipping_Rate();
		$rate->set_id( $args['id'] );
		$rate->set_method_id( $this->id );
		$rate->set_instance_id( $this->instance_id );
		$rate->set_label( $args['label'] );
		$rate->set_cost( $total_cost );
		$rate->set_taxes( $taxes );

		if ( ! empty( $args['meta_data'] ) ) {
			foreach ( $args['meta_data'] as $key => $value ) {
				$rate->add_meta_data( $key, $value );
			}
		}

		// Store package data.
		if ( $args['package'] ) {
			$items_in_package = array();
			foreach ( $args['package']['contents'] as $item ) {
				$product            = $item['data'];
				$items_in_package[] = $product->get_name() . ' &times; ' . $item['quantity'];
			}
			$rate->add_meta_data( __( 'Items', 'woocommerce' ), implode( ', ', $items_in_package ) );
		}

		$this->rates[ $args['id'] ] = apply_filters( 'woocommerce_shipping_method_add_rate', $rate, $args, $this );
	}

	/**
	 * Calc taxes per item being shipping in costs array.
	 *
	 * @since 2.6.0
	 * @param  array $costs Costs.
	 * @return array of taxes
	 */
	protected function get_taxes_per_item( $costs ) {
		$taxes = array();

		// If we have an array of costs we can look up each items tax class and add tax accordingly.
		if ( is_array( $costs ) ) {

			$cart = WC()->cart->get_cart();

			foreach ( $costs as $cost_key => $amount ) {
				if ( ! isset( $cart[ $cost_key ] ) ) {
					continue;
				}

				$item_taxes = WC_Tax::calc_shipping_tax( $amount, WC_Tax::get_shipping_tax_rates( $cart[ $cost_key ]['data']->get_tax_class() ) );

				// Sum the item taxes.
				foreach ( array_keys( $taxes + $item_taxes ) as $key ) {
					$taxes[ $key ] = ( isset( $item_taxes[ $key ] ) ? $item_taxes[ $key ] : 0 ) + ( isset( $taxes[ $key ] ) ? $taxes[ $key ] : 0 );
				}
			}

			// Add any cost for the order - order costs are in the key 'order'.
			if ( isset( $costs['order'] ) ) {
				$item_taxes = WC_Tax::calc_shipping_tax( $costs['order'], WC_Tax::get_shipping_tax_rates() );

				// Sum the item taxes.
				foreach ( array_keys( $taxes + $item_taxes ) as $key ) {
					$taxes[ $key ] = ( isset( $item_taxes[ $key ] ) ? $item_taxes[ $key ] : 0 ) + ( isset( $taxes[ $key ] ) ? $taxes[ $key ] : 0 );
				}
			}
		}

		return $taxes;
	}

	/**
	 * Is this method available?
	 *
	 * @param array $package Package.
	 * @return bool
	 */
	public function is_available( $package ) {
		$available = $this->is_enabled();

		// Country availability (legacy, for non-zone based methods).
		if ( ! $this->instance_id && $available ) {
			$countries = is_array( $this->countries ) ? $this->countries : array();

			switch ( $this->availability ) {
				case 'specific':
				case 'including':
					$available = in_array( $package['destination']['country'], array_intersect( $countries, array_keys( WC()->countries->get_shipping_countries() ) ) );
					break;
				case 'excluding':
					$available = in_array( $package['destination']['country'], array_diff( array_keys( WC()->countries->get_shipping_countries() ), $countries ) );
					break;
				default:
					$available = in_array( $package['destination']['country'], array_keys( WC()->countries->get_shipping_countries() ) );
					break;
			}
		}

		return apply_filters( 'woocommerce_shipping_' . $this->id . '_is_available', $available, $package, $this );
	}

	/**
	 * Get fee to add to shipping cost.
	 *
	 * @param string|float $fee Fee.
	 * @param float        $total Total.
	 * @return float
	 */
	public function get_fee( $fee, $total ) {
		if ( strstr( $fee, '%' ) ) {
			$fee = ( $total / 100 ) * str_replace( '%', '', $fee );
		}
		if ( ! empty( $this->minimum_fee ) && $this->minimum_fee > $fee ) {
			$fee = $this->minimum_fee;
		}
		return $fee;
	}

	/**
	 * Does this method have a settings page?
	 *
	 * @return bool
	 */
	public function has_settings() {
		return $this->instance_id ? $this->supports( 'instance-settings' ) : $this->supports( 'settings' );
	}

	/**
	 * Return admin options as a html string.
	 *
	 * @return string
	 */
	public function get_admin_options_html() {
		if ( $this->instance_id ) {
			$settings_html = $this->generate_settings_html( $this->get_instance_form_fields(), false );
		} else {
			$settings_html = $this->generate_settings_html( $this->get_form_fields(), false );
		}

		return '<table class="form-table">' . $settings_html . '</table>';
	}

	/**
	 * Output the shipping settings screen.
	 */
	public function admin_options() {
		if ( ! $this->instance_id ) {
			echo '<h2>' . esc_html( $this->get_method_title() ) . '</h2>';
		}
		echo wp_kses_post( wpautop( $this->get_method_description() ) );
		echo $this->get_admin_options_html(); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped
	}

	/**
	 * Get_option function.
	 *
	 * Gets and option from the settings API, using defaults if necessary to prevent undefined notices.
	 *
	 * @param  string $key Key.
	 * @param  mixed  $empty_value Empty value.
	 * @return mixed  The value specified for the option or a default value for the option.
	 */
	public function get_option( $key, $empty_value = null ) {
		// Instance options take priority over global options.
		if ( $this->instance_id && array_key_exists( $key, $this->get_instance_form_fields() ) ) {
			return $this->get_instance_option( $key, $empty_value );
		}

		// Return global option.
		$option = apply_filters( 'woocommerce_shipping_' . $this->id . '_option', parent::get_option( $key, $empty_value ), $key, $this );
		return $option;
	}

	/**
	 * Gets an option from the settings API, using defaults if necessary to prevent undefined notices.
	 *
	 * @param  string $key Key.
	 * @param  mixed  $empty_value Empty value.
	 * @return mixed  The value specified for the option or a default value for the option.
	 */
	public function get_instance_option( $key, $empty_value = null ) {
		if ( empty( $this->instance_settings ) ) {
			$this->init_instance_settings();
		}

		// Get option default if unset.
		if ( ! isset( $this->instance_settings[ $key ] ) ) {
			$form_fields                     = $this->get_instance_form_fields();
			$this->instance_settings[ $key ] = $this->get_field_default( $form_fields[ $key ] );
		}

		if ( ! is_null( $empty_value ) && '' === $this->instance_settings[ $key ] ) {
			$this->instance_settings[ $key ] = $empty_value;
		}

		$instance_option = apply_filters( 'woocommerce_shipping_' . $this->id . '_instance_option', $this->instance_settings[ $key ], $key, $this );
		return $instance_option;
	}

	/**
	 * Get settings fields for instances of this shipping method (within zones).
	 * Should be overridden by shipping methods to add options.
	 *
	 * @since 2.6.0
	 * @return array
	 */
	public function get_instance_form_fields() {
		return apply_filters( 'woocommerce_shipping_instance_form_fields_' . $this->id, array_map( array( $this, 'set_defaults' ), $this->instance_form_fields ) );
	}

	/**
	 * Return the name of the option in the WP DB.
	 *
	 * @since 2.6.0
	 * @return string
	 */
	public function get_instance_option_key() {
		return $this->instance_id ? $this->plugin_id . $this->id . '_' . $this->instance_id . '_settings' : '';
	}

	/**
	 * Initialise Settings for instances.
	 *
	 * @since 2.6.0
	 */
	public function init_instance_settings() {
		$this->instance_settings = get_option( $this->get_instance_option_key(), null );

		// If there are no settings defined, use defaults.
		if ( ! is_array( $this->instance_settings ) ) {
			$form_fields             = $this->get_instance_form_fields();
			$this->instance_settings = array_merge( array_fill_keys( array_keys( $form_fields ), '' ), wp_list_pluck( $form_fields, 'default' ) );
		}
	}

	/**
	 * Processes and saves global shipping method options in the admin area.
	 *
	 * This method is usually attached to woocommerce_update_options_x hooks.
	 *
	 * @since 2.6.0
	 * @return bool was anything saved?
	 */
	public function process_admin_options() {
		if ( ! $this->instance_id ) {
			return parent::process_admin_options();
		}

		// Check we are processing the correct form for this instance.
		if ( ! isset( $_REQUEST['instance_id'] ) || absint( $_REQUEST['instance_id'] ) !== $this->instance_id ) { // WPCS: input var ok, CSRF ok.
			return false;
		}

		$this->init_instance_settings();

		$post_data = $this->get_post_data();

		foreach ( $this->get_instance_form_fields() as $key => $field ) {
			if ( 'title' !== $this->get_field_type( $field ) ) {
				try {
					$this->instance_settings[ $key ] = $this->get_field_value( $key, $field, $post_data );
				} catch ( Exception $e ) {
					$this->add_error( $e->getMessage() );
				}
			}
		}

		return update_option( $this->get_instance_option_key(), apply_filters( 'woocommerce_shipping_' . $this->id . '_instance_settings_values', $this->instance_settings, $this ), 'yes' );
	}
}
abstracts/abstract-wc-payment-gateway.php000064400000033542151336044760014607 0ustar00<?php
/**
 * Abstract payment gateway
 *
 * Hanldes generic payment gateway functionality which is extended by idividual payment gateways.
 *
 * @class WC_Payment_Gateway
 * @version 2.1.0
 * @package WooCommerce\Abstracts
 */

use Automattic\Jetpack\Constants;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WooCommerce Payment Gateway class.
 *
 * Extended by individual payment gateways to handle payments.
 *
 * @class       WC_Payment_Gateway
 * @extends     WC_Settings_API
 * @version     2.1.0
 * @package     WooCommerce\Abstracts
 */
abstract class WC_Payment_Gateway extends WC_Settings_API {

	/**
	 * Set if the place order button should be renamed on selection.
	 *
	 * @var string
	 */
	public $order_button_text;

	/**
	 * Yes or no based on whether the method is enabled.
	 *
	 * @var string
	 */
	public $enabled = 'yes';

	/**
	 * Payment method title for the frontend.
	 *
	 * @var string
	 */
	public $title;

	/**
	 * Payment method description for the frontend.
	 *
	 * @var string
	 */
	public $description;

	/**
	 * Chosen payment method id.
	 *
	 * @var bool
	 */
	public $chosen;

	/**
	 * Gateway title.
	 *
	 * @var string
	 */
	public $method_title = '';

	/**
	 * Gateway description.
	 *
	 * @var string
	 */
	public $method_description = '';

	/**
	 * True if the gateway shows fields on the checkout.
	 *
	 * @var bool
	 */
	public $has_fields;

	/**
	 * Countries this gateway is allowed for.
	 *
	 * @var array
	 */
	public $countries;

	/**
	 * Available for all counties or specific.
	 *
	 * @var string
	 */
	public $availability;

	/**
	 * Icon for the gateway.
	 *
	 * @var string
	 */
	public $icon;

	/**
	 * Supported features such as 'default_credit_card_form', 'refunds'.
	 *
	 * @var array
	 */
	public $supports = array( 'products' );

	/**
	 * Maximum transaction amount, zero does not define a maximum.
	 *
	 * @var int
	 */
	public $max_amount = 0;

	/**
	 * Optional URL to view a transaction.
	 *
	 * @var string
	 */
	public $view_transaction_url = '';

	/**
	 * Optional label to show for "new payment method" in the payment
	 * method/token selection radio selection.
	 *
	 * @var string
	 */
	public $new_method_label = '';

	/**
	 * Pay button ID if supported.
	 *
	 * @var string
	 */
	public $pay_button_id = '';

	/**
	 * Contains a users saved tokens for this gateway.
	 *
	 * @var array
	 */
	protected $tokens = array();

	/**
	 * Returns a users saved tokens for this gateway.
	 *
	 * @since 2.6.0
	 * @return array
	 */
	public function get_tokens() {
		if ( count( $this->tokens ) > 0 ) {
			return $this->tokens;
		}

		if ( is_user_logged_in() && $this->supports( 'tokenization' ) ) {
			$this->tokens = WC_Payment_Tokens::get_customer_tokens( get_current_user_id(), $this->id );
		}

		return $this->tokens;
	}

	/**
	 * Return the title for admin screens.
	 *
	 * @return string
	 */
	public function get_method_title() {
		return apply_filters( 'woocommerce_gateway_method_title', $this->method_title, $this );
	}

	/**
	 * Return the description for admin screens.
	 *
	 * @return string
	 */
	public function get_method_description() {
		return apply_filters( 'woocommerce_gateway_method_description', $this->method_description, $this );
	}

	/**
	 * Output the gateway settings screen.
	 */
	public function admin_options() {
		echo '<h2>' . esc_html( $this->get_method_title() );
		wc_back_link( __( 'Return to payments', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=checkout' ) );
		echo '</h2>';
		echo wp_kses_post( wpautop( $this->get_method_description() ) );
		parent::admin_options();
	}

	/**
	 * Init settings for gateways.
	 */
	public function init_settings() {
		parent::init_settings();
		$this->enabled = ! empty( $this->settings['enabled'] ) && 'yes' === $this->settings['enabled'] ? 'yes' : 'no';
	}

	/**
	 * Return whether or not this gateway still requires setup to function.
	 *
	 * When this gateway is toggled on via AJAX, if this returns true a
	 * redirect will occur to the settings page instead.
	 *
	 * @since 3.4.0
	 * @return bool
	 */
	public function needs_setup() {
		return false;
	}

	/**
	 * Get the return url (thank you page).
	 *
	 * @param WC_Order|null $order Order object.
	 * @return string
	 */
	public function get_return_url( $order = null ) {
		if ( $order ) {
			$return_url = $order->get_checkout_order_received_url();
		} else {
			$return_url = wc_get_endpoint_url( 'order-received', '', wc_get_checkout_url() );
		}

		return apply_filters( 'woocommerce_get_return_url', $return_url, $order );
	}

	/**
	 * Get a link to the transaction on the 3rd party gateway site (if applicable).
	 *
	 * @param  WC_Order $order the order object.
	 * @return string transaction URL, or empty string.
	 */
	public function get_transaction_url( $order ) {

		$return_url     = '';
		$transaction_id = $order->get_transaction_id();

		if ( ! empty( $this->view_transaction_url ) && ! empty( $transaction_id ) ) {
			$return_url = sprintf( $this->view_transaction_url, $transaction_id );
		}

		return apply_filters( 'woocommerce_get_transaction_url', $return_url, $order, $this );
	}

	/**
	 * Get the order total in checkout and pay_for_order.
	 *
	 * @return float
	 */
	protected function get_order_total() {

		$total    = 0;
		$order_id = absint( get_query_var( 'order-pay' ) );

		// Gets order total from "pay for order" page.
		if ( 0 < $order_id ) {
			$order = wc_get_order( $order_id );
			if ( $order ) {
				$total = (float) $order->get_total();
			}

			// Gets order total from cart/checkout.
		} elseif ( 0 < WC()->cart->total ) {
			$total = (float) WC()->cart->total;
		}

		return $total;
	}

	/**
	 * Check if the gateway is available for use.
	 *
	 * @return bool
	 */
	public function is_available() {
		$is_available = ( 'yes' === $this->enabled );

		if ( WC()->cart && 0 < $this->get_order_total() && 0 < $this->max_amount && $this->max_amount < $this->get_order_total() ) {
			$is_available = false;
		}

		return $is_available;
	}

	/**
	 * Check if the gateway has fields on the checkout.
	 *
	 * @return bool
	 */
	public function has_fields() {
		return (bool) $this->has_fields;
	}

	/**
	 * Return the gateway's title.
	 *
	 * @return string
	 */
	public function get_title() {
		return apply_filters( 'woocommerce_gateway_title', $this->title, $this->id );
	}

	/**
	 * Return the gateway's description.
	 *
	 * @return string
	 */
	public function get_description() {
		return apply_filters( 'woocommerce_gateway_description', $this->description, $this->id );
	}

	/**
	 * Return the gateway's icon.
	 *
	 * @return string
	 */
	public function get_icon() {

		$icon = $this->icon ? '<img src="' . WC_HTTPS::force_https_url( $this->icon ) . '" alt="' . esc_attr( $this->get_title() ) . '" />' : '';

		return apply_filters( 'woocommerce_gateway_icon', $icon, $this->id );
	}

	/**
	 * Return the gateway's pay button ID.
	 *
	 * @since 3.9.0
	 * @return string
	 */
	public function get_pay_button_id() {
		return sanitize_html_class( $this->pay_button_id );
	}

	/**
	 * Set as current gateway.
	 *
	 * Set this as the current gateway.
	 */
	public function set_current() {
		$this->chosen = true;
	}

	/**
	 * Process Payment.
	 *
	 * Process the payment. Override this in your gateway. When implemented, this should.
	 * return the success and redirect in an array. e.g:
	 *
	 *        return array(
	 *            'result'   => 'success',
	 *            'redirect' => $this->get_return_url( $order )
	 *        );
	 *
	 * @param int $order_id Order ID.
	 * @return array
	 */
	public function process_payment( $order_id ) {
		return array();
	}

	/**
	 * Process refund.
	 *
	 * If the gateway declares 'refunds' support, this will allow it to refund.
	 * a passed in amount.
	 *
	 * @param  int        $order_id Order ID.
	 * @param  float|null $amount Refund amount.
	 * @param  string     $reason Refund reason.
	 * @return boolean True or false based on success, or a WP_Error object.
	 */
	public function process_refund( $order_id, $amount = null, $reason = '' ) {
		return false;
	}

	/**
	 * Validate frontend fields.
	 *
	 * Validate payment fields on the frontend.
	 *
	 * @return bool
	 */
	public function validate_fields() {
		return true;
	}

	/**
	 * If There are no payment fields show the description if set.
	 * Override this in your gateway if you have some.
	 */
	public function payment_fields() {
		$description = $this->get_description();
		if ( $description ) {
			echo wpautop( wptexturize( $description ) ); // @codingStandardsIgnoreLine.
		}

		if ( $this->supports( 'default_credit_card_form' ) ) {
			$this->credit_card_form(); // Deprecated, will be removed in a future version.
		}
	}

	/**
	 * Check if a gateway supports a given feature.
	 *
	 * Gateways should override this to declare support (or lack of support) for a feature.
	 * For backward compatibility, gateways support 'products' by default, but nothing else.
	 *
	 * @param string $feature string The name of a feature to test support for.
	 * @return bool True if the gateway supports the feature, false otherwise.
	 * @since 1.5.7
	 */
	public function supports( $feature ) {
		return apply_filters( 'woocommerce_payment_gateway_supports', in_array( $feature, $this->supports ), $feature, $this );
	}

	/**
	 * Can the order be refunded via this gateway?
	 *
	 * Should be extended by gateways to do their own checks.
	 *
	 * @param  WC_Order $order Order object.
	 * @return bool If false, the automatic refund button is hidden in the UI.
	 */
	public function can_refund_order( $order ) {
		return $order && $this->supports( 'refunds' );
	}

	/**
	 * Core credit card form which gateways can use if needed. Deprecated - inherit WC_Payment_Gateway_CC instead.
	 *
	 * @param  array $args Arguments.
	 * @param  array $fields Fields.
	 */
	public function credit_card_form( $args = array(), $fields = array() ) {
		wc_deprecated_function( 'credit_card_form', '2.6', 'WC_Payment_Gateway_CC->form' );
		$cc_form           = new WC_Payment_Gateway_CC();
		$cc_form->id       = $this->id;
		$cc_form->supports = $this->supports;
		$cc_form->form();
	}

	/**
	 * Enqueues our tokenization script to handle some of the new form options.
	 *
	 * @since 2.6.0
	 */
	public function tokenization_script() {
		wp_enqueue_script(
			'woocommerce-tokenization-form',
			plugins_url( '/assets/js/frontend/tokenization-form' . ( Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min' ) . '.js', WC_PLUGIN_FILE ),
			array( 'jquery' ),
			WC()->version
		);

		wp_localize_script(
			'woocommerce-tokenization-form',
			'wc_tokenization_form_params',
			array(
				'is_registration_required' => WC()->checkout()->is_registration_required(),
				'is_logged_in'             => is_user_logged_in(),
			)
		);
	}

	/**
	 * Grab and display our saved payment methods.
	 *
	 * @since 2.6.0
	 */
	public function saved_payment_methods() {
		$html = '<ul class="woocommerce-SavedPaymentMethods wc-saved-payment-methods" data-count="' . esc_attr( count( $this->get_tokens() ) ) . '">';

		foreach ( $this->get_tokens() as $token ) {
			$html .= $this->get_saved_payment_method_option_html( $token );
		}

		$html .= $this->get_new_payment_method_option_html();
		$html .= '</ul>';

		echo apply_filters( 'wc_payment_gateway_form_saved_payment_methods_html', $html, $this ); // @codingStandardsIgnoreLine
	}

	/**
	 * Gets saved payment method HTML from a token.
	 *
	 * @since 2.6.0
	 * @param  WC_Payment_Token $token Payment Token.
	 * @return string Generated payment method HTML
	 */
	public function get_saved_payment_method_option_html( $token ) {
		$html = sprintf(
			'<li class="woocommerce-SavedPaymentMethods-token">
				<input id="wc-%1$s-payment-token-%2$s" type="radio" name="wc-%1$s-payment-token" value="%2$s" style="width:auto;" class="woocommerce-SavedPaymentMethods-tokenInput" %4$s />
				<label for="wc-%1$s-payment-token-%2$s">%3$s</label>
			</li>',
			esc_attr( $this->id ),
			esc_attr( $token->get_id() ),
			esc_html( $token->get_display_name() ),
			checked( $token->is_default(), true, false )
		);

		return apply_filters( 'woocommerce_payment_gateway_get_saved_payment_method_option_html', $html, $token, $this );
	}

	/**
	 * Displays a radio button for entering a new payment method (new CC details) instead of using a saved method.
	 * Only displayed when a gateway supports tokenization.
	 *
	 * @since 2.6.0
	 */
	public function get_new_payment_method_option_html() {
		$label = apply_filters( 'woocommerce_payment_gateway_get_new_payment_method_option_html_label', $this->new_method_label ? $this->new_method_label : __( 'Use a new payment method', 'woocommerce' ), $this );
		$html  = sprintf(
			'<li class="woocommerce-SavedPaymentMethods-new">
				<input id="wc-%1$s-payment-token-new" type="radio" name="wc-%1$s-payment-token" value="new" style="width:auto;" class="woocommerce-SavedPaymentMethods-tokenInput" />
				<label for="wc-%1$s-payment-token-new">%2$s</label>
			</li>',
			esc_attr( $this->id ),
			esc_html( $label )
		);

		return apply_filters( 'woocommerce_payment_gateway_get_new_payment_method_option_html', $html, $this );
	}

	/**
	 * Outputs a checkbox for saving a new payment method to the database.
	 *
	 * @since 2.6.0
	 */
	public function save_payment_method_checkbox() {
		$html = sprintf(
			'<p class="form-row woocommerce-SavedPaymentMethods-saveNew">
				<input id="wc-%1$s-new-payment-method" name="wc-%1$s-new-payment-method" type="checkbox" value="true" style="width:auto;" />
				<label for="wc-%1$s-new-payment-method" style="display:inline;">%2$s</label>
			</p>',
			esc_attr( $this->id ),
			esc_html__( 'Save to account', 'woocommerce' )
		);

		echo apply_filters( 'woocommerce_payment_gateway_save_new_payment_method_option_html', $html, $this ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
	}

	/**
	 * Add payment method via account screen. This should be extended by gateway plugins.
	 *
	 * @since 3.2.0 Included here from 3.2.0, but supported from 3.0.0.
	 * @return array
	 */
	public function add_payment_method() {
		return array(
			'result'   => 'failure',
			'redirect' => wc_get_endpoint_url( 'payment-methods' ),
		);
	}
}
abstracts/abstract-wc-widget.php000064400000030263151336044760012753 0ustar00<?php
/**
 * Abstract widget class
 *
 * @class WC_Widget
 * @package  WooCommerce\Abstracts
 */

use Automattic\Jetpack\Constants;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC_Widget
 *
 * @package  WooCommerce\Abstracts
 * @version  2.5.0
 * @extends  WP_Widget
 */
abstract class WC_Widget extends WP_Widget {

	/**
	 * CSS class.
	 *
	 * @var string
	 */
	public $widget_cssclass;

	/**
	 * Widget description.
	 *
	 * @var string
	 */
	public $widget_description;

	/**
	 * Widget ID.
	 *
	 * @var string
	 */
	public $widget_id;

	/**
	 * Widget name.
	 *
	 * @var string
	 */
	public $widget_name;

	/**
	 * Settings.
	 *
	 * @var array
	 */
	public $settings;

	/**
	 * Constructor.
	 */
	public function __construct() {
		$widget_ops = array(
			'classname'                   => $this->widget_cssclass,
			'description'                 => $this->widget_description,
			'customize_selective_refresh' => true,
			'show_instance_in_rest'       => true,
		);

		parent::__construct( $this->widget_id, $this->widget_name, $widget_ops );

		add_action( 'save_post', array( $this, 'flush_widget_cache' ) );
		add_action( 'deleted_post', array( $this, 'flush_widget_cache' ) );
		add_action( 'switch_theme', array( $this, 'flush_widget_cache' ) );
	}

	/**
	 * Get cached widget.
	 *
	 * @param  array $args Arguments.
	 * @return bool true if the widget is cached otherwise false
	 */
	public function get_cached_widget( $args ) {
		// Don't get cache if widget_id doesn't exists.
		if ( empty( $args['widget_id'] ) ) {
			return false;
		}

		$cache = wp_cache_get( $this->get_widget_id_for_cache( $this->widget_id ), 'widget' );

		if ( ! is_array( $cache ) ) {
			$cache = array();
		}

		if ( isset( $cache[ $this->get_widget_id_for_cache( $args['widget_id'] ) ] ) ) {
			echo $cache[ $this->get_widget_id_for_cache( $args['widget_id'] ) ]; // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped
			return true;
		}

		return false;
	}

	/**
	 * Cache the widget.
	 *
	 * @param  array  $args Arguments.
	 * @param  string $content Content.
	 * @return string the content that was cached
	 */
	public function cache_widget( $args, $content ) {
		// Don't set any cache if widget_id doesn't exist.
		if ( empty( $args['widget_id'] ) ) {
			return $content;
		}

		$cache = wp_cache_get( $this->get_widget_id_for_cache( $this->widget_id ), 'widget' );

		if ( ! is_array( $cache ) ) {
			$cache = array();
		}

		$cache[ $this->get_widget_id_for_cache( $args['widget_id'] ) ] = $content;

		wp_cache_set( $this->get_widget_id_for_cache( $this->widget_id ), $cache, 'widget' );

		return $content;
	}

	/**
	 * Flush the cache.
	 */
	public function flush_widget_cache() {
		foreach ( array( 'https', 'http' ) as $scheme ) {
			wp_cache_delete( $this->get_widget_id_for_cache( $this->widget_id, $scheme ), 'widget' );
		}
	}

	/**
	 * Get this widgets title.
	 *
	 * @param array $instance Array of instance options.
	 * @return string
	 */
	protected function get_instance_title( $instance ) {
		if ( isset( $instance['title'] ) ) {
			return $instance['title'];
		}

		if ( isset( $this->settings, $this->settings['title'], $this->settings['title']['std'] ) ) {
			return $this->settings['title']['std'];
		}

		return '';
	}

	/**
	 * Output the html at the start of a widget.
	 *
	 * @param array $args Arguments.
	 * @param array $instance Instance.
	 */
	public function widget_start( $args, $instance ) {
		echo $args['before_widget']; // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped

		$title = apply_filters( 'widget_title', $this->get_instance_title( $instance ), $instance, $this->id_base );

		if ( $title ) {
			echo $args['before_title'] . $title . $args['after_title']; // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped
		}
	}

	/**
	 * Output the html at the end of a widget.
	 *
	 * @param  array $args Arguments.
	 */
	public function widget_end( $args ) {
		echo $args['after_widget']; // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped
	}

	/**
	 * Updates a particular instance of a widget.
	 *
	 * @see    WP_Widget->update
	 * @param  array $new_instance New instance.
	 * @param  array $old_instance Old instance.
	 * @return array
	 */
	public function update( $new_instance, $old_instance ) {

		$instance = $old_instance;

		if ( empty( $this->settings ) ) {
			return $instance;
		}

		// Loop settings and get values to save.
		foreach ( $this->settings as $key => $setting ) {
			if ( ! isset( $setting['type'] ) ) {
				continue;
			}

			// Format the value based on settings type.
			switch ( $setting['type'] ) {
				case 'number':
					$instance[ $key ] = absint( $new_instance[ $key ] );

					if ( isset( $setting['min'] ) && '' !== $setting['min'] ) {
						$instance[ $key ] = max( $instance[ $key ], $setting['min'] );
					}

					if ( isset( $setting['max'] ) && '' !== $setting['max'] ) {
						$instance[ $key ] = min( $instance[ $key ], $setting['max'] );
					}
					break;
				case 'textarea':
					$instance[ $key ] = wp_kses( trim( wp_unslash( $new_instance[ $key ] ) ), wp_kses_allowed_html( 'post' ) );
					break;
				case 'checkbox':
					$instance[ $key ] = empty( $new_instance[ $key ] ) ? 0 : 1;
					break;
				default:
					$instance[ $key ] = isset( $new_instance[ $key ] ) ? sanitize_text_field( $new_instance[ $key ] ) : $setting['std'];
					break;
			}

			/**
			 * Sanitize the value of a setting.
			 */
			$instance[ $key ] = apply_filters( 'woocommerce_widget_settings_sanitize_option', $instance[ $key ], $new_instance, $key, $setting );
		}

		$this->flush_widget_cache();

		return $instance;
	}

	/**
	 * Outputs the settings update form.
	 *
	 * @see   WP_Widget->form
	 *
	 * @param array $instance Instance.
	 */
	public function form( $instance ) {

		if ( empty( $this->settings ) ) {
			return;
		}

		foreach ( $this->settings as $key => $setting ) {

			$class = isset( $setting['class'] ) ? $setting['class'] : '';
			$value = isset( $instance[ $key ] ) ? $instance[ $key ] : $setting['std'];

			switch ( $setting['type'] ) {

				case 'text':
					?>
					<p>
						<label for="<?php echo esc_attr( $this->get_field_id( $key ) ); ?>"><?php echo wp_kses_post( $setting['label'] ); ?></label><?php // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?>
						<input class="widefat <?php echo esc_attr( $class ); ?>" id="<?php echo esc_attr( $this->get_field_id( $key ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( $key ) ); ?>" type="text" value="<?php echo esc_attr( $value ); ?>" />
					</p>
					<?php
					break;

				case 'number':
					?>
					<p>
						<label for="<?php echo esc_attr( $this->get_field_id( $key ) ); ?>"><?php echo $setting['label']; /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></label>
						<input class="widefat <?php echo esc_attr( $class ); ?>" id="<?php echo esc_attr( $this->get_field_id( $key ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( $key ) ); ?>" type="number" step="<?php echo esc_attr( $setting['step'] ); ?>" min="<?php echo esc_attr( $setting['min'] ); ?>" max="<?php echo esc_attr( $setting['max'] ); ?>" value="<?php echo esc_attr( $value ); ?>" />
					</p>
					<?php
					break;

				case 'select':
					?>
					<p>
						<label for="<?php echo esc_attr( $this->get_field_id( $key ) ); ?>"><?php echo $setting['label']; /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></label>
						<select class="widefat <?php echo esc_attr( $class ); ?>" id="<?php echo esc_attr( $this->get_field_id( $key ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( $key ) ); ?>">
							<?php foreach ( $setting['options'] as $option_key => $option_value ) : ?>
								<option value="<?php echo esc_attr( $option_key ); ?>" <?php selected( $option_key, $value ); ?>><?php echo esc_html( $option_value ); ?></option>
							<?php endforeach; ?>
						</select>
					</p>
					<?php
					break;

				case 'textarea':
					?>
					<p>
						<label for="<?php echo esc_attr( $this->get_field_id( $key ) ); ?>"><?php echo $setting['label']; /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></label>
						<textarea class="widefat <?php echo esc_attr( $class ); ?>" id="<?php echo esc_attr( $this->get_field_id( $key ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( $key ) ); ?>" cols="20" rows="3"><?php echo esc_textarea( $value ); ?></textarea>
						<?php if ( isset( $setting['desc'] ) ) : ?>
							<small><?php echo esc_html( $setting['desc'] ); ?></small>
						<?php endif; ?>
					</p>
					<?php
					break;

				case 'checkbox':
					?>
					<p>
						<input class="checkbox <?php echo esc_attr( $class ); ?>" id="<?php echo esc_attr( $this->get_field_id( $key ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( $key ) ); ?>" type="checkbox" value="1" <?php checked( $value, 1 ); ?> />
						<label for="<?php echo esc_attr( $this->get_field_id( $key ) ); ?>"><?php echo $setting['label']; /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></label>
					</p>
					<?php
					break;

				// Default: run an action.
				default:
					do_action( 'woocommerce_widget_field_' . $setting['type'], $key, $value, $setting, $instance );
					break;
			}
		}
	}

	/**
	 * Get current page URL with various filtering props supported by WC.
	 *
	 * @return string
	 * @since  3.3.0
	 */
	protected function get_current_page_url() {
		if ( Constants::is_defined( 'SHOP_IS_ON_FRONT' ) ) {
			$link = home_url();
		} elseif ( is_shop() ) {
			$link = get_permalink( wc_get_page_id( 'shop' ) );
		} elseif ( is_product_category() ) {
			$link = get_term_link( get_query_var( 'product_cat' ), 'product_cat' );
		} elseif ( is_product_tag() ) {
			$link = get_term_link( get_query_var( 'product_tag' ), 'product_tag' );
		} else {
			$queried_object = get_queried_object();
			$link           = get_term_link( $queried_object->slug, $queried_object->taxonomy );
		}

		// Min/Max.
		if ( isset( $_GET['min_price'] ) ) {
			$link = add_query_arg( 'min_price', wc_clean( wp_unslash( $_GET['min_price'] ) ), $link );
		}

		if ( isset( $_GET['max_price'] ) ) {
			$link = add_query_arg( 'max_price', wc_clean( wp_unslash( $_GET['max_price'] ) ), $link );
		}

		// Order by.
		if ( isset( $_GET['orderby'] ) ) {
			$link = add_query_arg( 'orderby', wc_clean( wp_unslash( $_GET['orderby'] ) ), $link );
		}

		/**
		 * Search Arg.
		 * To support quote characters, first they are decoded from &quot; entities, then URL encoded.
		 */
		if ( get_search_query() ) {
			$link = add_query_arg( 's', rawurlencode( htmlspecialchars_decode( get_search_query() ) ), $link );
		}

		// Post Type Arg.
		if ( isset( $_GET['post_type'] ) ) {
			$link = add_query_arg( 'post_type', wc_clean( wp_unslash( $_GET['post_type'] ) ), $link );

			// Prevent post type and page id when pretty permalinks are disabled.
			if ( is_shop() ) {
				$link = remove_query_arg( 'page_id', $link );
			}
		}

		// Min Rating Arg.
		if ( isset( $_GET['rating_filter'] ) ) {
			$link = add_query_arg( 'rating_filter', wc_clean( wp_unslash( $_GET['rating_filter'] ) ), $link );
		}

		// All current filters.
		if ( $_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes() ) { // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure, WordPress.CodeAnalysis.AssignmentInCondition.Found
			foreach ( $_chosen_attributes as $name => $data ) {
				$filter_name = wc_attribute_taxonomy_slug( $name );
				if ( ! empty( $data['terms'] ) ) {
					$link = add_query_arg( 'filter_' . $filter_name, implode( ',', $data['terms'] ), $link );
				}
				if ( 'or' === $data['query_type'] ) {
					$link = add_query_arg( 'query_type_' . $filter_name, 'or', $link );
				}
			}
		}

		return apply_filters( 'woocommerce_widget_get_current_page_url', $link, $this );
	}

	/**
	 * Get widget id plus scheme/protocol to prevent serving mixed content from (persistently) cached widgets.
	 *
	 * @since  3.4.0
	 * @param  string $widget_id Id of the cached widget.
	 * @param  string $scheme    Scheme for the widget id.
	 * @return string            Widget id including scheme/protocol.
	 */
	protected function get_widget_id_for_cache( $widget_id, $scheme = '' ) {
		if ( $scheme ) {
			$widget_id_for_cache = $widget_id . '-' . $scheme;
		} else {
			$widget_id_for_cache = $widget_id . '-' . ( is_ssl() ? 'https' : 'http' );
		}

		return apply_filters( 'woocommerce_cached_widget_id', $widget_id_for_cache );
	}
}
abstracts/abstract-wc-settings-api.php000064400000072434151336044760014105 0ustar00<?php
/**
 * Abstract Settings API Class
 *
 * Admin Settings API used by Integrations, Shipping Methods, and Payment Gateways.
 *
 * @package  WooCommerce\Abstracts
 */

defined( 'ABSPATH' ) || exit;

/**
 * WC_Settings_API class.
 */
abstract class WC_Settings_API {

	/**
	 * The plugin ID. Used for option names.
	 *
	 * @var string
	 */
	public $plugin_id = 'woocommerce_';

	/**
	 * ID of the class extending the settings API. Used in option names.
	 *
	 * @var string
	 */
	public $id = '';

	/**
	 * Validation errors.
	 *
	 * @var array of strings
	 */
	public $errors = array();

	/**
	 * Setting values.
	 *
	 * @var array
	 */
	public $settings = array();

	/**
	 * Form option fields.
	 *
	 * @var array
	 */
	public $form_fields = array();

	/**
	 * The posted settings data. When empty, $_POST data will be used.
	 *
	 * @var array
	 */
	protected $data = array();

	/**
	 * Get the form fields after they are initialized.
	 *
	 * @return array of options
	 */
	public function get_form_fields() {
		return apply_filters( 'woocommerce_settings_api_form_fields_' . $this->id, array_map( array( $this, 'set_defaults' ), $this->form_fields ) );
	}

	/**
	 * Set default required properties for each field.
	 *
	 * @param array $field Setting field array.
	 * @return array
	 */
	protected function set_defaults( $field ) {
		if ( ! isset( $field['default'] ) ) {
			$field['default'] = '';
		}
		return $field;
	}

	/**
	 * Output the admin options table.
	 */
	public function admin_options() {
		echo '<table class="form-table">' . $this->generate_settings_html( $this->get_form_fields(), false ) . '</table>'; // WPCS: XSS ok.
	}

	/**
	 * Initialise settings form fields.
	 *
	 * Add an array of fields to be displayed on the gateway's settings screen.
	 *
	 * @since  1.0.0
	 */
	public function init_form_fields() {}

	/**
	 * Return the name of the option in the WP DB.
	 *
	 * @since 2.6.0
	 * @return string
	 */
	public function get_option_key() {
		return $this->plugin_id . $this->id . '_settings';
	}

	/**
	 * Get a fields type. Defaults to "text" if not set.
	 *
	 * @param  array $field Field key.
	 * @return string
	 */
	public function get_field_type( $field ) {
		return empty( $field['type'] ) ? 'text' : $field['type'];
	}

	/**
	 * Get a fields default value. Defaults to "" if not set.
	 *
	 * @param  array $field Field key.
	 * @return string
	 */
	public function get_field_default( $field ) {
		return empty( $field['default'] ) ? '' : $field['default'];
	}

	/**
	 * Get a field's posted and validated value.
	 *
	 * @param string $key Field key.
	 * @param array  $field Field array.
	 * @param array  $post_data Posted data.
	 * @return string
	 */
	public function get_field_value( $key, $field, $post_data = array() ) {
		$type      = $this->get_field_type( $field );
		$field_key = $this->get_field_key( $key );
		$post_data = empty( $post_data ) ? $_POST : $post_data; // WPCS: CSRF ok, input var ok.
		$value     = isset( $post_data[ $field_key ] ) ? $post_data[ $field_key ] : null;

		if ( isset( $field['sanitize_callback'] ) && is_callable( $field['sanitize_callback'] ) ) {
			return call_user_func( $field['sanitize_callback'], $value );
		}

		// Look for a validate_FIELDID_field method for special handling.
		if ( is_callable( array( $this, 'validate_' . $key . '_field' ) ) ) {
			return $this->{'validate_' . $key . '_field'}( $key, $value );
		}

		// Look for a validate_FIELDTYPE_field method.
		if ( is_callable( array( $this, 'validate_' . $type . '_field' ) ) ) {
			return $this->{'validate_' . $type . '_field'}( $key, $value );
		}

		// Fallback to text.
		return $this->validate_text_field( $key, $value );
	}

	/**
	 * Sets the POSTed data. This method can be used to set specific data, instead of taking it from the $_POST array.
	 *
	 * @param array $data Posted data.
	 */
	public function set_post_data( $data = array() ) {
		$this->data = $data;
	}

	/**
	 * Returns the POSTed data, to be used to save the settings.
	 *
	 * @return array
	 */
	public function get_post_data() {
		if ( ! empty( $this->data ) && is_array( $this->data ) ) {
			return $this->data;
		}
		return $_POST; // WPCS: CSRF ok, input var ok.
	}

	/**
	 * Update a single option.
	 *
	 * @since 3.4.0
	 * @param string $key Option key.
	 * @param mixed  $value Value to set.
	 * @return bool was anything saved?
	 */
	public function update_option( $key, $value = '' ) {
		if ( empty( $this->settings ) ) {
			$this->init_settings();
		}

		$this->settings[ $key ] = $value;

		return update_option( $this->get_option_key(), apply_filters( 'woocommerce_settings_api_sanitized_fields_' . $this->id, $this->settings ), 'yes' );
	}

	/**
	 * Processes and saves options.
	 * If there is an error thrown, will continue to save and validate fields, but will leave the erroring field out.
	 *
	 * @return bool was anything saved?
	 */
	public function process_admin_options() {
		$this->init_settings();

		$post_data = $this->get_post_data();

		foreach ( $this->get_form_fields() as $key => $field ) {
			if ( 'title' !== $this->get_field_type( $field ) ) {
				try {
					$this->settings[ $key ] = $this->get_field_value( $key, $field, $post_data );
				} catch ( Exception $e ) {
					$this->add_error( $e->getMessage() );
				}
			}
		}

		return update_option( $this->get_option_key(), apply_filters( 'woocommerce_settings_api_sanitized_fields_' . $this->id, $this->settings ), 'yes' );
	}

	/**
	 * Add an error message for display in admin on save.
	 *
	 * @param string $error Error message.
	 */
	public function add_error( $error ) {
		$this->errors[] = $error;
	}

	/**
	 * Get admin error messages.
	 */
	public function get_errors() {
		return $this->errors;
	}

	/**
	 * Display admin error messages.
	 */
	public function display_errors() {
		if ( $this->get_errors() ) {
			echo '<div id="woocommerce_errors" class="error notice is-dismissible">';
			foreach ( $this->get_errors() as $error ) {
				echo '<p>' . wp_kses_post( $error ) . '</p>';
			}
			echo '</div>';
		}
	}

	/**
	 * Initialise Settings.
	 *
	 * Store all settings in a single database entry
	 * and make sure the $settings array is either the default
	 * or the settings stored in the database.
	 *
	 * @since 1.0.0
	 * @uses get_option(), add_option()
	 */
	public function init_settings() {
		$this->settings = get_option( $this->get_option_key(), null );

		// If there are no settings defined, use defaults.
		if ( ! is_array( $this->settings ) ) {
			$form_fields    = $this->get_form_fields();
			$this->settings = array_merge( array_fill_keys( array_keys( $form_fields ), '' ), wp_list_pluck( $form_fields, 'default' ) );
		}
	}

	/**
	 * Get option from DB.
	 *
	 * Gets an option from the settings API, using defaults if necessary to prevent undefined notices.
	 *
	 * @param  string $key Option key.
	 * @param  mixed  $empty_value Value when empty.
	 * @return string The value specified for the option or a default value for the option.
	 */
	public function get_option( $key, $empty_value = null ) {
		if ( empty( $this->settings ) ) {
			$this->init_settings();
		}

		// Get option default if unset.
		if ( ! isset( $this->settings[ $key ] ) ) {
			$form_fields            = $this->get_form_fields();
			$this->settings[ $key ] = isset( $form_fields[ $key ] ) ? $this->get_field_default( $form_fields[ $key ] ) : '';
		}

		if ( ! is_null( $empty_value ) && '' === $this->settings[ $key ] ) {
			$this->settings[ $key ] = $empty_value;
		}

		return $this->settings[ $key ];
	}

	/**
	 * Prefix key for settings.
	 *
	 * @param  string $key Field key.
	 * @return string
	 */
	public function get_field_key( $key ) {
		return $this->plugin_id . $this->id . '_' . $key;
	}

	/**
	 * Generate Settings HTML.
	 *
	 * Generate the HTML for the fields on the "settings" screen.
	 *
	 * @param array $form_fields (default: array()) Array of form fields.
	 * @param bool  $echo Echo or return.
	 * @return string the html for the settings
	 * @since  1.0.0
	 * @uses   method_exists()
	 */
	public function generate_settings_html( $form_fields = array(), $echo = true ) {
		if ( empty( $form_fields ) ) {
			$form_fields = $this->get_form_fields();
		}

		$html = '';
		foreach ( $form_fields as $k => $v ) {
			$type = $this->get_field_type( $v );

			if ( method_exists( $this, 'generate_' . $type . '_html' ) ) {
				$html .= $this->{'generate_' . $type . '_html'}( $k, $v );
			} else {
				$html .= $this->generate_text_html( $k, $v );
			}
		}

		if ( $echo ) {
			echo $html; // WPCS: XSS ok.
		} else {
			return $html;
		}
	}

	/**
	 * Get HTML for tooltips.
	 *
	 * @param  array $data Data for the tooltip.
	 * @return string
	 */
	public function get_tooltip_html( $data ) {
		if ( true === $data['desc_tip'] ) {
			$tip = $data['description'];
		} elseif ( ! empty( $data['desc_tip'] ) ) {
			$tip = $data['desc_tip'];
		} else {
			$tip = '';
		}

		return $tip ? wc_help_tip( $tip, true ) : '';
	}

	/**
	 * Get HTML for descriptions.
	 *
	 * @param  array $data Data for the description.
	 * @return string
	 */
	public function get_description_html( $data ) {
		if ( true === $data['desc_tip'] ) {
			$description = '';
		} elseif ( ! empty( $data['desc_tip'] ) ) {
			$description = $data['description'];
		} elseif ( ! empty( $data['description'] ) ) {
			$description = $data['description'];
		} else {
			$description = '';
		}

		return $description ? '<p class="description">' . wp_kses_post( $description ) . '</p>' . "\n" : '';
	}

	/**
	 * Get custom attributes.
	 *
	 * @param  array $data Field data.
	 * @return string
	 */
	public function get_custom_attribute_html( $data ) {
		$custom_attributes = array();

		if ( ! empty( $data['custom_attributes'] ) && is_array( $data['custom_attributes'] ) ) {
			foreach ( $data['custom_attributes'] as $attribute => $attribute_value ) {
				$custom_attributes[] = esc_attr( $attribute ) . '="' . esc_attr( $attribute_value ) . '"';
			}
		}

		return implode( ' ', $custom_attributes );
	}

	/**
	 * Generate Text Input HTML.
	 *
	 * @param string $key Field key.
	 * @param array  $data Field data.
	 * @since  1.0.0
	 * @return string
	 */
	public function generate_text_html( $key, $data ) {
		$field_key = $this->get_field_key( $key );
		$defaults  = array(
			'title'             => '',
			'disabled'          => false,
			'class'             => '',
			'css'               => '',
			'placeholder'       => '',
			'type'              => 'text',
			'desc_tip'          => false,
			'description'       => '',
			'custom_attributes' => array(),
		);

		$data = wp_parse_args( $data, $defaults );

		ob_start();
		?>
		<tr valign="top">
			<th scope="row" class="titledesc">
				<label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // WPCS: XSS ok. ?></label>
			</th>
			<td class="forminp">
				<fieldset>
					<legend class="screen-reader-text"><span><?php echo wp_kses_post( $data['title'] ); ?></span></legend>
					<input class="input-text regular-input <?php echo esc_attr( $data['class'] ); ?>" type="<?php echo esc_attr( $data['type'] ); ?>" name="<?php echo esc_attr( $field_key ); ?>" id="<?php echo esc_attr( $field_key ); ?>" style="<?php echo esc_attr( $data['css'] ); ?>" value="<?php echo esc_attr( $this->get_option( $key ) ); ?>" placeholder="<?php echo esc_attr( $data['placeholder'] ); ?>" <?php disabled( $data['disabled'], true ); ?> <?php echo $this->get_custom_attribute_html( $data ); // WPCS: XSS ok. ?> />
					<?php echo $this->get_description_html( $data ); // WPCS: XSS ok. ?>
				</fieldset>
			</td>
		</tr>
		<?php

		return ob_get_clean();
	}

	/**
	 * Generate Price Input HTML.
	 *
	 * @param string $key Field key.
	 * @param array  $data Field data.
	 * @since  1.0.0
	 * @return string
	 */
	public function generate_price_html( $key, $data ) {
		$field_key = $this->get_field_key( $key );
		$defaults  = array(
			'title'             => '',
			'disabled'          => false,
			'class'             => '',
			'css'               => '',
			'placeholder'       => '',
			'type'              => 'text',
			'desc_tip'          => false,
			'description'       => '',
			'custom_attributes' => array(),
		);

		$data = wp_parse_args( $data, $defaults );

		ob_start();
		?>
		<tr valign="top">
			<th scope="row" class="titledesc">
				<label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // WPCS: XSS ok. ?></label>
			</th>
			<td class="forminp">
				<fieldset>
					<legend class="screen-reader-text"><span><?php echo wp_kses_post( $data['title'] ); ?></span></legend>
					<input class="wc_input_price input-text regular-input <?php echo esc_attr( $data['class'] ); ?>" type="text" name="<?php echo esc_attr( $field_key ); ?>" id="<?php echo esc_attr( $field_key ); ?>" style="<?php echo esc_attr( $data['css'] ); ?>" value="<?php echo esc_attr( wc_format_localized_price( $this->get_option( $key ) ) ); ?>" placeholder="<?php echo esc_attr( $data['placeholder'] ); ?>" <?php disabled( $data['disabled'], true ); ?> <?php echo $this->get_custom_attribute_html( $data ); // WPCS: XSS ok. ?> />
					<?php echo $this->get_description_html( $data ); // WPCS: XSS ok. ?>
				</fieldset>
			</td>
		</tr>
		<?php

		return ob_get_clean();
	}

	/**
	 * Generate Decimal Input HTML.
	 *
	 * @param string $key Field key.
	 * @param array  $data Field data.
	 * @since  1.0.0
	 * @return string
	 */
	public function generate_decimal_html( $key, $data ) {
		$field_key = $this->get_field_key( $key );
		$defaults  = array(
			'title'             => '',
			'disabled'          => false,
			'class'             => '',
			'css'               => '',
			'placeholder'       => '',
			'type'              => 'text',
			'desc_tip'          => false,
			'description'       => '',
			'custom_attributes' => array(),
		);

		$data = wp_parse_args( $data, $defaults );

		ob_start();
		?>
		<tr valign="top">
			<th scope="row" class="titledesc">
				<label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // WPCS: XSS ok. ?></label>
			</th>
			<td class="forminp">
				<fieldset>
					<legend class="screen-reader-text"><span><?php echo wp_kses_post( $data['title'] ); ?></span></legend>
					<input class="wc_input_decimal input-text regular-input <?php echo esc_attr( $data['class'] ); ?>" type="text" name="<?php echo esc_attr( $field_key ); ?>" id="<?php echo esc_attr( $field_key ); ?>" style="<?php echo esc_attr( $data['css'] ); ?>" value="<?php echo esc_attr( wc_format_localized_decimal( $this->get_option( $key ) ) ); ?>" placeholder="<?php echo esc_attr( $data['placeholder'] ); ?>" <?php disabled( $data['disabled'], true ); ?> <?php echo $this->get_custom_attribute_html( $data ); // WPCS: XSS ok. ?> />
					<?php echo $this->get_description_html( $data ); // WPCS: XSS ok. ?>
				</fieldset>
			</td>
		</tr>
		<?php

		return ob_get_clean();
	}

	/**
	 * Generate Password Input HTML.
	 *
	 * @param string $key Field key.
	 * @param array  $data Field data.
	 * @since  1.0.0
	 * @return string
	 */
	public function generate_password_html( $key, $data ) {
		$data['type'] = 'password';
		return $this->generate_text_html( $key, $data );
	}

	/**
	 * Generate Color Picker Input HTML.
	 *
	 * @param string $key Field key.
	 * @param array  $data Field data.
	 * @since  1.0.0
	 * @return string
	 */
	public function generate_color_html( $key, $data ) {
		$field_key = $this->get_field_key( $key );
		$defaults  = array(
			'title'             => '',
			'disabled'          => false,
			'class'             => '',
			'css'               => '',
			'placeholder'       => '',
			'desc_tip'          => false,
			'description'       => '',
			'custom_attributes' => array(),
		);

		$data = wp_parse_args( $data, $defaults );

		ob_start();
		?>
		<tr valign="top">
			<th scope="row" class="titledesc">
				<label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // WPCS: XSS ok. ?></label>
			</th>
			<td class="forminp">
				<fieldset>
					<legend class="screen-reader-text"><span><?php echo wp_kses_post( $data['title'] ); ?></span></legend>
					<span class="colorpickpreview" style="background:<?php echo esc_attr( $this->get_option( $key ) ); ?>;">&nbsp;</span>
					<input class="colorpick <?php echo esc_attr( $data['class'] ); ?>" type="text" name="<?php echo esc_attr( $field_key ); ?>" id="<?php echo esc_attr( $field_key ); ?>" style="<?php echo esc_attr( $data['css'] ); ?>" value="<?php echo esc_attr( $this->get_option( $key ) ); ?>" placeholder="<?php echo esc_attr( $data['placeholder'] ); ?>" <?php disabled( $data['disabled'], true ); ?> <?php echo $this->get_custom_attribute_html( $data ); // WPCS: XSS ok. ?> />
					<div id="colorPickerDiv_<?php echo esc_attr( $field_key ); ?>" class="colorpickdiv" style="z-index: 100; background: #eee; border: 1px solid #ccc; position: absolute; display: none;"></div>
					<?php echo $this->get_description_html( $data ); // WPCS: XSS ok. ?>
				</fieldset>
			</td>
		</tr>
		<?php

		return ob_get_clean();
	}

	/**
	 * Generate Textarea HTML.
	 *
	 * @param string $key Field key.
	 * @param array  $data Field data.
	 * @since  1.0.0
	 * @return string
	 */
	public function generate_textarea_html( $key, $data ) {
		$field_key = $this->get_field_key( $key );
		$defaults  = array(
			'title'             => '',
			'disabled'          => false,
			'class'             => '',
			'css'               => '',
			'placeholder'       => '',
			'type'              => 'text',
			'desc_tip'          => false,
			'description'       => '',
			'custom_attributes' => array(),
		);

		$data = wp_parse_args( $data, $defaults );

		ob_start();
		?>
		<tr valign="top">
			<th scope="row" class="titledesc">
				<label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // WPCS: XSS ok. ?></label>
			</th>
			<td class="forminp">
				<fieldset>
					<legend class="screen-reader-text"><span><?php echo wp_kses_post( $data['title'] ); ?></span></legend>
					<textarea rows="3" cols="20" class="input-text wide-input <?php echo esc_attr( $data['class'] ); ?>" type="<?php echo esc_attr( $data['type'] ); ?>" name="<?php echo esc_attr( $field_key ); ?>" id="<?php echo esc_attr( $field_key ); ?>" style="<?php echo esc_attr( $data['css'] ); ?>" placeholder="<?php echo esc_attr( $data['placeholder'] ); ?>" <?php disabled( $data['disabled'], true ); ?> <?php echo $this->get_custom_attribute_html( $data ); // WPCS: XSS ok. ?>><?php echo esc_textarea( $this->get_option( $key ) ); ?></textarea>
					<?php echo $this->get_description_html( $data ); // WPCS: XSS ok. ?>
				</fieldset>
			</td>
		</tr>
		<?php

		return ob_get_clean();
	}

	/**
	 * Generate Checkbox HTML.
	 *
	 * @param string $key Field key.
	 * @param array  $data Field data.
	 * @since  1.0.0
	 * @return string
	 */
	public function generate_checkbox_html( $key, $data ) {
		$field_key = $this->get_field_key( $key );
		$defaults  = array(
			'title'             => '',
			'label'             => '',
			'disabled'          => false,
			'class'             => '',
			'css'               => '',
			'type'              => 'text',
			'desc_tip'          => false,
			'description'       => '',
			'custom_attributes' => array(),
		);

		$data = wp_parse_args( $data, $defaults );

		if ( ! $data['label'] ) {
			$data['label'] = $data['title'];
		}

		ob_start();
		?>
		<tr valign="top">
			<th scope="row" class="titledesc">
				<label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // WPCS: XSS ok. ?></label>
			</th>
			<td class="forminp">
				<fieldset>
					<legend class="screen-reader-text"><span><?php echo wp_kses_post( $data['title'] ); ?></span></legend>
					<label for="<?php echo esc_attr( $field_key ); ?>">
					<input <?php disabled( $data['disabled'], true ); ?> class="<?php echo esc_attr( $data['class'] ); ?>" type="checkbox" name="<?php echo esc_attr( $field_key ); ?>" id="<?php echo esc_attr( $field_key ); ?>" style="<?php echo esc_attr( $data['css'] ); ?>" value="1" <?php checked( $this->get_option( $key ), 'yes' ); ?> <?php echo $this->get_custom_attribute_html( $data ); // WPCS: XSS ok. ?> /> <?php echo wp_kses_post( $data['label'] ); ?></label><br/>
					<?php echo $this->get_description_html( $data ); // WPCS: XSS ok. ?>
				</fieldset>
			</td>
		</tr>
		<?php

		return ob_get_clean();
	}

	/**
	 * Generate Select HTML.
	 *
	 * @param string $key Field key.
	 * @param array  $data Field data.
	 * @since  1.0.0
	 * @return string
	 */
	public function generate_select_html( $key, $data ) {
		$field_key = $this->get_field_key( $key );
		$defaults  = array(
			'title'             => '',
			'disabled'          => false,
			'class'             => '',
			'css'               => '',
			'placeholder'       => '',
			'type'              => 'text',
			'desc_tip'          => false,
			'description'       => '',
			'custom_attributes' => array(),
			'options'           => array(),
		);

		$data = wp_parse_args( $data, $defaults );
		$value = $this->get_option( $key );

		ob_start();
		?>
		<tr valign="top">
			<th scope="row" class="titledesc">
				<label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // WPCS: XSS ok. ?></label>
			</th>
			<td class="forminp">
				<fieldset>
					<legend class="screen-reader-text"><span><?php echo wp_kses_post( $data['title'] ); ?></span></legend>
					<select class="select <?php echo esc_attr( $data['class'] ); ?>" name="<?php echo esc_attr( $field_key ); ?>" id="<?php echo esc_attr( $field_key ); ?>" style="<?php echo esc_attr( $data['css'] ); ?>" <?php disabled( $data['disabled'], true ); ?> <?php echo $this->get_custom_attribute_html( $data ); // WPCS: XSS ok. ?>>
						<?php foreach ( (array) $data['options'] as $option_key => $option_value ) : ?>
							<?php if ( is_array( $option_value ) ) : ?>
								<optgroup label="<?php echo esc_attr( $option_key ); ?>">
									<?php foreach ( $option_value as $option_key_inner => $option_value_inner ) : ?>
										<option value="<?php echo esc_attr( $option_key_inner ); ?>" <?php selected( (string) $option_key_inner, esc_attr( $value ) ); ?>><?php echo esc_html( $option_value_inner ); ?></option>
									<?php endforeach; ?>
								</optgroup>
							<?php else : ?>
								<option value="<?php echo esc_attr( $option_key ); ?>" <?php selected( (string) $option_key, esc_attr( $value ) ); ?>><?php echo esc_html( $option_value ); ?></option>
							<?php endif; ?>
						<?php endforeach; ?>
					</select>
					<?php echo $this->get_description_html( $data ); // WPCS: XSS ok. ?>
				</fieldset>
			</td>
		</tr>
		<?php

		return ob_get_clean();
	}

	/**
	 * Generate Multiselect HTML.
	 *
	 * @param string $key Field key.
	 * @param array  $data Field data.
	 * @since  1.0.0
	 * @return string
	 */
	public function generate_multiselect_html( $key, $data ) {
		$field_key = $this->get_field_key( $key );
		$defaults  = array(
			'title'             => '',
			'disabled'          => false,
			'class'             => '',
			'css'               => '',
			'placeholder'       => '',
			'type'              => 'text',
			'desc_tip'          => false,
			'description'       => '',
			'custom_attributes' => array(),
			'select_buttons'    => false,
			'options'           => array(),
		);

		$data  = wp_parse_args( $data, $defaults );
		$value = (array) $this->get_option( $key, array() );

		ob_start();
		?>
		<tr valign="top">
			<th scope="row" class="titledesc">
				<label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // WPCS: XSS ok. ?></label>
			</th>
			<td class="forminp">
				<fieldset>
					<legend class="screen-reader-text"><span><?php echo wp_kses_post( $data['title'] ); ?></span></legend>
					<select multiple="multiple" class="multiselect <?php echo esc_attr( $data['class'] ); ?>" name="<?php echo esc_attr( $field_key ); ?>[]" id="<?php echo esc_attr( $field_key ); ?>" style="<?php echo esc_attr( $data['css'] ); ?>" <?php disabled( $data['disabled'], true ); ?> <?php echo $this->get_custom_attribute_html( $data ); // WPCS: XSS ok. ?>>
						<?php foreach ( (array) $data['options'] as $option_key => $option_value ) : ?>
							<?php if ( is_array( $option_value ) ) : ?>
								<optgroup label="<?php echo esc_attr( $option_key ); ?>">
									<?php foreach ( $option_value as $option_key_inner => $option_value_inner ) : ?>
										<option value="<?php echo esc_attr( $option_key_inner ); ?>" <?php selected( in_array( (string) $option_key_inner, $value, true ), true ); ?>><?php echo esc_html( $option_value_inner ); ?></option>
									<?php endforeach; ?>
								</optgroup>
							<?php else : ?>
								<option value="<?php echo esc_attr( $option_key ); ?>" <?php selected( in_array( (string) $option_key, $value, true ), true ); ?>><?php echo esc_html( $option_value ); ?></option>
							<?php endif; ?>
						<?php endforeach; ?>
					</select>
					<?php echo $this->get_description_html( $data ); // WPCS: XSS ok. ?>
					<?php if ( $data['select_buttons'] ) : ?>
						<br/><a class="select_all button" href="#"><?php esc_html_e( 'Select all', 'woocommerce' ); ?></a> <a class="select_none button" href="#"><?php esc_html_e( 'Select none', 'woocommerce' ); ?></a>
					<?php endif; ?>
				</fieldset>
			</td>
		</tr>
		<?php

		return ob_get_clean();
	}

	/**
	 * Generate Title HTML.
	 *
	 * @param string $key Field key.
	 * @param array  $data Field data.
	 * @since  1.0.0
	 * @return string
	 */
	public function generate_title_html( $key, $data ) {
		$field_key = $this->get_field_key( $key );
		$defaults  = array(
			'title' => '',
			'class' => '',
		);

		$data = wp_parse_args( $data, $defaults );

		ob_start();
		?>
			</table>
			<h3 class="wc-settings-sub-title <?php echo esc_attr( $data['class'] ); ?>" id="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?></h3>
			<?php if ( ! empty( $data['description'] ) ) : ?>
				<p><?php echo wp_kses_post( $data['description'] ); ?></p>
			<?php endif; ?>
			<table class="form-table">
		<?php

		return ob_get_clean();
	}

	/**
	 * Validate Text Field.
	 *
	 * Make sure the data is escaped correctly, etc.
	 *
	 * @param  string $key Field key.
	 * @param  string $value Posted Value.
	 * @return string
	 */
	public function validate_text_field( $key, $value ) {
		$value = is_null( $value ) ? '' : $value;
		return wp_kses_post( trim( stripslashes( $value ) ) );
	}

	/**
	 * Validate Price Field.
	 *
	 * Make sure the data is escaped correctly, etc.
	 *
	 * @param  string $key Field key.
	 * @param  string $value Posted Value.
	 * @return string
	 */
	public function validate_price_field( $key, $value ) {
		$value = is_null( $value ) ? '' : $value;
		return ( '' === $value ) ? '' : wc_format_decimal( trim( stripslashes( $value ) ) );
	}

	/**
	 * Validate Decimal Field.
	 *
	 * Make sure the data is escaped correctly, etc.
	 *
	 * @param  string $key Field key.
	 * @param  string $value Posted Value.
	 * @return string
	 */
	public function validate_decimal_field( $key, $value ) {
		$value = is_null( $value ) ? '' : $value;
		return ( '' === $value ) ? '' : wc_format_decimal( trim( stripslashes( $value ) ) );
	}

	/**
	 * Validate Password Field. No input sanitization is used to avoid corrupting passwords.
	 *
	 * @param  string $key Field key.
	 * @param  string $value Posted Value.
	 * @return string
	 */
	public function validate_password_field( $key, $value ) {
		$value = is_null( $value ) ? '' : $value;
		return trim( stripslashes( $value ) );
	}

	/**
	 * Validate Textarea Field.
	 *
	 * @param  string $key Field key.
	 * @param  string $value Posted Value.
	 * @return string
	 */
	public function validate_textarea_field( $key, $value ) {
		$value = is_null( $value ) ? '' : $value;
		return wp_kses(
			trim( stripslashes( $value ) ),
			array_merge(
				array(
					'iframe' => array(
						'src'   => true,
						'style' => true,
						'id'    => true,
						'class' => true,
					),
				),
				wp_kses_allowed_html( 'post' )
			)
		);
	}

	/**
	 * Validate Checkbox Field.
	 *
	 * If not set, return "no", otherwise return "yes".
	 *
	 * @param  string $key Field key.
	 * @param  string $value Posted Value.
	 * @return string
	 */
	public function validate_checkbox_field( $key, $value ) {
		return ! is_null( $value ) ? 'yes' : 'no';
	}

	/**
	 * Validate Select Field.
	 *
	 * @param  string $key Field key.
	 * @param  string $value Posted Value.
	 * @return string
	 */
	public function validate_select_field( $key, $value ) {
		$value = is_null( $value ) ? '' : $value;
		return wc_clean( stripslashes( $value ) );
	}

	/**
	 * Validate Multiselect Field.
	 *
	 * @param  string $key Field key.
	 * @param  string $value Posted Value.
	 * @return string|array
	 */
	public function validate_multiselect_field( $key, $value ) {
		return is_array( $value ) ? array_map( 'wc_clean', array_map( 'stripslashes', $value ) ) : '';
	}

	/**
	 * Validate the data on the "Settings" form.
	 *
	 * @deprecated 2.6.0 No longer used.
	 * @param array $form_fields Array of fields.
	 */
	public function validate_settings_fields( $form_fields = array() ) {
		wc_deprecated_function( 'validate_settings_fields', '2.6' );
	}

	/**
	 * Format settings if needed.
	 *
	 * @deprecated 2.6.0 Unused.
	 * @param  array $value Value to format.
	 * @return array
	 */
	public function format_settings( $value ) {
		wc_deprecated_function( 'format_settings', '2.6' );
		return $value;
	}
}
abstracts/abstract-wc-session.php000064400000004457151336044760013161 0ustar00<?php
/**
 * Handle data for the current customers session
 *
 * @class       WC_Session
 * @version     2.0.0
 * @package     WooCommerce\Abstracts
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC_Session
 */
abstract class WC_Session {

	/**
	 * Customer ID.
	 *
	 * @var int $_customer_id Customer ID.
	 */
	protected $_customer_id;

	/**
	 * Session Data.
	 *
	 * @var array $_data Data array.
	 */
	protected $_data = array();

	/**
	 * Dirty when the session needs saving.
	 *
	 * @var bool $_dirty When something changes
	 */
	protected $_dirty = false;

	/**
	 * Init hooks and session data. Extended by child classes.
	 *
	 * @since 3.3.0
	 */
	public function init() {}

	/**
	 * Cleanup session data. Extended by child classes.
	 */
	public function cleanup_sessions() {}

	/**
	 * Magic get method.
	 *
	 * @param mixed $key Key to get.
	 * @return mixed
	 */
	public function __get( $key ) {
		return $this->get( $key );
	}

	/**
	 * Magic set method.
	 *
	 * @param mixed $key Key to set.
	 * @param mixed $value Value to set.
	 */
	public function __set( $key, $value ) {
		$this->set( $key, $value );
	}

	/**
	 * Magic isset method.
	 *
	 * @param mixed $key Key to check.
	 * @return bool
	 */
	public function __isset( $key ) {
		return isset( $this->_data[ sanitize_title( $key ) ] );
	}

	/**
	 * Magic unset method.
	 *
	 * @param mixed $key Key to unset.
	 */
	public function __unset( $key ) {
		if ( isset( $this->_data[ $key ] ) ) {
			unset( $this->_data[ $key ] );
			$this->_dirty = true;
		}
	}

	/**
	 * Get a session variable.
	 *
	 * @param string $key Key to get.
	 * @param mixed  $default used if the session variable isn't set.
	 * @return array|string value of session variable
	 */
	public function get( $key, $default = null ) {
		$key = sanitize_key( $key );
		return isset( $this->_data[ $key ] ) ? maybe_unserialize( $this->_data[ $key ] ) : $default;
	}

	/**
	 * Set a session variable.
	 *
	 * @param string $key Key to set.
	 * @param mixed  $value Value to set.
	 */
	public function set( $key, $value ) {
		if ( $value !== $this->get( $key ) ) {
			$this->_data[ sanitize_key( $key ) ] = maybe_serialize( $value );
			$this->_dirty                        = true;
		}
	}

	/**
	 * Get customer ID.
	 *
	 * @return int
	 */
	public function get_customer_id() {
		return $this->_customer_id;
	}
}
abstracts/class-wc-background-process.php000064400000011566151336044760014572 0ustar00<?php
/**
 * Abstract WP_Background_Process class.
 *
 * Uses https://github.com/A5hleyRich/wp-background-processing to handle DB
 * updates in the background.
 *
 * @package WooCommerce\Classes
 */

defined( 'ABSPATH' ) || exit;

if ( ! class_exists( 'WP_Async_Request', false ) ) {
	include_once dirname( WC_PLUGIN_FILE ) . '/includes/libraries/wp-async-request.php';
}

if ( ! class_exists( 'WP_Background_Process', false ) ) {
	include_once dirname( WC_PLUGIN_FILE ) . '/includes/libraries/wp-background-process.php';
}

/**
 * WC_Background_Process class.
 */
abstract class WC_Background_Process extends WP_Background_Process {

	/**
	 * Is queue empty.
	 *
	 * @return bool
	 */
	protected function is_queue_empty() {
		global $wpdb;

		$table  = $wpdb->options;
		$column = 'option_name';

		if ( is_multisite() ) {
			$table  = $wpdb->sitemeta;
			$column = 'meta_key';
		}

		$key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%';

		$count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE {$column} LIKE %s", $key ) ); // @codingStandardsIgnoreLine.

		return ! ( $count > 0 );
	}

	/**
	 * Get batch.
	 *
	 * @return stdClass Return the first batch from the queue.
	 */
	protected function get_batch() {
		global $wpdb;

		$table        = $wpdb->options;
		$column       = 'option_name';
		$key_column   = 'option_id';
		$value_column = 'option_value';

		if ( is_multisite() ) {
			$table        = $wpdb->sitemeta;
			$column       = 'meta_key';
			$key_column   = 'meta_id';
			$value_column = 'meta_value';
		}

		$key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%';

		$query = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE {$column} LIKE %s ORDER BY {$key_column} ASC LIMIT 1", $key ) ); // @codingStandardsIgnoreLine.

		$batch       = new stdClass();
		$batch->key  = $query->$column;
		$batch->data = array_filter( (array) maybe_unserialize( $query->$value_column ) );

		return $batch;
	}

	/**
	 * See if the batch limit has been exceeded.
	 *
	 * @return bool
	 */
	protected function batch_limit_exceeded() {
		return $this->time_exceeded() || $this->memory_exceeded();
	}

	/**
	 * Handle.
	 *
	 * Pass each queue item to the task handler, while remaining
	 * within server memory and time limit constraints.
	 */
	protected function handle() {
		$this->lock_process();

		do {
			$batch = $this->get_batch();

			foreach ( $batch->data as $key => $value ) {
				$task = $this->task( $value );

				if ( false !== $task ) {
					$batch->data[ $key ] = $task;
				} else {
					unset( $batch->data[ $key ] );
				}

				if ( $this->batch_limit_exceeded() ) {
					// Batch limits reached.
					break;
				}
			}

			// Update or delete current batch.
			if ( ! empty( $batch->data ) ) {
				$this->update( $batch->key, $batch->data );
			} else {
				$this->delete( $batch->key );
			}
		} while ( ! $this->batch_limit_exceeded() && ! $this->is_queue_empty() );

		$this->unlock_process();

		// Start next batch or complete process.
		if ( ! $this->is_queue_empty() ) {
			$this->dispatch();
		} else {
			$this->complete();
		}
	}

	/**
	 * Get memory limit.
	 *
	 * @return int
	 */
	protected function get_memory_limit() {
		if ( function_exists( 'ini_get' ) ) {
			$memory_limit = ini_get( 'memory_limit' );
		} else {
			// Sensible default.
			$memory_limit = '128M';
		}

		if ( ! $memory_limit || -1 === intval( $memory_limit ) ) {
			// Unlimited, set to 32GB.
			$memory_limit = '32G';
		}

		return wp_convert_hr_to_bytes( $memory_limit );
	}

	/**
	 * Schedule cron healthcheck.
	 *
	 * @param array $schedules Schedules.
	 * @return array
	 */
	public function schedule_cron_healthcheck( $schedules ) {
		$interval = apply_filters( $this->identifier . '_cron_interval', 5 );

		if ( property_exists( $this, 'cron_interval' ) ) {
			$interval = apply_filters( $this->identifier . '_cron_interval', $this->cron_interval );
		}

		// Adds every 5 minutes to the existing schedules.
		$schedules[ $this->identifier . '_cron_interval' ] = array(
			'interval' => MINUTE_IN_SECONDS * $interval,
			/* translators: %d: interval */
			'display'  => sprintf( __( 'Every %d minutes', 'woocommerce' ), $interval ),
		);

		return $schedules;
	}

	/**
	 * Delete all batches.
	 *
	 * @return WC_Background_Process
	 */
	public function delete_all_batches() {
		global $wpdb;

		$table  = $wpdb->options;
		$column = 'option_name';

		if ( is_multisite() ) {
			$table  = $wpdb->sitemeta;
			$column = 'meta_key';
		}

		$key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%';

		$wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE {$column} LIKE %s", $key ) ); // @codingStandardsIgnoreLine.

		return $this;
	}

	/**
	 * Kill process.
	 *
	 * Stop processing queue items, clear cronjob and delete all batches.
	 */
	public function kill_process() {
		if ( ! $this->is_queue_empty() ) {
			$this->delete_all_batches();
			wp_clear_scheduled_hook( $this->cron_hook_identifier );
		}
	}
}
abstracts/abstract-wc-integration.php000064400000003535151336044760014015 0ustar00<?php
/**
 * Abstract Integration class
 *
 * Extension of the Settings API which in turn gets extended
 * by individual integrations to offer additional functionality.
 *
 * @class       WC_Settings_API
 * @version     2.6.0
 * @package     WooCommerce\Abstracts
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Abstract Integration Class
 *
 * Extended by individual integrations to offer additional functionality.
 *
 * @class    WC_Integration
 * @extends  WC_Settings_API
 * @version  2.6.0
 * @package  WooCommerce\Abstracts
 */
abstract class WC_Integration extends WC_Settings_API {

	/**
	 * Yes or no based on whether the integration is enabled.
	 *
	 * @var string
	 */
	public $enabled = 'yes';

	/**
	 * Integration title.
	 *
	 * @var string
	 */
	public $method_title = '';

	/**
	 * Integration description.
	 *
	 * @var string
	 */
	public $method_description = '';

	/**
	 * Return the title for admin screens.
	 *
	 * @return string
	 */
	public function get_method_title() {
		return apply_filters( 'woocommerce_integration_title', $this->method_title, $this );
	}

	/**
	 * Return the description for admin screens.
	 *
	 * @return string
	 */
	public function get_method_description() {
		return apply_filters( 'woocommerce_integration_description', $this->method_description, $this );
	}

	/**
	 * Output the gateway settings screen.
	 */
	public function admin_options() {
		echo '<h2>' . esc_html( $this->get_method_title() ) . '</h2>';
		echo wp_kses_post( wpautop( $this->get_method_description() ) );
		echo '<div><input type="hidden" name="section" value="' . esc_attr( $this->id ) . '" /></div>';
		parent::admin_options();
	}

	/**
	 * Init settings for gateways.
	 */
	public function init_settings() {
		parent::init_settings();
		$this->enabled = ! empty( $this->settings['enabled'] ) && 'yes' === $this->settings['enabled'] ? 'yes' : 'no';
	}
}
abstracts/abstract-wc-privacy.php000064400000007547151336044760013156 0ustar00<?php
/**
 * WooCommerce abstract privacy class.
 *
 * @since 3.4.0
 * @package WooCommerce\Abstracts
 */

defined( 'ABSPATH' ) || exit;

/**
 * Abstract class that is intended to be extended by
 * specific privacy class. It handles the display
 * of the privacy message of the privacy id to the admin,
 * privacy data to be exported and privacy data to be deleted.
 *
 * @version  3.4.0
 * @package  WooCommerce\Abstracts
 */
abstract class WC_Abstract_Privacy {
	/**
	 * This is the name of this object type.
	 *
	 * @var string
	 */
	public $name;

	/**
	 * This is a list of exporters.
	 *
	 * @var array
	 */
	protected $exporters = array();

	/**
	 * This is a list of erasers.
	 *
	 * @var array
	 */
	protected $erasers = array();

	/**
	 * This is a priority for the wp_privacy_personal_data_exporters filter
	 *
	 * @var int
	 */
	protected $export_priority;

	/**
	 * This is a priority for the wp_privacy_personal_data_erasers filter
	 *
	 * @var int
	 */
	protected $erase_priority;

	/**
	 * WC_Abstract_Privacy Constructor.
	 *
	 * @param string $name            Plugin identifier.
	 * @param int    $export_priority Export priority.
	 * @param int    $erase_priority  Erase priority.
	 */
	public function __construct( $name = '', $export_priority = 5, $erase_priority = 10 ) {
		$this->name            = $name;
		$this->export_priority = $export_priority;
		$this->erase_priority  = $erase_priority;
		$this->init();
	}

	/**
	 * Hook in events.
	 */
	protected function init() {
		add_action( 'admin_init', array( $this, 'add_privacy_message' ) );
		// We set priority to 5 to help WooCommerce's findings appear before those from extensions in exported items.
		add_filter( 'wp_privacy_personal_data_exporters', array( $this, 'register_exporters' ), $this->export_priority );
		add_filter( 'wp_privacy_personal_data_erasers', array( $this, 'register_erasers' ), $this->erase_priority );
	}

	/**
	 * Adds the privacy message on WC privacy page.
	 */
	public function add_privacy_message() {
		if ( function_exists( 'wp_add_privacy_policy_content' ) ) {
			$content = $this->get_privacy_message();

			if ( $content ) {
				wp_add_privacy_policy_content( $this->name, $this->get_privacy_message() );
			}
		}
	}

	/**
	 * Gets the message of the privacy to display.
	 * To be overloaded by the implementor.
	 *
	 * @return string
	 */
	public function get_privacy_message() {
		return '';
	}

	/**
	 * Integrate this exporter implementation within the WordPress core exporters.
	 *
	 * @param array $exporters List of exporter callbacks.
	 * @return array
	 */
	public function register_exporters( $exporters = array() ) {
		foreach ( $this->exporters as $id => $exporter ) {
			$exporters[ $id ] = $exporter;
		}
		return $exporters;
	}

	/**
	 * Integrate this eraser implementation within the WordPress core erasers.
	 *
	 * @param array $erasers List of eraser callbacks.
	 * @return array
	 */
	public function register_erasers( $erasers = array() ) {
		foreach ( $this->erasers as $id => $eraser ) {
			$erasers[ $id ] = $eraser;
		}
		return $erasers;
	}

	/**
	 * Add exporter to list of exporters.
	 *
	 * @param string       $id       ID of the Exporter.
	 * @param string       $name     Exporter name.
	 * @param string|array $callback Exporter callback.
	 *
	 * @return array
	 */
	public function add_exporter( $id, $name, $callback ) {
		$this->exporters[ $id ] = array(
			'exporter_friendly_name' => $name,
			'callback'               => $callback,
		);
		return $this->exporters;
	}

	/**
	 * Add eraser to list of erasers.
	 *
	 * @param string       $id       ID of the Eraser.
	 * @param string       $name     Exporter name.
	 * @param string|array $callback Exporter callback.
	 *
	 * @return array
	 */
	public function add_eraser( $id, $name, $callback ) {
		$this->erasers[ $id ] = array(
			'eraser_friendly_name' => $name,
			'callback'             => $callback,
		);
		return $this->erasers;
	}
}
wc-notice-functions.php000064400000016775151336044760011204 0ustar00<?php
/**
 * WooCommerce Message Functions
 *
 * Functions for error/message handling and display.
 *
 * @package WooCommerce\Functions
 * @version 2.1.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Get the count of notices added, either for all notices (default) or for one.
 * particular notice type specified by $notice_type.
 *
 * @since  2.1
 * @param  string $notice_type Optional. The name of the notice type - either error, success or notice.
 * @return int
 */
function wc_notice_count( $notice_type = '' ) {
	if ( ! did_action( 'woocommerce_init' ) ) {
		wc_doing_it_wrong( __FUNCTION__, __( 'This function should not be called before woocommerce_init.', 'woocommerce' ), '2.3' );
		return;
	}

	$notice_count = 0;
	$all_notices  = WC()->session->get( 'wc_notices', array() );

	if ( isset( $all_notices[ $notice_type ] ) ) {

		$notice_count = count( $all_notices[ $notice_type ] );

	} elseif ( empty( $notice_type ) ) {

		foreach ( $all_notices as $notices ) {
			$notice_count += count( $notices );
		}
	}

	return $notice_count;
}

/**
 * Check if a notice has already been added.
 *
 * @since  2.1
 * @param  string $message The text to display in the notice.
 * @param  string $notice_type Optional. The name of the notice type - either error, success or notice.
 * @return bool
 */
function wc_has_notice( $message, $notice_type = 'success' ) {
	if ( ! did_action( 'woocommerce_init' ) ) {
		wc_doing_it_wrong( __FUNCTION__, __( 'This function should not be called before woocommerce_init.', 'woocommerce' ), '2.3' );
		return false;
	}

	$notices = WC()->session->get( 'wc_notices', array() );
	$notices = isset( $notices[ $notice_type ] ) ? $notices[ $notice_type ] : array();
	return array_search( $message, wp_list_pluck( $notices, 'notice' ), true ) !== false;
}

/**
 * Add and store a notice.
 *
 * @since 2.1
 * @version 3.9.0
 * @param string $message     The text to display in the notice.
 * @param string $notice_type Optional. The name of the notice type - either error, success or notice.
 * @param array  $data        Optional notice data.
 */
function wc_add_notice( $message, $notice_type = 'success', $data = array() ) {
	if ( ! did_action( 'woocommerce_init' ) ) {
		wc_doing_it_wrong( __FUNCTION__, __( 'This function should not be called before woocommerce_init.', 'woocommerce' ), '2.3' );
		return;
	}

	$notices = WC()->session->get( 'wc_notices', array() );

	// Backward compatibility.
	if ( 'success' === $notice_type ) {
		$message = apply_filters( 'woocommerce_add_message', $message );
	}

	$message = apply_filters( 'woocommerce_add_' . $notice_type, $message );

	if ( ! empty( $message ) ) {
		$notices[ $notice_type ][] = array(
			'notice' => $message,
			'data'   => $data,
		);
	}

	WC()->session->set( 'wc_notices', $notices );
}

/**
 * Set all notices at once.
 *
 * @since 2.6.0
 * @param array[] $notices Array of notices.
 */
function wc_set_notices( $notices ) {
	if ( ! did_action( 'woocommerce_init' ) ) {
		wc_doing_it_wrong( __FUNCTION__, __( 'This function should not be called before woocommerce_init.', 'woocommerce' ), '2.6' );
		return;
	}

	WC()->session->set( 'wc_notices', $notices );
}

/**
 * Unset all notices.
 *
 * @since 2.1
 */
function wc_clear_notices() {
	if ( ! did_action( 'woocommerce_init' ) ) {
		wc_doing_it_wrong( __FUNCTION__, __( 'This function should not be called before woocommerce_init.', 'woocommerce' ), '2.3' );
		return;
	}
	WC()->session->set( 'wc_notices', null );
}

/**
 * Prints messages and errors which are stored in the session, then clears them.
 *
 * @since 2.1
 * @param bool $return true to return rather than echo. @since 3.5.0.
 * @return string|null
 */
function wc_print_notices( $return = false ) {
	if ( ! did_action( 'woocommerce_init' ) ) {
		wc_doing_it_wrong( __FUNCTION__, __( 'This function should not be called before woocommerce_init.', 'woocommerce' ), '2.3' );
		return;
	}

	$all_notices  = WC()->session->get( 'wc_notices', array() );
	$notice_types = apply_filters( 'woocommerce_notice_types', array( 'error', 'success', 'notice' ) );

	// Buffer output.
	ob_start();

	foreach ( $notice_types as $notice_type ) {
		if ( wc_notice_count( $notice_type ) > 0 ) {
			$messages = array();

			foreach ( $all_notices[ $notice_type ] as $notice ) {
				$messages[] = isset( $notice['notice'] ) ? $notice['notice'] : $notice;
			}

			wc_get_template(
				"notices/{$notice_type}.php",
				array(
					'messages' => array_filter( $messages ), // @deprecated 3.9.0
					'notices'  => array_filter( $all_notices[ $notice_type ] ),
				)
			);
		}
	}

	wc_clear_notices();

	$notices = wc_kses_notice( ob_get_clean() );

	if ( $return ) {
		return $notices;
	}

	echo $notices; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}

/**
 * Print a single notice immediately.
 *
 * @since 2.1
 * @version 3.9.0
 * @param string $message The text to display in the notice.
 * @param string $notice_type Optional. The singular name of the notice type - either error, success or notice.
 * @param array  $data        Optional notice data. @since 3.9.0.
 */
function wc_print_notice( $message, $notice_type = 'success', $data = array() ) {
	if ( 'success' === $notice_type ) {
		$message = apply_filters( 'woocommerce_add_message', $message );
	}

	$message = apply_filters( 'woocommerce_add_' . $notice_type, $message );

	wc_get_template(
		"notices/{$notice_type}.php",
		array(
			'messages' => array( $message ), // @deprecated 3.9.0
			'notices'  => array(
				array(
					'notice' => $message,
					'data'   => $data,
				),
			),
		)
	);
}

/**
 * Returns all queued notices, optionally filtered by a notice type.
 *
 * @since  2.1
 * @version 3.9.0
 * @param  string $notice_type Optional. The singular name of the notice type - either error, success or notice.
 * @return array[]
 */
function wc_get_notices( $notice_type = '' ) {
	if ( ! did_action( 'woocommerce_init' ) ) {
		wc_doing_it_wrong( __FUNCTION__, __( 'This function should not be called before woocommerce_init.', 'woocommerce' ), '2.3' );
		return;
	}

	$all_notices = WC()->session->get( 'wc_notices', array() );

	if ( empty( $notice_type ) ) {
		$notices = $all_notices;
	} elseif ( isset( $all_notices[ $notice_type ] ) ) {
		$notices = $all_notices[ $notice_type ];
	} else {
		$notices = array();
	}

	return $notices;
}

/**
 * Add notices for WP Errors.
 *
 * @param WP_Error $errors Errors.
 */
function wc_add_wp_error_notices( $errors ) {
	if ( is_wp_error( $errors ) && $errors->get_error_messages() ) {
		foreach ( $errors->get_error_messages() as $error ) {
			wc_add_notice( $error, 'error' );
		}
	}
}

/**
 * Filters out the same tags as wp_kses_post, but allows tabindex for <a> element.
 *
 * @since 3.5.0
 * @param string $message Content to filter through kses.
 * @return string
 */
function wc_kses_notice( $message ) {
	$allowed_tags = array_replace_recursive(
		wp_kses_allowed_html( 'post' ),
		array(
			'a' => array(
				'tabindex' => true,
			),
		)
	);

	/**
	 * Kses notice allowed tags.
	 *
	 * @since 3.9.0
	 * @param array[]|string $allowed_tags An array of allowed HTML elements and attributes, or a context name such as 'post'.
	 */
	return wp_kses( $message, apply_filters( 'woocommerce_kses_notice_allowed_tags', $allowed_tags ) );
}

/**
 * Get notice data attribute.
 *
 * @since 3.9.0
 * @param array $notice Notice data.
 * @return string
 */
function wc_get_notice_data_attr( $notice ) {
	if ( empty( $notice['data'] ) ) {
		return;
	}

	$attr = '';

	foreach ( $notice['data'] as $key => $value ) {
		$attr .= sprintf(
			' data-%1$s="%2$s"',
			sanitize_title( $key ),
			esc_attr( $value )
		);
	}

	return $attr;
}
class-wc-data-store.php000064400000013545151336044760011053 0ustar00<?php
/**
 * WC Data Store.
 *
 * @package WooCommerce\Classes
 * @since   3.0.0
 * @version 3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Data store class.
 */
class WC_Data_Store {

	/**
	 * Contains an instance of the data store class that we are working with.
	 *
	 * @var WC_Data_Store
	 */
	private $instance = null;

	/**
	 * Contains an array of default WC supported data stores.
	 * Format of object name => class name.
	 * Example: 'product' => 'WC_Product_Data_Store_CPT'
	 * You can also pass something like product_<type> for product stores and
	 * that type will be used first when available, if a store is requested like
	 * this and doesn't exist, then the store would fall back to 'product'.
	 * Ran through `woocommerce_data_stores`.
	 *
	 * @var array
	 */
	private $stores = array(
		'coupon'                => 'WC_Coupon_Data_Store_CPT',
		'customer'              => 'WC_Customer_Data_Store',
		'customer-download'     => 'WC_Customer_Download_Data_Store',
		'customer-download-log' => 'WC_Customer_Download_Log_Data_Store',
		'customer-session'      => 'WC_Customer_Data_Store_Session',
		'order'                 => 'WC_Order_Data_Store_CPT',
		'order-refund'          => 'WC_Order_Refund_Data_Store_CPT',
		'order-item'            => 'WC_Order_Item_Data_Store',
		'order-item-coupon'     => 'WC_Order_Item_Coupon_Data_Store',
		'order-item-fee'        => 'WC_Order_Item_Fee_Data_Store',
		'order-item-product'    => 'WC_Order_Item_Product_Data_Store',
		'order-item-shipping'   => 'WC_Order_Item_Shipping_Data_Store',
		'order-item-tax'        => 'WC_Order_Item_Tax_Data_Store',
		'payment-token'         => 'WC_Payment_Token_Data_Store',
		'product'               => 'WC_Product_Data_Store_CPT',
		'product-grouped'       => 'WC_Product_Grouped_Data_Store_CPT',
		'product-variable'      => 'WC_Product_Variable_Data_Store_CPT',
		'product-variation'     => 'WC_Product_Variation_Data_Store_CPT',
		'shipping-zone'         => 'WC_Shipping_Zone_Data_Store',
		'webhook'               => 'WC_Webhook_Data_Store',
	);

	/**
	 * Contains the name of the current data store's class name.
	 *
	 * @var string
	 */
	private $current_class_name = '';

	/**
	 * The object type this store works with.
	 *
	 * @var string
	 */
	private $object_type = '';


	/**
	 * Tells WC_Data_Store which object (coupon, product, order, etc)
	 * store we want to work with.
	 *
	 * @throws Exception When validation fails.
	 * @param string $object_type Name of object.
	 */
	public function __construct( $object_type ) {
		$this->object_type = $object_type;
		$this->stores      = apply_filters( 'woocommerce_data_stores', $this->stores );

		// If this object type can't be found, check to see if we can load one
		// level up (so if product-type isn't found, we try product).
		if ( ! array_key_exists( $object_type, $this->stores ) ) {
			$pieces      = explode( '-', $object_type );
			$object_type = $pieces[0];
		}

		if ( array_key_exists( $object_type, $this->stores ) ) {
			$store = apply_filters( 'woocommerce_' . $object_type . '_data_store', $this->stores[ $object_type ] );
			if ( is_object( $store ) ) {
				if ( ! $store instanceof WC_Object_Data_Store_Interface ) {
					throw new Exception( __( 'Invalid data store.', 'woocommerce' ) );
				}
				$this->current_class_name = get_class( $store );
				$this->instance           = $store;
			} else {
				if ( ! class_exists( $store ) ) {
					throw new Exception( __( 'Invalid data store.', 'woocommerce' ) );
				}
				$this->current_class_name = $store;
				$this->instance           = new $store();
			}
		} else {
			throw new Exception( __( 'Invalid data store.', 'woocommerce' ) );
		}
	}

	/**
	 * Only store the object type to avoid serializing the data store instance.
	 *
	 * @return array
	 */
	public function __sleep() {
		return array( 'object_type' );
	}

	/**
	 * Re-run the constructor with the object type.
	 *
	 * @throws Exception When validation fails.
	 */
	public function __wakeup() {
		$this->__construct( $this->object_type );
	}

	/**
	 * Loads a data store.
	 *
	 * @param string $object_type Name of object.
	 *
	 * @since 3.0.0
	 * @throws Exception When validation fails.
	 * @return WC_Data_Store
	 */
	public static function load( $object_type ) {
		return new WC_Data_Store( $object_type );
	}

	/**
	 * Returns the class name of the current data store.
	 *
	 * @since 3.0.0
	 * @return string
	 */
	public function get_current_class_name() {
		return $this->current_class_name;
	}

	/**
	 * Reads an object from the data store.
	 *
	 * @since 3.0.0
	 * @param WC_Data $data WooCommerce data instance.
	 */
	public function read( &$data ) {
		$this->instance->read( $data );
	}

	/**
	 * Create an object in the data store.
	 *
	 * @since 3.0.0
	 * @param WC_Data $data WooCommerce data instance.
	 */
	public function create( &$data ) {
		$this->instance->create( $data );
	}

	/**
	 * Update an object in the data store.
	 *
	 * @since 3.0.0
	 * @param WC_Data $data WooCommerce data instance.
	 */
	public function update( &$data ) {
		$this->instance->update( $data );
	}

	/**
	 * Delete an object from the data store.
	 *
	 * @since 3.0.0
	 * @param WC_Data $data WooCommerce data instance.
	 * @param array   $args Array of args to pass to the delete method.
	 */
	public function delete( &$data, $args = array() ) {
		$this->instance->delete( $data, $args );
	}

	/**
	 * Data stores can define additional functions (for example, coupons have
	 * some helper methods for increasing or decreasing usage). This passes
	 * through to the instance if that function exists.
	 *
	 * @since 3.0.0
	 * @param string $method     Method.
	 * @param mixed  $parameters Parameters.
	 * @return mixed
	 */
	public function __call( $method, $parameters ) {
		if ( is_callable( array( $this->instance, $method ) ) ) {
			$object     = array_shift( $parameters );
			$parameters = array_merge( array( &$object ), $parameters );
			return $this->instance->$method( ...$parameters );
		}
	}
}
class-wc-cart-fees.php000064400000006644151336044760010663 0ustar00<?php
/**
 * Cart fees API.
 *
 * Developers can add fees to the cart via WC()->cart->fees_api() which will reference this class.
 *
 * We suggest using the action woocommerce_cart_calculate_fees hook for adding fees.
 *
 * @package WooCommerce\Classes
 * @version 3.2.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * WC_Cart_Fees class.
 *
 * @since 3.2.0
 */
final class WC_Cart_Fees {

	/**
	 * An array of fee objects.
	 *
	 * @var object[]
	 */
	private $fees = array();

	/**
	 * Reference to cart object.
	 *
	 * @since 3.2.0
	 * @var WC_Cart
	 */
	private $cart;

	/**
	 * New fees are made out of these props.
	 *
	 * @var array
	 */
	private $default_fee_props = array(
		'id'        => '',
		'name'      => '',
		'tax_class' => '',
		'taxable'   => false,
		'amount'    => 0,
		'total'     => 0,
	);

	/**
	 * Constructor. Reference to the cart.
	 *
	 * @since 3.2.0
	 * @throws Exception If missing WC_Cart object.
	 * @param WC_Cart $cart Cart object.
	 */
	public function __construct( &$cart ) {
		if ( ! is_a( $cart, 'WC_Cart' ) ) {
			throw new Exception( 'A valid WC_Cart object is required' );
		}

		$this->cart = $cart;
	}

	/**
	 * Register methods for this object on the appropriate WordPress hooks.
	 */
	public function init() {}

	/**
	 * Add a fee. Fee IDs must be unique.
	 *
	 * @since 3.2.0
	 * @param array $args Array of fee properties.
	 * @return object Either a fee object if added, or a WP_Error if it failed.
	 */
	public function add_fee( $args = array() ) {
		$fee_props            = (object) wp_parse_args( $args, $this->default_fee_props );
		$fee_props->name      = $fee_props->name ? $fee_props->name : __( 'Fee', 'woocommerce' );
		$fee_props->tax_class = in_array( $fee_props->tax_class, array_merge( WC_Tax::get_tax_classes(), WC_Tax::get_tax_class_slugs() ), true ) ? $fee_props->tax_class : '';
		$fee_props->taxable   = wc_string_to_bool( $fee_props->taxable );
		$fee_props->amount    = wc_format_decimal( $fee_props->amount );

		if ( empty( $fee_props->id ) ) {
			$fee_props->id = $this->generate_id( $fee_props );
		}

		if ( array_key_exists( $fee_props->id, $this->fees ) ) {
			return new WP_Error( 'fee_exists', __( 'Fee has already been added.', 'woocommerce' ) );
		}

		$this->fees[ $fee_props->id ] = $fee_props;

		return $this->fees[ $fee_props->id ];
	}

	/**
	 * Get fees.
	 *
	 * @return array
	 */
	public function get_fees() {
		uasort( $this->fees, array( $this, 'sort_fees_callback' ) );

		return $this->fees;
	}

	/**
	 * Set fees.
	 *
	 * @param object[] $raw_fees Array of fees.
	 */
	public function set_fees( $raw_fees = array() ) {
		$this->fees = array();

		foreach ( $raw_fees as $raw_fee ) {
			$this->add_fee( $raw_fee );
		}
	}

	/**
	 * Remove all fees.
	 *
	 * @since 3.2.0
	 */
	public function remove_all_fees() {
		$this->set_fees();
	}

	/**
	 * Sort fees by amount.
	 *
	 * @param stdClass $a Fee object.
	 * @param stdClass $b Fee object.
	 * @return int
	 */
	protected function sort_fees_callback( $a, $b ) {
		/**
		 * Filter sort fees callback.
		 *
		 * @since 3.8.0
		 * @param int Sort order, -1 or 1.
		 * @param stdClass $a Fee object.
		 * @param stdClass $b Fee object.
		 */
		return apply_filters( 'woocommerce_sort_fees_callback', $a->amount > $b->amount ? -1 : 1, $a, $b );
	}

	/**
	 * Generate a unique ID for the fee being added.
	 *
	 * @param string $fee Fee object.
	 * @return string fee key.
	 */
	private function generate_id( $fee ) {
		return sanitize_title( $fee->name );
	}
}
class-wc-tax.php000064400000112001151336044760007567 0ustar00<?php
/**
 * Tax calculation and rate finding class.
 *
 * @package WooCommerce\Classes
 */

use Automattic\WooCommerce\Utilities\NumberUtil;

defined( 'ABSPATH' ) || exit;

/**
 * Performs tax calculations and loads tax rates
 *
 * @class WC_Tax
 */
class WC_Tax {

	/**
	 * Precision.
	 *
	 * @var int
	 */
	public static $precision;

	/**
	 * Round at subtotal.
	 *
	 * @var bool
	 */
	public static $round_at_subtotal = false;

	/**
	 * Load options.
	 */
	public static function init() {
		self::$precision         = wc_get_rounding_precision();
		self::$round_at_subtotal = 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' );
	}

	/**
	 * When the woocommerce_tax_classes option is changed, remove any orphan rates.
	 *
	 * @deprecated 3.7.0
	 * @param  string $old_value Old rates value.
	 * @param  string $value New rates value.
	 */
	public static function maybe_remove_tax_class_rates( $old_value, $value ) {
		wc_deprecated_function( 'WC_Tax::maybe_remove_tax_class_rates', '3.7', 'WC_Tax::delete_tax_class_by' );

		$tax_classes          = array_filter( array_map( 'trim', explode( "\n", $value ) ) );
		$existing_tax_classes = self::get_tax_classes();
		$removed              = array_diff( $existing_tax_classes, $tax_classes );
		foreach ( $removed as $name ) {
			self::delete_tax_class_by( 'name', $name );
		}
	}

	/**
	 * Calculate tax for a line.
	 *
	 * @param  float   $price              Price to calc tax on.
	 * @param  array   $rates              Rates to apply.
	 * @param  boolean $price_includes_tax Whether the passed price has taxes included.
	 * @param  boolean $deprecated         Whether to suppress any rounding from taking place. No longer used here.
	 * @return array                       Array of rates + prices after tax.
	 */
	public static function calc_tax( $price, $rates, $price_includes_tax = false, $deprecated = false ) {
		if ( $price_includes_tax ) {
			$taxes = self::calc_inclusive_tax( $price, $rates );
		} else {
			$taxes = self::calc_exclusive_tax( $price, $rates );
		}
		return apply_filters( 'woocommerce_calc_tax', $taxes, $price, $rates, $price_includes_tax, $deprecated );
	}

	/**
	 * Calculate the shipping tax using a passed array of rates.
	 *
	 * @param float $price Shipping cost.
	 * @param array $rates Taxation Rate.
	 * @return array
	 */
	public static function calc_shipping_tax( $price, $rates ) {
		$taxes = self::calc_exclusive_tax( $price, $rates );
		return apply_filters( 'woocommerce_calc_shipping_tax', $taxes, $price, $rates );
	}

	/**
	 * Round to precision.
	 *
	 * Filter example: to return rounding to .5 cents you'd use:
	 *
	 * function euro_5cent_rounding( $in ) {
	 *      return round( $in / 5, 2 ) * 5;
	 * }
	 * add_filter( 'woocommerce_tax_round', 'euro_5cent_rounding' );
	 *
	 * @param float|int $in Value to round.
	 * @return float
	 */
	public static function round( $in ) {
		return apply_filters( 'woocommerce_tax_round', NumberUtil::round( $in, wc_get_rounding_precision() ), $in );
	}

	/**
	 * Calc tax from inclusive price.
	 *
	 * @param  float $price Price to calculate tax for.
	 * @param  array $rates Array of tax rates.
	 * @return array
	 */
	public static function calc_inclusive_tax( $price, $rates ) {
		$taxes          = array();
		$compound_rates = array();
		$regular_rates  = array();

		// Index array so taxes are output in correct order and see what compound/regular rates we have to calculate.
		foreach ( $rates as $key => $rate ) {
			$taxes[ $key ] = 0;

			if ( 'yes' === $rate['compound'] ) {
				$compound_rates[ $key ] = $rate['rate'];
			} else {
				$regular_rates[ $key ] = $rate['rate'];
			}
		}

		$compound_rates = array_reverse( $compound_rates, true ); // Working backwards.

		$non_compound_price = $price;

		foreach ( $compound_rates as $key => $compound_rate ) {
			$tax_amount         = apply_filters( 'woocommerce_price_inc_tax_amount', $non_compound_price - ( $non_compound_price / ( 1 + ( $compound_rate / 100 ) ) ), $key, $rates[ $key ], $price );
			$taxes[ $key ]     += $tax_amount;
			$non_compound_price = $non_compound_price - $tax_amount;
		}

		// Regular taxes.
		$regular_tax_rate = 1 + ( array_sum( $regular_rates ) / 100 );

		foreach ( $regular_rates as $key => $regular_rate ) {
			$the_rate       = ( $regular_rate / 100 ) / $regular_tax_rate;
			$net_price      = $price - ( $the_rate * $non_compound_price );
			$tax_amount     = apply_filters( 'woocommerce_price_inc_tax_amount', $price - $net_price, $key, $rates[ $key ], $price );
			$taxes[ $key ] += $tax_amount;
		}

		/**
		 * Round all taxes to precision (4DP) before passing them back. Note, this is not the same rounding
		 * as in the cart calculation class which, depending on settings, will round to 2DP when calculating
		 * final totals. Also unlike that class, this rounds .5 up for all cases.
		 */
		$taxes = array_map( array( __CLASS__, 'round' ), $taxes );

		return $taxes;
	}

	/**
	 * Calc tax from exclusive price.
	 *
	 * @param  float $price Price to calculate tax for.
	 * @param  array $rates Array of tax rates.
	 * @return array
	 */
	public static function calc_exclusive_tax( $price, $rates ) {
		$taxes = array();

		if ( ! empty( $rates ) ) {
			foreach ( $rates as $key => $rate ) {
				if ( 'yes' === $rate['compound'] ) {
					continue;
				}

				$tax_amount = $price * ( $rate['rate'] / 100 );
				$tax_amount = apply_filters( 'woocommerce_price_ex_tax_amount', $tax_amount, $key, $rate, $price ); // ADVANCED: Allow third parties to modify this rate.

				if ( ! isset( $taxes[ $key ] ) ) {
					$taxes[ $key ] = $tax_amount;
				} else {
					$taxes[ $key ] += $tax_amount;
				}
			}

			$pre_compound_total = array_sum( $taxes );

			// Compound taxes.
			foreach ( $rates as $key => $rate ) {
				if ( 'no' === $rate['compound'] ) {
					continue;
				}
				$the_price_inc_tax = $price + ( $pre_compound_total );
				$tax_amount        = $the_price_inc_tax * ( $rate['rate'] / 100 );
				$tax_amount        = apply_filters( 'woocommerce_price_ex_tax_amount', $tax_amount, $key, $rate, $price, $the_price_inc_tax, $pre_compound_total ); // ADVANCED: Allow third parties to modify this rate.

				if ( ! isset( $taxes[ $key ] ) ) {
					$taxes[ $key ] = $tax_amount;
				} else {
					$taxes[ $key ] += $tax_amount;
				}

				$pre_compound_total = array_sum( $taxes );
			}
		}

		/**
		 * Round all taxes to precision (4DP) before passing them back. Note, this is not the same rounding
		 * as in the cart calculation class which, depending on settings, will round to 2DP when calculating
		 * final totals. Also unlike that class, this rounds .5 up for all cases.
		 */
		$taxes = array_map( array( __CLASS__, 'round' ), $taxes );

		return $taxes;
	}

	/**
	 * Searches for all matching country/state/postcode tax rates.
	 *
	 * @param array $args Args that determine the rate to find.
	 * @return array
	 */
	public static function find_rates( $args = array() ) {
		$args = wp_parse_args(
			$args,
			array(
				'country'   => '',
				'state'     => '',
				'city'      => '',
				'postcode'  => '',
				'tax_class' => '',
			)
		);

		$country   = $args['country'];
		$state     = $args['state'];
		$city      = $args['city'];
		$postcode  = wc_normalize_postcode( wc_clean( $args['postcode'] ) );
		$tax_class = $args['tax_class'];

		if ( ! $country ) {
			return array();
		}

		$cache_key         = WC_Cache_Helper::get_cache_prefix( 'taxes' ) . 'wc_tax_rates_' . md5( sprintf( '%s+%s+%s+%s+%s', $country, $state, $city, $postcode, $tax_class ) );
		$matched_tax_rates = wp_cache_get( $cache_key, 'taxes' );

		if ( false === $matched_tax_rates ) {
			$matched_tax_rates = self::get_matched_tax_rates( $country, $state, $postcode, $city, $tax_class );
			wp_cache_set( $cache_key, $matched_tax_rates, 'taxes' );
		}

		return apply_filters( 'woocommerce_find_rates', $matched_tax_rates, $args );
	}

	/**
	 * Searches for all matching country/state/postcode tax rates.
	 *
	 * @param array $args Args that determine the rate to find.
	 * @return array
	 */
	public static function find_shipping_rates( $args = array() ) {
		$rates          = self::find_rates( $args );
		$shipping_rates = array();

		if ( is_array( $rates ) ) {
			foreach ( $rates as $key => $rate ) {
				if ( 'yes' === $rate['shipping'] ) {
					$shipping_rates[ $key ] = $rate;
				}
			}
		}

		return $shipping_rates;
	}

	/**
	 * Does the sort comparison. Compares (in this order):
	 * - Priority
	 * - Country
	 * - State
	 * - Number of postcodes
	 * - Number of cities
	 * - ID
	 *
	 * @param object $rate1 First rate to compare.
	 * @param object $rate2 Second rate to compare.
	 * @return int
	 */
	private static function sort_rates_callback( $rate1, $rate2 ) {
		if ( $rate1->tax_rate_priority !== $rate2->tax_rate_priority ) {
			return $rate1->tax_rate_priority < $rate2->tax_rate_priority ? -1 : 1; // ASC.
		}

		if ( $rate1->tax_rate_country !== $rate2->tax_rate_country ) {
			if ( '' === $rate1->tax_rate_country ) {
				return 1;
			}
			if ( '' === $rate2->tax_rate_country ) {
				return -1;
			}
			return strcmp( $rate1->tax_rate_country, $rate2->tax_rate_country ) > 0 ? 1 : -1;
		}

		if ( $rate1->tax_rate_state !== $rate2->tax_rate_state ) {
			if ( '' === $rate1->tax_rate_state ) {
				return 1;
			}
			if ( '' === $rate2->tax_rate_state ) {
				return -1;
			}
			return strcmp( $rate1->tax_rate_state, $rate2->tax_rate_state ) > 0 ? 1 : -1;
		}

		if ( isset( $rate1->postcode_count, $rate2->postcode_count ) && $rate1->postcode_count !== $rate2->postcode_count ) {
			return $rate1->postcode_count < $rate2->postcode_count ? 1 : -1;
		}

		if ( isset( $rate1->city_count, $rate2->city_count ) && $rate1->city_count !== $rate2->city_count ) {
			return $rate1->city_count < $rate2->city_count ? 1 : -1;
		}

		return $rate1->tax_rate_id < $rate2->tax_rate_id ? -1 : 1;
	}

	/**
	 * Logical sort order for tax rates based on the following in order of priority.
	 *
	 * @param  array $rates Rates to be sorted.
	 * @return array
	 */
	private static function sort_rates( $rates ) {
		uasort( $rates, __CLASS__ . '::sort_rates_callback' );
		$i = 0;
		foreach ( $rates as $key => $rate ) {
			$rates[ $key ]->tax_rate_order = $i++;
		}
		return $rates;
	}

	/**
	 * Loop through a set of tax rates and get the matching rates (1 per priority).
	 *
	 * @param  string $country Country code to match against.
	 * @param  string $state State code to match against.
	 * @param  string $postcode Postcode to match against.
	 * @param  string $city City to match against.
	 * @param  string $tax_class Tax class to match against.
	 * @return array
	 */
	private static function get_matched_tax_rates( $country, $state, $postcode, $city, $tax_class ) {
		global $wpdb;

		// Query criteria - these will be ANDed.
		$criteria   = array();
		$criteria[] = $wpdb->prepare( "tax_rate_country IN ( %s, '' )", strtoupper( $country ) );
		$criteria[] = $wpdb->prepare( "tax_rate_state IN ( %s, '' )", strtoupper( $state ) );
		$criteria[] = $wpdb->prepare( 'tax_rate_class = %s', sanitize_title( $tax_class ) );

		// Pre-query postcode ranges for PHP based matching.
		$postcode_search = wc_get_wildcard_postcodes( $postcode, $country );
		$postcode_ranges = $wpdb->get_results( "SELECT tax_rate_id, location_code FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE location_type = 'postcode' AND location_code LIKE '%...%';" );

		if ( $postcode_ranges ) {
			$matches = wc_postcode_location_matcher( $postcode, $postcode_ranges, 'tax_rate_id', 'location_code', $country );
			if ( ! empty( $matches ) ) {
				foreach ( $matches as $matched_postcodes ) {
					$postcode_search = array_merge( $postcode_search, $matched_postcodes );
				}
			}
		}

		$postcode_search = array_unique( $postcode_search );

		/**
		 * Location matching criteria - ORed
		 * Needs to match:
		 * - rates with no postcodes and cities
		 * - rates with a matching postcode and city
		 * - rates with matching postcode, no city
		 * - rates with matching city, no postcode
		 */
		$locations_criteria   = array();
		$locations_criteria[] = 'locations.location_type IS NULL';
		$locations_criteria[] = "
			locations.location_type = 'postcode' AND locations.location_code IN ('" . implode( "','", array_map( 'esc_sql', $postcode_search ) ) . "')
			AND (
				( locations2.location_type = 'city' AND locations2.location_code = '" . esc_sql( strtoupper( $city ) ) . "' )
				OR NOT EXISTS (
					SELECT sub.tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rate_locations as sub
					WHERE sub.location_type = 'city'
					AND sub.tax_rate_id = tax_rates.tax_rate_id
				)
			)
		";
		$locations_criteria[] = "
			locations.location_type = 'city' AND locations.location_code = '" . esc_sql( strtoupper( $city ) ) . "'
			AND NOT EXISTS (
				SELECT sub.tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rate_locations as sub
				WHERE sub.location_type = 'postcode'
				AND sub.tax_rate_id = tax_rates.tax_rate_id
			)
		";

		$criteria[] = '( ( ' . implode( ' ) OR ( ', $locations_criteria ) . ' ) )';

		$criteria_string = implode( ' AND ', $criteria );

		// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$found_rates = $wpdb->get_results(
			"
			SELECT tax_rates.*, COUNT( locations.location_id ) as postcode_count, COUNT( locations2.location_id ) as city_count
			FROM {$wpdb->prefix}woocommerce_tax_rates as tax_rates
			LEFT OUTER JOIN {$wpdb->prefix}woocommerce_tax_rate_locations as locations ON tax_rates.tax_rate_id = locations.tax_rate_id
			LEFT OUTER JOIN {$wpdb->prefix}woocommerce_tax_rate_locations as locations2 ON tax_rates.tax_rate_id = locations2.tax_rate_id
			WHERE 1=1 AND {$criteria_string}
			GROUP BY tax_rates.tax_rate_id
			ORDER BY tax_rates.tax_rate_priority
			"
		);
		// phpcs:enable

		$found_rates       = self::sort_rates( $found_rates );
		$matched_tax_rates = array();
		$found_priority    = array();

		foreach ( $found_rates as $found_rate ) {
			if ( in_array( $found_rate->tax_rate_priority, $found_priority, true ) ) {
				continue;
			}

			$matched_tax_rates[ $found_rate->tax_rate_id ] = array(
				'rate'     => (float) $found_rate->tax_rate,
				'label'    => $found_rate->tax_rate_name,
				'shipping' => $found_rate->tax_rate_shipping ? 'yes' : 'no',
				'compound' => $found_rate->tax_rate_compound ? 'yes' : 'no',
			);

			$found_priority[] = $found_rate->tax_rate_priority;
		}

		return apply_filters( 'woocommerce_matched_tax_rates', $matched_tax_rates, $country, $state, $postcode, $city, $tax_class );
	}

	/**
	 * Get the customer tax location based on their status and the current page.
	 *
	 * Used by get_rates(), get_shipping_rates().
	 *
	 * @param  string $tax_class string Optional, passed to the filter for advanced tax setups.
	 * @param  object $customer Override the customer object to get their location.
	 * @return array
	 */
	public static function get_tax_location( $tax_class = '', $customer = null ) {
		$location = array();

		if ( is_null( $customer ) && WC()->customer ) {
			$customer = WC()->customer;
		}

		if ( ! empty( $customer ) ) {
			$location = $customer->get_taxable_address();
		} elseif ( wc_prices_include_tax() || 'base' === get_option( 'woocommerce_default_customer_address' ) || 'base' === get_option( 'woocommerce_tax_based_on' ) ) {
			$location = array(
				WC()->countries->get_base_country(),
				WC()->countries->get_base_state(),
				WC()->countries->get_base_postcode(),
				WC()->countries->get_base_city(),
			);
		}

		return apply_filters( 'woocommerce_get_tax_location', $location, $tax_class, $customer );
	}

	/**
	 * Get's an array of matching rates for a tax class.
	 *
	 * @param string $tax_class Tax class to get rates for.
	 * @param object $customer Override the customer object to get their location.
	 * @return  array
	 */
	public static function get_rates( $tax_class = '', $customer = null ) {
		$tax_class = sanitize_title( $tax_class );
		$location  = self::get_tax_location( $tax_class, $customer );
		return self::get_rates_from_location( $tax_class, $location, $customer );
	}

	/**
	 * Get's an arrau of matching rates from location and tax class. $customer parameter is used to preserve backward compatibility for filter.
	 *
	 * @param string $tax_class Tax class to get rates for.
	 * @param array  $location  Location to compute rates for. Should be in form: array( country, state, postcode, city).
	 * @param object $customer  Only used to maintain backward compatibility for filter `woocommerce-matched_rates`.
	 *
	 * @return mixed|void Tax rates.
	 */
	public static function get_rates_from_location( $tax_class, $location, $customer = null ) {
		$tax_class         = sanitize_title( $tax_class );
		$matched_tax_rates = array();

		if ( count( $location ) === 4 ) {
			list( $country, $state, $postcode, $city ) = $location;

			$matched_tax_rates = self::find_rates(
				array(
					'country'   => $country,
					'state'     => $state,
					'postcode'  => $postcode,
					'city'      => $city,
					'tax_class' => $tax_class,
				)
			);
		}

		return apply_filters( 'woocommerce_matched_rates', $matched_tax_rates, $tax_class, $customer );
	}

	/**
	 * Get's an array of matching rates for the shop's base country.
	 *
	 * @param string $tax_class Tax Class.
	 * @return array
	 */
	public static function get_base_tax_rates( $tax_class = '' ) {
		return apply_filters(
			'woocommerce_base_tax_rates',
			self::find_rates(
				array(
					'country'   => WC()->countries->get_base_country(),
					'state'     => WC()->countries->get_base_state(),
					'postcode'  => WC()->countries->get_base_postcode(),
					'city'      => WC()->countries->get_base_city(),
					'tax_class' => $tax_class,
				)
			),
			$tax_class
		);
	}

	/**
	 * Alias for get_base_tax_rates().
	 *
	 * @deprecated 2.3
	 * @param string $tax_class Tax Class.
	 * @return array
	 */
	public static function get_shop_base_rate( $tax_class = '' ) {
		return self::get_base_tax_rates( $tax_class );
	}

	/**
	 * Gets an array of matching shipping tax rates for a given class.
	 *
	 * @param string $tax_class Tax class to get rates for.
	 * @param object $customer Override the customer object to get their location.
	 * @return mixed
	 */
	public static function get_shipping_tax_rates( $tax_class = null, $customer = null ) {
		// See if we have an explicitly set shipping tax class.
		$shipping_tax_class = get_option( 'woocommerce_shipping_tax_class' );

		if ( 'inherit' !== $shipping_tax_class ) {
			$tax_class = $shipping_tax_class;
		}

		$location          = self::get_tax_location( $tax_class, $customer );
		$matched_tax_rates = array();

		if ( 4 === count( $location ) ) {
			list( $country, $state, $postcode, $city ) = $location;

			if ( ! is_null( $tax_class ) ) {
				// This will be per item shipping.
				$matched_tax_rates = self::find_shipping_rates(
					array(
						'country'   => $country,
						'state'     => $state,
						'postcode'  => $postcode,
						'city'      => $city,
						'tax_class' => $tax_class,
					)
				);

			} elseif ( WC()->cart->get_cart() ) {

				// This will be per order shipping - loop through the order and find the highest tax class rate.
				$cart_tax_classes = WC()->cart->get_cart_item_tax_classes_for_shipping();

				// No tax classes = no taxable items.
				if ( empty( $cart_tax_classes ) ) {
					return array();
				}

				// If multiple classes are found, use the first one found unless a standard rate item is found. This will be the first listed in the 'additional tax class' section.
				if ( count( $cart_tax_classes ) > 1 && ! in_array( '', $cart_tax_classes, true ) ) {
					$tax_classes = self::get_tax_class_slugs();

					foreach ( $tax_classes as $tax_class ) {
						if ( in_array( $tax_class, $cart_tax_classes, true ) ) {
							$matched_tax_rates = self::find_shipping_rates(
								array(
									'country'   => $country,
									'state'     => $state,
									'postcode'  => $postcode,
									'city'      => $city,
									'tax_class' => $tax_class,
								)
							);
							break;
						}
					}
				} elseif ( 1 === count( $cart_tax_classes ) ) {
					// If a single tax class is found, use it.
					$matched_tax_rates = self::find_shipping_rates(
						array(
							'country'   => $country,
							'state'     => $state,
							'postcode'  => $postcode,
							'city'      => $city,
							'tax_class' => $cart_tax_classes[0],
						)
					);
				}
			}

			// Get standard rate if no taxes were found.
			if ( ! count( $matched_tax_rates ) ) {
				$matched_tax_rates = self::find_shipping_rates(
					array(
						'country'  => $country,
						'state'    => $state,
						'postcode' => $postcode,
						'city'     => $city,
					)
				);
			}
		}

		return $matched_tax_rates;
	}

	/**
	 * Return true/false depending on if a rate is a compound rate.
	 *
	 * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format.
	 * @return  bool
	 */
	public static function is_compound( $key_or_rate ) {
		global $wpdb;

		if ( is_object( $key_or_rate ) ) {
			$key      = $key_or_rate->tax_rate_id;
			$compound = $key_or_rate->tax_rate_compound;
		} else {
			$key      = $key_or_rate;
			$compound = (bool) $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_compound FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) );
		}

		return (bool) apply_filters( 'woocommerce_rate_compound', $compound, $key );
	}

	/**
	 * Return a given rates label.
	 *
	 * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format.
	 * @return  string
	 */
	public static function get_rate_label( $key_or_rate ) {
		global $wpdb;

		if ( is_object( $key_or_rate ) ) {
			$key       = $key_or_rate->tax_rate_id;
			$rate_name = $key_or_rate->tax_rate_name;
		} else {
			$key       = $key_or_rate;
			$rate_name = $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_name FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) );
		}

		if ( ! $rate_name ) {
			$rate_name = WC()->countries->tax_or_vat();
		}

		return apply_filters( 'woocommerce_rate_label', $rate_name, $key );
	}

	/**
	 * Return a given rates percent.
	 *
	 * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format.
	 * @return  string
	 */
	public static function get_rate_percent( $key_or_rate ) {
		$rate_percent_value = self::get_rate_percent_value( $key_or_rate );
		$tax_rate_id        = is_object( $key_or_rate ) ? $key_or_rate->tax_rate_id : $key_or_rate;
		return apply_filters( 'woocommerce_rate_percent', $rate_percent_value . '%', $tax_rate_id );
	}

	/**
	 * Return a given rates percent.
	 *
	 * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format.
	 * @return  float
	 */
	public static function get_rate_percent_value( $key_or_rate ) {
		global $wpdb;

		if ( is_object( $key_or_rate ) ) {
			$tax_rate = $key_or_rate->tax_rate;
		} else {
			$key      = $key_or_rate;
			$tax_rate = $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) );
		}

		return floatval( $tax_rate );
	}


	/**
	 * Get a rates code. Code is made up of COUNTRY-STATE-NAME-Priority. E.g GB-VAT-1, US-AL-TAX-1.
	 *
	 * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format.
	 * @return string
	 */
	public static function get_rate_code( $key_or_rate ) {
		global $wpdb;

		if ( is_object( $key_or_rate ) ) {
			$key  = $key_or_rate->tax_rate_id;
			$rate = $key_or_rate;
		} else {
			$key  = $key_or_rate;
			$rate = $wpdb->get_row( $wpdb->prepare( "SELECT tax_rate_country, tax_rate_state, tax_rate_name, tax_rate_priority FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) );
		}

		$code_string = '';

		if ( null !== $rate ) {
			$code        = array();
			$code[]      = $rate->tax_rate_country;
			$code[]      = $rate->tax_rate_state;
			$code[]      = $rate->tax_rate_name ? $rate->tax_rate_name : 'TAX';
			$code[]      = absint( $rate->tax_rate_priority );
			$code_string = strtoupper( implode( '-', array_filter( $code ) ) );
		}

		return apply_filters( 'woocommerce_rate_code', $code_string, $key );
	}

	/**
	 * Sums a set of taxes to form a single total. Values are pre-rounded to precision from 3.6.0.
	 *
	 * @param  array $taxes Array of taxes.
	 * @return float
	 */
	public static function get_tax_total( $taxes ) {
		return array_sum( $taxes );
	}

	/**
	 * Gets all tax rate classes from the database.
	 *
	 * @since 3.7.0
	 * @return array Array of tax class objects consisting of tax_rate_class_id, name, and slug.
	 */
	public static function get_tax_rate_classes() {
		global $wpdb;

		$cache_key        = 'tax-rate-classes';
		$tax_rate_classes = wp_cache_get( $cache_key, 'taxes' );

		if ( ! is_array( $tax_rate_classes ) ) {
			$tax_rate_classes = $wpdb->get_results(
				"
				SELECT * FROM {$wpdb->wc_tax_rate_classes} ORDER BY name;
				"
			);
			wp_cache_set( $cache_key, $tax_rate_classes, 'taxes' );
		}

		return $tax_rate_classes;
	}

	/**
	 * Get store tax class names.
	 *
	 * @return array Array of class names ("Reduced rate", "Zero rate", etc).
	 */
	public static function get_tax_classes() {
		return wp_list_pluck( self::get_tax_rate_classes(), 'name' );
	}

	/**
	 * Get store tax classes as slugs.
	 *
	 * @since  3.0.0
	 * @return array Array of class slugs ("reduced-rate", "zero-rate", etc).
	 */
	public static function get_tax_class_slugs() {
		return wp_list_pluck( self::get_tax_rate_classes(), 'slug' );
	}

	/**
	 * Create a new tax class.
	 *
	 * @since 3.7.0
	 * @param string $name Name of the tax class to add.
	 * @param string $slug (optional) Slug of the tax class to add. Defaults to sanitized name.
	 * @return WP_Error|array Returns name and slug (array) if the tax class is created, or WP_Error if something went wrong.
	 */
	public static function create_tax_class( $name, $slug = '' ) {
		global $wpdb;

		if ( empty( $name ) ) {
			return new WP_Error( 'tax_class_invalid_name', __( 'Tax class requires a valid name', 'woocommerce' ) );
		}

		$existing       = self::get_tax_classes();
		$existing_slugs = self::get_tax_class_slugs();
		$name           = wc_clean( $name );

		if ( in_array( $name, $existing, true ) ) {
			return new WP_Error( 'tax_class_exists', __( 'Tax class already exists', 'woocommerce' ) );
		}

		if ( ! $slug ) {
			$slug = sanitize_title( $name );
		}

		// Stop if there's no slug.
		if ( ! $slug ) {
			return new WP_Error( 'tax_class_slug_invalid', __( 'Tax class slug is invalid', 'woocommerce' ) );
		}

		if ( in_array( $slug, $existing_slugs, true ) ) {
			return new WP_Error( 'tax_class_slug_exists', __( 'Tax class slug already exists', 'woocommerce' ) );
		}

		$insert = $wpdb->insert(
			$wpdb->wc_tax_rate_classes,
			array(
				'name' => $name,
				'slug' => $slug,
			)
		);

		if ( is_wp_error( $insert ) ) {
			return new WP_Error( 'tax_class_insert_error', $insert->get_error_message() );
		}

		wp_cache_delete( 'tax-rate-classes', 'taxes' );

		return array(
			'name' => $name,
			'slug' => $slug,
		);
	}

	/**
	 * Get an existing tax class.
	 *
	 * @since 3.7.0
	 * @param string     $field Field to get by. Valid values are id, name, or slug.
	 * @param string|int $item Item to get.
	 * @return array|bool Returns the tax class as an array. False if not found.
	 */
	public static function get_tax_class_by( $field, $item ) {
		if ( ! in_array( $field, array( 'id', 'name', 'slug' ), true ) ) {
			return new WP_Error( 'invalid_field', __( 'Invalid field', 'woocommerce' ) );
		}

		if ( 'id' === $field ) {
			$field = 'tax_rate_class_id';
		}

		$matches = wp_list_filter(
			self::get_tax_rate_classes(),
			array(
				$field => $item,
			)
		);

		if ( ! $matches ) {
			return false;
		}

		$tax_class = current( $matches );

		return array(
			'name' => $tax_class->name,
			'slug' => $tax_class->slug,
		);
	}

	/**
	 * Delete an existing tax class.
	 *
	 * @since 3.7.0
	 * @param string     $field Field to delete by. Valid values are id, name, or slug.
	 * @param string|int $item Item to delete.
	 * @return WP_Error|bool Returns true if deleted successfully, false if nothing was deleted, or WP_Error if there is an invalid request.
	 */
	public static function delete_tax_class_by( $field, $item ) {
		global $wpdb;

		if ( ! in_array( $field, array( 'id', 'name', 'slug' ), true ) ) {
			return new WP_Error( 'invalid_field', __( 'Invalid field', 'woocommerce' ) );
		}

		$tax_class = self::get_tax_class_by( $field, $item );

		if ( ! $tax_class ) {
			return new WP_Error( 'invalid_tax_class', __( 'Invalid tax class', 'woocommerce' ) );
		}

		if ( 'id' === $field ) {
			$field = 'tax_rate_class_id';
		}

		$delete = $wpdb->delete(
			$wpdb->wc_tax_rate_classes,
			array(
				$field => $item,
			)
		);

		if ( $delete ) {
			// Delete associated tax rates.
			$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_class = %s;", $tax_class['slug'] ) );
			$wpdb->query( "DELETE locations FROM {$wpdb->prefix}woocommerce_tax_rate_locations locations LEFT JOIN {$wpdb->prefix}woocommerce_tax_rates rates ON rates.tax_rate_id = locations.tax_rate_id WHERE rates.tax_rate_id IS NULL;" );
		}

		wp_cache_delete( 'tax-rate-classes', 'taxes' );
		WC_Cache_Helper::invalidate_cache_group( 'taxes' );

		return (bool) $delete;
	}

	/**
	 * Format the city.
	 *
	 * @param  string $city Value to format.
	 * @return string
	 */
	private static function format_tax_rate_city( $city ) {
		return strtoupper( trim( $city ) );
	}

	/**
	 * Format the state.
	 *
	 * @param  string $state Value to format.
	 * @return string
	 */
	private static function format_tax_rate_state( $state ) {
		$state = strtoupper( $state );
		return ( '*' === $state ) ? '' : $state;
	}

	/**
	 * Format the country.
	 *
	 * @param  string $country Value to format.
	 * @return string
	 */
	private static function format_tax_rate_country( $country ) {
		$country = strtoupper( $country );
		return ( '*' === $country ) ? '' : $country;
	}

	/**
	 * Format the tax rate name.
	 *
	 * @param  string $name Value to format.
	 * @return string
	 */
	private static function format_tax_rate_name( $name ) {
		return $name ? $name : __( 'Tax', 'woocommerce' );
	}

	/**
	 * Format the rate.
	 *
	 * @param  float $rate Value to format.
	 * @return string
	 */
	private static function format_tax_rate( $rate ) {
		return number_format( (float) $rate, 4, '.', '' );
	}

	/**
	 * Format the priority.
	 *
	 * @param  string $priority Value to format.
	 * @return int
	 */
	private static function format_tax_rate_priority( $priority ) {
		return absint( $priority );
	}

	/**
	 * Format the class.
	 *
	 * @param  string $class Value to format.
	 * @return string
	 */
	public static function format_tax_rate_class( $class ) {
		$class   = sanitize_title( $class );
		$classes = self::get_tax_class_slugs();
		if ( ! in_array( $class, $classes, true ) ) {
			$class = '';
		}
		return ( 'standard' === $class ) ? '' : $class;
	}

	/**
	 * Prepare and format tax rate for DB insertion.
	 *
	 * @param  array $tax_rate Tax rate to format.
	 * @return array
	 */
	private static function prepare_tax_rate( $tax_rate ) {
		foreach ( $tax_rate as $key => $value ) {
			if ( method_exists( __CLASS__, 'format_' . $key ) ) {
				if ( 'tax_rate_state' === $key ) {
					$tax_rate[ $key ] = call_user_func( array( __CLASS__, 'format_' . $key ), sanitize_key( $value ) );
				} else {
					$tax_rate[ $key ] = call_user_func( array( __CLASS__, 'format_' . $key ), $value );
				}
			}
		}
		return $tax_rate;
	}

	/**
	 * Insert a new tax rate.
	 *
	 * Internal use only.
	 *
	 * @since 2.3.0
	 *
	 * @param  array $tax_rate Tax rate to insert.
	 * @return int tax rate id
	 */
	public static function _insert_tax_rate( $tax_rate ) {
		global $wpdb;

		$wpdb->insert( $wpdb->prefix . 'woocommerce_tax_rates', self::prepare_tax_rate( $tax_rate ) );

		$tax_rate_id = $wpdb->insert_id;

		WC_Cache_Helper::invalidate_cache_group( 'taxes' );

		do_action( 'woocommerce_tax_rate_added', $tax_rate_id, $tax_rate );

		return $tax_rate_id;
	}

	/**
	 * Get tax rate.
	 *
	 * Internal use only.
	 *
	 * @since 2.5.0
	 *
	 * @param int    $tax_rate_id Tax rate ID.
	 * @param string $output_type Type of output.
	 * @return array|object
	 */
	public static function _get_tax_rate( $tax_rate_id, $output_type = ARRAY_A ) {
		global $wpdb;

		return $wpdb->get_row(
			$wpdb->prepare(
				"
					SELECT *
					FROM {$wpdb->prefix}woocommerce_tax_rates
					WHERE tax_rate_id = %d
				",
				$tax_rate_id
			),
			$output_type
		);
	}

	/**
	 * Update a tax rate.
	 *
	 * Internal use only.
	 *
	 * @since 2.3.0
	 *
	 * @param int   $tax_rate_id Tax rate to update.
	 * @param array $tax_rate Tax rate values.
	 */
	public static function _update_tax_rate( $tax_rate_id, $tax_rate ) {
		global $wpdb;

		$tax_rate_id = absint( $tax_rate_id );

		$wpdb->update(
			$wpdb->prefix . 'woocommerce_tax_rates',
			self::prepare_tax_rate( $tax_rate ),
			array(
				'tax_rate_id' => $tax_rate_id,
			)
		);

		WC_Cache_Helper::invalidate_cache_group( 'taxes' );

		do_action( 'woocommerce_tax_rate_updated', $tax_rate_id, $tax_rate );
	}

	/**
	 * Delete a tax rate from the database.
	 *
	 * Internal use only.
	 *
	 * @since 2.3.0
	 * @param  int $tax_rate_id Tax rate to delete.
	 */
	public static function _delete_tax_rate( $tax_rate_id ) {
		global $wpdb;

		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE tax_rate_id = %d;", $tax_rate_id ) );
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %d;", $tax_rate_id ) );

		WC_Cache_Helper::invalidate_cache_group( 'taxes' );

		do_action( 'woocommerce_tax_rate_deleted', $tax_rate_id );
	}

	/**
	 * Update postcodes for a tax rate in the DB.
	 *
	 * Internal use only.
	 *
	 * @since 2.3.0
	 *
	 * @param int    $tax_rate_id Tax rate to update.
	 * @param string $postcodes String of postcodes separated by ; characters.
	 */
	public static function _update_tax_rate_postcodes( $tax_rate_id, $postcodes ) {
		if ( ! is_array( $postcodes ) ) {
			$postcodes = explode( ';', $postcodes );
		}
		// No normalization - postcodes are matched against both normal and formatted versions to support wildcards.
		foreach ( $postcodes as $key => $postcode ) {
			$postcodes[ $key ] = strtoupper( trim( str_replace( chr( 226 ) . chr( 128 ) . chr( 166 ), '...', $postcode ) ) );
		}
		self::update_tax_rate_locations( $tax_rate_id, array_diff( array_filter( $postcodes ), array( '*' ) ), 'postcode' );
	}

	/**
	 * Update cities for a tax rate in the DB.
	 *
	 * Internal use only.
	 *
	 * @since 2.3.0
	 *
	 * @param int    $tax_rate_id Tax rate to update.
	 * @param string $cities Cities to set.
	 */
	public static function _update_tax_rate_cities( $tax_rate_id, $cities ) {
		if ( ! is_array( $cities ) ) {
			$cities = explode( ';', $cities );
		}
		$cities = array_filter( array_diff( array_map( array( __CLASS__, 'format_tax_rate_city' ), $cities ), array( '*' ) ) );

		self::update_tax_rate_locations( $tax_rate_id, $cities, 'city' );
	}

	/**
	 * Updates locations (postcode and city).
	 *
	 * Internal use only.
	 *
	 * @since 2.3.0
	 *
	 * @param int    $tax_rate_id Tax rate ID to update.
	 * @param array  $values Values to set.
	 * @param string $type Location type.
	 */
	private static function update_tax_rate_locations( $tax_rate_id, $values, $type ) {
		global $wpdb;

		$tax_rate_id = absint( $tax_rate_id );

		$wpdb->query(
			$wpdb->prepare(
				"DELETE FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE tax_rate_id = %d AND location_type = %s;",
				$tax_rate_id,
				$type
			)
		);

		if ( count( $values ) > 0 ) {
			$sql = "( '" . implode( "', $tax_rate_id, '" . esc_sql( $type ) . "' ),( '", array_map( 'esc_sql', $values ) ) . "', $tax_rate_id, '" . esc_sql( $type ) . "' )";

			$wpdb->query( "INSERT INTO {$wpdb->prefix}woocommerce_tax_rate_locations ( location_code, tax_rate_id, location_type ) VALUES $sql;" ); // @codingStandardsIgnoreLine.
		}

		WC_Cache_Helper::invalidate_cache_group( 'taxes' );
	}

	/**
	 * Used by admin settings page.
	 *
	 * @param string $tax_class Tax class slug.
	 *
	 * @return array|null|object
	 */
	public static function get_rates_for_tax_class( $tax_class ) {
		global $wpdb;

		$tax_class = self::format_tax_rate_class( $tax_class );

		// Get all the rates and locations. Snagging all at once should significantly cut down on the number of queries.
		$rates     = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$wpdb->prefix}woocommerce_tax_rates` WHERE `tax_rate_class` = %s;", $tax_class ) );
		$locations = $wpdb->get_results( "SELECT * FROM `{$wpdb->prefix}woocommerce_tax_rate_locations`" );

		if ( ! empty( $rates ) ) {
			// Set the rates keys equal to their ids.
			$rates = array_combine( wp_list_pluck( $rates, 'tax_rate_id' ), $rates );
		}

		// Drop the locations into the rates array.
		foreach ( $locations as $location ) {
			// Don't set them for unexistent rates.
			if ( ! isset( $rates[ $location->tax_rate_id ] ) ) {
				continue;
			}
			// If the rate exists, initialize the array before appending to it.
			if ( ! isset( $rates[ $location->tax_rate_id ]->{$location->location_type} ) ) {
				$rates[ $location->tax_rate_id ]->{$location->location_type} = array();
			}
			$rates[ $location->tax_rate_id ]->{$location->location_type}[] = $location->location_code;
		}

		foreach ( $rates as $rate_id => $rate ) {
			$rates[ $rate_id ]->postcode_count = isset( $rates[ $rate_id ]->postcode ) ? count( $rates[ $rate_id ]->postcode ) : 0;
			$rates[ $rate_id ]->city_count     = isset( $rates[ $rate_id ]->city ) ? count( $rates[ $rate_id ]->city ) : 0;
		}

		$rates = self::sort_rates( $rates );

		return $rates;
	}
}
WC_Tax::init();
wc-webhook-functions.php000064400000013143151336044760011343 0ustar00<?php
/**
 * WooCommerce Webhook functions
 *
 * @package WooCommerce\Functions
 * @version 3.3.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Process the web hooks at the end of the request.
 *
 * @since 4.4.0
 */
function wc_webhook_execute_queue() {
	global $wc_queued_webhooks;
	if ( empty( $wc_queued_webhooks ) ) {
		return;
	}

	foreach ( $wc_queued_webhooks as $data ) {
		// Webhooks are processed in the background by default
		// so as to avoid delays or failures in delivery from affecting the
		// user who triggered it.
		if ( apply_filters( 'woocommerce_webhook_deliver_async', true, $data['webhook'], $data['arg'] ) ) {

			$queue_args = array(
				'webhook_id' => $data['webhook']->get_id(),
				'arg'        => $data['arg'],
			);

			$next_scheduled_date = WC()->queue()->get_next( 'woocommerce_deliver_webhook_async', $queue_args, 'woocommerce-webhooks' );

			// Make webhooks unique - only schedule one webhook every 10 minutes to maintain backward compatibility with WP Cron behaviour seen in WC < 3.5.0.
			if ( is_null( $next_scheduled_date ) || $next_scheduled_date->getTimestamp() >= ( 600 + gmdate( 'U' ) ) ) {
				WC()->queue()->add( 'woocommerce_deliver_webhook_async', $queue_args, 'woocommerce-webhooks' );
			}
		} else {
			// Deliver immediately.
			$data['webhook']->deliver( $data['arg'] );
		}
	}
}
add_action( 'shutdown', 'wc_webhook_execute_queue' );

/**
 * Process webhook delivery.
 *
 * @since 3.3.0
 * @param WC_Webhook $webhook Webhook instance.
 * @param array      $arg     Delivery arguments.
 */
function wc_webhook_process_delivery( $webhook, $arg ) {
	// We need to queue the webhook so that it can be ran after the request has finished processing.
	global $wc_queued_webhooks;
	if ( ! isset( $wc_queued_webhooks ) ) {
		$wc_queued_webhooks = array();
	}
	$wc_queued_webhooks[] = array(
		'webhook' => $webhook,
		'arg'     => $arg,
	);
}
add_action( 'woocommerce_webhook_process_delivery', 'wc_webhook_process_delivery', 10, 2 );

/**
 * Wrapper function to execute the `woocommerce_deliver_webhook_async` cron.
 * hook, see WC_Webhook::process().
 *
 * @since 2.2.0
 * @param int   $webhook_id Webhook ID to deliver.
 * @throws Exception        If webhook cannot be read/found and $data parameter of WC_Webhook class constructor is set.
 * @param mixed $arg        Hook argument.
 */
function wc_deliver_webhook_async( $webhook_id, $arg ) {
	$webhook = new WC_Webhook( $webhook_id );
	$webhook->deliver( $arg );
}
add_action( 'woocommerce_deliver_webhook_async', 'wc_deliver_webhook_async', 10, 2 );

/**
 * Check if the given topic is a valid webhook topic, a topic is valid if:
 *
 * + starts with `action.woocommerce_` or `action.wc_`.
 * + it has a valid resource & event.
 *
 * @since  2.2.0
 * @param  string $topic Webhook topic.
 * @return bool
 */
function wc_is_webhook_valid_topic( $topic ) {
	$invalid_topics = array(
		'action.woocommerce_login_credentials',
		'action.woocommerce_product_csv_importer_check_import_file_path',
		'action.woocommerce_webhook_should_deliver',
	);

	if ( in_array( $topic, $invalid_topics, true ) ) {
		return false;
	}

	// Custom topics are prefixed with woocommerce_ or wc_ are valid.
	if ( 0 === strpos( $topic, 'action.woocommerce_' ) || 0 === strpos( $topic, 'action.wc_' ) ) {
		return true;
	}

	$data = explode( '.', $topic );

	if ( ! isset( $data[0] ) || ! isset( $data[1] ) ) {
		return false;
	}

	$valid_resources = apply_filters( 'woocommerce_valid_webhook_resources', array( 'coupon', 'customer', 'order', 'product' ) );
	$valid_events    = apply_filters( 'woocommerce_valid_webhook_events', array( 'created', 'updated', 'deleted', 'restored' ) );

	if ( in_array( $data[0], $valid_resources, true ) && in_array( $data[1], $valid_events, true ) ) {
		return true;
	}

	return false;
}

/**
 * Check if given status is a valid webhook status.
 *
 * @since 3.5.3
 * @param string $status Status to check.
 * @return bool
 */
function wc_is_webhook_valid_status( $status ) {
	return in_array( $status, array_keys( wc_get_webhook_statuses() ), true );
}

/**
 * Get Webhook statuses.
 *
 * @since  2.3.0
 * @return array
 */
function wc_get_webhook_statuses() {
	return apply_filters(
		'woocommerce_webhook_statuses',
		array(
			'active'   => __( 'Active', 'woocommerce' ),
			'paused'   => __( 'Paused', 'woocommerce' ),
			'disabled' => __( 'Disabled', 'woocommerce' ),
		)
	);
}

/**
 * Load webhooks.
 *
 * @since  3.3.0
 * @throws Exception If webhook cannot be read/found and $data parameter of WC_Webhook class constructor is set.
 * @param  string   $status Optional - status to filter results by. Must be a key in return value of @see wc_get_webhook_statuses(). @since 3.5.0.
 * @param  null|int $limit Limit number of webhooks loaded. @since 3.6.0.
 * @return bool
 */
function wc_load_webhooks( $status = '', $limit = null ) {
	$data_store = WC_Data_Store::load( 'webhook' );
	$webhooks   = $data_store->get_webhooks_ids( $status );
	$loaded     = 0;

	foreach ( $webhooks as $webhook_id ) {
		$webhook = new WC_Webhook( $webhook_id );
		$webhook->enqueue();
		$loaded ++;

		if ( ! is_null( $limit ) && $loaded >= $limit ) {
			break;
		}
	}

	return 0 < $loaded;
}

/**
 * Get webhook.
 *
 * @param  int|WC_Webhook $id Webhook ID or object.
 * @throws Exception          If webhook cannot be read/found and $data parameter of WC_Webhook class constructor is set.
 * @return WC_Webhook|null
 */
function wc_get_webhook( $id ) {
	$webhook = new WC_Webhook( $id );

	return 0 !== $webhook->get_id() ? $webhook : null;
}

/**
 * Get webhoook REST API versions.
 *
 * @since 3.5.1
 * @return array
 */
function wc_get_webhook_rest_api_versions() {
	return array(
		'wp_api_v1',
		'wp_api_v2',
		'wp_api_v3',
	);
}
class-wc-order-item-shipping.php000064400000017323151336044760012674 0ustar00<?php
/**
 * Order Line Item (shipping)
 *
 * @package WooCommerce\Classes
 * @version 3.0.0
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Order item shipping class.
 */
class WC_Order_Item_Shipping extends WC_Order_Item {

	/**
	 * Order Data array. This is the core order data exposed in APIs since 3.0.0.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $extra_data = array(
		'method_title' => '',
		'method_id'    => '',
		'instance_id'  => '',
		'total'        => 0,
		'total_tax'    => 0,
		'taxes'        => array(
			'total' => array(),
		),
	);

	/**
	 * Calculate item taxes.
	 *
	 * @since  3.2.0
	 * @param  array $calculate_tax_for Location data to get taxes for. Required.
	 * @return bool  True if taxes were calculated.
	 */
	public function calculate_taxes( $calculate_tax_for = array() ) {
		if ( ! isset( $calculate_tax_for['country'], $calculate_tax_for['state'], $calculate_tax_for['postcode'], $calculate_tax_for['city'], $calculate_tax_for['tax_class'] ) ) {
			return false;
		}
		if ( wc_tax_enabled() ) {
			$tax_rates = WC_Tax::find_shipping_rates( $calculate_tax_for );
			$taxes     = WC_Tax::calc_tax( $this->get_total(), $tax_rates, false );
			$this->set_taxes( array( 'total' => $taxes ) );
		} else {
			$this->set_taxes( false );
		}

		do_action( 'woocommerce_order_item_shipping_after_calculate_taxes', $this, $calculate_tax_for );

		return true;
	}

	/*
	|--------------------------------------------------------------------------
	| Setters
	|--------------------------------------------------------------------------
	*/

	/**
	 * Set order item name.
	 *
	 * @param string $value Value to set.
	 * @throws WC_Data_Exception May throw exception if data is invalid.
	 */
	public function set_name( $value ) {
		$this->set_method_title( $value );
	}

	/**
	 * Set method title.
	 *
	 * @param string $value Value to set.
	 * @throws WC_Data_Exception May throw exception if data is invalid.
	 */
	public function set_method_title( $value ) {
		$this->set_prop( 'name', wc_clean( $value ) );
		$this->set_prop( 'method_title', wc_clean( $value ) );
	}

	/**
	 * Set shipping method id.
	 *
	 * @param string $value Value to set.
	 * @throws WC_Data_Exception May throw exception if data is invalid.
	 */
	public function set_method_id( $value ) {
		$this->set_prop( 'method_id', wc_clean( $value ) );
	}

	/**
	 * Set shipping instance id.
	 *
	 * @param string $value Value to set.
	 * @throws WC_Data_Exception May throw exception if data is invalid.
	 */
	public function set_instance_id( $value ) {
		$this->set_prop( 'instance_id', wc_clean( $value ) );
	}

	/**
	 * Set total.
	 *
	 * @param string $value Value to set.
	 * @throws WC_Data_Exception May throw exception if data is invalid.
	 */
	public function set_total( $value ) {
		$this->set_prop( 'total', wc_format_decimal( $value ) );
	}

	/**
	 * Set total tax.
	 *
	 * @param string $value Value to set.
	 * @throws WC_Data_Exception May throw exception if data is invalid.
	 */
	protected function set_total_tax( $value ) {
		$this->set_prop( 'total_tax', wc_format_decimal( $value ) );
	}

	/**
	 * Set taxes.
	 *
	 * This is an array of tax ID keys with total amount values.
	 *
	 * @param array $raw_tax_data Value to set.
	 * @throws WC_Data_Exception May throw exception if data is invalid.
	 */
	public function set_taxes( $raw_tax_data ) {
		$raw_tax_data = maybe_unserialize( $raw_tax_data );
		$tax_data     = array(
			'total' => array(),
		);
		if ( isset( $raw_tax_data['total'] ) ) {
			$tax_data['total'] = array_map( 'wc_format_decimal', $raw_tax_data['total'] );
		} elseif ( ! empty( $raw_tax_data ) && is_array( $raw_tax_data ) ) {
			// Older versions just used an array.
			$tax_data['total'] = array_map( 'wc_format_decimal', $raw_tax_data );
		}
		$this->set_prop( 'taxes', $tax_data );

		if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) {
			$this->set_total_tax( array_sum( $tax_data['total'] ) );
		} else {
			$this->set_total_tax( array_sum( array_map( 'wc_round_tax_total', $tax_data['total'] ) ) );
		}
	}

	/**
	 * Set properties based on passed in shipping rate object.
	 *
	 * @param WC_Shipping_Rate $shipping_rate Shipping rate to set.
	 */
	public function set_shipping_rate( $shipping_rate ) {
		$this->set_method_title( $shipping_rate->get_label() );
		$this->set_method_id( $shipping_rate->get_method_id() );
		$this->set_instance_id( $shipping_rate->get_instance_id() );
		$this->set_total( $shipping_rate->get_cost() );
		$this->set_taxes( $shipping_rate->get_taxes() );
		$this->set_meta_data( $shipping_rate->get_meta_data() );
	}

	/*
	|--------------------------------------------------------------------------
	| Getters
	|--------------------------------------------------------------------------
	*/

	/**
	 * Get order item type.
	 *
	 * @return string
	 */
	public function get_type() {
		return 'shipping';
	}

	/**
	 * Get order item name.
	 *
	 * @param  string $context View or edit context.
	 * @return string
	 */
	public function get_name( $context = 'view' ) {
		return $this->get_method_title( $context );
	}

	/**
	 * Get title.
	 *
	 * @param  string $context View or edit context.
	 * @return string
	 */
	public function get_method_title( $context = 'view' ) {
		$method_title = $this->get_prop( 'method_title', $context );
		if ( 'view' === $context ) {
			return $method_title ? $method_title : __( 'Shipping', 'woocommerce' );
		} else {
			return $method_title;
		}
	}

	/**
	 * Get method ID.
	 *
	 * @param  string $context View or edit context.
	 * @return string
	 */
	public function get_method_id( $context = 'view' ) {
		return $this->get_prop( 'method_id', $context );
	}

	/**
	 * Get instance ID.
	 *
	 * @param  string $context View or edit context.
	 * @return string
	 */
	public function get_instance_id( $context = 'view' ) {
		return $this->get_prop( 'instance_id', $context );
	}

	/**
	 * Get total cost.
	 *
	 * @param  string $context View or edit context.
	 * @return string
	 */
	public function get_total( $context = 'view' ) {
		return $this->get_prop( 'total', $context );
	}

	/**
	 * Get total tax.
	 *
	 * @param  string $context View or edit context.
	 * @return string
	 */
	public function get_total_tax( $context = 'view' ) {
		return $this->get_prop( 'total_tax', $context );
	}

	/**
	 * Get taxes.
	 *
	 * @param  string $context View or edit context.
	 * @return array
	 */
	public function get_taxes( $context = 'view' ) {
		return $this->get_prop( 'taxes', $context );
	}

	/**
	 * Get tax class.
	 *
	 * @param  string $context View or edit context.
	 * @return string
	 */
	public function get_tax_class( $context = 'view' ) {
		return get_option( 'woocommerce_shipping_tax_class' );
	}

	/*
	|--------------------------------------------------------------------------
	| Array Access Methods
	|--------------------------------------------------------------------------
	|
	| For backwards compatibility with legacy arrays.
	|
	*/

	/**
	 * Offset get: for ArrayAccess/Backwards compatibility.
	 *
	 * @param string $offset Key.
	 * @return mixed
	 */
	public function offsetGet( $offset ) {
		if ( 'cost' === $offset ) {
			$offset = 'total';
		}
		return parent::offsetGet( $offset );
	}

	/**
	 * Offset set: for ArrayAccess/Backwards compatibility.
	 *
	 * @deprecated 4.4.0
	 * @param string $offset Key.
	 * @param mixed  $value Value to set.
	 */
	public function offsetSet( $offset, $value ) {
		wc_deprecated_function( 'WC_Order_Item_Shipping::offsetSet', '4.4.0', '' );
		if ( 'cost' === $offset ) {
			$offset = 'total';
		}
		parent::offsetSet( $offset, $value );
	}

	/**
	 * Offset exists: for ArrayAccess.
	 *
	 * @param string $offset Key.
	 * @return bool
	 */
	public function offsetExists( $offset ) {
		if ( in_array( $offset, array( 'cost' ), true ) ) {
			return true;
		}
		return parent::offsetExists( $offset );
	}
}
walkers/class-wc-product-cat-dropdown-walker.php000064400000006004151336044760016012 0ustar00<?php
/**
 * WC_Product_Cat_Dropdown_Walker class
 *
 * @package WooCommerce\Classes\Walkers
 * @version 3.4.0
 */

defined( 'ABSPATH' ) || exit;

if ( class_exists( 'WC_Product_Cat_Dropdown_Walker', false ) ) {
	return;
}

/**
 * Product category dropdown walker class.
 */
class WC_Product_Cat_Dropdown_Walker extends Walker {

	/**
	 * What the class handles.
	 *
	 * @var string
	 */
	public $tree_type = 'category';

	/**
	 * DB fields to use.
	 *
	 * @var array
	 */
	public $db_fields = array(
		'parent' => 'parent',
		'id'     => 'term_id',
		'slug'   => 'slug',
	);

	/**
	 * Starts the list before the elements are added.
	 *
	 * @see Walker::start_el()
	 * @since 2.1.0
	 *
	 * @param string $output            Passed by reference. Used to append additional content.
	 * @param object $cat               Category.
	 * @param int    $depth             Depth of category in reference to parents.
	 * @param array  $args              Arguments.
	 * @param int    $current_object_id Current object ID.
	 */
	public function start_el( &$output, $cat, $depth = 0, $args = array(), $current_object_id = 0 ) {

		if ( ! empty( $args['hierarchical'] ) ) {
			$pad = str_repeat( '&nbsp;', $depth * 3 );
		} else {
			$pad = '';
		}

		$cat_name = apply_filters( 'list_product_cats', $cat->name, $cat );
		$value    = ( isset( $args['value'] ) && 'id' === $args['value'] ) ? $cat->term_id : $cat->slug;
		$output  .= "\t<option class=\"level-$depth\" value=\"" . esc_attr( $value ) . '"';

		if ( $value === $args['selected'] || ( is_array( $args['selected'] ) && in_array( $value, $args['selected'], true ) ) ) {
			$output .= ' selected="selected"';
		}

		$output .= '>';
		$output .= esc_html( $pad . $cat_name );

		if ( ! empty( $args['show_count'] ) ) {
			$output .= '&nbsp;(' . absint( $cat->count ) . ')';
		}

		$output .= "</option>\n";
	}

	/**
	 * Traverse elements to create list from elements.
	 *
	 * Display one element if the element doesn't have any children otherwise,
	 * display the element and its children. Will only traverse up to the max.
	 * depth and no ignore elements under that depth. It is possible to set the.
	 * max depth to include all depths, see walk() method.
	 *
	 * This method shouldn't be called directly, use the walk() method instead.
	 *
	 * @since 2.5.0
	 *
	 * @param object $element           Data object.
	 * @param array  $children_elements List of elements to continue traversing.
	 * @param int    $max_depth         Max depth to traverse.
	 * @param int    $depth             Depth of current element.
	 * @param array  $args              Arguments.
	 * @param string $output            Passed by reference. Used to append additional content.
	 * @return null Null on failure with no changes to parameters.
	 */
	public function display_element( $element, &$children_elements, $max_depth, $depth, $args, &$output ) {
		if ( ! $element || ( 0 === $element->count && ! empty( $args[0]['hide_empty'] ) ) ) {
			return;
		}
		parent::display_element( $element, $children_elements, $max_depth, $depth, $args, $output );
	}
}
walkers/class-wc-product-cat-list-walker.php000064400000011047151336044760015134 0ustar00<?php
/**
 * WC_Product_Cat_List_Walker class
 *
 * @package WooCommerce\Classes\Walkers
 * @version 3.4.0
 */

defined( 'ABSPATH' ) || exit;

if ( class_exists( 'WC_Product_Cat_List_Walker', false ) ) {
	return;
}

/**
 * Product cat list walker class.
 */
class WC_Product_Cat_List_Walker extends Walker {

	/**
	 * What the class handles.
	 *
	 * @var string
	 */
	public $tree_type = 'product_cat';

	/**
	 * DB fields to use.
	 *
	 * @var array
	 */
	public $db_fields = array(
		'parent' => 'parent',
		'id'     => 'term_id',
		'slug'   => 'slug',
	);

	/**
	 * Starts the list before the elements are added.
	 *
	 * @see Walker::start_lvl()
	 * @since 2.1.0
	 *
	 * @param string $output Passed by reference. Used to append additional content.
	 * @param int    $depth Depth of category. Used for tab indentation.
	 * @param array  $args Will only append content if style argument value is 'list'.
	 */
	public function start_lvl( &$output, $depth = 0, $args = array() ) {
		if ( 'list' !== $args['style'] ) {
			return;
		}

		$indent  = str_repeat( "\t", $depth );
		$output .= "$indent<ul class='children'>\n";
	}

	/**
	 * Ends the list of after the elements are added.
	 *
	 * @see Walker::end_lvl()
	 * @since 2.1.0
	 *
	 * @param string $output Passed by reference. Used to append additional content.
	 * @param int    $depth Depth of category. Used for tab indentation.
	 * @param array  $args Will only append content if style argument value is 'list'.
	 */
	public function end_lvl( &$output, $depth = 0, $args = array() ) {
		if ( 'list' !== $args['style'] ) {
			return;
		}

		$indent  = str_repeat( "\t", $depth );
		$output .= "$indent</ul>\n";
	}

	/**
	 * Start the element output.
	 *
	 * @see Walker::start_el()
	 * @since 2.1.0
	 *
	 * @param string  $output            Passed by reference. Used to append additional content.
	 * @param object  $cat               Category.
	 * @param int     $depth             Depth of category in reference to parents.
	 * @param array   $args              Arguments.
	 * @param integer $current_object_id Current object ID.
	 */
	public function start_el( &$output, $cat, $depth = 0, $args = array(), $current_object_id = 0 ) {
		$cat_id = intval( $cat->term_id );

		$output .= '<li class="cat-item cat-item-' . $cat_id;

		if ( $args['current_category'] === $cat_id ) {
			$output .= ' current-cat';
		}

		if ( $args['has_children'] && $args['hierarchical'] && ( empty( $args['max_depth'] ) || $args['max_depth'] > $depth + 1 ) ) {
			$output .= ' cat-parent';
		}

		if ( $args['current_category_ancestors'] && $args['current_category'] && in_array( $cat_id, $args['current_category_ancestors'], true ) ) {
			$output .= ' current-cat-parent';
		}

		$output .= '"><a href="' . get_term_link( $cat_id, $this->tree_type ) . '">' . apply_filters( 'list_product_cats', $cat->name, $cat ) . '</a>';

		if ( $args['show_count'] ) {
			$output .= ' <span class="count">(' . $cat->count . ')</span>';
		}
	}

	/**
	 * Ends the element output, if needed.
	 *
	 * @see Walker::end_el()
	 * @since 2.1.0
	 *
	 * @param string $output Passed by reference. Used to append additional content.
	 * @param object $cat    Category.
	 * @param int    $depth  Depth of category. Not used.
	 * @param array  $args   Only uses 'list' for whether should append to output.
	 */
	public function end_el( &$output, $cat, $depth = 0, $args = array() ) {
		$output .= "</li>\n";
	}

	/**
	 * Traverse elements to create list from elements.
	 *
	 * Display one element if the element doesn't have any children otherwise,
	 * display the element and its children. Will only traverse up to the max.
	 * depth and no ignore elements under that depth. It is possible to set the.
	 * max depth to include all depths, see walk() method.
	 *
	 * This method shouldn't be called directly, use the walk() method instead.
	 *
	 * @since 2.5.0
	 *
	 * @param object $element           Data object.
	 * @param array  $children_elements List of elements to continue traversing.
	 * @param int    $max_depth         Max depth to traverse.
	 * @param int    $depth             Depth of current element.
	 * @param array  $args              Arguments.
	 * @param string $output            Passed by reference. Used to append additional content.
	 * @return null Null on failure with no changes to parameters.
	 */
	public function display_element( $element, &$children_elements, $max_depth, $depth, $args, &$output ) {
		if ( ! $element || ( 0 === $element->count && ! empty( $args[0]['hide_empty'] ) ) ) {
			return;
		}
		parent::display_element( $element, $children_elements, $max_depth, $depth, $args, $output );
	}
}
walkers/class-product-cat-dropdown-walker.php000064400000000347151336044760015407 0ustar00<?php
/**
 * Legacy WC_Product_Cat_Dropdown_Walker file
 *
 * @package WooCommerce\Classes\Walkers
 * @deprecated 3.4.0
 */

defined( 'ABSPATH' ) || exit;

require dirname( __FILE__ ) . '/class-wc-product-cat-dropdown-walker.php';
walkers/class-product-cat-list-walker.php000064400000000337151336044760014525 0ustar00<?php
/**
 * Legacy WC_Product_Cat_List_Walker file
 *
 * @package WooCommerce\Classes\Walkers
 * @deprecated 3.4.0
 */

defined( 'ABSPATH' ) || exit;

require dirname( __FILE__ ) . '/class-wc-product-cat-list-walker.php';
class-wc-shortcodes.php000064400000042360151336044760011162 0ustar00<?php
/**
 * Shortcodes
 *
 * @package WooCommerce\Classes
 * @version 3.2.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * WooCommerce Shortcodes class.
 */
class WC_Shortcodes {

	/**
	 * Init shortcodes.
	 */
	public static function init() {
		$shortcodes = array(
			'product'                    => __CLASS__ . '::product',
			'product_page'               => __CLASS__ . '::product_page',
			'product_category'           => __CLASS__ . '::product_category',
			'product_categories'         => __CLASS__ . '::product_categories',
			'add_to_cart'                => __CLASS__ . '::product_add_to_cart',
			'add_to_cart_url'            => __CLASS__ . '::product_add_to_cart_url',
			'products'                   => __CLASS__ . '::products',
			'recent_products'            => __CLASS__ . '::recent_products',
			'sale_products'              => __CLASS__ . '::sale_products',
			'best_selling_products'      => __CLASS__ . '::best_selling_products',
			'top_rated_products'         => __CLASS__ . '::top_rated_products',
			'featured_products'          => __CLASS__ . '::featured_products',
			'product_attribute'          => __CLASS__ . '::product_attribute',
			'related_products'           => __CLASS__ . '::related_products',
			'shop_messages'              => __CLASS__ . '::shop_messages',
			'woocommerce_order_tracking' => __CLASS__ . '::order_tracking',
			'woocommerce_cart'           => __CLASS__ . '::cart',
			'woocommerce_checkout'       => __CLASS__ . '::checkout',
			'woocommerce_my_account'     => __CLASS__ . '::my_account',
		);

		foreach ( $shortcodes as $shortcode => $function ) {
			add_shortcode( apply_filters( "{$shortcode}_shortcode_tag", $shortcode ), $function );
		}

		// Alias for pre 2.1 compatibility.
		add_shortcode( 'woocommerce_messages', __CLASS__ . '::shop_messages' );
	}

	/**
	 * Shortcode Wrapper.
	 *
	 * @param string[] $function Callback function.
	 * @param array    $atts     Attributes. Default to empty array.
	 * @param array    $wrapper  Customer wrapper data.
	 *
	 * @return string
	 */
	public static function shortcode_wrapper(
		$function,
		$atts = array(),
		$wrapper = array(
			'class'  => 'woocommerce',
			'before' => null,
			'after'  => null,
		)
	) {
		ob_start();

		// @codingStandardsIgnoreStart
		echo empty( $wrapper['before'] ) ? '<div class="' . esc_attr( $wrapper['class'] ) . '">' : $wrapper['before'];
		call_user_func( $function, $atts );
		echo empty( $wrapper['after'] ) ? '</div>' : $wrapper['after'];
		// @codingStandardsIgnoreEnd

		return ob_get_clean();
	}

	/**
	 * Cart page shortcode.
	 *
	 * @return string
	 */
	public static function cart() {
		return is_null( WC()->cart ) ? '' : self::shortcode_wrapper( array( 'WC_Shortcode_Cart', 'output' ) );
	}

	/**
	 * Checkout page shortcode.
	 *
	 * @param array $atts Attributes.
	 * @return string
	 */
	public static function checkout( $atts ) {
		return self::shortcode_wrapper( array( 'WC_Shortcode_Checkout', 'output' ), $atts );
	}

	/**
	 * Order tracking page shortcode.
	 *
	 * @param array $atts Attributes.
	 * @return string
	 */
	public static function order_tracking( $atts ) {
		return self::shortcode_wrapper( array( 'WC_Shortcode_Order_Tracking', 'output' ), $atts );
	}

	/**
	 * My account page shortcode.
	 *
	 * @param array $atts Attributes.
	 * @return string
	 */
	public static function my_account( $atts ) {
		return self::shortcode_wrapper( array( 'WC_Shortcode_My_Account', 'output' ), $atts );
	}

	/**
	 * List products in a category shortcode.
	 *
	 * @param array $atts Attributes.
	 * @return string
	 */
	public static function product_category( $atts ) {
		if ( empty( $atts['category'] ) ) {
			return '';
		}

		$atts = array_merge(
			array(
				'limit'        => '12',
				'columns'      => '4',
				'orderby'      => 'menu_order title',
				'order'        => 'ASC',
				'category'     => '',
				'cat_operator' => 'IN',
			),
			(array) $atts
		);

		$shortcode = new WC_Shortcode_Products( $atts, 'product_category' );

		return $shortcode->get_content();
	}

	/**
	 * List all (or limited) product categories.
	 *
	 * @param array $atts Attributes.
	 * @return string
	 */
	public static function product_categories( $atts ) {
		if ( isset( $atts['number'] ) ) {
			$atts['limit'] = $atts['number'];
		}

		$atts = shortcode_atts(
			array(
				'limit'      => '-1',
				'orderby'    => 'name',
				'order'      => 'ASC',
				'columns'    => '4',
				'hide_empty' => 1,
				'parent'     => '',
				'ids'        => '',
			),
			$atts,
			'product_categories'
		);

		$ids        = array_filter( array_map( 'trim', explode( ',', $atts['ids'] ) ) );
		$hide_empty = ( true === $atts['hide_empty'] || 'true' === $atts['hide_empty'] || 1 === $atts['hide_empty'] || '1' === $atts['hide_empty'] ) ? 1 : 0;

		// Get terms and workaround WP bug with parents/pad counts.
		$args = array(
			'orderby'    => $atts['orderby'],
			'order'      => $atts['order'],
			'hide_empty' => $hide_empty,
			'include'    => $ids,
			'pad_counts' => true,
			'child_of'   => $atts['parent'],
		);

		$product_categories = apply_filters(
			'woocommerce_product_categories',
			get_terms( 'product_cat', $args )
		);

		if ( '' !== $atts['parent'] ) {
			$product_categories = wp_list_filter(
				$product_categories,
				array(
					'parent' => $atts['parent'],
				)
			);
		}

		if ( $hide_empty ) {
			foreach ( $product_categories as $key => $category ) {
				if ( 0 === $category->count ) {
					unset( $product_categories[ $key ] );
				}
			}
		}

		$atts['limit'] = '-1' === $atts['limit'] ? null : intval( $atts['limit'] );
		if ( $atts['limit'] ) {
			$product_categories = array_slice( $product_categories, 0, $atts['limit'] );
		}

		$columns = absint( $atts['columns'] );

		wc_set_loop_prop( 'columns', $columns );
		wc_set_loop_prop( 'is_shortcode', true );

		ob_start();

		if ( $product_categories ) {
			woocommerce_product_loop_start();

			foreach ( $product_categories as $category ) {
				wc_get_template(
					'content-product_cat.php',
					array(
						'category' => $category,
					)
				);
			}

			woocommerce_product_loop_end();
		}

		wc_reset_loop();

		return '<div class="woocommerce columns-' . $columns . '">' . ob_get_clean() . '</div>';
	}

	/**
	 * Recent Products shortcode.
	 *
	 * @param array $atts Attributes.
	 * @return string
	 */
	public static function recent_products( $atts ) {
		$atts = array_merge(
			array(
				'limit'        => '12',
				'columns'      => '4',
				'orderby'      => 'date',
				'order'        => 'DESC',
				'category'     => '',
				'cat_operator' => 'IN',
			),
			(array) $atts
		);

		$shortcode = new WC_Shortcode_Products( $atts, 'recent_products' );

		return $shortcode->get_content();
	}

	/**
	 * List multiple products shortcode.
	 *
	 * @param array $atts Attributes.
	 * @return string
	 */
	public static function products( $atts ) {
		$atts = (array) $atts;
		$type = 'products';

		// Allow list product based on specific cases.
		if ( isset( $atts['on_sale'] ) && wc_string_to_bool( $atts['on_sale'] ) ) {
			$type = 'sale_products';
		} elseif ( isset( $atts['best_selling'] ) && wc_string_to_bool( $atts['best_selling'] ) ) {
			$type = 'best_selling_products';
		} elseif ( isset( $atts['top_rated'] ) && wc_string_to_bool( $atts['top_rated'] ) ) {
			$type = 'top_rated_products';
		}

		$shortcode = new WC_Shortcode_Products( $atts, $type );

		return $shortcode->get_content();
	}

	/**
	 * Display a single product.
	 *
	 * @param array $atts Attributes.
	 * @return string
	 */
	public static function product( $atts ) {
		if ( empty( $atts ) ) {
			return '';
		}

		$atts['skus']  = isset( $atts['sku'] ) ? $atts['sku'] : '';
		$atts['ids']   = isset( $atts['id'] ) ? $atts['id'] : '';
		$atts['limit'] = '1';
		$shortcode     = new WC_Shortcode_Products( (array) $atts, 'product' );

		return $shortcode->get_content();
	}

	/**
	 * Display a single product price + cart button.
	 *
	 * @param array $atts Attributes.
	 * @return string
	 */
	public static function product_add_to_cart( $atts ) {
		global $post;

		if ( empty( $atts ) ) {
			return '';
		}

		$atts = shortcode_atts(
			array(
				'id'         => '',
				'class'      => '',
				'quantity'   => '1',
				'sku'        => '',
				'style'      => 'border:4px solid #ccc; padding: 12px;',
				'show_price' => 'true',
			),
			$atts,
			'product_add_to_cart'
		);

		if ( ! empty( $atts['id'] ) ) {
			$product_data = get_post( $atts['id'] );
		} elseif ( ! empty( $atts['sku'] ) ) {
			$product_id   = wc_get_product_id_by_sku( $atts['sku'] );
			$product_data = get_post( $product_id );
		} else {
			return '';
		}

		$product = is_object( $product_data ) && in_array( $product_data->post_type, array( 'product', 'product_variation' ), true ) ? wc_setup_product_data( $product_data ) : false;

		if ( ! $product ) {
			return '';
		}

		ob_start();

		echo '<p class="product woocommerce add_to_cart_inline ' . esc_attr( $atts['class'] ) . '" style="' . ( empty( $atts['style'] ) ? '' : esc_attr( $atts['style'] ) ) . '">';

		if ( wc_string_to_bool( $atts['show_price'] ) ) {
			// @codingStandardsIgnoreStart
			echo $product->get_price_html();
			// @codingStandardsIgnoreEnd
		}

		woocommerce_template_loop_add_to_cart(
			array(
				'quantity' => $atts['quantity'],
			)
		);

		echo '</p>';

		// Restore Product global in case this is shown inside a product post.
		wc_setup_product_data( $post );

		return ob_get_clean();
	}

	/**
	 * Get the add to cart URL for a product.
	 *
	 * @param array $atts Attributes.
	 * @return string
	 */
	public static function product_add_to_cart_url( $atts ) {
		if ( empty( $atts ) ) {
			return '';
		}

		if ( isset( $atts['id'] ) ) {
			$product_data = get_post( $atts['id'] );
		} elseif ( isset( $atts['sku'] ) ) {
			$product_id   = wc_get_product_id_by_sku( $atts['sku'] );
			$product_data = get_post( $product_id );
		} else {
			return '';
		}

		$product = is_object( $product_data ) && in_array( $product_data->post_type, array( 'product', 'product_variation' ), true ) ? wc_setup_product_data( $product_data ) : false;

		if ( ! $product ) {
			return '';
		}

		$_product = wc_get_product( $product_data );

		return esc_url( $_product->add_to_cart_url() );
	}

	/**
	 * List all products on sale.
	 *
	 * @param array $atts Attributes.
	 * @return string
	 */
	public static function sale_products( $atts ) {
		$atts = array_merge(
			array(
				'limit'        => '12',
				'columns'      => '4',
				'orderby'      => 'title',
				'order'        => 'ASC',
				'category'     => '',
				'cat_operator' => 'IN',
			),
			(array) $atts
		);

		$shortcode = new WC_Shortcode_Products( $atts, 'sale_products' );

		return $shortcode->get_content();
	}

	/**
	 * List best selling products on sale.
	 *
	 * @param array $atts Attributes.
	 * @return string
	 */
	public static function best_selling_products( $atts ) {
		$atts = array_merge(
			array(
				'limit'        => '12',
				'columns'      => '4',
				'category'     => '',
				'cat_operator' => 'IN',
			),
			(array) $atts
		);

		$shortcode = new WC_Shortcode_Products( $atts, 'best_selling_products' );

		return $shortcode->get_content();
	}

	/**
	 * List top rated products on sale.
	 *
	 * @param array $atts Attributes.
	 * @return string
	 */
	public static function top_rated_products( $atts ) {
		$atts = array_merge(
			array(
				'limit'        => '12',
				'columns'      => '4',
				'orderby'      => 'title',
				'order'        => 'ASC',
				'category'     => '',
				'cat_operator' => 'IN',
			),
			(array) $atts
		);

		$shortcode = new WC_Shortcode_Products( $atts, 'top_rated_products' );

		return $shortcode->get_content();
	}

	/**
	 * Output featured products.
	 *
	 * @param array $atts Attributes.
	 * @return string
	 */
	public static function featured_products( $atts ) {
		$atts = array_merge(
			array(
				'limit'        => '12',
				'columns'      => '4',
				'orderby'      => 'date',
				'order'        => 'DESC',
				'category'     => '',
				'cat_operator' => 'IN',
			),
			(array) $atts
		);

		$atts['visibility'] = 'featured';

		$shortcode = new WC_Shortcode_Products( $atts, 'featured_products' );

		return $shortcode->get_content();
	}

	/**
	 * Show a single product page.
	 *
	 * @param array $atts Attributes.
	 * @return string
	 */
	public static function product_page( $atts ) {
		if ( empty( $atts ) ) {
			return '';
		}

		if ( ! isset( $atts['id'] ) && ! isset( $atts['sku'] ) ) {
			return '';
		}

		$args = array(
			'posts_per_page'      => 1,
			'post_type'           => 'product',
			'post_status'         => ( ! empty( $atts['status'] ) ) ? $atts['status'] : 'publish',
			'ignore_sticky_posts' => 1,
			'no_found_rows'       => 1,
		);

		if ( isset( $atts['sku'] ) ) {
			$args['meta_query'][] = array(
				'key'     => '_sku',
				'value'   => sanitize_text_field( $atts['sku'] ),
				'compare' => '=',
			);

			$args['post_type'] = array( 'product', 'product_variation' );
		}

		if ( isset( $atts['id'] ) ) {
			$args['p'] = absint( $atts['id'] );
		}

		// Don't render titles if desired.
		if ( isset( $atts['show_title'] ) && ! $atts['show_title'] ) {
			remove_action( 'woocommerce_single_product_summary', 'woocommerce_template_single_title', 5 );
		}

		// Change form action to avoid redirect.
		add_filter( 'woocommerce_add_to_cart_form_action', '__return_empty_string' );

		$single_product = new WP_Query( $args );

		$preselected_id = '0';

		// Check if sku is a variation.
		if ( isset( $atts['sku'] ) && $single_product->have_posts() && 'product_variation' === $single_product->post->post_type ) {

			$variation  = wc_get_product_object( 'variation', $single_product->post->ID );
			$attributes = $variation->get_attributes();

			// Set preselected id to be used by JS to provide context.
			$preselected_id = $single_product->post->ID;

			// Get the parent product object.
			$args = array(
				'posts_per_page'      => 1,
				'post_type'           => 'product',
				'post_status'         => 'publish',
				'ignore_sticky_posts' => 1,
				'no_found_rows'       => 1,
				'p'                   => $single_product->post->post_parent,
			);

			$single_product = new WP_Query( $args );
			?>
			<script type="text/javascript">
				jQuery( function( $ ) {
					var $variations_form = $( '[data-product-page-preselected-id="<?php echo esc_attr( $preselected_id ); ?>"]' ).find( 'form.variations_form' );

					<?php foreach ( $attributes as $attr => $value ) { ?>
						$variations_form.find( 'select[name="<?php echo esc_attr( $attr ); ?>"]' ).val( '<?php echo esc_js( $value ); ?>' );
					<?php } ?>
				});
			</script>
			<?php
		}

		// For "is_single" to always make load comments_template() for reviews.
		$single_product->is_single = true;

		ob_start();

		global $wp_query;

		// Backup query object so following loops think this is a product page.
		$previous_wp_query = $wp_query;
		// @codingStandardsIgnoreStart
		$wp_query          = $single_product;
		// @codingStandardsIgnoreEnd

		wp_enqueue_script( 'wc-single-product' );

		while ( $single_product->have_posts() ) {
			$single_product->the_post()
			?>
			<div class="single-product" data-product-page-preselected-id="<?php echo esc_attr( $preselected_id ); ?>">
				<?php wc_get_template_part( 'content', 'single-product' ); ?>
			</div>
			<?php
		}

		// Restore $previous_wp_query and reset post data.
		// @codingStandardsIgnoreStart
		$wp_query = $previous_wp_query;
		// @codingStandardsIgnoreEnd
		wp_reset_postdata();

		// Re-enable titles if they were removed.
		if ( isset( $atts['show_title'] ) && ! $atts['show_title'] ) {
			add_action( 'woocommerce_single_product_summary', 'woocommerce_template_single_title', 5 );
		}

		remove_filter( 'woocommerce_add_to_cart_form_action', '__return_empty_string' );

		return '<div class="woocommerce">' . ob_get_clean() . '</div>';
	}

	/**
	 * Show messages.
	 *
	 * @return string
	 */
	public static function shop_messages() {
		if ( ! function_exists( 'wc_print_notices' ) ) {
			return '';
		}
		return '<div class="woocommerce">' . wc_print_notices( true ) . '</div>';
	}

	/**
	 * Order by rating.
	 *
	 * @deprecated 3.2.0 Use WC_Shortcode_Products::order_by_rating_post_clauses().
	 * @param      array $args Query args.
	 * @return     array
	 */
	public static function order_by_rating_post_clauses( $args ) {
		return WC_Shortcode_Products::order_by_rating_post_clauses( $args );
	}

	/**
	 * List products with an attribute shortcode.
	 * Example [product_attribute attribute="color" filter="black"].
	 *
	 * @param array $atts Attributes.
	 * @return string
	 */
	public static function product_attribute( $atts ) {
		$atts = array_merge(
			array(
				'limit'     => '12',
				'columns'   => '4',
				'orderby'   => 'title',
				'order'     => 'ASC',
				'attribute' => '',
				'terms'     => '',
			),
			(array) $atts
		);

		if ( empty( $atts['attribute'] ) ) {
			return '';
		}

		$shortcode = new WC_Shortcode_Products( $atts, 'product_attribute' );

		return $shortcode->get_content();
	}

	/**
	 * List related products.
	 *
	 * @param array $atts Attributes.
	 * @return string
	 */
	public static function related_products( $atts ) {
		if ( isset( $atts['per_page'] ) ) {
			$atts['limit'] = $atts['per_page'];
		}

		// @codingStandardsIgnoreStart
		$atts = shortcode_atts( array(
			'limit'    => '4',
			'columns'  => '4',
			'orderby'  => 'rand',
		), $atts, 'related_products' );
		// @codingStandardsIgnoreEnd

		ob_start();

		// Rename arg.
		$atts['posts_per_page'] = absint( $atts['limit'] );

		woocommerce_related_products( $atts );

		return ob_get_clean();
	}
}
queue/class-wc-queue.php000064400000003645151336044760011260 0ustar00<?php
/**
 * WC Queue
 *
 * @version 3.5.0
 * @package WooCommerce\Interface
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * WC Queue
 *
 * Singleton for managing the WC queue instance.
 *
 * @version 3.5.0
 */
class WC_Queue {

	/**
	 * The single instance of the queue.
	 *
	 * @var WC_Queue_Interface|null
	 */
	protected static $instance = null;

	/**
	 * The default queue class to initialize
	 *
	 * @var string
	 */
	protected static $default_cass = 'WC_Action_Queue';

	/**
	 * Single instance of WC_Queue_Interface
	 *
	 * @return WC_Queue_Interface
	 */
	final public static function instance() {

		if ( is_null( self::$instance ) ) {
			$class          = self::get_class();
			self::$instance = new $class();
			self::$instance = self::validate_instance( self::$instance );
		}
		return self::$instance;
	}

	/**
	 * Get class to instantiate
	 *
	 * And make sure 3rd party code has the chance to attach a custom queue class.
	 *
	 * @return string
	 */
	protected static function get_class() {
		if ( ! did_action( 'plugins_loaded' ) ) {
			wc_doing_it_wrong( __FUNCTION__, __( 'This function should not be called before plugins_loaded.', 'woocommerce' ), '3.5.0' );
		}

		return apply_filters( 'woocommerce_queue_class', self::$default_cass );
	}

	/**
	 * Enforce a WC_Queue_Interface
	 *
	 * @param WC_Queue_Interface $instance Instance class.
	 * @return WC_Queue_Interface
	 */
	protected static function validate_instance( $instance ) {
		if ( false === ( $instance instanceof WC_Queue_Interface ) ) {
			$default_class = self::$default_cass;
			/* translators: %s: Default class name */
			wc_doing_it_wrong( __FUNCTION__, sprintf( __( 'The class attached to the "woocommerce_queue_class" does not implement the WC_Queue_Interface interface. The default %s class will be used instead.', 'woocommerce' ), $default_class ), '3.5.0' );
			$instance = new $default_class();
		}

		return $instance;
	}
}
queue/class-wc-action-queue.php000064400000015434151336044760012532 0ustar00<?php
/**
 * Action Queue
 *
 * @version 3.5.0
 * @package WooCommerce\Interface
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * WC Action Queue
 *
 * A job queue using WordPress actions.
 *
 * @version 3.5.0
 */
class WC_Action_Queue implements WC_Queue_Interface {

	/**
	 * Enqueue an action to run one time, as soon as possible
	 *
	 * @param string $hook The hook to trigger.
	 * @param array  $args Arguments to pass when the hook triggers.
	 * @param string $group The group to assign this job to.
	 * @return string The action ID.
	 */
	public function add( $hook, $args = array(), $group = '' ) {
		return $this->schedule_single( time(), $hook, $args, $group );
	}

	/**
	 * Schedule an action to run once at some time in the future
	 *
	 * @param int    $timestamp When the job will run.
	 * @param string $hook The hook to trigger.
	 * @param array  $args Arguments to pass when the hook triggers.
	 * @param string $group The group to assign this job to.
	 * @return string The action ID.
	 */
	public function schedule_single( $timestamp, $hook, $args = array(), $group = '' ) {
		return as_schedule_single_action( $timestamp, $hook, $args, $group );
	}

	/**
	 * Schedule a recurring action
	 *
	 * @param int    $timestamp When the first instance of the job will run.
	 * @param int    $interval_in_seconds How long to wait between runs.
	 * @param string $hook The hook to trigger.
	 * @param array  $args Arguments to pass when the hook triggers.
	 * @param string $group The group to assign this job to.
	 * @return string The action ID.
	 */
	public function schedule_recurring( $timestamp, $interval_in_seconds, $hook, $args = array(), $group = '' ) {
		return as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args, $group );
	}

	/**
	 * Schedule an action that recurs on a cron-like schedule.
	 *
	 * @param int    $timestamp The schedule will start on or after this time.
	 * @param string $cron_schedule A cron-link schedule string.
	 * @see http://en.wikipedia.org/wiki/Cron
	 *   *    *    *    *    *    *
	 *   ┬    ┬    ┬    ┬    ┬    ┬
	 *   |    |    |    |    |    |
	 *   |    |    |    |    |    + year [optional]
	 *   |    |    |    |    +----- day of week (0 - 7) (Sunday=0 or 7)
	 *   |    |    |    +---------- month (1 - 12)
	 *   |    |    +--------------- day of month (1 - 31)
	 *   |    +-------------------- hour (0 - 23)
	 *   +------------------------- min (0 - 59)
	 * @param string $hook The hook to trigger.
	 * @param array  $args Arguments to pass when the hook triggers.
	 * @param string $group The group to assign this job to.
	 * @return string The action ID
	 */
	public function schedule_cron( $timestamp, $cron_schedule, $hook, $args = array(), $group = '' ) {
		return as_schedule_cron_action( $timestamp, $cron_schedule, $hook, $args, $group );
	}

	/**
	 * Dequeue the next scheduled instance of an action with a matching hook (and optionally matching args and group).
	 *
	 * Any recurring actions with a matching hook should also be cancelled, not just the next scheduled action.
	 *
	 * While technically only the next instance of a recurring or cron action is unscheduled by this method, that will also
	 * prevent all future instances of that recurring or cron action from being run. Recurring and cron actions are scheduled
	 * in a sequence instead of all being scheduled at once. Each successive occurrence of a recurring action is scheduled
	 * only after the former action is run. As the next instance is never run, because it's unscheduled by this function,
	 * then the following instance will never be scheduled (or exist), which is effectively the same as being unscheduled
	 * by this method also.
	 *
	 * @param string $hook The hook that the job will trigger.
	 * @param array  $args Args that would have been passed to the job.
	 * @param string $group The group the job is assigned to (if any).
	 */
	public function cancel( $hook, $args = array(), $group = '' ) {
		as_unschedule_action( $hook, $args, $group );
	}

	/**
	 * Dequeue all actions with a matching hook (and optionally matching args and group) so no matching actions are ever run.
	 *
	 * @param string $hook The hook that the job will trigger.
	 * @param array  $args Args that would have been passed to the job.
	 * @param string $group The group the job is assigned to (if any).
	 */
	public function cancel_all( $hook, $args = array(), $group = '' ) {
		as_unschedule_all_actions( $hook, $args, $group );
	}

	/**
	 * Get the date and time for the next scheduled occurence of an action with a given hook
	 * (an optionally that matches certain args and group), if any.
	 *
	 * @param string $hook The hook that the job will trigger.
	 * @param array  $args Filter to a hook with matching args that will be passed to the job when it runs.
	 * @param string $group Filter to only actions assigned to a specific group.
	 * @return WC_DateTime|null The date and time for the next occurrence, or null if there is no pending, scheduled action for the given hook.
	 */
	public function get_next( $hook, $args = null, $group = '' ) {

		$next_timestamp = as_next_scheduled_action( $hook, $args, $group );

		if ( is_numeric( $next_timestamp ) ) {
			return new WC_DateTime( "@{$next_timestamp}", new DateTimeZone( 'UTC' ) );
		}

		return null;
	}

	/**
	 * Find scheduled actions
	 *
	 * @param array  $args Possible arguments, with their default values:
	 *        'hook' => '' - the name of the action that will be triggered
	 *        'args' => null - the args array that will be passed with the action
	 *        'date' => null - the scheduled date of the action. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone.
	 *        'date_compare' => '<=' - operator for testing "date". accepted values are '!=', '>', '>=', '<', '<=', '='
	 *        'modified' => null - the date the action was last updated. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone.
	 *        'modified_compare' => '<=' - operator for testing "modified". accepted values are '!=', '>', '>=', '<', '<=', '='
	 *        'group' => '' - the group the action belongs to
	 *        'status' => '' - ActionScheduler_Store::STATUS_COMPLETE or ActionScheduler_Store::STATUS_PENDING
	 *        'claimed' => null - TRUE to find claimed actions, FALSE to find unclaimed actions, a string to find a specific claim ID
	 *        'per_page' => 5 - Number of results to return
	 *        'offset' => 0
	 *        'orderby' => 'date' - accepted values are 'hook', 'group', 'modified', or 'date'
	 *        'order' => 'ASC'.
	 *
	 * @param string $return_format OBJECT, ARRAY_A, or ids.
	 * @return array
	 */
	public function search( $args = array(), $return_format = OBJECT ) {
		return as_get_scheduled_actions( $args, $return_format );
	}
}
class-wc-privacy-background-process.php000064400000003253151336044760014251 0ustar00<?php
/**
 * Order cleanup background process.
 *
 * @package WooCommerce\Classes
 * @version 3.4.0
 * @since   3.4.0
 */

defined( 'ABSPATH' ) || exit;

if ( ! class_exists( 'WC_Background_Process', false ) ) {
	include_once dirname( __FILE__ ) . '/abstracts/class-wc-background-process.php';
}

/**
 * WC_Privacy_Background_Process class.
 */
class WC_Privacy_Background_Process extends WC_Background_Process {

	/**
	 * Initiate new background process.
	 */
	public function __construct() {
		// Uses unique prefix per blog so each blog has separate queue.
		$this->prefix = 'wp_' . get_current_blog_id();
		$this->action = 'wc_privacy_cleanup';
		parent::__construct();
	}

	/**
	 * Code to execute for each item in the queue
	 *
	 * @param string $item Queue item to iterate over.
	 * @return bool
	 */
	protected function task( $item ) {
		if ( ! $item || empty( $item['task'] ) ) {
			return false;
		}

		$process_count = 0;
		$process_limit = 20;

		switch ( $item['task'] ) {
			case 'trash_pending_orders':
				$process_count = WC_Privacy::trash_pending_orders( $process_limit );
				break;
			case 'trash_failed_orders':
				$process_count = WC_Privacy::trash_failed_orders( $process_limit );
				break;
			case 'trash_cancelled_orders':
				$process_count = WC_Privacy::trash_cancelled_orders( $process_limit );
				break;
			case 'anonymize_completed_orders':
				$process_count = WC_Privacy::anonymize_completed_orders( $process_limit );
				break;
			case 'delete_inactive_accounts':
				$process_count = WC_Privacy::delete_inactive_accounts( $process_limit );
				break;
		}

		if ( $process_limit === $process_count ) {
			// Needs to run again.
			return $item;
		}

		return false;
	}
}
emails/class-wc-email-customer-note.php000064400000007725151336044760014156 0ustar00<?php
/**
 * Class WC_Email_Customer_Note file.
 *
 * @package WooCommerce\Emails
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

if ( ! class_exists( 'WC_Email_Customer_Note', false ) ) :

	/**
	 * Customer Note Order Email.
	 *
	 * Customer note emails are sent when you add a note to an order.
	 *
	 * @class       WC_Email_Customer_Note
	 * @version     3.5.0
	 * @package     WooCommerce\Classes\Emails
	 * @extends     WC_Email
	 */
	class WC_Email_Customer_Note extends WC_Email {

		/**
		 * Customer note.
		 *
		 * @var string
		 */
		public $customer_note;

		/**
		 * Constructor.
		 */
		public function __construct() {
			$this->id             = 'customer_note';
			$this->customer_email = true;
			$this->title          = __( 'Customer note', 'woocommerce' );
			$this->description    = __( 'Customer note emails are sent when you add a note to an order.', 'woocommerce' );
			$this->template_html  = 'emails/customer-note.php';
			$this->template_plain = 'emails/plain/customer-note.php';
			$this->placeholders   = array(
				'{order_date}'   => '',
				'{order_number}' => '',
			);

			// Triggers.
			add_action( 'woocommerce_new_customer_note_notification', array( $this, 'trigger' ) );

			// Call parent constructor.
			parent::__construct();
		}

		/**
		 * Get email subject.
		 *
		 * @since  3.1.0
		 * @return string
		 */
		public function get_default_subject() {
			return __( 'Note added to your {site_title} order from {order_date}', 'woocommerce' );
		}

		/**
		 * Get email heading.
		 *
		 * @since  3.1.0
		 * @return string
		 */
		public function get_default_heading() {
			return __( 'A note has been added to your order', 'woocommerce' );
		}

		/**
		 * Trigger.
		 *
		 * @param array $args Email arguments.
		 */
		public function trigger( $args ) {
			$this->setup_locale();

			if ( ! empty( $args ) ) {
				$defaults = array(
					'order_id'      => '',
					'customer_note' => '',
				);

				$args = wp_parse_args( $args, $defaults );

				$order_id      = $args['order_id'];
				$customer_note = $args['customer_note'];

				if ( $order_id ) {
					$this->object = wc_get_order( $order_id );

					if ( $this->object ) {
						$this->recipient                      = $this->object->get_billing_email();
						$this->customer_note                  = $customer_note;
						$this->placeholders['{order_date}']   = wc_format_datetime( $this->object->get_date_created() );
						$this->placeholders['{order_number}'] = $this->object->get_order_number();
					}
				}
			}

			if ( $this->is_enabled() && $this->get_recipient() ) {
				$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
			}

			$this->restore_locale();
		}

		/**
		 * Get content html.
		 *
		 * @return string
		 */
		public function get_content_html() {
			return wc_get_template_html(
				$this->template_html,
				array(
					'order'              => $this->object,
					'email_heading'      => $this->get_heading(),
					'additional_content' => $this->get_additional_content(),
					'customer_note'      => $this->customer_note,
					'sent_to_admin'      => false,
					'plain_text'         => false,
					'email'              => $this,
				)
			);
		}

		/**
		 * Get content plain.
		 *
		 * @return string
		 */
		public function get_content_plain() {
			return wc_get_template_html(
				$this->template_plain,
				array(
					'order'              => $this->object,
					'email_heading'      => $this->get_heading(),
					'additional_content' => $this->get_additional_content(),
					'customer_note'      => $this->customer_note,
					'sent_to_admin'      => false,
					'plain_text'         => true,
					'email'              => $this,
				)
			);
		}

		/**
		 * Default content to show below main email content.
		 *
		 * @since 3.7.0
		 * @return string
		 */
		public function get_default_additional_content() {
			return __( 'Thanks for reading.', 'woocommerce' );
		}
	}

endif;

return new WC_Email_Customer_Note();
emails/class-wc-email-customer-on-hold-order.php000064400000010024151336044760015644 0ustar00<?php
/**
 * Class WC_Email_Customer_On_Hold_Order file.
 *
 * @package WooCommerce\Emails
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

if ( ! class_exists( 'WC_Email_Customer_On_Hold_Order', false ) ) :

	/**
	 * Customer On-hold Order Email.
	 *
	 * An email sent to the customer when a new order is on-hold for.
	 *
	 * @class       WC_Email_Customer_On_Hold_Order
	 * @version     2.6.0
	 * @package     WooCommerce\Classes\Emails
	 * @extends     WC_Email
	 */
	class WC_Email_Customer_On_Hold_Order extends WC_Email {

		/**
		 * Constructor.
		 */
		public function __construct() {
			$this->id             = 'customer_on_hold_order';
			$this->customer_email = true;
			$this->title          = __( 'Order on-hold', 'woocommerce' );
			$this->description    = __( 'This is an order notification sent to customers containing order details after an order is placed on-hold.', 'woocommerce' );
			$this->template_html  = 'emails/customer-on-hold-order.php';
			$this->template_plain = 'emails/plain/customer-on-hold-order.php';
			$this->placeholders   = array(
				'{order_date}'   => '',
				'{order_number}' => '',
			);

			// Triggers for this email.
			add_action( 'woocommerce_order_status_pending_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 );
			add_action( 'woocommerce_order_status_failed_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 );
			add_action( 'woocommerce_order_status_cancelled_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 );

			// Call parent constructor.
			parent::__construct();
		}

		/**
		 * Get email subject.
		 *
		 * @since  3.1.0
		 * @return string
		 */
		public function get_default_subject() {
			return __( 'Your {site_title} order has been received!', 'woocommerce' );
		}

		/**
		 * Get email heading.
		 *
		 * @since  3.1.0
		 * @return string
		 */
		public function get_default_heading() {
			return __( 'Thank you for your order', 'woocommerce' );
		}

		/**
		 * Trigger the sending of this email.
		 *
		 * @param int            $order_id The order ID.
		 * @param WC_Order|false $order Order object.
		 */
		public function trigger( $order_id, $order = false ) {
			$this->setup_locale();

			if ( $order_id && ! is_a( $order, 'WC_Order' ) ) {
				$order = wc_get_order( $order_id );
			}

			if ( is_a( $order, 'WC_Order' ) ) {
				$this->object                         = $order;
				$this->recipient                      = $this->object->get_billing_email();
				$this->placeholders['{order_date}']   = wc_format_datetime( $this->object->get_date_created() );
				$this->placeholders['{order_number}'] = $this->object->get_order_number();
			}

			if ( $this->is_enabled() && $this->get_recipient() ) {
				$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
			}

			$this->restore_locale();
		}

		/**
		 * Get content html.
		 *
		 * @return string
		 */
		public function get_content_html() {
			return wc_get_template_html(
				$this->template_html,
				array(
					'order'              => $this->object,
					'email_heading'      => $this->get_heading(),
					'additional_content' => $this->get_additional_content(),
					'sent_to_admin'      => false,
					'plain_text'         => false,
					'email'              => $this,
				)
			);
		}

		/**
		 * Get content plain.
		 *
		 * @return string
		 */
		public function get_content_plain() {
			return wc_get_template_html(
				$this->template_plain,
				array(
					'order'              => $this->object,
					'email_heading'      => $this->get_heading(),
					'additional_content' => $this->get_additional_content(),
					'sent_to_admin'      => false,
					'plain_text'         => true,
					'email'              => $this,
				)
			);
		}

		/**
		 * Default content to show below main email content.
		 *
		 * @since 3.7.0
		 * @return string
		 */
		public function get_default_additional_content() {
			return __( 'We look forward to fulfilling your order soon.', 'woocommerce' );
		}
	}

endif;

return new WC_Email_Customer_On_Hold_Order();
emails/class-wc-email-customer-refunded-order.php000064400000022070151336044760016104 0ustar00<?php
/**
 * Class WC_Email_Customer_Refunded_Order file.
 *
 * @package WooCommerce\Emails
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

if ( ! class_exists( 'WC_Email_Customer_Refunded_Order', false ) ) :

	/**
	 * Customer Refunded Order Email.
	 *
	 * Order refunded emails are sent to the customer when the order is marked refunded.
	 *
	 * @class    WC_Email_Customer_Refunded_Order
	 * @version  3.5.0
	 * @package  WooCommerce\Classes\Emails
	 * @extends  WC_Email
	 */
	class WC_Email_Customer_Refunded_Order extends WC_Email {

		/**
		 * Refund order.
		 *
		 * @var WC_Order|bool
		 */
		public $refund;

		/**
		 * Is the order partial refunded?
		 *
		 * @var bool
		 */
		public $partial_refund;

		/**
		 * Constructor.
		 */
		public function __construct() {
			$this->customer_email = true;
			$this->id             = 'customer_refunded_order';
			$this->title          = __( 'Refunded order', 'woocommerce' );
			$this->description    = __( 'Order refunded emails are sent to customers when their orders are refunded.', 'woocommerce' );
			$this->template_html  = 'emails/customer-refunded-order.php';
			$this->template_plain = 'emails/plain/customer-refunded-order.php';
			$this->placeholders   = array(
				'{order_date}'   => '',
				'{order_number}' => '',
			);

			// Triggers for this email.
			add_action( 'woocommerce_order_fully_refunded_notification', array( $this, 'trigger_full' ), 10, 2 );
			add_action( 'woocommerce_order_partially_refunded_notification', array( $this, 'trigger_partial' ), 10, 2 );

			// Call parent constructor.
			parent::__construct();
		}

		/**
		 * Get email subject.
		 *
		 * @param bool $partial Whether it is a partial refund or a full refund.
		 * @since  3.1.0
		 * @return string
		 */
		public function get_default_subject( $partial = false ) {
			if ( $partial ) {
				return __( 'Your {site_title} order #{order_number} has been partially refunded', 'woocommerce' );
			} else {
				return __( 'Your {site_title} order #{order_number} has been refunded', 'woocommerce' );
			}
		}

		/**
		 * Get email heading.
		 *
		 * @param bool $partial Whether it is a partial refund or a full refund.
		 * @since  3.1.0
		 * @return string
		 */
		public function get_default_heading( $partial = false ) {
			if ( $partial ) {
				return __( 'Partial Refund: Order {order_number}', 'woocommerce' );
			} else {
				return __( 'Order Refunded: {order_number}', 'woocommerce' );
			}
		}

		/**
		 * Get email subject.
		 *
		 * @return string
		 */
		public function get_subject() {
			if ( $this->partial_refund ) {
				$subject = $this->get_option( 'subject_partial', $this->get_default_subject( true ) );
			} else {
				$subject = $this->get_option( 'subject_full', $this->get_default_subject() );
			}
			return apply_filters( 'woocommerce_email_subject_customer_refunded_order', $this->format_string( $subject ), $this->object, $this );
		}

		/**
		 * Get email heading.
		 *
		 * @return string
		 */
		public function get_heading() {
			if ( $this->partial_refund ) {
				$heading = $this->get_option( 'heading_partial', $this->get_default_heading( true ) );
			} else {
				$heading = $this->get_option( 'heading_full', $this->get_default_heading() );
			}
			return apply_filters( 'woocommerce_email_heading_customer_refunded_order', $this->format_string( $heading ), $this->object, $this );
		}

		/**
		 * Set email strings.
		 *
		 * @param bool $partial_refund Whether it is a partial refund or a full refund.
		 * @deprecated 3.1.0 Unused.
		 */
		public function set_email_strings( $partial_refund = false ) {}

		/**
		 * Full refund notification.
		 *
		 * @param int $order_id Order ID.
		 * @param int $refund_id Refund ID.
		 */
		public function trigger_full( $order_id, $refund_id = null ) {
			$this->trigger( $order_id, false, $refund_id );
		}

		/**
		 * Partial refund notification.
		 *
		 * @param int $order_id Order ID.
		 * @param int $refund_id Refund ID.
		 */
		public function trigger_partial( $order_id, $refund_id = null ) {
			$this->trigger( $order_id, true, $refund_id );
		}

		/**
		 * Trigger.
		 *
		 * @param int  $order_id Order ID.
		 * @param bool $partial_refund Whether it is a partial refund or a full refund.
		 * @param int  $refund_id Refund ID.
		 */
		public function trigger( $order_id, $partial_refund = false, $refund_id = null ) {
			$this->setup_locale();
			$this->partial_refund = $partial_refund;
			$this->id             = $this->partial_refund ? 'customer_partially_refunded_order' : 'customer_refunded_order';

			if ( $order_id ) {
				$this->object                         = wc_get_order( $order_id );
				$this->recipient                      = $this->object->get_billing_email();
				$this->placeholders['{order_date}']   = wc_format_datetime( $this->object->get_date_created() );
				$this->placeholders['{order_number}'] = $this->object->get_order_number();
			}

			if ( ! empty( $refund_id ) ) {
				$this->refund = wc_get_order( $refund_id );
			} else {
				$this->refund = false;
			}

			if ( $this->is_enabled() && $this->get_recipient() ) {
				$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
			}

			$this->restore_locale();
		}

		/**
		 * Get content html.
		 *
		 * @return string
		 */
		public function get_content_html() {
			return wc_get_template_html(
				$this->template_html,
				array(
					'order'              => $this->object,
					'refund'             => $this->refund,
					'partial_refund'     => $this->partial_refund,
					'email_heading'      => $this->get_heading(),
					'additional_content' => $this->get_additional_content(),
					'sent_to_admin'      => false,
					'plain_text'         => false,
					'email'              => $this,
				)
			);
		}

		/**
		 * Get content plain.
		 *
		 * @return string
		 */
		public function get_content_plain() {
			return wc_get_template_html(
				$this->template_plain,
				array(
					'order'              => $this->object,
					'refund'             => $this->refund,
					'partial_refund'     => $this->partial_refund,
					'email_heading'      => $this->get_heading(),
					'additional_content' => $this->get_additional_content(),
					'sent_to_admin'      => false,
					'plain_text'         => true,
					'email'              => $this,
				)
			);
		}

		/**
		 * Default content to show below main email content.
		 *
		 * @since 3.7.0
		 * @return string
		 */
		public function get_default_additional_content() {
			return __( 'We hope to see you again soon.', 'woocommerce' );
		}

		/**
		 * Initialise settings form fields.
		 */
		public function init_form_fields() {
			/* translators: %s: list of placeholders */
			$placeholder_text  = sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '<code>' . esc_html( implode( '</code>, <code>', array_keys( $this->placeholders ) ) ) . '</code>' );
			$this->form_fields = array(
				'enabled'            => array(
					'title'   => __( 'Enable/Disable', 'woocommerce' ),
					'type'    => 'checkbox',
					'label'   => __( 'Enable this email notification', 'woocommerce' ),
					'default' => 'yes',
				),
				'subject_full'       => array(
					'title'       => __( 'Full refund subject', 'woocommerce' ),
					'type'        => 'text',
					'desc_tip'    => true,
					'description' => $placeholder_text,
					'placeholder' => $this->get_default_subject(),
					'default'     => '',
				),
				'subject_partial'    => array(
					'title'       => __( 'Partial refund subject', 'woocommerce' ),
					'type'        => 'text',
					'desc_tip'    => true,
					'description' => $placeholder_text,
					'placeholder' => $this->get_default_subject( true ),
					'default'     => '',
				),
				'heading_full'       => array(
					'title'       => __( 'Full refund email heading', 'woocommerce' ),
					'type'        => 'text',
					'desc_tip'    => true,
					'description' => $placeholder_text,
					'placeholder' => $this->get_default_heading(),
					'default'     => '',
				),
				'heading_partial'    => array(
					'title'       => __( 'Partial refund email heading', 'woocommerce' ),
					'type'        => 'text',
					'desc_tip'    => true,
					'description' => $placeholder_text,
					'placeholder' => $this->get_default_heading( true ),
					'default'     => '',
				),
				'additional_content' => array(
					'title'       => __( 'Additional content', 'woocommerce' ),
					'description' => __( 'Text to appear below the main email content.', 'woocommerce' ) . ' ' . $placeholder_text,
					'css'         => 'width:400px; height: 75px;',
					'placeholder' => __( 'N/A', 'woocommerce' ),
					'type'        => 'textarea',
					'default'     => $this->get_default_additional_content(),
					'desc_tip'    => true,
				),
				'email_type'         => array(
					'title'       => __( 'Email type', 'woocommerce' ),
					'type'        => 'select',
					'description' => __( 'Choose which format of email to send.', 'woocommerce' ),
					'default'     => 'html',
					'class'       => 'email_type wc-enhanced-select',
					'options'     => $this->get_email_type_options(),
					'desc_tip'    => true,
				),
			);
		}
	}

endif;

return new WC_Email_Customer_Refunded_Order();
emails/class-wc-email-cancelled-order.php000064400000014643151336044760014372 0ustar00<?php
/**
 * Class WC_Email_Cancelled_Order file.
 *
 * @package WooCommerce\Emails
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'WC_Email_Cancelled_Order', false ) ) :

	/**
	 * Cancelled Order Email.
	 *
	 * An email sent to the admin when an order is cancelled.
	 *
	 * @class       WC_Email_Cancelled_Order
	 * @version     2.2.7
	 * @package     WooCommerce\Classes\Emails
	 * @extends     WC_Email
	 */
	class WC_Email_Cancelled_Order extends WC_Email {

		/**
		 * Constructor.
		 */
		public function __construct() {
			$this->id             = 'cancelled_order';
			$this->title          = __( 'Cancelled order', 'woocommerce' );
			$this->description    = __( 'Cancelled order emails are sent to chosen recipient(s) when orders have been marked cancelled (if they were previously processing or on-hold).', 'woocommerce' );
			$this->template_html  = 'emails/admin-cancelled-order.php';
			$this->template_plain = 'emails/plain/admin-cancelled-order.php';
			$this->placeholders   = array(
				'{order_date}'              => '',
				'{order_number}'            => '',
				'{order_billing_full_name}' => '',
			);

			// Triggers for this email.
			add_action( 'woocommerce_order_status_processing_to_cancelled_notification', array( $this, 'trigger' ), 10, 2 );
			add_action( 'woocommerce_order_status_on-hold_to_cancelled_notification', array( $this, 'trigger' ), 10, 2 );

			// Call parent constructor.
			parent::__construct();

			// Other settings.
			$this->recipient = $this->get_option( 'recipient', get_option( 'admin_email' ) );
		}

		/**
		 * Get email subject.
		 *
		 * @since  3.1.0
		 * @return string
		 */
		public function get_default_subject() {
			return __( '[{site_title}]: Order #{order_number} has been cancelled', 'woocommerce' );
		}

		/**
		 * Get email heading.
		 *
		 * @since  3.1.0
		 * @return string
		 */
		public function get_default_heading() {
			return __( 'Order Cancelled: #{order_number}', 'woocommerce' );
		}

		/**
		 * Trigger the sending of this email.
		 *
		 * @param int            $order_id The order ID.
		 * @param WC_Order|false $order Order object.
		 */
		public function trigger( $order_id, $order = false ) {
			$this->setup_locale();

			if ( $order_id && ! is_a( $order, 'WC_Order' ) ) {
				$order = wc_get_order( $order_id );
			}

			if ( is_a( $order, 'WC_Order' ) ) {
				$this->object                                    = $order;
				$this->placeholders['{order_date}']              = wc_format_datetime( $this->object->get_date_created() );
				$this->placeholders['{order_number}']            = $this->object->get_order_number();
				$this->placeholders['{order_billing_full_name}'] = $this->object->get_formatted_billing_full_name();
			}

			if ( $this->is_enabled() && $this->get_recipient() ) {
				$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
			}

			$this->restore_locale();
		}

		/**
		 * Get content html.
		 *
		 * @return string
		 */
		public function get_content_html() {
			return wc_get_template_html(
				$this->template_html,
				array(
					'order'              => $this->object,
					'email_heading'      => $this->get_heading(),
					'additional_content' => $this->get_additional_content(),
					'sent_to_admin'      => true,
					'plain_text'         => false,
					'email'              => $this,
				)
			);
		}

		/**
		 * Get content plain.
		 *
		 * @return string
		 */
		public function get_content_plain() {
			return wc_get_template_html(
				$this->template_plain,
				array(
					'order'              => $this->object,
					'email_heading'      => $this->get_heading(),
					'additional_content' => $this->get_additional_content(),
					'sent_to_admin'      => true,
					'plain_text'         => true,
					'email'              => $this,
				)
			);
		}

		/**
		 * Default content to show below main email content.
		 *
		 * @since 3.7.0
		 * @return string
		 */
		public function get_default_additional_content() {
			return __( 'Thanks for reading.', 'woocommerce' );
		}

		/**
		 * Initialise settings form fields.
		 */
		public function init_form_fields() {
			/* translators: %s: list of placeholders */
			$placeholder_text  = sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '<code>' . esc_html( implode( '</code>, <code>', array_keys( $this->placeholders ) ) ) . '</code>' );
			$this->form_fields = array(
				'enabled'            => array(
					'title'   => __( 'Enable/Disable', 'woocommerce' ),
					'type'    => 'checkbox',
					'label'   => __( 'Enable this email notification', 'woocommerce' ),
					'default' => 'yes',
				),
				'recipient'          => array(
					'title'       => __( 'Recipient(s)', 'woocommerce' ),
					'type'        => 'text',
					/* translators: %s: admin email */
					'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to %s.', 'woocommerce' ), '<code>' . esc_attr( get_option( 'admin_email' ) ) . '</code>' ),
					'placeholder' => '',
					'default'     => '',
					'desc_tip'    => true,
				),
				'subject'            => array(
					'title'       => __( 'Subject', 'woocommerce' ),
					'type'        => 'text',
					'desc_tip'    => true,
					'description' => $placeholder_text,
					'placeholder' => $this->get_default_subject(),
					'default'     => '',
				),
				'heading'            => array(
					'title'       => __( 'Email heading', 'woocommerce' ),
					'type'        => 'text',
					'desc_tip'    => true,
					'description' => $placeholder_text,
					'placeholder' => $this->get_default_heading(),
					'default'     => '',
				),
				'additional_content' => array(
					'title'       => __( 'Additional content', 'woocommerce' ),
					'description' => __( 'Text to appear below the main email content.', 'woocommerce' ) . ' ' . $placeholder_text,
					'css'         => 'width:400px; height: 75px;',
					'placeholder' => __( 'N/A', 'woocommerce' ),
					'type'        => 'textarea',
					'default'     => $this->get_default_additional_content(),
					'desc_tip'    => true,
				),
				'email_type'         => array(
					'title'       => __( 'Email type', 'woocommerce' ),
					'type'        => 'select',
					'description' => __( 'Choose which format of email to send.', 'woocommerce' ),
					'default'     => 'html',
					'class'       => 'email_type wc-enhanced-select',
					'options'     => $this->get_email_type_options(),
					'desc_tip'    => true,
				),
			);
		}
	}

endif;

return new WC_Email_Cancelled_Order();
emails/class-wc-email-new-order.php000064400000016433151336044760013250 0ustar00<?php
/**
 * Class WC_Email_New_Order file
 *
 * @package WooCommerce\Emails
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'WC_Email_New_Order' ) ) :

	/**
	 * New Order Email.
	 *
	 * An email sent to the admin when a new order is received/paid for.
	 *
	 * @class       WC_Email_New_Order
	 * @version     2.0.0
	 * @package     WooCommerce\Classes\Emails
	 * @extends     WC_Email
	 */
	class WC_Email_New_Order extends WC_Email {

		/**
		 * Constructor.
		 */
		public function __construct() {
			$this->id             = 'new_order';
			$this->title          = __( 'New order', 'woocommerce' );
			$this->description    = __( 'New order emails are sent to chosen recipient(s) when a new order is received.', 'woocommerce' );
			$this->template_html  = 'emails/admin-new-order.php';
			$this->template_plain = 'emails/plain/admin-new-order.php';
			$this->placeholders   = array(
				'{order_date}'   => '',
				'{order_number}' => '',
			);

			// Triggers for this email.
			add_action( 'woocommerce_order_status_pending_to_processing_notification', array( $this, 'trigger' ), 10, 2 );
			add_action( 'woocommerce_order_status_pending_to_completed_notification', array( $this, 'trigger' ), 10, 2 );
			add_action( 'woocommerce_order_status_pending_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 );
			add_action( 'woocommerce_order_status_failed_to_processing_notification', array( $this, 'trigger' ), 10, 2 );
			add_action( 'woocommerce_order_status_failed_to_completed_notification', array( $this, 'trigger' ), 10, 2 );
			add_action( 'woocommerce_order_status_failed_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 );
			add_action( 'woocommerce_order_status_cancelled_to_processing_notification', array( $this, 'trigger' ), 10, 2 );
			add_action( 'woocommerce_order_status_cancelled_to_completed_notification', array( $this, 'trigger' ), 10, 2 );
			add_action( 'woocommerce_order_status_cancelled_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 );

			// Call parent constructor.
			parent::__construct();

			// Other settings.
			$this->recipient = $this->get_option( 'recipient', get_option( 'admin_email' ) );
		}

		/**
		 * Get email subject.
		 *
		 * @since  3.1.0
		 * @return string
		 */
		public function get_default_subject() {
			return __( '[{site_title}]: New order #{order_number}', 'woocommerce' );
		}

		/**
		 * Get email heading.
		 *
		 * @since  3.1.0
		 * @return string
		 */
		public function get_default_heading() {
			return __( 'New Order: #{order_number}', 'woocommerce' );
		}

		/**
		 * Trigger the sending of this email.
		 *
		 * @param int            $order_id The order ID.
		 * @param WC_Order|false $order Order object.
		 */
		public function trigger( $order_id, $order = false ) {
			$this->setup_locale();

			if ( $order_id && ! is_a( $order, 'WC_Order' ) ) {
				$order = wc_get_order( $order_id );
			}

			if ( is_a( $order, 'WC_Order' ) ) {
				$this->object                         = $order;
				$this->placeholders['{order_date}']   = wc_format_datetime( $this->object->get_date_created() );
				$this->placeholders['{order_number}'] = $this->object->get_order_number();

				$email_already_sent = $order->get_meta( '_new_order_email_sent' );
			}

			/**
			 * Controls if new order emails can be resend multiple times.
			 *
			 * @since 5.0.0
			 * @param bool $allows Defaults to false.
			 */
			if ( 'true' === $email_already_sent && ! apply_filters( 'woocommerce_new_order_email_allows_resend', false ) ) {
				return;
			}

			if ( $this->is_enabled() && $this->get_recipient() ) {
				$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );

				$order->update_meta_data( '_new_order_email_sent', 'true' );
				$order->save();
			}

			$this->restore_locale();
		}

		/**
		 * Get content html.
		 *
		 * @return string
		 */
		public function get_content_html() {
			return wc_get_template_html(
				$this->template_html,
				array(
					'order'              => $this->object,
					'email_heading'      => $this->get_heading(),
					'additional_content' => $this->get_additional_content(),
					'sent_to_admin'      => true,
					'plain_text'         => false,
					'email'              => $this,
				)
			);
		}

		/**
		 * Get content plain.
		 *
		 * @return string
		 */
		public function get_content_plain() {
			return wc_get_template_html(
				$this->template_plain,
				array(
					'order'              => $this->object,
					'email_heading'      => $this->get_heading(),
					'additional_content' => $this->get_additional_content(),
					'sent_to_admin'      => true,
					'plain_text'         => true,
					'email'              => $this,
				)
			);
		}

		/**
		 * Default content to show below main email content.
		 *
		 * @since 3.7.0
		 * @return string
		 */
		public function get_default_additional_content() {
			return __( 'Congratulations on the sale.', 'woocommerce' );
		}

		/**
		 * Initialise settings form fields.
		 */
		public function init_form_fields() {
			/* translators: %s: list of placeholders */
			$placeholder_text  = sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '<code>' . implode( '</code>, <code>', array_keys( $this->placeholders ) ) . '</code>' );
			$this->form_fields = array(
				'enabled'            => array(
					'title'   => __( 'Enable/Disable', 'woocommerce' ),
					'type'    => 'checkbox',
					'label'   => __( 'Enable this email notification', 'woocommerce' ),
					'default' => 'yes',
				),
				'recipient'          => array(
					'title'       => __( 'Recipient(s)', 'woocommerce' ),
					'type'        => 'text',
					/* translators: %s: WP admin email */
					'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to %s.', 'woocommerce' ), '<code>' . esc_attr( get_option( 'admin_email' ) ) . '</code>' ),
					'placeholder' => '',
					'default'     => '',
					'desc_tip'    => true,
				),
				'subject'            => array(
					'title'       => __( 'Subject', 'woocommerce' ),
					'type'        => 'text',
					'desc_tip'    => true,
					'description' => $placeholder_text,
					'placeholder' => $this->get_default_subject(),
					'default'     => '',
				),
				'heading'            => array(
					'title'       => __( 'Email heading', 'woocommerce' ),
					'type'        => 'text',
					'desc_tip'    => true,
					'description' => $placeholder_text,
					'placeholder' => $this->get_default_heading(),
					'default'     => '',
				),
				'additional_content' => array(
					'title'       => __( 'Additional content', 'woocommerce' ),
					'description' => __( 'Text to appear below the main email content.', 'woocommerce' ) . ' ' . $placeholder_text,
					'css'         => 'width:400px; height: 75px;',
					'placeholder' => __( 'N/A', 'woocommerce' ),
					'type'        => 'textarea',
					'default'     => $this->get_default_additional_content(),
					'desc_tip'    => true,
				),
				'email_type'         => array(
					'title'       => __( 'Email type', 'woocommerce' ),
					'type'        => 'select',
					'description' => __( 'Choose which format of email to send.', 'woocommerce' ),
					'default'     => 'html',
					'class'       => 'email_type wc-enhanced-select',
					'options'     => $this->get_email_type_options(),
					'desc_tip'    => true,
				),
			);
		}
	}

endif;

return new WC_Email_New_Order();
emails/class-wc-email-customer-completed-order.php000064400000007627151336044760016277 0ustar00<?php
/**
 * Class WC_Email_Customer_Completed_Order file.
 *
 * @package WooCommerce\Emails
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

if ( ! class_exists( 'WC_Email_Customer_Completed_Order', false ) ) :

	/**
	 * Customer Completed Order Email.
	 *
	 * Order complete emails are sent to the customer when the order is marked complete and usual indicates that the order has been shipped.
	 *
	 * @class       WC_Email_Customer_Completed_Order
	 * @version     2.0.0
	 * @package     WooCommerce\Classes\Emails
	 * @extends     WC_Email
	 */
	class WC_Email_Customer_Completed_Order extends WC_Email {

		/**
		 * Constructor.
		 */
		public function __construct() {
			$this->id             = 'customer_completed_order';
			$this->customer_email = true;
			$this->title          = __( 'Completed order', 'woocommerce' );
			$this->description    = __( 'Order complete emails are sent to customers when their orders are marked completed and usually indicate that their orders have been shipped.', 'woocommerce' );
			$this->template_html  = 'emails/customer-completed-order.php';
			$this->template_plain = 'emails/plain/customer-completed-order.php';
			$this->placeholders   = array(
				'{order_date}'   => '',
				'{order_number}' => '',
			);

			// Triggers for this email.
			add_action( 'woocommerce_order_status_completed_notification', array( $this, 'trigger' ), 10, 2 );

			// Call parent constructor.
			parent::__construct();
		}

		/**
		 * Trigger the sending of this email.
		 *
		 * @param int            $order_id The order ID.
		 * @param WC_Order|false $order Order object.
		 */
		public function trigger( $order_id, $order = false ) {
			$this->setup_locale();

			if ( $order_id && ! is_a( $order, 'WC_Order' ) ) {
				$order = wc_get_order( $order_id );
			}

			if ( is_a( $order, 'WC_Order' ) ) {
				$this->object                         = $order;
				$this->recipient                      = $this->object->get_billing_email();
				$this->placeholders['{order_date}']   = wc_format_datetime( $this->object->get_date_created() );
				$this->placeholders['{order_number}'] = $this->object->get_order_number();
			}

			if ( $this->is_enabled() && $this->get_recipient() ) {
				$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
			}

			$this->restore_locale();
		}

		/**
		 * Get email subject.
		 *
		 * @since  3.1.0
		 * @return string
		 */
		public function get_default_subject() {
			return __( 'Your {site_title} order is now complete', 'woocommerce' );
		}

		/**
		 * Get email heading.
		 *
		 * @since  3.1.0
		 * @return string
		 */
		public function get_default_heading() {
			return __( 'Thanks for shopping with us', 'woocommerce' );
		}

		/**
		 * Get content html.
		 *
		 * @return string
		 */
		public function get_content_html() {
			return wc_get_template_html(
				$this->template_html,
				array(
					'order'              => $this->object,
					'email_heading'      => $this->get_heading(),
					'additional_content' => $this->get_additional_content(),
					'sent_to_admin'      => false,
					'plain_text'         => false,
					'email'              => $this,
				)
			);
		}

		/**
		 * Get content plain.
		 *
		 * @return string
		 */
		public function get_content_plain() {
			return wc_get_template_html(
				$this->template_plain,
				array(
					'order'              => $this->object,
					'email_heading'      => $this->get_heading(),
					'additional_content' => $this->get_additional_content(),
					'sent_to_admin'      => false,
					'plain_text'         => true,
					'email'              => $this,
				)
			);
		}

		/**
		 * Default content to show below main email content.
		 *
		 * @since 3.7.0
		 * @return string
		 */
		public function get_default_additional_content() {
			return __( 'Thanks for shopping with us.', 'woocommerce' );
		}
	}

endif;

return new WC_Email_Customer_Completed_Order();
emails/class-wc-email-customer-reset-password.php000064400000010061151336044760016156 0ustar00<?php
/**
 * Class WC_Email_Customer_Reset_Password file.
 *
 * @package WooCommerce\Emails
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

if ( ! class_exists( 'WC_Email_Customer_Reset_Password', false ) ) :

	/**
	 * Customer Reset Password.
	 *
	 * An email sent to the customer when they reset their password.
	 *
	 * @class       WC_Email_Customer_Reset_Password
	 * @version     3.5.0
	 * @package     WooCommerce\Classes\Emails
	 * @extends     WC_Email
	 */
	class WC_Email_Customer_Reset_Password extends WC_Email {

		/**
		 * User ID.
		 *
		 * @var integer
		 */
		public $user_id;

		/**
		 * User login name.
		 *
		 * @var string
		 */
		public $user_login;

		/**
		 * User email.
		 *
		 * @var string
		 */
		public $user_email;

		/**
		 * Reset key.
		 *
		 * @var string
		 */
		public $reset_key;

		/**
		 * Constructor.
		 */
		public function __construct() {

			$this->id             = 'customer_reset_password';
			$this->customer_email = true;

			$this->title       = __( 'Reset password', 'woocommerce' );
			$this->description = __( 'Customer "reset password" emails are sent when customers reset their passwords.', 'woocommerce' );

			$this->template_html  = 'emails/customer-reset-password.php';
			$this->template_plain = 'emails/plain/customer-reset-password.php';

			// Trigger.
			add_action( 'woocommerce_reset_password_notification', array( $this, 'trigger' ), 10, 2 );

			// Call parent constructor.
			parent::__construct();
		}

		/**
		 * Get email subject.
		 *
		 * @since  3.1.0
		 * @return string
		 */
		public function get_default_subject() {
			return __( 'Password Reset Request for {site_title}', 'woocommerce' );
		}

		/**
		 * Get email heading.
		 *
		 * @since  3.1.0
		 * @return string
		 */
		public function get_default_heading() {
			return __( 'Password Reset Request', 'woocommerce' );
		}

		/**
		 * Trigger.
		 *
		 * @param string $user_login User login.
		 * @param string $reset_key Password reset key.
		 */
		public function trigger( $user_login = '', $reset_key = '' ) {
			$this->setup_locale();

			if ( $user_login && $reset_key ) {
				$this->object     = get_user_by( 'login', $user_login );
				$this->user_id    = $this->object->ID;
				$this->user_login = $user_login;
				$this->reset_key  = $reset_key;
				$this->user_email = stripslashes( $this->object->user_email );
				$this->recipient  = $this->user_email;
			}

			if ( $this->is_enabled() && $this->get_recipient() ) {
				$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
			}

			$this->restore_locale();
		}

		/**
		 * Get content html.
		 *
		 * @return string
		 */
		public function get_content_html() {
			return wc_get_template_html(
				$this->template_html,
				array(
					'email_heading'      => $this->get_heading(),
					'user_id'            => $this->user_id,
					'user_login'         => $this->user_login,
					'reset_key'          => $this->reset_key,
					'blogname'           => $this->get_blogname(),
					'additional_content' => $this->get_additional_content(),
					'sent_to_admin'      => false,
					'plain_text'         => false,
					'email'              => $this,
				)
			);
		}

		/**
		 * Get content plain.
		 *
		 * @return string
		 */
		public function get_content_plain() {
			return wc_get_template_html(
				$this->template_plain,
				array(
					'email_heading'      => $this->get_heading(),
					'user_id'            => $this->user_id,
					'user_login'         => $this->user_login,
					'reset_key'          => $this->reset_key,
					'blogname'           => $this->get_blogname(),
					'additional_content' => $this->get_additional_content(),
					'sent_to_admin'      => false,
					'plain_text'         => true,
					'email'              => $this,
				)
			);
		}

		/**
		 * Default content to show below main email content.
		 *
		 * @since 3.7.0
		 * @return string
		 */
		public function get_default_additional_content() {
			return __( 'Thanks for reading.', 'woocommerce' );
		}
	}

endif;

return new WC_Email_Customer_Reset_Password();
emails/class-wc-email-customer-invoice.php000064400000016636151336044760014646 0ustar00<?php
/**
 * Class WC_Email_Customer_Invoice file.
 *
 * @package WooCommerce\Emails
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

if ( ! class_exists( 'WC_Email_Customer_Invoice', false ) ) :

	/**
	 * Customer Invoice.
	 *
	 * An email sent to the customer via admin.
	 *
	 * @class       WC_Email_Customer_Invoice
	 * @version     3.5.0
	 * @package     WooCommerce\Classes\Emails
	 * @extends     WC_Email
	 */
	class WC_Email_Customer_Invoice extends WC_Email {

		/**
		 * Constructor.
		 */
		public function __construct() {
			$this->id             = 'customer_invoice';
			$this->customer_email = true;
			$this->title          = __( 'Customer invoice / Order details', 'woocommerce' );
			$this->description    = __( 'Customer invoice emails can be sent to customers containing their order information and payment links.', 'woocommerce' );
			$this->template_html  = 'emails/customer-invoice.php';
			$this->template_plain = 'emails/plain/customer-invoice.php';
			$this->placeholders   = array(
				'{order_date}'   => '',
				'{order_number}' => '',
			);

			// Call parent constructor.
			parent::__construct();

			$this->manual = true;
		}

		/**
		 * Get email subject.
		 *
		 * @param bool $paid Whether the order has been paid or not.
		 * @since  3.1.0
		 * @return string
		 */
		public function get_default_subject( $paid = false ) {
			if ( $paid ) {
				return __( 'Invoice for order #{order_number} on {site_title}', 'woocommerce' );
			} else {
				return __( 'Your latest {site_title} invoice', 'woocommerce' );
			}
		}

		/**
		 * Get email heading.
		 *
		 * @param bool $paid Whether the order has been paid or not.
		 * @since  3.1.0
		 * @return string
		 */
		public function get_default_heading( $paid = false ) {
			if ( $paid ) {
				return __( 'Invoice for order #{order_number}', 'woocommerce' );
			} else {
				return __( 'Your invoice for order #{order_number}', 'woocommerce' );
			}
		}

		/**
		 * Get email subject.
		 *
		 * @return string
		 */
		public function get_subject() {
			if ( $this->object->has_status( array( 'completed', 'processing' ) ) ) {
				$subject = $this->get_option( 'subject_paid', $this->get_default_subject( true ) );

				return apply_filters( 'woocommerce_email_subject_customer_invoice_paid', $this->format_string( $subject ), $this->object, $this );
			}

			$subject = $this->get_option( 'subject', $this->get_default_subject() );
			return apply_filters( 'woocommerce_email_subject_customer_invoice', $this->format_string( $subject ), $this->object, $this );
		}

		/**
		 * Get email heading.
		 *
		 * @return string
		 */
		public function get_heading() {
			if ( $this->object->has_status( wc_get_is_paid_statuses() ) ) {
				$heading = $this->get_option( 'heading_paid', $this->get_default_heading( true ) );
				return apply_filters( 'woocommerce_email_heading_customer_invoice_paid', $this->format_string( $heading ), $this->object, $this );
			}

			$heading = $this->get_option( 'heading', $this->get_default_heading() );
			return apply_filters( 'woocommerce_email_heading_customer_invoice', $this->format_string( $heading ), $this->object, $this );
		}

		/**
		 * Default content to show below main email content.
		 *
		 * @since 3.7.0
		 * @return string
		 */
		public function get_default_additional_content() {
			return __( 'Thanks for using {site_url}!', 'woocommerce' );
		}

		/**
		 * Trigger the sending of this email.
		 *
		 * @param int      $order_id The order ID.
		 * @param WC_Order $order Order object.
		 */
		public function trigger( $order_id, $order = false ) {
			$this->setup_locale();

			if ( $order_id && ! is_a( $order, 'WC_Order' ) ) {
				$order = wc_get_order( $order_id );
			}

			if ( is_a( $order, 'WC_Order' ) ) {
				$this->object                         = $order;
				$this->recipient                      = $this->object->get_billing_email();
				$this->placeholders['{order_date}']   = wc_format_datetime( $this->object->get_date_created() );
				$this->placeholders['{order_number}'] = $this->object->get_order_number();
			}

			if ( $this->get_recipient() ) {
				$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
			}

			$this->restore_locale();
		}

		/**
		 * Get content html.
		 *
		 * @return string
		 */
		public function get_content_html() {
			return wc_get_template_html(
				$this->template_html,
				array(
					'order'              => $this->object,
					'email_heading'      => $this->get_heading(),
					'additional_content' => $this->get_additional_content(),
					'sent_to_admin'      => false,
					'plain_text'         => false,
					'email'              => $this,
				)
			);
		}

		/**
		 * Get content plain.
		 *
		 * @return string
		 */
		public function get_content_plain() {
			return wc_get_template_html(
				$this->template_plain,
				array(
					'order'              => $this->object,
					'email_heading'      => $this->get_heading(),
					'additional_content' => $this->get_additional_content(),
					'sent_to_admin'      => false,
					'plain_text'         => true,
					'email'              => $this,
				)
			);
		}

		/**
		 * Initialise settings form fields.
		 */
		public function init_form_fields() {
			/* translators: %s: list of placeholders */
			$placeholder_text  = sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '<code>' . esc_html( implode( '</code>, <code>', array_keys( $this->placeholders ) ) ) . '</code>' );
			$this->form_fields = array(
				'subject'            => array(
					'title'       => __( 'Subject', 'woocommerce' ),
					'type'        => 'text',
					'desc_tip'    => true,
					'description' => $placeholder_text,
					'placeholder' => $this->get_default_subject(),
					'default'     => '',
				),
				'heading'            => array(
					'title'       => __( 'Email heading', 'woocommerce' ),
					'type'        => 'text',
					'desc_tip'    => true,
					'description' => $placeholder_text,
					'placeholder' => $this->get_default_heading(),
					'default'     => '',
				),
				'subject_paid'       => array(
					'title'       => __( 'Subject (paid)', 'woocommerce' ),
					'type'        => 'text',
					'desc_tip'    => true,
					'description' => $placeholder_text,
					'placeholder' => $this->get_default_subject( true ),
					'default'     => '',
				),
				'heading_paid'       => array(
					'title'       => __( 'Email heading (paid)', 'woocommerce' ),
					'type'        => 'text',
					'desc_tip'    => true,
					'description' => $placeholder_text,
					'placeholder' => $this->get_default_heading( true ),
					'default'     => '',
				),
				'additional_content' => array(
					'title'       => __( 'Additional content', 'woocommerce' ),
					'description' => __( 'Text to appear below the main email content.', 'woocommerce' ) . ' ' . $placeholder_text,
					'css'         => 'width:400px; height: 75px;',
					'placeholder' => __( 'N/A', 'woocommerce' ),
					'type'        => 'textarea',
					'default'     => $this->get_default_additional_content(),
					'desc_tip'    => true,
				),
				'email_type'         => array(
					'title'       => __( 'Email type', 'woocommerce' ),
					'type'        => 'select',
					'description' => __( 'Choose which format of email to send.', 'woocommerce' ),
					'default'     => 'html',
					'class'       => 'email_type wc-enhanced-select',
					'options'     => $this->get_email_type_options(),
					'desc_tip'    => true,
				),
			);
		}
	}

endif;

return new WC_Email_Customer_Invoice();
emails/class-wc-email-customer-new-account.php000064400000010237151336044770015425 0ustar00<?php
/**
 * Class WC_Email_Customer_New_Account file.
 *
 * @package WooCommerce\Emails
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

if ( ! class_exists( 'WC_Email_Customer_New_Account', false ) ) :

	/**
	 * Customer New Account.
	 *
	 * An email sent to the customer when they create an account.
	 *
	 * @class       WC_Email_Customer_New_Account
	 * @version     3.5.0
	 * @package     WooCommerce\Classes\Emails
	 * @extends     WC_Email
	 */
	class WC_Email_Customer_New_Account extends WC_Email {

		/**
		 * User login name.
		 *
		 * @var string
		 */
		public $user_login;

		/**
		 * User email.
		 *
		 * @var string
		 */
		public $user_email;

		/**
		 * User password.
		 *
		 * @var string
		 */
		public $user_pass;

		/**
		 * Is the password generated?
		 *
		 * @var bool
		 */
		public $password_generated;

		/**
		 * Constructor.
		 */
		public function __construct() {
			$this->id             = 'customer_new_account';
			$this->customer_email = true;
			$this->title          = __( 'New account', 'woocommerce' );
			$this->description    = __( 'Customer "new account" emails are sent to the customer when a customer signs up via checkout or account pages.', 'woocommerce' );
			$this->template_html  = 'emails/customer-new-account.php';
			$this->template_plain = 'emails/plain/customer-new-account.php';

			// Call parent constructor.
			parent::__construct();
		}

		/**
		 * Get email subject.
		 *
		 * @since  3.1.0
		 * @return string
		 */
		public function get_default_subject() {
			return __( 'Your {site_title} account has been created!', 'woocommerce' );
		}

		/**
		 * Get email heading.
		 *
		 * @since  3.1.0
		 * @return string
		 */
		public function get_default_heading() {
			return __( 'Welcome to {site_title}', 'woocommerce' );
		}

		/**
		 * Trigger.
		 *
		 * @param int    $user_id User ID.
		 * @param string $user_pass User password.
		 * @param bool   $password_generated Whether the password was generated automatically or not.
		 */
		public function trigger( $user_id, $user_pass = '', $password_generated = false ) {
			$this->setup_locale();

			if ( $user_id ) {
				$this->object = new WP_User( $user_id );

				$this->user_pass          = $user_pass;
				$this->user_login         = stripslashes( $this->object->user_login );
				$this->user_email         = stripslashes( $this->object->user_email );
				$this->recipient          = $this->user_email;
				$this->password_generated = $password_generated;
			}

			if ( $this->is_enabled() && $this->get_recipient() ) {
				$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
			}

			$this->restore_locale();
		}

		/**
		 * Get content html.
		 *
		 * @return string
		 */
		public function get_content_html() {
			return wc_get_template_html(
				$this->template_html,
				array(
					'email_heading'      => $this->get_heading(),
					'additional_content' => $this->get_additional_content(),
					'user_login'         => $this->user_login,
					'user_pass'          => $this->user_pass,
					'blogname'           => $this->get_blogname(),
					'password_generated' => $this->password_generated,
					'sent_to_admin'      => false,
					'plain_text'         => false,
					'email'              => $this,
				)
			);
		}

		/**
		 * Get content plain.
		 *
		 * @return string
		 */
		public function get_content_plain() {
			return wc_get_template_html(
				$this->template_plain,
				array(
					'email_heading'      => $this->get_heading(),
					'additional_content' => $this->get_additional_content(),
					'user_login'         => $this->user_login,
					'user_pass'          => $this->user_pass,
					'blogname'           => $this->get_blogname(),
					'password_generated' => $this->password_generated,
					'sent_to_admin'      => false,
					'plain_text'         => true,
					'email'              => $this,
				)
			);
		}

		/**
		 * Default content to show below main email content.
		 *
		 * @since 3.7.0
		 * @return string
		 */
		public function get_default_additional_content() {
			return __( 'We look forward to seeing you soon.', 'woocommerce' );
		}
	}

endif;

return new WC_Email_Customer_New_Account();
emails/class-wc-email-customer-processing-order.php000064400000010206151336044770016463 0ustar00<?php
/**
 * Class WC_Email_Customer_Processing_Order file.
 *
 * @package WooCommerce\Emails
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

if ( ! class_exists( 'WC_Email_Customer_Processing_Order', false ) ) :

	/**
	 * Customer Processing Order Email.
	 *
	 * An email sent to the customer when a new order is paid for.
	 *
	 * @class       WC_Email_Customer_Processing_Order
	 * @version     3.5.0
	 * @package     WooCommerce\Classes\Emails
	 * @extends     WC_Email
	 */
	class WC_Email_Customer_Processing_Order extends WC_Email {

		/**
		 * Constructor.
		 */
		public function __construct() {
			$this->id             = 'customer_processing_order';
			$this->customer_email = true;

			$this->title          = __( 'Processing order', 'woocommerce' );
			$this->description    = __( 'This is an order notification sent to customers containing order details after payment.', 'woocommerce' );
			$this->template_html  = 'emails/customer-processing-order.php';
			$this->template_plain = 'emails/plain/customer-processing-order.php';
			$this->placeholders   = array(
				'{order_date}'   => '',
				'{order_number}' => '',
			);

			// Triggers for this email.
			add_action( 'woocommerce_order_status_cancelled_to_processing_notification', array( $this, 'trigger' ), 10, 2 );
			add_action( 'woocommerce_order_status_failed_to_processing_notification', array( $this, 'trigger' ), 10, 2 );
			add_action( 'woocommerce_order_status_on-hold_to_processing_notification', array( $this, 'trigger' ), 10, 2 );
			add_action( 'woocommerce_order_status_pending_to_processing_notification', array( $this, 'trigger' ), 10, 2 );

			// Call parent constructor.
			parent::__construct();
		}

		/**
		 * Get email subject.
		 *
		 * @since  3.1.0
		 * @return string
		 */
		public function get_default_subject() {
			return __( 'Your {site_title} order has been received!', 'woocommerce' );
		}

		/**
		 * Get email heading.
		 *
		 * @since  3.1.0
		 * @return string
		 */
		public function get_default_heading() {
			return __( 'Thank you for your order', 'woocommerce' );
		}

		/**
		 * Trigger the sending of this email.
		 *
		 * @param int            $order_id The order ID.
		 * @param WC_Order|false $order Order object.
		 */
		public function trigger( $order_id, $order = false ) {
			$this->setup_locale();

			if ( $order_id && ! is_a( $order, 'WC_Order' ) ) {
				$order = wc_get_order( $order_id );
			}

			if ( is_a( $order, 'WC_Order' ) ) {
				$this->object                         = $order;
				$this->recipient                      = $this->object->get_billing_email();
				$this->placeholders['{order_date}']   = wc_format_datetime( $this->object->get_date_created() );
				$this->placeholders['{order_number}'] = $this->object->get_order_number();
			}

			if ( $this->is_enabled() && $this->get_recipient() ) {
				$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
			}

			$this->restore_locale();
		}

		/**
		 * Get content html.
		 *
		 * @return string
		 */
		public function get_content_html() {
			return wc_get_template_html(
				$this->template_html,
				array(
					'order'              => $this->object,
					'email_heading'      => $this->get_heading(),
					'additional_content' => $this->get_additional_content(),
					'sent_to_admin'      => false,
					'plain_text'         => false,
					'email'              => $this,
				)
			);
		}

		/**
		 * Get content plain.
		 *
		 * @return string
		 */
		public function get_content_plain() {
			return wc_get_template_html(
				$this->template_plain,
				array(
					'order'              => $this->object,
					'email_heading'      => $this->get_heading(),
					'additional_content' => $this->get_additional_content(),
					'sent_to_admin'      => false,
					'plain_text'         => true,
					'email'              => $this,
				)
			);
		}

		/**
		 * Default content to show below main email content.
		 *
		 * @since 3.7.0
		 * @return string
		 */
		public function get_default_additional_content() {
			return __( 'Thanks for using {site_url}!', 'woocommerce' );
		}
	}

endif;

return new WC_Email_Customer_Processing_Order();
emails/class-wc-email.php000064400000101335151336044770011345 0ustar00<?php
/**
 * Class WC_Email file.
 *
 * @package WooCommerce\Emails
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( class_exists( 'WC_Email', false ) ) {
	return;
}

/**
 * Email Class
 *
 * WooCommerce Email Class which is extended by specific email template classes to add emails to WooCommerce
 *
 * @class       WC_Email
 * @version     2.5.0
 * @package     WooCommerce\Classes\Emails
 * @extends     WC_Settings_API
 */
class WC_Email extends WC_Settings_API {

	/**
	 * Email method ID.
	 *
	 * @var String
	 */
	public $id;

	/**
	 * Email method title.
	 *
	 * @var string
	 */
	public $title;

	/**
	 * 'yes' if the method is enabled.
	 *
	 * @var string yes, no
	 */
	public $enabled;

	/**
	 * Description for the email.
	 *
	 * @var string
	 */
	public $description;

	/**
	 * Default heading.
	 *
	 * Supported for backwards compatibility but we recommend overloading the
	 * get_default_x methods instead so localization can be done when needed.
	 *
	 * @var string
	 */
	public $heading = '';

	/**
	 * Default subject.
	 *
	 * Supported for backwards compatibility but we recommend overloading the
	 * get_default_x methods instead so localization can be done when needed.
	 *
	 * @var string
	 */
	public $subject = '';

	/**
	 * Plain text template path.
	 *
	 * @var string
	 */
	public $template_plain;

	/**
	 * HTML template path.
	 *
	 * @var string
	 */
	public $template_html;

	/**
	 * Template path.
	 *
	 * @var string
	 */
	public $template_base;

	/**
	 * Recipients for the email.
	 *
	 * @var string
	 */
	public $recipient;

	/**
	 * Object this email is for, for example a customer, product, or email.
	 *
	 * @var object|bool
	 */
	public $object;

	/**
	 * Mime boundary (for multipart emails).
	 *
	 * @var string
	 */
	public $mime_boundary;

	/**
	 * Mime boundary header (for multipart emails).
	 *
	 * @var string
	 */
	public $mime_boundary_header;

	/**
	 * True when email is being sent.
	 *
	 * @var bool
	 */
	public $sending;

	/**
	 * True when the email notification is sent manually only.
	 *
	 * @var bool
	 */
	protected $manual = false;

	/**
	 * True when the email notification is sent to customers.
	 *
	 * @var bool
	 */
	protected $customer_email = false;

	/**
	 *  List of preg* regular expression patterns to search for,
	 *  used in conjunction with $plain_replace.
	 *  https://raw.github.com/ushahidi/wp-silcc/master/class.html2text.inc
	 *
	 *  @var array $plain_search
	 *  @see $plain_replace
	 */
	public $plain_search = array(
		"/\r/",                                                  // Non-legal carriage return.
		'/&(nbsp|#0*160);/i',                                    // Non-breaking space.
		'/&(quot|rdquo|ldquo|#0*8220|#0*8221|#0*147|#0*148);/i', // Double quotes.
		'/&(apos|rsquo|lsquo|#0*8216|#0*8217);/i',               // Single quotes.
		'/&gt;/i',                                               // Greater-than.
		'/&lt;/i',                                               // Less-than.
		'/&#0*38;/i',                                            // Ampersand.
		'/&amp;/i',                                              // Ampersand.
		'/&(copy|#0*169);/i',                                    // Copyright.
		'/&(trade|#0*8482|#0*153);/i',                           // Trademark.
		'/&(reg|#0*174);/i',                                     // Registered.
		'/&(mdash|#0*151|#0*8212);/i',                           // mdash.
		'/&(ndash|minus|#0*8211|#0*8722);/i',                    // ndash.
		'/&(bull|#0*149|#0*8226);/i',                            // Bullet.
		'/&(pound|#0*163);/i',                                   // Pound sign.
		'/&(euro|#0*8364);/i',                                   // Euro sign.
		'/&(dollar|#0*36);/i',                                   // Dollar sign.
		'/&[^&\s;]+;/i',                                         // Unknown/unhandled entities.
		'/[ ]{2,}/',                                             // Runs of spaces, post-handling.
	);

	/**
	 *  List of pattern replacements corresponding to patterns searched.
	 *
	 *  @var array $plain_replace
	 *  @see $plain_search
	 */
	public $plain_replace = array(
		'',                                             // Non-legal carriage return.
		' ',                                            // Non-breaking space.
		'"',                                            // Double quotes.
		"'",                                            // Single quotes.
		'>',                                            // Greater-than.
		'<',                                            // Less-than.
		'&',                                            // Ampersand.
		'&',                                            // Ampersand.
		'(c)',                                          // Copyright.
		'(tm)',                                         // Trademark.
		'(R)',                                          // Registered.
		'--',                                           // mdash.
		'-',                                            // ndash.
		'*',                                            // Bullet.
		'£',                                            // Pound sign.
		'EUR',                                          // Euro sign. € ?.
		'$',                                            // Dollar sign.
		'',                                             // Unknown/unhandled entities.
		' ',                                             // Runs of spaces, post-handling.
	);

	/**
	 * Strings to find/replace in subjects/headings.
	 *
	 * @var array
	 */
	protected $placeholders = array();

	/**
	 * Strings to find in subjects/headings.
	 *
	 * @deprecated 3.2.0 in favour of placeholders
	 * @var array
	 */
	public $find = array();

	/**
	 * Strings to replace in subjects/headings.
	 *
	 * @deprecated 3.2.0 in favour of placeholders
	 * @var array
	 */
	public $replace = array();

	/**
	 * Constructor.
	 */
	public function __construct() {
		// Find/replace.
		$this->placeholders = array_merge(
			array(
				'{site_title}'   => $this->get_blogname(),
				'{site_address}' => wp_parse_url( home_url(), PHP_URL_HOST ),
				'{site_url}'     => wp_parse_url( home_url(), PHP_URL_HOST ),
			),
			$this->placeholders
		);

		// Init settings.
		$this->init_form_fields();
		$this->init_settings();

		// Default template base if not declared in child constructor.
		if ( is_null( $this->template_base ) ) {
			$this->template_base = WC()->plugin_path() . '/templates/';
		}

		$this->email_type = $this->get_option( 'email_type' );
		$this->enabled    = $this->get_option( 'enabled' );

		add_action( 'phpmailer_init', array( $this, 'handle_multipart' ) );
		add_action( 'woocommerce_update_options_email_' . $this->id, array( $this, 'process_admin_options' ) );
	}

	/**
	 * Handle multipart mail.
	 *
	 * @param  PHPMailer $mailer PHPMailer object.
	 * @return PHPMailer
	 */
	public function handle_multipart( $mailer ) {
		if ( $this->sending && 'multipart' === $this->get_email_type() ) {
			$mailer->AltBody = wordwrap( // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
				preg_replace( $this->plain_search, $this->plain_replace, wp_strip_all_tags( $this->get_content_plain() ) )
			);
			$this->sending   = false;
		}
		return $mailer;
	}

	/**
	 * Format email string.
	 *
	 * @param mixed $string Text to replace placeholders in.
	 * @return string
	 */
	public function format_string( $string ) {
		$find    = array_keys( $this->placeholders );
		$replace = array_values( $this->placeholders );

		// If using legacy find replace, add those to our find/replace arrays first. @todo deprecate in 4.0.0.
		$find    = array_merge( (array) $this->find, $find );
		$replace = array_merge( (array) $this->replace, $replace );

		// Take care of blogname which is no longer defined as a valid placeholder.
		$find[]    = '{blogname}';
		$replace[] = $this->get_blogname();

		// If using the older style filters for find and replace, ensure the array is associative and then pass through filters. @todo deprecate in 4.0.0.
		if ( has_filter( 'woocommerce_email_format_string_replace' ) || has_filter( 'woocommerce_email_format_string_find' ) ) {
			$legacy_find    = $this->find;
			$legacy_replace = $this->replace;

			foreach ( $this->placeholders as $find => $replace ) {
				$legacy_key                    = sanitize_title( str_replace( '_', '-', trim( $find, '{}' ) ) );
				$legacy_find[ $legacy_key ]    = $find;
				$legacy_replace[ $legacy_key ] = $replace;
			}

			$string = str_replace( apply_filters( 'woocommerce_email_format_string_find', $legacy_find, $this ), apply_filters( 'woocommerce_email_format_string_replace', $legacy_replace, $this ), $string );
		}

		/**
		 * Filter for main find/replace.
		 *
		 * @since 3.2.0
		 */
		return apply_filters( 'woocommerce_email_format_string', str_replace( $find, $replace, $string ), $this );
	}

	/**
	 * Set the locale to the store locale for customer emails to make sure emails are in the store language.
	 */
	public function setup_locale() {
		if ( $this->is_customer_email() && apply_filters( 'woocommerce_email_setup_locale', true ) ) {
			wc_switch_to_site_locale();
		}
	}

	/**
	 * Restore the locale to the default locale. Use after finished with setup_locale.
	 */
	public function restore_locale() {
		if ( $this->is_customer_email() && apply_filters( 'woocommerce_email_restore_locale', true ) ) {
			wc_restore_locale();
		}
	}

	/**
	 * Get email subject.
	 *
	 * @since  3.1.0
	 * @return string
	 */
	public function get_default_subject() {
		return $this->subject;
	}

	/**
	 * Get email heading.
	 *
	 * @since  3.1.0
	 * @return string
	 */
	public function get_default_heading() {
		return $this->heading;
	}

	/**
	 * Default content to show below main email content.
	 *
	 * @since 3.7.0
	 * @return string
	 */
	public function get_default_additional_content() {
		return '';
	}

	/**
	 * Return content from the additional_content field.
	 *
	 * Displayed above the footer.
	 *
	 * @since 3.7.0
	 * @return string
	 */
	public function get_additional_content() {
		$content = $this->get_option( 'additional_content', '' );

		return apply_filters( 'woocommerce_email_additional_content_' . $this->id, $this->format_string( $content ), $this->object, $this );
	}

	/**
	 * Get email subject.
	 *
	 * @return string
	 */
	public function get_subject() {
		return apply_filters( 'woocommerce_email_subject_' . $this->id, $this->format_string( $this->get_option( 'subject', $this->get_default_subject() ) ), $this->object, $this );
	}

	/**
	 * Get email heading.
	 *
	 * @return string
	 */
	public function get_heading() {
		return apply_filters( 'woocommerce_email_heading_' . $this->id, $this->format_string( $this->get_option( 'heading', $this->get_default_heading() ) ), $this->object, $this );
	}

	/**
	 * Get valid recipients.
	 *
	 * @return string
	 */
	public function get_recipient() {
		$recipient  = apply_filters( 'woocommerce_email_recipient_' . $this->id, $this->recipient, $this->object, $this );
		$recipients = array_map( 'trim', explode( ',', $recipient ) );
		$recipients = array_filter( $recipients, 'is_email' );
		return implode( ', ', $recipients );
	}

	/**
	 * Get email headers.
	 *
	 * @return string
	 */
	public function get_headers() {
		$header = 'Content-Type: ' . $this->get_content_type() . "\r\n";

		if ( in_array( $this->id, array( 'new_order', 'cancelled_order', 'failed_order' ), true ) ) {
			if ( $this->object && $this->object->get_billing_email() && ( $this->object->get_billing_first_name() || $this->object->get_billing_last_name() ) ) {
				$header .= 'Reply-to: ' . $this->object->get_billing_first_name() . ' ' . $this->object->get_billing_last_name() . ' <' . $this->object->get_billing_email() . ">\r\n";
			}
		} elseif ( $this->get_from_address() && $this->get_from_name() ) {
			$header .= 'Reply-to: ' . $this->get_from_name() . ' <' . $this->get_from_address() . ">\r\n";
		}

		return apply_filters( 'woocommerce_email_headers', $header, $this->id, $this->object, $this );
	}

	/**
	 * Get email attachments.
	 *
	 * @return array
	 */
	public function get_attachments() {
		return apply_filters( 'woocommerce_email_attachments', array(), $this->id, $this->object, $this );
	}

	/**
	 * Return email type.
	 *
	 * @return string
	 */
	public function get_email_type() {
		return $this->email_type && class_exists( 'DOMDocument' ) ? $this->email_type : 'plain';
	}

	/**
	 * Get email content type.
	 *
	 * @param string $default_content_type Default wp_mail() content type.
	 * @return string
	 */
	public function get_content_type( $default_content_type = '' ) {
		switch ( $this->get_email_type() ) {
			case 'html':
				$content_type = 'text/html';
				break;
			case 'multipart':
				$content_type = 'multipart/alternative';
				break;
			default:
				$content_type = 'text/plain';
				break;
		}

		return apply_filters( 'woocommerce_email_content_type', $content_type, $this, $default_content_type );
	}

	/**
	 * Return the email's title
	 *
	 * @return string
	 */
	public function get_title() {
		return apply_filters( 'woocommerce_email_title', $this->title, $this );
	}

	/**
	 * Return the email's description
	 *
	 * @return string
	 */
	public function get_description() {
		return apply_filters( 'woocommerce_email_description', $this->description, $this );
	}

	/**
	 * Proxy to parent's get_option and attempt to localize the result using gettext.
	 *
	 * @param string $key Option key.
	 * @param mixed  $empty_value Value to use when option is empty.
	 * @return string
	 */
	public function get_option( $key, $empty_value = null ) {
		$value = parent::get_option( $key, $empty_value );
		return apply_filters( 'woocommerce_email_get_option', $value, $this, $value, $key, $empty_value );
	}

	/**
	 * Checks if this email is enabled and will be sent.
	 *
	 * @return bool
	 */
	public function is_enabled() {
		return apply_filters( 'woocommerce_email_enabled_' . $this->id, 'yes' === $this->enabled, $this->object, $this );
	}

	/**
	 * Checks if this email is manually sent
	 *
	 * @return bool
	 */
	public function is_manual() {
		return $this->manual;
	}

	/**
	 * Checks if this email is customer focussed.
	 *
	 * @return bool
	 */
	public function is_customer_email() {
		return $this->customer_email;
	}

	/**
	 * Get WordPress blog name.
	 *
	 * @return string
	 */
	public function get_blogname() {
		return wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
	}

	/**
	 * Get email content.
	 *
	 * @return string
	 */
	public function get_content() {
		$this->sending = true;

		if ( 'plain' === $this->get_email_type() ) {
			$email_content = wordwrap( preg_replace( $this->plain_search, $this->plain_replace, wp_strip_all_tags( $this->get_content_plain() ) ), 70 );
		} else {
			$email_content = $this->get_content_html();
		}

		return $email_content;
	}

	/**
	 * Apply inline styles to dynamic content.
	 *
	 * We only inline CSS for html emails, and to do so we use Emogrifier library (if supported).
	 *
	 * @version 4.0.0
	 * @param string|null $content Content that will receive inline styles.
	 * @return string
	 */
	public function style_inline( $content ) {
		if ( in_array( $this->get_content_type(), array( 'text/html', 'multipart/alternative' ), true ) ) {
			ob_start();
			wc_get_template( 'emails/email-styles.php' );
			$css = apply_filters( 'woocommerce_email_styles', ob_get_clean(), $this );

			$emogrifier_class = 'Pelago\\Emogrifier';

			if ( $this->supports_emogrifier() && class_exists( $emogrifier_class ) ) {
				try {
					$emogrifier = new $emogrifier_class( $content, $css );

					do_action( 'woocommerce_emogrifier', $emogrifier, $this );

					$content    = $emogrifier->emogrify();
					$html_prune = \Pelago\Emogrifier\HtmlProcessor\HtmlPruner::fromHtml( $content );
					$html_prune->removeElementsWithDisplayNone();
					$content    = $html_prune->render();
				} catch ( Exception $e ) {
					$logger = wc_get_logger();
					$logger->error( $e->getMessage(), array( 'source' => 'emogrifier' ) );
				}
			} else {
				$content = '<style type="text/css">' . $css . '</style>' . $content;
			}
		}

		return $content;
	}

	/**
	 * Return if emogrifier library is supported.
	 *
	 * @version 4.0.0
	 * @since 3.5.0
	 * @return bool
	 */
	protected function supports_emogrifier() {
		return class_exists( 'DOMDocument' );
	}

	/**
	 * Get the email content in plain text format.
	 *
	 * @return string
	 */
	public function get_content_plain() {
		return '';
	}

	/**
	 * Get the email content in HTML format.
	 *
	 * @return string
	 */
	public function get_content_html() {
		return '';
	}

	/**
	 * Get the from name for outgoing emails.
	 *
	 * @param string $from_name Default wp_mail() name associated with the "from" email address.
	 * @return string
	 */
	public function get_from_name( $from_name = '' ) {
		$from_name = apply_filters( 'woocommerce_email_from_name', get_option( 'woocommerce_email_from_name' ), $this, $from_name );
		return wp_specialchars_decode( esc_html( $from_name ), ENT_QUOTES );
	}

	/**
	 * Get the from address for outgoing emails.
	 *
	 * @param string $from_email Default wp_mail() email address to send from.
	 * @return string
	 */
	public function get_from_address( $from_email = '' ) {
		$from_email = apply_filters( 'woocommerce_email_from_address', get_option( 'woocommerce_email_from_address' ), $this, $from_email );
		return sanitize_email( $from_email );
	}

	/**
	 * Send an email.
	 *
	 * @param string $to Email to.
	 * @param string $subject Email subject.
	 * @param string $message Email message.
	 * @param string $headers Email headers.
	 * @param array  $attachments Email attachments.
	 * @return bool success
	 */
	public function send( $to, $subject, $message, $headers, $attachments ) {
		add_filter( 'wp_mail_from', array( $this, 'get_from_address' ) );
		add_filter( 'wp_mail_from_name', array( $this, 'get_from_name' ) );
		add_filter( 'wp_mail_content_type', array( $this, 'get_content_type' ) );

		$message              = apply_filters( 'woocommerce_mail_content', $this->style_inline( $message ) );
		$mail_callback        = apply_filters( 'woocommerce_mail_callback', 'wp_mail', $this );
		$mail_callback_params = apply_filters( 'woocommerce_mail_callback_params', array( $to, $subject, $message, $headers, $attachments ), $this );
		$return               = $mail_callback( ...$mail_callback_params );

		remove_filter( 'wp_mail_from', array( $this, 'get_from_address' ) );
		remove_filter( 'wp_mail_from_name', array( $this, 'get_from_name' ) );
		remove_filter( 'wp_mail_content_type', array( $this, 'get_content_type' ) );

		/**
		 * Action hook fired when an email is sent.
		 *
		 * @since 5.6.0
		 * @param bool     $return Whether the email was sent successfully.
		 * @param int      $id     Email ID.
		 * @param WC_Email $this   WC_Email instance.
		 */
		do_action( 'woocommerce_email_sent', $return, $this->id, $this );

		return $return;
	}

	/**
	 * Initialise Settings Form Fields - these are generic email options most will use.
	 */
	public function init_form_fields() {
		/* translators: %s: list of placeholders */
		$placeholder_text  = sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '<code>' . esc_html( implode( '</code>, <code>', array_keys( $this->placeholders ) ) ) . '</code>' );
		$this->form_fields = array(
			'enabled'            => array(
				'title'   => __( 'Enable/Disable', 'woocommerce' ),
				'type'    => 'checkbox',
				'label'   => __( 'Enable this email notification', 'woocommerce' ),
				'default' => 'yes',
			),
			'subject'            => array(
				'title'       => __( 'Subject', 'woocommerce' ),
				'type'        => 'text',
				'desc_tip'    => true,
				'description' => $placeholder_text,
				'placeholder' => $this->get_default_subject(),
				'default'     => '',
			),
			'heading'            => array(
				'title'       => __( 'Email heading', 'woocommerce' ),
				'type'        => 'text',
				'desc_tip'    => true,
				'description' => $placeholder_text,
				'placeholder' => $this->get_default_heading(),
				'default'     => '',
			),
			'additional_content' => array(
				'title'       => __( 'Additional content', 'woocommerce' ),
				'description' => __( 'Text to appear below the main email content.', 'woocommerce' ) . ' ' . $placeholder_text,
				'css'         => 'width:400px; height: 75px;',
				'placeholder' => __( 'N/A', 'woocommerce' ),
				'type'        => 'textarea',
				'default'     => $this->get_default_additional_content(),
				'desc_tip'    => true,
			),
			'email_type'         => array(
				'title'       => __( 'Email type', 'woocommerce' ),
				'type'        => 'select',
				'description' => __( 'Choose which format of email to send.', 'woocommerce' ),
				'default'     => 'html',
				'class'       => 'email_type wc-enhanced-select',
				'options'     => $this->get_email_type_options(),
				'desc_tip'    => true,
			),
		);
	}

	/**
	 * Email type options.
	 *
	 * @return array
	 */
	public function get_email_type_options() {
		$types = array( 'plain' => __( 'Plain text', 'woocommerce' ) );

		if ( class_exists( 'DOMDocument' ) ) {
			$types['html']      = __( 'HTML', 'woocommerce' );
			$types['multipart'] = __( 'Multipart', 'woocommerce' );
		}

		return $types;
	}

	/**
	 * Admin Panel Options Processing.
	 */
	public function process_admin_options() {
		// Save regular options.
		parent::process_admin_options();

		$post_data = $this->get_post_data();

		// Save templates.
		if ( isset( $post_data['template_html_code'] ) ) {
			$this->save_template( $post_data['template_html_code'], $this->template_html );
		}
		if ( isset( $post_data['template_plain_code'] ) ) {
			$this->save_template( $post_data['template_plain_code'], $this->template_plain );
		}
	}

	/**
	 * Get template.
	 *
	 * @param  string $type Template type. Can be either 'template_html' or 'template_plain'.
	 * @return string
	 */
	public function get_template( $type ) {
		$type = basename( $type );

		if ( 'template_html' === $type ) {
			return $this->template_html;
		} elseif ( 'template_plain' === $type ) {
			return $this->template_plain;
		}
		return '';
	}

	/**
	 * Save the email templates.
	 *
	 * @since 2.4.0
	 * @param string $template_code Template code.
	 * @param string $template_path Template path.
	 */
	protected function save_template( $template_code, $template_path ) {
		if ( current_user_can( 'edit_themes' ) && ! empty( $template_code ) && ! empty( $template_path ) ) {
			$saved = false;
			$file  = get_stylesheet_directory() . '/' . WC()->template_path() . $template_path;
			$code  = wp_unslash( $template_code );

			if ( is_writeable( $file ) ) { // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writeable
				$f = fopen( $file, 'w+' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen

				if ( false !== $f ) {
					fwrite( $f, $code ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite
					fclose( $f ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose
					$saved = true;
				}
			}

			if ( ! $saved ) {
				$redirect = add_query_arg( 'wc_error', rawurlencode( __( 'Could not write to template file.', 'woocommerce' ) ) );
				wp_safe_redirect( $redirect );
				exit;
			}
		}
	}

	/**
	 * Get the template file in the current theme.
	 *
	 * @param  string $template Template name.
	 *
	 * @return string
	 */
	public function get_theme_template_file( $template ) {
		return get_stylesheet_directory() . '/' . apply_filters( 'woocommerce_template_directory', 'woocommerce', $template ) . '/' . $template;
	}

	/**
	 * Move template action.
	 *
	 * @param string $template_type Template type.
	 */
	protected function move_template_action( $template_type ) {
		$template = $this->get_template( $template_type );
		if ( ! empty( $template ) ) {
			$theme_file = $this->get_theme_template_file( $template );

			if ( wp_mkdir_p( dirname( $theme_file ) ) && ! file_exists( $theme_file ) ) {

				// Locate template file.
				$core_file     = $this->template_base . $template;
				$template_file = apply_filters( 'woocommerce_locate_core_template', $core_file, $template, $this->template_base, $this->id );

				// Copy template file.
				copy( $template_file, $theme_file );

				/**
				 * Action hook fired after copying email template file.
				 *
				 * @param string $template_type The copied template type
				 * @param string $email The email object
				 */
				do_action( 'woocommerce_copy_email_template', $template_type, $this );

				?>
				<div class="updated">
					<p><?php echo esc_html__( 'Template file copied to theme.', 'woocommerce' ); ?></p>
				</div>
				<?php
			}
		}
	}

	/**
	 * Delete template action.
	 *
	 * @param string $template_type Template type.
	 */
	protected function delete_template_action( $template_type ) {
		$template = $this->get_template( $template_type );

		if ( $template ) {
			if ( ! empty( $template ) ) {
				$theme_file = $this->get_theme_template_file( $template );

				if ( file_exists( $theme_file ) ) {
					unlink( $theme_file ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_unlink

					/**
					 * Action hook fired after deleting template file.
					 *
					 * @param string $template The deleted template type
					 * @param string $email The email object
					 */
					do_action( 'woocommerce_delete_email_template', $template_type, $this );
					?>
					<div class="updated">
						<p><?php echo esc_html__( 'Template file deleted from theme.', 'woocommerce' ); ?></p>
					</div>
					<?php
				}
			}
		}
	}

	/**
	 * Admin actions.
	 */
	protected function admin_actions() {
		// Handle any actions.
		if (
			( ! empty( $this->template_html ) || ! empty( $this->template_plain ) )
			&& ( ! empty( $_GET['move_template'] ) || ! empty( $_GET['delete_template'] ) )
			&& 'GET' === $_SERVER['REQUEST_METHOD'] // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
		) {
			if ( empty( $_GET['_wc_email_nonce'] ) || ! wp_verify_nonce( wc_clean( wp_unslash( $_GET['_wc_email_nonce'] ) ), 'woocommerce_email_template_nonce' ) ) {
				wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) );
			}

			if ( ! current_user_can( 'edit_themes' ) ) {
				wp_die( esc_html__( 'You don&#8217;t have permission to do this.', 'woocommerce' ) );
			}

			if ( ! empty( $_GET['move_template'] ) ) {
				$this->move_template_action( wc_clean( wp_unslash( $_GET['move_template'] ) ) );
			}

			if ( ! empty( $_GET['delete_template'] ) ) {
				$this->delete_template_action( wc_clean( wp_unslash( $_GET['delete_template'] ) ) );
			}
		}
	}

	/**
	 * Admin Options.
	 *
	 * Setup the email settings screen.
	 * Override this in your email.
	 *
	 * @since 1.0.0
	 */
	public function admin_options() {
		// Do admin actions.
		$this->admin_actions();
		?>
		<h2><?php echo esc_html( $this->get_title() ); ?> <?php wc_back_link( __( 'Return to emails', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=email' ) ); ?></h2>

		<?php echo wpautop( wp_kses_post( $this->get_description() ) ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?>

		<?php
		/**
		 * Action hook fired before displaying email settings.
		 *
		 * @param string $email The email object
		 */
		do_action( 'woocommerce_email_settings_before', $this );
		?>

		<table class="form-table">
			<?php $this->generate_settings_html(); ?>
		</table>

		<?php
		/**
		 * Action hook fired after displaying email settings.
		 *
		 * @param string $email The email object
		 */
		do_action( 'woocommerce_email_settings_after', $this );
		?>

		<?php

		if ( current_user_can( 'edit_themes' ) && ( ! empty( $this->template_html ) || ! empty( $this->template_plain ) ) ) {
			?>
			<div id="template">
				<?php
				$templates = array(
					'template_html'  => __( 'HTML template', 'woocommerce' ),
					'template_plain' => __( 'Plain text template', 'woocommerce' ),
				);

				foreach ( $templates as $template_type => $title ) :
					$template = $this->get_template( $template_type );

					if ( empty( $template ) ) {
						continue;
					}

					$local_file    = $this->get_theme_template_file( $template );
					$core_file     = $this->template_base . $template;
					$template_file = apply_filters( 'woocommerce_locate_core_template', $core_file, $template, $this->template_base, $this->id );
					$template_dir  = apply_filters( 'woocommerce_template_directory', 'woocommerce', $template );
					?>
					<div class="template <?php echo esc_attr( $template_type ); ?>">
						<h4><?php echo wp_kses_post( $title ); ?></h4>

						<?php if ( file_exists( $local_file ) ) : ?>
							<p>
								<a href="#" class="button toggle_editor"></a>

								<?php if ( is_writable( $local_file ) ) : // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writable ?>
									<a href="<?php echo esc_url( wp_nonce_url( remove_query_arg( array( 'move_template', 'saved' ), add_query_arg( 'delete_template', $template_type ) ), 'woocommerce_email_template_nonce', '_wc_email_nonce' ) ); ?>" class="delete_template button">
										<?php esc_html_e( 'Delete template file', 'woocommerce' ); ?>
									</a>
								<?php endif; ?>

								<?php
								/* translators: %s: Path to template file */
								printf( esc_html__( 'This template has been overridden by your theme and can be found in: %s.', 'woocommerce' ), '<code>' . esc_html( trailingslashit( basename( get_stylesheet_directory() ) ) . $template_dir . '/' . $template ) . '</code>' );
								?>
							</p>

							<div class="editor" style="display:none">
								<textarea class="code" cols="25" rows="20"
								<?php
								if ( ! is_writable( $local_file ) ) : // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writable
									?>
									readonly="readonly" disabled="disabled"
								<?php else : ?>
									data-name="<?php echo esc_attr( $template_type ) . '_code'; ?>"<?php endif; ?>><?php echo esc_html( file_get_contents( $local_file ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents ?></textarea>
							</div>
						<?php elseif ( file_exists( $template_file ) ) : ?>
							<p>
								<a href="#" class="button toggle_editor"></a>

								<?php
								$emails_dir    = get_stylesheet_directory() . '/' . $template_dir . '/emails';
								$templates_dir = get_stylesheet_directory() . '/' . $template_dir;
								$theme_dir     = get_stylesheet_directory();

								if ( is_dir( $emails_dir ) ) {
									$target_dir = $emails_dir;
								} elseif ( is_dir( $templates_dir ) ) {
									$target_dir = $templates_dir;
								} else {
									$target_dir = $theme_dir;
								}

								if ( is_writable( $target_dir ) ) : // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writable
									?>
									<a href="<?php echo esc_url( wp_nonce_url( remove_query_arg( array( 'delete_template', 'saved' ), add_query_arg( 'move_template', $template_type ) ), 'woocommerce_email_template_nonce', '_wc_email_nonce' ) ); ?>" class="button">
										<?php esc_html_e( 'Copy file to theme', 'woocommerce' ); ?>
									</a>
								<?php endif; ?>

								<?php
								/* translators: 1: Path to template file 2: Path to theme folder */
								printf( esc_html__( 'To override and edit this email template copy %1$s to your theme folder: %2$s.', 'woocommerce' ), '<code>' . esc_html( plugin_basename( $template_file ) ) . '</code>', '<code>' . esc_html( trailingslashit( basename( get_stylesheet_directory() ) ) . $template_dir . '/' . $template ) . '</code>' );
								?>
							</p>

							<div class="editor" style="display:none">
								<textarea class="code" readonly="readonly" disabled="disabled" cols="25" rows="20"><?php echo esc_html( file_get_contents( $template_file ) );  // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents ?></textarea>
							</div>
						<?php else : ?>
							<p><?php esc_html_e( 'File was not found.', 'woocommerce' ); ?></p>
						<?php endif; ?>
					</div>
				<?php endforeach; ?>
			</div>

			<?php
			wc_enqueue_js(
				"jQuery( 'select.email_type' ).on( 'change', function() {

					var val = jQuery( this ).val();

					jQuery( '.template_plain, .template_html' ).show();

					if ( val != 'multipart' && val != 'html' ) {
						jQuery('.template_html').hide();
					}

					if ( val != 'multipart' && val != 'plain' ) {
						jQuery('.template_plain').hide();
					}

				}).trigger( 'change' );

				var view = '" . esc_js( __( 'View template', 'woocommerce' ) ) . "';
				var hide = '" . esc_js( __( 'Hide template', 'woocommerce' ) ) . "';

				jQuery( 'a.toggle_editor' ).text( view ).on( 'click', function() {
					var label = hide;

					if ( jQuery( this ).closest(' .template' ).find( '.editor' ).is(':visible') ) {
						var label = view;
					}

					jQuery( this ).text( label ).closest(' .template' ).find( '.editor' ).slideToggle();
					return false;
				} );

				jQuery( 'a.delete_template' ).on( 'click', function() {
					if ( window.confirm('" . esc_js( __( 'Are you sure you want to delete this template file?', 'woocommerce' ) ) . "') ) {
						return true;
					}

					return false;
				});

				jQuery( '.editor textarea' ).on( 'change', function() {
					var name = jQuery( this ).attr( 'data-name' );

					if ( name ) {
						jQuery( this ).attr( 'name', name );
					}
				});"
			);
		}
	}
}
emails/class-wc-email-failed-order.php000064400000014451151336044770013702 0ustar00<?php
/**
 * Class WC_Email_Failed_Order file.
 *
 * @package WooCommerce\Emails
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'WC_Email_Failed_Order', false ) ) :

	/**
	 * Failed Order Email.
	 *
	 * An email sent to the admin when payment fails to go through.
	 *
	 * @class       WC_Email_Failed_Order
	 * @version     2.5.0
	 * @package     WooCommerce\Classes\Emails
	 * @extends     WC_Email
	 */
	class WC_Email_Failed_Order extends WC_Email {

		/**
		 * Constructor.
		 */
		public function __construct() {
			$this->id             = 'failed_order';
			$this->title          = __( 'Failed order', 'woocommerce' );
			$this->description    = __( 'Failed order emails are sent to chosen recipient(s) when orders have been marked failed (if they were previously pending or on-hold).', 'woocommerce' );
			$this->template_html  = 'emails/admin-failed-order.php';
			$this->template_plain = 'emails/plain/admin-failed-order.php';
			$this->placeholders   = array(
				'{order_date}'   => '',
				'{order_number}' => '',
			);

			// Triggers for this email.
			add_action( 'woocommerce_order_status_pending_to_failed_notification', array( $this, 'trigger' ), 10, 2 );
			add_action( 'woocommerce_order_status_on-hold_to_failed_notification', array( $this, 'trigger' ), 10, 2 );

			// Call parent constructor.
			parent::__construct();

			// Other settings.
			$this->recipient = $this->get_option( 'recipient', get_option( 'admin_email' ) );
		}

		/**
		 * Get email subject.
		 *
		 * @since  3.1.0
		 * @return string
		 */
		public function get_default_subject() {
			return __( '[{site_title}]: Order #{order_number} has failed', 'woocommerce' );
		}

		/**
		 * Get email heading.
		 *
		 * @since  3.1.0
		 * @return string
		 */
		public function get_default_heading() {
			return __( 'Order Failed: #{order_number}', 'woocommerce' );
		}

		/**
		 * Trigger the sending of this email.
		 *
		 * @param int            $order_id The order ID.
		 * @param WC_Order|false $order Order object.
		 */
		public function trigger( $order_id, $order = false ) {
			$this->setup_locale();

			if ( $order_id && ! is_a( $order, 'WC_Order' ) ) {
				$order = wc_get_order( $order_id );
			}

			if ( is_a( $order, 'WC_Order' ) ) {
				$this->object                         = $order;
				$this->placeholders['{order_date}']   = wc_format_datetime( $this->object->get_date_created() );
				$this->placeholders['{order_number}'] = $this->object->get_order_number();
			}

			if ( $this->is_enabled() && $this->get_recipient() ) {
				$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
			}

			$this->restore_locale();
		}

		/**
		 * Get content html.
		 *
		 * @return string
		 */
		public function get_content_html() {
			return wc_get_template_html(
				$this->template_html,
				array(
					'order'              => $this->object,
					'email_heading'      => $this->get_heading(),
					'additional_content' => $this->get_additional_content(),
					'sent_to_admin'      => true,
					'plain_text'         => false,
					'email'              => $this,
				)
			);
		}

		/**
		 * Get content plain.
		 *
		 * @return string
		 */
		public function get_content_plain() {
			return wc_get_template_html(
				$this->template_plain,
				array(
					'order'              => $this->object,
					'email_heading'      => $this->get_heading(),
					'additional_content' => $this->get_additional_content(),
					'sent_to_admin'      => true,
					'plain_text'         => true,
					'email'              => $this,
				)
			);
		}

		/**
		 * Default content to show below main email content.
		 *
		 * @since 3.7.0
		 * @return string
		 */
		public function get_default_additional_content() {
			return __( 'Hopefully they’ll be back. Read more about <a href="https://docs.woocommerce.com/document/managing-orders/">troubleshooting failed payments</a>.', 'woocommerce' );
		}

		/**
		 * Initialise settings form fields.
		 */
		public function init_form_fields() {
			/* translators: %s: list of placeholders */
			$placeholder_text  = sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '<code>' . esc_html( implode( '</code>, <code>', array_keys( $this->placeholders ) ) ) . '</code>' );
			$this->form_fields = array(
				'enabled'            => array(
					'title'   => __( 'Enable/Disable', 'woocommerce' ),
					'type'    => 'checkbox',
					'label'   => __( 'Enable this email notification', 'woocommerce' ),
					'default' => 'yes',
				),
				'recipient'          => array(
					'title'       => __( 'Recipient(s)', 'woocommerce' ),
					'type'        => 'text',
					/* translators: %s: WP admin email */
					'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to %s.', 'woocommerce' ), '<code>' . esc_attr( get_option( 'admin_email' ) ) . '</code>' ),
					'placeholder' => '',
					'default'     => '',
					'desc_tip'    => true,
				),
				'subject'            => array(
					'title'       => __( 'Subject', 'woocommerce' ),
					'type'        => 'text',
					'desc_tip'    => true,
					'description' => $placeholder_text,
					'placeholder' => $this->get_default_subject(),
					'default'     => '',
				),
				'heading'            => array(
					'title'       => __( 'Email heading', 'woocommerce' ),
					'type'        => 'text',
					'desc_tip'    => true,
					'description' => $placeholder_text,
					'placeholder' => $this->get_default_heading(),
					'default'     => '',
				),
				'additional_content' => array(
					'title'       => __( 'Additional content', 'woocommerce' ),
					'description' => __( 'Text to appear below the main email content.', 'woocommerce' ) . ' ' . $placeholder_text,
					'css'         => 'width:400px; height: 75px;',
					'placeholder' => __( 'N/A', 'woocommerce' ),
					'type'        => 'textarea',
					'default'     => $this->get_default_additional_content(),
					'desc_tip'    => true,
				),
				'email_type'         => array(
					'title'       => __( 'Email type', 'woocommerce' ),
					'type'        => 'select',
					'description' => __( 'Choose which format of email to send.', 'woocommerce' ),
					'default'     => 'html',
					'class'       => 'email_type wc-enhanced-select',
					'options'     => $this->get_email_type_options(),
					'desc_tip'    => true,
				),
			);
		}
	}

endif;

return new WC_Email_Failed_Order();
class-wc-countries.php000064400000131415151336044770011021 0ustar00<?php
/**
 * WooCommerce countries
 *
 * @package WooCommerce\l10n
 * @version 3.3.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * The WooCommerce countries class stores country/state data.
 */
class WC_Countries {

	/**
	 * Locales list.
	 *
	 * @var array
	 */
	public $locale = array();

	/**
	 * List of address formats for locales.
	 *
	 * @var array
	 */
	public $address_formats = array();

	/**
	 * Auto-load in-accessible properties on demand.
	 *
	 * @param  mixed $key Key.
	 * @return mixed
	 */
	public function __get( $key ) {
		if ( 'countries' === $key ) {
			return $this->get_countries();
		} elseif ( 'states' === $key ) {
			return $this->get_states();
		}
	}

	/**
	 * Get all countries.
	 *
	 * @return array
	 */
	public function get_countries() {
		if ( empty( $this->countries ) ) {
			$this->countries = apply_filters( 'woocommerce_countries', include WC()->plugin_path() . '/i18n/countries.php' );
			if ( apply_filters( 'woocommerce_sort_countries', true ) ) {
				wc_asort_by_locale( $this->countries );
			}
		}

		return $this->countries;
	}

	/**
	 * Check if a given code represents a valid ISO 3166-1 alpha-2 code for a country known to us.
	 *
	 * @since 5.1.0
	 * @param string $country_code The country code to check as a ISO 3166-1 alpha-2 code.
	 * @return bool True if the country is known to us, false otherwise.
	 */
	public function country_exists( $country_code ) {
		return isset( $this->get_countries()[ $country_code ] );
	}

	/**
	 * Get all continents.
	 *
	 * @return array
	 */
	public function get_continents() {
		if ( empty( $this->continents ) ) {
			$this->continents = apply_filters( 'woocommerce_continents', include WC()->plugin_path() . '/i18n/continents.php' );
		}

		return $this->continents;
	}

	/**
	 * Get continent code for a country code.
	 *
	 * @since 2.6.0
	 * @param string $cc Country code.
	 * @return string
	 */
	public function get_continent_code_for_country( $cc ) {
		$cc                 = trim( strtoupper( $cc ) );
		$continents         = $this->get_continents();
		$continents_and_ccs = wp_list_pluck( $continents, 'countries' );
		foreach ( $continents_and_ccs as $continent_code => $countries ) {
			if ( false !== array_search( $cc, $countries, true ) ) {
				return $continent_code;
			}
		}

		return '';
	}

	/**
	 * Get calling code for a country code.
	 *
	 * @since 3.6.0
	 * @param string $cc Country code.
	 * @return string|array Some countries have multiple. The code will be stripped of - and spaces and always be prefixed with +.
	 */
	public function get_country_calling_code( $cc ) {
		$codes = wp_cache_get( 'calling-codes', 'countries' );

		if ( ! $codes ) {
			$codes = include WC()->plugin_path() . '/i18n/phone.php';
			wp_cache_set( 'calling-codes', $codes, 'countries' );
		}

		$calling_code = isset( $codes[ $cc ] ) ? $codes[ $cc ] : '';

		if ( is_array( $calling_code ) ) {
			$calling_code = $calling_code[0];
		}

		return $calling_code;
	}

	/**
	 * Get continents that the store ships to.
	 *
	 * @since 3.6.0
	 * @return array
	 */
	public function get_shipping_continents() {
		$continents             = $this->get_continents();
		$shipping_countries     = $this->get_shipping_countries();
		$shipping_country_codes = array_keys( $shipping_countries );
		$shipping_continents    = array();

		foreach ( $continents as $continent_code => $continent ) {
			if ( count( array_intersect( $continent['countries'], $shipping_country_codes ) ) ) {
				$shipping_continents[ $continent_code ] = $continent;
			}
		}

		return $shipping_continents;
	}

	/**
	 * Load the states.
	 *
	 * @deprecated 3.6.0 This method was used to load state files, but is no longer needed. @see get_states().
	 */
	public function load_country_states() {
		global $states;

		$states       = include WC()->plugin_path() . '/i18n/states.php';
		$this->states = apply_filters( 'woocommerce_states', $states );
	}

	/**
	 * Get the states for a country.
	 *
	 * @param  string $cc Country code.
	 * @return false|array of states
	 */
	public function get_states( $cc = null ) {
		if ( ! isset( $this->states ) ) {
			$this->states = apply_filters( 'woocommerce_states', include WC()->plugin_path() . '/i18n/states.php' );
		}

		if ( ! is_null( $cc ) ) {
			return isset( $this->states[ $cc ] ) ? $this->states[ $cc ] : false;
		} else {
			return $this->states;
		}
	}

	/**
	 * Get the base address (first line) for the store.
	 *
	 * @since 3.1.1
	 * @return string
	 */
	public function get_base_address() {
		$base_address = get_option( 'woocommerce_store_address', '' );
		return apply_filters( 'woocommerce_countries_base_address', $base_address );
	}

	/**
	 * Get the base address (second line) for the store.
	 *
	 * @since 3.1.1
	 * @return string
	 */
	public function get_base_address_2() {
		$base_address_2 = get_option( 'woocommerce_store_address_2', '' );
		return apply_filters( 'woocommerce_countries_base_address_2', $base_address_2 );
	}

	/**
	 * Get the base country for the store.
	 *
	 * @return string
	 */
	public function get_base_country() {
		$default = wc_get_base_location();
		return apply_filters( 'woocommerce_countries_base_country', $default['country'] );
	}

	/**
	 * Get the base state for the store.
	 *
	 * @return string
	 */
	public function get_base_state() {
		$default = wc_get_base_location();
		return apply_filters( 'woocommerce_countries_base_state', $default['state'] );
	}

	/**
	 * Get the base city for the store.
	 *
	 * @version 3.1.1
	 * @return string
	 */
	public function get_base_city() {
		$base_city = get_option( 'woocommerce_store_city', '' );
		return apply_filters( 'woocommerce_countries_base_city', $base_city );
	}

	/**
	 * Get the base postcode for the store.
	 *
	 * @since 3.1.1
	 * @return string
	 */
	public function get_base_postcode() {
		$base_postcode = get_option( 'woocommerce_store_postcode', '' );
		return apply_filters( 'woocommerce_countries_base_postcode', $base_postcode );
	}

	/**
	 * Get countries that the store sells to.
	 *
	 * @return array
	 */
	public function get_allowed_countries() {
		if ( 'all' === get_option( 'woocommerce_allowed_countries' ) ) {
			return apply_filters( 'woocommerce_countries_allowed_countries', $this->countries );
		}

		if ( 'all_except' === get_option( 'woocommerce_allowed_countries' ) ) {
			$except_countries = get_option( 'woocommerce_all_except_countries', array() );

			if ( ! $except_countries ) {
				return $this->countries;
			} else {
				$all_except_countries = $this->countries;
				foreach ( $except_countries as $country ) {
					unset( $all_except_countries[ $country ] );
				}
				return apply_filters( 'woocommerce_countries_allowed_countries', $all_except_countries );
			}
		}

		$countries = array();

		$raw_countries = get_option( 'woocommerce_specific_allowed_countries', array() );

		if ( $raw_countries ) {
			foreach ( $raw_countries as $country ) {
				$countries[ $country ] = $this->countries[ $country ];
			}
		}

		return apply_filters( 'woocommerce_countries_allowed_countries', $countries );
	}

	/**
	 * Get countries that the store ships to.
	 *
	 * @return array
	 */
	public function get_shipping_countries() {
		if ( '' === get_option( 'woocommerce_ship_to_countries' ) ) {
			return $this->get_allowed_countries();
		}

		if ( 'all' === get_option( 'woocommerce_ship_to_countries' ) ) {
			return $this->countries;
		}

		$countries = array();

		$raw_countries = get_option( 'woocommerce_specific_ship_to_countries' );

		if ( $raw_countries ) {
			foreach ( $raw_countries as $country ) {
				$countries[ $country ] = $this->countries[ $country ];
			}
		}

		return apply_filters( 'woocommerce_countries_shipping_countries', $countries );
	}

	/**
	 * Get allowed country states.
	 *
	 * @return array
	 */
	public function get_allowed_country_states() {
		if ( get_option( 'woocommerce_allowed_countries' ) !== 'specific' ) {
			return $this->states;
		}

		$states = array();

		$raw_countries = get_option( 'woocommerce_specific_allowed_countries' );

		if ( $raw_countries ) {
			foreach ( $raw_countries as $country ) {
				if ( isset( $this->states[ $country ] ) ) {
					$states[ $country ] = $this->states[ $country ];
				}
			}
		}

		return apply_filters( 'woocommerce_countries_allowed_country_states', $states );
	}

	/**
	 * Get shipping country states.
	 *
	 * @return array
	 */
	public function get_shipping_country_states() {
		if ( get_option( 'woocommerce_ship_to_countries' ) === '' ) {
			return $this->get_allowed_country_states();
		}

		if ( get_option( 'woocommerce_ship_to_countries' ) !== 'specific' ) {
			return $this->states;
		}

		$states = array();

		$raw_countries = get_option( 'woocommerce_specific_ship_to_countries' );

		if ( $raw_countries ) {
			foreach ( $raw_countries as $country ) {
				if ( ! empty( $this->states[ $country ] ) ) {
					$states[ $country ] = $this->states[ $country ];
				}
			}
		}

		return apply_filters( 'woocommerce_countries_shipping_country_states', $states );
	}

	/**
	 * Gets an array of countries in the EU.
	 *
	 * @param  string $type Type of countries to retrieve. Blank for EU member countries. eu_vat for EU VAT countries.
	 * @return string[]
	 */
	public function get_european_union_countries( $type = '' ) {
		$countries = array( 'AT', 'BE', 'BG', 'CY', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI', 'FR', 'GR', 'HR', 'HU', 'IE', 'IT', 'LT', 'LU', 'LV', 'MT', 'NL', 'PL', 'PT', 'RO', 'SE', 'SI', 'SK' );

		if ( 'eu_vat' === $type ) {
			$countries[] = 'MC';
		}

		return apply_filters( 'woocommerce_european_union_countries', $countries, $type );
	}

	/**
	 * Gets an array of Non-EU countries that use VAT as the Local name for their taxes based on this list - https://en.wikipedia.org/wiki/Value-added_tax#Non-European_Union_countries
	 *
	 * @deprecated 4.0.0
	 * @since 3.9.0
	 * @return string[]
	 */
	public function countries_using_vat() {
		wc_deprecated_function( 'countries_using_vat', '4.0', 'WC_Countries::get_vat_countries' );
		$countries = array( 'AE', 'AL', 'AR', 'AZ', 'BB', 'BH', 'BO', 'BS', 'BY', 'CL', 'CO', 'EC', 'EG', 'ET', 'FJ', 'GH', 'GM', 'GT', 'IL', 'IN', 'IR', 'KN', 'KR', 'KZ', 'LK', 'MD', 'ME', 'MK', 'MN', 'MU', 'MX', 'NA', 'NG', 'NP', 'PS', 'PY', 'RS', 'RU', 'RW', 'SA', 'SV', 'TH', 'TR', 'UA', 'UY', 'UZ', 'VE', 'VN', 'ZA' );

		return apply_filters( 'woocommerce_countries_using_vat', $countries );
	}

	/**
	 * Gets an array of countries using VAT.
	 *
	 * @since 4.0.0
	 * @return string[] of country codes.
	 */
	public function get_vat_countries() {
		$eu_countries  = $this->get_european_union_countries();
		$vat_countries = array( 'AE', 'AL', 'AR', 'AZ', 'BB', 'BH', 'BO', 'BS', 'BY', 'CL', 'CO', 'EC', 'EG', 'ET', 'FJ', 'GB', 'GH', 'GM', 'GT', 'IL', 'IM', 'IN', 'IR', 'KN', 'KR', 'KZ', 'LK', 'MC', 'MD', 'ME', 'MK', 'MN', 'MU', 'MX', 'NA', 'NG', 'NO', 'NP', 'PS', 'PY', 'RS', 'RU', 'RW', 'SA', 'SV', 'TH', 'TR', 'UA', 'UY', 'UZ', 'VE', 'VN', 'ZA' );

		return apply_filters( 'woocommerce_vat_countries', array_merge( $eu_countries, $vat_countries ) );
	}

	/**
	 * Gets the correct string for shipping - either 'to the' or 'to'.
	 *
	 * @param string $country_code Country code.
	 * @return string
	 */
	public function shipping_to_prefix( $country_code = '' ) {
		$country_code = $country_code ? $country_code : WC()->customer->get_shipping_country();
		$countries    = array( 'AE', 'CZ', 'DO', 'GB', 'NL', 'PH', 'US', 'USAF' );
		$return       = in_array( $country_code, $countries, true ) ? __( 'to the', 'woocommerce' ) : __( 'to', 'woocommerce' );

		return apply_filters( 'woocommerce_countries_shipping_to_prefix', $return, $country_code );
	}

	/**
	 * Prefix certain countries with 'the'.
	 *
	 * @param string $country_code Country code.
	 * @return string
	 */
	public function estimated_for_prefix( $country_code = '' ) {
		$country_code = $country_code ? $country_code : $this->get_base_country();
		$countries    = array( 'AE', 'CZ', 'DO', 'GB', 'NL', 'PH', 'US', 'USAF' );
		$return       = in_array( $country_code, $countries, true ) ? __( 'the', 'woocommerce' ) . ' ' : '';

		return apply_filters( 'woocommerce_countries_estimated_for_prefix', $return, $country_code );
	}

	/**
	 * Correctly name tax in some countries VAT on the frontend.
	 *
	 * @return string
	 */
	public function tax_or_vat() {
		$return = in_array( $this->get_base_country(), $this->get_vat_countries(), true ) ? __( 'VAT', 'woocommerce' ) : __( 'Tax', 'woocommerce' );

		return apply_filters( 'woocommerce_countries_tax_or_vat', $return );
	}

	/**
	 * Include the Inc Tax label.
	 *
	 * @return string
	 */
	public function inc_tax_or_vat() {
		$return = in_array( $this->get_base_country(), $this->get_vat_countries(), true ) ? __( '(incl. VAT)', 'woocommerce' ) : __( '(incl. tax)', 'woocommerce' );

		return apply_filters( 'woocommerce_countries_inc_tax_or_vat', $return );
	}

	/**
	 * Include the Ex Tax label.
	 *
	 * @return string
	 */
	public function ex_tax_or_vat() {
		$return = in_array( $this->get_base_country(), $this->get_vat_countries(), true ) ? __( '(ex. VAT)', 'woocommerce' ) : __( '(ex. tax)', 'woocommerce' );

		return apply_filters( 'woocommerce_countries_ex_tax_or_vat', $return );
	}

	/**
	 * Outputs the list of countries and states for use in dropdown boxes.
	 *
	 * @param string $selected_country Selected country.
	 * @param string $selected_state   Selected state.
	 * @param bool   $escape           If we should escape HTML.
	 */
	public function country_dropdown_options( $selected_country = '', $selected_state = '', $escape = false ) {
		if ( $this->countries ) {
			foreach ( $this->countries as $key => $value ) {
				$states = $this->get_states( $key );
				if ( $states ) {
					echo '<optgroup label="' . esc_attr( $value ) . '">';
					foreach ( $states as $state_key => $state_value ) {
						echo '<option value="' . esc_attr( $key ) . ':' . esc_attr( $state_key ) . '"';

						if ( $selected_country === $key && $selected_state === $state_key ) {
							echo ' selected="selected"';
						}

						echo '>' . esc_html( $value ) . ' &mdash; ' . ( $escape ? esc_html( $state_value ) : $state_value ) . '</option>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped

					}
					echo '</optgroup>';
				} else {
					echo '<option';
					if ( $selected_country === $key && '*' === $selected_state ) {
						echo ' selected="selected"';
					}
					echo ' value="' . esc_attr( $key ) . '">' . ( $escape ? esc_html( $value ) : $value ) . '</option>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
				}
			}
		}
	}

	/**
	 * Get country address formats.
	 *
	 * These define how addresses are formatted for display in various countries.
	 *
	 * @return array
	 */
	public function get_address_formats() {
		if ( empty( $this->address_formats ) ) {
			$this->address_formats = apply_filters(
				'woocommerce_localisation_address_formats',
				array(
					'default' => "{name}\n{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{postcode}\n{country}",
					'AT'      => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}",
					'AU'      => "{name}\n{company}\n{address_1}\n{address_2}\n{city} {state} {postcode}\n{country}",
					'BE'      => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}",
					'CA'      => "{company}\n{name}\n{address_1}\n{address_2}\n{city} {state_code} {postcode}\n{country}",
					'CH'      => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}",
					'CL'      => "{company}\n{name}\n{address_1}\n{address_2}\n{state}\n{postcode} {city}\n{country}",
					'CN'      => "{country} {postcode}\n{state}, {city}, {address_2}, {address_1}\n{company}\n{name}",
					'CZ'      => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}",
					'DE'      => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}",
					'DK'      => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}",
					'EE'      => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}",
					'ES'      => "{name}\n{company}\n{address_1}\n{address_2}\n{postcode} {city}\n{state}\n{country}",
					'FI'      => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}",
					'FR'      => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city_upper}\n{country}",
					'HK'      => "{company}\n{first_name} {last_name_upper}\n{address_1}\n{address_2}\n{city_upper}\n{state_upper}\n{country}",
					'HU'      => "{last_name} {first_name}\n{company}\n{city}\n{address_1}\n{address_2}\n{postcode}\n{country}",
					'IN'      => "{company}\n{name}\n{address_1}\n{address_2}\n{city} {postcode}\n{state}, {country}",
					'IS'      => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}",
					'IT'      => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode}\n{city}\n{state_upper}\n{country}",
					'JM'      => "{name}\n{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{postcode_upper}\n{country}",
					'JP'      => "{postcode}\n{state} {city} {address_1}\n{address_2}\n{company}\n{last_name} {first_name}\n{country}",
					'LI'      => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}",
					'NL'      => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}",
					'NO'      => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}",
					'NZ'      => "{name}\n{company}\n{address_1}\n{address_2}\n{city} {postcode}\n{country}",
					'PL'      => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}",
					'PR'      => "{company}\n{name}\n{address_1} {address_2}\n{city} \n{country} {postcode}",
					'PT'      => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}",
					'RS'      => "{name}\n{company}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}",
					'SE'      => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}",
					'SI'      => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}",
					'SK'      => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}",
					'TR'      => "{name}\n{company}\n{address_1}\n{address_2}\n{postcode} {city} {state}\n{country}",
					'TW'      => "{company}\n{last_name} {first_name}\n{address_1}\n{address_2}\n{state}, {city} {postcode}\n{country}",
					'UG'      => "{name}\n{company}\n{address_1}\n{address_2}\n{city}\n{state}, {country}",
					'US'      => "{name}\n{company}\n{address_1}\n{address_2}\n{city}, {state_code} {postcode}\n{country}",
					'VN'      => "{name}\n{company}\n{address_1}\n{city}\n{country}",
				)
			);
		}
		return $this->address_formats;
	}

	/**
	 * Get country address format.
	 *
	 * @param  array  $args Arguments.
	 * @param  string $separator How to separate address lines. @since 3.5.0.
	 * @return string
	 */
	public function get_formatted_address( $args = array(), $separator = '<br/>' ) {
		$default_args = array(
			'first_name' => '',
			'last_name'  => '',
			'company'    => '',
			'address_1'  => '',
			'address_2'  => '',
			'city'       => '',
			'state'      => '',
			'postcode'   => '',
			'country'    => '',
		);

		$args    = array_map( 'trim', wp_parse_args( $args, $default_args ) );
		$state   = $args['state'];
		$country = $args['country'];

		// Get all formats.
		$formats = $this->get_address_formats();

		// Get format for the address' country.
		$format = ( $country && isset( $formats[ $country ] ) ) ? $formats[ $country ] : $formats['default'];

		// Handle full country name.
		$full_country = ( isset( $this->countries[ $country ] ) ) ? $this->countries[ $country ] : $country;

		// Country is not needed if the same as base.
		if ( $country === $this->get_base_country() && ! apply_filters( 'woocommerce_formatted_address_force_country_display', false ) ) {
			$format = str_replace( '{country}', '', $format );
		}

		// Handle full state name.
		$full_state = ( $country && $state && isset( $this->states[ $country ][ $state ] ) ) ? $this->states[ $country ][ $state ] : $state;

		// Substitute address parts into the string.
		$replace = array_map(
			'esc_html',
			apply_filters(
				'woocommerce_formatted_address_replacements',
				array(
					'{first_name}'       => $args['first_name'],
					'{last_name}'        => $args['last_name'],
					'{name}'             => sprintf(
						/* translators: 1: first name 2: last name */
						_x( '%1$s %2$s', 'full name', 'woocommerce' ),
						$args['first_name'],
						$args['last_name']
					),
					'{company}'          => $args['company'],
					'{address_1}'        => $args['address_1'],
					'{address_2}'        => $args['address_2'],
					'{city}'             => $args['city'],
					'{state}'            => $full_state,
					'{postcode}'         => $args['postcode'],
					'{country}'          => $full_country,
					'{first_name_upper}' => wc_strtoupper( $args['first_name'] ),
					'{last_name_upper}'  => wc_strtoupper( $args['last_name'] ),
					'{name_upper}'       => wc_strtoupper(
						sprintf(
							/* translators: 1: first name 2: last name */
							_x( '%1$s %2$s', 'full name', 'woocommerce' ),
							$args['first_name'],
							$args['last_name']
						)
					),
					'{company_upper}'    => wc_strtoupper( $args['company'] ),
					'{address_1_upper}'  => wc_strtoupper( $args['address_1'] ),
					'{address_2_upper}'  => wc_strtoupper( $args['address_2'] ),
					'{city_upper}'       => wc_strtoupper( $args['city'] ),
					'{state_upper}'      => wc_strtoupper( $full_state ),
					'{state_code}'       => wc_strtoupper( $state ),
					'{postcode_upper}'   => wc_strtoupper( $args['postcode'] ),
					'{country_upper}'    => wc_strtoupper( $full_country ),
				),
				$args
			)
		);

		$formatted_address = str_replace( array_keys( $replace ), $replace, $format );

		// Clean up white space.
		$formatted_address = preg_replace( '/  +/', ' ', trim( $formatted_address ) );
		$formatted_address = preg_replace( '/\n\n+/', "\n", $formatted_address );

		// Break newlines apart and remove empty lines/trim commas and white space.
		$formatted_address = array_filter( array_map( array( $this, 'trim_formatted_address_line' ), explode( "\n", $formatted_address ) ) );

		// Add html breaks.
		$formatted_address = implode( $separator, $formatted_address );

		// We're done!
		return $formatted_address;
	}

	/**
	 * Trim white space and commas off a line.
	 *
	 * @param  string $line Line.
	 * @return string
	 */
	private function trim_formatted_address_line( $line ) {
		return trim( $line, ', ' );
	}

	/**
	 * Returns the fields we show by default. This can be filtered later on.
	 *
	 * @return array
	 */
	public function get_default_address_fields() {
		$address_2_label = __( 'Apartment, suite, unit, etc.', 'woocommerce' );

		// If necessary, append '(optional)' to the placeholder: we don't need to worry about the
		// label, though, as woocommerce_form_field() takes care of that.
		if ( 'optional' === get_option( 'woocommerce_checkout_address_2_field', 'optional' ) ) {
			$address_2_placeholder = __( 'Apartment, suite, unit, etc. (optional)', 'woocommerce' );
		} else {
			$address_2_placeholder = $address_2_label;
		}

		$fields = array(
			'first_name' => array(
				'label'        => __( 'First name', 'woocommerce' ),
				'required'     => true,
				'class'        => array( 'form-row-first' ),
				'autocomplete' => 'given-name',
				'priority'     => 10,
			),
			'last_name'  => array(
				'label'        => __( 'Last name', 'woocommerce' ),
				'required'     => true,
				'class'        => array( 'form-row-last' ),
				'autocomplete' => 'family-name',
				'priority'     => 20,
			),
			'company'    => array(
				'label'        => __( 'Company name', 'woocommerce' ),
				'class'        => array( 'form-row-wide' ),
				'autocomplete' => 'organization',
				'priority'     => 30,
				'required'     => 'required' === get_option( 'woocommerce_checkout_company_field', 'optional' ),
			),
			'country'    => array(
				'type'         => 'country',
				'label'        => __( 'Country / Region', 'woocommerce' ),
				'required'     => true,
				'class'        => array( 'form-row-wide', 'address-field', 'update_totals_on_change' ),
				'autocomplete' => 'country',
				'priority'     => 40,
			),
			'address_1'  => array(
				'label'        => __( 'Street address', 'woocommerce' ),
				/* translators: use local order of street name and house number. */
				'placeholder'  => esc_attr__( 'House number and street name', 'woocommerce' ),
				'required'     => true,
				'class'        => array( 'form-row-wide', 'address-field' ),
				'autocomplete' => 'address-line1',
				'priority'     => 50,
			),
			'address_2'  => array(
				'label'        => $address_2_label,
				'label_class'  => array( 'screen-reader-text' ),
				'placeholder'  => esc_attr( $address_2_placeholder ),
				'class'        => array( 'form-row-wide', 'address-field' ),
				'autocomplete' => 'address-line2',
				'priority'     => 60,
				'required'     => 'required' === get_option( 'woocommerce_checkout_address_2_field', 'optional' ),
			),
			'city'       => array(
				'label'        => __( 'Town / City', 'woocommerce' ),
				'required'     => true,
				'class'        => array( 'form-row-wide', 'address-field' ),
				'autocomplete' => 'address-level2',
				'priority'     => 70,
			),
			'state'      => array(
				'type'         => 'state',
				'label'        => __( 'State / County', 'woocommerce' ),
				'required'     => true,
				'class'        => array( 'form-row-wide', 'address-field' ),
				'validate'     => array( 'state' ),
				'autocomplete' => 'address-level1',
				'priority'     => 80,
			),
			'postcode'   => array(
				'label'        => __( 'Postcode / ZIP', 'woocommerce' ),
				'required'     => true,
				'class'        => array( 'form-row-wide', 'address-field' ),
				'validate'     => array( 'postcode' ),
				'autocomplete' => 'postal-code',
				'priority'     => 90,
			),
		);

		if ( 'hidden' === get_option( 'woocommerce_checkout_company_field', 'optional' ) ) {
			unset( $fields['company'] );
		}

		if ( 'hidden' === get_option( 'woocommerce_checkout_address_2_field', 'optional' ) ) {
			unset( $fields['address_2'] );
		}

		$default_address_fields = apply_filters( 'woocommerce_default_address_fields', $fields );
		// Sort each of the fields based on priority.
		uasort( $default_address_fields, 'wc_checkout_fields_uasort_comparison' );

		return $default_address_fields;
	}

	/**
	 * Get JS selectors for fields which are shown/hidden depending on the locale.
	 *
	 * @return array
	 */
	public function get_country_locale_field_selectors() {
		$locale_fields = array(
			'address_1' => '#billing_address_1_field, #shipping_address_1_field',
			'address_2' => '#billing_address_2_field, #shipping_address_2_field',
			'state'     => '#billing_state_field, #shipping_state_field, #calc_shipping_state_field',
			'postcode'  => '#billing_postcode_field, #shipping_postcode_field, #calc_shipping_postcode_field',
			'city'      => '#billing_city_field, #shipping_city_field, #calc_shipping_city_field',
		);
		return apply_filters( 'woocommerce_country_locale_field_selectors', $locale_fields );
	}

	/**
	 * Get country locale settings.
	 *
	 * These locales override the default country selections after a country is chosen.
	 *
	 * @return array
	 */
	public function get_country_locale() {
		if ( empty( $this->locale ) ) {
			$this->locale = apply_filters(
				'woocommerce_get_country_locale',
				array(
					'AE' => array(
						'postcode' => array(
							'required' => false,
							'hidden'   => true,
						),
						'state'    => array(
							'required' => false,
						),
					),
					'AF' => array(
						'state' => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'AO' => array(
						'postcode' => array(
							'required' => false,
							'hidden'   => true,
						),
						'state'    => array(
							'label' => __( 'Province', 'woocommerce' ),
						),
					),
					'AT' => array(
						'postcode' => array(
							'priority' => 65,
						),
						'state'    => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'AU' => array(
						'city'     => array(
							'label' => __( 'Suburb', 'woocommerce' ),
						),
						'postcode' => array(
							'label' => __( 'Postcode', 'woocommerce' ),
						),
						'state'    => array(
							'label' => __( 'State', 'woocommerce' ),
						),
					),
					'AX' => array(
						'postcode' => array(
							'priority' => 65,
						),
						'state'    => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'BA' => array(
						'postcode' => array(
							'priority' => 65,
						),
						'state'    => array(
							'label'    => __( 'Canton', 'woocommerce' ),
							'required' => false,
							'hidden'   => true,
						),
					),
					'BD' => array(
						'postcode' => array(
							'required' => false,
						),
						'state'    => array(
							'label' => __( 'District', 'woocommerce' ),
						),
					),
					'BE' => array(
						'postcode' => array(
							'priority' => 65,
						),
						'state'    => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'BH' => array(
						'postcode' => array(
							'required' => false,
						),
						'state'    => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'BI' => array(
						'state' => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'BO' => array(
						'postcode' => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'BS' => array(
						'postcode' => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'CA' => array(
						'postcode' => array(
							'label' => __( 'Postal code', 'woocommerce' ),
						),
						'state'    => array(
							'label' => __( 'Province', 'woocommerce' ),
						),
					),
					'CH' => array(
						'postcode' => array(
							'priority' => 65,
						),
						'state'    => array(
							'label'    => __( 'Canton', 'woocommerce' ),
							'required' => false,
						),
					),
					'CL' => array(
						'city'     => array(
							'required' => true,
						),
						'postcode' => array(
							'required' => false,
						),
						'state'    => array(
							'label' => __( 'Region', 'woocommerce' ),
						),
					),
					'CN' => array(
						'state' => array(
							'label' => __( 'Province', 'woocommerce' ),
						),
					),
					'CO' => array(
						'postcode' => array(
							'required' => false,
						),
					),
					'CW' => array(
						'postcode' => array(
							'required' => false,
							'hidden'   => true,
						),
						'state'    => array(
							'required' => false,
						),
					),
					'CZ' => array(
						'state' => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'DE' => array(
						'postcode' => array(
							'priority' => 65,
						),
						'state'    => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'DK' => array(
						'postcode' => array(
							'priority' => 65,
						),
						'state'    => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'EE' => array(
						'postcode' => array(
							'priority' => 65,
						),
						'state'    => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'FI' => array(
						'postcode' => array(
							'priority' => 65,
						),
						'state'    => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'FR' => array(
						'postcode' => array(
							'priority' => 65,
						),
						'state'    => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'GH' => array(
						'postcode' => array(
							'required' => false,
						),
						'state'    => array(
							'label' => __( 'Region', 'woocommerce' ),
						),
					),
					'GP' => array(
						'state' => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'GF' => array(
						'state' => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'GR' => array(
						'state' => array(
							'required' => false,
						),
					),
					'GT' => array(
						'postcode' => array(
							'required' => false,
							'hidden'   => true,
						),
						'state'    => array(
							'label' => __( 'Department', 'woocommerce' ),
						),
					),
					'HK' => array(
						'postcode' => array(
							'required' => false,
						),
						'city'     => array(
							'label' => __( 'Town / District', 'woocommerce' ),
						),
						'state'    => array(
							'label' => __( 'Region', 'woocommerce' ),
						),
					),
					'HU' => array(
						'last_name'  => array(
							'class'    => array( 'form-row-first' ),
							'priority' => 10,
						),
						'first_name' => array(
							'class'    => array( 'form-row-last' ),
							'priority' => 20,
						),
						'postcode'   => array(
							'class'    => array( 'form-row-first', 'address-field' ),
							'priority' => 65,
						),
						'city'       => array(
							'class' => array( 'form-row-last', 'address-field' ),
						),
						'address_1'  => array(
							'priority' => 71,
						),
						'address_2'  => array(
							'priority' => 72,
						),
						'state'      => array(
							'label' => __( 'County', 'woocommerce' ),
						),
					),
					'ID' => array(
						'state' => array(
							'label' => __( 'Province', 'woocommerce' ),
						),
					),
					'IE' => array(
						'postcode' => array(
							'required' => false,
							'label'    => __( 'Eircode', 'woocommerce' ),
						),
						'state'    => array(
							'label' => __( 'County', 'woocommerce' ),
						),
					),
					'IS' => array(
						'postcode' => array(
							'priority' => 65,
						),
						'state'    => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'IL' => array(
						'postcode' => array(
							'priority' => 65,
						),
						'state'    => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'IM' => array(
						'state' => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'IN' => array(
						'postcode' => array(
							'label' => __( 'PIN', 'woocommerce' ),
						),
						'state'    => array(
							'label' => __( 'State', 'woocommerce' ),
						),
					),
					'IT' => array(
						'postcode' => array(
							'priority' => 65,
						),
						'state'    => array(
							'required' => true,
							'label'    => __( 'Province', 'woocommerce' ),
						),
					),
					'JM' => array(
						'city'     => array(
							'label' => __( 'Town / City / Post Office', 'woocommerce' ),
						),
						'postcode' => array(
							'required' => false,
							'label'    => __( 'Postal Code', 'woocommerce' ),
						),
						'state'    => array(
							'required' => true,
							'label'    => __( 'Parish', 'woocommerce' ),
						),
					),
					'JP' => array(
						'last_name'  => array(
							'class'    => array( 'form-row-first' ),
							'priority' => 10,
						),
						'first_name' => array(
							'class'    => array( 'form-row-last' ),
							'priority' => 20,
						),
						'postcode'   => array(
							'class'    => array( 'form-row-first', 'address-field' ),
							'priority' => 65,
						),
						'state'      => array(
							'label'    => __( 'Prefecture', 'woocommerce' ),
							'class'    => array( 'form-row-last', 'address-field' ),
							'priority' => 66,
						),
						'city'       => array(
							'priority' => 67,
						),
						'address_1'  => array(
							'priority' => 68,
						),
						'address_2'  => array(
							'priority' => 69,
						),
					),
					'KR' => array(
						'state' => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'KW' => array(
						'state' => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'LV' => array(
						'state' => array(
							'label'    => __( 'Municipality', 'woocommerce' ),
							'required' => false,
						),
					),
					'LB' => array(
						'state' => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'MQ' => array(
						'state' => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'MT' => array(
						'state' => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'MZ' => array(
						'postcode' => array(
							'required' => false,
							'hidden'   => true,
						),
						'state'    => array(
							'label' => __( 'Province', 'woocommerce' ),
						),
					),
					'NL' => array(
						'postcode' => array(
							'priority' => 65,
						),
						'state'    => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'NG' => array(
						'postcode' => array(
							'label'    => __( 'Postcode', 'woocommerce' ),
							'required' => false,
							'hidden'   => true,
						),
						'state'    => array(
							'label' => __( 'State', 'woocommerce' ),
						),
					),
					'NZ' => array(
						'postcode' => array(
							'label' => __( 'Postcode', 'woocommerce' ),
						),
						'state'    => array(
							'required' => false,
							'label'    => __( 'Region', 'woocommerce' ),
						),
					),
					'NO' => array(
						'postcode' => array(
							'priority' => 65,
						),
						'state'    => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'NP' => array(
						'state'    => array(
							'label' => __( 'State / Zone', 'woocommerce' ),
						),
						'postcode' => array(
							'required' => false,
						),
					),
					'PL' => array(
						'postcode' => array(
							'priority' => 65,
						),
						'state'    => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'PR' => array(
						'city'  => array(
							'label' => __( 'Municipality', 'woocommerce' ),
						),
						'state' => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'PT' => array(
						'state' => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'RE' => array(
						'state' => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'RO' => array(
						'state' => array(
							'label'    => __( 'County', 'woocommerce' ),
							'required' => true,
						),
					),
					'RS' => array(
						'city'     => array(
							'required' => true,
						),
						'postcode' => array(
							'required' => true,
						),
						'state'    => array(
							'label'    => __( 'District', 'woocommerce' ),
							'required' => false,
						),
					),
					'SG' => array(
						'state' => array(
							'required' => false,
							'hidden'   => true,
						),
						'city'  => array(
							'required' => false,
						),
					),
					'SK' => array(
						'postcode' => array(
							'priority' => 65,
						),
						'state'    => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'SI' => array(
						'postcode' => array(
							'priority' => 65,
						),
						'state'    => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'SR' => array(
						'postcode' => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'ES' => array(
						'postcode' => array(
							'priority' => 65,
						),
						'state'    => array(
							'label' => __( 'Province', 'woocommerce' ),
						),
					),
					'LI' => array(
						'postcode' => array(
							'priority' => 65,
						),
						'state'    => array(
							'label'    => __( 'Municipality', 'woocommerce' ),
							'required' => false,
						),
					),
					'LK' => array(
						'state' => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'LU' => array(
						'state' => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'MD' => array(
						'state' => array(
							'label' => __( 'Municipality / District', 'woocommerce' ),
						),
					),
					'SE' => array(
						'postcode' => array(
							'priority' => 65,
						),
						'state'    => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'TR' => array(
						'postcode' => array(
							'priority' => 65,
						),
						'state'    => array(
							'label' => __( 'Province', 'woocommerce' ),
						),
					),
					'UG' => array(
						'postcode' => array(
							'required' => false,
							'hidden'   => true,
						),
						'city'     => array(
							'label'    => __( 'Town / Village', 'woocommerce' ),
							'required' => true,
						),
						'state'    => array(
							'label'    => __( 'District', 'woocommerce' ),
							'required' => true,
						),
					),
					'US' => array(
						'postcode' => array(
							'label' => __( 'ZIP Code', 'woocommerce' ),
						),
						'state'    => array(
							'label' => __( 'State', 'woocommerce' ),
						),
					),
					'GB' => array(
						'postcode' => array(
							'label' => __( 'Postcode', 'woocommerce' ),
						),
						'state'    => array(
							'label'    => __( 'County', 'woocommerce' ),
							'required' => false,
						),
					),
					'ST' => array(
						'postcode' => array(
							'required' => false,
							'hidden'   => true,
						),
						'state'    => array(
							'label' => __( 'District', 'woocommerce' ),
						),
					),
					'VN' => array(
						'state'     => array(
							'required' => false,
							'hidden'   => true,
						),
						'postcode'  => array(
							'priority' => 65,
							'required' => false,
							'hidden'   => false,
						),
						'address_2' => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'WS' => array(
						'postcode' => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'YT' => array(
						'state' => array(
							'required' => false,
							'hidden'   => true,
						),
					),
					'ZA' => array(
						'state' => array(
							'label' => __( 'Province', 'woocommerce' ),
						),
					),
					'ZW' => array(
						'postcode' => array(
							'required' => false,
							'hidden'   => true,
						),
					),
				)
			);

			$this->locale = array_intersect_key( $this->locale, array_merge( $this->get_allowed_countries(), $this->get_shipping_countries() ) );

			// Default Locale Can be filtered to override fields in get_address_fields(). Countries with no specific locale will use default.
			$this->locale['default'] = apply_filters( 'woocommerce_get_country_locale_default', $this->get_default_address_fields() );

			// Filter default AND shop base locales to allow overides via a single function. These will be used when changing countries on the checkout.
			if ( ! isset( $this->locale[ $this->get_base_country() ] ) ) {
				$this->locale[ $this->get_base_country() ] = $this->locale['default'];
			}

			$this->locale['default']                   = apply_filters( 'woocommerce_get_country_locale_base', $this->locale['default'] );
			$this->locale[ $this->get_base_country() ] = apply_filters( 'woocommerce_get_country_locale_base', $this->locale[ $this->get_base_country() ] );
		}

		return $this->locale;
	}

	/**
	 * Apply locale and get address fields.
	 *
	 * @param  mixed  $country Country.
	 * @param  string $type    Address type, defaults to 'billing_'.
	 * @return array
	 */
	public function get_address_fields( $country = '', $type = 'billing_' ) {
		if ( ! $country ) {
			$country = $this->get_base_country();
		}

		$fields = $this->get_default_address_fields();
		$locale = $this->get_country_locale();

		if ( isset( $locale[ $country ] ) ) {
			$fields = wc_array_overlay( $fields, $locale[ $country ] );
		}

		// Prepend field keys.
		$address_fields = array();

		foreach ( $fields as $key => $value ) {
			if ( 'state' === $key ) {
				$value['country_field'] = $type . 'country';
				$value['country']       = $country;
			}
			$address_fields[ $type . $key ] = $value;
		}

		// Add email and phone fields.
		if ( 'billing_' === $type ) {
			if ( 'hidden' !== get_option( 'woocommerce_checkout_phone_field', 'required' ) ) {
				$address_fields['billing_phone'] = array(
					'label'        => __( 'Phone', 'woocommerce' ),
					'required'     => 'required' === get_option( 'woocommerce_checkout_phone_field', 'required' ),
					'type'         => 'tel',
					'class'        => array( 'form-row-wide' ),
					'validate'     => array( 'phone' ),
					'autocomplete' => 'tel',
					'priority'     => 100,
				);
			}
			$address_fields['billing_email'] = array(
				'label'        => __( 'Email address', 'woocommerce' ),
				'required'     => true,
				'type'         => 'email',
				'class'        => array( 'form-row-wide' ),
				'validate'     => array( 'email' ),
				'autocomplete' => 'no' === get_option( 'woocommerce_registration_generate_username' ) ? 'email' : 'email username',
				'priority'     => 110,
			);
		}

		/**
		 * Important note on this filter: Changes to address fields can and will be overridden by
		 * the woocommerce_default_address_fields. The locales/default locales apply on top based
		 * on country selection. If you want to change things like the required status of an
		 * address field, filter woocommerce_default_address_fields instead.
		 */
		$address_fields = apply_filters( 'woocommerce_' . $type . 'fields', $address_fields, $country );
		// Sort each of the fields based on priority.
		uasort( $address_fields, 'wc_checkout_fields_uasort_comparison' );

		return $address_fields;
	}
}
class-wc-order-refund.php000064400000011610151336044770011374 0ustar00<?php
/**
 * Order refund. Refunds are based on orders (essentially negative orders) and
 * contain much of the same data.
 *
 * @version 3.0.0
 * @package WooCommerce\Classes
 */

defined( 'ABSPATH' ) || exit;

/**
 * Order refund class.
 */
class WC_Order_Refund extends WC_Abstract_Order {

	/**
	 * Which data store to load.
	 *
	 * @var string
	 */
	protected $data_store_name = 'order-refund';

	/**
	 * This is the name of this object type.
	 *
	 * @var string
	 */
	protected $object_type = 'order_refund';

	/**
	 * Stores product data.
	 *
	 * @var array
	 */
	protected $extra_data = array(
		'amount'           => '',
		'reason'           => '',
		'refunded_by'      => 0,
		'refunded_payment' => false,
	);

	/**
	 * Get internal type (post type.)
	 *
	 * @return string
	 */
	public function get_type() {
		return 'shop_order_refund';
	}

	/**
	 * Get status - always completed for refunds.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_status( $context = 'view' ) {
		return 'completed';
	}

	/**
	 * Get a title for the new post type.
	 */
	public function get_post_title() {
		// @codingStandardsIgnoreStart
		return sprintf( __( 'Refund &ndash; %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Order date parsed by strftime', 'woocommerce' ) ) );
		// @codingStandardsIgnoreEnd
	}

	/**
	 * Get refunded amount.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return int|float
	 */
	public function get_amount( $context = 'view' ) {
		return $this->get_prop( 'amount', $context );
	}

	/**
	 * Get refund reason.
	 *
	 * @since 2.2
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return int|float
	 */
	public function get_reason( $context = 'view' ) {
		return $this->get_prop( 'reason', $context );
	}

	/**
	 * Get ID of user who did the refund.
	 *
	 * @since 3.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return int
	 */
	public function get_refunded_by( $context = 'view' ) {
		return $this->get_prop( 'refunded_by', $context );
	}

	/**
	 * Return if the payment was refunded via API.
	 *
	 * @since  3.3
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return bool
	 */
	public function get_refunded_payment( $context = 'view' ) {
		return $this->get_prop( 'refunded_payment', $context );
	}

	/**
	 * Get formatted refunded amount.
	 *
	 * @since 2.4
	 * @return string
	 */
	public function get_formatted_refund_amount() {
		return apply_filters( 'woocommerce_formatted_refund_amount', wc_price( $this->get_amount(), array( 'currency' => $this->get_currency() ) ), $this );
	}

	/**
	 * Set refunded amount.
	 *
	 * @param string $value Value to set.
	 * @throws WC_Data_Exception Exception if the amount is invalid.
	 */
	public function set_amount( $value ) {
		$this->set_prop( 'amount', wc_format_decimal( $value ) );
	}

	/**
	 * Set refund reason.
	 *
	 * @param string $value Value to set.
	 * @throws WC_Data_Exception Exception if the amount is invalid.
	 */
	public function set_reason( $value ) {
		$this->set_prop( 'reason', $value );
	}

	/**
	 * Set refunded by.
	 *
	 * @param int $value Value to set.
	 * @throws WC_Data_Exception Exception if the amount is invalid.
	 */
	public function set_refunded_by( $value ) {
		$this->set_prop( 'refunded_by', absint( $value ) );
	}

	/**
	 * Set if the payment was refunded via API.
	 *
	 * @since 3.3
	 * @param bool $value Value to set.
	 */
	public function set_refunded_payment( $value ) {
		$this->set_prop( 'refunded_payment', (bool) $value );
	}

	/**
	 * Magic __get method for backwards compatibility.
	 *
	 * @param string $key Value to get.
	 * @return mixed
	 */
	public function __get( $key ) {
		wc_doing_it_wrong( $key, 'Refund properties should not be accessed directly.', '3.0' );
		/**
		 * Maps legacy vars to new getters.
		 */
		if ( 'reason' === $key ) {
			return $this->get_reason();
		} elseif ( 'refund_amount' === $key ) {
			return $this->get_amount();
		}
		return parent::__get( $key );
	}

	/**
	 * Gets an refund from the database.
	 *
	 * @deprecated 3.0
	 * @param int $id (default: 0).
	 * @return bool
	 */
	public function get_refund( $id = 0 ) {
		wc_deprecated_function( 'get_refund', '3.0', 'read' );

		if ( ! $id ) {
			return false;
		}

		$result = get_post( $id );

		if ( $result ) {
			$this->populate( $result );
			return true;
		}

		return false;
	}

	/**
	 * Get refund amount.
	 *
	 * @deprecated 3.0
	 * @return int|float
	 */
	public function get_refund_amount() {
		wc_deprecated_function( 'get_refund_amount', '3.0', 'get_amount' );
		return $this->get_amount();
	}

	/**
	 * Get refund reason.
	 *
	 * @deprecated 3.0
	 * @return int|float
	 */
	public function get_refund_reason() {
		wc_deprecated_function( 'get_refund_reason', '3.0', 'get_reason' );
		return $this->get_reason();
	}
}
class-wc-geolite-integration.php000064400000003764151336044770012764 0ustar00<?php
/**
 * Wrapper for MaxMind GeoLite2 Reader
 *
 * This class provide an interface to handle geolocation and error handling.
 *
 * Requires PHP 5.4+.
 *
 * @package WooCommerce\Classes
 * @since   3.4.0
 * @deprecated 3.9.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Geolite integration class.
 *
 * @deprecated 3.9.0
 */
class WC_Geolite_Integration {

	/**
	 * MaxMind GeoLite2 database path.
	 *
	 * @var string
	 */
	private $database = '';

	/**
	 * Logger instance.
	 *
	 * @var WC_Logger
	 */
	private $log = null;

	/**
	 * Constructor.
	 *
	 * @param string $database MaxMind GeoLite2 database path.
	 */
	public function __construct( $database ) {
		$this->database = $database;
	}

	/**
	 * Get country 2-letters ISO by IP address.
	 * Returns empty string when not able to find any ISO code.
	 *
	 * @param string $ip_address User IP address.
	 * @return string
	 * @deprecated 3.9.0
	 */
	public function get_country_iso( $ip_address ) {
		wc_deprecated_function( 'get_country_iso', '3.9.0' );

		$iso_code = '';

		try {
			$reader = new MaxMind\Db\Reader( $this->database ); // phpcs:ignore PHPCompatibility.LanguageConstructs.NewLanguageConstructs.t_ns_separatorFound
			$data   = $reader->get( $ip_address );

			if ( isset( $data['country']['iso_code'] ) ) {
				$iso_code = $data['country']['iso_code'];
			}

			$reader->close();
		} catch ( Exception $e ) {
			$this->log( $e->getMessage(), 'warning' );
		}

		return sanitize_text_field( strtoupper( $iso_code ) );
	}

	/**
	 * Logging method.
	 *
	 * @param string $message Log message.
	 * @param string $level   Log level.
	 *                        Available options: 'emergency', 'alert',
	 *                        'critical', 'error', 'warning', 'notice',
	 *                        'info' and 'debug'.
	 *                        Defaults to 'info'.
	 */
	private function log( $message, $level = 'info' ) {
		if ( is_null( $this->log ) ) {
			$this->log = wc_get_logger();
		}

		$this->log->log( $level, $message, array( 'source' => 'geoip' ) );
	}
}
class-wc-order-item-product.php000064400000032077151336044770012537 0ustar00<?php
/**
 * Order Line Item (product)
 *
 * @package WooCommerce\Classes
 * @version 3.0.0
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Order item product class.
 */
class WC_Order_Item_Product extends WC_Order_Item {

	/**
	 * Order Data array. This is the core order data exposed in APIs since 3.0.0.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $extra_data = array(
		'product_id'   => 0,
		'variation_id' => 0,
		'quantity'     => 1,
		'tax_class'    => '',
		'subtotal'     => 0,
		'subtotal_tax' => 0,
		'total'        => 0,
		'total_tax'    => 0,
		'taxes'        => array(
			'subtotal' => array(),
			'total'    => array(),
		),
	);

	/*
	|--------------------------------------------------------------------------
	| Setters
	|--------------------------------------------------------------------------
	*/

	/**
	 * Set quantity.
	 *
	 * @param int $value Quantity.
	 */
	public function set_quantity( $value ) {
		$this->set_prop( 'quantity', wc_stock_amount( $value ) );
	}

	/**
	 * Set tax class.
	 *
	 * @param string $value Tax class.
	 */
	public function set_tax_class( $value ) {
		if ( $value && ! in_array( $value, WC_Tax::get_tax_class_slugs(), true ) ) {
			$this->error( 'order_item_product_invalid_tax_class', __( 'Invalid tax class', 'woocommerce' ) );
		}
		$this->set_prop( 'tax_class', $value );
	}

	/**
	 * Set Product ID
	 *
	 * @param int $value Product ID.
	 */
	public function set_product_id( $value ) {
		if ( $value > 0 && 'product' !== get_post_type( absint( $value ) ) ) {
			$this->error( 'order_item_product_invalid_product_id', __( 'Invalid product ID', 'woocommerce' ) );
		}
		$this->set_prop( 'product_id', absint( $value ) );
	}

	/**
	 * Set variation ID.
	 *
	 * @param int $value Variation ID.
	 */
	public function set_variation_id( $value ) {
		if ( $value > 0 && 'product_variation' !== get_post_type( $value ) ) {
			$this->error( 'order_item_product_invalid_variation_id', __( 'Invalid variation ID', 'woocommerce' ) );
		}
		$this->set_prop( 'variation_id', absint( $value ) );
	}

	/**
	 * Line subtotal (before discounts).
	 *
	 * @param string $value Subtotal.
	 */
	public function set_subtotal( $value ) {
		$value = wc_format_decimal( $value );

		if ( ! is_numeric( $value ) ) {
			$value = 0;
		}

		$this->set_prop( 'subtotal', $value );
	}

	/**
	 * Line total (after discounts).
	 *
	 * @param string $value Total.
	 */
	public function set_total( $value ) {
		$value = wc_format_decimal( $value );

		if ( ! is_numeric( $value ) ) {
			$value = 0;
		}

		$this->set_prop( 'total', $value );

		// Subtotal cannot be less than total.
		if ( '' === $this->get_subtotal() || $this->get_subtotal() < $this->get_total() ) {
			$this->set_subtotal( $value );
		}
	}

	/**
	 * Line subtotal tax (before discounts).
	 *
	 * @param string $value Subtotal tax.
	 */
	public function set_subtotal_tax( $value ) {
		$this->set_prop( 'subtotal_tax', wc_format_decimal( $value ) );
	}

	/**
	 * Line total tax (after discounts).
	 *
	 * @param string $value Total tax.
	 */
	public function set_total_tax( $value ) {
		$this->set_prop( 'total_tax', wc_format_decimal( $value ) );
	}

	/**
	 * Set line taxes and totals for passed in taxes.
	 *
	 * @param array $raw_tax_data Raw tax data.
	 */
	public function set_taxes( $raw_tax_data ) {
		$raw_tax_data = maybe_unserialize( $raw_tax_data );
		$tax_data     = array(
			'total'    => array(),
			'subtotal' => array(),
		);
		if ( ! empty( $raw_tax_data['total'] ) && ! empty( $raw_tax_data['subtotal'] ) ) {
			$tax_data['subtotal'] = array_map( 'wc_format_decimal', $raw_tax_data['subtotal'] );
			$tax_data['total']    = array_map( 'wc_format_decimal', $raw_tax_data['total'] );

			// Subtotal cannot be less than total!
			if ( array_sum( $tax_data['subtotal'] ) < array_sum( $tax_data['total'] ) ) {
				$tax_data['subtotal'] = $tax_data['total'];
			}
		}
		$this->set_prop( 'taxes', $tax_data );

		if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) {
			$this->set_total_tax( array_sum( $tax_data['total'] ) );
			$this->set_subtotal_tax( array_sum( $tax_data['subtotal'] ) );
		} else {
			$this->set_total_tax( array_sum( array_map( 'wc_round_tax_total', $tax_data['total'] ) ) );
			$this->set_subtotal_tax( array_sum( array_map( 'wc_round_tax_total', $tax_data['subtotal'] ) ) );
		}
	}

	/**
	 * Set variation data (stored as meta data - write only).
	 *
	 * @param array $data Key/Value pairs.
	 */
	public function set_variation( $data = array() ) {
		if ( is_array( $data ) ) {
			foreach ( $data as $key => $value ) {
				$this->add_meta_data( str_replace( 'attribute_', '', $key ), $value, true );
			}
		}
	}

	/**
	 * Set properties based on passed in product object.
	 *
	 * @param WC_Product $product Product instance.
	 */
	public function set_product( $product ) {
		if ( ! is_a( $product, 'WC_Product' ) ) {
			$this->error( 'order_item_product_invalid_product', __( 'Invalid product', 'woocommerce' ) );
		}
		if ( $product->is_type( 'variation' ) ) {
			$this->set_product_id( $product->get_parent_id() );
			$this->set_variation_id( $product->get_id() );
			$this->set_variation( is_callable( array( $product, 'get_variation_attributes' ) ) ? $product->get_variation_attributes() : array() );
		} else {
			$this->set_product_id( $product->get_id() );
		}
		$this->set_name( $product->get_name() );
		$this->set_tax_class( $product->get_tax_class() );
	}

	/**
	 * Set meta data for backordered products.
	 */
	public function set_backorder_meta() {
		$product = $this->get_product();
		if ( $product && $product->backorders_require_notification() && $product->is_on_backorder( $this->get_quantity() ) ) {
			$this->add_meta_data( apply_filters( 'woocommerce_backordered_item_meta_name', __( 'Backordered', 'woocommerce' ), $this ), $this->get_quantity() - max( 0, $product->get_stock_quantity() ), true );
		}
	}

	/*
	|--------------------------------------------------------------------------
	| Getters
	|--------------------------------------------------------------------------
	*/

	/**
	 * Get order item type.
	 *
	 * @return string
	 */
	public function get_type() {
		return 'line_item';
	}

	/**
	 * Get product ID.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return int
	 */
	public function get_product_id( $context = 'view' ) {
		return $this->get_prop( 'product_id', $context );
	}

	/**
	 * Get variation ID.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return int
	 */
	public function get_variation_id( $context = 'view' ) {
		return $this->get_prop( 'variation_id', $context );
	}

	/**
	 * Get quantity.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return int
	 */
	public function get_quantity( $context = 'view' ) {
		return $this->get_prop( 'quantity', $context );
	}

	/**
	 * Get tax class.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_tax_class( $context = 'view' ) {
		return $this->get_prop( 'tax_class', $context );
	}

	/**
	 * Get subtotal.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_subtotal( $context = 'view' ) {
		return $this->get_prop( 'subtotal', $context );
	}

	/**
	 * Get subtotal tax.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_subtotal_tax( $context = 'view' ) {
		return $this->get_prop( 'subtotal_tax', $context );
	}

	/**
	 * Get total.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_total( $context = 'view' ) {
		return $this->get_prop( 'total', $context );
	}

	/**
	 * Get total tax.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_total_tax( $context = 'view' ) {
		return $this->get_prop( 'total_tax', $context );
	}

	/**
	 * Get taxes.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return array
	 */
	public function get_taxes( $context = 'view' ) {
		return $this->get_prop( 'taxes', $context );
	}

	/**
	 * Get the associated product.
	 *
	 * @return WC_Product|bool
	 */
	public function get_product() {
		if ( $this->get_variation_id() ) {
			$product = wc_get_product( $this->get_variation_id() );
		} else {
			$product = wc_get_product( $this->get_product_id() );
		}

		// Backwards compatible filter from WC_Order::get_product_from_item().
		if ( has_filter( 'woocommerce_get_product_from_item' ) ) {
			$product = apply_filters( 'woocommerce_get_product_from_item', $product, $this, $this->get_order() );
		}

		return apply_filters( 'woocommerce_order_item_product', $product, $this );
	}

	/**
	 * Get the Download URL.
	 *
	 * @param  int $download_id Download ID.
	 * @return string
	 */
	public function get_item_download_url( $download_id ) {
		$order = $this->get_order();

		return $order ? add_query_arg(
			array(
				'download_file' => $this->get_variation_id() ? $this->get_variation_id() : $this->get_product_id(),
				'order'         => $order->get_order_key(),
				'email'         => rawurlencode( $order->get_billing_email() ),
				'key'           => $download_id,
			),
			trailingslashit( home_url() )
		) : '';
	}

	/**
	 * Get any associated downloadable files.
	 *
	 * @return array
	 */
	public function get_item_downloads() {
		$files      = array();
		$product    = $this->get_product();
		$order      = $this->get_order();
		$product_id = $this->get_variation_id() ? $this->get_variation_id() : $this->get_product_id();

		if ( $product && $order && $product->is_downloadable() && $order->is_download_permitted() ) {
			$email_hash         = function_exists( 'hash' ) ? hash( 'sha256', $order->get_billing_email() ) : sha1( $order->get_billing_email() );
			$data_store         = WC_Data_Store::load( 'customer-download' );
			$customer_downloads = $data_store->get_downloads(
				array(
					'user_email' => $order->get_billing_email(),
					'order_id'   => $order->get_id(),
					'product_id' => $product_id,
				)
			);
			foreach ( $customer_downloads as $customer_download ) {
				$download_id = $customer_download->get_download_id();

				if ( $product->has_file( $download_id ) ) {
					$file                  = $product->get_file( $download_id );
					$files[ $download_id ] = $file->get_data();
					$files[ $download_id ]['downloads_remaining'] = $customer_download->get_downloads_remaining();
					$files[ $download_id ]['access_expires']      = $customer_download->get_access_expires();
					$files[ $download_id ]['download_url']        = add_query_arg(
						array(
							'download_file' => $product_id,
							'order'         => $order->get_order_key(),
							'uid'           => $email_hash,
							'key'           => $download_id,
						),
						trailingslashit( home_url() )
					);
				}
			}
		}

		return apply_filters( 'woocommerce_get_item_downloads', $files, $this, $order );
	}

	/**
	 * Get tax status.
	 *
	 * @return string
	 */
	public function get_tax_status() {
		$product = $this->get_product();
		return $product ? $product->get_tax_status() : 'taxable';
	}

	/*
	|--------------------------------------------------------------------------
	| Array Access Methods
	|--------------------------------------------------------------------------
	|
	| For backwards compatibility with legacy arrays.
	|
	*/

	/**
	 * OffsetGet for ArrayAccess/Backwards compatibility.
	 *
	 * @param string $offset Offset.
	 * @return mixed
	 */
	public function offsetGet( $offset ) {
		if ( 'line_subtotal' === $offset ) {
			$offset = 'subtotal';
		} elseif ( 'line_subtotal_tax' === $offset ) {
			$offset = 'subtotal_tax';
		} elseif ( 'line_total' === $offset ) {
			$offset = 'total';
		} elseif ( 'line_tax' === $offset ) {
			$offset = 'total_tax';
		} elseif ( 'line_tax_data' === $offset ) {
			$offset = 'taxes';
		} elseif ( 'qty' === $offset ) {
			$offset = 'quantity';
		}
		return parent::offsetGet( $offset );
	}

	/**
	 * OffsetSet for ArrayAccess/Backwards compatibility.
	 *
	 * @deprecated 4.4.0
	 * @param string $offset Offset.
	 * @param mixed  $value  Value.
	 */
	public function offsetSet( $offset, $value ) {
		wc_deprecated_function( 'WC_Order_Item_Product::offsetSet', '4.4.0', '' );
		if ( 'line_subtotal' === $offset ) {
			$offset = 'subtotal';
		} elseif ( 'line_subtotal_tax' === $offset ) {
			$offset = 'subtotal_tax';
		} elseif ( 'line_total' === $offset ) {
			$offset = 'total';
		} elseif ( 'line_tax' === $offset ) {
			$offset = 'total_tax';
		} elseif ( 'line_tax_data' === $offset ) {
			$offset = 'taxes';
		} elseif ( 'qty' === $offset ) {
			$offset = 'quantity';
		}
		parent::offsetSet( $offset, $value );
	}

	/**
	 * OffsetExists for ArrayAccess.
	 *
	 * @param string $offset Offset.
	 * @return bool
	 */
	public function offsetExists( $offset ) {
		if ( in_array( $offset, array( 'line_subtotal', 'line_subtotal_tax', 'line_total', 'line_tax', 'line_tax_data', 'item_meta_array', 'item_meta', 'qty' ), true ) ) {
			return true;
		}
		return parent::offsetExists( $offset );
	}
}
libraries/wp-background-process.php000064400000024261151336044770013467 0ustar00<?php // @codingStandardsIgnoreLine.
/**
 * Abstract WP_Background_Process class.
 *
 * @package WP-Background-Processing
 * @extends WP_Async_Request
 */

defined( 'ABSPATH' ) || exit;

/**
 * Abstract WP_Background_Process class.
 */
abstract class WP_Background_Process extends WP_Async_Request {

	/**
	 * Action
	 *
	 * (default value: 'background_process')
	 *
	 * @var string
	 * @access protected
	 */
	protected $action = 'background_process';

	/**
	 * Start time of current process.
	 *
	 * (default value: 0)
	 *
	 * @var int
	 * @access protected
	 */
	protected $start_time = 0;

	/**
	 * Cron_hook_identifier
	 *
	 * @var mixed
	 * @access protected
	 */
	protected $cron_hook_identifier;

	/**
	 * Cron_interval_identifier
	 *
	 * @var mixed
	 * @access protected
	 */
	protected $cron_interval_identifier;

	/**
	 * Initiate new background process
	 */
	public function __construct() {
		parent::__construct();

		$this->cron_hook_identifier     = $this->identifier . '_cron';
		$this->cron_interval_identifier = $this->identifier . '_cron_interval';

		add_action( $this->cron_hook_identifier, array( $this, 'handle_cron_healthcheck' ) );
		add_filter( 'cron_schedules', array( $this, 'schedule_cron_healthcheck' ) );
	}

	/**
	 * Dispatch
	 *
	 * @access public
	 * @return void
	 */
	public function dispatch() {
		// Schedule the cron healthcheck.
		$this->schedule_event();

		// Perform remote post.
		return parent::dispatch();
	}

	/**
	 * Push to queue
	 *
	 * @param mixed $data Data.
	 *
	 * @return $this
	 */
	public function push_to_queue( $data ) {
		$this->data[] = $data;

		return $this;
	}

	/**
	 * Save queue
	 *
	 * @return $this
	 */
	public function save() {
		$key = $this->generate_key();

		if ( ! empty( $this->data ) ) {
			update_site_option( $key, $this->data );
		}

		return $this;
	}

	/**
	 * Update queue
	 *
	 * @param string $key Key.
	 * @param array  $data Data.
	 *
	 * @return $this
	 */
	public function update( $key, $data ) {
		if ( ! empty( $data ) ) {
			update_site_option( $key, $data );
		}

		return $this;
	}

	/**
	 * Delete queue
	 *
	 * @param string $key Key.
	 *
	 * @return $this
	 */
	public function delete( $key ) {
		delete_site_option( $key );

		return $this;
	}

	/**
	 * Generate key
	 *
	 * Generates a unique key based on microtime. Queue items are
	 * given a unique key so that they can be merged upon save.
	 *
	 * @param int $length Length.
	 *
	 * @return string
	 */
	protected function generate_key( $length = 64 ) {
		$unique  = md5( microtime() . rand() );
		$prepend = $this->identifier . '_batch_';

		return substr( $prepend . $unique, 0, $length );
	}

	/**
	 * Maybe process queue
	 *
	 * Checks whether data exists within the queue and that
	 * the process is not already running.
	 */
	public function maybe_handle() {
		// Don't lock up other requests while processing
		session_write_close();

		if ( $this->is_process_running() ) {
			// Background process already running.
			wp_die();
		}

		if ( $this->is_queue_empty() ) {
			// No data to process.
			wp_die();
		}

		check_ajax_referer( $this->identifier, 'nonce' );

		$this->handle();

		wp_die();
	}

	/**
	 * Is queue empty
	 *
	 * @return bool
	 */
	protected function is_queue_empty() {
		global $wpdb;

		$table  = $wpdb->options;
		$column = 'option_name';

		if ( is_multisite() ) {
			$table  = $wpdb->sitemeta;
			$column = 'meta_key';
		}

		$key = $this->identifier . '_batch_%';

		$count = $wpdb->get_var( $wpdb->prepare( "
			SELECT COUNT(*)
			FROM {$table}
			WHERE {$column} LIKE %s
		", $key ) );

		return ! ( $count > 0 );
	}

	/**
	 * Is process running
	 *
	 * Check whether the current process is already running
	 * in a background process.
	 */
	protected function is_process_running() {
		if ( get_site_transient( $this->identifier . '_process_lock' ) ) {
			// Process already running.
			return true;
		}

		return false;
	}

	/**
	 * Lock process
	 *
	 * Lock the process so that multiple instances can't run simultaneously.
	 * Override if applicable, but the duration should be greater than that
	 * defined in the time_exceeded() method.
	 */
	protected function lock_process() {
		$this->start_time = time(); // Set start time of current process.

		$lock_duration = ( property_exists( $this, 'queue_lock_time' ) ) ? $this->queue_lock_time : 60; // 1 minute
		$lock_duration = apply_filters( $this->identifier . '_queue_lock_time', $lock_duration );

		set_site_transient( $this->identifier . '_process_lock', microtime(), $lock_duration );
	}

	/**
	 * Unlock process
	 *
	 * Unlock the process so that other instances can spawn.
	 *
	 * @return $this
	 */
	protected function unlock_process() {
		delete_site_transient( $this->identifier . '_process_lock' );

		return $this;
	}

	/**
	 * Get batch
	 *
	 * @return stdClass Return the first batch from the queue
	 */
	protected function get_batch() {
		global $wpdb;

		$table        = $wpdb->options;
		$column       = 'option_name';
		$key_column   = 'option_id';
		$value_column = 'option_value';

		if ( is_multisite() ) {
			$table        = $wpdb->sitemeta;
			$column       = 'meta_key';
			$key_column   = 'meta_id';
			$value_column = 'meta_value';
		}

		$key = $this->identifier . '_batch_%';

		$query = $wpdb->get_row( $wpdb->prepare( "
			SELECT *
			FROM {$table}
			WHERE {$column} LIKE %s
			ORDER BY {$key_column} ASC
			LIMIT 1
		", $key ) );

		$batch       = new stdClass();
		$batch->key  = $query->$column;
		$batch->data = maybe_unserialize( $query->$value_column );

		return $batch;
	}

	/**
	 * Handle
	 *
	 * Pass each queue item to the task handler, while remaining
	 * within server memory and time limit constraints.
	 */
	protected function handle() {
		$this->lock_process();

		do {
			$batch = $this->get_batch();

			foreach ( $batch->data as $key => $value ) {
				$task = $this->task( $value );

				if ( false !== $task ) {
					$batch->data[ $key ] = $task;
				} else {
					unset( $batch->data[ $key ] );
				}

				if ( $this->time_exceeded() || $this->memory_exceeded() ) {
					// Batch limits reached.
					break;
				}
			}

			// Update or delete current batch.
			if ( ! empty( $batch->data ) ) {
				$this->update( $batch->key, $batch->data );
			} else {
				$this->delete( $batch->key );
			}
		} while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() );

		$this->unlock_process();

		// Start next batch or complete process.
		if ( ! $this->is_queue_empty() ) {
			$this->dispatch();
		} else {
			$this->complete();
		}

		wp_die();
	}

	/**
	 * Memory exceeded
	 *
	 * Ensures the batch process never exceeds 90%
	 * of the maximum WordPress memory.
	 *
	 * @return bool
	 */
	protected function memory_exceeded() {
		$memory_limit   = $this->get_memory_limit() * 0.9; // 90% of max memory
		$current_memory = memory_get_usage( true );
		$return         = false;

		if ( $current_memory >= $memory_limit ) {
			$return = true;
		}

		return apply_filters( $this->identifier . '_memory_exceeded', $return );
	}

	/**
	 * Get memory limit
	 *
	 * @return int
	 */
	protected function get_memory_limit() {
		if ( function_exists( 'ini_get' ) ) {
			$memory_limit = ini_get( 'memory_limit' );
		} else {
			// Sensible default.
			$memory_limit = '128M';
		}

		if ( ! $memory_limit || -1 === $memory_limit ) {
			// Unlimited, set to 32GB.
			$memory_limit = '32000M';
		}

		return intval( $memory_limit ) * 1024 * 1024;
	}

	/**
	 * Time exceeded.
	 *
	 * Ensures the batch never exceeds a sensible time limit.
	 * A timeout limit of 30s is common on shared hosting.
	 *
	 * @return bool
	 */
	protected function time_exceeded() {
		$finish = $this->start_time + apply_filters( $this->identifier . '_default_time_limit', 20 ); // 20 seconds
		$return = false;

		if ( time() >= $finish ) {
			$return = true;
		}

		return apply_filters( $this->identifier . '_time_exceeded', $return );
	}

	/**
	 * Complete.
	 *
	 * Override if applicable, but ensure that the below actions are
	 * performed, or, call parent::complete().
	 */
	protected function complete() {
		// Unschedule the cron healthcheck.
		$this->clear_scheduled_event();
	}

	/**
	 * Schedule cron healthcheck
	 *
	 * @access public
	 * @param mixed $schedules Schedules.
	 * @return mixed
	 */
	public function schedule_cron_healthcheck( $schedules ) {
		$interval = apply_filters( $this->identifier . '_cron_interval', 5 );

		if ( property_exists( $this, 'cron_interval' ) ) {
			$interval = apply_filters( $this->identifier . '_cron_interval', $this->cron_interval );
		}

		// Adds every 5 minutes to the existing schedules.
		$schedules[ $this->identifier . '_cron_interval' ] = array(
			'interval' => MINUTE_IN_SECONDS * $interval,
			'display'  => sprintf( __( 'Every %d minutes', 'woocommerce' ), $interval ),
		);

		return $schedules;
	}

	/**
	 * Handle cron healthcheck
	 *
	 * Restart the background process if not already running
	 * and data exists in the queue.
	 */
	public function handle_cron_healthcheck() {
		if ( $this->is_process_running() ) {
			// Background process already running.
			exit;
		}

		if ( $this->is_queue_empty() ) {
			// No data to process.
			$this->clear_scheduled_event();
			exit;
		}

		$this->handle();

		exit;
	}

	/**
	 * Schedule event
	 */
	protected function schedule_event() {
		if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) {
			wp_schedule_event( time(), $this->cron_interval_identifier, $this->cron_hook_identifier );
		}
	}

	/**
	 * Clear scheduled event
	 */
	protected function clear_scheduled_event() {
		$timestamp = wp_next_scheduled( $this->cron_hook_identifier );

		if ( $timestamp ) {
			wp_unschedule_event( $timestamp, $this->cron_hook_identifier );
		}
	}

	/**
	 * Cancel Process
	 *
	 * Stop processing queue items, clear cronjob and delete batch.
	 *
	 */
	public function cancel_process() {
		if ( ! $this->is_queue_empty() ) {
			$batch = $this->get_batch();

			$this->delete( $batch->key );

			wp_clear_scheduled_hook( $this->cron_hook_identifier );
		}

	}

	/**
	 * Task
	 *
	 * Override this method to perform any actions required on each
	 * queue item. Return the modified item for further processing
	 * in the next pass through. Or, return false to remove the
	 * item from the queue.
	 *
	 * @param mixed $item Queue item to iterate over.
	 *
	 * @return mixed
	 */
	abstract protected function task( $item );

}
libraries/class-wc-eval-math.php000064400000032221151336044770012633 0ustar00<?php

use Automattic\Jetpack\Constants;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'WC_Eval_Math', false ) ) {
	/**
	 * Class WC_Eval_Math. Supports basic math only (removed eval function).
	 *
	 * Based on EvalMath by Miles Kaufman Copyright (C) 2005 Miles Kaufmann http://www.twmagic.com/.
	 */
	class WC_Eval_Math {

		/**
		 * Last error.
		 *
		 * @var string
		 */
		public static $last_error = null;

		/**
		 * Variables (and constants).
		 *
		 * @var array
		 */
		public static $v = array( 'e' => 2.71, 'pi' => 3.14 );

		/**
		 * User-defined functions.
		 *
		 * @var array
		 */
		public static $f = array();

		/**
		 * Constants.
		 *
		 * @var array
		 */
		public static $vb = array( 'e', 'pi' );

		/**
		 * Built-in functions.
		 *
		 * @var array
		 */
		public static $fb = array();

		/**
		 * Evaluate maths string.
		 *
		 * @param string  $expr
		 * @return mixed
		 */
		public static function evaluate( $expr ) {
			self::$last_error = null;
			$expr = trim( $expr );
			if ( substr( $expr, -1, 1 ) == ';' ) {
				$expr = substr( $expr, 0, strlen( $expr ) -1 ); // strip semicolons at the end
			}
			// ===============
			// is it a variable assignment?
			if ( preg_match( '/^\s*([a-z]\w*)\s*=\s*(.+)$/', $expr, $matches ) ) {
				if ( in_array( $matches[1], self::$vb ) ) { // make sure we're not assigning to a constant
					return self::trigger( "cannot assign to constant '$matches[1]'" );
				}
				if ( ( $tmp = self::pfx( self::nfx( $matches[2] ) ) ) === false ) {
					return false; // get the result and make sure it's good
				}
				self::$v[ $matches[1] ] = $tmp; // if so, stick it in the variable array
				return self::$v[ $matches[1] ]; // and return the resulting value
				// ===============
				// is it a function assignment?
			} elseif ( preg_match( '/^\s*([a-z]\w*)\s*\(\s*([a-z]\w*(?:\s*,\s*[a-z]\w*)*)\s*\)\s*=\s*(.+)$/', $expr, $matches ) ) {
				$fnn = $matches[1]; // get the function name
				if ( in_array( $matches[1], self::$fb ) ) { // make sure it isn't built in
					return self::trigger( "cannot redefine built-in function '$matches[1]()'" );
				}
				$args = explode( ",", preg_replace( "/\s+/", "", $matches[2] ) ); // get the arguments
				if ( ( $stack = self::nfx( $matches[3] ) ) === false ) {
					return false; // see if it can be converted to postfix
				}
				$stack_size = count( $stack );
				for ( $i = 0; $i < $stack_size; $i++ ) { // freeze the state of the non-argument variables
					$token = $stack[ $i ];
					if ( preg_match( '/^[a-z]\w*$/', $token ) and ! in_array( $token, $args ) ) {
						if ( array_key_exists( $token, self::$v ) ) {
							$stack[ $i ] = self::$v[ $token ];
						} else {
							return self::trigger( "undefined variable '$token' in function definition" );
						}
					}
				}
				self::$f[ $fnn ] = array( 'args' => $args, 'func' => $stack );
				return true;
				// ===============
			} else {
				return self::pfx( self::nfx( $expr ) ); // straight up evaluation, woo
			}
		}

		/**
		 * Convert infix to postfix notation.
		 *
		 * @param  string $expr
		 *
		 * @return array|string
		 */
		private static function nfx( $expr ) {

			$index = 0;
			$stack = new WC_Eval_Math_Stack;
			$output = array(); // postfix form of expression, to be passed to pfx()
			$expr = trim( $expr );

			$ops   = array( '+', '-', '*', '/', '^', '_' );
			$ops_r = array( '+' => 0, '-' => 0, '*' => 0, '/' => 0, '^' => 1 ); // right-associative operator?
			$ops_p = array( '+' => 0, '-' => 0, '*' => 1, '/' => 1, '_' => 1, '^' => 2 ); // operator precedence

			$expecting_op = false; // we use this in syntax-checking the expression
			// and determining when a - is a negation
			if ( preg_match( "/[^\w\s+*^\/()\.,-]/", $expr, $matches ) ) { // make sure the characters are all good
				return self::trigger( "illegal character '{$matches[0]}'" );
			}

			while ( 1 ) { // 1 Infinite Loop ;)
				$op = substr( $expr, $index, 1 ); // get the first character at the current index
				// find out if we're currently at the beginning of a number/variable/function/parenthesis/operand
				$ex = preg_match( '/^([A-Za-z]\w*\(?|\d+(?:\.\d*)?|\.\d+|\()/', substr( $expr, $index ), $match );
				// ===============
				if ( '-' === $op and ! $expecting_op ) { // is it a negation instead of a minus?
					$stack->push( '_' ); // put a negation on the stack
					$index++;
				} elseif ( '_' === $op ) { // we have to explicitly deny this, because it's legal on the stack
					return self::trigger( "illegal character '_'" ); // but not in the input expression
					// ===============
				} elseif ( ( in_array( $op, $ops ) or $ex ) and $expecting_op ) { // are we putting an operator on the stack?
					if ( $ex ) { // are we expecting an operator but have a number/variable/function/opening parenthesis?
						$op = '*';
						$index--; // it's an implicit multiplication
					}
					// heart of the algorithm:
					while ( $stack->count > 0 and ( $o2 = $stack->last() ) and in_array( $o2, $ops ) and ( $ops_r[ $op ] ? $ops_p[ $op ] < $ops_p[ $o2 ] : $ops_p[ $op ] <= $ops_p[ $o2 ] ) ) {
						$output[] = $stack->pop(); // pop stuff off the stack into the output
					}
					// many thanks: https://en.wikipedia.org/wiki/Reverse_Polish_notation#The_algorithm_in_detail
					$stack->push( $op ); // finally put OUR operator onto the stack
					$index++;
					$expecting_op = false;
					// ===============
				} elseif ( ')' === $op && $expecting_op ) { // ready to close a parenthesis?
					while ( ( $o2 = $stack->pop() ) != '(' ) { // pop off the stack back to the last (
						if ( is_null( $o2 ) ) {
							return self::trigger( "unexpected ')'" );
						} else {
							$output[] = $o2;
						}
					}
					if ( preg_match( "/^([A-Za-z]\w*)\($/", $stack->last( 2 ), $matches ) ) { // did we just close a function?
						$fnn = $matches[1]; // get the function name
						$arg_count = $stack->pop(); // see how many arguments there were (cleverly stored on the stack, thank you)
						$output[] = $stack->pop(); // pop the function and push onto the output
						if ( in_array( $fnn, self::$fb ) ) { // check the argument count
							if ( $arg_count > 1 ) {
								return self::trigger( "too many arguments ($arg_count given, 1 expected)" );
							}
						} elseif ( array_key_exists( $fnn, self::$f ) ) {
							if ( count( self::$f[ $fnn ]['args'] ) != $arg_count ) {
								return self::trigger( "wrong number of arguments ($arg_count given, " . count( self::$f[ $fnn ]['args'] ) . " expected)" );
							}
						} else { // did we somehow push a non-function on the stack? this should never happen
							return self::trigger( "internal error" );
						}
					}
					$index++;
					// ===============
				} elseif ( ',' === $op and $expecting_op ) { // did we just finish a function argument?
					while ( ( $o2 = $stack->pop() ) != '(' ) {
						if ( is_null( $o2 ) ) {
							return self::trigger( "unexpected ','" ); // oops, never had a (
						} else {
							$output[] = $o2; // pop the argument expression stuff and push onto the output
						}
					}
					// make sure there was a function
					if ( ! preg_match( "/^([A-Za-z]\w*)\($/", $stack->last( 2 ), $matches ) ) {
						return self::trigger( "unexpected ','" );
					}
					$stack->push( $stack->pop() + 1 ); // increment the argument count
					$stack->push( '(' ); // put the ( back on, we'll need to pop back to it again
					$index++;
					$expecting_op = false;
					// ===============
				} elseif ( '(' === $op and ! $expecting_op ) {
					$stack->push( '(' ); // that was easy
					$index++;
					// ===============
				} elseif ( $ex and ! $expecting_op ) { // do we now have a function/variable/number?
					$expecting_op = true;
					$val = $match[1];
					if ( preg_match( "/^([A-Za-z]\w*)\($/", $val, $matches ) ) { // may be func, or variable w/ implicit multiplication against parentheses...
						if ( in_array( $matches[1], self::$fb ) or array_key_exists( $matches[1], self::$f ) ) { // it's a func
							$stack->push( $val );
							$stack->push( 1 );
							$stack->push( '(' );
							$expecting_op = false;
						} else { // it's a var w/ implicit multiplication
							$val = $matches[1];
							$output[] = $val;
						}
					} else { // it's a plain old var or num
						$output[] = $val;
					}
					$index += strlen( $val );
					// ===============
				} elseif ( ')' === $op ) { // miscellaneous error checking
					return self::trigger( "unexpected ')'" );
				} elseif ( in_array( $op, $ops ) and ! $expecting_op ) {
					return self::trigger( "unexpected operator '$op'" );
				} else { // I don't even want to know what you did to get here
					return self::trigger( "an unexpected error occurred" );
				}
				if ( strlen( $expr ) == $index ) {
					if ( in_array( $op, $ops ) ) { // did we end with an operator? bad.
						return self::trigger( "operator '$op' lacks operand" );
					} else {
						break;
					}
				}
				while ( substr( $expr, $index, 1 ) == ' ' ) { // step the index past whitespace (pretty much turns whitespace
					$index++;                             // into implicit multiplication if no operator is there)
				}
			}
			while ( ! is_null( $op = $stack->pop() ) ) { // pop everything off the stack and push onto output
				if ( '(' === $op ) {
					return self::trigger( "expecting ')'" ); // if there are (s on the stack, ()s were unbalanced
				}
				$output[] = $op;
			}
			return $output;
		}

		/**
		 * Evaluate postfix notation.
		 *
		 * @param  mixed $tokens
		 * @param  array $vars
		 *
		 * @return mixed
		 */
		private static function pfx( $tokens, $vars = array() ) {
			if ( false == $tokens ) {
				return false;
			}
			$stack = new WC_Eval_Math_Stack;

			foreach ( $tokens as $token ) { // nice and easy
				// if the token is a binary operator, pop two values off the stack, do the operation, and push the result back on
				if ( in_array( $token, array( '+', '-', '*', '/', '^' ) ) ) {
					if ( is_null( $op2 = $stack->pop() ) ) {
						return self::trigger( "internal error" );
					}
					if ( is_null( $op1 = $stack->pop() ) ) {
						return self::trigger( "internal error" );
					}
					switch ( $token ) {
						case '+':
							$stack->push( $op1 + $op2 );
							break;
						case '-':
							$stack->push( $op1 - $op2 );
							break;
						case '*':
							$stack->push( $op1 * $op2 );
							break;
						case '/':
							if ( 0 == $op2 ) {
								return self::trigger( 'division by zero' );
							}
							$stack->push( $op1 / $op2 );
							break;
						case '^':
							$stack->push( pow( $op1, $op2 ) );
							break;
					}
					// if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
				} elseif ( '_' === $token ) {
					$stack->push( -1 * $stack->pop() );
					// if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on
				} elseif ( ! preg_match( "/^([a-z]\w*)\($/", $token, $matches ) ) {
					if ( is_numeric( $token ) ) {
						$stack->push( $token );
					} elseif ( array_key_exists( $token, self::$v ) ) {
						$stack->push( self::$v[ $token ] );
					} elseif ( array_key_exists( $token, $vars ) ) {
						$stack->push( $vars[ $token ] );
					} else {
						return self::trigger( "undefined variable '$token'" );
					}
				}
			}
			// when we're out of tokens, the stack should have a single element, the final result
			if ( 1 != $stack->count ) {
				return self::trigger( "internal error" );
			}
			return $stack->pop();
		}

		/**
		 * Trigger an error, but nicely, if need be.
		 *
		 * @param  string $msg
		 *
		 * @return bool
		 */
		private static function trigger( $msg ) {
			self::$last_error = $msg;
			if ( ! Constants::is_true( 'DOING_AJAX' ) && Constants::is_true( 'WP_DEBUG' ) ) {
				echo "\nError found in:";
				self::debugPrintCallingFunction();
				trigger_error( $msg, E_USER_WARNING );
			}
			return false;
		}

		/**
		 * Prints the file name, function name, and
		 * line number which called your function
		 * (not this function, then one that  called
		 * it to begin with)
		 */
		private static function debugPrintCallingFunction() {
			$file = 'n/a';
			$func = 'n/a';
			$line = 'n/a';
			$debugTrace = debug_backtrace();
			if ( isset( $debugTrace[1] ) ) {
				$file = $debugTrace[1]['file'] ? $debugTrace[1]['file'] : 'n/a';
				$line = $debugTrace[1]['line'] ? $debugTrace[1]['line'] : 'n/a';
			}
			if ( isset( $debugTrace[2] ) ) {
				$func = $debugTrace[2]['function'] ? $debugTrace[2]['function'] : 'n/a';
			}
			echo "\n$file, $func, $line\n";
		}
	}

	/**
	 * Class WC_Eval_Math_Stack.
	 */
	class WC_Eval_Math_Stack {

		/**
		 * Stack array.
		 *
		 * @var array
		 */
		public $stack = array();

		/**
		 * Stack counter.
		 *
		 * @var integer
		 */
		public $count = 0;

		/**
		 * Push value into stack.
		 *
		 * @param  mixed $val
		 */
		public function push( $val ) {
			$this->stack[ $this->count ] = $val;
			$this->count++;
		}

		/**
		 * Pop value from stack.
		 *
		 * @return mixed
		 */
		public function pop() {
			if ( $this->count > 0 ) {
				$this->count--;
				return $this->stack[ $this->count ];
			}
			return null;
		}

		/**
		 * Get last value from stack.
		 *
		 * @param  int $n
		 *
		 * @return mixed
		 */
		public function last( $n=1 ) {
			$key = $this->count - $n;
			return array_key_exists( $key, $this->stack ) ? $this->stack[ $key ] : null;
		}
	}
}
libraries/wp-async-request.php000064400000005244151336044770012477 0ustar00<?php
/**
 * WP Async Request
 *
 * @package WP-Background-Processing
 */

defined( 'ABSPATH' ) || exit;

/**
 * Abstract WP_Async_Request class.
 */
abstract class WP_Async_Request {

	/**
	 * Prefix
	 *
	 * (default value: 'wp')
	 *
	 * @var string
	 * @access protected
	 */
	protected $prefix = 'wp';

	/**
	 * Action
	 *
	 * (default value: 'async_request')
	 *
	 * @var string
	 * @access protected
	 */
	protected $action = 'async_request';

	/**
	 * Identifier
	 *
	 * @var mixed
	 * @access protected
	 */
	protected $identifier;

	/**
	 * Data
	 *
	 * (default value: array())
	 *
	 * @var array
	 * @access protected
	 */
	protected $data = array();

	/**
	 * Initiate new async request
	 */
	public function __construct() {
		$this->identifier = $this->prefix . '_' . $this->action;

		add_action( 'wp_ajax_' . $this->identifier, array( $this, 'maybe_handle' ) );
		add_action( 'wp_ajax_nopriv_' . $this->identifier, array( $this, 'maybe_handle' ) );
	}

	/**
	 * Set data used during the request
	 *
	 * @param array $data Data.
	 *
	 * @return $this
	 */
	public function data( $data ) {
		$this->data = $data;

		return $this;
	}

	/**
	 * Dispatch the async request
	 *
	 * @return array|WP_Error
	 */
	public function dispatch() {
		$url  = add_query_arg( $this->get_query_args(), $this->get_query_url() );
		$args = $this->get_post_args();

		return wp_remote_post( esc_url_raw( $url ), $args );
	}

	/**
	 * Get query args
	 *
	 * @return array
	 */
	protected function get_query_args() {
		if ( property_exists( $this, 'query_args' ) ) {
			return $this->query_args;
		}

		return array(
			'action' => $this->identifier,
			'nonce'  => wp_create_nonce( $this->identifier ),
		);
	}

	/**
	 * Get query URL
	 *
	 * @return string
	 */
	protected function get_query_url() {
		if ( property_exists( $this, 'query_url' ) ) {
			return $this->query_url;
		}

		return admin_url( 'admin-ajax.php' );
	}

	/**
	 * Get post args
	 *
	 * @return array
	 */
	protected function get_post_args() {
		if ( property_exists( $this, 'post_args' ) ) {
			return $this->post_args;
		}

		return array(
			'timeout'   => 0.01,
			'blocking'  => false,
			'body'      => $this->data,
			'cookies'   => $_COOKIE,
			'sslverify' => apply_filters( 'https_local_ssl_verify', false ),
		);
	}

	/**
	 * Maybe handle
	 *
	 * Check for correct nonce and pass to handler.
	 */
	public function maybe_handle() {
		// Don't lock up other requests while processing
		session_write_close();

		check_ajax_referer( $this->identifier, 'nonce' );

		$this->handle();

		wp_die();
	}

	/**
	 * Handle
	 *
	 * Override this method to perform any actions required
	 * during the async request.
	 */
	abstract protected function handle();

}
class-wc-customer-download-log.php000064400000006574151336044770013242 0ustar00<?php
/**
 * Class for customer download logs.
 *
 * @package WooCommerce\Classes
 * @version 3.3.0
 * @since   3.3.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Customer download log class.
 */
class WC_Customer_Download_Log extends WC_Data {

	/**
	 * This is the name of this object type.
	 *
	 * @var string
	 */
	protected $object_type = 'customer_download_log';

	/**
	 * Download Log Data array.
	 *
	 * @var array
	 */
	protected $data = array(
		'timestamp'       => null,
		'permission_id'   => 0,
		'user_id'         => null,
		'user_ip_address' => null,
	);

	/**
	 * Constructor.
	 *
	 * @param int|object|array $download_log Download log ID.
	 */
	public function __construct( $download_log = 0 ) {
		parent::__construct( $download_log );

		if ( is_numeric( $download_log ) && $download_log > 0 ) {
			$this->set_id( $download_log );
		} elseif ( $download_log instanceof self ) {
			$this->set_id( $download_log->get_id() );
		} elseif ( is_object( $download_log ) && ! empty( $download_log->download_log_id ) ) {
			$this->set_id( $download_log->download_log_id );
			$this->set_props( (array) $download_log );
			$this->set_object_read( true );
		} else {
			$this->set_object_read( true );
		}

		$this->data_store = WC_Data_Store::load( 'customer-download-log' );

		if ( $this->get_id() > 0 ) {
			$this->data_store->read( $this );
		}
	}

	/*
	|--------------------------------------------------------------------------
	| Getters
	|--------------------------------------------------------------------------
	*/

	/**
	 * Get timestamp.
	 *
	 * @param  string $context Get context.
	 * @return WC_DateTime|null Object if the date is set or null if there is no date.
	 */
	public function get_timestamp( $context = 'view' ) {
		return $this->get_prop( 'timestamp', $context );
	}

	/**
	 * Get permission id.
	 *
	 * @param  string $context Get context.
	 * @return integer
	 */
	public function get_permission_id( $context = 'view' ) {
		return $this->get_prop( 'permission_id', $context );
	}

	/**
	 * Get user id.
	 *
	 * @param  string $context Get context.
	 * @return integer
	 */
	public function get_user_id( $context = 'view' ) {
		return $this->get_prop( 'user_id', $context );
	}

	/**
	 * Get user ip address.
	 *
	 * @param  string $context Get context.
	 * @return string
	 */
	public function get_user_ip_address( $context = 'view' ) {
		return $this->get_prop( 'user_ip_address', $context );
	}

	/*
	|--------------------------------------------------------------------------
	| Setters
	|--------------------------------------------------------------------------
	*/

	/**
	 * Set timestamp.
	 *
	 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date.
	 */
	public function set_timestamp( $date = null ) {
		$this->set_date_prop( 'timestamp', $date );
	}

	/**
	 * Set permission id.
	 *
	 * @param int $value Value to set.
	 */
	public function set_permission_id( $value ) {
		$this->set_prop( 'permission_id', absint( $value ) );
	}

	/**
	 * Set user id.
	 *
	 * @param int $value Value to set.
	 */
	public function set_user_id( $value ) {
		$this->set_prop( 'user_id', absint( $value ) );
	}

	/**
	 * Set user ip address.
	 *
	 * @param string $value Value to set.
	 */
	public function set_user_ip_address( $value ) {
		$this->set_prop( 'user_ip_address', $value );
	}
}
class-wc-meta-data.php000064400000004272151336044770010643 0ustar00<?php
/**
 * Wraps an array (meta data for now) and tells if there was any changes.
 *
 * The main idea behind this class is to avoid doing unneeded
 * SQL updates if nothing changed.
 *
 * @version 3.2.0
 * @package WooCommerce
 */

defined( 'ABSPATH' ) || exit;

/**
 * Meta data class.
 */
class WC_Meta_Data implements JsonSerializable {

	/**
	 * Current data for metadata
	 *
	 * @since 3.2.0
	 * @var array
	 */
	protected $current_data;

	/**
	 * Metadata data
	 *
	 * @since 3.2.0
	 * @var array
	 */
	protected $data;

	/**
	 * Constructor.
	 *
	 * @param array $meta Data to wrap behind this function.
	 */
	public function __construct( $meta = array() ) {
		$this->current_data = $meta;
		$this->apply_changes();
	}

	/**
	 * When converted to JSON.
	 *
	 * @return object|array
	 */
	public function jsonSerialize() {
		return $this->get_data();
	}

	/**
	 * Merge changes with data and clear.
	 */
	public function apply_changes() {
		$this->data = $this->current_data;
	}

	/**
	 * Creates or updates a property in the metadata object.
	 *
	 * @param string $key Key to set.
	 * @param mixed  $value Value to set.
	 */
	public function __set( $key, $value ) {
		$this->current_data[ $key ] = $value;
	}

	/**
	 * Checks if a given key exists in our data. This is called internally
	 * by `empty` and `isset`.
	 *
	 * @param string $key Key to check if set.
	 *
	 * @return bool
	 */
	public function __isset( $key ) {
		return array_key_exists( $key, $this->current_data );
	}

	/**
	 * Returns the value of any property.
	 *
	 * @param string $key Key to get.
	 * @return mixed Property value or NULL if it does not exists
	 */
	public function __get( $key ) {
		if ( array_key_exists( $key, $this->current_data ) ) {
			return $this->current_data[ $key ];
		}
		return null;
	}

	/**
	 * Return data changes only.
	 *
	 * @return array
	 */
	public function get_changes() {
		$changes = array();
		foreach ( $this->current_data as $id => $value ) {
			if ( ! array_key_exists( $id, $this->data ) || $value !== $this->data[ $id ] ) {
				$changes[ $id ] = $value;
			}
		}
		return $changes;
	}

	/**
	 * Return all data as an array.
	 *
	 * @return array
	 */
	public function get_data() {
		return $this->data;
	}
}
theme-support/class-wc-twenty-eleven.php000064400000002453151336044770014427 0ustar00<?php
/**
 * Twenty Eleven support.
 *
 * @since   3.3.0
 * @package WooCommerce\Classes
 */

defined( 'ABSPATH' ) || exit;

/**
 * WC_Twenty_Eleven class.
 */
class WC_Twenty_Eleven {

	/**
	 * Theme init.
	 */
	public static function init() {
		// Remove default wrappers.
		remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper' );
		remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end' );

		// Add custom wrappers.
		add_action( 'woocommerce_before_main_content', array( __CLASS__, 'output_content_wrapper' ) );
		add_action( 'woocommerce_after_main_content', array( __CLASS__, 'output_content_wrapper_end' ) );

		// Declare theme support for features.
		add_theme_support( 'wc-product-gallery-zoom' );
		add_theme_support( 'wc-product-gallery-lightbox' );
		add_theme_support( 'wc-product-gallery-slider' );
		add_theme_support(
			'woocommerce',
			array(
				'thumbnail_image_width' => 150,
				'single_image_width'    => 300,
			)
		);
	}

	/**
	 * Open wrappers.
	 */
	public static function output_content_wrapper() {
		echo '<div id="primary"><div id="content" role="main" class="twentyeleven">';
	}

	/**
	 * Close wrappers.
	 */
	public static function output_content_wrapper_end() {
		echo '</div></div>';
	}
}

WC_Twenty_Eleven::init();
theme-support/class-wc-twenty-twenty-one.php000064400000004234151336044770015261 0ustar00<?php
/**
 * Twenty Twenty One support.
 *
 * @since   4.7.0
 * @package WooCommerce\Classes
 */

use Automattic\Jetpack\Constants;

defined( 'ABSPATH' ) || exit;

/**
 * WC_Twenty_Twenty_One class.
 */
class WC_Twenty_Twenty_One {

	/**
	 * Theme init.
	 */
	public static function init() {

		// Change WooCommerce wrappers.
		remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper', 10 );
		remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end', 10 );

		// This theme doesn't have a traditional sidebar.
		remove_action( 'woocommerce_sidebar', 'woocommerce_get_sidebar', 10 );

		// Enqueue theme compatibility styles.
		add_filter( 'woocommerce_enqueue_styles', array( __CLASS__, 'enqueue_styles' ) );

		// Enqueue wp-admin compatibility styles.
		add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_admin_styles' ) );

		// Register theme features.
		add_theme_support( 'wc-product-gallery-zoom' );
		add_theme_support( 'wc-product-gallery-lightbox' );
		add_theme_support( 'wc-product-gallery-slider' );
		add_theme_support(
			'woocommerce',
			array(
				'thumbnail_image_width' => 450,
				'single_image_width'    => 600,
			)
		);

	}

	/**
	 * Enqueue CSS for this theme.
	 *
	 * @param  array $styles Array of registered styles.
	 * @return array
	 */
	public static function enqueue_styles( $styles ) {
		unset( $styles['woocommerce-general'] );

		$styles['woocommerce-general'] = array(
			'src'     => str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/css/twenty-twenty-one.css',
			'deps'    => '',
			'version' => Constants::get_constant( 'WC_VERSION' ),
			'media'   => 'all',
			'has_rtl' => true,
		);

		return apply_filters( 'woocommerce_twenty_twenty_one_styles', $styles );
	}

	/**
	 * Enqueue the wp-admin CSS overrides for this theme.
	 */
	public static function enqueue_admin_styles() {
		wp_enqueue_style(
			'woocommerce-twenty-twenty-one-admin',
			str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/css/twenty-twenty-one-admin.css',
			'',
			Constants::get_constant( 'WC_VERSION' ),
			'all'
		);
	}


}

WC_Twenty_Twenty_One::init();
theme-support/class-wc-twenty-seventeen.php000064400000006425151336044770015150 0ustar00<?php
/**
 * Twenty Seventeen support.
 *
 * @since   2.6.9
 * @package WooCommerce\Classes
 */

use Automattic\Jetpack\Constants;

defined( 'ABSPATH' ) || exit;

/**
 * WC_Twenty_Seventeen class.
 */
class WC_Twenty_Seventeen {

	/**
	 * Theme init.
	 */
	public static function init() {
		remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper', 10 );
		remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end', 10 );

		add_action( 'woocommerce_before_main_content', array( __CLASS__, 'output_content_wrapper' ), 10 );
		add_action( 'woocommerce_after_main_content', array( __CLASS__, 'output_content_wrapper_end' ), 10 );
		add_filter( 'woocommerce_enqueue_styles', array( __CLASS__, 'enqueue_styles' ) );
		add_filter( 'twentyseventeen_custom_colors_css', array( __CLASS__, 'custom_colors_css' ), 10, 3 );

		add_theme_support( 'wc-product-gallery-zoom' );
		add_theme_support( 'wc-product-gallery-lightbox' );
		add_theme_support( 'wc-product-gallery-slider' );
		add_theme_support(
			'woocommerce',
			array(
				'thumbnail_image_width' => 250,
				'single_image_width'    => 350,
			)
		);
	}

	/**
	 * Enqueue CSS for this theme.
	 *
	 * @param  array $styles Array of registered styles.
	 * @return array
	 */
	public static function enqueue_styles( $styles ) {
		unset( $styles['woocommerce-general'] );

		$styles['woocommerce-general'] = array(
			'src'     => str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/css/twenty-seventeen.css',
			'deps'    => '',
			'version' => Constants::get_constant( 'WC_VERSION' ),
			'media'   => 'all',
			'has_rtl' => true,
		);

		return apply_filters( 'woocommerce_twenty_seventeen_styles', $styles );
	}

	/**
	 * Open the Twenty Seventeen wrapper.
	 */
	public static function output_content_wrapper() {
		echo '<div class="wrap">';
		echo '<div id="primary" class="content-area twentyseventeen">';
		echo '<main id="main" class="site-main" role="main">';
	}

	/**
	 * Close the Twenty Seventeen wrapper.
	 */
	public static function output_content_wrapper_end() {
		echo '</main>';
		echo '</div>';
		get_sidebar();
		echo '</div>';
	}

	/**
	 * Custom colors.
	 *
	 * @param  string $css Styles.
	 * @param  string $hue Color.
	 * @param  string $saturation Saturation.
	 * @return string
	 */
	public static function custom_colors_css( $css, $hue, $saturation ) {
		$css .= '
			.colors-custom .select2-container--default .select2-selection--single {
				border-color: hsl( ' . $hue . ', ' . $saturation . ', 73% );
			}
			.colors-custom .select2-container--default .select2-selection__rendered {
				color: hsl( ' . $hue . ', ' . $saturation . ', 40% );
			}
			.colors-custom .select2-container--default .select2-selection--single .select2-selection__arrow b {
				border-color: hsl( ' . $hue . ', ' . $saturation . ', 40% ) transparent transparent transparent;
			}
			.colors-custom .select2-container--focus .select2-selection {
				border-color: #000;
			}
			.colors-custom .select2-container--focus .select2-selection--single .select2-selection__arrow b {
				border-color: #000 transparent transparent transparent;
			}
			.colors-custom .select2-container--focus .select2-selection .select2-selection__rendered {
				color: #000;
			}
		';
		return $css;
	}
}

WC_Twenty_Seventeen::init();
theme-support/class-wc-twenty-ten.php000064400000002414151336044770013734 0ustar00<?php
/**
 * Twenty Ten support.
 *
 * @since   3.3.0
 * @package WooCommerce\Classes
 */

defined( 'ABSPATH' ) || exit;

/**
 * WC_Twenty_Ten class.
 */
class WC_Twenty_Ten {

	/**
	 * Theme init.
	 */
	public static function init() {
		// Remove default wrappers.
		remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper' );
		remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end' );

		// Add custom wrappers.
		add_action( 'woocommerce_before_main_content', array( __CLASS__, 'output_content_wrapper' ) );
		add_action( 'woocommerce_after_main_content', array( __CLASS__, 'output_content_wrapper_end' ) );

		// Declare theme support for features.
		add_theme_support( 'wc-product-gallery-zoom' );
		add_theme_support( 'wc-product-gallery-lightbox' );
		add_theme_support( 'wc-product-gallery-slider' );
		add_theme_support(
			'woocommerce',
			array(
				'thumbnail_image_width' => 200,
				'single_image_width'    => 300,
			)
		);
	}

	/**
	 * Open wrappers.
	 */
	public static function output_content_wrapper() {
		echo '<div id="container"><div id="content" role="main">';
	}

	/**
	 * Close wrappers.
	 */
	public static function output_content_wrapper_end() {
		echo '</div></div>';
	}
}

WC_Twenty_Ten::init();
theme-support/class-wc-twenty-twenty.php000064400000005471151336044770014506 0ustar00<?php
/**
 * Twenty Twenty support.
 *
 * @since   3.8.1
 * @package WooCommerce\Classes
 */

use Automattic\Jetpack\Constants;

defined( 'ABSPATH' ) || exit;

/**
 * WC_Twenty_Twenty class.
 */
class WC_Twenty_Twenty {

	/**
	 * Theme init.
	 */
	public static function init() {

		// Change WooCommerce wrappers.
		remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper', 10 );
		remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end', 10 );

		add_action( 'woocommerce_before_main_content', array( __CLASS__, 'output_content_wrapper' ), 10 );
		add_action( 'woocommerce_after_main_content', array( __CLASS__, 'output_content_wrapper_end' ), 10 );

		// This theme doesn't have a traditional sidebar.
		remove_action( 'woocommerce_sidebar', 'woocommerce_get_sidebar', 10 );

		// Enqueue theme compatibility styles.
		add_filter( 'woocommerce_enqueue_styles', array( __CLASS__, 'enqueue_styles' ) );

		// Register theme features.
		add_theme_support( 'wc-product-gallery-zoom' );
		add_theme_support( 'wc-product-gallery-lightbox' );
		add_theme_support( 'wc-product-gallery-slider' );
		add_theme_support(
			'woocommerce',
			array(
				'thumbnail_image_width' => 450,
				'single_image_width'    => 600,
			)
		);

		// Background color change.
		add_action( 'after_setup_theme', array( __CLASS__, 'set_white_background' ), 10 );

	}

	/**
	 * Open the Twenty Twenty wrapper.
	 */
	public static function output_content_wrapper() {
		echo '<section id="primary" class="content-area">';
		echo '<main id="main" class="site-main">';
	}

	/**
	 * Close the Twenty Twenty wrapper.
	 */
	public static function output_content_wrapper_end() {
		echo '</main>';
		echo '</section>';
	}

	/**
	 * Set background color to white if it's default, otherwise don't touch it.
	 */
	public static function set_white_background() {
		$background         = sanitize_hex_color_no_hash( get_theme_mod( 'background_color' ) );
		$background_default = 'f5efe0';

		// Don't change user's choice of background color.
		if ( ! empty( $background ) && $background !== $background_default ) {
			return;
		}

		// In case default background is found, change it to white.
		set_theme_mod( 'background_color', 'fff' );
	}

	/**
	 * Enqueue CSS for this theme.
	 *
	 * @param  array $styles Array of registered styles.
	 * @return array
	 */
	public static function enqueue_styles( $styles ) {
		unset( $styles['woocommerce-general'] );

		$styles['woocommerce-general'] = array(
			'src'     => str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/css/twenty-twenty.css',
			'deps'    => '',
			'version' => Constants::get_constant( 'WC_VERSION' ),
			'media'   => 'all',
			'has_rtl' => true,
		);

		return apply_filters( 'woocommerce_twenty_twenty_styles', $styles );
	}

}

WC_Twenty_Twenty::init();
theme-support/class-wc-twenty-fourteen.php000064400000002652151336044770015001 0ustar00<?php
/**
 * Twenty Fourteen support.
 *
 * @class   WC_Twenty_Fourteen
 * @since   3.3.0
 * @package WooCommerce\Classes
 */

defined( 'ABSPATH' ) || exit;

/**
 * WC_Twenty_Fourteen class.
 */
class WC_Twenty_Fourteen {

	/**
	 * Theme init.
	 */
	public static function init() {
		// Remove default wrappers.
		remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper' );
		remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end' );

		// Add custom wrappers.
		add_action( 'woocommerce_before_main_content', array( __CLASS__, 'output_content_wrapper' ) );
		add_action( 'woocommerce_after_main_content', array( __CLASS__, 'output_content_wrapper_end' ) );

		// Declare theme support for features.
		add_theme_support( 'wc-product-gallery-zoom' );
		add_theme_support( 'wc-product-gallery-lightbox' );
		add_theme_support( 'wc-product-gallery-slider' );
		add_theme_support(
			'woocommerce',
			array(
				'thumbnail_image_width' => 150,
				'single_image_width'    => 300,
			)
		);
	}

	/**
	 * Open wrappers.
	 */
	public static function output_content_wrapper() {
		echo '<div id="primary" class="content-area"><div id="content" role="main" class="site-content twentyfourteen"><div class="tfwc">';
	}

	/**
	 * Close wrappers.
	 */
	public static function output_content_wrapper_end() {
		echo '</div></div></div>';
		get_sidebar( 'content' );
	}
}

WC_Twenty_Fourteen::init();
theme-support/class-wc-twenty-thirteen.php000064400000002567151336044770015001 0ustar00<?php
/**
 * Twenty Thirteen support.
 *
 * @class   WC_Twenty_Thirteen
 * @since   3.3.0
 * @package WooCommerce\Classes
 */

defined( 'ABSPATH' ) || exit;

/**
 * WC_Twenty_Thirteen class.
 */
class WC_Twenty_Thirteen {

	/**
	 * Theme init.
	 */
	public static function init() {
		// Remove default wrappers.
		remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper' );
		remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end' );

		// Add custom wrappers.
		add_action( 'woocommerce_before_main_content', array( __CLASS__, 'output_content_wrapper' ) );
		add_action( 'woocommerce_after_main_content', array( __CLASS__, 'output_content_wrapper_end' ) );

		// Declare theme support for features.
		add_theme_support( 'wc-product-gallery-zoom' );
		add_theme_support( 'wc-product-gallery-lightbox' );
		add_theme_support( 'wc-product-gallery-slider' );
		add_theme_support(
			'woocommerce',
			array(
				'thumbnail_image_width' => 200,
				'single_image_width'    => 300,
			)
		);
	}

	/**
	 * Open wrappers.
	 */
	public static function output_content_wrapper() {
		echo '<div id="primary" class="site-content"><div id="content" role="main" class="entry-content twentythirteen">';
	}

	/**
	 * Close wrappers.
	 */
	public static function output_content_wrapper_end() {
		echo '</div></div>';
	}
}

WC_Twenty_Thirteen::init();
theme-support/class-wc-twenty-twelve.php000064400000002535151336044770014460 0ustar00<?php
/**
 * Twenty Twelve support.
 *
 * @class   WC_Twenty_Twelve
 * @since   3.3.0
 * @package WooCommerce\Classes
 */

defined( 'ABSPATH' ) || exit;

/**
 * WC_Twenty_Twelve class.
 */
class WC_Twenty_Twelve {

	/**
	 * Theme init.
	 */
	public static function init() {
		// Remove default wrappers.
		remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper' );
		remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end' );

		// Add custom wrappers.
		add_action( 'woocommerce_before_main_content', array( __CLASS__, 'output_content_wrapper' ) );
		add_action( 'woocommerce_after_main_content', array( __CLASS__, 'output_content_wrapper_end' ) );

		// Declare theme support for features.
		add_theme_support( 'wc-product-gallery-zoom' );
		add_theme_support( 'wc-product-gallery-lightbox' );
		add_theme_support( 'wc-product-gallery-slider' );
		add_theme_support(
			'woocommerce',
			array(
				'thumbnail_image_width' => 200,
				'single_image_width'    => 300,
			)
		);
	}

	/**
	 * Open wrappers.
	 */
	public static function output_content_wrapper() {
		echo '<div id="primary" class="site-content"><div id="content" role="main" class="twentytwelve">';
	}

	/**
	 * Close wrappers.
	 */
	public static function output_content_wrapper_end() {
		echo '</div></div>';
	}
}

WC_Twenty_Twelve::init();
theme-support/class-wc-twenty-sixteen.php000064400000002516151336044770014630 0ustar00<?php
/**
 * Twenty Sixteen support.
 *
 * @since   3.3.0
 * @package WooCommerce\Classes
 */

defined( 'ABSPATH' ) || exit;

/**
 * WC_Twenty_Sixteen class.
 */
class WC_Twenty_Sixteen {

	/**
	 * Theme init.
	 */
	public static function init() {
		// Remove default wrappers.
		remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper' );
		remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end' );

		// Add custom wrappers.
		add_action( 'woocommerce_before_main_content', array( __CLASS__, 'output_content_wrapper' ) );
		add_action( 'woocommerce_after_main_content', array( __CLASS__, 'output_content_wrapper_end' ) );

		// Declare theme support for features.
		add_theme_support( 'wc-product-gallery-zoom' );
		add_theme_support( 'wc-product-gallery-lightbox' );
		add_theme_support( 'wc-product-gallery-slider' );
		add_theme_support(
			'woocommerce',
			array(
				'thumbnail_image_width' => 250,
				'single_image_width'    => 400,
			)
		);
	}

	/**
	 * Open wrappers.
	 */
	public static function output_content_wrapper() {
		echo '<div id="primary" class="content-area twentysixteen"><main id="main" class="site-main" role="main">';
	}

	/**
	 * Close wrappers.
	 */
	public static function output_content_wrapper_end() {
		echo '</main></div>';
	}
}

WC_Twenty_Sixteen::init();
theme-support/class-wc-twenty-fifteen.php000064400000002560151336044770014570 0ustar00<?php
/**
 * Twenty Fifteen support.
 *
 * @class   WC_Twenty_Fifteen
 * @since   3.3.0
 * @package WooCommerce\Classes
 */

defined( 'ABSPATH' ) || exit;

/**
 * WC_Twenty_Fifteen class.
 */
class WC_Twenty_Fifteen {

	/**
	 * Theme init.
	 */
	public static function init() {
		// Remove default wrappers.
		remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper' );
		remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end' );

		// Add custom wrappers.
		add_action( 'woocommerce_before_main_content', array( __CLASS__, 'output_content_wrapper' ) );
		add_action( 'woocommerce_after_main_content', array( __CLASS__, 'output_content_wrapper_end' ) );

		// Declare theme support for features.
		add_theme_support( 'wc-product-gallery-zoom' );
		add_theme_support( 'wc-product-gallery-lightbox' );
		add_theme_support( 'wc-product-gallery-slider' );
		add_theme_support(
			'woocommerce',
			array(
				'thumbnail_image_width' => 200,
				'single_image_width'    => 350,
			)
		);
	}

	/**
	 * Open wrappers.
	 */
	public static function output_content_wrapper() {
		echo '<div id="primary" role="main" class="content-area twentyfifteen"><div id="main" class="site-main t15wc">';
	}

	/**
	 * Close wrappers.
	 */
	public static function output_content_wrapper_end() {
		echo '</div></div>';
	}
}

WC_Twenty_Fifteen::init();
theme-support/class-wc-twenty-nineteen.php000064400000007112151336044770014753 0ustar00<?php
/**
 * Twenty Nineteen support.
 *
 * @since   3.5.X
 * @package WooCommerce\Classes
 */

use Automattic\Jetpack\Constants;

defined( 'ABSPATH' ) || exit;

/**
 * WC_Twenty_Nineteen class.
 */
class WC_Twenty_Nineteen {

	/**
	 * Theme init.
	 */
	public static function init() {

		// Change WooCommerce wrappers.
		remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper', 10 );
		remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end', 10 );

		add_action( 'woocommerce_before_main_content', array( __CLASS__, 'output_content_wrapper' ), 10 );
		add_action( 'woocommerce_after_main_content', array( __CLASS__, 'output_content_wrapper_end' ), 10 );

		// This theme doesn't have a traditional sidebar.
		remove_action( 'woocommerce_sidebar', 'woocommerce_get_sidebar', 10 );

		// Enqueue theme compatibility styles.
		add_filter( 'woocommerce_enqueue_styles', array( __CLASS__, 'enqueue_styles' ) );

		// Register theme features.
		add_theme_support( 'wc-product-gallery-zoom' );
		add_theme_support( 'wc-product-gallery-lightbox' );
		add_theme_support( 'wc-product-gallery-slider' );
		add_theme_support(
			'woocommerce',
			array(
				'thumbnail_image_width' => 300,
				'single_image_width'    => 450,
			)
		);

		// Tweak Twenty Nineteen features.
		add_action( 'wp', array( __CLASS__, 'tweak_theme_features' ) );

		// Color scheme CSS.
		add_filter( 'twentynineteen_custom_colors_css', array( __CLASS__, 'custom_colors_css' ), 10, 3 );
	}

	/**
	 * Open the Twenty Nineteen wrapper.
	 */
	public static function output_content_wrapper() {
		echo '<section id="primary" class="content-area">';
		echo '<main id="main" class="site-main">';
	}

	/**
	 * Close the Twenty Nineteen wrapper.
	 */
	public static function output_content_wrapper_end() {
		echo '</main>';
		echo '</section>';
	}

	/**
	 * Enqueue CSS for this theme.
	 *
	 * @param  array $styles Array of registered styles.
	 * @return array
	 */
	public static function enqueue_styles( $styles ) {
		unset( $styles['woocommerce-general'] );

		$styles['woocommerce-general'] = array(
			'src'     => str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/css/twenty-nineteen.css',
			'deps'    => '',
			'version' => Constants::get_constant( 'WC_VERSION' ),
			'media'   => 'all',
			'has_rtl' => true,
		);

		return apply_filters( 'woocommerce_twenty_nineteen_styles', $styles );
	}

	/**
	 * Tweak Twenty Nineteen features.
	 */
	public static function tweak_theme_features() {
		if ( is_woocommerce() ) {
			add_filter( 'twentynineteen_can_show_post_thumbnail', '__return_false' );
		}
	}

	/**
	 * Filters Twenty Nineteen custom colors CSS.
	 *
	 * @param string $css           Base theme colors CSS.
	 * @param int    $primary_color The user's selected color hue.
	 * @param string $saturation    Filtered theme color saturation level.
	 */
	public static function custom_colors_css( $css, $primary_color, $saturation ) {
		if ( function_exists( 'register_block_type' ) && is_admin() ) {
			return $css;
		}

		$lightness = absint( apply_filters( 'twentynineteen_custom_colors_lightness', 33 ) );
		$lightness = $lightness . '%';

		$css .= '
			.onsale,
			.woocommerce-info,
			.woocommerce-store-notice {
				background-color: hsl( ' . $primary_color . ', ' . $saturation . ', ' . $lightness . ' );
			}

			.woocommerce-tabs ul li.active a {
				color: hsl( ' . $primary_color . ', ' . $saturation . ', ' . $lightness . ' );
				box-shadow: 0 2px 0 hsl( ' . $primary_color . ', ' . $saturation . ', ' . $lightness . ' );
			}
		';

		return $css;
	}
}

WC_Twenty_Nineteen::init();
data-stores/class-wc-order-item-fee-data-store.php000064400000004005151336044770016073 0ustar00<?php
/**
 * Class WC_Order_Item_Fee_Data_Store file.
 *
 * @package WooCommerce\DataStores
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Order Item Fee Data Store
 *
 * @version  3.0.0
 */
class WC_Order_Item_Fee_Data_Store extends Abstract_WC_Order_Item_Type_Data_Store implements WC_Object_Data_Store_Interface, WC_Order_Item_Type_Data_Store_Interface {

	/**
	 * Data stored in meta keys.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $internal_meta_keys = array( '_fee_amount', '_tax_class', '_tax_status', '_line_subtotal', '_line_subtotal_tax', '_line_total', '_line_tax', '_line_tax_data' );

	/**
	 * Read/populate data properties specific to this order item.
	 *
	 * @since 3.0.0
	 * @param WC_Order_Item_Fee $item Fee order item object.
	 */
	public function read( &$item ) {
		parent::read( $item );
		$id = $item->get_id();
		$item->set_props(
			array(
				'amount'     => get_metadata( 'order_item', $id, '_fee_amount', true ),
				'tax_class'  => get_metadata( 'order_item', $id, '_tax_class', true ),
				'tax_status' => get_metadata( 'order_item', $id, '_tax_status', true ),
				'total'      => get_metadata( 'order_item', $id, '_line_total', true ),
				'taxes'      => get_metadata( 'order_item', $id, '_line_tax_data', true ),
			)
		);
		$item->set_object_read( true );
	}

	/**
	 * Saves an item's data to the database / item meta.
	 * Ran after both create and update, so $id will be set.
	 *
	 * @since 3.0.0
	 * @param WC_Order_Item_Fee $item Fee order item object.
	 */
	public function save_item_data( &$item ) {
		$id          = $item->get_id();
		$save_values = array(
			'_fee_amount'    => $item->get_amount( 'edit' ),
			'_tax_class'     => $item->get_tax_class( 'edit' ),
			'_tax_status'    => $item->get_tax_status( 'edit' ),
			'_line_total'    => $item->get_total( 'edit' ),
			'_line_tax'      => $item->get_total_tax( 'edit' ),
			'_line_tax_data' => $item->get_taxes( 'edit' ),
		);
		foreach ( $save_values as $key => $value ) {
			update_metadata( 'order_item', $id, $key, $value );
		}
	}
}
data-stores/class-wc-customer-download-log-data-store.php000064400000015052151336044770017520 0ustar00<?php
/**
 * Class WC_Customer_Download_Log_Data_Store file.
 *
 * @version  3.3.0
 * @package WooCommerce\Classes
 */

defined( 'ABSPATH' ) || exit;

/**
 * WC_Customer_Download_Log_Data_Store class.
 */
class WC_Customer_Download_Log_Data_Store implements WC_Customer_Download_Log_Data_Store_Interface {

	// Table name for download logs.
	const WC_DOWNLOAD_LOG_TABLE = 'wc_download_log';

	/**
	 * Get the table name for download logs.
	 *
	 * @return string
	 */
	public static function get_table_name() {
		return self::WC_DOWNLOAD_LOG_TABLE;
	}

	/**
	 * Create download log entry.
	 *
	 * @param WC_Customer_Download_Log $download_log Customer download log object.
	 */
	public function create( WC_Customer_Download_Log &$download_log ) {
		global $wpdb;

		// Always set a timestamp.
		if ( is_null( $download_log->get_timestamp( 'edit' ) ) ) {
			$download_log->set_timestamp( time() );
		}

		$data = array(
			'timestamp'       => date( 'Y-m-d H:i:s', $download_log->get_timestamp( 'edit' )->getTimestamp() ),
			'permission_id'   => $download_log->get_permission_id( 'edit' ),
			'user_id'         => $download_log->get_user_id( 'edit' ),
			'user_ip_address' => $download_log->get_user_ip_address( 'edit' ),
		);

		$format = array(
			'%s',
			'%s',
			'%s',
			'%s',
		);

		$result = $wpdb->insert(
			$wpdb->prefix . self::get_table_name(),
			apply_filters( 'woocommerce_downloadable_product_download_log_insert_data', $data ),
			apply_filters( 'woocommerce_downloadable_product_download_log_insert_format', $format, $data )
		);

		do_action( 'woocommerce_downloadable_product_download_log_insert', $data );

		if ( $result ) {
			$download_log->set_id( $wpdb->insert_id );
			$download_log->apply_changes();
		} else {
			wp_die( esc_html__( 'Unable to insert download log entry in database.', 'woocommerce' ) );
		}
	}

	/**
	 * Method to read a download log from the database.
	 *
	 * @param WC_Customer_Download_Log $download_log Download log object.
	 * @throws Exception Exception when read is not possible.
	 */
	public function read( &$download_log ) {
		global $wpdb;

		$download_log->set_defaults();

		// Ensure we have an id to pull from the DB.
		if ( ! $download_log->get_id() ) {
			throw new Exception( __( 'Invalid download log: no ID.', 'woocommerce' ) );
		}

		$table = $wpdb->prefix . self::get_table_name();

		// Query the DB for the download log.
		$raw_download_log = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE download_log_id = %d", $download_log->get_id() ) ); // WPCS: unprepared SQL ok.

		if ( ! $raw_download_log ) {
			throw new Exception( __( 'Invalid download log: not found.', 'woocommerce' ) );
		}

		$download_log->set_props(
			array(
				'timestamp'       => strtotime( $raw_download_log->timestamp ),
				'permission_id'   => $raw_download_log->permission_id,
				'user_id'         => $raw_download_log->user_id,
				'user_ip_address' => $raw_download_log->user_ip_address,
			)
		);

		$download_log->set_object_read( true );
	}

	/**
	 * Method to update a download log in the database.
	 *
	 * @param WC_Customer_Download_Log $download_log Download log object.
	 */
	public function update( &$download_log ) {
		global $wpdb;

		$data = array(
			'timestamp'       => date( 'Y-m-d H:i:s', $download_log->get_timestamp( 'edit' )->getTimestamp() ),
			'permission_id'   => $download_log->get_permission_id( 'edit' ),
			'user_id'         => $download_log->get_user_id( 'edit' ),
			'user_ip_address' => $download_log->get_user_ip_address( 'edit' ),
		);

		$format = array(
			'%s',
			'%s',
			'%s',
			'%s',
		);

		$wpdb->update(
			$wpdb->prefix . self::get_table_name(),
			$data,
			array(
				'download_log_id' => $download_log->get_id(),
			),
			$format
		);
		$download_log->apply_changes();
	}

	/**
	 * Get a download log object.
	 *
	 * @param  array $data From the DB.
	 * @return WC_Customer_Download_Log
	 */
	private function get_download_log( $data ) {
		return new WC_Customer_Download_Log( $data );
	}

	/**
	 * Get array of download log ids by specified args.
	 *
	 * @param  array $args Arguments to define download logs to retrieve.
	 * @return array
	 */
	public function get_download_logs( $args = array() ) {
		global $wpdb;

		$args = wp_parse_args(
			$args,
			array(
				'permission_id'   => '',
				'user_id'         => '',
				'user_ip_address' => '',
				'orderby'         => 'download_log_id',
				'order'           => 'ASC',
				'limit'           => -1,
				'page'            => 1,
				'return'          => 'objects',
			)
		);

		$query   = array();
		$table   = $wpdb->prefix . self::get_table_name();
		$query[] = "SELECT * FROM {$table} WHERE 1=1";

		if ( $args['permission_id'] ) {
			$query[] = $wpdb->prepare( 'AND permission_id = %d', $args['permission_id'] );
		}

		if ( $args['user_id'] ) {
			$query[] = $wpdb->prepare( 'AND user_id = %d', $args['user_id'] );
		}

		if ( $args['user_ip_address'] ) {
			$query[] = $wpdb->prepare( 'AND user_ip_address = %s', $args['user_ip_address'] );
		}

		$allowed_orders = array( 'download_log_id', 'timestamp', 'permission_id', 'user_id' );
		$orderby        = in_array( $args['orderby'], $allowed_orders, true ) ? $args['orderby'] : 'download_log_id';
		$order          = 'DESC' === strtoupper( $args['order'] ) ? 'DESC' : 'ASC';
		$orderby_sql    = sanitize_sql_orderby( "{$orderby} {$order}" );
		$query[]        = "ORDER BY {$orderby_sql}";

		if ( 0 < $args['limit'] ) {
			$query[] = $wpdb->prepare( 'LIMIT %d, %d', absint( $args['limit'] ) * absint( $args['page'] - 1 ), absint( $args['limit'] ) );
		}

		$raw_download_logs = $wpdb->get_results( implode( ' ', $query ) ); // WPCS: unprepared SQL ok.

		switch ( $args['return'] ) {
			case 'ids':
				return wp_list_pluck( $raw_download_logs, 'download_log_id' );
			default:
				return array_map( array( $this, 'get_download_log' ), $raw_download_logs );
		}
	}

	/**
	 * Get download logs for a given download permission.
	 *
	 * @param int $permission_id Permission to get logs for.
	 * @return array
	 */
	public function get_download_logs_for_permission( $permission_id ) {
		// If no permission_id is passed, return an empty array.
		if ( empty( $permission_id ) ) {
			return array();
		}

		return $this->get_download_logs(
			array(
				'permission_id' => $permission_id,
			)
		);
	}

	/**
	 * Method to delete download logs for a given permission ID.
	 *
	 * @since 3.4.0
	 * @param int $id download_id of the downloads that will be deleted.
	 */
	public function delete_by_permission_id( $id ) {
		global $wpdb;
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE permission_id = %d", $id ) );
	}
}
data-stores/abstract-wc-order-item-type-data-store.php000064400000010657151336044770017025 0ustar00<?php
/**
 * Class Abstract_WC_Order_Item_Type_Data_Store file.
 *
 * @package WooCommerce\DataStores
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Order Item Data Store
 *
 * @version  3.0.0
 */
abstract class Abstract_WC_Order_Item_Type_Data_Store extends WC_Data_Store_WP implements WC_Object_Data_Store_Interface {

	/**
	 * Meta type. This should match up with
	 * the types available at https://developer.wordpress.org/reference/functions/add_metadata/.
	 * WP defines 'post', 'user', 'comment', and 'term'.
	 *
	 * @var string
	 */
	protected $meta_type = 'order_item';

	/**
	 * This only needs set if you are using a custom metadata type (for example payment tokens.
	 * This should be the name of the field your table uses for associating meta with objects.
	 * For example, in payment_tokenmeta, this would be payment_token_id.
	 *
	 * @var string
	 */
	protected $object_id_field_for_meta = 'order_item_id';

	/**
	 * Create a new order item in the database.
	 *
	 * @since 3.0.0
	 * @param WC_Order_Item $item Order item object.
	 */
	public function create( &$item ) {
		global $wpdb;

		$wpdb->insert(
			$wpdb->prefix . 'woocommerce_order_items',
			array(
				'order_item_name' => $item->get_name(),
				'order_item_type' => $item->get_type(),
				'order_id'        => $item->get_order_id(),
			)
		);
		$item->set_id( $wpdb->insert_id );
		$this->save_item_data( $item );
		$item->save_meta_data();
		$item->apply_changes();
		$this->clear_cache( $item );

		do_action( 'woocommerce_new_order_item', $item->get_id(), $item, $item->get_order_id() );
	}

	/**
	 * Update a order item in the database.
	 *
	 * @since 3.0.0
	 * @param WC_Order_Item $item Order item object.
	 */
	public function update( &$item ) {
		global $wpdb;

		$changes = $item->get_changes();

		if ( array_intersect( array( 'name', 'order_id' ), array_keys( $changes ) ) ) {
			$wpdb->update(
				$wpdb->prefix . 'woocommerce_order_items',
				array(
					'order_item_name' => $item->get_name(),
					'order_item_type' => $item->get_type(),
					'order_id'        => $item->get_order_id(),
				),
				array( 'order_item_id' => $item->get_id() )
			);
		}

		$this->save_item_data( $item );
		$item->save_meta_data();
		$item->apply_changes();
		$this->clear_cache( $item );

		do_action( 'woocommerce_update_order_item', $item->get_id(), $item, $item->get_order_id() );
	}

	/**
	 * Remove an order item from the database.
	 *
	 * @since 3.0.0
	 * @param WC_Order_Item $item Order item object.
	 * @param array         $args Array of args to pass to the delete method.
	 */
	public function delete( &$item, $args = array() ) {
		if ( $item->get_id() ) {
			global $wpdb;
			do_action( 'woocommerce_before_delete_order_item', $item->get_id() );
			$wpdb->delete( $wpdb->prefix . 'woocommerce_order_items', array( 'order_item_id' => $item->get_id() ) );
			$wpdb->delete( $wpdb->prefix . 'woocommerce_order_itemmeta', array( 'order_item_id' => $item->get_id() ) );
			do_action( 'woocommerce_delete_order_item', $item->get_id() );
			$this->clear_cache( $item );
		}
	}

	/**
	 * Read a order item from the database.
	 *
	 * @since 3.0.0
	 *
	 * @param WC_Order_Item $item Order item object.
	 *
	 * @throws Exception If invalid order item.
	 */
	public function read( &$item ) {
		global $wpdb;

		$item->set_defaults();

		// Get from cache if available.
		$data = wp_cache_get( 'item-' . $item->get_id(), 'order-items' );

		if ( false === $data ) {
			$data = $wpdb->get_row( $wpdb->prepare( "SELECT order_id, order_item_name FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d LIMIT 1;", $item->get_id() ) );
			wp_cache_set( 'item-' . $item->get_id(), $data, 'order-items' );
		}

		if ( ! $data ) {
			throw new Exception( __( 'Invalid order item.', 'woocommerce' ) );
		}

		$item->set_props(
			array(
				'order_id' => $data->order_id,
				'name'     => $data->order_item_name,
			)
		);
		$item->read_meta_data();
	}

	/**
	 * Saves an item's data to the database / item meta.
	 * Ran after both create and update, so $item->get_id() will be set.
	 *
	 * @since 3.0.0
	 * @param WC_Order_Item $item Order item object.
	 */
	public function save_item_data( &$item ) {}

	/**
	 * Clear meta cache.
	 *
	 * @param WC_Order_Item $item Order item object.
	 */
	public function clear_cache( &$item ) {
		wp_cache_delete( 'item-' . $item->get_id(), 'order-items' );
		wp_cache_delete( 'order-items-' . $item->get_order_id(), 'orders' );
		wp_cache_delete( $item->get_id(), $this->meta_type . '_meta' );
	}
}
data-stores/class-wc-webhook-data-store.php000064400000034324151336044770014734 0ustar00<?php
/**
 * Webhook Data Store
 *
 * @version  3.3.0
 * @package  WooCommerce\Classes\Data_Store
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Webhook data store class.
 */
class WC_Webhook_Data_Store implements WC_Webhook_Data_Store_Interface {

	/**
	 * Create a new webhook in the database.
	 *
	 * @since 3.3.0
	 * @param WC_Webhook $webhook Webhook instance.
	 */
	public function create( &$webhook ) {
		global $wpdb;

		$changes = $webhook->get_changes();
		if ( isset( $changes['date_created'] ) ) {
			$date_created     = $webhook->get_date_created()->date( 'Y-m-d H:i:s' );
			$date_created_gmt = gmdate( 'Y-m-d H:i:s', $webhook->get_date_created()->getTimestamp() );
		} else {
			$date_created     = current_time( 'mysql' );
			$date_created_gmt = current_time( 'mysql', 1 );
			$webhook->set_date_created( $date_created );
		}

		// Pending delivery by default if not set while creating a new webhook.
		if ( ! isset( $changes['pending_delivery'] ) ) {
			$webhook->set_pending_delivery( true );
		}

		$data = array(
			'status'           => $webhook->get_status( 'edit' ),
			'name'             => $webhook->get_name( 'edit' ),
			'user_id'          => $webhook->get_user_id( 'edit' ),
			'delivery_url'     => $webhook->get_delivery_url( 'edit' ),
			'secret'           => $webhook->get_secret( 'edit' ),
			'topic'            => $webhook->get_topic( 'edit' ),
			'date_created'     => $date_created,
			'date_created_gmt' => $date_created_gmt,
			'api_version'      => $this->get_api_version_number( $webhook->get_api_version( 'edit' ) ),
			'failure_count'    => $webhook->get_failure_count( 'edit' ),
			'pending_delivery' => $webhook->get_pending_delivery( 'edit' ),
		);

		$wpdb->insert( $wpdb->prefix . 'wc_webhooks', $data ); // WPCS: DB call ok.

		$webhook_id = $wpdb->insert_id;
		$webhook->set_id( $webhook_id );
		$webhook->apply_changes();

		$this->delete_transients( $webhook->get_status( 'edit' ) );
		WC_Cache_Helper::invalidate_cache_group( 'webhooks' );
		do_action( 'woocommerce_new_webhook', $webhook_id, $webhook );
	}

	/**
	 * Read a webhook from the database.
	 *
	 * @since  3.3.0
	 * @param  WC_Webhook $webhook Webhook instance.
	 * @throws Exception When webhook is invalid.
	 */
	public function read( &$webhook ) {
		global $wpdb;

		$data = wp_cache_get( $webhook->get_id(), 'webhooks' );

		if ( false === $data ) {
			$data = $wpdb->get_row( $wpdb->prepare( "SELECT webhook_id, status, name, user_id, delivery_url, secret, topic, date_created, date_modified, api_version, failure_count, pending_delivery FROM {$wpdb->prefix}wc_webhooks WHERE webhook_id = %d LIMIT 1;", $webhook->get_id() ), ARRAY_A ); // WPCS: cache ok, DB call ok.

			wp_cache_add( $webhook->get_id(), $data, 'webhooks' );
		}

		if ( is_array( $data ) ) {
			$webhook->set_props(
				array(
					'id'               => $data['webhook_id'],
					'status'           => $data['status'],
					'name'             => $data['name'],
					'user_id'          => $data['user_id'],
					'delivery_url'     => $data['delivery_url'],
					'secret'           => $data['secret'],
					'topic'            => $data['topic'],
					'date_created'     => '0000-00-00 00:00:00' === $data['date_created'] ? null : $data['date_created'],
					'date_modified'    => '0000-00-00 00:00:00' === $data['date_modified'] ? null : $data['date_modified'],
					'api_version'      => $data['api_version'],
					'failure_count'    => $data['failure_count'],
					'pending_delivery' => $data['pending_delivery'],
				)
			);
			$webhook->set_object_read( true );

			do_action( 'woocommerce_webhook_loaded', $webhook );
		} else {
			throw new Exception( __( 'Invalid webhook.', 'woocommerce' ) );
		}
	}

	/**
	 * Update a webhook.
	 *
	 * @since 3.3.0
	 * @param WC_Webhook $webhook Webhook instance.
	 */
	public function update( &$webhook ) {
		global $wpdb;

		$changes = $webhook->get_changes();
		$trigger = isset( $changes['delivery_url'] );

		if ( isset( $changes['date_modified'] ) ) {
			$date_modified     = $webhook->get_date_modified()->date( 'Y-m-d H:i:s' );
			$date_modified_gmt = gmdate( 'Y-m-d H:i:s', $webhook->get_date_modified()->getTimestamp() );
		} else {
			$date_modified     = current_time( 'mysql' );
			$date_modified_gmt = current_time( 'mysql', 1 );
			$webhook->set_date_modified( $date_modified );
		}

		$data = array(
			'status'            => $webhook->get_status( 'edit' ),
			'name'              => $webhook->get_name( 'edit' ),
			'user_id'           => $webhook->get_user_id( 'edit' ),
			'delivery_url'      => $webhook->get_delivery_url( 'edit' ),
			'secret'            => $webhook->get_secret( 'edit' ),
			'topic'             => $webhook->get_topic( 'edit' ),
			'date_modified'     => $date_modified,
			'date_modified_gmt' => $date_modified_gmt,
			'api_version'       => $this->get_api_version_number( $webhook->get_api_version( 'edit' ) ),
			'failure_count'     => $webhook->get_failure_count( 'edit' ),
			'pending_delivery'  => $webhook->get_pending_delivery( 'edit' ),
		);

		$wpdb->update(
			$wpdb->prefix . 'wc_webhooks',
			$data,
			array(
				'webhook_id' => $webhook->get_id(),
			)
		); // WPCS: DB call ok.

		$webhook->apply_changes();

		if ( isset( $changes['status'] ) ) {
			// We need to delete all transients, because we can't be sure of the old status.
			$this->delete_transients( 'all' );
		}
		wp_cache_delete( $webhook->get_id(), 'webhooks' );
		WC_Cache_Helper::invalidate_cache_group( 'webhooks' );

		if ( 'active' === $webhook->get_status() && ( $trigger || $webhook->get_pending_delivery() ) ) {
			$webhook->deliver_ping();
		}

		do_action( 'woocommerce_webhook_updated', $webhook->get_id() );
	}

	/**
	 * Remove a webhook from the database.
	 *
	 * @since 3.3.0
	 * @param WC_Webhook $webhook      Webhook instance.
	 */
	public function delete( &$webhook ) {
		global $wpdb;

		$wpdb->delete(
			$wpdb->prefix . 'wc_webhooks',
			array(
				'webhook_id' => $webhook->get_id(),
			),
			array( '%d' )
		); // WPCS: cache ok, DB call ok.

		$this->delete_transients( 'all' );
		wp_cache_delete( $webhook->get_id(), 'webhooks' );
		WC_Cache_Helper::invalidate_cache_group( 'webhooks' );
		do_action( 'woocommerce_webhook_deleted', $webhook->get_id(), $webhook );
	}

	/**
	 * Get API version number.
	 *
	 * @since  3.3.0
	 * @param  string $api_version REST API version.
	 * @return int
	 */
	public function get_api_version_number( $api_version ) {
		return 'legacy_v3' === $api_version ? -1 : intval( substr( $api_version, -1 ) );
	}

	/**
	 * Get webhooks IDs from the database.
	 *
	 * @since  3.3.0
	 * @throws InvalidArgumentException If a $status value is passed in that is not in the known wc_get_webhook_statuses() keys.
	 * @param  string $status Optional - status to filter results by. Must be a key in return value of @see wc_get_webhook_statuses(). @since 3.6.0.
	 * @return int[]
	 */
	public function get_webhooks_ids( $status = '' ) {
		if ( ! empty( $status ) ) {
			$this->validate_status( $status );
		}

		$ids = get_transient( $this->get_transient_key( $status ) );

		if ( false === $ids ) {
			$ids = $this->search_webhooks(
				array(
					'limit'  => -1,
					'status' => $status,
				)
			);
			$ids = array_map( 'absint', $ids );
			set_transient( $this->get_transient_key( $status ), $ids );
		}

		return $ids;
	}

	/**
	 * Search webhooks.
	 *
	 * @param  array $args Search arguments.
	 * @return array|object
	 */
	public function search_webhooks( $args ) {
		global $wpdb;

		$args = wp_parse_args(
			$args,
			array(
				'limit'    => 10,
				'offset'   => 0,
				'order'    => 'DESC',
				'orderby'  => 'id',
				'paginate' => false,
			)
		);

		// Map post statuses.
		$statuses = array(
			'publish' => 'active',
			'draft'   => 'paused',
			'pending' => 'disabled',
		);

		// Map orderby to support a few post keys.
		$orderby_mapping = array(
			'ID'            => 'webhook_id',
			'id'            => 'webhook_id',
			'name'          => 'name',
			'title'         => 'name',
			'post_title'    => 'name',
			'post_name'     => 'name',
			'date_created'  => 'date_created_gmt',
			'date'          => 'date_created_gmt',
			'post_date'     => 'date_created_gmt',
			'date_modified' => 'date_modified_gmt',
			'modified'      => 'date_modified_gmt',
			'post_modified' => 'date_modified_gmt',
		);
		$orderby         = isset( $orderby_mapping[ $args['orderby'] ] ) ? $orderby_mapping[ $args['orderby'] ] : 'webhook_id';
		$sort            = 'ASC' === strtoupper( $args['order'] ) ? 'ASC' : 'DESC';
		$order           = "ORDER BY {$orderby} {$sort}";
		$limit           = -1 < $args['limit'] ? $wpdb->prepare( 'LIMIT %d', $args['limit'] ) : '';
		$offset          = 0 < $args['offset'] ? $wpdb->prepare( 'OFFSET %d', $args['offset'] ) : '';
		$status          = ! empty( $args['status'] ) ? $wpdb->prepare( 'AND `status` = %s', isset( $statuses[ $args['status'] ] ) ? $statuses[ $args['status'] ] : $args['status'] ) : '';
		$search          = ! empty( $args['search'] ) ? $wpdb->prepare( 'AND `name` LIKE %s', '%' . $wpdb->esc_like( sanitize_text_field( $args['search'] ) ) . '%' ) : '';
		$include         = '';
		$exclude         = '';
		$date_created    = '';
		$date_modified   = '';

		if ( ! empty( $args['include'] ) ) {
			$args['include'] = implode( ',', wp_parse_id_list( $args['include'] ) );
			$include         = 'AND webhook_id IN (' . $args['include'] . ')';
		}

		if ( ! empty( $args['exclude'] ) ) {
			$args['exclude'] = implode( ',', wp_parse_id_list( $args['exclude'] ) );
			$exclude         = 'AND webhook_id NOT IN (' . $args['exclude'] . ')';
		}

		if ( ! empty( $args['after'] ) || ! empty( $args['before'] ) ) {
			$args['after']  = empty( $args['after'] ) ? '0000-00-00' : $args['after'];
			$args['before'] = empty( $args['before'] ) ? current_time( 'mysql', 1 ) : $args['before'];

			$date_created = "AND `date_created_gmt` BETWEEN STR_TO_DATE('" . esc_sql( $args['after'] ) . "', '%Y-%m-%d %H:%i:%s') and STR_TO_DATE('" . esc_sql( $args['before'] ) . "', '%Y-%m-%d %H:%i:%s')";
		}

		if ( ! empty( $args['modified_after'] ) || ! empty( $args['modified_before'] ) ) {
			$args['modified_after']  = empty( $args['modified_after'] ) ? '0000-00-00' : $args['modified_after'];
			$args['modified_before'] = empty( $args['modified_before'] ) ? current_time( 'mysql', 1 ) : $args['modified_before'];

			$date_modified = "AND `date_modified_gmt` BETWEEN STR_TO_DATE('" . esc_sql( $args['modified_after'] ) . "', '%Y-%m-%d %H:%i:%s') and STR_TO_DATE('" . esc_sql( $args['modified_before'] ) . "', '%Y-%m-%d %H:%i:%s')";
		}

		// Check for cache.
		$cache_key   = WC_Cache_Helper::get_cache_prefix( 'webhooks' ) . 'search_webhooks' . md5( implode( ',', $args ) );
		$cache_value = wp_cache_get( $cache_key, 'webhook_search_results' );

		if ( $cache_value ) {
			return $cache_value;
		}

		if ( $args['paginate'] ) {
			$query = trim(
				"SELECT SQL_CALC_FOUND_ROWS webhook_id
				FROM {$wpdb->prefix}wc_webhooks
				WHERE 1=1
				{$status}
				{$search}
				{$include}
				{$exclude}
				{$date_created}
				{$date_modified}
				{$order}
				{$limit}
				{$offset}"
			);

			$webhook_ids  = wp_parse_id_list( $wpdb->get_col( $query ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			$total        = (int) $wpdb->get_var( 'SELECT FOUND_ROWS();' );
			$return_value = (object) array(
				'webhooks'      => $webhook_ids,
				'total'         => $total,
				'max_num_pages' => $args['limit'] > 1 ? ceil( $total / $args['limit'] ) : 1,
			);
		} else {
			$query = trim(
				"SELECT webhook_id
				FROM {$wpdb->prefix}wc_webhooks
				WHERE 1=1
				{$status}
				{$search}
				{$include}
				{$exclude}
				{$date_created}
				{$date_modified}
				{$order}
				{$limit}
				{$offset}"
			);

			$webhook_ids  = wp_parse_id_list( $wpdb->get_col( $query ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			$return_value = $webhook_ids;
		}

		wp_cache_set( $cache_key, $return_value, 'webhook_search_results' );

		return $return_value;
	}

	/**
	 * Count webhooks.
	 *
	 * @since 3.6.0
	 * @param string $status Status to count.
	 * @return int
	 */
	protected function get_webhook_count( $status = 'active' ) {
		global $wpdb;
		$cache_key = WC_Cache_Helper::get_cache_prefix( 'webhooks' ) . $status . '_count';
		$count     = wp_cache_get( $cache_key, 'webhooks' );

		if ( false === $count ) {
			$count = absint( $wpdb->get_var( $wpdb->prepare( "SELECT count( webhook_id ) FROM {$wpdb->prefix}wc_webhooks WHERE `status` = %s;", $status ) ) );

			wp_cache_add( $cache_key, $count, 'webhooks' );
		}

		return $count;
	}

	/**
	 * Get total webhook counts by status.
	 *
	 * @return array
	 */
	public function get_count_webhooks_by_status() {
		$statuses = array_keys( wc_get_webhook_statuses() );
		$counts   = array();

		foreach ( $statuses as $status ) {
			$counts[ $status ] = $this->get_webhook_count( $status );
		}

		return $counts;
	}

	/**
	 * Check if a given string is in known statuses, based on return value of @see wc_get_webhook_statuses().
	 *
	 * @since  3.6.0
	 * @throws InvalidArgumentException If $status is not empty and not in the known wc_get_webhook_statuses() keys.
	 * @param  string $status Status to check.
	 */
	private function validate_status( $status ) {
		if ( ! array_key_exists( $status, wc_get_webhook_statuses() ) ) {
			throw new InvalidArgumentException( sprintf( 'Invalid status given: %s. Status must be one of: %s.', $status, implode( ', ', array_keys( wc_get_webhook_statuses() ) ) ) );
		}
	}

	/**
	 * Get the transient key used to cache a set of webhook IDs, optionally filtered by status.
	 *
	 * @since  3.6.0
	 * @param  string $status Optional - status of cache key.
	 * @return string
	 */
	private function get_transient_key( $status = '' ) {
		return empty( $status ) ? 'woocommerce_webhook_ids' : sprintf( 'woocommerce_webhook_ids_status_%s', $status );
	}

	/**
	 * Delete the transients used to cache a set of webhook IDs, optionally filtered by status.
	 *
	 * @since 3.6.0
	 * @param string $status Optional - status of cache to delete, or 'all' to delete all caches.
	 */
	private function delete_transients( $status = '' ) {

		// Always delete the non-filtered cache.
		delete_transient( $this->get_transient_key( '' ) );

		if ( ! empty( $status ) ) {
			if ( 'all' === $status ) {
				foreach ( wc_get_webhook_statuses() as $status_key => $status_string ) {
					delete_transient( $this->get_transient_key( $status_key ) );
				}
			} else {
				delete_transient( $this->get_transient_key( $status ) );
			}
		}
	}
}
data-stores/class-wc-order-item-coupon-data-store.php000064400000003044151336044770016641 0ustar00<?php
/**
 * Class WC_Order_Item_Coupon_Data_Store file.
 *
 * @package WooCommerce\DataStores
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Order Item Coupon Data Store
 *
 * @version  3.0.0
 */
class WC_Order_Item_Coupon_Data_Store extends Abstract_WC_Order_Item_Type_Data_Store implements WC_Object_Data_Store_Interface, WC_Order_Item_Type_Data_Store_Interface {

	/**
	 * Data stored in meta keys.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $internal_meta_keys = array( 'discount_amount', 'discount_amount_tax' );

	/**
	 * Read/populate data properties specific to this order item.
	 *
	 * @since 3.0.0
	 * @param WC_Order_Item_Coupon $item Coupon order item.
	 */
	public function read( &$item ) {
		parent::read( $item );
		$id = $item->get_id();
		$item->set_props(
			array(
				'discount'     => get_metadata( 'order_item', $id, 'discount_amount', true ),
				'discount_tax' => get_metadata( 'order_item', $id, 'discount_amount_tax', true ),
			)
		);
		$item->set_object_read( true );
	}

	/**
	 * Saves an item's data to the database / item meta.
	 * Ran after both create and update, so $item->get_id() will be set.
	 *
	 * @since 3.0.0
	 * @param WC_Order_Item_Coupon $item Coupon order item.
	 */
	public function save_item_data( &$item ) {
		$id          = $item->get_id();
		$save_values = array(
			'discount_amount'     => $item->get_discount( 'edit' ),
			'discount_amount_tax' => $item->get_discount_tax( 'edit' ),
		);
		foreach ( $save_values as $key => $value ) {
			update_metadata( 'order_item', $id, $key, $value );
		}
	}
}
data-stores/class-wc-order-data-store-cpt.php000064400000111011151336044770015162 0ustar00<?php
/**
 * WC_Order_Data_Store_CPT class file.
 *
 * @package WooCommerce\Classes
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Order Data Store: Stored in CPT.
 *
 * @version  3.0.0
 */
class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implements WC_Object_Data_Store_Interface, WC_Order_Data_Store_Interface {

	/**
	 * Data stored in meta keys, but not considered "meta" for an order.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $internal_meta_keys = array(
		'_customer_user',
		'_order_key',
		'_order_currency',
		'_billing_first_name',
		'_billing_last_name',
		'_billing_company',
		'_billing_address_1',
		'_billing_address_2',
		'_billing_city',
		'_billing_state',
		'_billing_postcode',
		'_billing_country',
		'_billing_email',
		'_billing_phone',
		'_shipping_first_name',
		'_shipping_last_name',
		'_shipping_company',
		'_shipping_address_1',
		'_shipping_address_2',
		'_shipping_city',
		'_shipping_state',
		'_shipping_postcode',
		'_shipping_country',
		'_shipping_phone',
		'_completed_date',
		'_paid_date',
		'_edit_lock',
		'_edit_last',
		'_cart_discount',
		'_cart_discount_tax',
		'_order_shipping',
		'_order_shipping_tax',
		'_order_tax',
		'_order_total',
		'_payment_method',
		'_payment_method_title',
		'_transaction_id',
		'_customer_ip_address',
		'_customer_user_agent',
		'_created_via',
		'_order_version',
		'_prices_include_tax',
		'_date_completed',
		'_date_paid',
		'_payment_tokens',
		'_billing_address_index',
		'_shipping_address_index',
		'_recorded_sales',
		'_recorded_coupon_usage_counts',
		'_download_permissions_granted',
		'_order_stock_reduced',
	);

	/**
	 * Method to create a new order in the database.
	 *
	 * @param WC_Order $order Order object.
	 */
	public function create( &$order ) {
		if ( '' === $order->get_order_key() ) {
			$order->set_order_key( wc_generate_order_key() );
		}
		parent::create( $order );
		do_action( 'woocommerce_new_order', $order->get_id(), $order );
	}

	/**
	 * Read order data. Can be overridden by child classes to load other props.
	 *
	 * @param WC_Order $order Order object.
	 * @param object   $post_object Post object.
	 * @since 3.0.0
	 */
	protected function read_order_data( &$order, $post_object ) {
		parent::read_order_data( $order, $post_object );
		$id             = $order->get_id();
		$date_completed = get_post_meta( $id, '_date_completed', true );
		$date_paid      = get_post_meta( $id, '_date_paid', true );

		if ( ! $date_completed ) {
			$date_completed = get_post_meta( $id, '_completed_date', true );
		}

		if ( ! $date_paid ) {
			$date_paid = get_post_meta( $id, '_paid_date', true );
		}

		$order->set_props(
			array(
				'order_key'            => get_post_meta( $id, '_order_key', true ),
				'customer_id'          => get_post_meta( $id, '_customer_user', true ),
				'billing_first_name'   => get_post_meta( $id, '_billing_first_name', true ),
				'billing_last_name'    => get_post_meta( $id, '_billing_last_name', true ),
				'billing_company'      => get_post_meta( $id, '_billing_company', true ),
				'billing_address_1'    => get_post_meta( $id, '_billing_address_1', true ),
				'billing_address_2'    => get_post_meta( $id, '_billing_address_2', true ),
				'billing_city'         => get_post_meta( $id, '_billing_city', true ),
				'billing_state'        => get_post_meta( $id, '_billing_state', true ),
				'billing_postcode'     => get_post_meta( $id, '_billing_postcode', true ),
				'billing_country'      => get_post_meta( $id, '_billing_country', true ),
				'billing_email'        => get_post_meta( $id, '_billing_email', true ),
				'billing_phone'        => get_post_meta( $id, '_billing_phone', true ),
				'shipping_first_name'  => get_post_meta( $id, '_shipping_first_name', true ),
				'shipping_last_name'   => get_post_meta( $id, '_shipping_last_name', true ),
				'shipping_company'     => get_post_meta( $id, '_shipping_company', true ),
				'shipping_address_1'   => get_post_meta( $id, '_shipping_address_1', true ),
				'shipping_address_2'   => get_post_meta( $id, '_shipping_address_2', true ),
				'shipping_city'        => get_post_meta( $id, '_shipping_city', true ),
				'shipping_state'       => get_post_meta( $id, '_shipping_state', true ),
				'shipping_postcode'    => get_post_meta( $id, '_shipping_postcode', true ),
				'shipping_country'     => get_post_meta( $id, '_shipping_country', true ),
				'shipping_phone'       => get_post_meta( $id, '_shipping_phone', true ),
				'payment_method'       => get_post_meta( $id, '_payment_method', true ),
				'payment_method_title' => get_post_meta( $id, '_payment_method_title', true ),
				'transaction_id'       => get_post_meta( $id, '_transaction_id', true ),
				'customer_ip_address'  => get_post_meta( $id, '_customer_ip_address', true ),
				'customer_user_agent'  => get_post_meta( $id, '_customer_user_agent', true ),
				'created_via'          => get_post_meta( $id, '_created_via', true ),
				'date_completed'       => $date_completed,
				'date_paid'            => $date_paid,
				'cart_hash'            => get_post_meta( $id, '_cart_hash', true ),
				'customer_note'        => $post_object->post_excerpt,
			)
		);
	}

	/**
	 * Method to update an order in the database.
	 *
	 * @param WC_Order $order Order object.
	 */
	public function update( &$order ) {
		// Before updating, ensure date paid is set if missing.
		if ( ! $order->get_date_paid( 'edit' ) && version_compare( $order->get_version( 'edit' ), '3.0', '<' ) && $order->has_status( apply_filters( 'woocommerce_payment_complete_order_status', $order->needs_processing() ? 'processing' : 'completed', $order->get_id(), $order ) ) ) {
			$order->set_date_paid( $order->get_date_created( 'edit' ) );
		}

		// Also grab the current status so we can compare.
		$previous_status = get_post_status( $order->get_id() );

		// Update the order.
		parent::update( $order );

		// Fire a hook depending on the status - this should be considered a creation if it was previously draft status.
		$new_status = $order->get_status( 'edit' );

		if ( $new_status !== $previous_status && in_array( $previous_status, array( 'new', 'auto-draft', 'draft' ), true ) ) {
			do_action( 'woocommerce_new_order', $order->get_id(), $order );
		} else {
			do_action( 'woocommerce_update_order', $order->get_id(), $order );
		}
	}

	/**
	 * Helper method that updates all the post meta for an order based on it's settings in the WC_Order class.
	 *
	 * @param WC_Order $order Order object.
	 * @since 3.0.0
	 */
	protected function update_post_meta( &$order ) {
		$updated_props     = array();
		$id                = $order->get_id();
		$meta_key_to_props = array(
			'_order_key'            => 'order_key',
			'_customer_user'        => 'customer_id',
			'_payment_method'       => 'payment_method',
			'_payment_method_title' => 'payment_method_title',
			'_transaction_id'       => 'transaction_id',
			'_customer_ip_address'  => 'customer_ip_address',
			'_customer_user_agent'  => 'customer_user_agent',
			'_created_via'          => 'created_via',
			'_date_completed'       => 'date_completed',
			'_date_paid'            => 'date_paid',
			'_cart_hash'            => 'cart_hash',
		);

		$props_to_update = $this->get_props_to_update( $order, $meta_key_to_props );

		foreach ( $props_to_update as $meta_key => $prop ) {
			$value = $order->{"get_$prop"}( 'edit' );
			$value = is_string( $value ) ? wp_slash( $value ) : $value;
			switch ( $prop ) {
				case 'date_paid':
				case 'date_completed':
					$value = ! is_null( $value ) ? $value->getTimestamp() : '';
					break;
			}

			$updated = $this->update_or_delete_post_meta( $order, $meta_key, $value );

			if ( $updated ) {
				$updated_props[] = $prop;
			}
		}

		$address_props = array(
			'billing'  => array(
				'_billing_first_name' => 'billing_first_name',
				'_billing_last_name'  => 'billing_last_name',
				'_billing_company'    => 'billing_company',
				'_billing_address_1'  => 'billing_address_1',
				'_billing_address_2'  => 'billing_address_2',
				'_billing_city'       => 'billing_city',
				'_billing_state'      => 'billing_state',
				'_billing_postcode'   => 'billing_postcode',
				'_billing_country'    => 'billing_country',
				'_billing_email'      => 'billing_email',
				'_billing_phone'      => 'billing_phone',
			),
			'shipping' => array(
				'_shipping_first_name' => 'shipping_first_name',
				'_shipping_last_name'  => 'shipping_last_name',
				'_shipping_company'    => 'shipping_company',
				'_shipping_address_1'  => 'shipping_address_1',
				'_shipping_address_2'  => 'shipping_address_2',
				'_shipping_city'       => 'shipping_city',
				'_shipping_state'      => 'shipping_state',
				'_shipping_postcode'   => 'shipping_postcode',
				'_shipping_country'    => 'shipping_country',
				'_shipping_phone'      => 'shipping_phone',
			),
		);

		foreach ( $address_props as $props_key => $props ) {
			$props_to_update = $this->get_props_to_update( $order, $props );
			foreach ( $props_to_update as $meta_key => $prop ) {
				$value   = $order->{"get_$prop"}( 'edit' );
				$value   = is_string( $value ) ? wp_slash( $value ) : $value;
				$updated = $this->update_or_delete_post_meta( $order, $meta_key, $value );

				if ( $updated ) {
					$updated_props[] = $prop;
					$updated_props[] = $props_key;
				}
			}
		}

		parent::update_post_meta( $order );

		// If address changed, store concatenated version to make searches faster.
		if ( in_array( 'billing', $updated_props, true ) || ! metadata_exists( 'post', $id, '_billing_address_index' ) ) {
			update_post_meta( $id, '_billing_address_index', implode( ' ', $order->get_address( 'billing' ) ) );
		}
		if ( in_array( 'shipping', $updated_props, true ) || ! metadata_exists( 'post', $id, '_shipping_address_index' ) ) {
			update_post_meta( $id, '_shipping_address_index', implode( ' ', $order->get_address( 'shipping' ) ) );
		}

		// Legacy date handling. @todo remove in 4.0.
		if ( in_array( 'date_paid', $updated_props, true ) ) {
			$value = $order->get_date_paid( 'edit' );
			// In 2.6.x date_paid was stored as _paid_date in local mysql format.
			update_post_meta( $id, '_paid_date', ! is_null( $value ) ? $value->date( 'Y-m-d H:i:s' ) : '' );
		}

		if ( in_array( 'date_completed', $updated_props, true ) ) {
			$value = $order->get_date_completed( 'edit' );
			// In 2.6.x date_completed was stored as _completed_date in local mysql format.
			update_post_meta( $id, '_completed_date', ! is_null( $value ) ? $value->date( 'Y-m-d H:i:s' ) : '' );
		}

		// If customer changed, update any downloadable permissions.
		if ( in_array( 'customer_id', $updated_props ) || in_array( 'billing_email', $updated_props ) ) {
			$data_store = WC_Data_Store::load( 'customer-download' );
			$data_store->update_user_by_order_id( $id, $order->get_customer_id(), $order->get_billing_email() );
		}

		// Mark user account as active.
		if ( in_array( 'customer_id', $updated_props, true ) ) {
			wc_update_user_last_active( $order->get_customer_id() );
		}

		do_action( 'woocommerce_order_object_updated_props', $order, $updated_props );
	}

	/**
	 * Excerpt for post.
	 *
	 * @param  WC_Order $order Order object.
	 * @return string
	 */
	protected function get_post_excerpt( $order ) {
		return $order->get_customer_note();
	}

	/**
	 * Get order key.
	 *
	 * @since 4.3.0
	 * @param WC_order $order Order object.
	 * @return string
	 */
	protected function get_order_key( $order ) {
		if ( '' !== $order->get_order_key() ) {
			return $order->get_order_key();
		}

		return parent::get_order_key( $order );
	}

	/**
	 * Get amount already refunded.
	 *
	 * @param  WC_Order $order Order object.
	 * @return float
	 */
	public function get_total_refunded( $order ) {
		global $wpdb;

		$total = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT SUM( postmeta.meta_value )
				FROM $wpdb->postmeta AS postmeta
				INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d )
				WHERE postmeta.meta_key = '_refund_amount'
				AND postmeta.post_id = posts.ID",
				$order->get_id()
			)
		);

		return floatval( $total );
	}

	/**
	 * Get the total tax refunded.
	 *
	 * @param  WC_Order $order Order object.
	 * @return float
	 */
	public function get_total_tax_refunded( $order ) {
		global $wpdb;

		$total = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT SUM( order_itemmeta.meta_value )
				FROM {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta
				INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d )
				INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ( order_items.order_id = posts.ID AND order_items.order_item_type = 'tax' )
				WHERE order_itemmeta.order_item_id = order_items.order_item_id
				AND order_itemmeta.meta_key IN ('tax_amount', 'shipping_tax_amount')",
				$order->get_id()
			)
		);

		return abs( $total );
	}

	/**
	 * Get the total shipping refunded.
	 *
	 * @param  WC_Order $order Order object.
	 * @return float
	 */
	public function get_total_shipping_refunded( $order ) {
		global $wpdb;

		$total = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT SUM( order_itemmeta.meta_value )
				FROM {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta
				INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d )
				INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ( order_items.order_id = posts.ID AND order_items.order_item_type = 'shipping' )
				WHERE order_itemmeta.order_item_id = order_items.order_item_id
				AND order_itemmeta.meta_key IN ('cost')",
				$order->get_id()
			)
		);

		return abs( $total );
	}

	/**
	 * Finds an Order ID based on an order key.
	 *
	 * @param string $order_key An order key has generated by.
	 * @return int The ID of an order, or 0 if the order could not be found
	 */
	public function get_order_id_by_order_key( $order_key ) {
		global $wpdb;
		return $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM {$wpdb->prefix}postmeta WHERE meta_key = '_order_key' AND meta_value = %s", $order_key ) );
	}

	/**
	 * Return count of orders with a specific status.
	 *
	 * @param  string $status Order status. Function wc_get_order_statuses() returns a list of valid statuses.
	 * @return int
	 */
	public function get_order_count( $status ) {
		global $wpdb;
		return absint( $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( * ) FROM {$wpdb->posts} WHERE post_type = 'shop_order' AND post_status = %s", $status ) ) );
	}

	/**
	 * Get all orders matching the passed in args.
	 *
	 * @deprecated 3.1.0 - Use wc_get_orders instead.
	 * @see    wc_get_orders()
	 *
	 * @param  array $args List of args passed to wc_get_orders().
	 *
	 * @return array|object
	 */
	public function get_orders( $args = array() ) {
		wc_deprecated_function( 'WC_Order_Data_Store_CPT::get_orders', '3.1.0', 'Use wc_get_orders instead.' );
		return wc_get_orders( $args );
	}

	/**
	 * Generate meta query for wc_get_orders.
	 *
	 * @param  array  $values List of customers ids or emails.
	 * @param  string $relation 'or' or 'and' relation used to build the WP meta_query.
	 * @return array
	 */
	private function get_orders_generate_customer_meta_query( $values, $relation = 'or' ) {
		$meta_query = array(
			'relation'        => strtoupper( $relation ),
			'customer_emails' => array(
				'key'     => '_billing_email',
				'value'   => array(),
				'compare' => 'IN',
			),
			'customer_ids'    => array(
				'key'     => '_customer_user',
				'value'   => array(),
				'compare' => 'IN',
			),
		);
		foreach ( $values as $value ) {
			if ( is_array( $value ) ) {
				$query_part = $this->get_orders_generate_customer_meta_query( $value, 'and' );
				if ( is_wp_error( $query_part ) ) {
					return $query_part;
				}
				$meta_query[] = $query_part;
			} elseif ( is_email( $value ) ) {
				$meta_query['customer_emails']['value'][] = sanitize_email( $value );
			} elseif ( is_numeric( $value ) ) {
				$meta_query['customer_ids']['value'][] = strval( absint( $value ) );
			} else {
				return new WP_Error( 'woocommerce_query_invalid', __( 'Invalid customer query.', 'woocommerce' ), $values );
			}
		}

		if ( empty( $meta_query['customer_emails']['value'] ) ) {
			unset( $meta_query['customer_emails'] );
			unset( $meta_query['relation'] );
		}

		if ( empty( $meta_query['customer_ids']['value'] ) ) {
			unset( $meta_query['customer_ids'] );
			unset( $meta_query['relation'] );
		}

		return $meta_query;
	}

	/**
	 * Get unpaid orders after a certain date,
	 *
	 * @param  int $date Timestamp.
	 * @return array
	 */
	public function get_unpaid_orders( $date ) {
		global $wpdb;

		$unpaid_orders = $wpdb->get_col(
			$wpdb->prepare(
				// @codingStandardsIgnoreStart
				"SELECT posts.ID
				FROM {$wpdb->posts} AS posts
				WHERE   posts.post_type   IN ('" . implode( "','", wc_get_order_types() ) . "')
				AND     posts.post_status = 'wc-pending'
				AND     posts.post_modified < %s",
				// @codingStandardsIgnoreEnd
				gmdate( 'Y-m-d H:i:s', absint( $date ) )
			)
		);

		return $unpaid_orders;
	}

	/**
	 * Search order data for a term and return ids.
	 *
	 * @param  string $term Searched term.
	 * @return array of ids
	 */
	public function search_orders( $term ) {
		global $wpdb;

		/**
		 * Searches on meta data can be slow - this lets you choose what fields to search.
		 * 3.0.0 added _billing_address and _shipping_address meta which contains all address data to make this faster.
		 * This however won't work on older orders unless updated, so search a few others (expand this using the filter if needed).
		 *
		 * @var array
		 */
		$search_fields = array_map(
			'wc_clean',
			apply_filters(
				'woocommerce_shop_order_search_fields',
				array(
					'_billing_address_index',
					'_shipping_address_index',
					'_billing_last_name',
					'_billing_email',
				)
			)
		);
		$order_ids     = array();

		if ( is_numeric( $term ) ) {
			$order_ids[] = absint( $term );
		}

		if ( ! empty( $search_fields ) ) {
			$order_ids = array_unique(
				array_merge(
					$order_ids,
					$wpdb->get_col(
						$wpdb->prepare(
							"SELECT DISTINCT p1.post_id FROM {$wpdb->postmeta} p1 WHERE p1.meta_value LIKE %s AND p1.meta_key IN ('" . implode( "','", array_map( 'esc_sql', $search_fields ) ) . "')", // @codingStandardsIgnoreLine
							'%' . $wpdb->esc_like( wc_clean( $term ) ) . '%'
						)
					),
					$wpdb->get_col(
						$wpdb->prepare(
							"SELECT order_id
							FROM {$wpdb->prefix}woocommerce_order_items as order_items
							WHERE order_item_name LIKE %s",
							'%' . $wpdb->esc_like( wc_clean( $term ) ) . '%'
						)
					)
				)
			);
		}

		return apply_filters( 'woocommerce_shop_order_search_results', $order_ids, $term, $search_fields );
	}

	/**
	 * Gets information about whether permissions were generated yet.
	 *
	 * @param WC_Order|int $order Order ID or order object.
	 * @return bool
	 */
	public function get_download_permissions_granted( $order ) {
		$order_id = WC_Order_Factory::get_order_id( $order );
		return wc_string_to_bool( get_post_meta( $order_id, '_download_permissions_granted', true ) );
	}

	/**
	 * Stores information about whether permissions were generated yet.
	 *
	 * @param WC_Order|int $order Order ID or order object.
	 * @param bool         $set True or false.
	 */
	public function set_download_permissions_granted( $order, $set ) {
		$order_id = WC_Order_Factory::get_order_id( $order );
		update_post_meta( $order_id, '_download_permissions_granted', wc_bool_to_string( $set ) );
	}

	/**
	 * Gets information about whether sales were recorded.
	 *
	 * @param WC_Order|int $order Order ID or order object.
	 * @return bool
	 */
	public function get_recorded_sales( $order ) {
		$order_id = WC_Order_Factory::get_order_id( $order );
		return wc_string_to_bool( get_post_meta( $order_id, '_recorded_sales', true ) );
	}

	/**
	 * Stores information about whether sales were recorded.
	 *
	 * @param WC_Order|int $order Order ID or order object.
	 * @param bool         $set True or false.
	 */
	public function set_recorded_sales( $order, $set ) {
		$order_id = WC_Order_Factory::get_order_id( $order );
		update_post_meta( $order_id, '_recorded_sales', wc_bool_to_string( $set ) );
	}

	/**
	 * Gets information about whether coupon counts were updated.
	 *
	 * @param WC_Order|int $order Order ID or order object.
	 * @return bool
	 */
	public function get_recorded_coupon_usage_counts( $order ) {
		$order_id = WC_Order_Factory::get_order_id( $order );
		return wc_string_to_bool( get_post_meta( $order_id, '_recorded_coupon_usage_counts', true ) );
	}

	/**
	 * Stores information about whether coupon counts were updated.
	 *
	 * @param WC_Order|int $order Order ID or order object.
	 * @param bool         $set True or false.
	 */
	public function set_recorded_coupon_usage_counts( $order, $set ) {
		$order_id = WC_Order_Factory::get_order_id( $order );
		update_post_meta( $order_id, '_recorded_coupon_usage_counts', wc_bool_to_string( $set ) );
	}

	/**
	 * Return array of coupon_code => meta_key for coupon which have usage limit and have tentative keys.
	 * Pass $coupon_id if key for only one of the coupon is needed.
	 *
	 * @param WC_Order $order     Order object.
	 * @param int      $coupon_id If passed, will return held key for that coupon.
	 *
	 * @return array|string Key value pair for coupon code and meta key name. If $coupon_id is passed, returns meta_key for only that coupon.
	 */
	public function get_coupon_held_keys( $order, $coupon_id = null ) {
		$held_keys = $order->get_meta( '_coupon_held_keys' );
		if ( $coupon_id ) {
			return isset( $held_keys[ $coupon_id ] ) ? $held_keys[ $coupon_id ] : null;
		}
		return $held_keys;
	}

	/**
	 * Return array of coupon_code => meta_key for coupon which have usage limit per customer and have tentative keys.
	 *
	 * @param WC_Order $order Order object.
	 * @param int      $coupon_id If passed, will return held key for that coupon.
	 *
	 * @return mixed
	 */
	public function get_coupon_held_keys_for_users( $order, $coupon_id = null ) {
		$held_keys_for_user = $order->get_meta( '_coupon_held_keys_for_users' );
		if ( $coupon_id ) {
			return isset( $held_keys_for_user[ $coupon_id ] ) ? $held_keys_for_user[ $coupon_id ] : null;
		}
		return $held_keys_for_user;
	}

	/**
	 * Add/Update list of meta keys that are currently being used by this order to hold a coupon.
	 * This is used to figure out what all meta entries we should delete when order is cancelled/completed.
	 *
	 * @param WC_Order $order              Order object.
	 * @param array    $held_keys          Array of coupon_code => meta_key.
	 * @param array    $held_keys_for_user Array of coupon_code => meta_key for held coupon for user.
	 *
	 * @return mixed
	 */
	public function set_coupon_held_keys( $order, $held_keys, $held_keys_for_user ) {
		if ( is_array( $held_keys ) && 0 < count( $held_keys ) ) {
			$order->update_meta_data( '_coupon_held_keys', $held_keys );
		}
		if ( is_array( $held_keys_for_user ) && 0 < count( $held_keys_for_user ) ) {
			$order->update_meta_data( '_coupon_held_keys_for_users', $held_keys_for_user );
		}
	}

	/**
	 * Release all coupons held by this order.
	 *
	 * @param WC_Order $order Current order object.
	 * @param bool     $save  Whether to delete keys from DB right away. Could be useful to pass `false` if you are building a bulk request.
	 */
	public function release_held_coupons( $order, $save = true ) {
		$coupon_held_keys = $this->get_coupon_held_keys( $order );
		if ( is_array( $coupon_held_keys ) ) {
			foreach ( $coupon_held_keys as $coupon_id => $meta_key ) {
				delete_post_meta( $coupon_id, $meta_key );
			}
		}
		$order->delete_meta_data( '_coupon_held_keys' );

		$coupon_held_keys_for_users = $this->get_coupon_held_keys_for_users( $order );
		if ( is_array( $coupon_held_keys_for_users ) ) {
			foreach ( $coupon_held_keys_for_users as $coupon_id => $meta_key ) {
				delete_post_meta( $coupon_id, $meta_key );
			}
		}
		$order->delete_meta_data( '_coupon_held_keys_for_users' );

		if ( $save ) {
			$order->save_meta_data();
		}

	}

	/**
	 * Gets information about whether stock was reduced.
	 *
	 * @param WC_Order|int $order Order ID or order object.
	 * @return bool
	 */
	public function get_stock_reduced( $order ) {
		$order_id = WC_Order_Factory::get_order_id( $order );
		return wc_string_to_bool( get_post_meta( $order_id, '_order_stock_reduced', true ) );
	}

	/**
	 * Stores information about whether stock was reduced.
	 *
	 * @param WC_Order|int $order Order ID or order object.
	 * @param bool         $set True or false.
	 */
	public function set_stock_reduced( $order, $set ) {
		$order_id = WC_Order_Factory::get_order_id( $order );
		update_post_meta( $order_id, '_order_stock_reduced', wc_bool_to_string( $set ) );
	}

	/**
	 * Get the order type based on Order ID.
	 *
	 * @since 3.0.0
	 * @param int|WP_Post $order Order | Order id.
	 *
	 * @return string
	 */
	public function get_order_type( $order ) {
		return get_post_type( $order );
	}

	/**
	 * Get valid WP_Query args from a WC_Order_Query's query variables.
	 *
	 * @since 3.1.0
	 * @param array $query_vars query vars from a WC_Order_Query.
	 * @return array
	 */
	protected function get_wp_query_args( $query_vars ) {

		// Map query vars to ones that get_wp_query_args or WP_Query recognize.
		$key_mapping = array(
			'customer_id'    => 'customer_user',
			'status'         => 'post_status',
			'currency'       => 'order_currency',
			'version'        => 'order_version',
			'discount_total' => 'cart_discount',
			'discount_tax'   => 'cart_discount_tax',
			'shipping_total' => 'order_shipping',
			'shipping_tax'   => 'order_shipping_tax',
			'cart_tax'       => 'order_tax',
			'total'          => 'order_total',
			'page'           => 'paged',
		);

		foreach ( $key_mapping as $query_key => $db_key ) {
			if ( isset( $query_vars[ $query_key ] ) ) {
				$query_vars[ $db_key ] = $query_vars[ $query_key ];
				unset( $query_vars[ $query_key ] );
			}
		}

		// Add the 'wc-' prefix to status if needed.
		if ( ! empty( $query_vars['post_status'] ) ) {
			if ( is_array( $query_vars['post_status'] ) ) {
				foreach ( $query_vars['post_status'] as &$status ) {
					$status = wc_is_order_status( 'wc-' . $status ) ? 'wc-' . $status : $status;
				}
			} else {
				$query_vars['post_status'] = wc_is_order_status( 'wc-' . $query_vars['post_status'] ) ? 'wc-' . $query_vars['post_status'] : $query_vars['post_status'];
			}
		}

		$wp_query_args = parent::get_wp_query_args( $query_vars );

		if ( ! isset( $wp_query_args['date_query'] ) ) {
			$wp_query_args['date_query'] = array();
		}
		if ( ! isset( $wp_query_args['meta_query'] ) ) {
			$wp_query_args['meta_query'] = array();
		}

		$date_queries = array(
			'date_created'   => 'post_date',
			'date_modified'  => 'post_modified',
			'date_completed' => '_date_completed',
			'date_paid'      => '_date_paid',
		);
		foreach ( $date_queries as $query_var_key => $db_key ) {
			if ( isset( $query_vars[ $query_var_key ] ) && '' !== $query_vars[ $query_var_key ] ) {

				// Remove any existing meta queries for the same keys to prevent conflicts.
				$existing_queries = wp_list_pluck( $wp_query_args['meta_query'], 'key', true );
				$meta_query_index = array_search( $db_key, $existing_queries, true );
				if ( false !== $meta_query_index ) {
					unset( $wp_query_args['meta_query'][ $meta_query_index ] );
				}

				$wp_query_args = $this->parse_date_for_wp_query( $query_vars[ $query_var_key ], $db_key, $wp_query_args );
			}
		}

		if ( isset( $query_vars['customer'] ) && '' !== $query_vars['customer'] && array() !== $query_vars['customer'] ) {
			$values         = is_array( $query_vars['customer'] ) ? $query_vars['customer'] : array( $query_vars['customer'] );
			$customer_query = $this->get_orders_generate_customer_meta_query( $values );
			if ( is_wp_error( $customer_query ) ) {
				$wp_query_args['errors'][] = $customer_query;
			} else {
				$wp_query_args['meta_query'][] = $customer_query;
			}
		}

		if ( isset( $query_vars['anonymized'] ) ) {
			if ( $query_vars['anonymized'] ) {
				$wp_query_args['meta_query'][] = array(
					'key'   => '_anonymized',
					'value' => 'yes',
				);
			} else {
				$wp_query_args['meta_query'][] = array(
					'key'     => '_anonymized',
					'compare' => 'NOT EXISTS',
				);
			}
		}

		if ( ! isset( $query_vars['paginate'] ) || ! $query_vars['paginate'] ) {
			$wp_query_args['no_found_rows'] = true;
		}

		return apply_filters( 'woocommerce_order_data_store_cpt_get_orders_query', $wp_query_args, $query_vars, $this );
	}

	/**
	 * Query for Orders matching specific criteria.
	 *
	 * @since 3.1.0
	 *
	 * @param array $query_vars query vars from a WC_Order_Query.
	 *
	 * @return array|object
	 */
	public function query( $query_vars ) {
		$args = $this->get_wp_query_args( $query_vars );

		if ( ! empty( $args['errors'] ) ) {
			$query = (object) array(
				'posts'         => array(),
				'found_posts'   => 0,
				'max_num_pages' => 0,
			);
		} else {
			$query = new WP_Query( $args );
		}

		if ( isset( $query_vars['return'] ) && 'ids' === $query_vars['return'] ) {
			$orders = $query->posts;
		} else {
			update_post_caches( $query->posts ); // We already fetching posts, might as well hydrate some caches.
			$order_ids = wp_list_pluck( $query->posts, 'ID' );
			$orders = $this->compile_orders( $order_ids, $query_vars, $query );
		}

		if ( isset( $query_vars['paginate'] ) && $query_vars['paginate'] ) {
			return (object) array(
				'orders'        => $orders,
				'total'         => $query->found_posts,
				'max_num_pages' => $query->max_num_pages,
			);
		}

		return $orders;
	}

	/**
	 * Compile order response and set caches as needed for order ids.
	 *
	 * @param array    $order_ids  List of order IDS to compile.
	 * @param array    $query_vars Original query arguments.
	 * @param WP_Query $query      Query object.
	 *
	 * @return array Orders.
	 */
	private function compile_orders( $order_ids, $query_vars, $query ) {
		if ( empty( $order_ids ) ) {
			return array();
		}
		$orders = array();

		// Lets do some cache hydrations so that we don't have to fetch data from DB for every order.
		$this->prime_raw_meta_cache_for_orders( $order_ids, $query_vars );
		$this->prime_refund_caches_for_order( $order_ids, $query_vars );
		$this->prime_order_item_caches_for_orders( $order_ids, $query_vars );

		foreach ( $query->posts as $post ) {
			$order = wc_get_order( $post );

			// If the order returns false, don't add it to the list.
			if ( false === $order ) {
				continue;
			}

			$orders[] = $order;
		}

		return $orders;
	}

	/**
	 * Prime refund cache for orders.
	 *
	 * @param array $order_ids  Order Ids to prime cache for.
	 * @param array $query_vars Query vars for the query.
	 */
	private function prime_refund_caches_for_order( $order_ids, $query_vars ) {
		if ( ! isset( $query_vars['type'] ) || ! ( 'shop_order' === $query_vars['type'] ) ) {
			return;
		}
		if ( isset( $query_vars['fields'] ) && 'all' !== $query_vars['fields'] ) {
			if ( is_array( $query_vars['fields'] ) && ! in_array( 'refunds', $query_vars['fields'] ) ) {
				return;
			}
		}
		$cache_keys_mapping = array();
		foreach ( $order_ids as $order_id ) {
			$cache_keys_mapping[ $order_id ] = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'refunds' . $order_id;
		}
		$non_cached_ids = array();
		$cache_values = wc_cache_get_multiple( array_values( $cache_keys_mapping ), 'orders' );
		foreach ( $order_ids as $order_id ) {
			if ( false === $cache_values[ $cache_keys_mapping[ $order_id ] ] ) {
				$non_cached_ids[] = $order_id;
			}
		}
		if ( empty( $non_cached_ids ) ) {
			return;
		}

		$refunds = wc_get_orders(
			array(
				'type'   => 'shop_order_refund',
				'post_parent__in' => $non_cached_ids,
				'limit'  => - 1,
			)
		);
		$order_refunds = array_reduce(
			$refunds,
			function ( $order_refunds_array, WC_Order_Refund $refund ) {
				if ( ! isset( $order_refunds_array[ $refund->get_parent_id() ] ) ) {
					$order_refunds_array[ $refund->get_parent_id() ] = array();
				}
				$order_refunds_array[ $refund->get_parent_id() ][] = $refund;
				return $order_refunds_array;
			},
			array()
		);
		foreach ( $non_cached_ids as $order_id ) {
			$refunds = array();
			if ( isset( $order_refunds[ $order_id ] ) ) {
				$refunds = $order_refunds[ $order_id ];
			}
			wp_cache_set( $cache_keys_mapping[ $order_id ], $refunds, 'orders' );
		}
	}

	/**
	 * Prime following caches:
	 *  1. item-$order_item_id   For individual items.
	 *  2. order-items-$order-id For fetching items associated with an order.
	 *  3. order-item meta.
	 *
	 * @param array $order_ids  Order Ids to prime cache for.
	 * @param array $query_vars Query vars for the query.
	 */
	private function prime_order_item_caches_for_orders( $order_ids, $query_vars ) {
		global $wpdb;
		if ( isset( $query_vars['fields'] ) && 'all' !== $query_vars['fields'] ) {
			$line_items = array(
				'line_items',
				'shipping_lines',
				'fee_lines',
				'coupon_lines',
			);

			if ( is_array( $query_vars['fields'] ) && 0 === count( array_intersect( $line_items, $query_vars['fields'] ) ) ) {
				return;
			}
		}
		$cache_keys = array_map(
			function ( $order_id ) {
				return 'order-items-' . $order_id;
			},
			$order_ids
		);
		$cache_values = wc_cache_get_multiple( $cache_keys, 'orders' );
		$non_cached_ids = array();
		foreach ( $order_ids as $order_id ) {
			if ( false === $cache_values[ 'order-items-' . $order_id ] ) {
				$non_cached_ids[] = $order_id;
			}
		}
		if ( empty( $non_cached_ids ) ) {
			return;
		}

		$non_cached_ids       = esc_sql( $non_cached_ids );
		$non_cached_ids_string = implode( ',', $non_cached_ids );
		$order_items = $wpdb->get_results(
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			"SELECT order_item_type, order_item_id, order_id, order_item_name FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id in ( $non_cached_ids_string ) ORDER BY order_item_id;"
		);
		if ( empty( $order_items ) ) {
			return;
		}

		$order_items_for_all_orders = array_reduce(
			$order_items,
			function ( $order_items_collection, $order_item ) {
				if ( ! isset( $order_items_collection[ $order_item->order_id ] ) ) {
					$order_items_collection[ $order_item->order_id ] = array();
				}
				$order_items_collection[ $order_item->order_id ][] = $order_item;
				return $order_items_collection;
			}
		);
		foreach ( $order_items_for_all_orders as $order_id => $items ) {
			wp_cache_set( 'order-items-' . $order_id, $items, 'orders' );
		}
		foreach ( $order_items as $item ) {
			wp_cache_set( 'item-' . $item->order_item_id, $item, 'order-items' );
		}
		$order_item_ids = wp_list_pluck( $order_items, 'order_item_id' );
		update_meta_cache( 'order_item', $order_item_ids );
	}

	/**
	 * Prime cache for raw meta data for orders in bulk. Difference between this and WP built-in metadata is that this method also fetches `meta_id` field which we use and cache it.
	 *
	 * @param array $order_ids  Order Ids to prime cache for.
	 * @param array $query_vars Query vars for the query.
	 */
	private function prime_raw_meta_cache_for_orders( $order_ids, $query_vars ) {
		global $wpdb;

		if ( isset( $query_vars['fields'] ) && 'all' !== $query_vars['fields'] ) {
			if ( is_array( $query_vars['fields'] ) && ! in_array( 'meta_data', $query_vars['fields'] ) ) {
				return;
			}
		}

		$cache_keys_mapping = array();
		foreach ( $order_ids as $order_id ) {
			$cache_keys_mapping[ $order_id ] = WC_Order::generate_meta_cache_key( $order_id, 'orders' );
		}
		$cache_values = wc_cache_get_multiple( array_values( $cache_keys_mapping ), 'orders' );
		$non_cached_ids = array();
		foreach ( $order_ids as $order_id ) {
			if ( false === $cache_values[ $cache_keys_mapping[ $order_id ] ] ) {
				$non_cached_ids[] = $order_id;
			}
		}
		if ( empty( $non_cached_ids ) ) {
			return;
		}
		$order_ids     = esc_sql( $non_cached_ids );
		$order_ids_in  = "'" . implode( "', '", $order_ids ) . "'";
		$raw_meta_data_array = $wpdb->get_results(
		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			"SELECT post_id as object_id, meta_id, meta_key, meta_value
				FROM {$wpdb->postmeta}
				WHERE post_id IN ( $order_ids_in )
				ORDER BY post_id"
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		);
		$raw_meta_data_collection = array_reduce(
			$raw_meta_data_array,
			function ( $collection, $raw_meta_data ) {
				if ( ! isset( $collection[ $raw_meta_data->object_id ] ) ) {
					$collection[ $raw_meta_data->object_id ] = array();
				}
				$collection[ $raw_meta_data->object_id ][] = $raw_meta_data;
				return $collection;
			},
			array()
		);
		WC_Order::prime_raw_meta_data_cache( $raw_meta_data_collection, 'orders' );
	}

	/**
	 * Return the order type of a given item which belongs to WC_Order.
	 *
	 * @since  3.2.0
	 * @param  WC_Order $order Order Object.
	 * @param  int      $order_item_id Order item id.
	 * @return string Order Item type
	 */
	public function get_order_item_type( $order, $order_item_id ) {
		global $wpdb;
		return $wpdb->get_var( $wpdb->prepare( "SELECT DISTINCT order_item_type FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = %d and order_item_id = %d;", $order->get_id(), $order_item_id ) );
	}
}
data-stores/class-wc-order-item-data-store.php000064400000011673151336044770015347 0ustar00<?php
/**
 * Class WC_Order_Item_Data_Store file.
 *
 * @package WooCommerce\DataStores
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Order Item Data Store: Misc Order Item Data functions.
 *
 * @version  3.0.0
 */
class WC_Order_Item_Data_Store implements WC_Order_Item_Data_Store_Interface {

	/**
	 * Add an order item to an order.
	 *
	 * @since  3.0.0
	 * @param  int   $order_id Order ID.
	 * @param  array $item order_item_name and order_item_type.
	 * @return int Order Item ID
	 */
	public function add_order_item( $order_id, $item ) {
		global $wpdb;
		$wpdb->insert(
			$wpdb->prefix . 'woocommerce_order_items',
			array(
				'order_item_name' => $item['order_item_name'],
				'order_item_type' => $item['order_item_type'],
				'order_id'        => $order_id,
			),
			array(
				'%s',
				'%s',
				'%d',
			)
		);

		$item_id = absint( $wpdb->insert_id );

		$this->clear_caches( $item_id, $order_id );

		return $item_id;
	}

	/**
	 * Update an order item.
	 *
	 * @since  3.0.0
	 * @param  int   $item_id Item ID.
	 * @param  array $item order_item_name or order_item_type.
	 * @return boolean
	 */
	public function update_order_item( $item_id, $item ) {
		global $wpdb;
		$updated = $wpdb->update( $wpdb->prefix . 'woocommerce_order_items', $item, array( 'order_item_id' => $item_id ) );
		$this->clear_caches( $item_id, null );
		return $updated;
	}

	/**
	 * Delete an order item.
	 *
	 * @since  3.0.0
	 * @param  int $item_id Item ID.
	 */
	public function delete_order_item( $item_id ) {
		// Load the order ID before the deletion, since after, it won't exist in the database.
		$order_id = $this->get_order_id_by_order_item_id( $item_id );

		global $wpdb;
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d", $item_id ) );
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE order_item_id = %d", $item_id ) );

		$this->clear_caches( $item_id, $order_id );
	}

	/**
	 * Update term meta.
	 *
	 * @since  3.0.0
	 * @param  int    $item_id Item ID.
	 * @param  string $meta_key Meta key.
	 * @param  mixed  $meta_value Meta value.
	 * @param  string $prev_value (default: '').
	 * @return bool
	 */
	public function update_metadata( $item_id, $meta_key, $meta_value, $prev_value = '' ) {
		return update_metadata( 'order_item', $item_id, $meta_key, is_string( $meta_value ) ? wp_slash( $meta_value ) : $meta_value, $prev_value );
	}

	/**
	 * Add term meta.
	 *
	 * @since  3.0.0
	 * @param  int    $item_id Item ID.
	 * @param  string $meta_key Meta key.
	 * @param  mixed  $meta_value Meta value.
	 * @param  bool   $unique (default: false).
	 * @return int    New row ID or 0
	 */
	public function add_metadata( $item_id, $meta_key, $meta_value, $unique = false ) {
		return add_metadata( 'order_item', $item_id, wp_slash( $meta_key ), is_string( $meta_value ) ? wp_slash( $meta_value ) : $meta_value, $unique );
	}

	/**
	 * Delete term meta.
	 *
	 * @since  3.0.0
	 * @param  int    $item_id Item ID.
	 * @param  string $meta_key Meta key.
	 * @param  string $meta_value (default: '').
	 * @param  bool   $delete_all (default: false).
	 * @return bool
	 */
	public function delete_metadata( $item_id, $meta_key, $meta_value = '', $delete_all = false ) {
		return delete_metadata( 'order_item', $item_id, $meta_key, is_string( $meta_value ) ? wp_slash( $meta_value ) : $meta_value, $delete_all );
	}

	/**
	 * Get term meta.
	 *
	 * @since  3.0.0
	 * @param  int    $item_id Item ID.
	 * @param  string $key Meta key.
	 * @param  bool   $single (default: true).
	 * @return mixed
	 */
	public function get_metadata( $item_id, $key, $single = true ) {
		return get_metadata( 'order_item', $item_id, $key, $single );
	}

	/**
	 * Get order ID by order item ID.
	 *
	 * @since 3.0.0
	 * @param  int $item_id Item ID.
	 * @return int
	 */
	public function get_order_id_by_order_item_id( $item_id ) {
		global $wpdb;
		return (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT order_id FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d",
				$item_id
			)
		);
	}

	/**
	 * Get the order item type based on Item ID.
	 *
	 * @since 3.0.0
	 * @param int $item_id Item ID.
	 * @return string|null Order item type or null if no order item entry found.
	 */
	public function get_order_item_type( $item_id ) {
		global $wpdb;
		$order_item_type = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT order_item_type FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d LIMIT 1;",
				$item_id
			)
		);

		return $order_item_type;
	}

	/**
	 * Clear meta cache.
	 *
	 * @param int      $item_id Item ID.
	 * @param int|null $order_id Order ID. If not set, it will be loaded using the item ID.
	 */
	protected function clear_caches( $item_id, $order_id ) {
		wp_cache_delete( 'item-' . $item_id, 'order-items' );

		if ( ! $order_id ) {
			$order_id = $this->get_order_id_by_order_item_id( $item_id );
		}
		if ( $order_id ) {
			wp_cache_delete( 'order-items-' . $order_id, 'orders' );
		}
	}
}
data-stores/class-wc-order-item-product-data-store.php000064400000006210151336044770017014 0ustar00<?php
/**
 * Class WC_Order_Item_Product_Data_Store file.
 *
 * @package WooCommerce\DataStores
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Order Item Product Data Store
 *
 * @version  3.0.0
 */
class WC_Order_Item_Product_Data_Store extends Abstract_WC_Order_Item_Type_Data_Store implements WC_Object_Data_Store_Interface, WC_Order_Item_Type_Data_Store_Interface, WC_Order_Item_Product_Data_Store_Interface {

	/**
	 * Data stored in meta keys.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $internal_meta_keys = array( '_product_id', '_variation_id', '_qty', '_tax_class', '_line_subtotal', '_line_subtotal_tax', '_line_total', '_line_tax', '_line_tax_data' );

	/**
	 * Read/populate data properties specific to this order item.
	 *
	 * @since 3.0.0
	 * @param WC_Order_Item_Product $item Product order item object.
	 */
	public function read( &$item ) {
		parent::read( $item );
		$id = $item->get_id();
		$item->set_props(
			array(
				'product_id'   => get_metadata( 'order_item', $id, '_product_id', true ),
				'variation_id' => get_metadata( 'order_item', $id, '_variation_id', true ),
				'quantity'     => get_metadata( 'order_item', $id, '_qty', true ),
				'tax_class'    => get_metadata( 'order_item', $id, '_tax_class', true ),
				'subtotal'     => get_metadata( 'order_item', $id, '_line_subtotal', true ),
				'total'        => get_metadata( 'order_item', $id, '_line_total', true ),
				'taxes'        => get_metadata( 'order_item', $id, '_line_tax_data', true ),
			)
		);
		$item->set_object_read( true );
	}

	/**
	 * Saves an item's data to the database / item meta.
	 * Ran after both create and update, so $id will be set.
	 *
	 * @since 3.0.0
	 * @param WC_Order_Item_Product $item Product order item object.
	 */
	public function save_item_data( &$item ) {
		$id                = $item->get_id();
		$changes           = $item->get_changes();
		$meta_key_to_props = array(
			'_product_id'        => 'product_id',
			'_variation_id'      => 'variation_id',
			'_qty'               => 'quantity',
			'_tax_class'         => 'tax_class',
			'_line_subtotal'     => 'subtotal',
			'_line_subtotal_tax' => 'subtotal_tax',
			'_line_total'        => 'total',
			'_line_tax'          => 'total_tax',
			'_line_tax_data'     => 'taxes',
		);
		$props_to_update   = $this->get_props_to_update( $item, $meta_key_to_props, 'order_item' );

		foreach ( $props_to_update as $meta_key => $prop ) {
			update_metadata( 'order_item', $id, $meta_key, $item->{"get_$prop"}( 'edit' ) );
		}
	}

	/**
	 * Get a list of download IDs for a specific item from an order.
	 *
	 * @since 3.0.0
	 * @param WC_Order_Item_Product $item Product order item object.
	 * @param WC_Order              $order Order object.
	 * @return array
	 */
	public function get_download_ids( $item, $order ) {
		global $wpdb;
		return $wpdb->get_col(
			$wpdb->prepare(
				"SELECT download_id FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE user_email = %s AND order_key = %s AND product_id = %d ORDER BY permission_id",
				$order->get_billing_email(),
				$order->get_order_key(),
				$item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id()
			)
		);
	}
}
data-stores/class-wc-shipping-zone-data-store.php000064400000026274151336044770016075 0ustar00<?php
/**
 * Class WC_Shipping_Zone_Data_Store file.
 *
 * @package WooCommerce\DataStores
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Shipping Zone Data Store.
 *
 * @version  3.0.0
 */
class WC_Shipping_Zone_Data_Store extends WC_Data_Store_WP implements WC_Shipping_Zone_Data_Store_Interface, WC_Object_Data_Store_Interface {

	/**
	 * Method to create a new shipping zone.
	 *
	 * @since 3.0.0
	 * @param WC_Shipping_Zone $zone Shipping zone object.
	 */
	public function create( &$zone ) {
		global $wpdb;
		$wpdb->insert(
			$wpdb->prefix . 'woocommerce_shipping_zones',
			array(
				'zone_name'  => $zone->get_zone_name(),
				'zone_order' => $zone->get_zone_order(),
			)
		);
		$zone->set_id( $wpdb->insert_id );
		$zone->save_meta_data();
		$this->save_locations( $zone );
		$zone->apply_changes();
		WC_Cache_Helper::invalidate_cache_group( 'shipping_zones' );
		WC_Cache_Helper::get_transient_version( 'shipping', true );
	}

	/**
	 * Update zone in the database.
	 *
	 * @since 3.0.0
	 * @param WC_Shipping_Zone $zone Shipping zone object.
	 */
	public function update( &$zone ) {
		global $wpdb;
		if ( $zone->get_id() ) {
			$wpdb->update(
				$wpdb->prefix . 'woocommerce_shipping_zones',
				array(
					'zone_name'  => $zone->get_zone_name(),
					'zone_order' => $zone->get_zone_order(),
				),
				array( 'zone_id' => $zone->get_id() )
			);
		}
		$zone->save_meta_data();
		$this->save_locations( $zone );
		$zone->apply_changes();
		WC_Cache_Helper::invalidate_cache_group( 'shipping_zones' );
		WC_Cache_Helper::get_transient_version( 'shipping', true );
	}

	/**
	 * Method to read a shipping zone from the database.
	 *
	 * @since 3.0.0
	 * @param WC_Shipping_Zone $zone Shipping zone object.
	 * @throws Exception If invalid data store.
	 */
	public function read( &$zone ) {
		global $wpdb;

		// Zone 0 is used as a default if no other zones fit.
		if ( 0 === $zone->get_id() || '0' === $zone->get_id() ) {
			$this->read_zone_locations( $zone );
			$zone->set_zone_name( __( 'Locations not covered by your other zones', 'woocommerce' ) );
			$zone->read_meta_data();
			$zone->set_object_read( true );

			/**
			 * Indicate that the WooCommerce shipping zone has been loaded.
			 *
			 * @param WC_Shipping_Zone $zone The shipping zone that has been loaded.
			 */
			do_action( 'woocommerce_shipping_zone_loaded', $zone );
			return;
		}

		$zone_data = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT zone_name, zone_order FROM {$wpdb->prefix}woocommerce_shipping_zones WHERE zone_id = %d LIMIT 1",
				$zone->get_id()
			)
		);

		if ( ! $zone_data ) {
			throw new Exception( __( 'Invalid data store.', 'woocommerce' ) );
		}

		$zone->set_zone_name( $zone_data->zone_name );
		$zone->set_zone_order( $zone_data->zone_order );
		$this->read_zone_locations( $zone );
		$zone->read_meta_data();
		$zone->set_object_read( true );

		/** This action is documented in includes/datastores/class-wc-shipping-zone-data-store.php. */
		do_action( 'woocommerce_shipping_zone_loaded', $zone );
	}

	/**
	 * Deletes a shipping zone from the database.
	 *
	 * @since  3.0.0
	 * @param  WC_Shipping_Zone $zone Shipping zone object.
	 * @param  array            $args Array of args to pass to the delete method.
	 * @return void
	 */
	public function delete( &$zone, $args = array() ) {
		$zone_id = $zone->get_id();

		if ( $zone_id ) {
			global $wpdb;

			// Delete methods and their settings.
			$methods = $this->get_methods( $zone_id, false );

			if ( $methods ) {
				foreach ( $methods as $method ) {
					$this->delete_method( $method->instance_id );
				}
			}

			// Delete zone.
			$wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zone_locations', array( 'zone_id' => $zone_id ) );
			$wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zones', array( 'zone_id' => $zone_id ) );

			$zone->set_id( null );

			WC_Cache_Helper::invalidate_cache_group( 'shipping_zones' );
			WC_Cache_Helper::get_transient_version( 'shipping', true );

			do_action( 'woocommerce_delete_shipping_zone', $zone_id );
		}
	}

	/**
	 * Get a list of shipping methods for a specific zone.
	 *
	 * @since  3.0.0
	 * @param  int  $zone_id      Zone ID.
	 * @param  bool $enabled_only True to request enabled methods only.
	 * @return array               Array of objects containing method_id, method_order, instance_id, is_enabled
	 */
	public function get_methods( $zone_id, $enabled_only ) {
		global $wpdb;

		if ( $enabled_only ) {
			$raw_methods_sql = "SELECT method_id, method_order, instance_id, is_enabled FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE zone_id = %d AND is_enabled = 1";
		} else {
			$raw_methods_sql = "SELECT method_id, method_order, instance_id, is_enabled FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE zone_id = %d";
		}

		return $wpdb->get_results( $wpdb->prepare( $raw_methods_sql, $zone_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
	}

	/**
	 * Get count of methods for a zone.
	 *
	 * @since  3.0.0
	 * @param  int $zone_id Zone ID.
	 * @return int Method Count
	 */
	public function get_method_count( $zone_id ) {
		global $wpdb;
		return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE zone_id = %d", $zone_id ) );
	}

	/**
	 * Add a shipping method to a zone.
	 *
	 * @since  3.0.0
	 * @param  int    $zone_id Zone ID.
	 * @param  string $type    Method Type/ID.
	 * @param  int    $order   Method Order.
	 * @return int             Instance ID
	 */
	public function add_method( $zone_id, $type, $order ) {
		global $wpdb;
		$wpdb->insert(
			$wpdb->prefix . 'woocommerce_shipping_zone_methods',
			array(
				'method_id'    => $type,
				'zone_id'      => $zone_id,
				'method_order' => $order,
			),
			array(
				'%s',
				'%d',
				'%d',
			)
		);
		return $wpdb->insert_id;
	}

	/**
	 * Delete a method instance.
	 *
	 * @since 3.0.0
	 * @param int $instance_id Instance ID.
	 */
	public function delete_method( $instance_id ) {
		global $wpdb;

		$method = $this->get_method( $instance_id );

		if ( ! $method ) {
			return;
		}

		delete_option( 'woocommerce_' . $method->method_id . '_' . $instance_id . '_settings' );

		$wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zone_methods', array( 'instance_id' => $instance_id ) );

		do_action( 'woocommerce_delete_shipping_zone_method', $instance_id );
	}

	/**
	 * Get a shipping zone method instance.
	 *
	 * @since  3.0.0
	 * @param  int $instance_id Instance ID.
	 * @return object
	 */
	public function get_method( $instance_id ) {
		global $wpdb;
		return $wpdb->get_row( $wpdb->prepare( "SELECT zone_id, method_id, instance_id, method_order, is_enabled FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE instance_id = %d LIMIT 1;", $instance_id ) );
	}

	/**
	 * Find a matching zone ID for a given package.
	 *
	 * @since  3.0.0
	 * @param  object $package Package information.
	 * @return int
	 */
	public function get_zone_id_from_package( $package ) {
		global $wpdb;

		$country   = strtoupper( wc_clean( $package['destination']['country'] ) );
		$state     = strtoupper( wc_clean( $package['destination']['state'] ) );
		$continent = strtoupper( wc_clean( WC()->countries->get_continent_code_for_country( $country ) ) );
		$postcode  = wc_normalize_postcode( wc_clean( $package['destination']['postcode'] ) );

		// Work out criteria for our zone search.
		$criteria   = array();
		$criteria[] = $wpdb->prepare( "( ( location_type = 'country' AND location_code = %s )", $country );
		$criteria[] = $wpdb->prepare( "OR ( location_type = 'state' AND location_code = %s )", $country . ':' . $state );
		$criteria[] = $wpdb->prepare( "OR ( location_type = 'continent' AND location_code = %s )", $continent );
		$criteria[] = 'OR ( location_type IS NULL ) )';

		// Postcode range and wildcard matching.
		$postcode_locations = $wpdb->get_results( "SELECT zone_id, location_code FROM {$wpdb->prefix}woocommerce_shipping_zone_locations WHERE location_type = 'postcode';" );

		if ( $postcode_locations ) {
			$zone_ids_with_postcode_rules = array_map( 'absint', wp_list_pluck( $postcode_locations, 'zone_id' ) );
			$matches                      = wc_postcode_location_matcher( $postcode, $postcode_locations, 'zone_id', 'location_code', $country );
			$do_not_match                 = array_unique( array_diff( $zone_ids_with_postcode_rules, array_keys( $matches ) ) );

			if ( ! empty( $do_not_match ) ) {
				$criteria[] = 'AND zones.zone_id NOT IN (' . implode( ',', $do_not_match ) . ')';
			}
		}

		/**
		 * Get shipping zone criteria
		 *
		 * @since 3.6.6
		 * @param array $criteria Get zone criteria.
		 * @param array $package Package information.
		 * @param array $postcode_locations Postcode range and wildcard matching.
		 */
		$criteria = apply_filters( 'woocommerce_get_zone_criteria', $criteria, $package, $postcode_locations );

		// Get matching zones.
		return $wpdb->get_var(
			"SELECT zones.zone_id FROM {$wpdb->prefix}woocommerce_shipping_zones as zones
			LEFT OUTER JOIN {$wpdb->prefix}woocommerce_shipping_zone_locations as locations ON zones.zone_id = locations.zone_id AND location_type != 'postcode'
			WHERE " . implode( ' ', $criteria ) // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			. ' ORDER BY zone_order ASC, zones.zone_id ASC LIMIT 1'
		);
	}

	/**
	 * Return an ordered list of zones.
	 *
	 * @since 3.0.0
	 * @return array An array of objects containing a zone_id, zone_name, and zone_order.
	 */
	public function get_zones() {
		global $wpdb;
		return $wpdb->get_results( "SELECT zone_id, zone_name, zone_order FROM {$wpdb->prefix}woocommerce_shipping_zones order by zone_order ASC, zone_id ASC;" );
	}


	/**
	 * Return a zone ID from an instance ID.
	 *
	 * @since  3.0.0
	 * @param  int $id Instnace ID.
	 * @return int
	 */
	public function get_zone_id_by_instance_id( $id ) {
		global $wpdb;
		return $wpdb->get_var( $wpdb->prepare( "SELECT zone_id FROM {$wpdb->prefix}woocommerce_shipping_zone_methods as methods WHERE methods.instance_id = %d LIMIT 1;", $id ) );
	}

	/**
	 * Read location data from the database.
	 *
	 * @param WC_Shipping_Zone $zone Shipping zone object.
	 */
	private function read_zone_locations( &$zone ) {
		global $wpdb;

		$locations = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT location_code, location_type FROM {$wpdb->prefix}woocommerce_shipping_zone_locations WHERE zone_id = %d",
				$zone->get_id()
			)
		);

		if ( $locations ) {
			foreach ( $locations as $location ) {
				$zone->add_location( $location->location_code, $location->location_type );
			}
		}
	}

	/**
	 * Save locations to the DB.
	 * This function clears old locations, then re-inserts new if any changes are found.
	 *
	 * @since 3.0.0
	 *
	 * @param WC_Shipping_Zone $zone Shipping zone object.
	 *
	 * @return bool|void
	 */
	private function save_locations( &$zone ) {
		$changed_props = array_keys( $zone->get_changes() );
		if ( ! in_array( 'zone_locations', $changed_props, true ) ) {
			return false;
		}

		global $wpdb;
		$wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zone_locations', array( 'zone_id' => $zone->get_id() ) );

		foreach ( $zone->get_zone_locations( 'edit' ) as $location ) {
			$wpdb->insert(
				$wpdb->prefix . 'woocommerce_shipping_zone_locations',
				array(
					'zone_id'       => $zone->get_id(),
					'location_code' => $location->code,
					'location_type' => $location->type,
				)
			);
		}
	}
}
data-stores/class-wc-payment-token-data-store.php000064400000024657151336044770016101 0ustar00<?php
/**
 * Class WC_Payment_Token_Data_Store file.
 *
 * @package WooCommerce\DataStores
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Payment Token Data Store: Custom Table.
 *
 * @version  3.0.0
 */
class WC_Payment_Token_Data_Store extends WC_Data_Store_WP implements WC_Payment_Token_Data_Store_Interface, WC_Object_Data_Store_Interface {

	/**
	 * Meta type. Payment tokens are a new object type.
	 *
	 * @var string
	 */
	protected $meta_type = 'payment_token';

	/**
	 * If we have already saved our extra data, don't do automatic / default handling.
	 *
	 * @var bool
	 */
	protected $extra_data_saved = false;

	/**
	 * Create a new payment token in the database.
	 *
	 * @since 3.0.0
	 *
	 * @param WC_Payment_Token $token Payment token object.
	 *
	 * @throws Exception Throw exception if invalid or missing payment token fields.
	 */
	public function create( &$token ) {
		if ( false === $token->validate() ) {
			throw new Exception( __( 'Invalid or missing payment token fields.', 'woocommerce' ) );
		}

		global $wpdb;
		if ( ! $token->is_default() && $token->get_user_id() > 0 ) {
			$default_token = WC_Payment_Tokens::get_customer_default_token( $token->get_user_id() );
			if ( is_null( $default_token ) ) {
				$token->set_default( true );
			}
		}

		$payment_token_data = array(
			'gateway_id' => $token->get_gateway_id( 'edit' ),
			'token'      => $token->get_token( 'edit' ),
			'user_id'    => $token->get_user_id( 'edit' ),
			'type'       => $token->get_type( 'edit' ),
		);

		$wpdb->insert( $wpdb->prefix . 'woocommerce_payment_tokens', $payment_token_data );
		$token_id = $wpdb->insert_id;
		$token->set_id( $token_id );
		$this->save_extra_data( $token, true );
		$token->save_meta_data();
		$token->apply_changes();

		// Make sure all other tokens are not set to default.
		if ( $token->is_default() && $token->get_user_id() > 0 ) {
			WC_Payment_Tokens::set_users_default( $token->get_user_id(), $token_id );
		}

		do_action( 'woocommerce_new_payment_token', $token_id, $token );
	}

	/**
	 * Update a payment token.
	 *
	 * @since 3.0.0
	 *
	 * @param WC_Payment_Token $token Payment token object.
	 *
	 * @throws Exception Throw exception if invalid or missing payment token fields.
	 */
	public function update( &$token ) {
		if ( false === $token->validate() ) {
			throw new Exception( __( 'Invalid or missing payment token fields.', 'woocommerce' ) );
		}

		global $wpdb;

		$updated_props = array();
		$core_props    = array( 'gateway_id', 'token', 'user_id', 'type' );
		$changed_props = array_keys( $token->get_changes() );

		foreach ( $changed_props as $prop ) {
			if ( ! in_array( $prop, $core_props, true ) ) {
				continue;
			}
			$updated_props[]             = $prop;
			$payment_token_data[ $prop ] = $token->{'get_' . $prop}( 'edit' );
		}

		if ( ! empty( $payment_token_data ) ) {
			$wpdb->update(
				$wpdb->prefix . 'woocommerce_payment_tokens',
				$payment_token_data,
				array( 'token_id' => $token->get_id() )
			);
		}

		$updated_extra_props = $this->save_extra_data( $token );
		$updated_props       = array_merge( $updated_props, $updated_extra_props );
		$token->save_meta_data();
		$token->apply_changes();

		// Make sure all other tokens are not set to default.
		if ( $token->is_default() && $token->get_user_id() > 0 ) {
			WC_Payment_Tokens::set_users_default( $token->get_user_id(), $token->get_id() );
		}

		do_action( 'woocommerce_payment_token_object_updated_props', $token, $updated_props );
		do_action( 'woocommerce_payment_token_updated', $token->get_id() );
	}

	/**
	 * Remove a payment token from the database.
	 *
	 * @since 3.0.0
	 * @param WC_Payment_Token $token Payment token object.
	 * @param bool             $force_delete Unused param.
	 */
	public function delete( &$token, $force_delete = false ) {
		global $wpdb;
		$wpdb->delete( $wpdb->prefix . 'woocommerce_payment_tokens', array( 'token_id' => $token->get_id() ), array( '%d' ) );
		$wpdb->delete( $wpdb->prefix . 'woocommerce_payment_tokenmeta', array( 'payment_token_id' => $token->get_id() ), array( '%d' ) );
		do_action( 'woocommerce_payment_token_deleted', $token->get_id(), $token );
	}

	/**
	 * Read a token from the database.
	 *
	 * @since 3.0.0
	 *
	 * @param WC_Payment_Token $token Payment token object.
	 *
	 * @throws Exception Throw exception if invalid payment token.
	 */
	public function read( &$token ) {
		global $wpdb;

		$data = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT token, user_id, gateway_id, is_default FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE token_id = %d LIMIT 1",
				$token->get_id()
			)
		);

		if ( $data ) {
			$token->set_props(
				array(
					'token'      => $data->token,
					'user_id'    => $data->user_id,
					'gateway_id' => $data->gateway_id,
					'default'    => $data->is_default,
				)
			);
			$this->read_extra_data( $token );
			$token->read_meta_data();
			$token->set_object_read( true );
			do_action( 'woocommerce_payment_token_loaded', $token );
		} else {
			throw new Exception( __( 'Invalid payment token.', 'woocommerce' ) );
		}
	}

	/**
	 * Read extra data associated with the token (like last4 digits of a card for expiry dates).
	 *
	 * @param WC_Payment_Token $token Payment token object.
	 * @since 3.0.0
	 */
	protected function read_extra_data( &$token ) {
		foreach ( $token->get_extra_data_keys() as $key ) {
			$function = 'set_' . $key;
			if ( is_callable( array( $token, $function ) ) ) {
				$token->{$function}( get_metadata( 'payment_token', $token->get_id(), $key, true ) );
			}
		}
	}

	/**
	 * Saves extra token data as meta.
	 *
	 * @since 3.0.0
	 * @param WC_Payment_Token $token Payment token object.
	 * @param bool             $force By default, only changed props are updated. When this param is true all props are updated.
	 * @return array List of updated props.
	 */
	protected function save_extra_data( &$token, $force = false ) {
		if ( $this->extra_data_saved ) {
			return array();
		}

		$updated_props     = array();
		$extra_data_keys   = $token->get_extra_data_keys();
		$meta_key_to_props = ! empty( $extra_data_keys ) ? array_combine( $extra_data_keys, $extra_data_keys ) : array();
		$props_to_update   = $force ? $meta_key_to_props : $this->get_props_to_update( $token, $meta_key_to_props );

		foreach ( $extra_data_keys as $key ) {
			if ( ! array_key_exists( $key, $props_to_update ) ) {
				continue;
			}
			$function = 'get_' . $key;
			if ( is_callable( array( $token, $function ) ) ) {
				if ( update_metadata( 'payment_token', $token->get_id(), $key, $token->{$function}( 'edit' ) ) ) {
					$updated_props[] = $key;
				}
			}
		}

		return $updated_props;
	}

	/**
	 * Returns an array of objects (stdObject) matching specific token criteria.
	 * Accepts token_id, user_id, gateway_id, and type.
	 * Each object should contain the fields token_id, gateway_id, token, user_id, type, is_default.
	 *
	 * @since 3.0.0
	 * @param array $args List of accepted args: token_id, gateway_id, user_id, type.
	 * @return array
	 */
	public function get_tokens( $args ) {
		global $wpdb;
		$args = wp_parse_args(
			$args,
			array(
				'token_id'   => '',
				'user_id'    => '',
				'gateway_id' => '',
				'type'       => '',
			)
		);

		$sql   = "SELECT * FROM {$wpdb->prefix}woocommerce_payment_tokens";
		$where = array( '1=1' );

		if ( $args['token_id'] ) {
			$token_ids = array_map( 'absint', is_array( $args['token_id'] ) ? $args['token_id'] : array( $args['token_id'] ) );
			$where[]   = "token_id IN ('" . implode( "','", array_map( 'esc_sql', $token_ids ) ) . "')";
		}

		if ( $args['user_id'] ) {
			$where[] = $wpdb->prepare( 'user_id = %d', absint( $args['user_id'] ) );
		}

		if ( $args['gateway_id'] ) {
			$gateway_ids = array( $args['gateway_id'] );
		} else {
			$gateways    = WC_Payment_Gateways::instance();
			$gateway_ids = $gateways->get_payment_gateway_ids();
		}

		$page           = isset( $args['page'] ) ? absint( $args['page'] ) : 1;
		$posts_per_page = isset( $args['limit'] ) ? absint( $args['limit'] ) : get_option( 'posts_per_page' );

		$pgstrt = absint( ( $page - 1 ) * $posts_per_page ) . ', ';
		$limits = 'LIMIT ' . $pgstrt . $posts_per_page;

		$gateway_ids[] = '';
		$where[]       = "gateway_id IN ('" . implode( "','", array_map( 'esc_sql', $gateway_ids ) ) . "')";

		if ( $args['type'] ) {
			$where[] = $wpdb->prepare( 'type = %s', $args['type'] );
		}

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$token_results = $wpdb->get_results( $sql . ' WHERE ' . implode( ' AND ', $where ) . ' ' . $limits );

		return $token_results;
	}

	/**
	 * Returns an stdObject of a token for a user's default token.
	 * Should contain the fields token_id, gateway_id, token, user_id, type, is_default.
	 *
	 * @since 3.0.0
	 * @param int $user_id User ID.
	 * @return object
	 */
	public function get_users_default_token( $user_id ) {
		global $wpdb;
		return $wpdb->get_row(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE user_id = %d AND is_default = 1",
				$user_id
			)
		);
	}

	/**
	 * Returns an stdObject of a token.
	 * Should contain the fields token_id, gateway_id, token, user_id, type, is_default.
	 *
	 * @since 3.0.0
	 * @param int $token_id Token ID.
	 * @return object
	 */
	public function get_token_by_id( $token_id ) {
		global $wpdb;
		return $wpdb->get_row(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE token_id = %d",
				$token_id
			)
		);
	}

	/**
	 * Returns metadata for a specific payment token.
	 *
	 * @since 3.0.0
	 * @param int $token_id Token ID.
	 * @return array
	 */
	public function get_metadata( $token_id ) {
		return get_metadata( 'payment_token', $token_id );
	}

	/**
	 * Get a token's type by ID.
	 *
	 * @since 3.0.0
	 * @param int $token_id Token ID.
	 * @return string
	 */
	public function get_token_type_by_id( $token_id ) {
		global $wpdb;
		return $wpdb->get_var(
			$wpdb->prepare(
				"SELECT type FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE token_id = %d",
				$token_id
			)
		);
	}

	/**
	 * Update's a tokens default status in the database. Used for quickly
	 * looping through tokens and setting their statuses instead of creating a bunch
	 * of objects.
	 *
	 * @since 3.0.0
	 *
	 * @param int  $token_id Token ID.
	 * @param bool $status Whether given payment token is the default payment token or not.
	 *
	 * @return void
	 */
	public function set_default_status( $token_id, $status = true ) {
		global $wpdb;
		$wpdb->update(
			$wpdb->prefix . 'woocommerce_payment_tokens',
			array( 'is_default' => (int) $status ),
			array(
				'token_id' => $token_id,
			)
		);
	}

}
data-stores/class-wc-customer-data-store.php000064400000034643151336044770015143 0ustar00<?php
/**
 * Class WC_Customer_Data_Store file.
 *
 * @package WooCommerce\DataStores
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Customer Data Store.
 *
 * @version  3.0.0
 */
class WC_Customer_Data_Store extends WC_Data_Store_WP implements WC_Customer_Data_Store_Interface, WC_Object_Data_Store_Interface {

	/**
	 * Data stored in meta keys, but not considered "meta".
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $internal_meta_keys = array(
		'locale',
		'billing_postcode',
		'billing_city',
		'billing_address_1',
		'billing_address_2',
		'billing_state',
		'billing_country',
		'shipping_postcode',
		'shipping_city',
		'shipping_address_1',
		'shipping_address_2',
		'shipping_state',
		'shipping_country',
		'paying_customer',
		'last_update',
		'first_name',
		'last_name',
		'display_name',
		'show_admin_bar_front',
		'use_ssl',
		'admin_color',
		'rich_editing',
		'comment_shortcuts',
		'dismissed_wp_pointers',
		'show_welcome_panel',
		'session_tokens',
		'nickname',
		'description',
		'billing_first_name',
		'billing_last_name',
		'billing_company',
		'billing_phone',
		'billing_email',
		'shipping_first_name',
		'shipping_last_name',
		'shipping_company',
		'shipping_phone',
		'wptests_capabilities',
		'wptests_user_level',
		'syntax_highlighting',
		'_order_count',
		'_money_spent',
		'_last_order',
		'_woocommerce_tracks_anon_id',
	);

	/**
	 * Internal meta type used to store user data.
	 *
	 * @var string
	 */
	protected $meta_type = 'user';

	/**
	 * Callback to remove unwanted meta data.
	 *
	 * @param object $meta Meta object.
	 * @return bool
	 */
	protected function exclude_internal_meta_keys( $meta ) {
		global $wpdb;

		$table_prefix = $wpdb->prefix ? $wpdb->prefix : 'wp_';

		return ! in_array( $meta->meta_key, $this->internal_meta_keys, true )
			&& 0 !== strpos( $meta->meta_key, '_woocommerce_persistent_cart' )
			&& 0 !== strpos( $meta->meta_key, 'closedpostboxes_' )
			&& 0 !== strpos( $meta->meta_key, 'metaboxhidden_' )
			&& 0 !== strpos( $meta->meta_key, 'manageedit-' )
			&& ! strstr( $meta->meta_key, $table_prefix )
			&& 0 !== stripos( $meta->meta_key, 'wp_' );
	}

	/**
	 * Method to create a new customer in the database.
	 *
	 * @since 3.0.0
	 *
	 * @param WC_Customer $customer Customer object.
	 *
	 * @throws WC_Data_Exception If unable to create new customer.
	 */
	public function create( &$customer ) {
		$id = wc_create_new_customer( $customer->get_email(), $customer->get_username(), $customer->get_password() );

		if ( is_wp_error( $id ) ) {
			throw new WC_Data_Exception( $id->get_error_code(), $id->get_error_message() );
		}

		$customer->set_id( $id );
		$this->update_user_meta( $customer );

		// Prevent wp_update_user calls in the same request and customer trigger the 'Notice of Password Changed' email.
		$customer->set_password( '' );

		wp_update_user(
			apply_filters(
				'woocommerce_update_customer_args',
				array(
					'ID'           => $customer->get_id(),
					'role'         => $customer->get_role(),
					'display_name' => $customer->get_display_name(),
				),
				$customer
			)
		);
		$wp_user = new WP_User( $customer->get_id() );
		$customer->set_date_created( $wp_user->user_registered );
		$customer->set_date_modified( get_user_meta( $customer->get_id(), 'last_update', true ) );
		$customer->save_meta_data();
		$customer->apply_changes();
		do_action( 'woocommerce_new_customer', $customer->get_id(), $customer );
	}

	/**
	 * Method to read a customer object.
	 *
	 * @since 3.0.0
	 * @param WC_Customer $customer Customer object.
	 * @throws Exception If invalid customer.
	 */
	public function read( &$customer ) {
		$user_object = $customer->get_id() ? get_user_by( 'id', $customer->get_id() ) : false;

		// User object is required.
		if ( ! $user_object || empty( $user_object->ID ) ) {
			throw new Exception( __( 'Invalid customer.', 'woocommerce' ) );
		}

		$customer_id = $customer->get_id();

		// Load meta but exclude deprecated props and parent keys.
		$user_meta = array_diff_key(
			array_change_key_case( array_map( 'wc_flatten_meta_callback', get_user_meta( $customer_id ) ) ),
			array_flip( array( 'country', 'state', 'postcode', 'city', 'address', 'address_2', 'default', 'location' ) ),
			array_change_key_case( (array) $user_object->data )
		);

		$customer->set_props( $user_meta );
		$customer->set_props(
			array(
				'is_paying_customer' => get_user_meta( $customer_id, 'paying_customer', true ),
				'email'              => $user_object->user_email,
				'username'           => $user_object->user_login,
				'display_name'       => $user_object->display_name,
				'date_created'       => $user_object->user_registered, // Mysql string in local format.
				'date_modified'      => get_user_meta( $customer_id, 'last_update', true ),
				'role'               => ! empty( $user_object->roles[0] ) ? $user_object->roles[0] : 'customer',
			)
		);
		$customer->read_meta_data();
		$customer->set_object_read( true );
		do_action( 'woocommerce_customer_loaded', $customer );
	}

	/**
	 * Updates a customer in the database.
	 *
	 * @since 3.0.0
	 * @param WC_Customer $customer Customer object.
	 */
	public function update( &$customer ) {
		wp_update_user(
			apply_filters(
				'woocommerce_update_customer_args',
				array(
					'ID'           => $customer->get_id(),
					'user_email'   => $customer->get_email(),
					'display_name' => $customer->get_display_name(),
				),
				$customer
			)
		);

		// Only update password if a new one was set with set_password.
		if ( $customer->get_password() ) {
			wp_update_user(
				array(
					'ID'        => $customer->get_id(),
					'user_pass' => $customer->get_password(),
				)
			);
			$customer->set_password( '' );
		}

		$this->update_user_meta( $customer );
		$customer->set_date_modified( get_user_meta( $customer->get_id(), 'last_update', true ) );
		$customer->save_meta_data();
		$customer->apply_changes();
		do_action( 'woocommerce_update_customer', $customer->get_id(), $customer );
	}

	/**
	 * Deletes a customer from the database.
	 *
	 * @since 3.0.0
	 * @param WC_Customer $customer Customer object.
	 * @param array       $args Array of args to pass to the delete method.
	 */
	public function delete( &$customer, $args = array() ) {
		if ( ! $customer->get_id() ) {
			return;
		}

		$args = wp_parse_args(
			$args,
			array(
				'reassign' => 0,
			)
		);

		$id = $customer->get_id();
		wp_delete_user( $id, $args['reassign'] );

		do_action( 'woocommerce_delete_customer', $id );
	}

	/**
	 * Helper method that updates all the meta for a customer. Used for update & create.
	 *
	 * @since 3.0.0
	 * @param WC_Customer $customer Customer object.
	 */
	private function update_user_meta( $customer ) {
		$updated_props = array();
		$changed_props = $customer->get_changes();

		$meta_key_to_props = array(
			'paying_customer' => 'is_paying_customer',
			'first_name'      => 'first_name',
			'last_name'       => 'last_name',
		);

		foreach ( $meta_key_to_props as $meta_key => $prop ) {
			if ( ! array_key_exists( $prop, $changed_props ) ) {
				continue;
			}

			if ( update_user_meta( $customer->get_id(), $meta_key, $customer->{"get_$prop"}( 'edit' ) ) ) {
				$updated_props[] = $prop;
			}
		}

		$billing_address_props = array(
			'billing_first_name' => 'billing_first_name',
			'billing_last_name'  => 'billing_last_name',
			'billing_company'    => 'billing_company',
			'billing_address_1'  => 'billing_address_1',
			'billing_address_2'  => 'billing_address_2',
			'billing_city'       => 'billing_city',
			'billing_state'      => 'billing_state',
			'billing_postcode'   => 'billing_postcode',
			'billing_country'    => 'billing_country',
			'billing_email'      => 'billing_email',
			'billing_phone'      => 'billing_phone',
		);

		foreach ( $billing_address_props as $meta_key => $prop ) {
			$prop_key = substr( $prop, 8 );

			if ( ! isset( $changed_props['billing'] ) || ! array_key_exists( $prop_key, $changed_props['billing'] ) ) {
				continue;
			}

			if ( update_user_meta( $customer->get_id(), $meta_key, $customer->{"get_$prop"}( 'edit' ) ) ) {
				$updated_props[] = $prop;
			}
		}

		$shipping_address_props = array(
			'shipping_first_name' => 'shipping_first_name',
			'shipping_last_name'  => 'shipping_last_name',
			'shipping_company'    => 'shipping_company',
			'shipping_address_1'  => 'shipping_address_1',
			'shipping_address_2'  => 'shipping_address_2',
			'shipping_city'       => 'shipping_city',
			'shipping_state'      => 'shipping_state',
			'shipping_postcode'   => 'shipping_postcode',
			'shipping_country'    => 'shipping_country',
			'shipping_phone'      => 'shipping_phone',
		);

		foreach ( $shipping_address_props as $meta_key => $prop ) {
			$prop_key = substr( $prop, 9 );

			if ( ! isset( $changed_props['shipping'] ) || ! array_key_exists( $prop_key, $changed_props['shipping'] ) ) {
				continue;
			}

			if ( update_user_meta( $customer->get_id(), $meta_key, $customer->{"get_$prop"}( 'edit' ) ) ) {
				$updated_props[] = $prop;
			}
		}

		do_action( 'woocommerce_customer_object_updated_props', $customer, $updated_props );
	}

	/**
	 * Gets the customers last order.
	 *
	 * @since 3.0.0
	 * @param WC_Customer $customer Customer object.
	 * @return WC_Order|false
	 */
	public function get_last_order( &$customer ) {
		$last_order = apply_filters(
			'woocommerce_customer_get_last_order',
			get_user_meta( $customer->get_id(), '_last_order', true ),
			$customer
		);

		if ( '' === $last_order ) {
			global $wpdb;

			$last_order = $wpdb->get_var(
				// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
				"SELECT posts.ID
				FROM $wpdb->posts AS posts
				LEFT JOIN {$wpdb->postmeta} AS meta on posts.ID = meta.post_id
				WHERE meta.meta_key = '_customer_user'
				AND   meta.meta_value = '" . esc_sql( $customer->get_id() ) . "'
				AND   posts.post_type = 'shop_order'
				AND   posts.post_status IN ( '" . implode( "','", array_map( 'esc_sql', array_keys( wc_get_order_statuses() ) ) ) . "' )
				ORDER BY posts.ID DESC"
				// phpcs:enable
			);
			update_user_meta( $customer->get_id(), '_last_order', $last_order );
		}

		if ( ! $last_order ) {
			return false;
		}

		return wc_get_order( absint( $last_order ) );
	}

	/**
	 * Return the number of orders this customer has.
	 *
	 * @since 3.0.0
	 * @param WC_Customer $customer Customer object.
	 * @return integer
	 */
	public function get_order_count( &$customer ) {
		$count = apply_filters(
			'woocommerce_customer_get_order_count',
			get_user_meta( $customer->get_id(), '_order_count', true ),
			$customer
		);

		if ( '' === $count ) {
			global $wpdb;

			$count = $wpdb->get_var(
				// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
				"SELECT COUNT(*)
				FROM $wpdb->posts as posts
				LEFT JOIN {$wpdb->postmeta} AS meta ON posts.ID = meta.post_id
				WHERE   meta.meta_key = '_customer_user'
				AND     posts.post_type = 'shop_order'
				AND     posts.post_status IN ( '" . implode( "','", array_map( 'esc_sql', array_keys( wc_get_order_statuses() ) ) ) . "' )
				AND     meta_value = '" . esc_sql( $customer->get_id() ) . "'"
				// phpcs:enable
			);
			update_user_meta( $customer->get_id(), '_order_count', $count );
		}

		return absint( $count );
	}

	/**
	 * Return how much money this customer has spent.
	 *
	 * @since 3.0.0
	 * @param WC_Customer $customer Customer object.
	 * @return float
	 */
	public function get_total_spent( &$customer ) {
		$spent = apply_filters(
			'woocommerce_customer_get_total_spent',
			get_user_meta( $customer->get_id(), '_money_spent', true ),
			$customer
		);

		if ( '' === $spent ) {
			global $wpdb;

			$statuses = array_map( 'esc_sql', wc_get_is_paid_statuses() );
			$spent    = $wpdb->get_var(
				// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
				apply_filters(
					'woocommerce_customer_get_total_spent_query',
					"SELECT SUM(meta2.meta_value)
					FROM $wpdb->posts as posts
					LEFT JOIN {$wpdb->postmeta} AS meta ON posts.ID = meta.post_id
					LEFT JOIN {$wpdb->postmeta} AS meta2 ON posts.ID = meta2.post_id
					WHERE   meta.meta_key       = '_customer_user'
					AND     meta.meta_value     = '" . esc_sql( $customer->get_id() ) . "'
					AND     posts.post_type     = 'shop_order'
					AND     posts.post_status   IN ( 'wc-" . implode( "','wc-", $statuses ) . "' )
					AND     meta2.meta_key      = '_order_total'",
					$customer
				)
				// phpcs:enable
			);

			if ( ! $spent ) {
				$spent = 0;
			}
			update_user_meta( $customer->get_id(), '_money_spent', $spent );
		}

		return wc_format_decimal( $spent, 2 );
	}

	/**
	 * Search customers and return customer IDs.
	 *
	 * @param  string     $term Search term.
	 * @param  int|string $limit Limit search results.
	 * @since 3.0.7
	 *
	 * @return array
	 */
	public function search_customers( $term, $limit = '' ) {
		$results = apply_filters( 'woocommerce_customer_pre_search_customers', false, $term, $limit );
		if ( is_array( $results ) ) {
			return $results;
		}

		$query = new WP_User_Query(
			apply_filters(
				'woocommerce_customer_search_customers',
				array(
					'search'         => '*' . esc_attr( $term ) . '*',
					'search_columns' => array( 'user_login', 'user_url', 'user_email', 'user_nicename', 'display_name' ),
					'fields'         => 'ID',
					'number'         => $limit,
				),
				$term,
				$limit,
				'main_query'
			)
		);

		$query2 = new WP_User_Query(
			apply_filters(
				'woocommerce_customer_search_customers',
				array(
					'fields'     => 'ID',
					'number'     => $limit,
					'meta_query' => array(
						'relation' => 'OR',
						array(
							'key'     => 'first_name',
							'value'   => $term,
							'compare' => 'LIKE',
						),
						array(
							'key'     => 'last_name',
							'value'   => $term,
							'compare' => 'LIKE',
						),
					),
				),
				$term,
				$limit,
				'meta_query'
			)
		);

		$results = wp_parse_id_list( array_merge( (array) $query->get_results(), (array) $query2->get_results() ) );

		if ( $limit && count( $results ) > $limit ) {
			$results = array_slice( $results, 0, $limit );
		}

		return $results;
	}

	/**
	 * Get all user ids who have `billing_email` set to any of the email passed in array.
	 *
	 * @param array $emails List of emails to check against.
	 *
	 * @return array
	 */
	public function get_user_ids_for_billing_email( $emails ) {
		$emails = array_unique( array_map( 'strtolower', array_map( 'sanitize_email', $emails ) ) );
		$users_query = new WP_User_Query(
			array(
				'fields'     => 'ID',
				'meta_query' => array(
					array(
						'key'     => 'billing_email',
						'value'   => $emails,
						'compare' => 'IN',
					),
				),
			)
		);
		return array_unique( $users_query->get_results() );
	}
}
data-stores/abstract-wc-order-data-store-cpt.php000064400000034234151336044770015673 0ustar00<?php
/**
 * Abstract_WC_Order_Data_Store_CPT class file.
 *
 * @package WooCommerce\Classes
 */

use Automattic\Jetpack\Constants;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Abstract Order Data Store: Stored in CPT.
 *
 * @version  3.0.0
 */
abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Data_Store_Interface, WC_Abstract_Order_Data_Store_Interface {

	/**
	 * Internal meta type used to store order data.
	 *
	 * @var string
	 */
	protected $meta_type = 'post';

	/**
	 * Data stored in meta keys, but not considered "meta" for an order.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $internal_meta_keys = array(
		'_order_currency',
		'_cart_discount',
		'_cart_discount_tax',
		'_order_shipping',
		'_order_shipping_tax',
		'_order_tax',
		'_order_total',
		'_order_version',
		'_prices_include_tax',
		'_payment_tokens',
	);

	/*
	|--------------------------------------------------------------------------
	| CRUD Methods
	|--------------------------------------------------------------------------
	*/

	/**
	 * Method to create a new order in the database.
	 *
	 * @param WC_Order $order Order object.
	 */
	public function create( &$order ) {
		$order->set_version( Constants::get_constant( 'WC_VERSION' ) );
		$order->set_currency( $order->get_currency() ? $order->get_currency() : get_woocommerce_currency() );
		if ( ! $order->get_date_created( 'edit' ) ) {
			$order->set_date_created( time() );
		}

		$id = wp_insert_post(
			apply_filters(
				'woocommerce_new_order_data',
				array(
					'post_date'     => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getOffsetTimestamp() ),
					'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
					'post_type'     => $order->get_type( 'edit' ),
					'post_status'   => $this->get_post_status( $order ),
					'ping_status'   => 'closed',
					'post_author'   => 1,
					'post_title'    => $this->get_post_title(),
					'post_password' => $this->get_order_key( $order ),
					'post_parent'   => $order->get_parent_id( 'edit' ),
					'post_excerpt'  => $this->get_post_excerpt( $order ),
				)
			),
			true
		);

		if ( $id && ! is_wp_error( $id ) ) {
			$order->set_id( $id );
			$this->update_post_meta( $order );
			$order->save_meta_data();
			$order->apply_changes();
			$this->clear_caches( $order );
		}
	}

	/**
	 * Method to read an order from the database.
	 *
	 * @param WC_Order $order Order object.
	 *
	 * @throws Exception If passed order is invalid.
	 */
	public function read( &$order ) {
		$order->set_defaults();
		$post_object = get_post( $order->get_id() );
		if ( ! $order->get_id() || ! $post_object || ! in_array( $post_object->post_type, wc_get_order_types(), true ) ) {
			throw new Exception( __( 'Invalid order.', 'woocommerce' ) );
		}

		$order->set_props(
			array(
				'parent_id'     => $post_object->post_parent,
				'date_created'  => $this->string_to_timestamp( $post_object->post_date_gmt ),
				'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ),
				'status'        => $post_object->post_status,
			)
		);

		$this->read_order_data( $order, $post_object );
		$order->read_meta_data();
		$order->set_object_read( true );

		/**
		 * In older versions, discounts may have been stored differently.
		 * Update them now so if the object is saved, the correct values are
		 * stored. @todo When meta is flattened, handle this during migration.
		 */
		if ( version_compare( $order->get_version( 'edit' ), '2.3.7', '<' ) && $order->get_prices_include_tax( 'edit' ) ) {
			$order->set_discount_total( (float) get_post_meta( $order->get_id(), '_cart_discount', true ) - (float) get_post_meta( $order->get_id(), '_cart_discount_tax', true ) );
		}
	}

	/**
	 * Method to update an order in the database.
	 *
	 * @param WC_Order $order Order object.
	 */
	public function update( &$order ) {
		$order->save_meta_data();
		$order->set_version( Constants::get_constant( 'WC_VERSION' ) );

		if ( null === $order->get_date_created( 'edit' ) ) {
			$order->set_date_created( time() );
		}

		$changes = $order->get_changes();

		// Only update the post when the post data changes.
		if ( array_intersect( array( 'date_created', 'date_modified', 'status', 'parent_id', 'post_excerpt' ), array_keys( $changes ) ) ) {
			$post_data = array(
				'post_date'         => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getOffsetTimestamp() ),
				'post_date_gmt'     => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
				'post_status'       => $this->get_post_status( $order ),
				'post_parent'       => $order->get_parent_id(),
				'post_excerpt'      => $this->get_post_excerpt( $order ),
				'post_modified'     => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $order->get_date_modified( 'edit' )->getOffsetTimestamp() ) : current_time( 'mysql' ),
				'post_modified_gmt' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $order->get_date_modified( 'edit' )->getTimestamp() ) : current_time( 'mysql', 1 ),
			);

			/**
			 * When updating this object, to prevent infinite loops, use $wpdb
			 * to update data, since wp_update_post spawns more calls to the
			 * save_post action.
			 *
			 * This ensures hooks are fired by either WP itself (admin screen save),
			 * or an update purely from CRUD.
			 */
			if ( doing_action( 'save_post' ) ) {
				$GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $order->get_id() ) );
				clean_post_cache( $order->get_id() );
			} else {
				wp_update_post( array_merge( array( 'ID' => $order->get_id() ), $post_data ) );
			}
			$order->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook.
		}
		$this->update_post_meta( $order );
		$order->apply_changes();
		$this->clear_caches( $order );
	}

	/**
	 * Method to delete an order from the database.
	 *
	 * @param WC_Order $order Order object.
	 * @param array    $args Array of args to pass to the delete method.
	 *
	 * @return void
	 */
	public function delete( &$order, $args = array() ) {
		$id   = $order->get_id();
		$args = wp_parse_args(
			$args,
			array(
				'force_delete' => false,
			)
		);

		if ( ! $id ) {
			return;
		}

		if ( $args['force_delete'] ) {
			wp_delete_post( $id );
			$order->set_id( 0 );
			do_action( 'woocommerce_delete_order', $id );
		} else {
			wp_trash_post( $id );
			$order->set_status( 'trash' );
			do_action( 'woocommerce_trash_order', $id );
		}
	}

	/*
	|--------------------------------------------------------------------------
	| Additional Methods
	|--------------------------------------------------------------------------
	*/

	/**
	 * Get the status to save to the post object.
	 *
	 * Plugins extending the order classes can override this to change the stored status/add prefixes etc.
	 *
	 * @since 3.6.0
	 * @param  WC_order $order Order object.
	 * @return string
	 */
	protected function get_post_status( $order ) {
		$order_status = $order->get_status( 'edit' );

		if ( ! $order_status ) {
			$order_status = apply_filters( 'woocommerce_default_order_status', 'pending' );
		}

		$post_status    = $order_status;
		$valid_statuses = get_post_stati();

		// Add a wc- prefix to the status, but exclude some core statuses which should not be prefixed.
		// @todo In the future this should only happen based on `wc_is_order_status`, but in order to
		// preserve back-compatibility this happens to all statuses except a select few. A doing_it_wrong
		// Notice will be needed here, followed by future removal.
		if ( ! in_array( $post_status, array( 'auto-draft', 'draft', 'trash' ), true ) && in_array( 'wc-' . $post_status, $valid_statuses, true ) ) {
			$post_status = 'wc-' . $post_status;
		}

		return $post_status;
	}

	/**
	 * Excerpt for post.
	 *
	 * @param  WC_order $order Order object.
	 * @return string
	 */
	protected function get_post_excerpt( $order ) {
		return '';
	}

	/**
	 * Get a title for the new post type.
	 *
	 * @return string
	 */
	protected function get_post_title() {
		// @codingStandardsIgnoreStart
		/* translators: %s: Order date */
		return sprintf( __( 'Order &ndash; %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Order date parsed by strftime', 'woocommerce' ) ) );
		// @codingStandardsIgnoreEnd
	}

	/**
	 * Get order key.
	 *
	 * @since 4.3.0
	 * @param WC_order $order Order object.
	 * @return string
	 */
	protected function get_order_key( $order ) {
		return wc_generate_order_key();
	}

	/**
	 * Read order data. Can be overridden by child classes to load other props.
	 *
	 * @param WC_Order $order Order object.
	 * @param object   $post_object Post object.
	 * @since 3.0.0
	 */
	protected function read_order_data( &$order, $post_object ) {
		$id = $order->get_id();

		$order->set_props(
			array(
				'currency'           => get_post_meta( $id, '_order_currency', true ),
				'discount_total'     => get_post_meta( $id, '_cart_discount', true ),
				'discount_tax'       => get_post_meta( $id, '_cart_discount_tax', true ),
				'shipping_total'     => get_post_meta( $id, '_order_shipping', true ),
				'shipping_tax'       => get_post_meta( $id, '_order_shipping_tax', true ),
				'cart_tax'           => get_post_meta( $id, '_order_tax', true ),
				'total'              => get_post_meta( $id, '_order_total', true ),
				'version'            => get_post_meta( $id, '_order_version', true ),
				'prices_include_tax' => metadata_exists( 'post', $id, '_prices_include_tax' ) ? 'yes' === get_post_meta( $id, '_prices_include_tax', true ) : 'yes' === get_option( 'woocommerce_prices_include_tax' ),
			)
		);

		// Gets extra data associated with the order if needed.
		foreach ( $order->get_extra_data_keys() as $key ) {
			$function = 'set_' . $key;
			if ( is_callable( array( $order, $function ) ) ) {
				$order->{$function}( get_post_meta( $order->get_id(), '_' . $key, true ) );
			}
		}
	}

	/**
	 * Helper method that updates all the post meta for an order based on it's settings in the WC_Order class.
	 *
	 * @param WC_Order $order Order object.
	 * @since 3.0.0
	 */
	protected function update_post_meta( &$order ) {
		$updated_props     = array();
		$meta_key_to_props = array(
			'_order_currency'     => 'currency',
			'_cart_discount'      => 'discount_total',
			'_cart_discount_tax'  => 'discount_tax',
			'_order_shipping'     => 'shipping_total',
			'_order_shipping_tax' => 'shipping_tax',
			'_order_tax'          => 'cart_tax',
			'_order_total'        => 'total',
			'_order_version'      => 'version',
			'_prices_include_tax' => 'prices_include_tax',
		);

		$props_to_update = $this->get_props_to_update( $order, $meta_key_to_props );

		foreach ( $props_to_update as $meta_key => $prop ) {
			$value = $order->{"get_$prop"}( 'edit' );
			$value = is_string( $value ) ? wp_slash( $value ) : $value;

			if ( 'prices_include_tax' === $prop ) {
				$value = $value ? 'yes' : 'no';
			}

			$updated = $this->update_or_delete_post_meta( $order, $meta_key, $value );

			if ( $updated ) {
				$updated_props[] = $prop;
			}
		}

		do_action( 'woocommerce_order_object_updated_props', $order, $updated_props );
	}

	/**
	 * Clear any caches.
	 *
	 * @param WC_Order $order Order object.
	 * @since 3.0.0
	 */
	protected function clear_caches( &$order ) {
		clean_post_cache( $order->get_id() );
		wc_delete_shop_order_transients( $order );
		wp_cache_delete( 'order-items-' . $order->get_id(), 'orders' );
	}

	/**
	 * Read order items of a specific type from the database for this order.
	 *
	 * @param  WC_Order $order Order object.
	 * @param  string   $type Order item type.
	 * @return array
	 */
	public function read_items( $order, $type ) {
		global $wpdb;

		// Get from cache if available.
		$items = 0 < $order->get_id() ? wp_cache_get( 'order-items-' . $order->get_id(), 'orders' ) : false;

		if ( false === $items ) {
			$items = $wpdb->get_results(
				$wpdb->prepare( "SELECT order_item_type, order_item_id, order_id, order_item_name FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = %d ORDER BY order_item_id;", $order->get_id() )
			);
			foreach ( $items as $item ) {
				wp_cache_set( 'item-' . $item->order_item_id, $item, 'order-items' );
			}
			if ( 0 < $order->get_id() ) {
				wp_cache_set( 'order-items-' . $order->get_id(), $items, 'orders' );
			}
		}

		$items = wp_list_filter( $items, array( 'order_item_type' => $type ) );

		if ( ! empty( $items ) ) {
			$items = array_map( array( 'WC_Order_Factory', 'get_order_item' ), array_combine( wp_list_pluck( $items, 'order_item_id' ), $items ) );
		} else {
			$items = array();
		}

		return $items;
	}

	/**
	 * Remove all line items (products, coupons, shipping, taxes) from the order.
	 *
	 * @param WC_Order $order Order object.
	 * @param string   $type Order item type. Default null.
	 */
	public function delete_items( $order, $type = null ) {
		global $wpdb;
		if ( ! empty( $type ) ) {
			$wpdb->query( $wpdb->prepare( "DELETE FROM itemmeta USING {$wpdb->prefix}woocommerce_order_itemmeta itemmeta INNER JOIN {$wpdb->prefix}woocommerce_order_items items WHERE itemmeta.order_item_id = items.order_item_id AND items.order_id = %d AND items.order_item_type = %s", $order->get_id(), $type ) );
			$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = %d AND order_item_type = %s", $order->get_id(), $type ) );
		} else {
			$wpdb->query( $wpdb->prepare( "DELETE FROM itemmeta USING {$wpdb->prefix}woocommerce_order_itemmeta itemmeta INNER JOIN {$wpdb->prefix}woocommerce_order_items items WHERE itemmeta.order_item_id = items.order_item_id and items.order_id = %d", $order->get_id() ) );
			$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = %d", $order->get_id() ) );
		}
		$this->clear_caches( $order );
	}

	/**
	 * Get token ids for an order.
	 *
	 * @param WC_Order $order Order object.
	 * @return array
	 */
	public function get_payment_token_ids( $order ) {
		$token_ids = array_filter( (array) get_post_meta( $order->get_id(), '_payment_tokens', true ) );
		return $token_ids;
	}

	/**
	 * Update token ids for an order.
	 *
	 * @param WC_Order $order Order object.
	 * @param array    $token_ids Payment token ids.
	 */
	public function update_payment_token_ids( $order, $token_ids ) {
		update_post_meta( $order->get_id(), '_payment_tokens', $token_ids );
	}
}
data-stores/class-wc-data-store-wp.php000064400000047344151336044770013732 0ustar00<?php
/**
 * Shared logic for WP based data.
 * Contains functions like meta handling for all default data stores.
 * Your own data store doesn't need to use WC_Data_Store_WP -- you can write
 * your own meta handling functions.
 *
 * @version 3.0.0
 * @package WooCommerce\Classes
 */

defined( 'ABSPATH' ) || exit;

/**
 * WC_Data_Store_WP class.
 */
class WC_Data_Store_WP {

	/**
	 * Meta type. This should match up with
	 * the types available at https://developer.wordpress.org/reference/functions/add_metadata/.
	 * WP defines 'post', 'user', 'comment', and 'term'.
	 *
	 * @var string
	 */
	protected $meta_type = 'post';

	/**
	 * This only needs set if you are using a custom metadata type (for example payment tokens.
	 * This should be the name of the field your table uses for associating meta with objects.
	 * For example, in payment_tokenmeta, this would be payment_token_id.
	 *
	 * @var string
	 */
	protected $object_id_field_for_meta = '';

	/**
	 * Data stored in meta keys, but not considered "meta" for an object.
	 *
	 * @since 3.0.0
	 *
	 * @var array
	 */
	protected $internal_meta_keys = array();

	/**
	 * Meta data which should exist in the DB, even if empty.
	 *
	 * @since 3.6.0
	 *
	 * @var array
	 */
	protected $must_exist_meta_keys = array();

	/**
	 * Get and store terms from a taxonomy.
	 *
	 * @since  3.0.0
	 * @param  WC_Data|integer $object WC_Data object or object ID.
	 * @param  string          $taxonomy Taxonomy name e.g. product_cat.
	 * @return array of terms
	 */
	protected function get_term_ids( $object, $taxonomy ) {
		if ( is_numeric( $object ) ) {
			$object_id = $object;
		} else {
			$object_id = $object->get_id();
		}
		$terms = get_the_terms( $object_id, $taxonomy );
		if ( false === $terms || is_wp_error( $terms ) ) {
			return array();
		}
		return wp_list_pluck( $terms, 'term_id' );
	}

	/**
	 * Returns an array of meta for an object.
	 *
	 * @since  3.0.0
	 * @param  WC_Data $object WC_Data object.
	 * @return array
	 */
	public function read_meta( &$object ) {
		global $wpdb;
		$db_info       = $this->get_db_info();
		$raw_meta_data = $wpdb->get_results(
			$wpdb->prepare(
				// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				"SELECT {$db_info['meta_id_field']} as meta_id, meta_key, meta_value
				FROM {$db_info['table']}
				WHERE {$db_info['object_id_field']} = %d
				ORDER BY {$db_info['meta_id_field']}",
				// phpcs:enable
				$object->get_id()
			)
		);
		return $this->filter_raw_meta_data( $object, $raw_meta_data );
	}

	/**
	 * Helper method to filter internal meta keys from all meta data rows for the object.
	 *
	 * @since 4.7.0
	 *
	 * @param WC_Data $object        WC_Data object.
	 * @param array   $raw_meta_data Array of std object of meta data to be filtered.
	 *
	 * @return mixed|void
	 */
	public function filter_raw_meta_data( &$object, $raw_meta_data ) {
		$this->internal_meta_keys = array_merge( array_map( array( $this, 'prefix_key' ), $object->get_data_keys() ), $this->internal_meta_keys );
		$meta_data                = array_filter( $raw_meta_data, array( $this, 'exclude_internal_meta_keys' ) );
		return apply_filters( "woocommerce_data_store_wp_{$this->meta_type}_read_meta", $meta_data, $object, $this );
	}

	/**
	 * Deletes meta based on meta ID.
	 *
	 * @since  3.0.0
	 * @param  WC_Data  $object WC_Data object.
	 * @param  stdClass $meta (containing at least ->id).
	 */
	public function delete_meta( &$object, $meta ) {
		delete_metadata_by_mid( $this->meta_type, $meta->id );
	}

	/**
	 * Add new piece of meta.
	 *
	 * @since  3.0.0
	 * @param  WC_Data  $object WC_Data object.
	 * @param  stdClass $meta (containing ->key and ->value).
	 * @return int meta ID
	 */
	public function add_meta( &$object, $meta ) {
		return add_metadata( $this->meta_type, $object->get_id(), wp_slash( $meta->key ), is_string( $meta->value ) ? wp_slash( $meta->value ) : $meta->value, false );
	}

	/**
	 * Update meta.
	 *
	 * @since  3.0.0
	 * @param  WC_Data  $object WC_Data object.
	 * @param  stdClass $meta (containing ->id, ->key and ->value).
	 */
	public function update_meta( &$object, $meta ) {
		update_metadata_by_mid( $this->meta_type, $meta->id, $meta->value, $meta->key );
	}

	/**
	 * Table structure is slightly different between meta types, this function will return what we need to know.
	 *
	 * @since  3.0.0
	 * @return array Array elements: table, object_id_field, meta_id_field
	 */
	protected function get_db_info() {
		global $wpdb;

		$meta_id_field = 'meta_id'; // for some reason users calls this umeta_id so we need to track this as well.
		$table         = $wpdb->prefix;

		// If we are dealing with a type of metadata that is not a core type, the table should be prefixed.
		if ( ! in_array( $this->meta_type, array( 'post', 'user', 'comment', 'term' ), true ) ) {
			$table .= 'woocommerce_';
		}

		$table          .= $this->meta_type . 'meta';
		$object_id_field = $this->meta_type . '_id';

		// Figure out our field names.
		if ( 'user' === $this->meta_type ) {
			$meta_id_field = 'umeta_id';
			$table         = $wpdb->usermeta;
		}

		if ( ! empty( $this->object_id_field_for_meta ) ) {
			$object_id_field = $this->object_id_field_for_meta;
		}

		return array(
			'table'           => $table,
			'object_id_field' => $object_id_field,
			'meta_id_field'   => $meta_id_field,
		);
	}

	/**
	 * Internal meta keys we don't want exposed as part of meta_data. This is in
	 * addition to all data props with _ prefix.
	 *
	 * @since 2.6.0
	 *
	 * @param string $key Prefix to be added to meta keys.
	 * @return string
	 */
	protected function prefix_key( $key ) {
		return '_' === substr( $key, 0, 1 ) ? $key : '_' . $key;
	}

	/**
	 * Callback to remove unwanted meta data.
	 *
	 * @param object $meta Meta object to check if it should be excluded or not.
	 * @return bool
	 */
	protected function exclude_internal_meta_keys( $meta ) {
		return ! in_array( $meta->meta_key, $this->internal_meta_keys, true ) && 0 !== stripos( $meta->meta_key, 'wp_' );
	}

	/**
	 * Gets a list of props and meta keys that need updated based on change state
	 * or if they are present in the database or not.
	 *
	 * @param  WC_Data $object              The WP_Data object (WC_Coupon for coupons, etc).
	 * @param  array   $meta_key_to_props   A mapping of meta keys => prop names.
	 * @param  string  $meta_type           The internal WP meta type (post, user, etc).
	 * @return array                        A mapping of meta keys => prop names, filtered by ones that should be updated.
	 */
	protected function get_props_to_update( $object, $meta_key_to_props, $meta_type = 'post' ) {
		$props_to_update = array();
		$changed_props   = $object->get_changes();

		// Props should be updated if they are a part of the $changed array or don't exist yet.
		foreach ( $meta_key_to_props as $meta_key => $prop ) {
			if ( array_key_exists( $prop, $changed_props ) || ! metadata_exists( $meta_type, $object->get_id(), $meta_key ) ) {
				$props_to_update[ $meta_key ] = $prop;
			}
		}

		return $props_to_update;
	}

	/**
	 * Update meta data in, or delete it from, the database.
	 *
	 * Avoids storing meta when it's either an empty string or empty array.
	 * Other empty values such as numeric 0 and null should still be stored.
	 * Data-stores can force meta to exist using `must_exist_meta_keys`.
	 *
	 * Note: WordPress `get_metadata` function returns an empty string when meta data does not exist.
	 *
	 * @param WC_Data $object The WP_Data object (WC_Coupon for coupons, etc).
	 * @param string  $meta_key Meta key to update.
	 * @param mixed   $meta_value Value to save.
	 *
	 * @since 3.6.0 Added to prevent empty meta being stored unless required.
	 *
	 * @return bool True if updated/deleted.
	 */
	protected function update_or_delete_post_meta( $object, $meta_key, $meta_value ) {
		if ( in_array( $meta_value, array( array(), '' ), true ) && ! in_array( $meta_key, $this->must_exist_meta_keys, true ) ) {
			$updated = delete_post_meta( $object->get_id(), $meta_key );
		} else {
			$updated = update_post_meta( $object->get_id(), $meta_key, $meta_value );
		}

		return (bool) $updated;
	}

	/**
	 * Get valid WP_Query args from a WC_Object_Query's query variables.
	 *
	 * @since 3.1.0
	 * @param array $query_vars query vars from a WC_Object_Query.
	 * @return array
	 */
	protected function get_wp_query_args( $query_vars ) {

		$skipped_values = array( '', array(), null );
		$wp_query_args  = array(
			'errors'     => array(),
			'meta_query' => array(), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
		);

		foreach ( $query_vars as $key => $value ) {
			if ( in_array( $value, $skipped_values, true ) || 'meta_query' === $key ) {
				continue;
			}

			// Build meta queries out of vars that are stored in internal meta keys.
			if ( in_array( '_' . $key, $this->internal_meta_keys, true ) ) {
				// Check for existing values if wildcard is used.
				if ( '*' === $value ) {
					$wp_query_args['meta_query'][] = array(
						array(
							'key'     => '_' . $key,
							'compare' => 'EXISTS',
						),
						array(
							'key'     => '_' . $key,
							'value'   => '',
							'compare' => '!=',
						),
					);
				} else {
					$wp_query_args['meta_query'][] = array(
						'key'     => '_' . $key,
						'value'   => $value,
						'compare' => is_array( $value ) ? 'IN' : '=',
					);
				}
			} else { // Other vars get mapped to wp_query args or just left alone.
				$key_mapping = array(
					'parent'         => 'post_parent',
					'parent_exclude' => 'post_parent__not_in',
					'exclude'        => 'post__not_in',
					'limit'          => 'posts_per_page',
					'type'           => 'post_type',
					'return'         => 'fields',
				);

				if ( isset( $key_mapping[ $key ] ) ) {
					$wp_query_args[ $key_mapping[ $key ] ] = $value;
				} else {
					$wp_query_args[ $key ] = $value;
				}
			}
		}

		return apply_filters( 'woocommerce_get_wp_query_args', $wp_query_args, $query_vars );
	}

	/**
	 * Map a valid date query var to WP_Query arguments.
	 * Valid date formats: YYYY-MM-DD or timestamp, possibly combined with an operator from $valid_operators.
	 * Also accepts a WC_DateTime object.
	 *
	 * @since 3.2.0
	 * @param mixed  $query_var A valid date format.
	 * @param string $key meta or db column key.
	 * @param array  $wp_query_args WP_Query args.
	 * @return array Modified $wp_query_args
	 */
	public function parse_date_for_wp_query( $query_var, $key, $wp_query_args = array() ) {
		$query_parse_regex = '/([^.<>]*)(>=|<=|>|<|\.\.\.)([^.<>]+)/';
		$valid_operators   = array( '>', '>=', '=', '<=', '<', '...' );

		// YYYY-MM-DD queries have 'day' precision. Timestamp/WC_DateTime queries have 'second' precision.
		$precision = 'second';

		$dates    = array();
		$operator = '=';

		try {
			// Specific time query with a WC_DateTime.
			if ( is_a( $query_var, 'WC_DateTime' ) ) {
				$dates[] = $query_var;
			} elseif ( is_numeric( $query_var ) ) { // Specific time query with a timestamp.
				$dates[] = new WC_DateTime( "@{$query_var}", new DateTimeZone( 'UTC' ) );
			} elseif ( preg_match( $query_parse_regex, $query_var, $sections ) ) { // Query with operators and possible range of dates.
				if ( ! empty( $sections[1] ) ) {
					$dates[] = is_numeric( $sections[1] ) ? new WC_DateTime( "@{$sections[1]}", new DateTimeZone( 'UTC' ) ) : wc_string_to_datetime( $sections[1] );
				}

				$operator = in_array( $sections[2], $valid_operators, true ) ? $sections[2] : '';
				$dates[]  = is_numeric( $sections[3] ) ? new WC_DateTime( "@{$sections[3]}", new DateTimeZone( 'UTC' ) ) : wc_string_to_datetime( $sections[3] );

				if ( ! is_numeric( $sections[1] ) && ! is_numeric( $sections[3] ) ) {
					$precision = 'day';
				}
			} else { // Specific time query with a string.
				$dates[]   = wc_string_to_datetime( $query_var );
				$precision = 'day';
			}
		} catch ( Exception $e ) {
			return $wp_query_args;
		}

		// Check for valid inputs.
		if ( ! $operator || empty( $dates ) || ( '...' === $operator && count( $dates ) < 2 ) ) {
			return $wp_query_args;
		}

		// Build date query for 'post_date' or 'post_modified' keys.
		if ( 'post_date' === $key || 'post_modified' === $key ) {
			if ( ! isset( $wp_query_args['date_query'] ) ) {
				$wp_query_args['date_query'] = array();
			}

			$query_arg = array(
				'column'    => 'day' === $precision ? $key : $key . '_gmt',
				'inclusive' => '>' !== $operator && '<' !== $operator,
			);

			// Add 'before'/'after' query args.
			$comparisons = array();
			if ( '>' === $operator || '>=' === $operator || '...' === $operator ) {
				$comparisons[] = 'after';
			}
			if ( '<' === $operator || '<=' === $operator || '...' === $operator ) {
				$comparisons[] = 'before';
			}

			foreach ( $comparisons as $index => $comparison ) {
				if ( 'day' === $precision ) {
					/**
					 * WordPress doesn't generate the correct SQL for inclusive day queries with both a 'before' and
					 * 'after' string query, so we have to use the array format in 'day' precision.
					 *
					 * @see https://core.trac.wordpress.org/ticket/29908
					 */
					$query_arg[ $comparison ]['year']  = $dates[ $index ]->date( 'Y' );
					$query_arg[ $comparison ]['month'] = $dates[ $index ]->date( 'n' );
					$query_arg[ $comparison ]['day']   = $dates[ $index ]->date( 'j' );
				} else {
					/**
					 * WordPress doesn't support 'hour'/'second'/'minute' in array format 'before'/'after' queries,
					 * so we have to use a string query.
					 */
					$query_arg[ $comparison ] = gmdate( 'm/d/Y H:i:s', $dates[ $index ]->getTimestamp() );
				}
			}

			if ( empty( $comparisons ) ) {
				$query_arg['year']  = $dates[0]->date( 'Y' );
				$query_arg['month'] = $dates[0]->date( 'n' );
				$query_arg['day']   = $dates[0]->date( 'j' );
				if ( 'second' === $precision ) {
					$query_arg['hour']   = $dates[0]->date( 'H' );
					$query_arg['minute'] = $dates[0]->date( 'i' );
					$query_arg['second'] = $dates[0]->date( 's' );
				}
			}
			$wp_query_args['date_query'][] = $query_arg;
			return $wp_query_args;
		}

		// Build meta query for unrecognized keys.
		if ( ! isset( $wp_query_args['meta_query'] ) ) {
			$wp_query_args['meta_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
		}

		// Meta dates are stored as timestamps in the db.
		// Check against beginning/end-of-day timestamps when using 'day' precision.
		if ( 'day' === $precision ) {
			$start_timestamp = strtotime( gmdate( 'm/d/Y 00:00:00', $dates[0]->getTimestamp() ) );
			$end_timestamp   = '...' !== $operator ? ( $start_timestamp + DAY_IN_SECONDS ) : strtotime( gmdate( 'm/d/Y 00:00:00', $dates[1]->getTimestamp() ) );
			switch ( $operator ) {
				case '>':
				case '<=':
					$wp_query_args['meta_query'][] = array(
						'key'     => $key,
						'value'   => $end_timestamp,
						'compare' => $operator,
					);
					break;
				case '<':
				case '>=':
					$wp_query_args['meta_query'][] = array(
						'key'     => $key,
						'value'   => $start_timestamp,
						'compare' => $operator,
					);
					break;
				default:
					$wp_query_args['meta_query'][] = array(
						'key'     => $key,
						'value'   => $start_timestamp,
						'compare' => '>=',
					);
					$wp_query_args['meta_query'][] = array(
						'key'     => $key,
						'value'   => $end_timestamp,
						'compare' => '<=',
					);
			}
		} else {
			if ( '...' !== $operator ) {
				$wp_query_args['meta_query'][] = array(
					'key'     => $key,
					'value'   => $dates[0]->getTimestamp(),
					'compare' => $operator,
				);
			} else {
				$wp_query_args['meta_query'][] = array(
					'key'     => $key,
					'value'   => $dates[0]->getTimestamp(),
					'compare' => '>=',
				);
				$wp_query_args['meta_query'][] = array(
					'key'     => $key,
					'value'   => $dates[1]->getTimestamp(),
					'compare' => '<=',
				);
			}
		}

		return $wp_query_args;
	}

	/**
	 * Return list of internal meta keys.
	 *
	 * @since 3.2.0
	 * @return array
	 */
	public function get_internal_meta_keys() {
		return $this->internal_meta_keys;
	}

	/**
	 * Check if the terms are suitable for searching.
	 *
	 * Uses an array of stopwords (terms) that are excluded from the separate
	 * term matching when searching for posts. The list of English stopwords is
	 * the approximate search engines list, and is translatable.
	 *
	 * @since 3.4.0
	 * @param array $terms Terms to check.
	 * @return array Terms that are not stopwords.
	 */
	protected function get_valid_search_terms( $terms ) {
		$valid_terms = array();
		$stopwords   = $this->get_search_stopwords();

		foreach ( $terms as $term ) {
			// keep before/after spaces when term is for exact match, otherwise trim quotes and spaces.
			if ( preg_match( '/^".+"$/', $term ) ) {
				$term = trim( $term, "\"'" );
			} else {
				$term = trim( $term, "\"' " );
			}

			// Avoid single A-Z and single dashes.
			if ( empty( $term ) || ( 1 === strlen( $term ) && preg_match( '/^[a-z\-]$/i', $term ) ) ) {
				continue;
			}

			if ( in_array( wc_strtolower( $term ), $stopwords, true ) ) {
				continue;
			}

			$valid_terms[] = $term;
		}

		return $valid_terms;
	}

	/**
	 * Retrieve stopwords used when parsing search terms.
	 *
	 * @since 3.4.0
	 * @return array Stopwords.
	 */
	protected function get_search_stopwords() {
		// Translators: This is a comma-separated list of very common words that should be excluded from a search, like a, an, and the. These are usually called "stopwords". You should not simply translate these individual words into your language. Instead, look for and provide commonly accepted stopwords in your language.
		$stopwords = array_map(
			'wc_strtolower',
			array_map(
				'trim',
				explode(
					',',
					_x(
						'about,an,are,as,at,be,by,com,for,from,how,in,is,it,of,on,or,that,the,this,to,was,what,when,where,who,will,with,www',
						'Comma-separated list of search stopwords in your language',
						'woocommerce'
					)
				)
			)
		);

		return apply_filters( 'wp_search_stopwords', $stopwords );
	}

	/**
	 * Get data to save to a lookup table.
	 *
	 * @since 3.6.0
	 * @param int    $id ID of object to update.
	 * @param string $table Lookup table name.
	 * @return array
	 */
	protected function get_data_for_lookup_table( $id, $table ) {
		return array();
	}

	/**
	 * Get primary key name for lookup table.
	 *
	 * @since 3.6.0
	 * @param string $table Lookup table name.
	 * @return string
	 */
	protected function get_primary_key_for_lookup_table( $table ) {
		return '';
	}

	/**
	 * Update a lookup table for an object.
	 *
	 * @since 3.6.0
	 * @param int    $id ID of object to update.
	 * @param string $table Lookup table name.
	 *
	 * @return NULL
	 */
	protected function update_lookup_table( $id, $table ) {
		global $wpdb;

		$id    = absint( $id );
		$table = sanitize_key( $table );

		if ( empty( $id ) || empty( $table ) ) {
			return false;
		}

		$existing_data = wp_cache_get( 'lookup_table', 'object_' . $id );
		$update_data   = $this->get_data_for_lookup_table( $id, $table );

		if ( ! empty( $update_data ) && $update_data !== $existing_data ) {
			$wpdb->replace(
				$wpdb->$table,
				$update_data
			);
			wp_cache_set( 'lookup_table', $update_data, 'object_' . $id );
		}
	}

	/**
	 * Delete lookup table data for an ID.
	 *
	 * @since 3.6.0
	 * @param int    $id ID of object to update.
	 * @param string $table Lookup table name.
	 */
	public function delete_from_lookup_table( $id, $table ) {
		global $wpdb;

		$id    = absint( $id );
		$table = sanitize_key( $table );

		if ( empty( $id ) || empty( $table ) ) {
			return false;
		}

		$pk = $this->get_primary_key_for_lookup_table( $table );

		$wpdb->delete(
			$wpdb->$table,
			array(
				$pk => $id,
			)
		);
		wp_cache_delete( 'lookup_table', 'object_' . $id );
	}

	/**
	 * Converts a WP post date string into a timestamp.
	 *
	 * @since 4.8.0
	 *
	 * @param  string $time_string The WP post date string.
	 * @return int|null The date string converted to a timestamp or null.
	 */
	protected function string_to_timestamp( $time_string ) {
		return '0000-00-00 00:00:00' !== $time_string ? wc_string_to_timestamp( $time_string ) : null;
	}
}
data-stores/class-wc-customer-data-store-session.php000064400000012336151336044770016617 0ustar00<?php
/**
 * Class WC_Customer_Data_Store_Session file.
 *
 * @package WooCommerce\DataStores
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Customer Data Store which stores the data in session.
 *
 * @version  3.0.0
 */
class WC_Customer_Data_Store_Session extends WC_Data_Store_WP implements WC_Customer_Data_Store_Interface, WC_Object_Data_Store_Interface {

	/**
	 * Keys which are also stored in a session (so we can make sure they get updated...)
	 *
	 * @var array
	 */
	protected $session_keys = array(
		'id',
		'date_modified',
		'billing_postcode',
		'billing_city',
		'billing_address_1',
		'billing_address',
		'billing_address_2',
		'billing_state',
		'billing_country',
		'shipping_postcode',
		'shipping_city',
		'shipping_address_1',
		'shipping_address',
		'shipping_address_2',
		'shipping_state',
		'shipping_country',
		'is_vat_exempt',
		'calculated_shipping',
		'billing_first_name',
		'billing_last_name',
		'billing_company',
		'billing_phone',
		'billing_email',
		'shipping_first_name',
		'shipping_last_name',
		'shipping_company',
		'shipping_phone',
	);

	/**
	 * Simply update the session.
	 *
	 * @param WC_Customer $customer Customer object.
	 */
	public function create( &$customer ) {
		$this->save_to_session( $customer );
	}

	/**
	 * Simply update the session.
	 *
	 * @param WC_Customer $customer Customer object.
	 */
	public function update( &$customer ) {
		$this->save_to_session( $customer );
	}

	/**
	 * Saves all customer data to the session.
	 *
	 * @param WC_Customer $customer Customer object.
	 */
	public function save_to_session( $customer ) {
		$data = array();
		foreach ( $this->session_keys as $session_key ) {
			$function_key = $session_key;
			if ( 'billing_' === substr( $session_key, 0, 8 ) ) {
				$session_key = str_replace( 'billing_', '', $session_key );
			}
			$data[ $session_key ] = (string) $customer->{"get_$function_key"}( 'edit' );
		}
		WC()->session->set( 'customer', $data );
	}

	/**
	 * Read customer data from the session unless the user has logged in, in
	 * which case the stored ID will differ from the actual ID.
	 *
	 * @since 3.0.0
	 * @param WC_Customer $customer Customer object.
	 */
	public function read( &$customer ) {
		$data = (array) WC()->session->get( 'customer' );

		/**
		 * There is a valid session if $data is not empty, and the ID matches the logged in user ID.
		 *
		 * If the user object has been updated since the session was created (based on date_modified) we should not load the session - data should be reloaded.
		 */
		if ( isset( $data['id'], $data['date_modified'] ) && $data['id'] === (string) $customer->get_id() && $data['date_modified'] === (string) $customer->get_date_modified( 'edit' ) ) {
			foreach ( $this->session_keys as $session_key ) {
				if ( in_array( $session_key, array( 'id', 'date_modified' ), true ) ) {
					continue;
				}
				$function_key = $session_key;
				if ( 'billing_' === substr( $session_key, 0, 8 ) ) {
					$session_key = str_replace( 'billing_', '', $session_key );
				}
				if ( isset( $data[ $session_key ] ) && is_callable( array( $customer, "set_{$function_key}" ) ) ) {
					$customer->{"set_{$function_key}"}( wp_unslash( $data[ $session_key ] ) );
				}
			}
		}
		$this->set_defaults( $customer );
		$customer->set_object_read( true );
	}

	/**
	 * Load default values if props are unset.
	 *
	 * @param WC_Customer $customer Customer object.
	 */
	protected function set_defaults( &$customer ) {
		try {
			$default = wc_get_customer_default_location();
			$has_shipping_address = $customer->has_shipping_address();

			if ( ! $customer->get_billing_country() ) {
				$customer->set_billing_country( $default['country'] );
			}

			if ( ! $customer->get_shipping_country() && ! $has_shipping_address ) {
				$customer->set_shipping_country( $customer->get_billing_country() );
			}

			if ( ! $customer->get_billing_state() ) {
				$customer->set_billing_state( $default['state'] );
			}

			if ( ! $customer->get_shipping_state() && ! $has_shipping_address ) {
				$customer->set_shipping_state( $customer->get_billing_state() );
			}

			if ( ! $customer->get_billing_email() && is_user_logged_in() ) {
				$current_user = wp_get_current_user();
				$customer->set_billing_email( $current_user->user_email );
			}
		} catch ( WC_Data_Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
		}
	}

	/**
	 * Deletes a customer from the database.
	 *
	 * @since 3.0.0
	 * @param WC_Customer $customer Customer object.
	 * @param array       $args Array of args to pass to the delete method.
	 */
	public function delete( &$customer, $args = array() ) {
		WC()->session->set( 'customer', null );
	}

	/**
	 * Gets the customers last order.
	 *
	 * @since 3.0.0
	 * @param WC_Customer $customer Customer object.
	 * @return WC_Order|false
	 */
	public function get_last_order( &$customer ) {
		return false;
	}

	/**
	 * Return the number of orders this customer has.
	 *
	 * @since 3.0.0
	 * @param WC_Customer $customer Customer object.
	 * @return integer
	 */
	public function get_order_count( &$customer ) {
		return 0;
	}

	/**
	 * Return how much money this customer has spent.
	 *
	 * @since 3.0.0
	 * @param WC_Customer $customer Customer object.
	 * @return float
	 */
	public function get_total_spent( &$customer ) {
		return 0;
	}
}
data-stores/class-wc-order-refund-data-store-cpt.php000064400000007006151336044770016453 0ustar00<?php
/**
 * Class WC_Order_Refund_Data_Store_CPT file.
 *
 * @package WooCommerce\DataStores
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Order Refund Data Store: Stored in CPT.
 *
 * @version  3.0.0
 */
class WC_Order_Refund_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implements WC_Object_Data_Store_Interface, WC_Order_Refund_Data_Store_Interface {

	/**
	 * Data stored in meta keys, but not considered "meta" for an order.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $internal_meta_keys = array(
		'_order_currency',
		'_cart_discount',
		'_refund_amount',
		'_refunded_by',
		'_refunded_payment',
		'_refund_reason',
		'_cart_discount_tax',
		'_order_shipping',
		'_order_shipping_tax',
		'_order_tax',
		'_order_total',
		'_order_version',
		'_prices_include_tax',
		'_payment_tokens',
	);

	/**
	 * Delete a refund - no trash is supported.
	 *
	 * @param WC_Order $order Order object.
	 * @param array    $args Array of args to pass to the delete method.
	 */
	public function delete( &$order, $args = array() ) {
		$id = $order->get_id();
		$parent_order_id = $order->get_parent_id();
		$refund_cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'refunds' . $parent_order_id;

		if ( ! $id ) {
			return;
		}

		wp_delete_post( $id );
		wp_cache_delete( $refund_cache_key, 'orders' );
		$order->set_id( 0 );
		do_action( 'woocommerce_delete_order_refund', $id );
	}

	/**
	 * Read refund data. Can be overridden by child classes to load other props.
	 *
	 * @param WC_Order_Refund $refund Refund object.
	 * @param object          $post_object Post object.
	 * @since 3.0.0
	 */
	protected function read_order_data( &$refund, $post_object ) {
		parent::read_order_data( $refund, $post_object );
		$id = $refund->get_id();
		$refund->set_props(
			array(
				'amount'           => get_post_meta( $id, '_refund_amount', true ),
				'refunded_by'      => metadata_exists( 'post', $id, '_refunded_by' ) ? get_post_meta( $id, '_refunded_by', true ) : absint( $post_object->post_author ),
				'refunded_payment' => wc_string_to_bool( get_post_meta( $id, '_refunded_payment', true ) ),
				'reason'           => metadata_exists( 'post', $id, '_refund_reason' ) ? get_post_meta( $id, '_refund_reason', true ) : $post_object->post_excerpt,
			)
		);
	}

	/**
	 * Helper method that updates all the post meta for an order based on it's settings in the WC_Order class.
	 *
	 * @param WC_Order_Refund $refund Refund object.
	 * @since 3.0.0
	 */
	protected function update_post_meta( &$refund ) {
		parent::update_post_meta( $refund );

		$updated_props     = array();
		$meta_key_to_props = array(
			'_refund_amount'    => 'amount',
			'_refunded_by'      => 'refunded_by',
			'_refunded_payment' => 'refunded_payment',
			'_refund_reason'    => 'reason',
		);

		$props_to_update = $this->get_props_to_update( $refund, $meta_key_to_props );
		foreach ( $props_to_update as $meta_key => $prop ) {
			$value = $refund->{"get_$prop"}( 'edit' );
			update_post_meta( $refund->get_id(), $meta_key, $value );
			$updated_props[] = $prop;
		}

		do_action( 'woocommerce_order_refund_object_updated_props', $refund, $updated_props );
	}

	/**
	 * Get a title for the new post type.
	 *
	 * @return string
	 */
	protected function get_post_title() {
		return sprintf(
			/* translators: %s: Order date */
			__( 'Refund &ndash; %s', 'woocommerce' ),
			strftime( _x( '%b %d, %Y @ %I:%M %p', 'Order date parsed by strftime', 'woocommerce' ) ) // phpcs:ignore WordPress.WP.I18n.MissingTranslatorsComment, WordPress.WP.I18n.UnorderedPlaceholdersText
		);
	}
}
data-stores/class-wc-product-grouped-data-store-cpt.php000064400000005422151336044770017202 0ustar00<?php
/**
 * Class WC_Product_Grouped_Data_Store_CPT file.
 *
 * @package WooCommerce\DataStores
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Grouped Product Data Store: Stored in CPT.
 *
 * @version  3.0.0
 */
class WC_Product_Grouped_Data_Store_CPT extends WC_Product_Data_Store_CPT implements WC_Object_Data_Store_Interface {

	/**
	 * Helper method that updates all the post meta for a grouped product.
	 *
	 * @param WC_Product $product Product object.
	 * @param bool       $force Force update. Used during create.
	 * @since 3.0.0
	 */
	protected function update_post_meta( &$product, $force = false ) {
		$meta_key_to_props = array(
			'_children' => 'children',
		);

		$props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $product, $meta_key_to_props );

		foreach ( $props_to_update as $meta_key => $prop ) {
			$value   = $product->{"get_$prop"}( 'edit' );
			$updated = update_post_meta( $product->get_id(), $meta_key, $value );
			if ( $updated ) {
				$this->updated_props[] = $prop;
			}
		}

		parent::update_post_meta( $product, $force );
	}

	/**
	 * Handle updated meta props after updating meta data.
	 *
	 * @since  3.0.0
	 * @param  WC_Product $product Product object.
	 */
	protected function handle_updated_props( &$product ) {
		if ( in_array( 'children', $this->updated_props, true ) ) {
			$this->update_prices_from_children( $product );
		}
		parent::handle_updated_props( $product );
	}

	/**
	 * Sync grouped product prices with children.
	 *
	 * @since 3.0.0
	 * @param WC_Product|int $product Product object or product ID.
	 */
	public function sync_price( &$product ) {
		$this->update_prices_from_children( $product );
	}

	/**
	 * Loop over child products and update the grouped product prices.
	 *
	 * @param WC_Product $product Product object.
	 */
	protected function update_prices_from_children( &$product ) {
		$child_prices = array();
		foreach ( $product->get_children( 'edit' ) as $child_id ) {
			$child = wc_get_product( $child_id );
			if ( $child ) {
				$child_prices[] = $child->get_price( 'edit' );
			}
		}
		$child_prices = array_filter( $child_prices );
		delete_post_meta( $product->get_id(), '_price' );
		delete_post_meta( $product->get_id(), '_sale_price' );
		delete_post_meta( $product->get_id(), '_regular_price' );

		if ( ! empty( $child_prices ) ) {
			add_post_meta( $product->get_id(), '_price', min( $child_prices ) );
			add_post_meta( $product->get_id(), '_price', max( $child_prices ) );
		}

		$this->update_lookup_table( $product->get_id(), 'wc_product_meta_lookup' );

		/**
		 * Fire an action for this direct update so it can be detected by other code.
		 *
		 * @since 3.6
		 * @param int $product_id Product ID that was updated directly.
		 */
		do_action( 'woocommerce_updated_product_price', $product->get_id() );
	}
}
data-stores/class-wc-product-data-store-cpt.php000064400000213400151336044770015534 0ustar00<?php
/**
 * WC_Product_Data_Store_CPT class file.
 *
 * @package WooCommerce\Classes
 */

use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Internal\DownloadPermissionsAdjuster;
use Automattic\WooCommerce\Utilities\NumberUtil;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Product Data Store: Stored in CPT.
 *
 * @version  3.0.0
 */
class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Data_Store_Interface, WC_Product_Data_Store_Interface {

	/**
	 * Data stored in meta keys, but not considered "meta".
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $internal_meta_keys = array(
		'_visibility',
		'_sku',
		'_price',
		'_regular_price',
		'_sale_price',
		'_sale_price_dates_from',
		'_sale_price_dates_to',
		'total_sales',
		'_tax_status',
		'_tax_class',
		'_manage_stock',
		'_stock',
		'_stock_status',
		'_backorders',
		'_low_stock_amount',
		'_sold_individually',
		'_weight',
		'_length',
		'_width',
		'_height',
		'_upsell_ids',
		'_crosssell_ids',
		'_purchase_note',
		'_default_attributes',
		'_product_attributes',
		'_virtual',
		'_downloadable',
		'_download_limit',
		'_download_expiry',
		'_featured',
		'_downloadable_files',
		'_wc_rating_count',
		'_wc_average_rating',
		'_wc_review_count',
		'_variation_description',
		'_thumbnail_id',
		'_file_paths',
		'_product_image_gallery',
		'_product_version',
		'_wp_old_slug',
		'_edit_last',
		'_edit_lock',
	);

	/**
	 * Meta data which should exist in the DB, even if empty.
	 *
	 * @since 3.6.0
	 *
	 * @var array
	 */
	protected $must_exist_meta_keys = array(
		'_tax_class',
	);

	/**
	 * If we have already saved our extra data, don't do automatic / default handling.
	 *
	 * @var bool
	 */
	protected $extra_data_saved = false;

	/**
	 * Stores updated props.
	 *
	 * @var array
	 */
	protected $updated_props = array();

	/*
	|--------------------------------------------------------------------------
	| CRUD Methods
	|--------------------------------------------------------------------------
	*/

	/**
	 * Method to create a new product in the database.
	 *
	 * @param WC_Product $product Product object.
	 */
	public function create( &$product ) {
		if ( ! $product->get_date_created( 'edit' ) ) {
			$product->set_date_created( time() );
		}

		$id = wp_insert_post(
			apply_filters(
				'woocommerce_new_product_data',
				array(
					'post_type'      => 'product',
					'post_status'    => $product->get_status() ? $product->get_status() : 'publish',
					'post_author'    => get_current_user_id(),
					'post_title'     => $product->get_name() ? $product->get_name() : __( 'Product', 'woocommerce' ),
					'post_content'   => $product->get_description(),
					'post_excerpt'   => $product->get_short_description(),
					'post_parent'    => $product->get_parent_id(),
					'comment_status' => $product->get_reviews_allowed() ? 'open' : 'closed',
					'ping_status'    => 'closed',
					'menu_order'     => $product->get_menu_order(),
					'post_password'  => $product->get_post_password( 'edit' ),
					'post_date'      => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ),
					'post_date_gmt'  => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ),
					'post_name'      => $product->get_slug( 'edit' ),
				)
			),
			true
		);

		if ( $id && ! is_wp_error( $id ) ) {
			$product->set_id( $id );

			$this->update_post_meta( $product, true );
			$this->update_terms( $product, true );
			$this->update_visibility( $product, true );
			$this->update_attributes( $product, true );
			$this->update_version_and_type( $product );
			$this->handle_updated_props( $product );
			$this->clear_caches( $product );

			$product->save_meta_data();
			$product->apply_changes();

			do_action( 'woocommerce_new_product', $id, $product );
		}
	}

	/**
	 * Method to read a product from the database.
	 *
	 * @param WC_Product $product Product object.
	 * @throws Exception If invalid product.
	 */
	public function read( &$product ) {
		$product->set_defaults();
		$post_object = get_post( $product->get_id() );

		if ( ! $product->get_id() || ! $post_object || 'product' !== $post_object->post_type ) {
			throw new Exception( __( 'Invalid product.', 'woocommerce' ) );
		}

		$product->set_props(
			array(
				'name'              => $post_object->post_title,
				'slug'              => $post_object->post_name,
				'date_created'      => $this->string_to_timestamp( $post_object->post_date_gmt ),
				'date_modified'     => $this->string_to_timestamp( $post_object->post_modified_gmt ),
				'status'            => $post_object->post_status,
				'description'       => $post_object->post_content,
				'short_description' => $post_object->post_excerpt,
				'parent_id'         => $post_object->post_parent,
				'menu_order'        => $post_object->menu_order,
				'post_password'     => $post_object->post_password,
				'reviews_allowed'   => 'open' === $post_object->comment_status,
			)
		);

		$this->read_attributes( $product );
		$this->read_downloads( $product );
		$this->read_visibility( $product );
		$this->read_product_data( $product );
		$this->read_extra_data( $product );
		$product->set_object_read( true );

		do_action( 'woocommerce_product_read', $product->get_id() );
	}

	/**
	 * Method to update a product in the database.
	 *
	 * @param WC_Product $product Product object.
	 */
	public function update( &$product ) {
		$product->save_meta_data();
		$changes = $product->get_changes();

		// Only update the post when the post data changes.
		if ( array_intersect( array( 'description', 'short_description', 'name', 'parent_id', 'reviews_allowed', 'status', 'menu_order', 'date_created', 'date_modified', 'slug' ), array_keys( $changes ) ) ) {
			$post_data = array(
				'post_content'   => $product->get_description( 'edit' ),
				'post_excerpt'   => $product->get_short_description( 'edit' ),
				'post_title'     => $product->get_name( 'edit' ),
				'post_parent'    => $product->get_parent_id( 'edit' ),
				'comment_status' => $product->get_reviews_allowed( 'edit' ) ? 'open' : 'closed',
				'post_status'    => $product->get_status( 'edit' ) ? $product->get_status( 'edit' ) : 'publish',
				'menu_order'     => $product->get_menu_order( 'edit' ),
				'post_password'  => $product->get_post_password( 'edit' ),
				'post_name'      => $product->get_slug( 'edit' ),
				'post_type'      => 'product',
			);
			if ( $product->get_date_created( 'edit' ) ) {
				$post_data['post_date']     = gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() );
				$post_data['post_date_gmt'] = gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() );
			}
			if ( isset( $changes['date_modified'] ) && $product->get_date_modified( 'edit' ) ) {
				$post_data['post_modified']     = gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getOffsetTimestamp() );
				$post_data['post_modified_gmt'] = gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getTimestamp() );
			} else {
				$post_data['post_modified']     = current_time( 'mysql' );
				$post_data['post_modified_gmt'] = current_time( 'mysql', 1 );
			}

			/**
			 * When updating this object, to prevent infinite loops, use $wpdb
			 * to update data, since wp_update_post spawns more calls to the
			 * save_post action.
			 *
			 * This ensures hooks are fired by either WP itself (admin screen save),
			 * or an update purely from CRUD.
			 */
			if ( doing_action( 'save_post' ) ) {
				$GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $product->get_id() ) );
				clean_post_cache( $product->get_id() );
			} else {
				wp_update_post( array_merge( array( 'ID' => $product->get_id() ), $post_data ) );
			}
			$product->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook.

		} else { // Only update post modified time to record this save event.
			$GLOBALS['wpdb']->update(
				$GLOBALS['wpdb']->posts,
				array(
					'post_modified'     => current_time( 'mysql' ),
					'post_modified_gmt' => current_time( 'mysql', 1 ),
				),
				array(
					'ID' => $product->get_id(),
				)
			);
			clean_post_cache( $product->get_id() );
		}

		$this->update_post_meta( $product );
		$this->update_terms( $product );
		$this->update_visibility( $product );
		$this->update_attributes( $product );
		$this->update_version_and_type( $product );
		$this->handle_updated_props( $product );
		$this->clear_caches( $product );

		wc_get_container()
			->get( DownloadPermissionsAdjuster::class )
			->maybe_schedule_adjust_download_permissions( $product );

		$product->apply_changes();

		do_action( 'woocommerce_update_product', $product->get_id(), $product );
	}

	/**
	 * Method to delete a product from the database.
	 *
	 * @param WC_Product $product Product object.
	 * @param array      $args Array of args to pass to the delete method.
	 */
	public function delete( &$product, $args = array() ) {
		$id        = $product->get_id();
		$post_type = $product->is_type( 'variation' ) ? 'product_variation' : 'product';

		$args = wp_parse_args(
			$args,
			array(
				'force_delete' => false,
			)
		);

		if ( ! $id ) {
			return;
		}

		if ( $args['force_delete'] ) {
			do_action( 'woocommerce_before_delete_' . $post_type, $id );
			wp_delete_post( $id );
			$product->set_id( 0 );
			do_action( 'woocommerce_delete_' . $post_type, $id );
		} else {
			wp_trash_post( $id );
			$product->set_status( 'trash' );
			do_action( 'woocommerce_trash_' . $post_type, $id );
		}
	}

	/*
	|--------------------------------------------------------------------------
	| Additional Methods
	|--------------------------------------------------------------------------
	*/

	/**
	 * Read product data. Can be overridden by child classes to load other props.
	 *
	 * @param WC_Product $product Product object.
	 * @since 3.0.0
	 */
	protected function read_product_data( &$product ) {
		$id                = $product->get_id();
		$post_meta_values  = get_post_meta( $id );
		$meta_key_to_props = array(
			'_sku'                   => 'sku',
			'_regular_price'         => 'regular_price',
			'_sale_price'            => 'sale_price',
			'_price'                 => 'price',
			'_sale_price_dates_from' => 'date_on_sale_from',
			'_sale_price_dates_to'   => 'date_on_sale_to',
			'total_sales'            => 'total_sales',
			'_tax_status'            => 'tax_status',
			'_tax_class'             => 'tax_class',
			'_manage_stock'          => 'manage_stock',
			'_backorders'            => 'backorders',
			'_low_stock_amount'      => 'low_stock_amount',
			'_sold_individually'     => 'sold_individually',
			'_weight'                => 'weight',
			'_length'                => 'length',
			'_width'                 => 'width',
			'_height'                => 'height',
			'_upsell_ids'            => 'upsell_ids',
			'_crosssell_ids'         => 'cross_sell_ids',
			'_purchase_note'         => 'purchase_note',
			'_default_attributes'    => 'default_attributes',
			'_virtual'               => 'virtual',
			'_downloadable'          => 'downloadable',
			'_download_limit'        => 'download_limit',
			'_download_expiry'       => 'download_expiry',
			'_thumbnail_id'          => 'image_id',
			'_stock'                 => 'stock_quantity',
			'_stock_status'          => 'stock_status',
			'_wc_average_rating'     => 'average_rating',
			'_wc_rating_count'       => 'rating_counts',
			'_wc_review_count'       => 'review_count',
			'_product_image_gallery' => 'gallery_image_ids',
		);

		$set_props = array();

		foreach ( $meta_key_to_props as $meta_key => $prop ) {
			$meta_value         = isset( $post_meta_values[ $meta_key ][0] ) ? $post_meta_values[ $meta_key ][0] : null;
			$set_props[ $prop ] = maybe_unserialize( $meta_value ); // get_post_meta only unserializes single values.
		}

		$set_props['category_ids']      = $this->get_term_ids( $product, 'product_cat' );
		$set_props['tag_ids']           = $this->get_term_ids( $product, 'product_tag' );
		$set_props['shipping_class_id'] = current( $this->get_term_ids( $product, 'product_shipping_class' ) );
		$set_props['gallery_image_ids'] = array_filter( explode( ',', $set_props['gallery_image_ids'] ) );

		$product->set_props( $set_props );
	}

	/**
	 * Re-reads stock from the DB ignoring changes.
	 *
	 * @param WC_Product $product Product object.
	 * @param int|float  $new_stock New stock level if already read.
	 */
	public function read_stock_quantity( &$product, $new_stock = null ) {
		$object_read = $product->get_object_read();
		$product->set_object_read( false ); // This makes update of qty go directly to data- instead of changes-array of the product object (which is needed as the data should hold status of the object as it was read from the db).
		$product->set_stock_quantity( is_null( $new_stock ) ? get_post_meta( $product->get_id(), '_stock', true ) : $new_stock );
		$product->set_object_read( $object_read );
	}

	/**
	 * Read extra data associated with the product, like button text or product URL for external products.
	 *
	 * @param WC_Product $product Product object.
	 * @since 3.0.0
	 */
	protected function read_extra_data( &$product ) {
		foreach ( $product->get_extra_data_keys() as $key ) {
			$function = 'set_' . $key;
			if ( is_callable( array( $product, $function ) ) ) {
				$product->{$function}( get_post_meta( $product->get_id(), '_' . $key, true ) );
			}
		}
	}

	/**
	 * Convert visibility terms to props.
	 * Catalog visibility valid values are 'visible', 'catalog', 'search', and 'hidden'.
	 *
	 * @param WC_Product $product Product object.
	 * @since 3.0.0
	 */
	protected function read_visibility( &$product ) {
		$terms           = get_the_terms( $product->get_id(), 'product_visibility' );
		$term_names      = is_array( $terms ) ? wp_list_pluck( $terms, 'name' ) : array();
		$featured        = in_array( 'featured', $term_names, true );
		$exclude_search  = in_array( 'exclude-from-search', $term_names, true );
		$exclude_catalog = in_array( 'exclude-from-catalog', $term_names, true );

		if ( $exclude_search && $exclude_catalog ) {
			$catalog_visibility = 'hidden';
		} elseif ( $exclude_search ) {
			$catalog_visibility = 'catalog';
		} elseif ( $exclude_catalog ) {
			$catalog_visibility = 'search';
		} else {
			$catalog_visibility = 'visible';
		}

		$product->set_props(
			array(
				'featured'           => $featured,
				'catalog_visibility' => $catalog_visibility,
			)
		);
	}

	/**
	 * Read attributes from post meta.
	 *
	 * @param WC_Product $product Product object.
	 */
	protected function read_attributes( &$product ) {
		$meta_attributes = get_post_meta( $product->get_id(), '_product_attributes', true );

		if ( ! empty( $meta_attributes ) && is_array( $meta_attributes ) ) {
			$attributes = array();
			foreach ( $meta_attributes as $meta_attribute_key => $meta_attribute_value ) {
				$meta_value = array_merge(
					array(
						'name'         => '',
						'value'        => '',
						'position'     => 0,
						'is_visible'   => 0,
						'is_variation' => 0,
						'is_taxonomy'  => 0,
					),
					(array) $meta_attribute_value
				);

				// Check if is a taxonomy attribute.
				if ( ! empty( $meta_value['is_taxonomy'] ) ) {
					if ( ! taxonomy_exists( $meta_value['name'] ) ) {
						continue;
					}
					$id      = wc_attribute_taxonomy_id_by_name( $meta_value['name'] );
					$options = wc_get_object_terms( $product->get_id(), $meta_value['name'], 'term_id' );
				} else {
					$id      = 0;
					$options = wc_get_text_attributes( $meta_value['value'] );
				}

				$attribute = new WC_Product_Attribute();
				$attribute->set_id( $id );
				$attribute->set_name( $meta_value['name'] );
				$attribute->set_options( $options );
				$attribute->set_position( $meta_value['position'] );
				$attribute->set_visible( $meta_value['is_visible'] );
				$attribute->set_variation( $meta_value['is_variation'] );
				$attributes[] = $attribute;
			}
			$product->set_attributes( $attributes );
		}
	}

	/**
	 * Read downloads from post meta.
	 *
	 * @param WC_Product $product Product object.
	 * @since 3.0.0
	 */
	protected function read_downloads( &$product ) {
		$meta_values = array_filter( (array) get_post_meta( $product->get_id(), '_downloadable_files', true ) );

		if ( $meta_values ) {
			$downloads = array();
			foreach ( $meta_values as $key => $value ) {
				if ( ! isset( $value['name'], $value['file'] ) ) {
					continue;
				}
				$download = new WC_Product_Download();
				$download->set_id( $key );
				$download->set_name( $value['name'] ? $value['name'] : wc_get_filename_from_url( $value['file'] ) );
				$download->set_file( apply_filters( 'woocommerce_file_download_path', $value['file'], $product, $key ) );
				$downloads[] = $download;
			}
			$product->set_downloads( $downloads );
		}
	}

	/**
	 * Helper method that updates all the post meta for a product based on it's settings in the WC_Product class.
	 *
	 * @param WC_Product $product Product object.
	 * @param bool       $force Force update. Used during create.
	 * @since 3.0.0
	 */
	protected function update_post_meta( &$product, $force = false ) {
		$meta_key_to_props = array(
			'_sku'                   => 'sku',
			'_regular_price'         => 'regular_price',
			'_sale_price'            => 'sale_price',
			'_sale_price_dates_from' => 'date_on_sale_from',
			'_sale_price_dates_to'   => 'date_on_sale_to',
			'total_sales'            => 'total_sales',
			'_tax_status'            => 'tax_status',
			'_tax_class'             => 'tax_class',
			'_manage_stock'          => 'manage_stock',
			'_backorders'            => 'backorders',
			'_low_stock_amount'      => 'low_stock_amount',
			'_sold_individually'     => 'sold_individually',
			'_weight'                => 'weight',
			'_length'                => 'length',
			'_width'                 => 'width',
			'_height'                => 'height',
			'_upsell_ids'            => 'upsell_ids',
			'_crosssell_ids'         => 'cross_sell_ids',
			'_purchase_note'         => 'purchase_note',
			'_default_attributes'    => 'default_attributes',
			'_virtual'               => 'virtual',
			'_downloadable'          => 'downloadable',
			'_product_image_gallery' => 'gallery_image_ids',
			'_download_limit'        => 'download_limit',
			'_download_expiry'       => 'download_expiry',
			'_thumbnail_id'          => 'image_id',
			'_stock'                 => 'stock_quantity',
			'_stock_status'          => 'stock_status',
			'_wc_average_rating'     => 'average_rating',
			'_wc_rating_count'       => 'rating_counts',
			'_wc_review_count'       => 'review_count',
		);

		// Make sure to take extra data (like product url or text for external products) into account.
		$extra_data_keys = $product->get_extra_data_keys();

		foreach ( $extra_data_keys as $key ) {
			$meta_key_to_props[ '_' . $key ] = $key;
		}

		$props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $product, $meta_key_to_props );

		foreach ( $props_to_update as $meta_key => $prop ) {
			$value = $product->{"get_$prop"}( 'edit' );
			$value = is_string( $value ) ? wp_slash( $value ) : $value;
			switch ( $prop ) {
				case 'virtual':
				case 'downloadable':
				case 'manage_stock':
				case 'sold_individually':
					$value = wc_bool_to_string( $value );
					break;
				case 'gallery_image_ids':
					$value = implode( ',', $value );
					break;
				case 'date_on_sale_from':
				case 'date_on_sale_to':
					$value = $value ? $value->getTimestamp() : '';
					break;
				case 'stock_quantity':
					// Fire actions to let 3rd parties know the stock is about to be changed.
					if ( $product->is_type( 'variation' ) ) {
						/**
						* Action to signal that the value of 'stock_quantity' for a variation is about to change.
						*
						* @since 4.9
						*
						* @param int $product The variation whose stock is about to change.
						*/
						do_action( 'woocommerce_variation_before_set_stock', $product );
					} else {
						/**
						* Action to signal that the value of 'stock_quantity' for a product is about to change.
						*
						* @since 4.9
						*
						* @param int $product The product whose stock is about to change.
						*/
						do_action( 'woocommerce_product_before_set_stock', $product );
					}
					break;
			}

			$updated = $this->update_or_delete_post_meta( $product, $meta_key, $value );

			if ( $updated ) {
				$this->updated_props[] = $prop;
			}
		}

		// Update extra data associated with the product like button text or product URL for external products.
		if ( ! $this->extra_data_saved ) {
			foreach ( $extra_data_keys as $key ) {
				$meta_key = '_' . $key;
				$function = 'get_' . $key;
				if ( ! array_key_exists( $meta_key, $props_to_update ) ) {
					continue;
				}
				if ( is_callable( array( $product, $function ) ) ) {
					$value   = $product->{$function}( 'edit' );
					$value   = is_string( $value ) ? wp_slash( $value ) : $value;
					$updated = $this->update_or_delete_post_meta( $product, $meta_key, $value );

					if ( $updated ) {
						$this->updated_props[] = $key;
					}
				}
			}
		}

		if ( $this->update_downloads( $product, $force ) ) {
			$this->updated_props[] = 'downloads';
		}
	}

	/**
	 * Handle updated meta props after updating meta data.
	 *
	 * @since 3.0.0
	 * @param WC_Product $product Product Object.
	 */
	protected function handle_updated_props( &$product ) {
		$price_is_synced = $product->is_type( array( 'variable', 'grouped' ) );

		if ( ! $price_is_synced ) {
			if ( in_array( 'regular_price', $this->updated_props, true ) || in_array( 'sale_price', $this->updated_props, true ) ) {
				if ( $product->get_sale_price( 'edit' ) >= $product->get_regular_price( 'edit' ) ) {
					update_post_meta( $product->get_id(), '_sale_price', '' );
					$product->set_sale_price( '' );
				}
			}

			if ( in_array( 'date_on_sale_from', $this->updated_props, true ) || in_array( 'date_on_sale_to', $this->updated_props, true ) || in_array( 'regular_price', $this->updated_props, true ) || in_array( 'sale_price', $this->updated_props, true ) || in_array( 'product_type', $this->updated_props, true ) ) {
				if ( $product->is_on_sale( 'edit' ) ) {
					update_post_meta( $product->get_id(), '_price', $product->get_sale_price( 'edit' ) );
					$product->set_price( $product->get_sale_price( 'edit' ) );
				} else {
					update_post_meta( $product->get_id(), '_price', $product->get_regular_price( 'edit' ) );
					$product->set_price( $product->get_regular_price( 'edit' ) );
				}
			}
		}

		if ( in_array( 'stock_quantity', $this->updated_props, true ) ) {
			if ( $product->is_type( 'variation' ) ) {
				do_action( 'woocommerce_variation_set_stock', $product );
			} else {
				do_action( 'woocommerce_product_set_stock', $product );
			}
		}

		if ( in_array( 'stock_status', $this->updated_props, true ) ) {
			if ( $product->is_type( 'variation' ) ) {
				do_action( 'woocommerce_variation_set_stock_status', $product->get_id(), $product->get_stock_status(), $product );
			} else {
				do_action( 'woocommerce_product_set_stock_status', $product->get_id(), $product->get_stock_status(), $product );
			}
		}

		if ( array_intersect( $this->updated_props, array( 'sku', 'regular_price', 'sale_price', 'date_on_sale_from', 'date_on_sale_to', 'total_sales', 'average_rating', 'stock_quantity', 'stock_status', 'manage_stock', 'downloadable', 'virtual', 'tax_status', 'tax_class' ) ) ) {
			$this->update_lookup_table( $product->get_id(), 'wc_product_meta_lookup' );
		}

		// Trigger action so 3rd parties can deal with updated props.
		do_action( 'woocommerce_product_object_updated_props', $product, $this->updated_props );

		// After handling, we can reset the props array.
		$this->updated_props = array();
	}

	/**
	 * For all stored terms in all taxonomies, save them to the DB.
	 *
	 * @param WC_Product $product Product object.
	 * @param bool       $force Force update. Used during create.
	 * @since 3.0.0
	 */
	protected function update_terms( &$product, $force = false ) {
		$changes = $product->get_changes();

		if ( $force || array_key_exists( 'category_ids', $changes ) ) {
			$categories = $product->get_category_ids( 'edit' );

			if ( empty( $categories ) && get_option( 'default_product_cat', 0 ) ) {
				$categories = array( get_option( 'default_product_cat', 0 ) );
			}

			wp_set_post_terms( $product->get_id(), $categories, 'product_cat', false );
		}
		if ( $force || array_key_exists( 'tag_ids', $changes ) ) {
			wp_set_post_terms( $product->get_id(), $product->get_tag_ids( 'edit' ), 'product_tag', false );
		}
		if ( $force || array_key_exists( 'shipping_class_id', $changes ) ) {
			wp_set_post_terms( $product->get_id(), array( $product->get_shipping_class_id( 'edit' ) ), 'product_shipping_class', false );
		}

		_wc_recount_terms_by_product( $product->get_id() );
	}

	/**
	 * Update visibility terms based on props.
	 *
	 * @since 3.0.0
	 *
	 * @param WC_Product $product Product object.
	 * @param bool       $force Force update. Used during create.
	 */
	protected function update_visibility( &$product, $force = false ) {
		$changes = $product->get_changes();

		if ( $force || array_intersect( array( 'featured', 'stock_status', 'average_rating', 'catalog_visibility' ), array_keys( $changes ) ) ) {
			$terms = array();

			if ( $product->get_featured() ) {
				$terms[] = 'featured';
			}

			if ( 'outofstock' === $product->get_stock_status() ) {
				$terms[] = 'outofstock';
			}

			$rating = min( 5, NumberUtil::round( $product->get_average_rating(), 0 ) );

			if ( $rating > 0 ) {
				$terms[] = 'rated-' . $rating;
			}

			switch ( $product->get_catalog_visibility() ) {
				case 'hidden':
					$terms[] = 'exclude-from-search';
					$terms[] = 'exclude-from-catalog';
					break;
				case 'catalog':
					$terms[] = 'exclude-from-search';
					break;
				case 'search':
					$terms[] = 'exclude-from-catalog';
					break;
			}

			if ( ! is_wp_error( wp_set_post_terms( $product->get_id(), $terms, 'product_visibility', false ) ) ) {
				do_action( 'woocommerce_product_set_visibility', $product->get_id(), $product->get_catalog_visibility() );
			}
		}
	}

	/**
	 * Update attributes which are a mix of terms and meta data.
	 *
	 * @param WC_Product $product Product object.
	 * @param bool       $force Force update. Used during create.
	 * @since 3.0.0
	 */
	protected function update_attributes( &$product, $force = false ) {
		$changes = $product->get_changes();

		if ( $force || array_key_exists( 'attributes', $changes ) ) {
			$attributes  = $product->get_attributes();
			$meta_values = array();

			if ( $attributes ) {
				foreach ( $attributes as $attribute_key => $attribute ) {
					$value = '';

					if ( is_null( $attribute ) ) {
						if ( taxonomy_exists( $attribute_key ) ) {
							// Handle attributes that have been unset.
							wp_set_object_terms( $product->get_id(), array(), $attribute_key );
						} elseif ( taxonomy_exists( urldecode( $attribute_key ) ) ) {
							// Handle attributes that have been unset.
							wp_set_object_terms( $product->get_id(), array(), urldecode( $attribute_key ) );
						}
						continue;

					} elseif ( $attribute->is_taxonomy() ) {
						wp_set_object_terms( $product->get_id(), wp_list_pluck( (array) $attribute->get_terms(), 'term_id' ), $attribute->get_name() );
					} else {
						$value = wc_implode_text_attributes( $attribute->get_options() );
					}

					// Store in format WC uses in meta.
					$meta_values[ $attribute_key ] = array(
						'name'         => $attribute->get_name(),
						'value'        => $value,
						'position'     => $attribute->get_position(),
						'is_visible'   => $attribute->get_visible() ? 1 : 0,
						'is_variation' => $attribute->get_variation() ? 1 : 0,
						'is_taxonomy'  => $attribute->is_taxonomy() ? 1 : 0,
					);
				}
			}
			// Note, we use wp_slash to add extra level of escaping. See https://codex.wordpress.org/Function_Reference/update_post_meta#Workaround.
			$this->update_or_delete_post_meta( $product, '_product_attributes', wp_slash( $meta_values ) );
		}
	}

	/**
	 * Update downloads.
	 *
	 * @since 3.0.0
	 * @param WC_Product $product Product object.
	 * @param bool       $force Force update. Used during create.
	 * @return bool If updated or not.
	 */
	protected function update_downloads( &$product, $force = false ) {
		$changes = $product->get_changes();

		if ( $force || array_key_exists( 'downloads', $changes ) ) {
			$downloads   = $product->get_downloads();
			$meta_values = array();

			if ( $downloads ) {
				foreach ( $downloads as $key => $download ) {
					// Store in format WC uses in meta.
					$meta_values[ $key ] = $download->get_data();
				}
			}

			if ( $product->is_type( 'variation' ) ) {
				do_action( 'woocommerce_process_product_file_download_paths', $product->get_parent_id(), $product->get_id(), $downloads );
			} else {
				do_action( 'woocommerce_process_product_file_download_paths', $product->get_id(), 0, $downloads );
			}

			return $this->update_or_delete_post_meta( $product, '_downloadable_files', wp_slash( $meta_values ) );
		}
		return false;
	}

	/**
	 * Make sure we store the product type and version (to track data changes).
	 *
	 * @param WC_Product $product Product object.
	 * @since 3.0.0
	 */
	protected function update_version_and_type( &$product ) {
		$old_type = WC_Product_Factory::get_product_type( $product->get_id() );
		$new_type = $product->get_type();

		wp_set_object_terms( $product->get_id(), $new_type, 'product_type' );
		update_post_meta( $product->get_id(), '_product_version', Constants::get_constant( 'WC_VERSION' ) );

		// Action for the transition.
		if ( $old_type !== $new_type ) {
			$this->updated_props[] = 'product_type';
			do_action( 'woocommerce_product_type_changed', $product, $old_type, $new_type );
		}
	}

	/**
	 * Clear any caches.
	 *
	 * @param WC_Product $product Product object.
	 * @since 3.0.0
	 */
	protected function clear_caches( &$product ) {
		wc_delete_product_transients( $product->get_id() );
		if ( $product->get_parent_id( 'edit' ) ) {
			wc_delete_product_transients( $product->get_parent_id( 'edit' ) );
			WC_Cache_Helper::invalidate_cache_group( 'product_' . $product->get_parent_id( 'edit' ) );
		}
		WC_Cache_Helper::invalidate_attribute_count( array_keys( $product->get_attributes() ) );
		WC_Cache_Helper::invalidate_cache_group( 'product_' . $product->get_id() );
	}

	/*
	|--------------------------------------------------------------------------
	| wc-product-functions.php methods
	|--------------------------------------------------------------------------
	*/

	/**
	 * Returns an array of on sale products, as an array of objects with an
	 * ID and parent_id present. Example: $return[0]->id, $return[0]->parent_id.
	 *
	 * @return array
	 * @since 3.0.0
	 */
	public function get_on_sale_products() {
		global $wpdb;

		$exclude_term_ids            = array();
		$outofstock_join             = '';
		$outofstock_where            = '';
		$non_published_where         = '';
		$product_visibility_term_ids = wc_get_product_visibility_term_ids();

		if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && $product_visibility_term_ids['outofstock'] ) {
			$exclude_term_ids[] = $product_visibility_term_ids['outofstock'];
		}

		if ( count( $exclude_term_ids ) ) {
			$outofstock_join  = " LEFT JOIN ( SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( " . implode( ',', array_map( 'absint', $exclude_term_ids ) ) . ' ) ) AS exclude_join ON exclude_join.object_id = id';
			$outofstock_where = ' AND exclude_join.object_id IS NULL';
		}

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		return $wpdb->get_results(
			"
			SELECT posts.ID as id, posts.post_parent as parent_id
			FROM {$wpdb->posts} AS posts
			INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id
			$outofstock_join
			WHERE posts.post_type IN ( 'product', 'product_variation' )
			AND posts.post_status = 'publish'
			AND lookup.onsale = 1
			$outofstock_where
			AND posts.post_parent NOT IN (
				SELECT ID FROM `$wpdb->posts` as posts
				WHERE posts.post_type = 'product'
				AND posts.post_parent = 0
				AND posts.post_status != 'publish'
			)
			GROUP BY posts.ID
			"
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
	}

	/**
	 * Returns a list of product IDs ( id as key => parent as value) that are
	 * featured. Uses get_posts instead of wc_get_products since we want
	 * some extra meta queries and ALL products (posts_per_page = -1).
	 *
	 * @return array
	 * @since 3.0.0
	 */
	public function get_featured_product_ids() {
		$product_visibility_term_ids = wc_get_product_visibility_term_ids();

		return get_posts(
			array(
				'post_type'      => array( 'product', 'product_variation' ),
				'posts_per_page' => -1,
				'post_status'    => 'publish',
				'tax_query'      => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
					'relation' => 'AND',
					array(
						'taxonomy' => 'product_visibility',
						'field'    => 'term_taxonomy_id',
						'terms'    => array( $product_visibility_term_ids['featured'] ),
					),
					array(
						'taxonomy' => 'product_visibility',
						'field'    => 'term_taxonomy_id',
						'terms'    => array( $product_visibility_term_ids['exclude-from-catalog'] ),
						'operator' => 'NOT IN',
					),
				),
				'fields'         => 'id=>parent',
			)
		);
	}

	/**
	 * Check if product sku is found for any other product IDs.
	 *
	 * @since 3.0.0
	 * @param int    $product_id Product ID.
	 * @param string $sku Will be slashed to work around https://core.trac.wordpress.org/ticket/27421.
	 * @return bool
	 */
	public function is_existing_sku( $product_id, $sku ) {
		global $wpdb;

		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
		return (bool) $wpdb->get_var(
			$wpdb->prepare(
				"
				SELECT posts.ID
				FROM {$wpdb->posts} as posts
				INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id
				WHERE
				posts.post_type IN ( 'product', 'product_variation' )
				AND posts.post_status != 'trash'
				AND lookup.sku = %s
				AND lookup.product_id <> %d
				LIMIT 1
				",
				wp_slash( $sku ),
				$product_id
			)
		);
	}

	/**
	 * Return product ID based on SKU.
	 *
	 * @since 3.0.0
	 * @param string $sku Product SKU.
	 * @return int
	 */
	public function get_product_id_by_sku( $sku ) {
		global $wpdb;

		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
		$id = $wpdb->get_var(
			$wpdb->prepare(
				"
				SELECT posts.ID
				FROM {$wpdb->posts} as posts
				INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id
				WHERE
				posts.post_type IN ( 'product', 'product_variation' )
				AND posts.post_status != 'trash'
				AND lookup.sku = %s
				LIMIT 1
				",
				$sku
			)
		);

		return (int) apply_filters( 'woocommerce_get_product_id_by_sku', $id, $sku );
	}

	/**
	 * Returns an array of IDs of products that have sales starting soon.
	 *
	 * @since 3.0.0
	 * @return array
	 */
	public function get_starting_sales() {
		global $wpdb;

		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
		return $wpdb->get_col(
			$wpdb->prepare(
				"SELECT postmeta.post_id FROM {$wpdb->postmeta} as postmeta
				LEFT JOIN {$wpdb->postmeta} as postmeta_2 ON postmeta.post_id = postmeta_2.post_id
				LEFT JOIN {$wpdb->postmeta} as postmeta_3 ON postmeta.post_id = postmeta_3.post_id
				WHERE postmeta.meta_key = '_sale_price_dates_from'
					AND postmeta_2.meta_key = '_price'
					AND postmeta_3.meta_key = '_sale_price'
					AND postmeta.meta_value > 0
					AND postmeta.meta_value < %s
					AND postmeta_2.meta_value != postmeta_3.meta_value",
				time()
			)
		);
	}

	/**
	 * Returns an array of IDs of products that have sales which are due to end.
	 *
	 * @since 3.0.0
	 * @return array
	 */
	public function get_ending_sales() {
		global $wpdb;

		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
		return $wpdb->get_col(
			$wpdb->prepare(
				"SELECT postmeta.post_id FROM {$wpdb->postmeta} as postmeta
				LEFT JOIN {$wpdb->postmeta} as postmeta_2 ON postmeta.post_id = postmeta_2.post_id
				LEFT JOIN {$wpdb->postmeta} as postmeta_3 ON postmeta.post_id = postmeta_3.post_id
				WHERE postmeta.meta_key = '_sale_price_dates_to'
					AND postmeta_2.meta_key = '_price'
					AND postmeta_3.meta_key = '_regular_price'
					AND postmeta.meta_value > 0
					AND postmeta.meta_value < %s
					AND postmeta_2.meta_value != postmeta_3.meta_value",
				time()
			)
		);
	}

	/**
	 * Find a matching (enabled) variation within a variable product.
	 *
	 * @since  3.0.0
	 * @param  WC_Product $product Variable product.
	 * @param  array      $match_attributes Array of attributes we want to try to match.
	 * @return int Matching variation ID or 0.
	 */
	public function find_matching_product_variation( $product, $match_attributes = array() ) {
		global $wpdb;

		$meta_attribute_names = array();

		// Get attributes to match in meta.
		foreach ( $product->get_attributes() as $attribute ) {
			if ( ! $attribute->get_variation() ) {
				continue;
			}
			$meta_attribute_names[] = 'attribute_' . sanitize_title( $attribute->get_name() );
		}

		// Get the attributes of the variations.
		$query = $wpdb->prepare(
			"
			SELECT postmeta.post_id, postmeta.meta_key, postmeta.meta_value, posts.menu_order FROM {$wpdb->postmeta} as postmeta
			LEFT JOIN {$wpdb->posts} as posts ON postmeta.post_id=posts.ID
			WHERE postmeta.post_id IN (
				SELECT ID FROM {$wpdb->posts}
				WHERE {$wpdb->posts}.post_parent = %d
				AND {$wpdb->posts}.post_status = 'publish'
				AND {$wpdb->posts}.post_type = 'product_variation'
			)
			",
			$product->get_id()
		);

		$query .= " AND postmeta.meta_key IN ( '" . implode( "','", array_map( 'esc_sql', $meta_attribute_names ) ) . "' )";

		$query .= ' ORDER BY posts.menu_order ASC, postmeta.post_id ASC;';

		$attributes = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

		if ( ! $attributes ) {
			return 0;
		}

		$sorted_meta = array();

		foreach ( $attributes as $m ) {
			$sorted_meta[ $m->post_id ][ $m->meta_key ] = $m->meta_value; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
		}

		/**
		 * Check each variation to find the one that matches the $match_attributes.
		 *
		 * Note: Not all meta fields will be set which is why we check existance.
		 */
		foreach ( $sorted_meta as $variation_id => $variation ) {
			$match = true;

			// Loop over the variation meta keys and values i.e. what is saved to the products. Note: $attribute_value is empty when 'any' is in use.
			foreach ( $variation as $attribute_key => $attribute_value ) {
				$match_any_value = '' === $attribute_value;

				if ( ! $match_any_value && ! array_key_exists( $attribute_key, $match_attributes ) ) {
					$match = false; // Requires a selection but no value was provide.
				}

				if ( array_key_exists( $attribute_key, $match_attributes ) ) { // Value to match was provided.
					if ( ! $match_any_value && $match_attributes[ $attribute_key ] !== $attribute_value ) {
						$match = false; // Provided value does not match variation.
					}
				}
			}

			if ( true === $match ) {
				return $variation_id;
			}
		}

		if ( version_compare( get_post_meta( $product->get_id(), '_product_version', true ), '2.4.0', '<' ) ) {
			/**
			 * Pre 2.4 handling where 'slugs' were saved instead of the full text attribute.
			 * Fallback is here because there are cases where data will be 'synced' but the product version will remain the same.
			 */
			return ( array_map( 'sanitize_title', $match_attributes ) === $match_attributes ) ? 0 : $this->find_matching_product_variation( $product, array_map( 'sanitize_title', $match_attributes ) );
		}

		return 0;
	}

	/**
	 * Creates all possible combinations of variations from the attributes, without creating duplicates.
	 *
	 * @since  3.6.0
	 * @todo   Add to interface in 4.0.
	 * @param  WC_Product $product Variable product.
	 * @param  int        $limit Limit the number of created variations.
	 * @return int        Number of created variations.
	 */
	public function create_all_product_variations( $product, $limit = -1 ) {
		$count = 0;

		if ( ! $product ) {
			return $count;
		}

		$attributes = wc_list_pluck( array_filter( $product->get_attributes(), 'wc_attributes_array_filter_variation' ), 'get_slugs' );

		if ( empty( $attributes ) ) {
			return $count;
		}

		// Get existing variations so we don't create duplicates.
		$existing_variations = array_map( 'wc_get_product', $product->get_children() );
		$existing_attributes = array();

		foreach ( $existing_variations as $existing_variation ) {
			$existing_attributes[] = $existing_variation->get_attributes();
		}

		$possible_attributes = array_reverse( wc_array_cartesian( $attributes ) );

		foreach ( $possible_attributes as $possible_attribute ) {
			// Allow any order if key/values -- do not use strict mode.
			if ( in_array( $possible_attribute, $existing_attributes ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
				continue;
			}
			$variation = wc_get_product_object( 'variation' );
			$variation->set_parent_id( $product->get_id() );
			$variation->set_attributes( $possible_attribute );
			$variation_id = $variation->save();

			do_action( 'product_variation_linked', $variation_id );

			$count ++;

			if ( $limit > 0 && $count >= $limit ) {
				break;
			}
		}

		return $count;
	}

	/**
	 * Make sure all variations have a sort order set so they can be reordered correctly.
	 *
	 * @param int $parent_id Product ID.
	 */
	public function sort_all_product_variations( $parent_id ) {
		global $wpdb;

		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
		$ids   = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product_variation' AND post_parent = %d AND post_status = 'publish' ORDER BY menu_order ASC, ID ASC",
				$parent_id
			)
		);
		$index = 1;

		foreach ( $ids as $id ) {
			// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
			$wpdb->update( $wpdb->posts, array( 'menu_order' => ( $index++ ) ), array( 'ID' => absint( $id ) ) );
		}
	}

	/**
	 * Return a list of related products (using data like categories and IDs).
	 *
	 * @since 3.0.0
	 * @param array $cats_array  List of categories IDs.
	 * @param array $tags_array  List of tags IDs.
	 * @param array $exclude_ids Excluded IDs.
	 * @param int   $limit       Limit of results.
	 * @param int   $product_id  Product ID.
	 * @return array
	 */
	public function get_related_products( $cats_array, $tags_array, $exclude_ids, $limit, $product_id ) {
		global $wpdb;

		$args = array(
			'categories'  => $cats_array,
			'tags'        => $tags_array,
			'exclude_ids' => $exclude_ids,
			'limit'       => $limit + 10,
		);

		$related_product_query = (array) apply_filters( 'woocommerce_product_related_posts_query', $this->get_related_products_query( $cats_array, $tags_array, $exclude_ids, $limit + 10 ), $product_id, $args );

		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared
		return $wpdb->get_col( implode( ' ', $related_product_query ) );
	}

	/**
	 * Builds the related posts query.
	 *
	 * @since 3.0.0
	 *
	 * @param array $cats_array  List of categories IDs.
	 * @param array $tags_array  List of tags IDs.
	 * @param array $exclude_ids Excluded IDs.
	 * @param int   $limit       Limit of results.
	 *
	 * @return array
	 */
	public function get_related_products_query( $cats_array, $tags_array, $exclude_ids, $limit ) {
		global $wpdb;

		$include_term_ids            = array_merge( $cats_array, $tags_array );
		$exclude_term_ids            = array();
		$product_visibility_term_ids = wc_get_product_visibility_term_ids();

		if ( $product_visibility_term_ids['exclude-from-catalog'] ) {
			$exclude_term_ids[] = $product_visibility_term_ids['exclude-from-catalog'];
		}

		if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && $product_visibility_term_ids['outofstock'] ) {
			$exclude_term_ids[] = $product_visibility_term_ids['outofstock'];
		}

		$query = array(
			'fields' => "
				SELECT DISTINCT ID FROM {$wpdb->posts} p
			",
			'join'   => '',
			'where'  => "
				WHERE 1=1
				AND p.post_status = 'publish'
				AND p.post_type = 'product'

			",
			'limits' => '
				LIMIT ' . absint( $limit ) . '
			',
		);

		if ( count( $exclude_term_ids ) ) {
			$query['join']  .= " LEFT JOIN ( SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( " . implode( ',', array_map( 'absint', $exclude_term_ids ) ) . ' ) ) AS exclude_join ON exclude_join.object_id = p.ID';
			$query['where'] .= ' AND exclude_join.object_id IS NULL';
		}

		if ( count( $include_term_ids ) ) {
			$query['join'] .= " INNER JOIN ( SELECT object_id FROM {$wpdb->term_relationships} INNER JOIN {$wpdb->term_taxonomy} using( term_taxonomy_id ) WHERE term_id IN ( " . implode( ',', array_map( 'absint', $include_term_ids ) ) . ' ) ) AS include_join ON include_join.object_id = p.ID';
		}

		if ( count( $exclude_ids ) ) {
			$query['where'] .= ' AND p.ID NOT IN ( ' . implode( ',', array_map( 'absint', $exclude_ids ) ) . ' )';
		}

		return $query;
	}

	/**
	 * Update a product's stock amount directly in the database.
	 *
	 * Updates both post meta and lookup tables. Ignores manage stock setting on the product.
	 *
	 * @param int            $product_id_with_stock Product ID.
	 * @param int|float|null $stock_quantity        Stock quantity.
	 */
	protected function set_product_stock( $product_id_with_stock, $stock_quantity ) {
		global $wpdb;

		// Generate SQL.
		$sql = $wpdb->prepare(
			"UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='_stock'",
			$stock_quantity,
			$product_id_with_stock
		);

		$sql = apply_filters( 'woocommerce_update_product_stock_query', $sql, $product_id_with_stock, $stock_quantity, 'set' );

		$wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared

		// Cache delete is required (not only) to set correct data for lookup table (which reads from cache).
		// Sometimes I wonder if it shouldn't be part of update_lookup_table.
		wp_cache_delete( $product_id_with_stock, 'post_meta' );

		$this->update_lookup_table( $product_id_with_stock, 'wc_product_meta_lookup' );
	}

	/**
	 * Update a product's stock amount directly.
	 *
	 * Uses queries rather than update_post_meta so we can do this in one query (to avoid stock issues).
	 * Ignores manage stock setting on the product and sets quantities directly in the db: post meta and lookup tables.
	 * Uses locking to update the quantity. If the lock is not acquired, change is lost.
	 *
	 * @since  3.0.0 this supports set, increase and decrease.
	 * @param  int            $product_id_with_stock Product ID.
	 * @param  int|float|null $stock_quantity Stock quantity.
	 * @param  string         $operation Set, increase and decrease.
	 * @return int|float New stock level.
	 */
	public function update_product_stock( $product_id_with_stock, $stock_quantity = null, $operation = 'set' ) {
		global $wpdb;

		// Ensures a row exists to update.
		add_post_meta( $product_id_with_stock, '_stock', 0, true );

		if ( 'set' === $operation ) {
			$new_stock = wc_stock_amount( $stock_quantity );

			// Generate SQL.
			$sql = $wpdb->prepare(
				"UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='_stock'",
				$new_stock,
				$product_id_with_stock
			);
		} else {
			$current_stock = wc_stock_amount(
				$wpdb->get_var(
					$wpdb->prepare(
						"SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key='_stock';",
						$product_id_with_stock
					)
				)
			);

			// Calculate new value for filter below. Set multiplier to subtract or add the meta_value.
			switch ( $operation ) {
				case 'increase':
					$new_stock  = $current_stock + wc_stock_amount( $stock_quantity );
					$multiplier = 1;
					break;
				default:
					$new_stock  = $current_stock - wc_stock_amount( $stock_quantity );
					$multiplier = -1;
					break;
			}

			// Generate SQL.
			$sql = $wpdb->prepare(
				"UPDATE {$wpdb->postmeta} SET meta_value = meta_value %+f WHERE post_id = %d AND meta_key='_stock'",
				wc_stock_amount( $stock_quantity ) * $multiplier, // This will either subtract or add depending on operation.
				$product_id_with_stock
			);
		}

		$sql = apply_filters( 'woocommerce_update_product_stock_query', $sql, $product_id_with_stock, $new_stock, $operation );

		$wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared

		// Cache delete is required (not only) to set correct data for lookup table (which reads from cache).
		// Sometimes I wonder if it shouldn't be part of update_lookup_table.
		wp_cache_delete( $product_id_with_stock, 'post_meta' );

		$this->update_lookup_table( $product_id_with_stock, 'wc_product_meta_lookup' );

		/**
		 * Fire an action for this direct update so it can be detected by other code.
		 *
		 * @since 3.6
		 * @param int $product_id_with_stock Product ID that was updated directly.
		 */
		do_action( 'woocommerce_updated_product_stock', $product_id_with_stock );

		return $new_stock;
	}

	/**
	 * Update a product's sale count directly.
	 *
	 * Uses queries rather than update_post_meta so we can do this in one query for performance.
	 *
	 * @since  3.0.0 this supports set, increase and decrease.
	 * @param  int      $product_id Product ID.
	 * @param  int|null $quantity Quantity.
	 * @param  string   $operation set, increase and decrease.
	 */
	public function update_product_sales( $product_id, $quantity = null, $operation = 'set' ) {
		global $wpdb;
		add_post_meta( $product_id, 'total_sales', 0, true );

		// Update stock in DB directly.
		switch ( $operation ) {
			case 'increase':
				// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
				$wpdb->query(
					$wpdb->prepare(
						"UPDATE {$wpdb->postmeta} SET meta_value = meta_value + %f WHERE post_id = %d AND meta_key='total_sales'",
						$quantity,
						$product_id
					)
				);
				break;
			case 'decrease':
				// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
				$wpdb->query(
					$wpdb->prepare(
						"UPDATE {$wpdb->postmeta} SET meta_value = meta_value - %f WHERE post_id = %d AND meta_key='total_sales'",
						$quantity,
						$product_id
					)
				);
				break;
			default:
				// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
				$wpdb->query(
					$wpdb->prepare(
						"UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='total_sales'",
						$quantity,
						$product_id
					)
				);
				break;
		}

		wp_cache_delete( $product_id, 'post_meta' );

		$this->update_lookup_table( $product_id, 'wc_product_meta_lookup' );

		/**
		 * Fire an action for this direct update so it can be detected by other code.
		 *
		 * @since 3.6
		 * @param int $product_id Product ID that was updated directly.
		 */
		do_action( 'woocommerce_updated_product_sales', $product_id );
	}

	/**
	 * Update a products average rating meta.
	 *
	 * @since 3.0.0
	 * @todo Deprecate unused function?
	 * @param WC_Product $product Product object.
	 */
	public function update_average_rating( $product ) {
		update_post_meta( $product->get_id(), '_wc_average_rating', $product->get_average_rating( 'edit' ) );
		self::update_visibility( $product, true );
	}

	/**
	 * Update a products review count meta.
	 *
	 * @since 3.0.0
	 * @todo Deprecate unused function?
	 * @param WC_Product $product Product object.
	 */
	public function update_review_count( $product ) {
		update_post_meta( $product->get_id(), '_wc_review_count', $product->get_review_count( 'edit' ) );
	}

	/**
	 * Update a products rating counts.
	 *
	 * @since 3.0.0
	 * @todo Deprecate unused function?
	 * @param WC_Product $product Product object.
	 */
	public function update_rating_counts( $product ) {
		update_post_meta( $product->get_id(), '_wc_rating_count', $product->get_rating_counts( 'edit' ) );
	}

	/**
	 * Get shipping class ID by slug.
	 *
	 * @since 3.0.0
	 * @param string $slug Product shipping class slug.
	 * @return int|false
	 */
	public function get_shipping_class_id_by_slug( $slug ) {
		$shipping_class_term = get_term_by( 'slug', $slug, 'product_shipping_class' );
		if ( $shipping_class_term ) {
			return $shipping_class_term->term_id;
		} else {
			return false;
		}
	}

	/**
	 * Returns an array of products.
	 *
	 * @param  array $args Args to pass to WC_Product_Query().
	 * @return array|object
	 * @see wc_get_products
	 */
	public function get_products( $args = array() ) {
		$query = new WC_Product_Query( $args );
		return $query->get_products();
	}

	/**
	 * Search product data for a term and return ids.
	 *
	 * @param  string     $term Search term.
	 * @param  string     $type Type of product.
	 * @param  bool       $include_variations Include variations in search or not.
	 * @param  bool       $all_statuses Should we search all statuses or limit to published.
	 * @param  null|int   $limit Limit returned results. @since 3.5.0.
	 * @param  null|array $include Keep specific results. @since 3.6.0.
	 * @param  null|array $exclude Discard specific results. @since 3.6.0.
	 * @return array of ids
	 */
	public function search_products( $term, $type = '', $include_variations = false, $all_statuses = false, $limit = null, $include = null, $exclude = null ) {
		global $wpdb;

		$custom_results = apply_filters( 'woocommerce_product_pre_search_products', false, $term, $type, $include_variations, $all_statuses, $limit );

		if ( is_array( $custom_results ) ) {
			return $custom_results;
		}

		$post_types   = $include_variations ? array( 'product', 'product_variation' ) : array( 'product' );
		$join_query   = '';
		$type_where   = '';
		$status_where = '';
		$limit_query  = '';

		// When searching variations we should include the parent's meta table for use in searches.
		if ( $include_variations ) {
			$join_query = " LEFT JOIN {$wpdb->wc_product_meta_lookup} parent_wc_product_meta_lookup
			 ON posts.post_type = 'product_variation' AND parent_wc_product_meta_lookup.product_id = posts.post_parent ";
		}

		/**
		 * Hook woocommerce_search_products_post_statuses.
		 *
		 * @since 3.7.0
		 * @param array $post_statuses List of post statuses.
		 */
		$post_statuses = apply_filters(
			'woocommerce_search_products_post_statuses',
			current_user_can( 'edit_private_products' ) ? array( 'private', 'publish' ) : array( 'publish' )
		);

		// See if search term contains OR keywords.
		if ( stristr( $term, ' or ' ) ) {
			$term_groups = preg_split( '/\s+or\s+/i', $term );
		} else {
			$term_groups = array( $term );
		}

		$search_where   = '';
		$search_queries = array();

		foreach ( $term_groups as $term_group ) {
			// Parse search terms.
			if ( preg_match_all( '/".*?("|$)|((?<=[\t ",+])|^)[^\t ",+]+/', $term_group, $matches ) ) {
				$search_terms = $this->get_valid_search_terms( $matches[0] );
				$count        = count( $search_terms );

				// if the search string has only short terms or stopwords, or is 10+ terms long, match it as sentence.
				if ( 9 < $count || 0 === $count ) {
					$search_terms = array( $term_group );
				}
			} else {
				$search_terms = array( $term_group );
			}

			$term_group_query = '';
			$searchand        = '';

			foreach ( $search_terms as $search_term ) {
				$like = '%' . $wpdb->esc_like( $search_term ) . '%';

				// Variations should also search the parent's meta table for fallback fields.
				if ( $include_variations ) {
					$variation_query = $wpdb->prepare( " OR ( wc_product_meta_lookup.sku = '' AND parent_wc_product_meta_lookup.sku LIKE %s ) ", $like );
				} else {
					$variation_query = '';
				}

				$term_group_query .= $wpdb->prepare( " {$searchand} ( ( posts.post_title LIKE %s) OR ( posts.post_excerpt LIKE %s) OR ( posts.post_content LIKE %s ) OR ( wc_product_meta_lookup.sku LIKE %s ) $variation_query)", $like, $like, $like, $like ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				$searchand         = ' AND ';
			}

			if ( $term_group_query ) {
				$search_queries[] = $term_group_query;
			}
		}

		if ( ! empty( $search_queries ) ) {
			$search_where = ' AND (' . implode( ') OR (', $search_queries ) . ') ';
		}

		if ( ! empty( $include ) && is_array( $include ) ) {
			$search_where .= ' AND posts.ID IN(' . implode( ',', array_map( 'absint', $include ) ) . ') ';
		}

		if ( ! empty( $exclude ) && is_array( $exclude ) ) {
			$search_where .= ' AND posts.ID NOT IN(' . implode( ',', array_map( 'absint', $exclude ) ) . ') ';
		}

		if ( 'virtual' === $type ) {
			$type_where = ' AND ( wc_product_meta_lookup.virtual = 1 ) ';
		} elseif ( 'downloadable' === $type ) {
			$type_where = ' AND ( wc_product_meta_lookup.downloadable = 1 ) ';
		}

		if ( ! $all_statuses ) {
			$status_where = " AND posts.post_status IN ('" . implode( "','", $post_statuses ) . "') ";
		}

		if ( $limit ) {
			$limit_query = $wpdb->prepare( ' LIMIT %d ', $limit );
		}

		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
		$search_results = $wpdb->get_results(
			// phpcs:disable
			"SELECT DISTINCT posts.ID as product_id, posts.post_parent as parent_id FROM {$wpdb->posts} posts
			 LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON posts.ID = wc_product_meta_lookup.product_id
			 $join_query
			WHERE posts.post_type IN ('" . implode( "','", $post_types ) . "')
			$search_where
			$status_where
			$type_where
			ORDER BY posts.post_parent ASC, posts.post_title ASC
			$limit_query
			"
			// phpcs:enable
		);

		$product_ids = wp_parse_id_list( array_merge( wp_list_pluck( $search_results, 'product_id' ), wp_list_pluck( $search_results, 'parent_id' ) ) );

		if ( is_numeric( $term ) ) {
			$post_id   = absint( $term );
			$post_type = get_post_type( $post_id );

			if ( 'product_variation' === $post_type && $include_variations ) {
				$product_ids[] = $post_id;
			} elseif ( 'product' === $post_type ) {
				$product_ids[] = $post_id;
			}

			$product_ids[] = wp_get_post_parent_id( $post_id );
		}

		return wp_parse_id_list( $product_ids );
	}

	/**
	 * Get the product type based on product ID.
	 *
	 * @since 3.0.0
	 * @param int $product_id Product ID.
	 * @return bool|string
	 */
	public function get_product_type( $product_id ) {
		$cache_key    = WC_Cache_Helper::get_cache_prefix( 'product_' . $product_id ) . '_type_' . $product_id;
		$product_type = wp_cache_get( $cache_key, 'products' );

		if ( $product_type ) {
			return $product_type;
		}

		$post_type = get_post_type( $product_id );

		if ( 'product_variation' === $post_type ) {
			$product_type = 'variation';
		} elseif ( 'product' === $post_type ) {
			$terms        = get_the_terms( $product_id, 'product_type' );
			$product_type = ! empty( $terms ) && ! is_wp_error( $terms ) ? sanitize_title( current( $terms )->name ) : 'simple';
		} else {
			$product_type = false;
		}

		wp_cache_set( $cache_key, $product_type, 'products' );

		return $product_type;
	}

	/**
	 * Add ability to get products by 'reviews_allowed' in WC_Product_Query.
	 *
	 * @since 3.2.0
	 * @param string   $where Where clause.
	 * @param WP_Query $wp_query WP_Query instance.
	 * @return string
	 */
	public function reviews_allowed_query_where( $where, $wp_query ) {
		global $wpdb;

		if ( isset( $wp_query->query_vars['reviews_allowed'] ) && is_bool( $wp_query->query_vars['reviews_allowed'] ) ) {
			if ( $wp_query->query_vars['reviews_allowed'] ) {
				$where .= " AND $wpdb->posts.comment_status = 'open'";
			} else {
				$where .= " AND $wpdb->posts.comment_status = 'closed'";
			}
		}

		return $where;
	}

	/**
	 * Get valid WP_Query args from a WC_Product_Query's query variables.
	 *
	 * @since 3.2.0
	 * @param array $query_vars Query vars from a WC_Product_Query.
	 * @return array
	 */
	protected function get_wp_query_args( $query_vars ) {

		// Map query vars to ones that get_wp_query_args or WP_Query recognize.
		$key_mapping = array(
			'status'         => 'post_status',
			'page'           => 'paged',
			'include'        => 'post__in',
			'stock_quantity' => 'stock',
			'average_rating' => 'wc_average_rating',
			'review_count'   => 'wc_review_count',
		);
		foreach ( $key_mapping as $query_key => $db_key ) {
			if ( isset( $query_vars[ $query_key ] ) ) {
				$query_vars[ $db_key ] = $query_vars[ $query_key ];
				unset( $query_vars[ $query_key ] );
			}
		}

		// Map boolean queries that are stored as 'yes'/'no' in the DB to 'yes' or 'no'.
		$boolean_queries = array(
			'virtual',
			'downloadable',
			'sold_individually',
			'manage_stock',
		);
		foreach ( $boolean_queries as $boolean_query ) {
			if ( isset( $query_vars[ $boolean_query ] ) && '' !== $query_vars[ $boolean_query ] ) {
				$query_vars[ $boolean_query ] = $query_vars[ $boolean_query ] ? 'yes' : 'no';
			}
		}

		// These queries cannot be auto-generated so we have to remove them and build them manually.
		$manual_queries = array(
			'sku'        => '',
			'featured'   => '',
			'visibility' => '',
		);
		foreach ( $manual_queries as $key => $manual_query ) {
			if ( isset( $query_vars[ $key ] ) ) {
				$manual_queries[ $key ] = $query_vars[ $key ];
				unset( $query_vars[ $key ] );
			}
		}

		$wp_query_args = parent::get_wp_query_args( $query_vars );

		if ( ! isset( $wp_query_args['date_query'] ) ) {
			$wp_query_args['date_query'] = array();
		}
		if ( ! isset( $wp_query_args['meta_query'] ) ) {
			$wp_query_args['meta_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
		}

		// Handle product types.
		if ( 'variation' === $query_vars['type'] ) {
			$wp_query_args['post_type'] = 'product_variation';
		} elseif ( is_array( $query_vars['type'] ) && in_array( 'variation', $query_vars['type'], true ) ) {
			$wp_query_args['post_type']   = array( 'product_variation', 'product' );
			$wp_query_args['tax_query'][] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
				'relation' => 'OR',
				array(
					'taxonomy' => 'product_type',
					'field'    => 'slug',
					'terms'    => $query_vars['type'],
				),
				array(
					'taxonomy' => 'product_type',
					'field'    => 'id',
					'operator' => 'NOT EXISTS',
				),
			);
		} else {
			$wp_query_args['post_type']   = 'product';
			$wp_query_args['tax_query'][] = array(
				'taxonomy' => 'product_type',
				'field'    => 'slug',
				'terms'    => $query_vars['type'],
			);
		}

		// Handle product categories.
		if ( ! empty( $query_vars['category'] ) ) {
			$wp_query_args['tax_query'][] = array(
				'taxonomy' => 'product_cat',
				'field'    => 'slug',
				'terms'    => $query_vars['category'],
			);
		}

		// Handle product tags.
		if ( ! empty( $query_vars['tag'] ) ) {
			unset( $wp_query_args['tag'] );
			$wp_query_args['tax_query'][] = array(
				'taxonomy' => 'product_tag',
				'field'    => 'slug',
				'terms'    => $query_vars['tag'],
			);
		}

		// Handle shipping classes.
		if ( ! empty( $query_vars['shipping_class'] ) ) {
			$wp_query_args['tax_query'][] = array(
				'taxonomy' => 'product_shipping_class',
				'field'    => 'slug',
				'terms'    => $query_vars['shipping_class'],
			);
		}

		// Handle total_sales.
		// This query doesn't get auto-generated since the meta key doesn't have the underscore prefix.
		if ( isset( $query_vars['total_sales'] ) && '' !== $query_vars['total_sales'] ) {
			$wp_query_args['meta_query'][] = array(
				'key'     => 'total_sales',
				'value'   => absint( $query_vars['total_sales'] ),
				'compare' => '=',
			);
		}

		// Handle SKU.
		if ( $manual_queries['sku'] ) {
			// Check for existing values if wildcard is used.
			if ( '*' === $manual_queries['sku'] ) {
				$wp_query_args['meta_query'][] = array(
					array(
						'key'     => '_sku',
						'compare' => 'EXISTS',
					),
					array(
						'key'     => '_sku',
						'value'   => '',
						'compare' => '!=',
					),
				);
			} else {
				$wp_query_args['meta_query'][] = array(
					'key'     => '_sku',
					'value'   => $manual_queries['sku'],
					'compare' => 'LIKE',
				);
			}
		}

		// Handle featured.
		if ( '' !== $manual_queries['featured'] ) {
			$product_visibility_term_ids = wc_get_product_visibility_term_ids();
			if ( $manual_queries['featured'] ) {
				$wp_query_args['tax_query'][] = array(
					'taxonomy' => 'product_visibility',
					'field'    => 'term_taxonomy_id',
					'terms'    => array( $product_visibility_term_ids['featured'] ),
				);
				$wp_query_args['tax_query'][] = array(
					'taxonomy' => 'product_visibility',
					'field'    => 'term_taxonomy_id',
					'terms'    => array( $product_visibility_term_ids['exclude-from-catalog'] ),
					'operator' => 'NOT IN',
				);
			} else {
				$wp_query_args['tax_query'][] = array(
					'taxonomy' => 'product_visibility',
					'field'    => 'term_taxonomy_id',
					'terms'    => array( $product_visibility_term_ids['featured'] ),
					'operator' => 'NOT IN',
				);
			}
		}

		// Handle visibility.
		if ( $manual_queries['visibility'] ) {
			switch ( $manual_queries['visibility'] ) {
				case 'search':
					$wp_query_args['tax_query'][] = array(
						'taxonomy' => 'product_visibility',
						'field'    => 'slug',
						'terms'    => array( 'exclude-from-search' ),
						'operator' => 'NOT IN',
					);
					break;
				case 'catalog':
					$wp_query_args['tax_query'][] = array(
						'taxonomy' => 'product_visibility',
						'field'    => 'slug',
						'terms'    => array( 'exclude-from-catalog' ),
						'operator' => 'NOT IN',
					);
					break;
				case 'visible':
					$wp_query_args['tax_query'][] = array(
						'taxonomy' => 'product_visibility',
						'field'    => 'slug',
						'terms'    => array( 'exclude-from-catalog', 'exclude-from-search' ),
						'operator' => 'NOT IN',
					);
					break;
				case 'hidden':
					$wp_query_args['tax_query'][] = array(
						'taxonomy' => 'product_visibility',
						'field'    => 'slug',
						'terms'    => array( 'exclude-from-catalog', 'exclude-from-search' ),
						'operator' => 'AND',
					);
					break;
			}
		}

		// Handle date queries.
		$date_queries = array(
			'date_created'      => 'post_date',
			'date_modified'     => 'post_modified',
			'date_on_sale_from' => '_sale_price_dates_from',
			'date_on_sale_to'   => '_sale_price_dates_to',
		);
		foreach ( $date_queries as $query_var_key => $db_key ) {
			if ( isset( $query_vars[ $query_var_key ] ) && '' !== $query_vars[ $query_var_key ] ) {

				// Remove any existing meta queries for the same keys to prevent conflicts.
				$existing_queries = wp_list_pluck( $wp_query_args['meta_query'], 'key', true );
				foreach ( $existing_queries as $query_index => $query_contents ) {
					unset( $wp_query_args['meta_query'][ $query_index ] );
				}

				$wp_query_args = $this->parse_date_for_wp_query( $query_vars[ $query_var_key ], $db_key, $wp_query_args );
			}
		}

		// Handle paginate.
		if ( ! isset( $query_vars['paginate'] ) || ! $query_vars['paginate'] ) {
			$wp_query_args['no_found_rows'] = true;
		}

		// Handle reviews_allowed.
		if ( isset( $query_vars['reviews_allowed'] ) && is_bool( $query_vars['reviews_allowed'] ) ) {
			add_filter( 'posts_where', array( $this, 'reviews_allowed_query_where' ), 10, 2 );
		}

		// Handle orderby.
		if ( isset( $query_vars['orderby'] ) && 'include' === $query_vars['orderby'] ) {
			$wp_query_args['orderby'] = 'post__in';
		}

		return apply_filters( 'woocommerce_product_data_store_cpt_get_products_query', $wp_query_args, $query_vars, $this );
	}

	/**
	 * Query for Products matching specific criteria.
	 *
	 * @since 3.2.0
	 *
	 * @param array $query_vars Query vars from a WC_Product_Query.
	 *
	 * @return array|object
	 */
	public function query( $query_vars ) {
		$args = $this->get_wp_query_args( $query_vars );

		if ( ! empty( $args['errors'] ) ) {
			$query = (object) array(
				'posts'         => array(),
				'found_posts'   => 0,
				'max_num_pages' => 0,
			);
		} else {
			$query = new WP_Query( $args );
		}

		if ( isset( $query_vars['return'] ) && 'objects' === $query_vars['return'] && ! empty( $query->posts ) ) {
			// Prime caches before grabbing objects.
			update_post_caches( $query->posts, array( 'product', 'product_variation' ) );
		}

		$products = ( isset( $query_vars['return'] ) && 'ids' === $query_vars['return'] ) ? $query->posts : array_filter( array_map( 'wc_get_product', $query->posts ) );

		if ( isset( $query_vars['paginate'] ) && $query_vars['paginate'] ) {
			return (object) array(
				'products'      => $products,
				'total'         => $query->found_posts,
				'max_num_pages' => $query->max_num_pages,
			);
		}

		return $products;
	}

	/**
	 * Get data to save to a lookup table.
	 *
	 * @since 3.6.0
	 * @param int    $id ID of object to update.
	 * @param string $table Lookup table name.
	 * @return array
	 */
	protected function get_data_for_lookup_table( $id, $table ) {
		if ( 'wc_product_meta_lookup' === $table ) {
			$price_meta   = (array) get_post_meta( $id, '_price', false );
			$manage_stock = get_post_meta( $id, '_manage_stock', true );
			$stock        = 'yes' === $manage_stock ? wc_stock_amount( get_post_meta( $id, '_stock', true ) ) : null;
			$price        = wc_format_decimal( get_post_meta( $id, '_price', true ) );
			$sale_price   = wc_format_decimal( get_post_meta( $id, '_sale_price', true ) );
			return array(
				'product_id'     => absint( $id ),
				'sku'            => get_post_meta( $id, '_sku', true ),
				'virtual'        => 'yes' === get_post_meta( $id, '_virtual', true ) ? 1 : 0,
				'downloadable'   => 'yes' === get_post_meta( $id, '_downloadable', true ) ? 1 : 0,
				'min_price'      => reset( $price_meta ),
				'max_price'      => end( $price_meta ),
				'onsale'         => $sale_price && $price === $sale_price ? 1 : 0,
				'stock_quantity' => $stock,
				'stock_status'   => get_post_meta( $id, '_stock_status', true ),
				'rating_count'   => array_sum( (array) get_post_meta( $id, '_wc_rating_count', true ) ),
				'average_rating' => get_post_meta( $id, '_wc_average_rating', true ),
				'total_sales'    => get_post_meta( $id, 'total_sales', true ),
				'tax_status'     => get_post_meta( $id, '_tax_status', true ),
				'tax_class'      => get_post_meta( $id, '_tax_class', true ),
			);
		}
		return array();
	}

	/**
	 * Get primary key name for lookup table.
	 *
	 * @since 3.6.0
	 * @param string $table Lookup table name.
	 * @return string
	 */
	protected function get_primary_key_for_lookup_table( $table ) {
		if ( 'wc_product_meta_lookup' === $table ) {
			return 'product_id';
		}
		return '';
	}

	/**
	 * Returns query statement for getting current `_stock` of a product.
	 *
	 * @internal MAX function below is used to make sure result is a scalar.
	 * @param int $product_id Product ID.
	 * @return string|void Query statement.
	 */
	public function get_query_for_stock( $product_id ) {
		global $wpdb;
		return $wpdb->prepare(
			"
			SELECT COALESCE ( MAX( meta_value ), 0 ) FROM $wpdb->postmeta as meta_table
			WHERE meta_table.meta_key = '_stock'
			AND meta_table.post_id = %d
			",
			$product_id
		);
	}
}
data-stores/class-wc-order-item-shipping-data-store.php000064400000004304151336044770017157 0ustar00<?php
/**
 * WC Order Item Shipping Data Store
 *
 * @version 3.0.0
 * @package WooCommerce\DataStores
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC_Order_Item_Shipping_Data_Store class.
 */
class WC_Order_Item_Shipping_Data_Store extends Abstract_WC_Order_Item_Type_Data_Store implements WC_Object_Data_Store_Interface, WC_Order_Item_Type_Data_Store_Interface {

	/**
	 * Data stored in meta keys.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $internal_meta_keys = array( 'method_id', 'instance_id', 'cost', 'total_tax', 'taxes' );

	/**
	 * Read/populate data properties specific to this order item.
	 *
	 * @since 3.0.0
	 * @param WC_Order_Item_Shipping $item Item to read to.
	 * @throws Exception If invalid shipping order item.
	 */
	public function read( &$item ) {
		parent::read( $item );
		$id = $item->get_id();
		$item->set_props(
			array(
				'method_id'   => get_metadata( 'order_item', $id, 'method_id', true ),
				'instance_id' => get_metadata( 'order_item', $id, 'instance_id', true ),
				'total'       => get_metadata( 'order_item', $id, 'cost', true ),
				'taxes'       => get_metadata( 'order_item', $id, 'taxes', true ),
			)
		);

		// BW compat.
		if ( '' === $item->get_instance_id() && strstr( $item->get_method_id(), ':' ) ) {
			$legacy_method_id = explode( ':', $item->get_method_id() );
			$item->set_method_id( $legacy_method_id[0] );
			$item->set_instance_id( $legacy_method_id[1] );
		}

		$item->set_object_read( true );
	}

	/**
	 * Saves an item's data to the database / item meta.
	 * Ran after both create and update, so $id will be set.
	 *
	 * @since 3.0.0
	 * @param WC_Order_Item_Shipping $item Item to save.
	 */
	public function save_item_data( &$item ) {
		$id                = $item->get_id();
		$changes           = $item->get_changes();
		$meta_key_to_props = array(
			'method_id'   => 'method_id',
			'instance_id' => 'instance_id',
			'cost'        => 'total',
			'total_tax'   => 'total_tax',
			'taxes'       => 'taxes',
		);
		$props_to_update   = $this->get_props_to_update( $item, $meta_key_to_props, 'order_item' );

		foreach ( $props_to_update as $meta_key => $prop ) {
			update_metadata( 'order_item', $id, $meta_key, $item->{"get_$prop"}( 'edit' ) );
		}
	}
}
data-stores/class-wc-order-item-tax-data-store.php000064400000004362151336044770016136 0ustar00<?php
/**
 * Class WC_Order_Item_Tax_Data_Store file.
 *
 * @package WooCommerce\DataStores
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Order Item Tax Data Store
 *
 * @version  3.0.0
 */
class WC_Order_Item_Tax_Data_Store extends Abstract_WC_Order_Item_Type_Data_Store implements WC_Object_Data_Store_Interface, WC_Order_Item_Type_Data_Store_Interface {

	/**
	 * Data stored in meta keys.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $internal_meta_keys = array( 'rate_id', 'label', 'compound', 'tax_amount', 'shipping_tax_amount', 'rate_percent' );

	/**
	 * Read/populate data properties specific to this order item.
	 *
	 * @since 3.0.0
	 * @param WC_Order_Item_Tax $item Tax order item object.
	 * @throws Exception If invalid order item.
	 */
	public function read( &$item ) {
		parent::read( $item );
		$id = $item->get_id();
		$item->set_props(
			array(
				'rate_id'            => get_metadata( 'order_item', $id, 'rate_id', true ),
				'label'              => get_metadata( 'order_item', $id, 'label', true ),
				'compound'           => get_metadata( 'order_item', $id, 'compound', true ),
				'tax_total'          => get_metadata( 'order_item', $id, 'tax_amount', true ),
				'shipping_tax_total' => get_metadata( 'order_item', $id, 'shipping_tax_amount', true ),
				'rate_percent'       => get_metadata( 'order_item', $id, 'rate_percent', true ),
			)
		);
		$item->set_object_read( true );
	}

	/**
	 * Saves an item's data to the database / item meta.
	 * Ran after both create and update, so $id will be set.
	 *
	 * @since 3.0.0
	 * @param WC_Order_Item_Tax $item Tax order item object.
	 */
	public function save_item_data( &$item ) {
		$id                = $item->get_id();
		$changes           = $item->get_changes();
		$meta_key_to_props = array(
			'rate_id'             => 'rate_id',
			'label'               => 'label',
			'compound'            => 'compound',
			'tax_amount'          => 'tax_total',
			'shipping_tax_amount' => 'shipping_tax_total',
			'rate_percent'        => 'rate_percent',
		);
		$props_to_update   = $this->get_props_to_update( $item, $meta_key_to_props, 'order_item' );

		foreach ( $props_to_update as $meta_key => $prop ) {
			update_metadata( 'order_item', $id, $meta_key, $item->{"get_$prop"}( 'edit' ) );
		}
	}
}
data-stores/class-wc-product-variation-data-store-cpt.php000064400000050403151336044770017530 0ustar00<?php
/**
 * Class WC_Product_Variation_Data_Store_CPT file.
 *
 * @package WooCommerce\DataStores
 */

use Automattic\Jetpack\Constants;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Variation Product Data Store: Stored in CPT.
 *
 * @version  3.0.0
 */
class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT implements WC_Object_Data_Store_Interface {

	/**
	 * Callback to remove unwanted meta data.
	 *
	 * @param object $meta Meta object.
	 * @return bool false if excluded.
	 */
	protected function exclude_internal_meta_keys( $meta ) {
		return ! in_array( $meta->meta_key, $this->internal_meta_keys, true ) && 0 !== stripos( $meta->meta_key, 'attribute_' ) && 0 !== stripos( $meta->meta_key, 'wp_' );
	}

	/*
	|--------------------------------------------------------------------------
	| CRUD Methods
	|--------------------------------------------------------------------------
	*/

	/**
	 * Reads a product from the database and sets its data to the class.
	 *
	 * @since 3.0.0
	 * @param WC_Product_Variation $product Product object.
	 * @throws WC_Data_Exception If WC_Product::set_tax_status() is called with an invalid tax status (via read_product_data), or when passing an invalid ID.
	 */
	public function read( &$product ) {
		$product->set_defaults();

		if ( ! $product->get_id() ) {
			return;
		}

		$post_object = get_post( $product->get_id() );

		if ( ! $post_object ) {
			return;
		}

		if ( 'product_variation' !== $post_object->post_type ) {
			throw new WC_Data_Exception( 'variation_invalid_id', __( 'Invalid product type: passed ID does not correspond to a product variation.', 'woocommerce' ) );
		}

		$product->set_props(
			array(
				'name'              => $post_object->post_title,
				'slug'              => $post_object->post_name,
				'date_created'      => $this->string_to_timestamp( $post_object->post_date_gmt ),
				'date_modified'     => $this->string_to_timestamp( $post_object->post_modified_gmt ),
				'status'            => $post_object->post_status,
				'menu_order'        => $post_object->menu_order,
				'reviews_allowed'   => 'open' === $post_object->comment_status,
				'parent_id'         => $post_object->post_parent,
				'attribute_summary' => $post_object->post_excerpt,
			)
		);

		// The post parent is not a valid variable product so we should prevent this.
		if ( $product->get_parent_id( 'edit' ) && 'product' !== get_post_type( $product->get_parent_id( 'edit' ) ) ) {
			$product->set_parent_id( 0 );
		}

		$this->read_downloads( $product );
		$this->read_product_data( $product );
		$this->read_extra_data( $product );
		$product->set_attributes( wc_get_product_variation_attributes( $product->get_id() ) );

		$updates = array();
		/**
		 * If a variation title is not in sync with the parent e.g. saved prior to 3.0, or if the parent title has changed, detect here and update.
		 */
		$new_title = $this->generate_product_title( $product );

		if ( $post_object->post_title !== $new_title ) {
			$product->set_name( $new_title );
			$updates = array_merge( $updates, array( 'post_title' => $new_title ) );
		}

		/**
		 * If the attribute summary is not in sync, update here. Used when searching for variations by attribute values.
		 * This is meant to also cover the case when global attribute name or value is updated, then the attribute summary is updated
		 * for respective products when they're read.
		 */
		$new_attribute_summary = $this->generate_attribute_summary( $product );

		if ( $new_attribute_summary !== $post_object->post_excerpt ) {
			$product->set_attribute_summary( $new_attribute_summary );
			$updates = array_merge( $updates, array( 'post_excerpt' => $new_attribute_summary ) );
		}

		if ( ! empty( $updates ) ) {
			$GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $updates, array( 'ID' => $product->get_id() ) );
			clean_post_cache( $product->get_id() );
		}

		// Set object_read true once all data is read.
		$product->set_object_read( true );
	}

	/**
	 * Create a new product.
	 *
	 * @since 3.0.0
	 * @param WC_Product_Variation $product Product object.
	 */
	public function create( &$product ) {
		if ( ! $product->get_date_created() ) {
			$product->set_date_created( time() );
		}

		$new_title = $this->generate_product_title( $product );

		if ( $product->get_name( 'edit' ) !== $new_title ) {
			$product->set_name( $new_title );
		}

		$attribute_summary = $this->generate_attribute_summary( $product );
		$product->set_attribute_summary( $attribute_summary );

		// The post parent is not a valid variable product so we should prevent this.
		if ( $product->get_parent_id( 'edit' ) && 'product' !== get_post_type( $product->get_parent_id( 'edit' ) ) ) {
			$product->set_parent_id( 0 );
		}

		$id = wp_insert_post(
			apply_filters(
				'woocommerce_new_product_variation_data',
				array(
					'post_type'      => 'product_variation',
					'post_status'    => $product->get_status() ? $product->get_status() : 'publish',
					'post_author'    => get_current_user_id(),
					'post_title'     => $product->get_name( 'edit' ),
					'post_excerpt'   => $product->get_attribute_summary( 'edit' ),
					'post_content'   => '',
					'post_parent'    => $product->get_parent_id(),
					'comment_status' => 'closed',
					'ping_status'    => 'closed',
					'menu_order'     => $product->get_menu_order(),
					'post_date'      => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ),
					'post_date_gmt'  => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ),
					'post_name'      => $product->get_slug( 'edit' ),
				)
			),
			true
		);

		if ( $id && ! is_wp_error( $id ) ) {
			$product->set_id( $id );

			$this->update_post_meta( $product, true );
			$this->update_terms( $product, true );
			$this->update_visibility( $product, true );
			$this->update_attributes( $product, true );
			$this->handle_updated_props( $product );

			$product->save_meta_data();
			$product->apply_changes();

			$this->update_version_and_type( $product );
			$this->update_guid( $product );

			$this->clear_caches( $product );

			do_action( 'woocommerce_new_product_variation', $id, $product );
		}
	}

	/**
	 * Updates an existing product.
	 *
	 * @since 3.0.0
	 * @param WC_Product_Variation $product Product object.
	 */
	public function update( &$product ) {
		$product->save_meta_data();

		if ( ! $product->get_date_created() ) {
			$product->set_date_created( time() );
		}

		$new_title = $this->generate_product_title( $product );

		if ( $product->get_name( 'edit' ) !== $new_title ) {
			$product->set_name( $new_title );
		}

		// The post parent is not a valid variable product so we should prevent this.
		if ( $product->get_parent_id( 'edit' ) && 'product' !== get_post_type( $product->get_parent_id( 'edit' ) ) ) {
			$product->set_parent_id( 0 );
		}

		$changes = $product->get_changes();

		if ( array_intersect( array( 'attributes' ), array_keys( $changes ) ) ) {
			$product->set_attribute_summary( $this->generate_attribute_summary( $product ) );
		}

		// Only update the post when the post data changes.
		if ( array_intersect( array( 'name', 'parent_id', 'status', 'menu_order', 'date_created', 'date_modified', 'attributes' ), array_keys( $changes ) ) ) {
			$post_data = array(
				'post_title'        => $product->get_name( 'edit' ),
				'post_excerpt'      => $product->get_attribute_summary( 'edit' ),
				'post_parent'       => $product->get_parent_id( 'edit' ),
				'comment_status'    => 'closed',
				'post_status'       => $product->get_status( 'edit' ) ? $product->get_status( 'edit' ) : 'publish',
				'menu_order'        => $product->get_menu_order( 'edit' ),
				'post_date'         => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ),
				'post_date_gmt'     => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ),
				'post_modified'     => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getOffsetTimestamp() ) : current_time( 'mysql' ),
				'post_modified_gmt' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getTimestamp() ) : current_time( 'mysql', 1 ),
				'post_type'         => 'product_variation',
				'post_name'         => $product->get_slug( 'edit' ),
			);

			/**
			 * When updating this object, to prevent infinite loops, use $wpdb
			 * to update data, since wp_update_post spawns more calls to the
			 * save_post action.
			 *
			 * This ensures hooks are fired by either WP itself (admin screen save),
			 * or an update purely from CRUD.
			 */
			if ( doing_action( 'save_post' ) ) {
				$GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $product->get_id() ) );
				clean_post_cache( $product->get_id() );
			} else {
				wp_update_post( array_merge( array( 'ID' => $product->get_id() ), $post_data ) );
			}
			$product->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook.

		} else { // Only update post modified time to record this save event.
			$GLOBALS['wpdb']->update(
				$GLOBALS['wpdb']->posts,
				array(
					'post_modified'     => current_time( 'mysql' ),
					'post_modified_gmt' => current_time( 'mysql', 1 ),
				),
				array(
					'ID' => $product->get_id(),
				)
			);
			clean_post_cache( $product->get_id() );
		}

		$this->update_post_meta( $product );
		$this->update_terms( $product );
		$this->update_visibility( $product, true );
		$this->update_attributes( $product );
		$this->handle_updated_props( $product );

		$product->apply_changes();

		$this->update_version_and_type( $product );

		$this->clear_caches( $product );

		do_action( 'woocommerce_update_product_variation', $product->get_id(), $product );
	}

	/*
	|--------------------------------------------------------------------------
	| Additional Methods
	|--------------------------------------------------------------------------
	*/

	/**
	 * Generates a title with attribute information for a variation.
	 * Products will get a title of the form "Name - Value, Value" or just "Name".
	 *
	 * @since 3.0.0
	 * @param WC_Product $product Product object.
	 * @return string
	 */
	protected function generate_product_title( $product ) {
		$attributes = (array) $product->get_attributes();

		// Do not include attributes if the product has 3+ attributes.
		$should_include_attributes = count( $attributes ) < 3;

		// Do not include attributes if an attribute name has 2+ words and the
		// product has multiple attributes.
		if ( $should_include_attributes && 1 < count( $attributes ) ) {
			foreach ( $attributes as $name => $value ) {
				if ( false !== strpos( $name, '-' ) ) {
					$should_include_attributes = false;
					break;
				}
			}
		}

		$should_include_attributes = apply_filters( 'woocommerce_product_variation_title_include_attributes', $should_include_attributes, $product );
		$separator                 = apply_filters( 'woocommerce_product_variation_title_attributes_separator', ' - ', $product );
		$title_base                = get_post_field( 'post_title', $product->get_parent_id() );
		$title_suffix              = $should_include_attributes ? wc_get_formatted_variation( $product, true, false ) : '';

		return apply_filters( 'woocommerce_product_variation_title', $title_suffix ? $title_base . $separator . $title_suffix : $title_base, $product, $title_base, $title_suffix );
	}

	/**
	 * Generates attribute summary for the variation.
	 *
	 * Attribute summary contains comma-delimited 'attribute_name: attribute_value' pairs for all attributes.
	 *
	 * @since 3.6.0
	 * @param WC_Product_Variation $product Product variation to generate the attribute summary for.
	 *
	 * @return string
	 */
	protected function generate_attribute_summary( $product ) {
		return wc_get_formatted_variation( $product, true, true );
	}

	/**
	 * Make sure we store the product version (to track data changes).
	 *
	 * @param WC_Product $product Product object.
	 * @since 3.0.0
	 */
	protected function update_version_and_type( &$product ) {
		wp_set_object_terms( $product->get_id(), '', 'product_type' );
		update_post_meta( $product->get_id(), '_product_version', Constants::get_constant( 'WC_VERSION' ) );
	}

	/**
	 * Read post data.
	 *
	 * @since 3.0.0
	 * @param WC_Product_Variation $product Product object.
	 * @throws WC_Data_Exception If WC_Product::set_tax_status() is called with an invalid tax status.
	 */
	protected function read_product_data( &$product ) {
		$id = $product->get_id();

		$product->set_props(
			array(
				'description'       => get_post_meta( $id, '_variation_description', true ),
				'regular_price'     => get_post_meta( $id, '_regular_price', true ),
				'sale_price'        => get_post_meta( $id, '_sale_price', true ),
				'date_on_sale_from' => get_post_meta( $id, '_sale_price_dates_from', true ),
				'date_on_sale_to'   => get_post_meta( $id, '_sale_price_dates_to', true ),
				'manage_stock'      => get_post_meta( $id, '_manage_stock', true ),
				'stock_status'      => get_post_meta( $id, '_stock_status', true ),
				'low_stock_amount'  => get_post_meta( $id, '_low_stock_amount', true ),
				'shipping_class_id' => current( $this->get_term_ids( $id, 'product_shipping_class' ) ),
				'virtual'           => get_post_meta( $id, '_virtual', true ),
				'downloadable'      => get_post_meta( $id, '_downloadable', true ),
				'gallery_image_ids' => array_filter( explode( ',', get_post_meta( $id, '_product_image_gallery', true ) ) ),
				'download_limit'    => get_post_meta( $id, '_download_limit', true ),
				'download_expiry'   => get_post_meta( $id, '_download_expiry', true ),
				'image_id'          => get_post_thumbnail_id( $id ),
				'backorders'        => get_post_meta( $id, '_backorders', true ),
				'sku'               => get_post_meta( $id, '_sku', true ),
				'stock_quantity'    => get_post_meta( $id, '_stock', true ),
				'weight'            => get_post_meta( $id, '_weight', true ),
				'length'            => get_post_meta( $id, '_length', true ),
				'width'             => get_post_meta( $id, '_width', true ),
				'height'            => get_post_meta( $id, '_height', true ),
				'tax_class'         => ! metadata_exists( 'post', $id, '_tax_class' ) ? 'parent' : get_post_meta( $id, '_tax_class', true ),
			)
		);

		if ( $product->is_on_sale( 'edit' ) ) {
			$product->set_price( $product->get_sale_price( 'edit' ) );
		} else {
			$product->set_price( $product->get_regular_price( 'edit' ) );
		}

		$parent_object   = get_post( $product->get_parent_id() );
		$terms           = get_the_terms( $product->get_parent_id(), 'product_visibility' );
		$term_names      = is_array( $terms ) ? wp_list_pluck( $terms, 'name' ) : array();
		$exclude_search  = in_array( 'exclude-from-search', $term_names, true );
		$exclude_catalog = in_array( 'exclude-from-catalog', $term_names, true );

		if ( $exclude_search && $exclude_catalog ) {
			$catalog_visibility = 'hidden';
		} elseif ( $exclude_search ) {
			$catalog_visibility = 'catalog';
		} elseif ( $exclude_catalog ) {
			$catalog_visibility = 'search';
		} else {
			$catalog_visibility = 'visible';
		}

		$product->set_parent_data(
			array(
				'title'              => $parent_object ? $parent_object->post_title : '',
				'status'             => $parent_object ? $parent_object->post_status : '',
				'sku'                => get_post_meta( $product->get_parent_id(), '_sku', true ),
				'manage_stock'       => get_post_meta( $product->get_parent_id(), '_manage_stock', true ),
				'backorders'         => get_post_meta( $product->get_parent_id(), '_backorders', true ),
				'stock_quantity'     => wc_stock_amount( get_post_meta( $product->get_parent_id(), '_stock', true ) ),
				'weight'             => get_post_meta( $product->get_parent_id(), '_weight', true ),
				'length'             => get_post_meta( $product->get_parent_id(), '_length', true ),
				'width'              => get_post_meta( $product->get_parent_id(), '_width', true ),
				'height'             => get_post_meta( $product->get_parent_id(), '_height', true ),
				'tax_class'          => get_post_meta( $product->get_parent_id(), '_tax_class', true ),
				'shipping_class_id'  => absint( current( $this->get_term_ids( $product->get_parent_id(), 'product_shipping_class' ) ) ),
				'image_id'           => get_post_thumbnail_id( $product->get_parent_id() ),
				'purchase_note'      => get_post_meta( $product->get_parent_id(), '_purchase_note', true ),
				'catalog_visibility' => $catalog_visibility,
			)
		);

		// Pull data from the parent when there is no user-facing way to set props.
		$product->set_sold_individually( get_post_meta( $product->get_parent_id(), '_sold_individually', true ) );
		$product->set_tax_status( get_post_meta( $product->get_parent_id(), '_tax_status', true ) );
		$product->set_cross_sell_ids( get_post_meta( $product->get_parent_id(), '_crosssell_ids', true ) );
	}

	/**
	 * For all stored terms in all taxonomies, save them to the DB.
	 *
	 * @since 3.0.0
	 * @param WC_Product $product Product object.
	 * @param bool       $force Force update. Used during create.
	 */
	protected function update_terms( &$product, $force = false ) {
		$changes = $product->get_changes();

		if ( $force || array_key_exists( 'shipping_class_id', $changes ) ) {
			wp_set_post_terms( $product->get_id(), array( $product->get_shipping_class_id( 'edit' ) ), 'product_shipping_class', false );
		}
	}

	/**
	 * Update visibility terms based on props.
	 *
	 * @since 3.0.0
	 *
	 * @param WC_Product $product Product object.
	 * @param bool       $force Force update. Used during create.
	 */
	protected function update_visibility( &$product, $force = false ) {
		$changes = $product->get_changes();

		if ( $force || array_intersect( array( 'stock_status' ), array_keys( $changes ) ) ) {
			$terms = array();

			if ( 'outofstock' === $product->get_stock_status() ) {
				$terms[] = 'outofstock';
			}

			wp_set_post_terms( $product->get_id(), $terms, 'product_visibility', false );
		}
	}

	/**
	 * Update attribute meta values.
	 *
	 * @since 3.0.0
	 * @param WC_Product $product Product object.
	 * @param bool       $force Force update. Used during create.
	 */
	protected function update_attributes( &$product, $force = false ) {
		$changes = $product->get_changes();

		if ( $force || array_key_exists( 'attributes', $changes ) ) {
			global $wpdb;

			$product_id             = $product->get_id();
			$attributes             = $product->get_attributes();
			$updated_attribute_keys = array();
			foreach ( $attributes as $key => $value ) {
				update_post_meta( $product_id, 'attribute_' . $key, wp_slash( $value ) );
				$updated_attribute_keys[] = 'attribute_' . $key;
			}

			// Remove old taxonomies attributes so data is kept up to date - first get attribute key names.
			$delete_attribute_keys = $wpdb->get_col(
				$wpdb->prepare(
					// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.QuotedDynamicPlaceholderGeneration
					"SELECT meta_key FROM {$wpdb->postmeta} WHERE meta_key LIKE %s AND meta_key NOT IN ( '" . implode( "','", array_map( 'esc_sql', $updated_attribute_keys ) ) . "' ) AND post_id = %d",
					$wpdb->esc_like( 'attribute_' ) . '%',
					$product_id
				)
			);

			foreach ( $delete_attribute_keys as $key ) {
				delete_post_meta( $product_id, $key );
			}
		}
	}

	/**
	 * Helper method that updates all the post meta for a product based on it's settings in the WC_Product class.
	 *
	 * @since 3.0.0
	 * @param WC_Product $product Product object.
	 * @param bool       $force Force update. Used during create.
	 */
	public function update_post_meta( &$product, $force = false ) {
		$meta_key_to_props = array(
			'_variation_description' => 'description',
		);

		$props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $product, $meta_key_to_props );

		foreach ( $props_to_update as $meta_key => $prop ) {
			$value   = $product->{"get_$prop"}( 'edit' );
			$updated = update_post_meta( $product->get_id(), $meta_key, $value );
			if ( $updated ) {
				$this->updated_props[] = $prop;
			}
		}

		parent::update_post_meta( $product, $force );
	}

	/**
	 * Update product variation guid.
	 *
	 * @param WC_Product_Variation $product Product variation object.
	 *
	 * @since 3.6.0
	 */
	protected function update_guid( $product ) {
		global $wpdb;

		$guid = home_url(
			add_query_arg(
				array(
					'post_type' => 'product_variation',
					'p'         => $product->get_id(),
				),
				''
			)
		);
		$wpdb->update( $wpdb->posts, array( 'guid' => $guid ), array( 'ID' => $product->get_id() ) );
	}
}
data-stores/class-wc-coupon-data-store-cpt.php000064400000060133151336044770015362 0ustar00<?php
/**
 * Class WC_Coupon_Data_Store_CPT file.
 *
 * @package WooCommerce\DataStores
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Coupon Data Store: Custom Post Type.
 *
 * @version  3.0.0
 */
class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Data_Store_Interface, WC_Object_Data_Store_Interface {

	/**
	 * Internal meta type used to store coupon data.
	 *
	 * @since 3.0.0
	 * @var string
	 */
	protected $meta_type = 'post';

	/**
	 * Data stored in meta keys, but not considered "meta" for a coupon.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $internal_meta_keys = array(
		'discount_type',
		'coupon_amount',
		'expiry_date',
		'date_expires',
		'usage_count',
		'individual_use',
		'product_ids',
		'exclude_product_ids',
		'usage_limit',
		'usage_limit_per_user',
		'limit_usage_to_x_items',
		'free_shipping',
		'product_categories',
		'exclude_product_categories',
		'exclude_sale_items',
		'minimum_amount',
		'maximum_amount',
		'customer_email',
		'_used_by',
		'_edit_lock',
		'_edit_last',
	);

	/**
	 * The updated coupon properties
	 *
	 * @since 4.1.0
	 * @var array
	 */
	protected $updated_props = array();

	/**
	 * Method to create a new coupon in the database.
	 *
	 * @since 3.0.0
	 * @param WC_Coupon $coupon Coupon object.
	 */
	public function create( &$coupon ) {
		if ( ! $coupon->get_date_created( 'edit' ) ) {
			$coupon->set_date_created( time() );
		}

		$coupon_id = wp_insert_post(
			apply_filters(
				'woocommerce_new_coupon_data',
				array(
					'post_type'     => 'shop_coupon',
					'post_status'   => 'publish',
					'post_author'   => get_current_user_id(),
					'post_title'    => $coupon->get_code( 'edit' ),
					'post_content'  => '',
					'post_excerpt'  => $coupon->get_description( 'edit' ),
					'post_date'     => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created()->getOffsetTimestamp() ),
					'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created()->getTimestamp() ),
				)
			),
			true
		);

		if ( $coupon_id ) {
			$coupon->set_id( $coupon_id );
			$this->update_post_meta( $coupon );
			$coupon->save_meta_data();
			$coupon->apply_changes();
			delete_transient( 'rest_api_coupons_type_count' );
			do_action( 'woocommerce_new_coupon', $coupon_id, $coupon );
		}
	}

	/**
	 * Method to read a coupon.
	 *
	 * @since 3.0.0
	 *
	 * @param WC_Coupon $coupon Coupon object.
	 *
	 * @throws Exception If invalid coupon.
	 */
	public function read( &$coupon ) {
		$coupon->set_defaults();

		$post_object = get_post( $coupon->get_id() );

		if ( ! $coupon->get_id() || ! $post_object || 'shop_coupon' !== $post_object->post_type ) {
			throw new Exception( __( 'Invalid coupon.', 'woocommerce' ) );
		}

		$coupon_id = $coupon->get_id();
		$coupon->set_props(
			array(
				'code'                        => $post_object->post_title,
				'description'                 => $post_object->post_excerpt,
				'date_created'                => $this->string_to_timestamp( $post_object->post_date_gmt ),
				'date_modified'               => $this->string_to_timestamp( $post_object->post_modified_gmt ),
				'date_expires'                => metadata_exists( 'post', $coupon_id, 'date_expires' ) ? get_post_meta( $coupon_id, 'date_expires', true ) : get_post_meta( $coupon_id, 'expiry_date', true ), // @todo: Migrate expiry_date meta to date_expires in upgrade routine.
				'discount_type'               => get_post_meta( $coupon_id, 'discount_type', true ),
				'amount'                      => get_post_meta( $coupon_id, 'coupon_amount', true ),
				'usage_count'                 => get_post_meta( $coupon_id, 'usage_count', true ),
				'individual_use'              => 'yes' === get_post_meta( $coupon_id, 'individual_use', true ),
				'product_ids'                 => array_filter( (array) explode( ',', get_post_meta( $coupon_id, 'product_ids', true ) ) ),
				'excluded_product_ids'        => array_filter( (array) explode( ',', get_post_meta( $coupon_id, 'exclude_product_ids', true ) ) ),
				'usage_limit'                 => get_post_meta( $coupon_id, 'usage_limit', true ),
				'usage_limit_per_user'        => get_post_meta( $coupon_id, 'usage_limit_per_user', true ),
				'limit_usage_to_x_items'      => 0 < get_post_meta( $coupon_id, 'limit_usage_to_x_items', true ) ? get_post_meta( $coupon_id, 'limit_usage_to_x_items', true ) : null,
				'free_shipping'               => 'yes' === get_post_meta( $coupon_id, 'free_shipping', true ),
				'product_categories'          => array_filter( (array) get_post_meta( $coupon_id, 'product_categories', true ) ),
				'excluded_product_categories' => array_filter( (array) get_post_meta( $coupon_id, 'exclude_product_categories', true ) ),
				'exclude_sale_items'          => 'yes' === get_post_meta( $coupon_id, 'exclude_sale_items', true ),
				'minimum_amount'              => get_post_meta( $coupon_id, 'minimum_amount', true ),
				'maximum_amount'              => get_post_meta( $coupon_id, 'maximum_amount', true ),
				'email_restrictions'          => array_filter( (array) get_post_meta( $coupon_id, 'customer_email', true ) ),
				'used_by'                     => array_filter( (array) get_post_meta( $coupon_id, '_used_by' ) ),
			)
		);
		$coupon->read_meta_data();
		$coupon->set_object_read( true );
		do_action( 'woocommerce_coupon_loaded', $coupon );
	}

	/**
	 * Updates a coupon in the database.
	 *
	 * @since 3.0.0
	 * @param WC_Coupon $coupon Coupon object.
	 */
	public function update( &$coupon ) {
		$coupon->save_meta_data();
		$changes = $coupon->get_changes();

		if ( array_intersect( array( 'code', 'description', 'date_created', 'date_modified' ), array_keys( $changes ) ) ) {
			$post_data = array(
				'post_title'        => $coupon->get_code( 'edit' ),
				'post_excerpt'      => $coupon->get_description( 'edit' ),
				'post_date'         => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created( 'edit' )->getOffsetTimestamp() ),
				'post_date_gmt'     => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created( 'edit' )->getTimestamp() ),
				'post_modified'     => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $coupon->get_date_modified( 'edit' )->getOffsetTimestamp() ) : current_time( 'mysql' ),
				'post_modified_gmt' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $coupon->get_date_modified( 'edit' )->getTimestamp() ) : current_time( 'mysql', 1 ),
			);

			/**
			 * When updating this object, to prevent infinite loops, use $wpdb
			 * to update data, since wp_update_post spawns more calls to the
			 * save_post action.
			 *
			 * This ensures hooks are fired by either WP itself (admin screen save),
			 * or an update purely from CRUD.
			 */
			if ( doing_action( 'save_post' ) ) {
				$GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $coupon->get_id() ) );
				clean_post_cache( $coupon->get_id() );
			} else {
				wp_update_post( array_merge( array( 'ID' => $coupon->get_id() ), $post_data ) );
			}
			$coupon->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook.
		}
		$this->update_post_meta( $coupon );
		$coupon->apply_changes();
		delete_transient( 'rest_api_coupons_type_count' );
		do_action( 'woocommerce_update_coupon', $coupon->get_id(), $coupon );
	}

	/**
	 * Deletes a coupon from the database.
	 *
	 * @since 3.0.0
	 *
	 * @param WC_Coupon $coupon Coupon object.
	 * @param array     $args Array of args to pass to the delete method.
	 */
	public function delete( &$coupon, $args = array() ) {
		$args = wp_parse_args(
			$args,
			array(
				'force_delete' => false,
			)
		);

		$id = $coupon->get_id();

		if ( ! $id ) {
			return;
		}

		if ( $args['force_delete'] ) {
			wp_delete_post( $id );

			wp_cache_delete( WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $coupon->get_code(), 'coupons' );

			$coupon->set_id( 0 );
			do_action( 'woocommerce_delete_coupon', $id );
		} else {
			wp_trash_post( $id );
			do_action( 'woocommerce_trash_coupon', $id );
		}
	}

	/**
	 * Helper method that updates all the post meta for a coupon based on it's settings in the WC_Coupon class.
	 *
	 * @param WC_Coupon $coupon Coupon object.
	 * @since 3.0.0
	 */
	private function update_post_meta( &$coupon ) {
		$meta_key_to_props = array(
			'discount_type'              => 'discount_type',
			'coupon_amount'              => 'amount',
			'individual_use'             => 'individual_use',
			'product_ids'                => 'product_ids',
			'exclude_product_ids'        => 'excluded_product_ids',
			'usage_limit'                => 'usage_limit',
			'usage_limit_per_user'       => 'usage_limit_per_user',
			'limit_usage_to_x_items'     => 'limit_usage_to_x_items',
			'usage_count'                => 'usage_count',
			'date_expires'               => 'date_expires',
			'free_shipping'              => 'free_shipping',
			'product_categories'         => 'product_categories',
			'exclude_product_categories' => 'excluded_product_categories',
			'exclude_sale_items'         => 'exclude_sale_items',
			'minimum_amount'             => 'minimum_amount',
			'maximum_amount'             => 'maximum_amount',
			'customer_email'             => 'email_restrictions',
		);

		$props_to_update = $this->get_props_to_update( $coupon, $meta_key_to_props );
		foreach ( $props_to_update as $meta_key => $prop ) {
			$value = $coupon->{"get_$prop"}( 'edit' );
			$value = is_string( $value ) ? wp_slash( $value ) : $value;
			switch ( $prop ) {
				case 'individual_use':
				case 'free_shipping':
				case 'exclude_sale_items':
					$value = wc_bool_to_string( $value );
					break;
				case 'product_ids':
				case 'excluded_product_ids':
					$value = implode( ',', array_filter( array_map( 'intval', $value ) ) );
					break;
				case 'product_categories':
				case 'excluded_product_categories':
					$value = array_filter( array_map( 'intval', $value ) );
					break;
				case 'email_restrictions':
					$value = array_filter( array_map( 'sanitize_email', $value ) );
					break;
				case 'date_expires':
					$value = $value ? $value->getTimestamp() : null;
					break;
			}

			$updated = $this->update_or_delete_post_meta( $coupon, $meta_key, $value );

			if ( $updated ) {
				$this->updated_props[] = $prop;
			}
		}

		do_action( 'woocommerce_coupon_object_updated_props', $coupon, $this->updated_props );
	}

	/**
	 * Increase usage count for current coupon.
	 *
	 * @since 3.0.0
	 * @param WC_Coupon $coupon           Coupon object.
	 * @param string    $used_by          Either user ID or billing email.
	 * @param WC_Order  $order (Optional) If passed, clears the hold record associated with order.

	 * @return int New usage count.
	 */
	public function increase_usage_count( &$coupon, $used_by = '', $order = null ) {
		$coupon_held_key_for_user = '';
		if ( $order instanceof WC_Order ) {
			$coupon_held_key_for_user = $order->get_data_store()->get_coupon_held_keys_for_users( $order, $coupon->get_id() );
		}

		$new_count = $this->update_usage_count_meta( $coupon, 'increase' );

		if ( $used_by ) {
			$this->add_coupon_used_by( $coupon, $used_by, $coupon_held_key_for_user );
			$coupon->set_used_by( (array) get_post_meta( $coupon->get_id(), '_used_by' ) );
		}

		do_action( 'woocommerce_increase_coupon_usage_count', $coupon, $new_count, $used_by );

		return $new_count;
	}

	/**
	 * Helper function to add a `_used_by` record to track coupons used by the user.
	 *
	 * @param WC_Coupon $coupon           Coupon object.
	 * @param string    $used_by          Either user ID or billing email.
	 * @param string    $coupon_held_key (Optional) Update meta key to `_used_by` instead of adding a new record.
	 */
	private function add_coupon_used_by( $coupon, $used_by, $coupon_held_key ) {
		global $wpdb;
		if ( $coupon_held_key && '' !== $coupon_held_key ) {
			// Looks like we added a tentative record for this coupon getting used.
			// Lets change the tentative record to a permanent one.
			$result = $wpdb->query(
				$wpdb->prepare(
					"
					UPDATE $wpdb->postmeta SET meta_key = %s, meta_value = %s WHERE meta_key = %s LIMIT 1",
					'_used_by',
					$used_by,
					$coupon_held_key
				)
			);
			if ( ! $result ) {
				// If no rows were updated, then insert a `_used_by` row manually to maintain consistency.
				add_post_meta( $coupon->get_id(), '_used_by', strtolower( $used_by ) );
			}
		} else {
			add_post_meta( $coupon->get_id(), '_used_by', strtolower( $used_by ) );
		}
	}

	/**
	 * Decrease usage count for current coupon.
	 *
	 * @since 3.0.0
	 * @param WC_Coupon $coupon Coupon object.
	 * @param string    $used_by Either user ID or billing email.
	 * @return int New usage count.
	 */
	public function decrease_usage_count( &$coupon, $used_by = '' ) {
		global $wpdb;
		$new_count = $this->update_usage_count_meta( $coupon, 'decrease' );
		if ( $used_by ) {
			/**
			 * We're doing this the long way because `delete_post_meta( $id, $key, $value )` deletes.
			 * all instances where the key and value match, and we only want to delete one.
			 */
			$meta_id = $wpdb->get_var(
				$wpdb->prepare(
					"SELECT meta_id FROM $wpdb->postmeta WHERE meta_key = '_used_by' AND meta_value = %s AND post_id = %d LIMIT 1;",
					$used_by,
					$coupon->get_id()
				)
			);
			if ( $meta_id ) {
				delete_metadata_by_mid( 'post', $meta_id );
				$coupon->set_used_by( (array) get_post_meta( $coupon->get_id(), '_used_by' ) );
			}
		}

		do_action( 'woocommerce_decrease_coupon_usage_count', $coupon, $new_count, $used_by );

		return $new_count;
	}

	/**
	 * Increase or decrease the usage count for a coupon by 1.
	 *
	 * @since 3.0.0
	 * @param WC_Coupon $coupon Coupon object.
	 * @param string    $operation 'increase' or 'decrease'.
	 * @return int New usage count
	 */
	private function update_usage_count_meta( &$coupon, $operation = 'increase' ) {
		global $wpdb;
		$id       = $coupon->get_id();
		$operator = ( 'increase' === $operation ) ? '+' : '-';

		add_post_meta( $id, 'usage_count', $coupon->get_usage_count( 'edit' ), true );
		$wpdb->query(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				"UPDATE $wpdb->postmeta SET meta_value = meta_value {$operator} 1 WHERE meta_key = 'usage_count' AND post_id = %d;",
				$id
			)
		);

		// Get the latest value direct from the DB, instead of possibly the WP meta cache.
		return (int) $wpdb->get_var( $wpdb->prepare( "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = 'usage_count' AND post_id = %d;", $id ) );
	}

	/**
	 * Returns tentative usage count for coupon.
	 *
	 * @param int $coupon_id Coupon ID.
	 *
	 * @return int Tentative usage count.
	 */
	public function get_tentative_usage_count( $coupon_id ) {
		global $wpdb;
		return $wpdb->get_var(
			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			$this->get_tentative_usage_query( $coupon_id )
		);
	}

	/**
	 * Get the number of uses for a coupon by user ID.
	 *
	 * @since 3.0.0
	 * @param WC_Coupon $coupon Coupon object.
	 * @param int       $user_id User ID.
	 * @return int
	 */
	public function get_usage_by_user_id( &$coupon, $user_id ) {
		global $wpdb;
		$usage_count = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT( meta_id ) FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_used_by' AND meta_value = %d;",
				$coupon->get_id(),
				$user_id
			)
		);
		$tentative_usage_count = $this->get_tentative_usages_for_user( $coupon->get_id(), array( $user_id ) );
		return $tentative_usage_count + $usage_count;
	}

	/**
	 * Get the number of uses for a coupon by email address
	 *
	 * @since 3.6.4
	 * @param WC_Coupon $coupon Coupon object.
	 * @param string    $email Email address.
	 * @return int
	 */
	public function get_usage_by_email( &$coupon, $email ) {
		global $wpdb;
		$usage_count = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT( meta_id ) FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_used_by' AND meta_value = %s;",
				$coupon->get_id(),
				$email
			)
		);
		$tentative_usage_count = $this->get_tentative_usages_for_user( $coupon->get_id(), array( $email ) );
		return $tentative_usage_count + $usage_count;
	}

	/**
	 * Get tentative coupon usages for user.
	 *
	 * @param int   $coupon_id    Coupon ID.
	 * @param array $user_aliases Array of user aliases to check tentative usages for.
	 *
	 * @return string|null
	 */
	public function get_tentative_usages_for_user( $coupon_id, $user_aliases ) {
		global $wpdb;
		return $wpdb->get_var(
			$this->get_tentative_usage_query_for_user( $coupon_id, $user_aliases )
		); // WPCS: unprepared SQL ok.

	}

	/**
	 * Get held time for resources before cancelling the order. Use 60 minutes as sane default.
	 * Note that the filter `woocommerce_coupon_hold_minutes` only support minutes because it's getting used elsewhere as well, however this function returns in seconds.
	 *
	 * @return int
	 */
	private function get_tentative_held_time() {
		return apply_filters( 'woocommerce_coupon_hold_minutes', ( (int) get_option( 'woocommerce_hold_stock_minutes', 60 ) ) ) * 60;
	}

	/**
	 * Check and records coupon usage tentatively for short period of time so that counts validation is correct. Returns early if there is no limit defined for the coupon.
	 *
	 * @param WC_Coupon $coupon Coupon object.
	 *
	 * @return bool|int|string|null Returns meta key if coupon was held, null if returned early.
	 */
	public function check_and_hold_coupon( $coupon ) {
		global $wpdb;

		$usage_limit = $coupon->get_usage_limit();
		$held_time   = $this->get_tentative_held_time();

		if ( 0 >= $usage_limit || 0 >= $held_time ) {
			return null;
		}

		if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', true ) ) {
			return null;
		}

		// Make sure we have usage_count meta key for this coupon because its required for `$query_for_usages`.
		// We are not directly modifying `$query_for_usages` to allow for `usage_count` not present only keep that query simple.
		if ( ! metadata_exists( 'post', $coupon->get_id(), 'usage_count' ) ) {
			$coupon->set_usage_count( $coupon->get_usage_count() ); // Use `get_usage_count` here to write default value, which may changed by a filter.
			$coupon->save();
		}

		$query_for_usages = $wpdb->prepare(
			"
			SELECT meta_value from $wpdb->postmeta
			WHERE {$wpdb->postmeta}.meta_key = 'usage_count'
			AND {$wpdb->postmeta}.post_id = %d
			LIMIT 1
			FOR UPDATE
			",
			$coupon->get_id()
		);

		$query_for_tentative_usages = $this->get_tentative_usage_query( $coupon->get_id() );
		$db_timestamp               = $wpdb->get_var( 'SELECT UNIX_TIMESTAMP() FROM DUAL' );

		$coupon_usage_key = '_coupon_held_' . ( (int) $db_timestamp + $held_time ) . '_' . wp_generate_password( 6, false );

		$insert_statement = $wpdb->prepare(
			"
			INSERT INTO $wpdb->postmeta ( post_id, meta_key, meta_value )
			SELECT %d, %s, %s FROM DUAL
			WHERE ( $query_for_usages ) + ( $query_for_tentative_usages ) < %d
			",
			$coupon->get_id(),
			$coupon_usage_key,
			'',
			$usage_limit
		); // WPCS: unprepared SQL ok.

		/**
		 * In some cases, specifically when there is a combined index on post_id,meta_key, the insert statement above could end up in a deadlock.
		 * We will try to insert 3 times before giving up to recover from deadlock.
		 */
		for ( $count = 0; $count < 3; $count++ ) {
			$result = $wpdb->query( $insert_statement ); // WPCS: unprepared SQL ok.
			if ( false !== $result ) {
				break;
			}
		}

		return $result > 0 ? $coupon_usage_key : $result;
	}

	/**
	 * Generate query to calculate tentative usages for the coupon.
	 *
	 * @param int $coupon_id Coupon ID to get tentative usage query for.
	 *
	 * @return string Query for tentative usages.
	 */
	private function get_tentative_usage_query( $coupon_id ) {
		global $wpdb;
		return $wpdb->prepare(
			"
			SELECT COUNT(meta_id) FROM $wpdb->postmeta
			WHERE {$wpdb->postmeta}.meta_key like %s
			AND {$wpdb->postmeta}.meta_key > %s
			AND {$wpdb->postmeta}.post_id = %d
			FOR UPDATE
			",
			array(
				'_coupon_held_%',
				'_coupon_held_' . time(),
				$coupon_id,
			)
		);  // WPCS: unprepared SQL ok.
	}

	/**
	 * Check and records coupon usage tentatively for passed user aliases for short period of time so that counts validation is correct. Returns early if there is no limit per user for the coupon.
	 *
	 * @param WC_Coupon $coupon       Coupon object.
	 * @param array     $user_aliases Emails or Ids to check for user.
	 * @param string    $user_alias   Email/ID to use as `used_by` value.
	 *
	 * @return null|false|int
	 */
	public function check_and_hold_coupon_for_user( $coupon, $user_aliases, $user_alias ) {
		global $wpdb;
		$limit_per_user = $coupon->get_usage_limit_per_user();
		$held_time      = $this->get_tentative_held_time();

		if ( 0 >= $limit_per_user || 0 >= $held_time ) {
			// This coupon do not have any restriction for usage per customer. No need to check further, lets bail.
			return null;
		}

		if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', true ) ) {
			return null;
		}

		$format = implode( "','", array_fill( 0, count( $user_aliases ), '%s' ) );

		$query_for_usages = $wpdb->prepare(
			"
				SELECT COUNT(*) FROM $wpdb->postmeta
				WHERE {$wpdb->postmeta}.meta_key = '_used_by'
				AND {$wpdb->postmeta}.meta_value IN ('$format')
				AND {$wpdb->postmeta}.post_id = %d
				FOR UPDATE
				",
			array_merge(
				$user_aliases,
				array( $coupon->get_id() )
			)
		); // WPCS: unprepared SQL ok.

		$query_for_tentative_usages = $this->get_tentative_usage_query_for_user( $coupon->get_id(), $user_aliases );
		$db_timestamp               = $wpdb->get_var( 'SELECT UNIX_TIMESTAMP() FROM DUAL' );

		$coupon_used_by_meta_key    = '_maybe_used_by_' . ( (int) $db_timestamp + $held_time ) . '_' . wp_generate_password( 6, false );
		$insert_statement           = $wpdb->prepare(
			"
			INSERT INTO $wpdb->postmeta ( post_id, meta_key, meta_value )
			SELECT %d, %s, %s FROM DUAL
			WHERE ( $query_for_usages ) + ( $query_for_tentative_usages ) < %d
			",
			$coupon->get_id(),
			$coupon_used_by_meta_key,
			$user_alias,
			$limit_per_user
		); // WPCS: unprepared SQL ok.

		// This query can potentially be deadlocked if a combined index on post_id and meta_key is present and there is
		// high concurrency, in which case DB will abort the query which has done less work to resolve deadlock.
		// We will try up to 3 times before giving up.
		for ( $count = 0; $count < 3; $count++ ) {
			$result = $wpdb->query( $insert_statement ); // WPCS: unprepared SQL ok.
			if ( false !== $result ) {
				break;
			}
		}

		return $result > 0 ? $coupon_used_by_meta_key : $result;
	}

	/**
	 * Generate query to calculate tentative usages for the coupon by the user.
	 *
	 * @param int   $coupon_id    Coupon ID.
	 * @param array $user_aliases List of user aliases to check for usages.
	 *
	 * @return string Tentative usages query.
	 */
	private function get_tentative_usage_query_for_user( $coupon_id, $user_aliases ) {
		global $wpdb;

		$format = implode( "','", array_fill( 0, count( $user_aliases ), '%s' ) );

		// Note that if you are debugging, `_maybe_used_by_%` will be converted to `_maybe_used_by_{...very long str...}` to very long string. This is expected, and is automatically corrected while running the insert query.
		return $wpdb->prepare(
			"
				SELECT COUNT( meta_id ) FROM $wpdb->postmeta
				WHERE {$wpdb->postmeta}.meta_key like %s
				AND {$wpdb->postmeta}.meta_key > %s
				AND {$wpdb->postmeta}.post_id = %d
				AND {$wpdb->postmeta}.meta_value IN ('$format')
				FOR UPDATE
				",
			array_merge(
				array(
					'_maybe_used_by_%',
					'_maybe_used_by_' . time(),
					$coupon_id,
				),
				$user_aliases
			)
		); // WPCS: unprepared SQL ok.
	}

	/**
	 * Return a coupon code for a specific ID.
	 *
	 * @since 3.0.0
	 * @param int $id Coupon ID.
	 * @return string Coupon Code
	 */
	public function get_code_by_id( $id ) {
		global $wpdb;
		return $wpdb->get_var(
			$wpdb->prepare(
				"SELECT post_title
				FROM $wpdb->posts
				WHERE ID = %d
				AND post_type = 'shop_coupon'
				AND post_status = 'publish'",
				$id
			)
		);
	}

	/**
	 * Return an array of IDs for for a specific coupon code.
	 * Can return multiple to check for existence.
	 *
	 * @since 3.0.0
	 * @param string $code Coupon code.
	 * @return array Array of IDs.
	 */
	public function get_ids_by_code( $code ) {
		global $wpdb;
		return $wpdb->get_col(
			$wpdb->prepare(
				"SELECT ID FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish' ORDER BY post_date DESC",
				wc_sanitize_coupon_code( $code )
			)
		);
	}
}
data-stores/class-wc-customer-download-data-store.php000064400000034560151336044770016746 0ustar00<?php
/**
 * WC_Customer_Download_Data_Store class file.
 *
 * @package WooCommerce\Classes
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Customer Download Data Store.
 *
 * @version  3.0.0
 */
class WC_Customer_Download_Data_Store implements WC_Customer_Download_Data_Store_Interface {

	/**
	 * Names of the database fields for the download permissions table.
	 */
	const DOWNLOAD_PERMISSION_DB_FIELDS = array(
		'download_id',
		'product_id',
		'user_id',
		'user_email',
		'order_id',
		'order_key',
		'downloads_remaining',
		'access_granted',
		'download_count',
		'access_expires',
	);

	/**
	 * Create download permission for a user, from an array of data.
	 *
	 * @param array $data Data to create the permission for.
	 * @returns int The database id of the created permission, or false if the permission creation failed.
	 */
	public function create_from_data( $data ) {
		$data = array_intersect_key( $data, array_flip( self::DOWNLOAD_PERMISSION_DB_FIELDS ) );

		$id = $this->insert_new_download_permission( $data );

		do_action( 'woocommerce_grant_product_download_access', $data );

		return $id;
	}

	/**
	 * Create download permission for a user.
	 *
	 * @param WC_Customer_Download $download WC_Customer_Download object.
	 */
	public function create( &$download ) {
		global $wpdb;

		// Always set a access granted date.
		if ( is_null( $download->get_access_granted( 'edit' ) ) ) {
			$download->set_access_granted( time() );
		}

		$data = array();
		foreach ( self::DOWNLOAD_PERMISSION_DB_FIELDS as $db_field_name ) {
			$value                  = call_user_func( array( $download, 'get_' . $db_field_name ), 'edit' );
			$data[ $db_field_name ] = $value;
		}

		$inserted_id = $this->insert_new_download_permission( $data );
		if ( $inserted_id ) {
			$download->set_id( $inserted_id );
			$download->apply_changes();
		}

		do_action( 'woocommerce_grant_product_download_access', $data );
	}

	/**
	 * Create download permission for a user, from an array of data.
	 * Assumes that all the keys in the passed data are valid.
	 *
	 * @param array $data Data to create the permission for.
	 * @return int The database id of the created permission, or false if the permission creation failed.
	 */
	private function insert_new_download_permission( $data ) {
		global $wpdb;

		// Always set a access granted date.
		if ( ! isset( $data['access_granted'] ) ) {
			$data['access_granted'] = time();
		}

		$data['access_granted'] = $this->adjust_date_for_db( $data['access_granted'] );

		if ( isset( $data['access_expires'] ) ) {
			$data['access_expires'] = $this->adjust_date_for_db( $data['access_expires'] );
		}

		$format = array(
			'%s',
			'%s',
			'%s',
			'%s',
			'%s',
			'%s',
			'%s',
			'%s',
			'%d',
			'%s',
		);

		$result = $wpdb->insert(
			$wpdb->prefix . 'woocommerce_downloadable_product_permissions',
			apply_filters( 'woocommerce_downloadable_file_permission_data', $data ),
			apply_filters( 'woocommerce_downloadable_file_permission_format', $format, $data )
		);

		return $result ? $wpdb->insert_id : false;
	}

	/**
	 * Adjust a date value to be inserted in the database.
	 *
	 * @param mixed $date The date value. Can be a WC_DateTime, a timestamp, or anything else that "date" recognizes.
	 * @return string The date converted to 'Y-m-d' format.
	 * @throws Exception The passed value can't be converted to a date.
	 */
	private function adjust_date_for_db( $date ) {
		if ( 'WC_DateTime' === get_class( $date ) ) {
			$date = $date->getTimestamp();
		}

		$adjusted_date = date( 'Y-m-d', $date ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date

		if ( $adjusted_date ) {
			return $adjusted_date;
		}

		$msg = sprintf( __( "I don't know how to get a date from a %s", 'woocommerce' ), is_object( $date ) ? get_class( $date ) : gettype( $date ) );
		throw new Exception( $msg );
	}

	/**
	 * Method to read a download permission from the database.
	 *
	 * @param WC_Customer_Download $download WC_Customer_Download object.
	 *
	 * @throws Exception Throw exception if invalid download is passed.
	 */
	public function read( &$download ) {
		global $wpdb;

		if ( ! $download->get_id() ) {
			throw new Exception( __( 'Invalid download.', 'woocommerce' ) );
		}

		$download->set_defaults();
		$raw_download = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE permission_id = %d",
				$download->get_id()
			)
		);

		if ( ! $raw_download ) {
			throw new Exception( __( 'Invalid download.', 'woocommerce' ) );
		}

		$download->set_props(
			array(
				'download_id'         => $raw_download->download_id,
				'product_id'          => $raw_download->product_id,
				'user_id'             => $raw_download->user_id,
				'user_email'          => $raw_download->user_email,
				'order_id'            => $raw_download->order_id,
				'order_key'           => $raw_download->order_key,
				'downloads_remaining' => $raw_download->downloads_remaining,
				'access_granted'      => strtotime( $raw_download->access_granted ),
				'download_count'      => $raw_download->download_count,
				'access_expires'      => is_null( $raw_download->access_expires ) ? null : strtotime( $raw_download->access_expires ),
			)
		);
		$download->set_object_read( true );
	}

	/**
	 * Method to update a download in the database.
	 *
	 * @param WC_Customer_Download $download WC_Customer_Download object.
	 */
	public function update( &$download ) {
		global $wpdb;

		$data = array(
			'download_id'         => $download->get_download_id( 'edit' ),
			'product_id'          => $download->get_product_id( 'edit' ),
			'user_id'             => $download->get_user_id( 'edit' ),
			'user_email'          => $download->get_user_email( 'edit' ),
			'order_id'            => $download->get_order_id( 'edit' ),
			'order_key'           => $download->get_order_key( 'edit' ),
			'downloads_remaining' => $download->get_downloads_remaining( 'edit' ),
			// phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
			'access_granted'      => date( 'Y-m-d', $download->get_access_granted( 'edit' )->getTimestamp() ),
			'download_count'      => $download->get_download_count( 'edit' ),
			// phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
			'access_expires'      => ! is_null( $download->get_access_expires( 'edit' ) ) ? date( 'Y-m-d', $download->get_access_expires( 'edit' )->getTimestamp() ) : null,
		);

		$format = array(
			'%s',
			'%s',
			'%s',
			'%s',
			'%s',
			'%s',
			'%s',
			'%s',
			'%d',
			'%s',
		);

		$wpdb->update(
			$wpdb->prefix . 'woocommerce_downloadable_product_permissions',
			$data,
			array(
				'permission_id' => $download->get_id(),
			),
			$format
		);
		$download->apply_changes();
	}

	/**
	 * Method to delete a download permission from the database.
	 *
	 * @param WC_Customer_Download $download WC_Customer_Download object.
	 * @param array                $args Array of args to pass to the delete method.
	 */
	public function delete( &$download, $args = array() ) {
		global $wpdb;

		$wpdb->query(
			$wpdb->prepare(
				"DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions
				WHERE permission_id = %d",
				$download->get_id()
			)
		);

		$download->set_id( 0 );
	}

	/**
	 * Method to delete a download permission from the database by ID.
	 *
	 * @param int $id permission_id of the download to be deleted.
	 */
	public function delete_by_id( $id ) {
		global $wpdb;
		$wpdb->query(
			$wpdb->prepare(
				"DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions
				WHERE permission_id = %d",
				$id
			)
		);
	}

	/**
	 * Method to delete a download permission from the database by order ID.
	 *
	 * @param int $id Order ID of the downloads that will be deleted.
	 */
	public function delete_by_order_id( $id ) {
		global $wpdb;
		$wpdb->query(
			$wpdb->prepare(
				"DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions
				WHERE order_id = %d",
				$id
			)
		);
	}

	/**
	 * Method to delete a download permission from the database by download ID.
	 *
	 * @param int $id download_id of the downloads that will be deleted.
	 */
	public function delete_by_download_id( $id ) {
		global $wpdb;
		$wpdb->query(
			$wpdb->prepare(
				"DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions
				WHERE download_id = %s",
				$id
			)
		);
	}

	/**
	 * Method to delete a download permission from the database by user ID.
	 *
	 * @since 3.4.0
	 * @param int $id user ID of the downloads that will be deleted.
	 * @return bool True if deleted rows.
	 */
	public function delete_by_user_id( $id ) {
		global $wpdb;
		return (bool) $wpdb->query(
			$wpdb->prepare(
				"DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions
				WHERE user_id = %d",
				$id
			)
		);
	}

	/**
	 * Method to delete a download permission from the database by user email.
	 *
	 * @since 3.4.0
	 * @param string $email email of the downloads that will be deleted.
	 * @return bool True if deleted rows.
	 */
	public function delete_by_user_email( $email ) {
		global $wpdb;
		return (bool) $wpdb->query(
			$wpdb->prepare(
				"DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions
				WHERE user_email = %s",
				$email
			)
		);
	}

	/**
	 * Get a download object.
	 *
	 * @param  array $data From the DB.
	 * @return WC_Customer_Download
	 */
	private function get_download( $data ) {
		return new WC_Customer_Download( $data );
	}

	/**
	 * Get array of download ids by specified args.
	 *
	 * @param  array $args Arguments to filter downloads. $args['return'] accepts the following values: 'objects' (default), 'ids' or a comma separeted list of fields (for example: 'order_id,user_id,user_email').
	 * @return array Can be an array of permission_ids, an array of WC_Customer_Download objects or an array of arrays containing specified fields depending on the value of $args['return'].
	 */
	public function get_downloads( $args = array() ) {
		global $wpdb;

		$args = wp_parse_args(
			$args,
			array(
				'user_email'  => '',
				'user_id'     => '',
				'order_id'    => '',
				'order_key'   => '',
				'product_id'  => '',
				'download_id' => '',
				'orderby'     => 'permission_id',
				'order'       => 'ASC',
				'limit'       => -1,
				'page'        => 1,
				'return'      => 'objects',
			)
		);

		$valid_fields       = array( 'permission_id', 'download_id', 'product_id', 'order_id', 'order_key', 'user_email', 'user_id', 'downloads_remaining', 'access_granted', 'access_expires', 'download_count' );
		$get_results_output = ARRAY_A;

		if ( 'ids' === $args['return'] ) {
			$fields = 'permission_id';
		} elseif ( 'objects' === $args['return'] ) {
			$fields             = '*';
			$get_results_output = OBJECT;
		} else {
			$fields = explode( ',', (string) $args['return'] );
			$fields = implode( ', ', array_intersect( $fields, $valid_fields ) );
		}

		$query   = array();
		$query[] = "SELECT {$fields} FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE 1=1";

		if ( $args['user_email'] ) {
			$query[] = $wpdb->prepare( 'AND user_email = %s', sanitize_email( $args['user_email'] ) );
		}

		if ( $args['user_id'] ) {
			$query[] = $wpdb->prepare( 'AND user_id = %d', absint( $args['user_id'] ) );
		}

		if ( $args['order_id'] ) {
			$query[] = $wpdb->prepare( 'AND order_id = %d', $args['order_id'] );
		}

		if ( $args['order_key'] ) {
			$query[] = $wpdb->prepare( 'AND order_key = %s', $args['order_key'] );
		}

		if ( $args['product_id'] ) {
			$query[] = $wpdb->prepare( 'AND product_id = %d', $args['product_id'] );
		}

		if ( $args['download_id'] ) {
			$query[] = $wpdb->prepare( 'AND download_id = %s', $args['download_id'] );
		}

		$orderby     = in_array( $args['orderby'], $valid_fields, true ) ? $args['orderby'] : 'permission_id';
		$order       = 'DESC' === strtoupper( $args['order'] ) ? 'DESC' : 'ASC';
		$orderby_sql = sanitize_sql_orderby( "{$orderby} {$order}" );
		$query[]     = "ORDER BY {$orderby_sql}";

		if ( 0 < $args['limit'] ) {
			$query[] = $wpdb->prepare( 'LIMIT %d, %d', absint( $args['limit'] ) * absint( $args['page'] - 1 ), absint( $args['limit'] ) );
		}

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$results = $wpdb->get_results( implode( ' ', $query ), $get_results_output );

		switch ( $args['return'] ) {
			case 'ids':
				return wp_list_pluck( $results, 'permission_id' );
			case 'objects':
				return array_map( array( $this, 'get_download' ), $results );
			default:
				return $results;
		}
	}

	/**
	 * Update download ids if the hash changes.
	 *
	 * @deprecated 3.3.0 Download id is now a static UUID and should not be changed based on file hash.
	 *
	 * @param  int    $product_id Product ID.
	 * @param  string $old_id Old download_id.
	 * @param  string $new_id New download_id.
	 */
	public function update_download_id( $product_id, $old_id, $new_id ) {
		global $wpdb;

		wc_deprecated_function( __METHOD__, '3.3' );

		$wpdb->update(
			$wpdb->prefix . 'woocommerce_downloadable_product_permissions',
			array(
				'download_id' => $new_id,
			),
			array(
				'download_id' => $old_id,
				'product_id'  => $product_id,
			)
		);
	}

	/**
	 * Get a customers downloads.
	 *
	 * @param  int $customer_id Customer ID.
	 * @return array
	 */
	public function get_downloads_for_customer( $customer_id ) {
		global $wpdb;

		return $wpdb->get_results(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions as permissions
				WHERE user_id = %d
				AND permissions.order_id > 0
				AND
					(
						permissions.downloads_remaining > 0
						OR permissions.downloads_remaining = ''
					)
				AND
					(
						permissions.access_expires IS NULL
						OR permissions.access_expires >= %s
						OR permissions.access_expires = '0000-00-00 00:00:00'
					)
				ORDER BY permissions.order_id, permissions.product_id, permissions.permission_id;",
				$customer_id,
				date( 'Y-m-d', current_time( 'timestamp' ) )  // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
			)
		);
	}

	/**
	 * Update user prop for downloads based on order id.
	 *
	 * @param  int    $order_id Order ID.
	 * @param  int    $customer_id Customer ID.
	 * @param  string $email Customer email address.
	 */
	public function update_user_by_order_id( $order_id, $customer_id, $email ) {
		global $wpdb;
		$wpdb->update(
			$wpdb->prefix . 'woocommerce_downloadable_product_permissions',
			array(
				'user_id'    => $customer_id,
				'user_email' => $email,
			),
			array(
				'order_id' => $order_id,
			),
			array(
				'%d',
				'%s',
			),
			array(
				'%d',
			)
		);
	}
}
data-stores/class-wc-product-variable-data-store-cpt.php000064400000057124151336044770017330 0ustar00<?php
/**
 * File for WC Variable Product Data Store class.
 *
 * @package WooCommerce\Classes
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC Variable Product Data Store: Stored in CPT.
 *
 * @version 3.0.0
 */
class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT implements WC_Object_Data_Store_Interface, WC_Product_Variable_Data_Store_Interface {

	/**
	 * Cached & hashed prices array for child variations.
	 *
	 * @var array
	 */
	protected $prices_array = array();

	/**
	 * Read attributes from post meta.
	 *
	 * @param WC_Product $product Product object.
	 */
	protected function read_attributes( &$product ) {
		$meta_attributes = get_post_meta( $product->get_id(), '_product_attributes', true );

		if ( ! empty( $meta_attributes ) && is_array( $meta_attributes ) ) {
			$attributes   = array();
			$force_update = false;
			foreach ( $meta_attributes as $meta_attribute_key => $meta_attribute_value ) {
				$meta_value = array_merge(
					array(
						'name'         => '',
						'value'        => '',
						'position'     => 0,
						'is_visible'   => 0,
						'is_variation' => 0,
						'is_taxonomy'  => 0,
					),
					(array) $meta_attribute_value
				);

				// Maintain data integrity. 4.9 changed sanitization functions - update the values here so variations function correctly.
				if ( $meta_value['is_variation'] && strstr( $meta_value['name'], '/' ) && sanitize_title( $meta_value['name'] ) !== $meta_attribute_key ) {
					global $wpdb;

					$old_slug      = 'attribute_' . $meta_attribute_key;
					$new_slug      = 'attribute_' . sanitize_title( $meta_value['name'] );
					$old_meta_rows = $wpdb->get_results( $wpdb->prepare( "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s;", $old_slug ) ); // WPCS: db call ok, cache ok.

					if ( $old_meta_rows ) {
						foreach ( $old_meta_rows as $old_meta_row ) {
							update_post_meta( $old_meta_row->post_id, $new_slug, $old_meta_row->meta_value );
						}
					}

					$force_update = true;
				}

				// Check if is a taxonomy attribute.
				if ( ! empty( $meta_value['is_taxonomy'] ) ) {
					if ( ! taxonomy_exists( $meta_value['name'] ) ) {
						continue;
					}
					$id      = wc_attribute_taxonomy_id_by_name( $meta_value['name'] );
					$options = wc_get_object_terms( $product->get_id(), $meta_value['name'], 'term_id' );
				} else {
					$id      = 0;
					$options = wc_get_text_attributes( $meta_value['value'] );
				}

				$attribute = new WC_Product_Attribute();
				$attribute->set_id( $id );
				$attribute->set_name( $meta_value['name'] );
				$attribute->set_options( $options );
				$attribute->set_position( $meta_value['position'] );
				$attribute->set_visible( $meta_value['is_visible'] );
				$attribute->set_variation( $meta_value['is_variation'] );
				$attributes[] = $attribute;
			}
			$product->set_attributes( $attributes );

			if ( $force_update ) {
				$this->update_attributes( $product, true );
			}
		}
	}

	/**
	 * Read product data.
	 *
	 * @param WC_Product $product Product object.
	 *
	 * @since 3.0.0
	 */
	protected function read_product_data( &$product ) {
		parent::read_product_data( $product );

		// Make sure data which does not apply to variables is unset.
		$product->set_regular_price( '' );
		$product->set_sale_price( '' );
	}

	/**
	 * Loads variation child IDs.
	 *
	 * @param WC_Product $product Product object.
	 * @param bool       $force_read True to bypass the transient.
	 *
	 * @return array
	 */
	public function read_children( &$product, $force_read = false ) {
		$children_transient_name = 'wc_product_children_' . $product->get_id();
		$children                = get_transient( $children_transient_name );

		if ( empty( $children ) || ! is_array( $children ) || ! isset( $children['all'] ) || ! isset( $children['visible'] ) || $force_read ) {
			$all_args = array(
				'post_parent' => $product->get_id(),
				'post_type'   => 'product_variation',
				'orderby'     => array(
					'menu_order' => 'ASC',
					'ID'         => 'ASC',
				),
				'fields'      => 'ids',
				'post_status' => array( 'publish', 'private' ),
				'numberposts' => -1, // phpcs:ignore WordPress.VIP.PostsPerPage.posts_per_page_numberposts
			);

			$visible_only_args                = $all_args;
			$visible_only_args['post_status'] = 'publish';

			if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) {
				$visible_only_args['tax_query'][] = array(
					'taxonomy' => 'product_visibility',
					'field'    => 'name',
					'terms'    => 'outofstock',
					'operator' => 'NOT IN',
				);
			}
			$children['all']     = get_posts( apply_filters( 'woocommerce_variable_children_args', $all_args, $product, false ) );
			$children['visible'] = get_posts( apply_filters( 'woocommerce_variable_children_args', $visible_only_args, $product, true ) );

			set_transient( $children_transient_name, $children, DAY_IN_SECONDS * 30 );
		}

		$children['all']     = wp_parse_id_list( (array) $children['all'] );
		$children['visible'] = wp_parse_id_list( (array) $children['visible'] );

		return $children;
	}

	/**
	 * Loads an array of attributes used for variations, as well as their possible values.
	 *
	 * @param WC_Product $product Product object.
	 *
	 * @return array
	 */
	public function read_variation_attributes( &$product ) {
		global $wpdb;

		$variation_attributes = array();
		$attributes           = $product->get_attributes();
		$child_ids            = $product->get_children();
		$cache_key            = WC_Cache_Helper::get_cache_prefix( 'product_' . $product->get_id() ) . 'product_variation_attributes_' . $product->get_id();
		$cache_group          = 'products';
		$cached_data          = wp_cache_get( $cache_key, $cache_group );

		if ( false !== $cached_data ) {
			return $cached_data;
		}

		if ( ! empty( $attributes ) ) {
			foreach ( $attributes as $attribute ) {
				if ( empty( $attribute['is_variation'] ) ) {
					continue;
				}

				// Get possible values for this attribute, for only visible variations.
				if ( ! empty( $child_ids ) ) {
					$format     = array_fill( 0, count( $child_ids ), '%d' );
					$query_in   = '(' . implode( ',', $format ) . ')';
					$query_args = array( 'attribute_name' => wc_variation_attribute_name( $attribute['name'] ) ) + $child_ids;
					$values     = array_unique(
						$wpdb->get_col(
							$wpdb->prepare(
								"SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s AND post_id IN {$query_in}", // @codingStandardsIgnoreLine.
								$query_args
							)
						)
					);
				} else {
					$values = array();
				}

				// Empty value indicates that all options for given attribute are available.
				if ( in_array( null, $values, true ) || in_array( '', $values, true ) || empty( $values ) ) {
					$values = $attribute['is_taxonomy'] ? wc_get_object_terms( $product->get_id(), $attribute['name'], 'slug' ) : wc_get_text_attributes( $attribute['value'] );
					// Get custom attributes (non taxonomy) as defined.
				} elseif ( ! $attribute['is_taxonomy'] ) {
					$text_attributes          = wc_get_text_attributes( $attribute['value'] );
					$assigned_text_attributes = $values;
					$values                   = array();

					// Pre 2.4 handling where 'slugs' were saved instead of the full text attribute.
					if ( version_compare( get_post_meta( $product->get_id(), '_product_version', true ), '2.4.0', '<' ) ) {
						$assigned_text_attributes = array_map( 'sanitize_title', $assigned_text_attributes );
						foreach ( $text_attributes as $text_attribute ) {
							if ( in_array( sanitize_title( $text_attribute ), $assigned_text_attributes, true ) ) {
								$values[] = $text_attribute;
							}
						}
					} else {
						foreach ( $text_attributes as $text_attribute ) {
							if ( in_array( $text_attribute, $assigned_text_attributes, true ) ) {
								$values[] = $text_attribute;
							}
						}
					}
				}
				$variation_attributes[ $attribute['name'] ] = array_unique( $values );
			}
		}

		wp_cache_set( $cache_key, $variation_attributes, $cache_group );

		return $variation_attributes;
	}

	/**
	 * Get an array of all sale and regular prices from all variations. This is used for example when displaying the price range at variable product level or seeing if the variable product is on sale.
	 *
	 * Can be filtered by plugins which modify costs, but otherwise will include the raw meta costs unlike get_price() which runs costs through the woocommerce_get_price filter.
	 * This is to ensure modified prices are not cached, unless intended.
	 *
	 * @param WC_Product $product Product object.
	 * @param bool       $for_display If true, prices will be adapted for display based on the `woocommerce_tax_display_shop` setting (including or excluding taxes).
	 *
	 * @return array of prices
	 * @since  3.0.0
	 */
	public function read_price_data( &$product, $for_display = false ) {

		/**
		 * Transient name for storing prices for this product (note: Max transient length is 45)
		 *
		 * @since 2.5.0 a single transient is used per product for all prices, rather than many transients per product.
		 */
		$transient_name    = 'wc_var_prices_' . $product->get_id();
		$transient_version = WC_Cache_Helper::get_transient_version( 'product' );
		$price_hash        = $this->get_price_hash( $product, $for_display );

		// Check if prices array is stale.
		if ( ! isset( $this->prices_array['version'] ) || $this->prices_array['version'] !== $transient_version ) {
			$this->prices_array = array(
				'version' => $transient_version,
			);
		}

		/**
		 * $this->prices_array is an array of values which may have been modified from what is stored in transients - this may not match $transient_cached_prices_array.
		 * If the value has already been generated, we don't need to grab the values again so just return them. They are already filtered.
		 */
		if ( empty( $this->prices_array[ $price_hash ] ) ) {
			$transient_cached_prices_array = array_filter( (array) json_decode( strval( get_transient( $transient_name ) ), true ) );

			// If the product version has changed since the transient was last saved, reset the transient cache.
			if ( ! isset( $transient_cached_prices_array['version'] ) || $transient_version !== $transient_cached_prices_array['version'] ) {
				$transient_cached_prices_array = array(
					'version' => $transient_version,
				);
			}

			// If the prices are not stored for this hash, generate them and add to the transient.
			if ( empty( $transient_cached_prices_array[ $price_hash ] ) ) {
				$prices_array = array(
					'price'         => array(),
					'regular_price' => array(),
					'sale_price'    => array(),
				);

				$variation_ids = $product->get_visible_children();

				if ( is_callable( '_prime_post_caches' ) ) {
					_prime_post_caches( $variation_ids );
				}

				foreach ( $variation_ids as $variation_id ) {
					$variation = wc_get_product( $variation_id );

					if ( $variation ) {
						$price         = apply_filters( 'woocommerce_variation_prices_price', $variation->get_price( 'edit' ), $variation, $product );
						$regular_price = apply_filters( 'woocommerce_variation_prices_regular_price', $variation->get_regular_price( 'edit' ), $variation, $product );
						$sale_price    = apply_filters( 'woocommerce_variation_prices_sale_price', $variation->get_sale_price( 'edit' ), $variation, $product );

						// Skip empty prices.
						if ( '' === $price ) {
							continue;
						}

						// If sale price does not equal price, the product is not yet on sale.
						if ( $sale_price === $regular_price || $sale_price !== $price ) {
							$sale_price = $regular_price;
						}

						// If we are getting prices for display, we need to account for taxes.
						if ( $for_display ) {
							if ( 'incl' === get_option( 'woocommerce_tax_display_shop' ) ) {
								$price         = '' === $price ? '' : wc_get_price_including_tax(
									$variation,
									array(
										'qty'   => 1,
										'price' => $price,
									)
								);
								$regular_price = '' === $regular_price ? '' : wc_get_price_including_tax(
									$variation,
									array(
										'qty'   => 1,
										'price' => $regular_price,
									)
								);
								$sale_price    = '' === $sale_price ? '' : wc_get_price_including_tax(
									$variation,
									array(
										'qty'   => 1,
										'price' => $sale_price,
									)
								);
							} else {
								$price         = '' === $price ? '' : wc_get_price_excluding_tax(
									$variation,
									array(
										'qty'   => 1,
										'price' => $price,
									)
								);
								$regular_price = '' === $regular_price ? '' : wc_get_price_excluding_tax(
									$variation,
									array(
										'qty'   => 1,
										'price' => $regular_price,
									)
								);
								$sale_price    = '' === $sale_price ? '' : wc_get_price_excluding_tax(
									$variation,
									array(
										'qty'   => 1,
										'price' => $sale_price,
									)
								);
							}
						}

						$prices_array['price'][ $variation_id ]         = wc_format_decimal( $price, wc_get_price_decimals() );
						$prices_array['regular_price'][ $variation_id ] = wc_format_decimal( $regular_price, wc_get_price_decimals() );
						$prices_array['sale_price'][ $variation_id ]    = wc_format_decimal( $sale_price, wc_get_price_decimals() );

						$prices_array = apply_filters( 'woocommerce_variation_prices_array', $prices_array, $variation, $for_display );
					}
				}

				// Add all pricing data to the transient array.
				foreach ( $prices_array as $key => $values ) {
					$transient_cached_prices_array[ $price_hash ][ $key ] = $values;
				}

				set_transient( $transient_name, wp_json_encode( $transient_cached_prices_array ), DAY_IN_SECONDS * 30 );
			}

			/**
			 * Give plugins one last chance to filter the variation prices array which has been generated and store locally to the class.
			 * This value may differ from the transient cache. It is filtered once before storing locally.
			 */
			$this->prices_array[ $price_hash ] = apply_filters( 'woocommerce_variation_prices', $transient_cached_prices_array[ $price_hash ], $product, $for_display );
		}
		return $this->prices_array[ $price_hash ];
	}

	/**
	 * Create unique cache key based on the tax location (affects displayed/cached prices), product version and active price filters.
	 * DEVELOPERS should filter this hash if offering conditional pricing to keep it unique.
	 *
	 * @param WC_Product $product Product object.
	 * @param bool       $for_display If taxes should be calculated or not.
	 *
	 * @since  3.0.0
	 * @return string
	 */
	protected function get_price_hash( &$product, $for_display = false ) {
		global $wp_filter;

		$price_hash = array( false );

		if ( $for_display && wc_tax_enabled() ) {
			$price_hash = array(
				get_option( 'woocommerce_tax_display_shop', 'excl' ),
				WC_Tax::get_rates(),
				empty( WC()->customer ) ? false : WC()->customer->is_vat_exempt(),
			);
		}

		$filter_names = array( 'woocommerce_variation_prices_price', 'woocommerce_variation_prices_regular_price', 'woocommerce_variation_prices_sale_price' );

		foreach ( $filter_names as $filter_name ) {
			if ( ! empty( $wp_filter[ $filter_name ] ) ) {
				$price_hash[ $filter_name ] = array();

				foreach ( $wp_filter[ $filter_name ] as $priority => $callbacks ) {
					$price_hash[ $filter_name ][] = array_values( wp_list_pluck( $callbacks, 'function' ) );
				}
			}
		}

		return md5( wp_json_encode( apply_filters( 'woocommerce_get_variation_prices_hash', $price_hash, $product, $for_display ) ) );
	}

	/**
	 * Does a child have a weight set?
	 *
	 * @param WC_Product $product Product object.
	 *
	 * @since  3.0.0
	 * @return boolean
	 */
	public function child_has_weight( $product ) {
		global $wpdb;
		$children = $product->get_visible_children();
		if ( ! $children ) {
			return false;
		}

		$format   = array_fill( 0, count( $children ), '%d' );
		$query_in = '(' . implode( ',', $format ) . ')';

		return null !== $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_weight' AND meta_value > 0 AND post_id IN {$query_in}", $children ) ); // @codingStandardsIgnoreLine.
	}

	/**
	 * Does a child have dimensions set?
	 *
	 * @param WC_Product $product Product object.
	 *
	 * @since  3.0.0
	 * @return boolean
	 */
	public function child_has_dimensions( $product ) {
		global $wpdb;
		$children = $product->get_visible_children();
		if ( ! $children ) {
			return false;
		}

		$format   = array_fill( 0, count( $children ), '%d' );
		$query_in = '(' . implode( ',', $format ) . ')';

		return null !== $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key IN ( '_length', '_width', '_height' ) AND meta_value > 0 AND post_id IN {$query_in}", $children ) ); // @codingStandardsIgnoreLine.
	}

	/**
	 * Is a child in stock?
	 *
	 * @param WC_Product $product Product object.
	 *
	 * @since  3.0.0
	 * @return boolean
	 */
	public function child_is_in_stock( $product ) {
		return $this->child_has_stock_status( $product, 'instock' );
	}

	/**
	 * Does a child have a stock status?
	 *
	 * @param WC_Product $product Product object.
	 * @param string     $status 'instock', 'outofstock', or 'onbackorder'.
	 *
	 * @since  3.3.0
	 * @return boolean
	 */
	public function child_has_stock_status( $product, $status ) {
		global $wpdb;

		$children = $product->get_children();

		if ( $children ) {
			$format     = array_fill( 0, count( $children ), '%d' );
			$query_in   = '(' . implode( ',', $format ) . ')';
			$query_args = array( 'stock_status' => $status ) + $children;
			// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
			if ( get_option( 'woocommerce_product_lookup_table_is_generating' ) ) {
				$query = "SELECT COUNT( post_id ) FROM {$wpdb->postmeta} WHERE meta_key = '_stock_status' AND meta_value = %s AND post_id IN {$query_in}";
			} else {
				$query = "SELECT COUNT( product_id ) FROM {$wpdb->wc_product_meta_lookup} WHERE stock_status = %s AND product_id IN {$query_in}";
			}
			$children_with_status = $wpdb->get_var(
				$wpdb->prepare(
					$query,
					$query_args
				)
			);
			// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
		} else {
			$children_with_status = 0;
		}

		return (bool) $children_with_status;
	}

	/**
	 * Syncs all variation names if the parent name is changed.
	 *
	 * @param WC_Product $product Product object.
	 * @param string     $previous_name Variation previous name.
	 * @param string     $new_name Variation new name.
	 *
	 * @since 3.0.0
	 */
	public function sync_variation_names( &$product, $previous_name = '', $new_name = '' ) {
		if ( $new_name !== $previous_name ) {
			global $wpdb;

			$wpdb->query(
				$wpdb->prepare(
					"UPDATE {$wpdb->posts}
					SET post_title = REPLACE( post_title, %s, %s )
					WHERE post_type = 'product_variation'
					AND post_parent = %d",
					$previous_name ? $previous_name : 'AUTO-DRAFT',
					$new_name,
					$product->get_id()
				)
			);
		}
	}

	/**
	 * Stock managed at the parent level - update children being managed by this product.
	 * This sync function syncs downwards (from parent to child) when the variable product is saved.
	 *
	 * @param WC_Product $product Product object.
	 *
	 * @since 3.0.0
	 */
	public function sync_managed_variation_stock_status( &$product ) {
		global $wpdb;

		if ( $product->get_manage_stock() ) {
			$children = $product->get_children();
			$changed  = false;

			if ( $children ) {
				$status           = $product->get_stock_status();
				$format           = array_fill( 0, count( $children ), '%d' );
				$query_in         = '(' . implode( ',', $format ) . ')';
				$managed_children = array_unique( $wpdb->get_col( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_manage_stock' AND meta_value != 'yes' AND post_id IN {$query_in}", $children ) ) ); // @codingStandardsIgnoreLine.
				foreach ( $managed_children as $managed_child ) {
					if ( update_post_meta( $managed_child, '_stock_status', $status ) ) {
						$this->update_lookup_table( $managed_child, 'wc_product_meta_lookup' );
						$changed = true;
					}
				}
			}

			if ( $changed ) {
				$children = $this->read_children( $product, true );
				$product->set_children( $children['all'] );
				$product->set_visible_children( $children['visible'] );
			}
		}
	}

	/**
	 * Sync variable product prices with children.
	 *
	 * @param WC_Product $product Product object.
	 *
	 * @since 3.0.0
	 */
	public function sync_price( &$product ) {
		global $wpdb;

		$children = $product->get_visible_children();
		if ( $children ) {
			$format   = array_fill( 0, count( $children ), '%d' );
			$query_in = '(' . implode( ',', $format ) . ')';
			$prices   = array_unique( $wpdb->get_col( $wpdb->prepare( "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = '_price' AND post_id IN {$query_in}", $children ) ) ); // @codingStandardsIgnoreLine.
		} else {
			$prices = array();
		}

		delete_post_meta( $product->get_id(), '_price' );
		delete_post_meta( $product->get_id(), '_sale_price' );
		delete_post_meta( $product->get_id(), '_regular_price' );

		if ( $prices ) {
			sort( $prices, SORT_NUMERIC );
			// To allow sorting and filtering by multiple values, we have no choice but to store child prices in this manner.
			foreach ( $prices as $price ) {
				if ( is_null( $price ) || '' === $price ) {
					continue;
				}
				add_post_meta( $product->get_id(), '_price', $price, false );
			}
		}

		$this->update_lookup_table( $product->get_id(), 'wc_product_meta_lookup' );

		/**
		 * Fire an action for this direct update so it can be detected by other code.
		 *
		 * @since 3.6
		 * @param int $product_id Product ID that was updated directly.
		 */
		do_action( 'woocommerce_updated_product_price', $product->get_id() );
	}

	/**
	 * Sync variable product stock status with children.
	 * Change does not persist unless saved by caller.
	 *
	 * @param WC_Product $product Product object.
	 *
	 * @since 3.0.0
	 */
	public function sync_stock_status( &$product ) {
		if ( $product->child_is_in_stock() ) {
			$product->set_stock_status( 'instock' );
		} elseif ( $product->child_is_on_backorder() ) {
			$product->set_stock_status( 'onbackorder' );
		} else {
			$product->set_stock_status( 'outofstock' );
		}
	}

	/**
	 * Delete variations of a product.
	 *
	 * @param int  $product_id Product ID.
	 * @param bool $force_delete False to trash.
	 *
	 * @since 3.0.0
	 */
	public function delete_variations( $product_id, $force_delete = false ) {
		if ( ! is_numeric( $product_id ) || 0 >= $product_id ) {
			return;
		}

		$variation_ids = wp_parse_id_list(
			get_posts(
				array(
					'post_parent' => $product_id,
					'post_type'   => 'product_variation',
					'fields'      => 'ids',
					'post_status' => array( 'any', 'trash', 'auto-draft' ),
					'numberposts' => -1, // phpcs:ignore WordPress.VIP.PostsPerPage.posts_per_page_numberposts
				)
			)
		);

		if ( ! empty( $variation_ids ) ) {
			foreach ( $variation_ids as $variation_id ) {
				if ( $force_delete ) {
					do_action( 'woocommerce_before_delete_product_variation', $variation_id );
					wp_delete_post( $variation_id, true );
					do_action( 'woocommerce_delete_product_variation', $variation_id );
				} else {
					wp_trash_post( $variation_id );
					do_action( 'woocommerce_trash_product_variation', $variation_id );
				}
			}
		}

		delete_transient( 'wc_product_children_' . $product_id );
	}

	/**
	 * Untrash variations.
	 *
	 * @param int $product_id Product ID.
	 */
	public function untrash_variations( $product_id ) {
		$variation_ids = wp_parse_id_list(
			get_posts(
				array(
					'post_parent' => $product_id,
					'post_type'   => 'product_variation',
					'fields'      => 'ids',
					'post_status' => 'trash',
					'numberposts' => -1, // phpcs:ignore WordPress.VIP.PostsPerPage.posts_per_page_numberposts
				)
			)
		);

		if ( ! empty( $variation_ids ) ) {
			foreach ( $variation_ids as $variation_id ) {
				wp_untrash_post( $variation_id );
			}
		}

		delete_transient( 'wc_product_children_' . $product_id );
	}
}
legacy/abstract-wc-legacy-payment-token.php000064400000003566151336044770015012 0ustar00<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Legacy Payment Tokens.
 * Payment Tokens were introduced in 2.6.0 with create and update as methods.
 * Major CRUD changes occurred in 3.0, so these were deprecated (save and delete still work).
 * This legacy class is for backwards compatibility in case any code called ->read, ->update or ->create
 * directly on the object.
 *
 * @version  3.0.0
 * @package  WooCommerce\Classes
 * @category Class
 * @author   WooCommerce
 */
abstract class WC_Legacy_Payment_Token extends WC_Data {

	/**
	 * Sets the type of this payment token (CC, eCheck, or something else).
	 *
	 * @param string Payment Token Type (CC, eCheck)
	 */
	public function set_type( $type ) {
		wc_deprecated_function( 'WC_Payment_Token::set_type', '3.0.0', 'Type cannot be overwritten.' );
	}

	/**
	 * Read a token by ID.
	 * @deprecated 3.0.0 - Init a token class with an ID.
	 *
	 * @param int $token_id
	 */
	public function read( $token_id ) {
		wc_deprecated_function( 'WC_Payment_Token::read', '3.0.0', 'a new token class initialized with an ID.' );
		$this->set_id( $token_id );
		$data_store = WC_Data_Store::load( 'payment-token' );
		$data_store->read( $this );
	}

	/**
	 * Update a token.
	 * @deprecated 3.0.0 - Use ::save instead.
	 */
	public function update() {
		wc_deprecated_function( 'WC_Payment_Token::update', '3.0.0', 'WC_Payment_Token::save instead.' );
		$data_store = WC_Data_Store::load( 'payment-token' );
		try {
			$data_store->update( $this );
		} catch ( Exception $e ) {
			return false;
		}
	}

	/**
	 * Create a token.
	 * @deprecated 3.0.0 - Use ::save instead.
	 */
	public function create() {
		wc_deprecated_function( 'WC_Payment_Token::create', '3.0.0', 'WC_Payment_Token::save instead.' );
		$data_store = WC_Data_Store::load( 'payment-token' );
		try {
			$data_store->create( $this );
		} catch ( Exception $e ) {
			return false;
		}
	}

}
legacy/class-wc-legacy-customer.php000064400000015663151336044770013363 0ustar00<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Legacy Customer.
 *
 * @version  3.0.0
 * @package  WooCommerce\Classes
 * @category Class
 * @author   WooThemes
 */
abstract class WC_Legacy_Customer extends WC_Data {

	/**
	 * __isset legacy.
	 * @param mixed $key
	 * @return bool
	 */
	public function __isset( $key ) {
		$legacy_keys = array(
			'id',
			'country',
			'state',
			'postcode',
			'city',
			'address_1',
			'address',
			'address_2',
			'shipping_country',
			'shipping_state',
			'shipping_postcode',
			'shipping_city',
			'shipping_address_1',
			'shipping_address',
			'shipping_address_2',
			'is_vat_exempt',
			'calculated_shipping',
		);
		$key = $this->filter_legacy_key( $key );
		return in_array( $key, $legacy_keys );
	}

	/**
	 * __get function.
	 * @param string $key
	 * @return string
	 */
	public function __get( $key ) {
		wc_doing_it_wrong( $key, 'Customer properties should not be accessed directly.', '3.0' );
		$key = $this->filter_legacy_key( $key );
		if ( in_array( $key, array( 'country', 'state', 'postcode', 'city', 'address_1', 'address', 'address_2' ) ) ) {
			$key = 'billing_' . $key;
		}
		return is_callable( array( $this, "get_{$key}" ) ) ? $this->{"get_{$key}"}() : '';
	}

	/**
	 * __set function.
	 *
	 * @param string $key
	 * @param mixed $value
	 */
	public function __set( $key, $value ) {
		wc_doing_it_wrong( $key, 'Customer properties should not be set directly.', '3.0' );
		$key = $this->filter_legacy_key( $key );

		if ( is_callable( array( $this, "set_{$key}" ) ) ) {
			$this->{"set_{$key}"}( $value );
		}
	}

	/**
	 * Address and shipping_address are aliased, so we want to get the 'real' key name.
	 * For all other keys, we can just return it.
	 * @since 3.0.0
	 * @param  string $key
	 * @return string
	 */
	private function filter_legacy_key( $key ) {
		if ( 'address' === $key ) {
			$key = 'address_1';
		}
		if ( 'shipping_address' === $key ) {
			$key = 'shipping_address_1';
		}

		return $key;
	}

	/**
	 * Sets session data for the location.
	 *
	 * @param string $country
	 * @param string $state
	 * @param string $postcode (default: '')
	 * @param string $city (default: '')
	 */
	public function set_location( $country, $state, $postcode = '', $city = '' ) {
		$this->set_billing_location( $country, $state, $postcode, $city );
		$this->set_shipping_location( $country, $state, $postcode, $city );
	}

	/**
	 * Get default country for a customer.
	 * @return string
	 */
	public function get_default_country() {
		wc_deprecated_function( 'WC_Customer::get_default_country', '3.0', 'wc_get_customer_default_location' );
		$default = wc_get_customer_default_location();
		return $default['country'];
	}

	/**
	 * Get default state for a customer.
	 * @return string
	 */
	public function get_default_state() {
		wc_deprecated_function( 'WC_Customer::get_default_state', '3.0', 'wc_get_customer_default_location' );
		$default = wc_get_customer_default_location();
		return $default['state'];
	}

	/**
	 * Set customer address to match shop base address.
	 */
	public function set_to_base() {
		wc_deprecated_function( 'WC_Customer::set_to_base', '3.0', 'WC_Customer::set_billing_address_to_base' );
		$this->set_billing_address_to_base();
	}

	/**
	 * Set customer shipping address to base address.
	 */
	public function set_shipping_to_base() {
		wc_deprecated_function( 'WC_Customer::set_shipping_to_base', '3.0', 'WC_Customer::set_shipping_address_to_base' );
		$this->set_shipping_address_to_base();
	}

	/**
	 * Calculated shipping.
	 * @param boolean $calculated
	 */
	public function calculated_shipping( $calculated = true ) {
		wc_deprecated_function( 'WC_Customer::calculated_shipping', '3.0', 'WC_Customer::set_calculated_shipping' );
		$this->set_calculated_shipping( $calculated );
	}

	/**
	 * Set default data for a customer.
	 */
	public function set_default_data() {
		wc_deprecated_function( 'WC_Customer::set_default_data', '3.0' );
	}

	/**
	 * Save data function.
	 */
	public function save_data() {
		$this->save();
	}

	/**
	 * Is the user a paying customer?
	 *
	 * @param int $user_id
	 *
	 * @return bool
	 */
	function is_paying_customer( $user_id = '' ) {
		wc_deprecated_function( 'WC_Customer::is_paying_customer', '3.0', 'WC_Customer::get_is_paying_customer' );
		if ( ! empty( $user_id ) ) {
			$user_id = get_current_user_id();
		}
		return '1' === get_user_meta( $user_id, 'paying_customer', true );
	}

	/**
	 * Legacy get address.
	 */
	function get_address() {
		wc_deprecated_function( 'WC_Customer::get_address', '3.0', 'WC_Customer::get_billing_address_1' );
		return $this->get_billing_address_1();
	}

	/**
	 * Legacy get address 2.
	 */
	function get_address_2() {
		wc_deprecated_function( 'WC_Customer::get_address_2', '3.0', 'WC_Customer::get_billing_address_2' );
		return $this->get_billing_address_2();
	}

	/**
	 * Legacy get country.
	 */
	function get_country() {
		wc_deprecated_function( 'WC_Customer::get_country', '3.0', 'WC_Customer::get_billing_country' );
		return $this->get_billing_country();
	}

	/**
	 * Legacy get state.
	 */
	function get_state() {
		wc_deprecated_function( 'WC_Customer::get_state', '3.0', 'WC_Customer::get_billing_state' );
		return $this->get_billing_state();
	}

	/**
	 * Legacy get postcode.
	 */
	function get_postcode() {
		wc_deprecated_function( 'WC_Customer::get_postcode', '3.0', 'WC_Customer::get_billing_postcode' );
		return $this->get_billing_postcode();
	}

	/**
	 * Legacy get city.
	 */
	function get_city() {
		wc_deprecated_function( 'WC_Customer::get_city', '3.0', 'WC_Customer::get_billing_city' );
		return $this->get_billing_city();
	}

	/**
	 * Legacy set country.
	 *
	 * @param string $country
	 */
	function set_country( $country ) {
		wc_deprecated_function( 'WC_Customer::set_country', '3.0', 'WC_Customer::set_billing_country' );
		$this->set_billing_country( $country );
	}

	/**
	 * Legacy set state.
	 *
	 * @param string $state
	 */
	function set_state( $state ) {
		wc_deprecated_function( 'WC_Customer::set_state', '3.0', 'WC_Customer::set_billing_state' );
		$this->set_billing_state( $state );
	}

	/**
	 * Legacy set postcode.
	 *
	 * @param string $postcode
	 */
	function set_postcode( $postcode ) {
		wc_deprecated_function( 'WC_Customer::set_postcode', '3.0', 'WC_Customer::set_billing_postcode' );
		$this->set_billing_postcode( $postcode );
	}

	/**
	 * Legacy set city.
	 *
	 * @param string $city
	 */
	function set_city( $city ) {
		wc_deprecated_function( 'WC_Customer::set_city', '3.0', 'WC_Customer::set_billing_city' );
		$this->set_billing_city( $city );
	}

	/**
	 * Legacy set address.
	 *
	 * @param string $address
	 */
	function set_address( $address ) {
		wc_deprecated_function( 'WC_Customer::set_address', '3.0', 'WC_Customer::set_billing_address' );
		$this->set_billing_address( $address );
	}

	/**
	 * Legacy set address.
	 *
	 * @param string $address
	 */
	function set_address_2( $address ) {
		wc_deprecated_function( 'WC_Customer::set_address_2', '3.0', 'WC_Customer::set_billing_address_2' );
		$this->set_billing_address_2( $address );
	}
}
legacy/api/v3/class-wc-api-authentication.php000064400000031433151336044770015140 0ustar00<?php
/**
 * WooCommerce API Authentication Class
 *
 * @author   WooThemes
 * @category API
 * @package  WooCommerce\RestApi
 * @since    2.1.0
 * @version  2.4.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WC_API_Authentication {

	/**
	 * Setup class
	 *
	 * @since 2.1
	 */
	public function __construct() {

		// To disable authentication, hook into this filter at a later priority and return a valid WP_User
		add_filter( 'woocommerce_api_check_authentication', array( $this, 'authenticate' ), 0 );
	}

	/**
	 * Authenticate the request. The authentication method varies based on whether the request was made over SSL or not.
	 *
	 * @since 2.1
	 * @param WP_User $user
	 * @return null|WP_Error|WP_User
	 */
	public function authenticate( $user ) {

		// Allow access to the index by default
		if ( '/' === WC()->api->server->path ) {
			return new WP_User( 0 );
		}

		try {
			if ( is_ssl() ) {
				$keys = $this->perform_ssl_authentication();
			} else {
				$keys = $this->perform_oauth_authentication();
			}

			// Check API key-specific permission
			$this->check_api_key_permissions( $keys['permissions'] );

			$user = $this->get_user_by_id( $keys['user_id'] );

			$this->update_api_key_last_access( $keys['key_id'] );

		} catch ( Exception $e ) {
			$user = new WP_Error( 'woocommerce_api_authentication_error', $e->getMessage(), array( 'status' => $e->getCode() ) );
		}

		return $user;
	}

	/**
	 * SSL-encrypted requests are not subject to sniffing or man-in-the-middle
	 * attacks, so the request can be authenticated by simply looking up the user
	 * associated with the given consumer key and confirming the consumer secret
	 * provided is valid
	 *
	 * @since 2.1
	 * @return array
	 * @throws Exception
	 */
	private function perform_ssl_authentication() {
		$params = WC()->api->server->params['GET'];

		// if the $_GET parameters are present, use those first
		if ( ! empty( $params['consumer_key'] ) && ! empty( $params['consumer_secret'] ) ) {
			$keys = $this->get_keys_by_consumer_key( $params['consumer_key'] );

			if ( ! $this->is_consumer_secret_valid( $keys['consumer_secret'], $params['consumer_secret'] ) ) {
				throw new Exception( __( 'Consumer secret is invalid.', 'woocommerce' ), 401 );
			}

			return $keys;
		}

		// if the above is not present, we will do full basic auth
		if ( empty( $_SERVER['PHP_AUTH_USER'] ) || empty( $_SERVER['PHP_AUTH_PW'] ) ) {
			$this->exit_with_unauthorized_headers();
		}

		$keys = $this->get_keys_by_consumer_key( $_SERVER['PHP_AUTH_USER'] );

		if ( ! $this->is_consumer_secret_valid( $keys['consumer_secret'], $_SERVER['PHP_AUTH_PW'] ) ) {
			$this->exit_with_unauthorized_headers();
		}

		return $keys;
	}

	/**
	 * If the consumer_key and consumer_secret $_GET parameters are NOT provided
	 * and the Basic auth headers are either not present or the consumer secret does not match the consumer
	 * key provided, then return the correct Basic headers and an error message.
	 *
	 * @since 2.4
	 */
	private function exit_with_unauthorized_headers() {
		$auth_message = __( 'WooCommerce API. Use a consumer key in the username field and a consumer secret in the password field.', 'woocommerce' );
		header( 'WWW-Authenticate: Basic realm="' . $auth_message . '"' );
		header( 'HTTP/1.0 401 Unauthorized' );
		throw new Exception( __( 'Consumer Secret is invalid.', 'woocommerce' ), 401 );
	}

	/**
	 * Perform OAuth 1.0a "one-legged" (http://oauthbible.com/#oauth-10a-one-legged) authentication for non-SSL requests
	 *
	 * This is required so API credentials cannot be sniffed or intercepted when making API requests over plain HTTP
	 *
	 * This follows the spec for simple OAuth 1.0a authentication (RFC 5849) as closely as possible, with two exceptions:
	 *
	 * 1) There is no token associated with request/responses, only consumer keys/secrets are used
	 *
	 * 2) The OAuth parameters are included as part of the request query string instead of part of the Authorization header,
	 *    This is because there is no cross-OS function within PHP to get the raw Authorization header
	 *
	 * @link http://tools.ietf.org/html/rfc5849 for the full spec
	 * @since 2.1
	 * @return array
	 * @throws Exception
	 */
	private function perform_oauth_authentication() {

		$params = WC()->api->server->params['GET'];

		$param_names = array( 'oauth_consumer_key', 'oauth_timestamp', 'oauth_nonce', 'oauth_signature', 'oauth_signature_method' );

		// Check for required OAuth parameters
		foreach ( $param_names as $param_name ) {

			if ( empty( $params[ $param_name ] ) ) {
				throw new Exception( sprintf( __( '%s parameter is missing', 'woocommerce' ), $param_name ), 404 );
			}
		}

		// Fetch WP user by consumer key
		$keys = $this->get_keys_by_consumer_key( $params['oauth_consumer_key'] );

		// Perform OAuth validation
		$this->check_oauth_signature( $keys, $params );
		$this->check_oauth_timestamp_and_nonce( $keys, $params['oauth_timestamp'], $params['oauth_nonce'] );

		// Authentication successful, return user
		return $keys;
	}

	/**
	 * Return the keys for the given consumer key
	 *
	 * @since 2.4.0
	 * @param string $consumer_key
	 * @return array
	 * @throws Exception
	 */
	private function get_keys_by_consumer_key( $consumer_key ) {
		global $wpdb;

		$consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) );

		$keys = $wpdb->get_row( $wpdb->prepare( "
			SELECT key_id, user_id, permissions, consumer_key, consumer_secret, nonces
			FROM {$wpdb->prefix}woocommerce_api_keys
			WHERE consumer_key = '%s'
		", $consumer_key ), ARRAY_A );

		if ( empty( $keys ) ) {
			throw new Exception( __( 'Consumer key is invalid.', 'woocommerce' ), 401 );
		}

		return $keys;
	}

	/**
	 * Get user by ID
	 *
	 * @since  2.4.0
	 *
	 * @param  int $user_id
	 *
	 * @return WP_User
	 * @throws Exception
	 */
	private function get_user_by_id( $user_id ) {
		$user = get_user_by( 'id', $user_id );

		if ( ! $user ) {
			throw new Exception( __( 'API user is invalid', 'woocommerce' ), 401 );
		}

		return $user;
	}

	/**
	 * Check if the consumer secret provided for the given user is valid
	 *
	 * @since 2.1
	 * @param string $keys_consumer_secret
	 * @param string $consumer_secret
	 * @return bool
	 */
	private function is_consumer_secret_valid( $keys_consumer_secret, $consumer_secret ) {
		return hash_equals( $keys_consumer_secret, $consumer_secret );
	}

	/**
	 * Verify that the consumer-provided request signature matches our generated signature, this ensures the consumer
	 * has a valid key/secret
	 *
	 * @param array $keys
	 * @param array $params the request parameters
	 * @throws Exception
	 */
	private function check_oauth_signature( $keys, $params ) {
		$http_method = strtoupper( WC()->api->server->method );

		$server_path = WC()->api->server->path;

		// if the requested URL has a trailingslash, make sure our base URL does as well
		if ( isset( $_SERVER['REDIRECT_URL'] ) && '/' === substr( $_SERVER['REDIRECT_URL'], -1 ) ) {
			$server_path .= '/';
		}

		$base_request_uri = rawurlencode( untrailingslashit( get_woocommerce_api_url( '' ) ) . $server_path );

		// Get the signature provided by the consumer and remove it from the parameters prior to checking the signature
		$consumer_signature = rawurldecode( str_replace( ' ', '+', $params['oauth_signature'] ) );
		unset( $params['oauth_signature'] );

		// Sort parameters
		if ( ! uksort( $params, 'strcmp' ) ) {
			throw new Exception( __( 'Invalid signature - failed to sort parameters.', 'woocommerce' ), 401 );
		}

		// Normalize parameter key/values
		$params = $this->normalize_parameters( $params );
		$query_parameters = array();
		foreach ( $params as $param_key => $param_value ) {
			if ( is_array( $param_value ) ) {
				foreach ( $param_value as $param_key_inner => $param_value_inner ) {
					$query_parameters[] = $param_key . '%255B' . $param_key_inner . '%255D%3D' . $param_value_inner;
				}
			} else {
				$query_parameters[] = $param_key . '%3D' . $param_value; // join with equals sign
			}
		}
		$query_string = implode( '%26', $query_parameters ); // join with ampersand

		$string_to_sign = $http_method . '&' . $base_request_uri . '&' . $query_string;

		if ( 'HMAC-SHA1' !== $params['oauth_signature_method'] && 'HMAC-SHA256' !== $params['oauth_signature_method'] ) {
			throw new Exception( __( 'Invalid signature - signature method is invalid.', 'woocommerce' ), 401 );
		}

		$hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) );

		$secret = $keys['consumer_secret'] . '&';
		$signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $secret, true ) );

		if ( ! hash_equals( $signature, $consumer_signature ) ) {
			throw new Exception( __( 'Invalid signature - provided signature does not match.', 'woocommerce' ), 401 );
		}
	}

	/**
	 * Normalize each parameter by assuming each parameter may have already been
	 * encoded, so attempt to decode, and then re-encode according to RFC 3986
	 *
	 * Note both the key and value is normalized so a filter param like:
	 *
	 * 'filter[period]' => 'week'
	 *
	 * is encoded to:
	 *
	 * 'filter%5Bperiod%5D' => 'week'
	 *
	 * This conforms to the OAuth 1.0a spec which indicates the entire query string
	 * should be URL encoded
	 *
	 * @since 2.1
	 * @see rawurlencode()
	 * @param array $parameters un-normalized parameters
	 * @return array normalized parameters
	 */
	private function normalize_parameters( $parameters ) {
		$keys = WC_API_Authentication::urlencode_rfc3986( array_keys( $parameters ) );
		$values = WC_API_Authentication::urlencode_rfc3986( array_values( $parameters ) );
		$parameters = array_combine( $keys, $values );
		return $parameters;
	}

	/**
	 * Encodes a value according to RFC 3986. Supports multidimensional arrays.
	 *
	 * @since 2.4
	 * @param  string|array $value The value to encode
	 * @return string|array        Encoded values
	 */
	public static function urlencode_rfc3986( $value ) {
		if ( is_array( $value ) ) {
			return array_map( array( 'WC_API_Authentication', 'urlencode_rfc3986' ), $value );
		} else {
			// Percent symbols (%) must be double-encoded
			return str_replace( '%', '%25', rawurlencode( rawurldecode( $value ) ) );
		}
	}

	/**
	 * Verify that the timestamp and nonce provided with the request are valid. This prevents replay attacks where
	 * an attacker could attempt to re-send an intercepted request at a later time.
	 *
	 * - A timestamp is valid if it is within 15 minutes of now
	 * - A nonce is valid if it has not been used within the last 15 minutes
	 *
	 * @param array $keys
	 * @param int $timestamp the unix timestamp for when the request was made
	 * @param string $nonce a unique (for the given user) 32 alphanumeric string, consumer-generated
	 * @throws Exception
	 */
	private function check_oauth_timestamp_and_nonce( $keys, $timestamp, $nonce ) {
		global $wpdb;

		$valid_window = 15 * 60; // 15 minute window

		if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) ) {
			throw new Exception( __( 'Invalid timestamp.', 'woocommerce' ), 401 );
		}

		$used_nonces = maybe_unserialize( $keys['nonces'] );

		if ( empty( $used_nonces ) ) {
			$used_nonces = array();
		}

		if ( in_array( $nonce, $used_nonces ) ) {
			throw new Exception( __( 'Invalid nonce - nonce has already been used.', 'woocommerce' ), 401 );
		}

		$used_nonces[ $timestamp ] = $nonce;

		// Remove expired nonces
		foreach ( $used_nonces as $nonce_timestamp => $nonce ) {
			if ( $nonce_timestamp < ( time() - $valid_window ) ) {
				unset( $used_nonces[ $nonce_timestamp ] );
			}
		}

		$used_nonces = maybe_serialize( $used_nonces );

		$wpdb->update(
			$wpdb->prefix . 'woocommerce_api_keys',
			array( 'nonces' => $used_nonces ),
			array( 'key_id' => $keys['key_id'] ),
			array( '%s' ),
			array( '%d' )
		);
	}

	/**
	 * Check that the API keys provided have the proper key-specific permissions to either read or write API resources
	 *
	 * @param string $key_permissions
	 * @throws Exception if the permission check fails
	 */
	public function check_api_key_permissions( $key_permissions ) {
		switch ( WC()->api->server->method ) {

			case 'HEAD':
			case 'GET':
				if ( 'read' !== $key_permissions && 'read_write' !== $key_permissions ) {
					throw new Exception( __( 'The API key provided does not have read permissions.', 'woocommerce' ), 401 );
				}
				break;

			case 'POST':
			case 'PUT':
			case 'PATCH':
			case 'DELETE':
				if ( 'write' !== $key_permissions && 'read_write' !== $key_permissions ) {
					throw new Exception( __( 'The API key provided does not have write permissions.', 'woocommerce' ), 401 );
				}
				break;
		}
	}

	/**
	 * Updated API Key last access datetime
	 *
	 * @since 2.4.0
	 *
	 * @param int $key_id
	 */
	private function update_api_key_last_access( $key_id ) {
		global $wpdb;

		$wpdb->update(
			$wpdb->prefix . 'woocommerce_api_keys',
			array( 'last_access' => current_time( 'mysql' ) ),
			array( 'key_id' => $key_id ),
			array( '%s' ),
			array( '%d' )
		);
	}
}
legacy/api/v3/class-wc-api-reports.php000064400000023064151336044770013620 0ustar00<?php
/**
 * WooCommerce API Reports Class
 *
 * Handles requests to the /reports endpoint
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WC_API_Reports extends WC_API_Resource {

	/** @var string $base the route base */
	protected $base = '/reports';

	/** @var WC_Admin_Report instance */
	private $report;

	/**
	 * Register the routes for this class
	 *
	 * GET /reports
	 * GET /reports/sales
	 *
	 * @since 2.1
	 * @param array $routes
	 * @return array
	 */
	public function register_routes( $routes ) {

		# GET /reports
		$routes[ $this->base ] = array(
			array( array( $this, 'get_reports' ),     WC_API_Server::READABLE ),
		);

		# GET /reports/sales
		$routes[ $this->base . '/sales' ] = array(
			array( array( $this, 'get_sales_report' ), WC_API_Server::READABLE ),
		);

		# GET /reports/sales/top_sellers
		$routes[ $this->base . '/sales/top_sellers' ] = array(
			array( array( $this, 'get_top_sellers_report' ), WC_API_Server::READABLE ),
		);

		return $routes;
	}

	/**
	 * Get a simple listing of available reports
	 *
	 * @since 2.1
	 * @return array
	 */
	public function get_reports() {
		return array( 'reports' => array( 'sales', 'sales/top_sellers' ) );
	}

	/**
	 * Get the sales report
	 *
	 * @since 2.1
	 * @param string $fields fields to include in response
	 * @param array $filter date filtering
	 * @return array|WP_Error
	 */
	public function get_sales_report( $fields = null, $filter = array() ) {

		// check user permissions
		$check = $this->validate_request();

		// check for WP_Error
		if ( is_wp_error( $check ) ) {
			return $check;
		}

		// set date filtering
		$this->setup_report( $filter );

		// new customers
		$users_query = new WP_User_Query(
			array(
				'fields' => array( 'user_registered' ),
				'role'   => 'customer',
			)
		);

		$customers = $users_query->get_results();

		foreach ( $customers as $key => $customer ) {
			if ( strtotime( $customer->user_registered ) < $this->report->start_date || strtotime( $customer->user_registered ) > $this->report->end_date ) {
				unset( $customers[ $key ] );
			}
		}

		$total_customers = count( $customers );
		$report_data     = $this->report->get_report_data();
		$period_totals   = array();

		// setup period totals by ensuring each period in the interval has data
		for ( $i = 0; $i <= $this->report->chart_interval; $i ++ ) {

			switch ( $this->report->chart_groupby ) {
				case 'day' :
					$time = date( 'Y-m-d', strtotime( "+{$i} DAY", $this->report->start_date ) );
					break;
				default :
					$time = date( 'Y-m', strtotime( "+{$i} MONTH", $this->report->start_date ) );
					break;
			}

			// set the customer signups for each period
			$customer_count = 0;
			foreach ( $customers as $customer ) {
				if ( date( ( 'day' == $this->report->chart_groupby ) ? 'Y-m-d' : 'Y-m', strtotime( $customer->user_registered ) ) == $time ) {
					$customer_count++;
				}
 			}

			$period_totals[ $time ] = array(
				'sales'     => wc_format_decimal( 0.00, 2 ),
				'orders'    => 0,
				'items'     => 0,
				'tax'       => wc_format_decimal( 0.00, 2 ),
				'shipping'  => wc_format_decimal( 0.00, 2 ),
				'discount'  => wc_format_decimal( 0.00, 2 ),
				'customers' => $customer_count,
			);
		}

		// add total sales, total order count, total tax and total shipping for each period
		foreach ( $report_data->orders as $order ) {
			$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) );

			if ( ! isset( $period_totals[ $time ] ) ) {
				continue;
			}

			$period_totals[ $time ]['sales']    = wc_format_decimal( $order->total_sales, 2 );
			$period_totals[ $time ]['tax']      = wc_format_decimal( $order->total_tax + $order->total_shipping_tax, 2 );
			$period_totals[ $time ]['shipping'] = wc_format_decimal( $order->total_shipping, 2 );
		}

		foreach ( $report_data->order_counts as $order ) {
			$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) );

			if ( ! isset( $period_totals[ $time ] ) ) {
				continue;
			}

			$period_totals[ $time ]['orders']   = (int) $order->count;
		}

		// add total order items for each period
		foreach ( $report_data->order_items as $order_item ) {
			$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order_item->post_date ) ) : date( 'Y-m', strtotime( $order_item->post_date ) );

			if ( ! isset( $period_totals[ $time ] ) ) {
				continue;
			}

			$period_totals[ $time ]['items'] = (int) $order_item->order_item_count;
		}

		// add total discount for each period
		foreach ( $report_data->coupons as $discount ) {
			$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $discount->post_date ) ) : date( 'Y-m', strtotime( $discount->post_date ) );

			if ( ! isset( $period_totals[ $time ] ) ) {
				continue;
			}

			$period_totals[ $time ]['discount'] = wc_format_decimal( $discount->discount_amount, 2 );
		}

		$sales_data  = array(
			'total_sales'       => $report_data->total_sales,
			'net_sales'         => $report_data->net_sales,
			'average_sales'     => $report_data->average_sales,
			'total_orders'      => $report_data->total_orders,
			'total_items'       => $report_data->total_items,
			'total_tax'         => wc_format_decimal( $report_data->total_tax + $report_data->total_shipping_tax, 2 ),
			'total_shipping'    => $report_data->total_shipping,
			'total_refunds'     => $report_data->total_refunds,
			'total_discount'    => $report_data->total_coupons,
			'totals_grouped_by' => $this->report->chart_groupby,
			'totals'            => $period_totals,
			'total_customers'   => $total_customers,
		);

		return array( 'sales' => apply_filters( 'woocommerce_api_report_response', $sales_data, $this->report, $fields, $this->server ) );
	}

	/**
	 * Get the top sellers report
	 *
	 * @since 2.1
	 * @param string $fields fields to include in response
	 * @param array $filter date filtering
	 * @return array|WP_Error
	 */
	public function get_top_sellers_report( $fields = null, $filter = array() ) {

		// check user permissions
		$check = $this->validate_request();

		if ( is_wp_error( $check ) ) {
			return $check;
		}

		// set date filtering
		$this->setup_report( $filter );

		$top_sellers = $this->report->get_order_report_data( array(
			'data' => array(
				'_product_id' => array(
					'type'            => 'order_item_meta',
					'order_item_type' => 'line_item',
					'function'        => '',
					'name'            => 'product_id',
				),
				'_qty' => array(
					'type'            => 'order_item_meta',
					'order_item_type' => 'line_item',
					'function'        => 'SUM',
					'name'            => 'order_item_qty',
				),
			),
			'order_by'     => 'order_item_qty DESC',
			'group_by'     => 'product_id',
			'limit'        => isset( $filter['limit'] ) ? absint( $filter['limit'] ) : 12,
			'query_type'   => 'get_results',
			'filter_range' => true,
		) );

		$top_sellers_data = array();

		foreach ( $top_sellers as $top_seller ) {

			$product = wc_get_product( $top_seller->product_id );

			if ( $product ) {
				$top_sellers_data[] = array(
					'title'      => $product->get_name(),
					'product_id' => $top_seller->product_id,
					'quantity'   => $top_seller->order_item_qty,
				);
			}
		}

		return array( 'top_sellers' => apply_filters( 'woocommerce_api_report_response', $top_sellers_data, $this->report, $fields, $this->server ) );
	}

	/**
	 * Setup the report object and parse any date filtering
	 *
	 * @since 2.1
	 * @param array $filter date filtering
	 */
	private function setup_report( $filter ) {

		include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' );
		include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-report-sales-by-date.php' );

		$this->report = new WC_Report_Sales_By_Date();

		if ( empty( $filter['period'] ) ) {

			// custom date range
			$filter['period'] = 'custom';

			if ( ! empty( $filter['date_min'] ) || ! empty( $filter['date_max'] ) ) {

				// overwrite _GET to make use of WC_Admin_Report::calculate_current_range() for custom date ranges
				$_GET['start_date'] = $this->server->parse_datetime( $filter['date_min'] );
				$_GET['end_date'] = isset( $filter['date_max'] ) ? $this->server->parse_datetime( $filter['date_max'] ) : null;

			} else {

				// default custom range to today
				$_GET['start_date'] = $_GET['end_date'] = date( 'Y-m-d', current_time( 'timestamp' ) );
			}
		} else {

			// ensure period is valid
			if ( ! in_array( $filter['period'], array( 'week', 'month', 'last_month', 'year' ) ) ) {
				$filter['period'] = 'week';
			}

			// TODO: change WC_Admin_Report class to use "week" instead, as it's more consistent with other periods
			// allow "week" for period instead of "7day"
			if ( 'week' === $filter['period'] ) {
				$filter['period'] = '7day';
			}
		}

		$this->report->calculate_current_range( $filter['period'] );
	}

	/**
	 * Verify that the current user has permission to view reports
	 *
	 * @since 2.1
	 * @see WC_API_Resource::validate_request()
	 *
	 * @param null $id unused
	 * @param null $type unused
	 * @param null $context unused
	 *
	 * @return true|WP_Error
	 */
	protected function validate_request( $id = null, $type = null, $context = null ) {

		if ( current_user_can( 'view_woocommerce_reports' ) ) {
			return true;
		}

		return new WP_Error(
			'woocommerce_api_user_cannot_read_report',
			__( 'You do not have permission to read this report', 'woocommerce' ),
			array( 'status' => 401 )
		);
	}
}
legacy/api/v3/class-wc-api-resource.php000064400000033363151336044770013754 0ustar00<?php
/**
 * WooCommerce API Resource class
 *
 * Provides shared functionality for resource-specific API classes
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WC_API_Resource {

	/** @var WC_API_Server the API server */
	protected $server;

	/** @var string sub-classes override this to set a resource-specific base route */
	protected $base;

	/**
	 * Setup class
	 *
	 * @since 2.1
	 * @param WC_API_Server $server
	 */
	public function __construct( WC_API_Server $server ) {

		$this->server = $server;

		// automatically register routes for sub-classes
		add_filter( 'woocommerce_api_endpoints', array( $this, 'register_routes' ) );

		// maybe add meta to top-level resource responses
		foreach ( array( 'order', 'coupon', 'customer', 'product', 'report' ) as $resource ) {
			add_filter( "woocommerce_api_{$resource}_response", array( $this, 'maybe_add_meta' ), 15, 2 );
		}

		$response_names = array(
			'order',
			'coupon',
			'customer',
			'product',
			'report',
			'customer_orders',
			'customer_downloads',
			'order_note',
			'order_refund',
			'product_reviews',
			'product_category',
			'tax',
			'tax_class',
		);

		foreach ( $response_names as $name ) {

			/**
			 * Remove fields from responses when requests specify certain fields
			 * note these are hooked at a later priority so data added via
			 * filters (e.g. customer data to the order response) still has the
			 * fields filtered properly
			 */
			add_filter( "woocommerce_api_{$name}_response", array( $this, 'filter_response_fields' ), 20, 3 );
		}
	}

	/**
	 * Validate the request by checking:
	 *
	 * 1) the ID is a valid integer
	 * 2) the ID returns a valid post object and matches the provided post type
	 * 3) the current user has the proper permissions to read/edit/delete the post
	 *
	 * @since 2.1
	 * @param string|int $id the post ID
	 * @param string $type the post type, either `shop_order`, `shop_coupon`, or `product`
	 * @param string $context the context of the request, either `read`, `edit` or `delete`
	 * @return int|WP_Error valid post ID or WP_Error if any of the checks fails
	 */
	protected function validate_request( $id, $type, $context ) {

		if ( 'shop_order' === $type || 'shop_coupon' === $type || 'shop_webhook' === $type ) {
			$resource_name = str_replace( 'shop_', '', $type );
		} else {
			$resource_name = $type;
		}

		$id = absint( $id );

		// Validate ID
		if ( empty( $id ) ) {
			return new WP_Error( "woocommerce_api_invalid_{$resource_name}_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) );
		}

		// Only custom post types have per-post type/permission checks
		if ( 'customer' !== $type ) {

			$post = get_post( $id );

			if ( null === $post ) {
				return new WP_Error( "woocommerce_api_no_{$resource_name}_found", sprintf( __( 'No %1$s found with the ID equal to %2$s', 'woocommerce' ), $resource_name, $id ), array( 'status' => 404 ) );
			}

			// For checking permissions, product variations are the same as the product post type
			$post_type = ( 'product_variation' === $post->post_type ) ? 'product' : $post->post_type;

			// Validate post type
			if ( $type !== $post_type ) {
				return new WP_Error( "woocommerce_api_invalid_{$resource_name}", sprintf( __( 'Invalid %s', 'woocommerce' ), $resource_name ), array( 'status' => 404 ) );
			}

			// Validate permissions
			switch ( $context ) {

				case 'read':
					if ( ! $this->is_readable( $post ) ) {
						return new WP_Error( "woocommerce_api_user_cannot_read_{$resource_name}", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
					}
					break;

				case 'edit':
					if ( ! $this->is_editable( $post ) ) {
						return new WP_Error( "woocommerce_api_user_cannot_edit_{$resource_name}", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
					}
					break;

				case 'delete':
					if ( ! $this->is_deletable( $post ) ) {
						return new WP_Error( "woocommerce_api_user_cannot_delete_{$resource_name}", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
					}
					break;
			}
		}

		return $id;
	}

	/**
	 * Add common request arguments to argument list before WP_Query is run
	 *
	 * @since 2.1
	 * @param array $base_args required arguments for the query (e.g. `post_type`, etc)
	 * @param array $request_args arguments provided in the request
	 * @return array
	 */
	protected function merge_query_args( $base_args, $request_args ) {

		$args = array();

		// date
		if ( ! empty( $request_args['created_at_min'] ) || ! empty( $request_args['created_at_max'] ) || ! empty( $request_args['updated_at_min'] ) || ! empty( $request_args['updated_at_max'] ) ) {

			$args['date_query'] = array();

			// resources created after specified date
			if ( ! empty( $request_args['created_at_min'] ) ) {
				$args['date_query'][] = array( 'column' => 'post_date_gmt', 'after' => $this->server->parse_datetime( $request_args['created_at_min'] ), 'inclusive' => true );
			}

			// resources created before specified date
			if ( ! empty( $request_args['created_at_max'] ) ) {
				$args['date_query'][] = array( 'column' => 'post_date_gmt', 'before' => $this->server->parse_datetime( $request_args['created_at_max'] ), 'inclusive' => true );
			}

			// resources updated after specified date
			if ( ! empty( $request_args['updated_at_min'] ) ) {
				$args['date_query'][] = array( 'column' => 'post_modified_gmt', 'after' => $this->server->parse_datetime( $request_args['updated_at_min'] ), 'inclusive' => true );
			}

			// resources updated before specified date
			if ( ! empty( $request_args['updated_at_max'] ) ) {
				$args['date_query'][] = array( 'column' => 'post_modified_gmt', 'before' => $this->server->parse_datetime( $request_args['updated_at_max'] ), 'inclusive' => true );
			}
		}

		// search
		if ( ! empty( $request_args['q'] ) ) {
			$args['s'] = $request_args['q'];
		}

		// resources per response
		if ( ! empty( $request_args['limit'] ) ) {
			$args['posts_per_page'] = $request_args['limit'];
		}

		// resource offset
		if ( ! empty( $request_args['offset'] ) ) {
			$args['offset'] = $request_args['offset'];
		}

		// order (ASC or DESC, ASC by default)
		if ( ! empty( $request_args['order'] ) ) {
			$args['order'] = $request_args['order'];
		}

		// orderby
		if ( ! empty( $request_args['orderby'] ) ) {
			$args['orderby'] = $request_args['orderby'];

			// allow sorting by meta value
			if ( ! empty( $request_args['orderby_meta_key'] ) ) {
				$args['meta_key'] = $request_args['orderby_meta_key'];
			}
		}

		// allow post status change
		if ( ! empty( $request_args['post_status'] ) ) {
			$args['post_status'] = $request_args['post_status'];
			unset( $request_args['post_status'] );
		}

		// filter by a list of post id
		if ( ! empty( $request_args['in'] ) ) {
			$args['post__in'] = explode( ',', $request_args['in'] );
			unset( $request_args['in'] );
		}

		// exclude by a list of post id
		if ( ! empty( $request_args['not_in'] ) ) {
			$args['post__not_in'] = explode( ',', $request_args['not_in'] );
			unset( $request_args['not_in'] );
		}

		// resource page
		$args['paged'] = ( isset( $request_args['page'] ) ) ? absint( $request_args['page'] ) : 1;

		$args = apply_filters( 'woocommerce_api_query_args', $args, $request_args );

		return array_merge( $base_args, $args );
	}

	/**
	 * Add meta to resources when requested by the client. Meta is added as a top-level
	 * `<resource_name>_meta` attribute (e.g. `order_meta`) as a list of key/value pairs
	 *
	 * @since 2.1
	 * @param array $data the resource data
	 * @param object $resource the resource object (e.g WC_Order)
	 * @return mixed
	 */
	public function maybe_add_meta( $data, $resource ) {

		if ( isset( $this->server->params['GET']['filter']['meta'] ) && 'true' === $this->server->params['GET']['filter']['meta'] && is_object( $resource ) ) {

			// don't attempt to add meta more than once
			if ( preg_grep( '/[a-z]+_meta/', array_keys( $data ) ) ) {
				return $data;
			}

			// define the top-level property name for the meta
			switch ( get_class( $resource ) ) {

				case 'WC_Order':
					$meta_name = 'order_meta';
					break;

				case 'WC_Coupon':
					$meta_name = 'coupon_meta';
					break;

				case 'WP_User':
					$meta_name = 'customer_meta';
					break;

				default:
					$meta_name = 'product_meta';
					break;
			}

			if ( is_a( $resource, 'WP_User' ) ) {

				// customer meta
				$meta = (array) get_user_meta( $resource->ID );

			} else {

				// coupon/order/product meta
				$meta = (array) get_post_meta( $resource->get_id() );
			}

			foreach ( $meta as $meta_key => $meta_value ) {

				// don't add hidden meta by default
				if ( ! is_protected_meta( $meta_key ) ) {
					$data[ $meta_name ][ $meta_key ] = maybe_unserialize( $meta_value[0] );
				}
			}
		}

		return $data;
	}

	/**
	 * Restrict the fields included in the response if the request specified certain only certain fields should be returned
	 *
	 * @since 2.1
	 * @param array $data the response data
	 * @param object $resource the object that provided the response data, e.g. WC_Coupon or WC_Order
	 * @param array|string the requested list of fields to include in the response
	 * @return array response data
	 */
	public function filter_response_fields( $data, $resource, $fields ) {

		if ( ! is_array( $data ) || empty( $fields ) ) {
			return $data;
		}

		$fields = explode( ',', $fields );
		$sub_fields = array();

		// get sub fields
		foreach ( $fields as $field ) {

			if ( false !== strpos( $field, '.' ) ) {

				list( $name, $value ) = explode( '.', $field );

				$sub_fields[ $name ] = $value;
			}
		}

		// iterate through top-level fields
		foreach ( $data as $data_field => $data_value ) {

			// if a field has sub-fields and the top-level field has sub-fields to filter
			if ( is_array( $data_value ) && in_array( $data_field, array_keys( $sub_fields ) ) ) {

				// iterate through each sub-field
				foreach ( $data_value as $sub_field => $sub_field_value ) {

					// remove non-matching sub-fields
					if ( ! in_array( $sub_field, $sub_fields ) ) {
						unset( $data[ $data_field ][ $sub_field ] );
					}
				}
			} else {

				// remove non-matching top-level fields
				if ( ! in_array( $data_field, $fields ) ) {
					unset( $data[ $data_field ] );
				}
			}
		}

		return $data;
	}

	/**
	 * Delete a given resource
	 *
	 * @since 2.1
	 * @param int $id the resource ID
	 * @param string $type the resource post type, or `customer`
	 * @param bool $force true to permanently delete resource, false to move to trash (not supported for `customer`)
	 * @return array|WP_Error
	 */
	protected function delete( $id, $type, $force = false ) {

		if ( 'shop_order' === $type || 'shop_coupon' === $type ) {
			$resource_name = str_replace( 'shop_', '', $type );
		} else {
			$resource_name = $type;
		}

		if ( 'customer' === $type ) {

			$result = wp_delete_user( $id );

			if ( $result ) {
				return array( 'message' => __( 'Permanently deleted customer', 'woocommerce' ) );
			} else {
				return new WP_Error( 'woocommerce_api_cannot_delete_customer', __( 'The customer cannot be deleted', 'woocommerce' ), array( 'status' => 500 ) );
			}
		} else {

			// delete order/coupon/product/webhook
			$result = ( $force ) ? wp_delete_post( $id, true ) : wp_trash_post( $id );

			if ( ! $result ) {
				return new WP_Error( "woocommerce_api_cannot_delete_{$resource_name}", sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), $resource_name ), array( 'status' => 500 ) );
			}

			if ( $force ) {
				return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), $resource_name ) );

			} else {

				$this->server->send_status( '202' );

				return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), $resource_name ) );
			}
		}
	}


	/**
	 * Checks if the given post is readable by the current user
	 *
	 * @since 2.1
	 * @see WC_API_Resource::check_permission()
	 * @param WP_Post|int $post
	 * @return bool
	 */
	protected function is_readable( $post ) {

		return $this->check_permission( $post, 'read' );
	}

	/**
	 * Checks if the given post is editable by the current user
	 *
	 * @since 2.1
	 * @see WC_API_Resource::check_permission()
	 * @param WP_Post|int $post
	 * @return bool
	 */
	protected function is_editable( $post ) {

		return $this->check_permission( $post, 'edit' );

	}

	/**
	 * Checks if the given post is deletable by the current user
	 *
	 * @since 2.1
	 * @see WC_API_Resource::check_permission()
	 * @param WP_Post|int $post
	 * @return bool
	 */
	protected function is_deletable( $post ) {

		return $this->check_permission( $post, 'delete' );
	}

	/**
	 * Checks the permissions for the current user given a post and context
	 *
	 * @since 2.1
	 * @param WP_Post|int $post
	 * @param string $context the type of permission to check, either `read`, `write`, or `delete`
	 * @return bool true if the current user has the permissions to perform the context on the post
	 */
	private function check_permission( $post, $context ) {
		$permission = false;

		if ( ! is_a( $post, 'WP_Post' ) ) {
			$post = get_post( $post );
		}

		if ( is_null( $post ) ) {
			return $permission;
		}

		$post_type = get_post_type_object( $post->post_type );

		if ( 'read' === $context ) {
			$permission = 'revision' !== $post->post_type && current_user_can( $post_type->cap->read_private_posts, $post->ID );
		} elseif ( 'edit' === $context ) {
			$permission = current_user_can( $post_type->cap->edit_post, $post->ID );
		} elseif ( 'delete' === $context ) {
			$permission = current_user_can( $post_type->cap->delete_post, $post->ID );
		}

		return apply_filters( 'woocommerce_api_check_permission', $permission, $context, $post, $post_type );
	}
}
legacy/api/v3/class-wc-api-orders.php000064400000172262151336044770013425 0ustar00<?php
/**
 * WooCommerce API Orders Class
 *
 * Handles requests to the /orders endpoint
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WC_API_Orders extends WC_API_Resource {

	/** @var string $base the route base */
	protected $base = '/orders';

	/** @var string $post_type the custom post type */
	protected $post_type = 'shop_order';

	/**
	 * Register the routes for this class
	 *
	 * GET|POST /orders
	 * GET /orders/count
	 * GET|PUT|DELETE /orders/<id>
	 * GET /orders/<id>/notes
	 *
	 * @since 2.1
	 * @param array $routes
	 * @return array
	 */
	public function register_routes( $routes ) {

		# GET|POST /orders
		$routes[ $this->base ] = array(
			array( array( $this, 'get_orders' ),     WC_API_Server::READABLE ),
			array( array( $this, 'create_order' ),   WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET /orders/count
		$routes[ $this->base . '/count' ] = array(
			array( array( $this, 'get_orders_count' ), WC_API_Server::READABLE ),
		);

		# GET /orders/statuses
		$routes[ $this->base . '/statuses' ] = array(
			array( array( $this, 'get_order_statuses' ), WC_API_Server::READABLE ),
		);

		# GET|PUT|DELETE /orders/<id>
		$routes[ $this->base . '/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_order' ),  WC_API_Server::READABLE ),
			array( array( $this, 'edit_order' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
			array( array( $this, 'delete_order' ), WC_API_Server::DELETABLE ),
		);

		# GET|POST /orders/<id>/notes
		$routes[ $this->base . '/(?P<order_id>\d+)/notes' ] = array(
			array( array( $this, 'get_order_notes' ), WC_API_Server::READABLE ),
			array( array( $this, 'create_order_note' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET|PUT|DELETE /orders/<order_id>/notes/<id>
		$routes[ $this->base . '/(?P<order_id>\d+)/notes/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_order_note' ), WC_API_Server::READABLE ),
			array( array( $this, 'edit_order_note' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ),
			array( array( $this, 'delete_order_note' ), WC_API_SERVER::DELETABLE ),
		);

		# GET|POST /orders/<order_id>/refunds
		$routes[ $this->base . '/(?P<order_id>\d+)/refunds' ] = array(
			array( array( $this, 'get_order_refunds' ), WC_API_Server::READABLE ),
			array( array( $this, 'create_order_refund' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET|PUT|DELETE /orders/<order_id>/refunds/<id>
		$routes[ $this->base . '/(?P<order_id>\d+)/refunds/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_order_refund' ), WC_API_Server::READABLE ),
			array( array( $this, 'edit_order_refund' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ),
			array( array( $this, 'delete_order_refund' ), WC_API_SERVER::DELETABLE ),
		);

		# POST|PUT /orders/bulk
		$routes[ $this->base . '/bulk' ] = array(
			array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
		);

		return $routes;
	}

	/**
	 * Get all orders
	 *
	 * @since 2.1
	 * @param string $fields
	 * @param array $filter
	 * @param string $status
	 * @param int $page
	 * @return array
	 */
	public function get_orders( $fields = null, $filter = array(), $status = null, $page = 1 ) {

		if ( ! empty( $status ) ) {
			$filter['status'] = $status;
		}

		$filter['page'] = $page;

		$query = $this->query_orders( $filter );

		$orders = array();

		foreach ( $query->posts as $order_id ) {

			if ( ! $this->is_readable( $order_id ) ) {
				continue;
			}

			$orders[] = current( $this->get_order( $order_id, $fields, $filter ) );
		}

		$this->server->add_pagination_headers( $query );

		return array( 'orders' => $orders );
	}


	/**
	 * Get the order for the given ID.
	 *
	 * @since 2.1
	 * @param int $id The order ID.
	 * @param array $fields Request fields.
	 * @param array $filter Request filters.
	 * @return array|WP_Error
	 */
	public function get_order( $id, $fields = null, $filter = array() ) {

		// Ensure order ID is valid & user has permission to read.
		$id = $this->validate_request( $id, $this->post_type, 'read' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		// Get the decimal precession.
		$dp     = ( isset( $filter['dp'] ) ? intval( $filter['dp'] ) : 2 );
		$order  = wc_get_order( $id );
		$expand = array();

		if ( ! empty( $filter['expand'] ) ) {
			$expand = explode( ',', $filter['expand'] );
		}

		$order_data = array(
			'id'                        => $order->get_id(),
			'order_number'              => $order->get_order_number(),
			'order_key'                 => $order->get_order_key(),
			'created_at'                => $this->server->format_datetime( $order->get_date_created() ? $order->get_date_created()->getTimestamp() : 0, false, false ), // API gives UTC times.
			'updated_at'                => $this->server->format_datetime( $order->get_date_modified() ? $order->get_date_modified()->getTimestamp() : 0, false, false ), // API gives UTC times.
			'completed_at'              => $this->server->format_datetime( $order->get_date_completed() ? $order->get_date_completed()->getTimestamp() : 0, false, false ), // API gives UTC times.
			'status'                    => $order->get_status(),
			'currency'                  => $order->get_currency(),
			'total'                     => wc_format_decimal( $order->get_total(), $dp ),
			'subtotal'                  => wc_format_decimal( $order->get_subtotal(), $dp ),
			'total_line_items_quantity' => $order->get_item_count(),
			'total_tax'                 => wc_format_decimal( $order->get_total_tax(), $dp ),
			'total_shipping'            => wc_format_decimal( $order->get_shipping_total(), $dp ),
			'cart_tax'                  => wc_format_decimal( $order->get_cart_tax(), $dp ),
			'shipping_tax'              => wc_format_decimal( $order->get_shipping_tax(), $dp ),
			'total_discount'            => wc_format_decimal( $order->get_total_discount(), $dp ),
			'shipping_methods'          => $order->get_shipping_method(),
			'payment_details' => array(
				'method_id'    => $order->get_payment_method(),
				'method_title' => $order->get_payment_method_title(),
				'paid'         => ! is_null( $order->get_date_paid() ),
			),
			'billing_address' => array(
				'first_name' => $order->get_billing_first_name(),
				'last_name'  => $order->get_billing_last_name(),
				'company'    => $order->get_billing_company(),
				'address_1'  => $order->get_billing_address_1(),
				'address_2'  => $order->get_billing_address_2(),
				'city'       => $order->get_billing_city(),
				'state'      => $order->get_billing_state(),
				'postcode'   => $order->get_billing_postcode(),
				'country'    => $order->get_billing_country(),
				'email'      => $order->get_billing_email(),
				'phone'      => $order->get_billing_phone(),
			),
			'shipping_address' => array(
				'first_name' => $order->get_shipping_first_name(),
				'last_name'  => $order->get_shipping_last_name(),
				'company'    => $order->get_shipping_company(),
				'address_1'  => $order->get_shipping_address_1(),
				'address_2'  => $order->get_shipping_address_2(),
				'city'       => $order->get_shipping_city(),
				'state'      => $order->get_shipping_state(),
				'postcode'   => $order->get_shipping_postcode(),
				'country'    => $order->get_shipping_country(),
			),
			'note'                      => $order->get_customer_note(),
			'customer_ip'               => $order->get_customer_ip_address(),
			'customer_user_agent'       => $order->get_customer_user_agent(),
			'customer_id'               => $order->get_user_id(),
			'view_order_url'            => $order->get_view_order_url(),
			'line_items'                => array(),
			'shipping_lines'            => array(),
			'tax_lines'                 => array(),
			'fee_lines'                 => array(),
			'coupon_lines'              => array(),
		);

		// Add line items.
		foreach ( $order->get_items() as $item_id => $item ) {
			$product    = $item->get_product();
			$hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_';
			$item_meta  = $item->get_formatted_meta_data( $hideprefix );

			foreach ( $item_meta as $key => $values ) {
				$item_meta[ $key ]->label = $values->display_key;
				unset( $item_meta[ $key ]->display_key );
				unset( $item_meta[ $key ]->display_value );
			}

			$line_item = array(
				'id'           => $item_id,
				'subtotal'     => wc_format_decimal( $order->get_line_subtotal( $item, false, false ), $dp ),
				'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), $dp ),
				'total'        => wc_format_decimal( $order->get_line_total( $item, false, false ), $dp ),
				'total_tax'    => wc_format_decimal( $item->get_total_tax(), $dp ),
				'price'        => wc_format_decimal( $order->get_item_total( $item, false, false ), $dp ),
				'quantity'     => $item->get_quantity(),
				'tax_class'    => $item->get_tax_class(),
				'name'         => $item->get_name(),
				'product_id'   => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(),
				'sku'          => is_object( $product ) ? $product->get_sku() : null,
				'meta'         => array_values( $item_meta ),
			);

			if ( in_array( 'products', $expand ) && is_object( $product ) ) {
				$_product_data = WC()->api->WC_API_Products->get_product( $product->get_id() );

				if ( isset( $_product_data['product'] ) ) {
					$line_item['product_data'] = $_product_data['product'];
				}
			}

			$order_data['line_items'][] = $line_item;
		}

		// Add shipping.
		foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) {
			$order_data['shipping_lines'][] = array(
				'id'           => $shipping_item_id,
				'method_id'    => $shipping_item->get_method_id(),
				'method_title' => $shipping_item->get_name(),
				'total'        => wc_format_decimal( $shipping_item->get_total(), $dp ),
			);
		}

		// Add taxes.
		foreach ( $order->get_tax_totals() as $tax_code => $tax ) {
			$tax_line = array(
				'id'       => $tax->id,
				'rate_id'  => $tax->rate_id,
				'code'     => $tax_code,
				'title'    => $tax->label,
				'total'    => wc_format_decimal( $tax->amount, $dp ),
				'compound' => (bool) $tax->is_compound,
			);

			if ( in_array( 'taxes', $expand ) ) {
				$_rate_data = WC()->api->WC_API_Taxes->get_tax( $tax->rate_id );

				if ( isset( $_rate_data['tax'] ) ) {
					$tax_line['rate_data'] = $_rate_data['tax'];
				}
			}

			$order_data['tax_lines'][] = $tax_line;
		}

		// Add fees.
		foreach ( $order->get_fees() as $fee_item_id => $fee_item ) {
			$order_data['fee_lines'][] = array(
				'id'        => $fee_item_id,
				'title'     => $fee_item->get_name(),
				'tax_class' => $fee_item->get_tax_class(),
				'total'     => wc_format_decimal( $order->get_line_total( $fee_item ), $dp ),
				'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), $dp ),
			);
		}

		// Add coupons.
		foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) {
			$coupon_line = array(
				'id'     => $coupon_item_id,
				'code'   => $coupon_item->get_code(),
				'amount' => wc_format_decimal( $coupon_item->get_discount(), $dp ),
			);

			if ( in_array( 'coupons', $expand ) ) {
				$_coupon_data = WC()->api->WC_API_Coupons->get_coupon_by_code( $coupon_item->get_code() );

				if ( ! is_wp_error( $_coupon_data ) && isset( $_coupon_data['coupon'] ) ) {
					$coupon_line['coupon_data'] = $_coupon_data['coupon'];
				}
			}

			$order_data['coupon_lines'][] = $coupon_line;
		}

		return array( 'order' => apply_filters( 'woocommerce_api_order_response', $order_data, $order, $fields, $this->server ) );
	}

	/**
	 * Get the total number of orders
	 *
	 * @since 2.4
	 *
	 * @param string $status
	 * @param array $filter
	 *
	 * @return array|WP_Error
	 */
	public function get_orders_count( $status = null, $filter = array() ) {

		try {
			if ( ! current_user_can( 'read_private_shop_orders' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_orders_count', __( 'You do not have permission to read the orders count', 'woocommerce' ), 401 );
			}

			if ( ! empty( $status ) ) {

				if ( 'any' === $status ) {

					$order_statuses = array();

					foreach ( wc_get_order_statuses() as $slug => $name ) {
						$filter['status'] = str_replace( 'wc-', '', $slug );
						$query = $this->query_orders( $filter );
						$order_statuses[ str_replace( 'wc-', '', $slug ) ] = (int) $query->found_posts;
					}

					return array( 'count' => $order_statuses );

				} else {
					$filter['status'] = $status;
				}
			}

			$query = $this->query_orders( $filter );

			return array( 'count' => (int) $query->found_posts );

		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get a list of valid order statuses
	 *
	 * Note this requires no specific permissions other than being an authenticated
	 * API user. Order statuses (particularly custom statuses) could be considered
	 * private information which is why it's not in the API index.
	 *
	 * @since 2.1
	 * @return array
	 */
	public function get_order_statuses() {

		$order_statuses = array();

		foreach ( wc_get_order_statuses() as $slug => $name ) {
			$order_statuses[ str_replace( 'wc-', '', $slug ) ] = $name;
		}

		return array( 'order_statuses' => apply_filters( 'woocommerce_api_order_statuses_response', $order_statuses, $this ) );
	}

	/**
	 * Create an order
	 *
	 * @since 2.2
	 * @param array $data raw order data
	 * @return array|WP_Error
	 */
	public function create_order( $data ) {
		global $wpdb;

		try {
			if ( ! isset( $data['order'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order' ), 400 );
			}

			$data = $data['order'];

			// permission check
			if ( ! current_user_can( 'publish_shop_orders' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order', __( 'You do not have permission to create orders', 'woocommerce' ), 401 );
			}

			$data = apply_filters( 'woocommerce_api_create_order_data', $data, $this );

			// default order args, note that status is checked for validity in wc_create_order()
			$default_order_args = array(
				'status'        => isset( $data['status'] ) ? $data['status'] : '',
				'customer_note' => isset( $data['note'] ) ? $data['note'] : null,
			);

			// if creating order for existing customer
			if ( ! empty( $data['customer_id'] ) ) {

				// make sure customer exists
				if ( false === get_user_by( 'id', $data['customer_id'] ) ) {
					throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 );
				}

				$default_order_args['customer_id'] = $data['customer_id'];
			}

			// create the pending order
			$order = $this->create_base_order( $default_order_args, $data );

			if ( is_wp_error( $order ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_order', sprintf( __( 'Cannot create order: %s', 'woocommerce' ), implode( ', ', $order->get_error_messages() ) ), 400 );
			}

			// billing/shipping addresses
			$this->set_order_addresses( $order, $data );

			$lines = array(
				'line_item' => 'line_items',
				'shipping'  => 'shipping_lines',
				'fee'       => 'fee_lines',
				'coupon'    => 'coupon_lines',
			);

			foreach ( $lines as $line_type => $line ) {

				if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) {

					$set_item = "set_{$line_type}";

					foreach ( $data[ $line ] as $item ) {

						$this->$set_item( $order, $item, 'create' );
					}
				}
			}

			// set is vat exempt
			if ( isset( $data['is_vat_exempt'] ) ) {
				update_post_meta( $order->get_id(), '_is_vat_exempt', $data['is_vat_exempt'] ? 'yes' : 'no' );
			}

			// calculate totals and set them
			$order->calculate_totals();

			// payment method (and payment_complete() if `paid` == true)
			if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) {

				// method ID & title are required
				if ( empty( $data['payment_details']['method_id'] ) || empty( $data['payment_details']['method_title'] ) ) {
					throw new WC_API_Exception( 'woocommerce_invalid_payment_details', __( 'Payment method ID and title are required', 'woocommerce' ), 400 );
				}

				update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] );
				update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) );

				// mark as paid if set
				if ( isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) {
					$order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' );
				}
			}

			// set order currency
			if ( isset( $data['currency'] ) ) {

				if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) {
					throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 );
				}

				update_post_meta( $order->get_id(), '_order_currency', $data['currency'] );
			}

			// set order meta
			if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) {
				$this->set_order_meta( $order->get_id(), $data['order_meta'] );
			}

			// HTTP 201 Created
			$this->server->send_status( 201 );

			wc_delete_shop_order_transients( $order );

			do_action( 'woocommerce_api_create_order', $order->get_id(), $data, $this );
			do_action( 'woocommerce_new_order', $order->get_id() );

			return $this->get_order( $order->get_id() );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Creates new WC_Order.
	 *
	 * Requires a separate function for classes that extend WC_API_Orders.
	 *
	 * @since 2.3
	 *
	 * @param $args array
	 * @param $data
	 *
	 * @return WC_Order
	 */
	protected function create_base_order( $args, $data ) {
		return wc_create_order( $args );
	}

	/**
	 * Edit an order
	 *
	 * @since 2.2
	 * @param int $id the order ID
	 * @param array $data
	 * @return array|WP_Error
	 */
	public function edit_order( $id, $data ) {
		try {
			if ( ! isset( $data['order'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order' ), 400 );
			}

			$data = $data['order'];

			$update_totals = false;

			$id = $this->validate_request( $id, $this->post_type, 'edit' );

			if ( is_wp_error( $id ) ) {
				return $id;
			}

			$data  = apply_filters( 'woocommerce_api_edit_order_data', $data, $id, $this );
			$order = wc_get_order( $id );

			if ( empty( $order ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 );
			}

			$order_args = array( 'order_id' => $order->get_id() );

			// Customer note.
			if ( isset( $data['note'] ) ) {
				$order_args['customer_note'] = $data['note'];
			}

			// Customer ID.
			if ( isset( $data['customer_id'] ) && $data['customer_id'] != $order->get_user_id() ) {
				// Make sure customer exists.
				if ( false === get_user_by( 'id', $data['customer_id'] ) ) {
					throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 );
				}

				update_post_meta( $order->get_id(), '_customer_user', $data['customer_id'] );
			}

			// Billing/shipping address.
			$this->set_order_addresses( $order, $data );

			$lines = array(
				'line_item' => 'line_items',
				'shipping'  => 'shipping_lines',
				'fee'       => 'fee_lines',
				'coupon'    => 'coupon_lines',
			);

			foreach ( $lines as $line_type => $line ) {

				if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) {

					$update_totals = true;

					foreach ( $data[ $line ] as $item ) {
						// Item ID is always required.
						if ( ! array_key_exists( 'id', $item ) ) {
							$item['id'] = null;
						}

						// Create item.
						if ( is_null( $item['id'] ) ) {
							$this->set_item( $order, $line_type, $item, 'create' );
						} elseif ( $this->item_is_null( $item ) ) {
							// Delete item.
							wc_delete_order_item( $item['id'] );
						} else {
							// Update item.
							$this->set_item( $order, $line_type, $item, 'update' );
						}
					}
				}
			}

			// Payment method (and payment_complete() if `paid` == true and order needs payment).
			if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) {

				// Method ID.
				if ( isset( $data['payment_details']['method_id'] ) ) {
					update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] );
				}

				// Method title.
				if ( isset( $data['payment_details']['method_title'] ) ) {
					update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) );
				}

				// Mark as paid if set.
				if ( $order->needs_payment() && isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) {
					$order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' );
				}
			}

			// Set order currency.
			if ( isset( $data['currency'] ) ) {
				if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) {
					throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 );
				}

				update_post_meta( $order->get_id(), '_order_currency', $data['currency'] );
			}

			// If items have changed, recalculate order totals.
			if ( $update_totals ) {
				$order->calculate_totals();
			}

			// Update order meta.
			if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) {
				$this->set_order_meta( $order->get_id(), $data['order_meta'] );
			}

			// Update the order post to set customer note/modified date.
			wc_update_order( $order_args );

			// Order status.
			if ( ! empty( $data['status'] ) ) {
				// Refresh the order instance.
				$order = wc_get_order( $order->get_id() );
				$order->update_status( $data['status'], isset( $data['status_note'] ) ? $data['status_note'] : '', true );
			}

			wc_delete_shop_order_transients( $order );

			do_action( 'woocommerce_api_edit_order', $order->get_id(), $data, $this );
			do_action( 'woocommerce_update_order', $order->get_id() );

			return $this->get_order( $id );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete an order
	 *
	 * @param int $id the order ID
	 * @param bool $force true to permanently delete order, false to move to trash
	 * @return array|WP_Error
	 */
	public function delete_order( $id, $force = false ) {

		$id = $this->validate_request( $id, $this->post_type, 'delete' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		wc_delete_shop_order_transients( $id );

		do_action( 'woocommerce_api_delete_order', $id, $this );

		return $this->delete( $id, 'order',  ( 'true' === $force ) );
	}

	/**
	 * Helper method to get order post objects
	 *
	 * @since 2.1
	 * @param array $args request arguments for filtering query
	 * @return WP_Query
	 */
	protected function query_orders( $args ) {

		// set base query arguments
		$query_args = array(
			'fields'      => 'ids',
			'post_type'   => $this->post_type,
			'post_status' => array_keys( wc_get_order_statuses() ),
		);

		// add status argument
		if ( ! empty( $args['status'] ) ) {
			$statuses                  = 'wc-' . str_replace( ',', ',wc-', $args['status'] );
			$statuses                  = explode( ',', $statuses );
			$query_args['post_status'] = $statuses;

			unset( $args['status'] );
		}

		if ( ! empty( $args['customer_id'] ) ) {
			$query_args['meta_query'] = array(
				array(
					'key'     => '_customer_user',
					'value'   => absint( $args['customer_id'] ),
					'compare' => '=',
				),
			);
		}

		$query_args = $this->merge_query_args( $query_args, $args );

		return new WP_Query( $query_args );
	}

	/**
	 * Helper method to set/update the billing & shipping addresses for
	 * an order
	 *
	 * @since 2.1
	 * @param \WC_Order $order
	 * @param array $data
	 */
	protected function set_order_addresses( $order, $data ) {

		$address_fields = array(
			'first_name',
			'last_name',
			'company',
			'email',
			'phone',
			'address_1',
			'address_2',
			'city',
			'state',
			'postcode',
			'country',
		);

		$billing_address = $shipping_address = array();

		// billing address
		if ( isset( $data['billing_address'] ) && is_array( $data['billing_address'] ) ) {

			foreach ( $address_fields as $field ) {

				if ( isset( $data['billing_address'][ $field ] ) ) {
					$billing_address[ $field ] = wc_clean( $data['billing_address'][ $field ] );
				}
			}

			unset( $address_fields['email'] );
			unset( $address_fields['phone'] );
		}

		// shipping address
		if ( isset( $data['shipping_address'] ) && is_array( $data['shipping_address'] ) ) {

			foreach ( $address_fields as $field ) {

				if ( isset( $data['shipping_address'][ $field ] ) ) {
					$shipping_address[ $field ] = wc_clean( $data['shipping_address'][ $field ] );
				}
			}
		}

		$this->update_address( $order, $billing_address, 'billing' );
		$this->update_address( $order, $shipping_address, 'shipping' );

		// update user meta
		if ( $order->get_user_id() ) {
			foreach ( $billing_address as $key => $value ) {
				update_user_meta( $order->get_user_id(), 'billing_' . $key, $value );
			}
			foreach ( $shipping_address as $key => $value ) {
				update_user_meta( $order->get_user_id(), 'shipping_' . $key, $value );
			}
		}
	}

	/**
	 * Update address.
	 *
	 * @param WC_Order $order
	 * @param array $posted
	 * @param string $type
	 */
	protected function update_address( $order, $posted, $type = 'billing' ) {
		foreach ( $posted as $key => $value ) {
			if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) {
				$order->{"set_{$type}_{$key}"}( $value );
			}
		}
	}

	/**
	 * Helper method to add/update order meta, with two restrictions:
	 *
	 * 1) Only non-protected meta (no leading underscore) can be set
	 * 2) Meta values must be scalar (int, string, bool)
	 *
	 * @since 2.2
	 * @param int $order_id valid order ID
	 * @param array $order_meta order meta in array( 'meta_key' => 'meta_value' ) format
	 */
	protected function set_order_meta( $order_id, $order_meta ) {

		foreach ( $order_meta as $meta_key => $meta_value ) {

			if ( is_string( $meta_key ) && ! is_protected_meta( $meta_key ) && is_scalar( $meta_value ) ) {
				update_post_meta( $order_id, $meta_key, $meta_value );
			}
		}
	}

	/**
	 * Helper method to check if the resource ID associated with the provided item is null
	 *
	 * Items can be deleted by setting the resource ID to null
	 *
	 * @since 2.2
	 * @param array $item item provided in the request body
	 * @return bool true if the item resource ID is null, false otherwise
	 */
	protected function item_is_null( $item ) {

		$keys = array( 'product_id', 'method_id', 'title', 'code' );

		foreach ( $keys as $key ) {
			if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Wrapper method to create/update order items
	 *
	 * When updating, the item ID provided is checked to ensure it is associated
	 * with the order.
	 *
	 * @since 2.2
	 * @param \WC_Order $order order
	 * @param string $item_type
	 * @param array $item item provided in the request body
	 * @param string $action either 'create' or 'update'
	 * @throws WC_API_Exception if item ID is not associated with order
	 */
	protected function set_item( $order, $item_type, $item, $action ) {
		global $wpdb;

		$set_method = "set_{$item_type}";

		// verify provided line item ID is associated with order
		if ( 'update' === $action ) {

			$result = $wpdb->get_row(
				$wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d AND order_id = %d",
				absint( $item['id'] ),
				absint( $order->get_id() )
			) );

			if ( is_null( $result ) ) {
				throw new WC_API_Exception( 'woocommerce_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 );
			}
		}

		$this->$set_method( $order, $item, $action );
	}

	/**
	 * Create or update a line item
	 *
	 * @since 2.2
	 * @param \WC_Order $order
	 * @param array $item line item data
	 * @param string $action 'create' to add line item or 'update' to update it
	 * @throws WC_API_Exception invalid data, server error
	 */
	protected function set_line_item( $order, $item, $action ) {
		$creating  = ( 'create' === $action );

		// product is always required
		if ( ! isset( $item['product_id'] ) && ! isset( $item['sku'] ) ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID or SKU is required', 'woocommerce' ), 400 );
		}

		// when updating, ensure product ID provided matches
		if ( 'update' === $action ) {

			$item_product_id   = wc_get_order_item_meta( $item['id'], '_product_id' );
			$item_variation_id = wc_get_order_item_meta( $item['id'], '_variation_id' );

			if ( $item['product_id'] != $item_product_id && $item['product_id'] != $item_variation_id ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID provided does not match this line item', 'woocommerce' ), 400 );
			}
		}

		if ( isset( $item['product_id'] ) ) {
			$product_id = $item['product_id'];
		} elseif ( isset( $item['sku'] ) ) {
			$product_id = wc_get_product_id_by_sku( $item['sku'] );
		}

		// variations must each have a key & value
		$variation_id = 0;
		if ( isset( $item['variations'] ) && is_array( $item['variations'] ) ) {
			foreach ( $item['variations'] as $key => $value ) {
				if ( ! $key || ! $value ) {
					throw new WC_API_Exception( 'woocommerce_api_invalid_product_variation', __( 'The product variation is invalid', 'woocommerce' ), 400 );
				}
			}
			$variation_id = $this->get_variation_id( wc_get_product( $product_id ), $item['variations'] );
		}

		$product = wc_get_product( $variation_id ? $variation_id : $product_id );

		// must be a valid WC_Product
		if ( ! is_object( $product ) ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product', __( 'Product is invalid.', 'woocommerce' ), 400 );
		}

		// quantity must be positive float
		if ( isset( $item['quantity'] ) && floatval( $item['quantity'] ) <= 0 ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity must be a positive float.', 'woocommerce' ), 400 );
		}

		// quantity is required when creating
		if ( $creating && ! isset( $item['quantity'] ) ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity is required.', 'woocommerce' ), 400 );
		}

		// quantity
		if ( $creating ) {
			$line_item = new WC_Order_Item_Product();
		} else {
			$line_item = new WC_Order_Item_Product( $item['id'] );
		}

		$line_item->set_product( $product );
		$line_item->set_order_id( $order->get_id() );

		if ( isset( $item['quantity'] ) ) {
			$line_item->set_quantity( $item['quantity'] );
		}
		if ( isset( $item['total'] ) ) {
			$line_item->set_total( floatval( $item['total'] ) );
		} elseif ( $creating ) {
			$total = wc_get_price_excluding_tax( $product, array( 'qty' => $line_item->get_quantity() ) );
			$line_item->set_total( $total );
			$line_item->set_subtotal( $total );
		}
		if ( isset( $item['total_tax'] ) ) {
			$line_item->set_total_tax( floatval( $item['total_tax'] ) );
		}
		if ( isset( $item['subtotal'] ) ) {
			$line_item->set_subtotal( floatval( $item['subtotal'] ) );
		}
		if ( isset( $item['subtotal_tax'] ) ) {
			$line_item->set_subtotal_tax( floatval( $item['subtotal_tax'] ) );
		}
		if ( $variation_id ) {
			$line_item->set_variation_id( $variation_id );
			$line_item->set_variation( $item['variations'] );
		}

		// Save or add to order.
		if ( $creating ) {
			$order->add_item( $line_item );
		} else {
			$item_id = $line_item->save();

			if ( ! $item_id ) {
				throw new WC_API_Exception( 'woocommerce_cannot_create_line_item', __( 'Cannot create line item, try again.', 'woocommerce' ), 500 );
			}
		}
	}

	/**
	 * Given a product ID & API provided variations, find the correct variation ID to use for calculation
	 * We can't just trust input from the API to pass a variation_id manually, otherwise you could pass
	 * the cheapest variation ID but provide other information so we have to look up the variation ID.
	 *
	 * @param  WC_Product $product Product instance
	 * @param array $variations
	 *
	 * @return int Returns an ID if a valid variation was found for this product
	 */
	public function get_variation_id( $product, $variations = array() ) {
		$variation_id = null;
		$variations_normalized = array();

		if ( $product->is_type( 'variable' ) && $product->has_child() ) {
			if ( isset( $variations ) && is_array( $variations ) ) {
				// start by normalizing the passed variations
				foreach ( $variations as $key => $value ) {
					$key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) ); // from get_attributes in class-wc-api-products.php
					$variations_normalized[ $key ] = strtolower( $value );
				}
				// now search through each product child and see if our passed variations match anything
				foreach ( $product->get_children() as $variation ) {
					$meta = array();
					foreach ( get_post_meta( $variation ) as $key => $value ) {
						$value = $value[0];
						$key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) );
						$meta[ $key ] = strtolower( $value );
					}
					// if the variation array is a part of the $meta array, we found our match
					if ( $this->array_contains( $variations_normalized, $meta ) ) {
						$variation_id = $variation;
						break;
					}
				}
			}
		}

		return $variation_id;
	}

	/**
	 * Utility function to see if the meta array contains data from variations
	 *
	 * @param array $needles
	 * @param array $haystack
	 *
	 * @return bool
	 */
	protected function array_contains( $needles, $haystack ) {
		foreach ( $needles as $key => $value ) {
			if ( $haystack[ $key ] !== $value ) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Create or update an order shipping method
	 *
	 * @since 2.2
	 * @param \WC_Order $order
	 * @param array $shipping item data
	 * @param string $action 'create' to add shipping or 'update' to update it
	 * @throws WC_API_Exception invalid data, server error
	 */
	protected function set_shipping( $order, $shipping, $action ) {

		// total must be a positive float
		if ( isset( $shipping['total'] ) && floatval( $shipping['total'] ) < 0 ) {
			throw new WC_API_Exception( 'woocommerce_invalid_shipping_total', __( 'Shipping total must be a positive amount.', 'woocommerce' ), 400 );
		}

		if ( 'create' === $action ) {

			// method ID is required
			if ( ! isset( $shipping['method_id'] ) ) {
				throw new WC_API_Exception( 'woocommerce_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 );
			}

			$rate = new WC_Shipping_Rate( $shipping['method_id'], isset( $shipping['method_title'] ) ? $shipping['method_title'] : '', isset( $shipping['total'] ) ? floatval( $shipping['total'] ) : 0, array(), $shipping['method_id'] );
			$item = new WC_Order_Item_Shipping();
			$item->set_order_id( $order->get_id() );
			$item->set_shipping_rate( $rate );
			$order->add_item( $item );
		} else {

			$item = new WC_Order_Item_Shipping( $shipping['id'] );

			if ( isset( $shipping['method_id'] ) ) {
				$item->set_method_id( $shipping['method_id'] );
			}

			if ( isset( $shipping['method_title'] ) ) {
				$item->set_method_title( $shipping['method_title'] );
			}

			if ( isset( $shipping['total'] ) ) {
				$item->set_total( floatval( $shipping['total'] ) );
			}

			$shipping_id = $item->save();

			if ( ! $shipping_id ) {
				throw new WC_API_Exception( 'woocommerce_cannot_update_shipping', __( 'Cannot update shipping method, try again.', 'woocommerce' ), 500 );
			}
		}
	}

	/**
	 * Create or update an order fee
	 *
	 * @since 2.2
	 * @param \WC_Order $order
	 * @param array $fee item data
	 * @param string $action 'create' to add fee or 'update' to update it
	 * @throws WC_API_Exception invalid data, server error
	 */
	protected function set_fee( $order, $fee, $action ) {

		if ( 'create' === $action ) {

			// fee title is required
			if ( ! isset( $fee['title'] ) ) {
				throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee title is required', 'woocommerce' ), 400 );
			}

			$item = new WC_Order_Item_Fee();
			$item->set_order_id( $order->get_id() );
			$item->set_name( wc_clean( $fee['title'] ) );
			$item->set_total( isset( $fee['total'] ) ? floatval( $fee['total'] ) : 0 );

			// if taxable, tax class and total are required
			if ( ! empty( $fee['taxable'] ) ) {
				if ( ! isset( $fee['tax_class'] ) ) {
					throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee tax class is required when fee is taxable.', 'woocommerce' ), 400 );
				}

				$item->set_tax_status( 'taxable' );
				$item->set_tax_class( $fee['tax_class'] );

				if ( isset( $fee['total_tax'] ) ) {
					$item->set_total_tax( isset( $fee['total_tax'] ) ? wc_format_refund_total( $fee['total_tax'] ) : 0 );
				}

				if ( isset( $fee['tax_data'] ) ) {
					$item->set_total_tax( wc_format_refund_total( array_sum( $fee['tax_data'] ) ) );
					$item->set_taxes( array_map( 'wc_format_refund_total', $fee['tax_data'] ) );
				}
			}

			$order->add_item( $item );
		} else {

			$item = new WC_Order_Item_Fee( $fee['id'] );

			if ( isset( $fee['title'] ) ) {
				$item->set_name( wc_clean( $fee['title'] ) );
			}

			if ( isset( $fee['tax_class'] ) ) {
				$item->set_tax_class( $fee['tax_class'] );
			}

			if ( isset( $fee['total'] ) ) {
				$item->set_total( floatval( $fee['total'] ) );
			}

			if ( isset( $fee['total_tax'] ) ) {
				$item->set_total_tax( floatval( $fee['total_tax'] ) );
			}

			$fee_id = $item->save();

			if ( ! $fee_id ) {
				throw new WC_API_Exception( 'woocommerce_cannot_update_fee', __( 'Cannot update fee, try again.', 'woocommerce' ), 500 );
			}
		}
	}

	/**
	 * Create or update an order coupon
	 *
	 * @since 2.2
	 * @param \WC_Order $order
	 * @param array $coupon item data
	 * @param string $action 'create' to add coupon or 'update' to update it
	 * @throws WC_API_Exception invalid data, server error
	 */
	protected function set_coupon( $order, $coupon, $action ) {

		// coupon amount must be positive float
		if ( isset( $coupon['amount'] ) && floatval( $coupon['amount'] ) < 0 ) {
			throw new WC_API_Exception( 'woocommerce_invalid_coupon_total', __( 'Coupon discount total must be a positive amount.', 'woocommerce' ), 400 );
		}

		if ( 'create' === $action ) {

			// coupon code is required
			if ( empty( $coupon['code'] ) ) {
				throw new WC_API_Exception( 'woocommerce_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 );
			}

			$item = new WC_Order_Item_Coupon();
			$item->set_props( array(
				'code'         => $coupon['code'],
				'discount'     => isset( $coupon['amount'] ) ? floatval( $coupon['amount'] ) : 0,
				'discount_tax' => 0,
				'order_id'     => $order->get_id(),
			) );
			$order->add_item( $item );
		} else {

			$item = new WC_Order_Item_Coupon( $coupon['id'] );

			if ( isset( $coupon['code'] ) ) {
				$item->set_code( $coupon['code'] );
			}

			if ( isset( $coupon['amount'] ) ) {
				$item->set_discount( floatval( $coupon['amount'] ) );
			}

			$coupon_id = $item->save();

			if ( ! $coupon_id ) {
				throw new WC_API_Exception( 'woocommerce_cannot_update_order_coupon', __( 'Cannot update coupon, try again.', 'woocommerce' ), 500 );
			}
		}
	}

	/**
	 * Get the admin order notes for an order
	 *
	 * @since 2.1
	 * @param string $order_id order ID
	 * @param string|null $fields fields to include in response
	 * @return array|WP_Error
	 */
	public function get_order_notes( $order_id, $fields = null ) {

		// ensure ID is valid order ID
		$order_id = $this->validate_request( $order_id, $this->post_type, 'read' );

		if ( is_wp_error( $order_id ) ) {
			return $order_id;
		}

		$args = array(
			'post_id' => $order_id,
			'approve' => 'approve',
			'type'    => 'order_note',
		);

		remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );

		$notes = get_comments( $args );

		add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );

		$order_notes = array();

		foreach ( $notes as $note ) {

			$order_notes[] = current( $this->get_order_note( $order_id, $note->comment_ID, $fields ) );
		}

		return array( 'order_notes' => apply_filters( 'woocommerce_api_order_notes_response', $order_notes, $order_id, $fields, $notes, $this->server ) );
	}

	/**
	 * Get an order note for the given order ID and ID
	 *
	 * @since 2.2
	 *
	 * @param string $order_id order ID
	 * @param string $id order note ID
	 * @param string|null $fields fields to limit response to
	 *
	 * @return array|WP_Error
	 */
	public function get_order_note( $order_id, $id, $fields = null ) {
		try {
			// Validate order ID
			$order_id = $this->validate_request( $order_id, $this->post_type, 'read' );

			if ( is_wp_error( $order_id ) ) {
				return $order_id;
			}

			$id = absint( $id );

			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 );
			}

			$note = get_comment( $id );

			if ( is_null( $note ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$order_note = array(
				'id'            => $note->comment_ID,
				'created_at'    => $this->server->format_datetime( $note->comment_date_gmt ),
				'note'          => $note->comment_content,
				'customer_note' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ),
			);

			return array( 'order_note' => apply_filters( 'woocommerce_api_order_note_response', $order_note, $id, $fields, $note, $order_id, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create a new order note for the given order
	 *
	 * @since 2.2
	 * @param string $order_id order ID
	 * @param array $data raw request data
	 * @return WP_Error|array error or created note response data
	 */
	public function create_order_note( $order_id, $data ) {
		try {
			if ( ! isset( $data['order_note'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_note' ), 400 );
			}

			$data = $data['order_note'];

			// permission check
			if ( ! current_user_can( 'publish_shop_orders' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_note', __( 'You do not have permission to create order notes', 'woocommerce' ), 401 );
			}

			$order_id = $this->validate_request( $order_id, $this->post_type, 'edit' );

			if ( is_wp_error( $order_id ) ) {
				return $order_id;
			}

			$order = wc_get_order( $order_id );

			$data = apply_filters( 'woocommerce_api_create_order_note_data', $data, $order_id, $this );

			// note content is required
			if ( ! isset( $data['note'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note', __( 'Order note is required', 'woocommerce' ), 400 );
			}

			$is_customer_note = ( isset( $data['customer_note'] ) && true === $data['customer_note'] );

			// create the note
			$note_id = $order->add_order_note( $data['note'], $is_customer_note );

			if ( ! $note_id ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_note', __( 'Cannot create order note, please try again.', 'woocommerce' ), 500 );
			}

			// HTTP 201 Created
			$this->server->send_status( 201 );

			do_action( 'woocommerce_api_create_order_note', $note_id, $order_id, $this );

			return $this->get_order_note( $order->get_id(), $note_id );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit the order note
	 *
	 * @since 2.2
	 * @param string $order_id order ID
	 * @param string $id note ID
	 * @param array $data parsed request data
	 * @return WP_Error|array error or edited note response data
	 */
	public function edit_order_note( $order_id, $id, $data ) {
		try {
			if ( ! isset( $data['order_note'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_note' ), 400 );
			}

			$data = $data['order_note'];

			// Validate order ID
			$order_id = $this->validate_request( $order_id, $this->post_type, 'edit' );

			if ( is_wp_error( $order_id ) ) {
				return $order_id;
			}

			$order = wc_get_order( $order_id );

			// Validate note ID
			$id = absint( $id );

			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 );
			}

			// Ensure note ID is valid
			$note = get_comment( $id );

			if ( is_null( $note ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			// Ensure note ID is associated with given order
			if ( $note->comment_post_ID != $order->get_id() ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 );
			}

			$data = apply_filters( 'woocommerce_api_edit_order_note_data', $data, $note->comment_ID, $order->get_id(), $this );

			// Note content
			if ( isset( $data['note'] ) ) {

				wp_update_comment(
					array(
						'comment_ID'      => $note->comment_ID,
						'comment_content' => $data['note'],
					)
				);
			}

			// Customer note
			if ( isset( $data['customer_note'] ) ) {

				update_comment_meta( $note->comment_ID, 'is_customer_note', true === $data['customer_note'] ? 1 : 0 );
			}

			do_action( 'woocommerce_api_edit_order_note', $note->comment_ID, $order->get_id(), $this );

			return $this->get_order_note( $order->get_id(), $note->comment_ID );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete order note
	 *
	 * @since 2.2
	 * @param string $order_id order ID
	 * @param string $id note ID
	 * @return WP_Error|array error or deleted message
	 */
	public function delete_order_note( $order_id, $id ) {
		try {
			$order_id = $this->validate_request( $order_id, $this->post_type, 'delete' );

			if ( is_wp_error( $order_id ) ) {
				return $order_id;
			}

			// Validate note ID
			$id = absint( $id );

			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 );
			}

			// Ensure note ID is valid
			$note = get_comment( $id );

			if ( is_null( $note ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			// Ensure note ID is associated with given order
			if ( $note->comment_post_ID != $order_id ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 );
			}

			// Force delete since trashed order notes could not be managed through comments list table
			$result = wc_delete_order_note( $note->comment_ID );

			if ( ! $result ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_delete_order_note', __( 'This order note cannot be deleted', 'woocommerce' ), 500 );
			}

			do_action( 'woocommerce_api_delete_order_note', $note->comment_ID, $order_id, $this );

			return array( 'message' => __( 'Permanently deleted order note', 'woocommerce' ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the order refunds for an order
	 *
	 * @since 2.2
	 * @param string $order_id order ID
	 * @param string|null $fields fields to include in response
	 * @return array|WP_Error
	 */
	public function get_order_refunds( $order_id, $fields = null ) {

		// Ensure ID is valid order ID
		$order_id = $this->validate_request( $order_id, $this->post_type, 'read' );

		if ( is_wp_error( $order_id ) ) {
			return $order_id;
		}

		$refund_items = wc_get_orders( array(
			'type'   => 'shop_order_refund',
			'parent' => $order_id,
			'limit'  => -1,
			'return' => 'ids',
		) );
		$order_refunds = array();

		foreach ( $refund_items as $refund_id ) {
			$order_refunds[] = current( $this->get_order_refund( $order_id, $refund_id, $fields ) );
		}

		return array( 'order_refunds' => apply_filters( 'woocommerce_api_order_refunds_response', $order_refunds, $order_id, $fields, $refund_items, $this ) );
	}

	/**
	 * Get an order refund for the given order ID and ID
	 *
	 * @since 2.2
	 *
	 * @param string $order_id order ID
	 * @param int $id
	 * @param string|null $fields fields to limit response to
	 * @param array $filter
	 *
	 * @return array|WP_Error
	 */
	public function get_order_refund( $order_id, $id, $fields = null, $filter = array() ) {
		try {
			// Validate order ID
			$order_id = $this->validate_request( $order_id, $this->post_type, 'read' );

			if ( is_wp_error( $order_id ) ) {
				return $order_id;
			}

			$id = absint( $id );

			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 );
			}

			$order  = wc_get_order( $order_id );
			$refund = wc_get_order( $id );

			if ( ! $refund ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 );
			}

			$line_items = array();

			// Add line items
			foreach ( $refund->get_items( 'line_item' ) as $item_id => $item ) {
				$product    = $item->get_product();
				$hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_';
				$item_meta  = $item->get_formatted_meta_data( $hideprefix );

				foreach ( $item_meta as $key => $values ) {
					$item_meta[ $key ]->label = $values->display_key;
					unset( $item_meta[ $key ]->display_key );
					unset( $item_meta[ $key ]->display_value );
				}

				$line_items[] = array(
					'id'               => $item_id,
					'subtotal'         => wc_format_decimal( $order->get_line_subtotal( $item ), 2 ),
					'subtotal_tax'     => wc_format_decimal( $item->get_subtotal_tax(), 2 ),
					'total'            => wc_format_decimal( $order->get_line_total( $item ), 2 ),
					'total_tax'        => wc_format_decimal( $order->get_line_tax( $item ), 2 ),
					'price'            => wc_format_decimal( $order->get_item_total( $item ), 2 ),
					'quantity'         => $item->get_quantity(),
					'tax_class'        => $item->get_tax_class(),
					'name'             => $item->get_name(),
					'product_id'       => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(),
					'sku'              => is_object( $product ) ? $product->get_sku() : null,
					'meta'             => array_values( $item_meta ),
					'refunded_item_id' => (int) $item->get_meta( 'refunded_item_id' ),
				);
			}

			$order_refund = array(
				'id'         => $refund->get_id(),
				'created_at' => $this->server->format_datetime( $refund->get_date_created() ? $refund->get_date_created()->getTimestamp() : 0, false, false ),
				'amount'     => wc_format_decimal( $refund->get_amount(), 2 ),
				'reason'     => $refund->get_reason(),
				'line_items' => $line_items,
			);

			return array( 'order_refund' => apply_filters( 'woocommerce_api_order_refund_response', $order_refund, $id, $fields, $refund, $order_id, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create a new order refund for the given order
	 *
	 * @since 2.2
	 * @param string $order_id order ID
	 * @param array $data raw request data
	 * @param bool $api_refund do refund using a payment gateway API
	 * @return WP_Error|array error or created refund response data
	 */
	public function create_order_refund( $order_id, $data, $api_refund = true ) {
		try {
			if ( ! isset( $data['order_refund'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_refund' ), 400 );
			}

			$data = $data['order_refund'];

			// Permission check
			if ( ! current_user_can( 'publish_shop_orders' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_refund', __( 'You do not have permission to create order refunds', 'woocommerce' ), 401 );
			}

			$order_id = absint( $order_id );

			if ( empty( $order_id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 );
			}

			$data = apply_filters( 'woocommerce_api_create_order_refund_data', $data, $order_id, $this );

			// Refund amount is required
			if ( ! isset( $data['amount'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount is required.', 'woocommerce' ), 400 );
			} elseif ( 0 > $data['amount'] ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount must be positive.', 'woocommerce' ), 400 );
			}

			$data['order_id']  = $order_id;
			$data['refund_id'] = 0;

			// Create the refund
			$refund = wc_create_refund( $data );

			if ( ! $refund ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 );
			}

			// Refund via API
			if ( $api_refund ) {
				if ( WC()->payment_gateways() ) {
					$payment_gateways = WC()->payment_gateways->payment_gateways();
				}

				$order = wc_get_order( $order_id );

				if ( isset( $payment_gateways[ $order->get_payment_method() ] ) && $payment_gateways[ $order->get_payment_method() ]->supports( 'refunds' ) ) {
					$result = $payment_gateways[ $order->get_payment_method() ]->process_refund( $order_id, $refund->get_amount(), $refund->get_reason() );

					if ( is_wp_error( $result ) ) {
						return $result;
					} elseif ( ! $result ) {
						throw new WC_API_Exception( 'woocommerce_api_create_order_refund_api_failed', __( 'An error occurred while attempting to create the refund using the payment gateway API.', 'woocommerce' ), 500 );
					}
				}
			}

			// HTTP 201 Created
			$this->server->send_status( 201 );

			do_action( 'woocommerce_api_create_order_refund', $refund->get_id(), $order_id, $this );

			return $this->get_order_refund( $order_id, $refund->get_id() );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit an order refund
	 *
	 * @since 2.2
	 * @param string $order_id order ID
	 * @param string $id refund ID
	 * @param array $data parsed request data
	 * @return WP_Error|array error or edited refund response data
	 */
	public function edit_order_refund( $order_id, $id, $data ) {
		try {
			if ( ! isset( $data['order_refund'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_refund' ), 400 );
			}

			$data = $data['order_refund'];

			// Validate order ID
			$order_id = $this->validate_request( $order_id, $this->post_type, 'edit' );

			if ( is_wp_error( $order_id ) ) {
				return $order_id;
			}

			// Validate refund ID
			$id = absint( $id );

			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 );
			}

			// Ensure order ID is valid
			$refund = get_post( $id );

			if ( ! $refund ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 );
			}

			// Ensure refund ID is associated with given order
			if ( $refund->post_parent != $order_id ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 );
			}

			$data = apply_filters( 'woocommerce_api_edit_order_refund_data', $data, $refund->ID, $order_id, $this );

			// Update reason
			if ( isset( $data['reason'] ) ) {
				$updated_refund = wp_update_post( array( 'ID' => $refund->ID, 'post_excerpt' => $data['reason'] ) );

				if ( is_wp_error( $updated_refund ) ) {
					return $updated_refund;
				}
			}

			// Update refund amount
			if ( isset( $data['amount'] ) && 0 < $data['amount'] ) {
				update_post_meta( $refund->ID, '_refund_amount', wc_format_decimal( $data['amount'] ) );
			}

			do_action( 'woocommerce_api_edit_order_refund', $refund->ID, $order_id, $this );

			return $this->get_order_refund( $order_id, $refund->ID );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete order refund
	 *
	 * @since 2.2
	 * @param string $order_id order ID
	 * @param string $id refund ID
	 * @return WP_Error|array error or deleted message
	 */
	public function delete_order_refund( $order_id, $id ) {
		try {
			$order_id = $this->validate_request( $order_id, $this->post_type, 'delete' );

			if ( is_wp_error( $order_id ) ) {
				return $order_id;
			}

			// Validate refund ID
			$id = absint( $id );

			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 );
			}

			// Ensure refund ID is valid
			$refund = get_post( $id );

			if ( ! $refund ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 );
			}

			// Ensure refund ID is associated with given order
			if ( $refund->post_parent != $order_id ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 );
			}

			wc_delete_shop_order_transients( $order_id );

			do_action( 'woocommerce_api_delete_order_refund', $refund->ID, $order_id, $this );

			return $this->delete( $refund->ID, 'refund', true );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Bulk update or insert orders
	 * Accepts an array with orders in the formats supported by
	 * WC_API_Orders->create_order() and WC_API_Orders->edit_order()
	 *
	 * @since 2.4.0
	 *
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function bulk( $data ) {

		try {
			if ( ! isset( $data['orders'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_orders_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'orders' ), 400 );
			}

			$data  = $data['orders'];
			$limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'orders' );

			// Limit bulk operation
			if ( count( $data ) > $limit ) {
				throw new WC_API_Exception( 'woocommerce_api_orders_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 );
			}

			$orders = array();

			foreach ( $data as $_order ) {
				$order_id = 0;

				// Try to get the order ID
				if ( isset( $_order['id'] ) ) {
					$order_id = intval( $_order['id'] );
				}

				if ( $order_id ) {

					// Order exists / edit order
					$edit = $this->edit_order( $order_id, array( 'order' => $_order ) );

					if ( is_wp_error( $edit ) ) {
						$orders[] = array(
							'id'    => $order_id,
							'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ),
						);
					} else {
						$orders[] = $edit['order'];
					}
				} else {

					// Order don't exists / create order
					$new = $this->create_order( array( 'order' => $_order ) );

					if ( is_wp_error( $new ) ) {
						$orders[] = array(
							'id'    => $order_id,
							'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ),
						);
					} else {
						$orders[] = $new['order'];
					}
				}
			}

			return array( 'orders' => apply_filters( 'woocommerce_api_orders_bulk_response', $orders, $this ) );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}
}
legacy/api/v3/class-wc-api-customers.php000064400000061021151336044770014141 0ustar00<?php
/**
 * WooCommerce API Customers Class
 *
 * Handles requests to the /customers endpoint
 *
 * @author   WooThemes
 * @category API
 * @package  WooCommerce\RestApi
 * @since    2.2
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WC_API_Customers extends WC_API_Resource {

	/** @var string $base the route base */
	protected $base = '/customers';

	/** @var string $created_at_min for date filtering */
	private $created_at_min = null;

	/** @var string $created_at_max for date filtering */
	private $created_at_max = null;

	/**
	 * Setup class, overridden to provide customer data to order response
	 *
	 * @since 2.1
	 * @param WC_API_Server $server
	 */
	public function __construct( WC_API_Server $server ) {

		parent::__construct( $server );

		// add customer data to order responses
		add_filter( 'woocommerce_api_order_response', array( $this, 'add_customer_data' ), 10, 2 );

		// modify WP_User_Query to support created_at date filtering
		add_action( 'pre_user_query', array( $this, 'modify_user_query' ) );
	}

	/**
	 * Register the routes for this class
	 *
	 * GET /customers
	 * GET /customers/count
	 * GET /customers/<id>
	 * GET /customers/<id>/orders
	 *
	 * @since 2.2
	 * @param array $routes
	 * @return array
	 */
	public function register_routes( $routes ) {

		# GET/POST /customers
		$routes[ $this->base ] = array(
			array( array( $this, 'get_customers' ),   WC_API_SERVER::READABLE ),
			array( array( $this, 'create_customer' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET /customers/count
		$routes[ $this->base . '/count' ] = array(
			array( array( $this, 'get_customers_count' ), WC_API_SERVER::READABLE ),
		);

		# GET/PUT/DELETE /customers/<id>
		$routes[ $this->base . '/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_customer' ),    WC_API_SERVER::READABLE ),
			array( array( $this, 'edit_customer' ),   WC_API_SERVER::EDITABLE | WC_API_SERVER::ACCEPT_DATA ),
			array( array( $this, 'delete_customer' ), WC_API_SERVER::DELETABLE ),
		);

		# GET /customers/email/<email>
		$routes[ $this->base . '/email/(?P<email>.+)' ] = array(
			array( array( $this, 'get_customer_by_email' ), WC_API_SERVER::READABLE ),
		);

		# GET /customers/<id>/orders
		$routes[ $this->base . '/(?P<id>\d+)/orders' ] = array(
			array( array( $this, 'get_customer_orders' ), WC_API_SERVER::READABLE ),
		);

		# GET /customers/<id>/downloads
		$routes[ $this->base . '/(?P<id>\d+)/downloads' ] = array(
			array( array( $this, 'get_customer_downloads' ), WC_API_SERVER::READABLE ),
		);

		# POST|PUT /customers/bulk
		$routes[ $this->base . '/bulk' ] = array(
			array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
		);

		return $routes;
	}

	/**
	 * Get all customers
	 *
	 * @since 2.1
	 * @param array $fields
	 * @param array $filter
	 * @param int $page
	 * @return array
	 */
	public function get_customers( $fields = null, $filter = array(), $page = 1 ) {

		$filter['page'] = $page;

		$query = $this->query_customers( $filter );

		$customers = array();

		foreach ( $query->get_results() as $user_id ) {

			if ( ! $this->is_readable( $user_id ) ) {
				continue;
			}

			$customers[] = current( $this->get_customer( $user_id, $fields ) );
		}

		$this->server->add_pagination_headers( $query );

		return array( 'customers' => $customers );
	}

	/**
	 * Get the customer for the given ID
	 *
	 * @since 2.1
	 * @param int $id the customer ID
	 * @param array $fields
	 * @return array|WP_Error
	 */
	public function get_customer( $id, $fields = null ) {
		global $wpdb;

		$id = $this->validate_request( $id, 'customer', 'read' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$customer      = new WC_Customer( $id );
		$last_order    = $customer->get_last_order();
		$customer_data = array(
			'id'               => $customer->get_id(),
			'created_at'       => $this->server->format_datetime( $customer->get_date_created() ? $customer->get_date_created()->getTimestamp() : 0 ), // API gives UTC times.
			'last_update'      => $this->server->format_datetime( $customer->get_date_modified() ? $customer->get_date_modified()->getTimestamp() : 0 ), // API gives UTC times.
			'email'            => $customer->get_email(),
			'first_name'       => $customer->get_first_name(),
			'last_name'        => $customer->get_last_name(),
			'username'         => $customer->get_username(),
			'role'             => $customer->get_role(),
			'last_order_id'    => is_object( $last_order ) ? $last_order->get_id() : null,
			'last_order_date'  => is_object( $last_order ) ? $this->server->format_datetime( $last_order->get_date_created() ? $last_order->get_date_created()->getTimestamp() : 0 ) : null, // API gives UTC times.
			'orders_count'     => $customer->get_order_count(),
			'total_spent'      => wc_format_decimal( $customer->get_total_spent(), 2 ),
			'avatar_url'       => $customer->get_avatar_url(),
			'billing_address'  => array(
				'first_name' => $customer->get_billing_first_name(),
				'last_name'  => $customer->get_billing_last_name(),
				'company'    => $customer->get_billing_company(),
				'address_1'  => $customer->get_billing_address_1(),
				'address_2'  => $customer->get_billing_address_2(),
				'city'       => $customer->get_billing_city(),
				'state'      => $customer->get_billing_state(),
				'postcode'   => $customer->get_billing_postcode(),
				'country'    => $customer->get_billing_country(),
				'email'      => $customer->get_billing_email(),
				'phone'      => $customer->get_billing_phone(),
			),
			'shipping_address' => array(
				'first_name' => $customer->get_shipping_first_name(),
				'last_name'  => $customer->get_shipping_last_name(),
				'company'    => $customer->get_shipping_company(),
				'address_1'  => $customer->get_shipping_address_1(),
				'address_2'  => $customer->get_shipping_address_2(),
				'city'       => $customer->get_shipping_city(),
				'state'      => $customer->get_shipping_state(),
				'postcode'   => $customer->get_shipping_postcode(),
				'country'    => $customer->get_shipping_country(),
			),
		);

		return array( 'customer' => apply_filters( 'woocommerce_api_customer_response', $customer_data, $customer, $fields, $this->server ) );
	}

	/**
	 * Get the customer for the given email
	 *
	 * @since 2.1
	 *
	 * @param string $email the customer email
	 * @param array $fields
	 *
	 * @return array|WP_Error
	 */
	public function get_customer_by_email( $email, $fields = null ) {
		try {
			if ( is_email( $email ) ) {
				$customer = get_user_by( 'email', $email );
				if ( ! is_object( $customer ) ) {
					throw new WC_API_Exception( 'woocommerce_api_invalid_customer_email', __( 'Invalid customer email', 'woocommerce' ), 404 );
				}
			} else {
				throw new WC_API_Exception( 'woocommerce_api_invalid_customer_email', __( 'Invalid customer email', 'woocommerce' ), 404 );
			}

			return $this->get_customer( $customer->ID, $fields );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the total number of customers
	 *
	 * @since 2.1
	 *
	 * @param array $filter
	 *
	 * @return array|WP_Error
	 */
	public function get_customers_count( $filter = array() ) {
		try {
			if ( ! current_user_can( 'list_users' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_customers_count', __( 'You do not have permission to read the customers count', 'woocommerce' ), 401 );
			}

			$query = $this->query_customers( $filter );

			return array( 'count' => $query->get_total() );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get customer billing address fields.
	 *
	 * @since  2.2
	 * @return array
	 */
	protected function get_customer_billing_address() {
		$billing_address = apply_filters( 'woocommerce_api_customer_billing_address', array(
			'first_name',
			'last_name',
			'company',
			'address_1',
			'address_2',
			'city',
			'state',
			'postcode',
			'country',
			'email',
			'phone',
		) );

		return $billing_address;
	}

	/**
	 * Get customer shipping address fields.
	 *
	 * @since  2.2
	 * @return array
	 */
	protected function get_customer_shipping_address() {
		$shipping_address = apply_filters( 'woocommerce_api_customer_shipping_address', array(
			'first_name',
			'last_name',
			'company',
			'address_1',
			'address_2',
			'city',
			'state',
			'postcode',
			'country',
		) );

		return $shipping_address;
	}

	/**
	 * Add/Update customer data.
	 *
	 * @since 2.2
	 * @param int $id the customer ID
	 * @param array $data
	 * @param WC_Customer $customer
	 */
	protected function update_customer_data( $id, $data, $customer ) {

		// Customer first name.
		if ( isset( $data['first_name'] ) ) {
			$customer->set_first_name( wc_clean( $data['first_name'] ) );
		}

		// Customer last name.
		if ( isset( $data['last_name'] ) ) {
			$customer->set_last_name( wc_clean( $data['last_name'] ) );
		}

		// Customer billing address.
		if ( isset( $data['billing_address'] ) ) {
			foreach ( $this->get_customer_billing_address() as $field ) {
				if ( isset( $data['billing_address'][ $field ] ) ) {
					if ( is_callable( array( $customer, "set_billing_{$field}" ) ) ) {
						$customer->{"set_billing_{$field}"}( $data['billing_address'][ $field ] );
					} else {
						$customer->update_meta_data( 'billing_' . $field, wc_clean( $data['billing_address'][ $field ] ) );
					}
				}
			}
		}

		// Customer shipping address.
		if ( isset( $data['shipping_address'] ) ) {
			foreach ( $this->get_customer_shipping_address() as $field ) {
				if ( isset( $data['shipping_address'][ $field ] ) ) {
					if ( is_callable( array( $customer, "set_shipping_{$field}" ) ) ) {
						$customer->{"set_shipping_{$field}"}( $data['shipping_address'][ $field ] );
					} else {
						$customer->update_meta_data( 'shipping_' . $field, wc_clean( $data['shipping_address'][ $field ] ) );
					}
				}
			}
		}

		do_action( 'woocommerce_api_update_customer_data', $id, $data, $customer );
	}

	/**
	 * Create a customer
	 *
	 * @since 2.2
	 *
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function create_customer( $data ) {
		try {
			if ( ! isset( $data['customer'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_customer_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'customer' ), 400 );
			}

			$data = $data['customer'];

			// Checks with can create new users.
			if ( ! current_user_can( 'create_users' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_customer', __( 'You do not have permission to create this customer', 'woocommerce' ), 401 );
			}

			$data = apply_filters( 'woocommerce_api_create_customer_data', $data, $this );

			// Checks with the email is missing.
			if ( ! isset( $data['email'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_customer_email', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'email' ), 400 );
			}

			// Create customer.
			$customer = new WC_Customer;
			$customer->set_username( ! empty( $data['username'] ) ? $data['username'] : '' );
			$customer->set_password( ! empty( $data['password'] ) ? $data['password'] : '' );
			$customer->set_email( $data['email'] );
			$customer->save();

			if ( ! $customer->get_id() ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_customer', __( 'This resource cannot be created.', 'woocommerce' ), 400 );
			}

			// Added customer data.
			$this->update_customer_data( $customer->get_id(), $data, $customer );
			$customer->save();

			do_action( 'woocommerce_api_create_customer', $customer->get_id(), $data );

			$this->server->send_status( 201 );

			return $this->get_customer( $customer->get_id() );
		} catch ( Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit a customer
	 *
	 * @since 2.2
	 *
	 * @param int $id the customer ID
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function edit_customer( $id, $data ) {
		try {
			if ( ! isset( $data['customer'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_customer_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'customer' ), 400 );
			}

			$data = $data['customer'];

			// Validate the customer ID.
			$id = $this->validate_request( $id, 'customer', 'edit' );

			// Return the validate error.
			if ( is_wp_error( $id ) ) {
				throw new WC_API_Exception( $id->get_error_code(), $id->get_error_message(), 400 );
			}

			$data = apply_filters( 'woocommerce_api_edit_customer_data', $data, $this );

			$customer = new WC_Customer( $id );

			// Customer email.
			if ( isset( $data['email'] ) ) {
				$customer->set_email( $data['email'] );
			}

			// Customer password.
			if ( isset( $data['password'] ) ) {
				$customer->set_password( $data['password'] );
			}

			// Update customer data.
			$this->update_customer_data( $customer->get_id(), $data, $customer );

			$customer->save();

			do_action( 'woocommerce_api_edit_customer', $customer->get_id(), $data );

			return $this->get_customer( $customer->get_id() );
		} catch ( Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete a customer
	 *
	 * @since 2.2
	 * @param int $id the customer ID
	 * @return array|WP_Error
	 */
	public function delete_customer( $id ) {

		// Validate the customer ID.
		$id = $this->validate_request( $id, 'customer', 'delete' );

		// Return the validate error.
		if ( is_wp_error( $id ) ) {
			return $id;
		}

		do_action( 'woocommerce_api_delete_customer', $id, $this );

		return $this->delete( $id, 'customer' );
	}

	/**
	 * Get the orders for a customer
	 *
	 * @since 2.1
	 * @param int $id the customer ID
	 * @param string $fields fields to include in response
	 * @param array $filter filters
	 * @return array|WP_Error
	 */
	public function get_customer_orders( $id, $fields = null, $filter = array() ) {
		$id = $this->validate_request( $id, 'customer', 'read' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$filter['customer_id'] = $id;
		$orders = WC()->api->WC_API_Orders->get_orders( $fields, $filter, null, -1 );

		return $orders;
	}

	/**
	 * Get the available downloads for a customer
	 *
	 * @since 2.2
	 * @param int $id the customer ID
	 * @param string $fields fields to include in response
	 * @return array|WP_Error
	 */
	public function get_customer_downloads( $id, $fields = null ) {
		$id = $this->validate_request( $id, 'customer', 'read' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$downloads  = array();
		$_downloads = wc_get_customer_available_downloads( $id );

		foreach ( $_downloads as $key => $download ) {
			$downloads[] = array(
				'download_url'        => $download['download_url'],
				'download_id'         => $download['download_id'],
				'product_id'          => $download['product_id'],
				'download_name'       => $download['download_name'],
				'order_id'            => $download['order_id'],
				'order_key'           => $download['order_key'],
				'downloads_remaining' => $download['downloads_remaining'],
				'access_expires'      => $download['access_expires'] ? $this->server->format_datetime( $download['access_expires'] ) : null,
				'file'                => $download['file'],
			);
		}

		return array( 'downloads' => apply_filters( 'woocommerce_api_customer_downloads_response', $downloads, $id, $fields, $this->server ) );
	}

	/**
	 * Helper method to get customer user objects
	 *
	 * Note that WP_User_Query does not have built-in pagination so limit & offset are used to provide limited
	 * pagination support
	 *
	 * The filter for role can only be a single role in a string.
	 *
	 * @since 2.3
	 * @param array $args request arguments for filtering query
	 * @return WP_User_Query
	 */
	private function query_customers( $args = array() ) {

		// default users per page
		$users_per_page = get_option( 'posts_per_page' );

		// Set base query arguments
		$query_args = array(
			'fields'  => 'ID',
			'role'    => 'customer',
			'orderby' => 'registered',
			'number'  => $users_per_page,
		);

		// Custom Role
		if ( ! empty( $args['role'] ) ) {
			$query_args['role'] = $args['role'];

			// Show users on all roles
			if ( 'all' === $query_args['role'] ) {
				unset( $query_args['role'] );
			}
		}

		// Search
		if ( ! empty( $args['q'] ) ) {
			$query_args['search'] = $args['q'];
		}

		// Limit number of users returned
		if ( ! empty( $args['limit'] ) ) {
			if ( -1 == $args['limit'] ) {
				unset( $query_args['number'] );
			} else {
				$query_args['number'] = absint( $args['limit'] );
				$users_per_page       = absint( $args['limit'] );
			}
		} else {
			$args['limit'] = $query_args['number'];
		}

		// Page
		$page = ( isset( $args['page'] ) ) ? absint( $args['page'] ) : 1;

		// Offset
		if ( ! empty( $args['offset'] ) ) {
			$query_args['offset'] = absint( $args['offset'] );
		} else {
			$query_args['offset'] = $users_per_page * ( $page - 1 );
		}

		// Created date
		if ( ! empty( $args['created_at_min'] ) ) {
			$this->created_at_min = $this->server->parse_datetime( $args['created_at_min'] );
		}

		if ( ! empty( $args['created_at_max'] ) ) {
			$this->created_at_max = $this->server->parse_datetime( $args['created_at_max'] );
		}

		// Order (ASC or DESC, ASC by default)
		if ( ! empty( $args['order'] ) ) {
			$query_args['order'] = $args['order'];
		}

		// Order by
		if ( ! empty( $args['orderby'] ) ) {
			$query_args['orderby'] = $args['orderby'];

			// Allow sorting by meta value
			if ( ! empty( $args['orderby_meta_key'] ) ) {
				$query_args['meta_key'] = $args['orderby_meta_key'];
			}
		}

		$query = new WP_User_Query( $query_args );

		// Helper members for pagination headers
		$query->total_pages = ( -1 == $args['limit'] ) ? 1 : ceil( $query->get_total() / $users_per_page );
		$query->page = $page;

		return $query;
	}

	/**
	 * Add customer data to orders
	 *
	 * @since 2.1
	 * @param $order_data
	 * @param $order
	 * @return array
	 */
	public function add_customer_data( $order_data, $order ) {

		if ( 0 == $order->get_user_id() ) {

			// add customer data from order
			$order_data['customer'] = array(
				'id'               => 0,
				'email'            => $order->get_billing_email(),
				'first_name'       => $order->get_billing_first_name(),
				'last_name'        => $order->get_billing_last_name(),
				'billing_address'  => array(
					'first_name' => $order->get_billing_first_name(),
					'last_name'  => $order->get_billing_last_name(),
					'company'    => $order->get_billing_company(),
					'address_1'  => $order->get_billing_address_1(),
					'address_2'  => $order->get_billing_address_2(),
					'city'       => $order->get_billing_city(),
					'state'      => $order->get_billing_state(),
					'postcode'   => $order->get_billing_postcode(),
					'country'    => $order->get_billing_country(),
					'email'      => $order->get_billing_email(),
					'phone'      => $order->get_billing_phone(),
				),
				'shipping_address' => array(
					'first_name' => $order->get_shipping_first_name(),
					'last_name'  => $order->get_shipping_last_name(),
					'company'    => $order->get_shipping_company(),
					'address_1'  => $order->get_shipping_address_1(),
					'address_2'  => $order->get_shipping_address_2(),
					'city'       => $order->get_shipping_city(),
					'state'      => $order->get_shipping_state(),
					'postcode'   => $order->get_shipping_postcode(),
					'country'    => $order->get_shipping_country(),
				),
			);

		} else {

			$order_data['customer'] = current( $this->get_customer( $order->get_user_id() ) );
		}

		return $order_data;
	}

	/**
	 * Modify the WP_User_Query to support filtering on the date the customer was created
	 *
	 * @since 2.1
	 * @param WP_User_Query $query
	 */
	public function modify_user_query( $query ) {

		if ( $this->created_at_min ) {
			$query->query_where .= sprintf( " AND user_registered >= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%H:%%i:%%s' )", esc_sql( $this->created_at_min ) );
		}

		if ( $this->created_at_max ) {
			$query->query_where .= sprintf( " AND user_registered <= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%H:%%i:%%s' )", esc_sql( $this->created_at_max ) );
		}
	}

	/**
	 * Validate the request by checking:
	 *
	 * 1) the ID is a valid integer
	 * 2) the ID returns a valid WP_User
	 * 3) the current user has the proper permissions
	 *
	 * @since 2.1
	 * @see WC_API_Resource::validate_request()
	 * @param integer $id the customer ID
	 * @param string $type the request type, unused because this method overrides the parent class
	 * @param string $context the context of the request, either `read`, `edit` or `delete`
	 * @return int|WP_Error valid user ID or WP_Error if any of the checks fails
	 */
	protected function validate_request( $id, $type, $context ) {

		try {
			$id = absint( $id );

			// validate ID
			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Invalid customer ID', 'woocommerce' ), 404 );
			}

			// non-existent IDs return a valid WP_User object with the user ID = 0
			$customer = new WP_User( $id );

			if ( 0 === $customer->ID ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_customer', __( 'Invalid customer', 'woocommerce' ), 404 );
			}

			// validate permissions
			switch ( $context ) {

				case 'read':
					if ( ! current_user_can( 'list_users' ) ) {
						throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_customer', __( 'You do not have permission to read this customer', 'woocommerce' ), 401 );
					}
					break;

				case 'edit':
					if ( ! wc_rest_check_user_permissions( 'edit', $customer->ID ) ) {
						throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_customer', __( 'You do not have permission to edit this customer', 'woocommerce' ), 401 );
					}
					break;

				case 'delete':
					if ( ! wc_rest_check_user_permissions( 'delete', $customer->ID ) ) {
						throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_customer', __( 'You do not have permission to delete this customer', 'woocommerce' ), 401 );
					}
					break;
			}

			return $id;
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Check if the current user can read users
	 *
	 * @since 2.1
	 * @see WC_API_Resource::is_readable()
	 * @param int|WP_Post $post unused
	 * @return bool true if the current user can read users, false otherwise
	 */
	protected function is_readable( $post ) {
		return current_user_can( 'list_users' );
	}

	/**
	 * Bulk update or insert customers
	 * Accepts an array with customers in the formats supported by
	 * WC_API_Customers->create_customer() and WC_API_Customers->edit_customer()
	 *
	 * @since 2.4.0
	 *
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function bulk( $data ) {

		try {
			if ( ! isset( $data['customers'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_customers_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'customers' ), 400 );
			}

			$data  = $data['customers'];
			$limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'customers' );

			// Limit bulk operation
			if ( count( $data ) > $limit ) {
				throw new WC_API_Exception( 'woocommerce_api_customers_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 );
			}

			$customers = array();

			foreach ( $data as $_customer ) {
				$customer_id = 0;

				// Try to get the customer ID
				if ( isset( $_customer['id'] ) ) {
					$customer_id = intval( $_customer['id'] );
				}

				if ( $customer_id ) {

					// Customer exists / edit customer
					$edit = $this->edit_customer( $customer_id, array( 'customer' => $_customer ) );

					if ( is_wp_error( $edit ) ) {
						$customers[] = array(
							'id'    => $customer_id,
							'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ),
						);
					} else {
						$customers[] = $edit['customer'];
					}
				} else {

					// Customer don't exists / create customer
					$new = $this->create_customer( array( 'customer' => $_customer ) );

					if ( is_wp_error( $new ) ) {
						$customers[] = array(
							'id'    => $customer_id,
							'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ),
						);
					} else {
						$customers[] = $new['customer'];
					}
				}
			}

			return array( 'customers' => apply_filters( 'woocommerce_api_customers_bulk_response', $customers, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}
}
legacy/api/v3/class-wc-api-exception.php000064400000002213151336044770014111 0ustar00<?php
/**
 * WooCommerce API Exception Class
 *
 * Extends Exception to provide additional data
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.2
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WC_API_Exception extends Exception {

	/** @var string sanitized error code */
	protected $error_code;

	/**
	 * Setup exception, requires 3 params:
	 *
	 * error code - machine-readable, e.g. `woocommerce_invalid_product_id`
	 * error message - friendly message, e.g. 'Product ID is invalid'
	 * http status code - proper HTTP status code to respond with, e.g. 400
	 *
	 * @since 2.2
	 * @param string $error_code
	 * @param string $error_message user-friendly translated error message
	 * @param int $http_status_code HTTP status code to respond with
	 */
	public function __construct( $error_code, $error_message, $http_status_code ) {
		$this->error_code = $error_code;
		parent::__construct( $error_message, $http_status_code );
	}

	/**
	 * Returns the error code
	 *
	 * @since 2.2
	 * @return string
	 */
	public function getErrorCode() {
		return $this->error_code;
	}
}
legacy/api/v3/class-wc-api-products.php000064400000330554151336044770013772 0ustar00<?php
/**
 * WooCommerce API Products Class
 *
 * Handles requests to the /products endpoint
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 * @version     3.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WC_API_Products extends WC_API_Resource {

	/** @var string $base the route base */
	protected $base = '/products';

	/**
	 * Register the routes for this class
	 *
	 * GET/POST /products
	 * GET /products/count
	 * GET/PUT/DELETE /products/<id>
	 * GET /products/<id>/reviews
	 *
	 * @since 2.1
	 * @param array $routes
	 * @return array
	 */
	public function register_routes( $routes ) {

		# GET/POST /products
		$routes[ $this->base ] = array(
			array( array( $this, 'get_products' ), WC_API_Server::READABLE ),
			array( array( $this, 'create_product' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET /products/count
		$routes[ $this->base . '/count' ] = array(
			array( array( $this, 'get_products_count' ), WC_API_Server::READABLE ),
		);

		# GET/PUT/DELETE /products/<id>
		$routes[ $this->base . '/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_product' ), WC_API_Server::READABLE ),
			array( array( $this, 'edit_product' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
			array( array( $this, 'delete_product' ), WC_API_Server::DELETABLE ),
		);

		# GET /products/<id>/reviews
		$routes[ $this->base . '/(?P<id>\d+)/reviews' ] = array(
			array( array( $this, 'get_product_reviews' ), WC_API_Server::READABLE ),
		);

		# GET /products/<id>/orders
		$routes[ $this->base . '/(?P<id>\d+)/orders' ] = array(
			array( array( $this, 'get_product_orders' ), WC_API_Server::READABLE ),
		);

		# GET/POST /products/categories
		$routes[ $this->base . '/categories' ] = array(
			array( array( $this, 'get_product_categories' ), WC_API_Server::READABLE ),
			array( array( $this, 'create_product_category' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET/PUT/DELETE /products/categories/<id>
		$routes[ $this->base . '/categories/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_product_category' ), WC_API_Server::READABLE ),
			array( array( $this, 'edit_product_category' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
			array( array( $this, 'delete_product_category' ), WC_API_Server::DELETABLE ),
		);

		# GET/POST /products/tags
		$routes[ $this->base . '/tags' ] = array(
			array( array( $this, 'get_product_tags' ), WC_API_Server::READABLE ),
			array( array( $this, 'create_product_tag' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET/PUT/DELETE /products/tags/<id>
		$routes[ $this->base . '/tags/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_product_tag' ), WC_API_Server::READABLE ),
			array( array( $this, 'edit_product_tag' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
			array( array( $this, 'delete_product_tag' ), WC_API_Server::DELETABLE ),
		);

		# GET/POST /products/shipping_classes
		$routes[ $this->base . '/shipping_classes' ] = array(
			array( array( $this, 'get_product_shipping_classes' ), WC_API_Server::READABLE ),
			array( array( $this, 'create_product_shipping_class' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET/PUT/DELETE /products/shipping_classes/<id>
		$routes[ $this->base . '/shipping_classes/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_product_shipping_class' ), WC_API_Server::READABLE ),
			array( array( $this, 'edit_product_shipping_class' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
			array( array( $this, 'delete_product_shipping_class' ), WC_API_Server::DELETABLE ),
		);

		# GET/POST /products/attributes
		$routes[ $this->base . '/attributes' ] = array(
			array( array( $this, 'get_product_attributes' ), WC_API_Server::READABLE ),
			array( array( $this, 'create_product_attribute' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET/PUT/DELETE /products/attributes/<id>
		$routes[ $this->base . '/attributes/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_product_attribute' ), WC_API_Server::READABLE ),
			array( array( $this, 'edit_product_attribute' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
			array( array( $this, 'delete_product_attribute' ), WC_API_Server::DELETABLE ),
		);

		# GET/POST /products/attributes/<attribute_id>/terms
		$routes[ $this->base . '/attributes/(?P<attribute_id>\d+)/terms' ] = array(
			array( array( $this, 'get_product_attribute_terms' ), WC_API_Server::READABLE ),
			array( array( $this, 'create_product_attribute_term' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET/PUT/DELETE /products/attributes/<attribute_id>/terms/<id>
		$routes[ $this->base . '/attributes/(?P<attribute_id>\d+)/terms/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_product_attribute_term' ), WC_API_Server::READABLE ),
			array( array( $this, 'edit_product_attribute_term' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
			array( array( $this, 'delete_product_attribute_term' ), WC_API_Server::DELETABLE ),
		);

		# POST|PUT /products/bulk
		$routes[ $this->base . '/bulk' ] = array(
			array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
		);

		return $routes;
	}

	/**
	 * Get all products
	 *
	 * @since 2.1
	 * @param string $fields
	 * @param string $type
	 * @param array $filter
	 * @param int $page
	 * @return array
	 */
	public function get_products( $fields = null, $type = null, $filter = array(), $page = 1 ) {

		if ( ! empty( $type ) ) {
			$filter['type'] = $type;
		}

		$filter['page'] = $page;

		$query = $this->query_products( $filter );

		$products = array();

		foreach ( $query->posts as $product_id ) {

			if ( ! $this->is_readable( $product_id ) ) {
				continue;
			}

			$products[] = current( $this->get_product( $product_id, $fields ) );
		}

		$this->server->add_pagination_headers( $query );

		return array( 'products' => $products );
	}

	/**
	 * Get the product for the given ID
	 *
	 * @since 2.1
	 * @param int $id the product ID
	 * @param string $fields
	 * @return array|WP_Error
	 */
	public function get_product( $id, $fields = null ) {

		$id = $this->validate_request( $id, 'product', 'read' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$product = wc_get_product( $id );

		// add data that applies to every product type
		$product_data = $this->get_product_data( $product );

		// add variations to variable products
		if ( $product->is_type( 'variable' ) && $product->has_child() ) {
			$product_data['variations'] = $this->get_variation_data( $product );
		}

		// add the parent product data to an individual variation
		if ( $product->is_type( 'variation' ) && $product->get_parent_id() ) {
			$product_data['parent'] = $this->get_product_data( $product->get_parent_id() );
		}

		// Add grouped products data
		if ( $product->is_type( 'grouped' ) && $product->has_child() ) {
			$product_data['grouped_products'] = $this->get_grouped_products_data( $product );
		}

		if ( $product->is_type( 'simple' ) ) {
			$parent_id = $product->get_parent_id();
			if ( ! empty( $parent_id ) ) {
				$_product               = wc_get_product( $parent_id );
				$product_data['parent'] = $this->get_product_data( $_product );
			}
		}

		return array( 'product' => apply_filters( 'woocommerce_api_product_response', $product_data, $product, $fields, $this->server ) );
	}

	/**
	 * Get the total number of products
	 *
	 * @since 2.1
	 *
	 * @param string $type
	 * @param array $filter
	 *
	 * @return array|WP_Error
	 */
	public function get_products_count( $type = null, $filter = array() ) {
		try {
			if ( ! current_user_can( 'read_private_products' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_products_count', __( 'You do not have permission to read the products count', 'woocommerce' ), 401 );
			}

			if ( ! empty( $type ) ) {
				$filter['type'] = $type;
			}

			$query = $this->query_products( $filter );

			return array( 'count' => (int) $query->found_posts );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create a new product.
	 *
	 * @since 2.2
	 *
	 * @param array $data posted data
	 *
	 * @return array|WP_Error
	 */
	public function create_product( $data ) {
		$id = 0;

		try {
			if ( ! isset( $data['product'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product' ), 400 );
			}

			$data = $data['product'];

			// Check permissions.
			if ( ! current_user_can( 'publish_products' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product', __( 'You do not have permission to create products', 'woocommerce' ), 401 );
			}

			$data = apply_filters( 'woocommerce_api_create_product_data', $data, $this );

			// Check if product title is specified.
			if ( ! isset( $data['title'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_title', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'title' ), 400 );
			}

			// Check product type.
			if ( ! isset( $data['type'] ) ) {
				$data['type'] = 'simple';
			}

			// Set visible visibility when not sent.
			if ( ! isset( $data['catalog_visibility'] ) ) {
				$data['catalog_visibility'] = 'visible';
			}

			// Validate the product type.
			if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 );
			}

			// Enable description html tags.
			$post_content = isset( $data['description'] ) ? wc_clean( $data['description'] ) : '';
			if ( $post_content && isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) {

				$post_content = wp_filter_post_kses( $data['description'] );
			}

			// Enable short description html tags.
			$post_excerpt = isset( $data['short_description'] ) ? wc_clean( $data['short_description'] ) : '';
			if ( $post_excerpt && isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) {
				$post_excerpt = wp_filter_post_kses( $data['short_description'] );
			}

			$classname = WC_Product_Factory::get_classname_from_product_type( $data['type'] );
			if ( ! class_exists( $classname ) ) {
				$classname = 'WC_Product_Simple';
			}
			$product = new $classname();

			$product->set_name( wc_clean( $data['title'] ) );
			$product->set_status( isset( $data['status'] ) ? wc_clean( $data['status'] ) : 'publish' );
			$product->set_short_description( isset( $data['short_description'] ) ? $post_excerpt : '' );
			$product->set_description( isset( $data['description'] ) ? $post_content : '' );
			$product->set_menu_order( isset( $data['menu_order'] ) ? intval( $data['menu_order'] ) : 0 );

			if ( ! empty( $data['name'] ) ) {
				$product->set_slug( sanitize_title( $data['name'] ) );
			}

			// Attempts to create the new product.
			$product->save();
			$id = $product->get_id();

			// Checks for an error in the product creation.
			if ( 0 >= $id ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_product', $id->get_error_message(), 400 );
			}

			// Check for featured/gallery images, upload it and set it.
			if ( isset( $data['images'] ) ) {
				$product = $this->save_product_images( $product, $data['images'] );
			}

			// Save product meta fields.
			$product = $this->save_product_meta( $product, $data );
			$product->save();

			// Save variations.
			if ( isset( $data['type'] ) && 'variable' == $data['type'] && isset( $data['variations'] ) && is_array( $data['variations'] ) ) {
				$this->save_variations( $product, $data );
			}

			do_action( 'woocommerce_api_create_product', $id, $data );

			// Clear cache/transients.
			wc_delete_product_transients( $id );

			$this->server->send_status( 201 );

			return $this->get_product( $id );
		} catch ( WC_Data_Exception $e ) {
			$this->clear_product( $id );
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		} catch ( WC_API_Exception $e ) {
			$this->clear_product( $id );
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit a product
	 *
	 * @since 2.2
	 *
	 * @param int $id the product ID
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function edit_product( $id, $data ) {
		try {
			if ( ! isset( $data['product'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product' ), 400 );
			}

			$data = $data['product'];

			$id = $this->validate_request( $id, 'product', 'edit' );

			if ( is_wp_error( $id ) ) {
				return $id;
			}

			$product = wc_get_product( $id );

			$data = apply_filters( 'woocommerce_api_edit_product_data', $data, $this );

			// Product title.
			if ( isset( $data['title'] ) ) {
				$product->set_name( wc_clean( $data['title'] ) );
			}

			// Product name (slug).
			if ( isset( $data['name'] ) ) {
				$product->set_slug( wc_clean( $data['name'] ) );
			}

			// Product status.
			if ( isset( $data['status'] ) ) {
				$product->set_status( wc_clean( $data['status'] ) );
			}

			// Product short description.
			if ( isset( $data['short_description'] ) ) {
				// Enable short description html tags.
				$post_excerpt = ( isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) ? wp_filter_post_kses( $data['short_description'] ) : wc_clean( $data['short_description'] );
				$product->set_short_description( $post_excerpt );
			}

			// Product description.
			if ( isset( $data['description'] ) ) {
				// Enable description html tags.
				$post_content = ( isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) ? wp_filter_post_kses( $data['description'] ) : wc_clean( $data['description'] );
				$product->set_description( $post_content );
			}

			// Validate the product type.
			if ( isset( $data['type'] ) && ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 );
			}

			// Menu order.
			if ( isset( $data['menu_order'] ) ) {
				$product->set_menu_order( intval( $data['menu_order'] ) );
			}

			// Check for featured/gallery images, upload it and set it.
			if ( isset( $data['images'] ) ) {
				$product = $this->save_product_images( $product, $data['images'] );
			}

			// Save product meta fields.
			$product = $this->save_product_meta( $product, $data );

			// Save variations.
			if ( $product->is_type( 'variable' ) ) {
				if ( isset( $data['variations'] ) && is_array( $data['variations'] ) ) {
					$this->save_variations( $product, $data );
				} else {
					// Just sync variations.
					$product = WC_Product_Variable::sync( $product, false );
				}
			}

			$product->save();

			do_action( 'woocommerce_api_edit_product', $id, $data );

			// Clear cache/transients.
			wc_delete_product_transients( $id );

			return $this->get_product( $id );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete a product.
	 *
	 * @since 2.2
	 *
	 * @param int $id the product ID.
	 * @param bool $force true to permanently delete order, false to move to trash.
	 *
	 * @return array|WP_Error
	 */
	public function delete_product( $id, $force = false ) {

		$id = $this->validate_request( $id, 'product', 'delete' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$product = wc_get_product( $id );

		do_action( 'woocommerce_api_delete_product', $id, $this );

		// If we're forcing, then delete permanently.
		if ( $force ) {
			if ( $product->is_type( 'variable' ) ) {
				foreach ( $product->get_children() as $child_id ) {
					$child = wc_get_product( $child_id );
					if ( ! empty( $child ) ) {
						$child->delete( true );
					}
				}
			} else {
				// For other product types, if the product has children, remove the relationship.
				foreach ( $product->get_children() as $child_id ) {
					$child = wc_get_product( $child_id );
					if ( ! empty( $child ) ) {
						$child->set_parent_id( 0 );
						$child->save();
					}
				}
			}

			$product->delete( true );
			$result = ! ( $product->get_id() > 0 );
		} else {
			$product->delete();
			$result = 'trash' === $product->get_status();
		}

		if ( ! $result ) {
			return new WP_Error( 'woocommerce_api_cannot_delete_product', sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), 'product' ), array( 'status' => 500 ) );
		}

		// Delete parent product transients.
		if ( $parent_id = wp_get_post_parent_id( $id ) ) {
			wc_delete_product_transients( $parent_id );
		}

		if ( $force ) {
			return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), 'product' ) );
		} else {
			$this->server->send_status( '202' );

			return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product' ) );
		}
	}

	/**
	 * Get the reviews for a product
	 *
	 * @since 2.1
	 * @param int $id the product ID to get reviews for
	 * @param string $fields fields to include in response
	 * @return array|WP_Error
	 */
	public function get_product_reviews( $id, $fields = null ) {

		$id = $this->validate_request( $id, 'product', 'read' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$comments = get_approved_comments( $id );
		$reviews  = array();

		foreach ( $comments as $comment ) {

			$reviews[] = array(
				'id'             => intval( $comment->comment_ID ),
				'created_at'     => $this->server->format_datetime( $comment->comment_date_gmt ),
				'review'         => $comment->comment_content,
				'rating'         => get_comment_meta( $comment->comment_ID, 'rating', true ),
				'reviewer_name'  => $comment->comment_author,
				'reviewer_email' => $comment->comment_author_email,
				'verified'       => wc_review_is_from_verified_owner( $comment->comment_ID ),
			);
		}

		return array( 'product_reviews' => apply_filters( 'woocommerce_api_product_reviews_response', $reviews, $id, $fields, $comments, $this->server ) );
	}

	/**
	 * Get the orders for a product
	 *
	 * @since 2.4.0
	 * @param int $id the product ID to get orders for
	 * @param string fields  fields to retrieve
	 * @param array $filter filters to include in response
	 * @param string $status the order status to retrieve
	 * @param $page  $page   page to retrieve
	 * @return array|WP_Error
	 */
	public function get_product_orders( $id, $fields = null, $filter = array(), $status = null, $page = 1 ) {
		global $wpdb;

		$id = $this->validate_request( $id, 'product', 'read' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$order_ids = $wpdb->get_col( $wpdb->prepare( "
			SELECT order_id
			FROM {$wpdb->prefix}woocommerce_order_items
			WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d )
			AND order_item_type = 'line_item'
		 ", $id ) );

		if ( empty( $order_ids ) ) {
			return array( 'orders' => array() );
		}

		$filter = array_merge( $filter, array(
			'in' => implode( ',', $order_ids ),
		) );

		$orders = WC()->api->WC_API_Orders->get_orders( $fields, $filter, $status, $page );

		return array( 'orders' => apply_filters( 'woocommerce_api_product_orders_response', $orders['orders'], $id, $filter, $fields, $this->server ) );
	}

	/**
	 * Get a listing of product categories
	 *
	 * @since 2.2
	 *
	 * @param string|null $fields fields to limit response to
	 *
	 * @return array|WP_Error
	 */
	public function get_product_categories( $fields = null ) {
		try {
			// Permissions check
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 );
			}

			$product_categories = array();

			$terms = get_terms( 'product_cat', array( 'hide_empty' => false, 'fields' => 'ids' ) );

			foreach ( $terms as $term_id ) {
				$product_categories[] = current( $this->get_product_category( $term_id, $fields ) );
			}

			return array( 'product_categories' => apply_filters( 'woocommerce_api_product_categories_response', $product_categories, $terms, $fields, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the product category for the given ID
	 *
	 * @since 2.2
	 *
	 * @param string $id product category term ID
	 * @param string|null $fields fields to limit response to
	 *
	 * @return array|WP_Error
	 */
	public function get_product_category( $id, $fields = null ) {
		try {
			$id = absint( $id );

			// Validate ID
			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'Invalid product category ID', 'woocommerce' ), 400 );
			}

			// Permissions check
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 );
			}

			$term = get_term( $id, 'product_cat' );

			if ( is_wp_error( $term ) || is_null( $term ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'A product category with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$term_id = intval( $term->term_id );

			// Get category display type
			$display_type = get_term_meta( $term_id, 'display_type', true );

			// Get category image
			$image = '';
			if ( $image_id = get_term_meta( $term_id, 'thumbnail_id', true ) ) {
				$image = wp_get_attachment_url( $image_id );
			}

			$product_category = array(
				'id'          => $term_id,
				'name'        => $term->name,
				'slug'        => $term->slug,
				'parent'      => $term->parent,
				'description' => $term->description,
				'display'     => $display_type ? $display_type : 'default',
				'image'       => $image ? esc_url( $image ) : '',
				'count'       => intval( $term->count ),
			);

			return array( 'product_category' => apply_filters( 'woocommerce_api_product_category_response', $product_category, $id, $fields, $term, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create a new product category.
	 *
	 * @since  2.5.0
	 * @param  array          $data Posted data
	 * @return array|WP_Error       Product category if succeed, otherwise WP_Error
	 *                              will be returned
	 */
	public function create_product_category( $data ) {
		global $wpdb;

		try {
			if ( ! isset( $data['product_category'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_category_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_category' ), 400 );
			}

			// Check permissions
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_category', __( 'You do not have permission to create product categories', 'woocommerce' ), 401 );
			}

			$defaults = array(
				'name'        => '',
				'slug'        => '',
				'description' => '',
				'parent'      => 0,
				'display'     => 'default',
				'image'       => '',
			);

			$data = wp_parse_args( $data['product_category'], $defaults );
			$data = apply_filters( 'woocommerce_api_create_product_category_data', $data, $this );

			// Check parent.
			$data['parent'] = absint( $data['parent'] );
			if ( $data['parent'] ) {
				$parent = get_term_by( 'id', $data['parent'], 'product_cat' );
				if ( ! $parent ) {
					throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_parent', __( 'Product category parent is invalid', 'woocommerce' ), 400 );
				}
			}

			// If value of image is numeric, assume value as image_id.
			$image    = $data['image'];
			$image_id = 0;
			if ( is_numeric( $image ) ) {
				$image_id = absint( $image );
			} elseif ( ! empty( $image ) ) {
				$upload   = $this->upload_product_category_image( esc_url_raw( $image ) );
				$image_id = $this->set_product_category_image_as_attachment( $upload );
			}

			$insert = wp_insert_term( $data['name'], 'product_cat', $data );
			if ( is_wp_error( $insert ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_category', $insert->get_error_message(), 400 );
			}

			$id = $insert['term_id'];

			update_term_meta( $id, 'display_type', 'default' === $data['display'] ? '' : sanitize_text_field( $data['display'] ) );

			// Check if image_id is a valid image attachment before updating the term meta.
			if ( $image_id && wp_attachment_is_image( $image_id ) ) {
				update_term_meta( $id, 'thumbnail_id', $image_id );
			}

			do_action( 'woocommerce_api_create_product_category', $id, $data );

			$this->server->send_status( 201 );

			return $this->get_product_category( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit a product category.
	 *
	 * @since  2.5.0
	 * @param  int            $id   Product category term ID
	 * @param  array          $data Posted data
	 * @return array|WP_Error       Product category if succeed, otherwise WP_Error
	 *                              will be returned
	 */
	public function edit_product_category( $id, $data ) {
		global $wpdb;

		try {
			if ( ! isset( $data['product_category'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_category', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_category' ), 400 );
			}

			$id   = absint( $id );
			$data = $data['product_category'];

			// Check permissions.
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_category', __( 'You do not have permission to edit product categories', 'woocommerce' ), 401 );
			}

			$data     = apply_filters( 'woocommerce_api_edit_product_category_data', $data, $this );
			$category = $this->get_product_category( $id );

			if ( is_wp_error( $category ) ) {
				return $category;
			}

			if ( isset( $data['image'] ) ) {
				$image_id = 0;

				// If value of image is numeric, assume value as image_id.
				$image = $data['image'];
				if ( is_numeric( $image ) ) {
					$image_id = absint( $image );
				} elseif ( ! empty( $image ) ) {
					$upload   = $this->upload_product_category_image( esc_url_raw( $image ) );
					$image_id = $this->set_product_category_image_as_attachment( $upload );
				}

				// In case client supplies invalid image or wants to unset category image.
				if ( ! wp_attachment_is_image( $image_id ) ) {
					$image_id = '';
				}
			}

			$update = wp_update_term( $id, 'product_cat', $data );
			if ( is_wp_error( $update ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_catgory', __( 'Could not edit the category', 'woocommerce' ), 400 );
			}

			if ( ! empty( $data['display'] ) ) {
				update_term_meta( $id, 'display_type', 'default' === $data['display'] ? '' : sanitize_text_field( $data['display'] ) );
			}

			if ( isset( $image_id ) ) {
				update_term_meta( $id, 'thumbnail_id', $image_id );
			}

			do_action( 'woocommerce_api_edit_product_category', $id, $data );

			return $this->get_product_category( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete a product category.
	 *
	 * @since  2.5.0
	 * @param  int            $id Product category term ID
	 * @return array|WP_Error     Success message if succeed, otherwise WP_Error
	 *                            will be returned
	 */
	public function delete_product_category( $id ) {
		global $wpdb;

		try {
			// Check permissions
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_category', __( 'You do not have permission to delete product category', 'woocommerce' ), 401 );
			}

			$id      = absint( $id );
			$deleted = wp_delete_term( $id, 'product_cat' );
			if ( ! $deleted || is_wp_error( $deleted ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_category', __( 'Could not delete the category', 'woocommerce' ), 401 );
			}

			do_action( 'woocommerce_api_delete_product_category', $id, $this );

			return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_category' ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get a listing of product tags.
	 *
	 * @since  2.5.0
	 *
	 * @param  string|null $fields Fields to limit response to
	 *
	 * @return array|WP_Error
	 */
	public function get_product_tags( $fields = null ) {
		try {
			// Permissions check
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_tags', __( 'You do not have permission to read product tags', 'woocommerce' ), 401 );
			}

			$product_tags = array();

			$terms = get_terms( 'product_tag', array( 'hide_empty' => false, 'fields' => 'ids' ) );

			foreach ( $terms as $term_id ) {
				$product_tags[] = current( $this->get_product_tag( $term_id, $fields ) );
			}

			return array( 'product_tags' => apply_filters( 'woocommerce_api_product_tags_response', $product_tags, $terms, $fields, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the product tag for the given ID.
	 *
	 * @since  2.5.0
	 *
	 * @param  string $id          Product tag term ID
	 * @param  string|null $fields Fields to limit response to
	 *
	 * @return array|WP_Error
	 */
	public function get_product_tag( $id, $fields = null ) {
		try {
			$id = absint( $id );

			// Validate ID
			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_tag_id', __( 'Invalid product tag ID', 'woocommerce' ), 400 );
			}

			// Permissions check
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_tags', __( 'You do not have permission to read product tags', 'woocommerce' ), 401 );
			}

			$term = get_term( $id, 'product_tag' );

			if ( is_wp_error( $term ) || is_null( $term ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_tag_id', __( 'A product tag with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$term_id = intval( $term->term_id );

			$tag = array(
				'id'          => $term_id,
				'name'        => $term->name,
				'slug'        => $term->slug,
				'description' => $term->description,
				'count'       => intval( $term->count ),
			);

			return array( 'product_tag' => apply_filters( 'woocommerce_api_product_tag_response', $tag, $id, $fields, $term, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create a new product tag.
	 *
	 * @since  2.5.0
	 * @param  array          $data Posted data
	 * @return array|WP_Error       Product tag if succeed, otherwise WP_Error
	 *                              will be returned
	 */
	public function create_product_tag( $data ) {
		try {
			if ( ! isset( $data['product_tag'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_tag_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_tag' ), 400 );
			}

			// Check permissions
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_tag', __( 'You do not have permission to create product tags', 'woocommerce' ), 401 );
			}

			$defaults = array(
				'name'        => '',
				'slug'        => '',
				'description' => '',
			);

			$data = wp_parse_args( $data['product_tag'], $defaults );
			$data = apply_filters( 'woocommerce_api_create_product_tag_data', $data, $this );

			$insert = wp_insert_term( $data['name'], 'product_tag', $data );
			if ( is_wp_error( $insert ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_tag', $insert->get_error_message(), 400 );
			}
			$id = $insert['term_id'];

			do_action( 'woocommerce_api_create_product_tag', $id, $data );

			$this->server->send_status( 201 );

			return $this->get_product_tag( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit a product tag.
	 *
	 * @since  2.5.0
	 * @param  int            $id   Product tag term ID
	 * @param  array          $data Posted data
	 * @return array|WP_Error       Product tag if succeed, otherwise WP_Error
	 *                              will be returned
	 */
	public function edit_product_tag( $id, $data ) {
		try {
			if ( ! isset( $data['product_tag'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_tag', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_tag' ), 400 );
			}

			$id   = absint( $id );
			$data = $data['product_tag'];

			// Check permissions.
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_tag', __( 'You do not have permission to edit product tags', 'woocommerce' ), 401 );
			}

			$data = apply_filters( 'woocommerce_api_edit_product_tag_data', $data, $this );
			$tag  = $this->get_product_tag( $id );

			if ( is_wp_error( $tag ) ) {
				return $tag;
			}

			$update = wp_update_term( $id, 'product_tag', $data );
			if ( is_wp_error( $update ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_tag', __( 'Could not edit the tag', 'woocommerce' ), 400 );
			}

			do_action( 'woocommerce_api_edit_product_tag', $id, $data );

			return $this->get_product_tag( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete a product tag.
	 *
	 * @since  2.5.0
	 * @param  int            $id Product tag term ID
	 * @return array|WP_Error     Success message if succeed, otherwise WP_Error
	 *                            will be returned
	 */
	public function delete_product_tag( $id ) {
		try {
			// Check permissions
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_tag', __( 'You do not have permission to delete product tag', 'woocommerce' ), 401 );
			}

			$id      = absint( $id );
			$deleted = wp_delete_term( $id, 'product_tag' );
			if ( ! $deleted || is_wp_error( $deleted ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_tag', __( 'Could not delete the tag', 'woocommerce' ), 401 );
			}

			do_action( 'woocommerce_api_delete_product_tag', $id, $this );

			return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_tag' ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Helper method to get product post objects
	 *
	 * @since 2.1
	 * @param array $args request arguments for filtering query
	 * @return WP_Query
	 */
	private function query_products( $args ) {

		// Set base query arguments
		$query_args = array(
			'fields'      => 'ids',
			'post_type'   => 'product',
			'post_status' => 'publish',
			'meta_query'  => array(),
		);

		// Taxonomy query to filter products by type, category, tag, shipping class, and
		// attribute.
		$tax_query = array();

		// Map between taxonomy name and arg's key.
		$taxonomies_arg_map = array(
			'product_type'           => 'type',
			'product_cat'            => 'category',
			'product_tag'            => 'tag',
			'product_shipping_class' => 'shipping_class',
		);

		// Add attribute taxonomy names into the map.
		foreach ( wc_get_attribute_taxonomy_names() as $attribute_name ) {
			$taxonomies_arg_map[ $attribute_name ] = $attribute_name;
		}

		// Set tax_query for each passed arg.
		foreach ( $taxonomies_arg_map as $tax_name => $arg ) {
			if ( ! empty( $args[ $arg ] ) ) {
				$terms = explode( ',', $args[ $arg ] );

				$tax_query[] = array(
					'taxonomy' => $tax_name,
					'field'    => 'slug',
					'terms'    => $terms,
				);

				unset( $args[ $arg ] );
			}
		}

		if ( ! empty( $tax_query ) ) {
			$query_args['tax_query'] = $tax_query;
		}

		// Filter by specific sku
		if ( ! empty( $args['sku'] ) ) {
			if ( ! is_array( $query_args['meta_query'] ) ) {
				$query_args['meta_query'] = array();
			}

			$query_args['meta_query'][] = array(
				'key'     => '_sku',
				'value'   => $args['sku'],
				'compare' => '=',
			);

			$query_args['post_type'] = array( 'product', 'product_variation' );
		}

		$query_args = $this->merge_query_args( $query_args, $args );

		return new WP_Query( $query_args );
	}

	/**
	 * Get standard product data that applies to every product type
	 *
	 * @since 2.1
	 * @param WC_Product|int $product
	 *
	 * @return array
	 */
	private function get_product_data( $product ) {
		if ( is_numeric( $product ) ) {
			$product = wc_get_product( $product );
		}

		if ( ! is_a( $product, 'WC_Product' ) ) {
			return array();
		}

		return array(
			'title'              => $product->get_name(),
			'id'                 => $product->get_id(),
			'created_at'         => $this->server->format_datetime( $product->get_date_created(), false, true ),
			'updated_at'         => $this->server->format_datetime( $product->get_date_modified(), false, true ),
			'type'               => $product->get_type(),
			'status'             => $product->get_status(),
			'downloadable'       => $product->is_downloadable(),
			'virtual'            => $product->is_virtual(),
			'permalink'          => $product->get_permalink(),
			'sku'                => $product->get_sku(),
			'price'              => $product->get_price(),
			'regular_price'      => $product->get_regular_price(),
			'sale_price'         => $product->get_sale_price() ? $product->get_sale_price() : null,
			'price_html'         => $product->get_price_html(),
			'taxable'            => $product->is_taxable(),
			'tax_status'         => $product->get_tax_status(),
			'tax_class'          => $product->get_tax_class(),
			'managing_stock'     => $product->managing_stock(),
			'stock_quantity'     => $product->get_stock_quantity(),
			'in_stock'           => $product->is_in_stock(),
			'backorders_allowed' => $product->backorders_allowed(),
			'backordered'        => $product->is_on_backorder(),
			'sold_individually'  => $product->is_sold_individually(),
			'purchaseable'       => $product->is_purchasable(),
			'featured'           => $product->is_featured(),
			'visible'            => $product->is_visible(),
			'catalog_visibility' => $product->get_catalog_visibility(),
			'on_sale'            => $product->is_on_sale(),
			'product_url'        => $product->is_type( 'external' ) ? $product->get_product_url() : '',
			'button_text'        => $product->is_type( 'external' ) ? $product->get_button_text() : '',
			'weight'             => $product->get_weight() ? $product->get_weight() : null,
			'dimensions'         => array(
				'length' => $product->get_length(),
				'width'  => $product->get_width(),
				'height' => $product->get_height(),
				'unit'   => get_option( 'woocommerce_dimension_unit' ),
			),
			'shipping_required'  => $product->needs_shipping(),
			'shipping_taxable'   => $product->is_shipping_taxable(),
			'shipping_class'     => $product->get_shipping_class(),
			'shipping_class_id'  => ( 0 !== $product->get_shipping_class_id() ) ? $product->get_shipping_class_id() : null,
			'description'        => wpautop( do_shortcode( $product->get_description() ) ),
			'short_description'  => apply_filters( 'woocommerce_short_description', $product->get_short_description() ),
			'reviews_allowed'    => $product->get_reviews_allowed(),
			'average_rating'     => wc_format_decimal( $product->get_average_rating(), 2 ),
			'rating_count'       => $product->get_rating_count(),
			'related_ids'        => array_map( 'absint', array_values( wc_get_related_products( $product->get_id() ) ) ),
			'upsell_ids'         => array_map( 'absint', $product->get_upsell_ids() ),
			'cross_sell_ids'     => array_map( 'absint', $product->get_cross_sell_ids() ),
			'parent_id'          => $product->get_parent_id(),
			'categories'         => wc_get_object_terms( $product->get_id(), 'product_cat', 'name' ),
			'tags'               => wc_get_object_terms( $product->get_id(), 'product_tag', 'name' ),
			'images'             => $this->get_images( $product ),
			'featured_src'       => wp_get_attachment_url( get_post_thumbnail_id( $product->get_id() ) ),
			'attributes'         => $this->get_attributes( $product ),
			'downloads'          => $this->get_downloads( $product ),
			'download_limit'     => $product->get_download_limit(),
			'download_expiry'    => $product->get_download_expiry(),
			'download_type'      => 'standard',
			'purchase_note'      => wpautop( do_shortcode( wp_kses_post( $product->get_purchase_note() ) ) ),
			'total_sales'        => $product->get_total_sales(),
			'variations'         => array(),
			'parent'             => array(),
			'grouped_products'   => array(),
			'menu_order'         => $this->get_product_menu_order( $product ),
		);
	}

	/**
	 * Get product menu order.
	 *
	 * @since 2.5.3
	 * @param WC_Product $product
	 * @return int
	 */
	private function get_product_menu_order( $product ) {
		$menu_order = $product->get_menu_order();

		return apply_filters( 'woocommerce_api_product_menu_order', $menu_order, $product );
	}

	/**
	 * Get an individual variation's data.
	 *
	 * @since 2.1
	 * @param WC_Product $product
	 * @return array
	 */
	private function get_variation_data( $product ) {
		$variations = array();

		foreach ( $product->get_children() as $child_id ) {
			$variation = wc_get_product( $child_id );

			if ( ! $variation || ! $variation->exists() ) {
				continue;
			}

			$variations[] = array(
				'id'                 => $variation->get_id(),
				'created_at'         => $this->server->format_datetime( $variation->get_date_created(), false, true ),
				'updated_at'         => $this->server->format_datetime( $variation->get_date_modified(), false, true ),
				'downloadable'       => $variation->is_downloadable(),
				'virtual'            => $variation->is_virtual(),
				'permalink'          => $variation->get_permalink(),
				'sku'                => $variation->get_sku(),
				'price'              => $variation->get_price(),
				'regular_price'      => $variation->get_regular_price(),
				'sale_price'         => $variation->get_sale_price() ? $variation->get_sale_price() : null,
				'taxable'            => $variation->is_taxable(),
				'tax_status'         => $variation->get_tax_status(),
				'tax_class'          => $variation->get_tax_class(),
				'managing_stock'     => $variation->managing_stock(),
				'stock_quantity'     => $variation->get_stock_quantity(),
				'in_stock'           => $variation->is_in_stock(),
				'backorders_allowed' => $variation->backorders_allowed(),
				'backordered'        => $variation->is_on_backorder(),
				'purchaseable'       => $variation->is_purchasable(),
				'visible'            => $variation->variation_is_visible(),
				'on_sale'            => $variation->is_on_sale(),
				'weight'             => $variation->get_weight() ? $variation->get_weight() : null,
				'dimensions'         => array(
					'length' => $variation->get_length(),
					'width'  => $variation->get_width(),
					'height' => $variation->get_height(),
					'unit'   => get_option( 'woocommerce_dimension_unit' ),
				),
				'shipping_class'    => $variation->get_shipping_class(),
				'shipping_class_id' => ( 0 !== $variation->get_shipping_class_id() ) ? $variation->get_shipping_class_id() : null,
				'image'             => $this->get_images( $variation ),
				'attributes'        => $this->get_attributes( $variation ),
				'downloads'         => $this->get_downloads( $variation ),
				'download_limit'    => (int) $product->get_download_limit(),
				'download_expiry'   => (int) $product->get_download_expiry(),
			);
		}

		return $variations;
	}

	/**
	 * Get grouped products data
	 *
	 * @since  2.5.0
	 * @param  WC_Product $product
	 *
	 * @return array
	 */
	private function get_grouped_products_data( $product ) {
		$products = array();

		foreach ( $product->get_children() as $child_id ) {
			$_product = wc_get_product( $child_id );

			if ( ! $_product || ! $_product->exists() ) {
				continue;
			}

			$products[] = $this->get_product_data( $_product );

		}

		return $products;
	}

	/**
	 * Save default attributes.
	 *
	 * @since 3.0.0
	 *
	 * @param WC_Product $product
	 * @param WP_REST_Request $request
	 * @return WC_Product
	 */
	protected function save_default_attributes( $product, $request ) {
		// Update default attributes options setting.
		if ( isset( $request['default_attribute'] ) ) {
			$request['default_attributes'] = $request['default_attribute'];
		}

		if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) {
			$attributes         = $product->get_attributes();
			$default_attributes = array();

			foreach ( $request['default_attributes'] as $default_attr_key => $default_attr ) {
				if ( ! isset( $default_attr['name'] ) ) {
					continue;
				}

				$taxonomy = sanitize_title( $default_attr['name'] );

				if ( isset( $default_attr['slug'] ) ) {
					$taxonomy = $this->get_attribute_taxonomy_by_slug( $default_attr['slug'] );
				}

				if ( isset( $attributes[ $taxonomy ] ) ) {
					$_attribute = $attributes[ $taxonomy ];

					if ( $_attribute['is_variation'] ) {
						$value = '';

						if ( isset( $default_attr['option'] ) ) {
							if ( $_attribute['is_taxonomy'] ) {
								// Don't use wc_clean as it destroys sanitized characters
								$value = sanitize_title( trim( stripslashes( $default_attr['option'] ) ) );
							} else {
								$value = wc_clean( trim( stripslashes( $default_attr['option'] ) ) );
							}
						}

						if ( $value ) {
							$default_attributes[ $taxonomy ] = $value;
						}
					}
				}
			}

			$product->set_default_attributes( $default_attributes );
		}

		return $product;
	}

	/**
	 * Save product meta.
	 *
	 * @since  2.2
	 * @param  WC_Product $product
	 * @param  array $data
	 * @return WC_Product
	 * @throws WC_API_Exception
	 */
	protected function save_product_meta( $product, $data ) {
		global $wpdb;

		// Virtual.
		if ( isset( $data['virtual'] ) ) {
			$product->set_virtual( $data['virtual'] );
		}

		// Tax status.
		if ( isset( $data['tax_status'] ) ) {
			$product->set_tax_status( wc_clean( $data['tax_status'] ) );
		}

		// Tax Class.
		if ( isset( $data['tax_class'] ) ) {
			$product->set_tax_class( wc_clean( $data['tax_class'] ) );
		}

		// Catalog Visibility.
		if ( isset( $data['catalog_visibility'] ) ) {
			$product->set_catalog_visibility( wc_clean( $data['catalog_visibility'] ) );
		}

		// Purchase Note.
		if ( isset( $data['purchase_note'] ) ) {
			$product->set_purchase_note( wc_clean( $data['purchase_note'] ) );
		}

		// Featured Product.
		if ( isset( $data['featured'] ) ) {
			$product->set_featured( $data['featured'] );
		}

		// Shipping data.
		$product = $this->save_product_shipping_data( $product, $data );

		// SKU.
		if ( isset( $data['sku'] ) ) {
			$sku     = $product->get_sku();
			$new_sku = wc_clean( $data['sku'] );

			if ( '' == $new_sku ) {
				$product->set_sku( '' );
			} elseif ( $new_sku !== $sku ) {
				if ( ! empty( $new_sku ) ) {
					$unique_sku = wc_product_has_unique_sku( $product->get_id(), $new_sku );
					if ( ! $unique_sku ) {
						throw new WC_API_Exception( 'woocommerce_api_product_sku_already_exists', __( 'The SKU already exists on another product.', 'woocommerce' ), 400 );
					} else {
						$product->set_sku( $new_sku );
					}
				} else {
					$product->set_sku( '' );
				}
			}
		}

		// Attributes.
		if ( isset( $data['attributes'] ) ) {
			$attributes = array();

			foreach ( $data['attributes'] as $attribute ) {
				$is_taxonomy = 0;
				$taxonomy    = 0;

				if ( ! isset( $attribute['name'] ) ) {
					continue;
				}

				$attribute_slug = sanitize_title( $attribute['name'] );

				if ( isset( $attribute['slug'] ) ) {
					$taxonomy       = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] );
					$attribute_slug = sanitize_title( $attribute['slug'] );
				}

				if ( $taxonomy ) {
					$is_taxonomy = 1;
				}

				if ( $is_taxonomy ) {

					$attribute_id = wc_attribute_taxonomy_id_by_name( $attribute['name'] );

					if ( isset( $attribute['options'] ) ) {
						$options = $attribute['options'];

						if ( ! is_array( $attribute['options'] ) ) {
							// Text based attributes - Posted values are term names.
							$options = explode( WC_DELIMITER, $options );
						}

						$values = array_map( 'wc_sanitize_term_text_based', $options );
						$values = array_filter( $values, 'strlen' );
					} else {
						$values = array();
					}

					// Update post terms
					if ( taxonomy_exists( $taxonomy ) ) {
						wp_set_object_terms( $product->get_id(), $values, $taxonomy );
					}

					if ( ! empty( $values ) ) {
						// Add attribute to array, but don't set values.
						$attribute_object = new WC_Product_Attribute();
						$attribute_object->set_id( $attribute_id );
						$attribute_object->set_name( $taxonomy );
						$attribute_object->set_options( $values );
						$attribute_object->set_position( isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0 );
						$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
						$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
						$attributes[] = $attribute_object;
					}
				} elseif ( isset( $attribute['options'] ) ) {
					// Array based.
					if ( is_array( $attribute['options'] ) ) {
						$values = $attribute['options'];

					// Text based, separate by pipe.
					} else {
						$values = array_map( 'wc_clean', explode( WC_DELIMITER, $attribute['options'] ) );
					}

					// Custom attribute - Add attribute to array and set the values.
					$attribute_object = new WC_Product_Attribute();
					$attribute_object->set_name( $attribute['name'] );
					$attribute_object->set_options( $values );
					$attribute_object->set_position( isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0 );
					$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
					$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
					$attributes[] = $attribute_object;
				}
			}

			uasort( $attributes, 'wc_product_attribute_uasort_comparison' );

			$product->set_attributes( $attributes );
		}

		// Sales and prices.
		if ( in_array( $product->get_type(), array( 'variable', 'grouped' ) ) ) {

			// Variable and grouped products have no prices.
			$product->set_regular_price( '' );
			$product->set_sale_price( '' );
			$product->set_date_on_sale_to( '' );
			$product->set_date_on_sale_from( '' );
			$product->set_price( '' );

		} else {

			// Regular Price.
			if ( isset( $data['regular_price'] ) ) {
				$regular_price = ( '' === $data['regular_price'] ) ? '' : $data['regular_price'];
				$product->set_regular_price( $regular_price );
			}

			// Sale Price.
			if ( isset( $data['sale_price'] ) ) {
				$sale_price = ( '' === $data['sale_price'] ) ? '' : $data['sale_price'];
				$product->set_sale_price( $sale_price );
			}

			if ( isset( $data['sale_price_dates_from'] ) ) {
				$date_from = $data['sale_price_dates_from'];
			} else {
				$date_from = $product->get_date_on_sale_from() ? date( 'Y-m-d', $product->get_date_on_sale_from()->getTimestamp() ) : '';
			}

			if ( isset( $data['sale_price_dates_to'] ) ) {
				$date_to = $data['sale_price_dates_to'];
			} else {
				$date_to = $product->get_date_on_sale_to() ? date( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() ) : '';
			}

			if ( $date_to && ! $date_from ) {
				$date_from = strtotime( 'NOW', current_time( 'timestamp', true ) );
			}

			$product->set_date_on_sale_to( $date_to );
			$product->set_date_on_sale_from( $date_from );

			if ( $product->is_on_sale( 'edit' ) ) {
				$product->set_price( $product->get_sale_price( 'edit' ) );
			} else {
				$product->set_price( $product->get_regular_price( 'edit' ) );
			}
		}

		// Product parent ID for groups.
		if ( isset( $data['parent_id'] ) ) {
			$product->set_parent_id( absint( $data['parent_id'] ) );
		}

		// Sold Individually.
		if ( isset( $data['sold_individually'] ) ) {
			$product->set_sold_individually( true === $data['sold_individually'] ? 'yes' : '' );
		}

		// Stock status.
		if ( isset( $data['in_stock'] ) ) {
			$stock_status = ( true === $data['in_stock'] ) ? 'instock' : 'outofstock';
		} else {
			$stock_status = $product->get_stock_status();

			if ( '' === $stock_status ) {
				$stock_status = 'instock';
			}
		}

		// Stock Data.
		if ( 'yes' == get_option( 'woocommerce_manage_stock' ) ) {
			// Manage stock.
			if ( isset( $data['managing_stock'] ) ) {
				$managing_stock = ( true === $data['managing_stock'] ) ? 'yes' : 'no';
				$product->set_manage_stock( $managing_stock );
			} else {
				$managing_stock = $product->get_manage_stock() ? 'yes' : 'no';
			}

			// Backorders.
			if ( isset( $data['backorders'] ) ) {
				if ( 'notify' === $data['backorders'] ) {
					$backorders = 'notify';
				} else {
					$backorders = ( true === $data['backorders'] ) ? 'yes' : 'no';
				}

				$product->set_backorders( $backorders );
			} else {
				$backorders = $product->get_backorders();
			}

			if ( $product->is_type( 'grouped' ) ) {
				$product->set_manage_stock( 'no' );
				$product->set_backorders( 'no' );
				$product->set_stock_quantity( '' );
				$product->set_stock_status( $stock_status );
			} elseif ( $product->is_type( 'external' ) ) {
				$product->set_manage_stock( 'no' );
				$product->set_backorders( 'no' );
				$product->set_stock_quantity( '' );
				$product->set_stock_status( 'instock' );
			} elseif ( 'yes' == $managing_stock ) {
				$product->set_backorders( $backorders );

				// Stock status is always determined by children so sync later.
				if ( ! $product->is_type( 'variable' ) ) {
					$product->set_stock_status( $stock_status );
				}

				// Stock quantity.
				if ( isset( $data['stock_quantity'] ) ) {
					$product->set_stock_quantity( wc_stock_amount( $data['stock_quantity'] ) );
				} elseif ( isset( $data['inventory_delta'] ) ) {
					$stock_quantity  = wc_stock_amount( $product->get_stock_quantity() );
					$stock_quantity += wc_stock_amount( $data['inventory_delta'] );
					$product->set_stock_quantity( wc_stock_amount( $stock_quantity ) );
				}
			} else {
				// Don't manage stock.
				$product->set_manage_stock( 'no' );
				$product->set_backorders( $backorders );
				$product->set_stock_quantity( '' );
				$product->set_stock_status( $stock_status );
			}
		} elseif ( ! $product->is_type( 'variable' ) ) {
			$product->set_stock_status( $stock_status );
		}

		// Upsells.
		if ( isset( $data['upsell_ids'] ) ) {
			$upsells = array();
			$ids     = $data['upsell_ids'];

			if ( ! empty( $ids ) ) {
				foreach ( $ids as $id ) {
					if ( $id && $id > 0 ) {
						$upsells[] = $id;
					}
				}

				$product->set_upsell_ids( $upsells );
			} else {
				$product->set_upsell_ids( array() );
			}
		}

		// Cross sells.
		if ( isset( $data['cross_sell_ids'] ) ) {
			$crosssells = array();
			$ids        = $data['cross_sell_ids'];

			if ( ! empty( $ids ) ) {
				foreach ( $ids as $id ) {
					if ( $id && $id > 0 ) {
						$crosssells[] = $id;
					}
				}

				$product->set_cross_sell_ids( $crosssells );
			} else {
				$product->set_cross_sell_ids( array() );
			}
		}

		// Product categories.
		if ( isset( $data['categories'] ) && is_array( $data['categories'] ) ) {
			$product->set_category_ids( $data['categories'] );
		}

		// Product tags.
		if ( isset( $data['tags'] ) && is_array( $data['tags'] ) ) {
			$product->set_tag_ids( $data['tags'] );
		}

		// Downloadable.
		if ( isset( $data['downloadable'] ) ) {
			$is_downloadable = ( true === $data['downloadable'] ) ? 'yes' : 'no';
			$product->set_downloadable( $is_downloadable );
		} else {
			$is_downloadable = $product->get_downloadable() ? 'yes' : 'no';
		}

		// Downloadable options.
		if ( 'yes' == $is_downloadable ) {

			// Downloadable files.
			if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) {
				$product = $this->save_downloadable_files( $product, $data['downloads'] );
			}

			// Download limit.
			if ( isset( $data['download_limit'] ) ) {
				$product->set_download_limit( $data['download_limit'] );
			}

			// Download expiry.
			if ( isset( $data['download_expiry'] ) ) {
				$product->set_download_expiry( $data['download_expiry'] );
			}
		}

		// Product url.
		if ( $product->is_type( 'external' ) ) {
			if ( isset( $data['product_url'] ) ) {
				$product->set_product_url( $data['product_url'] );
			}

			if ( isset( $data['button_text'] ) ) {
				$product->set_button_text( $data['button_text'] );
			}
		}

		// Reviews allowed.
		if ( isset( $data['reviews_allowed'] ) ) {
			$product->set_reviews_allowed( $data['reviews_allowed'] );
		}

		// Save default attributes for variable products.
		if ( $product->is_type( 'variable' ) ) {
			$product = $this->save_default_attributes( $product, $data );
		}

		// Do action for product type
		do_action( 'woocommerce_api_process_product_meta_' . $product->get_type(), $product->get_id(), $data );

		return $product;
	}

	/**
	 * Save variations.
	 *
	 * @since  2.2
	 *
	 * @param  WC_Product $product
	 * @param  array $request
	 *
	 * @return bool
	 * @throws WC_API_Exception
	 */
	protected function save_variations( $product, $request ) {
		global $wpdb;

		$id         = $product->get_id();
		$variations = $request['variations'];
		$attributes = $product->get_attributes();

		foreach ( $variations as $menu_order => $data ) {
			$variation_id = isset( $data['id'] ) ? absint( $data['id'] ) : 0;
			$variation    = new WC_Product_Variation( $variation_id );

			// Create initial name and status.
			if ( ! $variation->get_slug() ) {
				/* translators: 1: variation id 2: product name */
				$variation->set_name( sprintf( __( 'Variation #%1$s of %2$s', 'woocommerce' ), $variation->get_id(), $product->get_name() ) );
				$variation->set_status( isset( $data['visible'] ) && false === $data['visible'] ? 'private' : 'publish' );
			}

			// Parent ID.
			$variation->set_parent_id( $product->get_id() );

			// Menu order.
			$variation->set_menu_order( $menu_order );

			// Status.
			if ( isset( $data['visible'] ) ) {
				$variation->set_status( false === $data['visible'] ? 'private' : 'publish' );
			}

			// SKU.
			if ( isset( $data['sku'] ) ) {
				$variation->set_sku( wc_clean( $data['sku'] ) );
			}

			// Thumbnail.
			if ( isset( $data['image'] ) && is_array( $data['image'] ) ) {
				$image = current( $data['image'] );
				if ( is_array( $image ) ) {
					$image['position'] = 0;
				}

				$variation = $this->save_product_images( $variation, array( $image ) );
			}

			// Virtual variation.
			if ( isset( $data['virtual'] ) ) {
				$variation->set_virtual( $data['virtual'] );
			}

			// Downloadable variation.
			if ( isset( $data['downloadable'] ) ) {
				$is_downloadable = $data['downloadable'];
				$variation->set_downloadable( $is_downloadable );
			} else {
				$is_downloadable = $variation->get_downloadable();
			}

			// Downloads.
			if ( $is_downloadable ) {
				// Downloadable files.
				if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) {
					$variation = $this->save_downloadable_files( $variation, $data['downloads'] );
				}

				// Download limit.
				if ( isset( $data['download_limit'] ) ) {
					$variation->set_download_limit( $data['download_limit'] );
				}

				// Download expiry.
				if ( isset( $data['download_expiry'] ) ) {
					$variation->set_download_expiry( $data['download_expiry'] );
				}
			}

			// Shipping data.
			$variation = $this->save_product_shipping_data( $variation, $data );

			// Stock handling.
			$manage_stock = (bool) $variation->get_manage_stock();
			if ( isset( $data['managing_stock'] ) ) {
				$manage_stock = $data['managing_stock'];
			}
			$variation->set_manage_stock( $manage_stock );

			$stock_status = $variation->get_stock_status();
			if ( isset( $data['in_stock'] ) ) {
				$stock_status = true === $data['in_stock'] ? 'instock' : 'outofstock';
			}
			$variation->set_stock_status( $stock_status );

			$backorders = $variation->get_backorders();
			if ( isset( $data['backorders'] ) ) {
				$backorders = $data['backorders'];
			}
			$variation->set_backorders( $backorders );

			if ( $manage_stock ) {
				if ( isset( $data['stock_quantity'] ) ) {
					$variation->set_stock_quantity( $data['stock_quantity'] );
				} elseif ( isset( $data['inventory_delta'] ) ) {
					$stock_quantity  = wc_stock_amount( $variation->get_stock_quantity() );
					$stock_quantity += wc_stock_amount( $data['inventory_delta'] );
					$variation->set_stock_quantity( $stock_quantity );
				}
			} else {
				$variation->set_backorders( 'no' );
				$variation->set_stock_quantity( '' );
			}

			// Regular Price.
			if ( isset( $data['regular_price'] ) ) {
				$variation->set_regular_price( $data['regular_price'] );
			}

			// Sale Price.
			if ( isset( $data['sale_price'] ) ) {
				$variation->set_sale_price( $data['sale_price'] );
			}

			if ( isset( $data['sale_price_dates_from'] ) ) {
				$variation->set_date_on_sale_from( $data['sale_price_dates_from'] );
			}

			if ( isset( $data['sale_price_dates_to'] ) ) {
				$variation->set_date_on_sale_to( $data['sale_price_dates_to'] );
			}

			// Tax class.
			if ( isset( $data['tax_class'] ) ) {
				$variation->set_tax_class( $data['tax_class'] );
			}

			// Description.
			if ( isset( $data['description'] ) ) {
				$variation->set_description( wp_kses_post( $data['description'] ) );
			}

			// Update taxonomies.
			if ( isset( $data['attributes'] ) ) {
				$_attributes = array();

				foreach ( $data['attributes'] as $attribute_key => $attribute ) {
					if ( ! isset( $attribute['name'] ) ) {
						continue;
					}

					$taxonomy   = 0;
					$_attribute = array();

					if ( isset( $attribute['slug'] ) ) {
						$taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] );
					}

					if ( ! $taxonomy ) {
						$taxonomy = sanitize_title( $attribute['name'] );
					}

					if ( isset( $attributes[ $taxonomy ] ) ) {
						$_attribute = $attributes[ $taxonomy ];
					}

					if ( isset( $_attribute['is_variation'] ) && $_attribute['is_variation'] ) {
						$_attribute_key = sanitize_title( $_attribute['name'] );

						if ( isset( $_attribute['is_taxonomy'] ) && $_attribute['is_taxonomy'] ) {
							// Don't use wc_clean as it destroys sanitized characters.
							$_attribute_value = isset( $attribute['option'] ) ? sanitize_title( stripslashes( $attribute['option'] ) ) : '';
						} else {
							$_attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : '';
						}

						$_attributes[ $_attribute_key ] = $_attribute_value;
					}
				}

				$variation->set_attributes( $_attributes );
			}

			$variation->save();

			do_action( 'woocommerce_api_save_product_variation', $variation_id, $menu_order, $variation );
		}

		return true;
	}

	/**
	 * Save product shipping data
	 *
	 * @since 2.2
	 * @param WC_Product $product
	 * @param array $data
	 * @return WC_Product
	 */
	private function save_product_shipping_data( $product, $data ) {
		if ( isset( $data['weight'] ) ) {
			$product->set_weight( '' === $data['weight'] ? '' : wc_format_decimal( $data['weight'] ) );
		}

		// Product dimensions
		if ( isset( $data['dimensions'] ) ) {
			// Height
			if ( isset( $data['dimensions']['height'] ) ) {
				$product->set_height( '' === $data['dimensions']['height'] ? '' : wc_format_decimal( $data['dimensions']['height'] ) );
			}

			// Width
			if ( isset( $data['dimensions']['width'] ) ) {
				$product->set_width( '' === $data['dimensions']['width'] ? '' : wc_format_decimal( $data['dimensions']['width'] ) );
			}

			// Length
			if ( isset( $data['dimensions']['length'] ) ) {
				$product->set_length( '' === $data['dimensions']['length'] ? '' : wc_format_decimal( $data['dimensions']['length'] ) );
			}
		}

		// Virtual
		if ( isset( $data['virtual'] ) ) {
			$virtual = ( true === $data['virtual'] ) ? 'yes' : 'no';

			if ( 'yes' == $virtual ) {
				$product->set_weight( '' );
				$product->set_height( '' );
				$product->set_length( '' );
				$product->set_width( '' );
			}
		}

		// Shipping class
		if ( isset( $data['shipping_class'] ) ) {
			$data_store        = $product->get_data_store();
			$shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $data['shipping_class'] ) );
			$product->set_shipping_class_id( $shipping_class_id );
		}

		return $product;
	}

	/**
	 * Save downloadable files
	 *
	 * @since 2.2
	 * @param WC_Product $product
	 * @param array $downloads
	 * @param int $deprecated Deprecated since 3.0.
	 * @return WC_Product
	 */
	private function save_downloadable_files( $product, $downloads, $deprecated = 0 ) {
		if ( $deprecated ) {
			wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() does not require a variation_id anymore.' );
		}

		$files = array();
		foreach ( $downloads as $key => $file ) {
			if ( isset( $file['url'] ) ) {
				$file['file'] = $file['url'];
			}

			if ( empty( $file['file'] ) ) {
				continue;
			}

			$download = new WC_Product_Download();
			$download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() );
			$download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) );
			$download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) );
			$files[]  = $download;
		}
		$product->set_downloads( $files );

		return $product;
	}

	/**
	 * Get attribute taxonomy by slug.
	 *
	 * @since 2.2
	 * @param string $slug
	 * @return string|null
	 */
	private function get_attribute_taxonomy_by_slug( $slug ) {
		$taxonomy = null;
		$attribute_taxonomies = wc_get_attribute_taxonomies();

		foreach ( $attribute_taxonomies as $key => $tax ) {
			if ( $slug == $tax->attribute_name ) {
				$taxonomy = 'pa_' . $tax->attribute_name;

				break;
			}
		}

		return $taxonomy;
	}

	/**
	 * Get the images for a product or product variation
	 *
	 * @since 2.1
	 * @param WC_Product|WC_Product_Variation $product
	 * @return array
	 */
	private function get_images( $product ) {
		$images        = $attachment_ids = array();
		$product_image = $product->get_image_id();

		// Add featured image.
		if ( ! empty( $product_image ) ) {
			$attachment_ids[] = $product_image;
		}

		// Add gallery images.
		$attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() );

		// Build image data.
		foreach ( $attachment_ids as $position => $attachment_id ) {

			$attachment_post = get_post( $attachment_id );

			if ( is_null( $attachment_post ) ) {
				continue;
			}

			$attachment = wp_get_attachment_image_src( $attachment_id, 'full' );

			if ( ! is_array( $attachment ) ) {
				continue;
			}

			$images[] = array(
				'id'         => (int) $attachment_id,
				'created_at' => $this->server->format_datetime( $attachment_post->post_date_gmt ),
				'updated_at' => $this->server->format_datetime( $attachment_post->post_modified_gmt ),
				'src'        => current( $attachment ),
				'title'      => get_the_title( $attachment_id ),
				'alt'        => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ),
				'position'   => (int) $position,
			);
		}

		// Set a placeholder image if the product has no images set.
		if ( empty( $images ) ) {

			$images[] = array(
				'id'         => 0,
				'created_at' => $this->server->format_datetime( time() ), // Default to now.
				'updated_at' => $this->server->format_datetime( time() ),
				'src'        => wc_placeholder_img_src(),
				'title'      => __( 'Placeholder', 'woocommerce' ),
				'alt'        => __( 'Placeholder', 'woocommerce' ),
				'position'   => 0,
			);
		}

		return $images;
	}

	/**
	 * Save product images.
	 *
	 * @since  2.2
	 * @param  WC_Product $product
	 * @param  array $images
	 * @throws WC_API_Exception
	 * @return WC_Product
	 */
	protected function save_product_images( $product, $images ) {
		if ( is_array( $images ) ) {
			$gallery = array();

			foreach ( $images as $image ) {
				if ( isset( $image['position'] ) && 0 == $image['position'] ) {
					$attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0;

					if ( 0 === $attachment_id && isset( $image['src'] ) ) {
						$upload = $this->upload_product_image( esc_url_raw( $image['src'] ) );

						if ( is_wp_error( $upload ) ) {
							throw new WC_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 );
						}

						$attachment_id = $this->set_product_image_as_attachment( $upload, $product->get_id() );
					}

					$product->set_image_id( $attachment_id );
				} else {
					$attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0;

					if ( 0 === $attachment_id && isset( $image['src'] ) ) {
						$upload = $this->upload_product_image( esc_url_raw( $image['src'] ) );

						if ( is_wp_error( $upload ) ) {
							throw new WC_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 );
						}

						$attachment_id = $this->set_product_image_as_attachment( $upload, $product->get_id() );
					}

					$gallery[] = $attachment_id;
				}

				// Set the image alt if present.
				if ( ! empty( $image['alt'] ) && $attachment_id ) {
					update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) );
				}

				// Set the image title if present.
				if ( ! empty( $image['title'] ) && $attachment_id ) {
					wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['title'] ) );
				}
			}

			if ( ! empty( $gallery ) ) {
				$product->set_gallery_image_ids( $gallery );
			}
		} else {
			$product->set_image_id( '' );
			$product->set_gallery_image_ids( array() );
		}

		return $product;
	}

	/**
	 * Upload image from URL
	 *
	 * @since 2.2
	 * @param string $image_url
	 * @return int|WP_Error attachment id
	 */
	public function upload_product_image( $image_url ) {
		return $this->upload_image_from_url( $image_url, 'product_image' );
	}

	/**
	 * Upload product category image from URL.
	 *
	 * @since 2.5.0
	 * @param string $image_url
	 * @return int|WP_Error attachment id
	 */
	public function upload_product_category_image( $image_url ) {
		return $this->upload_image_from_url( $image_url, 'product_category_image' );
	}

	/**
	 * Upload image from URL.
	 *
	 * @throws WC_API_Exception
	 *
	 * @since 2.5.0
	 * @param string $image_url
	 * @param string $upload_for
	 * @return array
	 */
	protected function upload_image_from_url( $image_url, $upload_for = 'product_image' ) {
		$upload = wc_rest_upload_image_from_url( $image_url );
		if ( is_wp_error( $upload ) ) {
			throw new WC_API_Exception( 'woocommerce_api_' . $upload_for . '_upload_error', $upload->get_error_message(), 400 );
		}

		do_action( 'woocommerce_api_uploaded_image_from_url', $upload, $image_url, $upload_for );

		return $upload;
	}

	/**
	 * Sets product image as attachment and returns the attachment ID.
	 *
	 * @since 2.2
	 * @param array $upload
	 * @param int $id
	 * @return int
	 */
	protected function set_product_image_as_attachment( $upload, $id ) {
		return $this->set_uploaded_image_as_attachment( $upload, $id );
	}

	/**
	 * Sets uploaded category image as attachment and returns the attachment ID.
	 *
	 * @since  2.5.0
	 * @param  integer $upload Upload information from wp_upload_bits
	 * @return int             Attachment ID
	 */
	protected function set_product_category_image_as_attachment( $upload ) {
		return $this->set_uploaded_image_as_attachment( $upload );
	}

	/**
	 * Set uploaded image as attachment.
	 *
	 * @since  2.5.0
	 * @param  array $upload Upload information from wp_upload_bits
	 * @param  int   $id     Post ID. Default to 0.
	 * @return int           Attachment ID
	 */
	protected function set_uploaded_image_as_attachment( $upload, $id = 0 ) {
		$info    = wp_check_filetype( $upload['file'] );
		$title   = '';
		$content = '';

		if ( $image_meta = @wp_read_image_metadata( $upload['file'] ) ) {
			if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) {
				$title = wc_clean( $image_meta['title'] );
			}
			if ( trim( $image_meta['caption'] ) ) {
				$content = wc_clean( $image_meta['caption'] );
			}
		}

		$attachment = array(
			'post_mime_type' => $info['type'],
			'guid'           => $upload['url'],
			'post_parent'    => $id,
			'post_title'     => $title,
			'post_content'   => $content,
		);

		$attachment_id = wp_insert_attachment( $attachment, $upload['file'], $id );
		if ( ! is_wp_error( $attachment_id ) ) {
			wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) );
		}

		return $attachment_id;
	}

	/**
	 * Get attribute options.
	 *
	 * @param int $product_id
	 * @param array $attribute
	 * @return array
	 */
	protected function get_attribute_options( $product_id, $attribute ) {
		if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) {
			return wc_get_product_terms( $product_id, $attribute['name'], array( 'fields' => 'names' ) );
		} elseif ( isset( $attribute['value'] ) ) {
			return array_map( 'trim', explode( '|', $attribute['value'] ) );
		}

		return array();
	}

	/**
	 * Get the attributes for a product or product variation
	 *
	 * @since 2.1
	 * @param WC_Product|WC_Product_Variation $product
	 * @return array
	 */
	private function get_attributes( $product ) {

		$attributes = array();

		if ( $product->is_type( 'variation' ) ) {

			// variation attributes
			foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) {

				// taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_`
				$attributes[] = array(
					'name'   => wc_attribute_label( str_replace( 'attribute_', '', $attribute_name ), $product ),
					'slug'   => str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $attribute_name ) ),
					'option' => $attribute,
				);
			}
		} else {

			foreach ( $product->get_attributes() as $attribute ) {
				$attributes[] = array(
					'name'      => wc_attribute_label( $attribute['name'], $product ),
					'slug'      => wc_attribute_taxonomy_slug( $attribute['name'] ),
					'position'  => (int) $attribute['position'],
					'visible'   => (bool) $attribute['is_visible'],
					'variation' => (bool) $attribute['is_variation'],
					'options'   => $this->get_attribute_options( $product->get_id(), $attribute ),
				);
			}
		}

		return $attributes;
	}

	/**
	 * Get the downloads for a product or product variation
	 *
	 * @since 2.1
	 * @param WC_Product|WC_Product_Variation $product
	 * @return array
	 */
	private function get_downloads( $product ) {

		$downloads = array();

		if ( $product->is_downloadable() ) {

			foreach ( $product->get_downloads() as $file_id => $file ) {

				$downloads[] = array(
					'id'   => $file_id, // do not cast as int as this is a hash
					'name' => $file['name'],
					'file' => $file['file'],
				);
			}
		}

		return $downloads;
	}

	/**
	 * Get a listing of product attributes
	 *
	 * @since 2.5.0
	 *
	 * @param string|null $fields fields to limit response to
	 *
	 * @return array|WP_Error
	 */
	public function get_product_attributes( $fields = null ) {
		try {
			// Permissions check.
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attributes', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 );
			}

			$product_attributes   = array();
			$attribute_taxonomies = wc_get_attribute_taxonomies();

			foreach ( $attribute_taxonomies as $attribute ) {
				$product_attributes[] = array(
					'id'           => intval( $attribute->attribute_id ),
					'name'         => $attribute->attribute_label,
					'slug'         => wc_attribute_taxonomy_name( $attribute->attribute_name ),
					'type'         => $attribute->attribute_type,
					'order_by'     => $attribute->attribute_orderby,
					'has_archives' => (bool) $attribute->attribute_public,
				);
			}

			return array( 'product_attributes' => apply_filters( 'woocommerce_api_product_attributes_response', $product_attributes, $attribute_taxonomies, $fields, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the product attribute for the given ID
	 *
	 * @since 2.5.0
	 *
	 * @param string $id product attribute term ID
	 * @param string|null $fields fields to limit response to
	 *
	 * @return array|WP_Error
	 */
	public function get_product_attribute( $id, $fields = null ) {
		global $wpdb;

		try {
			$id = absint( $id );

			// Validate ID
			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'Invalid product attribute ID', 'woocommerce' ), 400 );
			}

			// Permissions check
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attributes', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 );
			}

			$attribute = $wpdb->get_row( $wpdb->prepare( "
				SELECT *
				FROM {$wpdb->prefix}woocommerce_attribute_taxonomies
				WHERE attribute_id = %d
			 ", $id ) );

			if ( is_wp_error( $attribute ) || is_null( $attribute ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$product_attribute = array(
				'id'           => intval( $attribute->attribute_id ),
				'name'         => $attribute->attribute_label,
				'slug'         => wc_attribute_taxonomy_name( $attribute->attribute_name ),
				'type'         => $attribute->attribute_type,
				'order_by'     => $attribute->attribute_orderby,
				'has_archives' => (bool) $attribute->attribute_public,
			);

			return array( 'product_attribute' => apply_filters( 'woocommerce_api_product_attribute_response', $product_attribute, $id, $fields, $attribute, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Validate attribute data.
	 *
	 * @since  2.5.0
	 * @param  string $name
	 * @param  string $slug
	 * @param  string $type
	 * @param  string $order_by
	 * @param  bool   $new_data
	 * @return bool
	 * @throws WC_API_Exception
	 */
	protected function validate_attribute_data( $name, $slug, $type, $order_by, $new_data = true ) {
		if ( empty( $name ) ) {
			throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 );
		}

		if ( strlen( $slug ) >= 28 ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $slug ), 400 );
		} elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $slug ), 400 );
		} elseif ( $new_data && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'woocommerce' ), $slug ), 400 );
		}

		// Validate the attribute type
		if ( ! in_array( wc_clean( $type ), array_keys( wc_get_attribute_types() ) ) ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_type', sprintf( __( 'Invalid product attribute type - the product attribute type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_attribute_types() ) ) ), 400 );
		}

		// Validate the attribute order by
		if ( ! in_array( wc_clean( $order_by ), array( 'menu_order', 'name', 'name_num', 'id' ) ) ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_order_by', sprintf( __( 'Invalid product attribute order_by type - the product attribute order_by type must be any of these: %s', 'woocommerce' ), implode( ', ', array( 'menu_order', 'name', 'name_num', 'id' ) ) ), 400 );
		}

		return true;
	}

	/**
	 * Create a new product attribute.
	 *
	 * @since 2.5.0
	 *
	 * @param array $data Posted data.
	 *
	 * @return array|WP_Error
	 */
	public function create_product_attribute( $data ) {
		global $wpdb;

		try {
			if ( ! isset( $data['product_attribute'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_attribute' ), 400 );
			}

			$data = $data['product_attribute'];

			// Check permissions.
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_attribute', __( 'You do not have permission to create product attributes', 'woocommerce' ), 401 );
			}

			$data = apply_filters( 'woocommerce_api_create_product_attribute_data', $data, $this );

			if ( ! isset( $data['name'] ) ) {
				$data['name'] = '';
			}

			// Set the attribute slug.
			if ( ! isset( $data['slug'] ) ) {
				$data['slug'] = wc_sanitize_taxonomy_name( stripslashes( $data['name'] ) );
			} else {
				$data['slug'] = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) ) );
			}

			// Set attribute type when not sent.
			if ( ! isset( $data['type'] ) ) {
				$data['type'] = 'select';
			}

			// Set order by when not sent.
			if ( ! isset( $data['order_by'] ) ) {
				$data['order_by'] = 'menu_order';
			}

			// Validate the attribute data.
			$this->validate_attribute_data( $data['name'], $data['slug'], $data['type'], $data['order_by'], true );

			$insert = $wpdb->insert(
				$wpdb->prefix . 'woocommerce_attribute_taxonomies',
				array(
					'attribute_label'   => $data['name'],
					'attribute_name'    => $data['slug'],
					'attribute_type'    => $data['type'],
					'attribute_orderby' => $data['order_by'],
					'attribute_public'  => isset( $data['has_archives'] ) && true === $data['has_archives'] ? 1 : 0,
				),
				array( '%s', '%s', '%s', '%s', '%d' )
			);

			// Checks for an error in the product creation.
			if ( is_wp_error( $insert ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_attribute', $insert->get_error_message(), 400 );
			}

			$id = $wpdb->insert_id;

			do_action( 'woocommerce_api_create_product_attribute', $id, $data );

			// Clear transients.
			wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' );
			delete_transient( 'wc_attribute_taxonomies' );
			WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' );

			$this->server->send_status( 201 );

			return $this->get_product_attribute( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit a product attribute.
	 *
	 * @since 2.5.0
	 *
	 * @param int $id the attribute ID.
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function edit_product_attribute( $id, $data ) {
		global $wpdb;

		try {
			if ( ! isset( $data['product_attribute'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_attribute' ), 400 );
			}

			$id   = absint( $id );
			$data = $data['product_attribute'];

			// Check permissions.
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_attribute', __( 'You do not have permission to edit product attributes', 'woocommerce' ), 401 );
			}

			$data      = apply_filters( 'woocommerce_api_edit_product_attribute_data', $data, $this );
			$attribute = $this->get_product_attribute( $id );

			if ( is_wp_error( $attribute ) ) {
				return $attribute;
			}

			$attribute_name     = isset( $data['name'] ) ? $data['name'] : $attribute['product_attribute']['name'];
			$attribute_type     = isset( $data['type'] ) ? $data['type'] : $attribute['product_attribute']['type'];
			$attribute_order_by = isset( $data['order_by'] ) ? $data['order_by'] : $attribute['product_attribute']['order_by'];

			if ( isset( $data['slug'] ) ) {
				$attribute_slug = wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) );
			} else {
				$attribute_slug = $attribute['product_attribute']['slug'];
			}
			$attribute_slug = preg_replace( '/^pa\_/', '', $attribute_slug );

			if ( isset( $data['has_archives'] ) ) {
				$attribute_public = true === $data['has_archives'] ? 1 : 0;
			} else {
				$attribute_public = $attribute['product_attribute']['has_archives'];
			}

			// Validate the attribute data.
			$this->validate_attribute_data( $attribute_name, $attribute_slug, $attribute_type, $attribute_order_by, false );

			$update = $wpdb->update(
				$wpdb->prefix . 'woocommerce_attribute_taxonomies',
				array(
					'attribute_label'   => $attribute_name,
					'attribute_name'    => $attribute_slug,
					'attribute_type'    => $attribute_type,
					'attribute_orderby' => $attribute_order_by,
					'attribute_public'  => $attribute_public,
				),
				array( 'attribute_id' => $id ),
				array( '%s', '%s', '%s', '%s', '%d' ),
				array( '%d' )
			);

			// Checks for an error in the product creation.
			if ( false === $update ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_attribute', __( 'Could not edit the attribute', 'woocommerce' ), 400 );
			}

			do_action( 'woocommerce_api_edit_product_attribute', $id, $data );

			// Clear transients.
			wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' );
			delete_transient( 'wc_attribute_taxonomies' );
			WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' );

			return $this->get_product_attribute( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete a product attribute.
	 *
	 * @since  2.5.0
	 *
	 * @param  int $id the product attribute ID.
	 *
	 * @return array|WP_Error
	 */
	public function delete_product_attribute( $id ) {
		global $wpdb;

		try {
			// Check permissions.
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_attribute', __( 'You do not have permission to delete product attributes', 'woocommerce' ), 401 );
			}

			$id = absint( $id );

			$attribute_name = $wpdb->get_var( $wpdb->prepare( "
				SELECT attribute_name
				FROM {$wpdb->prefix}woocommerce_attribute_taxonomies
				WHERE attribute_id = %d
			 ", $id ) );

			if ( is_null( $attribute_name ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$deleted = $wpdb->delete(
				$wpdb->prefix . 'woocommerce_attribute_taxonomies',
				array( 'attribute_id' => $id ),
				array( '%d' )
			);

			if ( false === $deleted ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute', __( 'Could not delete the attribute', 'woocommerce' ), 401 );
			}

			$taxonomy = wc_attribute_taxonomy_name( $attribute_name );

			if ( taxonomy_exists( $taxonomy ) ) {
				$terms = get_terms( $taxonomy, 'orderby=name&hide_empty=0' );
				foreach ( $terms as $term ) {
					wp_delete_term( $term->term_id, $taxonomy );
				}
			}

			do_action( 'woocommerce_attribute_deleted', $id, $attribute_name, $taxonomy );
			do_action( 'woocommerce_api_delete_product_attribute', $id, $this );

			// Clear transients.
			wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' );
			delete_transient( 'wc_attribute_taxonomies' );
			WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' );

			return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_attribute' ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get a listing of product attribute terms.
	 *
	 * @since 2.5.0
	 *
	 * @param int $attribute_id Attribute ID.
	 * @param string|null $fields Fields to limit response to.
	 *
	 * @return array|WP_Error
	 */
	public function get_product_attribute_terms( $attribute_id, $fields = null ) {
		try {
			// Permissions check.
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attribute_terms', __( 'You do not have permission to read product attribute terms', 'woocommerce' ), 401 );
			}

			$attribute_id = absint( $attribute_id );
			$taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id );

			if ( ! $taxonomy ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$terms = get_terms( $taxonomy, array( 'hide_empty' => false ) );
			$attribute_terms = array();

			foreach ( $terms as $term ) {
				$attribute_terms[] = array(
					'id'    => $term->term_id,
					'slug'  => $term->slug,
					'name'  => $term->name,
					'count' => $term->count,
				);
			}

			return array( 'product_attribute_terms' => apply_filters( 'woocommerce_api_product_attribute_terms_response', $attribute_terms, $terms, $fields, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the product attribute term for the given ID.
	 *
	 * @since 2.5.0
	 *
	 * @param int $attribute_id Attribute ID.
	 * @param string $id Product attribute term ID.
	 * @param string|null $fields Fields to limit response to.
	 *
	 * @return array|WP_Error
	 */
	public function get_product_attribute_term( $attribute_id, $id, $fields = null ) {
		global $wpdb;

		try {
			$id = absint( $id );
			$attribute_id = absint( $attribute_id );

			// Validate ID
			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_term_id', __( 'Invalid product attribute ID', 'woocommerce' ), 400 );
			}

			// Permissions check
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attribute_terms', __( 'You do not have permission to read product attribute terms', 'woocommerce' ), 401 );
			}

			$taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id );

			if ( ! $taxonomy ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$term = get_term( $id, $taxonomy );

			if ( is_wp_error( $term ) || is_null( $term ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_term_id', __( 'A product attribute term with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$attribute_term = array(
				'id'    => $term->term_id,
				'name'  => $term->name,
				'slug'  => $term->slug,
				'count' => $term->count,
			);

			return array( 'product_attribute_term' => apply_filters( 'woocommerce_api_product_attribute_response', $attribute_term, $id, $fields, $term, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create a new product attribute term.
	 *
	 * @since 2.5.0
	 *
	 * @param int $attribute_id Attribute ID.
	 * @param array $data Posted data.
	 *
	 * @return array|WP_Error
	 */
	public function create_product_attribute_term( $attribute_id, $data ) {
		global $wpdb;

		try {
			if ( ! isset( $data['product_attribute_term'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_term_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_attribute_term' ), 400 );
			}

			$data = $data['product_attribute_term'];

			// Check permissions.
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_attribute', __( 'You do not have permission to create product attributes', 'woocommerce' ), 401 );
			}

			$taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id );

			if ( ! $taxonomy ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$data = apply_filters( 'woocommerce_api_create_product_attribute_term_data', $data, $this );

			// Check if attribute term name is specified.
			if ( ! isset( $data['name'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_term_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 );
			}

			$args = array();

			// Set the attribute term slug.
			if ( isset( $data['slug'] ) ) {
				$args['slug'] = sanitize_title( wp_unslash( $data['slug'] ) );
			}

			$term = wp_insert_term( $data['name'], $taxonomy, $args );

			// Checks for an error in the term creation.
			if ( is_wp_error( $term ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_attribute', $term->get_error_message(), 400 );
			}

			$id = $term['term_id'];

			do_action( 'woocommerce_api_create_product_attribute_term', $id, $data );

			$this->server->send_status( 201 );

			return $this->get_product_attribute_term( $attribute_id, $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit a product attribute term.
	 *
	 * @since 2.5.0
	 *
	 * @param int $attribute_id Attribute ID.
	 * @param int $id the attribute ID.
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function edit_product_attribute_term( $attribute_id, $id, $data ) {
		global $wpdb;

		try {
			if ( ! isset( $data['product_attribute_term'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_term_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_attribute_term' ), 400 );
			}

			$id   = absint( $id );
			$data = $data['product_attribute_term'];

			// Check permissions.
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_attribute', __( 'You do not have permission to edit product attributes', 'woocommerce' ), 401 );
			}

			$taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id );

			if ( ! $taxonomy ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$data = apply_filters( 'woocommerce_api_edit_product_attribute_term_data', $data, $this );

			$args = array();

			// Update name.
			if ( isset( $data['name'] ) ) {
				$args['name'] = wc_clean( wp_unslash( $data['name'] ) );
			}

			// Update slug.
			if ( isset( $data['slug'] ) ) {
				$args['slug'] = sanitize_title( wp_unslash( $data['slug'] ) );
			}

			$term = wp_update_term( $id, $taxonomy, $args );

			if ( is_wp_error( $term ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_attribute_term', $term->get_error_message(), 400 );
			}

			do_action( 'woocommerce_api_edit_product_attribute_term', $id, $data );

			return $this->get_product_attribute_term( $attribute_id, $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete a product attribute term.
	 *
	 * @since  2.5.0
	 *
	 * @param int $attribute_id Attribute ID.
	 * @param int $id the product attribute ID.
	 *
	 * @return array|WP_Error
	 */
	public function delete_product_attribute_term( $attribute_id, $id ) {
		global $wpdb;

		try {
			// Check permissions.
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_attribute_term', __( 'You do not have permission to delete product attribute terms', 'woocommerce' ), 401 );
			}

			$taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id );

			if ( ! $taxonomy ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$id   = absint( $id );
			$term = wp_delete_term( $id, $taxonomy );

			if ( ! $term ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute_term', sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), 'product_attribute_term' ), 500 );
			} elseif ( is_wp_error( $term ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute_term', $term->get_error_message(), 400 );
			}

			do_action( 'woocommerce_api_delete_product_attribute_term', $id, $this );

			return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_attribute' ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Clear product
	 *
	 * @param int $product_id
	 */
	protected function clear_product( $product_id ) {
		if ( ! is_numeric( $product_id ) || 0 >= $product_id ) {
			return;
		}

		// Delete product attachments
		$attachments = get_children( array(
			'post_parent' => $product_id,
			'post_status' => 'any',
			'post_type'   => 'attachment',
		) );

		foreach ( (array) $attachments as $attachment ) {
			wp_delete_attachment( $attachment->ID, true );
		}

		// Delete product
		$product = wc_get_product( $product_id );
		$product->delete( true );
	}

	/**
	 * Bulk update or insert products
	 * Accepts an array with products in the formats supported by
	 * WC_API_Products->create_product() and WC_API_Products->edit_product()
	 *
	 * @since 2.4.0
	 *
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function bulk( $data ) {

		try {
			if ( ! isset( $data['products'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_products_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'products' ), 400 );
			}

			$data  = $data['products'];
			$limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'products' );

			// Limit bulk operation
			if ( count( $data ) > $limit ) {
				throw new WC_API_Exception( 'woocommerce_api_products_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 );
			}

			$products = array();

			foreach ( $data as $_product ) {
				$product_id  = 0;
				$product_sku = '';

				// Try to get the product ID
				if ( isset( $_product['id'] ) ) {
					$product_id = intval( $_product['id'] );
				}

				if ( ! $product_id && isset( $_product['sku'] ) ) {
					$product_sku = wc_clean( $_product['sku'] );
					$product_id  = wc_get_product_id_by_sku( $product_sku );
				}

				if ( $product_id ) {

					// Product exists / edit product
					$edit = $this->edit_product( $product_id, array( 'product' => $_product ) );

					if ( is_wp_error( $edit ) ) {
						$products[] = array(
							'id'    => $product_id,
							'sku'   => $product_sku,
							'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ),
						);
					} else {
						$products[] = $edit['product'];
					}
				} else {

					// Product don't exists / create product
					$new = $this->create_product( array( 'product' => $_product ) );

					if ( is_wp_error( $new ) ) {
						$products[] = array(
							'id'    => $product_id,
							'sku'   => $product_sku,
							'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ),
						);
					} else {
						$products[] = $new['product'];
					}
				}
			}

			return array( 'products' => apply_filters( 'woocommerce_api_products_bulk_response', $products, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get a listing of product shipping classes.
	 *
	 * @since  2.5.0
	 * @param  string|null    $fields Fields to limit response to
	 * @return array|WP_Error         List of product shipping classes if succeed,
	 *                                otherwise WP_Error will be returned
	 */
	public function get_product_shipping_classes( $fields = null ) {
		try {
			// Permissions check
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_shipping_classes', __( 'You do not have permission to read product shipping classes', 'woocommerce' ), 401 );
			}

			$product_shipping_classes = array();

			$terms = get_terms( 'product_shipping_class', array( 'hide_empty' => false, 'fields' => 'ids' ) );

			foreach ( $terms as $term_id ) {
				$product_shipping_classes[] = current( $this->get_product_shipping_class( $term_id, $fields ) );
			}

			return array( 'product_shipping_classes' => apply_filters( 'woocommerce_api_product_shipping_classes_response', $product_shipping_classes, $terms, $fields, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the product shipping class for the given ID.
	 *
	 * @since  2.5.0
	 * @param  string         $id     Product shipping class term ID
	 * @param  string|null    $fields Fields to limit response to
	 * @return array|WP_Error         Product shipping class if succeed, otherwise
	 *                                WP_Error will be returned
	 */
	public function get_product_shipping_class( $id, $fields = null ) {
		try {
			$id = absint( $id );
			if ( ! $id ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_shipping_class_id', __( 'Invalid product shipping class ID', 'woocommerce' ), 400 );
			}

			// Permissions check
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_shipping_classes', __( 'You do not have permission to read product shipping classes', 'woocommerce' ), 401 );
			}

			$term = get_term( $id, 'product_shipping_class' );

			if ( is_wp_error( $term ) || is_null( $term ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_shipping_class_id', __( 'A product shipping class with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$term_id = intval( $term->term_id );

			$product_shipping_class = array(
				'id'          => $term_id,
				'name'        => $term->name,
				'slug'        => $term->slug,
				'parent'      => $term->parent,
				'description' => $term->description,
				'count'       => intval( $term->count ),
			);

			return array( 'product_shipping_class' => apply_filters( 'woocommerce_api_product_shipping_class_response', $product_shipping_class, $id, $fields, $term, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create a new product shipping class.
	 *
	 * @since  2.5.0
	 * @param  array          $data Posted data
	 * @return array|WP_Error       Product shipping class if succeed, otherwise
	 *                              WP_Error will be returned
	 */
	public function create_product_shipping_class( $data ) {
		global $wpdb;

		try {
			if ( ! isset( $data['product_shipping_class'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_shipping_class_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_shipping_class' ), 400 );
			}

			// Check permissions
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_shipping_class', __( 'You do not have permission to create product shipping classes', 'woocommerce' ), 401 );
			}

			$defaults = array(
				'name'        => '',
				'slug'        => '',
				'description' => '',
				'parent'      => 0,
			);

			$data = wp_parse_args( $data['product_shipping_class'], $defaults );
			$data = apply_filters( 'woocommerce_api_create_product_shipping_class_data', $data, $this );

			// Check parent.
			$data['parent'] = absint( $data['parent'] );
			if ( $data['parent'] ) {
				$parent = get_term_by( 'id', $data['parent'], 'product_shipping_class' );
				if ( ! $parent ) {
					throw new WC_API_Exception( 'woocommerce_api_invalid_product_shipping_class_parent', __( 'Product shipping class parent is invalid', 'woocommerce' ), 400 );
				}
			}

			$insert = wp_insert_term( $data['name'], 'product_shipping_class', $data );
			if ( is_wp_error( $insert ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_shipping_class', $insert->get_error_message(), 400 );
			}

			$id = $insert['term_id'];

			do_action( 'woocommerce_api_create_product_shipping_class', $id, $data );

			$this->server->send_status( 201 );

			return $this->get_product_shipping_class( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit a product shipping class.
	 *
	 * @since  2.5.0
	 * @param  int            $id   Product shipping class term ID
	 * @param  array          $data Posted data
	 * @return array|WP_Error       Product shipping class if succeed, otherwise
	 *                              WP_Error will be returned
	 */
	public function edit_product_shipping_class( $id, $data ) {
		global $wpdb;

		try {
			if ( ! isset( $data['product_shipping_class'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_shipping_class', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_shipping_class' ), 400 );
			}

			$id   = absint( $id );
			$data = $data['product_shipping_class'];

			// Check permissions
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_shipping_class', __( 'You do not have permission to edit product shipping classes', 'woocommerce' ), 401 );
			}

			$data           = apply_filters( 'woocommerce_api_edit_product_shipping_class_data', $data, $this );
			$shipping_class = $this->get_product_shipping_class( $id );

			if ( is_wp_error( $shipping_class ) ) {
				return $shipping_class;
			}

			$update = wp_update_term( $id, 'product_shipping_class', $data );
			if ( is_wp_error( $update ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_shipping_class', __( 'Could not edit the shipping class', 'woocommerce' ), 400 );
			}

			do_action( 'woocommerce_api_edit_product_shipping_class', $id, $data );

			return $this->get_product_shipping_class( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete a product shipping class.
	 *
	 * @since  2.5.0
	 * @param  int            $id Product shipping class term ID
	 * @return array|WP_Error     Success message if succeed, otherwise WP_Error
	 *                            will be returned
	 */
	public function delete_product_shipping_class( $id ) {
		global $wpdb;

		try {
			// Check permissions
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_shipping_class', __( 'You do not have permission to delete product shipping classes', 'woocommerce' ), 401 );
			}

			$id      = absint( $id );
			$deleted = wp_delete_term( $id, 'product_shipping_class' );
			if ( ! $deleted || is_wp_error( $deleted ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_shipping_class', __( 'Could not delete the shipping class', 'woocommerce' ), 401 );
			}

			do_action( 'woocommerce_api_delete_product_shipping_class', $id, $this );

			return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_shipping_class' ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}
}
legacy/api/v3/class-wc-api-webhooks.php000064400000036334151336044770013747 0ustar00<?php
/**
 * WooCommerce API Webhooks class
 *
 * Handles requests to the /webhooks endpoint
 *
 * @author   WooThemes
 * @category API
 * @package  WooCommerce\RestApi
 * @since    2.2
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WC_API_Webhooks extends WC_API_Resource {

	/** @var string $base the route base */
	protected $base = '/webhooks';

	/**
	 * Register the routes for this class
	 *
	 * @since 2.2
	 * @param array $routes
	 * @return array
	 */
	public function register_routes( $routes ) {

		# GET|POST /webhooks
		$routes[ $this->base ] = array(
			array( array( $this, 'get_webhooks' ),     WC_API_Server::READABLE ),
			array( array( $this, 'create_webhook' ),   WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET /webhooks/count
		$routes[ $this->base . '/count' ] = array(
			array( array( $this, 'get_webhooks_count' ), WC_API_Server::READABLE ),
		);

		# GET|PUT|DELETE /webhooks/<id>
		$routes[ $this->base . '/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_webhook' ),  WC_API_Server::READABLE ),
			array( array( $this, 'edit_webhook' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
			array( array( $this, 'delete_webhook' ), WC_API_Server::DELETABLE ),
		);

		# GET /webhooks/<id>/deliveries
		$routes[ $this->base . '/(?P<webhook_id>\d+)/deliveries' ] = array(
			array( array( $this, 'get_webhook_deliveries' ), WC_API_Server::READABLE ),
		);

		# GET /webhooks/<webhook_id>/deliveries/<id>
		$routes[ $this->base . '/(?P<webhook_id>\d+)/deliveries/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_webhook_delivery' ), WC_API_Server::READABLE ),
		);

		return $routes;
	}

	/**
	 * Get all webhooks
	 *
	 * @since 2.2
	 *
	 * @param array $fields
	 * @param array $filter
	 * @param string $status
	 * @param int $page
	 *
	 * @return array
	 */
	public function get_webhooks( $fields = null, $filter = array(), $status = null, $page = 1 ) {

		if ( ! empty( $status ) ) {
			$filter['status'] = $status;
		}

		$filter['page'] = $page;

		$query = $this->query_webhooks( $filter );

		$webhooks = array();

		foreach ( $query['results'] as $webhook_id ) {
			$webhooks[] = current( $this->get_webhook( $webhook_id, $fields ) );
		}

		$this->server->add_pagination_headers( $query['headers'] );

		return array( 'webhooks' => $webhooks );
	}

	/**
	 * Get the webhook for the given ID
	 *
	 * @since 2.2
	 * @param int $id webhook ID
	 * @param array $fields
	 * @return array|WP_Error
	 */
	public function get_webhook( $id, $fields = null ) {

		// ensure webhook ID is valid & user has permission to read
		$id = $this->validate_request( $id, 'shop_webhook', 'read' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$webhook = wc_get_webhook( $id );

		$webhook_data = array(
			'id'           => $webhook->get_id(),
			'name'         => $webhook->get_name(),
			'status'       => $webhook->get_status(),
			'topic'        => $webhook->get_topic(),
			'resource'     => $webhook->get_resource(),
			'event'        => $webhook->get_event(),
			'hooks'        => $webhook->get_hooks(),
			'delivery_url' => $webhook->get_delivery_url(),
			'created_at'   => $this->server->format_datetime( $webhook->get_date_created() ? $webhook->get_date_created()->getTimestamp() : 0, false, false ), // API gives UTC times.
			'updated_at'   => $this->server->format_datetime( $webhook->get_date_modified() ? $webhook->get_date_modified()->getTimestamp() : 0, false, false ), // API gives UTC times.
		);

		return array( 'webhook' => apply_filters( 'woocommerce_api_webhook_response', $webhook_data, $webhook, $fields, $this ) );
	}

	/**
	 * Get the total number of webhooks
	 *
	 * @since 2.2
	 *
	 * @param string $status
	 * @param array $filter
	 *
	 * @return array|WP_Error
	 */
	public function get_webhooks_count( $status = null, $filter = array() ) {
		try {
			if ( ! current_user_can( 'manage_woocommerce' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_webhooks_count', __( 'You do not have permission to read the webhooks count', 'woocommerce' ), 401 );
			}

			if ( ! empty( $status ) ) {
				$filter['status'] = $status;
			}

			$query = $this->query_webhooks( $filter );

			return array( 'count' => $query['headers']->total );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create an webhook
	 *
	 * @since 2.2
	 *
	 * @param array $data parsed webhook data
	 *
	 * @return array|WP_Error
	 */
	public function create_webhook( $data ) {

		try {
			if ( ! isset( $data['webhook'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_webhook_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'webhook' ), 400 );
			}

			$data = $data['webhook'];

			// permission check
			if ( ! current_user_can( 'manage_woocommerce' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_webhooks', __( 'You do not have permission to create webhooks.', 'woocommerce' ), 401 );
			}

			$data = apply_filters( 'woocommerce_api_create_webhook_data', $data, $this );

			// validate topic
			if ( empty( $data['topic'] ) || ! wc_is_webhook_valid_topic( strtolower( $data['topic'] ) ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_topic', __( 'Webhook topic is required and must be valid.', 'woocommerce' ), 400 );
			}

			// validate delivery URL
			if ( empty( $data['delivery_url'] ) || ! wc_is_valid_url( $data['delivery_url'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_url', __( 'Webhook delivery URL must be a valid URL starting with http:// or https://', 'woocommerce' ), 400 );
			}

			$webhook_data = apply_filters( 'woocommerce_new_webhook_data', array(
				'post_type'     => 'shop_webhook',
				'post_status'   => 'publish',
				'ping_status'   => 'closed',
				'post_author'   => get_current_user_id(),
				'post_password' => 'webhook_' . wp_generate_password(),
				'post_title'    => ! empty( $data['name'] ) ? $data['name'] : sprintf( __( 'Webhook created on %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Webhook created on date parsed by strftime', 'woocommerce' ) ) ),
			), $data, $this );

			$webhook = new WC_Webhook();

			$webhook->set_name( $webhook_data['post_title'] );
			$webhook->set_user_id( $webhook_data['post_author'] );
			$webhook->set_status( 'publish' === $webhook_data['post_status'] ? 'active' : 'disabled' );
			$webhook->set_topic( $data['topic'] );
			$webhook->set_delivery_url( $data['delivery_url'] );
			$webhook->set_secret( ! empty( $data['secret'] ) ? $data['secret'] : wp_generate_password( 50, true, true ) );
			$webhook->set_api_version( 'legacy_v3' );
			$webhook->save();

			$webhook->deliver_ping();

			// HTTP 201 Created
			$this->server->send_status( 201 );

			do_action( 'woocommerce_api_create_webhook', $webhook->get_id(), $this );

			return $this->get_webhook( $webhook->get_id() );

		} catch ( WC_API_Exception $e ) {

			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit a webhook
	 *
	 * @since 2.2
	 *
	 * @param int $id webhook ID
	 * @param array $data parsed webhook data
	 *
	 * @return array|WP_Error
	 */
	public function edit_webhook( $id, $data ) {

		try {
			if ( ! isset( $data['webhook'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_webhook_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'webhook' ), 400 );
			}

			$data = $data['webhook'];

			$id = $this->validate_request( $id, 'shop_webhook', 'edit' );

			if ( is_wp_error( $id ) ) {
				return $id;
			}

			$data = apply_filters( 'woocommerce_api_edit_webhook_data', $data, $id, $this );

			$webhook = wc_get_webhook( $id );

			// update topic
			if ( ! empty( $data['topic'] ) ) {

				if ( wc_is_webhook_valid_topic( strtolower( $data['topic'] ) ) ) {

					$webhook->set_topic( $data['topic'] );

				} else {
					throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_topic', __( 'Webhook topic must be valid.', 'woocommerce' ), 400 );
				}
			}

			// update delivery URL
			if ( ! empty( $data['delivery_url'] ) ) {
				if ( wc_is_valid_url( $data['delivery_url'] ) ) {

					$webhook->set_delivery_url( $data['delivery_url'] );

				} else {
					throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_url', __( 'Webhook delivery URL must be a valid URL starting with http:// or https://', 'woocommerce' ), 400 );
				}
			}

			// update secret
			if ( ! empty( $data['secret'] ) ) {
				$webhook->set_secret( $data['secret'] );
			}

			// update status
			if ( ! empty( $data['status'] ) ) {
				$webhook->set_status( $data['status'] );
			}

			// update name
			if ( ! empty( $data['name'] ) ) {
				$webhook->set_name( $data['name'] );
			}

			$webhook->save();

			do_action( 'woocommerce_api_edit_webhook', $webhook->get_id(), $this );

			return $this->get_webhook( $webhook->get_id() );

		} catch ( WC_API_Exception $e ) {

			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete a webhook
	 *
	 * @since 2.2
	 * @param int $id webhook ID
	 * @return array|WP_Error
	 */
	public function delete_webhook( $id ) {

		$id = $this->validate_request( $id, 'shop_webhook', 'delete' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		do_action( 'woocommerce_api_delete_webhook', $id, $this );

		$webhook = wc_get_webhook( $id );

		return $webhook->delete( true );
	}

	/**
	 * Helper method to get webhook post objects
	 *
	 * @since 2.2
	 * @param array $args Request arguments for filtering query.
	 * @return array
	 */
	private function query_webhooks( $args ) {
		$args = $this->merge_query_args( array(), $args );

		$args['limit'] = isset( $args['posts_per_page'] ) ? intval( $args['posts_per_page'] ) : intval( get_option( 'posts_per_page' ) );

		if ( empty( $args['offset'] ) ) {
			$args['offset'] = 1 < $args['paged'] ? ( $args['paged'] - 1 ) * $args['limit'] : 0;
		}

		$page = $args['paged'];
		unset( $args['paged'], $args['posts_per_page'] );

		if ( isset( $args['s'] ) ) {
			$args['search'] = $args['s'];
			unset( $args['s'] );
		}

		// Post type to webhook status.
		if ( ! empty( $args['post_status'] ) ) {
			$args['status'] = $args['post_status'];
			unset( $args['post_status'] );
		}

		if ( ! empty( $args['post__in'] ) ) {
			$args['include'] = $args['post__in'];
			unset( $args['post__in'] );
		}

		if ( ! empty( $args['date_query'] ) ) {
			foreach ( $args['date_query'] as $date_query ) {
				if ( 'post_date_gmt' === $date_query['column'] ) {
					$args['after']  = isset( $date_query['after'] ) ? $date_query['after'] : null;
					$args['before'] = isset( $date_query['before'] ) ? $date_query['before'] : null;
				} elseif ( 'post_modified_gmt' === $date_query['column'] ) {
					$args['modified_after']  = isset( $date_query['after'] ) ? $date_query['after'] : null;
					$args['modified_before'] = isset( $date_query['before'] ) ? $date_query['before'] : null;
				}
			}

			unset( $args['date_query'] );
		}

		$args['paginate'] = true;

		// Get the webhooks.
		$data_store = WC_Data_Store::load( 'webhook' );
		$results    = $data_store->search_webhooks( $args );

		// Get total items.
		$headers              = new stdClass;
		$headers->page        = $page;
		$headers->total       = $results->total;
		$headers->is_single   = $args['limit'] > $headers->total;
		$headers->total_pages = $results->max_num_pages;

		return array(
			'results' => $results->webhooks,
			'headers' => $headers,
		);
	}

	/**
	 * Get deliveries for a webhook
	 *
	 * @since 2.2
	 * @deprecated 3.3.0 Webhooks deliveries logs now uses logging system.
	 * @param string $webhook_id webhook ID
	 * @param string|null $fields fields to include in response
	 * @return array|WP_Error
	 */
	public function get_webhook_deliveries( $webhook_id, $fields = null ) {

		// Ensure ID is valid webhook ID
		$webhook_id = $this->validate_request( $webhook_id, 'shop_webhook', 'read' );

		if ( is_wp_error( $webhook_id ) ) {
			return $webhook_id;
		}

		return array( 'webhook_deliveries' => array() );
	}

	/**
	 * Get the delivery log for the given webhook ID and delivery ID
	 *
	 * @since 2.2
	 * @deprecated 3.3.0 Webhooks deliveries logs now uses logging system.
	 * @param string $webhook_id webhook ID
	 * @param string $id delivery log ID
	 * @param string|null $fields fields to limit response to
	 *
	 * @return array|WP_Error
	 */
	public function get_webhook_delivery( $webhook_id, $id, $fields = null ) {
		try {
			// Validate webhook ID
			$webhook_id = $this->validate_request( $webhook_id, 'shop_webhook', 'read' );

			if ( is_wp_error( $webhook_id ) ) {
				return $webhook_id;
			}

			$id = absint( $id );

			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery ID.', 'woocommerce' ), 404 );
			}

			$webhook = new WC_Webhook( $webhook_id );

			$log = 0;

			if ( ! $log ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery.', 'woocommerce' ), 400 );
			}

			return array( 'webhook_delivery' => apply_filters( 'woocommerce_api_webhook_delivery_response', array(), $id, $fields, $log, $webhook_id, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Validate the request by checking:
	 *
	 * 1) the ID is a valid integer.
	 * 2) the ID returns a valid post object and matches the provided post type.
	 * 3) the current user has the proper permissions to read/edit/delete the post.
	 *
	 * @since 3.3.0
	 * @param string|int $id The post ID
	 * @param string $type The post type, either `shop_order`, `shop_coupon`, or `product`.
	 * @param string $context The context of the request, either `read`, `edit` or `delete`.
	 * @return int|WP_Error Valid post ID or WP_Error if any of the checks fails.
	 */
	protected function validate_request( $id, $type, $context ) {
		$id = absint( $id );

		// Validate ID.
		if ( empty( $id ) ) {
			return new WP_Error( "woocommerce_api_invalid_webhook_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) );
		}

		$webhook = wc_get_webhook( $id );

		if ( null === $webhook ) {
			return new WP_Error( "woocommerce_api_no_webhook_found", sprintf( __( 'No %1$s found with the ID equal to %2$s', 'woocommerce' ), 'webhook', $id ), array( 'status' => 404 ) );
		}

		// Validate permissions.
		switch ( $context ) {

			case 'read':
				if ( ! current_user_can( 'manage_woocommerce' ) ) {
					return new WP_Error( "woocommerce_api_user_cannot_read_webhook", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) );
				}
				break;

			case 'edit':
				if ( ! current_user_can( 'manage_woocommerce' ) ) {
					return new WP_Error( "woocommerce_api_user_cannot_edit_webhook", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) );
				}
				break;

			case 'delete':
				if ( ! current_user_can( 'manage_woocommerce' ) ) {
					return new WP_Error( "woocommerce_api_user_cannot_delete_webhook", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) );
				}
				break;
		}

		return $id;
	}
}
legacy/api/v3/class-wc-api-json-handler.php000064400000003656151336044770014513 0ustar00<?php
/**
 * WooCommerce API
 *
 * Handles parsing JSON request bodies and generating JSON responses
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WC_API_JSON_Handler implements WC_API_Handler {

	/**
	 * Get the content type for the response
	 *
	 * @since 2.1
	 * @return string
	 */
	public function get_content_type() {

		return sprintf( '%s; charset=%s', isset( $_GET['_jsonp'] ) ? 'application/javascript' : 'application/json', get_option( 'blog_charset' ) );
	}

	/**
	 * Parse the raw request body entity
	 *
	 * @since 2.1
	 * @param string $body the raw request body
	 * @return array|mixed
	 */
	public function parse_body( $body ) {

		return json_decode( $body, true );
	}

	/**
	 * Generate a JSON response given an array of data
	 *
	 * @since 2.1
	 * @param array $data the response data
	 * @return string
	 */
	public function generate_response( $data ) {
		if ( isset( $_GET['_jsonp'] ) ) {

			if ( ! apply_filters( 'woocommerce_api_jsonp_enabled', true ) ) {
				WC()->api->server->send_status( 400 );
				return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_disabled', 'message' => __( 'JSONP support is disabled on this site', 'woocommerce' ) ) ) );
			}

			$jsonp_callback = $_GET['_jsonp'];

			if ( ! wp_check_jsonp_callback( $jsonp_callback ) ) {
				WC()->api->server->send_status( 400 );
				return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_callback_invalid', __( 'The JSONP callback function is invalid', 'woocommerce' ) ) ) );
			}

			WC()->api->server->header( 'X-Content-Type-Options', 'nosniff' );

			// Prepend '/**/' to mitigate possible JSONP Flash attacks.
			// https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
			return '/**/' . $jsonp_callback . '(' . wp_json_encode( $data ) . ')';
		}

		return wp_json_encode( $data );
	}
}
legacy/api/v3/interface-wc-api-handler.php000064400000001515151336044770014367 0ustar00<?php
/**
 * WooCommerce API
 *
 * Defines an interface that API request/response handlers should implement
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

interface WC_API_Handler {

	/**
	 * Get the content type for the response
	 *
	 * This should return the proper HTTP content-type for the response
	 *
	 * @since 2.1
	 * @return string
	 */
	public function get_content_type();

	/**
	 * Parse the raw request body entity into an array
	 *
	 * @since 2.1
	 * @param string $data
	 * @return array
	 */
	public function parse_body( $data );

	/**
	 * Generate a response from an array of data
	 *
	 * @since 2.1
	 * @param array $data
	 * @return string
	 */
	public function generate_response( $data );

}
legacy/api/v3/class-wc-api-server.php000064400000051035151336044770013427 0ustar00<?php
/**
 * WooCommerce API
 *
 * Handles REST API requests
 *
 * This class and related code (JSON response handler, resource classes) are based on WP-API v0.6 (https://github.com/WP-API/WP-API)
 * Many thanks to Ryan McCue and any other contributors!
 *
 * @author   WooThemes
 * @category API
 * @package  WooCommerce\RestApi
 * @since    2.1
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

require_once ABSPATH . 'wp-admin/includes/admin.php';

class WC_API_Server {

	const METHOD_GET    = 1;
	const METHOD_POST   = 2;
	const METHOD_PUT    = 4;
	const METHOD_PATCH  = 8;
	const METHOD_DELETE = 16;

	const READABLE   = 1;  // GET
	const CREATABLE  = 2;  // POST
	const EDITABLE   = 14; // POST | PUT | PATCH
	const DELETABLE  = 16; // DELETE
	const ALLMETHODS = 31; // GET | POST | PUT | PATCH | DELETE

	/**
	 * Does the endpoint accept a raw request body?
	 */
	const ACCEPT_RAW_DATA = 64;

	/** Does the endpoint accept a request body? (either JSON or XML) */
	const ACCEPT_DATA = 128;

	/**
	 * Should we hide this endpoint from the index?
	 */
	const HIDDEN_ENDPOINT = 256;

	/**
	 * Map of HTTP verbs to constants
	 * @var array
	 */
	public static $method_map = array(
		'HEAD'   => self::METHOD_GET,
		'GET'    => self::METHOD_GET,
		'POST'   => self::METHOD_POST,
		'PUT'    => self::METHOD_PUT,
		'PATCH'  => self::METHOD_PATCH,
		'DELETE' => self::METHOD_DELETE,
	);

	/**
	 * Requested path (relative to the API root, wp-json.php)
	 *
	 * @var string
	 */
	public $path = '';

	/**
	 * Requested method (GET/HEAD/POST/PUT/PATCH/DELETE)
	 *
	 * @var string
	 */
	public $method = 'HEAD';

	/**
	 * Request parameters
	 *
	 * This acts as an abstraction of the superglobals
	 * (GET => $_GET, POST => $_POST)
	 *
	 * @var array
	 */
	public $params = array( 'GET' => array(), 'POST' => array() );

	/**
	 * Request headers
	 *
	 * @var array
	 */
	public $headers = array();

	/**
	 * Request files (matches $_FILES)
	 *
	 * @var array
	 */
	public $files = array();

	/**
	 * Request/Response handler, either JSON by default
	 * or XML if requested by client
	 *
	 * @var WC_API_Handler
	 */
	public $handler;


	/**
	 * Setup class and set request/response handler
	 *
	 * @since 2.1
	 * @param $path
	 */
	public function __construct( $path ) {

		if ( empty( $path ) ) {
			if ( isset( $_SERVER['PATH_INFO'] ) ) {
				$path = $_SERVER['PATH_INFO'];
			} else {
				$path = '/';
			}
		}

		$this->path           = $path;
		$this->method         = $_SERVER['REQUEST_METHOD'];
		$this->params['GET']  = $_GET;
		$this->params['POST'] = $_POST;
		$this->headers        = $this->get_headers( $_SERVER );
		$this->files          = $_FILES;

		// Compatibility for clients that can't use PUT/PATCH/DELETE
		if ( isset( $_GET['_method'] ) ) {
			$this->method = strtoupper( $_GET['_method'] );
		} elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) {
			$this->method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'];
		}

		// load response handler
		$handler_class = apply_filters( 'woocommerce_api_default_response_handler', 'WC_API_JSON_Handler', $this->path, $this );

		$this->handler = new $handler_class();
	}

	/**
	 * Check authentication for the request
	 *
	 * @since 2.1
	 * @return WP_User|WP_Error WP_User object indicates successful login, WP_Error indicates unsuccessful login
	 */
	public function check_authentication() {

		// allow plugins to remove default authentication or add their own authentication
		$user = apply_filters( 'woocommerce_api_check_authentication', null, $this );

		if ( is_a( $user, 'WP_User' ) ) {

			// API requests run under the context of the authenticated user
			wp_set_current_user( $user->ID );

		} elseif ( ! is_wp_error( $user ) ) {

			// WP_Errors are handled in serve_request()
			$user = new WP_Error( 'woocommerce_api_authentication_error', __( 'Invalid authentication method', 'woocommerce' ), array( 'code' => 500 ) );

		}

		return $user;
	}

	/**
	 * Convert an error to an array
	 *
	 * This iterates over all error codes and messages to change it into a flat
	 * array. This enables simpler client behaviour, as it is represented as a
	 * list in JSON rather than an object/map
	 *
	 * @since 2.1
	 * @param WP_Error $error
	 * @return array List of associative arrays with code and message keys
	 */
	protected function error_to_array( $error ) {
		$errors = array();
		foreach ( (array) $error->errors as $code => $messages ) {
			foreach ( (array) $messages as $message ) {
				$errors[] = array( 'code' => $code, 'message' => $message );
			}
		}

		return array( 'errors' => $errors );
	}

	/**
	 * Handle serving an API request
	 *
	 * Matches the current server URI to a route and runs the first matching
	 * callback then outputs a JSON representation of the returned value.
	 *
	 * @since 2.1
	 * @uses WC_API_Server::dispatch()
	 */
	public function serve_request() {

		do_action( 'woocommerce_api_server_before_serve', $this );

		$this->header( 'Content-Type', $this->handler->get_content_type(), true );

		// the API is enabled by default
		if ( ! apply_filters( 'woocommerce_api_enabled', true, $this ) || ( 'no' === get_option( 'woocommerce_api_enabled' ) ) ) {

			$this->send_status( 404 );

			echo $this->handler->generate_response( array( 'errors' => array( 'code' => 'woocommerce_api_disabled', 'message' => 'The WooCommerce API is disabled on this site' ) ) );

			return;
		}

		$result = $this->check_authentication();

		// if authorization check was successful, dispatch the request
		if ( ! is_wp_error( $result ) ) {
			$result = $this->dispatch();
		}

		// handle any dispatch errors
		if ( is_wp_error( $result ) ) {
			$data = $result->get_error_data();
			if ( is_array( $data ) && isset( $data['status'] ) ) {
				$this->send_status( $data['status'] );
			}

			$result = $this->error_to_array( $result );
		}

		// This is a filter rather than an action, since this is designed to be
		// re-entrant if needed
		$served = apply_filters( 'woocommerce_api_serve_request', false, $result, $this );

		if ( ! $served ) {

			if ( 'HEAD' === $this->method ) {
				return;
			}

			echo $this->handler->generate_response( $result );
		}
	}

	/**
	 * Retrieve the route map
	 *
	 * The route map is an associative array with path regexes as the keys. The
	 * value is an indexed array with the callback function/method as the first
	 * item, and a bitmask of HTTP methods as the second item (see the class
	 * constants).
	 *
	 * Each route can be mapped to more than one callback by using an array of
	 * the indexed arrays. This allows mapping e.g. GET requests to one callback
	 * and POST requests to another.
	 *
	 * Note that the path regexes (array keys) must have @ escaped, as this is
	 * used as the delimiter with preg_match()
	 *
	 * @since 2.1
	 * @return array `'/path/regex' => array( $callback, $bitmask )` or `'/path/regex' => array( array( $callback, $bitmask ), ...)`
	 */
	public function get_routes() {

		// index added by default
		$endpoints = array(

			'/' => array( array( $this, 'get_index' ), self::READABLE ),
		);

		$endpoints = apply_filters( 'woocommerce_api_endpoints', $endpoints );

		// Normalise the endpoints
		foreach ( $endpoints as $route => &$handlers ) {
			if ( count( $handlers ) <= 2 && isset( $handlers[1] ) && ! is_array( $handlers[1] ) ) {
				$handlers = array( $handlers );
			}
		}

		return $endpoints;
	}

	/**
	 * Match the request to a callback and call it
	 *
	 * @since 2.1
	 * @return mixed The value returned by the callback, or a WP_Error instance
	 */
	public function dispatch() {

		switch ( $this->method ) {

			case 'HEAD' :
			case 'GET' :
				$method = self::METHOD_GET;
				break;

			case 'POST' :
				$method = self::METHOD_POST;
				break;

			case 'PUT' :
				$method = self::METHOD_PUT;
				break;

			case 'PATCH' :
				$method = self::METHOD_PATCH;
				break;

			case 'DELETE' :
				$method = self::METHOD_DELETE;
				break;

			default :
				return new WP_Error( 'woocommerce_api_unsupported_method', __( 'Unsupported request method', 'woocommerce' ), array( 'status' => 400 ) );
		}

		foreach ( $this->get_routes() as $route => $handlers ) {
			foreach ( $handlers as $handler ) {
				$callback  = $handler[0];
				$supported = isset( $handler[1] ) ? $handler[1] : self::METHOD_GET;

				if ( ! ( $supported & $method ) ) {
					continue;
				}

				$match = preg_match( '@^' . $route . '$@i', urldecode( $this->path ), $args );

				if ( ! $match ) {
					continue;
				}

				if ( ! is_callable( $callback ) ) {
					return new WP_Error( 'woocommerce_api_invalid_handler', __( 'The handler for the route is invalid', 'woocommerce' ), array( 'status' => 500 ) );
				}

				$args = array_merge( $args, $this->params['GET'] );
				if ( $method & self::METHOD_POST ) {
					$args = array_merge( $args, $this->params['POST'] );
				}
				if ( $supported & self::ACCEPT_DATA ) {
					$data = $this->handler->parse_body( $this->get_raw_data() );
					$args = array_merge( $args, array( 'data' => $data ) );
				} elseif ( $supported & self::ACCEPT_RAW_DATA ) {
					$data = $this->get_raw_data();
					$args = array_merge( $args, array( 'data' => $data ) );
				}

				$args['_method']  = $method;
				$args['_route']   = $route;
				$args['_path']    = $this->path;
				$args['_headers'] = $this->headers;
				$args['_files']   = $this->files;

				$args = apply_filters( 'woocommerce_api_dispatch_args', $args, $callback );

				// Allow plugins to halt the request via this filter
				if ( is_wp_error( $args ) ) {
					return $args;
				}

				$params = $this->sort_callback_params( $callback, $args );
				if ( is_wp_error( $params ) ) {
					return $params;
				}

				return call_user_func_array( $callback, $params );
			}
		}

		return new WP_Error( 'woocommerce_api_no_route', __( 'No route was found matching the URL and request method', 'woocommerce' ), array( 'status' => 404 ) );
	}

	/**
	 * urldecode deep.
	 *
	 * @since  2.2
	 * @param  string|array $value Data to decode with urldecode.
	 *
	 * @return string|array        Decoded data.
	 */
	protected function urldecode_deep( $value ) {
		if ( is_array( $value ) ) {
			return array_map( array( $this, 'urldecode_deep' ), $value );
		} else {
			return urldecode( $value );
		}
	}

	/**
	 * Sort parameters by order specified in method declaration
	 *
	 * Takes a callback and a list of available params, then filters and sorts
	 * by the parameters the method actually needs, using the Reflection API
	 *
	 * @since 2.2
	 *
	 * @param callable|array $callback the endpoint callback
	 * @param array $provided the provided request parameters
	 *
	 * @return array|WP_Error
	 */
	protected function sort_callback_params( $callback, $provided ) {
		if ( is_array( $callback ) ) {
			$ref_func = new ReflectionMethod( $callback[0], $callback[1] );
		} else {
			$ref_func = new ReflectionFunction( $callback );
		}

		$wanted = $ref_func->getParameters();
		$ordered_parameters = array();

		foreach ( $wanted as $param ) {
			if ( isset( $provided[ $param->getName() ] ) ) {
				// We have this parameters in the list to choose from
				if ( 'data' == $param->getName() ) {
					$ordered_parameters[] = $provided[ $param->getName() ];
					continue;
				}

				$ordered_parameters[] = $this->urldecode_deep( $provided[ $param->getName() ] );
			} elseif ( $param->isDefaultValueAvailable() ) {
				// We don't have this parameter, but it's optional
				$ordered_parameters[] = $param->getDefaultValue();
			} else {
				// We don't have this parameter and it wasn't optional, abort!
				return new WP_Error( 'woocommerce_api_missing_callback_param', sprintf( __( 'Missing parameter %s', 'woocommerce' ), $param->getName() ), array( 'status' => 400 ) );
			}
		}

		return $ordered_parameters;
	}

	/**
	 * Get the site index.
	 *
	 * This endpoint describes the capabilities of the site.
	 *
	 * @since 2.3
	 * @return array Index entity
	 */
	public function get_index() {

		// General site data
		$available = array(
			'store' => array(
				'name'        => get_option( 'blogname' ),
				'description' => get_option( 'blogdescription' ),
				'URL'         => get_option( 'siteurl' ),
				'wc_version'  => WC()->version,
				'version'     => WC_API::VERSION,
				'routes'      => array(),
				'meta'        => array(
					'timezone'           => wc_timezone_string(),
					'currency'           => get_woocommerce_currency(),
					'currency_format'    => get_woocommerce_currency_symbol(),
					'currency_position'  => get_option( 'woocommerce_currency_pos' ),
					'thousand_separator' => get_option( 'woocommerce_price_thousand_sep' ),
					'decimal_separator'  => get_option( 'woocommerce_price_decimal_sep' ),
					'price_num_decimals' => wc_get_price_decimals(),
					'tax_included'       => wc_prices_include_tax(),
					'weight_unit'        => get_option( 'woocommerce_weight_unit' ),
					'dimension_unit'     => get_option( 'woocommerce_dimension_unit' ),
					'ssl_enabled'        => ( 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) || wc_site_is_https() ),
					'permalinks_enabled' => ( '' !== get_option( 'permalink_structure' ) ),
					'generate_password'  => ( 'yes' === get_option( 'woocommerce_registration_generate_password' ) ),
					'links'              => array(
						'help' => 'https://woocommerce.github.io/woocommerce-rest-api-docs/',
					),
				),
			),
		);

		// Find the available routes
		foreach ( $this->get_routes() as $route => $callbacks ) {
			$data = array();

			$route = preg_replace( '#\(\?P(<\w+?>).*?\)#', '$1', $route );

			foreach ( self::$method_map as $name => $bitmask ) {
				foreach ( $callbacks as $callback ) {
					// Skip to the next route if any callback is hidden
					if ( $callback[1] & self::HIDDEN_ENDPOINT ) {
						continue 3;
					}

					if ( $callback[1] & $bitmask ) {
						$data['supports'][] = $name;
					}

					if ( $callback[1] & self::ACCEPT_DATA ) {
						$data['accepts_data'] = true;
					}

					// For non-variable routes, generate links
					if ( strpos( $route, '<' ) === false ) {
						$data['meta'] = array(
							'self' => get_woocommerce_api_url( $route ),
						);
					}
				}
			}

			$available['store']['routes'][ $route ] = apply_filters( 'woocommerce_api_endpoints_description', $data );
		}

		return apply_filters( 'woocommerce_api_index', $available );
	}

	/**
	 * Send a HTTP status code
	 *
	 * @since 2.1
	 * @param int $code HTTP status
	 */
	public function send_status( $code ) {
		status_header( $code );
	}

	/**
	 * Send a HTTP header
	 *
	 * @since 2.1
	 * @param string $key Header key
	 * @param string $value Header value
	 * @param boolean $replace Should we replace the existing header?
	 */
	public function header( $key, $value, $replace = true ) {
		header( sprintf( '%s: %s', $key, $value ), $replace );
	}

	/**
	 * Send a Link header
	 *
	 * @internal The $rel parameter is first, as this looks nicer when sending multiple
	 *
	 * @link http://tools.ietf.org/html/rfc5988
	 * @link http://www.iana.org/assignments/link-relations/link-relations.xml
	 *
	 * @since 2.1
	 * @param string $rel Link relation. Either a registered type, or an absolute URL
	 * @param string $link Target IRI for the link
	 * @param array $other Other parameters to send, as an associative array
	 */
	public function link_header( $rel, $link, $other = array() ) {

		$header = sprintf( '<%s>; rel="%s"', $link, esc_attr( $rel ) );

		foreach ( $other as $key => $value ) {

			if ( 'title' == $key ) {

				$value = '"' . $value . '"';
			}

			$header .= '; ' . $key . '=' . $value;
		}

		$this->header( 'Link', $header, false );
	}

	/**
	 * Send pagination headers for resources
	 *
	 * @since 2.1
	 * @param WP_Query|WP_User_Query|stdClass $query
	 */
	public function add_pagination_headers( $query ) {

		// WP_User_Query
		if ( is_a( $query, 'WP_User_Query' ) ) {

			$single      = count( $query->get_results() ) == 1;
			$total       = $query->get_total();

			if ( $query->get( 'number' ) > 0 ) {
				$page = ( $query->get( 'offset' ) / $query->get( 'number' ) ) + 1;
				$total_pages = ceil( $total / $query->get( 'number' ) );
			} else {
				$page = 1;
				$total_pages = 1;
			}
		} elseif ( is_a( $query, 'stdClass' ) ) {
			$page        = $query->page;
			$single      = $query->is_single;
			$total       = $query->total;
			$total_pages = $query->total_pages;

		// WP_Query
		} else {

			$page        = $query->get( 'paged' );
			$single      = $query->is_single();
			$total       = $query->found_posts;
			$total_pages = $query->max_num_pages;
		}

		if ( ! $page ) {
			$page = 1;
		}

		$next_page = absint( $page ) + 1;

		if ( ! $single ) {

			// first/prev
			if ( $page > 1 ) {
				$this->link_header( 'first', $this->get_paginated_url( 1 ) );
				$this->link_header( 'prev', $this->get_paginated_url( $page -1 ) );
			}

			// next
			if ( $next_page <= $total_pages ) {
				$this->link_header( 'next', $this->get_paginated_url( $next_page ) );
			}

			// last
			if ( $page != $total_pages ) {
				$this->link_header( 'last', $this->get_paginated_url( $total_pages ) );
			}
		}

		$this->header( 'X-WC-Total', $total );
		$this->header( 'X-WC-TotalPages', $total_pages );

		do_action( 'woocommerce_api_pagination_headers', $this, $query );
	}

	/**
	 * Returns the request URL with the page query parameter set to the specified page
	 *
	 * @since 2.1
	 * @param int $page
	 * @return string
	 */
	private function get_paginated_url( $page ) {

		// remove existing page query param
		$request = remove_query_arg( 'page' );

		// add provided page query param
		$request = urldecode( add_query_arg( 'page', $page, $request ) );

		// get the home host
		$host = parse_url( get_home_url(), PHP_URL_HOST );

		return set_url_scheme( "http://{$host}{$request}" );
	}

	/**
	 * Retrieve the raw request entity (body)
	 *
	 * @since 2.1
	 * @return string
	 */
	public function get_raw_data() {
		// @codingStandardsIgnoreStart
		// $HTTP_RAW_POST_DATA is deprecated on PHP 5.6.
		if ( function_exists( 'phpversion' ) && version_compare( phpversion(), '5.6', '>=' ) ) {
			return file_get_contents( 'php://input' );
		}

		global $HTTP_RAW_POST_DATA;

		// A bug in PHP < 5.2.2 makes $HTTP_RAW_POST_DATA not set by default,
		// but we can do it ourself.
		if ( ! isset( $HTTP_RAW_POST_DATA ) ) {
			$HTTP_RAW_POST_DATA = file_get_contents( 'php://input' );
		}

		return $HTTP_RAW_POST_DATA;
		// @codingStandardsIgnoreEnd
	}

	/**
	 * Parse an RFC3339 datetime into a MySQl datetime
	 *
	 * Invalid dates default to unix epoch
	 *
	 * @since 2.1
	 * @param string $datetime RFC3339 datetime
	 * @return string MySQl datetime (YYYY-MM-DD HH:MM:SS)
	 */
	public function parse_datetime( $datetime ) {

		// Strip millisecond precision (a full stop followed by one or more digits)
		if ( strpos( $datetime, '.' ) !== false ) {
			$datetime = preg_replace( '/\.\d+/', '', $datetime );
		}

		// default timezone to UTC
		$datetime = preg_replace( '/[+-]\d+:+\d+$/', '+00:00', $datetime );

		try {

			$datetime = new DateTime( $datetime, new DateTimeZone( 'UTC' ) );

		} catch ( Exception $e ) {

			$datetime = new DateTime( '@0' );

		}

		return $datetime->format( 'Y-m-d H:i:s' );
	}

	/**
	 * Format a unix timestamp or MySQL datetime into an RFC3339 datetime
	 *
	 * @since 2.1
	 * @param int|string $timestamp unix timestamp or MySQL datetime
	 * @param bool $convert_to_utc
	 * @param bool $convert_to_gmt Use GMT timezone.
	 * @return string RFC3339 datetime
	 */
	public function format_datetime( $timestamp, $convert_to_utc = false, $convert_to_gmt = false ) {
		if ( $convert_to_gmt ) {
			if ( is_numeric( $timestamp ) ) {
				$timestamp = date( 'Y-m-d H:i:s', $timestamp );
			}

			$timestamp = get_gmt_from_date( $timestamp );
		}

		if ( $convert_to_utc ) {
			$timezone = new DateTimeZone( wc_timezone_string() );
		} else {
			$timezone = new DateTimeZone( 'UTC' );
		}

		try {

			if ( is_numeric( $timestamp ) ) {
				$date = new DateTime( "@{$timestamp}" );
			} else {
				$date = new DateTime( $timestamp, $timezone );
			}

			// convert to UTC by adjusting the time based on the offset of the site's timezone
			if ( $convert_to_utc ) {
				$date->modify( -1 * $date->getOffset() . ' seconds' );
			}
		} catch ( Exception $e ) {

			$date = new DateTime( '@0' );
		}

		return $date->format( 'Y-m-d\TH:i:s\Z' );
	}

	/**
	 * Extract headers from a PHP-style $_SERVER array
	 *
	 * @since 2.1
	 * @param array $server Associative array similar to $_SERVER
	 * @return array Headers extracted from the input
	 */
	public function get_headers( $server ) {
		$headers = array();
		// CONTENT_* headers are not prefixed with HTTP_
		$additional = array( 'CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true );

		foreach ( $server as $key => $value ) {
			if ( strpos( $key, 'HTTP_' ) === 0 ) {
				$headers[ substr( $key, 5 ) ] = $value;
			} elseif ( isset( $additional[ $key ] ) ) {
				$headers[ $key ] = $value;
			}
		}

		return $headers;
	}
}
legacy/api/v3/class-wc-api-coupons.php000064400000050102151336044770013601 0ustar00<?php
/**
 * WooCommerce API Coupons Class
 *
 * Handles requests to the /coupons endpoint
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WC_API_Coupons extends WC_API_Resource {

	/** @var string $base the route base */
	protected $base = '/coupons';

	/**
	 * Register the routes for this class
	 *
	 * GET /coupons
	 * GET /coupons/count
	 * GET /coupons/<id>
	 *
	 * @since 2.1
	 * @param array $routes
	 * @return array
	 */
	public function register_routes( $routes ) {

		# GET/POST /coupons
		$routes[ $this->base ] = array(
			array( array( $this, 'get_coupons' ),     WC_API_Server::READABLE ),
			array( array( $this, 'create_coupon' ),   WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET /coupons/count
		$routes[ $this->base . '/count' ] = array(
			array( array( $this, 'get_coupons_count' ), WC_API_Server::READABLE ),
		);

		# GET/PUT/DELETE /coupons/<id>
		$routes[ $this->base . '/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_coupon' ),    WC_API_Server::READABLE ),
			array( array( $this, 'edit_coupon' ),   WC_API_SERVER::EDITABLE | WC_API_SERVER::ACCEPT_DATA ),
			array( array( $this, 'delete_coupon' ), WC_API_SERVER::DELETABLE ),
		);

		# GET /coupons/code/<code>, note that coupon codes can contain spaces, dashes and underscores
		$routes[ $this->base . '/code/(?P<code>\w[\w\s\-]*)' ] = array(
			array( array( $this, 'get_coupon_by_code' ), WC_API_Server::READABLE ),
		);

		# POST|PUT /coupons/bulk
		$routes[ $this->base . '/bulk' ] = array(
			array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
		);

		return $routes;
	}

	/**
	 * Get all coupons
	 *
	 * @since 2.1
	 * @param string $fields
	 * @param array $filter
	 * @param int $page
	 * @return array
	 */
	public function get_coupons( $fields = null, $filter = array(), $page = 1 ) {

		$filter['page'] = $page;

		$query = $this->query_coupons( $filter );

		$coupons = array();

		foreach ( $query->posts as $coupon_id ) {

			if ( ! $this->is_readable( $coupon_id ) ) {
				continue;
			}

			$coupons[] = current( $this->get_coupon( $coupon_id, $fields ) );
		}

		$this->server->add_pagination_headers( $query );

		return array( 'coupons' => $coupons );
	}

	/**
	 * Get the coupon for the given ID
	 *
	 * @since 2.1
	 * @param int $id the coupon ID
	 * @param string $fields fields to include in response
	 * @return array|WP_Error
	 */
	public function get_coupon( $id, $fields = null ) {
		try {

			$id = $this->validate_request( $id, 'shop_coupon', 'read' );

			if ( is_wp_error( $id ) ) {
				return $id;
			}

			$coupon = new WC_Coupon( $id );

			if ( 0 === $coupon->get_id() ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_id', __( 'Invalid coupon ID', 'woocommerce' ), 404 );
			}

			$coupon_data = array(
				'id'                           => $coupon->get_id(),
				'code'                         => $coupon->get_code(),
				'type'                         => $coupon->get_discount_type(),
				'created_at'                   => $this->server->format_datetime( $coupon->get_date_created() ? $coupon->get_date_created()->getTimestamp() : 0 ), // API gives UTC times.
				'updated_at'                   => $this->server->format_datetime( $coupon->get_date_modified() ? $coupon->get_date_modified()->getTimestamp() : 0 ), // API gives UTC times.
				'amount'                       => wc_format_decimal( $coupon->get_amount(), 2 ),
				'individual_use'               => $coupon->get_individual_use(),
				'product_ids'                  => array_map( 'absint', (array) $coupon->get_product_ids() ),
				'exclude_product_ids'          => array_map( 'absint', (array) $coupon->get_excluded_product_ids() ),
				'usage_limit'                  => $coupon->get_usage_limit() ? $coupon->get_usage_limit() : null,
				'usage_limit_per_user'         => $coupon->get_usage_limit_per_user() ? $coupon->get_usage_limit_per_user() : null,
				'limit_usage_to_x_items'       => (int) $coupon->get_limit_usage_to_x_items(),
				'usage_count'                  => (int) $coupon->get_usage_count(),
				'expiry_date'                  => $coupon->get_date_expires() ? $this->server->format_datetime( $coupon->get_date_expires()->getTimestamp() ) : null, // API gives UTC times.
				'enable_free_shipping'         => $coupon->get_free_shipping(),
				'product_category_ids'         => array_map( 'absint', (array) $coupon->get_product_categories() ),
				'exclude_product_category_ids' => array_map( 'absint', (array) $coupon->get_excluded_product_categories() ),
				'exclude_sale_items'           => $coupon->get_exclude_sale_items(),
				'minimum_amount'               => wc_format_decimal( $coupon->get_minimum_amount(), 2 ),
				'maximum_amount'               => wc_format_decimal( $coupon->get_maximum_amount(), 2 ),
				'customer_emails'              => $coupon->get_email_restrictions(),
				'description'                  => $coupon->get_description(),
			);

			return array( 'coupon' => apply_filters( 'woocommerce_api_coupon_response', $coupon_data, $coupon, $fields, $this->server ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the total number of coupons
	 *
	 * @since 2.1
	 * @param array $filter
	 * @return array|WP_Error
	 */
	public function get_coupons_count( $filter = array() ) {
		try {
			if ( ! current_user_can( 'read_private_shop_coupons' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_coupons_count', __( 'You do not have permission to read the coupons count', 'woocommerce' ), 401 );
			}

			$query = $this->query_coupons( $filter );

			return array( 'count' => (int) $query->found_posts );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the coupon for the given code
	 *
	 * @since 2.1
	 * @param string $code the coupon code
	 * @param string $fields fields to include in response
	 * @return int|WP_Error
	 */
	public function get_coupon_by_code( $code, $fields = null ) {
		global $wpdb;

		try {
			$id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish' ORDER BY post_date DESC LIMIT 1;", $code ) );

			if ( is_null( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_code', __( 'Invalid coupon code', 'woocommerce' ), 404 );
			}

			return $this->get_coupon( $id, $fields );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create a coupon
	 *
	 * @since 2.2
	 *
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function create_coupon( $data ) {
		global $wpdb;

		try {
			if ( ! isset( $data['coupon'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_coupon_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'coupon' ), 400 );
			}

			$data = $data['coupon'];

			// Check user permission
			if ( ! current_user_can( 'publish_shop_coupons' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_coupon', __( 'You do not have permission to create coupons', 'woocommerce' ), 401 );
			}

			$data = apply_filters( 'woocommerce_api_create_coupon_data', $data, $this );

			// Check if coupon code is specified
			if ( ! isset( $data['code'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_coupon_code', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'code' ), 400 );
			}

			$coupon_code  = wc_format_coupon_code( $data['code'] );
			$id_from_code = wc_get_coupon_id_by_code( $coupon_code );

			if ( $id_from_code ) {
				throw new WC_API_Exception( 'woocommerce_api_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), 400 );
			}

			$defaults = array(
				'type'                         => 'fixed_cart',
				'amount'                       => 0,
				'individual_use'               => false,
				'product_ids'                  => array(),
				'exclude_product_ids'          => array(),
				'usage_limit'                  => '',
				'usage_limit_per_user'         => '',
				'limit_usage_to_x_items'       => '',
				'usage_count'                  => '',
				'expiry_date'                  => '',
				'enable_free_shipping'         => false,
				'product_category_ids'         => array(),
				'exclude_product_category_ids' => array(),
				'exclude_sale_items'           => false,
				'minimum_amount'               => '',
				'maximum_amount'               => '',
				'customer_emails'              => array(),
				'description'                  => '',
			);

			$coupon_data = wp_parse_args( $data, $defaults );

			// Validate coupon types
			if ( ! in_array( wc_clean( $coupon_data['type'] ), array_keys( wc_get_coupon_types() ) ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_type', sprintf( __( 'Invalid coupon type - the coupon type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_coupon_types() ) ) ), 400 );
			}

			$new_coupon = array(
				'post_title'   => $coupon_code,
				'post_content' => '',
				'post_status'  => 'publish',
				'post_author'  => get_current_user_id(),
				'post_type'    => 'shop_coupon',
				'post_excerpt' => $coupon_data['description'],
	 		);

			$id = wp_insert_post( $new_coupon, true );

			if ( is_wp_error( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_coupon', $id->get_error_message(), 400 );
			}

			// Set coupon meta
			update_post_meta( $id, 'discount_type', $coupon_data['type'] );
			update_post_meta( $id, 'coupon_amount', wc_format_decimal( $coupon_data['amount'] ) );
			update_post_meta( $id, 'individual_use', ( true === $coupon_data['individual_use'] ) ? 'yes' : 'no' );
			update_post_meta( $id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $coupon_data['product_ids'] ) ) ) );
			update_post_meta( $id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $coupon_data['exclude_product_ids'] ) ) ) );
			update_post_meta( $id, 'usage_limit', absint( $coupon_data['usage_limit'] ) );
			update_post_meta( $id, 'usage_limit_per_user', absint( $coupon_data['usage_limit_per_user'] ) );
			update_post_meta( $id, 'limit_usage_to_x_items', absint( $coupon_data['limit_usage_to_x_items'] ) );
			update_post_meta( $id, 'usage_count', absint( $coupon_data['usage_count'] ) );
			update_post_meta( $id, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $coupon_data['expiry_date'] ) ) );
			update_post_meta( $id, 'date_expires', $this->get_coupon_expiry_date( wc_clean( $coupon_data['expiry_date'] ), true ) );
			update_post_meta( $id, 'free_shipping', ( true === $coupon_data['enable_free_shipping'] ) ? 'yes' : 'no' );
			update_post_meta( $id, 'product_categories', array_filter( array_map( 'intval', $coupon_data['product_category_ids'] ) ) );
			update_post_meta( $id, 'exclude_product_categories', array_filter( array_map( 'intval', $coupon_data['exclude_product_category_ids'] ) ) );
			update_post_meta( $id, 'exclude_sale_items', ( true === $coupon_data['exclude_sale_items'] ) ? 'yes' : 'no' );
			update_post_meta( $id, 'minimum_amount', wc_format_decimal( $coupon_data['minimum_amount'] ) );
			update_post_meta( $id, 'maximum_amount', wc_format_decimal( $coupon_data['maximum_amount'] ) );
			update_post_meta( $id, 'customer_email', array_filter( array_map( 'sanitize_email', $coupon_data['customer_emails'] ) ) );

			do_action( 'woocommerce_api_create_coupon', $id, $data );
			do_action( 'woocommerce_new_coupon', $id );

			$this->server->send_status( 201 );

			return $this->get_coupon( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit a coupon
	 *
	 * @since 2.2
	 *
	 * @param int $id the coupon ID
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function edit_coupon( $id, $data ) {

		try {
			if ( ! isset( $data['coupon'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_coupon_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'coupon' ), 400 );
			}

			$data = $data['coupon'];

			$id = $this->validate_request( $id, 'shop_coupon', 'edit' );

			if ( is_wp_error( $id ) ) {
				return $id;
			}

			$data = apply_filters( 'woocommerce_api_edit_coupon_data', $data, $id, $this );

			if ( isset( $data['code'] ) ) {
				global $wpdb;

				$coupon_code  = wc_format_coupon_code( $data['code'] );
				$id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id );

				if ( $id_from_code ) {
					throw new WC_API_Exception( 'woocommerce_api_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), 400 );
				}

				$updated = wp_update_post( array( 'ID' => intval( $id ), 'post_title' => $coupon_code ) );

				if ( 0 === $updated ) {
					throw new WC_API_Exception( 'woocommerce_api_cannot_update_coupon', __( 'Failed to update coupon', 'woocommerce' ), 400 );
				}
			}

			if ( isset( $data['description'] ) ) {
				$updated = wp_update_post( array( 'ID' => intval( $id ), 'post_excerpt' => $data['description'] ) );

				if ( 0 === $updated ) {
					throw new WC_API_Exception( 'woocommerce_api_cannot_update_coupon', __( 'Failed to update coupon', 'woocommerce' ), 400 );
				}
			}

			if ( isset( $data['type'] ) ) {
				// Validate coupon types
				if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_coupon_types() ) ) ) {
					throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_type', sprintf( __( 'Invalid coupon type - the coupon type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_coupon_types() ) ) ), 400 );
				}
				update_post_meta( $id, 'discount_type', $data['type'] );
			}

			if ( isset( $data['amount'] ) ) {
				update_post_meta( $id, 'coupon_amount', wc_format_decimal( $data['amount'] ) );
			}

			if ( isset( $data['individual_use'] ) ) {
				update_post_meta( $id, 'individual_use', ( true === $data['individual_use'] ) ? 'yes' : 'no' );
			}

			if ( isset( $data['product_ids'] ) ) {
				update_post_meta( $id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $data['product_ids'] ) ) ) );
			}

			if ( isset( $data['exclude_product_ids'] ) ) {
				update_post_meta( $id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $data['exclude_product_ids'] ) ) ) );
			}

			if ( isset( $data['usage_limit'] ) ) {
				update_post_meta( $id, 'usage_limit', absint( $data['usage_limit'] ) );
			}

			if ( isset( $data['usage_limit_per_user'] ) ) {
				update_post_meta( $id, 'usage_limit_per_user', absint( $data['usage_limit_per_user'] ) );
			}

			if ( isset( $data['limit_usage_to_x_items'] ) ) {
				update_post_meta( $id, 'limit_usage_to_x_items', absint( $data['limit_usage_to_x_items'] ) );
			}

			if ( isset( $data['usage_count'] ) ) {
				update_post_meta( $id, 'usage_count', absint( $data['usage_count'] ) );
			}

			if ( isset( $data['expiry_date'] ) ) {
				update_post_meta( $id, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ) ) );
				update_post_meta( $id, 'date_expires', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ), true ) );
			}

			if ( isset( $data['enable_free_shipping'] ) ) {
				update_post_meta( $id, 'free_shipping', ( true === $data['enable_free_shipping'] ) ? 'yes' : 'no' );
			}

			if ( isset( $data['product_category_ids'] ) ) {
				update_post_meta( $id, 'product_categories', array_filter( array_map( 'intval', $data['product_category_ids'] ) ) );
			}

			if ( isset( $data['exclude_product_category_ids'] ) ) {
				update_post_meta( $id, 'exclude_product_categories', array_filter( array_map( 'intval', $data['exclude_product_category_ids'] ) ) );
			}

			if ( isset( $data['exclude_sale_items'] ) ) {
				update_post_meta( $id, 'exclude_sale_items', ( true === $data['exclude_sale_items'] ) ? 'yes' : 'no' );
			}

			if ( isset( $data['minimum_amount'] ) ) {
				update_post_meta( $id, 'minimum_amount', wc_format_decimal( $data['minimum_amount'] ) );
			}

			if ( isset( $data['maximum_amount'] ) ) {
				update_post_meta( $id, 'maximum_amount', wc_format_decimal( $data['maximum_amount'] ) );
			}

			if ( isset( $data['customer_emails'] ) ) {
				update_post_meta( $id, 'customer_email', array_filter( array_map( 'sanitize_email', $data['customer_emails'] ) ) );
			}

			do_action( 'woocommerce_api_edit_coupon', $id, $data );
			do_action( 'woocommerce_update_coupon', $id );

			return $this->get_coupon( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete a coupon
	 *
	 * @since  2.2
	 *
	 * @param int $id the coupon ID
	 * @param bool $force true to permanently delete coupon, false to move to trash
	 *
	 * @return array|int|WP_Error
	 */
	public function delete_coupon( $id, $force = false ) {

		$id = $this->validate_request( $id, 'shop_coupon', 'delete' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		do_action( 'woocommerce_api_delete_coupon', $id, $this );

		return $this->delete( $id, 'shop_coupon', ( 'true' === $force ) );
	}

	/**
	 * expiry_date format
	 *
	 * @since  2.3.0
	 * @param  string $expiry_date
	 * @param bool $as_timestamp (default: false)
	 * @return string|int
	 */
	protected function get_coupon_expiry_date( $expiry_date, $as_timestamp = false ) {
		if ( '' != $expiry_date ) {
			if ( $as_timestamp ) {
				return strtotime( $expiry_date );
			}

			return date( 'Y-m-d', strtotime( $expiry_date ) );
		}

		return '';
	}

	/**
	 * Helper method to get coupon post objects
	 *
	 * @since 2.1
	 * @param array $args request arguments for filtering query
	 * @return WP_Query
	 */
	private function query_coupons( $args ) {

		// set base query arguments
		$query_args = array(
			'fields'      => 'ids',
			'post_type'   => 'shop_coupon',
			'post_status' => 'publish',
		);

		$query_args = $this->merge_query_args( $query_args, $args );

		return new WP_Query( $query_args );
	}

	/**
	 * Bulk update or insert coupons
	 * Accepts an array with coupons in the formats supported by
	 * WC_API_Coupons->create_coupon() and WC_API_Coupons->edit_coupon()
	 *
	 * @since 2.4.0
	 *
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function bulk( $data ) {

		try {
			if ( ! isset( $data['coupons'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_coupons_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'coupons' ), 400 );
			}

			$data  = $data['coupons'];
			$limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'coupons' );

			// Limit bulk operation
			if ( count( $data ) > $limit ) {
				throw new WC_API_Exception( 'woocommerce_api_coupons_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 );
			}

			$coupons = array();

			foreach ( $data as $_coupon ) {
				$coupon_id = 0;

				// Try to get the coupon ID
				if ( isset( $_coupon['id'] ) ) {
					$coupon_id = intval( $_coupon['id'] );
				}

				if ( $coupon_id ) {

					// Coupon exists / edit coupon
					$edit = $this->edit_coupon( $coupon_id, array( 'coupon' => $_coupon ) );

					if ( is_wp_error( $edit ) ) {
						$coupons[] = array(
							'id'    => $coupon_id,
							'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ),
						);
					} else {
						$coupons[] = $edit['coupon'];
					}
				} else {

					// Coupon don't exists / create coupon
					$new = $this->create_coupon( array( 'coupon' => $_coupon ) );

					if ( is_wp_error( $new ) ) {
						$coupons[] = array(
							'id'    => $coupon_id,
							'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ),
						);
					} else {
						$coupons[] = $new['coupon'];
					}
				}
			}

			return array( 'coupons' => apply_filters( 'woocommerce_api_coupons_bulk_response', $coupons, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}
}
legacy/api/v3/class-wc-api-taxes.php000064400000044210151336044770013242 0ustar00<?php
/**
 * WooCommerce API Taxes Class
 *
 * Handles requests to the /taxes endpoint
 *
 * @author   WooThemes
 * @category API
 * @package  WooCommerce\RestApi
 * @since    2.5.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WC_API_Taxes extends WC_API_Resource {

	/** @var string $base the route base */
	protected $base = '/taxes';

	/**
	 * Register the routes for this class
	 *
	 * GET /taxes
	 * GET /taxes/count
	 * GET /taxes/<id>
	 *
	 * @since 2.1
	 * @param array $routes
	 * @return array
	 */
	public function register_routes( $routes ) {

		# GET/POST /taxes
		$routes[ $this->base ] = array(
			array( array( $this, 'get_taxes' ), WC_API_Server::READABLE ),
			array( array( $this, 'create_tax' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET /taxes/count
		$routes[ $this->base . '/count' ] = array(
			array( array( $this, 'get_taxes_count' ), WC_API_Server::READABLE ),
		);

		# GET/PUT/DELETE /taxes/<id>
		$routes[ $this->base . '/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_tax' ), WC_API_Server::READABLE ),
			array( array( $this, 'edit_tax' ), WC_API_SERVER::EDITABLE | WC_API_SERVER::ACCEPT_DATA ),
			array( array( $this, 'delete_tax' ), WC_API_SERVER::DELETABLE ),
		);

		# GET/POST /taxes/classes
		$routes[ $this->base . '/classes' ] = array(
			array( array( $this, 'get_tax_classes' ), WC_API_Server::READABLE ),
			array( array( $this, 'create_tax_class' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET /taxes/classes/count
		$routes[ $this->base . '/classes/count' ] = array(
			array( array( $this, 'get_tax_classes_count' ), WC_API_Server::READABLE ),
		);

		# GET /taxes/classes/<slug>
		$routes[ $this->base . '/classes/(?P<slug>\w[\w\s\-]*)' ] = array(
			array( array( $this, 'delete_tax_class' ), WC_API_SERVER::DELETABLE ),
		);

		# POST|PUT /taxes/bulk
		$routes[ $this->base . '/bulk' ] = array(
			array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
		);

		return $routes;
	}

	/**
	 * Get all taxes
	 *
	 * @since 2.5.0
	 *
	 * @param string $fields
	 * @param array  $filter
	 * @param string $class
	 * @param int    $page
	 *
	 * @return array
	 */
	public function get_taxes( $fields = null, $filter = array(), $class = null, $page = 1 ) {
		if ( ! empty( $class ) ) {
			$filter['tax_rate_class'] = $class;
		}

		$filter['page'] = $page;

		$query = $this->query_tax_rates( $filter );

		$taxes = array();

		foreach ( $query['results'] as $tax ) {
			$taxes[] = current( $this->get_tax( $tax->tax_rate_id, $fields ) );
		}

		// Set pagination headers
		$this->server->add_pagination_headers( $query['headers'] );

		return array( 'taxes' => $taxes );
	}

	/**
	 * Get the tax for the given ID
	 *
	 * @since 2.5.0
	 *
	 * @param int $id The tax ID
	 * @param string $fields fields to include in response
	 *
	 * @return array|WP_Error
	 */
	public function get_tax( $id, $fields = null ) {
		global $wpdb;

		try {
			$id = absint( $id );

			// Permissions check
			if ( ! current_user_can( 'manage_woocommerce' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_tax', __( 'You do not have permission to read tax rate', 'woocommerce' ), 401 );
			}

			// Get tax rate details
			$tax = WC_Tax::_get_tax_rate( $id );

			if ( is_wp_error( $tax ) || empty( $tax ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_tax_id', __( 'A tax rate with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$tax_data = array(
				'id'       => (int) $tax['tax_rate_id'],
				'country'  => $tax['tax_rate_country'],
				'state'    => $tax['tax_rate_state'],
				'postcode' => '',
				'city'     => '',
				'rate'     => $tax['tax_rate'],
				'name'     => $tax['tax_rate_name'],
				'priority' => (int) $tax['tax_rate_priority'],
				'compound' => (bool) $tax['tax_rate_compound'],
				'shipping' => (bool) $tax['tax_rate_shipping'],
				'order'    => (int) $tax['tax_rate_order'],
				'class'    => $tax['tax_rate_class'] ? $tax['tax_rate_class'] : 'standard',
			);

			// Get locales from a tax rate
			$locales = $wpdb->get_results( $wpdb->prepare( "
				SELECT location_code, location_type
				FROM {$wpdb->prefix}woocommerce_tax_rate_locations
				WHERE tax_rate_id = %d
			", $id ) );

			if ( ! is_wp_error( $tax ) && ! is_null( $tax ) ) {
				foreach ( $locales as $locale ) {
					$tax_data[ $locale->location_type ] = $locale->location_code;
				}
			}

			return array( 'tax' => apply_filters( 'woocommerce_api_tax_response', $tax_data, $tax, $fields, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create a tax
	 *
	 * @since 2.5.0
	 *
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function create_tax( $data ) {
		try {
			if ( ! isset( $data['tax'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_tax_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'tax' ), 400 );
			}

			// Check permissions
			if ( ! current_user_can( 'manage_woocommerce' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_tax', __( 'You do not have permission to create tax rates', 'woocommerce' ), 401 );
			}

			$data = apply_filters( 'woocommerce_api_create_tax_data', $data['tax'], $this );

			$tax_data = array(
				'tax_rate_country'  => '',
				'tax_rate_state'    => '',
				'tax_rate'          => '',
				'tax_rate_name'     => '',
				'tax_rate_priority' => 1,
				'tax_rate_compound' => 0,
				'tax_rate_shipping' => 1,
				'tax_rate_order'    => 0,
				'tax_rate_class'    => '',
			);

			foreach ( $tax_data as $key => $value ) {
				$new_key = str_replace( 'tax_rate_', '', $key );
				$new_key = 'tax_rate' === $new_key ? 'rate' : $new_key;

				if ( isset( $data[ $new_key ] ) ) {
					if ( in_array( $new_key, array( 'compound', 'shipping' ) ) ) {
						$tax_data[ $key ] = $data[ $new_key ] ? 1 : 0;
					} else {
						$tax_data[ $key ] = $data[ $new_key ];
					}
				}
			}

			// Create tax rate
			$id = WC_Tax::_insert_tax_rate( $tax_data );

			// Add locales
			if ( ! empty( $data['postcode'] ) ) {
				WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $data['postcode'] ) );
			}

			if ( ! empty( $data['city'] ) ) {
				WC_Tax::_update_tax_rate_cities( $id, wc_clean( $data['city'] ) );
			}

			do_action( 'woocommerce_api_create_tax', $id, $data );

			$this->server->send_status( 201 );

			return $this->get_tax( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit a tax
	 *
	 * @since 2.5.0
	 *
	 * @param int $id The tax ID
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function edit_tax( $id, $data ) {
		try {
			if ( ! isset( $data['tax'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_tax_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'tax' ), 400 );
			}

			// Check permissions
			if ( ! current_user_can( 'manage_woocommerce' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_tax', __( 'You do not have permission to edit tax rates', 'woocommerce' ), 401 );
			}

			$data = $data['tax'];

			// Get current tax rate data
			$tax = $this->get_tax( $id );

			if ( is_wp_error( $tax ) ) {
				$error_data = $tax->get_error_data();
				throw new WC_API_Exception( $tax->get_error_code(), $tax->get_error_message(), $error_data['status'] );
			}

			$current_data   = $tax['tax'];
			$data           = apply_filters( 'woocommerce_api_edit_tax_data', $data, $this );
			$tax_data       = array();
			$default_fields = array(
				'tax_rate_country',
				'tax_rate_state',
				'tax_rate',
				'tax_rate_name',
				'tax_rate_priority',
				'tax_rate_compound',
				'tax_rate_shipping',
				'tax_rate_order',
				'tax_rate_class',
			);

			foreach ( $data as $key => $value ) {
				$new_key = 'rate' === $key ? 'tax_rate' : 'tax_rate_' . $key;

				// Check if the key is valid
				if ( ! in_array( $new_key, $default_fields ) ) {
					continue;
				}

				// Test new data against current data
				if ( $value === $current_data[ $key ] ) {
					continue;
				}

				// Fix compound and shipping values
				if ( in_array( $key, array( 'compound', 'shipping' ) ) ) {
					$value = $value ? 1 : 0;
				}

				$tax_data[ $new_key ] = $value;
			}

			// Update tax rate
			WC_Tax::_update_tax_rate( $id, $tax_data );

			// Update locales
			if ( ! empty( $data['postcode'] ) && $current_data['postcode'] != $data['postcode'] ) {
				WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $data['postcode'] ) );
			}

			if ( ! empty( $data['city'] ) && $current_data['city'] != $data['city'] ) {
				WC_Tax::_update_tax_rate_cities( $id, wc_clean( $data['city'] ) );
			}

			do_action( 'woocommerce_api_edit_tax_rate', $id, $data );

			return $this->get_tax( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete a tax
	 *
	 * @since 2.5.0
	 *
	 * @param int $id The tax ID
	 *
	 * @return array|WP_Error
	 */
	public function delete_tax( $id ) {
		global $wpdb;

		try {
			// Check permissions
			if ( ! current_user_can( 'manage_woocommerce' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_tax', __( 'You do not have permission to delete tax rates', 'woocommerce' ), 401 );
			}

			$id = absint( $id );

			WC_Tax::_delete_tax_rate( $id );

			if ( 0 === $wpdb->rows_affected ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_delete_tax', __( 'Could not delete the tax rate', 'woocommerce' ), 401 );
			}

			return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'tax' ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the total number of taxes
	 *
	 * @since 2.5.0
	 *
	 * @param string $class
	 * @param array $filter
	 *
	 * @return array|WP_Error
	 */
	public function get_taxes_count( $class = null, $filter = array() ) {
		try {
			if ( ! current_user_can( 'manage_woocommerce' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_taxes_count', __( 'You do not have permission to read the taxes count', 'woocommerce' ), 401 );
			}

			if ( ! empty( $class ) ) {
				$filter['tax_rate_class'] = $class;
			}

			$query = $this->query_tax_rates( $filter, true );

			return array( 'count' => (int) $query['headers']->total );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Helper method to get tax rates objects
	 *
	 * @since 2.5.0
	 *
	 * @param  array $args
	 * @param  bool  $count_only
	 *
	 * @return array
	 */
	protected function query_tax_rates( $args, $count_only = false ) {
		global $wpdb;

		$results = '';

		// Set args
		$args = $this->merge_query_args( $args, array() );

		$query = "
			SELECT tax_rate_id
			FROM {$wpdb->prefix}woocommerce_tax_rates
			WHERE 1 = 1
		";

		// Filter by tax class
		if ( ! empty( $args['tax_rate_class'] ) ) {
			$tax_rate_class = 'standard' !== $args['tax_rate_class'] ? sanitize_title( $args['tax_rate_class'] ) : '';
			$query .= " AND tax_rate_class = '$tax_rate_class'";
		}

		// Order tax rates
		$order_by = ' ORDER BY tax_rate_order';

		// Pagination
		$per_page   = isset( $args['posts_per_page'] ) ? $args['posts_per_page'] : get_option( 'posts_per_page' );
		$offset     = 1 < $args['paged'] ? ( $args['paged'] - 1 ) * $per_page : 0;
		$pagination = sprintf( ' LIMIT %d, %d', $offset, $per_page );

		if ( ! $count_only ) {
			$results = $wpdb->get_results( $query . $order_by . $pagination );
		}

		$wpdb->get_results( $query );
		$headers              = new stdClass;
		$headers->page        = $args['paged'];
		$headers->total       = (int) $wpdb->num_rows;
		$headers->is_single   = $per_page > $headers->total;
		$headers->total_pages = ceil( $headers->total / $per_page );

		return array(
			'results' => $results,
			'headers' => $headers,
		);
	}

	/**
	 * Bulk update or insert taxes
	 * Accepts an array with taxes in the formats supported by
	 * WC_API_Taxes->create_tax() and WC_API_Taxes->edit_tax()
	 *
	 * @since 2.5.0
	 *
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function bulk( $data ) {
		try {
			if ( ! isset( $data['taxes'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_taxes_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'taxes' ), 400 );
			}

			$data  = $data['taxes'];
			$limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'taxes' );

			// Limit bulk operation
			if ( count( $data ) > $limit ) {
				throw new WC_API_Exception( 'woocommerce_api_taxes_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 );
			}

			$taxes = array();

			foreach ( $data as $_tax ) {
				$tax_id = 0;

				// Try to get the tax rate ID
				if ( isset( $_tax['id'] ) ) {
					$tax_id = intval( $_tax['id'] );
				}

				if ( $tax_id ) {

					// Tax rate exists / edit tax rate
					$edit = $this->edit_tax( $tax_id, array( 'tax' => $_tax ) );

					if ( is_wp_error( $edit ) ) {
						$taxes[] = array(
							'id'    => $tax_id,
							'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ),
						);
					} else {
						$taxes[] = $edit['tax'];
					}
				} else {

					// Tax rate don't exists / create tax rate
					$new = $this->create_tax( array( 'tax' => $_tax ) );

					if ( is_wp_error( $new ) ) {
						$taxes[] = array(
							'id'    => $tax_id,
							'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ),
						);
					} else {
						$taxes[] = $new['tax'];
					}
				}
			}

			return array( 'taxes' => apply_filters( 'woocommerce_api_taxes_bulk_response', $taxes, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get all tax classes
	 *
	 * @since 2.5.0
	 *
	 * @param string $fields
	 *
	 * @return array|WP_Error
	 */
	public function get_tax_classes( $fields = null ) {
		try {
			// Permissions check
			if ( ! current_user_can( 'manage_woocommerce' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_tax_classes', __( 'You do not have permission to read tax classes', 'woocommerce' ), 401 );
			}

			$tax_classes = array();

			// Add standard class
			$tax_classes[] = array(
				'slug' => 'standard',
				'name' => __( 'Standard rate', 'woocommerce' ),
			);

			$classes = WC_Tax::get_tax_classes();

			foreach ( $classes as $class ) {
				$tax_classes[] = apply_filters( 'woocommerce_api_tax_class_response', array(
					'slug' => sanitize_title( $class ),
					'name' => $class,
				), $class, $fields, $this );
			}

			return array( 'tax_classes' => apply_filters( 'woocommerce_api_tax_classes_response', $tax_classes, $classes, $fields, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create a tax class.
	 *
	 * @since 2.5.0
	 *
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function create_tax_class( $data ) {
		try {
			if ( ! isset( $data['tax_class'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_tax_class_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'tax_class' ), 400 );
			}

			// Check permissions
			if ( ! current_user_can( 'manage_woocommerce' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_tax_class', __( 'You do not have permission to create tax classes', 'woocommerce' ), 401 );
			}

			$data = $data['tax_class'];

			if ( empty( $data['name'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_tax_class_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 );
			}

			$name      = sanitize_text_field( $data['name'] );
			$tax_class = WC_Tax::create_tax_class( $name );

			if ( is_wp_error( $tax_class ) ) {
				return new WP_Error( 'woocommerce_api_' . $tax_class->get_error_code(), $tax_class->get_error_message(), 401 );
			}

			do_action( 'woocommerce_api_create_tax_class', $tax_class['slug'], $data );

			$this->server->send_status( 201 );

			return array(
				'tax_class' => $tax_class,
			);
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete a tax class
	 *
	 * @since 2.5.0
	 *
	 * @param int $slug The tax class slug
	 *
	 * @return array|WP_Error
	 */
	public function delete_tax_class( $slug ) {
		global $wpdb;

		try {
			// Check permissions
			if ( ! current_user_can( 'manage_woocommerce' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_tax_class', __( 'You do not have permission to delete tax classes', 'woocommerce' ), 401 );
			}

			$slug      = sanitize_title( $slug );
			$tax_class = WC_Tax::get_tax_class_by( 'slug', $slug );
			$deleted   = WC_Tax::delete_tax_class_by( 'slug', $slug );

			if ( is_wp_error( $deleted ) || ! $deleted ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_delete_tax_class', __( 'Could not delete the tax class', 'woocommerce' ), 401 );
			}

			return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'tax_class' ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the total number of tax classes
	 *
	 * @since 2.5.0
	 *
	 * @return array|WP_Error
	 */
	public function get_tax_classes_count() {
		try {
			if ( ! current_user_can( 'manage_woocommerce' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_tax_classes_count', __( 'You do not have permission to read the tax classes count', 'woocommerce' ), 401 );
			}

			$total = count( WC_Tax::get_tax_classes() ) + 1; // +1 for Standard Rate

			return array( 'count' => $total );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}
}
legacy/api/class-wc-rest-legacy-orders-controller.php000064400000022351151336044770016715 0ustar00<?php
/**
 * REST API Legacy Orders controller
 *
 * Handles requests to the /orders endpoint.
 *
 * @author   WooThemes
 * @category API
 * @package  WooCommerce\RestApi
 * @since    3.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * REST API Legacy Orders controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_CRUD_Controller
 */
class WC_REST_Legacy_Orders_Controller extends WC_REST_CRUD_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';

	/**
	 * Query args.
	 *
	 * @deprecated 3.0
	 *
	 * @param array $args
	 * @param WP_REST_Request $request
	 * @return array
	 */
	public function query_args( $args, $request ) {
		global $wpdb;

		// Set post_status.
		if ( 'any' !== $request['status'] ) {
			$args['post_status'] = 'wc-' . $request['status'];
		} else {
			$args['post_status'] = 'any';
		}

		if ( ! empty( $request['customer'] ) ) {
			if ( ! empty( $args['meta_query'] ) ) {
				$args['meta_query'] = array();
			}

			$args['meta_query'][] = array(
				'key'   => '_customer_user',
				'value' => $request['customer'],
				'type'  => 'NUMERIC',
			);
		}

		// Search by product.
		if ( ! empty( $request['product'] ) ) {
			$order_ids = $wpdb->get_col( $wpdb->prepare( "
				SELECT order_id
				FROM {$wpdb->prefix}woocommerce_order_items
				WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d )
				AND order_item_type = 'line_item'
			 ", $request['product'] ) );

			// Force WP_Query return empty if don't found any order.
			$order_ids = ! empty( $order_ids ) ? $order_ids : array( 0 );

			$args['post__in'] = $order_ids;
		}

		// Search.
		if ( ! empty( $args['s'] ) ) {
			$order_ids = wc_order_search( $args['s'] );

			if ( ! empty( $order_ids ) ) {
				unset( $args['s'] );
				$args['post__in'] = array_merge( $order_ids, array( 0 ) );
			}
		}

		return $args;
	}

	/**
	 * Prepare a single order output for response.
	 *
	 * @deprecated 3.0
	 *
	 * @param WP_Post $post Post object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $data
	 */
	public function prepare_item_for_response( $post, $request ) {
		$this->request       = $request;
		$this->request['dp'] = is_null( $this->request['dp'] ) ? wc_get_price_decimals() : absint( $this->request['dp'] );
		$statuses            = wc_get_order_statuses();
		$order               = wc_get_order( $post );
		$data                = array_merge( array( 'id' => $order->get_id() ), $order->get_data() );
		$format_decimal      = array( 'discount_total', 'discount_tax', 'shipping_total', 'shipping_tax', 'shipping_total', 'shipping_tax', 'cart_tax', 'total', 'total_tax' );
		$format_date         = array( 'date_created', 'date_modified', 'date_completed', 'date_paid' );
		$format_line_items   = array( 'line_items', 'tax_lines', 'shipping_lines', 'fee_lines', 'coupon_lines' );

		// Format decimal values.
		foreach ( $format_decimal as $key ) {
			$data[ $key ] = wc_format_decimal( $data[ $key ], $this->request['dp'] );
		}

		// Format date values.
		foreach ( $format_date as $key ) {
			$data[ $key ] = $data[ $key ] ? wc_rest_prepare_date_response( get_gmt_from_date( date( 'Y-m-d H:i:s', $data[ $key ] ) ) ) : false;
		}

		// Format the order status.
		$data['status'] = 'wc-' === substr( $data['status'], 0, 3 ) ? substr( $data['status'], 3 ) : $data['status'];

		// Format line items.
		foreach ( $format_line_items as $key ) {
			$data[ $key ] = array_values( array_map( array( $this, 'get_order_item_data' ), $data[ $key ] ) );
		}

		// Refunds.
		$data['refunds'] = array();
		foreach ( $order->get_refunds() as $refund ) {
			$data['refunds'][] = array(
				'id'     => $refund->get_id(),
				'refund' => $refund->get_reason() ? $refund->get_reason() : '',
				'total'  => '-' . wc_format_decimal( $refund->get_amount(), $this->request['dp'] ),
			);
		}

		$context  = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data     = $this->add_additional_fields_to_object( $data, $request );
		$data     = $this->filter_response_by_context( $data, $context );
		$response = rest_ensure_response( $data );
		$response->add_links( $this->prepare_links( $order, $request ) );

		/**
		 * Filter the data for a response.
		 *
		 * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
		 * prepared for the response.
		 *
		 * @param WP_REST_Response   $response   The response object.
		 * @param WP_Post            $post       Post object.
		 * @param WP_REST_Request    $request    Request object.
		 */
		return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request );
	}

	/**
	 * Prepare a single order for create.
	 *
	 * @deprecated 3.0
	 *
	 * @param  WP_REST_Request $request Request object.
	 * @return WP_Error|WC_Order $data Object.
	 */
	protected function prepare_item_for_database( $request ) {
		$id        = isset( $request['id'] ) ? absint( $request['id'] ) : 0;
		$order     = new WC_Order( $id );
		$schema    = $this->get_item_schema();
		$data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) );

		// Handle all writable props
		foreach ( $data_keys as $key ) {
			$value = $request[ $key ];

			if ( ! is_null( $value ) ) {
				switch ( $key ) {
					case 'billing' :
					case 'shipping' :
						$this->update_address( $order, $value, $key );
						break;
					case 'line_items' :
					case 'shipping_lines' :
					case 'fee_lines' :
					case 'coupon_lines' :
						if ( is_array( $value ) ) {
							foreach ( $value as $item ) {
								if ( is_array( $item ) ) {
									if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) {
										$order->remove_item( $item['id'] );
									} else {
										$this->set_item( $order, $key, $item );
									}
								}
							}
						}
						break;
					case 'meta_data' :
						if ( is_array( $value ) ) {
							foreach ( $value as $meta ) {
								$order->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
							}
						}
						break;
					default :
						if ( is_callable( array( $order, "set_{$key}" ) ) ) {
							$order->{"set_{$key}"}( $value );
						}
						break;
				}
			}
		}

		/**
		 * Filter the data for the insert.
		 *
		 * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
		 * prepared for the response.
		 *
		 * @param WC_Order           $order      The Order object.
		 * @param WP_REST_Request    $request    Request object.
		 */
		return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $order, $request );
	}

	/**
	 * Create base WC Order object.
	 *
	 * @deprecated 3.0.0
	 *
	 * @param array $data
	 * @return WC_Order
	 */
	protected function create_base_order( $data ) {
		return wc_create_order( $data );
	}

	/**
	 * Create order.
	 *
	 * @deprecated 3.0.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return int|WP_Error
	 */
	protected function create_order( $request ) {
		try {
			// Make sure customer exists.
			if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] && false === get_user_by( 'id', $request['customer_id'] ) ) {
				throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id',__( 'Customer ID is invalid.', 'woocommerce' ), 400 );
			}

			// Make sure customer is part of blog.
			if ( is_multisite() && ! is_user_member_of_blog( $request['customer_id'] ) ) {
				add_user_to_blog( get_current_blog_id(), $request['customer_id'], 'customer' );
			}

			$order = $this->prepare_item_for_database( $request );
			$order->set_created_via( 'rest-api' );
			$order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) );
			$order->calculate_totals();
			$order->save();

			// Handle set paid.
			if ( true === $request['set_paid'] ) {
				$order->payment_complete( $request['transaction_id'] );
			}

			return $order->get_id();
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
		} catch ( WC_REST_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Update order.
	 *
	 * @deprecated 3.0.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return int|WP_Error
	 */
	protected function update_order( $request ) {
		try {
			$order = $this->prepare_item_for_database( $request );
			$order->save();

			// Handle set paid.
			if ( $order->needs_payment() && true === $request['set_paid'] ) {
				$order->payment_complete( $request['transaction_id'] );
			}

			// If items have changed, recalculate order totals.
			if ( isset( $request['billing'] ) || isset( $request['shipping'] ) || isset( $request['line_items'] ) || isset( $request['shipping_lines'] ) || isset( $request['fee_lines'] ) || isset( $request['coupon_lines'] ) ) {
				$order->calculate_totals();
			}

			return $order->get_id();
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
		} catch ( WC_REST_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}
}
legacy/api/class-wc-rest-legacy-products-controller.php000064400000055362151336044770017272 0ustar00<?php
/**
 * REST API Legacy Products controller
 *
 * Handles requests to the /products endpoint.
 *
 * @author   WooThemes
 * @category API
 * @package  WooCommerce\RestApi
 * @since    3.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * REST API Legacy Products controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_CRUD_Controller
 */
class WC_REST_Legacy_Products_Controller extends WC_REST_CRUD_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v2';

	/**
	 * Query args.
	 *
	 * @deprecated 3.0.0
	 *
	 * @param array           $args    Request args.
	 * @param WP_REST_Request $request Request data.
	 * @return array
	 */
	public function query_args( $args, $request ) {
		// Set post_status.
		$args['post_status'] = $request['status'];

		// Taxonomy query to filter products by type, category,
		// tag, shipping class, and attribute.
		$tax_query = array();

		// Map between taxonomy name and arg's key.
		$taxonomies = array(
			'product_cat'            => 'category',
			'product_tag'            => 'tag',
			'product_shipping_class' => 'shipping_class',
		);

		// Set tax_query for each passed arg.
		foreach ( $taxonomies as $taxonomy => $key ) {
			if ( ! empty( $request[ $key ] ) ) {
				$tax_query[] = array(
					'taxonomy' => $taxonomy,
					'field'    => 'term_id',
					'terms'    => $request[ $key ],
				);
			}
		}

		// Filter product type by slug.
		if ( ! empty( $request['type'] ) ) {
			$tax_query[] = array(
				'taxonomy' => 'product_type',
				'field'    => 'slug',
				'terms'    => $request['type'],
			);
		}

		// Filter by attribute and term.
		if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) {
			if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names(), true ) ) {
				$tax_query[] = array(
					'taxonomy' => $request['attribute'],
					'field'    => 'term_id',
					'terms'    => $request['attribute_term'],
				);
			}
		}

		if ( ! empty( $tax_query ) ) {
			$args['tax_query'] = $tax_query;
		}

		// Filter featured.
		if ( is_bool( $request['featured'] ) ) {
			$args['tax_query'][] = array(
				'taxonomy' => 'product_visibility',
				'field'    => 'name',
				'terms'    => 'featured',
				'operator' => true === $request['featured'] ? 'IN' : 'NOT IN',
			);
		}

		// Filter by sku.
		if ( ! empty( $request['sku'] ) ) {
			$skus = explode( ',', $request['sku'] );
			// Include the current string as a SKU too.
			if ( 1 < count( $skus ) ) {
				$skus[] = $request['sku'];
			}

			$args['meta_query'] = $this->add_meta_query( $args, array(
				'key'     => '_sku',
				'value'   => $skus,
				'compare' => 'IN',
			) );
		}

		// Filter by tax class.
		if ( ! empty( $request['tax_class'] ) ) {
			$args['meta_query'] = $this->add_meta_query( $args, array(
				'key'   => '_tax_class',
				'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '',
			) );
		}

		// Price filter.
		if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) {
			$args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) );
		}

		// Filter product in stock or out of stock.
		if ( is_bool( $request['in_stock'] ) ) {
			$args['meta_query'] = $this->add_meta_query( $args, array(
				'key'   => '_stock_status',
				'value' => true === $request['in_stock'] ? 'instock' : 'outofstock',
			) );
		}

		// Filter by on sale products.
		if ( is_bool( $request['on_sale'] ) ) {
			$on_sale_key           = $request['on_sale'] ? 'post__in' : 'post__not_in';
			$args[ $on_sale_key ] += wc_get_product_ids_on_sale();
		}

		// Force the post_type argument, since it's not a user input variable.
		if ( ! empty( $request['sku'] ) ) {
			$args['post_type'] = array( 'product', 'product_variation' );
		} else {
			$args['post_type'] = $this->post_type;
		}

		return $args;
	}

	/**
	 * Prepare a single product output for response.
	 *
	 * @deprecated 3.0.0
	 *
	 * @param WP_Post         $post    Post object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_item_for_response( $post, $request ) {
		$product = wc_get_product( $post );
		$data    = $this->get_product_data( $product );

		// Add variations to variable products.
		if ( $product->is_type( 'variable' ) && $product->has_child() ) {
			$data['variations'] = $product->get_children();
		}

		// Add grouped products data.
		if ( $product->is_type( 'grouped' ) && $product->has_child() ) {
			$data['grouped_products'] = $product->get_children();
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		$response->add_links( $this->prepare_links( $product, $request ) );

		/**
		 * Filter the data for a response.
		 *
		 * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
		 * prepared for the response.
		 *
		 * @param WP_REST_Response   $response   The response object.
		 * @param WP_Post            $post       Post object.
		 * @param WP_REST_Request    $request    Request object.
		 */
		return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request );
	}

	/**
	 * Get product menu order.
	 *
	 * @deprecated 3.0.0
	 * @param WC_Product $product Product instance.
	 * @return int
	 */
	protected function get_product_menu_order( $product ) {
		return $product->get_menu_order();
	}

	/**
	 * Save product meta.
	 *
	 * @deprecated 3.0.0
	 * @param WC_Product $product
	 * @param WP_REST_Request $request
	 * @return bool
	 * @throws WC_REST_Exception
	 */
	protected function save_product_meta( $product, $request ) {
		$product = $this->set_product_meta( $product, $request );
		$product->save();

		return true;
	}

	/**
	 * Set product meta.
	 *
	 * @deprecated 3.0.0
	 *
	 * @throws WC_REST_Exception REST API exceptions.
	 * @param WC_Product      $product Product instance.
	 * @param WP_REST_Request $request Request data.
	 * @return WC_Product
	 */
	protected function set_product_meta( $product, $request ) {
		// Virtual.
		if ( isset( $request['virtual'] ) ) {
			$product->set_virtual( $request['virtual'] );
		}

		// Tax status.
		if ( isset( $request['tax_status'] ) ) {
			$product->set_tax_status( $request['tax_status'] );
		}

		// Tax Class.
		if ( isset( $request['tax_class'] ) ) {
			$product->set_tax_class( $request['tax_class'] );
		}

		// Catalog Visibility.
		if ( isset( $request['catalog_visibility'] ) ) {
			$product->set_catalog_visibility( $request['catalog_visibility'] );
		}

		// Purchase Note.
		if ( isset( $request['purchase_note'] ) ) {
			$product->set_purchase_note( wc_clean( $request['purchase_note'] ) );
		}

		// Featured Product.
		if ( isset( $request['featured'] ) ) {
			$product->set_featured( $request['featured'] );
		}

		// Shipping data.
		$product = $this->save_product_shipping_data( $product, $request );

		// SKU.
		if ( isset( $request['sku'] ) ) {
			$product->set_sku( wc_clean( $request['sku'] ) );
		}

		// Attributes.
		if ( isset( $request['attributes'] ) ) {
			$attributes = array();

			foreach ( $request['attributes'] as $attribute ) {
				$attribute_id   = 0;
				$attribute_name = '';

				// Check ID for global attributes or name for product attributes.
				if ( ! empty( $attribute['id'] ) ) {
					$attribute_id   = absint( $attribute['id'] );
					$attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
				} elseif ( ! empty( $attribute['name'] ) ) {
					$attribute_name = wc_clean( $attribute['name'] );
				}

				if ( ! $attribute_id && ! $attribute_name ) {
					continue;
				}

				if ( $attribute_id ) {

					if ( isset( $attribute['options'] ) ) {
						$options = $attribute['options'];

						if ( ! is_array( $attribute['options'] ) ) {
							// Text based attributes - Posted values are term names.
							$options = explode( WC_DELIMITER, $options );
						}

						$values = array_map( 'wc_sanitize_term_text_based', $options );
						$values = array_filter( $values, 'strlen' );
					} else {
						$values = array();
					}

					if ( ! empty( $values ) ) {
						// Add attribute to array, but don't set values.
						$attribute_object = new WC_Product_Attribute();
						$attribute_object->set_id( $attribute_id );
						$attribute_object->set_name( $attribute_name );
						$attribute_object->set_options( $values );
						$attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' );
						$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
						$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
						$attributes[] = $attribute_object;
					}
				} elseif ( isset( $attribute['options'] ) ) {
					// Custom attribute - Add attribute to array and set the values.
					if ( is_array( $attribute['options'] ) ) {
						$values = $attribute['options'];
					} else {
						$values = explode( WC_DELIMITER, $attribute['options'] );
					}
					$attribute_object = new WC_Product_Attribute();
					$attribute_object->set_name( $attribute_name );
					$attribute_object->set_options( $values );
					$attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' );
					$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
					$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
					$attributes[] = $attribute_object;
				}
			}
			$product->set_attributes( $attributes );
		}

		// Sales and prices.
		if ( in_array( $product->get_type(), array( 'variable', 'grouped' ), true ) ) {
			$product->set_regular_price( '' );
			$product->set_sale_price( '' );
			$product->set_date_on_sale_to( '' );
			$product->set_date_on_sale_from( '' );
			$product->set_price( '' );
		} else {
			// Regular Price.
			if ( isset( $request['regular_price'] ) ) {
				$product->set_regular_price( $request['regular_price'] );
			}

			// Sale Price.
			if ( isset( $request['sale_price'] ) ) {
				$product->set_sale_price( $request['sale_price'] );
			}

			if ( isset( $request['date_on_sale_from'] ) ) {
				$product->set_date_on_sale_from( $request['date_on_sale_from'] );
			}

			if ( isset( $request['date_on_sale_to'] ) ) {
				$product->set_date_on_sale_to( $request['date_on_sale_to'] );
			}
		}

		// Product parent ID for groups.
		if ( isset( $request['parent_id'] ) ) {
			$product->set_parent_id( $request['parent_id'] );
		}

		// Sold individually.
		if ( isset( $request['sold_individually'] ) ) {
			$product->set_sold_individually( $request['sold_individually'] );
		}

		// Stock status.
		if ( isset( $request['in_stock'] ) ) {
			$stock_status = true === $request['in_stock'] ? 'instock' : 'outofstock';
		} else {
			$stock_status = $product->get_stock_status();
		}

		// Stock data.
		if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
			// Manage stock.
			if ( isset( $request['manage_stock'] ) ) {
				$product->set_manage_stock( $request['manage_stock'] );
			}

			// Backorders.
			if ( isset( $request['backorders'] ) ) {
				$product->set_backorders( $request['backorders'] );
			}

			if ( $product->is_type( 'grouped' ) ) {
				$product->set_manage_stock( 'no' );
				$product->set_backorders( 'no' );
				$product->set_stock_quantity( '' );
				$product->set_stock_status( $stock_status );
			} elseif ( $product->is_type( 'external' ) ) {
				$product->set_manage_stock( 'no' );
				$product->set_backorders( 'no' );
				$product->set_stock_quantity( '' );
				$product->set_stock_status( 'instock' );
			} elseif ( $product->get_manage_stock() ) {
				// Stock status is always determined by children so sync later.
				if ( ! $product->is_type( 'variable' ) ) {
					$product->set_stock_status( $stock_status );
				}

				// Stock quantity.
				if ( isset( $request['stock_quantity'] ) ) {
					$product->set_stock_quantity( wc_stock_amount( $request['stock_quantity'] ) );
				} elseif ( isset( $request['inventory_delta'] ) ) {
					$stock_quantity  = wc_stock_amount( $product->get_stock_quantity() );
					$stock_quantity += wc_stock_amount( $request['inventory_delta'] );
					$product->set_stock_quantity( wc_stock_amount( $stock_quantity ) );
				}
			} else {
				// Don't manage stock.
				$product->set_manage_stock( 'no' );
				$product->set_stock_quantity( '' );
				$product->set_stock_status( $stock_status );
			}
		} elseif ( ! $product->is_type( 'variable' ) ) {
			$product->set_stock_status( $stock_status );
		}

		// Upsells.
		if ( isset( $request['upsell_ids'] ) ) {
			$upsells = array();
			$ids     = $request['upsell_ids'];

			if ( ! empty( $ids ) ) {
				foreach ( $ids as $id ) {
					if ( $id && $id > 0 ) {
						$upsells[] = $id;
					}
				}
			}

			$product->set_upsell_ids( $upsells );
		}

		// Cross sells.
		if ( isset( $request['cross_sell_ids'] ) ) {
			$crosssells = array();
			$ids        = $request['cross_sell_ids'];

			if ( ! empty( $ids ) ) {
				foreach ( $ids as $id ) {
					if ( $id && $id > 0 ) {
						$crosssells[] = $id;
					}
				}
			}

			$product->set_cross_sell_ids( $crosssells );
		}

		// Product categories.
		if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) {
			$product = $this->save_taxonomy_terms( $product, $request['categories'] );
		}

		// Product tags.
		if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) {
			$product = $this->save_taxonomy_terms( $product, $request['tags'], 'tag' );
		}

		// Downloadable.
		if ( isset( $request['downloadable'] ) ) {
			$product->set_downloadable( $request['downloadable'] );
		}

		// Downloadable options.
		if ( $product->get_downloadable() ) {

			// Downloadable files.
			if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) {
				$product = $this->save_downloadable_files( $product, $request['downloads'] );
			}

			// Download limit.
			if ( isset( $request['download_limit'] ) ) {
				$product->set_download_limit( $request['download_limit'] );
			}

			// Download expiry.
			if ( isset( $request['download_expiry'] ) ) {
				$product->set_download_expiry( $request['download_expiry'] );
			}
		}

		// Product url and button text for external products.
		if ( $product->is_type( 'external' ) ) {
			if ( isset( $request['external_url'] ) ) {
				$product->set_product_url( $request['external_url'] );
			}

			if ( isset( $request['button_text'] ) ) {
				$product->set_button_text( $request['button_text'] );
			}
		}

		// Save default attributes for variable products.
		if ( $product->is_type( 'variable' ) ) {
			$product = $this->save_default_attributes( $product, $request );
		}

		return $product;
	}

	/**
	 * Save variations.
	 *
	 * @deprecated 3.0.0
	 *
	 * @throws WC_REST_Exception REST API exceptions.
	 * @param WC_Product      $product          Product instance.
	 * @param WP_REST_Request $request          Request data.
	 * @return bool
	 */
	protected function save_variations_data( $product, $request ) {
		foreach ( $request['variations'] as $menu_order => $data ) {
			$variation = new WC_Product_Variation( isset( $data['id'] ) ? absint( $data['id'] ) : 0 );

			// Create initial name and status.
			if ( ! $variation->get_slug() ) {
				/* translators: 1: variation id 2: product name */
				$variation->set_name( sprintf( __( 'Variation #%1$s of %2$s', 'woocommerce' ), $variation->get_id(), $product->get_name() ) );
				$variation->set_status( isset( $data['visible'] ) && false === $data['visible'] ? 'private' : 'publish' );
			}

			// Parent ID.
			$variation->set_parent_id( $product->get_id() );

			// Menu order.
			$variation->set_menu_order( $menu_order );

			// Status.
			if ( isset( $data['visible'] ) ) {
				$variation->set_status( false === $data['visible'] ? 'private' : 'publish' );
			}

			// SKU.
			if ( isset( $data['sku'] ) ) {
				$variation->set_sku( wc_clean( $data['sku'] ) );
			}

			// Thumbnail.
			if ( isset( $data['image'] ) && is_array( $data['image'] ) ) {
				$image = $data['image'];
				$image = current( $image );
				if ( is_array( $image ) ) {
					$image['position'] = 0;
				}

				$variation = $this->set_product_images( $variation, array( $image ) );
			}

			// Virtual variation.
			if ( isset( $data['virtual'] ) ) {
				$variation->set_virtual( $data['virtual'] );
			}

			// Downloadable variation.
			if ( isset( $data['downloadable'] ) ) {
				$variation->set_downloadable( $data['downloadable'] );
			}

			// Downloads.
			if ( $variation->get_downloadable() ) {
				// Downloadable files.
				if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) {
					$variation = $this->save_downloadable_files( $variation, $data['downloads'] );
				}

				// Download limit.
				if ( isset( $data['download_limit'] ) ) {
					$variation->set_download_limit( $data['download_limit'] );
				}

				// Download expiry.
				if ( isset( $data['download_expiry'] ) ) {
					$variation->set_download_expiry( $data['download_expiry'] );
				}
			}

			// Shipping data.
			$variation = $this->save_product_shipping_data( $variation, $data );

			// Stock handling.
			if ( isset( $data['manage_stock'] ) ) {
				$variation->set_manage_stock( $data['manage_stock'] );
			}

			if ( isset( $data['in_stock'] ) ) {
				$variation->set_stock_status( true === $data['in_stock'] ? 'instock' : 'outofstock' );
			}

			if ( isset( $data['backorders'] ) ) {
				$variation->set_backorders( $data['backorders'] );
			}

			if ( $variation->get_manage_stock() ) {
				if ( isset( $data['stock_quantity'] ) ) {
					$variation->set_stock_quantity( $data['stock_quantity'] );
				} elseif ( isset( $data['inventory_delta'] ) ) {
					$stock_quantity  = wc_stock_amount( $variation->get_stock_quantity() );
					$stock_quantity += wc_stock_amount( $data['inventory_delta'] );
					$variation->set_stock_quantity( $stock_quantity );
				}
			} else {
				$variation->set_backorders( 'no' );
				$variation->set_stock_quantity( '' );
			}

			// Regular Price.
			if ( isset( $data['regular_price'] ) ) {
				$variation->set_regular_price( $data['regular_price'] );
			}

			// Sale Price.
			if ( isset( $data['sale_price'] ) ) {
				$variation->set_sale_price( $data['sale_price'] );
			}

			if ( isset( $data['date_on_sale_from'] ) ) {
				$variation->set_date_on_sale_from( $data['date_on_sale_from'] );
			}

			if ( isset( $data['date_on_sale_to'] ) ) {
				$variation->set_date_on_sale_to( $data['date_on_sale_to'] );
			}

			// Tax class.
			if ( isset( $data['tax_class'] ) ) {
				$variation->set_tax_class( $data['tax_class'] );
			}

			// Description.
			if ( isset( $data['description'] ) ) {
				$variation->set_description( wp_kses_post( $data['description'] ) );
			}

			// Update taxonomies.
			if ( isset( $data['attributes'] ) ) {
				$attributes = array();
				$parent_attributes = $product->get_attributes();

				foreach ( $data['attributes'] as $attribute ) {
					$attribute_id   = 0;
					$attribute_name = '';

					// Check ID for global attributes or name for product attributes.
					if ( ! empty( $attribute['id'] ) ) {
						$attribute_id   = absint( $attribute['id'] );
						$attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
					} elseif ( ! empty( $attribute['name'] ) ) {
						$attribute_name = sanitize_title( $attribute['name'] );
					}

					if ( ! $attribute_id && ! $attribute_name ) {
						continue;
					}

					if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) {
						continue;
					}

					$attribute_key   = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() );
					$attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : '';

					if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) {
						// If dealing with a taxonomy, we need to get the slug from the name posted to the API.
						$term = get_term_by( 'name', $attribute_value, $attribute_name );

						if ( $term && ! is_wp_error( $term ) ) {
							$attribute_value = $term->slug;
						} else {
							$attribute_value = sanitize_title( $attribute_value );
						}
					}

					$attributes[ $attribute_key ] = $attribute_value;
				}

				$variation->set_attributes( $attributes );
			}

			$variation->save();

			do_action( 'woocommerce_rest_save_product_variation', $variation->get_id(), $menu_order, $data );
		}

		return true;
	}

	/**
	 * Add post meta fields.
	 *
	 * @deprecated 3.0.0
	 *
	 * @param WP_Post         $post    Post data.
	 * @param WP_REST_Request $request Request data.
	 * @return bool|WP_Error
	 */
	protected function add_post_meta_fields( $post, $request ) {
		return $this->update_post_meta_fields( $post, $request );
	}

	/**
	 * Update post meta fields.
	 *
	 * @param WP_Post         $post    Post data.
	 * @param WP_REST_Request $request Request data.
	 * @return bool|WP_Error
	 */
	protected function update_post_meta_fields( $post, $request ) {
		$product = wc_get_product( $post );

		// Check for featured/gallery images, upload it and set it.
		if ( isset( $request['images'] ) ) {
			$product = $this->set_product_images( $product, $request['images'] );
		}

		// Save product meta fields.
		$product = $this->set_product_meta( $product, $request );

		// Save the product data.
		$product->save();

		// Save variations.
		if ( $product->is_type( 'variable' ) ) {
			if ( isset( $request['variations'] ) && is_array( $request['variations'] ) ) {
				$this->save_variations_data( $product, $request );
			}
		}

		// Clear caches here so in sync with any new variations/children.
		wc_delete_product_transients( $product->get_id() );
		wp_cache_delete( 'product-' . $product->get_id(), 'products' );

		return true;
	}

	/**
	 * Delete post.
	 *
	 * @deprecated 3.0.0
	 *
	 * @param int|WP_Post $id Post ID or WP_Post instance.
	 */
	protected function delete_post( $id ) {
		if ( ! empty( $id->ID ) ) {
			$id = $id->ID;
		} elseif ( ! is_numeric( $id ) || 0 >= $id ) {
			return;
		}

		// Delete product attachments.
		$attachments = get_posts( array(
			'post_parent' => $id,
			'post_status' => 'any',
			'post_type'   => 'attachment',
		) );

		foreach ( (array) $attachments as $attachment ) {
			wp_delete_attachment( $attachment->ID, true );
		}

		// Delete product.
		$product = wc_get_product( $id );
		$product->delete( true );
	}

	/**
	 * Get post types.
	 *
	 * @deprecated 3.0.0
	 *
	 * @return array
	 */
	protected function get_post_types() {
		return array( 'product', 'product_variation' );
	}

	/**
	 * Save product images.
	 *
	 * @deprecated 3.0.0
	 *
	 * @param int $product_id
	 * @param array $images
	 * @throws WC_REST_Exception
	 */
	protected function save_product_images( $product_id, $images ) {
		$product = wc_get_product( $product_id );

		return set_product_images( $product, $images );
	}
}
legacy/api/v2/class-wc-api-json-handler.php000064400000003656151336044770014512 0ustar00<?php
/**
 * WooCommerce API
 *
 * Handles parsing JSON request bodies and generating JSON responses
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WC_API_JSON_Handler implements WC_API_Handler {

	/**
	 * Get the content type for the response
	 *
	 * @since 2.1
	 * @return string
	 */
	public function get_content_type() {

		return sprintf( '%s; charset=%s', isset( $_GET['_jsonp'] ) ? 'application/javascript' : 'application/json', get_option( 'blog_charset' ) );
	}

	/**
	 * Parse the raw request body entity
	 *
	 * @since 2.1
	 * @param string $body the raw request body
	 * @return array|mixed
	 */
	public function parse_body( $body ) {

		return json_decode( $body, true );
	}

	/**
	 * Generate a JSON response given an array of data
	 *
	 * @since 2.1
	 * @param array $data the response data
	 * @return string
	 */
	public function generate_response( $data ) {
		if ( isset( $_GET['_jsonp'] ) ) {

			if ( ! apply_filters( 'woocommerce_api_jsonp_enabled', true ) ) {
				WC()->api->server->send_status( 400 );
				return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_disabled', 'message' => __( 'JSONP support is disabled on this site', 'woocommerce' ) ) ) );
			}

			$jsonp_callback = $_GET['_jsonp'];

			if ( ! wp_check_jsonp_callback( $jsonp_callback ) ) {
				WC()->api->server->send_status( 400 );
				return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_callback_invalid', __( 'The JSONP callback function is invalid', 'woocommerce' ) ) ) );
			}

			WC()->api->server->header( 'X-Content-Type-Options', 'nosniff' );

			// Prepend '/**/' to mitigate possible JSONP Flash attacks.
			// https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
			return '/**/' . $jsonp_callback . '(' . wp_json_encode( $data ) . ')';
		}

		return wp_json_encode( $data );
	}
}
legacy/api/v2/class-wc-api-server.php000064400000050735151336044770013434 0ustar00<?php
/**
 * WooCommerce API
 *
 * Handles REST API requests
 *
 * This class and related code (JSON response handler, resource classes) are based on WP-API v0.6 (https://github.com/WP-API/WP-API)
 * Many thanks to Ryan McCue and any other contributors!
 *
 * @author   WooThemes
 * @category API
 * @package  WooCommerce\RestApi
 * @since    2.1
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

require_once ABSPATH . 'wp-admin/includes/admin.php';

class WC_API_Server {

	const METHOD_GET    = 1;
	const METHOD_POST   = 2;
	const METHOD_PUT    = 4;
	const METHOD_PATCH  = 8;
	const METHOD_DELETE = 16;

	const READABLE   = 1;  // GET
	const CREATABLE  = 2;  // POST
	const EDITABLE   = 14; // POST | PUT | PATCH
	const DELETABLE  = 16; // DELETE
	const ALLMETHODS = 31; // GET | POST | PUT | PATCH | DELETE

	/**
	 * Does the endpoint accept a raw request body?
	 */
	const ACCEPT_RAW_DATA = 64;

	/** Does the endpoint accept a request body? (either JSON or XML) */
	const ACCEPT_DATA = 128;

	/**
	 * Should we hide this endpoint from the index?
	 */
	const HIDDEN_ENDPOINT = 256;

	/**
	 * Map of HTTP verbs to constants
	 * @var array
	 */
	public static $method_map = array(
		'HEAD'   => self::METHOD_GET,
		'GET'    => self::METHOD_GET,
		'POST'   => self::METHOD_POST,
		'PUT'    => self::METHOD_PUT,
		'PATCH'  => self::METHOD_PATCH,
		'DELETE' => self::METHOD_DELETE,
	);

	/**
	 * Requested path (relative to the API root, wp-json.php)
	 *
	 * @var string
	 */
	public $path = '';

	/**
	 * Requested method (GET/HEAD/POST/PUT/PATCH/DELETE)
	 *
	 * @var string
	 */
	public $method = 'HEAD';

	/**
	 * Request parameters
	 *
	 * This acts as an abstraction of the superglobals
	 * (GET => $_GET, POST => $_POST)
	 *
	 * @var array
	 */
	public $params = array( 'GET' => array(), 'POST' => array() );

	/**
	 * Request headers
	 *
	 * @var array
	 */
	public $headers = array();

	/**
	 * Request files (matches $_FILES)
	 *
	 * @var array
	 */
	public $files = array();

	/**
	 * Request/Response handler, either JSON by default
	 * or XML if requested by client
	 *
	 * @var WC_API_Handler
	 */
	public $handler;


	/**
	 * Setup class and set request/response handler
	 *
	 * @since 2.1
	 * @param $path
	 */
	public function __construct( $path ) {

		if ( empty( $path ) ) {
			if ( isset( $_SERVER['PATH_INFO'] ) ) {
				$path = $_SERVER['PATH_INFO'];
			} else {
				$path = '/';
			}
		}

		$this->path           = $path;
		$this->method         = $_SERVER['REQUEST_METHOD'];
		$this->params['GET']  = $_GET;
		$this->params['POST'] = $_POST;
		$this->headers        = $this->get_headers( $_SERVER );
		$this->files          = $_FILES;

		// Compatibility for clients that can't use PUT/PATCH/DELETE
		if ( isset( $_GET['_method'] ) ) {
			$this->method = strtoupper( $_GET['_method'] );
		} elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) {
			$this->method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'];
		}

		// load response handler
		$handler_class = apply_filters( 'woocommerce_api_default_response_handler', 'WC_API_JSON_Handler', $this->path, $this );

		$this->handler = new $handler_class();
	}

	/**
	 * Check authentication for the request
	 *
	 * @since 2.1
	 * @return WP_User|WP_Error WP_User object indicates successful login, WP_Error indicates unsuccessful login
	 */
	public function check_authentication() {

		// allow plugins to remove default authentication or add their own authentication
		$user = apply_filters( 'woocommerce_api_check_authentication', null, $this );

		if ( is_a( $user, 'WP_User' ) ) {

			// API requests run under the context of the authenticated user
			wp_set_current_user( $user->ID );

		} elseif ( ! is_wp_error( $user ) ) {

			// WP_Errors are handled in serve_request()
			$user = new WP_Error( 'woocommerce_api_authentication_error', __( 'Invalid authentication method', 'woocommerce' ), array( 'code' => 500 ) );

		}

		return $user;
	}

	/**
	 * Convert an error to an array
	 *
	 * This iterates over all error codes and messages to change it into a flat
	 * array. This enables simpler client behaviour, as it is represented as a
	 * list in JSON rather than an object/map
	 *
	 * @since 2.1
	 * @param WP_Error $error
	 * @return array List of associative arrays with code and message keys
	 */
	protected function error_to_array( $error ) {
		$errors = array();
		foreach ( (array) $error->errors as $code => $messages ) {
			foreach ( (array) $messages as $message ) {
				$errors[] = array( 'code' => $code, 'message' => $message );
			}
		}

		return array( 'errors' => $errors );
	}

	/**
	 * Handle serving an API request
	 *
	 * Matches the current server URI to a route and runs the first matching
	 * callback then outputs a JSON representation of the returned value.
	 *
	 * @since 2.1
	 * @uses WC_API_Server::dispatch()
	 */
	public function serve_request() {

		do_action( 'woocommerce_api_server_before_serve', $this );

		$this->header( 'Content-Type', $this->handler->get_content_type(), true );

		// the API is enabled by default
		if ( ! apply_filters( 'woocommerce_api_enabled', true, $this ) || ( 'no' === get_option( 'woocommerce_api_enabled' ) ) ) {

			$this->send_status( 404 );

			echo $this->handler->generate_response( array( 'errors' => array( 'code' => 'woocommerce_api_disabled', 'message' => 'The WooCommerce API is disabled on this site' ) ) );

			return;
		}

		$result = $this->check_authentication();

		// if authorization check was successful, dispatch the request
		if ( ! is_wp_error( $result ) ) {
			$result = $this->dispatch();
		}

		// handle any dispatch errors
		if ( is_wp_error( $result ) ) {
			$data = $result->get_error_data();
			if ( is_array( $data ) && isset( $data['status'] ) ) {
				$this->send_status( $data['status'] );
			}

			$result = $this->error_to_array( $result );
		}

		// This is a filter rather than an action, since this is designed to be
		// re-entrant if needed
		$served = apply_filters( 'woocommerce_api_serve_request', false, $result, $this );

		if ( ! $served ) {

			if ( 'HEAD' === $this->method ) {
				return;
			}

			echo $this->handler->generate_response( $result );
		}
	}

	/**
	 * Retrieve the route map
	 *
	 * The route map is an associative array with path regexes as the keys. The
	 * value is an indexed array with the callback function/method as the first
	 * item, and a bitmask of HTTP methods as the second item (see the class
	 * constants).
	 *
	 * Each route can be mapped to more than one callback by using an array of
	 * the indexed arrays. This allows mapping e.g. GET requests to one callback
	 * and POST requests to another.
	 *
	 * Note that the path regexes (array keys) must have @ escaped, as this is
	 * used as the delimiter with preg_match()
	 *
	 * @since 2.1
	 * @return array `'/path/regex' => array( $callback, $bitmask )` or `'/path/regex' => array( array( $callback, $bitmask ), ...)`
	 */
	public function get_routes() {

		// index added by default
		$endpoints = array(

			'/' => array( array( $this, 'get_index' ), self::READABLE ),
		);

		$endpoints = apply_filters( 'woocommerce_api_endpoints', $endpoints );

		// Normalise the endpoints
		foreach ( $endpoints as $route => &$handlers ) {
			if ( count( $handlers ) <= 2 && isset( $handlers[1] ) && ! is_array( $handlers[1] ) ) {
				$handlers = array( $handlers );
			}
		}

		return $endpoints;
	}

	/**
	 * Match the request to a callback and call it
	 *
	 * @since 2.1
	 * @return mixed The value returned by the callback, or a WP_Error instance
	 */
	public function dispatch() {

		switch ( $this->method ) {

			case 'HEAD' :
			case 'GET' :
				$method = self::METHOD_GET;
				break;

			case 'POST' :
				$method = self::METHOD_POST;
				break;

			case 'PUT' :
				$method = self::METHOD_PUT;
				break;

			case 'PATCH' :
				$method = self::METHOD_PATCH;
				break;

			case 'DELETE' :
				$method = self::METHOD_DELETE;
				break;

			default :
				return new WP_Error( 'woocommerce_api_unsupported_method', __( 'Unsupported request method', 'woocommerce' ), array( 'status' => 400 ) );
		}

		foreach ( $this->get_routes() as $route => $handlers ) {
			foreach ( $handlers as $handler ) {
				$callback  = $handler[0];
				$supported = isset( $handler[1] ) ? $handler[1] : self::METHOD_GET;

				if ( ! ( $supported & $method ) ) {
					continue;
				}

				$match = preg_match( '@^' . $route . '$@i', urldecode( $this->path ), $args );

				if ( ! $match ) {
					continue;
				}

				if ( ! is_callable( $callback ) ) {
					return new WP_Error( 'woocommerce_api_invalid_handler', __( 'The handler for the route is invalid', 'woocommerce' ), array( 'status' => 500 ) );
				}

				$args = array_merge( $args, $this->params['GET'] );
				if ( $method & self::METHOD_POST ) {
					$args = array_merge( $args, $this->params['POST'] );
				}
				if ( $supported & self::ACCEPT_DATA ) {
					$data = $this->handler->parse_body( $this->get_raw_data() );
					$args = array_merge( $args, array( 'data' => $data ) );
				} elseif ( $supported & self::ACCEPT_RAW_DATA ) {
					$data = $this->get_raw_data();
					$args = array_merge( $args, array( 'data' => $data ) );
				}

				$args['_method']  = $method;
				$args['_route']   = $route;
				$args['_path']    = $this->path;
				$args['_headers'] = $this->headers;
				$args['_files']   = $this->files;

				$args = apply_filters( 'woocommerce_api_dispatch_args', $args, $callback );

				// Allow plugins to halt the request via this filter
				if ( is_wp_error( $args ) ) {
					return $args;
				}

				$params = $this->sort_callback_params( $callback, $args );
				if ( is_wp_error( $params ) ) {
					return $params;
				}

				return call_user_func_array( $callback, $params );
			}
		}

		return new WP_Error( 'woocommerce_api_no_route', __( 'No route was found matching the URL and request method', 'woocommerce' ), array( 'status' => 404 ) );
	}

	/**
	 * urldecode deep.
	 *
	 * @since  2.2
	 * @param  string|array $value Data to decode with urldecode.
	 * @return string|array        Decoded data.
	 */
	protected function urldecode_deep( $value ) {
		if ( is_array( $value ) ) {
			return array_map( array( $this, 'urldecode_deep' ), $value );
		} else {
			return urldecode( $value );
		}
	}

	/**
	 * Sort parameters by order specified in method declaration
	 *
	 * Takes a callback and a list of available params, then filters and sorts
	 * by the parameters the method actually needs, using the Reflection API
	 *
	 * @since 2.2
	 *
	 * @param callable|array $callback the endpoint callback
	 * @param array $provided the provided request parameters
	 *
	 * @return array|WP_Error
	 */
	protected function sort_callback_params( $callback, $provided ) {
		if ( is_array( $callback ) ) {
			$ref_func = new ReflectionMethod( $callback[0], $callback[1] );
		} else {
			$ref_func = new ReflectionFunction( $callback );
		}

		$wanted = $ref_func->getParameters();
		$ordered_parameters = array();

		foreach ( $wanted as $param ) {
			if ( isset( $provided[ $param->getName() ] ) ) {
				// We have this parameters in the list to choose from
				if ( 'data' == $param->getName() ) {
					$ordered_parameters[] = $provided[ $param->getName() ];
					continue;
				}

				$ordered_parameters[] = $this->urldecode_deep( $provided[ $param->getName() ] );
			} elseif ( $param->isDefaultValueAvailable() ) {
				// We don't have this parameter, but it's optional
				$ordered_parameters[] = $param->getDefaultValue();
			} else {
				// We don't have this parameter and it wasn't optional, abort!
				return new WP_Error( 'woocommerce_api_missing_callback_param', sprintf( __( 'Missing parameter %s', 'woocommerce' ), $param->getName() ), array( 'status' => 400 ) );
			}
		}

		return $ordered_parameters;
	}

	/**
	 * Get the site index.
	 *
	 * This endpoint describes the capabilities of the site.
	 *
	 * @since 2.3
	 * @return array Index entity
	 */
	public function get_index() {

		// General site data
		$available = array(
			'store' => array(
				'name'        => get_option( 'blogname' ),
				'description' => get_option( 'blogdescription' ),
				'URL'         => get_option( 'siteurl' ),
				'wc_version'  => WC()->version,
				'routes'      => array(),
				'meta'        => array(
					'timezone'           => wc_timezone_string(),
					'currency'           => get_woocommerce_currency(),
					'currency_format'    => get_woocommerce_currency_symbol(),
					'currency_position'  => get_option( 'woocommerce_currency_pos' ),
					'thousand_separator' => get_option( 'woocommerce_price_thousand_sep' ),
					'decimal_separator'  => get_option( 'woocommerce_price_decimal_sep' ),
					'price_num_decimals' => wc_get_price_decimals(),
					'tax_included'       => wc_prices_include_tax(),
					'weight_unit'        => get_option( 'woocommerce_weight_unit' ),
					'dimension_unit'     => get_option( 'woocommerce_dimension_unit' ),
					'ssl_enabled'        => ( 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) ),
					'permalinks_enabled' => ( '' !== get_option( 'permalink_structure' ) ),
					'generate_password'  => ( 'yes' === get_option( 'woocommerce_registration_generate_password' ) ),
					'links'              => array(
						'help' => 'https://woocommerce.github.io/woocommerce-rest-api-docs/',
					),
				),
			),
		);

		// Find the available routes
		foreach ( $this->get_routes() as $route => $callbacks ) {
			$data = array();

			$route = preg_replace( '#\(\?P(<\w+?>).*?\)#', '$1', $route );

			foreach ( self::$method_map as $name => $bitmask ) {
				foreach ( $callbacks as $callback ) {
					// Skip to the next route if any callback is hidden
					if ( $callback[1] & self::HIDDEN_ENDPOINT ) {
						continue 3;
					}

					if ( $callback[1] & $bitmask ) {
						$data['supports'][] = $name;
					}

					if ( $callback[1] & self::ACCEPT_DATA ) {
						$data['accepts_data'] = true;
					}

					// For non-variable routes, generate links
					if ( strpos( $route, '<' ) === false ) {
						$data['meta'] = array(
							'self' => get_woocommerce_api_url( $route ),
						);
					}
				}
			}

			$available['store']['routes'][ $route ] = apply_filters( 'woocommerce_api_endpoints_description', $data );
		}

		return apply_filters( 'woocommerce_api_index', $available );
	}

	/**
	 * Send a HTTP status code
	 *
	 * @since 2.1
	 * @param int $code HTTP status
	 */
	public function send_status( $code ) {
		status_header( $code );
	}

	/**
	 * Send a HTTP header
	 *
	 * @since 2.1
	 * @param string $key Header key
	 * @param string $value Header value
	 * @param boolean $replace Should we replace the existing header?
	 */
	public function header( $key, $value, $replace = true ) {
		header( sprintf( '%s: %s', $key, $value ), $replace );
	}

	/**
	 * Send a Link header
	 *
	 * @internal The $rel parameter is first, as this looks nicer when sending multiple
	 *
	 * @link http://tools.ietf.org/html/rfc5988
	 * @link http://www.iana.org/assignments/link-relations/link-relations.xml
	 *
	 * @since 2.1
	 * @param string $rel Link relation. Either a registered type, or an absolute URL
	 * @param string $link Target IRI for the link
	 * @param array $other Other parameters to send, as an associative array
	 */
	public function link_header( $rel, $link, $other = array() ) {

		$header = sprintf( '<%s>; rel="%s"', $link, esc_attr( $rel ) );

		foreach ( $other as $key => $value ) {

			if ( 'title' == $key ) {

				$value = '"' . $value . '"';
			}

			$header .= '; ' . $key . '=' . $value;
		}

		$this->header( 'Link', $header, false );
	}

	/**
	 * Send pagination headers for resources
	 *
	 * @since 2.1
	 * @param WP_Query|WP_User_Query|stdClass $query
	 */
	public function add_pagination_headers( $query ) {

		// WP_User_Query
		if ( is_a( $query, 'WP_User_Query' ) ) {

			$single      = count( $query->get_results() ) == 1;
			$total       = $query->get_total();

			if ( $query->get( 'number' ) > 0 ) {
				$page = ( $query->get( 'offset' ) / $query->get( 'number' ) ) + 1;
				$total_pages = ceil( $total / $query->get( 'number' ) );
			} else {
				$page = 1;
				$total_pages = 1;
			}
		} elseif ( is_a( $query, 'stdClass' ) ) {
			$page        = $query->page;
			$single      = $query->is_single;
			$total       = $query->total;
			$total_pages = $query->total_pages;

		// WP_Query
		} else {

			$page        = $query->get( 'paged' );
			$single      = $query->is_single();
			$total       = $query->found_posts;
			$total_pages = $query->max_num_pages;
		}

		if ( ! $page ) {
			$page = 1;
		}

		$next_page = absint( $page ) + 1;

		if ( ! $single ) {

			// first/prev
			if ( $page > 1 ) {
				$this->link_header( 'first', $this->get_paginated_url( 1 ) );
				$this->link_header( 'prev', $this->get_paginated_url( $page -1 ) );
			}

			// next
			if ( $next_page <= $total_pages ) {
				$this->link_header( 'next', $this->get_paginated_url( $next_page ) );
			}

			// last
			if ( $page != $total_pages ) {
				$this->link_header( 'last', $this->get_paginated_url( $total_pages ) );
			}
		}

		$this->header( 'X-WC-Total', $total );
		$this->header( 'X-WC-TotalPages', $total_pages );

		do_action( 'woocommerce_api_pagination_headers', $this, $query );
	}

	/**
	 * Returns the request URL with the page query parameter set to the specified page
	 *
	 * @since 2.1
	 * @param int $page
	 * @return string
	 */
	private function get_paginated_url( $page ) {

		// remove existing page query param
		$request = remove_query_arg( 'page' );

		// add provided page query param
		$request = urldecode( add_query_arg( 'page', $page, $request ) );

		// get the home host
		$host = parse_url( get_home_url(), PHP_URL_HOST );

		return set_url_scheme( "http://{$host}{$request}" );
	}

	/**
	 * Retrieve the raw request entity (body)
	 *
	 * @since 2.1
	 * @return string
	 */
	public function get_raw_data() {
		// @codingStandardsIgnoreStart
		// $HTTP_RAW_POST_DATA is deprecated on PHP 5.6.
		if ( function_exists( 'phpversion' ) && version_compare( phpversion(), '5.6', '>=' ) ) {
			return file_get_contents( 'php://input' );
		}

		global $HTTP_RAW_POST_DATA;

		// A bug in PHP < 5.2.2 makes $HTTP_RAW_POST_DATA not set by default,
		// but we can do it ourself.
		if ( ! isset( $HTTP_RAW_POST_DATA ) ) {
			$HTTP_RAW_POST_DATA = file_get_contents( 'php://input' );
		}

		return $HTTP_RAW_POST_DATA;
		// @codingStandardsIgnoreEnd
	}

	/**
	 * Parse an RFC3339 datetime into a MySQl datetime
	 *
	 * Invalid dates default to unix epoch
	 *
	 * @since 2.1
	 * @param string $datetime RFC3339 datetime
	 * @return string MySQl datetime (YYYY-MM-DD HH:MM:SS)
	 */
	public function parse_datetime( $datetime ) {

		// Strip millisecond precision (a full stop followed by one or more digits)
		if ( strpos( $datetime, '.' ) !== false ) {
			$datetime = preg_replace( '/\.\d+/', '', $datetime );
		}

		// default timezone to UTC
		$datetime = preg_replace( '/[+-]\d+:+\d+$/', '+00:00', $datetime );

		try {

			$datetime = new DateTime( $datetime, new DateTimeZone( 'UTC' ) );

		} catch ( Exception $e ) {

			$datetime = new DateTime( '@0' );

		}

		return $datetime->format( 'Y-m-d H:i:s' );
	}

	/**
	 * Format a unix timestamp or MySQL datetime into an RFC3339 datetime
	 *
	 * @since 2.1
	 * @param int|string $timestamp unix timestamp or MySQL datetime
	 * @param bool $convert_to_utc
	 * @param bool $convert_to_gmt Use GMT timezone.
	 * @return string RFC3339 datetime
	 */
	public function format_datetime( $timestamp, $convert_to_utc = false, $convert_to_gmt = false ) {
		if ( $convert_to_gmt ) {
			if ( is_numeric( $timestamp ) ) {
				$timestamp = date( 'Y-m-d H:i:s', $timestamp );
			}

			$timestamp = get_gmt_from_date( $timestamp );
		}

		if ( $convert_to_utc ) {
			$timezone = new DateTimeZone( wc_timezone_string() );
		} else {
			$timezone = new DateTimeZone( 'UTC' );
		}

		try {

			if ( is_numeric( $timestamp ) ) {
				$date = new DateTime( "@{$timestamp}" );
			} else {
				$date = new DateTime( $timestamp, $timezone );
			}

			// convert to UTC by adjusting the time based on the offset of the site's timezone
			if ( $convert_to_utc ) {
				$date->modify( -1 * $date->getOffset() . ' seconds' );
			}
		} catch ( Exception $e ) {

			$date = new DateTime( '@0' );
		}

		return $date->format( 'Y-m-d\TH:i:s\Z' );
	}

	/**
	 * Extract headers from a PHP-style $_SERVER array
	 *
	 * @since 2.1
	 * @param array $server Associative array similar to $_SERVER
	 * @return array Headers extracted from the input
	 */
	public function get_headers( $server ) {
		$headers = array();
		// CONTENT_* headers are not prefixed with HTTP_
		$additional = array( 'CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true );

		foreach ( $server as $key => $value ) {
			if ( strpos( $key, 'HTTP_' ) === 0 ) {
				$headers[ substr( $key, 5 ) ] = $value;
			} elseif ( isset( $additional[ $key ] ) ) {
				$headers[ $key ] = $value;
			}
		}

		return $headers;
	}
}
legacy/api/v2/class-wc-api-customers.php000064400000061142151336044770014144 0ustar00<?php
/**
 * WooCommerce API Customers Class
 *
 * Handles requests to the /customers endpoint
 *
 * @author   WooThemes
 * @category API
 * @package  WooCommerce\RestApi
 * @since    2.2
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WC_API_Customers extends WC_API_Resource {

	/** @var string $base the route base */
	protected $base = '/customers';

	/** @var string $created_at_min for date filtering */
	private $created_at_min = null;

	/** @var string $created_at_max for date filtering */
	private $created_at_max = null;

	/**
	 * Setup class, overridden to provide customer data to order response
	 *
	 * @since 2.1
	 * @param WC_API_Server $server
	 */
	public function __construct( WC_API_Server $server ) {

		parent::__construct( $server );

		// add customer data to order responses
		add_filter( 'woocommerce_api_order_response', array( $this, 'add_customer_data' ), 10, 2 );

		// modify WP_User_Query to support created_at date filtering
		add_action( 'pre_user_query', array( $this, 'modify_user_query' ) );
	}

	/**
	 * Register the routes for this class
	 *
	 * GET /customers
	 * GET /customers/count
	 * GET /customers/<id>
	 * GET /customers/<id>/orders
	 *
	 * @since 2.2
	 * @param array $routes
	 * @return array
	 */
	public function register_routes( $routes ) {

		# GET/POST /customers
		$routes[ $this->base ] = array(
			array( array( $this, 'get_customers' ),   WC_API_SERVER::READABLE ),
			array( array( $this, 'create_customer' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET /customers/count
		$routes[ $this->base . '/count' ] = array(
			array( array( $this, 'get_customers_count' ), WC_API_SERVER::READABLE ),
		);

		# GET/PUT/DELETE /customers/<id>
		$routes[ $this->base . '/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_customer' ),    WC_API_SERVER::READABLE ),
			array( array( $this, 'edit_customer' ),   WC_API_SERVER::EDITABLE | WC_API_SERVER::ACCEPT_DATA ),
			array( array( $this, 'delete_customer' ), WC_API_SERVER::DELETABLE ),
		);

		# GET /customers/email/<email>
		$routes[ $this->base . '/email/(?P<email>.+)' ] = array(
			array( array( $this, 'get_customer_by_email' ), WC_API_SERVER::READABLE ),
		);

		# GET /customers/<id>/orders
		$routes[ $this->base . '/(?P<id>\d+)/orders' ] = array(
			array( array( $this, 'get_customer_orders' ), WC_API_SERVER::READABLE ),
		);

		# GET /customers/<id>/downloads
		$routes[ $this->base . '/(?P<id>\d+)/downloads' ] = array(
			array( array( $this, 'get_customer_downloads' ), WC_API_SERVER::READABLE ),
		);

		# POST|PUT /customers/bulk
		$routes[ $this->base . '/bulk' ] = array(
			array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
		);

		return $routes;
	}

	/**
	 * Get all customers
	 *
	 * @since 2.1
	 * @param array $fields
	 * @param array $filter
	 * @param int $page
	 * @return array
	 */
	public function get_customers( $fields = null, $filter = array(), $page = 1 ) {

		$filter['page'] = $page;

		$query = $this->query_customers( $filter );

		$customers = array();

		foreach ( $query->get_results() as $user_id ) {

			if ( ! $this->is_readable( $user_id ) ) {
				continue;
			}

			$customers[] = current( $this->get_customer( $user_id, $fields ) );
		}

		$this->server->add_pagination_headers( $query );

		return array( 'customers' => $customers );
	}

	/**
	 * Get the customer for the given ID
	 *
	 * @since 2.1
	 * @param int $id the customer ID
	 * @param array $fields
	 * @return array|WP_Error
	 */
	public function get_customer( $id, $fields = null ) {
		global $wpdb;

		$id = $this->validate_request( $id, 'customer', 'read' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$customer      = new WC_Customer( $id );
		$last_order    = $customer->get_last_order();
		$customer_data = array(
			'id'               => $customer->get_id(),
			'created_at'       => $this->server->format_datetime( $customer->get_date_created() ? $customer->get_date_created()->getTimestamp() : 0 ), // API gives UTC times.
			'email'            => $customer->get_email(),
			'first_name'       => $customer->get_first_name(),
			'last_name'        => $customer->get_last_name(),
			'username'         => $customer->get_username(),
			'role'             => $customer->get_role(),
			'last_order_id'    => is_object( $last_order ) ? $last_order->get_id() : null,
			'last_order_date'  => is_object( $last_order ) ? $this->server->format_datetime( $last_order->get_date_created() ? $last_order->get_date_created()->getTimestamp() : 0 ) : null, // API gives UTC times.
			'orders_count'     => $customer->get_order_count(),
			'total_spent'      => wc_format_decimal( $customer->get_total_spent(), 2 ),
			'avatar_url'       => $customer->get_avatar_url(),
			'billing_address'  => array(
				'first_name' => $customer->get_billing_first_name(),
				'last_name'  => $customer->get_billing_last_name(),
				'company'    => $customer->get_billing_company(),
				'address_1'  => $customer->get_billing_address_1(),
				'address_2'  => $customer->get_billing_address_2(),
				'city'       => $customer->get_billing_city(),
				'state'      => $customer->get_billing_state(),
				'postcode'   => $customer->get_billing_postcode(),
				'country'    => $customer->get_billing_country(),
				'email'      => $customer->get_billing_email(),
				'phone'      => $customer->get_billing_phone(),
			),
			'shipping_address' => array(
				'first_name' => $customer->get_shipping_first_name(),
				'last_name'  => $customer->get_shipping_last_name(),
				'company'    => $customer->get_shipping_company(),
				'address_1'  => $customer->get_shipping_address_1(),
				'address_2'  => $customer->get_shipping_address_2(),
				'city'       => $customer->get_shipping_city(),
				'state'      => $customer->get_shipping_state(),
				'postcode'   => $customer->get_shipping_postcode(),
				'country'    => $customer->get_shipping_country(),
			),
		);

		return array( 'customer' => apply_filters( 'woocommerce_api_customer_response', $customer_data, $customer, $fields, $this->server ) );
	}

	/**
	 * Get the customer for the given email
	 *
	 * @since 2.1
	 *
	 * @param string $email the customer email
	 * @param array $fields
	 *
	 * @return array|WP_Error
	 */
	public function get_customer_by_email( $email, $fields = null ) {
		try {
			if ( is_email( $email ) ) {
				$customer = get_user_by( 'email', $email );
				if ( ! is_object( $customer ) ) {
					throw new WC_API_Exception( 'woocommerce_api_invalid_customer_email', __( 'Invalid customer email', 'woocommerce' ), 404 );
				}
			} else {
				throw new WC_API_Exception( 'woocommerce_api_invalid_customer_email', __( 'Invalid customer email', 'woocommerce' ), 404 );
			}

			return $this->get_customer( $customer->ID, $fields );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the total number of customers
	 *
	 * @since 2.1
	 *
	 * @param array $filter
	 *
	 * @return array|WP_Error
	 */
	public function get_customers_count( $filter = array() ) {
		try {
			if ( ! current_user_can( 'list_users' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_customers_count', __( 'You do not have permission to read the customers count', 'woocommerce' ), 401 );
			}

			$query = $this->query_customers( $filter );

			return array( 'count' => $query->get_total() );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get customer billing address fields.
	 *
	 * @since  2.2
	 * @return array
	 */
	protected function get_customer_billing_address() {
		$billing_address = apply_filters( 'woocommerce_api_customer_billing_address', array(
			'first_name',
			'last_name',
			'company',
			'address_1',
			'address_2',
			'city',
			'state',
			'postcode',
			'country',
			'email',
			'phone',
		) );

		return $billing_address;
	}

	/**
	 * Get customer shipping address fields.
	 *
	 * @since  2.2
	 * @return array
	 */
	protected function get_customer_shipping_address() {
		$shipping_address = apply_filters( 'woocommerce_api_customer_shipping_address', array(
			'first_name',
			'last_name',
			'company',
			'address_1',
			'address_2',
			'city',
			'state',
			'postcode',
			'country',
		) );

		return $shipping_address;
	}

	/**
	 * Add/Update customer data.
	 *
	 * @since 2.2
	 * @param int $id the customer ID
	 * @param array $data
	 * @param WC_Customer $customer
	 */
	protected function update_customer_data( $id, $data, $customer ) {

		// Customer first name.
		if ( isset( $data['first_name'] ) ) {
			$customer->set_first_name( wc_clean( $data['first_name'] ) );
		}

		// Customer last name.
		if ( isset( $data['last_name'] ) ) {
			$customer->set_last_name( wc_clean( $data['last_name'] ) );
		}

		// Customer billing address.
		if ( isset( $data['billing_address'] ) ) {
			foreach ( $this->get_customer_billing_address() as $field ) {
				if ( isset( $data['billing_address'][ $field ] ) ) {
					if ( is_callable( array( $customer, "set_billing_{$field}" ) ) ) {
						$customer->{"set_billing_{$field}"}( $data['billing_address'][ $field ] );
					} else {
						$customer->update_meta_data( 'billing_' . $field, wc_clean( $data['billing_address'][ $field ] ) );
					}
				}
			}
		}

		// Customer shipping address.
		if ( isset( $data['shipping_address'] ) ) {
			foreach ( $this->get_customer_shipping_address() as $field ) {
				if ( isset( $data['shipping_address'][ $field ] ) ) {
					if ( is_callable( array( $customer, "set_shipping_{$field}" ) ) ) {
						$customer->{"set_shipping_{$field}"}( $data['shipping_address'][ $field ] );
					} else {
						$customer->update_meta_data( 'shipping_' . $field, wc_clean( $data['shipping_address'][ $field ] ) );
					}
				}
			}
		}

		do_action( 'woocommerce_api_update_customer_data', $id, $data, $customer );
	}

	/**
	 * Create a customer
	 *
	 * @since 2.2
	 *
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function create_customer( $data ) {
		try {
			if ( ! isset( $data['customer'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_customer_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'customer' ), 400 );
			}

			$data = $data['customer'];

			// Checks with can create new users.
			if ( ! current_user_can( 'create_users' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_customer', __( 'You do not have permission to create this customer', 'woocommerce' ), 401 );
			}

			$data = apply_filters( 'woocommerce_api_create_customer_data', $data, $this );

			// Checks with the email is missing.
			if ( ! isset( $data['email'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_customer_email', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'email' ), 400 );
			}

			// Create customer.
			$customer = new WC_Customer;
			$customer->set_username( ! empty( $data['username'] ) ? $data['username'] : '' );
			$customer->set_password( ! empty( $data['password'] ) ? $data['password'] : '' );
			$customer->set_email( $data['email'] );
			$customer->save();

			if ( ! $customer->get_id() ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_customer', __( 'This resource cannot be created.', 'woocommerce' ), 400 );
			}

			// Added customer data.
			$this->update_customer_data( $customer->get_id(), $data, $customer );
			$customer->save();

			do_action( 'woocommerce_api_create_customer', $customer->get_id(), $data );

			$this->server->send_status( 201 );

			return $this->get_customer( $customer->get_id() );
		} catch ( Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit a customer
	 *
	 * @since 2.2
	 *
	 * @param int $id the customer ID
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function edit_customer( $id, $data ) {
		try {
			if ( ! isset( $data['customer'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_customer_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'customer' ), 400 );
			}

			$data = $data['customer'];

			// Validate the customer ID.
			$id = $this->validate_request( $id, 'customer', 'edit' );

			// Return the validate error.
			if ( is_wp_error( $id ) ) {
				throw new WC_API_Exception( $id->get_error_code(), $id->get_error_message(), 400 );
			}

			$data = apply_filters( 'woocommerce_api_edit_customer_data', $data, $this );

			$customer = new WC_Customer( $id );

			// Customer email.
			if ( isset( $data['email'] ) ) {
				$customer->set_email( $data['email'] );
			}

			// Customer password.
			if ( isset( $data['password'] ) ) {
				$customer->set_password( $data['password'] );
			}

			// Update customer data.
			$this->update_customer_data( $customer->get_id(), $data, $customer );

			$customer->save();

			do_action( 'woocommerce_api_edit_customer', $customer->get_id(), $data );

			return $this->get_customer( $customer->get_id() );
		} catch ( Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete a customer
	 *
	 * @since 2.2
	 * @param int $id the customer ID
	 * @return array|WP_Error
	 */
	public function delete_customer( $id ) {

		// Validate the customer ID.
		$id = $this->validate_request( $id, 'customer', 'delete' );

		// Return the validate error.
		if ( is_wp_error( $id ) ) {
			return $id;
		}

		do_action( 'woocommerce_api_delete_customer', $id, $this );

		return $this->delete( $id, 'customer' );
	}

	/**
	 * Get the orders for a customer
	 *
	 * @since 2.1
	 * @param int $id the customer ID
	 * @param string $fields fields to include in response
	 * @return array|WP_Error
	 */
	public function get_customer_orders( $id, $fields = null ) {
		global $wpdb;

		$id = $this->validate_request( $id, 'customer', 'read' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$order_ids = wc_get_orders( array(
			'customer' => $id,
			'limit'    => -1,
			'orderby'  => 'date',
			'order'    => 'ASC',
			'return'   => 'ids',
		) );

		if ( empty( $order_ids ) ) {
			return array( 'orders' => array() );
		}

		$orders = array();

		foreach ( $order_ids as $order_id ) {
			$orders[] = current( WC()->api->WC_API_Orders->get_order( $order_id, $fields ) );
		}

		return array( 'orders' => apply_filters( 'woocommerce_api_customer_orders_response', $orders, $id, $fields, $order_ids, $this->server ) );
	}

	/**
	 * Get the available downloads for a customer
	 *
	 * @since 2.2
	 * @param int $id the customer ID
	 * @param string $fields fields to include in response
	 * @return array|WP_Error
	 */
	public function get_customer_downloads( $id, $fields = null ) {
		$id = $this->validate_request( $id, 'customer', 'read' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$downloads  = array();
		$_downloads = wc_get_customer_available_downloads( $id );

		foreach ( $_downloads as $key => $download ) {
			$downloads[] = array(
				'download_url'        => $download['download_url'],
				'download_id'         => $download['download_id'],
				'product_id'          => $download['product_id'],
				'download_name'       => $download['download_name'],
				'order_id'            => $download['order_id'],
				'order_key'           => $download['order_key'],
				'downloads_remaining' => $download['downloads_remaining'],
				'access_expires'      => $download['access_expires'] ? $this->server->format_datetime( $download['access_expires'] ) : null,
				'file'                => $download['file'],
			);
		}

		return array( 'downloads' => apply_filters( 'woocommerce_api_customer_downloads_response', $downloads, $id, $fields, $this->server ) );
	}

	/**
	 * Helper method to get customer user objects
	 *
	 * Note that WP_User_Query does not have built-in pagination so limit & offset are used to provide limited
	 * pagination support
	 *
	 * The filter for role can only be a single role in a string.
	 *
	 * @since 2.3
	 * @param array $args request arguments for filtering query
	 * @return WP_User_Query
	 */
	private function query_customers( $args = array() ) {

		// default users per page
		$users_per_page = get_option( 'posts_per_page' );

		// Set base query arguments
		$query_args = array(
			'fields'  => 'ID',
			'role'    => 'customer',
			'orderby' => 'registered',
			'number'  => $users_per_page,
		);

		// Custom Role
		if ( ! empty( $args['role'] ) ) {
			$query_args['role'] = $args['role'];
		}

		// Search
		if ( ! empty( $args['q'] ) ) {
			$query_args['search'] = $args['q'];
		}

		// Limit number of users returned
		if ( ! empty( $args['limit'] ) ) {
			if ( -1 == $args['limit'] ) {
				unset( $query_args['number'] );
			} else {
				$query_args['number'] = absint( $args['limit'] );
				$users_per_page       = absint( $args['limit'] );
			}
		} else {
			$args['limit'] = $query_args['number'];
		}

		// Page
		$page = ( isset( $args['page'] ) ) ? absint( $args['page'] ) : 1;

		// Offset
		if ( ! empty( $args['offset'] ) ) {
			$query_args['offset'] = absint( $args['offset'] );
		} else {
			$query_args['offset'] = $users_per_page * ( $page - 1 );
		}

		// Created date
		if ( ! empty( $args['created_at_min'] ) ) {
			$this->created_at_min = $this->server->parse_datetime( $args['created_at_min'] );
		}

		if ( ! empty( $args['created_at_max'] ) ) {
			$this->created_at_max = $this->server->parse_datetime( $args['created_at_max'] );
		}

		// Order (ASC or DESC, ASC by default)
		if ( ! empty( $args['order'] ) ) {
			$query_args['order'] = $args['order'];
		}

		// Order by
		if ( ! empty( $args['orderby'] ) ) {
			$query_args['orderby'] = $args['orderby'];

			// Allow sorting by meta value
			if ( ! empty( $args['orderby_meta_key'] ) ) {
				$query_args['meta_key'] = $args['orderby_meta_key'];
			}
		}

		$query = new WP_User_Query( $query_args );

		// Helper members for pagination headers
		$query->total_pages = ( -1 == $args['limit'] ) ? 1 : ceil( $query->get_total() / $users_per_page );
		$query->page = $page;

		return $query;
	}

	/**
	 * Add customer data to orders
	 *
	 * @since 2.1
	 * @param $order_data
	 * @param $order
	 * @return array
	 */
	public function add_customer_data( $order_data, $order ) {

		if ( 0 == $order->get_user_id() ) {

			// add customer data from order
			$order_data['customer'] = array(
				'id'               => 0,
				'email'            => $order->get_billing_email(),
				'first_name'       => $order->get_billing_first_name(),
				'last_name'        => $order->get_billing_last_name(),
				'billing_address'  => array(
					'first_name' => $order->get_billing_first_name(),
					'last_name'  => $order->get_billing_last_name(),
					'company'    => $order->get_billing_company(),
					'address_1'  => $order->get_billing_address_1(),
					'address_2'  => $order->get_billing_address_2(),
					'city'       => $order->get_billing_city(),
					'state'      => $order->get_billing_state(),
					'postcode'   => $order->get_billing_postcode(),
					'country'    => $order->get_billing_country(),
					'email'      => $order->get_billing_email(),
					'phone'      => $order->get_billing_phone(),
				),
				'shipping_address' => array(
					'first_name' => $order->get_shipping_first_name(),
					'last_name'  => $order->get_shipping_last_name(),
					'company'    => $order->get_shipping_company(),
					'address_1'  => $order->get_shipping_address_1(),
					'address_2'  => $order->get_shipping_address_2(),
					'city'       => $order->get_shipping_city(),
					'state'      => $order->get_shipping_state(),
					'postcode'   => $order->get_shipping_postcode(),
					'country'    => $order->get_shipping_country(),
				),
			);

		} else {

			$order_data['customer'] = current( $this->get_customer( $order->get_user_id() ) );
		}

		return $order_data;
	}

	/**
	 * Modify the WP_User_Query to support filtering on the date the customer was created
	 *
	 * @since 2.1
	 * @param WP_User_Query $query
	 */
	public function modify_user_query( $query ) {

		if ( $this->created_at_min ) {
			$query->query_where .= sprintf( " AND user_registered >= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%H:%%i:%%s' )", esc_sql( $this->created_at_min ) );
		}

		if ( $this->created_at_max ) {
			$query->query_where .= sprintf( " AND user_registered <= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%H:%%i:%%s' )", esc_sql( $this->created_at_max ) );
		}
	}

	/**
	 * Validate the request by checking:
	 *
	 * 1) the ID is a valid integer
	 * 2) the ID returns a valid WP_User
	 * 3) the current user has the proper permissions
	 *
	 * @since 2.1
	 * @see WC_API_Resource::validate_request()
	 * @param integer $id the customer ID
	 * @param string $type the request type, unused because this method overrides the parent class
	 * @param string $context the context of the request, either `read`, `edit` or `delete`
	 * @return int|WP_Error valid user ID or WP_Error if any of the checks fails
	 */
	protected function validate_request( $id, $type, $context ) {

		try {
			$id = absint( $id );

			// validate ID
			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Invalid customer ID', 'woocommerce' ), 404 );
			}

			// non-existent IDs return a valid WP_User object with the user ID = 0
			$customer = new WP_User( $id );

			if ( 0 === $customer->ID ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_customer', __( 'Invalid customer', 'woocommerce' ), 404 );
			}

			// validate permissions
			switch ( $context ) {

				case 'read':
					if ( ! current_user_can( 'list_users' ) ) {
						throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_customer', __( 'You do not have permission to read this customer', 'woocommerce' ), 401 );
					}
					break;

				case 'edit':
					if ( ! wc_rest_check_user_permissions( 'edit', $customer->ID ) ) {
						throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_customer', __( 'You do not have permission to edit this customer', 'woocommerce' ), 401 );
					}
					break;

				case 'delete':
					if ( ! wc_rest_check_user_permissions( 'delete', $customer->ID ) ) {
						throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_customer', __( 'You do not have permission to delete this customer', 'woocommerce' ), 401 );
					}
					break;
			}

			return $id;
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Check if the current user can read users
	 *
	 * @since 2.1
	 * @see WC_API_Resource::is_readable()
	 * @param int|WP_Post $post unused
	 * @return bool true if the current user can read users, false otherwise
	 */
	protected function is_readable( $post ) {
		return current_user_can( 'list_users' );
	}

	/**
	 * Bulk update or insert customers
	 * Accepts an array with customers in the formats supported by
	 * WC_API_Customers->create_customer() and WC_API_Customers->edit_customer()
	 *
	 * @since 2.4.0
	 *
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function bulk( $data ) {

		try {
			if ( ! isset( $data['customers'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_customers_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'customers' ), 400 );
			}

			$data  = $data['customers'];
			$limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'customers' );

			// Limit bulk operation
			if ( count( $data ) > $limit ) {
				throw new WC_API_Exception( 'woocommerce_api_customers_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 );
			}

			$customers = array();

			foreach ( $data as $_customer ) {
				$customer_id = 0;

				// Try to get the customer ID
				if ( isset( $_customer['id'] ) ) {
					$customer_id = intval( $_customer['id'] );
				}

				// Customer exists / edit customer
				if ( $customer_id ) {
					$edit = $this->edit_customer( $customer_id, array( 'customer' => $_customer ) );

					if ( is_wp_error( $edit ) ) {
						$customers[] = array(
							'id'    => $customer_id,
							'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ),
						);
					} else {
						$customers[] = $edit['customer'];
					}
				} else {
					// Customer don't exists / create customer
					$new = $this->create_customer( array( 'customer' => $_customer ) );

					if ( is_wp_error( $new ) ) {
						$customers[] = array(
							'id'    => $customer_id,
							'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ),
						);
					} else {
						$customers[] = $new['customer'];
					}
				}
			}

			return array( 'customers' => apply_filters( 'woocommerce_api_customers_bulk_response', $customers, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}
}
legacy/api/v2/class-wc-api-exception.php000064400000002213151336044770014110 0ustar00<?php
/**
 * WooCommerce API Exception Class
 *
 * Extends Exception to provide additional data
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.2
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WC_API_Exception extends Exception {

	/** @var string sanitized error code */
	protected $error_code;

	/**
	 * Setup exception, requires 3 params:
	 *
	 * error code - machine-readable, e.g. `woocommerce_invalid_product_id`
	 * error message - friendly message, e.g. 'Product ID is invalid'
	 * http status code - proper HTTP status code to respond with, e.g. 400
	 *
	 * @since 2.2
	 * @param string $error_code
	 * @param string $error_message user-friendly translated error message
	 * @param int $http_status_code HTTP status code to respond with
	 */
	public function __construct( $error_code, $error_message, $http_status_code ) {
		$this->error_code = $error_code;
		parent::__construct( $error_message, $http_status_code );
	}

	/**
	 * Returns the error code
	 *
	 * @since 2.2
	 * @return string
	 */
	public function getErrorCode() {
		return $this->error_code;
	}
}
legacy/api/v2/class-wc-api-webhooks.php000064400000036334151336044770013746 0ustar00<?php
/**
 * WooCommerce API Webhooks class
 *
 * Handles requests to the /webhooks endpoint
 *
 * @author   WooThemes
 * @category API
 * @package  WooCommerce\RestApi
 * @since    2.2
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WC_API_Webhooks extends WC_API_Resource {

	/** @var string $base the route base */
	protected $base = '/webhooks';

	/**
	 * Register the routes for this class
	 *
	 * @since 2.2
	 * @param array $routes
	 * @return array
	 */
	public function register_routes( $routes ) {

		# GET|POST /webhooks
		$routes[ $this->base ] = array(
			array( array( $this, 'get_webhooks' ),     WC_API_Server::READABLE ),
			array( array( $this, 'create_webhook' ),   WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET /webhooks/count
		$routes[ $this->base . '/count' ] = array(
			array( array( $this, 'get_webhooks_count' ), WC_API_Server::READABLE ),
		);

		# GET|PUT|DELETE /webhooks/<id>
		$routes[ $this->base . '/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_webhook' ),  WC_API_Server::READABLE ),
			array( array( $this, 'edit_webhook' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
			array( array( $this, 'delete_webhook' ), WC_API_Server::DELETABLE ),
		);

		# GET /webhooks/<id>/deliveries
		$routes[ $this->base . '/(?P<webhook_id>\d+)/deliveries' ] = array(
			array( array( $this, 'get_webhook_deliveries' ), WC_API_Server::READABLE ),
		);

		# GET /webhooks/<webhook_id>/deliveries/<id>
		$routes[ $this->base . '/(?P<webhook_id>\d+)/deliveries/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_webhook_delivery' ), WC_API_Server::READABLE ),
		);

		return $routes;
	}

	/**
	 * Get all webhooks
	 *
	 * @since 2.2
	 *
	 * @param array $fields
	 * @param array $filter
	 * @param string $status
	 * @param int $page
	 *
	 * @return array
	 */
	public function get_webhooks( $fields = null, $filter = array(), $status = null, $page = 1 ) {

		if ( ! empty( $status ) ) {
			$filter['status'] = $status;
		}

		$filter['page'] = $page;

		$query = $this->query_webhooks( $filter );

		$webhooks = array();

		foreach ( $query['results'] as $webhook_id ) {
			$webhooks[] = current( $this->get_webhook( $webhook_id, $fields ) );
		}

		$this->server->add_pagination_headers( $query['headers'] );

		return array( 'webhooks' => $webhooks );
	}

	/**
	 * Get the webhook for the given ID
	 *
	 * @since 2.2
	 * @param int $id webhook ID
	 * @param array $fields
	 * @return array|WP_Error
	 */
	public function get_webhook( $id, $fields = null ) {

		// ensure webhook ID is valid & user has permission to read
		$id = $this->validate_request( $id, 'shop_webhook', 'read' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$webhook = wc_get_webhook( $id );

		$webhook_data = array(
			'id'           => $webhook->get_id(),
			'name'         => $webhook->get_name(),
			'status'       => $webhook->get_status(),
			'topic'        => $webhook->get_topic(),
			'resource'     => $webhook->get_resource(),
			'event'        => $webhook->get_event(),
			'hooks'        => $webhook->get_hooks(),
			'delivery_url' => $webhook->get_delivery_url(),
			'created_at'   => $this->server->format_datetime( $webhook->get_date_created() ? $webhook->get_date_created()->getTimestamp() : 0, false, false ), // API gives UTC times.
			'updated_at'   => $this->server->format_datetime( $webhook->get_date_modified() ? $webhook->get_date_modified()->getTimestamp() : 0, false, false ), // API gives UTC times.
		);

		return array( 'webhook' => apply_filters( 'woocommerce_api_webhook_response', $webhook_data, $webhook, $fields, $this ) );
	}

	/**
	 * Get the total number of webhooks
	 *
	 * @since 2.2
	 *
	 * @param string $status
	 * @param array $filter
	 *
	 * @return array|WP_Error
	 */
	public function get_webhooks_count( $status = null, $filter = array() ) {
		try {
			if ( ! current_user_can( 'manage_woocommerce' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_webhooks_count', __( 'You do not have permission to read the webhooks count', 'woocommerce' ), 401 );
			}

			if ( ! empty( $status ) ) {
				$filter['status'] = $status;
			}

			$query = $this->query_webhooks( $filter );

			return array( 'count' => $query['headers']->total );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create an webhook
	 *
	 * @since 2.2
	 *
	 * @param array $data parsed webhook data
	 *
	 * @return array|WP_Error
	 */
	public function create_webhook( $data ) {

		try {
			if ( ! isset( $data['webhook'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_webhook_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'webhook' ), 400 );
			}

			$data = $data['webhook'];

			// permission check
			if ( ! current_user_can( 'manage_woocommerce' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_webhooks', __( 'You do not have permission to create webhooks.', 'woocommerce' ), 401 );
			}

			$data = apply_filters( 'woocommerce_api_create_webhook_data', $data, $this );

			// validate topic
			if ( empty( $data['topic'] ) || ! wc_is_webhook_valid_topic( strtolower( $data['topic'] ) ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_topic', __( 'Webhook topic is required and must be valid.', 'woocommerce' ), 400 );
			}

			// validate delivery URL
			if ( empty( $data['delivery_url'] ) || ! wc_is_valid_url( $data['delivery_url'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_url', __( 'Webhook delivery URL must be a valid URL starting with http:// or https://', 'woocommerce' ), 400 );
			}

			$webhook_data = apply_filters( 'woocommerce_new_webhook_data', array(
				'post_type'     => 'shop_webhook',
				'post_status'   => 'publish',
				'ping_status'   => 'closed',
				'post_author'   => get_current_user_id(),
				'post_password' => 'webhook_' . wp_generate_password(),
				'post_title'    => ! empty( $data['name'] ) ? $data['name'] : sprintf( __( 'Webhook created on %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Webhook created on date parsed by strftime', 'woocommerce' ) ) ),
			), $data, $this );

			$webhook = new WC_Webhook();

			$webhook->set_name( $webhook_data['post_title'] );
			$webhook->set_user_id( $webhook_data['post_author'] );
			$webhook->set_status( 'publish' === $webhook_data['post_status'] ? 'active' : 'disabled' );
			$webhook->set_topic( $data['topic'] );
			$webhook->set_delivery_url( $data['delivery_url'] );
			$webhook->set_secret( ! empty( $data['secret'] ) ? $data['secret'] : wp_generate_password( 50, true, true ) );
			$webhook->set_api_version( 'legacy_v3' );
			$webhook->save();

			$webhook->deliver_ping();

			// HTTP 201 Created
			$this->server->send_status( 201 );

			do_action( 'woocommerce_api_create_webhook', $webhook->get_id(), $this );

			return $this->get_webhook( $webhook->get_id() );

		} catch ( WC_API_Exception $e ) {

			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit a webhook
	 *
	 * @since 2.2
	 *
	 * @param int $id webhook ID
	 * @param array $data parsed webhook data
	 *
	 * @return array|WP_Error
	 */
	public function edit_webhook( $id, $data ) {

		try {
			if ( ! isset( $data['webhook'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_webhook_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'webhook' ), 400 );
			}

			$data = $data['webhook'];

			$id = $this->validate_request( $id, 'shop_webhook', 'edit' );

			if ( is_wp_error( $id ) ) {
				return $id;
			}

			$data = apply_filters( 'woocommerce_api_edit_webhook_data', $data, $id, $this );

			$webhook = wc_get_webhook( $id );

			// update topic
			if ( ! empty( $data['topic'] ) ) {

				if ( wc_is_webhook_valid_topic( strtolower( $data['topic'] ) ) ) {

					$webhook->set_topic( $data['topic'] );

				} else {
					throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_topic', __( 'Webhook topic must be valid.', 'woocommerce' ), 400 );
				}
			}

			// update delivery URL
			if ( ! empty( $data['delivery_url'] ) ) {
				if ( wc_is_valid_url( $data['delivery_url'] ) ) {

					$webhook->set_delivery_url( $data['delivery_url'] );

				} else {
					throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_url', __( 'Webhook delivery URL must be a valid URL starting with http:// or https://', 'woocommerce' ), 400 );
				}
			}

			// update secret
			if ( ! empty( $data['secret'] ) ) {
				$webhook->set_secret( $data['secret'] );
			}

			// update status
			if ( ! empty( $data['status'] ) ) {
				$webhook->set_status( $data['status'] );
			}

			// update name
			if ( ! empty( $data['name'] ) ) {
				$webhook->set_name( $data['name'] );
			}

			$webhook->save();

			do_action( 'woocommerce_api_edit_webhook', $webhook->get_id(), $this );

			return $this->get_webhook( $webhook->get_id() );

		} catch ( WC_API_Exception $e ) {

			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete a webhook
	 *
	 * @since 2.2
	 * @param int $id webhook ID
	 * @return array|WP_Error
	 */
	public function delete_webhook( $id ) {

		$id = $this->validate_request( $id, 'shop_webhook', 'delete' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		do_action( 'woocommerce_api_delete_webhook', $id, $this );

		$webhook = wc_get_webhook( $id );

		return $webhook->delete( true );
	}

	/**
	 * Helper method to get webhook post objects
	 *
	 * @since 2.2
	 * @param array $args Request arguments for filtering query.
	 * @return array
	 */
	private function query_webhooks( $args ) {
		$args = $this->merge_query_args( array(), $args );

		$args['limit'] = isset( $args['posts_per_page'] ) ? intval( $args['posts_per_page'] ) : intval( get_option( 'posts_per_page' ) );

		if ( empty( $args['offset'] ) ) {
			$args['offset'] = 1 < $args['paged'] ? ( $args['paged'] - 1 ) * $args['limit'] : 0;
		}

		$page = $args['paged'];
		unset( $args['paged'], $args['posts_per_page'] );

		if ( isset( $args['s'] ) ) {
			$args['search'] = $args['s'];
			unset( $args['s'] );
		}

		// Post type to webhook status.
		if ( ! empty( $args['post_status'] ) ) {
			$args['status'] = $args['post_status'];
			unset( $args['post_status'] );
		}

		if ( ! empty( $args['post__in'] ) ) {
			$args['include'] = $args['post__in'];
			unset( $args['post__in'] );
		}

		if ( ! empty( $args['date_query'] ) ) {
			foreach ( $args['date_query'] as $date_query ) {
				if ( 'post_date_gmt' === $date_query['column'] ) {
					$args['after']  = isset( $date_query['after'] ) ? $date_query['after'] : null;
					$args['before'] = isset( $date_query['before'] ) ? $date_query['before'] : null;
				} elseif ( 'post_modified_gmt' === $date_query['column'] ) {
					$args['modified_after']  = isset( $date_query['after'] ) ? $date_query['after'] : null;
					$args['modified_before'] = isset( $date_query['before'] ) ? $date_query['before'] : null;
				}
			}

			unset( $args['date_query'] );
		}

		$args['paginate'] = true;

		// Get the webhooks.
		$data_store = WC_Data_Store::load( 'webhook' );
		$results    = $data_store->search_webhooks( $args );

		// Get total items.
		$headers              = new stdClass;
		$headers->page        = $page;
		$headers->total       = $results->total;
		$headers->is_single   = $args['limit'] > $headers->total;
		$headers->total_pages = $results->max_num_pages;

		return array(
			'results' => $results->webhooks,
			'headers' => $headers,
		);
	}

	/**
	 * Get deliveries for a webhook
	 *
	 * @since 2.2
	 * @deprecated 3.3.0 Webhooks deliveries logs now uses logging system.
	 * @param string $webhook_id webhook ID
	 * @param string|null $fields fields to include in response
	 * @return array|WP_Error
	 */
	public function get_webhook_deliveries( $webhook_id, $fields = null ) {

		// Ensure ID is valid webhook ID
		$webhook_id = $this->validate_request( $webhook_id, 'shop_webhook', 'read' );

		if ( is_wp_error( $webhook_id ) ) {
			return $webhook_id;
		}

		return array( 'webhook_deliveries' => array() );
	}

	/**
	 * Get the delivery log for the given webhook ID and delivery ID
	 *
	 * @since 2.2
	 * @deprecated 3.3.0 Webhooks deliveries logs now uses logging system.
	 * @param string $webhook_id webhook ID
	 * @param string $id delivery log ID
	 * @param string|null $fields fields to limit response to
	 *
	 * @return array|WP_Error
	 */
	public function get_webhook_delivery( $webhook_id, $id, $fields = null ) {
		try {
			// Validate webhook ID
			$webhook_id = $this->validate_request( $webhook_id, 'shop_webhook', 'read' );

			if ( is_wp_error( $webhook_id ) ) {
				return $webhook_id;
			}

			$id = absint( $id );

			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery ID.', 'woocommerce' ), 404 );
			}

			$webhook = new WC_Webhook( $webhook_id );

			$log = 0;

			if ( ! $log ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery.', 'woocommerce' ), 400 );
			}

			return array( 'webhook_delivery' => apply_filters( 'woocommerce_api_webhook_delivery_response', array(), $id, $fields, $log, $webhook_id, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Validate the request by checking:
	 *
	 * 1) the ID is a valid integer.
	 * 2) the ID returns a valid post object and matches the provided post type.
	 * 3) the current user has the proper permissions to read/edit/delete the post.
	 *
	 * @since 3.3.0
	 * @param string|int $id The post ID
	 * @param string $type The post type, either `shop_order`, `shop_coupon`, or `product`.
	 * @param string $context The context of the request, either `read`, `edit` or `delete`.
	 * @return int|WP_Error Valid post ID or WP_Error if any of the checks fails.
	 */
	protected function validate_request( $id, $type, $context ) {
		$id = absint( $id );

		// Validate ID.
		if ( empty( $id ) ) {
			return new WP_Error( "woocommerce_api_invalid_webhook_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) );
		}

		$webhook = wc_get_webhook( $id );

		if ( null === $webhook ) {
			return new WP_Error( "woocommerce_api_no_webhook_found", sprintf( __( 'No %1$s found with the ID equal to %2$s', 'woocommerce' ), 'webhook', $id ), array( 'status' => 404 ) );
		}

		// Validate permissions.
		switch ( $context ) {

			case 'read':
				if ( ! current_user_can( 'manage_woocommerce' ) ) {
					return new WP_Error( "woocommerce_api_user_cannot_read_webhook", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) );
				}
				break;

			case 'edit':
				if ( ! current_user_can( 'manage_woocommerce' ) ) {
					return new WP_Error( "woocommerce_api_user_cannot_edit_webhook", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) );
				}
				break;

			case 'delete':
				if ( ! current_user_can( 'manage_woocommerce' ) ) {
					return new WP_Error( "woocommerce_api_user_cannot_delete_webhook", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) );
				}
				break;
		}

		return $id;
	}
}
legacy/api/v2/class-wc-api-reports.php000064400000023071151336044770013615 0ustar00<?php
/**
 * WooCommerce API Reports Class
 *
 * Handles requests to the /reports endpoint
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WC_API_Reports extends WC_API_Resource {

	/** @var string $base the route base */
	protected $base = '/reports';

	/** @var WC_Admin_Report instance */
	private $report;

	/**
	 * Register the routes for this class
	 *
	 * GET /reports
	 * GET /reports/sales
	 *
	 * @since 2.1
	 * @param array $routes
	 * @return array
	 */
	public function register_routes( $routes ) {

		# GET /reports
		$routes[ $this->base ] = array(
			array( array( $this, 'get_reports' ),     WC_API_Server::READABLE ),
		);

		# GET /reports/sales
		$routes[ $this->base . '/sales' ] = array(
			array( array( $this, 'get_sales_report' ), WC_API_Server::READABLE ),
		);

		# GET /reports/sales/top_sellers
		$routes[ $this->base . '/sales/top_sellers' ] = array(
			array( array( $this, 'get_top_sellers_report' ), WC_API_Server::READABLE ),
		);

		return $routes;
	}

	/**
	 * Get a simple listing of available reports
	 *
	 * @since 2.1
	 * @return array
	 */
	public function get_reports() {
		return array( 'reports' => array( 'sales', 'sales/top_sellers' ) );
	}

	/**
	 * Get the sales report
	 *
	 * @since 2.1
	 * @param string $fields fields to include in response
	 * @param array $filter date filtering
	 * @return array|WP_Error
	 */
	public function get_sales_report( $fields = null, $filter = array() ) {

		// check user permissions
		$check = $this->validate_request();

		// check for WP_Error
		if ( is_wp_error( $check ) ) {
			return $check;
		}

		// set date filtering
		$this->setup_report( $filter );

		// new customers
		$users_query = new WP_User_Query(
			array(
				'fields' => array( 'user_registered' ),
				'role'   => 'customer',
			)
		);

		$customers = $users_query->get_results();

		foreach ( $customers as $key => $customer ) {
			if ( strtotime( $customer->user_registered ) < $this->report->start_date || strtotime( $customer->user_registered ) > $this->report->end_date ) {
				unset( $customers[ $key ] );
			}
		}

		$total_customers = count( $customers );
		$report_data     = $this->report->get_report_data();
		$period_totals   = array();

		// setup period totals by ensuring each period in the interval has data
		for ( $i = 0; $i <= $this->report->chart_interval; $i ++ ) {

			switch ( $this->report->chart_groupby ) {
				case 'day' :
					$time = date( 'Y-m-d', strtotime( "+{$i} DAY", $this->report->start_date ) );
					break;
				default :
					$time = date( 'Y-m', strtotime( "+{$i} MONTH", $this->report->start_date ) );
					break;
			}

			// set the customer signups for each period
			$customer_count = 0;
			foreach ( $customers as $customer ) {
				if ( date( ( 'day' == $this->report->chart_groupby ) ? 'Y-m-d' : 'Y-m', strtotime( $customer->user_registered ) ) == $time ) {
					$customer_count++;
				}
 			}

			$period_totals[ $time ] = array(
				'sales'     => wc_format_decimal( 0.00, 2 ),
				'orders'    => 0,
				'items'     => 0,
				'tax'       => wc_format_decimal( 0.00, 2 ),
				'shipping'  => wc_format_decimal( 0.00, 2 ),
				'discount'  => wc_format_decimal( 0.00, 2 ),
				'customers' => $customer_count,
			);
		}

		// add total sales, total order count, total tax and total shipping for each period
		foreach ( $report_data->orders as $order ) {
			$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) );

			if ( ! isset( $period_totals[ $time ] ) ) {
				continue;
			}

			$period_totals[ $time ]['sales']    = wc_format_decimal( $order->total_sales, 2 );
			$period_totals[ $time ]['tax']      = wc_format_decimal( $order->total_tax + $order->total_shipping_tax, 2 );
			$period_totals[ $time ]['shipping'] = wc_format_decimal( $order->total_shipping, 2 );
		}

		foreach ( $report_data->order_counts as $order ) {
			$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) );

			if ( ! isset( $period_totals[ $time ] ) ) {
				continue;
			}

			$period_totals[ $time ]['orders']   = (int) $order->count;
		}

		// add total order items for each period
		foreach ( $report_data->order_items as $order_item ) {
			$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order_item->post_date ) ) : date( 'Y-m', strtotime( $order_item->post_date ) );

			if ( ! isset( $period_totals[ $time ] ) ) {
				continue;
			}

			$period_totals[ $time ]['items'] = (int) $order_item->order_item_count;
		}

		// add total discount for each period
		foreach ( $report_data->coupons as $discount ) {
			$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $discount->post_date ) ) : date( 'Y-m', strtotime( $discount->post_date ) );

			if ( ! isset( $period_totals[ $time ] ) ) {
				continue;
			}

			$period_totals[ $time ]['discount'] = wc_format_decimal( $discount->discount_amount, 2 );
		}

		$sales_data  = array(
			'total_sales'       => $report_data->total_sales,
			'net_sales'         => $report_data->net_sales,
			'average_sales'     => $report_data->average_sales,
			'total_orders'      => $report_data->total_orders,
			'total_items'       => $report_data->total_items,
			'total_tax'         => wc_format_decimal( $report_data->total_tax + $report_data->total_shipping_tax, 2 ),
			'total_shipping'    => $report_data->total_shipping,
			'total_refunds'     => $report_data->total_refunds,
			'total_discount'    => $report_data->total_coupons,
			'totals_grouped_by' => $this->report->chart_groupby,
			'totals'            => $period_totals,
			'total_customers'   => $total_customers,
		);

		return array( 'sales' => apply_filters( 'woocommerce_api_report_response', $sales_data, $this->report, $fields, $this->server ) );
	}

	/**
	 * Get the top sellers report
	 *
	 * @since 2.1
	 * @param string $fields fields to include in response
	 * @param array $filter date filtering
	 * @return array|WP_Error
	 */
	public function get_top_sellers_report( $fields = null, $filter = array() ) {

		// check user permissions
		$check = $this->validate_request();

		if ( is_wp_error( $check ) ) {
			return $check;
		}

		// set date filtering
		$this->setup_report( $filter );

		$top_sellers = $this->report->get_order_report_data( array(
			'data' => array(
				'_product_id' => array(
					'type'            => 'order_item_meta',
					'order_item_type' => 'line_item',
					'function'        => '',
					'name'            => 'product_id',
				),
				'_qty' => array(
					'type'            => 'order_item_meta',
					'order_item_type' => 'line_item',
					'function'        => 'SUM',
					'name'            => 'order_item_qty',
				),
			),
			'order_by'     => 'order_item_qty DESC',
			'group_by'     => 'product_id',
			'limit'        => isset( $filter['limit'] ) ? absint( $filter['limit'] ) : 12,
			'query_type'   => 'get_results',
			'filter_range' => true,
		) );

		$top_sellers_data = array();

		foreach ( $top_sellers as $top_seller ) {

			$product = wc_get_product( $top_seller->product_id );

			if ( $product ) {
				$top_sellers_data[] = array(
					'title'      => $product->get_name(),
					'product_id' => $top_seller->product_id,
					'quantity'   => $top_seller->order_item_qty,
				);
			}
		}

		return array( 'top_sellers' => apply_filters( 'woocommerce_api_report_response', $top_sellers_data, $this->report, $fields, $this->server ) );
	}

	/**
	 * Setup the report object and parse any date filtering
	 *
	 * @since 2.1
	 * @param array $filter date filtering
	 */
	private function setup_report( $filter ) {

		include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' );
		include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-report-sales-by-date.php' );

		$this->report = new WC_Report_Sales_By_Date();

		if ( empty( $filter['period'] ) ) {

			// custom date range
			$filter['period'] = 'custom';

			if ( ! empty( $filter['date_min'] ) || ! empty( $filter['date_max'] ) ) {

				// overwrite _GET to make use of WC_Admin_Report::calculate_current_range() for custom date ranges
				$_GET['start_date'] = $this->server->parse_datetime( $filter['date_min'] );
				$_GET['end_date'] = isset( $filter['date_max'] ) ? $this->server->parse_datetime( $filter['date_max'] ) : null;

			} else {

				// default custom range to today
				$_GET['start_date'] = $_GET['end_date'] = date( 'Y-m-d', current_time( 'timestamp' ) );
			}
		} else {

			// ensure period is valid
			if ( ! in_array( $filter['period'], array( 'week', 'month', 'last_month', 'year' ) ) ) {
				$filter['period'] = 'week';
			}

			// TODO: change WC_Admin_Report class to use "week" instead, as it's more consistent with other periods
			// allow "week" for period instead of "7day"
			if ( 'week' === $filter['period'] ) {
				$filter['period'] = '7day';
			}
		}

		$this->report->calculate_current_range( $filter['period'] );
	}

	/**
	 * Verify that the current user has permission to view reports
	 *
	 * @since 2.1
	 * @see WC_API_Resource::validate_request()
	 *
	 * @param null $id unused
	 * @param null $type unused
	 * @param null $context unused
	 *
	 * @return bool|WP_Error
	 */
	protected function validate_request( $id = null, $type = null, $context = null ) {

		if ( ! current_user_can( 'view_woocommerce_reports' ) ) {

			return new WP_Error( 'woocommerce_api_user_cannot_read_report', __( 'You do not have permission to read this report', 'woocommerce' ), array( 'status' => 401 ) );

		} else {

			return true;
		}
	}
}
legacy/api/v2/class-wc-api-orders.php000064400000167337151336044770013433 0ustar00<?php
/**
 * WooCommerce API Orders Class
 *
 * Handles requests to the /orders endpoint
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WC_API_Orders extends WC_API_Resource {

	/** @var string $base the route base */
	protected $base = '/orders';

	/** @var string $post_type the custom post type */
	protected $post_type = 'shop_order';

	/**
	 * Register the routes for this class
	 *
	 * GET|POST /orders
	 * GET /orders/count
	 * GET|PUT|DELETE /orders/<id>
	 * GET /orders/<id>/notes
	 *
	 * @since 2.1
	 * @param array $routes
	 * @return array
	 */
	public function register_routes( $routes ) {

		# GET|POST /orders
		$routes[ $this->base ] = array(
			array( array( $this, 'get_orders' ),     WC_API_Server::READABLE ),
			array( array( $this, 'create_order' ),   WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET /orders/count
		$routes[ $this->base . '/count' ] = array(
			array( array( $this, 'get_orders_count' ), WC_API_Server::READABLE ),
		);

		# GET /orders/statuses
		$routes[ $this->base . '/statuses' ] = array(
			array( array( $this, 'get_order_statuses' ), WC_API_Server::READABLE ),
		);

		# GET|PUT|DELETE /orders/<id>
		$routes[ $this->base . '/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_order' ),  WC_API_Server::READABLE ),
			array( array( $this, 'edit_order' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
			array( array( $this, 'delete_order' ), WC_API_Server::DELETABLE ),
		);

		# GET|POST /orders/<id>/notes
		$routes[ $this->base . '/(?P<order_id>\d+)/notes' ] = array(
			array( array( $this, 'get_order_notes' ), WC_API_Server::READABLE ),
			array( array( $this, 'create_order_note' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET|PUT|DELETE /orders/<order_id>/notes/<id>
		$routes[ $this->base . '/(?P<order_id>\d+)/notes/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_order_note' ), WC_API_Server::READABLE ),
			array( array( $this, 'edit_order_note' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ),
			array( array( $this, 'delete_order_note' ), WC_API_SERVER::DELETABLE ),
		);

		# GET|POST /orders/<order_id>/refunds
		$routes[ $this->base . '/(?P<order_id>\d+)/refunds' ] = array(
			array( array( $this, 'get_order_refunds' ), WC_API_Server::READABLE ),
			array( array( $this, 'create_order_refund' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET|PUT|DELETE /orders/<order_id>/refunds/<id>
		$routes[ $this->base . '/(?P<order_id>\d+)/refunds/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_order_refund' ), WC_API_Server::READABLE ),
			array( array( $this, 'edit_order_refund' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ),
			array( array( $this, 'delete_order_refund' ), WC_API_SERVER::DELETABLE ),
		);

		# POST|PUT /orders/bulk
		$routes[ $this->base . '/bulk' ] = array(
			array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
		);

		return $routes;
	}

	/**
	 * Get all orders
	 *
	 * @since 2.1
	 * @param string $fields
	 * @param array $filter
	 * @param string $status
	 * @param int $page
	 * @return array
	 */
	public function get_orders( $fields = null, $filter = array(), $status = null, $page = 1 ) {

		if ( ! empty( $status ) ) {
			$filter['status'] = $status;
		}

		$filter['page'] = $page;

		$query = $this->query_orders( $filter );

		$orders = array();

		foreach ( $query->posts as $order_id ) {

			if ( ! $this->is_readable( $order_id ) ) {
				continue;
			}

			$orders[] = current( $this->get_order( $order_id, $fields, $filter ) );
		}

		$this->server->add_pagination_headers( $query );

		return array( 'orders' => $orders );
	}


	/**
	 * Get the order for the given ID
	 *
	 * @since 2.1
	 * @param int $id the order ID
	 * @param array $fields
	 * @param array $filter
	 * @return array|WP_Error
	 */
	public function get_order( $id, $fields = null, $filter = array() ) {

		// ensure order ID is valid & user has permission to read
		$id = $this->validate_request( $id, $this->post_type, 'read' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		// Get the decimal precession
		$dp         = ( isset( $filter['dp'] ) ? intval( $filter['dp'] ) : 2 );
		$order      = wc_get_order( $id );
		$order_data = array(
			'id'                        => $order->get_id(),
			'order_number'              => $order->get_order_number(),
			'created_at'                => $this->server->format_datetime( $order->get_date_created() ? $order->get_date_created()->getTimestamp() : 0, false, false ), // API gives UTC times.
			'updated_at'                => $this->server->format_datetime( $order->get_date_modified() ? $order->get_date_modified()->getTimestamp() : 0, false, false ), // API gives UTC times.
			'completed_at'              => $this->server->format_datetime( $order->get_date_completed() ? $order->get_date_completed()->getTimestamp() : 0, false, false ), // API gives UTC times.
			'status'                    => $order->get_status(),
			'currency'                  => $order->get_currency(),
			'total'                     => wc_format_decimal( $order->get_total(), $dp ),
			'subtotal'                  => wc_format_decimal( $order->get_subtotal(), $dp ),
			'total_line_items_quantity' => $order->get_item_count(),
			'total_tax'                 => wc_format_decimal( $order->get_total_tax(), $dp ),
			'total_shipping'            => wc_format_decimal( $order->get_shipping_total(), $dp ),
			'cart_tax'                  => wc_format_decimal( $order->get_cart_tax(), $dp ),
			'shipping_tax'              => wc_format_decimal( $order->get_shipping_tax(), $dp ),
			'total_discount'            => wc_format_decimal( $order->get_total_discount(), $dp ),
			'shipping_methods'          => $order->get_shipping_method(),
			'payment_details' => array(
				'method_id'    => $order->get_payment_method(),
				'method_title' => $order->get_payment_method_title(),
				'paid'         => ! is_null( $order->get_date_paid() ),
			),
			'billing_address' => array(
				'first_name' => $order->get_billing_first_name(),
				'last_name'  => $order->get_billing_last_name(),
				'company'    => $order->get_billing_company(),
				'address_1'  => $order->get_billing_address_1(),
				'address_2'  => $order->get_billing_address_2(),
				'city'       => $order->get_billing_city(),
				'state'      => $order->get_billing_state(),
				'postcode'   => $order->get_billing_postcode(),
				'country'    => $order->get_billing_country(),
				'email'      => $order->get_billing_email(),
				'phone'      => $order->get_billing_phone(),
			),
			'shipping_address' => array(
				'first_name' => $order->get_shipping_first_name(),
				'last_name'  => $order->get_shipping_last_name(),
				'company'    => $order->get_shipping_company(),
				'address_1'  => $order->get_shipping_address_1(),
				'address_2'  => $order->get_shipping_address_2(),
				'city'       => $order->get_shipping_city(),
				'state'      => $order->get_shipping_state(),
				'postcode'   => $order->get_shipping_postcode(),
				'country'    => $order->get_shipping_country(),
			),
			'note'                      => $order->get_customer_note(),
			'customer_ip'               => $order->get_customer_ip_address(),
			'customer_user_agent'       => $order->get_customer_user_agent(),
			'customer_id'               => $order->get_user_id(),
			'view_order_url'            => $order->get_view_order_url(),
			'line_items'                => array(),
			'shipping_lines'            => array(),
			'tax_lines'                 => array(),
			'fee_lines'                 => array(),
			'coupon_lines'              => array(),
		);

		// add line items
		foreach ( $order->get_items() as $item_id => $item ) {
			$product    = $item->get_product();
			$hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_';
			$item_meta  = $item->get_formatted_meta_data( $hideprefix );

			foreach ( $item_meta as $key => $values ) {
				$item_meta[ $key ]->label = $values->display_key;
				unset( $item_meta[ $key ]->display_key );
				unset( $item_meta[ $key ]->display_value );
			}

			$order_data['line_items'][] = array(
				'id'           => $item_id,
				'subtotal'     => wc_format_decimal( $order->get_line_subtotal( $item, false, false ), $dp ),
				'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), $dp ),
				'total'        => wc_format_decimal( $order->get_line_total( $item, false, false ), $dp ),
				'total_tax'    => wc_format_decimal( $item->get_total_tax(), $dp ),
				'price'        => wc_format_decimal( $order->get_item_total( $item, false, false ), $dp ),
				'quantity'     => $item->get_quantity(),
				'tax_class'    => $item->get_tax_class(),
				'name'         => $item->get_name(),
				'product_id'   => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(),
				'sku'          => is_object( $product ) ? $product->get_sku() : null,
				'meta'         => array_values( $item_meta ),
			);
		}

		// add shipping
		foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) {
			$order_data['shipping_lines'][] = array(
				'id'           => $shipping_item_id,
				'method_id'    => $shipping_item->get_method_id(),
				'method_title' => $shipping_item->get_name(),
				'total'        => wc_format_decimal( $shipping_item->get_total(), $dp ),
			);
		}

		// add taxes
		foreach ( $order->get_tax_totals() as $tax_code => $tax ) {
			$order_data['tax_lines'][] = array(
				'id'       => $tax->id,
				'rate_id'  => $tax->rate_id,
				'code'     => $tax_code,
				'title'    => $tax->label,
				'total'    => wc_format_decimal( $tax->amount, $dp ),
				'compound' => (bool) $tax->is_compound,
			);
		}

		// add fees
		foreach ( $order->get_fees() as $fee_item_id => $fee_item ) {
			$order_data['fee_lines'][] = array(
				'id'        => $fee_item_id,
				'title'     => $fee_item->get_name(),
				'tax_class' => $fee_item->get_tax_class(),
				'total'     => wc_format_decimal( $order->get_line_total( $fee_item ), $dp ),
				'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), $dp ),
			);
		}

		// add coupons
		foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) {
			$order_data['coupon_lines'][] = array(
				'id'     => $coupon_item_id,
				'code'   => $coupon_item->get_code(),
				'amount' => wc_format_decimal( $coupon_item->get_discount(), $dp ),
			);
		}

		return array( 'order' => apply_filters( 'woocommerce_api_order_response', $order_data, $order, $fields, $this->server ) );
	}

	/**
	 * Get the total number of orders
	 *
	 * @since 2.4
	 *
	 * @param string $status
	 * @param array $filter
	 *
	 * @return array|WP_Error
	 */
	public function get_orders_count( $status = null, $filter = array() ) {

		try {
			if ( ! current_user_can( 'read_private_shop_orders' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_orders_count', __( 'You do not have permission to read the orders count', 'woocommerce' ), 401 );
			}

			if ( ! empty( $status ) ) {

				if ( 'any' === $status ) {

					$order_statuses = array();

					foreach ( wc_get_order_statuses() as $slug => $name ) {
						$filter['status'] = str_replace( 'wc-', '', $slug );
						$query = $this->query_orders( $filter );
						$order_statuses[ str_replace( 'wc-', '', $slug ) ] = (int) $query->found_posts;
					}

					return array( 'count' => $order_statuses );

				} else {
					$filter['status'] = $status;
				}
			}

			$query = $this->query_orders( $filter );

			return array( 'count' => (int) $query->found_posts );

		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get a list of valid order statuses
	 *
	 * Note this requires no specific permissions other than being an authenticated
	 * API user. Order statuses (particularly custom statuses) could be considered
	 * private information which is why it's not in the API index.
	 *
	 * @since 2.1
	 * @return array
	 */
	public function get_order_statuses() {

		$order_statuses = array();

		foreach ( wc_get_order_statuses() as $slug => $name ) {
			$order_statuses[ str_replace( 'wc-', '', $slug ) ] = $name;
		}

		return array( 'order_statuses' => apply_filters( 'woocommerce_api_order_statuses_response', $order_statuses, $this ) );
	}

	/**
	 * Create an order
	 *
	 * @since 2.2
	 *
	 * @param array $data raw order data
	 *
	 * @return array|WP_Error
	 */
	public function create_order( $data ) {
		global $wpdb;

		try {
			if ( ! isset( $data['order'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order' ), 400 );
			}

			$data = $data['order'];

			// permission check
			if ( ! current_user_can( 'publish_shop_orders' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order', __( 'You do not have permission to create orders', 'woocommerce' ), 401 );
			}

			$data = apply_filters( 'woocommerce_api_create_order_data', $data, $this );

			// default order args, note that status is checked for validity in wc_create_order()
			$default_order_args = array(
				'status'        => isset( $data['status'] ) ? $data['status'] : '',
				'customer_note' => isset( $data['note'] ) ? $data['note'] : null,
			);

			// if creating order for existing customer
			if ( ! empty( $data['customer_id'] ) ) {

				// make sure customer exists
				if ( false === get_user_by( 'id', $data['customer_id'] ) ) {
					throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 );
				}

				$default_order_args['customer_id'] = $data['customer_id'];
			}

			// create the pending order
			$order = $this->create_base_order( $default_order_args, $data );

			if ( is_wp_error( $order ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_order', sprintf( __( 'Cannot create order: %s', 'woocommerce' ), implode( ', ', $order->get_error_messages() ) ), 400 );
			}

			// billing/shipping addresses
			$this->set_order_addresses( $order, $data );

			$lines = array(
				'line_item' => 'line_items',
				'shipping'  => 'shipping_lines',
				'fee'       => 'fee_lines',
				'coupon'    => 'coupon_lines',
			);

			foreach ( $lines as $line_type => $line ) {

				if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) {

					$set_item = "set_{$line_type}";

					foreach ( $data[ $line ] as $item ) {

						$this->$set_item( $order, $item, 'create' );
					}
				}
			}

			// calculate totals and set them
			$order->calculate_totals();

			// payment method (and payment_complete() if `paid` == true)
			if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) {

				// method ID & title are required
				if ( empty( $data['payment_details']['method_id'] ) || empty( $data['payment_details']['method_title'] ) ) {
					throw new WC_API_Exception( 'woocommerce_invalid_payment_details', __( 'Payment method ID and title are required', 'woocommerce' ), 400 );
				}

				update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] );
				update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) );

				// mark as paid if set
				if ( isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) {
					$order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' );
				}
			}

			// set order currency
			if ( isset( $data['currency'] ) ) {

				if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) {
					throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 );
				}

				update_post_meta( $order->get_id(), '_order_currency', $data['currency'] );
			}

			// set order meta
			if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) {
				$this->set_order_meta( $order->get_id(), $data['order_meta'] );
			}

			// HTTP 201 Created
			$this->server->send_status( 201 );

			wc_delete_shop_order_transients( $order );

			do_action( 'woocommerce_api_create_order', $order->get_id(), $data, $this );
			do_action( 'woocommerce_new_order', $order->get_id() );

			return $this->get_order( $order->get_id() );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Creates new WC_Order.
	 *
	 * Requires a separate function for classes that extend WC_API_Orders.
	 *
	 * @since 2.3
	 *
	 * @param $args array
	 * @param $data
	 *
	 * @return WC_Order
	 */
	protected function create_base_order( $args, $data ) {
		return wc_create_order( $args );
	}

	/**
	 * Edit an order
	 *
	 * @since 2.2
	 *
	 * @param int $id the order ID
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function edit_order( $id, $data ) {
		try {
			if ( ! isset( $data['order'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order' ), 400 );
			}

			$data = $data['order'];

			$update_totals = false;

			$id = $this->validate_request( $id, $this->post_type, 'edit' );

			if ( is_wp_error( $id ) ) {
				return $id;
			}

			$data  = apply_filters( 'woocommerce_api_edit_order_data', $data, $id, $this );
			$order = wc_get_order( $id );

			if ( empty( $order ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 );
			}

			$order_args = array( 'order_id' => $order->get_id() );

			// Customer note.
			if ( isset( $data['note'] ) ) {
				$order_args['customer_note'] = $data['note'];
			}

			// Customer ID.
			if ( isset( $data['customer_id'] ) && $data['customer_id'] != $order->get_user_id() ) {
				// Make sure customer exists.
				if ( false === get_user_by( 'id', $data['customer_id'] ) ) {
					throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 );
				}

				update_post_meta( $order->get_id(), '_customer_user', $data['customer_id'] );
			}

			// Billing/shipping address.
			$this->set_order_addresses( $order, $data );

			$lines = array(
				'line_item' => 'line_items',
				'shipping'  => 'shipping_lines',
				'fee'       => 'fee_lines',
				'coupon'    => 'coupon_lines',
			);

			foreach ( $lines as $line_type => $line ) {

				if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) {

					$update_totals = true;

					foreach ( $data[ $line ] as $item ) {

						// Item ID is always required.
						if ( ! array_key_exists( 'id', $item ) ) {
							$item['id'] = null;
						}

						// Create item.
						if ( is_null( $item['id'] ) ) {
							$this->set_item( $order, $line_type, $item, 'create' );
						} elseif ( $this->item_is_null( $item ) ) {
							// Delete item.
							wc_delete_order_item( $item['id'] );
						} else {
							// Update item.
							$this->set_item( $order, $line_type, $item, 'update' );
						}
					}
				}
			}

			// Payment method (and payment_complete() if `paid` == true and order needs payment).
			if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) {

				// Method ID.
				if ( isset( $data['payment_details']['method_id'] ) ) {
					update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] );
				}

				// Method title.
				if ( isset( $data['payment_details']['method_title'] ) ) {
					update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) );
				}

				// Mark as paid if set.
				if ( $order->needs_payment() && isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) {
					$order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' );
				}
			}

			// Set order currency.
			if ( isset( $data['currency'] ) ) {
				if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) {
					throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 );
				}

				update_post_meta( $order->get_id(), '_order_currency', $data['currency'] );
			}

			// If items have changed, recalculate order totals.
			if ( $update_totals ) {
				$order->calculate_totals();
			}

			// Update order meta.
			if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) {
				$this->set_order_meta( $order->get_id(), $data['order_meta'] );
			}

			// Update the order post to set customer note/modified date.
			wc_update_order( $order_args );

			// Order status.
			if ( ! empty( $data['status'] ) ) {
				// Refresh the order instance.
				$order = wc_get_order( $order->get_id() );
				$order->update_status( $data['status'], isset( $data['status_note'] ) ? $data['status_note'] : '', true );
			}

			wc_delete_shop_order_transients( $order );

			do_action( 'woocommerce_api_edit_order', $order->get_id(), $data, $this );
			do_action( 'woocommerce_update_order', $order->get_id() );

			return $this->get_order( $id );

		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete an order
	 *
	 * @param int $id the order ID
	 * @param bool $force true to permanently delete order, false to move to trash
	 * @return array|WP_Error
	 */
	public function delete_order( $id, $force = false ) {

		$id = $this->validate_request( $id, $this->post_type, 'delete' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		wc_delete_shop_order_transients( $id );

		do_action( 'woocommerce_api_delete_order', $id, $this );

		return $this->delete( $id, 'order',  ( 'true' === $force ) );
	}

	/**
	 * Helper method to get order post objects
	 *
	 * @since 2.1
	 * @param array $args request arguments for filtering query
	 * @return WP_Query
	 */
	protected function query_orders( $args ) {

		// set base query arguments
		$query_args = array(
			'fields'      => 'ids',
			'post_type'   => $this->post_type,
			'post_status' => array_keys( wc_get_order_statuses() ),
		);

		// add status argument
		if ( ! empty( $args['status'] ) ) {

			$statuses                  = 'wc-' . str_replace( ',', ',wc-', $args['status'] );
			$statuses                  = explode( ',', $statuses );
			$query_args['post_status'] = $statuses;

			unset( $args['status'] );

		}

		$query_args = $this->merge_query_args( $query_args, $args );

		return new WP_Query( $query_args );
	}

	/**
	 * Helper method to set/update the billing & shipping addresses for
	 * an order
	 *
	 * @since 2.1
	 * @param \WC_Order $order
	 * @param array $data
	 */
	protected function set_order_addresses( $order, $data ) {

		$address_fields = array(
			'first_name',
			'last_name',
			'company',
			'email',
			'phone',
			'address_1',
			'address_2',
			'city',
			'state',
			'postcode',
			'country',
		);

		$billing_address = $shipping_address = array();

		// billing address
		if ( isset( $data['billing_address'] ) && is_array( $data['billing_address'] ) ) {

			foreach ( $address_fields as $field ) {

				if ( isset( $data['billing_address'][ $field ] ) ) {
					$billing_address[ $field ] = wc_clean( $data['billing_address'][ $field ] );
				}
			}

			unset( $address_fields['email'] );
			unset( $address_fields['phone'] );
		}

		// shipping address
		if ( isset( $data['shipping_address'] ) && is_array( $data['shipping_address'] ) ) {

			foreach ( $address_fields as $field ) {

				if ( isset( $data['shipping_address'][ $field ] ) ) {
					$shipping_address[ $field ] = wc_clean( $data['shipping_address'][ $field ] );
				}
			}
		}

		$this->update_address( $order, $billing_address, 'billing' );
		$this->update_address( $order, $shipping_address, 'shipping' );

		// update user meta
		if ( $order->get_user_id() ) {
			foreach ( $billing_address as $key => $value ) {
				update_user_meta( $order->get_user_id(), 'billing_' . $key, $value );
			}
			foreach ( $shipping_address as $key => $value ) {
				update_user_meta( $order->get_user_id(), 'shipping_' . $key, $value );
			}
		}
	}

	/**
	 * Update address.
	 *
	 * @param WC_Order $order
	 * @param array $posted
	 * @param string $type
	 */
	protected function update_address( $order, $posted, $type = 'billing' ) {
		foreach ( $posted as $key => $value ) {
			if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) {
				$order->{"set_{$type}_{$key}"}( $value );
			}
		}
	}

	/**
	 * Helper method to add/update order meta, with two restrictions:
	 *
	 * 1) Only non-protected meta (no leading underscore) can be set
	 * 2) Meta values must be scalar (int, string, bool)
	 *
	 * @since 2.2
	 * @param int $order_id valid order ID
	 * @param array $order_meta order meta in array( 'meta_key' => 'meta_value' ) format
	 */
	protected function set_order_meta( $order_id, $order_meta ) {

		foreach ( $order_meta as $meta_key => $meta_value ) {

			if ( is_string( $meta_key ) && ! is_protected_meta( $meta_key ) && is_scalar( $meta_value ) ) {
				update_post_meta( $order_id, $meta_key, $meta_value );
			}
		}
	}

	/**
	 * Helper method to check if the resource ID associated with the provided item is null
	 *
	 * Items can be deleted by setting the resource ID to null
	 *
	 * @since 2.2
	 * @param array $item item provided in the request body
	 * @return bool true if the item resource ID is null, false otherwise
	 */
	protected function item_is_null( $item ) {

		$keys = array( 'product_id', 'method_id', 'title', 'code' );

		foreach ( $keys as $key ) {
			if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Wrapper method to create/update order items
	 *
	 * When updating, the item ID provided is checked to ensure it is associated
	 * with the order.
	 *
	 * @since 2.2
	 * @param \WC_Order $order order
	 * @param string $item_type
	 * @param array $item item provided in the request body
	 * @param string $action either 'create' or 'update'
	 * @throws WC_API_Exception if item ID is not associated with order
	 */
	protected function set_item( $order, $item_type, $item, $action ) {
		global $wpdb;

		$set_method = "set_{$item_type}";

		// verify provided line item ID is associated with order
		if ( 'update' === $action ) {

			$result = $wpdb->get_row(
				$wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d AND order_id = %d",
				absint( $item['id'] ),
				absint( $order->get_id() )
			) );

			if ( is_null( $result ) ) {
				throw new WC_API_Exception( 'woocommerce_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 );
			}
		}

		$this->$set_method( $order, $item, $action );
	}

	/**
	 * Create or update a line item
	 *
	 * @since 2.2
	 * @param \WC_Order $order
	 * @param array $item line item data
	 * @param string $action 'create' to add line item or 'update' to update it
	 * @throws WC_API_Exception invalid data, server error
	 */
	protected function set_line_item( $order, $item, $action ) {
		$creating  = ( 'create' === $action );

		// product is always required
		if ( ! isset( $item['product_id'] ) && ! isset( $item['sku'] ) ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID or SKU is required', 'woocommerce' ), 400 );
		}

		// when updating, ensure product ID provided matches
		if ( 'update' === $action ) {

			$item_product_id   = wc_get_order_item_meta( $item['id'], '_product_id' );
			$item_variation_id = wc_get_order_item_meta( $item['id'], '_variation_id' );

			if ( $item['product_id'] != $item_product_id && $item['product_id'] != $item_variation_id ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID provided does not match this line item', 'woocommerce' ), 400 );
			}
		}

		if ( isset( $item['product_id'] ) ) {
			$product_id = $item['product_id'];
		} elseif ( isset( $item['sku'] ) ) {
			$product_id = wc_get_product_id_by_sku( $item['sku'] );
		}

		// variations must each have a key & value
		$variation_id = 0;
		if ( isset( $item['variations'] ) && is_array( $item['variations'] ) ) {
			foreach ( $item['variations'] as $key => $value ) {
				if ( ! $key || ! $value ) {
					throw new WC_API_Exception( 'woocommerce_api_invalid_product_variation', __( 'The product variation is invalid', 'woocommerce' ), 400 );
				}
			}
			$variation_id = $this->get_variation_id( wc_get_product( $product_id ), $item['variations'] );
		}

		$product = wc_get_product( $variation_id ? $variation_id : $product_id );

		// must be a valid WC_Product
		if ( ! is_object( $product ) ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product', __( 'Product is invalid.', 'woocommerce' ), 400 );
		}

		// quantity must be positive float
		if ( isset( $item['quantity'] ) && floatval( $item['quantity'] ) <= 0 ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity must be a positive float.', 'woocommerce' ), 400 );
		}

		// quantity is required when creating
		if ( $creating && ! isset( $item['quantity'] ) ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity is required.', 'woocommerce' ), 400 );
		}

		if ( $creating ) {
			$line_item = new WC_Order_Item_Product();
		} else {
			$line_item = new WC_Order_Item_Product( $item['id'] );
		}

		$line_item->set_product( $product );
		$line_item->set_order_id( $order->get_id() );

		if ( isset( $item['quantity'] ) ) {
			$line_item->set_quantity( $item['quantity'] );
		}
		if ( isset( $item['total'] ) ) {
			$line_item->set_total( floatval( $item['total'] ) );
		} elseif ( $creating ) {
			$total = wc_get_price_excluding_tax( $product, array( 'qty' => $line_item->get_quantity() ) );
			$line_item->set_total( $total );
			$line_item->set_subtotal( $total );
		}
		if ( isset( $item['total_tax'] ) ) {
			$line_item->set_total_tax( floatval( $item['total_tax'] ) );
		}
		if ( isset( $item['subtotal'] ) ) {
			$line_item->set_subtotal( floatval( $item['subtotal'] ) );
		}
		if ( isset( $item['subtotal_tax'] ) ) {
			$line_item->set_subtotal_tax( floatval( $item['subtotal_tax'] ) );
		}
		if ( $variation_id ) {
			$line_item->set_variation_id( $variation_id );
			$line_item->set_variation( $item['variations'] );
		}

		// Save or add to order.
		if ( $creating ) {
			$order->add_item( $line_item );
		} else {
			$item_id = $line_item->save();

			if ( ! $item_id ) {
				throw new WC_API_Exception( 'woocommerce_cannot_create_line_item', __( 'Cannot create line item, try again.', 'woocommerce' ), 500 );
			}
		}
	}

	/**
	 * Given a product ID & API provided variations, find the correct variation ID to use for calculation
	 * We can't just trust input from the API to pass a variation_id manually, otherwise you could pass
	 * the cheapest variation ID but provide other information so we have to look up the variation ID.
	 *
	 * @param WC_Product $product
	 * @param array $variations
	 *
	 * @return int returns an ID if a valid variation was found for this product
	 */
	function get_variation_id( $product, $variations = array() ) {
		$variation_id = null;
		$variations_normalized = array();

		if ( $product->is_type( 'variable' ) && $product->has_child() ) {
			if ( isset( $variations ) && is_array( $variations ) ) {
				// start by normalizing the passed variations
				foreach ( $variations as $key => $value ) {
					$key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) ); // from get_attributes in class-wc-api-products.php
					$variations_normalized[ $key ] = strtolower( $value );
				}
				// now search through each product child and see if our passed variations match anything
				foreach ( $product->get_children() as $variation ) {
					$meta = array();
					foreach ( get_post_meta( $variation ) as $key => $value ) {
						$value = $value[0];
						$key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) );
						$meta[ $key ] = strtolower( $value );
					}
					// if the variation array is a part of the $meta array, we found our match
					if ( $this->array_contains( $variations_normalized, $meta ) ) {
						$variation_id = $variation;
						break;
					}
				}
			}
		}

		return $variation_id;
	}

	/**
	 * Utility function to see if the meta array contains data from variations
	 *
	 * @param array $needles
	 * @param array $haystack
	 *
	 * @return bool
	 */
	protected function array_contains( $needles, $haystack ) {
		foreach ( $needles as $key => $value ) {
			if ( $haystack[ $key ] !== $value ) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Create or update an order shipping method
	 *
	 * @since 2.2
	 * @param \WC_Order $order
	 * @param array $shipping item data
	 * @param string $action 'create' to add shipping or 'update' to update it
	 * @throws WC_API_Exception invalid data, server error
	 */
	protected function set_shipping( $order, $shipping, $action ) {

		// total must be a positive float
		if ( isset( $shipping['total'] ) && floatval( $shipping['total'] ) < 0 ) {
			throw new WC_API_Exception( 'woocommerce_invalid_shipping_total', __( 'Shipping total must be a positive amount.', 'woocommerce' ), 400 );
		}

		if ( 'create' === $action ) {

			// method ID is required
			if ( ! isset( $shipping['method_id'] ) ) {
				throw new WC_API_Exception( 'woocommerce_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 );
			}

			$rate = new WC_Shipping_Rate( $shipping['method_id'], isset( $shipping['method_title'] ) ? $shipping['method_title'] : '', isset( $shipping['total'] ) ? floatval( $shipping['total'] ) : 0, array(), $shipping['method_id'] );
			$item = new WC_Order_Item_Shipping();
			$item->set_order_id( $order->get_id() );
			$item->set_shipping_rate( $rate );
			$order->add_item( $item );
		} else {

			$item = new WC_Order_Item_Shipping( $shipping['id'] );

			if ( isset( $shipping['method_id'] ) ) {
				$item->set_method_id( $shipping['method_id'] );
			}

			if ( isset( $shipping['method_title'] ) ) {
				$item->set_method_title( $shipping['method_title'] );
			}

			if ( isset( $shipping['total'] ) ) {
				$item->set_total( floatval( $shipping['total'] ) );
			}

			$shipping_id = $item->save();

			if ( ! $shipping_id ) {
				throw new WC_API_Exception( 'woocommerce_cannot_update_shipping', __( 'Cannot update shipping method, try again.', 'woocommerce' ), 500 );
			}
		}
	}

	/**
	 * Create or update an order fee
	 *
	 * @since 2.2
	 * @param \WC_Order $order
	 * @param array $fee item data
	 * @param string $action 'create' to add fee or 'update' to update it
	 * @throws WC_API_Exception invalid data, server error
	 */
	protected function set_fee( $order, $fee, $action ) {

		if ( 'create' === $action ) {

			// fee title is required
			if ( ! isset( $fee['title'] ) ) {
				throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee title is required', 'woocommerce' ), 400 );
			}

			$item = new WC_Order_Item_Fee();
			$item->set_order_id( $order->get_id() );
			$item->set_name( wc_clean( $fee['title'] ) );
			$item->set_total( isset( $fee['total'] ) ? floatval( $fee['total'] ) : 0 );

			// if taxable, tax class and total are required
			if ( ! empty( $fee['taxable'] ) ) {
				if ( ! isset( $fee['tax_class'] ) ) {
					throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee tax class is required when fee is taxable.', 'woocommerce' ), 400 );
				}

				$item->set_tax_status( 'taxable' );
				$item->set_tax_class( $fee['tax_class'] );

				if ( isset( $fee['total_tax'] ) ) {
					$item->set_total_tax( isset( $fee['total_tax'] ) ? wc_format_refund_total( $fee['total_tax'] ) : 0 );
				}

				if ( isset( $fee['tax_data'] ) ) {
					$item->set_total_tax( wc_format_refund_total( array_sum( $fee['tax_data'] ) ) );
					$item->set_taxes( array_map( 'wc_format_refund_total', $fee['tax_data'] ) );
				}
			}

			$order->add_item( $item );
		} else {

			$item = new WC_Order_Item_Fee( $fee['id'] );

			if ( isset( $fee['title'] ) ) {
				$item->set_name( wc_clean( $fee['title'] ) );
			}

			if ( isset( $fee['tax_class'] ) ) {
				$item->set_tax_class( $fee['tax_class'] );
			}

			if ( isset( $fee['total'] ) ) {
				$item->set_total( floatval( $fee['total'] ) );
			}

			if ( isset( $fee['total_tax'] ) ) {
				$item->set_total_tax( floatval( $fee['total_tax'] ) );
			}

			$fee_id = $item->save();

			if ( ! $fee_id ) {
				throw new WC_API_Exception( 'woocommerce_cannot_update_fee', __( 'Cannot update fee, try again.', 'woocommerce' ), 500 );
			}
		}
	}

	/**
	 * Create or update an order coupon
	 *
	 * @since 2.2
	 * @param \WC_Order $order
	 * @param array $coupon item data
	 * @param string $action 'create' to add coupon or 'update' to update it
	 * @throws WC_API_Exception invalid data, server error
	 */
	protected function set_coupon( $order, $coupon, $action ) {

		// coupon amount must be positive float
		if ( isset( $coupon['amount'] ) && floatval( $coupon['amount'] ) < 0 ) {
			throw new WC_API_Exception( 'woocommerce_invalid_coupon_total', __( 'Coupon discount total must be a positive amount.', 'woocommerce' ), 400 );
		}

		if ( 'create' === $action ) {

			// coupon code is required
			if ( empty( $coupon['code'] ) ) {
				throw new WC_API_Exception( 'woocommerce_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 );
			}

			$item = new WC_Order_Item_Coupon();
			$item->set_props( array(
				'code'         => $coupon['code'],
				'discount'     => isset( $coupon['amount'] ) ? floatval( $coupon['amount'] ) : 0,
				'discount_tax' => 0,
				'order_id'     => $order->get_id(),
			) );
			$order->add_item( $item );
		} else {

			$item = new WC_Order_Item_Coupon( $coupon['id'] );

			if ( isset( $coupon['code'] ) ) {
				$item->set_code( $coupon['code'] );
			}

			if ( isset( $coupon['amount'] ) ) {
				$item->set_discount( floatval( $coupon['amount'] ) );
			}

			$coupon_id = $item->save();

			if ( ! $coupon_id ) {
				throw new WC_API_Exception( 'woocommerce_cannot_update_order_coupon', __( 'Cannot update coupon, try again.', 'woocommerce' ), 500 );
			}
		}
	}

	/**
	 * Get the admin order notes for an order
	 *
	 * @since 2.1
	 * @param string $order_id order ID
	 * @param string|null $fields fields to include in response
	 * @return array|WP_Error
	 */
	public function get_order_notes( $order_id, $fields = null ) {

		// ensure ID is valid order ID
		$order_id = $this->validate_request( $order_id, $this->post_type, 'read' );

		if ( is_wp_error( $order_id ) ) {
			return $order_id;
		}

		$args = array(
			'post_id' => $order_id,
			'approve' => 'approve',
			'type'    => 'order_note',
		);

		remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );

		$notes = get_comments( $args );

		add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );

		$order_notes = array();

		foreach ( $notes as $note ) {

			$order_notes[] = current( $this->get_order_note( $order_id, $note->comment_ID, $fields ) );
		}

		return array( 'order_notes' => apply_filters( 'woocommerce_api_order_notes_response', $order_notes, $order_id, $fields, $notes, $this->server ) );
	}

	/**
	 * Get an order note for the given order ID and ID
	 *
	 * @since 2.2
	 *
	 * @param string $order_id order ID
	 * @param string $id order note ID
	 * @param string|null $fields fields to limit response to
	 *
	 * @return array|WP_Error
	 */
	public function get_order_note( $order_id, $id, $fields = null ) {
		try {
			// Validate order ID
			$order_id = $this->validate_request( $order_id, $this->post_type, 'read' );

			if ( is_wp_error( $order_id ) ) {
				return $order_id;
			}

			$id = absint( $id );

			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 );
			}

			$note = get_comment( $id );

			if ( is_null( $note ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$order_note = array(
				'id'            => $note->comment_ID,
				'created_at'    => $this->server->format_datetime( $note->comment_date_gmt ),
				'note'          => $note->comment_content,
				'customer_note' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ),
			);

			return array( 'order_note' => apply_filters( 'woocommerce_api_order_note_response', $order_note, $id, $fields, $note, $order_id, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create a new order note for the given order
	 *
	 * @since 2.2
	 * @param string $order_id order ID
	 * @param array $data raw request data
	 * @return WP_Error|array error or created note response data
	 */
	public function create_order_note( $order_id, $data ) {
		try {
			if ( ! isset( $data['order_note'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_note' ), 400 );
			}

			$data = $data['order_note'];

			// permission check
			if ( ! current_user_can( 'publish_shop_orders' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_note', __( 'You do not have permission to create order notes', 'woocommerce' ), 401 );
			}

			$order_id = $this->validate_request( $order_id, $this->post_type, 'edit' );

			if ( is_wp_error( $order_id ) ) {
				return $order_id;
			}

			$order = wc_get_order( $order_id );

			$data = apply_filters( 'woocommerce_api_create_order_note_data', $data, $order_id, $this );

			// note content is required
			if ( ! isset( $data['note'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note', __( 'Order note is required', 'woocommerce' ), 400 );
			}

			$is_customer_note = ( isset( $data['customer_note'] ) && true === $data['customer_note'] );

			// create the note
			$note_id = $order->add_order_note( $data['note'], $is_customer_note );

			if ( ! $note_id ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_note', __( 'Cannot create order note, please try again.', 'woocommerce' ), 500 );
			}

			// HTTP 201 Created
			$this->server->send_status( 201 );

			do_action( 'woocommerce_api_create_order_note', $note_id, $order_id, $this );

			return $this->get_order_note( $order->get_id(), $note_id );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit the order note
	 *
	 * @since 2.2
	 * @param string $order_id order ID
	 * @param string $id note ID
	 * @param array $data parsed request data
	 * @return WP_Error|array error or edited note response data
	 */
	public function edit_order_note( $order_id, $id, $data ) {
		try {
			if ( ! isset( $data['order_note'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_note' ), 400 );
			}

			$data = $data['order_note'];

			// Validate order ID
			$order_id = $this->validate_request( $order_id, $this->post_type, 'edit' );

			if ( is_wp_error( $order_id ) ) {
				return $order_id;
			}

			$order = wc_get_order( $order_id );

			// Validate note ID
			$id = absint( $id );

			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 );
			}

			// Ensure note ID is valid
			$note = get_comment( $id );

			if ( is_null( $note ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			// Ensure note ID is associated with given order
			if ( $note->comment_post_ID != $order->get_id() ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 );
			}

			$data = apply_filters( 'woocommerce_api_edit_order_note_data', $data, $note->comment_ID, $order->get_id(), $this );

			// Note content
			if ( isset( $data['note'] ) ) {

				wp_update_comment(
					array(
						'comment_ID'      => $note->comment_ID,
						'comment_content' => $data['note'],
					)
				);
			}

			// Customer note
			if ( isset( $data['customer_note'] ) ) {

				update_comment_meta( $note->comment_ID, 'is_customer_note', true === $data['customer_note'] ? 1 : 0 );
			}

			do_action( 'woocommerce_api_edit_order_note', $note->comment_ID, $order->get_id(), $this );

			return $this->get_order_note( $order->get_id(), $note->comment_ID );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete order note
	 *
	 * @since 2.2
	 * @param string $order_id order ID
	 * @param string $id note ID
	 * @return WP_Error|array error or deleted message
	 */
	public function delete_order_note( $order_id, $id ) {
		try {
			$order_id = $this->validate_request( $order_id, $this->post_type, 'delete' );

			if ( is_wp_error( $order_id ) ) {
				return $order_id;
			}

			// Validate note ID
			$id = absint( $id );

			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 );
			}

			// Ensure note ID is valid
			$note = get_comment( $id );

			if ( is_null( $note ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			// Ensure note ID is associated with given order
			if ( $note->comment_post_ID != $order_id ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 );
			}

			// Force delete since trashed order notes could not be managed through comments list table
			$result = wc_delete_order_note( $note->comment_ID );

			if ( ! $result ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_delete_order_note', __( 'This order note cannot be deleted', 'woocommerce' ), 500 );
			}

			do_action( 'woocommerce_api_delete_order_note', $note->comment_ID, $order_id, $this );

			return array( 'message' => __( 'Permanently deleted order note', 'woocommerce' ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the order refunds for an order
	 *
	 * @since 2.2
	 * @param string $order_id order ID
	 * @param string|null $fields fields to include in response
	 * @return array|WP_Error
	 */
	public function get_order_refunds( $order_id, $fields = null ) {

		// Ensure ID is valid order ID
		$order_id = $this->validate_request( $order_id, $this->post_type, 'read' );

		if ( is_wp_error( $order_id ) ) {
			return $order_id;
		}

		$refund_items = wc_get_orders( array(
			'type'   => 'shop_order_refund',
			'parent' => $order_id,
			'limit'  => -1,
			'return' => 'ids',
		) );
		$order_refunds = array();

		foreach ( $refund_items as $refund_id ) {
			$order_refunds[] = current( $this->get_order_refund( $order_id, $refund_id, $fields ) );
		}

		return array( 'order_refunds' => apply_filters( 'woocommerce_api_order_refunds_response', $order_refunds, $order_id, $fields, $refund_items, $this ) );
	}

	/**
	 * Get an order refund for the given order ID and ID
	 *
	 * @since 2.2
	 *
	 * @param string $order_id order ID
	 * @param int $id
	 * @param string|null $fields fields to limit response to
	 * @param array $filter
	 *
	 * @return array|WP_Error
	 */
	public function get_order_refund( $order_id, $id, $fields = null, $filter = array() ) {
		try {
			// Validate order ID
			$order_id = $this->validate_request( $order_id, $this->post_type, 'read' );

			if ( is_wp_error( $order_id ) ) {
				return $order_id;
			}

			$id = absint( $id );

			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 );
			}

			$order  = wc_get_order( $order_id );
			$refund = wc_get_order( $id );

			if ( ! $refund ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 );
			}

			$line_items = array();

			// Add line items
			foreach ( $refund->get_items( 'line_item' ) as $item_id => $item ) {
				$product    = $item->get_product();
				$hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_';
				$item_meta  = $item->get_formatted_meta_data( $hideprefix );

				foreach ( $item_meta as $key => $values ) {
					$item_meta[ $key ]->label = $values->display_key;
					unset( $item_meta[ $key ]->display_key );
					unset( $item_meta[ $key ]->display_value );
				}

				$line_items[] = array(
					'id'               => $item_id,
					'subtotal'         => wc_format_decimal( $order->get_line_subtotal( $item ), 2 ),
					'subtotal_tax'     => wc_format_decimal( $item->get_subtotal_tax(), 2 ),
					'total'            => wc_format_decimal( $order->get_line_total( $item ), 2 ),
					'total_tax'        => wc_format_decimal( $order->get_line_tax( $item ), 2 ),
					'price'            => wc_format_decimal( $order->get_item_total( $item ), 2 ),
					'quantity'         => $item->get_quantity(),
					'tax_class'        => $item->get_tax_class(),
					'name'             => $item->get_name(),
					'product_id'       => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(),
					'sku'              => is_object( $product ) ? $product->get_sku() : null,
					'meta'             => array_values( $item_meta ),
					'refunded_item_id' => (int) $item->get_meta( 'refunded_item_id' ),
				);
			}

			$order_refund = array(
				'id'         => $refund->get_id(),
				'created_at' => $this->server->format_datetime( $refund->get_date_created() ? $refund->get_date_created()->getTimestamp() : 0, false, false ),
				'amount'     => wc_format_decimal( $refund->get_amount(), 2 ),
				'reason'     => $refund->get_reason(),
				'line_items' => $line_items,
			);

			return array( 'order_refund' => apply_filters( 'woocommerce_api_order_refund_response', $order_refund, $id, $fields, $refund, $order_id, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create a new order refund for the given order
	 *
	 * @since 2.2
	 * @param string $order_id order ID
	 * @param array $data raw request data
	 * @param bool $api_refund do refund using a payment gateway API
	 * @return WP_Error|array error or created refund response data
	 */
	public function create_order_refund( $order_id, $data, $api_refund = true ) {
		try {
			if ( ! isset( $data['order_refund'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_refund' ), 400 );
			}

			$data = $data['order_refund'];

			// Permission check
			if ( ! current_user_can( 'publish_shop_orders' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_refund', __( 'You do not have permission to create order refunds', 'woocommerce' ), 401 );
			}

			$order_id = absint( $order_id );

			if ( empty( $order_id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 );
			}

			$data = apply_filters( 'woocommerce_api_create_order_refund_data', $data, $order_id, $this );

			// Refund amount is required
			if ( ! isset( $data['amount'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount is required.', 'woocommerce' ), 400 );
			} elseif ( 0 > $data['amount'] ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount must be positive.', 'woocommerce' ), 400 );
			}

			$data['order_id']  = $order_id;
			$data['refund_id'] = 0;

			// Create the refund
			$refund = wc_create_refund( $data );

			if ( ! $refund ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 );
			}

			// Refund via API
			if ( $api_refund ) {
				if ( WC()->payment_gateways() ) {
					$payment_gateways = WC()->payment_gateways->payment_gateways();
				}

				$order = wc_get_order( $order_id );

				if ( isset( $payment_gateways[ $order->get_payment_method() ] ) && $payment_gateways[ $order->get_payment_method() ]->supports( 'refunds' ) ) {
					$result = $payment_gateways[ $order->get_payment_method() ]->process_refund( $order_id, $refund->get_amount(), $refund->get_reason() );

					if ( is_wp_error( $result ) ) {
						return $result;
					} elseif ( ! $result ) {
						throw new WC_API_Exception( 'woocommerce_api_create_order_refund_api_failed', __( 'An error occurred while attempting to create the refund using the payment gateway API.', 'woocommerce' ), 500 );
					}
				}
			}

			// HTTP 201 Created
			$this->server->send_status( 201 );

			do_action( 'woocommerce_api_create_order_refund', $refund->get_id(), $order_id, $this );

			return $this->get_order_refund( $order_id, $refund->get_id() );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit an order refund
	 *
	 * @since 2.2
	 * @param string $order_id order ID
	 * @param string $id refund ID
	 * @param array $data parsed request data
	 * @return WP_Error|array error or edited refund response data
	 */
	public function edit_order_refund( $order_id, $id, $data ) {
		try {
			if ( ! isset( $data['order_refund'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_refund' ), 400 );
			}

			$data = $data['order_refund'];

			// Validate order ID
			$order_id = $this->validate_request( $order_id, $this->post_type, 'edit' );

			if ( is_wp_error( $order_id ) ) {
				return $order_id;
			}

			// Validate refund ID
			$id = absint( $id );

			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 );
			}

			// Ensure order ID is valid
			$refund = get_post( $id );

			if ( ! $refund ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 );
			}

			// Ensure refund ID is associated with given order
			if ( $refund->post_parent != $order_id ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 );
			}

			$data = apply_filters( 'woocommerce_api_edit_order_refund_data', $data, $refund->ID, $order_id, $this );

			// Update reason
			if ( isset( $data['reason'] ) ) {
				$updated_refund = wp_update_post( array( 'ID' => $refund->ID, 'post_excerpt' => $data['reason'] ) );

				if ( is_wp_error( $updated_refund ) ) {
					return $updated_refund;
				}
			}

			// Update refund amount
			if ( isset( $data['amount'] ) && 0 < $data['amount'] ) {
				update_post_meta( $refund->ID, '_refund_amount', wc_format_decimal( $data['amount'] ) );
			}

			do_action( 'woocommerce_api_edit_order_refund', $refund->ID, $order_id, $this );

			return $this->get_order_refund( $order_id, $refund->ID );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete order refund
	 *
	 * @since 2.2
	 * @param string $order_id order ID
	 * @param string $id refund ID
	 * @return WP_Error|array error or deleted message
	 */
	public function delete_order_refund( $order_id, $id ) {
		try {
			$order_id = $this->validate_request( $order_id, $this->post_type, 'delete' );

			if ( is_wp_error( $order_id ) ) {
				return $order_id;
			}

			// Validate refund ID
			$id = absint( $id );

			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 );
			}

			// Ensure refund ID is valid
			$refund = get_post( $id );

			if ( ! $refund ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 );
			}

			// Ensure refund ID is associated with given order
			if ( $refund->post_parent != $order_id ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 );
			}

			wc_delete_shop_order_transients( $order_id );

			do_action( 'woocommerce_api_delete_order_refund', $refund->ID, $order_id, $this );

			return $this->delete( $refund->ID, 'refund', true );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Bulk update or insert orders
	 * Accepts an array with orders in the formats supported by
	 * WC_API_Orders->create_order() and WC_API_Orders->edit_order()
	 *
	 * @since 2.4.0
	 *
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function bulk( $data ) {

		try {
			if ( ! isset( $data['orders'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_orders_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'orders' ), 400 );
			}

			$data  = $data['orders'];
			$limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'orders' );

			// Limit bulk operation
			if ( count( $data ) > $limit ) {
				throw new WC_API_Exception( 'woocommerce_api_orders_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 );
			}

			$orders = array();

			foreach ( $data as $_order ) {
				$order_id = 0;

				// Try to get the order ID
				if ( isset( $_order['id'] ) ) {
					$order_id = intval( $_order['id'] );
				}

				// Order exists / edit order
				if ( $order_id ) {
					$edit = $this->edit_order( $order_id, array( 'order' => $_order ) );

					if ( is_wp_error( $edit ) ) {
						$orders[] = array(
							'id'    => $order_id,
							'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ),
						);
					} else {
						$orders[] = $edit['order'];
					}
				} else {
					// Order don't exists / create order
					$new = $this->create_order( array( 'order' => $_order ) );

					if ( is_wp_error( $new ) ) {
						$orders[] = array(
							'id'    => $order_id,
							'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ),
						);
					} else {
						$orders[] = $new['order'];
					}
				}
			}

			return array( 'orders' => apply_filters( 'woocommerce_api_orders_bulk_response', $orders, $this ) );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}
}
legacy/api/v2/class-wc-api-coupons.php000064400000050074151336044770013610 0ustar00<?php
/**
 * WooCommerce API Coupons Class
 *
 * Handles requests to the /coupons endpoint
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WC_API_Coupons extends WC_API_Resource {

	/** @var string $base the route base */
	protected $base = '/coupons';

	/**
	 * Register the routes for this class
	 *
	 * GET /coupons
	 * GET /coupons/count
	 * GET /coupons/<id>
	 *
	 * @since 2.1
	 * @param array $routes
	 * @return array
	 */
	public function register_routes( $routes ) {

		# GET/POST /coupons
		$routes[ $this->base ] = array(
			array( array( $this, 'get_coupons' ),     WC_API_Server::READABLE ),
			array( array( $this, 'create_coupon' ),   WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET /coupons/count
		$routes[ $this->base . '/count' ] = array(
			array( array( $this, 'get_coupons_count' ), WC_API_Server::READABLE ),
		);

		# GET/PUT/DELETE /coupons/<id>
		$routes[ $this->base . '/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_coupon' ),    WC_API_Server::READABLE ),
			array( array( $this, 'edit_coupon' ),   WC_API_SERVER::EDITABLE | WC_API_SERVER::ACCEPT_DATA ),
			array( array( $this, 'delete_coupon' ), WC_API_SERVER::DELETABLE ),
		);

		# GET /coupons/code/<code>, note that coupon codes can contain spaces, dashes and underscores
		$routes[ $this->base . '/code/(?P<code>\w[\w\s\-]*)' ] = array(
			array( array( $this, 'get_coupon_by_code' ), WC_API_Server::READABLE ),
		);

		# POST|PUT /coupons/bulk
		$routes[ $this->base . '/bulk' ] = array(
			array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
		);

		return $routes;
	}

	/**
	 * Get all coupons
	 *
	 * @since 2.1
	 * @param string $fields
	 * @param array $filter
	 * @param int $page
	 * @return array
	 */
	public function get_coupons( $fields = null, $filter = array(), $page = 1 ) {

		$filter['page'] = $page;

		$query = $this->query_coupons( $filter );

		$coupons = array();

		foreach ( $query->posts as $coupon_id ) {

			if ( ! $this->is_readable( $coupon_id ) ) {
				continue;
			}

			$coupons[] = current( $this->get_coupon( $coupon_id, $fields ) );
		}

		$this->server->add_pagination_headers( $query );

		return array( 'coupons' => $coupons );
	}

	/**
	 * Get the coupon for the given ID
	 *
	 * @since 2.1
	 * @param int $id the coupon ID
	 * @param string $fields fields to include in response
	 * @return array|WP_Error
	 */
	public function get_coupon( $id, $fields = null ) {
		try {

			$id = $this->validate_request( $id, 'shop_coupon', 'read' );

			if ( is_wp_error( $id ) ) {
				return $id;
			}

			$coupon = new WC_Coupon( $id );

			if ( 0 === $coupon->get_id() ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_id', __( 'Invalid coupon ID', 'woocommerce' ), 404 );
			}

			$coupon_data = array(
				'id'                           => $coupon->get_id(),
				'code'                         => $coupon->get_code(),
				'type'                         => $coupon->get_discount_type(),
				'created_at'                   => $this->server->format_datetime( $coupon->get_date_created() ? $coupon->get_date_created()->getTimestamp() : 0 ), // API gives UTC times.
				'updated_at'                   => $this->server->format_datetime( $coupon->get_date_modified() ? $coupon->get_date_modified()->getTimestamp() : 0 ), // API gives UTC times.
				'amount'                       => wc_format_decimal( $coupon->get_amount(), 2 ),
				'individual_use'               => $coupon->get_individual_use(),
				'product_ids'                  => array_map( 'absint', (array) $coupon->get_product_ids() ),
				'exclude_product_ids'          => array_map( 'absint', (array) $coupon->get_excluded_product_ids() ),
				'usage_limit'                  => $coupon->get_usage_limit() ? $coupon->get_usage_limit() : null,
				'usage_limit_per_user'         => $coupon->get_usage_limit_per_user() ? $coupon->get_usage_limit_per_user() : null,
				'limit_usage_to_x_items'       => (int) $coupon->get_limit_usage_to_x_items(),
				'usage_count'                  => (int) $coupon->get_usage_count(),
				'expiry_date'                  => $coupon->get_date_expires() ? $this->server->format_datetime( $coupon->get_date_expires()->getTimestamp() ) : null, // API gives UTC times.
				'enable_free_shipping'         => $coupon->get_free_shipping(),
				'product_category_ids'         => array_map( 'absint', (array) $coupon->get_product_categories() ),
				'exclude_product_category_ids' => array_map( 'absint', (array) $coupon->get_excluded_product_categories() ),
				'exclude_sale_items'           => $coupon->get_exclude_sale_items(),
				'minimum_amount'               => wc_format_decimal( $coupon->get_minimum_amount(), 2 ),
				'maximum_amount'               => wc_format_decimal( $coupon->get_maximum_amount(), 2 ),
				'customer_emails'              => $coupon->get_email_restrictions(),
				'description'                  => $coupon->get_description(),
			);

			return array( 'coupon' => apply_filters( 'woocommerce_api_coupon_response', $coupon_data, $coupon, $fields, $this->server ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the total number of coupons
	 *
	 * @since 2.1
	 *
	 * @param array $filter
	 *
	 * @return array|WP_Error
	 */
	public function get_coupons_count( $filter = array() ) {
		try {
			if ( ! current_user_can( 'read_private_shop_coupons' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_coupons_count', __( 'You do not have permission to read the coupons count', 'woocommerce' ), 401 );
			}

			$query = $this->query_coupons( $filter );

			return array( 'count' => (int) $query->found_posts );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the coupon for the given code
	 *
	 * @since 2.1
	 * @param string $code the coupon code
	 * @param string $fields fields to include in response
	 * @return int|WP_Error
	 */
	public function get_coupon_by_code( $code, $fields = null ) {
		global $wpdb;

		try {
			$id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish' ORDER BY post_date DESC LIMIT 1;", $code ) );

			if ( is_null( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_code', __( 'Invalid coupon code', 'woocommerce' ), 404 );
			}

			return $this->get_coupon( $id, $fields );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create a coupon
	 *
	 * @since 2.2
	 *
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function create_coupon( $data ) {
		global $wpdb;

		try {
			if ( ! isset( $data['coupon'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_coupon_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'coupon' ), 400 );
			}

			$data = $data['coupon'];

			// Check user permission
			if ( ! current_user_can( 'publish_shop_coupons' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_coupon', __( 'You do not have permission to create coupons', 'woocommerce' ), 401 );
			}

			$data = apply_filters( 'woocommerce_api_create_coupon_data', $data, $this );

			// Check if coupon code is specified
			if ( ! isset( $data['code'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_coupon_code', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'code' ), 400 );
			}

			$coupon_code  = wc_format_coupon_code( $data['code'] );
			$id_from_code = wc_get_coupon_id_by_code( $coupon_code );

			if ( $id_from_code ) {
				throw new WC_API_Exception( 'woocommerce_api_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), 400 );
			}

			$defaults = array(
				'type'                         => 'fixed_cart',
				'amount'                       => 0,
				'individual_use'               => false,
				'product_ids'                  => array(),
				'exclude_product_ids'          => array(),
				'usage_limit'                  => '',
				'usage_limit_per_user'         => '',
				'limit_usage_to_x_items'       => '',
				'usage_count'                  => '',
				'expiry_date'                  => '',
				'enable_free_shipping'         => false,
				'product_category_ids'         => array(),
				'exclude_product_category_ids' => array(),
				'exclude_sale_items'           => false,
				'minimum_amount'               => '',
				'maximum_amount'               => '',
				'customer_emails'              => array(),
				'description'                  => '',
			);

			$coupon_data = wp_parse_args( $data, $defaults );

			// Validate coupon types
			if ( ! in_array( wc_clean( $coupon_data['type'] ), array_keys( wc_get_coupon_types() ) ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_type', sprintf( __( 'Invalid coupon type - the coupon type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_coupon_types() ) ) ), 400 );
			}

			$new_coupon = array(
				'post_title'   => $coupon_code,
				'post_content' => '',
				'post_status'  => 'publish',
				'post_author'  => get_current_user_id(),
				'post_type'    => 'shop_coupon',
				'post_excerpt' => $coupon_data['description'],
	 		);

			$id = wp_insert_post( $new_coupon, true );

			if ( is_wp_error( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_coupon', $id->get_error_message(), 400 );
			}

			// Set coupon meta
			update_post_meta( $id, 'discount_type', $coupon_data['type'] );
			update_post_meta( $id, 'coupon_amount', wc_format_decimal( $coupon_data['amount'] ) );
			update_post_meta( $id, 'individual_use', ( true === $coupon_data['individual_use'] ) ? 'yes' : 'no' );
			update_post_meta( $id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $coupon_data['product_ids'] ) ) ) );
			update_post_meta( $id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $coupon_data['exclude_product_ids'] ) ) ) );
			update_post_meta( $id, 'usage_limit', absint( $coupon_data['usage_limit'] ) );
			update_post_meta( $id, 'usage_limit_per_user', absint( $coupon_data['usage_limit_per_user'] ) );
			update_post_meta( $id, 'limit_usage_to_x_items', absint( $coupon_data['limit_usage_to_x_items'] ) );
			update_post_meta( $id, 'usage_count', absint( $coupon_data['usage_count'] ) );
			update_post_meta( $id, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $coupon_data['expiry_date'] ) ) );
			update_post_meta( $id, 'date_expires', $this->get_coupon_expiry_date( wc_clean( $coupon_data['expiry_date'] ), true ) );
			update_post_meta( $id, 'free_shipping', ( true === $coupon_data['enable_free_shipping'] ) ? 'yes' : 'no' );
			update_post_meta( $id, 'product_categories', array_filter( array_map( 'intval', $coupon_data['product_category_ids'] ) ) );
			update_post_meta( $id, 'exclude_product_categories', array_filter( array_map( 'intval', $coupon_data['exclude_product_category_ids'] ) ) );
			update_post_meta( $id, 'exclude_sale_items', ( true === $coupon_data['exclude_sale_items'] ) ? 'yes' : 'no' );
			update_post_meta( $id, 'minimum_amount', wc_format_decimal( $coupon_data['minimum_amount'] ) );
			update_post_meta( $id, 'maximum_amount', wc_format_decimal( $coupon_data['maximum_amount'] ) );
			update_post_meta( $id, 'customer_email', array_filter( array_map( 'sanitize_email', $coupon_data['customer_emails'] ) ) );

			do_action( 'woocommerce_api_create_coupon', $id, $data );
			do_action( 'woocommerce_new_coupon', $id );

			$this->server->send_status( 201 );

			return $this->get_coupon( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit a coupon
	 *
	 * @since 2.2
	 *
	 * @param int $id the coupon ID
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function edit_coupon( $id, $data ) {

		try {
			if ( ! isset( $data['coupon'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_coupon_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'coupon' ), 400 );
			}

			$data = $data['coupon'];

			$id = $this->validate_request( $id, 'shop_coupon', 'edit' );

			if ( is_wp_error( $id ) ) {
				return $id;
			}

			$data = apply_filters( 'woocommerce_api_edit_coupon_data', $data, $id, $this );

			if ( isset( $data['code'] ) ) {
				global $wpdb;

				$coupon_code  = wc_format_coupon_code( $data['code'] );
				$id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id );

				if ( $id_from_code ) {
					throw new WC_API_Exception( 'woocommerce_api_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), 400 );
				}

				$updated = wp_update_post( array( 'ID' => intval( $id ), 'post_title' => $coupon_code ) );

				if ( 0 === $updated ) {
					throw new WC_API_Exception( 'woocommerce_api_cannot_update_coupon', __( 'Failed to update coupon', 'woocommerce' ), 400 );
				}
			}

			if ( isset( $data['description'] ) ) {
				$updated = wp_update_post( array( 'ID' => intval( $id ), 'post_excerpt' => $data['description'] ) );

				if ( 0 === $updated ) {
					throw new WC_API_Exception( 'woocommerce_api_cannot_update_coupon', __( 'Failed to update coupon', 'woocommerce' ), 400 );
				}
			}

			if ( isset( $data['type'] ) ) {
				// Validate coupon types
				if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_coupon_types() ) ) ) {
					throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_type', sprintf( __( 'Invalid coupon type - the coupon type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_coupon_types() ) ) ), 400 );
				}
				update_post_meta( $id, 'discount_type', $data['type'] );
			}

			if ( isset( $data['amount'] ) ) {
				update_post_meta( $id, 'coupon_amount', wc_format_decimal( $data['amount'] ) );
			}

			if ( isset( $data['individual_use'] ) ) {
				update_post_meta( $id, 'individual_use', ( true === $data['individual_use'] ) ? 'yes' : 'no' );
			}

			if ( isset( $data['product_ids'] ) ) {
				update_post_meta( $id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $data['product_ids'] ) ) ) );
			}

			if ( isset( $data['exclude_product_ids'] ) ) {
				update_post_meta( $id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $data['exclude_product_ids'] ) ) ) );
			}

			if ( isset( $data['usage_limit'] ) ) {
				update_post_meta( $id, 'usage_limit', absint( $data['usage_limit'] ) );
			}

			if ( isset( $data['usage_limit_per_user'] ) ) {
				update_post_meta( $id, 'usage_limit_per_user', absint( $data['usage_limit_per_user'] ) );
			}

			if ( isset( $data['limit_usage_to_x_items'] ) ) {
				update_post_meta( $id, 'limit_usage_to_x_items', absint( $data['limit_usage_to_x_items'] ) );
			}

			if ( isset( $data['usage_count'] ) ) {
				update_post_meta( $id, 'usage_count', absint( $data['usage_count'] ) );
			}

			if ( isset( $data['expiry_date'] ) ) {
				update_post_meta( $id, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ) ) );
				update_post_meta( $id, 'date_expires', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ), true ) );
			}

			if ( isset( $data['enable_free_shipping'] ) ) {
				update_post_meta( $id, 'free_shipping', ( true === $data['enable_free_shipping'] ) ? 'yes' : 'no' );
			}

			if ( isset( $data['product_category_ids'] ) ) {
				update_post_meta( $id, 'product_categories', array_filter( array_map( 'intval', $data['product_category_ids'] ) ) );
			}

			if ( isset( $data['exclude_product_category_ids'] ) ) {
				update_post_meta( $id, 'exclude_product_categories', array_filter( array_map( 'intval', $data['exclude_product_category_ids'] ) ) );
			}

			if ( isset( $data['exclude_sale_items'] ) ) {
				update_post_meta( $id, 'exclude_sale_items', ( true === $data['exclude_sale_items'] ) ? 'yes' : 'no' );
			}

			if ( isset( $data['minimum_amount'] ) ) {
				update_post_meta( $id, 'minimum_amount', wc_format_decimal( $data['minimum_amount'] ) );
			}

			if ( isset( $data['maximum_amount'] ) ) {
				update_post_meta( $id, 'maximum_amount', wc_format_decimal( $data['maximum_amount'] ) );
			}

			if ( isset( $data['customer_emails'] ) ) {
				update_post_meta( $id, 'customer_email', array_filter( array_map( 'sanitize_email', $data['customer_emails'] ) ) );
			}

			do_action( 'woocommerce_api_edit_coupon', $id, $data );
			do_action( 'woocommerce_update_coupon', $id );

			return $this->get_coupon( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete a coupon
	 *
	 * @since  2.2
	 * @param int $id the coupon ID
	 * @param bool $force true to permanently delete coupon, false to move to trash
	 * @return array|WP_Error
	 */
	public function delete_coupon( $id, $force = false ) {

		$id = $this->validate_request( $id, 'shop_coupon', 'delete' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		do_action( 'woocommerce_api_delete_coupon', $id, $this );

		return $this->delete( $id, 'shop_coupon', ( 'true' === $force ) );
	}

	/**
	 * expiry_date format
	 *
	 * @since  2.3.0
	 * @param  string $expiry_date
	 * @param bool $as_timestamp (default: false)
	 * @return string|int
	 */
	protected function get_coupon_expiry_date( $expiry_date, $as_timestamp = false ) {
		if ( '' != $expiry_date ) {
			if ( $as_timestamp ) {
				return strtotime( $expiry_date );
			}

			return date( 'Y-m-d', strtotime( $expiry_date ) );
		}

		return '';
	}

	/**
	 * Helper method to get coupon post objects
	 *
	 * @since 2.1
	 * @param array $args request arguments for filtering query
	 * @return WP_Query
	 */
	private function query_coupons( $args ) {

		// set base query arguments
		$query_args = array(
			'fields'      => 'ids',
			'post_type'   => 'shop_coupon',
			'post_status' => 'publish',
		);

		$query_args = $this->merge_query_args( $query_args, $args );

		return new WP_Query( $query_args );
	}

	/**
	 * Bulk update or insert coupons
	 * Accepts an array with coupons in the formats supported by
	 * WC_API_Coupons->create_coupon() and WC_API_Coupons->edit_coupon()
	 *
	 * @since 2.4.0
	 *
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function bulk( $data ) {

		try {
			if ( ! isset( $data['coupons'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_coupons_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'coupons' ), 400 );
			}

			$data  = $data['coupons'];
			$limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'coupons' );

			// Limit bulk operation
			if ( count( $data ) > $limit ) {
				throw new WC_API_Exception( 'woocommerce_api_coupons_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 );
			}

			$coupons = array();

			foreach ( $data as $_coupon ) {
				$coupon_id = 0;

				// Try to get the coupon ID
				if ( isset( $_coupon['id'] ) ) {
					$coupon_id = intval( $_coupon['id'] );
				}

				// Coupon exists / edit coupon
				if ( $coupon_id ) {
					$edit = $this->edit_coupon( $coupon_id, array( 'coupon' => $_coupon ) );

					if ( is_wp_error( $edit ) ) {
						$coupons[] = array(
							'id'    => $coupon_id,
							'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ),
						);
					} else {
						$coupons[] = $edit['coupon'];
					}
				} else {

					// Coupon don't exists / create coupon
					$new = $this->create_coupon( array( 'coupon' => $_coupon ) );

					if ( is_wp_error( $new ) ) {
						$coupons[] = array(
							'id'    => $coupon_id,
							'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ),
						);
					} else {
						$coupons[] = $new['coupon'];
					}
				}
			}

			return array( 'coupons' => apply_filters( 'woocommerce_api_coupons_bulk_response', $coupons, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}
}
legacy/api/v2/class-wc-api-products.php000064400000220632151336044770013764 0ustar00<?php
/**
 * WooCommerce API Products Class
 *
 * Handles requests to the /products endpoint
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 * @version     3.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WC_API_Products extends WC_API_Resource {

	/** @var string $base the route base */
	protected $base = '/products';

	/**
	 * Register the routes for this class
	 *
	 * GET/POST /products
	 * GET /products/count
	 * GET/PUT/DELETE /products/<id>
	 * GET /products/<id>/reviews
	 *
	 * @since 2.1
	 * @param array $routes
	 * @return array
	 */
	public function register_routes( $routes ) {

		# GET/POST /products
		$routes[ $this->base ] = array(
			array( array( $this, 'get_products' ), WC_API_Server::READABLE ),
			array( array( $this, 'create_product' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET /products/count
		$routes[ $this->base . '/count' ] = array(
			array( array( $this, 'get_products_count' ), WC_API_Server::READABLE ),
		);

		# GET/PUT/DELETE /products/<id>
		$routes[ $this->base . '/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_product' ), WC_API_Server::READABLE ),
			array( array( $this, 'edit_product' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
			array( array( $this, 'delete_product' ), WC_API_Server::DELETABLE ),
		);

		# GET /products/<id>/reviews
		$routes[ $this->base . '/(?P<id>\d+)/reviews' ] = array(
			array( array( $this, 'get_product_reviews' ), WC_API_Server::READABLE ),
		);

		# GET /products/<id>/orders
		$routes[ $this->base . '/(?P<id>\d+)/orders' ] = array(
			array( array( $this, 'get_product_orders' ), WC_API_Server::READABLE ),
		);

		# GET /products/categories
		$routes[ $this->base . '/categories' ] = array(
			array( array( $this, 'get_product_categories' ), WC_API_Server::READABLE ),
		);

		# GET /products/categories/<id>
		$routes[ $this->base . '/categories/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_product_category' ), WC_API_Server::READABLE ),
		);

		# GET/POST /products/attributes
		$routes[ $this->base . '/attributes' ] = array(
			array( array( $this, 'get_product_attributes' ), WC_API_Server::READABLE ),
			array( array( $this, 'create_product_attribute' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET/PUT/DELETE /attributes/<id>
		$routes[ $this->base . '/attributes/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_product_attribute' ), WC_API_Server::READABLE ),
			array( array( $this, 'edit_product_attribute' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
			array( array( $this, 'delete_product_attribute' ), WC_API_Server::DELETABLE ),
		);

		# GET /products/sku/<product sku>
		$routes[ $this->base . '/sku/(?P<sku>\w[\w\s\-]*)' ] = array(
			array( array( $this, 'get_product_by_sku' ), WC_API_Server::READABLE ),
		);

		# POST|PUT /products/bulk
		$routes[ $this->base . '/bulk' ] = array(
			array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
		);

		return $routes;
	}

	/**
	 * Get all products
	 *
	 * @since 2.1
	 * @param string $fields
	 * @param string $type
	 * @param array $filter
	 * @param int $page
	 * @return array
	 */
	public function get_products( $fields = null, $type = null, $filter = array(), $page = 1 ) {

		if ( ! empty( $type ) ) {
			$filter['type'] = $type;
		}

		$filter['page'] = $page;

		$query = $this->query_products( $filter );

		$products = array();

		foreach ( $query->posts as $product_id ) {

			if ( ! $this->is_readable( $product_id ) ) {
				continue;
			}

			$products[] = current( $this->get_product( $product_id, $fields ) );
		}

		$this->server->add_pagination_headers( $query );

		return array( 'products' => $products );
	}

	/**
	 * Get the product for the given ID
	 *
	 * @since 2.1
	 * @param int $id the product ID
	 * @param string $fields
	 * @return array|WP_Error
	 */
	public function get_product( $id, $fields = null ) {

		$id = $this->validate_request( $id, 'product', 'read' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$product = wc_get_product( $id );

		// add data that applies to every product type
		$product_data = $this->get_product_data( $product );

		// add variations to variable products
		if ( $product->is_type( 'variable' ) && $product->has_child() ) {
			$product_data['variations'] = $this->get_variation_data( $product );
		}

		// add the parent product data to an individual variation
		if ( $product->is_type( 'variation' ) && $product->get_parent_id() ) {
			$_product = wc_get_product( $product->get_parent_id() );
			$product_data['parent'] = $this->get_product_data( $_product );
		}

		return array( 'product' => apply_filters( 'woocommerce_api_product_response', $product_data, $product, $fields, $this->server ) );
	}

	/**
	 * Get the total number of products
	 *
	 * @since 2.1
	 *
	 * @param string $type
	 * @param array $filter
	 *
	 * @return array|WP_Error
	 */
	public function get_products_count( $type = null, $filter = array() ) {
		try {
			if ( ! current_user_can( 'read_private_products' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_products_count', __( 'You do not have permission to read the products count', 'woocommerce' ), 401 );
			}

			if ( ! empty( $type ) ) {
				$filter['type'] = $type;
			}

			$query = $this->query_products( $filter );

			return array( 'count' => (int) $query->found_posts );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Create a new product
	 *
	 * @since 2.2
	 *
	 * @param array $data posted data
	 *
	 * @return array|WP_Error
	 */
	public function create_product( $data ) {
		$id = 0;

		try {
			if ( ! isset( $data['product'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product' ), 400 );
			}

			$data = $data['product'];

			// Check permissions
			if ( ! current_user_can( 'publish_products' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product', __( 'You do not have permission to create products', 'woocommerce' ), 401 );
			}

			$data = apply_filters( 'woocommerce_api_create_product_data', $data, $this );

			// Check if product title is specified
			if ( ! isset( $data['title'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_title', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'title' ), 400 );
			}

			// Check product type
			if ( ! isset( $data['type'] ) ) {
				$data['type'] = 'simple';
			}

			// Set visible visibility when not sent
			if ( ! isset( $data['catalog_visibility'] ) ) {
				$data['catalog_visibility'] = 'visible';
			}

			// Validate the product type
			if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 );
			}

			// Enable description html tags.
			$post_content = isset( $data['description'] ) ? wc_clean( $data['description'] ) : '';
			if ( $post_content && isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) {

				$post_content = wp_filter_post_kses( $data['description'] );
			}

			// Enable short description html tags.
			$post_excerpt = isset( $data['short_description'] ) ? wc_clean( $data['short_description'] ) : '';
			if ( $post_excerpt && isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) {
				$post_excerpt = wp_filter_post_kses( $data['short_description'] );
			}

			$classname = WC_Product_Factory::get_classname_from_product_type( $data['type'] );
			if ( ! class_exists( $classname ) ) {
				$classname = 'WC_Product_Simple';
			}
			$product = new $classname();

			$product->set_name( wc_clean( $data['title'] ) );
			$product->set_status( isset( $data['status'] ) ? wc_clean( $data['status'] ) : 'publish' );
			$product->set_short_description( isset( $data['short_description'] ) ? $post_excerpt : '' );
			$product->set_description( isset( $data['description'] ) ? $post_content : '' );

			// Attempts to create the new product.
			$product->save();
			$id = $product->get_id();

			// Checks for an error in the product creation
			if ( 0 >= $id ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_product', $id->get_error_message(), 400 );
			}

			// Check for featured/gallery images, upload it and set it
			if ( isset( $data['images'] ) ) {
				$product = $this->save_product_images( $product, $data['images'] );
			}

			// Save product meta fields
			$product = $this->save_product_meta( $product, $data );
			$product->save();

			// Save variations
			if ( isset( $data['type'] ) && 'variable' == $data['type'] && isset( $data['variations'] ) && is_array( $data['variations'] ) ) {
				$this->save_variations( $product, $data );
			}

			do_action( 'woocommerce_api_create_product', $id, $data );

			// Clear cache/transients
			wc_delete_product_transients( $id );

			$this->server->send_status( 201 );

			return $this->get_product( $id );
		} catch ( WC_Data_Exception $e ) {
			$this->clear_product( $id );
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		} catch ( WC_API_Exception $e ) {
			$this->clear_product( $id );
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit a product
	 *
	 * @since 2.2
	 *
	 * @param int $id the product ID
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function edit_product( $id, $data ) {
		try {
			if ( ! isset( $data['product'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product' ), 400 );
			}

			$data = $data['product'];

			$id = $this->validate_request( $id, 'product', 'edit' );

			if ( is_wp_error( $id ) ) {
				return $id;
			}

			$product = wc_get_product( $id );

			$data = apply_filters( 'woocommerce_api_edit_product_data', $data, $this );

			// Product title.
			if ( isset( $data['title'] ) ) {
				$product->set_name( wc_clean( $data['title'] ) );
			}

			// Product name (slug).
			if ( isset( $data['name'] ) ) {
				$product->set_slug( wc_clean( $data['name'] ) );
			}

			// Product status.
			if ( isset( $data['status'] ) ) {
				$product->set_status( wc_clean( $data['status'] ) );
			}

			// Product short description.
			if ( isset( $data['short_description'] ) ) {
				// Enable short description html tags.
				$post_excerpt = ( isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) ? wp_filter_post_kses( $data['short_description'] ) : wc_clean( $data['short_description'] );
				$product->set_short_description( $post_excerpt );
			}

			// Product description.
			if ( isset( $data['description'] ) ) {
				// Enable description html tags.
				$post_content = ( isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) ? wp_filter_post_kses( $data['description'] ) : wc_clean( $data['description'] );
				$product->set_description( $post_content );
			}

			// Validate the product type.
			if ( isset( $data['type'] ) && ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 );
			}

			// Check for featured/gallery images, upload it and set it.
			if ( isset( $data['images'] ) ) {
				$product = $this->save_product_images( $product, $data['images'] );
			}

			// Save product meta fields.
			$product = $this->save_product_meta( $product, $data );

			// Save variations.
			if ( $product->is_type( 'variable' ) ) {
				if ( isset( $data['variations'] ) && is_array( $data['variations'] ) ) {
					$this->save_variations( $product, $data );
				} else {
					// Just sync variations.
					$product = WC_Product_Variable::sync( $product, false );
				}
			}

			$product->save();

			do_action( 'woocommerce_api_edit_product', $id, $data );

			// Clear cache/transients.
			wc_delete_product_transients( $id );

			return $this->get_product( $id );
		} catch ( WC_Data_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete a product.
	 *
	 * @since 2.2
	 *
	 * @param int $id the product ID.
	 * @param bool $force true to permanently delete order, false to move to trash.
	 *
	 * @return array|WP_Error
	 */
	public function delete_product( $id, $force = false ) {

		$id = $this->validate_request( $id, 'product', 'delete' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$product = wc_get_product( $id );

		do_action( 'woocommerce_api_delete_product', $id, $this );

		// If we're forcing, then delete permanently.
		if ( $force ) {
			if ( $product->is_type( 'variable' ) ) {
				foreach ( $product->get_children() as $child_id ) {
					$child = wc_get_product( $child_id );
					if ( ! empty( $child ) ) {
						$child->delete( true );
					}
				}
			} else {
				// For other product types, if the product has children, remove the relationship.
				foreach ( $product->get_children() as $child_id ) {
					$child = wc_get_product( $child_id );
					if ( ! empty( $child ) ) {
						$child->set_parent_id( 0 );
						$child->save();
					}
				}
			}

			$product->delete( true );
			$result = ! ( $product->get_id() > 0 );
		} else {
			$product->delete();
			$result = 'trash' === $product->get_status();
		}

		if ( ! $result ) {
			return new WP_Error( 'woocommerce_api_cannot_delete_product', sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), 'product' ), array( 'status' => 500 ) );
		}

		// Delete parent product transients.
		if ( $parent_id = wp_get_post_parent_id( $id ) ) {
			wc_delete_product_transients( $parent_id );
		}

		if ( $force ) {
			return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), 'product' ) );
		} else {
			$this->server->send_status( '202' );

			return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product' ) );
		}
	}

	/**
	 * Get the reviews for a product
	 *
	 * @since 2.1
	 * @param int $id the product ID to get reviews for
	 * @param string $fields fields to include in response
	 * @return array|WP_Error
	 */
	public function get_product_reviews( $id, $fields = null ) {

		$id = $this->validate_request( $id, 'product', 'read' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$comments = get_approved_comments( $id );
		$reviews  = array();

		foreach ( $comments as $comment ) {

			$reviews[] = array(
				'id'             => intval( $comment->comment_ID ),
				'created_at'     => $this->server->format_datetime( $comment->comment_date_gmt ),
				'review'         => $comment->comment_content,
				'rating'         => get_comment_meta( $comment->comment_ID, 'rating', true ),
				'reviewer_name'  => $comment->comment_author,
				'reviewer_email' => $comment->comment_author_email,
				'verified'       => wc_review_is_from_verified_owner( $comment->comment_ID ),
			);
		}

		return array( 'product_reviews' => apply_filters( 'woocommerce_api_product_reviews_response', $reviews, $id, $fields, $comments, $this->server ) );
	}

	/**
	 * Get the orders for a product
	 *
	 * @since 2.4.0
	 * @param int $id the product ID to get orders for
	 * @param string fields  fields to retrieve
	 * @param array $filter filters to include in response
	 * @param string $status the order status to retrieve
	 * @param $page  $page   page to retrieve
	 * @return array|WP_Error
	 */
	public function get_product_orders( $id, $fields = null, $filter = array(), $status = null, $page = 1 ) {
		global $wpdb;

		$id = $this->validate_request( $id, 'product', 'read' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$order_ids = $wpdb->get_col( $wpdb->prepare( "
			SELECT order_id
			FROM {$wpdb->prefix}woocommerce_order_items
			WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d )
			AND order_item_type = 'line_item'
		 ", $id ) );

		if ( empty( $order_ids ) ) {
			return array( 'orders' => array() );
		}

		$filter = array_merge( $filter, array(
			'in' => implode( ',', $order_ids ),
		) );

		$orders = WC()->api->WC_API_Orders->get_orders( $fields, $filter, $status, $page );

		return array( 'orders' => apply_filters( 'woocommerce_api_product_orders_response', $orders['orders'], $id, $filter, $fields, $this->server ) );
	}

	/**
	 * Get a listing of product categories
	 *
	 * @since 2.2
	 *
	 * @param string|null $fields fields to limit response to
	 *
	 * @return array|WP_Error
	 */
	public function get_product_categories( $fields = null ) {
		try {
			// Permissions check
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 );
			}

			$product_categories = array();

			$terms = get_terms( 'product_cat', array( 'hide_empty' => false, 'fields' => 'ids' ) );

			foreach ( $terms as $term_id ) {
				$product_categories[] = current( $this->get_product_category( $term_id, $fields ) );
			}

			return array( 'product_categories' => apply_filters( 'woocommerce_api_product_categories_response', $product_categories, $terms, $fields, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the product category for the given ID
	 *
	 * @since 2.2
	 *
	 * @param string $id product category term ID
	 * @param string|null $fields fields to limit response to
	 *
	 * @return array|WP_Error
	 */
	public function get_product_category( $id, $fields = null ) {
		try {
			$id = absint( $id );

			// Validate ID
			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'Invalid product category ID', 'woocommerce' ), 400 );
			}

			// Permissions check
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 );
			}

			$term = get_term( $id, 'product_cat' );

			if ( is_wp_error( $term ) || is_null( $term ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'A product category with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$term_id = intval( $term->term_id );

			// Get category display type
			$display_type = get_term_meta( $term_id, 'display_type', true );

			// Get category image
			$image = '';
			if ( $image_id = get_term_meta( $term_id, 'thumbnail_id', true ) ) {
				$image = wp_get_attachment_url( $image_id );
			}

			$product_category = array(
				'id'          => $term_id,
				'name'        => $term->name,
				'slug'        => $term->slug,
				'parent'      => $term->parent,
				'description' => $term->description,
				'display'     => $display_type ? $display_type : 'default',
				'image'       => $image ? esc_url( $image ) : '',
				'count'       => intval( $term->count ),
			);

			return array( 'product_category' => apply_filters( 'woocommerce_api_product_category_response', $product_category, $id, $fields, $term, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Helper method to get product post objects
	 *
	 * @since 2.1
	 * @param array $args request arguments for filtering query
	 * @return WP_Query
	 */
	private function query_products( $args ) {

		// Set base query arguments
		$query_args = array(
			'fields'      => 'ids',
			'post_type'   => 'product',
			'post_status' => 'publish',
			'meta_query'  => array(),
		);

		if ( ! empty( $args['type'] ) ) {

			$types = explode( ',', $args['type'] );

			$query_args['tax_query'] = array(
				array(
					'taxonomy' => 'product_type',
					'field'    => 'slug',
					'terms'    => $types,
				),
			);

			unset( $args['type'] );
		}

		// Filter products by category
		if ( ! empty( $args['category'] ) ) {
			$query_args['product_cat'] = $args['category'];
		}

		// Filter by specific sku
		if ( ! empty( $args['sku'] ) ) {
			if ( ! is_array( $query_args['meta_query'] ) ) {
				$query_args['meta_query'] = array();
			}

			$query_args['meta_query'][] = array(
				'key'     => '_sku',
				'value'   => $args['sku'],
				'compare' => '=',
			);

			$query_args['post_type'] = array( 'product', 'product_variation' );
		}

		$query_args = $this->merge_query_args( $query_args, $args );

		return new WP_Query( $query_args );
	}

	/**
	 * Get standard product data that applies to every product type
	 *
	 * @since 2.1
	 * @param WC_Product|int $product
	 * @return array
	 */
	private function get_product_data( $product ) {
		if ( is_numeric( $product ) ) {
			$product = wc_get_product( $product );
		}

		if ( ! is_a( $product, 'WC_Product' ) ) {
			return array();
		}

		$prices_precision = wc_get_price_decimals();
		return array(
			'title'              => $product->get_name(),
			'id'                 => $product->get_id(),
			'created_at'         => $this->server->format_datetime( $product->get_date_created(), false, true ),
			'updated_at'         => $this->server->format_datetime( $product->get_date_modified(), false, true ),
			'type'               => $product->get_type(),
			'status'             => $product->get_status(),
			'downloadable'       => $product->is_downloadable(),
			'virtual'            => $product->is_virtual(),
			'permalink'          => $product->get_permalink(),
			'sku'                => $product->get_sku(),
			'price'              => wc_format_decimal( $product->get_price(), $prices_precision ),
			'regular_price'      => wc_format_decimal( $product->get_regular_price(), $prices_precision ),
			'sale_price'         => $product->get_sale_price() ? wc_format_decimal( $product->get_sale_price(), $prices_precision ) : null,
			'price_html'         => $product->get_price_html(),
			'taxable'            => $product->is_taxable(),
			'tax_status'         => $product->get_tax_status(),
			'tax_class'          => $product->get_tax_class(),
			'managing_stock'     => $product->managing_stock(),
			'stock_quantity'     => $product->get_stock_quantity(),
			'in_stock'           => $product->is_in_stock(),
			'backorders_allowed' => $product->backorders_allowed(),
			'backordered'        => $product->is_on_backorder(),
			'sold_individually'  => $product->is_sold_individually(),
			'purchaseable'       => $product->is_purchasable(),
			'featured'           => $product->is_featured(),
			'visible'            => $product->is_visible(),
			'catalog_visibility' => $product->get_catalog_visibility(),
			'on_sale'            => $product->is_on_sale(),
			'product_url'        => $product->is_type( 'external' ) ? $product->get_product_url() : '',
			'button_text'        => $product->is_type( 'external' ) ? $product->get_button_text() : '',
			'weight'             => $product->get_weight() ? wc_format_decimal( $product->get_weight(), 2 ) : null,
			'dimensions'         => array(
				'length' => $product->get_length(),
				'width'  => $product->get_width(),
				'height' => $product->get_height(),
				'unit'   => get_option( 'woocommerce_dimension_unit' ),
			),
			'shipping_required'  => $product->needs_shipping(),
			'shipping_taxable'   => $product->is_shipping_taxable(),
			'shipping_class'     => $product->get_shipping_class(),
			'shipping_class_id'  => ( 0 !== $product->get_shipping_class_id() ) ? $product->get_shipping_class_id() : null,
			'description'        => wpautop( do_shortcode( $product->get_description() ) ),
			'short_description'  => apply_filters( 'woocommerce_short_description', $product->get_short_description() ),
			'reviews_allowed'    => $product->get_reviews_allowed(),
			'average_rating'     => wc_format_decimal( $product->get_average_rating(), 2 ),
			'rating_count'       => $product->get_rating_count(),
			'related_ids'        => array_map( 'absint', array_values( wc_get_related_products( $product->get_id() ) ) ),
			'upsell_ids'         => array_map( 'absint', $product->get_upsell_ids() ),
			'cross_sell_ids'     => array_map( 'absint', $product->get_cross_sell_ids() ),
			'parent_id'          => $product->get_parent_id(),
			'categories'         => wc_get_object_terms( $product->get_id(), 'product_cat', 'name' ),
			'tags'               => wc_get_object_terms( $product->get_id(), 'product_tag', 'name' ),
			'images'             => $this->get_images( $product ),
			'featured_src'       => wp_get_attachment_url( get_post_thumbnail_id( $product->get_id() ) ),
			'attributes'         => $this->get_attributes( $product ),
			'downloads'          => $this->get_downloads( $product ),
			'download_limit'     => $product->get_download_limit(),
			'download_expiry'    => $product->get_download_expiry(),
			'download_type'      => 'standard',
			'purchase_note'      => wpautop( do_shortcode( wp_kses_post( $product->get_purchase_note() ) ) ),
			'total_sales'        => $product->get_total_sales(),
			'variations'         => array(),
			'parent'             => array(),
		);
	}

	/**
	 * Get an individual variation's data
	 *
	 * @since 2.1
	 * @param WC_Product $product
	 * @return array
	 */
	private function get_variation_data( $product ) {
		$prices_precision = wc_get_price_decimals();
		$variations       = array();

		foreach ( $product->get_children() as $child_id ) {

			$variation = wc_get_product( $child_id );

			if ( ! $variation || ! $variation->exists() ) {
				continue;
			}

			$variations[] = array(
				'id'                => $variation->get_id(),
				'created_at'        => $this->server->format_datetime( $variation->get_date_created(), false, true ),
				'updated_at'        => $this->server->format_datetime( $variation->get_date_modified(), false, true ),
				'downloadable'      => $variation->is_downloadable(),
				'virtual'           => $variation->is_virtual(),
				'permalink'         => $variation->get_permalink(),
				'sku'               => $variation->get_sku(),
				'price'             => wc_format_decimal( $variation->get_price(), $prices_precision ),
				'regular_price'     => wc_format_decimal( $variation->get_regular_price(), $prices_precision ),
				'sale_price'        => $variation->get_sale_price() ? wc_format_decimal( $variation->get_sale_price(), $prices_precision ) : null,
				'taxable'           => $variation->is_taxable(),
				'tax_status'        => $variation->get_tax_status(),
				'tax_class'         => $variation->get_tax_class(),
				'managing_stock'    => $variation->managing_stock(),
				'stock_quantity'    => (int) $variation->get_stock_quantity(),
				'in_stock'          => $variation->is_in_stock(),
				'backordered'       => $variation->is_on_backorder(),
				'purchaseable'      => $variation->is_purchasable(),
				'visible'           => $variation->variation_is_visible(),
				'on_sale'           => $variation->is_on_sale(),
				'weight'            => $variation->get_weight() ? wc_format_decimal( $variation->get_weight(), 2 ) : null,
				'dimensions'        => array(
					'length' => $variation->get_length(),
					'width'  => $variation->get_width(),
					'height' => $variation->get_height(),
					'unit'   => get_option( 'woocommerce_dimension_unit' ),
				),
				'shipping_class'    => $variation->get_shipping_class(),
				'shipping_class_id' => ( 0 !== $variation->get_shipping_class_id() ) ? $variation->get_shipping_class_id() : null,
				'image'             => $this->get_images( $variation ),
				'attributes'        => $this->get_attributes( $variation ),
				'downloads'         => $this->get_downloads( $variation ),
				'download_limit'    => (int) $product->get_download_limit(),
				'download_expiry'   => (int) $product->get_download_expiry(),
			);
		}

		return $variations;
	}

	/**
	 * Save default attributes.
	 *
	 * @since 3.0.0
	 * @param WC_Product $product
	 * @param array $request
	 * @return WC_Product
	 */
	protected function save_default_attributes( $product, $request ) {
		// Update default attributes options setting.
		if ( isset( $request['default_attribute'] ) ) {
			$request['default_attributes'] = $request['default_attribute'];
		}

		if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) {
			$attributes         = $product->get_attributes();
			$default_attributes = array();

			foreach ( $request['default_attributes'] as $default_attr_key => $default_attr ) {
				if ( ! isset( $default_attr['name'] ) ) {
					continue;
				}

				$taxonomy = sanitize_title( $default_attr['name'] );

				if ( isset( $default_attr['slug'] ) ) {
					$taxonomy = $this->get_attribute_taxonomy_by_slug( $default_attr['slug'] );
				}

				if ( isset( $attributes[ $taxonomy ] ) ) {
					$_attribute = $attributes[ $taxonomy ];

					if ( $_attribute['is_variation'] ) {
						$value = '';

						if ( isset( $default_attr['option'] ) ) {
							if ( $_attribute['is_taxonomy'] ) {
								// Don't use wc_clean as it destroys sanitized characters.
								$value = sanitize_title( trim( stripslashes( $default_attr['option'] ) ) );
							} else {
								$value = wc_clean( trim( stripslashes( $default_attr['option'] ) ) );
							}
						}

						if ( $value ) {
							$default_attributes[ $taxonomy ] = $value;
						}
					}
				}
			}

			$product->set_default_attributes( $default_attributes );
		}

		return $product;
	}

	/**
	 * Save product meta
	 *
	 * @since  2.2
	 * @param  WC_Product $product
	 * @param  array $data
	 * @return WC_Product
	 * @throws WC_API_Exception
	 */
	protected function save_product_meta( $product, $data ) {
		global $wpdb;

		// Virtual
		if ( isset( $data['virtual'] ) ) {
			$product->set_virtual( $data['virtual'] );
		}

		// Tax status
		if ( isset( $data['tax_status'] ) ) {
			$product->set_tax_status( wc_clean( $data['tax_status'] ) );
		}

		// Tax Class
		if ( isset( $data['tax_class'] ) ) {
			$product->set_tax_class( wc_clean( $data['tax_class'] ) );
		}

		// Catalog Visibility
		if ( isset( $data['catalog_visibility'] ) ) {
			$product->set_catalog_visibility( wc_clean( $data['catalog_visibility'] ) );
		}

		// Purchase Note
		if ( isset( $data['purchase_note'] ) ) {
			$product->set_purchase_note( wc_clean( $data['purchase_note'] ) );
		}

		// Featured Product
		if ( isset( $data['featured'] ) ) {
			$product->set_featured( $data['featured'] );
		}

		// Shipping data
		$product = $this->save_product_shipping_data( $product, $data );

		// SKU
		if ( isset( $data['sku'] ) ) {
			$sku     = $product->get_sku();
			$new_sku = wc_clean( $data['sku'] );

			if ( '' == $new_sku ) {
				$product->set_sku( '' );
			} elseif ( $new_sku !== $sku ) {
				if ( ! empty( $new_sku ) ) {
					$unique_sku = wc_product_has_unique_sku( $product->get_id(), $new_sku );
					if ( ! $unique_sku ) {
						throw new WC_API_Exception( 'woocommerce_api_product_sku_already_exists', __( 'The SKU already exists on another product.', 'woocommerce' ), 400 );
					} else {
						$product->set_sku( $new_sku );
					}
				} else {
					$product->set_sku( '' );
				}
			}
		}

		// Attributes
		if ( isset( $data['attributes'] ) ) {
			$attributes = array();

			foreach ( $data['attributes'] as $attribute ) {
				$is_taxonomy = 0;
				$taxonomy    = 0;

				if ( ! isset( $attribute['name'] ) ) {
					continue;
				}

				$attribute_slug = sanitize_title( $attribute['name'] );

				if ( isset( $attribute['slug'] ) ) {
					$taxonomy       = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] );
					$attribute_slug = sanitize_title( $attribute['slug'] );
				}

				if ( $taxonomy ) {
					$is_taxonomy = 1;
				}

				if ( $is_taxonomy ) {

					$attribute_id = wc_attribute_taxonomy_id_by_name( $attribute['name'] );

					if ( isset( $attribute['options'] ) ) {
						$options = $attribute['options'];

						if ( ! is_array( $attribute['options'] ) ) {
							// Text based attributes - Posted values are term names
							$options = explode( WC_DELIMITER, $options );
						}

						$values = array_map( 'wc_sanitize_term_text_based', $options );
						$values = array_filter( $values, 'strlen' );
					} else {
						$values = array();
					}

					// Update post terms
					if ( taxonomy_exists( $taxonomy ) ) {
						wp_set_object_terms( $product->get_id(), $values, $taxonomy );
					}

					if ( ! empty( $values ) ) {
						// Add attribute to array, but don't set values.
						$attribute_object = new WC_Product_Attribute();
						$attribute_object->set_id( $attribute_id );
						$attribute_object->set_name( $taxonomy );
						$attribute_object->set_options( $values );
						$attribute_object->set_position( isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0 );
						$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
						$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
						$attributes[] = $attribute_object;
					}
				} elseif ( isset( $attribute['options'] ) ) {
					// Array based
					if ( is_array( $attribute['options'] ) ) {
						$values = $attribute['options'];

					// Text based, separate by pipe
					} else {
						$values = array_map( 'wc_clean', explode( WC_DELIMITER, $attribute['options'] ) );
					}

					// Custom attribute - Add attribute to array and set the values.
					$attribute_object = new WC_Product_Attribute();
					$attribute_object->set_name( $attribute['name'] );
					$attribute_object->set_options( $values );
					$attribute_object->set_position( isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0 );
					$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
					$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
					$attributes[] = $attribute_object;
				}
			}

			uasort( $attributes, 'wc_product_attribute_uasort_comparison' );

			$product->set_attributes( $attributes );
		}

		// Sales and prices.
		if ( in_array( $product->get_type(), array( 'variable', 'grouped' ) ) ) {

			// Variable and grouped products have no prices.
			$product->set_regular_price( '' );
			$product->set_sale_price( '' );
			$product->set_date_on_sale_to( '' );
			$product->set_date_on_sale_from( '' );
			$product->set_price( '' );

		} else {

			// Regular Price.
			if ( isset( $data['regular_price'] ) ) {
				$regular_price = ( '' === $data['regular_price'] ) ? '' : $data['regular_price'];
				$product->set_regular_price( $regular_price );
			}

			// Sale Price.
			if ( isset( $data['sale_price'] ) ) {
				$sale_price = ( '' === $data['sale_price'] ) ? '' : $data['sale_price'];
				$product->set_sale_price( $sale_price );
			}

			if ( isset( $data['sale_price_dates_from'] ) ) {
				$date_from = $data['sale_price_dates_from'];
			} else {
				$date_from = $product->get_date_on_sale_from() ? date( 'Y-m-d', $product->get_date_on_sale_from()->getTimestamp() ) : '';
			}

			if ( isset( $data['sale_price_dates_to'] ) ) {
				$date_to = $data['sale_price_dates_to'];
			} else {
				$date_to = $product->get_date_on_sale_to() ? date( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() ) : '';
			}

			if ( $date_to && ! $date_from ) {
				$date_from = strtotime( 'NOW', current_time( 'timestamp', true ) );
			}

			$product->set_date_on_sale_to( $date_to );
			$product->set_date_on_sale_from( $date_from );

			if ( $product->is_on_sale( 'edit' ) ) {
				$product->set_price( $product->get_sale_price( 'edit' ) );
			} else {
				$product->set_price( $product->get_regular_price( 'edit' ) );
			}
		}

		// Product parent ID for groups
		if ( isset( $data['parent_id'] ) ) {
			$product->set_parent_id( absint( $data['parent_id'] ) );
		}

		// Sold Individually
		if ( isset( $data['sold_individually'] ) ) {
			$product->set_sold_individually( true === $data['sold_individually'] ? 'yes' : '' );
		}

		// Stock status
		if ( isset( $data['in_stock'] ) ) {
			$stock_status = ( true === $data['in_stock'] ) ? 'instock' : 'outofstock';
		} else {
			$stock_status = $product->get_stock_status();

			if ( '' === $stock_status ) {
				$stock_status = 'instock';
			}
		}

		// Stock Data
		if ( 'yes' == get_option( 'woocommerce_manage_stock' ) ) {
			// Manage stock
			if ( isset( $data['managing_stock'] ) ) {
				$managing_stock = ( true === $data['managing_stock'] ) ? 'yes' : 'no';
				$product->set_manage_stock( $managing_stock );
			} else {
				$managing_stock = $product->get_manage_stock() ? 'yes' : 'no';
			}

			// Backorders
			if ( isset( $data['backorders'] ) ) {
				if ( 'notify' == $data['backorders'] ) {
					$backorders = 'notify';
				} else {
					$backorders = ( true === $data['backorders'] ) ? 'yes' : 'no';
				}

				$product->set_backorders( $backorders );
			} else {
				$backorders = $product->get_backorders();
			}

			if ( $product->is_type( 'grouped' ) ) {
				$product->set_manage_stock( 'no' );
				$product->set_backorders( 'no' );
				$product->set_stock_quantity( '' );
				$product->set_stock_status( $stock_status );
			} elseif ( $product->is_type( 'external' ) ) {
				$product->set_manage_stock( 'no' );
				$product->set_backorders( 'no' );
				$product->set_stock_quantity( '' );
				$product->set_stock_status( 'instock' );
			} elseif ( 'yes' == $managing_stock ) {
				$product->set_backorders( $backorders );

				// Stock status is always determined by children so sync later.
				if ( ! $product->is_type( 'variable' ) ) {
					$product->set_stock_status( $stock_status );
				}

				// Stock quantity
				if ( isset( $data['stock_quantity'] ) ) {
					$product->set_stock_quantity( wc_stock_amount( $data['stock_quantity'] ) );
				}
			} else {
				// Don't manage stock.
				$product->set_manage_stock( 'no' );
				$product->set_backorders( $backorders );
				$product->set_stock_quantity( '' );
				$product->set_stock_status( $stock_status );
			}
		} elseif ( ! $product->is_type( 'variable' ) ) {
			$product->set_stock_status( $stock_status );
		}

		// Upsells
		if ( isset( $data['upsell_ids'] ) ) {
			$upsells = array();
			$ids     = $data['upsell_ids'];

			if ( ! empty( $ids ) ) {
				foreach ( $ids as $id ) {
					if ( $id && $id > 0 ) {
						$upsells[] = $id;
					}
				}

				$product->set_upsell_ids( $upsells );
			} else {
				$product->set_upsell_ids( array() );
			}
		}

		// Cross sells
		if ( isset( $data['cross_sell_ids'] ) ) {
			$crosssells = array();
			$ids        = $data['cross_sell_ids'];

			if ( ! empty( $ids ) ) {
				foreach ( $ids as $id ) {
					if ( $id && $id > 0 ) {
						$crosssells[] = $id;
					}
				}

				$product->set_cross_sell_ids( $crosssells );
			} else {
				$product->set_cross_sell_ids( array() );
			}
		}

		// Product categories
		if ( isset( $data['categories'] ) && is_array( $data['categories'] ) ) {
			$product->set_category_ids( $data['categories'] );
		}

		// Product tags
		if ( isset( $data['tags'] ) && is_array( $data['tags'] ) ) {
			$product->set_tag_ids( $data['tags'] );
		}

		// Downloadable
		if ( isset( $data['downloadable'] ) ) {
			$is_downloadable = ( true === $data['downloadable'] ) ? 'yes' : 'no';
			$product->set_downloadable( $is_downloadable );
		} else {
			$is_downloadable = $product->get_downloadable() ? 'yes' : 'no';
		}

		// Downloadable options
		if ( 'yes' == $is_downloadable ) {

			// Downloadable files
			if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) {
				$product = $this->save_downloadable_files( $product, $data['downloads'] );
			}

			// Download limit
			if ( isset( $data['download_limit'] ) ) {
				$product->set_download_limit( $data['download_limit'] );
			}

			// Download expiry
			if ( isset( $data['download_expiry'] ) ) {
				$product->set_download_expiry( $data['download_expiry'] );
			}
		}

		// Product url
		if ( $product->is_type( 'external' ) ) {
			if ( isset( $data['product_url'] ) ) {
				$product->set_product_url( $data['product_url'] );
			}

			if ( isset( $data['button_text'] ) ) {
				$product->set_button_text( $data['button_text'] );
			}
		}

		// Reviews allowed
		if ( isset( $data['reviews_allowed'] ) ) {
			$product->set_reviews_allowed( $data['reviews_allowed'] );
		}

		// Save default attributes for variable products.
		if ( $product->is_type( 'variable' ) ) {
			$product = $this->save_default_attributes( $product, $data );
		}

		// Do action for product type
		do_action( 'woocommerce_api_process_product_meta_' . $product->get_type(), $product->get_id(), $data );

		return $product;
	}

	/**
	 * Save variations
	 *
	 * @since  2.2
	 * @param  WC_Product $product
	 * @param  array $request
	 *
	 * @return true
	 *
	 * @throws WC_API_Exception
	 */
	protected function save_variations( $product, $request ) {
		global $wpdb;

		$id         = $product->get_id();
		$attributes = $product->get_attributes();

		foreach ( $request['variations'] as $menu_order => $data ) {
			$variation_id = isset( $data['id'] ) ? absint( $data['id'] ) : 0;
			$variation    = new WC_Product_Variation( $variation_id );

			// Create initial name and status.
			if ( ! $variation->get_slug() ) {
				/* translators: 1: variation id 2: product name */
				$variation->set_name( sprintf( __( 'Variation #%1$s of %2$s', 'woocommerce' ), $variation->get_id(), $product->get_name() ) );
				$variation->set_status( isset( $data['visible'] ) && false === $data['visible'] ? 'private' : 'publish' );
			}

			// Parent ID.
			$variation->set_parent_id( $product->get_id() );

			// Menu order.
			$variation->set_menu_order( $menu_order );

			// Status.
			if ( isset( $data['visible'] ) ) {
				$variation->set_status( false === $data['visible'] ? 'private' : 'publish' );
			}

			// SKU.
			if ( isset( $data['sku'] ) ) {
				$variation->set_sku( wc_clean( $data['sku'] ) );
			}

			// Thumbnail.
			if ( isset( $data['image'] ) && is_array( $data['image'] ) ) {
				$image = current( $data['image'] );
				if ( is_array( $image ) ) {
					$image['position'] = 0;
				}

				$variation = $this->save_product_images( $variation, array( $image ) );
			}

			// Virtual variation.
			if ( isset( $data['virtual'] ) ) {
				$variation->set_virtual( $data['virtual'] );
			}

			// Downloadable variation.
			if ( isset( $data['downloadable'] ) ) {
				$is_downloadable = $data['downloadable'];
				$variation->set_downloadable( $is_downloadable );
			} else {
				$is_downloadable = $variation->get_downloadable();
			}

			// Downloads.
			if ( $is_downloadable ) {
				// Downloadable files.
				if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) {
					$variation = $this->save_downloadable_files( $variation, $data['downloads'] );
				}

				// Download limit.
				if ( isset( $data['download_limit'] ) ) {
					$variation->set_download_limit( $data['download_limit'] );
				}

				// Download expiry.
				if ( isset( $data['download_expiry'] ) ) {
					$variation->set_download_expiry( $data['download_expiry'] );
				}
			}

			// Shipping data.
			$variation = $this->save_product_shipping_data( $variation, $data );

			// Stock handling.
			$manage_stock = (bool) $variation->get_manage_stock();
			if ( isset( $data['managing_stock'] ) ) {
				$manage_stock = $data['managing_stock'];
			}
			$variation->set_manage_stock( $manage_stock );

			$stock_status = $variation->get_stock_status();
			if ( isset( $data['in_stock'] ) ) {
				$stock_status = true === $data['in_stock'] ? 'instock' : 'outofstock';
			}
			$variation->set_stock_status( $stock_status );

			$backorders = $variation->get_backorders();
			if ( isset( $data['backorders'] ) ) {
				$backorders = $data['backorders'];
			}
			$variation->set_backorders( $backorders );

			if ( $manage_stock ) {
				if ( isset( $data['stock_quantity'] ) ) {
					$variation->set_stock_quantity( $data['stock_quantity'] );
				}
			} else {
				$variation->set_backorders( 'no' );
				$variation->set_stock_quantity( '' );
			}

			// Regular Price.
			if ( isset( $data['regular_price'] ) ) {
				$variation->set_regular_price( $data['regular_price'] );
			}

			// Sale Price.
			if ( isset( $data['sale_price'] ) ) {
				$variation->set_sale_price( $data['sale_price'] );
			}

			if ( isset( $data['sale_price_dates_from'] ) ) {
				$variation->set_date_on_sale_from( $data['sale_price_dates_from'] );
			}

			if ( isset( $data['sale_price_dates_to'] ) ) {
				$variation->set_date_on_sale_to( $data['sale_price_dates_to'] );
			}

			// Tax class.
			if ( isset( $data['tax_class'] ) ) {
				$variation->set_tax_class( $data['tax_class'] );
			}

			// Update taxonomies.
			if ( isset( $data['attributes'] ) ) {
				$_attributes = array();

				foreach ( $data['attributes'] as $attribute_key => $attribute ) {
					if ( ! isset( $attribute['name'] ) ) {
						continue;
					}

					$taxonomy   = 0;
					$_attribute = array();

					if ( isset( $attribute['slug'] ) ) {
						$taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] );
					}

					if ( ! $taxonomy ) {
						$taxonomy = sanitize_title( $attribute['name'] );
					}

					if ( isset( $attributes[ $taxonomy ] ) ) {
						$_attribute = $attributes[ $taxonomy ];
					}

					if ( isset( $_attribute['is_variation'] ) && $_attribute['is_variation'] ) {
						$_attribute_key = sanitize_title( $_attribute['name'] );

						if ( isset( $_attribute['is_taxonomy'] ) && $_attribute['is_taxonomy'] ) {
							// Don't use wc_clean as it destroys sanitized characters
							$_attribute_value = isset( $attribute['option'] ) ? sanitize_title( stripslashes( $attribute['option'] ) ) : '';
						} else {
							$_attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : '';
						}

						$_attributes[ $_attribute_key ] = $_attribute_value;
					}
				}

				$variation->set_attributes( $_attributes );
			}

			$variation->save();

			do_action( 'woocommerce_api_save_product_variation', $variation_id, $menu_order, $variation );
		}

		return true;
	}

	/**
	 * Save product shipping data
	 *
	 * @since 2.2
	 * @param WC_Product $product
	 * @param array $data
	 * @return WC_Product
	 */
	private function save_product_shipping_data( $product, $data ) {
		if ( isset( $data['weight'] ) ) {
			$product->set_weight( '' === $data['weight'] ? '' : wc_format_decimal( $data['weight'] ) );
		}

		// Product dimensions
		if ( isset( $data['dimensions'] ) ) {
			// Height
			if ( isset( $data['dimensions']['height'] ) ) {
				$product->set_height( '' === $data['dimensions']['height'] ? '' : wc_format_decimal( $data['dimensions']['height'] ) );
			}

			// Width
			if ( isset( $data['dimensions']['width'] ) ) {
				$product->set_width( '' === $data['dimensions']['width'] ? '' : wc_format_decimal( $data['dimensions']['width'] ) );
			}

			// Length
			if ( isset( $data['dimensions']['length'] ) ) {
				$product->set_length( '' === $data['dimensions']['length'] ? '' : wc_format_decimal( $data['dimensions']['length'] ) );
			}
		}

		// Virtual
		if ( isset( $data['virtual'] ) ) {
			$virtual = ( true === $data['virtual'] ) ? 'yes' : 'no';

			if ( 'yes' == $virtual ) {
				$product->set_weight( '' );
				$product->set_height( '' );
				$product->set_length( '' );
				$product->set_width( '' );
			}
		}

		// Shipping class
		if ( isset( $data['shipping_class'] ) ) {
			$data_store        = $product->get_data_store();
			$shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $data['shipping_class'] ) );
			$product->set_shipping_class_id( $shipping_class_id );
		}

		return $product;
	}

	/**
	 * Save downloadable files
	 *
	 * @since 2.2
	 * @param WC_Product $product
	 * @param array $downloads
	 * @param int $deprecated Deprecated since 3.0.
	 * @return WC_Product
	 */
	private function save_downloadable_files( $product, $downloads, $deprecated = 0 ) {
		if ( $deprecated ) {
			wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() does not require a variation_id anymore.' );
		}

		$files = array();
		foreach ( $downloads as $key => $file ) {
			if ( isset( $file['url'] ) ) {
				$file['file'] = $file['url'];
			}

			if ( empty( $file['file'] ) ) {
				continue;
			}

			$download = new WC_Product_Download();
			$download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() );
			$download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) );
			$download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) );
			$files[]  = $download;
		}
		$product->set_downloads( $files );

		return $product;
	}

	/**
	 * Get attribute taxonomy by slug.
	 *
	 * @since 2.2
	 * @param string $slug
	 * @return string|null
	 */
	private function get_attribute_taxonomy_by_slug( $slug ) {
		$taxonomy = null;
		$attribute_taxonomies = wc_get_attribute_taxonomies();

		foreach ( $attribute_taxonomies as $key => $tax ) {
			if ( $slug == $tax->attribute_name ) {
				$taxonomy = 'pa_' . $tax->attribute_name;

				break;
			}
		}

		return $taxonomy;
	}

	/**
	 * Get the images for a product or product variation
	 *
	 * @since 2.1
	 * @param WC_Product|WC_Product_Variation $product
	 * @return array
	 */
	private function get_images( $product ) {
		$images        = $attachment_ids = array();
		$product_image = $product->get_image_id();

		// Add featured image.
		if ( ! empty( $product_image ) ) {
			$attachment_ids[] = $product_image;
		}

		// Add gallery images.
		$attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() );

		// Build image data.
		foreach ( $attachment_ids as $position => $attachment_id ) {

			$attachment_post = get_post( $attachment_id );

			if ( is_null( $attachment_post ) ) {
				continue;
			}

			$attachment = wp_get_attachment_image_src( $attachment_id, 'full' );

			if ( ! is_array( $attachment ) ) {
				continue;
			}

			$images[] = array(
				'id'         => (int) $attachment_id,
				'created_at' => $this->server->format_datetime( $attachment_post->post_date_gmt ),
				'updated_at' => $this->server->format_datetime( $attachment_post->post_modified_gmt ),
				'src'        => current( $attachment ),
				'title'      => get_the_title( $attachment_id ),
				'alt'        => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ),
				'position'   => (int) $position,
			);
		}

		// Set a placeholder image if the product has no images set.
		if ( empty( $images ) ) {

			$images[] = array(
				'id'         => 0,
				'created_at' => $this->server->format_datetime( time() ), // Default to now.
				'updated_at' => $this->server->format_datetime( time() ),
				'src'        => wc_placeholder_img_src(),
				'title'      => __( 'Placeholder', 'woocommerce' ),
				'alt'        => __( 'Placeholder', 'woocommerce' ),
				'position'   => 0,
			);
		}

		return $images;
	}

	/**
	 * Save product images
	 *
	 * @since  2.2
	 *
	 * @param WC_Product $product
	 * @param array      $images
	 *
	 * @return WC_Product
	 * @throws WC_API_Exception
	 */
	protected function save_product_images( $product, $images ) {
		if ( is_array( $images ) ) {
			$gallery = array();

			foreach ( $images as $image ) {
				if ( isset( $image['position'] ) && 0 == $image['position'] ) {
					$attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0;

					if ( 0 === $attachment_id && isset( $image['src'] ) ) {
						$upload = $this->upload_product_image( esc_url_raw( $image['src'] ) );

						if ( is_wp_error( $upload ) ) {
							throw new WC_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 );
						}

						$attachment_id = $this->set_product_image_as_attachment( $upload, $product->get_id() );
					}

					$product->set_image_id( $attachment_id );
				} else {
					$attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0;

					if ( 0 === $attachment_id && isset( $image['src'] ) ) {
						$upload = $this->upload_product_image( esc_url_raw( $image['src'] ) );

						if ( is_wp_error( $upload ) ) {
							throw new WC_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 );
						}

						$gallery[] = $this->set_product_image_as_attachment( $upload, $product->get_id() );
					} else {
						$gallery[] = $attachment_id;
					}
				}
			}

			if ( ! empty( $gallery ) ) {
				$product->set_gallery_image_ids( $gallery );
			}
		} else {
			$product->set_image_id( '' );
			$product->set_gallery_image_ids( array() );
		}

		return $product;
	}

	/**
	 * Upload image from URL
	 *
	 * @since  2.2
	 *
	 * @param  string $image_url
	 *
	 * @return array
	 *
	 * @throws WC_API_Exception
	 */
	public function upload_product_image( $image_url ) {
		$upload = wc_rest_upload_image_from_url( $image_url );
		if ( is_wp_error( $upload ) ) {
			throw new WC_API_Exception( 'woocommerce_api_product_image_upload_error', $upload->get_error_message(), 400 );
		}

		return $upload;
	}

	/**
	 * Sets product image as attachment and returns the attachment ID.
	 *
	 * @since 2.2
	 * @param array $upload
	 * @param int $id
	 * @return int
	 */
	protected function set_product_image_as_attachment( $upload, $id ) {
		$info    = wp_check_filetype( $upload['file'] );
		$title   = '';
		$content = '';

		if ( $image_meta = @wp_read_image_metadata( $upload['file'] ) ) {
			if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) {
				$title = wc_clean( $image_meta['title'] );
			}
			if ( trim( $image_meta['caption'] ) ) {
				$content = wc_clean( $image_meta['caption'] );
			}
		}

		$attachment = array(
			'post_mime_type' => $info['type'],
			'guid'           => $upload['url'],
			'post_parent'    => $id,
			'post_title'     => $title,
			'post_content'   => $content,
		);

		$attachment_id = wp_insert_attachment( $attachment, $upload['file'], $id );
		if ( ! is_wp_error( $attachment_id ) ) {
			wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) );
		}

		return $attachment_id;
	}

	/**
	 * Get attribute options.
	 *
	 * @param int $product_id
	 * @param array $attribute
	 * @return array
	 */
	protected function get_attribute_options( $product_id, $attribute ) {
		if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) {
			return wc_get_product_terms( $product_id, $attribute['name'], array( 'fields' => 'names' ) );
		} elseif ( isset( $attribute['value'] ) ) {
			return array_map( 'trim', explode( '|', $attribute['value'] ) );
		}

		return array();
	}

	/**
	 * Get the attributes for a product or product variation
	 *
	 * @since 2.1
	 * @param WC_Product|WC_Product_Variation $product
	 * @return array
	 */
	private function get_attributes( $product ) {

		$attributes = array();

		if ( $product->is_type( 'variation' ) ) {

			// variation attributes
			foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) {

				// taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_`
				$attributes[] = array(
					'name'   => wc_attribute_label( str_replace( 'attribute_', '', $attribute_name ) ),
					'slug'   => str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $attribute_name ) ),
					'option' => $attribute,
				);
			}
		} else {

			foreach ( $product->get_attributes() as $attribute ) {
				$attributes[] = array(
					'name'      => wc_attribute_label( $attribute['name'] ),
					'slug'      => wc_attribute_taxonomy_slug( $attribute['name'] ),
					'position'  => (int) $attribute['position'],
					'visible'   => (bool) $attribute['is_visible'],
					'variation' => (bool) $attribute['is_variation'],
					'options'   => $this->get_attribute_options( $product->get_id(), $attribute ),
				);
			}
		}

		return $attributes;
	}

	/**
	 * Get the downloads for a product or product variation
	 *
	 * @since 2.1
	 * @param WC_Product|WC_Product_Variation $product
	 * @return array
	 */
	private function get_downloads( $product ) {

		$downloads = array();

		if ( $product->is_downloadable() ) {

			foreach ( $product->get_downloads() as $file_id => $file ) {

				$downloads[] = array(
					'id'   => $file_id, // do not cast as int as this is a hash
					'name' => $file['name'],
					'file' => $file['file'],
				);
			}
		}

		return $downloads;
	}

	/**
	 * Get a listing of product attributes
	 *
	 * @since 2.4.0
	 *
	 * @param string|null $fields fields to limit response to
	 *
	 * @return array|WP_Error
	 */
	public function get_product_attributes( $fields = null ) {
		try {
			// Permissions check
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attributes', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 );
			}

			$product_attributes   = array();
			$attribute_taxonomies = wc_get_attribute_taxonomies();

			foreach ( $attribute_taxonomies as $attribute ) {
				$product_attributes[] = array(
					'id'           => intval( $attribute->attribute_id ),
					'name'         => $attribute->attribute_label,
					'slug'         => wc_attribute_taxonomy_name( $attribute->attribute_name ),
					'type'         => $attribute->attribute_type,
					'order_by'     => $attribute->attribute_orderby,
					'has_archives' => (bool) $attribute->attribute_public,
				);
			}

			return array( 'product_attributes' => apply_filters( 'woocommerce_api_product_attributes_response', $product_attributes, $attribute_taxonomies, $fields, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get the product attribute for the given ID
	 *
	 * @since 2.4.0
	 *
	 * @param string $id product attribute term ID
	 * @param string|null $fields fields to limit response to
	 *
	 * @return array|WP_Error
	 */
	public function get_product_attribute( $id, $fields = null ) {
		global $wpdb;

		try {
			$id = absint( $id );

			// Validate ID
			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'Invalid product attribute ID', 'woocommerce' ), 400 );
			}

			// Permissions check
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 );
			}

			$attribute = $wpdb->get_row( $wpdb->prepare( "
				SELECT *
				FROM {$wpdb->prefix}woocommerce_attribute_taxonomies
				WHERE attribute_id = %d
			 ", $id ) );

			if ( is_wp_error( $attribute ) || is_null( $attribute ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$product_attribute = array(
				'id'           => intval( $attribute->attribute_id ),
				'name'         => $attribute->attribute_label,
				'slug'         => wc_attribute_taxonomy_name( $attribute->attribute_name ),
				'type'         => $attribute->attribute_type,
				'order_by'     => $attribute->attribute_orderby,
				'has_archives' => (bool) $attribute->attribute_public,
			);

			return array( 'product_attribute' => apply_filters( 'woocommerce_api_product_attribute_response', $product_attribute, $id, $fields, $attribute, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Validate attribute data.
	 *
	 * @since  2.4.0
	 * @param  string $name
	 * @param  string $slug
	 * @param  string $type
	 * @param  string $order_by
	 * @param  bool   $new_data
	 * @return bool
	 * @throws WC_API_Exception
	 */
	protected function validate_attribute_data( $name, $slug, $type, $order_by, $new_data = true ) {
		if ( empty( $name ) ) {
			throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 );
		}

		if ( strlen( $slug ) >= 28 ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $slug ), 400 );
		} elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $slug ), 400 );
		} elseif ( $new_data && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'woocommerce' ), $slug ), 400 );
		}

		// Validate the attribute type
		if ( ! in_array( wc_clean( $type ), array_keys( wc_get_attribute_types() ) ) ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_type', sprintf( __( 'Invalid product attribute type - the product attribute type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_attribute_types() ) ) ), 400 );
		}

		// Validate the attribute order by
		if ( ! in_array( wc_clean( $order_by ), array( 'menu_order', 'name', 'name_num', 'id' ) ) ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_order_by', sprintf( __( 'Invalid product attribute order_by type - the product attribute order_by type must be any of these: %s', 'woocommerce' ), implode( ', ', array( 'menu_order', 'name', 'name_num', 'id' ) ) ), 400 );
		}

		return true;
	}

	/**
	 * Create a new product attribute
	 *
	 * @since 2.4.0
	 *
	 * @param array $data posted data
	 *
	 * @return array|WP_Error
	 */
	public function create_product_attribute( $data ) {
		global $wpdb;

		try {
			if ( ! isset( $data['product_attribute'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_attribute' ), 400 );
			}

			$data = $data['product_attribute'];

			// Check permissions
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_attribute', __( 'You do not have permission to create product attributes', 'woocommerce' ), 401 );
			}

			$data = apply_filters( 'woocommerce_api_create_product_attribute_data', $data, $this );

			if ( ! isset( $data['name'] ) ) {
				$data['name'] = '';
			}

			// Set the attribute slug
			if ( ! isset( $data['slug'] ) ) {
				$data['slug'] = wc_sanitize_taxonomy_name( stripslashes( $data['name'] ) );
			} else {
				$data['slug'] = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) ) );
			}

			// Set attribute type when not sent
			if ( ! isset( $data['type'] ) ) {
				$data['type'] = 'select';
			}

			// Set order by when not sent
			if ( ! isset( $data['order_by'] ) ) {
				$data['order_by'] = 'menu_order';
			}

			// Validate the attribute data
			$this->validate_attribute_data( $data['name'], $data['slug'], $data['type'], $data['order_by'], true );

			$insert = $wpdb->insert(
				$wpdb->prefix . 'woocommerce_attribute_taxonomies',
				array(
					'attribute_label'   => $data['name'],
					'attribute_name'    => $data['slug'],
					'attribute_type'    => $data['type'],
					'attribute_orderby' => $data['order_by'],
					'attribute_public'  => isset( $data['has_archives'] ) && true === $data['has_archives'] ? 1 : 0,
				),
				array( '%s', '%s', '%s', '%s', '%d' )
			);

			// Checks for an error in the product creation
			if ( is_wp_error( $insert ) ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_attribute', $insert->get_error_message(), 400 );
			}

			$id = $wpdb->insert_id;

			do_action( 'woocommerce_api_create_product_attribute', $id, $data );

			// Clear transients
			delete_transient( 'wc_attribute_taxonomies' );
			WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' );

			$this->server->send_status( 201 );

			return $this->get_product_attribute( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Edit a product attribute
	 *
	 * @since 2.4.0
	 *
	 * @param int $id the attribute ID
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function edit_product_attribute( $id, $data ) {
		global $wpdb;

		try {
			if ( ! isset( $data['product_attribute'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_attribute' ), 400 );
			}

			$id   = absint( $id );
			$data = $data['product_attribute'];

			// Check permissions
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_attribute', __( 'You do not have permission to edit product attributes', 'woocommerce' ), 401 );
			}

			$data      = apply_filters( 'woocommerce_api_edit_product_attribute_data', $data, $this );
			$attribute = $this->get_product_attribute( $id );

			if ( is_wp_error( $attribute ) ) {
				return $attribute;
			}

			$attribute_name     = isset( $data['name'] ) ? $data['name'] : $attribute['product_attribute']['name'];
			$attribute_type     = isset( $data['type'] ) ? $data['type'] : $attribute['product_attribute']['type'];
			$attribute_order_by = isset( $data['order_by'] ) ? $data['order_by'] : $attribute['product_attribute']['order_by'];

			if ( isset( $data['slug'] ) ) {
				$attribute_slug = wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) );
			} else {
				$attribute_slug = $attribute['product_attribute']['slug'];
			}
			$attribute_slug = preg_replace( '/^pa\_/', '', $attribute_slug );

			if ( isset( $data['has_archives'] ) ) {
				$attribute_public = true === $data['has_archives'] ? 1 : 0;
			} else {
				$attribute_public = $attribute['product_attribute']['has_archives'];
			}

			// Validate the attribute data
			$this->validate_attribute_data( $attribute_name, $attribute_slug, $attribute_type, $attribute_order_by, false );

			$update = $wpdb->update(
				$wpdb->prefix . 'woocommerce_attribute_taxonomies',
				array(
					'attribute_label'   => $attribute_name,
					'attribute_name'    => $attribute_slug,
					'attribute_type'    => $attribute_type,
					'attribute_orderby' => $attribute_order_by,
					'attribute_public'  => $attribute_public,
				),
				array( 'attribute_id' => $id ),
				array( '%s', '%s', '%s', '%s', '%d' ),
				array( '%d' )
			);

			// Checks for an error in the product creation
			if ( false === $update ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_attribute', __( 'Could not edit the attribute', 'woocommerce' ), 400 );
			}

			do_action( 'woocommerce_api_edit_product_attribute', $id, $data );

			// Clear transients
			delete_transient( 'wc_attribute_taxonomies' );
			WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' );

			return $this->get_product_attribute( $id );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Delete a product attribute
	 *
	 * @since  2.4.0
	 *
	 * @param  int $id the product attribute ID
	 *
	 * @return array|WP_Error
	 */
	public function delete_product_attribute( $id ) {
		global $wpdb;

		try {
			// Check permissions
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_attribute', __( 'You do not have permission to delete product attributes', 'woocommerce' ), 401 );
			}

			$id = absint( $id );

			$attribute_name = $wpdb->get_var( $wpdb->prepare( "
				SELECT attribute_name
				FROM {$wpdb->prefix}woocommerce_attribute_taxonomies
				WHERE attribute_id = %d
			 ", $id ) );

			if ( is_null( $attribute_name ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 );
			}

			$deleted = $wpdb->delete(
				$wpdb->prefix . 'woocommerce_attribute_taxonomies',
				array( 'attribute_id' => $id ),
				array( '%d' )
			);

			if ( false === $deleted ) {
				throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute', __( 'Could not delete the attribute', 'woocommerce' ), 401 );
			}

			$taxonomy = wc_attribute_taxonomy_name( $attribute_name );

			if ( taxonomy_exists( $taxonomy ) ) {
				$terms = get_terms( $taxonomy, 'orderby=name&hide_empty=0' );
				foreach ( $terms as $term ) {
					wp_delete_term( $term->term_id, $taxonomy );
				}
			}

			do_action( 'woocommerce_attribute_deleted', $id, $attribute_name, $taxonomy );
			do_action( 'woocommerce_api_delete_product_attribute', $id, $this );

			// Clear transients
			delete_transient( 'wc_attribute_taxonomies' );
			WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' );

			return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_attribute' ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Get product by SKU
	 *
	 * @deprecated 2.4.0
	 *
	 * @since  2.3.0
	 *
	 * @param  int    $sku the product SKU
	 * @param  string $fields
	 *
	 * @return array|WP_Error
	 */
	public function get_product_by_sku( $sku, $fields = null ) {
		try {
			$id = wc_get_product_id_by_sku( $sku );

			if ( empty( $id ) ) {
				throw new WC_API_Exception( 'woocommerce_api_invalid_product_sku', __( 'Invalid product SKU', 'woocommerce' ), 404 );
			}

			return $this->get_product( $id, $fields );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Clear product
	 *
	 * @param int $product_id
	 */
	protected function clear_product( $product_id ) {
		if ( ! is_numeric( $product_id ) || 0 >= $product_id ) {
			return;
		}

		// Delete product attachments
		$attachments = get_children( array(
			'post_parent' => $product_id,
			'post_status' => 'any',
			'post_type'   => 'attachment',
		) );

		foreach ( (array) $attachments as $attachment ) {
			wp_delete_attachment( $attachment->ID, true );
		}

		// Delete product
		$product = wc_get_product( $product_id );
		$product->delete();
	}

	/**
	 * Bulk update or insert products
	 * Accepts an array with products in the formats supported by
	 * WC_API_Products->create_product() and WC_API_Products->edit_product()
	 *
	 * @since 2.4.0
	 *
	 * @param array $data
	 *
	 * @return array|WP_Error
	 */
	public function bulk( $data ) {

		try {
			if ( ! isset( $data['products'] ) ) {
				throw new WC_API_Exception( 'woocommerce_api_missing_products_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'products' ), 400 );
			}

			$data  = $data['products'];
			$limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'products' );

			// Limit bulk operation
			if ( count( $data ) > $limit ) {
				throw new WC_API_Exception( 'woocommerce_api_products_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 );
			}

			$products = array();

			foreach ( $data as $_product ) {
				$product_id  = 0;
				$product_sku = '';

				// Try to get the product ID
				if ( isset( $_product['id'] ) ) {
					$product_id = intval( $_product['id'] );
				}

				if ( ! $product_id && isset( $_product['sku'] ) ) {
					$product_sku = wc_clean( $_product['sku'] );
					$product_id  = wc_get_product_id_by_sku( $product_sku );
				}

				if ( $product_id ) {

					// Product exists / edit product
					$edit = $this->edit_product( $product_id, array( 'product' => $_product ) );

					if ( is_wp_error( $edit ) ) {
						$products[] = array(
							'id'    => $product_id,
							'sku'   => $product_sku,
							'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ),
						);
					} else {
						$products[] = $edit['product'];
					}
				} else {

					// Product don't exists / create product
					$new = $this->create_product( array( 'product' => $_product ) );

					if ( is_wp_error( $new ) ) {
						$products[] = array(
							'id'    => $product_id,
							'sku'   => $product_sku,
							'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ),
						);
					} else {
						$products[] = $new['product'];
					}
				}
			}

			return array( 'products' => apply_filters( 'woocommerce_api_products_bulk_response', $products, $this ) );
		} catch ( WC_API_Exception $e ) {
			return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}
}
legacy/api/v2/interface-wc-api-handler.php000064400000001515151336044770014366 0ustar00<?php
/**
 * WooCommerce API
 *
 * Defines an interface that API request/response handlers should implement
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

interface WC_API_Handler {

	/**
	 * Get the content type for the response
	 *
	 * This should return the proper HTTP content-type for the response
	 *
	 * @since 2.1
	 * @return string
	 */
	public function get_content_type();

	/**
	 * Parse the raw request body entity into an array
	 *
	 * @since 2.1
	 * @param string $data
	 * @return array
	 */
	public function parse_body( $data );

	/**
	 * Generate a response from an array of data
	 *
	 * @since 2.1
	 * @param array $data
	 * @return string
	 */
	public function generate_response( $data );

}
legacy/api/v2/class-wc-api-authentication.php000064400000027436151336044770015147 0ustar00<?php
/**
 * WooCommerce API Authentication Class
 *
 * @author   WooThemes
 * @category API
 * @package  WooCommerce\RestApi
 * @since    2.1.0
 * @version  2.4.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WC_API_Authentication {

	/**
	 * Setup class
	 *
	 * @since 2.1
	 */
	public function __construct() {

		// To disable authentication, hook into this filter at a later priority and return a valid WP_User
		add_filter( 'woocommerce_api_check_authentication', array( $this, 'authenticate' ), 0 );
	}

	/**
	 * Authenticate the request. The authentication method varies based on whether the request was made over SSL or not.
	 *
	 * @since 2.1
	 * @param WP_User $user
	 * @return null|WP_Error|WP_User
	 */
	public function authenticate( $user ) {

		// Allow access to the index by default
		if ( '/' === WC()->api->server->path ) {
			return new WP_User( 0 );
		}

		try {

			if ( is_ssl() ) {
				$keys = $this->perform_ssl_authentication();
			} else {
				$keys = $this->perform_oauth_authentication();
			}

			// Check API key-specific permission
			$this->check_api_key_permissions( $keys['permissions'] );

			$user = $this->get_user_by_id( $keys['user_id'] );

			$this->update_api_key_last_access( $keys['key_id'] );

		} catch ( Exception $e ) {
			$user = new WP_Error( 'woocommerce_api_authentication_error', $e->getMessage(), array( 'status' => $e->getCode() ) );
		}

		return $user;
	}

	/**
	 * SSL-encrypted requests are not subject to sniffing or man-in-the-middle
	 * attacks, so the request can be authenticated by simply looking up the user
	 * associated with the given consumer key and confirming the consumer secret
	 * provided is valid
	 *
	 * @since 2.1
	 * @return array
	 * @throws Exception
	 */
	private function perform_ssl_authentication() {

		$params = WC()->api->server->params['GET'];

		// Get consumer key
		if ( ! empty( $_SERVER['PHP_AUTH_USER'] ) ) {

			// Should be in HTTP Auth header by default
			$consumer_key = $_SERVER['PHP_AUTH_USER'];

		} elseif ( ! empty( $params['consumer_key'] ) ) {

			// Allow a query string parameter as a fallback
			$consumer_key = $params['consumer_key'];

		} else {

			throw new Exception( __( 'Consumer key is missing.', 'woocommerce' ), 404 );
		}

		// Get consumer secret
		if ( ! empty( $_SERVER['PHP_AUTH_PW'] ) ) {

			// Should be in HTTP Auth header by default
			$consumer_secret = $_SERVER['PHP_AUTH_PW'];

		} elseif ( ! empty( $params['consumer_secret'] ) ) {

			// Allow a query string parameter as a fallback
			$consumer_secret = $params['consumer_secret'];

		} else {

			throw new Exception( __( 'Consumer secret is missing.', 'woocommerce' ), 404 );
		}

		$keys = $this->get_keys_by_consumer_key( $consumer_key );

		if ( ! $this->is_consumer_secret_valid( $keys['consumer_secret'], $consumer_secret ) ) {
			throw new Exception( __( 'Consumer secret is invalid.', 'woocommerce' ), 401 );
		}

		return $keys;
	}

	/**
	 * Perform OAuth 1.0a "one-legged" (http://oauthbible.com/#oauth-10a-one-legged) authentication for non-SSL requests
	 *
	 * This is required so API credentials cannot be sniffed or intercepted when making API requests over plain HTTP
	 *
	 * This follows the spec for simple OAuth 1.0a authentication (RFC 5849) as closely as possible, with two exceptions:
	 *
	 * 1) There is no token associated with request/responses, only consumer keys/secrets are used
	 *
	 * 2) The OAuth parameters are included as part of the request query string instead of part of the Authorization header,
	 *    This is because there is no cross-OS function within PHP to get the raw Authorization header
	 *
	 * @link http://tools.ietf.org/html/rfc5849 for the full spec
	 * @since 2.1
	 * @return array
	 * @throws Exception
	 */
	private function perform_oauth_authentication() {

		$params = WC()->api->server->params['GET'];

		$param_names = array( 'oauth_consumer_key', 'oauth_timestamp', 'oauth_nonce', 'oauth_signature', 'oauth_signature_method' );

		// Check for required OAuth parameters
		foreach ( $param_names as $param_name ) {

			if ( empty( $params[ $param_name ] ) ) {
				throw new Exception( sprintf( __( '%s parameter is missing', 'woocommerce' ), $param_name ), 404 );
			}
		}

		// Fetch WP user by consumer key
		$keys = $this->get_keys_by_consumer_key( $params['oauth_consumer_key'] );

		// Perform OAuth validation
		$this->check_oauth_signature( $keys, $params );
		$this->check_oauth_timestamp_and_nonce( $keys, $params['oauth_timestamp'], $params['oauth_nonce'] );

		// Authentication successful, return user
		return $keys;
	}

	/**
	 * Return the keys for the given consumer key
	 *
	 * @since 2.4.0
	 * @param string $consumer_key
	 * @return array
	 * @throws Exception
	 */
	private function get_keys_by_consumer_key( $consumer_key ) {
		global $wpdb;

		$consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) );

		$keys = $wpdb->get_row( $wpdb->prepare( "
			SELECT key_id, user_id, permissions, consumer_key, consumer_secret, nonces
			FROM {$wpdb->prefix}woocommerce_api_keys
			WHERE consumer_key = '%s'
		", $consumer_key ), ARRAY_A );

		if ( empty( $keys ) ) {
			throw new Exception( __( 'Consumer key is invalid.', 'woocommerce' ), 401 );
		}

		return $keys;
	}

	/**
	 * Get user by ID
	 *
	 * @since  2.4.0
	 * @param  int $user_id
	 * @return WP_User
	 * @throws Exception
	 */
	private function get_user_by_id( $user_id ) {
		$user = get_user_by( 'id', $user_id );

		if ( ! $user ) {
			throw new Exception( __( 'API user is invalid', 'woocommerce' ), 401 );
		}

		return $user;
	}

	/**
	 * Check if the consumer secret provided for the given user is valid
	 *
	 * @since 2.1
	 * @param string $keys_consumer_secret
	 * @param string $consumer_secret
	 * @return bool
	 */
	private function is_consumer_secret_valid( $keys_consumer_secret, $consumer_secret ) {
		return hash_equals( $keys_consumer_secret, $consumer_secret );
	}

	/**
	 * Verify that the consumer-provided request signature matches our generated signature, this ensures the consumer
	 * has a valid key/secret
	 *
	 * @param array $keys
	 * @param array $params the request parameters
	 * @throws Exception
	 */
	private function check_oauth_signature( $keys, $params ) {

		$http_method = strtoupper( WC()->api->server->method );

		$base_request_uri = rawurlencode( untrailingslashit( get_woocommerce_api_url( '' ) ) . WC()->api->server->path );

		// Get the signature provided by the consumer and remove it from the parameters prior to checking the signature
		$consumer_signature = rawurldecode( str_replace( ' ', '+', $params['oauth_signature'] ) );
		unset( $params['oauth_signature'] );

		// Remove filters and convert them from array to strings to void normalize issues
		if ( isset( $params['filter'] ) ) {
			$filters = $params['filter'];
			unset( $params['filter'] );
			foreach ( $filters as $filter => $filter_value ) {
				$params[ 'filter[' . $filter . ']' ] = $filter_value;
			}
		}

		// Normalize parameter key/values
		$params = $this->normalize_parameters( $params );

		// Sort parameters
		if ( ! uksort( $params, 'strcmp' ) ) {
			throw new Exception( __( 'Invalid signature - failed to sort parameters.', 'woocommerce' ), 401 );
		}

		// Form query string
		$query_params = array();
		foreach ( $params as $param_key => $param_value ) {

			$query_params[] = $param_key . '%3D' . $param_value; // join with equals sign
		}
		$query_string = implode( '%26', $query_params ); // join with ampersand

		$string_to_sign = $http_method . '&' . $base_request_uri . '&' . $query_string;

		if ( 'HMAC-SHA1' !== $params['oauth_signature_method'] && 'HMAC-SHA256' !== $params['oauth_signature_method'] ) {
			throw new Exception( __( 'Invalid signature - signature method is invalid.', 'woocommerce' ), 401 );
		}

		$hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) );

		$signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $keys['consumer_secret'], true ) );

		if ( ! hash_equals( $signature, $consumer_signature ) ) {
			throw new Exception( __( 'Invalid signature - provided signature does not match.', 'woocommerce' ), 401 );
		}
	}

	/**
	 * Normalize each parameter by assuming each parameter may have already been
	 * encoded, so attempt to decode, and then re-encode according to RFC 3986
	 *
	 * Note both the key and value is normalized so a filter param like:
	 *
	 * 'filter[period]' => 'week'
	 *
	 * is encoded to:
	 *
	 * 'filter%5Bperiod%5D' => 'week'
	 *
	 * This conforms to the OAuth 1.0a spec which indicates the entire query string
	 * should be URL encoded
	 *
	 * @since 2.1
	 * @see rawurlencode()
	 * @param array $parameters un-normalized parameters
	 * @return array normalized parameters
	 */
	private function normalize_parameters( $parameters ) {

		$normalized_parameters = array();

		foreach ( $parameters as $key => $value ) {

			// Percent symbols (%) must be double-encoded
			$key   = str_replace( '%', '%25', rawurlencode( rawurldecode( $key ) ) );
			$value = str_replace( '%', '%25', rawurlencode( rawurldecode( $value ) ) );

			$normalized_parameters[ $key ] = $value;
		}

		return $normalized_parameters;
	}

	/**
	 * Verify that the timestamp and nonce provided with the request are valid. This prevents replay attacks where
	 * an attacker could attempt to re-send an intercepted request at a later time.
	 *
	 * - A timestamp is valid if it is within 15 minutes of now
	 * - A nonce is valid if it has not been used within the last 15 minutes
	 *
	 * @param array $keys
	 * @param int $timestamp the unix timestamp for when the request was made
	 * @param string $nonce a unique (for the given user) 32 alphanumeric string, consumer-generated
	 * @throws Exception
	 */
	private function check_oauth_timestamp_and_nonce( $keys, $timestamp, $nonce ) {
		global $wpdb;

		$valid_window = 15 * 60; // 15 minute window

		if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) ) {
			throw new Exception( __( 'Invalid timestamp.', 'woocommerce' ), 401 );
		}

		$used_nonces = maybe_unserialize( $keys['nonces'] );

		if ( empty( $used_nonces ) ) {
			$used_nonces = array();
		}

		if ( in_array( $nonce, $used_nonces ) ) {
			throw new Exception( __( 'Invalid nonce - nonce has already been used.', 'woocommerce' ), 401 );
		}

		$used_nonces[ $timestamp ] = $nonce;

		// Remove expired nonces
		foreach ( $used_nonces as $nonce_timestamp => $nonce ) {
			if ( $nonce_timestamp < ( time() - $valid_window ) ) {
				unset( $used_nonces[ $nonce_timestamp ] );
			}
		}

		$used_nonces = maybe_serialize( $used_nonces );

		$wpdb->update(
			$wpdb->prefix . 'woocommerce_api_keys',
			array( 'nonces' => $used_nonces ),
			array( 'key_id' => $keys['key_id'] ),
			array( '%s' ),
			array( '%d' )
		);
	}

	/**
	 * Check that the API keys provided have the proper key-specific permissions to either read or write API resources
	 *
	 * @param string $key_permissions
	 * @throws Exception if the permission check fails
	 */
	public function check_api_key_permissions( $key_permissions ) {
		switch ( WC()->api->server->method ) {

			case 'HEAD':
			case 'GET':
				if ( 'read' !== $key_permissions && 'read_write' !== $key_permissions ) {
					throw new Exception( __( 'The API key provided does not have read permissions.', 'woocommerce' ), 401 );
				}
				break;

			case 'POST':
			case 'PUT':
			case 'PATCH':
			case 'DELETE':
				if ( 'write' !== $key_permissions && 'read_write' !== $key_permissions ) {
					throw new Exception( __( 'The API key provided does not have write permissions.', 'woocommerce' ), 401 );
				}
				break;
		}
	}

	/**
	 * Updated API Key last access datetime
	 *
	 * @since 2.4.0
	 *
	 * @param int $key_id
	 */
	private function update_api_key_last_access( $key_id ) {
		global $wpdb;

		$wpdb->update(
			$wpdb->prefix . 'woocommerce_api_keys',
			array( 'last_access' => current_time( 'mysql' ) ),
			array( 'key_id' => $key_id ),
			array( '%s' ),
			array( '%d' )
		);
	}
}
legacy/api/v2/class-wc-api-resource.php000064400000033103151336044770013743 0ustar00<?php
/**
 * WooCommerce API Resource class
 *
 * Provides shared functionality for resource-specific API classes
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WC_API_Resource {

	/** @var WC_API_Server the API server */
	protected $server;

	/** @var string sub-classes override this to set a resource-specific base route */
	protected $base;

	/**
	 * Setup class
	 *
	 * @since 2.1
	 * @param WC_API_Server $server
	 */
	public function __construct( WC_API_Server $server ) {

		$this->server = $server;

		// automatically register routes for sub-classes
		add_filter( 'woocommerce_api_endpoints', array( $this, 'register_routes' ) );

		// maybe add meta to top-level resource responses
		foreach ( array( 'order', 'coupon', 'customer', 'product', 'report' ) as $resource ) {
			add_filter( "woocommerce_api_{$resource}_response", array( $this, 'maybe_add_meta' ), 15, 2 );
		}

		$response_names = array(
			'order',
			'coupon',
			'customer',
			'product',
			'report',
			'customer_orders',
			'customer_downloads',
			'order_note',
			'order_refund',
			'product_reviews',
			'product_category',
		);

		foreach ( $response_names as $name ) {

			/**
			 * Remove fields from responses when requests specify certain fields
			 * note these are hooked at a later priority so data added via
			 * filters (e.g. customer data to the order response) still has the
			 * fields filtered properly
			 */
			add_filter( "woocommerce_api_{$name}_response", array( $this, 'filter_response_fields' ), 20, 3 );
		}
	}

	/**
	 * Validate the request by checking:
	 *
	 * 1) the ID is a valid integer
	 * 2) the ID returns a valid post object and matches the provided post type
	 * 3) the current user has the proper permissions to read/edit/delete the post
	 *
	 * @since 2.1
	 * @param string|int $id the post ID
	 * @param string $type the post type, either `shop_order`, `shop_coupon`, or `product`
	 * @param string $context the context of the request, either `read`, `edit` or `delete`
	 * @return int|WP_Error valid post ID or WP_Error if any of the checks fails
	 */
	protected function validate_request( $id, $type, $context ) {

		if ( 'shop_order' === $type || 'shop_coupon' === $type || 'shop_webhook' === $type ) {
			$resource_name = str_replace( 'shop_', '', $type );
		} else {
			$resource_name = $type;
		}

		$id = absint( $id );

		// Validate ID
		if ( empty( $id ) ) {
			return new WP_Error( "woocommerce_api_invalid_{$resource_name}_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) );
		}

		// Only custom post types have per-post type/permission checks
		if ( 'customer' !== $type ) {

			$post = get_post( $id );

			if ( null === $post ) {
				return new WP_Error( "woocommerce_api_no_{$resource_name}_found", sprintf( __( 'No %1$s found with the ID equal to %2$s', 'woocommerce' ), $resource_name, $id ), array( 'status' => 404 ) );
			}

			// For checking permissions, product variations are the same as the product post type
			$post_type = ( 'product_variation' === $post->post_type ) ? 'product' : $post->post_type;

			// Validate post type
			if ( $type !== $post_type ) {
				return new WP_Error( "woocommerce_api_invalid_{$resource_name}", sprintf( __( 'Invalid %s', 'woocommerce' ), $resource_name ), array( 'status' => 404 ) );
			}

			// Validate permissions
			switch ( $context ) {

				case 'read':
					if ( ! $this->is_readable( $post ) ) {
						return new WP_Error( "woocommerce_api_user_cannot_read_{$resource_name}", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
					}
					break;

				case 'edit':
					if ( ! $this->is_editable( $post ) ) {
						return new WP_Error( "woocommerce_api_user_cannot_edit_{$resource_name}", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
					}
					break;

				case 'delete':
					if ( ! $this->is_deletable( $post ) ) {
						return new WP_Error( "woocommerce_api_user_cannot_delete_{$resource_name}", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
					}
					break;
			}
		}

		return $id;
	}

	/**
	 * Add common request arguments to argument list before WP_Query is run
	 *
	 * @since 2.1
	 * @param array $base_args required arguments for the query (e.g. `post_type`, etc)
	 * @param array $request_args arguments provided in the request
	 * @return array
	 */
	protected function merge_query_args( $base_args, $request_args ) {

		$args = array();

		// date
		if ( ! empty( $request_args['created_at_min'] ) || ! empty( $request_args['created_at_max'] ) || ! empty( $request_args['updated_at_min'] ) || ! empty( $request_args['updated_at_max'] ) ) {

			$args['date_query'] = array();

			// resources created after specified date
			if ( ! empty( $request_args['created_at_min'] ) ) {
				$args['date_query'][] = array( 'column' => 'post_date_gmt', 'after' => $this->server->parse_datetime( $request_args['created_at_min'] ), 'inclusive' => true );
			}

			// resources created before specified date
			if ( ! empty( $request_args['created_at_max'] ) ) {
				$args['date_query'][] = array( 'column' => 'post_date_gmt', 'before' => $this->server->parse_datetime( $request_args['created_at_max'] ), 'inclusive' => true );
			}

			// resources updated after specified date
			if ( ! empty( $request_args['updated_at_min'] ) ) {
				$args['date_query'][] = array( 'column' => 'post_modified_gmt', 'after' => $this->server->parse_datetime( $request_args['updated_at_min'] ), 'inclusive' => true );
			}

			// resources updated before specified date
			if ( ! empty( $request_args['updated_at_max'] ) ) {
				$args['date_query'][] = array( 'column' => 'post_modified_gmt', 'before' => $this->server->parse_datetime( $request_args['updated_at_max'] ), 'inclusive' => true );
			}
		}

		// search
		if ( ! empty( $request_args['q'] ) ) {
			$args['s'] = $request_args['q'];
		}

		// resources per response
		if ( ! empty( $request_args['limit'] ) ) {
			$args['posts_per_page'] = $request_args['limit'];
		}

		// resource offset
		if ( ! empty( $request_args['offset'] ) ) {
			$args['offset'] = $request_args['offset'];
		}

		// order (ASC or DESC, ASC by default)
		if ( ! empty( $request_args['order'] ) ) {
			$args['order'] = $request_args['order'];
		}

		// orderby
		if ( ! empty( $request_args['orderby'] ) ) {
			$args['orderby'] = $request_args['orderby'];

			// allow sorting by meta value
			if ( ! empty( $request_args['orderby_meta_key'] ) ) {
				$args['meta_key'] = $request_args['orderby_meta_key'];
			}
		}

		// allow post status change
		if ( ! empty( $request_args['post_status'] ) ) {
			$args['post_status'] = $request_args['post_status'];
			unset( $request_args['post_status'] );
		}

		// filter by a list of post id
		if ( ! empty( $request_args['in'] ) ) {
			$args['post__in'] = explode( ',', $request_args['in'] );
			unset( $request_args['in'] );
		}

		// filter by a list of post id
		if ( ! empty( $request_args['in'] ) ) {
			$args['post__in'] = explode( ',', $request_args['in'] );
			unset( $request_args['in'] );
		}

		// resource page
		$args['paged'] = ( isset( $request_args['page'] ) ) ? absint( $request_args['page'] ) : 1;

		$args = apply_filters( 'woocommerce_api_query_args', $args, $request_args );

		return array_merge( $base_args, $args );
	}

	/**
	 * Add meta to resources when requested by the client. Meta is added as a top-level
	 * `<resource_name>_meta` attribute (e.g. `order_meta`) as a list of key/value pairs
	 *
	 * @since 2.1
	 * @param array $data the resource data
	 * @param object $resource the resource object (e.g WC_Order)
	 * @return mixed
	 */
	public function maybe_add_meta( $data, $resource ) {

		if ( isset( $this->server->params['GET']['filter']['meta'] ) && 'true' === $this->server->params['GET']['filter']['meta'] && is_object( $resource ) ) {

			// don't attempt to add meta more than once
			if ( preg_grep( '/[a-z]+_meta/', array_keys( $data ) ) ) {
				return $data;
			}

			// define the top-level property name for the meta
			switch ( get_class( $resource ) ) {

				case 'WC_Order':
					$meta_name = 'order_meta';
					break;

				case 'WC_Coupon':
					$meta_name = 'coupon_meta';
					break;

				case 'WP_User':
					$meta_name = 'customer_meta';
					break;

				default:
					$meta_name = 'product_meta';
					break;
			}

			if ( is_a( $resource, 'WP_User' ) ) {

				// customer meta
				$meta = (array) get_user_meta( $resource->ID );

			} else {

				// coupon/order/product meta
				$meta = (array) get_post_meta( $resource->get_id() );
			}

			foreach ( $meta as $meta_key => $meta_value ) {

				// don't add hidden meta by default
				if ( ! is_protected_meta( $meta_key ) ) {
					$data[ $meta_name ][ $meta_key ] = maybe_unserialize( $meta_value[0] );
				}
			}
		}

		return $data;
	}

	/**
	 * Restrict the fields included in the response if the request specified certain only certain fields should be returned
	 *
	 * @since 2.1
	 * @param array $data the response data
	 * @param object $resource the object that provided the response data, e.g. WC_Coupon or WC_Order
	 * @param array|string the requested list of fields to include in the response
	 * @return array response data
	 */
	public function filter_response_fields( $data, $resource, $fields ) {

		if ( ! is_array( $data ) || empty( $fields ) ) {
			return $data;
		}

		$fields = explode( ',', $fields );
		$sub_fields = array();

		// get sub fields
		foreach ( $fields as $field ) {

			if ( false !== strpos( $field, '.' ) ) {

				list( $name, $value ) = explode( '.', $field );

				$sub_fields[ $name ] = $value;
			}
		}

		// iterate through top-level fields
		foreach ( $data as $data_field => $data_value ) {

			// if a field has sub-fields and the top-level field has sub-fields to filter
			if ( is_array( $data_value ) && in_array( $data_field, array_keys( $sub_fields ) ) ) {

				// iterate through each sub-field
				foreach ( $data_value as $sub_field => $sub_field_value ) {

					// remove non-matching sub-fields
					if ( ! in_array( $sub_field, $sub_fields ) ) {
						unset( $data[ $data_field ][ $sub_field ] );
					}
				}
			} else {

				// remove non-matching top-level fields
				if ( ! in_array( $data_field, $fields ) ) {
					unset( $data[ $data_field ] );
				}
			}
		}

		return $data;
	}

	/**
	 * Delete a given resource
	 *
	 * @since 2.1
	 * @param int $id the resource ID
	 * @param string $type the resource post type, or `customer`
	 * @param bool $force true to permanently delete resource, false to move to trash (not supported for `customer`)
	 * @return array|WP_Error
	 */
	protected function delete( $id, $type, $force = false ) {

		if ( 'shop_order' === $type || 'shop_coupon' === $type ) {
			$resource_name = str_replace( 'shop_', '', $type );
		} else {
			$resource_name = $type;
		}

		if ( 'customer' === $type ) {

			$result = wp_delete_user( $id );

			if ( $result ) {
				return array( 'message' => __( 'Permanently deleted customer', 'woocommerce' ) );
			} else {
				return new WP_Error( 'woocommerce_api_cannot_delete_customer', __( 'The customer cannot be deleted', 'woocommerce' ), array( 'status' => 500 ) );
			}
		} else {

			// delete order/coupon/webhook
			$result = ( $force ) ? wp_delete_post( $id, true ) : wp_trash_post( $id );

			if ( ! $result ) {
				return new WP_Error( "woocommerce_api_cannot_delete_{$resource_name}", sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), $resource_name ), array( 'status' => 500 ) );
			}

			if ( $force ) {
				return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), $resource_name ) );
			} else {
				$this->server->send_status( '202' );

				return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), $resource_name ) );
			}
		}
	}


	/**
	 * Checks if the given post is readable by the current user
	 *
	 * @since 2.1
	 * @see WC_API_Resource::check_permission()
	 * @param WP_Post|int $post
	 * @return bool
	 */
	protected function is_readable( $post ) {

		return $this->check_permission( $post, 'read' );
	}

	/**
	 * Checks if the given post is editable by the current user
	 *
	 * @since 2.1
	 * @see WC_API_Resource::check_permission()
	 * @param WP_Post|int $post
	 * @return bool
	 */
	protected function is_editable( $post ) {

		return $this->check_permission( $post, 'edit' );

	}

	/**
	 * Checks if the given post is deletable by the current user
	 *
	 * @since 2.1
	 * @see WC_API_Resource::check_permission()
	 * @param WP_Post|int $post
	 * @return bool
	 */
	protected function is_deletable( $post ) {

		return $this->check_permission( $post, 'delete' );
	}

	/**
	 * Checks the permissions for the current user given a post and context
	 *
	 * @since 2.1
	 * @param WP_Post|int $post
	 * @param string $context the type of permission to check, either `read`, `write`, or `delete`
	 * @return bool true if the current user has the permissions to perform the context on the post
	 */
	private function check_permission( $post, $context ) {

		if ( ! is_a( $post, 'WP_Post' ) ) {
			$post = get_post( $post );
		}

		if ( is_null( $post ) ) {
			return false;
		}

		$post_type = get_post_type_object( $post->post_type );

		if ( 'read' === $context ) {
			return ( 'revision' !== $post->post_type && current_user_can( $post_type->cap->read_private_posts, $post->ID ) );
		} elseif ( 'edit' === $context ) {
			return current_user_can( $post_type->cap->edit_post, $post->ID );
		} elseif ( 'delete' === $context ) {
			return current_user_can( $post_type->cap->delete_post, $post->ID );
		} else {
			return false;
		}
	}
}
legacy/api/class-wc-rest-legacy-coupons-controller.php000064400000011710151336044770017102 0ustar00<?php
/**
 * REST API Legacy Coupons controller
 *
 * Handles requests to the /coupons endpoint.
 *
 * @author   WooThemes
 * @category API
 * @package  WooCommerce\RestApi
 * @since    3.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * REST API Legacy Coupons controller class.
 *
 * @package WooCommerce\RestApi
 * @extends WC_REST_CRUD_Controller
 */
class WC_REST_Legacy_Coupons_Controller extends WC_REST_CRUD_Controller {

	/**
	 * Query args.
	 *
	 * @deprecated 3.0.0
	 *
	 * @param array $args Query args
	 * @param WP_REST_Request $request Request data.
	 * @return array
	 */
	public function query_args( $args, $request ) {
		if ( ! empty( $request['code'] ) ) {
			$id = wc_get_coupon_id_by_code( $request['code'] );
			$args['post__in'] = array( $id );
		}

		return $args;
	}

	/**
	 * Prepare a single coupon output for response.
	 *
	 * @deprecated 3.0.0
	 *
	 * @param WP_Post $post Post object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $data
	 */
	public function prepare_item_for_response( $post, $request ) {
		$coupon = new WC_Coupon( (int) $post->ID );
		$data   = $coupon->get_data();

		$format_decimal = array( 'amount', 'minimum_amount', 'maximum_amount' );
		$format_date    = array( 'date_created', 'date_modified', 'date_expires' );
		$format_null    = array( 'usage_limit', 'usage_limit_per_user', 'limit_usage_to_x_items' );

		// Format decimal values.
		foreach ( $format_decimal as $key ) {
			$data[ $key ] = wc_format_decimal( $data[ $key ], 2 );
		}

		// Format date values.
		foreach ( $format_date as $key ) {
			$data[ $key ] = $data[ $key ] ? wc_rest_prepare_date_response( get_gmt_from_date( date( 'Y-m-d H:i:s', $data[ $key ] ) ) ) : null;
		}

		// Format null values.
		foreach ( $format_null as $key ) {
			$data[ $key ] = $data[ $key ] ? $data[ $key ] : null;
		}

		$context  = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data     = $this->add_additional_fields_to_object( $data, $request );
		$data     = $this->filter_response_by_context( $data, $context );
		$response = rest_ensure_response( $data );
		$response->add_links( $this->prepare_links( $post, $request ) );

		/**
		 * Filter the data for a response.
		 *
		 * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
		 * prepared for the response.
		 *
		 * @param WP_REST_Response   $response   The response object.
		 * @param WP_Post            $post       Post object.
		 * @param WP_REST_Request    $request    Request object.
		 */
		return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request );
	}

	/**
	 * Prepare a single coupon for create or update.
	 *
	 * @deprecated 3.0.0
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_Error|stdClass $data Post object.
	 */
	protected function prepare_item_for_database( $request ) {
		global $wpdb;

		$id        = isset( $request['id'] ) ? absint( $request['id'] ) : 0;
		$coupon    = new WC_Coupon( $id );
		$schema    = $this->get_item_schema();
		$data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) );

		// Validate required POST fields.
		if ( 'POST' === $request->get_method() && 0 === $coupon->get_id() ) {
			if ( empty( $request['code'] ) ) {
				return new WP_Error( 'woocommerce_rest_empty_coupon_code', sprintf( __( 'The coupon code cannot be empty.', 'woocommerce' ), 'code' ), array( 'status' => 400 ) );
			}
		}

		// Handle all writable props.
		foreach ( $data_keys as $key ) {
			$value = $request[ $key ];

			if ( ! is_null( $value ) ) {
				switch ( $key ) {
					case 'code' :
						$coupon_code = wc_format_coupon_code( $value );
						$id          = $coupon->get_id() ? $coupon->get_id() : 0;
						$id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id );

						if ( $id_from_code ) {
							return new WP_Error( 'woocommerce_rest_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), array( 'status' => 400 ) );
						}

						$coupon->set_code( $coupon_code );
						break;
					case 'meta_data' :
						if ( is_array( $value ) ) {
							foreach ( $value as $meta ) {
								$coupon->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
							}
						}
						break;
					case 'description' :
						$coupon->set_description( wp_filter_post_kses( $value ) );
						break;
					default :
						if ( is_callable( array( $coupon, "set_{$key}" ) ) ) {
							$coupon->{"set_{$key}"}( $value );
						}
						break;
				}
			}
		}

		/**
		 * Filter the query_vars used in `get_items` for the constructed query.
		 *
		 * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
		 * prepared for insertion.
		 *
		 * @param WC_Coupon       $coupon        The coupon object.
		 * @param WP_REST_Request $request       Request object.
		 */
		return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $coupon, $request );
	}
}
legacy/api/v1/class-wc-api-reports.php000064400000032372151336044770013620 0ustar00<?php
/**
 * WooCommerce API Reports Class
 *
 * Handles requests to the /reports endpoint
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 * @version     2.1
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WC_API_Reports extends WC_API_Resource {

	/** @var string $base the route base */
	protected $base = '/reports';

	/** @var WC_Admin_Report instance */
	private $report;

	/**
	 * Register the routes for this class
	 *
	 * GET /reports
	 * GET /reports/sales
	 *
	 * @since 2.1
	 * @param array $routes
	 * @return array
	 */
	public function register_routes( $routes ) {

		# GET /reports
		$routes[ $this->base ] = array(
			array( array( $this, 'get_reports' ),     WC_API_Server::READABLE ),
		);

		# GET /reports/sales
		$routes[ $this->base . '/sales' ] = array(
			array( array( $this, 'get_sales_report' ), WC_API_Server::READABLE ),
		);

		# GET /reports/sales/top_sellers
		$routes[ $this->base . '/sales/top_sellers' ] = array(
			array( array( $this, 'get_top_sellers_report' ), WC_API_Server::READABLE ),
		);

		return $routes;
	}

	/**
	 * Get a simple listing of available reports
	 *
	 * @since 2.1
	 * @return array
	 */
	public function get_reports() {

		return array( 'reports' => array( 'sales', 'sales/top_sellers' ) );
	}

	/**
	 * Get the sales report
	 *
	 * @since 2.1
	 * @param string $fields fields to include in response
	 * @param array $filter date filtering
	 * @return array|WP_Error
	 */
	public function get_sales_report( $fields = null, $filter = array() ) {

		// check user permissions
		$check = $this->validate_request();

		if ( is_wp_error( $check ) ) {
			return $check;
		}

		// set date filtering
		$this->setup_report( $filter );

		// total sales, taxes, shipping, and order count
		$totals = $this->report->get_order_report_data( array(
			'data' => array(
				'_order_total' => array(
					'type'     => 'meta',
					'function' => 'SUM',
					'name'     => 'sales',
				),
				'_order_tax' => array(
					'type'            => 'meta',
					'function'        => 'SUM',
					'name'            => 'tax',
				),
				'_order_shipping_tax' => array(
					'type'            => 'meta',
					'function'        => 'SUM',
					'name'            => 'shipping_tax',
				),
				'_order_shipping' => array(
					'type'     => 'meta',
					'function' => 'SUM',
					'name'     => 'shipping',
				),
				'ID' => array(
					'type'     => 'post_data',
					'function' => 'COUNT',
					'name'     => 'order_count',
				),
			),
			'filter_range' => true,
		) );

		// total items ordered
		$total_items = absint( $this->report->get_order_report_data( array(
			'data' => array(
				'_qty' => array(
					'type'            => 'order_item_meta',
					'order_item_type' => 'line_item',
					'function'        => 'SUM',
					'name'            => 'order_item_qty',
				),
			),
			'query_type' => 'get_var',
			'filter_range' => true,
		) ) );

		// total discount used
		$total_discount = $this->report->get_order_report_data( array(
			'data' => array(
				'discount_amount' => array(
					'type'            => 'order_item_meta',
					'order_item_type' => 'coupon',
					'function'        => 'SUM',
					'name'            => 'discount_amount',
				),
			),
			'where' => array(
				array(
					'key'      => 'order_item_type',
					'value'    => 'coupon',
					'operator' => '=',
				),
			),
			'query_type' => 'get_var',
			'filter_range' => true,
		) );

		// new customers
		$users_query = new WP_User_Query(
			array(
				'fields'  => array( 'user_registered' ),
				'role'    => 'customer',
			)
		);

		$customers = $users_query->get_results();

		foreach ( $customers as $key => $customer ) {
			if ( strtotime( $customer->user_registered ) < $this->report->start_date || strtotime( $customer->user_registered ) > $this->report->end_date ) {
				unset( $customers[ $key ] );
			}
		}

		$total_customers = count( $customers );

		// get order totals grouped by period
		$orders = $this->report->get_order_report_data( array(
			'data' => array(
				'_order_total' => array(
					'type'     => 'meta',
					'function' => 'SUM',
					'name'     => 'total_sales',
				),
				'_order_shipping' => array(
					'type'     => 'meta',
					'function' => 'SUM',
					'name'     => 'total_shipping',
				),
				'_order_tax' => array(
					'type'     => 'meta',
					'function' => 'SUM',
					'name'     => 'total_tax',
				),
				'_order_shipping_tax' => array(
					'type'     => 'meta',
					'function' => 'SUM',
					'name'     => 'total_shipping_tax',
				),
				'ID' => array(
					'type'     => 'post_data',
					'function' => 'COUNT',
					'name'     => 'total_orders',
					'distinct' => true,
				),
				'post_date' => array(
					'type'     => 'post_data',
					'function' => '',
					'name'     => 'post_date',
				),
			),
			'group_by'     => $this->report->group_by_query,
			'order_by'     => 'post_date ASC',
			'query_type'   => 'get_results',
			'filter_range' => true,
		) );

		// get order item totals grouped by period
		$order_items = $this->report->get_order_report_data( array(
			'data' => array(
				'_qty' => array(
					'type'            => 'order_item_meta',
					'order_item_type' => 'line_item',
					'function'        => 'SUM',
					'name'            => 'order_item_count',
				),
				'post_date' => array(
					'type'     => 'post_data',
					'function' => '',
					'name'     => 'post_date',
				),
			),
			'where' => array(
				array(
					'key'      => 'order_item_type',
					'value'    => 'line_item',
					'operator' => '=',
				),
			),
			'group_by'     => $this->report->group_by_query,
			'order_by'     => 'post_date ASC',
			'query_type'   => 'get_results',
			'filter_range' => true,
		) );

		// get discount totals grouped by period
		$discounts = $this->report->get_order_report_data( array(
			'data' => array(
				'discount_amount' => array(
					'type'            => 'order_item_meta',
					'order_item_type' => 'coupon',
					'function'        => 'SUM',
					'name'            => 'discount_amount',
				),
				'post_date' => array(
					'type'     => 'post_data',
					'function' => '',
					'name'     => 'post_date',
				),
			),
			'where' => array(
				array(
					'key'      => 'order_item_type',
					'value'    => 'coupon',
					'operator' => '=',
				),
			),
			'group_by'     => $this->report->group_by_query . ', order_item_name',
			'order_by'     => 'post_date ASC',
			'query_type'   => 'get_results',
			'filter_range' => true,
		) );

		$period_totals = array();

		// setup period totals by ensuring each period in the interval has data
		for ( $i = 0; $i <= $this->report->chart_interval; $i ++ ) {

			switch ( $this->report->chart_groupby ) {
				case 'day' :
					$time = date( 'Y-m-d', strtotime( "+{$i} DAY", $this->report->start_date ) );
					break;
				case 'month' :
					$time = date( 'Y-m', strtotime( "+{$i} MONTH", $this->report->start_date ) );
					break;
			}

			// set the customer signups for each period
			$customer_count = 0;
			foreach ( $customers as $customer ) {

				if ( date( ( 'day' == $this->report->chart_groupby ) ? 'Y-m-d' : 'Y-m', strtotime( $customer->user_registered ) ) == $time ) {
					$customer_count++;
				}
 			}

			$period_totals[ $time ] = array(
				'sales'     => wc_format_decimal( 0.00, 2 ),
				'orders'    => 0,
				'items'     => 0,
				'tax'       => wc_format_decimal( 0.00, 2 ),
				'shipping'  => wc_format_decimal( 0.00, 2 ),
				'discount'  => wc_format_decimal( 0.00, 2 ),
				'customers' => $customer_count,
			);
		}

		// add total sales, total order count, total tax and total shipping for each period
		foreach ( $orders as $order ) {

			$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) );

			if ( ! isset( $period_totals[ $time ] ) ) {
				continue;
			}

			$period_totals[ $time ]['sales']    = wc_format_decimal( $order->total_sales, 2 );
			$period_totals[ $time ]['orders']   = (int) $order->total_orders;
			$period_totals[ $time ]['tax']      = wc_format_decimal( $order->total_tax + $order->total_shipping_tax, 2 );
			$period_totals[ $time ]['shipping'] = wc_format_decimal( $order->total_shipping, 2 );
		}

		// add total order items for each period
		foreach ( $order_items as $order_item ) {

			$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order_item->post_date ) ) : date( 'Y-m', strtotime( $order_item->post_date ) );

			if ( ! isset( $period_totals[ $time ] ) ) {
				continue;
			}

			$period_totals[ $time ]['items'] = (int) $order_item->order_item_count;
		}

		// add total discount for each period
		foreach ( $discounts as $discount ) {

			$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $discount->post_date ) ) : date( 'Y-m', strtotime( $discount->post_date ) );

			if ( ! isset( $period_totals[ $time ] ) ) {
				continue;
			}

			$period_totals[ $time ]['discount'] = wc_format_decimal( $discount->discount_amount, 2 );
		}

		$sales_data = array(
			'total_sales'       => wc_format_decimal( $totals->sales, 2 ),
			'average_sales'     => wc_format_decimal( $totals->sales / ( $this->report->chart_interval + 1 ), 2 ),
			'total_orders'      => (int) $totals->order_count,
			'total_items'       => $total_items,
			'total_tax'         => wc_format_decimal( $totals->tax + $totals->shipping_tax, 2 ),
			'total_shipping'    => wc_format_decimal( $totals->shipping, 2 ),
			'total_discount'    => is_null( $total_discount ) ? wc_format_decimal( 0.00, 2 ) : wc_format_decimal( $total_discount, 2 ),
			'totals_grouped_by' => $this->report->chart_groupby,
			'totals'            => $period_totals,
			'total_customers'   => $total_customers,
		);

		return array( 'sales' => apply_filters( 'woocommerce_api_report_response', $sales_data, $this->report, $fields, $this->server ) );
	}

	/**
	 * Get the top sellers report
	 *
	 * @since 2.1
	 * @param string $fields fields to include in response
	 * @param array $filter date filtering
	 * @return array|WP_Error
	 */
	public function get_top_sellers_report( $fields = null, $filter = array() ) {

		// check user permissions
		$check = $this->validate_request();

		if ( is_wp_error( $check ) ) {
			return $check;
		}

		// set date filtering
		$this->setup_report( $filter );

		$top_sellers = $this->report->get_order_report_data( array(
			'data' => array(
				'_product_id' => array(
					'type'            => 'order_item_meta',
					'order_item_type' => 'line_item',
					'function'        => '',
					'name'            => 'product_id',
				),
				'_qty' => array(
					'type'            => 'order_item_meta',
					'order_item_type' => 'line_item',
					'function'        => 'SUM',
					'name'            => 'order_item_qty',
				),
			),
			'order_by'     => 'order_item_qty DESC',
			'group_by'     => 'product_id',
			'limit'        => isset( $filter['limit'] ) ? absint( $filter['limit'] ) : 12,
			'query_type'   => 'get_results',
			'filter_range' => true,
		) );

		$top_sellers_data = array();

		foreach ( $top_sellers as $top_seller ) {

			$product = wc_get_product( $top_seller->product_id );

			$top_sellers_data[] = array(
				'title'      => $product->get_name(),
				'product_id' => $top_seller->product_id,
				'quantity'   => $top_seller->order_item_qty,
			);
		}

		return array( 'top_sellers' => apply_filters( 'woocommerce_api_report_response', $top_sellers_data, $this->report, $fields, $this->server ) );
	}

	/**
	 * Setup the report object and parse any date filtering
	 *
	 * @since 2.1
	 * @param array $filter date filtering
	 */
	private function setup_report( $filter ) {

		include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' );

		$this->report = new WC_Admin_Report();

		if ( empty( $filter['period'] ) ) {

			// custom date range
			$filter['period'] = 'custom';

			if ( ! empty( $filter['date_min'] ) || ! empty( $filter['date_max'] ) ) {

				// overwrite _GET to make use of WC_Admin_Report::calculate_current_range() for custom date ranges
				$_GET['start_date'] = $this->server->parse_datetime( $filter['date_min'] );
				$_GET['end_date'] = isset( $filter['date_max'] ) ? $this->server->parse_datetime( $filter['date_max'] ) : null;

			} else {

				// default custom range to today
				$_GET['start_date'] = $_GET['end_date'] = date( 'Y-m-d', current_time( 'timestamp' ) );
			}
		} else {

			// ensure period is valid
			if ( ! in_array( $filter['period'], array( 'week', 'month', 'last_month', 'year' ) ) ) {
				$filter['period'] = 'week';
			}

			// TODO: change WC_Admin_Report class to use "week" instead, as it's more consistent with other periods
			// allow "week" for period instead of "7day"
			if ( 'week' === $filter['period'] ) {
				$filter['period'] = '7day';
			}
		}

		$this->report->calculate_current_range( $filter['period'] );
	}

	/**
	 * Verify that the current user has permission to view reports
	 *
	 * @since 2.1
	 * @see WC_API_Resource::validate_request()
	 * @param null $id unused
	 * @param null $type unused
	 * @param null $context unused
	 * @return true|WP_Error
	 */
	protected function validate_request( $id = null, $type = null, $context = null ) {

		if ( ! current_user_can( 'view_woocommerce_reports' ) ) {

			return new WP_Error( 'woocommerce_api_user_cannot_read_report', __( 'You do not have permission to read this report', 'woocommerce' ), array( 'status' => 401 ) );

		} else {

			return true;
		}
	}
}
legacy/api/v1/interface-wc-api-handler.php000064400000001541151336044770014364 0ustar00<?php
/**
 * WooCommerce API
 *
 * Defines an interface that API request/response handlers should implement
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 * @version     2.1
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

interface WC_API_Handler {

	/**
	 * Get the content type for the response
	 *
	 * This should return the proper HTTP content-type for the response
	 *
	 * @since 2.1
	 * @return string
	 */
	public function get_content_type();

	/**
	 * Parse the raw request body entity into an array
	 *
	 * @since 2.1
	 * @param string $data
	 * @return array
	 */
	public function parse_body( $data );

	/**
	 * Generate a response from an array of data
	 *
	 * @since 2.1
	 * @param array $data
	 * @return string
	 */
	public function generate_response( $data );

}
legacy/api/v1/class-wc-api-customers.php000064400000033130151336044770014137 0ustar00<?php
/**
 * WooCommerce API Customers Class
 *
 * Handles requests to the /customers endpoint
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 * @version     2.1
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WC_API_Customers extends WC_API_Resource {

	/** @var string $base the route base */
	protected $base = '/customers';

	/** @var string $created_at_min for date filtering */
	private $created_at_min = null;

	/** @var string $created_at_max for date filtering */
	private $created_at_max = null;

	/**
	 * Setup class, overridden to provide customer data to order response
	 *
	 * @since 2.1
	 * @param WC_API_Server $server
	 */
	public function __construct( WC_API_Server $server ) {

		parent::__construct( $server );

		// add customer data to order responses
		add_filter( 'woocommerce_api_order_response', array( $this, 'add_customer_data' ), 10, 2 );

		// modify WP_User_Query to support created_at date filtering
		add_action( 'pre_user_query', array( $this, 'modify_user_query' ) );
	}

	/**
	 * Register the routes for this class
	 *
	 * GET /customers
	 * GET /customers/count
	 * GET /customers/<id>
	 * GET /customers/<id>/orders
	 *
	 * @since 2.1
	 * @param array $routes
	 * @return array
	 */
	public function register_routes( $routes ) {

		# GET /customers
		$routes[ $this->base ] = array(
			array( array( $this, 'get_customers' ),     WC_API_SERVER::READABLE ),
		);

		# GET /customers/count
		$routes[ $this->base . '/count' ] = array(
			array( array( $this, 'get_customers_count' ), WC_API_SERVER::READABLE ),
		);

		# GET /customers/<id>
		$routes[ $this->base . '/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_customer' ),  WC_API_SERVER::READABLE ),
		);

		# GET /customers/<id>/orders
		$routes[ $this->base . '/(?P<id>\d+)/orders' ] = array(
			array( array( $this, 'get_customer_orders' ), WC_API_SERVER::READABLE ),
		);

		return $routes;
	}

	/**
	 * Get all customers
	 *
	 * @since 2.1
	 * @param array $fields
	 * @param array $filter
	 * @param int $page
	 * @return array
	 */
	public function get_customers( $fields = null, $filter = array(), $page = 1 ) {

		$filter['page'] = $page;

		$query = $this->query_customers( $filter );

		$customers = array();

		foreach ( $query->get_results() as $user_id ) {

			if ( ! $this->is_readable( $user_id ) ) {
				continue;
			}

			$customers[] = current( $this->get_customer( $user_id, $fields ) );
		}

		$this->server->add_pagination_headers( $query );

		return array( 'customers' => $customers );
	}

	/**
	 * Get the customer for the given ID
	 *
	 * @since 2.1
	 * @param int $id the customer ID
	 * @param string $fields
	 * @return array|WP_Error
	 */
	public function get_customer( $id, $fields = null ) {
		global $wpdb;

		$id = $this->validate_request( $id, 'customer', 'read' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$customer      = new WC_Customer( $id );
		$last_order    = $customer->get_last_order();
		$customer_data = array(
			'id'               => $customer->get_id(),
			'created_at'       => $this->server->format_datetime( $customer->get_date_created() ? $customer->get_date_created()->getTimestamp() : 0 ), // API gives UTC times.
			'email'            => $customer->get_email(),
			'first_name'       => $customer->get_first_name(),
			'last_name'        => $customer->get_last_name(),
			'username'         => $customer->get_username(),
			'last_order_id'    => is_object( $last_order ) ? $last_order->get_id() : null,
			'last_order_date'  => is_object( $last_order ) ? $this->server->format_datetime( $last_order->get_date_created() ? $last_order->get_date_created()->getTimestamp() : 0 ) : null, // API gives UTC times.
			'orders_count'     => $customer->get_order_count(),
			'total_spent'      => wc_format_decimal( $customer->get_total_spent(), 2 ),
			'avatar_url'       => $customer->get_avatar_url(),
			'billing_address'  => array(
				'first_name' => $customer->get_billing_first_name(),
				'last_name'  => $customer->get_billing_last_name(),
				'company'    => $customer->get_billing_company(),
				'address_1'  => $customer->get_billing_address_1(),
				'address_2'  => $customer->get_billing_address_2(),
				'city'       => $customer->get_billing_city(),
				'state'      => $customer->get_billing_state(),
				'postcode'   => $customer->get_billing_postcode(),
				'country'    => $customer->get_billing_country(),
				'email'      => $customer->get_billing_email(),
				'phone'      => $customer->get_billing_phone(),
			),
			'shipping_address' => array(
				'first_name' => $customer->get_shipping_first_name(),
				'last_name'  => $customer->get_shipping_last_name(),
				'company'    => $customer->get_shipping_company(),
				'address_1'  => $customer->get_shipping_address_1(),
				'address_2'  => $customer->get_shipping_address_2(),
				'city'       => $customer->get_shipping_city(),
				'state'      => $customer->get_shipping_state(),
				'postcode'   => $customer->get_shipping_postcode(),
				'country'    => $customer->get_shipping_country(),
			),
		);

		return array( 'customer' => apply_filters( 'woocommerce_api_customer_response', $customer_data, $customer, $fields, $this->server ) );
	}

	/**
	 * Get the total number of customers
	 *
	 * @since 2.1
	 * @param array $filter
	 * @return array|WP_Error
	 */
	public function get_customers_count( $filter = array() ) {

		$query = $this->query_customers( $filter );

		if ( ! current_user_can( 'list_users' ) ) {
			return new WP_Error( 'woocommerce_api_user_cannot_read_customers_count', __( 'You do not have permission to read the customers count', 'woocommerce' ), array( 'status' => 401 ) );
		}

		return array( 'count' => count( $query->get_results() ) );
	}


	/**
	 * Create a customer
	 *
	 * @param array $data
	 * @return array|WP_Error
	 */
	public function create_customer( $data ) {

		if ( ! current_user_can( 'create_users' ) ) {
			return new WP_Error( 'woocommerce_api_user_cannot_create_customer', __( 'You do not have permission to create this customer', 'woocommerce' ), array( 'status' => 401 ) );
		}

		return array();
	}

	/**
	 * Edit a customer
	 *
	 * @param int $id the customer ID
	 * @param array $data
	 * @return array|WP_Error
	 */
	public function edit_customer( $id, $data ) {

		$id = $this->validate_request( $id, 'customer', 'edit' );

		if ( ! is_wp_error( $id ) ) {
			return $id;
		}

		return $this->get_customer( $id );
	}

	/**
	 * Delete a customer
	 *
	 * @param int $id the customer ID
	 * @return array|WP_Error
	 */
	public function delete_customer( $id ) {

		$id = $this->validate_request( $id, 'customer', 'delete' );

		if ( ! is_wp_error( $id ) ) {
			return $id;
		}

		return $this->delete( $id, 'customer' );
	}

	/**
	 * Get the orders for a customer
	 *
	 * @since 2.1
	 * @param int $id the customer ID
	 * @param string $fields fields to include in response
	 * @return array|WP_Error
	 */
	public function get_customer_orders( $id, $fields = null ) {
		global $wpdb;

		$id = $this->validate_request( $id, 'customer', 'read' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$order_ids = wc_get_orders( array(
			'customer' => $id,
			'limit'    => -1,
			'orderby'  => 'date',
			'order'    => 'ASC',
			'return'   => 'ids',
		) );

		if ( empty( $order_ids ) ) {
			return array( 'orders' => array() );
		}

		$orders = array();

		foreach ( $order_ids as $order_id ) {
			$orders[] = current( WC()->api->WC_API_Orders->get_order( $order_id, $fields ) );
		}

		return array( 'orders' => apply_filters( 'woocommerce_api_customer_orders_response', $orders, $id, $fields, $order_ids, $this->server ) );
	}

	/**
	 * Helper method to get customer user objects
	 *
	 * Note that WP_User_Query does not have built-in pagination so limit & offset are used to provide limited
	 * pagination support
	 *
	 * @since 2.1
	 * @param array $args request arguments for filtering query
	 * @return WP_User_Query
	 */
	private function query_customers( $args = array() ) {

		// default users per page
		$users_per_page = get_option( 'posts_per_page' );

		// set base query arguments
		$query_args = array(
			'fields'  => 'ID',
			'role'    => 'customer',
			'orderby' => 'registered',
			'number'  => $users_per_page,
		);

		// search
		if ( ! empty( $args['q'] ) ) {
			$query_args['search'] = $args['q'];
		}

		// limit number of users returned
		if ( ! empty( $args['limit'] ) ) {

			$query_args['number'] = absint( $args['limit'] );

			$users_per_page = absint( $args['limit'] );
		}

		// page
		$page = ( isset( $args['page'] ) ) ? absint( $args['page'] ) : 1;

		// offset
		if ( ! empty( $args['offset'] ) ) {
			$query_args['offset'] = absint( $args['offset'] );
		} else {
			$query_args['offset'] = $users_per_page * ( $page - 1 );
		}

		// created date
		if ( ! empty( $args['created_at_min'] ) ) {
			$this->created_at_min = $this->server->parse_datetime( $args['created_at_min'] );
		}

		if ( ! empty( $args['created_at_max'] ) ) {
			$this->created_at_max = $this->server->parse_datetime( $args['created_at_max'] );
		}

		$query = new WP_User_Query( $query_args );

		// helper members for pagination headers
		$query->total_pages = ceil( $query->get_total() / $users_per_page );
		$query->page = $page;

		return $query;
	}

	/**
	 * Add customer data to orders
	 *
	 * @since 2.1
	 * @param $order_data
	 * @param $order
	 * @return array
	 */
	public function add_customer_data( $order_data, $order ) {

		if ( 0 == $order->get_user_id() ) {

			// add customer data from order
			$order_data['customer'] = array(
				'id'               => 0,
				'email'            => $order->get_billing_email(),
				'first_name'       => $order->get_billing_first_name(),
				'last_name'        => $order->get_billing_last_name(),
				'billing_address'  => array(
					'first_name' => $order->get_billing_first_name(),
					'last_name'  => $order->get_billing_last_name(),
					'company'    => $order->get_billing_company(),
					'address_1'  => $order->get_billing_address_1(),
					'address_2'  => $order->get_billing_address_2(),
					'city'       => $order->get_billing_city(),
					'state'      => $order->get_billing_state(),
					'postcode'   => $order->get_billing_postcode(),
					'country'    => $order->get_billing_country(),
					'email'      => $order->get_billing_email(),
					'phone'      => $order->get_billing_phone(),
				),
				'shipping_address' => array(
					'first_name' => $order->get_shipping_first_name(),
					'last_name'  => $order->get_shipping_last_name(),
					'company'    => $order->get_shipping_company(),
					'address_1'  => $order->get_shipping_address_1(),
					'address_2'  => $order->get_shipping_address_2(),
					'city'       => $order->get_shipping_city(),
					'state'      => $order->get_shipping_state(),
					'postcode'   => $order->get_shipping_postcode(),
					'country'    => $order->get_shipping_country(),
				),
			);

		} else {

			$order_data['customer'] = current( $this->get_customer( $order->get_user_id() ) );
		}

		return $order_data;
	}

	/**
	 * Modify the WP_User_Query to support filtering on the date the customer was created
	 *
	 * @since 2.1
	 * @param WP_User_Query $query
	 */
	public function modify_user_query( $query ) {

		if ( $this->created_at_min ) {
			$query->query_where .= sprintf( " AND user_registered >= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%h:%%i:%%s' )", esc_sql( $this->created_at_min ) );
		}

		if ( $this->created_at_max ) {
			$query->query_where .= sprintf( " AND user_registered <= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%h:%%i:%%s' )", esc_sql( $this->created_at_max ) );
		}
	}

	/**
	 * Validate the request by checking:
	 *
	 * 1) the ID is a valid integer
	 * 2) the ID returns a valid WP_User
	 * 3) the current user has the proper permissions
	 *
	 * @since 2.1
	 * @see WC_API_Resource::validate_request()
	 * @param string|int $id the customer ID
	 * @param string $type the request type, unused because this method overrides the parent class
	 * @param string $context the context of the request, either `read`, `edit` or `delete`
	 * @return int|WP_Error valid user ID or WP_Error if any of the checks fails
	 */
	protected function validate_request( $id, $type, $context ) {

		$id = absint( $id );

		// validate ID
		if ( empty( $id ) ) {
			return new WP_Error( 'woocommerce_api_invalid_customer_id', __( 'Invalid customer ID', 'woocommerce' ), array( 'status' => 404 ) );
		}

		// non-existent IDs return a valid WP_User object with the user ID = 0
		$customer = new WP_User( $id );

		if ( 0 === $customer->ID ) {
			return new WP_Error( 'woocommerce_api_invalid_customer', __( 'Invalid customer', 'woocommerce' ), array( 'status' => 404 ) );
		}

		// validate permissions
		switch ( $context ) {

			case 'read':
				if ( ! current_user_can( 'list_users' ) ) {
					return new WP_Error( 'woocommerce_api_user_cannot_read_customer', __( 'You do not have permission to read this customer', 'woocommerce' ), array( 'status' => 401 ) );
				}
				break;

			case 'edit':
				if ( ! current_user_can( 'edit_users' ) ) {
					return new WP_Error( 'woocommerce_api_user_cannot_edit_customer', __( 'You do not have permission to edit this customer', 'woocommerce' ), array( 'status' => 401 ) );
				}
				break;

			case 'delete':
				if ( ! current_user_can( 'delete_users' ) ) {
					return new WP_Error( 'woocommerce_api_user_cannot_delete_customer', __( 'You do not have permission to delete this customer', 'woocommerce' ), array( 'status' => 401 ) );
				}
				break;
		}

		return $id;
	}

	/**
	 * Check if the current user can read users
	 *
	 * @since 2.1
	 * @see WC_API_Resource::is_readable()
	 * @param int|WP_Post $post unused
	 * @return bool true if the current user can read users, false otherwise
	 */
	protected function is_readable( $post ) {

		return current_user_can( 'list_users' );
	}
}
legacy/api/v1/class-wc-api-resource.php000064400000027744151336044770013760 0ustar00<?php
/**
 * WooCommerce API Resource class
 *
 * Provides shared functionality for resource-specific API classes
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 * @version     2.1
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WC_API_Resource {

	/** @var WC_API_Server the API server */
	protected $server;

	/** @var string sub-classes override this to set a resource-specific base route */
	protected $base;

	/**
	 * Setup class
	 *
	 * @since 2.1
	 * @param WC_API_Server $server
	 */
	public function __construct( WC_API_Server $server ) {

		$this->server = $server;

		// automatically register routes for sub-classes
		add_filter( 'woocommerce_api_endpoints', array( $this, 'register_routes' ) );

		// remove fields from responses when requests specify certain fields
		// note these are hooked at a later priority so data added via filters (e.g. customer data to the order response)
		// still has the fields filtered properly
		foreach ( array( 'order', 'coupon', 'customer', 'product', 'report' ) as $resource ) {

			add_filter( "woocommerce_api_{$resource}_response", array( $this, 'maybe_add_meta' ), 15, 2 );
			add_filter( "woocommerce_api_{$resource}_response", array( $this, 'filter_response_fields' ), 20, 3 );
		}
	}

	/**
	 * Validate the request by checking:
	 *
	 * 1) the ID is a valid integer
	 * 2) the ID returns a valid post object and matches the provided post type
	 * 3) the current user has the proper permissions to read/edit/delete the post
	 *
	 * @since 2.1
	 * @param string|int $id the post ID
	 * @param string $type the post type, either `shop_order`, `shop_coupon`, or `product`
	 * @param string $context the context of the request, either `read`, `edit` or `delete`
	 * @return int|WP_Error valid post ID or WP_Error if any of the checks fails
	 */
	protected function validate_request( $id, $type, $context ) {

		if ( 'shop_order' === $type || 'shop_coupon' === $type ) {
			$resource_name = str_replace( 'shop_', '', $type );
		} else {
			$resource_name = $type;
		}

		$id = absint( $id );

		// validate ID
		if ( empty( $id ) ) {
			return new WP_Error( "woocommerce_api_invalid_{$resource_name}_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) );
		}

		// only custom post types have per-post type/permission checks
		if ( 'customer' !== $type ) {

			$post = get_post( $id );

			// for checking permissions, product variations are the same as the product post type
			$post_type = ( 'product_variation' === $post->post_type ) ? 'product' : $post->post_type;

			// validate post type
			if ( $type !== $post_type ) {
				return new WP_Error( "woocommerce_api_invalid_{$resource_name}", sprintf( __( 'Invalid %s', 'woocommerce' ), $resource_name ), array( 'status' => 404 ) );
			}

			// validate permissions
			switch ( $context ) {

				case 'read':
					if ( ! $this->is_readable( $post ) ) {
						return new WP_Error( "woocommerce_api_user_cannot_read_{$resource_name}", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
					}
					break;

				case 'edit':
					if ( ! $this->is_editable( $post ) ) {
						return new WP_Error( "woocommerce_api_user_cannot_edit_{$resource_name}", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
					}
					break;

				case 'delete':
					if ( ! $this->is_deletable( $post ) ) {
						return new WP_Error( "woocommerce_api_user_cannot_delete_{$resource_name}", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
					}
					break;
			}
		}

		return $id;
	}

	/**
	 * Add common request arguments to argument list before WP_Query is run
	 *
	 * @since 2.1
	 * @param array $base_args required arguments for the query (e.g. `post_type`, etc)
	 * @param array $request_args arguments provided in the request
	 * @return array
	 */
	protected function merge_query_args( $base_args, $request_args ) {

		$args = array();

		// date
		if ( ! empty( $request_args['created_at_min'] ) || ! empty( $request_args['created_at_max'] ) || ! empty( $request_args['updated_at_min'] ) || ! empty( $request_args['updated_at_max'] ) ) {

			$args['date_query'] = array();

			// resources created after specified date
			if ( ! empty( $request_args['created_at_min'] ) ) {
				$args['date_query'][] = array( 'column' => 'post_date_gmt', 'after' => $this->server->parse_datetime( $request_args['created_at_min'] ), 'inclusive' => true );
			}

			// resources created before specified date
			if ( ! empty( $request_args['created_at_max'] ) ) {
				$args['date_query'][] = array( 'column' => 'post_date_gmt', 'before' => $this->server->parse_datetime( $request_args['created_at_max'] ), 'inclusive' => true );
			}

			// resources updated after specified date
			if ( ! empty( $request_args['updated_at_min'] ) ) {
				$args['date_query'][] = array( 'column' => 'post_modified_gmt', 'after' => $this->server->parse_datetime( $request_args['updated_at_min'] ), 'inclusive' => true );
			}

			// resources updated before specified date
			if ( ! empty( $request_args['updated_at_max'] ) ) {
				$args['date_query'][] = array( 'column' => 'post_modified_gmt', 'before' => $this->server->parse_datetime( $request_args['updated_at_max'] ), 'inclusive' => true );
			}
		}

		// search
		if ( ! empty( $request_args['q'] ) ) {
			$args['s'] = $request_args['q'];
		}

		// resources per response
		if ( ! empty( $request_args['limit'] ) ) {
			$args['posts_per_page'] = $request_args['limit'];
		}

		// resource offset
		if ( ! empty( $request_args['offset'] ) ) {
			$args['offset'] = $request_args['offset'];
		}

		// resource page
		$args['paged'] = ( isset( $request_args['page'] ) ) ? absint( $request_args['page'] ) : 1;

		return array_merge( $base_args, $args );
	}

	/**
	 * Add meta to resources when requested by the client. Meta is added as a top-level
	 * `<resource_name>_meta` attribute (e.g. `order_meta`) as a list of key/value pairs
	 *
	 * @since 2.1
	 * @param array $data the resource data
	 * @param object $resource the resource object (e.g WC_Order)
	 * @return mixed
	 */
	public function maybe_add_meta( $data, $resource ) {

		if ( isset( $this->server->params['GET']['filter']['meta'] ) && 'true' === $this->server->params['GET']['filter']['meta'] && is_object( $resource ) ) {

			// don't attempt to add meta more than once
			if ( preg_grep( '/[a-z]+_meta/', array_keys( $data ) ) ) {
				return $data;
			}

			// define the top-level property name for the meta
			switch ( get_class( $resource ) ) {

				case 'WC_Order':
					$meta_name = 'order_meta';
					break;

				case 'WC_Coupon':
					$meta_name = 'coupon_meta';
					break;

				case 'WP_User':
					$meta_name = 'customer_meta';
					break;

				default:
					$meta_name = 'product_meta';
					break;
			}

			if ( is_a( $resource, 'WP_User' ) ) {

				// customer meta
				$meta = (array) get_user_meta( $resource->ID );

			} else {

				// coupon/order/product meta
				$meta = (array) get_post_meta( $resource->get_id() );
			}

			foreach ( $meta as $meta_key => $meta_value ) {

				// don't add hidden meta by default
				if ( ! is_protected_meta( $meta_key ) ) {
					$data[ $meta_name ][ $meta_key ] = maybe_unserialize( $meta_value[0] );
				}
			}
		}

		return $data;
	}

	/**
	 * Restrict the fields included in the response if the request specified certain only certain fields should be returned
	 *
	 * @since 2.1
	 * @param array $data the response data
	 * @param object $resource the object that provided the response data, e.g. WC_Coupon or WC_Order
	 * @param array|string the requested list of fields to include in the response
	 * @return array response data
	 */
	public function filter_response_fields( $data, $resource, $fields ) {

		if ( ! is_array( $data ) || empty( $fields ) ) {
			return $data;
		}

		$fields = explode( ',', $fields );
		$sub_fields = array();

		// get sub fields
		foreach ( $fields as $field ) {

			if ( false !== strpos( $field, '.' ) ) {

				list( $name, $value ) = explode( '.', $field );

				$sub_fields[ $name ] = $value;
			}
		}

		// iterate through top-level fields
		foreach ( $data as $data_field => $data_value ) {

			// if a field has sub-fields and the top-level field has sub-fields to filter
			if ( is_array( $data_value ) && in_array( $data_field, array_keys( $sub_fields ) ) ) {

				// iterate through each sub-field
				foreach ( $data_value as $sub_field => $sub_field_value ) {

					// remove non-matching sub-fields
					if ( ! in_array( $sub_field, $sub_fields ) ) {
						unset( $data[ $data_field ][ $sub_field ] );
					}
				}
			} else {

				// remove non-matching top-level fields
				if ( ! in_array( $data_field, $fields ) ) {
					unset( $data[ $data_field ] );
				}
			}
		}

		return $data;
	}

	/**
	 * Delete a given resource
	 *
	 * @since 2.1
	 * @param int $id the resource ID
	 * @param string $type the resource post type, or `customer`
	 * @param bool $force true to permanently delete resource, false to move to trash (not supported for `customer`)
	 * @return array|WP_Error
	 */
	protected function delete( $id, $type, $force = false ) {

		if ( 'shop_order' === $type || 'shop_coupon' === $type ) {
			$resource_name = str_replace( 'shop_', '', $type );
		} else {
			$resource_name = $type;
		}

		if ( 'customer' === $type ) {

			$result = wp_delete_user( $id );

			if ( $result ) {
				return array( 'message' => __( 'Permanently deleted customer', 'woocommerce' ) );
			} else {
				return new WP_Error( 'woocommerce_api_cannot_delete_customer', __( 'The customer cannot be deleted', 'woocommerce' ), array( 'status' => 500 ) );
			}
		} else {

			// delete order/coupon/product
			$result = ( $force ) ? wp_delete_post( $id, true ) : wp_trash_post( $id );

			if ( ! $result ) {
				return new WP_Error( "woocommerce_api_cannot_delete_{$resource_name}", sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), $resource_name ), array( 'status' => 500 ) );
			}

			if ( $force ) {
				return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), $resource_name ) );

			} else {

				$this->server->send_status( '202' );

				return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), $resource_name ) );
			}
		}
	}


	/**
	 * Checks if the given post is readable by the current user
	 *
	 * @since 2.1
	 * @see WC_API_Resource::check_permission()
	 * @param WP_Post|int $post
	 * @return bool
	 */
	protected function is_readable( $post ) {

		return $this->check_permission( $post, 'read' );
	}

	/**
	 * Checks if the given post is editable by the current user
	 *
	 * @since 2.1
	 * @see WC_API_Resource::check_permission()
	 * @param WP_Post|int $post
	 * @return bool
	 */
	protected function is_editable( $post ) {

		return $this->check_permission( $post, 'edit' );

	}

	/**
	 * Checks if the given post is deletable by the current user
	 *
	 * @since 2.1
	 * @see WC_API_Resource::check_permission()
	 * @param WP_Post|int $post
	 * @return bool
	 */
	protected function is_deletable( $post ) {

		return $this->check_permission( $post, 'delete' );
	}

	/**
	 * Checks the permissions for the current user given a post and context
	 *
	 * @since 2.1
	 * @param WP_Post|int $post
	 * @param string $context the type of permission to check, either `read`, `write`, or `delete`
	 * @return bool true if the current user has the permissions to perform the context on the post
	 */
	private function check_permission( $post, $context ) {

		if ( ! is_a( $post, 'WP_Post' ) ) {
			$post = get_post( $post );
		}

		if ( is_null( $post ) ) {
			return false;
		}

		$post_type = get_post_type_object( $post->post_type );

		if ( 'read' === $context ) {
			return current_user_can( $post_type->cap->read_private_posts, $post->ID );
		} elseif ( 'edit' === $context ) {
			return current_user_can( $post_type->cap->edit_post, $post->ID );
		} elseif ( 'delete' === $context ) {
			return current_user_can( $post_type->cap->delete_post, $post->ID );
		} else {
			return false;
		}
	}
}
legacy/api/v1/class-wc-api-json-handler.php000064400000003702151336044770014501 0ustar00<?php
/**
 * WooCommerce API
 *
 * Handles parsing JSON request bodies and generating JSON responses
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 * @version     2.1
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WC_API_JSON_Handler implements WC_API_Handler {

	/**
	 * Get the content type for the response
	 *
	 * @since 2.1
	 * @return string
	 */
	public function get_content_type() {

		return sprintf( '%s; charset=%s', isset( $_GET['_jsonp'] ) ? 'application/javascript' : 'application/json', get_option( 'blog_charset' ) );
	}

	/**
	 * Parse the raw request body entity
	 *
	 * @since 2.1
	 * @param string $body the raw request body
	 * @return array|mixed
	 */
	public function parse_body( $body ) {

		return json_decode( $body, true );
	}

	/**
	 * Generate a JSON response given an array of data
	 *
	 * @since 2.1
	 * @param array $data the response data
	 * @return string
	 */
	public function generate_response( $data ) {
		if ( isset( $_GET['_jsonp'] ) ) {

			if ( ! apply_filters( 'woocommerce_api_jsonp_enabled', true ) ) {
				WC()->api->server->send_status( 400 );
				return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_disabled', 'message' => __( 'JSONP support is disabled on this site', 'woocommerce' ) ) ) );
			}

			$jsonp_callback = $_GET['_jsonp'];

			if ( ! wp_check_jsonp_callback( $jsonp_callback ) ) {
				WC()->api->server->send_status( 400 );
				return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_callback_invalid', __( 'The JSONP callback function is invalid', 'woocommerce' ) ) ) );
			}

			WC()->api->server->header( 'X-Content-Type-Options', 'nosniff' );

			// Prepend '/**/' to mitigate possible JSONP Flash attacks.
			// https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
			return '/**/' . $jsonp_callback . '(' . wp_json_encode( $data ) . ')';
		}

		return wp_json_encode( $data );
	}
}
legacy/api/v1/class-wc-api-authentication.php000064400000027507151336044770015145 0ustar00<?php
/**
 * WooCommerce API Authentication Class
 *
 * @author   WooThemes
 * @category API
 * @package  WooCommerce\RestApi
 * @since    2.1.0
 * @version  2.4.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WC_API_Authentication {

	/**
	 * Setup class
	 *
	 * @since 2.1
	 */
	public function __construct() {

		// To disable authentication, hook into this filter at a later priority and return a valid WP_User
		add_filter( 'woocommerce_api_check_authentication', array( $this, 'authenticate' ), 0 );
	}

	/**
	 * Authenticate the request. The authentication method varies based on whether the request was made over SSL or not.
	 *
	 * @since 2.1
	 * @param WP_User $user
	 * @return null|WP_Error|WP_User
	 */
	public function authenticate( $user ) {

		// Allow access to the index by default
		if ( '/' === WC()->api->server->path ) {
			return new WP_User( 0 );
		}

		try {

			if ( is_ssl() ) {
				$keys = $this->perform_ssl_authentication();
			} else {
				$keys = $this->perform_oauth_authentication();
			}

			// Check API key-specific permission
			$this->check_api_key_permissions( $keys['permissions'] );

			$user = $this->get_user_by_id( $keys['user_id'] );

			$this->update_api_key_last_access( $keys['key_id'] );

		} catch ( Exception $e ) {
			$user = new WP_Error( 'woocommerce_api_authentication_error', $e->getMessage(), array( 'status' => $e->getCode() ) );
		}

		return $user;
	}

	/**
	 * SSL-encrypted requests are not subject to sniffing or man-in-the-middle
	 * attacks, so the request can be authenticated by simply looking up the user
	 * associated with the given consumer key and confirming the consumer secret
	 * provided is valid
	 *
	 * @since 2.1
	 * @return array
	 * @throws Exception
	 */
	private function perform_ssl_authentication() {

		$params = WC()->api->server->params['GET'];

		// Get consumer key
		if ( ! empty( $_SERVER['PHP_AUTH_USER'] ) ) {

			// Should be in HTTP Auth header by default
			$consumer_key = $_SERVER['PHP_AUTH_USER'];

		} elseif ( ! empty( $params['consumer_key'] ) ) {

			// Allow a query string parameter as a fallback
			$consumer_key = $params['consumer_key'];

		} else {

			throw new Exception( __( 'Consumer key is missing.', 'woocommerce' ), 404 );
		}

		// Get consumer secret
		if ( ! empty( $_SERVER['PHP_AUTH_PW'] ) ) {

			// Should be in HTTP Auth header by default
			$consumer_secret = $_SERVER['PHP_AUTH_PW'];

		} elseif ( ! empty( $params['consumer_secret'] ) ) {

			// Allow a query string parameter as a fallback
			$consumer_secret = $params['consumer_secret'];

		} else {

			throw new Exception( __( 'Consumer secret is missing.', 'woocommerce' ), 404 );
		}

		$keys = $this->get_keys_by_consumer_key( $consumer_key );

		if ( ! $this->is_consumer_secret_valid( $keys['consumer_secret'], $consumer_secret ) ) {
			throw new Exception( __( 'Consumer secret is invalid.', 'woocommerce' ), 401 );
		}

		return $keys;
	}

	/**
	 * Perform OAuth 1.0a "one-legged" (http://oauthbible.com/#oauth-10a-one-legged) authentication for non-SSL requests
	 *
	 * This is required so API credentials cannot be sniffed or intercepted when making API requests over plain HTTP
	 *
	 * This follows the spec for simple OAuth 1.0a authentication (RFC 5849) as closely as possible, with two exceptions:
	 *
	 * 1) There is no token associated with request/responses, only consumer keys/secrets are used
	 *
	 * 2) The OAuth parameters are included as part of the request query string instead of part of the Authorization header,
	 *    This is because there is no cross-OS function within PHP to get the raw Authorization header
	 *
	 * @link http://tools.ietf.org/html/rfc5849 for the full spec
	 * @since 2.1
	 * @return array
	 * @throws Exception
	 */
	private function perform_oauth_authentication() {

		$params = WC()->api->server->params['GET'];

		$param_names = array( 'oauth_consumer_key', 'oauth_timestamp', 'oauth_nonce', 'oauth_signature', 'oauth_signature_method' );

		// Check for required OAuth parameters
		foreach ( $param_names as $param_name ) {

			if ( empty( $params[ $param_name ] ) ) {
				/* translators: %s: parameter name */
				throw new Exception( sprintf( __( '%s parameter is missing', 'woocommerce' ), $param_name ), 404 );
			}
		}

		// Fetch WP user by consumer key
		$keys = $this->get_keys_by_consumer_key( $params['oauth_consumer_key'] );

		// Perform OAuth validation
		$this->check_oauth_signature( $keys, $params );
		$this->check_oauth_timestamp_and_nonce( $keys, $params['oauth_timestamp'], $params['oauth_nonce'] );

		// Authentication successful, return user
		return $keys;
	}

	/**
	 * Return the keys for the given consumer key
	 *
	 * @since 2.4.0
	 * @param string $consumer_key
	 * @return array
	 * @throws Exception
	 */
	private function get_keys_by_consumer_key( $consumer_key ) {
		global $wpdb;

		$consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) );

		$keys = $wpdb->get_row( $wpdb->prepare( "
			SELECT key_id, user_id, permissions, consumer_key, consumer_secret, nonces
			FROM {$wpdb->prefix}woocommerce_api_keys
			WHERE consumer_key = '%s'
		", $consumer_key ), ARRAY_A );

		if ( empty( $keys ) ) {
			throw new Exception( __( 'Consumer key is invalid.', 'woocommerce' ), 401 );
		}

		return $keys;
	}

	/**
	 * Get user by ID
	 *
	 * @since  2.4.0
	 * @param  int $user_id
	 * @return WP_User
	 *
	 * @throws Exception
	 */
	private function get_user_by_id( $user_id ) {
		$user = get_user_by( 'id', $user_id );

		if ( ! $user ) {
			throw new Exception( __( 'API user is invalid', 'woocommerce' ), 401 );
		}

		return $user;
	}

	/**
	 * Check if the consumer secret provided for the given user is valid
	 *
	 * @since 2.1
	 * @param string $keys_consumer_secret
	 * @param string $consumer_secret
	 * @return bool
	 */
	private function is_consumer_secret_valid( $keys_consumer_secret, $consumer_secret ) {
		return hash_equals( $keys_consumer_secret, $consumer_secret );
	}

	/**
	 * Verify that the consumer-provided request signature matches our generated signature, this ensures the consumer
	 * has a valid key/secret
	 *
	 * @param array $keys
	 * @param array $params the request parameters
	 * @throws Exception
	 */
	private function check_oauth_signature( $keys, $params ) {

		$http_method = strtoupper( WC()->api->server->method );

		$base_request_uri = rawurlencode( untrailingslashit( get_woocommerce_api_url( '' ) ) . WC()->api->server->path );

		// Get the signature provided by the consumer and remove it from the parameters prior to checking the signature
		$consumer_signature = rawurldecode( str_replace( ' ', '+', $params['oauth_signature'] ) );
		unset( $params['oauth_signature'] );

		// Remove filters and convert them from array to strings to void normalize issues
		if ( isset( $params['filter'] ) ) {
			$filters = $params['filter'];
			unset( $params['filter'] );
			foreach ( $filters as $filter => $filter_value ) {
				$params[ 'filter[' . $filter . ']' ] = $filter_value;
			}
		}

		// Normalize parameter key/values
		$params = $this->normalize_parameters( $params );

		// Sort parameters
		if ( ! uksort( $params, 'strcmp' ) ) {
			throw new Exception( __( 'Invalid signature - failed to sort parameters.', 'woocommerce' ), 401 );
		}

		// Form query string
		$query_params = array();
		foreach ( $params as $param_key => $param_value ) {

			$query_params[] = $param_key . '%3D' . $param_value; // join with equals sign
		}
		$query_string = implode( '%26', $query_params ); // join with ampersand

		$string_to_sign = $http_method . '&' . $base_request_uri . '&' . $query_string;

		if ( 'HMAC-SHA1' !== $params['oauth_signature_method'] && 'HMAC-SHA256' !== $params['oauth_signature_method'] ) {
			throw new Exception( __( 'Invalid signature - signature method is invalid.', 'woocommerce' ), 401 );
		}

		$hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) );

		$signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $keys['consumer_secret'], true ) );

		if ( ! hash_equals( $signature, $consumer_signature ) ) {
			throw new Exception( __( 'Invalid signature - provided signature does not match.', 'woocommerce' ), 401 );
		}
	}

	/**
	 * Normalize each parameter by assuming each parameter may have already been
	 * encoded, so attempt to decode, and then re-encode according to RFC 3986
	 *
	 * Note both the key and value is normalized so a filter param like:
	 *
	 * 'filter[period]' => 'week'
	 *
	 * is encoded to:
	 *
	 * 'filter%5Bperiod%5D' => 'week'
	 *
	 * This conforms to the OAuth 1.0a spec which indicates the entire query string
	 * should be URL encoded
	 *
	 * @since 2.1
	 * @see rawurlencode()
	 * @param array $parameters un-normalized parameters
	 * @return array normalized parameters
	 */
	private function normalize_parameters( $parameters ) {

		$normalized_parameters = array();

		foreach ( $parameters as $key => $value ) {

			// Percent symbols (%) must be double-encoded
			$key   = str_replace( '%', '%25', rawurlencode( rawurldecode( $key ) ) );
			$value = str_replace( '%', '%25', rawurlencode( rawurldecode( $value ) ) );

			$normalized_parameters[ $key ] = $value;
		}

		return $normalized_parameters;
	}

	/**
	 * Verify that the timestamp and nonce provided with the request are valid. This prevents replay attacks where
	 * an attacker could attempt to re-send an intercepted request at a later time.
	 *
	 * - A timestamp is valid if it is within 15 minutes of now
	 * - A nonce is valid if it has not been used within the last 15 minutes
	 *
	 * @param array $keys
	 * @param int $timestamp the unix timestamp for when the request was made
	 * @param string $nonce a unique (for the given user) 32 alphanumeric string, consumer-generated
	 * @throws Exception
	 */
	private function check_oauth_timestamp_and_nonce( $keys, $timestamp, $nonce ) {
		global $wpdb;

		$valid_window = 15 * 60; // 15 minute window

		if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) ) {
			throw new Exception( __( 'Invalid timestamp.', 'woocommerce' ) );
		}

		$used_nonces = maybe_unserialize( $keys['nonces'] );

		if ( empty( $used_nonces ) ) {
			$used_nonces = array();
		}

		if ( in_array( $nonce, $used_nonces ) ) {
			throw new Exception( __( 'Invalid nonce - nonce has already been used.', 'woocommerce' ), 401 );
		}

		$used_nonces[ $timestamp ] = $nonce;

		// Remove expired nonces
		foreach ( $used_nonces as $nonce_timestamp => $nonce ) {
			if ( $nonce_timestamp < ( time() - $valid_window ) ) {
				unset( $used_nonces[ $nonce_timestamp ] );
			}
		}

		$used_nonces = maybe_serialize( $used_nonces );

		$wpdb->update(
			$wpdb->prefix . 'woocommerce_api_keys',
			array( 'nonces' => $used_nonces ),
			array( 'key_id' => $keys['key_id'] ),
			array( '%s' ),
			array( '%d' )
		);
	}

	/**
	 * Check that the API keys provided have the proper key-specific permissions to either read or write API resources
	 *
	 * @param string $key_permissions
	 * @throws Exception if the permission check fails
	 */
	public function check_api_key_permissions( $key_permissions ) {
		switch ( WC()->api->server->method ) {

			case 'HEAD':
			case 'GET':
				if ( 'read' !== $key_permissions && 'read_write' !== $key_permissions ) {
					throw new Exception( __( 'The API key provided does not have read permissions.', 'woocommerce' ), 401 );
				}
				break;

			case 'POST':
			case 'PUT':
			case 'PATCH':
			case 'DELETE':
				if ( 'write' !== $key_permissions && 'read_write' !== $key_permissions ) {
					throw new Exception( __( 'The API key provided does not have write permissions.', 'woocommerce' ), 401 );
				}
				break;
		}
	}

	/**
	 * Updated API Key last access datetime
	 *
	 * @since 2.4.0
	 *
	 * @param int $key_id
	 */
	private function update_api_key_last_access( $key_id ) {
		global $wpdb;

		$wpdb->update(
			$wpdb->prefix . 'woocommerce_api_keys',
			array( 'last_access' => current_time( 'mysql' ) ),
			array( 'key_id' => $key_id ),
			array( '%s' ),
			array( '%d' )
		);
	}
}
legacy/api/v1/class-wc-api-xml-handler.php000064400000016707151336044770014341 0ustar00<?php
/**
 * WooCommerce API
 *
 * Handles parsing XML request bodies and generating XML responses
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 * @version     2.1
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WC_API_XML_Handler implements WC_API_Handler {

	/** @var XMLWriter instance */
	private $xml;

	/**
	 * Add some response filters
	 *
	 * @since 2.1
	 */
	public function __construct() {

		// tweak sales report response data
		add_filter( 'woocommerce_api_report_response', array( $this, 'format_sales_report_data' ), 100 );

		// tweak product response data
		add_filter( 'woocommerce_api_product_response', array( $this, 'format_product_data' ), 100 );
	}

	/**
	 * Get the content type for the response
	 *
	 * @since 2.1
	 * @return string
	 */
	public function get_content_type() {

		return 'application/xml; charset=' . get_option( 'blog_charset' );
	}

	/**
	 * Parse the raw request body entity
	 *
	 * @since 2.1
	 * @param string $data the raw request body
	 * @return array
	 */
	public function parse_body( $data ) {

		// TODO: implement simpleXML parsing
	}

	/**
	 * Generate an XML response given an array of data
	 *
	 * @since 2.1
	 * @param array $data the response data
	 * @return string
	 */
	public function generate_response( $data ) {

		$this->xml = new XMLWriter();

		$this->xml->openMemory();

		$this->xml->setIndent( true );

		$this->xml->startDocument( '1.0', 'UTF-8' );

		$root_element = key( $data );

		$data = $data[ $root_element ];

		switch ( $root_element ) {

			case 'orders':
				$data = array( 'order' => $data );
				break;

			case 'order_notes':
				$data = array( 'order_note' => $data );
				break;

			case 'customers':
				$data = array( 'customer' => $data );
				break;

			case 'coupons':
				$data = array( 'coupon' => $data );
				break;

			case 'products':
				$data = array( 'product' => $data );
				break;

			case 'product_reviews':
				$data = array( 'product_review' => $data );
				break;

			default:
				$data = apply_filters( 'woocommerce_api_xml_data', $data, $root_element );
				break;
		}

		// generate xml starting with the root element and recursively generating child elements
		$this->array_to_xml( $root_element, $data );

		$this->xml->endDocument();

		return $this->xml->outputMemory();
	}

	/**
	 * Convert array into XML by recursively generating child elements
	 *
	 * @since 2.1
	 * @param string|array $element_key - name for element, e.g. <OrderID>
	 * @param string|array $element_value - value for element, e.g. 1234
	 * @return string - generated XML
	 */
	private function array_to_xml( $element_key, $element_value = array() ) {

		if ( is_array( $element_value ) ) {

			// handle attributes
			if ( '@attributes' === $element_key ) {
				foreach ( $element_value as $attribute_key => $attribute_value ) {

					$this->xml->startAttribute( $attribute_key );
					$this->xml->text( $attribute_value );
					$this->xml->endAttribute();
				}
				return;
			}

			// handle multi-elements (e.g. multiple <Order> elements)
			if ( is_numeric( key( $element_value ) ) ) {

				// recursively generate child elements
				foreach ( $element_value as $child_element_key => $child_element_value ) {

					$this->xml->startElement( $element_key );

					foreach ( $child_element_value as $sibling_element_key => $sibling_element_value ) {
						$this->array_to_xml( $sibling_element_key, $sibling_element_value );
					}

					$this->xml->endElement();
				}
			} else {

				// start root element
				$this->xml->startElement( $element_key );

				// recursively generate child elements
				foreach ( $element_value as $child_element_key => $child_element_value ) {
					$this->array_to_xml( $child_element_key, $child_element_value );
				}

				// end root element
				$this->xml->endElement();
			}
		} else {

			// handle single elements
			if ( '@value' == $element_key ) {

				$this->xml->text( $element_value );

			} else {

				// wrap element in CDATA tags if it contains illegal characters
				if ( false !== strpos( $element_value, '<' ) || false !== strpos( $element_value, '>' ) ) {

					$this->xml->startElement( $element_key );
					$this->xml->writeCdata( $element_value );
					$this->xml->endElement();

				} else {

					$this->xml->writeElement( $element_key, $element_value );
				}
			}

			return;
		}
	}

	/**
	 * Adjust the sales report array format to change totals keyed with the sales date to become an
	 * attribute for the totals element instead
	 *
	 * @since 2.1
	 * @param array $data
	 * @return array
	 */
	public function format_sales_report_data( $data ) {

		if ( ! empty( $data['totals'] ) ) {

			foreach ( $data['totals'] as $date => $totals ) {

				unset( $data['totals'][ $date ] );

				$data['totals'][] = array_merge( array( '@attributes' => array( 'date' => $date ) ), $totals );
			}
		}

		return $data;
	}

	/**
	 * Adjust the product data to handle options for attributes without a named child element and other
	 * fields that have no named child elements (e.g. categories = array( 'cat1', 'cat2' ) )
	 *
	 * Note that the parent product data for variations is also adjusted in the same manner as needed
	 *
	 * @since 2.1
	 * @param array $data
	 * @return array
	 */
	public function format_product_data( $data ) {

		// handle attribute values
		if ( ! empty( $data['attributes'] ) ) {

			foreach ( $data['attributes'] as $attribute_key => $attribute ) {

				if ( ! empty( $attribute['options'] ) && is_array( $attribute['options'] ) ) {

					foreach ( $attribute['options'] as $option_key => $option ) {

						unset( $data['attributes'][ $attribute_key ]['options'][ $option_key ] );

						$data['attributes'][ $attribute_key ]['options']['option'][] = array( $option );
					}
				}
			}
		}

		// simple arrays are fine for JSON, but XML requires a child element name, so this adjusts the data
		// array to define a child element name for each field
		$fields_to_fix = array(
			'related_ids'    => 'related_id',
			'upsell_ids'     => 'upsell_id',
			'cross_sell_ids' => 'cross_sell_id',
			'categories'     => 'category',
			'tags'           => 'tag',
		);

		foreach ( $fields_to_fix as $parent_field_name => $child_field_name ) {

			if ( ! empty( $data[ $parent_field_name ] ) ) {

				foreach ( $data[ $parent_field_name ] as $field_key => $field ) {

					unset( $data[ $parent_field_name ][ $field_key ] );

					$data[ $parent_field_name ][ $child_field_name ][] = array( $field );
				}
			}
		}

		// handle adjusting the parent product for variations
		if ( ! empty( $data['parent'] ) ) {

			// attributes
			if ( ! empty( $data['parent']['attributes'] ) ) {

				foreach ( $data['parent']['attributes'] as $attribute_key => $attribute ) {

					if ( ! empty( $attribute['options'] ) && is_array( $attribute['options'] ) ) {

						foreach ( $attribute['options'] as $option_key => $option ) {

							unset( $data['parent']['attributes'][ $attribute_key ]['options'][ $option_key ] );

							$data['parent']['attributes'][ $attribute_key ]['options']['option'][] = array( $option );
						}
					}
				}
			}

			// fields
			foreach ( $fields_to_fix as $parent_field_name => $child_field_name ) {

				if ( ! empty( $data['parent'][ $parent_field_name ] ) ) {

					foreach ( $data['parent'][ $parent_field_name ] as $field_key => $field ) {

						unset( $data['parent'][ $parent_field_name ][ $field_key ] );

						$data['parent'][ $parent_field_name ][ $child_field_name ][] = array( $field );
					}
				}
			}
		}

		return $data;
	}
}
legacy/api/v1/class-wc-api-products.php000064400000041105151336044770013757 0ustar00<?php
/**
 * WooCommerce API Products Class
 *
 * Handles requests to the /products endpoint
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 * @version     3.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WC_API_Products extends WC_API_Resource {

	/** @var string $base the route base */
	protected $base = '/products';

	/**
	 * Register the routes for this class
	 *
	 * GET /products
	 * GET /products/count
	 * GET /products/<id>
	 * GET /products/<id>/reviews
	 *
	 * @since 2.1
	 * @param array $routes
	 * @return array
	 */
	public function register_routes( $routes ) {

		# GET /products
		$routes[ $this->base ] = array(
			array( array( $this, 'get_products' ),     WC_API_Server::READABLE ),
		);

		# GET /products/count
		$routes[ $this->base . '/count' ] = array(
			array( array( $this, 'get_products_count' ), WC_API_Server::READABLE ),
		);

		# GET /products/<id>
		$routes[ $this->base . '/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_product' ),  WC_API_Server::READABLE ),
		);

		# GET /products/<id>/reviews
		$routes[ $this->base . '/(?P<id>\d+)/reviews' ] = array(
			array( array( $this, 'get_product_reviews' ), WC_API_Server::READABLE ),
		);

		return $routes;
	}

	/**
	 * Get all products
	 *
	 * @since 2.1
	 * @param string $fields
	 * @param string $type
	 * @param array $filter
	 * @param int $page
	 * @return array
	 */
	public function get_products( $fields = null, $type = null, $filter = array(), $page = 1 ) {

		if ( ! empty( $type ) ) {
			$filter['type'] = $type;
		}

		$filter['page'] = $page;

		$query = $this->query_products( $filter );

		$products = array();

		foreach ( $query->posts as $product_id ) {

			if ( ! $this->is_readable( $product_id ) ) {
				continue;
			}

			$products[] = current( $this->get_product( $product_id, $fields ) );
		}

		$this->server->add_pagination_headers( $query );

		return array( 'products' => $products );
	}

	/**
	 * Get the product for the given ID
	 *
	 * @since 2.1
	 * @param int $id the product ID
	 * @param string $fields
	 * @return array|WP_Error
	 */
	public function get_product( $id, $fields = null ) {

		$id = $this->validate_request( $id, 'product', 'read' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$product = wc_get_product( $id );

		// add data that applies to every product type
		$product_data = $this->get_product_data( $product );

		// add variations to variable products
		if ( $product->is_type( 'variable' ) && $product->has_child() ) {
			$product_data['variations'] = $this->get_variation_data( $product );
		}

		// add the parent product data to an individual variation
		if ( $product->is_type( 'variation' ) ) {
			$product_data['parent'] = $this->get_product_data( $product->get_parent_id() );
		}

		return array( 'product' => apply_filters( 'woocommerce_api_product_response', $product_data, $product, $fields, $this->server ) );
	}

	/**
	 * Get the total number of orders
	 *
	 * @since 2.1
	 *
	 * @param string $type
	 * @param array $filter
	 *
	 * @return array|WP_Error
	 */
	public function get_products_count( $type = null, $filter = array() ) {

		if ( ! empty( $type ) ) {
			$filter['type'] = $type;
		}

		if ( ! current_user_can( 'read_private_products' ) ) {
			return new WP_Error( 'woocommerce_api_user_cannot_read_products_count', __( 'You do not have permission to read the products count', 'woocommerce' ), array( 'status' => 401 ) );
		}

		$query = $this->query_products( $filter );

		return array( 'count' => (int) $query->found_posts );
	}

	/**
	 * Edit a product
	 *
	 * @param int $id the product ID
	 * @param array $data
	 * @return array|WP_Error
	 */
	public function edit_product( $id, $data ) {

		$id = $this->validate_request( $id, 'product', 'edit' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		return $this->get_product( $id );
	}

	/**
	 * Delete a product
	 *
	 * @param int $id the product ID
	 * @param bool $force true to permanently delete order, false to move to trash
	 * @return array|WP_Error
	 */
	public function delete_product( $id, $force = false ) {

		$id = $this->validate_request( $id, 'product', 'delete' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		return $this->delete( $id, 'product', ( 'true' === $force ) );
	}

	/**
	 * Get the reviews for a product
	 *
	 * @since 2.1
	 * @param int $id the product ID to get reviews for
	 * @param string $fields fields to include in response
	 * @return array|WP_Error
	 */
	public function get_product_reviews( $id, $fields = null ) {

		$id = $this->validate_request( $id, 'product', 'read' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$args = array(
			'post_id' => $id,
			'approve' => 'approve',
		);

		$comments = get_comments( $args );

		$reviews = array();

		foreach ( $comments as $comment ) {

			$reviews[] = array(
				'id'             => $comment->comment_ID,
				'created_at'     => $this->server->format_datetime( $comment->comment_date_gmt ),
				'review'         => $comment->comment_content,
				'rating'         => get_comment_meta( $comment->comment_ID, 'rating', true ),
				'reviewer_name'  => $comment->comment_author,
				'reviewer_email' => $comment->comment_author_email,
				'verified'       => wc_review_is_from_verified_owner( $comment->comment_ID ),
			);
		}

		return array( 'product_reviews' => apply_filters( 'woocommerce_api_product_reviews_response', $reviews, $id, $fields, $comments, $this->server ) );
	}

	/**
	 * Helper method to get product post objects
	 *
	 * @since 2.1
	 * @param array $args request arguments for filtering query
	 * @return WP_Query
	 */
	private function query_products( $args ) {

		// set base query arguments
		$query_args = array(
			'fields'      => 'ids',
			'post_type'   => 'product',
			'post_status' => 'publish',
			'meta_query'  => array(),
		);

		if ( ! empty( $args['type'] ) ) {

			$types = explode( ',', $args['type'] );

			$query_args['tax_query'] = array(
				array(
					'taxonomy' => 'product_type',
					'field'    => 'slug',
					'terms'    => $types,
				),
			);

			unset( $args['type'] );
		}

		$query_args = $this->merge_query_args( $query_args, $args );

		return new WP_Query( $query_args );
	}

	/**
	 * Get standard product data that applies to every product type
	 *
	 * @since 2.1
	 * @param WC_Product|int $product
	 * @return array
	 */
	private function get_product_data( $product ) {
		if ( is_numeric( $product ) ) {
			$product = wc_get_product( $product );
		}

		if ( ! is_a( $product, 'WC_Product' ) ) {
			return array();
		}

		return array(
			'title'              => $product->get_name(),
			'id'                 => $product->get_id(),
			'created_at'         => $this->server->format_datetime( $product->get_date_created(), false, true ),
			'updated_at'         => $this->server->format_datetime( $product->get_date_modified(), false, true ),
			'type'               => $product->get_type(),
			'status'             => $product->get_status(),
			'downloadable'       => $product->is_downloadable(),
			'virtual'            => $product->is_virtual(),
			'permalink'          => $product->get_permalink(),
			'sku'                => $product->get_sku(),
			'price'              => wc_format_decimal( $product->get_price(), 2 ),
			'regular_price'      => wc_format_decimal( $product->get_regular_price(), 2 ),
			'sale_price'         => $product->get_sale_price() ? wc_format_decimal( $product->get_sale_price(), 2 ) : null,
			'price_html'         => $product->get_price_html(),
			'taxable'            => $product->is_taxable(),
			'tax_status'         => $product->get_tax_status(),
			'tax_class'          => $product->get_tax_class(),
			'managing_stock'     => $product->managing_stock(),
			'stock_quantity'     => $product->get_stock_quantity(),
			'in_stock'           => $product->is_in_stock(),
			'backorders_allowed' => $product->backorders_allowed(),
			'backordered'        => $product->is_on_backorder(),
			'sold_individually'  => $product->is_sold_individually(),
			'purchaseable'       => $product->is_purchasable(),
			'featured'           => $product->is_featured(),
			'visible'            => $product->is_visible(),
			'catalog_visibility' => $product->get_catalog_visibility(),
			'on_sale'            => $product->is_on_sale(),
			'weight'             => $product->get_weight() ? wc_format_decimal( $product->get_weight(), 2 ) : null,
			'dimensions'         => array(
				'length' => $product->get_length(),
				'width'  => $product->get_width(),
				'height' => $product->get_height(),
				'unit'   => get_option( 'woocommerce_dimension_unit' ),
			),
			'shipping_required'  => $product->needs_shipping(),
			'shipping_taxable'   => $product->is_shipping_taxable(),
			'shipping_class'     => $product->get_shipping_class(),
			'shipping_class_id'  => ( 0 !== $product->get_shipping_class_id() ) ? $product->get_shipping_class_id() : null,
			'description'        => apply_filters( 'the_content', $product->get_description() ),
			'short_description'  => apply_filters( 'woocommerce_short_description', $product->get_short_description() ),
			'reviews_allowed'    => $product->get_reviews_allowed(),
			'average_rating'     => wc_format_decimal( $product->get_average_rating(), 2 ),
			'rating_count'       => $product->get_rating_count(),
			'related_ids'        => array_map( 'absint', array_values( wc_get_related_products( $product->get_id() ) ) ),
			'upsell_ids'         => array_map( 'absint', $product->get_upsell_ids() ),
			'cross_sell_ids'     => array_map( 'absint', $product->get_cross_sell_ids() ),
			'categories'         => wc_get_object_terms( $product->get_id(), 'product_cat', 'name' ),
			'tags'               => wc_get_object_terms( $product->get_id(), 'product_tag', 'name' ),
			'images'             => $this->get_images( $product ),
			'featured_src'       => wp_get_attachment_url( get_post_thumbnail_id( $product->get_id() ) ),
			'attributes'         => $this->get_attributes( $product ),
			'downloads'          => $this->get_downloads( $product ),
			'download_limit'     => $product->get_download_limit(),
			'download_expiry'    => $product->get_download_expiry(),
			'download_type'      => 'standard',
			'purchase_note'      => apply_filters( 'the_content', $product->get_purchase_note() ),
			'total_sales'        => $product->get_total_sales(),
			'variations'         => array(),
			'parent'             => array(),
		);
	}

	/**
	 * Get an individual variation's data
	 *
	 * @since 2.1
	 * @param WC_Product $product
	 * @return array
	 */
	private function get_variation_data( $product ) {
		$variations = array();

		foreach ( $product->get_children() as $child_id ) {
			$variation = wc_get_product( $child_id );

			if ( ! $variation || ! $variation->exists() ) {
				continue;
			}

			$variations[] = array(
				'id'                => $variation->get_id(),
				'created_at'        => $this->server->format_datetime( $variation->get_date_created(), false, true ),
				'updated_at'        => $this->server->format_datetime( $variation->get_date_modified(), false, true ),
				'downloadable'      => $variation->is_downloadable(),
				'virtual'           => $variation->is_virtual(),
				'permalink'         => $variation->get_permalink(),
				'sku'               => $variation->get_sku(),
				'price'             => wc_format_decimal( $variation->get_price(), 2 ),
				'regular_price'     => wc_format_decimal( $variation->get_regular_price(), 2 ),
				'sale_price'        => $variation->get_sale_price() ? wc_format_decimal( $variation->get_sale_price(), 2 ) : null,
				'taxable'           => $variation->is_taxable(),
				'tax_status'        => $variation->get_tax_status(),
				'tax_class'         => $variation->get_tax_class(),
				'stock_quantity'    => (int) $variation->get_stock_quantity(),
				'in_stock'          => $variation->is_in_stock(),
				'backordered'       => $variation->is_on_backorder(),
				'purchaseable'      => $variation->is_purchasable(),
				'visible'           => $variation->variation_is_visible(),
				'on_sale'           => $variation->is_on_sale(),
				'weight'            => $variation->get_weight() ? wc_format_decimal( $variation->get_weight(), 2 ) : null,
				'dimensions'        => array(
					'length' => $variation->get_length(),
					'width'  => $variation->get_width(),
					'height' => $variation->get_height(),
					'unit'   => get_option( 'woocommerce_dimension_unit' ),
				),
				'shipping_class'    => $variation->get_shipping_class(),
				'shipping_class_id' => ( 0 !== $variation->get_shipping_class_id() ) ? $variation->get_shipping_class_id() : null,
				'image'             => $this->get_images( $variation ),
				'attributes'        => $this->get_attributes( $variation ),
				'downloads'         => $this->get_downloads( $variation ),
				'download_limit'    => (int) $product->get_download_limit(),
				'download_expiry'   => (int) $product->get_download_expiry(),
			);
		}

		return $variations;
	}

	/**
	 * Get the images for a product or product variation
	 *
	 * @since 2.1
	 * @param WC_Product|WC_Product_Variation $product
	 * @return array
	 */
	private function get_images( $product ) {
		$images        = $attachment_ids = array();
		$product_image = $product->get_image_id();

		// Add featured image.
		if ( ! empty( $product_image ) ) {
			$attachment_ids[] = $product_image;
		}

		// add gallery images.
		$attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() );

		// Build image data.
		foreach ( $attachment_ids as $position => $attachment_id ) {

			$attachment_post = get_post( $attachment_id );

			if ( is_null( $attachment_post ) ) {
				continue;
			}

			$attachment = wp_get_attachment_image_src( $attachment_id, 'full' );

			if ( ! is_array( $attachment ) ) {
				continue;
			}

			$images[] = array(
				'id'         => (int) $attachment_id,
				'created_at' => $this->server->format_datetime( $attachment_post->post_date_gmt ),
				'updated_at' => $this->server->format_datetime( $attachment_post->post_modified_gmt ),
				'src'        => current( $attachment ),
				'title'      => get_the_title( $attachment_id ),
				'alt'        => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ),
				'position'   => $position,
			);
		}

		// Set a placeholder image if the product has no images set.
		if ( empty( $images ) ) {

			$images[] = array(
				'id'         => 0,
				'created_at' => $this->server->format_datetime( time() ), // default to now
				'updated_at' => $this->server->format_datetime( time() ),
				'src'        => wc_placeholder_img_src(),
				'title'      => __( 'Placeholder', 'woocommerce' ),
				'alt'        => __( 'Placeholder', 'woocommerce' ),
				'position'   => 0,
			);
		}

		return $images;
	}

	/**
	 * Get attribute options.
	 *
	 * @param int $product_id
	 * @param array $attribute
	 * @return array
	 */
	protected function get_attribute_options( $product_id, $attribute ) {
		if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) {
			return wc_get_product_terms( $product_id, $attribute['name'], array( 'fields' => 'names' ) );
		} elseif ( isset( $attribute['value'] ) ) {
			return array_map( 'trim', explode( '|', $attribute['value'] ) );
		}

		return array();
	}

	/**
	 * Get the attributes for a product or product variation
	 *
	 * @since 2.1
	 * @param WC_Product|WC_Product_Variation $product
	 * @return array
	 */
	private function get_attributes( $product ) {

		$attributes = array();

		if ( $product->is_type( 'variation' ) ) {

			// variation attributes
			foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) {

				// taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_`
				$attributes[] = array(
					'name'   => ucwords( str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $attribute_name ) ) ),
					'option' => $attribute,
				);
			}
		} else {

			foreach ( $product->get_attributes() as $attribute ) {
				$attributes[] = array(
					'name'      => ucwords( wc_attribute_taxonomy_slug( $attribute['name'] ) ),
					'position'  => $attribute['position'],
					'visible'   => (bool) $attribute['is_visible'],
					'variation' => (bool) $attribute['is_variation'],
					'options'   => $this->get_attribute_options( $product->get_id(), $attribute ),
				);
			}
		}

		return $attributes;
	}

	/**
	 * Get the downloads for a product or product variation
	 *
	 * @since 2.1
	 * @param WC_Product|WC_Product_Variation $product
	 * @return array
	 */
	private function get_downloads( $product ) {

		$downloads = array();

		if ( $product->is_downloadable() ) {

			foreach ( $product->get_downloads() as $file_id => $file ) {

				$downloads[] = array(
					'id'   => $file_id, // do not cast as int as this is a hash
					'name' => $file['name'],
					'file' => $file['file'],
				);
			}
		}

		return $downloads;
	}
}
legacy/api/v1/class-wc-api-server.php000064400000051246151336044770013431 0ustar00<?php
/**
 * WooCommerce API
 *
 * Handles REST API requests
 *
 * This class and related code (JSON response handler, resource classes) are based on WP-API v0.6 (https://github.com/WP-API/WP-API)
 * Many thanks to Ryan McCue and any other contributors!
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 * @version     2.1
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

require_once ABSPATH . 'wp-admin/includes/admin.php';

class WC_API_Server {

	const METHOD_GET    = 1;
	const METHOD_POST   = 2;
	const METHOD_PUT    = 4;
	const METHOD_PATCH  = 8;
	const METHOD_DELETE = 16;

	const READABLE   = 1;  // GET
	const CREATABLE  = 2;  // POST
	const EDITABLE   = 14; // POST | PUT | PATCH
	const DELETABLE  = 16; // DELETE
	const ALLMETHODS = 31; // GET | POST | PUT | PATCH | DELETE

	/**
	 * Does the endpoint accept a raw request body?
	 */
	const ACCEPT_RAW_DATA = 64;

	/** Does the endpoint accept a request body? (either JSON or XML) */
	const ACCEPT_DATA = 128;

	/**
	 * Should we hide this endpoint from the index?
	 */
	const HIDDEN_ENDPOINT = 256;

	/**
	 * Map of HTTP verbs to constants
	 * @var array
	 */
	public static $method_map = array(
		'HEAD'   => self::METHOD_GET,
		'GET'    => self::METHOD_GET,
		'POST'   => self::METHOD_POST,
		'PUT'    => self::METHOD_PUT,
		'PATCH'  => self::METHOD_PATCH,
		'DELETE' => self::METHOD_DELETE,
	);

	/**
	 * Requested path (relative to the API root, wp-json.php)
	 *
	 * @var string
	 */
	public $path = '';

	/**
	 * Requested method (GET/HEAD/POST/PUT/PATCH/DELETE)
	 *
	 * @var string
	 */
	public $method = 'HEAD';

	/**
	 * Request parameters
	 *
	 * This acts as an abstraction of the superglobals
	 * (GET => $_GET, POST => $_POST)
	 *
	 * @var array
	 */
	public $params = array( 'GET' => array(), 'POST' => array() );

	/**
	 * Request headers
	 *
	 * @var array
	 */
	public $headers = array();

	/**
	 * Request files (matches $_FILES)
	 *
	 * @var array
	 */
	public $files = array();

	/**
	 * Request/Response handler, either JSON by default
	 * or XML if requested by client
	 *
	 * @var WC_API_Handler
	 */
	public $handler;


	/**
	 * Setup class and set request/response handler
	 *
	 * @since 2.1
	 * @param $path
	 */
	public function __construct( $path ) {

		if ( empty( $path ) ) {
			if ( isset( $_SERVER['PATH_INFO'] ) ) {
				$path = $_SERVER['PATH_INFO'];
			} else {
				$path = '/';
			}
		}

		$this->path           = $path;
		$this->method         = $_SERVER['REQUEST_METHOD'];
		$this->params['GET']  = $_GET;
		$this->params['POST'] = $_POST;
		$this->headers        = $this->get_headers( $_SERVER );
		$this->files          = $_FILES;

		// Compatibility for clients that can't use PUT/PATCH/DELETE
		if ( isset( $_GET['_method'] ) ) {
			$this->method = strtoupper( $_GET['_method'] );
		} elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) {
			$this->method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'];
		}

		// determine type of request/response and load handler, JSON by default
		if ( $this->is_json_request() ) {
			$handler_class = 'WC_API_JSON_Handler';
		} elseif ( $this->is_xml_request() ) {
			$handler_class = 'WC_API_XML_Handler';
		} else {
			$handler_class = apply_filters( 'woocommerce_api_default_response_handler', 'WC_API_JSON_Handler', $this->path, $this );
		}

		$this->handler = new $handler_class();
	}

	/**
	 * Check authentication for the request
	 *
	 * @since 2.1
	 * @return WP_User|WP_Error WP_User object indicates successful login, WP_Error indicates unsuccessful login
	 */
	public function check_authentication() {

		// allow plugins to remove default authentication or add their own authentication
		$user = apply_filters( 'woocommerce_api_check_authentication', null, $this );

		// API requests run under the context of the authenticated user
		if ( is_a( $user, 'WP_User' ) ) {
			wp_set_current_user( $user->ID );
		} elseif ( ! is_wp_error( $user ) ) {
			// WP_Errors are handled in serve_request()
			$user = new WP_Error( 'woocommerce_api_authentication_error', __( 'Invalid authentication method', 'woocommerce' ), array( 'code' => 500 ) );
		}

		return $user;
	}

	/**
	 * Convert an error to an array
	 *
	 * This iterates over all error codes and messages to change it into a flat
	 * array. This enables simpler client behaviour, as it is represented as a
	 * list in JSON rather than an object/map
	 *
	 * @since 2.1
	 * @param WP_Error $error
	 * @return array List of associative arrays with code and message keys
	 */
	protected function error_to_array( $error ) {
		$errors = array();
		foreach ( (array) $error->errors as $code => $messages ) {
			foreach ( (array) $messages as $message ) {
				$errors[] = array( 'code' => $code, 'message' => $message );
			}
		}
		return array( 'errors' => $errors );
	}

	/**
	 * Handle serving an API request
	 *
	 * Matches the current server URI to a route and runs the first matching
	 * callback then outputs a JSON representation of the returned value.
	 *
	 * @since 2.1
	 * @uses WC_API_Server::dispatch()
	 */
	public function serve_request() {

		do_action( 'woocommerce_api_server_before_serve', $this );

		$this->header( 'Content-Type', $this->handler->get_content_type(), true );

		// the API is enabled by default
		if ( ! apply_filters( 'woocommerce_api_enabled', true, $this ) || ( 'no' === get_option( 'woocommerce_api_enabled' ) ) ) {

			$this->send_status( 404 );

			echo $this->handler->generate_response( array( 'errors' => array( 'code' => 'woocommerce_api_disabled', 'message' => 'The WooCommerce API is disabled on this site' ) ) );

			return;
		}

		$result = $this->check_authentication();

		// if authorization check was successful, dispatch the request
		if ( ! is_wp_error( $result ) ) {
			$result = $this->dispatch();
		}

		// handle any dispatch errors
		if ( is_wp_error( $result ) ) {
			$data = $result->get_error_data();
			if ( is_array( $data ) && isset( $data['status'] ) ) {
				$this->send_status( $data['status'] );
			}

			$result = $this->error_to_array( $result );
		}

		// This is a filter rather than an action, since this is designed to be
		// re-entrant if needed
		$served = apply_filters( 'woocommerce_api_serve_request', false, $result, $this );

		if ( ! $served ) {

			if ( 'HEAD' === $this->method ) {
				return;
			}

			echo $this->handler->generate_response( $result );
		}
	}

	/**
	 * Retrieve the route map
	 *
	 * The route map is an associative array with path regexes as the keys. The
	 * value is an indexed array with the callback function/method as the first
	 * item, and a bitmask of HTTP methods as the second item (see the class
	 * constants).
	 *
	 * Each route can be mapped to more than one callback by using an array of
	 * the indexed arrays. This allows mapping e.g. GET requests to one callback
	 * and POST requests to another.
	 *
	 * Note that the path regexes (array keys) must have @ escaped, as this is
	 * used as the delimiter with preg_match()
	 *
	 * @since 2.1
	 * @return array `'/path/regex' => array( $callback, $bitmask )` or `'/path/regex' => array( array( $callback, $bitmask ), ...)`
	 */
	public function get_routes() {

		// index added by default
		$endpoints = array(

			'/' => array( array( $this, 'get_index' ), self::READABLE ),
		);

		$endpoints = apply_filters( 'woocommerce_api_endpoints', $endpoints );

		// Normalise the endpoints
		foreach ( $endpoints as $route => &$handlers ) {
			if ( count( $handlers ) <= 2 && isset( $handlers[1] ) && ! is_array( $handlers[1] ) ) {
				$handlers = array( $handlers );
			}
		}

		return $endpoints;
	}

	/**
	 * Match the request to a callback and call it
	 *
	 * @since 2.1
	 * @return mixed The value returned by the callback, or a WP_Error instance
	 */
	public function dispatch() {

		switch ( $this->method ) {

			case 'HEAD':
			case 'GET':
				$method = self::METHOD_GET;
				break;

			case 'POST':
				$method = self::METHOD_POST;
				break;

			case 'PUT':
				$method = self::METHOD_PUT;
				break;

			case 'PATCH':
				$method = self::METHOD_PATCH;
				break;

			case 'DELETE':
				$method = self::METHOD_DELETE;
				break;

			default:
				return new WP_Error( 'woocommerce_api_unsupported_method', __( 'Unsupported request method', 'woocommerce' ), array( 'status' => 400 ) );
		}

		foreach ( $this->get_routes() as $route => $handlers ) {
			foreach ( $handlers as $handler ) {
				$callback = $handler[0];
				$supported = isset( $handler[1] ) ? $handler[1] : self::METHOD_GET;

				if ( ! ( $supported & $method ) ) {
					continue;
				}

				$match = preg_match( '@^' . $route . '$@i', urldecode( $this->path ), $args );

				if ( ! $match ) {
					continue;
				}

				if ( ! is_callable( $callback ) ) {
					return new WP_Error( 'woocommerce_api_invalid_handler', __( 'The handler for the route is invalid', 'woocommerce' ), array( 'status' => 500 ) );
				}

				$args = array_merge( $args, $this->params['GET'] );
				if ( $method & self::METHOD_POST ) {
					$args = array_merge( $args, $this->params['POST'] );
				}
				if ( $supported & self::ACCEPT_DATA ) {
					$data = $this->handler->parse_body( $this->get_raw_data() );
					$args = array_merge( $args, array( 'data' => $data ) );
				} elseif ( $supported & self::ACCEPT_RAW_DATA ) {
					$data = $this->get_raw_data();
					$args = array_merge( $args, array( 'data' => $data ) );
				}

				$args['_method']  = $method;
				$args['_route']   = $route;
				$args['_path']    = $this->path;
				$args['_headers'] = $this->headers;
				$args['_files']   = $this->files;

				$args = apply_filters( 'woocommerce_api_dispatch_args', $args, $callback );

				// Allow plugins to halt the request via this filter
				if ( is_wp_error( $args ) ) {
					return $args;
				}

				$params = $this->sort_callback_params( $callback, $args );
				if ( is_wp_error( $params ) ) {
					return $params;
				}

				return call_user_func_array( $callback, $params );
			}
		}

		return new WP_Error( 'woocommerce_api_no_route', __( 'No route was found matching the URL and request method', 'woocommerce' ), array( 'status' => 404 ) );
	}

	/**
	 * Sort parameters by order specified in method declaration
	 *
	 * Takes a callback and a list of available params, then filters and sorts
	 * by the parameters the method actually needs, using the Reflection API
	 *
	 * @since 2.1
	 *
	 * @param callable|array $callback the endpoint callback
	 * @param array $provided the provided request parameters
	 *
	 * @return array|WP_Error
	 */
	protected function sort_callback_params( $callback, $provided ) {
		if ( is_array( $callback ) ) {
			$ref_func = new ReflectionMethod( $callback[0], $callback[1] );
		} else {
			$ref_func = new ReflectionFunction( $callback );
		}

		$wanted = $ref_func->getParameters();
		$ordered_parameters = array();

		foreach ( $wanted as $param ) {
			if ( isset( $provided[ $param->getName() ] ) ) {
				// We have this parameters in the list to choose from
				$ordered_parameters[] = is_array( $provided[ $param->getName() ] ) ? array_map( 'urldecode', $provided[ $param->getName() ] ) : urldecode( $provided[ $param->getName() ] );
			} elseif ( $param->isDefaultValueAvailable() ) {
				// We don't have this parameter, but it's optional
				$ordered_parameters[] = $param->getDefaultValue();
			} else {
				// We don't have this parameter and it wasn't optional, abort!
				return new WP_Error( 'woocommerce_api_missing_callback_param', sprintf( __( 'Missing parameter %s', 'woocommerce' ), $param->getName() ), array( 'status' => 400 ) );
			}
		}
		return $ordered_parameters;
	}

	/**
	 * Get the site index.
	 *
	 * This endpoint describes the capabilities of the site.
	 *
	 * @since 2.1
	 * @return array Index entity
	 */
	public function get_index() {

		// General site data
		$available = array(
			'store' => array(
				'name'        => get_option( 'blogname' ),
				'description' => get_option( 'blogdescription' ),
				'URL'         => get_option( 'siteurl' ),
				'wc_version'  => WC()->version,
				'routes'      => array(),
				'meta'        => array(
					'timezone'			 => wc_timezone_string(),
					'currency'       	 => get_woocommerce_currency(),
					'currency_format'    => get_woocommerce_currency_symbol(),
					'tax_included'   	 => wc_prices_include_tax(),
					'weight_unit'    	 => get_option( 'woocommerce_weight_unit' ),
					'dimension_unit' 	 => get_option( 'woocommerce_dimension_unit' ),
					'ssl_enabled'    	 => ( 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) ),
					'permalinks_enabled' => ( '' !== get_option( 'permalink_structure' ) ),
					'links'          	 => array(
						'help' => 'https://woocommerce.github.io/woocommerce/rest-api/',
					),
				),
			),
		);

		// Find the available routes
		foreach ( $this->get_routes() as $route => $callbacks ) {
			$data = array();

			$route = preg_replace( '#\(\?P(<\w+?>).*?\)#', '$1', $route );
			$methods = array();
			foreach ( self::$method_map as $name => $bitmask ) {
				foreach ( $callbacks as $callback ) {
					// Skip to the next route if any callback is hidden
					if ( $callback[1] & self::HIDDEN_ENDPOINT ) {
						continue 3;
					}

					if ( $callback[1] & $bitmask ) {
						$data['supports'][] = $name;
					}

					if ( $callback[1] & self::ACCEPT_DATA ) {
						$data['accepts_data'] = true;
					}

					// For non-variable routes, generate links
					if ( strpos( $route, '<' ) === false ) {
						$data['meta'] = array(
							'self' => get_woocommerce_api_url( $route ),
						);
					}
				}
			}
			$available['store']['routes'][ $route ] = apply_filters( 'woocommerce_api_endpoints_description', $data );
		}
		return apply_filters( 'woocommerce_api_index', $available );
	}

	/**
	 * Send a HTTP status code
	 *
	 * @since 2.1
	 * @param int $code HTTP status
	 */
	public function send_status( $code ) {
		status_header( $code );
	}

	/**
	 * Send a HTTP header
	 *
	 * @since 2.1
	 * @param string $key Header key
	 * @param string $value Header value
	 * @param boolean $replace Should we replace the existing header?
	 */
	public function header( $key, $value, $replace = true ) {
		header( sprintf( '%s: %s', $key, $value ), $replace );
	}

	/**
	 * Send a Link header
	 *
	 * @internal The $rel parameter is first, as this looks nicer when sending multiple
	 *
	 * @link http://tools.ietf.org/html/rfc5988
	 * @link http://www.iana.org/assignments/link-relations/link-relations.xml
	 *
	 * @since 2.1
	 * @param string $rel Link relation. Either a registered type, or an absolute URL
	 * @param string $link Target IRI for the link
	 * @param array $other Other parameters to send, as an associative array
	 */
	public function link_header( $rel, $link, $other = array() ) {

		$header = sprintf( '<%s>; rel="%s"', $link, esc_attr( $rel ) );

		foreach ( $other as $key => $value ) {

			if ( 'title' == $key ) {

				$value = '"' . $value . '"';
			}

			$header .= '; ' . $key . '=' . $value;
		}

		$this->header( 'Link', $header, false );
	}

	/**
	 * Send pagination headers for resources
	 *
	 * @since 2.1
	 * @param WP_Query|WP_User_Query $query
	 */
	public function add_pagination_headers( $query ) {

		// WP_User_Query
		if ( is_a( $query, 'WP_User_Query' ) ) {

			$page        = $query->page;
			$single      = count( $query->get_results() ) == 1;
			$total       = $query->get_total();
			$total_pages = $query->total_pages;

		// WP_Query
		} else {

			$page        = $query->get( 'paged' );
			$single      = $query->is_single();
			$total       = $query->found_posts;
			$total_pages = $query->max_num_pages;
		}

		if ( ! $page ) {
			$page = 1;
		}

		$next_page = absint( $page ) + 1;

		if ( ! $single ) {

			// first/prev
			if ( $page > 1 ) {
				$this->link_header( 'first', $this->get_paginated_url( 1 ) );
				$this->link_header( 'prev', $this->get_paginated_url( $page -1 ) );
			}

			// next
			if ( $next_page <= $total_pages ) {
				$this->link_header( 'next', $this->get_paginated_url( $next_page ) );
			}

			// last
			if ( $page != $total_pages ) {
				$this->link_header( 'last', $this->get_paginated_url( $total_pages ) );
			}
		}

		$this->header( 'X-WC-Total', $total );
		$this->header( 'X-WC-TotalPages', $total_pages );

		do_action( 'woocommerce_api_pagination_headers', $this, $query );
	}

	/**
	 * Returns the request URL with the page query parameter set to the specified page
	 *
	 * @since 2.1
	 * @param int $page
	 * @return string
	 */
	private function get_paginated_url( $page ) {

		// remove existing page query param
		$request = remove_query_arg( 'page' );

		// add provided page query param
		$request = urldecode( add_query_arg( 'page', $page, $request ) );

		// get the home host
		$host = parse_url( get_home_url(), PHP_URL_HOST );

		return set_url_scheme( "http://{$host}{$request}" );
	}

	/**
	 * Retrieve the raw request entity (body)
	 *
	 * @since 2.1
	 * @return string
	 */
	public function get_raw_data() {
		// @codingStandardsIgnoreStart
		// $HTTP_RAW_POST_DATA is deprecated on PHP 5.6.
		if ( function_exists( 'phpversion' ) && version_compare( phpversion(), '5.6', '>=' ) ) {
			return file_get_contents( 'php://input' );
		}

		global $HTTP_RAW_POST_DATA;

		// A bug in PHP < 5.2.2 makes $HTTP_RAW_POST_DATA not set by default,
		// but we can do it ourself.
		if ( ! isset( $HTTP_RAW_POST_DATA ) ) {
			$HTTP_RAW_POST_DATA = file_get_contents( 'php://input' );
		}

		return $HTTP_RAW_POST_DATA;
		// @codingStandardsIgnoreEnd
	}

	/**
	 * Parse an RFC3339 datetime into a MySQl datetime
	 *
	 * Invalid dates default to unix epoch
	 *
	 * @since 2.1
	 * @param string $datetime RFC3339 datetime
	 * @return string MySQl datetime (YYYY-MM-DD HH:MM:SS)
	 */
	public function parse_datetime( $datetime ) {

		// Strip millisecond precision (a full stop followed by one or more digits)
		if ( strpos( $datetime, '.' ) !== false ) {
			$datetime = preg_replace( '/\.\d+/', '', $datetime );
		}

		// default timezone to UTC
		$datetime = preg_replace( '/[+-]\d+:+\d+$/', '+00:00', $datetime );

		try {

			$datetime = new DateTime( $datetime, new DateTimeZone( 'UTC' ) );

		} catch ( Exception $e ) {

			$datetime = new DateTime( '@0' );

		}

		return $datetime->format( 'Y-m-d H:i:s' );
	}

	/**
	 * Format a unix timestamp or MySQL datetime into an RFC3339 datetime
	 *
	 * @since 2.1
	 * @param int|string $timestamp unix timestamp or MySQL datetime
	 * @param bool $convert_to_utc
	 * @param bool $convert_to_gmt Use GMT timezone.
	 * @return string RFC3339 datetime
	 */
	public function format_datetime( $timestamp, $convert_to_utc = false, $convert_to_gmt = false ) {
		if ( $convert_to_gmt ) {
			if ( is_numeric( $timestamp ) ) {
				$timestamp = date( 'Y-m-d H:i:s', $timestamp );
			}

			$timestamp = get_gmt_from_date( $timestamp );
		}

		if ( $convert_to_utc ) {
			$timezone = new DateTimeZone( wc_timezone_string() );
		} else {
			$timezone = new DateTimeZone( 'UTC' );
		}

		try {

			if ( is_numeric( $timestamp ) ) {
				$date = new DateTime( "@{$timestamp}" );
			} else {
				$date = new DateTime( $timestamp, $timezone );
			}

			// convert to UTC by adjusting the time based on the offset of the site's timezone
			if ( $convert_to_utc ) {
				$date->modify( -1 * $date->getOffset() . ' seconds' );
			}
		} catch ( Exception $e ) {

			$date = new DateTime( '@0' );
		}

		return $date->format( 'Y-m-d\TH:i:s\Z' );
	}

	/**
	 * Extract headers from a PHP-style $_SERVER array
	 *
	 * @since 2.1
	 * @param array $server Associative array similar to $_SERVER
	 * @return array Headers extracted from the input
	 */
	public function get_headers( $server ) {
		$headers = array();
		// CONTENT_* headers are not prefixed with HTTP_
		$additional = array( 'CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true );

		foreach ( $server as $key => $value ) {
			if ( strpos( $key, 'HTTP_' ) === 0 ) {
				$headers[ substr( $key, 5 ) ] = $value;
			} elseif ( isset( $additional[ $key ] ) ) {
				$headers[ $key ] = $value;
			}
		}

		return $headers;
	}

	/**
	 * Check if the current request accepts a JSON response by checking the endpoint suffix (.json) or
	 * the HTTP ACCEPT header
	 *
	 * @since 2.1
	 * @return bool
	 */
	private function is_json_request() {

		// check path
		if ( false !== stripos( $this->path, '.json' ) ) {
			return true;
		}

		// check ACCEPT header, only 'application/json' is acceptable, see RFC 4627
		if ( isset( $this->headers['ACCEPT'] ) && 'application/json' == $this->headers['ACCEPT'] ) {
			return true;
		}

		return false;
	}

	/**
	 * Check if the current request accepts an XML response by checking the endpoint suffix (.xml) or
	 * the HTTP ACCEPT header
	 *
	 * @since 2.1
	 * @return bool
	 */
	private function is_xml_request() {

		// check path
		if ( false !== stripos( $this->path, '.xml' ) ) {
			return true;
		}

		// check headers, 'application/xml' or 'text/xml' are acceptable, see RFC 2376
		if ( isset( $this->headers['ACCEPT'] ) && ( 'application/xml' == $this->headers['ACCEPT'] || 'text/xml' == $this->headers['ACCEPT'] ) ) {
			return true;
		}

		return false;
	}
}
legacy/api/v1/class-wc-api-orders.php000064400000027600151336044770013416 0ustar00<?php
/**
 * WooCommerce API Orders Class
 *
 * Handles requests to the /orders endpoint
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 * @version     2.1
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WC_API_Orders extends WC_API_Resource {

	/** @var string $base the route base */
	protected $base = '/orders';

	/**
	 * Register the routes for this class
	 *
	 * GET /orders
	 * GET /orders/count
	 * GET|PUT /orders/<id>
	 * GET /orders/<id>/notes
	 *
	 * @since 2.1
	 * @param array $routes
	 * @return array
	 */
	public function register_routes( $routes ) {

		# GET /orders
		$routes[ $this->base ] = array(
			array( array( $this, 'get_orders' ),     WC_API_Server::READABLE ),
		);

		# GET /orders/count
		$routes[ $this->base . '/count' ] = array(
			array( array( $this, 'get_orders_count' ), WC_API_Server::READABLE ),
		);

		# GET|PUT /orders/<id>
		$routes[ $this->base . '/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_order' ),  WC_API_Server::READABLE ),
			array( array( $this, 'edit_order' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
		);

		# GET /orders/<id>/notes
		$routes[ $this->base . '/(?P<id>\d+)/notes' ] = array(
			array( array( $this, 'get_order_notes' ), WC_API_Server::READABLE ),
		);

		return $routes;
	}

	/**
	 * Get all orders
	 *
	 * @since 2.1
	 * @param string $fields
	 * @param array $filter
	 * @param string $status
	 * @param int $page
	 * @return array
	 */
	public function get_orders( $fields = null, $filter = array(), $status = null, $page = 1 ) {

		if ( ! empty( $status ) ) {
			$filter['status'] = $status;
		}

		$filter['page'] = $page;

		$query = $this->query_orders( $filter );

		$orders = array();

		foreach ( $query->posts as $order_id ) {

			if ( ! $this->is_readable( $order_id ) ) {
				continue;
			}

			$orders[] = current( $this->get_order( $order_id, $fields ) );
		}

		$this->server->add_pagination_headers( $query );

		return array( 'orders' => $orders );
	}


	/**
	 * Get the order for the given ID
	 *
	 * @since 2.1
	 * @param int $id the order ID
	 * @param array $fields
	 * @return array|WP_Error
	 */
	public function get_order( $id, $fields = null ) {

		// ensure order ID is valid & user has permission to read
		$id = $this->validate_request( $id, 'shop_order', 'read' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$order      = wc_get_order( $id );
		$order_data = array(
			'id'                        => $order->get_id(),
			'order_number'              => $order->get_order_number(),
			'created_at'                => $this->server->format_datetime( $order->get_date_created() ? $order->get_date_created()->getTimestamp() : 0, false, false ), // API gives UTC times.
			'updated_at'                => $this->server->format_datetime( $order->get_date_modified() ? $order->get_date_modified()->getTimestamp() : 0, false, false ), // API gives UTC times.
			'completed_at'              => $this->server->format_datetime( $order->get_date_completed() ? $order->get_date_completed()->getTimestamp() : 0, false, false ), // API gives UTC times.
			'status'                    => $order->get_status(),
			'currency'                  => $order->get_currency(),
			'total'                     => wc_format_decimal( $order->get_total(), 2 ),
			'subtotal'                  => wc_format_decimal( $this->get_order_subtotal( $order ), 2 ),
			'total_line_items_quantity' => $order->get_item_count(),
			'total_tax'                 => wc_format_decimal( $order->get_total_tax(), 2 ),
			'total_shipping'            => wc_format_decimal( $order->get_shipping_total(), 2 ),
			'cart_tax'                  => wc_format_decimal( $order->get_cart_tax(), 2 ),
			'shipping_tax'              => wc_format_decimal( $order->get_shipping_tax(), 2 ),
			'total_discount'            => wc_format_decimal( $order->get_total_discount(), 2 ),
			'cart_discount'             => wc_format_decimal( 0, 2 ),
			'order_discount'            => wc_format_decimal( 0, 2 ),
			'shipping_methods'          => $order->get_shipping_method(),
			'payment_details' => array(
				'method_id'    => $order->get_payment_method(),
				'method_title' => $order->get_payment_method_title(),
				'paid'         => ! is_null( $order->get_date_paid() ),
			),
			'billing_address' => array(
				'first_name' => $order->get_billing_first_name(),
				'last_name'  => $order->get_billing_last_name(),
				'company'    => $order->get_billing_company(),
				'address_1'  => $order->get_billing_address_1(),
				'address_2'  => $order->get_billing_address_2(),
				'city'       => $order->get_billing_city(),
				'state'      => $order->get_billing_state(),
				'postcode'   => $order->get_billing_postcode(),
				'country'    => $order->get_billing_country(),
				'email'      => $order->get_billing_email(),
				'phone'      => $order->get_billing_phone(),
			),
			'shipping_address' => array(
				'first_name' => $order->get_shipping_first_name(),
				'last_name'  => $order->get_shipping_last_name(),
				'company'    => $order->get_shipping_company(),
				'address_1'  => $order->get_shipping_address_1(),
				'address_2'  => $order->get_shipping_address_2(),
				'city'       => $order->get_shipping_city(),
				'state'      => $order->get_shipping_state(),
				'postcode'   => $order->get_shipping_postcode(),
				'country'    => $order->get_shipping_country(),
			),
			'note'                      => $order->get_customer_note(),
			'customer_ip'               => $order->get_customer_ip_address(),
			'customer_user_agent'       => $order->get_customer_user_agent(),
			'customer_id'               => $order->get_user_id(),
			'view_order_url'            => $order->get_view_order_url(),
			'line_items'                => array(),
			'shipping_lines'            => array(),
			'tax_lines'                 => array(),
			'fee_lines'                 => array(),
			'coupon_lines'              => array(),
		);

		// add line items
		foreach ( $order->get_items() as $item_id => $item ) {
			$product                    = $item->get_product();
			$order_data['line_items'][] = array(
				'id'         => $item_id,
				'subtotal'   => wc_format_decimal( $order->get_line_subtotal( $item ), 2 ),
				'total'      => wc_format_decimal( $order->get_line_total( $item ), 2 ),
				'total_tax'  => wc_format_decimal( $order->get_line_tax( $item ), 2 ),
				'price'      => wc_format_decimal( $order->get_item_total( $item ), 2 ),
				'quantity'   => $item->get_quantity(),
				'tax_class'  => $item->get_tax_class(),
				'name'       => $item->get_name(),
				'product_id' => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(),
				'sku'        => is_object( $product ) ? $product->get_sku() : null,
			);
		}

		// add shipping
		foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) {
			$order_data['shipping_lines'][] = array(
				'id'           => $shipping_item_id,
				'method_id'    => $shipping_item->get_method_id(),
				'method_title' => $shipping_item->get_name(),
				'total'        => wc_format_decimal( $shipping_item->get_total(), 2 ),
			);
		}

		// add taxes
		foreach ( $order->get_tax_totals() as $tax_code => $tax ) {
			$order_data['tax_lines'][] = array(
				'code'     => $tax_code,
				'title'    => $tax->label,
				'total'    => wc_format_decimal( $tax->amount, 2 ),
				'compound' => (bool) $tax->is_compound,
			);
		}

		// add fees
		foreach ( $order->get_fees() as $fee_item_id => $fee_item ) {
			$order_data['fee_lines'][] = array(
				'id'        => $fee_item_id,
				'title'     => $fee_item->get_name(),
				'tax_class' => $fee_item->get_tax_class(),
				'total'     => wc_format_decimal( $order->get_line_total( $fee_item ), 2 ),
				'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), 2 ),
			);
		}

		// add coupons
		foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) {
			$order_data['coupon_lines'][] = array(
				'id'     => $coupon_item_id,
				'code'   => $coupon_item->get_code(),
				'amount' => wc_format_decimal( $coupon_item->get_discount(), 2 ),
			);
		}

		return array( 'order' => apply_filters( 'woocommerce_api_order_response', $order_data, $order, $fields, $this->server ) );
	}

	/**
	 * Get the total number of orders
	 *
	 * @since 2.1
	 *
	 * @param string $status
	 * @param array $filter
	 *
	 * @return array|WP_Error
	 */
	public function get_orders_count( $status = null, $filter = array() ) {

		if ( ! empty( $status ) ) {
			$filter['status'] = $status;
		}

		$query = $this->query_orders( $filter );

		if ( ! current_user_can( 'read_private_shop_orders' ) ) {
			return new WP_Error( 'woocommerce_api_user_cannot_read_orders_count', __( 'You do not have permission to read the orders count', 'woocommerce' ), array( 'status' => 401 ) );
		}

		return array( 'count' => (int) $query->found_posts );
	}

	/**
	 * Edit an order
	 *
	 * API v1 only allows updating the status of an order
	 *
	 * @since 2.1
	 * @param int $id the order ID
	 * @param array $data
	 * @return array|WP_Error
	 */
	public function edit_order( $id, $data ) {

		$id = $this->validate_request( $id, 'shop_order', 'edit' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$order = wc_get_order( $id );

		if ( ! empty( $data['status'] ) ) {

			$order->update_status( $data['status'], isset( $data['note'] ) ? $data['note'] : '' );
		}

		return $this->get_order( $id );
	}

	/**
	 * Delete an order
	 *
	 * @param int $id the order ID
	 * @param bool $force true to permanently delete order, false to move to trash
	 * @return array
	 */
	public function delete_order( $id, $force = false ) {

		$id = $this->validate_request( $id, 'shop_order', 'delete' );

		return $this->delete( $id, 'order',  ( 'true' === $force ) );
	}

	/**
	 * Get the admin order notes for an order
	 *
	 * @since 2.1
	 * @param int $id the order ID
	 * @param string $fields fields to include in response
	 * @return array|WP_Error
	 */
	public function get_order_notes( $id, $fields = null ) {

		// ensure ID is valid order ID
		$id = $this->validate_request( $id, 'shop_order', 'read' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$args = array(
			'post_id' => $id,
			'approve' => 'approve',
			'type'    => 'order_note',
		);

		remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );

		$notes = get_comments( $args );

		add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );

		$order_notes = array();

		foreach ( $notes as $note ) {

			$order_notes[] = array(
				'id'            => $note->comment_ID,
				'created_at'    => $this->server->format_datetime( $note->comment_date_gmt ),
				'note'          => $note->comment_content,
				'customer_note' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ),
			);
		}

		return array( 'order_notes' => apply_filters( 'woocommerce_api_order_notes_response', $order_notes, $id, $fields, $notes, $this->server ) );
	}

	/**
	 * Helper method to get order post objects
	 *
	 * @since 2.1
	 * @param array $args request arguments for filtering query
	 * @return WP_Query
	 */
	private function query_orders( $args ) {

		// set base query arguments
		$query_args = array(
			'fields'      => 'ids',
			'post_type'   => 'shop_order',
			'post_status' => array_keys( wc_get_order_statuses() ),
		);

		// add status argument
		if ( ! empty( $args['status'] ) ) {

			$statuses                  = 'wc-' . str_replace( ',', ',wc-', $args['status'] );
			$statuses                  = explode( ',', $statuses );
			$query_args['post_status'] = $statuses;

			unset( $args['status'] );

		}

		$query_args = $this->merge_query_args( $query_args, $args );

		return new WP_Query( $query_args );
	}

	/**
	 * Helper method to get the order subtotal
	 *
	 * @since 2.1
	 * @param WC_Order $order
	 * @return float
	 */
	private function get_order_subtotal( $order ) {
		$subtotal = 0;

		// subtotal
		foreach ( $order->get_items() as $item ) {
			$subtotal += $item->get_subtotal();
		}

		return $subtotal;
	}
}
legacy/api/v1/class-wc-api-coupons.php000064400000015767151336044770013621 0ustar00<?php
/**
 * WooCommerce API Coupons Class
 *
 * Handles requests to the /coupons endpoint
 *
 * @author      WooThemes
 * @category    API
 * @package     WooCommerce\RestApi
 * @since       2.1
 * @version     2.1
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

class WC_API_Coupons extends WC_API_Resource {

	/** @var string $base the route base */
	protected $base = '/coupons';

	/**
	 * Register the routes for this class
	 *
	 * GET /coupons
	 * GET /coupons/count
	 * GET /coupons/<id>
	 *
	 * @since 2.1
	 * @param array $routes
	 * @return array
	 */
	public function register_routes( $routes ) {

		# GET /coupons
		$routes[ $this->base ] = array(
			array( array( $this, 'get_coupons' ),     WC_API_Server::READABLE ),
		);

		# GET /coupons/count
		$routes[ $this->base . '/count' ] = array(
			array( array( $this, 'get_coupons_count' ), WC_API_Server::READABLE ),
		);

		# GET /coupons/<id>
		$routes[ $this->base . '/(?P<id>\d+)' ] = array(
			array( array( $this, 'get_coupon' ),  WC_API_Server::READABLE ),
		);

		# GET /coupons/code/<code>, note that coupon codes can contain spaces, dashes and underscores
		$routes[ $this->base . '/code/(?P<code>\w[\w\s\-]*)' ] = array(
			array( array( $this, 'get_coupon_by_code' ), WC_API_Server::READABLE ),
		);

		return $routes;
	}

	/**
	 * Get all coupons
	 *
	 * @since 2.1
	 * @param string $fields
	 * @param array $filter
	 * @param int $page
	 * @return array
	 */
	public function get_coupons( $fields = null, $filter = array(), $page = 1 ) {

		$filter['page'] = $page;

		$query = $this->query_coupons( $filter );

		$coupons = array();

		foreach ( $query->posts as $coupon_id ) {

			if ( ! $this->is_readable( $coupon_id ) ) {
				continue;
			}

			$coupons[] = current( $this->get_coupon( $coupon_id, $fields ) );
		}

		$this->server->add_pagination_headers( $query );

		return array( 'coupons' => $coupons );
	}

	/**
	 * Get the coupon for the given ID
	 *
	 * @since 2.1
	 *
	 * @param int $id the coupon ID
	 * @param string $fields fields to include in response
	 *
	 * @return array|WP_Error
	 * @throws WC_API_Exception
	 */
	public function get_coupon( $id, $fields = null ) {
		$id = $this->validate_request( $id, 'shop_coupon', 'read' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		$coupon = new WC_Coupon( $id );

		if ( 0 === $coupon->get_id() ) {
			throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_id', __( 'Invalid coupon ID', 'woocommerce' ), 404 );
		}

		$coupon_data = array(
			'id'                           => $coupon->get_id(),
			'code'                         => $coupon->get_code(),
			'type'                         => $coupon->get_discount_type(),
			'created_at'                   => $this->server->format_datetime( $coupon->get_date_created() ? $coupon->get_date_created()->getTimestamp() : 0 ), // API gives UTC times.
			'updated_at'                   => $this->server->format_datetime( $coupon->get_date_modified() ? $coupon->get_date_modified()->getTimestamp() : 0 ), // API gives UTC times.
			'amount'                       => wc_format_decimal( $coupon->get_amount(), 2 ),
			'individual_use'               => $coupon->get_individual_use(),
			'product_ids'                  => array_map( 'absint', (array) $coupon->get_product_ids() ),
			'exclude_product_ids'          => array_map( 'absint', (array) $coupon->get_excluded_product_ids() ),
			'usage_limit'                  => $coupon->get_usage_limit() ? $coupon->get_usage_limit() : null,
			'usage_limit_per_user'         => $coupon->get_usage_limit_per_user() ? $coupon->get_usage_limit_per_user() : null,
			'limit_usage_to_x_items'       => (int) $coupon->get_limit_usage_to_x_items(),
			'usage_count'                  => (int) $coupon->get_usage_count(),
			'expiry_date'                  => $this->server->format_datetime( $coupon->get_date_expires() ? $coupon->get_date_expires()->getTimestamp() : 0 ), // API gives UTC times.
			'enable_free_shipping'         => $coupon->get_free_shipping(),
			'product_category_ids'         => array_map( 'absint', (array) $coupon->get_product_categories() ),
			'exclude_product_category_ids' => array_map( 'absint', (array) $coupon->get_excluded_product_categories() ),
			'exclude_sale_items'           => $coupon->get_exclude_sale_items(),
			'minimum_amount'               => wc_format_decimal( $coupon->get_minimum_amount(), 2 ),
			'customer_emails'              => $coupon->get_email_restrictions(),
		);

		return array( 'coupon' => apply_filters( 'woocommerce_api_coupon_response', $coupon_data, $coupon, $fields, $this->server ) );
	}

	/**
	 * Get the total number of coupons
	 *
	 * @since 2.1
	 *
	 * @param array $filter
	 *
	 * @return array|WP_Error
	 */
	public function get_coupons_count( $filter = array() ) {

		$query = $this->query_coupons( $filter );

		if ( ! current_user_can( 'read_private_shop_coupons' ) ) {
			return new WP_Error( 'woocommerce_api_user_cannot_read_coupons_count', __( 'You do not have permission to read the coupons count', 'woocommerce' ), array( 'status' => 401 ) );
		}

		return array( 'count' => (int) $query->found_posts );
	}

	/**
	 * Get the coupon for the given code
	 *
	 * @since 2.1
	 * @param string $code the coupon code
	 * @param string $fields fields to include in response
	 * @return int|WP_Error
	 */
	public function get_coupon_by_code( $code, $fields = null ) {
		global $wpdb;

		$id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish' ORDER BY post_date DESC LIMIT 1;", $code ) );

		if ( is_null( $id ) ) {
			return new WP_Error( 'woocommerce_api_invalid_coupon_code', __( 'Invalid coupon code', 'woocommerce' ), array( 'status' => 404 ) );
		}

		return $this->get_coupon( $id, $fields );
	}

	/**
	 * Create a coupon
	 *
	 * @param array $data
	 * @return array
	 */
	public function create_coupon( $data ) {

		return array();
	}

	/**
	 * Edit a coupon
	 *
	 * @param int $id the coupon ID
	 * @param array $data
	 * @return array|WP_Error
	 */
	public function edit_coupon( $id, $data ) {

		$id = $this->validate_request( $id, 'shop_coupon', 'edit' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		return $this->get_coupon( $id );
	}

	/**
	 * Delete a coupon
	 *
	 * @param int $id the coupon ID
	 * @param bool $force true to permanently delete coupon, false to move to trash
	 * @return array|WP_Error
	 */
	public function delete_coupon( $id, $force = false ) {

		$id = $this->validate_request( $id, 'shop_coupon', 'delete' );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		return $this->delete( $id, 'shop_coupon', ( 'true' === $force ) );
	}

	/**
	 * Helper method to get coupon post objects
	 *
	 * @since 2.1
	 * @param array $args request arguments for filtering query
	 * @return WP_Query
	 */
	private function query_coupons( $args ) {

		// set base query arguments
		$query_args = array(
			'fields'      => 'ids',
			'post_type'   => 'shop_coupon',
			'post_status' => 'publish',
		);

		$query_args = $this->merge_query_args( $query_args, $args );

		return new WP_Query( $query_args );
	}
}
legacy/abstract-wc-legacy-product.php000064400000052544151336044770013677 0ustar00<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Legacy Abstract Product
 *
 * Legacy and deprecated functions are here to keep the WC_Abstract_Product
 * clean.
 * This class will be removed in future versions.
 *
 * @version  3.0.0
 * @package  WooCommerce\Abstracts
 * @category Abstract Class
 * @author   WooThemes
 */
abstract class WC_Abstract_Legacy_Product extends WC_Data {

	/**
	 * Magic __isset method for backwards compatibility. Legacy properties which could be accessed directly in the past.
	 *
	 * @param  string $key Key name.
	 * @return bool
	 */
	public function __isset( $key ) {
		$valid = array(
			'id',
			'product_attributes',
			'visibility',
			'sale_price_dates_from',
			'sale_price_dates_to',
			'post',
			'download_type',
			'product_image_gallery',
			'variation_shipping_class',
			'shipping_class',
			'total_stock',
			'crosssell_ids',
			'parent',
		);
		if ( $this->is_type( 'variation' ) ) {
			$valid = array_merge( $valid, array(
				'variation_id',
				'variation_data',
				'variation_has_stock',
				'variation_shipping_class_id',
				'variation_has_sku',
				'variation_has_length',
				'variation_has_width',
				'variation_has_height',
				'variation_has_weight',
				'variation_has_tax_class',
				'variation_has_downloadable_files',
			) );
		}
		return in_array( $key, array_merge( $valid, array_keys( $this->data ) ) ) || metadata_exists( 'post', $this->get_id(), '_' . $key ) || metadata_exists( 'post', $this->get_parent_id(), '_' . $key );
	}

	/**
	 * Magic __get method for backwards compatibility. Maps legacy vars to new getters.
	 *
	 * @param  string $key Key name.
	 * @return mixed
	 */
	public function __get( $key ) {

		if ( 'post_type' === $key ) {
			return $this->post_type;
		}

		wc_doing_it_wrong( $key, __( 'Product properties should not be accessed directly.', 'woocommerce' ), '3.0' );

		switch ( $key ) {
			case 'id' :
				$value = $this->is_type( 'variation' ) ? $this->get_parent_id() : $this->get_id();
				break;
			case 'product_type' :
				$value = $this->get_type();
				break;
			case 'product_attributes' :
				$value = isset( $this->data['attributes'] ) ? $this->data['attributes'] : '';
				break;
			case 'visibility' :
				$value = $this->get_catalog_visibility();
				break;
			case 'sale_price_dates_from' :
				return $this->get_date_on_sale_from() ? $this->get_date_on_sale_from()->getTimestamp() : '';
				break;
			case 'sale_price_dates_to' :
				return $this->get_date_on_sale_to() ? $this->get_date_on_sale_to()->getTimestamp() : '';
				break;
			case 'post' :
				$value = get_post( $this->get_id() );
				break;
			case 'download_type' :
				return 'standard';
				break;
			case 'product_image_gallery' :
				$value = $this->get_gallery_image_ids();
				break;
			case 'variation_shipping_class' :
			case 'shipping_class' :
				$value = $this->get_shipping_class();
				break;
			case 'total_stock' :
				$value = $this->get_total_stock();
				break;
			case 'downloadable' :
			case 'virtual' :
			case 'manage_stock' :
			case 'featured' :
			case 'sold_individually' :
				$value = $this->{"get_$key"}() ? 'yes' : 'no';
				break;
			case 'crosssell_ids' :
				$value = $this->get_cross_sell_ids();
				break;
			case 'upsell_ids' :
				$value = $this->get_upsell_ids();
				break;
			case 'parent' :
				$value = wc_get_product( $this->get_parent_id() );
				break;
			case 'variation_id' :
				$value = $this->is_type( 'variation' ) ? $this->get_id() : '';
				break;
			case 'variation_data' :
				$value = $this->is_type( 'variation' ) ? wc_get_product_variation_attributes( $this->get_id() ) : '';
				break;
			case 'variation_has_stock' :
				$value = $this->is_type( 'variation' ) ? $this->managing_stock() : '';
				break;
			case 'variation_shipping_class_id' :
				$value = $this->is_type( 'variation' ) ? $this->get_shipping_class_id() : '';
				break;
			case 'variation_has_sku' :
			case 'variation_has_length' :
			case 'variation_has_width' :
			case 'variation_has_height' :
			case 'variation_has_weight' :
			case 'variation_has_tax_class' :
			case 'variation_has_downloadable_files' :
				$value = true; // These were deprecated in 2.2 and simply returned true in 2.6.x.
				break;
			default :
				if ( in_array( $key, array_keys( $this->data ) ) ) {
					$value = $this->{"get_$key"}();
				} else {
					$value = get_post_meta( $this->id, '_' . $key, true );
				}
				break;
		}
		return $value;
	}

	/**
	 * If set, get the default attributes for a variable product.
	 *
	 * @deprecated 3.0.0
	 * @return array
	 */
	public function get_variation_default_attributes() {
		wc_deprecated_function( 'WC_Product_Variable::get_variation_default_attributes', '3.0', 'WC_Product::get_default_attributes' );
		return apply_filters( 'woocommerce_product_default_attributes', $this->get_default_attributes(), $this );
	}

	/**
	 * Returns the gallery attachment ids.
	 *
	 * @deprecated 3.0.0
	 * @return array
	 */
	public function get_gallery_attachment_ids() {
		wc_deprecated_function( 'WC_Product::get_gallery_attachment_ids', '3.0', 'WC_Product::get_gallery_image_ids' );
		return $this->get_gallery_image_ids();
	}

	/**
	 * Set stock level of the product.
	 *
	 * @deprecated 3.0.0
	 *
	 * @param int $amount
	 * @param string $mode
	 *
	 * @return int
	 */
	public function set_stock( $amount = null, $mode = 'set' ) {
		wc_deprecated_function( 'WC_Product::set_stock', '3.0', 'wc_update_product_stock' );
		return wc_update_product_stock( $this, $amount, $mode );
	}

	/**
	 * Reduce stock level of the product.
	 *
	 * @deprecated 3.0.0
	 * @param int $amount Amount to reduce by. Default: 1
	 * @return int new stock level
	 */
	public function reduce_stock( $amount = 1 ) {
		wc_deprecated_function( 'WC_Product::reduce_stock', '3.0', 'wc_update_product_stock' );
		return wc_update_product_stock( $this, $amount, 'decrease' );
	}

	/**
	 * Increase stock level of the product.
	 *
	 * @deprecated 3.0.0
	 * @param int $amount Amount to increase by. Default 1.
	 * @return int new stock level
	 */
	public function increase_stock( $amount = 1 ) {
		wc_deprecated_function( 'WC_Product::increase_stock', '3.0', 'wc_update_product_stock' );
		return wc_update_product_stock( $this, $amount, 'increase' );
	}

	/**
	 * Check if the stock status needs changing.
	 *
	 * @deprecated 3.0.0 Sync is done automatically on read/save, so calling this should not be needed any more.
	 */
	public function check_stock_status() {
		wc_deprecated_function( 'WC_Product::check_stock_status', '3.0' );
	}

	/**
	 * Get and return related products.
	 * @deprecated 3.0.0 Use wc_get_related_products instead.
	 *
	 * @param int $limit
	 *
	 * @return array
	 */
	public function get_related( $limit = 5 ) {
		wc_deprecated_function( 'WC_Product::get_related', '3.0', 'wc_get_related_products' );
		return wc_get_related_products( $this->get_id(), $limit );
	}

	/**
	 * Retrieves related product terms.
	 * @deprecated 3.0.0 Use wc_get_product_term_ids instead.
	 *
	 * @param $term
	 *
	 * @return array
	 */
	protected function get_related_terms( $term ) {
		wc_deprecated_function( 'WC_Product::get_related_terms', '3.0', 'wc_get_product_term_ids' );
		return array_merge( array( 0 ), wc_get_product_term_ids( $this->get_id(), $term ) );
	}

	/**
	 * Builds the related posts query.
	 * @deprecated 3.0.0 Use Product Data Store get_related_products_query instead.
	 *
	 * @param $cats_array
	 * @param $tags_array
	 * @param $exclude_ids
	 * @param $limit
	 */
	protected function build_related_query( $cats_array, $tags_array, $exclude_ids, $limit ) {
		wc_deprecated_function( 'WC_Product::build_related_query', '3.0', 'Product Data Store get_related_products_query' );
		$data_store = WC_Data_Store::load( 'product' );
		return $data_store->get_related_products_query( $cats_array, $tags_array, $exclude_ids, $limit );
	}

	/**
	 * Returns the child product.
	 * @deprecated 3.0.0 Use wc_get_product instead.
	 * @param mixed $child_id
	 * @return WC_Product|WC_Product|WC_Product_variation
	 */
	public function get_child( $child_id ) {
		wc_deprecated_function( 'WC_Product::get_child', '3.0', 'wc_get_product' );
		return wc_get_product( $child_id );
	}

	/**
	 * Functions for getting parts of a price, in html, used by get_price_html.
	 *
	 * @deprecated 3.0.0
	 * @return string
	 */
	public function get_price_html_from_text() {
		wc_deprecated_function( 'WC_Product::get_price_html_from_text', '3.0', 'wc_get_price_html_from_text' );
		return wc_get_price_html_from_text();
	}

	/**
	 * Functions for getting parts of a price, in html, used by get_price_html.
	 *
	 * @deprecated 3.0.0 Use wc_format_sale_price instead.
	 * @param  string $from String or float to wrap with 'from' text
	 * @param  mixed $to String or float to wrap with 'to' text
	 * @return string
	 */
	public function get_price_html_from_to( $from, $to ) {
		wc_deprecated_function( 'WC_Product::get_price_html_from_to', '3.0', 'wc_format_sale_price' );
		return apply_filters( 'woocommerce_get_price_html_from_to', wc_format_sale_price( $from, $to ), $from, $to, $this );
	}

	/**
	 * Lists a table of attributes for the product page.
	 * @deprecated 3.0.0 Use wc_display_product_attributes instead.
	 */
	public function list_attributes() {
		wc_deprecated_function( 'WC_Product::list_attributes', '3.0', 'wc_display_product_attributes' );
		wc_display_product_attributes( $this );
	}

	/**
	 * Returns the price (including tax). Uses customer tax rates. Can work for a specific $qty for more accurate taxes.
	 *
	 * @deprecated 3.0.0 Use wc_get_price_including_tax instead.
	 * @param  int $qty
	 * @param  string $price to calculate, left blank to just use get_price()
	 * @return string
	 */
	public function get_price_including_tax( $qty = 1, $price = '' ) {
		wc_deprecated_function( 'WC_Product::get_price_including_tax', '3.0', 'wc_get_price_including_tax' );
		return wc_get_price_including_tax( $this, array( 'qty' => $qty, 'price' => $price ) );
	}

	/**
	 * Returns the price including or excluding tax, based on the 'woocommerce_tax_display_shop' setting.
	 *
	 * @deprecated 3.0.0 Use wc_get_price_to_display instead.
	 * @param  string  $price to calculate, left blank to just use get_price()
	 * @param  integer $qty   passed on to get_price_including_tax() or get_price_excluding_tax()
	 * @return string
	 */
	public function get_display_price( $price = '', $qty = 1 ) {
		wc_deprecated_function( 'WC_Product::get_display_price', '3.0', 'wc_get_price_to_display' );
		return wc_get_price_to_display( $this, array( 'qty' => $qty, 'price' => $price ) );
	}

	/**
	 * Returns the price (excluding tax) - ignores tax_class filters since the price may *include* tax and thus needs subtracting.
	 * Uses store base tax rates. Can work for a specific $qty for more accurate taxes.
	 *
	 * @deprecated 3.0.0 Use wc_get_price_excluding_tax instead.
	 * @param  int $qty
	 * @param  string $price to calculate, left blank to just use get_price()
	 * @return string
	 */
	public function get_price_excluding_tax( $qty = 1, $price = '' ) {
		wc_deprecated_function( 'WC_Product::get_price_excluding_tax', '3.0', 'wc_get_price_excluding_tax' );
		return wc_get_price_excluding_tax( $this, array( 'qty' => $qty, 'price' => $price ) );
	}

	/**
	 * Adjust a products price dynamically.
	 *
	 * @deprecated 3.0.0
	 * @param mixed $price
	 */
	public function adjust_price( $price ) {
		wc_deprecated_function( 'WC_Product::adjust_price', '3.0', 'WC_Product::set_price / WC_Product::get_price' );
		$this->data['price'] = $this->data['price'] + $price;
	}

	/**
	 * Returns the product categories.
	 *
	 * @deprecated 3.0.0
	 * @param string $sep (default: ', ').
	 * @param string $before (default: '').
	 * @param string $after (default: '').
	 * @return string
	 */
	public function get_categories( $sep = ', ', $before = '', $after = '' ) {
		wc_deprecated_function( 'WC_Product::get_categories', '3.0', 'wc_get_product_category_list' );
		return wc_get_product_category_list( $this->get_id(), $sep, $before, $after );
	}

	/**
	 * Returns the product tags.
	 *
	 * @deprecated 3.0.0
	 * @param string $sep (default: ', ').
	 * @param string $before (default: '').
	 * @param string $after (default: '').
	 * @return array
	 */
	public function get_tags( $sep = ', ', $before = '', $after = '' ) {
		wc_deprecated_function( 'WC_Product::get_tags', '3.0', 'wc_get_product_tag_list' );
		return wc_get_product_tag_list( $this->get_id(), $sep, $before, $after );
	}

	/**
	 * Get the product's post data.
	 *
	 * @deprecated 3.0.0
	 * @return WP_Post
	 */
	public function get_post_data() {
		wc_deprecated_function( 'WC_Product::get_post_data', '3.0', 'get_post' );

		// In order to keep backwards compatibility it's required to use the parent data for variations.
		if ( $this->is_type( 'variation' ) ) {
			$post_data = get_post( $this->get_parent_id() );
		} else {
			$post_data = get_post( $this->get_id() );
		}

		return $post_data;
	}

	/**
	 * Get the parent of the post.
	 *
	 * @deprecated 3.0.0
	 * @return int
	 */
	public function get_parent() {
		wc_deprecated_function( 'WC_Product::get_parent', '3.0', 'WC_Product::get_parent_id' );
		return apply_filters( 'woocommerce_product_parent', absint( $this->get_post_data()->post_parent ), $this );
	}

	/**
	 * Returns the upsell product ids.
	 *
	 * @deprecated 3.0.0
	 * @return array
	 */
	public function get_upsells() {
		wc_deprecated_function( 'WC_Product::get_upsells', '3.0', 'WC_Product::get_upsell_ids' );
		return apply_filters( 'woocommerce_product_upsell_ids', $this->get_upsell_ids(), $this );
	}

	/**
	 * Returns the cross sell product ids.
	 *
	 * @deprecated 3.0.0
	 * @return array
	 */
	public function get_cross_sells() {
		wc_deprecated_function( 'WC_Product::get_cross_sells', '3.0', 'WC_Product::get_cross_sell_ids' );
		return apply_filters( 'woocommerce_product_crosssell_ids', $this->get_cross_sell_ids(), $this );
	}

	/**
	 * Check if variable product has default attributes set.
	 *
	 * @deprecated 3.0.0
	 * @return bool
	 */
	public function has_default_attributes() {
		wc_deprecated_function( 'WC_Product_Variable::has_default_attributes', '3.0', 'a check against WC_Product::get_default_attributes directly' );
		if ( ! $this->get_default_attributes() ) {
			return true;
		}
		return false;
	}

	/**
	 * Get variation ID.
	 *
	 * @deprecated 3.0.0
	 * @return int
	 */
	public function get_variation_id() {
		wc_deprecated_function( 'WC_Product::get_variation_id', '3.0', 'WC_Product::get_id(). It will always be the variation ID if this is a variation.' );
		return $this->get_id();
	}

	/**
	 * Get product variation description.
	 *
	 * @deprecated 3.0.0
	 * @return string
	 */
	public function get_variation_description() {
		wc_deprecated_function( 'WC_Product::get_variation_description', '3.0', 'WC_Product::get_description()' );
		return $this->get_description();
	}

	/**
	 * Check if all variation's attributes are set.
	 *
	 * @deprecated 3.0.0
	 * @return boolean
	 */
	public function has_all_attributes_set() {
		wc_deprecated_function( 'WC_Product::has_all_attributes_set', '3.0', 'an array filter on get_variation_attributes for a quick solution.' );
		$set = true;

		// undefined attributes have null strings as array values
		foreach ( $this->get_variation_attributes() as $att ) {
			if ( ! $att ) {
				$set = false;
				break;
			}
		}
		return $set;
	}

	/**
	 * Returns whether or not the variations parent is visible.
	 *
	 * @deprecated 3.0.0
	 * @return bool
	 */
	public function parent_is_visible() {
		wc_deprecated_function( 'WC_Product::parent_is_visible', '3.0' );
		return $this->is_visible();
	}

	/**
	 * Get total stock - This is the stock of parent and children combined.
	 *
	 * @deprecated 3.0.0
	 * @return int
	 */
	public function get_total_stock() {
		wc_deprecated_function( 'WC_Product::get_total_stock', '3.0', 'get_stock_quantity on each child. Beware of performance issues in doing so.' );
		if ( sizeof( $this->get_children() ) > 0 ) {
			$total_stock = max( 0, $this->get_stock_quantity() );

			foreach ( $this->get_children() as $child_id ) {
				if ( 'yes' === get_post_meta( $child_id, '_manage_stock', true ) ) {
					$stock = get_post_meta( $child_id, '_stock', true );
					$total_stock += max( 0, wc_stock_amount( $stock ) );
				}
			}
		} else {
			$total_stock = $this->get_stock_quantity();
		}
		return wc_stock_amount( $total_stock );
	}

	/**
	 * Get formatted variation data with WC < 2.4 back compat and proper formatting of text-based attribute names.
	 *
	 * @deprecated 3.0.0
	 *
	 * @param bool $flat
	 *
	 * @return string
	 */
	public function get_formatted_variation_attributes( $flat = false ) {
		wc_deprecated_function( 'WC_Product::get_formatted_variation_attributes', '3.0', 'wc_get_formatted_variation' );
		return wc_get_formatted_variation( $this, $flat );
	}

	/**
	 * Sync variable product prices with the children lowest/highest prices.
	 *
	 * @deprecated 3.0.0 not used in core.
	 *
	 * @param int $product_id
	 */
	public function variable_product_sync( $product_id = 0 ) {
		wc_deprecated_function( 'WC_Product::variable_product_sync', '3.0' );
		if ( empty( $product_id ) ) {
			$product_id = $this->get_id();
		}

		// Sync prices with children
		if ( is_callable( array( __CLASS__, 'sync' ) ) ) {
			self::sync( $product_id );
		}
	}

	/**
	 * Sync the variable product's attributes with the variations.
	 *
	 * @param $product
	 * @param bool $children
	 */
	public static function sync_attributes( $product, $children = false ) {
		if ( ! is_a( $product, 'WC_Product' ) ) {
			$product = wc_get_product( $product );
		}

		/**
		 * Pre 2.4 handling where 'slugs' were saved instead of the full text attribute.
		 * Attempt to get full version of the text attribute from the parent and UPDATE meta.
		 */
		if ( version_compare( get_post_meta( $product->get_id(), '_product_version', true ), '2.4.0', '<' ) ) {
			$parent_attributes = array_filter( (array) get_post_meta( $product->get_id(), '_product_attributes', true ) );

			if ( ! $children ) {
				$children = $product->get_children( 'edit' );
			}

			foreach ( $children as $child_id ) {
				$all_meta = get_post_meta( $child_id );

				foreach ( $all_meta as $name => $value ) {
					if ( 0 !== strpos( $name, 'attribute_' ) ) {
						continue;
					}
					if ( sanitize_title( $value[0] ) === $value[0] ) {
						foreach ( $parent_attributes as $attribute ) {
							if ( 'attribute_' . sanitize_title( $attribute['name'] ) !== $name ) {
								continue;
							}
							$text_attributes = wc_get_text_attributes( $attribute['value'] );
							foreach ( $text_attributes as $text_attribute ) {
								if ( sanitize_title( $text_attribute ) === $value[0] ) {
									update_post_meta( $child_id, $name, $text_attribute );
									break;
								}
							}
						}
					}
				}
			}
		}
	}

	/**
	 * Match a variation to a given set of attributes using a WP_Query.
	 * @deprecated 3.0.0 in favour of Product data store's find_matching_product_variation.
	 *
	 * @param array $match_attributes
	 */
	public function get_matching_variation( $match_attributes = array() ) {
		wc_deprecated_function( 'WC_Product::get_matching_variation', '3.0', 'Product data store find_matching_product_variation' );
		$data_store = WC_Data_Store::load( 'product' );
		return $data_store->find_matching_product_variation( $this, $match_attributes );
	}

	/**
	 * Returns whether or not we are showing dimensions on the product page.
	 * @deprecated 3.0.0 Unused.
	 * @return bool
	 */
	public function enable_dimensions_display() {
		wc_deprecated_function( 'WC_Product::enable_dimensions_display', '3.0' );
		return apply_filters( 'wc_product_enable_dimensions_display', true ) && ( $this->has_dimensions() || $this->has_weight() || $this->child_has_weight() || $this->child_has_dimensions() );
	}

	/**
	 * Returns the product rating in html format.
	 *
	 * @deprecated 3.0.0
	 * @param string $rating (default: '')
	 * @return string
	 */
	public function get_rating_html( $rating = null ) {
		wc_deprecated_function( 'WC_Product::get_rating_html', '3.0', 'wc_get_rating_html' );
		return wc_get_rating_html( $rating );
	}

	/**
	 * Sync product rating. Can be called statically.
	 *
	 * @deprecated 3.0.0
	 * @param  int $post_id
	 */
	public static function sync_average_rating( $post_id ) {
		wc_deprecated_function( 'WC_Product::sync_average_rating', '3.0', 'WC_Comments::get_average_rating_for_product or leave to CRUD.' );
		// See notes in https://github.com/woocommerce/woocommerce/pull/22909#discussion_r262393401.
		// Sync count first like in the original method https://github.com/woocommerce/woocommerce/blob/2.6.0/includes/abstracts/abstract-wc-product.php#L1101-L1128.
		self::sync_rating_count( $post_id );
		$average = WC_Comments::get_average_rating_for_product( wc_get_product( $post_id ) );
		update_post_meta( $post_id, '_wc_average_rating', $average );
	}

	/**
	 * Sync product rating count. Can be called statically.
	 *
	 * @deprecated 3.0.0
	 * @param  int $post_id
	 */
	public static function sync_rating_count( $post_id ) {
		wc_deprecated_function( 'WC_Product::sync_rating_count', '3.0', 'WC_Comments::get_rating_counts_for_product or leave to CRUD.' );
		$counts     = WC_Comments::get_rating_counts_for_product( wc_get_product( $post_id ) );
		update_post_meta( $post_id, '_wc_rating_count', $counts );
	}

	/**
	 * Same as get_downloads in CRUD.
	 *
	 * @deprecated 3.0.0
	 * @return array
	 */
	public function get_files() {
		wc_deprecated_function( 'WC_Product::get_files', '3.0', 'WC_Product::get_downloads' );
		return $this->get_downloads();
	}

	/**
	 * @deprecated 3.0.0 Sync is taken care of during save - no need to call this directly.
	 */
	public function grouped_product_sync() {
		wc_deprecated_function( 'WC_Product::grouped_product_sync', '3.0' );
	}
}
legacy/class-wc-legacy-api.php000064400000020512151336044770012260 0ustar00<?php
/**
 * WooCommerce Legacy API. Was deprecated with 2.6.0.
 *
 * @author   WooThemes
 * @category API
 * @package  WooCommerce\RestApi
 * @since    2.6
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Legacy API.
 */
class WC_Legacy_API {

	/**
	 * This is the major version for the REST API and takes
	 * first-order position in endpoint URLs.
	 *
	 * @deprecated 2.6.0
	 * @var string
	 */
	const VERSION = '3.1.0';

	/**
	 * The REST API server.
	 *
	 * @deprecated 2.6.0
	 * @var WC_API_Server
	 */
	public $server;

	/**
	 * REST API authentication class instance.
	 *
	 * @deprecated 2.6.0
	 * @var WC_API_Authentication
	 */
	public $authentication;

	/**
	 * Init the legacy API.
	 */
	public function init() {
		add_action( 'parse_request', array( $this, 'handle_rest_api_requests' ), 0 );
	}

	/**
	 * Add new query vars.
	 *
	 * @since 2.0
	 * @param array $vars Vars.
	 * @return string[]
	 */
	public function add_query_vars( $vars ) {
		$vars[] = 'wc-api-version'; // Deprecated since 2.6.0.
		$vars[] = 'wc-api-route'; // Deprecated since 2.6.0.
		return $vars;
	}

	/**
	 * Add new endpoints.
	 *
	 * @since 2.0
	 */
	public static function add_endpoint() {
		// REST API, deprecated since 2.6.0.
		add_rewrite_rule( '^wc-api/v([1-3]{1})/?$', 'index.php?wc-api-version=$matches[1]&wc-api-route=/', 'top' );
		add_rewrite_rule( '^wc-api/v([1-3]{1})(.*)?', 'index.php?wc-api-version=$matches[1]&wc-api-route=$matches[2]', 'top' );
	}

	/**
	 * Handle REST API requests.
	 *
	 * @since 2.2
	 * @deprecated 2.6.0
	 */
	public function handle_rest_api_requests() {
		global $wp;

		if ( ! empty( $_GET['wc-api-version'] ) ) {
			$wp->query_vars['wc-api-version'] = $_GET['wc-api-version'];
		}

		if ( ! empty( $_GET['wc-api-route'] ) ) {
			$wp->query_vars['wc-api-route'] = $_GET['wc-api-route'];
		}

		// REST API request.
		if ( ! empty( $wp->query_vars['wc-api-version'] ) && ! empty( $wp->query_vars['wc-api-route'] ) ) {

			wc_maybe_define_constant( 'WC_API_REQUEST', true );
			wc_maybe_define_constant( 'WC_API_REQUEST_VERSION', absint( $wp->query_vars['wc-api-version'] ) );

			// Legacy v1 API request.
			if ( 1 === WC_API_REQUEST_VERSION ) {
				$this->handle_v1_rest_api_request();
			} elseif ( 2 === WC_API_REQUEST_VERSION ) {
				$this->handle_v2_rest_api_request();
			} else {
				$this->includes();

				$this->server = new WC_API_Server( $wp->query_vars['wc-api-route'] );

				// load API resource classes.
				$this->register_resources( $this->server );

				// Fire off the request.
				$this->server->serve_request();
			}

			exit;
		}
	}

	/**
	 * Include required files for REST API request.
	 *
	 * @since 2.1
	 * @deprecated 2.6.0
	 */
	public function includes() {

		// API server / response handlers.
		include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-exception.php' );
		include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-server.php' );
		include_once( dirname( __FILE__ ) . '/api/v3/interface-wc-api-handler.php' );
		include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-json-handler.php' );

		// Authentication.
		include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-authentication.php' );
		$this->authentication = new WC_API_Authentication();

		include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-resource.php' );
		include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-coupons.php' );
		include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-customers.php' );
		include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-orders.php' );
		include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-products.php' );
		include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-reports.php' );
		include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-taxes.php' );
		include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-webhooks.php' );

		// Allow plugins to load other response handlers or resource classes.
		do_action( 'woocommerce_api_loaded' );
	}

	/**
	 * Register available API resources.
	 *
	 * @since 2.1
	 * @deprecated 2.6.0
	 * @param WC_API_Server $server the REST server.
	 */
	public function register_resources( $server ) {

		$api_classes = apply_filters( 'woocommerce_api_classes',
			array(
				'WC_API_Coupons',
				'WC_API_Customers',
				'WC_API_Orders',
				'WC_API_Products',
				'WC_API_Reports',
				'WC_API_Taxes',
				'WC_API_Webhooks',
			)
		);

		foreach ( $api_classes as $api_class ) {
			$this->$api_class = new $api_class( $server );
		}
	}


	/**
	 * Handle legacy v1 REST API requests.
	 *
	 * @since 2.2
	 * @deprecated 2.6.0
	 */
	private function handle_v1_rest_api_request() {

		// Include legacy required files for v1 REST API request.
		include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-server.php' );
		include_once( dirname( __FILE__ ) . '/api/v1/interface-wc-api-handler.php' );
		include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-json-handler.php' );
		include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-xml-handler.php' );

		include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-authentication.php' );
		$this->authentication = new WC_API_Authentication();

		include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-resource.php' );
		include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-coupons.php' );
		include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-customers.php' );
		include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-orders.php' );
		include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-products.php' );
		include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-reports.php' );

		// Allow plugins to load other response handlers or resource classes.
		do_action( 'woocommerce_api_loaded' );

		$this->server = new WC_API_Server( $GLOBALS['wp']->query_vars['wc-api-route'] );

		// Register available resources for legacy v1 REST API request.
		$api_classes = apply_filters( 'woocommerce_api_classes',
			array(
				'WC_API_Customers',
				'WC_API_Orders',
				'WC_API_Products',
				'WC_API_Coupons',
				'WC_API_Reports',
			)
		);

		foreach ( $api_classes as $api_class ) {
			$this->$api_class = new $api_class( $this->server );
		}

		// Fire off the request.
		$this->server->serve_request();
	}

	/**
	 * Handle legacy v2 REST API requests.
	 *
	 * @since 2.4
	 * @deprecated 2.6.0
	 */
	private function handle_v2_rest_api_request() {
		include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-exception.php' );
		include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-server.php' );
		include_once( dirname( __FILE__ ) . '/api/v2/interface-wc-api-handler.php' );
		include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-json-handler.php' );

		include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-authentication.php' );
		$this->authentication = new WC_API_Authentication();

		include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-resource.php' );
		include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-coupons.php' );
		include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-customers.php' );
		include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-orders.php' );
		include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-products.php' );
		include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-reports.php' );
		include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-webhooks.php' );

		// allow plugins to load other response handlers or resource classes.
		do_action( 'woocommerce_api_loaded' );

		$this->server = new WC_API_Server( $GLOBALS['wp']->query_vars['wc-api-route'] );

		// Register available resources for legacy v2 REST API request.
		$api_classes = apply_filters( 'woocommerce_api_classes',
			array(
				'WC_API_Customers',
				'WC_API_Orders',
				'WC_API_Products',
				'WC_API_Coupons',
				'WC_API_Reports',
				'WC_API_Webhooks',
			)
		);

		foreach ( $api_classes as $api_class ) {
			$this->$api_class = new $api_class( $this->server );
		}

		// Fire off the request.
		$this->server->serve_request();
	}

	/**
	 * Rest API Init.
	 *
	 * @deprecated 3.7.0 - REST API clases autoload.
	 */
	public function rest_api_init() {}

	/**
	 * Include REST API classes.
	 *
	 * @deprecated 3.7.0 - REST API clases autoload.
	 */
	public function rest_api_includes() {
		$this->rest_api_init();
	}
	/**
	 * Register REST API routes.
	 *
	 * @deprecated 3.7.0
	 */
	public function register_rest_routes() {
		wc_deprecated_function( 'WC_Legacy_API::register_rest_routes', '3.7.0', '' );
		$this->register_wp_admin_settings();
	}
}
legacy/abstract-wc-legacy-order.php000064400000061220151336044770013321 0ustar00<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Legacy Abstract Order
 *
 * Legacy and deprecated functions are here to keep the WC_Abstract_Order clean.
 * This class will be removed in future versions.
 *
 * @version	 3.0.0
 * @package	 WooCommerce\Abstracts
 * @category	Abstract Class
 * @author	  WooThemes
 */
abstract class WC_Abstract_Legacy_Order extends WC_Data {

	/**
	 * Add coupon code to the order.
	 * @param string|array $code
	 * @param int $discount tax amount.
	 * @param int $discount_tax amount.
	 * @return int order item ID
	 * @throws WC_Data_Exception
	 */
	public function add_coupon( $code = array(), $discount = 0, $discount_tax = 0 ) {
		wc_deprecated_function( 'WC_Order::add_coupon', '3.0', 'a new WC_Order_Item_Coupon object and add to order with WC_Order::add_item()' );

		$item = new WC_Order_Item_Coupon();
		$item->set_props( array(
			'code'         => $code,
			'discount'     => $discount,
			'discount_tax' => $discount_tax,
			'order_id'     => $this->get_id(),
		) );
		$item->save();
		$this->add_item( $item );
		wc_do_deprecated_action( 'woocommerce_order_add_coupon', array( $this->get_id(), $item->get_id(), $code, $discount, $discount_tax ), '3.0', 'woocommerce_new_order_item action instead.' );
		return $item->get_id();
	}

	/**
	 * Add a tax row to the order.
	 * @param int $tax_rate_id
	 * @param int $tax_amount amount of tax.
	 * @param int $shipping_tax_amount shipping amount.
	 * @return int order item ID
	 * @throws WC_Data_Exception
	 */
	public function add_tax( $tax_rate_id, $tax_amount = 0, $shipping_tax_amount = 0 ) {
		wc_deprecated_function( 'WC_Order::add_tax', '3.0', 'a new WC_Order_Item_Tax object and add to order with WC_Order::add_item()' );

		$item = new WC_Order_Item_Tax();
		$item->set_props( array(
			'rate_id'            => $tax_rate_id,
			'tax_total'          => $tax_amount,
			'shipping_tax_total' => $shipping_tax_amount,
		) );
		$item->set_rate( $tax_rate_id );
		$item->set_order_id( $this->get_id() );
		$item->save();
		$this->add_item( $item );
		wc_do_deprecated_action( 'woocommerce_order_add_tax', array( $this->get_id(), $item->get_id(), $tax_rate_id, $tax_amount, $shipping_tax_amount ), '3.0', 'woocommerce_new_order_item action instead.' );
		return $item->get_id();
	}

	/**
	 * Add a shipping row to the order.
	 * @param WC_Shipping_Rate shipping_rate
	 * @return int order item ID
	 * @throws WC_Data_Exception
	 */
	public function add_shipping( $shipping_rate ) {
		wc_deprecated_function( 'WC_Order::add_shipping', '3.0', 'a new WC_Order_Item_Shipping object and add to order with WC_Order::add_item()' );

		$item = new WC_Order_Item_Shipping();
		$item->set_props( array(
			'method_title' => $shipping_rate->label,
			'method_id'    => $shipping_rate->id,
			'total'        => wc_format_decimal( $shipping_rate->cost ),
			'taxes'        => $shipping_rate->taxes,
			'order_id'     => $this->get_id(),
		) );
		foreach ( $shipping_rate->get_meta_data() as $key => $value ) {
			$item->add_meta_data( $key, $value, true );
		}
		$item->save();
		$this->add_item( $item );
		wc_do_deprecated_action( 'woocommerce_order_add_shipping', array( $this->get_id(), $item->get_id(), $shipping_rate ), '3.0', 'woocommerce_new_order_item action instead.' );
		return $item->get_id();
	}

	/**
	 * Add a fee to the order.
	 * Order must be saved prior to adding items.
	 *
	 * Fee is an amount of money charged for a particular piece of work
	 * or for a particular right or service, and not supposed to be negative.
	 *
	 * @throws WC_Data_Exception
	 * @param  object $fee Fee data.
	 * @return int         Updated order item ID.
	 */
	public function add_fee( $fee ) {
		wc_deprecated_function( 'WC_Order::add_fee', '3.0', 'a new WC_Order_Item_Fee object and add to order with WC_Order::add_item()' );

		$item = new WC_Order_Item_Fee();
		$item->set_props( array(
			'name'      => $fee->name,
			'tax_class' => $fee->taxable ? $fee->tax_class : 0,
			'total'     => $fee->amount,
			'total_tax' => $fee->tax,
			'taxes'     => array(
				'total' => $fee->tax_data,
			),
			'order_id'  => $this->get_id(),
		) );
		$item->save();
		$this->add_item( $item );
		wc_do_deprecated_action( 'woocommerce_order_add_fee', array( $this->get_id(), $item->get_id(), $fee ), '3.0', 'woocommerce_new_order_item action instead.' );
		return $item->get_id();
	}

	/**
	 * Update a line item for the order.
	 *
	 * Note this does not update order totals.
	 *
	 * @param object|int $item order item ID or item object.
	 * @param WC_Product $product
	 * @param array $args data to update.
	 * @return int updated order item ID
	 * @throws WC_Data_Exception
	 */
	public function update_product( $item, $product, $args ) {
		wc_deprecated_function( 'WC_Order::update_product', '3.0', 'an interaction with the WC_Order_Item_Product class' );
		if ( is_numeric( $item ) ) {
			$item = $this->get_item( $item );
		}
		if ( ! is_object( $item ) || ! $item->is_type( 'line_item' ) ) {
			return false;
		}
		if ( ! $this->get_id() ) {
			$this->save(); // Order must exist
		}

		// BW compatibility with old args
		if ( isset( $args['totals'] ) ) {
			foreach ( $args['totals'] as $key => $value ) {
				if ( 'tax' === $key ) {
					$args['total_tax'] = $value;
				} elseif ( 'tax_data' === $key ) {
					$args['taxes'] = $value;
				} else {
					$args[ $key ] = $value;
				}
			}
		}

		// Handle qty if set.
		if ( isset( $args['qty'] ) ) {
			if ( $product->backorders_require_notification() && $product->is_on_backorder( $args['qty'] ) ) {
				$item->add_meta_data( apply_filters( 'woocommerce_backordered_item_meta_name', __( 'Backordered', 'woocommerce' ), $item ), $args['qty'] - max( 0, $product->get_stock_quantity() ), true );
			}
			$args['subtotal'] = $args['subtotal'] ? $args['subtotal'] : wc_get_price_excluding_tax( $product, array( 'qty' => $args['qty'] ) );
			$args['total']	= $args['total'] ? $args['total'] : wc_get_price_excluding_tax( $product, array( 'qty' => $args['qty'] ) );
		}

		$item->set_order_id( $this->get_id() );
		$item->set_props( $args );
		$item->save();
		do_action( 'woocommerce_order_edit_product', $this->get_id(), $item->get_id(), $args, $product );

		return $item->get_id();
	}

	/**
	 * Update coupon for order. Note this does not update order totals.
	 * @param object|int $item
	 * @param array $args
	 * @return int updated order item ID
	 * @throws WC_Data_Exception
	 */
	public function update_coupon( $item, $args ) {
		wc_deprecated_function( 'WC_Order::update_coupon', '3.0', 'an interaction with the WC_Order_Item_Coupon class' );
		if ( is_numeric( $item ) ) {
			$item = $this->get_item( $item );
		}
		if ( ! is_object( $item ) || ! $item->is_type( 'coupon' ) ) {
			return false;
		}
		if ( ! $this->get_id() ) {
			$this->save(); // Order must exist
		}

		// BW compatibility for old args
		if ( isset( $args['discount_amount'] ) ) {
			$args['discount'] = $args['discount_amount'];
		}
		if ( isset( $args['discount_amount_tax'] ) ) {
			$args['discount_tax'] = $args['discount_amount_tax'];
		}

		$item->set_order_id( $this->get_id() );
		$item->set_props( $args );
		$item->save();

		do_action( 'woocommerce_order_update_coupon', $this->get_id(), $item->get_id(), $args );

		return $item->get_id();
	}

	/**
	 * Update shipping method for order.
	 *
	 * Note this does not update the order total.
	 *
	 * @param object|int $item
	 * @param array $args
	 * @return int updated order item ID
	 * @throws WC_Data_Exception
	 */
	public function update_shipping( $item, $args ) {
		wc_deprecated_function( 'WC_Order::update_shipping', '3.0', 'an interaction with the WC_Order_Item_Shipping class' );
		if ( is_numeric( $item ) ) {
			$item = $this->get_item( $item );
		}
		if ( ! is_object( $item ) || ! $item->is_type( 'shipping' ) ) {
			return false;
		}
		if ( ! $this->get_id() ) {
			$this->save(); // Order must exist
		}

		// BW compatibility for old args
		if ( isset( $args['cost'] ) ) {
			$args['total'] = $args['cost'];
		}

		$item->set_order_id( $this->get_id() );
		$item->set_props( $args );
		$item->save();
		$this->calculate_shipping();

		do_action( 'woocommerce_order_update_shipping', $this->get_id(), $item->get_id(), $args );

		return $item->get_id();
	}

	/**
	 * Update fee for order.
	 *
	 * Note this does not update order totals.
	 *
	 * @param object|int $item
	 * @param array $args
	 * @return int updated order item ID
	 * @throws WC_Data_Exception
	 */
	public function update_fee( $item, $args ) {
		wc_deprecated_function( 'WC_Order::update_fee', '3.0', 'an interaction with the WC_Order_Item_Fee class' );
		if ( is_numeric( $item ) ) {
			$item = $this->get_item( $item );
		}
		if ( ! is_object( $item ) || ! $item->is_type( 'fee' ) ) {
			return false;
		}
		if ( ! $this->get_id() ) {
			$this->save(); // Order must exist
		}

		$item->set_order_id( $this->get_id() );
		$item->set_props( $args );
		$item->save();

		do_action( 'woocommerce_order_update_fee', $this->get_id(), $item->get_id(), $args );

		return $item->get_id();
	}

	/**
	 * Update tax line on order.
	 * Note this does not update order totals.
	 *
	 * @since 3.0
	 * @param object|int $item
	 * @param array $args
	 * @return int updated order item ID
	 * @throws WC_Data_Exception
	 */
	public function update_tax( $item, $args ) {
		wc_deprecated_function( 'WC_Order::update_tax', '3.0', 'an interaction with the WC_Order_Item_Tax class' );
		if ( is_numeric( $item ) ) {
			$item = $this->get_item( $item );
		}
		if ( ! is_object( $item ) || ! $item->is_type( 'tax' ) ) {
			return false;
		}
		if ( ! $this->get_id() ) {
			$this->save(); // Order must exist
		}

		$item->set_order_id( $this->get_id() );
		$item->set_props( $args );
		$item->save();

		do_action( 'woocommerce_order_update_tax', $this->get_id(), $item->get_id(), $args );

		return $item->get_id();
	}

	/**
	 * Get a product (either product or variation).
	 * @deprecated 4.4.0
	 * @param object $item
	 * @return WC_Product|bool
	 */
	public function get_product_from_item( $item ) {
		wc_deprecated_function( 'WC_Abstract_Legacy_Order::get_product_from_item', '4.4.0', '$item->get_product()' );
		if ( is_callable( array( $item, 'get_product' ) ) ) {
			$product = $item->get_product();
		} else {
			$product = false;
		}
		return apply_filters( 'woocommerce_get_product_from_item', $product, $item, $this );
	}

	/**
	 * Set the customer address.
	 * @param array $address Address data.
	 * @param string $type billing or shipping.
	 */
	public function set_address( $address, $type = 'billing' ) {
		foreach ( $address as $key => $value ) {
			update_post_meta( $this->get_id(), "_{$type}_" . $key, $value );
			if ( is_callable( array( $this, "set_{$type}_{$key}" ) ) ) {
				$this->{"set_{$type}_{$key}"}( $value );
			}
		}
	}

	/**
	 * Set an order total.
	 * @param float $amount
	 * @param string $total_type
	 * @return bool
	 */
	public function legacy_set_total( $amount, $total_type = 'total' ) {
		if ( ! in_array( $total_type, array( 'shipping', 'tax', 'shipping_tax', 'total', 'cart_discount', 'cart_discount_tax' ) ) ) {
			return false;
		}

		switch ( $total_type ) {
			case 'total' :
				$amount = wc_format_decimal( $amount, wc_get_price_decimals() );
				$this->set_total( $amount );
				update_post_meta( $this->get_id(), '_order_total', $amount );
				break;
			case 'cart_discount' :
				$amount = wc_format_decimal( $amount );
				$this->set_discount_total( $amount );
				update_post_meta( $this->get_id(), '_cart_discount', $amount );
				break;
			case 'cart_discount_tax' :
				$amount = wc_format_decimal( $amount );
				$this->set_discount_tax( $amount );
				update_post_meta( $this->get_id(), '_cart_discount_tax', $amount );
				break;
			case 'shipping' :
				$amount = wc_format_decimal( $amount );
				$this->set_shipping_total( $amount );
				update_post_meta( $this->get_id(), '_order_shipping', $amount );
				break;
			case 'shipping_tax' :
				$amount = wc_format_decimal( $amount );
				$this->set_shipping_tax( $amount );
				update_post_meta( $this->get_id(), '_order_shipping_tax', $amount );
				break;
			case 'tax' :
				$amount = wc_format_decimal( $amount );
				$this->set_cart_tax( $amount );
				update_post_meta( $this->get_id(), '_order_tax', $amount );
				break;
		}

		return true;
	}

	/**
	 * Magic __isset method for backwards compatibility. Handles legacy properties which could be accessed directly in the past.
	 *
	 * @param string $key
	 * @return bool
	 */
	public function __isset( $key ) {
		$legacy_props = array( 'completed_date', 'id', 'order_type', 'post', 'status', 'post_status', 'customer_note', 'customer_message', 'user_id', 'customer_user', 'prices_include_tax', 'tax_display_cart', 'display_totals_ex_tax', 'display_cart_ex_tax', 'order_date', 'modified_date', 'cart_discount', 'cart_discount_tax', 'order_shipping', 'order_shipping_tax', 'order_total', 'order_tax', 'billing_first_name', 'billing_last_name', 'billing_company', 'billing_address_1', 'billing_address_2', 'billing_city', 'billing_state', 'billing_postcode', 'billing_country', 'billing_phone', 'billing_email', 'shipping_first_name', 'shipping_last_name', 'shipping_company', 'shipping_address_1', 'shipping_address_2', 'shipping_city', 'shipping_state', 'shipping_postcode', 'shipping_country', 'customer_ip_address', 'customer_user_agent', 'payment_method_title', 'payment_method', 'order_currency' );
		return $this->get_id() ? ( in_array( $key, $legacy_props ) || metadata_exists( 'post', $this->get_id(), '_' . $key ) ) : false;
	}

	/**
	 * Magic __get method for backwards compatibility.
	 *
	 * @param string $key
	 * @return mixed
	 */
	public function __get( $key ) {
		wc_doing_it_wrong( $key, 'Order properties should not be accessed directly.', '3.0' );

		if ( 'completed_date' === $key ) {
			return $this->get_date_completed() ? gmdate( 'Y-m-d H:i:s', $this->get_date_completed()->getOffsetTimestamp() ) : '';
		} elseif ( 'paid_date' === $key ) {
			return $this->get_date_paid() ? gmdate( 'Y-m-d H:i:s', $this->get_date_paid()->getOffsetTimestamp() ) : '';
		} elseif ( 'modified_date' === $key ) {
			return $this->get_date_modified() ? gmdate( 'Y-m-d H:i:s', $this->get_date_modified()->getOffsetTimestamp() ) : '';
		} elseif ( 'order_date' === $key ) {
			return $this->get_date_created() ? gmdate( 'Y-m-d H:i:s', $this->get_date_created()->getOffsetTimestamp() ) : '';
		} elseif ( 'id' === $key ) {
			return $this->get_id();
		} elseif ( 'post' === $key ) {
			return get_post( $this->get_id() );
		} elseif ( 'status' === $key ) {
			return $this->get_status();
		} elseif ( 'post_status' === $key ) {
			return get_post_status( $this->get_id() );
		} elseif ( 'customer_message' === $key || 'customer_note' === $key ) {
			return $this->get_customer_note();
		} elseif ( in_array( $key, array( 'user_id', 'customer_user' ) ) ) {
			return $this->get_customer_id();
		} elseif ( 'tax_display_cart' === $key ) {
			return get_option( 'woocommerce_tax_display_cart' );
		} elseif ( 'display_totals_ex_tax' === $key ) {
			return 'excl' === get_option( 'woocommerce_tax_display_cart' );
		} elseif ( 'display_cart_ex_tax' === $key ) {
			return 'excl' === get_option( 'woocommerce_tax_display_cart' );
		} elseif ( 'cart_discount' === $key ) {
			return $this->get_total_discount();
		} elseif ( 'cart_discount_tax' === $key ) {
			return $this->get_discount_tax();
		} elseif ( 'order_tax' === $key ) {
			return $this->get_cart_tax();
		} elseif ( 'order_shipping_tax' === $key ) {
			return $this->get_shipping_tax();
		} elseif ( 'order_shipping' === $key ) {
			return $this->get_shipping_total();
		} elseif ( 'order_total' === $key ) {
			return $this->get_total();
		} elseif ( 'order_type' === $key ) {
			return $this->get_type();
		} elseif ( 'order_currency' === $key ) {
			return $this->get_currency();
		} elseif ( 'order_version' === $key ) {
			return $this->get_version();
	 	} elseif ( is_callable( array( $this, "get_{$key}" ) ) ) {
			return $this->{"get_{$key}"}();
		} else {
			return get_post_meta( $this->get_id(), '_' . $key, true );
		}
	}

	/**
	 * has_meta function for order items. This is different to the WC_Data
	 * version and should be removed in future versions.
	 *
	 * @deprecated 3.0
	 *
	 * @param int $order_item_id
	 *
	 * @return array of meta data.
	 */
	public function has_meta( $order_item_id ) {
		global $wpdb;

		wc_deprecated_function( 'WC_Order::has_meta( $order_item_id )', '3.0', 'WC_Order_item::get_meta_data' );

		return $wpdb->get_results( $wpdb->prepare( "SELECT meta_key, meta_value, meta_id, order_item_id
			FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE order_item_id = %d
			ORDER BY meta_id", absint( $order_item_id ) ), ARRAY_A );
	}

	/**
	 * Display meta data belonging to an item.
	 * @param  array $item
	 */
	public function display_item_meta( $item ) {
		wc_deprecated_function( 'WC_Order::display_item_meta', '3.0', 'wc_display_item_meta' );
		$product   = $item->get_product();
		$item_meta = new WC_Order_Item_Meta( $item, $product );
		$item_meta->display();
	}

	/**
	 * Display download links for an order item.
	 * @param  array $item
	 */
	public function display_item_downloads( $item ) {
		wc_deprecated_function( 'WC_Order::display_item_downloads', '3.0', 'wc_display_item_downloads' );
		$product   = $item->get_product();

		if ( $product && $product->exists() && $product->is_downloadable() && $this->is_download_permitted() ) {
			$download_files = $this->get_item_downloads( $item );
			$i			  = 0;
			$links		  = array();

			foreach ( $download_files as $download_id => $file ) {
				$i++;
				/* translators: 1: current item count */
				$prefix  = count( $download_files ) > 1 ? sprintf( __( 'Download %d', 'woocommerce' ), $i ) : __( 'Download', 'woocommerce' );
				$links[] = '<small class="download-url">' . esc_html( $prefix ) . ': <a href="' . esc_url( $file['download_url'] ) . '" target="_blank">' . esc_html( $file['name'] ) . '</a></small>' . "\n";
			}

			echo '<br/>' . implode( '<br/>', $links );
		}
	}

	/**
	 * Get the Download URL.
	 *
	 * @param  int $product_id
	 * @param  int $download_id
	 * @return string
	 */
	public function get_download_url( $product_id, $download_id ) {
		wc_deprecated_function( 'WC_Order::get_download_url', '3.0', 'WC_Order_Item_Product::get_item_download_url' );
		return add_query_arg( array(
			'download_file' => $product_id,
			'order'         => $this->get_order_key(),
			'email'         => urlencode( $this->get_billing_email() ),
			'key'           => $download_id,
		), trailingslashit( home_url() ) );
	}

	/**
	 * Get the downloadable files for an item in this order.
	 *
	 * @param  array $item
	 * @return array
	 */
	public function get_item_downloads( $item ) {
		wc_deprecated_function( 'WC_Order::get_item_downloads', '3.0', 'WC_Order_Item_Product::get_item_downloads' );

		if ( ! $item instanceof WC_Order_Item ) {
			if ( ! empty( $item['variation_id'] ) ) {
				$product_id = $item['variation_id'];
			} elseif ( ! empty( $item['product_id'] ) ) {
				$product_id = $item['product_id'];
			} else {
				return array();
			}

			// Create a 'virtual' order item to allow retrieving item downloads when
			// an array of product_id is passed instead of actual order item.
			$item = new WC_Order_Item_Product();
			$item->set_product( wc_get_product( $product_id ) );
			$item->set_order_id( $this->get_id() );
		}

		return $item->get_item_downloads();
	}

	/**
	 * Gets shipping total. Alias of WC_Order::get_shipping_total().
	 * @deprecated 3.0.0 since this is an alias only.
	 * @return float
	 */
	public function get_total_shipping() {
		return $this->get_shipping_total();
	}

	/**
	 * Get order item meta.
	 * @deprecated 3.0.0
	 * @param mixed $order_item_id
	 * @param string $key (default: '')
	 * @param bool $single (default: false)
	 * @return array|string
	 */
	public function get_item_meta( $order_item_id, $key = '', $single = false ) {
		wc_deprecated_function( 'WC_Order::get_item_meta', '3.0', 'wc_get_order_item_meta' );
		return get_metadata( 'order_item', $order_item_id, $key, $single );
	}

	/**
	 * Get all item meta data in array format in the order it was saved. Does not group meta by key like get_item_meta().
	 *
	 * @param mixed $order_item_id
	 * @return array of objects
	 */
	public function get_item_meta_array( $order_item_id ) {
		wc_deprecated_function( 'WC_Order::get_item_meta_array', '3.0', 'WC_Order_Item::get_meta_data() (note the format has changed)' );
		$item            = $this->get_item( $order_item_id );
		$meta_data       = $item->get_meta_data();
		$item_meta_array = array();

		foreach ( $meta_data as $meta ) {
			$item_meta_array[ $meta->id ] = $meta;
		}

		return $item_meta_array;
	}

	/**
	 * Get coupon codes only.
	 *
	 * @deprecated 3.7.0 - Replaced with better named method to reflect the actual data being returned.
	 * @return array
	 */
	public function get_used_coupons() {
		wc_deprecated_function( 'get_used_coupons', '3.7', 'WC_Abstract_Order::get_coupon_codes' );
		return $this->get_coupon_codes();
	}

	/**
	 * Expand item meta into the $item array.
	 * @deprecated 3.0.0 Item meta no longer expanded due to new order item
	 *		classes. This function now does nothing to avoid data breakage.
	 * @param array $item before expansion.
	 * @return array
	 */
	public function expand_item_meta( $item ) {
		wc_deprecated_function( 'WC_Order::expand_item_meta', '3.0' );
		return $item;
	}

	/**
	 * Load the order object. Called from the constructor.
	 * @deprecated 3.0.0 Logic moved to constructor
	 * @param int|object|WC_Order $order Order to init.
	 */
	protected function init( $order ) {
		wc_deprecated_function( 'WC_Order::init', '3.0', 'Logic moved to constructor' );
		if ( is_numeric( $order ) ) {
			$this->set_id( $order );
		} elseif ( $order instanceof WC_Order ) {
			$this->set_id( absint( $order->get_id() ) );
		} elseif ( isset( $order->ID ) ) {
			$this->set_id( absint( $order->ID ) );
		}
		$this->set_object_read( false );
		$this->data_store->read( $this );
	}

	/**
	 * Gets an order from the database.
	 * @deprecated 3.0
	 * @param int $id (default: 0).
	 * @return bool
	 */
	public function get_order( $id = 0 ) {
		wc_deprecated_function( 'WC_Order::get_order', '3.0' );

		if ( ! $id ) {
			return false;
		}

		$result = get_post( $id );

		if ( $result ) {
			$this->populate( $result );
			return true;
		}

		return false;
	}

	/**
	 * Populates an order from the loaded post data.
	 * @deprecated 3.0
	 * @param mixed $result
	 */
	public function populate( $result ) {
		wc_deprecated_function( 'WC_Order::populate', '3.0' );
		$this->set_id( $result->ID );
		$this->set_object_read( false );
		$this->data_store->read( $this );
	}

	/**
	 * Cancel the order and restore the cart (before payment).
	 * @deprecated 3.0.0 Moved to event handler.
	 * @param string $note (default: '') Optional note to add.
	 */
	public function cancel_order( $note = '' ) {
		wc_deprecated_function( 'WC_Order::cancel_order', '3.0', 'WC_Order::update_status' );
		WC()->session->set( 'order_awaiting_payment', false );
		$this->update_status( 'cancelled', $note );
	}

	/**
	 * Record sales.
	 * @deprecated 3.0.0
	 */
	public function record_product_sales() {
		wc_deprecated_function( 'WC_Order::record_product_sales', '3.0', 'wc_update_total_sales_counts' );
		wc_update_total_sales_counts( $this->get_id() );
	}

	/**
	 * Increase applied coupon counts.
	 * @deprecated 3.0.0
	 */
	public function increase_coupon_usage_counts() {
		wc_deprecated_function( 'WC_Order::increase_coupon_usage_counts', '3.0', 'wc_update_coupon_usage_counts' );
		wc_update_coupon_usage_counts( $this->get_id() );
	}

	/**
	 * Decrease applied coupon counts.
	 * @deprecated 3.0.0
	 */
	public function decrease_coupon_usage_counts() {
		wc_deprecated_function( 'WC_Order::decrease_coupon_usage_counts', '3.0', 'wc_update_coupon_usage_counts' );
		wc_update_coupon_usage_counts( $this->get_id() );
	}

	/**
	 * Reduce stock levels for all line items in the order.
	 * @deprecated 3.0.0
	 */
	public function reduce_order_stock() {
		wc_deprecated_function( 'WC_Order::reduce_order_stock', '3.0', 'wc_reduce_stock_levels' );
		wc_reduce_stock_levels( $this->get_id() );
	}

	/**
	 * Send the stock notifications.
	 * @deprecated 3.0.0 No longer needs to be called directly.
	 *
	 * @param $product
	 * @param $new_stock
	 * @param $qty_ordered
	 */
	public function send_stock_notifications( $product, $new_stock, $qty_ordered ) {
		wc_deprecated_function( 'WC_Order::send_stock_notifications', '3.0' );
	}

	/**
	 * Output items for display in html emails.
	 * @deprecated 3.0.0 Moved to template functions.
	 * @param array $args Items args.
	 * @return string
	 */
	public function email_order_items_table( $args = array() ) {
		wc_deprecated_function( 'WC_Order::email_order_items_table', '3.0', 'wc_get_email_order_items' );
		return wc_get_email_order_items( $this, $args );
	}

	/**
	 * Get currency.
	 * @deprecated 3.0.0
	 */
	public function get_order_currency() {
		wc_deprecated_function( 'WC_Order::get_order_currency', '3.0', 'WC_Order::get_currency' );
		return apply_filters( 'woocommerce_get_order_currency', $this->get_currency(), $this );
	}
}
legacy/class-wc-legacy-webhook.php000064400000005010151336044770013141 0ustar00<?php
/**
 * Legacy Webhook
 *
 * Legacy and deprecated functions are here to keep the WC_Legacy_Webhook class clean.
 * This class will be removed in future versions.
 *
 * @version  3.2.0
 * @package  WooCommerce\Classes
 * @category Class
 * @author   Automattic
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Legacy Webhook class.
 */
abstract class WC_Legacy_Webhook extends WC_Data {

	/**
	 * Magic __isset method for backwards compatibility. Legacy properties which could be accessed directly in the past.
	 *
	 * @param  string $key Item to check.
	 * @return bool
	 */
	public function __isset( $key ) {
		$legacy_keys = array(
			'id',
			'status',
			'post_data',
			'delivery_url',
			'secret',
			'topic',
			'hooks',
			'resource',
			'event',
			'failure_count',
			'api_version',
		);

		if ( in_array( $key, $legacy_keys, true ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Magic __get method for backwards compatibility. Maps legacy vars to new getters.
	 *
	 * @param  string $key Item to get.
	 * @return mixed
	 */
	public function __get( $key ) {
		wc_doing_it_wrong( $key, 'Webhook properties should not be accessed directly.', '3.2' );

		switch ( $key ) {
			case 'id' :
				$value = $this->get_id();
				break;
			case 'status' :
				$value = $this->get_status();
				break;
			case 'post_data' :
				$value = null;
				break;
			case 'delivery_url' :
				$value = $this->get_delivery_url();
				break;
			case 'secret' :
				$value = $this->get_secret();
				break;
			case 'topic' :
				$value = $this->get_topic();
				break;
			case 'hooks' :
				$value = $this->get_hooks();
				break;
			case 'resource' :
				$value = $this->get_resource();
				break;
			case 'event' :
				$value = $this->get_event();
				break;
			case 'failure_count' :
				$value = $this->get_failure_count();
				break;
			case 'api_version' :
				$value = $this->get_api_version();
				break;

			default :
				$value = '';
				break;
		} // End switch().

		return $value;
	}

	/**
	 * Get the post data for the webhook.
	 *
	 * @deprecated 3.2.0
	 * @since      2.2
	 * @return     null|WP_Post
	 */
	public function get_post_data() {
		wc_deprecated_function( 'WC_Webhook::get_post_data', '3.2' );

		return null;
	}

	/**
	 * Update the webhook status.
	 *
	 * @deprecated 3.2.0
	 * @since      2.2.0
	 * @param      string $status Status to set.
	 */
	public function update_status( $status ) {
		wc_deprecated_function( 'WC_Webhook::update_status', '3.2', 'WC_Webhook::set_status' );

		$this->set_status( $status );
		$this->save();
	}
}
legacy/class-wc-legacy-shipping-zone.php000064400000003107151336044770014302 0ustar00<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Legacy Shipping Zone.
 *
 * @version  3.0.0
 * @package  WooCommerce\Classes
 * @category Class
 * @author   WooThemes
 */
abstract class WC_Legacy_Shipping_Zone extends WC_Data {

	/**
	 * Get zone ID
	 * @return int|null Null if the zone does not exist. 0 is the default zone.
	 * @deprecated 3.0
	 */
	public function get_zone_id() {
		wc_deprecated_function( 'WC_Shipping_Zone::get_zone_id', '3.0', 'WC_Shipping_Zone::get_id' );
		return $this->get_id();
	}

	/**
	 * Read a shipping zone by ID.
	 * @deprecated 3.0.0 - Init a shipping zone with an ID.
	 *
	 * @param int $zone_id
	 */
	public function read( $zone_id ) {
		wc_deprecated_function( 'WC_Shipping_Zone::read', '3.0', 'a shipping zone initialized with an ID.' );
		$this->set_id( $zone_id );
		$data_store = WC_Data_Store::load( 'shipping-zone' );
		$data_store->read( $this );
	}

	/**
	 * Update a zone.
	 * @deprecated 3.0.0 - Use ::save instead.
	 */
	public function update() {
		wc_deprecated_function( 'WC_Shipping_Zone::update', '3.0', 'WC_Shipping_Zone::save instead.' );
		$data_store = WC_Data_Store::load( 'shipping-zone' );
		try {
			$data_store->update( $this );
		} catch ( Exception $e ) {
			return false;
		}
	}

	/**
	 * Create a zone.
	 * @deprecated 3.0.0 - Use ::save instead.
	 */
	public function create() {
		wc_deprecated_function( 'WC_Shipping_Zone::create', '3.0', 'WC_Shipping_Zone::save instead.' );
		$data_store = WC_Data_Store::load( 'shipping-zone' );
		try {
			$data_store->create( $this );
		} catch ( Exception $e ) {
			return false;
		}
	}


}
legacy/class-wc-legacy-coupon.php000064400000011631151336044770013014 0ustar00<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Legacy Coupon.
 *
 * Legacy and deprecated functions are here to keep the WC_Legacy_Coupon class clean.
 * This class will be removed in future versions.
 *
 * @class       WC_Legacy_Coupon
 * @version     3.0.0
 * @package     WooCommerce\Classes
 * @category    Class
 * @author      WooThemes
 */
abstract class WC_Legacy_Coupon extends WC_Data {

	/**
	 * Magic __isset method for backwards compatibility. Legacy properties which could be accessed directly in the past.
	 * @param  string $key
	 * @return bool
	 */
	public function __isset( $key ) {
		$legacy_keys = array(
			'id',
			'exists',
			'coupon_custom_fields',
			'type',
			'discount_type',
			'amount',
			'coupon_amount',
			'code',
			'individual_use',
			'product_ids',
			'exclude_product_ids',
			'usage_limit',
			'usage_limit_per_user',
			'limit_usage_to_x_items',
			'usage_count',
			'expiry_date',
			'product_categories',
			'exclude_product_categories',
			'minimum_amount',
			'maximum_amount',
			'customer_email',
		);
		if ( in_array( $key, $legacy_keys ) ) {
			return true;
		}
		return false;
	}

	/**
	 * Magic __get method for backwards compatibility. Maps legacy vars to new getters.
	 * @param  string $key
	 * @return mixed
	 */
	public function __get( $key ) {
		wc_doing_it_wrong( $key, 'Coupon properties should not be accessed directly.', '3.0' );

		switch ( $key ) {
			case 'id' :
				$value = $this->get_id();
			break;
			case 'exists' :
				$value = $this->get_id() > 0;
			break;
			case 'coupon_custom_fields' :
				$legacy_custom_fields = array();
				$custom_fields = $this->get_id() ? $this->get_meta_data() : array();
				if ( ! empty( $custom_fields ) ) {
					foreach ( $custom_fields as  $cf_value ) {
						// legacy only supports 1 key
						$legacy_custom_fields[ $cf_value->key ][0] = $cf_value->value;
					}
				}
				$value = $legacy_custom_fields;
			break;
			case 'type' :
			case 'discount_type' :
				$value = $this->get_discount_type();
			break;
			case 'amount' :
			case 'coupon_amount' :
				$value = $this->get_amount();
			break;
			case 'code' :
				$value = $this->get_code();
			break;
			case 'individual_use' :
				$value = ( true === $this->get_individual_use() ) ? 'yes' : 'no';
			break;
			case 'product_ids' :
				$value = $this->get_product_ids();
			break;
			case 'exclude_product_ids' :
				$value = $this->get_excluded_product_ids();
			break;
			case 'usage_limit' :
				$value = $this->get_usage_limit();
			break;
			case 'usage_limit_per_user' :
				$value = $this->get_usage_limit_per_user();
			break;
			case 'limit_usage_to_x_items' :
				$value = $this->get_limit_usage_to_x_items();
			break;
			case 'usage_count' :
				$value = $this->get_usage_count();
			break;
			case 'expiry_date' :
				$value = ( $this->get_date_expires() ? $this->get_date_expires()->date( 'Y-m-d' ) : '' );
			break;
			case 'product_categories' :
				$value = $this->get_product_categories();
			break;
			case 'exclude_product_categories' :
				$value = $this->get_excluded_product_categories();
			break;
			case 'minimum_amount' :
				$value = $this->get_minimum_amount();
			break;
			case 'maximum_amount' :
				$value = $this->get_maximum_amount();
			break;
			case 'customer_email' :
				$value = $this->get_email_restrictions();
			break;
			default :
				$value = '';
			break;
		}

		return $value;
	}

	/**
	 * Format loaded data as array.
	 * @param  string|array $array
	 * @return array
	 */
	public function format_array( $array ) {
		wc_deprecated_function( 'WC_Coupon::format_array', '3.0' );
		if ( ! is_array( $array ) ) {
			if ( is_serialized( $array ) ) {
				$array = maybe_unserialize( $array );
			} else {
				$array = explode( ',', $array );
			}
		}
		return array_filter( array_map( 'trim', array_map( 'strtolower', $array ) ) );
	}


	/**
	 * Check if coupon needs applying before tax.
	 *
	 * @return bool
	 */
	public function apply_before_tax() {
		wc_deprecated_function( 'WC_Coupon::apply_before_tax', '3.0' );
		return true;
	}

	/**
	 * Check if a coupon enables free shipping.
	 *
	 * @return bool
	 */
	public function enable_free_shipping() {
		wc_deprecated_function( 'WC_Coupon::enable_free_shipping', '3.0', 'WC_Coupon::get_free_shipping' );
		return $this->get_free_shipping();
	}

	/**
	 * Check if a coupon excludes sale items.
	 *
	 * @return bool
	 */
	public function exclude_sale_items() {
		wc_deprecated_function( 'WC_Coupon::exclude_sale_items', '3.0', 'WC_Coupon::get_exclude_sale_items' );
		return $this->get_exclude_sale_items();
	}

	/**
	 * Increase usage count for current coupon.
	 *
	 * @param string $used_by Either user ID or billing email
	 */
	public function inc_usage_count( $used_by = '' ) {
		$this->increase_usage_count( $used_by );
	}

	/**
	 * Decrease usage count for current coupon.
	 *
	 * @param string $used_by Either user ID or billing email
	 */
	public function dcr_usage_count( $used_by = '' ) {
		$this->decrease_usage_count( $used_by );
	}
}
legacy/class-wc-legacy-cart.php000064400000030561151336044770012445 0ustar00<?php
/**
 * Legacy cart
 *
 * Legacy and deprecated functions are here to keep the WC_Cart class clean.
 * This class will be removed in future versions.
 *
 * @version  3.2.0
 * @package  WooCommerce\Classes
 * @category Class
 * @author   Automattic
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Legacy cart class.
 */
abstract class WC_Legacy_Cart {

	/**
	 * Array of defaults. Not used since 3.2.
	 *
	 * @deprecated 3.2.0
	 */
	public $cart_session_data = array(
		'cart_contents_total'         => 0,
		'total'                       => 0,
		'subtotal'                    => 0,
		'subtotal_ex_tax'             => 0,
		'tax_total'                   => 0,
		'taxes'                       => array(),
		'shipping_taxes'              => array(),
		'discount_cart'               => 0,
		'discount_cart_tax'           => 0,
		'shipping_total'              => 0,
		'shipping_tax_total'          => 0,
		'coupon_discount_amounts'     => array(),
		'coupon_discount_tax_amounts' => array(),
		'fee_total'                   => 0,
		'fees'                        => array(),
	);

	/**
	 * Contains an array of coupon usage counts after they have been applied.
	 *
	 * @deprecated 3.2.0
	 * @var array
	 */
	public $coupon_applied_count = array();

	/**
	 * Map legacy variables.
	 *
	 * @param string $name Property name.
	 * @param mixed  $value Value to set.
	 */
	public function __isset( $name ) {
		$legacy_keys = array_merge(
			array(
				'dp',
				'prices_include_tax',
				'round_at_subtotal',
				'cart_contents_total',
				'total',
				'subtotal',
				'subtotal_ex_tax',
				'tax_total',
				'fee_total',
				'discount_cart',
				'discount_cart_tax',
				'shipping_total',
				'shipping_tax_total',
				'display_totals_ex_tax',
				'display_cart_ex_tax',
				'cart_contents_weight',
				'cart_contents_count',
				'coupons',
				'taxes',
				'shipping_taxes',
				'coupon_discount_amounts',
				'coupon_discount_tax_amounts',
				'fees',
				'tax',
				'discount_total',
				'tax_display_cart',
			),
			is_array( $this->cart_session_data ) ? array_keys( $this->cart_session_data ) : array()
		);

		if ( in_array( $name, $legacy_keys, true ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Magic getters.
	 *
	 * If you add/remove cases here please update $legacy_keys in __isset accordingly.
	 *
	 * @param string $name Property name.
	 * @return mixed
	 */
	public function &__get( $name ) {
		$value = '';

		switch ( $name ) {
			case 'dp' :
				$value = wc_get_price_decimals();
				break;
			case 'prices_include_tax' :
				$value = wc_prices_include_tax();
				break;
			case 'round_at_subtotal' :
				$value = 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' );
				break;
			case 'cart_contents_total' :
				$value = $this->get_cart_contents_total();
				break;
			case 'total' :
				$value = $this->get_total( 'edit' );
				break;
			case 'subtotal' :
				$value = $this->get_subtotal() + $this->get_subtotal_tax();
				break;
			case 'subtotal_ex_tax' :
				$value = $this->get_subtotal();
				break;
			case 'tax_total' :
				$value = $this->get_fee_tax() + $this->get_cart_contents_tax();
				break;
			case 'fee_total' :
				$value = $this->get_fee_total();
				break;
			case 'discount_cart' :
				$value = $this->get_discount_total();
				break;
			case 'discount_cart_tax' :
				$value = $this->get_discount_tax();
				break;
			case 'shipping_total' :
				$value = $this->get_shipping_total();
				break;
			case 'shipping_tax_total' :
				$value = $this->get_shipping_tax();
				break;
			case 'display_totals_ex_tax' :
			case 'display_cart_ex_tax' :
				$value = ! $this->display_prices_including_tax();
				break;
			case 'cart_contents_weight' :
				$value = $this->get_cart_contents_weight();
				break;
			case 'cart_contents_count' :
				$value = $this->get_cart_contents_count();
				break;
			case 'coupons' :
				$value = $this->get_coupons();
				break;

			// Arrays returned by reference to allow modification without notices. TODO: Remove in 4.0.
			case 'taxes' :
				wc_deprecated_function( 'WC_Cart->taxes', '3.2', sprintf( 'getters (%s) and setters (%s)', 'WC_Cart::get_cart_contents_taxes()', 'WC_Cart::set_cart_contents_taxes()' ) );
				$value = &$this->totals[ 'cart_contents_taxes' ];
				break;
			case 'shipping_taxes' :
				wc_deprecated_function( 'WC_Cart->shipping_taxes', '3.2', sprintf( 'getters (%s) and setters (%s)', 'WC_Cart::get_shipping_taxes()', 'WC_Cart::set_shipping_taxes()' ) );
				$value = &$this->totals[ 'shipping_taxes' ];
				break;
			case 'coupon_discount_amounts' :
				$value = &$this->coupon_discount_totals;
				break;
			case 'coupon_discount_tax_amounts' :
				$value = &$this->coupon_discount_tax_totals;
				break;
			case 'fees' :
				wc_deprecated_function( 'WC_Cart->fees', '3.2', sprintf( 'the fees API (%s)', 'WC_Cart::get_fees' ) );

				// Grab fees from the new API.
				$new_fees   = $this->fees_api()->get_fees();

				// Add new fees to the legacy prop so it can be adjusted via legacy property.
				$this->fees = $new_fees;

				// Return by reference.
				$value = &$this->fees;
				break;
			// Deprecated args. TODO: Remove in 4.0.
			case 'tax' :
				wc_deprecated_argument( 'WC_Cart->tax', '2.3', 'Use WC_Tax directly' );
				$this->tax = new WC_Tax();
				$value = $this->tax;
				break;
			case 'discount_total':
				wc_deprecated_argument( 'WC_Cart->discount_total', '2.3', 'After tax coupons are no longer supported. For more information see: https://woocommerce.wordpress.com/2014/12/upcoming-coupon-changes-in-woocommerce-2-3/' );
				$value = 0;
				break;
			case 'tax_display_cart':
				wc_deprecated_argument( 'WC_Cart->tax_display_cart', '4.4', 'Use WC_Cart->get_tax_price_display_mode() instead.' );
				$value = $this->get_tax_price_display_mode();
				break;
		}
		return $value;
	}

	/**
	 * Map legacy variables to setters.
	 *
	 * @param string $name Property name.
	 * @param mixed  $value Value to set.
	 */
	public function __set( $name, $value ) {
		switch ( $name ) {
			case 'cart_contents_total' :
				$this->set_cart_contents_total( $value );
				break;
			case 'total' :
				$this->set_total( $value );
				break;
			case 'subtotal' :
				$this->set_subtotal( $value );
				break;
			case 'subtotal_ex_tax' :
				$this->set_subtotal( $value );
				break;
			case 'tax_total' :
				$this->set_cart_contents_tax( $value );
				$this->set_fee_tax( 0 );
				break;
			case 'taxes' :
				$this->set_cart_contents_taxes( $value );
				break;
			case 'shipping_taxes' :
				$this->set_shipping_taxes( $value );
				break;
			case 'fee_total' :
				$this->set_fee_total( $value );
				break;
			case 'discount_cart' :
				$this->set_discount_total( $value );
				break;
			case 'discount_cart_tax' :
				$this->set_discount_tax( $value );
				break;
			case 'shipping_total' :
				$this->set_shipping_total( $value );
				break;
			case 'shipping_tax_total' :
				$this->set_shipping_tax( $value );
				break;
			case 'coupon_discount_amounts' :
				$this->set_coupon_discount_totals( $value );
				break;
			case 'coupon_discount_tax_amounts' :
				$this->set_coupon_discount_tax_totals( $value );
				break;
			case 'fees' :
				wc_deprecated_function( 'WC_Cart->fees', '3.2', sprintf( 'the fees API (%s)', 'WC_Cart::add_fee' ) );
				$this->fees = $value;
				break;
			default :
				$this->$name = $value;
				break;
		}
	}

	/**
	 * Methods moved to session class in 3.2.0.
	 */
	public function get_cart_from_session() { $this->session->get_cart_from_session(); }
	public function maybe_set_cart_cookies() { $this->session->maybe_set_cart_cookies(); }
	public function set_session() { $this->session->set_session(); }
	public function get_cart_for_session() { return $this->session->get_cart_for_session(); }
	public function persistent_cart_update() { $this->session->persistent_cart_update(); }
	public function persistent_cart_destroy() { $this->session->persistent_cart_destroy(); }

	/**
	 * Get the total of all cart discounts.
	 *
	 * @return float
	 */
	public function get_cart_discount_total() {
		return $this->get_discount_total();
	}

	/**
	 * Get the total of all cart tax discounts (used for discounts on tax inclusive prices).
	 *
	 * @return float
	 */
	public function get_cart_discount_tax_total() {
		return $this->get_discount_tax();
	}

	/**
	 * Renamed for consistency.
	 *
	 * @param string $coupon_code
	 * @return bool	True if the coupon is applied, false if it does not exist or cannot be applied.
	 */
	public function add_discount( $coupon_code ) {
		return $this->apply_coupon( $coupon_code );
	}
	/**
	 * Remove taxes.
	 *
	 * @deprecated 3.2.0 Taxes are never calculated if customer is tax except making this function unused.
	 */
	public function remove_taxes() {
		wc_deprecated_function( 'WC_Cart::remove_taxes', '3.2', '' );
	}
	/**
	 * Init.
	 *
	 * @deprecated 3.2.0 Session is loaded via hooks rather than directly.
	 */
	public function init() {
		wc_deprecated_function( 'WC_Cart::init', '3.2', '' );
		$this->get_cart_from_session();
	}

	/**
	 * Function to apply discounts to a product and get the discounted price (before tax is applied).
	 *
	 * @deprecated 3.2.0 Calculation and coupon logic is handled in WC_Cart_Totals.
	 * @param mixed $values Cart item.
	 * @param mixed $price Price of item.
	 * @param bool  $add_totals Legacy.
	 * @return float price
	 */
	public function get_discounted_price( $values, $price, $add_totals = false ) {
		wc_deprecated_function( 'WC_Cart::get_discounted_price', '3.2', '' );

		$cart_item_key = $values['key'];
		$cart_item     = $this->cart_contents[ $cart_item_key ];

		return $cart_item['line_total'];
	}

	/**
	 * Gets the url to the cart page.
	 *
	 * @deprecated 2.5.0 in favor to wc_get_cart_url()
	 * @return string url to page
	 */
	public function get_cart_url() {
		wc_deprecated_function( 'WC_Cart::get_cart_url', '2.5', 'wc_get_cart_url' );
		return wc_get_cart_url();
	}

	/**
	 * Gets the url to the checkout page.
	 *
	 * @deprecated 2.5.0 in favor to wc_get_checkout_url()
	 * @return string url to page
	 */
	public function get_checkout_url() {
		wc_deprecated_function( 'WC_Cart::get_checkout_url', '2.5', 'wc_get_checkout_url' );
		return wc_get_checkout_url();
	}

	/**
	 * Sees if we need a shipping address.
	 *
	 * @deprecated 2.5.0 in favor to wc_ship_to_billing_address_only()
	 * @return bool
	 */
	public function ship_to_billing_address_only() {
		wc_deprecated_function( 'WC_Cart::ship_to_billing_address_only', '2.5', 'wc_ship_to_billing_address_only' );
		return wc_ship_to_billing_address_only();
	}

	/**
	 * Coupons enabled function. Filterable.
	 *
	 * @deprecated 2.5.0
	 * @return bool
	 */
	public function coupons_enabled() {
		wc_deprecated_function( 'WC_Legacy_Cart::coupons_enabled', '2.5.0', 'wc_coupons_enabled' );
		return wc_coupons_enabled();
	}

	/**
	 * Gets the total (product) discount amount - these are applied before tax.
	 *
	 * @deprecated 2.3.0 Order discounts (after tax) removed in 2.3 so multiple methods for discounts are no longer required.
	 * @return mixed formatted price or false if there are none.
	 */
	public function get_discounts_before_tax() {
		wc_deprecated_function( 'get_discounts_before_tax', '2.3', 'get_total_discount' );
		if ( $this->get_cart_discount_total() ) {
			$discounts_before_tax = wc_price( $this->get_cart_discount_total() );
		} else {
			$discounts_before_tax = false;
		}
		return apply_filters( 'woocommerce_cart_discounts_before_tax', $discounts_before_tax, $this );
	}

	/**
	 * Get the total of all order discounts (after tax discounts).
	 *
	 * @deprecated 2.3.0 Order discounts (after tax) removed in 2.3.
	 * @return int
	 */
	public function get_order_discount_total() {
		wc_deprecated_function( 'get_order_discount_total', '2.3' );
		return 0;
	}

	/**
	 * Function to apply cart discounts after tax.
	 *
	 * @deprecated 2.3.0 Coupons can not be applied after tax.
	 * @param $values
	 * @param $price
	 */
	public function apply_cart_discounts_after_tax( $values, $price ) {
		wc_deprecated_function( 'apply_cart_discounts_after_tax', '2.3' );
	}

	/**
	 * Function to apply product discounts after tax.
	 *
	 * @deprecated 2.3.0 Coupons can not be applied after tax.
	 *
	 * @param $values
	 * @param $price
	 */
	public function apply_product_discounts_after_tax( $values, $price ) {
		wc_deprecated_function( 'apply_product_discounts_after_tax', '2.3' );
	}

	/**
	 * Gets the order discount amount - these are applied after tax.
	 *
	 * @deprecated 2.3.0 Coupons can not be applied after tax.
	 */
	public function get_discounts_after_tax() {
		wc_deprecated_function( 'get_discounts_after_tax', '2.3' );
	}
}
class-wc-background-emailer.php000064400000011115151336044770012533 0ustar00<?php
/**
 * Background Emailer
 *
 * @version 3.0.1
 * @package WooCommerce\Classes
 */

use Automattic\Jetpack\Constants;

defined( 'ABSPATH' ) || exit;

if ( ! class_exists( 'WC_Background_Process', false ) ) {
	include_once dirname( __FILE__ ) . '/abstracts/class-wc-background-process.php';
}

/**
 * WC_Background_Emailer Class.
 */
class WC_Background_Emailer extends WC_Background_Process {

	/**
	 * Initiate new background process.
	 */
	public function __construct() {
		// Uses unique prefix per blog so each blog has separate queue.
		$this->prefix = 'wp_' . get_current_blog_id();
		$this->action = 'wc_emailer';

		// Dispatch queue after shutdown.
		add_action( 'shutdown', array( $this, 'dispatch_queue' ), 100 );

		parent::__construct();
	}

	/**
	 * Schedule fallback event.
	 */
	protected function schedule_event() {
		if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) {
			wp_schedule_event( time() + 10, $this->cron_interval_identifier, $this->cron_hook_identifier );
		}
	}

	/**
	 * Task
	 *
	 * Override this method to perform any actions required on each
	 * queue item. Return the modified item for further processing
	 * in the next pass through. Or, return false to remove the
	 * item from the queue.
	 *
	 * @param array $callback Update callback function.
	 * @return mixed
	 */
	protected function task( $callback ) {
		if ( isset( $callback['filter'], $callback['args'] ) ) {
			try {
				WC_Emails::send_queued_transactional_email( $callback['filter'], $callback['args'] );
			} catch ( Exception $e ) {
				if ( Constants::is_true( 'WP_DEBUG' ) ) {
					trigger_error( 'Transactional email triggered fatal error for callback ' . esc_html( $callback['filter'] ), E_USER_WARNING ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
				}
			}
		}
		return false;
	}

	/**
	 * Finishes replying to the client, but keeps the process running for further (async) code execution.
	 *
	 * @see https://core.trac.wordpress.org/ticket/41358 .
	 */
	protected function close_http_connection() {
		// Only 1 PHP process can access a session object at a time, close this so the next request isn't kept waiting.
		// @codingStandardsIgnoreStart
		if ( session_id() ) {
			session_write_close();
		}
		// @codingStandardsIgnoreEnd

		wc_set_time_limit( 0 );

		// fastcgi_finish_request is the cleanest way to send the response and keep the script running, but not every server has it.
		if ( is_callable( 'fastcgi_finish_request' ) ) {
			fastcgi_finish_request();
		} else {
			// Fallback: send headers and flush buffers.
			if ( ! headers_sent() ) {
				header( 'Connection: close' );
			}
			@ob_end_flush(); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
			flush();
		}
	}

	/**
	 * Save and run queue.
	 */
	public function dispatch_queue() {
		if ( ! empty( $this->data ) ) {
			$this->close_http_connection();
			$this->save()->dispatch();
		}
	}

	/**
	 * Get post args
	 *
	 * @return array
	 */
	protected function get_post_args() {
		if ( property_exists( $this, 'post_args' ) ) {
			return $this->post_args;
		}

		// Pass cookies through with the request so nonces function.
		$cookies = array();

		foreach ( $_COOKIE as $name => $value ) { // WPCS: input var ok.
			if ( 'PHPSESSID' === $name ) {
				continue;
			}
			$cookies[] = new WP_Http_Cookie( array(
				'name'  => $name,
				'value' => $value,
			) );
		}

		return array(
			'timeout'   => 0.01,
			'blocking'  => false,
			'body'      => $this->data,
			'cookies'   => $cookies,
			'sslverify' => apply_filters( 'https_local_ssl_verify', false ),
		);
	}

	/**
	 * Handle
	 *
	 * Pass each queue item to the task handler, while remaining
	 * within server memory and time limit constraints.
	 */
	protected function handle() {
		$this->lock_process();

		do {
			$batch = $this->get_batch();

			if ( empty( $batch->data ) ) {
				break;
			}

			foreach ( $batch->data as $key => $value ) {
				$task = $this->task( $value );

				if ( false !== $task ) {
					$batch->data[ $key ] = $task;
				} else {
					unset( $batch->data[ $key ] );
				}

				// Update batch before sending more to prevent duplicate email possibility.
				$this->update( $batch->key, $batch->data );

				if ( $this->time_exceeded() || $this->memory_exceeded() ) {
					// Batch limits reached.
					break;
				}
			}
			if ( empty( $batch->data ) ) {
				$this->delete( $batch->key );
			}
		} while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() );

		$this->unlock_process();

		// Start next batch or complete process.
		if ( ! $this->is_queue_empty() ) {
			$this->dispatch();
		} else {
			$this->complete();
		}
	}
}
class-wc-frontend-scripts.php000064400000063643151336044770012321 0ustar00<?php
/**
 * Handle frontend scripts
 *
 * @package WooCommerce\Classes
 * @version 2.3.0
 */

use Automattic\Jetpack\Constants;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Frontend scripts class.
 */
class WC_Frontend_Scripts {

	/**
	 * Contains an array of script handles registered by WC.
	 *
	 * @var array
	 */
	private static $scripts = array();

	/**
	 * Contains an array of script handles registered by WC.
	 *
	 * @var array
	 */
	private static $styles = array();

	/**
	 * Contains an array of script handles localized by WC.
	 *
	 * @var array
	 */
	private static $wp_localize_scripts = array();

	/**
	 * Hook in methods.
	 */
	public static function init() {
		add_action( 'wp_enqueue_scripts', array( __CLASS__, 'load_scripts' ) );
		add_action( 'wp_print_scripts', array( __CLASS__, 'localize_printed_scripts' ), 5 );
		add_action( 'wp_print_footer_scripts', array( __CLASS__, 'localize_printed_scripts' ), 5 );
	}

	/**
	 * Get styles for the frontend.
	 *
	 * @return array
	 */
	public static function get_styles() {
		$version = Constants::get_constant( 'WC_VERSION' );

		return apply_filters(
			'woocommerce_enqueue_styles',
			array(
				'woocommerce-layout'      => array(
					'src'     => self::get_asset_url( 'assets/css/woocommerce-layout.css' ),
					'deps'    => '',
					'version' => $version,
					'media'   => 'all',
					'has_rtl' => true,
				),
				'woocommerce-smallscreen' => array(
					'src'     => self::get_asset_url( 'assets/css/woocommerce-smallscreen.css' ),
					'deps'    => 'woocommerce-layout',
					'version' => $version,
					'media'   => 'only screen and (max-width: ' . apply_filters( 'woocommerce_style_smallscreen_breakpoint', '768px' ) . ')',
					'has_rtl' => true,
				),
				'woocommerce-general'     => array(
					'src'     => self::get_asset_url( 'assets/css/woocommerce.css' ),
					'deps'    => '',
					'version' => $version,
					'media'   => 'all',
					'has_rtl' => true,
				),
			)
		);
	}

	/**
	 * Return asset URL.
	 *
	 * @param string $path Assets path.
	 * @return string
	 */
	private static function get_asset_url( $path ) {
		return apply_filters( 'woocommerce_get_asset_url', plugins_url( $path, WC_PLUGIN_FILE ), $path );
	}

	/**
	 * Register a script for use.
	 *
	 * @uses   wp_register_script()
	 * @param  string   $handle    Name of the script. Should be unique.
	 * @param  string   $path      Full URL of the script, or path of the script relative to the WordPress root directory.
	 * @param  string[] $deps      An array of registered script handles this script depends on.
	 * @param  string   $version   String specifying script version number, if it has one, which is added to the URL as a query string for cache busting purposes. If version is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added.
	 * @param  boolean  $in_footer Whether to enqueue the script before </body> instead of in the <head>. Default 'false'.
	 */
	private static function register_script( $handle, $path, $deps = array( 'jquery' ), $version = WC_VERSION, $in_footer = true ) {
		self::$scripts[] = $handle;
		wp_register_script( $handle, $path, $deps, $version, $in_footer );
	}

	/**
	 * Register and enqueue a script for use.
	 *
	 * @uses   wp_enqueue_script()
	 * @param  string   $handle    Name of the script. Should be unique.
	 * @param  string   $path      Full URL of the script, or path of the script relative to the WordPress root directory.
	 * @param  string[] $deps      An array of registered script handles this script depends on.
	 * @param  string   $version   String specifying script version number, if it has one, which is added to the URL as a query string for cache busting purposes. If version is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added.
	 * @param  boolean  $in_footer Whether to enqueue the script before </body> instead of in the <head>. Default 'false'.
	 */
	private static function enqueue_script( $handle, $path = '', $deps = array( 'jquery' ), $version = WC_VERSION, $in_footer = true ) {
		if ( ! in_array( $handle, self::$scripts, true ) && $path ) {
			self::register_script( $handle, $path, $deps, $version, $in_footer );
		}
		wp_enqueue_script( $handle );
	}

	/**
	 * Register a style for use.
	 *
	 * @uses   wp_register_style()
	 * @param  string   $handle  Name of the stylesheet. Should be unique.
	 * @param  string   $path    Full URL of the stylesheet, or path of the stylesheet relative to the WordPress root directory.
	 * @param  string[] $deps    An array of registered stylesheet handles this stylesheet depends on.
	 * @param  string   $version String specifying stylesheet version number, if it has one, which is added to the URL as a query string for cache busting purposes. If version is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added.
	 * @param  string   $media   The media for which this stylesheet has been defined. Accepts media types like 'all', 'print' and 'screen', or media queries like '(orientation: portrait)' and '(max-width: 640px)'.
	 * @param  boolean  $has_rtl If has RTL version to load too.
	 */
	private static function register_style( $handle, $path, $deps = array(), $version = WC_VERSION, $media = 'all', $has_rtl = false ) {
		self::$styles[] = $handle;
		wp_register_style( $handle, $path, $deps, $version, $media );

		if ( $has_rtl ) {
			wp_style_add_data( $handle, 'rtl', 'replace' );
		}
	}

	/**
	 * Register and enqueue a styles for use.
	 *
	 * @uses   wp_enqueue_style()
	 * @param  string   $handle  Name of the stylesheet. Should be unique.
	 * @param  string   $path    Full URL of the stylesheet, or path of the stylesheet relative to the WordPress root directory.
	 * @param  string[] $deps    An array of registered stylesheet handles this stylesheet depends on.
	 * @param  string   $version String specifying stylesheet version number, if it has one, which is added to the URL as a query string for cache busting purposes. If version is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added.
	 * @param  string   $media   The media for which this stylesheet has been defined. Accepts media types like 'all', 'print' and 'screen', or media queries like '(orientation: portrait)' and '(max-width: 640px)'.
	 * @param  boolean  $has_rtl If has RTL version to load too.
	 */
	private static function enqueue_style( $handle, $path = '', $deps = array(), $version = WC_VERSION, $media = 'all', $has_rtl = false ) {
		if ( ! in_array( $handle, self::$styles, true ) && $path ) {
			self::register_style( $handle, $path, $deps, $version, $media, $has_rtl );
		}
		wp_enqueue_style( $handle );
	}

	/**
	 * Register all WC scripts.
	 */
	private static function register_scripts() {
		$suffix  = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min';
		$version = Constants::get_constant( 'WC_VERSION' );

		$register_scripts = array(
			'flexslider'                 => array(
				'src'     => self::get_asset_url( 'assets/js/flexslider/jquery.flexslider' . $suffix . '.js' ),
				'deps'    => array( 'jquery' ),
				'version' => '2.7.2-wc.' . $version,
			),
			'js-cookie'                  => array(
				'src'     => self::get_asset_url( 'assets/js/js-cookie/js.cookie' . $suffix . '.js' ),
				'deps'    => array(),
				'version' => '2.1.4-wc.' . $version,
			),
			'jquery-blockui'             => array(
				'src'     => self::get_asset_url( 'assets/js/jquery-blockui/jquery.blockUI' . $suffix . '.js' ),
				'deps'    => array( 'jquery' ),
				'version' => '2.7.0-wc.' . $version,
			),
			'jquery-cookie'              => array( // deprecated.
				'src'     => self::get_asset_url( 'assets/js/jquery-cookie/jquery.cookie' . $suffix . '.js' ),
				'deps'    => array( 'jquery' ),
				'version' => '1.4.1-wc.' . $version,
			),
			'jquery-payment'             => array(
				'src'     => self::get_asset_url( 'assets/js/jquery-payment/jquery.payment' . $suffix . '.js' ),
				'deps'    => array( 'jquery' ),
				'version' => '3.0.0-wc.' . $version,
			),
			'photoswipe'                 => array(
				'src'     => self::get_asset_url( 'assets/js/photoswipe/photoswipe' . $suffix . '.js' ),
				'deps'    => array(),
				'version' => '4.1.1-wc.' . $version,
			),
			'photoswipe-ui-default'      => array(
				'src'     => self::get_asset_url( 'assets/js/photoswipe/photoswipe-ui-default' . $suffix . '.js' ),
				'deps'    => array( 'photoswipe' ),
				'version' => '4.1.1-wc.' . $version,
			),
			'prettyPhoto'                => array( // deprecated.
				'src'     => self::get_asset_url( 'assets/js/prettyPhoto/jquery.prettyPhoto' . $suffix . '.js' ),
				'deps'    => array( 'jquery' ),
				'version' => '3.1.6-wc.' . $version,
			),
			'prettyPhoto-init'           => array( // deprecated.
				'src'     => self::get_asset_url( 'assets/js/prettyPhoto/jquery.prettyPhoto.init' . $suffix . '.js' ),
				'deps'    => array( 'jquery', 'prettyPhoto' ),
				'version' => $version,
			),
			'select2'                    => array(
				'src'     => self::get_asset_url( 'assets/js/select2/select2.full' . $suffix . '.js' ),
				'deps'    => array( 'jquery' ),
				'version' => '4.0.3-wc.' . $version,
			),
			'selectWoo'                  => array(
				'src'     => self::get_asset_url( 'assets/js/selectWoo/selectWoo.full' . $suffix . '.js' ),
				'deps'    => array( 'jquery' ),
				'version' => '1.0.9-wc.' . $version,
			),
			'wc-address-i18n'            => array(
				'src'     => self::get_asset_url( 'assets/js/frontend/address-i18n' . $suffix . '.js' ),
				'deps'    => array( 'jquery', 'wc-country-select' ),
				'version' => $version,
			),
			'wc-add-payment-method'      => array(
				'src'     => self::get_asset_url( 'assets/js/frontend/add-payment-method' . $suffix . '.js' ),
				'deps'    => array( 'jquery', 'woocommerce' ),
				'version' => $version,
			),
			'wc-cart'                    => array(
				'src'     => self::get_asset_url( 'assets/js/frontend/cart' . $suffix . '.js' ),
				'deps'    => array( 'jquery', 'woocommerce', 'wc-country-select', 'wc-address-i18n' ),
				'version' => $version,
			),
			'wc-cart-fragments'          => array(
				'src'     => self::get_asset_url( 'assets/js/frontend/cart-fragments' . $suffix . '.js' ),
				'deps'    => array( 'jquery', 'js-cookie' ),
				'version' => $version,
			),
			'wc-checkout'                => array(
				'src'     => self::get_asset_url( 'assets/js/frontend/checkout' . $suffix . '.js' ),
				'deps'    => array( 'jquery', 'woocommerce', 'wc-country-select', 'wc-address-i18n' ),
				'version' => $version,
			),
			'wc-country-select'          => array(
				'src'     => self::get_asset_url( 'assets/js/frontend/country-select' . $suffix . '.js' ),
				'deps'    => array( 'jquery' ),
				'version' => $version,
			),
			'wc-credit-card-form'        => array(
				'src'     => self::get_asset_url( 'assets/js/frontend/credit-card-form' . $suffix . '.js' ),
				'deps'    => array( 'jquery', 'jquery-payment' ),
				'version' => $version,
			),
			'wc-add-to-cart'             => array(
				'src'     => self::get_asset_url( 'assets/js/frontend/add-to-cart' . $suffix . '.js' ),
				'deps'    => array( 'jquery', 'jquery-blockui' ),
				'version' => $version,
			),
			'wc-add-to-cart-variation'   => array(
				'src'     => self::get_asset_url( 'assets/js/frontend/add-to-cart-variation' . $suffix . '.js' ),
				'deps'    => array( 'jquery', 'wp-util', 'jquery-blockui' ),
				'version' => $version,
			),
			'wc-geolocation'             => array(
				'src'     => self::get_asset_url( 'assets/js/frontend/geolocation' . $suffix . '.js' ),
				'deps'    => array( 'jquery' ),
				'version' => $version,
			),
			'wc-lost-password'           => array(
				'src'     => self::get_asset_url( 'assets/js/frontend/lost-password' . $suffix . '.js' ),
				'deps'    => array( 'jquery', 'woocommerce' ),
				'version' => $version,
			),
			'wc-password-strength-meter' => array(
				'src'     => self::get_asset_url( 'assets/js/frontend/password-strength-meter' . $suffix . '.js' ),
				'deps'    => array( 'jquery', 'password-strength-meter' ),
				'version' => $version,
			),
			'wc-single-product'          => array(
				'src'     => self::get_asset_url( 'assets/js/frontend/single-product' . $suffix . '.js' ),
				'deps'    => array( 'jquery' ),
				'version' => $version,
			),
			'woocommerce'                => array(
				'src'     => self::get_asset_url( 'assets/js/frontend/woocommerce' . $suffix . '.js' ),
				'deps'    => array( 'jquery', 'jquery-blockui', 'js-cookie' ),
				'version' => $version,
			),
			'zoom'                       => array(
				'src'     => self::get_asset_url( 'assets/js/zoom/jquery.zoom' . $suffix . '.js' ),
				'deps'    => array( 'jquery' ),
				'version' => '1.7.21-wc.' . $version,
			),
		);
		foreach ( $register_scripts as $name => $props ) {
			self::register_script( $name, $props['src'], $props['deps'], $props['version'] );
		}
	}

	/**
	 * Register all WC styles.
	 */
	private static function register_styles() {
		$version = Constants::get_constant( 'WC_VERSION' );

		$register_styles = array(
			'photoswipe'                  => array(
				'src'     => self::get_asset_url( 'assets/css/photoswipe/photoswipe.min.css' ),
				'deps'    => array(),
				'version' => $version,
				'has_rtl' => false,
			),
			'photoswipe-default-skin'     => array(
				'src'     => self::get_asset_url( 'assets/css/photoswipe/default-skin/default-skin.min.css' ),
				'deps'    => array( 'photoswipe' ),
				'version' => $version,
				'has_rtl' => false,
			),
			'select2'                     => array(
				'src'     => self::get_asset_url( 'assets/css/select2.css' ),
				'deps'    => array(),
				'version' => $version,
				'has_rtl' => false,
			),
			'woocommerce_prettyPhoto_css' => array( // deprecated.
				'src'     => self::get_asset_url( 'assets/css/prettyPhoto.css' ),
				'deps'    => array(),
				'version' => $version,
				'has_rtl' => true,
			),
		);
		foreach ( $register_styles as $name => $props ) {
			self::register_style( $name, $props['src'], $props['deps'], $props['version'], 'all', $props['has_rtl'] );
		}
	}

	/**
	 * Register/queue frontend scripts.
	 */
	public static function load_scripts() {
		global $post;

		if ( ! did_action( 'before_woocommerce_init' ) ) {
			return;
		}

		self::register_scripts();
		self::register_styles();

		if ( 'yes' === get_option( 'woocommerce_enable_ajax_add_to_cart' ) ) {
			self::enqueue_script( 'wc-add-to-cart' );
		}
		if ( is_cart() ) {
			self::enqueue_script( 'wc-cart' );
		}
		if ( is_cart() || is_checkout() || is_account_page() ) {
			self::enqueue_script( 'selectWoo' );
			self::enqueue_style( 'select2' );

			// Password strength meter. Load in checkout, account login and edit account page.
			if ( ( 'no' === get_option( 'woocommerce_registration_generate_password' ) && ! is_user_logged_in() ) || is_edit_account_page() || is_lost_password_page() ) {
				self::enqueue_script( 'wc-password-strength-meter' );
			}
		}
		if ( is_checkout() ) {
			self::enqueue_script( 'wc-checkout' );
		}
		if ( is_add_payment_method_page() ) {
			self::enqueue_script( 'wc-add-payment-method' );
		}
		if ( is_lost_password_page() ) {
			self::enqueue_script( 'wc-lost-password' );
		}

		// Load gallery scripts on product pages only if supported.
		if ( is_product() || ( ! empty( $post->post_content ) && strstr( $post->post_content, '[product_page' ) ) ) {
			if ( current_theme_supports( 'wc-product-gallery-zoom' ) ) {
				self::enqueue_script( 'zoom' );
			}
			if ( current_theme_supports( 'wc-product-gallery-slider' ) ) {
				self::enqueue_script( 'flexslider' );
			}
			if ( current_theme_supports( 'wc-product-gallery-lightbox' ) ) {
				self::enqueue_script( 'photoswipe-ui-default' );
				self::enqueue_style( 'photoswipe-default-skin' );
				add_action( 'wp_footer', 'woocommerce_photoswipe' );
			}
			self::enqueue_script( 'wc-single-product' );
		}

		// Only enqueue the geolocation script if the Default Current Address is set to "Geolocate
		// (with Page Caching Support) and outside of the cart, checkout, account and customizer preview.
		if (
			'geolocation_ajax' === get_option( 'woocommerce_default_customer_address' )
			&& ! ( is_cart() || is_account_page() || is_checkout() || is_customize_preview() )
		) {
			$ua = strtolower( wc_get_user_agent() ); // Exclude common bots from geolocation by user agent.

			if ( ! strstr( $ua, 'bot' ) && ! strstr( $ua, 'spider' ) && ! strstr( $ua, 'crawl' ) ) {
				self::enqueue_script( 'wc-geolocation' );
			}
		}

		// Global frontend scripts.
		self::enqueue_script( 'woocommerce' );
		self::enqueue_script( 'wc-cart-fragments' );

		// CSS Styles.
		$enqueue_styles = self::get_styles();
		if ( $enqueue_styles ) {
			foreach ( $enqueue_styles as $handle => $args ) {
				if ( ! isset( $args['has_rtl'] ) ) {
					$args['has_rtl'] = false;
				}

				self::enqueue_style( $handle, $args['src'], $args['deps'], $args['version'], $args['media'], $args['has_rtl'] );
			}
		}

		// Placeholder style.
		wp_register_style( 'woocommerce-inline', false ); // phpcs:ignore
		wp_enqueue_style( 'woocommerce-inline' );

		if ( true === wc_string_to_bool( get_option( 'woocommerce_checkout_highlight_required_fields', 'yes' ) ) ) {
			wp_add_inline_style( 'woocommerce-inline', '.woocommerce form .form-row .required { visibility: visible; }' );
		} else {
			wp_add_inline_style( 'woocommerce-inline', '.woocommerce form .form-row .required { visibility: hidden; }' );
		}
	}

	/**
	 * Localize a WC script once.
	 *
	 * @since 2.3.0 this needs less wp_script_is() calls due to https://core.trac.wordpress.org/ticket/28404 being added in WP 4.0.
	 * @param string $handle Script handle the data will be attached to.
	 */
	private static function localize_script( $handle ) {
		if ( ! in_array( $handle, self::$wp_localize_scripts, true ) && wp_script_is( $handle ) ) {
			$data = self::get_script_data( $handle );

			if ( ! $data ) {
				return;
			}

			$name                        = str_replace( '-', '_', $handle ) . '_params';
			self::$wp_localize_scripts[] = $handle;
			wp_localize_script( $handle, $name, apply_filters( $name, $data ) );
		}
	}

	/**
	 * Return data for script handles.
	 *
	 * @param  string $handle Script handle the data will be attached to.
	 * @return array|bool
	 */
	private static function get_script_data( $handle ) {
		global $wp;

		switch ( $handle ) {
			case 'woocommerce':
				$params = array(
					'ajax_url'    => WC()->ajax_url(),
					'wc_ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ),
				);
				break;
			case 'wc-geolocation':
				$params = array(
					'wc_ajax_url'  => WC_AJAX::get_endpoint( '%%endpoint%%' ),
					'home_url'     => remove_query_arg( 'lang', home_url() ), // FIX for WPML compatibility.
				);
				break;
			case 'wc-single-product':
				$params = array(
					'i18n_required_rating_text' => esc_attr__( 'Please select a rating', 'woocommerce' ),
					'review_rating_required'    => wc_review_ratings_required() ? 'yes' : 'no',
					'flexslider'                => apply_filters(
						'woocommerce_single_product_carousel_options',
						array(
							'rtl'            => is_rtl(),
							'animation'      => 'slide',
							'smoothHeight'   => true,
							'directionNav'   => false,
							'controlNav'     => 'thumbnails',
							'slideshow'      => false,
							'animationSpeed' => 500,
							'animationLoop'  => false, // Breaks photoswipe pagination if true.
							'allowOneSlide'  => false,
						)
					),
					'zoom_enabled'              => apply_filters( 'woocommerce_single_product_zoom_enabled', get_theme_support( 'wc-product-gallery-zoom' ) ),
					'zoom_options'              => apply_filters( 'woocommerce_single_product_zoom_options', array() ),
					'photoswipe_enabled'        => apply_filters( 'woocommerce_single_product_photoswipe_enabled', get_theme_support( 'wc-product-gallery-lightbox' ) ),
					'photoswipe_options'        => apply_filters(
						'woocommerce_single_product_photoswipe_options',
						array(
							'shareEl'               => false,
							'closeOnScroll'         => false,
							'history'               => false,
							'hideAnimationDuration' => 0,
							'showAnimationDuration' => 0,
						)
					),
					'flexslider_enabled'        => apply_filters( 'woocommerce_single_product_flexslider_enabled', get_theme_support( 'wc-product-gallery-slider' ) ),
				);
				break;
			case 'wc-checkout':
				$params = array(
					'ajax_url'                  => WC()->ajax_url(),
					'wc_ajax_url'               => WC_AJAX::get_endpoint( '%%endpoint%%' ),
					'update_order_review_nonce' => wp_create_nonce( 'update-order-review' ),
					'apply_coupon_nonce'        => wp_create_nonce( 'apply-coupon' ),
					'remove_coupon_nonce'       => wp_create_nonce( 'remove-coupon' ),
					'option_guest_checkout'     => get_option( 'woocommerce_enable_guest_checkout' ),
					'checkout_url'              => WC_AJAX::get_endpoint( 'checkout' ),
					'is_checkout'               => is_checkout() && empty( $wp->query_vars['order-pay'] ) && ! isset( $wp->query_vars['order-received'] ) ? 1 : 0,
					'debug_mode'                => Constants::is_true( 'WP_DEBUG' ),
					'i18n_checkout_error'       => esc_attr__( 'Error processing checkout. Please try again.', 'woocommerce' ),
				);
				break;
			case 'wc-address-i18n':
				$params = array(
					'locale'             => wp_json_encode( WC()->countries->get_country_locale() ),
					'locale_fields'      => wp_json_encode( WC()->countries->get_country_locale_field_selectors() ),
					'i18n_required_text' => esc_attr__( 'required', 'woocommerce' ),
					'i18n_optional_text' => esc_html__( 'optional', 'woocommerce' ),
				);
				break;
			case 'wc-cart':
				$params = array(
					'ajax_url'                     => WC()->ajax_url(),
					'wc_ajax_url'                  => WC_AJAX::get_endpoint( '%%endpoint%%' ),
					'update_shipping_method_nonce' => wp_create_nonce( 'update-shipping-method' ),
					'apply_coupon_nonce'           => wp_create_nonce( 'apply-coupon' ),
					'remove_coupon_nonce'          => wp_create_nonce( 'remove-coupon' ),
				);
				break;
			case 'wc-cart-fragments':
				$params = array(
					'ajax_url'        => WC()->ajax_url(),
					'wc_ajax_url'     => WC_AJAX::get_endpoint( '%%endpoint%%' ),
					'cart_hash_key'   => apply_filters( 'woocommerce_cart_hash_key', 'wc_cart_hash_' . md5( get_current_blog_id() . '_' . get_site_url( get_current_blog_id(), '/' ) . get_template() ) ),
					'fragment_name'   => apply_filters( 'woocommerce_cart_fragment_name', 'wc_fragments_' . md5( get_current_blog_id() . '_' . get_site_url( get_current_blog_id(), '/' ) . get_template() ) ),
					'request_timeout' => 5000,
				);
				break;
			case 'wc-add-to-cart':
				$params = array(
					'ajax_url'                => WC()->ajax_url(),
					'wc_ajax_url'             => WC_AJAX::get_endpoint( '%%endpoint%%' ),
					'i18n_view_cart'          => esc_attr__( 'View cart', 'woocommerce' ),
					'cart_url'                => apply_filters( 'woocommerce_add_to_cart_redirect', wc_get_cart_url(), null ),
					'is_cart'                 => is_cart(),
					'cart_redirect_after_add' => get_option( 'woocommerce_cart_redirect_after_add' ),
				);
				break;
			case 'wc-add-to-cart-variation':
				// We also need the wp.template for this script :).
				wc_get_template( 'single-product/add-to-cart/variation.php' );

				$params = array(
					'wc_ajax_url'                      => WC_AJAX::get_endpoint( '%%endpoint%%' ),
					'i18n_no_matching_variations_text' => esc_attr__( 'Sorry, no products matched your selection. Please choose a different combination.', 'woocommerce' ),
					'i18n_make_a_selection_text'       => esc_attr__( 'Please select some product options before adding this product to your cart.', 'woocommerce' ),
					'i18n_unavailable_text'            => esc_attr__( 'Sorry, this product is unavailable. Please choose a different combination.', 'woocommerce' ),
				);
				break;
			case 'wc-country-select':
				$params = array(
					'countries'                 => wp_json_encode( array_merge( WC()->countries->get_allowed_country_states(), WC()->countries->get_shipping_country_states() ) ),
					'i18n_select_state_text'    => esc_attr__( 'Select an option&hellip;', 'woocommerce' ),
					'i18n_no_matches'           => _x( 'No matches found', 'enhanced select', 'woocommerce' ),
					'i18n_ajax_error'           => _x( 'Loading failed', 'enhanced select', 'woocommerce' ),
					'i18n_input_too_short_1'    => _x( 'Please enter 1 or more characters', 'enhanced select', 'woocommerce' ),
					'i18n_input_too_short_n'    => _x( 'Please enter %qty% or more characters', 'enhanced select', 'woocommerce' ),
					'i18n_input_too_long_1'     => _x( 'Please delete 1 character', 'enhanced select', 'woocommerce' ),
					'i18n_input_too_long_n'     => _x( 'Please delete %qty% characters', 'enhanced select', 'woocommerce' ),
					'i18n_selection_too_long_1' => _x( 'You can only select 1 item', 'enhanced select', 'woocommerce' ),
					'i18n_selection_too_long_n' => _x( 'You can only select %qty% items', 'enhanced select', 'woocommerce' ),
					'i18n_load_more'            => _x( 'Loading more results&hellip;', 'enhanced select', 'woocommerce' ),
					'i18n_searching'            => _x( 'Searching&hellip;', 'enhanced select', 'woocommerce' ),
				);
				break;
			case 'wc-password-strength-meter':
				$params = array(
					'min_password_strength' => apply_filters( 'woocommerce_min_password_strength', 3 ),
					'stop_checkout'         => apply_filters( 'woocommerce_enforce_password_strength_meter_on_checkout', false ),
					'i18n_password_error'   => esc_attr__( 'Please enter a stronger password.', 'woocommerce' ),
					'i18n_password_hint'    => esc_attr( wp_get_password_hint() ),
				);
				break;
			default:
				$params = false;
		}

		$params = apply_filters_deprecated( $handle . '_params', array( $params ), '3.0.0', 'woocommerce_get_script_data' );

		return apply_filters( 'woocommerce_get_script_data', $params, $handle );
	}

	/**
	 * Localize scripts only when enqueued.
	 */
	public static function localize_printed_scripts() {
		foreach ( self::$scripts as $handle ) {
			self::localize_script( $handle );
		}
	}
}

WC_Frontend_Scripts::init();
shipping/legacy-local-delivery/class-wc-shipping-legacy-local-delivery.php000064400000013371151336044770023000 0ustar00<?php
/**
 * Class WC_Shipping_Legacy_Local_Delivery file.
 *
 * @package WooCommerce\Shipping
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Local Delivery Shipping Method.
 *
 * This class is here for backwards compatibility for methods existing before zones existed.
 *
 * @deprecated  2.6.0
 * @version     2.3.0
 * @package     WooCommerce\Classes\Shipping
 */
class WC_Shipping_Legacy_Local_Delivery extends WC_Shipping_Local_Pickup {

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->id           = 'legacy_local_delivery';
		$this->method_title = __( 'Local delivery (legacy)', 'woocommerce' );
		/* translators: %s: Admin shipping settings URL */
		$this->method_description = '<strong>' . sprintf( __( 'This method is deprecated in 2.6.0 and will be removed in future versions - we recommend disabling it and instead setting up a new rate within your <a href="%s">Shipping zones</a>.', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=shipping' ) ) . '</strong>';
		$this->init();
	}

	/**
	 * Process and redirect if disabled.
	 */
	public function process_admin_options() {
		parent::process_admin_options();

		if ( 'no' === $this->settings['enabled'] ) {
			wp_redirect( admin_url( 'admin.php?page=wc-settings&tab=shipping&section=options' ) );
			exit;
		}
	}

	/**
	 * Return the name of the option in the WP DB.
	 *
	 * @since 2.6.0
	 * @return string
	 */
	public function get_option_key() {
		return $this->plugin_id . 'local_delivery_settings';
	}

	/**
	 * Init function.
	 */
	public function init() {

		// Load the settings.
		$this->init_form_fields();
		$this->init_settings();

		// Define user set variables.
		$this->title        = $this->get_option( 'title' );
		$this->type         = $this->get_option( 'type' );
		$this->fee          = $this->get_option( 'fee' );
		$this->type         = $this->get_option( 'type' );
		$this->codes        = $this->get_option( 'codes' );
		$this->availability = $this->get_option( 'availability' );
		$this->countries    = $this->get_option( 'countries' );

		add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) );
	}

	/**
	 * Calculate_shipping function.
	 *
	 * @param array $package (default: array()).
	 */
	public function calculate_shipping( $package = array() ) {
		$shipping_total = 0;

		switch ( $this->type ) {
			case 'fixed':
				$shipping_total = $this->fee;
				break;
			case 'percent':
				$shipping_total = $package['contents_cost'] * ( $this->fee / 100 );
				break;
			case 'product':
				foreach ( $package['contents'] as $item_id => $values ) {
					if ( $values['quantity'] > 0 && $values['data']->needs_shipping() ) {
						$shipping_total += $this->fee * $values['quantity'];
					}
				}
				break;
		}

		$rate = array(
			'id'      => $this->id,
			'label'   => $this->title,
			'cost'    => $shipping_total,
			'package' => $package,
		);

		$this->add_rate( $rate );
	}

	/**
	 * Init form fields.
	 */
	public function init_form_fields() {
		$this->form_fields = array(
			'enabled'      => array(
				'title'   => __( 'Enable', 'woocommerce' ),
				'type'    => 'checkbox',
				'label'   => __( 'Once disabled, this legacy method will no longer be available.', 'woocommerce' ),
				'default' => 'no',
			),
			'title'        => array(
				'title'       => __( 'Title', 'woocommerce' ),
				'type'        => 'text',
				'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ),
				'default'     => __( 'Local delivery', 'woocommerce' ),
				'desc_tip'    => true,
			),
			'type'         => array(
				'title'       => __( 'Fee type', 'woocommerce' ),
				'type'        => 'select',
				'class'       => 'wc-enhanced-select',
				'description' => __( 'How to calculate delivery charges', 'woocommerce' ),
				'default'     => 'fixed',
				'options'     => array(
					'fixed'   => __( 'Fixed amount', 'woocommerce' ),
					'percent' => __( 'Percentage of cart total', 'woocommerce' ),
					'product' => __( 'Fixed amount per product', 'woocommerce' ),
				),
				'desc_tip'    => true,
			),
			'fee'          => array(
				'title'       => __( 'Delivery fee', 'woocommerce' ),
				'type'        => 'price',
				'description' => __( 'What fee do you want to charge for local delivery, disregarded if you choose free. Leave blank to disable.', 'woocommerce' ),
				'default'     => '',
				'desc_tip'    => true,
				'placeholder' => wc_format_localized_price( 0 ),
			),
			'codes'        => array(
				'title'       => __( 'Allowed ZIP/post codes', 'woocommerce' ),
				'type'        => 'text',
				'desc_tip'    => __( 'What ZIP/post codes are available for local delivery?', 'woocommerce' ),
				'default'     => '',
				'description' => __( 'Separate codes with a comma. Accepts wildcards, e.g. <code>P*</code> will match a postcode of PE30. Also accepts a pattern, e.g. <code>NG1___</code> would match NG1 1AA but not NG10 1AA', 'woocommerce' ),
				'placeholder' => 'e.g. 12345, 56789',
			),
			'availability' => array(
				'title'   => __( 'Method availability', 'woocommerce' ),
				'type'    => 'select',
				'default' => 'all',
				'class'   => 'availability wc-enhanced-select',
				'options' => array(
					'all'      => __( 'All allowed countries', 'woocommerce' ),
					'specific' => __( 'Specific Countries', 'woocommerce' ),
				),
			),
			'countries'    => array(
				'title'             => __( 'Specific countries', 'woocommerce' ),
				'type'              => 'multiselect',
				'class'             => 'wc-enhanced-select',
				'css'               => 'width: 400px;',
				'default'           => '',
				'options'           => WC()->countries->get_shipping_countries(),
				'custom_attributes' => array(
					'data-placeholder' => __( 'Select some countries', 'woocommerce' ),
				),
			),
		);
	}
}
shipping/local-pickup/class-wc-shipping-local-pickup.php000064400000005367151336044770017422 0ustar00<?php
/**
 * Class WC_Shipping_Local_Pickup file.
 *
 * @package WooCommerce\Shipping
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Local Pickup Shipping Method.
 *
 * A simple shipping method allowing free pickup as a shipping method.
 *
 * @class       WC_Shipping_Local_Pickup
 * @version     2.6.0
 * @package     WooCommerce\Classes\Shipping
 */
class WC_Shipping_Local_Pickup extends WC_Shipping_Method {

	/**
	 * Constructor.
	 *
	 * @param int $instance_id Instance ID.
	 */
	public function __construct( $instance_id = 0 ) {
		$this->id                 = 'local_pickup';
		$this->instance_id        = absint( $instance_id );
		$this->method_title       = __( 'Local pickup', 'woocommerce' );
		$this->method_description = __( 'Allow customers to pick up orders themselves. By default, when using local pickup store base taxes will apply regardless of customer address.', 'woocommerce' );
		$this->supports           = array(
			'shipping-zones',
			'instance-settings',
			'instance-settings-modal',
		);
		$this->init();
	}

	/**
	 * Initialize local pickup.
	 */
	public function init() {

		// Load the settings.
		$this->init_form_fields();
		$this->init_settings();

		// Define user set variables.
		$this->title      = $this->get_option( 'title' );
		$this->tax_status = $this->get_option( 'tax_status' );
		$this->cost       = $this->get_option( 'cost' );

		// Actions.
		add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) );
	}

	/**
	 * Calculate local pickup shipping.
	 *
	 * @param array $package Package information.
	 */
	public function calculate_shipping( $package = array() ) {
		$this->add_rate(
			array(
				'label'   => $this->title,
				'package' => $package,
				'cost'    => $this->cost,
			)
		);
	}

	/**
	 * Init form fields.
	 */
	public function init_form_fields() {
		$this->instance_form_fields = array(
			'title'      => array(
				'title'       => __( 'Title', 'woocommerce' ),
				'type'        => 'text',
				'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ),
				'default'     => __( 'Local pickup', 'woocommerce' ),
				'desc_tip'    => true,
			),
			'tax_status' => array(
				'title'   => __( 'Tax status', 'woocommerce' ),
				'type'    => 'select',
				'class'   => 'wc-enhanced-select',
				'default' => 'taxable',
				'options' => array(
					'taxable' => __( 'Taxable', 'woocommerce' ),
					'none'    => _x( 'None', 'Tax status', 'woocommerce' ),
				),
			),
			'cost'       => array(
				'title'       => __( 'Cost', 'woocommerce' ),
				'type'        => 'text',
				'placeholder' => '0',
				'description' => __( 'Optional cost for local pickup.', 'woocommerce' ),
				'default'     => '',
				'desc_tip'    => true,
			),
		);
	}
}
shipping/free-shipping/class-wc-shipping-free-shipping.php000064400000015710151336044770017745 0ustar00<?php
/**
 * Class WC_Shipping_Free_Shipping file.
 *
 * @package WooCommerce\Shipping
 */

use Automattic\WooCommerce\Utilities\NumberUtil;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Free Shipping Method.
 *
 * A simple shipping method for free shipping.
 *
 * @class   WC_Shipping_Free_Shipping
 * @version 2.6.0
 * @package WooCommerce\Classes\Shipping
 */
class WC_Shipping_Free_Shipping extends WC_Shipping_Method {

	/**
	 * Min amount to be valid.
	 *
	 * @var integer
	 */
	public $min_amount = 0;

	/**
	 * Requires option.
	 *
	 * @var string
	 */
	public $requires = '';

	/**
	 * Constructor.
	 *
	 * @param int $instance_id Shipping method instance.
	 */
	public function __construct( $instance_id = 0 ) {
		$this->id                 = 'free_shipping';
		$this->instance_id        = absint( $instance_id );
		$this->method_title       = __( 'Free shipping', 'woocommerce' );
		$this->method_description = __( 'Free shipping is a special method which can be triggered with coupons and minimum spends.', 'woocommerce' );
		$this->supports           = array(
			'shipping-zones',
			'instance-settings',
			'instance-settings-modal',
		);

		$this->init();
	}

	/**
	 * Initialize free shipping.
	 */
	public function init() {
		// Load the settings.
		$this->init_form_fields();
		$this->init_settings();

		// Define user set variables.
		$this->title            = $this->get_option( 'title' );
		$this->min_amount       = $this->get_option( 'min_amount', 0 );
		$this->requires         = $this->get_option( 'requires' );
		$this->ignore_discounts = $this->get_option( 'ignore_discounts' );

		// Actions.
		add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) );
		add_action( 'admin_footer', array( 'WC_Shipping_Free_Shipping', 'enqueue_admin_js' ), 10 ); // Priority needs to be higher than wc_print_js (25).
	}

	/**
	 * Init form fields.
	 */
	public function init_form_fields() {
		$this->instance_form_fields = array(
			'title'            => array(
				'title'       => __( 'Title', 'woocommerce' ),
				'type'        => 'text',
				'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ),
				'default'     => $this->method_title,
				'desc_tip'    => true,
			),
			'requires'         => array(
				'title'   => __( 'Free shipping requires...', 'woocommerce' ),
				'type'    => 'select',
				'class'   => 'wc-enhanced-select',
				'default' => '',
				'options' => array(
					''           => __( 'N/A', 'woocommerce' ),
					'coupon'     => __( 'A valid free shipping coupon', 'woocommerce' ),
					'min_amount' => __( 'A minimum order amount', 'woocommerce' ),
					'either'     => __( 'A minimum order amount OR a coupon', 'woocommerce' ),
					'both'       => __( 'A minimum order amount AND a coupon', 'woocommerce' ),
				),
			),
			'min_amount'       => array(
				'title'       => __( 'Minimum order amount', 'woocommerce' ),
				'type'        => 'price',
				'placeholder' => wc_format_localized_price( 0 ),
				'description' => __( 'Users will need to spend this amount to get free shipping (if enabled above).', 'woocommerce' ),
				'default'     => '0',
				'desc_tip'    => true,
			),
			'ignore_discounts' => array(
				'title'       => __( 'Coupons discounts', 'woocommerce' ),
				'label'       => __( 'Apply minimum order rule before coupon discount', 'woocommerce' ),
				'type'        => 'checkbox',
				'description' => __( 'If checked, free shipping would be available based on pre-discount order amount.', 'woocommerce' ),
				'default'     => 'no',
				'desc_tip'    => true,
			),
		);
	}

	/**
	 * Get setting form fields for instances of this shipping method within zones.
	 *
	 * @return array
	 */
	public function get_instance_form_fields() {
		return parent::get_instance_form_fields();
	}

	/**
	 * See if free shipping is available based on the package and cart.
	 *
	 * @param array $package Shipping package.
	 * @return bool
	 */
	public function is_available( $package ) {
		$has_coupon         = false;
		$has_met_min_amount = false;

		if ( in_array( $this->requires, array( 'coupon', 'either', 'both' ), true ) ) {
			$coupons = WC()->cart->get_coupons();

			if ( $coupons ) {
				foreach ( $coupons as $code => $coupon ) {
					if ( $coupon->is_valid() && $coupon->get_free_shipping() ) {
						$has_coupon = true;
						break;
					}
				}
			}
		}

		if ( in_array( $this->requires, array( 'min_amount', 'either', 'both' ), true ) ) {
			$total = WC()->cart->get_displayed_subtotal();

			if ( WC()->cart->display_prices_including_tax() ) {
				$total = $total - WC()->cart->get_discount_tax();
			}

			if ( 'no' === $this->ignore_discounts ) {
				$total = $total - WC()->cart->get_discount_total();
			}

			$total = NumberUtil::round( $total, wc_get_price_decimals() );

			if ( $total >= $this->min_amount ) {
				$has_met_min_amount = true;
			}
		}

		switch ( $this->requires ) {
			case 'min_amount':
				$is_available = $has_met_min_amount;
				break;
			case 'coupon':
				$is_available = $has_coupon;
				break;
			case 'both':
				$is_available = $has_met_min_amount && $has_coupon;
				break;
			case 'either':
				$is_available = $has_met_min_amount || $has_coupon;
				break;
			default:
				$is_available = true;
				break;
		}

		return apply_filters( 'woocommerce_shipping_' . $this->id . '_is_available', $is_available, $package, $this );
	}

	/**
	 * Called to calculate shipping rates for this method. Rates can be added using the add_rate() method.
	 *
	 * @uses WC_Shipping_Method::add_rate()
	 *
	 * @param array $package Shipping package.
	 */
	public function calculate_shipping( $package = array() ) {
		$this->add_rate(
			array(
				'label'   => $this->title,
				'cost'    => 0,
				'taxes'   => false,
				'package' => $package,
			)
		);
	}

	/**
	 * Enqueue JS to handle free shipping options.
	 *
	 * Static so that's enqueued only once.
	 */
	public static function enqueue_admin_js() {
		wc_enqueue_js(
			"jQuery( function( $ ) {
				function wcFreeShippingShowHideMinAmountField( el ) {
					var form = $( el ).closest( 'form' );
					var minAmountField = $( '#woocommerce_free_shipping_min_amount', form ).closest( 'tr' );
					var ignoreDiscountField = $( '#woocommerce_free_shipping_ignore_discounts', form ).closest( 'tr' );
					if ( 'coupon' === $( el ).val() || '' === $( el ).val() ) {
						minAmountField.hide();
						ignoreDiscountField.hide();
					} else {
						minAmountField.show();
						ignoreDiscountField.show();
					}
				}

				$( document.body ).on( 'change', '#woocommerce_free_shipping_requires', function() {
					wcFreeShippingShowHideMinAmountField( this );
				});

				// Change while load.
				$( '#woocommerce_free_shipping_requires' ).trigger( 'change' );
				$( document.body ).on( 'wc_backbone_modal_loaded', function( evt, target ) {
					if ( 'wc-modal-shipping-method-settings' === target ) {
						wcFreeShippingShowHideMinAmountField( $( '#wc-backbone-modal-dialog #woocommerce_free_shipping_requires', evt.currentTarget ) );
					}
				} );
			});"
		);
	}
}
shipping/legacy-free-shipping/class-wc-shipping-legacy-free-shipping.php000064400000016051151336044770022450 0ustar00<?php
/**
 * Class WC_Shipping_Legacy_Free_Shipping file.
 *
 * @package WooCommerce\Shipping
 */

use Automattic\WooCommerce\Utilities\NumberUtil;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Free Shipping Method.
 *
 * This class is here for backwards compatibility for methods existing before zones existed.
 *
 * @deprecated  2.6.0
 * @version 2.4.0
 * @package WooCommerce\Classes\Shipping
 */
class WC_Shipping_Legacy_Free_Shipping extends WC_Shipping_Method {

	/**
	 * Min amount to be valid.
	 *
	 * @var float
	 */
	public $min_amount;

	/**
	 * Requires option.
	 *
	 * @var string
	 */
	public $requires;

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->id           = 'legacy_free_shipping';
		$this->method_title = __( 'Free shipping (legacy)', 'woocommerce' );
		/* translators: %s: Admin shipping settings URL */
		$this->method_description = '<strong>' . sprintf( __( 'This method is deprecated in 2.6.0 and will be removed in future versions - we recommend disabling it and instead setting up a new rate within your <a href="%s">Shipping zones</a>.', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=shipping' ) ) . '</strong>';
		$this->init();
	}

	/**
	 * Process and redirect if disabled.
	 */
	public function process_admin_options() {
		parent::process_admin_options();

		if ( 'no' === $this->settings['enabled'] ) {
			wp_redirect( admin_url( 'admin.php?page=wc-settings&tab=shipping&section=options' ) );
			exit;
		}
	}

	/**
	 * Return the name of the option in the WP DB.
	 *
	 * @since 2.6.0
	 * @return string
	 */
	public function get_option_key() {
		return $this->plugin_id . 'free_shipping_settings';
	}

	/**
	 * Init function.
	 */
	public function init() {

		// Load the settings.
		$this->init_form_fields();
		$this->init_settings();

		// Define user set variables.
		$this->enabled      = $this->get_option( 'enabled' );
		$this->title        = $this->get_option( 'title' );
		$this->min_amount   = $this->get_option( 'min_amount', 0 );
		$this->availability = $this->get_option( 'availability' );
		$this->countries    = $this->get_option( 'countries' );
		$this->requires     = $this->get_option( 'requires' );

		// Actions.
		add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) );
	}

	/**
	 * Initialise Gateway Settings Form Fields.
	 */
	public function init_form_fields() {
		$this->form_fields = array(
			'enabled'      => array(
				'title'   => __( 'Enable/Disable', 'woocommerce' ),
				'type'    => 'checkbox',
				'label'   => __( 'Once disabled, this legacy method will no longer be available.', 'woocommerce' ),
				'default' => 'no',
			),
			'title'        => array(
				'title'       => __( 'Method title', 'woocommerce' ),
				'type'        => 'text',
				'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ),
				'default'     => __( 'Free Shipping', 'woocommerce' ),
				'desc_tip'    => true,
			),
			'availability' => array(
				'title'   => __( 'Method availability', 'woocommerce' ),
				'type'    => 'select',
				'default' => 'all',
				'class'   => 'availability wc-enhanced-select',
				'options' => array(
					'all'      => __( 'All allowed countries', 'woocommerce' ),
					'specific' => __( 'Specific Countries', 'woocommerce' ),
				),
			),
			'countries'    => array(
				'title'             => __( 'Specific countries', 'woocommerce' ),
				'type'              => 'multiselect',
				'class'             => 'wc-enhanced-select',
				'css'               => 'width: 400px;',
				'default'           => '',
				'options'           => WC()->countries->get_shipping_countries(),
				'custom_attributes' => array(
					'data-placeholder' => __( 'Select some countries', 'woocommerce' ),
				),
			),
			'requires'     => array(
				'title'   => __( 'Free shipping requires...', 'woocommerce' ),
				'type'    => 'select',
				'class'   => 'wc-enhanced-select',
				'default' => '',
				'options' => array(
					''           => __( 'N/A', 'woocommerce' ),
					'coupon'     => __( 'A valid free shipping coupon', 'woocommerce' ),
					'min_amount' => __( 'A minimum order amount', 'woocommerce' ),
					'either'     => __( 'A minimum order amount OR a coupon', 'woocommerce' ),
					'both'       => __( 'A minimum order amount AND a coupon', 'woocommerce' ),
				),
			),
			'min_amount'   => array(
				'title'       => __( 'Minimum order amount', 'woocommerce' ),
				'type'        => 'price',
				'placeholder' => wc_format_localized_price( 0 ),
				'description' => __( 'Users will need to spend this amount to get free shipping (if enabled above).', 'woocommerce' ),
				'default'     => '0',
				'desc_tip'    => true,
			),
		);
	}

	/**
	 * Check if package is available.
	 *
	 * @param array $package Package information.
	 * @return bool
	 */
	public function is_available( $package ) {
		if ( 'no' === $this->enabled ) {
			return false;
		}

		if ( 'specific' === $this->availability ) {
			$ship_to_countries = $this->countries;
		} else {
			$ship_to_countries = array_keys( WC()->countries->get_shipping_countries() );
		}

		if ( is_array( $ship_to_countries ) && ! in_array( $package['destination']['country'], $ship_to_countries, true ) ) {
			return false;
		}

		// Enabled logic.
		$is_available       = false;
		$has_coupon         = false;
		$has_met_min_amount = false;

		if ( in_array( $this->requires, array( 'coupon', 'either', 'both' ), true ) ) {
			$coupons = WC()->cart->get_coupons();

			if ( $coupons ) {
				foreach ( $coupons as $code => $coupon ) {
					if ( $coupon->is_valid() && $coupon->get_free_shipping() ) {
						$has_coupon = true;
					}
				}
			}
		}

		if ( in_array( $this->requires, array( 'min_amount', 'either', 'both' ), true ) ) {
			$total = WC()->cart->get_displayed_subtotal();

			if ( WC()->cart->display_prices_including_tax() ) {
				$total = NumberUtil::round( $total - ( WC()->cart->get_discount_total() + WC()->cart->get_discount_tax() ), wc_get_price_decimals() );
			} else {
				$total = NumberUtil::round( $total - WC()->cart->get_discount_total(), wc_get_price_decimals() );
			}

			if ( $total >= $this->min_amount ) {
				$has_met_min_amount = true;
			}
		}

		switch ( $this->requires ) {
			case 'min_amount':
				if ( $has_met_min_amount ) {
					$is_available = true;
				}
				break;
			case 'coupon':
				if ( $has_coupon ) {
					$is_available = true;
				}
				break;
			case 'both':
				if ( $has_met_min_amount && $has_coupon ) {
					$is_available = true;
				}
				break;
			case 'either':
				if ( $has_met_min_amount || $has_coupon ) {
					$is_available = true;
				}
				break;
			default:
				$is_available = true;
				break;
		}

		return apply_filters( 'woocommerce_shipping_' . $this->id . '_is_available', $is_available, $package, $this );
	}

	/**
	 * Calculate shipping.
	 *
	 * @param array $package Package information.
	 */
	public function calculate_shipping( $package = array() ) {
		$args = array(
			'id'      => $this->id,
			'label'   => $this->title,
			'cost'    => 0,
			'taxes'   => false,
			'package' => $package,
		);
		$this->add_rate( $args );
	}
}
shipping/flat-rate/includes/settings-flat-rate.php000064400000006517151336044770016323 0ustar00<?php
/**
 * Settings for flat rate shipping.
 *
 * @package WooCommerce\Classes\Shipping
 */

defined( 'ABSPATH' ) || exit;

$cost_desc = __( 'Enter a cost (excl. tax) or sum, e.g. <code>10.00 * [qty]</code>.', 'woocommerce' ) . '<br/><br/>' . __( 'Use <code>[qty]</code> for the number of items, <br/><code>[cost]</code> for the total cost of items, and <code>[fee percent="10" min_fee="20" max_fee=""]</code> for percentage based fees.', 'woocommerce' );

$settings = array(
	'title'      => array(
		'title'       => __( 'Method title', 'woocommerce' ),
		'type'        => 'text',
		'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ),
		'default'     => __( 'Flat rate', 'woocommerce' ),
		'desc_tip'    => true,
	),
	'tax_status' => array(
		'title'   => __( 'Tax status', 'woocommerce' ),
		'type'    => 'select',
		'class'   => 'wc-enhanced-select',
		'default' => 'taxable',
		'options' => array(
			'taxable' => __( 'Taxable', 'woocommerce' ),
			'none'    => _x( 'None', 'Tax status', 'woocommerce' ),
		),
	),
	'cost'       => array(
		'title'             => __( 'Cost', 'woocommerce' ),
		'type'              => 'text',
		'placeholder'       => '',
		'description'       => $cost_desc,
		'default'           => '0',
		'desc_tip'          => true,
		'sanitize_callback' => array( $this, 'sanitize_cost' ),
	),
);

$shipping_classes = WC()->shipping()->get_shipping_classes();

if ( ! empty( $shipping_classes ) ) {
	$settings['class_costs'] = array(
		'title'       => __( 'Shipping class costs', 'woocommerce' ),
		'type'        => 'title',
		'default'     => '',
		/* translators: %s: URL for link. */
		'description' => sprintf( __( 'These costs can optionally be added based on the <a href="%s">product shipping class</a>.', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=shipping&section=classes' ) ),
	);
	foreach ( $shipping_classes as $shipping_class ) {
		if ( ! isset( $shipping_class->term_id ) ) {
			continue;
		}
		$settings[ 'class_cost_' . $shipping_class->term_id ] = array(
			/* translators: %s: shipping class name */
			'title'             => sprintf( __( '"%s" shipping class cost', 'woocommerce' ), esc_html( $shipping_class->name ) ),
			'type'              => 'text',
			'placeholder'       => __( 'N/A', 'woocommerce' ),
			'description'       => $cost_desc,
			'default'           => $this->get_option( 'class_cost_' . $shipping_class->slug ), // Before 2.5.0, we used slug here which caused issues with long setting names.
			'desc_tip'          => true,
			'sanitize_callback' => array( $this, 'sanitize_cost' ),
		);
	}

	$settings['no_class_cost'] = array(
		'title'             => __( 'No shipping class cost', 'woocommerce' ),
		'type'              => 'text',
		'placeholder'       => __( 'N/A', 'woocommerce' ),
		'description'       => $cost_desc,
		'default'           => '',
		'desc_tip'          => true,
		'sanitize_callback' => array( $this, 'sanitize_cost' ),
	);

	$settings['type'] = array(
		'title'   => __( 'Calculation type', 'woocommerce' ),
		'type'    => 'select',
		'class'   => 'wc-enhanced-select',
		'default' => 'class',
		'options' => array(
			'class' => __( 'Per class: Charge shipping for each shipping class individually', 'woocommerce' ),
			'order' => __( 'Per order: Charge shipping for the most expensive shipping class', 'woocommerce' ),
		),
	);
}

return $settings;
shipping/flat-rate/class-wc-shipping-flat-rate.php000064400000017372151336044770016211 0ustar00<?php
/**
 * Flat Rate Shipping Method.
 *
 * @version 2.6.0
 * @package WooCommerce\Classes\Shipping
 */

defined( 'ABSPATH' ) || exit;

/**
 * WC_Shipping_Flat_Rate class.
 */
class WC_Shipping_Flat_Rate extends WC_Shipping_Method {

	/**
	 * Cost passed to [fee] shortcode.
	 *
	 * @var string Cost.
	 */
	protected $fee_cost = '';

	/**
	 * Constructor.
	 *
	 * @param int $instance_id Shipping method instance ID.
	 */
	public function __construct( $instance_id = 0 ) {
		$this->id                 = 'flat_rate';
		$this->instance_id        = absint( $instance_id );
		$this->method_title       = __( 'Flat rate', 'woocommerce' );
		$this->method_description = __( 'Lets you charge a fixed rate for shipping.', 'woocommerce' );
		$this->supports           = array(
			'shipping-zones',
			'instance-settings',
			'instance-settings-modal',
		);
		$this->init();

		add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) );
	}

	/**
	 * Init user set variables.
	 */
	public function init() {
		$this->instance_form_fields = include __DIR__ . '/includes/settings-flat-rate.php';
		$this->title                = $this->get_option( 'title' );
		$this->tax_status           = $this->get_option( 'tax_status' );
		$this->cost                 = $this->get_option( 'cost' );
		$this->type                 = $this->get_option( 'type', 'class' );
	}

	/**
	 * Evaluate a cost from a sum/string.
	 *
	 * @param  string $sum Sum of shipping.
	 * @param  array  $args Args, must contain `cost` and `qty` keys. Having `array()` as default is for back compat reasons.
	 * @return string
	 */
	protected function evaluate_cost( $sum, $args = array() ) {
		// Add warning for subclasses.
		if ( ! is_array( $args ) || ! array_key_exists( 'qty', $args ) || ! array_key_exists( 'cost', $args ) ) {
			wc_doing_it_wrong( __FUNCTION__, '$args must contain `cost` and `qty` keys.', '4.0.1' );
		}

		include_once WC()->plugin_path() . '/includes/libraries/class-wc-eval-math.php';

		// Allow 3rd parties to process shipping cost arguments.
		$args           = apply_filters( 'woocommerce_evaluate_shipping_cost_args', $args, $sum, $this );
		$locale         = localeconv();
		$decimals       = array( wc_get_price_decimal_separator(), $locale['decimal_point'], $locale['mon_decimal_point'], ',' );
		$this->fee_cost = $args['cost'];

		// Expand shortcodes.
		add_shortcode( 'fee', array( $this, 'fee' ) );

		$sum = do_shortcode(
			str_replace(
				array(
					'[qty]',
					'[cost]',
				),
				array(
					$args['qty'],
					$args['cost'],
				),
				$sum
			)
		);

		remove_shortcode( 'fee', array( $this, 'fee' ) );

		// Remove whitespace from string.
		$sum = preg_replace( '/\s+/', '', $sum );

		// Remove locale from string.
		$sum = str_replace( $decimals, '.', $sum );

		// Trim invalid start/end characters.
		$sum = rtrim( ltrim( $sum, "\t\n\r\0\x0B+*/" ), "\t\n\r\0\x0B+-*/" );

		// Do the math.
		return $sum ? WC_Eval_Math::evaluate( $sum ) : 0;
	}

	/**
	 * Work out fee (shortcode).
	 *
	 * @param  array $atts Attributes.
	 * @return string
	 */
	public function fee( $atts ) {
		$atts = shortcode_atts(
			array(
				'percent' => '',
				'min_fee' => '',
				'max_fee' => '',
			),
			$atts,
			'fee'
		);

		$calculated_fee = 0;

		if ( $atts['percent'] ) {
			$calculated_fee = $this->fee_cost * ( floatval( $atts['percent'] ) / 100 );
		}

		if ( $atts['min_fee'] && $calculated_fee < $atts['min_fee'] ) {
			$calculated_fee = $atts['min_fee'];
		}

		if ( $atts['max_fee'] && $calculated_fee > $atts['max_fee'] ) {
			$calculated_fee = $atts['max_fee'];
		}

		return $calculated_fee;
	}

	/**
	 * Calculate the shipping costs.
	 *
	 * @param array $package Package of items from cart.
	 */
	public function calculate_shipping( $package = array() ) {
		$rate = array(
			'id'      => $this->get_rate_id(),
			'label'   => $this->title,
			'cost'    => 0,
			'package' => $package,
		);

		// Calculate the costs.
		$has_costs = false; // True when a cost is set. False if all costs are blank strings.
		$cost      = $this->get_option( 'cost' );

		if ( '' !== $cost ) {
			$has_costs    = true;
			$rate['cost'] = $this->evaluate_cost(
				$cost,
				array(
					'qty'  => $this->get_package_item_qty( $package ),
					'cost' => $package['contents_cost'],
				)
			);
		}

		// Add shipping class costs.
		$shipping_classes = WC()->shipping()->get_shipping_classes();

		if ( ! empty( $shipping_classes ) ) {
			$found_shipping_classes = $this->find_shipping_classes( $package );
			$highest_class_cost     = 0;

			foreach ( $found_shipping_classes as $shipping_class => $products ) {
				// Also handles BW compatibility when slugs were used instead of ids.
				$shipping_class_term = get_term_by( 'slug', $shipping_class, 'product_shipping_class' );
				$class_cost_string   = $shipping_class_term && $shipping_class_term->term_id ? $this->get_option( 'class_cost_' . $shipping_class_term->term_id, $this->get_option( 'class_cost_' . $shipping_class, '' ) ) : $this->get_option( 'no_class_cost', '' );

				if ( '' === $class_cost_string ) {
					continue;
				}

				$has_costs  = true;
				$class_cost = $this->evaluate_cost(
					$class_cost_string,
					array(
						'qty'  => array_sum( wp_list_pluck( $products, 'quantity' ) ),
						'cost' => array_sum( wp_list_pluck( $products, 'line_total' ) ),
					)
				);

				if ( 'class' === $this->type ) {
					$rate['cost'] += $class_cost;
				} else {
					$highest_class_cost = $class_cost > $highest_class_cost ? $class_cost : $highest_class_cost;
				}
			}

			if ( 'order' === $this->type && $highest_class_cost ) {
				$rate['cost'] += $highest_class_cost;
			}
		}

		if ( $has_costs ) {
			$this->add_rate( $rate );
		}

		/**
		 * Developers can add additional flat rates based on this one via this action since @version 2.4.
		 *
		 * Previously there were (overly complex) options to add additional rates however this was not user.
		 * friendly and goes against what Flat Rate Shipping was originally intended for.
		 */
		do_action( 'woocommerce_' . $this->id . '_shipping_add_rate', $this, $rate );
	}

	/**
	 * Get items in package.
	 *
	 * @param  array $package Package of items from cart.
	 * @return int
	 */
	public function get_package_item_qty( $package ) {
		$total_quantity = 0;
		foreach ( $package['contents'] as $item_id => $values ) {
			if ( $values['quantity'] > 0 && $values['data']->needs_shipping() ) {
				$total_quantity += $values['quantity'];
			}
		}
		return $total_quantity;
	}

	/**
	 * Finds and returns shipping classes and the products with said class.
	 *
	 * @param mixed $package Package of items from cart.
	 * @return array
	 */
	public function find_shipping_classes( $package ) {
		$found_shipping_classes = array();

		foreach ( $package['contents'] as $item_id => $values ) {
			if ( $values['data']->needs_shipping() ) {
				$found_class = $values['data']->get_shipping_class();

				if ( ! isset( $found_shipping_classes[ $found_class ] ) ) {
					$found_shipping_classes[ $found_class ] = array();
				}

				$found_shipping_classes[ $found_class ][ $item_id ] = $values;
			}
		}

		return $found_shipping_classes;
	}

	/**
	 * Sanitize the cost field.
	 *
	 * @since 3.4.0
	 * @param string $value Unsanitized value.
	 * @throws Exception Last error triggered.
	 * @return string
	 */
	public function sanitize_cost( $value ) {
		$value = is_null( $value ) ? '' : $value;
		$value = wp_kses_post( trim( wp_unslash( $value ) ) );
		$value = str_replace( array( get_woocommerce_currency_symbol(), html_entity_decode( get_woocommerce_currency_symbol() ) ), '', $value );
		// Thrown an error on the front end if the evaluate_cost will fail.
		$dummy_cost = $this->evaluate_cost(
			$value,
			array(
				'cost' => 1,
				'qty'  => 1,
			)
		);
		if ( false === $dummy_cost ) {
			throw new Exception( WC_Eval_Math::$last_error );
		}
		return $value;
	}
}
shipping/legacy-international-delivery/class-wc-shipping-legacy-international-delivery.php000064400000005123151336044770026326 0ustar00<?php
/**
 * Class WC_Shipping_Legacy_International_Delivery file.
 *
 * @package WooCommerce\Shipping
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * International Delivery - Based on the Flat Rate Shipping Method.
 *
 * This class is here for backwards compatibility for methods existing before zones existed.
 *
 * @deprecated  2.6.0
 * @version     2.4.0
 * @package     WooCommerce\Classes\Shipping
 */
class WC_Shipping_Legacy_International_Delivery extends WC_Shipping_Legacy_Flat_Rate {

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->id           = 'legacy_international_delivery';
		$this->method_title = __( 'International flat rate (legacy)', 'woocommerce' );
		/* translators: %s: Admin shipping settings URL */
		$this->method_description = '<strong>' . sprintf( __( 'This method is deprecated in 2.6.0 and will be removed in future versions - we recommend disabling it and instead setting up a new rate within your <a href="%s">Shipping zones</a>.', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=shipping' ) ) . '</strong>';
		$this->init();

		add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) );
	}

	/**
	 * Return the name of the option in the WP DB.
	 *
	 * @since 2.6.0
	 * @return string
	 */
	public function get_option_key() {
		return $this->plugin_id . 'international_delivery_settings';
	}

	/**
	 * Initialise settings form fields.
	 */
	public function init_form_fields() {
		parent::init_form_fields();
		$this->form_fields['availability'] = array(
			'title'       => __( 'Availability', 'woocommerce' ),
			'type'        => 'select',
			'class'       => 'wc-enhanced-select',
			'description' => '',
			'default'     => 'including',
			'options'     => array(
				'including' => __( 'Selected countries', 'woocommerce' ),
				'excluding' => __( 'Excluding selected countries', 'woocommerce' ),
			),
		);
	}

	/**
	 * Check if package is available.
	 *
	 * @param array $package Package information.
	 * @return bool
	 */
	public function is_available( $package ) {
		if ( 'no' === $this->enabled ) {
			return false;
		}
		if ( 'including' === $this->availability ) {
			if ( is_array( $this->countries ) && ! in_array( $package['destination']['country'], $this->countries, true ) ) {
				return false;
			}
		} else {
			if ( is_array( $this->countries ) && ( in_array( $package['destination']['country'], $this->countries, true ) || ! $package['destination']['country'] ) ) {
				return false;
			}
		}
		return apply_filters( 'woocommerce_shipping_' . $this->id . '_is_available', true, $package, $this );
	}
}
shipping/legacy-flat-rate/includes/settings-flat-rate.php000064400000011745151336044770017564 0ustar00<?php
/**
 * Legacy flat rate settings.
 *
 * @package WooCommerce\Shipping
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

$cost_desc = __( 'Enter a cost (excl. tax) or sum, e.g. <code>10.00 * [qty]</code>.', 'woocommerce' ) . '<br/>' . __( 'Supports the following placeholders: <code>[qty]</code> = number of items, <code>[cost]</code> = cost of items, <code>[fee percent="10" min_fee="20"]</code> = Percentage based fee.', 'woocommerce' );

/**
 * Settings for flat rate shipping.
 */
$settings = array(
	'enabled'      => array(
		'title'   => __( 'Enable/Disable', 'woocommerce' ),
		'type'    => 'checkbox',
		'label'   => __( 'Once disabled, this legacy method will no longer be available.', 'woocommerce' ),
		'default' => 'no',
	),
	'title'        => array(
		'title'       => __( 'Method title', 'woocommerce' ),
		'type'        => 'text',
		'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ),
		'default'     => __( 'Flat rate', 'woocommerce' ),
		'desc_tip'    => true,
	),
	'availability' => array(
		'title'   => __( 'Availability', 'woocommerce' ),
		'type'    => 'select',
		'default' => 'all',
		'class'   => 'availability wc-enhanced-select',
		'options' => array(
			'all'      => __( 'All allowed countries', 'woocommerce' ),
			'specific' => __( 'Specific Countries', 'woocommerce' ),
		),
	),
	'countries'    => array(
		'title'             => __( 'Specific countries', 'woocommerce' ),
		'type'              => 'multiselect',
		'class'             => 'wc-enhanced-select',
		'css'               => 'width: 400px;',
		'default'           => '',
		'options'           => WC()->countries->get_shipping_countries(),
		'custom_attributes' => array(
			'data-placeholder' => __( 'Select some countries', 'woocommerce' ),
		),
	),
	'tax_status'   => array(
		'title'   => __( 'Tax status', 'woocommerce' ),
		'type'    => 'select',
		'class'   => 'wc-enhanced-select',
		'default' => 'taxable',
		'options' => array(
			'taxable' => __( 'Taxable', 'woocommerce' ),
			'none'    => _x( 'None', 'Tax status', 'woocommerce' ),
		),
	),
	'cost'         => array(
		'title'       => __( 'Cost', 'woocommerce' ),
		'type'        => 'text',
		'placeholder' => '',
		'description' => $cost_desc,
		'default'     => '',
		'desc_tip'    => true,
	),
);

$shipping_classes = WC()->shipping()->get_shipping_classes();

if ( ! empty( $shipping_classes ) ) {
	$settings['class_costs'] = array(
		'title'       => __( 'Shipping class costs', 'woocommerce' ),
		'type'        => 'title',
		'default'     => '',
		/* translators: %s: Admin shipping settings URL */
		'description' => sprintf( __( 'These costs can optionally be added based on the <a href="%s">product shipping class</a>.', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=shipping&section=classes' ) ),
	);
	foreach ( $shipping_classes as $shipping_class ) {
		if ( ! isset( $shipping_class->term_id ) ) {
			continue;
		}
		$settings[ 'class_cost_' . $shipping_class->term_id ] = array(
			/* translators: %s: shipping class name */
			'title'       => sprintf( __( '"%s" shipping class cost', 'woocommerce' ), esc_html( $shipping_class->name ) ),
			'type'        => 'text',
			'placeholder' => __( 'N/A', 'woocommerce' ),
			'description' => $cost_desc,
			'default'     => $this->get_option( 'class_cost_' . $shipping_class->slug ), // Before 2.5.0, we used slug here which caused issues with long setting names.
			'desc_tip'    => true,
		);
	}
	$settings['no_class_cost'] = array(
		'title'       => __( 'No shipping class cost', 'woocommerce' ),
		'type'        => 'text',
		'placeholder' => __( 'N/A', 'woocommerce' ),
		'description' => $cost_desc,
		'default'     => '',
		'desc_tip'    => true,
	);
	$settings['type']          = array(
		'title'   => __( 'Calculation type', 'woocommerce' ),
		'type'    => 'select',
		'class'   => 'wc-enhanced-select',
		'default' => 'class',
		'options' => array(
			'class' => __( 'Per class: Charge shipping for each shipping class individually', 'woocommerce' ),
			'order' => __( 'Per order: Charge shipping for the most expensive shipping class', 'woocommerce' ),
		),
	);
}

if ( apply_filters( 'woocommerce_enable_deprecated_additional_flat_rates', $this->get_option( 'options', false ) ) ) {
	$settings['additional_rates'] = array(
		'title'       => __( 'Additional rates', 'woocommerce' ),
		'type'        => 'title',
		'default'     => '',
		'description' => __( 'These rates are extra shipping options with additional costs (based on the flat rate).', 'woocommerce' ),
	);
	$settings['options']          = array(
		'title'       => __( 'Additional rates', 'woocommerce' ),
		'type'        => 'textarea',
		'description' => __( 'One per line: Option name | Additional cost [+- Percents] | Per cost type (order, class, or item) Example: <code>Priority mail | 6.95 [+ 0.2%] | order</code>.', 'woocommerce' ),
		'default'     => '',
		'desc_tip'    => true,
		'placeholder' => __( 'Option name | Additional cost [+- Percents%] | Per cost type (order, class, or item)', 'woocommerce' ),
	);
}

return $settings;
shipping/legacy-flat-rate/class-wc-shipping-legacy-flat-rate.php000064400000027337151336044770020717 0ustar00<?php
/**
 * Class WC_Shipping_Legacy_Flat_Rate file.
 *
 * @package WooCommerce\Shipping
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Flat Rate Shipping Method.
 *
 * This class is here for backwards compatibility for methods existing before zones existed.
 *
 * @deprecated  2.6.0
 * @version     2.4.0
 * @package     WooCommerce\Classes\Shipping
 */
class WC_Shipping_Legacy_Flat_Rate extends WC_Shipping_Method {

	/**
	 * Cost passed to [fee] shortcode.
	 *
	 * @var string
	 */
	protected $fee_cost = '';

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->id           = 'legacy_flat_rate';
		$this->method_title = __( 'Flat rate (legacy)', 'woocommerce' );
		/* translators: %s: Admin shipping settings URL */
		$this->method_description = '<strong>' . sprintf( __( 'This method is deprecated in 2.6.0 and will be removed in future versions - we recommend disabling it and instead setting up a new rate within your <a href="%s">Shipping zones</a>.', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=shipping' ) ) . '</strong>';
		$this->init();

		add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) );
		add_action( 'woocommerce_flat_rate_shipping_add_rate', array( $this, 'calculate_extra_shipping' ), 10, 2 );
	}

	/**
	 * Process and redirect if disabled.
	 */
	public function process_admin_options() {
		parent::process_admin_options();

		if ( 'no' === $this->settings['enabled'] ) {
			wp_redirect( admin_url( 'admin.php?page=wc-settings&tab=shipping&section=options' ) );
			exit;
		}
	}

	/**
	 * Return the name of the option in the WP DB.
	 *
	 * @since 2.6.0
	 * @return string
	 */
	public function get_option_key() {
		return $this->plugin_id . 'flat_rate_settings';
	}

	/**
	 * Init function.
	 */
	public function init() {
		// Load the settings.
		$this->init_form_fields();
		$this->init_settings();

		// Define user set variables.
		$this->title        = $this->get_option( 'title' );
		$this->availability = $this->get_option( 'availability' );
		$this->countries    = $this->get_option( 'countries' );
		$this->tax_status   = $this->get_option( 'tax_status' );
		$this->cost         = $this->get_option( 'cost' );
		$this->type         = $this->get_option( 'type', 'class' );
		$this->options      = $this->get_option( 'options', false ); // @deprecated 2.4.0
	}

	/**
	 * Initialise Settings Form Fields.
	 */
	public function init_form_fields() {
		$this->form_fields = include __DIR__ . '/includes/settings-flat-rate.php';
	}

	/**
	 * Evaluate a cost from a sum/string.
	 *
	 * @param  string $sum Sum to evaluate.
	 * @param  array  $args Arguments.
	 * @return string
	 */
	protected function evaluate_cost( $sum, $args = array() ) {
		include_once WC()->plugin_path() . '/includes/libraries/class-wc-eval-math.php';

		$locale   = localeconv();
		$decimals = array( wc_get_price_decimal_separator(), $locale['decimal_point'], $locale['mon_decimal_point'] );

		$this->fee_cost = $args['cost'];

		// Expand shortcodes.
		add_shortcode( 'fee', array( $this, 'fee' ) );

		$sum = do_shortcode(
			str_replace(
				array(
					'[qty]',
					'[cost]',
				),
				array(
					$args['qty'],
					$args['cost'],
				),
				$sum
			)
		);

		remove_shortcode( 'fee', array( $this, 'fee' ) );

		// Remove whitespace from string.
		$sum = preg_replace( '/\s+/', '', $sum );

		// Remove locale from string.
		$sum = str_replace( $decimals, '.', $sum );

		// Trim invalid start/end characters.
		$sum = rtrim( ltrim( $sum, "\t\n\r\0\x0B+*/" ), "\t\n\r\0\x0B+-*/" );

		// Do the math.
		return $sum ? WC_Eval_Math::evaluate( $sum ) : 0;
	}

	/**
	 * Work out fee (shortcode).
	 *
	 * @param  array $atts Shortcode attributes.
	 * @return string
	 */
	public function fee( $atts ) {
		$atts = shortcode_atts(
			array(
				'percent' => '',
				'min_fee' => '',
			),
			$atts,
			'fee'
		);

		$calculated_fee = 0;

		if ( $atts['percent'] ) {
			$calculated_fee = $this->fee_cost * ( floatval( $atts['percent'] ) / 100 );
		}

		if ( $atts['min_fee'] && $calculated_fee < $atts['min_fee'] ) {
			$calculated_fee = $atts['min_fee'];
		}

		return $calculated_fee;
	}

	/**
	 * Calculate shipping.
	 *
	 * @param array $package (default: array()).
	 */
	public function calculate_shipping( $package = array() ) {
		$rate = array(
			'id'      => $this->id,
			'label'   => $this->title,
			'cost'    => 0,
			'package' => $package,
		);

		// Calculate the costs.
		$has_costs = false; // True when a cost is set. False if all costs are blank strings.
		$cost      = $this->get_option( 'cost' );

		if ( '' !== $cost ) {
			$has_costs    = true;
			$rate['cost'] = $this->evaluate_cost(
				$cost,
				array(
					'qty'  => $this->get_package_item_qty( $package ),
					'cost' => $package['contents_cost'],
				)
			);
		}

		// Add shipping class costs.
		$found_shipping_classes = $this->find_shipping_classes( $package );
		$highest_class_cost     = 0;

		foreach ( $found_shipping_classes as $shipping_class => $products ) {
			// Also handles BW compatibility when slugs were used instead of ids.
			$shipping_class_term = get_term_by( 'slug', $shipping_class, 'product_shipping_class' );
			$class_cost_string   = $shipping_class_term && $shipping_class_term->term_id ? $this->get_option( 'class_cost_' . $shipping_class_term->term_id, $this->get_option( 'class_cost_' . $shipping_class, '' ) ) : $this->get_option( 'no_class_cost', '' );

			if ( '' === $class_cost_string ) {
				continue;
			}

			$has_costs  = true;
			$class_cost = $this->evaluate_cost(
				$class_cost_string,
				array(
					'qty'  => array_sum( wp_list_pluck( $products, 'quantity' ) ),
					'cost' => array_sum( wp_list_pluck( $products, 'line_total' ) ),
				)
			);

			if ( 'class' === $this->type ) {
				$rate['cost'] += $class_cost;
			} else {
				$highest_class_cost = $class_cost > $highest_class_cost ? $class_cost : $highest_class_cost;
			}
		}

		if ( 'order' === $this->type && $highest_class_cost ) {
			$rate['cost'] += $highest_class_cost;
		}

		$rate['package'] = $package;

		// Add the rate.
		if ( $has_costs ) {
			$this->add_rate( $rate );
		}

		/**
		 * Developers can add additional flat rates based on this one via this action since @version 2.4.
		 *
		 * Previously there were (overly complex) options to add additional rates however this was not user.
		 * friendly and goes against what Flat Rate Shipping was originally intended for.
		 *
		 * This example shows how you can add an extra rate based on this flat rate via custom function:
		 *
		 *      add_action( 'woocommerce_flat_rate_shipping_add_rate', 'add_another_custom_flat_rate', 10, 2 );
		 *
		 *      function add_another_custom_flat_rate( $method, $rate ) {
		 *          $new_rate          = $rate;
		 *          $new_rate['id']    .= ':' . 'custom_rate_name'; // Append a custom ID.
		 *          $new_rate['label'] = 'Rushed Shipping'; // Rename to 'Rushed Shipping'.
		 *          $new_rate['cost']  += 2; // Add $2 to the cost.
		 *
		 *          // Add it to WC.
		 *          $method->add_rate( $new_rate );
		 *      }.
		 */
		do_action( 'woocommerce_flat_rate_shipping_add_rate', $this, $rate );
	}

	/**
	 * Get items in package.
	 *
	 * @param  array $package Package information.
	 * @return int
	 */
	public function get_package_item_qty( $package ) {
		$total_quantity = 0;
		foreach ( $package['contents'] as $item_id => $values ) {
			if ( $values['quantity'] > 0 && $values['data']->needs_shipping() ) {
				$total_quantity += $values['quantity'];
			}
		}
		return $total_quantity;
	}

	/**
	 * Finds and returns shipping classes and the products with said class.
	 *
	 * @param mixed $package Package information.
	 * @return array
	 */
	public function find_shipping_classes( $package ) {
		$found_shipping_classes = array();

		foreach ( $package['contents'] as $item_id => $values ) {
			if ( $values['data']->needs_shipping() ) {
				$found_class = $values['data']->get_shipping_class();

				if ( ! isset( $found_shipping_classes[ $found_class ] ) ) {
					$found_shipping_classes[ $found_class ] = array();
				}

				$found_shipping_classes[ $found_class ][ $item_id ] = $values;
			}
		}

		return $found_shipping_classes;
	}

	/**
	 * Adds extra calculated flat rates.
	 *
	 * @deprecated 2.4.0
	 *
	 * Additional rates defined like this:
	 *  Option Name | Additional Cost [+- Percents%] | Per Cost Type (order, class, or item).
	 *
	 * @param null  $method Deprecated.
	 * @param array $rate Rate information.
	 */
	public function calculate_extra_shipping( $method, $rate ) {
		if ( $this->options ) {
			$options = array_filter( (array) explode( "\n", $this->options ) );

			foreach ( $options as $option ) {
				$this_option = array_map( 'trim', explode( WC_DELIMITER, $option ) );
				if ( count( $this_option ) !== 3 ) {
					continue;
				}
				$extra_rate          = $rate;
				$extra_rate['id']    = $this->id . ':' . urldecode( sanitize_title( $this_option[0] ) );
				$extra_rate['label'] = $this_option[0];
				$extra_cost          = $this->get_extra_cost( $this_option[1], $this_option[2], $rate['package'] );
				if ( is_array( $extra_rate['cost'] ) ) {
					$extra_rate['cost']['order'] = $extra_rate['cost']['order'] + $extra_cost;
				} else {
					$extra_rate['cost'] += $extra_cost;
				}

				$this->add_rate( $extra_rate );
			}
		}
	}

	/**
	 * Calculate the percentage adjustment for each shipping rate.
	 *
	 * @deprecated 2.4.0
	 * @param  float  $cost Cost.
	 * @param  float  $percent_adjustment Percent adjusment.
	 * @param  string $percent_operator Percent operator.
	 * @param  float  $base_price Base price.
	 * @return float
	 */
	public function calc_percentage_adjustment( $cost, $percent_adjustment, $percent_operator, $base_price ) {
		if ( '+' === $percent_operator ) {
			$cost += $percent_adjustment * $base_price;
		} else {
			$cost -= $percent_adjustment * $base_price;
		}
		return $cost;
	}

	/**
	 * Get extra cost.
	 *
	 * @deprecated 2.4.0
	 * @param  string $cost_string Cost string.
	 * @param  string $type Type.
	 * @param  array  $package Package information.
	 * @return float
	 */
	public function get_extra_cost( $cost_string, $type, $package ) {
		$cost         = $cost_string;
		$cost_percent = false;
		// @codingStandardsIgnoreStart
		$pattern      =
			'/' .           // Start regex.
			'(\d+\.?\d*)' . // Capture digits, optionally capture a `.` and more digits.
			'\s*' .         // Match whitespace.
			'(\+|-)' .      // Capture the operand.
			'\s*' .         // Match whitespace.
			'(\d+\.?\d*)' . // Capture digits, optionally capture a `.` and more digits.
			'\%/';          // Match the percent sign & end regex.
		// @codingStandardsIgnoreEnd
		if ( preg_match( $pattern, $cost_string, $this_cost_matches ) ) {
			$cost_operator = $this_cost_matches[2];
			$cost_percent  = $this_cost_matches[3] / 100;
			$cost          = $this_cost_matches[1];
		}
		switch ( $type ) {
			case 'class':
				$cost = $cost * count( $this->find_shipping_classes( $package ) );
				break;
			case 'item':
				$cost = $cost * $this->get_package_item_qty( $package );
				break;
		}
		if ( $cost_percent ) {
			switch ( $type ) {
				case 'class':
					$shipping_classes = $this->find_shipping_classes( $package );
					foreach ( $shipping_classes as $shipping_class => $items ) {
						foreach ( $items as $item_id => $values ) {
							$cost = $this->calc_percentage_adjustment( $cost, $cost_percent, $cost_operator, $values['line_total'] );
						}
					}
					break;
				case 'item':
					foreach ( $package['contents'] as $item_id => $values ) {
						if ( $values['data']->needs_shipping() ) {
							$cost = $this->calc_percentage_adjustment( $cost, $cost_percent, $cost_operator, $values['line_total'] );
						}
					}
					break;
				case 'order':
					$cost = $this->calc_percentage_adjustment( $cost, $cost_percent, $cost_operator, $package['contents_cost'] );
					break;
			}
		}
		return $cost;
	}
}
shipping/legacy-local-pickup/class-wc-shipping-legacy-local-pickup.php000064400000015354151336044770022123 0ustar00<?php
/**
 * Class WC_Shipping_Legacy_Local_Pickup file.
 *
 * @package WooCommerce\Shipping
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Local Pickup Shipping Method.
 *
 * This class is here for backwards compatibility for methods existing before zones existed.
 *
 * @deprecated  2.6.0
 * @version     2.3.0
 * @package     WooCommerce\Classes\Shipping
 */
class WC_Shipping_Legacy_Local_Pickup extends WC_Shipping_Method {

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->id           = 'legacy_local_pickup';
		$this->method_title = __( 'Local pickup (legacy)', 'woocommerce' );
		/* translators: %s: Admin shipping settings URL */
		$this->method_description = '<strong>' . sprintf( __( 'This method is deprecated in 2.6.0 and will be removed in future versions - we recommend disabling it and instead setting up a new rate within your <a href="%s">Shipping zones</a>.', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=shipping' ) ) . '</strong>';
		$this->init();
	}

	/**
	 * Process and redirect if disabled.
	 */
	public function process_admin_options() {
		parent::process_admin_options();

		if ( 'no' === $this->settings['enabled'] ) {
			wp_redirect( admin_url( 'admin.php?page=wc-settings&tab=shipping&section=options' ) );
			exit;
		}
	}

	/**
	 * Return the name of the option in the WP DB.
	 *
	 * @since 2.6.0
	 * @return string
	 */
	public function get_option_key() {
		return $this->plugin_id . 'local_pickup_settings';
	}

	/**
	 * Init function.
	 */
	public function init() {

		// Load the settings.
		$this->init_form_fields();
		$this->init_settings();

		// Define user set variables.
		$this->enabled      = $this->get_option( 'enabled' );
		$this->title        = $this->get_option( 'title' );
		$this->codes        = $this->get_option( 'codes' );
		$this->availability = $this->get_option( 'availability' );
		$this->countries    = $this->get_option( 'countries' );

		// Actions.
		add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) );
	}

	/**
	 * Calculate shipping.
	 *
	 * @param array $package Package information.
	 */
	public function calculate_shipping( $package = array() ) {
		$rate = array(
			'id'      => $this->id,
			'label'   => $this->title,
			'package' => $package,
		);
		$this->add_rate( $rate );
	}

	/**
	 * Initialize form fields.
	 */
	public function init_form_fields() {
		$this->form_fields = array(
			'enabled'      => array(
				'title'   => __( 'Enable', 'woocommerce' ),
				'type'    => 'checkbox',
				'label'   => __( 'Once disabled, this legacy method will no longer be available.', 'woocommerce' ),
				'default' => 'no',
			),
			'title'        => array(
				'title'       => __( 'Title', 'woocommerce' ),
				'type'        => 'text',
				'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ),
				'default'     => __( 'Local pickup', 'woocommerce' ),
				'desc_tip'    => true,
			),
			'codes'        => array(
				'title'       => __( 'Allowed ZIP/post codes', 'woocommerce' ),
				'type'        => 'text',
				'desc_tip'    => __( 'What ZIP/post codes are available for local pickup?', 'woocommerce' ),
				'default'     => '',
				'description' => __( 'Separate codes with a comma. Accepts wildcards, e.g. <code>P*</code> will match a postcode of PE30. Also accepts a pattern, e.g. <code>NG1___</code> would match NG1 1AA but not NG10 1AA', 'woocommerce' ),
				'placeholder' => 'e.g. 12345, 56789',
			),
			'availability' => array(
				'title'   => __( 'Method availability', 'woocommerce' ),
				'type'    => 'select',
				'default' => 'all',
				'class'   => 'availability wc-enhanced-select',
				'options' => array(
					'all'      => __( 'All allowed countries', 'woocommerce' ),
					'specific' => __( 'Specific countries', 'woocommerce' ),
				),
			),
			'countries'    => array(
				'title'             => __( 'Specific countries', 'woocommerce' ),
				'type'              => 'multiselect',
				'class'             => 'wc-enhanced-select',
				'css'               => 'width: 400px;',
				'default'           => '',
				'options'           => WC()->countries->get_shipping_countries(),
				'custom_attributes' => array(
					'data-placeholder' => __( 'Select some countries', 'woocommerce' ),
				),
			),
		);
	}

	/**
	 * Get postcodes for this method.
	 *
	 * @return array
	 */
	public function get_valid_postcodes() {
		$codes = array();

		if ( '' !== $this->codes ) {
			foreach ( explode( ',', $this->codes ) as $code ) {
				$codes[] = strtoupper( trim( $code ) );
			}
		}

		return $codes;
	}

	/**
	 * See if a given postcode matches valid postcodes.
	 *
	 * @param  string $postcode Postcode to check.
	 * @param  string $country code Code of the country to check postcode against.
	 * @return boolean
	 */
	public function is_valid_postcode( $postcode, $country ) {
		$codes              = $this->get_valid_postcodes();
		$postcode           = $this->clean( $postcode );
		$formatted_postcode = wc_format_postcode( $postcode, $country );

		if ( in_array( $postcode, $codes, true ) || in_array( $formatted_postcode, $codes, true ) ) {
			return true;
		}

		// Pattern matching.
		foreach ( $codes as $c ) {
			$pattern = '/^' . str_replace( '_', '[0-9a-zA-Z]', preg_quote( $c ) ) . '$/i';
			if ( preg_match( $pattern, $postcode ) ) {
				return true;
			}
		}

		// Wildcard search.
		$wildcard_postcode = $formatted_postcode . '*';
		$postcode_length   = strlen( $formatted_postcode );

		for ( $i = 0; $i < $postcode_length; $i++ ) {
			if ( in_array( $wildcard_postcode, $codes, true ) ) {
				return true;
			}
			$wildcard_postcode = substr( $wildcard_postcode, 0, -2 ) . '*';
		}

		return false;
	}

	/**
	 * See if the method is available.
	 *
	 * @param array $package Package information.
	 * @return bool
	 */
	public function is_available( $package ) {
		$is_available = 'yes' === $this->enabled;

		if ( $is_available && $this->get_valid_postcodes() ) {
			$is_available = $this->is_valid_postcode( $package['destination']['postcode'], $package['destination']['country'] );
		}

		if ( $is_available ) {
			if ( 'specific' === $this->availability ) {
				$ship_to_countries = $this->countries;
			} else {
				$ship_to_countries = array_keys( WC()->countries->get_shipping_countries() );
			}
			if ( is_array( $ship_to_countries ) && ! in_array( $package['destination']['country'], $ship_to_countries, true ) ) {
				$is_available = false;
			}
		}

		return apply_filters( 'woocommerce_shipping_' . $this->id . '_is_available', $is_available, $package, $this );
	}

	/**
	 * Clean function.
	 *
	 * @access public
	 * @param mixed $code Code.
	 * @return string
	 */
	public function clean( $code ) {
		return str_replace( '-', '', sanitize_title( $code ) ) . ( strstr( $code, '*' ) ? '*' : '' );
	}
}
class-wc-cart-totals.php000064400000070227151336044770011246 0ustar00<?php
/**
 * Cart totals calculation class.
 *
 * Methods are protected and class is final to keep this as an internal API.
 * May be opened in the future once structure is stable.
 *
 * Rounding guide:
 * - if something is being stored e.g. item total, store unrounded. This is so taxes can be recalculated later accurately.
 * - if calculating a total, round (if settings allow).
 *
 * @package WooCommerce\Classes
 * @version 3.2.0
 */

use Automattic\WooCommerce\Utilities\NumberUtil;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC_Cart_Totals class.
 *
 * @since 3.2.0
 */
final class WC_Cart_Totals {
	use WC_Item_Totals;

	/**
	 * Reference to cart object.
	 *
	 * @since 3.2.0
	 * @var WC_Cart
	 */
	protected $cart;

	/**
	 * Reference to customer object.
	 *
	 * @since 3.2.0
	 * @var array
	 */
	protected $customer;

	/**
	 * Line items to calculate.
	 *
	 * @since 3.2.0
	 * @var array
	 */
	protected $items = array();

	/**
	 * Fees to calculate.
	 *
	 * @since 3.2.0
	 * @var array
	 */
	protected $fees = array();

	/**
	 * Shipping costs.
	 *
	 * @since 3.2.0
	 * @var array
	 */
	protected $shipping = array();

	/**
	 * Applied coupon objects.
	 *
	 * @since 3.2.0
	 * @var array
	 */
	protected $coupons = array();

	/**
	 * Item/coupon discount totals.
	 *
	 * @since 3.2.0
	 * @var array
	 */
	protected $coupon_discount_totals = array();

	/**
	 * Item/coupon discount tax totals.
	 *
	 * @since 3.2.0
	 * @var array
	 */
	protected $coupon_discount_tax_totals = array();

	/**
	 * Should taxes be calculated?
	 *
	 * @var boolean
	 */
	protected $calculate_tax = true;

	/**
	 * Stores totals.
	 *
	 * @since 3.2.0
	 * @var array
	 */
	protected $totals = array(
		'fees_total'         => 0,
		'fees_total_tax'     => 0,
		'items_subtotal'     => 0,
		'items_subtotal_tax' => 0,
		'items_total'        => 0,
		'items_total_tax'    => 0,
		'total'              => 0,
		'shipping_total'     => 0,
		'shipping_tax_total' => 0,
		'discounts_total'    => 0,
	);

	/**
	 * Sets up the items provided, and calculate totals.
	 *
	 * @since 3.2.0
	 * @throws Exception If missing WC_Cart object.
	 * @param WC_Cart $cart Cart object to calculate totals for.
	 */
	public function __construct( &$cart = null ) {
		if ( ! is_a( $cart, 'WC_Cart' ) ) {
			throw new Exception( 'A valid WC_Cart object is required' );
		}

		$this->cart          = $cart;
		$this->calculate_tax = wc_tax_enabled() && ! $cart->get_customer()->get_is_vat_exempt();
		$this->calculate();
	}

	/**
	 * Run all calculation methods on the given items in sequence.
	 *
	 * @since 3.2.0
	 */
	protected function calculate() {
		$this->calculate_item_totals();
		$this->calculate_shipping_totals();
		$this->calculate_fee_totals();
		$this->calculate_totals();
	}

	/**
	 * Get default blank set of props used per item.
	 *
	 * @since  3.2.0
	 * @return array
	 */
	protected function get_default_item_props() {
		return (object) array(
			'object'             => null,
			'tax_class'          => '',
			'taxable'            => false,
			'quantity'           => 0,
			'product'            => false,
			'price_includes_tax' => false,
			'subtotal'           => 0,
			'subtotal_tax'       => 0,
			'subtotal_taxes'     => array(),
			'total'              => 0,
			'total_tax'          => 0,
			'taxes'              => array(),
		);
	}

	/**
	 * Get default blank set of props used per fee.
	 *
	 * @since  3.2.0
	 * @return array
	 */
	protected function get_default_fee_props() {
		return (object) array(
			'object'    => null,
			'tax_class' => '',
			'taxable'   => false,
			'total_tax' => 0,
			'taxes'     => array(),
		);
	}

	/**
	 * Get default blank set of props used per shipping row.
	 *
	 * @since  3.2.0
	 * @return array
	 */
	protected function get_default_shipping_props() {
		return (object) array(
			'object'    => null,
			'tax_class' => '',
			'taxable'   => false,
			'total'     => 0,
			'total_tax' => 0,
			'taxes'     => array(),
		);
	}

	/**
	 * Handles a cart or order object passed in for calculation. Normalises data
	 * into the same format for use by this class.
	 *
	 * Each item is made up of the following props, in addition to those returned by get_default_item_props() for totals.
	 *  - key: An identifier for the item (cart item key or line item ID).
	 *  - cart_item: For carts, the cart item from the cart which may include custom data.
	 *  - quantity: The qty for this line.
	 *  - price: The line price in cents.
	 *  - product: The product object this cart item is for.
	 *
	 * @since 3.2.0
	 */
	protected function get_items_from_cart() {
		$this->items = array();

		foreach ( $this->cart->get_cart() as $cart_item_key => $cart_item ) {
			$item                          = $this->get_default_item_props();
			$item->key                     = $cart_item_key;
			$item->object                  = $cart_item;
			$item->tax_class               = $cart_item['data']->get_tax_class();
			$item->taxable                 = 'taxable' === $cart_item['data']->get_tax_status();
			$item->price_includes_tax      = wc_prices_include_tax();
			$item->quantity                = $cart_item['quantity'];
			$item->price                   = wc_add_number_precision_deep( (float) $cart_item['data']->get_price() * (float) $cart_item['quantity'] );
			$item->product                 = $cart_item['data'];
			$item->tax_rates               = $this->get_item_tax_rates( $item );
			$this->items[ $cart_item_key ] = $item;
		}
	}

	/**
	 * Get item costs grouped by tax class.
	 *
	 * @since  3.2.0
	 * @return array
	 */
	protected function get_tax_class_costs() {
		$item_tax_classes     = wp_list_pluck( $this->items, 'tax_class' );
		$shipping_tax_classes = wp_list_pluck( $this->shipping, 'tax_class' );
		$fee_tax_classes      = wp_list_pluck( $this->fees, 'tax_class' );
		$costs                = array_fill_keys( $item_tax_classes + $shipping_tax_classes + $fee_tax_classes, 0 );
		$costs['non-taxable'] = 0;

		foreach ( $this->items + $this->fees + $this->shipping as $item ) {
			if ( 0 > $item->total ) {
				continue;
			}
			if ( ! $item->taxable ) {
				$costs['non-taxable'] += $item->total;
			} elseif ( 'inherit' === $item->tax_class ) {
				$costs[ reset( $item_tax_classes ) ] += $item->total;
			} else {
				$costs[ $item->tax_class ] += $item->total;
			}
		}
		return array_filter( $costs );
	}

	/**
	 * Get fee objects from the cart. Normalises data
	 * into the same format for use by this class.
	 *
	 * @since 3.2.0
	 */
	protected function get_fees_from_cart() {
		$this->fees = array();
		$this->cart->calculate_fees();

		$fee_running_total = 0;

		foreach ( $this->cart->get_fees() as $fee_key => $fee_object ) {
			$fee            = $this->get_default_fee_props();
			$fee->object    = $fee_object;
			$fee->tax_class = $fee->object->tax_class;
			$fee->taxable   = $fee->object->taxable;
			$fee->total     = wc_add_number_precision_deep( $fee->object->amount );

			// Negative fees should not make the order total go negative.
			if ( 0 > $fee->total ) {
				$max_discount = NumberUtil::round( $this->get_total( 'items_total', true ) + $fee_running_total + $this->get_total( 'shipping_total', true ) ) * -1;

				if ( $fee->total < $max_discount ) {
					$fee->total = $max_discount;
				}
			}

			$fee_running_total += $fee->total;

			if ( $this->calculate_tax ) {
				if ( 0 > $fee->total ) {
					// Negative fees should have the taxes split between all items so it works as a true discount.
					$tax_class_costs = $this->get_tax_class_costs();
					$total_cost      = array_sum( $tax_class_costs );

					if ( $total_cost ) {
						foreach ( $tax_class_costs as $tax_class => $tax_class_cost ) {
							if ( 'non-taxable' === $tax_class ) {
								continue;
							}
							$proportion               = $tax_class_cost / $total_cost;
							$cart_discount_proportion = $fee->total * $proportion;
							$fee->taxes               = wc_array_merge_recursive_numeric( $fee->taxes, WC_Tax::calc_tax( $fee->total * $proportion, WC_Tax::get_rates( $tax_class ) ) );
						}
					}
				} elseif ( $fee->object->taxable ) {
					$fee->taxes = WC_Tax::calc_tax( $fee->total, WC_Tax::get_rates( $fee->tax_class, $this->cart->get_customer() ), false );
				}
			}

			$fee->taxes     = apply_filters( 'woocommerce_cart_totals_get_fees_from_cart_taxes', $fee->taxes, $fee, $this );
			$fee->total_tax = array_sum( array_map( array( $this, 'round_line_tax' ), $fee->taxes ) );

			// Set totals within object.
			$fee->object->total    = wc_remove_number_precision_deep( $fee->total );
			$fee->object->tax_data = wc_remove_number_precision_deep( $fee->taxes );
			$fee->object->tax      = wc_remove_number_precision_deep( $fee->total_tax );

			$this->fees[ $fee_key ] = $fee;
		}
	}

	/**
	 * Get shipping methods from the cart and normalise.
	 *
	 * @since 3.2.0
	 */
	protected function get_shipping_from_cart() {
		$this->shipping = array();

		if ( ! $this->cart->show_shipping() ) {
			return;
		}

		foreach ( $this->cart->calculate_shipping() as $key => $shipping_object ) {
			$shipping_line            = $this->get_default_shipping_props();
			$shipping_line->object    = $shipping_object;
			$shipping_line->tax_class = get_option( 'woocommerce_shipping_tax_class' );
			$shipping_line->taxable   = true;
			$shipping_line->total     = wc_add_number_precision_deep( $shipping_object->cost );
			$shipping_line->taxes     = wc_add_number_precision_deep( $shipping_object->taxes, false );
			$shipping_line->taxes     = array_map( array( $this, 'round_item_subtotal' ), $shipping_line->taxes );
			$shipping_line->total_tax = array_sum( $shipping_line->taxes );

			$this->shipping[ $key ] = $shipping_line;
		}
	}

	/**
	 * Return array of coupon objects from the cart. Normalises data
	 * into the same format for use by this class.
	 *
	 * @since  3.2.0
	 */
	protected function get_coupons_from_cart() {
		$this->coupons = $this->cart->get_coupons();

		foreach ( $this->coupons as $coupon ) {
			switch ( $coupon->get_discount_type() ) {
				case 'fixed_product':
					$coupon->sort = 1;
					break;
				case 'percent':
					$coupon->sort = 2;
					break;
				case 'fixed_cart':
					$coupon->sort = 3;
					break;
				default:
					$coupon->sort = 0;
					break;
			}

			// Allow plugins to override the default order.
			$coupon->sort = apply_filters( 'woocommerce_coupon_sort', $coupon->sort, $coupon );
		}

		uasort( $this->coupons, array( $this, 'sort_coupons_callback' ) );
	}

	/**
	 * Sort coupons so discounts apply consistently across installs.
	 *
	 * In order of priority;
	 *  - sort param
	 *  - usage restriction
	 *  - coupon value
	 *  - ID
	 *
	 * @param WC_Coupon $a Coupon object.
	 * @param WC_Coupon $b Coupon object.
	 * @return int
	 */
	protected function sort_coupons_callback( $a, $b ) {
		if ( $a->sort === $b->sort ) {
			if ( $a->get_limit_usage_to_x_items() === $b->get_limit_usage_to_x_items() ) {
				if ( $a->get_amount() === $b->get_amount() ) {
					return $b->get_id() - $a->get_id();
				}
				return ( $a->get_amount() < $b->get_amount() ) ? -1 : 1;
			}
			return ( $a->get_limit_usage_to_x_items() < $b->get_limit_usage_to_x_items() ) ? -1 : 1;
		}
		return ( $a->sort < $b->sort ) ? -1 : 1;
	}

	/**
	 * Ran to remove all base taxes from an item. Used when prices include tax, and the customer is tax exempt.
	 *
	 * @since 3.2.2
	 * @param object $item Item to adjust the prices of.
	 * @return object
	 */
	protected function remove_item_base_taxes( $item ) {
		if ( $item->price_includes_tax && $item->taxable ) {
			if ( apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) {
				$base_tax_rates = WC_Tax::get_base_tax_rates( $item->product->get_tax_class( 'unfiltered' ) );
			} else {
				/**
				 * If we want all customers to pay the same price on this store, we should not remove base taxes from a VAT exempt user's price,
				 * but just the relevent tax rate. See issue #20911.
				 */
				$base_tax_rates = $item->tax_rates;
			}

			// Work out a new base price without the shop's base tax.
			$taxes = WC_Tax::calc_tax( $item->price, $base_tax_rates, true );

			// Now we have a new item price (excluding TAX).
			$item->price              = NumberUtil::round( $item->price - array_sum( $taxes ) );
			$item->price_includes_tax = false;
		}
		return $item;
	}

	/**
	 * Only ran if woocommerce_adjust_non_base_location_prices is true.
	 *
	 * If the customer is outside of the base location, this removes the base
	 * taxes. This is off by default unless the filter is used.
	 *
	 * Uses edit context so unfiltered tax class is returned.
	 *
	 * @since 3.2.0
	 * @param object $item Item to adjust the prices of.
	 * @return object
	 */
	protected function adjust_non_base_location_price( $item ) {
		if ( $item->price_includes_tax && $item->taxable ) {
			$base_tax_rates = WC_Tax::get_base_tax_rates( $item->product->get_tax_class( 'unfiltered' ) );

			if ( $item->tax_rates !== $base_tax_rates ) {
				// Work out a new base price without the shop's base tax.
				$taxes     = WC_Tax::calc_tax( $item->price, $base_tax_rates, true );
				$new_taxes = WC_Tax::calc_tax( $item->price - array_sum( $taxes ), $item->tax_rates, false );

				// Now we have a new item price.
				$item->price = $item->price - array_sum( $taxes ) + array_sum( $new_taxes );
			}
		}
		return $item;
	}

	/**
	 * Get discounted price of an item with precision (in cents).
	 *
	 * @since  3.2.0
	 * @param  object $item_key Item to get the price of.
	 * @return int
	 */
	protected function get_discounted_price_in_cents( $item_key ) {
		$item  = $this->items[ $item_key ];
		$price = isset( $this->coupon_discount_totals[ $item_key ] ) ? $item->price - $this->coupon_discount_totals[ $item_key ] : $item->price;
		return $price;
	}

	/**
	 * Get tax rates for an item. Caches rates in class to avoid multiple look ups.
	 *
	 * @param  object $item Item to get tax rates for.
	 * @return array of taxes
	 */
	protected function get_item_tax_rates( $item ) {
		if ( ! wc_tax_enabled() ) {
			return array();
		}
		$tax_class      = $item->product->get_tax_class();
		$item_tax_rates = isset( $this->item_tax_rates[ $tax_class ] ) ? $this->item_tax_rates[ $tax_class ] : $this->item_tax_rates[ $tax_class ] = WC_Tax::get_rates( $item->product->get_tax_class(), $this->cart->get_customer() );

		// Allow plugins to filter item tax rates.
		return apply_filters( 'woocommerce_cart_totals_get_item_tax_rates', $item_tax_rates, $item, $this->cart );
	}

	/**
	 * Get item costs grouped by tax class.
	 *
	 * @since  3.2.0
	 * @return array
	 */
	protected function get_item_costs_by_tax_class() {
		$tax_classes = array(
			'non-taxable' => 0,
		);

		foreach ( $this->items + $this->fees + $this->shipping as $item ) {
			if ( ! isset( $tax_classes[ $item->tax_class ] ) ) {
				$tax_classes[ $item->tax_class ] = 0;
			}

			if ( $item->taxable ) {
				$tax_classes[ $item->tax_class ] += $item->total;
			} else {
				$tax_classes['non-taxable'] += $item->total;
			}
		}

		return $tax_classes;
	}

	/**
	 * Get a single total with or without precision (in cents).
	 *
	 * @since  3.2.0
	 * @param  string $key Total to get.
	 * @param  bool   $in_cents Should the totals be returned in cents, or without precision.
	 * @return int|float
	 */
	public function get_total( $key = 'total', $in_cents = false ) {
		$totals = $this->get_totals( $in_cents );
		return isset( $totals[ $key ] ) ? $totals[ $key ] : 0;
	}

	/**
	 * Set a single total.
	 *
	 * @since  3.2.0
	 * @param string $key Total name you want to set.
	 * @param int    $total Total to set.
	 */
	protected function set_total( $key, $total ) {
		$this->totals[ $key ] = $total;
	}

	/**
	 * Get all totals with or without precision (in cents).
	 *
	 * @since  3.2.0
	 * @param  bool $in_cents Should the totals be returned in cents, or without precision.
	 * @return array.
	 */
	public function get_totals( $in_cents = false ) {
		return $in_cents ? $this->totals : wc_remove_number_precision_deep( $this->totals );
	}

	/**
	 * Returns array of values for totals calculation.
	 *
	 * @param string $field Field name. Will probably be `total` or `subtotal`.
	 * @return array Items object
	 */
	protected function get_values_for_total( $field ) {
		return array_values( wp_list_pluck( $this->items, $field ) );
	}

	/**
	 * Get taxes merged by type.
	 *
	 * @since 3.2.0
	 * @param  bool         $in_cents If returned value should be in cents.
	 * @param  array|string $types    Types to merge and return. Defaults to all.
	 * @return array
	 */
	protected function get_merged_taxes( $in_cents = false, $types = array( 'items', 'fees', 'shipping' ) ) {
		$items = array();
		$taxes = array();

		if ( is_string( $types ) ) {
			$types = array( $types );
		}

		foreach ( $types as $type ) {
			if ( isset( $this->$type ) ) {
				$items = array_merge( $items, $this->$type );
			}
		}

		foreach ( $items as $item ) {
			foreach ( $item->taxes as $rate_id => $rate ) {
				if ( ! isset( $taxes[ $rate_id ] ) ) {
					$taxes[ $rate_id ] = 0;
				}
				$taxes[ $rate_id ] += $this->round_line_tax( $rate );
			}
		}

		return $in_cents ? $taxes : wc_remove_number_precision_deep( $taxes );
	}

	/**
	 * Round merged taxes.
	 *
	 * @deprecated 3.9.0 `calculate_item_subtotals` should already appropriately round the tax values.
	 * @since 3.5.4
	 * @param array $taxes Taxes to round.
	 * @return array
	 */
	protected function round_merged_taxes( $taxes ) {
		foreach ( $taxes as $rate_id => $tax ) {
			$taxes[ $rate_id ] = $this->round_line_tax( $tax );
		}

		return $taxes;
	}

	/**
	 * Combine item taxes into a single array, preserving keys.
	 *
	 * @since 3.2.0
	 * @param array $item_taxes Taxes to combine.
	 * @return array
	 */
	protected function combine_item_taxes( $item_taxes ) {
		$merged_taxes = array();
		foreach ( $item_taxes as $taxes ) {
			foreach ( $taxes as $tax_id => $tax_amount ) {
				if ( ! isset( $merged_taxes[ $tax_id ] ) ) {
					$merged_taxes[ $tax_id ] = 0;
				}
				$merged_taxes[ $tax_id ] += $tax_amount;
			}
		}
		return $merged_taxes;
	}

	/*
	|--------------------------------------------------------------------------
	| Calculation methods.
	|--------------------------------------------------------------------------
	*/

	/**
	 * Calculate item totals.
	 *
	 * @since 3.2.0
	 */
	protected function calculate_item_totals() {
		$this->get_items_from_cart();
		$this->calculate_item_subtotals();
		$this->calculate_discounts();

		foreach ( $this->items as $item_key => $item ) {
			$item->total     = $this->get_discounted_price_in_cents( $item_key );
			$item->total_tax = 0;

			if ( has_filter( 'woocommerce_get_discounted_price' ) ) {
				/**
				 * Allow plugins to filter this price like in the legacy cart class.
				 *
				 * This is legacy and should probably be deprecated in the future.
				 * $item->object is the cart item object.
				 * $this->cart is the cart object.
				 */
				$item->total = wc_add_number_precision(
					apply_filters( 'woocommerce_get_discounted_price', wc_remove_number_precision( $item->total ), $item->object, $this->cart )
				);
			}

			if ( $this->calculate_tax && $item->product->is_taxable() ) {
				$total_taxes     = apply_filters( 'woocommerce_calculate_item_totals_taxes', WC_Tax::calc_tax( $item->total, $item->tax_rates, $item->price_includes_tax ), $item, $this );
				$item->taxes     = $total_taxes;
				$item->total_tax = array_sum( array_map( array( $this, 'round_line_tax' ), $item->taxes ) );

				if ( $item->price_includes_tax ) {
					// Use unrounded taxes so we can re-calculate from the orders screen accurately later.
					$item->total = $item->total - array_sum( $item->taxes );
				}
			}

			$this->cart->cart_contents[ $item_key ]['line_tax_data']['total'] = wc_remove_number_precision_deep( $item->taxes );
			$this->cart->cart_contents[ $item_key ]['line_total']             = wc_remove_number_precision( $item->total );
			$this->cart->cart_contents[ $item_key ]['line_tax']               = wc_remove_number_precision( $item->total_tax );
		}

		$items_total = $this->get_rounded_items_total( $this->get_values_for_total( 'total' ) );

		$this->set_total( 'items_total', $items_total );
		$this->set_total( 'items_total_tax', array_sum( array_values( wp_list_pluck( $this->items, 'total_tax' ) ) ) );

		$this->cart->set_cart_contents_total( $this->get_total( 'items_total' ) );
		$this->cart->set_cart_contents_tax( array_sum( $this->get_merged_taxes( false, 'items' ) ) );
		$this->cart->set_cart_contents_taxes( $this->get_merged_taxes( false, 'items' ) );
	}

	/**
	 * Subtotals are costs before discounts.
	 *
	 * To prevent rounding issues we need to work with the inclusive price where possible
	 * otherwise we'll see errors such as when working with a 9.99 inc price, 20% VAT which would
	 * be 8.325 leading to totals being 1p off.
	 *
	 * Pre tax coupons come off the price the customer thinks they are paying - tax is calculated
	 * afterwards.
	 *
	 * e.g. $100 bike with $10 coupon = customer pays $90 and tax worked backwards from that.
	 *
	 * @since 3.2.0
	 */
	protected function calculate_item_subtotals() {
		$merged_subtotal_taxes = array(); // Taxes indexed by tax rate ID for storage later.

		$adjust_non_base_location_prices = apply_filters( 'woocommerce_adjust_non_base_location_prices', true );
		$is_customer_vat_exempt          = $this->cart->get_customer()->get_is_vat_exempt();

		foreach ( $this->items as $item_key => $item ) {
			if ( $item->price_includes_tax ) {
				if ( $is_customer_vat_exempt ) {
					$item = $this->remove_item_base_taxes( $item );
				} elseif ( $adjust_non_base_location_prices ) {
					$item = $this->adjust_non_base_location_price( $item );
				}
			}

			$item->subtotal = $item->price;

			if ( $this->calculate_tax && $item->product->is_taxable() ) {
				$item->subtotal_taxes = WC_Tax::calc_tax( $item->subtotal, $item->tax_rates, $item->price_includes_tax );
				$item->subtotal_tax   = array_sum( array_map( array( $this, 'round_line_tax' ), $item->subtotal_taxes ) );

				if ( $item->price_includes_tax ) {
					// Use unrounded taxes so we can re-calculate from the orders screen accurately later.
					$item->subtotal = $item->subtotal - array_sum( $item->subtotal_taxes );
				}

				foreach ( $item->subtotal_taxes as $rate_id => $rate ) {
					if ( ! isset( $merged_subtotal_taxes[ $rate_id ] ) ) {
						$merged_subtotal_taxes[ $rate_id ] = 0;
					}
					$merged_subtotal_taxes[ $rate_id ] += $this->round_line_tax( $rate );
				}
			}

			$this->cart->cart_contents[ $item_key ]['line_tax_data']     = array( 'subtotal' => wc_remove_number_precision_deep( $item->subtotal_taxes ) );
			$this->cart->cart_contents[ $item_key ]['line_subtotal']     = wc_remove_number_precision( $item->subtotal );
			$this->cart->cart_contents[ $item_key ]['line_subtotal_tax'] = wc_remove_number_precision( $item->subtotal_tax );
		}

		$items_subtotal = $this->get_rounded_items_total( $this->get_values_for_total( 'subtotal' ) );

		// Prices are not rounded here because they should already be rounded based on settings in `get_rounded_items_total` and in `round_line_tax` method calls.
		$this->set_total( 'items_subtotal', $items_subtotal );
		$this->set_total( 'items_subtotal_tax', array_sum( $merged_subtotal_taxes ), 0 );

		$this->cart->set_subtotal( $this->get_total( 'items_subtotal' ) );
		$this->cart->set_subtotal_tax( $this->get_total( 'items_subtotal_tax' ) );
	}

	/**
	 * Calculate COUPON based discounts which change item prices.
	 *
	 * @since 3.2.0
	 * @uses  WC_Discounts class.
	 */
	protected function calculate_discounts() {
		$this->get_coupons_from_cart();

		$discounts = new WC_Discounts( $this->cart );

		// Set items directly so the discounts class can see any tax adjustments made thus far using subtotals.
		$discounts->set_items( $this->items );

		foreach ( $this->coupons as $coupon ) {
			$discounts->apply_coupon( $coupon );
		}

		$coupon_discount_amounts     = $discounts->get_discounts_by_coupon( true );
		$coupon_discount_tax_amounts = array();

		// See how much tax was 'discounted' per item and per coupon.
		if ( $this->calculate_tax ) {
			foreach ( $discounts->get_discounts( true ) as $coupon_code => $coupon_discounts ) {
				$coupon_discount_tax_amounts[ $coupon_code ] = 0;

				foreach ( $coupon_discounts as $item_key => $coupon_discount ) {
					$item = $this->items[ $item_key ];

					if ( $item->product->is_taxable() ) {
						// Item subtotals were sent, so set 3rd param.
						$item_tax = array_sum( WC_Tax::calc_tax( $coupon_discount, $item->tax_rates, $item->price_includes_tax ) );

						// Sum total tax.
						$coupon_discount_tax_amounts[ $coupon_code ] += $item_tax;

						// Remove tax from discount total.
						if ( $item->price_includes_tax ) {
							$coupon_discount_amounts[ $coupon_code ] -= $item_tax;
						}
					}
				}
			}
		}

		$this->coupon_discount_totals     = (array) $discounts->get_discounts_by_item( true );
		$this->coupon_discount_tax_totals = $coupon_discount_tax_amounts;

		if ( wc_prices_include_tax() ) {
			$this->set_total( 'discounts_total', array_sum( $this->coupon_discount_totals ) - array_sum( $this->coupon_discount_tax_totals ) );
			$this->set_total( 'discounts_tax_total', array_sum( $this->coupon_discount_tax_totals ) );
		} else {
			$this->set_total( 'discounts_total', array_sum( $this->coupon_discount_totals ) );
			$this->set_total( 'discounts_tax_total', array_sum( $this->coupon_discount_tax_totals ) );
		}

		$this->cart->set_coupon_discount_totals( wc_remove_number_precision_deep( $coupon_discount_amounts ) );
		$this->cart->set_coupon_discount_tax_totals( wc_remove_number_precision_deep( $coupon_discount_tax_amounts ) );

		// Add totals to cart object. Note: Discount total for cart is excl tax.
		$this->cart->set_discount_total( $this->get_total( 'discounts_total' ) );
		$this->cart->set_discount_tax( $this->get_total( 'discounts_tax_total' ) );
	}

	/**
	 * Triggers the cart fees API, grabs the list of fees, and calculates taxes.
	 *
	 * Note: This class sets the totals for the 'object' as they are calculated. This is so that APIs like the fees API can see these totals if needed.
	 *
	 * @since 3.2.0
	 */
	protected function calculate_fee_totals() {
		$this->get_fees_from_cart();

		$this->set_total( 'fees_total', array_sum( wp_list_pluck( $this->fees, 'total' ) ) );
		$this->set_total( 'fees_total_tax', array_sum( wp_list_pluck( $this->fees, 'total_tax' ) ) );

		$this->cart->fees_api()->set_fees( wp_list_pluck( $this->fees, 'object' ) );
		$this->cart->set_fee_total( wc_remove_number_precision_deep( array_sum( wp_list_pluck( $this->fees, 'total' ) ) ) );
		$this->cart->set_fee_tax( wc_remove_number_precision_deep( array_sum( wp_list_pluck( $this->fees, 'total_tax' ) ) ) );
		$this->cart->set_fee_taxes( wc_remove_number_precision_deep( $this->combine_item_taxes( wp_list_pluck( $this->fees, 'taxes' ) ) ) );
	}

	/**
	 * Calculate any shipping taxes.
	 *
	 * @since 3.2.0
	 */
	protected function calculate_shipping_totals() {
		$this->get_shipping_from_cart();
		$this->set_total( 'shipping_total', array_sum( wp_list_pluck( $this->shipping, 'total' ) ) );
		$this->set_total( 'shipping_tax_total', array_sum( wp_list_pluck( $this->shipping, 'total_tax' ) ) );

		$this->cart->set_shipping_total( $this->get_total( 'shipping_total' ) );
		$this->cart->set_shipping_tax( $this->get_total( 'shipping_tax_total' ) );
		$this->cart->set_shipping_taxes( wc_remove_number_precision_deep( $this->combine_item_taxes( wp_list_pluck( $this->shipping, 'taxes' ) ) ) );
	}

	/**
	 * Main cart totals.
	 *
	 * @since 3.2.0
	 */
	protected function calculate_totals() {
		$this->set_total( 'total', NumberUtil::round( $this->get_total( 'items_total', true ) + $this->get_total( 'fees_total', true ) + $this->get_total( 'shipping_total', true ) + array_sum( $this->get_merged_taxes( true ) ), 0 ) );
		$items_tax = array_sum( $this->get_merged_taxes( false, array( 'items' ) ) );
		// Shipping and fee taxes are rounded seperately because they were entered excluding taxes (as opposed to item prices, which may or may not be including taxes depending upon settings).
		$shipping_and_fee_taxes = NumberUtil::round( array_sum( $this->get_merged_taxes( false, array( 'fees', 'shipping' ) ) ), wc_get_price_decimals() );
		$this->cart->set_total_tax( $items_tax + $shipping_and_fee_taxes );

		// Allow plugins to hook and alter totals before final total is calculated.
		if ( has_action( 'woocommerce_calculate_totals' ) ) {
			do_action( 'woocommerce_calculate_totals', $this->cart );
		}

		// Allow plugins to filter the grand total, and sum the cart totals in case of modifications.
		$this->cart->set_total( max( 0, apply_filters( 'woocommerce_calculated_total', $this->get_total( 'total' ), $this->cart ) ) );
	}
}
class-wc-privacy.php000064400000037030151336044770010461 0ustar00<?php
/**
 * Privacy/GDPR related functionality which ties into WordPress functionality.
 *
 * @since 3.4.0
 * @package WooCommerce\Classes
 */

defined( 'ABSPATH' ) || exit;

if ( ! class_exists( 'WC_Privacy_Background_Process', false ) ) {
	include_once __DIR__ . '/class-wc-privacy-background-process.php';
}

/**
 * WC_Privacy Class.
 */
class WC_Privacy extends WC_Abstract_Privacy {

	/**
	 * Background process to clean up orders.
	 *
	 * @var WC_Privacy_Background_Process
	 */
	protected static $background_process;

	/**
	 * Init - hook into events.
	 */
	public function __construct() {
		parent::__construct();

		// Initialize data exporters and erasers.
		add_action( 'plugins_loaded', array( $this, 'register_erasers_exporters' ) );

		// Cleanup orders daily - this is a callback on a daily cron event.
		add_action( 'woocommerce_cleanup_personal_data', array( $this, 'queue_cleanup_personal_data' ) );

		// Handles custom anonomization types not included in core.
		add_filter( 'wp_privacy_anonymize_data', array( $this, 'anonymize_custom_data_types' ), 10, 3 );

		// When this is fired, data is removed in a given order. Called from bulk actions.
		add_action( 'woocommerce_remove_order_personal_data', array( 'WC_Privacy_Erasers', 'remove_order_personal_data' ) );
	}

	/**
	 * Initial registration of privacy erasers and exporters.
	 *
	 * Due to the use of translation functions, this should run only after plugins loaded.
	 */
	public function register_erasers_exporters() {
		$this->name = __( 'WooCommerce', 'woocommerce' );

		if ( ! self::$background_process ) {
			self::$background_process = new WC_Privacy_Background_Process();
		}

		// Include supporting classes.
		include_once __DIR__ . '/class-wc-privacy-erasers.php';
		include_once __DIR__ . '/class-wc-privacy-exporters.php';

		// This hook registers WooCommerce data exporters.
		$this->add_exporter( 'woocommerce-customer-data', __( 'WooCommerce Customer Data', 'woocommerce' ), array( 'WC_Privacy_Exporters', 'customer_data_exporter' ) );
		$this->add_exporter( 'woocommerce-customer-orders', __( 'WooCommerce Customer Orders', 'woocommerce' ), array( 'WC_Privacy_Exporters', 'order_data_exporter' ) );
		$this->add_exporter( 'woocommerce-customer-downloads', __( 'WooCommerce Customer Downloads', 'woocommerce' ), array( 'WC_Privacy_Exporters', 'download_data_exporter' ) );
		$this->add_exporter( 'woocommerce-customer-tokens', __( 'WooCommerce Customer Payment Tokens', 'woocommerce' ), array( 'WC_Privacy_Exporters', 'customer_tokens_exporter' ) );

		// This hook registers WooCommerce data erasers.
		$this->add_eraser( 'woocommerce-customer-data', __( 'WooCommerce Customer Data', 'woocommerce' ), array( 'WC_Privacy_Erasers', 'customer_data_eraser' ) );
		$this->add_eraser( 'woocommerce-customer-orders', __( 'WooCommerce Customer Orders', 'woocommerce' ), array( 'WC_Privacy_Erasers', 'order_data_eraser' ) );
		$this->add_eraser( 'woocommerce-customer-downloads', __( 'WooCommerce Customer Downloads', 'woocommerce' ), array( 'WC_Privacy_Erasers', 'download_data_eraser' ) );
		$this->add_eraser( 'woocommerce-customer-tokens', __( 'WooCommerce Customer Payment Tokens', 'woocommerce' ), array( 'WC_Privacy_Erasers', 'customer_tokens_eraser' ) );
	}

	/**
	 * Add privacy policy content for the privacy policy page.
	 *
	 * @since 3.4.0
	 */
	public function get_privacy_message() {
		$content = '<div class="wp-suggested-text">' .
			'<p class="privacy-policy-tutorial">' .
				__( 'This sample language includes the basics around what personal data your store may be collecting, storing and sharing, as well as who may have access to that data. Depending on what settings are enabled and which additional plugins are used, the specific information shared by your store will vary. We recommend consulting with a lawyer when deciding what information to disclose on your privacy policy.', 'woocommerce' ) .
			'</p>' .
			'<p>' . __( 'We collect information about you during the checkout process on our store.', 'woocommerce' ) . '</p>' .
			'<h2>' . __( 'What we collect and store', 'woocommerce' ) . '</h2>' .
			'<p>' . __( 'While you visit our site, we’ll track:', 'woocommerce' ) . '</p>' .
			'<ul>' .
				'<li>' . __( 'Products you’ve viewed: we’ll use this to, for example, show you products you’ve recently viewed', 'woocommerce' ) . '</li>' .
				'<li>' . __( 'Location, IP address and browser type: we’ll use this for purposes like estimating taxes and shipping', 'woocommerce' ) . '</li>' .
				'<li>' . __( 'Shipping address: we’ll ask you to enter this so we can, for instance, estimate shipping before you place an order, and send you the order!', 'woocommerce' ) . '</li>' .
			'</ul>' .
			'<p>' . __( 'We’ll also use cookies to keep track of cart contents while you’re browsing our site.', 'woocommerce' ) . '</p>' .
			'<p class="privacy-policy-tutorial">' .
				__( 'Note: you may want to further detail your cookie policy, and link to that section from here.', 'woocommerce' ) .
			'</p>' .
			'<p>' . __( 'When you purchase from us, we’ll ask you to provide information including your name, billing address, shipping address, email address, phone number, credit card/payment details and optional account information like username and password. We’ll use this information for purposes, such as, to:', 'woocommerce' ) . '</p>' .
			'<ul>' .
				'<li>' . __( 'Send you information about your account and order', 'woocommerce' ) . '</li>' .
				'<li>' . __( 'Respond to your requests, including refunds and complaints', 'woocommerce' ) . '</li>' .
				'<li>' . __( 'Process payments and prevent fraud', 'woocommerce' ) . '</li>' .
				'<li>' . __( 'Set up your account for our store', 'woocommerce' ) . '</li>' .
				'<li>' . __( 'Comply with any legal obligations we have, such as calculating taxes', 'woocommerce' ) . '</li>' .
				'<li>' . __( 'Improve our store offerings', 'woocommerce' ) . '</li>' .
				'<li>' . __( 'Send you marketing messages, if you choose to receive them', 'woocommerce' ) . '</li>' .
			'</ul>' .
			'<p>' . __( 'If you create an account, we will store your name, address, email and phone number, which will be used to populate the checkout for future orders.', 'woocommerce' ) . '</p>' .
			'<p>' . __( 'We generally store information about you for as long as we need the information for the purposes for which we collect and use it, and we are not legally required to continue to keep it. For example, we will store order information for XXX years for tax and accounting purposes. This includes your name, email address and billing and shipping addresses.', 'woocommerce' ) . '</p>' .
			'<p>' . __( 'We will also store comments or reviews, if you choose to leave them.', 'woocommerce' ) . '</p>' .
			'<h2>' . __( 'Who on our team has access', 'woocommerce' ) . '</h2>' .
			'<p>' . __( 'Members of our team have access to the information you provide us. For example, both Administrators and Shop Managers can access:', 'woocommerce' ) . '</p>' .
			'<ul>' .
				'<li>' . __( 'Order information like what was purchased, when it was purchased and where it should be sent, and', 'woocommerce' ) . '</li>' .
				'<li>' . __( 'Customer information like your name, email address, and billing and shipping information.', 'woocommerce' ) . '</li>' .
			'</ul>' .
			'<p>' . __( 'Our team members have access to this information to help fulfill orders, process refunds and support you.', 'woocommerce' ) . '</p>' .
			'<h2>' . __( 'What we share with others', 'woocommerce' ) . '</h2>' .
			'<p class="privacy-policy-tutorial">' .
				__( 'In this section you should list who you’re sharing data with, and for what purpose. This could include, but may not be limited to, analytics, marketing, payment gateways, shipping providers, and third party embeds.', 'woocommerce' ) .
			'</p>' .
			'<p>' . __( 'We share information with third parties who help us provide our orders and store services to you; for example --', 'woocommerce' ) . '</p>' .
			'<h3>' . __( 'Payments', 'woocommerce' ) . '</h3>' .
			'<p class="privacy-policy-tutorial">' .
				__( 'In this subsection you should list which third party payment processors you’re using to take payments on your store since these may handle customer data. We’ve included PayPal as an example, but you should remove this if you’re not using PayPal.', 'woocommerce' ) .
			'</p>' .
			'<p>' . __( 'We accept payments through PayPal. When processing payments, some of your data will be passed to PayPal, including information required to process or support the payment, such as the purchase total and billing information.', 'woocommerce' ) . '</p>' .
			'<p>' . __( 'Please see the <a href="https://www.paypal.com/us/webapps/mpp/ua/privacy-full">PayPal Privacy Policy</a> for more details.', 'woocommerce' ) . '</p>' .
			'</div>';

		return apply_filters( 'wc_privacy_policy_content', $content );
	}

	/**
	 * Spawn events for order cleanup.
	 */
	public function queue_cleanup_personal_data() {
		self::$background_process->push_to_queue( array( 'task' => 'trash_pending_orders' ) );
		self::$background_process->push_to_queue( array( 'task' => 'trash_failed_orders' ) );
		self::$background_process->push_to_queue( array( 'task' => 'trash_cancelled_orders' ) );
		self::$background_process->push_to_queue( array( 'task' => 'anonymize_completed_orders' ) );
		self::$background_process->push_to_queue( array( 'task' => 'delete_inactive_accounts' ) );
		self::$background_process->save()->dispatch();
	}

	/**
	 * Handle some custom types of data and anonymize them.
	 *
	 * @param string $anonymous Anonymized string.
	 * @param string $type Type of data.
	 * @param string $data The data being anonymized.
	 * @return string Anonymized string.
	 */
	public function anonymize_custom_data_types( $anonymous, $type, $data ) {
		switch ( $type ) {
			case 'address_state':
			case 'address_country':
				$anonymous = ''; // Empty string - we don't want to store anything after removal.
				break;
			case 'phone':
				$anonymous = preg_replace( '/\d/u', '0', $data );
				break;
			case 'numeric_id':
				$anonymous = 0;
				break;
		}
		return $anonymous;
	}

	/**
	 * Find and trash old orders.
	 *
	 * @since 3.4.0
	 * @param  int $limit Limit orders to process per batch.
	 * @return int Number of orders processed.
	 */
	public static function trash_pending_orders( $limit = 20 ) {
		$option = wc_parse_relative_date_option( get_option( 'woocommerce_trash_pending_orders' ) );

		if ( empty( $option['number'] ) ) {
			return 0;
		}

		return self::trash_orders_query(
			apply_filters(
				'woocommerce_trash_pending_orders_query_args',
				array(
					'date_created' => '<' . strtotime( '-' . $option['number'] . ' ' . $option['unit'] ),
					'limit'        => $limit, // Batches of 20.
					'status'       => 'wc-pending',
					'type'         => 'shop_order',
				)
			)
		);
	}

	/**
	 * Find and trash old orders.
	 *
	 * @since 3.4.0
	 * @param  int $limit Limit orders to process per batch.
	 * @return int Number of orders processed.
	 */
	public static function trash_failed_orders( $limit = 20 ) {
		$option = wc_parse_relative_date_option( get_option( 'woocommerce_trash_failed_orders' ) );

		if ( empty( $option['number'] ) ) {
			return 0;
		}

		return self::trash_orders_query(
			apply_filters(
				'woocommerce_trash_failed_orders_query_args',
				array(
					'date_created' => '<' . strtotime( '-' . $option['number'] . ' ' . $option['unit'] ),
					'limit'        => $limit, // Batches of 20.
					'status'       => 'wc-failed',
					'type'         => 'shop_order',
				)
			)
		);
	}

	/**
	 * Find and trash old orders.
	 *
	 * @since 3.4.0
	 * @param  int $limit Limit orders to process per batch.
	 * @return int Number of orders processed.
	 */
	public static function trash_cancelled_orders( $limit = 20 ) {
		$option = wc_parse_relative_date_option( get_option( 'woocommerce_trash_cancelled_orders' ) );

		if ( empty( $option['number'] ) ) {
			return 0;
		}

		return self::trash_orders_query(
			apply_filters(
				'woocommerce_trash_cancelled_orders_query_args',
				array(
					'date_created' => '<' . strtotime( '-' . $option['number'] . ' ' . $option['unit'] ),
					'limit'        => $limit, // Batches of 20.
					'status'       => 'wc-cancelled',
					'type'         => 'shop_order',
				)
			)
		);
	}

	/**
	 * For a given query trash all matches.
	 *
	 * @since 3.4.0
	 * @param array $query Query array to pass to wc_get_orders().
	 * @return int Count of orders that were trashed.
	 */
	protected static function trash_orders_query( $query ) {
		$orders = wc_get_orders( $query );
		$count  = 0;

		if ( $orders ) {
			foreach ( $orders as $order ) {
				$order->delete( false );
				$count ++;
			}
		}

		return $count;
	}

	/**
	 * Anonymize old completed orders.
	 *
	 * @since 3.4.0
	 * @param  int $limit Limit orders to process per batch.
	 * @return int Number of orders processed.
	 */
	public static function anonymize_completed_orders( $limit = 20 ) {
		$option = wc_parse_relative_date_option( get_option( 'woocommerce_anonymize_completed_orders' ) );

		if ( empty( $option['number'] ) ) {
			return 0;
		}

		return self::anonymize_orders_query(
			apply_filters(
				'woocommerce_anonymize_completed_orders_query_args',
				array(
					'date_created' => '<' . strtotime( '-' . $option['number'] . ' ' . $option['unit'] ),
					'limit'        => $limit, // Batches of 20.
					'status'       => 'wc-completed',
					'anonymized'   => false,
					'type'         => 'shop_order',
				)
			)
		);
	}

	/**
	 * For a given query, anonymize all matches.
	 *
	 * @since 3.4.0
	 * @param array $query Query array to pass to wc_get_orders().
	 * @return int Count of orders that were anonymized.
	 */
	protected static function anonymize_orders_query( $query ) {
		$orders = wc_get_orders( $query );
		$count  = 0;

		if ( $orders ) {
			foreach ( $orders as $order ) {
				WC_Privacy_Erasers::remove_order_personal_data( $order );
				$count ++;
			}
		}

		return $count;
	}

	/**
	 * Delete inactive accounts.
	 *
	 * @since 3.4.0
	 * @param  int $limit Limit users to process per batch.
	 * @return int Number of users processed.
	 */
	public static function delete_inactive_accounts( $limit = 20 ) {
		$option = wc_parse_relative_date_option( get_option( 'woocommerce_delete_inactive_accounts' ) );

		if ( empty( $option['number'] ) ) {
			return 0;
		}

		return self::delete_inactive_accounts_query( strtotime( '-' . $option['number'] . ' ' . $option['unit'] ), $limit );
	}

	/**
	 * Delete inactive accounts.
	 *
	 * @since 3.4.0
	 * @param int $timestamp Timestamp to delete customers before.
	 * @param int $limit     Limit number of users to delete per run.
	 * @return int Count of customers that were deleted.
	 */
	protected static function delete_inactive_accounts_query( $timestamp, $limit = 20 ) {
		$count      = 0;
		$user_query = new WP_User_Query(
			array(
				'fields'     => 'ID',
				'number'     => $limit,
				'role__in'   => apply_filters(
					'woocommerce_delete_inactive_account_roles',
					array(
						'Customer',
						'Subscriber',
					)
				),
				'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
					'relation' => 'AND',
					array(
						'key'     => 'wc_last_active',
						'value'   => (string) $timestamp,
						'compare' => '<',
						'type'    => 'NUMERIC',
					),
					array(
						'key'     => 'wc_last_active',
						'value'   => '0',
						'compare' => '>',
						'type'    => 'NUMERIC',
					),
				),
			)
		);

		$user_ids = $user_query->get_results();

		if ( $user_ids ) {
			if ( ! function_exists( 'wp_delete_user' ) ) {
				require_once ABSPATH . 'wp-admin/includes/user.php';
			}

			foreach ( $user_ids as $user_id ) {
				wp_delete_user( $user_id );
				$count ++;
			}
		}

		return $count;
	}
}

new WC_Privacy();
class-wc-autoloader.php000064400000005372151336044770011147 0ustar00<?php
/**
 * WooCommerce Autoloader.
 *
 * @package WooCommerce\Classes
 * @version 2.3.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Autoloader class.
 */
class WC_Autoloader {

	/**
	 * Path to the includes directory.
	 *
	 * @var string
	 */
	private $include_path = '';

	/**
	 * The Constructor.
	 */
	public function __construct() {
		if ( function_exists( '__autoload' ) ) {
			spl_autoload_register( '__autoload' );
		}

		spl_autoload_register( array( $this, 'autoload' ) );

		$this->include_path = untrailingslashit( plugin_dir_path( WC_PLUGIN_FILE ) ) . '/includes/';
	}

	/**
	 * Take a class name and turn it into a file name.
	 *
	 * @param  string $class Class name.
	 * @return string
	 */
	private function get_file_name_from_class( $class ) {
		return 'class-' . str_replace( '_', '-', $class ) . '.php';
	}

	/**
	 * Include a class file.
	 *
	 * @param  string $path File path.
	 * @return bool Successful or not.
	 */
	private function load_file( $path ) {
		if ( $path && is_readable( $path ) ) {
			include_once $path;
			return true;
		}
		return false;
	}

	/**
	 * Auto-load WC classes on demand to reduce memory consumption.
	 *
	 * @param string $class Class name.
	 */
	public function autoload( $class ) {
		$class = strtolower( $class );

		if ( 0 !== strpos( $class, 'wc_' ) ) {
			return;
		}

		$file = $this->get_file_name_from_class( $class );
		$path = '';

		if ( 0 === strpos( $class, 'wc_addons_gateway_' ) ) {
			$path = $this->include_path . 'gateways/' . substr( str_replace( '_', '-', $class ), 18 ) . '/';
		} elseif ( 0 === strpos( $class, 'wc_gateway_' ) ) {
			$path = $this->include_path . 'gateways/' . substr( str_replace( '_', '-', $class ), 11 ) . '/';
		} elseif ( 0 === strpos( $class, 'wc_shipping_' ) ) {
			$path = $this->include_path . 'shipping/' . substr( str_replace( '_', '-', $class ), 12 ) . '/';
		} elseif ( 0 === strpos( $class, 'wc_shortcode_' ) ) {
			$path = $this->include_path . 'shortcodes/';
		} elseif ( 0 === strpos( $class, 'wc_meta_box' ) ) {
			$path = $this->include_path . 'admin/meta-boxes/';
		} elseif ( 0 === strpos( $class, 'wc_admin' ) ) {
			$path = $this->include_path . 'admin/';
		} elseif ( 0 === strpos( $class, 'wc_payment_token_' ) ) {
			$path = $this->include_path . 'payment-tokens/';
		} elseif ( 0 === strpos( $class, 'wc_log_handler_' ) ) {
			$path = $this->include_path . 'log-handlers/';
		} elseif ( 0 === strpos( $class, 'wc_integration' ) ) {
			$path = $this->include_path . 'integrations/' . substr( str_replace( '_', '-', $class ), 15 ) . '/';
		} elseif ( 0 === strpos( $class, 'wc_notes_' ) ) {
			$path = $this->include_path . 'admin/notes/';
		}

		if ( empty( $path ) || ! $this->load_file( $path . $file ) ) {
			$this->load_file( $this->include_path . $file );
		}
	}
}

new WC_Autoloader();
tracks/class-wc-tracks-client.php000064400000012345151336044770013040 0ustar00<?php
/**
 * Send Tracks events on behalf of a user.
 *
 * @package WooCommerce\Tracks
 */

use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Utilities\NumberUtil;

defined( 'ABSPATH' ) || exit;

/**
 * WC_Tracks_Client class.
 */
class WC_Tracks_Client {

	/**
	 * Pixel URL.
	 */
	const PIXEL = 'https://pixel.wp.com/t.gif';

	/**
	 * Browser type.
	 */
	const BROWSER_TYPE = 'php-agent';

	/**
	 * User agent.
	 */
	const USER_AGENT_SLUG = 'tracks-client';

	/**
	 * Initialize tracks client class
	 *
	 * @return void
	 */
	public static function init() {
		// Use wp hook for setting the identity cookie to avoid headers already sent warnings.
		add_action( 'admin_init', array( __CLASS__, 'maybe_set_identity_cookie' ) );
	}

	/**
	 * Check if identiy cookie is set, if not set it.
	 *
	 * @return void
	 */
	public static function maybe_set_identity_cookie() {
		// Do not set on AJAX requests.
		if ( Constants::is_true( 'DOING_AJAX' ) ) {
			return;
		}

		// Bail if cookie already set.
		if ( isset( $_COOKIE['tk_ai'] ) ) {
			return;
		}

		$user = wp_get_current_user();

		// We don't want to track user events during unit tests/CI runs.
		if ( $user instanceof WP_User && 'wptests_capabilities' === $user->cap_key ) {
			return false;
		}
		$user_id = $user->ID;
		$anon_id = get_user_meta( $user_id, '_woocommerce_tracks_anon_id', true );

		// If an id is still not found, create one and save it.
		if ( ! $anon_id ) {
			$anon_id = self::get_anon_id();
			update_user_meta( $user_id, '_woocommerce_tracks_anon_id', $anon_id );
		}

		// Don't set cookie on API requests.
		if ( ! Constants::is_true( 'REST_REQUEST' ) && ! Constants::is_true( 'XMLRPC_REQUEST' ) ) {
			wc_setcookie( 'tk_ai', $anon_id );
		}
	}

	/**
	 * Record a Tracks event
	 *
	 * @param  array $event Array of event properties.
	 * @return bool|WP_Error         True on success, WP_Error on failure.
	 */
	public static function record_event( $event ) {
		if ( ! $event instanceof WC_Tracks_Event ) {
			$event = new WC_Tracks_Event( $event );
		}

		if ( is_wp_error( $event ) ) {
			return $event;
		}

		$pixel = $event->build_pixel_url( $event );

		if ( ! $pixel ) {
			return new WP_Error( 'invalid_pixel', 'cannot generate tracks pixel for given input', 400 );
		}

		return self::record_pixel( $pixel );
	}

	/**
	 * Synchronously request the pixel.
	 *
	 * @param string $pixel pixel url and query string.
	 * @return bool Always returns true.
	 */
	public static function record_pixel( $pixel ) {
		// Add the Request Timestamp and URL terminator just before the HTTP request.
		$pixel .= '&_rt=' . self::build_timestamp() . '&_=_';

		wp_safe_remote_get(
			$pixel,
			array(
				'blocking'    => false,
				'redirection' => 2,
				'httpversion' => '1.1',
				'timeout'     => 1,
			)
		);

		return true;
	}

	/**
	 * Create a timestap representing milliseconds since 1970-01-01
	 *
	 * @return string A string representing a timestamp.
	 */
	public static function build_timestamp() {
		$ts = NumberUtil::round( microtime( true ) * 1000 );

		return number_format( $ts, 0, '', '' );
	}

	/**
	 * Get a user's identity to send to Tracks. If Jetpack exists, default to its implementation.
	 *
	 * @param int $user_id User id.
	 * @return array Identity properties.
	 */
	public static function get_identity( $user_id ) {
		$jetpack_lib = '/tracks/client.php';

		if ( class_exists( 'Jetpack' ) && Constants::is_defined( 'JETPACK__VERSION' ) ) {
			if ( version_compare( Constants::get_constant( 'JETPACK__VERSION' ), '7.5', '<' ) ) {
				if ( file_exists( jetpack_require_lib_dir() . $jetpack_lib ) ) {
					include_once jetpack_require_lib_dir() . $jetpack_lib;
					if ( function_exists( 'jetpack_tracks_get_identity' ) ) {
						return jetpack_tracks_get_identity( $user_id );
					}
				}
			} else {
				$tracking = new Automattic\Jetpack\Tracking();
				return $tracking->tracks_get_identity( $user_id );
			}
		}

		// Start with a previously set cookie.
		$anon_id = isset( $_COOKIE['tk_ai'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['tk_ai'] ) ) : false;

		// If there is no cookie, apply a saved id.
		if ( ! $anon_id ) {
			$anon_id = get_user_meta( $user_id, '_woocommerce_tracks_anon_id', true );
		}

		// If an id is still not found, create one and save it.
		if ( ! $anon_id ) {
			$anon_id = self::get_anon_id();

			update_user_meta( $user_id, '_woocommerce_tracks_anon_id', $anon_id );
		}

		return array(
			'_ut' => 'anon',
			'_ui' => $anon_id,
		);
	}

	/**
	 * Grabs the user's anon id from cookies, or generates and sets a new one
	 *
	 * @return string An anon id for the user
	 */
	public static function get_anon_id() {
		static $anon_id = null;

		if ( ! isset( $anon_id ) ) {

			// Did the browser send us a cookie?
			if ( isset( $_COOKIE['tk_ai'] ) ) {
				$anon_id = sanitize_text_field( wp_unslash( $_COOKIE['tk_ai'] ) );
			} else {

				$binary = '';

				// Generate a new anonId and try to save it in the browser's cookies.
				// Note that base64-encoding an 18 character string generates a 24-character anon id.
				for ( $i = 0; $i < 18; ++$i ) {
					$binary .= chr( wp_rand( 0, 255 ) );
				}

				// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
				$anon_id = 'woo:' . base64_encode( $binary );
			}
		}

		return $anon_id;
	}
}

WC_Tracks_Client::init();
tracks/class-wc-tracks-footer-pixel.php000064400000004214151336044770014173 0ustar00<?php
/**
 * Send Tracks events on behalf of a user using pixel images in page footer.
 *
 * @package WooCommerce\Tracks
 */

defined( 'ABSPATH' ) || exit;

/**
 * WC_Tracks_Footer_Pixel class.
 */
class WC_Tracks_Footer_Pixel {
	/**
	 * Singleton instance.
	 *
	 * @var WC_Tracks_Footer_Pixel
	 */
	protected static $instance = null;

	/**
	 * Events to send to Tracks.
	 *
	 * @var array
	 */
	protected $events = array();

	/**
	 * Instantiate the singleton.
	 *
	 * @return WC_Tracks_Footer_Pixel
	 */
	public static function instance() {
		if ( is_null( self::$instance ) ) {
			self::$instance = new WC_Tracks_Footer_Pixel();
		}

		return self::$instance;
	}

	/**
	 * Constructor - attach hooks to the singleton instance.
	 */
	public function __construct() {
		add_action( 'admin_footer', array( $this, 'render_tracking_pixels' ) );
		add_action( 'shutdown', array( $this, 'send_tracks_requests' ) );
	}

	/**
	 * Record a Tracks event
	 *
	 * @param  array $event Array of event properties.
	 * @return bool|WP_Error True on success, WP_Error on failure.
	 */
	public static function record_event( $event ) {
		if ( ! $event instanceof WC_Tracks_Event ) {
			$event = new WC_Tracks_Event( $event );
		}

		if ( is_wp_error( $event ) ) {
			return $event;
		}

		self::instance()->add_event( $event );

		return true;
	}

	/**
	 * Add a Tracks event to the queue.
	 *
	 * @param WC_Tracks_Event $event Event to track.
	 */
	public function add_event( $event ) {
		$this->events[] = $event;
	}

	/**
	 * Add events as tracking pixels to page footer.
	 */
	public function render_tracking_pixels() {
		if ( empty( $this->events ) ) {
			return;
		}

		foreach ( $this->events as $event ) {
			$pixel = $event->build_pixel_url();

			if ( ! $pixel ) {
				continue;
			}

			echo '<img style="position: fixed;" src="', esc_url( $pixel ), '" />';
		}

		$this->events = array();
	}

	/**
	 * Fire off API calls for events that weren't converted to pixels.
	 *
	 * This handles wp_redirect().
	 */
	public function send_tracks_requests() {
		if ( empty( $this->events ) ) {
			return;
		}

		foreach ( $this->events as $event ) {
			WC_Tracks_Client::record_event( $event );
		}
	}
}
tracks/events/class-wc-coupons-tracking.php000064400000003472151336044770015070 0ustar00<?php
/**
 * WooCommerce Coupons Tracking
 *
 * @package WooCommerce\Tracks
 */

defined( 'ABSPATH' ) || exit;

/**
 * This class adds actions to track usage of WooCommerce Orders.
 */
class WC_Coupons_Tracking {
	/**
	 * Init tracking.
	 */
	public function init() {
		add_action( 'load-edit.php', array( $this, 'tracks_coupons_events' ), 10 );
	}

	/**
	 * Add a listener on the "Apply" button to track bulk actions.
	 */
	public function tracks_coupons_bulk_actions() {
		wc_enqueue_js(
			"
			function onApplyBulkActions( event ) {
				var id = event.data.id;
				var action = $( '#' + id ).val();
				
				if ( action && '-1' !== action ) {
					window.wcTracks.recordEvent( 'coupons_view_bulk_action', {
						action: action
					} );
				}
			}
			$( '#doaction' ).on( 'click', { id: 'bulk-action-selector-top' }, onApplyBulkActions );
			$( '#doaction2' ).on( 'click', { id: 'bulk-action-selector-bottom' }, onApplyBulkActions );
		"
		);
	}

	/**
	 * Track page view events.
	 */
	public function tracks_coupons_events() {
		if ( isset( $_GET['post_type'] ) && 'shop_coupon' === $_GET['post_type'] ) {

			$this->tracks_coupons_bulk_actions();

			WC_Tracks::record_event(
				'coupons_view',
				array(
					'status' => isset( $_GET['post_status'] ) ? sanitize_text_field( wp_unslash( $_GET['post_status'] ) ) : 'all',
				)
			);

			if ( isset( $_GET['filter_action'] ) && 'Filter' === sanitize_text_field( wp_unslash( $_GET['filter_action'] ) ) && isset( $_GET['coupon_type'] ) ) {
				WC_Tracks::record_event(
					'coupons_filter',
					array(
						'filter' => 'coupon_type',
						'value'  => sanitize_text_field( wp_unslash( $_GET['coupon_type'] ) ),
					)
				);
			}

			if ( isset( $_GET['s'] ) && 0 < strlen( sanitize_text_field( wp_unslash( $_GET['s'] ) ) ) ) {
				WC_Tracks::record_event( 'coupons_search' );
			}
		}
	}
}
tracks/events/class-wc-products-tracking.php000064400000014356151336044770015250 0ustar00<?php
/**
 * WooCommerce Import Tracking
 *
 * @package WooCommerce\Tracks
 */

use Automattic\Jetpack\Constants;

defined( 'ABSPATH' ) || exit;

/**
 * This class adds actions to track usage of WooCommerce Products.
 */
class WC_Products_Tracking {
	/**
	 * Init tracking.
	 */
	public function init() {
		add_action( 'load-edit.php', array( $this, 'track_products_view' ), 10 );
		add_action( 'load-edit-tags.php', array( $this, 'track_categories_and_tags_view' ), 10, 2 );
		add_action( 'edit_post', array( $this, 'track_product_updated' ), 10, 2 );
		add_action( 'transition_post_status', array( $this, 'track_product_published' ), 10, 3 );
		add_action( 'created_product_cat', array( $this, 'track_product_category_created' ) );
		add_action( 'add_meta_boxes_product', array( $this, 'track_product_updated_client_side' ), 10 );
	}

	/**
	 * Send a Tracks event when the Products page is viewed.
	 */
	public function track_products_view() {
		// We only record Tracks event when no `_wp_http_referer` query arg is set, since
		// when searching, the request gets sent from the browser twice,
		// once with the `_wp_http_referer` and once without it.
		//
		// Otherwise, we would double-record the view and search events.

		// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification
		if (
			isset( $_GET['post_type'] )
			&& 'product' === wp_unslash( $_GET['post_type'] )
			&& ! isset( $_GET['_wp_http_referer'] )
		) {
			// phpcs:enable

			WC_Tracks::record_event( 'products_view' );

			// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification
			if (
				isset( $_GET['s'] )
				&& 0 < strlen( sanitize_text_field( wp_unslash( $_GET['s'] ) ) )
			) {
				// phpcs:enable

				WC_Tracks::record_event( 'products_search' );
			}
		}
	}

	/**
	 * Send a Tracks event when the Products Categories and Tags page is viewed.
	 */
	public function track_categories_and_tags_view() {
		// We only record Tracks event when no `_wp_http_referer` query arg is set, since
		// when searching, the request gets sent from the browser twice,
		// once with the `_wp_http_referer` and once without it.
		//
		// Otherwise, we would double-record the view and search events.

		// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification
		if (
			isset( $_GET['post_type'] )
			&& 'product' === wp_unslash( $_GET['post_type'] )
			&& isset( $_GET['taxonomy'] )
			&& ! isset( $_GET['_wp_http_referer'] )
		) {
			$taxonomy = wp_unslash( $_GET['taxonomy'] );
			// phpcs:enable

			if ( 'product_cat' === $taxonomy ) {
				WC_Tracks::record_event( 'categories_view' );
			} elseif ( 'product_tag' === $taxonomy ) {
				WC_Tracks::record_event( 'tags_view' );
			}

			// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification
			if (
				isset( $_GET['s'] )
				&& 0 < strlen( sanitize_text_field( wp_unslash( $_GET['s'] ) ) )
			) {
				// phpcs:enable

				if ( 'product_cat' === $taxonomy ) {
					WC_Tracks::record_event( 'categories_search' );
				} elseif ( 'product_tag' === $taxonomy ) {
					WC_Tracks::record_event( 'tags_search' );
				}
			}
		}
	}

	/**
	 * Send a Tracks event when a product is updated.
	 *
	 * @param int    $product_id Product id.
	 * @param object $post       WordPress post.
	 */
	public function track_product_updated( $product_id, $post ) {
		if ( 'product' !== $post->post_type ) {
			return;
		}

		$properties = array(
			'product_id' => $product_id,
		);

		WC_Tracks::record_event( 'product_edit', $properties );
	}

	/**
	 * Track the Update button being clicked on the client side.
	 * This is needed because `track_product_updated` (using the `edit_post`
	 * hook) is called in response to a number of other triggers.
	 *
	 * @param WP_Post $post The post, not used.
	 */
	public function track_product_updated_client_side( $post ) {
		wc_enqueue_js(
			"
			if ( $( 'h1.wp-heading-inline' ).text().trim() === '" . __( 'Edit product', 'woocommerce' ) . "') {
				var initialStockValue = $( '#_stock' ).val();
				var hasRecordedEvent = false;

				$( '#publish' ).on( 'click', function() {
					if ( hasRecordedEvent ) {
						return;
					}

					var currentStockValue = $( '#_stock' ).val();
					var properties = {
						product_type:			$( '#product-type' ).val(),
						is_virtual:				$( '#_virtual' ).is( ':checked' ) ? 'Y' : 'N',
						is_downloadable:		$( '#_downloadable' ).is( ':checked' ) ? 'Y' : 'N',
						manage_stock:			$( '#_manage_stock' ).is( ':checked' ) ? 'Y' : 'N',
						stock_quantity_update:	( initialStockValue != currentStockValue ) ? 'Y' : 'N',
					};

					window.wcTracks.recordEvent( 'product_update', properties );
					hasRecordedEvent = true;
				} );
			}
			"
		);
	}

	/**
	 * Send a Tracks event when a product is published.
	 *
	 * @param string $new_status New post_status.
	 * @param string $old_status Previous post_status.
	 * @param object $post WordPress post.
	 */
	public function track_product_published( $new_status, $old_status, $post ) {
		if (
			'product' !== $post->post_type ||
			'publish' !== $new_status ||
			'publish' === $old_status
		) {
			return;
		}

		$properties = array(
			'product_id' => $post->ID,
		);

		WC_Tracks::record_event( 'product_add_publish', $properties );
	}

	/**
	 * Send a Tracks event when a product category is created.
	 *
	 * @param int $category_id Category ID.
	 */
	public function track_product_category_created( $category_id ) {
		// phpcs:disable WordPress.Security.NonceVerification.Missing
		// Only track category creation from the edit product screen or the
		// category management screen (which both occur via AJAX).
		if (
			! Constants::is_defined( 'DOING_AJAX' ) ||
			empty( $_POST['action'] ) ||
			(
				// Product Categories screen.
				'add-tag' !== $_POST['action'] &&
				// Edit Product screen.
				'add-product_cat' !== $_POST['action']
			)
		) {
			return;
		}

		$category   = get_term( $category_id, 'product_cat' );
		$properties = array(
			'category_id' => $category_id,
			'parent_id'   => $category->parent,
			'page'        => ( 'add-tag' === $_POST['action'] ) ? 'categories' : 'product',
		);
		// phpcs:enable

		WC_Tracks::record_event( 'product_category_add', $properties );
	}
}
tracks/events/class-wc-importer-tracking.php000064400000004313151336044770015236 0ustar00<?php
/**
 * WooCommerce Import Tracking
 *
 * @package WooCommerce\Tracks
 */

defined( 'ABSPATH' ) || exit;

/**
 * This class adds actions to track usage of WooCommerce Imports.
 */
class WC_Importer_Tracking {
	/**
	 * Init tracking.
	 */
	public function init() {
		add_action( 'product_page_product_importer', array( $this, 'track_product_importer' ) );
	}

	/**
	 * Route product importer action to the right callback.
	 *
	 * @return void
	 */
	public function track_product_importer() {
		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		if ( ! isset( $_REQUEST['step'] ) ) {
			return;
		}

		if ( 'import' === $_REQUEST['step'] ) {
			return $this->track_product_importer_start();
		}

		if ( 'done' === $_REQUEST['step'] ) {
			return $this->track_product_importer_complete();
		}
		// phpcs:enable
	}

	/**
	 * Send a Tracks event when the product importer is started.
	 *
	 * @return void
	 */
	public function track_product_importer_start() {
		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		if ( ! isset( $_REQUEST['file'] ) || ! isset( $_REQUEST['_wpnonce'] ) ) {
			return;
		}

		$properties = array(
			'update_existing' => isset( $_REQUEST['update_existing'] ) ? (bool) $_REQUEST['update_existing'] : false,
			'delimiter'       => empty( $_REQUEST['delimiter'] ) ? ',' : wc_clean( wp_unslash( $_REQUEST['delimiter'] ) ),
		);
		// phpcs:enable

		WC_Tracks::record_event( 'product_import_start', $properties );
	}

	/**
	 * Send a Tracks event when the product importer has finished.
	 *
	 * @return void
	 */
	public function track_product_importer_complete() {
		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		if ( ! isset( $_REQUEST['nonce'] ) ) {
			return;
		}

		$properties = array(
			'imported' => isset( $_GET['products-imported'] ) ? absint( $_GET['products-imported'] ) : 0,
			'updated'  => isset( $_GET['products-updated'] ) ? absint( $_GET['products-updated'] ) : 0,
			'failed'   => isset( $_GET['products-failed'] ) ? absint( $_GET['products-failed'] ) : 0,
			'skipped'  => isset( $_GET['products-skipped'] ) ? absint( $_GET['products-skipped'] ) : 0,
		);
		// phpcs:enable

		WC_Tracks::record_event( 'product_import_complete', $properties );
	}
}
tracks/events/class-wc-order-tracking.php000064400000001611151336044770014506 0ustar00<?php
/**
 * WooCommerce Order Tracking
 *
 * @package WooCommerce\Tracks
 */

defined( 'ABSPATH' ) || exit;

/**
 * This class adds actions to track usage of a WooCommerce Order.
 */
class WC_Order_Tracking {

	/**
	 * Init tracking.
	 */
	public function init() {
		add_action( 'woocommerce_admin_order_data_after_order_details', array( $this, 'track_order_viewed' ) );
	}

	/**
	 * Send a Tracks event when an order is viewed.
	 *
	 * @param WC_Order $order Order.
	 */
	public function track_order_viewed( $order ) {
		if ( ! $order instanceof WC_Order || ! $order->get_id() ) {
			return;
		}
		$properties = array(
			'current_status' => $order->get_status(),
			'date_created'   => $order->get_date_created() ? $order->get_date_created()->format( DateTime::ATOM ) : '',
			'payment_method' => $order->get_payment_method(),
		);

		WC_Tracks::record_event( 'single_order_view', $properties );
	}
}

tracks/events/class-wc-settings-tracking.php000064400000006123151336044770015236 0ustar00<?php
/**
 * WooCommerce Settings Tracking
 *
 * @package WooCommerce\Tracks
 */

defined( 'ABSPATH' ) || exit;

/**
 * This class adds actions to track usage of WooCommerce Settings.
 */
class WC_Settings_Tracking {

	/**
	 * List of allowed WooCommerce settings to potentially track updates for.
	 *
	 * @var array
	 */
	protected $allowed_options = array();

	/**
	 * WooCommerce settings that have been updated (and will be tracked).
	 *
	 * @var array
	 */
	protected $updated_options = array();

	/**
	 * Init tracking.
	 */
	public function init() {
		add_action( 'woocommerce_settings_page_init', array( $this, 'track_settings_page_view' ) );
		add_action( 'woocommerce_update_option', array( $this, 'add_option_to_list' ) );
		add_action( 'woocommerce_update_options', array( $this, 'send_settings_change_event' ) );
	}

	/**
	 * Add a WooCommerce option name to our allowed options list and attach
	 * the `update_option` hook. Rather than inspecting every updated
	 * option and pattern matching for "woocommerce", just build a dynamic
	 * list for WooCommerce options that might get updated.
	 *
	 * See `woocommerce_update_option` hook.
	 *
	 * @param array $option WooCommerce option (config) that might get updated.
	 */
	public function add_option_to_list( $option ) {
		$this->allowed_options[] = $option['id'];

		// Delay attaching this action since it could get fired a lot.
		if ( false === has_action( 'update_option', array( $this, 'track_setting_change' ) ) ) {
			add_action( 'update_option', array( $this, 'track_setting_change' ), 10, 3 );
		}
	}

	/**
	 * Add WooCommerce option to a list of updated options.
	 *
	 * @param string $option_name Option being updated.
	 * @param mixed  $old_value Old value of option.
	 * @param mixed  $new_value New value of option.
	 */
	public function track_setting_change( $option_name, $old_value, $new_value ) {
		// Make sure this is a WooCommerce option.
		if ( ! in_array( $option_name, $this->allowed_options, true ) ) {
			return;
		}

		// Check to make sure the new value is truly different.
		// `woocommerce_price_num_decimals` tends to trigger this
		// because form values aren't coerced (e.g. '2' vs. 2).
		if (
			is_scalar( $old_value ) &&
			is_scalar( $new_value ) &&
			(string) $old_value === (string) $new_value
		) {
			return;
		}

		$this->updated_options[] = $option_name;
	}

	/**
	 * Send a Tracks event for WooCommerce options that changed values.
	 */
	public function send_settings_change_event() {
		global $current_tab;

		if ( empty( $this->updated_options ) ) {
			return;
		}

		$properties = array(
			'settings' => implode( ',', $this->updated_options ),
		);

		if ( isset( $current_tab ) ) {
			$properties['tab'] = $current_tab;
		}

		WC_Tracks::record_event( 'settings_change', $properties );
	}

	/**
	 * Send a Tracks event for WooCommerce settings page views.
	 */
	public function track_settings_page_view() {
		global $current_tab, $current_section;

		$properties = array(
			'tab'     => $current_tab,
			'section' => empty( $current_section ) ? null : $current_section,
		);

		WC_Tracks::record_event( 'settings_view', $properties );
	}
}
tracks/events/class-wc-admin-setup-wizard-tracking.php000064400000011425151336044770017123 0ustar00<?php
/**
 * WooCommerce Admin Setup Wizard Tracking
 *
 * @package WooCommerce\Tracks
 *
 * @deprecated 4.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * This class adds actions to track usage of the WooCommerce Onboarding Wizard.
 */
class WC_Admin_Setup_Wizard_Tracking {
	/**
	 * Steps for the setup wizard
	 *
	 * @var array
	 */
	private $steps = array();

	/**
	 * Init tracking.
	 *
	 * @deprecated 4.6.0
	 */
	public function init() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
	}

	/**
	 * Get the name of the current step.
	 *
	 * @deprecated 4.6.0
	 * @return string
	 */
	public function get_current_step() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
		return isset( $_GET['step'] ) ? sanitize_key( $_GET['step'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
	}

	/**
	 * Add footer scripts to OBW via woocommerce_setup_footer
	 *
	 * @deprecated 4.6.0
	 */
	public function add_footer_scripts() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
	}

	/**
	 * Dequeue unwanted scripts from OBW footer.
	 *
	 * @deprecated 4.6.0
	 */
	public function dequeue_non_allowed_scripts() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
		global $wp_scripts;
		$allowed = array( 'woo-tracks' );

		foreach ( $wp_scripts->queue as $script ) {
			if ( in_array( $script, $allowed, true ) ) {
				continue;
			}
			wp_dequeue_script( $script );
		}
	}

	/**
	 * Track when tracking is opted into and OBW has started.
	 *
	 * @param string $option Option name.
	 * @param string $value  Option value.
	 *
	 * @deprecated 4.6.0
	 */
	public function track_start( $option, $value ) {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
	}

	/**
	 * Track the marketing form on submit.
	 *
	 * @deprecated 4.6.0
	 */
	public function track_ready_next_steps() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
	}

	/**
	 * Track various events when a step is saved.
	 *
	 * @deprecated 4.6.0
	 */
	public function add_step_save_events() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
	}

	/**
	 * Track store setup and store properties on save.
	 *
	 * @deprecated 4.6.0
	 */
	public function track_store_setup() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
	}

	/**
	 * Track payment gateways selected.
	 *
	 * @deprecated 4.6.0
	 */
	public function track_payments() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
	}

	/**
	 * Track shipping units and whether or not labels are set.
	 *
	 * @deprecated 4.6.0
	 */
	public function track_shipping() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
	}

	/**
	 * Track recommended plugins selected for install.
	 *
	 * @deprecated 4.6.0
	 */
	public function track_recommended() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
	}

	/**
	 * Tracks when Jetpack is activated through the OBW.
	 *
	 * @deprecated 4.6.0
	 */
	public function track_jetpack_activate() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
	}

	/**
	 * Tracks when last next_steps screen is viewed in the OBW.
	 *
	 * @deprecated 4.6.0
	 */
	public function track_next_steps() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
	}

	/**
	 * Track skipped steps.
	 *
	 * @deprecated 4.6.0
	 */
	public function track_skip_step() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
	}

	/**
	 * Set the OBW steps inside this class instance.
	 *
	 * @param array $steps Array of OBW steps.
	 *
	 * @deprecated 4.6.0
	 */
	public function set_obw_steps( $steps ) {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
		$this->steps = $steps;

		return $steps;
	}
}
tracks/events/class-wc-extensions-tracking.php000064400000006103151336044770015573 0ustar00<?php
/**
 * WooCommerce Extensions Tracking
 *
 * @package WooCommerce\Tracks
 */

defined( 'ABSPATH' ) || exit;

/**
 * This class adds actions to track usage of the WooCommerce Extensions page.
 */
class WC_Extensions_Tracking {
	/**
	 * Init tracking.
	 */
	public function init() {
		add_action( 'load-woocommerce_page_wc-addons', array( $this, 'track_extensions_page' ) );
		add_action( 'woocommerce_helper_connect_start', array( $this, 'track_helper_connection_start' ) );
		add_action( 'woocommerce_helper_denied', array( $this, 'track_helper_connection_cancelled' ) );
		add_action( 'woocommerce_helper_connected', array( $this, 'track_helper_connection_complete' ) );
		add_action( 'woocommerce_helper_disconnected', array( $this, 'track_helper_disconnected' ) );
		add_action( 'woocommerce_helper_subscriptions_refresh', array( $this, 'track_helper_subscriptions_refresh' ) );
		add_action( 'woocommerce_addon_installed', array( $this, 'track_addon_install' ), 10, 2 );
	}

	/**
	 * Send a Tracks event when an Extensions page is viewed.
	 */
	public function track_extensions_page() {
		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		$properties = array(
			'section' => empty( $_REQUEST['section'] ) ? '_featured' : wc_clean( wp_unslash( $_REQUEST['section'] ) ),
		);

		$event      = 'extensions_view';
		if ( 'helper' === $properties['section'] ) {
			$event = 'subscriptions_view';
		}

		if ( ! empty( $_REQUEST['search'] ) ) {
			$event                     = 'extensions_view_search';
			$properties['search_term'] = wc_clean( wp_unslash( $_REQUEST['search'] ) );
		}
		// phpcs:enable

		WC_Tracks::record_event( $event, $properties );
	}

	/**
	 * Send a Tracks even when a Helper connection process is initiated.
	 */
	public function track_helper_connection_start() {
		WC_Tracks::record_event( 'extensions_subscriptions_connect' );
	}

	/**
	 * Send a Tracks even when a Helper connection process is cancelled.
	 */
	public function track_helper_connection_cancelled() {
		WC_Tracks::record_event( 'extensions_subscriptions_cancelled' );
	}

	/**
	 * Send a Tracks even when a Helper connection process completed successfully.
	 */
	public function track_helper_connection_complete() {
		WC_Tracks::record_event( 'extensions_subscriptions_connected' );
	}

	/**
	 * Send a Tracks even when a Helper has been disconnected.
	 */
	public function track_helper_disconnected() {
		WC_Tracks::record_event( 'extensions_subscriptions_disconnect' );
	}

	/**
	 * Send a Tracks even when Helper subscriptions are refreshed.
	 */
	public function track_helper_subscriptions_refresh() {
		WC_Tracks::record_event( 'extensions_subscriptions_update' );
	}

	/**
	 * Send a Tracks event when addon is installed via the Extensions page.
	 *
	 * @param string $addon_id Addon slug.
	 * @param string $section  Extensions tab.
	 */
	public function track_addon_install( $addon_id, $section ) {
		$properties = array(
			'context' => 'extensions',
			'section' => $section,
		);

		if ( 'woocommerce-payments' === $addon_id ) {
			WC_Tracks::record_event( 'woocommerce_payments_install', $properties );
		}
	}
}
tracks/events/class-wc-orders-tracking.php000064400000013575151336044770014705 0ustar00<?php
/**
 * WooCommerce Orders Tracking
 *
 * @package WooCommerce\Tracks
 */

defined( 'ABSPATH' ) || exit;

/**
 * This class adds actions to track usage of WooCommerce Orders.
 */
class WC_Orders_Tracking {
	/**
	 * Init tracking.
	 */
	public function init() {
		add_action( 'woocommerce_order_status_changed', array( $this, 'track_order_status_change' ), 10, 3 );
		add_action( 'load-edit.php', array( $this, 'track_orders_view' ), 10 );
		add_action( 'pre_post_update', array( $this, 'track_created_date_change' ), 10 );
		// WC_Meta_Box_Order_Actions::save() hooks in at priority 50.
		add_action( 'woocommerce_process_shop_order_meta', array( $this, 'track_order_action' ), 51 );
		add_action( 'load-post-new.php', array( $this, 'track_add_order_from_edit' ), 10 );
		add_filter( 'woocommerce_shop_order_search_results', array( $this, 'track_order_search' ), 10, 3 );
	}

	/**
	 * Send a track event when on the Order Listing page, and search results are being displayed.
	 *
	 * @param array  $order_ids Array of order_ids that are matches for the search.
	 * @param string $term The string that was used in the search.
	 * @param array  $search_fields Fields that were used in the original search.
	 */
	public function track_order_search( $order_ids, $term, $search_fields ) {
		// Since `woocommerce_shop_order_search_results` can run in the front-end context, exit if get_current_screen isn't defined.
		if ( ! function_exists( 'get_current_screen' ) ) {
			return $order_ids;
		}

		$screen = get_current_screen();

		// We only want to record this track when the filter is executed on the order listing page.
		if ( 'edit-shop_order' === $screen->id ) {
			// we are on the order listing page, and query results are being shown.
			WC_Tracks::record_event( 'orders_view_search' );
		}

		return $order_ids;
	}

	/**
	 * Send a Tracks event when the Orders page is viewed.
	 */
	public function track_orders_view() {
		if ( isset( $_GET['post_type'] ) && 'shop_order' === wp_unslash( $_GET['post_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

			// phpcs:disable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput
			$properties = array(
				'status' => isset( $_GET['post_status'] ) ? sanitize_text_field( $_GET['post_status'] ) : 'all',
			);
			// phpcs:enable

			WC_Tracks::record_event( 'orders_view', $properties );
		}
	}

	/**
	 * Send a Tracks event when an order status is changed.
	 *
	 * @param int    $id Order id.
	 * @param string $previous_status the old WooCommerce order status.
	 * @param string $next_status the new WooCommerce order status.
	 */
	public function track_order_status_change( $id, $previous_status, $next_status ) {
		$order = wc_get_order( $id );

		$properties = array(
			'order_id'        => $id,
			'next_status'     => $next_status,
			'previous_status' => $previous_status,
			'date_created'    => $order->get_date_created() ? $order->get_date_created()->date( 'Y-m-d' ) : '',
			'payment_method'  => $order->get_payment_method(),
			'order_total'     => $order->get_total(),
		);

		WC_Tracks::record_event( 'orders_edit_status_change', $properties );
	}

	/**
	 * Send a Tracks event when an order date is changed.
	 *
	 * @param int $id Order id.
	 */
	public function track_created_date_change( $id ) {
		$post_type = get_post_type( $id );

		if ( 'shop_order' !== $post_type ) {
			return;
		}

		if ( 'auto-draft' === get_post_status( $id ) ) {
			return;
		}

		$order        = wc_get_order( $id );
		$date_created = $order->get_date_created() ? $order->get_date_created()->date( 'Y-m-d H:i:s' ) : '';
		// phpcs:disable WordPress.Security.NonceVerification
		$new_date = sprintf(
			'%s %2d:%2d:%2d',
			isset( $_POST['order_date'] ) ? wc_clean( wp_unslash( $_POST['order_date'] ) ) : '',
			isset( $_POST['order_date_hour'] ) ? wc_clean( wp_unslash( $_POST['order_date_hour'] ) ) : '',
			isset( $_POST['order_date_minute'] ) ? wc_clean( wp_unslash( $_POST['order_date_minute'] ) ) : '',
			isset( $_POST['order_date_second'] ) ? wc_clean( wp_unslash( $_POST['order_date_second'] ) ) : ''
		);
		// phpcs:enable

		if ( $new_date !== $date_created ) {
			$properties = array(
				'order_id' => $id,
				'status'   => $order->get_status(),
			);

			WC_Tracks::record_event( 'order_edit_date_created', $properties );
		}
	}

	/**
	 * Track order actions taken.
	 *
	 * @param int $order_id Order ID.
	 */
	public function track_order_action( $order_id ) {
		// phpcs:disable WordPress.Security.NonceVerification
		if ( ! empty( $_POST['wc_order_action'] ) ) {
			$order      = wc_get_order( $order_id );
			$action     = wc_clean( wp_unslash( $_POST['wc_order_action'] ) );
			$properties = array(
				'order_id' => $order_id,
				'status'   => $order->get_status(),
				'action'   => $action,
			);

			WC_Tracks::record_event( 'order_edit_order_action', $properties );
		}
		// phpcs:enable
	}

	/**
	 * Track "add order" button on the Edit Order screen.
	 */
	public function track_add_order_from_edit() {
		// phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		if ( isset( $_GET['post_type'] ) && 'shop_order' === wp_unslash( $_GET['post_type'] ) ) {
			$referer = wp_get_referer();

			if ( $referer ) {
				$referring_page = wp_parse_url( $referer );
				$referring_args = array();
				$post_edit_page = wp_parse_url( admin_url( 'post.php' ) );

				if ( ! empty( $referring_page['query'] ) ) {
					parse_str( $referring_page['query'], $referring_args );
				}

				// Determine if we arrived from an Order Edit screen.
				if (
					$post_edit_page['path'] === $referring_page['path'] &&
					isset( $referring_args['action'] ) &&
					'edit' === $referring_args['action'] &&
					isset( $referring_args['post'] ) &&
					'shop_order' === get_post_type( $referring_args['post'] )
				) {
					WC_Tracks::record_event( 'order_edit_add_order' );
				}
			}
		}
	}
}
tracks/events/class-wc-coupon-tracking.php000064400000002205151336044770014676 0ustar00<?php
/**
 * WooCommerce Coupon Tracking
 *
 * @package WooCommerce\Tracks
 */

/**
 * This class adds actions to track usage of a WooCommerce Coupon.
 */
class WC_Coupon_Tracking {

	/**
	 * Init tracking.
	 */
	public function init() {
		add_action( 'woocommerce_coupon_object_updated_props', array( $this, 'track_coupon_updated' ), 10, 2 );
	}

	/**
	 * Send a Tracks event when a coupon is updated.
	 *
	 * @param WC_Coupon $coupon        The coupon that has been updated.
	 * @param Array     $updated_props The props of the coupon that have been updated.
	 */
	public function track_coupon_updated( $coupon, $updated_props ) {
		$properties = array(
			'discount_code'        => $coupon->get_code(),
			'free_shipping'        => $coupon->get_free_shipping(),
			'individual_use'       => $coupon->get_individual_use(),
			'exclude_sale_items'   => $coupon->get_exclude_sale_items(),
			'usage_limits_applied' => 0 < intval( $coupon->get_usage_limit() )
									|| 0 < intval( $coupon->get_usage_limit_per_user() )
									|| 0 < intval( $coupon->get_limit_usage_to_x_items() ),
		);

		WC_Tracks::record_event( 'coupon_updated', $properties );
	}
}
tracks/events/class-wc-status-tracking.php000064400000002025151336044770014716 0ustar00<?php
/**
 * WooCommerce Status Tracking
 *
 * @package WooCommerce\Tracks
 */

defined( 'ABSPATH' ) || exit;

/**
 * This class adds actions to track usage of WooCommerce Orders.
 */
class WC_Status_Tracking {
	/**
	 * Init tracking.
	 */
	public function init() {
		add_action( 'admin_init', array( $this, 'track_status_view' ), 10 );
	}

	/**
	 * Add Tracks events to the status page.
	 */
	public function track_status_view() {
		if ( isset( $_GET['page'] ) && 'wc-status' === sanitize_text_field( wp_unslash( $_GET['page'] ) ) ) {

			$tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : 'status';

			WC_Tracks::record_event(
				'status_view',
				array(
					'tab'       => $tab,
					'tool_used' => isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : null,
				)
			);

			if ( 'status' === $tab ) {
				wc_enqueue_js(
					"
					$( 'a.debug-report' ).on( 'click', function() {
						window.wcTracks.recordEvent( 'status_view_reports' );
					} );
				"
				);
			}
		}
	}
}
tracks/class-wc-site-tracking.php000064400000013354151336044770013042 0ustar00<?php
/**
 * Nosara Tracks for WooCommerce
 *
 * @package WooCommerce\Tracks
 */

defined( 'ABSPATH' ) || exit;

/**
 * This class adds actions to track usage of WooCommerce.
 */
class WC_Site_Tracking {
	/**
	 * Check if tracking is enabled.
	 *
	 * @return bool
	 */
	public static function is_tracking_enabled() {
		/**
		 * Don't track users if a filter has been applied to turn it off.
		 * `woocommerce_apply_tracking` will be deprecated. Please use
		 * `woocommerce_apply_user_tracking` instead.
		 */
		if ( ! apply_filters( 'woocommerce_apply_user_tracking', true ) || ! apply_filters( 'woocommerce_apply_tracking', true ) ) {
			return false;
		}

		// Check if tracking is actively being opted into.
		$is_obw_opting_in = isset( $_POST['wc_tracker_checkbox'] ) && 'yes' === sanitize_text_field( $_POST['wc_tracker_checkbox'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput

		/**
		 * Don't track users who haven't opted-in to tracking or aren't in
		 * the process of opting-in.
		 */
		if ( 'yes' !== get_option( 'woocommerce_allow_tracking' ) && ! $is_obw_opting_in ) {
			return false;
		}

		if ( ! class_exists( 'WC_Tracks' ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Register scripts required to record events from javascript.
	 */
	public static function register_scripts() {
		wp_register_script( 'woo-tracks', 'https://stats.wp.com/w.js', array( 'wp-hooks' ), gmdate( 'YW' ), false );
	}

	/**
	 * Add scripts required to record events from javascript.
	 */
	public static function enqueue_scripts() {
		wp_enqueue_script( 'woo-tracks' );
	}

	/**
	 * Adds the tracking function to the admin footer.
	 */
	public static function add_tracking_function() {
		?>
		<!-- WooCommerce Tracks -->
		<script type="text/javascript">
			window.wcTracks = window.wcTracks || {};
			window.wcTracks.isEnabled = <?php echo self::is_tracking_enabled() ? 'true' : 'false'; ?>;
			window.wcTracks.recordEvent = function( name, properties ) {
				if ( ! window.wcTracks.isEnabled ) {
					return;
				}

				var eventName = '<?php echo esc_attr( WC_Tracks::PREFIX ); ?>' + name;
				var eventProperties = properties || {};
				eventProperties.url = '<?php echo esc_html( home_url() ); ?>'
				eventProperties.products_count = '<?php echo intval( WC_Tracks::get_products_count() ); ?>';
				if ( window.wp && window.wp.hooks && window.wp.hooks.applyFilters ) {
					eventProperties = window.wp.hooks.applyFilters( 'woocommerce_tracks_client_event_properties', eventProperties, eventName );
					delete( eventProperties._ui );
					delete( eventProperties._ut );
				}
				window._tkq = window._tkq || [];
				window._tkq.push( [ 'recordEvent', eventName, eventProperties ] );
			}
		</script>
		<?php
	}

	/**
	 * Adds a function to load tracking scripts and enable them client-side on the fly.
	 * Note that this function does not update `woocommerce_allow_tracking` in the database
	 * and will not persist enabled tracking across page loads.
	 */
	public static function add_enable_tracking_function() {
		global $wp_scripts;

		if ( ! isset( $wp_scripts->registered['woo-tracks'] ) ) {
			return;
		}

		$woo_tracks_script = $wp_scripts->registered['woo-tracks']->src;

		?>
		<script type="text/javascript">
			window.wcTracks.enable = function( callback ) {
				window.wcTracks.isEnabled = true;

				var scriptUrl = '<?php echo esc_url( $woo_tracks_script ); ?>';
				var existingScript = document.querySelector( `script[src="${ scriptUrl }"]` );
				if ( existingScript ) {
					return;
				}

				var script = document.createElement('script');
				script.src = scriptUrl;
				document.body.append(script);

				// Callback after scripts have loaded.
				script.onload = function() {
					if ( 'function' === typeof callback ) {
						callback( true );
					}
				}

				// Callback triggered if the script fails to load.
				script.onerror = function() {
					if ( 'function' === typeof callback ) {
						callback( false );
					}
				}
			}
		</script>
		<?php
	}

	/**
	 * Init tracking.
	 */
	public static function init() {

		// Define window.wcTracks.recordEvent in case it is enabled client-side.
		self::register_scripts();
		add_filter( 'admin_footer', array( __CLASS__, 'add_tracking_function' ), 24 );

		if ( ! self::is_tracking_enabled() ) {
			add_filter( 'admin_footer', array( __CLASS__, 'add_enable_tracking_function' ), 24 );
			return;
		}

		self::enqueue_scripts();

		include_once WC_ABSPATH . 'includes/tracks/events/class-wc-admin-setup-wizard-tracking.php';
		include_once WC_ABSPATH . 'includes/tracks/events/class-wc-extensions-tracking.php';
		include_once WC_ABSPATH . 'includes/tracks/events/class-wc-importer-tracking.php';
		include_once WC_ABSPATH . 'includes/tracks/events/class-wc-products-tracking.php';
		include_once WC_ABSPATH . 'includes/tracks/events/class-wc-orders-tracking.php';
		include_once WC_ABSPATH . 'includes/tracks/events/class-wc-settings-tracking.php';
		include_once WC_ABSPATH . 'includes/tracks/events/class-wc-status-tracking.php';
		include_once WC_ABSPATH . 'includes/tracks/events/class-wc-coupons-tracking.php';
		include_once WC_ABSPATH . 'includes/tracks/events/class-wc-order-tracking.php';
		include_once WC_ABSPATH . 'includes/tracks/events/class-wc-coupon-tracking.php';

		$tracking_classes = array(
			'WC_Extensions_Tracking',
			'WC_Importer_Tracking',
			'WC_Products_Tracking',
			'WC_Orders_Tracking',
			'WC_Settings_Tracking',
			'WC_Status_Tracking',
			'WC_Coupons_Tracking',
			'WC_Order_Tracking',
			'WC_Coupon_Tracking',
		);

		foreach ( $tracking_classes as $tracking_class ) {
			$tracker_instance    = new $tracking_class();
			$tracker_init_method = array( $tracker_instance, 'init' );

			if ( is_callable( $tracker_init_method ) ) {
				call_user_func( $tracker_init_method );
			}
		}
	}
}
tracks/class-wc-tracks.php000064400000007232151336044770011563 0ustar00<?php
/**
 * PHP Tracks Client
 *
 * @package WooCommerce\Tracks
 */

/**
 * WC_Tracks class.
 */
class WC_Tracks {

	/**
	 * Tracks event name prefix.
	 */
	const PREFIX = 'wcadmin_';

	/**
	 * Get total product counts.
	 *
	 * @return int Number of products.
	 */
	public static function get_products_count() {
		$product_counts = WC_Tracker::get_product_counts();
		return $product_counts['total'];
	}

	/**
	 * Gather blog related properties.
	 *
	 * @param int $user_id User id.
	 * @return array Blog details.
	 */
	public static function get_blog_details( $user_id ) {
		$blog_details = get_transient( 'wc_tracks_blog_details' );
		if ( false === $blog_details ) {
			$blog_details = array(
				'url'            => home_url(),
				'blog_lang'      => get_user_locale( $user_id ),
				'blog_id'        => class_exists( 'Jetpack_Options' ) ? Jetpack_Options::get_option( 'id' ) : null,
				'products_count' => self::get_products_count(),
				'wc_version'     => WC()->version,
			);
			set_transient( 'wc_tracks_blog_details', $blog_details, DAY_IN_SECONDS );
		}
		return $blog_details;
	}

	/**
	 * Gather details from the request to the server.
	 *
	 * @return array Server details.
	 */
	public static function get_server_details() {
		$data = array();

		$data['_via_ua'] = isset( $_SERVER['HTTP_USER_AGENT'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '';
		$data['_via_ip'] = isset( $_SERVER['REMOTE_ADDR'] ) ? wc_clean( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
		$data['_lg']     = isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) : '';
		$data['_dr']     = isset( $_SERVER['HTTP_REFERER'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '';

		$uri         = isset( $_SERVER['REQUEST_URI'] ) ? wc_clean( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
		$host        = isset( $_SERVER['HTTP_HOST'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '';
		$data['_dl'] = isset( $_SERVER['REQUEST_SCHEME'] ) ? wc_clean( wp_unslash( $_SERVER['REQUEST_SCHEME'] ) ) . '://' . $host . $uri : '';

		return $data;
	}

	/**
	 * Record an event in Tracks - this is the preferred way to record events from PHP.
	 *
	 * @param string $event_name The name of the event.
	 * @param array  $properties Custom properties to send with the event.
	 * @return bool|WP_Error True for success or WP_Error if the event pixel could not be fired.
	 */
	public static function record_event( $event_name, $properties = array() ) {
		/**
		 * Don't track users who don't have tracking enabled.
		 */
		if ( ! WC_Site_Tracking::is_tracking_enabled() ) {
			return false;
		}

		$user = wp_get_current_user();

		// We don't want to track user events during unit tests/CI runs.
		if ( $user instanceof WP_User && 'wptests_capabilities' === $user->cap_key ) {
			return false;
		}
		$prefixed_event_name = self::PREFIX . $event_name;

		$data = array(
			'_en' => $prefixed_event_name,
			'_ts' => WC_Tracks_Client::build_timestamp(),
		);

		$server_details = self::get_server_details();
		$identity       = WC_Tracks_Client::get_identity( $user->ID );
		$blog_details   = self::get_blog_details( $user->ID );

		// Allow event props to be filtered to enable adding site-wide props.
		$filtered_properties = apply_filters( 'woocommerce_tracks_event_properties', $properties, $prefixed_event_name );

		// Delete _ui and _ut protected properties.
		unset( $filtered_properties['_ui'] );
		unset( $filtered_properties['_ut'] );

		$event_obj = new WC_Tracks_Event( array_merge( $data, $server_details, $identity, $blog_details, $filtered_properties ) );

		if ( is_wp_error( $event_obj->error ) ) {
			return $event_obj->error;
		}

		return $event_obj->record();
	}
}
tracks/class-wc-tracks-event.php000064400000007131151336044770012700 0ustar00<?php
/**
 * This class represents an event used to record a Tracks event
 *
 * @package WooCommerce\Tracks
 */

use Automattic\Jetpack\Constants;

defined( 'ABSPATH' ) || exit;

/**
 * WC_Tracks_Event class.
 */
class WC_Tracks_Event {

	/**
	 * Event name regex.
	 */
	const EVENT_NAME_REGEX = '/^(([a-z0-9]+)_){2}([a-z0-9_]+)$/';

	/**
	 * Property name regex.
	 */
	const PROP_NAME_REGEX = '/^[a-z_][a-z0-9_]*$/';

	/**
	 * Error message as WP_Error.
	 *
	 * @var WP_Error
	 */
	public $error;

	/**
	 * WC_Tracks_Event constructor.
	 *
	 * @param array $event Event properties.
	 */
	public function __construct( $event ) {
		$_event = self::validate_and_sanitize( $event );
		if ( is_wp_error( $_event ) ) {
			$this->error = $_event;
			return;
		}

		foreach ( $_event as $key => $value ) {
			$this->{$key} = $value;
		}
	}

	/**
	 * Record Tracks event
	 *
	 * @return bool Always returns true.
	 */
	public function record() {
		if ( wp_doing_ajax() || Constants::is_true( 'REST_REQUEST' ) ) {
			return WC_Tracks_Client::record_event( $this );
		}

		return WC_Tracks_Footer_Pixel::record_event( $this );
	}

	/**
	 * Annotate the event with all relevant info.
	 *
	 * @param  array $event Event arguments.
	 * @return bool|WP_Error True on success, WP_Error on failure.
	 */
	public static function validate_and_sanitize( $event ) {
		$event = (object) $event;

		// Required.
		if ( ! $event->_en ) {
			return new WP_Error( 'invalid_event', 'A valid event must be specified via `_en`', 400 );
		}

		// Delete non-routable addresses otherwise geoip will discard the record entirely.
		if ( property_exists( $event, '_via_ip' ) && preg_match( '/^192\.168|^10\./', $event->_via_ip ) ) {
			unset( $event->_via_ip );
		}

		$validated = array(
			'browser_type' => WC_Tracks_Client::BROWSER_TYPE,
		);

		$_event = (object) array_merge( (array) $event, $validated );

		// If you want to block property names, do it here.
		// Make sure we have an event timestamp.
		if ( ! isset( $_event->_ts ) ) {
			$_event->_ts = WC_Tracks_Client::build_timestamp();
		}

		return $_event;
	}

	/**
	 * Build a pixel URL that will send a Tracks event when fired.
	 * On error, returns an empty string ('').
	 *
	 * @return string A pixel URL or empty string ('') if there were invalid args.
	 */
	public function build_pixel_url() {
		if ( $this->error ) {
			return '';
		}

		$args = get_object_vars( $this );

		// Request Timestamp and URL Terminator must be added just before the HTTP request or not at all.
		unset( $args['_rt'], $args['_'] );

		$validated = self::validate_and_sanitize( $args );

		if ( is_wp_error( $validated ) ) {
			return '';
		}

		return esc_url_raw( WC_Tracks_Client::PIXEL . '?' . http_build_query( $validated ) );
	}

	/**
	 * Check if event name is valid.
	 *
	 * @param string $name Event name.
	 * @return false|int
	 */
	public static function event_name_is_valid( $name ) {
		return preg_match( self::EVENT_NAME_REGEX, $name );
	}

	/**
	 * Check if a property name is valid.
	 *
	 * @param string $name Event property.
	 * @return false|int
	 */
	public static function prop_name_is_valid( $name ) {
		return preg_match( self::PROP_NAME_REGEX, $name );
	}

	/**
	 * Check event names
	 *
	 * @param object $event An event object.
	 */
	public static function scrutinize_event_names( $event ) {
		if ( ! self::event_name_is_valid( $event->_en ) ) {
			return;
		}

		$allowed_key_names = array(
			'anonId',
			'Browser_Type',
		);

		foreach ( array_keys( (array) $event ) as $key ) {
			if ( in_array( $key, $allowed_key_names, true ) ) {
				continue;
			}
			if ( ! self::prop_name_is_valid( $key ) ) {
				return;
			}
		}
	}
}
class-wc-api.php000064400000011761151336044770007560 0ustar00<?php
/**
 * WC-API endpoint handler.
 *
 * This handles API related functionality in WooCommerce.
 * - wc-api endpoint - Commonly used by Payment gateways for callbacks.
 * - Legacy REST API - Deprecated in 2.6.0. @see class-wc-legacy-api.php
 * - WP REST API - The main REST API in WooCommerce which is built on top of the WP REST API.
 *
 * @package WooCommerce\RestApi
 * @since   2.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * WC_API class.
 */
class WC_API extends WC_Legacy_API {

	/**
	 * Init the API by setting up action and filter hooks.
	 */
	public function init() {
		parent::init();
		add_action( 'init', array( $this, 'add_endpoint' ), 0 );
		add_filter( 'query_vars', array( $this, 'add_query_vars' ), 0 );
		add_action( 'parse_request', array( $this, 'handle_api_requests' ), 0 );
		add_action( 'rest_api_init', array( $this, 'register_wp_admin_settings' ) );
	}

	/**
	 * Get the version of the REST API package being ran. Since API package was merged into core, this now follows WC version.
	 *
	 * @since 3.7.0
	 * @return string|null
	 */
	public function get_rest_api_package_version() {
		if ( ! $this->is_rest_api_loaded() ) {
			return null;
		}
		if ( method_exists( \Automattic\WooCommerce\RestApi\Server::class, 'get_path' ) ) {
			$path = \Automattic\WooCommerce\RestApi\Server::get_path();
			if ( 0 === strpos( $path, __DIR__ ) ) {
				// We are loading API from included version.
				return WC()->version;
			}
		}
		// We are loading API from external plugin.
		return \Automattic\WooCommerce\RestApi\Package::get_version();
	}

	/**
	 * Get the version of the REST API package being ran.
	 *
	 * @since 3.7.0
	 * @return string
	 */
	public function get_rest_api_package_path() {
		if ( ! $this->is_rest_api_loaded() ) {
			return null;
		}
		if ( method_exists( \Automattic\WooCommerce\RestApi\Server::class, 'get_path' ) ) {
			// We are loading API from included version.
			return \Automattic\WooCommerce\RestApi\Server::get_path();
		}
		// We are loading API from external plugin.
		return \Automattic\WooCommerce\RestApi\Package::get_path();
	}

	/**
	 * Return if the rest API classes were already loaded.
	 *
	 * @since 3.7.0
	 * @return boolean
	 */
	protected function is_rest_api_loaded() {
		return class_exists( '\Automattic\WooCommerce\RestApi\Server', false );
	}

	/**
	 * Get data from a WooCommerce API endpoint.
	 *
	 * @since 3.7.0
	 * @param string $endpoint Endpoint.
	 * @param array  $params Params to passwith request.
	 * @return array|\WP_Error
	 */
	public function get_endpoint_data( $endpoint, $params = array() ) {
		if ( ! $this->is_rest_api_loaded() ) {
			return new WP_Error( 'rest_api_unavailable', __( 'The Rest API is unavailable.', 'woocommerce' ) );
		}
		$request = new \WP_REST_Request( 'GET', $endpoint );
		if ( $params ) {
			$request->set_query_params( $params );
		}
		$response = rest_do_request( $request );
		$server   = rest_get_server();
		$json     = wp_json_encode( $server->response_to_data( $response, false ) );
		return json_decode( $json, true );
	}

	/**
	 * Add new query vars.
	 *
	 * @since 2.0
	 * @param array $vars Query vars.
	 * @return string[]
	 */
	public function add_query_vars( $vars ) {
		$vars   = parent::add_query_vars( $vars );
		$vars[] = 'wc-api';
		return $vars;
	}

	/**
	 * WC API for payment gateway IPNs, etc.
	 *
	 * @since 2.0
	 */
	public static function add_endpoint() {
		parent::add_endpoint();
		add_rewrite_endpoint( 'wc-api', EP_ALL );
	}

	/**
	 * API request - Trigger any API requests.
	 *
	 * @since   2.0
	 * @version 2.4
	 */
	public function handle_api_requests() {
		global $wp;

		if ( ! empty( $_GET['wc-api'] ) ) { // WPCS: input var okay, CSRF ok.
			$wp->query_vars['wc-api'] = sanitize_key( wp_unslash( $_GET['wc-api'] ) ); // WPCS: input var okay, CSRF ok.
		}

		// wc-api endpoint requests.
		if ( ! empty( $wp->query_vars['wc-api'] ) ) {

			// Buffer, we won't want any output here.
			ob_start();

			// No cache headers.
			wc_nocache_headers();

			// Clean the API request.
			$api_request = strtolower( wc_clean( $wp->query_vars['wc-api'] ) );

			// Make sure gateways are available for request.
			WC()->payment_gateways();

			// Trigger generic action before request hook.
			do_action( 'woocommerce_api_request', $api_request );

			// Is there actually something hooked into this API request? If not trigger 400 - Bad request.
			status_header( has_action( 'woocommerce_api_' . $api_request ) ? 200 : 400 );

			// Trigger an action which plugins can hook into to fulfill the request.
			do_action( 'woocommerce_api_' . $api_request );

			// Done, clear buffer and exit.
			ob_end_clean();
			die( '-1' );
		}
	}

	/**
	 * Register WC settings from WP-API to the REST API.
	 *
	 * @since  3.0.0
	 */
	public function register_wp_admin_settings() {
		$pages = WC_Admin_Settings::get_settings_pages();
		foreach ( $pages as $page ) {
			new WC_Register_WP_Admin_Settings( $page, 'page' );
		}

		$emails = WC_Emails::instance();
		foreach ( $emails->get_emails() as $email ) {
			new WC_Register_WP_Admin_Settings( $email, 'email' );
		}
	}
}
interfaces/class-wc-order-item-type-data-store-interface.php000064400000001013151336044770020144 0ustar00<?php
/**
 * Order Item Type Data Store Interface
 *
 * @version 3.0.0
 * @package WooCommerce\Interface
 */

/**
 * WC Order Item Data Store Interface
 *
 * Functions that must be defined by order item store classes.
 *
 * @version  3.0.0
 */
interface WC_Order_Item_Type_Data_Store_Interface {
	/**
	 * Saves an item's data to the database / item meta.
	 * Ran after both create and update, so $item->get_id() will be set.
	 *
	 * @param WC_Order_Item $item Item object.
	 */
	public function save_item_data( &$item );
}
interfaces/class-wc-order-item-product-data-store-interface.php000064400000001040151336044770020643 0ustar00<?php
/**
 * Order Item Product Data Store Interface
 *
 * @version 3.0.0
 * @package WooCommerce\Interface
 */

/**
 * WC Order Item Data Store Interface
 *
 * Functions that must be defined by order item store classes.
 *
 * @version  3.0.0
 */
interface WC_Order_Item_Product_Data_Store_Interface {
	/**
	 * Get a list of download IDs for a specific item from an order.
	 *
	 * @param WC_Order_Item $item Item object.
	 * @param WC_Order      $order Order object.
	 * @return array
	 */
	public function get_download_ids( $item, $order );
}
interfaces/class-wc-shipping-zone-data-store-interface.php000064400000003465151336044770017725 0ustar00<?php
/**
 * Shipping Zone Data Store Interface
 *
 * @version 3.0.0
 * @package WooCommerce\Interface
 */

/**
 * WC Shipping Zone Data Store Interface.
 *
 * Functions that must be defined by shipping zone store classes.
 *
 * @version  3.0.0
 */
interface WC_Shipping_Zone_Data_Store_Interface {
	/**
	 * Get a list of shipping methods for a specific zone.
	 *
	 * @param  int  $zone_id Zone ID.
	 * @param  bool $enabled_only True to request enabled methods only.
	 * @return array Array of objects containing method_id, method_order, instance_id, is_enabled
	 */
	public function get_methods( $zone_id, $enabled_only );

	/**
	 * Get count of methods for a zone.
	 *
	 * @param int $zone_id Zone ID.
	 * @return int Method Count
	 */
	public function get_method_count( $zone_id );

	/**
	 * Add a shipping method to a zone.
	 *
	 * @param int    $zone_id Zone ID.
	 * @param string $type Method Type/ID.
	 * @param int    $order Method Order ID.
	 * @return int Instance ID
	 */
	public function add_method( $zone_id, $type, $order );

	/**
	 * Delete a method instance.
	 *
	 * @param int $instance_id Intance ID.
	 */
	public function delete_method( $instance_id );

	/**
	 * Get a shipping zone method instance.
	 *
	 * @param int $instance_id Instance ID.
	 * @return object
	 */
	public function get_method( $instance_id );

	/**
	 * Find a matching zone ID for a given package.
	 *
	 * @param object $package Zone package object.
	 * @return int
	 */
	public function get_zone_id_from_package( $package );

	/**
	 * Return an ordered list of zones.
	 *
	 * @return array An array of objects containing a zone_id, zone_name, and zone_order.
	 */
	public function get_zones();

	/**
	 * Return a zone ID from an instance ID.
	 *
	 * @param int $id Instance ID.
	 * @return int
	 */
	public function get_zone_id_by_instance_id( $id );
}
interfaces/class-wc-coupon-data-store-interface.php000064400000002521151336044770016426 0ustar00<?php
/**
 * Coupon Data Store Interface
 *
 * @version 3.0.0
 * @package WooCommerce\Interfaces
 */

/**
 * WC Coupon Data Store Interface
 *
 * Functions that must be defined by coupon store classes.
 *
 * @version  3.0.0
 */
interface WC_Coupon_Data_Store_Interface {
	/**
	 * Increase usage count for current coupon.
	 *
	 * @param WC_Coupon $coupon Coupon object.
	 * @param string    $used_by Either user ID or billing email.
	 */
	public function increase_usage_count( &$coupon, $used_by = '' );

	/**
	 *  Decrease usage count for current coupon.
	 *
	 * @param WC_Coupon $coupon Coupon object.
	 * @param string    $used_by Either user ID or billing email.
	 */
	public function decrease_usage_count( &$coupon, $used_by = '' );

	/**
	 * Get the number of uses for a coupon by user ID.
	 *
	 * @param WC_Coupon $coupon Coupon object.
	 * @param int       $user_id User ID.
	 * @return int
	 */
	public function get_usage_by_user_id( &$coupon, $user_id );

	/**
	 * Return a coupon code for a specific ID.
	 *
	 * @param int $id Coupon ID.
	 * @return string Coupon Code.
	 */
	public function get_code_by_id( $id );

	/**
	 * Return an array of IDs for for a specific coupon code.
	 * Can return multiple to check for existence.
	 *
	 * @param string $code Coupon code.
	 * @return array Array of IDs.
	 */
	public function get_ids_by_code( $code );
}
interfaces/class-wc-queue-interface.php000064400000012635151336044770014215 0ustar00<?php
/**
 * Queue Interface
 *
 * @version 3.5.0
 * @package WooCommerce\Interface
 */

/**
 * WC Queue Interface
 *
 * Functions that must be defined to implement an action/job/event queue.
 *
 * @version 3.5.0
 */
interface WC_Queue_Interface {

	/**
	 * Enqueue an action to run one time, as soon as possible
	 *
	 * @param string $hook The hook to trigger.
	 * @param array  $args Arguments to pass when the hook triggers.
	 * @param string $group The group to assign this job to.
	 * @return string The action ID
	 */
	public function add( $hook, $args = array(), $group = '' );

	/**
	 * Schedule an action to run once at some time in the future
	 *
	 * @param int    $timestamp When the job will run.
	 * @param string $hook The hook to trigger.
	 * @param array  $args Arguments to pass when the hook triggers.
	 * @param string $group The group to assign this job to.
	 * @return string The action ID
	 */
	public function schedule_single( $timestamp, $hook, $args = array(), $group = '' );

	/**
	 * Schedule a recurring action
	 *
	 * @param int    $timestamp When the first instance of the job will run.
	 * @param int    $interval_in_seconds How long to wait between runs.
	 * @param string $hook The hook to trigger.
	 * @param array  $args Arguments to pass when the hook triggers.
	 * @param string $group The group to assign this job to.
	 * @return string The action ID
	 */
	public function schedule_recurring( $timestamp, $interval_in_seconds, $hook, $args = array(), $group = '' );

	/**
	 * Schedule an action that recurs on a cron-like schedule.
	 *
	 * @param int    $timestamp The schedule will start on or after this time.
	 * @param string $cron_schedule A cron-link schedule string.
	 * @see http://en.wikipedia.org/wiki/Cron
	 *   *    *    *    *    *    *
	 *   ┬    ┬    ┬    ┬    ┬    ┬
	 *   |    |    |    |    |    |
	 *   |    |    |    |    |    + year [optional]
	 *   |    |    |    |    +----- day of week (0 - 7) (Sunday=0 or 7)
	 *   |    |    |    +---------- month (1 - 12)
	 *   |    |    +--------------- day of month (1 - 31)
	 *   |    +-------------------- hour (0 - 23)
	 *   +------------------------- min (0 - 59)
	 * @param string $hook The hook to trigger.
	 * @param array  $args Arguments to pass when the hook triggers.
	 * @param string $group The group to assign this job to.
	 * @return string The action ID
	 */
	public function schedule_cron( $timestamp, $cron_schedule, $hook, $args = array(), $group = '' );

	/**
	 * Dequeue the next scheduled instance of an action with a matching hook (and optionally matching args and group).
	 *
	 * Any recurring actions with a matching hook should also be cancelled, not just the next scheduled action.
	 *
	 * @param string $hook The hook that the job will trigger.
	 * @param array  $args Args that would have been passed to the job.
	 * @param string $group The group the job is assigned to (if any).
	 */
	public function cancel( $hook, $args = array(), $group = '' );

	/**
	 * Dequeue all actions with a matching hook (and optionally matching args and group) so no matching actions are ever run.
	 *
	 * @param string $hook The hook that the job will trigger.
	 * @param array  $args Args that would have been passed to the job.
	 * @param string $group The group the job is assigned to (if any).
	 */
	public function cancel_all( $hook, $args = array(), $group = '' );

	/**
	 * Get the date and time for the next scheduled occurence of an action with a given hook
	 * (an optionally that matches certain args and group), if any.
	 *
	 * @param string $hook The hook that the job will trigger.
	 * @param array  $args Filter to a hook with matching args that will be passed to the job when it runs.
	 * @param string $group Filter to only actions assigned to a specific group.
	 * @return WC_DateTime|null The date and time for the next occurrence, or null if there is no pending, scheduled action for the given hook
	 */
	public function get_next( $hook, $args = null, $group = '' );

	/**
	 * Find scheduled actions.
	 *
	 * @param array  $args Possible arguments, with their default values.
	 *        'hook' => '' - the name of the action that will be triggered.
	 *        'args' => null - the args array that will be passed with the action.
	 *        'date' => null - the scheduled date of the action. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone.
	 *        'date_compare' => '<=' - operator for testing "date". accepted values are '!=', '>', '>=', '<', '<=', '='.
	 *        'modified' => null - the date the action was last updated. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone.
	 *        'modified_compare' => '<=' - operator for testing "modified". accepted values are '!=', '>', '>=', '<', '<=', '='.
	 *        'group' => '' - the group the action belongs to.
	 *        'status' => '' - ActionScheduler_Store::STATUS_COMPLETE or ActionScheduler_Store::STATUS_PENDING.
	 *        'claimed' => null - TRUE to find claimed actions, FALSE to find unclaimed actions, a string to find a specific claim ID.
	 *        'per_page' => 5 - Number of results to return.
	 *        'offset' => 0.
	 *        'orderby' => 'date' - accepted values are 'hook', 'group', 'modified', or 'date'.
	 *        'order' => 'ASC'.
	 * @param string $return_format OBJECT, ARRAY_A, or ids.
	 * @return array
	 */
	public function search( $args = array(), $return_format = OBJECT );
}
interfaces/class-wc-customer-download-log-data-store-interface.php000064400000001222151336044770021345 0ustar00<?php
/**
 * Customer Download Log Data Store Interface
 *
 * @version 3.3.0
 * @package WooCommerce\Interface
 */

/**
 * WC Customer Download Log Data Store Interface.
 *
 * @version  3.3.0
 */
interface WC_Customer_Download_Log_Data_Store_Interface {

	/**
	 * Get array of download log ids by specified args.
	 *
	 * @param  array $args Arguments.
	 * @return array of WC_Customer_Download_Log
	 */
	public function get_download_logs( $args = array() );

	/**
	 * Get logs for a specific download permission.
	 *
	 * @param  int $permission_id Permission ID.
	 * @return array
	 */
	public function get_download_logs_for_permission( $permission_id );

}
interfaces/class-wc-abstract-order-data-store-interface.php000064400000002134151336044770020037 0ustar00<?php
/**
 * Order Data Store Interface
 *
 * @version 3.0.0
 * @package WooCommerce\Interfaces
 */

/**
 * WC Order Data Store Interface
 *
 * Functions that must be defined by order store classes.
 *
 * @version  3.0.0
 */
interface WC_Abstract_Order_Data_Store_Interface {

	/**
	 * Read order items of a specific type from the database for this order.
	 *
	 * @param WC_Order $order Order object.
	 * @param string   $type Order item type.
	 * @return array
	 */
	public function read_items( $order, $type );

	/**
	 * Remove all line items (products, coupons, shipping, taxes) from the order.
	 *
	 * @param WC_Order $order Order object.
	 * @param string   $type Order item type. Default null.
	 */
	public function delete_items( $order, $type = null );

	/**
	 * Get token ids for an order.
	 *
	 * @param WC_Order $order Order object.
	 * @return array
	 */
	public function get_payment_token_ids( $order );

	/**
	 * Update token ids for an order.
	 *
	 * @param WC_Order $order Order object.
	 * @param array    $token_ids Token IDs.
	 */
	public function update_payment_token_ids( $order, $token_ids );
}
interfaces/class-wc-importer-interface.php000064400000002171151336044770014724 0ustar00<?php
/**
 * WooCommerce Importer Interface
 *
 * @package  WooCommerce\Interface
 * @version  3.1.0
 */

/**
 * WC_Importer_Interface class.
 */
interface WC_Importer_Interface {

	/**
	 * Process importation.
	 * Returns an array with the imported and failed items.
	 * 'imported' contains a list of IDs.
	 * 'failed' contains a list of WP_Error objects.
	 *
	 * Example:
	 * ['imported' => [], 'failed' => []]
	 *
	 * @return array
	 */
	public function import();

	/**
	 * Get file raw keys.
	 *
	 * CSV - Headers.
	 * XML - Element names.
	 * JSON - Keys
	 *
	 * @return array
	 */
	public function get_raw_keys();

	/**
	 * Get file mapped headers.
	 *
	 * @return array
	 */
	public function get_mapped_keys();

	/**
	 * Get raw data.
	 *
	 * @return array
	 */
	public function get_raw_data();

	/**
	 * Get parsed data.
	 *
	 * @return array
	 */
	public function get_parsed_data();

	/**
	 * Get file pointer position from the last read.
	 *
	 * @return int
	 */
	public function get_file_position();

	/**
	 * Get file pointer position as a percentage of file size.
	 *
	 * @return int
	 */
	public function get_percent_complete();
}
interfaces/class-wc-log-handler-interface.php000064400000001262151336044770015257 0ustar00<?php
/**
 * Log Handler Interface
 *
 * @version 3.3.0
 * @package WooCommerce\Interface
 */

/**
 * WC Log Handler Interface
 *
 * Functions that must be defined to correctly fulfill log handler API.
 *
 * @version 3.3.0
 */
interface WC_Log_Handler_Interface {

	/**
	 * Handle a log entry.
	 *
	 * @param int    $timestamp Log timestamp.
	 * @param string $level emergency|alert|critical|error|warning|notice|info|debug.
	 * @param string $message Log message.
	 * @param array  $context Additional information for log handlers.
	 *
	 * @return bool False if value was not handled and true if value was handled.
	 */
	public function handle( $timestamp, $level, $message, $context );
}
interfaces/class-wc-order-refund-data-store-interface.php000064400000000441151336044770017516 0ustar00<?php
/**
 * Order Refund Data Store Interface
 *
 * @version 3.0.0
 * @package WooCommerce\Interface
 */

/**
 * WC Order Refund Data Store Interface
 *
 * Functions that must be defined by order store classes.
 *
 * @version  3.0.0
 */
interface WC_Order_Refund_Data_Store_Interface {
}
interfaces/class-wc-payment-token-data-store-interface.php000064400000003423151336044770017720 0ustar00<?php
/**
 * Payment Token Data Store Interface
 *
 * @version 3.0.0
 * @package WooCommerce\Interface
 */

/**
 * WC Payment Token Data Store Interface
 *
 * Functions that must be defined by payment token store classes.
 *
 * @version  3.0.0
 */
interface WC_Payment_Token_Data_Store_Interface {
	/**
	 * Returns an array of objects (stdObject) matching specific token criteria.
	 * Accepts token_id, user_id, gateway_id, and type.
	 * Each object should contain the fields token_id, gateway_id, token, user_id, type, is_default.
	 *
	 * @param array $args Arguments.
	 * @return array
	 */
	public function get_tokens( $args );

	/**
	 * Returns an stdObject of a token for a user's default token.
	 * Should contain the fields token_id, gateway_id, token, user_id, type, is_default.
	 *
	 * @param int $user_id User ID.
	 * @return object
	 */
	public function get_users_default_token( $user_id );

	/**
	 * Returns an stdObject of a token.
	 * Should contain the fields token_id, gateway_id, token, user_id, type, is_default.
	 *
	 * @param int $token_id Token ID.
	 * @return object
	 */
	public function get_token_by_id( $token_id );

	/**
	 * Returns metadata for a specific payment token.
	 *
	 * @param int $token_id Token ID.
	 * @return array
	 */
	public function get_metadata( $token_id );

	/**
	 * Get a token's type by ID.
	 *
	 * @param int $token_id Token ID.
	 * @return string
	 */
	public function get_token_type_by_id( $token_id );

	/**
	 * Update's a tokens default status in the database. Used for quickly
	 * looping through tokens and setting their statuses instead of creating a bunch
	 * of objects.
	 *
	 * @param int  $token_id Token ID.
	 * @param bool $status If should update status.
	 * @return string
	 */
	public function set_default_status( $token_id, $status = true );
}
interfaces/class-wc-webhooks-data-store-interface.php000064400000001423151336044770016744 0ustar00<?php
/**
 * Webhook Data Store Interface
 *
 * @version  3.2.0
 * @package  WooCommerce\Interface
 */

/**
 * WooCommerce Webhook data store interface.
 */
interface WC_Webhook_Data_Store_Interface {

	/**
	 * Get API version number.
	 *
	 * @since  3.2.0
	 * @param  string $api_version REST API version.
	 * @return int
	 */
	public function get_api_version_number( $api_version );

	/**
	 * Get all webhooks IDs.
	 *
	 * @since  3.2.0
	 * @throws InvalidArgumentException If a $status value is passed in that is not in the known wc_get_webhook_statuses() keys.
	 * @param  string $status Optional - status to filter results by. Must be a key in return value of @see wc_get_webhook_statuses(). @since 3.6.0.
	 * @return int[]
	 */
	public function get_webhooks_ids( $status = '' );
}
interfaces/class-wc-product-data-store-interface.php000064400000007370151336044770016612 0ustar00<?php
/**
 * Product Data Store Interface
 *
 * @version 3.0.0
 * @package WooCommerce\Interface
 */

/**
 * WC Product Data Store Interface
 *
 * Functions that must be defined by product store classes.
 *
 * @version  3.0.0
 */
interface WC_Product_Data_Store_Interface {

	/**
	 * Returns an array of on sale products, as an array of objects with an
	 * ID and parent_id present. Example: $return[0]->id, $return[0]->parent_id.
	 *
	 * @return array
	 */
	public function get_on_sale_products();

	/**
	 * Returns a list of product IDs ( id as key => parent as value) that are
	 * featured. Uses get_posts instead of wc_get_products since we want
	 * some extra meta queries and ALL products (posts_per_page = -1).
	 *
	 * @return array
	 */
	public function get_featured_product_ids();

	/**
	 * Check if product sku is found for any other product IDs.
	 *
	 * @param int    $product_id Product ID.
	 * @param string $sku SKU.
	 * @return bool
	 */
	public function is_existing_sku( $product_id, $sku );

	/**
	 * Return product ID based on SKU.
	 *
	 * @param string $sku SKU.
	 * @return int
	 */
	public function get_product_id_by_sku( $sku );

	/**
	 * Returns an array of IDs of products that have sales starting soon.
	 *
	 * @return array
	 */
	public function get_starting_sales();

	/**
	 * Returns an array of IDs of products that have sales which are due to end.
	 *
	 * @return array
	 */
	public function get_ending_sales();

	/**
	 * Find a matching (enabled) variation within a variable product.
	 *
	 * @param WC_Product $product Variable product object.
	 * @param array      $match_attributes Array of attributes we want to try to match.
	 * @return int Matching variation ID or 0.
	 */
	public function find_matching_product_variation( $product, $match_attributes = array() );

	/**
	 * Make sure all variations have a sort order set so they can be reordered correctly.
	 *
	 * @param int $parent_id Parent ID.
	 */
	public function sort_all_product_variations( $parent_id );

	/**
	 * Return a list of related products (using data like categories and IDs).
	 *
	 * @param array $cats_array List of categories IDs.
	 * @param array $tags_array List of tags IDs.
	 * @param array $exclude_ids Excluded IDs.
	 * @param int   $limit Limit of results.
	 * @param int   $product_id Product ID.
	 * @return array
	 */
	public function get_related_products( $cats_array, $tags_array, $exclude_ids, $limit, $product_id );

	/**
	 * Update a product's stock amount directly.
	 *
	 * Uses queries rather than update_post_meta so we can do this in one query (to avoid stock issues).
	 *
	 * @param int      $product_id_with_stock Product ID.
	 * @param int|null $stock_quantity Stock quantity to update to.
	 * @param string   $operation Either set, increase or decrease.
	 */
	public function update_product_stock( $product_id_with_stock, $stock_quantity = null, $operation = 'set' );

	/**
	 * Update a product's sale count directly.
	 *
	 * Uses queries rather than update_post_meta so we can do this in one query for performance.
	 *
	 * @param int      $product_id Product ID.
	 * @param int|null $quantity Stock quantity to use for update.
	 * @param string   $operation Either set, increase or decrease.
	 */
	public function update_product_sales( $product_id, $quantity = null, $operation = 'set' );

	/**
	 * Get shipping class ID by slug.
	 *
	 * @param string $slug Shipping class slug.
	 * @return int|false
	 */
	public function get_shipping_class_id_by_slug( $slug );

	/**
	 * Returns an array of products.
	 *
	 * @param array $args @see wc_get_products.
	 * @return array
	 */
	public function get_products( $args = array() );

	/**
	 * Get the product type based on product ID.
	 *
	 * @param int $product_id Product ID.
	 * @return bool|string
	 */
	public function get_product_type( $product_id );
}
interfaces/class-wc-customer-download-data-store-interface.php000064400000003116151336044770020572 0ustar00<?php
/**
 * Customer Download Data Store Interface
 *
 * @version 3.0.0
 * @package WooCommerce\Interface
 */

/**
 * WC Customer Download Data Store Interface.
 *
 * @version  3.0.0
 */
interface WC_Customer_Download_Data_Store_Interface {

	/**
	 * Method to delete a download permission from the database by ID.
	 *
	 * @param int $id Download Permission ID.
	 */
	public function delete_by_id( $id );

	/**
	 * Method to delete a download permission from the database by order ID.
	 *
	 * @param int $id Order ID.
	 */
	public function delete_by_order_id( $id );

	/**
	 * Method to delete a download permission from the database by download ID.
	 *
	 * @param int $id Download ID.
	 */
	public function delete_by_download_id( $id );

	/**
	 * Get array of download ids by specified args.
	 *
	 * @param  array $args Arguments.
	 * @return array of WC_Customer_Download
	 */
	public function get_downloads( $args = array() );

	/**
	 * Update download ids if the hash changes.
	 *
	 * @param  int    $product_id Product ID.
	 * @param  string $old_id Old ID.
	 * @param  string $new_id New ID.
	 */
	public function update_download_id( $product_id, $old_id, $new_id );

	/**
	 * Get a customers downloads.
	 *
	 * @param  int $customer_id Customer ID.
	 * @return array
	 */
	public function get_downloads_for_customer( $customer_id );

	/**
	 * Update user prop for downloads based on order id.
	 *
	 * @param  int    $order_id Order ID.
	 * @param  int    $customer_id Customer ID.
	 * @param  string $email Email Address.
	 */
	public function update_user_by_order_id( $order_id, $customer_id, $email );
}
interfaces/class-wc-order-item-data-store-interface.php000064400000004657151336044770017206 0ustar00<?php
/**
 * Order Item Data Store Interface
 *
 * @version 3.0.0
 * @package WooCommerce\Interface
 */

/**
 * WC Order Item Data Store Interface
 *
 * Functions that must be defined by the order item data store (for functions).
 *
 * @version  3.0.0
 */
interface WC_Order_Item_Data_Store_Interface {

	/**
	 * Add an order item to an order.
	 *
	 * @param  int   $order_id Order ID.
	 * @param  array $item order_item_name and order_item_type.
	 * @return int   Order Item ID
	 */
	public function add_order_item( $order_id, $item );

	/**
	 * Update an order item.
	 *
	 * @param  int   $item_id Item ID.
	 * @param  array $item order_item_name or order_item_type.
	 * @return boolean
	 */
	public function update_order_item( $item_id, $item );

	/**
	 * Delete an order item.
	 *
	 * @param int $item_id Item ID.
	 */
	public function delete_order_item( $item_id );

	/**
	 * Update term meta.
	 *
	 * @param  int    $item_id Item ID.
	 * @param  string $meta_key Meta key.
	 * @param  mixed  $meta_value Meta value.
	 * @param  string $prev_value Previous value (default: '').
	 * @return bool
	 */
	public function update_metadata( $item_id, $meta_key, $meta_value, $prev_value = '' );

	/**
	 * Add term meta.
	 *
	 * @param  int    $item_id Item ID.
	 * @param  string $meta_key Meta key.
	 * @param  mixed  $meta_value Meta value.
	 * @param  bool   $unique Unique? (default: false).
	 * @return int    New row ID or 0
	 */
	public function add_metadata( $item_id, $meta_key, $meta_value, $unique = false );


	/**
	 * Delete term meta.
	 *
	 * @param  int    $item_id Item ID.
	 * @param  string $meta_key Meta key.
	 * @param  string $meta_value Meta value (default: '').
	 * @param  bool   $delete_all Delete all matching entries? (default: false).
	 * @return bool
	 */
	public function delete_metadata( $item_id, $meta_key, $meta_value = '', $delete_all = false );

	/**
	 * Get term meta.
	 *
	 * @param  int    $item_id Item ID.
	 * @param  string $key Meta key.
	 * @param  bool   $single Store as single value and not serialised (default: true).
	 * @return mixed
	 */
	public function get_metadata( $item_id, $key, $single = true );

	/**
	 * Get order ID by order item ID.
	 *
	 * @param  int $item_id Item ID.
	 * @return int
	 */
	public function get_order_id_by_order_item_id( $item_id );

	/**
	 * Get the order item type based on Item ID.
	 *
	 * @param  int $item_id Item ID.
	 * @return string
	 */
	public function get_order_item_type( $item_id );
}
interfaces/class-wc-product-variable-data-store-interface.php000064400000003605151336044770020372 0ustar00<?php
/**
 * Product Variable Data Store Interface
 *
 * @version 3.0.0
 * @package WooCommerce\Interface
 */

/**
 * WC Product Variable Data Store Interface
 *
 * Functions that must be defined by product variable store classes.
 *
 * @version  3.0.0
 */
interface WC_Product_Variable_Data_Store_Interface {
	/**
	 * Does a child have a weight set?
	 *
	 * @param WC_Product $product Product object.
	 * @return boolean
	 */
	public function child_has_weight( $product );

	/**
	 * Does a child have dimensions set?
	 *
	 * @param WC_Product $product Product object.
	 * @return boolean
	 */
	public function child_has_dimensions( $product );

	/**
	 * Is a child in stock?
	 *
	 * @param WC_Product $product Product object.
	 * @return boolean
	 */
	public function child_is_in_stock( $product );

	/**
	 * Syncs all variation names if the parent name is changed.
	 *
	 * @param WC_Product $product Product object.
	 * @param string     $previous_name Previous name.
	 * @param string     $new_name New name.
	 */
	public function sync_variation_names( &$product, $previous_name = '', $new_name = '' );

	/**
	 * Stock managed at the parent level - update children being managed by this product.
	 * This sync function syncs downwards (from parent to child) when the variable product is saved.
	 *
	 * @param WC_Product $product Product object.
	 */
	public function sync_managed_variation_stock_status( &$product );

	/**
	 * Sync variable product prices with children.
	 *
	 * @param WC_Product|int $product Product object or ID.
	 */
	public function sync_price( &$product );

	/**
	 * Delete variations of a product.
	 *
	 * @param int  $product_id Product ID.
	 * @param bool $force_delete False to trash.
	 */
	public function delete_variations( $product_id, $force_delete = false );

	/**
	 * Untrash variations.
	 *
	 * @param int $product_id Product ID.
	 */
	public function untrash_variations( $product_id );
}
interfaces/class-wc-object-data-store-interface.php000064400000003167151336044770016400 0ustar00<?php
/**
 * Object Data Store Interface
 *
 * @version 3.0.0
 * @package WooCommerce\Interface
 */

/**
 * WC Data Store Interface
 *
 * @version  3.0.0
 */
interface WC_Object_Data_Store_Interface {
	/**
	 * Method to create a new record of a WC_Data based object.
	 *
	 * @param WC_Data $data Data object.
	 */
	public function create( &$data );

	/**
	 * Method to read a record. Creates a new WC_Data based object.
	 *
	 * @param WC_Data $data Data object.
	 */
	public function read( &$data );

	/**
	 * Updates a record in the database.
	 *
	 * @param WC_Data $data Data object.
	 */
	public function update( &$data );

	/**
	 * Deletes a record from the database.
	 *
	 * @param  WC_Data $data Data object.
	 * @param  array   $args Array of args to pass to the delete method.
	 * @return bool result
	 */
	public function delete( &$data, $args = array() );

	/**
	 * Returns an array of meta for an object.
	 *
	 * @param  WC_Data $data Data object.
	 * @return array
	 */
	public function read_meta( &$data );

	/**
	 * Deletes meta based on meta ID.
	 *
	 * @param  WC_Data $data Data object.
	 * @param  object  $meta Meta object (containing at least ->id).
	 * @return array
	 */
	public function delete_meta( &$data, $meta );

	/**
	 * Add new piece of meta.
	 *
	 * @param  WC_Data $data Data object.
	 * @param  object  $meta Meta object (containing ->key and ->value).
	 * @return int meta ID
	 */
	public function add_meta( &$data, $meta );

	/**
	 * Update meta.
	 *
	 * @param  WC_Data $data Data object.
	 * @param  object  $meta Meta object (containing ->id, ->key and ->value).
	 */
	public function update_meta( &$data, $meta );
}
interfaces/class-wc-customer-data-store-interface.php000064400000001464151336044770016771 0ustar00<?php
/**
 * Customer Data Store Interface
 *
 * @version 3.0.0
 * @package WooCommerce\Interface
 */

/**
 * WC Customer Data Store Interface
 *
 * Functions that must be defined by customer store classes.
 *
 * @version  3.0.0
 */
interface WC_Customer_Data_Store_Interface {

	/**
	 * Gets the customers last order.
	 *
	 * @param WC_Customer $customer Customer object.
	 * @return WC_Order|false
	 */
	public function get_last_order( &$customer );

	/**
	 * Return the number of orders this customer has.
	 *
	 * @param WC_Customer $customer Customer object.
	 * @return integer
	 */
	public function get_order_count( &$customer );

	/**
	 * Return how much money this customer has spent.
	 *
	 * @param WC_Customer $customer Customer object.
	 * @return float
	 */
	public function get_total_spent( &$customer );
}
interfaces/class-wc-order-data-store-interface.php000064400000006013151336044770016236 0ustar00<?php
/**
 * Order Data Store Interface
 *
 * @version 3.0.0
 * @package WooCommerce\Interface
 */

/**
 * WC Order Data Store Interface
 *
 * Functions that must be defined by order store classes.
 *
 * @version  3.0.0
 */
interface WC_Order_Data_Store_Interface {
	/**
	 * Get amount already refunded.
	 *
	 * @param WC_Order $order Order object.
	 * @return float
	 */
	public function get_total_refunded( $order );

	/**
	 * Get the total tax refunded.
	 *
	 * @param WC_Order $order Order object.
	 * @return float
	 */
	public function get_total_tax_refunded( $order );

	/**
	 * Get the total shipping refunded.
	 *
	 * @param WC_Order $order Order object.
	 * @return float
	 */
	public function get_total_shipping_refunded( $order );

	/**
	 * Finds an Order ID based on an order key.
	 *
	 * @param string $order_key An order key has generated by.
	 * @return int The ID of an order, or 0 if the order could not be found.
	 */
	public function get_order_id_by_order_key( $order_key );

	/**
	 * Return count of orders with a specific status.
	 *
	 * @param string $status Order status.
	 * @return int
	 */
	public function get_order_count( $status );

	/**
	 * Get all orders matching the passed in args.
	 *
	 * @see    wc_get_orders()
	 * @param array $args Arguments.
	 * @return array of orders
	 */
	public function get_orders( $args = array() );

	/**
	 * Get unpaid orders after a certain date,
	 *
	 * @param int $date timestamp.
	 * @return array
	 */
	public function get_unpaid_orders( $date );

	/**
	 * Search order data for a term and return ids.
	 *
	 * @param  string $term Term name.
	 * @return array of ids
	 */
	public function search_orders( $term );

	/**
	 * Gets information about whether permissions were generated yet.
	 *
	 * @param WC_Order $order Order object.
	 * @return bool
	 */
	public function get_download_permissions_granted( $order );

	/**
	 * Stores information about whether permissions were generated yet.
	 *
	 * @param WC_Order $order Order object.
	 * @param bool     $set If should set.
	 */
	public function set_download_permissions_granted( $order, $set );

	/**
	 * Gets information about whether sales were recorded.
	 *
	 * @param WC_Order $order Order object.
	 * @return bool
	 */
	public function get_recorded_sales( $order );

	/**
	 * Stores information about whether sales were recorded.
	 *
	 * @param WC_Order $order Order object.
	 * @param bool     $set If should set.
	 */
	public function set_recorded_sales( $order, $set );

	/**
	 * Gets information about whether coupon counts were updated.
	 *
	 * @param WC_Order $order Order object.
	 * @return bool
	 */
	public function get_recorded_coupon_usage_counts( $order );

	/**
	 * Stores information about whether coupon counts were updated.
	 *
	 * @param WC_Order $order Order object.
	 * @param bool     $set If should set.
	 */
	public function set_recorded_coupon_usage_counts( $order, $set );

	/**
	 * Get the order type based on Order ID.
	 *
	 * @param int $order_id Order ID.
	 * @return string
	 */
	public function get_order_type( $order_id );
}
interfaces/class-wc-logger-interface.php000064400000007261151336044770014347 0ustar00<?php
/**
 * Logger Interface
 *
 * @version 3.0.0
 * @package WooCommerce\Interface
 */

/**
 * WC Logger Interface
 *
 * Functions that must be defined to correctly fulfill logger API.
 *
 * @version 3.0.0
 */
interface WC_Logger_Interface {

	/**
	 * Add a log entry.
	 *
	 * This is not the preferred method for adding log messages. Please use log() or any one of
	 * the level methods (debug(), info(), etc.). This method may be deprecated in the future.
	 *
	 * @param string $handle File handle.
	 * @param string $message Log message.
	 * @param string $level Log level.
	 *
	 * @return bool True if log was added, otherwise false.
	 */
	public function add( $handle, $message, $level = WC_Log_Levels::NOTICE );

	/**
	 * Add a log entry.
	 *
	 * @param string $level One of the following:
	 *     'emergency': System is unusable.
	 *     'alert': Action must be taken immediately.
	 *     'critical': Critical conditions.
	 *     'error': Error conditions.
	 *     'warning': Warning conditions.
	 *     'notice': Normal but significant condition.
	 *     'info': Informational messages.
	 *     'debug': Debug-level messages.
	 * @param string $message Log message.
	 * @param array  $context Optional. Additional information for log handlers.
	 */
	public function log( $level, $message, $context = array() );

	/**
	 * Adds an emergency level message.
	 *
	 * System is unusable.
	 *
	 * @param string $message Log message.
	 * @param array  $context Optional. Additional information for log handlers.
	 */
	public function emergency( $message, $context = array() );

	/**
	 * Adds an alert level message.
	 *
	 * Action must be taken immediately.
	 * Example: Entire website down, database unavailable, etc.
	 *
	 * @param string $message Log message.
	 * @param array  $context Optional. Additional information for log handlers.
	 */
	public function alert( $message, $context = array() );

	/**
	 * Adds a critical level message.
	 *
	 * Critical conditions.
	 * Example: Application component unavailable, unexpected exception.
	 *
	 * @param string $message Log message.
	 * @param array  $context Optional. Additional information for log handlers.
	 */
	public function critical( $message, $context = array() );

	/**
	 * Adds an error level message.
	 *
	 * Runtime errors that do not require immediate action but should typically be logged
	 * and monitored.
	 *
	 * @param string $message Log message.
	 * @param array  $context Optional. Additional information for log handlers.
	 */
	public function error( $message, $context = array() );

	/**
	 * Adds a warning level message.
	 *
	 * Exceptional occurrences that are not errors.
	 *
	 * Example: Use of deprecated APIs, poor use of an API, undesirable things that are not
	 * necessarily wrong.
	 *
	 * @param string $message Log message.
	 * @param array  $context Optional. Additional information for log handlers.
	 */
	public function warning( $message, $context = array() );

	/**
	 * Adds a notice level message.
	 *
	 * Normal but significant events.
	 *
	 * @param string $message Log message.
	 * @param array  $context Optional. Additional information for log handlers.
	 */
	public function notice( $message, $context = array() );

	/**
	 * Adds a info level message.
	 *
	 * Interesting events.
	 * Example: User logs in, SQL logs.
	 *
	 * @param string $message Log message.
	 * @param array  $context Optional. Additional information for log handlers.
	 */
	public function info( $message, $context = array() );

	/**
	 * Adds a debug level message.
	 *
	 * Detailed debug information.
	 *
	 * @param string $message Log message.
	 * @param array  $context Optional. Additional information for log handlers.
	 */
	public function debug( $message, $context = array() );
}
wc-rest-functions.php000064400000025336151336044770010672 0ustar00<?php
/**
 * WooCommerce REST Functions
 *
 * Functions for REST specific things.
 *
 * @package WooCommerce\Functions
 * @version 2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Parses and formats a date for ISO8601/RFC3339.
 *
 * Required WP 4.4 or later.
 * See https://developer.wordpress.org/reference/functions/mysql_to_rfc3339/
 *
 * @since  2.6.0
 * @param  string|null|WC_DateTime $date Date.
 * @param  bool                    $utc  Send false to get local/offset time.
 * @return string|null ISO8601/RFC3339 formatted datetime.
 */
function wc_rest_prepare_date_response( $date, $utc = true ) {
	if ( is_numeric( $date ) ) {
		$date = new WC_DateTime( "@$date", new DateTimeZone( 'UTC' ) );
		$date->setTimezone( new DateTimeZone( wc_timezone_string() ) );
	} elseif ( is_string( $date ) ) {
		$date = new WC_DateTime( $date, new DateTimeZone( 'UTC' ) );
		$date->setTimezone( new DateTimeZone( wc_timezone_string() ) );
	}

	if ( ! is_a( $date, 'WC_DateTime' ) ) {
		return null;
	}

	// Get timestamp before changing timezone to UTC.
	return gmdate( 'Y-m-d\TH:i:s', $utc ? $date->getTimestamp() : $date->getOffsetTimestamp() );
}

/**
 * Returns image mime types users are allowed to upload via the API.
 *
 * @since  2.6.4
 * @return array
 */
function wc_rest_allowed_image_mime_types() {
	return apply_filters(
		'woocommerce_rest_allowed_image_mime_types',
		array(
			'jpg|jpeg|jpe' => 'image/jpeg',
			'gif'          => 'image/gif',
			'png'          => 'image/png',
			'bmp'          => 'image/bmp',
			'tiff|tif'     => 'image/tiff',
			'ico'          => 'image/x-icon',
		)
	);
}

/**
 * Upload image from URL.
 *
 * @since 2.6.0
 * @param string $image_url Image URL.
 * @return array|WP_Error Attachment data or error message.
 */
function wc_rest_upload_image_from_url( $image_url ) {
	$parsed_url = wp_parse_url( $image_url );

	// Check parsed URL.
	if ( ! $parsed_url || ! is_array( $parsed_url ) ) {
		/* translators: %s: image URL */
		return new WP_Error( 'woocommerce_rest_invalid_image_url', sprintf( __( 'Invalid URL %s.', 'woocommerce' ), $image_url ), array( 'status' => 400 ) );
	}

	// Ensure url is valid.
	$image_url = esc_url_raw( $image_url );

	// download_url function is part of wp-admin.
	if ( ! function_exists( 'download_url' ) ) {
		include_once ABSPATH . 'wp-admin/includes/file.php';
	}

	$file_array         = array();
	$file_array['name'] = basename( current( explode( '?', $image_url ) ) );

	// Download file to temp location.
	$file_array['tmp_name'] = download_url( $image_url );

	// If error storing temporarily, return the error.
	if ( is_wp_error( $file_array['tmp_name'] ) ) {
		return new WP_Error(
			'woocommerce_rest_invalid_remote_image_url',
			/* translators: %s: image URL */
			sprintf( __( 'Error getting remote image %s.', 'woocommerce' ), $image_url ) . ' '
			/* translators: %s: error message */
			. sprintf( __( 'Error: %s', 'woocommerce' ), $file_array['tmp_name']->get_error_message() ),
			array( 'status' => 400 )
		);
	}

	// Do the validation and storage stuff.
	$file = wp_handle_sideload(
		$file_array,
		array(
			'test_form' => false,
			'mimes'     => wc_rest_allowed_image_mime_types(),
		),
		current_time( 'Y/m' )
	);

	if ( isset( $file['error'] ) ) {
		@unlink( $file_array['tmp_name'] ); // @codingStandardsIgnoreLine.

		/* translators: %s: error message */
		return new WP_Error( 'woocommerce_rest_invalid_image', sprintf( __( 'Invalid image: %s', 'woocommerce' ), $file['error'] ), array( 'status' => 400 ) );
	}

	do_action( 'woocommerce_rest_api_uploaded_image_from_url', $file, $image_url );

	return $file;
}

/**
 * Set uploaded image as attachment.
 *
 * @since 2.6.0
 * @param array $upload Upload information from wp_upload_bits.
 * @param int   $id Post ID. Default to 0.
 * @return int Attachment ID
 */
function wc_rest_set_uploaded_image_as_attachment( $upload, $id = 0 ) {
	$info    = wp_check_filetype( $upload['file'] );
	$title   = '';
	$content = '';

	if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) {
		include_once ABSPATH . 'wp-admin/includes/image.php';
	}

	$image_meta = wp_read_image_metadata( $upload['file'] );
	if ( $image_meta ) {
		if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) {
			$title = wc_clean( $image_meta['title'] );
		}
		if ( trim( $image_meta['caption'] ) ) {
			$content = wc_clean( $image_meta['caption'] );
		}
	}

	$attachment = array(
		'post_mime_type' => $info['type'],
		'guid'           => $upload['url'],
		'post_parent'    => $id,
		'post_title'     => $title ? $title : basename( $upload['file'] ),
		'post_content'   => $content,
	);

	$attachment_id = wp_insert_attachment( $attachment, $upload['file'], $id );
	if ( ! is_wp_error( $attachment_id ) ) {
		wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) );
	}

	return $attachment_id;
}

/**
 * Validate reports request arguments.
 *
 * @since 2.6.0
 * @param mixed           $value   Value to valdate.
 * @param WP_REST_Request $request Request instance.
 * @param string          $param   Param to validate.
 * @return WP_Error|boolean
 */
function wc_rest_validate_reports_request_arg( $value, $request, $param ) {

	$attributes = $request->get_attributes();
	if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) {
		return true;
	}
	$args = $attributes['args'][ $param ];

	if ( 'string' === $args['type'] && ! is_string( $value ) ) {
		/* translators: 1: param 2: type */
		return new WP_Error( 'woocommerce_rest_invalid_param', sprintf( __( '%1$s is not of type %2$s', 'woocommerce' ), $param, 'string' ) );
	}

	if ( 'date' === $args['format'] ) {
		$regex = '#^\d{4}-\d{2}-\d{2}$#';

		if ( ! preg_match( $regex, $value, $matches ) ) {
			return new WP_Error( 'woocommerce_rest_invalid_date', __( 'The date you provided is invalid.', 'woocommerce' ) );
		}
	}

	return true;
}

/**
 * Encodes a value according to RFC 3986.
 * Supports multidimensional arrays.
 *
 * @since 2.6.0
 * @param string|array $value The value to encode.
 * @return string|array       Encoded values.
 */
function wc_rest_urlencode_rfc3986( $value ) {
	if ( is_array( $value ) ) {
		return array_map( 'wc_rest_urlencode_rfc3986', $value );
	}

	return str_replace( array( '+', '%7E' ), array( ' ', '~' ), rawurlencode( $value ) );
}

/**
 * Check permissions of posts on REST API.
 *
 * @since 2.6.0
 * @param string $post_type Post type.
 * @param string $context   Request context.
 * @param int    $object_id Post ID.
 * @return bool
 */
function wc_rest_check_post_permissions( $post_type, $context = 'read', $object_id = 0 ) {
	$contexts = array(
		'read'   => 'read_private_posts',
		'create' => 'publish_posts',
		'edit'   => 'edit_post',
		'delete' => 'delete_post',
		'batch'  => 'edit_others_posts',
	);

	if ( 'revision' === $post_type ) {
		$permission = false;
	} else {
		$cap              = $contexts[ $context ];
		$post_type_object = get_post_type_object( $post_type );
		$permission       = current_user_can( $post_type_object->cap->$cap, $object_id );
	}

	return apply_filters( 'woocommerce_rest_check_permissions', $permission, $context, $object_id, $post_type );
}

/**
 * Check permissions of users on REST API.
 *
 * @since 2.6.0
 * @param string $context   Request context.
 * @param int    $object_id Post ID.
 * @return bool
 */
function wc_rest_check_user_permissions( $context = 'read', $object_id = 0 ) {
	$contexts = array(
		'read'   => 'list_users',
		'create' => 'promote_users', // Check if current user can create users, shop managers are not allowed to create users.
		'edit'   => 'edit_users',
		'delete' => 'delete_users',
		'batch'  => 'promote_users',
	);

	// Check to allow shop_managers to manage only customers.
	if ( in_array( $context, array( 'edit', 'delete' ), true ) && wc_current_user_has_role( 'shop_manager' ) ) {
		$permission                  = false;
		$user_data                   = get_userdata( $object_id );
		$shop_manager_editable_roles = apply_filters( 'woocommerce_shop_manager_editable_roles', array( 'customer' ) );

		if ( isset( $user_data->roles ) ) {
			$can_manage_users = array_intersect( $user_data->roles, array_unique( $shop_manager_editable_roles ) );

			// Check if Shop Manager can edit customer or with the is same shop manager.
			if ( 0 < count( $can_manage_users ) || intval( $object_id ) === intval( get_current_user_id() ) ) {
				$permission = current_user_can( $contexts[ $context ], $object_id );
			}
		}
	} else {
		$permission = current_user_can( $contexts[ $context ], $object_id );
	}

	return apply_filters( 'woocommerce_rest_check_permissions', $permission, $context, $object_id, 'user' );
}

/**
 * Check permissions of product terms on REST API.
 *
 * @since 2.6.0
 * @param string $taxonomy  Taxonomy.
 * @param string $context   Request context.
 * @param int    $object_id Post ID.
 * @return bool
 */
function wc_rest_check_product_term_permissions( $taxonomy, $context = 'read', $object_id = 0 ) {
	$contexts = array(
		'read'   => 'manage_terms',
		'create' => 'edit_terms',
		'edit'   => 'edit_terms',
		'delete' => 'delete_terms',
		'batch'  => 'edit_terms',
	);

	$cap             = $contexts[ $context ];
	$taxonomy_object = get_taxonomy( $taxonomy );
	$permission      = current_user_can( $taxonomy_object->cap->$cap, $object_id );

	return apply_filters( 'woocommerce_rest_check_permissions', $permission, $context, $object_id, $taxonomy );
}

/**
 * Check manager permissions on REST API.
 *
 * @since 2.6.0
 * @param string $object  Object.
 * @param string $context Request context.
 * @return bool
 */
function wc_rest_check_manager_permissions( $object, $context = 'read' ) {
	$objects = array(
		'reports'          => 'view_woocommerce_reports',
		'settings'         => 'manage_woocommerce',
		'system_status'    => 'manage_woocommerce',
		'attributes'       => 'manage_product_terms',
		'shipping_methods' => 'manage_woocommerce',
		'payment_gateways' => 'manage_woocommerce',
		'webhooks'         => 'manage_woocommerce',
	);

	$permission = current_user_can( $objects[ $object ] );

	return apply_filters( 'woocommerce_rest_check_permissions', $permission, $context, 0, $object );
}

/**
 * Check product reviews permissions on REST API.
 *
 * @since 3.5.0
 * @param string $context   Request context.
 * @param string $object_id Object ID.
 * @return bool
 */
function wc_rest_check_product_reviews_permissions( $context = 'read', $object_id = 0 ) {
	$permission = false;
	$contexts   = array(
		'read'   => 'moderate_comments',
		'create' => 'moderate_comments',
		'edit'   => 'moderate_comments',
		'delete' => 'moderate_comments',
		'batch'  => 'moderate_comments',
	);

	if ( isset( $contexts[ $context ] ) ) {
		$permission = current_user_can( $contexts[ $context ] );
	}

	return apply_filters( 'woocommerce_rest_check_permissions', $permission, $context, $object_id, 'product_review' );
}
class-wc-post-data.php000064400000050404151336044770010700 0ustar00<?php
/**
 * Post Data
 *
 * Standardises certain post data on save.
 *
 * @package WooCommerce\Classes\Data
 * @version 2.2.0
 */

use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore as ProductAttributesLookupDataStore;
use Automattic\WooCommerce\Proxies\LegacyProxy;

defined( 'ABSPATH' ) || exit;

/**
 * Post data class.
 */
class WC_Post_Data {

	/**
	 * Editing term.
	 *
	 * @var object
	 */
	private static $editing_term = null;

	/**
	 * Hook in methods.
	 */
	public static function init() {
		add_filter( 'post_type_link', array( __CLASS__, 'variation_post_link' ), 10, 2 );
		add_action( 'shutdown', array( __CLASS__, 'do_deferred_product_sync' ), 10 );
		add_action( 'set_object_terms', array( __CLASS__, 'force_default_term' ), 10, 5 );
		add_action( 'set_object_terms', array( __CLASS__, 'delete_product_query_transients' ) );
		add_action( 'deleted_term_relationships', array( __CLASS__, 'delete_product_query_transients' ) );
		add_action( 'woocommerce_product_set_stock_status', array( __CLASS__, 'delete_product_query_transients' ) );
		add_action( 'woocommerce_product_set_visibility', array( __CLASS__, 'delete_product_query_transients' ) );
		add_action( 'woocommerce_product_type_changed', array( __CLASS__, 'product_type_changed' ), 10, 3 );

		add_action( 'edit_term', array( __CLASS__, 'edit_term' ), 10, 3 );
		add_action( 'edited_term', array( __CLASS__, 'edited_term' ), 10, 3 );
		add_filter( 'update_order_item_metadata', array( __CLASS__, 'update_order_item_metadata' ), 10, 5 );
		add_filter( 'update_post_metadata', array( __CLASS__, 'update_post_metadata' ), 10, 5 );
		add_filter( 'wp_insert_post_data', array( __CLASS__, 'wp_insert_post_data' ) );
		add_filter( 'oembed_response_data', array( __CLASS__, 'filter_oembed_response_data' ), 10, 2 );
		add_filter( 'wp_untrash_post_status', array( __CLASS__, 'wp_untrash_post_status' ), 10, 3 );

		// Status transitions.
		add_action( 'transition_post_status', array( __CLASS__, 'transition_post_status' ), 10, 3 );
		add_action( 'delete_post', array( __CLASS__, 'delete_post' ) );
		add_action( 'wp_trash_post', array( __CLASS__, 'trash_post' ) );
		add_action( 'untrashed_post', array( __CLASS__, 'untrash_post' ) );
		add_action( 'before_delete_post', array( __CLASS__, 'before_delete_order' ) );

		// Meta cache flushing.
		add_action( 'updated_post_meta', array( __CLASS__, 'flush_object_meta_cache' ), 10, 4 );
		add_action( 'updated_order_item_meta', array( __CLASS__, 'flush_object_meta_cache' ), 10, 4 );
	}

	/**
	 * Link to parent products when getting permalink for variation.
	 *
	 * @param string  $permalink Permalink.
	 * @param WP_Post $post      Post data.
	 *
	 * @return string
	 */
	public static function variation_post_link( $permalink, $post ) {
		if ( isset( $post->ID, $post->post_type ) && 'product_variation' === $post->post_type ) {
			$variation = wc_get_product( $post->ID );

			if ( $variation && $variation->get_parent_id() ) {
				return $variation->get_permalink();
			}
		}
		return $permalink;
	}

	/**
	 * Sync products queued to sync.
	 */
	public static function do_deferred_product_sync() {
		global $wc_deferred_product_sync;

		if ( ! empty( $wc_deferred_product_sync ) ) {
			$wc_deferred_product_sync = wp_parse_id_list( $wc_deferred_product_sync );
			array_walk( $wc_deferred_product_sync, array( __CLASS__, 'deferred_product_sync' ) );
		}
	}

	/**
	 * Sync a product.
	 *
	 * @param int $product_id Product ID.
	 */
	public static function deferred_product_sync( $product_id ) {
		$product = wc_get_product( $product_id );

		if ( is_callable( array( $product, 'sync' ) ) ) {
			$product->sync( $product );
		}
	}

	/**
	 * When a post status changes.
	 *
	 * @param string  $new_status New status.
	 * @param string  $old_status Old status.
	 * @param WP_Post $post       Post data.
	 */
	public static function transition_post_status( $new_status, $old_status, $post ) {
		if ( ( 'publish' === $new_status || 'publish' === $old_status ) && in_array( $post->post_type, array( 'product', 'product_variation' ), true ) ) {
			self::delete_product_query_transients();
		}
	}

	/**
	 * Delete product view transients when needed e.g. when post status changes, or visibility/stock status is modified.
	 */
	public static function delete_product_query_transients() {
		WC_Cache_Helper::get_transient_version( 'product_query', true );
	}

	/**
	 * Handle type changes.
	 *
	 * @since 3.0.0
	 *
	 * @param WC_Product $product Product data.
	 * @param string     $from    Origin type.
	 * @param string     $to      New type.
	 */
	public static function product_type_changed( $product, $from, $to ) {
		/**
		 * Filter to prevent variations from being deleted while switching from a variable product type to a variable product type.
		 *
		 * @since 5.0.0
		 *
		 * @param bool       A boolean value of true will delete the variations.
		 * @param WC_Product $product Product data.
		 * @return string    $from    Origin type.
		 * @param string     $to      New type.
		 */
		if ( apply_filters( 'woocommerce_delete_variations_on_product_type_change', 'variable' === $from && 'variable' !== $to, $product, $from, $to ) ) {
			// If the product is no longer variable, we should ensure all variations are removed.
			$data_store = WC_Data_Store::load( 'product-variable' );
			$data_store->delete_variations( $product->get_id(), true );
		}
	}

	/**
	 * When editing a term, check for product attributes.
	 *
	 * @param  int    $term_id  Term ID.
	 * @param  int    $tt_id    Term taxonomy ID.
	 * @param  string $taxonomy Taxonomy slug.
	 */
	public static function edit_term( $term_id, $tt_id, $taxonomy ) {
		if ( strpos( $taxonomy, 'pa_' ) === 0 ) {
			self::$editing_term = get_term_by( 'id', $term_id, $taxonomy );
		} else {
			self::$editing_term = null;
		}
	}

	/**
	 * When a term is edited, check for product attributes and update variations.
	 *
	 * @param  int    $term_id  Term ID.
	 * @param  int    $tt_id    Term taxonomy ID.
	 * @param  string $taxonomy Taxonomy slug.
	 */
	public static function edited_term( $term_id, $tt_id, $taxonomy ) {
		if ( ! is_null( self::$editing_term ) && strpos( $taxonomy, 'pa_' ) === 0 ) {
			$edited_term = get_term_by( 'id', $term_id, $taxonomy );

			if ( $edited_term->slug !== self::$editing_term->slug ) {
				global $wpdb;

				$wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = %s WHERE meta_key = %s AND meta_value = %s;", $edited_term->slug, 'attribute_' . sanitize_title( $taxonomy ), self::$editing_term->slug ) );

				$wpdb->query(
					$wpdb->prepare(
						"UPDATE {$wpdb->postmeta} SET meta_value = REPLACE( meta_value, %s, %s ) WHERE meta_key = '_default_attributes'",
						serialize( self::$editing_term->taxonomy ) . serialize( self::$editing_term->slug ),
						serialize( $edited_term->taxonomy ) . serialize( $edited_term->slug )
					)
				);
			}
		} else {
			self::$editing_term = null;
		}
	}

	/**
	 * Ensure floats are correctly converted to strings based on PHP locale.
	 *
	 * @param  null   $check      Whether to allow updating metadata for the given type.
	 * @param  int    $object_id  Object ID.
	 * @param  string $meta_key   Meta key.
	 * @param  mixed  $meta_value Meta value. Must be serializable if non-scalar.
	 * @param  mixed  $prev_value If specified, only update existing metadata entries with the specified value. Otherwise, update all entries.
	 * @return null|bool
	 */
	public static function update_order_item_metadata( $check, $object_id, $meta_key, $meta_value, $prev_value ) {
		if ( ! empty( $meta_value ) && is_float( $meta_value ) ) {

			// Convert float to string.
			$meta_value = wc_float_to_string( $meta_value );

			// Update meta value with new string.
			update_metadata( 'order_item', $object_id, $meta_key, $meta_value, $prev_value );

			return true;
		}
		return $check;
	}

	/**
	 * Ensure floats are correctly converted to strings based on PHP locale.
	 *
	 * @param  null   $check      Whether to allow updating metadata for the given type.
	 * @param  int    $object_id  Object ID.
	 * @param  string $meta_key   Meta key.
	 * @param  mixed  $meta_value Meta value. Must be serializable if non-scalar.
	 * @param  mixed  $prev_value If specified, only update existing metadata entries with the specified value. Otherwise, update all entries.
	 * @return null|bool
	 */
	public static function update_post_metadata( $check, $object_id, $meta_key, $meta_value, $prev_value ) {
		// Delete product cache if someone uses meta directly.
		if ( in_array( get_post_type( $object_id ), array( 'product', 'product_variation' ), true ) ) {
			wp_cache_delete( 'product-' . $object_id, 'products' );
		}

		if ( ! empty( $meta_value ) && is_float( $meta_value ) && ! registered_meta_key_exists( 'post', $meta_key ) && in_array( get_post_type( $object_id ), array_merge( wc_get_order_types(), array( 'shop_coupon', 'product', 'product_variation' ) ), true ) ) {

			// Convert float to string.
			$meta_value = wc_float_to_string( $meta_value );

			// Update meta value with new string.
			update_metadata( 'post', $object_id, $meta_key, $meta_value, $prev_value );

			return true;
		}
		return $check;
	}

	/**
	 * Forces the order posts to have a title in a certain format (containing the date).
	 * Forces certain product data based on the product's type, e.g. grouped products cannot have a parent.
	 *
	 * @param array $data An array of slashed post data.
	 * @return array
	 */
	public static function wp_insert_post_data( $data ) {
		if ( 'shop_order' === $data['post_type'] && isset( $data['post_date'] ) ) {
			$order_title = 'Order';
			if ( $data['post_date'] ) {
				$order_title .= ' &ndash; ' . date_i18n( 'F j, Y @ h:i A', strtotime( $data['post_date'] ) );
			}
			$data['post_title'] = $order_title;
		} elseif ( 'product' === $data['post_type'] && isset( $_POST['product-type'] ) ) { // WPCS: input var ok, CSRF ok.
			$product_type = wc_clean( wp_unslash( $_POST['product-type'] ) ); // WPCS: input var ok, CSRF ok.
			switch ( $product_type ) {
				case 'grouped':
				case 'variable':
					$data['post_parent'] = 0;
					break;
			}
		} elseif ( 'product' === $data['post_type'] && 'auto-draft' === $data['post_status'] ) {
			$data['post_title'] = 'AUTO-DRAFT';
		} elseif ( 'shop_coupon' === $data['post_type'] ) {
			// Coupons should never allow unfiltered HTML.
			$data['post_title'] = wp_filter_kses( $data['post_title'] );
		}

		return $data;
	}

	/**
	 * Change embed data for certain post types.
	 *
	 * @since 3.2.0
	 * @param array   $data The response data.
	 * @param WP_Post $post The post object.
	 * @return array
	 */
	public static function filter_oembed_response_data( $data, $post ) {
		if ( in_array( $post->post_type, array( 'shop_order', 'shop_coupon' ), true ) ) {
			return array();
		}
		return $data;
	}

	/**
	 * Removes variations etc belonging to a deleted post, and clears transients.
	 *
	 * @param mixed $id ID of post being deleted.
	 */
	public static function delete_post( $id ) {
		$container = wc_get_container();
		if ( ! $container->get( LegacyProxy::class )->call_function( 'current_user_can', 'delete_posts' ) || ! $id ) {
			return;
		}

		$post_type = self::get_post_type( $id );
		switch ( $post_type ) {
			case 'product':
				$data_store = WC_Data_Store::load( 'product-variable' );
				$data_store->delete_variations( $id, true );
				$data_store->delete_from_lookup_table( $id, 'wc_product_meta_lookup' );
				$container->get( ProductAttributesLookupDataStore::class )->on_product_deleted( $id );

				$parent_id = wp_get_post_parent_id( $id );
				if ( $parent_id ) {
					wc_delete_product_transients( $parent_id );
				}

				break;
			case 'product_variation':
				$data_store = WC_Data_Store::load( 'product' );
				$data_store->delete_from_lookup_table( $id, 'wc_product_meta_lookup' );
				wc_delete_product_transients( wp_get_post_parent_id( $id ) );
				$container->get( ProductAttributesLookupDataStore::class )->on_product_deleted( $id );

				break;
			case 'shop_order':
				global $wpdb;

				$refunds = $wpdb->get_results( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type = 'shop_order_refund' AND post_parent = %d", $id ) );

				if ( ! is_null( $refunds ) ) {
					foreach ( $refunds as $refund ) {
						wp_delete_post( $refund->ID, true );
					}
				}
				break;
		}
	}

	/**
	 * Trash post.
	 *
	 * @param mixed $id Post ID.
	 */
	public static function trash_post( $id ) {
		if ( ! $id ) {
			return;
		}

		$post_type = self::get_post_type( $id );

		// If this is an order, trash any refunds too.
		if ( in_array( $post_type, wc_get_order_types( 'order-count' ), true ) ) {
			global $wpdb;

			$refunds = $wpdb->get_results( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type = 'shop_order_refund' AND post_parent = %d", $id ) );

			foreach ( $refunds as $refund ) {
				$wpdb->update( $wpdb->posts, array( 'post_status' => 'trash' ), array( 'ID' => $refund->ID ) );
			}

			wc_delete_shop_order_transients( $id );

			// If this is a product, trash children variations.
		} elseif ( 'product' === $post_type ) {
			$data_store = WC_Data_Store::load( 'product-variable' );
			$data_store->delete_variations( $id, false );
			wc_get_container()->get( ProductAttributesLookupDataStore::class )->on_product_deleted( $id );
		} elseif ( 'product_variation' === $post_type ) {
			wc_get_container()->get( ProductAttributesLookupDataStore::class )->on_product_deleted( $id );
		}
	}

	/**
	 * Untrash post.
	 *
	 * @param mixed $id Post ID.
	 */
	public static function untrash_post( $id ) {
		if ( ! $id ) {
			return;
		}

		$post_type = self::get_post_type( $id );

		if ( in_array( $post_type, wc_get_order_types( 'order-count' ), true ) ) {
			global $wpdb;

			$refunds = $wpdb->get_results( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type = 'shop_order_refund' AND post_parent = %d", $id ) );

			foreach ( $refunds as $refund ) {
				$wpdb->update( $wpdb->posts, array( 'post_status' => 'wc-completed' ), array( 'ID' => $refund->ID ) );
			}

			wc_delete_shop_order_transients( $id );

		} elseif ( 'product' === $post_type ) {
			$data_store = WC_Data_Store::load( 'product-variable' );
			$data_store->untrash_variations( $id );

			wc_product_force_unique_sku( $id );

			wc_get_container()->get( ProductAttributesLookupDataStore::class )->on_product_changed( $id );
		} elseif ( 'product_variation' === $post_type ) {
			wc_get_container()->get( ProductAttributesLookupDataStore::class )->on_product_changed( $id );
		}
	}

	/**
	 * Get the post type for a given post.
	 *
	 * @param int $id The post id.
	 * @return string The post type.
	 */
	private static function get_post_type( $id ) {
		return wc_get_container()->get( LegacyProxy::class )->call_function( 'get_post_type', $id );
	}

	/**
	 * Before deleting an order, do some cleanup.
	 *
	 * @since 3.2.0
	 * @param int $order_id Order ID.
	 */
	public static function before_delete_order( $order_id ) {
		if ( in_array( get_post_type( $order_id ), wc_get_order_types(), true ) ) {
			// Clean up user.
			$order = wc_get_order( $order_id );

			// Check for `get_customer_id`, since this may be e.g. a refund order (which doesn't implement it).
			$customer_id = is_callable( array( $order, 'get_customer_id' ) ) ? $order->get_customer_id() : 0;

			if ( $customer_id > 0 && 'shop_order' === $order->get_type() ) {
				$customer    = new WC_Customer( $customer_id );
				$order_count = $customer->get_order_count();
				$order_count --;

				if ( 0 === $order_count ) {
					$customer->set_is_paying_customer( false );
					$customer->save();
				}

				// Delete order count and last order meta.
				delete_user_meta( $customer_id, '_order_count' );
				delete_user_meta( $customer_id, '_last_order' );
			}

			// Clean up items.
			self::delete_order_items( $order_id );
			self::delete_order_downloadable_permissions( $order_id );
		}
	}

	/**
	 * Remove item meta on permanent deletion.
	 *
	 * @param int $postid Post ID.
	 */
	public static function delete_order_items( $postid ) {
		global $wpdb;

		if ( in_array( get_post_type( $postid ), wc_get_order_types(), true ) ) {
			do_action( 'woocommerce_delete_order_items', $postid );

			$wpdb->query(
				"
				DELETE {$wpdb->prefix}woocommerce_order_items, {$wpdb->prefix}woocommerce_order_itemmeta
				FROM {$wpdb->prefix}woocommerce_order_items
				JOIN {$wpdb->prefix}woocommerce_order_itemmeta ON {$wpdb->prefix}woocommerce_order_items.order_item_id = {$wpdb->prefix}woocommerce_order_itemmeta.order_item_id
				WHERE {$wpdb->prefix}woocommerce_order_items.order_id = '{$postid}';
				"
			); // WPCS: unprepared SQL ok.

			do_action( 'woocommerce_deleted_order_items', $postid );
		}
	}

	/**
	 * Remove downloadable permissions on permanent order deletion.
	 *
	 * @param int $postid Post ID.
	 */
	public static function delete_order_downloadable_permissions( $postid ) {
		if ( in_array( get_post_type( $postid ), wc_get_order_types(), true ) ) {
			do_action( 'woocommerce_delete_order_downloadable_permissions', $postid );

			$data_store = WC_Data_Store::load( 'customer-download' );
			$data_store->delete_by_order_id( $postid );

			do_action( 'woocommerce_deleted_order_downloadable_permissions', $postid );
		}
	}

	/**
	 * Flush meta cache for CRUD objects on direct update.
	 *
	 * @param  int    $meta_id    Meta ID.
	 * @param  int    $object_id  Object ID.
	 * @param  string $meta_key   Meta key.
	 * @param  string $meta_value Meta value.
	 */
	public static function flush_object_meta_cache( $meta_id, $object_id, $meta_key, $meta_value ) {
		WC_Cache_Helper::invalidate_cache_group( 'object_' . $object_id );
	}

	/**
	 * Ensure default category gets set.
	 *
	 * @since 3.3.0
	 * @param int    $object_id Product ID.
	 * @param array  $terms     Terms array.
	 * @param array  $tt_ids    Term ids array.
	 * @param string $taxonomy  Taxonomy name.
	 * @param bool   $append    Are we appending or setting terms.
	 */
	public static function force_default_term( $object_id, $terms, $tt_ids, $taxonomy, $append ) {
		if ( ! $append && 'product_cat' === $taxonomy && empty( $tt_ids ) && 'product' === get_post_type( $object_id ) ) {
			$default_term = absint( get_option( 'default_product_cat', 0 ) );
			$tt_ids       = array_map( 'absint', $tt_ids );

			if ( $default_term && ! in_array( $default_term, $tt_ids, true ) ) {
				wp_set_post_terms( $object_id, array( $default_term ), 'product_cat', true );
			}
		}
	}

	/**
	 * Ensure statuses are correctly reassigned when restoring orders and products.
	 *
	 * @param string $new_status      The new status of the post being restored.
	 * @param int    $post_id         The ID of the post being restored.
	 * @param string $previous_status The status of the post at the point where it was trashed.
	 * @return string
	 */
	public static function wp_untrash_post_status( $new_status, $post_id, $previous_status ) {
		$post_types = array( 'shop_order', 'shop_coupon', 'product', 'product_variation' );

		if ( in_array( get_post_type( $post_id ), $post_types, true ) ) {
			$new_status = $previous_status;
		}

		return $new_status;
	}

	/**
	 * When setting stock level, ensure the stock status is kept in sync.
	 *
	 * @param  int    $meta_id    Meta ID.
	 * @param  int    $object_id  Object ID.
	 * @param  string $meta_key   Meta key.
	 * @param  mixed  $meta_value Meta value.
	 * @deprecated    3.3
	 */
	public static function sync_product_stock_status( $meta_id, $object_id, $meta_key, $meta_value ) {}

	/**
	 * Update changed downloads.
	 *
	 * @deprecated  3.3.0 No action is necessary on changes to download paths since download_id is no longer based on file hash.
	 * @param int   $product_id   Product ID.
	 * @param int   $variation_id Variation ID. Optional product variation identifier.
	 * @param array $downloads    Newly set files.
	 */
	public static function process_product_file_download_paths( $product_id, $variation_id, $downloads ) {
		wc_deprecated_function( __FUNCTION__, '3.3' );
	}

	/**
	 * Delete transients when terms are set.
	 *
	 * @deprecated   3.6
	 * @param int    $object_id  Object ID.
	 * @param mixed  $terms      An array of object terms.
	 * @param array  $tt_ids     An array of term taxonomy IDs.
	 * @param string $taxonomy   Taxonomy slug.
	 * @param mixed  $append     Whether to append new terms to the old terms.
	 * @param array  $old_tt_ids Old array of term taxonomy IDs.
	 */
	public static function set_object_terms( $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) {
		if ( in_array( get_post_type( $object_id ), array( 'product', 'product_variation' ), true ) ) {
			self::delete_product_query_transients();
		}
	}
}

WC_Post_Data::init();
traits/trait-wc-item-totals.php000064400000004114151336044770012567 0ustar00<?php
/**
 * This ongoing trait will have shared calculation logic between WC_Abstract_Order and WC_Cart_Totals classes.
 *
 * @package WooCommerce\Traits
 * @version 3.9.0
 */

use Automattic\WooCommerce\Utilities\NumberUtil;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Trait WC_Item_Totals.
 *
 * Right now this do not have much, but plan is to eventually move all shared calculation logic between Orders and Cart in this file.
 *
 * @since 3.9.0
 */
trait WC_Item_Totals {

	/**
	 * Line items to calculate. Define in child class.
	 *
	 * @since 3.9.0
	 * @param string $field Field name to calculate upon.
	 *
	 * @return array having `total`|`subtotal` property.
	 */
	abstract protected function get_values_for_total( $field );

	/**
	 * Return rounded total based on settings. Will be used by Cart and Orders.
	 *
	 * @since 3.9.0
	 *
	 * @param array $values Values to round. Should be with precision.
	 *
	 * @return float|int Appropriately rounded value.
	 */
	public static function get_rounded_items_total( $values ) {
		return array_sum(
			array_map(
				array( self::class, 'round_item_subtotal' ),
				$values
			)
		);
	}

	/**
	 * Apply rounding to item subtotal before summing.
	 *
	 * @since 3.9.0
	 * @param float $value Item subtotal value.
	 * @return float
	 */
	public static function round_item_subtotal( $value ) {
		if ( ! self::round_at_subtotal() ) {
			$value = NumberUtil::round( $value );
		}
		return $value;
	}

	/**
	 * Should always round at subtotal?
	 *
	 * @since 3.9.0
	 * @return bool
	 */
	protected static function round_at_subtotal() {
		return 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' );
	}

	/**
	 * Apply rounding to an array of taxes before summing. Rounds to store DP setting, ignoring precision.
	 *
	 * @since  3.2.6
	 * @param  float $value    Tax value.
	 * @param  bool  $in_cents Whether precision of value is in cents.
	 * @return float
	 */
	protected static function round_line_tax( $value, $in_cents = true ) {
		if ( ! self::round_at_subtotal() ) {
			$value = wc_round_tax_total( $value, $in_cents ? 0 : null );
		}
		return $value;
	}

}
class-wc-https.php000064400000010510151336044770010140 0ustar00<?php

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

/**
 * WC_HTTPS class.
 *
 * @class    WC_HTTPS
 * @version  2.2.0
 * @package  WooCommerce\Classes
 * @category Class
 * @author   WooThemes
 */
class WC_HTTPS {

	/**
	 * Hook in our HTTPS functions if we're on the frontend. This will ensure any links output to a page (when viewing via HTTPS) are also served over HTTPS.
	 */
	public static function init() {
		if ( 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) && ! is_admin() ) {
			// HTTPS urls with SSL on
			$filters = array(
				'post_thumbnail_html',
				'wp_get_attachment_image_attributes',
				'wp_get_attachment_url',
				'option_stylesheet_url',
				'option_template_url',
				'script_loader_src',
				'style_loader_src',
				'template_directory_uri',
				'stylesheet_directory_uri',
				'site_url',
			);

			foreach ( $filters as $filter ) {
				add_filter( $filter, array( __CLASS__, 'force_https_url' ), 999 );
			}

			add_filter( 'page_link', array( __CLASS__, 'force_https_page_link' ), 10, 2 );
			add_action( 'template_redirect', array( __CLASS__, 'force_https_template_redirect' ) );

			if ( 'yes' == get_option( 'woocommerce_unforce_ssl_checkout' ) ) {
				add_action( 'template_redirect', array( __CLASS__, 'unforce_https_template_redirect' ) );
			}
		}
		add_action( 'http_api_curl', array( __CLASS__, 'http_api_curl' ), 10, 3 );
	}

	/**
	 * Force https for urls.
	 *
	 * @param mixed $content
	 * @return string
	 */
	public static function force_https_url( $content ) {
		if ( is_ssl() ) {
			if ( is_array( $content ) ) {
				$content = array_map( 'WC_HTTPS::force_https_url', $content );
			} else {
				$content = str_replace( 'http:', 'https:', $content );
			}
		}
		return $content;
	}

	/**
	 * Force a post link to be SSL if needed.
	 *
	 * @param string $link
	 * @param int $page_id
	 *
	 * @return string
	 */
	public static function force_https_page_link( $link, $page_id ) {
		if ( in_array( $page_id, array( get_option( 'woocommerce_checkout_page_id' ), get_option( 'woocommerce_myaccount_page_id' ) ) ) ) {
			$link = str_replace( 'http:', 'https:', $link );
		} elseif ( 'yes' === get_option( 'woocommerce_unforce_ssl_checkout' ) && ! wc_site_is_https() ) {
			$link = str_replace( 'https:', 'http:', $link );
		}
		return $link;
	}

	/**
	 * Template redirect - if we end up on a page ensure it has the correct http/https url.
	 */
	public static function force_https_template_redirect() {
		if ( ! is_ssl() && ( is_checkout() || is_account_page() || apply_filters( 'woocommerce_force_ssl_checkout', false ) ) ) {

			if ( 0 === strpos( $_SERVER['REQUEST_URI'], 'http' ) ) {
				wp_safe_redirect( preg_replace( '|^http://|', 'https://', $_SERVER['REQUEST_URI'] ) );
				exit;
			} else {
				wp_safe_redirect( 'https://' . ( ! empty( $_SERVER['HTTP_X_FORWARDED_HOST'] ) ? $_SERVER['HTTP_X_FORWARDED_HOST'] : $_SERVER['HTTP_HOST'] ) . $_SERVER['REQUEST_URI'] );
				exit;
			}
		}
	}

	/**
	 * Template redirect - if we end up on a page ensure it has the correct http/https url.
	 */
	public static function unforce_https_template_redirect() {
		if ( function_exists( 'is_customize_preview' ) && is_customize_preview() ) {
			return;
		}

		if ( ! wc_site_is_https() && is_ssl() && $_SERVER['REQUEST_URI'] && ! is_checkout() && ! is_ajax() && ! is_account_page() && apply_filters( 'woocommerce_unforce_ssl_checkout', true ) ) {

			if ( 0 === strpos( $_SERVER['REQUEST_URI'], 'http' ) ) {
				wp_safe_redirect( preg_replace( '|^https://|', 'http://', $_SERVER['REQUEST_URI'] ) );
				exit;
			} else {
				wp_safe_redirect( 'http://' . ( ! empty( $_SERVER['HTTP_X_FORWARDED_HOST'] ) ? $_SERVER['HTTP_X_FORWARDED_HOST'] : $_SERVER['HTTP_HOST'] ) . $_SERVER['REQUEST_URI'] );
				exit;
			}
		}
	}

	/**
	 * Force posts to PayPal to use TLS v1.2. See:
	 *        https://core.trac.wordpress.org/ticket/36320
	 *        https://core.trac.wordpress.org/ticket/34924#comment:13
	 *        https://www.paypal-knowledge.com/infocenter/index?page=content&widgetview=true&id=FAQ1914&viewlocale=en_US
	 *
	 * @param string $handle
	 * @param mixed $r
	 * @param string $url
	 */
	public static function http_api_curl( $handle, $r, $url ) {
		if ( strstr( $url, 'https://' ) && ( strstr( $url, '.paypal.com/nvp' ) || strstr( $url, '.paypal.com/cgi-bin/webscr' ) ) ) {
			curl_setopt( $handle, CURLOPT_SSLVERSION, 6 );
		}
	}
}

WC_HTTPS::init();
class-wc-install.php000064400000172146151336044770010462 0ustar00<?php
/**
 * Installation related functions and actions.
 *
 * @package WooCommerce\Classes
 * @version 3.0.0
 */

use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Internal\WCCom\ConnectionHelper as WCConnectionHelper;

defined( 'ABSPATH' ) || exit;

/**
 * WC_Install Class.
 */
class WC_Install {

	/**
	 * DB updates and callbacks that need to be run per version.
	 *
	 * @var array
	 */
	private static $db_updates = array(
		'2.0.0' => array(
			'wc_update_200_file_paths',
			'wc_update_200_permalinks',
			'wc_update_200_subcat_display',
			'wc_update_200_taxrates',
			'wc_update_200_line_items',
			'wc_update_200_images',
			'wc_update_200_db_version',
		),
		'2.0.9' => array(
			'wc_update_209_brazillian_state',
			'wc_update_209_db_version',
		),
		'2.1.0' => array(
			'wc_update_210_remove_pages',
			'wc_update_210_file_paths',
			'wc_update_210_db_version',
		),
		'2.2.0' => array(
			'wc_update_220_shipping',
			'wc_update_220_order_status',
			'wc_update_220_variations',
			'wc_update_220_attributes',
			'wc_update_220_db_version',
		),
		'2.3.0' => array(
			'wc_update_230_options',
			'wc_update_230_db_version',
		),
		'2.4.0' => array(
			'wc_update_240_options',
			'wc_update_240_shipping_methods',
			'wc_update_240_api_keys',
			'wc_update_240_refunds',
			'wc_update_240_db_version',
		),
		'2.4.1' => array(
			'wc_update_241_variations',
			'wc_update_241_db_version',
		),
		'2.5.0' => array(
			'wc_update_250_currency',
			'wc_update_250_db_version',
		),
		'2.6.0' => array(
			'wc_update_260_options',
			'wc_update_260_termmeta',
			'wc_update_260_zones',
			'wc_update_260_zone_methods',
			'wc_update_260_refunds',
			'wc_update_260_db_version',
		),
		'3.0.0' => array(
			'wc_update_300_grouped_products',
			'wc_update_300_settings',
			'wc_update_300_product_visibility',
			'wc_update_300_db_version',
		),
		'3.1.0' => array(
			'wc_update_310_downloadable_products',
			'wc_update_310_old_comments',
			'wc_update_310_db_version',
		),
		'3.1.2' => array(
			'wc_update_312_shop_manager_capabilities',
			'wc_update_312_db_version',
		),
		'3.2.0' => array(
			'wc_update_320_mexican_states',
			'wc_update_320_db_version',
		),
		'3.3.0' => array(
			'wc_update_330_image_options',
			'wc_update_330_webhooks',
			'wc_update_330_product_stock_status',
			'wc_update_330_set_default_product_cat',
			'wc_update_330_clear_transients',
			'wc_update_330_set_paypal_sandbox_credentials',
			'wc_update_330_db_version',
		),
		'3.4.0' => array(
			'wc_update_340_states',
			'wc_update_340_state',
			'wc_update_340_last_active',
			'wc_update_340_db_version',
		),
		'3.4.3' => array(
			'wc_update_343_cleanup_foreign_keys',
			'wc_update_343_db_version',
		),
		'3.4.4' => array(
			'wc_update_344_recreate_roles',
			'wc_update_344_db_version',
		),
		'3.5.0' => array(
			'wc_update_350_reviews_comment_type',
			'wc_update_350_db_version',
		),
		'3.5.2' => array(
			'wc_update_352_drop_download_log_fk',
		),
		'3.5.4' => array(
			'wc_update_354_modify_shop_manager_caps',
			'wc_update_354_db_version',
		),
		'3.6.0' => array(
			'wc_update_360_product_lookup_tables',
			'wc_update_360_term_meta',
			'wc_update_360_downloadable_product_permissions_index',
			'wc_update_360_db_version',
		),
		'3.7.0' => array(
			'wc_update_370_tax_rate_classes',
			'wc_update_370_mro_std_currency',
			'wc_update_370_db_version',
		),
		'3.9.0' => array(
			'wc_update_390_move_maxmind_database',
			'wc_update_390_change_geolocation_database_update_cron',
			'wc_update_390_db_version',
		),
		'4.0.0' => array(
			'wc_update_product_lookup_tables',
			'wc_update_400_increase_size_of_column',
			'wc_update_400_reset_action_scheduler_migration_status',
			'wc_update_400_db_version',
		),
		'4.4.0' => array(
			'wc_update_440_insert_attribute_terms_for_variable_products',
			'wc_update_440_db_version',
		),
		'4.5.0' => array(
			'wc_update_450_sanitize_coupons_code',
			'wc_update_450_db_version',
		),
		'5.0.0' => array(
			'wc_update_500_fix_product_review_count',
			'wc_update_500_db_version',
		),
		'5.6.0' => array(
			'wc_update_560_create_refund_returns_page',
			'wc_update_560_db_version',
		),
	);

	/**
	 * Hook in tabs.
	 */
	public static function init() {
		add_action( 'init', array( __CLASS__, 'check_version' ), 5 );
		add_action( 'init', array( __CLASS__, 'manual_database_update' ), 20 );
		add_action( 'admin_init', array( __CLASS__, 'wc_admin_db_update_notice' ) );
		add_action( 'admin_init', array( __CLASS__, 'add_admin_note_after_page_created' ) );
		add_action( 'woocommerce_run_update_callback', array( __CLASS__, 'run_update_callback' ) );
		add_action( 'admin_init', array( __CLASS__, 'install_actions' ) );
		add_action( 'woocommerce_page_created', array( __CLASS__, 'page_created' ), 10, 2 );
		add_filter( 'plugin_action_links_' . WC_PLUGIN_BASENAME, array( __CLASS__, 'plugin_action_links' ) );
		add_filter( 'plugin_row_meta', array( __CLASS__, 'plugin_row_meta' ), 10, 2 );
		add_filter( 'wpmu_drop_tables', array( __CLASS__, 'wpmu_drop_tables' ) );
		add_filter( 'cron_schedules', array( __CLASS__, 'cron_schedules' ) );
	}

	/**
	 * Check WooCommerce version and run the updater is required.
	 *
	 * This check is done on all requests and runs if the versions do not match.
	 */
	public static function check_version() {
		if ( ! Constants::is_defined( 'IFRAME_REQUEST' ) && version_compare( get_option( 'woocommerce_version' ), WC()->version, '<' ) ) {
			self::install();
			do_action( 'woocommerce_updated' );
		}
	}

	/**
	 * Performan manual database update when triggered by WooCommerce System Tools.
	 *
	 * @since 3.6.5
	 */
	public static function manual_database_update() {
		$blog_id = get_current_blog_id();

		add_action( 'wp_' . $blog_id . '_wc_updater_cron', array( __CLASS__, 'run_manual_database_update' ) );
	}

	/**
	 * Add WC Admin based db update notice.
	 *
	 * @since 4.0.0
	 */
	public static function wc_admin_db_update_notice() {
		if (
			WC()->is_wc_admin_active() &&
			false !== get_option( 'woocommerce_admin_install_timestamp' )
		) {
			new WC_Notes_Run_Db_Update();
		}
	}

	/**
	 * Run manual database update.
	 */
	public static function run_manual_database_update() {
		self::update();
	}

	/**
	 * Run an update callback when triggered by ActionScheduler.
	 *
	 * @param string $update_callback Callback name.
	 *
	 * @since 3.6.0
	 */
	public static function run_update_callback( $update_callback ) {
		include_once dirname( __FILE__ ) . '/wc-update-functions.php';

		if ( is_callable( $update_callback ) ) {
			self::run_update_callback_start( $update_callback );
			$result = (bool) call_user_func( $update_callback );
			self::run_update_callback_end( $update_callback, $result );
		}
	}

	/**
	 * Triggered when a callback will run.
	 *
	 * @since 3.6.0
	 * @param string $callback Callback name.
	 */
	protected static function run_update_callback_start( $callback ) {
		wc_maybe_define_constant( 'WC_UPDATING', true );
	}

	/**
	 * Triggered when a callback has ran.
	 *
	 * @since 3.6.0
	 * @param string $callback Callback name.
	 * @param bool   $result Return value from callback. Non-false need to run again.
	 */
	protected static function run_update_callback_end( $callback, $result ) {
		if ( $result ) {
			WC()->queue()->add(
				'woocommerce_run_update_callback',
				array(
					'update_callback' => $callback,
				),
				'woocommerce-db-updates'
			);
		}
	}

	/**
	 * Install actions when a update button is clicked within the admin area.
	 *
	 * This function is hooked into admin_init to affect admin only.
	 */
	public static function install_actions() {
		if ( ! empty( $_GET['do_update_woocommerce'] ) ) { // WPCS: input var ok.
			check_admin_referer( 'wc_db_update', 'wc_db_update_nonce' );
			self::update();
			WC_Admin_Notices::add_notice( 'update', true );
		}
	}

	/**
	 * Install WC.
	 */
	public static function install() {
		if ( ! is_blog_installed() ) {
			return;
		}

		// Check if we are not already running this routine.
		if ( 'yes' === get_transient( 'wc_installing' ) ) {
			return;
		}

		// If we made it till here nothing is running yet, lets set the transient now.
		set_transient( 'wc_installing', 'yes', MINUTE_IN_SECONDS * 10 );
		wc_maybe_define_constant( 'WC_INSTALLING', true );

		WC()->wpdb_table_fix();
		self::remove_admin_notices();
		self::create_tables();
		self::verify_base_tables();
		self::create_options();
		self::create_roles();
		self::setup_environment();
		self::create_terms();
		self::create_cron_jobs();
		self::create_files();
		self::maybe_create_pages();
		self::maybe_set_activation_transients();
		self::set_paypal_standard_load_eligibility();
		self::update_wc_version();
		self::maybe_update_db_version();

		delete_transient( 'wc_installing' );

		do_action( 'woocommerce_flush_rewrite_rules' );
		do_action( 'woocommerce_installed' );
	}

	/**
	 * Check if all the base tables are present.
	 *
	 * @param bool $modify_notice Whether to modify notice based on if all tables are present.
	 * @param bool $execute       Whether to execute get_schema queries as well.
	 *
	 * @return array List of querues.
	 */
	public static function verify_base_tables( $modify_notice = true, $execute = false ) {
		require_once ABSPATH . 'wp-admin/includes/upgrade.php';

		if ( $execute ) {
			self::create_tables();
		}
		$queries        = dbDelta( self::get_schema(), false );
		$missing_tables = array();
		foreach ( $queries as $table_name => $result ) {
			if ( "Created table $table_name" === $result ) {
				$missing_tables[] = $table_name;
			}
		}

		if ( 0 < count( $missing_tables ) ) {
			if ( $modify_notice ) {
				WC_Admin_Notices::add_notice( 'base_tables_missing' );
			}
			update_option( 'woocommerce_schema_missing_tables', $missing_tables );
		} else {
			if ( $modify_notice ) {
				WC_Admin_Notices::remove_notice( 'base_tables_missing' );
			}
			update_option( 'woocommerce_schema_version', WC()->db_version );
			delete_option( 'woocommerce_schema_missing_tables' );
		}
		return $missing_tables;
	}

	/**
	 * Reset any notices added to admin.
	 *
	 * @since 3.2.0
	 */
	private static function remove_admin_notices() {
		include_once dirname( __FILE__ ) . '/admin/class-wc-admin-notices.php';
		WC_Admin_Notices::remove_all_notices();
	}

	/**
	 * Setup WC environment - post types, taxonomies, endpoints.
	 *
	 * @since 3.2.0
	 */
	private static function setup_environment() {
		WC_Post_types::register_post_types();
		WC_Post_types::register_taxonomies();
		WC()->query->init_query_vars();
		WC()->query->add_endpoints();
		WC_API::add_endpoint();
		WC_Auth::add_endpoint();
	}

	/**
	 * Is this a brand new WC install?
	 *
	 * A brand new install has no version yet. Also treat empty installs as 'new'.
	 *
	 * @since  3.2.0
	 * @return boolean
	 */
	public static function is_new_install() {
		$product_count = array_sum( (array) wp_count_posts( 'product' ) );

		return is_null( get_option( 'woocommerce_version', null ) ) || ( 0 === $product_count && -1 === wc_get_page_id( 'shop' ) );
	}

	/**
	 * Is a DB update needed?
	 *
	 * @since  3.2.0
	 * @return boolean
	 */
	public static function needs_db_update() {
		$current_db_version = get_option( 'woocommerce_db_version', null );
		$updates            = self::get_db_update_callbacks();
		$update_versions    = array_keys( $updates );
		usort( $update_versions, 'version_compare' );

		return ! is_null( $current_db_version ) && version_compare( $current_db_version, end( $update_versions ), '<' );
	}

	/**
	 * See if we need to set redirect transients for activation or not.
	 *
	 * @since 4.6.0
	 */
	private static function maybe_set_activation_transients() {
		if ( self::is_new_install() ) {
			set_transient( '_wc_activation_redirect', 1, 30 );
		}
	}

	/**
	 * See if we need to show or run database updates during install.
	 *
	 * @since 3.2.0
	 */
	private static function maybe_update_db_version() {
		if ( self::needs_db_update() ) {
			if ( apply_filters( 'woocommerce_enable_auto_update_db', false ) ) {
				self::update();
			} else {
				WC_Admin_Notices::add_notice( 'update', true );
			}
		} else {
			self::update_db_version();
		}
	}

	/**
	 * Update WC version to current.
	 */
	private static function update_wc_version() {
		update_option( 'woocommerce_version', WC()->version );
	}

	/**
	 * Get list of DB update callbacks.
	 *
	 * @since  3.0.0
	 * @return array
	 */
	public static function get_db_update_callbacks() {
		return self::$db_updates;
	}

	/**
	 * Push all needed DB updates to the queue for processing.
	 */
	private static function update() {
		$current_db_version = get_option( 'woocommerce_db_version' );
		$loop               = 0;

		foreach ( self::get_db_update_callbacks() as $version => $update_callbacks ) {
			if ( version_compare( $current_db_version, $version, '<' ) ) {
				foreach ( $update_callbacks as $update_callback ) {
					WC()->queue()->schedule_single(
						time() + $loop,
						'woocommerce_run_update_callback',
						array(
							'update_callback' => $update_callback,
						),
						'woocommerce-db-updates'
					);
					$loop++;
				}
			}
		}
	}

	/**
	 * Update DB version to current.
	 *
	 * @param string|null $version New WooCommerce DB version or null.
	 */
	public static function update_db_version( $version = null ) {
		update_option( 'woocommerce_db_version', is_null( $version ) ? WC()->version : $version );
	}

	/**
	 * Add more cron schedules.
	 *
	 * @param array $schedules List of WP scheduled cron jobs.
	 *
	 * @return array
	 */
	public static function cron_schedules( $schedules ) {
		$schedules['monthly']     = array(
			'interval' => 2635200,
			'display'  => __( 'Monthly', 'woocommerce' ),
		);
		$schedules['fifteendays'] = array(
			'interval' => 1296000,
			'display'  => __( 'Every 15 Days', 'woocommerce' ),
		);
		return $schedules;
	}

	/**
	 * Create cron jobs (clear them first).
	 */
	private static function create_cron_jobs() {
		wp_clear_scheduled_hook( 'woocommerce_scheduled_sales' );
		wp_clear_scheduled_hook( 'woocommerce_cancel_unpaid_orders' );
		wp_clear_scheduled_hook( 'woocommerce_cleanup_sessions' );
		wp_clear_scheduled_hook( 'woocommerce_cleanup_personal_data' );
		wp_clear_scheduled_hook( 'woocommerce_cleanup_logs' );
		wp_clear_scheduled_hook( 'woocommerce_geoip_updater' );
		wp_clear_scheduled_hook( 'woocommerce_tracker_send_event' );

		$ve = get_option( 'gmt_offset' ) > 0 ? '-' : '+';

		wp_schedule_event( strtotime( '00:00 tomorrow ' . $ve . absint( get_option( 'gmt_offset' ) ) . ' HOURS' ), 'daily', 'woocommerce_scheduled_sales' );

		$held_duration = get_option( 'woocommerce_hold_stock_minutes', '60' );

		if ( '' !== $held_duration ) {
			$cancel_unpaid_interval = apply_filters( 'woocommerce_cancel_unpaid_orders_interval_minutes', absint( $held_duration ) );
			wp_schedule_single_event( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders' );
		}

		// Delay the first run of `woocommerce_cleanup_personal_data` by 10 seconds
		// so it doesn't occur in the same request. WooCommerce Admin also schedules
		// a daily cron that gets lost due to a race condition. WC_Privacy's background
		// processing instance updates the cron schedule from within a cron job.
		wp_schedule_event( time() + 10, 'daily', 'woocommerce_cleanup_personal_data' );
		wp_schedule_event( time() + ( 3 * HOUR_IN_SECONDS ), 'daily', 'woocommerce_cleanup_logs' );
		wp_schedule_event( time() + ( 6 * HOUR_IN_SECONDS ), 'twicedaily', 'woocommerce_cleanup_sessions' );
		wp_schedule_event( time() + MINUTE_IN_SECONDS, 'fifteendays', 'woocommerce_geoip_updater' );
		wp_schedule_event( time() + 10, apply_filters( 'woocommerce_tracker_event_recurrence', 'daily' ), 'woocommerce_tracker_send_event' );
	}

	/**
	 * Create pages on installation.
	 */
	public static function maybe_create_pages() {
		if ( empty( get_option( 'woocommerce_db_version' ) ) ) {
			self::create_pages();
		}
	}

	/**
	 * Create pages that the plugin relies on, storing page IDs in variables.
	 */
	public static function create_pages() {
		include_once dirname( __FILE__ ) . '/admin/wc-admin-functions.php';

		$pages = apply_filters(
			'woocommerce_create_pages',
			array(
				'shop'          => array(
					'name'    => _x( 'shop', 'Page slug', 'woocommerce' ),
					'title'   => _x( 'Shop', 'Page title', 'woocommerce' ),
					'content' => '',
				),
				'cart'          => array(
					'name'    => _x( 'cart', 'Page slug', 'woocommerce' ),
					'title'   => _x( 'Cart', 'Page title', 'woocommerce' ),
					'content' => '<!-- wp:shortcode -->[' . apply_filters( 'woocommerce_cart_shortcode_tag', 'woocommerce_cart' ) . ']<!-- /wp:shortcode -->',
				),
				'checkout'      => array(
					'name'    => _x( 'checkout', 'Page slug', 'woocommerce' ),
					'title'   => _x( 'Checkout', 'Page title', 'woocommerce' ),
					'content' => '<!-- wp:shortcode -->[' . apply_filters( 'woocommerce_checkout_shortcode_tag', 'woocommerce_checkout' ) . ']<!-- /wp:shortcode -->',
				),
				'myaccount'     => array(
					'name'    => _x( 'my-account', 'Page slug', 'woocommerce' ),
					'title'   => _x( 'My account', 'Page title', 'woocommerce' ),
					'content' => '<!-- wp:shortcode -->[' . apply_filters( 'woocommerce_my_account_shortcode_tag', 'woocommerce_my_account' ) . ']<!-- /wp:shortcode -->',
				),
				'refund_returns' => array(
					'name'        => _x( 'refund_returns', 'Page slug', 'woocommerce' ),
					'title'       => _x( 'Refund and Returns Policy', 'Page title', 'woocommerce' ),
					'content'     => self::get_refunds_return_policy_page_content(),
					'post_status' => 'draft',
				),
			)
		);

		foreach ( $pages as $key => $page ) {
			wc_create_page(
				esc_sql( $page['name'] ),
				'woocommerce_' . $key . '_page_id',
				$page['title'],
				$page['content'],
				! empty( $page['parent'] ) ? wc_get_page_id( $page['parent'] ) : '',
				! empty( $page['post_status'] ) ? $page['post_status'] : 'publish'
			);
		}
	}

	/**
	 * Default options.
	 *
	 * Sets up the default options used on the settings page.
	 */
	private static function create_options() {
		// Include settings so that we can run through defaults.
		include_once dirname( __FILE__ ) . '/admin/class-wc-admin-settings.php';

		$settings = WC_Admin_Settings::get_settings_pages();

		foreach ( $settings as $section ) {
			if ( ! method_exists( $section, 'get_settings' ) ) {
				continue;
			}
			$subsections = array_unique( array_merge( array( '' ), array_keys( $section->get_sections() ) ) );

			/**
			 * We are using 'WC_Settings_Page::get_settings' on purpose even thought it's deprecated.
			 * See the method documentation for an explanation.
			 */

			foreach ( $subsections as $subsection ) {
				foreach ( $section->get_settings( $subsection ) as $value ) {
					if ( isset( $value['default'] ) && isset( $value['id'] ) ) {
						$autoload = isset( $value['autoload'] ) ? (bool) $value['autoload'] : true;
						add_option( $value['id'], $value['default'], '', ( $autoload ? 'yes' : 'no' ) );
					}
				}
			}
		}

		// Define other defaults if not in setting screens.
		add_option( 'woocommerce_single_image_width', '600', '', 'yes' );
		add_option( 'woocommerce_thumbnail_image_width', '300', '', 'yes' );
		add_option( 'woocommerce_checkout_highlight_required_fields', 'yes', '', 'yes' );
		add_option( 'woocommerce_demo_store', 'no', '', 'no' );

		if ( self::is_new_install() ) {
			// Define initial tax classes.
			WC_Tax::create_tax_class( __( 'Reduced rate', 'woocommerce' ) );
			WC_Tax::create_tax_class( __( 'Zero rate', 'woocommerce' ) );
		}
	}

	/**
	 * Add the default terms for WC taxonomies - product types and order statuses. Modify this at your own risk.
	 */
	public static function create_terms() {
		$taxonomies = array(
			'product_type'       => array(
				'simple',
				'grouped',
				'variable',
				'external',
			),
			'product_visibility' => array(
				'exclude-from-search',
				'exclude-from-catalog',
				'featured',
				'outofstock',
				'rated-1',
				'rated-2',
				'rated-3',
				'rated-4',
				'rated-5',
			),
		);

		foreach ( $taxonomies as $taxonomy => $terms ) {
			foreach ( $terms as $term ) {
				if ( ! get_term_by( 'name', $term, $taxonomy ) ) { // @codingStandardsIgnoreLine.
					wp_insert_term( $term, $taxonomy );
				}
			}
		}

		$woocommerce_default_category = (int) get_option( 'default_product_cat', 0 );

		if ( ! $woocommerce_default_category || ! term_exists( $woocommerce_default_category, 'product_cat' ) ) {
			$default_product_cat_id   = 0;
			$default_product_cat_slug = sanitize_title( _x( 'Uncategorized', 'Default category slug', 'woocommerce' ) );
			$default_product_cat      = get_term_by( 'slug', $default_product_cat_slug, 'product_cat' ); // @codingStandardsIgnoreLine.

			if ( $default_product_cat ) {
				$default_product_cat_id = absint( $default_product_cat->term_taxonomy_id );
			} else {
				$result = wp_insert_term( _x( 'Uncategorized', 'Default category slug', 'woocommerce' ), 'product_cat', array( 'slug' => $default_product_cat_slug ) );

				if ( ! is_wp_error( $result ) && ! empty( $result['term_taxonomy_id'] ) ) {
					$default_product_cat_id = absint( $result['term_taxonomy_id'] );
				}
			}

			if ( $default_product_cat_id ) {
				update_option( 'default_product_cat', $default_product_cat_id );
			}
		}
	}

	/**
	 * Set up the database tables which the plugin needs to function.
	 * WARNING: If you are modifying this method, make sure that its safe to call regardless of the state of database.
	 *
	 * This is called from `install` method and is executed in-sync when WC is installed or updated. This can also be called optionally from `verify_base_tables`.
	 *
	 * TODO: Add all crucial tables that we have created from workers in the past.
	 *
	 * Tables:
	 *      woocommerce_attribute_taxonomies - Table for storing attribute taxonomies - these are user defined
	 *      woocommerce_downloadable_product_permissions - Table for storing user and guest download permissions.
	 *          KEY(order_id, product_id, download_id) used for organizing downloads on the My Account page
	 *      woocommerce_order_items - Order line items are stored in a table to make them easily queryable for reports
	 *      woocommerce_order_itemmeta - Order line item meta is stored in a table for storing extra data.
	 *      woocommerce_tax_rates - Tax Rates are stored inside 2 tables making tax queries simple and efficient.
	 *      woocommerce_tax_rate_locations - Each rate can be applied to more than one postcode/city hence the second table.
	 */
	private static function create_tables() {
		global $wpdb;

		$wpdb->hide_errors();

		require_once ABSPATH . 'wp-admin/includes/upgrade.php';

		/**
		 * Before updating with DBDELTA, remove any primary keys which could be
		 * modified due to schema updates.
		 */
		if ( $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->prefix}woocommerce_downloadable_product_permissions';" ) ) {
			if ( ! $wpdb->get_var( "SHOW COLUMNS FROM `{$wpdb->prefix}woocommerce_downloadable_product_permissions` LIKE 'permission_id';" ) ) {
				$wpdb->query( "ALTER TABLE {$wpdb->prefix}woocommerce_downloadable_product_permissions DROP PRIMARY KEY, ADD `permission_id` BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT;" );
			}
		}

		/**
		 * Change wp_woocommerce_sessions schema to use a bigint auto increment field instead of char(32) field as
		 * the primary key as it is not a good practice to use a char(32) field as the primary key of a table and as
		 * there were reports of issues with this table (see https://github.com/woocommerce/woocommerce/issues/20912).
		 *
		 * This query needs to run before dbDelta() as this WP function is not able to handle primary key changes
		 * (see https://github.com/woocommerce/woocommerce/issues/21534 and https://core.trac.wordpress.org/ticket/40357).
		 */
		if ( $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->prefix}woocommerce_sessions'" ) ) {
			if ( ! $wpdb->get_var( "SHOW KEYS FROM {$wpdb->prefix}woocommerce_sessions WHERE Key_name = 'PRIMARY' AND Column_name = 'session_id'" ) ) {
				$wpdb->query(
					"ALTER TABLE `{$wpdb->prefix}woocommerce_sessions` DROP PRIMARY KEY, DROP KEY `session_id`, ADD PRIMARY KEY(`session_id`), ADD UNIQUE KEY(`session_key`)"
				);
			}
		}

		dbDelta( self::get_schema() );

		$index_exists = $wpdb->get_row( "SHOW INDEX FROM {$wpdb->comments} WHERE column_name = 'comment_type' and key_name = 'woo_idx_comment_type'" );

		if ( is_null( $index_exists ) ) {
			// Add an index to the field comment_type to improve the response time of the query
			// used by WC_Comments::wp_count_comments() to get the number of comments by type.
			$wpdb->query( "ALTER TABLE {$wpdb->comments} ADD INDEX woo_idx_comment_type (comment_type)" );
		}

		// Get tables data types and check it matches before adding constraint.
		$download_log_columns     = $wpdb->get_results( "SHOW COLUMNS FROM {$wpdb->prefix}wc_download_log WHERE Field = 'permission_id'", ARRAY_A );
		$download_log_column_type = '';
		if ( isset( $download_log_columns[0]['Type'] ) ) {
			$download_log_column_type = $download_log_columns[0]['Type'];
		}

		$download_permissions_columns     = $wpdb->get_results( "SHOW COLUMNS FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE Field = 'permission_id'", ARRAY_A );
		$download_permissions_column_type = '';
		if ( isset( $download_permissions_columns[0]['Type'] ) ) {
			$download_permissions_column_type = $download_permissions_columns[0]['Type'];
		}

		// Add constraint to download logs if the columns matches.
		if ( ! empty( $download_permissions_column_type ) && ! empty( $download_log_column_type ) && $download_permissions_column_type === $download_log_column_type ) {
			$fk_result = $wpdb->get_row( "SHOW CREATE TABLE {$wpdb->prefix}wc_download_log" );
			if ( false === strpos( $fk_result->{'Create Table'}, "fk_{$wpdb->prefix}wc_download_log_permission_id" ) ) {
				$wpdb->query(
					"ALTER TABLE `{$wpdb->prefix}wc_download_log`
					ADD CONSTRAINT `fk_{$wpdb->prefix}wc_download_log_permission_id`
					FOREIGN KEY (`permission_id`)
					REFERENCES `{$wpdb->prefix}woocommerce_downloadable_product_permissions` (`permission_id`) ON DELETE CASCADE;"
				);
			}
		}

		// Clear table caches.
		delete_transient( 'wc_attribute_taxonomies' );
	}

	/**
	 * Get Table schema.
	 *
	 * See https://github.com/woocommerce/woocommerce/wiki/Database-Description/
	 *
	 * A note on indexes; Indexes have a maximum size of 767 bytes. Historically, we haven't need to be concerned about that.
	 * As of WordPress 4.2, however, we moved to utf8mb4, which uses 4 bytes per character. This means that an index which
	 * used to have room for floor(767/3) = 255 characters, now only has room for floor(767/4) = 191 characters.
	 *
	 * Changing indexes may cause duplicate index notices in logs due to https://core.trac.wordpress.org/ticket/34870 but dropping
	 * indexes first causes too much load on some servers/larger DB.
	 *
	 * When adding or removing a table, make sure to update the list of tables in WC_Install::get_tables().
	 *
	 * @return string
	 */
	private static function get_schema() {
		global $wpdb;

		$collate = '';

		if ( $wpdb->has_cap( 'collation' ) ) {
			$collate = $wpdb->get_charset_collate();
		}

		/*
		 * Indexes have a maximum size of 767 bytes. Historically, we haven't need to be concerned about that.
		 * As of WP 4.2, however, they moved to utf8mb4, which uses 4 bytes per character. This means that an index which
		 * used to have room for floor(767/3) = 255 characters, now only has room for floor(767/4) = 191 characters.
		 */
		$max_index_length = 191;

		$tables = "
CREATE TABLE {$wpdb->prefix}woocommerce_sessions (
  session_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  session_key char(32) NOT NULL,
  session_value longtext NOT NULL,
  session_expiry BIGINT UNSIGNED NOT NULL,
  PRIMARY KEY  (session_id),
  UNIQUE KEY session_key (session_key)
) $collate;
CREATE TABLE {$wpdb->prefix}woocommerce_api_keys (
  key_id BIGINT UNSIGNED NOT NULL auto_increment,
  user_id BIGINT UNSIGNED NOT NULL,
  description varchar(200) NULL,
  permissions varchar(10) NOT NULL,
  consumer_key char(64) NOT NULL,
  consumer_secret char(43) NOT NULL,
  nonces longtext NULL,
  truncated_key char(7) NOT NULL,
  last_access datetime NULL default null,
  PRIMARY KEY  (key_id),
  KEY consumer_key (consumer_key),
  KEY consumer_secret (consumer_secret)
) $collate;
CREATE TABLE {$wpdb->prefix}woocommerce_attribute_taxonomies (
  attribute_id BIGINT UNSIGNED NOT NULL auto_increment,
  attribute_name varchar(200) NOT NULL,
  attribute_label varchar(200) NULL,
  attribute_type varchar(20) NOT NULL,
  attribute_orderby varchar(20) NOT NULL,
  attribute_public int(1) NOT NULL DEFAULT 1,
  PRIMARY KEY  (attribute_id),
  KEY attribute_name (attribute_name(20))
) $collate;
CREATE TABLE {$wpdb->prefix}woocommerce_downloadable_product_permissions (
  permission_id BIGINT UNSIGNED NOT NULL auto_increment,
  download_id varchar(36) NOT NULL,
  product_id BIGINT UNSIGNED NOT NULL,
  order_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
  order_key varchar(200) NOT NULL,
  user_email varchar(200) NOT NULL,
  user_id BIGINT UNSIGNED NULL,
  downloads_remaining varchar(9) NULL,
  access_granted datetime NOT NULL default '0000-00-00 00:00:00',
  access_expires datetime NULL default null,
  download_count BIGINT UNSIGNED NOT NULL DEFAULT 0,
  PRIMARY KEY  (permission_id),
  KEY download_order_key_product (product_id,order_id,order_key(16),download_id),
  KEY download_order_product (download_id,order_id,product_id),
  KEY order_id (order_id),
  KEY user_order_remaining_expires (user_id,order_id,downloads_remaining,access_expires)
) $collate;
CREATE TABLE {$wpdb->prefix}woocommerce_order_items (
  order_item_id BIGINT UNSIGNED NOT NULL auto_increment,
  order_item_name TEXT NOT NULL,
  order_item_type varchar(200) NOT NULL DEFAULT '',
  order_id BIGINT UNSIGNED NOT NULL,
  PRIMARY KEY  (order_item_id),
  KEY order_id (order_id)
) $collate;
CREATE TABLE {$wpdb->prefix}woocommerce_order_itemmeta (
  meta_id BIGINT UNSIGNED NOT NULL auto_increment,
  order_item_id BIGINT UNSIGNED NOT NULL,
  meta_key varchar(255) default NULL,
  meta_value longtext NULL,
  PRIMARY KEY  (meta_id),
  KEY order_item_id (order_item_id),
  KEY meta_key (meta_key(32))
) $collate;
CREATE TABLE {$wpdb->prefix}woocommerce_tax_rates (
  tax_rate_id BIGINT UNSIGNED NOT NULL auto_increment,
  tax_rate_country varchar(2) NOT NULL DEFAULT '',
  tax_rate_state varchar(200) NOT NULL DEFAULT '',
  tax_rate varchar(8) NOT NULL DEFAULT '',
  tax_rate_name varchar(200) NOT NULL DEFAULT '',
  tax_rate_priority BIGINT UNSIGNED NOT NULL,
  tax_rate_compound int(1) NOT NULL DEFAULT 0,
  tax_rate_shipping int(1) NOT NULL DEFAULT 1,
  tax_rate_order BIGINT UNSIGNED NOT NULL,
  tax_rate_class varchar(200) NOT NULL DEFAULT '',
  PRIMARY KEY  (tax_rate_id),
  KEY tax_rate_country (tax_rate_country),
  KEY tax_rate_state (tax_rate_state(2)),
  KEY tax_rate_class (tax_rate_class(10)),
  KEY tax_rate_priority (tax_rate_priority)
) $collate;
CREATE TABLE {$wpdb->prefix}woocommerce_tax_rate_locations (
  location_id BIGINT UNSIGNED NOT NULL auto_increment,
  location_code varchar(200) NOT NULL,
  tax_rate_id BIGINT UNSIGNED NOT NULL,
  location_type varchar(40) NOT NULL,
  PRIMARY KEY  (location_id),
  KEY tax_rate_id (tax_rate_id),
  KEY location_type_code (location_type(10),location_code(20))
) $collate;
CREATE TABLE {$wpdb->prefix}woocommerce_shipping_zones (
  zone_id BIGINT UNSIGNED NOT NULL auto_increment,
  zone_name varchar(200) NOT NULL,
  zone_order BIGINT UNSIGNED NOT NULL,
  PRIMARY KEY  (zone_id)
) $collate;
CREATE TABLE {$wpdb->prefix}woocommerce_shipping_zone_locations (
  location_id BIGINT UNSIGNED NOT NULL auto_increment,
  zone_id BIGINT UNSIGNED NOT NULL,
  location_code varchar(200) NOT NULL,
  location_type varchar(40) NOT NULL,
  PRIMARY KEY  (location_id),
  KEY location_id (location_id),
  KEY location_type_code (location_type(10),location_code(20))
) $collate;
CREATE TABLE {$wpdb->prefix}woocommerce_shipping_zone_methods (
  zone_id BIGINT UNSIGNED NOT NULL,
  instance_id BIGINT UNSIGNED NOT NULL auto_increment,
  method_id varchar(200) NOT NULL,
  method_order BIGINT UNSIGNED NOT NULL,
  is_enabled tinyint(1) NOT NULL DEFAULT '1',
  PRIMARY KEY  (instance_id)
) $collate;
CREATE TABLE {$wpdb->prefix}woocommerce_payment_tokens (
  token_id BIGINT UNSIGNED NOT NULL auto_increment,
  gateway_id varchar(200) NOT NULL,
  token text NOT NULL,
  user_id BIGINT UNSIGNED NOT NULL DEFAULT '0',
  type varchar(200) NOT NULL,
  is_default tinyint(1) NOT NULL DEFAULT '0',
  PRIMARY KEY  (token_id),
  KEY user_id (user_id)
) $collate;
CREATE TABLE {$wpdb->prefix}woocommerce_payment_tokenmeta (
  meta_id BIGINT UNSIGNED NOT NULL auto_increment,
  payment_token_id BIGINT UNSIGNED NOT NULL,
  meta_key varchar(255) NULL,
  meta_value longtext NULL,
  PRIMARY KEY  (meta_id),
  KEY payment_token_id (payment_token_id),
  KEY meta_key (meta_key(32))
) $collate;
CREATE TABLE {$wpdb->prefix}woocommerce_log (
  log_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  timestamp datetime NOT NULL,
  level smallint(4) NOT NULL,
  source varchar(200) NOT NULL,
  message longtext NOT NULL,
  context longtext NULL,
  PRIMARY KEY (log_id),
  KEY level (level)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_webhooks (
  webhook_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  status varchar(200) NOT NULL,
  name text NOT NULL,
  user_id BIGINT UNSIGNED NOT NULL,
  delivery_url text NOT NULL,
  secret text NOT NULL,
  topic varchar(200) NOT NULL,
  date_created datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  date_created_gmt datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  date_modified datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  date_modified_gmt datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  api_version smallint(4) NOT NULL,
  failure_count smallint(10) NOT NULL DEFAULT '0',
  pending_delivery tinyint(1) NOT NULL DEFAULT '0',
  PRIMARY KEY  (webhook_id),
  KEY user_id (user_id)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_download_log (
  download_log_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  timestamp datetime NOT NULL,
  permission_id BIGINT UNSIGNED NOT NULL,
  user_id BIGINT UNSIGNED NULL,
  user_ip_address VARCHAR(100) NULL DEFAULT '',
  PRIMARY KEY  (download_log_id),
  KEY permission_id (permission_id),
  KEY timestamp (timestamp)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_product_meta_lookup (
  `product_id` bigint(20) NOT NULL,
  `sku` varchar(100) NULL default '',
  `virtual` tinyint(1) NULL default 0,
  `downloadable` tinyint(1) NULL default 0,
  `min_price` decimal(19,4) NULL default NULL,
  `max_price` decimal(19,4) NULL default NULL,
  `onsale` tinyint(1) NULL default 0,
  `stock_quantity` double NULL default NULL,
  `stock_status` varchar(100) NULL default 'instock',
  `rating_count` bigint(20) NULL default 0,
  `average_rating` decimal(3,2) NULL default 0.00,
  `total_sales` bigint(20) NULL default 0,
  `tax_status` varchar(100) NULL default 'taxable',
  `tax_class` varchar(100) NULL default '',
  PRIMARY KEY  (`product_id`),
  KEY `virtual` (`virtual`),
  KEY `downloadable` (`downloadable`),
  KEY `stock_status` (`stock_status`),
  KEY `stock_quantity` (`stock_quantity`),
  KEY `onsale` (`onsale`),
  KEY min_max_price (`min_price`, `max_price`)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_tax_rate_classes (
  tax_rate_class_id BIGINT UNSIGNED NOT NULL auto_increment,
  name varchar(200) NOT NULL DEFAULT '',
  slug varchar(200) NOT NULL DEFAULT '',
  PRIMARY KEY  (tax_rate_class_id),
  UNIQUE KEY slug (slug($max_index_length))
) $collate;
CREATE TABLE {$wpdb->prefix}wc_reserved_stock (
	`order_id` bigint(20) NOT NULL,
	`product_id` bigint(20) NOT NULL,
	`stock_quantity` double NOT NULL DEFAULT 0,
	`timestamp` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
	`expires` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
	PRIMARY KEY  (`order_id`, `product_id`)
) $collate;
		";

		return $tables;
	}

	/**
	 * Return a list of WooCommerce tables. Used to make sure all WC tables are dropped when uninstalling the plugin
	 * in a single site or multi site environment.
	 *
	 * @return array WC tables.
	 */
	public static function get_tables() {
		global $wpdb;

		$tables = array(
			"{$wpdb->prefix}wc_download_log",
			"{$wpdb->prefix}wc_product_meta_lookup",
			"{$wpdb->prefix}wc_tax_rate_classes",
			"{$wpdb->prefix}wc_webhooks",
			"{$wpdb->prefix}woocommerce_api_keys",
			"{$wpdb->prefix}woocommerce_attribute_taxonomies",
			"{$wpdb->prefix}woocommerce_downloadable_product_permissions",
			"{$wpdb->prefix}woocommerce_log",
			"{$wpdb->prefix}woocommerce_order_itemmeta",
			"{$wpdb->prefix}woocommerce_order_items",
			"{$wpdb->prefix}woocommerce_payment_tokenmeta",
			"{$wpdb->prefix}woocommerce_payment_tokens",
			"{$wpdb->prefix}woocommerce_sessions",
			"{$wpdb->prefix}woocommerce_shipping_zone_locations",
			"{$wpdb->prefix}woocommerce_shipping_zone_methods",
			"{$wpdb->prefix}woocommerce_shipping_zones",
			"{$wpdb->prefix}woocommerce_tax_rate_locations",
			"{$wpdb->prefix}woocommerce_tax_rates",
			"{$wpdb->prefix}wc_reserved_stock",
		);

		/**
		 * Filter the list of known WooCommerce tables.
		 *
		 * If WooCommerce plugins need to add new tables, they can inject them here.
		 *
		 * @param array $tables An array of WooCommerce-specific database table names.
		 */
		$tables = apply_filters( 'woocommerce_install_get_tables', $tables );

		return $tables;
	}

	/**
	 * Drop WooCommerce tables.
	 *
	 * @return void
	 */
	public static function drop_tables() {
		global $wpdb;

		$tables = self::get_tables();

		foreach ( $tables as $table ) {
			$wpdb->query( "DROP TABLE IF EXISTS {$table}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		}
	}

	/**
	 * Uninstall tables when MU blog is deleted.
	 *
	 * @param array $tables List of tables that will be deleted by WP.
	 *
	 * @return string[]
	 */
	public static function wpmu_drop_tables( $tables ) {
		return array_merge( $tables, self::get_tables() );
	}

	/**
	 * Create roles and capabilities.
	 */
	public static function create_roles() {
		global $wp_roles;

		if ( ! class_exists( 'WP_Roles' ) ) {
			return;
		}

		if ( ! isset( $wp_roles ) ) {
			$wp_roles = new WP_Roles(); // @codingStandardsIgnoreLine
		}

		// Dummy gettext calls to get strings in the catalog.
		/* translators: user role */
		_x( 'Customer', 'User role', 'woocommerce' );
		/* translators: user role */
		_x( 'Shop manager', 'User role', 'woocommerce' );

		// Customer role.
		add_role(
			'customer',
			'Customer',
			array(
				'read' => true,
			)
		);

		// Shop manager role.
		add_role(
			'shop_manager',
			'Shop manager',
			array(
				'level_9'                => true,
				'level_8'                => true,
				'level_7'                => true,
				'level_6'                => true,
				'level_5'                => true,
				'level_4'                => true,
				'level_3'                => true,
				'level_2'                => true,
				'level_1'                => true,
				'level_0'                => true,
				'read'                   => true,
				'read_private_pages'     => true,
				'read_private_posts'     => true,
				'edit_posts'             => true,
				'edit_pages'             => true,
				'edit_published_posts'   => true,
				'edit_published_pages'   => true,
				'edit_private_pages'     => true,
				'edit_private_posts'     => true,
				'edit_others_posts'      => true,
				'edit_others_pages'      => true,
				'publish_posts'          => true,
				'publish_pages'          => true,
				'delete_posts'           => true,
				'delete_pages'           => true,
				'delete_private_pages'   => true,
				'delete_private_posts'   => true,
				'delete_published_pages' => true,
				'delete_published_posts' => true,
				'delete_others_posts'    => true,
				'delete_others_pages'    => true,
				'manage_categories'      => true,
				'manage_links'           => true,
				'moderate_comments'      => true,
				'upload_files'           => true,
				'export'                 => true,
				'import'                 => true,
				'list_users'             => true,
				'edit_theme_options'     => true,
			)
		);

		$capabilities = self::get_core_capabilities();

		foreach ( $capabilities as $cap_group ) {
			foreach ( $cap_group as $cap ) {
				$wp_roles->add_cap( 'shop_manager', $cap );
				$wp_roles->add_cap( 'administrator', $cap );
			}
		}
	}

	/**
	 * Get capabilities for WooCommerce - these are assigned to admin/shop manager during installation or reset.
	 *
	 * @return array
	 */
	public static function get_core_capabilities() {
		$capabilities = array();

		$capabilities['core'] = array(
			'manage_woocommerce',
			'view_woocommerce_reports',
		);

		$capability_types = array( 'product', 'shop_order', 'shop_coupon' );

		foreach ( $capability_types as $capability_type ) {

			$capabilities[ $capability_type ] = array(
				// Post type.
				"edit_{$capability_type}",
				"read_{$capability_type}",
				"delete_{$capability_type}",
				"edit_{$capability_type}s",
				"edit_others_{$capability_type}s",
				"publish_{$capability_type}s",
				"read_private_{$capability_type}s",
				"delete_{$capability_type}s",
				"delete_private_{$capability_type}s",
				"delete_published_{$capability_type}s",
				"delete_others_{$capability_type}s",
				"edit_private_{$capability_type}s",
				"edit_published_{$capability_type}s",

				// Terms.
				"manage_{$capability_type}_terms",
				"edit_{$capability_type}_terms",
				"delete_{$capability_type}_terms",
				"assign_{$capability_type}_terms",
			);
		}

		return $capabilities;
	}

	/**
	 * Remove WooCommerce roles.
	 */
	public static function remove_roles() {
		global $wp_roles;

		if ( ! class_exists( 'WP_Roles' ) ) {
			return;
		}

		if ( ! isset( $wp_roles ) ) {
			$wp_roles = new WP_Roles(); // @codingStandardsIgnoreLine
		}

		$capabilities = self::get_core_capabilities();

		foreach ( $capabilities as $cap_group ) {
			foreach ( $cap_group as $cap ) {
				$wp_roles->remove_cap( 'shop_manager', $cap );
				$wp_roles->remove_cap( 'administrator', $cap );
			}
		}

		remove_role( 'customer' );
		remove_role( 'shop_manager' );
	}

	/**
	 * Create files/directories.
	 */
	private static function create_files() {
		// Bypass if filesystem is read-only and/or non-standard upload system is used.
		if ( apply_filters( 'woocommerce_install_skip_create_files', false ) ) {
			return;
		}

		// Install files and folders for uploading files and prevent hotlinking.
		$upload_dir      = wp_get_upload_dir();
		$download_method = get_option( 'woocommerce_file_download_method', 'force' );

		$files = array(
			array(
				'base'    => $upload_dir['basedir'] . '/woocommerce_uploads',
				'file'    => 'index.html',
				'content' => '',
			),
			array(
				'base'    => WC_LOG_DIR,
				'file'    => '.htaccess',
				'content' => 'deny from all',
			),
			array(
				'base'    => WC_LOG_DIR,
				'file'    => 'index.html',
				'content' => '',
			),
			array(
				'base'    => $upload_dir['basedir'] . '/woocommerce_uploads',
				'file'    => '.htaccess',
				'content' => 'redirect' === $download_method ? 'Options -Indexes' : 'deny from all',
			),
		);

		foreach ( $files as $file ) {
			if ( wp_mkdir_p( $file['base'] ) && ! file_exists( trailingslashit( $file['base'] ) . $file['file'] ) ) {
				$file_handle = @fopen( trailingslashit( $file['base'] ) . $file['file'], 'wb' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_fopen
				if ( $file_handle ) {
					fwrite( $file_handle, $file['content'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite
					fclose( $file_handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose
				}
			}
		}

		// Create attachment for placeholders.
		self::create_placeholder_image();
	}

	/**
	 * Create a placeholder image in the media library.
	 *
	 * @since 3.5.0
	 */
	private static function create_placeholder_image() {
		$placeholder_image = get_option( 'woocommerce_placeholder_image', 0 );

		// Validate current setting if set. If set, return.
		if ( ! empty( $placeholder_image ) ) {
			if ( ! is_numeric( $placeholder_image ) ) {
				return;
			} elseif ( $placeholder_image && wp_attachment_is_image( $placeholder_image ) ) {
				return;
			}
		}

		$upload_dir = wp_upload_dir();
		$source     = WC()->plugin_path() . '/assets/images/placeholder-attachment.png';
		$filename   = $upload_dir['basedir'] . '/woocommerce-placeholder.png';

		if ( ! file_exists( $filename ) ) {
			copy( $source, $filename ); // @codingStandardsIgnoreLine.
		}

		if ( ! file_exists( $filename ) ) {
			update_option( 'woocommerce_placeholder_image', 0 );
			return;
		}

		$filetype   = wp_check_filetype( basename( $filename ), null );
		$attachment = array(
			'guid'           => $upload_dir['url'] . '/' . basename( $filename ),
			'post_mime_type' => $filetype['type'],
			'post_title'     => preg_replace( '/\.[^.]+$/', '', basename( $filename ) ),
			'post_content'   => '',
			'post_status'    => 'inherit',
		);

		$attach_id = wp_insert_attachment( $attachment, $filename );
		if ( is_wp_error( $attach_id ) ) {
			update_option( 'woocommerce_placeholder_image', 0 );
			return;
		}

		update_option( 'woocommerce_placeholder_image', $attach_id );

		// Make sure that this file is included, as wp_generate_attachment_metadata() depends on it.
		require_once ABSPATH . 'wp-admin/includes/image.php';

		// Generate the metadata for the attachment, and update the database record.
		$attach_data = wp_generate_attachment_metadata( $attach_id, $filename );
		wp_update_attachment_metadata( $attach_id, $attach_data );
	}

	/**
	 * Show action links on the plugin screen.
	 *
	 * @param mixed $links Plugin Action links.
	 *
	 * @return array
	 */
	public static function plugin_action_links( $links ) {
		$action_links = array(
			'settings' => '<a href="' . admin_url( 'admin.php?page=wc-settings' ) . '" aria-label="' . esc_attr__( 'View WooCommerce settings', 'woocommerce' ) . '">' . esc_html__( 'Settings', 'woocommerce' ) . '</a>',
		);

		return array_merge( $action_links, $links );
	}

	/**
	 * Show row meta on the plugin screen.
	 *
	 * @param mixed $links Plugin Row Meta.
	 * @param mixed $file  Plugin Base file.
	 *
	 * @return array
	 */
	public static function plugin_row_meta( $links, $file ) {
		if ( WC_PLUGIN_BASENAME !== $file ) {
			return $links;
		}

		$row_meta = array(
			'docs'    => '<a href="' . esc_url( apply_filters( 'woocommerce_docs_url', 'https://docs.woocommerce.com/documentation/plugins/woocommerce/' ) ) . '" aria-label="' . esc_attr__( 'View WooCommerce documentation', 'woocommerce' ) . '">' . esc_html__( 'Docs', 'woocommerce' ) . '</a>',
			'apidocs' => '<a href="' . esc_url( apply_filters( 'woocommerce_apidocs_url', 'https://docs.woocommerce.com/wc-apidocs/' ) ) . '" aria-label="' . esc_attr__( 'View WooCommerce API docs', 'woocommerce' ) . '">' . esc_html__( 'API docs', 'woocommerce' ) . '</a>',
			'support' => '<a href="' . esc_url( apply_filters( 'woocommerce_community_support_url', 'https://wordpress.org/support/plugin/woocommerce/' ) ) . '" aria-label="' . esc_attr__( 'Visit community forums', 'woocommerce' ) . '">' . esc_html__( 'Community support', 'woocommerce' ) . '</a>',
		);

		if ( WCConnectionHelper::is_connected() ) {
			$row_meta['premium_support'] = '<a href="' . esc_url( apply_filters( 'woocommerce_support_url', 'https://woocommerce.com/my-account/create-a-ticket/' ) ) . '" aria-label="' . esc_attr__( 'Visit premium customer support', 'woocommerce' ) . '">' . esc_html__( 'Premium support', 'woocommerce' ) . '</a>';
		}

		return array_merge( $links, $row_meta );
	}

	/**
	 * Get slug from path and associate it with the path.
	 *
	 * @param array  $plugins Associative array of plugin files to paths.
	 * @param string $key Plugin relative path. Example: woocommerce/woocommerce.php.
	 */
	private static function associate_plugin_file( $plugins, $key ) {
		$path                 = explode( '/', $key );
		$filename             = end( $path );
		$plugins[ $filename ] = $key;
		return $plugins;
	}

	/**
	 * Install a plugin from .org in the background via a cron job (used by
	 * installer - opt in).
	 *
	 * @param string $plugin_to_install_id Plugin ID.
	 * @param array  $plugin_to_install Plugin information.
	 *
	 * @throws Exception If unable to proceed with plugin installation.
	 * @since  2.6.0
	 */
	public static function background_installer( $plugin_to_install_id, $plugin_to_install ) {
		// Explicitly clear the event.
		$args = func_get_args();

		if ( ! empty( $plugin_to_install['repo-slug'] ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
			require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
			require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
			require_once ABSPATH . 'wp-admin/includes/plugin.php';

			WP_Filesystem();

			$skin              = new Automatic_Upgrader_Skin();
			$upgrader          = new WP_Upgrader( $skin );
			$installed_plugins = array_reduce( array_keys( get_plugins() ), array( __CLASS__, 'associate_plugin_file' ) );
			if ( empty( $installed_plugins ) ) {
				$installed_plugins = array();
			}
			$plugin_slug = $plugin_to_install['repo-slug'];
			$plugin_file = isset( $plugin_to_install['file'] ) ? $plugin_to_install['file'] : $plugin_slug . '.php';
			$installed   = false;
			$activate    = false;

			// See if the plugin is installed already.
			if ( isset( $installed_plugins[ $plugin_file ] ) ) {
				$installed = true;
				$activate  = ! is_plugin_active( $installed_plugins[ $plugin_file ] );
			}

			// Install this thing!
			if ( ! $installed ) {
				// Suppress feedback.
				ob_start();

				try {
					$plugin_information = plugins_api(
						'plugin_information',
						array(
							'slug'   => $plugin_slug,
							'fields' => array(
								'short_description' => false,
								'sections'          => false,
								'requires'          => false,
								'rating'            => false,
								'ratings'           => false,
								'downloaded'        => false,
								'last_updated'      => false,
								'added'             => false,
								'tags'              => false,
								'homepage'          => false,
								'donate_link'       => false,
								'author_profile'    => false,
								'author'            => false,
							),
						)
					);

					if ( is_wp_error( $plugin_information ) ) {
						throw new Exception( $plugin_information->get_error_message() );
					}

					$package  = $plugin_information->download_link;
					$download = $upgrader->download_package( $package );

					if ( is_wp_error( $download ) ) {
						throw new Exception( $download->get_error_message() );
					}

					$working_dir = $upgrader->unpack_package( $download, true );

					if ( is_wp_error( $working_dir ) ) {
						throw new Exception( $working_dir->get_error_message() );
					}

					$result = $upgrader->install_package(
						array(
							'source'                      => $working_dir,
							'destination'                 => WP_PLUGIN_DIR,
							'clear_destination'           => false,
							'abort_if_destination_exists' => false,
							'clear_working'               => true,
							'hook_extra'                  => array(
								'type'   => 'plugin',
								'action' => 'install',
							),
						)
					);

					if ( is_wp_error( $result ) ) {
						throw new Exception( $result->get_error_message() );
					}

					$activate = true;

				} catch ( Exception $e ) {
					WC_Admin_Notices::add_custom_notice(
						$plugin_to_install_id . '_install_error',
						sprintf(
							// translators: 1: plugin name, 2: error message, 3: URL to install plugin manually.
							__( '%1$s could not be installed (%2$s). <a href="%3$s">Please install it manually by clicking here.</a>', 'woocommerce' ),
							$plugin_to_install['name'],
							$e->getMessage(),
							esc_url( admin_url( 'index.php?wc-install-plugin-redirect=' . $plugin_slug ) )
						)
					);
				}

				// Discard feedback.
				ob_end_clean();
			}

			wp_clean_plugins_cache();

			// Activate this thing.
			if ( $activate ) {
				try {
					add_action( 'add_option_mailchimp_woocommerce_plugin_do_activation_redirect', array( __CLASS__, 'remove_mailchimps_redirect' ), 10, 2 );
					$result = activate_plugin( $installed ? $installed_plugins[ $plugin_file ] : $plugin_slug . '/' . $plugin_file );

					if ( is_wp_error( $result ) ) {
						throw new Exception( $result->get_error_message() );
					}
				} catch ( Exception $e ) {
					WC_Admin_Notices::add_custom_notice(
						$plugin_to_install_id . '_install_error',
						sprintf(
							// translators: 1: plugin name, 2: URL to WP plugin page.
							__( '%1$s was installed but could not be activated. <a href="%2$s">Please activate it manually by clicking here.</a>', 'woocommerce' ),
							$plugin_to_install['name'],
							admin_url( 'plugins.php' )
						)
					);
				}
			}
		}
	}

	/**
	 * Removes redirect added during MailChimp plugin's activation.
	 *
	 * @param string $option Option name.
	 * @param string $value  Option value.
	 */
	public static function remove_mailchimps_redirect( $option, $value ) {
		// Remove this action to prevent infinite looping.
		remove_action( 'add_option_mailchimp_woocommerce_plugin_do_activation_redirect', array( __CLASS__, 'remove_mailchimps_redirect' ) );

		// Update redirect back to false.
		update_option( 'mailchimp_woocommerce_plugin_do_activation_redirect', false );
	}

	/**
	 * Install a theme from .org in the background via a cron job (used by installer - opt in).
	 *
	 * @param string $theme_slug Theme slug.
	 *
	 * @throws Exception If unable to proceed with theme installation.
	 * @since  3.1.0
	 */
	public static function theme_background_installer( $theme_slug ) {
		// Explicitly clear the event.
		$args = func_get_args();

		if ( ! empty( $theme_slug ) ) {
			// Suppress feedback.
			ob_start();

			try {
				$theme = wp_get_theme( $theme_slug );

				if ( ! $theme->exists() ) {
					require_once ABSPATH . 'wp-admin/includes/file.php';
					include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
					include_once ABSPATH . 'wp-admin/includes/theme.php';

					WP_Filesystem();

					$skin     = new Automatic_Upgrader_Skin();
					$upgrader = new Theme_Upgrader( $skin );
					$api      = themes_api(
						'theme_information',
						array(
							'slug'   => $theme_slug,
							'fields' => array( 'sections' => false ),
						)
					);
					$result   = $upgrader->install( $api->download_link );

					if ( is_wp_error( $result ) ) {
						throw new Exception( $result->get_error_message() );
					} elseif ( is_wp_error( $skin->result ) ) {
						throw new Exception( $skin->result->get_error_message() );
					} elseif ( is_null( $result ) ) {
						throw new Exception( 'Unable to connect to the filesystem. Please confirm your credentials.' );
					}
				}

				switch_theme( $theme_slug );
			} catch ( Exception $e ) {
				WC_Admin_Notices::add_custom_notice(
					$theme_slug . '_install_error',
					sprintf(
						// translators: 1: theme slug, 2: error message, 3: URL to install theme manually.
						__( '%1$s could not be installed (%2$s). <a href="%3$s">Please install it manually by clicking here.</a>', 'woocommerce' ),
						$theme_slug,
						$e->getMessage(),
						esc_url( admin_url( 'update.php?action=install-theme&theme=' . $theme_slug . '&_wpnonce=' . wp_create_nonce( 'install-theme_' . $theme_slug ) ) )
					)
				);
			}

			// Discard feedback.
			ob_end_clean();
		}
	}

	/**
	 * Sets whether PayPal Standard will be loaded on install.
	 *
	 * @since 5.5.0
	 */
	private static function set_paypal_standard_load_eligibility() {
		// Initiating the payment gateways sets the flag.
		if ( class_exists( 'WC_Gateway_Paypal' ) ) {
			( new WC_Gateway_Paypal() )->should_load();
		}
	}

	/**
	 * Gets the content of the sample refunds and return policy page.
	 *
	 * @since 5.6.0
	 * @return HTML The content for the page
	 */
	private static function get_refunds_return_policy_page_content() {
		return <<<EOT
<!-- wp:paragraph -->
<p><b>This is a sample page.</b></p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<h3>Overview</h3>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Our refund and returns policy lasts 30 days. If 30 days have passed since your purchase, we can’t offer you a full refund or exchange.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>To be eligible for a return, your item must be unused and in the same condition that you received it. It must also be in the original packaging.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Several types of goods are exempt from being returned. Perishable goods such as food, flowers, newspapers or magazines cannot be returned. We also do not accept products that are intimate or sanitary goods, hazardous materials, or flammable liquids or gases.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Additional non-returnable items:</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul>
<li>Gift cards</li>
<li>Downloadable software products</li>
<li>Some health and personal care items</li>
</ul>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>To complete your return, we require a receipt or proof of purchase.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Please do not send your purchase back to the manufacturer.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>There are certain situations where only partial refunds are granted:</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul>
<li>Book with obvious signs of use</li>
<li>CD, DVD, VHS tape, software, video game, cassette tape, or vinyl record that has been opened.</li>
<li>Any item not in its original condition, is damaged or missing parts for reasons not due to our error.</li>
<li>Any item that is returned more than 30 days after delivery</li>
</ul>
<!-- /wp:list -->

<!-- wp:paragraph -->
<h2>Refunds</h2>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Once your return is received and inspected, we will send you an email to notify you that we have received your returned item. We will also notify you of the approval or rejection of your refund.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>If you are approved, then your refund will be processed, and a credit will automatically be applied to your credit card or original method of payment, within a certain amount of days.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<b>Late or missing refunds</b>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>If you haven’t received a refund yet, first check your bank account again.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Then contact your credit card company, it may take some time before your refund is officially posted.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Next contact your bank. There is often some processing time before a refund is posted.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>If you’ve done all of this and you still have not received your refund yet, please contact us at {email address}.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<b>Sale items</b>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Only regular priced items may be refunded. Sale items cannot be refunded.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<h2>Exchanges</h2>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>We only replace items if they are defective or damaged. If you need to exchange it for the same item, send us an email at {email address} and send your item to: {physical address}.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<h2>Gifts</h2>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>If the item was marked as a gift when purchased and shipped directly to you, you’ll receive a gift credit for the value of your return. Once the returned item is received, a gift certificate will be mailed to you.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>If the item wasn’t marked as a gift when purchased, or the gift giver had the order shipped to themselves to give to you later, we will send a refund to the gift giver and they will find out about your return.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<h2>Shipping returns</h2>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>To return your product, you should mail your product to: {physical address}.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>You will be responsible for paying for your own shipping costs for returning your item. Shipping costs are non-refundable. If you receive a refund, the cost of return shipping will be deducted from your refund.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Depending on where you live, the time it may take for your exchanged product to reach you may vary.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>If you are returning more expensive items, you may consider using a trackable shipping service or purchasing shipping insurance. We don’t guarantee that we will receive your returned item.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<h2>Need help?</h2>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Contact us at {email} for questions related to refunds and returns.</p>
<!-- /wp:paragraph -->
EOT;
	}

	/**
	 * Adds an admin inbox note after a page has been created to notify
	 * user. For example to take action to edit the page such as the
	 * Refund and returns page.
	 *
	 * @since 5.6.0
	 * @return void
	 */
	public static function add_admin_note_after_page_created() {
		if ( ! WC()->is_wc_admin_active() ) {
			return;
		}

		$page_id = get_option( 'woocommerce_refund_returns_page_created', null );

		if ( null === $page_id ) {
			return;
		}

		WC_Notes_Refund_Returns::possibly_add_note( $page_id );
	}

	/**
	 * When pages are created, we might want to take some action.
	 * In this case we want to set an option when refund and returns
	 * page is created.
	 *
	 * @since 5.6.0
	 * @param int   $page_id ID of the page.
	 * @param array $page_data The data of the page created.
	 * @return void
	 */
	public static function page_created( $page_id, $page_data ) {
		if ( 'refund_returns' === $page_data['post_name'] ) {
			delete_option( 'woocommerce_refund_returns_page_created' );
			add_option( 'woocommerce_refund_returns_page_created', $page_id, '', false );
		}
	}
}

WC_Install::init();
wc-conditional-functions.php000064400000027464151336044770012224 0ustar00<?php
/**
 * WooCommerce Conditional Functions
 *
 * Functions for determining the current query/page.
 *
 * @package     WooCommerce\Functions
 * @version     2.3.0
 */

use Automattic\Jetpack\Constants;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Is_woocommerce - Returns true if on a page which uses WooCommerce templates (cart and checkout are standard pages with shortcodes and thus are not included).
 *
 * @return bool
 */
function is_woocommerce() {
	return apply_filters( 'is_woocommerce', is_shop() || is_product_taxonomy() || is_product() );
}

if ( ! function_exists( 'is_shop' ) ) {

	/**
	 * Is_shop - Returns true when viewing the product type archive (shop).
	 *
	 * @return bool
	 */
	function is_shop() {
		return ( is_post_type_archive( 'product' ) || is_page( wc_get_page_id( 'shop' ) ) );
	}
}

if ( ! function_exists( 'is_product_taxonomy' ) ) {

	/**
	 * Is_product_taxonomy - Returns true when viewing a product taxonomy archive.
	 *
	 * @return bool
	 */
	function is_product_taxonomy() {
		return is_tax( get_object_taxonomies( 'product' ) );
	}
}

if ( ! function_exists( 'is_product_category' ) ) {

	/**
	 * Is_product_category - Returns true when viewing a product category.
	 *
	 * @param  string $term (default: '') The term slug your checking for. Leave blank to return true on any.
	 * @return bool
	 */
	function is_product_category( $term = '' ) {
		return is_tax( 'product_cat', $term );
	}
}

if ( ! function_exists( 'is_product_tag' ) ) {

	/**
	 * Is_product_tag - Returns true when viewing a product tag.
	 *
	 * @param  string $term (default: '') The term slug your checking for. Leave blank to return true on any.
	 * @return bool
	 */
	function is_product_tag( $term = '' ) {
		return is_tax( 'product_tag', $term );
	}
}

if ( ! function_exists( 'is_product' ) ) {

	/**
	 * Is_product - Returns true when viewing a single product.
	 *
	 * @return bool
	 */
	function is_product() {
		return is_singular( array( 'product' ) );
	}
}

if ( ! function_exists( 'is_cart' ) ) {

	/**
	 * Is_cart - Returns true when viewing the cart page.
	 *
	 * @return bool
	 */
	function is_cart() {
		$page_id = wc_get_page_id( 'cart' );

		return ( $page_id && is_page( $page_id ) ) || Constants::is_defined( 'WOOCOMMERCE_CART' ) || wc_post_content_has_shortcode( 'woocommerce_cart' );
	}
}

if ( ! function_exists( 'is_checkout' ) ) {

	/**
	 * Is_checkout - Returns true when viewing the checkout page.
	 *
	 * @return bool
	 */
	function is_checkout() {
		$page_id = wc_get_page_id( 'checkout' );

		return ( $page_id && is_page( $page_id ) ) || wc_post_content_has_shortcode( 'woocommerce_checkout' ) || apply_filters( 'woocommerce_is_checkout', false ) || Constants::is_defined( 'WOOCOMMERCE_CHECKOUT' );
	}
}

if ( ! function_exists( 'is_checkout_pay_page' ) ) {

	/**
	 * Is_checkout_pay - Returns true when viewing the checkout's pay page.
	 *
	 * @return bool
	 */
	function is_checkout_pay_page() {
		global $wp;

		return is_checkout() && ! empty( $wp->query_vars['order-pay'] );
	}
}

if ( ! function_exists( 'is_wc_endpoint_url' ) ) {

	/**
	 * Is_wc_endpoint_url - Check if an endpoint is showing.
	 *
	 * @param string|false $endpoint Whether endpoint.
	 * @return bool
	 */
	function is_wc_endpoint_url( $endpoint = false ) {
		global $wp;

		$wc_endpoints = WC()->query->get_query_vars();

		if ( false !== $endpoint ) {
			if ( ! isset( $wc_endpoints[ $endpoint ] ) ) {
				return false;
			} else {
				$endpoint_var = $wc_endpoints[ $endpoint ];
			}

			return isset( $wp->query_vars[ $endpoint_var ] );
		} else {
			foreach ( $wc_endpoints as $key => $value ) {
				if ( isset( $wp->query_vars[ $key ] ) ) {
					return true;
				}
			}

			return false;
		}
	}
}

if ( ! function_exists( 'is_account_page' ) ) {

	/**
	 * Is_account_page - Returns true when viewing an account page.
	 *
	 * @return bool
	 */
	function is_account_page() {
		$page_id = wc_get_page_id( 'myaccount' );

		return ( $page_id && is_page( $page_id ) ) || wc_post_content_has_shortcode( 'woocommerce_my_account' ) || apply_filters( 'woocommerce_is_account_page', false );
	}
}

if ( ! function_exists( 'is_view_order_page' ) ) {

	/**
	 * Is_view_order_page - Returns true when on the view order page.
	 *
	 * @return bool
	 */
	function is_view_order_page() {
		global $wp;

		$page_id = wc_get_page_id( 'myaccount' );

		return ( $page_id && is_page( $page_id ) && isset( $wp->query_vars['view-order'] ) );
	}
}

if ( ! function_exists( 'is_edit_account_page' ) ) {

	/**
	 * Check for edit account page.
	 * Returns true when viewing the edit account page.
	 *
	 * @since 2.5.1
	 * @return bool
	 */
	function is_edit_account_page() {
		global $wp;

		$page_id = wc_get_page_id( 'myaccount' );

		return ( $page_id && is_page( $page_id ) && isset( $wp->query_vars['edit-account'] ) );
	}
}

if ( ! function_exists( 'is_order_received_page' ) ) {

	/**
	 * Is_order_received_page - Returns true when viewing the order received page.
	 *
	 * @return bool
	 */
	function is_order_received_page() {
		global $wp;

		$page_id = wc_get_page_id( 'checkout' );

		return apply_filters( 'woocommerce_is_order_received_page', ( $page_id && is_page( $page_id ) && isset( $wp->query_vars['order-received'] ) ) );
	}
}

if ( ! function_exists( 'is_add_payment_method_page' ) ) {

	/**
	 * Is_add_payment_method_page - Returns true when viewing the add payment method page.
	 *
	 * @return bool
	 */
	function is_add_payment_method_page() {
		global $wp;

		$page_id = wc_get_page_id( 'myaccount' );

		return ( $page_id && is_page( $page_id ) && ( isset( $wp->query_vars['payment-methods'] ) || isset( $wp->query_vars['add-payment-method'] ) ) );
	}
}

if ( ! function_exists( 'is_lost_password_page' ) ) {

	/**
	 * Is_lost_password_page - Returns true when viewing the lost password page.
	 *
	 * @return bool
	 */
	function is_lost_password_page() {
		global $wp;

		$page_id = wc_get_page_id( 'myaccount' );

		return ( $page_id && is_page( $page_id ) && isset( $wp->query_vars['lost-password'] ) );
	}
}

if ( ! function_exists( 'is_ajax' ) ) {

	/**
	 * Is_ajax - Returns true when the page is loaded via ajax.
	 *
	 * @return bool
	 */
	function is_ajax() {
		return function_exists( 'wp_doing_ajax' ) ? wp_doing_ajax() : Constants::is_defined( 'DOING_AJAX' );
	}
}

if ( ! function_exists( 'is_store_notice_showing' ) ) {

	/**
	 * Is_store_notice_showing - Returns true when store notice is active.
	 *
	 * @return bool
	 */
	function is_store_notice_showing() {
		return 'no' !== get_option( 'woocommerce_demo_store', 'no' );
	}
}

if ( ! function_exists( 'is_filtered' ) ) {

	/**
	 * Is_filtered - Returns true when filtering products using layered nav or price sliders.
	 *
	 * @return bool
	 */
	function is_filtered() {
		return apply_filters( 'woocommerce_is_filtered', ( count( WC_Query::get_layered_nav_chosen_attributes() ) > 0 || isset( $_GET['max_price'] ) || isset( $_GET['min_price'] ) || isset( $_GET['rating_filter'] ) ) ); // WPCS: CSRF ok.
	}
}

if ( ! function_exists( 'taxonomy_is_product_attribute' ) ) {

	/**
	 * Returns true when the passed taxonomy name is a product attribute.
	 *
	 * @uses   $wc_product_attributes global which stores taxonomy names upon registration
	 * @param  string $name of the attribute.
	 * @return bool
	 */
	function taxonomy_is_product_attribute( $name ) {
		global $wc_product_attributes;

		return taxonomy_exists( $name ) && array_key_exists( $name, (array) $wc_product_attributes );
	}
}

if ( ! function_exists( 'meta_is_product_attribute' ) ) {

	/**
	 * Returns true when the passed meta name is a product attribute.
	 *
	 * @param  string $name of the attribute.
	 * @param  string $value of the attribute.
	 * @param  int    $product_id to check for attribute.
	 * @return bool
	 */
	function meta_is_product_attribute( $name, $value, $product_id ) {
		$product = wc_get_product( $product_id );

		if ( $product && method_exists( $product, 'get_variation_attributes' ) ) {
			$variation_attributes = $product->get_variation_attributes();
			$attributes           = $product->get_attributes();
			return ( in_array( $name, array_keys( $attributes ), true ) && in_array( $value, $variation_attributes[ $attributes[ $name ]['name'] ], true ) );
		} else {
			return false;
		}
	}
}

if ( ! function_exists( 'wc_tax_enabled' ) ) {

	/**
	 * Are store-wide taxes enabled?
	 *
	 * @return bool
	 */
	function wc_tax_enabled() {
		return apply_filters( 'wc_tax_enabled', get_option( 'woocommerce_calc_taxes' ) === 'yes' );
	}
}

if ( ! function_exists( 'wc_shipping_enabled' ) ) {

	/**
	 * Is shipping enabled?
	 *
	 * @return bool
	 */
	function wc_shipping_enabled() {
		return apply_filters( 'wc_shipping_enabled', get_option( 'woocommerce_ship_to_countries' ) !== 'disabled' );
	}
}

if ( ! function_exists( 'wc_prices_include_tax' ) ) {

	/**
	 * Are prices inclusive of tax?
	 *
	 * @return bool
	 */
	function wc_prices_include_tax() {
		return wc_tax_enabled() && apply_filters( 'woocommerce_prices_include_tax', get_option( 'woocommerce_prices_include_tax' ) === 'yes' );
	}
}

/**
 * Simple check for validating a URL, it must start with http:// or https://.
 * and pass FILTER_VALIDATE_URL validation.
 *
 * @param  string $url to check.
 * @return bool
 */
function wc_is_valid_url( $url ) {

	// Must start with http:// or https://.
	if ( 0 !== strpos( $url, 'http://' ) && 0 !== strpos( $url, 'https://' ) ) {
		return false;
	}

	// Must pass validation.
	if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
		return false;
	}

	return true;
}

/**
 * Check if the home URL is https. If it is, we don't need to do things such as 'force ssl'.
 *
 * @since  2.4.13
 * @return bool
 */
function wc_site_is_https() {
	return false !== strstr( get_option( 'home' ), 'https:' );
}

/**
 * Check if the checkout is configured for https. Look at options, WP HTTPS plugin, or the permalink itself.
 *
 * @since  2.5.0
 * @return bool
 */
function wc_checkout_is_https() {
	return wc_site_is_https() || 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) || class_exists( 'WordPressHTTPS' ) || strstr( wc_get_page_permalink( 'checkout' ), 'https:' );
}

/**
 * Checks whether the content passed contains a specific short code.
 *
 * @param  string $tag Shortcode tag to check.
 * @return bool
 */
function wc_post_content_has_shortcode( $tag = '' ) {
	global $post;

	return is_singular() && is_a( $post, 'WP_Post' ) && has_shortcode( $post->post_content, $tag );
}

/**
 * Check if reviews are enabled.
 *
 * @since 3.6.0
 * @return bool
 */
function wc_reviews_enabled() {
	return 'yes' === get_option( 'woocommerce_enable_reviews' );
}

/**
 * Check if reviews ratings are enabled.
 *
 * @since 3.6.0
 * @return bool
 */
function wc_review_ratings_enabled() {
	return wc_reviews_enabled() && 'yes' === get_option( 'woocommerce_enable_review_rating' );
}

/**
 * Check if review ratings are required.
 *
 * @since 3.6.0
 * @return bool
 */
function wc_review_ratings_required() {
	return 'yes' === get_option( 'woocommerce_review_rating_required' );
}

/**
 * Check if a CSV file is valid.
 *
 * @since 3.6.5
 * @param string $file       File name.
 * @param bool   $check_path If should check for the path.
 * @return bool
 */
function wc_is_file_valid_csv( $file, $check_path = true ) {
	/**
	 * Filter check for CSV file path.
	 *
	 * @since 3.6.4
	 * @param bool $check_import_file_path If requires file path check. Defaults to true.
	 */
	$check_import_file_path = apply_filters( 'woocommerce_csv_importer_check_import_file_path', true );

	if ( $check_path && $check_import_file_path && false !== stripos( $file, '://' ) ) {
		return false;
	}

	/**
	 * Filter CSV valid file types.
	 *
	 * @since 3.6.5
	 * @param array $valid_filetypes List of valid file types.
	 */
	$valid_filetypes = apply_filters(
		'woocommerce_csv_import_valid_filetypes',
		array(
			'csv' => 'text/csv',
			'txt' => 'text/plain',
		)
	);

	$filetype = wp_check_filetype( $file, $valid_filetypes );

	if ( in_array( $filetype['type'], $valid_filetypes, true ) ) {
		return true;
	}

	return false;
}
class-wc-validation.php000064400000013527151336044770011143 0ustar00<?php
/**
 * General user data validation methods
 *
 * @package WooCommerce\Classes
 * @version  2.4.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Validation class.
 */
class WC_Validation {

	/**
	 * Validates an email using WordPress native is_email function.
	 *
	 * @param  string $email Email address to validate.
	 * @return bool
	 */
	public static function is_email( $email ) {
		return is_email( $email );
	}

	/**
	 * Validates a phone number using a regular expression.
	 *
	 * @param  string $phone Phone number to validate.
	 * @return bool
	 */
	public static function is_phone( $phone ) {
		if ( 0 < strlen( trim( preg_replace( '/[\s\#0-9_\-\+\/\(\)\.]/', '', $phone ) ) ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Checks for a valid postcode.
	 *
	 * @param  string $postcode Postcode to validate.
	 * @param  string $country Country to validate the postcode for.
	 * @return bool
	 */
	public static function is_postcode( $postcode, $country ) {
		if ( strlen( trim( preg_replace( '/[\s\-A-Za-z0-9]/', '', $postcode ) ) ) > 0 ) {
			return false;
		}

		switch ( $country ) {
			case 'AT':
				$valid = (bool) preg_match( '/^([0-9]{4})$/', $postcode );
				break;
			case 'BA':
				$valid = (bool) preg_match( '/^([7-8]{1})([0-9]{4})$/', $postcode );
				break;
			case 'BE':
				$valid = (bool) preg_match( '/^([0-9]{4})$/i', $postcode );
				break;
			case 'BR':
				$valid = (bool) preg_match( '/^([0-9]{5})([-])?([0-9]{3})$/', $postcode );
				break;
			case 'CH':
				$valid = (bool) preg_match( '/^([0-9]{4})$/i', $postcode );
				break;
			case 'DE':
				$valid = (bool) preg_match( '/^([0]{1}[1-9]{1}|[1-9]{1}[0-9]{1})[0-9]{3}$/', $postcode );
				break;
			case 'ES':
			case 'FR':
			case 'IT':
				$valid = (bool) preg_match( '/^([0-9]{5})$/i', $postcode );
				break;
			case 'GB':
				$valid = self::is_gb_postcode( $postcode );
				break;
			case 'HU':
				$valid = (bool) preg_match( '/^([0-9]{4})$/i', $postcode );
				break;
			case 'IE':
				$valid = (bool) preg_match( '/([AC-FHKNPRTV-Y]\d{2}|D6W)[0-9AC-FHKNPRTV-Y]{4}/', wc_normalize_postcode( $postcode ) );
				break;
			case 'IN':
				$valid = (bool) preg_match( '/^[1-9]{1}[0-9]{2}\s{0,1}[0-9]{3}$/', $postcode );
				break;
			case 'JP':
				$valid = (bool) preg_match( '/^([0-9]{3})([-]?)([0-9]{4})$/', $postcode );
				break;
			case 'PT':
				$valid = (bool) preg_match( '/^([0-9]{4})([-])([0-9]{3})$/', $postcode );
				break;
			case 'PR':
			case 'US':
				$valid = (bool) preg_match( '/^([0-9]{5})(-[0-9]{4})?$/i', $postcode );
				break;
			case 'CA':
				// CA Postal codes cannot contain D,F,I,O,Q,U and cannot start with W or Z. https://en.wikipedia.org/wiki/Postal_codes_in_Canada#Number_of_possible_postal_codes.
				$valid = (bool) preg_match( '/^([ABCEGHJKLMNPRSTVXY]\d[ABCEGHJKLMNPRSTVWXYZ])([\ ])?(\d[ABCEGHJKLMNPRSTVWXYZ]\d)$/i', $postcode );
				break;
			case 'PL':
				$valid = (bool) preg_match( '/^([0-9]{2})([-])([0-9]{3})$/', $postcode );
				break;
			case 'CZ':
			case 'SK':
				$valid = (bool) preg_match( '/^([0-9]{3})(\s?)([0-9]{2})$/', $postcode );
				break;
			case 'NL':
				$valid = (bool) preg_match( '/^([1-9][0-9]{3})(\s?)(?!SA|SD|SS)[A-Z]{2}$/i', $postcode );
				break;
			case 'SI':
				$valid = (bool) preg_match( '/^([1-9][0-9]{3})$/', $postcode );
				break;
			case 'LI':
				$valid = (bool) preg_match( '/^(94[8-9][0-9])$/', $postcode );
				break;
			default:
				$valid = true;
				break;
		}

		return apply_filters( 'woocommerce_validate_postcode', $valid, $postcode, $country );
	}

	/**
	 * Check if is a GB postcode.
	 *
	 * @param  string $to_check A postcode.
	 * @return bool
	 */
	public static function is_gb_postcode( $to_check ) {

		// Permitted letters depend upon their position in the postcode.
		// https://en.wikipedia.org/wiki/Postcodes_in_the_United_Kingdom#Validation.
		$alpha1 = '[abcdefghijklmnoprstuwyz]'; // Character 1.
		$alpha2 = '[abcdefghklmnopqrstuvwxy]'; // Character 2.
		$alpha3 = '[abcdefghjkpstuw]';         // Character 3 == ABCDEFGHJKPSTUW.
		$alpha4 = '[abehmnprvwxy]';            // Character 4 == ABEHMNPRVWXY.
		$alpha5 = '[abdefghjlnpqrstuwxyz]';    // Character 5 != CIKMOV.

		$pcexp = array();

		// Expression for postcodes: AN NAA, ANN NAA, AAN NAA, and AANN NAA.
		$pcexp[0] = '/^(' . $alpha1 . '{1}' . $alpha2 . '{0,1}[0-9]{1,2})([0-9]{1}' . $alpha5 . '{2})$/';

		// Expression for postcodes: ANA NAA.
		$pcexp[1] = '/^(' . $alpha1 . '{1}[0-9]{1}' . $alpha3 . '{1})([0-9]{1}' . $alpha5 . '{2})$/';

		// Expression for postcodes: AANA NAA.
		$pcexp[2] = '/^(' . $alpha1 . '{1}' . $alpha2 . '[0-9]{1}' . $alpha4 . ')([0-9]{1}' . $alpha5 . '{2})$/';

		// Exception for the special postcode GIR 0AA.
		$pcexp[3] = '/^(gir)(0aa)$/';

		// Standard BFPO numbers.
		$pcexp[4] = '/^(bfpo)([0-9]{1,4})$/';

		// c/o BFPO numbers.
		$pcexp[5] = '/^(bfpo)(c\/o[0-9]{1,3})$/';

		// Load up the string to check, converting into lowercase and removing spaces.
		$postcode = strtolower( $to_check );
		$postcode = str_replace( ' ', '', $postcode );

		// Assume we are not going to find a valid postcode.
		$valid = false;

		// Check the string against the six types of postcodes.
		foreach ( $pcexp as $regexp ) {
			if ( preg_match( $regexp, $postcode, $matches ) ) {
				// Remember that we have found that the code is valid and break from loop.
				$valid = true;
				break;
			}
		}

		return $valid;
	}

	/**
	 * Format the postcode according to the country and length of the postcode.
	 *
	 * @param  string $postcode Postcode to format.
	 * @param  string $country Country to format the postcode for.
	 * @return string  Formatted postcode.
	 */
	public static function format_postcode( $postcode, $country ) {
		return wc_format_postcode( $postcode, $country );
	}

	/**
	 * Format a given phone number.
	 *
	 * @param  mixed $tel Phone number to format.
	 * @return string
	 */
	public static function format_phone( $tel ) {
		return wc_format_phone_number( $tel );
	}
}
class-wc-deprecated-action-hooks.php000064400000015602151336044770013501 0ustar00<?php
/**
 * Deprecated action hooks
 *
 * @package WooCommerce\Abstracts
 * @since   3.0.0
 * @version 3.3.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Handles deprecation notices and triggering of legacy action hooks.
 */
class WC_Deprecated_Action_Hooks extends WC_Deprecated_Hooks {

	/**
	 * Array of deprecated hooks we need to handle. Format of 'new' => 'old'.
	 *
	 * @var array
	 */
	protected $deprecated_hooks = array(
		'woocommerce_new_order_item'        => array(
			'woocommerce_order_add_shipping',
			'woocommerce_order_add_coupon',
			'woocommerce_order_add_tax',
			'woocommerce_order_add_fee',
			'woocommerce_add_shipping_order_item',
			'woocommerce_add_order_item_meta',
			'woocommerce_add_order_fee_meta',
		),
		'woocommerce_update_order_item'     => array(
			'woocommerce_order_edit_product',
			'woocommerce_order_update_coupon',
			'woocommerce_order_update_shipping',
			'woocommerce_order_update_fee',
			'woocommerce_order_update_tax',
		),
		'woocommerce_new_payment_token'     => 'woocommerce_payment_token_created',
		'woocommerce_new_product_variation' => 'woocommerce_create_product_variation',
		'woocommerce_order_details_after_order_table_items' => 'woocommerce_order_items_table',

		'woocommerce_settings_advanced_page_options' => array(
			'woocommerce_settings_checkout_page_options',
			'woocommerce_settings_account_page_options',
		),
		'woocommerce_settings_advanced_page_options_end' => array(
			'woocommerce_settings_checkout_page_options_end',
			'woocommerce_settings_account_page_options_end',
		),
		'woocommerce_settings_advanced_page_options_after' => array(
			'woocommerce_settings_checkout_page_options_after',
			'woocommerce_settings_account_page_options_after',
		),
	);

	/**
	 * Array of versions on each hook has been deprecated.
	 *
	 * @var array
	 */
	protected $deprecated_version = array(
		'woocommerce_order_add_shipping'                   => '3.0.0',
		'woocommerce_order_add_coupon'                     => '3.0.0',
		'woocommerce_order_add_tax'                        => '3.0.0',
		'woocommerce_order_add_fee'                        => '3.0.0',
		'woocommerce_add_shipping_order_item'              => '3.0.0',
		'woocommerce_add_order_item_meta'                  => '3.0.0',
		'woocommerce_add_order_fee_meta'                   => '3.0.0',
		'woocommerce_order_edit_product'                   => '3.0.0',
		'woocommerce_order_update_coupon'                  => '3.0.0',
		'woocommerce_order_update_shipping'                => '3.0.0',
		'woocommerce_order_update_fee'                     => '3.0.0',
		'woocommerce_order_update_tax'                     => '3.0.0',
		'woocommerce_payment_token_created'                => '3.0.0',
		'woocommerce_create_product_variation'             => '3.0.0',
		'woocommerce_order_items_table'                    => '3.0.0',
		'woocommerce_settings_checkout_page_options'       => '3.4.0',
		'woocommerce_settings_account_page_options'        => '3.4.0',
		'woocommerce_settings_checkout_page_options_end'   => '3.4.0',
		'woocommerce_settings_account_page_options_end'    => '3.4.0',
		'woocommerce_settings_checkout_page_options_after' => '3.4.0',
		'woocommerce_settings_account_page_options_after'  => '3.4.0',
	);

	/**
	 * Hook into the new hook so we can handle deprecated hooks once fired.
	 *
	 * @param string $hook_name Hook name.
	 */
	public function hook_in( $hook_name ) {
		add_action( $hook_name, array( $this, 'maybe_handle_deprecated_hook' ), -1000, 8 );
	}

	/**
	 * If the old hook is in-use, trigger it.
	 *
	 * @param  string $new_hook          New hook name.
	 * @param  string $old_hook          Old hook name.
	 * @param  array  $new_callback_args New callback args.
	 * @param  mixed  $return_value      Returned value.
	 * @return mixed
	 */
	public function handle_deprecated_hook( $new_hook, $old_hook, $new_callback_args, $return_value ) {
		if ( has_action( $old_hook ) ) {
			$this->display_notice( $old_hook, $new_hook );
			$return_value = $this->trigger_hook( $old_hook, $new_callback_args );
		}
		return $return_value;
	}

	/**
	 * Fire off a legacy hook with it's args.
	 *
	 * @param  string $old_hook          Old hook name.
	 * @param  array  $new_callback_args New callback args.
	 * @return mixed
	 */
	protected function trigger_hook( $old_hook, $new_callback_args ) {
		switch ( $old_hook ) {
			case 'woocommerce_order_add_shipping':
			case 'woocommerce_order_add_fee':
				$item_id  = $new_callback_args[0];
				$item     = $new_callback_args[1];
				$order_id = $new_callback_args[2];
				if ( is_a( $item, 'WC_Order_Item_Shipping' ) || is_a( $item, 'WC_Order_Item_Fee' ) ) {
					do_action( $old_hook, $order_id, $item_id, $item );
				}
				break;
			case 'woocommerce_order_add_coupon':
				$item_id  = $new_callback_args[0];
				$item     = $new_callback_args[1];
				$order_id = $new_callback_args[2];
				if ( is_a( $item, 'WC_Order_Item_Coupon' ) ) {
					do_action( $old_hook, $order_id, $item_id, $item->get_code(), $item->get_discount(), $item->get_discount_tax() );
				}
				break;
			case 'woocommerce_order_add_tax':
				$item_id  = $new_callback_args[0];
				$item     = $new_callback_args[1];
				$order_id = $new_callback_args[2];
				if ( is_a( $item, 'WC_Order_Item_Tax' ) ) {
					do_action( $old_hook, $order_id, $item_id, $item->get_rate_id(), $item->get_tax_total(), $item->get_shipping_tax_total() );
				}
				break;
			case 'woocommerce_add_shipping_order_item':
				$item_id  = $new_callback_args[0];
				$item     = $new_callback_args[1];
				$order_id = $new_callback_args[2];
				if ( is_a( $item, 'WC_Order_Item_Shipping' ) ) {
					do_action( $old_hook, $order_id, $item_id, $item->legacy_package_key );
				}
				break;
			case 'woocommerce_add_order_item_meta':
				$item_id  = $new_callback_args[0];
				$item     = $new_callback_args[1];
				$order_id = $new_callback_args[2];
				if ( is_a( $item, 'WC_Order_Item_Product' ) ) {
					do_action( $old_hook, $item_id, $item->legacy_values, $item->legacy_cart_item_key );
				}
				break;
			case 'woocommerce_add_order_fee_meta':
				$item_id  = $new_callback_args[0];
				$item     = $new_callback_args[1];
				$order_id = $new_callback_args[2];
				if ( is_a( $item, 'WC_Order_Item_Fee' ) ) {
					do_action( $old_hook, $order_id, $item_id, $item->legacy_fee, $item->legacy_fee_key );
				}
				break;
			case 'woocommerce_order_edit_product':
				$item_id  = $new_callback_args[0];
				$item     = $new_callback_args[1];
				$order_id = $new_callback_args[2];
				if ( is_a( $item, 'WC_Order_Item_Product' ) ) {
					do_action( $old_hook, $order_id, $item_id, $item, $item->get_product() );
				}
				break;
			case 'woocommerce_order_update_coupon':
			case 'woocommerce_order_update_shipping':
			case 'woocommerce_order_update_fee':
			case 'woocommerce_order_update_tax':
				if ( ! is_a( $item, 'WC_Order_Item_Product' ) ) {
					do_action( $old_hook, $order_id, $item_id, $item );
				}
				break;
			default:
				do_action_ref_array( $old_hook, $new_callback_args );
				break;
		}
	}
}
wc-coupon-functions.php000064400000005265151336044770011217 0ustar00<?php
/**
 * WooCommerce Coupons Functions
 *
 * Functions for coupon specific things.
 *
 * @package WooCommerce\Functions
 * @version 3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Get coupon types.
 *
 * @return array
 */
function wc_get_coupon_types() {
	return (array) apply_filters(
		'woocommerce_coupon_discount_types',
		array(
			'percent'       => __( 'Percentage discount', 'woocommerce' ),
			'fixed_cart'    => __( 'Fixed cart discount', 'woocommerce' ),
			'fixed_product' => __( 'Fixed product discount', 'woocommerce' ),
		)
	);
}

/**
 * Get a coupon type's name.
 *
 * @param string $type Coupon type.
 * @return string
 */
function wc_get_coupon_type( $type = '' ) {
	$types = wc_get_coupon_types();
	return isset( $types[ $type ] ) ? $types[ $type ] : '';
}

/**
 * Coupon types that apply to individual products. Controls which validation rules will apply.
 *
 * @since  2.5.0
 * @return array
 */
function wc_get_product_coupon_types() {
	return (array) apply_filters( 'woocommerce_product_coupon_types', array( 'fixed_product', 'percent' ) );
}

/**
 * Coupon types that apply to the cart as a whole. Controls which validation rules will apply.
 *
 * @since  2.5.0
 * @return array
 */
function wc_get_cart_coupon_types() {
	return (array) apply_filters( 'woocommerce_cart_coupon_types', array( 'fixed_cart' ) );
}

/**
 * Check if coupons are enabled.
 * Filterable.
 *
 * @since  2.5.0
 *
 * @return bool
 */
function wc_coupons_enabled() {
	return apply_filters( 'woocommerce_coupons_enabled', 'yes' === get_option( 'woocommerce_enable_coupons' ) );
}

/**
 * Get coupon code by ID.
 *
 * @since 3.0.0
 * @param int $id Coupon ID.
 * @return string
 */
function wc_get_coupon_code_by_id( $id ) {
	$data_store = WC_Data_Store::load( 'coupon' );
	return empty( $id ) ? '' : (string) $data_store->get_code_by_id( $id );
}

/**
 * Get coupon ID by code.
 *
 * @since 3.0.0
 * @param string $code    Coupon code.
 * @param int    $exclude Used to exclude an ID from the check if you're checking existence.
 * @return int
 */
function wc_get_coupon_id_by_code( $code, $exclude = 0 ) {

	if ( empty( $code ) ) {
		return 0;
	}

	$data_store = WC_Data_Store::load( 'coupon' );
	$ids        = wp_cache_get( WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $code, 'coupons' );

	if ( false === $ids ) {
		$ids = $data_store->get_ids_by_code( $code );
		if ( $ids ) {
			wp_cache_set( WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $code, $ids, 'coupons' );
		}
	}

	$ids = array_diff( array_filter( array_map( 'absint', (array) $ids ) ), array( $exclude ) );

	return apply_filters( 'woocommerce_get_coupon_id_from_code', absint( current( $ids ) ), $code, $exclude );
}
class-wc-product-query.php000064400000004267151336044770011635 0ustar00<?php
/**
 * Class for parameter-based Product querying
 *
 * Args and usage: https://github.com/woocommerce/woocommerce/wiki/wc_get_products-and-WC_Product_Query
 *
 * @package  WooCommerce\Classes
 * @version  3.2.0
 * @since    3.2.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Product query class.
 */
class WC_Product_Query extends WC_Object_Query {

	/**
	 * Valid query vars for products.
	 *
	 * @return array
	 */
	protected function get_default_query_vars() {
		return array_merge(
			parent::get_default_query_vars(),
			array(
				'status'            => array( 'draft', 'pending', 'private', 'publish' ),
				'type'              => array_merge( array_keys( wc_get_product_types() ) ),
				'limit'             => get_option( 'posts_per_page' ),
				'include'           => array(),
				'date_created'      => '',
				'date_modified'     => '',
				'featured'          => '',
				'visibility'        => '',
				'sku'               => '',
				'price'             => '',
				'regular_price'     => '',
				'sale_price'        => '',
				'date_on_sale_from' => '',
				'date_on_sale_to'   => '',
				'total_sales'       => '',
				'tax_status'        => '',
				'tax_class'         => '',
				'manage_stock'      => '',
				'stock_quantity'    => '',
				'stock_status'      => '',
				'backorders'        => '',
				'low_stock_amount'  => '',
				'sold_individually' => '',
				'weight'            => '',
				'length'            => '',
				'width'             => '',
				'height'            => '',
				'reviews_allowed'   => '',
				'virtual'           => '',
				'downloadable'      => '',
				'category'          => array(),
				'tag'               => array(),
				'shipping_class'    => array(),
				'download_limit'    => '',
				'download_expiry'   => '',
				'average_rating'    => '',
				'review_count'      => '',
			)
		);
	}

	/**
	 * Get products matching the current query vars.
	 *
	 * @return array|object of WC_Product objects
	 */
	public function get_products() {
		$args    = apply_filters( 'woocommerce_product_object_query_args', $this->get_query_vars() );
		$results = WC_Data_Store::load( 'product' )->query( $args );
		return apply_filters( 'woocommerce_product_object_query', $results, $args );
	}
}
class-wc-auth.php000064400000027342151336044770007752 0ustar00<?php
/**
 * WooCommerce Auth
 *
 * Handles wc-auth endpoint requests.
 *
 * @package WooCommerce\RestApi
 * @since   2.4.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Auth class.
 */
class WC_Auth {

	/**
	 * Version.
	 *
	 * @var int
	 */
	const VERSION = 1;

	/**
	 * Setup class.
	 *
	 * @since 2.4.0
	 */
	public function __construct() {
		// Add query vars.
		add_filter( 'query_vars', array( $this, 'add_query_vars' ), 0 );

		// Register auth endpoint.
		add_action( 'init', array( __CLASS__, 'add_endpoint' ), 0 );

		// Handle auth requests.
		add_action( 'parse_request', array( $this, 'handle_auth_requests' ), 0 );
	}

	/**
	 * Add query vars.
	 *
	 * @since  2.4.0
	 * @param  array $vars Query variables.
	 * @return string[]
	 */
	public function add_query_vars( $vars ) {
		$vars[] = 'wc-auth-version';
		$vars[] = 'wc-auth-route';
		return $vars;
	}

	/**
	 * Add auth endpoint.
	 *
	 * @since 2.4.0
	 */
	public static function add_endpoint() {
		add_rewrite_rule( '^wc-auth/v([1]{1})/(.*)?', 'index.php?wc-auth-version=$matches[1]&wc-auth-route=$matches[2]', 'top' );
	}

	/**
	 * Get scope name.
	 *
	 * @since 2.4.0
	 * @param  string $scope Permission scope.
	 * @return string
	 */
	protected function get_i18n_scope( $scope ) {
		$permissions = array(
			'read'       => __( 'Read', 'woocommerce' ),
			'write'      => __( 'Write', 'woocommerce' ),
			'read_write' => __( 'Read/Write', 'woocommerce' ),
		);

		return $permissions[ $scope ];
	}

	/**
	 * Return a list of permissions a scope allows.
	 *
	 * @since  2.4.0
	 * @param  string $scope Permission scope.
	 * @return array
	 */
	protected function get_permissions_in_scope( $scope ) {
		$permissions = array();
		switch ( $scope ) {
			case 'read':
				$permissions[] = __( 'View coupons', 'woocommerce' );
				$permissions[] = __( 'View customers', 'woocommerce' );
				$permissions[] = __( 'View orders and sales reports', 'woocommerce' );
				$permissions[] = __( 'View products', 'woocommerce' );
				break;
			case 'write':
				$permissions[] = __( 'Create webhooks', 'woocommerce' );
				$permissions[] = __( 'Create coupons', 'woocommerce' );
				$permissions[] = __( 'Create customers', 'woocommerce' );
				$permissions[] = __( 'Create orders', 'woocommerce' );
				$permissions[] = __( 'Create products', 'woocommerce' );
				break;
			case 'read_write':
				$permissions[] = __( 'Create webhooks', 'woocommerce' );
				$permissions[] = __( 'View and manage coupons', 'woocommerce' );
				$permissions[] = __( 'View and manage customers', 'woocommerce' );
				$permissions[] = __( 'View and manage orders and sales reports', 'woocommerce' );
				$permissions[] = __( 'View and manage products', 'woocommerce' );
				break;
		}
		return apply_filters( 'woocommerce_api_permissions_in_scope', $permissions, $scope );
	}

	/**
	 * Build auth urls.
	 *
	 * @since  2.4.0
	 * @param  array  $data     Data to build URL.
	 * @param  string $endpoint Endpoint.
	 * @return string
	 */
	protected function build_url( $data, $endpoint ) {
		$url = wc_get_endpoint_url( 'wc-auth/v' . self::VERSION, $endpoint, home_url( '/' ) );

		return add_query_arg(
			array(
				'app_name'     => wc_clean( $data['app_name'] ),
				'user_id'      => wc_clean( $data['user_id'] ),
				'return_url'   => rawurlencode( $this->get_formatted_url( $data['return_url'] ) ),
				'callback_url' => rawurlencode( $this->get_formatted_url( $data['callback_url'] ) ),
				'scope'        => wc_clean( $data['scope'] ),
			), $url
		);
	}

	/**
	 * Decode and format a URL.
	 *
	 * @param  string $url URL.
	 * @return string
	 */
	protected function get_formatted_url( $url ) {
		$url = urldecode( $url );

		if ( ! strstr( $url, '://' ) ) {
			$url = 'https://' . $url;
		}

		return $url;
	}

	/**
	 * Make validation.
	 *
	 * @since  2.4.0
	 * @throws Exception When validate fails.
	 */
	protected function make_validation() {
		$data   = array();
		$params = array(
			'app_name',
			'user_id',
			'return_url',
			'callback_url',
			'scope',
		);

		foreach ( $params as $param ) {
			if ( empty( $_REQUEST[ $param ] ) ) { // WPCS: input var ok, CSRF ok.
				/* translators: %s: parameter */
				throw new Exception( sprintf( __( 'Missing parameter %s', 'woocommerce' ), $param ) );
			}

			$data[ $param ] = wp_unslash( $_REQUEST[ $param ] ); // WPCS: input var ok, CSRF ok, sanitization ok.
		}

		if ( ! in_array( $data['scope'], array( 'read', 'write', 'read_write' ), true ) ) {
			/* translators: %s: scope */
			throw new Exception( sprintf( __( 'Invalid scope %s', 'woocommerce' ), wc_clean( $data['scope'] ) ) );
		}

		foreach ( array( 'return_url', 'callback_url' ) as $param ) {
			$param = $this->get_formatted_url( $data[ $param ] );

			if ( false === filter_var( $param, FILTER_VALIDATE_URL ) ) {
				/* translators: %s: url */
				throw new Exception( sprintf( __( 'The %s is not a valid URL', 'woocommerce' ), $param ) );
			}
		}

		$callback_url = $this->get_formatted_url( $data['callback_url'] );

		if ( 0 !== stripos( $callback_url, 'https://' ) ) {
			throw new Exception( __( 'The callback_url needs to be over SSL', 'woocommerce' ) );
		}
	}

	/**
	 * Create keys.
	 *
	 * @since  2.4.0
	 *
	 * @param  string $app_name    App name.
	 * @param  string $app_user_id User ID.
	 * @param  string $scope       Scope.
	 *
	 * @return array
	 */
	protected function create_keys( $app_name, $app_user_id, $scope ) {
		global $wpdb;

		$description = sprintf(
			/* translators: 1: app name 2: scope 3: date 4: time */
			__( '%1$s - API %2$s (created on %3$s at %4$s).', 'woocommerce' ),
			wc_clean( $app_name ),
			$this->get_i18n_scope( $scope ),
			date_i18n( wc_date_format() ),
			date_i18n( wc_time_format() )
		);
		$user = wp_get_current_user();

		// Created API keys.
		$permissions     = in_array( $scope, array( 'read', 'write', 'read_write' ), true ) ? sanitize_text_field( $scope ) : 'read';
		$consumer_key    = 'ck_' . wc_rand_hash();
		$consumer_secret = 'cs_' . wc_rand_hash();

		$wpdb->insert(
			$wpdb->prefix . 'woocommerce_api_keys',
			array(
				'user_id'         => $user->ID,
				'description'     => $description,
				'permissions'     => $permissions,
				'consumer_key'    => wc_api_hash( $consumer_key ),
				'consumer_secret' => $consumer_secret,
				'truncated_key'   => substr( $consumer_key, -7 ),
			),
			array(
				'%d',
				'%s',
				'%s',
				'%s',
				'%s',
				'%s',
			)
		);

		return array(
			'key_id'          => $wpdb->insert_id,
			'user_id'         => $app_user_id,
			'consumer_key'    => $consumer_key,
			'consumer_secret' => $consumer_secret,
			'key_permissions' => $permissions,
		);
	}

	/**
	 * Post consumer data.
	 *
	 * @since  2.4.0
	 *
	 * @throws Exception When validation fails.
	 * @param  array  $consumer_data Consumer data.
	 * @param  string $url           URL.
	 * @return bool
	 */
	protected function post_consumer_data( $consumer_data, $url ) {
		$params = array(
			'body'    => wp_json_encode( $consumer_data ),
			'timeout' => 60,
			'headers' => array(
				'Content-Type' => 'application/json;charset=' . get_bloginfo( 'charset' ),
			),
		);

		$response = wp_safe_remote_post( esc_url_raw( $url ), $params );

		if ( is_wp_error( $response ) ) {
			throw new Exception( $response->get_error_message() );
		} elseif ( 200 !== intval( $response['response']['code'] ) ) {
			throw new Exception( __( 'An error occurred in the request and at the time were unable to send the consumer data', 'woocommerce' ) );
		}

		return true;
	}

	/**
	 * Handle auth requests.
	 *
	 * @since 2.4.0
	 * @throws Exception When auth_endpoint validation fails.
	 */
	public function handle_auth_requests() {
		global $wp;

		if ( ! empty( $_GET['wc-auth-version'] ) ) { // WPCS: input var ok, CSRF ok.
			$wp->query_vars['wc-auth-version'] = wc_clean( wp_unslash( $_GET['wc-auth-version'] ) ); // WPCS: input var ok, CSRF ok.
		}

		if ( ! empty( $_GET['wc-auth-route'] ) ) { // WPCS: input var ok, CSRF ok.
			$wp->query_vars['wc-auth-route'] = wc_clean( wp_unslash( $_GET['wc-auth-route'] ) ); // WPCS: input var ok, CSRF ok.
		}

		// wc-auth endpoint requests.
		if ( ! empty( $wp->query_vars['wc-auth-version'] ) && ! empty( $wp->query_vars['wc-auth-route'] ) ) {
			$this->auth_endpoint( $wp->query_vars['wc-auth-route'] );
		}
	}

	/**
	 * Auth endpoint.
	 *
	 * @since 2.4.0
	 * @throws Exception When validation fails.
	 * @param string $route Route.
	 */
	protected function auth_endpoint( $route ) {
		ob_start();

		$consumer_data = array();

		try {
			$route = strtolower( wc_clean( $route ) );
			$this->make_validation();

			$data = wp_unslash( $_REQUEST ); // WPCS: input var ok, CSRF ok.

			// Login endpoint.
			if ( 'login' === $route && ! is_user_logged_in() ) {
				wc_get_template(
					'auth/form-login.php', array(
						'app_name'     => wc_clean( $data['app_name'] ),
						'return_url'   => add_query_arg(
							array(
								'success' => 0,
								'user_id' => wc_clean( $data['user_id'] ),
							), $this->get_formatted_url( $data['return_url'] )
						),
						'redirect_url' => $this->build_url( $data, 'authorize' ),
					)
				);
				exit;

			} elseif ( 'login' === $route && is_user_logged_in() ) {
				// Redirect with user is logged in.
				wp_redirect( esc_url_raw( $this->build_url( $data, 'authorize' ) ) );
				exit;

			} elseif ( 'authorize' === $route && ! is_user_logged_in() ) {
				// Redirect with user is not logged in and trying to access the authorize endpoint.
				wp_redirect( esc_url_raw( $this->build_url( $data, 'login' ) ) );
				exit;

			} elseif ( 'authorize' === $route && current_user_can( 'manage_woocommerce' ) ) {
				// Authorize endpoint.
				wc_get_template(
					'auth/form-grant-access.php', array(
						'app_name'    => wc_clean( $data['app_name'] ),
						'return_url'  => add_query_arg(
							array(
								'success' => 0,
								'user_id' => wc_clean( $data['user_id'] ),
							), $this->get_formatted_url( $data['return_url'] )
						),
						'scope'       => $this->get_i18n_scope( wc_clean( $data['scope'] ) ),
						'permissions' => $this->get_permissions_in_scope( wc_clean( $data['scope'] ) ),
						'granted_url' => wp_nonce_url( $this->build_url( $data, 'access_granted' ), 'wc_auth_grant_access', 'wc_auth_nonce' ),
						'logout_url'  => wp_logout_url( $this->build_url( $data, 'login' ) ),
						'user'        => wp_get_current_user(),
					)
				);
				exit;

			} elseif ( 'access_granted' === $route && current_user_can( 'manage_woocommerce' ) ) {
				// Granted access endpoint.
				if ( ! isset( $_GET['wc_auth_nonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_GET['wc_auth_nonce'] ) ), 'wc_auth_grant_access' ) ) { // WPCS: input var ok.
					throw new Exception( __( 'Invalid nonce verification', 'woocommerce' ) );
				}

				$consumer_data = $this->create_keys( $data['app_name'], $data['user_id'], $data['scope'] );
				$response      = $this->post_consumer_data( $consumer_data, $this->get_formatted_url( $data['callback_url'] ) );

				if ( $response ) {
					wp_redirect(
						esc_url_raw(
							add_query_arg(
								array(
									'success' => 1,
									'user_id' => wc_clean( $data['user_id'] ),
								), $this->get_formatted_url( $data['return_url'] )
							)
						)
					);
					exit;
				}
			} else {
				throw new Exception( __( 'You do not have permission to access this page', 'woocommerce' ) );
			}
		} catch ( Exception $e ) {
			$this->maybe_delete_key( $consumer_data );

			/* translators: %s: error message */
			wp_die( sprintf( esc_html__( 'Error: %s.', 'woocommerce' ), esc_html( $e->getMessage() ) ), esc_html__( 'Access denied', 'woocommerce' ), array( 'response' => 401 ) );
		}
	}

	/**
	 * Maybe delete key.
	 *
	 * @since 2.4.0
	 *
	 * @param array $key Key.
	 */
	private function maybe_delete_key( $key ) {
		global $wpdb;

		if ( isset( $key['key_id'] ) ) {
			$wpdb->delete( $wpdb->prefix . 'woocommerce_api_keys', array( 'key_id' => $key['key_id'] ), array( '%d' ) );
		}
	}
}
new WC_Auth();
class-wc-form-handler.php000064400000127752151336044770011375 0ustar00<?php
/**
 * Handle frontend forms.
 *
 * @package WooCommerce\Classes\
 */

defined( 'ABSPATH' ) || exit;

/**
 * WC_Form_Handler class.
 */
class WC_Form_Handler {

	/**
	 * Hook in methods.
	 */
	public static function init() {
		add_action( 'template_redirect', array( __CLASS__, 'redirect_reset_password_link' ) );
		add_action( 'template_redirect', array( __CLASS__, 'save_address' ) );
		add_action( 'template_redirect', array( __CLASS__, 'save_account_details' ) );
		add_action( 'wp_loaded', array( __CLASS__, 'checkout_action' ), 20 );
		add_action( 'wp_loaded', array( __CLASS__, 'process_login' ), 20 );
		add_action( 'wp_loaded', array( __CLASS__, 'process_registration' ), 20 );
		add_action( 'wp_loaded', array( __CLASS__, 'process_lost_password' ), 20 );
		add_action( 'wp_loaded', array( __CLASS__, 'process_reset_password' ), 20 );
		add_action( 'wp_loaded', array( __CLASS__, 'cancel_order' ), 20 );
		add_action( 'wp_loaded', array( __CLASS__, 'update_cart_action' ), 20 );
		add_action( 'wp_loaded', array( __CLASS__, 'add_to_cart_action' ), 20 );

		// May need $wp global to access query vars.
		add_action( 'wp', array( __CLASS__, 'pay_action' ), 20 );
		add_action( 'wp', array( __CLASS__, 'add_payment_method_action' ), 20 );
		add_action( 'wp', array( __CLASS__, 'delete_payment_method_action' ), 20 );
		add_action( 'wp', array( __CLASS__, 'set_default_payment_method_action' ), 20 );
	}

	/**
	 * Remove key and user ID (or user login, as a fallback) from query string, set cookie, and redirect to account page to show the form.
	 */
	public static function redirect_reset_password_link() {
		if ( is_account_page() && isset( $_GET['key'] ) && ( isset( $_GET['id'] ) || isset( $_GET['login'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended

			// If available, get $user_id from query string parameter for fallback purposes.
			if ( isset( $_GET['login'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
				$user    = get_user_by( 'login', sanitize_user( wp_unslash( $_GET['login'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
				$user_id = $user ? $user->ID : 0;
			} else {
				$user_id = absint( $_GET['id'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			}

			// If the reset token is not for the current user, ignore the reset request (don't redirect).
			$logged_in_user_id = get_current_user_id();
			if ( $logged_in_user_id && $logged_in_user_id !== $user_id ) {
				wc_add_notice( __( 'This password reset key is for a different user account. Please log out and try again.', 'woocommerce' ), 'error' );
				return;
			}

			$action = isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : '';
			$value   = sprintf( '%d:%s', $user_id, wp_unslash( $_GET['key'] ) ); // phpcs:ignore
			WC_Shortcode_My_Account::set_reset_password_cookie( $value );
			wp_safe_redirect(
				add_query_arg(
					array(
						'show-reset-form' => 'true',
						'action'          => $action,
					),
					wc_lostpassword_url()
				)
			);
			exit;
		}
	}

	/**
	 * Save and and update a billing or shipping address if the
	 * form was submitted through the user account page.
	 */
	public static function save_address() {
		global $wp;

		$nonce_value = wc_get_var( $_REQUEST['woocommerce-edit-address-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine.

		if ( ! wp_verify_nonce( $nonce_value, 'woocommerce-edit_address' ) ) {
			return;
		}

		if ( empty( $_POST['action'] ) || 'edit_address' !== $_POST['action'] ) {
			return;
		}

		wc_nocache_headers();

		$user_id = get_current_user_id();

		if ( $user_id <= 0 ) {
			return;
		}

		$customer = new WC_Customer( $user_id );

		if ( ! $customer ) {
			return;
		}

		$load_address = isset( $wp->query_vars['edit-address'] ) ? wc_edit_address_i18n( sanitize_title( $wp->query_vars['edit-address'] ), true ) : 'billing';

		if ( ! isset( $_POST[ $load_address . '_country' ] ) ) {
			return;
		}

		$address = WC()->countries->get_address_fields( wc_clean( wp_unslash( $_POST[ $load_address . '_country' ] ) ), $load_address . '_' );

		foreach ( $address as $key => $field ) {
			if ( ! isset( $field['type'] ) ) {
				$field['type'] = 'text';
			}

			// Get Value.
			if ( 'checkbox' === $field['type'] ) {
				$value = (int) isset( $_POST[ $key ] );
			} else {
				$value = isset( $_POST[ $key ] ) ? wc_clean( wp_unslash( $_POST[ $key ] ) ) : '';
			}

			// Hook to allow modification of value.
			$value = apply_filters( 'woocommerce_process_myaccount_field_' . $key, $value );

			// Validation: Required fields.
			if ( ! empty( $field['required'] ) && empty( $value ) ) {
				/* translators: %s: Field name. */
				wc_add_notice( sprintf( __( '%s is a required field.', 'woocommerce' ), $field['label'] ), 'error', array( 'id' => $key ) );
			}

			if ( ! empty( $value ) ) {
				// Validation and formatting rules.
				if ( ! empty( $field['validate'] ) && is_array( $field['validate'] ) ) {
					foreach ( $field['validate'] as $rule ) {
						switch ( $rule ) {
							case 'postcode':
								$country = wc_clean( wp_unslash( $_POST[ $load_address . '_country' ] ) );
								$value   = wc_format_postcode( $value, $country );

								if ( '' !== $value && ! WC_Validation::is_postcode( $value, $country ) ) {
									switch ( $country ) {
										case 'IE':
											$postcode_validation_notice = __( 'Please enter a valid Eircode.', 'woocommerce' );
											break;
										default:
											$postcode_validation_notice = __( 'Please enter a valid postcode / ZIP.', 'woocommerce' );
									}
									wc_add_notice( $postcode_validation_notice, 'error' );
								}
								break;
							case 'phone':
								if ( '' !== $value && ! WC_Validation::is_phone( $value ) ) {
									/* translators: %s: Phone number. */
									wc_add_notice( sprintf( __( '%s is not a valid phone number.', 'woocommerce' ), '<strong>' . $field['label'] . '</strong>' ), 'error' );
								}
								break;
							case 'email':
								$value = strtolower( $value );

								if ( ! is_email( $value ) ) {
									/* translators: %s: Email address. */
									wc_add_notice( sprintf( __( '%s is not a valid email address.', 'woocommerce' ), '<strong>' . $field['label'] . '</strong>' ), 'error' );
								}
								break;
						}
					}
				}
			}

			try {
				// Set prop in customer object.
				if ( is_callable( array( $customer, "set_$key" ) ) ) {
					$customer->{"set_$key"}( $value );
				} else {
					$customer->update_meta_data( $key, $value );
				}
			} catch ( WC_Data_Exception $e ) {
				// Set notices. Ignore invalid billing email, since is already validated.
				if ( 'customer_invalid_billing_email' !== $e->getErrorCode() ) {
					wc_add_notice( $e->getMessage(), 'error' );
				}
			}
		}

		/**
		 * Hook: woocommerce_after_save_address_validation.
		 *
		 * Allow developers to add custom validation logic and throw an error to prevent save.
		 *
		 * @param int         $user_id User ID being saved.
		 * @param string      $load_address Type of address e.g. billing or shipping.
		 * @param array       $address The address fields.
		 * @param WC_Customer $customer The customer object being saved. @since 3.6.0
		 */
		do_action( 'woocommerce_after_save_address_validation', $user_id, $load_address, $address, $customer );

		if ( 0 < wc_notice_count( 'error' ) ) {
			return;
		}

		$customer->save();

		wc_add_notice( __( 'Address changed successfully.', 'woocommerce' ) );

		do_action( 'woocommerce_customer_save_address', $user_id, $load_address );

		wp_safe_redirect( wc_get_endpoint_url( 'edit-address', '', wc_get_page_permalink( 'myaccount' ) ) );
		exit;
	}

	/**
	 * Save the password/account details and redirect back to the my account page.
	 */
	public static function save_account_details() {
		$nonce_value = wc_get_var( $_REQUEST['save-account-details-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine.

		if ( ! wp_verify_nonce( $nonce_value, 'save_account_details' ) ) {
			return;
		}

		if ( empty( $_POST['action'] ) || 'save_account_details' !== $_POST['action'] ) {
			return;
		}

		wc_nocache_headers();

		$user_id = get_current_user_id();

		if ( $user_id <= 0 ) {
			return;
		}

		$account_first_name   = ! empty( $_POST['account_first_name'] ) ? wc_clean( wp_unslash( $_POST['account_first_name'] ) ) : '';
		$account_last_name    = ! empty( $_POST['account_last_name'] ) ? wc_clean( wp_unslash( $_POST['account_last_name'] ) ) : '';
		$account_display_name = ! empty( $_POST['account_display_name'] ) ? wc_clean( wp_unslash( $_POST['account_display_name'] ) ) : '';
		$account_email        = ! empty( $_POST['account_email'] ) ? wc_clean( wp_unslash( $_POST['account_email'] ) ) : '';
		$pass_cur             = ! empty( $_POST['password_current'] ) ? $_POST['password_current'] : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
		$pass1                = ! empty( $_POST['password_1'] ) ? $_POST['password_1'] : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
		$pass2                = ! empty( $_POST['password_2'] ) ? $_POST['password_2'] : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
		$save_pass            = true;

		// Current user data.
		$current_user       = get_user_by( 'id', $user_id );
		$current_first_name = $current_user->first_name;
		$current_last_name  = $current_user->last_name;
		$current_email      = $current_user->user_email;

		// New user data.
		$user               = new stdClass();
		$user->ID           = $user_id;
		$user->first_name   = $account_first_name;
		$user->last_name    = $account_last_name;
		$user->display_name = $account_display_name;

		// Prevent display name to be changed to email.
		if ( is_email( $account_display_name ) ) {
			wc_add_notice( __( 'Display name cannot be changed to email address due to privacy concern.', 'woocommerce' ), 'error' );
		}

		// Handle required fields.
		$required_fields = apply_filters(
			'woocommerce_save_account_details_required_fields',
			array(
				'account_first_name'   => __( 'First name', 'woocommerce' ),
				'account_last_name'    => __( 'Last name', 'woocommerce' ),
				'account_display_name' => __( 'Display name', 'woocommerce' ),
				'account_email'        => __( 'Email address', 'woocommerce' ),
			)
		);

		foreach ( $required_fields as $field_key => $field_name ) {
			if ( empty( $_POST[ $field_key ] ) ) {
				/* translators: %s: Field name. */
				wc_add_notice( sprintf( __( '%s is a required field.', 'woocommerce' ), '<strong>' . esc_html( $field_name ) . '</strong>' ), 'error', array( 'id' => $field_key ) );
			}
		}

		if ( $account_email ) {
			$account_email = sanitize_email( $account_email );
			if ( ! is_email( $account_email ) ) {
				wc_add_notice( __( 'Please provide a valid email address.', 'woocommerce' ), 'error' );
			} elseif ( email_exists( $account_email ) && $account_email !== $current_user->user_email ) {
				wc_add_notice( __( 'This email address is already registered.', 'woocommerce' ), 'error' );
			}
			$user->user_email = $account_email;
		}

		if ( ! empty( $pass_cur ) && empty( $pass1 ) && empty( $pass2 ) ) {
			wc_add_notice( __( 'Please fill out all password fields.', 'woocommerce' ), 'error' );
			$save_pass = false;
		} elseif ( ! empty( $pass1 ) && empty( $pass_cur ) ) {
			wc_add_notice( __( 'Please enter your current password.', 'woocommerce' ), 'error' );
			$save_pass = false;
		} elseif ( ! empty( $pass1 ) && empty( $pass2 ) ) {
			wc_add_notice( __( 'Please re-enter your password.', 'woocommerce' ), 'error' );
			$save_pass = false;
		} elseif ( ( ! empty( $pass1 ) || ! empty( $pass2 ) ) && $pass1 !== $pass2 ) {
			wc_add_notice( __( 'New passwords do not match.', 'woocommerce' ), 'error' );
			$save_pass = false;
		} elseif ( ! empty( $pass1 ) && ! wp_check_password( $pass_cur, $current_user->user_pass, $current_user->ID ) ) {
			wc_add_notice( __( 'Your current password is incorrect.', 'woocommerce' ), 'error' );
			$save_pass = false;
		}

		if ( $pass1 && $save_pass ) {
			$user->user_pass = $pass1;
		}

		// Allow plugins to return their own errors.
		$errors = new WP_Error();
		do_action_ref_array( 'woocommerce_save_account_details_errors', array( &$errors, &$user ) );

		if ( $errors->get_error_messages() ) {
			foreach ( $errors->get_error_messages() as $error ) {
				wc_add_notice( $error, 'error' );
			}
		}

		if ( wc_notice_count( 'error' ) === 0 ) {
			wp_update_user( $user );

			// Update customer object to keep data in sync.
			$customer = new WC_Customer( $user->ID );

			if ( $customer ) {
				// Keep billing data in sync if data changed.
				if ( is_email( $user->user_email ) && $current_email !== $user->user_email ) {
					$customer->set_billing_email( $user->user_email );
				}

				if ( $current_first_name !== $user->first_name ) {
					$customer->set_billing_first_name( $user->first_name );
				}

				if ( $current_last_name !== $user->last_name ) {
					$customer->set_billing_last_name( $user->last_name );
				}

				$customer->save();
			}

			wc_add_notice( __( 'Account details changed successfully.', 'woocommerce' ) );

			do_action( 'woocommerce_save_account_details', $user->ID );

			wp_safe_redirect( wc_get_page_permalink( 'myaccount' ) );
			exit;
		}
	}

	/**
	 * Process the checkout form.
	 */
	public static function checkout_action() {
		if ( isset( $_POST['woocommerce_checkout_place_order'] ) || isset( $_POST['woocommerce_checkout_update_totals'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
			wc_nocache_headers();

			if ( WC()->cart->is_empty() ) {
				wp_safe_redirect( wc_get_cart_url() );
				exit;
			}

			wc_maybe_define_constant( 'WOOCOMMERCE_CHECKOUT', true );

			WC()->checkout()->process_checkout();
		}
	}

	/**
	 * Process the pay form.
	 *
	 * @throws Exception On payment error.
	 */
	public static function pay_action() {
		global $wp;

		if ( isset( $_POST['woocommerce_pay'], $_GET['key'] ) ) {
			wc_nocache_headers();

			$nonce_value = wc_get_var( $_REQUEST['woocommerce-pay-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine.

			if ( ! wp_verify_nonce( $nonce_value, 'woocommerce-pay' ) ) {
				return;
			}

			ob_start();

			// Pay for existing order.
			$order_key = wp_unslash( $_GET['key'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			$order_id  = absint( $wp->query_vars['order-pay'] );
			$order     = wc_get_order( $order_id );

			if ( $order_id === $order->get_id() && hash_equals( $order->get_order_key(), $order_key ) && $order->needs_payment() ) {

				do_action( 'woocommerce_before_pay_action', $order );

				WC()->customer->set_props(
					array(
						'billing_country'  => $order->get_billing_country() ? $order->get_billing_country() : null,
						'billing_state'    => $order->get_billing_state() ? $order->get_billing_state() : null,
						'billing_postcode' => $order->get_billing_postcode() ? $order->get_billing_postcode() : null,
						'billing_city'     => $order->get_billing_city() ? $order->get_billing_city() : null,
					)
				);
				WC()->customer->save();

				if ( ! empty( $_POST['terms-field'] ) && empty( $_POST['terms'] ) ) {
					wc_add_notice( __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' ), 'error' );
					return;
				}

				// Update payment method.
				if ( $order->needs_payment() ) {
					try {
						$payment_method_id = isset( $_POST['payment_method'] ) ? wc_clean( wp_unslash( $_POST['payment_method'] ) ) : false;

						if ( ! $payment_method_id ) {
							throw new Exception( __( 'Invalid payment method.', 'woocommerce' ) );
						}

						$available_gateways = WC()->payment_gateways->get_available_payment_gateways();
						$payment_method     = isset( $available_gateways[ $payment_method_id ] ) ? $available_gateways[ $payment_method_id ] : false;

						if ( ! $payment_method ) {
							throw new Exception( __( 'Invalid payment method.', 'woocommerce' ) );
						}

						$order->set_payment_method( $payment_method );
						$order->save();

						$payment_method->validate_fields();

						if ( 0 === wc_notice_count( 'error' ) ) {

							$result = $payment_method->process_payment( $order_id );

							// Redirect to success/confirmation/payment page.
							if ( isset( $result['result'] ) && 'success' === $result['result'] ) {
								$result['order_id'] = $order_id;

								$result = apply_filters( 'woocommerce_payment_successful_result', $result, $order_id );

								wp_redirect( $result['redirect'] ); //phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect
								exit;
							}
						}
					} catch ( Exception $e ) {
						wc_add_notice( $e->getMessage(), 'error' );
					}
				} else {
					// No payment was required for order.
					$order->payment_complete();
					wp_safe_redirect( $order->get_checkout_order_received_url() );
					exit;
				}

				do_action( 'woocommerce_after_pay_action', $order );

			}
		}
	}

	/**
	 * Process the add payment method form.
	 */
	public static function add_payment_method_action() {
		if ( isset( $_POST['woocommerce_add_payment_method'], $_POST['payment_method'] ) ) {
			wc_nocache_headers();

			$nonce_value = wc_get_var( $_REQUEST['woocommerce-add-payment-method-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine.

			if ( ! wp_verify_nonce( $nonce_value, 'woocommerce-add-payment-method' ) ) {
				return;
			}

			if ( ! apply_filters( 'woocommerce_add_payment_method_form_is_valid', true ) ) {
				return;
			}

			// Test rate limit.
			$current_user_id = get_current_user_id();
			$rate_limit_id   = 'add_payment_method_' . $current_user_id;
			$delay           = (int) apply_filters( 'woocommerce_payment_gateway_add_payment_method_delay', 20 );

			if ( WC_Rate_Limiter::retried_too_soon( $rate_limit_id ) ) {
				wc_add_notice(
					sprintf(
						/* translators: %d number of seconds */
						_n(
							'You cannot add a new payment method so soon after the previous one. Please wait for %d second.',
							'You cannot add a new payment method so soon after the previous one. Please wait for %d seconds.',
							$delay,
							'woocommerce'
						),
						$delay
					),
					'error'
				);
				return;
			}

			WC_Rate_Limiter::set_rate_limit( $rate_limit_id, $delay );

			ob_start();

			$payment_method_id  = wc_clean( wp_unslash( $_POST['payment_method'] ) );
			$available_gateways = WC()->payment_gateways->get_available_payment_gateways();

			if ( isset( $available_gateways[ $payment_method_id ] ) ) {
				$gateway = $available_gateways[ $payment_method_id ];

				if ( ! $gateway->supports( 'add_payment_method' ) && ! $gateway->supports( 'tokenization' ) ) {
					wc_add_notice( __( 'Invalid payment gateway.', 'woocommerce' ), 'error' );
					return;
				}

				$gateway->validate_fields();

				if ( wc_notice_count( 'error' ) > 0 ) {
					return;
				}

				$result = $gateway->add_payment_method();

				if ( 'success' === $result['result'] ) {
					wc_add_notice( __( 'Payment method successfully added.', 'woocommerce' ) );
				}

				if ( 'failure' === $result['result'] ) {
					wc_add_notice( __( 'Unable to add payment method to your account.', 'woocommerce' ), 'error' );
				}

				if ( ! empty( $result['redirect'] ) ) {
					wp_redirect( $result['redirect'] ); //phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect
					exit();
				}
			}
		}
	}

	/**
	 * Process the delete payment method form.
	 */
	public static function delete_payment_method_action() {
		global $wp;

		if ( isset( $wp->query_vars['delete-payment-method'] ) ) {
			wc_nocache_headers();

			$token_id = absint( $wp->query_vars['delete-payment-method'] );
			$token    = WC_Payment_Tokens::get( $token_id );

			if ( is_null( $token ) || get_current_user_id() !== $token->get_user_id() || ! isset( $_REQUEST['_wpnonce'] ) || false === wp_verify_nonce( wp_unslash( $_REQUEST['_wpnonce'] ), 'delete-payment-method-' . $token_id ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
				wc_add_notice( __( 'Invalid payment method.', 'woocommerce' ), 'error' );
			} else {
				WC_Payment_Tokens::delete( $token_id );
				wc_add_notice( __( 'Payment method deleted.', 'woocommerce' ) );
			}

			wp_safe_redirect( wc_get_account_endpoint_url( 'payment-methods' ) );
			exit();
		}

	}

	/**
	 * Process the delete payment method form.
	 */
	public static function set_default_payment_method_action() {
		global $wp;

		if ( isset( $wp->query_vars['set-default-payment-method'] ) ) {
			wc_nocache_headers();

			$token_id = absint( $wp->query_vars['set-default-payment-method'] );
			$token    = WC_Payment_Tokens::get( $token_id );

			if ( is_null( $token ) || get_current_user_id() !== $token->get_user_id() || ! isset( $_REQUEST['_wpnonce'] ) || false === wp_verify_nonce( wp_unslash( $_REQUEST['_wpnonce'] ), 'set-default-payment-method-' . $token_id ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
				wc_add_notice( __( 'Invalid payment method.', 'woocommerce' ), 'error' );
			} else {
				WC_Payment_Tokens::set_users_default( $token->get_user_id(), intval( $token_id ) );
				wc_add_notice( __( 'This payment method was successfully set as your default.', 'woocommerce' ) );
			}

			wp_safe_redirect( wc_get_account_endpoint_url( 'payment-methods' ) );
			exit();
		}

	}

	/**
	 * Remove from cart/update.
	 */
	public static function update_cart_action() {
		if ( ! ( isset( $_REQUEST['apply_coupon'] ) || isset( $_REQUEST['remove_coupon'] ) || isset( $_REQUEST['remove_item'] ) || isset( $_REQUEST['undo_item'] ) || isset( $_REQUEST['update_cart'] ) || isset( $_REQUEST['proceed'] ) ) ) {
			return;
		}

		wc_nocache_headers();

		$nonce_value = wc_get_var( $_REQUEST['woocommerce-cart-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine.

		if ( ! empty( $_POST['apply_coupon'] ) && ! empty( $_POST['coupon_code'] ) ) {
			WC()->cart->add_discount( wc_format_coupon_code( wp_unslash( $_POST['coupon_code'] ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

		} elseif ( isset( $_GET['remove_coupon'] ) ) {
			WC()->cart->remove_coupon( wc_format_coupon_code( urldecode( wp_unslash( $_GET['remove_coupon'] ) ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

		} elseif ( ! empty( $_GET['remove_item'] ) && wp_verify_nonce( $nonce_value, 'woocommerce-cart' ) ) {
			$cart_item_key = sanitize_text_field( wp_unslash( $_GET['remove_item'] ) );
			$cart_item     = WC()->cart->get_cart_item( $cart_item_key );

			if ( $cart_item ) {
				WC()->cart->remove_cart_item( $cart_item_key );

				$product = wc_get_product( $cart_item['product_id'] );

				/* translators: %s: Item name. */
				$item_removed_title = apply_filters( 'woocommerce_cart_item_removed_title', $product ? sprintf( _x( '&ldquo;%s&rdquo;', 'Item name in quotes', 'woocommerce' ), $product->get_name() ) : __( 'Item', 'woocommerce' ), $cart_item );

				// Don't show undo link if removed item is out of stock.
				if ( $product && $product->is_in_stock() && $product->has_enough_stock( $cart_item['quantity'] ) ) {
					/* Translators: %s Product title. */
					$removed_notice  = sprintf( __( '%s removed.', 'woocommerce' ), $item_removed_title );
					$removed_notice .= ' <a href="' . esc_url( wc_get_cart_undo_url( $cart_item_key ) ) . '" class="restore-item">' . __( 'Undo?', 'woocommerce' ) . '</a>';
				} else {
					/* Translators: %s Product title. */
					$removed_notice = sprintf( __( '%s removed.', 'woocommerce' ), $item_removed_title );
				}

				wc_add_notice( $removed_notice, apply_filters( 'woocommerce_cart_item_removed_notice_type', 'success' ) );
			}

			$referer = wp_get_referer() ? remove_query_arg( array( 'remove_item', 'add-to-cart', 'added-to-cart', 'order_again', '_wpnonce' ), add_query_arg( 'removed_item', '1', wp_get_referer() ) ) : wc_get_cart_url();
			wp_safe_redirect( $referer );
			exit;

		} elseif ( ! empty( $_GET['undo_item'] ) && isset( $_GET['_wpnonce'] ) && wp_verify_nonce( $nonce_value, 'woocommerce-cart' ) ) {

			// Undo Cart Item.
			$cart_item_key = sanitize_text_field( wp_unslash( $_GET['undo_item'] ) );

			WC()->cart->restore_cart_item( $cart_item_key );

			$referer = wp_get_referer() ? remove_query_arg( array( 'undo_item', '_wpnonce' ), wp_get_referer() ) : wc_get_cart_url();
			wp_safe_redirect( $referer );
			exit;

		}

		// Update Cart - checks apply_coupon too because they are in the same form.
		if ( ( ! empty( $_POST['apply_coupon'] ) || ! empty( $_POST['update_cart'] ) || ! empty( $_POST['proceed'] ) ) && wp_verify_nonce( $nonce_value, 'woocommerce-cart' ) ) {

			$cart_updated = false;
			$cart_totals  = isset( $_POST['cart'] ) ? wp_unslash( $_POST['cart'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

			if ( ! WC()->cart->is_empty() && is_array( $cart_totals ) ) {
				foreach ( WC()->cart->get_cart() as $cart_item_key => $values ) {

					$_product = $values['data'];

					// Skip product if no updated quantity was posted.
					if ( ! isset( $cart_totals[ $cart_item_key ] ) || ! isset( $cart_totals[ $cart_item_key ]['qty'] ) ) {
						continue;
					}

					// Sanitize.
					$quantity = apply_filters( 'woocommerce_stock_amount_cart_item', wc_stock_amount( preg_replace( '/[^0-9\.]/', '', $cart_totals[ $cart_item_key ]['qty'] ) ), $cart_item_key );

					if ( '' === $quantity || $quantity === $values['quantity'] ) {
						continue;
					}

					// Update cart validation.
					$passed_validation = apply_filters( 'woocommerce_update_cart_validation', true, $cart_item_key, $values, $quantity );

					// is_sold_individually.
					if ( $_product->is_sold_individually() && $quantity > 1 ) {
						/* Translators: %s Product title. */
						wc_add_notice( sprintf( __( 'You can only have 1 %s in your cart.', 'woocommerce' ), $_product->get_name() ), 'error' );
						$passed_validation = false;
					}

					if ( $passed_validation ) {
						WC()->cart->set_quantity( $cart_item_key, $quantity, false );
						$cart_updated = true;
					}
				}
			}

			// Trigger action - let 3rd parties update the cart if they need to and update the $cart_updated variable.
			$cart_updated = apply_filters( 'woocommerce_update_cart_action_cart_updated', $cart_updated );

			if ( $cart_updated ) {
				WC()->cart->calculate_totals();
			}

			if ( ! empty( $_POST['proceed'] ) ) {
				wp_safe_redirect( wc_get_checkout_url() );
				exit;
			} elseif ( $cart_updated ) {
				wc_add_notice( __( 'Cart updated.', 'woocommerce' ), apply_filters( 'woocommerce_cart_updated_notice_type', 'success' ) );
				$referer = remove_query_arg( array( 'remove_coupon', 'add-to-cart' ), ( wp_get_referer() ? wp_get_referer() : wc_get_cart_url() ) );
				wp_safe_redirect( $referer );
				exit;
			}
		}
	}

	/**
	 * Place a previous order again.
	 *
	 * @deprecated 3.5.0 Logic moved to cart session handling.
	 */
	public static function order_again() {
		wc_deprecated_function( 'WC_Form_Handler::order_again', '3.5', 'This method should not be called manually.' );
	}

	/**
	 * Cancel a pending order.
	 */
	public static function cancel_order() {
		if (
			isset( $_GET['cancel_order'] ) &&
			isset( $_GET['order'] ) &&
			isset( $_GET['order_id'] ) &&
			( isset( $_GET['_wpnonce'] ) && wp_verify_nonce( wp_unslash( $_GET['_wpnonce'] ), 'woocommerce-cancel_order' ) ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		) {
			wc_nocache_headers();

			$order_key        = wp_unslash( $_GET['order'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			$order_id         = absint( $_GET['order_id'] );
			$order            = wc_get_order( $order_id );
			$user_can_cancel  = current_user_can( 'cancel_order', $order_id );
			$order_can_cancel = $order->has_status( apply_filters( 'woocommerce_valid_order_statuses_for_cancel', array( 'pending', 'failed' ), $order ) );
			$redirect         = isset( $_GET['redirect'] ) ? wp_unslash( $_GET['redirect'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

			if ( $user_can_cancel && $order_can_cancel && $order->get_id() === $order_id && hash_equals( $order->get_order_key(), $order_key ) ) {

				// Cancel the order + restore stock.
				WC()->session->set( 'order_awaiting_payment', false );
				$order->update_status( 'cancelled', __( 'Order cancelled by customer.', 'woocommerce' ) );

				wc_add_notice( apply_filters( 'woocommerce_order_cancelled_notice', __( 'Your order was cancelled.', 'woocommerce' ) ), apply_filters( 'woocommerce_order_cancelled_notice_type', 'notice' ) );

				do_action( 'woocommerce_cancelled_order', $order->get_id() );

			} elseif ( $user_can_cancel && ! $order_can_cancel ) {
				wc_add_notice( __( 'Your order can no longer be cancelled. Please contact us if you need assistance.', 'woocommerce' ), 'error' );
			} else {
				wc_add_notice( __( 'Invalid order.', 'woocommerce' ), 'error' );
			}

			if ( $redirect ) {
				wp_safe_redirect( $redirect );
				exit;
			}
		}
	}

	/**
	 * Add to cart action.
	 *
	 * Checks for a valid request, does validation (via hooks) and then redirects if valid.
	 *
	 * @param bool $url (default: false) URL to redirect to.
	 */
	public static function add_to_cart_action( $url = false ) {
		if ( ! isset( $_REQUEST['add-to-cart'] ) || ! is_numeric( wp_unslash( $_REQUEST['add-to-cart'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			return;
		}

		wc_nocache_headers();

		$product_id        = apply_filters( 'woocommerce_add_to_cart_product_id', absint( wp_unslash( $_REQUEST['add-to-cart'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$was_added_to_cart = false;
		$adding_to_cart    = wc_get_product( $product_id );

		if ( ! $adding_to_cart ) {
			return;
		}

		$add_to_cart_handler = apply_filters( 'woocommerce_add_to_cart_handler', $adding_to_cart->get_type(), $adding_to_cart );

		if ( 'variable' === $add_to_cart_handler || 'variation' === $add_to_cart_handler ) {
			$was_added_to_cart = self::add_to_cart_handler_variable( $product_id );
		} elseif ( 'grouped' === $add_to_cart_handler ) {
			$was_added_to_cart = self::add_to_cart_handler_grouped( $product_id );
		} elseif ( has_action( 'woocommerce_add_to_cart_handler_' . $add_to_cart_handler ) ) {
			do_action( 'woocommerce_add_to_cart_handler_' . $add_to_cart_handler, $url ); // Custom handler.
		} else {
			$was_added_to_cart = self::add_to_cart_handler_simple( $product_id );
		}

		// If we added the product to the cart we can now optionally do a redirect.
		if ( $was_added_to_cart && 0 === wc_notice_count( 'error' ) ) {
			$url = apply_filters( 'woocommerce_add_to_cart_redirect', $url, $adding_to_cart );

			if ( $url ) {
				wp_safe_redirect( $url );
				exit;
			} elseif ( 'yes' === get_option( 'woocommerce_cart_redirect_after_add' ) ) {
				wp_safe_redirect( wc_get_cart_url() );
				exit;
			}
		}
	}

	/**
	 * Handle adding simple products to the cart.
	 *
	 * @since 2.4.6 Split from add_to_cart_action.
	 * @param int $product_id Product ID to add to the cart.
	 * @return bool success or not
	 */
	private static function add_to_cart_handler_simple( $product_id ) {
		$quantity          = empty( $_REQUEST['quantity'] ) ? 1 : wc_stock_amount( wp_unslash( $_REQUEST['quantity'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity );

		if ( $passed_validation && false !== WC()->cart->add_to_cart( $product_id, $quantity ) ) {
			wc_add_to_cart_message( array( $product_id => $quantity ), true );
			return true;
		}
		return false;
	}

	/**
	 * Handle adding grouped products to the cart.
	 *
	 * @since 2.4.6 Split from add_to_cart_action.
	 * @param int $product_id Product ID to add to the cart.
	 * @return bool success or not
	 */
	private static function add_to_cart_handler_grouped( $product_id ) {
		$was_added_to_cart = false;
		$added_to_cart     = array();
		$items             = isset( $_REQUEST['quantity'] ) && is_array( $_REQUEST['quantity'] ) ? wp_unslash( $_REQUEST['quantity'] ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

		if ( ! empty( $items ) ) {
			$quantity_set = false;

			foreach ( $items as $item => $quantity ) {
				$quantity = wc_stock_amount( $quantity );
				if ( $quantity <= 0 ) {
					continue;
				}
				$quantity_set = true;

				// Add to cart validation.
				$passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $item, $quantity );

				// Suppress total recalculation until finished.
				remove_action( 'woocommerce_add_to_cart', array( WC()->cart, 'calculate_totals' ), 20, 0 );

				if ( $passed_validation && false !== WC()->cart->add_to_cart( $item, $quantity ) ) {
					$was_added_to_cart      = true;
					$added_to_cart[ $item ] = $quantity;
				}

				add_action( 'woocommerce_add_to_cart', array( WC()->cart, 'calculate_totals' ), 20, 0 );
			}

			if ( ! $was_added_to_cart && ! $quantity_set ) {
				wc_add_notice( __( 'Please choose the quantity of items you wish to add to your cart&hellip;', 'woocommerce' ), 'error' );
			} elseif ( $was_added_to_cart ) {
				wc_add_to_cart_message( $added_to_cart );
				WC()->cart->calculate_totals();
				return true;
			}
		} elseif ( $product_id ) {
			/* Link on product archives */
			wc_add_notice( __( 'Please choose a product to add to your cart&hellip;', 'woocommerce' ), 'error' );
		}
		return false;
	}

	/**
	 * Handle adding variable products to the cart.
	 *
	 * @since 2.4.6 Split from add_to_cart_action.
	 * @throws Exception If add to cart fails.
	 * @param int $product_id Product ID to add to the cart.
	 * @return bool success or not
	 */
	private static function add_to_cart_handler_variable( $product_id ) {
		$variation_id = empty( $_REQUEST['variation_id'] ) ? '' : absint( wp_unslash( $_REQUEST['variation_id'] ) );  // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$quantity     = empty( $_REQUEST['quantity'] ) ? 1 : wc_stock_amount( wp_unslash( $_REQUEST['quantity'] ) );  // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$variations   = array();

		$product      = wc_get_product( $product_id );

		foreach ( $_REQUEST as $key => $value ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			if ( 'attribute_' !== substr( $key, 0, 10 ) ) {
				continue;
			}

			$variations[ sanitize_title( wp_unslash( $key ) ) ] = wp_unslash( $value );
		}

		$passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity, $variation_id, $variations );

		if ( ! $passed_validation ) {
			return false;
		}

		// Prevent parent variable product from being added to cart.
		if ( empty( $variation_id ) && $product && $product->is_type( 'variable' ) ) {
			/* translators: 1: product link, 2: product name */
			wc_add_notice( sprintf( __( 'Please choose product options by visiting <a href="%1$s" title="%2$s">%2$s</a>.', 'woocommerce' ), esc_url( get_permalink( $product_id ) ), esc_html( $product->get_name() ) ), 'error' );

			return false;
		}

		if ( false !== WC()->cart->add_to_cart( $product_id, $quantity, $variation_id, $variations ) ) {
			wc_add_to_cart_message( array( $product_id => $quantity ), true );
			return true;
		}

		return false;
	}

	/**
	 * Process the login form.
	 *
	 * @throws Exception On login error.
	 */
	public static function process_login() {
		// The global form-login.php template used `_wpnonce` in template versions < 3.3.0.
		$nonce_value = wc_get_var( $_REQUEST['woocommerce-login-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine.

		if ( isset( $_POST['login'], $_POST['username'], $_POST['password'] ) && wp_verify_nonce( $nonce_value, 'woocommerce-login' ) ) {

			try {
				$creds = array(
					'user_login'    => trim( wp_unslash( $_POST['username'] ) ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
					'user_password' => $_POST['password'], // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
					'remember'      => isset( $_POST['rememberme'] ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
				);

				$validation_error = new WP_Error();
				$validation_error = apply_filters( 'woocommerce_process_login_errors', $validation_error, $creds['user_login'], $creds['user_password'] );

				if ( $validation_error->get_error_code() ) {
					throw new Exception( '<strong>' . __( 'Error:', 'woocommerce' ) . '</strong> ' . $validation_error->get_error_message() );
				}

				if ( empty( $creds['user_login'] ) ) {
					throw new Exception( '<strong>' . __( 'Error:', 'woocommerce' ) . '</strong> ' . __( 'Username is required.', 'woocommerce' ) );
				}

				// On multisite, ensure user exists on current site, if not add them before allowing login.
				if ( is_multisite() ) {
					$user_data = get_user_by( is_email( $creds['user_login'] ) ? 'email' : 'login', $creds['user_login'] );

					if ( $user_data && ! is_user_member_of_blog( $user_data->ID, get_current_blog_id() ) ) {
						add_user_to_blog( get_current_blog_id(), $user_data->ID, 'customer' );
					}
				}

				// Perform the login.
				$user = wp_signon( apply_filters( 'woocommerce_login_credentials', $creds ), is_ssl() );

				if ( is_wp_error( $user ) ) {
					throw new Exception( $user->get_error_message() );
				} else {

					if ( ! empty( $_POST['redirect'] ) ) {
						$redirect = wp_unslash( $_POST['redirect'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
					} elseif ( wc_get_raw_referer() ) {
						$redirect = wc_get_raw_referer();
					} else {
						$redirect = wc_get_page_permalink( 'myaccount' );
					}

					wp_redirect( wp_validate_redirect( apply_filters( 'woocommerce_login_redirect', remove_query_arg( 'wc_error', $redirect ), $user ), wc_get_page_permalink( 'myaccount' ) ) ); // phpcs:ignore
					exit;
				}
			} catch ( Exception $e ) {
				wc_add_notice( apply_filters( 'login_errors', $e->getMessage() ), 'error' );
				do_action( 'woocommerce_login_failed' );
			}
		}
	}

	/**
	 * Handle lost password form.
	 */
	public static function process_lost_password() {
		if ( isset( $_POST['wc_reset_password'], $_POST['user_login'] ) ) {
			$nonce_value = wc_get_var( $_REQUEST['woocommerce-lost-password-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine.

			if ( ! wp_verify_nonce( $nonce_value, 'lost_password' ) ) {
				return;
			}

			$success = WC_Shortcode_My_Account::retrieve_password();

			// If successful, redirect to my account with query arg set.
			if ( $success ) {
				wp_safe_redirect( add_query_arg( 'reset-link-sent', 'true', wc_get_account_endpoint_url( 'lost-password' ) ) );
				exit;
			}
		}
	}

	/**
	 * Handle reset password form.
	 */
	public static function process_reset_password() {
		$nonce_value = wc_get_var( $_REQUEST['woocommerce-reset-password-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine.

		if ( ! wp_verify_nonce( $nonce_value, 'reset_password' ) ) {
			return;
		}

		$posted_fields = array( 'wc_reset_password', 'password_1', 'password_2', 'reset_key', 'reset_login' );

		foreach ( $posted_fields as $field ) {
			if ( ! isset( $_POST[ $field ] ) ) {
				return;
			}

			if ( in_array( $field, array( 'password_1', 'password_2' ), true ) ) {
				// Don't unslash password fields
				// @see https://github.com/woocommerce/woocommerce/issues/23922.
				$posted_fields[ $field ] = $_POST[ $field ]; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
			} else {
				$posted_fields[ $field ] = wp_unslash( $_POST[ $field ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			}
		}

		$user = WC_Shortcode_My_Account::check_password_reset_key( $posted_fields['reset_key'], $posted_fields['reset_login'] );

		if ( $user instanceof WP_User ) {
			if ( empty( $posted_fields['password_1'] ) ) {
				wc_add_notice( __( 'Please enter your password.', 'woocommerce' ), 'error' );
			}

			if ( $posted_fields['password_1'] !== $posted_fields['password_2'] ) {
				wc_add_notice( __( 'Passwords do not match.', 'woocommerce' ), 'error' );
			}

			$errors = new WP_Error();

			do_action( 'validate_password_reset', $errors, $user );

			wc_add_wp_error_notices( $errors );

			if ( 0 === wc_notice_count( 'error' ) ) {
				WC_Shortcode_My_Account::reset_password( $user, $posted_fields['password_1'] );

				do_action( 'woocommerce_customer_reset_password', $user );

				wp_safe_redirect( add_query_arg( 'password-reset', 'true', wc_get_page_permalink( 'myaccount' ) ) );
				exit;
			}
		}
	}

	/**
	 * Process the registration form.
	 *
	 * @throws Exception On registration error.
	 */
	public static function process_registration() {
		$nonce_value = isset( $_POST['_wpnonce'] ) ? wp_unslash( $_POST['_wpnonce'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$nonce_value = isset( $_POST['woocommerce-register-nonce'] ) ? wp_unslash( $_POST['woocommerce-register-nonce'] ) : $nonce_value; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

		if ( isset( $_POST['register'], $_POST['email'] ) && wp_verify_nonce( $nonce_value, 'woocommerce-register' ) ) {
			$username = 'no' === get_option( 'woocommerce_registration_generate_username' ) && isset( $_POST['username'] ) ? wp_unslash( $_POST['username'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			$password = 'no' === get_option( 'woocommerce_registration_generate_password' ) && isset( $_POST['password'] ) ? $_POST['password'] : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
			$email    = wp_unslash( $_POST['email'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

			try {
				$validation_error  = new WP_Error();
				$validation_error  = apply_filters( 'woocommerce_process_registration_errors', $validation_error, $username, $password, $email );
				$validation_errors = $validation_error->get_error_messages();

				if ( 1 === count( $validation_errors ) ) {
					throw new Exception( $validation_error->get_error_message() );
				} elseif ( $validation_errors ) {
					foreach ( $validation_errors as $message ) {
						wc_add_notice( '<strong>' . __( 'Error:', 'woocommerce' ) . '</strong> ' . $message, 'error' );
					}
					throw new Exception();
				}

				$new_customer = wc_create_new_customer( sanitize_email( $email ), wc_clean( $username ), $password );

				if ( is_wp_error( $new_customer ) ) {
					throw new Exception( $new_customer->get_error_message() );
				}

				if ( 'yes' === get_option( 'woocommerce_registration_generate_password' ) ) {
					wc_add_notice( __( 'Your account was created successfully and a password has been sent to your email address.', 'woocommerce' ) );
				} else {
					wc_add_notice( __( 'Your account was created successfully. Your login details have been sent to your email address.', 'woocommerce' ) );
				}

				// Only redirect after a forced login - otherwise output a success notice.
				if ( apply_filters( 'woocommerce_registration_auth_new_customer', true, $new_customer ) ) {
					wc_set_customer_auth_cookie( $new_customer );

					if ( ! empty( $_POST['redirect'] ) ) {
						$redirect = wp_sanitize_redirect( wp_unslash( $_POST['redirect'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
					} elseif ( wc_get_raw_referer() ) {
						$redirect = wc_get_raw_referer();
					} else {
						$redirect = wc_get_page_permalink( 'myaccount' );
					}

					wp_redirect( wp_validate_redirect( apply_filters( 'woocommerce_registration_redirect', $redirect ), wc_get_page_permalink( 'myaccount' ) ) ); //phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect
					exit;
				}
			} catch ( Exception $e ) {
				if ( $e->getMessage() ) {
					wc_add_notice( '<strong>' . __( 'Error:', 'woocommerce' ) . '</strong> ' . $e->getMessage(), 'error' );
				}
			}
		}
	}
}

WC_Form_Handler::init();
class-wc-comments.php000064400000035771151336044770010643 0ustar00<?php
/**
 * Comments
 *
 * Handle comments (reviews and order notes).
 *
 * @package WooCommerce\Classes\Products
 * @version 2.3.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Comments class.
 */
class WC_Comments {

	/**
	 * Hook in methods.
	 */
	public static function init() {
		// Rating posts.
		add_filter( 'comments_open', array( __CLASS__, 'comments_open' ), 10, 2 );
		add_filter( 'preprocess_comment', array( __CLASS__, 'check_comment_rating' ), 0 );
		add_action( 'comment_post', array( __CLASS__, 'add_comment_rating' ), 1 );
		add_action( 'comment_moderation_recipients', array( __CLASS__, 'comment_moderation_recipients' ), 10, 2 );

		// Clear transients.
		add_action( 'wp_update_comment_count', array( __CLASS__, 'clear_transients' ) );

		// Secure order notes.
		add_filter( 'comments_clauses', array( __CLASS__, 'exclude_order_comments' ), 10, 1 );
		add_filter( 'comment_feed_where', array( __CLASS__, 'exclude_order_comments_from_feed_where' ) );

		// Secure webhook comments.
		add_filter( 'comments_clauses', array( __CLASS__, 'exclude_webhook_comments' ), 10, 1 );
		add_filter( 'comment_feed_where', array( __CLASS__, 'exclude_webhook_comments_from_feed_where' ) );

		// Count comments.
		add_filter( 'wp_count_comments', array( __CLASS__, 'wp_count_comments' ), 10, 2 );

		// Delete comments count cache whenever there is a new comment or a comment status changes.
		add_action( 'wp_insert_comment', array( __CLASS__, 'delete_comments_count_cache' ) );
		add_action( 'wp_set_comment_status', array( __CLASS__, 'delete_comments_count_cache' ) );

		// Support avatars for `review` comment type.
		add_filter( 'get_avatar_comment_types', array( __CLASS__, 'add_avatar_for_review_comment_type' ) );

		// Review of verified purchase.
		add_action( 'comment_post', array( __CLASS__, 'add_comment_purchase_verification' ) );

		// Set comment type.
		add_action( 'preprocess_comment', array( __CLASS__, 'update_comment_type' ), 1 );

		// Validate product reviews if requires verified owners.
		add_action( 'pre_comment_on_post', array( __CLASS__, 'validate_product_review_verified_owners' ) );
	}

	/**
	 * See if comments are open.
	 *
	 * @since  3.1.0
	 * @param  bool $open    Whether the current post is open for comments.
	 * @param  int  $post_id Post ID.
	 * @return bool
	 */
	public static function comments_open( $open, $post_id ) {
		if ( 'product' === get_post_type( $post_id ) && ! post_type_supports( 'product', 'comments' ) ) {
			$open = false;
		}
		return $open;
	}

	/**
	 * Exclude order comments from queries and RSS.
	 *
	 * This code should exclude shop_order comments from queries. Some queries (like the recent comments widget on the dashboard) are hardcoded.
	 * and are not filtered, however, the code current_user_can( 'read_post', $comment->comment_post_ID ) should keep them safe since only admin and.
	 * shop managers can view orders anyway.
	 *
	 * The frontend view order pages get around this filter by using remove_filter('comments_clauses', array( 'WC_Comments' ,'exclude_order_comments'), 10, 1 );
	 *
	 * @param  array $clauses A compacted array of comment query clauses.
	 * @return array
	 */
	public static function exclude_order_comments( $clauses ) {
		$clauses['where'] .= ( $clauses['where'] ? ' AND ' : '' ) . " comment_type != 'order_note' ";
		return $clauses;
	}

	/**
	 * Exclude order comments from feed.
	 *
	 * @deprecated 3.1
	 * @param mixed $join Deprecated.
	 */
	public static function exclude_order_comments_from_feed_join( $join ) {
		wc_deprecated_function( 'WC_Comments::exclude_order_comments_from_feed_join', '3.1' );
	}

	/**
	 * Exclude order comments from queries and RSS.
	 *
	 * @param  string $where The WHERE clause of the query.
	 * @return string
	 */
	public static function exclude_order_comments_from_feed_where( $where ) {
		return $where . ( $where ? ' AND ' : '' ) . " comment_type != 'order_note' ";
	}

	/**
	 * Exclude webhook comments from queries and RSS.
	 *
	 * @since  2.2
	 * @param  array $clauses A compacted array of comment query clauses.
	 * @return array
	 */
	public static function exclude_webhook_comments( $clauses ) {
		$clauses['where'] .= ( $clauses['where'] ? ' AND ' : '' ) . " comment_type != 'webhook_delivery' ";
		return $clauses;
	}

	/**
	 * Exclude webhooks comments from feed.
	 *
	 * @deprecated 3.1
	 * @param mixed $join Deprecated.
	 */
	public static function exclude_webhook_comments_from_feed_join( $join ) {
		wc_deprecated_function( 'WC_Comments::exclude_webhook_comments_from_feed_join', '3.1' );
	}

	/**
	 * Exclude webhook comments from queries and RSS.
	 *
	 * @since  2.1
	 * @param  string $where The WHERE clause of the query.
	 * @return string
	 */
	public static function exclude_webhook_comments_from_feed_where( $where ) {
		return $where . ( $where ? ' AND ' : '' ) . " comment_type != 'webhook_delivery' ";
	}

	/**
	 * Validate the comment ratings.
	 *
	 * @param  array $comment_data Comment data.
	 * @return array
	 */
	public static function check_comment_rating( $comment_data ) {
		// If posting a comment (not trackback etc) and not logged in.
		if ( ! is_admin() && isset( $_POST['comment_post_ID'], $_POST['rating'], $comment_data['comment_type'] ) && 'product' === get_post_type( absint( $_POST['comment_post_ID'] ) ) && empty( $_POST['rating'] ) && self::is_default_comment_type( $comment_data['comment_type'] ) && wc_review_ratings_enabled() && wc_review_ratings_required() ) { // WPCS: input var ok, CSRF ok.
			wp_die( esc_html__( 'Please rate the product.', 'woocommerce' ) );
			exit;
		}
		return $comment_data;
	}

	/**
	 * Rating field for comments.
	 *
	 * @param int $comment_id Comment ID.
	 */
	public static function add_comment_rating( $comment_id ) {
		if ( isset( $_POST['rating'], $_POST['comment_post_ID'] ) && 'product' === get_post_type( absint( $_POST['comment_post_ID'] ) ) ) { // WPCS: input var ok, CSRF ok.
			if ( ! $_POST['rating'] || $_POST['rating'] > 5 || $_POST['rating'] < 0 ) { // WPCS: input var ok, CSRF ok, sanitization ok.
				return;
			}
			add_comment_meta( $comment_id, 'rating', intval( $_POST['rating'] ), true ); // WPCS: input var ok, CSRF ok.

			$post_id = isset( $_POST['comment_post_ID'] ) ? absint( $_POST['comment_post_ID'] ) : 0; // WPCS: input var ok, CSRF ok.
			if ( $post_id ) {
				self::clear_transients( $post_id );
			}
		}
	}

	/**
	 * Modify recipient of review email.
	 *
	 * @param array $emails     Emails.
	 * @param int   $comment_id Comment ID.
	 * @return array
	 */
	public static function comment_moderation_recipients( $emails, $comment_id ) {
		$comment = get_comment( $comment_id );

		if ( $comment && 'product' === get_post_type( $comment->comment_post_ID ) ) {
			$emails = array( get_option( 'admin_email' ) );
		}

		return $emails;
	}

	/**
	 * Ensure product average rating and review count is kept up to date.
	 *
	 * @param int $post_id Post ID.
	 */
	public static function clear_transients( $post_id ) {
		if ( 'product' === get_post_type( $post_id ) ) {
			$product = wc_get_product( $post_id );
			$product->set_rating_counts( self::get_rating_counts_for_product( $product ) );
			$product->set_average_rating( self::get_average_rating_for_product( $product ) );
			$product->set_review_count( self::get_review_count_for_product( $product ) );
			$product->save();
		}
	}

	/**
	 * Delete comments count cache whenever there is
	 * new comment or the status of a comment changes. Cache
	 * will be regenerated next time WC_Comments::wp_count_comments()
	 * is called.
	 */
	public static function delete_comments_count_cache() {
		delete_transient( 'wc_count_comments' );
	}

	/**
	 * Remove order notes and webhook delivery logs from wp_count_comments().
	 *
	 * @since  2.2
	 * @param  object $stats   Comment stats.
	 * @param  int    $post_id Post ID.
	 * @return object
	 */
	public static function wp_count_comments( $stats, $post_id ) {
		global $wpdb;

		if ( 0 === $post_id ) {
			$stats = get_transient( 'wc_count_comments' );

			if ( ! $stats ) {
				$stats = array(
					'total_comments' => 0,
					'all'            => 0,
				);

				$count = $wpdb->get_results(
					"
					SELECT comment_approved, COUNT(*) AS num_comments
					FROM {$wpdb->comments}
					WHERE comment_type NOT IN ('action_log', 'order_note', 'webhook_delivery')
					GROUP BY comment_approved
					",
					ARRAY_A
				);

				$approved = array(
					'0'            => 'moderated',
					'1'            => 'approved',
					'spam'         => 'spam',
					'trash'        => 'trash',
					'post-trashed' => 'post-trashed',
				);

				foreach ( (array) $count as $row ) {
					// Don't count post-trashed toward totals.
					if ( ! in_array( $row['comment_approved'], array( 'post-trashed', 'trash', 'spam' ), true ) ) {
						$stats['all']            += $row['num_comments'];
						$stats['total_comments'] += $row['num_comments'];
					} elseif ( ! in_array( $row['comment_approved'], array( 'post-trashed', 'trash' ), true ) ) {
						$stats['total_comments'] += $row['num_comments'];
					}
					if ( isset( $approved[ $row['comment_approved'] ] ) ) {
						$stats[ $approved[ $row['comment_approved'] ] ] = $row['num_comments'];
					}
				}

				foreach ( $approved as $key ) {
					if ( empty( $stats[ $key ] ) ) {
						$stats[ $key ] = 0;
					}
				}

				$stats = (object) $stats;
				set_transient( 'wc_count_comments', $stats );
			}
		}

		return $stats;
	}

	/**
	 * Make sure WP displays avatars for comments with the `review` type.
	 *
	 * @since  2.3
	 * @param  array $comment_types Comment types.
	 * @return array
	 */
	public static function add_avatar_for_review_comment_type( $comment_types ) {
		return array_merge( $comment_types, array( 'review' ) );
	}

	/**
	 * Determine if a review is from a verified owner at submission.
	 *
	 * @param int $comment_id Comment ID.
	 * @return bool
	 */
	public static function add_comment_purchase_verification( $comment_id ) {
		$comment  = get_comment( $comment_id );
		$verified = false;
		if ( 'product' === get_post_type( $comment->comment_post_ID ) ) {
			$verified = wc_customer_bought_product( $comment->comment_author_email, $comment->user_id, $comment->comment_post_ID );
			add_comment_meta( $comment_id, 'verified', (int) $verified, true );
		}
		return $verified;
	}

	/**
	 * Get product rating for a product. Please note this is not cached.
	 *
	 * @since 3.0.0
	 * @param WC_Product $product Product instance.
	 * @return float
	 */
	public static function get_average_rating_for_product( &$product ) {
		global $wpdb;

		$count = $product->get_rating_count();

		if ( $count ) {
			$ratings = $wpdb->get_var(
				$wpdb->prepare(
					"
				SELECT SUM(meta_value) FROM $wpdb->commentmeta
				LEFT JOIN $wpdb->comments ON $wpdb->commentmeta.comment_id = $wpdb->comments.comment_ID
				WHERE meta_key = 'rating'
				AND comment_post_ID = %d
				AND comment_approved = '1'
				AND meta_value > 0
					",
					$product->get_id()
				)
			);
			$average = number_format( $ratings / $count, 2, '.', '' );
		} else {
			$average = 0;
		}

		return $average;
	}

	/**
	 * Utility function for getting review counts for multiple products in one query. This is not cached.
	 *
	 * @since 5.0.0
	 *
	 * @param array $product_ids Array of product IDs.
	 *
	 * @return array
	 */
	public static function get_review_counts_for_product_ids( $product_ids ) {
		global $wpdb;

		if ( empty( $product_ids ) ) {
			return array();
		}

		$product_id_string_placeholder = substr( str_repeat( ',%s', count( $product_ids ) ), 1 );

		$review_counts = $wpdb->get_results(
			// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Ignored for allowing interpolation in IN query.
			$wpdb->prepare(
				"
					SELECT comment_post_ID as product_id, COUNT( comment_post_ID ) as review_count
					FROM $wpdb->comments
					WHERE
						comment_parent = 0
						AND comment_post_ID IN ( $product_id_string_placeholder )
						AND comment_approved = '1'
						AND comment_type in ( 'review', '', 'comment' )
					GROUP BY product_id
				",
				$product_ids
			),
			// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared.
			ARRAY_A
		);

		// Convert to key value pairs.
		$counts = array_replace( array_fill_keys( $product_ids, 0 ), array_column( $review_counts, 'review_count', 'product_id' ) );

		return $counts;
	}

	/**
	 * Get product review count for a product (not replies). Please note this is not cached.
	 *
	 * @since 3.0.0
	 * @param WC_Product $product Product instance.
	 * @return int
	 */
	public static function get_review_count_for_product( &$product ) {
		$counts = self::get_review_counts_for_product_ids( array( $product->get_id() ) );

		return $counts[ $product->get_id() ];
	}

	/**
	 * Get product rating count for a product. Please note this is not cached.
	 *
	 * @since 3.0.0
	 * @param WC_Product $product Product instance.
	 * @return int[]
	 */
	public static function get_rating_counts_for_product( &$product ) {
		global $wpdb;

		$counts     = array();
		$raw_counts = $wpdb->get_results(
			$wpdb->prepare(
				"
			SELECT meta_value, COUNT( * ) as meta_value_count FROM $wpdb->commentmeta
			LEFT JOIN $wpdb->comments ON $wpdb->commentmeta.comment_id = $wpdb->comments.comment_ID
			WHERE meta_key = 'rating'
			AND comment_post_ID = %d
			AND comment_approved = '1'
			AND meta_value > 0
			GROUP BY meta_value
				",
				$product->get_id()
			)
		);

		foreach ( $raw_counts as $count ) {
			$counts[ $count->meta_value ] = absint( $count->meta_value_count ); // WPCS: slow query ok.
		}

		return $counts;
	}

	/**
	 * Update comment type of product reviews.
	 *
	 * @since 3.5.0
	 * @param array $comment_data Comment data.
	 * @return array
	 */
	public static function update_comment_type( $comment_data ) {
		if ( ! is_admin() && isset( $_POST['comment_post_ID'], $comment_data['comment_type'] ) && self::is_default_comment_type( $comment_data['comment_type'] ) && 'product' === get_post_type( absint( $_POST['comment_post_ID'] ) ) ) { // WPCS: input var ok, CSRF ok.
			$comment_data['comment_type'] = 'review';
		}

		return $comment_data;
	}

	/**
	 * Validate product reviews if requires a verified owner.
	 *
	 * @param int $comment_post_id Post ID.
	 */
	public static function validate_product_review_verified_owners( $comment_post_id ) {
		// Only validate if option is enabled.
		if ( 'yes' !== get_option( 'woocommerce_review_rating_verification_required' ) ) {
			return;
		}

		// Validate only products.
		if ( 'product' !== get_post_type( $comment_post_id ) ) {
			return;
		}

		// Skip if is a verified owner.
		if ( wc_customer_bought_product( '', get_current_user_id(), $comment_post_id ) ) {
			return;
		}

		wp_die(
			esc_html__( 'Only logged in customers who have purchased this product may leave a review.', 'woocommerce' ),
			esc_html__( 'Reviews can only be left by "verified owners"', 'woocommerce' ),
			array(
				'code' => 403,
			)
		);
	}

	/**
	 * Determines if a comment is of the default type.
	 *
	 * Prior to WordPress 5.5, '' was the default comment type.
	 * As of 5.5, the default type is 'comment'.
	 *
	 * @since 4.3.0
	 * @param string $comment_type Comment type.
	 * @return bool
	 */
	private static function is_default_comment_type( $comment_type ) {
		return ( '' === $comment_type || 'comment' === $comment_type );
	}
}

WC_Comments::init();
class-wc-post-types.php000064400000070144151336044770011136 0ustar00<?php
/**
 * Post Types
 *
 * Registers post types and taxonomies.
 *
 * @package WooCommerce\Classes\Products
 * @version 2.5.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Post types Class.
 */
class WC_Post_Types {

	/**
	 * Hook in methods.
	 */
	public static function init() {
		add_action( 'init', array( __CLASS__, 'register_taxonomies' ), 5 );
		add_action( 'init', array( __CLASS__, 'register_post_types' ), 5 );
		add_action( 'init', array( __CLASS__, 'register_post_status' ), 9 );
		add_action( 'init', array( __CLASS__, 'support_jetpack_omnisearch' ) );
		add_filter( 'term_updated_messages', array( __CLASS__, 'updated_term_messages' ) );
		add_filter( 'rest_api_allowed_post_types', array( __CLASS__, 'rest_api_allowed_post_types' ) );
		add_action( 'woocommerce_after_register_post_type', array( __CLASS__, 'maybe_flush_rewrite_rules' ) );
		add_action( 'woocommerce_flush_rewrite_rules', array( __CLASS__, 'flush_rewrite_rules' ) );
		add_filter( 'gutenberg_can_edit_post_type', array( __CLASS__, 'gutenberg_can_edit_post_type' ), 10, 2 );
		add_filter( 'use_block_editor_for_post_type', array( __CLASS__, 'gutenberg_can_edit_post_type' ), 10, 2 );
	}

	/**
	 * Register core taxonomies.
	 */
	public static function register_taxonomies() {

		if ( ! is_blog_installed() ) {
			return;
		}

		if ( taxonomy_exists( 'product_type' ) ) {
			return;
		}

		do_action( 'woocommerce_register_taxonomy' );

		$permalinks = wc_get_permalink_structure();

		register_taxonomy(
			'product_type',
			apply_filters( 'woocommerce_taxonomy_objects_product_type', array( 'product' ) ),
			apply_filters(
				'woocommerce_taxonomy_args_product_type',
				array(
					'hierarchical'      => false,
					'show_ui'           => false,
					'show_in_nav_menus' => false,
					'query_var'         => is_admin(),
					'rewrite'           => false,
					'public'            => false,
					'label'             => _x( 'Product type', 'Taxonomy name', 'woocommerce' ),
				)
			)
		);

		register_taxonomy(
			'product_visibility',
			apply_filters( 'woocommerce_taxonomy_objects_product_visibility', array( 'product', 'product_variation' ) ),
			apply_filters(
				'woocommerce_taxonomy_args_product_visibility',
				array(
					'hierarchical'      => false,
					'show_ui'           => false,
					'show_in_nav_menus' => false,
					'query_var'         => is_admin(),
					'rewrite'           => false,
					'public'            => false,
					'label'             => _x( 'Product visibility', 'Taxonomy name', 'woocommerce' ),
				)
			)
		);

		register_taxonomy(
			'product_cat',
			apply_filters( 'woocommerce_taxonomy_objects_product_cat', array( 'product' ) ),
			apply_filters(
				'woocommerce_taxonomy_args_product_cat',
				array(
					'hierarchical'          => true,
					'update_count_callback' => '_wc_term_recount',
					'label'                 => __( 'Categories', 'woocommerce' ),
					'labels'                => array(
						'name'                  => __( 'Product categories', 'woocommerce' ),
						'singular_name'         => __( 'Category', 'woocommerce' ),
						'menu_name'             => _x( 'Categories', 'Admin menu name', 'woocommerce' ),
						'search_items'          => __( 'Search categories', 'woocommerce' ),
						'all_items'             => __( 'All categories', 'woocommerce' ),
						'parent_item'           => __( 'Parent category', 'woocommerce' ),
						'parent_item_colon'     => __( 'Parent category:', 'woocommerce' ),
						'edit_item'             => __( 'Edit category', 'woocommerce' ),
						'update_item'           => __( 'Update category', 'woocommerce' ),
						'add_new_item'          => __( 'Add new category', 'woocommerce' ),
						'new_item_name'         => __( 'New category name', 'woocommerce' ),
						'not_found'             => __( 'No categories found', 'woocommerce' ),
						'item_link'             => __( 'Product Category Link', 'woocommerce' ),
						'item_link_description' => __( 'A link to a product category.', 'woocommerce' ),
					),
					'show_in_rest'          => true,
					'show_ui'               => true,
					'query_var'             => true,
					'capabilities'          => array(
						'manage_terms' => 'manage_product_terms',
						'edit_terms'   => 'edit_product_terms',
						'delete_terms' => 'delete_product_terms',
						'assign_terms' => 'assign_product_terms',
					),
					'rewrite'               => array(
						'slug'         => $permalinks['category_rewrite_slug'],
						'with_front'   => false,
						'hierarchical' => true,
					),
				)
			)
		);

		register_taxonomy(
			'product_tag',
			apply_filters( 'woocommerce_taxonomy_objects_product_tag', array( 'product' ) ),
			apply_filters(
				'woocommerce_taxonomy_args_product_tag',
				array(
					'hierarchical'          => false,
					'update_count_callback' => '_wc_term_recount',
					'label'                 => __( 'Product tags', 'woocommerce' ),
					'labels'                => array(
						'name'                       => __( 'Product tags', 'woocommerce' ),
						'singular_name'              => __( 'Tag', 'woocommerce' ),
						'menu_name'                  => _x( 'Tags', 'Admin menu name', 'woocommerce' ),
						'search_items'               => __( 'Search tags', 'woocommerce' ),
						'all_items'                  => __( 'All tags', 'woocommerce' ),
						'edit_item'                  => __( 'Edit tag', 'woocommerce' ),
						'update_item'                => __( 'Update tag', 'woocommerce' ),
						'add_new_item'               => __( 'Add new tag', 'woocommerce' ),
						'new_item_name'              => __( 'New tag name', 'woocommerce' ),
						'popular_items'              => __( 'Popular tags', 'woocommerce' ),
						'separate_items_with_commas' => __( 'Separate tags with commas', 'woocommerce' ),
						'add_or_remove_items'        => __( 'Add or remove tags', 'woocommerce' ),
						'choose_from_most_used'      => __( 'Choose from the most used tags', 'woocommerce' ),
						'not_found'                  => __( 'No tags found', 'woocommerce' ),
						'item_link'                  => __( 'Product Tag Link', 'woocommerce' ),
						'item_link_description'      => __( 'A link to a product tag.', 'woocommerce' ),
					),
					'show_in_rest'          => true,
					'show_ui'               => true,
					'query_var'             => true,
					'capabilities'          => array(
						'manage_terms' => 'manage_product_terms',
						'edit_terms'   => 'edit_product_terms',
						'delete_terms' => 'delete_product_terms',
						'assign_terms' => 'assign_product_terms',
					),
					'rewrite'               => array(
						'slug'       => $permalinks['tag_rewrite_slug'],
						'with_front' => false,
					),
				)
			)
		);

		register_taxonomy(
			'product_shipping_class',
			apply_filters( 'woocommerce_taxonomy_objects_product_shipping_class', array( 'product', 'product_variation' ) ),
			apply_filters(
				'woocommerce_taxonomy_args_product_shipping_class',
				array(
					'hierarchical'          => false,
					'update_count_callback' => '_update_post_term_count',
					'label'                 => __( 'Shipping classes', 'woocommerce' ),
					'labels'                => array(
						'name'              => __( 'Product shipping classes', 'woocommerce' ),
						'singular_name'     => __( 'Shipping class', 'woocommerce' ),
						'menu_name'         => _x( 'Shipping classes', 'Admin menu name', 'woocommerce' ),
						'search_items'      => __( 'Search shipping classes', 'woocommerce' ),
						'all_items'         => __( 'All shipping classes', 'woocommerce' ),
						'parent_item'       => __( 'Parent shipping class', 'woocommerce' ),
						'parent_item_colon' => __( 'Parent shipping class:', 'woocommerce' ),
						'edit_item'         => __( 'Edit shipping class', 'woocommerce' ),
						'update_item'       => __( 'Update shipping class', 'woocommerce' ),
						'add_new_item'      => __( 'Add new shipping class', 'woocommerce' ),
						'new_item_name'     => __( 'New shipping class Name', 'woocommerce' ),
					),
					'show_ui'               => false,
					'show_in_quick_edit'    => false,
					'show_in_nav_menus'     => false,
					'query_var'             => is_admin(),
					'capabilities'          => array(
						'manage_terms' => 'manage_product_terms',
						'edit_terms'   => 'edit_product_terms',
						'delete_terms' => 'delete_product_terms',
						'assign_terms' => 'assign_product_terms',
					),
					'rewrite'               => false,
				)
			)
		);

		global $wc_product_attributes;

		$wc_product_attributes = array();
		$attribute_taxonomies  = wc_get_attribute_taxonomies();

		if ( $attribute_taxonomies ) {
			foreach ( $attribute_taxonomies as $tax ) {
				$name = wc_attribute_taxonomy_name( $tax->attribute_name );

				if ( $name ) {
					$tax->attribute_public          = absint( isset( $tax->attribute_public ) ? $tax->attribute_public : 1 );
					$label                          = ! empty( $tax->attribute_label ) ? $tax->attribute_label : $tax->attribute_name;
					$wc_product_attributes[ $name ] = $tax;
					$taxonomy_data                  = array(
						'hierarchical'          => false,
						'update_count_callback' => '_update_post_term_count',
						'labels'                => array(
							/* translators: %s: attribute name */
							'name'              => sprintf( _x( 'Product %s', 'Product Attribute', 'woocommerce' ), $label ),
							'singular_name'     => $label,
							/* translators: %s: attribute name */
							'search_items'      => sprintf( __( 'Search %s', 'woocommerce' ), $label ),
							/* translators: %s: attribute name */
							'all_items'         => sprintf( __( 'All %s', 'woocommerce' ), $label ),
							/* translators: %s: attribute name */
							'parent_item'       => sprintf( __( 'Parent %s', 'woocommerce' ), $label ),
							/* translators: %s: attribute name */
							'parent_item_colon' => sprintf( __( 'Parent %s:', 'woocommerce' ), $label ),
							/* translators: %s: attribute name */
							'edit_item'         => sprintf( __( 'Edit %s', 'woocommerce' ), $label ),
							/* translators: %s: attribute name */
							'update_item'       => sprintf( __( 'Update %s', 'woocommerce' ), $label ),
							/* translators: %s: attribute name */
							'add_new_item'      => sprintf( __( 'Add new %s', 'woocommerce' ), $label ),
							/* translators: %s: attribute name */
							'new_item_name'     => sprintf( __( 'New %s', 'woocommerce' ), $label ),
							/* translators: %s: attribute name */
							'not_found'         => sprintf( __( 'No &quot;%s&quot; found', 'woocommerce' ), $label ),
							/* translators: %s: attribute name */
							'back_to_items'     => sprintf( __( '&larr; Back to "%s" attributes', 'woocommerce' ), $label ),
						),
						'show_ui'               => true,
						'show_in_quick_edit'    => false,
						'show_in_menu'          => false,
						'meta_box_cb'           => false,
						'query_var'             => 1 === $tax->attribute_public,
						'rewrite'               => false,
						'sort'                  => false,
						'public'                => 1 === $tax->attribute_public,
						'show_in_nav_menus'     => 1 === $tax->attribute_public && apply_filters( 'woocommerce_attribute_show_in_nav_menus', false, $name ),
						'capabilities'          => array(
							'manage_terms' => 'manage_product_terms',
							'edit_terms'   => 'edit_product_terms',
							'delete_terms' => 'delete_product_terms',
							'assign_terms' => 'assign_product_terms',
						),
					);

					if ( 1 === $tax->attribute_public && sanitize_title( $tax->attribute_name ) ) {
						$taxonomy_data['rewrite'] = array(
							'slug'         => trailingslashit( $permalinks['attribute_rewrite_slug'] ) . urldecode( sanitize_title( $tax->attribute_name ) ),
							'with_front'   => false,
							'hierarchical' => true,
						);
					}

					register_taxonomy( $name, apply_filters( "woocommerce_taxonomy_objects_{$name}", array( 'product' ) ), apply_filters( "woocommerce_taxonomy_args_{$name}", $taxonomy_data ) );
				}
			}
		}

		do_action( 'woocommerce_after_register_taxonomy' );
	}

	/**
	 * Register core post types.
	 */
	public static function register_post_types() {
		if ( ! is_blog_installed() || post_type_exists( 'product' ) ) {
			return;
		}

		do_action( 'woocommerce_register_post_type' );

		$permalinks = wc_get_permalink_structure();
		$supports   = array( 'title', 'editor', 'excerpt', 'thumbnail', 'custom-fields', 'publicize', 'wpcom-markdown' );

		if ( 'yes' === get_option( 'woocommerce_enable_reviews', 'yes' ) ) {
			$supports[] = 'comments';
		}

		$shop_page_id = wc_get_page_id( 'shop' );

		if ( current_theme_supports( 'woocommerce' ) ) {
			$has_archive = $shop_page_id && get_post( $shop_page_id ) ? urldecode( get_page_uri( $shop_page_id ) ) : 'shop';
		} else {
			$has_archive = false;
		}

		// If theme support changes, we may need to flush permalinks since some are changed based on this flag.
		$theme_support = current_theme_supports( 'woocommerce' ) ? 'yes' : 'no';
		if ( get_option( 'current_theme_supports_woocommerce' ) !== $theme_support && update_option( 'current_theme_supports_woocommerce', $theme_support ) ) {
			update_option( 'woocommerce_queue_flush_rewrite_rules', 'yes' );
		}

		register_post_type(
			'product',
			apply_filters(
				'woocommerce_register_post_type_product',
				array(
					'labels'              => array(
						'name'                  => __( 'Products', 'woocommerce' ),
						'singular_name'         => __( 'Product', 'woocommerce' ),
						'all_items'             => __( 'All Products', 'woocommerce' ),
						'menu_name'             => _x( 'Products', 'Admin menu name', 'woocommerce' ),
						'add_new'               => __( 'Add New', 'woocommerce' ),
						'add_new_item'          => __( 'Add new product', 'woocommerce' ),
						'edit'                  => __( 'Edit', 'woocommerce' ),
						'edit_item'             => __( 'Edit product', 'woocommerce' ),
						'new_item'              => __( 'New product', 'woocommerce' ),
						'view_item'             => __( 'View product', 'woocommerce' ),
						'view_items'            => __( 'View products', 'woocommerce' ),
						'search_items'          => __( 'Search products', 'woocommerce' ),
						'not_found'             => __( 'No products found', 'woocommerce' ),
						'not_found_in_trash'    => __( 'No products found in trash', 'woocommerce' ),
						'parent'                => __( 'Parent product', 'woocommerce' ),
						'featured_image'        => __( 'Product image', 'woocommerce' ),
						'set_featured_image'    => __( 'Set product image', 'woocommerce' ),
						'remove_featured_image' => __( 'Remove product image', 'woocommerce' ),
						'use_featured_image'    => __( 'Use as product image', 'woocommerce' ),
						'insert_into_item'      => __( 'Insert into product', 'woocommerce' ),
						'uploaded_to_this_item' => __( 'Uploaded to this product', 'woocommerce' ),
						'filter_items_list'     => __( 'Filter products', 'woocommerce' ),
						'items_list_navigation' => __( 'Products navigation', 'woocommerce' ),
						'items_list'            => __( 'Products list', 'woocommerce' ),
						'item_link'             => __( 'Product Link', 'woocommerce' ),
						'item_link_description' => __( 'A link to a product.', 'woocommerce' ),
					),
					'description'         => __( 'This is where you can browse products in this store.', 'woocommerce' ),
					'public'              => true,
					'show_ui'             => true,
					'menu_icon'           => 'dashicons-archive',
					'capability_type'     => 'product',
					'map_meta_cap'        => true,
					'publicly_queryable'  => true,
					'exclude_from_search' => false,
					'hierarchical'        => false, // Hierarchical causes memory issues - WP loads all records!
					'rewrite'             => $permalinks['product_rewrite_slug'] ? array(
						'slug'       => $permalinks['product_rewrite_slug'],
						'with_front' => false,
						'feeds'      => true,
					) : false,
					'query_var'           => true,
					'supports'            => $supports,
					'has_archive'         => $has_archive,
					'show_in_nav_menus'   => true,
					'show_in_rest'        => true,
				)
			)
		);

		register_post_type(
			'product_variation',
			apply_filters(
				'woocommerce_register_post_type_product_variation',
				array(
					'label'           => __( 'Variations', 'woocommerce' ),
					'public'          => false,
					'hierarchical'    => false,
					'supports'        => false,
					'capability_type' => 'product',
					'rewrite'         => false,
				)
			)
		);

		wc_register_order_type(
			'shop_order',
			apply_filters(
				'woocommerce_register_post_type_shop_order',
				array(
					'labels'              => array(
						'name'                  => __( 'Orders', 'woocommerce' ),
						'singular_name'         => _x( 'Order', 'shop_order post type singular name', 'woocommerce' ),
						'add_new'               => __( 'Add order', 'woocommerce' ),
						'add_new_item'          => __( 'Add new order', 'woocommerce' ),
						'edit'                  => __( 'Edit', 'woocommerce' ),
						'edit_item'             => __( 'Edit order', 'woocommerce' ),
						'new_item'              => __( 'New order', 'woocommerce' ),
						'view_item'             => __( 'View order', 'woocommerce' ),
						'search_items'          => __( 'Search orders', 'woocommerce' ),
						'not_found'             => __( 'No orders found', 'woocommerce' ),
						'not_found_in_trash'    => __( 'No orders found in trash', 'woocommerce' ),
						'parent'                => __( 'Parent orders', 'woocommerce' ),
						'menu_name'             => _x( 'Orders', 'Admin menu name', 'woocommerce' ),
						'filter_items_list'     => __( 'Filter orders', 'woocommerce' ),
						'items_list_navigation' => __( 'Orders navigation', 'woocommerce' ),
						'items_list'            => __( 'Orders list', 'woocommerce' ),
					),
					'description'         => __( 'This is where store orders are stored.', 'woocommerce' ),
					'public'              => false,
					'show_ui'             => true,
					'capability_type'     => 'shop_order',
					'map_meta_cap'        => true,
					'publicly_queryable'  => false,
					'exclude_from_search' => true,
					'show_in_menu'        => current_user_can( 'edit_others_shop_orders' ) ? 'woocommerce' : true,
					'hierarchical'        => false,
					'show_in_nav_menus'   => false,
					'rewrite'             => false,
					'query_var'           => false,
					'supports'            => array( 'title', 'comments', 'custom-fields' ),
					'has_archive'         => false,
				)
			)
		);

		wc_register_order_type(
			'shop_order_refund',
			apply_filters(
				'woocommerce_register_post_type_shop_order_refund',
				array(
					'label'                            => __( 'Refunds', 'woocommerce' ),
					'capability_type'                  => 'shop_order',
					'public'                           => false,
					'hierarchical'                     => false,
					'supports'                         => false,
					'exclude_from_orders_screen'       => false,
					'add_order_meta_boxes'             => false,
					'exclude_from_order_count'         => true,
					'exclude_from_order_views'         => false,
					'exclude_from_order_reports'       => false,
					'exclude_from_order_sales_reports' => true,
					'class_name'                       => 'WC_Order_Refund',
					'rewrite'                          => false,
				)
			)
		);

		if ( 'yes' === get_option( 'woocommerce_enable_coupons' ) ) {
			register_post_type(
				'shop_coupon',
				apply_filters(
					'woocommerce_register_post_type_shop_coupon',
					array(
						'labels'              => array(
							'name'                  => __( 'Coupons', 'woocommerce' ),
							'singular_name'         => __( 'Coupon', 'woocommerce' ),
							'menu_name'             => _x( 'Coupons', 'Admin menu name', 'woocommerce' ),
							'add_new'               => __( 'Add coupon', 'woocommerce' ),
							'add_new_item'          => __( 'Add new coupon', 'woocommerce' ),
							'edit'                  => __( 'Edit', 'woocommerce' ),
							'edit_item'             => __( 'Edit coupon', 'woocommerce' ),
							'new_item'              => __( 'New coupon', 'woocommerce' ),
							'view_item'             => __( 'View coupon', 'woocommerce' ),
							'search_items'          => __( 'Search coupons', 'woocommerce' ),
							'not_found'             => __( 'No coupons found', 'woocommerce' ),
							'not_found_in_trash'    => __( 'No coupons found in trash', 'woocommerce' ),
							'parent'                => __( 'Parent coupon', 'woocommerce' ),
							'filter_items_list'     => __( 'Filter coupons', 'woocommerce' ),
							'items_list_navigation' => __( 'Coupons navigation', 'woocommerce' ),
							'items_list'            => __( 'Coupons list', 'woocommerce' ),
						),
						'description'         => __( 'This is where you can add new coupons that customers can use in your store.', 'woocommerce' ),
						'public'              => false,
						'show_ui'             => true,
						'capability_type'     => 'shop_coupon',
						'map_meta_cap'        => true,
						'publicly_queryable'  => false,
						'exclude_from_search' => true,
						'show_in_menu'        => current_user_can( 'edit_others_shop_orders' ) ? 'woocommerce' : true,
						'hierarchical'        => false,
						'rewrite'             => false,
						'query_var'           => false,
						'supports'            => array( 'title' ),
						'show_in_nav_menus'   => false,
						'show_in_admin_bar'   => true,
					)
				)
			);
		}

		do_action( 'woocommerce_after_register_post_type' );
	}

	/**
	 * Customize taxonomies update messages.
	 *
	 * @param array $messages The list of available messages.
	 * @since 4.4.0
	 * @return bool
	 */
	public static function updated_term_messages( $messages ) {
		$messages['product_cat'] = array(
			0 => '',
			1 => __( 'Category added.', 'woocommerce' ),
			2 => __( 'Category deleted.', 'woocommerce' ),
			3 => __( 'Category updated.', 'woocommerce' ),
			4 => __( 'Category not added.', 'woocommerce' ),
			5 => __( 'Category not updated.', 'woocommerce' ),
			6 => __( 'Categories deleted.', 'woocommerce' ),
		);

		$messages['product_tag'] = array(
			0 => '',
			1 => __( 'Tag added.', 'woocommerce' ),
			2 => __( 'Tag deleted.', 'woocommerce' ),
			3 => __( 'Tag updated.', 'woocommerce' ),
			4 => __( 'Tag not added.', 'woocommerce' ),
			5 => __( 'Tag not updated.', 'woocommerce' ),
			6 => __( 'Tags deleted.', 'woocommerce' ),
		);

		$wc_product_attributes = array();
		$attribute_taxonomies  = wc_get_attribute_taxonomies();

		if ( $attribute_taxonomies ) {
			foreach ( $attribute_taxonomies as $tax ) {
				$name = wc_attribute_taxonomy_name( $tax->attribute_name );

				if ( $name ) {
					$label = ! empty( $tax->attribute_label ) ? $tax->attribute_label : $tax->attribute_name;

					$messages[ $name ] = array(
						0 => '',
						/* translators: %s: taxonomy label */
						1 => sprintf( _x( '%s added', 'taxonomy term messages', 'woocommerce' ), $label ),
						/* translators: %s: taxonomy label */
						2 => sprintf( _x( '%s deleted', 'taxonomy term messages', 'woocommerce' ), $label ),
						/* translators: %s: taxonomy label */
						3 => sprintf( _x( '%s updated', 'taxonomy term messages', 'woocommerce' ), $label ),
						/* translators: %s: taxonomy label */
						4 => sprintf( _x( '%s not added', 'taxonomy term messages', 'woocommerce' ), $label ),
						/* translators: %s: taxonomy label */
						5 => sprintf( _x( '%s not updated', 'taxonomy term messages', 'woocommerce' ), $label ),
						/* translators: %s: taxonomy label */
						6 => sprintf( _x( '%s deleted', 'taxonomy term messages', 'woocommerce' ), $label ),
					);
				}
			}
		}

		return $messages;
	}

	/**
	 * Register our custom post statuses, used for order status.
	 */
	public static function register_post_status() {

		$order_statuses = apply_filters(
			'woocommerce_register_shop_order_post_statuses',
			array(
				'wc-pending'    => array(
					'label'                     => _x( 'Pending payment', 'Order status', 'woocommerce' ),
					'public'                    => false,
					'exclude_from_search'       => false,
					'show_in_admin_all_list'    => true,
					'show_in_admin_status_list' => true,
					/* translators: %s: number of orders */
					'label_count'               => _n_noop( 'Pending payment <span class="count">(%s)</span>', 'Pending payment <span class="count">(%s)</span>', 'woocommerce' ),
				),
				'wc-processing' => array(
					'label'                     => _x( 'Processing', 'Order status', 'woocommerce' ),
					'public'                    => false,
					'exclude_from_search'       => false,
					'show_in_admin_all_list'    => true,
					'show_in_admin_status_list' => true,
					/* translators: %s: number of orders */
					'label_count'               => _n_noop( 'Processing <span class="count">(%s)</span>', 'Processing <span class="count">(%s)</span>', 'woocommerce' ),
				),
				'wc-on-hold'    => array(
					'label'                     => _x( 'On hold', 'Order status', 'woocommerce' ),
					'public'                    => false,
					'exclude_from_search'       => false,
					'show_in_admin_all_list'    => true,
					'show_in_admin_status_list' => true,
					/* translators: %s: number of orders */
					'label_count'               => _n_noop( 'On hold <span class="count">(%s)</span>', 'On hold <span class="count">(%s)</span>', 'woocommerce' ),
				),
				'wc-completed'  => array(
					'label'                     => _x( 'Completed', 'Order status', 'woocommerce' ),
					'public'                    => false,
					'exclude_from_search'       => false,
					'show_in_admin_all_list'    => true,
					'show_in_admin_status_list' => true,
					/* translators: %s: number of orders */
					'label_count'               => _n_noop( 'Completed <span class="count">(%s)</span>', 'Completed <span class="count">(%s)</span>', 'woocommerce' ),
				),
				'wc-cancelled'  => array(
					'label'                     => _x( 'Cancelled', 'Order status', 'woocommerce' ),
					'public'                    => false,
					'exclude_from_search'       => false,
					'show_in_admin_all_list'    => true,
					'show_in_admin_status_list' => true,
					/* translators: %s: number of orders */
					'label_count'               => _n_noop( 'Cancelled <span class="count">(%s)</span>', 'Cancelled <span class="count">(%s)</span>', 'woocommerce' ),
				),
				'wc-refunded'   => array(
					'label'                     => _x( 'Refunded', 'Order status', 'woocommerce' ),
					'public'                    => false,
					'exclude_from_search'       => false,
					'show_in_admin_all_list'    => true,
					'show_in_admin_status_list' => true,
					/* translators: %s: number of orders */
					'label_count'               => _n_noop( 'Refunded <span class="count">(%s)</span>', 'Refunded <span class="count">(%s)</span>', 'woocommerce' ),
				),
				'wc-failed'     => array(
					'label'                     => _x( 'Failed', 'Order status', 'woocommerce' ),
					'public'                    => false,
					'exclude_from_search'       => false,
					'show_in_admin_all_list'    => true,
					'show_in_admin_status_list' => true,
					/* translators: %s: number of orders */
					'label_count'               => _n_noop( 'Failed <span class="count">(%s)</span>', 'Failed <span class="count">(%s)</span>', 'woocommerce' ),
				),
			)
		);

		foreach ( $order_statuses as $order_status => $values ) {
			register_post_status( $order_status, $values );
		}
	}

	/**
	 * Flush rules if the event is queued.
	 *
	 * @since 3.3.0
	 */
	public static function maybe_flush_rewrite_rules() {
		if ( 'yes' === get_option( 'woocommerce_queue_flush_rewrite_rules' ) ) {
			update_option( 'woocommerce_queue_flush_rewrite_rules', 'no' );
			self::flush_rewrite_rules();
		}
	}

	/**
	 * Flush rewrite rules.
	 */
	public static function flush_rewrite_rules() {
		flush_rewrite_rules();
	}

	/**
	 * Disable Gutenberg for products.
	 *
	 * @param bool   $can_edit Whether the post type can be edited or not.
	 * @param string $post_type The post type being checked.
	 * @return bool
	 */
	public static function gutenberg_can_edit_post_type( $can_edit, $post_type ) {
		return 'product' === $post_type ? false : $can_edit;
	}

	/**
	 * Add Product Support to Jetpack Omnisearch.
	 */
	public static function support_jetpack_omnisearch() {
		if ( class_exists( 'Jetpack_Omnisearch_Posts' ) ) {
			new Jetpack_Omnisearch_Posts( 'product' );
		}
	}

	/**
	 * Added product for Jetpack related posts.
	 *
	 * @param  array $post_types Post types.
	 * @return array
	 */
	public static function rest_api_allowed_post_types( $post_types ) {
		$post_types[] = 'product';

		return $post_types;
	}
}

WC_Post_types::init();
wc-order-functions.php000064400000104353151336044770011025 0ustar00<?php
/**
 * WooCommerce Order Functions
 *
 * Functions for order specific things.
 *
 * @package WooCommerce\Functions
 * @version 3.4.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Standard way of retrieving orders based on certain parameters.
 *
 * This function should be used for order retrieval so that when we move to
 * custom tables, functions still work.
 *
 * Args and usage: https://github.com/woocommerce/woocommerce/wiki/wc_get_orders-and-WC_Order_Query
 *
 * @since  2.6.0
 * @param  array $args Array of args (above).
 * @return WC_Order[]|stdClass Number of pages and an array of order objects if
 *                             paginate is true, or just an array of values.
 */
function wc_get_orders( $args ) {
	$map_legacy = array(
		'numberposts'    => 'limit',
		'post_type'      => 'type',
		'post_status'    => 'status',
		'post_parent'    => 'parent',
		'author'         => 'customer',
		'email'          => 'billing_email',
		'posts_per_page' => 'limit',
		'paged'          => 'page',
	);

	foreach ( $map_legacy as $from => $to ) {
		if ( isset( $args[ $from ] ) ) {
			$args[ $to ] = $args[ $from ];
		}
	}

	// Map legacy date args to modern date args.
	$date_before = false;
	$date_after  = false;

	if ( ! empty( $args['date_before'] ) ) {
		$datetime    = wc_string_to_datetime( $args['date_before'] );
		$date_before = strpos( $args['date_before'], ':' ) ? $datetime->getOffsetTimestamp() : $datetime->date( 'Y-m-d' );
	}
	if ( ! empty( $args['date_after'] ) ) {
		$datetime   = wc_string_to_datetime( $args['date_after'] );
		$date_after = strpos( $args['date_after'], ':' ) ? $datetime->getOffsetTimestamp() : $datetime->date( 'Y-m-d' );
	}

	if ( $date_before && $date_after ) {
		$args['date_created'] = $date_after . '...' . $date_before;
	} elseif ( $date_before ) {
		$args['date_created'] = '<' . $date_before;
	} elseif ( $date_after ) {
		$args['date_created'] = '>' . $date_after;
	}

	$query = new WC_Order_Query( $args );
	return $query->get_orders();
}

/**
 * Main function for returning orders, uses the WC_Order_Factory class.
 *
 * @since  2.2
 *
 * @param mixed $the_order       Post object or post ID of the order.
 *
 * @return bool|WC_Order|WC_Order_Refund
 */
function wc_get_order( $the_order = false ) {
	if ( ! did_action( 'woocommerce_after_register_post_type' ) ) {
		wc_doing_it_wrong( __FUNCTION__, 'wc_get_order should not be called before post types are registered (woocommerce_after_register_post_type action)', '2.5' );
		return false;
	}
	return WC()->order_factory->get_order( $the_order );
}

/**
 * Get all order statuses.
 *
 * @since 2.2
 * @used-by WC_Order::set_status
 * @return array
 */
function wc_get_order_statuses() {
	$order_statuses = array(
		'wc-pending'    => _x( 'Pending payment', 'Order status', 'woocommerce' ),
		'wc-processing' => _x( 'Processing', 'Order status', 'woocommerce' ),
		'wc-on-hold'    => _x( 'On hold', 'Order status', 'woocommerce' ),
		'wc-completed'  => _x( 'Completed', 'Order status', 'woocommerce' ),
		'wc-cancelled'  => _x( 'Cancelled', 'Order status', 'woocommerce' ),
		'wc-refunded'   => _x( 'Refunded', 'Order status', 'woocommerce' ),
		'wc-failed'     => _x( 'Failed', 'Order status', 'woocommerce' ),
	);
	return apply_filters( 'wc_order_statuses', $order_statuses );
}

/**
 * See if a string is an order status.
 *
 * @param  string $maybe_status Status, including any wc- prefix.
 * @return bool
 */
function wc_is_order_status( $maybe_status ) {
	$order_statuses = wc_get_order_statuses();
	return isset( $order_statuses[ $maybe_status ] );
}

/**
 * Get list of statuses which are consider 'paid'.
 *
 * @since  3.0.0
 * @return array
 */
function wc_get_is_paid_statuses() {
	return apply_filters( 'woocommerce_order_is_paid_statuses', array( 'processing', 'completed' ) );
}

/**
 * Get list of statuses which are consider 'pending payment'.
 *
 * @since  3.6.0
 * @return array
 */
function wc_get_is_pending_statuses() {
	return apply_filters( 'woocommerce_order_is_pending_statuses', array( 'pending' ) );
}

/**
 * Get the nice name for an order status.
 *
 * @since  2.2
 * @param  string $status Status.
 * @return string
 */
function wc_get_order_status_name( $status ) {
	$statuses = wc_get_order_statuses();
	$status   = 'wc-' === substr( $status, 0, 3 ) ? substr( $status, 3 ) : $status;
	$status   = isset( $statuses[ 'wc-' . $status ] ) ? $statuses[ 'wc-' . $status ] : $status;
	return $status;
}

/**
 * Generate an order key with prefix.
 *
 * @since 3.5.4
 * @param string $key Order key without a prefix. By default generates a 13 digit secret.
 * @return string The order key.
 */
function wc_generate_order_key( $key = '' ) {
	if ( '' === $key ) {
		$key = wp_generate_password( 13, false );
	}

	return 'wc_' . apply_filters( 'woocommerce_generate_order_key', 'order_' . $key );
}

/**
 * Finds an Order ID based on an order key.
 *
 * @param string $order_key An order key has generated by.
 * @return int The ID of an order, or 0 if the order could not be found.
 */
function wc_get_order_id_by_order_key( $order_key ) {
	$data_store = WC_Data_Store::load( 'order' );
	return $data_store->get_order_id_by_order_key( $order_key );
}

/**
 * Get all registered order types.
 *
 * @since  2.2
 * @param  string $for Optionally define what you are getting order types for so
 *                     only relevant types are returned.
 *                     e.g. for 'order-meta-boxes', 'order-count'.
 * @return array
 */
function wc_get_order_types( $for = '' ) {
	global $wc_order_types;

	if ( ! is_array( $wc_order_types ) ) {
		$wc_order_types = array();
	}

	$order_types = array();

	switch ( $for ) {
		case 'order-count':
			foreach ( $wc_order_types as $type => $args ) {
				if ( ! $args['exclude_from_order_count'] ) {
					$order_types[] = $type;
				}
			}
			break;
		case 'order-meta-boxes':
			foreach ( $wc_order_types as $type => $args ) {
				if ( $args['add_order_meta_boxes'] ) {
					$order_types[] = $type;
				}
			}
			break;
		case 'view-orders':
			foreach ( $wc_order_types as $type => $args ) {
				if ( ! $args['exclude_from_order_views'] ) {
					$order_types[] = $type;
				}
			}
			break;
		case 'reports':
			foreach ( $wc_order_types as $type => $args ) {
				if ( ! $args['exclude_from_order_reports'] ) {
					$order_types[] = $type;
				}
			}
			break;
		case 'sales-reports':
			foreach ( $wc_order_types as $type => $args ) {
				if ( ! $args['exclude_from_order_sales_reports'] ) {
					$order_types[] = $type;
				}
			}
			break;
		case 'order-webhooks':
			foreach ( $wc_order_types as $type => $args ) {
				if ( ! $args['exclude_from_order_webhooks'] ) {
					$order_types[] = $type;
				}
			}
			break;
		default:
			$order_types = array_keys( $wc_order_types );
			break;
	}

	return apply_filters( 'wc_order_types', $order_types, $for );
}

/**
 * Get an order type by post type name.
 *
 * @param  string $type Post type name.
 * @return bool|array Details about the order type.
 */
function wc_get_order_type( $type ) {
	global $wc_order_types;

	if ( isset( $wc_order_types[ $type ] ) ) {
		return $wc_order_types[ $type ];
	}

	return false;
}

/**
 * Register order type. Do not use before init.
 *
 * Wrapper for register post type, as well as a method of telling WC which.
 * post types are types of orders, and having them treated as such.
 *
 * $args are passed to register_post_type, but there are a few specific to this function:
 *      - exclude_from_orders_screen (bool) Whether or not this order type also get shown in the main.
 *      orders screen.
 *      - add_order_meta_boxes (bool) Whether or not the order type gets shop_order meta boxes.
 *      - exclude_from_order_count (bool) Whether or not this order type is excluded from counts.
 *      - exclude_from_order_views (bool) Whether or not this order type is visible by customers when.
 *      viewing orders e.g. on the my account page.
 *      - exclude_from_order_reports (bool) Whether or not to exclude this type from core reports.
 *      - exclude_from_order_sales_reports (bool) Whether or not to exclude this type from core sales reports.
 *
 * @since  2.2
 * @see    register_post_type for $args used in that function
 * @param  string $type Post type. (max. 20 characters, can not contain capital letters or spaces).
 * @param  array  $args An array of arguments.
 * @return bool Success or failure
 */
function wc_register_order_type( $type, $args = array() ) {
	if ( post_type_exists( $type ) ) {
		return false;
	}

	global $wc_order_types;

	if ( ! is_array( $wc_order_types ) ) {
		$wc_order_types = array();
	}

	// Register as a post type.
	if ( is_wp_error( register_post_type( $type, $args ) ) ) {
		return false;
	}

	// Register for WC usage.
	$order_type_args = array(
		'exclude_from_orders_screen'       => false,
		'add_order_meta_boxes'             => true,
		'exclude_from_order_count'         => false,
		'exclude_from_order_views'         => false,
		'exclude_from_order_webhooks'      => false,
		'exclude_from_order_reports'       => false,
		'exclude_from_order_sales_reports' => false,
		'class_name'                       => 'WC_Order',
	);

	$args                    = array_intersect_key( $args, $order_type_args );
	$args                    = wp_parse_args( $args, $order_type_args );
	$wc_order_types[ $type ] = $args;

	return true;
}

/**
 * Return the count of processing orders.
 *
 * @return int
 */
function wc_processing_order_count() {
	return wc_orders_count( 'processing' );
}

/**
 * Return the orders count of a specific order status.
 *
 * @param string $status Status.
 * @return int
 */
function wc_orders_count( $status ) {
	$count          = 0;
	$status         = 'wc-' . $status;
	$order_statuses = array_keys( wc_get_order_statuses() );

	if ( ! in_array( $status, $order_statuses, true ) ) {
		return 0;
	}

	$cache_key    = WC_Cache_Helper::get_cache_prefix( 'orders' ) . $status;
	$cached_count = wp_cache_get( $cache_key, 'counts' );

	if ( false !== $cached_count ) {
		return $cached_count;
	}

	foreach ( wc_get_order_types( 'order-count' ) as $type ) {
		$data_store = WC_Data_Store::load( 'shop_order' === $type ? 'order' : $type );
		if ( $data_store ) {
			$count += $data_store->get_order_count( $status );
		}
	}

	wp_cache_set( $cache_key, $count, 'counts' );

	return $count;
}

/**
 * Grant downloadable product access to the file identified by $download_id.
 *
 * @param  string         $download_id File identifier.
 * @param  int|WC_Product $product     Product instance or ID.
 * @param  WC_Order       $order       Order data.
 * @param  int            $qty         Quantity purchased.
 * @param  WC_Order_Item  $item        Item of the order.
 * @return int|bool insert id or false on failure.
 */
function wc_downloadable_file_permission( $download_id, $product, $order, $qty = 1, $item = null ) {
	if ( is_numeric( $product ) ) {
		$product = wc_get_product( $product );
	}
	$download = new WC_Customer_Download();
	$download->set_download_id( $download_id );
	$download->set_product_id( $product->get_id() );
	$download->set_user_id( $order->get_customer_id() );
	$download->set_order_id( $order->get_id() );
	$download->set_user_email( $order->get_billing_email() );
	$download->set_order_key( $order->get_order_key() );
	$download->set_downloads_remaining( 0 > $product->get_download_limit() ? '' : $product->get_download_limit() * $qty );
	$download->set_access_granted( time() );
	$download->set_download_count( 0 );

	$expiry = $product->get_download_expiry();

	if ( $expiry > 0 ) {
		$from_date = $order->get_date_completed() ? $order->get_date_completed()->format( 'Y-m-d' ) : current_time( 'mysql', true );
		$download->set_access_expires( strtotime( $from_date . ' + ' . $expiry . ' DAY' ) );
	}

	$download = apply_filters( 'woocommerce_downloadable_file_permission', $download, $product, $order, $qty, $item );

	return $download->save();
}

/**
 * Order Status completed - give downloadable product access to customer.
 *
 * @param int  $order_id Order ID.
 * @param bool $force    Force downloadable permissions.
 */
function wc_downloadable_product_permissions( $order_id, $force = false ) {
	$order = wc_get_order( $order_id );

	if ( ! $order || ( $order->get_data_store()->get_download_permissions_granted( $order ) && ! $force ) ) {
		return;
	}

	if ( $order->has_status( 'processing' ) && 'no' === get_option( 'woocommerce_downloads_grant_access_after_payment' ) ) {
		return;
	}

	if ( count( $order->get_items() ) > 0 ) {
		foreach ( $order->get_items() as $item ) {
			$product = $item->get_product();

			if ( $product && $product->exists() && $product->is_downloadable() ) {
				$downloads = $product->get_downloads();

				foreach ( array_keys( $downloads ) as $download_id ) {
					wc_downloadable_file_permission( $download_id, $product, $order, $item->get_quantity(), $item );
				}
			}
		}
	}

	$order->get_data_store()->set_download_permissions_granted( $order, true );
	do_action( 'woocommerce_grant_product_download_permissions', $order_id );
}
add_action( 'woocommerce_order_status_completed', 'wc_downloadable_product_permissions' );
add_action( 'woocommerce_order_status_processing', 'wc_downloadable_product_permissions' );

/**
 * Clear all transients cache for order data.
 *
 * @param int|WC_Order $order Order instance or ID.
 */
function wc_delete_shop_order_transients( $order = 0 ) {
	if ( is_numeric( $order ) ) {
		$order = wc_get_order( $order );
	}
	$reports             = WC_Admin_Reports::get_reports();
	$transients_to_clear = array(
		'wc_admin_report',
	);

	foreach ( $reports as $report_group ) {
		foreach ( $report_group['reports'] as $report_key => $report ) {
			$transients_to_clear[] = 'wc_report_' . $report_key;
		}
	}

	foreach ( $transients_to_clear as $transient ) {
		delete_transient( $transient );
	}

	// Clear customer's order related caches.
	if ( is_a( $order, 'WC_Order' ) ) {
		$order_id = $order->get_id();
		delete_user_meta( $order->get_customer_id(), '_money_spent' );
		delete_user_meta( $order->get_customer_id(), '_order_count' );
		delete_user_meta( $order->get_customer_id(), '_last_order' );
	} else {
		$order_id = 0;
	}

	// Increments the transient version to invalidate cache.
	WC_Cache_Helper::get_transient_version( 'orders', true );

	// Do the same for regular cache.
	WC_Cache_Helper::invalidate_cache_group( 'orders' );

	do_action( 'woocommerce_delete_shop_order_transients', $order_id );
}

/**
 * See if we only ship to billing addresses.
 *
 * @return bool
 */
function wc_ship_to_billing_address_only() {
	return 'billing_only' === get_option( 'woocommerce_ship_to_destination' );
}

/**
 * Create a new order refund programmatically.
 *
 * Returns a new refund object on success which can then be used to add additional data.
 *
 * @since 2.2
 * @throws Exception Throws exceptions when fail to create, but returns WP_Error instead.
 * @param array $args New refund arguments.
 * @return WC_Order_Refund|WP_Error
 */
function wc_create_refund( $args = array() ) {
	$default_args = array(
		'amount'         => 0,
		'reason'         => null,
		'order_id'       => 0,
		'refund_id'      => 0,
		'line_items'     => array(),
		'refund_payment' => false,
		'restock_items'  => false,
	);

	try {
		$args  = wp_parse_args( $args, $default_args );
		$order = wc_get_order( $args['order_id'] );

		if ( ! $order ) {
			throw new Exception( __( 'Invalid order ID.', 'woocommerce' ) );
		}

		$remaining_refund_amount = $order->get_remaining_refund_amount();
		$remaining_refund_items  = $order->get_remaining_refund_items();
		$refund_item_count       = 0;
		$refund                  = new WC_Order_Refund( $args['refund_id'] );

		if ( 0 > $args['amount'] || $args['amount'] > $remaining_refund_amount ) {
			throw new Exception( __( 'Invalid refund amount.', 'woocommerce' ) );
		}

		$refund->set_currency( $order->get_currency() );
		$refund->set_amount( $args['amount'] );
		$refund->set_parent_id( absint( $args['order_id'] ) );
		$refund->set_refunded_by( get_current_user_id() ? get_current_user_id() : 1 );
		$refund->set_prices_include_tax( $order->get_prices_include_tax() );

		if ( ! is_null( $args['reason'] ) ) {
			$refund->set_reason( $args['reason'] );
		}

		// Negative line items.
		if ( count( $args['line_items'] ) > 0 ) {
			$items = $order->get_items( array( 'line_item', 'fee', 'shipping' ) );

			foreach ( $items as $item_id => $item ) {
				if ( ! isset( $args['line_items'][ $item_id ] ) ) {
					continue;
				}

				$qty          = isset( $args['line_items'][ $item_id ]['qty'] ) ? $args['line_items'][ $item_id ]['qty'] : 0;
				$refund_total = $args['line_items'][ $item_id ]['refund_total'];
				$refund_tax   = isset( $args['line_items'][ $item_id ]['refund_tax'] ) ? array_filter( (array) $args['line_items'][ $item_id ]['refund_tax'] ) : array();

				if ( empty( $qty ) && empty( $refund_total ) && empty( $args['line_items'][ $item_id ]['refund_tax'] ) ) {
					continue;
				}

				$class         = get_class( $item );
				$refunded_item = new $class( $item );
				$refunded_item->set_id( 0 );
				$refunded_item->add_meta_data( '_refunded_item_id', $item_id, true );
				$refunded_item->set_total( wc_format_refund_total( $refund_total ) );
				$refunded_item->set_taxes(
					array(
						'total'    => array_map( 'wc_format_refund_total', $refund_tax ),
						'subtotal' => array_map( 'wc_format_refund_total', $refund_tax ),
					)
				);

				if ( is_callable( array( $refunded_item, 'set_subtotal' ) ) ) {
					$refunded_item->set_subtotal( wc_format_refund_total( $refund_total ) );
				}

				if ( is_callable( array( $refunded_item, 'set_quantity' ) ) ) {
					$refunded_item->set_quantity( $qty * -1 );
				}

				$refund->add_item( $refunded_item );
				$refund_item_count += $qty;
			}
		}

		$refund->update_taxes();
		$refund->calculate_totals( false );
		$refund->set_total( $args['amount'] * -1 );

		// this should remain after update_taxes(), as this will save the order, and write the current date to the db
		// so we must wait until the order is persisted to set the date.
		if ( isset( $args['date_created'] ) ) {
			$refund->set_date_created( $args['date_created'] );
		}

		/**
		 * Action hook to adjust refund before save.
		 *
		 * @since 3.0.0
		 */
		do_action( 'woocommerce_create_refund', $refund, $args );

		if ( $refund->save() ) {
			if ( $args['refund_payment'] ) {
				$result = wc_refund_payment( $order, $refund->get_amount(), $refund->get_reason() );

				if ( is_wp_error( $result ) ) {
					$refund->delete();
					return $result;
				}

				$refund->set_refunded_payment( true );
				$refund->save();
			}

			if ( $args['restock_items'] ) {
				wc_restock_refunded_items( $order, $args['line_items'] );
			}

			// Trigger notification emails.
			if ( ( $remaining_refund_amount - $args['amount'] ) > 0 || ( $order->has_free_item() && ( $remaining_refund_items - $refund_item_count ) > 0 ) ) {
				do_action( 'woocommerce_order_partially_refunded', $order->get_id(), $refund->get_id() );
			} else {
				do_action( 'woocommerce_order_fully_refunded', $order->get_id(), $refund->get_id() );

				$parent_status = apply_filters( 'woocommerce_order_fully_refunded_status', 'refunded', $order->get_id(), $refund->get_id() );

				if ( $parent_status ) {
					$order->update_status( $parent_status );
				}
			}
		}

		do_action( 'woocommerce_refund_created', $refund->get_id(), $args );
		do_action( 'woocommerce_order_refunded', $order->get_id(), $refund->get_id() );

	} catch ( Exception $e ) {
		if ( isset( $refund ) && is_a( $refund, 'WC_Order_Refund' ) ) {
			wp_delete_post( $refund->get_id(), true );
		}
		return new WP_Error( 'error', $e->getMessage() );
	}

	return $refund;
}

/**
 * Try to refund the payment for an order via the gateway.
 *
 * @since 3.0.0
 * @throws Exception Throws exceptions when fail to refund, but returns WP_Error instead.
 * @param WC_Order $order  Order instance.
 * @param string   $amount Amount to refund.
 * @param string   $reason Refund reason.
 * @return bool|WP_Error
 */
function wc_refund_payment( $order, $amount, $reason = '' ) {
	try {
		if ( ! is_a( $order, 'WC_Order' ) ) {
			throw new Exception( __( 'Invalid order.', 'woocommerce' ) );
		}

		$gateway_controller = WC_Payment_Gateways::instance();
		$all_gateways       = $gateway_controller->payment_gateways();
		$payment_method     = $order->get_payment_method();
		$gateway            = isset( $all_gateways[ $payment_method ] ) ? $all_gateways[ $payment_method ] : false;

		if ( ! $gateway ) {
			throw new Exception( __( 'The payment gateway for this order does not exist.', 'woocommerce' ) );
		}

		if ( ! $gateway->supports( 'refunds' ) ) {
			throw new Exception( __( 'The payment gateway for this order does not support automatic refunds.', 'woocommerce' ) );
		}

		$result = $gateway->process_refund( $order->get_id(), $amount, $reason );

		if ( ! $result ) {
			throw new Exception( __( 'An error occurred while attempting to create the refund using the payment gateway API.', 'woocommerce' ) );
		}

		if ( is_wp_error( $result ) ) {
			throw new Exception( $result->get_error_message() );
		}

		return true;

	} catch ( Exception $e ) {
		return new WP_Error( 'error', $e->getMessage() );
	}
}

/**
 * Restock items during refund.
 *
 * @since 3.0.0
 * @param WC_Order $order               Order instance.
 * @param array    $refunded_line_items Refunded items list.
 */
function wc_restock_refunded_items( $order, $refunded_line_items ) {
	if ( ! apply_filters( 'woocommerce_can_restock_refunded_items', true, $order, $refunded_line_items ) ) {
		return;
	}

	$line_items = $order->get_items();

	foreach ( $line_items as $item_id => $item ) {
		if ( ! isset( $refunded_line_items[ $item_id ], $refunded_line_items[ $item_id ]['qty'] ) ) {
			continue;
		}
		$product                = $item->get_product();
		$item_stock_reduced     = $item->get_meta( '_reduced_stock', true );
		$restock_refunded_items = (int) $item->get_meta( '_restock_refunded_items', true );
		$qty_to_refund          = $refunded_line_items[ $item_id ]['qty'];

		if ( ! $item_stock_reduced || ! $qty_to_refund || ! $product || ! $product->managing_stock() ) {
			continue;
		}

		$old_stock = $product->get_stock_quantity();
		$new_stock = wc_update_product_stock( $product, $qty_to_refund, 'increase' );

		// Update _reduced_stock meta to track changes.
		$item_stock_reduced = $item_stock_reduced - $qty_to_refund;

		if ( 0 < $item_stock_reduced ) {
			// Keeps track of total running tally of reduced stock.
			$item->update_meta_data( '_reduced_stock', $item_stock_reduced );

			// Keeps track of only refunded items that needs restock.
			$item->update_meta_data( '_restock_refunded_items', $qty_to_refund + $restock_refunded_items );
		} else {
			$item->delete_meta_data( '_reduced_stock' );
			$item->delete_meta_data( '_restock_refunded_items' );
		}

		/* translators: 1: product ID 2: old stock level 3: new stock level */
		$order->add_order_note( sprintf( __( 'Item #%1$s stock increased from %2$s to %3$s.', 'woocommerce' ), $product->get_id(), $old_stock, $new_stock ) );

		$item->save();

		do_action( 'woocommerce_restock_refunded_item', $product->get_id(), $old_stock, $new_stock, $order, $product );
	}
}

/**
 * Get tax class by tax id.
 *
 * @since 2.2
 * @param int $tax_id Tax ID.
 * @return string
 */
function wc_get_tax_class_by_tax_id( $tax_id ) {
	global $wpdb;
	return $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_class FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %d", $tax_id ) );
}

/**
 * Get payment gateway class by order data.
 *
 * @since 2.2
 * @param int|WC_Order $order Order instance.
 * @return WC_Payment_Gateway|bool
 */
function wc_get_payment_gateway_by_order( $order ) {
	if ( WC()->payment_gateways() ) {
		$payment_gateways = WC()->payment_gateways()->payment_gateways();
	} else {
		$payment_gateways = array();
	}

	if ( ! is_object( $order ) ) {
		$order_id = absint( $order );
		$order    = wc_get_order( $order_id );
	}

	return is_a( $order, 'WC_Order' ) && isset( $payment_gateways[ $order->get_payment_method() ] ) ? $payment_gateways[ $order->get_payment_method() ] : false;
}

/**
 * When refunding an order, create a refund line item if the partial refunds do not match order total.
 *
 * This is manual; no gateway refund will be performed.
 *
 * @since 2.4
 * @param int $order_id Order ID.
 */
function wc_order_fully_refunded( $order_id ) {
	$order      = wc_get_order( $order_id );
	$max_refund = wc_format_decimal( $order->get_total() - $order->get_total_refunded() );

	if ( ! $max_refund ) {
		return;
	}

	// Create the refund object.
	wc_switch_to_site_locale();
	wc_create_refund(
		array(
			'amount'     => $max_refund,
			'reason'     => __( 'Order fully refunded.', 'woocommerce' ),
			'order_id'   => $order_id,
			'line_items' => array(),
		)
	);
	wc_restore_locale();

	$order->add_order_note( __( 'Order status set to refunded. To return funds to the customer you will need to issue a refund through your payment gateway.', 'woocommerce' ) );
}
add_action( 'woocommerce_order_status_refunded', 'wc_order_fully_refunded' );

/**
 * Search orders.
 *
 * @since  2.6.0
 * @param  string $term Term to search.
 * @return array List of orders ID.
 */
function wc_order_search( $term ) {
	$data_store = WC_Data_Store::load( 'order' );
	return $data_store->search_orders( str_replace( 'Order #', '', wc_clean( $term ) ) );
}

/**
 * Update total sales amount for each product within a paid order.
 *
 * @since 3.0.0
 * @param int $order_id Order ID.
 */
function wc_update_total_sales_counts( $order_id ) {
	$order = wc_get_order( $order_id );

	if ( ! $order || $order->get_data_store()->get_recorded_sales( $order ) ) {
		return;
	}

	if ( count( $order->get_items() ) > 0 ) {
		foreach ( $order->get_items() as $item ) {
			$product_id = $item->get_product_id();

			if ( $product_id ) {
				$data_store = WC_Data_Store::load( 'product' );
				$data_store->update_product_sales( $product_id, absint( $item->get_quantity() ), 'increase' );
			}
		}
	}

	$order->get_data_store()->set_recorded_sales( $order, true );

	/**
	 * Called when sales for an order are recorded
	 *
	 * @param int $order_id order id
	 */
	do_action( 'woocommerce_recorded_sales', $order_id );
}
add_action( 'woocommerce_order_status_completed', 'wc_update_total_sales_counts' );
add_action( 'woocommerce_order_status_processing', 'wc_update_total_sales_counts' );
add_action( 'woocommerce_order_status_on-hold', 'wc_update_total_sales_counts' );

/**
 * Update used coupon amount for each coupon within an order.
 *
 * @since 3.0.0
 * @param int $order_id Order ID.
 */
function wc_update_coupon_usage_counts( $order_id ) {
	$order = wc_get_order( $order_id );

	if ( ! $order ) {
		return;
	}

	$has_recorded = $order->get_data_store()->get_recorded_coupon_usage_counts( $order );

	if ( $order->has_status( 'cancelled' ) && $has_recorded ) {
		$action = 'reduce';
		$order->get_data_store()->set_recorded_coupon_usage_counts( $order, false );
	} elseif ( ! $order->has_status( 'cancelled' ) && ! $has_recorded ) {
		$action = 'increase';
		$order->get_data_store()->set_recorded_coupon_usage_counts( $order, true );
	} elseif ( $order->has_status( 'cancelled' ) ) {
		$order->get_data_store()->release_held_coupons( $order, true );
		return;
	} else {
		return;
	}

	if ( count( $order->get_coupon_codes() ) > 0 ) {
		foreach ( $order->get_coupon_codes() as $code ) {
			if ( ! $code ) {
				continue;
			}

			$coupon  = new WC_Coupon( $code );
			$used_by = $order->get_user_id();

			if ( ! $used_by ) {
				$used_by = $order->get_billing_email();
			}

			switch ( $action ) {
				case 'reduce':
					$coupon->decrease_usage_count( $used_by );
					break;
				case 'increase':
					$coupon->increase_usage_count( $used_by, $order );
					break;
			}
		}
		$order->get_data_store()->release_held_coupons( $order, true );
	}
}
add_action( 'woocommerce_order_status_pending', 'wc_update_coupon_usage_counts' );
add_action( 'woocommerce_order_status_completed', 'wc_update_coupon_usage_counts' );
add_action( 'woocommerce_order_status_processing', 'wc_update_coupon_usage_counts' );
add_action( 'woocommerce_order_status_on-hold', 'wc_update_coupon_usage_counts' );
add_action( 'woocommerce_order_status_cancelled', 'wc_update_coupon_usage_counts' );

/**
 * Cancel all unpaid orders after held duration to prevent stock lock for those products.
 */
function wc_cancel_unpaid_orders() {
	$held_duration = get_option( 'woocommerce_hold_stock_minutes' );

	// Re-schedule the event before cancelling orders
	// this way in case of a DB timeout or (plugin) crash the event is always scheduled for retry.
	wp_clear_scheduled_hook( 'woocommerce_cancel_unpaid_orders' );
	$cancel_unpaid_interval = apply_filters( 'woocommerce_cancel_unpaid_orders_interval_minutes', absint( $held_duration ) );
	wp_schedule_single_event( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders' );

	if ( $held_duration < 1 || 'yes' !== get_option( 'woocommerce_manage_stock' ) ) {
		return;
	}

	$data_store    = WC_Data_Store::load( 'order' );
	$unpaid_orders = $data_store->get_unpaid_orders( strtotime( '-' . absint( $held_duration ) . ' MINUTES', current_time( 'timestamp' ) ) );

	if ( $unpaid_orders ) {
		foreach ( $unpaid_orders as $unpaid_order ) {
			$order = wc_get_order( $unpaid_order );

			if ( apply_filters( 'woocommerce_cancel_unpaid_order', 'checkout' === $order->get_created_via(), $order ) ) {
				$order->update_status( 'cancelled', __( 'Unpaid order cancelled - time limit reached.', 'woocommerce' ) );
			}
		}
	}
}
add_action( 'woocommerce_cancel_unpaid_orders', 'wc_cancel_unpaid_orders' );

/**
 * Sanitize order id removing unwanted characters.
 *
 * E.g Users can sometimes try to track an order id using # with no success.
 * This function will fix this.
 *
 * @since 3.1.0
 * @param int $order_id Order ID.
 */
function wc_sanitize_order_id( $order_id ) {
	return (int) filter_var( $order_id, FILTER_SANITIZE_NUMBER_INT );
}
add_filter( 'woocommerce_shortcode_order_tracking_order_id', 'wc_sanitize_order_id' );

/**
 * Get an order note.
 *
 * @since  3.2.0
 * @param  int|WP_Comment $data Note ID (or WP_Comment instance for internal use only).
 * @return stdClass|null        Object with order note details or null when does not exists.
 */
function wc_get_order_note( $data ) {
	if ( is_numeric( $data ) ) {
		$data = get_comment( $data );
	}

	if ( ! is_a( $data, 'WP_Comment' ) ) {
		return null;
	}

	return (object) apply_filters(
		'woocommerce_get_order_note',
		array(
			'id'            => (int) $data->comment_ID,
			'date_created'  => wc_string_to_datetime( $data->comment_date ),
			'content'       => $data->comment_content,
			'customer_note' => (bool) get_comment_meta( $data->comment_ID, 'is_customer_note', true ),
			'added_by'      => __( 'WooCommerce', 'woocommerce' ) === $data->comment_author ? 'system' : $data->comment_author,
		),
		$data
	);
}

/**
 * Get order notes.
 *
 * @since  3.2.0
 * @param  array $args Query arguments {
 *     Array of query parameters.
 *
 *     @type string $limit         Maximum number of notes to retrieve.
 *                                 Default empty (no limit).
 *     @type int    $order_id      Limit results to those affiliated with a given order ID.
 *                                 Default 0.
 *     @type array  $order__in     Array of order IDs to include affiliated notes for.
 *                                 Default empty.
 *     @type array  $order__not_in Array of order IDs to exclude affiliated notes for.
 *                                 Default empty.
 *     @type string $orderby       Define how should sort notes.
 *                                 Accepts 'date_created', 'date_created_gmt' or 'id'.
 *                                 Default: 'id'.
 *     @type string $order         How to order retrieved notes.
 *                                 Accepts 'ASC' or 'DESC'.
 *                                 Default: 'DESC'.
 *     @type string $type          Define what type of note should retrieve.
 *                                 Accepts 'customer', 'internal' or empty for both.
 *                                 Default empty.
 * }
 * @return stdClass[]              Array of stdClass objects with order notes details.
 */
function wc_get_order_notes( $args ) {
	$key_mapping = array(
		'limit'         => 'number',
		'order_id'      => 'post_id',
		'order__in'     => 'post__in',
		'order__not_in' => 'post__not_in',
	);

	foreach ( $key_mapping as $query_key => $db_key ) {
		if ( isset( $args[ $query_key ] ) ) {
			$args[ $db_key ] = $args[ $query_key ];
			unset( $args[ $query_key ] );
		}
	}

	// Define orderby.
	$orderby_mapping = array(
		'date_created'     => 'comment_date',
		'date_created_gmt' => 'comment_date_gmt',
		'id'               => 'comment_ID',
	);

	$args['orderby'] = ! empty( $args['orderby'] ) && in_array( $args['orderby'], array( 'date_created', 'date_created_gmt', 'id' ), true ) ? $orderby_mapping[ $args['orderby'] ] : 'comment_ID';

	// Set WooCommerce order type.
	if ( isset( $args['type'] ) && 'customer' === $args['type'] ) {
		$args['meta_query'] = array( // WPCS: slow query ok.
			array(
				'key'     => 'is_customer_note',
				'value'   => 1,
				'compare' => '=',
			),
		);
	} elseif ( isset( $args['type'] ) && 'internal' === $args['type'] ) {
		$args['meta_query'] = array( // WPCS: slow query ok.
			array(
				'key'     => 'is_customer_note',
				'compare' => 'NOT EXISTS',
			),
		);
	}

	// Set correct comment type.
	$args['type'] = 'order_note';

	// Always approved.
	$args['status'] = 'approve';

	// Does not support 'count' or 'fields'.
	unset( $args['count'], $args['fields'] );

	remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );

	$notes = get_comments( $args );

	add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );

	return array_filter( array_map( 'wc_get_order_note', $notes ) );
}

/**
 * Create an order note.
 *
 * @since  3.2.0
 * @param  int    $order_id         Order ID.
 * @param  string $note             Note to add.
 * @param  bool   $is_customer_note If is a costumer note.
 * @param  bool   $added_by_user    If note is create by an user.
 * @return int|WP_Error             Integer when created or WP_Error when found an error.
 */
function wc_create_order_note( $order_id, $note, $is_customer_note = false, $added_by_user = false ) {
	$order = wc_get_order( $order_id );

	if ( ! $order ) {
		return new WP_Error( 'invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 400 ) );
	}

	return $order->add_order_note( $note, (int) $is_customer_note, $added_by_user );
}

/**
 * Delete an order note.
 *
 * @since  3.2.0
 * @param  int $note_id Order note.
 * @return bool         True on success, false on failure.
 */
function wc_delete_order_note( $note_id ) {
	return wp_delete_comment( $note_id, true );
}
wc-order-item-functions.php000064400000012041151336044770011751 0ustar00<?php
/**
 * WooCommerce Order Item Functions
 *
 * Functions for order specific things.
 *
 * @package WooCommerce\Functions
 * @version 3.4.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Add a item to an order (for example a line item).
 *
 * @param int   $order_id   Order ID.
 * @param array $item_array Items list.
 *
 * @throws Exception        When `WC_Data_Store::load` validation fails.
 * @return int|bool         Item ID or false
 */
function wc_add_order_item( $order_id, $item_array ) {
	$order_id = absint( $order_id );

	if ( ! $order_id ) {
		return false;
	}

	$defaults = array(
		'order_item_name' => '',
		'order_item_type' => 'line_item',
	);

	$item_array = wp_parse_args( $item_array, $defaults );
	$data_store = WC_Data_Store::load( 'order-item' );
	$item_id    = $data_store->add_order_item( $order_id, $item_array );
	$item       = WC_Order_Factory::get_order_item( $item_id );

	do_action( 'woocommerce_new_order_item', $item_id, $item, $order_id );

	return $item_id;
}

/**
 * Update an item for an order.
 *
 * @since 2.2
 * @param int   $item_id Item ID.
 * @param array $args    Either `order_item_type` or `order_item_name`.
 *
 * @throws Exception     When `WC_Data_Store::load` validation fails.
 * @return bool          True if successfully updated, false otherwise.
 */
function wc_update_order_item( $item_id, $args ) {
	$data_store = WC_Data_Store::load( 'order-item' );
	$update     = $data_store->update_order_item( $item_id, $args );

	if ( false === $update ) {
		return false;
	}

	do_action( 'woocommerce_update_order_item', $item_id, $args );

	return true;
}

/**
 * Delete an item from the order it belongs to based on item id.
 *
 * @param int $item_id  Item ID.
 *
 * @throws Exception    When `WC_Data_Store::load` validation fails.
 * @return bool
 */
function wc_delete_order_item( $item_id ) {
	$item_id = absint( $item_id );

	if ( ! $item_id ) {
		return false;
	}

	$data_store = WC_Data_Store::load( 'order-item' );

	do_action( 'woocommerce_before_delete_order_item', $item_id );

	$data_store->delete_order_item( $item_id );

	do_action( 'woocommerce_delete_order_item', $item_id );

	return true;
}

/**
 * WooCommerce Order Item Meta API - Update term meta.
 *
 * @param int    $item_id    Item ID.
 * @param string $meta_key   Meta key.
 * @param string $meta_value Meta value.
 * @param string $prev_value Previous value (default: '').
 *
 * @throws Exception         When `WC_Data_Store::load` validation fails.
 * @return bool
 */
function wc_update_order_item_meta( $item_id, $meta_key, $meta_value, $prev_value = '' ) {
	$data_store = WC_Data_Store::load( 'order-item' );
	if ( $data_store->update_metadata( $item_id, $meta_key, $meta_value, $prev_value ) ) {
		WC_Cache_Helper::invalidate_cache_group( 'object_' . $item_id ); // Invalidate cache.
		return true;
	}
	return false;
}

/**
 * WooCommerce Order Item Meta API - Add term meta.
 *
 * @param int    $item_id    Item ID.
 * @param string $meta_key   Meta key.
 * @param string $meta_value Meta value.
 * @param bool   $unique     If meta data should be unique (default: false).
 *
 * @throws Exception         When `WC_Data_Store::load` validation fails.
 * @return int               New row ID or 0.
 */
function wc_add_order_item_meta( $item_id, $meta_key, $meta_value, $unique = false ) {
	$data_store = WC_Data_Store::load( 'order-item' );
	$meta_id    = $data_store->add_metadata( $item_id, $meta_key, $meta_value, $unique );

	if ( $meta_id ) {
		WC_Cache_Helper::invalidate_cache_group( 'object_' . $item_id ); // Invalidate cache.
		return $meta_id;
	}
	return 0;
}

/**
 * WooCommerce Order Item Meta API - Delete term meta.
 *
 * @param int    $item_id    Item ID.
 * @param string $meta_key   Meta key.
 * @param string $meta_value Meta value (default: '').
 * @param bool   $delete_all Delete all meta data, defaults to `false`.
 *
 * @throws Exception         When `WC_Data_Store::load` validation fails.
 * @return bool
 */
function wc_delete_order_item_meta( $item_id, $meta_key, $meta_value = '', $delete_all = false ) {
	$data_store = WC_Data_Store::load( 'order-item' );
	if ( $data_store->delete_metadata( $item_id, $meta_key, $meta_value, $delete_all ) ) {
		WC_Cache_Helper::invalidate_cache_group( 'object_' . $item_id ); // Invalidate cache.
		return true;
	}
	return false;
}

/**
 * WooCommerce Order Item Meta API - Get term meta.
 *
 * @param int    $item_id Item ID.
 * @param string $key     Meta key.
 * @param bool   $single  Whether to return a single value. (default: true).
 *
 * @throws Exception      When `WC_Data_Store::load` validation fails.
 * @return mixed
 */
function wc_get_order_item_meta( $item_id, $key, $single = true ) {
	$data_store = WC_Data_Store::load( 'order-item' );
	return $data_store->get_metadata( $item_id, $key, $single );
}

/**
 * Get order ID by order item ID.
 *
 * @param  int $item_id Item ID.
 *
 * @throws Exception    When `WC_Data_Store::load` validation fails.
 * @return int
 */
function wc_get_order_id_by_order_item_id( $item_id ) {
	$data_store = WC_Data_Store::load( 'order-item' );
	return $data_store->get_order_id_by_order_item_id( $item_id );
}
log-handlers/class-wc-log-handler-file.php000064400000027021151336044770014473 0ustar00<?php
/**
 * Class WC_Log_Handler_File file.
 *
 * @package WooCommerce\Log Handlers
 */

use Automattic\Jetpack\Constants;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Handles log entries by writing to a file.
 *
 * @class          WC_Log_Handler_File
 * @version        1.0.0
 * @package        WooCommerce\Classes\Log_Handlers
 */
class WC_Log_Handler_File extends WC_Log_Handler {

	/**
	 * Stores open file handles.
	 *
	 * @var array
	 */
	protected $handles = array();

	/**
	 * File size limit for log files in bytes.
	 *
	 * @var int
	 */
	protected $log_size_limit;

	/**
	 * Cache logs that could not be written.
	 *
	 * If a log is written too early in the request, pluggable functions may be unavailable. These
	 * logs will be cached and written on 'plugins_loaded' action.
	 *
	 * @var array
	 */
	protected $cached_logs = array();

	/**
	 * Constructor for the logger.
	 *
	 * @param int $log_size_limit Optional. Size limit for log files. Default 5mb.
	 */
	public function __construct( $log_size_limit = null ) {
		if ( null === $log_size_limit ) {
			$log_size_limit = 5 * 1024 * 1024;
		}

		$this->log_size_limit = apply_filters( 'woocommerce_log_file_size_limit', $log_size_limit );

		add_action( 'plugins_loaded', array( $this, 'write_cached_logs' ) );
	}

	/**
	 * Destructor.
	 *
	 * Cleans up open file handles.
	 */
	public function __destruct() {
		foreach ( $this->handles as $handle ) {
			if ( is_resource( $handle ) ) {
				fclose( $handle ); // @codingStandardsIgnoreLine.
			}
		}
	}

	/**
	 * Handle a log entry.
	 *
	 * @param int    $timestamp Log timestamp.
	 * @param string $level emergency|alert|critical|error|warning|notice|info|debug.
	 * @param string $message Log message.
	 * @param array  $context {
	 *      Additional information for log handlers.
	 *
	 *     @type string $source Optional. Determines log file to write to. Default 'log'.
	 *     @type bool $_legacy Optional. Default false. True to use outdated log format
	 *         originally used in deprecated WC_Logger::add calls.
	 * }
	 *
	 * @return bool False if value was not handled and true if value was handled.
	 */
	public function handle( $timestamp, $level, $message, $context ) {

		if ( isset( $context['source'] ) && $context['source'] ) {
			$handle = $context['source'];
		} else {
			$handle = 'log';
		}

		$entry = self::format_entry( $timestamp, $level, $message, $context );

		return $this->add( $entry, $handle );
	}

	/**
	 * Builds a log entry text from timestamp, level and message.
	 *
	 * @param int    $timestamp Log timestamp.
	 * @param string $level emergency|alert|critical|error|warning|notice|info|debug.
	 * @param string $message Log message.
	 * @param array  $context Additional information for log handlers.
	 *
	 * @return string Formatted log entry.
	 */
	protected static function format_entry( $timestamp, $level, $message, $context ) {

		if ( isset( $context['_legacy'] ) && true === $context['_legacy'] ) {
			if ( isset( $context['source'] ) && $context['source'] ) {
				$handle = $context['source'];
			} else {
				$handle = 'log';
			}
			$message = apply_filters( 'woocommerce_logger_add_message', $message, $handle );
			$time    = date_i18n( 'm-d-Y @ H:i:s' );
			$entry   = "{$time} - {$message}";
		} else {
			$entry = parent::format_entry( $timestamp, $level, $message, $context );
		}

		return $entry;
	}

	/**
	 * Open log file for writing.
	 *
	 * @param string $handle Log handle.
	 * @param string $mode Optional. File mode. Default 'a'.
	 * @return bool Success.
	 */
	protected function open( $handle, $mode = 'a' ) {
		if ( $this->is_open( $handle ) ) {
			return true;
		}

		$file = self::get_log_file_path( $handle );

		if ( $file ) {
			if ( ! file_exists( $file ) ) {
				$temphandle = @fopen( $file, 'w+' ); // @codingStandardsIgnoreLine.
				if ( is_resource( $temphandle ) ) {
					@fclose( $temphandle ); // @codingStandardsIgnoreLine.

					if ( Constants::is_defined( 'FS_CHMOD_FILE' ) ) {
						@chmod( $file, FS_CHMOD_FILE ); // @codingStandardsIgnoreLine.
					}
				}
			}

			$resource = @fopen( $file, $mode ); // @codingStandardsIgnoreLine.

			if ( $resource ) {
				$this->handles[ $handle ] = $resource;
				return true;
			}
		}

		return false;
	}

	/**
	 * Check if a handle is open.
	 *
	 * @param string $handle Log handle.
	 * @return bool True if $handle is open.
	 */
	protected function is_open( $handle ) {
		return array_key_exists( $handle, $this->handles ) && is_resource( $this->handles[ $handle ] );
	}

	/**
	 * Close a handle.
	 *
	 * @param string $handle Log handle.
	 * @return bool success
	 */
	protected function close( $handle ) {
		$result = false;

		if ( $this->is_open( $handle ) ) {
			$result = fclose( $this->handles[ $handle ] ); // @codingStandardsIgnoreLine.
			unset( $this->handles[ $handle ] );
		}

		return $result;
	}

	/**
	 * Add a log entry to chosen file.
	 *
	 * @param string $entry Log entry text.
	 * @param string $handle Log entry handle.
	 *
	 * @return bool True if write was successful.
	 */
	protected function add( $entry, $handle ) {
		$result = false;

		if ( $this->should_rotate( $handle ) ) {
			$this->log_rotate( $handle );
		}

		if ( $this->open( $handle ) && is_resource( $this->handles[ $handle ] ) ) {
			$result = fwrite( $this->handles[ $handle ], $entry . PHP_EOL ); // @codingStandardsIgnoreLine.
		} else {
			$this->cache_log( $entry, $handle );
		}

		return false !== $result;
	}

	/**
	 * Clear entries from chosen file.
	 *
	 * @param string $handle Log handle.
	 *
	 * @return bool
	 */
	public function clear( $handle ) {
		$result = false;

		// Close the file if it's already open.
		$this->close( $handle );

		/**
		 * $this->open( $handle, 'w' ) == Open the file for writing only. Place the file pointer at
		 * the beginning of the file, and truncate the file to zero length.
		 */
		if ( $this->open( $handle, 'w' ) && is_resource( $this->handles[ $handle ] ) ) {
			$result = true;
		}

		do_action( 'woocommerce_log_clear', $handle );

		return $result;
	}

	/**
	 * Remove/delete the chosen file.
	 *
	 * @param string $handle Log handle.
	 *
	 * @return bool
	 */
	public function remove( $handle ) {
		$removed = false;
		$logs    = $this->get_log_files();
		$handle  = sanitize_title( $handle );

		if ( isset( $logs[ $handle ] ) && $logs[ $handle ] ) {
			$file = realpath( trailingslashit( WC_LOG_DIR ) . $logs[ $handle ] );
			if ( 0 === stripos( $file, realpath( trailingslashit( WC_LOG_DIR ) ) ) && is_file( $file ) && is_writable( $file ) ) { // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writable
				$this->close( $file ); // Close first to be certain no processes keep it alive after it is unlinked.
				$removed = unlink( $file ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_unlink
			}
			do_action( 'woocommerce_log_remove', $handle, $removed );
		}
		return $removed;
	}

	/**
	 * Check if log file should be rotated.
	 *
	 * Compares the size of the log file to determine whether it is over the size limit.
	 *
	 * @param string $handle Log handle.
	 * @return bool True if if should be rotated.
	 */
	protected function should_rotate( $handle ) {
		$file = self::get_log_file_path( $handle );
		if ( $file ) {
			if ( $this->is_open( $handle ) ) {
				$file_stat = fstat( $this->handles[ $handle ] );
				return $file_stat['size'] > $this->log_size_limit;
			} elseif ( file_exists( $file ) ) {
				return filesize( $file ) > $this->log_size_limit;
			} else {
				return false;
			}
		} else {
			return false;
		}
	}

	/**
	 * Rotate log files.
	 *
	 * Logs are rotated by prepending '.x' to the '.log' suffix.
	 * The current log plus 10 historical logs are maintained.
	 * For example:
	 *     base.9.log -> [ REMOVED ]
	 *     base.8.log -> base.9.log
	 *     ...
	 *     base.0.log -> base.1.log
	 *     base.log   -> base.0.log
	 *
	 * @param string $handle Log handle.
	 */
	protected function log_rotate( $handle ) {
		for ( $i = 8; $i >= 0; $i-- ) {
			$this->increment_log_infix( $handle, $i );
		}
		$this->increment_log_infix( $handle );
	}

	/**
	 * Increment a log file suffix.
	 *
	 * @param string   $handle Log handle.
	 * @param null|int $number Optional. Default null. Log suffix number to be incremented.
	 * @return bool True if increment was successful, otherwise false.
	 */
	protected function increment_log_infix( $handle, $number = null ) {
		if ( null === $number ) {
			$suffix      = '';
			$next_suffix = '.0';
		} else {
			$suffix      = '.' . $number;
			$next_suffix = '.' . ( $number + 1 );
		}

		$rename_from = self::get_log_file_path( "{$handle}{$suffix}" );
		$rename_to   = self::get_log_file_path( "{$handle}{$next_suffix}" );

		if ( $this->is_open( $rename_from ) ) {
			$this->close( $rename_from );
		}

		if ( is_writable( $rename_from ) ) { // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writable
			return rename( $rename_from, $rename_to ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_rename
		} else {
			return false;
		}

	}

	/**
	 * Get a log file path.
	 *
	 * @param string $handle Log name.
	 * @return bool|string The log file path or false if path cannot be determined.
	 */
	public static function get_log_file_path( $handle ) {
		if ( function_exists( 'wp_hash' ) ) {
			return trailingslashit( WC_LOG_DIR ) . self::get_log_file_name( $handle );
		} else {
			wc_doing_it_wrong( __METHOD__, __( 'This method should not be called before plugins_loaded.', 'woocommerce' ), '3.0' );
			return false;
		}
	}

	/**
	 * Get a log file name.
	 *
	 * File names consist of the handle, followed by the date, followed by a hash, .log.
	 *
	 * @since 3.3
	 * @param string $handle Log name.
	 * @return bool|string The log file name or false if cannot be determined.
	 */
	public static function get_log_file_name( $handle ) {
		if ( function_exists( 'wp_hash' ) ) {
			$date_suffix = date( 'Y-m-d', time() );
			$hash_suffix = wp_hash( $handle );
			return sanitize_file_name( implode( '-', array( $handle, $date_suffix, $hash_suffix ) ) . '.log' );
		} else {
			wc_doing_it_wrong( __METHOD__, __( 'This method should not be called before plugins_loaded.', 'woocommerce' ), '3.3' );
			return false;
		}
	}

	/**
	 * Cache log to write later.
	 *
	 * @param string $entry Log entry text.
	 * @param string $handle Log entry handle.
	 */
	protected function cache_log( $entry, $handle ) {
		$this->cached_logs[] = array(
			'entry'  => $entry,
			'handle' => $handle,
		);
	}

	/**
	 * Write cached logs.
	 */
	public function write_cached_logs() {
		foreach ( $this->cached_logs as $log ) {
			$this->add( $log['entry'], $log['handle'] );
		}
	}

	/**
	 * Delete all logs older than a defined timestamp.
	 *
	 * @since 3.4.0
	 * @param integer $timestamp Timestamp to delete logs before.
	 */
	public static function delete_logs_before_timestamp( $timestamp = 0 ) {
		if ( ! $timestamp ) {
			return;
		}

		$log_files = self::get_log_files();

		foreach ( $log_files as $log_file ) {
			$last_modified = filemtime( trailingslashit( WC_LOG_DIR ) . $log_file );

			if ( $last_modified < $timestamp ) {
				@unlink( trailingslashit( WC_LOG_DIR ) . $log_file ); // @codingStandardsIgnoreLine.
			}
		}
	}

	/**
	 * Get all log files in the log directory.
	 *
	 * @since 3.4.0
	 * @return array
	 */
	public static function get_log_files() {
		$files  = @scandir( WC_LOG_DIR ); // @codingStandardsIgnoreLine.
		$result = array();

		if ( ! empty( $files ) ) {
			foreach ( $files as $key => $value ) {
				if ( ! in_array( $value, array( '.', '..' ), true ) ) {
					if ( ! is_dir( $value ) && strstr( $value, '.log' ) ) {
						$result[ sanitize_title( $value ) ] = $value;
					}
				}
			}
		}

		return $result;
	}
}
log-handlers/class-wc-log-handler-email.php000064400000013405151336044770014644 0ustar00<?php
/**
 * Class WC_Log_Handler_Email file.
 *
 * @package WooCommerce\Log Handlers
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Handles log entries by sending an email.
 *
 * WARNING!
 * This log handler has known limitations.
 *
 * Log messages are aggregated and sent once per request (if necessary). If the site experiences a
 * problem, the log email may never be sent. This handler should be used with another handler which
 * stores logs in order to prevent loss.
 *
 * It is not recommended to use this handler on a high traffic site. There will be a maximum of 1
 * email sent per request per handler, but that could still be a dangerous amount of emails under
 * heavy traffic. Do not confuse this handler with an appropriate monitoring solution!
 *
 * If you understand these limitations, feel free to use this handler or borrow parts of the design
 * to implement your own!
 *
 * @class          WC_Log_Handler_Email
 * @version        1.0.0
 * @package        WooCommerce\Classes\Log_Handlers
 */
class WC_Log_Handler_Email extends WC_Log_Handler {

	/**
	 * Minimum log level this handler will process.
	 *
	 * @var int Integer representation of minimum log level to handle.
	 */
	protected $threshold;

	/**
	 * Stores email recipients.
	 *
	 * @var array
	 */
	protected $recipients = array();

	/**
	 * Stores log messages.
	 *
	 * @var array
	 */
	protected $logs = array();

	/**
	 * Stores integer representation of maximum logged level.
	 *
	 * @var int
	 */
	protected $max_severity = null;

	/**
	 * Constructor for log handler.
	 *
	 * @param string|array $recipients Optional. Email(s) to receive log messages. Defaults to site admin email.
	 * @param string       $threshold Optional. Minimum level that should receive log messages.
	 *           Default 'alert'. One of: emergency|alert|critical|error|warning|notice|info|debug.
	 */
	public function __construct( $recipients = null, $threshold = 'alert' ) {
		if ( null === $recipients ) {
			$recipients = get_option( 'admin_email' );
		}

		if ( is_array( $recipients ) ) {
			foreach ( $recipients as $recipient ) {
				$this->add_email( $recipient );
			}
		} else {
			$this->add_email( $recipients );
		}

		$this->set_threshold( $threshold );
		add_action( 'shutdown', array( $this, 'send_log_email' ) );
	}

	/**
	 * Set handler severity threshold.
	 *
	 * @param string $level emergency|alert|critical|error|warning|notice|info|debug.
	 */
	public function set_threshold( $level ) {
		$this->threshold = WC_Log_Levels::get_level_severity( $level );
	}

	/**
	 * Determine whether handler should handle log.
	 *
	 * @param string $level emergency|alert|critical|error|warning|notice|info|debug.
	 * @return bool True if the log should be handled.
	 */
	protected function should_handle( $level ) {
		return $this->threshold <= WC_Log_Levels::get_level_severity( $level );
	}

	/**
	 * Handle a log entry.
	 *
	 * @param int    $timestamp Log timestamp.
	 * @param string $level emergency|alert|critical|error|warning|notice|info|debug.
	 * @param string $message Log message.
	 * @param array  $context Optional. Additional information for log handlers.
	 *
	 * @return bool False if value was not handled and true if value was handled.
	 */
	public function handle( $timestamp, $level, $message, $context ) {

		if ( $this->should_handle( $level ) ) {
			$this->add_log( $timestamp, $level, $message, $context );
			return true;
		}

		return false;
	}

	/**
	 * Send log email.
	 *
	 * @return bool True if email is successfully sent otherwise false.
	 */
	public function send_log_email() {
		$result = false;

		if ( ! empty( $this->logs ) ) {
			$subject = $this->get_subject();
			$body    = $this->get_body();
			$result  = wp_mail( $this->recipients, $subject, $body );
			$this->clear_logs();
		}

		return $result;
	}

	/**
	 * Build subject for log email.
	 *
	 * @return string subject
	 */
	protected function get_subject() {
		$site_name = get_bloginfo( 'name' );
		$max_level = strtoupper( WC_Log_Levels::get_severity_level( $this->max_severity ) );
		$log_count = count( $this->logs );

		return sprintf(
			/* translators: 1: Site name 2: Maximum level 3: Log count */
			_n(
				'[%1$s] %2$s: %3$s WooCommerce log message',
				'[%1$s] %2$s: %3$s WooCommerce log messages',
				$log_count,
				'woocommerce'
			),
			$site_name,
			$max_level,
			$log_count
		);
	}

	/**
	 * Build body for log email.
	 *
	 * @return string body
	 */
	protected function get_body() {
		$site_name = get_bloginfo( 'name' );
		$entries   = implode( PHP_EOL, $this->logs );
		$log_count = count( $this->logs );
		return _n(
			'You have received the following WooCommerce log message:',
			'You have received the following WooCommerce log messages:',
			$log_count,
			'woocommerce'
		) . PHP_EOL
			. PHP_EOL
			. $entries
			. PHP_EOL
			. PHP_EOL
			/* translators: %s: Site name */
			. sprintf( __( 'Visit %s admin area:', 'woocommerce' ), $site_name )
			. PHP_EOL
			. admin_url();
	}

	/**
	 * Adds an email to the list of recipients.
	 *
	 * @param string $email Email address to add.
	 */
	public function add_email( $email ) {
		array_push( $this->recipients, $email );
	}

	/**
	 * Add log message.
	 *
	 * @param int    $timestamp Log timestamp.
	 * @param string $level emergency|alert|critical|error|warning|notice|info|debug.
	 * @param string $message Log message.
	 * @param array  $context Additional information for log handlers.
	 */
	protected function add_log( $timestamp, $level, $message, $context ) {
		$this->logs[] = $this->format_entry( $timestamp, $level, $message, $context );

		$log_severity = WC_Log_Levels::get_level_severity( $level );
		if ( $this->max_severity < $log_severity ) {
			$this->max_severity = $log_severity;
		}
	}

	/**
	 * Clear log messages.
	 */
	protected function clear_logs() {
		$this->logs = array();
	}

}
log-handlers/class-wc-log-handler-db.php000064400000011503151336044770014137 0ustar00<?php
/**
 * Class WC_Log_Handler_DB file.
 *
 * @package WooCommerce\Log Handlers
 */

use Automattic\Jetpack\Constants;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Handles log entries by writing to database.
 *
 * @class          WC_Log_Handler_DB
 * @version        1.0.0
 * @package        WooCommerce\Classes\Log_Handlers
 */
class WC_Log_Handler_DB extends WC_Log_Handler {

	/**
	 * Handle a log entry.
	 *
	 * @param int    $timestamp Log timestamp.
	 * @param string $level emergency|alert|critical|error|warning|notice|info|debug.
	 * @param string $message Log message.
	 * @param array  $context {
	 *      Additional information for log handlers.
	 *
	 *     @type string $source Optional. Source will be available in log table.
	 *                  If no source is provided, attempt to provide sensible default.
	 * }
	 *
	 * @see WC_Log_Handler_DB::get_log_source() for default source.
	 *
	 * @return bool False if value was not handled and true if value was handled.
	 */
	public function handle( $timestamp, $level, $message, $context ) {

		if ( isset( $context['source'] ) && $context['source'] ) {
			$source = $context['source'];
		} else {
			$source = $this->get_log_source();
		}

		return $this->add( $timestamp, $level, $message, $source, $context );
	}

	/**
	 * Add a log entry to chosen file.
	 *
	 * @param int    $timestamp Log timestamp.
	 * @param string $level emergency|alert|critical|error|warning|notice|info|debug.
	 * @param string $message Log message.
	 * @param string $source Log source. Useful for filtering and sorting.
	 * @param array  $context Context will be serialized and stored in database.
	 *
	 * @return bool True if write was successful.
	 */
	protected static function add( $timestamp, $level, $message, $source, $context ) {
		global $wpdb;

		$insert = array(
			'timestamp' => date( 'Y-m-d H:i:s', $timestamp ),
			'level'     => WC_Log_Levels::get_level_severity( $level ),
			'message'   => $message,
			'source'    => $source,
		);

		$format = array(
			'%s',
			'%d',
			'%s',
			'%s',
			'%s', // possible serialized context.
		);

		if ( ! empty( $context ) ) {
			$insert['context'] = serialize( $context ); // @codingStandardsIgnoreLine.
		}

		return false !== $wpdb->insert( "{$wpdb->prefix}woocommerce_log", $insert, $format );
	}

	/**
	 * Clear all logs from the DB.
	 *
	 * @return bool True if flush was successful.
	 */
	public static function flush() {
		global $wpdb;

		return $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}woocommerce_log" );
	}

	/**
	 * Clear entries for a chosen handle/source.
	 *
	 * @param string $source Log source.
	 * @return bool
	 */
	public function clear( $source ) {
		global $wpdb;

		return $wpdb->query(
			$wpdb->prepare(
				"DELETE FROM {$wpdb->prefix}woocommerce_log WHERE source = %s",
				$source
			)
		);
	}

	/**
	 * Delete selected logs from DB.
	 *
	 * @param int|string|array $log_ids Log ID or array of Log IDs to be deleted.
	 *
	 * @return bool
	 */
	public static function delete( $log_ids ) {
		global $wpdb;

		if ( ! is_array( $log_ids ) ) {
			$log_ids = array( $log_ids );
		}

		$format   = array_fill( 0, count( $log_ids ), '%d' );
		$query_in = '(' . implode( ',', $format ) . ')';
		return $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_log WHERE log_id IN {$query_in}", $log_ids ) ); // @codingStandardsIgnoreLine.
	}

	/**
	 * Delete all logs older than a defined timestamp.
	 *
	 * @since 3.4.0
	 * @param integer $timestamp Timestamp to delete logs before.
	 */
	public static function delete_logs_before_timestamp( $timestamp = 0 ) {
		if ( ! $timestamp ) {
			return;
		}

		global $wpdb;

		$wpdb->query(
			$wpdb->prepare(
				"DELETE FROM {$wpdb->prefix}woocommerce_log WHERE timestamp < %s",
				date( 'Y-m-d H:i:s', $timestamp )
			)
		);
	}

	/**
	 * Get appropriate source based on file name.
	 *
	 * Try to provide an appropriate source in case none is provided.
	 *
	 * @return string Text to use as log source. "" (empty string) if none is found.
	 */
	protected static function get_log_source() {
		static $ignore_files = array( 'class-wc-log-handler-db', 'class-wc-logger' );

		/**
		 * PHP < 5.3.6 correct behavior
		 *
		 * @see http://php.net/manual/en/function.debug-backtrace.php#refsect1-function.debug-backtrace-parameters
		 */
		if ( Constants::is_defined( 'DEBUG_BACKTRACE_IGNORE_ARGS' ) ) {
			$debug_backtrace_arg = DEBUG_BACKTRACE_IGNORE_ARGS; // phpcs:ignore PHPCompatibility.Constants.NewConstants.debug_backtrace_ignore_argsFound
		} else {
			$debug_backtrace_arg = false;
		}

		$trace = debug_backtrace( $debug_backtrace_arg ); // @codingStandardsIgnoreLine.
		foreach ( $trace as $t ) {
			if ( isset( $t['file'] ) ) {
				$filename = pathinfo( $t['file'], PATHINFO_FILENAME );
				if ( ! in_array( $filename, $ignore_files, true ) ) {
					return $filename;
				}
			}
		}

		return '';
	}

}
class-wc-order-item-tax.php000064400000014646151336044770011655 0ustar00<?php
/**
 * Order Line Item (tax)
 *
 * @package WooCommerce\Classes
 * @version 3.0.0
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Order item tax.
 */
class WC_Order_Item_Tax extends WC_Order_Item {

	/**
	 * Order Data array. This is the core order data exposed in APIs since 3.0.0.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $extra_data = array(
		'rate_code'          => '',
		'rate_id'            => 0,
		'label'              => '',
		'compound'           => false,
		'tax_total'          => 0,
		'shipping_tax_total' => 0,
		'rate_percent'       => null,
	);

	/*
	|--------------------------------------------------------------------------
	| Setters
	|--------------------------------------------------------------------------
	*/

	/**
	 * Set order item name.
	 *
	 * @param string $value Name.
	 */
	public function set_name( $value ) {
		$this->set_rate_code( $value );
	}

	/**
	 * Set item name.
	 *
	 * @param string $value Rate code.
	 */
	public function set_rate_code( $value ) {
		$this->set_prop( 'rate_code', wc_clean( $value ) );
	}

	/**
	 * Set item name.
	 *
	 * @param string $value Label.
	 */
	public function set_label( $value ) {
		$this->set_prop( 'label', wc_clean( $value ) );
	}

	/**
	 * Set tax rate id.
	 *
	 * @param int $value Rate ID.
	 */
	public function set_rate_id( $value ) {
		$this->set_prop( 'rate_id', absint( $value ) );
	}

	/**
	 * Set tax total.
	 *
	 * @param string $value Tax total.
	 */
	public function set_tax_total( $value ) {
		$this->set_prop( 'tax_total', $value ? wc_format_decimal( $value ) : 0 );
	}

	/**
	 * Set shipping tax total.
	 *
	 * @param string $value Shipping tax total.
	 */
	public function set_shipping_tax_total( $value ) {
		$this->set_prop( 'shipping_tax_total', $value ? wc_format_decimal( $value ) : 0 );
	}

	/**
	 * Set compound.
	 *
	 * @param bool $value If tax is compound.
	 */
	public function set_compound( $value ) {
		$this->set_prop( 'compound', (bool) $value );
	}

	/**
	 * Set rate value.
	 *
	 * @param float $value tax rate value.
	 */
	public function set_rate_percent( $value ) {
		$this->set_prop( 'rate_percent', (float) $value );
	}

	/**
	 * Set properties based on passed in tax rate by ID.
	 *
	 * @param int $tax_rate_id Tax rate ID.
	 */
	public function set_rate( $tax_rate_id ) {
		$tax_rate = WC_Tax::_get_tax_rate( $tax_rate_id, OBJECT );

		$this->set_rate_id( $tax_rate_id );
		$this->set_rate_code( WC_Tax::get_rate_code( $tax_rate ) );
		$this->set_label( WC_Tax::get_rate_label( $tax_rate ) );
		$this->set_compound( WC_Tax::is_compound( $tax_rate ) );
		$this->set_rate_percent( WC_Tax::get_rate_percent_value( $tax_rate ) );
	}

	/*
	|--------------------------------------------------------------------------
	| Getters
	|--------------------------------------------------------------------------
	*/

	/**
	 * Get order item type.
	 *
	 * @return string
	 */
	public function get_type() {
		return 'tax';
	}

	/**
	 * Get rate code/name.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_name( $context = 'view' ) {
		return $this->get_rate_code( $context );
	}

	/**
	 * Get rate code/name.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_rate_code( $context = 'view' ) {
		return $this->get_prop( 'rate_code', $context );
	}

	/**
	 * Get label.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_label( $context = 'view' ) {
		$label = $this->get_prop( 'label', $context );
		if ( 'view' === $context ) {
			return $label ? $label : __( 'Tax', 'woocommerce' );
		} else {
			return $label;
		}
	}

	/**
	 * Get tax rate ID.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return int
	 */
	public function get_rate_id( $context = 'view' ) {
		return $this->get_prop( 'rate_id', $context );
	}

	/**
	 * Get tax_total
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_tax_total( $context = 'view' ) {
		return $this->get_prop( 'tax_total', $context );
	}

	/**
	 * Get shipping_tax_total
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_shipping_tax_total( $context = 'view' ) {
		return $this->get_prop( 'shipping_tax_total', $context );
	}

	/**
	 * Get compound.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return bool
	 */
	public function get_compound( $context = 'view' ) {
		return $this->get_prop( 'compound', $context );
	}

	/**
	 * Is this a compound tax rate?
	 *
	 * @return boolean
	 */
	public function is_compound() {
		return $this->get_compound();
	}

	/**
	 * Get rate value
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return float
	 */
	public function get_rate_percent( $context = 'view' ) {
		return $this->get_prop( 'rate_percent', $context );
	}

	/*
	|--------------------------------------------------------------------------
	| Array Access Methods
	|--------------------------------------------------------------------------
	|
	| For backwards compatibility with legacy arrays.
	|
	*/

	/**
	 * O for ArrayAccess/Backwards compatibility.
	 *
	 * @param string $offset Offset.
	 * @return mixed
	 */
	public function offsetGet( $offset ) {
		if ( 'tax_amount' === $offset ) {
			$offset = 'tax_total';
		} elseif ( 'shipping_tax_amount' === $offset ) {
			$offset = 'shipping_tax_total';
		}
		return parent::offsetGet( $offset );
	}

	/**
	 * OffsetSet for ArrayAccess/Backwards compatibility.
	 *
	 * @deprecated 4.4.0
	 * @param string $offset Offset.
	 * @param mixed  $value  Value.
	 */
	public function offsetSet( $offset, $value ) {
		wc_deprecated_function( 'WC_Order_Item_Tax::offsetSet', '4.4.0', '' );
		if ( 'tax_amount' === $offset ) {
			$offset = 'tax_total';
		} elseif ( 'shipping_tax_amount' === $offset ) {
			$offset = 'shipping_tax_total';
		}
		parent::offsetSet( $offset, $value );
	}

	/**
	 * OffsetExists for ArrayAccess.
	 *
	 * @param string $offset Offset.
	 * @return bool
	 */
	public function offsetExists( $offset ) {
		if ( in_array( $offset, array( 'tax_amount', 'shipping_tax_amount' ), true ) ) {
			return true;
		}
		return parent::offsetExists( $offset );
	}
}
class-wc-cart-session.php000064400000035270151336044770011422 0ustar00<?php
/**
 * Cart session handling class.
 *
 * @package WooCommerce\Classes
 * @version 3.2.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC_Cart_Session class.
 *
 * @since 3.2.0
 */
final class WC_Cart_Session {

	/**
	 * Reference to cart object.
	 *
	 * @since 3.2.0
	 * @var WC_Cart
	 */
	protected $cart;

	/**
	 * Sets up the items provided, and calculate totals.
	 *
	 * @since 3.2.0
	 * @throws Exception If missing WC_Cart object.
	 *
	 * @param WC_Cart $cart Cart object to calculate totals for.
	 */
	public function __construct( &$cart ) {
		if ( ! is_a( $cart, 'WC_Cart' ) ) {
			throw new Exception( 'A valid WC_Cart object is required' );
		}

		$this->cart = $cart;
	}

	/**
	 * Register methods for this object on the appropriate WordPress hooks.
	 */
	public function init() {
		add_action( 'wp_loaded', array( $this, 'get_cart_from_session' ) );
		add_action( 'woocommerce_cart_emptied', array( $this, 'destroy_cart_session' ) );
		add_action( 'woocommerce_after_calculate_totals', array( $this, 'set_session' ) );
		add_action( 'woocommerce_cart_loaded_from_session', array( $this, 'set_session' ) );
		add_action( 'woocommerce_removed_coupon', array( $this, 'set_session' ) );

		// Persistent cart stored to usermeta.
		add_action( 'woocommerce_add_to_cart', array( $this, 'persistent_cart_update' ) );
		add_action( 'woocommerce_cart_item_removed', array( $this, 'persistent_cart_update' ) );
		add_action( 'woocommerce_cart_item_restored', array( $this, 'persistent_cart_update' ) );
		add_action( 'woocommerce_cart_item_set_quantity', array( $this, 'persistent_cart_update' ) );

		// Cookie events - cart cookies need to be set before headers are sent.
		add_action( 'woocommerce_add_to_cart', array( $this, 'maybe_set_cart_cookies' ) );
		add_action( 'wp', array( $this, 'maybe_set_cart_cookies' ), 99 );
		add_action( 'shutdown', array( $this, 'maybe_set_cart_cookies' ), 0 );
	}

	/**
	 * Get the cart data from the PHP session and store it in class variables.
	 *
	 * @since 3.2.0
	 */
	public function get_cart_from_session() {
		do_action( 'woocommerce_load_cart_from_session' );
		$this->cart->set_totals( WC()->session->get( 'cart_totals', null ) );
		$this->cart->set_applied_coupons( WC()->session->get( 'applied_coupons', array() ) );
		$this->cart->set_coupon_discount_totals( WC()->session->get( 'coupon_discount_totals', array() ) );
		$this->cart->set_coupon_discount_tax_totals( WC()->session->get( 'coupon_discount_tax_totals', array() ) );
		$this->cart->set_removed_cart_contents( WC()->session->get( 'removed_cart_contents', array() ) );

		$update_cart_session = false; // Flag to indicate the stored cart should be updated.
		$order_again         = false; // Flag to indicate whether this is a re-order.
		$cart                = WC()->session->get( 'cart', null );
		$merge_saved_cart    = (bool) get_user_meta( get_current_user_id(), '_woocommerce_load_saved_cart_after_login', true );

		// Merge saved cart with current cart.
		if ( is_null( $cart ) || $merge_saved_cart ) {
			$saved_cart          = $this->get_saved_cart();
			$cart                = is_null( $cart ) ? array() : $cart;
			$cart                = array_merge( $saved_cart, $cart );
			$update_cart_session = true;

			delete_user_meta( get_current_user_id(), '_woocommerce_load_saved_cart_after_login' );
		}

		// Populate cart from order.
		if ( isset( $_GET['order_again'], $_GET['_wpnonce'] ) && is_user_logged_in() && wp_verify_nonce( wp_unslash( $_GET['_wpnonce'] ), 'woocommerce-order_again' ) ) { // WPCS: input var ok, sanitization ok.
			$cart        = $this->populate_cart_from_order( absint( $_GET['order_again'] ), $cart ); // WPCS: input var ok.
			$order_again = true;
		}

		// Prime caches to reduce future queries.
		if ( is_callable( '_prime_post_caches' ) ) {
			_prime_post_caches( wp_list_pluck( $cart, 'product_id' ) );
		}

		$cart_contents = array();

		foreach ( $cart as $key => $values ) {
			if ( ! is_customize_preview() && 'customize-preview' === $key ) {
				continue;
			}

			$product = wc_get_product( $values['variation_id'] ? $values['variation_id'] : $values['product_id'] );

			if ( empty( $product ) || ! $product->exists() || 0 >= $values['quantity'] ) {
				continue;
			}

			/**
			 * Allow 3rd parties to validate this item before it's added to cart and add their own notices.
			 *
			 * @since 3.6.0
			 *
			 * @param bool $remove_cart_item_from_session If true, the item will not be added to the cart. Default: false.
			 * @param string $key Cart item key.
			 * @param array $values Cart item values e.g. quantity and product_id.
			 */
			if ( apply_filters( 'woocommerce_pre_remove_cart_item_from_session', false, $key, $values ) ) {
				$update_cart_session = true;
				do_action( 'woocommerce_remove_cart_item_from_session', $key, $values );

			} elseif ( ! $product->is_purchasable() ) {
				$update_cart_session = true;
				/* translators: %s: product name */
				$message = sprintf( __( '%s has been removed from your cart because it can no longer be purchased. Please contact us if you need assistance.', 'woocommerce' ), $product->get_name() );
				/**
				 * Filter message about item removed from the cart.
				 *
				 * @since 3.8.0
				 * @param string     $message Message.
				 * @param WC_Product $product Product data.
				 */
				$message = apply_filters( 'woocommerce_cart_item_removed_message', $message, $product );
				wc_add_notice( $message, 'error' );
				do_action( 'woocommerce_remove_cart_item_from_session', $key, $values );

			} elseif ( ! empty( $values['data_hash'] ) && ! hash_equals( $values['data_hash'], wc_get_cart_item_data_hash( $product ) ) ) { // phpcs:ignore PHPCompatibility.PHP.NewFunctions.hash_equalsFound
				$update_cart_session = true;
				/* translators: %1$s: product name. %2$s product permalink */
				wc_add_notice( sprintf( __( '%1$s has been removed from your cart because it has since been modified. You can add it back to your cart <a href="%2$s">here</a>.', 'woocommerce' ), $product->get_name(), $product->get_permalink() ), 'notice' );
				do_action( 'woocommerce_remove_cart_item_from_session', $key, $values );

			} else {
				// Put session data into array. Run through filter so other plugins can load their own session data.
				$session_data = array_merge(
					$values,
					array(
						'data' => $product,
					)
				);

				$cart_contents[ $key ] = apply_filters( 'woocommerce_get_cart_item_from_session', $session_data, $values, $key );

				// Add to cart right away so the product is visible in woocommerce_get_cart_item_from_session hook.
				$this->cart->set_cart_contents( $cart_contents );
			}
		}

		// If it's not empty, it's been already populated by the loop above.
		if ( ! empty( $cart_contents ) ) {
			$this->cart->set_cart_contents( apply_filters( 'woocommerce_cart_contents_changed', $cart_contents ) );
		}

		do_action( 'woocommerce_cart_loaded_from_session', $this->cart );

		if ( $update_cart_session || is_null( WC()->session->get( 'cart_totals', null ) ) ) {
			WC()->session->set( 'cart', $this->get_cart_for_session() );
			$this->cart->calculate_totals();

			if ( $merge_saved_cart ) {
				$this->persistent_cart_update();
			}
		}

		// If this is a re-order, redirect to the cart page to get rid of the `order_again` query string.
		if ( $order_again ) {
			wp_safe_redirect( wc_get_cart_url() );
			exit;
		}
	}

	/**
	 * Destroy cart session data.
	 *
	 * @since 3.2.0
	 */
	public function destroy_cart_session() {
		WC()->session->set( 'cart', null );
		WC()->session->set( 'cart_totals', null );
		WC()->session->set( 'applied_coupons', null );
		WC()->session->set( 'coupon_discount_totals', null );
		WC()->session->set( 'coupon_discount_tax_totals', null );
		WC()->session->set( 'removed_cart_contents', null );
		WC()->session->set( 'order_awaiting_payment', null );
	}

	/**
	 * Will set cart cookies if needed and when possible.
	 *
	 * @since 3.2.0
	 */
	public function maybe_set_cart_cookies() {
		if ( ! headers_sent() && did_action( 'wp_loaded' ) ) {
			if ( ! $this->cart->is_empty() ) {
				$this->set_cart_cookies( true );
			} elseif ( isset( $_COOKIE['woocommerce_items_in_cart'] ) ) { // WPCS: input var ok.
				$this->set_cart_cookies( false );
			}
		}
	}

	/**
	 * Sets the php session data for the cart and coupons.
	 */
	public function set_session() {
		WC()->session->set( 'cart', $this->get_cart_for_session() );
		WC()->session->set( 'cart_totals', $this->cart->get_totals() );
		WC()->session->set( 'applied_coupons', $this->cart->get_applied_coupons() );
		WC()->session->set( 'coupon_discount_totals', $this->cart->get_coupon_discount_totals() );
		WC()->session->set( 'coupon_discount_tax_totals', $this->cart->get_coupon_discount_tax_totals() );
		WC()->session->set( 'removed_cart_contents', $this->cart->get_removed_cart_contents() );

		do_action( 'woocommerce_cart_updated' );
	}

	/**
	 * Returns the contents of the cart in an array without the 'data' element.
	 *
	 * @return array contents of the cart
	 */
	public function get_cart_for_session() {
		$cart_session = array();

		foreach ( $this->cart->get_cart() as $key => $values ) {
			$cart_session[ $key ] = $values;
			unset( $cart_session[ $key ]['data'] ); // Unset product object.
		}

		return $cart_session;
	}

	/**
	 * Save the persistent cart when the cart is updated.
	 */
	public function persistent_cart_update() {
		if ( get_current_user_id() && apply_filters( 'woocommerce_persistent_cart_enabled', true ) ) {
			update_user_meta(
				get_current_user_id(),
				'_woocommerce_persistent_cart_' . get_current_blog_id(),
				array(
					'cart' => $this->get_cart_for_session(),
				)
			);
		}
	}

	/**
	 * Delete the persistent cart permanently.
	 */
	public function persistent_cart_destroy() {
		if ( get_current_user_id() && apply_filters( 'woocommerce_persistent_cart_enabled', true ) ) {
			delete_user_meta( get_current_user_id(), '_woocommerce_persistent_cart_' . get_current_blog_id() );
		}
	}

	/**
	 * Set cart hash cookie and items in cart if not already set.
	 *
	 * @param bool $set Should cookies be set (true) or unset.
	 */
	private function set_cart_cookies( $set = true ) {
		if ( $set ) {
			$setcookies = array(
				'woocommerce_items_in_cart' => '1',
				'woocommerce_cart_hash'     => WC()->cart->get_cart_hash(),
			);
			foreach ( $setcookies as $name => $value ) {
				if ( ! isset( $_COOKIE[ $name ] ) || $_COOKIE[ $name ] !== $value ) {
					wc_setcookie( $name, $value );
				}
			}
		} else {
			$unsetcookies = array(
				'woocommerce_items_in_cart',
				'woocommerce_cart_hash',
			);
			foreach ( $unsetcookies as $name ) {
				if ( isset( $_COOKIE[ $name ] ) ) {
					wc_setcookie( $name, 0, time() - HOUR_IN_SECONDS );
					unset( $_COOKIE[ $name ] );
				}
			}
		}

		do_action( 'woocommerce_set_cart_cookies', $set );
	}

	/**
	 * Get the persistent cart from the database.
	 *
	 * @since  3.5.0
	 * @return array
	 */
	private function get_saved_cart() {
		$saved_cart = array();

		if ( apply_filters( 'woocommerce_persistent_cart_enabled', true ) ) {
			$saved_cart_meta = get_user_meta( get_current_user_id(), '_woocommerce_persistent_cart_' . get_current_blog_id(), true );

			if ( isset( $saved_cart_meta['cart'] ) ) {
				$saved_cart = array_filter( (array) $saved_cart_meta['cart'] );
			}
		}

		return $saved_cart;
	}

	/**
	 * Get a cart from an order, if user has permission.
	 *
	 * @since  3.5.0
	 *
	 * @param int   $order_id Order ID to try to load.
	 * @param array $cart Current cart array.
	 *
	 * @return array
	 */
	private function populate_cart_from_order( $order_id, $cart ) {
		$order = wc_get_order( $order_id );

		if ( ! $order->get_id() || ! $order->has_status( apply_filters( 'woocommerce_valid_order_statuses_for_order_again', array( 'completed' ) ) ) || ! current_user_can( 'order_again', $order->get_id() ) ) {
			return;
		}

		if ( apply_filters( 'woocommerce_empty_cart_when_order_again', true ) ) {
			$cart = array();
		}

		$inital_cart_size = count( $cart );
		$order_items      = $order->get_items();

		foreach ( $order_items as $item ) {
			$product_id     = (int) apply_filters( 'woocommerce_add_to_cart_product_id', $item->get_product_id() );
			$quantity       = $item->get_quantity();
			$variation_id   = (int) $item->get_variation_id();
			$variations     = array();
			$cart_item_data = apply_filters( 'woocommerce_order_again_cart_item_data', array(), $item, $order );
			$product        = $item->get_product();

			if ( ! $product ) {
				continue;
			}

			// Prevent reordering variable products if no selected variation.
			if ( ! $variation_id && $product->is_type( 'variable' ) ) {
				continue;
			}

			// Prevent reordering items specifically out of stock.
			if ( ! $product->is_in_stock() ) {
				continue;
			}

			foreach ( $item->get_meta_data() as $meta ) {
				if ( taxonomy_is_product_attribute( $meta->key ) ) {
					$term                     = get_term_by( 'slug', $meta->value, $meta->key );
					$variations[ $meta->key ] = $term ? $term->name : $meta->value;
				} elseif ( meta_is_product_attribute( $meta->key, $meta->value, $product_id ) ) {
					$variations[ $meta->key ] = $meta->value;
				}
			}

			if ( ! apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity, $variation_id, $variations, $cart_item_data ) ) {
				continue;
			}

			// Add to cart directly.
			$cart_id          = WC()->cart->generate_cart_id( $product_id, $variation_id, $variations, $cart_item_data );
			$product_data     = wc_get_product( $variation_id ? $variation_id : $product_id );
			$cart[ $cart_id ] = apply_filters(
				'woocommerce_add_order_again_cart_item',
				array_merge(
					$cart_item_data,
					array(
						'key'          => $cart_id,
						'product_id'   => $product_id,
						'variation_id' => $variation_id,
						'variation'    => $variations,
						'quantity'     => $quantity,
						'data'         => $product_data,
						'data_hash'    => wc_get_cart_item_data_hash( $product_data ),
					)
				),
				$cart_id
			);
		}

		do_action_ref_array( 'woocommerce_ordered_again', array( $order->get_id(), $order_items, &$cart ) );

		$num_items_in_cart           = count( $cart );
		$num_items_in_original_order = count( $order_items );
		$num_items_added             = $num_items_in_cart - $inital_cart_size;

		if ( $num_items_in_original_order > $num_items_added ) {
			wc_add_notice(
				sprintf(
					/* translators: %d item count */
					_n(
						'%d item from your previous order is currently unavailable and could not be added to your cart.',
						'%d items from your previous order are currently unavailable and could not be added to your cart.',
						$num_items_in_original_order - $num_items_added,
						'woocommerce'
					),
					$num_items_in_original_order - $num_items_added
				),
				'error'
			);
		}

		if ( 0 < $num_items_added ) {
			wc_add_notice( __( 'The cart has been filled with the items from your previous order.', 'woocommerce' ) );
		}

		return $cart;
	}
}
class-wc-order.php000064400000173322151336044770010124 0ustar00<?php
/**
 * Regular order
 *
 * @package WooCommerce\Classes
 * @version 2.2.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Order Class.
 *
 * These are regular WooCommerce orders, which extend the abstract order class.
 */
class WC_Order extends WC_Abstract_Order {

	/**
	 * Stores data about status changes so relevant hooks can be fired.
	 *
	 * @var bool|array
	 */
	protected $status_transition = false;

	/**
	 * Order Data array. This is the core order data exposed in APIs since 3.0.0.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $data = array(
		// Abstract order props.
		'parent_id'            => 0,
		'status'               => '',
		'currency'             => '',
		'version'              => '',
		'prices_include_tax'   => false,
		'date_created'         => null,
		'date_modified'        => null,
		'discount_total'       => 0,
		'discount_tax'         => 0,
		'shipping_total'       => 0,
		'shipping_tax'         => 0,
		'cart_tax'             => 0,
		'total'                => 0,
		'total_tax'            => 0,

		// Order props.
		'customer_id'          => 0,
		'order_key'            => '',
		'billing'              => array(
			'first_name' => '',
			'last_name'  => '',
			'company'    => '',
			'address_1'  => '',
			'address_2'  => '',
			'city'       => '',
			'state'      => '',
			'postcode'   => '',
			'country'    => '',
			'email'      => '',
			'phone'      => '',
		),
		'shipping'             => array(
			'first_name' => '',
			'last_name'  => '',
			'company'    => '',
			'address_1'  => '',
			'address_2'  => '',
			'city'       => '',
			'state'      => '',
			'postcode'   => '',
			'country'    => '',
			'phone'      => '',
		),
		'payment_method'       => '',
		'payment_method_title' => '',
		'transaction_id'       => '',
		'customer_ip_address'  => '',
		'customer_user_agent'  => '',
		'created_via'          => '',
		'customer_note'        => '',
		'date_completed'       => null,
		'date_paid'            => null,
		'cart_hash'            => '',
	);

	/**
	 * When a payment is complete this function is called.
	 *
	 * Most of the time this should mark an order as 'processing' so that admin can process/post the items.
	 * If the cart contains only downloadable items then the order is 'completed' since the admin needs to take no action.
	 * Stock levels are reduced at this point.
	 * Sales are also recorded for products.
	 * Finally, record the date of payment.
	 *
	 * @param string $transaction_id Optional transaction id to store in post meta.
	 * @return bool success
	 */
	public function payment_complete( $transaction_id = '' ) {
		if ( ! $this->get_id() ) { // Order must exist.
			return false;
		}

		try {
			do_action( 'woocommerce_pre_payment_complete', $this->get_id() );

			if ( WC()->session ) {
				WC()->session->set( 'order_awaiting_payment', false );
			}

			if ( $this->has_status( apply_filters( 'woocommerce_valid_order_statuses_for_payment_complete', array( 'on-hold', 'pending', 'failed', 'cancelled' ), $this ) ) ) {
				if ( ! empty( $transaction_id ) ) {
					$this->set_transaction_id( $transaction_id );
				}
				if ( ! $this->get_date_paid( 'edit' ) ) {
					$this->set_date_paid( time() );
				}
				$this->set_status( apply_filters( 'woocommerce_payment_complete_order_status', $this->needs_processing() ? 'processing' : 'completed', $this->get_id(), $this ) );
				$this->save();

				do_action( 'woocommerce_payment_complete', $this->get_id() );
			} else {
				do_action( 'woocommerce_payment_complete_order_status_' . $this->get_status(), $this->get_id() );
			}
		} catch ( Exception $e ) {
			/**
			 * If there was an error completing the payment, log to a file and add an order note so the admin can take action.
			 */
			$logger = wc_get_logger();
			$logger->error(
				sprintf(
					'Error completing payment for order #%d',
					$this->get_id()
				),
				array(
					'order' => $this,
					'error' => $e,
				)
			);
			$this->add_order_note( __( 'Payment complete event failed.', 'woocommerce' ) . ' ' . $e->getMessage() );
			return false;
		}
		return true;
	}

	/**
	 * Gets order total - formatted for display.
	 *
	 * @param string $tax_display      Type of tax display.
	 * @param bool   $display_refunded If should include refunded value.
	 *
	 * @return string
	 */
	public function get_formatted_order_total( $tax_display = '', $display_refunded = true ) {
		$formatted_total = wc_price( $this->get_total(), array( 'currency' => $this->get_currency() ) );
		$order_total     = $this->get_total();
		$total_refunded  = $this->get_total_refunded();
		$tax_string      = '';

		// Tax for inclusive prices.
		if ( wc_tax_enabled() && 'incl' === $tax_display ) {
			$tax_string_array = array();
			$tax_totals       = $this->get_tax_totals();

			if ( 'itemized' === get_option( 'woocommerce_tax_total_display' ) ) {
				foreach ( $tax_totals as $code => $tax ) {
					$tax_amount         = ( $total_refunded && $display_refunded ) ? wc_price( WC_Tax::round( $tax->amount - $this->get_total_tax_refunded_by_rate_id( $tax->rate_id ) ), array( 'currency' => $this->get_currency() ) ) : $tax->formatted_amount;
					$tax_string_array[] = sprintf( '%s %s', $tax_amount, $tax->label );
				}
			} elseif ( ! empty( $tax_totals ) ) {
				$tax_amount         = ( $total_refunded && $display_refunded ) ? $this->get_total_tax() - $this->get_total_tax_refunded() : $this->get_total_tax();
				$tax_string_array[] = sprintf( '%s %s', wc_price( $tax_amount, array( 'currency' => $this->get_currency() ) ), WC()->countries->tax_or_vat() );
			}

			if ( ! empty( $tax_string_array ) ) {
				/* translators: %s: taxes */
				$tax_string = ' <small class="includes_tax">' . sprintf( __( '(includes %s)', 'woocommerce' ), implode( ', ', $tax_string_array ) ) . '</small>';
			}
		}

		if ( $total_refunded && $display_refunded ) {
			$formatted_total = '<del aria-hidden="true">' . wp_strip_all_tags( $formatted_total ) . '</del> <ins>' . wc_price( $order_total - $total_refunded, array( 'currency' => $this->get_currency() ) ) . $tax_string . '</ins>';
		} else {
			$formatted_total .= $tax_string;
		}

		/**
		 * Filter WooCommerce formatted order total.
		 *
		 * @param string   $formatted_total  Total to display.
		 * @param WC_Order $order            Order data.
		 * @param string   $tax_display      Type of tax display.
		 * @param bool     $display_refunded If should include refunded value.
		 */
		return apply_filters( 'woocommerce_get_formatted_order_total', $formatted_total, $this, $tax_display, $display_refunded );
	}

	/*
	|--------------------------------------------------------------------------
	| CRUD methods
	|--------------------------------------------------------------------------
	|
	| Methods which create, read, update and delete orders from the database.
	| Written in abstract fashion so that the way orders are stored can be
	| changed more easily in the future.
	|
	| A save method is included for convenience (chooses update or create based
	| on if the order exists yet).
	|
	*/

	/**
	 * Save data to the database.
	 *
	 * @since 3.0.0
	 * @return int order ID
	 */
	public function save() {
		$this->maybe_set_user_billing_email();
		parent::save();
		$this->status_transition();

		return $this->get_id();
	}

	/**
	 * Log an error about this order is exception is encountered.
	 *
	 * @param Exception $e Exception object.
	 * @param string    $message Message regarding exception thrown.
	 * @since 3.7.0
	 */
	protected function handle_exception( $e, $message = 'Error' ) {
		wc_get_logger()->error(
			$message,
			array(
				'order' => $this,
				'error' => $e,
			)
		);
		$this->add_order_note( $message . ' ' . $e->getMessage() );
	}

	/**
	 * Set order status.
	 *
	 * @since 3.0.0
	 * @param string $new_status    Status to change the order to. No internal wc- prefix is required.
	 * @param string $note          Optional note to add.
	 * @param bool   $manual_update Is this a manual order status change?.
	 * @return array
	 */
	public function set_status( $new_status, $note = '', $manual_update = false ) {
		$result = parent::set_status( $new_status );

		if ( true === $this->object_read && ! empty( $result['from'] ) && $result['from'] !== $result['to'] ) {
			$this->status_transition = array(
				'from'   => ! empty( $this->status_transition['from'] ) ? $this->status_transition['from'] : $result['from'],
				'to'     => $result['to'],
				'note'   => $note,
				'manual' => (bool) $manual_update,
			);

			if ( $manual_update ) {
				do_action( 'woocommerce_order_edit_status', $this->get_id(), $result['to'] );
			}

			$this->maybe_set_date_paid();
			$this->maybe_set_date_completed();
		}

		return $result;
	}

	/**
	 * Maybe set date paid.
	 *
	 * Sets the date paid variable when transitioning to the payment complete
	 * order status. This is either processing or completed. This is not filtered
	 * to avoid infinite loops e.g. if loading an order via the filter.
	 *
	 * Date paid is set once in this manner - only when it is not already set.
	 * This ensures the data exists even if a gateway does not use the
	 * `payment_complete` method.
	 *
	 * @since 3.0.0
	 */
	public function maybe_set_date_paid() {
		// This logic only runs if the date_paid prop has not been set yet.
		if ( ! $this->get_date_paid( 'edit' ) ) {
			$payment_completed_status = apply_filters( 'woocommerce_payment_complete_order_status', $this->needs_processing() ? 'processing' : 'completed', $this->get_id(), $this );

			if ( $this->has_status( $payment_completed_status ) ) {
				// If payment complete status is reached, set paid now.
				$this->set_date_paid( time() );

			} elseif ( 'processing' === $payment_completed_status && $this->has_status( 'completed' ) ) {
				// If payment complete status was processing, but we've passed that and still have no date, set it now.
				$this->set_date_paid( time() );
			}
		}
	}

	/**
	 * Maybe set date completed.
	 *
	 * Sets the date completed variable when transitioning to completed status.
	 *
	 * @since 3.0.0
	 */
	protected function maybe_set_date_completed() {
		if ( $this->has_status( 'completed' ) ) {
			$this->set_date_completed( time() );
		}
	}

	/**
	 * Updates status of order immediately.
	 *
	 * @uses WC_Order::set_status()
	 * @param string $new_status    Status to change the order to. No internal wc- prefix is required.
	 * @param string $note          Optional note to add.
	 * @param bool   $manual        Is this a manual order status change?.
	 * @return bool
	 */
	public function update_status( $new_status, $note = '', $manual = false ) {
		if ( ! $this->get_id() ) { // Order must exist.
			return false;
		}

		try {
			$this->set_status( $new_status, $note, $manual );
			$this->save();
		} catch ( Exception $e ) {
			$logger = wc_get_logger();
			$logger->error(
				sprintf(
					'Error updating status for order #%d',
					$this->get_id()
				),
				array(
					'order' => $this,
					'error' => $e,
				)
			);
			$this->add_order_note( __( 'Update status event failed.', 'woocommerce' ) . ' ' . $e->getMessage() );
			return false;
		}
		return true;
	}

	/**
	 * Handle the status transition.
	 */
	protected function status_transition() {
		$status_transition = $this->status_transition;

		// Reset status transition variable.
		$this->status_transition = false;

		if ( $status_transition ) {
			try {
				do_action( 'woocommerce_order_status_' . $status_transition['to'], $this->get_id(), $this );

				if ( ! empty( $status_transition['from'] ) ) {
					/* translators: 1: old order status 2: new order status */
					$transition_note = sprintf( __( 'Order status changed from %1$s to %2$s.', 'woocommerce' ), wc_get_order_status_name( $status_transition['from'] ), wc_get_order_status_name( $status_transition['to'] ) );

					// Note the transition occurred.
					$this->add_status_transition_note( $transition_note, $status_transition );

					do_action( 'woocommerce_order_status_' . $status_transition['from'] . '_to_' . $status_transition['to'], $this->get_id(), $this );
					do_action( 'woocommerce_order_status_changed', $this->get_id(), $status_transition['from'], $status_transition['to'], $this );

					// Work out if this was for a payment, and trigger a payment_status hook instead.
					if (
						in_array( $status_transition['from'], apply_filters( 'woocommerce_valid_order_statuses_for_payment', array( 'pending', 'failed' ), $this ), true )
						&& in_array( $status_transition['to'], wc_get_is_paid_statuses(), true )
					) {
						/**
						 * Fires when the order progresses from a pending payment status to a paid one.
						 *
						 * @since 3.9.0
						 * @param int Order ID
						 * @param WC_Order Order object
						 */
						do_action( 'woocommerce_order_payment_status_changed', $this->get_id(), $this );
					}
				} else {
					/* translators: %s: new order status */
					$transition_note = sprintf( __( 'Order status set to %s.', 'woocommerce' ), wc_get_order_status_name( $status_transition['to'] ) );

					// Note the transition occurred.
					$this->add_status_transition_note( $transition_note, $status_transition );
				}
			} catch ( Exception $e ) {
				$logger = wc_get_logger();
				$logger->error(
					sprintf(
						'Status transition of order #%d errored!',
						$this->get_id()
					),
					array(
						'order' => $this,
						'error' => $e,
					)
				);
				$this->add_order_note( __( 'Error during status transition.', 'woocommerce' ) . ' ' . $e->getMessage() );
			}
		}
	}

	/*
	|--------------------------------------------------------------------------
	| Getters
	|--------------------------------------------------------------------------
	|
	| Methods for getting data from the order object.
	|
	*/

	/**
	 * Get basic order data in array format.
	 *
	 * @return array
	 */
	public function get_base_data() {
		return array_merge(
			array( 'id' => $this->get_id() ),
			$this->data,
			array( 'number' => $this->get_order_number() )
		);
	}

	/**
	 * Get all class data in array format.
	 *
	 * @since 3.0.0
	 * @return array
	 */
	public function get_data() {
		return array_merge(
			$this->get_base_data(),
			array(
				'meta_data'      => $this->get_meta_data(),
				'line_items'     => $this->get_items( 'line_item' ),
				'tax_lines'      => $this->get_items( 'tax' ),
				'shipping_lines' => $this->get_items( 'shipping' ),
				'fee_lines'      => $this->get_items( 'fee' ),
				'coupon_lines'   => $this->get_items( 'coupon' ),
			)
		);
	}

	/**
	 * Expands the shipping and billing information in the changes array.
	 */
	public function get_changes() {
		$changed_props = parent::get_changes();
		$subs          = array( 'shipping', 'billing' );
		foreach ( $subs as $sub ) {
			if ( ! empty( $changed_props[ $sub ] ) ) {
				foreach ( $changed_props[ $sub ] as $sub_prop => $value ) {
					$changed_props[ $sub . '_' . $sub_prop ] = $value;
				}
			}
		}
		if ( isset( $changed_props['customer_note'] ) ) {
			$changed_props['post_excerpt'] = $changed_props['customer_note'];
		}
		return $changed_props;
	}

	/**
	 * Gets the order number for display (by default, order ID).
	 *
	 * @return string
	 */
	public function get_order_number() {
		return (string) apply_filters( 'woocommerce_order_number', $this->get_id(), $this );
	}

	/**
	 * Get order key.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_order_key( $context = 'view' ) {
		return $this->get_prop( 'order_key', $context );
	}

	/**
	 * Get customer_id.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return int
	 */
	public function get_customer_id( $context = 'view' ) {
		return $this->get_prop( 'customer_id', $context );
	}

	/**
	 * Alias for get_customer_id().
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return int
	 */
	public function get_user_id( $context = 'view' ) {
		return $this->get_customer_id( $context );
	}

	/**
	 * Get the user associated with the order. False for guests.
	 *
	 * @return WP_User|false
	 */
	public function get_user() {
		return $this->get_user_id() ? get_user_by( 'id', $this->get_user_id() ) : false;
	}

	/**
	 * Gets a prop for a getter method.
	 *
	 * @since  3.0.0
	 * @param  string $prop Name of prop to get.
	 * @param  string $address billing or shipping.
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return mixed
	 */
	protected function get_address_prop( $prop, $address = 'billing', $context = 'view' ) {
		$value = null;

		if ( array_key_exists( $prop, $this->data[ $address ] ) ) {
			$value = isset( $this->changes[ $address ][ $prop ] ) ? $this->changes[ $address ][ $prop ] : $this->data[ $address ][ $prop ];

			if ( 'view' === $context ) {
				$value = apply_filters( $this->get_hook_prefix() . $address . '_' . $prop, $value, $this );
			}
		}
		return $value;
	}

	/**
	 * Get billing first name.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_billing_first_name( $context = 'view' ) {
		return $this->get_address_prop( 'first_name', 'billing', $context );
	}

	/**
	 * Get billing last name.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_billing_last_name( $context = 'view' ) {
		return $this->get_address_prop( 'last_name', 'billing', $context );
	}

	/**
	 * Get billing company.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_billing_company( $context = 'view' ) {
		return $this->get_address_prop( 'company', 'billing', $context );
	}

	/**
	 * Get billing address line 1.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_billing_address_1( $context = 'view' ) {
		return $this->get_address_prop( 'address_1', 'billing', $context );
	}

	/**
	 * Get billing address line 2.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_billing_address_2( $context = 'view' ) {
		return $this->get_address_prop( 'address_2', 'billing', $context );
	}

	/**
	 * Get billing city.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_billing_city( $context = 'view' ) {
		return $this->get_address_prop( 'city', 'billing', $context );
	}

	/**
	 * Get billing state.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_billing_state( $context = 'view' ) {
		return $this->get_address_prop( 'state', 'billing', $context );
	}

	/**
	 * Get billing postcode.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_billing_postcode( $context = 'view' ) {
		return $this->get_address_prop( 'postcode', 'billing', $context );
	}

	/**
	 * Get billing country.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_billing_country( $context = 'view' ) {
		return $this->get_address_prop( 'country', 'billing', $context );
	}

	/**
	 * Get billing email.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_billing_email( $context = 'view' ) {
		return $this->get_address_prop( 'email', 'billing', $context );
	}

	/**
	 * Get billing phone.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_billing_phone( $context = 'view' ) {
		return $this->get_address_prop( 'phone', 'billing', $context );
	}

	/**
	 * Get shipping first name.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_shipping_first_name( $context = 'view' ) {
		return $this->get_address_prop( 'first_name', 'shipping', $context );
	}

	/**
	 * Get shipping_last_name.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_shipping_last_name( $context = 'view' ) {
		return $this->get_address_prop( 'last_name', 'shipping', $context );
	}

	/**
	 * Get shipping company.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_shipping_company( $context = 'view' ) {
		return $this->get_address_prop( 'company', 'shipping', $context );
	}

	/**
	 * Get shipping address line 1.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_shipping_address_1( $context = 'view' ) {
		return $this->get_address_prop( 'address_1', 'shipping', $context );
	}

	/**
	 * Get shipping address line 2.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_shipping_address_2( $context = 'view' ) {
		return $this->get_address_prop( 'address_2', 'shipping', $context );
	}

	/**
	 * Get shipping city.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_shipping_city( $context = 'view' ) {
		return $this->get_address_prop( 'city', 'shipping', $context );
	}

	/**
	 * Get shipping state.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_shipping_state( $context = 'view' ) {
		return $this->get_address_prop( 'state', 'shipping', $context );
	}

	/**
	 * Get shipping postcode.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_shipping_postcode( $context = 'view' ) {
		return $this->get_address_prop( 'postcode', 'shipping', $context );
	}

	/**
	 * Get shipping country.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_shipping_country( $context = 'view' ) {
		return $this->get_address_prop( 'country', 'shipping', $context );
	}

	/**
	 * Get shipping phone.
	 *
	 * @since  5.6.0
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_shipping_phone( $context = 'view' ) {
		return $this->get_address_prop( 'phone', 'shipping', $context );
	}

	/**
	 * Get the payment method.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_payment_method( $context = 'view' ) {
		return $this->get_prop( 'payment_method', $context );
	}

	/**
	 * Get payment method title.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_payment_method_title( $context = 'view' ) {
		return $this->get_prop( 'payment_method_title', $context );
	}

	/**
	 * Get transaction d.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_transaction_id( $context = 'view' ) {
		return $this->get_prop( 'transaction_id', $context );
	}

	/**
	 * Get customer ip address.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_customer_ip_address( $context = 'view' ) {
		return $this->get_prop( 'customer_ip_address', $context );
	}

	/**
	 * Get customer user agent.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_customer_user_agent( $context = 'view' ) {
		return $this->get_prop( 'customer_user_agent', $context );
	}

	/**
	 * Get created via.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_created_via( $context = 'view' ) {
		return $this->get_prop( 'created_via', $context );
	}

	/**
	 * Get customer note.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_customer_note( $context = 'view' ) {
		return $this->get_prop( 'customer_note', $context );
	}

	/**
	 * Get date completed.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return WC_DateTime|NULL object if the date is set or null if there is no date.
	 */
	public function get_date_completed( $context = 'view' ) {
		return $this->get_prop( 'date_completed', $context );
	}

	/**
	 * Get date paid.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return WC_DateTime|NULL object if the date is set or null if there is no date.
	 */
	public function get_date_paid( $context = 'view' ) {
		$date_paid = $this->get_prop( 'date_paid', $context );

		if ( 'view' === $context && ! $date_paid && version_compare( $this->get_version( 'edit' ), '3.0', '<' ) && $this->has_status( apply_filters( 'woocommerce_payment_complete_order_status', $this->needs_processing() ? 'processing' : 'completed', $this->get_id(), $this ) ) ) {
			// In view context, return a date if missing.
			$date_paid = $this->get_date_created( 'edit' );
		}
		return $date_paid;
	}

	/**
	 * Get cart hash.
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 * @return string
	 */
	public function get_cart_hash( $context = 'view' ) {
		return $this->get_prop( 'cart_hash', $context );
	}

	/**
	 * Returns the requested address in raw, non-formatted way.
	 * Note: Merges raw data with get_prop data so changes are returned too.
	 *
	 * @since  2.4.0
	 * @param  string $type Billing or shipping. Anything else besides 'billing' will return shipping address.
	 * @return array The stored address after filter.
	 */
	public function get_address( $type = 'billing' ) {
		return apply_filters( 'woocommerce_get_order_address', array_merge( $this->data[ $type ], $this->get_prop( $type, 'view' ) ), $type, $this );
	}

	/**
	 * Get a formatted shipping address for the order.
	 *
	 * @return string
	 */
	public function get_shipping_address_map_url() {
		$address = $this->get_address( 'shipping' );

		// Remove name and company before generate the Google Maps URL.
		unset( $address['first_name'], $address['last_name'], $address['company'], $address['phone'] );

		$address = apply_filters( 'woocommerce_shipping_address_map_url_parts', $address, $this );

		return apply_filters( 'woocommerce_shipping_address_map_url', 'https://maps.google.com/maps?&q=' . rawurlencode( implode( ', ', $address ) ) . '&z=16', $this );
	}

	/**
	 * Get a formatted billing full name.
	 *
	 * @return string
	 */
	public function get_formatted_billing_full_name() {
		/* translators: 1: first name 2: last name */
		return sprintf( _x( '%1$s %2$s', 'full name', 'woocommerce' ), $this->get_billing_first_name(), $this->get_billing_last_name() );
	}

	/**
	 * Get a formatted shipping full name.
	 *
	 * @return string
	 */
	public function get_formatted_shipping_full_name() {
		/* translators: 1: first name 2: last name */
		return sprintf( _x( '%1$s %2$s', 'full name', 'woocommerce' ), $this->get_shipping_first_name(), $this->get_shipping_last_name() );
	}

	/**
	 * Get a formatted billing address for the order.
	 *
	 * @param string $empty_content Content to show if no address is present. @since 3.3.0.
	 * @return string
	 */
	public function get_formatted_billing_address( $empty_content = '' ) {
		$raw_address = apply_filters( 'woocommerce_order_formatted_billing_address', $this->get_address( 'billing' ), $this );
		$address     = WC()->countries->get_formatted_address( $raw_address );

		/**
		 * Filter orders formatted billing address.
		 *
		 * @since 3.8.0
		 * @param string   $address     Formatted billing address string.
		 * @param array    $raw_address Raw billing address.
		 * @param WC_Order $order       Order data. @since 3.9.0
		 */
		return apply_filters( 'woocommerce_order_get_formatted_billing_address', $address ? $address : $empty_content, $raw_address, $this );
	}

	/**
	 * Get a formatted shipping address for the order.
	 *
	 * @param string $empty_content Content to show if no address is present. @since 3.3.0.
	 * @return string
	 */
	public function get_formatted_shipping_address( $empty_content = '' ) {
		$address     = '';
		$raw_address = $this->get_address( 'shipping' );

		if ( $this->has_shipping_address() ) {
			$raw_address = apply_filters( 'woocommerce_order_formatted_shipping_address', $raw_address, $this );
			$address     = WC()->countries->get_formatted_address( $raw_address );
		}

		/**
		 * Filter orders formatted shipping address.
		 *
		 * @since 3.8.0
		 * @param string   $address     Formatted billing address string.
		 * @param array    $raw_address Raw billing address.
		 * @param WC_Order $order       Order data. @since 3.9.0
		 */
		return apply_filters( 'woocommerce_order_get_formatted_shipping_address', $address ? $address : $empty_content, $raw_address, $this );
	}

	/**
	 * Returns true if the order has a billing address.
	 *
	 * @since  3.0.4
	 * @return boolean
	 */
	public function has_billing_address() {
		return $this->get_billing_address_1() || $this->get_billing_address_2();
	}

	/**
	 * Returns true if the order has a shipping address.
	 *
	 * @since  3.0.4
	 * @return boolean
	 */
	public function has_shipping_address() {
		return $this->get_shipping_address_1() || $this->get_shipping_address_2();
	}

	/*
	|--------------------------------------------------------------------------
	| Setters
	|--------------------------------------------------------------------------
	|
	| Functions for setting order data. These should not update anything in the
	| database itself and should only change what is stored in the class
	| object. However, for backwards compatibility pre 3.0.0 some of these
	| setters may handle both.
	|
	*/

	/**
	 * Sets a prop for a setter method.
	 *
	 * @since 3.0.0
	 * @param string $prop Name of prop to set.
	 * @param string $address Name of address to set. billing or shipping.
	 * @param mixed  $value Value of the prop.
	 */
	protected function set_address_prop( $prop, $address, $value ) {
		if ( array_key_exists( $prop, $this->data[ $address ] ) ) {
			if ( true === $this->object_read ) {
				if ( $value !== $this->data[ $address ][ $prop ] || ( isset( $this->changes[ $address ] ) && array_key_exists( $prop, $this->changes[ $address ] ) ) ) {
					$this->changes[ $address ][ $prop ] = $value;
				}
			} else {
				$this->data[ $address ][ $prop ] = $value;
			}
		}
	}

	/**
	 * Set order key.
	 *
	 * @param string $value Max length 22 chars.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_order_key( $value ) {
		$this->set_prop( 'order_key', substr( $value, 0, 22 ) );
	}

	/**
	 * Set customer id.
	 *
	 * @param int $value Customer ID.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_customer_id( $value ) {
		$this->set_prop( 'customer_id', absint( $value ) );
	}

	/**
	 * Set billing first name.
	 *
	 * @param string $value Billing first name.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_billing_first_name( $value ) {
		$this->set_address_prop( 'first_name', 'billing', $value );
	}

	/**
	 * Set billing last name.
	 *
	 * @param string $value Billing last name.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_billing_last_name( $value ) {
		$this->set_address_prop( 'last_name', 'billing', $value );
	}

	/**
	 * Set billing company.
	 *
	 * @param string $value Billing company.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_billing_company( $value ) {
		$this->set_address_prop( 'company', 'billing', $value );
	}

	/**
	 * Set billing address line 1.
	 *
	 * @param string $value Billing address line 1.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_billing_address_1( $value ) {
		$this->set_address_prop( 'address_1', 'billing', $value );
	}

	/**
	 * Set billing address line 2.
	 *
	 * @param string $value Billing address line 2.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_billing_address_2( $value ) {
		$this->set_address_prop( 'address_2', 'billing', $value );
	}

	/**
	 * Set billing city.
	 *
	 * @param string $value Billing city.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_billing_city( $value ) {
		$this->set_address_prop( 'city', 'billing', $value );
	}

	/**
	 * Set billing state.
	 *
	 * @param string $value Billing state.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_billing_state( $value ) {
		$this->set_address_prop( 'state', 'billing', $value );
	}

	/**
	 * Set billing postcode.
	 *
	 * @param string $value Billing postcode.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_billing_postcode( $value ) {
		$this->set_address_prop( 'postcode', 'billing', $value );
	}

	/**
	 * Set billing country.
	 *
	 * @param string $value Billing country.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_billing_country( $value ) {
		$this->set_address_prop( 'country', 'billing', $value );
	}

	/**
	 * Maybe set empty billing email to that of the user who owns the order.
	 */
	protected function maybe_set_user_billing_email() {
		$user = $this->get_user();
		if ( ! $this->get_billing_email() && $user ) {
			try {
				$this->set_billing_email( $user->user_email );
			} catch ( WC_Data_Exception $e ) {
				unset( $e );
			}
		}
	}

	/**
	 * Set billing email.
	 *
	 * @param string $value Billing email.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_billing_email( $value ) {
		if ( $value && ! is_email( $value ) ) {
			$this->error( 'order_invalid_billing_email', __( 'Invalid billing email address', 'woocommerce' ) );
		}
		$this->set_address_prop( 'email', 'billing', sanitize_email( $value ) );
	}

	/**
	 * Set billing phone.
	 *
	 * @param string $value Billing phone.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_billing_phone( $value ) {
		$this->set_address_prop( 'phone', 'billing', $value );
	}

	/**
	 * Set shipping first name.
	 *
	 * @param string $value Shipping first name.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_shipping_first_name( $value ) {
		$this->set_address_prop( 'first_name', 'shipping', $value );
	}

	/**
	 * Set shipping last name.
	 *
	 * @param string $value Shipping last name.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_shipping_last_name( $value ) {
		$this->set_address_prop( 'last_name', 'shipping', $value );
	}

	/**
	 * Set shipping company.
	 *
	 * @param string $value Shipping company.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_shipping_company( $value ) {
		$this->set_address_prop( 'company', 'shipping', $value );
	}

	/**
	 * Set shipping address line 1.
	 *
	 * @param string $value Shipping address line 1.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_shipping_address_1( $value ) {
		$this->set_address_prop( 'address_1', 'shipping', $value );
	}

	/**
	 * Set shipping address line 2.
	 *
	 * @param string $value Shipping address line 2.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_shipping_address_2( $value ) {
		$this->set_address_prop( 'address_2', 'shipping', $value );
	}

	/**
	 * Set shipping city.
	 *
	 * @param string $value Shipping city.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_shipping_city( $value ) {
		$this->set_address_prop( 'city', 'shipping', $value );
	}

	/**
	 * Set shipping state.
	 *
	 * @param string $value Shipping state.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_shipping_state( $value ) {
		$this->set_address_prop( 'state', 'shipping', $value );
	}

	/**
	 * Set shipping postcode.
	 *
	 * @param string $value Shipping postcode.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_shipping_postcode( $value ) {
		$this->set_address_prop( 'postcode', 'shipping', $value );
	}

	/**
	 * Set shipping country.
	 *
	 * @param string $value Shipping country.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_shipping_country( $value ) {
		$this->set_address_prop( 'country', 'shipping', $value );
	}

	/**
	 * Set shipping phone.
	 *
	 * @since 5.6.0
	 * @param string $value Shipping phone.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_shipping_phone( $value ) {
		$this->set_address_prop( 'phone', 'shipping', $value );
	}

	/**
	 * Set the payment method.
	 *
	 * @param string $payment_method Supports WC_Payment_Gateway for bw compatibility with < 3.0.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_payment_method( $payment_method = '' ) {
		if ( is_object( $payment_method ) ) {
			$this->set_payment_method( $payment_method->id );
			$this->set_payment_method_title( $payment_method->get_title() );
		} elseif ( '' === $payment_method ) {
			$this->set_prop( 'payment_method', '' );
			$this->set_prop( 'payment_method_title', '' );
		} else {
			$this->set_prop( 'payment_method', $payment_method );
		}
	}

	/**
	 * Set payment method title.
	 *
	 * @param string $value Payment method title.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_payment_method_title( $value ) {
		$this->set_prop( 'payment_method_title', $value );
	}

	/**
	 * Set transaction id.
	 *
	 * @param string $value Transaction id.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_transaction_id( $value ) {
		$this->set_prop( 'transaction_id', $value );
	}

	/**
	 * Set customer ip address.
	 *
	 * @param string $value Customer ip address.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_customer_ip_address( $value ) {
		$this->set_prop( 'customer_ip_address', $value );
	}

	/**
	 * Set customer user agent.
	 *
	 * @param string $value Customer user agent.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_customer_user_agent( $value ) {
		$this->set_prop( 'customer_user_agent', $value );
	}

	/**
	 * Set created via.
	 *
	 * @param string $value Created via.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_created_via( $value ) {
		$this->set_prop( 'created_via', $value );
	}

	/**
	 * Set customer note.
	 *
	 * @param string $value Customer note.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_customer_note( $value ) {
		$this->set_prop( 'customer_note', $value );
	}

	/**
	 * Set date completed.
	 *
	 * @param  string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_date_completed( $date = null ) {
		$this->set_date_prop( 'date_completed', $date );
	}

	/**
	 * Set date paid.
	 *
	 * @param  string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_date_paid( $date = null ) {
		$this->set_date_prop( 'date_paid', $date );
	}

	/**
	 * Set cart hash.
	 *
	 * @param string $value Cart hash.
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 */
	public function set_cart_hash( $value ) {
		$this->set_prop( 'cart_hash', $value );
	}

	/*
	|--------------------------------------------------------------------------
	| Conditionals
	|--------------------------------------------------------------------------
	|
	| Checks if a condition is true or false.
	|
	*/

	/**
	 * Check if an order key is valid.
	 *
	 * @param string $key Order key.
	 * @return bool
	 */
	public function key_is_valid( $key ) {
		return hash_equals( $this->get_order_key(), $key );
	}

	/**
	 * See if order matches cart_hash.
	 *
	 * @param string $cart_hash Cart hash.
	 * @return bool
	 */
	public function has_cart_hash( $cart_hash = '' ) {
		return hash_equals( $this->get_cart_hash(), $cart_hash ); // @codingStandardsIgnoreLine
	}

	/**
	 * Checks if an order can be edited, specifically for use on the Edit Order screen.
	 *
	 * @return bool
	 */
	public function is_editable() {
		return apply_filters( 'wc_order_is_editable', in_array( $this->get_status(), array( 'pending', 'on-hold', 'auto-draft' ), true ), $this );
	}

	/**
	 * Returns if an order has been paid for based on the order status.
	 *
	 * @since 2.5.0
	 * @return bool
	 */
	public function is_paid() {
		return apply_filters( 'woocommerce_order_is_paid', $this->has_status( wc_get_is_paid_statuses() ), $this );
	}

	/**
	 * Checks if product download is permitted.
	 *
	 * @return bool
	 */
	public function is_download_permitted() {
		return apply_filters( 'woocommerce_order_is_download_permitted', $this->has_status( 'completed' ) || ( 'yes' === get_option( 'woocommerce_downloads_grant_access_after_payment' ) && $this->has_status( 'processing' ) ), $this );
	}

	/**
	 * Checks if an order needs display the shipping address, based on shipping method.
	 *
	 * @return bool
	 */
	public function needs_shipping_address() {
		if ( 'no' === get_option( 'woocommerce_calc_shipping' ) ) {
			return false;
		}

		$hide          = apply_filters( 'woocommerce_order_hide_shipping_address', array( 'local_pickup' ), $this );
		$needs_address = false;

		foreach ( $this->get_shipping_methods() as $shipping_method ) {
			$shipping_method_id = $shipping_method->get_method_id();

			if ( ! in_array( $shipping_method_id, $hide, true ) ) {
				$needs_address = true;
				break;
			}
		}

		return apply_filters( 'woocommerce_order_needs_shipping_address', $needs_address, $hide, $this );
	}

	/**
	 * Returns true if the order contains a downloadable product.
	 *
	 * @return bool
	 */
	public function has_downloadable_item() {
		foreach ( $this->get_items() as $item ) {
			if ( $item->is_type( 'line_item' ) ) {
				$product = $item->get_product();

				if ( $product && $product->has_file() ) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Get downloads from all line items for this order.
	 *
	 * @since  3.2.0
	 * @return array
	 */
	public function get_downloadable_items() {
		$downloads = array();

		foreach ( $this->get_items() as $item ) {
			if ( ! is_object( $item ) ) {
				continue;
			}

			// Check item refunds.
			$refunded_qty = abs( $this->get_qty_refunded_for_item( $item->get_id() ) );
			if ( $refunded_qty && $item->get_quantity() === $refunded_qty ) {
				continue;
			}

			if ( $item->is_type( 'line_item' ) ) {
				$item_downloads = $item->get_item_downloads();
				$product        = $item->get_product();
				if ( $product && $item_downloads ) {
					foreach ( $item_downloads as $file ) {
						$downloads[] = array(
							'download_url'        => $file['download_url'],
							'download_id'         => $file['id'],
							'product_id'          => $product->get_id(),
							'product_name'        => $product->get_name(),
							'product_url'         => $product->is_visible() ? $product->get_permalink() : '', // Since 3.3.0.
							'download_name'       => $file['name'],
							'order_id'            => $this->get_id(),
							'order_key'           => $this->get_order_key(),
							'downloads_remaining' => $file['downloads_remaining'],
							'access_expires'      => $file['access_expires'],
							'file'                => array(
								'name' => $file['name'],
								'file' => $file['file'],
							),
						);
					}
				}
			}
		}

		return apply_filters( 'woocommerce_order_get_downloadable_items', $downloads, $this );
	}

	/**
	 * Checks if an order needs payment, based on status and order total.
	 *
	 * @return bool
	 */
	public function needs_payment() {
		$valid_order_statuses = apply_filters( 'woocommerce_valid_order_statuses_for_payment', array( 'pending', 'failed' ), $this );
		return apply_filters( 'woocommerce_order_needs_payment', ( $this->has_status( $valid_order_statuses ) && $this->get_total() > 0 ), $this, $valid_order_statuses );
	}

	/**
	 * See if the order needs processing before it can be completed.
	 *
	 * Orders which only contain virtual, downloadable items do not need admin
	 * intervention.
	 *
	 * Uses a transient so these calls are not repeated multiple times, and because
	 * once the order is processed this code/transient does not need to persist.
	 *
	 * @since 3.0.0
	 * @return bool
	 */
	public function needs_processing() {
		$transient_name   = 'wc_order_' . $this->get_id() . '_needs_processing';
		$needs_processing = get_transient( $transient_name );

		if ( false === $needs_processing ) {
			$needs_processing = 0;

			if ( count( $this->get_items() ) > 0 ) {
				foreach ( $this->get_items() as $item ) {
					if ( $item->is_type( 'line_item' ) ) {
						$product = $item->get_product();

						if ( ! $product ) {
							continue;
						}

						$virtual_downloadable_item = $product->is_downloadable() && $product->is_virtual();

						if ( apply_filters( 'woocommerce_order_item_needs_processing', ! $virtual_downloadable_item, $product, $this->get_id() ) ) {
							$needs_processing = 1;
							break;
						}
					}
				}
			}

			set_transient( $transient_name, $needs_processing, DAY_IN_SECONDS );
		}

		return 1 === absint( $needs_processing );
	}

	/*
	|--------------------------------------------------------------------------
	| URLs and Endpoints
	|--------------------------------------------------------------------------
	*/

	/**
	 * Generates a URL so that a customer can pay for their (unpaid - pending) order. Pass 'true' for the checkout version which doesn't offer gateway choices.
	 *
	 * @param  bool $on_checkout If on checkout.
	 * @return string
	 */
	public function get_checkout_payment_url( $on_checkout = false ) {
		$pay_url = wc_get_endpoint_url( 'order-pay', $this->get_id(), wc_get_checkout_url() );

		if ( $on_checkout ) {
			$pay_url = add_query_arg( 'key', $this->get_order_key(), $pay_url );
		} else {
			$pay_url = add_query_arg(
				array(
					'pay_for_order' => 'true',
					'key'           => $this->get_order_key(),
				),
				$pay_url
			);
		}

		return apply_filters( 'woocommerce_get_checkout_payment_url', $pay_url, $this );
	}

	/**
	 * Generates a URL for the thanks page (order received).
	 *
	 * @return string
	 */
	public function get_checkout_order_received_url() {
		$order_received_url = wc_get_endpoint_url( 'order-received', $this->get_id(), wc_get_checkout_url() );
		$order_received_url = add_query_arg( 'key', $this->get_order_key(), $order_received_url );

		return apply_filters( 'woocommerce_get_checkout_order_received_url', $order_received_url, $this );
	}

	/**
	 * Generates a URL so that a customer can cancel their (unpaid - pending) order.
	 *
	 * @param string $redirect Redirect URL.
	 * @return string
	 */
	public function get_cancel_order_url( $redirect = '' ) {
		return apply_filters(
			'woocommerce_get_cancel_order_url',
			wp_nonce_url(
				add_query_arg(
					array(
						'cancel_order' => 'true',
						'order'        => $this->get_order_key(),
						'order_id'     => $this->get_id(),
						'redirect'     => $redirect,
					),
					$this->get_cancel_endpoint()
				),
				'woocommerce-cancel_order'
			)
		);
	}

	/**
	 * Generates a raw (unescaped) cancel-order URL for use by payment gateways.
	 *
	 * @param string $redirect Redirect URL.
	 * @return string The unescaped cancel-order URL.
	 */
	public function get_cancel_order_url_raw( $redirect = '' ) {
		return apply_filters(
			'woocommerce_get_cancel_order_url_raw',
			add_query_arg(
				array(
					'cancel_order' => 'true',
					'order'        => $this->get_order_key(),
					'order_id'     => $this->get_id(),
					'redirect'     => $redirect,
					'_wpnonce'     => wp_create_nonce( 'woocommerce-cancel_order' ),
				),
				$this->get_cancel_endpoint()
			)
		);
	}

	/**
	 * Helper method to return the cancel endpoint.
	 *
	 * @return string the cancel endpoint; either the cart page or the home page.
	 */
	public function get_cancel_endpoint() {
		$cancel_endpoint = wc_get_cart_url();
		if ( ! $cancel_endpoint ) {
			$cancel_endpoint = home_url();
		}

		if ( false === strpos( $cancel_endpoint, '?' ) ) {
			$cancel_endpoint = trailingslashit( $cancel_endpoint );
		}

		return $cancel_endpoint;
	}

	/**
	 * Generates a URL to view an order from the my account page.
	 *
	 * @return string
	 */
	public function get_view_order_url() {
		return apply_filters( 'woocommerce_get_view_order_url', wc_get_endpoint_url( 'view-order', $this->get_id(), wc_get_page_permalink( 'myaccount' ) ), $this );
	}

	/**
	 * Get's the URL to edit the order in the backend.
	 *
	 * @since 3.3.0
	 * @return string
	 */
	public function get_edit_order_url() {
		return apply_filters( 'woocommerce_get_edit_order_url', get_admin_url( null, 'post.php?post=' . $this->get_id() . '&action=edit' ), $this );
	}

	/*
	|--------------------------------------------------------------------------
	| Order notes.
	|--------------------------------------------------------------------------
	*/

	/**
	 * Adds a note (comment) to the order. Order must exist.
	 *
	 * @param  string $note              Note to add.
	 * @param  int    $is_customer_note  Is this a note for the customer?.
	 * @param  bool   $added_by_user     Was the note added by a user?.
	 * @return int                       Comment ID.
	 */
	public function add_order_note( $note, $is_customer_note = 0, $added_by_user = false ) {
		if ( ! $this->get_id() ) {
			return 0;
		}

		if ( is_user_logged_in() && current_user_can( 'edit_shop_orders', $this->get_id() ) && $added_by_user ) {
			$user                 = get_user_by( 'id', get_current_user_id() );
			$comment_author       = $user->display_name;
			$comment_author_email = $user->user_email;
		} else {
			$comment_author        = __( 'WooCommerce', 'woocommerce' );
			$comment_author_email  = strtolower( __( 'WooCommerce', 'woocommerce' ) ) . '@';
			$comment_author_email .= isset( $_SERVER['HTTP_HOST'] ) ? str_replace( 'www.', '', sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) ) : 'noreply.com'; // WPCS: input var ok.
			$comment_author_email  = sanitize_email( $comment_author_email );
		}
		$commentdata = apply_filters(
			'woocommerce_new_order_note_data',
			array(
				'comment_post_ID'      => $this->get_id(),
				'comment_author'       => $comment_author,
				'comment_author_email' => $comment_author_email,
				'comment_author_url'   => '',
				'comment_content'      => $note,
				'comment_agent'        => 'WooCommerce',
				'comment_type'         => 'order_note',
				'comment_parent'       => 0,
				'comment_approved'     => 1,
			),
			array(
				'order_id'         => $this->get_id(),
				'is_customer_note' => $is_customer_note,
			)
		);

		$comment_id = wp_insert_comment( $commentdata );

		if ( $is_customer_note ) {
			add_comment_meta( $comment_id, 'is_customer_note', 1 );

			do_action(
				'woocommerce_new_customer_note',
				array(
					'order_id'      => $this->get_id(),
					'customer_note' => $commentdata['comment_content'],
				)
			);
		}

		/**
		 * Action hook fired after an order note is added.
		 *
		 * @param int      $order_note_id Order note ID.
		 * @param WC_Order $order         Order data.
		 *
		 * @since 4.4.0
		 */
		do_action( 'woocommerce_order_note_added', $comment_id, $this );

		return $comment_id;
	}

	/**
	 * Add an order note for status transition
	 *
	 * @since 3.9.0
	 * @uses WC_Order::add_order_note()
	 * @param string $note          Note to be added giving status transition from and to details.
	 * @param bool   $transition    Details of the status transition.
	 * @return int                  Comment ID.
	 */
	private function add_status_transition_note( $note, $transition ) {
		return $this->add_order_note( trim( $transition['note'] . ' ' . $note ), 0, $transition['manual'] );
	}

	/**
	 * List order notes (public) for the customer.
	 *
	 * @return array
	 */
	public function get_customer_order_notes() {
		$notes = array();
		$args  = array(
			'post_id' => $this->get_id(),
			'approve' => 'approve',
			'type'    => '',
		);

		remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ) );

		$comments = get_comments( $args );

		foreach ( $comments as $comment ) {
			if ( ! get_comment_meta( $comment->comment_ID, 'is_customer_note', true ) ) {
				continue;
			}
			$comment->comment_content = make_clickable( $comment->comment_content );
			$notes[]                  = $comment;
		}

		add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ) );

		return $notes;
	}

	/*
	|--------------------------------------------------------------------------
	| Refunds
	|--------------------------------------------------------------------------
	*/

	/**
	 * Get order refunds.
	 *
	 * @since 2.2
	 * @return array of WC_Order_Refund objects
	 */
	public function get_refunds() {
		$cache_key   = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'refunds' . $this->get_id();
		$cached_data = wp_cache_get( $cache_key, $this->cache_group );

		if ( false !== $cached_data ) {
			return $cached_data;
		}

		$this->refunds = wc_get_orders(
			array(
				'type'   => 'shop_order_refund',
				'parent' => $this->get_id(),
				'limit'  => -1,
			)
		);

		wp_cache_set( $cache_key, $this->refunds, $this->cache_group );

		return $this->refunds;
	}

	/**
	 * Get amount already refunded.
	 *
	 * @since 2.2
	 * @return string
	 */
	public function get_total_refunded() {
		$cache_key   = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'total_refunded' . $this->get_id();
		$cached_data = wp_cache_get( $cache_key, $this->cache_group );

		if ( false !== $cached_data ) {
			return $cached_data;
		}

		$total_refunded = $this->data_store->get_total_refunded( $this );

		wp_cache_set( $cache_key, $total_refunded, $this->cache_group );

		return $total_refunded;
	}

	/**
	 * Get the total tax refunded.
	 *
	 * @since  2.3
	 * @return float
	 */
	public function get_total_tax_refunded() {
		$cache_key   = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'total_tax_refunded' . $this->get_id();
		$cached_data = wp_cache_get( $cache_key, $this->cache_group );

		if ( false !== $cached_data ) {
			return $cached_data;
		}

		$total_refunded = $this->data_store->get_total_tax_refunded( $this );

		wp_cache_set( $cache_key, $total_refunded, $this->cache_group );

		return $total_refunded;
	}

	/**
	 * Get the total shipping refunded.
	 *
	 * @since  2.4
	 * @return float
	 */
	public function get_total_shipping_refunded() {
		$cache_key   = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'total_shipping_refunded' . $this->get_id();
		$cached_data = wp_cache_get( $cache_key, $this->cache_group );

		if ( false !== $cached_data ) {
			return $cached_data;
		}

		$total_refunded = $this->data_store->get_total_shipping_refunded( $this );

		wp_cache_set( $cache_key, $total_refunded, $this->cache_group );

		return $total_refunded;
	}

	/**
	 * Gets the count of order items of a certain type that have been refunded.
	 *
	 * @since  2.4.0
	 * @param string $item_type Item type.
	 * @return string
	 */
	public function get_item_count_refunded( $item_type = '' ) {
		if ( empty( $item_type ) ) {
			$item_type = array( 'line_item' );
		}
		if ( ! is_array( $item_type ) ) {
			$item_type = array( $item_type );
		}
		$count = 0;

		foreach ( $this->get_refunds() as $refund ) {
			foreach ( $refund->get_items( $item_type ) as $refunded_item ) {
				$count += abs( $refunded_item->get_quantity() );
			}
		}

		return apply_filters( 'woocommerce_get_item_count_refunded', $count, $item_type, $this );
	}

	/**
	 * Get the total number of items refunded.
	 *
	 * @since  2.4.0
	 *
	 * @param  string $item_type Type of the item we're checking, if not a line_item.
	 * @return int
	 */
	public function get_total_qty_refunded( $item_type = 'line_item' ) {
		$qty = 0;
		foreach ( $this->get_refunds() as $refund ) {
			foreach ( $refund->get_items( $item_type ) as $refunded_item ) {
				$qty += $refunded_item->get_quantity();
			}
		}
		return $qty;
	}

	/**
	 * Get the refunded amount for a line item.
	 *
	 * @param  int    $item_id   ID of the item we're checking.
	 * @param  string $item_type Type of the item we're checking, if not a line_item.
	 * @return int
	 */
	public function get_qty_refunded_for_item( $item_id, $item_type = 'line_item' ) {
		$qty = 0;
		foreach ( $this->get_refunds() as $refund ) {
			foreach ( $refund->get_items( $item_type ) as $refunded_item ) {
				if ( absint( $refunded_item->get_meta( '_refunded_item_id' ) ) === $item_id ) {
					$qty += $refunded_item->get_quantity();
				}
			}
		}
		return $qty;
	}

	/**
	 * Get the refunded amount for a line item.
	 *
	 * @param  int    $item_id   ID of the item we're checking.
	 * @param  string $item_type Type of the item we're checking, if not a line_item.
	 * @return int
	 */
	public function get_total_refunded_for_item( $item_id, $item_type = 'line_item' ) {
		$total = 0;
		foreach ( $this->get_refunds() as $refund ) {
			foreach ( $refund->get_items( $item_type ) as $refunded_item ) {
				if ( absint( $refunded_item->get_meta( '_refunded_item_id' ) ) === $item_id ) {
					$total += $refunded_item->get_total();
				}
			}
		}
		return $total * -1;
	}

	/**
	 * Get the refunded tax amount for a line item.
	 *
	 * @param  int    $item_id   ID of the item we're checking.
	 * @param  int    $tax_id    ID of the tax we're checking.
	 * @param  string $item_type Type of the item we're checking, if not a line_item.
	 * @return double
	 */
	public function get_tax_refunded_for_item( $item_id, $tax_id, $item_type = 'line_item' ) {
		$total = 0;
		foreach ( $this->get_refunds() as $refund ) {
			foreach ( $refund->get_items( $item_type ) as $refunded_item ) {
				$refunded_item_id = (int) $refunded_item->get_meta( '_refunded_item_id' );
				if ( $refunded_item_id === $item_id ) {
					$taxes  = $refunded_item->get_taxes();
					$total += isset( $taxes['total'][ $tax_id ] ) ? (float) $taxes['total'][ $tax_id ] : 0;
					break;
				}
			}
		}
		return wc_round_tax_total( $total ) * -1;
	}

	/**
	 * Get total tax refunded by rate ID.
	 *
	 * @param  int $rate_id Rate ID.
	 * @return float
	 */
	public function get_total_tax_refunded_by_rate_id( $rate_id ) {
		$total = 0;
		foreach ( $this->get_refunds() as $refund ) {
			foreach ( $refund->get_items( 'tax' ) as $refunded_item ) {
				if ( absint( $refunded_item->get_rate_id() ) === $rate_id ) {
					$total += abs( $refunded_item->get_tax_total() ) + abs( $refunded_item->get_shipping_tax_total() );
				}
			}
		}

		return $total;
	}

	/**
	 * How much money is left to refund?
	 *
	 * @return string
	 */
	public function get_remaining_refund_amount() {
		return wc_format_decimal( $this->get_total() - $this->get_total_refunded(), wc_get_price_decimals() );
	}

	/**
	 * How many items are left to refund?
	 *
	 * @return int
	 */
	public function get_remaining_refund_items() {
		return absint( $this->get_item_count() - $this->get_item_count_refunded() );
	}

	/**
	 * Add total row for the payment method.
	 *
	 * @param array  $total_rows  Total rows.
	 * @param string $tax_display Tax to display.
	 */
	protected function add_order_item_totals_payment_method_row( &$total_rows, $tax_display ) {
		if ( $this->get_total() > 0 && $this->get_payment_method_title() && 'other' !== $this->get_payment_method_title() ) {
			$total_rows['payment_method'] = array(
				'label' => __( 'Payment method:', 'woocommerce' ),
				'value' => $this->get_payment_method_title(),
			);
		}
	}

	/**
	 * Add total row for refunds.
	 *
	 * @param array  $total_rows  Total rows.
	 * @param string $tax_display Tax to display.
	 */
	protected function add_order_item_totals_refund_rows( &$total_rows, $tax_display ) {
		$refunds = $this->get_refunds();
		if ( $refunds ) {
			foreach ( $refunds as $id => $refund ) {
				$total_rows[ 'refund_' . $id ] = array(
					'label' => $refund->get_reason() ? $refund->get_reason() : __( 'Refund', 'woocommerce' ) . ':',
					'value' => wc_price( '-' . $refund->get_amount(), array( 'currency' => $this->get_currency() ) ),
				);
			}
		}
	}

	/**
	 * Get totals for display on pages and in emails.
	 *
	 * @param string $tax_display Tax to display.
	 * @return array
	 */
	public function get_order_item_totals( $tax_display = '' ) {
		$tax_display = $tax_display ? $tax_display : get_option( 'woocommerce_tax_display_cart' );
		$total_rows  = array();

		$this->add_order_item_totals_subtotal_row( $total_rows, $tax_display );
		$this->add_order_item_totals_discount_row( $total_rows, $tax_display );
		$this->add_order_item_totals_shipping_row( $total_rows, $tax_display );
		$this->add_order_item_totals_fee_rows( $total_rows, $tax_display );
		$this->add_order_item_totals_tax_rows( $total_rows, $tax_display );
		$this->add_order_item_totals_payment_method_row( $total_rows, $tax_display );
		$this->add_order_item_totals_refund_rows( $total_rows, $tax_display );
		$this->add_order_item_totals_total_row( $total_rows, $tax_display );

		return apply_filters( 'woocommerce_get_order_item_totals', $total_rows, $this, $tax_display );
	}

	/**
	 * Check if order has been created via admin, checkout, or in another way.
	 *
	 * @since 4.0.0
	 * @param string $modus Way of creating the order to test for.
	 * @return bool
	 */
	public function is_created_via( $modus ) {
		return apply_filters( 'woocommerce_order_is_created_via', $modus === $this->get_created_via(), $this, $modus );
	}
}
class-wc-download-handler.php000064400000060132151336044770012225 0ustar00<?php
/**
 * Download handler
 *
 * Handle digital downloads.
 *
 * @package WooCommerce\Classes
 * @version 2.2.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Download handler class.
 */
class WC_Download_Handler {

	/**
	 * Hook in methods.
	 */
	public static function init() {
		if ( isset( $_GET['download_file'], $_GET['order'] ) && ( isset( $_GET['email'] ) || isset( $_GET['uid'] ) ) ) { // WPCS: input var ok, CSRF ok.
			add_action( 'init', array( __CLASS__, 'download_product' ) );
		}
		add_action( 'woocommerce_download_file_redirect', array( __CLASS__, 'download_file_redirect' ), 10, 2 );
		add_action( 'woocommerce_download_file_xsendfile', array( __CLASS__, 'download_file_xsendfile' ), 10, 2 );
		add_action( 'woocommerce_download_file_force', array( __CLASS__, 'download_file_force' ), 10, 2 );
	}

	/**
	 * Check if we need to download a file and check validity.
	 */
	public static function download_product() {
		$product_id = absint( $_GET['download_file'] ); // phpcs:ignore WordPress.VIP.SuperGlobalInputUsage.AccessDetected, WordPress.VIP.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.InputNotValidated
		$product    = wc_get_product( $product_id );
		$data_store = WC_Data_Store::load( 'customer-download' );

		if ( ! $product || empty( $_GET['key'] ) || empty( $_GET['order'] ) ) { // WPCS: input var ok, CSRF ok.
			self::download_error( __( 'Invalid download link.', 'woocommerce' ) );
		}

		// Fallback, accept email address if it's passed.
		if ( empty( $_GET['email'] ) && empty( $_GET['uid'] ) ) { // WPCS: input var ok, CSRF ok.
			self::download_error( __( 'Invalid download link.', 'woocommerce' ) );
		}

		$order_id = wc_get_order_id_by_order_key( wc_clean( wp_unslash( $_GET['order'] ) ) ); // WPCS: input var ok, CSRF ok.
		$order    = wc_get_order( $order_id );

		if ( isset( $_GET['email'] ) ) { // WPCS: input var ok, CSRF ok.
			$email_address = wp_unslash( $_GET['email'] ); // WPCS: input var ok, CSRF ok, sanitization ok.
		} else {
			// Get email address from order to verify hash.
			$email_address = is_a( $order, 'WC_Order' ) ? $order->get_billing_email() : null;

			// Prepare email address hash.
			$email_hash = function_exists( 'hash' ) ? hash( 'sha256', $email_address ) : sha1( $email_address );

			if ( is_null( $email_address ) || ! hash_equals( wp_unslash( $_GET['uid'] ), $email_hash ) ) { // WPCS: input var ok, CSRF ok, sanitization ok.
				self::download_error( __( 'Invalid download link.', 'woocommerce' ) );
			}
		}

		$download_ids = $data_store->get_downloads(
			array(
				'user_email'  => sanitize_email( str_replace( ' ', '+', $email_address ) ),
				'order_key'   => wc_clean( wp_unslash( $_GET['order'] ) ), // WPCS: input var ok, CSRF ok.
				'product_id'  => $product_id,
				'download_id' => wc_clean( preg_replace( '/\s+/', ' ', wp_unslash( $_GET['key'] ) ) ), // WPCS: input var ok, CSRF ok, sanitization ok.
				'orderby'     => 'downloads_remaining',
				'order'       => 'DESC',
				'limit'       => 1,
				'return'      => 'ids',
			)
		);

		if ( empty( $download_ids ) ) {
			self::download_error( __( 'Invalid download link.', 'woocommerce' ) );
		}

		$download = new WC_Customer_Download( current( $download_ids ) );

		/**
		 * Filter download filepath.
		 *
		 * @since 4.0.0
		 * @param string $file_path File path.
		 * @param string $email_address Email address.
		 * @param WC_Order|bool $order Order object or false.
		 * @param WC_Product $product Product object.
		 * @param WC_Customer_Download $download Download data.
		 */
		$file_path = apply_filters(
			'woocommerce_download_product_filepath',
			$product->get_file_download_path( $download->get_download_id() ),
			$email_address,
			$order,
			$product,
			$download
		);

		$parsed_file_path = self::parse_file_path( $file_path );
		$download_range   = self::get_download_range( @filesize( $parsed_file_path['file_path'] ) );  // @codingStandardsIgnoreLine.

		self::check_order_is_valid( $download );
		if ( ! $download_range['is_range_request'] ) {
			// If the remaining download count goes to 0, allow range requests to be able to finish streaming from iOS devices.
			self::check_downloads_remaining( $download );
		}
		self::check_download_expiry( $download );
		self::check_download_login_required( $download );

		do_action(
			'woocommerce_download_product',
			$download->get_user_email(),
			$download->get_order_key(),
			$download->get_product_id(),
			$download->get_user_id(),
			$download->get_download_id(),
			$download->get_order_id()
		);
		$download->save();

		// Track the download in logs and change remaining/counts.
		$current_user_id = get_current_user_id();
		$ip_address      = WC_Geolocation::get_ip_address();
		if ( ! $download_range['is_range_request'] ) {
			$download->track_download( $current_user_id > 0 ? $current_user_id : null, ! empty( $ip_address ) ? $ip_address : null );
		}

		self::download( $file_path, $download->get_product_id() );
	}

	/**
	 * Check if an order is valid for downloading from.
	 *
	 * @param WC_Customer_Download $download Download instance.
	 */
	private static function check_order_is_valid( $download ) {
		if ( $download->get_order_id() ) {
			$order = wc_get_order( $download->get_order_id() );

			if ( $order && ! $order->is_download_permitted() ) {
				self::download_error( __( 'Invalid order.', 'woocommerce' ), '', 403 );
			}
		}
	}

	/**
	 * Check if there are downloads remaining.
	 *
	 * @param WC_Customer_Download $download Download instance.
	 */
	private static function check_downloads_remaining( $download ) {
		if ( '' !== $download->get_downloads_remaining() && 0 >= $download->get_downloads_remaining() ) {
			self::download_error( __( 'Sorry, you have reached your download limit for this file', 'woocommerce' ), '', 403 );
		}
	}

	/**
	 * Check if the download has expired.
	 *
	 * @param WC_Customer_Download $download Download instance.
	 */
	private static function check_download_expiry( $download ) {
		if ( ! is_null( $download->get_access_expires() ) && $download->get_access_expires()->getTimestamp() < strtotime( 'midnight', time() ) ) {
			self::download_error( __( 'Sorry, this download has expired', 'woocommerce' ), '', 403 );
		}
	}

	/**
	 * Check if a download requires the user to login first.
	 *
	 * @param WC_Customer_Download $download Download instance.
	 */
	private static function check_download_login_required( $download ) {
		if ( $download->get_user_id() && 'yes' === get_option( 'woocommerce_downloads_require_login' ) ) {
			if ( ! is_user_logged_in() ) {
				if ( wc_get_page_id( 'myaccount' ) ) {
					wp_safe_redirect( add_query_arg( 'wc_error', rawurlencode( __( 'You must be logged in to download files.', 'woocommerce' ) ), wc_get_page_permalink( 'myaccount' ) ) );
					exit;
				} else {
					self::download_error( __( 'You must be logged in to download files.', 'woocommerce' ) . ' <a href="' . esc_url( wp_login_url( wc_get_page_permalink( 'myaccount' ) ) ) . '" class="wc-forward">' . __( 'Login', 'woocommerce' ) . '</a>', __( 'Log in to Download Files', 'woocommerce' ), 403 );
				}
			} elseif ( ! current_user_can( 'download_file', $download ) ) {
				self::download_error( __( 'This is not your download link.', 'woocommerce' ), '', 403 );
			}
		}
	}

	/**
	 * Count download.
	 *
	 * @deprecated 4.4.0
	 * @param array $download_data Download data.
	 */
	public static function count_download( $download_data ) {
		wc_deprecated_function( 'WC_Download_Handler::count_download', '4.4.0', '' );
	}

	/**
	 * Download a file - hook into init function.
	 *
	 * @param string  $file_path  URL to file.
	 * @param integer $product_id Product ID of the product being downloaded.
	 */
	public static function download( $file_path, $product_id ) {
		if ( ! $file_path ) {
			self::download_error( __( 'No file defined', 'woocommerce' ) );
		}

		$filename = basename( $file_path );

		if ( strstr( $filename, '?' ) ) {
			$filename = current( explode( '?', $filename ) );
		}

		$filename = apply_filters( 'woocommerce_file_download_filename', $filename, $product_id );

		/**
		 * Filter download method.
		 *
		 * @since 4.5.0
		 * @param string $method     Download method.
		 * @param int    $product_id Product ID.
		 * @param string $file_path  URL to file.
		 */
		$file_download_method = apply_filters( 'woocommerce_file_download_method', get_option( 'woocommerce_file_download_method', 'force' ), $product_id, $file_path );

		// Add action to prevent issues in IE.
		add_action( 'nocache_headers', array( __CLASS__, 'ie_nocache_headers_fix' ) );

		// Trigger download via one of the methods.
		do_action( 'woocommerce_download_file_' . $file_download_method, $file_path, $filename );
	}

	/**
	 * Redirect to a file to start the download.
	 *
	 * @param string $file_path File path.
	 * @param string $filename  File name.
	 */
	public static function download_file_redirect( $file_path, $filename = '' ) {
		header( 'Location: ' . $file_path );
		exit;
	}

	/**
	 * Parse file path and see if its remote or local.
	 *
	 * @param  string $file_path File path.
	 * @return array
	 */
	public static function parse_file_path( $file_path ) {
		$wp_uploads     = wp_upload_dir();
		$wp_uploads_dir = $wp_uploads['basedir'];
		$wp_uploads_url = $wp_uploads['baseurl'];

		/**
		 * Replace uploads dir, site url etc with absolute counterparts if we can.
		 * Note the str_replace on site_url is on purpose, so if https is forced
		 * via filters we can still do the string replacement on a HTTP file.
		 */
		$replacements = array(
			$wp_uploads_url                                                   => $wp_uploads_dir,
			network_site_url( '/', 'https' )                                  => ABSPATH,
			str_replace( 'https:', 'http:', network_site_url( '/', 'http' ) ) => ABSPATH,
			site_url( '/', 'https' )                                          => ABSPATH,
			str_replace( 'https:', 'http:', site_url( '/', 'http' ) )         => ABSPATH,
		);

		$count            = 0;
		$file_path        = str_replace( array_keys( $replacements ), array_values( $replacements ), $file_path );
		$parsed_file_path = wp_parse_url( $file_path );
		$remote_file      = null === $count || 0 === $count; // Remote file only if there were no replacements.

		// Paths that begin with '//' are always remote URLs.
		if ( '//' === substr( $file_path, 0, 2 ) ) {
			return array(
				'remote_file' => true,
				'file_path'   => is_ssl() ? 'https:' . $file_path : 'http:' . $file_path,
			);
		}

		// See if path needs an abspath prepended to work.
		if ( file_exists( ABSPATH . $file_path ) ) {
			$remote_file = false;
			$file_path   = ABSPATH . $file_path;

		} elseif ( '/wp-content' === substr( $file_path, 0, 11 ) ) {
			$remote_file = false;
			$file_path   = realpath( WP_CONTENT_DIR . substr( $file_path, 11 ) );

			// Check if we have an absolute path.
		} elseif ( ( ! isset( $parsed_file_path['scheme'] ) || ! in_array( $parsed_file_path['scheme'], array( 'http', 'https', 'ftp' ), true ) ) && isset( $parsed_file_path['path'] ) ) {
			$remote_file = false;
			$file_path   = $parsed_file_path['path'];
		}

		return array(
			'remote_file' => $remote_file,
			'file_path'   => $file_path,
		);
	}

	/**
	 * Download a file using X-Sendfile, X-Lighttpd-Sendfile, or X-Accel-Redirect if available.
	 *
	 * @param string $file_path File path.
	 * @param string $filename  File name.
	 */
	public static function download_file_xsendfile( $file_path, $filename ) {
		$parsed_file_path = self::parse_file_path( $file_path );

		/**
		 * Fallback on force download method for remote files. This is because:
		 * 1. xsendfile needs proxy configuration to work for remote files, which cannot be assumed to be available on most hosts.
		 * 2. Force download method is more secure than redirect method if `allow_url_fopen` is enabled in `php.ini`.
		 */
		if ( $parsed_file_path['remote_file'] && ! apply_filters( 'woocommerce_use_xsendfile_for_remote', false ) ) {
			do_action( 'woocommerce_download_file_force', $file_path, $filename );
			return;
		}

		if ( function_exists( 'apache_get_modules' ) && in_array( 'mod_xsendfile', apache_get_modules(), true ) ) {
			self::download_headers( $parsed_file_path['file_path'], $filename );
			$filepath = apply_filters( 'woocommerce_download_file_xsendfile_file_path', $parsed_file_path['file_path'], $file_path, $filename, $parsed_file_path );
			header( 'X-Sendfile: ' . $filepath );
			exit;
		} elseif ( stristr( getenv( 'SERVER_SOFTWARE' ), 'lighttpd' ) ) {
			self::download_headers( $parsed_file_path['file_path'], $filename );
			$filepath = apply_filters( 'woocommerce_download_file_xsendfile_lighttpd_file_path', $parsed_file_path['file_path'], $file_path, $filename, $parsed_file_path );
			header( 'X-Lighttpd-Sendfile: ' . $filepath );
			exit;
		} elseif ( stristr( getenv( 'SERVER_SOFTWARE' ), 'nginx' ) || stristr( getenv( 'SERVER_SOFTWARE' ), 'cherokee' ) ) {
			self::download_headers( $parsed_file_path['file_path'], $filename );
			$xsendfile_path = trim( preg_replace( '`^' . str_replace( '\\', '/', getcwd() ) . '`', '', $parsed_file_path['file_path'] ), '/' );
			$xsendfile_path = apply_filters( 'woocommerce_download_file_xsendfile_x_accel_redirect_file_path', $xsendfile_path, $file_path, $filename, $parsed_file_path );
			header( "X-Accel-Redirect: /$xsendfile_path" );
			exit;
		}

		// Fallback.
		wc_get_logger()->warning(
			sprintf(
				/* translators: %1$s contains the filepath of the digital asset. */
				__( '%1$s could not be served using the X-Accel-Redirect/X-Sendfile method. A Force Download will be used instead.', 'woocommerce' ),
				$file_path
			)
		);
		self::download_file_force( $file_path, $filename );
	}

	/**
	 * Parse the HTTP_RANGE request from iOS devices.
	 * Does not support multi-range requests.
	 *
	 * @param int $file_size Size of file in bytes.
	 * @return array {
	 *     Information about range download request: beginning and length of
	 *     file chunk, whether the range is valid/supported and whether the request is a range request.
	 *
	 *     @type int  $start            Byte offset of the beginning of the range. Default 0.
	 *     @type int  $length           Length of the requested file chunk in bytes. Optional.
	 *     @type bool $is_range_valid   Whether the requested range is a valid and supported range.
	 *     @type bool $is_range_request Whether the request is a range request.
	 * }
	 */
	protected static function get_download_range( $file_size ) {
		$start          = 0;
		$download_range = array(
			'start'            => $start,
			'is_range_valid'   => false,
			'is_range_request' => false,
		);

		if ( ! $file_size ) {
			return $download_range;
		}

		$end                      = $file_size - 1;
		$download_range['length'] = $file_size;

		if ( isset( $_SERVER['HTTP_RANGE'] ) ) { // @codingStandardsIgnoreLine.
			$http_range                         = sanitize_text_field( wp_unslash( $_SERVER['HTTP_RANGE'] ) ); // WPCS: input var ok.
			$download_range['is_range_request'] = true;

			$c_start = $start;
			$c_end   = $end;
			// Extract the range string.
			list( , $range ) = explode( '=', $http_range, 2 );
			// Make sure the client hasn't sent us a multibyte range.
			if ( strpos( $range, ',' ) !== false ) {
				return $download_range;
			}

			/*
			 * If the range starts with an '-' we start from the beginning.
			 * If not, we forward the file pointer
			 * and make sure to get the end byte if specified.
			 */
			if ( '-' === $range[0] ) {
				// The n-number of the last bytes is requested.
				$c_start = $file_size - substr( $range, 1 );
			} else {
				$range   = explode( '-', $range );
				$c_start = ( isset( $range[0] ) && is_numeric( $range[0] ) ) ? (int) $range[0] : 0;
				$c_end   = ( isset( $range[1] ) && is_numeric( $range[1] ) ) ? (int) $range[1] : $file_size;
			}

			/*
			 * Check the range and make sure it's treated according to the specs: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html.
			 * End bytes can not be larger than $end.
			 */
			$c_end = ( $c_end > $end ) ? $end : $c_end;
			// Validate the requested range and return an error if it's not correct.
			if ( $c_start > $c_end || $c_start > $file_size - 1 || $c_end >= $file_size ) {
				return $download_range;
			}
			$start  = $c_start;
			$end    = $c_end;
			$length = $end - $start + 1;

			$download_range['start']          = $start;
			$download_range['length']         = $length;
			$download_range['is_range_valid'] = true;
		}
		return $download_range;
	}

	/**
	 * Force download - this is the default method.
	 *
	 * @param string $file_path File path.
	 * @param string $filename  File name.
	 */
	public static function download_file_force( $file_path, $filename ) {
		$parsed_file_path = self::parse_file_path( $file_path );
		$download_range   = self::get_download_range( @filesize( $parsed_file_path['file_path'] ) ); // @codingStandardsIgnoreLine.

		self::download_headers( $parsed_file_path['file_path'], $filename, $download_range );

		$start  = isset( $download_range['start'] ) ? $download_range['start'] : 0;
		$length = isset( $download_range['length'] ) ? $download_range['length'] : 0;
		if ( ! self::readfile_chunked( $parsed_file_path['file_path'], $start, $length ) ) {
			if ( $parsed_file_path['remote_file'] && 'yes' === get_option( 'woocommerce_downloads_redirect_fallback_allowed' ) ) {
				wc_get_logger()->warning(
					sprintf(
						/* translators: %1$s contains the filepath of the digital asset. */
						__( '%1$s could not be served using the Force Download method. A redirect will be used instead.', 'woocommerce' ),
						$file_path
					)
				);
				self::download_file_redirect( $file_path );
			} else {
				self::download_error( __( 'File not found', 'woocommerce' ) );
			}
		}

		exit;
	}

	/**
	 * Get content type of a download.
	 *
	 * @param  string $file_path File path.
	 * @return string
	 */
	private static function get_download_content_type( $file_path ) {
		$file_extension = strtolower( substr( strrchr( $file_path, '.' ), 1 ) );
		$ctype          = 'application/force-download';

		foreach ( get_allowed_mime_types() as $mime => $type ) {
			$mimes = explode( '|', $mime );
			if ( in_array( $file_extension, $mimes, true ) ) {
				$ctype = $type;
				break;
			}
		}

		return $ctype;
	}

	/**
	 * Set headers for the download.
	 *
	 * @param string $file_path      File path.
	 * @param string $filename       File name.
	 * @param array  $download_range Array containing info about range download request (see {@see get_download_range} for structure).
	 */
	private static function download_headers( $file_path, $filename, $download_range = array() ) {
		self::check_server_config();
		self::clean_buffers();
		wc_nocache_headers();

		header( 'X-Robots-Tag: noindex, nofollow', true );
		header( 'Content-Type: ' . self::get_download_content_type( $file_path ) );
		header( 'Content-Description: File Transfer' );
		header( 'Content-Disposition: attachment; filename="' . $filename . '";' );
		header( 'Content-Transfer-Encoding: binary' );

		$file_size = @filesize( $file_path ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
		if ( ! $file_size ) {
			return;
		}

		if ( isset( $download_range['is_range_request'] ) && true === $download_range['is_range_request'] ) {
			if ( false === $download_range['is_range_valid'] ) {
				header( 'HTTP/1.1 416 Requested Range Not Satisfiable' );
				header( 'Content-Range: bytes 0-' . ( $file_size - 1 ) . '/' . $file_size );
				exit;
			}

			$start  = $download_range['start'];
			$end    = $download_range['start'] + $download_range['length'] - 1;
			$length = $download_range['length'];

			header( 'HTTP/1.1 206 Partial Content' );
			header( "Accept-Ranges: 0-$file_size" );
			header( "Content-Range: bytes $start-$end/$file_size" );
			header( "Content-Length: $length" );
		} else {
			header( 'Content-Length: ' . $file_size );
		}
	}

	/**
	 * Check and set certain server config variables to ensure downloads work as intended.
	 */
	private static function check_server_config() {
		wc_set_time_limit( 0 );
		if ( function_exists( 'apache_setenv' ) ) {
			@apache_setenv( 'no-gzip', 1 ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_apache_setenv
		}
		@ini_set( 'zlib.output_compression', 'Off' ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_ini_set
		@session_write_close(); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.VIP.SessionFunctionsUsage.session_session_write_close
	}

	/**
	 * Clean all output buffers.
	 *
	 * Can prevent errors, for example: transfer closed with 3 bytes remaining to read.
	 */
	private static function clean_buffers() {
		if ( ob_get_level() ) {
			$levels = ob_get_level();
			for ( $i = 0; $i < $levels; $i++ ) {
				@ob_end_clean(); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
			}
		} else {
			@ob_end_clean(); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
		}
	}

	/**
	 * Read file chunked.
	 *
	 * Reads file in chunks so big downloads are possible without changing PHP.INI - http://codeigniter.com/wiki/Download_helper_for_large_files/.
	 *
	 * @param  string $file   File.
	 * @param  int    $start  Byte offset/position of the beginning from which to read from the file.
	 * @param  int    $length Length of the chunk to be read from the file in bytes, 0 means full file.
	 * @return bool Success or fail
	 */
	public static function readfile_chunked( $file, $start = 0, $length = 0 ) {
		if ( ! defined( 'WC_CHUNK_SIZE' ) ) {
			define( 'WC_CHUNK_SIZE', 1024 * 1024 );
		}
		$handle = @fopen( $file, 'r' ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_fopen

		if ( false === $handle ) {
			return false;
		}

		if ( ! $length ) {
			$length = @filesize( $file ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
		}

		$read_length = (int) WC_CHUNK_SIZE;

		if ( $length ) {
			$end = $start + $length - 1;

			@fseek( $handle, $start ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
			$p = @ftell( $handle ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged

			while ( ! @feof( $handle ) && $p <= $end ) { // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
				// Don't run past the end of file.
				if ( $p + $read_length > $end ) {
					$read_length = $end - $p + 1;
				}

				echo @fread( $handle, $read_length ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.WP.AlternativeFunctions.file_system_read_fread
				$p = @ftell( $handle ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged

				if ( ob_get_length() ) {
					ob_flush();
					flush();
				}
			}
		} else {
			while ( ! @feof( $handle ) ) { // @codingStandardsIgnoreLine.
				echo @fread( $handle, $read_length ); // @codingStandardsIgnoreLine.
				if ( ob_get_length() ) {
					ob_flush();
					flush();
				}
			}
		}

		return @fclose( $handle ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_fclose
	}

	/**
	 * Filter headers for IE to fix issues over SSL.
	 *
	 * IE bug prevents download via SSL when Cache Control and Pragma no-cache headers set.
	 *
	 * @param array $headers HTTP headers.
	 * @return array
	 */
	public static function ie_nocache_headers_fix( $headers ) {
		if ( is_ssl() && ! empty( $GLOBALS['is_IE'] ) ) {
			$headers['Cache-Control'] = 'private';
			unset( $headers['Pragma'] );
		}
		return $headers;
	}

	/**
	 * Die with an error message if the download fails.
	 *
	 * @param string  $message Error message.
	 * @param string  $title   Error title.
	 * @param integer $status  Error status.
	 */
	private static function download_error( $message, $title = '', $status = 404 ) {
		/*
		 * Since we will now render a message instead of serving a download, we should unwind some of the previously set
		 * headers.
		 */
		header( 'Content-Type: ' . get_option( 'html_type' ) . '; charset=' . get_option( 'blog_charset' ) );
		header_remove( 'Content-Description;' );
		header_remove( 'Content-Disposition' );
		header_remove( 'Content-Transfer-Encoding' );

		if ( ! strstr( $message, '<a ' ) ) {
			$message .= ' <a href="' . esc_url( wc_get_page_permalink( 'shop' ) ) . '" class="wc-forward">' . esc_html__( 'Go to shop', 'woocommerce' ) . '</a>';
		}
		wp_die( $message, $title, array( 'response' => $status ) ); // WPCS: XSS ok.
	}
}

WC_Download_Handler::init();
class-wc-customer.php000064400000071646151336044770010660 0ustar00<?php
/**
 * The WooCommerce customer class handles storage of the current customer's data, such as location.
 *
 * @package WooCommerce\Classes
 * @version 3.0.0
 */

defined( 'ABSPATH' ) || exit;

require_once dirname( __FILE__ ) . '/legacy/class-wc-legacy-customer.php';

/**
 * Customer class.
 */
class WC_Customer extends WC_Legacy_Customer {

	/**
	 * Stores customer data.
	 *
	 * @var array
	 */
	protected $data = array(
		'date_created'       => null,
		'date_modified'      => null,
		'email'              => '',
		'first_name'         => '',
		'last_name'          => '',
		'display_name'       => '',
		'role'               => 'customer',
		'username'           => '',
		'billing'            => array(
			'first_name' => '',
			'last_name'  => '',
			'company'    => '',
			'address_1'  => '',
			'address_2'  => '',
			'city'       => '',
			'postcode'   => '',
			'country'    => '',
			'state'      => '',
			'email'      => '',
			'phone'      => '',
		),
		'shipping'           => array(
			'first_name' => '',
			'last_name'  => '',
			'company'    => '',
			'address_1'  => '',
			'address_2'  => '',
			'city'       => '',
			'postcode'   => '',
			'country'    => '',
			'state'      => '',
			'phone'      => '',
		),
		'is_paying_customer' => false,
	);

	/**
	 * Stores a password if this needs to be changed. Write-only and hidden from _data.
	 *
	 * @var string
	 */
	protected $password = '';

	/**
	 * Stores if user is VAT exempt for this session.
	 *
	 * @var string
	 */
	protected $is_vat_exempt = false;

	/**
	 * Stores if user has calculated shipping in this session.
	 *
	 * @var string
	 */
	protected $calculated_shipping = false;

	/**
	 * This is the name of this object type.
	 *
	 * @since 5.6.0
	 * @var string
	 */
	protected $object_type = 'customer';

	/**
	 * Load customer data based on how WC_Customer is called.
	 *
	 * If $customer is 'new', you can build a new WC_Customer object. If it's empty, some
	 * data will be pulled from the session for the current user/customer.
	 *
	 * @param WC_Customer|int $data       Customer ID or data.
	 * @param bool            $is_session True if this is the customer session.
	 * @throws Exception If customer cannot be read/found and $data is set.
	 */
	public function __construct( $data = 0, $is_session = false ) {
		parent::__construct( $data );

		if ( $data instanceof WC_Customer ) {
			$this->set_id( absint( $data->get_id() ) );
		} elseif ( is_numeric( $data ) ) {
			$this->set_id( $data );
		}

		$this->data_store = WC_Data_Store::load( 'customer' );

		// If we have an ID, load the user from the DB.
		if ( $this->get_id() ) {
			try {
				$this->data_store->read( $this );
			} catch ( Exception $e ) {
				$this->set_id( 0 );
				$this->set_object_read( true );
			}
		} else {
			$this->set_object_read( true );
		}

		// If this is a session, set or change the data store to sessions. Changes do not persist in the database.
		if ( $is_session && isset( WC()->session ) ) {
			$this->data_store = WC_Data_Store::load( 'customer-session' );
			$this->data_store->read( $this );
		}
	}

	/**
	 * Delete a customer and reassign posts..
	 *
	 * @param int $reassign Reassign posts and links to new User ID.
	 * @since 3.0.0
	 * @return bool
	 */
	public function delete_and_reassign( $reassign = null ) {
		if ( $this->data_store ) {
			$this->data_store->delete(
				$this,
				array(
					'force_delete' => true,
					'reassign'     => $reassign,
				)
			);
			$this->set_id( 0 );
			return true;
		}
		return false;
	}

	/**
	 * Is customer outside base country (for tax purposes)?
	 *
	 * @return bool
	 */
	public function is_customer_outside_base() {
		list( $country, $state ) = $this->get_taxable_address();
		if ( $country ) {
			$default = wc_get_base_location();
			if ( $default['country'] !== $country ) {
				return true;
			}
			if ( $default['state'] && $default['state'] !== $state ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Return this customer's avatar.
	 *
	 * @since 3.0.0
	 * @return string
	 */
	public function get_avatar_url() {
		return get_avatar_url( $this->get_email() );
	}

	/**
	 * Get taxable address.
	 *
	 * @return array
	 */
	public function get_taxable_address() {
		$tax_based_on = get_option( 'woocommerce_tax_based_on' );

		// Check shipping method at this point to see if we need special handling.
		if ( true === apply_filters( 'woocommerce_apply_base_tax_for_local_pickup', true ) && count( array_intersect( wc_get_chosen_shipping_method_ids(), apply_filters( 'woocommerce_local_pickup_methods', array( 'legacy_local_pickup', 'local_pickup' ) ) ) ) > 0 ) {
			$tax_based_on = 'base';
		}

		if ( 'base' === $tax_based_on ) {
			$country  = WC()->countries->get_base_country();
			$state    = WC()->countries->get_base_state();
			$postcode = WC()->countries->get_base_postcode();
			$city     = WC()->countries->get_base_city();
		} elseif ( 'billing' === $tax_based_on ) {
			$country  = $this->get_billing_country();
			$state    = $this->get_billing_state();
			$postcode = $this->get_billing_postcode();
			$city     = $this->get_billing_city();
		} else {
			$country  = $this->get_shipping_country();
			$state    = $this->get_shipping_state();
			$postcode = $this->get_shipping_postcode();
			$city     = $this->get_shipping_city();
		}

		return apply_filters( 'woocommerce_customer_taxable_address', array( $country, $state, $postcode, $city ) );
	}

	/**
	 * Gets a customer's downloadable products.
	 *
	 * @return array Array of downloadable products
	 */
	public function get_downloadable_products() {
		$downloads = array();
		if ( $this->get_id() ) {
			$downloads = wc_get_customer_available_downloads( $this->get_id() );
		}
		return apply_filters( 'woocommerce_customer_get_downloadable_products', $downloads );
	}

	/**
	 * Is customer VAT exempt?
	 *
	 * @return bool
	 */
	public function is_vat_exempt() {
		return $this->get_is_vat_exempt();
	}

	/**
	 * Has calculated shipping?
	 *
	 * @return bool
	 */
	public function has_calculated_shipping() {
		return $this->get_calculated_shipping();
	}

	/**
	 * Indicates if the customer has a non-empty shipping address.
	 *
	 * Note that this does not indicate if the customer's shipping address
	 * is complete, only that one or more fields are populated.
	 *
	 * @since 5.3.0
	 *
	 * @return bool
	 */
	public function has_shipping_address() {
		foreach ( $this->get_shipping() as $address_field ) {
			// Trim guards against a case where a subset of saved shipping address fields contain whitespace.
			if ( strlen( trim( $address_field ) ) > 0 ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Get if customer is VAT exempt?
	 *
	 * @since 3.0.0
	 * @return bool
	 */
	public function get_is_vat_exempt() {
		return $this->is_vat_exempt;
	}

	/**
	 * Get password (only used when updating the user object).
	 *
	 * @return string
	 */
	public function get_password() {
		return $this->password;
	}

	/**
	 * Has customer calculated shipping?
	 *
	 * @return bool
	 */
	public function get_calculated_shipping() {
		return $this->calculated_shipping;
	}

	/**
	 * Set if customer has tax exemption.
	 *
	 * @param bool $is_vat_exempt If is vat exempt.
	 */
	public function set_is_vat_exempt( $is_vat_exempt ) {
		$this->is_vat_exempt = wc_string_to_bool( $is_vat_exempt );
	}

	/**
	 * Calculated shipping?
	 *
	 * @param bool $calculated If shipping is calculated.
	 */
	public function set_calculated_shipping( $calculated = true ) {
		$this->calculated_shipping = wc_string_to_bool( $calculated );
	}

	/**
	 * Set customer's password.
	 *
	 * @since 3.0.0
	 * @param string $password Password.
	 */
	public function set_password( $password ) {
		$this->password = $password;
	}

	/**
	 * Gets the customers last order.
	 *
	 * @return WC_Order|false
	 */
	public function get_last_order() {
		return $this->data_store->get_last_order( $this );
	}

	/**
	 * Return the number of orders this customer has.
	 *
	 * @return integer
	 */
	public function get_order_count() {
		return $this->data_store->get_order_count( $this );
	}

	/**
	 * Return how much money this customer has spent.
	 *
	 * @return float
	 */
	public function get_total_spent() {
		return $this->data_store->get_total_spent( $this );
	}

	/*
	|--------------------------------------------------------------------------
	| Getters
	|--------------------------------------------------------------------------
	*/

	/**
	 * Return the customer's username.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_username( $context = 'view' ) {
		return $this->get_prop( 'username', $context );
	}

	/**
	 * Return the customer's email.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_email( $context = 'view' ) {
		return $this->get_prop( 'email', $context );
	}

	/**
	 * Return customer's first name.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_first_name( $context = 'view' ) {
		return $this->get_prop( 'first_name', $context );
	}

	/**
	 * Return customer's last name.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_last_name( $context = 'view' ) {
		return $this->get_prop( 'last_name', $context );
	}

	/**
	 * Return customer's display name.
	 *
	 * @since  3.1.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_display_name( $context = 'view' ) {
		return $this->get_prop( 'display_name', $context );
	}

	/**
	 * Return customer's user role.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_role( $context = 'view' ) {
		return $this->get_prop( 'role', $context );
	}

	/**
	 * Return the date this customer was created.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return WC_DateTime|null object if the date is set or null if there is no date.
	 */
	public function get_date_created( $context = 'view' ) {
		return $this->get_prop( 'date_created', $context );
	}

	/**
	 * Return the date this customer was last updated.
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return WC_DateTime|null object if the date is set or null if there is no date.
	 */
	public function get_date_modified( $context = 'view' ) {
		return $this->get_prop( 'date_modified', $context );
	}

	/**
	 * Gets a prop for a getter method.
	 *
	 * @since  3.0.0
	 * @param  string $prop Name of prop to get.
	 * @param  string $address billing or shipping.
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'. What the value is for. Valid values are view and edit.
	 * @return mixed
	 */
	protected function get_address_prop( $prop, $address = 'billing', $context = 'view' ) {
		$value = null;

		if ( array_key_exists( $prop, $this->data[ $address ] ) ) {
			$value = isset( $this->changes[ $address ][ $prop ] ) ? $this->changes[ $address ][ $prop ] : $this->data[ $address ][ $prop ];

			if ( 'view' === $context ) {
				$value = apply_filters( $this->get_hook_prefix() . $address . '_' . $prop, $value, $this );
			}
		}
		return $value;
	}

	/**
	 * Get billing.
	 *
	 * @since  3.2.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return array
	 */
	public function get_billing( $context = 'view' ) {
		$value = null;
		$prop  = 'billing';

		if ( array_key_exists( $prop, $this->data ) ) {
			$changes = array_key_exists( $prop, $this->changes ) ? $this->changes[ $prop ] : array();
			$value   = array_merge( $this->data[ $prop ], $changes );

			if ( 'view' === $context ) {
				$value = apply_filters( $this->get_hook_prefix() . $prop, $value, $this );
			}
		}

		return $value;
	}

	/**
	 * Get billing_first_name.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_billing_first_name( $context = 'view' ) {
		return $this->get_address_prop( 'first_name', 'billing', $context );
	}

	/**
	 * Get billing_last_name.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_billing_last_name( $context = 'view' ) {
		return $this->get_address_prop( 'last_name', 'billing', $context );
	}

	/**
	 * Get billing_company.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_billing_company( $context = 'view' ) {
		return $this->get_address_prop( 'company', 'billing', $context );
	}

	/**
	 * Get billing_address_1.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_billing_address( $context = 'view' ) {
		return $this->get_billing_address_1( $context );
	}

	/**
	 * Get billing_address_1.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_billing_address_1( $context = 'view' ) {
		return $this->get_address_prop( 'address_1', 'billing', $context );
	}

	/**
	 * Get billing_address_2.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string $value
	 */
	public function get_billing_address_2( $context = 'view' ) {
		return $this->get_address_prop( 'address_2', 'billing', $context );
	}

	/**
	 * Get billing_city.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string $value
	 */
	public function get_billing_city( $context = 'view' ) {
		return $this->get_address_prop( 'city', 'billing', $context );
	}

	/**
	 * Get billing_state.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_billing_state( $context = 'view' ) {
		return $this->get_address_prop( 'state', 'billing', $context );
	}

	/**
	 * Get billing_postcode.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_billing_postcode( $context = 'view' ) {
		return $this->get_address_prop( 'postcode', 'billing', $context );
	}

	/**
	 * Get billing_country.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_billing_country( $context = 'view' ) {
		return $this->get_address_prop( 'country', 'billing', $context );
	}

	/**
	 * Get billing_email.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_billing_email( $context = 'view' ) {
		return $this->get_address_prop( 'email', 'billing', $context );
	}

	/**
	 * Get billing_phone.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_billing_phone( $context = 'view' ) {
		return $this->get_address_prop( 'phone', 'billing', $context );
	}

	/**
	 * Get shipping.
	 *
	 * @since  3.2.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return array
	 */
	public function get_shipping( $context = 'view' ) {
		$value = null;
		$prop  = 'shipping';

		if ( array_key_exists( $prop, $this->data ) ) {
			$changes = array_key_exists( $prop, $this->changes ) ? $this->changes[ $prop ] : array();
			$value   = array_merge( $this->data[ $prop ], $changes );

			if ( 'view' === $context ) {
				$value = apply_filters( $this->get_hook_prefix() . $prop, $value, $this );
			}
		}

		return $value;
	}

	/**
	 * Get shipping_first_name.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_shipping_first_name( $context = 'view' ) {
		return $this->get_address_prop( 'first_name', 'shipping', $context );
	}

	/**
	 * Get shipping_last_name.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_shipping_last_name( $context = 'view' ) {
		return $this->get_address_prop( 'last_name', 'shipping', $context );
	}

	/**
	 * Get shipping_company.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_shipping_company( $context = 'view' ) {
		return $this->get_address_prop( 'company', 'shipping', $context );
	}

	/**
	 * Get shipping_address_1.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_shipping_address( $context = 'view' ) {
		return $this->get_shipping_address_1( $context );
	}

	/**
	 * Get shipping_address_1.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_shipping_address_1( $context = 'view' ) {
		return $this->get_address_prop( 'address_1', 'shipping', $context );
	}

	/**
	 * Get shipping_address_2.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_shipping_address_2( $context = 'view' ) {
		return $this->get_address_prop( 'address_2', 'shipping', $context );
	}

	/**
	 * Get shipping_city.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_shipping_city( $context = 'view' ) {
		return $this->get_address_prop( 'city', 'shipping', $context );
	}

	/**
	 * Get shipping_state.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_shipping_state( $context = 'view' ) {
		return $this->get_address_prop( 'state', 'shipping', $context );
	}

	/**
	 * Get shipping_postcode.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_shipping_postcode( $context = 'view' ) {
		return $this->get_address_prop( 'postcode', 'shipping', $context );
	}

	/**
	 * Get shipping_country.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_shipping_country( $context = 'view' ) {
		return $this->get_address_prop( 'country', 'shipping', $context );
	}

	/**
	 * Get shipping phone.
	 *
	 * @since 5.6.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_shipping_phone( $context = 'view' ) {
		return $this->get_address_prop( 'phone', 'shipping', $context );
	}

	/**
	 * Is the user a paying customer?
	 *
	 * @since  3.0.0
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return bool
	 */
	public function get_is_paying_customer( $context = 'view' ) {
		return $this->get_prop( 'is_paying_customer', $context );
	}

	/*
	|--------------------------------------------------------------------------
	| Setters
	|--------------------------------------------------------------------------
	*/

	/**
	 * Set customer's username.
	 *
	 * @since 3.0.0
	 * @param string $username Username.
	 */
	public function set_username( $username ) {
		$this->set_prop( 'username', $username );
	}

	/**
	 * Set customer's email.
	 *
	 * @since 3.0.0
	 * @param string $value Email.
	 */
	public function set_email( $value ) {
		if ( $value && ! is_email( $value ) ) {
			$this->error( 'customer_invalid_email', __( 'Invalid email address', 'woocommerce' ) );
		}
		$this->set_prop( 'email', sanitize_email( $value ) );
	}

	/**
	 * Set customer's first name.
	 *
	 * @since 3.0.0
	 * @param string $first_name First name.
	 */
	public function set_first_name( $first_name ) {
		$this->set_prop( 'first_name', $first_name );
	}

	/**
	 * Set customer's last name.
	 *
	 * @since 3.0.0
	 * @param string $last_name Last name.
	 */
	public function set_last_name( $last_name ) {
		$this->set_prop( 'last_name', $last_name );
	}

	/**
	 * Set customer's display name.
	 *
	 * @since 3.1.0
	 * @param string $display_name Display name.
	 */
	public function set_display_name( $display_name ) {
		/* translators: 1: first name 2: last name */
		$this->set_prop( 'display_name', is_email( $display_name ) ? sprintf( _x( '%1$s %2$s', 'display name', 'woocommerce' ), $this->get_first_name(), $this->get_last_name() ) : $display_name );
	}

	/**
	 * Set customer's user role(s).
	 *
	 * @since 3.0.0
	 * @param mixed $role User role.
	 */
	public function set_role( $role ) {
		global $wp_roles;

		if ( $role && ! empty( $wp_roles->roles ) && ! in_array( $role, array_keys( $wp_roles->roles ), true ) ) {
			$this->error( 'customer_invalid_role', __( 'Invalid role', 'woocommerce' ) );
		}
		$this->set_prop( 'role', $role );
	}

	/**
	 * Set the date this customer was last updated.
	 *
	 * @since  3.0.0
	 * @param  string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date.
	 */
	public function set_date_created( $date = null ) {
		$this->set_date_prop( 'date_created', $date );
	}

	/**
	 * Set the date this customer was last updated.
	 *
	 * @since  3.0.0
	 * @param  string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date.
	 */
	public function set_date_modified( $date = null ) {
		$this->set_date_prop( 'date_modified', $date );
	}

	/**
	 * Set customer address to match shop base address.
	 *
	 * @since 3.0.0
	 */
	public function set_billing_address_to_base() {
		$base = wc_get_customer_default_location();
		$this->set_billing_location( $base['country'], $base['state'], '', '' );
	}

	/**
	 * Set customer shipping address to base address.
	 *
	 * @since 3.0.0
	 */
	public function set_shipping_address_to_base() {
		$base = wc_get_customer_default_location();
		$this->set_shipping_location( $base['country'], $base['state'], '', '' );
	}

	/**
	 * Sets all address info at once.
	 *
	 * @param string $country  Country.
	 * @param string $state    State.
	 * @param string $postcode Postcode.
	 * @param string $city     City.
	 */
	public function set_billing_location( $country, $state = '', $postcode = '', $city = '' ) {
		$address_data = $this->get_prop( 'billing', 'edit' );

		$address_data['address_1'] = '';
		$address_data['address_2'] = '';
		$address_data['city']      = $city;
		$address_data['state']     = $state;
		$address_data['postcode']  = $postcode;
		$address_data['country']   = $country;

		$this->set_prop( 'billing', $address_data );
	}

	/**
	 * Sets all shipping info at once.
	 *
	 * @param string $country  Country.
	 * @param string $state    State.
	 * @param string $postcode Postcode.
	 * @param string $city     City.
	 */
	public function set_shipping_location( $country, $state = '', $postcode = '', $city = '' ) {
		$address_data = $this->get_prop( 'shipping', 'edit' );

		$address_data['address_1'] = '';
		$address_data['address_2'] = '';
		$address_data['city']      = $city;
		$address_data['state']     = $state;
		$address_data['postcode']  = $postcode;
		$address_data['country']   = $country;

		$this->set_prop( 'shipping', $address_data );
	}

	/**
	 * Sets a prop for a setter method.
	 *
	 * @since 3.0.0
	 * @param string $prop    Name of prop to set.
	 * @param string $address Name of address to set. billing or shipping.
	 * @param mixed  $value   Value of the prop.
	 */
	protected function set_address_prop( $prop, $address, $value ) {
		if ( array_key_exists( $prop, $this->data[ $address ] ) ) {
			if ( true === $this->object_read ) {
				if ( $value !== $this->data[ $address ][ $prop ] || ( isset( $this->changes[ $address ] ) && array_key_exists( $prop, $this->changes[ $address ] ) ) ) {
					$this->changes[ $address ][ $prop ] = $value;
				}
			} else {
				$this->data[ $address ][ $prop ] = $value;
			}
		}
	}

	/**
	 * Set billing_first_name.
	 *
	 * @param string $value Billing first name.
	 */
	public function set_billing_first_name( $value ) {
		$this->set_address_prop( 'first_name', 'billing', $value );
	}

	/**
	 * Set billing_last_name.
	 *
	 * @param string $value Billing last name.
	 */
	public function set_billing_last_name( $value ) {
		$this->set_address_prop( 'last_name', 'billing', $value );
	}

	/**
	 * Set billing_company.
	 *
	 * @param string $value Billing company.
	 */
	public function set_billing_company( $value ) {
		$this->set_address_prop( 'company', 'billing', $value );
	}

	/**
	 * Set billing_address_1.
	 *
	 * @param string $value Billing address line 1.
	 */
	public function set_billing_address( $value ) {
		$this->set_billing_address_1( $value );
	}

	/**
	 * Set billing_address_1.
	 *
	 * @param string $value Billing address line 1.
	 */
	public function set_billing_address_1( $value ) {
		$this->set_address_prop( 'address_1', 'billing', $value );
	}

	/**
	 * Set billing_address_2.
	 *
	 * @param string $value Billing address line 2.
	 */
	public function set_billing_address_2( $value ) {
		$this->set_address_prop( 'address_2', 'billing', $value );
	}

	/**
	 * Set billing_city.
	 *
	 * @param string $value Billing city.
	 */
	public function set_billing_city( $value ) {
		$this->set_address_prop( 'city', 'billing', $value );
	}

	/**
	 * Set billing_state.
	 *
	 * @param string $value Billing state.
	 */
	public function set_billing_state( $value ) {
		$this->set_address_prop( 'state', 'billing', $value );
	}

	/**
	 * Set billing_postcode.
	 *
	 * @param string $value Billing postcode.
	 */
	public function set_billing_postcode( $value ) {
		$this->set_address_prop( 'postcode', 'billing', $value );
	}

	/**
	 * Set billing_country.
	 *
	 * @param string $value Billing country.
	 */
	public function set_billing_country( $value ) {
		$this->set_address_prop( 'country', 'billing', $value );
	}

	/**
	 * Set billing_email.
	 *
	 * @param string $value Billing email.
	 */
	public function set_billing_email( $value ) {
		if ( $value && ! is_email( $value ) ) {
			$this->error( 'customer_invalid_billing_email', __( 'Invalid billing email address', 'woocommerce' ) );
		}
		$this->set_address_prop( 'email', 'billing', sanitize_email( $value ) );
	}

	/**
	 * Set billing_phone.
	 *
	 * @param string $value Billing phone.
	 */
	public function set_billing_phone( $value ) {
		$this->set_address_prop( 'phone', 'billing', $value );
	}

	/**
	 * Set shipping_first_name.
	 *
	 * @param string $value Shipping first name.
	 */
	public function set_shipping_first_name( $value ) {
		$this->set_address_prop( 'first_name', 'shipping', $value );
	}

	/**
	 * Set shipping_last_name.
	 *
	 * @param string $value Shipping last name.
	 */
	public function set_shipping_last_name( $value ) {
		$this->set_address_prop( 'last_name', 'shipping', $value );
	}

	/**
	 * Set shipping_company.
	 *
	 * @param string $value Shipping company.
	 */
	public function set_shipping_company( $value ) {
		$this->set_address_prop( 'company', 'shipping', $value );
	}

	/**
	 * Set shipping_address_1.
	 *
	 * @param string $value Shipping address line 1.
	 */
	public function set_shipping_address( $value ) {
		$this->set_shipping_address_1( $value );
	}

	/**
	 * Set shipping_address_1.
	 *
	 * @param string $value Shipping address line 1.
	 */
	public function set_shipping_address_1( $value ) {
		$this->set_address_prop( 'address_1', 'shipping', $value );
	}

	/**
	 * Set shipping_address_2.
	 *
	 * @param string $value Shipping address line 2.
	 */
	public function set_shipping_address_2( $value ) {
		$this->set_address_prop( 'address_2', 'shipping', $value );
	}

	/**
	 * Set shipping_city.
	 *
	 * @param string $value Shipping city.
	 */
	public function set_shipping_city( $value ) {
		$this->set_address_prop( 'city', 'shipping', $value );
	}

	/**
	 * Set shipping_state.
	 *
	 * @param string $value Shipping state.
	 */
	public function set_shipping_state( $value ) {
		$this->set_address_prop( 'state', 'shipping', $value );
	}

	/**
	 * Set shipping_postcode.
	 *
	 * @param string $value Shipping postcode.
	 */
	public function set_shipping_postcode( $value ) {
		$this->set_address_prop( 'postcode', 'shipping', $value );
	}

	/**
	 * Set shipping_country.
	 *
	 * @param string $value Shipping country.
	 */
	public function set_shipping_country( $value ) {
		$this->set_address_prop( 'country', 'shipping', $value );
	}

	/**
	 * Set shipping phone.
	 *
	 * @since 5.6.0
	 * @param string $value Shipping phone.
	 */
	public function set_shipping_phone( $value ) {
		$this->set_address_prop( 'phone', 'shipping', $value );
	}

	/**
	 * Set if the user a paying customer.
	 *
	 * @since 3.0.0
	 * @param bool $is_paying_customer If is a paying customer.
	 */
	public function set_is_paying_customer( $is_paying_customer ) {
		$this->set_prop( 'is_paying_customer', (bool) $is_paying_customer );
	}
}
wccom-site/class-wc-wccom-site-installer-requirements-check.php000064400000002612151336044770020715 0ustar00<?php
/**
 * WooCommerce.com Product Installation Requirements Check.
 *
 * @package WooCommerce\WCCom
 * @since   3.8.0
 */

use Automattic\Jetpack\Constants;

defined( 'ABSPATH' ) || exit;

/**
 * WC_WCCOM_Site_Installer_Requirements_Check Class
 * Contains functionality to check the necessary requirements for the installer.
 */
class WC_WCCOM_Site_Installer_Requirements_Check {
	/**
	 * Check if the site met the requirements
	 *
	 * @version 3.8.0
	 * @return bool|WP_Error Does the site met the requirements?
	 */
	public static function met_requirements() {
		$errs = array();

		if ( ! self::met_wp_cron_requirement() ) {
			$errs[] = 'wp-cron';
		}

		if ( ! self::met_filesystem_requirement() ) {
			$errs[] = 'filesystem';
		}

		if ( ! empty( $errs ) ) {
			// translators: %s: Requirements unmet.
			return new WP_Error( 'requirements_not_met', sprintf( __( 'Server requirements not met, missing requirement(s): %s.', 'woocommerce' ), implode( ', ', $errs ) ), array( 'status' => 400 ) );
		}

		return true;
	}

	/**
	 * Validates if WP CRON is enabled.
	 *
	 * @since 3.8.0
	 * @return bool
	 */
	private static function met_wp_cron_requirement() {
		return ! Constants::is_true( 'DISABLE_WP_CRON' );
	}

	/**
	 * Validates if `WP_CONTENT_DIR` is writable.
	 *
	 * @since 3.8.0
	 * @return bool
	 */
	private static function met_filesystem_requirement() {
		return is_writable( WP_CONTENT_DIR );
	}
}
wccom-site/class-wc-wccom-site.php000064400000020540151336044770013126 0ustar00<?php
/**
 * WooCommerce.com Product Installation.
 *
 * @package WooCommerce\WCCom
 * @since   3.7.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * WC_WCCOM_Site Class
 *
 * Main class for WooCommerce.com connected site.
 */
class WC_WCCOM_Site {

	const AUTH_ERROR_FILTER_NAME = 'wccom_auth_error';

	/**
	 * Load the WCCOM site class.
	 *
	 * @since 3.7.0
	 */
	public static function load() {
		self::includes();

		add_action( 'woocommerce_wccom_install_products', array( 'WC_WCCOM_Site_Installer', 'install' ) );
		add_filter( 'determine_current_user', array( __CLASS__, 'authenticate_wccom' ), 14 );
		add_action( 'woocommerce_rest_api_get_rest_namespaces', array( __CLASS__, 'register_rest_namespace' ) );
	}

	/**
	 * Include support files.
	 *
	 * @since 3.7.0
	 */
	protected static function includes() {
		require_once WC_ABSPATH . 'includes/admin/helper/class-wc-helper.php';
		require_once WC_ABSPATH . 'includes/wccom-site/class-wc-wccom-site-installer.php';
		require_once WC_ABSPATH . 'includes/wccom-site/class-wc-wccom-site-installer-requirements-check.php';
	}

	/**
	 * Authenticate WooCommerce.com request.
	 *
	 * @since 3.7.0
	 * @param int|false $user_id User ID.
	 * @return int|false
	 */
	public static function authenticate_wccom( $user_id ) {
		if ( ! empty( $user_id ) || ! self::is_request_to_wccom_site_rest_api() ) {
			return $user_id;
		}

		$auth_header = trim( self::get_authorization_header() );

		if ( stripos( $auth_header, 'Bearer ' ) === 0 ) {
			$access_token = trim( substr( $auth_header, 7 ) );
		} elseif ( ! empty( $_GET['token'] ) && is_string( $_GET['token'] ) ) {  // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$access_token = trim( $_GET['token'] );  // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		} else {
			add_filter(
				self::AUTH_ERROR_FILTER_NAME,
				function() {
					return new WP_Error(
						WC_REST_WCCOM_Site_Installer_Errors::NO_ACCESS_TOKEN_CODE,
						WC_REST_WCCOM_Site_Installer_Errors::NO_ACCESS_TOKEN_MESSAGE,
						array( 'status' => WC_REST_WCCOM_Site_Installer_Errors::NO_ACCESS_TOKEN_HTTP_CODE )
					);
				}
			);
			return false;
		}

		if ( ! empty( $_SERVER['HTTP_X_WOO_SIGNATURE'] ) ) {
			$signature = trim( $_SERVER['HTTP_X_WOO_SIGNATURE'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		} elseif ( ! empty( $_GET['signature'] ) && is_string( $_GET['signature'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$signature = trim( $_GET['signature'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		} else {
			add_filter(
				self::AUTH_ERROR_FILTER_NAME,
				function() {
					return new WP_Error(
						WC_REST_WCCOM_Site_Installer_Errors::NO_SIGNATURE_CODE,
						WC_REST_WCCOM_Site_Installer_Errors::NO_SIGNATURE_MESSAGE,
						array( 'status' => WC_REST_WCCOM_Site_Installer_Errors::NO_SIGNATURE_HTTP_CODE )
					);
				}
			);
			return false;
		}

		require_once WC_ABSPATH . 'includes/admin/helper/class-wc-helper-options.php';
		$site_auth = WC_Helper_Options::get( 'auth' );

		if ( empty( $site_auth['access_token'] ) ) {
			add_filter(
				self::AUTH_ERROR_FILTER_NAME,
				function() {
					return new WP_Error(
						WC_REST_WCCOM_Site_Installer_Errors::SITE_NOT_CONNECTED_CODE,
						WC_REST_WCCOM_Site_Installer_Errors::SITE_NOT_CONNECTED_MESSAGE,
						array( 'status' => WC_REST_WCCOM_Site_Installer_Errors::SITE_NOT_CONNECTED_HTTP_CODE )
					);
				}
			);
			return false;
		}

		if ( ! hash_equals( $access_token, $site_auth['access_token'] ) ) {
			add_filter(
				self::AUTH_ERROR_FILTER_NAME,
				function() {
					return new WP_Error(
						WC_REST_WCCOM_Site_Installer_Errors::INVALID_TOKEN_CODE,
						WC_REST_WCCOM_Site_Installer_Errors::INVALID_TOKEN_MESSAGE,
						array( 'status' => WC_REST_WCCOM_Site_Installer_Errors::INVALID_TOKEN_HTTP_CODE )
					);
				}
			);
			return false;
		}

		$body = WP_REST_Server::get_raw_data();

		if ( ! self::verify_wccom_request( $body, $signature, $site_auth['access_token_secret'] ) ) {
			add_filter(
				self::AUTH_ERROR_FILTER_NAME,
				function() {
					return new WP_Error(
						WC_REST_WCCOM_Site_Installer_Errors::REQUEST_VERIFICATION_FAILED_CODE,
						WC_REST_WCCOM_Site_Installer_Errors::REQUEST_VERIFICATION_FAILED_MESSAGE,
						array( 'status' => WC_REST_WCCOM_Site_Installer_Errors::REQUEST_VERIFICATION_FAILED_HTTP_CODE )
					);
				}
			);
			return false;
		}

		$user = get_user_by( 'id', $site_auth['user_id'] );
		if ( ! $user ) {
			add_filter(
				self::AUTH_ERROR_FILTER_NAME,
				function() {
					return new WP_Error(
						WC_REST_WCCOM_Site_Installer_Errors::USER_NOT_FOUND_CODE,
						WC_REST_WCCOM_Site_Installer_Errors::USER_NOT_FOUND_MESSAGE,
						array( 'status' => WC_REST_WCCOM_Site_Installer_Errors::USER_NOT_FOUND_HTTP_CODE )
					);
				}
			);
			return false;
		}

		return $user;
	}

	/**
	 * Get the authorization header.
	 *
	 * On certain systems and configurations, the Authorization header will be
	 * stripped out by the server or PHP. Typically this is then used to
	 * generate `PHP_AUTH_USER`/`PHP_AUTH_PASS` but not passed on. We use
	 * `getallheaders` here to try and grab it out instead.
	 *
	 * @since 3.7.0
	 * @return string Authorization header if set.
	 */
	protected static function get_authorization_header() {
		if ( ! empty( $_SERVER['HTTP_AUTHORIZATION'] ) ) {
			return wp_unslash( $_SERVER['HTTP_AUTHORIZATION'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		}

		if ( function_exists( 'getallheaders' ) ) {
			$headers = getallheaders();
			// Check for the authoization header case-insensitively.
			foreach ( $headers as $key => $value ) {
				if ( 'authorization' === strtolower( $key ) ) {
					return $value;
				}
			}
		}

		return '';
	}

	/**
	 * Check if this is a request to WCCOM Site REST API.
	 *
	 * @since 3.7.0
	 * @return bool
	 */
	protected static function is_request_to_wccom_site_rest_api() {

		if ( isset( $_REQUEST['rest_route'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$route       = wp_unslash( $_REQUEST['rest_route'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended
			$rest_prefix = '';
		} else {
			$route       = wp_unslash( add_query_arg( array() ) );
			$rest_prefix = trailingslashit( rest_get_url_prefix() );
		}

		return false !== strpos( $route, $rest_prefix . 'wccom-site/' );
	}

	/**
	 * Verify WooCommerce.com request from a given body and signature request.
	 *
	 * @since 3.7.0
	 * @param string $body                Request body.
	 * @param string $signature           Request signature found in X-Woo-Signature header.
	 * @param string $access_token_secret Access token secret for this site.
	 * @return bool
	 */
	protected static function verify_wccom_request( $body, $signature, $access_token_secret ) {
		// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$data = array(
			'host'        => $_SERVER['HTTP_HOST'],
			'request_uri' => urldecode( remove_query_arg( array( 'token', 'signature' ), $_SERVER['REQUEST_URI'] ) ),
			'method'      => strtoupper( $_SERVER['REQUEST_METHOD'] ),
		);
		// phpcs:enable

		if ( ! empty( $body ) ) {
			$data['body'] = $body;
		}

		$expected_signature = hash_hmac( 'sha256', wp_json_encode( $data ), $access_token_secret );

		return hash_equals( $expected_signature, $signature );
	}

	/**
	 * Register wccom-site REST namespace.
	 *
	 * @since 3.7.0
	 * @param array $namespaces List of registered namespaces.
	 * @return array Registered namespaces.
	 */
	public static function register_rest_namespace( $namespaces ) {
		require_once WC_ABSPATH . 'includes/wccom-site/rest-api/class-wc-rest-wccom-site-installer-errors.php';
		require_once WC_ABSPATH . 'includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-installer-controller.php';

		$namespaces['wccom-site/v1'] = array(
			'installer' => 'WC_REST_WCCOM_Site_Installer_Controller',
		);

		return $namespaces;
	}
}

WC_WCCOM_Site::load();
wccom-site/class-wc-wccom-site-installer.php000064400000037166151336044770015135 0ustar00<?php
/**
 * WooCommerce.com Product Installation.
 *
 * @package WooCommerce\WCCom
 * @since   3.7.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * WC_WCCOM_Site_Installer Class
 *
 * Contains functionalities to install products via WooCommerce.com helper connection.
 */
class WC_WCCOM_Site_Installer {

	/**
	 * Error message returned install_package if the folder already exists.
	 *
	 * @var string
	 */
	private static $folder_exists = 'folder_exists';

	/**
	 * Default state.
	 *
	 * @var array
	 */
	private static $default_state = array(
		'status'       => 'idle',
		'steps'        => array(),
		'current_step' => null,
	);

	/**
	 * Represents product step state.
	 *
	 * @var array
	 */
	private static $default_step_state = array(
		'download_url'   => '',
		'product_type'   => '',
		'last_step'      => '',
		'last_error'     => '',
		'download_path'  => '',
		'unpacked_path'  => '',
		'installed_path' => '',
		'activate'       => false,
	);

	/**
	 * Product install steps. Each step is a method name in this class that
	 * will be passed with product ID arg \WP_Upgrader instance.
	 *
	 * @var array
	 */
	private static $install_steps = array(
		'get_product_info',
		'download_product',
		'unpack_product',
		'move_product',
		'activate_product',
	);

	/**
	 * Get the product install state.
	 *
	 * @since 3.7.0
	 * @param string $key Key in state data. If empty key is passed array of
	 *                    state will be returned.
	 * @return array Product install state.
	 */
	public static function get_state( $key = '' ) {
		$state = WC_Helper_Options::get( 'product_install', self::$default_state );
		if ( ! empty( $key ) ) {
			return isset( $state[ $key ] ) ? $state[ $key ] : null;
		}

		return $state;
	}

	/**
	 * Update the product install state.
	 *
	 * @since 3.7.0
	 * @param string $key   Key in state data.
	 * @param mixed  $value Value.
	 */
	public static function update_state( $key, $value ) {
		$state = WC_Helper_Options::get( 'product_install', self::$default_state );

		$state[ $key ] = $value;
		WC_Helper_Options::update( 'product_install', $state );
	}

	/**
	 * Reset product install state.
	 *
	 * @since 3.7.0
	 * @param array $products List of product IDs.
	 */
	public static function reset_state( $products = array() ) {
		WC()->queue()->cancel_all( 'woocommerce_wccom_install_products' );
		WC_Helper_Options::update( 'product_install', self::$default_state );
	}

	/**
	 * Schedule installing given list of products.
	 *
	 * @since 3.7.0
	 * @param array $products Array of products where key is product ID and
	 *                        element is install args.
	 * @return array State.
	 */
	public static function schedule_install( $products ) {
		$state  = self::get_state();
		$status = ! empty( $state['status'] ) ? $state['status'] : '';
		if ( 'in-progress' === $status ) {
			return $state;
		}
		self::update_state( 'status', 'in-progress' );

		$steps = array_fill_keys( array_keys( $products ), self::$default_step_state );
		self::update_state( 'steps', $steps );

		self::update_state( 'current_step', null );

		$args = array(
			'products' => $products,
		);

		// Clear the cache of customer's subscription before asking for them.
		// Thus, they will be re-fetched from WooCommerce.com after a purchase.
		WC_Helper::_flush_subscriptions_cache();

		WC()->queue()->cancel_all( 'woocommerce_wccom_install_products', $args );
		WC()->queue()->add( 'woocommerce_wccom_install_products', $args );

		return self::get_state();
	}

	/**
	 * Install a given product IDs.
	 *
	 * Run via `woocommerce_wccom_install_products` hook.
	 *
	 * @since 3.7.0
	 * @param array $products Array of products where key is product ID and
	 *                        element is install args.
	 */
	public static function install( $products ) {
		require_once ABSPATH . 'wp-admin/includes/file.php';
		require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
		require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
		require_once ABSPATH . 'wp-admin/includes/plugin.php';

		WP_Filesystem();
		$upgrader = new WP_Upgrader( new Automatic_Upgrader_Skin() );
		$upgrader->init();
		wp_clean_plugins_cache();

		foreach ( $products as $product_id => $install_args ) {
			self::install_product( $product_id, $install_args, $upgrader );
		}

		self::finish_installation();
	}

	/**
	 * Finish installation by updating the state.
	 *
	 * @since 3.7.0
	 */
	private static function finish_installation() {
		$state = self::get_state();
		if ( empty( $state['steps'] ) ) {
			return;
		}

		foreach ( $state['steps'] as $step ) {
			if ( ! empty( $step['last_error'] ) ) {
				$state['status'] = 'has_error';
				break;
			}
		}

		if ( 'has_error' !== $state['status'] ) {
			$state['status'] = 'finished';
		}

		WC_Helper_Options::update( 'product_install', $state );
	}

	/**
	 * Install a single product given its ID.
	 *
	 * @since 3.7.0
	 * @param int          $product_id   Product ID.
	 * @param array        $install_args Install args.
	 * @param \WP_Upgrader $upgrader     Core class to handle installation.
	 */
	private static function install_product( $product_id, $install_args, $upgrader ) {
		foreach ( self::$install_steps as $step ) {
			self::do_install_step( $product_id, $install_args, $step, $upgrader );
		}
	}

	/**
	 * Perform product installation step.
	 *
	 * @since 3.7.0
	 * @param int          $product_id   Product ID.
	 * @param array        $install_args Install args.
	 * @param string       $step         Installation step.
	 * @param \WP_Upgrader $upgrader     Core class to handle installation.
	 */
	private static function do_install_step( $product_id, $install_args, $step, $upgrader ) {
		$state_steps = self::get_state( 'steps' );
		if ( empty( $state_steps[ $product_id ] ) ) {
			$state_steps[ $product_id ] = self::$default_step_state;
		}

		if ( ! empty( $state_steps[ $product_id ]['last_error'] ) ) {
			return;
		}

		$state_steps[ $product_id ]['last_step'] = $step;

		if ( ! empty( $install_args['activate'] ) ) {
			$state_steps[ $product_id ]['activate'] = true;
		}

		self::update_state(
			'current_step',
			array(
				'product_id' => $product_id,
				'step'       => $step,
			)
		);

		$result = call_user_func( array( __CLASS__, $step ), $product_id, $upgrader );
		if ( is_wp_error( $result ) ) {
			$state_steps[ $product_id ]['last_error'] = $result->get_error_message();
		} else {
			switch ( $step ) {
				case 'get_product_info':
					$state_steps[ $product_id ]['download_url'] = $result['download_url'];
					$state_steps[ $product_id ]['product_type'] = $result['product_type'];
					$state_steps[ $product_id ]['product_name'] = $result['product_name'];
					break;
				case 'download_product':
					$state_steps[ $product_id ]['download_path'] = $result;
					break;
				case 'unpack_product':
					$state_steps[ $product_id ]['unpacked_path'] = $result;
					break;
				case 'move_product':
					$state_steps[ $product_id ]['installed_path'] = $result['destination'];
					if ( isset( $result[ self::$folder_exists ] ) ) {
						$state_steps[ $product_id ]['warning'] = array(
							'message'     => self::$folder_exists,
							'plugin_info' => self::get_plugin_info( $state_steps[ $product_id ]['installed_path'] ),
						);
					}
					break;
			}
		}

		self::update_state( 'steps', $state_steps );
	}

	/**
	 * Get product info from its ID.
	 *
	 * @since 3.7.0
	 * @param int $product_id Product ID.
	 * @return array|\WP_Error
	 */
	private static function get_product_info( $product_id ) {
		$product_info = array(
			'download_url' => '',
			'product_type' => '',
		);

		// Get product info from woocommerce.com.
		$request = WC_Helper_API::get(
			add_query_arg(
				array( 'product_id' => absint( $product_id ) ),
				'info'
			),
			array(
				'authenticated' => true,
			)
		);

		if ( 200 !== wp_remote_retrieve_response_code( $request ) ) {
			return new WP_Error( 'product_info_failed', __( 'Failed to retrieve product info from woocommerce.com', 'woocommerce' ) );
		}

		$result = json_decode( wp_remote_retrieve_body( $request ), true );

		$product_info['product_type'] = $result['_product_type'];
		$product_info['product_name'] = $result['name'];

		if ( ! empty( $result['_wporg_product'] ) && ! empty( $result['download_link'] ) ) {
			// For wporg product, download is set already from info response.
			$product_info['download_url'] = $result['download_link'];
		} elseif ( ! WC_Helper::has_product_subscription( $product_id ) ) {
			// Non-wporg product needs subscription.
			return new WP_Error( 'missing_subscription', __( 'Missing product subscription', 'woocommerce' ) );
		} else {
			// Retrieve download URL for non-wporg product.
			WC_Helper_Updater::flush_updates_cache();
			$updates = WC_Helper_Updater::get_update_data();
			if ( empty( $updates[ $product_id ]['package'] ) ) {
				return new WP_Error( 'missing_product_package', __( 'Could not find product package.', 'woocommerce' ) );
			}

			$product_info['download_url'] = $updates[ $product_id ]['package'];
		}

		return $product_info;
	}

	/**
	 * Download product by its ID and returns the path of the zip package.
	 *
	 * @since 3.7.0
	 * @param int          $product_id Product ID.
	 * @param \WP_Upgrader $upgrader   Core class to handle installation.
	 * @return \WP_Error|string
	 */
	private static function download_product( $product_id, $upgrader ) {
		$steps = self::get_state( 'steps' );
		if ( empty( $steps[ $product_id ]['download_url'] ) ) {
			return new WP_Error( 'missing_download_url', __( 'Could not find download url for the product.', 'woocommerce' ) );
		}
		return $upgrader->download_package( $steps[ $product_id ]['download_url'] );
	}

	/**
	 * Unpack downloaded product.
	 *
	 * @since 3.7.0
	 * @param int          $product_id Product ID.
	 * @param \WP_Upgrader $upgrader   Core class to handle installation.
	 * @return \WP_Error|string
	 */
	private static function unpack_product( $product_id, $upgrader ) {
		$steps = self::get_state( 'steps' );
		if ( empty( $steps[ $product_id ]['download_path'] ) ) {
			return new WP_Error( 'missing_download_path', __( 'Could not find download path.', 'woocommerce' ) );
		}

		return $upgrader->unpack_package( $steps[ $product_id ]['download_path'], true );
	}

	/**
	 * Move product to plugins directory.
	 *
	 * @since 3.7.0
	 * @param int          $product_id Product ID.
	 * @param \WP_Upgrader $upgrader   Core class to handle installation.
	 * @return array|\WP_Error
	 */
	private static function move_product( $product_id, $upgrader ) {
		$steps = self::get_state( 'steps' );
		if ( empty( $steps[ $product_id ]['unpacked_path'] ) ) {
			return new WP_Error( 'missing_unpacked_path', __( 'Could not find unpacked path.', 'woocommerce' ) );
		}

		$destination = 'plugin' === $steps[ $product_id ]['product_type']
			? WP_PLUGIN_DIR
			: get_theme_root();

		$package = array(
			'source'        => $steps[ $product_id ]['unpacked_path'],
			'destination'   => $destination,
			'clear_working' => true,
			'hook_extra'    => array(
				'type'   => $steps[ $product_id ]['product_type'],
				'action' => 'install',
			),
		);

		$result = $upgrader->install_package( $package );

		/**
		 * If install package returns error 'folder_exists' threat as success.
		 */
		if ( is_wp_error( $result ) && array_key_exists( self::$folder_exists, $result->errors ) ) {
			return array(
				self::$folder_exists => true,
				'destination'        => $result->error_data[ self::$folder_exists ],
			);
		}
		return $result;
	}

	/**
	 * Activate product given its product ID.
	 *
	 * @since 3.7.0
	 * @param int $product_id Product ID.
	 * @return \WP_Error|null
	 */
	private static function activate_product( $product_id ) {
		$steps = self::get_state( 'steps' );
		if ( ! $steps[ $product_id ]['activate'] ) {
			return null;
		}

		if ( 'plugin' === $steps[ $product_id ]['product_type'] ) {
			return self::activate_plugin( $product_id );
		}
		return self::activate_theme( $product_id );
	}

	/**
	 * Activate plugin given its product ID.
	 *
	 * @since 3.7.0
	 * @param int $product_id Product ID.
	 * @return \WP_Error|null
	 */
	private static function activate_plugin( $product_id ) {
		// Clear plugins cache used in `WC_Helper::get_local_woo_plugins`.
		wp_clean_plugins_cache();
		$filename = false;

		// If product is WP.org one, find out its filename.
		$dir_name = self::get_wporg_product_dir_name( $product_id );
		if ( false !== $dir_name ) {
			$filename = self::get_wporg_plugin_main_file( $dir_name );
		}

		if ( false === $filename ) {
			$plugins = wp_list_filter(
				WC_Helper::get_local_woo_plugins(),
				array(
					'_product_id' => $product_id,
				)
			);

			$filename = is_array( $plugins ) && ! empty( $plugins ) ? key( $plugins ) : '';
		}

		if ( empty( $filename ) ) {
			return new WP_Error( 'unknown_filename', __( 'Unknown product filename.', 'woocommerce' ) );
		}

		return activate_plugin( $filename );
	}

	/**
	 * Activate theme given its product ID.
	 *
	 * @since 3.7.0
	 * @param int $product_id Product ID.
	 * @return \WP_Error|null
	 */
	private static function activate_theme( $product_id ) {
		// Clear plugins cache used in `WC_Helper::get_local_woo_themes`.
		wp_clean_themes_cache();
		$theme_slug = false;

		// If product is WP.org theme, find out its slug.
		$dir_name = self::get_wporg_product_dir_name( $product_id );
		if ( false !== $dir_name ) {
			$theme_slug = basename( $dir_name );
		}

		if ( false === $theme_slug ) {
			$themes = wp_list_filter(
				WC_Helper::get_local_woo_themes(),
				array(
					'_product_id' => $product_id,
				)
			);

			$theme_slug = is_array( $themes ) && ! empty( $themes ) ? dirname( key( $themes ) ) : '';
		}

		if ( empty( $theme_slug ) ) {
			return new WP_Error( 'unknown_filename', __( 'Unknown product filename.', 'woocommerce' ) );
		}

		return switch_theme( $theme_slug );
	}

	/**
	 * Get installed directory of WP.org product.
	 *
	 * @since 3.7.0
	 * @param int $product_id Product ID.
	 * @return bool|string
	 */
	private static function get_wporg_product_dir_name( $product_id ) {
		$steps   = self::get_state( 'steps' );
		$product = $steps[ $product_id ];

		if ( empty( $product['download_url'] ) || empty( $product['installed_path'] ) ) {
			return false;
		}

		// Check whether product was downloaded from WordPress.org.
		$parsed_url = wp_parse_url( $product['download_url'] );
		if ( ! empty( $parsed_url['host'] ) && 'downloads.wordpress.org' !== $parsed_url['host'] ) {
			return false;
		}

		return basename( $product['installed_path'] );
	}

	/**
	 * Get WP.org plugin's main file.
	 *
	 * @since 3.7.0
	 * @param string $dir Directory name of the plugin.
	 * @return bool|string
	 */
	private static function get_wporg_plugin_main_file( $dir ) {
		// Ensure that exact dir name is used.
		$dir = trailingslashit( $dir );

		if ( ! function_exists( 'get_plugins' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}

		$plugins = get_plugins();
		foreach ( $plugins as $path => $plugin ) {
			if ( 0 === strpos( $path, $dir ) ) {
				return $path;
			}
		}

		return false;
	}


	/**
	 * Get plugin info
	 *
	 * @since 3.9.0
	 * @param string $dir Directory name of the plugin.
	 * @return bool|array
	 */
	private static function get_plugin_info( $dir ) {
		$plugin_folder = basename( $dir );

		if ( ! function_exists( 'get_plugins' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}

		$plugins = get_plugins();

		$related_plugins = array_filter(
			$plugins,
			function( $key ) use ( $plugin_folder ) {
				return strpos( $key, $plugin_folder . '/' ) === 0;
			},
			ARRAY_FILTER_USE_KEY
		);

		if ( 1 === count( $related_plugins ) ) {
			$plugin_key  = array_keys( $related_plugins )[0];
			$plugin_data = $plugins[ $plugin_key ];
			return array(
				'name'    => $plugin_data['Name'],
				'version' => $plugin_data['Version'],
				'active'  => is_plugin_active( $plugin_key ),
			);
		}
		return false;
	}
}
wccom-site/rest-api/class-wc-rest-wccom-site-installer-errors.php000064400000003717151336044770021141 0ustar00<?php
/**
 * WCCOM Site Installer Errors Class
 *
 * @package WooCommerce\WCCom\API
 * @since   3.9.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * WCCOM Site Installer Errors Class
 *
 * Stores data for errors, returned by installer API.
 */
class WC_REST_WCCOM_Site_Installer_Errors {

	/**
	 * Not unauthenticated generic error
	 */
	const NOT_AUTHENTICATED_CODE      = 'not_authenticated';
	const NOT_AUTHENTICATED_MESSAGE   = 'Authentication required';
	const NOT_AUTHENTICATED_HTTP_CODE = 401;

	/**
	 * No access token provided
	 */
	const NO_ACCESS_TOKEN_CODE      = 'no_access_token';
	const NO_ACCESS_TOKEN_MESSAGE   = 'No access token provided';
	const NO_ACCESS_TOKEN_HTTP_CODE = 400;

	/**
	 * No signature provided
	 */
	const NO_SIGNATURE_CODE      = 'no_signature';
	const NO_SIGNATURE_MESSAGE   = 'No signature provided';
	const NO_SIGNATURE_HTTP_CODE = 400;

	/**
	 * Site not connected to WooCommerce.com
	 */
	const SITE_NOT_CONNECTED_CODE      = 'site_not_connnected';
	const SITE_NOT_CONNECTED_MESSAGE   = 'Site not connected to WooCommerce.com';
	const SITE_NOT_CONNECTED_HTTP_CODE = 401;

	/**
	* Provided access token is not valid
	*/
	const INVALID_TOKEN_CODE      = 'invalid_token';
	const INVALID_TOKEN_MESSAGE   = 'Invalid access token provided';
	const INVALID_TOKEN_HTTP_CODE = 401;

	/**
	 * Request verification by provided signature failed
	 */
	const REQUEST_VERIFICATION_FAILED_CODE      = 'request_verification_failed';
	const REQUEST_VERIFICATION_FAILED_MESSAGE   = 'Request verification by signature failed';
	const REQUEST_VERIFICATION_FAILED_HTTP_CODE = 400;

	/**
	 * User doesn't exist
	 */
	const USER_NOT_FOUND_CODE      = 'user_not_found';
	const USER_NOT_FOUND_MESSAGE   = 'Token owning user not found';
	const USER_NOT_FOUND_HTTP_CODE = 401;

	/**
	 * No permissions error
	 */
	const NO_PERMISSION_CODE      = 'forbidden';
	const NO_PERMISSION_MESSAGE   = 'You do not have permission to install plugin or theme';
	const NO_PERMISSION_HTTP_CODE = 403;
}
wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-installer-controller.php000064400000011261151336044770024004 0ustar00<?php
/**
 * WCCOM Site Installer REST API Controller
 *
 * Handles requests to /installer.
 *
 * @package WooCommerce\WCCom\API
 * @since   3.7.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API WCCOM Site Installer Controller Class.
 *
 * @extends WC_REST_Controller
 */
class WC_REST_WCCOM_Site_Installer_Controller extends WC_REST_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wccom-site/v1';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'installer';

	/**
	 * Register the routes for product reviews.
	 *
	 * @since 3.7.0
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_install_state' ),
					'permission_callback' => array( $this, 'check_permission' ),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'install' ),
					'permission_callback' => array( $this, 'check_permission' ),
					'args'                => array(
						'products' => array(
							'required' => true,
							'type'     => 'object',
						),
					),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'reset_install' ),
					'permission_callback' => array( $this, 'check_permission' ),
				),
			)
		);
	}

	/**
	 * Check permissions.
	 *
	 * @since 3.7.0
	 * @param WP_REST_Request $request Full details about the request.
	 * @return bool|WP_Error
	 */
	public function check_permission( $request ) {
		$current_user = wp_get_current_user();

		if ( empty( $current_user ) || ( $current_user instanceof WP_User && ! $current_user->exists() ) ) {
			return apply_filters(
				WC_WCCOM_Site::AUTH_ERROR_FILTER_NAME,
				new WP_Error(
					WC_REST_WCCOM_Site_Installer_Errors::NOT_AUTHENTICATED_CODE,
					WC_REST_WCCOM_Site_Installer_Errors::NOT_AUTHENTICATED_MESSAGE,
					array( 'status' => WC_REST_WCCOM_Site_Installer_Errors::NOT_AUTHENTICATED_HTTP_CODE )
				)
			);
		}

		if ( ! user_can( $current_user, 'install_plugins' ) || ! user_can( $current_user, 'install_themes' ) ) {
			return new WP_Error(
				WC_REST_WCCOM_Site_Installer_Errors::NO_PERMISSION_CODE,
				WC_REST_WCCOM_Site_Installer_Errors::NO_PERMISSION_MESSAGE,
				array( 'status' => WC_REST_WCCOM_Site_Installer_Errors::NO_PERMISSION_HTTP_CODE )
			);
		}

		return true;
	}

	/**
	 * Get installation state.
	 *
	 * @since 3.7.0
	 * @param WP_REST_Request $request Full details about the request.
	 * @return bool|WP_Error
	 */
	public function get_install_state( $request ) {
		$requirements_met = WC_WCCOM_Site_Installer_Requirements_Check::met_requirements();
		if ( is_wp_error( $requirements_met ) ) {
			return $requirements_met;
		}

		return rest_ensure_response( WC_WCCOM_Site_Installer::get_state() );
	}

	/**
	 * Install WooCommerce.com products.
	 *
	 * @since 3.7.0
	 * @param WP_REST_Request $request Full details about the request.
	 * @return bool|WP_Error
	 */
	public function install( $request ) {
		$requirements_met = WC_WCCOM_Site_Installer_Requirements_Check::met_requirements();
		if ( is_wp_error( $requirements_met ) ) {
			return $requirements_met;
		}

		if ( empty( $request['products'] ) ) {
			return new WP_Error( 'missing_products', __( 'Missing products in request body.', 'woocommerce' ), array( 'status' => 400 ) );
		}

		$validation_result = $this->validate_products( $request['products'] );
		if ( is_wp_error( $validation_result ) ) {
			return $validation_result;
		}

		return rest_ensure_response( WC_WCCOM_Site_Installer::schedule_install( $request['products'] ) );
	}

	/**
	 * Reset installation state.
	 *
	 * @since 3.7.0
	 * @param WP_REST_Request $request Full details about the request.
	 * @return bool|WP_Error
	 */
	public function reset_install( $request ) {
		$resp = rest_ensure_response( WC_WCCOM_Site_Installer::reset_state() );
		$resp->set_status( 204 );

		return $resp;
	}

	/**
	 * Validate products from request body.
	 *
	 * @since 3.7.0
	 * @param array $products Array of products where key is product ID and
	 *                        element is install args.
	 * @return bool|WP_Error
	 */
	protected function validate_products( $products ) {
		$err = new WP_Error( 'invalid_products', __( 'Invalid products in request body.', 'woocommerce' ), array( 'status' => 400 ) );

		if ( ! is_array( $products ) ) {
			return $err;
		}

		foreach ( $products as $product_id => $install_args ) {
			if ( ! absint( $product_id ) ) {
				return $err;
			}

			if ( empty( $install_args ) || ! is_array( $install_args ) ) {
				return $err;
			}
		}

		return true;
	}
}
class-wc-query.php000064400000077151151336044770010161 0ustar00<?php
/**
 * Contains the query functions for WooCommerce which alter the front-end post queries and loops
 *
 * @version 3.2.0
 * @package WooCommerce\Classes
 */

use Automattic\WooCommerce\Internal\ProductAttributesLookup\Filterer;

defined( 'ABSPATH' ) || exit;

/**
 * WC_Query Class.
 */
class WC_Query {

	/**
	 * Query vars to add to wp.
	 *
	 * @var array
	 */
	public $query_vars = array();

	/**
	 * Reference to the main product query on the page.
	 *
	 * @var WP_Query
	 */
	private static $product_query;

	/**
	 * Stores chosen attributes.
	 *
	 * @var array
	 */
	private static $chosen_attributes;

	/**
	 * The instance of the class that helps filtering with the product attributes lookup table.
	 *
	 * @var Filterer
	 */
	private $filterer;

	/**
	 * Constructor for the query class. Hooks in methods.
	 */
	public function __construct() {
		$this->filterer = wc_get_container()->get( Filterer::class );

		add_action( 'init', array( $this, 'add_endpoints' ) );
		if ( ! is_admin() ) {
			add_action( 'wp_loaded', array( $this, 'get_errors' ), 20 );
			add_filter( 'query_vars', array( $this, 'add_query_vars' ), 0 );
			add_action( 'parse_request', array( $this, 'parse_request' ), 0 );
			add_action( 'pre_get_posts', array( $this, 'pre_get_posts' ) );
			add_filter( 'get_pagenum_link', array( $this, 'remove_add_to_cart_pagination' ), 10, 1 );
		}
		$this->init_query_vars();
	}

	/**
	 * Reset the chosen attributes so that get_layered_nav_chosen_attributes will get them from the query again.
	 */
	public static function reset_chosen_attributes() {
		self::$chosen_attributes = null;
	}

	/**
	 * Get any errors from querystring.
	 */
	public function get_errors() {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$error = ! empty( $_GET['wc_error'] ) ? sanitize_text_field( wp_unslash( $_GET['wc_error'] ) ) : '';

		if ( $error && ! wc_has_notice( $error, 'error' ) ) {
			wc_add_notice( $error, 'error' );
		}
	}

	/**
	 * Init query vars by loading options.
	 */
	public function init_query_vars() {
		// Query vars to add to WP.
		$this->query_vars = array(
			// Checkout actions.
			'order-pay'                  => get_option( 'woocommerce_checkout_pay_endpoint', 'order-pay' ),
			'order-received'             => get_option( 'woocommerce_checkout_order_received_endpoint', 'order-received' ),
			// My account actions.
			'orders'                     => get_option( 'woocommerce_myaccount_orders_endpoint', 'orders' ),
			'view-order'                 => get_option( 'woocommerce_myaccount_view_order_endpoint', 'view-order' ),
			'downloads'                  => get_option( 'woocommerce_myaccount_downloads_endpoint', 'downloads' ),
			'edit-account'               => get_option( 'woocommerce_myaccount_edit_account_endpoint', 'edit-account' ),
			'edit-address'               => get_option( 'woocommerce_myaccount_edit_address_endpoint', 'edit-address' ),
			'payment-methods'            => get_option( 'woocommerce_myaccount_payment_methods_endpoint', 'payment-methods' ),
			'lost-password'              => get_option( 'woocommerce_myaccount_lost_password_endpoint', 'lost-password' ),
			'customer-logout'            => get_option( 'woocommerce_logout_endpoint', 'customer-logout' ),
			'add-payment-method'         => get_option( 'woocommerce_myaccount_add_payment_method_endpoint', 'add-payment-method' ),
			'delete-payment-method'      => get_option( 'woocommerce_myaccount_delete_payment_method_endpoint', 'delete-payment-method' ),
			'set-default-payment-method' => get_option( 'woocommerce_myaccount_set_default_payment_method_endpoint', 'set-default-payment-method' ),
		);
	}

	/**
	 * Get page title for an endpoint.
	 *
	 * @param string $endpoint Endpoint key.
	 * @param string $action Optional action or variation within the endpoint.
	 *
	 * @since 2.3.0
	 * @since 4.6.0 Added $action parameter.
	 * @return string The page title.
	 */
	public function get_endpoint_title( $endpoint, $action = '' ) {
		global $wp;

		switch ( $endpoint ) {
			case 'order-pay':
				$title = __( 'Pay for order', 'woocommerce' );
				break;
			case 'order-received':
				$title = __( 'Order received', 'woocommerce' );
				break;
			case 'orders':
				if ( ! empty( $wp->query_vars['orders'] ) ) {
					/* translators: %s: page */
					$title = sprintf( __( 'Orders (page %d)', 'woocommerce' ), intval( $wp->query_vars['orders'] ) );
				} else {
					$title = __( 'Orders', 'woocommerce' );
				}
				break;
			case 'view-order':
				$order = wc_get_order( $wp->query_vars['view-order'] );
				/* translators: %s: order number */
				$title = ( $order ) ? sprintf( __( 'Order #%s', 'woocommerce' ), $order->get_order_number() ) : '';
				break;
			case 'downloads':
				$title = __( 'Downloads', 'woocommerce' );
				break;
			case 'edit-account':
				$title = __( 'Account details', 'woocommerce' );
				break;
			case 'edit-address':
				$title = __( 'Addresses', 'woocommerce' );
				break;
			case 'payment-methods':
				$title = __( 'Payment methods', 'woocommerce' );
				break;
			case 'add-payment-method':
				$title = __( 'Add payment method', 'woocommerce' );
				break;
			case 'lost-password':
				if ( in_array( $action, array( 'rp', 'resetpass', 'newaccount' ), true ) ) {
					$title = __( 'Set password', 'woocommerce' );
				} else {
					$title = __( 'Lost password', 'woocommerce' );
				}
				break;
			default:
				$title = '';
				break;
		}

		/**
		 * Filters the page title used for my-account endpoints.
		 *
		 * @since 2.6.0
		 * @since 4.6.0 Added $action parameter.
		 *
		 * @see get_endpoint_title()
		 *
		 * @param string $title Default title.
		 * @param string $endpoint Endpoint key.
		 * @param string $action Optional action or variation within the endpoint.
		 */
		return apply_filters( 'woocommerce_endpoint_' . $endpoint . '_title', $title, $endpoint, $action );
	}

	/**
	 * Endpoint mask describing the places the endpoint should be added.
	 *
	 * @since 2.6.2
	 * @return int
	 */
	public function get_endpoints_mask() {
		if ( 'page' === get_option( 'show_on_front' ) ) {
			$page_on_front     = get_option( 'page_on_front' );
			$myaccount_page_id = get_option( 'woocommerce_myaccount_page_id' );
			$checkout_page_id  = get_option( 'woocommerce_checkout_page_id' );

			if ( in_array( $page_on_front, array( $myaccount_page_id, $checkout_page_id ), true ) ) {
				return EP_ROOT | EP_PAGES;
			}
		}

		return EP_PAGES;
	}

	/**
	 * Add endpoints for query vars.
	 */
	public function add_endpoints() {
		$mask = $this->get_endpoints_mask();

		foreach ( $this->get_query_vars() as $key => $var ) {
			if ( ! empty( $var ) ) {
				add_rewrite_endpoint( $var, $mask );
			}
		}
	}

	/**
	 * Add query vars.
	 *
	 * @param array $vars Query vars.
	 * @return array
	 */
	public function add_query_vars( $vars ) {
		foreach ( $this->get_query_vars() as $key => $var ) {
			$vars[] = $key;
		}
		return $vars;
	}

	/**
	 * Get query vars.
	 *
	 * @return array
	 */
	public function get_query_vars() {
		return apply_filters( 'woocommerce_get_query_vars', $this->query_vars );
	}

	/**
	 * Get query current active query var.
	 *
	 * @return string
	 */
	public function get_current_endpoint() {
		global $wp;

		foreach ( $this->get_query_vars() as $key => $value ) {
			if ( isset( $wp->query_vars[ $key ] ) ) {
				return $key;
			}
		}
		return '';
	}

	/**
	 * Parse the request and look for query vars - endpoints may not be supported.
	 */
	public function parse_request() {
		global $wp;

		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		// Map query vars to their keys, or get them if endpoints are not supported.
		foreach ( $this->get_query_vars() as $key => $var ) {
			if ( isset( $_GET[ $var ] ) ) {
				$wp->query_vars[ $key ] = sanitize_text_field( wp_unslash( $_GET[ $var ] ) );
			} elseif ( isset( $wp->query_vars[ $var ] ) ) {
				$wp->query_vars[ $key ] = $wp->query_vars[ $var ];
			}
		}
		// phpcs:enable WordPress.Security.NonceVerification.Recommended
	}

	/**
	 * Are we currently on the front page?
	 *
	 * @param WP_Query $q Query instance.
	 * @return bool
	 */
	private function is_showing_page_on_front( $q ) {
		return ( $q->is_home() && ! $q->is_posts_page ) && 'page' === get_option( 'show_on_front' );
	}

	/**
	 * Is the front page a page we define?
	 *
	 * @param int $page_id Page ID.
	 * @return bool
	 */
	private function page_on_front_is( $page_id ) {
		return absint( get_option( 'page_on_front' ) ) === absint( $page_id );
	}

	/**
	 * Hook into pre_get_posts to do the main product query.
	 *
	 * @param WP_Query $q Query instance.
	 */
	public function pre_get_posts( $q ) {
		// We only want to affect the main query.
		if ( ! $q->is_main_query() ) {
			return;
		}

		// Fixes for queries on static homepages.
		if ( $this->is_showing_page_on_front( $q ) ) {

			// Fix for endpoints on the homepage.
			if ( ! $this->page_on_front_is( $q->get( 'page_id' ) ) ) {
				$_query = wp_parse_args( $q->query );
				if ( ! empty( $_query ) && array_intersect( array_keys( $_query ), array_keys( $this->get_query_vars() ) ) ) {
					$q->is_page     = true;
					$q->is_home     = false;
					$q->is_singular = true;
					$q->set( 'page_id', (int) get_option( 'page_on_front' ) );
					add_filter( 'redirect_canonical', '__return_false' );
				}
			}

			// When orderby is set, WordPress shows posts on the front-page. Get around that here.
			if ( $this->page_on_front_is( wc_get_page_id( 'shop' ) ) ) {
				$_query = wp_parse_args( $q->query );
				if ( empty( $_query ) || ! array_diff( array_keys( $_query ), array( 'preview', 'page', 'paged', 'cpage', 'orderby' ) ) ) {
					$q->set( 'page_id', (int) get_option( 'page_on_front' ) );
					$q->is_page = true;
					$q->is_home = false;

					// WP supporting themes show post type archive.
					if ( current_theme_supports( 'woocommerce' ) ) {
						$q->set( 'post_type', 'product' );
					} else {
						$q->is_singular = true;
					}
				}
			} elseif ( ! empty( $_GET['orderby'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
				$q->set( 'page_id', (int) get_option( 'page_on_front' ) );
				$q->is_page     = true;
				$q->is_home     = false;
				$q->is_singular = true;
			}
		}

		// Fix product feeds.
		if ( $q->is_feed() && $q->is_post_type_archive( 'product' ) ) {
			$q->is_comment_feed = false;
		}

		// Special check for shops with the PRODUCT POST TYPE ARCHIVE on front.
		if ( current_theme_supports( 'woocommerce' ) && $q->is_page() && 'page' === get_option( 'show_on_front' ) && absint( $q->get( 'page_id' ) ) === wc_get_page_id( 'shop' ) ) {
			// This is a front-page shop.
			$q->set( 'post_type', 'product' );
			$q->set( 'page_id', '' );

			if ( isset( $q->query['paged'] ) ) {
				$q->set( 'paged', $q->query['paged'] );
			}

			// Define a variable so we know this is the front page shop later on.
			wc_maybe_define_constant( 'SHOP_IS_ON_FRONT', true );

			// Get the actual WP page to avoid errors and let us use is_front_page().
			// This is hacky but works. Awaiting https://core.trac.wordpress.org/ticket/21096.
			global $wp_post_types;

			$shop_page = get_post( wc_get_page_id( 'shop' ) );

			$wp_post_types['product']->ID         = $shop_page->ID;
			$wp_post_types['product']->post_title = $shop_page->post_title;
			$wp_post_types['product']->post_name  = $shop_page->post_name;
			$wp_post_types['product']->post_type  = $shop_page->post_type;
			$wp_post_types['product']->ancestors  = get_ancestors( $shop_page->ID, $shop_page->post_type );

			// Fix conditional Functions like is_front_page.
			$q->is_singular          = false;
			$q->is_post_type_archive = true;
			$q->is_archive           = true;
			$q->is_page              = true;

			// Remove post type archive name from front page title tag.
			add_filter( 'post_type_archive_title', '__return_empty_string', 5 );

			// Fix WP SEO.
			if ( class_exists( 'WPSEO_Meta' ) ) {
				add_filter( 'wpseo_metadesc', array( $this, 'wpseo_metadesc' ) );
				add_filter( 'wpseo_metakey', array( $this, 'wpseo_metakey' ) );
			}
		} elseif ( ! $q->is_post_type_archive( 'product' ) && ! $q->is_tax( get_object_taxonomies( 'product' ) ) ) {
			// Only apply to product categories, the product post archive, the shop page, product tags, and product attribute taxonomies.
			return;
		}

		$this->product_query( $q );
	}

	/**
	 * Handler for the 'the_posts' WP filter.
	 *
	 * @param array    $posts Posts from WP Query.
	 * @param WP_Query $query Current query.
	 *
	 * @return array
	 */
	public function handle_get_posts( $posts, $query ) {
		if ( 'product_query' !== $query->get( 'wc_query' ) ) {
			return $posts;
		}
		$this->remove_product_query_filters( $posts );
		return $posts;
	}


	/**
	 * Pre_get_posts above may adjust the main query to add WooCommerce logic. When this query is done, we need to ensure
	 * all custom filters are removed.
	 *
	 * This is done here during the_posts filter. The input is not changed.
	 *
	 * @param array $posts Posts from WP Query.
	 * @return array
	 */
	public function remove_product_query_filters( $posts ) {
		$this->remove_ordering_args();
		remove_filter( 'posts_clauses', array( $this, 'price_filter_post_clauses' ), 10, 2 );
		return $posts;
	}

	/**
	 * This function used to be hooked to found_posts and adjust the posts count when the filtering by attribute
	 * widget was used and variable products were present. Now it isn't hooked anymore and does nothing but return
	 * the input unchanged, since the pull request in which it was introduced has been reverted.
	 *
	 * @since 4.4.0
	 * @param int      $count Original posts count, as supplied by the found_posts filter.
	 * @param WP_Query $query The current WP_Query object.
	 *
	 * @return int Adjusted posts count.
	 */
	public function adjust_posts_count( $count, $query ) {
		return $count;
	}

	/**
	 * Instance version of get_layered_nav_chosen_attributes, needed for unit tests.
	 *
	 * @return array
	 */
	protected function get_layered_nav_chosen_attributes_inst() {
		return self::get_layered_nav_chosen_attributes();
	}

	/**
	 * Get the posts (or the ids of the posts) found in the current WP loop.
	 *
	 * @return array Array of posts or post ids.
	 */
	protected function get_current_posts() {
		return $GLOBALS['wp_query']->posts;
	}

	/**
	 * WP SEO meta description.
	 *
	 * Hooked into wpseo_ hook already, so no need for function_exist.
	 *
	 * @return string
	 */
	public function wpseo_metadesc() {
		return WPSEO_Meta::get_value( 'metadesc', wc_get_page_id( 'shop' ) );
	}

	/**
	 * WP SEO meta key.
	 *
	 * Hooked into wpseo_ hook already, so no need for function_exist.
	 *
	 * @return string
	 */
	public function wpseo_metakey() {
		return WPSEO_Meta::get_value( 'metakey', wc_get_page_id( 'shop' ) );
	}

	/**
	 * Query the products, applying sorting/ordering etc.
	 * This applies to the main WordPress loop.
	 *
	 * @param WP_Query $q Query instance.
	 */
	public function product_query( $q ) {
		if ( ! is_feed() ) {
			$ordering = $this->get_catalog_ordering_args();
			$q->set( 'orderby', $ordering['orderby'] );
			$q->set( 'order', $ordering['order'] );

			if ( isset( $ordering['meta_key'] ) ) {
				$q->set( 'meta_key', $ordering['meta_key'] );
			}
		}

		// Query vars that affect posts shown.
		$q->set( 'meta_query', $this->get_meta_query( $q->get( 'meta_query' ), true ) );
		$q->set( 'tax_query', $this->get_tax_query( $q->get( 'tax_query' ), true ) );
		$q->set( 'wc_query', 'product_query' );
		$q->set( 'post__in', array_unique( (array) apply_filters( 'loop_shop_post_in', array() ) ) );

		// Work out how many products to query.
		$q->set( 'posts_per_page', $q->get( 'posts_per_page' ) ? $q->get( 'posts_per_page' ) : apply_filters( 'loop_shop_per_page', wc_get_default_products_per_row() * wc_get_default_product_rows_per_page() ) );

		// Store reference to this query.
		self::$product_query = $q;

		// Additonal hooks to change WP Query.
		add_filter(
			'posts_clauses',
			function( $args, $wp_query ) {
				return $this->product_query_post_clauses( $args, $wp_query );
			},
			10,
			2
		);
		add_filter( 'the_posts', array( $this, 'handle_get_posts' ), 10, 2 );

		do_action( 'woocommerce_product_query', $q, $this );
	}

	/**
	 * Add extra clauses to the product query.
	 *
	 * @param array    $args Product query clauses.
	 * @param WP_Query $wp_query The current product query.
	 * @return array The updated product query clauses array.
	 */
	private function product_query_post_clauses( $args, $wp_query ) {
		$args = $this->price_filter_post_clauses( $args, $wp_query );
		$args = $this->filterer->filter_by_attribute_post_clauses( $args, $wp_query, $this->get_layered_nav_chosen_attributes() );

		return $args;
	}

	/**
	 * Remove the query.
	 */
	public function remove_product_query() {
		remove_action( 'pre_get_posts', array( $this, 'pre_get_posts' ) );
	}

	/**
	 * Remove ordering queries.
	 */
	public function remove_ordering_args() {
		remove_filter( 'posts_clauses', array( $this, 'order_by_price_asc_post_clauses' ) );
		remove_filter( 'posts_clauses', array( $this, 'order_by_price_desc_post_clauses' ) );
		remove_filter( 'posts_clauses', array( $this, 'order_by_popularity_post_clauses' ) );
		remove_filter( 'posts_clauses', array( $this, 'order_by_rating_post_clauses' ) );
	}

	/**
	 * Returns an array of arguments for ordering products based on the selected values.
	 *
	 * @param string $orderby Order by param.
	 * @param string $order Order param.
	 * @return array
	 */
	public function get_catalog_ordering_args( $orderby = '', $order = '' ) {
		// Get ordering from query string unless defined.
		if ( ! $orderby ) {
			// phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$orderby_value = isset( $_GET['orderby'] ) ? wc_clean( (string) wp_unslash( $_GET['orderby'] ) ) : wc_clean( get_query_var( 'orderby' ) );

			if ( ! $orderby_value ) {
				if ( is_search() ) {
					$orderby_value = 'relevance';
				} else {
					$orderby_value = apply_filters( 'woocommerce_default_catalog_orderby', get_option( 'woocommerce_default_catalog_orderby', 'menu_order' ) );
				}
			}

			// Get order + orderby args from string.
			$orderby_value = is_array( $orderby_value ) ? $orderby_value : explode( '-', $orderby_value );
			$orderby       = esc_attr( $orderby_value[0] );
			$order         = ! empty( $orderby_value[1] ) ? $orderby_value[1] : $order;
		}

		// Convert to correct format.
		$orderby = strtolower( is_array( $orderby ) ? (string) current( $orderby ) : (string) $orderby );
		$order   = strtoupper( is_array( $order ) ? (string) current( $order ) : (string) $order );
		$args    = array(
			'orderby'  => $orderby,
			'order'    => ( 'DESC' === $order ) ? 'DESC' : 'ASC',
			'meta_key' => '', // @codingStandardsIgnoreLine
		);

		switch ( $orderby ) {
			case 'id':
				$args['orderby'] = 'ID';
				break;
			case 'menu_order':
				$args['orderby'] = 'menu_order title';
				break;
			case 'title':
				$args['orderby'] = 'title';
				$args['order']   = ( 'DESC' === $order ) ? 'DESC' : 'ASC';
				break;
			case 'relevance':
				$args['orderby'] = 'relevance';
				$args['order']   = 'DESC';
				break;
			case 'rand':
				$args['orderby'] = 'rand'; // @codingStandardsIgnoreLine
				break;
			case 'date':
				$args['orderby'] = 'date ID';
				$args['order']   = ( 'ASC' === $order ) ? 'ASC' : 'DESC';
				break;
			case 'price':
				$callback = 'DESC' === $order ? 'order_by_price_desc_post_clauses' : 'order_by_price_asc_post_clauses';
				add_filter( 'posts_clauses', array( $this, $callback ) );
				break;
			case 'popularity':
				add_filter( 'posts_clauses', array( $this, 'order_by_popularity_post_clauses' ) );
				break;
			case 'rating':
				add_filter( 'posts_clauses', array( $this, 'order_by_rating_post_clauses' ) );
				break;
		}

		return apply_filters( 'woocommerce_get_catalog_ordering_args', $args, $orderby, $order );
	}

	/**
	 * Custom query used to filter products by price.
	 *
	 * @since 3.6.0
	 *
	 * @param array    $args Query args.
	 * @param WP_Query $wp_query WP_Query object.
	 *
	 * @return array
	 */
	public function price_filter_post_clauses( $args, $wp_query ) {
		global $wpdb;

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( ! $wp_query->is_main_query() || ( ! isset( $_GET['max_price'] ) && ! isset( $_GET['min_price'] ) ) ) {
			return $args;
		}

		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		$current_min_price = isset( $_GET['min_price'] ) ? floatval( wp_unslash( $_GET['min_price'] ) ) : 0;
		$current_max_price = isset( $_GET['max_price'] ) ? floatval( wp_unslash( $_GET['max_price'] ) ) : PHP_INT_MAX;
		// phpcs:enable WordPress.Security.NonceVerification.Recommended

		/**
		 * Adjust if the store taxes are not displayed how they are stored.
		 * Kicks in when prices excluding tax are displayed including tax.
		 */
		if ( wc_tax_enabled() && 'incl' === get_option( 'woocommerce_tax_display_shop' ) && ! wc_prices_include_tax() ) {
			$tax_class = apply_filters( 'woocommerce_price_filter_widget_tax_class', '' ); // Uses standard tax class.
			$tax_rates = WC_Tax::get_rates( $tax_class );

			if ( $tax_rates ) {
				$current_min_price -= WC_Tax::get_tax_total( WC_Tax::calc_inclusive_tax( $current_min_price, $tax_rates ) );
				$current_max_price -= WC_Tax::get_tax_total( WC_Tax::calc_inclusive_tax( $current_max_price, $tax_rates ) );
			}
		}

		$args['join']   = $this->append_product_sorting_table_join( $args['join'] );
		$args['where'] .= $wpdb->prepare(
			' AND NOT (%f<wc_product_meta_lookup.min_price OR %f>wc_product_meta_lookup.max_price ) ',
			$current_max_price,
			$current_min_price
		);
		return $args;
	}

	/**
	 * Handle numeric price sorting.
	 *
	 * @param array $args Query args.
	 * @return array
	 */
	public function order_by_price_asc_post_clauses( $args ) {
		$args['join']    = $this->append_product_sorting_table_join( $args['join'] );
		$args['orderby'] = ' wc_product_meta_lookup.min_price ASC, wc_product_meta_lookup.product_id ASC ';
		return $args;
	}

	/**
	 * Handle numeric price sorting.
	 *
	 * @param array $args Query args.
	 * @return array
	 */
	public function order_by_price_desc_post_clauses( $args ) {
		$args['join']    = $this->append_product_sorting_table_join( $args['join'] );
		$args['orderby'] = ' wc_product_meta_lookup.max_price DESC, wc_product_meta_lookup.product_id DESC ';
		return $args;
	}

	/**
	 * WP Core does not let us change the sort direction for individual orderby params - https://core.trac.wordpress.org/ticket/17065.
	 *
	 * This lets us sort by meta value desc, and have a second orderby param.
	 *
	 * @param array $args Query args.
	 * @return array
	 */
	public function order_by_popularity_post_clauses( $args ) {
		$args['join']    = $this->append_product_sorting_table_join( $args['join'] );
		$args['orderby'] = ' wc_product_meta_lookup.total_sales DESC, wc_product_meta_lookup.product_id DESC ';
		return $args;
	}

	/**
	 * Order by rating post clauses.
	 *
	 * @param array $args Query args.
	 * @return array
	 */
	public function order_by_rating_post_clauses( $args ) {
		$args['join']    = $this->append_product_sorting_table_join( $args['join'] );
		$args['orderby'] = ' wc_product_meta_lookup.average_rating DESC, wc_product_meta_lookup.rating_count DESC, wc_product_meta_lookup.product_id DESC ';
		return $args;
	}

	/**
	 * Join wc_product_meta_lookup to posts if not already joined.
	 *
	 * @param string $sql SQL join.
	 * @return string
	 */
	private function append_product_sorting_table_join( $sql ) {
		global $wpdb;

		if ( ! strstr( $sql, 'wc_product_meta_lookup' ) ) {
			$sql .= " LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON $wpdb->posts.ID = wc_product_meta_lookup.product_id ";
		}
		return $sql;
	}

	/**
	 * Appends meta queries to an array.
	 *
	 * @param  array $meta_query Meta query.
	 * @param  bool  $main_query If is main query.
	 * @return array
	 */
	public function get_meta_query( $meta_query = array(), $main_query = false ) {
		if ( ! is_array( $meta_query ) ) {
			$meta_query = array();
		}
		return array_filter( apply_filters( 'woocommerce_product_query_meta_query', $meta_query, $this ) );
	}

	/**
	 * Appends tax queries to an array.
	 *
	 * @param  array $tax_query  Tax query.
	 * @param  bool  $main_query If is main query.
	 * @return array
	 */
	public function get_tax_query( $tax_query = array(), $main_query = false ) {
		if ( ! is_array( $tax_query ) ) {
			$tax_query = array(
				'relation' => 'AND',
			);
		}

		if ( $main_query && ! $this->filterer->filtering_via_lookup_table_is_active() ) {
			// Layered nav filters on terms.
			foreach ( $this->get_layered_nav_chosen_attributes() as $taxonomy => $data ) {
				$tax_query[] = array(
					'taxonomy'         => $taxonomy,
					'field'            => 'slug',
					'terms'            => $data['terms'],
					'operator'         => 'and' === $data['query_type'] ? 'AND' : 'IN',
					'include_children' => false,
				);
			}
		}

		$product_visibility_terms  = wc_get_product_visibility_term_ids();
		$product_visibility_not_in = array( is_search() && $main_query ? $product_visibility_terms['exclude-from-search'] : $product_visibility_terms['exclude-from-catalog'] );

		// Hide out of stock products.
		if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) {
			$product_visibility_not_in[] = $product_visibility_terms['outofstock'];
		}

		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		// Filter by rating.
		if ( isset( $_GET['rating_filter'] ) ) {
			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			$rating_filter = array_filter( array_map( 'absint', explode( ',', wp_unslash( $_GET['rating_filter'] ) ) ) );
			$rating_terms  = array();
			for ( $i = 1; $i <= 5; $i ++ ) {
				if ( in_array( $i, $rating_filter, true ) && isset( $product_visibility_terms[ 'rated-' . $i ] ) ) {
					$rating_terms[] = $product_visibility_terms[ 'rated-' . $i ];
				}
			}
			if ( ! empty( $rating_terms ) ) {
				$tax_query[] = array(
					'taxonomy'      => 'product_visibility',
					'field'         => 'term_taxonomy_id',
					'terms'         => $rating_terms,
					'operator'      => 'IN',
					'rating_filter' => true,
				);
			}
		}
		// phpcs:enable WordPress.Security.NonceVerification.Recommended

		if ( ! empty( $product_visibility_not_in ) ) {
			$tax_query[] = array(
				'taxonomy' => 'product_visibility',
				'field'    => 'term_taxonomy_id',
				'terms'    => $product_visibility_not_in,
				'operator' => 'NOT IN',
			);
		}

		return array_filter( apply_filters( 'woocommerce_product_query_tax_query', $tax_query, $this ) );
	}

	/**
	 * Get the main query which product queries ran against.
	 *
	 * @return WP_Query
	 */
	public static function get_main_query() {
		return self::$product_query;
	}

	/**
	 * Get the tax query which was used by the main query.
	 *
	 * @return array
	 */
	public static function get_main_tax_query() {
		$tax_query = isset( self::$product_query->tax_query, self::$product_query->tax_query->queries ) ? self::$product_query->tax_query->queries : array();

		return $tax_query;
	}

	/**
	 * Get the meta query which was used by the main query.
	 *
	 * @return array
	 */
	public static function get_main_meta_query() {
		$args       = self::$product_query->query_vars;
		$meta_query = isset( $args['meta_query'] ) ? $args['meta_query'] : array();

		return $meta_query;
	}

	/**
	 * Based on WP_Query::parse_search
	 */
	public static function get_main_search_query_sql() {
		global $wpdb;

		$args         = self::$product_query->query_vars;
		$search_terms = isset( $args['search_terms'] ) ? $args['search_terms'] : array();
		$sql          = array();

		foreach ( $search_terms as $term ) {
			// Terms prefixed with '-' should be excluded.
			$include = '-' !== substr( $term, 0, 1 );

			if ( $include ) {
				$like_op  = 'LIKE';
				$andor_op = 'OR';
			} else {
				$like_op  = 'NOT LIKE';
				$andor_op = 'AND';
				$term     = substr( $term, 1 );
			}

			$like = '%' . $wpdb->esc_like( $term ) . '%';
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$sql[] = $wpdb->prepare( "(($wpdb->posts.post_title $like_op %s) $andor_op ($wpdb->posts.post_excerpt $like_op %s) $andor_op ($wpdb->posts.post_content $like_op %s))", $like, $like, $like );
		}

		if ( ! empty( $sql ) && ! is_user_logged_in() ) {
			$sql[] = "($wpdb->posts.post_password = '')";
		}

		return implode( ' AND ', $sql );
	}

	/**
	 * Get an array of attributes and terms selected with the layered nav widget.
	 *
	 * @return array
	 */
	public static function get_layered_nav_chosen_attributes() {
		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		if ( ! is_array( self::$chosen_attributes ) ) {
			self::$chosen_attributes = array();

			if ( ! empty( $_GET ) ) {
				foreach ( $_GET as $key => $value ) {
					if ( 0 === strpos( $key, 'filter_' ) ) {
						$attribute    = wc_sanitize_taxonomy_name( str_replace( 'filter_', '', $key ) );
						$taxonomy     = wc_attribute_taxonomy_name( $attribute );
						$filter_terms = ! empty( $value ) ? explode( ',', wc_clean( wp_unslash( $value ) ) ) : array();

						if ( empty( $filter_terms ) || ! taxonomy_exists( $taxonomy ) || ! wc_attribute_taxonomy_id_by_name( $attribute ) ) {
							continue;
						}

						$query_type                                    = ! empty( $_GET[ 'query_type_' . $attribute ] ) && in_array( $_GET[ 'query_type_' . $attribute ], array( 'and', 'or' ), true ) ? wc_clean( wp_unslash( $_GET[ 'query_type_' . $attribute ] ) ) : '';
						self::$chosen_attributes[ $taxonomy ]['terms'] = array_map( 'sanitize_title', $filter_terms ); // Ensures correct encoding.
						self::$chosen_attributes[ $taxonomy ]['query_type'] = $query_type ? $query_type : apply_filters( 'woocommerce_layered_nav_default_query_type', 'and' );
					}
				}
			}
		}
		return self::$chosen_attributes;
		// phpcs:disable WordPress.Security.NonceVerification.Recommended
	}

	/**
	 * Remove the add-to-cart param from pagination urls.
	 *
	 * @param string $url URL.
	 * @return string
	 */
	public function remove_add_to_cart_pagination( $url ) {
		return remove_query_arg( 'add-to-cart', $url );
	}

	/**
	 * Return a meta query for filtering by rating.
	 *
	 * @deprecated 3.0.0 Replaced with taxonomy.
	 * @return array
	 */
	public function rating_filter_meta_query() {
		return array();
	}

	/**
	 * Returns a meta query to handle product visibility.
	 *
	 * @deprecated 3.0.0 Replaced with taxonomy.
	 * @param string $compare (default: 'IN').
	 * @return array
	 */
	public function visibility_meta_query( $compare = 'IN' ) {
		return array();
	}

	/**
	 * Returns a meta query to handle product stock status.
	 *
	 * @deprecated 3.0.0 Replaced with taxonomy.
	 * @param string $status (default: 'instock').
	 * @return array
	 */
	public function stock_status_meta_query( $status = 'instock' ) {
		return array();
	}

	/**
	 * Layered nav init.
	 *
	 * @deprecated 2.6.0
	 */
	public function layered_nav_init() {
		wc_deprecated_function( 'layered_nav_init', '2.6' );
	}

	/**
	 * Get an unpaginated list all product IDs (both filtered and unfiltered). Makes use of transients.
	 *
	 * @deprecated 2.6.0 due to performance concerns
	 */
	public function get_products_in_view() {
		wc_deprecated_function( 'get_products_in_view', '2.6' );
	}

	/**
	 * Layered Nav post filter.
	 *
	 * @deprecated 2.6.0 due to performance concerns
	 *
	 * @param mixed $deprecated Deprecated.
	 */
	public function layered_nav_query( $deprecated ) {
		wc_deprecated_function( 'layered_nav_query', '2.6' );
	}

	/**
	 * Search post excerpt.
	 *
	 * @param string $where Where clause.
	 *
	 * @deprecated 3.2.0 - Not needed anymore since WordPress 4.5.
	 */
	public function search_post_excerpt( $where = '' ) {
		wc_deprecated_function( 'WC_Query::search_post_excerpt', '3.2.0', 'Excerpt added to search query by default since WordPress 4.5.' );
		return $where;
	}

	/**
	 * Remove the posts_where filter.
	 *
	 * @deprecated 3.2.0 - Nothing to remove anymore because search_post_excerpt() is deprecated.
	 */
	public function remove_posts_where() {
		wc_deprecated_function( 'WC_Query::remove_posts_where', '3.2.0', 'Nothing to remove anymore because search_post_excerpt() is deprecated.' );
	}
}
class-wc-datetime.php000064400000004313151336044770010576 0ustar00<?php
/**
 * WC Wrapper for PHP DateTime which adds support for gmt/utc offset when a
 * timezone is absent
 *
 * @since   3.0.0
 * @package WooCommerce\Classes
 */

defined( 'ABSPATH' ) || exit;

/**
 * Datetime class.
 */
class WC_DateTime extends DateTime {

	/**
	 * UTC Offset, if needed. Only used when a timezone is not set. When
	 * timezones are used this will equal 0.
	 *
	 * @var integer
	 */
	protected $utc_offset = 0;

	/**
	 * Output an ISO 8601 date string in local (WordPress) timezone.
	 *
	 * @since  3.0.0
	 * @return string
	 */
	public function __toString() {
		return $this->format( DATE_ATOM );
	}

	/**
	 * Set UTC offset - this is a fixed offset instead of a timezone.
	 *
	 * @param int $offset Offset.
	 */
	public function set_utc_offset( $offset ) {
		$this->utc_offset = intval( $offset );
	}

	/**
	 * Get UTC offset if set, or default to the DateTime object's offset.
	 */
	public function getOffset() {
		return $this->utc_offset ? $this->utc_offset : parent::getOffset();
	}

	/**
	 * Set timezone.
	 *
	 * @param DateTimeZone $timezone DateTimeZone instance.
	 * @return DateTime
	 */
	public function setTimezone( $timezone ) {
		$this->utc_offset = 0;
		return parent::setTimezone( $timezone );
	}

	/**
	 * Missing in PHP 5.2 so just here so it can be supported consistently.
	 *
	 * @since  3.0.0
	 * @return int
	 */
	public function getTimestamp() {
		return method_exists( 'DateTime', 'getTimestamp' ) ? parent::getTimestamp() : $this->format( 'U' );
	}

	/**
	 * Get the timestamp with the WordPress timezone offset added or subtracted.
	 *
	 * @since  3.0.0
	 * @return int
	 */
	public function getOffsetTimestamp() {
		return $this->getTimestamp() + $this->getOffset();
	}

	/**
	 * Format a date based on the offset timestamp.
	 *
	 * @since  3.0.0
	 * @param  string $format Date format.
	 * @return string
	 */
	public function date( $format ) {
		return gmdate( $format, $this->getOffsetTimestamp() );
	}

	/**
	 * Return a localised date based on offset timestamp. Wrapper for date_i18n function.
	 *
	 * @since  3.0.0
	 * @param  string $format Date format.
	 * @return string
	 */
	public function date_i18n( $format = 'Y-m-d' ) {
		return date_i18n( $format, $this->getOffsetTimestamp() );
	}
}
admin/views/html-notice-no-shipping-methods.php000064400000002450151336044770015632 0ustar00<?php
/**
 * Admin View: Notice - No Shipping methods.
 *
 * @package WooCommerce\Admin\Notices
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>
<div id="message" class="updated woocommerce-message">
	<a class="woocommerce-message-close notice-dismiss" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-hide-notice', 'no_shipping_methods' ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ) ); ?>">
		<?php esc_html_e( 'Dismiss', 'woocommerce' ); ?>
	</a>

	<p class="main">
		<strong>
			<?php esc_html_e( 'Add shipping methods &amp; zones', 'woocommerce' ); ?>
		</strong>
	</p>
	<p>
		<?php esc_html_e( 'Shipping is currently enabled, but you have not added any shipping methods to your shipping zones.', 'woocommerce' ); ?>
	</p>
	<p>
		<?php esc_html_e( 'Customers will not be able to purchase physical goods from your store until a shipping method is available.', 'woocommerce' ); ?>
	</p>

	<p class="submit">
		<a class="button-primary" href="<?php echo esc_url( admin_url( 'admin.php?page=wc-settings&tab=shipping' ) ); ?>">
			<?php esc_html_e( 'Setup shipping zones', 'woocommerce' ); ?>
		</a>
		<a class="button-secondary" href="https://docs.woocommerce.com/document/setting-up-shipping-zones/">
			<?php esc_html_e( 'Learn more about shipping zones', 'woocommerce' ); ?>
		</a>
	</p>
</div>
admin/views/html-admin-page-addons.php000064400000013310151336044770013724 0ustar00<?php
/**
 * Admin View: Page - Addons
 *
 * @package WooCommerce\Admin
 * @var string $view
 * @var object $addons
 * @var object $promotions
 */

use Automattic\WooCommerce\Admin\RemoteInboxNotifications as PromotionRuleEngine;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

$current_section_name = __( 'Browse Categories', 'woocommerce' );
?>
<div class="woocommerce wc-addons-wrap">
	<h1 class="screen-reader-text"><?php esc_html_e( 'Marketplace', 'woocommerce' ); ?></h1>

	<?php if ( $sections ) : ?>
	<div class="marketplace-header">
		<h1 class="marketplace-header__title"><?php esc_html_e( 'WooCommerce Marketplace', 'woocommerce' ); ?></h1>
		<p class="marketplace-header__description"><?php esc_html_e( 'Grow your business with hundreds of free and paid WooCommerce extensions.', 'woocommerce' ); ?></p>
		<form class="marketplace-header__search-form" method="GET">
			<input
				type="text"
				name="search"
				value="<?php echo esc_attr( ! empty( $search ) ? sanitize_text_field( wp_unslash( $search ) ) : '' ); ?>"
				placeholder="<?php esc_attr_e( 'Search for extensions', 'woocommerce' ); ?>"
			/>
			<button type="submit">
				<span class="dashicons dashicons-search"></span>
			</button>
			<input type="hidden" name="page" value="wc-addons">
			<input type="hidden" name="section" value="_all">
		</form>
	</div>

	<div class="top-bar">
		<div id="marketplace-current-section-dropdown" class="current-section-dropdown">
			<ul>
				<?php foreach ( $sections as $section ) : ?>
					<?php
					if ( $current_section === $section->slug && '_featured' !== $section->slug ) {
						$current_section_name = $section->label;
					}
					?>
					<li>
						<a
							class="<?php echo $current_section === $section->slug ? 'current' : ''; ?>"
							href="<?php echo esc_url( admin_url( 'admin.php?page=wc-addons&section=' . esc_attr( $section->slug ) ) ); ?>">
							<?php echo esc_html( $section->label ); ?>
						</a>
					</li>
				<?php endforeach; ?>
			</ul>
			<div id="marketplace-current-section-name" class="current-section-name"><?php echo esc_html( $current_section_name ); ?></div>
		</div>
	</div>

	<div class="wp-header-end"></div>

	<div class="wrap">
		<div class="marketplace-content-wrapper">
			<?php if ( ! empty( $search ) && 0 === count( $addons ) ) : ?>
				<h1 class="search-form-title">
					<?php esc_html_e( 'Sorry, could not find anything. Try searching again using a different term.', 'woocommerce' ); ?></p>
				</h1>
			<?php endif; ?>
			<?php if ( ! empty( $search ) && count( $addons ) > 0 ) : ?>
				<h1 class="search-form-title">
					<?php // translators: search keyword. ?>
					<?php printf( esc_html__( 'Search results for "%s"', 'woocommerce' ), esc_html( sanitize_text_field( wp_unslash( $search ) ) ) ); ?>
				</h1>
			<?php endif; ?>

			<?php if ( '_featured' === $current_section ) : ?>
				<div class="addons-featured">
					<?php WC_Admin_Addons::render_featured(); ?>
				</div>
			<?php endif; ?>
			<?php if ( '_featured' !== $current_section && $addons ) : ?>
				<?php
				if ( ! empty( $promotions ) && WC()->is_wc_admin_active() ) {
					foreach ( $promotions as $promotion ) {
						WC_Admin_Addons::output_search_promotion_block( $promotion );
					}
				}
				?>
				<ul class="products">
					<?php foreach ( $addons as $addon ) : ?>
						<?php
						if ( 'shipping_methods' === $current_section ) {
							// Do not show USPS or Canada Post extensions for US and CA stores, respectively.
							$country = WC()->countries->get_base_country();
							if ( 'US' === $country
								&& false !== strpos(
									$addon->link,
									'woocommerce.com/products/usps-shipping-method'
								)
							) {
								continue;
							}
							if ( 'CA' === $country
								&& false !== strpos(
									$addon->link,
									'woocommerce.com/products/canada-post-shipping-method'
								)
							) {
								continue;
							}
						}

						WC_Admin_Addons::render_product_card( $addon );
						?>
					<?php endforeach; ?>
				</ul>
			<?php endif; ?>
		</div>
		<?php else : ?>
			<?php /* translators: a url */ ?>
			<p><?php printf( wp_kses_post( __( 'Our catalog of WooCommerce Extensions can be found on WooCommerce.com here: <a href="%s">WooCommerce Extensions Catalog</a>', 'woocommerce' ) ), 'https://woocommerce.com/product-category/woocommerce-extensions/' ); ?></p>
		<?php endif; ?>

		<?php if ( 'Storefront' !== $theme['Name'] && '_featured' !== $current_section ) : ?>
			<?php
				$storefront_url = WC_Admin_Addons::add_in_app_purchase_url_params( 'https://woocommerce.com/storefront/?utm_source=extensionsscreen&utm_medium=product&utm_campaign=wcaddon' );
			?>
			<div class="storefront">
				<a href="<?php echo esc_url( $storefront_url ); ?>" target="_blank"><img src="<?php echo esc_url( WC()->plugin_url() ); ?>/assets/images/storefront.png" alt="<?php esc_attr_e( 'Storefront', 'woocommerce' ); ?>" /></a>
				<h2><?php esc_html_e( 'Looking for a WooCommerce theme?', 'woocommerce' ); ?></h2>
				<p><?php echo wp_kses_post( __( 'We recommend Storefront, the <em>official</em> WooCommerce theme.', 'woocommerce' ) ); ?></p>
				<p><?php echo wp_kses_post( __( 'Storefront is an intuitive, flexible and <strong>free</strong> WordPress theme offering deep integration with WooCommerce and many of the most popular customer-facing extensions.', 'woocommerce' ) ); ?></p>
				<p>
					<a href="<?php echo esc_url( $storefront_url ); ?>" target="_blank" class="button"><?php esc_html_e( 'Read all about it', 'woocommerce' ); ?></a>
					<a href="<?php echo esc_url( wp_nonce_url( self_admin_url( 'update.php?action=install-theme&theme=storefront' ), 'install-theme_storefront' ) ); ?>" class="button button-primary"><?php esc_html_e( 'Download &amp; install', 'woocommerce' ); ?></a>
				</p>
			</div>
		<?php endif; ?>
	</div>
</div>
admin/views/html-admin-page-product-export.php000064400000007640151336044770015464 0ustar00<?php
/**
 * Admin View: Product Export
 *
 * @package WooCommerce\Admin\Export
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

wp_enqueue_script( 'wc-product-export' );

$exporter = new WC_Product_CSV_Exporter();
?>
<div class="wrap woocommerce">
	<h1><?php esc_html_e( 'Export Products', 'woocommerce' ); ?></h1>

	<div class="woocommerce-exporter-wrapper">
		<form class="woocommerce-exporter">
			<header>
				<span class="spinner is-active"></span>
				<h2><?php esc_html_e( 'Export products to a CSV file', 'woocommerce' ); ?></h2>
				<p><?php esc_html_e( 'This tool allows you to generate and download a CSV file containing a list of all products.', 'woocommerce' ); ?></p>
			</header>
			<section>
				<table class="form-table woocommerce-exporter-options">
					<tbody>
						<tr>
							<th scope="row">
								<label for="woocommerce-exporter-columns"><?php esc_html_e( 'Which columns should be exported?', 'woocommerce' ); ?></label>
							</th>
							<td>
								<select id="woocommerce-exporter-columns" class="woocommerce-exporter-columns wc-enhanced-select" style="width:100%;" multiple data-placeholder="<?php esc_attr_e( 'Export all columns', 'woocommerce' ); ?>">
									<?php
									foreach ( $exporter->get_default_column_names() as $column_id => $column_name ) {
										echo '<option value="' . esc_attr( $column_id ) . '">' . esc_html( $column_name ) . '</option>';
									}
									?>
									<option value="downloads"><?php esc_html_e( 'Downloads', 'woocommerce' ); ?></option>
									<option value="attributes"><?php esc_html_e( 'Attributes', 'woocommerce' ); ?></option>
								</select>
							</td>
						</tr>
						<tr>
							<th scope="row">
								<label for="woocommerce-exporter-types"><?php esc_html_e( 'Which product types should be exported?', 'woocommerce' ); ?></label>
							</th>
							<td>
								<select id="woocommerce-exporter-types" class="woocommerce-exporter-types wc-enhanced-select" style="width:100%;" multiple data-placeholder="<?php esc_attr_e( 'Export all products', 'woocommerce' ); ?>">
									<?php
									foreach ( WC_Admin_Exporters::get_product_types() as $value => $label ) {
										echo '<option value="' . esc_attr( $value ) . '">' . esc_html( $label ) . '</option>';
									}
									?>
								</select>
							</td>
						</tr>
						<tr>
							<th scope="row">
								<label for="woocommerce-exporter-category"><?php esc_html_e( 'Which product category should be exported?', 'woocommerce' ); ?></label>
							</th>
							<td>
								<select id="woocommerce-exporter-category" class="woocommerce-exporter-category wc-enhanced-select" style="width:100%;" multiple data-placeholder="<?php esc_attr_e( 'Export all categories', 'woocommerce' ); ?>">
								<?php
								$categories = get_categories(
									array(
										'taxonomy'   => 'product_cat',
										'hide_empty' => false,
									)
								);
								foreach ( $categories as $category ) {
									echo '<option value="' . esc_attr( $category->slug ) . '">' . esc_html( $category->name ) . '</option>';
								}
								?>
								</select>
							</td>
						</tr>
						<tr>
							<th scope="row">
								<label for="woocommerce-exporter-meta"><?php esc_html_e( 'Export custom meta?', 'woocommerce' ); ?></label>
							</th>
							<td>
								<input type="checkbox" id="woocommerce-exporter-meta" value="1" />
								<label for="woocommerce-exporter-meta"><?php esc_html_e( 'Yes, export all custom meta', 'woocommerce' ); ?></label>
							</td>
						</tr>
						<?php do_action( 'woocommerce_product_export_row' ); ?>
					</tbody>
				</table>
				<progress class="woocommerce-exporter-progress" max="100" value="0"></progress>
			</section>
			<div class="wc-actions">
				<button type="submit" class="woocommerce-exporter-button button button-primary" value="<?php esc_attr_e( 'Generate CSV', 'woocommerce' ); ?>"><?php esc_html_e( 'Generate CSV', 'woocommerce' ); ?></button>
			</div>
		</form>
	</div>
</div>
admin/views/html-notice-base-table-missing.php000064400000003111151336044770015377 0ustar00<?php
/**
 * Admin View: Notice - Base table missing.
 *
 * @package WooCommerce\Admin
 */

defined( 'ABSPATH' ) || exit;

?>
<div class="updated woocommerce-message">
	<a class="woocommerce-message-close notice-dismiss" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-hide-notice', 'base_tables_missing' ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ) ); ?>">
		<?php esc_html_e( 'Dismiss', 'woocommerce' ); ?>
	</a>

	<p>
		<strong><?php esc_html_e( 'Database tables missing', 'woocommerce' ); ?></strong>
	</p>
	<p>
		<?php
		$verify_db_tool_available = array_key_exists( 'verify_db_tables', WC_Admin_Status::get_tools() );
		$missing_tables           = get_option( 'woocommerce_schema_missing_tables' );
		if ( $verify_db_tool_available ) {
			echo wp_kses_post(
				sprintf(
				/* translators: %1%s: Missing tables (seperated by ",") %2$s: Link to check again */
					__( 'One or more tables required for WooCommerce to function are missing, some features may not work as expected. Missing tables: %1$s. <a href="%2$s">Check again.</a>', 'woocommerce' ),
					esc_html( implode( ', ', $missing_tables ) ),
					wp_nonce_url( admin_url( 'admin.php?page=wc-status&tab=tools&action=verify_db_tables' ), 'debug_action' )
				)
			);
		} else {
			echo wp_kses_post(
				sprintf(
				/* translators: %1%s: Missing tables (seperated by ",") */
					__( 'One or more tables required for WooCommerce to function are missing, some features may not work as expected. Missing tables: %1$s.', 'woocommerce' ),
					esc_html( implode( ', ', $missing_tables ) )
				)
			);
		}
		?>
	</p>
</div>
admin/views/html-admin-page-status-logs.php000064400000003713151336044770014747 0ustar00<?php
/**
 * Admin View: Page - Status Logs
 *
 * @package WooCommerce\Admin\Logs
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

?>
<?php if ( $logs ) : ?>
	<div id="log-viewer-select">
		<div class="alignleft">
			<h2>
				<?php echo esc_html( $viewed_log ); ?>
				<?php if ( ! empty( $viewed_log ) ) : ?>
					<a class="page-title-action" href="<?php echo esc_url( wp_nonce_url( add_query_arg( array( 'handle' => sanitize_title( $viewed_log ) ), admin_url( 'admin.php?page=wc-status&tab=logs' ) ), 'remove_log' ) ); ?>" class="button"><?php esc_html_e( 'Delete log', 'woocommerce' ); ?></a>
				<?php endif; ?>
			</h2>
		</div>
		<div class="alignright">
			<form action="<?php echo esc_url( admin_url( 'admin.php?page=wc-status&tab=logs' ) ); ?>" method="post">
				<select name="log_file">
					<?php foreach ( $logs as $log_key => $log_file ) : ?>
						<?php
							$timestamp = filemtime( WC_LOG_DIR . $log_file );
							$date      = sprintf(
								/* translators: 1: last access date 2: last access time 3: last access timezone abbreviation */
								__( '%1$s at %2$s %3$s', 'woocommerce' ),
								wp_date( wc_date_format(), $timestamp ),
								wp_date( wc_time_format(), $timestamp ),
								wp_date( 'T', $timestamp )
							);
						?>
						<option value="<?php echo esc_attr( $log_key ); ?>" <?php selected( sanitize_title( $viewed_log ), $log_key ); ?>><?php echo esc_html( $log_file ); ?> (<?php echo esc_html( $date ); ?>)</option>
					<?php endforeach; ?>
				</select>
				<button type="submit" class="button" value="<?php esc_attr_e( 'View', 'woocommerce' ); ?>"><?php esc_html_e( 'View', 'woocommerce' ); ?></button>
			</form>
		</div>
		<div class="clear"></div>
	</div>
	<div id="log-viewer">
		<pre><?php echo esc_html( file_get_contents( WC_LOG_DIR . $viewed_log ) ); ?></pre>
	</div>
<?php else : ?>
	<div class="updated woocommerce-message inline"><p><?php esc_html_e( 'There are currently no logs to view.', 'woocommerce' ); ?></p></div>
<?php endif; ?>
admin/views/html-notice-secure-connection.php000064400000001525151336044770015363 0ustar00<?php
/**
 * Admin View: Notice - Secure connection.
 *
 * @package WooCommerce\Admin\Notices
 */

defined( 'ABSPATH' ) || exit;

?>
<div id="message" class="updated woocommerce-message">
	<a class="woocommerce-message-close notice-dismiss" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-hide-notice', 'no_secure_connection' ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ) ); ?>"><?php esc_html_e( 'Dismiss', 'woocommerce' ); ?></a>

	<p>
	<?php
		echo wp_kses_post( sprintf(
			/* translators: %s: documentation URL */
			__( 'Your store does not appear to be using a secure connection. We highly recommend serving your entire website over an HTTPS connection to help keep customer data secure. <a href="%s">Learn more here.</a>', 'woocommerce' ),
			'https://docs.woocommerce.com/document/ssl-and-https/'
		) );
	?>
	</p>
</div>
admin/views/html-notice-regenerating-thumbnails.php000064400000001152151336044770016552 0ustar00<?php
/**
 * Admin View: Notice - Regenerating thumbnails.
 */

defined( 'ABSPATH' ) || exit;

?>
<div id="message" class="updated woocommerce-message">
	<a class="woocommerce-message-close notice-dismiss" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-hide-notice', 'regenerating_thumbnails' ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ) ); ?>"><?php _e( 'Cancel thumbnail regeneration', 'woocommerce' ); ?></a>

	<p><?php esc_html_e( 'Thumbnail regeneration is running in the background. Depending on the amount of images in your store this may take a while.', 'woocommerce' ); ?></p>
</div>
admin/views/html-notice-updating.php000064400000002171151336044770013551 0ustar00<?php
/**
 * Admin View: Notice - Updating
 *
 * @package WooCommerce\Admin
 */

use Automattic\Jetpack\Constants;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

$pending_actions_url = admin_url( 'admin.php?page=wc-status&tab=action-scheduler&s=woocommerce_run_update&status=pending' );
$cron_disabled       = Constants::is_true( 'DISABLE_WP_CRON' );
$cron_cta            = $cron_disabled ? __( 'You can manually run queued updates here.', 'woocommerce' ) : __( 'View progress &rarr;', 'woocommerce' );
?>
<div id="message" class="updated woocommerce-message wc-connect">
	<p>
		<strong><?php esc_html_e( 'WooCommerce database update', 'woocommerce' ); ?></strong><br>
		<?php esc_html_e( 'WooCommerce is updating the database in the background. The database update process may take a little while, so please be patient.', 'woocommerce' ); ?>
		<?php
		if ( $cron_disabled ) {
			echo '<br>' . esc_html__( 'Note: WP CRON has been disabled on your install which may prevent this update from completing.', 'woocommerce' );
		}
		?>
		&nbsp;<a href="<?php echo esc_url( $pending_actions_url ); ?>"><?php echo esc_html( $cron_cta ); ?></a>
	</p>
</div>
admin/views/html-admin-page-status-report.php000064400000152722151336044770015323 0ustar00<?php
/**
 * Admin View: Page - Status Report.
 *
 * @package WooCommerce
 */

use Automattic\Jetpack\Constants;

defined( 'ABSPATH' ) || exit;

global $wpdb;

$report             = wc()->api->get_endpoint_data( '/wc/v3/system_status' );
$environment        = $report['environment'];
$database           = $report['database'];
$post_type_counts   = isset( $report['post_type_counts'] ) ? $report['post_type_counts'] : array();
$active_plugins     = $report['active_plugins'];
$inactive_plugins   = $report['inactive_plugins'];
$dropins_mu_plugins = $report['dropins_mu_plugins'];
$theme              = $report['theme'];
$security           = $report['security'];
$settings           = $report['settings'];
$wp_pages           = $report['pages'];
$plugin_updates     = new WC_Plugin_Updates();
$untested_plugins   = $plugin_updates->get_untested_plugins( WC()->version, Constants::get_constant( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE' ) );
?>
<div class="updated woocommerce-message inline">
	<p>
		<?php esc_html_e( 'Please copy and paste this information in your ticket when contacting support:', 'woocommerce' ); ?>
	</p>
	<p class="submit">
		<a href="#" class="button-primary debug-report"><?php esc_html_e( 'Get system report', 'woocommerce' ); ?></a>
		<a class="button-secondary docs" href="https://docs.woocommerce.com/document/understanding-the-woocommerce-system-status-report/" target="_blank">
			<?php esc_html_e( 'Understanding the status report', 'woocommerce' ); ?>
		</a>
	</p>
	<div id="debug-report">
		<textarea readonly="readonly"></textarea>
		<p class="submit">
			<button id="copy-for-support" class="button-primary" href="#" data-tip="<?php esc_attr_e( 'Copied!', 'woocommerce' ); ?>">
				<?php esc_html_e( 'Copy for support', 'woocommerce' ); ?>
			</button>
		</p>
		<p class="copy-error hidden">
			<?php esc_html_e( 'Copying to clipboard failed. Please press Ctrl/Cmd+C to copy.', 'woocommerce' ); ?>
		</p>
	</div>
</div>
<table class="wc_status_table widefat" cellspacing="0" id="status">
	<thead>
		<tr>
			<th colspan="3" data-export-label="WordPress Environment"><h2><?php esc_html_e( 'WordPress environment', 'woocommerce' ); ?></h2></th>
		</tr>
	</thead>
	<tbody>
		<tr>
			<td data-export-label="WordPress address (URL)"><?php esc_html_e( 'WordPress address (URL)', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'The root URL of your site.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td><?php echo esc_html( $environment['site_url'] ); ?></td>
		</tr>
		<tr>
			<td data-export-label="Site address (URL)"><?php esc_html_e( 'Site address (URL)', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'The homepage URL of your site.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td><?php echo esc_html( $environment['home_url'] ); ?></td>
		</tr>
		<tr>
			<td data-export-label="WC Version"><?php esc_html_e( 'WooCommerce version', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'The version of WooCommerce installed on your site.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td><?php echo esc_html( $environment['version'] ); ?></td>
		</tr>
		<tr>
			<td data-export-label="REST API Version"><?php esc_html_e( 'WooCommerce REST API package', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'The WooCommerce REST API package running on your site.', 'woocommerce' ) ); ?></td>
			<td>
				<?php
				$version = wc()->api->get_rest_api_package_version();

				if ( ! is_null( $version ) ) {
					echo '<mark class="yes"><span class="dashicons dashicons-yes"></span> ' . esc_html( $version ) . ' <code class="private">' . esc_html( wc()->api->get_rest_api_package_path() ) . '</code></mark> ';
				} else {
					echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . esc_html__( 'Unable to detect the REST API package.', 'woocommerce' ) . '</mark>';
				}
				?>
			</td>
		</tr>
		<tr>
			<td data-export-label="WC Blocks Version"><?php esc_html_e( 'WooCommerce Blocks package', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'The WooCommerce Blocks package running on your site.', 'woocommerce' ) ); ?></td>
			<td>
				<?php
				if ( class_exists( '\Automattic\WooCommerce\Blocks\Package' ) ) {
					$version = \Automattic\WooCommerce\Blocks\Package::get_version();
					$path    = \Automattic\WooCommerce\Blocks\Package::get_path(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
				} else {
					$version = null;
				}

				if ( ! is_null( $version ) ) {
					echo '<mark class="yes"><span class="dashicons dashicons-yes"></span> ' . esc_html( $version ) . ' <code class="private">' . esc_html( $path ) . '</code></mark> ';
				} else {
					echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . esc_html__( 'Unable to detect the Blocks package.', 'woocommerce' ) . '</mark>';
				}
				?>
			</td>
		</tr>
		<tr>
			<td data-export-label="Action Scheduler Version"><?php esc_html_e( 'Action Scheduler package', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'Action Scheduler package running on your site.', 'woocommerce' ) ); ?></td>
			<td>
				<?php
				if ( class_exists( 'ActionScheduler_Versions' ) && class_exists( 'ActionScheduler' ) ) {
					$version = ActionScheduler_Versions::instance()->latest_version();
					$path    = ActionScheduler::plugin_path( '' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
				} else {
					$version = null;
				}

				if ( ! is_null( $version ) ) {
					echo '<mark class="yes"><span class="dashicons dashicons-yes"></span> ' . esc_html( $version ) . ' <code class="private">' . esc_html( $path ) . '</code></mark> ';
				} else {
					echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . esc_html__( 'Unable to detect the Action Scheduler package.', 'woocommerce' ) . '</mark>';
				}
				?>
			</td>
		</tr>
		<tr>
			<td data-export-label="WC Admin Version"><?php esc_html_e( 'WooCommerce Admin package', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'The WooCommerce Admin package running on your site.', 'woocommerce' ) ); ?></td>
			<td>
				<?php
				$wc_admin_path = null;
				if ( defined( 'WC_ADMIN_VERSION_NUMBER' ) ) {
					// Plugin version of WC Admin.
					$version        = WC_ADMIN_VERSION_NUMBER;
					$package_active = false;
				} elseif ( class_exists( '\Automattic\WooCommerce\Admin\Composer\Package' ) ) {
					if ( WC()->is_wc_admin_active() ) {
						// Fully active package version of WC Admin.
						$version        = \Automattic\WooCommerce\Admin\Composer\Package::get_active_version();
						$package_active = \Automattic\WooCommerce\Admin\Composer\Package::is_package_active();
					} else {
						// with WP version < 5.3, package is present, but inactive.
						$version = sprintf(
							/* translators: %s: Version number of wc-admin package */
							__( 'Inactive %s', 'woocommerce' ),
							\Automattic\WooCommerce\Admin\Composer\Package::VERSION
						);
						$package_active = false;
					}
					$wc_admin_path = \Automattic\WooCommerce\Admin\Composer\Package::get_path();
				} else {
					$version = null;
				}

				if ( ! is_null( $version ) ) {
					if ( ! isset( $wc_admin_path ) ) {
						if ( defined( 'WC_ADMIN_PLUGIN_FILE' ) ) {
							$wc_admin_path = dirname( WC_ADMIN_PLUGIN_FILE );
						} else {
							$wc_admin_path = __( 'Active Plugin', 'woocommerce' );
						}
					}
					if ( WC()->is_wc_admin_active() ) {
						echo '<mark class="yes"><span class="dashicons dashicons-yes"></span> ' . esc_html( $version ) . ' <code class="private">' . esc_html( $wc_admin_path ) . '</code></mark> ';
					} else {
						echo '<span class="dashicons dashicons-no-alt"></span> ' . esc_html( $version ) . ' <code class="private">' . esc_html( $wc_admin_path ) . '</code> ';
					}
				} else {
					echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . esc_html__( 'Unable to detect the WC Admin package.', 'woocommerce' ) . '</mark>';
				}
				?>
			</td>
		</tr>
		<tr>
			<td data-export-label="Log Directory Writable"><?php esc_html_e( 'Log directory writable', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'Several WooCommerce extensions can write logs which makes debugging problems easier. The directory must be writable for this to happen.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td>
				<?php
				if ( $environment['log_directory_writable'] ) {
					echo '<mark class="yes"><span class="dashicons dashicons-yes"></span> <code class="private">' . esc_html( $environment['log_directory'] ) . '</code></mark> ';
				} else {
					/* Translators: %1$s: Log directory, %2$s: Log directory constant */
					echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . sprintf( esc_html__( 'To allow logging, make %1$s writable or define a custom %2$s.', 'woocommerce' ), '<code>' . esc_html( $environment['log_directory'] ) . '</code>', '<code>WC_LOG_DIR</code>' ) . '</mark>';
				}
				?>
			</td>
		</tr>
		<tr>
			<td data-export-label="WP Version"><?php esc_html_e( 'WordPress version', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'The version of WordPress installed on your site.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td>
				<?php
				$latest_version = get_transient( 'woocommerce_system_status_wp_version_check' );

				if ( false === $latest_version ) {
					$version_check = wp_remote_get( 'https://api.wordpress.org/core/version-check/1.7/' );
					$api_response  = json_decode( wp_remote_retrieve_body( $version_check ), true );

					if ( $api_response && isset( $api_response['offers'], $api_response['offers'][0], $api_response['offers'][0]['version'] ) ) {
						$latest_version = $api_response['offers'][0]['version'];
					} else {
						$latest_version = $environment['wp_version'];
					}
					set_transient( 'woocommerce_system_status_wp_version_check', $latest_version, DAY_IN_SECONDS );
				}

				if ( version_compare( $environment['wp_version'], $latest_version, '<' ) ) {
					/* Translators: %1$s: Current version, %2$s: New version */
					echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . sprintf( esc_html__( '%1$s - There is a newer version of WordPress available (%2$s)', 'woocommerce' ), esc_html( $environment['wp_version'] ), esc_html( $latest_version ) ) . '</mark>';
				} else {
					echo '<mark class="yes">' . esc_html( $environment['wp_version'] ) . '</mark>';
				}
				?>
			</td>
		</tr>
		<tr>
			<td data-export-label="WP Multisite"><?php esc_html_e( 'WordPress multisite', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'Whether or not you have WordPress Multisite enabled.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td><?php echo ( $environment['wp_multisite'] ) ? '<span class="dashicons dashicons-yes"></span>' : '&ndash;'; ?></td>
		</tr>
		<tr>
			<td data-export-label="WP Memory Limit"><?php esc_html_e( 'WordPress memory limit', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'The maximum amount of memory (RAM) that your site can use at one time.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td>
				<?php
				if ( $environment['wp_memory_limit'] < 67108864 ) {
					/* Translators: %1$s: Memory limit, %2$s: Docs link. */
					echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . sprintf( esc_html__( '%1$s - We recommend setting memory to at least 64MB. See: %2$s', 'woocommerce' ), esc_html( size_format( $environment['wp_memory_limit'] ) ), '<a href="https://wordpress.org/support/article/editing-wp-config-php/#increasing-memory-allocated-to-php" target="_blank">' . esc_html__( 'Increasing memory allocated to PHP', 'woocommerce' ) . '</a>' ) . '</mark>';
				} else {
					echo '<mark class="yes">' . esc_html( size_format( $environment['wp_memory_limit'] ) ) . '</mark>';
				}
				?>
			</td>
		</tr>
		<tr>
			<td data-export-label="WP Debug Mode"><?php esc_html_e( 'WordPress debug mode', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'Displays whether or not WordPress is in Debug Mode.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td>
				<?php if ( $environment['wp_debug_mode'] ) : ?>
					<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>
				<?php else : ?>
					<mark class="no">&ndash;</mark>
				<?php endif; ?>
			</td>
		</tr>
		<tr>
			<td data-export-label="WP Cron"><?php esc_html_e( 'WordPress cron', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'Displays whether or not WP Cron Jobs are enabled.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td>
				<?php if ( $environment['wp_cron'] ) : ?>
					<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>
				<?php else : ?>
					<mark class="no">&ndash;</mark>
				<?php endif; ?>
			</td>
		</tr>
		<tr>
			<td data-export-label="Language"><?php esc_html_e( 'Language', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'The current language used by WordPress. Default = English', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td><?php echo esc_html( $environment['language'] ); ?></td>
		</tr>
		<tr>
			<td data-export-label="External object cache"><?php esc_html_e( 'External object cache', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'Displays whether or not WordPress is using an external object cache.', 'woocommerce' ) ); ?></td>
			<td>
				<?php if ( $environment['external_object_cache'] ) : ?>
					<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>
				<?php else : ?>
					<mark class="no">&ndash;</mark>
				<?php endif; ?>
			</td>
		</tr>
	</tbody>
</table>
<table class="wc_status_table widefat" cellspacing="0">
	<thead>
		<tr>
			<th colspan="3" data-export-label="Server Environment"><h2><?php esc_html_e( 'Server environment', 'woocommerce' ); ?></h2></th>
		</tr>
	</thead>
	<tbody>
		<tr>
			<td data-export-label="Server Info"><?php esc_html_e( 'Server info', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'Information about the web server that is currently hosting your site.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td><?php echo esc_html( $environment['server_info'] ); ?></td>
		</tr>
		<tr>
			<td data-export-label="PHP Version"><?php esc_html_e( 'PHP version', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'The version of PHP installed on your hosting server.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td>
				<?php
				if ( version_compare( $environment['php_version'], '7.2', '>=' ) ) {
					echo '<mark class="yes">' . esc_html( $environment['php_version'] ) . '</mark>';
				} else {
					$update_link = ' <a href="https://docs.woocommerce.com/document/how-to-update-your-php-version/" target="_blank">' . esc_html__( 'How to update your PHP version', 'woocommerce' ) . '</a>';
					$class       = 'error';

					if ( version_compare( $environment['php_version'], '5.4', '<' ) ) {
						$notice = '<span class="dashicons dashicons-warning"></span> ' . __( 'WooCommerce will run under this version of PHP, however, some features such as geolocation are not compatible. Support for this version will be dropped in the next major release. We recommend using PHP version 7.2 or above for greater performance and security.', 'woocommerce' ) . $update_link;
					} elseif ( version_compare( $environment['php_version'], '5.6', '<' ) ) {
						$notice = '<span class="dashicons dashicons-warning"></span> ' . __( 'WooCommerce will run under this version of PHP, however, it has reached end of life. We recommend using PHP version 7.2 or above for greater performance and security.', 'woocommerce' ) . $update_link;
					} elseif ( version_compare( $environment['php_version'], '7.2', '<' ) ) {
						$notice = __( 'We recommend using PHP version 7.2 or above for greater performance and security.', 'woocommerce' ) . $update_link;
						$class  = 'recommendation';
					}

					echo '<mark class="' . esc_attr( $class ) . '">' . esc_html( $environment['php_version'] ) . ' - ' . wp_kses_post( $notice ) . '</mark>';
				}
				?>
			</td>
		</tr>
		<?php if ( function_exists( 'ini_get' ) ) : ?>
			<tr>
				<td data-export-label="PHP Post Max Size"><?php esc_html_e( 'PHP post max size', 'woocommerce' ); ?>:</td>
				<td class="help"><?php echo wc_help_tip( esc_html__( 'The largest filesize that can be contained in one post.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
				<td><?php echo esc_html( size_format( $environment['php_post_max_size'] ) ); ?></td>
			</tr>
			<tr>
				<td data-export-label="PHP Time Limit"><?php esc_html_e( 'PHP time limit', 'woocommerce' ); ?>:</td>
				<td class="help"><?php echo wc_help_tip( esc_html__( 'The amount of time (in seconds) that your site will spend on a single operation before timing out (to avoid server lockups)', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
				<td><?php echo esc_html( $environment['php_max_execution_time'] ); ?></td>
			</tr>
			<tr>
				<td data-export-label="PHP Max Input Vars"><?php esc_html_e( 'PHP max input vars', 'woocommerce' ); ?>:</td>
				<td class="help"><?php echo wc_help_tip( esc_html__( 'The maximum number of variables your server can use for a single function to avoid overloads.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
				<td><?php echo esc_html( $environment['php_max_input_vars'] ); ?></td>
			</tr>
			<tr>
				<td data-export-label="cURL Version"><?php esc_html_e( 'cURL version', 'woocommerce' ); ?>:</td>
				<td class="help"><?php echo wc_help_tip( esc_html__( 'The version of cURL installed on your server.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
				<td><?php echo esc_html( $environment['curl_version'] ); ?></td>
			</tr>
			<tr>
				<td data-export-label="SUHOSIN Installed"><?php esc_html_e( 'SUHOSIN installed', 'woocommerce' ); ?>:</td>
				<td class="help"><?php echo wc_help_tip( esc_html__( 'Suhosin is an advanced protection system for PHP installations. It was designed to protect your servers on the one hand against a number of well known problems in PHP applications and on the other hand against potential unknown vulnerabilities within these applications or the PHP core itself. If enabled on your server, Suhosin may need to be configured to increase its data submission limits.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
				<td><?php echo $environment['suhosin_installed'] ? '<span class="dashicons dashicons-yes"></span>' : '&ndash;'; ?></td>
			</tr>
		<?php endif; ?>

		<?php

		if ( $environment['mysql_version'] ) :
			?>
			<tr>
				<td data-export-label="MySQL Version"><?php esc_html_e( 'MySQL version', 'woocommerce' ); ?>:</td>
				<td class="help"><?php echo wc_help_tip( esc_html__( 'The version of MySQL installed on your hosting server.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
				<td>
					<?php
					if ( version_compare( $environment['mysql_version'], '5.6', '<' ) && ! strstr( $environment['mysql_version_string'], 'MariaDB' ) ) {
						/* Translators: %1$s: MySQL version, %2$s: Recommended MySQL version. */
						echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . sprintf( esc_html__( '%1$s - We recommend a minimum MySQL version of 5.6. See: %2$s', 'woocommerce' ), esc_html( $environment['mysql_version_string'] ), '<a href="https://wordpress.org/about/requirements/" target="_blank">' . esc_html__( 'WordPress requirements', 'woocommerce' ) . '</a>' ) . '</mark>';
					} else {
						echo '<mark class="yes">' . esc_html( $environment['mysql_version_string'] ) . '</mark>';
					}
					?>
				</td>
			</tr>
		<?php endif; ?>
		<tr>
			<td data-export-label="Max Upload Size"><?php esc_html_e( 'Max upload size', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'The largest filesize that can be uploaded to your WordPress installation.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td><?php echo esc_html( size_format( $environment['max_upload_size'] ) ); ?></td>
		</tr>
		<tr>
			<td data-export-label="Default Timezone is UTC"><?php esc_html_e( 'Default timezone is UTC', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'The default timezone for your server.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td>
				<?php
				if ( 'UTC' !== $environment['default_timezone'] ) {
					/* Translators: %s: default timezone.. */
					echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . sprintf( esc_html__( 'Default timezone is %s - it should be UTC', 'woocommerce' ), esc_html( $environment['default_timezone'] ) ) . '</mark>';
				} else {
					echo '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>';
				}
				?>
			</td>
		</tr>
		<tr>
			<td data-export-label="fsockopen/cURL"><?php esc_html_e( 'fsockopen/cURL', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'Payment gateways can use cURL to communicate with remote servers to authorize payments, other plugins may also use it when communicating with remote services.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td>
				<?php
				if ( $environment['fsockopen_or_curl_enabled'] ) {
					echo '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>';
				} else {
					echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . esc_html__( 'Your server does not have fsockopen or cURL enabled - PayPal IPN and other scripts which communicate with other servers will not work. Contact your hosting provider.', 'woocommerce' ) . '</mark>';
				}
				?>
			</td>
		</tr>
		<tr>
			<td data-export-label="SoapClient"><?php esc_html_e( 'SoapClient', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'Some webservices like shipping use SOAP to get information from remote servers, for example, live shipping quotes from FedEx require SOAP to be installed.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td>
				<?php
				if ( $environment['soapclient_enabled'] ) {
					echo '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>';
				} else {
					/* Translators: %s classname and link. */
					echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . sprintf( esc_html__( 'Your server does not have the %s class enabled - some gateway plugins which use SOAP may not work as expected.', 'woocommerce' ), '<a href="https://php.net/manual/en/class.soapclient.php">SoapClient</a>' ) . '</mark>';
				}
				?>
			</td>
		</tr>
		<tr>
			<td data-export-label="DOMDocument"><?php esc_html_e( 'DOMDocument', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'HTML/Multipart emails use DOMDocument to generate inline CSS in templates.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td>
				<?php
				if ( $environment['domdocument_enabled'] ) {
					echo '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>';
				} else {
					/* Translators: %s: classname and link. */
					echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . sprintf( esc_html__( 'Your server does not have the %s class enabled - HTML/Multipart emails, and also some extensions, will not work without DOMDocument.', 'woocommerce' ), '<a href="https://php.net/manual/en/class.domdocument.php">DOMDocument</a>' ) . '</mark>';
				}
				?>
			</td>
		</tr>
		<tr>
			<td data-export-label="GZip"><?php esc_html_e( 'GZip', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'GZip (gzopen) is used to open the GEOIP database from MaxMind.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td>
				<?php
				if ( $environment['gzip_enabled'] ) {
					echo '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>';
				} else {
					/* Translators: %s: classname and link. */
					echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . sprintf( esc_html__( 'Your server does not support the %s function - this is required to use the GeoIP database from MaxMind.', 'woocommerce' ), '<a href="https://php.net/manual/en/zlib.installation.php">gzopen</a>' ) . '</mark>';
				}
				?>
			</td>
		</tr>
		<tr>
			<td data-export-label="Multibyte String"><?php esc_html_e( 'Multibyte string', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'Multibyte String (mbstring) is used to convert character encoding, like for emails or converting characters to lowercase.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td>
				<?php
				if ( $environment['mbstring_enabled'] ) {
					echo '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>';
				} else {
					/* Translators: %s: classname and link. */
					echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . sprintf( esc_html__( 'Your server does not support the %s functions - this is required for better character encoding. Some fallbacks will be used instead for it.', 'woocommerce' ), '<a href="https://php.net/manual/en/mbstring.installation.php">mbstring</a>' ) . '</mark>';
				}
				?>
			</td>
		</tr>
		<tr>
			<td data-export-label="Remote Post"><?php esc_html_e( 'Remote post', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'PayPal uses this method of communicating when sending back transaction information.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td>
				<?php
				if ( $environment['remote_post_successful'] ) {
					echo '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>';
				} else {
					/* Translators: %s: function name. */
					echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . sprintf( esc_html__( '%s failed. Contact your hosting provider.', 'woocommerce' ), 'wp_remote_post()' ) . ' ' . esc_html( $environment['remote_post_response'] ) . '</mark>';
				}
				?>
			</td>
		</tr>
		<tr>
			<td data-export-label="Remote Get"><?php esc_html_e( 'Remote get', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'WooCommerce plugins may use this method of communication when checking for plugin updates.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td>
				<?php
				if ( $environment['remote_get_successful'] ) {
					echo '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>';
				} else {
					/* Translators: %s: function name. */
					echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . sprintf( esc_html__( '%s failed. Contact your hosting provider.', 'woocommerce' ), 'wp_remote_get()' ) . ' ' . esc_html( $environment['remote_get_response'] ) . '</mark>';
				}
				?>
			</td>
		</tr>
		<?php
		$rows = apply_filters( 'woocommerce_system_status_environment_rows', array() );
		foreach ( $rows as $row ) {
			if ( ! empty( $row['success'] ) ) {
				$css_class = 'yes';
				$icon      = '<span class="dashicons dashicons-yes"></span>';
			} else {
				$css_class = 'error';
				$icon      = '<span class="dashicons dashicons-no-alt"></span>';
			}
			?>
			<tr>
				<td data-export-label="<?php echo esc_attr( $row['name'] ); ?>"><?php echo esc_html( $row['name'] ); ?>:</td>
				<td class="help"><?php echo esc_html( isset( $row['help'] ) ? $row['help'] : '' ); ?></td>
				<td>
					<mark class="<?php echo esc_attr( $css_class ); ?>">
						<?php echo wp_kses_post( $icon ); ?> <?php echo wp_kses_data( ! empty( $row['note'] ) ? $row['note'] : '' ); ?>
					</mark>
				</td>
			</tr>
			<?php
		}
		?>
	</tbody>
</table>
<table id="status-database" class="wc_status_table widefat" cellspacing="0">
	<thead>
	<tr>
		<th colspan="3" data-export-label="Database">
			<h2>
				<?php
					esc_html_e( 'Database', 'woocommerce' );
					self::output_tables_info();
				?>
			</h2>
		</th>
	</tr>
	</thead>
	<tbody>
		<tr>
			<td data-export-label="WC Database Version"><?php esc_html_e( 'WooCommerce database version', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'The database version for WooCommerce. Note that it may not match WooCommerce core version and that is normal.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td><?php echo esc_html( $database['wc_database_version'] ); ?></td>
		</tr>
		<tr>
			<td data-export-label="WC Database Prefix"><?php esc_html_e( 'Database prefix', 'woocommerce' ); ?></td>
			<td class="help">&nbsp;</td>
			<td>
				<?php
				if ( strlen( $database['database_prefix'] ) > 20 ) {
					/* Translators: %1$s: Database prefix, %2$s: Docs link. */
					echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . sprintf( esc_html__( '%1$s - We recommend using a prefix with less than 20 characters. See: %2$s', 'woocommerce' ), esc_html( $database['database_prefix'] ), '<a href="https://docs.woocommerce.com/document/completed-order-email-doesnt-contain-download-links/#section-2" target="_blank">' . esc_html__( 'How to update your database table prefix', 'woocommerce' ) . '</a>' ) . '</mark>';
				} else {
					echo '<mark class="yes">' . esc_html( $database['database_prefix'] ) . '</mark>';
				}
				?>
			</td>
		</tr>

		<?php if ( ! empty( $database['database_size'] ) && ! empty( $database['database_tables'] ) ) : ?>
			<tr>
				<td><?php esc_html_e( 'Total Database Size', 'woocommerce' ); ?></td>
				<td class="help">&nbsp;</td>
				<td><?php printf( '%.2fMB', esc_html( $database['database_size']['data'] + $database['database_size']['index'] ) ); ?></td>
			</tr>

			<tr>
				<td><?php esc_html_e( 'Database Data Size', 'woocommerce' ); ?></td>
				<td class="help">&nbsp;</td>
				<td><?php printf( '%.2fMB', esc_html( $database['database_size']['data'] ) ); ?></td>
			</tr>

			<tr>
				<td><?php esc_html_e( 'Database Index Size', 'woocommerce' ); ?></td>
				<td class="help">&nbsp;</td>
				<td><?php printf( '%.2fMB', esc_html( $database['database_size']['index'] ) ); ?></td>
			</tr>

			<?php foreach ( $database['database_tables']['woocommerce'] as $table => $table_data ) { ?>
				<tr>
					<td><?php echo esc_html( $table ); ?></td>
					<td class="help">&nbsp;</td>
					<td>
						<?php
						if ( ! $table_data ) {
							echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . esc_html__( 'Table does not exist', 'woocommerce' ) . '</mark>';
						} else {
							/* Translators: %1$f: Table size, %2$f: Index size, %3$s Engine. */
							printf( esc_html__( 'Data: %1$.2fMB + Index: %2$.2fMB + Engine %3$s', 'woocommerce' ), esc_html( wc_format_decimal( $table_data['data'], 2 ) ), esc_html( wc_format_decimal( $table_data['index'], 2 ) ), esc_html( $table_data['engine'] ) );
						}
						?>
					</td>
				</tr>
			<?php } ?>

			<?php foreach ( $database['database_tables']['other'] as $table => $table_data ) { ?>
				<tr>
					<td><?php echo esc_html( $table ); ?></td>
					<td class="help">&nbsp;</td>
					<td>
						<?php
							/* Translators: %1$f: Table size, %2$f: Index size, %3$s Engine. */
							printf( esc_html__( 'Data: %1$.2fMB + Index: %2$.2fMB + Engine %3$s', 'woocommerce' ), esc_html( wc_format_decimal( $table_data['data'], 2 ) ), esc_html( wc_format_decimal( $table_data['index'], 2 ) ), esc_html( $table_data['engine'] ) );
						?>
					</td>
				</tr>
			<?php } ?>
		<?php else : ?>
			<tr>
				<td><?php esc_html_e( 'Database information:', 'woocommerce' ); ?></td>
				<td class="help">&nbsp;</td>
				<td>
					<?php
					esc_html_e(
						'Unable to retrieve database information. Usually, this is not a problem, and it only means that your install is using a class that replaces the WordPress database class (e.g., HyperDB) and WooCommerce is unable to get database information.',
						'woocommerce'
					);
					?>
				</td>
			</tr>
		<?php endif; ?>
	</tbody>
</table>
<?php if ( $post_type_counts ) : ?>
	<table class="wc_status_table widefat" cellspacing="0">
		<thead>
		<tr>
			<th colspan="3" data-export-label="Post Type Counts"><h2><?php esc_html_e( 'Post Type Counts', 'woocommerce' ); ?></h2></th>
		</tr>
		</thead>
		<tbody>
			<?php
			foreach ( $post_type_counts as $ptype ) {
				?>
				<tr>
					<td><?php echo esc_html( $ptype['type'] ); ?></td>
					<td class="help">&nbsp;</td>
					<td><?php echo absint( $ptype['count'] ); ?></td>
				</tr>
				<?php
			}
			?>
		</tbody>
	</table>
<?php endif; ?>
<table class="wc_status_table widefat" cellspacing="0">
	<thead>
		<tr>
			<th colspan="3" data-export-label="Security"><h2><?php esc_html_e( 'Security', 'woocommerce' ); ?></h2></th>
		</tr>
	</thead>
	<tbody>
		<tr>
			<td data-export-label="Secure connection (HTTPS)"><?php esc_html_e( 'Secure connection (HTTPS)', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'Is the connection to your store secure?', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td>
				<?php if ( $security['secure_connection'] ) : ?>
					<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>
				<?php else : ?>
					<mark class="error"><span class="dashicons dashicons-warning"></span>
					<?php
					/* Translators: %s: docs link. */
					echo wp_kses_post( sprintf( __( 'Your store is not using HTTPS. <a href="%s" target="_blank">Learn more about HTTPS and SSL Certificates</a>.', 'woocommerce' ), 'https://docs.woocommerce.com/document/ssl-and-https/' ) );
					?>
					</mark>
				<?php endif; ?>
			</td>
		</tr>
		<tr>
			<td data-export-label="Hide errors from visitors"><?php esc_html_e( 'Hide errors from visitors', 'woocommerce' ); ?></td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'Error messages can contain sensitive information about your store environment. These should be hidden from untrusted visitors.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td>
				<?php if ( $security['hide_errors'] ) : ?>
					<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>
				<?php else : ?>
					<mark class="error"><span class="dashicons dashicons-warning"></span><?php esc_html_e( 'Error messages should not be shown to visitors.', 'woocommerce' ); ?></mark>
				<?php endif; ?>
			</td>
		</tr>
	</tbody>
</table>
<table class="wc_status_table widefat" cellspacing="0">
	<thead>
		<tr>
			<th colspan="3" data-export-label="Active Plugins (<?php echo count( $active_plugins ); ?>)"><h2><?php esc_html_e( 'Active plugins', 'woocommerce' ); ?> (<?php echo count( $active_plugins ); ?>)</h2></th>
		</tr>
	</thead>
	<tbody>
		<?php self::output_plugins_info( $active_plugins, $untested_plugins ); ?>
	</tbody>
</table>
<table class="wc_status_table widefat" cellspacing="0">
	<thead>
		<tr>
			<th colspan="3" data-export-label="Inactive Plugins (<?php echo count( $inactive_plugins ); ?>)"><h2><?php esc_html_e( 'Inactive plugins', 'woocommerce' ); ?> (<?php echo count( $inactive_plugins ); ?>)</h2></th>
		</tr>
	</thead>
	<tbody>
		<?php self::output_plugins_info( $inactive_plugins, $untested_plugins ); ?>
	</tbody>
</table>
<?php
if ( 0 < count( $dropins_mu_plugins['dropins'] ) ) :
	?>
	<table class="wc_status_table widefat" cellspacing="0">
		<thead>
			<tr>
				<th colspan="3" data-export-label="Dropin Plugins (<?php echo count( $dropins_mu_plugins['dropins'] ); ?>)"><h2><?php esc_html_e( 'Dropin Plugins', 'woocommerce' ); ?> (<?php echo count( $dropins_mu_plugins['dropins'] ); ?>)</h2></th>
			</tr>
		</thead>
		<tbody>
			<?php
			foreach ( $dropins_mu_plugins['dropins'] as $dropin ) {
				?>
				<tr>
					<td><?php echo wp_kses_post( $dropin['plugin'] ); ?></td>
					<td class="help">&nbsp;</td>
					<td><?php echo wp_kses_post( $dropin['name'] ); ?>
				</tr>
				<?php
			}
			?>
		</tbody>
	</table>
	<?php
endif;
if ( 0 < count( $dropins_mu_plugins['mu_plugins'] ) ) :
	?>
	<table class="wc_status_table widefat" cellspacing="0">
		<thead>
			<tr>
				<th colspan="3" data-export-label="Must Use Plugins (<?php echo count( $dropins_mu_plugins['mu_plugins'] ); ?>)"><h2><?php esc_html_e( 'Must Use Plugins', 'woocommerce' ); ?> (<?php echo count( $dropins_mu_plugins['mu_plugins'] ); ?>)</h2></th>
			</tr>
		</thead>
		<tbody>
			<?php
			foreach ( $dropins_mu_plugins['mu_plugins'] as $mu_plugin ) { // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
				$plugin_name = esc_html( $mu_plugin['name'] );
				if ( ! empty( $mu_plugin['url'] ) ) {
					$plugin_name = '<a href="' . esc_url( $mu_plugin['url'] ) . '" aria-label="' . esc_attr__( 'Visit plugin homepage', 'woocommerce' ) . '" target="_blank">' . $plugin_name . '</a>';
				}
				?>
				<tr>
					<td><?php echo wp_kses_post( $plugin_name ); ?></td>
					<td class="help">&nbsp;</td>
					<td>
					<?php
						/* translators: %s: plugin author */
						printf( esc_html__( 'by %s', 'woocommerce' ), esc_html( $mu_plugin['author_name'] ) );
						echo ' &ndash; ' . esc_html( $mu_plugin['version'] );
					?>
				</tr>
				<?php
			}
			?>
		</tbody>
	</table>
<?php endif; ?>
<table class="wc_status_table widefat" cellspacing="0">
	<thead>
		<tr>
			<th colspan="3" data-export-label="Settings"><h2><?php esc_html_e( 'Settings', 'woocommerce' ); ?></h2></th>
		</tr>
	</thead>
	<tbody>
		<tr>
			<td data-export-label="API Enabled"><?php esc_html_e( 'API enabled', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'Does your site have REST API enabled?', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td><?php echo $settings['api_enabled'] ? '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>' : '<mark class="no">&ndash;</mark>'; ?></td>
		</tr>
		<tr>
			<td data-export-label="Force SSL"><?php esc_html_e( 'Force SSL', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'Does your site force a SSL Certificate for transactions?', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td><?php echo $settings['force_ssl'] ? '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>' : '<mark class="no">&ndash;</mark>'; ?></td>
		</tr>
		<tr>
			<td data-export-label="Currency"><?php esc_html_e( 'Currency', 'woocommerce' ); ?></td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'What currency prices are listed at in the catalog and which currency gateways will take payments in.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td><?php echo esc_html( $settings['currency'] ); ?> (<?php echo esc_html( $settings['currency_symbol'] ); ?>)</td>
		</tr>
		<tr>
			<td data-export-label="Currency Position"><?php esc_html_e( 'Currency position', 'woocommerce' ); ?></td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'The position of the currency symbol.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td><?php echo esc_html( $settings['currency_position'] ); ?></td>
		</tr>
		<tr>
			<td data-export-label="Thousand Separator"><?php esc_html_e( 'Thousand separator', 'woocommerce' ); ?></td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'The thousand separator of displayed prices.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td><?php echo esc_html( $settings['thousand_separator'] ); ?></td>
		</tr>
		<tr>
			<td data-export-label="Decimal Separator"><?php esc_html_e( 'Decimal separator', 'woocommerce' ); ?></td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'The decimal separator of displayed prices.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td><?php echo esc_html( $settings['decimal_separator'] ); ?></td>
		</tr>
		<tr>
			<td data-export-label="Number of Decimals"><?php esc_html_e( 'Number of decimals', 'woocommerce' ); ?></td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'The number of decimal points shown in displayed prices.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td><?php echo esc_html( $settings['number_of_decimals'] ); ?></td>
		</tr>
		<tr>
			<td data-export-label="Taxonomies: Product Types"><?php esc_html_e( 'Taxonomies: Product types', 'woocommerce' ); ?></th>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'A list of taxonomy terms that can be used in regard to order/product statuses.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td>
				<?php
				$display_terms = array();
				foreach ( $settings['taxonomies'] as $slug => $name ) {
					$display_terms[] = strtolower( $name ) . ' (' . $slug . ')';
				}
				echo implode( ', ', array_map( 'esc_html', $display_terms ) );
				?>
			</td>
		</tr>
		<tr>
			<td data-export-label="Taxonomies: Product Visibility"><?php esc_html_e( 'Taxonomies: Product visibility', 'woocommerce' ); ?></th>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'A list of taxonomy terms used for product visibility.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td>
				<?php
				$display_terms = array();
				foreach ( $settings['product_visibility_terms'] as $slug => $name ) {
					$display_terms[] = strtolower( $name ) . ' (' . $slug . ')';
				}
				echo implode( ', ', array_map( 'esc_html', $display_terms ) );
				?>
			</td>
		</tr>
		<tr>
			<td data-export-label="Connected to WooCommerce.com"><?php esc_html_e( 'Connected to WooCommerce.com', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'Is your site connected to WooCommerce.com?', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td><?php echo 'yes' === $settings['woocommerce_com_connected'] ? '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>' : '<mark class="no">&ndash;</mark>'; ?></td>
		</tr>
	</tbody>
</table>
<table class="wc_status_table widefat" cellspacing="0">
	<thead>
		<tr>
			<th colspan="3" data-export-label="WC Pages"><h2><?php esc_html_e( 'WooCommerce pages', 'woocommerce' ); ?></h2></th>
		</tr>
	</thead>
	<tbody>
		<?php
		$alt = 1;
		foreach ( $wp_pages as $_page ) {
			$found_error = false;

			if ( $_page['page_id'] ) {
				/* Translators: %s: page name. */
				$page_name = '<a href="' . get_edit_post_link( $_page['page_id'] ) . '" aria-label="' . sprintf( esc_html__( 'Edit %s page', 'woocommerce' ), esc_html( $_page['page_name'] ) ) . '">' . esc_html( $_page['page_name'] ) . '</a>';
			} else {
				$page_name = esc_html( $_page['page_name'] );
			}

			echo '<tr><td data-export-label="' . esc_attr( $page_name ) . '">' . wp_kses_post( $page_name ) . ':</td>';
			/* Translators: %s: page name. */
			echo '<td class="help">' . wc_help_tip( sprintf( esc_html__( 'The URL of your %s page (along with the Page ID).', 'woocommerce' ), $page_name ) ) . '</td><td>';

			// Page ID check.
			if ( ! $_page['page_set'] ) {
				echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . esc_html__( 'Page not set', 'woocommerce' ) . '</mark>';
				$found_error = true;
			} elseif ( ! $_page['page_exists'] ) {
				echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . esc_html__( 'Page ID is set, but the page does not exist', 'woocommerce' ) . '</mark>';
				$found_error = true;
			} elseif ( ! $_page['page_visible'] ) {
				/* Translators: %s: docs link. */
				echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . wp_kses_post( sprintf( __( 'Page visibility should be <a href="%s" target="_blank">public</a>', 'woocommerce' ), 'https://wordpress.org/support/article/content-visibility/' ) ) . '</mark>';
				$found_error = true;
			} else {
				// Shortcode and block check.
				if ( $_page['shortcode_required'] || $_page['block_required'] ) {
					if ( ! $_page['shortcode_present'] && ! $_page['block_present'] ) {
						/* Translators: %1$s: shortcode text, %2$s: block slug. */
						echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . ( $_page['block_required'] ? sprintf( esc_html__( 'Page does not contain the %1$s shortcode or the %2$s block.', 'woocommerce' ), esc_html( $_page['shortcode'] ), esc_html( $_page['block'] ) ) : sprintf( esc_html__( 'Page does not contain the %s shortcode.', 'woocommerce' ), esc_html( $_page['shortcode'] ) ) ) . '</mark>'; /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */
						$found_error = true;
					}
				}
			}

			if ( ! $found_error ) {
				echo '<mark class="yes">#' . absint( $_page['page_id'] ) . ' - ' . esc_html( str_replace( home_url(), '', get_permalink( $_page['page_id'] ) ) ) . '</mark>';
			}

			echo '</td></tr>';
		}
		?>
	</tbody>
</table>
<table class="wc_status_table widefat" cellspacing="0">
	<thead>
		<tr>
			<th colspan="3" data-export-label="Theme"><h2><?php esc_html_e( 'Theme', 'woocommerce' ); ?></h2></th>
		</tr>
	</thead>
	<tbody>
		<tr>
			<td data-export-label="Name"><?php esc_html_e( 'Name', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'The name of the current active theme.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td><?php echo esc_html( $theme['name'] ); ?></td>
		</tr>
		<tr>
			<td data-export-label="Version"><?php esc_html_e( 'Version', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'The installed version of the current active theme.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td>
				<?php
				if ( version_compare( $theme['version'], $theme['version_latest'], '<' ) ) {
					/* translators: 1: current version. 2: latest version */
					echo esc_html( sprintf( __( '%1$s (update to version %2$s is available)', 'woocommerce' ), $theme['version'], $theme['version_latest'] ) );
				} else {
					echo esc_html( $theme['version'] );
				}
				?>
			</td>
		</tr>
		<tr>
			<td data-export-label="Author URL"><?php esc_html_e( 'Author URL', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'The theme developers URL.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td><?php echo esc_html( $theme['author_url'] ); ?></td>
		</tr>
		<tr>
			<td data-export-label="Child Theme"><?php esc_html_e( 'Child theme', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'Displays whether or not the current theme is a child theme.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td>
				<?php
				if ( $theme['is_child_theme'] ) {
					echo '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>';
				} else {
					/* Translators: %s docs link. */
					echo '<span class="dashicons dashicons-no-alt"></span> &ndash; ' . wp_kses_post( sprintf( __( 'If you are modifying WooCommerce on a parent theme that you did not build personally we recommend using a child theme. See: <a href="%s" target="_blank">How to create a child theme</a>', 'woocommerce' ), 'https://developer.wordpress.org/themes/advanced-topics/child-themes/' ) );
				}
				?>
				</td>
		</tr>
		<?php if ( $theme['is_child_theme'] ) : ?>
			<tr>
				<td data-export-label="Parent Theme Name"><?php esc_html_e( 'Parent theme name', 'woocommerce' ); ?>:</td>
				<td class="help"><?php echo wc_help_tip( esc_html__( 'The name of the parent theme.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
				<td><?php echo esc_html( $theme['parent_name'] ); ?></td>
			</tr>
			<tr>
				<td data-export-label="Parent Theme Version"><?php esc_html_e( 'Parent theme version', 'woocommerce' ); ?>:</td>
				<td class="help"><?php echo wc_help_tip( esc_html__( 'The installed version of the parent theme.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
				<td>
					<?php
					echo esc_html( $theme['parent_version'] );
					if ( version_compare( $theme['parent_version'], $theme['parent_version_latest'], '<' ) ) {
						/* translators: %s: parent theme latest version */
						echo ' &ndash; <strong style="color:red;">' . sprintf( esc_html__( '%s is available', 'woocommerce' ), esc_html( $theme['parent_version_latest'] ) ) . '</strong>';
					}
					?>
				</td>
			</tr>
			<tr>
				<td data-export-label="Parent Theme Author URL"><?php esc_html_e( 'Parent theme author URL', 'woocommerce' ); ?>:</td>
				<td class="help"><?php echo wc_help_tip( esc_html__( 'The parent theme developers URL.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
				<td><?php echo esc_html( $theme['parent_author_url'] ); ?></td>
			</tr>
		<?php endif ?>
		<tr>
			<td data-export-label="WooCommerce Support"><?php esc_html_e( 'WooCommerce support', 'woocommerce' ); ?>:</td>
			<td class="help"><?php echo wc_help_tip( esc_html__( 'Displays whether or not the current active theme declares WooCommerce support.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
			<td>
				<?php
				if ( ! $theme['has_woocommerce_support'] ) {
					echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . esc_html__( 'Not declared', 'woocommerce' ) . '</mark>';
				} else {
					echo '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>';
				}
				?>
			</td>
		</tr>
	</tbody>
</table>
<table class="wc_status_table widefat" cellspacing="0">
	<thead>
		<tr>
			<th colspan="3" data-export-label="Templates"><h2><?php esc_html_e( 'Templates', 'woocommerce' ); ?><?php echo wc_help_tip( esc_html__( 'This section shows any files that are overriding the default WooCommerce template pages.', 'woocommerce' ) ); ?></h2></th>
		</tr>
	</thead>
	<tbody>
		<?php if ( $theme['has_woocommerce_file'] ) : ?>
		<tr>
			<td data-export-label="Archive Template"><?php esc_html_e( 'Archive template', 'woocommerce' ); ?>:</td>
			<td class="help">&nbsp;</td>
			<td><?php esc_html_e( 'Your theme has a woocommerce.php file, you will not be able to override the woocommerce/archive-product.php custom template since woocommerce.php has priority over archive-product.php. This is intended to prevent display issues.', 'woocommerce' ); ?></td>
		</tr>
		<?php endif ?>
		<?php if ( ! empty( $theme['overrides'] ) ) : ?>
			<tr>
				<td data-export-label="Overrides"><?php esc_html_e( 'Overrides', 'woocommerce' ); ?></td>
				<td class="help">&nbsp;</td>
				<td>
					<?php
					$total_overrides = count( $theme['overrides'] );
					for ( $i = 0; $i < $total_overrides; $i++ ) {
						$override = $theme['overrides'][ $i ];
						if ( $override['core_version'] && ( empty( $override['version'] ) || version_compare( $override['version'], $override['core_version'], '<' ) ) ) {
							$current_version = $override['version'] ? $override['version'] : '-';
							printf(
								/* Translators: %1$s: Template name, %2$s: Template version, %3$s: Core version. */
								esc_html__( '%1$s version %2$s is out of date. The core version is %3$s', 'woocommerce' ),
								'<code>' . esc_html( $override['file'] ) . '</code>',
								'<strong style="color:red">' . esc_html( $current_version ) . '</strong>',
								esc_html( $override['core_version'] )
							);
						} else {
							echo esc_html( $override['file'] );
						}
						if ( ( count( $theme['overrides'] ) - 1 ) !== $i ) {
							echo ', ';
						}
						echo '<br />';
					}
					?>
				</td>
			</tr>
		<?php else : ?>
			<tr>
				<td data-export-label="Overrides"><?php esc_html_e( 'Overrides', 'woocommerce' ); ?>:</td>
				<td class="help">&nbsp;</td>
				<td>&ndash;</td>
			</tr>
		<?php endif; ?>

		<?php if ( true === $theme['has_outdated_templates'] ) : ?>
			<tr>
				<td data-export-label="Outdated Templates"><?php esc_html_e( 'Outdated templates', 'woocommerce' ); ?>:</td>
				<td class="help">&nbsp;</td>
				<td>
					<mark class="error">
						<span class="dashicons dashicons-warning"></span>
					</mark>
					<a href="https://docs.woocommerce.com/document/fix-outdated-templates-woocommerce/" target="_blank">
						<?php esc_html_e( 'Learn how to update', 'woocommerce' ); ?>
					</a>
				</td>
			</tr>
		<?php endif; ?>
	</tbody>
</table>

<?php do_action( 'woocommerce_system_status_report' ); ?>

<table class="wc_status_table widefat" cellspacing="0">
	<thead>
	<tr>
		<th colspan="3" data-export-label="Status report information"><h2><?php esc_html_e( 'Status report information', 'woocommerce' ); ?><?php echo wc_help_tip( esc_html__( 'This section shows information about this status report.', 'woocommerce' ) ); ?></h2></th>
	</tr>
	</thead>
	<tbody>
	<tr>
		<td data-export-label="Generated at"><?php esc_html_e( 'Generated at', 'woocommerce' ); ?>:</td>
		<td class="help">&nbsp;</td>
		<td><?php echo esc_html( current_time( 'Y-m-d H:i:s P' ) ); ?></td>

	</tr>
	</tbody>
</table>
admin/views/html-bulk-edit-product.php000064400000026274151336044770014027 0ustar00<?php
/**
 * Admin View: Bulk Edit Products
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

?>

<fieldset class="inline-edit-col-right">
	<div id="woocommerce-fields-bulk" class="inline-edit-col">

		<h4><?php _e( 'Product data', 'woocommerce' ); ?></h4>

		<?php do_action( 'woocommerce_product_bulk_edit_start' ); ?>

		<div class="inline-edit-group">
			<label class="alignleft">
				<span class="title"><?php _e( 'Price', 'woocommerce' ); ?></span>
				<span class="input-text-wrap">
					<select class="change_regular_price change_to" name="change_regular_price">
						<?php
						$options = array(
							''  => __( '— No change —', 'woocommerce' ),
							'1' => __( 'Change to:', 'woocommerce' ),
							'2' => __( 'Increase existing price by (fixed amount or %):', 'woocommerce' ),
							'3' => __( 'Decrease existing price by (fixed amount or %):', 'woocommerce' ),
						);
						foreach ( $options as $key => $value ) {
							echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
						}
						?>
					</select>
				</span>
			</label>
			<label class="change-input">
				<input type="text" name="_regular_price" class="text regular_price" placeholder="<?php printf( esc_attr__( 'Enter price (%s)', 'woocommerce' ), get_woocommerce_currency_symbol() ); ?>" value="" />
			</label>
		</div>

		<div class="inline-edit-group">
			<label class="alignleft">
				<span class="title"><?php _e( 'Sale', 'woocommerce' ); ?></span>
				<span class="input-text-wrap">
					<select class="change_sale_price change_to" name="change_sale_price">
						<?php
						$options = array(
							''  => __( '— No change —', 'woocommerce' ),
							'1' => __( 'Change to:', 'woocommerce' ),
							'2' => __( 'Increase existing sale price by (fixed amount or %):', 'woocommerce' ),
							'3' => __( 'Decrease existing sale price by (fixed amount or %):', 'woocommerce' ),
							'4' => __( 'Set to regular price decreased by (fixed amount or %):', 'woocommerce' ),
						);
						foreach ( $options as $key => $value ) {
							echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
						}
						?>
					</select>
				</span>
			</label>
			<label class="change-input">
				<input type="text" name="_sale_price" class="text sale_price" placeholder="<?php printf( esc_attr__( 'Enter sale price (%s)', 'woocommerce' ), get_woocommerce_currency_symbol() ); ?>" value="" />
			</label>
		</div>

		<?php if ( wc_tax_enabled() ) : ?>
			<label>
				<span class="title"><?php _e( 'Tax status', 'woocommerce' ); ?></span>
				<span class="input-text-wrap">
					<select class="tax_status" name="_tax_status">
						<?php
						$options = array(
							''         => __( '— No change —', 'woocommerce' ),
							'taxable'  => __( 'Taxable', 'woocommerce' ),
							'shipping' => __( 'Shipping only', 'woocommerce' ),
							'none'     => _x( 'None', 'Tax status', 'woocommerce' ),
						);
						foreach ( $options as $key => $value ) {
							echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
						}
						?>
					</select>
				</span>
			</label>

			<label>
				<span class="title"><?php _e( 'Tax class', 'woocommerce' ); ?></span>
				<span class="input-text-wrap">
					<select class="tax_class" name="_tax_class">
						<?php
						$options = array(
							''         => __( '— No change —', 'woocommerce' ),
							'standard' => __( 'Standard', 'woocommerce' ),
						);

						$tax_classes = WC_Tax::get_tax_classes();

						if ( ! empty( $tax_classes ) ) {
							foreach ( $tax_classes as $class ) {
								$options[ sanitize_title( $class ) ] = esc_html( $class );
							}
						}

						foreach ( $options as $key => $value ) {
							echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
						}
						?>
					</select>
				</span>
			</label>
		<?php endif; ?>

		<?php if ( wc_product_weight_enabled() ) : ?>
			<div class="inline-edit-group">
				<label class="alignleft">
					<span class="title"><?php _e( 'Weight', 'woocommerce' ); ?></span>
					<span class="input-text-wrap">
						<select class="change_weight change_to" name="change_weight">
							<?php
								$options = array(
									''  => __( '— No change —', 'woocommerce' ),
									'1' => __( 'Change to:', 'woocommerce' ),
								);
							foreach ( $options as $key => $value ) {
								echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
							}
							?>
						</select>
					</span>
				</label>
				<label class="change-input">
					<input type="text" name="_weight" class="text weight" placeholder="<?php printf( esc_attr__( '%1$s (%2$s)', 'woocommerce' ), wc_format_localized_decimal( 0 ), get_option( 'woocommerce_weight_unit' ) ); ?>" value="">
				</label>
			</div>
		<?php endif; ?>

		<?php if ( wc_product_dimensions_enabled() ) : ?>
			<div class="inline-edit-group dimensions">
				<label class="alignleft">
					<span class="title"><?php _e( 'L/W/H', 'woocommerce' ); ?></span>
					<span class="input-text-wrap">
						<select class="change_dimensions change_to" name="change_dimensions">
							<?php
							$options = array(
								''  => __( '— No change —', 'woocommerce' ),
								'1' => __( 'Change to:', 'woocommerce' ),
							);
							foreach ( $options as $key => $value ) {
								echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
							}
							?>
						</select>
					</span>
				</label>
				<label class="change-input">
					<input type="text" name="_length" class="text length" placeholder="<?php printf( esc_attr__( 'Length (%s)', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ); ?>" value="">
					<input type="text" name="_width" class="text width" placeholder="<?php printf( esc_attr__( 'Width (%s)', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ); ?>" value="">
					<input type="text" name="_height" class="text height" placeholder="<?php printf( esc_attr__( 'Height (%s)', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ); ?>" value="">
				</label>
			</div>
		<?php endif; ?>

		<label>
			<span class="title"><?php _e( 'Shipping class', 'woocommerce' ); ?></span>
			<span class="input-text-wrap">
				<select class="shipping_class" name="_shipping_class">
					<option value=""><?php _e( '— No change —', 'woocommerce' ); ?></option>
					<option value="_no_shipping_class"><?php _e( 'No shipping class', 'woocommerce' ); ?></option>
					<?php
					foreach ( $shipping_class as $key => $value ) {
						echo '<option value="' . esc_attr( $value->slug ) . '">' . esc_html( $value->name ) . '</option>';
					}
					?>
				</select>
			</span>
		</label>

		<label>
			<span class="title"><?php _e( 'Visibility', 'woocommerce' ); ?></span>
			<span class="input-text-wrap">
				<select class="visibility" name="_visibility">
					<?php
					$options = array(
						''        => __( '— No change —', 'woocommerce' ),
						'visible' => __( 'Catalog &amp; search', 'woocommerce' ),
						'catalog' => __( 'Catalog', 'woocommerce' ),
						'search'  => __( 'Search', 'woocommerce' ),
						'hidden'  => __( 'Hidden', 'woocommerce' ),
					);
					foreach ( $options as $key => $value ) {
						echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
					}
					?>
				</select>
			</span>
		</label>
		<label>
			<span class="title"><?php _e( 'Featured', 'woocommerce' ); ?></span>
			<span class="input-text-wrap">
				<select class="featured" name="_featured">
					<?php
					$options = array(
						''    => __( '— No change —', 'woocommerce' ),
						'yes' => __( 'Yes', 'woocommerce' ),
						'no'  => __( 'No', 'woocommerce' ),
					);
					foreach ( $options as $key => $value ) {
						echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
					}
					?>
				</select>
			</span>
		</label>

		<label>
			<span class="title"><?php _e( 'In stock?', 'woocommerce' ); ?></span>
			<span class="input-text-wrap">
				<select class="stock_status" name="_stock_status">
					<?php
					echo '<option value="">' . esc_html__( '— No Change —', 'woocommerce' ) . '</option>';

					foreach ( wc_get_product_stock_status_options() as $key => $value ) {
						echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
					}
					?>
				</select>
			</span>
		</label>
		<?php if ( 'yes' == get_option( 'woocommerce_manage_stock' ) ) : ?>

			<label>
				<span class="title"><?php _e( 'Manage stock?', 'woocommerce' ); ?></span>
				<span class="input-text-wrap">
					<select class="manage_stock" name="_manage_stock">
						<?php
						$options = array(
							''    => __( '— No change —', 'woocommerce' ),
							'yes' => __( 'Yes', 'woocommerce' ),
							'no'  => __( 'No', 'woocommerce' ),
						);
						foreach ( $options as $key => $value ) {
							echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
						}
						?>
					</select>
				</span>
			</label>

			<div class="inline-edit-group">
				<label class="alignleft stock_qty_field">
					<span class="title"><?php _e( 'Stock qty', 'woocommerce' ); ?></span>
					<span class="input-text-wrap">
						<select class="change_stock change_to" name="change_stock">
							<?php
							$options = array(
								''  => __( '— No change —', 'woocommerce' ),
								'1' => __( 'Change to:', 'woocommerce' ),
								'2' => __( 'Increase existing stock by:', 'woocommerce' ),
								'3' => __( 'Decrease existing stock by:', 'woocommerce' ),
							);
							foreach ( $options as $key => $value ) {
								echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
							}
							?>
						</select>
					</span>
				</label>
				<label class="change-input">
					<input type="text" name="_stock" class="text stock" placeholder="<?php esc_attr_e( 'Stock qty', 'woocommerce' ); ?>" step="any" value="">
				</label>
			</div>

			<label>
				<span class="title"><?php _e( 'Backorders?', 'woocommerce' ); ?></span>
				<span class="input-text-wrap">
					<select class="backorders" name="_backorders">
						<?php
						echo '<option value="">' . esc_html__( '— No Change —', 'woocommerce' ) . '</option>';

						foreach ( wc_get_product_backorder_options() as $key => $value ) {
							echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
						}
						?>
					</select>
				</span>
			</label>

		<?php endif; ?>

		<label>
			<span class="title"><?php esc_html_e( 'Sold individually?', 'woocommerce' ); ?></span>
			<span class="input-text-wrap">
				<select class="sold_individually" name="_sold_individually">
					<?php
					$options = array(
						''    => __( '— No change —', 'woocommerce' ),
						'yes' => __( 'Yes', 'woocommerce' ),
						'no'  => __( 'No', 'woocommerce' ),
					);
					foreach ( $options as $key => $value ) {
						echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
					}
					?>
				</select>
			</span>
		</label>

		<?php do_action( 'woocommerce_product_bulk_edit_end' ); ?>

		<input type="hidden" name="woocommerce_bulk_edit" value="1" />
		<input type="hidden" name="woocommerce_quick_edit_nonce" value="<?php echo wp_create_nonce( 'woocommerce_quick_edit_nonce' ); ?>" />
	</div>
</fieldset>
admin/views/html-admin-page-status-logs-db.php000064400000001456151336044770015334 0ustar00<?php
/**
 * Admin View: Page - Status Database Logs
 *
 * @package WooCommerce\Admin\Logs
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

?>
<form method="post" id="mainform" action="">
	<?php $log_table_list->search_box( __( 'Search logs', 'woocommerce' ), 'log' ); ?>
	<?php $log_table_list->display(); ?>

	<input type="hidden" name="page" value="wc-status" />
	<input type="hidden" name="tab" value="logs" />

	<?php submit_button( __( 'Flush all logs', 'woocommerce' ), 'delete', 'flush-logs' ); ?>
	<?php wp_nonce_field( 'woocommerce-status-logs' ); ?>
</form>
<?php
wc_enqueue_js(
	"jQuery( '#flush-logs' ).on( 'click', function() {
		if ( window.confirm('" . esc_js( __( 'Are you sure you want to clear all logs from the database?', 'woocommerce' ) ) . "') ) {
			return true;
		}
		return false;
	});"
);
admin/views/html-notice-redirect-only-download.php000064400000002315151336044770016323 0ustar00<?php
/**
 * Admin View: Notice - Redirect only download method is selected.
 *
 * @package WooCommerce\Admin\Notices
 */

defined( 'ABSPATH' ) || exit;

?>
<div class="updated woocommerce-message">
	<a class="woocommerce-message-close notice-dismiss" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-hide-notice', 'redirect_download_method' ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ) ); ?>"><?php esc_html_e( 'Dismiss', 'woocommerce' ); ?></a>
	<p>
		<?php
		echo wp_kses_post(
			sprintf(
				/* translators: %s: Link to settings page. */
				__( 'Your store is configured to serve digital products using "Redirect only" method. This method is deprecated, <a href="%s">please switch to a different method instead.</a><br><em>If you use a remote server for downloadable files (such as Google Drive, Dropbox, Amazon S3), you may optionally wish to "allow using redirects as a last resort". Enabling that and/or selecting any of the other options will make this notice go away.</em>', 'woocommerce' ),
				add_query_arg(
					array(
						'page'    => 'wc-settings',
						'tab'     => 'products',
						'section' => 'downloadable',
					),
					admin_url( 'admin.php' )
				)
			)
		);
		?>
	</p>
</div>
admin/views/html-notice-legacy-shipping.php000064400000003232151336044770015020 0ustar00<?php
/**
 * Admin View: Notice - Legacy Shipping.
 *
 * @package WooCommerce\Admin\Notices
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>
<div id="message" class="updated woocommerce-message">
	<a class="woocommerce-message-close notice-dismiss" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-hide-notice', 'legacy_shipping' ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ) ); ?>">
		<?php esc_html_e( 'Dismiss', 'woocommerce' ); ?>
	</a>

	<p class="main">
		<strong><?php esc_html_e( 'New:', 'woocommerce' ); ?> <?php esc_html_e( 'Shipping zones', 'woocommerce' ); ?></strong> &#8211; <?php esc_html_e( 'a group of regions that can be assigned different shipping methods and rates.', 'woocommerce' ); ?>
	</p>
	<p>
		<?php esc_html_e( 'Legacy shipping methods (flat rate, international flat rate, local pickup and delivery, and free shipping) are deprecated but will continue to work as normal for now. <b><em>They will be removed in future versions of WooCommerce</em></b>. We recommend disabling these and setting up new rates within shipping zones as soon as possible.', 'woocommerce' ); ?>
	</p>

	<p class="submit">
		<?php if ( empty( $_GET['page'] ) || empty( $_GET['tab'] ) || 'wc-settings' !== $_GET['page'] || 'shipping' !== $_GET['tab'] ) : ?>
			<a class="button-primary" href="<?php echo esc_url( admin_url( 'admin.php?page=wc-settings&tab=shipping' ) ); ?>">
				<?php esc_html_e( 'Setup shipping zones', 'woocommerce' ); ?>
			</a>
		<?php endif; ?>
		<a class="button-secondary" href="https://docs.woocommerce.com/document/setting-up-shipping-zones/">
			<?php esc_html_e( 'Learn more about shipping zones', 'woocommerce' ); ?>
		</a>
	</p>
</div>
admin/views/html-notice-template-check.php000064400000003253151336044770014626 0ustar00<?php
/**
 * Admin View: Notice - Template Check
 *
 * @package WooCommerce\Views
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

$theme = wp_get_theme();
?>
<div id="message" class="updated woocommerce-message">
	<a class="woocommerce-message-close notice-dismiss" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-hide-notice', 'template_files' ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ) ); ?>"><?php esc_html_e( 'Dismiss', 'woocommerce' ); ?></a>

	<p>
		<?php /* translators: %s: theme name */ ?>
		<?php printf( __( '<strong>Your theme (%s) contains outdated copies of some WooCommerce template files.</strong> These files may need updating to ensure they are compatible with the current version of WooCommerce. Suggestions to fix this:', 'woocommerce' ), esc_html( $theme['Name'] ) ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?>
		<ol>
			<li><?php esc_html_e( 'Update your theme to the latest version. If no update is available contact your theme author asking about compatibility with the current WooCommerce version.', 'woocommerce' ); ?></li>
			<li><?php esc_html_e( 'If you copied over a template file to change something, then you will need to copy the new version of the template and apply your changes again.', 'woocommerce' ); ?></li>
		</ol>
	</p>
	<p class="submit">
		<a class="button-primary" href="https://docs.woocommerce.com/document/template-structure/" target="_blank"><?php esc_html_e( 'Learn more about templates', 'woocommerce' ); ?></a>
		<a class="button-primary" href="<?php echo esc_url( admin_url( 'admin.php?page=wc-status' ) ); ?>" target="_blank"><?php esc_html_e( 'View affected templates', 'woocommerce' ); ?></a>
	</p>
</div>
admin/views/html-notice-update.php000064400000002560151336044770013222 0ustar00<?php
/**
 * Admin View: Notice - Update
 *
 * @package WooCommerce\Admin
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

$update_url = wp_nonce_url(
	add_query_arg( 'do_update_woocommerce', 'true', admin_url( 'admin.php?page=wc-settings' ) ),
	'wc_db_update',
	'wc_db_update_nonce'
);

?>
<div id="message" class="updated woocommerce-message wc-connect">
	<p>
		<strong><?php esc_html_e( 'WooCommerce database update required', 'woocommerce' ); ?></strong>
	</p>
	<p>
		<?php
			esc_html_e( 'WooCommerce has been updated! To keep things running smoothly, we have to update your database to the newest version.', 'woocommerce' );

			/* translators: 1: Link to docs 2: Close link. */
			printf( ' ' . esc_html__( 'The database update process runs in the background and may take a little while, so please be patient. Advanced users can alternatively update via %1$sWP CLI%2$s.', 'woocommerce' ), '<a href="https://github.com/woocommerce/woocommerce/wiki/Upgrading-the-database-using-WP-CLI">', '</a>' );
		?>
	</p>
	<p class="submit">
		<a href="<?php echo esc_url( $update_url ); ?>" class="wc-update-now button-primary">
			<?php esc_html_e( 'Update WooCommerce Database', 'woocommerce' ); ?>
		</a>
		<a href="https://docs.woocommerce.com/document/how-to-update-woocommerce/" class="button-secondary">
			<?php esc_html_e( 'Learn more about updates', 'woocommerce' ); ?>
		</a>
	</p>
</div>
admin/views/html-notice-custom.php000064400000000704151336044770013250 0ustar00<?php
/**
 * Admin View: Custom Notices
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

?>
<div id="message" class="updated woocommerce-message">
	<a class="woocommerce-message-close notice-dismiss" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-hide-notice', $notice ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ) ); ?>"><?php _e( 'Dismiss', 'woocommerce' ); ?></a>
	<?php echo wp_kses_post( wpautop( $notice_html ) ); ?>
</div>
admin/views/html-quick-edit-product.php000064400000017154151336044770014203 0ustar00<?php
/**
 * Admin View: Quick Edit Product
 *
 * @package WooCommerce\Admin\Notices
 */

defined( 'ABSPATH' ) || exit;
?>

<fieldset class="inline-edit-col-left">
	<div id="woocommerce-fields" class="inline-edit-col">

		<h4><?php esc_html_e( 'Product data', 'woocommerce' ); ?></h4>

		<?php do_action( 'woocommerce_product_quick_edit_start' ); ?>

		<?php if ( wc_product_sku_enabled() ) : ?>

			<label>
				<span class="title"><?php esc_html_e( 'SKU', 'woocommerce' ); ?></span>
				<span class="input-text-wrap">
					<input type="text" name="_sku" class="text sku" value="">
				</span>
			</label>
			<br class="clear" />

		<?php endif; ?>

		<div class="price_fields">
			<label>
				<span class="title"><?php esc_html_e( 'Price', 'woocommerce' ); ?></span>
				<span class="input-text-wrap">
					<input type="text" name="_regular_price" class="text wc_input_price regular_price" placeholder="<?php esc_attr_e( 'Regular price', 'woocommerce' ); ?>" value="">
				</span>
			</label>
			<br class="clear" />
			<label>
				<span class="title"><?php esc_html_e( 'Sale', 'woocommerce' ); ?></span>
				<span class="input-text-wrap">
					<input type="text" name="_sale_price" class="text wc_input_price sale_price" placeholder="<?php esc_attr_e( 'Sale price', 'woocommerce' ); ?>" value="">
				</span>
			</label>
			<br class="clear" />
		</div>

		<?php if ( wc_tax_enabled() ) : ?>
			<label class="alignleft">
				<span class="title"><?php esc_html_e( 'Tax status', 'woocommerce' ); ?></span>
				<span class="input-text-wrap">
					<select class="tax_status" name="_tax_status">
						<?php
						$options = array(
							'taxable'  => __( 'Taxable', 'woocommerce' ),
							'shipping' => __( 'Shipping only', 'woocommerce' ),
							'none'     => _x( 'None', 'Tax status', 'woocommerce' ),
						);
						foreach ( $options as $key => $value ) {
							echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
						}
						?>
					</select>
				</span>
			</label>
			<br class="clear" />
			<label class="alignleft">
				<span class="title"><?php esc_html_e( 'Tax class', 'woocommerce' ); ?></span>
				<span class="input-text-wrap">
					<select class="tax_class" name="_tax_class">
						<?php
						$options = array(
							'' => __( 'Standard', 'woocommerce' ),
						);

						$tax_classes = WC_Tax::get_tax_classes();

						if ( ! empty( $tax_classes ) ) {
							foreach ( $tax_classes as $class ) {
								$options[ sanitize_title( $class ) ] = esc_html( $class );
							}
						}

						foreach ( $options as $key => $value ) {
							echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
						}
						?>
					</select>
				</span>
			</label>
			<br class="clear" />
		<?php endif; ?>

		<?php if ( wc_product_weight_enabled() || wc_product_dimensions_enabled() ) : ?>
		<div class="dimension_fields">

			<?php if ( wc_product_weight_enabled() ) : ?>
				<label>
					<span class="title"><?php esc_html_e( 'Weight', 'woocommerce' ); ?></span>
					<span class="input-text-wrap">
						<input type="text" name="_weight" class="text weight" placeholder="<?php echo esc_attr( wc_format_localized_decimal( 0 ) ); ?>" value="">
					</span>
				</label>
				<br class="clear" />
			<?php endif; ?>

			<?php if ( wc_product_dimensions_enabled() ) : ?>
				<div class="inline-edit-group dimensions">
					<div>
						<span class="title"><?php esc_html_e( 'L/W/H', 'woocommerce' ); ?></span>
						<span class="input-text-wrap">
							<input type="text" name="_length" class="text wc_input_decimal length" placeholder="<?php esc_attr_e( 'Length', 'woocommerce' ); ?>" value="">
							<input type="text" name="_width" class="text wc_input_decimal width" placeholder="<?php esc_attr_e( 'Width', 'woocommerce' ); ?>" value="">
							<input type="text" name="_height" class="text wc_input_decimal height" placeholder="<?php esc_attr_e( 'Height', 'woocommerce' ); ?>" value="">
						</span>
					</div>
				</div>
			<?php endif; ?>

		</div>
		<?php endif; ?>

		<div class="inline-edit-group">
			<span class="title"><?php esc_html_e( 'Shipping class', 'woocommerce' ); ?></span>
			<span class="input-text-wrap">
				<select class="shipping_class" name="_shipping_class">
					<option value="_no_shipping_class"><?php esc_html_e( 'No shipping class', 'woocommerce' ); ?></option>
					<?php
					foreach ( $shipping_class as $key => $value ) {
						echo '<option value="' . esc_attr( $value->slug ) . '">' . esc_html( $value->name ) . '</option>';
					}
					?>
				</select>
			</span>
		</div>

		<div class="inline-edit-group">
			<label class="alignleft">
				<span class="title"><?php esc_html_e( 'Visibility', 'woocommerce' ); ?></span>
				<span class="input-text-wrap">
					<select class="visibility" name="_visibility">
						<?php
						$options = apply_filters(
							'woocommerce_product_visibility_options',
							array(
								'visible' => __( 'Catalog &amp; search', 'woocommerce' ),
								'catalog' => __( 'Catalog', 'woocommerce' ),
								'search'  => __( 'Search', 'woocommerce' ),
								'hidden'  => __( 'Hidden', 'woocommerce' ),
							)
						);
						foreach ( $options as $key => $value ) {
							echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
						}
						?>
					</select>
				</span>
			</label>
			<label class="alignleft featured">
				<input type="checkbox" name="_featured" value="1">
				<span class="checkbox-title"><?php esc_html_e( 'Featured', 'woocommerce' ); ?></span>
			</label>
		</div>

		<?php if ( get_option( 'woocommerce_manage_stock' ) === 'yes' ) : ?>
			<div class="inline-edit-group manage_stock_field">
				<label class="manage_stock">
					<input type="checkbox" name="_manage_stock" value="1">
					<span class="checkbox-title"><?php esc_html_e( 'Manage stock?', 'woocommerce' ); ?></span>
				</label>
			</div>
		<?php endif; ?>

		<label class="stock_status_field">
			<span class="title"><?php esc_html_e( 'In stock?', 'woocommerce' ); ?></span>
			<span class="input-text-wrap">
				<select class="stock_status" name="_stock_status">
					<?php
					echo '<option value="" id="stock_status_no_change">' . esc_html__( '— No Change —', 'woocommerce' ) . '</option>';
					foreach ( wc_get_product_stock_status_options() as $key => $value ) {
						echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
					}
					?>
				</select>
				<div class="wc-quick-edit-warning" style="display:none">
					<?php echo esc_html__( 'This will change the stock status of all variations.', 'woocommerce' ); ?></p>
				</div>
			</span>
		</label>

		<div class="stock_fields">
			<?php if ( get_option( 'woocommerce_manage_stock' ) === 'yes' ) : ?>
				<label class="stock_qty_field">
					<span class="title"><?php esc_html_e( 'Stock qty', 'woocommerce' ); ?></span>
					<span class="input-text-wrap">
						<input type="number" name="_stock" class="text stock" step="any" value="">
					</span>
				</label>
			<?php endif; ?>
		</div>

		<label class="alignleft backorder_field">
			<span class="title"><?php esc_html_e( 'Backorders?', 'woocommerce' ); ?></span>
			<span class="input-text-wrap">
				<select class="backorders" name="_backorders">
					<?php
					foreach ( wc_get_product_backorder_options() as $key => $value ) {
						echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
					}
					?>
				</select>
			</span>
		</label>

		<?php do_action( 'woocommerce_product_quick_edit_end' ); ?>

		<input type="hidden" name="woocommerce_quick_edit" value="1" />
		<input type="hidden" name="woocommerce_quick_edit_nonce" value="<?php echo esc_attr( wp_create_nonce( 'woocommerce_quick_edit_nonce' ) ); ?>" />
	</div>
</fieldset>
admin/views/html-notice-install.php000064400000001334151336044770013404 0ustar00<?php
/**
 * Admin View: Notice - Install
 *
 * @deprecated 4.6.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

?>
<div id="message" class="updated woocommerce-message wc-connect">
	<p><?php _e( '<strong>Welcome to WooCommerce</strong> &#8211; You&lsquo;re almost ready to start selling :)', 'woocommerce' ); ?></p>
	<p class="submit"><a href="<?php echo esc_url( admin_url( 'admin.php?page=wc-setup' ) ); ?>" class="button-primary"><?php _e( 'Run the Setup Wizard', 'woocommerce' ); ?></a> <a class="button-secondary skip" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-hide-notice', 'install' ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ) ); ?>"><?php _e( 'Skip setup', 'woocommerce' ); ?></a></p>
</div>
admin/views/html-notice-uploads-directory-is-unprotected.php000064400000002034151336044770020350 0ustar00<?php
/**
 * Admin View: Notice - Uploads directory is unprotected.
 *
 * @package WooCommerce\Admin\Notices
 * @since   4.2.0
 */

defined( 'ABSPATH' ) || exit;

$uploads = wp_get_upload_dir();

?>
<div id="message" class="error woocommerce-message">
	<a class="woocommerce-message-close notice-dismiss" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-hide-notice', 'uploads_directory_is_public' ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ) ); ?>"><?php esc_html_e( 'Dismiss', 'woocommerce' ); ?></a>

	<p>
	<?php
		echo wp_kses_post(
			sprintf(
				/* translators: 1: uploads directory URL 2: documentation URL */
				__( 'Your store\'s uploads directory is <a href="%1$s">browsable via the web</a>. We strongly recommend <a href="%2$s">configuring your web server to prevent directory indexing</a>.', 'woocommerce' ),
				esc_url( $uploads['baseurl'] . '/woocommerce_uploads' ),
				'https://docs.woocommerce.com/document/digital-downloadable-product-handling/#protecting-your-uploads-directory'
			)
		);
		?>
	</p>
</div>
admin/views/html-email-template-preview.php000064400000002034151336044770015034 0ustar00<?php
/**
 * Admin View: Email Template Preview
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

?>

<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed aliquet diam a facilisis eleifend. Cras ac justo felis. Mauris faucibus, orci eu blandit fermentum, lorem nibh sollicitudin mi, sit amet interdum metus urna ut lacus.</p>

<p><a class="link" href="#">Sed sit amet sapien odio</a></p>

<p>Phasellus quis varius augue. Fusce eu euismod leo, a accumsan tellus. Quisque vitae dolor eu justo cursus egestas. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed sit amet sapien odio. Sed pellentesque arcu mi, quis malesuada lectus lacinia et. Cras a tempor leo.</p>

<h2>Lorem ipsum dolor</h2>

<p>Fusce eu euismod leo, a accumsan tellus. Quisque vitae dolor eu justo cursus egestas. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed sit amet sapien odio. Sed pellentesque arcu mi, quis malesuada lectus lacinia et. Cras a tempor leo.</p>
admin/views/html-notice-wp-php-minimum-requirements.php000064400000001652151336044770017346 0ustar00<?php
/**
 * Admin View: Notice - PHP & WP minimum requirements.
 *
 * @package WooCommerce\Admin\Notices
 */

defined( 'ABSPATH' ) || exit;
?>
<div id="message" class="updated woocommerce-message">
	<a class="woocommerce-message-close notice-dismiss" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-hide-notice', WC_PHP_MIN_REQUIREMENTS_NOTICE ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ) ); ?>"><?php esc_html_e( 'Dismiss', 'woocommerce' ); ?></a>

	<p>
		<?php
		echo wp_kses_post(
			sprintf(
				$msg . '<p><a href="%s" class="button button-primary">' . __( 'Learn how to upgrade', 'woocommerce' ) . '</a></p>',
				add_query_arg(
					array(
						'utm_source'   => 'wpphpupdatebanner',
						'utm_medium'   => 'product',
						'utm_campaign' => 'woocommerceplugin',
						'utm_content'  => 'docs',
					),
					'https://docs.woocommerce.com/document/update-php-wordpress/'
				)
			)
		);
		?>
	</p>
</div>
admin/views/html-admin-page-reports.php000064400000004622151336044770014160 0ustar00<?php
/**
 * Admin View: Page - Reports
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

?>
<div class="wrap woocommerce">
	<?php if ( WC()->is_wc_admin_active() ) { ?>
	<div id="message" class="error inline" style="margin-top:30px">
		<p>
			<strong>
			<?php
			/* translators: 1: Link URL */
			echo wp_kses_post( sprintf( __( 'With the release of WooCommerce 4.0, these reports are being replaced. There is a new and better Analytics section available for users running WordPress 5.3+. Head on over to the <a href="%1$s">WooCommerce Analytics</a> or learn more about the new experience in the <a href="https://docs.woocommerce.com/document/woocommerce-analytics/" target="_blank">WooCommerce Analytics documentation</a>.', 'woocommerce' ), esc_url( wc_admin_url( '&path=/analytics/overview' ) ) ) );
			?>
			</strong>
		</p>
	</div>
	<?php } ?>
	<nav class="nav-tab-wrapper woo-nav-tab-wrapper">
		<?php
		foreach ( $reports as $key => $report_group ) {
			echo '<a href="' . admin_url( 'admin.php?page=wc-reports&tab=' . urlencode( $key ) ) . '" class="nav-tab ';
			if ( $current_tab == $key ) {
				echo 'nav-tab-active';
			}
			echo '">' . esc_html( $report_group['title'] ) . '</a>';
		}

		do_action( 'wc_reports_tabs' );
		?>
	</nav>
	<?php
	if ( count( $reports[ $current_tab ]['reports'] ) > 1 ) {
		?>
		<ul class="subsubsub">
			<li>
			<?php

			$links = array();

			foreach ( $reports[ $current_tab ]['reports'] as $key => $report ) {
				$link = '<a href="admin.php?page=wc-reports&tab=' . urlencode( $current_tab ) . '&amp;report=' . urlencode( $key ) . '" class="';

				if ( $key == $current_report ) {
					$link .= 'current';
				}

				$link .= '">' . $report['title'] . '</a>';

				$links[] = $link;
			}

			echo implode( ' | </li><li>', $links );

			?>
			</li>
		</ul>
		<br class="clear" />
		<?php
	}

	if ( isset( $reports[ $current_tab ]['reports'][ $current_report ] ) ) {
		$report = $reports[ $current_tab ]['reports'][ $current_report ];

		if ( ! isset( $report['hide_title'] ) || true != $report['hide_title'] ) {
			echo '<h1>' . esc_html( $report['title'] ) . '</h1>';
		} else {
			echo '<h1 class="screen-reader-text">' . esc_html( $report['title'] ) . '</h1>';
		}

		if ( $report['description'] ) {
			echo '<p>' . $report['description'] . '</p>';
		}

		if ( $report['callback'] && ( is_callable( $report['callback'] ) ) ) {
			call_user_func( $report['callback'], $current_report );
		}
	}
	?>
</div>
admin/views/html-notice-maxmind-license-key.php000064400000002232151336044770015577 0ustar00<?php
/**
 * Admin View: Notice - Missing MaxMind license key
 *
 * @package WooCommerce\Admin
 */

defined( 'ABSPATH' ) || exit;

?>

<div id="message" class="updated woocommerce-message">
	<a class="woocommerce-message-close notice-dismiss" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-hide-notice', 'maxmind_license_key' ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ) ); ?>"><?php esc_html_e( 'Dismiss', 'woocommerce' ); ?></a>

	<p>
		<strong><?php esc_html_e( 'Geolocation has not been configured.', 'woocommerce' ); ?></strong>
	</p>

	<p>
		<?php
		echo wp_kses_post(
			sprintf(
				/* translators: %1%s: integration page %2$s: general settings page */
				__( 'You must enter a valid license key on the <a href="%1$s">MaxMind integration settings page</a> in order to use the geolocation service. If you do not need geolocation for shipping or taxes, you should change the default customer location on the <a href="%2$s">general settings page</a>.', 'woocommerce' ),
				admin_url( 'admin.php?page=wc-settings&tab=integration&section=maxmind_geolocation' ),
				admin_url( 'admin.php?page=wc-settings&tab=general' )
			)
		);
		?>
	</p>
</div>
admin/views/html-admin-settings.php000064400000003651151336044770013411 0ustar00<?php
/**
 * Admin View: Settings
 *
 * @package WooCommerce
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

$tab_exists        = isset( $tabs[ $current_tab ] ) || has_action( 'woocommerce_sections_' . $current_tab ) || has_action( 'woocommerce_settings_' . $current_tab ) || has_action( 'woocommerce_settings_tabs_' . $current_tab );
$current_tab_label = isset( $tabs[ $current_tab ] ) ? $tabs[ $current_tab ] : '';

if ( ! $tab_exists ) {
	wp_safe_redirect( admin_url( 'admin.php?page=wc-settings' ) );
	exit;
}
?>
<div class="wrap woocommerce">
	<?php do_action( 'woocommerce_before_settings_' . $current_tab ); ?>
	<form method="<?php echo esc_attr( apply_filters( 'woocommerce_settings_form_method_tab_' . $current_tab, 'post' ) ); ?>" id="mainform" action="" enctype="multipart/form-data">
		<nav class="nav-tab-wrapper woo-nav-tab-wrapper">
			<?php

			foreach ( $tabs as $slug => $label ) {
				echo '<a href="' . esc_html( admin_url( 'admin.php?page=wc-settings&tab=' . esc_attr( $slug ) ) ) . '" class="nav-tab ' . ( $current_tab === $slug ? 'nav-tab-active' : '' ) . '">' . esc_html( $label ) . '</a>';
			}

			do_action( 'woocommerce_settings_tabs' );

			?>
		</nav>
		<h1 class="screen-reader-text"><?php echo esc_html( $current_tab_label ); ?></h1>
		<?php
			do_action( 'woocommerce_sections_' . $current_tab );

			self::show_messages();

			do_action( 'woocommerce_settings_' . $current_tab );
			do_action( 'woocommerce_settings_tabs_' . $current_tab ); // @deprecated 3.4.0 hook.
		?>
		<p class="submit">
			<?php if ( empty( $GLOBALS['hide_save_button'] ) ) : ?>
				<button name="save" class="button-primary woocommerce-save-button" type="submit" value="<?php esc_attr_e( 'Save changes', 'woocommerce' ); ?>"><?php esc_html_e( 'Save changes', 'woocommerce' ); ?></button>
			<?php endif; ?>
			<?php wp_nonce_field( 'woocommerce-settings' ); ?>
		</p>
	</form>
	<?php do_action( 'woocommerce_after_settings_' . $current_tab ); ?>
</div>
admin/views/html-admin-page-status.php000064400000002377151336044770014012 0ustar00<?php
/**
 * Admin View: Page - Status
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

$current_tab = ! empty( $_REQUEST['tab'] ) ? sanitize_title( $_REQUEST['tab'] ) : 'status';
$tabs        = array(
	'status' => __( 'System status', 'woocommerce' ),
	'tools'  => __( 'Tools', 'woocommerce' ),
	'logs'   => __( 'Logs', 'woocommerce' ),
);
$tabs        = apply_filters( 'woocommerce_admin_status_tabs', $tabs );
?>
<div class="wrap woocommerce">
	<nav class="nav-tab-wrapper woo-nav-tab-wrapper">
		<?php
		foreach ( $tabs as $name => $label ) {
			echo '<a href="' . admin_url( 'admin.php?page=wc-status&tab=' . $name ) . '" class="nav-tab ';
			if ( $current_tab == $name ) {
				echo 'nav-tab-active';
			}
			echo '">' . $label . '</a>';
		}
		?>
	</nav>
	<h1 class="screen-reader-text"><?php echo esc_html( $tabs[ $current_tab ] ); ?></h1>
	<?php
	switch ( $current_tab ) {
		case 'tools':
			WC_Admin_Status::status_tools();
			break;
		case 'logs':
			WC_Admin_Status::status_logs();
			break;
		default:
			if ( array_key_exists( $current_tab, $tabs ) && has_action( 'woocommerce_admin_status_content_' . $current_tab ) ) {
				do_action( 'woocommerce_admin_status_content_' . $current_tab );
			} else {
				WC_Admin_Status::status_report();
			}
			break;
	}
	?>
</div>
admin/views/html-report-by-date.php000064400000010143151336044770013313 0ustar00<?php
/**
 * Admin View: Report by Date (with date filters)
 *
 * @package WooCommerce\Admin\Reporting
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}
?>
<div id="poststuff" class="woocommerce-reports-wide">
	<div class="postbox">

	<?php if ( 'custom' === $current_range && isset( $_GET['start_date'], $_GET['end_date'] ) ) : ?>
		<h3 class="screen-reader-text">
			<?php
			/* translators: 1: start date 2: end date */
			printf(
				esc_html__( 'From %1$s to %2$s', 'woocommerce' ),
				esc_html( wc_clean( wp_unslash( $_GET['start_date'] ) ) ),
				esc_html( wc_clean( wp_unslash( $_GET['end_date'] ) ) )
			);
			?>
		</h3>
	<?php else : ?>
		<h3 class="screen-reader-text"><?php echo esc_html( $ranges[ $current_range ] ); ?></h3>
	<?php endif; ?>

		<div class="stats_range">
			<?php $this->get_export_button(); ?>
			<ul>
				<?php
				foreach ( $ranges as $range => $name ) {
					echo '<li class="' . ( $current_range == $range ? 'active' : '' ) . '"><a href="' . esc_url( remove_query_arg( array( 'start_date', 'end_date' ), add_query_arg( 'range', $range ) ) ) . '">' . esc_html( $name ) . '</a></li>';
				}
				?>
				<li class="custom <?php echo ( 'custom' === $current_range ) ? 'active' : ''; ?>">
					<?php esc_html_e( 'Custom:', 'woocommerce' ); ?>
					<form method="GET">
						<div>
							<?php
							// Maintain query string.
							foreach ( $_GET as $key => $value ) {
								if ( is_array( $value ) ) {
									foreach ( $value as $v ) {
										echo '<input type="hidden" name="' . esc_attr( sanitize_text_field( $key ) ) . '[]" value="' . esc_attr( sanitize_text_field( $v ) ) . '" />';
									}
								} else {
									echo '<input type="hidden" name="' . esc_attr( sanitize_text_field( $key ) ) . '" value="' . esc_attr( sanitize_text_field( $value ) ) . '" />';
								}
							}
							?>
							<input type="hidden" name="range" value="custom" />
							<input type="text" size="11" placeholder="yyyy-mm-dd" value="<?php echo ( ! empty( $_GET['start_date'] ) ) ? esc_attr( wp_unslash( $_GET['start_date'] ) ) : ''; ?>" name="start_date" class="range_datepicker from" autocomplete="off" /><?php //@codingStandardsIgnoreLine ?>
							<span>&ndash;</span>
							<input type="text" size="11" placeholder="yyyy-mm-dd" value="<?php echo ( ! empty( $_GET['end_date'] ) ) ? esc_attr( wp_unslash( $_GET['end_date'] ) ) : ''; ?>" name="end_date" class="range_datepicker to" autocomplete="off" /><?php //@codingStandardsIgnoreLine ?>
							<button type="submit" class="button" value="<?php esc_attr_e( 'Go', 'woocommerce' ); ?>"><?php esc_html_e( 'Go', 'woocommerce' ); ?></button>
							<?php wp_nonce_field( 'custom_range', 'wc_reports_nonce', false ); ?>
						</div>
					</form>
				</li>
			</ul>
		</div>
		<?php if ( empty( $hide_sidebar ) ) : ?>
			<div class="inside chart-with-sidebar">
				<div class="chart-sidebar">
					<?php if ( $legends = $this->get_chart_legend() ) : ?>
						<ul class="chart-legend">
							<?php foreach ( $legends as $legend ) : ?>
								<?php // @codingStandardsIgnoreStart ?>
								<li style="border-color: <?php echo $legend['color']; ?>" <?php if ( isset( $legend['highlight_series'] ) ) echo 'class="highlight_series ' . ( isset( $legend['placeholder'] ) ? 'tips' : '' ) . '" data-series="' . esc_attr( $legend['highlight_series'] ) . '"'; ?> data-tip="<?php echo isset( $legend['placeholder'] ) ? $legend['placeholder'] : ''; ?>">
									<?php echo $legend['title']; ?>
								</li>
								<?php // @codingStandardsIgnoreEnd ?>
							<?php endforeach; ?>
						</ul>
					<?php endif; ?>
					<ul class="chart-widgets">
						<?php foreach ( $this->get_chart_widgets() as $widget ) : ?>
							<li class="chart-widget">
								<?php if ( $widget['title'] ) : ?>
									<h4><?php echo esc_html( $widget['title'] ); ?></h4>
								<?php endif; ?>
								<?php call_user_func( $widget['callback'] ); ?>
							</li>
						<?php endforeach; ?>
					</ul>
				</div>
				<div class="main">
					<?php $this->get_main_chart(); ?>
				</div>
			</div>
		<?php else : ?>
			<div class="inside">
				<?php $this->get_main_chart(); ?>
			</div>
		<?php endif; ?>
	</div>
</div>
admin/views/html-admin-page-status-tools.php000064400000004102151336044770015134 0ustar00<?php
/**
 * Admin View: Page - Status Tools
 *
 * @package WooCommerce
 */

use Automattic\WooCommerce\Utilities\ArrayUtil;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

foreach ( $tools as $action_name => $tool ) {
	?>
	<form id="<?php echo esc_attr( 'form_' . $action_name ); ?>" method="GET" action="<?php echo esc_attr( esc_url( admin_url( 'admin.php?foo=bar' ) ) ); ?>">
		<?php wp_nonce_field( 'debug_action', '_wpnonce', false ); ?>
		<input type="hidden" name="page" value="wc-status"/>
		<input type="hidden" name="tab" value="tools"/>
		<input type="hidden" name="action" value="<?php echo esc_attr( $action_name ); ?>"/>
	</form>
	<?php
}
?>

<table class="wc_status_table wc_status_table--tools widefat" cellspacing="0">
	<tbody class="tools">
		<?php foreach ( $tools as $action_name => $tool ) : ?>
			<tr class="<?php echo sanitize_html_class( $action_name ); ?>">
				<th>
					<strong class="name"><?php echo esc_html( $tool['name'] ); ?></strong>
					<p class="description">
						<?php
						echo wp_kses_post( $tool['desc'] );
						if ( ! is_null( ArrayUtil::get_value_or_default( $tool, 'selector' ) ) ) {
							$selector = $tool['selector'];
							if ( isset( $selector['description'] ) ) {
								echo '</p><p class="description">';
								echo wp_kses_post( $selector['description'] );
							}
							// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
							echo "&nbsp;&nbsp;<select style='width: 300px;' form='form_$action_name' id='selector_$action_name' data-allow_clear='true' class='${selector['class']}' name='${selector['name']}' data-placeholder='${selector['placeholder']}' data-action='${selector['search_action']}'></select>";
						}
						?>
					</p>
				</th>
				<td class="run-tool">
					<?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
					<input <?php echo ArrayUtil::is_truthy( $tool, 'disabled' ) ? 'disabled' : ''; ?> type="submit" form="<?php echo 'form_' . $action_name; ?>" class="button button-large" value="<?php echo esc_attr( $tool['button'] ); ?>" />
				</td>
			</tr>
		<?php endforeach; ?>
	</tbody>
</table>
admin/views/html-notice-updated.php000064400000001224151336044770013362 0ustar00<?php
/**
 * Admin View: Notice - Updated.
 *
 * @package WooCommerce\Admin
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

?>
<div id="message" class="updated woocommerce-message wc-connect woocommerce-message--success">
	<a class="woocommerce-message-close notice-dismiss" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wc-hide-notice', 'update', remove_query_arg( 'do_update_woocommerce' ) ), 'woocommerce_hide_notices_nonce', '_wc_notice_nonce' ) ); ?>"><?php esc_html_e( 'Dismiss', 'woocommerce' ); ?></a>

	<p><?php esc_html_e( 'WooCommerce database update complete. Thank you for updating to the latest version!', 'woocommerce' ); ?></p>
</div>
admin/views/html-notice-regenerating-lookup-table.php000064400000002256151336044770017010 0ustar00<?php
/**
 * Admin View: Notice - Regenerating product lookup table.
 *
 * @package WooCommerce\Admin
 */

use Automattic\Jetpack\Constants;

defined( 'ABSPATH' ) || exit;

$pending_actions_url = admin_url( 'admin.php?page=wc-status&tab=action-scheduler&s=wc_update_product_lookup_tables&status=pending' );
$cron_disabled       = Constants::is_true( 'DISABLE_WP_CRON' );
$cron_cta            = $cron_disabled ? __( 'You can manually run queued updates here.', 'woocommerce' ) : __( 'View progress &rarr;', 'woocommerce' );
?>
<div id="message" class="updated woocommerce-message">
	<p>
		<strong><?php esc_html_e( 'WooCommerce is updating product data in the background', 'woocommerce' ); ?></strong><br>
		<?php
		esc_html_e( 'Product display, sorting, and reports may not be accurate until this finishes. It will take a few minutes and this notice will disappear when complete.', 'woocommerce' );

		if ( $cron_disabled ) {
			echo '<br>' . esc_html__( 'Note: WP CRON has been disabled on your install which may prevent this update from completing.', 'woocommerce' );
		}
		?>
		&nbsp;<a href="<?php echo esc_url( $pending_actions_url ); ?>"><?php echo esc_html( $cron_cta ); ?></a>
	</p>
</div>
admin/views/html-admin-dashboard-setup.php000064400000002463151336044770014636 0ustar00<?php
/**
 * Admin View: Dashboard - Finish Setup
 *
 * @package WooCommerce\Admin
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>
<div class="dashboard-widget-finish-setup">
	<span class='progress-wrapper'>
		<svg class="circle-progress" width="17" height="17" version="1.1" xmlns="http://www.w3.org/2000/svg">
		  <circle r="6.5" cx="10" cy="10" fill="transparent" stroke-dasharray="40.859" stroke-dashoffset="0"></circle>
		  <circle class="bar" r="6.5" cx="190" cy="10" fill="transparent" stroke-dasharray="40.859" stroke-dashoffset="<?php echo esc_attr( $circle_dashoffset ); ?>" transform='rotate(-90 100 100)'></circle>
		</svg>
		<span><?php echo esc_html_e( 'Step', 'woocommerce' ); ?> <?php echo esc_html( $completed_tasks_count ); ?> <?php echo esc_html_e( 'of', 'woocommerce' ); ?> <?php echo esc_html( $tasks_count ); ?></span>
	</span>

	<div class="description">
		<div>
			<?php echo esc_html_e( 'You\'re almost there! Once you complete store setup you can start receiving orders.', 'woocommerce' ); ?>
			<div><a href='<?php echo esc_attr( $button_link ); ?>' class='button button-primary'><?php echo esc_html_e( 'Start selling', 'woocommerce' ); ?></a></div>
		</div>
		<img src="<?php echo esc_url( WC()->plugin_url() ); ?>/assets/images/dashboard-widget-setup.png" />
	</div>
	<div class="clear"></div>
</div>
admin/class-wc-admin-addons.php000064400000120605151336044770012433 0ustar00<?php
/**
 * Addons Page
 *
 * @package  WooCommerce\Admin
 * @version  2.5.0
 */

use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Admin\RemoteInboxNotifications as PromotionRuleEngine;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC_Admin_Addons Class.
 */
class WC_Admin_Addons {

	/**
	 * Get featured for the addons screen
	 *
	 * @deprecated 5.9.0 No longer used in In-App Marketplace
	 *
	 * @return array of objects
	 */
	public static function get_featured() {
		$featured = get_transient( 'wc_addons_featured' );
		if ( false === $featured ) {
			$headers = array();
			$auth    = WC_Helper_Options::get( 'auth' );

			if ( ! empty( $auth['access_token'] ) ) {
				$headers['Authorization'] = 'Bearer ' . $auth['access_token'];
			}

			$raw_featured = wp_safe_remote_get(
				'https://woocommerce.com/wp-json/wccom-extensions/1.0/featured',
				array(
					'headers'    => $headers,
					'user-agent' => 'WooCommerce Addons Page',
				)
			);

			if ( ! is_wp_error( $raw_featured ) ) {
				$featured = json_decode( wp_remote_retrieve_body( $raw_featured ) );
				if ( $featured ) {
					set_transient( 'wc_addons_featured', $featured, DAY_IN_SECONDS );
				}
			}
		}

		if ( is_object( $featured ) ) {
			self::output_featured_sections( $featured->sections );
			return $featured;
		}
	}

	/**
	 * Render featured products and banners using WCCOM's the Featured 2.0 Endpoint
	 *
	 * @return void
	 */
	public static function render_featured() {
		$featured = get_transient( 'wc_addons_featured_2' );
		if ( false === $featured ) {
			$headers = array();
			$auth    = WC_Helper_Options::get( 'auth' );

			if ( ! empty( $auth['access_token'] ) ) {
				$headers['Authorization'] = 'Bearer ' . $auth['access_token'];
			}

			$parameter_string = '';
			$country          = WC()->countries->get_base_country();
			if ( ! empty( $country ) ) {
				$parameter_string = '?' . http_build_query( array( 'country' => $country ) );
			}

			// Important: WCCOM Extensions API v2.0 is used.
			$raw_featured = wp_safe_remote_get(
				'https://woocommerce.com/wp-json/wccom-extensions/2.0/featured' . $parameter_string,
				array(
					'headers'    => $headers,
					'user-agent' => 'WooCommerce Addons Page',
				)
			);

			if ( ! is_wp_error( $raw_featured ) ) {
				$featured = json_decode( wp_remote_retrieve_body( $raw_featured ) );
				if ( $featured ) {
					set_transient( 'wc_addons_featured_2', $featured, DAY_IN_SECONDS );
				}
			}
		}

		if ( ! empty( $featured ) ) {
			self::output_featured( $featured );
		}
	}

	/**
	 * Build url parameter string
	 *
	 * @param  string $category Addon (sub) category.
	 * @param  string $term     Search terms.
	 * @param  string $country  Store country.
	 *
	 * @return string url parameter string
	 */
	public static function build_parameter_string( $category, $term, $country ) {

		$parameters = array(
			'category' => $category,
			'term'     => $term,
			'country'  => $country,
		);

		return '?' . http_build_query( $parameters );
	}

	/**
	 * Call API to get extensions
	 *
	 * @param  string $category Addon (sub) category.
	 * @param  string $term     Search terms.
	 * @param  string $country  Store country.
	 *
	 * @return object of extensions and promotions.
	 */
	public static function get_extension_data( $category, $term, $country ) {
		$parameters = self::build_parameter_string( $category, $term, $country );

		$headers = array();
		$auth    = WC_Helper_Options::get( 'auth' );

		if ( ! empty( $auth['access_token'] ) ) {
			$headers['Authorization'] = 'Bearer ' . $auth['access_token'];
		}

		$raw_extensions = wp_safe_remote_get(
			'https://woocommerce.com/wp-json/wccom-extensions/1.0/search' . $parameters,
			array( 'headers' => $headers )
		);

		if ( ! is_wp_error( $raw_extensions ) ) {
			$addons = json_decode( wp_remote_retrieve_body( $raw_extensions ) );
		}
		return $addons;
	}

	/**
	 * Get sections for the addons screen
	 *
	 * @return array of objects
	 */
	public static function get_sections() {
		$addon_sections = get_transient( 'wc_addons_sections' );
		if ( false === ( $addon_sections ) ) {
			$raw_sections = wp_safe_remote_get(
				'https://woocommerce.com/wp-json/wccom-extensions/1.0/categories'
			);
			if ( ! is_wp_error( $raw_sections ) ) {
				$addon_sections = json_decode( wp_remote_retrieve_body( $raw_sections ) );
				if ( $addon_sections ) {
					set_transient( 'wc_addons_sections', $addon_sections, WEEK_IN_SECONDS );
				}
			}
		}
		return apply_filters( 'woocommerce_addons_sections', $addon_sections );
	}

	/**
	 * Get section for the addons screen.
	 *
	 * @param  string $section_id Required section ID.
	 *
	 * @return object|bool
	 */
	public static function get_section( $section_id ) {
		$sections = self::get_sections();
		if ( isset( $sections[ $section_id ] ) ) {
			return $sections[ $section_id ];
		}
		return false;
	}


	/**
	 * Get section content for the addons screen.
	 *
	 * @deprecated 5.9.0 No longer used in In-App Marketplace
	 *
	 * @param  string $section_id Required section ID.
	 *
	 * @return array
	 */
	public static function get_section_data( $section_id ) {
		$section      = self::get_section( $section_id );
		$section_data = '';

		if ( ! empty( $section->endpoint ) ) {
			$section_data = get_transient( 'wc_addons_section_' . $section_id );
			if ( false === $section_data ) {
				$raw_section = wp_safe_remote_get( esc_url_raw( $section->endpoint ), array( 'user-agent' => 'WooCommerce Addons Page' ) );

				if ( ! is_wp_error( $raw_section ) ) {
					$section_data = json_decode( wp_remote_retrieve_body( $raw_section ) );

					if ( ! empty( $section_data->products ) ) {
						set_transient( 'wc_addons_section_' . $section_id, $section_data, WEEK_IN_SECONDS );
					}
				}
			}
		}

		return apply_filters( 'woocommerce_addons_section_data', $section_data->products, $section_id );
	}

	/**
	 * Handles the outputting of a contextually aware Storefront link (points to child themes if Storefront is already active).
	 *
	 * @deprecated 5.9.0 No longer used in In-App Marketplace
	 */
	public static function output_storefront_button() {
		$template   = get_option( 'template' );
		$stylesheet = get_option( 'stylesheet' );

		if ( 'storefront' === $template ) {
			if ( 'storefront' === $stylesheet ) {
				$url         = 'https://woocommerce.com/product-category/themes/storefront-child-theme-themes/';
				$text        = __( 'Need a fresh look? Try Storefront child themes', 'woocommerce' );
				$utm_content = 'nostorefrontchildtheme';
			} else {
				$url         = 'https://woocommerce.com/product-category/themes/storefront-child-theme-themes/';
				$text        = __( 'View more Storefront child themes', 'woocommerce' );
				$utm_content = 'hasstorefrontchildtheme';
			}
		} else {
			$url         = 'https://woocommerce.com/storefront/';
			$text        = __( 'Need a theme? Try Storefront', 'woocommerce' );
			$utm_content = 'nostorefront';
		}

		$url = add_query_arg(
			array(
				'utm_source'   => 'addons',
				'utm_medium'   => 'product',
				'utm_campaign' => 'woocommerceplugin',
				'utm_content'  => $utm_content,
			),
			$url
		);

		echo '<a href="' . esc_url( $url ) . '" class="add-new-h2">' . esc_html( $text ) . '</a>' . "\n";
	}

	/**
	 * Handles the outputting of a banner block.
	 *
	 * @deprecated 5.9.0 No longer used in In-App Marketplace
	 *
	 * @param object $block Banner data.
	 */
	public static function output_banner_block( $block ) {
		?>
		<div class="addons-banner-block">
			<h1><?php echo esc_html( $block->title ); ?></h1>
			<p><?php echo esc_html( $block->description ); ?></p>
			<div class="addons-banner-block-items">
				<?php foreach ( $block->items as $item ) : ?>
					<?php if ( self::show_extension( $item ) ) : ?>
						<div class="addons-banner-block-item">
							<div class="addons-banner-block-item-icon">
								<img class="addons-img" src="<?php echo esc_url( $item->image ); ?>" />
							</div>
							<div class="addons-banner-block-item-content">
								<h3><?php echo esc_html( $item->title ); ?></h3>
								<p><?php echo esc_html( $item->description ); ?></p>
								<?php
									self::output_button(
										$item->href,
										$item->button,
										'addons-button-solid',
										$item->plugin
									);
								?>
							</div>
						</div>
					<?php endif; ?>
				<?php endforeach; ?>
			</div>
		</div>
		<?php
	}

	/**
	 * Handles the outputting of a column.
	 *
	 * @deprecated 5.9.0 No longer used in In-App Marketplace
	 *
	 * @param object $block Column data.
	 */
	public static function output_column( $block ) {
		if ( isset( $block->container ) && 'column_container_start' === $block->container ) {
			?>
			<div class="addons-column-section">
			<?php
		}
		if ( 'column_start' === $block->module ) {
			?>
			<div class="addons-column">
			<?php
		} else {
			?>
			</div>
			<?php
		}
		if ( isset( $block->container ) && 'column_container_end' === $block->container ) {
			?>
			</div>
			<?php
		}
	}

	/**
	 * Handles the outputting of a column block.
	 *
	 * @deprecated 5.9.0 No longer used in In-App Marketplace
	 *
	 * @param object $block Column block data.
	 */
	public static function output_column_block( $block ) {
		?>
		<div class="addons-column-block">
			<h1><?php echo esc_html( $block->title ); ?></h1>
			<p><?php echo esc_html( $block->description ); ?></p>
			<?php foreach ( $block->items as $item ) : ?>
				<?php if ( self::show_extension( $item ) ) : ?>
					<div class="addons-column-block-item">
						<div class="addons-column-block-item-icon">
							<img class="addons-img" src="<?php echo esc_url( $item->image ); ?>" />
						</div>
						<div class="addons-column-block-item-content">
							<h2><?php echo esc_html( $item->title ); ?></h2>
							<?php
								self::output_button(
									$item->href,
									$item->button,
									'addons-button-solid',
									$item->plugin
								);
							?>
							<p><?php echo esc_html( $item->description ); ?></p>
						</div>
					</div>
				<?php endif; ?>
			<?php endforeach; ?>
		</div>

		<?php
	}

	/**
	 * Handles the outputting of a small light block.
	 *
	 * @deprecated 5.9.0 No longer used in In-App Marketplace
	 *
	 * @param object $block Block data.
	 */
	public static function output_small_light_block( $block ) {
		?>
		<div class="addons-small-light-block">
			<img class="addons-img" src="<?php echo esc_url( $block->image ); ?>" />
			<div class="addons-small-light-block-content">
				<h1><?php echo esc_html( $block->title ); ?></h1>
				<p><?php echo esc_html( $block->description ); ?></p>
				<div class="addons-small-light-block-buttons">
					<?php foreach ( $block->buttons as $button ) : ?>
						<?php
							self::output_button(
								$button->href,
								$button->text,
								'addons-button-solid'
							);
						?>
					<?php endforeach; ?>
				</div>
			</div>
		</div>
		<?php
	}

	/**
	 * Handles the outputting of a small dark block.
	 *
	 * @deprecated 5.9.0 No longer used in In-App Marketplace
	 *
	 * @param object $block Block data.
	 */
	public static function output_small_dark_block( $block ) {
		?>
		<div class="addons-small-dark-block">
			<h1><?php echo esc_html( $block->title ); ?></h1>
			<p><?php echo esc_html( $block->description ); ?></p>
			<div class="addons-small-dark-items">
				<?php foreach ( $block->items as $item ) : ?>
					<div class="addons-small-dark-item">
						<?php if ( ! empty( $item->image ) ) : ?>
							<div class="addons-small-dark-item-icon">
								<img class="addons-img" src="<?php echo esc_url( $item->image ); ?>" />
							</div>
						<?php endif; ?>
						<?php
							self::output_button(
								$item->href,
								$item->button,
								'addons-button-outline-white'
							);
						?>
					</div>
				<?php endforeach; ?>
			</div>
		</div>
		<?php
	}

	/**
	 * Handles the outputting of the WooCommerce Services banner block.
	 *
	 * @deprecated 5.9.0 No longer used in In-App Marketplace
	 *
	 * @param object $block Block data.
	 */
	public static function output_wcs_banner_block( $block = array() ) {
		$is_active = is_plugin_active( 'woocommerce-services/woocommerce-services.php' );
		$location  = wc_get_base_location();

		if (
			! in_array( $location['country'], array( 'US' ), true ) ||
			$is_active ||
			! current_user_can( 'install_plugins' ) ||
			! current_user_can( 'activate_plugins' )
		) {
			return;
		}

		$button_url = wp_nonce_url(
			add_query_arg(
				array(
					'install-addon' => 'woocommerce-services',
				)
			),
			'install-addon_woocommerce-services'
		);

		$defaults = array(
			'image'       => WC()->plugin_url() . '/assets/images/wcs-extensions-banner-3x.jpg',
			'image_alt'   => __( 'WooCommerce Shipping', 'woocommerce' ),
			'title'       => __( 'Save time and money with WooCommerce Shipping', 'woocommerce' ),
			'description' => __( 'Print discounted USPS and DHL labels straight from your WooCommerce dashboard and save on shipping.', 'woocommerce' ),
			'button'      => __( 'Free - Install now', 'woocommerce' ),
			'href'        => $button_url,
			'logos'       => array(),
		);

		switch ( $location['country'] ) {
			case 'US':
				$local_defaults = array(
					'logos' => array_merge(
						$defaults['logos'],
						array(
							array(
								'link' => WC()->plugin_url() . '/assets/images/wcs-usps-logo.png',
								'alt'  => 'USPS logo',
							),
							array(
								'link' => WC()->plugin_url() . '/assets/images/wcs-dhlexpress-logo.png',
								'alt'  => 'DHL Express logo',
							),
						)
					),
				);
				break;
			default:
				$local_defaults = array();
		}

		$block_data = array_merge( $defaults, $local_defaults, $block );
		?>
		<div class="addons-wcs-banner-block">
			<div class="addons-wcs-banner-block-image is-full-image">
				<img
					class="addons-img"
					src="<?php echo esc_url( $block_data['image'] ); ?>"
					alt="<?php echo esc_attr( $block_data['image_alt'] ); ?>"
				/>
			</div>
			<div class="addons-wcs-banner-block-content">
				<h1><?php echo esc_html( $block_data['title'] ); ?></h1>
				<p><?php echo esc_html( $block_data['description'] ); ?></p>
				<ul class="wcs-logos-container">
					<?php foreach ( $block_data['logos'] as $logo ) : ?>
						<li>
							<img
								alt="<?php echo esc_attr( $logo['alt'] ); ?>"
								class="wcs-service-logo"
								src="<?php echo esc_url( $logo['link'] ); ?>"
							>
						</li>
					<?php endforeach; ?>
				</ul>
				<?php
					self::output_button(
						$block_data['href'],
						$block_data['button'],
						'addons-button-outline-purple'
					);
				?>
			</div>
		</div>
		<?php
	}

	/**
	 * Handles the outputting of the WooCommerce Pay banner block.
	 *
	 * @deprecated 5.9.0 No longer used in In-App Marketplace
	 *
	 * @param object $block Block data.
	 */
	public static function output_wcpay_banner_block( $block = array() ) {
		$is_active = is_plugin_active( 'woocommerce-payments/woocommerce-payments.php' );
		$location  = wc_get_base_location();

		if (
			! in_array( $location['country'], array( 'US' ), true ) ||
			$is_active ||
			! current_user_can( 'install_plugins' ) ||
			! current_user_can( 'activate_plugins' )
		) {
			return;
		}

		$button_url = wp_nonce_url(
			add_query_arg(
				array(
					'install-addon' => 'woocommerce-payments',
				)
			),
			'install-addon_woocommerce-payments'
		);

		$defaults = array(
			'image'       => WC()->plugin_url() . '/assets/images/wcpayments-icon-secure.png',
			'image_alt'   => __( 'WooCommerce Payments', 'woocommerce' ),
			'title'       => __( 'Payments made simple, with no monthly fees &mdash; exclusively for WooCommerce stores.', 'woocommerce' ),
			'description' => __( 'Securely accept cards in your store. See payments, track cash flow into your bank account, and stay on top of disputes – right from your dashboard.', 'woocommerce' ),
			'button'      => __( 'Free - Install now', 'woocommerce' ),
			'href'        => $button_url,
			'logos'       => array(),
		);

		$block_data = array_merge( $defaults, $block );
		?>
		<div class="addons-wcs-banner-block">
			<div class="addons-wcs-banner-block-image">
				<img
					class="addons-img"
					src="<?php echo esc_url( $block_data['image'] ); ?>"
					alt="<?php echo esc_attr( $block_data['image_alt'] ); ?>"
				/>
			</div>
			<div class="addons-wcs-banner-block-content">
				<h1><?php echo esc_html( $block_data['title'] ); ?></h1>
				<p><?php echo esc_html( $block_data['description'] ); ?></p>
				<?php
					self::output_button(
						$block_data['href'],
						$block_data['button'],
						'addons-button-outline-purple'
					);
				?>
			</div>
		</div>
		<?php
	}


	/**
	 * Output the HTML for the promotion block.
	 *
	 * @param array $promotion Array of promotion block data.
	 * @return void
	 */
	public static function output_search_promotion_block( array $promotion ) {
		?>
		<div class="addons-wcs-banner-block">
			<div class="addons-wcs-banner-block-image">
				<img
					class="addons-img"
					src="<?php echo esc_url( $promotion['image'] ); ?>"
					alt="<?php echo esc_attr( $promotion['image_alt'] ); ?>"
				/>
			</div>
			<div class="addons-wcs-banner-block-content">
				<h1><?php echo esc_html( $promotion['title'] ); ?></h1>
				<p><?php echo esc_html( $promotion['description'] ); ?></p>
				<?php
				if ( ! empty( $promotion['actions'] ) ) {
					foreach ( $promotion['actions'] as $action ) {
						self::output_promotion_action( $action );
					}
				}
				?>
			</div>
		</div>
		<?php
	}


	/**
	 * Handles the output of a full-width block.
	 *
	 * @deprecated 5.9.0 No longer used in In-App Marketplace
	 *
	 * @param array $section Section data.
	 */
	public static function output_promotion_block( $section ) {
		if (
			! current_user_can( 'install_plugins' ) ||
			! current_user_can( 'activate_plugins' )
		) {
			return;
		}

		$section_object = (object) $section;

		if ( ! empty( $section_object->geowhitelist ) ) {
			$section_object->geowhitelist = explode( ',', $section_object->geowhitelist );
		}

		if ( ! empty( $section_object->geoblacklist ) ) {
			$section_object->geoblacklist = explode( ',', $section_object->geoblacklist );
		}

		if ( ! self::show_extension( $section_object ) ) {
			return;
		}

		?>
		<div class="addons-banner-block addons-promotion-block">
			<img
				class="addons-img"
				src="<?php echo esc_url( $section['image'] ); ?>"
				alt="<?php echo esc_attr( $section['image_alt'] ); ?>"
			/>
			<div class="addons-promotion-block-content">
				<h1 class="addons-promotion-block-title"><?php echo esc_html( $section['title'] ); ?></h1>
				<div class="addons-promotion-block-description">
					<?php echo wp_kses_post( $section['description'] ); ?>
				</div>
				<div class="addons-promotion-block-buttons">
					<?php
					if ( $section['button_1'] ) {
						self::output_button(
							$section['button_1_href'],
							$section['button_1'],
							'addons-button-expandable addons-button-solid',
							$section['plugin']
						);
					}

					if ( $section['button_2'] ) {
						self::output_button(
							$section['button_2_href'],
							$section['button_2'],
							'addons-button-expandable addons-button-outline-purple',
							$section['plugin']
						);
					}
					?>
				</div>
			</div>
		</div>
		<?php
	}

	/**
	 * Handles the outputting of featured sections
	 *
	 * @param array $sections Section data.
	 */
	public static function output_featured_sections( $sections ) {
		foreach ( $sections as $section ) {
			switch ( $section->module ) {
				case 'banner_block':
					self::output_banner_block( $section );
					break;
				case 'column_start':
					self::output_column( $section );
					break;
				case 'column_end':
					self::output_column( $section );
					break;
				case 'column_block':
					self::output_column_block( $section );
					break;
				case 'small_light_block':
					self::output_small_light_block( $section );
					break;
				case 'small_dark_block':
					self::output_small_dark_block( $section );
					break;
				case 'wcs_banner_block':
					self::output_wcs_banner_block( (array) $section );
					break;
				case 'wcpay_banner_block':
					self::output_wcpay_banner_block( (array) $section );
					break;
				case 'promotion_block':
					self::output_promotion_block( (array) $section );
					break;
			}
		}
	}

	/**
	 * Handles the outputting of featured page
	 *
	 * @param array $blocks Featured page's blocks.
	 */
	private static function output_featured( $blocks ) {
		foreach ( $blocks as $block ) {
			$block_type = $block->type ?? null;
			switch ( $block_type ) {
				case 'group':
					self::output_group( $block );
					break;
				case 'banner':
					self::output_banner( $block );
					break;
			}
		}
	}

	/**
	 * Render a group block including products
	 *
	 * @param mixed $block Block of the page for rendering.
	 *
	 * @return void
	 */
	private static function output_group( $block ) {
		$capacity             = $block->capacity ?? 3;
		$product_list_classes = 3 === $capacity ? 'three-column' : 'two-column';
		$product_list_classes = 'products addons-products-' . $product_list_classes;
		?>
			<section class="addon-product-group">
				<h1 class="addon-product-group-title"><?php echo esc_html( $block->title ); ?></h1>
				<div class="addon-product-group-description-container">
					<?php if ( ! empty( $block->description ) ) : ?>
					<div class="addon-product-group-description">
						<?php echo esc_html( $block->description ); ?>
					</div>
					<?php endif; ?>
					<?php if ( null !== $block->url ) : ?>
					<a class="addon-product-group-see-more" href="<?php echo esc_url( $block->url ); ?>">
						<?php esc_html_e( 'See more', 'woocommerce' ); ?>
					</a>
					<?php endif; ?>
				</div>
				<div class="addon-product-group__items">
					<ul class="<?php echo esc_attr( $product_list_classes ); ?>">
					<?php
					$products = array_slice( $block->items, 0, $capacity );
					foreach ( $products as $item ) {
						self::render_product_card( $item );
					}
					?>
					</ul>
				<div>
			</section>
		<?php
	}

	/**
	 * Render a banner contains a product
	 *
	 * @param mixed $block Block of the page for rendering.
	 *
	 * @return void
	 */
	private static function output_banner( $block ) {
		if ( empty( $block->buttons ) ) {
			// Render a product-like banner.
			?>
			<ul class="products">
				<?php self::render_product_card( $block, $block->type ); ?>
			</ul>
			<?php
		} else {
			// Render a banner with buttons.
			?>
			<ul class="products">
				<li class="product addons-buttons-banner">
					<div class="addons-buttons-banner-image"
						style="background-image:url(<?php echo esc_url( $block->image ); ?>)"
						title="<?php echo esc_attr( $block->image_alt ); ?>"></div>
					<div class="product-details addons-buttons-banner-details-container">
						<div class="addons-buttons-banner-details">
							<h2><?php echo esc_html( $block->title ); ?></h2>
							<p><?php echo wp_kses( $block->description, array() ); ?></p>
						</div>
						<div class="addons-buttons-banner-button-container">
						<?php
						foreach ( $block->buttons as $button ) {
							$button_classes = array( 'button', 'addons-buttons-banner-button' );
							$type           = $button->type ?? null;
							if ( 'primary' === $type ) {
								$button_classes[] = 'addons-buttons-banner-button-primary';
							}
							?>
							<a class="<?php echo esc_attr( implode( ' ', $button_classes ) ); ?>"
								href="<?php echo esc_url( $button->href ); ?>">
								<?php echo esc_html( $button->title ); ?>
							</a>
						<?php } ?>
						</div>
					</div>
				</li>
			</ul>
			<?php
		}
	}

	/**
	 * Returns in-app-purchase URL params.
	 */
	public static function get_in_app_purchase_url_params() {
		// Get url (from path onward) for the current page,
		// so WCCOM "back" link returns user to where they were.
		$back_admin_path = add_query_arg( array() );
		return array(
			'wccom-site'          => site_url(),
			'wccom-back'          => rawurlencode( $back_admin_path ),
			'wccom-woo-version'   => Constants::get_constant( 'WC_VERSION' ),
			'wccom-connect-nonce' => wp_create_nonce( 'connect' ),
		);
	}

	/**
	 * Add in-app-purchase URL params to link.
	 *
	 * Adds various url parameters to a url to support a streamlined
	 * flow for obtaining and setting up WooCommerce extensons.
	 *
	 * @param string $url    Destination URL.
	 */
	public static function add_in_app_purchase_url_params( $url ) {
		return add_query_arg(
			self::get_in_app_purchase_url_params(),
			$url
		);
	}

	/**
	 * Outputs a button.
	 *
	 * @param string $url    Destination URL.
	 * @param string $text   Button label text.
	 * @param string $style  Button style class.
	 * @param string $plugin The plugin the button is promoting.
	 */
	public static function output_button( $url, $text, $style, $plugin = '' ) {
		$style = __( 'Free', 'woocommerce' ) === $text ? 'addons-button-outline-purple' : $style;
		$style = is_plugin_active( $plugin ) ? 'addons-button-installed' : $style;
		$text  = is_plugin_active( $plugin ) ? __( 'Installed', 'woocommerce' ) : $text;
		$url   = self::add_in_app_purchase_url_params( $url );
		?>
		<a
			class="addons-button <?php echo esc_attr( $style ); ?>"
			href="<?php echo esc_url( $url ); ?>">
			<?php echo esc_html( $text ); ?>
		</a>
		<?php
	}

	/**
	 * Output HTML for a promotion action.
	 *
	 * @param array $action Array of action properties.
	 *
	 * @return void
	 */
	public static function output_promotion_action( array $action ) {
		if ( empty( $action ) ) {
			return;
		}
		$style = ( ! empty( $action['primary'] ) && $action['primary'] ) ? 'addons-button-solid' : 'addons-button-outline-purple';
		?>
		<a
			class="addons-button <?php echo esc_attr( $style ); ?>"
			href="<?php echo esc_url( $action['url'] ); ?>">
			<?php echo esc_html( $action['label'] ); ?>
		</a>
		<?php
	}


	/**
	 * Handles output of the addons page in admin.
	 */
	public static function output() {
		$section = isset( $_GET['section'] ) ? sanitize_text_field( wp_unslash( $_GET['section'] ) ) : '_featured';
		$search  = isset( $_GET['search'] ) ? sanitize_text_field( wp_unslash( $_GET['search'] ) ) : '';

		if ( isset( $_GET['section'] ) && 'helper' === $_GET['section'] ) {
			do_action( 'woocommerce_helper_output' );
			return;
		}

		if ( isset( $_GET['install-addon'] ) ) {
			switch ( $_GET['install-addon'] ) {
				case 'woocommerce-services':
					self::install_woocommerce_services_addon();
					break;
				case 'woocommerce-payments':
					self::install_woocommerce_payments_addon( $section );
					break;
				default:
					// Do nothing.
					break;
			}
		}

		$sections        = self::get_sections();
		$theme           = wp_get_theme();
		$current_section = isset( $_GET['section'] ) ? $section : '_featured';
		$promotions      = array();
		$addons          = array();

		if ( '_featured' !== $current_section ) {
			$category       = $section ? $section : null;
			$term           = $search ? $search : null;
			$country        = WC()->countries->get_base_country();
			$extension_data = self::get_extension_data( $category, $term, $country );
			$addons         = $extension_data->products;
			$promotions     = ! empty( $extension_data->promotions ) ? $extension_data->promotions : array();
		}

		// We need Automattic\WooCommerce\Admin\RemoteInboxNotifications for the next part, if not remove all promotions.
		if ( ! WC()->is_wc_admin_active() ) {
			$promotions = array();
		}
		// Check for existence of promotions and evaluate out if we should show them.
		if ( ! empty( $promotions ) ) {
			foreach ( $promotions as $promo_id => $promotion ) {
				$evaluator = new PromotionRuleEngine\RuleEvaluator();
				$passed    = $evaluator->evaluate( $promotion->rules );
				if ( ! $passed ) {
					unset( $promotions[ $promo_id ] );
				}
			}
			// Transform promotions to the correct format ready for output.
			$promotions = self::format_promotions( $promotions );
		}

		/**
		 * Addon page view.
		 *
		 * @uses $addons
		 * @uses $search
		 * @uses $sections
		 * @uses $theme
		 * @uses $current_section
		 */
		include_once dirname( __FILE__ ) . '/views/html-admin-page-addons.php';
	}

	/**
	 * Install WooCommerce Services from Extensions screens.
	 */
	public static function install_woocommerce_services_addon() {
		check_admin_referer( 'install-addon_woocommerce-services' );

		$services_plugin_id = 'woocommerce-services';
		$services_plugin    = array(
			'name'      => __( 'WooCommerce Services', 'woocommerce' ),
			'repo-slug' => 'woocommerce-services',
		);

		WC_Install::background_installer( $services_plugin_id, $services_plugin );

		wp_safe_redirect( remove_query_arg( array( 'install-addon', '_wpnonce' ) ) );
		exit;
	}

	/**
	 * Install WooCommerce Payments from the Extensions screens.
	 *
	 * @param string $section Optional. Extenstions tab.
	 *
	 * @return void
	 */
	public static function install_woocommerce_payments_addon( $section = '_featured' ) {
		check_admin_referer( 'install-addon_woocommerce-payments' );

		$wcpay_plugin_id = 'woocommerce-payments';
		$wcpay_plugin    = array(
			'name'      => __( 'WooCommerce Payments', 'woocommerce' ),
			'repo-slug' => 'woocommerce-payments',
		);

		WC_Install::background_installer( $wcpay_plugin_id, $wcpay_plugin );

		do_action( 'woocommerce_addon_installed', $wcpay_plugin_id, $section );

		wp_safe_redirect( remove_query_arg( array( 'install-addon', '_wpnonce' ) ) );
		exit;
	}

	/**
	 * We're displaying page=wc-addons and page=wc-addons&section=helper as two separate pages.
	 * When we're on those pages, add body classes to distinguishe them.
	 *
	 * @param string $admin_body_class Unfiltered body class.
	 *
	 * @return string Body class with added class for Marketplace or My Subscriptions page.
	 */
	public static function filter_admin_body_classes( string $admin_body_class = '' ): string {
		if ( isset( $_GET['section'] ) && 'helper' === $_GET['section'] ) {
			return " $admin_body_class woocommerce-page-wc-subscriptions ";
		}

		return " $admin_body_class woocommerce-page-wc-marketplace ";
	}

	/**
	 * Take an action object and return the URL based on properties of the action.
	 *
	 * @param object $action Action object.
	 * @return string URL.
	 */
	public static function get_action_url( $action ): string {
		if ( ! isset( $action->url ) ) {
			return '';
		}

		if ( isset( $action->url_is_admin_query ) && $action->url_is_admin_query ) {
			return wc_admin_url( $action->url );
		}

		if ( isset( $action->url_is_admin_nonce_query ) && $action->url_is_admin_nonce_query ) {
			if ( empty( $action->nonce ) ) {
				return '';
			}
			return wp_nonce_url(
				admin_url( $action->url ),
				$action->nonce
			);
		}

		return $action->url;
	}

	/**
	 * Format the promotion data ready for display, ie fetch locales and actions.
	 *
	 * @param array $promotions Array of promotoin objects.
	 * @return array Array of formatted promotions ready for output.
	 */
	public static function format_promotions( array $promotions ): array {
		$formatted_promotions = array();
		foreach ( $promotions as $promotion ) {
			// Get the matching locale or fall back to en-US.
			$locale = PromotionRuleEngine\SpecRunner::get_locale( $promotion->locales );
			if ( null === $locale ) {
				continue;
			}

			$promotion_actions = array();
			if ( ! empty( $promotion->actions ) ) {
				foreach ( $promotion->actions as $action ) {
					$action_locale = PromotionRuleEngine\SpecRunner::get_action_locale( $action->locales );
					$url           = self::get_action_url( $action );

					$promotion_actions[] = array(
						'name'    => $action->name,
						'label'   => $action_locale->label,
						'url'     => $url,
						'primary' => isset( $action->is_primary ) ? $action->is_primary : false,
					);
				}
			}

			$formatted_promotions[] = array(
				'title'       => $locale->title,
				'description' => $locale->description,
				'image'       => ( 'http' === substr( $locale->image, 0, 4 ) ) ? $locale->image : WC()->plugin_url() . $locale->image,
				'image_alt'   => $locale->image_alt,
				'actions'     => $promotion_actions,
			);
		}
		return $formatted_promotions;
	}

	/**
	 * Map data from different endpoints to a universal format
	 *
	 * Search and featured products has a slightly different products' field names.
	 * Mapping converts different data structures into a universal one for further processing.
	 *
	 * @param mixed $data Product Card Data.
	 *
	 * @return object Converted data.
	 */
	public static function map_product_card_data( $data ) {
		$mapped = (object) null;

		$type = $data->type ?? null;

		// Icon.
		$mapped->icon = $data->icon ?? null;
		if ( null === $mapped->icon && 'banner' === $type ) {
			// For product-related banners icon is a product's image.
			$mapped->icon = $data->image ?? null;
		}
		// URL.
		$mapped->url = $data->link ?? null;
		if ( empty( $mapped->url ) ) {
			$mapped->url = $data->url ?? null;
		}
		// Title.
		$mapped->title = $data->title ?? null;
		// Vendor Name.
		$mapped->vendor_name = $data->vendor_name ?? null;
		if ( empty( $mapped->vendor_name ) ) {
			$mapped->vendor_name = $data->vendorName ?? null; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
		}
		// Vendor URL.
		$mapped->vendor_url = $data->vendor_url ?? null;
		if ( empty( $mapped->vendor_url ) ) {
			$mapped->vendor_url = $data->vendorUrl ?? null; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
		}
		// Description.
		$mapped->description = $data->excerpt ?? null;
		if ( empty( $mapped->description ) ) {
			$mapped->description = $data->description ?? null;
		}
		$has_currency = ! empty( $data->currency ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase

		// Is Free.
		if ( $has_currency ) {
			$mapped->is_free = 0 === (int) $data->price;
		} else {
			$mapped->is_free = '&#36;0.00' === $data->price;
		}
		// Price.
		if ( $has_currency ) {
			$mapped->price = wc_price( $data->price, array( 'currency' => $data->currency ) );
		} else {
			$mapped->price = $data->price;
		}
		// Rating.
		$mapped->rating = $data->rating ?? null;
		if ( null === $mapped->rating ) {
			$mapped->rating = $data->averageRating ?? null; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
		}
		// Reviews Count.
		$mapped->reviews_count = $data->reviews_count ?? null;
		if ( null === $mapped->reviews_count ) {
			$mapped->reviews_count = $data->reviewsCount ?? null; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
		}
		// Featured & Promoted product card.
		// Label.
		$mapped->label = $data->label ?? null;
		// Primary color.
		$mapped->primary_color = $data->primary_color ?? null;
		// Text color.
		$mapped->text_color = $data->text_color ?? null;
		// Button text.
		$mapped->button = $data->button ?? null;

		return $mapped;
	}

	/**
	 * Render a product card
	 *
	 * There's difference in data structure (e.g. field names) between endpoints such as search and
	 * featured. Inner mapping helps to use universal field names for further work.
	 *
	 * @param mixed  $data       Product data.
	 * @param string $block_type Block type that's different from the default product card, e.g. a banner.
	 *
	 * @return void
	 */
	public static function render_product_card( $data, $block_type = null ) {
		$mapped      = self::map_product_card_data( $data );
		$product_url = self::add_in_app_purchase_url_params( $mapped->url );
		$class_names = array( 'product' );
		// Specify a class name according to $block_type (if it's specified).
		if ( null !== $block_type ) {
			$class_names[] = 'addons-product-' . $block_type;
		}

		$product_details_classes = 'product-details';
		if ( 'banner' === $block_type ) {
			$product_details_classes .= ' addon-product-banner-details';
		}

		if ( isset( $mapped->label ) && 'promoted' === $mapped->label ) {
			$product_details_classes .= ' promoted';
		} elseif ( isset( $mapped->label ) && 'featured' === $mapped->label ) {
			$product_details_classes .= ' featured';
		}

		if ( 'promoted' === $mapped->label
			 && ! empty( $mapped->primary_color )
			 && ! empty( $mapped->text_color )
			 && ! empty( $mapped->button ) ) {
			// Promoted product card.
			?>
			<li class="product">
				<div class="<?php echo esc_attr( $product_details_classes ); ?>" style="border-top: 5px  solid <?php echo esc_html( $mapped->primary_color ); ?>;">
					<span class="label promoted"><?php esc_attr_e( 'Promoted', 'woocommerce' ); ?></span>
					<a href="<?php echo esc_url( $product_url ); ?>">
						<h2><?php echo esc_html( $mapped->title ); ?></h2>
					</a>
					<p><?php echo wp_kses_post( $mapped->description ); ?></p>
				</div>
				<div class="product-footer-promoted">
					<span class="icon"><img src="<?php echo esc_url( $mapped->icon ); ?>" /></span>
					<a class="addons-button addons-button-promoted" style="background: <?php echo esc_html( $mapped->primary_color ); ?>; color: <?php echo esc_html( $mapped->text_color ); ?>;" href="<?php echo esc_url( $product_url ); ?>">
						<?php echo esc_html( $mapped->button ); ?>
					</a>
				</div>
			</li>
			<?php
		} else {
			// Normal or "featured" product card.
			?>
			<li class="<?php echo esc_attr( implode( ' ', $class_names ) ); ?>">
				<div class="<?php echo esc_attr( $product_details_classes ); ?>">
					<div class="product-text-container">
						<?php if ( isset( $mapped->label ) && 'featured' === $mapped->label ) { ?>
							<span class="label featured"><?php esc_attr_e( 'Featured', 'woocommerce' ); ?></span>
						<?php } ?>
						<a href="<?php echo esc_url( $product_url ); ?>">
							<h2><?php echo esc_html( $mapped->title ); ?></h2>
						</a>
						<?php if ( ! empty( $mapped->vendor_name ) && ! empty( $mapped->vendor_url ) ) : ?>
							<div class="product-developed-by">
								<?php
								$vendor_url = add_query_arg(
									array(
										'utm_source'   => 'extensionsscreen',
										'utm_medium'   => 'product',
										'utm_campaign' => 'wcaddons',
										'utm_content'  => 'devpartner',
									),
									$mapped->vendor_url
								);

								printf(
								/* translators: %s vendor link */
									esc_html__( 'Developed by %s', 'woocommerce' ),
									sprintf(
										'<a class="product-vendor-link" href="%1$s" target="_blank">%2$s</a>',
										esc_url_raw( $vendor_url ),
										esc_html( $mapped->vendor_name )
									)
								);
								?>
							</div>
						<?php endif; ?>
						<p><?php echo wp_kses_post( $mapped->description ); ?></p>
					</div>
					<?php if ( ! empty( $mapped->icon ) ) : ?>
						<span class="product-img-wrap">
							<?php /* Show an icon if it exists */ ?>
							<img src="<?php echo esc_url( $mapped->icon ); ?>" />
						</span>
					<?php endif; ?>
				</div>
				<div class="product-footer">
					<div class="product-price-and-reviews-container">
						<div class="product-price-block">
							<?php if ( $mapped->is_free ) : ?>
								<span class="price"><?php esc_html_e( 'Free', 'woocommerce' ); ?></span>
							<?php else : ?>
								<span class="price">
									<?php
									echo wp_kses(
										$mapped->price,
										array(
											'span' => array(
												'class' => array(),
											),
											'bdi'  => array(),
										)
									);
									?>
								</span>
								<span class="price-suffix"><?php esc_html_e( 'per year', 'woocommerce' ); ?></span>
							<?php endif; ?>
						</div>
						<?php if ( ! empty( $mapped->reviews_count ) && ! empty( $mapped->rating ) ) : ?>
							<?php /* Show rating and the number of reviews */ ?>
							<div class="product-reviews-block">
								<?php for ( $index = 1; $index <= 5; ++$index ) : ?>
									<?php $rating_star_class = 'product-rating-star product-rating-star__' . self::get_star_class( $mapped->rating, $index ); ?>
									<div class="<?php echo esc_attr( $rating_star_class ); ?>"></div>
								<?php endfor; ?>
								<span class="product-reviews-count">(<?php echo (int) $mapped->reviews_count; ?>)</span>
							</div>
						<?php endif; ?>
					</div>
					<a class="button" href="<?php echo esc_url( $product_url ); ?>">
						<?php esc_html_e( 'View details', 'woocommerce' ); ?>
					</a>
				</div>
			</li>
			<?php
		}
	}

	/**
	 * Determine which class should be used for a rating star:
	 * - golden
	 * - half-filled (50/50 golden and gray)
	 * - gray
	 *
	 * Consider ratings from 3.0 to 4.0 as an example
	 * 3.0 will produce 3 stars
	 * 3.1 to 3.5 will produce 3 stars and a half star
	 * 3.6 to 4.0 will product 4 stars
	 *
	 * @param float $rating Rating of a product.
	 * @param int   $index  Index of a star in a row.
	 *
	 * @return string CSS class to use.
	 */
	public static function get_star_class( $rating, $index ) {
		if ( $rating >= $index ) {
			// Rating more that current star to show.
			return 'fill';
		} elseif (
			abs( $index - 1 - floor( $rating ) ) < 0.0000001 &&
			0 < ( $rating - floor( $rating ) )
		) {
			// For rating more than x.0 and less than x.5 or equal it will show a half star.
			return 50 >= floor( ( $rating - floor( $rating ) ) * 100 )
				? 'half-fill'
				: 'fill';
		}

		// Don't show a golden star otherwise.
		return 'no-fill';
	}
}
admin/class-wc-admin-setup-wizard.php000064400000253332151336044770013625 0ustar00<?php
/**
 * Setup Wizard Class
 *
 * Takes new users through some basic steps to setup their store.
 *
 * @package     WooCommerce\Admin
 * @version     2.6.0
 * @deprecated  4.6.0
 */

use Automattic\Jetpack\Constants;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC_Admin_Setup_Wizard class.
 */
class WC_Admin_Setup_Wizard {

	/**
	 * Current step
	 *
	 * @var string
	 */
	private $step = '';

	/**
	 * Steps for the setup wizard
	 *
	 * @var array
	 */
	private $steps = array();

	/**
	 * Actions to be executed after the HTTP response has completed
	 *
	 * @var array
	 */
	private $deferred_actions = array();

	/**
	 * Tweets user can optionally send after install
	 *
	 * @var array
	 */
	private $tweets = array(
		'Someone give me woo-t, I just set up a new store with #WordPress and @WooCommerce!',
		'Someone give me high five, I just set up a new store with #WordPress and @WooCommerce!',
	);

	/**
	 * The version of WordPress required to run the WooCommerce Admin plugin
	 *
	 * @var string
	 */
	private $wc_admin_plugin_minimum_wordpress_version = '5.3';

	/**
	 * Hook in tabs.
	 *
	 * @deprecated 4.6.0
	 */
	public function __construct() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
	}

	/**
	 * Add admin menus/screens.
	 *
	 * @deprecated 4.6.0
	 */
	public function admin_menus() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		add_dashboard_page( '', '', 'manage_options', 'wc-setup', '' );
	}

	/**
	 * The theme "extra" should only be shown if the current user can modify themes
	 * and the store doesn't already have a WooCommerce theme.
	 *
	 * @deprecated 4.6.0
	 * @return boolean
	 */
	protected function should_show_theme() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$support_woocommerce = current_theme_supports( 'woocommerce' ) && ! wc_is_wp_default_theme_active();

		return (
			current_user_can( 'install_themes' ) &&
			current_user_can( 'switch_themes' ) &&
			! is_multisite() &&
			! $support_woocommerce
		);
	}

	/**
	 * The "automated tax" extra should only be shown if the current user can
	 * install plugins and the store is in a supported country.
	 *
	 * @deprecated 4.6.0
	 */
	protected function should_show_automated_tax() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		if ( ! current_user_can( 'install_plugins' ) ) {
			return false;
		}

		$country_code = WC()->countries->get_base_country();
		// https://developers.taxjar.com/api/reference/#countries .
		$tax_supported_countries = array_merge(
			array( 'US', 'CA', 'AU' ),
			WC()->countries->get_european_union_countries()
		);

		return in_array( $country_code, $tax_supported_countries, true );
	}

	/**
	 * Should we show the MailChimp install option?
	 * True only if the user can install plugins.
	 *
	 * @deprecated 4.6.0
	 * @return boolean
	 */
	protected function should_show_mailchimp() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		return current_user_can( 'install_plugins' );
	}

	/**
	 * Should we show the Facebook install option?
	 * True only if the user can install plugins,
	 * and up until the end date of the recommendation.
	 *
	 * @deprecated 4.6.0
	 * @return boolean
	 */
	protected function should_show_facebook() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		return current_user_can( 'install_plugins' );
	}

	/**
	 * Is the WooCommerce Admin actively included in the WooCommerce core?
	 * Based on presence of a basic WC Admin function.
	 *
	 * @deprecated 4.6.0
	 * @return boolean
	 */
	protected function is_wc_admin_active() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		return function_exists( 'wc_admin_url' );
	}

	/**
	 * Should we show the WooCommerce Admin install option?
	 * True only if the user can install plugins,
	 * and is running the correct version of WordPress.
	 *
	 * @see WC_Admin_Setup_Wizard::$wc_admin_plugin_minimum_wordpress_version
	 *
	 * @deprecated 4.6.0
	 * @return boolean
	 */
	protected function should_show_wc_admin() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$wordpress_minimum_met = version_compare( get_bloginfo( 'version' ), $this->wc_admin_plugin_minimum_wordpress_version, '>=' );
		return current_user_can( 'install_plugins' ) && $wordpress_minimum_met && ! $this->is_wc_admin_active();
	}

	/**
	 * Should we show the new WooCommerce Admin onboarding experience?
	 *
	 * @deprecated 4.6.0
	 * @return boolean
	 */
	protected function should_show_wc_admin_onboarding() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		// As of WooCommerce 4.1, all new sites should use the latest OBW from wc-admin package.
		// This filter will allow for forcing the old wizard while we migrate e2e tests.
		return ! apply_filters( 'woocommerce_setup_wizard_force_legacy', false );
	}

	/**
	 * Should we display the 'Recommended' step?
	 * True if at least one of the recommendations will be displayed.
	 *
	 * @deprecated 4.6.0
	 * @return boolean
	 */
	protected function should_show_recommended_step() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		return $this->should_show_theme()
			|| $this->should_show_automated_tax()
			|| $this->should_show_mailchimp()
			|| $this->should_show_facebook()
			|| $this->should_show_wc_admin();
	}

	/**
	 * Register/enqueue scripts and styles for the Setup Wizard.
	 *
	 * Hooked onto 'admin_enqueue_scripts'.
	 *
	 * @deprecated 4.6.0
	 */
	public function enqueue_scripts() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
	}

	/**
	 * Show the setup wizard.
	 *
	 * @deprecated 4.6.0
	 */
	public function setup_wizard() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		if ( empty( $_GET['page'] ) || 'wc-setup' !== $_GET['page'] ) { // WPCS: CSRF ok, input var ok.
			return;
		}
		$default_steps = array(
			'new_onboarding' => array(
				'name'    => '',
				'view'    => array( $this, 'wc_setup_new_onboarding' ),
				'handler' => array( $this, 'wc_setup_new_onboarding_save' ),
			),
			'store_setup'    => array(
				'name'    => __( 'Store setup', 'woocommerce' ),
				'view'    => array( $this, 'wc_setup_store_setup' ),
				'handler' => array( $this, 'wc_setup_store_setup_save' ),
			),
			'payment'        => array(
				'name'    => __( 'Payment', 'woocommerce' ),
				'view'    => array( $this, 'wc_setup_payment' ),
				'handler' => array( $this, 'wc_setup_payment_save' ),
			),
			'shipping'       => array(
				'name'    => __( 'Shipping', 'woocommerce' ),
				'view'    => array( $this, 'wc_setup_shipping' ),
				'handler' => array( $this, 'wc_setup_shipping_save' ),
			),
			'recommended'    => array(
				'name'    => __( 'Recommended', 'woocommerce' ),
				'view'    => array( $this, 'wc_setup_recommended' ),
				'handler' => array( $this, 'wc_setup_recommended_save' ),
			),
			'activate'       => array(
				'name'    => __( 'Activate', 'woocommerce' ),
				'view'    => array( $this, 'wc_setup_activate' ),
				'handler' => array( $this, 'wc_setup_activate_save' ),
			),
			'next_steps'     => array(
				'name'    => __( 'Ready!', 'woocommerce' ),
				'view'    => array( $this, 'wc_setup_ready' ),
				'handler' => '',
			),
		);

		// Hide the new/improved onboarding experience screen if the user is not part of the a/b test.
		if ( ! $this->should_show_wc_admin_onboarding() ) {
			unset( $default_steps['new_onboarding'] );
		}

		// Hide recommended step if nothing is going to be shown there.
		if ( ! $this->should_show_recommended_step() ) {
			unset( $default_steps['recommended'] );
		}

		// Hide shipping step if the store is selling digital products only.
		if ( 'virtual' === get_option( 'woocommerce_product_type' ) ) {
			unset( $default_steps['shipping'] );
		}

		// Hide activate section when the user does not have capabilities to install plugins, think multiside admins not being a super admin.
		if ( ! current_user_can( 'install_plugins' ) ) {
			unset( $default_steps['activate'] );
		}

		$this->steps = apply_filters( 'woocommerce_setup_wizard_steps', $default_steps );
		$this->step  = isset( $_GET['step'] ) ? sanitize_key( $_GET['step'] ) : current( array_keys( $this->steps ) ); // WPCS: CSRF ok, input var ok.

		// @codingStandardsIgnoreStart
		if ( ! empty( $_POST['save_step'] ) && isset( $this->steps[ $this->step ]['handler'] ) ) {
			call_user_func( $this->steps[ $this->step ]['handler'], $this );
		}
		// @codingStandardsIgnoreEnd

		ob_start();
		$this->setup_wizard_header();
		$this->setup_wizard_steps();
		$this->setup_wizard_content();
		$this->setup_wizard_footer();
		exit;
	}

	/**
	 * Get the URL for the next step's screen.
	 *
	 * @param string $step  slug (default: current step).
	 * @return string       URL for next step if a next step exists.
	 *                      Admin URL if it's the last step.
	 *                      Empty string on failure.
	 *
	 * @deprecated 4.6.0
	 * @since 3.0.0
	 */
	public function get_next_step_link( $step = '' ) {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		if ( ! $step ) {
			$step = $this->step;
		}

		$keys = array_keys( $this->steps );
		if ( end( $keys ) === $step ) {
			return admin_url();
		}

		$step_index = array_search( $step, $keys, true );
		if ( false === $step_index ) {
			return '';
		}

		return add_query_arg( 'step', $keys[ $step_index + 1 ], remove_query_arg( 'activate_error' ) );
	}

	/**
	 * Setup Wizard Header.
	 *
	 * @deprecated 4.6.0
	 */
	public function setup_wizard_header() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		// same as default WP from wp-admin/admin-header.php.
		$wp_version_class = 'branch-' . str_replace( array( '.', ',' ), '-', floatval( get_bloginfo( 'version' ) ) );

		set_current_screen();
		?>
		<!DOCTYPE html>
		<html <?php language_attributes(); ?>>
		<head>
			<meta name="viewport" content="width=device-width" />
			<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
			<title><?php esc_html_e( 'WooCommerce &rsaquo; Setup Wizard', 'woocommerce' ); ?></title>
			<?php do_action( 'admin_enqueue_scripts' ); ?>
			<?php wp_print_scripts( 'wc-setup' ); ?>
			<?php do_action( 'admin_print_styles' ); ?>
			<?php do_action( 'admin_head' ); ?>
		</head>
		<body class="wc-setup wp-core-ui <?php echo esc_attr( 'wc-setup-step__' . $this->step ); ?> <?php echo esc_attr( $wp_version_class ); ?>">
		<h1 class="wc-logo"><a href="https://woocommerce.com/"><img src="<?php echo esc_url( WC()->plugin_url() ); ?>/assets/images/woocommerce_logo.png" alt="<?php esc_attr_e( 'WooCommerce', 'woocommerce' ); ?>" /></a></h1>
		<?php
	}

	/**
	 * Setup Wizard Footer.
	 *
	 * @deprecated 4.6.0
	 */
	public function setup_wizard_footer() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$current_step = $this->step;
		?>
			<?php if ( 'new_onboarding' === $current_step || 'store-setup' === $current_step ) : ?>
				<a class="wc-setup-footer-links" href="<?php echo esc_url( admin_url() ); ?>"><?php esc_html_e( 'Not right now', 'woocommerce' ); ?></a>
			<?php elseif ( 'recommended' === $current_step || 'activate' === $current_step ) : ?>
				<a class="wc-setup-footer-links" href="<?php echo esc_url( $this->get_next_step_link() ); ?>"><?php esc_html_e( 'Skip this step', 'woocommerce' ); ?></a>
			<?php endif; ?>
			<?php do_action( 'woocommerce_setup_footer' ); ?>
			</body>
		</html>
		<?php
	}

	/**
	 * Output the steps.
	 *
	 * @deprecated 4.6.0
	 */
	public function setup_wizard_steps() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$output_steps      = $this->steps;
		$selected_features = array_filter( $this->wc_setup_activate_get_feature_list() );

		// Hide the activate step if Jetpack is already active, unless WooCommerce Services
		// features are selected, or unless the Activate step was already taken.
		if ( class_exists( 'Jetpack' ) && Jetpack::is_active() && empty( $selected_features ) && 'yes' !== get_transient( 'wc_setup_activated' ) ) {
			unset( $output_steps['activate'] );
		}

		unset( $output_steps['new_onboarding'] );

		?>
		<ol class="wc-setup-steps">
			<?php
			foreach ( $output_steps as $step_key => $step ) {
				$is_completed = array_search( $this->step, array_keys( $this->steps ), true ) > array_search( $step_key, array_keys( $this->steps ), true );

				if ( $step_key === $this->step ) {
					?>
					<li class="active"><?php echo esc_html( $step['name'] ); ?></li>
					<?php
				} elseif ( $is_completed ) {
					?>
					<li class="done">
						<a href="<?php echo esc_url( add_query_arg( 'step', $step_key, remove_query_arg( 'activate_error' ) ) ); ?>"><?php echo esc_html( $step['name'] ); ?></a>
					</li>
					<?php
				} else {
					?>
					<li><?php echo esc_html( $step['name'] ); ?></li>
					<?php
				}
			}
			?>
		</ol>
		<?php
	}

	/**
	 * Output the content for the current step.
	 *
	 * @deprecated 4.6.0
	 */
	public function setup_wizard_content() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		echo '<div class="wc-setup-content">';
		if ( ! empty( $this->steps[ $this->step ]['view'] ) ) {
			call_user_func( $this->steps[ $this->step ]['view'], $this );
		}
		echo '</div>';
	}

	/**
	 * Display's a prompt for users to try out the new improved WooCommerce onboarding experience in WooCommerce Admin.
	 *
	 * @deprecated 4.6.0
	 */
	public function wc_setup_new_onboarding() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		?>
			<div class="wc-setup-step__new_onboarding-wrapper">
				<p class="wc-setup-step__new_onboarding-welcome"><?php esc_html_e( 'Welcome to', 'woocommerce' ); ?></p>
				<h1 class="wc-logo"><a href="https://woocommerce.com/"><img src="<?php echo esc_url( WC()->plugin_url() ); ?>/assets/images/woocommerce_logo.png" alt="<?php esc_attr_e( 'WooCommerce', 'woocommerce' ); ?>" /></a></h1>
				<p><?php esc_html_e( 'Get your store up and running more quickly with our new and improved setup experience', 'woocommerce' ); ?></p>

				<form method="post" class="activate-new-onboarding">
					<?php wp_nonce_field( 'wc-setup' ); ?>
					<input type="hidden" name="save_step" value="new_onboarding" />
					<p class="wc-setup-actions step">
						<button class="button-primary button button-large" value="<?php esc_attr_e( 'Yes please', 'woocommerce' ); ?>" name="save_step"><?php esc_html_e( 'Yes please', 'woocommerce' ); ?></button>
					</p>
				</form>
				<?php if ( ! $this->is_wc_admin_active() ) : ?>
					<p class="wc-setup-step__new_onboarding-plugin-info"><?php esc_html_e( 'The "WooCommerce Admin" plugin will be installed and activated', 'woocommerce' ); ?></p>
				<?php endif; ?>
			</div>
		<?php
	}

	/**
	 * Installs WooCommerce admin and redirects to the new onboarding experience.
	 *
	 * @deprecated 4.6.0
	 */
	public function wc_setup_new_onboarding_save() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
	}

	/**
	 * Initial "store setup" step.
	 * Location, product type, page setup, and tracking opt-in.
	 */
	public function wc_setup_store_setup() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$address        = WC()->countries->get_base_address();
		$address_2      = WC()->countries->get_base_address_2();
		$city           = WC()->countries->get_base_city();
		$state          = WC()->countries->get_base_state();
		$country        = WC()->countries->get_base_country();
		$postcode       = WC()->countries->get_base_postcode();
		$currency       = get_option( 'woocommerce_currency', 'USD' );
		$product_type   = get_option( 'woocommerce_product_type', 'both' );
		$sell_in_person = get_option( 'woocommerce_sell_in_person', 'none_selected' );

		if ( empty( $country ) ) {
			$user_location = WC_Geolocation::geolocate_ip();
			$country       = $user_location['country'];
			$state         = $user_location['state'];
		}

		$locale_info         = include WC()->plugin_path() . '/i18n/locale-info.php';
		$currency_by_country = wp_list_pluck( $locale_info, 'currency_code' );
		?>
		<form method="post" class="address-step">
			<input type="hidden" name="save_step" value="store_setup" />
			<?php wp_nonce_field( 'wc-setup' ); ?>
			<p class="store-setup"><?php esc_html_e( 'The following wizard will help you configure your store and get you started quickly.', 'woocommerce' ); ?></p>

			<div class="store-address-container">

				<label for="store_country" class="location-prompt"><?php esc_html_e( 'Where is your store based?', 'woocommerce' ); ?></label>
				<select id="store_country" name="store_country" required data-placeholder="<?php esc_attr_e( 'Choose a country / region&hellip;', 'woocommerce' ); ?>" aria-label="<?php esc_attr_e( 'Country / Region', 'woocommerce' ); ?>" class="location-input wc-enhanced-select dropdown">
					<?php foreach ( WC()->countries->get_countries() as $code => $label ) : ?>
						<option <?php selected( $code, $country ); ?> value="<?php echo esc_attr( $code ); ?>"><?php echo esc_html( $label ); ?></option>
					<?php endforeach; ?>
				</select>

				<label class="location-prompt" for="store_address"><?php esc_html_e( 'Address', 'woocommerce' ); ?></label>
				<input type="text" id="store_address" class="location-input" name="store_address" required value="<?php echo esc_attr( $address ); ?>" />

				<label class="location-prompt" for="store_address_2"><?php esc_html_e( 'Address line 2', 'woocommerce' ); ?></label>
				<input type="text" id="store_address_2" class="location-input" name="store_address_2" value="<?php echo esc_attr( $address_2 ); ?>" />

				<div class="city-and-postcode">
					<div>
						<label class="location-prompt" for="store_city"><?php esc_html_e( 'City', 'woocommerce' ); ?></label>
						<input type="text" id="store_city" class="location-input" name="store_city" required value="<?php echo esc_attr( $city ); ?>" />
					</div>
					<div class="store-state-container hidden">
						<label for="store_state" class="location-prompt">
							<?php esc_html_e( 'State', 'woocommerce' ); ?>
						</label>
						<select id="store_state" name="store_state" data-placeholder="<?php esc_attr_e( 'Choose a state&hellip;', 'woocommerce' ); ?>" aria-label="<?php esc_attr_e( 'State', 'woocommerce' ); ?>" class="location-input wc-enhanced-select dropdown"></select>
					</div>
					<div>
						<label class="location-prompt" for="store_postcode"><?php esc_html_e( 'Postcode / ZIP', 'woocommerce' ); ?></label>
						<input type="text" id="store_postcode" class="location-input" name="store_postcode" required value="<?php echo esc_attr( $postcode ); ?>" />
					</div>
				</div>
			</div>

			<div class="store-currency-container">
			<label class="location-prompt" for="currency_code">
				<?php esc_html_e( 'What currency do you accept payments in?', 'woocommerce' ); ?>
			</label>
			<select
				id="currency_code"
				name="currency_code"
				required
				data-placeholder="<?php esc_attr_e( 'Choose a currency&hellip;', 'woocommerce' ); ?>"
				class="location-input wc-enhanced-select dropdown"
			>
				<option value=""><?php esc_html_e( 'Choose a currency&hellip;', 'woocommerce' ); ?></option>
				<?php foreach ( get_woocommerce_currencies() as $code => $name ) : ?>
					<option value="<?php echo esc_attr( $code ); ?>" <?php selected( $currency, $code ); ?>>
						<?php
						$symbol = get_woocommerce_currency_symbol( $code );

						if ( $symbol === $code ) {
							/* translators: 1: currency name 2: currency code */
							echo esc_html( sprintf( __( '%1$s (%2$s)', 'woocommerce' ), $name, $code ) );
						} else {
							/* translators: 1: currency name 2: currency symbol, 3: currency code */
							echo esc_html( sprintf( __( '%1$s (%2$s %3$s)', 'woocommerce' ), $name, get_woocommerce_currency_symbol( $code ), $code ) );
						}
						?>
					</option>
				<?php endforeach; ?>
			</select>
			<script type="text/javascript">
				var wc_setup_currencies = JSON.parse( decodeURIComponent( '<?php echo rawurlencode( wp_json_encode( $currency_by_country ) ); ?>' ) );
				var wc_base_state       = "<?php echo esc_js( $state ); ?>";
			</script>
			</div>

			<div class="product-type-container">
			<label class="location-prompt" for="product_type">
				<?php esc_html_e( 'What type of products do you plan to sell?', 'woocommerce' ); ?>
			</label>
			<select id="product_type" name="product_type" required class="location-input wc-enhanced-select dropdown">
				<option value="both" <?php selected( $product_type, 'both' ); ?>><?php esc_html_e( 'I plan to sell both physical and digital products', 'woocommerce' ); ?></option>
				<option value="physical" <?php selected( $product_type, 'physical' ); ?>><?php esc_html_e( 'I plan to sell physical products', 'woocommerce' ); ?></option>
				<option value="virtual" <?php selected( $product_type, 'virtual' ); ?>><?php esc_html_e( 'I plan to sell digital products', 'woocommerce' ); ?></option>
			</select>
			</div>

			<div class="sell-in-person-container">
			<input
				type="checkbox"
				id="woocommerce_sell_in_person"
				name="sell_in_person"
				value="yes"
				<?php checked( $sell_in_person, true ); ?>
			/>
			<label class="location-prompt" for="woocommerce_sell_in_person">
				<?php esc_html_e( 'I will also be selling products or services in person.', 'woocommerce' ); ?>
			</label>
			</div>

			<input type="checkbox" id="wc_tracker_checkbox" name="wc_tracker_checkbox" value="yes" <?php checked( 'yes', get_option( 'woocommerce_allow_tracking', 'no' ) ); ?> />

			<?php $this->tracking_modal(); ?>

			<p class="wc-setup-actions step">
				<button class="button-primary button button-large" value="<?php esc_attr_e( "Let's go!", 'woocommerce' ); ?>" name="save_step"><?php esc_html_e( "Let's go!", 'woocommerce' ); ?></button>
			</p>
		</form>
		<?php
	}

	/**
	 * Template for the usage tracking modal.
	 *
	 * @deprecated 4.6.0
	 */
	public function tracking_modal() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		?>
		<script type="text/template" id="tmpl-wc-modal-tracking-setup">
			<div class="wc-backbone-modal woocommerce-tracker">
				<div class="wc-backbone-modal-content">
					<section class="wc-backbone-modal-main" role="main">
						<header class="wc-backbone-modal-header">
							<h1><?php esc_html_e( 'Help improve WooCommerce with usage tracking', 'woocommerce' ); ?></h1>
						</header>
						<article>
							<p>
							<?php
								printf(
									wp_kses(
										/* translators: %1$s: usage tracking help link */
										__( 'Learn more about how usage tracking works, and how you\'ll be helping in our <a href="%1$s" target="_blank">usage tracking documentation</a>.', 'woocommerce' ),
										array(
											'a' => array(
												'href'   => array(),
												'target' => array(),
											),
										)
									),
									'https://woocommerce.com/usage-tracking/'
								);
							?>
							</p>
							<p class="woocommerce-tracker-checkbox">
								<input type="checkbox" id="wc_tracker_checkbox_dialog" name="wc_tracker_checkbox_dialog" value="yes" <?php checked( 'yes', get_option( 'woocommerce_allow_tracking', 'no' ) ); ?> />
								<label for="wc_tracker_checkbox_dialog"><?php esc_html_e( 'Enable usage tracking and help improve WooCommerce', 'woocommerce' ); ?></label>
							</p>
						</article>
						<footer>
							<div class="inner">
								<button class="button button-primary button-large" id="wc_tracker_submit" aria-label="<?php esc_attr_e( 'Continue', 'woocommerce' ); ?>"><?php esc_html_e( 'Continue', 'woocommerce' ); ?></button>
							</div>
						</footer>
					</section>
				</div>
			</div>
			<div class="wc-backbone-modal-backdrop modal-close"></div>
		</script>
		<?php
	}

	/**
	 * Save initial store settings.
	 *
	 * @deprecated 4.6.0
	 */
	public function wc_setup_store_setup_save() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
	}

	/**
	 * Finishes replying to the client, but keeps the process running for further (async) code execution.
	 *
	 * @see https://core.trac.wordpress.org/ticket/41358 .
	 */
	protected function close_http_connection() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		// Only 1 PHP process can access a session object at a time, close this so the next request isn't kept waiting.
		// @codingStandardsIgnoreStart
		if ( session_id() ) {
			session_write_close();
		}
		// @codingStandardsIgnoreEnd

		wc_set_time_limit( 0 );

		// fastcgi_finish_request is the cleanest way to send the response and keep the script running, but not every server has it.
		if ( is_callable( 'fastcgi_finish_request' ) ) {
			fastcgi_finish_request();
		} else {
			// Fallback: send headers and flush buffers.
			if ( ! headers_sent() ) {
				header( 'Connection: close' );
			}
			@ob_end_flush(); // @codingStandardsIgnoreLine.
			flush();
		}
	}

	/**
	 * Function called after the HTTP request is finished, so it's executed without the client having to wait for it.
	 *
	 * @see WC_Admin_Setup_Wizard::install_plugin
	 * @see WC_Admin_Setup_Wizard::install_theme
	 *
	 * @deprecated 4.6.0
	 */
	public function run_deferred_actions() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$this->close_http_connection();
		foreach ( $this->deferred_actions as $action ) {
			$action['func']( ...$action['args'] );

			// Clear the background installation flag if this is a plugin.
			if (
				isset( $action['func'][1] ) &&
				'background_installer' === $action['func'][1] &&
				isset( $action['args'][0] )
			) {
				delete_option( 'woocommerce_setup_background_installing_' . $action['args'][0] );
			}
		}
	}

	/**
	 * Helper method to queue the background install of a plugin.
	 *
	 * @param string $plugin_id  Plugin id used for background install.
	 * @param array  $plugin_info Plugin info array containing name and repo-slug, and optionally file if different from [repo-slug].php.
	 *
	 * @deprecated 4.6.0
	 */
	protected function install_plugin( $plugin_id, $plugin_info ) {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		// Make sure we don't trigger multiple simultaneous installs.
		if ( get_option( 'woocommerce_setup_background_installing_' . $plugin_id ) ) {
			return;
		}

		$plugin_file = isset( $plugin_info['file'] ) ? $plugin_info['file'] : $plugin_info['repo-slug'] . '.php';
		if ( is_plugin_active( $plugin_info['repo-slug'] . '/' . $plugin_file ) ) {
			return;
		}

		if ( empty( $this->deferred_actions ) ) {
			add_action( 'shutdown', array( $this, 'run_deferred_actions' ) );
		}

		array_push(
			$this->deferred_actions,
			array(
				'func' => array( 'WC_Install', 'background_installer' ),
				'args' => array( $plugin_id, $plugin_info ),
			)
		);

		// Set the background installation flag for this plugin.
		update_option( 'woocommerce_setup_background_installing_' . $plugin_id, true );
	}


	/**
	 * Helper method to queue the background install of a theme.
	 *
	 * @param string $theme_id  Theme id used for background install.
	 *
	 * @deprecated 4.6.0
	 */
	protected function install_theme( $theme_id ) {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		if ( empty( $this->deferred_actions ) ) {
			add_action( 'shutdown', array( $this, 'run_deferred_actions' ) );
		}
		array_push(
			$this->deferred_actions,
			array(
				'func' => array( 'WC_Install', 'theme_background_installer' ),
				'args' => array( $theme_id ),
			)
		);
	}

	/**
	 * Helper method to install Jetpack.
	 *
	 * @deprecated 4.6.0
	 */
	protected function install_jetpack() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$this->install_plugin(
			'jetpack',
			array(
				'name'      => __( 'Jetpack', 'woocommerce' ),
				'repo-slug' => 'jetpack',
			)
		);
	}

	/**
	 * Helper method to install WooCommerce Services and its Jetpack dependency.
	 *
	 * @deprecated 4.6.0
	 */
	protected function install_woocommerce_services() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$this->install_jetpack();
		$this->install_plugin(
			'woocommerce-services',
			array(
				'name'      => __( 'WooCommerce Services', 'woocommerce' ),
				'repo-slug' => 'woocommerce-services',
			)
		);
	}

	/**
	 * Retrieve info for missing WooCommerce Services and/or Jetpack plugin.
	 *
	 * @deprecated 4.6.0
	 * @return array
	 */
	protected function get_wcs_requisite_plugins() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$plugins = array();
		if ( ! is_plugin_active( 'woocommerce-services/woocommerce-services.php' ) && ! get_option( 'woocommerce_setup_background_installing_woocommerce-services' ) ) {
			$plugins[] = array(
				'name' => __( 'WooCommerce Services', 'woocommerce' ),
				'slug' => 'woocommerce-services',
			);
		}
		if ( ! is_plugin_active( 'jetpack/jetpack.php' ) && ! get_option( 'woocommerce_setup_background_installing_jetpack' ) ) {
			$plugins[] = array(
				'name' => __( 'Jetpack', 'woocommerce' ),
				'slug' => 'jetpack',
			);
		}
		return $plugins;
	}

	/**
	 * Plugin install info message markup with heading.
	 *
	 * @deprecated 4.6.0
	 */
	public function plugin_install_info() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		?>
		<span class="plugin-install-info">
			<span class="plugin-install-info-label"><?php esc_html_e( 'The following plugins will be installed and activated for you:', 'woocommerce' ); ?></span>
			<span class="plugin-install-info-list"></span>
		</span>
		<?php
	}

	/**
	 * Get shipping methods based on country code.
	 *
	 * @param string $country_code Country code.
	 * @param string $currency_code Currency code.
	 *
	 * @deprecated 4.6.0
	 * @return array
	 */
	protected function get_wizard_shipping_methods( $country_code, $currency_code ) {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$shipping_methods = array(
			'flat_rate'     => array(
				'name'        => __( 'Flat Rate', 'woocommerce' ),
				'description' => __( 'Set a fixed price to cover shipping costs.', 'woocommerce' ),
				'settings'    => array(
					'cost' => array(
						'type'          => 'text',
						'default_value' => __( 'Cost', 'woocommerce' ),
						'description'   => __( 'What would you like to charge for flat rate shipping?', 'woocommerce' ),
						'required'      => true,
					),
				),
			),
			'free_shipping' => array(
				'name'        => __( 'Free Shipping', 'woocommerce' ),
				'description' => __( "Don't charge for shipping.", 'woocommerce' ),
			),
		);

		return $shipping_methods;
	}

	/**
	 * Render the available shipping methods for a given country code.
	 *
	 * @param string $country_code Country code.
	 * @param string $currency_code Currency code.
	 * @param string $input_prefix Input prefix.
	 *
	 * @deprecated 4.6.0
	 */
	protected function shipping_method_selection_form( $country_code, $currency_code, $input_prefix ) {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$selected         = 'flat_rate';
		$shipping_methods = $this->get_wizard_shipping_methods( $country_code, $currency_code );
		?>
		<div class="wc-wizard-shipping-method-select">
			<div class="wc-wizard-shipping-method-dropdown">
				<select
					id="<?php echo esc_attr( "{$input_prefix}[method]" ); ?>"
					name="<?php echo esc_attr( "{$input_prefix}[method]" ); ?>"
					class="method wc-enhanced-select"
					data-plugins="<?php echo wc_esc_json( wp_json_encode( $this->get_wcs_requisite_plugins() ) ); ?>"
				>
				<?php foreach ( $shipping_methods as $method_id => $method ) : ?>
					<option value="<?php echo esc_attr( $method_id ); ?>" <?php selected( $selected, $method_id ); ?>><?php echo esc_html( $method['name'] ); ?></option>
				<?php endforeach; ?>
				</select>
			</div>
			<div class="shipping-method-descriptions">
				<?php foreach ( $shipping_methods as $method_id => $method ) : ?>
					<p class="shipping-method-description <?php echo esc_attr( $method_id ); ?> <?php echo $method_id !== $selected ? 'hide' : ''; ?>">
						<?php echo esc_html( $method['description'] ); ?>
					</p>
				<?php endforeach; ?>
			</div>
		</div>

		<div class="shipping-method-settings">
		<?php foreach ( $shipping_methods as $method_id => $method ) : ?>
			<?php
			if ( empty( $method['settings'] ) ) {
				continue;
			}
			?>
			<div class="shipping-method-setting <?php echo esc_attr( $method_id ); ?> <?php echo $method_id !== $selected ? 'hide' : ''; ?>">
			<?php foreach ( $method['settings'] as $setting_id => $setting ) : ?>
				<?php $method_setting_id = "{$input_prefix}[{$method_id}][{$setting_id}]"; ?>
				<input
					type="<?php echo esc_attr( $setting['type'] ); ?>"
					placeholder="<?php echo esc_attr( $setting['default_value'] ); ?>"
					id="<?php echo esc_attr( $method_setting_id ); ?>"
					name="<?php echo esc_attr( $method_setting_id ); ?>"
					class="<?php echo esc_attr( $setting['required'] ? 'shipping-method-required-field' : '' ); ?>"
					<?php echo ( $method_id === $selected && $setting['required'] ) ? 'required' : ''; ?>
				/>
				<p class="description">
					<?php echo esc_html( $setting['description'] ); ?>
				</p>
			<?php endforeach; ?>
			</div>
		<?php endforeach; ?>
		</div>
		<?php
	}

	/**
	 * Render a product weight unit dropdown.
	 *
	 * @deprecated 4.6.0
	 * @return string
	 */
	protected function get_product_weight_selection() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$weight_unit = get_option( 'woocommerce_weight_unit' );
		ob_start();
		?>
		<span class="wc-setup-shipping-unit">
			<select id="weight_unit" name="weight_unit" class="wc-enhanced-select">
				<option value="kg" <?php selected( $weight_unit, 'kg' ); ?>><?php esc_html_e( 'Kilograms', 'woocommerce' ); ?></option>
				<option value="g" <?php selected( $weight_unit, 'g' ); ?>><?php esc_html_e( 'Grams', 'woocommerce' ); ?></option>
				<option value="lbs" <?php selected( $weight_unit, 'lbs' ); ?>><?php esc_html_e( 'Pounds', 'woocommerce' ); ?></option>
				<option value="oz" <?php selected( $weight_unit, 'oz' ); ?>><?php esc_html_e( 'Ounces', 'woocommerce' ); ?></option>
			</select>
		</span>
		<?php

		return ob_get_clean();
	}

	/**
	 * Render a product dimension unit dropdown.
	 *
	 * @deprecated 4.6.0
	 * @return string
	 */
	protected function get_product_dimension_selection() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$dimension_unit = get_option( 'woocommerce_dimension_unit' );
		ob_start();
		?>
		<span class="wc-setup-shipping-unit">
			<select id="dimension_unit" name="dimension_unit" class="wc-enhanced-select">
				<option value="m" <?php selected( $dimension_unit, 'm' ); ?>><?php esc_html_e( 'Meters', 'woocommerce' ); ?></option>
				<option value="cm" <?php selected( $dimension_unit, 'cm' ); ?>><?php esc_html_e( 'Centimeters', 'woocommerce' ); ?></option>
				<option value="mm" <?php selected( $dimension_unit, 'mm' ); ?>><?php esc_html_e( 'Millimeters', 'woocommerce' ); ?></option>
				<option value="in" <?php selected( $dimension_unit, 'in' ); ?>><?php esc_html_e( 'Inches', 'woocommerce' ); ?></option>
				<option value="yd" <?php selected( $dimension_unit, 'yd' ); ?>><?php esc_html_e( 'Yards', 'woocommerce' ); ?></option>
			</select>
		</span>
		<?php

		return ob_get_clean();
	}

	/**
	 * Shipping.
	 *
	 * @deprecated 4.6.0
	 */
	public function wc_setup_shipping() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$country_code          = WC()->countries->get_base_country();
		$country_name          = WC()->countries->countries[ $country_code ];
		$prefixed_country_name = WC()->countries->estimated_for_prefix( $country_code ) . $country_name;
		$currency_code         = get_woocommerce_currency();
		$existing_zones        = WC_Shipping_Zones::get_zones();
		$intro_text            = '';

		if ( empty( $existing_zones ) ) {
			$intro_text = sprintf(
				/* translators: %s: country name including the 'the' prefix if needed */
				__( "We've created two Shipping Zones - for %s and for the rest of the world. Below you can set Flat Rate shipping costs for these Zones or offer Free Shipping.", 'woocommerce' ),
				$prefixed_country_name
			);
		}

		$is_wcs_labels_supported  = $this->is_wcs_shipping_labels_supported_country( $country_code );
		$is_shipstation_supported = $this->is_shipstation_supported_country( $country_code );

		?>
		<h1><?php esc_html_e( 'Shipping', 'woocommerce' ); ?></h1>
		<?php if ( $intro_text ) : ?>
			<p><?php echo wp_kses_post( $intro_text ); ?></p>
		<?php endif; ?>
		<form method="post">
			<?php if ( $is_wcs_labels_supported || $is_shipstation_supported ) : ?>
				<ul class="wc-setup-shipping-recommended">
				<?php
				if ( $is_wcs_labels_supported ) :
					$this->display_recommended_item(
						array(
							'type'        => 'woocommerce_services',
							'title'       => __( 'Did you know you can print shipping labels at home?', 'woocommerce' ),
							'description' => __( 'Use WooCommerce Shipping (powered by WooCommerce Services & Jetpack) to save time at the post office by printing your shipping labels at home.', 'woocommerce' ),
							'img_url'     => WC()->plugin_url() . '/assets/images/obw-woocommerce-services-icon.png',
							'img_alt'     => __( 'WooCommerce Services icon', 'woocommerce' ),
							'plugins'     => $this->get_wcs_requisite_plugins(),
						)
					);
				elseif ( $is_shipstation_supported ) :
					$this->display_recommended_item(
						array(
							'type'        => 'shipstation',
							'title'       => __( 'Did you know you can print shipping labels at home?', 'woocommerce' ),
							'description' => __( 'We recommend using ShipStation to save time at the post office by printing your shipping labels at home. Try ShipStation free for 30 days.', 'woocommerce' ),
							'img_url'     => WC()->plugin_url() . '/assets/images/obw-shipstation-icon.png',
							'img_alt'     => __( 'ShipStation icon', 'woocommerce' ),
							'plugins'     => array(
								array(
									'name' => __( 'ShipStation', 'woocommerce' ),
									'slug' => 'woocommerce-shipstation-integration',
								),
							),
						)
					);
				endif;
				?>
				</ul>
			<?php endif; ?>

			<?php if ( empty( $existing_zones ) ) : ?>
				<ul class="wc-wizard-services shipping">
					<li class="wc-wizard-service-item">
						<div class="wc-wizard-service-name">
							<p><?php echo esc_html_e( 'Shipping Zone', 'woocommerce' ); ?></p>
						</div>
						<div class="wc-wizard-service-description">
							<p><?php echo esc_html_e( 'Shipping Method', 'woocommerce' ); ?></p>
						</div>
					</li>
					<li class="wc-wizard-service-item">
						<div class="wc-wizard-service-name">
							<p><?php echo esc_html( $country_name ); ?></p>
						</div>
						<div class="wc-wizard-service-description">
							<?php $this->shipping_method_selection_form( $country_code, $currency_code, 'shipping_zones[domestic]' ); ?>
						</div>
						<div class="wc-wizard-service-enable">
							<span class="wc-wizard-service-toggle">
								<input id="shipping_zones[domestic][enabled]" type="checkbox" name="shipping_zones[domestic][enabled]" value="yes" checked="checked" class="wc-wizard-shipping-method-enable" data-plugins="true" />
								<label for="shipping_zones[domestic][enabled]">
							</span>
						</div>
					</li>
					<li class="wc-wizard-service-item">
						<div class="wc-wizard-service-name">
							<p><?php echo esc_html_e( 'Locations not covered by your other zones', 'woocommerce' ); ?></p>
						</div>
						<div class="wc-wizard-service-description">
							<?php $this->shipping_method_selection_form( $country_code, $currency_code, 'shipping_zones[intl]' ); ?>
						</div>
						<div class="wc-wizard-service-enable">
							<span class="wc-wizard-service-toggle">
								<input id="shipping_zones[intl][enabled]" type="checkbox" name="shipping_zones[intl][enabled]" value="yes" checked="checked" class="wc-wizard-shipping-method-enable" data-plugins="true" />
								<label for="shipping_zones[intl][enabled]">
							</span>
						</div>
					</li>
					<li class="wc-wizard-service-info">
						<p>
						<?php
						printf(
							wp_kses(
								/* translators: %1$s: live rates tooltip text, %2$s: shipping extensions URL */
								__( 'If you\'d like to offer <span class="help_tip" data-tip="%1$s">live rates</span> from a specific carrier (e.g. UPS) you can find a variety of extensions available for WooCommerce <a href="%2$s" target="_blank">here</a>.', 'woocommerce' ),
								array(
									'span' => array(
										'class'    => array(),
										'data-tip' => array(),
									),
									'a'    => array(
										'href'   => array(),
										'target' => array(),
									),
								)
							),
							esc_attr__( 'A live rate is the exact cost to ship an order, quoted directly from the shipping carrier.', 'woocommerce' ),
							'https://woocommerce.com/product-category/woocommerce-extensions/shipping-methods/shipping-carriers/'
						);
						?>
						</p>
					</li>
				</ul>
			<?php endif; ?>

			<div class="wc-setup-shipping-units">
				<p>
					<?php
						echo wp_kses(
							sprintf(
								/* translators: %1$s: weight unit dropdown, %2$s: dimension unit dropdown */
								esc_html__( 'We\'ll use %1$s for product weight and %2$s for product dimensions.', 'woocommerce' ),
								$this->get_product_weight_selection(),
								$this->get_product_dimension_selection()
							),
							array(
								'span'   => array(
									'class' => array(),
								),
								'select' => array(
									'id'    => array(),
									'name'  => array(),
									'class' => array(),
								),
								'option' => array(
									'value'    => array(),
									'selected' => array(),
								),
							)
						);
					?>
				</p>
			</div>

			<p class="wc-setup-actions step">
				<?php $this->plugin_install_info(); ?>
				<button class="button-primary button button-large button-next" value="<?php esc_attr_e( 'Continue', 'woocommerce' ); ?>" name="save_step"><?php esc_html_e( 'Continue', 'woocommerce' ); ?></button>
				<?php wp_nonce_field( 'wc-setup' ); ?>
			</p>
		</form>
		<?php
	}

	/**
	 * Save shipping options.
	 *
	 * @deprecated 4.6.0
	 */
	public function wc_setup_shipping_save() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
	}

	/**
	 * Is Stripe country supported
	 * https://stripe.com/global .
	 *
	 * @param string $country_code Country code.
	 *
	 * @deprecated 4.6.0
	 */
	protected function is_stripe_supported_country( $country_code ) {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$stripe_supported_countries = array(
			'AU',
			'AT',
			'BE',
			'CA',
			'DK',
			'FI',
			'FR',
			'DE',
			'HK',
			'IE',
			'JP',
			'LU',
			'NL',
			'NZ',
			'NO',
			'SG',
			'ES',
			'SE',
			'CH',
			'GB',
			'US',
		);

		return in_array( $country_code, $stripe_supported_countries, true );
	}

	/**
	 * Is PayPal currency supported.
	 *
	 * @param string $currency Currency code.
	 * @return boolean
	 *
	 * @deprecated 4.6.0
	 */
	protected function is_paypal_supported_currency( $currency ) {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$supported_currencies = array(
			'AUD',
			'BRL',
			'CAD',
			'MXN',
			'NZD',
			'HKD',
			'SGD',
			'USD',
			'EUR',
			'JPY',
			'TRY',
			'NOK',
			'CZK',
			'DKK',
			'HUF',
			'ILS',
			'MYR',
			'PHP',
			'PLN',
			'SEK',
			'CHF',
			'TWD',
			'THB',
			'GBP',
			'RMB',
			'RUB',
			'INR',
		);
		return in_array( $currency, $supported_currencies, true );
	}

	/**
	 * Is Klarna Checkout country supported.
	 *
	 * @param string $country_code Country code.
	 *
	 * @deprecated 4.6.0
	 */
	protected function is_klarna_checkout_supported_country( $country_code ) {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$supported_countries = array(
			'SE', // Sweden.
			'FI', // Finland.
			'NO', // Norway.
			'NL', // Netherlands.
		);
		return in_array( $country_code, $supported_countries, true );
	}

	/**
	 * Is Klarna Payments country supported.
	 *
	 * @param string $country_code Country code.
	 *
	 * @deprecated 4.6.0
	 */
	protected function is_klarna_payments_supported_country( $country_code ) {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$supported_countries = array(
			'DK', // Denmark.
			'DE', // Germany.
			'AT', // Austria.
		);
		return in_array( $country_code, $supported_countries, true );
	}

	/**
	 * Is Square country supported
	 *
	 * @param string $country_code Country code.
	 *
	 * @deprecated 4.6.0
	 */
	protected function is_square_supported_country( $country_code ) {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$square_supported_countries = array(
			'US',
			'CA',
			'JP',
			'GB',
			'AU',
		);
		return in_array( $country_code, $square_supported_countries, true );
	}

	/**
	 * Is eWAY Payments country supported
	 *
	 * @param string $country_code Country code.
	 *
	 * @deprecated 4.6.0
	 */
	protected function is_eway_payments_supported_country( $country_code ) {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$supported_countries = array(
			'AU', // Australia.
			'NZ', // New Zealand.
		);
		return in_array( $country_code, $supported_countries, true );
	}

	/**
	 * Is ShipStation country supported
	 *
	 * @param string $country_code Country code.
	 *
	 * @deprecated 4.6.0
	 */
	protected function is_shipstation_supported_country( $country_code ) {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$supported_countries = array(
			'AU', // Australia.
			'CA', // Canada.
			'GB', // United Kingdom.
		);
		return in_array( $country_code, $supported_countries, true );
	}

	/**
	 * Is WooCommerce Services shipping label country supported
	 *
	 * @param string $country_code Country code.
	 *
	 * @deprecated 4.6.0
	 */
	protected function is_wcs_shipping_labels_supported_country( $country_code ) {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$supported_countries = array(
			'US', // United States.
		);
		return in_array( $country_code, $supported_countries, true );
	}

	/**
	 * Helper method to retrieve the current user's email address.
	 *
	 * @deprecated 4.6.0
	 * @return string Email address
	 */
	protected function get_current_user_email() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$current_user = wp_get_current_user();
		$user_email   = $current_user->user_email;

		return $user_email;
	}

	/**
	 * Array of all possible "in cart" gateways that can be offered.
	 *
	 * @deprecated 4.6.0
	 * @return array
	 */
	protected function get_wizard_available_in_cart_payment_gateways() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$user_email = $this->get_current_user_email();

		$stripe_description = '<p>' . sprintf(
			/* translators: %s: URL */
			__( 'Accept debit and credit cards in 135+ currencies, methods such as Alipay, and one-touch checkout with Apple Pay. <a href="%s" target="_blank">Learn more</a>.', 'woocommerce' ),
			'https://woocommerce.com/products/stripe/'
		) . '</p>';
		$paypal_checkout_description = '<p>' . sprintf(
			/* translators: %s: URL */
			__( 'Safe and secure payments using credit cards or your customer\'s PayPal account. <a href="%s" target="_blank">Learn more</a>.', 'woocommerce' ),
			'https://woocommerce.com/products/woocommerce-gateway-paypal-checkout/'
		) . '</p>';
		$klarna_checkout_description = '<p>' . sprintf(
			/* translators: %s: URL */
			__( 'Full checkout experience with pay now, pay later and slice it. No credit card numbers, no passwords, no worries. <a href="%s" target="_blank">Learn more about Klarna</a>.', 'woocommerce' ),
			'https://woocommerce.com/products/klarna-checkout/'
		) . '</p>';
		$klarna_payments_description = '<p>' . sprintf(
			/* translators: %s: URL */
			__( 'Choose the payment that you want, pay now, pay later or slice it. No credit card numbers, no passwords, no worries. <a href="%s" target="_blank">Learn more about Klarna</a>.', 'woocommerce' ),
			'https://woocommerce.com/products/klarna-payments/ '
		) . '</p>';
		$square_description = '<p>' . sprintf(
			/* translators: %s: URL */
			__( 'Securely accept credit and debit cards with one low rate, no surprise fees (custom rates available). Sell online and in store and track sales and inventory in one place. <a href="%s" target="_blank">Learn more about Square</a>.', 'woocommerce' ),
			'https://woocommerce.com/products/square/'
		) . '</p>';

		return array(
			'stripe'          => array(
				'name'        => __( 'WooCommerce Stripe Gateway', 'woocommerce' ),
				'image'       => WC()->plugin_url() . '/assets/images/stripe.png',
				'description' => $stripe_description,
				'class'       => 'checked stripe-logo',
				'repo-slug'   => 'woocommerce-gateway-stripe',
				'settings'    => array(
					'create_account' => array(
						'label'       => __( 'Set up Stripe for me using this email:', 'woocommerce' ),
						'type'        => 'checkbox',
						'value'       => 'yes',
						'default'     => 'yes',
						'placeholder' => '',
						'required'    => false,
						'plugins'     => $this->get_wcs_requisite_plugins(),
					),
					'email'          => array(
						'label'       => __( 'Stripe email address:', 'woocommerce' ),
						'type'        => 'email',
						'value'       => $user_email,
						'placeholder' => __( 'Stripe email address', 'woocommerce' ),
						'required'    => true,
					),
				),
			),
			'ppec_paypal'     => array(
				'name'        => __( 'WooCommerce PayPal Checkout Gateway', 'woocommerce' ),
				'image'       => WC()->plugin_url() . '/assets/images/paypal.png',
				'description' => $paypal_checkout_description,
				'enabled'     => false,
				'class'       => 'checked paypal-logo',
				'repo-slug'   => 'woocommerce-gateway-paypal-express-checkout',
				'settings'    => array(
					'reroute_requests' => array(
						'label'       => __( 'Set up PayPal for me using this email:', 'woocommerce' ),
						'type'        => 'checkbox',
						'value'       => 'yes',
						'default'     => 'yes',
						'placeholder' => '',
						'required'    => false,
						'plugins'     => $this->get_wcs_requisite_plugins(),
					),
					'email'            => array(
						'label'       => __( 'Direct payments to email address:', 'woocommerce' ),
						'type'        => 'email',
						'value'       => $user_email,
						'placeholder' => __( 'Email address to receive payments', 'woocommerce' ),
						'required'    => true,
					),
				),
			),
			'paypal'          => array(
				'name'        => __( 'PayPal Standard', 'woocommerce' ),
				'description' => __( 'Accept payments via PayPal using account balance or credit card.', 'woocommerce' ),
				'image'       => '',
				'settings'    => array(
					'email' => array(
						'label'       => __( 'PayPal email address:', 'woocommerce' ),
						'type'        => 'email',
						'value'       => $user_email,
						'placeholder' => __( 'PayPal email address', 'woocommerce' ),
						'required'    => true,
					),
				),
			),
			'klarna_checkout' => array(
				'name'        => __( 'Klarna Checkout for WooCommerce', 'woocommerce' ),
				'description' => $klarna_checkout_description,
				'image'       => WC()->plugin_url() . '/assets/images/klarna-black.png',
				'enabled'     => true,
				'class'       => 'klarna-logo',
				'repo-slug'   => 'klarna-checkout-for-woocommerce',
			),
			'klarna_payments' => array(
				'name'        => __( 'Klarna Payments for WooCommerce', 'woocommerce' ),
				'description' => $klarna_payments_description,
				'image'       => WC()->plugin_url() . '/assets/images/klarna-black.png',
				'enabled'     => true,
				'class'       => 'klarna-logo',
				'repo-slug'   => 'klarna-payments-for-woocommerce',
			),
			'square'          => array(
				'name'        => __( 'WooCommerce Square', 'woocommerce' ),
				'description' => $square_description,
				'image'       => WC()->plugin_url() . '/assets/images/square-black.png',
				'class'       => 'square-logo',
				'enabled'     => false,
				'repo-slug'   => 'woocommerce-square',
			),
			'eway'            => array(
				'name'        => __( 'WooCommerce eWAY Gateway', 'woocommerce' ),
				'description' => __( 'The eWAY extension for WooCommerce allows you to take credit card payments directly on your store without redirecting your customers to a third party site to make payment.', 'woocommerce' ),
				'image'       => WC()->plugin_url() . '/assets/images/eway-logo.jpg',
				'enabled'     => false,
				'class'       => 'eway-logo',
				'repo-slug'   => 'woocommerce-gateway-eway',
			),
			'payfast'         => array(
				'name'        => __( 'WooCommerce PayFast Gateway', 'woocommerce' ),
				'description' => __( 'The PayFast extension for WooCommerce enables you to accept payments by Credit Card and EFT via one of South Africa’s most popular payment gateways. No setup fees or monthly subscription costs.', 'woocommerce' ),
				'image'       => WC()->plugin_url() . '/assets/images/payfast.png',
				'class'       => 'payfast-logo',
				'enabled'     => false,
				'repo-slug'   => 'woocommerce-payfast-gateway',
				'file'        => 'gateway-payfast.php',
			),
		);
	}

	/**
	 * Simple array of "in cart" gateways to show in wizard.
	 *
	 * @deprecated 4.6.0
	 * @return array
	 */
	public function get_wizard_in_cart_payment_gateways() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$gateways = $this->get_wizard_available_in_cart_payment_gateways();
		$country  = WC()->countries->get_base_country();
		$currency = get_woocommerce_currency();

		$can_stripe  = $this->is_stripe_supported_country( $country );
		$can_eway    = $this->is_eway_payments_supported_country( $country );
		$can_payfast = ( 'ZA' === $country ); // South Africa.
		$can_paypal  = $this->is_paypal_supported_currency( $currency );

		if ( ! current_user_can( 'install_plugins' ) ) {
			return $can_paypal ? array( 'paypal' => $gateways['paypal'] ) : array();
		}

		$klarna_or_square = false;

		if ( $this->is_klarna_checkout_supported_country( $country ) ) {
			$klarna_or_square = 'klarna_checkout';
		} elseif ( $this->is_klarna_payments_supported_country( $country ) ) {
			$klarna_or_square = 'klarna_payments';
		} elseif ( $this->is_square_supported_country( $country ) && get_option( 'woocommerce_sell_in_person' ) ) {
			$klarna_or_square = 'square';
		}

		$offered_gateways = array();

		if ( $can_stripe ) {
			$gateways['stripe']['enabled']  = true;
			$gateways['stripe']['featured'] = true;
			$offered_gateways              += array( 'stripe' => $gateways['stripe'] );
		} elseif ( $can_paypal ) {
			$gateways['ppec_paypal']['enabled'] = true;
		}

		if ( $klarna_or_square ) {
			if ( in_array( $klarna_or_square, array( 'klarna_checkout', 'klarna_payments' ), true ) ) {
				$gateways[ $klarna_or_square ]['enabled']  = true;
				$gateways[ $klarna_or_square ]['featured'] = false;
				$offered_gateways                         += array(
					$klarna_or_square => $gateways[ $klarna_or_square ],
				);
			} else {
				$offered_gateways += array(
					$klarna_or_square => $gateways[ $klarna_or_square ],
				);
			}
		}

		if ( $can_paypal ) {
			$offered_gateways += array( 'ppec_paypal' => $gateways['ppec_paypal'] );
		}

		if ( $can_eway ) {
			$offered_gateways += array( 'eway' => $gateways['eway'] );
		}

		if ( $can_payfast ) {
			$offered_gateways += array( 'payfast' => $gateways['payfast'] );
		}

		return $offered_gateways;
	}

	/**
	 * Simple array of "manual" gateways to show in wizard.
	 *
	 * @deprecated 4.6.0
	 * @return array
	 */
	public function get_wizard_manual_payment_gateways() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$gateways = array(
			'cheque' => array(
				'name'        => _x( 'Check payments', 'Check payment method', 'woocommerce' ),
				'description' => __( 'A simple offline gateway that lets you accept a check as method of payment.', 'woocommerce' ),
				'image'       => '',
				'class'       => '',
			),
			'bacs'   => array(
				'name'        => __( 'Bank transfer (BACS) payments', 'woocommerce' ),
				'description' => __( 'A simple offline gateway that lets you accept BACS payment.', 'woocommerce' ),
				'image'       => '',
				'class'       => '',
			),
			'cod'    => array(
				'name'        => __( 'Cash on delivery', 'woocommerce' ),
				'description' => __( 'A simple offline gateway that lets you accept cash on delivery.', 'woocommerce' ),
				'image'       => '',
				'class'       => '',
			),
		);

		return $gateways;
	}

	/**
	 * Display service item in list.
	 *
	 * @param int   $item_id Item ID.
	 * @param array $item_info Item info array.
	 *
	 * @deprecated 4.6.0
	 */
	public function display_service_item( $item_id, $item_info ) {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$item_class = 'wc-wizard-service-item';
		if ( isset( $item_info['class'] ) ) {
			$item_class .= ' ' . $item_info['class'];
		}

		$previously_saved_settings = get_option( 'woocommerce_' . $item_id . '_settings' );

		// Show the user-saved state if it was previously saved.
		// Otherwise, rely on the item info.
		if ( is_array( $previously_saved_settings ) ) {
			$should_enable_toggle = ( isset( $previously_saved_settings['enabled'] ) && 'yes' === $previously_saved_settings['enabled'] ) ? true : ( isset( $item_info['enabled'] ) && $item_info['enabled'] );
		} else {
			$should_enable_toggle = isset( $item_info['enabled'] ) && $item_info['enabled'];
		}

		$plugins = null;
		if ( isset( $item_info['repo-slug'] ) ) {
			$plugin  = array(
				'slug' => $item_info['repo-slug'],
				'name' => $item_info['name'],
			);
			$plugins = array( $plugin );
		}

		?>
		<li class="<?php echo esc_attr( $item_class ); ?>">
			<div class="wc-wizard-service-name">
				<?php if ( ! empty( $item_info['image'] ) ) : ?>
					<img src="<?php echo esc_attr( $item_info['image'] ); ?>" alt="<?php echo esc_attr( $item_info['name'] ); ?>" />
				<?php else : ?>
					<p><?php echo esc_html( $item_info['name'] ); ?></p>
				<?php endif; ?>
			</div>
			<div class="wc-wizard-service-enable">
				<span class="wc-wizard-service-toggle <?php echo esc_attr( $should_enable_toggle ? '' : 'disabled' ); ?>" tabindex="0">
					<input
						id="wc-wizard-service-<?php echo esc_attr( $item_id ); ?>"
						type="checkbox"
						name="wc-wizard-service-<?php echo esc_attr( $item_id ); ?>-enabled"
						value="yes" <?php checked( $should_enable_toggle ); ?>
						data-plugins="<?php echo wc_esc_json( wp_json_encode( $plugins ) ); ?>"
					/>
					<label for="wc-wizard-service-<?php echo esc_attr( $item_id ); ?>">
				</span>
			</div>
			<div class="wc-wizard-service-description">
				<?php echo wp_kses_post( wpautop( $item_info['description'] ) ); ?>
				<?php if ( ! empty( $item_info['settings'] ) ) : ?>
					<div class="wc-wizard-service-settings <?php echo $should_enable_toggle ? '' : 'hide'; ?>">
						<?php foreach ( $item_info['settings'] as $setting_id => $setting ) : ?>
							<?php
							$is_checkbox = 'checkbox' === $setting['type'];

							if ( $is_checkbox ) {
								$checked = false;
								if ( isset( $previously_saved_settings[ $setting_id ] ) ) {
									$checked = 'yes' === $previously_saved_settings[ $setting_id ];
								} elseif ( false === $previously_saved_settings && isset( $setting['default'] ) ) {
									$checked = 'yes' === $setting['default'];
								}
							}
							if ( 'email' === $setting['type'] ) {
								$value = empty( $previously_saved_settings[ $setting_id ] )
									? $setting['value']
									: $previously_saved_settings[ $setting_id ];
							}
							?>
							<?php $input_id = $item_id . '_' . $setting_id; ?>
							<div class="<?php echo esc_attr( 'wc-wizard-service-setting-' . $input_id ); ?>">
								<label
									for="<?php echo esc_attr( $input_id ); ?>"
									class="<?php echo esc_attr( $input_id ); ?>"
								>
									<?php echo esc_html( $setting['label'] ); ?>
								</label>
								<input
									type="<?php echo esc_attr( $setting['type'] ); ?>"
									id="<?php echo esc_attr( $input_id ); ?>"
									class="<?php echo esc_attr( 'payment-' . $setting['type'] . '-input' ); ?>"
									name="<?php echo esc_attr( $input_id ); ?>"
									value="<?php echo esc_attr( isset( $value ) ? $value : $setting['value'] ); ?>"
									placeholder="<?php echo esc_attr( $setting['placeholder'] ); ?>"
									<?php echo ( $setting['required'] ) ? 'required' : ''; ?>
									<?php echo $is_checkbox ? checked( isset( $checked ) && $checked, true, false ) : ''; ?>
									data-plugins="<?php echo wc_esc_json( wp_json_encode( isset( $setting['plugins'] ) ? $setting['plugins'] : null ) ); ?>"
								/>
								<?php if ( ! empty( $setting['description'] ) ) : ?>
									<span class="wc-wizard-service-settings-description"><?php echo esc_html( $setting['description'] ); ?></span>
								<?php endif; ?>
							</div>
						<?php endforeach; ?>
					</div>
				<?php endif; ?>
			</div>
		</li>
		<?php
	}

	/**
	 * Is it a featured service?
	 *
	 * @param array $service Service info array.
	 *
	 * @deprecated 4.6.0
	 * @return boolean
	 */
	public function is_featured_service( $service ) {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		return ! empty( $service['featured'] );
	}

	/**
	 * Is this a non featured service?
	 *
	 * @param array $service Service info array.
	 *
	 * @deprecated 4.6.0
	 * @return boolean
	 */
	public function is_not_featured_service( $service ) {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		return ! $this->is_featured_service( $service );
	}

	/**
	 * Payment Step.
	 *
	 * @deprecated 4.6.0
	 */
	public function wc_setup_payment() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$featured_gateways = array_filter( $this->get_wizard_in_cart_payment_gateways(), array( $this, 'is_featured_service' ) );
		$in_cart_gateways  = array_filter( $this->get_wizard_in_cart_payment_gateways(), array( $this, 'is_not_featured_service' ) );
		$manual_gateways   = $this->get_wizard_manual_payment_gateways();
		?>
		<h1><?php esc_html_e( 'Payment', 'woocommerce' ); ?></h1>
		<form method="post" class="wc-wizard-payment-gateway-form">
			<p>
				<?php
				printf(
					wp_kses(
						/* translators: %s: Link */
						__( 'WooCommerce can accept both online and offline payments. <a href="%s" target="_blank">Additional payment methods</a> can be installed later.', 'woocommerce' ),
						array(
							'a' => array(
								'href'   => array(),
								'target' => array(),
							),
						)
					),
					esc_url( admin_url( 'admin.php?page=wc-addons&section=payment-gateways' ) )
				);
				?>
			</p>
			<?php if ( $featured_gateways ) : ?>
			<ul class="wc-wizard-services featured">
				<?php
				foreach ( $featured_gateways as $gateway_id => $gateway ) {
					$this->display_service_item( $gateway_id, $gateway );
				}
				?>
			</ul>
			<?php endif; ?>
			<?php if ( $in_cart_gateways ) : ?>
			<ul class="wc-wizard-services in-cart">
				<?php
				foreach ( $in_cart_gateways as $gateway_id => $gateway ) {
					$this->display_service_item( $gateway_id, $gateway );
				}
				?>
			</ul>
			<?php endif; ?>
			<ul class="wc-wizard-services manual">
				<li class="wc-wizard-services-list-toggle closed">
					<div class="wc-wizard-service-name">
						<?php esc_html_e( 'Offline Payments', 'woocommerce' ); ?>
					</div>
					<div class="wc-wizard-service-description">
						<?php esc_html_e( 'Collect payments from customers offline.', 'woocommerce' ); ?>
					</div>
					<div class="wc-wizard-service-enable" tabindex="0">
						<input class="wc-wizard-service-list-toggle" id="wc-wizard-service-list-toggle" type="checkbox">
						<label for="wc-wizard-service-list-toggle"></label>
					</div>
				</li>
				<?php
				foreach ( $manual_gateways as $gateway_id => $gateway ) {
					$this->display_service_item( $gateway_id, $gateway );
				}
				?>
			</ul>
			<p class="wc-setup-actions step">
				<?php $this->plugin_install_info(); ?>
				<button type="submit" class="button-primary button button-large button-next" value="<?php esc_attr_e( 'Continue', 'woocommerce' ); ?>" name="save_step"><?php esc_html_e( 'Continue', 'woocommerce' ); ?></button>
				<?php wp_nonce_field( 'wc-setup' ); ?>
			</p>
		</form>
		<?php
	}

	/**
	 * Payment Step save.
	 *
	 * @deprecated 4.6.0
	 */
	public function wc_setup_payment_save() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
	}

	protected function display_recommended_item( $item_info ) {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$type        = $item_info['type'];
		$title       = $item_info['title'];
		$description = $item_info['description'];
		$img_url     = $item_info['img_url'];
		$img_alt     = $item_info['img_alt'];
		?>
		<li class="recommended-item checkbox">
			<input
				id="<?php echo esc_attr( 'wc_recommended_' . $type ); ?>"
				type="checkbox"
				name="<?php echo esc_attr( 'setup_' . $type ); ?>"
				value="yes"
				checked
				data-plugins="<?php echo wc_esc_json( wp_json_encode( isset( $item_info['plugins'] ) ? $item_info['plugins'] : null ) ); ?>"
			/>
			<label for="<?php echo esc_attr( 'wc_recommended_' . $type ); ?>">
				<img
					src="<?php echo esc_url( $img_url ); ?>"
					class="<?php echo esc_attr( 'recommended-item-icon-' . $type ); ?> recommended-item-icon"
					alt="<?php echo esc_attr( $img_alt ); ?>" />
				<div class="recommended-item-description-container">
					<h3><?php echo esc_html( $title ); ?></h3>
					<p><?php echo wp_kses( $description, array(
						'a' => array(
							'href'   => array(),
							'target' => array(),
							'rel'    => array(),
						),
						'em' => array(),
					) ); ?></p>
				</div>
			</label>
		</li>
		<?php
	}

	/**
	 * Recommended step
	 *
	 * @deprecated 4.6.0
	 */
	public function wc_setup_recommended() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		?>
		<h1><?php esc_html_e( 'Recommended for All WooCommerce Stores', 'woocommerce' ); ?></h1>
		<p>
			<?php esc_html_e( 'Enhance your store with these recommended free features.', 'woocommerce' ); ?>
		</p>
		<form method="post">
			<ul class="recommended-step">
				<?php
				if ( $this->should_show_theme() ) :
					$theme      = wp_get_theme();
					$theme_name = $theme['Name'];
					$this->display_recommended_item( array(
						'type'        => 'storefront_theme',
						'title'       => __( 'Storefront Theme', 'woocommerce' ),
						'description' => sprintf( __(
								'Design your store with deep WooCommerce integration. If toggled on, we’ll install <a href="https://woocommerce.com/storefront/" target="_blank" rel="noopener noreferrer">Storefront</a>, and your current theme <em>%s</em> will be deactivated.', 'woocommerce' ),
								$theme_name
						),
						'img_url'     => WC()->plugin_url() . '/assets/images/obw-storefront-icon.svg',
						'img_alt'     => __( 'Storefront icon', 'woocommerce' ),
					) );
				endif;

				if ( $this->should_show_automated_tax() ) :
					$this->display_recommended_item( array(
						'type'        => 'automated_taxes',
						'title'       => __( 'Automated Taxes', 'woocommerce' ),
						'description' => __( 'Save time and errors with automated tax calculation and collection at checkout. Powered by WooCommerce Services and Jetpack.', 'woocommerce' ),
						'img_url'     => WC()->plugin_url() . '/assets/images/obw-taxes-icon.svg',
						'img_alt'     => __( 'automated taxes icon', 'woocommerce' ),
						'plugins'     => $this->get_wcs_requisite_plugins(),
					) );
				endif;

				if ( $this->should_show_wc_admin() ) :
					$this->display_recommended_item( array(
						'type'        => 'wc_admin',
						'title'       => __( 'WooCommerce Admin', 'woocommerce' ),
						'description' => __( 'Manage your store\'s reports and monitor key metrics with a new and improved interface and dashboard.', 'woocommerce' ),
						'img_url'     => WC()->plugin_url() . '/assets/images/obw-woocommerce-admin-icon.svg',
						'img_alt'     => __( 'WooCommerce Admin icon', 'woocommerce' ),
						'plugins'     => array( array( 'name' => __( 'WooCommerce Admin', 'woocommerce' ), 'slug' => 'woocommerce-admin' ) ),
					) );
				endif;

				if ( $this->should_show_mailchimp() ) :
					$this->display_recommended_item( array(
						'type'        => 'mailchimp',
						'title'       => __( 'Mailchimp', 'woocommerce' ),
						'description' => __( 'Join the 16 million customers who use Mailchimp. Sync list and store data to send automated emails, and targeted campaigns.', 'woocommerce' ),
						'img_url'     => WC()->plugin_url() . '/assets/images/obw-mailchimp-icon.svg',
						'img_alt'     => __( 'Mailchimp icon', 'woocommerce' ),
						'plugins'     => array( array( 'name' => __( 'Mailchimp for WooCommerce', 'woocommerce' ), 'slug' => 'mailchimp-for-woocommerce' ) ),
					) );
				endif;

				if ( $this->should_show_facebook() ) :
					$this->display_recommended_item( array(
						'type'        => 'facebook',
						'title'       => __( 'Facebook', 'woocommerce' ),
						'description' => __( 'Enjoy all Facebook products combined in one extension: pixel tracking, catalog sync, messenger chat, shop functionality and Instagram shopping (coming soon)!', 'woocommerce' ),
						'img_url'     => WC()->plugin_url() . '/assets/images/obw-facebook-icon.svg',
						'img_alt'     => __( 'Facebook icon', 'woocommerce' ),
						'plugins'     => array( array( 'name' => __( 'Facebook for WooCommerce', 'woocommerce' ), 'slug' => 'facebook-for-woocommerce' ) ),
					) );
				endif;
			?>
		</ul>
			<p class="wc-setup-actions step">
				<?php $this->plugin_install_info(); ?>
				<button type="submit" class="button-primary button button-large button-next" value="<?php esc_attr_e( 'Continue', 'woocommerce' ); ?>" name="save_step"><?php esc_html_e( 'Continue', 'woocommerce' ); ?></button>
				<?php wp_nonce_field( 'wc-setup' ); ?>
			</p>
		</form>
		<?php
	}

	/**
	 * Recommended step save.
	 *
	 * @deprecated 4.6.0
	 */
	public function wc_setup_recommended_save() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
	}

	/**
	 * Go to the next step if Jetpack was connected.
	 */
	protected function wc_setup_activate_actions() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		if (
			isset( $_GET['from'] ) &&
			'wpcom' === $_GET['from'] &&
			class_exists( 'Jetpack' ) &&
			Jetpack::is_active()
		) {
			wp_redirect( esc_url_raw( remove_query_arg( 'from', $this->get_next_step_link() ) ) );
			exit;
		}
	}

	/**
	 *
	 * @deprecated 4.6.0
	 */
	protected function wc_setup_activate_get_feature_list() {
		$features = array();

		$stripe_settings = get_option( 'woocommerce_stripe_settings', false );
		$stripe_enabled  = is_array( $stripe_settings )
			&& isset( $stripe_settings['create_account'] ) && 'yes' === $stripe_settings['create_account']
			&& isset( $stripe_settings['enabled'] ) && 'yes' === $stripe_settings['enabled'];
		$ppec_settings   = get_option( 'woocommerce_ppec_paypal_settings', false );
		$ppec_enabled    = is_array( $ppec_settings )
			&& isset( $ppec_settings['reroute_requests'] ) && 'yes' === $ppec_settings['reroute_requests']
			&& isset( $ppec_settings['enabled'] ) && 'yes' === $ppec_settings['enabled'];

		$features['payment'] = $stripe_enabled || $ppec_enabled;
		$features['taxes']   = (bool) get_option( 'woocommerce_setup_automated_taxes', false );
		$features['labels']  = (bool) get_option( 'woocommerce_setup_shipping_labels', false );

		return $features;
	}

	/**
	 *
	 * @deprecated 4.6.0
	 */
	protected function wc_setup_activate_get_feature_list_str() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$features = $this->wc_setup_activate_get_feature_list();
		if ( $features['payment'] && $features['taxes'] && $features['labels'] ) {
			return __( 'payment setup, automated taxes and discounted shipping labels', 'woocommerce' );
		} else if ( $features['payment'] && $features['taxes'] ) {
			return __( 'payment setup and automated taxes', 'woocommerce' );
		} else if ( $features['payment'] && $features['labels'] ) {
			return __( 'payment setup and discounted shipping labels', 'woocommerce' );
		} else if ( $features['payment'] ) {
			return __( 'payment setup', 'woocommerce' );
		} else if ( $features['taxes'] && $features['labels'] ) {
			return __( 'automated taxes and discounted shipping labels', 'woocommerce' );
		} else if ( $features['taxes'] ) {
			return __( 'automated taxes', 'woocommerce' );
		} else if ( $features['labels'] ) {
			return __( 'discounted shipping labels', 'woocommerce' );
		}
		return false;
	}

	/**
	 * Activate step.
	 *
	 * @deprecated 4.6.0
	 */
	public function wc_setup_activate() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$this->wc_setup_activate_actions();

		$jetpack_connected = class_exists( 'Jetpack' ) && Jetpack::is_active();

		$has_jetpack_error = false;
		if ( isset( $_GET['activate_error'] ) ) {
			$has_jetpack_error = true;

			$title = __( "Sorry, we couldn't connect your store to Jetpack", 'woocommerce' );

			$error_message = $this->get_activate_error_message( sanitize_text_field( wp_unslash( $_GET['activate_error'] ) ) );
			$description = $error_message;
		} else {
			$feature_list = $this->wc_setup_activate_get_feature_list_str();

			$description = false;

			if ( $feature_list ) {
				if ( ! $jetpack_connected ) {
					/* translators: %s: list of features, potentially comma separated */
					$description_base = __( 'Your store is almost ready! To activate services like %s, just connect with Jetpack.', 'woocommerce' );
				} else {
					$description_base = __( 'Thanks for using Jetpack! Your store is almost ready: to activate services like %s, just connect your store.', 'woocommerce' );
				}
				$description = sprintf( $description_base, $feature_list );
			}

			if ( ! $jetpack_connected ) {
				$title = $feature_list ?
					__( 'Connect your store to Jetpack', 'woocommerce' ) :
					__( 'Connect your store to Jetpack to enable extra features', 'woocommerce' );
				$button_text = __( 'Continue with Jetpack', 'woocommerce' );
			} elseif ( $feature_list ) {
				$title = __( 'Connect your store to activate WooCommerce Services', 'woocommerce' );
				$button_text = __( 'Continue with WooCommerce Services', 'woocommerce' );
			} else {
				wp_redirect( esc_url_raw( $this->get_next_step_link() ) );
				exit;
			}
		}
		?>
		<h1><?php echo esc_html( $title ); ?></h1>
		<p><?php echo esc_html( $description ); ?></p>

		<?php if ( $jetpack_connected ) : ?>
			<div class="activate-splash">
				<img
					class="jetpack-logo"
					src="<?php echo esc_url( WC()->plugin_url() . '/assets/images/jetpack_horizontal_logo.png' ); ?>"
					alt="<?php esc_attr_e( 'Jetpack logo', 'woocommerce' ); ?>"
				/>
				<img
					class="wcs-notice"
					src="<?php echo esc_url( WC()->plugin_url() . '/assets/images/wcs-notice.png' ); ?>"
				/>
			</div>
		<?php else : ?>
			<img
				class="jetpack-logo"
				src="<?php echo esc_url( WC()->plugin_url() . '/assets/images/jetpack_vertical_logo.png' ); ?>"
				alt="<?php esc_attr_e( 'Jetpack logo', 'woocommerce' ); ?>"
			/>
		<?php endif; ?>

		<?php if ( $has_jetpack_error ) : ?>
			<p class="wc-setup-actions step">
				<a
					href="<?php echo esc_url( $this->get_next_step_link() ); ?>"
					class="button-primary button button-large"
				>
					<?php esc_html_e( 'Finish setting up your store', 'woocommerce' ); ?>
				</a>
			</p>
		<?php else : ?>
			<p class="jetpack-terms">
				<?php
					printf(
						wp_kses_post( __( 'By connecting your site you agree to our fascinating <a href="%1$s" target="_blank">Terms of Service</a> and to <a href="%2$s" target="_blank">share details</a> with WordPress.com', 'woocommerce' ) ),
						'https://wordpress.com/tos',
						'https://jetpack.com/support/what-data-does-jetpack-sync'
					);
				?>
			</p>
			<form method="post" class="activate-jetpack">
				<p class="wc-setup-actions step">
					<button type="submit" class="button-primary button button-large" value="<?php echo esc_attr( $button_text ); ?>"><?php echo esc_html( $button_text ); ?></button>
				</p>
				<input type="hidden" name="save_step" value="activate" />
				<?php wp_nonce_field( 'wc-setup' ); ?>
			</form>
			<?php if ( ! $jetpack_connected ) : ?>
				<h3 class="jetpack-reasons">
					<?php
						echo esc_html( $description ?
							__( "Bonus reasons you'll love Jetpack", 'woocommerce' ) :
							__( "Reasons you'll love Jetpack", 'woocommerce' )
						);
					?>
				</h3>
				<ul class="wc-wizard-features">
					<li class="wc-wizard-feature-item">
						<p class="wc-wizard-feature-name">
							<strong><?php esc_html_e( 'Better security', 'woocommerce' ); ?></strong>
						</p>
						<p class="wc-wizard-feature-description">
							<?php esc_html_e( 'Protect your store from unauthorized access.', 'woocommerce' ); ?>
						</p>
					</li>
					<li class="wc-wizard-feature-item">
						<p class="wc-wizard-feature-name">
							<strong><?php esc_html_e( 'Store stats', 'woocommerce' ); ?></strong>
						</p>
						<p class="wc-wizard-feature-description">
							<?php esc_html_e( 'Get insights on how your store is doing, including total sales, top products, and more.', 'woocommerce' ); ?>
						</p>
					</li>
					<li class="wc-wizard-feature-item">
						<p class="wc-wizard-feature-name">
							<strong><?php esc_html_e( 'Store monitoring', 'woocommerce' ); ?></strong>
						</p>
						<p class="wc-wizard-feature-description">
							<?php esc_html_e( 'Get an alert if your store is down for even a few minutes.', 'woocommerce' ); ?>
						</p>
					</li>
					<li class="wc-wizard-feature-item">
						<p class="wc-wizard-feature-name">
							<strong><?php esc_html_e( 'Product promotion', 'woocommerce' ); ?></strong>
						</p>
						<p class="wc-wizard-feature-description">
							<?php esc_html_e( "Share new items on social media the moment they're live in your store.", 'woocommerce' ); ?>
						</p>
					</li>
				</ul>
			<?php endif; ?>
		<?php endif; ?>
	<?php
	}

	/**
	 *
	 * @deprecated 4.6.0
	 */
	protected function get_all_activate_errors() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		return array(
			'default' => __( "Sorry! We tried, but we couldn't connect Jetpack just now 😭. Please go to the Plugins tab to connect Jetpack, so that you can finish setting up your store.", 'woocommerce' ),
			'jetpack_cant_be_installed' => __( "Sorry! We tried, but we couldn't install Jetpack for you 😭. Please go to the Plugins tab to install it, and finish setting up your store.", 'woocommerce' ),
			'register_http_request_failed' => __( "Sorry! We couldn't contact Jetpack just now 😭. Please make sure that your site is visible over the internet, and that it accepts incoming and outgoing requests via curl. You can also try to connect to Jetpack again, and if you run into any more issues, please contact support.", 'woocommerce' ),
			'siteurl_private_ip_dev' => __( "Your site might be on a private network. Jetpack can only connect to public sites. Please make sure your site is visible over the internet, and then try connecting again 🙏." , 'woocommerce' ),
		);
	}

	/**
	 *
	 * @deprecated 4.6.0
	 */
	protected function get_activate_error_message( $code = '' ) {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		$errors = $this->get_all_activate_errors();
		return array_key_exists( $code, $errors ) ? $errors[ $code ] : $errors['default'];
	}

	/**
	 * Activate step save.
	 *
	 * Install, activate, and launch connection flow for Jetpack.
	 *
	 * @deprecated 4.6.0
	 */
	public function wc_setup_activate_save() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
	}

	/**
	 * Final step.
	 *
	 * @deprecated 4.6.0
	 */
	public function wc_setup_ready() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
		// We've made it! Don't prompt the user to run the wizard again.
		WC_Admin_Notices::remove_notice( 'install', true );

		$user_email   = $this->get_current_user_email();
		$docs_url     = 'https://docs.woocommerce.com/documentation/plugins/woocommerce/getting-started/?utm_source=setupwizard&utm_medium=product&utm_content=docs&utm_campaign=woocommerceplugin';
		$help_text    = sprintf(
			/* translators: %1$s: link to docs */
			__( 'Visit WooCommerce.com to learn more about <a href="%1$s" target="_blank">getting started</a>.', 'woocommerce' ),
			$docs_url
		);
		?>
		<h1><?php esc_html_e( "You're ready to start selling!", 'woocommerce' ); ?></h1>

		<div class="woocommerce-message woocommerce-newsletter">
			<p><?php esc_html_e( "We're here for you — get tips, product updates, and inspiration straight to your mailbox.", 'woocommerce' ); ?></p>
			<form action="//woocommerce.us8.list-manage.com/subscribe/post?u=2c1434dc56f9506bf3c3ecd21&amp;id=13860df971&amp;SIGNUPPAGE=plugin" method="post" target="_blank" novalidate>
				<div class="newsletter-form-container">
					<input
						class="newsletter-form-email"
						type="email"
						value="<?php echo esc_attr( $user_email ); ?>"
						name="EMAIL"
						placeholder="<?php esc_attr_e( 'Email address', 'woocommerce' ); ?>"
						required
					>
					<p class="wc-setup-actions step newsletter-form-button-container">
						<button
							type="submit"
							value="<?php esc_attr_e( 'Yes please!', 'woocommerce' ); ?>"
							name="subscribe"
							id="mc-embedded-subscribe"
							class="button-primary button newsletter-form-button"
						><?php esc_html_e( 'Yes please!', 'woocommerce' ); ?></button>
					</p>
				</div>
			</form>
		</div>

		<ul class="wc-wizard-next-steps">
			<li class="wc-wizard-next-step-item">
				<div class="wc-wizard-next-step-description">
					<p class="next-step-heading"><?php esc_html_e( 'Next step', 'woocommerce' ); ?></p>
					<h3 class="next-step-description"><?php esc_html_e( 'Create some products', 'woocommerce' ); ?></h3>
					<p class="next-step-extra-info"><?php esc_html_e( "You're ready to add products to your store.", 'woocommerce' ); ?></p>
				</div>
				<div class="wc-wizard-next-step-action">
					<p class="wc-setup-actions step">
						<a class="button button-primary button-large" href="<?php echo esc_url( admin_url( 'post-new.php?post_type=product&tutorial=true' ) ); ?>">
							<?php esc_html_e( 'Create a product', 'woocommerce' ); ?>
						</a>
					</p>
				</div>
			</li>
			<li class="wc-wizard-next-step-item">
				<div class="wc-wizard-next-step-description">
					<p class="next-step-heading"><?php esc_html_e( 'Have an existing store?', 'woocommerce' ); ?></p>
					<h3 class="next-step-description"><?php esc_html_e( 'Import products', 'woocommerce' ); ?></h3>
					<p class="next-step-extra-info"><?php esc_html_e( 'Transfer existing products to your new store — just import a CSV file.', 'woocommerce' ); ?></p>
				</div>
				<div class="wc-wizard-next-step-action">
					<p class="wc-setup-actions step">
						<a class="button button-large" href="<?php echo esc_url( admin_url( 'edit.php?post_type=product&page=product_importer' ) ); ?>">
							<?php esc_html_e( 'Import products', 'woocommerce' ); ?>
						</a>
					</p>
				</div>
			</li>
			<li class="wc-wizard-additional-steps">
				<div class="wc-wizard-next-step-description">
					<p class="next-step-heading"><?php esc_html_e( 'You can also:', 'woocommerce' ); ?></p>
				</div>
				<div class="wc-wizard-next-step-action">
					<p class="wc-setup-actions step">
						<a class="button button-large" href="<?php echo esc_url( admin_url() ); ?>">
							<?php esc_html_e( 'Visit Dashboard', 'woocommerce' ); ?>
						</a>
						<a class="button button-large" href="<?php echo esc_url( admin_url( 'admin.php?page=wc-settings' ) ); ?>">
							<?php esc_html_e( 'Review Settings', 'woocommerce' ); ?>
						</a>
						<a class="button button-large" href="<?php echo esc_url( add_query_arg( array( 'autofocus' => array( 'panel' => 'woocommerce' ), 'url' => wc_get_page_permalink( 'shop' ) ), admin_url( 'customize.php' ) ) ); ?>">
							<?php esc_html_e( 'View &amp; Customize', 'woocommerce' ); ?>
						</a>
					</p>
				</div>
			</li>
		</ul>
		<p class="next-steps-help-text"><?php echo wp_kses_post( $help_text ); ?></p>
		<?php
	}
}

admin/marketplace-suggestions/templates/html-product-data-extensions.php000064400000003046151336044770022746 0ustar00<?php
/**
 * The marketplace suggestions tab HTML in the product tabs
 *
 * @package WooCommerce\Classes
 * @since   3.6.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

?>
<div id="marketplace_suggestions" class="panel woocommerce_options_panel hidden">
	<?php
		WC_Marketplace_Suggestions::render_suggestions_container( 'product-edit-meta-tab-header' );
		WC_Marketplace_Suggestions::render_suggestions_container( 'product-edit-meta-tab-body' );
		WC_Marketplace_Suggestions::render_suggestions_container( 'product-edit-meta-tab-footer' );
	?>
	<div class="marketplace-suggestions-metabox-nosuggestions-placeholder hidden">
		<img src="https://woocommerce.com/wp-content/plugins/wccom-plugins/marketplace-suggestions/icons/get_more_options.svg" class="marketplace-suggestion-icon">
		<div class="marketplace-suggestion-placeholder-content">
			<h4><?php esc_html_e( 'Enhance your products', 'woocommerce' ); ?></h4>
			<p><?php esc_html_e( 'Extensions can add new functionality to your product pages that make your store stand out', 'woocommerce' ); ?></p>
		</div>
		<a href="https://woocommerce.com/product-category/woocommerce-extensions/?utm_source=editproduct&amp;utm_campaign=marketplacesuggestions&amp;utm_medium=product" target="blank" class="button"><?php esc_html_e( 'Browse the Marketplace', 'woocommerce' ); ?></a><br />
		<a class="marketplace-suggestion-manage-link" href="<?php echo esc_url( admin_url( 'admin.php?page=wc-settings&tab=advanced&section=woocommerce_com' ) ); ?>"><?php esc_html_e( 'Manage suggestions', 'woocommerce' ); ?></a>
	</div>
</div>
admin/marketplace-suggestions/class-wc-marketplace-suggestions.php000064400000013716151336044770021601 0ustar00<?php
/**
 * Marketplace suggestions
 *
 * Behaviour for displaying in-context suggestions for marketplace extensions.
 *
 * @package WooCommerce\Classes
 * @since   3.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Marketplace suggestions core behaviour.
 */
class WC_Marketplace_Suggestions {

	/**
	 * Initialise.
	 */
	public static function init() {
		if ( ! self::allow_suggestions() ) {
			return;
		}

		// Add suggestions to the product tabs.
		add_action( 'woocommerce_product_data_tabs', array( __CLASS__, 'product_data_tabs' ) );
		add_action( 'woocommerce_product_data_panels', array( __CLASS__, 'product_data_panels' ) );

		// Register ajax api handlers.
		add_action( 'wp_ajax_woocommerce_add_dismissed_marketplace_suggestion', array( __CLASS__, 'post_add_dismissed_suggestion_handler' ) );

		// Register hooks for rendering suggestions container markup.
		add_action( 'wc_marketplace_suggestions_products_empty_state', array( __CLASS__, 'render_products_list_empty_state' ) );
		add_action( 'wc_marketplace_suggestions_orders_empty_state', array( __CLASS__, 'render_orders_list_empty_state' ) );
	}

	/**
	 * Product data tabs filter
	 *
	 * Adds a new Extensions tab to the product data meta box.
	 *
	 * @param array $tabs Existing tabs.
	 *
	 * @return array
	 */
	public static function product_data_tabs( $tabs ) {
		$tabs['marketplace-suggestions'] = array(
			'label'    => _x( 'Get more options', 'Marketplace suggestions', 'woocommerce' ),
			'target'   => 'marketplace_suggestions',
			'class'    => array(),
			'priority' => 1000,
		);

		return $tabs;
	}

	/**
	 * Render additional panels in the product data metabox.
	 */
	public static function product_data_panels() {
		include dirname( __FILE__ ) . '/templates/html-product-data-extensions.php';
	}

	/**
	 * Return an array of suggestions the user has dismissed.
	 */
	public static function get_dismissed_suggestions() {
		$dismissed_suggestions = array();

		$dismissed_suggestions_data = get_user_meta( get_current_user_id(), 'wc_marketplace_suggestions_dismissed_suggestions', true );
		if ( $dismissed_suggestions_data ) {
			$dismissed_suggestions = $dismissed_suggestions_data;
			if ( ! is_array( $dismissed_suggestions ) ) {
				$dismissed_suggestions = array();
			}
		}

		return $dismissed_suggestions;
	}

	/**
	 * POST handler for adding a dismissed suggestion.
	 */
	public static function post_add_dismissed_suggestion_handler() {
		if ( ! check_ajax_referer( 'add_dismissed_marketplace_suggestion' ) ) {
			wp_die();
		}

		$post_data       = wp_unslash( $_POST );
		$suggestion_slug = sanitize_text_field( $post_data['slug'] );
		if ( ! $suggestion_slug ) {
			wp_die();
		}

		$dismissed_suggestions = self::get_dismissed_suggestions();

		if ( in_array( $suggestion_slug, $dismissed_suggestions, true ) ) {
			wp_die();
		}

		$dismissed_suggestions[] = $suggestion_slug;
		update_user_meta(
			get_current_user_id(),
			'wc_marketplace_suggestions_dismissed_suggestions',
			$dismissed_suggestions
		);

		wp_die();
	}

	/**
	 * Render suggestions containers in products list empty state.
	 */
	public static function render_products_list_empty_state() {
		self::render_suggestions_container( 'products-list-empty-header' );
		self::render_suggestions_container( 'products-list-empty-body' );
		self::render_suggestions_container( 'products-list-empty-footer' );
	}

	/**
	 * Render suggestions containers in orders list empty state.
	 */
	public static function render_orders_list_empty_state() {
		self::render_suggestions_container( 'orders-list-empty-header' );
		self::render_suggestions_container( 'orders-list-empty-body' );
		self::render_suggestions_container( 'orders-list-empty-footer' );
	}

	/**
	 * Render a suggestions container element, with the specified context.
	 *
	 * @param string $context Suggestion context name (rendered as a css class).
	 */
	public static function render_suggestions_container( $context ) {
		include dirname( __FILE__ ) . '/views/container.php';
	}

	/**
	 * Should suggestions be displayed?
	 *
	 * @param string $screen_id The current admin screen.
	 *
	 * @return bool
	 */
	public static function show_suggestions_for_screen( $screen_id ) {
		// We only show suggestions on certain admin screens.
		if ( ! in_array( $screen_id, array( 'edit-product', 'edit-shop_order', 'product' ), true ) ) {
			return false;
		}

		return self::allow_suggestions();
	}


	/**
	 * Should suggestions be displayed?
	 *
	 * @return bool
	 */
	public static function allow_suggestions() {
		// We currently only support English suggestions.
		$locale             = get_locale();
		$suggestion_locales = array(
			'en_AU',
			'en_CA',
			'en_GB',
			'en_NZ',
			'en_US',
			'en_ZA',
		);
		if ( ! in_array( $locale, $suggestion_locales, true ) ) {
			return false;
		}

		// Suggestions are only displayed if user can install plugins.
		if ( ! current_user_can( 'install_plugins' ) ) {
			return false;
		}

		// Suggestions may be disabled via a setting under Accounts & Privacy.
		if ( 'no' === get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) ) {
			return false;
		}

		// User can disabled all suggestions via filter.
		return apply_filters( 'woocommerce_allow_marketplace_suggestions', true );
	}

	/**
	 * Pull suggestion data from options. This is retrieved from a remote endpoint.
	 *
	 * @return array of json API data
	 */
	public static function get_suggestions_api_data() {
		$data = get_option( 'woocommerce_marketplace_suggestions', array() );

		// If the options have never been updated, or were updated over a week ago, queue update.
		if ( empty( $data['updated'] ) || ( time() - WEEK_IN_SECONDS ) > $data['updated'] ) {
			$next = WC()->queue()->get_next( 'woocommerce_update_marketplace_suggestions' );
			if ( ! $next ) {
				WC()->queue()->cancel_all( 'woocommerce_update_marketplace_suggestions' );
				WC()->queue()->schedule_single( time(), 'woocommerce_update_marketplace_suggestions' );
			}
		}

		return ! empty( $data['suggestions'] ) ? $data['suggestions'] : array();
	}
}

WC_Marketplace_Suggestions::init();

admin/marketplace-suggestions/class-wc-marketplace-updater.php000064400000004045151336044770020666 0ustar00<?php
/**
 * Marketplace suggestions updater
 *
 * Uses WC_Queue to ensure marketplace suggestions data is up to date and cached locally.
 *
 * @package WooCommerce\Classes
 * @since   3.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Marketplace Suggestions Updater
 */
class WC_Marketplace_Updater {

	/**
	 * Setup.
	 */
	public static function load() {
		add_action( 'init', array( __CLASS__, 'init' ) );
	}

	/**
	 * Schedule events and hook appropriate actions.
	 */
	public static function init() {
		add_action( 'woocommerce_update_marketplace_suggestions', array( __CLASS__, 'update_marketplace_suggestions' ) );
	}

	/**
	 * Fetches new marketplace data, updates wc_marketplace_suggestions.
	 */
	public static function update_marketplace_suggestions() {
		$data = get_option(
			'woocommerce_marketplace_suggestions',
			array(
				'suggestions' => array(),
				'updated'     => time(),
			)
		);

		$data['updated'] = time();

		$url     = 'https://woocommerce.com/wp-json/wccom/marketplace-suggestions/1.0/suggestions.json';
		$request = wp_safe_remote_get( $url );

		if ( is_wp_error( $request ) ) {
			self::retry();
			return update_option( 'woocommerce_marketplace_suggestions', $data, false );
		}

		$body = wp_remote_retrieve_body( $request );
		if ( empty( $body ) ) {
			self::retry();
			return update_option( 'woocommerce_marketplace_suggestions', $data, false );
		}

		$body = json_decode( $body, true );
		if ( empty( $body ) || ! is_array( $body ) ) {
			self::retry();
			return update_option( 'woocommerce_marketplace_suggestions', $data, false );
		}

		$data['suggestions'] = $body;
		return update_option( 'woocommerce_marketplace_suggestions', $data, false );
	}

	/**
	 * Used when an error has occured when fetching suggestions.
	 * Re-schedules the job earlier than the main weekly one.
	 */
	public static function retry() {
		WC()->queue()->cancel_all( 'woocommerce_update_marketplace_suggestions' );
		WC()->queue()->schedule_single( time() + DAY_IN_SECONDS, 'woocommerce_update_marketplace_suggestions' );
	}
}

WC_Marketplace_Updater::load();
admin/marketplace-suggestions/views/container.php000064400000000437151336044770016342 0ustar00<?php
/**
 * Marketplace suggestions container
 *
 * @package  WooCommerce\Templates
 * @version  3.6.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

?>
<div class="marketplace-suggestions-container"
	data-marketplace-suggestions-context="<?php echo esc_attr( $context ); ?>"
	>
</div>
admin/reports/class-wc-report-sales-by-category.php000064400000032403151336044770016434 0ustar00<?php
/**
 * Sales by category report functionality
 *
 * @package WooCommerce\Admin\Reporting
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * WC_Report_Sales_By_Category
 *
 * @package     WooCommerce\Admin\Reports
 * @version     2.1.0
 */
class WC_Report_Sales_By_Category extends WC_Admin_Report {

	/**
	 * Chart colors.
	 *
	 * @var array
	 */
	public $chart_colours = array();

	/**
	 * Categories ids.
	 *
	 * @var array
	 */
	public $show_categories = array();

	/**
	 * Item sales.
	 *
	 * @var array
	 */
	private $item_sales = array();

	/**
	 * Item sales and times.
	 *
	 * @var array
	 */
	private $item_sales_and_times = array();

	/**
	 * Constructor.
	 */
	public function __construct() {
		if ( isset( $_GET['show_categories'] ) ) {
			$this->show_categories = is_array( $_GET['show_categories'] ) ? array_map( 'absint', $_GET['show_categories'] ) : array( absint( $_GET['show_categories'] ) );
		}
	}

	/**
	 * Get all product ids in a category (and its children).
	 *
	 * @param  int $category_id Category ID.
	 * @return array
	 */
	public function get_products_in_category( $category_id ) {
		$term_ids    = get_term_children( $category_id, 'product_cat' );
		$term_ids[]  = $category_id;
		$product_ids = get_objects_in_term( $term_ids, 'product_cat' );

		return array_unique( apply_filters( 'woocommerce_report_sales_by_category_get_products_in_category', $product_ids, $category_id ) );
	}

	/**
	 * Get the legend for the main chart sidebar.
	 *
	 * @return array
	 */
	public function get_chart_legend() {

		if ( empty( $this->show_categories ) ) {
			return array();
		}

		$legend = array();
		$index  = 0;

		foreach ( $this->show_categories as $category ) {

			$category    = get_term( $category, 'product_cat' );
			$total       = 0;
			$product_ids = $this->get_products_in_category( $category->term_id );

			foreach ( $product_ids as $id ) {

				if ( isset( $this->item_sales[ $id ] ) ) {
					$total += $this->item_sales[ $id ];
				}
			}

			$legend[] = array(
				/* translators: 1: total items sold 2: category name */
				'title'            => sprintf( __( '%1$s sales in %2$s', 'woocommerce' ), '<strong>' . wc_price( $total ) . '</strong>', $category->name ),
				'color'            => isset( $this->chart_colours[ $index ] ) ? $this->chart_colours[ $index ] : $this->chart_colours[0],
				'highlight_series' => $index,
			);

			$index++;
		}

		return $legend;
	}

	/**
	 * Output the report.
	 */
	public function output_report() {

		$ranges = array(
			'year'       => __( 'Year', 'woocommerce' ),
			'last_month' => __( 'Last month', 'woocommerce' ),
			'month'      => __( 'This month', 'woocommerce' ),
			'7day'       => __( 'Last 7 days', 'woocommerce' ),
		);

		$this->chart_colours = array( '#3498db', '#34495e', '#1abc9c', '#2ecc71', '#f1c40f', '#e67e22', '#e74c3c', '#2980b9', '#8e44ad', '#2c3e50', '#16a085', '#27ae60', '#f39c12', '#d35400', '#c0392b' );

		$current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : '7day';

		if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ) ) ) {
			$current_range = '7day';
		}

		$this->check_current_range_nonce( $current_range );
		$this->calculate_current_range( $current_range );

		// Get item sales data.
		if ( ! empty( $this->show_categories ) ) {
			$order_items = $this->get_order_report_data(
				array(
					'data'         => array(
						'_product_id' => array(
							'type'            => 'order_item_meta',
							'order_item_type' => 'line_item',
							'function'        => '',
							'name'            => 'product_id',
						),
						'_line_total' => array(
							'type'            => 'order_item_meta',
							'order_item_type' => 'line_item',
							'function'        => 'SUM',
							'name'            => 'order_item_amount',
						),
						'post_date'   => array(
							'type'     => 'post_data',
							'function' => '',
							'name'     => 'post_date',
						),
					),
					'group_by'     => 'ID, product_id, post_date',
					'query_type'   => 'get_results',
					'filter_range' => true,
				)
			);

			$this->item_sales           = array();
			$this->item_sales_and_times = array();

			if ( is_array( $order_items ) ) {

				foreach ( $order_items as $order_item ) {

					switch ( $this->chart_groupby ) {
						case 'day':
							$time = strtotime( gmdate( 'Ymd', strtotime( $order_item->post_date ) ) ) * 1000;
							break;
						case 'month':
						default:
							$time = strtotime( gmdate( 'Ym', strtotime( $order_item->post_date ) ) . '01' ) * 1000;
							break;
					}

					$this->item_sales_and_times[ $time ][ $order_item->product_id ] = isset( $this->item_sales_and_times[ $time ][ $order_item->product_id ] ) ? $this->item_sales_and_times[ $time ][ $order_item->product_id ] + $order_item->order_item_amount : $order_item->order_item_amount;

					$this->item_sales[ $order_item->product_id ] = isset( $this->item_sales[ $order_item->product_id ] ) ? $this->item_sales[ $order_item->product_id ] + $order_item->order_item_amount : $order_item->order_item_amount;
				}
			}
		}

		include WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php';
	}

	/**
	 * Get chart widgets.
	 *
	 * @return array
	 */
	public function get_chart_widgets() {

		return array(
			array(
				'title'    => __( 'Categories', 'woocommerce' ),
				'callback' => array( $this, 'category_widget' ),
			),
		);
	}

	/**
	 * Output category widget.
	 */
	public function category_widget() {

		$categories = get_terms( 'product_cat', array( 'orderby' => 'name' ) );
		?>
		<form method="GET">
			<div>
				<select multiple="multiple" data-placeholder="<?php esc_attr_e( 'Select categories&hellip;', 'woocommerce' ); ?>" class="wc-enhanced-select" id="show_categories" name="show_categories[]" style="width: 205px;">
					<?php
						$r                 = array();
						$r['pad_counts']   = 1;
						$r['hierarchical'] = 1;
						$r['hide_empty']   = 1;
						$r['value']        = 'id';
						$r['selected']     = $this->show_categories;

						include_once WC()->plugin_path() . '/includes/walkers/class-wc-product-cat-dropdown-walker.php';

						echo wc_walk_category_dropdown_tree( $categories, 0, $r ); // @codingStandardsIgnoreLine
					?>
				</select>
				<?php // @codingStandardsIgnoreStart ?>
				<a href="#" class="select_none"><?php esc_html_e( 'None', 'woocommerce' ); ?></a>
				<a href="#" class="select_all"><?php esc_html_e( 'All', 'woocommerce' ); ?></a>
				<button type="submit" class="submit button" value="<?php esc_attr_e( 'Show', 'woocommerce' ); ?>"><?php esc_html_e( 'Show', 'woocommerce' ); ?></button>
				<input type="hidden" name="range" value="<?php echo ( ! empty( $_GET['range'] ) ) ? esc_attr( wp_unslash( $_GET['range'] ) ) : ''; ?>" />
				<input type="hidden" name="start_date" value="<?php echo ( ! empty( $_GET['start_date'] ) ) ? esc_attr( wp_unslash( $_GET['start_date'] ) ) : ''; ?>" />
				<input type="hidden" name="end_date" value="<?php echo ( ! empty( $_GET['end_date'] ) ) ? esc_attr( wp_unslash( $_GET['end_date'] ) ) : ''; ?>" />
				<input type="hidden" name="page" value="<?php echo ( ! empty( $_GET['page'] ) ) ? esc_attr( wp_unslash( $_GET['page'] ) ) : ''; ?>" />
				<input type="hidden" name="tab" value="<?php echo ( ! empty( $_GET['tab'] ) ) ? esc_attr( wp_unslash( $_GET['tab'] ) ) : ''; ?>" />
				<input type="hidden" name="report" value="<?php echo ( ! empty( $_GET['report'] ) ) ? esc_attr( wp_unslash( $_GET['report'] ) ) : ''; ?>" />
				<?php // @codingStandardsIgnoreEnd ?>
			</div>
			<script type="text/javascript">
				jQuery(function(){
					// Select all/None
					jQuery( '.chart-widget' ).on( 'click', '.select_all', function() {
						jQuery(this).closest( 'div' ).find( 'select option' ).attr( 'selected', 'selected' );
						jQuery(this).closest( 'div' ).find('select').trigger( 'change' );
						return false;
					});

					jQuery( '.chart-widget').on( 'click', '.select_none', function() {
						jQuery(this).closest( 'div' ).find( 'select option' ).prop( 'selected', false );
						jQuery(this).closest( 'div' ).find('select').trigger( 'change' );
						return false;
					});
				});
			</script>
		</form>
		<?php
	}

	/**
	 * Output an export link.
	 */
	public function get_export_button() {

		$current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : '7day';
		?>
		<a
			href="#"
			download="report-<?php echo esc_attr( $current_range ); ?>-<?php echo esc_attr( date_i18n( 'Y-m-d', current_time( 'timestamp' ) ) ); ?>.csv"
			class="export_csv"
			data-export="chart"
			data-xaxes="<?php esc_attr_e( 'Date', 'woocommerce' ); ?>"
			data-groupby="<?php echo esc_attr( $this->chart_groupby ); ?>"
		>
			<?php esc_html_e( 'Export CSV', 'woocommerce' ); ?>
		</a>
		<?php
	}

	/**
	 * Get the main chart.
	 */
	public function get_main_chart() {
		global $wp_locale;

		if ( empty( $this->show_categories ) ) {
			?>
			<div class="chart-container">
				<p class="chart-prompt"><?php esc_html_e( 'Choose a category to view stats', 'woocommerce' ); ?></p>
			</div>
			<?php
		} else {
			$chart_data = array();
			$index      = 0;

			foreach ( $this->show_categories as $category ) {

				$category            = get_term( $category, 'product_cat' );
				$product_ids         = $this->get_products_in_category( $category->term_id );
				$category_chart_data = array();

				for ( $i = 0; $i <= $this->chart_interval; $i ++ ) {

					$interval_total = 0;

					switch ( $this->chart_groupby ) {
						case 'day':
							$time = strtotime( gmdate( 'Ymd', strtotime( "+{$i} DAY", $this->start_date ) ) ) * 1000;
							break;
						case 'month':
						default:
							$time = strtotime( gmdate( 'Ym', strtotime( "+{$i} MONTH", $this->start_date ) ) . '01' ) * 1000;
							break;
					}

					foreach ( $product_ids as $id ) {

						if ( isset( $this->item_sales_and_times[ $time ][ $id ] ) ) {
							$interval_total += $this->item_sales_and_times[ $time ][ $id ];
						}
					}

					$category_chart_data[] = array( $time, (float) wc_format_decimal( $interval_total, wc_get_price_decimals() ) );
				}

				$chart_data[ $category->term_id ]['category'] = $category->name;
				$chart_data[ $category->term_id ]['data']     = $category_chart_data;

				$index++;
			}
			?>
			<div class="chart-container">
				<div class="chart-placeholder main"></div>
			</div>
			<?php // @codingStandardsIgnoreStart ?>
			<script type="text/javascript">
				var main_chart;

				jQuery(function(){
					var drawGraph = function( highlight ) {
						var series = [
							<?php
								$index = 0;
								foreach ( $chart_data as $data ) {
									$color  = isset( $this->chart_colours[ $index ] ) ? $this->chart_colours[ $index ] : $this->chart_colours[0];
									$width  = $this->barwidth / sizeof( $chart_data );
									$offset = ( $width * $index );
									$series = $data['data'];

									foreach ( $series as $key => $series_data ) {
										$series[ $key ][0] = $series_data[0] + $offset;
									}

									$series = wp_json_encode( $series );

									echo '{
											label: "' . esc_js( $data['category'] ) . '",
											data: JSON.parse( decodeURIComponent( "' . rawurlencode( $series ) . '" ) ),
											color: "' . $color . '",
											bars: {
												fillColor: "' . $color . '",
												fill: true,
												show: true,
												lineWidth: 1,
												align: "center",
												barWidth: ' . $width * 0.75 . ',
												stack: false
											},
											' . $this->get_currency_tooltip() . ',
											enable_tooltip: true,
											prepend_label: true
										},';
									$index++;
								}
							?>
						];

						if ( highlight !== 'undefined' && series[ highlight ] ) {
							highlight_series = series[ highlight ];

							highlight_series.color = '#9c5d90';

							if ( highlight_series.bars ) {
								highlight_series.bars.fillColor = '#9c5d90';
							}

							if ( highlight_series.lines ) {
								highlight_series.lines.lineWidth = 5;
							}
						}

						main_chart = jQuery.plot(
							jQuery('.chart-placeholder.main'),
							series,
							{
								legend: {
									show: false
								},
								grid: {
									color: '#aaa',
									borderColor: 'transparent',
									borderWidth: 0,
									hoverable: true
								},
								xaxes: [ {
									color: '#aaa',
									reserveSpace: true,
									position: "bottom",
									tickColor: 'transparent',
									mode: "time",
									timeformat: "<?php echo ( 'day' === $this->chart_groupby ) ? '%d %b' : '%b'; ?>",
									monthNames: JSON.parse( decodeURIComponent( '<?php echo rawurlencode( wp_json_encode( array_values( $wp_locale->month_abbrev ) ) ); ?>' ) ),
									tickLength: 1,
									minTickSize: [1, "<?php echo $this->chart_groupby; ?>"],
									tickSize: [1, "<?php echo $this->chart_groupby; ?>"],
									font: {
										color: "#aaa"
									}
								} ],
								yaxes: [
									{
										min: 0,
										tickDecimals: 2,
										color: 'transparent',
										font: { color: "#aaa" }
									}
								],
							}
						);

						jQuery('.chart-placeholder').trigger( 'resize' );

					}

					drawGraph();

					jQuery('.highlight_series').on( 'mouseenter',
						function() {
							drawGraph( jQuery(this).data('series') );
						} ).on( 'mouseleave',
						function() {
							drawGraph();
						}
					);
				});
			</script>
			<?php // @codingStandardsIgnoreEnd ?>
			<?php
		}
	}
}
admin/reports/class-wc-report-sales-by-product.php000064400000050515151336044770016303 0ustar00<?php
/**
 * Sales By Product Reporting
 *
 * @package WooCommerce\Admin\Reporting
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * WC_Report_Sales_By_Product
 *
 * @package     WooCommerce\Admin\Reports
 * @version     2.1.0
 */
class WC_Report_Sales_By_Product extends WC_Admin_Report {

	/**
	 * Chart colors.
	 *
	 * @var array
	 */
	public $chart_colours = array();

	/**
	 * Product ids.
	 *
	 * @var array
	 */
	public $product_ids = array();

	/**
	 * Product ids with titles.
	 *
	 * @var array
	 */
	public $product_ids_titles = array();

	/**
	 * Constructor.
	 */
	public function __construct() {
		// @codingStandardsIgnoreStart
		if ( isset( $_GET['product_ids'] ) && is_array( $_GET['product_ids'] ) ) {
			$this->product_ids = array_filter( array_map( 'absint', $_GET['product_ids'] ) );
		} elseif ( isset( $_GET['product_ids'] ) ) {
			$this->product_ids = array_filter( array( absint( $_GET['product_ids'] ) ) );
		}
		// @codingStandardsIgnoreEnd
	}

	/**
	 * Get the legend for the main chart sidebar.
	 *
	 * @return array
	 */
	public function get_chart_legend() {

		if ( empty( $this->product_ids ) ) {
			return array();
		}

		$legend = array();

		$total_sales = $this->get_order_report_data(
			array(
				'data'         => array(
					'_line_total' => array(
						'type'            => 'order_item_meta',
						'order_item_type' => 'line_item',
						'function'        => 'SUM',
						'name'            => 'order_item_amount',
					),
				),
				'where_meta'   => array(
					'relation' => 'OR',
					array(
						'type'       => 'order_item_meta',
						'meta_key'   => array( '_product_id', '_variation_id' ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
						'meta_value' => $this->product_ids, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
						'operator'   => 'IN',
					),
				),
				'query_type'   => 'get_var',
				'filter_range' => true,
				'order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ),
			)
		);

		$total_items = absint(
			$this->get_order_report_data(
				array(
					'data'         => array(
						'_qty' => array(
							'type'            => 'order_item_meta',
							'order_item_type' => 'line_item',
							'function'        => 'SUM',
							'name'            => 'order_item_count',
						),
					),
					'where_meta'   => array(
						'relation' => 'OR',
						array(
							'type'       => 'order_item_meta',
							'meta_key'   => array( '_product_id', '_variation_id' ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
							'meta_value' => $this->product_ids, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
							'operator'   => 'IN',
						),
					),
					'query_type'   => 'get_var',
					'filter_range' => true,
					'order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ),
				)
			)
		);

		$legend[] = array(
			/* translators: %s: total items sold */
			'title'            => sprintf( __( '%s sales for the selected items', 'woocommerce' ), '<strong>' . wc_price( $total_sales ) . '</strong>' ),
			'color'            => $this->chart_colours['sales_amount'],
			'highlight_series' => 1,
		);

		$legend[] = array(
			/* translators: %s: total items purchased */
			'title'            => sprintf( __( '%s purchases for the selected items', 'woocommerce' ), '<strong>' . ( $total_items ) . '</strong>' ),
			'color'            => $this->chart_colours['item_count'],
			'highlight_series' => 0,
		);

		return $legend;
	}

	/**
	 * Output the report.
	 */
	public function output_report() {

		$ranges = array(
			'year'       => __( 'Year', 'woocommerce' ),
			'last_month' => __( 'Last month', 'woocommerce' ),
			'month'      => __( 'This month', 'woocommerce' ),
			'7day'       => __( 'Last 7 days', 'woocommerce' ),
		);

		$this->chart_colours = array(
			'sales_amount' => '#3498db',
			'item_count'   => '#d4d9dc',
		);

		$current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : '7day'; //phpcs:ignore WordPress.Security.NonceVerification.Recommended

		if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ), true ) ) {
			$current_range = '7day';
		}

		$this->check_current_range_nonce( $current_range );
		$this->calculate_current_range( $current_range );

		include WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php';
	}

	/**
	 * Get chart widgets.
	 *
	 * @return array
	 */
	public function get_chart_widgets() {

		$widgets = array();

		if ( ! empty( $this->product_ids ) ) {
			$widgets[] = array(
				'title'    => __( 'Showing reports for:', 'woocommerce' ),
				'callback' => array( $this, 'current_filters' ),
			);
		}

		$widgets[] = array(
			'title'    => '',
			'callback' => array( $this, 'products_widget' ),
		);

		return $widgets;
	}

	/**
	 * Output current filters.
	 */
	public function current_filters() {

		$this->product_ids_titles = array();

		foreach ( $this->product_ids as $product_id ) {

			$product = wc_get_product( $product_id );

			if ( $product ) {
				$this->product_ids_titles[] = $product->get_formatted_name();
			} else {
				$this->product_ids_titles[] = '#' . $product_id;
			}
		}

		echo '<p><strong>' . wp_kses_post( implode( ', ', $this->product_ids_titles ) ) . '</strong></p>';
		echo '<p><a class="button" href="' . esc_url( remove_query_arg( 'product_ids' ) ) . '">' . esc_html__( 'Reset', 'woocommerce' ) . '</a></p>';
	}

	/**
	 * Output products widget.
	 */
	public function products_widget() {
		?>
		<h4 class="section_title"><span><?php esc_html_e( 'Product search', 'woocommerce' ); ?></span></h4>
		<div class="section">
			<form method="GET">
				<div>
					<?php // @codingStandardsIgnoreStart ?>
					<select class="wc-product-search" style="width:203px;" multiple="multiple" id="product_ids" name="product_ids[]" data-placeholder="<?php esc_attr_e( 'Search for a product&hellip;', 'woocommerce' ); ?>" data-action="woocommerce_json_search_products_and_variations"></select>
					<button type="submit" class="submit button" value="<?php esc_attr_e( 'Show', 'woocommerce' ); ?>"><?php esc_html_e( 'Show', 'woocommerce' ); ?></button>
					<input type="hidden" name="range" value="<?php echo ( ! empty( $_GET['range'] ) ) ? esc_attr( $_GET['range'] ) : ''; ?>" />
					<input type="hidden" name="start_date" value="<?php echo ( ! empty( $_GET['start_date'] ) ) ? esc_attr( $_GET['start_date'] ) : ''; ?>" />
					<input type="hidden" name="end_date" value="<?php echo ( ! empty( $_GET['end_date'] ) ) ? esc_attr( $_GET['end_date'] ) : ''; ?>" />
					<input type="hidden" name="page" value="<?php echo ( ! empty( $_GET['page'] ) ) ? esc_attr( $_GET['page'] ) : ''; ?>" />
					<input type="hidden" name="tab" value="<?php echo ( ! empty( $_GET['tab'] ) ) ? esc_attr( $_GET['tab'] ) : ''; ?>" />
					<input type="hidden" name="report" value="<?php echo ( ! empty( $_GET['report'] ) ) ? esc_attr( $_GET['report'] ) : ''; ?>" />
					<?php wp_nonce_field( 'custom_range', 'wc_reports_nonce', false ); ?>
					<?php // @codingStandardsIgnoreEnd ?>
				</div>
			</form>
		</div>
		<h4 class="section_title"><span><?php esc_html_e( 'Top sellers', 'woocommerce' ); ?></span></h4>
		<div class="section">
			<table cellspacing="0">
				<?php
				$top_sellers = $this->get_order_report_data(
					array(
						'data'         => array(
							'_product_id' => array(
								'type'            => 'order_item_meta',
								'order_item_type' => 'line_item',
								'function'        => '',
								'name'            => 'product_id',
							),
							'_qty'        => array(
								'type'            => 'order_item_meta',
								'order_item_type' => 'line_item',
								'function'        => 'SUM',
								'name'            => 'order_item_qty',
							),
						),
						'order_by'     => 'order_item_qty DESC',
						'group_by'     => 'product_id',
						'limit'        => 12,
						'query_type'   => 'get_results',
						'filter_range' => true,
						'order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ),
					)
				);

				if ( $top_sellers ) {
					// @codingStandardsIgnoreStart
					foreach ( $top_sellers as $product ) {
						echo '<tr class="' . ( in_array( $product->product_id, $this->product_ids ) ? 'active' : '' ) . '">
							<td class="count">' . esc_html( $product->order_item_qty ) . '</td>
							<td class="name"><a href="' . esc_url( add_query_arg( 'product_ids', $product->product_id ) ) . '">' . esc_html( get_the_title( $product->product_id ) ) . '</a></td>
							<td class="sparkline">' . $this->sales_sparkline( $product->product_id, 7, 'count' ) . '</td>
						</tr>';
					}
					// @codingStandardsIgnoreEnd
				} else {
					echo '<tr><td colspan="3">' . esc_html__( 'No products found in range', 'woocommerce' ) . '</td></tr>';
				}
				?>
			</table>
		</div>
		<h4 class="section_title"><span><?php esc_html_e( 'Top freebies', 'woocommerce' ); ?></span></h4>
		<div class="section">
			<table cellspacing="0">
				<?php
				$top_freebies = $this->get_order_report_data(
					array(
						'data'         => array(
							'_product_id' => array(
								'type'            => 'order_item_meta',
								'order_item_type' => 'line_item',
								'function'        => '',
								'name'            => 'product_id',
							),
							'_qty'        => array(
								'type'            => 'order_item_meta',
								'order_item_type' => 'line_item',
								'function'        => 'SUM',
								'name'            => 'order_item_qty',
							),
						),
						'where_meta'   => array(
							array(
								'type'       => 'order_item_meta',
								'meta_key'   => '_line_subtotal', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
								'meta_value' => '0', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
								'operator'   => '=',
							),
						),
						'order_by'     => 'order_item_qty DESC',
						'group_by'     => 'product_id',
						'limit'        => 12,
						'query_type'   => 'get_results',
						'filter_range' => true,
					)
				);

				if ( $top_freebies ) {
					// @codingStandardsIgnoreStart
					foreach ( $top_freebies as $product ) {
						echo '<tr class="' . ( in_array( $product->product_id, $this->product_ids ) ? 'active' : '' ) . '">
							<td class="count">' . esc_html( $product->order_item_qty ) . '</td>
							<td class="name"><a href="' . esc_url( add_query_arg( 'product_ids', $product->product_id ) ) . '">' . esc_html( get_the_title( $product->product_id ) ) . '</a></td>
							<td class="sparkline">' . $this->sales_sparkline( $product->product_id, 7, 'count' ) . '</td>
						</tr>';
					}
					// @codingStandardsIgnoreEnd
				} else {
					echo '<tr><td colspan="3">' . esc_html__( 'No products found in range', 'woocommerce' ) . '</td></tr>';
				}
				?>
			</table>
		</div>
		<h4 class="section_title"><span><?php esc_html_e( 'Top earners', 'woocommerce' ); ?></span></h4>
		<div class="section">
			<table cellspacing="0">
				<?php
				$top_earners = $this->get_order_report_data(
					array(
						'data'         => array(
							'_product_id' => array(
								'type'            => 'order_item_meta',
								'order_item_type' => 'line_item',
								'function'        => '',
								'name'            => 'product_id',
							),
							'_line_total' => array(
								'type'            => 'order_item_meta',
								'order_item_type' => 'line_item',
								'function'        => 'SUM',
								'name'            => 'order_item_total',
							),
						),
						'order_by'     => 'order_item_total DESC',
						'group_by'     => 'product_id',
						'limit'        => 12,
						'query_type'   => 'get_results',
						'filter_range' => true,
						'order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ),
					)
				);

				if ( $top_earners ) {
					// @codingStandardsIgnoreStart
					foreach ( $top_earners as $product ) {
						echo '<tr class="' . ( in_array( $product->product_id, $this->product_ids ) ? 'active' : '' ) . '">
							<td class="count">' . wc_price( $product->order_item_total ) . '</td>
							<td class="name"><a href="' . esc_url( add_query_arg( 'product_ids', $product->product_id ) ) . '">' . esc_html( get_the_title( $product->product_id ) ) . '</a></td>
							<td class="sparkline">' . $this->sales_sparkline( $product->product_id, 7, 'sales' ) . '</td>
						</tr>';
					}
					// @codingStandardsIgnoreEnd
				} else {
					echo '<tr><td colspan="3">' . esc_html__( 'No products found in range', 'woocommerce' ) . '</td></tr>';
				}
				?>
			</table>
		</div>
		<script type="text/javascript">
			jQuery( '.section_title' ).on( 'click', function() {
				var next_section = jQuery( this ).next( '.section' );

				if ( jQuery( next_section ).is( ':visible' ) ) {
					return false;
				}

				jQuery( '.section:visible' ).slideUp();
				jQuery( '.section_title' ).removeClass( 'open' );
				jQuery( this ).addClass( 'open' ).next( '.section' ).slideDown();

				return false;
			} );
			jQuery( '.section' ).slideUp( 100, function() {
				<?php if ( empty( $this->product_ids ) ) : ?>
					jQuery( '.section_title:eq(1)' ).trigger( 'click' );
				<?php endif; ?>
			} );
		</script>
		<?php
	}

	/**
	 * Output an export link.
	 */
	public function get_export_button() {

		$current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : '7day'; //phpcs:ignore WordPress.Security.NonceVerification.Recommended
		?>
		<a
			href="#"
			download="report-<?php echo esc_attr( $current_range ); ?>-<?php echo esc_html( date_i18n( 'Y-m-d', current_time( 'timestamp' ) ) ); ?>.csv"
			class="export_csv"
			data-export="chart"
			data-xaxes="<?php esc_attr_e( 'Date', 'woocommerce' ); ?>"
			data-groupby="<?php echo $this->chart_groupby; ?>"<?php // @codingStandardsIgnoreLine ?>
		>
			<?php esc_html_e( 'Export CSV', 'woocommerce' ); ?>
		</a>
		<?php
	}

	/**
	 * Get the main chart.
	 */
	public function get_main_chart() {
		global $wp_locale;

		if ( empty( $this->product_ids ) ) {
			?>
			<div class="chart-container">
				<p class="chart-prompt"><?php esc_html_e( 'Choose a product to view stats', 'woocommerce' ); ?></p>
			</div>
			<?php
		} else {
			// Get orders and dates in range - we want the SUM of order totals, COUNT of order items, COUNT of orders, and the date.
			$order_item_counts = $this->get_order_report_data(
				array(
					'data'         => array(
						'_qty'        => array(
							'type'            => 'order_item_meta',
							'order_item_type' => 'line_item',
							'function'        => 'SUM',
							'name'            => 'order_item_count',
						),
						'post_date'   => array(
							'type'     => 'post_data',
							'function' => '',
							'name'     => 'post_date',
						),
						'_product_id' => array(
							'type'            => 'order_item_meta',
							'order_item_type' => 'line_item',
							'function'        => '',
							'name'            => 'product_id',
						),
					),
					'where_meta'   => array(
						'relation' => 'OR',
						array(
							'type'       => 'order_item_meta',
							'meta_key'   => array( '_product_id', '_variation_id' ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
							'meta_value' => $this->product_ids, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
							'operator'   => 'IN',
						),
					),
					'group_by'     => 'product_id,' . $this->group_by_query,
					'order_by'     => 'post_date ASC',
					'query_type'   => 'get_results',
					'filter_range' => true,
					'order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ),
				)
			);

			$order_item_amounts = $this->get_order_report_data(
				array(
					'data'         => array(
						'_line_total' => array(
							'type'            => 'order_item_meta',
							'order_item_type' => 'line_item',
							'function'        => 'SUM',
							'name'            => 'order_item_amount',
						),
						'post_date'   => array(
							'type'     => 'post_data',
							'function' => '',
							'name'     => 'post_date',
						),
						'_product_id' => array(
							'type'            => 'order_item_meta',
							'order_item_type' => 'line_item',
							'function'        => '',
							'name'            => 'product_id',
						),
					),
					'where_meta'   => array(
						'relation' => 'OR',
						array(
							'type'       => 'order_item_meta',
							'meta_key'   => array( '_product_id', '_variation_id' ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
							'meta_value' => $this->product_ids, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
							'operator'   => 'IN',
						),
					),
					'group_by'     => 'product_id, ' . $this->group_by_query,
					'order_by'     => 'post_date ASC',
					'query_type'   => 'get_results',
					'filter_range' => true,
					'order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ),
				)
			);

			// Prepare data for report.
			$order_item_counts  = $this->prepare_chart_data( $order_item_counts, 'post_date', 'order_item_count', $this->chart_interval, $this->start_date, $this->chart_groupby );
			$order_item_amounts = $this->prepare_chart_data( $order_item_amounts, 'post_date', 'order_item_amount', $this->chart_interval, $this->start_date, $this->chart_groupby );

			// Encode in json format.
			$chart_data = wp_json_encode(
				array(
					'order_item_counts'  => array_values( $order_item_counts ),
					'order_item_amounts' => array_values( $order_item_amounts ),
				)
			);
			?>
			<div class="chart-container">
				<div class="chart-placeholder main"></div>
			</div>
			<?php // @codingStandardsIgnoreStart ?>
			<script type="text/javascript">
				var main_chart;

				jQuery(function(){
					var order_data = JSON.parse( decodeURIComponent( '<?php echo rawurlencode( $chart_data ); ?>' ) );

					var drawGraph = function( highlight ) {

						var series = [
							{
								label: "<?php echo esc_js( __( 'Number of items sold', 'woocommerce' ) ) ?>",
								data: order_data.order_item_counts,
								color: '<?php echo $this->chart_colours['item_count']; ?>',
								bars: { fillColor: '<?php echo $this->chart_colours['item_count']; ?>', fill: true, show: true, lineWidth: 0, barWidth: <?php echo $this->barwidth; ?> * 0.5, align: 'center' },
								shadowSize: 0,
								hoverable: false
							},
							{
								label: "<?php echo esc_js( __( 'Sales amount', 'woocommerce' ) ) ?>",
								data: order_data.order_item_amounts,
								yaxis: 2,
								color: '<?php echo $this->chart_colours['sales_amount']; ?>',
								points: { show: true, radius: 5, lineWidth: 3, fillColor: '#fff', fill: true },
								lines: { show: true, lineWidth: 4, fill: false },
								shadowSize: 0,
								<?php echo $this->get_currency_tooltip(); ?>
							}
						];

						if ( highlight !== 'undefined' && series[ highlight ] ) {
							highlight_series = series[ highlight ];

							highlight_series.color = '#9c5d90';

							if ( highlight_series.bars )
								highlight_series.bars.fillColor = '#9c5d90';

							if ( highlight_series.lines ) {
								highlight_series.lines.lineWidth = 5;
							}
						}

						main_chart = jQuery.plot(
							jQuery('.chart-placeholder.main'),
							series,
							{
								legend: {
									show: false
								},
								grid: {
									color: '#aaa',
									borderColor: 'transparent',
									borderWidth: 0,
									hoverable: true
								},
								xaxes: [ {
									color: '#aaa',
									position: "bottom",
									tickColor: 'transparent',
									mode: "time",
									timeformat: "<?php echo ( 'day' === $this->chart_groupby ) ? '%d %b' : '%b'; ?>",
									monthNames: JSON.parse( decodeURIComponent( '<?php echo rawurlencode( wp_json_encode( array_values( $wp_locale->month_abbrev ) ) ); ?>' ) ),
									tickLength: 1,
									minTickSize: [1, "<?php echo $this->chart_groupby; ?>"],
									font: {
										color: "#aaa"
									}
								} ],
								yaxes: [
									{
										min: 0,
										minTickSize: 1,
										tickDecimals: 0,
										color: '#ecf0f1',
										font: { color: "#aaa" }
									},
									{
										position: "right",
										min: 0,
										tickDecimals: 2,
										alignTicksWithAxis: 1,
										color: 'transparent',
										font: { color: "#aaa" }
									}
								],
							}
						);

						jQuery('.chart-placeholder').trigger( 'resize' );
					}

					drawGraph();

					jQuery('.highlight_series').on( 'mouseenter',
						function() {
							drawGraph( jQuery(this).data('series') );
						} ).on( 'mouseleave',
						function() {
							drawGraph();
						}
					);
				});
			</script>
			<?php
			// @codingStandardsIgnoreEnd
		}
	}
}
admin/reports/class-wc-report-stock.php000064400000011007151336044770014222 0ustar00<?php

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

if ( ! class_exists( 'WP_List_Table' ) ) {
	require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}

/**
 * WC_Report_Stock.
 *
 * @author      WooThemes
 * @category    Admin
 * @package     WooCommerce\Admin\Reports
 * @version     2.1.0
 */
class WC_Report_Stock extends WP_List_Table {

	/**
	 * Max items.
	 *
	 * @var int
	 */
	protected $max_items;

	/**
	 * Constructor.
	 */
	public function __construct() {

		parent::__construct(
			array(
				'singular' => 'stock',
				'plural'   => 'stock',
				'ajax'     => false,
			)
		);
	}

	/**
	 * No items found text.
	 */
	public function no_items() {
		_e( 'No products found.', 'woocommerce' );
	}

	/**
	 * Don't need this.
	 *
	 * @param string $position
	 */
	public function display_tablenav( $position ) {

		if ( 'top' !== $position ) {
			parent::display_tablenav( $position );
		}
	}

	/**
	 * Output the report.
	 */
	public function output_report() {

		$this->prepare_items();
		echo '<div id="poststuff" class="woocommerce-reports-wide">';
		$this->display();
		echo '</div>';
	}

	/**
	 * Get column value.
	 *
	 * @param mixed  $item
	 * @param string $column_name
	 */
	public function column_default( $item, $column_name ) {
		global $product;

		if ( ! $product || $product->get_id() !== $item->id ) {
			$product = wc_get_product( $item->id );
		}

		if ( ! $product ) {
			return;
		}

		switch ( $column_name ) {

			case 'product':
				if ( $sku = $product->get_sku() ) {
					echo esc_html( $sku ) . ' - ';
				}

				echo esc_html( $product->get_name() );

				// Get variation data.
				if ( $product->is_type( 'variation' ) ) {
					echo '<div class="description">' . wp_kses_post( wc_get_formatted_variation( $product, true ) ) . '</div>';
				}
				break;

			case 'parent':
				if ( $item->parent ) {
					echo esc_html( get_the_title( $item->parent ) );
				} else {
					echo '-';
				}
				break;

			case 'stock_status':
				if ( $product->is_on_backorder() ) {
					$stock_html = '<mark class="onbackorder">' . __( 'On backorder', 'woocommerce' ) . '</mark>';
				} elseif ( $product->is_in_stock() ) {
					$stock_html = '<mark class="instock">' . __( 'In stock', 'woocommerce' ) . '</mark>';
				} else {
					$stock_html = '<mark class="outofstock">' . __( 'Out of stock', 'woocommerce' ) . '</mark>';
				}
				echo apply_filters( 'woocommerce_admin_stock_html', $stock_html, $product );
				break;

			case 'stock_level':
				echo esc_html( $product->get_stock_quantity() );
				break;

			case 'wc_actions':
				?><p>
					<?php
					$actions   = array();
					$action_id = $product->is_type( 'variation' ) ? $item->parent : $item->id;

					$actions['edit'] = array(
						'url'    => admin_url( 'post.php?post=' . $action_id . '&action=edit' ),
						'name'   => __( 'Edit', 'woocommerce' ),
						'action' => 'edit',
					);

					if ( $product->is_visible() ) {
						$actions['view'] = array(
							'url'    => get_permalink( $action_id ),
							'name'   => __( 'View', 'woocommerce' ),
							'action' => 'view',
						);
					}

					$actions = apply_filters( 'woocommerce_admin_stock_report_product_actions', $actions, $product );

					foreach ( $actions as $action ) {
						printf(
							'<a class="button tips %1$s" href="%2$s" data-tip="%3$s">%4$s</a>',
							esc_attr( $action['action'] ),
							esc_url( $action['url'] ),
							sprintf( esc_attr__( '%s product', 'woocommerce' ), $action['name'] ),
							esc_html( $action['name'] )
						);
					}
					?>
				</p>
				<?php
				break;
		}
	}

	/**
	 * Get columns.
	 *
	 * @return array
	 */
	public function get_columns() {

		$columns = array(
			'product'      => __( 'Product', 'woocommerce' ),
			'parent'       => __( 'Parent', 'woocommerce' ),
			'stock_level'  => __( 'Units in stock', 'woocommerce' ),
			'stock_status' => __( 'Stock status', 'woocommerce' ),
			'wc_actions'   => __( 'Actions', 'woocommerce' ),
		);

		return $columns;
	}

	/**
	 * Prepare customer list items.
	 */
	public function prepare_items() {

		$this->_column_headers = array( $this->get_columns(), array(), $this->get_sortable_columns() );
		$current_page          = absint( $this->get_pagenum() );
		$per_page              = apply_filters( 'woocommerce_admin_stock_report_products_per_page', 20 );

		$this->get_items( $current_page, $per_page );

		/**
		 * Pagination.
		 */
		$this->set_pagination_args(
			array(
				'total_items' => $this->max_items,
				'per_page'    => $per_page,
				'total_pages' => ceil( $this->max_items / $per_page ),
			)
		);
	}
}
admin/reports/class-wc-report-coupon-usage.php000064400000042723151336044770015515 0ustar00<?php
/**
 * Coupon usage report functionality
 *
 * @package WooCommerce\Admin\Reports
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * WC_Report_Coupon_Usage
 *
 * @package     WooCommerce\Admin\Reports
 * @version     2.1.0
 */
class WC_Report_Coupon_Usage extends WC_Admin_Report {

	/**
	 * Chart colors.
	 *
	 * @var array
	 */
	public $chart_colours = array();

	/**
	 * Coupon codes.
	 *
	 * @var array
	 */
	public $coupon_codes = array();

	/**
	 * Constructor.
	 */
	public function __construct() {
		if ( isset( $_GET['coupon_codes'] ) && is_array( $_GET['coupon_codes'] ) ) {
			$this->coupon_codes = array_filter( array_map( 'sanitize_text_field', wp_unslash( $_GET['coupon_codes'] ) ) );
		} elseif ( isset( $_GET['coupon_codes'] ) ) {
			$this->coupon_codes = array_filter( array( sanitize_text_field( wp_unslash( $_GET['coupon_codes'] ) ) ) );
		}
	}

	/**
	 * Get the legend for the main chart sidebar.
	 *
	 * @return array
	 */
	public function get_chart_legend() {
		$legend = array();

		$total_discount_query = array(
			'data'         => array(
				'discount_amount' => array(
					'type'            => 'order_item_meta',
					'order_item_type' => 'coupon',
					'function'        => 'SUM',
					'name'            => 'discount_amount',
				),
			),
			'where'        => array(
				array(
					'key'      => 'order_item_type',
					'value'    => 'coupon',
					'operator' => '=',
				),
			),
			'query_type'   => 'get_var',
			'filter_range' => true,
			'order_types'  => wc_get_order_types( 'order-count' ),
		);

		$total_coupons_query = array(
			'data'         => array(
				'order_item_id' => array(
					'type'            => 'order_item',
					'order_item_type' => 'coupon',
					'function'        => 'COUNT',
					'name'            => 'order_coupon_count',
				),
			),
			'where'        => array(
				array(
					'key'      => 'order_item_type',
					'value'    => 'coupon',
					'operator' => '=',
				),
			),
			'query_type'   => 'get_var',
			'filter_range' => true,
			'order_types'  => wc_get_order_types( 'order-count' ),
		);

		if ( ! empty( $this->coupon_codes ) ) {
			$coupon_code_query = array(
				'type'     => 'order_item',
				'key'      => 'order_item_name',
				'value'    => $this->coupon_codes,
				'operator' => 'IN',
			);

			$total_discount_query['where'][] = $coupon_code_query;
			$total_coupons_query['where'][]  = $coupon_code_query;
		}

		$total_discount = $this->get_order_report_data( $total_discount_query );
		$total_coupons  = absint( $this->get_order_report_data( $total_coupons_query ) );

		$legend[] = array(
			/* translators: %s: discount amount */
			'title'            => sprintf( __( '%s discounts in total', 'woocommerce' ), '<strong>' . wc_price( $total_discount ) . '</strong>' ),
			'color'            => $this->chart_colours['discount_amount'],
			'highlight_series' => 1,
		);

		$legend[] = array(
			/* translators: %s: coupons amount */
			'title'            => sprintf( __( '%s coupons used in total', 'woocommerce' ), '<strong>' . $total_coupons . '</strong>' ),
			'color'            => $this->chart_colours['coupon_count'],
			'highlight_series' => 0,
		);

		return $legend;
	}

	/**
	 * Output the report.
	 */
	public function output_report() {

		$ranges = array(
			'year'       => __( 'Year', 'woocommerce' ),
			'last_month' => __( 'Last month', 'woocommerce' ),
			'month'      => __( 'This month', 'woocommerce' ),
			'7day'       => __( 'Last 7 days', 'woocommerce' ),
		);

		$this->chart_colours = array(
			'discount_amount' => '#3498db',
			'coupon_count'    => '#d4d9dc',
		);

		$current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : '7day';

		if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ) ) ) {
			$current_range = '7day';
		}

		$this->check_current_range_nonce( $current_range );
		$this->calculate_current_range( $current_range );

		include WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php';
	}

	/**
	 * Get chart widgets.
	 *
	 * @return array
	 */
	public function get_chart_widgets() {
		$widgets = array();

		$widgets[] = array(
			'title'    => '',
			'callback' => array( $this, 'coupons_widget' ),
		);

		return $widgets;
	}

	/**
	 * Output coupons widget.
	 */
	public function coupons_widget() {
		?>
		<h4 class="section_title"><span><?php esc_html_e( 'Filter by coupon', 'woocommerce' ); ?></span></h4>
		<div class="section">
			<form method="GET">
				<div>
					<?php
					$used_coupons = $this->get_order_report_data(
						array(
							'data'         => array(
								'order_item_name' => array(
									'type'            => 'order_item',
									'order_item_type' => 'coupon',
									'function'        => '',
									'distinct'        => true,
									'name'            => 'order_item_name',
								),
							),
							'where'        => array(
								array(
									'key'      => 'order_item_type',
									'value'    => 'coupon',
									'operator' => '=',
								),
							),
							'query_type'   => 'get_col',
							'filter_range' => false,
						)
					);

					if ( ! empty( $used_coupons ) && is_array( $used_coupons ) ) :
						?>
						<select id="coupon_codes" name="coupon_codes" class="wc-enhanced-select" data-placeholder="<?php esc_attr_e( 'Choose coupons&hellip;', 'woocommerce' ); ?>" style="width:100%;">
							<option value=""><?php esc_html_e( 'All coupons', 'woocommerce' ); ?></option>
							<?php
							foreach ( $used_coupons as $coupon ) {
								echo '<option value="' . esc_attr( $coupon ) . '"' . wc_selected( $coupon, $this->coupon_codes ) . '>' . esc_html( $coupon ) . '</option>';
							}
							?>
						</select>
						<?php // @codingStandardsIgnoreStart ?>
						<button type="submit" class="submit button" value="<?php esc_attr_e( 'Show', 'woocommerce' ); ?>"><?php esc_html_e( 'Show', 'woocommerce' ); ?></button>
						<input type="hidden" name="range" value="<?php echo ( ! empty( $_GET['range'] ) ) ? esc_attr( wp_unslash( $_GET['range'] ) ) : ''; ?>" />
						<input type="hidden" name="start_date" value="<?php echo ( ! empty( $_GET['start_date'] ) ) ? esc_attr( wp_unslash( $_GET['start_date'] ) ) : ''; ?>" />
						<input type="hidden" name="end_date" value="<?php echo ( ! empty( $_GET['end_date'] ) ) ? esc_attr( wp_unslash( $_GET['end_date'] ) ) : ''; ?>" />
						<input type="hidden" name="page" value="<?php echo ( ! empty( $_GET['page'] ) ) ? esc_attr( wp_unslash( $_GET['page'] ) ) : ''; ?>" />
						<input type="hidden" name="tab" value="<?php echo ( ! empty( $_GET['tab'] ) ) ? esc_attr( wp_unslash( $_GET['tab'] ) ) : ''; ?>" />
						<input type="hidden" name="report" value="<?php echo ( ! empty( $_GET['report'] ) ) ? esc_attr( wp_unslash( $_GET['report'] ) ) : ''; ?>" />
						<?php // @codingStandardsIgnoreEnd ?>
					<?php else : ?>
						<span><?php esc_html_e( 'No used coupons found', 'woocommerce' ); ?></span>
					<?php endif; ?>
				</div>
			</form>
		</div>
		<h4 class="section_title"><span><?php esc_html_e( 'Most popular', 'woocommerce' ); ?></span></h4>
		<div class="section">
			<table cellspacing="0">
				<?php
				$most_popular = $this->get_order_report_data(
					array(
						'data'         => array(
							'order_item_name' => array(
								'type'            => 'order_item',
								'order_item_type' => 'coupon',
								'function'        => '',
								'name'            => 'coupon_code',
							),
							'order_item_id'   => array(
								'type'            => 'order_item',
								'order_item_type' => 'coupon',
								'function'        => 'COUNT',
								'name'            => 'coupon_count',
							),
						),
						'where'        => array(
							array(
								'type'     => 'order_item',
								'key'      => 'order_item_type',
								'value'    => 'coupon',
								'operator' => '=',
							),
						),
						'order_by'     => 'coupon_count DESC',
						'group_by'     => 'order_item_name',
						'limit'        => 12,
						'query_type'   => 'get_results',
						'filter_range' => true,
					)
				);

				if ( ! empty( $most_popular ) && is_array( $most_popular ) ) {
					foreach ( $most_popular as $coupon ) {
						echo '<tr class="' . ( in_array( $coupon->coupon_code, $this->coupon_codes ) ? 'active' : '' ) . '">
							<td class="count" width="1%">' . esc_html( $coupon->coupon_count ) . '</td>
							<td class="name"><a href="' . esc_url( add_query_arg( 'coupon_codes', $coupon->coupon_code ) ) . '">' . esc_html( $coupon->coupon_code ) . '</a></td>
						</tr>';
					}
				} else {
					echo '<tr><td colspan="2">' . esc_html__( 'No coupons found in range', 'woocommerce' ) . '</td></tr>';
				}
				?>
			</table>
		</div>
		<h4 class="section_title"><span><?php esc_html_e( 'Most discount', 'woocommerce' ); ?></span></h4>
		<div class="section">
			<table cellspacing="0">
				<?php
				$most_discount = $this->get_order_report_data(
					array(
						'data'         => array(
							'order_item_name' => array(
								'type'            => 'order_item',
								'order_item_type' => 'coupon',
								'function'        => '',
								'name'            => 'coupon_code',
							),
							'discount_amount' => array(
								'type'            => 'order_item_meta',
								'order_item_type' => 'coupon',
								'function'        => 'SUM',
								'name'            => 'discount_amount',
							),
						),
						'where'        => array(
							array(
								'type'     => 'order_item',
								'key'      => 'order_item_type',
								'value'    => 'coupon',
								'operator' => '=',
							),
						),
						'order_by'     => 'discount_amount DESC',
						'group_by'     => 'order_item_name',
						'limit'        => 12,
						'query_type'   => 'get_results',
						'filter_range' => true,
					)
				);

				if ( ! empty( $most_discount ) && is_array( $most_discount ) ) {
					foreach ( $most_discount as $coupon ) {
						// @codingStandardsIgnoreStart
						echo '<tr class="' . ( in_array( $coupon->coupon_code, $this->coupon_codes ) ? 'active' : '' ) . '">
							<td class="count" width="1%">' . wc_price( $coupon->discount_amount ) . '</td>
							<td class="name"><a href="' . esc_url( add_query_arg( 'coupon_codes', $coupon->coupon_code ) ) . '">' . esc_html( $coupon->coupon_code ) . '</a></td>
						</tr>';
						// @codingStandardsIgnoreEnd
					}
				} else {
					echo '<tr><td colspan="3">' . esc_html__( 'No coupons found in range', 'woocommerce' ) . '</td></tr>';
				}
				?>
			</table>
		</div>
		<script type="text/javascript">
			jQuery( '.section_title' ).on( 'click', function() {
				var next_section = jQuery( this ).next( '.section' );

				if ( jQuery( next_section ).is( ':visible' ) ) {
					return false;
				}

				jQuery( '.section:visible' ).slideUp();
				jQuery( '.section_title' ).removeClass( 'open' );
				jQuery( this ).addClass( 'open' ).next( '.section' ).slideDown();

				return false;
			} );
			jQuery( '.section' ).slideUp( 100, function() {
				<?php if ( empty( $this->coupon_codes ) ) : ?>
					jQuery( '.section_title:eq(1)' ).trigger( 'click' );
				<?php else : ?>
					jQuery( '.section_title:eq(0)' ).trigger( 'click' );
				<?php endif; ?>
			} );
		</script>
		<?php
	}

	/**
	 * Output an export link.
	 */
	public function get_export_button() {
		$current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : '7day';
		?>
		<a
			href="#"
			download="report-<?php echo esc_attr( $current_range ); ?>-<?php echo esc_attr( date_i18n( 'Y-m-d', current_time( 'timestamp' ) ) ); ?>.csv"
			class="export_csv"
			data-export="chart"
			data-xaxes="<?php esc_attr_e( 'Date', 'woocommerce' ); ?>"
			data-groupby="<?php echo esc_attr( $this->chart_groupby ); ?>"
		>
			<?php esc_html_e( 'Export CSV', 'woocommerce' ); ?>
		</a>
		<?php
	}

	/**
	 * Get the main chart.
	 */
	public function get_main_chart() {
		global $wp_locale;

		// Get orders and dates in range - we want the SUM of order totals, COUNT of order items, COUNT of orders, and the date.
		$order_coupon_counts_query = array(
			'data'         => array(
				'order_item_name' => array(
					'type'            => 'order_item',
					'order_item_type' => 'coupon',
					'function'        => 'COUNT',
					'name'            => 'order_coupon_count',
				),
				'post_date'       => array(
					'type'     => 'post_data',
					'function' => '',
					'name'     => 'post_date',
				),
			),
			'where'        => array(
				array(
					'key'      => 'order_item_type',
					'value'    => 'coupon',
					'operator' => '=',
				),
			),
			'group_by'     => $this->group_by_query,
			'order_by'     => 'post_date ASC',
			'query_type'   => 'get_results',
			'filter_range' => true,
			'order_types'  => wc_get_order_types( 'order-count' ),
		);

		$order_discount_amounts_query = array(
			'data'         => array(
				'discount_amount' => array(
					'type'            => 'order_item_meta',
					'order_item_type' => 'coupon',
					'function'        => 'SUM',
					'name'            => 'discount_amount',
				),
				'post_date'       => array(
					'type'     => 'post_data',
					'function' => '',
					'name'     => 'post_date',
				),
			),
			'where'        => array(
				array(
					'key'      => 'order_item_type',
					'value'    => 'coupon',
					'operator' => '=',
				),
			),
			'group_by'     => $this->group_by_query . ', order_item_name',
			'order_by'     => 'post_date ASC',
			'query_type'   => 'get_results',
			'filter_range' => true,
			'order_types'  => wc_get_order_types( 'order-count' ),
		);

		if ( ! empty( $this->coupon_codes ) ) {
			$coupon_code_query = array(
				'type'     => 'order_item',
				'key'      => 'order_item_name',
				'value'    => $this->coupon_codes,
				'operator' => 'IN',
			);

			$order_coupon_counts_query['where'][]    = $coupon_code_query;
			$order_discount_amounts_query['where'][] = $coupon_code_query;
		}

		$order_coupon_counts    = $this->get_order_report_data( $order_coupon_counts_query );
		$order_discount_amounts = $this->get_order_report_data( $order_discount_amounts_query );

		// Prepare data for report.
		$order_coupon_counts    = $this->prepare_chart_data( $order_coupon_counts, 'post_date', 'order_coupon_count', $this->chart_interval, $this->start_date, $this->chart_groupby );
		$order_discount_amounts = $this->prepare_chart_data( $order_discount_amounts, 'post_date', 'discount_amount', $this->chart_interval, $this->start_date, $this->chart_groupby );

		// Encode in json format.
		$chart_data = wp_json_encode(
			array(
				'order_coupon_counts'    => array_values( $order_coupon_counts ),
				'order_discount_amounts' => array_values( $order_discount_amounts ),
			)
		);
		?>
		<div class="chart-container">
			<div class="chart-placeholder main"></div>
		</div>
		<script type="text/javascript">
			var main_chart;

			jQuery(function(){
				var order_data = JSON.parse( decodeURIComponent( '<?php echo rawurlencode( $chart_data ); ?>' ) );

				var drawGraph = function( highlight ) {
					var series = [
						{
							label: "<?php echo esc_js( __( 'Number of coupons used', 'woocommerce' ) ); ?>",
							data: order_data.order_coupon_counts,
							color: '<?php echo esc_js( $this->chart_colours['coupon_count'] ); ?>',
							bars: { fillColor: '<?php echo esc_js( $this->chart_colours['coupon_count'] ); ?>', fill: true, show: true, lineWidth: 0, barWidth: <?php echo esc_js( $this->barwidth ); ?> * 0.5, align: 'center' },
							shadowSize: 0,
							hoverable: false
						},
						{
							label: "<?php echo esc_js( __( 'Discount amount', 'woocommerce' ) ); ?>",
							data: order_data.order_discount_amounts,
							yaxis: 2,
							color: '<?php echo esc_js( $this->chart_colours['discount_amount'] ); ?>',
							points: { show: true, radius: 5, lineWidth: 3, fillColor: '#fff', fill: true },
							lines: { show: true, lineWidth: 4, fill: false },
							shadowSize: 0,
							<?php echo $this->get_currency_tooltip(); ?><?php // @codingStandardsIgnoreLine ?>
						}
					];

					if ( highlight !== 'undefined' && series[ highlight ] ) {
						highlight_series = series[ highlight ];

						highlight_series.color = '#9c5d90';

						if ( highlight_series.bars )
							highlight_series.bars.fillColor = '#9c5d90';

						if ( highlight_series.lines ) {
							highlight_series.lines.lineWidth = 5;
						}
					}

					main_chart = jQuery.plot(
						jQuery('.chart-placeholder.main'),
						series,
						{
							legend: {
								show: false
							},
							grid: {
								color: '#aaa',
								borderColor: 'transparent',
								borderWidth: 0,
								hoverable: true
							},
							xaxes: [ {
								color: '#aaa',
								position: "bottom",
								tickColor: 'transparent',
								mode: "time",
								timeformat: "<?php echo ( 'day' === $this->chart_groupby ) ? '%d %b' : '%b'; ?>",
								monthNames: JSON.parse( decodeURIComponent( '<?php echo rawurlencode( wp_json_encode( array_values( $wp_locale->month_abbrev ) ) ); ?>' ) ),
								tickLength: 1,
								minTickSize: [1, "<?php echo esc_js( $this->chart_groupby ); ?>"],
								font: {
									color: "#aaa"
								}
							} ],
							yaxes: [
								{
									min: 0,
									minTickSize: 1,
									tickDecimals: 0,
									color: '#ecf0f1',
									font: { color: "#aaa" }
								},
								{
									position: "right",
									min: 0,
									tickDecimals: 2,
									alignTicksWithAxis: 1,
									color: 'transparent',
									font: { color: "#aaa" }
								}
							],
						}
					);

					jQuery('.chart-placeholder').trigger( 'resize' );
				}

				drawGraph();

				jQuery('.highlight_series').on( 'mouseenter',
					function() {
						drawGraph( jQuery(this).data('series') );
					} ).on( 'mouseleave',
					function() {
						drawGraph();
					}
				);
			});
		</script>
		<?php
	}
}
admin/reports/class-wc-report-taxes-by-code.php000064400000020111151336044770015537 0ustar00<?php
/**
 * Taxes by tax code report.
 *
 * @package     WooCommerce\Admin\Reports
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * WC_Report_Taxes_By_Code
 *
 * @package     WooCommerce\Admin\Reports
 * @version     2.1.0
 */
class WC_Report_Taxes_By_Code extends WC_Admin_Report {

	/**
	 * Get the legend for the main chart sidebar.
	 *
	 * @return array
	 */
	public function get_chart_legend() {
		return array();
	}

	/**
	 * Output an export link.
	 */
	public function get_export_button() {

		$current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : 'last_month';
		?>
		<a
			href="#"
			download="report-<?php echo esc_attr( $current_range ); ?>-<?php echo esc_attr( date_i18n( 'Y-m-d', current_time( 'timestamp' ) ) ); ?>.csv"
			class="export_csv"
			data-export="table"
		>
			<?php esc_html_e( 'Export CSV', 'woocommerce' ); ?>
		</a>
		<?php
	}

	/**
	 * Output the report.
	 */
	public function output_report() {

		$ranges = array(
			'year'       => __( 'Year', 'woocommerce' ),
			'last_month' => __( 'Last month', 'woocommerce' ),
			'month'      => __( 'This month', 'woocommerce' ),
		);

		$current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : 'last_month';

		if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ) ) ) {
			$current_range = 'last_month';
		}

		$this->check_current_range_nonce( $current_range );
		$this->calculate_current_range( $current_range );

		$hide_sidebar = true;

		include WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php';
	}

	/**
	 * Get the main chart.
	 */
	public function get_main_chart() {
		global $wpdb;

		$query_data = array(
			'order_item_name'     => array(
				'type'     => 'order_item',
				'function' => '',
				'name'     => 'tax_rate',
			),
			'tax_amount'          => array(
				'type'            => 'order_item_meta',
				'order_item_type' => 'tax',
				'function'        => '',
				'name'            => 'tax_amount',
			),
			'shipping_tax_amount' => array(
				'type'            => 'order_item_meta',
				'order_item_type' => 'tax',
				'function'        => '',
				'name'            => 'shipping_tax_amount',
			),
			'rate_id'             => array(
				'type'            => 'order_item_meta',
				'order_item_type' => 'tax',
				'function'        => '',
				'name'            => 'rate_id',
			),
			'ID'                  => array(
				'type'     => 'post_data',
				'function' => '',
				'name'     => 'post_id',
			),
		);

		$query_where = array(
			array(
				'key'      => 'order_item_type',
				'value'    => 'tax',
				'operator' => '=',
			),
			array(
				'key'      => 'order_item_name',
				'value'    => '',
				'operator' => '!=',
			),
		);

		// We exclude on-hold orders as they are still pending payment.
		$tax_rows_orders = $this->get_order_report_data(
			array(
				'data'         => $query_data,
				'where'        => $query_where,
				'order_by'     => 'posts.post_date ASC',
				'query_type'   => 'get_results',
				'filter_range' => true,
				'order_types'  => wc_get_order_types( 'sales-reports' ),
				'order_status' => array( 'completed', 'processing', 'refunded' ),
			)
		);

		$tax_rows_partial_refunds = $this->get_order_report_data(
			array(
				'data'                => $query_data,
				'where'               => $query_where,
				'order_by'            => 'posts.post_date ASC',
				'query_type'          => 'get_results',
				'filter_range'        => true,
				'order_types'         => array( 'shop_order_refund' ),
				'parent_order_status' => array( 'completed', 'processing' ), // Partial refunds inside refunded orders should be ignored.
			)
		);

		$tax_rows_full_refunds = $this->get_order_report_data(
			array(
				'data'                => $query_data,
				'where'               => $query_where,
				'order_by'            => 'posts.post_date ASC',
				'query_type'          => 'get_results',
				'filter_range'        => true,
				'order_types'         => array( 'shop_order_refund' ),
				'parent_order_status' => array( 'refunded' ),
			)
		);

		// Merge.
		$tax_rows = array();

		foreach ( $tax_rows_orders + $tax_rows_partial_refunds as $tax_row ) {
			$key                                    = $tax_row->rate_id;
			$tax_rows[ $key ]                       = isset( $tax_rows[ $key ] ) ? $tax_rows[ $key ] : (object) array(
				'tax_amount'          => 0,
				'shipping_tax_amount' => 0,
				'total_orders'        => 0,
			);
			$tax_rows[ $key ]->total_orders        += 1;
			$tax_rows[ $key ]->tax_rate             = $tax_row->tax_rate;
			$tax_rows[ $key ]->tax_amount          += wc_round_tax_total( $tax_row->tax_amount );
			$tax_rows[ $key ]->shipping_tax_amount += wc_round_tax_total( $tax_row->shipping_tax_amount );
		}

		foreach ( $tax_rows_full_refunds as $tax_row ) {
			$key                                    = $tax_row->rate_id;
			$tax_rows[ $key ]                       = isset( $tax_rows[ $key ] ) ? $tax_rows[ $key ] : (object) array(
				'tax_amount'          => 0,
				'shipping_tax_amount' => 0,
				'total_orders'        => 0,
			);
			$tax_rows[ $key ]->tax_rate             = $tax_row->tax_rate;
			$tax_rows[ $key ]->tax_amount          += wc_round_tax_total( $tax_row->tax_amount );
			$tax_rows[ $key ]->shipping_tax_amount += wc_round_tax_total( $tax_row->shipping_tax_amount );
		}
		?>
		<table class="widefat">
			<thead>
				<tr>
					<th><?php esc_html_e( 'Tax', 'woocommerce' ); ?></th>
					<th><?php esc_html_e( 'Rate', 'woocommerce' ); ?></th>
					<th class="total_row"><?php esc_html_e( 'Number of orders', 'woocommerce' ); ?></th>
					<th class="total_row"><?php esc_html_e( 'Tax amount', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'This is the sum of the "Tax rows" tax amount within your orders.', 'woocommerce' ) ); ?></th>
					<th class="total_row"><?php esc_html_e( 'Shipping tax amount', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'This is the sum of the "Tax rows" shipping tax amount within your orders.', 'woocommerce' ) ); ?></th>
					<th class="total_row"><?php esc_html_e( 'Total tax', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'This is the total tax for the rate (shipping tax + product tax).', 'woocommerce' ) ); ?></th>
				</tr>
			</thead>
			<?php if ( ! empty( $tax_rows ) ) : ?>
				<tbody>
					<?php
					foreach ( $tax_rows as $rate_id => $tax_row ) {
						$rate = $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %d;", $rate_id ) );
						?>
						<tr>
							<th scope="row"><?php echo wp_kses_post( apply_filters( 'woocommerce_reports_taxes_tax_rate', $tax_row->tax_rate, $rate_id, $tax_row ) ); ?></th>
							<td><?php echo wp_kses_post( apply_filters( 'woocommerce_reports_taxes_rate', $rate, $rate_id, $tax_row ) ); ?>%</td>
							<td class="total_row"><?php echo esc_html( $tax_row->total_orders ); ?></td>
							<td class="total_row"><?php echo wc_price( $tax_row->tax_amount ); // phpcs:ignore ?></td>
							<td class="total_row"><?php echo wc_price( $tax_row->shipping_tax_amount ); // phpcs:ignore ?></td>
							<td class="total_row"><?php echo wc_price( $tax_row->tax_amount + $tax_row->shipping_tax_amount ); // phpcs:ignore ?></td>
						</tr>
						<?php
					}
					?>
				</tbody>
				<tfoot>
					<tr>
						<th scope="row" colspan="3"><?php esc_html_e( 'Total', 'woocommerce' ); ?></th>
						<th class="total_row"><?php echo wc_price( wc_round_tax_total( array_sum( wp_list_pluck( (array) $tax_rows, 'tax_amount' ) ) ) ); // phpcs:ignore ?></th>
						<th class="total_row"><?php echo wc_price( wc_round_tax_total( array_sum( wp_list_pluck( (array) $tax_rows, 'shipping_tax_amount' ) ) ) ); // phpcs:ignore ?></th>
						<th class="total_row"><strong><?php echo wc_price( wc_round_tax_total( array_sum( wp_list_pluck( (array) $tax_rows, 'tax_amount' ) ) + array_sum( wp_list_pluck( (array) $tax_rows, 'shipping_tax_amount' ) ) ) ); // phpcs:ignore ?></strong></th>
					</tr>
				</tfoot>
			<?php else : ?>
				<tbody>
					<tr>
						<td><?php esc_html_e( 'No taxes found in this period', 'woocommerce' ); ?></td>
					</tr>
				</tbody>
			<?php endif; ?>
		</table>
		<?php
	}
}
admin/reports/class-wc-report-out-of-stock.php000064400000003173151336044770015436 0ustar00<?php
/**
 * WC_Report_Out_Of_Stock.
 *
 * @package WooCommerce\Admin\Reports
 */

defined( 'ABSPATH' ) || exit;

if ( ! class_exists( 'WC_Report_Stock' ) ) {
	require_once dirname( __FILE__ ) . '/class-wc-report-stock.php';
}

/**
 * WC_Report_Out_Of_Stock class.
 */
class WC_Report_Out_Of_Stock extends WC_Report_Stock {

	/**
	 * No items found text.
	 */
	public function no_items() {
		esc_html_e( 'No out of stock products found.', 'woocommerce' );
	}

	/**
	 * Get Products matching stock criteria.
	 *
	 * @param int $current_page Current page number.
	 * @param int $per_page How many results to show per page.
	 */
	public function get_items( $current_page, $per_page ) {
		global $wpdb;

		$this->max_items = 0;
		$this->items     = array();

		$stock = absint( max( get_option( 'woocommerce_notify_no_stock_amount' ), 0 ) );

		$query_from = apply_filters(
			'woocommerce_report_out_of_stock_query_from',
			$wpdb->prepare(
				"
				FROM {$wpdb->posts} as posts
				INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id
				WHERE 1=1
				AND posts.post_type IN ( 'product', 'product_variation' )
				AND posts.post_status = 'publish'
				AND lookup.stock_quantity <= %d
				",
				$stock
			)
		);

		$this->items     = $wpdb->get_results( $wpdb->prepare( "SELECT SQL_CALC_FOUND_ROWS posts.ID as id, posts.post_parent as parent {$query_from} ORDER BY posts.post_title DESC LIMIT %d, %d;", ( $current_page - 1 ) * $per_page, $per_page ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$this->max_items = $wpdb->get_var( 'SELECT FOUND_ROWS();' ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
	}
}
admin/reports/class-wc-report-downloads.php000064400000024720151336044770015077 0ustar00<?php
/**
 * Download report.
 *
 * @author      WooThemes
 * @category    Admin
 * @package     WooCommerce\Admin\Reports
 * @version     3.3.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'WP_List_Table' ) ) {
	require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}

/**
 * WC_Report_Downloads.
 */
class WC_Report_Downloads extends WP_List_Table {

	/**
	 * Max items.
	 *
	 * @var int
	 */
	protected $max_items;

	/**
	 * Constructor.
	 */
	public function __construct() {

		parent::__construct(
			array(
				'singular' => 'download',
				'plural'   => 'downloads',
				'ajax'     => false,
			)
		);
	}

	/**
	 * Don't need this.
	 *
	 * @param string $position Top or bottom.
	 */
	public function display_tablenav( $position ) {
		if ( 'top' !== $position ) {
			parent::display_tablenav( $position );
		}
	}

	/**
	 * Output the report.
	 */
	public function output_report() {

		$this->prepare_items();

		// Subtitle for permission if set.
		if ( ! empty( $_GET['permission_id'] ) ) { // WPCS: input var ok.
			$permission_id = absint( $_GET['permission_id'] ); // WPCS: input var ok.

			// Load the permission, order, etc. so we can render more information.
			$permission = null;
			$product    = null;

			try {
				$permission = new WC_Customer_Download( $permission_id );
				$product    = wc_get_product( $permission->product_id );
			} catch ( Exception $e ) {
				wp_die( sprintf( esc_html__( 'Permission #%d not found.', 'woocommerce' ), esc_html( $permission_id ) ) );
			}
		}

		echo '<h1>' . esc_html__( 'Customer downloads', 'woocommerce' );

		$filters      = $this->get_filter_vars();
		$filter_list  = array();
		$filter_names = array(
			'product_id'      => __( 'Product', 'woocommerce' ),
			'download_id'     => __( 'File ID', 'woocommerce' ),
			'permission_id'   => __( 'Permission ID', 'woocommerce' ),
			'order_id'        => __( 'Order', 'woocommerce' ),
			'user_id'         => __( 'User', 'woocommerce' ),
			'user_ip_address' => __( 'IP address', 'woocommerce' ),
		);

		foreach ( $filters as $key => $value ) {
			if ( is_null( $value ) ) {
				continue;
			}
			switch ( $key ) {
				case 'order_id':
					$order = wc_get_order( $value );
					if ( $order ) {
						$display_value = _x( '#', 'hash before order number', 'woocommerce' ) . $order->get_order_number();
					} else {
						break 2;
					}
					break;
				case 'product_id':
					$product = wc_get_product( $value );
					if ( $product ) {
						$display_value = $product->get_formatted_name();
					} else {
						break 2;
					}
					break;
				default:
					$display_value = $value;
					break;
			}
			$filter_list[] = $filter_names[ $key ] . ' ' . $display_value . ' <a href="' . esc_url( remove_query_arg( $key ) ) . '" class="woocommerce-reports-remove-filter">&times;</a>';
		}

		echo '</h1>';

		echo '<div id="active-filters" class="woocommerce-reports-wide"><h2>';
		echo esc_html__( 'Active filters', 'woocommerce' ) . ': ';
		echo $filter_list ? wp_kses_post( implode( ', ', $filter_list ) ) : '';
		echo '</h2></div>';

		echo '<div id="poststuff" class="woocommerce-reports-wide">';
		$this->display();
		echo '</div>';
	}

	/**
	 * Get column value.
	 *
	 * @param mixed  $item Item being displayed.
	 * @param string $column_name Column name.
	 */
	public function column_default( $item, $column_name ) {
		$permission = null;
		$product    = null;
		try {
			$permission = new WC_Customer_Download( $item->permission_id );
			$product    = wc_get_product( $permission->product_id );
		} catch ( Exception $e ) {
			// Ok to continue rendering other information even if permission and/or product is not found.
			return;
		}

		switch ( $column_name ) {
			case 'timestamp':
				echo esc_html( $item->timestamp );
				break;
			case 'product':
				if ( ! empty( $product ) ) {
					edit_post_link( esc_html( $product->get_formatted_name() ), '', '', $product->get_id(), 'view-link' );

					echo '<div class="row-actions">';
					echo '<a href="' . esc_url( add_query_arg( 'product_id', $product->get_id() ) ) . '">' . esc_html__( 'Filter by product', 'woocommerce' ) . '</a>';
					echo '</div>';
				}
				break;
			case 'file':
				if ( ! empty( $permission ) && ! empty( $product ) ) {
					// File information.
					$file = $product->get_file( $permission->get_download_id() );

					if ( false === $file ) {
						echo esc_html__( 'File does not exist', 'woocommerce' );
					} else {
						echo esc_html( $file->get_name() . ' - ' . basename( $file->get_file() ) );

						echo '<div class="row-actions">';
						echo '<a href="' . esc_url( add_query_arg( 'download_id', $permission->get_download_id() ) ) . '">' . esc_html__( 'Filter by file', 'woocommerce' ) . '</a>';
						echo '</div>';
					}
				}
				break;
			case 'order':
				if ( ! empty( $permission ) && ( $order = wc_get_order( $permission->order_id ) ) ) {
					edit_post_link( esc_html( _x( '#', 'hash before order number', 'woocommerce' ) . $order->get_order_number() ), '', '', $permission->order_id, 'view-link' );

					echo '<div class="row-actions">';
					echo '<a href="' . esc_url( add_query_arg( 'order_id', $order->get_id() ) ) . '">' . esc_html__( 'Filter by order', 'woocommerce' ) . '</a>';
					echo '</div>';
				}
				break;
			case 'user':
				if ( $item->user_id > 0 ) {
					$user = get_user_by( 'id', $item->user_id );

					if ( ! empty( $user ) ) {
						echo '<a href="' . esc_url( get_edit_user_link( $item->user_id ) ) . '">' . esc_html( $user->display_name ) . '</a>';
						echo '<div class="row-actions">';
						echo '<a href="' . esc_url( add_query_arg( 'user_id', $item->user_id ) ) . '">' . esc_html__( 'Filter by user', 'woocommerce' ) . '</a>';
						echo '</div>';
					}
				} else {
					esc_html_e( 'Guest', 'woocommerce' );
				}
				break;
			case 'user_ip_address':
				echo esc_html( $item->user_ip_address );

				echo '<div class="row-actions">';
				echo '<a href="' . esc_url( add_query_arg( 'user_ip_address', $item->user_ip_address ) ) . '">' . esc_html__( 'Filter by IP address', 'woocommerce' ) . '</a>';
				echo '</div>';
				break;
		}
	}

	/**
	 * Get columns.
	 *
	 * @return array
	 */
	public function get_columns() {
		$columns = array(
			'timestamp'       => __( 'Timestamp', 'woocommerce' ),
			'product'         => __( 'Product', 'woocommerce' ),
			'file'            => __( 'File', 'woocommerce' ),
			'order'           => __( 'Order', 'woocommerce' ),
			'user'            => __( 'User', 'woocommerce' ),
			'user_ip_address' => __( 'IP address', 'woocommerce' ),
		);

		return $columns;
	}

	/**
	 * Prepare download list items.
	 */
	public function prepare_items() {

		$this->_column_headers = array( $this->get_columns(), array(), $this->get_sortable_columns() );
		$current_page          = absint( $this->get_pagenum() );
		// Allow filtering per_page value, but ensure it's at least 1.
		$per_page = max( 1, apply_filters( 'woocommerce_admin_downloads_report_downloads_per_page', 20 ) );

		$this->get_items( $current_page, $per_page );

		/**
		 * Pagination.
		 */
		$this->set_pagination_args(
			array(
				'total_items' => $this->max_items,
				'per_page'    => $per_page,
				'total_pages' => ceil( $this->max_items / $per_page ),
			)
		);
	}

	/**
	 * No items found text.
	 */
	public function no_items() {
		esc_html_e( 'No customer downloads found.', 'woocommerce' );
	}

	/**
	 * Get filters from querystring.
	 *
	 * @return object
	 */
	protected function get_filter_vars() {
		$product_id      = ! empty( $_GET['product_id'] ) ? absint( wp_unslash( $_GET['product_id'] ) ) : null; // WPCS: input var ok.
		$download_id     = ! empty( $_GET['download_id'] ) ? wc_clean( wp_unslash( $_GET['download_id'] ) ) : null; // WPCS: input var ok.
		$permission_id   = ! empty( $_GET['permission_id'] ) ? absint( wp_unslash( $_GET['permission_id'] ) ) : null; // WPCS: input var ok.
		$order_id        = ! empty( $_GET['order_id'] ) ? absint( wp_unslash( $_GET['order_id'] ) ) : null; // WPCS: input var ok.
		$user_id         = ! empty( $_GET['user_id'] ) ? absint( wp_unslash( $_GET['user_id'] ) ) : null; // WPCS: input var ok.
		$user_ip_address = ! empty( $_GET['user_ip_address'] ) ? wc_clean( wp_unslash( $_GET['user_ip_address'] ) ) : null; // WPCS: input var ok.

		return (object) array(
			'product_id'      => $product_id,
			'download_id'     => $download_id,
			'permission_id'   => $permission_id,
			'order_id'        => $order_id,
			'user_id'         => $user_id,
			'user_ip_address' => $user_ip_address,
		);
	}

	/**
	 * Get downloads matching criteria.
	 *
	 * @param int $current_page Current viewed page.
	 * @param int $per_page How many results to show per page.
	 */
	public function get_items( $current_page, $per_page ) {
		global $wpdb;

		$this->max_items = 0;
		$this->items     = array();
		$filters         = $this->get_filter_vars();

		// Get downloads from database.
		$table      = $wpdb->prefix . WC_Customer_Download_Log_Data_Store::get_table_name();
		$query_from = " FROM {$table} as downloads ";

		if ( ! is_null( $filters->product_id ) || ! is_null( $filters->download_id ) || ! is_null( $filters->order_id ) ) {
			$query_from .= " LEFT JOIN {$wpdb->prefix}woocommerce_downloadable_product_permissions as permissions on downloads.permission_id = permissions.permission_id ";
		}

		$query_from .= ' WHERE 1=1 ';

		if ( ! is_null( $filters->product_id ) ) {
			$query_from .= $wpdb->prepare( ' AND product_id = %d ', $filters->product_id );
		}

		if ( ! is_null( $filters->download_id ) ) {
			$query_from .= $wpdb->prepare( ' AND download_id = %s ', $filters->download_id );
		}

		if ( ! is_null( $filters->order_id ) ) {
			$query_from .= $wpdb->prepare( ' AND order_id = %d ', $filters->order_id );
		}

		if ( ! is_null( $filters->permission_id ) ) {
			$query_from .= $wpdb->prepare( ' AND downloads.permission_id = %d ', $filters->permission_id );
		}

		if ( ! is_null( $filters->user_id ) ) {
			$query_from .= $wpdb->prepare( ' AND downloads.user_id = %d ', $filters->user_id );
		}

		if ( ! is_null( $filters->user_ip_address ) ) {
			$query_from .= $wpdb->prepare( ' AND user_ip_address = %s ', $filters->user_ip_address );
		}

		$query_from  = apply_filters( 'woocommerce_report_downloads_query_from', $query_from );
		$query_order = $wpdb->prepare( 'ORDER BY timestamp DESC LIMIT %d, %d;', ( $current_page - 1 ) * $per_page, $per_page );

		$this->items     = $wpdb->get_results( "SELECT * {$query_from} {$query_order}" ); // WPCS: cache ok, db call ok, unprepared SQL ok.
		$this->max_items = $wpdb->get_var( "SELECT COUNT( DISTINCT download_log_id ) {$query_from};" ); // WPCS: cache ok, db call ok, unprepared SQL ok.
	}
}
admin/reports/class-wc-report-sales-by-date.php000064400000076512151336044770015545 0ustar00<?php
/**
 * WC_Report_Sales_By_Date
 *
 * @package     WooCommerce\Admin\Reports
 * @version     2.1.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * WC_Report_Sales_By_Date
 */
class WC_Report_Sales_By_Date extends WC_Admin_Report {

	/**
	 * Chart colors.
	 *
	 * @var array
	 */
	public $chart_colours = array();

	/**
	 * The report data.
	 *
	 * @var stdClass
	 */
	private $report_data;

	/**
	 * Get report data.
	 *
	 * @return stdClass
	 */
	public function get_report_data() {
		if ( empty( $this->report_data ) ) {
			$this->query_report_data();
		}
		return $this->report_data;
	}

	/**
	 * Get all data needed for this report and store in the class.
	 */
	private function query_report_data() {
		$this->report_data = new stdClass();

		$this->report_data->order_counts = (array) $this->get_order_report_data(
			array(
				'data'         => array(
					'ID'        => array(
						'type'     => 'post_data',
						'function' => 'COUNT',
						'name'     => 'count',
						'distinct' => true,
					),
					'post_date' => array(
						'type'     => 'post_data',
						'function' => '',
						'name'     => 'post_date',
					),
				),
				'group_by'     => $this->group_by_query,
				'order_by'     => 'post_date ASC',
				'query_type'   => 'get_results',
				'filter_range' => true,
				'order_types'  => wc_get_order_types( 'order-count' ),
				'order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ),
			)
		);

		$this->report_data->coupons = (array) $this->get_order_report_data(
			array(
				'data'         => array(
					'order_item_name' => array(
						'type'     => 'order_item',
						'function' => '',
						'name'     => 'order_item_name',
					),
					'discount_amount' => array(
						'type'            => 'order_item_meta',
						'order_item_type' => 'coupon',
						'function'        => 'SUM',
						'name'            => 'discount_amount',
					),
					'post_date'       => array(
						'type'     => 'post_data',
						'function' => '',
						'name'     => 'post_date',
					),
				),
				'where'        => array(
					array(
						'key'      => 'order_items.order_item_type',
						'value'    => 'coupon',
						'operator' => '=',
					),
				),
				'group_by'     => $this->group_by_query . ', order_item_name',
				'order_by'     => 'post_date ASC',
				'query_type'   => 'get_results',
				'filter_range' => true,
				'order_types'  => wc_get_order_types( 'order-count' ),
				'order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ),
			)
		);

		// All items from orders - even those refunded.
		$this->report_data->order_items = (array) $this->get_order_report_data(
			array(
				'data'         => array(
					'_qty'      => array(
						'type'            => 'order_item_meta',
						'order_item_type' => 'line_item',
						'function'        => 'SUM',
						'name'            => 'order_item_count',
					),
					'post_date' => array(
						'type'     => 'post_data',
						'function' => '',
						'name'     => 'post_date',
					),
				),
				'where'        => array(
					array(
						'key'      => 'order_items.order_item_type',
						'value'    => 'line_item',
						'operator' => '=',
					),
				),
				'group_by'     => $this->group_by_query,
				'order_by'     => 'post_date ASC',
				'query_type'   => 'get_results',
				'filter_range' => true,
				'order_types'  => wc_get_order_types( 'order-count' ),
				'order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ),
			)
		);

		/**
		 * Get total of fully refunded items.
		 */
		$this->report_data->refunded_order_items = absint(
			$this->get_order_report_data(
				array(
					'data'         => array(
						'_qty' => array(
							'type'            => 'order_item_meta',
							'order_item_type' => 'line_item',
							'function'        => 'SUM',
							'name'            => 'order_item_count',
						),
					),
					'where'        => array(
						array(
							'key'      => 'order_items.order_item_type',
							'value'    => 'line_item',
							'operator' => '=',
						),
					),
					'query_type'   => 'get_var',
					'filter_range' => true,
					'order_types'  => wc_get_order_types( 'order-count' ),
					'order_status' => array( 'refunded' ),
				)
			)
		);

		/**
		 * Order totals by date. Charts should show GROSS amounts to avoid going -ve.
		 */
		$this->report_data->orders = (array) $this->get_order_report_data(
			array(
				'data'         => array(
					'_order_total'        => array(
						'type'     => 'meta',
						'function' => 'SUM',
						'name'     => 'total_sales',
					),
					'_order_shipping'     => array(
						'type'     => 'meta',
						'function' => 'SUM',
						'name'     => 'total_shipping',
					),
					'_order_tax'          => array(
						'type'     => 'meta',
						'function' => 'SUM',
						'name'     => 'total_tax',
					),
					'_order_shipping_tax' => array(
						'type'     => 'meta',
						'function' => 'SUM',
						'name'     => 'total_shipping_tax',
					),
					'post_date'           => array(
						'type'     => 'post_data',
						'function' => '',
						'name'     => 'post_date',
					),
				),
				'group_by'     => $this->group_by_query,
				'order_by'     => 'post_date ASC',
				'query_type'   => 'get_results',
				'filter_range' => true,
				'order_types'  => wc_get_order_types( 'sales-reports' ),
				'order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ),
			)
		);

		/**
		 * If an order is 100% refunded we should look at the parent's totals, but the refunds dates.
		 * We also need to ensure each parent order's values are only counted/summed once.
		 */
		$this->report_data->full_refunds = (array) $this->get_order_report_data(
			array(
				'data'                => array(
					'_order_total'        => array(
						'type'     => 'parent_meta',
						'function' => '',
						'name'     => 'total_refund',
					),
					'_order_shipping'     => array(
						'type'     => 'parent_meta',
						'function' => '',
						'name'     => 'total_shipping',
					),
					'_order_tax'          => array(
						'type'     => 'parent_meta',
						'function' => '',
						'name'     => 'total_tax',
					),
					'_order_shipping_tax' => array(
						'type'     => 'parent_meta',
						'function' => '',
						'name'     => 'total_shipping_tax',
					),
					'post_date'           => array(
						'type'     => 'post_data',
						'function' => '',
						'name'     => 'post_date',
					),
				),
				'group_by'            => 'posts.post_parent',
				'query_type'          => 'get_results',
				'filter_range'        => true,
				'order_status'        => false,
				'parent_order_status' => array( 'refunded' ),
			)
		);

		foreach ( $this->report_data->full_refunds as $key => $order ) {
			$total_refund       = is_numeric( $order->total_refund ) ? $order->total_refund : 0;
			$total_shipping     = is_numeric( $order->total_shipping ) ? $order->total_shipping : 0;
			$total_tax          = is_numeric( $order->total_tax ) ? $order->total_tax : 0;
			$total_shipping_tax = is_numeric( $order->total_shipping_tax ) ? $order->total_shipping_tax : 0;

			$this->report_data->full_refunds[ $key ]->net_refund = $total_refund - ( $total_shipping + $total_tax + $total_shipping_tax );
		}

		/**
		 * Partial refunds. This includes line items, shipping and taxes. Not grouped by date.
		 */
		$this->report_data->partial_refunds = (array) $this->get_order_report_data(
			array(
				'data'                => array(
					'ID'                  => array(
						'type'     => 'post_data',
						'function' => '',
						'name'     => 'refund_id',
					),
					'_refund_amount'      => array(
						'type'     => 'meta',
						'function' => '',
						'name'     => 'total_refund',
					),
					'post_date'           => array(
						'type'     => 'post_data',
						'function' => '',
						'name'     => 'post_date',
					),
					'order_item_type'     => array(
						'type'      => 'order_item',
						'function'  => '',
						'name'      => 'item_type',
						'join_type' => 'LEFT',
					),
					'_order_total'        => array(
						'type'     => 'meta',
						'function' => '',
						'name'     => 'total_sales',
					),
					'_order_shipping'     => array(
						'type'      => 'meta',
						'function'  => '',
						'name'      => 'total_shipping',
						'join_type' => 'LEFT',
					),
					'_order_tax'          => array(
						'type'      => 'meta',
						'function'  => '',
						'name'      => 'total_tax',
						'join_type' => 'LEFT',
					),
					'_order_shipping_tax' => array(
						'type'      => 'meta',
						'function'  => '',
						'name'      => 'total_shipping_tax',
						'join_type' => 'LEFT',
					),
					'_qty'                => array(
						'type'      => 'order_item_meta',
						'function'  => 'SUM',
						'name'      => 'order_item_count',
						'join_type' => 'LEFT',
					),
				),
				'group_by'            => 'refund_id',
				'order_by'            => 'post_date ASC',
				'query_type'          => 'get_results',
				'filter_range'        => true,
				'order_status'        => false,
				'parent_order_status' => array( 'completed', 'processing', 'on-hold' ),
			)
		);

		foreach ( $this->report_data->partial_refunds as $key => $order ) {
			$this->report_data->partial_refunds[ $key ]->net_refund = $order->total_refund - ( $order->total_shipping + $order->total_tax + $order->total_shipping_tax );
		}

		/**
		 * Refund lines - all partial refunds on all order types so we can plot full AND partial refunds on the chart.
		 */
		$this->report_data->refund_lines = (array) $this->get_order_report_data(
			array(
				'data'                => array(
					'ID'                  => array(
						'type'     => 'post_data',
						'function' => '',
						'name'     => 'refund_id',
					),
					'_refund_amount'      => array(
						'type'     => 'meta',
						'function' => '',
						'name'     => 'total_refund',
					),
					'post_date'           => array(
						'type'     => 'post_data',
						'function' => '',
						'name'     => 'post_date',
					),
					'order_item_type'     => array(
						'type'      => 'order_item',
						'function'  => '',
						'name'      => 'item_type',
						'join_type' => 'LEFT',
					),
					'_order_total'        => array(
						'type'     => 'meta',
						'function' => '',
						'name'     => 'total_sales',
					),
					'_order_shipping'     => array(
						'type'      => 'meta',
						'function'  => '',
						'name'      => 'total_shipping',
						'join_type' => 'LEFT',
					),
					'_order_tax'          => array(
						'type'      => 'meta',
						'function'  => '',
						'name'      => 'total_tax',
						'join_type' => 'LEFT',
					),
					'_order_shipping_tax' => array(
						'type'      => 'meta',
						'function'  => '',
						'name'      => 'total_shipping_tax',
						'join_type' => 'LEFT',
					),
					'_qty'                => array(
						'type'      => 'order_item_meta',
						'function'  => 'SUM',
						'name'      => 'order_item_count',
						'join_type' => 'LEFT',
					),
				),
				'group_by'            => 'refund_id',
				'order_by'            => 'post_date ASC',
				'query_type'          => 'get_results',
				'filter_range'        => true,
				'order_status'        => false,
				'parent_order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ),
			)
		);

		/**
		 * Total up refunds. Note: when an order is fully refunded, a refund line will be added.
		 */
		$this->report_data->total_tax_refunded          = 0;
		$this->report_data->total_shipping_refunded     = 0;
		$this->report_data->total_shipping_tax_refunded = 0;
		$this->report_data->total_refunds               = 0;

		$this->report_data->refunded_orders = array_merge( $this->report_data->partial_refunds, $this->report_data->full_refunds );

		foreach ( $this->report_data->refunded_orders as $key => $value ) {
			$this->report_data->total_tax_refunded          += floatval( $value->total_tax < 0 ? $value->total_tax * -1 : $value->total_tax );
			$this->report_data->total_refunds               += floatval( $value->total_refund );
			$this->report_data->total_shipping_tax_refunded += floatval( $value->total_shipping_tax < 0 ? $value->total_shipping_tax * -1 : $value->total_shipping_tax );
			$this->report_data->total_shipping_refunded     += floatval( $value->total_shipping < 0 ? $value->total_shipping * -1 : $value->total_shipping );

			// Only applies to parial.
			if ( isset( $value->order_item_count ) ) {
				$this->report_data->refunded_order_items += floatval( $value->order_item_count < 0 ? $value->order_item_count * -1 : $value->order_item_count );
			}
		}

		// Totals from all orders - including those refunded. Subtract refunded amounts.
		$this->report_data->total_tax          = wc_format_decimal( array_sum( wp_list_pluck( $this->report_data->orders, 'total_tax' ) ) - $this->report_data->total_tax_refunded, 2 );
		$this->report_data->total_shipping     = wc_format_decimal( array_sum( wp_list_pluck( $this->report_data->orders, 'total_shipping' ) ) - $this->report_data->total_shipping_refunded, 2 );
		$this->report_data->total_shipping_tax = wc_format_decimal( array_sum( wp_list_pluck( $this->report_data->orders, 'total_shipping_tax' ) ) - $this->report_data->total_shipping_tax_refunded, 2 );

		// Total the refunds and sales amounts. Sales subract refunds. Note - total_sales also includes shipping costs.
		$this->report_data->total_sales = wc_format_decimal( array_sum( wp_list_pluck( $this->report_data->orders, 'total_sales' ) ) - $this->report_data->total_refunds, 2 );
		$this->report_data->net_sales   = wc_format_decimal( $this->report_data->total_sales - $this->report_data->total_shipping - max( 0, $this->report_data->total_tax ) - max( 0, $this->report_data->total_shipping_tax ), 2 );

		// Calculate average based on net.
		$this->report_data->average_sales       = wc_format_decimal( $this->report_data->net_sales / ( $this->chart_interval + 1 ), 2 );
		$this->report_data->average_total_sales = wc_format_decimal( $this->report_data->total_sales / ( $this->chart_interval + 1 ), 2 );

		// Total orders and discounts also includes those which have been refunded at some point.
		$this->report_data->total_coupons         = number_format( array_sum( wp_list_pluck( $this->report_data->coupons, 'discount_amount' ) ), 2, '.', '' );
		$this->report_data->total_refunded_orders = absint( count( $this->report_data->full_refunds ) );

		// Total orders in this period, even if refunded.
		$this->report_data->total_orders = absint( array_sum( wp_list_pluck( $this->report_data->order_counts, 'count' ) ) );

		// Item items ordered in this period, even if refunded.
		$this->report_data->total_items = absint( array_sum( wp_list_pluck( $this->report_data->order_items, 'order_item_count' ) ) );

		// 3rd party filtering of report data
		$this->report_data = apply_filters( 'woocommerce_admin_report_data', $this->report_data );
	}

	/**
	 * Get the legend for the main chart sidebar.
	 *
	 * @return array
	 */
	public function get_chart_legend() {
		$legend = array();
		$data   = $this->get_report_data();

		switch ( $this->chart_groupby ) {
			case 'day':
				$average_total_sales_title = sprintf(
					/* translators: %s: average total sales */
					__( '%s average gross daily sales', 'woocommerce' ),
					'<strong>' . wc_price( $data->average_total_sales ) . '</strong>'
				);
				$average_sales_title = sprintf(
					/* translators: %s: average sales */
					__( '%s average net daily sales', 'woocommerce' ),
					'<strong>' . wc_price( $data->average_sales ) . '</strong>'
				);
				break;
			case 'month':
			default:
				$average_total_sales_title = sprintf(
					/* translators: %s: average total sales */
					__( '%s average gross monthly sales', 'woocommerce' ),
					'<strong>' . wc_price( $data->average_total_sales ) . '</strong>'
				);
				$average_sales_title = sprintf(
					/* translators: %s: average sales */
					__( '%s average net monthly sales', 'woocommerce' ),
					'<strong>' . wc_price( $data->average_sales ) . '</strong>'
				);
				break;
		}

		$legend[] = array(
			'title'            => sprintf(
				/* translators: %s: total sales */
				__( '%s gross sales in this period', 'woocommerce' ),
				'<strong>' . wc_price( $data->total_sales ) . '</strong>'
			),
			'placeholder'      => __( 'This is the sum of the order totals after any refunds and including shipping and taxes.', 'woocommerce' ),
			'color'            => $this->chart_colours['sales_amount'],
			'highlight_series' => 6,
		);
		if ( $data->average_total_sales > 0 ) {
			$legend[] = array(
				'title'            => $average_total_sales_title,
				'color'            => $this->chart_colours['average'],
				'highlight_series' => 2,
			);
		}

		$legend[] = array(
			'title'            => sprintf(
				/* translators: %s: net sales */
				__( '%s net sales in this period', 'woocommerce' ),
				'<strong>' . wc_price( $data->net_sales ) . '</strong>'
			),
			'placeholder'      => __( 'This is the sum of the order totals after any refunds and excluding shipping and taxes.', 'woocommerce' ),
			'color'            => $this->chart_colours['net_sales_amount'],
			'highlight_series' => 7,
		);
		if ( $data->average_sales > 0 ) {
			$legend[] = array(
				'title'            => $average_sales_title,
				'color'            => $this->chart_colours['net_average'],
				'highlight_series' => 3,
			);
		}

		$legend[] = array(
			'title'            => sprintf(
				/* translators: %s: total orders */
				__( '%s orders placed', 'woocommerce' ),
				'<strong>' . $data->total_orders . '</strong>'
			),
			'color'            => $this->chart_colours['order_count'],
			'highlight_series' => 1,
		);

		$legend[] = array(
			'title'            => sprintf(
				/* translators: %s: total items */
				__( '%s items purchased', 'woocommerce' ),
				'<strong>' . $data->total_items . '</strong>'
			),
			'color'            => $this->chart_colours['item_count'],
			'highlight_series' => 0,
		);
		$legend[] = array(
			'title'            => sprintf(
				/* translators: 1: total refunds 2: total refunded orders 3: refunded items */
				_n( '%1$s refunded %2$d order (%3$d item)', '%1$s refunded %2$d orders (%3$d items)', $this->report_data->total_refunded_orders, 'woocommerce' ),
				'<strong>' . wc_price( $data->total_refunds ) . '</strong>',
				$this->report_data->total_refunded_orders,
				$this->report_data->refunded_order_items
			),
			'color'            => $this->chart_colours['refund_amount'],
			'highlight_series' => 8,
		);
		$legend[] = array(
			'title'            => sprintf(
				/* translators: %s: total shipping */
				__( '%s charged for shipping', 'woocommerce' ),
				'<strong>' . wc_price( $data->total_shipping ) . '</strong>'
			),
			'color'            => $this->chart_colours['shipping_amount'],
			'highlight_series' => 5,
		);
		$legend[] = array(
			'title'            => sprintf(
				/* translators: %s: total coupons */
				__( '%s worth of coupons used', 'woocommerce' ),
				'<strong>' . wc_price( $data->total_coupons ) . '</strong>'
			),
			'color'            => $this->chart_colours['coupon_amount'],
			'highlight_series' => 4,
		);

		return $legend;
	}

	/**
	 * Output the report.
	 */
	public function output_report() {
		$ranges = array(
			'year'       => __( 'Year', 'woocommerce' ),
			'last_month' => __( 'Last month', 'woocommerce' ),
			'month'      => __( 'This month', 'woocommerce' ),
			'7day'       => __( 'Last 7 days', 'woocommerce' ),
		);

		$this->chart_colours = array(
			'sales_amount'     => '#b1d4ea',
			'net_sales_amount' => '#3498db',
			'average'          => '#b1d4ea',
			'net_average'      => '#3498db',
			'order_count'      => '#dbe1e3',
			'item_count'       => '#ecf0f1',
			'shipping_amount'  => '#5cc488',
			'coupon_amount'    => '#f1c40f',
			'refund_amount'    => '#e74c3c',
		);

		$current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : '7day'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended

		if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ), true ) ) {
			$current_range = '7day';
		}

		$this->check_current_range_nonce( $current_range );
		$this->calculate_current_range( $current_range );

		include WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php';
	}

	/**
	 * Output an export link.
	 */
	public function get_export_button() {
		$current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : '7day'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		?>
		<a
			href="#"
			download="report-<?php echo esc_attr( $current_range ); ?>-<?php echo esc_attr( date_i18n( 'Y-m-d', current_time( 'timestamp' ) ) ); ?>.csv"
			class="export_csv"
			data-export="chart"
			data-xaxes="<?php esc_attr_e( 'Date', 'woocommerce' ); ?>"
			data-exclude_series="2"
			data-groupby="<?php echo esc_attr( $this->chart_groupby ); ?>"
		>
			<?php esc_html_e( 'Export CSV', 'woocommerce' ); ?>
		</a>
		<?php
	}

	/**
	 * Round our totals correctly.
	 *
	 * @param array|string $amount Chart total.
	 *
	 * @return array|string
	 */
	private function round_chart_totals( $amount ) {
		if ( is_array( $amount ) ) {
			return array( $amount[0], wc_format_decimal( $amount[1], wc_get_price_decimals() ) );
		} else {
			return wc_format_decimal( $amount, wc_get_price_decimals() );
		}
	}

	/**
	 * Get the main chart.
	 */
	public function get_main_chart() {
		global $wp_locale;

		// Prepare data for report.
		$data = array(
			'order_counts'         => $this->prepare_chart_data( $this->report_data->order_counts, 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby ),
			'order_item_counts'    => $this->prepare_chart_data( $this->report_data->order_items, 'post_date', 'order_item_count', $this->chart_interval, $this->start_date, $this->chart_groupby ),
			'order_amounts'        => $this->prepare_chart_data( $this->report_data->orders, 'post_date', 'total_sales', $this->chart_interval, $this->start_date, $this->chart_groupby ),
			'coupon_amounts'       => $this->prepare_chart_data( $this->report_data->coupons, 'post_date', 'discount_amount', $this->chart_interval, $this->start_date, $this->chart_groupby ),
			'shipping_amounts'     => $this->prepare_chart_data( $this->report_data->orders, 'post_date', 'total_shipping', $this->chart_interval, $this->start_date, $this->chart_groupby ),
			'refund_amounts'       => $this->prepare_chart_data( $this->report_data->refund_lines, 'post_date', 'total_refund', $this->chart_interval, $this->start_date, $this->chart_groupby ),
			'net_refund_amounts'   => $this->prepare_chart_data( $this->report_data->refunded_orders, 'post_date', 'net_refund', $this->chart_interval, $this->start_date, $this->chart_groupby ),
			'shipping_tax_amounts' => $this->prepare_chart_data( $this->report_data->orders, 'post_date', 'total_shipping_tax', $this->chart_interval, $this->start_date, $this->chart_groupby ),
			'tax_amounts'          => $this->prepare_chart_data( $this->report_data->orders, 'post_date', 'total_tax', $this->chart_interval, $this->start_date, $this->chart_groupby ),
			'net_order_amounts'    => array(),
			'gross_order_amounts'  => array(),
		);

		foreach ( $data['order_amounts'] as $order_amount_key => $order_amount_value ) {
			$data['gross_order_amounts'][ $order_amount_key ]     = $order_amount_value;
			$data['gross_order_amounts'][ $order_amount_key ][1] -= $data['refund_amounts'][ $order_amount_key ][1];

			$data['net_order_amounts'][ $order_amount_key ] = $order_amount_value;
			// Subtract the sum of the values from net order amounts.
			$data['net_order_amounts'][ $order_amount_key ][1] -=
				$data['net_refund_amounts'][ $order_amount_key ][1] +
				$data['shipping_amounts'][ $order_amount_key ][1] +
				$data['shipping_tax_amounts'][ $order_amount_key ][1] +
				$data['tax_amounts'][ $order_amount_key ][1];
		}

		// 3rd party filtering of report data.
		$data = apply_filters( 'woocommerce_admin_report_chart_data', $data );

		// Encode in json format.
		$chart_data = wp_json_encode(
			array(
				'order_counts'        => array_values( $data['order_counts'] ),
				'order_item_counts'   => array_values( $data['order_item_counts'] ),
				'order_amounts'       => array_map( array( $this, 'round_chart_totals' ), array_values( $data['order_amounts'] ) ),
				'gross_order_amounts' => array_map( array( $this, 'round_chart_totals' ), array_values( $data['gross_order_amounts'] ) ),
				'net_order_amounts'   => array_map( array( $this, 'round_chart_totals' ), array_values( $data['net_order_amounts'] ) ),
				'shipping_amounts'    => array_map( array( $this, 'round_chart_totals' ), array_values( $data['shipping_amounts'] ) ),
				'coupon_amounts'      => array_map( array( $this, 'round_chart_totals' ), array_values( $data['coupon_amounts'] ) ),
				'refund_amounts'      => array_map( array( $this, 'round_chart_totals' ), array_values( $data['refund_amounts'] ) ),
			)
		);
		?>
		<div class="chart-container">
			<div class="chart-placeholder main"></div>
		</div>
		<script type="text/javascript">

			var main_chart;

			jQuery(function(){
				var order_data = JSON.parse( decodeURIComponent( '<?php echo rawurlencode( $chart_data ); ?>' ) );
				var drawGraph = function( highlight ) {
					var series = [
						{
							label: "<?php echo esc_js( __( 'Number of items sold', 'woocommerce' ) ); ?>",
							data: order_data.order_item_counts,
							color: '<?php echo esc_js( $this->chart_colours['item_count'] ); ?>',
							bars: { fillColor: '<?php echo esc_js( $this->chart_colours['item_count'] ); ?>', fill: true, show: true, lineWidth: 0, barWidth: <?php echo esc_js( $this->barwidth ); ?> * 0.5, align: 'center' },
							shadowSize: 0,
							hoverable: false
						},
						{
							label: "<?php echo esc_js( __( 'Number of orders', 'woocommerce' ) ); ?>",
							data: order_data.order_counts,
							color: '<?php echo esc_js( $this->chart_colours['order_count'] ); ?>',
							bars: { fillColor: '<?php echo esc_js( $this->chart_colours['order_count'] ); ?>', fill: true, show: true, lineWidth: 0, barWidth: <?php echo esc_js( $this->barwidth ); ?> * 0.5, align: 'center' },
							shadowSize: 0,
							hoverable: false
						},
						{
							label: "<?php echo esc_js( __( 'Average gross sales amount', 'woocommerce' ) ); ?>",
							data: [ [ <?php echo esc_js( min( array_keys( $data['order_amounts'] ) ) ); ?>, <?php echo esc_js( $this->report_data->average_total_sales ); ?> ], [ <?php echo esc_js( max( array_keys( $data['order_amounts'] ) ) ); ?>, <?php echo esc_js( $this->report_data->average_total_sales ); ?> ] ],
							yaxis: 2,
							color: '<?php echo esc_js( $this->chart_colours['average'] ); ?>',
							points: { show: false },
							lines: { show: true, lineWidth: 2, fill: false },
							shadowSize: 0,
							hoverable: false
						},
						{
							label: "<?php echo esc_js( __( 'Average net sales amount', 'woocommerce' ) ); ?>",
							data: [ [ <?php echo esc_js( min( array_keys( $data['order_amounts'] ) ) ); ?>, <?php echo esc_js( $this->report_data->average_sales ); ?> ], [ <?php echo esc_js( max( array_keys( $data['order_amounts'] ) ) ); ?>, <?php echo esc_js( $this->report_data->average_sales ); ?> ] ],
							yaxis: 2,
							color: '<?php echo esc_js( $this->chart_colours['net_average'] ); ?>',
							points: { show: false },
							lines: { show: true, lineWidth: 2, fill: false },
							shadowSize: 0,
							hoverable: false
						},
						{
							label: "<?php echo esc_js( __( 'Coupon amount', 'woocommerce' ) ); ?>",
							data: order_data.coupon_amounts,
							yaxis: 2,
							color: '<?php echo esc_js( $this->chart_colours['coupon_amount'] ); ?>',
							points: { show: true, radius: 5, lineWidth: 2, fillColor: '#fff', fill: true },
							lines: { show: true, lineWidth: 2, fill: false },
							shadowSize: 0,
							<?php echo $this->get_currency_tooltip();  // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?>
						},
						{
							label: "<?php echo esc_js( __( 'Shipping amount', 'woocommerce' ) ); ?>",
							data: order_data.shipping_amounts,
							yaxis: 2,
							color: '<?php echo esc_js( $this->chart_colours['shipping_amount'] ); ?>',
							points: { show: true, radius: 5, lineWidth: 2, fillColor: '#fff', fill: true },
							lines: { show: true, lineWidth: 2, fill: false },
							shadowSize: 0,
							prepend_tooltip: "<?php echo get_woocommerce_currency_symbol(); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?>"
						},
						{
							label: "<?php echo esc_js( __( 'Gross sales amount', 'woocommerce' ) ); ?>",
							data: order_data.gross_order_amounts,
							yaxis: 2,
							color: '<?php echo esc_js( $this->chart_colours['sales_amount'] ); ?>',
							points: { show: true, radius: 5, lineWidth: 2, fillColor: '#fff', fill: true },
							lines: { show: true, lineWidth: 2, fill: false },
							shadowSize: 0,
							<?php echo $this->get_currency_tooltip(); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?>
						},
						{
							label: "<?php echo esc_js( __( 'Net sales amount', 'woocommerce' ) ); ?>",
							data: order_data.net_order_amounts,
							yaxis: 2,
							color: '<?php echo esc_js( $this->chart_colours['net_sales_amount'] ); ?>',
							points: { show: true, radius: 6, lineWidth: 4, fillColor: '#fff', fill: true },
							lines: { show: true, lineWidth: 5, fill: false },
							shadowSize: 0,
							<?php echo $this->get_currency_tooltip(); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?>
						},
						{
							label: "<?php echo esc_js( __( 'Refund amount', 'woocommerce' ) ); ?>",
							data: order_data.refund_amounts,
							yaxis: 2,
							color: '<?php echo esc_js( $this->chart_colours['refund_amount'] ); ?>',
							points: { show: true, radius: 5, lineWidth: 2, fillColor: '#fff', fill: true },
							lines: { show: true, lineWidth: 2, fill: false },
							shadowSize: 0,
							prepend_tooltip: "<?php echo get_woocommerce_currency_symbol(); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?>"
						},
					];

					if ( highlight !== 'undefined' && series[ highlight ] ) {
						highlight_series = series[ highlight ];

						highlight_series.color = '#9c5d90';

						if ( highlight_series.bars ) {
							highlight_series.bars.fillColor = '#9c5d90';
						}

						if ( highlight_series.lines ) {
							highlight_series.lines.lineWidth = 5;
						}
					}

					main_chart = jQuery.plot(
						jQuery('.chart-placeholder.main'),
						series,
						{
							legend: {
								show: false
							},
							grid: {
								color: '#aaa',
								borderColor: 'transparent',
								borderWidth: 0,
								hoverable: true
							},
							xaxes: [ {
								color: '#aaa',
								position: "bottom",
								tickColor: 'transparent',
								mode: "time",
								timeformat: "<?php echo ( 'day' === $this->chart_groupby ) ? '%d %b' : '%b'; ?>",
								monthNames: JSON.parse( decodeURIComponent( '<?php echo rawurlencode( wp_json_encode( array_values( $wp_locale->month_abbrev ) ) ); ?>' ) ),
								tickLength: 1,
								minTickSize: [1, "<?php echo esc_js( $this->chart_groupby ); ?>"],
								font: {
									color: "#aaa"
								}
							} ],
							yaxes: [
								{
									min: 0,
									minTickSize: 1,
									tickDecimals: 0,
									color: '#d4d9dc',
									font: { color: "#aaa" }
								},
								{
									position: "right",
									min: 0,
									tickDecimals: 2,
									alignTicksWithAxis: 1,
									color: 'transparent',
									font: { color: "#aaa" }
								}
							],
						}
					);

					jQuery('.chart-placeholder').trigger( 'resize' );
				}

				drawGraph();

				jQuery('.highlight_series').on( 'mouseenter',
					function() {
						drawGraph( jQuery(this).data('series') );
					} ).on( 'mouseleave',
					function() {
						drawGraph();
					}
				);
			});
		</script>
		<?php
	}
}
admin/reports/class-wc-report-customers.php000064400000027071151336044770015133 0ustar00<?php
/**
 * Class WC_Report_Customers file.
 *
 * @package WooCommerce\Reports
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * WC_Report_Customers
 *
 * @package     WooCommerce\Admin\Reports
 * @version     2.1.0
 */
class WC_Report_Customers extends WC_Admin_Report {

	/**
	 * Chart colors.
	 *
	 * @var array
	 */
	public $chart_colours = array();

	/**
	 * Customers.
	 *
	 * @var array
	 */
	public $customers = array();

	/**
	 * Get the legend for the main chart sidebar.
	 *
	 * @return array
	 */
	public function get_chart_legend() {
		$legend = array();

		$legend[] = array(
			/* translators: %s: signups amount */
			'title'            => sprintf( __( '%s signups in this period', 'woocommerce' ), '<strong>' . count( $this->customers ) . '</strong>' ),
			'color'            => $this->chart_colours['signups'],
			'highlight_series' => 2,
		);

		return $legend;
	}

	/**
	 * Get chart widgets.
	 *
	 * @return array
	 */
	public function get_chart_widgets() {
		$widgets = array();

		$widgets[] = array(
			'title'    => '',
			'callback' => array( $this, 'customers_vs_guests' ),
		);

		return $widgets;
	}

	/**
	 * Output customers vs guests chart.
	 */
	public function customers_vs_guests() {

		$customer_order_totals = $this->get_order_report_data(
			array(
				'data'         => array(
					'ID' => array(
						'type'     => 'post_data',
						'function' => 'COUNT',
						'name'     => 'total_orders',
					),
				),
				'where_meta'   => array(
					array(
						'meta_key'   => '_customer_user',
						'meta_value' => '0',
						'operator'   => '>',
					),
				),
				'filter_range' => true,
			)
		);

		$guest_order_totals = $this->get_order_report_data(
			array(
				'data'         => array(
					'ID' => array(
						'type'     => 'post_data',
						'function' => 'COUNT',
						'name'     => 'total_orders',
					),
				),
				'where_meta'   => array(
					array(
						'meta_key'   => '_customer_user',
						'meta_value' => '0',
						'operator'   => '=',
					),
				),
				'filter_range' => true,
			)
		);
		?>
		<div class="chart-container">
			<div class="chart-placeholder customers_vs_guests pie-chart" style="height:200px"></div>
			<ul class="pie-chart-legend">
				<li style="border-color: <?php echo esc_attr( $this->chart_colours['customers'] ); ?>"><?php esc_html_e( 'Customer sales', 'woocommerce' ); ?></li>
				<li style="border-color: <?php echo esc_attr( $this->chart_colours['guests'] ); ?>"><?php esc_html_e( 'Guest sales', 'woocommerce' ); ?></li>
			</ul>
		</div>
		<script type="text/javascript">
			jQuery(function(){
				 jQuery.plot(
					jQuery('.chart-placeholder.customers_vs_guests'),
					[
						{
							label: '<?php esc_html_e( 'Customer orders', 'woocommerce' ); ?>',
							data:  "<?php echo esc_html( $customer_order_totals->total_orders ); ?>",
							color: '<?php echo esc_html( $this->chart_colours['customers'] ); ?>'
						},
						{
							label: '<?php esc_html_e( 'Guest orders', 'woocommerce' ); ?>',
							data:  "<?php echo esc_html( $guest_order_totals->total_orders ); ?>",
							color: '<?php echo esc_html( $this->chart_colours['guests'] ); ?>'
						}
					],
					{
						grid: {
							hoverable: true
						},
						series: {
							pie: {
								show: true,
								radius: 1,
								innerRadius: 0.6,
								label: {
									show: false
								}
							},
							enable_tooltip: true,
							append_tooltip: "<?php echo esc_html( ' ' . __( 'orders', 'woocommerce' ) ); ?>",
						},
						legend: {
							show: false
						}
					}
				);

				jQuery('.chart-placeholder.customers_vs_guests').trigger( 'resize' );
			});
		</script>
		<?php
	}

	/**
	 * Output the report.
	 */
	public function output_report() {

		$ranges = array(
			'year'       => __( 'Year', 'woocommerce' ),
			'last_month' => __( 'Last month', 'woocommerce' ),
			'month'      => __( 'This month', 'woocommerce' ),
			'7day'       => __( 'Last 7 days', 'woocommerce' ),
		);

		$this->chart_colours = array(
			'signups'   => '#3498db',
			'customers' => '#1abc9c',
			'guests'    => '#8fdece',
		);

		$current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : '7day';

		if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ), true ) ) {
			$current_range = '7day';
		}

		$this->check_current_range_nonce( $current_range );
		$this->calculate_current_range( $current_range );

		$admin_users = new WP_User_Query(
			array(
				'role'   => 'administrator',
				'fields' => 'ID',
			)
		);

		$manager_users = new WP_User_Query(
			array(
				'role'   => 'shop_manager',
				'fields' => 'ID',
			)
		);

		$users_query = new WP_User_Query(
			apply_filters(
				'woocommerce_admin_report_customers_user_query_args',
				array(
					'fields'  => array( 'user_registered' ),
					'exclude' => array_merge( $admin_users->get_results(), $manager_users->get_results() ),
				)
			)
		);

		$this->customers = $users_query->get_results();

		foreach ( $this->customers as $key => $customer ) {
			if ( strtotime( $customer->user_registered ) < $this->start_date || strtotime( $customer->user_registered ) > $this->end_date ) {
				unset( $this->customers[ $key ] );
			}
		}

		include WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php';
	}

	/**
	 * Output an export link.
	 */
	public function get_export_button() {

		$current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : '7day';
		?>
		<a
			href="#"
			download="report-<?php echo esc_attr( $current_range ); ?>-<?php echo esc_attr( date_i18n( 'Y-m-d', current_time( 'timestamp' ) ) ); ?>.csv"
			class="export_csv"
			data-export="chart"
			data-xaxes="<?php esc_attr_e( 'Date', 'woocommerce' ); ?>"
			data-groupby="<?php echo esc_attr( $this->chart_groupby ); ?>"
		>
			<?php esc_html_e( 'Export CSV', 'woocommerce' ); ?>
		</a>
		<?php
	}

	/**
	 * Output the main chart.
	 */
	public function get_main_chart() {
		global $wp_locale;

		$customer_orders = $this->get_order_report_data(
			array(
				'data'         => array(
					'ID'        => array(
						'type'     => 'post_data',
						'function' => 'COUNT',
						'name'     => 'total_orders',
					),
					'post_date' => array(
						'type'     => 'post_data',
						'function' => '',
						'name'     => 'post_date',
					),
				),
				'where_meta'   => array(
					array(
						'meta_key'   => '_customer_user',
						'meta_value' => '0',
						'operator'   => '>',
					),
				),
				'group_by'     => $this->group_by_query,
				'order_by'     => 'post_date ASC',
				'query_type'   => 'get_results',
				'filter_range' => true,
			)
		);

		$guest_orders = $this->get_order_report_data(
			array(
				'data'         => array(
					'ID'        => array(
						'type'     => 'post_data',
						'function' => 'COUNT',
						'name'     => 'total_orders',
					),
					'post_date' => array(
						'type'     => 'post_data',
						'function' => '',
						'name'     => 'post_date',
					),
				),
				'where_meta'   => array(
					array(
						'meta_key'   => '_customer_user',
						'meta_value' => '0',
						'operator'   => '=',
					),
				),
				'group_by'     => $this->group_by_query,
				'order_by'     => 'post_date ASC',
				'query_type'   => 'get_results',
				'filter_range' => true,
			)
		);

		$signups         = $this->prepare_chart_data( $this->customers, 'user_registered', '', $this->chart_interval, $this->start_date, $this->chart_groupby );
		$customer_orders = $this->prepare_chart_data( $customer_orders, 'post_date', 'total_orders', $this->chart_interval, $this->start_date, $this->chart_groupby );
		$guest_orders    = $this->prepare_chart_data( $guest_orders, 'post_date', 'total_orders', $this->chart_interval, $this->start_date, $this->chart_groupby );

		$chart_data = wp_json_encode(
			array(
				'signups'         => array_values( $signups ),
				'customer_orders' => array_values( $customer_orders ),
				'guest_orders'    => array_values( $guest_orders ),
			)
		);
		?>
		<div class="chart-container">
			<div class="chart-placeholder main"></div>
		</div>
		<script type="text/javascript">
			var main_chart;

			jQuery(function(){
				var chart_data = JSON.parse( decodeURIComponent( '<?php echo rawurlencode( $chart_data ); ?>' ) );

				var drawGraph = function( highlight ) {
					var series = [
							{
								label: "<?php echo esc_js( __( 'Customer orders', 'woocommerce' ) ); ?>",
								data: chart_data.customer_orders,
								color: '<?php echo esc_html( $this->chart_colours['customers'] ); ?>',
								bars: { fillColor: '<?php echo esc_html( $this->chart_colours['customers'] ); ?>', fill: true, show: true, lineWidth: 0, barWidth: <?php echo esc_html( $this->barwidth ); ?> * 0.5, align: 'center' },
								shadowSize: 0,
								enable_tooltip: true,
								append_tooltip: "<?php echo esc_html( ' ' . __( 'customer orders', 'woocommerce' ) ); ?>",
								stack: true,
							},
							{
								label: "<?php echo esc_js( __( 'Guest orders', 'woocommerce' ) ); ?>",
								data: chart_data.guest_orders,
								color: '<?php echo esc_html( $this->chart_colours['guests'] ); ?>',
								bars: { fillColor: '<?php echo esc_html( $this->chart_colours['guests'] ); ?>', fill: true, show: true, lineWidth: 0, barWidth: <?php echo esc_html( $this->barwidth ); ?> * 0.5, align: 'center' },
								shadowSize: 0,
								enable_tooltip: true,
								append_tooltip: "<?php echo esc_html( ' ' . __( 'guest orders', 'woocommerce' ) ); ?>",
								stack: true,
							},
							{
								label: "<?php echo esc_js( __( 'Signups', 'woocommerce' ) ); ?>",
								data: chart_data.signups,
								color: '<?php echo esc_html( $this->chart_colours['signups'] ); ?>',
								points: { show: true, radius: 5, lineWidth: 3, fillColor: '#fff', fill: true },
								lines: { show: true, lineWidth: 4, fill: false },
								shadowSize: 0,
								enable_tooltip: true,
								append_tooltip: "<?php echo esc_html( ' ' . __( 'new users', 'woocommerce' ) ); ?>",
								stack: false
							},
						];

					if ( highlight !== 'undefined' && series[ highlight ] ) {
						highlight_series = series[ highlight ];

						highlight_series.color = '#9c5d90';

						if ( highlight_series.bars )
							highlight_series.bars.fillColor = '#9c5d90';

						if ( highlight_series.lines ) {
							highlight_series.lines.lineWidth = 5;
						}
					}

					main_chart = jQuery.plot(
						jQuery('.chart-placeholder.main'),
						series,
						{
							legend: {
								show: false
							},
							grid: {
								color: '#aaa',
								borderColor: 'transparent',
								borderWidth: 0,
								hoverable: true
							},
							xaxes: [ {
								color: '#aaa',
								position: "bottom",
								tickColor: 'transparent',
								mode: "time",
								timeformat: "<?php echo ( 'day' === $this->chart_groupby ) ? '%d %b' : '%b'; ?>",
								monthNames: JSON.parse( decodeURIComponent( '<?php echo rawurlencode( wp_json_encode( array_values( $wp_locale->month_abbrev ) ) ); ?>' ) ),
								tickLength: 1,
								minTickSize: [1, "<?php echo esc_html( $this->chart_groupby ); ?>"],
								tickSize: [1, "<?php echo esc_html( $this->chart_groupby ); ?>"],
								font: {
									color: "#aaa"
								}
							} ],
							yaxes: [
								{
									min: 0,
									minTickSize: 1,
									tickDecimals: 0,
									color: '#ecf0f1',
									font: { color: "#aaa" }
								}
							],
						}
					);
					jQuery('.chart-placeholder').trigger( 'resize' );
				}

				drawGraph();

				jQuery('.highlight_series').on( 'mouseenter',
					function() {
						drawGraph( jQuery(this).data('series') );
					} ).on( 'mouseleave',
					function() {
						drawGraph();
					}
				);
			});
		</script>
		<?php
	}
}
admin/reports/class-wc-admin-report.php000064400000053622151336044770014200 0ustar00<?php

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

/**
 * Admin Report.
 *
 * Extended by reports to show charts and stats in admin.
 *
 * @author      WooThemes
 * @category    Admin
 * @package     WooCommerce\Admin\Reports
 * @version     2.1.0
 */
class WC_Admin_Report {

	/**
	 * @var array List of transients name that have been updated and need persisting.
	 */
	protected static $transients_to_update = array();

	/**
	 * @var array The list of transients.
	 */
	protected static $cached_results = array();

	/**
	 * The chart interval.
	 *
	 * @var int
	 */
	public $chart_interval;

	/**
	 * Group by SQL query.
	 *
	 * @var string
	 */
	public $group_by_query;

	/**
	 * The bar width.
	 *
	 * @var int
	 */
	public $barwidth;

	/**
	 * Group chart item by day or month.
	 *
	 * @var string
	 */
	public $chart_groupby;

	/**
	 * The start date of the report.
	 *
	 * @var int timestamp
	 */
	public $start_date;

	/**
	 * The end date of the report.
	 *
	 * @var int timestamp
	 */
	public $end_date;

	/**
	 * Get report totals such as order totals and discount amounts.
	 *
	 * Data example:
	 *
	 * '_order_total' => array(
	 *     'type'     => 'meta',
	 *     'function' => 'SUM',
	 *     'name'     => 'total_sales'
	 * )
	 *
	 * @param  array $args
	 * @return mixed depending on query_type
	 */
	public function get_order_report_data( $args = array() ) {
		global $wpdb;

		$default_args = array(
			'data'                => array(),
			'where'               => array(),
			'where_meta'          => array(),
			'query_type'          => 'get_row',
			'group_by'            => '',
			'order_by'            => '',
			'limit'               => '',
			'filter_range'        => false,
			'nocache'             => false,
			'debug'               => false,
			'order_types'         => wc_get_order_types( 'reports' ),
			'order_status'        => array( 'completed', 'processing', 'on-hold' ),
			'parent_order_status' => false,
		);
		$args         = apply_filters( 'woocommerce_reports_get_order_report_data_args', $args );
		$args         = wp_parse_args( $args, $default_args );

		extract( $args );

		if ( empty( $data ) ) {
			return '';
		}

		$order_status = apply_filters( 'woocommerce_reports_order_statuses', $order_status );

		$query  = array();
		$select = array();

		foreach ( $data as $raw_key => $value ) {
			$key      = sanitize_key( $raw_key );
			$distinct = '';

			if ( isset( $value['distinct'] ) ) {
				$distinct = 'DISTINCT';
			}

			switch ( $value['type'] ) {
				case 'meta':
					$get_key = "meta_{$key}.meta_value";
					break;
				case 'parent_meta':
					$get_key = "parent_meta_{$key}.meta_value";
					break;
				case 'post_data':
					$get_key = "posts.{$key}";
					break;
				case 'order_item_meta':
					$get_key = "order_item_meta_{$key}.meta_value";
					break;
				case 'order_item':
					$get_key = "order_items.{$key}";
					break;
			}

			if ( empty( $get_key ) ) {
				// Skip to the next foreach iteration else the query will be invalid.
				continue;
			}

			if ( $value['function'] ) {
				$get = "{$value['function']}({$distinct} {$get_key})";
			} else {
				$get = "{$distinct} {$get_key}";
			}

			$select[] = "{$get} as {$value['name']}";
		}

		$query['select'] = 'SELECT ' . implode( ',', $select );
		$query['from']   = "FROM {$wpdb->posts} AS posts";

		// Joins
		$joins = array();

		foreach ( ( $data + $where ) as $raw_key => $value ) {
			$join_type = isset( $value['join_type'] ) ? $value['join_type'] : 'INNER';
			$type      = isset( $value['type'] ) ? $value['type'] : false;
			$key       = sanitize_key( $raw_key );

			switch ( $type ) {
				case 'meta':
					$joins[ "meta_{$key}" ] = "{$join_type} JOIN {$wpdb->postmeta} AS meta_{$key} ON ( posts.ID = meta_{$key}.post_id AND meta_{$key}.meta_key = '{$raw_key}' )";
					break;
				case 'parent_meta':
					$joins[ "parent_meta_{$key}" ] = "{$join_type} JOIN {$wpdb->postmeta} AS parent_meta_{$key} ON (posts.post_parent = parent_meta_{$key}.post_id) AND (parent_meta_{$key}.meta_key = '{$raw_key}')";
					break;
				case 'order_item_meta':
					$joins['order_items'] = "{$join_type} JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON (posts.ID = order_items.order_id)";

					if ( ! empty( $value['order_item_type'] ) ) {
						$joins['order_items'] .= " AND (order_items.order_item_type = '{$value['order_item_type']}')";
					}

					$joins[ "order_item_meta_{$key}" ] = "{$join_type} JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS order_item_meta_{$key} ON " .
														"(order_items.order_item_id = order_item_meta_{$key}.order_item_id) " .
														" AND (order_item_meta_{$key}.meta_key = '{$raw_key}')";
					break;
				case 'order_item':
					$joins['order_items'] = "{$join_type} JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON posts.ID = order_items.order_id";
					break;
			}
		}

		if ( ! empty( $where_meta ) ) {
			foreach ( $where_meta as $value ) {
				if ( ! is_array( $value ) ) {
					continue;
				}
				$join_type = isset( $value['join_type'] ) ? $value['join_type'] : 'INNER';
				$type      = isset( $value['type'] ) ? $value['type'] : false;
				$key       = sanitize_key( is_array( $value['meta_key'] ) ? $value['meta_key'][0] . '_array' : $value['meta_key'] );

				if ( 'order_item_meta' === $type ) {

					$joins['order_items']              = "{$join_type} JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON posts.ID = order_items.order_id";
					$joins[ "order_item_meta_{$key}" ] = "{$join_type} JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS order_item_meta_{$key} ON order_items.order_item_id = order_item_meta_{$key}.order_item_id";

				} else {
					// If we have a where clause for meta, join the postmeta table
					$joins[ "meta_{$key}" ] = "{$join_type} JOIN {$wpdb->postmeta} AS meta_{$key} ON posts.ID = meta_{$key}.post_id";
				}
			}
		}

		if ( ! empty( $parent_order_status ) ) {
			$joins['parent'] = "LEFT JOIN {$wpdb->posts} AS parent ON posts.post_parent = parent.ID";
		}

		$query['join'] = implode( ' ', $joins );

		$query['where'] = "
			WHERE 	posts.post_type 	IN ( '" . implode( "','", $order_types ) . "' )
			";

		if ( ! empty( $order_status ) ) {
			$query['where'] .= "
				AND 	posts.post_status 	IN ( 'wc-" . implode( "','wc-", $order_status ) . "')
			";
		}

		if ( ! empty( $parent_order_status ) ) {
			if ( ! empty( $order_status ) ) {
				$query['where'] .= " AND ( parent.post_status IN ( 'wc-" . implode( "','wc-", $parent_order_status ) . "') OR parent.ID IS NULL ) ";
			} else {
				$query['where'] .= " AND parent.post_status IN ( 'wc-" . implode( "','wc-", $parent_order_status ) . "') ";
			}
		}

		if ( $filter_range ) {
			$query['where'] .= "
				AND 	posts.post_date >= '" . date( 'Y-m-d H:i:s', $this->start_date ) . "'
				AND 	posts.post_date < '" . date( 'Y-m-d H:i:s', strtotime( '+1 DAY', $this->end_date ) ) . "'
			";
		}

		if ( ! empty( $where_meta ) ) {

			$relation = isset( $where_meta['relation'] ) ? $where_meta['relation'] : 'AND';

			$query['where'] .= ' AND (';

			foreach ( $where_meta as $index => $value ) {

				if ( ! is_array( $value ) ) {
					continue;
				}

				$key = sanitize_key( is_array( $value['meta_key'] ) ? $value['meta_key'][0] . '_array' : $value['meta_key'] );

				if ( strtolower( $value['operator'] ) == 'in' || strtolower( $value['operator'] ) == 'not in' ) {

					if ( is_array( $value['meta_value'] ) ) {
						$value['meta_value'] = implode( "','", $value['meta_value'] );
					}

					if ( ! empty( $value['meta_value'] ) ) {
						$where_value = "{$value['operator']} ('{$value['meta_value']}')";
					}
				} else {
					$where_value = "{$value['operator']} '{$value['meta_value']}'";
				}

				if ( ! empty( $where_value ) ) {
					if ( $index > 0 ) {
						$query['where'] .= ' ' . $relation;
					}

					if ( isset( $value['type'] ) && 'order_item_meta' === $value['type'] ) {

						if ( is_array( $value['meta_key'] ) ) {
							$query['where'] .= " ( order_item_meta_{$key}.meta_key   IN ('" . implode( "','", $value['meta_key'] ) . "')";
						} else {
							$query['where'] .= " ( order_item_meta_{$key}.meta_key   = '{$value['meta_key']}'";
						}

						$query['where'] .= " AND order_item_meta_{$key}.meta_value {$where_value} )";
					} else {

						if ( is_array( $value['meta_key'] ) ) {
							$query['where'] .= " ( meta_{$key}.meta_key   IN ('" . implode( "','", $value['meta_key'] ) . "')";
						} else {
							$query['where'] .= " ( meta_{$key}.meta_key   = '{$value['meta_key']}'";
						}

						$query['where'] .= " AND meta_{$key}.meta_value {$where_value} )";
					}
				}
			}

			$query['where'] .= ')';
		}

		if ( ! empty( $where ) ) {

			foreach ( $where as $value ) {

				if ( strtolower( $value['operator'] ) == 'in' || strtolower( $value['operator'] ) == 'not in' ) {

					if ( is_array( $value['value'] ) ) {
						$value['value'] = implode( "','", $value['value'] );
					}

					if ( ! empty( $value['value'] ) ) {
						$where_value = "{$value['operator']} ('{$value['value']}')";
					}
				} else {
					$where_value = "{$value['operator']} '{$value['value']}'";
				}

				if ( ! empty( $where_value ) ) {
					$query['where'] .= " AND {$value['key']} {$where_value}";
				}
			}
		}

		if ( $group_by ) {
			$query['group_by'] = "GROUP BY {$group_by}";
		}

		if ( $order_by ) {
			$query['order_by'] = "ORDER BY {$order_by}";
		}

		if ( $limit ) {
			$query['limit'] = "LIMIT {$limit}";
		}

		$query = apply_filters( 'woocommerce_reports_get_order_report_query', $query );
		$query = implode( ' ', $query );

		if ( $debug ) {
			echo '<pre>';
			wc_print_r( $query );
			echo '</pre>';
		}

		if ( $debug || $nocache ) {
			self::enable_big_selects();

			$result = apply_filters( 'woocommerce_reports_get_order_report_data', $wpdb->$query_type( $query ), $data );
		} else {
			$query_hash = md5( $query_type . $query );
			$result     = $this->get_cached_query( $query_hash );
			if ( $result === null ) {
				self::enable_big_selects();

				$result = apply_filters( 'woocommerce_reports_get_order_report_data', $wpdb->$query_type( $query ), $data );
			}
			$this->set_cached_query( $query_hash, $result );
		}

		return $result;
	}

	/**
	 * Init the static hooks of the class.
	 */
	protected static function add_update_transients_hook() {
		if ( ! has_action( 'shutdown', array( 'WC_Admin_Report', 'maybe_update_transients' ) ) ) {
			add_action( 'shutdown', array( 'WC_Admin_Report', 'maybe_update_transients' ) );
		}
	}

	/**
	 * Enables big mysql selects for reports, just once for this session.
	 */
	protected static function enable_big_selects() {
		static $big_selects = false;

		global $wpdb;

		if ( ! $big_selects ) {
			$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
			$big_selects = true;
		}
	}

	/**
	 * Get the cached query result or null if it's not in the cache.
	 *
	 * @param string $query_hash The query hash.
	 *
	 * @return mixed
	 */
	protected function get_cached_query( $query_hash ) {
		$class = strtolower( get_class( $this ) );

		if ( ! isset( self::$cached_results[ $class ] ) ) {
			self::$cached_results[ $class ] = get_transient( strtolower( get_class( $this ) ) );
		}

		if ( isset( self::$cached_results[ $class ][ $query_hash ] ) ) {
			return self::$cached_results[ $class ][ $query_hash ];
		}

		return null;
	}

	/**
	 * Set the cached query result.
	 *
	 * @param string $query_hash The query hash.
	 * @param mixed  $data The data to cache.
	 */
	protected function set_cached_query( $query_hash, $data ) {
		$class = strtolower( get_class( $this ) );

		if ( ! isset( self::$cached_results[ $class ] ) ) {
			self::$cached_results[ $class ] = get_transient( strtolower( get_class( $this ) ) );
		}

		self::add_update_transients_hook();

		self::$transients_to_update[ $class ]          = $class;
		self::$cached_results[ $class ][ $query_hash ] = $data;
	}

	/**
	 * Function to update the modified transients at the end of the request.
	 */
	public static function maybe_update_transients() {
		foreach ( self::$transients_to_update as $key => $transient_name ) {
			set_transient( $transient_name, self::$cached_results[ $transient_name ], DAY_IN_SECONDS );
		}
		// Transients have been updated reset the list.
		self::$transients_to_update = array();
	}

	/**
	 * Put data with post_date's into an array of times.
	 *
	 * @param  array  $data array of your data
	 * @param  string $date_key key for the 'date' field. e.g. 'post_date'
	 * @param  string $data_key key for the data you are charting
	 * @param  int    $interval
	 * @param  string $start_date
	 * @param  string $group_by
	 * @return array
	 */
	public function prepare_chart_data( $data, $date_key, $data_key, $interval, $start_date, $group_by ) {
		$prepared_data = array();

		// Ensure all days (or months) have values in this range.
		if ( 'day' === $group_by ) {
			for ( $i = 0; $i <= $interval; $i ++ ) {
				$time = strtotime( date( 'Ymd', strtotime( "+{$i} DAY", $start_date ) ) ) . '000';

				if ( ! isset( $prepared_data[ $time ] ) ) {
					$prepared_data[ $time ] = array( esc_js( $time ), 0 );
				}
			}
		} else {
			$current_yearnum  = date( 'Y', $start_date );
			$current_monthnum = date( 'm', $start_date );

			for ( $i = 0; $i <= $interval; $i ++ ) {
				$time = strtotime( $current_yearnum . str_pad( $current_monthnum, 2, '0', STR_PAD_LEFT ) . '01' ) . '000';

				if ( ! isset( $prepared_data[ $time ] ) ) {
					$prepared_data[ $time ] = array( esc_js( $time ), 0 );
				}

				$current_monthnum ++;

				if ( $current_monthnum > 12 ) {
					$current_monthnum = 1;
					$current_yearnum  ++;
				}
			}
		}

		foreach ( $data as $d ) {
			switch ( $group_by ) {
				case 'day':
					$time = strtotime( date( 'Ymd', strtotime( $d->$date_key ) ) ) . '000';
					break;
				case 'month':
				default:
					$time = strtotime( date( 'Ym', strtotime( $d->$date_key ) ) . '01' ) . '000';
					break;
			}

			if ( ! isset( $prepared_data[ $time ] ) ) {
				continue;
			}

			if ( $data_key ) {
				$prepared_data[ $time ][1] += $d->$data_key;
			} else {
				$prepared_data[ $time ][1] ++;
			}
		}

		return $prepared_data;
	}

	/**
	 * Prepares a sparkline to show sales in the last X days.
	 *
	 * @param  int    $id ID of the product to show. Blank to get all orders.
	 * @param  int    $days Days of stats to get.
	 * @param  string $type Type of sparkline to get. Ignored if ID is not set.
	 * @return string
	 */
	public function sales_sparkline( $id = '', $days = 7, $type = 'sales' ) {

		if ( $id ) {
			$meta_key = ( 'sales' === $type ) ? '_line_total' : '_qty';

			$data = $this->get_order_report_data(
				array(
					'data'         => array(
						'_product_id' => array(
							'type'            => 'order_item_meta',
							'order_item_type' => 'line_item',
							'function'        => '',
							'name'            => 'product_id',
						),
						$meta_key     => array(
							'type'            => 'order_item_meta',
							'order_item_type' => 'line_item',
							'function'        => 'SUM',
							'name'            => 'sparkline_value',
						),
						'post_date'   => array(
							'type'     => 'post_data',
							'function' => '',
							'name'     => 'post_date',
						),
					),
					'where'        => array(
						array(
							'key'      => 'post_date',
							'value'    => date( 'Y-m-d', strtotime( 'midnight -' . ( $days - 1 ) . ' days', current_time( 'timestamp' ) ) ),
							'operator' => '>',
						),
						array(
							'key'      => 'order_item_meta__product_id.meta_value',
							'value'    => $id,
							'operator' => '=',
						),
					),
					'group_by'     => 'YEAR(posts.post_date), MONTH(posts.post_date), DAY(posts.post_date)',
					'query_type'   => 'get_results',
					'filter_range' => false,
				)
			);
		} else {

			$data = $this->get_order_report_data(
				array(
					'data'         => array(
						'_order_total' => array(
							'type'     => 'meta',
							'function' => 'SUM',
							'name'     => 'sparkline_value',
						),
						'post_date'    => array(
							'type'     => 'post_data',
							'function' => '',
							'name'     => 'post_date',
						),
					),
					'where'        => array(
						array(
							'key'      => 'post_date',
							'value'    => date( 'Y-m-d', strtotime( 'midnight -' . ( $days - 1 ) . ' days', current_time( 'timestamp' ) ) ),
							'operator' => '>',
						),
					),
					'group_by'     => 'YEAR(posts.post_date), MONTH(posts.post_date), DAY(posts.post_date)',
					'query_type'   => 'get_results',
					'filter_range' => false,
				)
			);
		}

		$total = 0;
		foreach ( $data as $d ) {
			$total += $d->sparkline_value;
		}

		if ( 'sales' === $type ) {
			/* translators: 1: total income 2: days */
			$tooltip = sprintf( __( 'Sold %1$s worth in the last %2$d days', 'woocommerce' ), strip_tags( wc_price( $total ) ), $days );
		} else {
			/* translators: 1: total items sold 2: days */
			$tooltip = sprintf( _n( 'Sold %1$d item in the last %2$d days', 'Sold %1$d items in the last %2$d days', $total, 'woocommerce' ), $total, $days );
		}

		$sparkline_data = array_values( $this->prepare_chart_data( $data, 'post_date', 'sparkline_value', $days - 1, strtotime( 'midnight -' . ( $days - 1 ) . ' days', current_time( 'timestamp' ) ), 'day' ) );

		return '<span class="wc_sparkline ' . ( ( 'sales' === $type ) ? 'lines' : 'bars' ) . ' tips" data-color="#777" data-tip="' . esc_attr( $tooltip ) . '" data-barwidth="' . 60 * 60 * 16 * 1000 . '" data-sparkline="' . wc_esc_json( wp_json_encode( $sparkline_data ) ) . '"></span>';
	}

	/**
	 * Get the current range and calculate the start and end dates.
	 *
	 * @param  string $current_range
	 */
	public function calculate_current_range( $current_range ) {

		switch ( $current_range ) {

			case 'custom':
				$this->start_date = max( strtotime( '-20 years' ), strtotime( sanitize_text_field( $_GET['start_date'] ) ) );

				if ( empty( $_GET['end_date'] ) ) {
					$this->end_date = strtotime( 'midnight', current_time( 'timestamp' ) );
				} else {
					$this->end_date = strtotime( 'midnight', strtotime( sanitize_text_field( $_GET['end_date'] ) ) );
				}

				$interval = 0;
				$min_date = $this->start_date;

				while ( ( $min_date = strtotime( '+1 MONTH', $min_date ) ) <= $this->end_date ) {
					$interval ++;
				}

				// 3 months max for day view
				if ( $interval > 3 ) {
					$this->chart_groupby = 'month';
				} else {
					$this->chart_groupby = 'day';
				}
				break;

			case 'year':
				$this->start_date    = strtotime( date( 'Y-01-01', current_time( 'timestamp' ) ) );
				$this->end_date      = strtotime( 'midnight', current_time( 'timestamp' ) );
				$this->chart_groupby = 'month';
				break;

			case 'last_month':
				$first_day_current_month = strtotime( date( 'Y-m-01', current_time( 'timestamp' ) ) );
				$this->start_date        = strtotime( date( 'Y-m-01', strtotime( '-1 DAY', $first_day_current_month ) ) );
				$this->end_date          = strtotime( date( 'Y-m-t', strtotime( '-1 DAY', $first_day_current_month ) ) );
				$this->chart_groupby     = 'day';
				break;

			case 'month':
				$this->start_date    = strtotime( date( 'Y-m-01', current_time( 'timestamp' ) ) );
				$this->end_date      = strtotime( 'midnight', current_time( 'timestamp' ) );
				$this->chart_groupby = 'day';
				break;

			case '7day':
				$this->start_date    = strtotime( '-6 days', strtotime( 'midnight', current_time( 'timestamp' ) ) );
				$this->end_date      = strtotime( 'midnight', current_time( 'timestamp' ) );
				$this->chart_groupby = 'day';
				break;
		}

		// Group by
		switch ( $this->chart_groupby ) {

			case 'day':
				$this->group_by_query = 'YEAR(posts.post_date), MONTH(posts.post_date), DAY(posts.post_date)';
				$this->chart_interval = absint( ceil( max( 0, ( $this->end_date - $this->start_date ) / ( 60 * 60 * 24 ) ) ) );
				$this->barwidth       = 60 * 60 * 24 * 1000;
				break;

			case 'month':
				$this->group_by_query = 'YEAR(posts.post_date), MONTH(posts.post_date)';
				$this->chart_interval = 0;
				$min_date             = strtotime( date( 'Y-m-01', $this->start_date ) );

				while ( ( $min_date = strtotime( '+1 MONTH', $min_date ) ) <= $this->end_date ) {
					$this->chart_interval ++;
				}

				$this->barwidth = 60 * 60 * 24 * 7 * 4 * 1000;
				break;
		}
	}

	/**
	 * Return currency tooltip JS based on WooCommerce currency position settings.
	 *
	 * @return string
	 */
	public function get_currency_tooltip() {
		switch ( get_option( 'woocommerce_currency_pos' ) ) {
			case 'right':
				$currency_tooltip = 'append_tooltip: "' . get_woocommerce_currency_symbol() . '"';
				break;
			case 'right_space':
				$currency_tooltip = 'append_tooltip: "&nbsp;' . get_woocommerce_currency_symbol() . '"';
				break;
			case 'left':
				$currency_tooltip = 'prepend_tooltip: "' . get_woocommerce_currency_symbol() . '"';
				break;
			case 'left_space':
			default:
				$currency_tooltip = 'prepend_tooltip: "' . get_woocommerce_currency_symbol() . '&nbsp;"';
				break;
		}

		return $currency_tooltip;
	}

	/**
	 * Get the main chart.
	 */
	public function get_main_chart() {}

	/**
	 * Get the legend for the main chart sidebar.
	 *
	 * @return array
	 */
	public function get_chart_legend() {
		return array();
	}

	/**
	 * Get chart widgets.
	 *
	 * @return array
	 */
	public function get_chart_widgets() {
		return array();
	}

	/**
	 * Get an export link if needed.
	 */
	public function get_export_button() {}

	/**
	 * Output the report.
	 */
	public function output_report() {}

	/**
	 * Check nonce for current range.
	 *
	 * @since  3.0.4
	 * @param  string $current_range Current range.
	 */
	public function check_current_range_nonce( $current_range ) {
		if ( 'custom' !== $current_range ) {
			return;
		}

		if ( ! isset( $_GET['wc_reports_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['wc_reports_nonce'] ), 'custom_range' ) ) { // WPCS: input var ok, CSRF ok.
			wp_die(
				/* translators: %1$s: open link, %2$s: close link */
				sprintf( esc_html__( 'This report link has expired. %1$sClick here to view the filtered report%2$s.', 'woocommerce' ), '<a href="' . esc_url( wp_nonce_url( esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ), 'custom_range', 'wc_reports_nonce' ) ) . '">', '</a>' ), // @codingStandardsIgnoreLine.
				esc_attr__( 'Confirm navigation', 'woocommerce' )
			);
			exit;
		}
	}
}
admin/reports/class-wc-report-most-stocked.php000064400000002773151336044770015525 0ustar00<?php
/**
 * WC_Report_Most_Stocked.
 *
 * @package WooCommerce\Admin\Reports
 */

defined( 'ABSPATH' ) || exit;

if ( ! class_exists( 'WC_Report_Stock' ) ) {
	require_once dirname( __FILE__ ) . '/class-wc-report-stock.php';
}

/**
 * WC_Report_Most_Stocked.
 */
class WC_Report_Most_Stocked extends WC_Report_Stock {

	/**
	 * Get Products matching stock criteria.
	 *
	 * @param int $current_page Current page number.
	 * @param int $per_page How many results to show per page.
	 */
	public function get_items( $current_page, $per_page ) {
		global $wpdb;

		$this->max_items = 0;
		$this->items     = array();

		$stock = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 0 ) );

		$query_from = apply_filters(
			'woocommerce_report_most_stocked_query_from',
			$wpdb->prepare(
				"
				FROM {$wpdb->posts} as posts
				INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id
				WHERE 1=1
				AND posts.post_type IN ( 'product', 'product_variation' )
				AND posts.post_status = 'publish'
				AND lookup.stock_quantity > %d
				",
				$stock
			)
		);

		$this->items     = $wpdb->get_results( $wpdb->prepare( "SELECT SQL_CALC_FOUND_ROWS posts.ID as id, posts.post_parent as parent {$query_from} ORDER BY lookup.stock_quantity DESC, id ASC LIMIT %d, %d;", ( $current_page - 1 ) * $per_page, $per_page ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$this->max_items = $wpdb->get_var( 'SELECT FOUND_ROWS();' ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
	}
}
admin/reports/class-wc-report-low-in-stock.php000064400000003376151336044770015437 0ustar00<?php
/**
 * WC_Report_Low_In_Stock.
 *
 * @package WooCommerce\Admin\Reports
 */

defined( 'ABSPATH' ) || exit;

if ( ! class_exists( 'WC_Report_Stock' ) ) {
	require_once dirname( __FILE__ ) . '/class-wc-report-stock.php';
}

/**
 * Low stock report class.
 */
class WC_Report_Low_In_Stock extends WC_Report_Stock {

	/**
	 * No items found text.
	 */
	public function no_items() {
		esc_html_e( 'No low in stock products found.', 'woocommerce' );
	}

	/**
	 * Get Products matching stock criteria.
	 *
	 * @param int $current_page Current page number.
	 * @param int $per_page How many results to show per page.
	 */
	public function get_items( $current_page, $per_page ) {
		global $wpdb;

		$this->max_items = 0;
		$this->items     = array();

		$stock   = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) );
		$nostock = absint( max( get_option( 'woocommerce_notify_no_stock_amount' ), 0 ) );

		$query_from = apply_filters(
			'woocommerce_report_low_in_stock_query_from',
			$wpdb->prepare(
				"
				FROM {$wpdb->posts} as posts
				INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id
				WHERE 1=1
				AND posts.post_type IN ( 'product', 'product_variation' )
				AND posts.post_status = 'publish'
				AND lookup.stock_quantity <= %d
				AND lookup.stock_quantity > %d
				",
				$stock,
				$nostock
			)
		);

		$this->items     = $wpdb->get_results( $wpdb->prepare( "SELECT SQL_CALC_FOUND_ROWS posts.ID as id, posts.post_parent as parent {$query_from} ORDER BY posts.post_title DESC LIMIT %d, %d;", ( $current_page - 1 ) * $per_page, $per_page ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$this->max_items = $wpdb->get_var( 'SELECT FOUND_ROWS();' ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
	}
}
admin/reports/class-wc-report-taxes-by-date.php000064400000022316151336044770015553 0ustar00<?php

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

/**
 * WC_Report_Taxes_By_Date
 *
 * @author      WooThemes
 * @category    Admin
 * @package     WooCommerce\Admin\Reports
 * @version     2.1.0
 */
class WC_Report_Taxes_By_Date extends WC_Admin_Report {

	/**
	 * Get the legend for the main chart sidebar.
	 *
	 * @return array
	 */
	public function get_chart_legend() {
		return array();
	}

	/**
	 * Output an export link.
	 */
	public function get_export_button() {

		$current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( $_GET['range'] ) : 'last_month';
		?>
		<a
			href="#"
			download="report-<?php echo esc_attr( $current_range ); ?>-<?php echo date_i18n( 'Y-m-d', current_time( 'timestamp' ) ); ?>.csv"
			class="export_csv"
			data-export="table"
		>
			<?php _e( 'Export CSV', 'woocommerce' ); ?>
		</a>
		<?php
	}

	/**
	 * Output the report.
	 */
	public function output_report() {

		$ranges = array(
			'year'       => __( 'Year', 'woocommerce' ),
			'last_month' => __( 'Last month', 'woocommerce' ),
			'month'      => __( 'This month', 'woocommerce' ),
		);

		$current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( $_GET['range'] ) : 'last_month';

		if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ) ) ) {
			$current_range = 'last_month';
		}

		$this->check_current_range_nonce( $current_range );
		$this->calculate_current_range( $current_range );

		$hide_sidebar = true;

		include WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php';
	}

	/**
	 * Get the main chart.
	 */
	public function get_main_chart() {
		$query_data = array(
			'_order_tax'          => array(
				'type'     => 'meta',
				'function' => 'SUM',
				'name'     => 'tax_amount',
			),
			'_order_shipping_tax' => array(
				'type'     => 'meta',
				'function' => 'SUM',
				'name'     => 'shipping_tax_amount',
			),
			'_order_total'        => array(
				'type'     => 'meta',
				'function' => 'SUM',
				'name'     => 'total_sales',
			),
			'_order_shipping'     => array(
				'type'     => 'meta',
				'function' => 'SUM',
				'name'     => 'total_shipping',
			),
			'ID'                  => array(
				'type'     => 'post_data',
				'function' => 'COUNT',
				'name'     => 'total_orders',
				'distinct' => true,
			),
			'post_date'           => array(
				'type'     => 'post_data',
				'function' => '',
				'name'     => 'post_date',
			),
		);

		// We exlude on-hold orders are they are still pending payment.
		$tax_rows_orders = $this->get_order_report_data(
			array(
				'data'         => $query_data,
				'group_by'     => $this->group_by_query,
				'order_by'     => 'post_date ASC',
				'query_type'   => 'get_results',
				'filter_range' => true,
				'order_types'  => wc_get_order_types( 'sales-reports' ),
				'order_status' => array( 'completed', 'processing', 'refunded' ),
			)
		);

		$tax_rows_full_refunds = $this->get_order_report_data(
			array(
				'data'                => array(
					'ID'          => array(
						'type'     => 'post_data',
						'distinct' => true,
						'function' => '',
						'name'     => 'ID',
					),
					'post_parent' => array(
						'type'     => 'post_data',
						'function' => '',
						'name'     => 'post_parent',
					),
					'post_date'   => array(
						'type'     => 'post_data',
						'function' => '',
						'name'     => 'post_date',
					),
				),
				'query_type'          => 'get_results',
				'filter_range'        => true,
				'order_types'         => array( 'shop_order_refund' ),
				'parent_order_status' => array( 'refunded' ),
			)
		);

		$tax_rows_partial_refunds = $this->get_order_report_data(
			array(
				'data'                => $query_data,
				'group_by'            => $this->group_by_query,
				'order_by'            => 'post_date ASC',
				'query_type'          => 'get_results',
				'filter_range'        => true,
				'order_types'         => array( 'shop_order_refund' ),
				'parent_order_status' => array( 'completed', 'processing' ), // Partial refunds inside refunded orders should be ignored.
			)
		);

		$tax_rows = array();

		foreach ( $tax_rows_orders + $tax_rows_partial_refunds as $tax_row ) {
			$key              = date( ( 'month' === $this->chart_groupby ) ? 'Ym' : 'Ymd', strtotime( $tax_row->post_date ) );
			$tax_rows[ $key ] = isset( $tax_rows[ $key ] ) ? $tax_rows[ $key ] : (object) array(
				'tax_amount'          => 0,
				'shipping_tax_amount' => 0,
				'total_sales'         => 0,
				'total_shipping'      => 0,
				'total_orders'        => 0,
			);
		}

		foreach ( $tax_rows_orders as $tax_row ) {
			$key                                    = date( ( 'month' === $this->chart_groupby ) ? 'Ym' : 'Ymd', strtotime( $tax_row->post_date ) );
			$tax_rows[ $key ]->total_orders        += $tax_row->total_orders;
			$tax_rows[ $key ]->tax_amount          += $tax_row->tax_amount;
			$tax_rows[ $key ]->shipping_tax_amount += $tax_row->shipping_tax_amount;
			$tax_rows[ $key ]->total_sales         += $tax_row->total_sales;
			$tax_rows[ $key ]->total_shipping      += $tax_row->total_shipping;
		}

		foreach ( $tax_rows_partial_refunds as $tax_row ) {
			$key                                    = date( ( 'month' === $this->chart_groupby ) ? 'Ym' : 'Ymd', strtotime( $tax_row->post_date ) );
			$tax_rows[ $key ]->tax_amount          += $tax_row->tax_amount;
			$tax_rows[ $key ]->shipping_tax_amount += $tax_row->shipping_tax_amount;
			$tax_rows[ $key ]->total_sales         += $tax_row->total_sales;
			$tax_rows[ $key ]->total_shipping      += $tax_row->total_shipping;
		}

		foreach ( $tax_rows_full_refunds as $tax_row ) {
			$key              = date( ( 'month' === $this->chart_groupby ) ? 'Ym' : 'Ymd', strtotime( $tax_row->post_date ) );
			$tax_rows[ $key ] = isset( $tax_rows[ $key ] ) ? $tax_rows[ $key ] : (object) array(
				'tax_amount'          => 0,
				'shipping_tax_amount' => 0,
				'total_sales'         => 0,
				'total_shipping'      => 0,
				'total_orders'        => 0,
			);
			$parent_order     = wc_get_order( $tax_row->post_parent );

			if ( $parent_order ) {
				$tax_rows[ $key ]->tax_amount          += $parent_order->get_cart_tax() * -1;
				$tax_rows[ $key ]->shipping_tax_amount += $parent_order->get_shipping_tax() * -1;
				$tax_rows[ $key ]->total_sales         += $parent_order->get_total() * -1;
				$tax_rows[ $key ]->total_shipping      += $parent_order->get_shipping_total() * -1;
			}
		}
		?>
		<table class="widefat">
			<thead>
				<tr>
					<th><?php _e( 'Period', 'woocommerce' ); ?></th>
					<th class="total_row"><?php _e( 'Number of orders', 'woocommerce' ); ?></th>
					<th class="total_row"><?php _e( 'Total sales', 'woocommerce' ); ?> <?php echo wc_help_tip( __( "This is the sum of the 'Order total' field within your orders.", 'woocommerce' ) ); ?></th>
					<th class="total_row"><?php _e( 'Total shipping', 'woocommerce' ); ?> <?php echo wc_help_tip( __( "This is the sum of the 'Shipping total' field within your orders.", 'woocommerce' ) ); ?></th>
					<th class="total_row"><?php _e( 'Total tax', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'This is the total tax for the rate (shipping tax + product tax).', 'woocommerce' ) ); ?></th>
					<th class="total_row"><?php _e( 'Net profit', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'Total sales minus shipping and tax.', 'woocommerce' ) ); ?></th>
				</tr>
			</thead>
			<?php if ( ! empty( $tax_rows ) ) : ?>
				<tbody>
					<?php
					foreach ( $tax_rows as $date => $tax_row ) {
						$gross     = $tax_row->total_sales - $tax_row->total_shipping;
						$total_tax = $tax_row->tax_amount + $tax_row->shipping_tax_amount;
						?>
						<tr>
							<th scope="row">
								<?php echo ( 'month' === $this->chart_groupby ) ? date_i18n( 'F', strtotime( $date . '01' ) ) : date_i18n( get_option( 'date_format' ), strtotime( $date ) ); ?>
							</th>
							<td class="total_row"><?php echo $tax_row->total_orders; ?></td>
							<td class="total_row"><?php echo wc_price( $gross ); ?></td>
							<td class="total_row"><?php echo wc_price( $tax_row->total_shipping ); ?></td>
							<td class="total_row"><?php echo wc_price( $total_tax ); ?></td>
							<td class="total_row"><?php echo wc_price( $gross - $total_tax ); ?></td>
						</tr>
						<?php
					}
					?>
				</tbody>
				<tfoot>
					<?php
						$gross     = array_sum( wp_list_pluck( (array) $tax_rows, 'total_sales' ) ) - array_sum( wp_list_pluck( (array) $tax_rows, 'total_shipping' ) );
						$total_tax = array_sum( wp_list_pluck( (array) $tax_rows, 'tax_amount' ) ) + array_sum( wp_list_pluck( (array) $tax_rows, 'shipping_tax_amount' ) );
					?>
					<tr>
						<th scope="row"><?php _e( 'Totals', 'woocommerce' ); ?></th>
						<th class="total_row"><?php echo array_sum( wp_list_pluck( (array) $tax_rows, 'total_orders' ) ); ?></th>
						<th class="total_row"><?php echo wc_price( $gross ); ?></th>
						<th class="total_row"><?php echo wc_price( array_sum( wp_list_pluck( (array) $tax_rows, 'total_shipping' ) ) ); ?></th>
						<th class="total_row"><?php echo wc_price( $total_tax ); ?></th>
						<th class="total_row"><?php echo wc_price( $gross - $total_tax ); ?></th>
					</tr>
				</tfoot>
			<?php else : ?>
				<tbody>
					<tr>
						<td><?php _e( 'No taxes found in this period', 'woocommerce' ); ?></td>
					</tr>
				</tbody>
			<?php endif; ?>
		</table>
		<?php
	}
}
admin/reports/class-wc-report-customer-list.php000064400000021062151336044770015713 0ustar00<?php
/**
 * Class WC_Report_Customer_List file.
 *
 * @package WooCommerce\Reports
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

if ( ! class_exists( 'WP_List_Table' ) ) {
	require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}

/**
 * WC_Report_Customer_List.
 *
 * @package     WooCommerce\Admin\Reports
 * @version     2.1.0
 */
class WC_Report_Customer_List extends WP_List_Table {

	/**
	 * Constructor.
	 */
	public function __construct() {

		parent::__construct(
			array(
				'singular' => 'customer',
				'plural'   => 'customers',
				'ajax'     => false,
			)
		);
	}

	/**
	 * No items found text.
	 */
	public function no_items() {
		esc_html_e( 'No customers found.', 'woocommerce' );
	}

	/**
	 * Output the report.
	 */
	public function output_report() {
		$this->prepare_items();

		echo '<div id="poststuff" class="woocommerce-reports-wide">';

		if ( ! empty( $_GET['link_orders'] ) && wp_verify_nonce( $_REQUEST['_wpnonce'], 'link_orders' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
			$linked = wc_update_new_customer_past_orders( absint( $_GET['link_orders'] ) );
			/* translators: single or plural number of orders */
			echo '<div class="updated"><p>' . sprintf( esc_html( _n( '%s previous order linked', '%s previous orders linked', $linked, 'woocommerce' ), $linked ) ) . '</p></div>';
		}

		if ( ! empty( $_GET['refresh'] ) && wp_verify_nonce( $_REQUEST['_wpnonce'], 'refresh' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
			$user_id = absint( $_GET['refresh'] );
			$user    = get_user_by( 'id', $user_id );

			delete_user_meta( $user_id, '_money_spent' );
			delete_user_meta( $user_id, '_order_count' );
			delete_user_meta( $user_id, '_last_order' );
			/* translators: User display name */
			echo '<div class="updated"><p>' . sprintf( esc_html__( 'Refreshed stats for %s', 'woocommerce' ), esc_html( $user->display_name ) ) . '</p></div>';
		}

		echo '<form method="post" id="woocommerce_customers">';

		$this->search_box( __( 'Search customers', 'woocommerce' ), 'customer_search' );
		$this->display();

		echo '</form>';
		echo '</div>';
	}

	/**
	 * Get column value.
	 *
	 * @param WP_User $user WP User object.
	 * @param string  $column_name Column name.
	 * @return string
	 */
	public function column_default( $user, $column_name ) {
		switch ( $column_name ) {

			case 'customer_name':
				if ( $user->last_name && $user->first_name ) {
					return $user->last_name . ', ' . $user->first_name;
				} else {
					return '-';
				}

			case 'username':
				return $user->user_login;

			case 'location':
				$state_code   = get_user_meta( $user->ID, 'billing_state', true );
				$country_code = get_user_meta( $user->ID, 'billing_country', true );

				$state   = isset( WC()->countries->states[ $country_code ][ $state_code ] ) ? WC()->countries->states[ $country_code ][ $state_code ] : $state_code;
				$country = isset( WC()->countries->countries[ $country_code ] ) ? WC()->countries->countries[ $country_code ] : $country_code;

				$value = '';

				if ( $state ) {
					$value .= $state . ', ';
				}

				$value .= $country;

				if ( $value ) {
					return $value;
				} else {
					return '-';
				}

			case 'email':
				return '<a href="mailto:' . $user->user_email . '">' . $user->user_email . '</a>';

			case 'spent':
				return wc_price( wc_get_customer_total_spent( $user->ID ) );

			case 'orders':
				return wc_get_customer_order_count( $user->ID );

			case 'last_order':
				$orders = wc_get_orders(
					array(
						'limit'    => 1,
						'status'   => array_map( 'wc_get_order_status_name', wc_get_is_paid_statuses() ),
						'customer' => $user->ID,
					)
				);

				if ( ! empty( $orders ) ) {
					$order = $orders[0];
					return '<a href="' . admin_url( 'post.php?post=' . $order->get_id() . '&action=edit' ) . '">' . _x( '#', 'hash before order number', 'woocommerce' ) . $order->get_order_number() . '</a> &ndash; ' . wc_format_datetime( $order->get_date_created() );
				} else {
					return '-';
				}

				break;

			case 'wc_actions':
				ob_start();
				?><p>
					<?php
					do_action( 'woocommerce_admin_user_actions_start', $user );

					$actions = array();

					$actions['refresh'] = array(
						'url'    => wp_nonce_url( add_query_arg( 'refresh', $user->ID ), 'refresh' ),
						'name'   => __( 'Refresh stats', 'woocommerce' ),
						'action' => 'refresh',
					);

					$actions['edit'] = array(
						'url'    => admin_url( 'user-edit.php?user_id=' . $user->ID ),
						'name'   => __( 'Edit', 'woocommerce' ),
						'action' => 'edit',
					);

					$actions['view'] = array(
						'url'    => admin_url( 'edit.php?post_type=shop_order&_customer_user=' . $user->ID ),
						'name'   => __( 'View orders', 'woocommerce' ),
						'action' => 'view',
					);

					$orders = wc_get_orders(
						array(
							'limit'    => 1,
							'status'   => array_map( 'wc_get_order_status_name', wc_get_is_paid_statuses() ),
							'customer' => array( array( 0, $user->user_email ) ),
						)
					);

					if ( $orders ) {
						$actions['link'] = array(
							'url'    => wp_nonce_url( add_query_arg( 'link_orders', $user->ID ), 'link_orders' ),
							'name'   => __( 'Link previous orders', 'woocommerce' ),
							'action' => 'link',
						);
					}

					$actions = apply_filters( 'woocommerce_admin_user_actions', $actions, $user );

					foreach ( $actions as $action ) {
						printf( '<a class="button tips %s" href="%s" data-tip="%s">%s</a>', esc_attr( $action['action'] ), esc_url( $action['url'] ), esc_attr( $action['name'] ), esc_attr( $action['name'] ) );
					}

					do_action( 'woocommerce_admin_user_actions_end', $user );
					?>
				</p>
				<?php
				$user_actions = ob_get_contents();
				ob_end_clean();

				return $user_actions;
		}

		return '';
	}

	/**
	 * Get columns.
	 *
	 * @return array
	 */
	public function get_columns() {
		$columns = array(
			'customer_name' => __( 'Name (Last, First)', 'woocommerce' ),
			'username'      => __( 'Username', 'woocommerce' ),
			'email'         => __( 'Email', 'woocommerce' ),
			'location'      => __( 'Location', 'woocommerce' ),
			'orders'        => __( 'Orders', 'woocommerce' ),
			'spent'         => __( 'Money spent', 'woocommerce' ),
			'last_order'    => __( 'Last order', 'woocommerce' ),
			'wc_actions'    => __( 'Actions', 'woocommerce' ),
		);

		return $columns;
	}

	/**
	 * Order users by name.
	 *
	 * @param WP_User_Query $query Query that gets passed through.
	 * @return WP_User_Query
	 */
	public function order_by_last_name( $query ) {
		global $wpdb;

		$s = ! empty( $_REQUEST['s'] ) ? wp_unslash( $_REQUEST['s'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

		$query->query_from   .= " LEFT JOIN {$wpdb->usermeta} as meta2 ON ({$wpdb->users}.ID = meta2.user_id) ";
		$query->query_where  .= " AND meta2.meta_key = 'last_name' ";
		$query->query_orderby = ' ORDER BY meta2.meta_value, user_login ASC ';

		if ( $s ) {
			$query->query_from   .= " LEFT JOIN {$wpdb->usermeta} as meta3 ON ({$wpdb->users}.ID = meta3.user_id)";
			$query->query_where  .= " AND ( user_login LIKE '%" . esc_sql( str_replace( '*', '', $s ) ) . "%' OR user_nicename LIKE '%" . esc_sql( str_replace( '*', '', $s ) ) . "%' OR meta3.meta_value LIKE '%" . esc_sql( str_replace( '*', '', $s ) ) . "%' ) ";
			$query->query_orderby = ' GROUP BY ID ' . $query->query_orderby;
		}

		return $query;
	}

	/**
	 * Prepare customer list items.
	 */
	public function prepare_items() {
		$current_page = absint( $this->get_pagenum() );
		$per_page     = 20;

		/**
		 * Init column headers.
		 */
		$this->_column_headers = array( $this->get_columns(), array(), $this->get_sortable_columns() );

		add_action( 'pre_user_query', array( $this, 'order_by_last_name' ) );

		/**
		 * Get users.
		 */
		$admin_users = new WP_User_Query(
			array(
				'role'   => 'administrator',
				'fields' => 'ID',
			)
		);

		$manager_users = new WP_User_Query(
			array(
				'role'   => 'shop_manager',
				'fields' => 'ID',
			)
		);

		$query = new WP_User_Query(
			apply_filters(
				'woocommerce_admin_report_customer_list_user_query_args',
				array(
					'exclude' => array_merge( $admin_users->get_results(), $manager_users->get_results() ),
					'number'  => $per_page,
					'offset'  => ( $current_page - 1 ) * $per_page,
				)
			)
		);

		$this->items = $query->get_results();

		remove_action( 'pre_user_query', array( $this, 'order_by_last_name' ) );

		/**
		 * Pagination.
		 */
		$this->set_pagination_args(
			array(
				'total_items' => $query->total_users,
				'per_page'    => $per_page,
				'total_pages' => ceil( $query->total_users / $per_page ),
			)
		);
	}
}
admin/importers/views/html-product-csv-import-form.php000064400000010372151336044770017227 0ustar00<?php
/**
 * Admin View: Product import form
 *
 * @package WooCommerce\Admin
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>
<form class="wc-progress-form-content woocommerce-importer" enctype="multipart/form-data" method="post">
	<header>
		<h2><?php esc_html_e( 'Import products from a CSV file', 'woocommerce' ); ?></h2>
		<p><?php esc_html_e( 'This tool allows you to import (or merge) product data to your store from a CSV or TXT file.', 'woocommerce' ); ?></p>
	</header>
	<section>
		<table class="form-table woocommerce-importer-options">
			<tbody>
				<tr>
					<th scope="row">
						<label for="upload">
							<?php esc_html_e( 'Choose a CSV file from your computer:', 'woocommerce' ); ?>
						</label>
					</th>
					<td>
						<?php
						if ( ! empty( $upload_dir['error'] ) ) {
							?>
							<div class="inline error">
								<p><?php esc_html_e( 'Before you can upload your import file, you will need to fix the following error:', 'woocommerce' ); ?></p>
								<p><strong><?php echo esc_html( $upload_dir['error'] ); ?></strong></p>
							</div>
							<?php
						} else {
							?>
							<input type="file" id="upload" name="import" size="25" />
							<input type="hidden" name="action" value="save" />
							<input type="hidden" name="max_file_size" value="<?php echo esc_attr( $bytes ); ?>" />
							<br>
							<small>
								<?php
								printf(
									/* translators: %s: maximum upload size */
									esc_html__( 'Maximum size: %s', 'woocommerce' ),
									esc_html( $size )
								);
								?>
							</small>
							<?php
						}
						?>
					</td>
				</tr>
				<tr>
					<th><label for="woocommerce-importer-update-existing"><?php esc_html_e( 'Update existing products', 'woocommerce' ); ?></label><br/></th>
					<td>
						<input type="hidden" name="update_existing" value="0" />
						<input type="checkbox" id="woocommerce-importer-update-existing" name="update_existing" value="1" />
						<label for="woocommerce-importer-update-existing"><?php esc_html_e( 'Existing products that match by ID or SKU will be updated. Products that do not exist will be skipped.', 'woocommerce' ); ?></label>
					</td>
				</tr>
				<tr class="woocommerce-importer-advanced hidden">
					<th>
						<label for="woocommerce-importer-file-url"><?php esc_html_e( 'Alternatively, enter the path to a CSV file on your server:', 'woocommerce' ); ?></label>
					</th>
					<td>
						<label for="woocommerce-importer-file-url" class="woocommerce-importer-file-url-field-wrapper">
							<code><?php echo esc_html( ABSPATH ) . ' '; ?></code><input type="text" id="woocommerce-importer-file-url" name="file_url" />
						</label>
					</td>
				</tr>
				<tr class="woocommerce-importer-advanced hidden">
					<th><label><?php esc_html_e( 'CSV Delimiter', 'woocommerce' ); ?></label><br/></th>
					<td><input type="text" name="delimiter" placeholder="," size="2" /></td>
				</tr>
				<tr class="woocommerce-importer-advanced hidden">
					<th><label><?php esc_html_e( 'Use previous column mapping preferences?', 'woocommerce' ); ?></label><br/></th>
					<td><input type="checkbox" id="woocommerce-importer-map-preferences" name="map_preferences" value="1" /></td>
				</tr>
			</tbody>
		</table>
	</section>
	<script type="text/javascript">
		jQuery(function() {
			jQuery( '.woocommerce-importer-toggle-advanced-options' ).on( 'click', function() {
				var elements = jQuery( '.woocommerce-importer-advanced' );
				if ( elements.is( '.hidden' ) ) {
					elements.removeClass( 'hidden' );
					jQuery( this ).text( jQuery( this ).data( 'hidetext' ) );
				} else {
					elements.addClass( 'hidden' );
					jQuery( this ).text( jQuery( this ).data( 'showtext' ) );
				}
				return false;
			} );
		});
	</script>
	<div class="wc-actions">
		<a href="#" class="woocommerce-importer-toggle-advanced-options" data-hidetext="<?php esc_attr_e( 'Hide advanced options', 'woocommerce' ); ?>" data-showtext="<?php esc_attr_e( 'Show advanced options', 'woocommerce' ); ?>"><?php esc_html_e( 'Show advanced options', 'woocommerce' ); ?></a>
		<button type="submit" class="button button-primary button-next" value="<?php esc_attr_e( 'Continue', 'woocommerce' ); ?>" name="save_step"><?php esc_html_e( 'Continue', 'woocommerce' ); ?></button>
		<?php wp_nonce_field( 'woocommerce-csv-importer' ); ?>
	</div>
</form>
admin/importers/views/html-csv-import-done.php000064400000005642151336044770015537 0ustar00<?php
/**
 * Admin View: Importer - Done!
 *
 * @package WooCommerce\Admin\Importers
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>
<div class="wc-progress-form-content woocommerce-importer">
	<section class="woocommerce-importer-done">
		<?php
		$results = array();

		if ( 0 < $imported ) {
			$results[] = sprintf(
				/* translators: %d: products count */
				_n( '%s product imported', '%s products imported', $imported, 'woocommerce' ),
				'<strong>' . number_format_i18n( $imported ) . '</strong>'
			);
		}

		if ( 0 < $updated ) {
			$results[] = sprintf(
				/* translators: %d: products count */
				_n( '%s product updated', '%s products updated', $updated, 'woocommerce' ),
				'<strong>' . number_format_i18n( $updated ) . '</strong>'
			);
		}

		if ( 0 < $skipped ) {
			$results[] = sprintf(
				/* translators: %d: products count */
				_n( '%s product was skipped', '%s products were skipped', $skipped, 'woocommerce' ),
				'<strong>' . number_format_i18n( $skipped ) . '</strong>'
			);
		}

		if ( 0 < $failed ) {
			$results [] = sprintf(
				/* translators: %d: products count */
				_n( 'Failed to import %s product', 'Failed to import %s products', $failed, 'woocommerce' ),
				'<strong>' . number_format_i18n( $failed ) . '</strong>'
			);
		}

		if ( 0 < $failed || 0 < $skipped ) {
			$results[] = '<a href="#" class="woocommerce-importer-done-view-errors">' . __( 'View import log', 'woocommerce' ) . '</a>';
		}

		if ( ! empty( $file_name ) ) {
			$results[] = sprintf(
				/* translators: %s: File name */
				__( 'File uploaded: %s', 'woocommerce' ),
				'<strong>' . $file_name . '</strong>'
			);
		}

		/* translators: %d: import results */
		echo wp_kses_post( __( 'Import complete!', 'woocommerce' ) . ' ' . implode( '. ', $results ) );
		?>
	</section>
	<section class="wc-importer-error-log" style="display:none">
		<table class="widefat wc-importer-error-log-table">
			<thead>
				<tr>
					<th><?php esc_html_e( 'Product', 'woocommerce' ); ?></th>
					<th><?php esc_html_e( 'Reason for failure', 'woocommerce' ); ?></th>
				</tr>
			</thead>
			<tbody>
				<?php
				if ( count( $errors ) ) {
					foreach ( $errors as $error ) {
						if ( ! is_wp_error( $error ) ) {
							continue;
						}
						$error_data = $error->get_error_data();
						?>
						<tr>
							<th><code><?php echo esc_html( $error_data['row'] ); ?></code></th>
							<td><?php echo esc_html( $error->get_error_message() ); ?></td>
						</tr>
						<?php
					}
				}
				?>
			</tbody>
		</table>
	</section>
	<script type="text/javascript">
		jQuery(function() {
			jQuery( '.woocommerce-importer-done-view-errors' ).on( 'click', function() {
				jQuery( '.wc-importer-error-log' ).slideToggle();
				return false;
			} );
		} );
	</script>
	<div class="wc-actions">
		<a class="button button-primary" href="<?php echo esc_url( admin_url( 'edit.php?post_type=product' ) ); ?>"><?php esc_html_e( 'View products', 'woocommerce' ); ?></a>
	</div>
</div>
admin/importers/views/html-csv-import-mapping.php000064400000005755151336044770016252 0ustar00<?php
/**
 * Admin View: Importer - CSV mapping
 *
 * @package WooCommerce\Admin\Importers
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>
<form class="wc-progress-form-content woocommerce-importer" method="post" action="<?php echo esc_url( $this->get_next_step_link() ); ?>">
	<header>
		<h2><?php esc_html_e( 'Map CSV fields to products', 'woocommerce' ); ?></h2>
		<p><?php esc_html_e( 'Select fields from your CSV file to map against products fields, or to ignore during import.', 'woocommerce' ); ?></p>
	</header>
	<section class="wc-importer-mapping-table-wrapper">
		<table class="widefat wc-importer-mapping-table">
			<thead>
				<tr>
					<th><?php esc_html_e( 'Column name', 'woocommerce' ); ?></th>
					<th><?php esc_html_e( 'Map to field', 'woocommerce' ); ?></th>
				</tr>
			</thead>
			<tbody>
				<?php foreach ( $headers as $index => $name ) : ?>
					<?php $mapped_value = $mapped_items[ $index ]; ?>
					<tr>
						<td class="wc-importer-mapping-table-name">
							<?php echo esc_html( $name ); ?>
							<?php if ( ! empty( $sample[ $index ] ) ) : ?>
								<span class="description"><?php esc_html_e( 'Sample:', 'woocommerce' ); ?> <code><?php echo esc_html( $sample[ $index ] ); ?></code></span>
							<?php endif; ?>
						</td>
						<td class="wc-importer-mapping-table-field">
							<input type="hidden" name="map_from[<?php echo esc_attr( $index ); ?>]" value="<?php echo esc_attr( $name ); ?>" />
							<select name="map_to[<?php echo esc_attr( $index ); ?>]">
								<option value=""><?php esc_html_e( 'Do not import', 'woocommerce' ); ?></option>
								<option value="">--------------</option>
								<?php foreach ( $this->get_mapping_options( $mapped_value ) as $key => $value ) : ?>
									<?php if ( is_array( $value ) ) : ?>
										<optgroup label="<?php echo esc_attr( $value['name'] ); ?>">
											<?php foreach ( $value['options'] as $sub_key => $sub_value ) : ?>
												<option value="<?php echo esc_attr( $sub_key ); ?>" <?php selected( $mapped_value, $sub_key ); ?>><?php echo esc_html( $sub_value ); ?></option>
											<?php endforeach ?>
										</optgroup>
									<?php else : ?>
										<option value="<?php echo esc_attr( $key ); ?>" <?php selected( $mapped_value, $key ); ?>><?php echo esc_html( $value ); ?></option>
									<?php endif; ?>
								<?php endforeach ?>
							</select>
						</td>
					</tr>
				<?php endforeach; ?>
			</tbody>
		</table>
	</section>
	<div class="wc-actions">
		<button type="submit" class="button button-primary button-next" value="<?php esc_attr_e( 'Run the importer', 'woocommerce' ); ?>" name="save_step"><?php esc_html_e( 'Run the importer', 'woocommerce' ); ?></button>
		<input type="hidden" name="file" value="<?php echo esc_attr( $this->file ); ?>" />
		<input type="hidden" name="delimiter" value="<?php echo esc_attr( $this->delimiter ); ?>" />
		<input type="hidden" name="update_existing" value="<?php echo (int) $this->update_existing; ?>" />
		<?php wp_nonce_field( 'woocommerce-csv-importer' ); ?>
	</div>
</form>
admin/importers/views/html-csv-import-header.php000064400000000420151336044770016027 0ustar00<?php
/**
 * Admin View: Header
 *
 * @package WooCommerce\Admin\Importers
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>
<div class="wrap woocommerce">
	<h1><?php esc_html_e( 'Import Products', 'woocommerce' ); ?></h1>

	<div class="woocommerce-progress-form-wrapper">
admin/importers/views/html-csv-import-steps.php000064400000001146151336044770015743 0ustar00<?php
/**
 * Admin View: Steps
 *
 * @package WooCommerce\Admin\Importers
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>
<ol class="wc-progress-steps">
	<?php foreach ( $this->steps as $step_key => $step ) : ?>
		<?php
		$step_class = '';
		if ( $step_key === $this->step ) {
			$step_class = 'active';
		} elseif ( array_search( $this->step, array_keys( $this->steps ), true ) > array_search( $step_key, array_keys( $this->steps ), true ) ) {
			$step_class = 'done';
		}
		?>
		<li class="<?php echo esc_attr( $step_class ); ?>">
			<?php echo esc_html( $step['name'] ); ?>
		</li>
	<?php endforeach; ?>
</ol>
admin/importers/views/html-csv-import-progress.php000064400000001067151336044770016453 0ustar00<?php
/**
 * Admin View: Importer - CSV import progress
 *
 * @package WooCommerce\Admin\Importers
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>
<div class="wc-progress-form-content woocommerce-importer woocommerce-importer__importing">
	<header>
		<span class="spinner is-active"></span>
		<h2><?php esc_html_e( 'Importing', 'woocommerce' ); ?></h2>
		<p><?php esc_html_e( 'Your products are now being imported...', 'woocommerce' ); ?></p>
	</header>
	<section>
		<progress class="woocommerce-importer-progress" max="100" value="0"></progress>
	</section>
</div>
admin/importers/views/html-csv-import-footer.php000064400000000213151336044770016075 0ustar00<?php
/**
 * Admin View: Header
 *
 * @package WooCommerce\Admin\Importers
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>
	</div>
</div>
admin/importers/class-wc-tax-rate-importer.php000064400000021675151336044770015514 0ustar00<?php
/**
 * Tax importer class file
 *
 * @version 2.3.0
 * @package WooCommerce\Admin
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'WP_Importer' ) ) {
	return;
}

/**
 * Tax Rates importer - import tax rates and local tax rates into WooCommerce.
 *
 * @package     WooCommerce\Admin\Importers
 * @version     2.3.0
 */
class WC_Tax_Rate_Importer extends WP_Importer {

	/**
	 * The current file id.
	 *
	 * @var int
	 */
	public $id;

	/**
	 * The current file url.
	 *
	 * @var string
	 */
	public $file_url;

	/**
	 * The current import page.
	 *
	 * @var string
	 */
	public $import_page;

	/**
	 * The current delimiter.
	 *
	 * @var string
	 */
	public $delimiter;

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->import_page = 'woocommerce_tax_rate_csv';
		$this->delimiter   = empty( $_POST['delimiter'] ) ? ',' : (string) wc_clean( wp_unslash( $_POST['delimiter'] ) ); // WPCS: CSRF ok.
	}

	/**
	 * Registered callback function for the WordPress Importer.
	 *
	 * Manages the three separate stages of the CSV import process.
	 */
	public function dispatch() {

		$this->header();

		$step = empty( $_GET['step'] ) ? 0 : (int) $_GET['step'];

		switch ( $step ) {

			case 0:
				$this->greet();
				break;

			case 1:
				check_admin_referer( 'import-upload' );

				if ( $this->handle_upload() ) {

					if ( $this->id ) {
						$file = get_attached_file( $this->id );
					} else {
						$file = ABSPATH . $this->file_url;
					}

					add_filter( 'http_request_timeout', array( $this, 'bump_request_timeout' ) );

					$this->import( $file );
				}
				break;
		}

		$this->footer();
	}

	/**
	 * Import is starting.
	 */
	private function import_start() {
		if ( function_exists( 'gc_enable' ) ) {
			gc_enable(); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.gc_enableFound
		}
		wc_set_time_limit( 0 );
		@ob_flush();
		@flush();
		@ini_set( 'auto_detect_line_endings', '1' );
	}

	/**
	 * UTF-8 encode the data if `$enc` value isn't UTF-8.
	 *
	 * @param mixed  $data Data.
	 * @param string $enc Encoding.
	 * @return string
	 */
	public function format_data_from_csv( $data, $enc ) {
		return ( 'UTF-8' === $enc ) ? $data : utf8_encode( $data );
	}

	/**
	 * Import the file if it exists and is valid.
	 *
	 * @param mixed $file File.
	 */
	public function import( $file ) {
		if ( ! is_file( $file ) ) {
			$this->import_error( __( 'The file does not exist, please try again.', 'woocommerce' ) );
		}

		$this->import_start();

		$loop   = 0;
		$handle = fopen( $file, 'r' );

		if ( false !== $handle ) {

			$header = fgetcsv( $handle, 0, $this->delimiter );

			if ( 10 === count( $header ) ) {

				$row = fgetcsv( $handle, 0, $this->delimiter );

				while ( false !== $row ) {

					list( $country, $state, $postcode, $city, $rate, $name, $priority, $compound, $shipping, $class ) = $row;

					$tax_rate = array(
						'tax_rate_country'  => $country,
						'tax_rate_state'    => $state,
						'tax_rate'          => $rate,
						'tax_rate_name'     => $name,
						'tax_rate_priority' => $priority,
						'tax_rate_compound' => $compound ? 1 : 0,
						'tax_rate_shipping' => $shipping ? 1 : 0,
						'tax_rate_order'    => $loop ++,
						'tax_rate_class'    => $class,
					);

					$tax_rate_id = WC_Tax::_insert_tax_rate( $tax_rate );
					WC_Tax::_update_tax_rate_postcodes( $tax_rate_id, wc_clean( $postcode ) );
					WC_Tax::_update_tax_rate_cities( $tax_rate_id, wc_clean( $city ) );

					$row = fgetcsv( $handle, 0, $this->delimiter );
				}
			} else {
				$this->import_error( __( 'The CSV is invalid.', 'woocommerce' ) );
			}

			fclose( $handle );
		}

		// Show Result.
		echo '<div class="updated settings-error"><p>';
		printf(
			/* translators: %s: tax rates count */
			esc_html__( 'Import complete - imported %s tax rates.', 'woocommerce' ),
			'<strong>' . absint( $loop ) . '</strong>'
		);
		echo '</p></div>';

		$this->import_end();
	}

	/**
	 * Performs post-import cleanup of files and the cache.
	 */
	public function import_end() {
		echo '<p>' . esc_html__( 'All done!', 'woocommerce' ) . ' <a href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=tax' ) ) . '">' . esc_html__( 'View tax rates', 'woocommerce' ) . '</a></p>';

		do_action( 'import_end' );
	}

	/**
	 * Handles the CSV upload and initial parsing of the file to prepare for.
	 * displaying author import options.
	 *
	 * @return bool False if error uploading or invalid file, true otherwise
	 */
	public function handle_upload() {
		$file_url = isset( $_POST['file_url'] ) ? wc_clean( wp_unslash( $_POST['file_url'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce already verified in WC_Tax_Rate_Importer::dispatch()

		if ( empty( $file_url ) ) {
			$file = wp_import_handle_upload();

			if ( isset( $file['error'] ) ) {
				$this->import_error( $file['error'] );
			}

			if ( ! wc_is_file_valid_csv( $file['file'], false ) ) {
				// Remove file if not valid.
				wp_delete_attachment( $file['id'], true );

				$this->import_error( __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) );
			}

			$this->id = absint( $file['id'] );
		} elseif ( file_exists( ABSPATH . $file_url ) ) {
			if ( ! wc_is_file_valid_csv( ABSPATH . $file_url ) ) {
				$this->import_error( __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) );
			}

			$this->file_url = esc_attr( $file_url );
		} else {
			$this->import_error();
		}

		return true;
	}

	/**
	 * Output header html.
	 */
	public function header() {
		echo '<div class="wrap">';
		echo '<h1>' . esc_html__( 'Import tax rates', 'woocommerce' ) . '</h1>';
	}

	/**
	 * Output footer html.
	 */
	public function footer() {
		echo '</div>';
	}

	/**
	 * Output information about the uploading process.
	 */
	public function greet() {

		echo '<div class="narrow">';
		echo '<p>' . esc_html__( 'Hi there! Upload a CSV file containing tax rates to import the contents into your shop. Choose a .csv file to upload, then click "Upload file and import".', 'woocommerce' ) . '</p>';

		/* translators: 1: Link to tax rates sample file 2: Closing link. */
		echo '<p>' . sprintf( esc_html__( 'Your CSV needs to include columns in a specific order. %1$sClick here to download a sample%2$s.', 'woocommerce' ), '<a href="' . esc_url( WC()->plugin_url() ) . '/sample-data/sample_tax_rates.csv">', '</a>' ) . '</p>';

		$action = 'admin.php?import=woocommerce_tax_rate_csv&step=1';

		$bytes      = apply_filters( 'import_upload_size_limit', wp_max_upload_size() );
		$size       = size_format( $bytes );
		$upload_dir = wp_upload_dir();
		if ( ! empty( $upload_dir['error'] ) ) :
			?>
			<div class="error">
				<p><?php esc_html_e( 'Before you can upload your import file, you will need to fix the following error:', 'woocommerce' ); ?></p>
				<p><strong><?php echo esc_html( $upload_dir['error'] ); ?></strong></p>
			</div>
		<?php else : ?>
			<form enctype="multipart/form-data" id="import-upload-form" method="post" action="<?php echo esc_attr( wp_nonce_url( $action, 'import-upload' ) ); ?>">
				<table class="form-table">
					<tbody>
						<tr>
							<th>
								<label for="upload"><?php esc_html_e( 'Choose a file from your computer:', 'woocommerce' ); ?></label>
							</th>
							<td>
								<input type="file" id="upload" name="import" size="25" />
								<input type="hidden" name="action" value="save" />
								<input type="hidden" name="max_file_size" value="<?php echo absint( $bytes ); ?>" />
								<small>
									<?php
									printf(
										/* translators: %s: maximum upload size */
										esc_html__( 'Maximum size: %s', 'woocommerce' ),
										esc_attr( $size )
									);
									?>
								</small>
							</td>
						</tr>
						<tr>
							<th>
								<label for="file_url"><?php esc_html_e( 'OR enter path to file:', 'woocommerce' ); ?></label>
							</th>
							<td>
								<?php echo ' ' . esc_html( ABSPATH ) . ' '; ?><input type="text" id="file_url" name="file_url" size="25" />
							</td>
						</tr>
						<tr>
							<th><label><?php esc_html_e( 'Delimiter', 'woocommerce' ); ?></label><br/></th>
							<td><input type="text" name="delimiter" placeholder="," size="2" /></td>
						</tr>
					</tbody>
				</table>
				<p class="submit">
					<button type="submit" class="button" value="<?php esc_attr_e( 'Upload file and import', 'woocommerce' ); ?>"><?php esc_html_e( 'Upload file and import', 'woocommerce' ); ?></button>
				</p>
			</form>
			<?php
		endif;

		echo '</div>';
	}

	/**
	 * Show import error and quit.
	 *
	 * @param  string $message Error message.
	 */
	private function import_error( $message = '' ) {
		echo '<p><strong>' . esc_html__( 'Sorry, there has been an error.', 'woocommerce' ) . '</strong><br />';
		if ( $message ) {
			echo esc_html( $message );
		}
		echo '</p>';
		$this->footer();
		die();
	}

	/**
	 * Added to http_request_timeout filter to force timeout at 60 seconds during import.
	 *
	 * @param  int $val Value.
	 * @return int 60
	 */
	public function bump_request_timeout( $val ) {
		return 60;
	}
}
admin/importers/class-wc-product-csv-importer-controller.php000064400000061740151336044770020416 0ustar00<?php
/**
 * Class WC_Product_CSV_Importer_Controller file.
 *
 * @package WooCommerce\Admin\Importers
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'WP_Importer' ) ) {
	return;
}

/**
 * Product importer controller - handles file upload and forms in admin.
 *
 * @package     WooCommerce\Admin\Importers
 * @version     3.1.0
 */
class WC_Product_CSV_Importer_Controller {

	/**
	 * The path to the current file.
	 *
	 * @var string
	 */
	protected $file = '';

	/**
	 * The current import step.
	 *
	 * @var string
	 */
	protected $step = '';

	/**
	 * Progress steps.
	 *
	 * @var array
	 */
	protected $steps = array();

	/**
	 * Errors.
	 *
	 * @var array
	 */
	protected $errors = array();

	/**
	 * The current delimiter for the file being read.
	 *
	 * @var string
	 */
	protected $delimiter = ',';

	/**
	 * Whether to use previous mapping selections.
	 *
	 * @var bool
	 */
	protected $map_preferences = false;

	/**
	 * Whether to skip existing products.
	 *
	 * @var bool
	 */
	protected $update_existing = false;

	/**
	 * Get importer instance.
	 *
	 * @param  string $file File to import.
	 * @param  array  $args Importer arguments.
	 * @return WC_Product_CSV_Importer
	 */
	public static function get_importer( $file, $args = array() ) {
		$importer_class = apply_filters( 'woocommerce_product_csv_importer_class', 'WC_Product_CSV_Importer' );
		$args           = apply_filters( 'woocommerce_product_csv_importer_args', $args, $importer_class );
		return new $importer_class( $file, $args );
	}

	/**
	 * Check whether a file is a valid CSV file.
	 *
	 * @todo Replace this method with wc_is_file_valid_csv() function.
	 * @param string $file File path.
	 * @param bool   $check_path Whether to also check the file is located in a valid location (Default: true).
	 * @return bool
	 */
	public static function is_file_valid_csv( $file, $check_path = true ) {
		if ( $check_path && apply_filters( 'woocommerce_product_csv_importer_check_import_file_path', true ) && false !== stripos( $file, '://' ) ) {
			return false;
		}

		$valid_filetypes = self::get_valid_csv_filetypes();
		$filetype        = wp_check_filetype( $file, $valid_filetypes );
		if ( in_array( $filetype['type'], $valid_filetypes, true ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Get all the valid filetypes for a CSV file.
	 *
	 * @return array
	 */
	protected static function get_valid_csv_filetypes() {
		return apply_filters(
			'woocommerce_csv_product_import_valid_filetypes',
			array(
				'csv' => 'text/csv',
				'txt' => 'text/plain',
			)
		);
	}

	/**
	 * Constructor.
	 */
	public function __construct() {
		$default_steps = array(
			'upload'  => array(
				'name'    => __( 'Upload CSV file', 'woocommerce' ),
				'view'    => array( $this, 'upload_form' ),
				'handler' => array( $this, 'upload_form_handler' ),
			),
			'mapping' => array(
				'name'    => __( 'Column mapping', 'woocommerce' ),
				'view'    => array( $this, 'mapping_form' ),
				'handler' => '',
			),
			'import'  => array(
				'name'    => __( 'Import', 'woocommerce' ),
				'view'    => array( $this, 'import' ),
				'handler' => '',
			),
			'done'    => array(
				'name'    => __( 'Done!', 'woocommerce' ),
				'view'    => array( $this, 'done' ),
				'handler' => '',
			),
		);

		$this->steps = apply_filters( 'woocommerce_product_csv_importer_steps', $default_steps );

		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		$this->step            = isset( $_REQUEST['step'] ) ? sanitize_key( $_REQUEST['step'] ) : current( array_keys( $this->steps ) );
		$this->file            = isset( $_REQUEST['file'] ) ? wc_clean( wp_unslash( $_REQUEST['file'] ) ) : '';
		$this->update_existing = isset( $_REQUEST['update_existing'] ) ? (bool) $_REQUEST['update_existing'] : false;
		$this->delimiter       = ! empty( $_REQUEST['delimiter'] ) ? wc_clean( wp_unslash( $_REQUEST['delimiter'] ) ) : ',';
		$this->map_preferences = isset( $_REQUEST['map_preferences'] ) ? (bool) $_REQUEST['map_preferences'] : false;
		// phpcs:enable

		// Import mappings for CSV data.
		include_once dirname( __FILE__ ) . '/mappings/mappings.php';

		if ( $this->map_preferences ) {
			add_filter( 'woocommerce_csv_product_import_mapped_columns', array( $this, 'auto_map_user_preferences' ), 9999 );
		}
	}

	/**
	 * Get the URL for the next step's screen.
	 *
	 * @param string $step  slug (default: current step).
	 * @return string       URL for next step if a next step exists.
	 *                      Admin URL if it's the last step.
	 *                      Empty string on failure.
	 */
	public function get_next_step_link( $step = '' ) {
		if ( ! $step ) {
			$step = $this->step;
		}

		$keys = array_keys( $this->steps );

		if ( end( $keys ) === $step ) {
			return admin_url();
		}

		$step_index = array_search( $step, $keys, true );

		if ( false === $step_index ) {
			return '';
		}

		$params = array(
			'step'            => $keys[ $step_index + 1 ],
			'file'            => str_replace( DIRECTORY_SEPARATOR, '/', $this->file ),
			'delimiter'       => $this->delimiter,
			'update_existing' => $this->update_existing,
			'map_preferences' => $this->map_preferences,
			'_wpnonce'        => wp_create_nonce( 'woocommerce-csv-importer' ), // wp_nonce_url() escapes & to &amp; breaking redirects.
		);

		return add_query_arg( $params );
	}

	/**
	 * Output header view.
	 */
	protected function output_header() {
		include dirname( __FILE__ ) . '/views/html-csv-import-header.php';
	}

	/**
	 * Output steps view.
	 */
	protected function output_steps() {
		include dirname( __FILE__ ) . '/views/html-csv-import-steps.php';
	}

	/**
	 * Output footer view.
	 */
	protected function output_footer() {
		include dirname( __FILE__ ) . '/views/html-csv-import-footer.php';
	}

	/**
	 * Add error message.
	 *
	 * @param string $message Error message.
	 * @param array  $actions List of actions with 'url' and 'label'.
	 */
	protected function add_error( $message, $actions = array() ) {
		$this->errors[] = array(
			'message' => $message,
			'actions' => $actions,
		);
	}

	/**
	 * Add error message.
	 */
	protected function output_errors() {
		if ( ! $this->errors ) {
			return;
		}

		foreach ( $this->errors as $error ) {
			echo '<div class="error inline">';
			echo '<p>' . esc_html( $error['message'] ) . '</p>';

			if ( ! empty( $error['actions'] ) ) {
				echo '<p>';
				foreach ( $error['actions'] as $action ) {
					echo '<a class="button button-primary" href="' . esc_url( $action['url'] ) . '">' . esc_html( $action['label'] ) . '</a> ';
				}
				echo '</p>';
			}
			echo '</div>';
		}
	}

	/**
	 * Dispatch current step and show correct view.
	 */
	public function dispatch() {
		// phpcs:ignore WordPress.Security.NonceVerification.Missing
		if ( ! empty( $_POST['save_step'] ) && ! empty( $this->steps[ $this->step ]['handler'] ) ) {
			call_user_func( $this->steps[ $this->step ]['handler'], $this );
		}
		$this->output_header();
		$this->output_steps();
		$this->output_errors();
		call_user_func( $this->steps[ $this->step ]['view'], $this );
		$this->output_footer();
	}

	/**
	 * Output information about the uploading process.
	 */
	protected function upload_form() {
		$bytes      = apply_filters( 'import_upload_size_limit', wp_max_upload_size() );
		$size       = size_format( $bytes );
		$upload_dir = wp_upload_dir();

		include dirname( __FILE__ ) . '/views/html-product-csv-import-form.php';
	}

	/**
	 * Handle the upload form and store options.
	 */
	public function upload_form_handler() {
		check_admin_referer( 'woocommerce-csv-importer' );

		$file = $this->handle_upload();

		if ( is_wp_error( $file ) ) {
			$this->add_error( $file->get_error_message() );
			return;
		} else {
			$this->file = $file;
		}

		wp_redirect( esc_url_raw( $this->get_next_step_link() ) );
		exit;
	}

	/**
	 * Handles the CSV upload and initial parsing of the file to prepare for
	 * displaying author import options.
	 *
	 * @return string|WP_Error
	 */
	public function handle_upload() {
		// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce already verified in WC_Product_CSV_Importer_Controller::upload_form_handler()
		$file_url = isset( $_POST['file_url'] ) ? wc_clean( wp_unslash( $_POST['file_url'] ) ) : '';

		if ( empty( $file_url ) ) {
			if ( ! isset( $_FILES['import'] ) ) {
				return new WP_Error( 'woocommerce_product_csv_importer_upload_file_empty', __( 'File is empty. Please upload something more substantial. This error could also be caused by uploads being disabled in your php.ini or by post_max_size being defined as smaller than upload_max_filesize in php.ini.', 'woocommerce' ) );
			}

			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
			if ( ! self::is_file_valid_csv( wc_clean( wp_unslash( $_FILES['import']['name'] ) ), false ) ) {
				return new WP_Error( 'woocommerce_product_csv_importer_upload_file_invalid', __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) );
			}

			$overrides = array(
				'test_form' => false,
				'mimes'     => self::get_valid_csv_filetypes(),
			);
			$import    = $_FILES['import']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash
			$upload    = wp_handle_upload( $import, $overrides );

			if ( isset( $upload['error'] ) ) {
				return new WP_Error( 'woocommerce_product_csv_importer_upload_error', $upload['error'] );
			}

			// Construct the object array.
			$object = array(
				'post_title'     => basename( $upload['file'] ),
				'post_content'   => $upload['url'],
				'post_mime_type' => $upload['type'],
				'guid'           => $upload['url'],
				'context'        => 'import',
				'post_status'    => 'private',
			);

			// Save the data.
			$id = wp_insert_attachment( $object, $upload['file'] );

			/*
			 * Schedule a cleanup for one day from now in case of failed
			 * import or missing wp_import_cleanup() call.
			 */
			wp_schedule_single_event( time() + DAY_IN_SECONDS, 'importer_scheduled_cleanup', array( $id ) );

			return $upload['file'];
		} elseif ( file_exists( ABSPATH . $file_url ) ) {
			if ( ! self::is_file_valid_csv( ABSPATH . $file_url ) ) {
				return new WP_Error( 'woocommerce_product_csv_importer_upload_file_invalid', __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) );
			}

			return ABSPATH . $file_url;
		}
		// phpcs:enable

		return new WP_Error( 'woocommerce_product_csv_importer_upload_invalid_file', __( 'Please upload or provide the link to a valid CSV file.', 'woocommerce' ) );
	}

	/**
	 * Mapping step.
	 */
	protected function mapping_form() {
		check_admin_referer( 'woocommerce-csv-importer' );
		$args = array(
			'lines'     => 1,
			'delimiter' => $this->delimiter,
		);

		$importer     = self::get_importer( $this->file, $args );
		$headers      = $importer->get_raw_keys();
		$mapped_items = $this->auto_map_columns( $headers );
		$sample       = current( $importer->get_raw_data() );

		if ( empty( $sample ) ) {
			$this->add_error(
				__( 'The file is empty or using a different encoding than UTF-8, please try again with a new file.', 'woocommerce' ),
				array(
					array(
						'url'   => admin_url( 'edit.php?post_type=product&page=product_importer' ),
						'label' => __( 'Upload a new file', 'woocommerce' ),
					),
				)
			);

			// Force output the errors in the same page.
			$this->output_errors();
			return;
		}

		include_once dirname( __FILE__ ) . '/views/html-csv-import-mapping.php';
	}

	/**
	 * Import the file if it exists and is valid.
	 */
	public function import() {
		// Displaying this page triggers Ajax action to run the import with a valid nonce,
		// therefore this page needs to be nonce protected as well.
		check_admin_referer( 'woocommerce-csv-importer' );

		if ( ! self::is_file_valid_csv( $this->file ) ) {
			$this->add_error( __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) );
			$this->output_errors();
			return;
		}

		if ( ! is_file( $this->file ) ) {
			$this->add_error( __( 'The file does not exist, please try again.', 'woocommerce' ) );
			$this->output_errors();
			return;
		}

		if ( ! empty( $_POST['map_from'] ) && ! empty( $_POST['map_to'] ) ) {
			$mapping_from = wc_clean( wp_unslash( $_POST['map_from'] ) );
			$mapping_to   = wc_clean( wp_unslash( $_POST['map_to'] ) );

			// Save mapping preferences for future imports.
			update_user_option( get_current_user_id(), 'woocommerce_product_import_mapping', $mapping_to );
		} else {
			wp_redirect( esc_url_raw( $this->get_next_step_link( 'upload' ) ) );
			exit;
		}

		wp_localize_script(
			'wc-product-import',
			'wc_product_import_params',
			array(
				'import_nonce'    => wp_create_nonce( 'wc-product-import' ),
				'mapping'         => array(
					'from' => $mapping_from,
					'to'   => $mapping_to,
				),
				'file'            => $this->file,
				'update_existing' => $this->update_existing,
				'delimiter'       => $this->delimiter,
			)
		);
		wp_enqueue_script( 'wc-product-import' );

		include_once dirname( __FILE__ ) . '/views/html-csv-import-progress.php';
	}

	/**
	 * Done step.
	 */
	protected function done() {
		check_admin_referer( 'woocommerce-csv-importer' );
		$imported  = isset( $_GET['products-imported'] ) ? absint( $_GET['products-imported'] ) : 0;
		$updated   = isset( $_GET['products-updated'] ) ? absint( $_GET['products-updated'] ) : 0;
		$failed    = isset( $_GET['products-failed'] ) ? absint( $_GET['products-failed'] ) : 0;
		$skipped   = isset( $_GET['products-skipped'] ) ? absint( $_GET['products-skipped'] ) : 0;
		$file_name = isset( $_GET['file-name'] ) ? sanitize_text_field( wp_unslash( $_GET['file-name'] ) ) : '';
		$errors    = array_filter( (array) get_user_option( 'product_import_error_log' ) );

		include_once dirname( __FILE__ ) . '/views/html-csv-import-done.php';
	}

	/**
	 * Columns to normalize.
	 *
	 * @param  array $columns List of columns names and keys.
	 * @return array
	 */
	protected function normalize_columns_names( $columns ) {
		$normalized = array();

		foreach ( $columns as $key => $value ) {
			$normalized[ strtolower( $key ) ] = $value;
		}

		return $normalized;
	}

	/**
	 * Auto map column names.
	 *
	 * @param  array $raw_headers Raw header columns.
	 * @param  bool  $num_indexes If should use numbers or raw header columns as indexes.
	 * @return array
	 */
	protected function auto_map_columns( $raw_headers, $num_indexes = true ) {
		$weight_unit    = get_option( 'woocommerce_weight_unit' );
		$dimension_unit = get_option( 'woocommerce_dimension_unit' );

		/*
		 * @hooked wc_importer_generic_mappings - 10
		 * @hooked wc_importer_wordpress_mappings - 10
		 * @hooked wc_importer_default_english_mappings - 100
		 */
		$default_columns = $this->normalize_columns_names(
			apply_filters(
				'woocommerce_csv_product_import_mapping_default_columns',
				array(
					__( 'ID', 'woocommerce' )             => 'id',
					__( 'Type', 'woocommerce' )           => 'type',
					__( 'SKU', 'woocommerce' )            => 'sku',
					__( 'Name', 'woocommerce' )           => 'name',
					__( 'Published', 'woocommerce' )      => 'published',
					__( 'Is featured?', 'woocommerce' )   => 'featured',
					__( 'Visibility in catalog', 'woocommerce' ) => 'catalog_visibility',
					__( 'Short description', 'woocommerce' ) => 'short_description',
					__( 'Description', 'woocommerce' )    => 'description',
					__( 'Date sale price starts', 'woocommerce' ) => 'date_on_sale_from',
					__( 'Date sale price ends', 'woocommerce' ) => 'date_on_sale_to',
					__( 'Tax status', 'woocommerce' )     => 'tax_status',
					__( 'Tax class', 'woocommerce' )      => 'tax_class',
					__( 'In stock?', 'woocommerce' )      => 'stock_status',
					__( 'Stock', 'woocommerce' )          => 'stock_quantity',
					__( 'Backorders allowed?', 'woocommerce' ) => 'backorders',
					__( 'Low stock amount', 'woocommerce' ) => 'low_stock_amount',
					__( 'Sold individually?', 'woocommerce' ) => 'sold_individually',
					/* translators: %s: Weight unit */
					sprintf( __( 'Weight (%s)', 'woocommerce' ), $weight_unit ) => 'weight',
					/* translators: %s: Length unit */
					sprintf( __( 'Length (%s)', 'woocommerce' ), $dimension_unit ) => 'length',
					/* translators: %s: Width unit */
					sprintf( __( 'Width (%s)', 'woocommerce' ), $dimension_unit ) => 'width',
					/* translators: %s: Height unit */
					sprintf( __( 'Height (%s)', 'woocommerce' ), $dimension_unit ) => 'height',
					__( 'Allow customer reviews?', 'woocommerce' ) => 'reviews_allowed',
					__( 'Purchase note', 'woocommerce' )  => 'purchase_note',
					__( 'Sale price', 'woocommerce' )     => 'sale_price',
					__( 'Regular price', 'woocommerce' )  => 'regular_price',
					__( 'Categories', 'woocommerce' )     => 'category_ids',
					__( 'Tags', 'woocommerce' )           => 'tag_ids',
					__( 'Shipping class', 'woocommerce' ) => 'shipping_class_id',
					__( 'Images', 'woocommerce' )         => 'images',
					__( 'Download limit', 'woocommerce' ) => 'download_limit',
					__( 'Download expiry days', 'woocommerce' ) => 'download_expiry',
					__( 'Parent', 'woocommerce' )         => 'parent_id',
					__( 'Upsells', 'woocommerce' )        => 'upsell_ids',
					__( 'Cross-sells', 'woocommerce' )    => 'cross_sell_ids',
					__( 'Grouped products', 'woocommerce' ) => 'grouped_products',
					__( 'External URL', 'woocommerce' )   => 'product_url',
					__( 'Button text', 'woocommerce' )    => 'button_text',
					__( 'Position', 'woocommerce' )       => 'menu_order',
				),
				$raw_headers
			)
		);

		$special_columns = $this->get_special_columns(
			$this->normalize_columns_names(
				apply_filters(
					'woocommerce_csv_product_import_mapping_special_columns',
					array(
						/* translators: %d: Attribute number */
						__( 'Attribute %d name', 'woocommerce' ) => 'attributes:name',
						/* translators: %d: Attribute number */
						__( 'Attribute %d value(s)', 'woocommerce' ) => 'attributes:value',
						/* translators: %d: Attribute number */
						__( 'Attribute %d visible', 'woocommerce' ) => 'attributes:visible',
						/* translators: %d: Attribute number */
						__( 'Attribute %d global', 'woocommerce' ) => 'attributes:taxonomy',
						/* translators: %d: Attribute number */
						__( 'Attribute %d default', 'woocommerce' ) => 'attributes:default',
						/* translators: %d: Download number */
						__( 'Download %d ID', 'woocommerce' ) => 'downloads:id',
						/* translators: %d: Download number */
						__( 'Download %d name', 'woocommerce' ) => 'downloads:name',
						/* translators: %d: Download number */
						__( 'Download %d URL', 'woocommerce' ) => 'downloads:url',
						/* translators: %d: Meta number */
						__( 'Meta: %s', 'woocommerce' ) => 'meta:',
					),
					$raw_headers
				)
			)
		);

		$headers = array();
		foreach ( $raw_headers as $key => $field ) {
			$normalized_field  = strtolower( $field );
			$index             = $num_indexes ? $key : $field;
			$headers[ $index ] = $normalized_field;

			if ( isset( $default_columns[ $normalized_field ] ) ) {
				$headers[ $index ] = $default_columns[ $normalized_field ];
			} else {
				foreach ( $special_columns as $regex => $special_key ) {
					// Don't use the normalized field in the regex since meta might be case-sensitive.
					if ( preg_match( $regex, $field, $matches ) ) {
						$headers[ $index ] = $special_key . $matches[1];
						break;
					}
				}
			}
		}

		return apply_filters( 'woocommerce_csv_product_import_mapped_columns', $headers, $raw_headers );
	}

	/**
	 * Map columns using the user's lastest import mappings.
	 *
	 * @param  array $headers Header columns.
	 * @return array
	 */
	public function auto_map_user_preferences( $headers ) {
		$mapping_preferences = get_user_option( 'woocommerce_product_import_mapping' );

		if ( ! empty( $mapping_preferences ) && is_array( $mapping_preferences ) ) {
			return $mapping_preferences;
		}

		return $headers;
	}

	/**
	 * Sanitize special column name regex.
	 *
	 * @param  string $value Raw special column name.
	 * @return string
	 */
	protected function sanitize_special_column_name_regex( $value ) {
		return '/' . str_replace( array( '%d', '%s' ), '(.*)', trim( quotemeta( $value ) ) ) . '/i';
	}

	/**
	 * Get special columns.
	 *
	 * @param  array $columns Raw special columns.
	 * @return array
	 */
	protected function get_special_columns( $columns ) {
		$formatted = array();

		foreach ( $columns as $key => $value ) {
			$regex = $this->sanitize_special_column_name_regex( $key );

			$formatted[ $regex ] = $value;
		}

		return $formatted;
	}

	/**
	 * Get mapping options.
	 *
	 * @param  string $item Item name.
	 * @return array
	 */
	protected function get_mapping_options( $item = '' ) {
		// Get index for special column names.
		$index = $item;

		if ( preg_match( '/\d+/', $item, $matches ) ) {
			$index = $matches[0];
		}

		// Properly format for meta field.
		$meta = str_replace( 'meta:', '', $item );

		// Available options.
		$weight_unit    = get_option( 'woocommerce_weight_unit' );
		$dimension_unit = get_option( 'woocommerce_dimension_unit' );
		$options        = array(
			'id'                 => __( 'ID', 'woocommerce' ),
			'type'               => __( 'Type', 'woocommerce' ),
			'sku'                => __( 'SKU', 'woocommerce' ),
			'name'               => __( 'Name', 'woocommerce' ),
			'published'          => __( 'Published', 'woocommerce' ),
			'featured'           => __( 'Is featured?', 'woocommerce' ),
			'catalog_visibility' => __( 'Visibility in catalog', 'woocommerce' ),
			'short_description'  => __( 'Short description', 'woocommerce' ),
			'description'        => __( 'Description', 'woocommerce' ),
			'price'              => array(
				'name'    => __( 'Price', 'woocommerce' ),
				'options' => array(
					'regular_price'     => __( 'Regular price', 'woocommerce' ),
					'sale_price'        => __( 'Sale price', 'woocommerce' ),
					'date_on_sale_from' => __( 'Date sale price starts', 'woocommerce' ),
					'date_on_sale_to'   => __( 'Date sale price ends', 'woocommerce' ),
				),
			),
			'tax_status'         => __( 'Tax status', 'woocommerce' ),
			'tax_class'          => __( 'Tax class', 'woocommerce' ),
			'stock_status'       => __( 'In stock?', 'woocommerce' ),
			'stock_quantity'     => _x( 'Stock', 'Quantity in stock', 'woocommerce' ),
			'backorders'         => __( 'Backorders allowed?', 'woocommerce' ),
			'low_stock_amount'   => __( 'Low stock amount', 'woocommerce' ),
			'sold_individually'  => __( 'Sold individually?', 'woocommerce' ),
			/* translators: %s: weight unit */
			'weight'             => sprintf( __( 'Weight (%s)', 'woocommerce' ), $weight_unit ),
			'dimensions'         => array(
				'name'    => __( 'Dimensions', 'woocommerce' ),
				'options' => array(
					/* translators: %s: dimension unit */
					'length' => sprintf( __( 'Length (%s)', 'woocommerce' ), $dimension_unit ),
					/* translators: %s: dimension unit */
					'width'  => sprintf( __( 'Width (%s)', 'woocommerce' ), $dimension_unit ),
					/* translators: %s: dimension unit */
					'height' => sprintf( __( 'Height (%s)', 'woocommerce' ), $dimension_unit ),
				),
			),
			'category_ids'       => __( 'Categories', 'woocommerce' ),
			'tag_ids'            => __( 'Tags (comma separated)', 'woocommerce' ),
			'tag_ids_spaces'     => __( 'Tags (space separated)', 'woocommerce' ),
			'shipping_class_id'  => __( 'Shipping class', 'woocommerce' ),
			'images'             => __( 'Images', 'woocommerce' ),
			'parent_id'          => __( 'Parent', 'woocommerce' ),
			'upsell_ids'         => __( 'Upsells', 'woocommerce' ),
			'cross_sell_ids'     => __( 'Cross-sells', 'woocommerce' ),
			'grouped_products'   => __( 'Grouped products', 'woocommerce' ),
			'external'           => array(
				'name'    => __( 'External product', 'woocommerce' ),
				'options' => array(
					'product_url' => __( 'External URL', 'woocommerce' ),
					'button_text' => __( 'Button text', 'woocommerce' ),
				),
			),
			'downloads'          => array(
				'name'    => __( 'Downloads', 'woocommerce' ),
				'options' => array(
					'downloads:id' . $index   => __( 'Download ID', 'woocommerce' ),
					'downloads:name' . $index => __( 'Download name', 'woocommerce' ),
					'downloads:url' . $index  => __( 'Download URL', 'woocommerce' ),
					'download_limit'          => __( 'Download limit', 'woocommerce' ),
					'download_expiry'         => __( 'Download expiry days', 'woocommerce' ),
				),
			),
			'attributes'         => array(
				'name'    => __( 'Attributes', 'woocommerce' ),
				'options' => array(
					'attributes:name' . $index     => __( 'Attribute name', 'woocommerce' ),
					'attributes:value' . $index    => __( 'Attribute value(s)', 'woocommerce' ),
					'attributes:taxonomy' . $index => __( 'Is a global attribute?', 'woocommerce' ),
					'attributes:visible' . $index  => __( 'Attribute visibility', 'woocommerce' ),
					'attributes:default' . $index  => __( 'Default attribute', 'woocommerce' ),
				),
			),
			'reviews_allowed'    => __( 'Allow customer reviews?', 'woocommerce' ),
			'purchase_note'      => __( 'Purchase note', 'woocommerce' ),
			'meta:' . $meta      => __( 'Import as meta data', 'woocommerce' ),
			'menu_order'         => __( 'Position', 'woocommerce' ),
		);

		return apply_filters( 'woocommerce_csv_product_import_mapping_options', $options, $item );
	}
}
admin/importers/mappings/mappings.php000064400000000530151336044770014035 0ustar00<?php
/**
 * Load up extra automatic mappings for the CSV importer.
 *
 * @package WooCommerce\Admin\Importers
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

require dirname( __FILE__ ) . '/default.php';
require dirname( __FILE__ ) . '/generic.php';
require dirname( __FILE__ ) . '/shopify.php';
require dirname( __FILE__ ) . '/wordpress.php';
admin/importers/mappings/default.php000064400000010420151336044770013642 0ustar00<?php
/**
 * Default mappings
 *
 * @package WooCommerce\Admin\Importers
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Importer current locale.
 *
 * @since 3.1.0
 * @return string
 */
function wc_importer_current_locale() {
	$locale = get_locale();
	if ( function_exists( 'get_user_locale' ) ) {
		$locale = get_user_locale();
	}

	return $locale;
}

/**
 * Add English mapping placeholders when not using English as current language.
 *
 * @since 3.1.0
 * @param array $mappings Importer columns mappings.
 * @return array
 */
function wc_importer_default_english_mappings( $mappings ) {
	if ( 'en_US' === wc_importer_current_locale() ) {
		return $mappings;
	}

	$weight_unit    = get_option( 'woocommerce_weight_unit' );
	$dimension_unit = get_option( 'woocommerce_dimension_unit' );
	$new_mappings   = array(
		'ID'                                      => 'id',
		'Type'                                    => 'type',
		'SKU'                                     => 'sku',
		'Name'                                    => 'name',
		'Published'                               => 'published',
		'Is featured?'                            => 'featured',
		'Visibility in catalog'                   => 'catalog_visibility',
		'Short description'                       => 'short_description',
		'Description'                             => 'description',
		'Date sale price starts'                  => 'date_on_sale_from',
		'Date sale price ends'                    => 'date_on_sale_to',
		'Tax status'                              => 'tax_status',
		'Tax class'                               => 'tax_class',
		'In stock?'                               => 'stock_status',
		'Stock'                                   => 'stock_quantity',
		'Backorders allowed?'                     => 'backorders',
		'Low stock amount'                        => 'low_stock_amount',
		'Sold individually?'                      => 'sold_individually',
		sprintf( 'Weight (%s)', $weight_unit )    => 'weight',
		sprintf( 'Length (%s)', $dimension_unit ) => 'length',
		sprintf( 'Width (%s)', $dimension_unit )  => 'width',
		sprintf( 'Height (%s)', $dimension_unit ) => 'height',
		'Allow customer reviews?'                 => 'reviews_allowed',
		'Purchase note'                           => 'purchase_note',
		'Sale price'                              => 'sale_price',
		'Regular price'                           => 'regular_price',
		'Categories'                              => 'category_ids',
		'Tags'                                    => 'tag_ids',
		'Shipping class'                          => 'shipping_class_id',
		'Images'                                  => 'images',
		'Download limit'                          => 'download_limit',
		'Download expiry days'                    => 'download_expiry',
		'Parent'                                  => 'parent_id',
		'Upsells'                                 => 'upsell_ids',
		'Cross-sells'                             => 'cross_sell_ids',
		'Grouped products'                        => 'grouped_products',
		'External URL'                            => 'product_url',
		'Button text'                             => 'button_text',
		'Position'                                => 'menu_order',
	);

	return array_merge( $mappings, $new_mappings );
}
add_filter( 'woocommerce_csv_product_import_mapping_default_columns', 'wc_importer_default_english_mappings', 100 );

/**
 * Add English special mapping placeholders when not using English as current language.
 *
 * @since 3.1.0
 * @param array $mappings Importer columns mappings.
 * @return array
 */
function wc_importer_default_special_english_mappings( $mappings ) {
	if ( 'en_US' === wc_importer_current_locale() ) {
		return $mappings;
	}

	$new_mappings = array(
		'Attribute %d name'     => 'attributes:name',
		'Attribute %d value(s)' => 'attributes:value',
		'Attribute %d visible'  => 'attributes:visible',
		'Attribute %d global'   => 'attributes:taxonomy',
		'Attribute %d default'  => 'attributes:default',
		'Download %d ID'        => 'downloads:id',
		'Download %d name'      => 'downloads:name',
		'Download %d URL'       => 'downloads:url',
		'Meta: %s'              => 'meta:',
	);

	return array_merge( $mappings, $new_mappings );
}
add_filter( 'woocommerce_csv_product_import_mapping_special_columns', 'wc_importer_default_special_english_mappings', 100 );
admin/importers/mappings/generic.php000064400000001461151336044770013637 0ustar00<?php
/**
 * Generic mappings
 *
 * @package WooCommerce\Admin\Importers
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Add generic mappings.
 *
 * @since 3.1.0
 * @param array $mappings Importer columns mappings.
 * @return array
 */
function wc_importer_generic_mappings( $mappings ) {
	$generic_mappings = array(
		__( 'Title', 'woocommerce' )         => 'name',
		__( 'Product Title', 'woocommerce' ) => 'name',
		__( 'Price', 'woocommerce' )         => 'regular_price',
		__( 'Parent SKU', 'woocommerce' )    => 'parent_id',
		__( 'Quantity', 'woocommerce' )      => 'stock_quantity',
		__( 'Menu order', 'woocommerce' )    => 'menu_order',
	);

	return array_merge( $mappings, $generic_mappings );
}
add_filter( 'woocommerce_csv_product_import_mapping_default_columns', 'wc_importer_generic_mappings' );
admin/importers/mappings/shopify.php000064400000005526151336044770013712 0ustar00<?php
/**
 * Shopify mappings
 *
 * @package WooCommerce\Admin\Importers
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Add Shopify mappings.
 *
 * @since 3.7.0
 * @param array $mappings    Importer columns mappings.
 * @param array $raw_headers Raw headers from CSV being imported.
 * @return array
 */
function wc_importer_shopify_mappings( $mappings, $raw_headers ) {
	// Only map if this is looks like a Shopify export.
	if ( 0 !== count( array_diff( array( 'Title', 'Body (HTML)', 'Type', 'Variant SKU' ), $raw_headers ) ) ) {
		return $mappings;
	}
	$shopify_mappings = array(
		'Variant SKU'               => 'sku',
		'Title'                     => 'name',
		'Body (HTML)'               => 'description',
		'Quantity'                  => 'stock_quantity',
		'Variant Inventory Qty'     => 'stock_quantity',
		'Image Src'                 => 'images',
		'Variant Image'             => 'images',
		'Variant SKU'               => 'sku',
		'Variant Price'             => 'sale_price',
		'Variant Compare At Price'  => 'regular_price',
		'Type'                      => 'category_ids',
		'Tags'                      => 'tag_ids_spaces',
		'Variant Grams'             => 'weight',
		'Variant Requires Shipping' => 'meta:shopify_requires_shipping',
		'Variant Taxable'           => 'tax_status',
	);
	return array_merge( $mappings, $shopify_mappings );
}
add_filter( 'woocommerce_csv_product_import_mapping_default_columns', 'wc_importer_shopify_mappings', 10, 2 );

/**
 * Add special wildcard Shopify mappings.
 *
 * @since 3.7.0
 * @param array $mappings    Importer columns mappings.
 * @param array $raw_headers Raw headers from CSV being imported.
 * @return array
 */
function wc_importer_shopify_special_mappings( $mappings, $raw_headers ) {
	// Only map if this is looks like a Shopify export.
	if ( 0 !== count( array_diff( array( 'Title', 'Body (HTML)', 'Type', 'Variant SKU' ), $raw_headers ) ) ) {
		return $mappings;
	}
	$shopify_mappings = array(
		'Option%d Name'  => 'attributes:name',
		'Option%d Value' => 'attributes:value',
	);
	return array_merge( $mappings, $shopify_mappings );
}
add_filter( 'woocommerce_csv_product_import_mapping_special_columns', 'wc_importer_shopify_special_mappings', 10, 2 );

/**
 * Expand special Shopify columns to WC format.
 *
 * @since 3.7.0
 * @param  array $data Array of data.
 * @return array Expanded data.
 */
function wc_importer_shopify_expand_data( $data ) {
	if ( isset( $data['meta:shopify_requires_shipping'] ) ) {
		$requires_shipping = wc_string_to_bool( $data['meta:shopify_requires_shipping'] );

		if ( ! $requires_shipping ) {
			if ( isset( $data['type'] ) ) {
				$data['type'][] = 'virtual';
			} else {
				$data['type'] = array( 'virtual' );
			}
		}

		unset( $data['meta:shopify_requires_shipping'] );
	}
	return $data;
}
add_filter( 'woocommerce_product_importer_pre_expand_data', 'wc_importer_shopify_expand_data' );
admin/importers/mappings/wordpress.php000064400000001224151336044770014250 0ustar00<?php
/**
 * WordPress mappings
 *
 * @package WooCommerce\Admin\Importers
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Add mappings for WordPress tables.
 *
 * @since 3.1.0
 * @param array $mappings Importer columns mappings.
 * @return array
 */
function wc_importer_wordpress_mappings( $mappings ) {

	$wp_mappings = array(
		'post_id'      => 'id',
		'post_title'   => 'name',
		'post_content' => 'description',
		'post_excerpt' => 'short_description',
		'post_parent'  => 'parent_id',
	);

	return array_merge( $mappings, $wp_mappings );
}
add_filter( 'woocommerce_csv_product_import_mapping_default_columns', 'wc_importer_wordpress_mappings' );
admin/class-wc-admin-taxonomies.php000064400000042524151336044770013354 0ustar00<?php
/**
 * Handles taxonomies in admin
 *
 * @class    WC_Admin_Taxonomies
 * @version  2.3.10
 * @package  WooCommerce\Admin
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

use Automattic\WooCommerce\Internal\AssignDefaultCategory;

/**
 * WC_Admin_Taxonomies class.
 */
class WC_Admin_Taxonomies {

	/**
	 * Class instance.
	 *
	 * @var WC_Admin_Taxonomies instance
	 */
	protected static $instance = false;

	/**
	 * Default category ID.
	 *
	 * @var int
	 */
	private $default_cat_id = 0;

	/**
	 * Get class instance
	 */
	public static function get_instance() {
		if ( ! self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Constructor.
	 */
	public function __construct() {
		// Default category ID.
		$this->default_cat_id = get_option( 'default_product_cat', 0 );

		// Category/term ordering.
		add_action( 'create_term', array( $this, 'create_term' ), 5, 3 );
		add_action(
			'delete_product_cat',
			function() {
				wc_get_container()->get( AssignDefaultCategory::class )->schedule_action();
			}
		);

		// Add form.
		add_action( 'product_cat_add_form_fields', array( $this, 'add_category_fields' ) );
		add_action( 'product_cat_edit_form_fields', array( $this, 'edit_category_fields' ), 10 );
		add_action( 'created_term', array( $this, 'save_category_fields' ), 10, 3 );
		add_action( 'edit_term', array( $this, 'save_category_fields' ), 10, 3 );

		// Add columns.
		add_filter( 'manage_edit-product_cat_columns', array( $this, 'product_cat_columns' ) );
		add_filter( 'manage_product_cat_custom_column', array( $this, 'product_cat_column' ), 10, 3 );

		// Add row actions.
		add_filter( 'product_cat_row_actions', array( $this, 'product_cat_row_actions' ), 10, 2 );
		add_filter( 'admin_init', array( $this, 'handle_product_cat_row_actions' ) );

		// Taxonomy page descriptions.
		add_action( 'product_cat_pre_add_form', array( $this, 'product_cat_description' ) );
		add_action( 'after-product_cat-table', array( $this, 'product_cat_notes' ) );

		$attribute_taxonomies = wc_get_attribute_taxonomies();

		if ( ! empty( $attribute_taxonomies ) ) {
			foreach ( $attribute_taxonomies as $attribute ) {
				add_action( 'pa_' . $attribute->attribute_name . '_pre_add_form', array( $this, 'product_attribute_description' ) );
			}
		}

		// Maintain hierarchy of terms.
		add_filter( 'wp_terms_checklist_args', array( $this, 'disable_checked_ontop' ) );

		// Admin footer scripts for this product categories admin screen.
		add_action( 'admin_footer', array( $this, 'scripts_at_product_cat_screen_footer' ) );
	}

	/**
	 * Order term when created (put in position 0).
	 *
	 * @param mixed  $term_id Term ID.
	 * @param mixed  $tt_id Term taxonomy ID.
	 * @param string $taxonomy Taxonomy slug.
	 */
	public function create_term( $term_id, $tt_id = '', $taxonomy = '' ) {
		if ( 'product_cat' !== $taxonomy && ! taxonomy_is_product_attribute( $taxonomy ) ) {
			return;
		}

		$meta_name = taxonomy_is_product_attribute( $taxonomy ) ? 'order_' . esc_attr( $taxonomy ) : 'order';

		update_term_meta( $term_id, $meta_name, 0 );
	}

	/**
	 * When a term is deleted, delete its meta.
	 *
	 * @deprecated 3.6.0 No longer needed.
	 * @param mixed $term_id Term ID.
	 */
	public function delete_term( $term_id ) {
		wc_deprecated_function( 'delete_term', '3.6' );
	}

	/**
	 * Category thumbnail fields.
	 */
	public function add_category_fields() {
		?>
		<div class="form-field term-display-type-wrap">
			<label for="display_type"><?php esc_html_e( 'Display type', 'woocommerce' ); ?></label>
			<select id="display_type" name="display_type" class="postform">
				<option value=""><?php esc_html_e( 'Default', 'woocommerce' ); ?></option>
				<option value="products"><?php esc_html_e( 'Products', 'woocommerce' ); ?></option>
				<option value="subcategories"><?php esc_html_e( 'Subcategories', 'woocommerce' ); ?></option>
				<option value="both"><?php esc_html_e( 'Both', 'woocommerce' ); ?></option>
			</select>
		</div>
		<div class="form-field term-thumbnail-wrap">
			<label><?php esc_html_e( 'Thumbnail', 'woocommerce' ); ?></label>
			<div id="product_cat_thumbnail" style="float: left; margin-right: 10px;"><img src="<?php echo esc_url( wc_placeholder_img_src() ); ?>" width="60px" height="60px" /></div>
			<div style="line-height: 60px;">
				<input type="hidden" id="product_cat_thumbnail_id" name="product_cat_thumbnail_id" />
				<button type="button" class="upload_image_button button"><?php esc_html_e( 'Upload/Add image', 'woocommerce' ); ?></button>
				<button type="button" class="remove_image_button button"><?php esc_html_e( 'Remove image', 'woocommerce' ); ?></button>
			</div>
			<script type="text/javascript">

				// Only show the "remove image" button when needed
				if ( ! jQuery( '#product_cat_thumbnail_id' ).val() ) {
					jQuery( '.remove_image_button' ).hide();
				}

				// Uploading files
				var file_frame;

				jQuery( document ).on( 'click', '.upload_image_button', function( event ) {

					event.preventDefault();

					// If the media frame already exists, reopen it.
					if ( file_frame ) {
						file_frame.open();
						return;
					}

					// Create the media frame.
					file_frame = wp.media.frames.downloadable_file = wp.media({
						title: '<?php esc_html_e( 'Choose an image', 'woocommerce' ); ?>',
						button: {
							text: '<?php esc_html_e( 'Use image', 'woocommerce' ); ?>'
						},
						multiple: false
					});

					// When an image is selected, run a callback.
					file_frame.on( 'select', function() {
						var attachment           = file_frame.state().get( 'selection' ).first().toJSON();
						var attachment_thumbnail = attachment.sizes.thumbnail || attachment.sizes.full;

						jQuery( '#product_cat_thumbnail_id' ).val( attachment.id );
						jQuery( '#product_cat_thumbnail' ).find( 'img' ).attr( 'src', attachment_thumbnail.url );
						jQuery( '.remove_image_button' ).show();
					});

					// Finally, open the modal.
					file_frame.open();
				});

				jQuery( document ).on( 'click', '.remove_image_button', function() {
					jQuery( '#product_cat_thumbnail' ).find( 'img' ).attr( 'src', '<?php echo esc_js( wc_placeholder_img_src() ); ?>' );
					jQuery( '#product_cat_thumbnail_id' ).val( '' );
					jQuery( '.remove_image_button' ).hide();
					return false;
				});

				jQuery( document ).ajaxComplete( function( event, request, options ) {
					if ( request && 4 === request.readyState && 200 === request.status
						&& options.data && 0 <= options.data.indexOf( 'action=add-tag' ) ) {

						var res = wpAjax.parseAjaxResponse( request.responseXML, 'ajax-response' );
						if ( ! res || res.errors ) {
							return;
						}
						// Clear Thumbnail fields on submit
						jQuery( '#product_cat_thumbnail' ).find( 'img' ).attr( 'src', '<?php echo esc_js( wc_placeholder_img_src() ); ?>' );
						jQuery( '#product_cat_thumbnail_id' ).val( '' );
						jQuery( '.remove_image_button' ).hide();
						// Clear Display type field on submit
						jQuery( '#display_type' ).val( '' );
						return;
					}
				} );

			</script>
			<div class="clear"></div>
		</div>
		<?php
	}

	/**
	 * Edit category thumbnail field.
	 *
	 * @param mixed $term Term (category) being edited.
	 */
	public function edit_category_fields( $term ) {

		$display_type = get_term_meta( $term->term_id, 'display_type', true );
		$thumbnail_id = absint( get_term_meta( $term->term_id, 'thumbnail_id', true ) );

		if ( $thumbnail_id ) {
			$image = wp_get_attachment_thumb_url( $thumbnail_id );
		} else {
			$image = wc_placeholder_img_src();
		}
		?>
		<tr class="form-field term-display-type-wrap">
			<th scope="row" valign="top"><label><?php esc_html_e( 'Display type', 'woocommerce' ); ?></label></th>
			<td>
				<select id="display_type" name="display_type" class="postform">
					<option value="" <?php selected( '', $display_type ); ?>><?php esc_html_e( 'Default', 'woocommerce' ); ?></option>
					<option value="products" <?php selected( 'products', $display_type ); ?>><?php esc_html_e( 'Products', 'woocommerce' ); ?></option>
					<option value="subcategories" <?php selected( 'subcategories', $display_type ); ?>><?php esc_html_e( 'Subcategories', 'woocommerce' ); ?></option>
					<option value="both" <?php selected( 'both', $display_type ); ?>><?php esc_html_e( 'Both', 'woocommerce' ); ?></option>
				</select>
			</td>
		</tr>
		<tr class="form-field term-thumbnail-wrap">
			<th scope="row" valign="top"><label><?php esc_html_e( 'Thumbnail', 'woocommerce' ); ?></label></th>
			<td>
				<div id="product_cat_thumbnail" style="float: left; margin-right: 10px;"><img src="<?php echo esc_url( $image ); ?>" width="60px" height="60px" /></div>
				<div style="line-height: 60px;">
					<input type="hidden" id="product_cat_thumbnail_id" name="product_cat_thumbnail_id" value="<?php echo esc_attr( $thumbnail_id ); ?>" />
					<button type="button" class="upload_image_button button"><?php esc_html_e( 'Upload/Add image', 'woocommerce' ); ?></button>
					<button type="button" class="remove_image_button button"><?php esc_html_e( 'Remove image', 'woocommerce' ); ?></button>
				</div>
				<script type="text/javascript">

					// Only show the "remove image" button when needed
					if ( '0' === jQuery( '#product_cat_thumbnail_id' ).val() ) {
						jQuery( '.remove_image_button' ).hide();
					}

					// Uploading files
					var file_frame;

					jQuery( document ).on( 'click', '.upload_image_button', function( event ) {

						event.preventDefault();

						// If the media frame already exists, reopen it.
						if ( file_frame ) {
							file_frame.open();
							return;
						}

						// Create the media frame.
						file_frame = wp.media.frames.downloadable_file = wp.media({
							title: '<?php esc_html_e( 'Choose an image', 'woocommerce' ); ?>',
							button: {
								text: '<?php esc_html_e( 'Use image', 'woocommerce' ); ?>'
							},
							multiple: false
						});

						// When an image is selected, run a callback.
						file_frame.on( 'select', function() {
							var attachment           = file_frame.state().get( 'selection' ).first().toJSON();
							var attachment_thumbnail = attachment.sizes.thumbnail || attachment.sizes.full;

							jQuery( '#product_cat_thumbnail_id' ).val( attachment.id );
							jQuery( '#product_cat_thumbnail' ).find( 'img' ).attr( 'src', attachment_thumbnail.url );
							jQuery( '.remove_image_button' ).show();
						});

						// Finally, open the modal.
						file_frame.open();
					});

					jQuery( document ).on( 'click', '.remove_image_button', function() {
						jQuery( '#product_cat_thumbnail' ).find( 'img' ).attr( 'src', '<?php echo esc_js( wc_placeholder_img_src() ); ?>' );
						jQuery( '#product_cat_thumbnail_id' ).val( '' );
						jQuery( '.remove_image_button' ).hide();
						return false;
					});

				</script>
				<div class="clear"></div>
			</td>
		</tr>
		<?php
	}

	/**
	 * Save category fields
	 *
	 * @param mixed  $term_id Term ID being saved.
	 * @param mixed  $tt_id Term taxonomy ID.
	 * @param string $taxonomy Taxonomy slug.
	 */
	public function save_category_fields( $term_id, $tt_id = '', $taxonomy = '' ) {
		if ( isset( $_POST['display_type'] ) && 'product_cat' === $taxonomy ) { // WPCS: CSRF ok, input var ok.
			update_term_meta( $term_id, 'display_type', esc_attr( $_POST['display_type'] ) ); // WPCS: CSRF ok, sanitization ok, input var ok.
		}
		if ( isset( $_POST['product_cat_thumbnail_id'] ) && 'product_cat' === $taxonomy ) { // WPCS: CSRF ok, input var ok.
			update_term_meta( $term_id, 'thumbnail_id', absint( $_POST['product_cat_thumbnail_id'] ) ); // WPCS: CSRF ok, input var ok.
		}
	}

	/**
	 * Description for product_cat page to aid users.
	 */
	public function product_cat_description() {
		echo wp_kses(
			wpautop( __( 'Product categories for your store can be managed here. To change the order of categories on the front-end you can drag and drop to sort them. To see more categories listed click the "screen options" link at the top-right of this page.', 'woocommerce' ) ),
			array( 'p' => array() )
		);
	}

	/**
	 * Add some notes to describe the behavior of the default category.
	 */
	public function product_cat_notes() {
		$category_id   = get_option( 'default_product_cat', 0 );
		$category      = get_term( $category_id, 'product_cat' );
		$category_name = ( ! $category || is_wp_error( $category ) ) ? _x( 'Uncategorized', 'Default category slug', 'woocommerce' ) : $category->name;
		?>
		<div class="form-wrap edit-term-notes">
			<p>
				<strong><?php esc_html_e( 'Note:', 'woocommerce' ); ?></strong><br>
				<?php
					printf(
						/* translators: %s: default category */
						esc_html__( 'Deleting a category does not delete the products in that category. Instead, products that were only assigned to the deleted category are set to the category %s.', 'woocommerce' ),
						'<strong>' . esc_html( $category_name ) . '</strong>'
					);
				?>
			</p>
		</div>
		<?php
	}

	/**
	 * Description for shipping class page to aid users.
	 */
	public function product_attribute_description() {
		echo wp_kses(
			wpautop( __( 'Attribute terms can be assigned to products and variations.<br/><br/><b>Note</b>: Deleting a term will remove it from all products and variations to which it has been assigned. Recreating a term will not automatically assign it back to products.', 'woocommerce' ) ),
			array( 'p' => array() )
		);
	}

	/**
	 * Thumbnail column added to category admin.
	 *
	 * @param mixed $columns Columns array.
	 * @return array
	 */
	public function product_cat_columns( $columns ) {
		$new_columns = array();

		if ( isset( $columns['cb'] ) ) {
			$new_columns['cb'] = $columns['cb'];
			unset( $columns['cb'] );
		}

		$new_columns['thumb'] = __( 'Image', 'woocommerce' );

		$columns           = array_merge( $new_columns, $columns );
		$columns['handle'] = '';

		return $columns;
	}

	/**
	 * Adjust row actions.
	 *
	 * @param array  $actions Array of actions.
	 * @param object $term Term object.
	 * @return array
	 */
	public function product_cat_row_actions( $actions, $term ) {
		$default_category_id = absint( get_option( 'default_product_cat', 0 ) );

		if ( $default_category_id !== $term->term_id && current_user_can( 'edit_term', $term->term_id ) ) {
			$actions['make_default'] = sprintf(
				'<a href="%s" aria-label="%s">%s</a>',
				wp_nonce_url( 'edit-tags.php?action=make_default&amp;taxonomy=product_cat&amp;post_type=product&amp;tag_ID=' . absint( $term->term_id ), 'make_default_' . absint( $term->term_id ) ),
				/* translators: %s: taxonomy term name */
				esc_attr( sprintf( __( 'Make &#8220;%s&#8221; the default category', 'woocommerce' ), $term->name ) ),
				__( 'Make default', 'woocommerce' )
			);
		}

		return $actions;
	}

	/**
	 * Handle custom row actions.
	 */
	public function handle_product_cat_row_actions() {
		if ( isset( $_GET['action'], $_GET['tag_ID'], $_GET['_wpnonce'] ) && 'make_default' === $_GET['action'] ) { // WPCS: CSRF ok, input var ok.
			$make_default_id = absint( $_GET['tag_ID'] ); // WPCS: Input var ok.

			if ( wp_verify_nonce( $_GET['_wpnonce'], 'make_default_' . $make_default_id ) && current_user_can( 'edit_term', $make_default_id ) ) { // WPCS: Sanitization ok, input var ok, CSRF ok.
				update_option( 'default_product_cat', $make_default_id );
			}
		}
	}

	/**
	 * Thumbnail column value added to category admin.
	 *
	 * @param string $columns Column HTML output.
	 * @param string $column Column name.
	 * @param int    $id Product ID.
	 *
	 * @return string
	 */
	public function product_cat_column( $columns, $column, $id ) {
		if ( 'thumb' === $column ) {
			// Prepend tooltip for default category.
			$default_category_id = absint( get_option( 'default_product_cat', 0 ) );

			if ( $default_category_id === $id ) {
				$columns .= wc_help_tip( __( 'This is the default category and it cannot be deleted. It will be automatically assigned to products with no category.', 'woocommerce' ) );
			}

			$thumbnail_id = get_term_meta( $id, 'thumbnail_id', true );

			if ( $thumbnail_id ) {
				$image = wp_get_attachment_thumb_url( $thumbnail_id );
			} else {
				$image = wc_placeholder_img_src();
			}

			// Prevent esc_url from breaking spaces in urls for image embeds. Ref: https://core.trac.wordpress.org/ticket/23605 .
			$image    = str_replace( ' ', '%20', $image );
			$columns .= '<img src="' . esc_url( $image ) . '" alt="' . esc_attr__( 'Thumbnail', 'woocommerce' ) . '" class="wp-post-image" height="48" width="48" />';
		}
		if ( 'handle' === $column ) {
			$columns .= '<input type="hidden" name="term_id" value="' . esc_attr( $id ) . '" />';
		}
		return $columns;
	}

	/**
	 * Maintain term hierarchy when editing a product.
	 *
	 * @param  array $args Term checklist args.
	 * @return array
	 */
	public function disable_checked_ontop( $args ) {
		if ( ! empty( $args['taxonomy'] ) && 'product_cat' === $args['taxonomy'] ) {
			$args['checked_ontop'] = false;
		}
		return $args;
	}

	/**
	 * Admin footer scripts for the product categories admin screen
	 *
	 * @return void
	 */
	public function scripts_at_product_cat_screen_footer() {
		if ( ! isset( $_GET['taxonomy'] ) || 'product_cat' !== $_GET['taxonomy'] ) { // WPCS: CSRF ok, input var ok.
			return;
		}
		// Ensure the tooltip is displayed when the image column is disabled on product categories.
		wc_enqueue_js(
			"(function( $ ) {
				'use strict';
				var product_cat = $( 'tr#tag-" . absint( $this->default_cat_id ) . "' );
				product_cat.find( 'th' ).empty();
				product_cat.find( 'td.thumb span' ).detach( 'span' ).appendTo( product_cat.find( 'th' ) );
			})( jQuery );"
		);
	}
}

$wc_admin_taxonomies = WC_Admin_Taxonomies::get_instance();
admin/class-wc-admin-notices.php000064400000045670151336044770012637 0ustar00<?php
/**
 * Display notices in admin
 *
 * @package WooCommerce\Admin
 * @version 3.4.0
 */

use Automattic\Jetpack\Constants;

defined( 'ABSPATH' ) || exit;

/**
 * WC_Admin_Notices Class.
 */
class WC_Admin_Notices {

	/**
	 * Stores notices.
	 *
	 * @var array
	 */
	private static $notices = array();

	/**
	 * Array of notices - name => callback.
	 *
	 * @var array
	 */
	private static $core_notices = array(
		'update'                           => 'update_notice',
		'template_files'                   => 'template_file_check_notice',
		'legacy_shipping'                  => 'legacy_shipping_notice',
		'no_shipping_methods'              => 'no_shipping_methods_notice',
		'regenerating_thumbnails'          => 'regenerating_thumbnails_notice',
		'regenerating_lookup_table'        => 'regenerating_lookup_table_notice',
		'no_secure_connection'             => 'secure_connection_notice',
		WC_PHP_MIN_REQUIREMENTS_NOTICE     => 'wp_php_min_requirements_notice',
		'maxmind_license_key'              => 'maxmind_missing_license_key_notice',
		'redirect_download_method'         => 'redirect_download_method_notice',
		'uploads_directory_is_unprotected' => 'uploads_directory_is_unprotected_notice',
		'base_tables_missing'              => 'base_tables_missing_notice',
	);

	/**
	 * Constructor.
	 */
	public static function init() {
		self::$notices = get_option( 'woocommerce_admin_notices', array() );

		add_action( 'switch_theme', array( __CLASS__, 'reset_admin_notices' ) );
		add_action( 'woocommerce_installed', array( __CLASS__, 'reset_admin_notices' ) );
		add_action( 'wp_loaded', array( __CLASS__, 'add_redirect_download_method_notice' ) );
		add_action( 'wp_loaded', array( __CLASS__, 'hide_notices' ) );
		// @TODO: This prevents Action Scheduler async jobs from storing empty list of notices during WC installation.
		// That could lead to OBW not starting and 'Run setup wizard' notice not appearing in WP admin, which we want
		// to avoid.
		if ( ! WC_Install::is_new_install() || ! wc_is_running_from_async_action_scheduler() ) {
			add_action( 'shutdown', array( __CLASS__, 'store_notices' ) );
		}

		if ( current_user_can( 'manage_woocommerce' ) ) {
			add_action( 'admin_print_styles', array( __CLASS__, 'add_notices' ) );
		}
	}

	/**
	 * Parses query to create nonces when available.
	 *
	 * @deprecated 5.4.0
	 * @param object $response The WP_REST_Response we're working with.
	 * @return object $response The prepared WP_REST_Response object.
	 */
	public static function prepare_note_with_nonce( $response ) {
		wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '5.4.0' );

		return $response;
	}

	/**
	 * Store notices to DB
	 */
	public static function store_notices() {
		update_option( 'woocommerce_admin_notices', self::get_notices() );
	}

	/**
	 * Get notices
	 *
	 * @return array
	 */
	public static function get_notices() {
		return self::$notices;
	}

	/**
	 * Remove all notices.
	 */
	public static function remove_all_notices() {
		self::$notices = array();
	}

	/**
	 * Reset notices for themes when switched or a new version of WC is installed.
	 */
	public static function reset_admin_notices() {
		if ( ! self::is_ssl() ) {
			self::add_notice( 'no_secure_connection' );
		}
		if ( ! self::is_uploads_directory_protected() ) {
			self::add_notice( 'uploads_directory_is_unprotected' );
		}
		self::add_notice( 'template_files' );
		self::add_min_version_notice();
		self::add_maxmind_missing_license_key_notice();
	}

	/**
	 * Show a notice.
	 *
	 * @param string $name Notice name.
	 * @param bool   $force_save Force saving inside this method instead of at the 'shutdown'.
	 */
	public static function add_notice( $name, $force_save = false ) {
		self::$notices = array_unique( array_merge( self::get_notices(), array( $name ) ) );

		if ( $force_save ) {
			// Adding early save to prevent more race conditions with notices.
			self::store_notices();
		}
	}

	/**
	 * Remove a notice from being displayed.
	 *
	 * @param string $name Notice name.
	 * @param bool   $force_save Force saving inside this method instead of at the 'shutdown'.
	 */
	public static function remove_notice( $name, $force_save = false ) {
		self::$notices = array_diff( self::get_notices(), array( $name ) );
		delete_option( 'woocommerce_admin_notice_' . $name );

		if ( $force_save ) {
			// Adding early save to prevent more race conditions with notices.
			self::store_notices();
		}
	}

	/**
	 * See if a notice is being shown.
	 *
	 * @param string $name Notice name.
	 *
	 * @return boolean
	 */
	public static function has_notice( $name ) {
		return in_array( $name, self::get_notices(), true );
	}

	/**
	 * Hide a notice if the GET variable is set.
	 */
	public static function hide_notices() {
		if ( isset( $_GET['wc-hide-notice'] ) && isset( $_GET['_wc_notice_nonce'] ) ) { // WPCS: input var ok, CSRF ok.
			if ( ! wp_verify_nonce( sanitize_key( wp_unslash( $_GET['_wc_notice_nonce'] ) ), 'woocommerce_hide_notices_nonce' ) ) { // WPCS: input var ok, CSRF ok.
				wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) );
			}

			if ( ! current_user_can( 'manage_woocommerce' ) ) {
				wp_die( esc_html__( 'You don&#8217;t have permission to do this.', 'woocommerce' ) );
			}

			$hide_notice = sanitize_text_field( wp_unslash( $_GET['wc-hide-notice'] ) ); // WPCS: input var ok, CSRF ok.

			self::remove_notice( $hide_notice );

			update_user_meta( get_current_user_id(), 'dismissed_' . $hide_notice . '_notice', true );

			do_action( 'woocommerce_hide_' . $hide_notice . '_notice' );
		}
	}

	/**
	 * Add notices + styles if needed.
	 */
	public static function add_notices() {
		$notices = self::get_notices();

		if ( empty( $notices ) ) {
			return;
		}

		$screen          = get_current_screen();
		$screen_id       = $screen ? $screen->id : '';
		$show_on_screens = array(
			'dashboard',
			'plugins',
		);

		// Notices should only show on WooCommerce screens, the main dashboard, and on the plugins screen.
		if ( ! in_array( $screen_id, wc_get_screen_ids(), true ) && ! in_array( $screen_id, $show_on_screens, true ) ) {
			return;
		}

		wp_enqueue_style( 'woocommerce-activation', plugins_url( '/assets/css/activation.css', WC_PLUGIN_FILE ), array(), Constants::get_constant( 'WC_VERSION' ) );

		// Add RTL support.
		wp_style_add_data( 'woocommerce-activation', 'rtl', 'replace' );

		foreach ( $notices as $notice ) {
			if ( ! empty( self::$core_notices[ $notice ] ) && apply_filters( 'woocommerce_show_admin_notice', true, $notice ) ) {
				add_action( 'admin_notices', array( __CLASS__, self::$core_notices[ $notice ] ) );
			} else {
				add_action( 'admin_notices', array( __CLASS__, 'output_custom_notices' ) );
			}
		}
	}

	/**
	 * Add a custom notice.
	 *
	 * @param string $name        Notice name.
	 * @param string $notice_html Notice HTML.
	 */
	public static function add_custom_notice( $name, $notice_html ) {
		self::add_notice( $name );
		update_option( 'woocommerce_admin_notice_' . $name, wp_kses_post( $notice_html ) );
	}

	/**
	 * Output any stored custom notices.
	 */
	public static function output_custom_notices() {
		$notices = self::get_notices();

		if ( ! empty( $notices ) ) {
			foreach ( $notices as $notice ) {
				if ( empty( self::$core_notices[ $notice ] ) ) {
					$notice_html = get_option( 'woocommerce_admin_notice_' . $notice );

					if ( $notice_html ) {
						include dirname( __FILE__ ) . '/views/html-notice-custom.php';
					}
				}
			}
		}
	}

	/**
	 * If we need to update the database, include a message with the DB update button.
	 */
	public static function update_notice() {
		$screen    = get_current_screen();
		$screen_id = $screen ? $screen->id : '';
		if ( WC()->is_wc_admin_active() && in_array( $screen_id, wc_get_screen_ids(), true ) ) {
			return;
		}

		if ( WC_Install::needs_db_update() ) {
			$next_scheduled_date = WC()->queue()->get_next( 'woocommerce_run_update_callback', null, 'woocommerce-db-updates' );

			if ( $next_scheduled_date || ! empty( $_GET['do_update_woocommerce'] ) ) { // WPCS: input var ok, CSRF ok.
				include dirname( __FILE__ ) . '/views/html-notice-updating.php';
			} else {
				include dirname( __FILE__ ) . '/views/html-notice-update.php';
			}
		} else {
			include dirname( __FILE__ ) . '/views/html-notice-updated.php';
		}
	}

	/**
	 * If we have just installed, show a message with the install pages button.
	 *
	 * @deprecated 4.6.0
	 */
	public static function install_notice() {
		_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
	}

	/**
	 * Show a notice highlighting bad template files.
	 */
	public static function template_file_check_notice() {
		$core_templates = WC_Admin_Status::scan_template_files( WC()->plugin_path() . '/templates' );
		$outdated       = false;

		foreach ( $core_templates as $file ) {

			$theme_file = false;
			if ( file_exists( get_stylesheet_directory() . '/' . $file ) ) {
				$theme_file = get_stylesheet_directory() . '/' . $file;
			} elseif ( file_exists( get_stylesheet_directory() . '/' . WC()->template_path() . $file ) ) {
				$theme_file = get_stylesheet_directory() . '/' . WC()->template_path() . $file;
			} elseif ( file_exists( get_template_directory() . '/' . $file ) ) {
				$theme_file = get_template_directory() . '/' . $file;
			} elseif ( file_exists( get_template_directory() . '/' . WC()->template_path() . $file ) ) {
				$theme_file = get_template_directory() . '/' . WC()->template_path() . $file;
			}

			if ( false !== $theme_file ) {
				$core_version  = WC_Admin_Status::get_file_version( WC()->plugin_path() . '/templates/' . $file );
				$theme_version = WC_Admin_Status::get_file_version( $theme_file );

				if ( $core_version && $theme_version && version_compare( $theme_version, $core_version, '<' ) ) {
					$outdated = true;
					break;
				}
			}
		}

		if ( $outdated ) {
			include dirname( __FILE__ ) . '/views/html-notice-template-check.php';
		} else {
			self::remove_notice( 'template_files' );
		}
	}

	/**
	 * Show a notice asking users to convert to shipping zones.
	 *
	 * @todo remove in 4.0.0
	 */
	public static function legacy_shipping_notice() {
		$maybe_load_legacy_methods = array( 'flat_rate', 'free_shipping', 'international_delivery', 'local_delivery', 'local_pickup' );
		$enabled                   = false;

		foreach ( $maybe_load_legacy_methods as $method ) {
			$options = get_option( 'woocommerce_' . $method . '_settings' );
			if ( $options && isset( $options['enabled'] ) && 'yes' === $options['enabled'] ) {
				$enabled = true;
			}
		}

		if ( $enabled ) {
			include dirname( __FILE__ ) . '/views/html-notice-legacy-shipping.php';
		} else {
			self::remove_notice( 'template_files' );
		}
	}

	/**
	 * No shipping methods.
	 */
	public static function no_shipping_methods_notice() {
		if ( wc_shipping_enabled() && ( empty( $_GET['page'] ) || empty( $_GET['tab'] ) || 'wc-settings' !== $_GET['page'] || 'shipping' !== $_GET['tab'] ) ) { // WPCS: input var ok, CSRF ok.
			$product_count = wp_count_posts( 'product' );
			$method_count  = wc_get_shipping_method_count();

			if ( $product_count->publish > 0 && 0 === $method_count ) {
				include dirname( __FILE__ ) . '/views/html-notice-no-shipping-methods.php';
			}

			if ( $method_count > 0 ) {
				self::remove_notice( 'no_shipping_methods' );
			}
		}
	}

	/**
	 * Notice shown when regenerating thumbnails background process is running.
	 */
	public static function regenerating_thumbnails_notice() {
		include dirname( __FILE__ ) . '/views/html-notice-regenerating-thumbnails.php';
	}

	/**
	 * Notice about secure connection.
	 */
	public static function secure_connection_notice() {
		if ( self::is_ssl() || get_user_meta( get_current_user_id(), 'dismissed_no_secure_connection_notice', true ) ) {
			return;
		}

		include dirname( __FILE__ ) . '/views/html-notice-secure-connection.php';
	}

	/**
	 * Notice shown when regenerating thumbnails background process is running.
	 *
	 * @since 3.6.0
	 */
	public static function regenerating_lookup_table_notice() {
		// See if this is still relevent.
		if ( ! wc_update_product_lookup_tables_is_running() ) {
			self::remove_notice( 'regenerating_lookup_table' );
			return;
		}

		include dirname( __FILE__ ) . '/views/html-notice-regenerating-lookup-table.php';
	}

	/**
	 * Add notice about minimum PHP and WordPress requirement.
	 *
	 * @since 3.6.5
	 */
	public static function add_min_version_notice() {
		if ( version_compare( phpversion(), WC_NOTICE_MIN_PHP_VERSION, '<' ) || version_compare( get_bloginfo( 'version' ), WC_NOTICE_MIN_WP_VERSION, '<' ) ) {
			self::add_notice( WC_PHP_MIN_REQUIREMENTS_NOTICE );
		}
	}

	/**
	 * Notice about WordPress and PHP minimum requirements.
	 *
	 * @since 3.6.5
	 * @return void
	 */
	public static function wp_php_min_requirements_notice() {
		if ( apply_filters( 'woocommerce_hide_php_wp_nag', get_user_meta( get_current_user_id(), 'dismissed_' . WC_PHP_MIN_REQUIREMENTS_NOTICE . '_notice', true ) ) ) {
			self::remove_notice( WC_PHP_MIN_REQUIREMENTS_NOTICE );
			return;
		}

		$old_php = version_compare( phpversion(), WC_NOTICE_MIN_PHP_VERSION, '<' );
		$old_wp  = version_compare( get_bloginfo( 'version' ), WC_NOTICE_MIN_WP_VERSION, '<' );

		// Both PHP and WordPress up to date version => no notice.
		if ( ! $old_php && ! $old_wp ) {
			return;
		}

		if ( $old_php && $old_wp ) {
			$msg = sprintf(
				/* translators: 1: Minimum PHP version 2: Minimum WordPress version */
				__( 'Update required: WooCommerce will soon require PHP version %1$s and WordPress version %2$s or newer.', 'woocommerce' ),
				WC_NOTICE_MIN_PHP_VERSION,
				WC_NOTICE_MIN_WP_VERSION
			);
		} elseif ( $old_php ) {
			$msg = sprintf(
				/* translators: %s: Minimum PHP version */
				__( 'Update required: WooCommerce will soon require PHP version %s or newer.', 'woocommerce' ),
				WC_NOTICE_MIN_PHP_VERSION
			);
		} elseif ( $old_wp ) {
			$msg = sprintf(
				/* translators: %s: Minimum WordPress version */
				__( 'Update required: WooCommerce will soon require WordPress version %s or newer.', 'woocommerce' ),
				WC_NOTICE_MIN_WP_VERSION
			);
		}

		include dirname( __FILE__ ) . '/views/html-notice-wp-php-minimum-requirements.php';
	}

	/**
	 * Add MaxMind missing license key notice.
	 *
	 * @since 3.9.0
	 */
	public static function add_maxmind_missing_license_key_notice() {
		$default_address = get_option( 'woocommerce_default_customer_address' );

		if ( ! in_array( $default_address, array( 'geolocation', 'geolocation_ajax' ), true ) ) {
			return;
		}

		$integration_options = get_option( 'woocommerce_maxmind_geolocation_settings' );
		if ( empty( $integration_options['license_key'] ) ) {
			self::add_notice( 'maxmind_license_key' );

		}
	}

	/**
	 *  Add notice about Redirect-only download method, nudging user to switch to a different method instead.
	 */
	public static function add_redirect_download_method_notice() {
		if ( 'redirect' === get_option( 'woocommerce_file_download_method' ) ) {
			self::add_notice( 'redirect_download_method' );
		} else {
			self::remove_notice( 'redirect_download_method' );
		}
	}

	/**
	 * Display MaxMind missing license key notice.
	 *
	 * @since 3.9.0
	 */
	public static function maxmind_missing_license_key_notice() {
		$user_dismissed_notice   = get_user_meta( get_current_user_id(), 'dismissed_maxmind_license_key_notice', true );
		$filter_dismissed_notice = ! apply_filters( 'woocommerce_maxmind_geolocation_display_notices', true );

		if ( $user_dismissed_notice || $filter_dismissed_notice ) {
			self::remove_notice( 'maxmind_license_key' );
			return;
		}

		include dirname( __FILE__ ) . '/views/html-notice-maxmind-license-key.php';
	}

	/**
	 * Notice about Redirect-Only download method.
	 *
	 * @since 4.0
	 */
	public static function redirect_download_method_notice() {
		if ( apply_filters( 'woocommerce_hide_redirect_method_nag', get_user_meta( get_current_user_id(), 'dismissed_redirect_download_method_notice', true ) ) ) {
			self::remove_notice( 'redirect_download_method' );
			return;
		}

		include dirname( __FILE__ ) . '/views/html-notice-redirect-only-download.php';
	}

	/**
	 * Notice about uploads directory begin unprotected.
	 *
	 * @since 4.2.0
	 */
	public static function uploads_directory_is_unprotected_notice() {
		if ( get_user_meta( get_current_user_id(), 'dismissed_uploads_directory_is_unprotected_notice', true ) || self::is_uploads_directory_protected() ) {
			self::remove_notice( 'uploads_directory_is_unprotected' );
			return;
		}

		include dirname( __FILE__ ) . '/views/html-notice-uploads-directory-is-unprotected.php';
	}

	/**
	 * Notice about base tables missing.
	 */
	public static function base_tables_missing_notice() {
		$notice_dismissed = apply_filters(
			'woocommerce_hide_base_tables_missing_nag',
			get_user_meta( get_current_user_id(), 'dismissed_base_tables_missing_notice', true )
		);
		if ( $notice_dismissed ) {
			self::remove_notice( 'base_tables_missing' );
		}

		include dirname( __FILE__ ) . '/views/html-notice-base-table-missing.php';
	}

	/**
	 * Determine if the store is running SSL.
	 *
	 * @return bool Flag SSL enabled.
	 * @since  3.5.1
	 */
	protected static function is_ssl() {
		$shop_page = wc_get_page_permalink( 'shop' );

		return ( is_ssl() && 'https' === substr( $shop_page, 0, 5 ) );
	}

	/**
	 * Wrapper for is_plugin_active.
	 *
	 * @param string $plugin Plugin to check.
	 * @return boolean
	 */
	protected static function is_plugin_active( $plugin ) {
		if ( ! function_exists( 'is_plugin_active' ) ) {
			include_once ABSPATH . 'wp-admin/includes/plugin.php';
		}
		return is_plugin_active( $plugin );
	}

	/**
	 * Simplify Commerce is no longer in core.
	 *
	 * @deprecated 3.6.0 No longer shown.
	 */
	public static function simplify_commerce_notice() {
		wc_deprecated_function( 'WC_Admin_Notices::simplify_commerce_notice', '3.6.0' );
	}

	/**
	 * Show the Theme Check notice.
	 *
	 * @deprecated 3.3.0 No longer shown.
	 */
	public static function theme_check_notice() {
		wc_deprecated_function( 'WC_Admin_Notices::theme_check_notice', '3.3.0' );
	}

	/**
	 * Check if uploads directory is protected.
	 *
	 * @since 4.2.0
	 * @return bool
	 */
	protected static function is_uploads_directory_protected() {
		$cache_key = '_woocommerce_upload_directory_status';
		$status    = get_transient( $cache_key );

		// Check for cache.
		if ( false !== $status ) {
			return 'protected' === $status;
		}

		// Get only data from the uploads directory.
		$uploads = wp_get_upload_dir();

		// Check for the "uploads/woocommerce_uploads" directory.
		$response         = wp_safe_remote_get(
			esc_url_raw( $uploads['baseurl'] . '/woocommerce_uploads/' ),
			array(
				'redirection' => 0,
			)
		);
		$response_code    = intval( wp_remote_retrieve_response_code( $response ) );
		$response_content = wp_remote_retrieve_body( $response );

		// Check if returns 200 with empty content in case can open an index.html file,
		// and check for non-200 codes in case the directory is protected.
		$is_protected = ( 200 === $response_code && empty( $response_content ) ) || ( 200 !== $response_code );
		set_transient( $cache_key, $is_protected ? 'protected' : 'unprotected', 1 * DAY_IN_SECONDS );

		return $is_protected;
	}
}

WC_Admin_Notices::init();
admin/class-wc-admin-dashboard.php000064400000051505151336044770013114 0ustar00<?php
/**
 * Admin Dashboard
 *
 * @package     WooCommerce\Admin
 * @version     2.1.0
 */

use Automattic\Jetpack\Constants;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

if ( ! class_exists( 'WC_Admin_Dashboard', false ) ) :

	/**
	 * WC_Admin_Dashboard Class.
	 */
	class WC_Admin_Dashboard {

		/**
		 * Hook in tabs.
		 */
		public function __construct() {
			// Only hook in admin parts if the user has admin access.
			if ( $this->should_display_widget() ) {
				// If on network admin, only load the widget that works in that context and skip the rest.
				if ( is_multisite() && is_network_admin() ) {
					add_action( 'wp_network_dashboard_setup', array( $this, 'register_network_order_widget' ) );
				} else {
					add_action( 'wp_dashboard_setup', array( $this, 'init' ) );
				}
			}
		}

		/**
		 * Init dashboard widgets.
		 */
		public function init() {
			// Reviews Widget.
			if ( current_user_can( 'publish_shop_orders' ) && post_type_supports( 'product', 'comments' ) ) {
				wp_add_dashboard_widget( 'woocommerce_dashboard_recent_reviews', __( 'WooCommerce Recent Reviews', 'woocommerce' ), array( $this, 'recent_reviews' ) );
			}
			wp_add_dashboard_widget( 'woocommerce_dashboard_status', __( 'WooCommerce Status', 'woocommerce' ), array( $this, 'status_widget' ) );

			// Network Order Widget.
			if ( is_multisite() && is_main_site() ) {
				$this->register_network_order_widget();
			}
		}

		/**
		 * Register the network order dashboard widget.
		 */
		public function register_network_order_widget() {
			wp_add_dashboard_widget( 'woocommerce_network_orders', __( 'WooCommerce Network Orders', 'woocommerce' ), array( $this, 'network_orders' ) );
		}

		/**
		 * Check to see if we should display the widget.
		 *
		 * @return bool
		 */
		private function should_display_widget() {
			if ( ! WC()->is_wc_admin_active() ) {
				return false;
			}

			$has_permission           = current_user_can( 'view_woocommerce_reports' ) || current_user_can( 'manage_woocommerce' ) || current_user_can( 'publish_shop_orders' );
			$task_completed_or_hidden = 'yes' === get_option( 'woocommerce_task_list_complete' ) || 'yes' === get_option( 'woocommerce_task_list_hidden' );
			return $task_completed_or_hidden && $has_permission;
		}

		/**
		 * Get top seller from DB.
		 *
		 * @return object
		 */
		private function get_top_seller() {
			global $wpdb;

			$query            = array();
			$query['fields']  = "SELECT SUM( order_item_meta.meta_value ) as qty, order_item_meta_2.meta_value as product_id
			FROM {$wpdb->posts} as posts";
			$query['join']    = "INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON posts.ID = order_id ";
			$query['join']   .= "INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS order_item_meta ON order_items.order_item_id = order_item_meta.order_item_id ";
			$query['join']   .= "INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS order_item_meta_2 ON order_items.order_item_id = order_item_meta_2.order_item_id ";
			$query['where']   = "WHERE posts.post_type IN ( '" . implode( "','", wc_get_order_types( 'order-count' ) ) . "' ) ";
			$query['where']  .= "AND posts.post_status IN ( 'wc-" . implode( "','wc-", apply_filters( 'woocommerce_reports_order_statuses', array( 'completed', 'processing', 'on-hold' ) ) ) . "' ) ";
			$query['where']  .= "AND order_item_meta.meta_key = '_qty' ";
			$query['where']  .= "AND order_item_meta_2.meta_key = '_product_id' ";
			$query['where']  .= "AND posts.post_date >= '" . gmdate( 'Y-m-01', current_time( 'timestamp' ) ) . "' ";
			$query['where']  .= "AND posts.post_date <= '" . gmdate( 'Y-m-d H:i:s', current_time( 'timestamp' ) ) . "' ";
			$query['groupby'] = 'GROUP BY product_id';
			$query['orderby'] = 'ORDER BY qty DESC';
			$query['limits']  = 'LIMIT 1';

			return $wpdb->get_row( implode( ' ', apply_filters( 'woocommerce_dashboard_status_widget_top_seller_query', $query ) ) ); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		}

		/**
		 * Get sales report data.
		 *
		 * @return object
		 */
		private function get_sales_report_data() {
			include_once dirname( __FILE__ ) . '/reports/class-wc-report-sales-by-date.php';

			$sales_by_date                 = new WC_Report_Sales_By_Date();
			$sales_by_date->start_date     = strtotime( gmdate( 'Y-m-01', current_time( 'timestamp' ) ) );
			$sales_by_date->end_date       = strtotime( gmdate( 'Y-m-d', current_time( 'timestamp' ) ) );
			$sales_by_date->chart_groupby  = 'day';
			$sales_by_date->group_by_query = 'YEAR(posts.post_date), MONTH(posts.post_date), DAY(posts.post_date)';

			return $sales_by_date->get_report_data();
		}

		/**
		 * Show status widget.
		 */
		public function status_widget() {
			$suffix  = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min';
			$version = Constants::get_constant( 'WC_VERSION' );

			wp_enqueue_script( 'wc-status-widget', WC()->plugin_url() . '/assets/js/admin/wc-status-widget' . $suffix . '.js', array( 'jquery' ), $version, true );

			include_once dirname( __FILE__ ) . '/reports/class-wc-admin-report.php';

			$is_wc_admin_disabled = apply_filters( 'woocommerce_admin_disabled', false );

			$reports = new WC_Admin_Report();

			$net_sales_link  = 'admin.php?page=wc-reports&tab=orders&range=month';
			$top_seller_link = 'admin.php?page=wc-reports&tab=orders&report=sales_by_product&range=month&product_ids=';
			$report_data     = $is_wc_admin_disabled ? $this->get_sales_report_data() : $this->get_wc_admin_performance_data();
			if ( ! $is_wc_admin_disabled ) {
				$net_sales_link  = 'admin.php?page=wc-admin&path=%2Fanalytics%2Frevenue&chart=net_revenue&orderby=net_revenue&period=month&compare=previous_period';
				$top_seller_link = 'admin.php?page=wc-admin&filter=single_product&path=%2Fanalytics%2Fproducts&products=';
			}

			echo '<ul class="wc_status_list">';

			if ( current_user_can( 'view_woocommerce_reports' ) ) {

				if ( $report_data ) {
					?>
				<li class="sales-this-month">
				<a href="<?php echo esc_url( admin_url( $net_sales_link ) ); ?>">
					<?php echo $this->sales_sparkline( $reports, $is_wc_admin_disabled, '' ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?>
					<?php
						printf(
							/* translators: %s: net sales */
							esc_html__( '%s net sales this month', 'woocommerce' ),
							'<strong>' . wc_price( $report_data->net_sales ) . '</strong>'
						); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped
					?>
					</a>
				</li>
					<?php
				}

				$top_seller = $this->get_top_seller();
				if ( $top_seller && $top_seller->qty ) {
					?>
				<li class="best-seller-this-month">
				<a href="<?php echo esc_url( admin_url( $top_seller_link . $top_seller->product_id ) ); ?>">
					<?php echo $this->sales_sparkline( $reports, $is_wc_admin_disabled, $top_seller->product_id, 'count' ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?>
					<?php
						printf(
							/* translators: 1: top seller product title 2: top seller quantity */
							esc_html__( '%1$s top seller this month (sold %2$d)', 'woocommerce' ),
							'<strong>' . get_the_title( $top_seller->product_id ) . '</strong>',
							$top_seller->qty
						); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped
					?>
					</a>
				</li>
					<?php
				}
			}

			$this->status_widget_order_rows();
			$this->status_widget_stock_rows( $is_wc_admin_disabled );

			do_action( 'woocommerce_after_dashboard_status_widget', $reports );
			echo '</ul>';
		}

		/**
		 * Show order data is status widget.
		 */
		private function status_widget_order_rows() {
			if ( ! current_user_can( 'edit_shop_orders' ) ) {
				return;
			}
			$on_hold_count    = 0;
			$processing_count = 0;

			foreach ( wc_get_order_types( 'order-count' ) as $type ) {
				$counts            = (array) wp_count_posts( $type );
				$on_hold_count    += isset( $counts['wc-on-hold'] ) ? $counts['wc-on-hold'] : 0;
				$processing_count += isset( $counts['wc-processing'] ) ? $counts['wc-processing'] : 0;
			}
			?>
			<li class="processing-orders">
			<a href="<?php echo esc_url( admin_url( 'edit.php?post_status=wc-processing&post_type=shop_order' ) ); ?>">
				<?php
					printf(
						/* translators: %s: order count */
						_n( '<strong>%s order</strong> awaiting processing', '<strong>%s orders</strong> awaiting processing', $processing_count, 'woocommerce' ),
						$processing_count
					); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped
				?>
				</a>
			</li>
			<li class="on-hold-orders">
				<a href="<?php echo esc_url( admin_url( 'edit.php?post_status=wc-on-hold&post_type=shop_order' ) ); ?>">
				<?php
					printf(
						/* translators: %s: order count */
						_n( '<strong>%s order</strong> on-hold', '<strong>%s orders</strong> on-hold', $on_hold_count, 'woocommerce' ),
						$on_hold_count
					); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped
				?>
				</a>
			</li>
			<?php
		}

		/**
		 * Show stock data is status widget.
		 *
		 * @param bool $is_wc_admin_disabled if woocommerce admin is disabled.
		 */
		private function status_widget_stock_rows( $is_wc_admin_disabled ) {
			global $wpdb;

			// Requires lookup table added in 3.6.
			if ( version_compare( get_option( 'woocommerce_db_version', null ), '3.6', '<' ) ) {
				return;
			}

			$stock   = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) );
			$nostock = absint( max( get_option( 'woocommerce_notify_no_stock_amount' ), 0 ) );

			$transient_name   = 'wc_low_stock_count';
			$lowinstock_count = get_transient( $transient_name );

			if ( false === $lowinstock_count ) {
				/**
				 * Status widget low in stock count pre query.
				 *
				 * @since 4.3.0
				 * @param null|string $low_in_stock_count Low in stock count, by default null.
				 * @param int         $stock              Low stock amount.
				 * @param int         $nostock            No stock amount
				 */
				$lowinstock_count = apply_filters( 'woocommerce_status_widget_low_in_stock_count_pre_query', null, $stock, $nostock );

				if ( is_null( $lowinstock_count ) ) {
					$lowinstock_count = $wpdb->get_var(
						$wpdb->prepare(
							"SELECT COUNT( product_id )
							FROM {$wpdb->wc_product_meta_lookup} AS lookup
							INNER JOIN {$wpdb->posts} as posts ON lookup.product_id = posts.ID
							WHERE stock_quantity <= %d
							AND stock_quantity > %d
							AND posts.post_status = 'publish'",
							$stock,
							$nostock
						)
					);
				}

				set_transient( $transient_name, (int) $lowinstock_count, DAY_IN_SECONDS * 30 );
			}

			$transient_name   = 'wc_outofstock_count';
			$outofstock_count = get_transient( $transient_name );
			$lowstock_link    = 'admin.php?page=wc-reports&tab=stock&report=low_in_stock';
			$outofstock_link  = 'admin.php?page=wc-reports&tab=stock&report=out_of_stock';

			if ( false === $is_wc_admin_disabled ) {
				$lowstock_link   = 'admin.php?page=wc-admin&type=lowstock&path=%2Fanalytics%2Fstock';
				$outofstock_link = 'admin.php?page=wc-admin&type=outofstock&path=%2Fanalytics%2Fstock';
			}

			if ( false === $outofstock_count ) {
				/**
				 * Status widget out of stock count pre query.
				 *
				 * @since 4.3.0
				 * @param null|string $outofstock_count Out of stock count, by default null.
				 * @param int         $nostock          No stock amount
				 */
				$outofstock_count = apply_filters( 'woocommerce_status_widget_out_of_stock_count_pre_query', null, $nostock );

				if ( is_null( $outofstock_count ) ) {
					$outofstock_count = (int) $wpdb->get_var(
						$wpdb->prepare(
							"SELECT COUNT( product_id )
							FROM {$wpdb->wc_product_meta_lookup} AS lookup
							INNER JOIN {$wpdb->posts} as posts ON lookup.product_id = posts.ID
							WHERE stock_quantity <= %d
							AND posts.post_status = 'publish'",
							$nostock
						)
					);
				}

				set_transient( $transient_name, (int) $outofstock_count, DAY_IN_SECONDS * 30 );
			}
			?>
			<li class="low-in-stock">
			<a href="<?php echo esc_url( admin_url( $lowstock_link ) ); ?>">
				<?php
					printf(
						/* translators: %s: order count */
						_n( '<strong>%s product</strong> low in stock', '<strong>%s products</strong> low in stock', $lowinstock_count, 'woocommerce' ),
						$lowinstock_count
					); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped
				?>
				</a>
			</li>
			<li class="out-of-stock">
				<a href="<?php echo esc_url( admin_url( $outofstock_link ) ); ?>">
				<?php
					printf(
						/* translators: %s: order count */
						_n( '<strong>%s product</strong> out of stock', '<strong>%s products</strong> out of stock', $outofstock_count, 'woocommerce' ),
						$outofstock_count
					); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped
				?>
				</a>
			</li>
			<?php
		}

		/**
		 * Recent reviews widget.
		 */
		public function recent_reviews() {
			global $wpdb;

			$query_from = apply_filters(
				'woocommerce_report_recent_reviews_query_from',
				"FROM {$wpdb->comments} comments
				LEFT JOIN {$wpdb->posts} posts ON (comments.comment_post_ID = posts.ID)
				WHERE comments.comment_approved = '1'
				AND comments.comment_type = 'review'
				AND posts.post_password = ''
				AND posts.post_type = 'product'
				AND comments.comment_parent = 0
				ORDER BY comments.comment_date_gmt DESC
				LIMIT 5"
			);

			$comments = $wpdb->get_results(
				"SELECT posts.ID, posts.post_title, comments.comment_author, comments.comment_author_email, comments.comment_ID, comments.comment_content {$query_from};" // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			);

			if ( $comments ) {
				echo '<ul>';
				foreach ( $comments as $comment ) {

					echo '<li>';

					echo get_avatar( $comment->comment_author_email, '32' );

					$rating = intval( get_comment_meta( $comment->comment_ID, 'rating', true ) );

					/* translators: %s: rating */
					echo '<div class="star-rating"><span style="width:' . esc_attr( $rating * 20 ) . '%">' . sprintf( esc_html__( '%s out of 5', 'woocommerce' ), esc_html( $rating ) ) . '</span></div>';

					/* translators: %s: review author */
					echo '<h4 class="meta"><a href="' . esc_url( get_permalink( $comment->ID ) ) . '#comment-' . esc_attr( absint( $comment->comment_ID ) ) . '">' . esc_html( apply_filters( 'woocommerce_admin_dashboard_recent_reviews', $comment->post_title, $comment ) ) . '</a> ' . sprintf( esc_html__( 'reviewed by %s', 'woocommerce' ), esc_html( $comment->comment_author ) ) . '</h4>';
					echo '<blockquote>' . wp_kses_data( $comment->comment_content ) . '</blockquote></li>';

				}
				echo '</ul>';
			} else {
				echo '<p>' . esc_html__( 'There are no product reviews yet.', 'woocommerce' ) . '</p>';
			}
		}

		/**
		 * Network orders widget.
		 */
		public function network_orders() {
			$suffix  = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min';
			$version = Constants::get_constant( 'WC_VERSION' );

			wp_enqueue_style( 'wc-network-orders', WC()->plugin_url() . '/assets/css/network-order-widget.css', array(), $version );

			wp_enqueue_script( 'wc-network-orders', WC()->plugin_url() . '/assets/js/admin/network-orders' . $suffix . '.js', array( 'jquery', 'underscore' ), $version, true );

			$user     = wp_get_current_user();
			$blogs    = get_blogs_of_user( $user->ID );
			$blog_ids = wp_list_pluck( $blogs, 'userblog_id' );

			wp_localize_script(
				'wc-network-orders',
				'woocommerce_network_orders',
				array(
					'nonce'          => wp_create_nonce( 'wp_rest' ),
					'sites'          => array_values( $blog_ids ),
					'order_endpoint' => get_rest_url( null, 'wc/v3/orders/network' ),
				)
			);
			?>
			<div class="post-type-shop_order">
			<div id="woocommerce-network-order-table-loading" class="woocommerce-network-order-table-loading is-active">
				<p>
					<span class="spinner is-active"></span> <?php esc_html_e( 'Loading network orders', 'woocommerce' ); ?>
				</p>

			</div>
			<table id="woocommerce-network-order-table" class="woocommerce-network-order-table">
				<thead>
					<tr>
						<td><?php esc_html_e( 'Order', 'woocommerce' ); ?></td>
						<td><?php esc_html_e( 'Status', 'woocommerce' ); ?></td>
						<td><?php esc_html_e( 'Total', 'woocommerce' ); ?></td>
					</tr>
				</thead>
				<tbody id="network-orders-tbody">

				</tbody>
			</table>
			<div id="woocommerce-network-orders-no-orders" class="woocommerce-network-orders-no-orders">
				<p>
					<?php esc_html_e( 'No orders found', 'woocommerce' ); ?>
				</p>
			</div>
			<?php // @codingStandardsIgnoreStart ?>
			<script type="text/template" id="network-orders-row-template">
				<tr>
					<td>
						<a href="<%- edit_url %>" class="order-view"><strong>#<%- number %> <%- customer %></strong></a>
						<br>
						<em>
							<%- blog.blogname %>
						</em>
					</td>
					<td>
						<mark class="order-status status-<%- status %>"><span><%- status_name %></span></mark>
					</td>
					<td>
						<%= formatted_total %>
					</td>
				</tr>
			</script>
			<?php // @codingStandardsIgnoreEnd ?>
		</div>
			<?php
		}

		/**
		 * Gets the sales performance data from the new WooAdmin store.
		 *
		 * @return stdClass|WP_Error|WP_REST_Response
		 */
		private function get_wc_admin_performance_data() {
			$request    = new \WP_REST_Request( 'GET', '/wc-analytics/reports/performance-indicators' );
			$start_date = gmdate( 'Y-m-01 00:00:00', current_time( 'timestamp' ) );
			$end_date   = gmdate( 'Y-m-d 23:59:59', current_time( 'timestamp' ) );
			$request->set_query_params(
				array(
					'before' => $end_date,
					'after'  => $start_date,
					'stats'  => 'revenue/total_sales,revenue/net_revenue,orders/orders_count,products/items_sold,variations/items_sold',
				)
			);
			$response = rest_do_request( $request );

			if ( is_wp_error( $response ) ) {
				return $response;
			}

			if ( 200 !== $response->get_status() ) {
				return new \WP_Error( 'woocommerce_analytics_performance_indicators_result_failed', __( 'Sorry, fetching performance indicators failed.', 'woocommerce' ) );
			}
			$report_keys      = array(
				'net_revenue' => 'net_sales',
			);
			$performance_data = new stdClass();
			foreach ( $response->get_data() as $indicator ) {
				if ( isset( $indicator['chart'] ) && isset( $indicator['value'] ) ) {
					$key                    = isset( $report_keys[ $indicator['chart'] ] ) ? $report_keys[ $indicator['chart'] ] : $indicator['chart'];
					$performance_data->$key = $indicator['value'];
				}
			}
			return $performance_data;
		}

		/**
		 * Overwrites the original sparkline to use the new reports data if WooAdmin is enabled.
		 * Prepares a sparkline to show sales in the last X days.
		 *
		 * @param  WC_Admin_Report $reports old class for getting reports.
		 * @param  bool            $is_wc_admin_disabled If WC Admin is disabled or not.
		 * @param  int             $id ID of the product to show. Blank to get all orders.
		 * @param  string          $type Type of sparkline to get. Ignored if ID is not set.
		 * @return string
		 */
		private function sales_sparkline( $reports, $is_wc_admin_disabled = false, $id = '', $type = 'sales' ) {
			$days = max( 7, gmdate( 'd', current_time( 'timestamp' ) ) );
			if ( $is_wc_admin_disabled ) {
				return $reports->sales_sparkline( $id, $days, $type );
			}
			$sales_endpoint = '/wc-analytics/reports/revenue/stats';
			$start_date     = gmdate( 'Y-m-d 00:00:00', current_time( 'timestamp' ) - ( ( $days - 1 ) * DAY_IN_SECONDS ) );
			$end_date       = gmdate( 'Y-m-d 23:59:59', current_time( 'timestamp' ) );
			$meta_key       = 'net_revenue';
			$params         = array(
				'order'    => 'asc',
				'interval' => 'day',
				'per_page' => 100,
				'before'   => $end_date,
				'after'    => $start_date,
			);
			if ( $id ) {
				$sales_endpoint     = '/wc-analytics/reports/products/stats';
				$meta_key           = ( 'sales' === $type ) ? 'net_revenue' : 'items_sold';
				$params['products'] = $id;
			}
			$request          = new \WP_REST_Request( 'GET', $sales_endpoint );
			$params['fields'] = array( $meta_key );
			$request->set_query_params( $params );

			$response = rest_do_request( $request );

			if ( is_wp_error( $response ) ) {
				return $response;
			}

			$resp_data = $response->get_data();
			$data      = $resp_data['intervals'];

			$sparkline_data = array();
			$total          = 0;
			foreach ( $data as $d ) {
				$total += $d['subtotals']->$meta_key;
				array_push( $sparkline_data, array( strval( strtotime( $d['interval'] ) * 1000 ), $d['subtotals']->$meta_key ) );
			}

			if ( 'sales' === $type ) {
				/* translators: 1: total income 2: days */
				$tooltip = sprintf( __( 'Sold %1$s worth in the last %2$d days', 'woocommerce' ), strip_tags( wc_price( $total ) ), $days );
			} else {
				/* translators: 1: total items sold 2: days */
				$tooltip = sprintf( _n( 'Sold %1$d item in the last %2$d days', 'Sold %1$d items in the last %2$d days', $total, 'woocommerce' ), $total, $days );
			}

			return '<span class="wc_sparkline ' . ( ( 'sales' === $type ) ? 'lines' : 'bars' ) . ' tips" data-color="#777" data-tip="' . esc_attr( $tooltip ) . '" data-barwidth="' . 60 * 60 * 16 * 1000 . '" data-sparkline="' . wc_esc_json( wp_json_encode( $sparkline_data ) ) . '"></span>';
		}
	}

endif;

return new WC_Admin_Dashboard();
admin/class-wc-admin-dashboard-setup.php000064400000013111151336044770014241 0ustar00<?php
/**
 * Admin Dashboard - Setup
 *
 * @package     WooCommerce\Admin
 * @version     2.1.0
 */

use Automattic\Jetpack\Constants;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

if ( ! class_exists( 'WC_Admin_Dashboard_Setup', false ) ) :

	/**
	 * WC_Admin_Dashboard_Setup Class.
	 */
	class WC_Admin_Dashboard_Setup {

		/**
		 * List of tasks.
		 *
		 * @var array
		 */
		private $tasks = array(
			'store_details'        => array(
				'completed'   => false,
				'button_link' => 'admin.php?page=wc-admin&path=%2Fsetup-wizard',
			),
			'products'             => array(
				'completed'   => false,
				'button_link' => 'admin.php?page=wc-admin&task=products',
			),
			'woocommerce-payments' => array(
				'completed'   => false,
				'button_link' => 'admin.php?page=wc-admin&path=%2Fpayments%2Fconnect',
			),
			'payments'             => array(
				'completed'   => false,
				'button_link' => 'admin.php?page=wc-admin&task=payments',
			),
			'tax'                  => array(
				'completed'   => false,
				'button_link' => 'admin.php?page=wc-admin&task=tax',
			),
			'shipping'             => array(
				'completed'   => false,
				'button_link' => 'admin.php?page=wc-admin&task=shipping',
			),
			'appearance'           => array(
				'completed'   => false,
				'button_link' => 'admin.php?page=wc-admin&task=appearance',
			),
		);

		/**
		 * # of completed tasks.
		 *
		 * @var int
		 */
		private $completed_tasks_count = 0;

		/**
		 * WC_Admin_Dashboard_Setup constructor.
		 */
		public function __construct() {
			if ( $this->should_display_widget() ) {
				$this->populate_general_tasks();
				$this->populate_payment_tasks();
				$this->completed_tasks_count = $this->get_completed_tasks_count();
				add_meta_box(
					'wc_admin_dashboard_setup',
					__( 'WooCommerce Setup', 'woocommerce' ),
					array( $this, 'render' ),
					'dashboard',
					'normal',
					'high'
				);
			}
		}

		/**
		 * Render meta box output.
		 */
		public function render() {
			$version = Constants::get_constant( 'WC_VERSION' );
			wp_enqueue_style( 'wc-dashboard-setup', WC()->plugin_url() . '/assets/css/dashboard-setup.css', array(), $version );

			$task = $this->get_next_task();
			if ( ! $task ) {
				return;
			}

			$button_link           = $task['button_link'];
			$completed_tasks_count = $this->completed_tasks_count;
			$tasks_count           = count( $this->tasks );

			// Given 'r' (circle element's r attr), dashoffset = ((100-$desired_percentage)/100) * PI * (r*2).
			$progress_percentage = ( $completed_tasks_count / $tasks_count ) * 100;
			$circle_r            = 6.5;
			$circle_dashoffset   = ( ( 100 - $progress_percentage ) / 100 ) * ( pi() * ( $circle_r * 2 ) );

			include __DIR__ . '/views/html-admin-dashboard-setup.php';
		}

		/**
		 * Populate tasks from the database.
		 */
		private function populate_general_tasks() {
			$tasks = get_option( 'woocommerce_task_list_tracked_completed_tasks', array() );
			foreach ( $tasks as $task ) {
				if ( isset( $this->tasks[ $task ] ) ) {
					$this->tasks[ $task ]['completed']   = true;
					$this->tasks[ $task ]['button_link'] = wc_admin_url( $this->tasks[ $task ]['button_link'] );
				}
			}
		}

		/**
		 * Getter for $tasks
		 *
		 * @return array
		 */
		public function get_tasks() {
			return $this->tasks;
		}

		/**
		 * Return # of completed tasks
		 */
		public function get_completed_tasks_count() {
			$completed_tasks = array_filter(
				$this->tasks,
				function( $task ) {
					return $task['completed'];
				}
			);

			return count( $completed_tasks );
		}

		/**
		 * Get the next task.
		 *
		 * @return array|null
		 */
		private function get_next_task() {
			foreach ( $this->get_tasks() as $task ) {
				if ( false === $task['completed'] ) {
					return $task;
				}
			}

			return null;
		}

		/**
		 * Check to see if we should display the widget
		 *
		 * @return bool
		 */
		private function should_display_widget() {
			return WC()->is_wc_admin_active() &&
				'yes' !== get_option( 'woocommerce_task_list_complete' ) &&
				'yes' !== get_option( 'woocommerce_task_list_hidden' );
		}

		/**
		 * Populate payment tasks's visibility and completion
		 */
		private function populate_payment_tasks() {
			$is_woo_payment_installed = is_plugin_active( 'woocommerce-payments/woocommerce-payments.php' );
			$country                  = explode( ':', get_option( 'woocommerce_default_country', 'US:CA' ) )[0];

			// woocommerce-payments requires its plugin activated and country must be US.
			if ( ! $is_woo_payment_installed || 'US' !== $country ) {
				unset( $this->tasks['woocommerce-payments'] );
			}

			// payments can't be used when woocommerce-payments exists and country is US.
			if ( $is_woo_payment_installed && 'US' === $country ) {
				unset( $this->tasks['payments'] );
			}

			if ( isset( $this->tasks['payments'] ) ) {
				$gateways                             = WC()->payment_gateways->get_available_payment_gateways();
				$enabled_gateways                     = array_filter(
					$gateways,
					function ( $gateway ) {
						return 'yes' === $gateway->enabled;
					}
				);
				$this->tasks['payments']['completed'] = ! empty( $enabled_gateways );
			}

			if ( isset( $this->tasks['woocommerce-payments'] ) ) {
				$wc_pay_is_connected = false;
				if ( class_exists( '\WC_Payments' ) ) {
					$wc_payments_gateway = \WC_Payments::get_gateway();
					$wc_pay_is_connected = method_exists( $wc_payments_gateway, 'is_connected' )
						? $wc_payments_gateway->is_connected()
						: false;
				}
				$this->tasks['woocommerce-payments']['completed'] = $wc_pay_is_connected;
			}
		}
	}

endif;

return new WC_Admin_Dashboard_Setup();
admin/class-wc-admin-meta-boxes.php000064400000022655151336044770013235 0ustar00<?php
/**
 * WooCommerce Meta Boxes
 *
 * Sets up the write panels used by products and orders (custom post types).
 *
 * @package WooCommerce\Admin\Meta Boxes
 */

use Automattic\Jetpack\Constants;

defined( 'ABSPATH' ) || exit;

/**
 * WC_Admin_Meta_Boxes.
 */
class WC_Admin_Meta_Boxes {

	/**
	 * Is meta boxes saved once?
	 *
	 * @var boolean
	 */
	private static $saved_meta_boxes = false;

	/**
	 * Meta box error messages.
	 *
	 * @var array
	 */
	public static $meta_box_errors = array();

	/**
	 * Constructor.
	 */
	public function __construct() {
		add_action( 'add_meta_boxes', array( $this, 'remove_meta_boxes' ), 10 );
		add_action( 'add_meta_boxes', array( $this, 'rename_meta_boxes' ), 20 );
		add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ), 30 );
		add_action( 'save_post', array( $this, 'save_meta_boxes' ), 1, 2 );

		/**
		 * Save Order Meta Boxes.
		 *
		 * In order:
		 *      Save the order items.
		 *      Save the order totals.
		 *      Save the order downloads.
		 *      Save order data - also updates status and sends out admin emails if needed. Last to show latest data.
		 *      Save actions - sends out other emails. Last to show latest data.
		 */
		add_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Items::save', 10 );
		add_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Downloads::save', 30, 2 );
		add_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Data::save', 40 );
		add_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Actions::save', 50, 2 );

		// Save Product Meta Boxes.
		add_action( 'woocommerce_process_product_meta', 'WC_Meta_Box_Product_Data::save', 10, 2 );
		add_action( 'woocommerce_process_product_meta', 'WC_Meta_Box_Product_Images::save', 20, 2 );

		// Save Coupon Meta Boxes.
		add_action( 'woocommerce_process_shop_coupon_meta', 'WC_Meta_Box_Coupon_Data::save', 10, 2 );

		// Save Rating Meta Boxes.
		add_filter( 'wp_update_comment_data', 'WC_Meta_Box_Product_Reviews::save', 1 );

		// Error handling (for showing errors from meta boxes on next page load).
		add_action( 'admin_notices', array( $this, 'output_errors' ) );
		add_action( 'shutdown', array( $this, 'save_errors' ) );

		add_filter( 'theme_product_templates', array( $this, 'remove_block_templates' ), 10, 1 );
	}

	/**
	 * Add an error message.
	 *
	 * @param string $text Error to add.
	 */
	public static function add_error( $text ) {
		self::$meta_box_errors[] = $text;
	}

	/**
	 * Save errors to an option.
	 */
	public function save_errors() {
		update_option( 'woocommerce_meta_box_errors', self::$meta_box_errors );
	}

	/**
	 * Show any stored error messages.
	 */
	public function output_errors() {
		$errors = array_filter( (array) get_option( 'woocommerce_meta_box_errors' ) );

		if ( ! empty( $errors ) ) {

			echo '<div id="woocommerce_errors" class="error notice is-dismissible">';

			foreach ( $errors as $error ) {
				echo '<p>' . wp_kses_post( $error ) . '</p>';
			}

			echo '</div>';

			// Clear.
			delete_option( 'woocommerce_meta_box_errors' );
		}
	}

	/**
	 * Add WC Meta boxes.
	 */
	public function add_meta_boxes() {
		$screen    = get_current_screen();
		$screen_id = $screen ? $screen->id : '';

		// Products.
		add_meta_box( 'postexcerpt', __( 'Product short description', 'woocommerce' ), 'WC_Meta_Box_Product_Short_Description::output', 'product', 'normal' );
		add_meta_box( 'woocommerce-product-data', __( 'Product data', 'woocommerce' ), 'WC_Meta_Box_Product_Data::output', 'product', 'normal', 'high' );
		add_meta_box( 'woocommerce-product-images', __( 'Product gallery', 'woocommerce' ), 'WC_Meta_Box_Product_Images::output', 'product', 'side', 'low' );

		// Orders.
		foreach ( wc_get_order_types( 'order-meta-boxes' ) as $type ) {
			$order_type_object = get_post_type_object( $type );
			/* Translators: %s order type name. */
			add_meta_box( 'woocommerce-order-data', sprintf( __( '%s data', 'woocommerce' ), $order_type_object->labels->singular_name ), 'WC_Meta_Box_Order_Data::output', $type, 'normal', 'high' );
			add_meta_box( 'woocommerce-order-items', __( 'Items', 'woocommerce' ), 'WC_Meta_Box_Order_Items::output', $type, 'normal', 'high' );
			/* Translators: %s order type name. */
			add_meta_box( 'woocommerce-order-notes', sprintf( __( '%s notes', 'woocommerce' ), $order_type_object->labels->singular_name ), 'WC_Meta_Box_Order_Notes::output', $type, 'side', 'default' );
			add_meta_box( 'woocommerce-order-downloads', __( 'Downloadable product permissions', 'woocommerce' ) . wc_help_tip( __( 'Note: Permissions for order items will automatically be granted when the order status changes to processing/completed.', 'woocommerce' ) ), 'WC_Meta_Box_Order_Downloads::output', $type, 'normal', 'default' );
			/* Translators: %s order type name. */
			add_meta_box( 'woocommerce-order-actions', sprintf( __( '%s actions', 'woocommerce' ), $order_type_object->labels->singular_name ), 'WC_Meta_Box_Order_Actions::output', $type, 'side', 'high' );
		}

		// Coupons.
		add_meta_box( 'woocommerce-coupon-data', __( 'Coupon data', 'woocommerce' ), 'WC_Meta_Box_Coupon_Data::output', 'shop_coupon', 'normal', 'high' );

		// Comment rating.
		if ( 'comment' === $screen_id && isset( $_GET['c'] ) && metadata_exists( 'comment', wc_clean( wp_unslash( $_GET['c'] ) ), 'rating' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			add_meta_box( 'woocommerce-rating', __( 'Rating', 'woocommerce' ), 'WC_Meta_Box_Product_Reviews::output', 'comment', 'normal', 'high' );
		}
	}

	/**
	 * Remove bloat.
	 */
	public function remove_meta_boxes() {
		remove_meta_box( 'postexcerpt', 'product', 'normal' );
		remove_meta_box( 'product_shipping_classdiv', 'product', 'side' );
		remove_meta_box( 'commentsdiv', 'product', 'normal' );
		remove_meta_box( 'commentstatusdiv', 'product', 'side' );
		remove_meta_box( 'commentstatusdiv', 'product', 'normal' );
		remove_meta_box( 'woothemes-settings', 'shop_coupon', 'normal' );
		remove_meta_box( 'commentstatusdiv', 'shop_coupon', 'normal' );
		remove_meta_box( 'slugdiv', 'shop_coupon', 'normal' );

		foreach ( wc_get_order_types( 'order-meta-boxes' ) as $type ) {
			remove_meta_box( 'commentsdiv', $type, 'normal' );
			remove_meta_box( 'woothemes-settings', $type, 'normal' );
			remove_meta_box( 'commentstatusdiv', $type, 'normal' );
			remove_meta_box( 'slugdiv', $type, 'normal' );
			remove_meta_box( 'submitdiv', $type, 'side' );
		}
	}

	/**
	 * Rename core meta boxes.
	 */
	public function rename_meta_boxes() {
		global $post;

		// Comments/Reviews.
		if ( isset( $post ) && ( 'publish' === $post->post_status || 'private' === $post->post_status ) && post_type_supports( 'product', 'comments' ) ) {
			remove_meta_box( 'commentsdiv', 'product', 'normal' );
			add_meta_box( 'commentsdiv', __( 'Reviews', 'woocommerce' ), 'post_comment_meta_box', 'product', 'normal' );
		}
	}

	/**
	 * Check if we're saving, the trigger an action based on the post type.
	 *
	 * @param  int    $post_id Post ID.
	 * @param  object $post Post object.
	 */
	public function save_meta_boxes( $post_id, $post ) {
		$post_id = absint( $post_id );

		// $post_id and $post are required
		if ( empty( $post_id ) || empty( $post ) || self::$saved_meta_boxes ) {
			return;
		}

		// Dont' save meta boxes for revisions or autosaves.
		if ( Constants::is_true( 'DOING_AUTOSAVE' ) || is_int( wp_is_post_revision( $post ) ) || is_int( wp_is_post_autosave( $post ) ) ) {
			return;
		}

		// Check the nonce.
		if ( empty( $_POST['woocommerce_meta_nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['woocommerce_meta_nonce'] ), 'woocommerce_save_data' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			return;
		}

		// Check the post being saved == the $post_id to prevent triggering this call for other save_post events.
		if ( empty( $_POST['post_ID'] ) || absint( $_POST['post_ID'] ) !== $post_id ) {
			return;
		}

		// Check user has permission to edit.
		if ( ! current_user_can( 'edit_post', $post_id ) ) {
			return;
		}

		// We need this save event to run once to avoid potential endless loops. This would have been perfect:
		// remove_action( current_filter(), __METHOD__ );
		// But cannot be used due to https://github.com/woocommerce/woocommerce/issues/6485
		// When that is patched in core we can use the above.
		self::$saved_meta_boxes = true;

		// Check the post type.
		if ( in_array( $post->post_type, wc_get_order_types( 'order-meta-boxes' ), true ) ) {
			do_action( 'woocommerce_process_shop_order_meta', $post_id, $post );
		} elseif ( in_array( $post->post_type, array( 'product', 'shop_coupon' ), true ) ) {
			do_action( 'woocommerce_process_' . $post->post_type . '_meta', $post_id, $post );
		}
	}

	/**
	 * Remove block-based templates from the list of available templates for products.
	 *
	 * @param string[] $templates Array of template header names keyed by the template file name.
	 *
	 * @return string[] Templates array excluding block-based templates.
	 */
	public function remove_block_templates( $templates ) {
		if ( count( $templates ) === 0 || ! function_exists( 'gutenberg_get_block_template' ) ) {
			return $templates;
		}

		$theme              = wp_get_theme()->get_stylesheet();
		$filtered_templates = array();

		foreach ( $templates as $template_key => $template_name ) {
			$gutenberg_template = gutenberg_get_block_template( $theme . '//' . $template_key );

			if ( ! $gutenberg_template ) {
				$filtered_templates[ $template_key ] = $template_name;
			}
		}

		return $filtered_templates;
	}
}

new WC_Admin_Meta_Boxes();
admin/plugin-updates/class-wc-plugins-screen-updates.php000064400000013046151336044770017437 0ustar00<?php
/**
 * Manages WooCommerce plugin updating on the Plugins screen.
 *
 * @package     WooCommerce\Admin
 * @version     3.2.0
 */

use Automattic\Jetpack\Constants;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'WC_Plugin_Updates' ) ) {
	include_once dirname( __FILE__ ) . '/class-wc-plugin-updates.php';
}

/**
 * Class WC_Plugins_Screen_Updates
 */
class WC_Plugins_Screen_Updates extends WC_Plugin_Updates {

	/**
	 * The upgrade notice shown inline.
	 *
	 * @var string
	 */
	protected $upgrade_notice = '';

	/**
	 * Constructor.
	 */
	public function __construct() {
		add_action( 'in_plugin_update_message-woocommerce/woocommerce.php', array( $this, 'in_plugin_update_message' ), 10, 2 );
	}

	/**
	 * Show plugin changes on the plugins screen. Code adapted from W3 Total Cache.
	 *
	 * @param array    $args Unused parameter.
	 * @param stdClass $response Plugin update response.
	 */
	public function in_plugin_update_message( $args, $response ) {
		$version_type = Constants::get_constant( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE' );
		if ( ! is_string( $version_type ) ) {
			$version_type = 'none';
		}

		$this->new_version            = $response->new_version;
		$this->upgrade_notice         = $this->get_upgrade_notice( $response->new_version );
		$this->major_untested_plugins = $this->get_untested_plugins( $response->new_version, $version_type );

		$current_version_parts = explode( '.', Constants::get_constant( 'WC_VERSION' ) );
		$new_version_parts     = explode( '.', $this->new_version );

		// If user has already moved to the minor version, we don't need to flag up anything.
		if ( version_compare( $current_version_parts[0] . '.' . $current_version_parts[1], $new_version_parts[0] . '.' . $new_version_parts[1], '=' ) ) {
			return;
		}

		if ( ! empty( $this->major_untested_plugins ) ) {
			$this->upgrade_notice .= $this->get_extensions_inline_warning_major();
		}

		if ( ! empty( $this->major_untested_plugins ) ) {
			$this->upgrade_notice .= $this->get_extensions_modal_warning();
			add_action( 'admin_print_footer_scripts', array( $this, 'plugin_screen_modal_js' ) );
		}

		echo apply_filters( 'woocommerce_in_plugin_update_message', $this->upgrade_notice ? '</p>' . wp_kses_post( $this->upgrade_notice ) . '<p class="dummy">' : '' ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped
	}

	/**
	 * Get the upgrade notice from WordPress.org.
	 *
	 * @param  string $version WooCommerce new version.
	 * @return string
	 */
	protected function get_upgrade_notice( $version ) {
		$transient_name = 'wc_upgrade_notice_' . $version;
		$upgrade_notice = get_transient( $transient_name );

		if ( false === $upgrade_notice ) {
			$response = wp_safe_remote_get( 'https://plugins.svn.wordpress.org/woocommerce/trunk/readme.txt' );

			if ( ! is_wp_error( $response ) && ! empty( $response['body'] ) ) {
				$upgrade_notice = $this->parse_update_notice( $response['body'], $version );
				set_transient( $transient_name, $upgrade_notice, DAY_IN_SECONDS );
			}
		}
		return $upgrade_notice;
	}

	/**
	 * Parse update notice from readme file.
	 *
	 * @param  string $content WooCommerce readme file content.
	 * @param  string $new_version WooCommerce new version.
	 * @return string
	 */
	private function parse_update_notice( $content, $new_version ) {
		$version_parts     = explode( '.', $new_version );
		$check_for_notices = array(
			$version_parts[0] . '.0', // Major.
			$version_parts[0] . '.0.0', // Major.
			$version_parts[0] . '.' . $version_parts[1], // Minor.
			$version_parts[0] . '.' . $version_parts[1] . '.' . $version_parts[2], // Patch.
		);
		$notice_regexp     = '~==\s*Upgrade Notice\s*==\s*=\s*(.*)\s*=(.*)(=\s*' . preg_quote( $new_version ) . '\s*=|$)~Uis';
		$upgrade_notice    = '';

		foreach ( $check_for_notices as $check_version ) {
			if ( version_compare( Constants::get_constant( 'WC_VERSION' ), $check_version, '>' ) ) {
				continue;
			}

			$matches = null;
			if ( preg_match( $notice_regexp, $content, $matches ) ) {
				$notices = (array) preg_split( '~[\r\n]+~', trim( $matches[2] ) );

				if ( version_compare( trim( $matches[1] ), $check_version, '=' ) ) {
					$upgrade_notice .= '<p class="wc_plugin_upgrade_notice">';

					foreach ( $notices as $index => $line ) {
						$upgrade_notice .= preg_replace( '~\[([^\]]*)\]\(([^\)]*)\)~', '<a href="${2}">${1}</a>', $line );
					}

					$upgrade_notice .= '</p>';
				}
				break;
			}
		}
		return wp_kses_post( $upgrade_notice );
	}

	/**
	 * JS for the modal window on the plugins screen.
	 */
	public function plugin_screen_modal_js() {
		?>
		<script>
			( function( $ ) {
				var $update_box = $( '#woocommerce-update' );
				var $update_link = $update_box.find('a.update-link').first();
				var update_url = $update_link.attr( 'href' );

				// Set up thickbox.
				$update_link.removeClass( 'update-link' );
				$update_link.addClass( 'wc-thickbox' );
				$update_link.attr( 'href', '#TB_inline?height=600&width=550&inlineId=wc_untested_extensions_modal' );

				// Trigger the update if the user accepts the modal's warning.
				$( '#wc_untested_extensions_modal .accept' ).on( 'click', function( evt ) {
					evt.preventDefault();
					tb_remove();
					$update_link.removeClass( 'wc-thickbox open-plugin-details-modal' );
					$update_link.addClass( 'update-link' );
					$update_link.attr( 'href', update_url );
					$update_link.trigger( 'click' );
				});

				$( '#wc_untested_extensions_modal .cancel' ).on( 'click', function( evt ) {
					evt.preventDefault();
					tb_remove();
				});
			})( jQuery );
		</script>
		<?php
		$this->generic_modal_js();
	}
}
new WC_Plugins_Screen_Updates();
admin/plugin-updates/class-wc-updates-screen-updates.php000064400000006070151336044770017422 0ustar00<?php
/**
 * Manages WooCommerce plugin updating on the Updates screen.
 *
 * @package     WooCommerce\Admin
 * @version     3.2.0
 */

use Automattic\Jetpack\Constants;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'WC_Plugin_Updates' ) ) {
	include_once dirname( __FILE__ ) . '/class-wc-plugin-updates.php';
}

/**
 * Class WC_Updates_Screen_Updates
 */
class WC_Updates_Screen_Updates extends WC_Plugin_Updates {

	/**
	 * Constructor.
	 */
	public function __construct() {
		add_action( 'admin_print_footer_scripts', array( $this, 'update_screen_modal' ) );
	}

	/**
	 * Show a warning message on the upgrades screen if the user tries to upgrade and has untested plugins.
	 */
	public function update_screen_modal() {
		$updateable_plugins = get_plugin_updates();
		if ( empty( $updateable_plugins['woocommerce/woocommerce.php'] )
			|| empty( $updateable_plugins['woocommerce/woocommerce.php']->update )
			|| empty( $updateable_plugins['woocommerce/woocommerce.php']->update->new_version ) ) {
			return;
		}

		$version_type = Constants::get_constant( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE' );
		if ( ! is_string( $version_type ) ) {
			$version_type = 'none';
		}

		$this->new_version            = wc_clean( $updateable_plugins['woocommerce/woocommerce.php']->update->new_version );
		$this->major_untested_plugins = $this->get_untested_plugins( $this->new_version, $version_type );

		if ( ! empty( $this->major_untested_plugins ) ) {
			echo $this->get_extensions_modal_warning(); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped
			$this->update_screen_modal_js();
		}
	}

	/**
	 * JS for the modal window on the updates screen.
	 */
	protected function update_screen_modal_js() {
		?>
		<script>
			( function( $ ) {
				var modal_dismissed = false;

				// Show the modal if the WC upgrade checkbox is checked.
				var show_modal_if_checked = function() {
					if ( modal_dismissed ) {
						return;
					}
					var $checkbox = $( 'input[value="woocommerce/woocommerce.php"]' );
					if ( $checkbox.prop( 'checked' ) ) {
						$( '#wc-upgrade-warning' ).trigger( 'click' );
					}
				}

				$( '#plugins-select-all, input[value="woocommerce/woocommerce.php"]' ).on( 'change', function() {
					show_modal_if_checked();
				} );

				// Add a hidden thickbox link to use for bringing up the modal.
				$('body').append( '<a href="#TB_inline?height=600&width=550&inlineId=wc_untested_extensions_modal" class="wc-thickbox" id="wc-upgrade-warning" style="display:none"></a>' );

				// Don't show the modal again once it's been accepted.
				$( '#wc_untested_extensions_modal .accept' ).on( 'click', function( evt ) {
					evt.preventDefault();
					modal_dismissed = true;
					tb_remove();
				});

				// Uncheck the WC update checkbox if the modal is canceled.
				$( '#wc_untested_extensions_modal .cancel' ).on( 'click', function( evt ) {
					evt.preventDefault();
					$( 'input[value="woocommerce/woocommerce.php"]' ).prop( 'checked', false );
					tb_remove();
				});
			})( jQuery );
		</script>
		<?php
		$this->generic_modal_js();
	}
}
new WC_Updates_Screen_Updates();
admin/plugin-updates/views/html-notice-untested-extensions-modal.php000064400000003565151336044770022031 0ustar00<?php
/**
 * Admin View: Notice - Untested extensions.
 *
 * @package WooCommerce\Admin
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

$untested_plugins_msg = sprintf(
	/* translators: %s: version number */
	__( 'The following active plugin(s) have not declared compatibility with WooCommerce %s yet and should be updated and examined further before you proceed:', 'woocommerce' ),
	$new_version
);

?>
<div id="wc_untested_extensions_modal">
	<div class="wc_untested_extensions_modal--content">
		<h1><?php esc_html_e( "Are you sure you're ready?", 'woocommerce' ); ?></h1>
		<div class="wc_plugin_upgrade_notice extensions_warning">
			<p><?php echo esc_html( $untested_plugins_msg ); ?></p>

			<div class="plugin-details-table-container">
				<table class="plugin-details-table" cellspacing="0">
					<thead>
						<tr>
							<th><?php esc_html_e( 'Plugin', 'woocommerce' ); ?></th>
							<th><?php esc_html_e( 'Tested up to WooCommerce version', 'woocommerce' ); ?></th>
						</tr>
					</thead>
					<tbody>
						<?php foreach ( $plugins as $plugin ) : ?>
							<tr>
								<td><?php echo esc_html( $plugin['Name'] ); ?></td>
								<td><?php echo esc_html( $plugin['WC tested up to'] ); ?></td>
							</tr>
						<?php endforeach ?>
					</tbody>
				</table>
			</div>

			<p><?php esc_html_e( 'We strongly recommend creating a backup of your site before updating.', 'woocommerce' ); ?> <a href="https://woocommerce.com/2017/05/create-use-backups-woocommerce/" target="_blank"><?php esc_html_e( 'Learn more', 'woocommerce' ); ?></a></p>

			<?php if ( current_user_can( 'update_plugins' ) ) : ?>
				<div class="actions">
					<a href="#" class="button button-secondary cancel"><?php esc_html_e( 'Cancel', 'woocommerce' ); ?></a>
					<a class="button button-primary accept" href="#"><?php esc_html_e( 'Update now', 'woocommerce' ); ?></a>
				</div>
			<?php endif ?>
		</div>
	</div>
</div>
admin/plugin-updates/views/html-notice-untested-extensions-inline.php000064400000001410151336044770022176 0ustar00<?php
/**
 * Admin View: Notice - Untested extensions.
 *
 * @package WooCommerce\Admin
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>
<div class="wc_plugin_upgrade_notice extensions_warning <?php echo esc_attr( $upgrade_type ); ?>">
	<p><?php echo wp_kses_post( $message ); ?></p>

	<table class="plugin-details-table" cellspacing="0">
		<thead>
			<tr>
				<th><?php esc_html_e( 'Plugin', 'woocommerce' ); ?></th>
				<th><?php esc_html_e( 'Tested up to WooCommerce version', 'woocommerce' ); ?></th>
			</tr>
		</thead>
		<tbody>
			<?php foreach ( $plugins as $plugin ) : ?>
				<tr>
					<td><?php echo esc_html( $plugin['Name'] ); ?></td>
					<td><?php echo esc_html( $plugin['WC tested up to'] ); ?></td>
				</tr>
			<?php endforeach ?>
		</tbody>
	</table>
</div>
admin/plugin-updates/class-wc-plugin-updates.php000064400000015367151336044770016007 0ustar00<?php
/**
 * Class for displaying plugin warning notifications and determining 3rd party plugin compatibility.
 *
 * @package     WooCommerce\Admin
 * @version     3.2.0
 */

use Automattic\Jetpack\Constants;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC_Admin_Plugin_Updates Class.
 */
class WC_Plugin_Updates {

	/**
	 * This is the header used by extensions to show requirements.
	 *
	 * @var string
	 */
	const VERSION_REQUIRED_HEADER = 'WC requires at least';

	/**
	 * This is the header used by extensions to show testing.
	 *
	 * @var string
	 */
	const VERSION_TESTED_HEADER = 'WC tested up to';

	/**
	 * The version for the update to WooCommerce.
	 *
	 * @var string
	 */
	protected $new_version = '';

	/**
	 * Array of plugins lacking testing with the major version.
	 *
	 * @var array
	 */
	protected $major_untested_plugins = array();

	/**
	 * Common JS for initializing and managing thickbox-based modals.
	 */
	protected function generic_modal_js() {
		?>
		<script>
			( function( $ ) {
				// Initialize thickbox.
				tb_init( '.wc-thickbox' );

				var old_tb_position = false;

				// Make the WC thickboxes look good when opened.
				$( '.wc-thickbox' ).on( 'click', function( evt ) {
					var $overlay = $( '#TB_overlay' );
					if ( ! $overlay.length ) {
						$( 'body' ).append( '<div id="TB_overlay"></div><div id="TB_window" class="wc_untested_extensions_modal_container"></div>' );
					} else {
						$( '#TB_window' ).removeClass( 'thickbox-loading' ).addClass( 'wc_untested_extensions_modal_container' );
					}

					// WP overrides the tb_position function. We need to use a different tb_position function than that one.
					// This is based on the original tb_position.
					if ( ! old_tb_position ) {
						old_tb_position = tb_position;
					}
					tb_position = function() {
						$( '#TB_window' ).css( { marginLeft: '-' + parseInt( ( TB_WIDTH / 2 ), 10 ) + 'px', width: TB_WIDTH + 'px' } );
						$( '#TB_window' ).css( { marginTop: '-' + parseInt( ( TB_HEIGHT / 2 ), 10 ) + 'px' } );
					};
				});

				// Reset tb_position to WP default when modal is closed.
				$( 'body' ).on( 'thickbox:removed', function() {
					if ( old_tb_position ) {
						tb_position = old_tb_position;
					}
				});
			})( jQuery );
		</script>
		<?php
	}

	/*
	|--------------------------------------------------------------------------
	| Message Helpers
	|--------------------------------------------------------------------------
	|
	| Methods for getting messages.
	*/

	/**
	 * Get the inline warning notice for major version updates.
	 *
	 * @return string
	 */
	protected function get_extensions_inline_warning_major() {
		$upgrade_type  = 'major';
		$plugins       = $this->major_untested_plugins;
		$version_parts = explode( '.', $this->new_version );
		$new_version   = $version_parts[0] . '.0';

		if ( empty( $plugins ) ) {
			return;
		}

		/* translators: %s: version number */
		$message = sprintf( __( "<strong>Heads up!</strong> The versions of the following plugins you're running haven't been tested with WooCommerce %s. Please update them or confirm compatibility before updating WooCommerce, or you may experience issues:", 'woocommerce' ), $new_version );

		ob_start();
		include __DIR__ . '/views/html-notice-untested-extensions-inline.php';
		return ob_get_clean();
	}

	/**
	 * Get the warning notice for the modal window.
	 *
	 * @return string
	 */
	protected function get_extensions_modal_warning() {
		$version_parts = explode( '.', $this->new_version );
		$new_version   = $version_parts[0] . '.0';
		$plugins       = $this->major_untested_plugins;

		ob_start();
		include __DIR__ . '/views/html-notice-untested-extensions-modal.php';
		return ob_get_clean();
	}

	/*
	|--------------------------------------------------------------------------
	| Data Helpers
	|--------------------------------------------------------------------------
	|
	| Methods for getting & manipulating data.
	*/

	/**
	 * Get installed plugins that have a tested version lower than the input version.
	 *
	 * In case of testing major version compatibility and if current WC version is >= major version part
	 * of the $new_version, no plugins are returned, even if they don't explicitly declare compatibility
	 * with the $new_version.
	 *
	 * @param string $new_version WooCommerce version to test against.
	 * @param string $release 'major', 'minor', or 'none'.
	 * @return array of plugin info arrays
	 */
	public function get_untested_plugins( $new_version, $release ) {
		// Since 5.0 all versions are backwards compatible.
		if ( 'none' === $release ) {
			return array();
		}

		$extensions        = array_merge( $this->get_plugins_with_header( self::VERSION_TESTED_HEADER ), $this->get_plugins_for_woocommerce() );
		$untested          = array();
		$new_version_parts = explode( '.', $new_version );
		$version           = $new_version_parts[0];

		if ( 'minor' === $release ) {
			$version .= '.' . $new_version_parts[1];
		}

		foreach ( $extensions as $file => $plugin ) {
			if ( ! empty( $plugin[ self::VERSION_TESTED_HEADER ] ) ) {
				$plugin_version_parts = explode( '.', $plugin[ self::VERSION_TESTED_HEADER ] );

				if ( ! is_numeric( $plugin_version_parts[0] )
					|| ( 'minor' === $release && ! isset( $plugin_version_parts[1] ) )
					|| ( 'minor' === $release && ! is_numeric( $plugin_version_parts[1] ) )
					) {
					continue;
				}

				$plugin_version = $plugin_version_parts[0];

				if ( 'minor' === $release ) {
					$plugin_version .= '.' . $plugin_version_parts[1];
				}

				if ( version_compare( $plugin_version, $version, '<' ) ) {
					$untested[ $file ] = $plugin;
				}
			} else {
				$plugin[ self::VERSION_TESTED_HEADER ] = __( 'unknown', 'woocommerce' );
				$untested[ $file ]                     = $plugin;
			}
		}

		return $untested;
	}

	/**
	 * Get plugins that have a valid value for a specific header.
	 *
	 * @param string $header Plugin header to search for.
	 * @return array Array of plugins that contain the searched header.
	 */
	protected function get_plugins_with_header( $header ) {
		$plugins = get_plugins();
		$matches = array();

		foreach ( $plugins as $file => $plugin ) {
			if ( ! empty( $plugin[ $header ] ) ) {
				$matches[ $file ] = $plugin;
			}
		}

		return apply_filters( 'woocommerce_get_plugins_with_header', $matches, $header, $plugins );
	}

	/**
	 * Get plugins which "maybe" are for WooCommerce.
	 *
	 * @return array of plugin info arrays
	 */
	protected function get_plugins_for_woocommerce() {
		$plugins = get_plugins();
		$matches = array();

		foreach ( $plugins as $file => $plugin ) {
			if ( 'WooCommerce' !== $plugin['Name'] && ( stristr( $plugin['Name'], 'woocommerce' ) || stristr( $plugin['Description'], 'woocommerce' ) ) ) {
				$matches[ $file ] = $plugin;
			}
		}

		return apply_filters( 'woocommerce_get_plugins_for_woocommerce', $matches, $plugins );
	}
}
admin/class-wc-admin-post-types.php000064400000107313151336044770013313 0ustar00<?php
/**
 * Post Types Admin
 *
 * @package  WooCommerce\Admin
 * @version  3.3.0
 */

use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Utilities\NumberUtil;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( class_exists( 'WC_Admin_Post_Types', false ) ) {
	new WC_Admin_Post_Types();
	return;
}

/**
 * WC_Admin_Post_Types Class.
 *
 * Handles the edit posts views and some functionality on the edit post screen for WC post types.
 */
class WC_Admin_Post_Types {

	/**
	 * Constructor.
	 */
	public function __construct() {
		include_once __DIR__ . '/class-wc-admin-meta-boxes.php';

		if ( ! function_exists( 'duplicate_post_plugin_activation' ) ) {
			include_once __DIR__ . '/class-wc-admin-duplicate-product.php';
		}

		// Load correct list table classes for current screen.
		add_action( 'current_screen', array( $this, 'setup_screen' ) );
		add_action( 'check_ajax_referer', array( $this, 'setup_screen' ) );

		// Admin notices.
		add_filter( 'post_updated_messages', array( $this, 'post_updated_messages' ) );
		add_filter( 'bulk_post_updated_messages', array( $this, 'bulk_post_updated_messages' ), 10, 2 );

		// Disable Auto Save.
		add_action( 'admin_print_scripts', array( $this, 'disable_autosave' ) );

		// Extra post data and screen elements.
		add_action( 'edit_form_top', array( $this, 'edit_form_top' ) );
		add_filter( 'enter_title_here', array( $this, 'enter_title_here' ), 1, 2 );
		add_action( 'edit_form_after_title', array( $this, 'edit_form_after_title' ) );
		add_filter( 'default_hidden_meta_boxes', array( $this, 'hidden_meta_boxes' ), 10, 2 );
		add_action( 'post_submitbox_misc_actions', array( $this, 'product_data_visibility' ) );

		// Uploads.
		add_filter( 'upload_dir', array( $this, 'upload_dir' ) );
		add_filter( 'wp_unique_filename', array( $this, 'update_filename' ), 10, 3 );
		add_action( 'media_upload_downloadable_product', array( $this, 'media_upload_downloadable_product' ) );

		// Hide template for CPT archive.
		add_filter( 'theme_page_templates', array( $this, 'hide_cpt_archive_templates' ), 10, 3 );
		add_action( 'edit_form_top', array( $this, 'show_cpt_archive_notice' ) );

		// Add a post display state for special WC pages.
		add_filter( 'display_post_states', array( $this, 'add_display_post_states' ), 10, 2 );

		// Bulk / quick edit.
		add_action( 'bulk_edit_custom_box', array( $this, 'bulk_edit' ), 10, 2 );
		add_action( 'quick_edit_custom_box', array( $this, 'quick_edit' ), 10, 2 );
		add_action( 'save_post', array( $this, 'bulk_and_quick_edit_hook' ), 10, 2 );
		add_action( 'woocommerce_product_bulk_and_quick_edit', array( $this, 'bulk_and_quick_edit_save_post' ), 10, 2 );
	}

	/**
	 * Looks at the current screen and loads the correct list table handler.
	 *
	 * @since 3.3.0
	 */
	public function setup_screen() {
		global $wc_list_table;

		$request_data = $this->request_data();

		$screen_id = false;

		if ( function_exists( 'get_current_screen' ) ) {
			$screen    = get_current_screen();
			$screen_id = isset( $screen, $screen->id ) ? $screen->id : '';
		}

		if ( ! empty( $request_data['screen'] ) ) {
			$screen_id = wc_clean( wp_unslash( $request_data['screen'] ) );
		}

		switch ( $screen_id ) {
			case 'edit-shop_order':
				include_once __DIR__ . '/list-tables/class-wc-admin-list-table-orders.php';
				$wc_list_table = new WC_Admin_List_Table_Orders();
				break;
			case 'edit-shop_coupon':
				include_once __DIR__ . '/list-tables/class-wc-admin-list-table-coupons.php';
				$wc_list_table = new WC_Admin_List_Table_Coupons();
				break;
			case 'edit-product':
				include_once __DIR__ . '/list-tables/class-wc-admin-list-table-products.php';
				$wc_list_table = new WC_Admin_List_Table_Products();
				break;
		}

		// Ensure the table handler is only loaded once. Prevents multiple loads if a plugin calls check_ajax_referer many times.
		remove_action( 'current_screen', array( $this, 'setup_screen' ) );
		remove_action( 'check_ajax_referer', array( $this, 'setup_screen' ) );
	}

	/**
	 * Change messages when a post type is updated.
	 *
	 * @param  array $messages Array of messages.
	 * @return array
	 */
	public function post_updated_messages( $messages ) {
		global $post;

		$messages['product'] = array(
			0  => '', // Unused. Messages start at index 1.
			/* translators: %s: Product view URL. */
			1  => sprintf( __( 'Product updated. <a href="%s">View Product</a>', 'woocommerce' ), esc_url( get_permalink( $post->ID ) ) ),
			2  => __( 'Custom field updated.', 'woocommerce' ),
			3  => __( 'Custom field deleted.', 'woocommerce' ),
			4  => __( 'Product updated.', 'woocommerce' ),
			5  => __( 'Revision restored.', 'woocommerce' ),
			/* translators: %s: product url */
			6  => sprintf( __( 'Product published. <a href="%s">View Product</a>', 'woocommerce' ), esc_url( get_permalink( $post->ID ) ) ),
			7  => __( 'Product saved.', 'woocommerce' ),
			/* translators: %s: product url */
			8  => sprintf( __( 'Product submitted. <a target="_blank" href="%s">Preview product</a>', 'woocommerce' ), esc_url( add_query_arg( 'preview', 'true', get_permalink( $post->ID ) ) ) ),
			9  => sprintf(
				/* translators: 1: date 2: product url */
				__( 'Product scheduled for: %1$s. <a target="_blank" href="%2$s">Preview product</a>', 'woocommerce' ),
				'<strong>' . date_i18n( __( 'M j, Y @ G:i', 'woocommerce' ), strtotime( $post->post_date ) ) . '</strong>',
				esc_url( get_permalink( $post->ID ) )
			),
			/* translators: %s: product url */
			10 => sprintf( __( 'Product draft updated. <a target="_blank" href="%s">Preview product</a>', 'woocommerce' ), esc_url( add_query_arg( 'preview', 'true', get_permalink( $post->ID ) ) ) ),
		);

		$messages['shop_order'] = array(
			0  => '', // Unused. Messages start at index 1.
			1  => __( 'Order updated.', 'woocommerce' ),
			2  => __( 'Custom field updated.', 'woocommerce' ),
			3  => __( 'Custom field deleted.', 'woocommerce' ),
			4  => __( 'Order updated.', 'woocommerce' ),
			5  => __( 'Revision restored.', 'woocommerce' ),
			6  => __( 'Order updated.', 'woocommerce' ),
			7  => __( 'Order saved.', 'woocommerce' ),
			8  => __( 'Order submitted.', 'woocommerce' ),
			9  => sprintf(
				/* translators: %s: date */
				__( 'Order scheduled for: %s.', 'woocommerce' ),
				'<strong>' . date_i18n( __( 'M j, Y @ G:i', 'woocommerce' ), strtotime( $post->post_date ) ) . '</strong>'
			),
			10 => __( 'Order draft updated.', 'woocommerce' ),
			11 => __( 'Order updated and sent.', 'woocommerce' ),
		);

		$messages['shop_coupon'] = array(
			0  => '', // Unused. Messages start at index 1.
			1  => __( 'Coupon updated.', 'woocommerce' ),
			2  => __( 'Custom field updated.', 'woocommerce' ),
			3  => __( 'Custom field deleted.', 'woocommerce' ),
			4  => __( 'Coupon updated.', 'woocommerce' ),
			5  => __( 'Revision restored.', 'woocommerce' ),
			6  => __( 'Coupon updated.', 'woocommerce' ),
			7  => __( 'Coupon saved.', 'woocommerce' ),
			8  => __( 'Coupon submitted.', 'woocommerce' ),
			9  => sprintf(
				/* translators: %s: date */
				__( 'Coupon scheduled for: %s.', 'woocommerce' ),
				'<strong>' . date_i18n( __( 'M j, Y @ G:i', 'woocommerce' ), strtotime( $post->post_date ) ) . '</strong>'
			),
			10 => __( 'Coupon draft updated.', 'woocommerce' ),
		);

		return $messages;
	}

	/**
	 * Specify custom bulk actions messages for different post types.
	 *
	 * @param  array $bulk_messages Array of messages.
	 * @param  array $bulk_counts Array of how many objects were updated.
	 * @return array
	 */
	public function bulk_post_updated_messages( $bulk_messages, $bulk_counts ) {
		$bulk_messages['product'] = array(
			/* translators: %s: product count */
			'updated'   => _n( '%s product updated.', '%s products updated.', $bulk_counts['updated'], 'woocommerce' ),
			/* translators: %s: product count */
			'locked'    => _n( '%s product not updated, somebody is editing it.', '%s products not updated, somebody is editing them.', $bulk_counts['locked'], 'woocommerce' ),
			/* translators: %s: product count */
			'deleted'   => _n( '%s product permanently deleted.', '%s products permanently deleted.', $bulk_counts['deleted'], 'woocommerce' ),
			/* translators: %s: product count */
			'trashed'   => _n( '%s product moved to the Trash.', '%s products moved to the Trash.', $bulk_counts['trashed'], 'woocommerce' ),
			/* translators: %s: product count */
			'untrashed' => _n( '%s product restored from the Trash.', '%s products restored from the Trash.', $bulk_counts['untrashed'], 'woocommerce' ),
		);

		$bulk_messages['shop_order'] = array(
			/* translators: %s: order count */
			'updated'   => _n( '%s order updated.', '%s orders updated.', $bulk_counts['updated'], 'woocommerce' ),
			/* translators: %s: order count */
			'locked'    => _n( '%s order not updated, somebody is editing it.', '%s orders not updated, somebody is editing them.', $bulk_counts['locked'], 'woocommerce' ),
			/* translators: %s: order count */
			'deleted'   => _n( '%s order permanently deleted.', '%s orders permanently deleted.', $bulk_counts['deleted'], 'woocommerce' ),
			/* translators: %s: order count */
			'trashed'   => _n( '%s order moved to the Trash.', '%s orders moved to the Trash.', $bulk_counts['trashed'], 'woocommerce' ),
			/* translators: %s: order count */
			'untrashed' => _n( '%s order restored from the Trash.', '%s orders restored from the Trash.', $bulk_counts['untrashed'], 'woocommerce' ),
		);

		$bulk_messages['shop_coupon'] = array(
			/* translators: %s: coupon count */
			'updated'   => _n( '%s coupon updated.', '%s coupons updated.', $bulk_counts['updated'], 'woocommerce' ),
			/* translators: %s: coupon count */
			'locked'    => _n( '%s coupon not updated, somebody is editing it.', '%s coupons not updated, somebody is editing them.', $bulk_counts['locked'], 'woocommerce' ),
			/* translators: %s: coupon count */
			'deleted'   => _n( '%s coupon permanently deleted.', '%s coupons permanently deleted.', $bulk_counts['deleted'], 'woocommerce' ),
			/* translators: %s: coupon count */
			'trashed'   => _n( '%s coupon moved to the Trash.', '%s coupons moved to the Trash.', $bulk_counts['trashed'], 'woocommerce' ),
			/* translators: %s: coupon count */
			'untrashed' => _n( '%s coupon restored from the Trash.', '%s coupons restored from the Trash.', $bulk_counts['untrashed'], 'woocommerce' ),
		);

		return $bulk_messages;
	}

	/**
	 * Custom bulk edit - form.
	 *
	 * @param string $column_name Column being shown.
	 * @param string $post_type Post type being shown.
	 */
	public function bulk_edit( $column_name, $post_type ) {
		if ( 'price' !== $column_name || 'product' !== $post_type ) {
			return;
		}

		$shipping_class = get_terms(
			'product_shipping_class',
			array(
				'hide_empty' => false,
			)
		);

		include WC()->plugin_path() . '/includes/admin/views/html-bulk-edit-product.php';
	}

	/**
	 * Custom quick edit - form.
	 *
	 * @param string $column_name Column being shown.
	 * @param string $post_type Post type being shown.
	 */
	public function quick_edit( $column_name, $post_type ) {
		if ( 'price' !== $column_name || 'product' !== $post_type ) {
			return;
		}

		$shipping_class = get_terms(
			'product_shipping_class',
			array(
				'hide_empty' => false,
			)
		);

		include WC()->plugin_path() . '/includes/admin/views/html-quick-edit-product.php';
	}

	/**
	 * Offers a way to hook into save post without causing an infinite loop
	 * when quick/bulk saving product info.
	 *
	 * @since 3.0.0
	 * @param int    $post_id Post ID being saved.
	 * @param object $post Post object being saved.
	 */
	public function bulk_and_quick_edit_hook( $post_id, $post ) {
		remove_action( 'save_post', array( $this, 'bulk_and_quick_edit_hook' ) );
		do_action( 'woocommerce_product_bulk_and_quick_edit', $post_id, $post );
		add_action( 'save_post', array( $this, 'bulk_and_quick_edit_hook' ), 10, 2 );
	}

	/**
	 * Quick and bulk edit saving.
	 *
	 * @param int    $post_id Post ID being saved.
	 * @param object $post Post object being saved.
	 * @return int
	 */
	public function bulk_and_quick_edit_save_post( $post_id, $post ) {
		$request_data = $this->request_data();

		// If this is an autosave, our form has not been submitted, so we don't want to do anything.
		if ( Constants::is_true( 'DOING_AUTOSAVE' ) ) {
			return $post_id;
		}

		// Don't save revisions and autosaves.
		if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) || 'product' !== $post->post_type || ! current_user_can( 'edit_post', $post_id ) ) {
			return $post_id;
		}

		// Check nonce.
		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
		if ( ! isset( $request_data['woocommerce_quick_edit_nonce'] ) || ! wp_verify_nonce( $request_data['woocommerce_quick_edit_nonce'], 'woocommerce_quick_edit_nonce' ) ) {
			return $post_id;
		}

		// Get the product and save.
		$product = wc_get_product( $post );

		if ( ! empty( $request_data['woocommerce_quick_edit'] ) ) { // WPCS: input var ok.
			$this->quick_edit_save( $post_id, $product );
		} else {
			$this->bulk_edit_save( $post_id, $product );
		}

		return $post_id;
	}

	/**
	 * Quick edit.
	 *
	 * @param int        $post_id Post ID being saved.
	 * @param WC_Product $product Product object.
	 */
	private function quick_edit_save( $post_id, $product ) {
		$request_data = $this->request_data();

		$data_store        = $product->get_data_store();
		$old_regular_price = $product->get_regular_price();
		$old_sale_price    = $product->get_sale_price();
		$input_to_props    = array(
			'_weight'     => 'weight',
			'_length'     => 'length',
			'_width'      => 'width',
			'_height'     => 'height',
			'_visibility' => 'catalog_visibility',
			'_tax_class'  => 'tax_class',
			'_tax_status' => 'tax_status',
		);

		foreach ( $input_to_props as $input_var => $prop ) {
			if ( isset( $request_data[ $input_var ] ) ) {
				$product->{"set_{$prop}"}( wc_clean( wp_unslash( $request_data[ $input_var ] ) ) );
			}
		}

		if ( isset( $request_data['_sku'] ) ) {
			$sku = $product->get_sku();
			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
			$new_sku = (string) wc_clean( $request_data['_sku'] );

			if ( $new_sku !== $sku ) {
				if ( ! empty( $new_sku ) ) {
					$unique_sku = wc_product_has_unique_sku( $post_id, $new_sku );
					if ( $unique_sku ) {
						$product->set_sku( wc_clean( wp_unslash( $new_sku ) ) );
					}
				} else {
					$product->set_sku( '' );
				}
			}
		}

		if ( ! empty( $request_data['_shipping_class'] ) ) {
			if ( '_no_shipping_class' === $request_data['_shipping_class'] ) {
				$product->set_shipping_class_id( 0 );
			} else {
				// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
				$shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $request_data['_shipping_class'] ) );
				$product->set_shipping_class_id( $shipping_class_id );
			}
		}

		$product->set_featured( isset( $request_data['_featured'] ) );

		if ( $product->is_type( 'simple' ) || $product->is_type( 'external' ) ) {

			if ( isset( $request_data['_regular_price'] ) ) {
				// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
				$new_regular_price = ( '' === $request_data['_regular_price'] ) ? '' : wc_format_decimal( $request_data['_regular_price'] );
				$product->set_regular_price( $new_regular_price );
			} else {
				$new_regular_price = null;
			}
			if ( isset( $request_data['_sale_price'] ) ) {
				// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
				$new_sale_price = ( '' === $request_data['_sale_price'] ) ? '' : wc_format_decimal( $request_data['_sale_price'] );
				$product->set_sale_price( $new_sale_price );
			} else {
				$new_sale_price = null;
			}

			// Handle price - remove dates and set to lowest.
			$price_changed = false;

			if ( ! is_null( $new_regular_price ) && $new_regular_price !== $old_regular_price ) {
				$price_changed = true;
			} elseif ( ! is_null( $new_sale_price ) && $new_sale_price !== $old_sale_price ) {
				$price_changed = true;
			}

			if ( $price_changed ) {
				$product->set_date_on_sale_to( '' );
				$product->set_date_on_sale_from( '' );
			}
		}

		// Handle Stock Data.
		// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$manage_stock = ! empty( $request_data['_manage_stock'] ) && 'grouped' !== $product->get_type() ? 'yes' : 'no';
		$backorders   = ! empty( $request_data['_backorders'] ) ? wc_clean( $request_data['_backorders'] ) : 'no';
		if ( ! empty( $request_data['_stock_status'] ) ) {
			$stock_status = wc_clean( $request_data['_stock_status'] );
		} else {
			$stock_status = $product->is_type( 'variable' ) ? null : 'instock';
		}
		// phpcs:enable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

		$product->set_manage_stock( $manage_stock );

		if ( 'external' !== $product->get_type() ) {
			$product->set_backorders( $backorders );
		}

		if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
			$stock_amount = 'yes' === $manage_stock && isset( $request_data['_stock'] ) && is_numeric( wp_unslash( $request_data['_stock'] ) ) ? wc_stock_amount( wp_unslash( $request_data['_stock'] ) ) : '';
			$product->set_stock_quantity( $stock_amount );
		}

		$product = $this->maybe_update_stock_status( $product, $stock_status );

		$product->save();

		do_action( 'woocommerce_product_quick_edit_save', $product );
	}

	/**
	 * Bulk edit.
	 *
	 * @param int        $post_id Post ID being saved.
	 * @param WC_Product $product Product object.
	 */
	public function bulk_edit_save( $post_id, $product ) {
		// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash

		$request_data = $this->request_data();

		$data_store = $product->get_data_store();

		if ( ! empty( $request_data['change_weight'] ) && isset( $request_data['_weight'] ) ) {
			$product->set_weight( wc_clean( wp_unslash( $request_data['_weight'] ) ) );
		}

		if ( ! empty( $request_data['change_dimensions'] ) ) {
			if ( isset( $request_data['_length'] ) ) {
				$product->set_length( wc_clean( wp_unslash( $request_data['_length'] ) ) );
			}
			if ( isset( $request_data['_width'] ) ) {
				$product->set_width( wc_clean( wp_unslash( $request_data['_width'] ) ) );
			}
			if ( isset( $request_data['_height'] ) ) {
				$product->set_height( wc_clean( wp_unslash( $request_data['_height'] ) ) );
			}
		}

		if ( ! empty( $request_data['_tax_status'] ) ) {
			$product->set_tax_status( wc_clean( $request_data['_tax_status'] ) );
		}

		if ( ! empty( $request_data['_tax_class'] ) ) {
			$tax_class = wc_clean( wp_unslash( $request_data['_tax_class'] ) );
			if ( 'standard' === $tax_class ) {
				$tax_class = '';
			}
			$product->set_tax_class( $tax_class );
		}

		if ( ! empty( $request_data['_shipping_class'] ) ) {
			if ( '_no_shipping_class' === $request_data['_shipping_class'] ) {
				$product->set_shipping_class_id( 0 );
			} else {
				$shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $request_data['_shipping_class'] ) );
				$product->set_shipping_class_id( $shipping_class_id );
			}
		}

		if ( ! empty( $request_data['_visibility'] ) ) {
			$product->set_catalog_visibility( wc_clean( $request_data['_visibility'] ) );
		}

		if ( ! empty( $request_data['_featured'] ) ) {
			// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			$product->set_featured( wp_unslash( $request_data['_featured'] ) );
			// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		}

		if ( ! empty( $request_data['_sold_individually'] ) ) {
			if ( 'yes' === $request_data['_sold_individually'] ) {
				$product->set_sold_individually( 'yes' );
			} else {
				$product->set_sold_individually( '' );
			}
		}

		// Handle price - remove dates and set to lowest.
		$change_price_product_types    = apply_filters( 'woocommerce_bulk_edit_save_price_product_types', array( 'simple', 'external' ) );
		$can_product_type_change_price = false;
		foreach ( $change_price_product_types as $product_type ) {
			if ( $product->is_type( $product_type ) ) {
				$can_product_type_change_price = true;
				break;
			}
		}

		if ( $can_product_type_change_price ) {
			$regular_price_changed = $this->set_new_price( $product, 'regular' );
			$sale_price_changed    = $this->set_new_price( $product, 'sale' );

			if ( $regular_price_changed || $sale_price_changed ) {
				$product->set_date_on_sale_to( '' );
				$product->set_date_on_sale_from( '' );

				if ( $product->get_regular_price() < $product->get_sale_price() ) {
					$product->set_sale_price( '' );
				}
			}
		}

		// Handle Stock Data.
		$was_managing_stock = $product->get_manage_stock() ? 'yes' : 'no';
		$backorders         = $product->get_backorders();
		$backorders         = ! empty( $request_data['_backorders'] ) ? wc_clean( $request_data['_backorders'] ) : $backorders;

		if ( ! empty( $request_data['_manage_stock'] ) ) {
			$manage_stock = 'yes' === wc_clean( $request_data['_manage_stock'] ) && 'grouped' !== $product->get_type() ? 'yes' : 'no';
		} else {
			$manage_stock = $was_managing_stock;
		}

		$stock_amount = 'yes' === $manage_stock && ! empty( $request_data['change_stock'] ) && isset( $request_data['_stock'] ) ? wc_stock_amount( $request_data['_stock'] ) : $product->get_stock_quantity();

		$product->set_manage_stock( $manage_stock );

		if ( 'external' !== $product->get_type() ) {
			$product->set_backorders( $backorders );
		}

		if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
			$change_stock = absint( $request_data['change_stock'] );
			switch ( $change_stock ) {
				case 2:
					wc_update_product_stock( $product, $stock_amount, 'increase', true );
					break;
				case 3:
					wc_update_product_stock( $product, $stock_amount, 'decrease', true );
					break;
				default:
					wc_update_product_stock( $product, $stock_amount, 'set', true );
					break;
			}
		} else {
			// Reset values if WooCommerce Setting - Manage Stock status is disabled.
			$product->set_stock_quantity( '' );
			$product->set_manage_stock( 'no' );
		}

		$stock_status = empty( $request_data['_stock_status'] ) ? null : wc_clean( $request_data['_stock_status'] );
		$product      = $this->maybe_update_stock_status( $product, $stock_status );

		$product->save();

		do_action( 'woocommerce_product_bulk_edit_save', $product );

		// phpcs:enable WordPress.Security.ValidatedSanitizedInput.MissingUnslash
	}

	/**
	 * Disable the auto-save functionality for Orders.
	 */
	public function disable_autosave() {
		global $post;

		if ( $post && in_array( get_post_type( $post->ID ), wc_get_order_types( 'order-meta-boxes' ), true ) ) {
			wp_dequeue_script( 'autosave' );
		}
	}

	/**
	 * Output extra data on post forms.
	 *
	 * @param WP_Post $post Current post object.
	 */
	public function edit_form_top( $post ) {
		echo '<input type="hidden" id="original_post_title" name="original_post_title" value="' . esc_attr( $post->post_title ) . '" />';
	}

	/**
	 * Change title boxes in admin.
	 *
	 * @param string  $text Text to shown.
	 * @param WP_Post $post Current post object.
	 * @return string
	 */
	public function enter_title_here( $text, $post ) {
		switch ( $post->post_type ) {
			case 'product':
				$text = esc_html__( 'Product name', 'woocommerce' );
				break;
			case 'shop_coupon':
				$text = esc_html__( 'Coupon code', 'woocommerce' );
				break;
		}
		return $text;
	}

	/**
	 * Print coupon description textarea field.
	 *
	 * @param WP_Post $post Current post object.
	 */
	public function edit_form_after_title( $post ) {
		// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
		if ( 'shop_coupon' === $post->post_type ) {
			?>
			<textarea id="woocommerce-coupon-description" name="excerpt" cols="5" rows="2" placeholder="<?php esc_attr_e( 'Description (optional)', 'woocommerce' ); ?>"><?php echo $post->post_excerpt; ?></textarea>
			<?php
		}
		// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
	}

	/**
	 * Hidden default Meta-Boxes.
	 *
	 * @param  array  $hidden Hidden boxes.
	 * @param  object $screen Current screen.
	 * @return array
	 */
	public function hidden_meta_boxes( $hidden, $screen ) {
		if ( 'product' === $screen->post_type && 'post' === $screen->base ) {
			$hidden = array_merge( $hidden, array( 'postcustom' ) );
		}

		return $hidden;
	}

	/**
	 * Output product visibility options.
	 */
	public function product_data_visibility() {
		global $post, $thepostid, $product_object;

		if ( 'product' !== $post->post_type ) {
			return;
		}

		$thepostid          = $post->ID;
		$product_object     = $thepostid ? wc_get_product( $thepostid ) : new WC_Product();
		$current_visibility = $product_object->get_catalog_visibility();
		$current_featured   = wc_bool_to_string( $product_object->get_featured() );
		$visibility_options = wc_get_product_visibility_options();
		?>
		<div class="misc-pub-section" id="catalog-visibility">
			<?php esc_html_e( 'Catalog visibility:', 'woocommerce' ); ?>
			<strong id="catalog-visibility-display">
				<?php

				echo isset( $visibility_options[ $current_visibility ] ) ? esc_html( $visibility_options[ $current_visibility ] ) : esc_html( $current_visibility );

				if ( 'yes' === $current_featured ) {
					echo ', ' . esc_html__( 'Featured', 'woocommerce' );
				}
				?>
			</strong>

			<a href="#catalog-visibility" class="edit-catalog-visibility hide-if-no-js"><?php esc_html_e( 'Edit', 'woocommerce' ); ?></a>

			<div id="catalog-visibility-select" class="hide-if-js">

				<input type="hidden" name="current_visibility" id="current_visibility" value="<?php echo esc_attr( $current_visibility ); ?>" />
				<input type="hidden" name="current_featured" id="current_featured" value="<?php echo esc_attr( $current_featured ); ?>" />

				<?php
				echo '<p>' . esc_html__( 'This setting determines which shop pages products will be listed on.', 'woocommerce' ) . '</p>';

				foreach ( $visibility_options as $name => $label ) {
					echo '<input type="radio" name="_visibility" id="_visibility_' . esc_attr( $name ) . '" value="' . esc_attr( $name ) . '" ' . checked( $current_visibility, $name, false ) . ' data-label="' . esc_attr( $label ) . '" /> <label for="_visibility_' . esc_attr( $name ) . '" class="selectit">' . esc_html( $label ) . '</label><br />';
				}

				echo '<br /><input type="checkbox" name="_featured" id="_featured" ' . checked( $current_featured, 'yes', false ) . ' /> <label for="_featured">' . esc_html__( 'This is a featured product', 'woocommerce' ) . '</label><br />';
				?>
				<p>
					<a href="#catalog-visibility" class="save-post-visibility hide-if-no-js button"><?php esc_html_e( 'OK', 'woocommerce' ); ?></a>
					<a href="#catalog-visibility" class="cancel-post-visibility hide-if-no-js"><?php esc_html_e( 'Cancel', 'woocommerce' ); ?></a>
				</p>
			</div>
		</div>
		<?php
	}

	/**
	 * Change upload dir for downloadable files.
	 *
	 * @param array $pathdata Array of paths.
	 * @return array
	 */
	public function upload_dir( $pathdata ) {
		// phpcs:disable WordPress.Security.NonceVerification.Missing
		if ( isset( $_POST['type'] ) && 'downloadable_product' === $_POST['type'] ) {

			if ( empty( $pathdata['subdir'] ) ) {
				$pathdata['path']   = $pathdata['path'] . '/woocommerce_uploads';
				$pathdata['url']    = $pathdata['url'] . '/woocommerce_uploads';
				$pathdata['subdir'] = '/woocommerce_uploads';
			} else {
				$new_subdir = '/woocommerce_uploads' . $pathdata['subdir'];

				$pathdata['path']   = str_replace( $pathdata['subdir'], $new_subdir, $pathdata['path'] );
				$pathdata['url']    = str_replace( $pathdata['subdir'], $new_subdir, $pathdata['url'] );
				$pathdata['subdir'] = str_replace( $pathdata['subdir'], $new_subdir, $pathdata['subdir'] );
			}
		}
		return $pathdata;
		// phpcs:enable WordPress.Security.NonceVerification.Missing
	}

	/**
	 * Change filename for WooCommerce uploads and prepend unique chars for security.
	 *
	 * @param string $full_filename Original filename.
	 * @param string $ext           Extension of file.
	 * @param string $dir           Directory path.
	 *
	 * @return string New filename with unique hash.
	 * @since 4.0
	 */
	public function update_filename( $full_filename, $ext, $dir ) {
		// phpcs:disable WordPress.Security.NonceVerification.Missing
		if ( ! isset( $_POST['type'] ) || ! 'downloadable_product' === $_POST['type'] ) {
			return $full_filename;
		}

		if ( ! strpos( $dir, 'woocommerce_uploads' ) ) {
			return $full_filename;
		}

		if ( 'no' === get_option( 'woocommerce_downloads_add_hash_to_filename' ) ) {
			return $full_filename;
		}

		return $this->unique_filename( $full_filename, $ext );
		// phpcs:enable WordPress.Security.NonceVerification.Missing
	}

	/**
	 * Change filename to append random text.
	 *
	 * @param string $full_filename Original filename with extension.
	 * @param string $ext           Extension.
	 *
	 * @return string Modified filename.
	 */
	public function unique_filename( $full_filename, $ext ) {
		$ideal_random_char_length = 6;   // Not going with a larger length because then downloaded filename will not be pretty.
		$max_filename_length      = 255; // Max file name length for most file systems.
		$length_to_prepend        = min( $ideal_random_char_length, $max_filename_length - strlen( $full_filename ) - 1 );

		if ( 1 > $length_to_prepend ) {
			return $full_filename;
		}

		$suffix   = strtolower( wp_generate_password( $length_to_prepend, false, false ) );
		$filename = $full_filename;

		if ( strlen( $ext ) > 0 ) {
			$filename = substr( $filename, 0, strlen( $filename ) - strlen( $ext ) );
		}

		$full_filename = str_replace(
			$filename,
			"$filename-$suffix",
			$full_filename
		);

		return $full_filename;
	}

	/**
	 * Run a filter when uploading a downloadable product.
	 */
	public function woocommerce_media_upload_downloadable_product() {
		do_action( 'media_upload_file' );
	}

	/**
	 * Grant downloadable file access to any newly added files on any existing.
	 * orders for this product that have previously been granted downloadable file access.
	 *
	 * @param int   $product_id product identifier.
	 * @param int   $variation_id optional product variation identifier.
	 * @param array $downloadable_files newly set files.
	 * @deprecated 3.3.0 and moved to post-data class.
	 */
	public function process_product_file_download_paths( $product_id, $variation_id, $downloadable_files ) {
		wc_deprecated_function( 'WC_Admin_Post_Types::process_product_file_download_paths', '3.3', '' );
		WC_Post_Data::process_product_file_download_paths( $product_id, $variation_id, $downloadable_files );
	}

	/**
	 * When editing the shop page, we should hide templates.
	 *
	 * @param array   $page_templates Templates array.
	 * @param string  $theme Classname.
	 * @param WP_Post $post The current post object.
	 * @return array
	 */
	public function hide_cpt_archive_templates( $page_templates, $theme, $post ) {
		$shop_page_id = wc_get_page_id( 'shop' );

		if ( $post && absint( $post->ID ) === $shop_page_id ) {
			$page_templates = array();
		}

		return $page_templates;
	}

	/**
	 * Show a notice above the CPT archive.
	 *
	 * @param WP_Post $post The current post object.
	 */
	public function show_cpt_archive_notice( $post ) {
		$shop_page_id = wc_get_page_id( 'shop' );

		if ( $post && absint( $post->ID ) === $shop_page_id ) {
			echo '<div class="notice notice-info">';
			/* translators: %s: URL to read more about the shop page. */
			echo '<p>' . sprintf( wp_kses_post( __( 'This is the WooCommerce shop page. The shop page is a special archive that lists your products. <a href="%s">You can read more about this here</a>.', 'woocommerce' ) ), 'https://docs.woocommerce.com/document/woocommerce-pages/#section-4' ) . '</p>';
			echo '</div>';
		}
	}

	/**
	 * Add a post display state for special WC pages in the page list table.
	 *
	 * @param array   $post_states An array of post display states.
	 * @param WP_Post $post        The current post object.
	 */
	public function add_display_post_states( $post_states, $post ) {
		if ( wc_get_page_id( 'shop' ) === $post->ID ) {
			$post_states['wc_page_for_shop'] = __( 'Shop Page', 'woocommerce' );
		}

		if ( wc_get_page_id( 'cart' ) === $post->ID ) {
			$post_states['wc_page_for_cart'] = __( 'Cart Page', 'woocommerce' );
		}

		if ( wc_get_page_id( 'checkout' ) === $post->ID ) {
			$post_states['wc_page_for_checkout'] = __( 'Checkout Page', 'woocommerce' );
		}

		if ( wc_get_page_id( 'myaccount' ) === $post->ID ) {
			$post_states['wc_page_for_myaccount'] = __( 'My Account Page', 'woocommerce' );
		}

		if ( wc_get_page_id( 'terms' ) === $post->ID ) {
			$post_states['wc_page_for_terms'] = __( 'Terms and Conditions Page', 'woocommerce' );
		}

		return $post_states;
	}

	/**
	 * Apply product type constraints to stock status.
	 *
	 * @param WC_Product  $product The product whose stock status will be adjusted.
	 * @param string|null $stock_status The stock status to use for adjustment, or null if no new stock status has been supplied in the request.
	 * @return WC_Product The supplied product, or the synced product if it was a variable product.
	 */
	private function maybe_update_stock_status( $product, $stock_status ) {
		if ( $product->is_type( 'external' ) ) {
			// External products are always in stock.
			$product->set_stock_status( 'instock' );
		} elseif ( isset( $stock_status ) ) {
			if ( $product->is_type( 'variable' ) && ! $product->get_manage_stock() ) {
				// Stock status is determined by children.
				foreach ( $product->get_children() as $child_id ) {
					$child = wc_get_product( $child_id );
					if ( ! $product->get_manage_stock() ) {
						$child->set_stock_status( $stock_status );
						$child->save();
					}
				}
				$product = WC_Product_Variable::sync( $product, false );
			} else {
				$product->set_stock_status( $stock_status );
			}
		}

		return $product;
	}

	/**
	 * Set the new regular or sale price if requested.
	 *
	 * @param WC_Product $product The product to set the new price for.
	 * @param string     $price_type 'regular' or 'sale'.
	 * @return bool true if a new price has been set, false otherwise.
	 */
	private function set_new_price( $product, $price_type ) {
		// phpcs:disable WordPress.Security.NonceVerification.Recommended

		$request_data = $this->request_data();

		if ( empty( $request_data[ "change_{$price_type}_price" ] ) || ! isset( $request_data[ "_{$price_type}_price" ] ) ) {
			return false;
		}

		$old_price     = $product->{"get_{$price_type}_price"}();
		$price_changed = false;

		$change_price  = absint( $request_data[ "change_{$price_type}_price" ] );
		$raw_price     = wc_clean( wp_unslash( $request_data[ "_{$price_type}_price" ] ) );
		$is_percentage = (bool) strstr( $raw_price, '%' );
		$price         = wc_format_decimal( $raw_price );

		switch ( $change_price ) {
			case 1:
				$new_price = $price;
				break;
			case 2:
				if ( $is_percentage ) {
					$percent   = $price / 100;
					$new_price = $old_price + ( $old_price * $percent );
				} else {
					$new_price = $old_price + $price;
				}
				break;
			case 3:
				if ( $is_percentage ) {
					$percent   = $price / 100;
					$new_price = max( 0, $old_price - ( $old_price * $percent ) );
				} else {
					$new_price = max( 0, $old_price - $price );
				}
				break;
			case 4:
				if ( 'sale' !== $price_type ) {
					break;
				}
				$regular_price = $product->get_regular_price();
				if ( $is_percentage ) {
					$percent   = $price / 100;
					$new_price = max( 0, $regular_price - ( NumberUtil::round( $regular_price * $percent, wc_get_price_decimals() ) ) );
				} else {
					$new_price = max( 0, $regular_price - $price );
				}
				break;

			default:
				break;
		}

		if ( isset( $new_price ) && $new_price !== $old_price ) {
			$price_changed = true;
			$new_price     = NumberUtil::round( $new_price, wc_get_price_decimals() );
			$product->{"set_{$price_type}_price"}( $new_price );
		}

		return $price_changed;

		// phpcs:disable WordPress.Security.NonceVerification.Recommended
	}

	/**
	 * Get the current request data ($_REQUEST superglobal).
	 * This method is added to ease unit testing.
	 *
	 * @return array The $_REQUEST superglobal.
	 */
	protected function request_data() {
		return $_REQUEST;
	}
}

new WC_Admin_Post_Types();
admin/wc-admin-functions.php000064400000040540151336044770012067 0ustar00<?php
/**
 * WooCommerce Admin Functions
 *
 * @package  WooCommerce\Admin\Functions
 * @version  2.4.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Get all WooCommerce screen ids.
 *
 * @return array
 */
function wc_get_screen_ids() {

	$wc_screen_id = sanitize_title( __( 'WooCommerce', 'woocommerce' ) );
	$screen_ids   = array(
		'toplevel_page_' . $wc_screen_id,
		$wc_screen_id . '_page_wc-reports',
		$wc_screen_id . '_page_wc-shipping',
		$wc_screen_id . '_page_wc-settings',
		$wc_screen_id . '_page_wc-status',
		$wc_screen_id . '_page_wc-addons',
		'toplevel_page_wc-reports',
		'product_page_product_attributes',
		'product_page_product_exporter',
		'product_page_product_importer',
		'edit-product',
		'product',
		'edit-shop_coupon',
		'shop_coupon',
		'edit-product_cat',
		'edit-product_tag',
		'profile',
		'user-edit',
	);

	foreach ( wc_get_order_types() as $type ) {
		$screen_ids[] = $type;
		$screen_ids[] = 'edit-' . $type;
	}

	$attributes = wc_get_attribute_taxonomies();

	if ( $attributes ) {
		foreach ( $attributes as $attribute ) {
			$screen_ids[] = 'edit-' . wc_attribute_taxonomy_name( $attribute->attribute_name );
		}
	}

	return apply_filters( 'woocommerce_screen_ids', $screen_ids );
}

/**
 * Create a page and store the ID in an option.
 *
 * @param mixed  $slug Slug for the new page.
 * @param string $option Option name to store the page's ID.
 * @param string $page_title (default: '') Title for the new page.
 * @param string $page_content (default: '') Content for the new page.
 * @param int    $post_parent (default: 0) Parent for the new page.
 * @param string $post_status (default: publish) The post status of the new page.
 * @return int page ID.
 */
function wc_create_page( $slug, $option = '', $page_title = '', $page_content = '', $post_parent = 0, $post_status = 'publish' ) {
	global $wpdb;

	$option_value = get_option( $option );

	if ( $option_value > 0 ) {
		$page_object = get_post( $option_value );

		if ( $page_object && 'page' === $page_object->post_type && ! in_array( $page_object->post_status, array( 'pending', 'trash', 'future', 'auto-draft' ), true ) ) {
			// Valid page is already in place.
			return $page_object->ID;
		}
	}

	if ( strlen( $page_content ) > 0 ) {
		// Search for an existing page with the specified page content (typically a shortcode).
		$shortcode = str_replace( array( '<!-- wp:shortcode -->', '<!-- /wp:shortcode -->' ), '', $page_content );
		$valid_page_found = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type='page' AND post_status NOT IN ( 'pending', 'trash', 'future', 'auto-draft' ) AND post_content LIKE %s LIMIT 1;", "%{$shortcode}%" ) );
	} else {
		// Search for an existing page with the specified page slug.
		$valid_page_found = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type='page' AND post_status NOT IN ( 'pending', 'trash', 'future', 'auto-draft' )  AND post_name = %s LIMIT 1;", $slug ) );
	}

	$valid_page_found = apply_filters( 'woocommerce_create_page_id', $valid_page_found, $slug, $page_content );

	if ( $valid_page_found ) {
		if ( $option ) {
			update_option( $option, $valid_page_found );
		}
		return $valid_page_found;
	}

	// Search for a matching valid trashed page.
	if ( strlen( $page_content ) > 0 ) {
		// Search for an existing page with the specified page content (typically a shortcode).
		$trashed_page_found = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type='page' AND post_status = 'trash' AND post_content LIKE %s LIMIT 1;", "%{$page_content}%" ) );
	} else {
		// Search for an existing page with the specified page slug.
		$trashed_page_found = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type='page' AND post_status = 'trash' AND post_name = %s LIMIT 1;", $slug ) );
	}

	if ( $trashed_page_found ) {
		$page_id   = $trashed_page_found;
		$page_data = array(
			'ID'          => $page_id,
			'post_status' => $post_status,
		);
		wp_update_post( $page_data );
	} else {
		$page_data = array(
			'post_status'    => $post_status,
			'post_type'      => 'page',
			'post_author'    => 1,
			'post_name'      => $slug,
			'post_title'     => $page_title,
			'post_content'   => $page_content,
			'post_parent'    => $post_parent,
			'comment_status' => 'closed',
		);
		$page_id   = wp_insert_post( $page_data );

		do_action( 'woocommerce_page_created', $page_id, $page_data );
	}

	if ( $option ) {
		update_option( $option, $page_id );
	}

	return $page_id;
}

/**
 * Output admin fields.
 *
 * Loops through the woocommerce options array and outputs each field.
 *
 * @param array $options Opens array to output.
 */
function woocommerce_admin_fields( $options ) {

	if ( ! class_exists( 'WC_Admin_Settings', false ) ) {
		include dirname( __FILE__ ) . '/class-wc-admin-settings.php';
	}

	WC_Admin_Settings::output_fields( $options );
}

/**
 * Update all settings which are passed.
 *
 * @param array $options Option fields to save.
 * @param array $data Passed data.
 */
function woocommerce_update_options( $options, $data = null ) {

	if ( ! class_exists( 'WC_Admin_Settings', false ) ) {
		include dirname( __FILE__ ) . '/class-wc-admin-settings.php';
	}

	WC_Admin_Settings::save_fields( $options, $data );
}

/**
 * Get a setting from the settings API.
 *
 * @param mixed $option_name Option name to save.
 * @param mixed $default Default value to save.
 * @return string
 */
function woocommerce_settings_get_option( $option_name, $default = '' ) {

	if ( ! class_exists( 'WC_Admin_Settings', false ) ) {
		include dirname( __FILE__ ) . '/class-wc-admin-settings.php';
	}

	return WC_Admin_Settings::get_option( $option_name, $default );
}

/**
 * Sees if line item stock has already reduced stock, and whether those values need adjusting e.g. after changing item qty.
 *
 * @since 3.6.0
 * @param WC_Order_Item $item Item object.
 * @param integer       $item_quantity Optional quantity to check against. Read from object if not passed.
 * @return boolean|array|WP_Error Array of changes or error object when stock is updated (@see wc_update_product_stock). False if nothing changes.
 */
function wc_maybe_adjust_line_item_product_stock( $item, $item_quantity = -1 ) {
	if ( 'line_item' !== $item->get_type() ) {
		return false;
	}

	/**
	 * Prevent adjust line item product stock.
	 *
	 * @since 3.7.1
	 * @param bool $prevent If should prevent.
	 * @param WC_Order_Item $item Item object.
	 * @param int           $item_quantity Optional quantity to check against.
	 */
	if ( apply_filters( 'woocommerce_prevent_adjust_line_item_product_stock', false, $item, $item_quantity ) ) {
		return false;
	}

	$product = $item->get_product();

	if ( ! $product || ! $product->managing_stock() ) {
		return false;
	}

	$item_quantity          = wc_stock_amount( $item_quantity >= 0 ? $item_quantity : $item->get_quantity() );
	$already_reduced_stock  = wc_stock_amount( $item->get_meta( '_reduced_stock', true ) );
	$restock_refunded_items = wc_stock_amount( $item->get_meta( '_restock_refunded_items', true ) );
	$order                  = $item->get_order();
	$refunded_item_quantity = $order->get_qty_refunded_for_item( $item->get_id() );

	$diff = $item_quantity - $restock_refunded_items - $already_reduced_stock;

	/*
	 * 0 as $item_quantity usually indicates we're deleting the order item.
	 * Let's restore back the reduced count.
	 */
	if ( 0 === $item_quantity ) {
		$diff = $already_reduced_stock * -1;
	}

	if ( $diff < 0 ) {
		$new_stock = wc_update_product_stock( $product, $diff * -1, 'increase' );
	} elseif ( $diff > 0 ) {
		$new_stock = wc_update_product_stock( $product, $diff, 'decrease' );
	} else {
		return false;
	}

	if ( is_wp_error( $new_stock ) ) {
		return $new_stock;
	}

	$item->update_meta_data( '_reduced_stock', $item_quantity - $restock_refunded_items );
	$item->save();

	if ( $item_quantity > 0 ) {
		// If stock was reduced, then we need to mark this on parent order object as well so that cancel logic works properly.
		$order_data_store = WC_Data_Store::load( 'order' );
		if ( $item->get_order_id() && ! $order_data_store->get_stock_reduced( $item->get_order_id() ) ) {
			$order_data_store->set_stock_reduced( $item->get_order_id(), true );
		}
	}

	return array(
		'from' => $new_stock + $diff,
		'to'   => $new_stock,
	);
}

/**
 * Save order items. Uses the CRUD.
 *
 * @since 2.2
 * @param int   $order_id Order ID.
 * @param array $items Order items to save.
 */
function wc_save_order_items( $order_id, $items ) {
	// Allow other plugins to check change in order items before they are saved.
	do_action( 'woocommerce_before_save_order_items', $order_id, $items );

	$qty_change_order_notes = array();
	$order                  = wc_get_order( $order_id );

	// Line items and fees.
	if ( isset( $items['order_item_id'] ) ) {
		$data_keys = array(
			'line_tax'             => array(),
			'line_subtotal_tax'    => array(),
			'order_item_name'      => null,
			'order_item_qty'       => null,
			'order_item_tax_class' => null,
			'line_total'           => null,
			'line_subtotal'        => null,
		);
		foreach ( $items['order_item_id'] as $item_id ) {
			$item = WC_Order_Factory::get_order_item( absint( $item_id ) );

			if ( ! $item ) {
				continue;
			}

			$item_data = array();

			foreach ( $data_keys as $key => $default ) {
				$item_data[ $key ] = isset( $items[ $key ][ $item_id ] ) ? wc_check_invalid_utf8( wp_unslash( $items[ $key ][ $item_id ] ) ) : $default;
			}

			if ( '0' === $item_data['order_item_qty'] ) {
				$changed_stock = wc_maybe_adjust_line_item_product_stock( $item, 0 );
				if ( $changed_stock && ! is_wp_error( $changed_stock ) ) {
					$qty_change_order_notes[] = $item->get_name() . ' &ndash; ' . $changed_stock['from'] . '&rarr;' . $changed_stock['to'];
				}
				$item->delete();
				continue;
			}

			$item->set_props(
				array(
					'name'      => $item_data['order_item_name'],
					'quantity'  => $item_data['order_item_qty'],
					'tax_class' => $item_data['order_item_tax_class'],
					'total'     => $item_data['line_total'],
					'subtotal'  => $item_data['line_subtotal'],
					'taxes'     => array(
						'total'    => $item_data['line_tax'],
						'subtotal' => $item_data['line_subtotal_tax'],
					),
				)
			);

			if ( 'fee' === $item->get_type() ) {
				$item->set_amount( $item_data['line_total'] );
			}

			if ( isset( $items['meta_key'][ $item_id ], $items['meta_value'][ $item_id ] ) ) {
				foreach ( $items['meta_key'][ $item_id ] as $meta_id => $meta_key ) {
					$meta_key   = substr( wp_unslash( $meta_key ), 0, 255 );
					$meta_value = isset( $items['meta_value'][ $item_id ][ $meta_id ] ) ? wp_unslash( $items['meta_value'][ $item_id ][ $meta_id ] ) : '';

					if ( '' === $meta_key && '' === $meta_value ) {
						if ( ! strstr( $meta_id, 'new-' ) ) {
							$item->delete_meta_data_by_mid( $meta_id );
						}
					} elseif ( strstr( $meta_id, 'new-' ) ) {
						$item->add_meta_data( $meta_key, $meta_value, false );
					} else {
						$item->update_meta_data( $meta_key, $meta_value, $meta_id );
					}
				}
			}

			// Allow other plugins to change item object before it is saved.
			do_action( 'woocommerce_before_save_order_item', $item );

			$item->save();

			if ( in_array( $order->get_status(), array( 'processing', 'completed', 'on-hold' ) ) ) {
				$changed_stock = wc_maybe_adjust_line_item_product_stock( $item );
				if ( $changed_stock && ! is_wp_error( $changed_stock ) ) {
					$qty_change_order_notes[] = $item->get_name() . ' (' . $changed_stock['from'] . '&rarr;' . $changed_stock['to'] . ')';
				}
			}
		}
	}

	// Shipping Rows.
	if ( isset( $items['shipping_method_id'] ) ) {
		$data_keys = array(
			'shipping_method'       => null,
			'shipping_method_title' => null,
			'shipping_cost'         => 0,
			'shipping_taxes'        => array(),
		);

		foreach ( $items['shipping_method_id'] as $item_id ) {
			$item = WC_Order_Factory::get_order_item( absint( $item_id ) );

			if ( ! $item ) {
				continue;
			}

			$item_data = array();

			foreach ( $data_keys as $key => $default ) {
				$item_data[ $key ] = isset( $items[ $key ][ $item_id ] ) ? wc_clean( wp_unslash( $items[ $key ][ $item_id ] ) ) : $default;
			}

			$item->set_props(
				array(
					'method_id'    => $item_data['shipping_method'],
					'method_title' => $item_data['shipping_method_title'],
					'total'        => $item_data['shipping_cost'],
					'taxes'        => array(
						'total' => $item_data['shipping_taxes'],
					),
				)
			);

			if ( isset( $items['meta_key'][ $item_id ], $items['meta_value'][ $item_id ] ) ) {
				foreach ( $items['meta_key'][ $item_id ] as $meta_id => $meta_key ) {
					$meta_value = isset( $items['meta_value'][ $item_id ][ $meta_id ] ) ? wp_unslash( $items['meta_value'][ $item_id ][ $meta_id ] ) : '';

					if ( '' === $meta_key && '' === $meta_value ) {
						if ( ! strstr( $meta_id, 'new-' ) ) {
							$item->delete_meta_data_by_mid( $meta_id );
						}
					} elseif ( strstr( $meta_id, 'new-' ) ) {
						$item->add_meta_data( $meta_key, $meta_value, false );
					} else {
						$item->update_meta_data( $meta_key, $meta_value, $meta_id );
					}
				}
			}

			$item->save();
		}
	}

	$order = wc_get_order( $order_id );

	if ( ! empty( $qty_change_order_notes ) ) {
		/* translators: %s item name. */
		$order->add_order_note( sprintf( __( 'Adjusted stock: %s', 'woocommerce' ), implode( ', ', $qty_change_order_notes ) ), false, true );
	}

	$order->update_taxes();
	$order->calculate_totals( false );

	// Inform other plugins that the items have been saved.
	do_action( 'woocommerce_saved_order_items', $order_id, $items );
}

/**
 * Get HTML for some action buttons. Used in list tables.
 *
 * @since 3.3.0
 * @param array $actions Actions to output.
 * @return string
 */
function wc_render_action_buttons( $actions ) {
	$actions_html = '';

	foreach ( $actions as $action ) {
		if ( isset( $action['group'] ) ) {
			$actions_html .= '<div class="wc-action-button-group"><label>' . $action['group'] . '</label> <span class="wc-action-button-group__items">' . wc_render_action_buttons( $action['actions'] ) . '</span></div>';
		} elseif ( isset( $action['action'], $action['url'], $action['name'] ) ) {
			$actions_html .= sprintf( '<a class="button wc-action-button wc-action-button-%1$s %1$s" href="%2$s" aria-label="%3$s" title="%3$s">%4$s</a>', esc_attr( $action['action'] ), esc_url( $action['url'] ), esc_attr( isset( $action['title'] ) ? $action['title'] : $action['name'] ), esc_html( $action['name'] ) );
		}
	}

	return $actions_html;
}

/**
 * Shows a notice if variations are missing prices.
 *
 * @since 3.6.0
 * @param WC_Product $product_object Product object.
 */
function wc_render_invalid_variation_notice( $product_object ) {
	global $wpdb;

	// Give ability for extensions to hide this notice.
	if ( ! apply_filters( 'woocommerce_show_invalid_variations_notice', true, $product_object ) ) {
		return;
	}

	$variation_ids = $product_object ? $product_object->get_children() : array();

	if ( empty( $variation_ids ) ) {
		return;
	}

	$variation_count = count( $variation_ids );

	// Check if a variation exists without pricing data.
	// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
	$invalid_variation_count = $wpdb->get_var(
		"
		SELECT count(post_id) FROM {$wpdb->postmeta}
		WHERE post_id in (" . implode( ',', array_map( 'absint', $variation_ids ) ) . ")
		AND meta_key='_price'
		AND meta_value >= 0
		AND meta_value != ''
		"
	);
	// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared

	if ( 0 < ( $variation_count - $invalid_variation_count ) ) {
		?>
		<div id="message" class="inline notice woocommerce-message woocommerce-notice-invalid-variation">
			<p>
			<?php
			echo wp_kses_post(
				sprintf(
					/* Translators: %d variation count. */
					_n( '%d variation does not have a price.', '%d variations do not have prices.', ( $variation_count - $invalid_variation_count ), 'woocommerce' ),
					( $variation_count - $invalid_variation_count )
				) . '&nbsp;' .
				__( 'Variations (and their attributes) that do not have prices will not be shown in your store.', 'woocommerce' )
			);
			?>
			</p>
		</div>
		<?php
	}
}

/**
 * Get current admin page URL.
 *
 * Returns an empty string if it cannot generate a URL.
 *
 * @internal
 * @since 4.4.0
 * @return string
 */
function wc_get_current_admin_url() {
	$uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
	$uri = preg_replace( '|^.*/wp-admin/|i', '', $uri );

	if ( ! $uri ) {
		return '';
	}

	return remove_query_arg( array( '_wpnonce', '_wc_notice_nonce', 'wc_db_update', 'wc_db_update_nonce', 'wc-hide-notice' ), admin_url( $uri ) );
}
admin/class-wc-admin-settings.php000064400000103074151336044770013024 0ustar00<?php
/**
 * WooCommerce Admin Settings Class
 *
 * @package  WooCommerce\Admin
 * @version  3.4.0
 */

use Automattic\Jetpack\Constants;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'WC_Admin_Settings', false ) ) :

	/**
	 * WC_Admin_Settings Class.
	 */
	class WC_Admin_Settings {

		/**
		 * Setting pages.
		 *
		 * @var array
		 */
		private static $settings = array();

		/**
		 * Error messages.
		 *
		 * @var array
		 */
		private static $errors = array();

		/**
		 * Update messages.
		 *
		 * @var array
		 */
		private static $messages = array();

		/**
		 * Include the settings page classes.
		 */
		public static function get_settings_pages() {
			if ( empty( self::$settings ) ) {
				$settings = array();

				include_once dirname( __FILE__ ) . '/settings/class-wc-settings-page.php';

				$settings[] = include __DIR__ . '/settings/class-wc-settings-general.php';
				$settings[] = include __DIR__ . '/settings/class-wc-settings-products.php';
				$settings[] = include __DIR__ . '/settings/class-wc-settings-tax.php';
				$settings[] = include __DIR__ . '/settings/class-wc-settings-shipping.php';
				$settings[] = include __DIR__ . '/settings/class-wc-settings-payment-gateways.php';
				$settings[] = include __DIR__ . '/settings/class-wc-settings-accounts.php';
				$settings[] = include __DIR__ . '/settings/class-wc-settings-emails.php';
				$settings[] = include __DIR__ . '/settings/class-wc-settings-integrations.php';
				$settings[] = include __DIR__ . '/settings/class-wc-settings-advanced.php';

				self::$settings = apply_filters( 'woocommerce_get_settings_pages', $settings );
			}

			return self::$settings;
		}

		/**
		 * Save the settings.
		 */
		public static function save() {
			global $current_tab;

			check_admin_referer( 'woocommerce-settings' );

			// Trigger actions.
			do_action( 'woocommerce_settings_save_' . $current_tab );
			do_action( 'woocommerce_update_options_' . $current_tab );
			do_action( 'woocommerce_update_options' );

			self::add_message( __( 'Your settings have been saved.', 'woocommerce' ) );
			self::check_download_folder_protection();

			// Clear any unwanted data and flush rules.
			update_option( 'woocommerce_queue_flush_rewrite_rules', 'yes' );
			WC()->query->init_query_vars();
			WC()->query->add_endpoints();

			do_action( 'woocommerce_settings_saved' );
		}

		/**
		 * Add a message.
		 *
		 * @param string $text Message.
		 */
		public static function add_message( $text ) {
			self::$messages[] = $text;
		}

		/**
		 * Add an error.
		 *
		 * @param string $text Message.
		 */
		public static function add_error( $text ) {
			self::$errors[] = $text;
		}

		/**
		 * Output messages + errors.
		 */
		public static function show_messages() {
			if ( count( self::$errors ) > 0 ) {
				foreach ( self::$errors as $error ) {
					echo '<div id="message" class="error inline"><p><strong>' . esc_html( $error ) . '</strong></p></div>';
				}
			} elseif ( count( self::$messages ) > 0 ) {
				foreach ( self::$messages as $message ) {
					echo '<div id="message" class="updated inline"><p><strong>' . esc_html( $message ) . '</strong></p></div>';
				}
			}
		}

		/**
		 * Settings page.
		 *
		 * Handles the display of the main woocommerce settings page in admin.
		 */
		public static function output() {
			global $current_section, $current_tab;

			$suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min';

			do_action( 'woocommerce_settings_start' );

			wp_enqueue_script( 'woocommerce_settings', WC()->plugin_url() . '/assets/js/admin/settings' . $suffix . '.js', array( 'jquery', 'wp-util', 'jquery-ui-datepicker', 'jquery-ui-sortable', 'iris', 'selectWoo' ), WC()->version, true );

			wp_localize_script(
				'woocommerce_settings',
				'woocommerce_settings_params',
				array(
					'i18n_nav_warning'                    => __( 'The changes you made will be lost if you navigate away from this page.', 'woocommerce' ),
					'i18n_moved_up'                       => __( 'Item moved up', 'woocommerce' ),
					'i18n_moved_down'                     => __( 'Item moved down', 'woocommerce' ),
					'i18n_no_specific_countries_selected' => __( 'Selecting no country / region to sell to prevents from completing the checkout. Continue anyway?', 'woocommerce' ),
				)
			);

			// Get tabs for the settings page.
			$tabs = apply_filters( 'woocommerce_settings_tabs_array', array() );

			include dirname( __FILE__ ) . '/views/html-admin-settings.php';
		}

		/**
		 * Get a setting from the settings API.
		 *
		 * @param string $option_name Option name.
		 * @param mixed  $default     Default value.
		 * @return mixed
		 */
		public static function get_option( $option_name, $default = '' ) {
			if ( ! $option_name ) {
				return $default;
			}

			// Array value.
			if ( strstr( $option_name, '[' ) ) {

				parse_str( $option_name, $option_array );

				// Option name is first key.
				$option_name = current( array_keys( $option_array ) );

				// Get value.
				$option_values = get_option( $option_name, '' );

				$key = key( $option_array[ $option_name ] );

				if ( isset( $option_values[ $key ] ) ) {
					$option_value = $option_values[ $key ];
				} else {
					$option_value = null;
				}
			} else {
				// Single value.
				$option_value = get_option( $option_name, null );
			}

			if ( is_array( $option_value ) ) {
				$option_value = wp_unslash( $option_value );
			} elseif ( ! is_null( $option_value ) ) {
				$option_value = stripslashes( $option_value );
			}

			return ( null === $option_value ) ? $default : $option_value;
		}

		/**
		 * Output admin fields.
		 *
		 * Loops through the woocommerce options array and outputs each field.
		 *
		 * @param array[] $options Opens array to output.
		 */
		public static function output_fields( $options ) {
			foreach ( $options as $value ) {
				if ( ! isset( $value['type'] ) ) {
					continue;
				}
				if ( ! isset( $value['id'] ) ) {
					$value['id'] = '';
				}
				if ( ! isset( $value['title'] ) ) {
					$value['title'] = isset( $value['name'] ) ? $value['name'] : '';
				}
				if ( ! isset( $value['class'] ) ) {
					$value['class'] = '';
				}
				if ( ! isset( $value['css'] ) ) {
					$value['css'] = '';
				}
				if ( ! isset( $value['default'] ) ) {
					$value['default'] = '';
				}
				if ( ! isset( $value['desc'] ) ) {
					$value['desc'] = '';
				}
				if ( ! isset( $value['desc_tip'] ) ) {
					$value['desc_tip'] = false;
				}
				if ( ! isset( $value['placeholder'] ) ) {
					$value['placeholder'] = '';
				}
				if ( ! isset( $value['suffix'] ) ) {
					$value['suffix'] = '';
				}
				if ( ! isset( $value['value'] ) ) {
					$value['value'] = self::get_option( $value['id'], $value['default'] );
				}

				// Custom attribute handling.
				$custom_attributes = array();

				if ( ! empty( $value['custom_attributes'] ) && is_array( $value['custom_attributes'] ) ) {
					foreach ( $value['custom_attributes'] as $attribute => $attribute_value ) {
						$custom_attributes[] = esc_attr( $attribute ) . '="' . esc_attr( $attribute_value ) . '"';
					}
				}

				// Description handling.
				$field_description = self::get_field_description( $value );
				$description       = $field_description['description'];
				$tooltip_html      = $field_description['tooltip_html'];

				// Switch based on type.
				switch ( $value['type'] ) {

					// Section Titles.
					case 'title':
						if ( ! empty( $value['title'] ) ) {
							echo '<h2>' . esc_html( $value['title'] ) . '</h2>';
						}
						if ( ! empty( $value['desc'] ) ) {
							echo '<div id="' . esc_attr( sanitize_title( $value['id'] ) ) . '-description">';
							echo wp_kses_post( wpautop( wptexturize( $value['desc'] ) ) );
							echo '</div>';
						}
						echo '<table class="form-table">' . "\n\n";
						if ( ! empty( $value['id'] ) ) {
							do_action( 'woocommerce_settings_' . sanitize_title( $value['id'] ) );
						}
						break;

					// Section Ends.
					case 'sectionend':
						if ( ! empty( $value['id'] ) ) {
							do_action( 'woocommerce_settings_' . sanitize_title( $value['id'] ) . '_end' );
						}
						echo '</table>';
						if ( ! empty( $value['id'] ) ) {
							do_action( 'woocommerce_settings_' . sanitize_title( $value['id'] ) . '_after' );
						}
						break;

					// Standard text inputs and subtypes like 'number'.
					case 'text':
					case 'password':
					case 'datetime':
					case 'datetime-local':
					case 'date':
					case 'month':
					case 'time':
					case 'week':
					case 'number':
					case 'email':
					case 'url':
					case 'tel':
						$option_value = $value['value'];

						?><tr valign="top">
							<th scope="row" class="titledesc">
								<label for="<?php echo esc_attr( $value['id'] ); ?>"><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html; // WPCS: XSS ok. ?></label>
							</th>
							<td class="forminp forminp-<?php echo esc_attr( sanitize_title( $value['type'] ) ); ?>">
								<input
									name="<?php echo esc_attr( $value['id'] ); ?>"
									id="<?php echo esc_attr( $value['id'] ); ?>"
									type="<?php echo esc_attr( $value['type'] ); ?>"
									style="<?php echo esc_attr( $value['css'] ); ?>"
									value="<?php echo esc_attr( $option_value ); ?>"
									class="<?php echo esc_attr( $value['class'] ); ?>"
									placeholder="<?php echo esc_attr( $value['placeholder'] ); ?>"
									<?php echo implode( ' ', $custom_attributes ); // WPCS: XSS ok. ?>
									/><?php echo esc_html( $value['suffix'] ); ?> <?php echo $description; // WPCS: XSS ok. ?>
							</td>
						</tr>
						<?php
						break;

					// Color picker.
					case 'color':
						$option_value = $value['value'];

						?>
						<tr valign="top">
							<th scope="row" class="titledesc">
								<label for="<?php echo esc_attr( $value['id'] ); ?>"><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html; // WPCS: XSS ok. ?></label>
							</th>
							<td class="forminp forminp-<?php echo esc_attr( sanitize_title( $value['type'] ) ); ?>">&lrm;
								<span class="colorpickpreview" style="background: <?php echo esc_attr( $option_value ); ?>">&nbsp;</span>
								<input
									name="<?php echo esc_attr( $value['id'] ); ?>"
									id="<?php echo esc_attr( $value['id'] ); ?>"
									type="text"
									dir="ltr"
									style="<?php echo esc_attr( $value['css'] ); ?>"
									value="<?php echo esc_attr( $option_value ); ?>"
									class="<?php echo esc_attr( $value['class'] ); ?>colorpick"
									placeholder="<?php echo esc_attr( $value['placeholder'] ); ?>"
									<?php echo implode( ' ', $custom_attributes ); // WPCS: XSS ok. ?>
									/>&lrm; <?php echo $description; // WPCS: XSS ok. ?>
									<div id="colorPickerDiv_<?php echo esc_attr( $value['id'] ); ?>" class="colorpickdiv" style="z-index: 100;background:#eee;border:1px solid #ccc;position:absolute;display:none;"></div>
							</td>
						</tr>
						<?php
						break;

					// Textarea.
					case 'textarea':
						$option_value = $value['value'];

						?>
						<tr valign="top">
							<th scope="row" class="titledesc">
								<label for="<?php echo esc_attr( $value['id'] ); ?>"><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html; // WPCS: XSS ok. ?></label>
							</th>
							<td class="forminp forminp-<?php echo esc_attr( sanitize_title( $value['type'] ) ); ?>">
								<?php echo $description; // WPCS: XSS ok. ?>

								<textarea
									name="<?php echo esc_attr( $value['id'] ); ?>"
									id="<?php echo esc_attr( $value['id'] ); ?>"
									style="<?php echo esc_attr( $value['css'] ); ?>"
									class="<?php echo esc_attr( $value['class'] ); ?>"
									placeholder="<?php echo esc_attr( $value['placeholder'] ); ?>"
									<?php echo implode( ' ', $custom_attributes ); // WPCS: XSS ok. ?>
									><?php echo esc_textarea( $option_value ); // WPCS: XSS ok. ?></textarea>
							</td>
						</tr>
						<?php
						break;

					// Select boxes.
					case 'select':
					case 'multiselect':
						$option_value = $value['value'];

						?>
						<tr valign="top">
							<th scope="row" class="titledesc">
								<label for="<?php echo esc_attr( $value['id'] ); ?>"><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html; // WPCS: XSS ok. ?></label>
							</th>
							<td class="forminp forminp-<?php echo esc_attr( sanitize_title( $value['type'] ) ); ?>">
								<select
									name="<?php echo esc_attr( $value['id'] ); ?><?php echo ( 'multiselect' === $value['type'] ) ? '[]' : ''; ?>"
									id="<?php echo esc_attr( $value['id'] ); ?>"
									style="<?php echo esc_attr( $value['css'] ); ?>"
									class="<?php echo esc_attr( $value['class'] ); ?>"
									<?php echo implode( ' ', $custom_attributes ); // WPCS: XSS ok. ?>
									<?php echo 'multiselect' === $value['type'] ? 'multiple="multiple"' : ''; ?>
									>
									<?php
									foreach ( $value['options'] as $key => $val ) {
										?>
										<option value="<?php echo esc_attr( $key ); ?>"
											<?php

											if ( is_array( $option_value ) ) {
												selected( in_array( (string) $key, $option_value, true ), true );
											} else {
												selected( $option_value, (string) $key );
											}

											?>
										><?php echo esc_html( $val ); ?></option>
										<?php
									}
									?>
								</select> <?php echo $description; // WPCS: XSS ok. ?>
							</td>
						</tr>
						<?php
						break;

					// Radio inputs.
					case 'radio':
						$option_value = $value['value'];

						?>
						<tr valign="top">
							<th scope="row" class="titledesc">
								<label for="<?php echo esc_attr( $value['id'] ); ?>"><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html; // WPCS: XSS ok. ?></label>
							</th>
							<td class="forminp forminp-<?php echo esc_attr( sanitize_title( $value['type'] ) ); ?>">
								<fieldset>
									<?php echo $description; // WPCS: XSS ok. ?>
									<ul>
									<?php
									foreach ( $value['options'] as $key => $val ) {
										?>
										<li>
											<label><input
												name="<?php echo esc_attr( $value['id'] ); ?>"
												value="<?php echo esc_attr( $key ); ?>"
												type="radio"
												style="<?php echo esc_attr( $value['css'] ); ?>"
												class="<?php echo esc_attr( $value['class'] ); ?>"
												<?php echo implode( ' ', $custom_attributes ); // WPCS: XSS ok. ?>
												<?php checked( $key, $option_value ); ?>
												/> <?php echo esc_html( $val ); ?></label>
										</li>
										<?php
									}
									?>
									</ul>
								</fieldset>
							</td>
						</tr>
						<?php
						break;

					// Checkbox input.
					case 'checkbox':
						$option_value     = $value['value'];
						$visibility_class = array();

						if ( ! isset( $value['hide_if_checked'] ) ) {
							$value['hide_if_checked'] = false;
						}
						if ( ! isset( $value['show_if_checked'] ) ) {
							$value['show_if_checked'] = false;
						}
						if ( 'yes' === $value['hide_if_checked'] || 'yes' === $value['show_if_checked'] ) {
							$visibility_class[] = 'hidden_option';
						}
						if ( 'option' === $value['hide_if_checked'] ) {
							$visibility_class[] = 'hide_options_if_checked';
						}
						if ( 'option' === $value['show_if_checked'] ) {
							$visibility_class[] = 'show_options_if_checked';
						}

						if ( ! isset( $value['checkboxgroup'] ) || 'start' === $value['checkboxgroup'] ) {
							?>
								<tr valign="top" class="<?php echo esc_attr( implode( ' ', $visibility_class ) ); ?>">
									<th scope="row" class="titledesc"><?php echo esc_html( $value['title'] ); ?></th>
									<td class="forminp forminp-checkbox">
										<fieldset>
							<?php
						} else {
							?>
								<fieldset class="<?php echo esc_attr( implode( ' ', $visibility_class ) ); ?>">
							<?php
						}

						if ( ! empty( $value['title'] ) ) {
							?>
								<legend class="screen-reader-text"><span><?php echo esc_html( $value['title'] ); ?></span></legend>
							<?php
						}

						?>
							<label for="<?php echo esc_attr( $value['id'] ); ?>">
								<input
									name="<?php echo esc_attr( $value['id'] ); ?>"
									id="<?php echo esc_attr( $value['id'] ); ?>"
									type="checkbox"
									class="<?php echo esc_attr( isset( $value['class'] ) ? $value['class'] : '' ); ?>"
									value="1"
									<?php checked( $option_value, 'yes' ); ?>
									<?php echo implode( ' ', $custom_attributes ); // WPCS: XSS ok. ?>
								/> <?php echo $description; // WPCS: XSS ok. ?>
							</label> <?php echo $tooltip_html; // WPCS: XSS ok. ?>
						<?php

						if ( ! isset( $value['checkboxgroup'] ) || 'end' === $value['checkboxgroup'] ) {
							?>
										</fieldset>
									</td>
								</tr>
							<?php
						} else {
							?>
								</fieldset>
							<?php
						}
						break;

					// Image width settings. @todo deprecate and remove in 4.0. No longer needed by core.
					case 'image_width':
						$image_size       = str_replace( '_image_size', '', $value['id'] );
						$size             = wc_get_image_size( $image_size );
						$width            = isset( $size['width'] ) ? $size['width'] : $value['default']['width'];
						$height           = isset( $size['height'] ) ? $size['height'] : $value['default']['height'];
						$crop             = isset( $size['crop'] ) ? $size['crop'] : $value['default']['crop'];
						$disabled_attr    = '';
						$disabled_message = '';

						if ( has_filter( 'woocommerce_get_image_size_' . $image_size ) ) {
							$disabled_attr    = 'disabled="disabled"';
							$disabled_message = '<p><small>' . esc_html__( 'The settings of this image size have been disabled because its values are being overwritten by a filter.', 'woocommerce' ) . '</small></p>';
						}

						?>
						<tr valign="top">
							<th scope="row" class="titledesc">
							<label><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html . $disabled_message; // WPCS: XSS ok. ?></label>
						</th>
							<td class="forminp image_width_settings">

								<input name="<?php echo esc_attr( $value['id'] ); ?>[width]" <?php echo $disabled_attr; // WPCS: XSS ok. ?> id="<?php echo esc_attr( $value['id'] ); ?>-width" type="text" size="3" value="<?php echo esc_attr( $width ); ?>" /> &times; <input name="<?php echo esc_attr( $value['id'] ); ?>[height]" <?php echo $disabled_attr; // WPCS: XSS ok. ?> id="<?php echo esc_attr( $value['id'] ); ?>-height" type="text" size="3" value="<?php echo esc_attr( $height ); ?>" />px

								<label><input name="<?php echo esc_attr( $value['id'] ); ?>[crop]" <?php echo $disabled_attr; // WPCS: XSS ok. ?> id="<?php echo esc_attr( $value['id'] ); ?>-crop" type="checkbox" value="1" <?php checked( 1, $crop ); ?> /> <?php esc_html_e( 'Hard crop?', 'woocommerce' ); ?></label>

								</td>
						</tr>
						<?php
						break;

					// Single page selects.
					case 'single_select_page':
						$args = array(
							'name'             => $value['id'],
							'id'               => $value['id'],
							'sort_column'      => 'menu_order',
							'sort_order'       => 'ASC',
							'show_option_none' => ' ',
							'class'            => $value['class'],
							'echo'             => false,
							'selected'         => absint( $value['value'] ),
							'post_status'      => 'publish,private,draft',
						);

						if ( isset( $value['args'] ) ) {
							$args = wp_parse_args( $value['args'], $args );
						}

						?>
						<tr valign="top" class="single_select_page">
							<th scope="row" class="titledesc">
								<label><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html; // WPCS: XSS ok. ?></label>
							</th>
							<td class="forminp">
								<?php echo str_replace( ' id=', " data-placeholder='" . esc_attr__( 'Select a page&hellip;', 'woocommerce' ) . "' style='" . $value['css'] . "' class='" . $value['class'] . "' id=", wp_dropdown_pages( $args ) ); // WPCS: XSS ok. ?> <?php echo $description; // WPCS: XSS ok. ?>
							</td>
						</tr>
						<?php
						break;

					case 'single_select_page_with_search':
						$option_value = $value['value'];
						$page         = get_post( $option_value );

						if ( ! is_null( $page ) ) {
							$page                = get_post( $option_value );
							$option_display_name = sprintf(
								/* translators: 1: page name 2: page ID */
								__( '%1$s (ID: %2$s)', 'woocommerce' ),
								$page->post_title,
								$option_value
							);
						}
						?>
						<tr valign="top" class="single_select_page">
							<th scope="row" class="titledesc">
								<label for="<?php echo esc_attr( $value['id'] ); ?>"><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></label>
							</th>
							<td class="forminp forminp-<?php echo esc_attr( sanitize_title( $value['type'] ) ); ?>">
								<select
									name="<?php echo esc_attr( $value['id'] ); ?>"
									id="<?php echo esc_attr( $value['id'] ); ?>"
									style="<?php echo esc_attr( $value['css'] ); ?>"
									class="<?php echo esc_attr( $value['class'] ); ?>"
									<?php echo implode( ' ', $custom_attributes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
									data-placeholder="<?php esc_attr_e( 'Search for a page&hellip;', 'woocommerce' ); ?>"
									data-allow_clear="true"
									data-exclude="<?php echo wc_esc_json( wp_json_encode( $value['args']['exclude'] ) ); ?>"
									>
									<option value=""></option>
									<?php if ( ! is_null( $page ) ) { ?>
										<option value="<?php echo esc_attr( $option_value ); ?>" selected="selected">
										<?php echo wp_strip_all_tags( $option_display_name ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
										</option>
									<?php } ?>
								</select> <?php echo $description; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
							</td>
						</tr>
						<?php
						break;

					// Single country selects.
					case 'single_select_country':
						$country_setting = (string) $value['value'];

						if ( strstr( $country_setting, ':' ) ) {
							$country_setting = explode( ':', $country_setting );
							$country         = current( $country_setting );
							$state           = end( $country_setting );
						} else {
							$country = $country_setting;
							$state   = '*';
						}
						?>
						<tr valign="top">
							<th scope="row" class="titledesc">
								<label for="<?php echo esc_attr( $value['id'] ); ?>"><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html; // WPCS: XSS ok. ?></label>
							</th>
							<td class="forminp"><select name="<?php echo esc_attr( $value['id'] ); ?>" style="<?php echo esc_attr( $value['css'] ); ?>" data-placeholder="<?php esc_attr_e( 'Choose a country / region&hellip;', 'woocommerce' ); ?>" aria-label="<?php esc_attr_e( 'Country / Region', 'woocommerce' ); ?>" class="wc-enhanced-select">
								<?php WC()->countries->country_dropdown_options( $country, $state ); ?>
							</select> <?php echo $description; // WPCS: XSS ok. ?>
							</td>
						</tr>
						<?php
						break;

					// Country multiselects.
					case 'multi_select_countries':
						$selections = (array) $value['value'];

						if ( ! empty( $value['options'] ) ) {
							$countries = $value['options'];
						} else {
							$countries = WC()->countries->countries;
						}

						asort( $countries );
						?>
						<tr valign="top">
							<th scope="row" class="titledesc">
								<label for="<?php echo esc_attr( $value['id'] ); ?>"><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html; // WPCS: XSS ok. ?></label>
							</th>
							<td class="forminp">
								<select multiple="multiple" name="<?php echo esc_attr( $value['id'] ); ?>[]" style="width:350px" data-placeholder="<?php esc_attr_e( 'Choose countries / regions&hellip;', 'woocommerce' ); ?>" aria-label="<?php esc_attr_e( 'Country / Region', 'woocommerce' ); ?>" class="wc-enhanced-select">
									<?php
									if ( ! empty( $countries ) ) {
										foreach ( $countries as $key => $val ) {
											echo '<option value="' . esc_attr( $key ) . '"' . wc_selected( $key, $selections ) . '>' . esc_html( $val ) . '</option>'; // WPCS: XSS ok.
										}
									}
									?>
								</select> <?php echo ( $description ) ? $description : ''; // WPCS: XSS ok. ?> <br /><a class="select_all button" href="#"><?php esc_html_e( 'Select all', 'woocommerce' ); ?></a> <a class="select_none button" href="#"><?php esc_html_e( 'Select none', 'woocommerce' ); ?></a>
							</td>
						</tr>
						<?php
						break;

					// Days/months/years selector.
					case 'relative_date_selector':
						$periods      = array(
							'days'   => __( 'Day(s)', 'woocommerce' ),
							'weeks'  => __( 'Week(s)', 'woocommerce' ),
							'months' => __( 'Month(s)', 'woocommerce' ),
							'years'  => __( 'Year(s)', 'woocommerce' ),
						);
						$option_value = wc_parse_relative_date_option( $value['value'] );
						?>
						<tr valign="top">
							<th scope="row" class="titledesc">
								<label for="<?php echo esc_attr( $value['id'] ); ?>"><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html; // WPCS: XSS ok. ?></label>
							</th>
							<td class="forminp">
							<input
									name="<?php echo esc_attr( $value['id'] ); ?>[number]"
									id="<?php echo esc_attr( $value['id'] ); ?>"
									type="number"
									style="width: 80px;"
									value="<?php echo esc_attr( $option_value['number'] ); ?>"
									class="<?php echo esc_attr( $value['class'] ); ?>"
									placeholder="<?php echo esc_attr( $value['placeholder'] ); ?>"
									step="1"
									min="1"
									<?php echo implode( ' ', $custom_attributes ); // WPCS: XSS ok. ?>
								/>&nbsp;
								<select name="<?php echo esc_attr( $value['id'] ); ?>[unit]" style="width: auto;">
									<?php
									foreach ( $periods as $value => $label ) {
										echo '<option value="' . esc_attr( $value ) . '"' . selected( $option_value['unit'], $value, false ) . '>' . esc_html( $label ) . '</option>';
									}
									?>
								</select> <?php echo ( $description ) ? $description : ''; // WPCS: XSS ok. ?>
							</td>
						</tr>
						<?php
						break;

					// Default: run an action.
					default:
						do_action( 'woocommerce_admin_field_' . $value['type'], $value );
						break;
				}
			}
		}

		/**
		 * Helper function to get the formatted description and tip HTML for a
		 * given form field. Plugins can call this when implementing their own custom
		 * settings types.
		 *
		 * @param  array $value The form field value array.
		 * @return array The description and tip as a 2 element array.
		 */
		public static function get_field_description( $value ) {
			$description  = '';
			$tooltip_html = '';

			if ( true === $value['desc_tip'] ) {
				$tooltip_html = $value['desc'];
			} elseif ( ! empty( $value['desc_tip'] ) ) {
				$description  = $value['desc'];
				$tooltip_html = $value['desc_tip'];
			} elseif ( ! empty( $value['desc'] ) ) {
				$description = $value['desc'];
			}

			if ( $description && in_array( $value['type'], array( 'textarea', 'radio' ), true ) ) {
				$description = '<p style="margin-top:0">' . wp_kses_post( $description ) . '</p>';
			} elseif ( $description && in_array( $value['type'], array( 'checkbox' ), true ) ) {
				$description = wp_kses_post( $description );
			} elseif ( $description ) {
				$description = '<p class="description">' . wp_kses_post( $description ) . '</p>';
			}

			if ( $tooltip_html && in_array( $value['type'], array( 'checkbox' ), true ) ) {
				$tooltip_html = '<p class="description">' . $tooltip_html . '</p>';
			} elseif ( $tooltip_html ) {
				$tooltip_html = wc_help_tip( $tooltip_html );
			}

			return array(
				'description'  => $description,
				'tooltip_html' => $tooltip_html,
			);
		}

		/**
		 * Save admin fields.
		 *
		 * Loops through the woocommerce options array and outputs each field.
		 *
		 * @param array $options Options array to output.
		 * @param array $data    Optional. Data to use for saving. Defaults to $_POST.
		 * @return bool
		 */
		public static function save_fields( $options, $data = null ) {
			if ( is_null( $data ) ) {
				$data = $_POST; // WPCS: input var okay, CSRF ok.
			}
			if ( empty( $data ) ) {
				return false;
			}

			// Options to update will be stored here and saved later.
			$update_options   = array();
			$autoload_options = array();

			// Loop options and get values to save.
			foreach ( $options as $option ) {
				if ( ! isset( $option['id'] ) || ! isset( $option['type'] ) || ( isset( $option['is_option'] ) && false === $option['is_option'] ) ) {
					continue;
				}

				// Get posted value.
				if ( strstr( $option['id'], '[' ) ) {
					parse_str( $option['id'], $option_name_array );
					$option_name  = current( array_keys( $option_name_array ) );
					$setting_name = key( $option_name_array[ $option_name ] );
					$raw_value    = isset( $data[ $option_name ][ $setting_name ] ) ? wp_unslash( $data[ $option_name ][ $setting_name ] ) : null;
				} else {
					$option_name  = $option['id'];
					$setting_name = '';
					$raw_value    = isset( $data[ $option['id'] ] ) ? wp_unslash( $data[ $option['id'] ] ) : null;
				}

				// Format the value based on option type.
				switch ( $option['type'] ) {
					case 'checkbox':
						$value = '1' === $raw_value || 'yes' === $raw_value ? 'yes' : 'no';
						break;
					case 'textarea':
						$value = wp_kses_post( trim( $raw_value ) );
						break;
					case 'multiselect':
					case 'multi_select_countries':
						$value = array_filter( array_map( 'wc_clean', (array) $raw_value ) );
						break;
					case 'image_width':
						$value = array();
						if ( isset( $raw_value['width'] ) ) {
							$value['width']  = wc_clean( $raw_value['width'] );
							$value['height'] = wc_clean( $raw_value['height'] );
							$value['crop']   = isset( $raw_value['crop'] ) ? 1 : 0;
						} else {
							$value['width']  = $option['default']['width'];
							$value['height'] = $option['default']['height'];
							$value['crop']   = $option['default']['crop'];
						}
						break;
					case 'select':
						$allowed_values = empty( $option['options'] ) ? array() : array_map( 'strval', array_keys( $option['options'] ) );
						if ( empty( $option['default'] ) && empty( $allowed_values ) ) {
							$value = null;
							break;
						}
						$default = ( empty( $option['default'] ) ? $allowed_values[0] : $option['default'] );
						$value   = in_array( $raw_value, $allowed_values, true ) ? $raw_value : $default;
						break;
					case 'relative_date_selector':
						$value = wc_parse_relative_date_option( $raw_value );
						break;
					default:
						$value = wc_clean( $raw_value );
						break;
				}

				/**
				 * Fire an action when a certain 'type' of field is being saved.
				 *
				 * @deprecated 2.4.0 - doesn't allow manipulation of values!
				 */
				if ( has_action( 'woocommerce_update_option_' . sanitize_title( $option['type'] ) ) ) {
					wc_deprecated_function( 'The woocommerce_update_option_X action', '2.4.0', 'woocommerce_admin_settings_sanitize_option filter' );
					do_action( 'woocommerce_update_option_' . sanitize_title( $option['type'] ), $option );
					continue;
				}

				/**
				 * Sanitize the value of an option.
				 *
				 * @since 2.4.0
				 */
				$value = apply_filters( 'woocommerce_admin_settings_sanitize_option', $value, $option, $raw_value );

				/**
				 * Sanitize the value of an option by option name.
				 *
				 * @since 2.4.0
				 */
				$value = apply_filters( "woocommerce_admin_settings_sanitize_option_$option_name", $value, $option, $raw_value );

				if ( is_null( $value ) ) {
					continue;
				}

				// Check if option is an array and handle that differently to single values.
				if ( $option_name && $setting_name ) {
					if ( ! isset( $update_options[ $option_name ] ) ) {
						$update_options[ $option_name ] = get_option( $option_name, array() );
					}
					if ( ! is_array( $update_options[ $option_name ] ) ) {
						$update_options[ $option_name ] = array();
					}
					$update_options[ $option_name ][ $setting_name ] = $value;
				} else {
					$update_options[ $option_name ] = $value;
				}

				$autoload_options[ $option_name ] = isset( $option['autoload'] ) ? (bool) $option['autoload'] : true;

				/**
				 * Fire an action before saved.
				 *
				 * @deprecated 2.4.0 - doesn't allow manipulation of values!
				 */
				do_action( 'woocommerce_update_option', $option );
			}

			// Save all options in our array.
			foreach ( $update_options as $name => $value ) {
				update_option( $name, $value, $autoload_options[ $name ] ? 'yes' : 'no' );
			}

			return true;
		}

		/**
		 * Checks which method we're using to serve downloads.
		 *
		 * If using force or x-sendfile, this ensures the .htaccess is in place.
		 */
		public static function check_download_folder_protection() {
			$upload_dir      = wp_get_upload_dir();
			$downloads_path  = $upload_dir['basedir'] . '/woocommerce_uploads';
			$download_method = get_option( 'woocommerce_file_download_method' );
			$file_path       = $downloads_path . '/.htaccess';
			$file_content    = 'redirect' === $download_method ? 'Options -Indexes' : 'deny from all';
			$create          = false;

			if ( wp_mkdir_p( $downloads_path ) && ! file_exists( $file_path ) ) {
				$create = true;
			} else {
				$current_content = @file_get_contents( $file_path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents

				if ( $current_content !== $file_content ) {
					unlink( $file_path );
					$create = true;
				}
			}

			if ( $create ) {
				$file_handle = @fopen( $file_path, 'wb' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_fopen
				if ( $file_handle ) {
					fwrite( $file_handle, $file_content ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite
					fclose( $file_handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose
				}
			}
		}
	}

endif;
admin/class-wc-admin-api-keys-table-list.php000064400000016110151336044770014736 0ustar00<?php
/**
 * WooCommerce API Keys Table List
 *
 * @package WooCommerce\Admin
 * @version 2.4.0
 */

defined( 'ABSPATH' ) || exit;

if ( ! class_exists( 'WP_List_Table' ) ) {
	require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}

/**
 * API Keys table list class.
 */
class WC_Admin_API_Keys_Table_List extends WP_List_Table {

	/**
	 * Initialize the API key table list.
	 */
	public function __construct() {
		parent::__construct(
			array(
				'singular' => 'key',
				'plural'   => 'keys',
				'ajax'     => false,
			)
		);
	}

	/**
	 * No items found text.
	 */
	public function no_items() {
		esc_html_e( 'No keys found.', 'woocommerce' );
	}

	/**
	 * Get list columns.
	 *
	 * @return array
	 */
	public function get_columns() {
		return array(
			'cb'            => '<input type="checkbox" />',
			'title'         => __( 'Description', 'woocommerce' ),
			'truncated_key' => __( 'Consumer key ending in', 'woocommerce' ),
			'user'          => __( 'User', 'woocommerce' ),
			'permissions'   => __( 'Permissions', 'woocommerce' ),
			'last_access'   => __( 'Last access', 'woocommerce' ),
		);
	}

	/**
	 * Column cb.
	 *
	 * @param  array $key Key data.
	 * @return string
	 */
	public function column_cb( $key ) {
		return sprintf( '<input type="checkbox" name="key[]" value="%1$s" />', $key['key_id'] );
	}

	/**
	 * Return title column.
	 *
	 * @param  array $key Key data.
	 * @return string
	 */
	public function column_title( $key ) {
		$url     = admin_url( 'admin.php?page=wc-settings&tab=advanced&section=keys&edit-key=' . $key['key_id'] );
		$user_id = intval( $key['user_id'] );

		// Check if current user can edit other users or if it's the same user.
		$can_edit = current_user_can( 'edit_user', $user_id ) || get_current_user_id() === $user_id;

		$output = '<strong>';
		if ( $can_edit ) {
			$output .= '<a href="' . esc_url( $url ) . '" class="row-title">';
		}
		if ( empty( $key['description'] ) ) {
			$output .= esc_html__( 'API key', 'woocommerce' );
		} else {
			$output .= esc_html( $key['description'] );
		}
		if ( $can_edit ) {
			$output .= '</a>';
		}
		$output .= '</strong>';

		// Get actions.
		$actions = array(
			/* translators: %s: API key ID. */
			'id' => sprintf( __( 'ID: %d', 'woocommerce' ), $key['key_id'] ),
		);

		if ( $can_edit ) {
			$actions['edit']  = '<a href="' . esc_url( $url ) . '">' . __( 'View/Edit', 'woocommerce' ) . '</a>';
			$actions['trash'] = '<a class="submitdelete" aria-label="' . esc_attr__( 'Revoke API key', 'woocommerce' ) . '" href="' . esc_url(
				wp_nonce_url(
					add_query_arg(
						array(
							'revoke-key' => $key['key_id'],
						),
						admin_url( 'admin.php?page=wc-settings&tab=advanced&section=keys' )
					),
					'revoke'
				)
			) . '">' . esc_html__( 'Revoke', 'woocommerce' ) . '</a>';
		}

		$row_actions = array();

		foreach ( $actions as $action => $link ) {
			$row_actions[] = '<span class="' . esc_attr( $action ) . '">' . $link . '</span>';
		}

		$output .= '<div class="row-actions">' . implode( ' | ', $row_actions ) . '</div>';

		return $output;
	}

	/**
	 * Return truncated consumer key column.
	 *
	 * @param  array $key Key data.
	 * @return string
	 */
	public function column_truncated_key( $key ) {
		return '<code>&hellip;' . esc_html( $key['truncated_key'] ) . '</code>';
	}

	/**
	 * Return user column.
	 *
	 * @param  array $key Key data.
	 * @return string
	 */
	public function column_user( $key ) {
		$user = get_user_by( 'id', $key['user_id'] );

		if ( ! $user ) {
			return '';
		}

		if ( current_user_can( 'edit_user', $user->ID ) ) {
			return '<a href="' . esc_url( add_query_arg( array( 'user_id' => $user->ID ), admin_url( 'user-edit.php' ) ) ) . '">' . esc_html( $user->display_name ) . '</a>';
		}

		return esc_html( $user->display_name );
	}

	/**
	 * Return permissions column.
	 *
	 * @param  array $key Key data.
	 * @return string
	 */
	public function column_permissions( $key ) {
		$permission_key = $key['permissions'];
		$permissions    = array(
			'read'       => __( 'Read', 'woocommerce' ),
			'write'      => __( 'Write', 'woocommerce' ),
			'read_write' => __( 'Read/Write', 'woocommerce' ),
		);

		if ( isset( $permissions[ $permission_key ] ) ) {
			return esc_html( $permissions[ $permission_key ] );
		} else {
			return '';
		}
	}

	/**
	 * Return last access column.
	 *
	 * @param  array $key Key data.
	 * @return string
	 */
	public function column_last_access( $key ) {
		if ( ! empty( $key['last_access'] ) ) {
			/* translators: 1: last access date 2: last access time */
			$date = sprintf( __( '%1$s at %2$s', 'woocommerce' ), date_i18n( wc_date_format(), strtotime( $key['last_access'] ) ), date_i18n( wc_time_format(), strtotime( $key['last_access'] ) ) );

			return apply_filters( 'woocommerce_api_key_last_access_datetime', $date, $key['last_access'] );
		}

		return __( 'Unknown', 'woocommerce' );
	}

	/**
	 * Get bulk actions.
	 *
	 * @return array
	 */
	protected function get_bulk_actions() {
		if ( ! current_user_can( 'remove_users' ) ) {
			return array();
		}

		return array(
			'revoke' => __( 'Revoke', 'woocommerce' ),
		);
	}

	/**
	 * Search box.
	 *
	 * @param  string $text     Button text.
	 * @param  string $input_id Input ID.
	 */
	public function search_box( $text, $input_id ) {
		if ( empty( $_REQUEST['s'] ) && ! $this->has_items() ) { // WPCS: input var okay, CSRF ok.
			return;
		}

		$input_id     = $input_id . '-search-input';
		$search_query = isset( $_REQUEST['s'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ) : ''; // WPCS: input var okay, CSRF ok.

		echo '<p class="search-box">';
		echo '<label class="screen-reader-text" for="' . esc_attr( $input_id ) . '">' . esc_html( $text ) . ':</label>';
		echo '<input type="search" id="' . esc_attr( $input_id ) . '" name="s" value="' . esc_attr( $search_query ) . '" />';
		submit_button(
			$text,
			'',
			'',
			false,
			array(
				'id' => 'search-submit',
			)
		);
		echo '</p>';
	}

	/**
	 * Prepare table list items.
	 */
	public function prepare_items() {
		global $wpdb;

		$per_page     = $this->get_items_per_page( 'woocommerce_keys_per_page' );
		$current_page = $this->get_pagenum();

		if ( 1 < $current_page ) {
			$offset = $per_page * ( $current_page - 1 );
		} else {
			$offset = 0;
		}

		$search = '';

		if ( ! empty( $_REQUEST['s'] ) ) { // WPCS: input var okay, CSRF ok.
			$search = "AND description LIKE '%" . esc_sql( $wpdb->esc_like( wc_clean( wp_unslash( $_REQUEST['s'] ) ) ) ) . "%' "; // WPCS: input var okay, CSRF ok.
		}

		// Get the API keys.
		$keys = $wpdb->get_results(
			"SELECT key_id, user_id, description, permissions, truncated_key, last_access FROM {$wpdb->prefix}woocommerce_api_keys WHERE 1 = 1 {$search}" .
			$wpdb->prepare( 'ORDER BY key_id DESC LIMIT %d OFFSET %d;', $per_page, $offset ),
			ARRAY_A
		); // WPCS: unprepared SQL ok.

		$count = $wpdb->get_var( "SELECT COUNT(key_id) FROM {$wpdb->prefix}woocommerce_api_keys WHERE 1 = 1 {$search};" ); // WPCS: unprepared SQL ok.

		$this->items = $keys;

		// Set the pagination.
		$this->set_pagination_args(
			array(
				'total_items' => $count,
				'per_page'    => $per_page,
				'total_pages' => ceil( $count / $per_page ),
			)
		);
	}
}
admin/class-wc-admin-webhooks-table-list.php000064400000021644151336044770015045 0ustar00<?php
/**
 * WooCommerce Webhooks Table List
 *
 * @package WooCommerce\Admin
 * @version 3.3.0
 */

defined( 'ABSPATH' ) || exit;

if ( ! class_exists( 'WP_List_Table' ) ) {
	require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}

/**
 * Webooks table list class.
 */
class WC_Admin_Webhooks_Table_List extends WP_List_Table {

	/**
	 * Initialize the webhook table list.
	 */
	public function __construct() {
		parent::__construct(
			array(
				'singular' => 'webhook',
				'plural'   => 'webhooks',
				'ajax'     => false,
			)
		);
	}

	/**
	 * No items found text.
	 */
	public function no_items() {
		esc_html_e( 'No webhooks found.', 'woocommerce' );
	}

	/**
	 * Get list columns.
	 *
	 * @return array
	 */
	public function get_columns() {
		return array(
			'cb'           => '<input type="checkbox" />',
			'title'        => __( 'Name', 'woocommerce' ),
			'status'       => __( 'Status', 'woocommerce' ),
			'topic'        => __( 'Topic', 'woocommerce' ),
			'delivery_url' => __( 'Delivery URL', 'woocommerce' ),
		);
	}

	/**
	 * Column cb.
	 *
	 * @param  WC_Webhook $webhook Webhook instance.
	 * @return string
	 */
	public function column_cb( $webhook ) {
		return sprintf( '<input type="checkbox" name="%1$s[]" value="%2$s" />', $this->_args['singular'], $webhook->get_id() );
	}

	/**
	 * Return title column.
	 *
	 * @param  WC_Webhook $webhook Webhook instance.
	 * @return string
	 */
	public function column_title( $webhook ) {
		$edit_link = admin_url( 'admin.php?page=wc-settings&amp;tab=advanced&amp;section=webhooks&amp;edit-webhook=' . $webhook->get_id() );
		$output    = '';

		// Title.
		$output .= '<strong><a href="' . esc_url( $edit_link ) . '" class="row-title">' . esc_html( $webhook->get_name() ) . '</a></strong>';

		// Get actions.
		$actions = array(
			/* translators: %s: webhook ID. */
			'id'     => sprintf( __( 'ID: %d', 'woocommerce' ), $webhook->get_id() ),
			'edit'   => '<a href="' . esc_url( $edit_link ) . '">' . esc_html__( 'Edit', 'woocommerce' ) . '</a>',
			/* translators: %s: webhook name */
			'delete' => '<a class="submitdelete" aria-label="' . esc_attr( sprintf( __( 'Delete "%s" permanently', 'woocommerce' ), $webhook->get_name() ) ) . '" href="' . esc_url(
				wp_nonce_url(
					add_query_arg(
						array(
							'delete' => $webhook->get_id(),
						),
						admin_url( 'admin.php?page=wc-settings&tab=advanced&section=webhooks' )
					),
					'delete-webhook'
				)
			) . '">' . esc_html__( 'Delete permanently', 'woocommerce' ) . '</a>',
		);

		$actions     = apply_filters( 'webhook_row_actions', $actions, $webhook );
		$row_actions = array();

		foreach ( $actions as $action => $link ) {
			$row_actions[] = '<span class="' . esc_attr( $action ) . '">' . $link . '</span>';
		}

		$output .= '<div class="row-actions">' . implode( ' | ', $row_actions ) . '</div>';

		return $output;
	}

	/**
	 * Return status column.
	 *
	 * @param  WC_Webhook $webhook Webhook instance.
	 * @return string
	 */
	public function column_status( $webhook ) {
		return $webhook->get_i18n_status();
	}

	/**
	 * Return topic column.
	 *
	 * @param  WC_Webhook $webhook Webhook instance.
	 * @return string
	 */
	public function column_topic( $webhook ) {
		return $webhook->get_topic();
	}

	/**
	 * Return delivery URL column.
	 *
	 * @param  WC_Webhook $webhook Webhook instance.
	 * @return string
	 */
	public function column_delivery_url( $webhook ) {
		return $webhook->get_delivery_url();
	}

	/**
	 * Get the status label for webhooks.
	 *
	 * @param string $status_name Status name.
	 * @param int    $amount      Amount of webhooks.
	 * @return array
	 */
	private function get_status_label( $status_name, $amount ) {
		$statuses = wc_get_webhook_statuses();

		if ( isset( $statuses[ $status_name ] ) ) {
			return array(
				'singular' => sprintf( '%s <span class="count">(%s)</span>', esc_html( $statuses[ $status_name ] ), $amount ),
				'plural'   => sprintf( '%s <span class="count">(%s)</span>', esc_html( $statuses[ $status_name ] ), $amount ),
				'context'  => '',
				'domain'   => 'woocommerce',
			);
		}

		return array(
			'singular' => sprintf( '%s <span class="count">(%s)</span>', esc_html( $status_name ), $amount ),
			'plural'   => sprintf( '%s <span class="count">(%s)</span>', esc_html( $status_name ), $amount ),
			'context'  => '',
			'domain'   => 'woocommerce',
		);
	}

	/**
	 * Table list views.
	 *
	 * @return array
	 */
	protected function get_views() {
		$status_links   = array();
		$data_store     = WC_Data_Store::load( 'webhook' );
		$num_webhooks   = $data_store->get_count_webhooks_by_status();
		$total_webhooks = array_sum( (array) $num_webhooks );
		$statuses       = array_keys( wc_get_webhook_statuses() );
		$class          = empty( $_REQUEST['status'] ) ? ' class="current"' : ''; // WPCS: input var okay. CSRF ok.

		/* translators: %s: count */
		$status_links['all'] = "<a href='admin.php?page=wc-settings&amp;tab=advanced&amp;section=webhooks'$class>" . sprintf( _nx( 'All <span class="count">(%s)</span>', 'All <span class="count">(%s)</span>', $total_webhooks, 'posts', 'woocommerce' ), number_format_i18n( $total_webhooks ) ) . '</a>';

		foreach ( $statuses as $status_name ) {
			$class = '';

			if ( empty( $num_webhooks[ $status_name ] ) ) {
				continue;
			}

			if ( isset( $_REQUEST['status'] ) && sanitize_key( wp_unslash( $_REQUEST['status'] ) ) === $status_name ) { // WPCS: input var okay, CSRF ok.
				$class = ' class="current"';
			}

			$label = $this->get_status_label( $status_name, $num_webhooks[ $status_name ] );

			$status_links[ $status_name ] = "<a href='admin.php?page=wc-settings&amp;tab=advanced&amp;section=webhooks&amp;status=$status_name'$class>" . sprintf( translate_nooped_plural( $label, $num_webhooks[ $status_name ] ), number_format_i18n( $num_webhooks[ $status_name ] ) ) . '</a>';
		}

		return $status_links;
	}

	/**
	 * Get bulk actions.
	 *
	 * @return array
	 */
	protected function get_bulk_actions() {
		return array(
			'delete' => __( 'Delete permanently', 'woocommerce' ),
		);
	}

	/**
	 * Process bulk actions.
	 */
	public function process_bulk_action() {
		$action   = $this->current_action();
		$webhooks = isset( $_REQUEST['webhook'] ) ? array_map( 'absint', (array) $_REQUEST['webhook'] ) : array(); // WPCS: input var okay, CSRF ok.

		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			wp_die( esc_html__( 'You do not have permission to edit Webhooks', 'woocommerce' ) );
		}

		if ( 'delete' === $action ) {
			WC_Admin_Webhooks::bulk_delete( $webhooks );
		}
	}

	/**
	 * Generate the table navigation above or below the table.
	 * Included to remove extra nonce input.
	 *
	 * @param string $which The location of the extra table nav markup: 'top' or 'bottom'.
	 */
	protected function display_tablenav( $which ) {
		echo '<div class="tablenav ' . esc_attr( $which ) . '">';

		if ( $this->has_items() ) {
			echo '<div class="alignleft actions bulkactions">';
			$this->bulk_actions( $which );
			echo '</div>';
		}

		$this->extra_tablenav( $which );
		$this->pagination( $which );
		echo '<br class="clear" />';
		echo '</div>';
	}

	/**
	 * Search box.
	 *
	 * @param  string $text     Button text.
	 * @param  string $input_id Input ID.
	 */
	public function search_box( $text, $input_id ) {
		if ( empty( $_REQUEST['s'] ) && ! $this->has_items() ) { // WPCS: input var okay, CSRF ok.
			return;
		}

		$input_id     = $input_id . '-search-input';
		$search_query = isset( $_REQUEST['s'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ) : ''; // WPCS: input var okay, CSRF ok.

		echo '<p class="search-box">';
		echo '<label class="screen-reader-text" for="' . esc_attr( $input_id ) . '">' . esc_html( $text ) . ':</label>';
		echo '<input type="search" id="' . esc_attr( $input_id ) . '" name="s" value="' . esc_attr( $search_query ) . '" />';
		submit_button(
			$text,
			'',
			'',
			false,
			array(
				'id' => 'search-submit',
			)
		);
		echo '</p>';
	}

	/**
	 * Prepare table list items.
	 */
	public function prepare_items() {
		$per_page     = $this->get_items_per_page( 'woocommerce_webhooks_per_page' );
		$current_page = $this->get_pagenum();

		// Query args.
		$args = array(
			'limit'  => $per_page,
			'offset' => $per_page * ( $current_page - 1 ),
		);

		// Handle the status query.
		if ( ! empty( $_REQUEST['status'] ) ) { // WPCS: input var okay, CSRF ok.
			$args['status'] = sanitize_key( wp_unslash( $_REQUEST['status'] ) ); // WPCS: input var okay, CSRF ok.
		}

		if ( ! empty( $_REQUEST['s'] ) ) { // WPCS: input var okay, CSRF ok.
			$args['search'] = sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ); // WPCS: input var okay, CSRF ok.
		}

		$args['paginate'] = true;

		// Get the webhooks.
		$data_store  = WC_Data_Store::load( 'webhook' );
		$webhooks    = $data_store->search_webhooks( $args );
		$this->items = array_map( 'wc_get_webhook', $webhooks->webhooks );

		// Set the pagination.
		$this->set_pagination_args(
			array(
				'total_items' => $webhooks->total,
				'per_page'    => $per_page,
				'total_pages' => $webhooks->max_num_pages,
			)
		);
	}
}
admin/class-wc-admin-customize.php000064400000005004151336044770013200 0ustar00<?php
/**
 * Setup customize items.
 *
 * @package WooCommerce\Admin\Customize
 * @version 3.1.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'WC_Admin_Customize', false ) ) :

	/**
	 * WC_Admin_Customize Class.
	 */
	class WC_Admin_Customize {

		/**
		 * Initialize customize actions.
		 */
		public function __construct() {
			// Include custom items to customizer nav menu settings.
			add_filter( 'customize_nav_menu_available_item_types', array( $this, 'register_customize_nav_menu_item_types' ) );
			add_filter( 'customize_nav_menu_available_items', array( $this, 'register_customize_nav_menu_items' ), 10, 4 );
		}

		/**
		 * Register customize new nav menu item types.
		 * This will register WooCommerce account endpoints as a nav menu item type.
		 *
		 * @since  3.1.0
		 * @param  array $item_types Menu item types.
		 * @return array
		 */
		public function register_customize_nav_menu_item_types( $item_types ) {
			$item_types[] = array(
				'title'      => __( 'WooCommerce Endpoints', 'woocommerce' ),
				'type_label' => __( 'WooCommerce Endpoint', 'woocommerce' ),
				'type'       => 'woocommerce_nav',
				'object'     => 'woocommerce_endpoint',
			);

			return $item_types;
		}

		/**
		 * Register account endpoints to customize nav menu items.
		 *
		 * @since  3.1.0
		 * @param  array   $items  List of nav menu items.
		 * @param  string  $type   Nav menu type.
		 * @param  string  $object Nav menu object.
		 * @param  integer $page   Page number.
		 * @return array
		 */
		public function register_customize_nav_menu_items( $items = array(), $type = '', $object = '', $page = 0 ) {
			if ( 'woocommerce_endpoint' !== $object ) {
				return $items;
			}

			// Don't allow pagination since all items are loaded at once.
			if ( 0 < $page ) {
				return $items;
			}

			// Get items from account menu.
			$endpoints = wc_get_account_menu_items();

			// Remove dashboard item.
			if ( isset( $endpoints['dashboard'] ) ) {
				unset( $endpoints['dashboard'] );
			}

			// Include missing lost password.
			$endpoints['lost-password'] = __( 'Lost password', 'woocommerce' );

			$endpoints = apply_filters( 'woocommerce_custom_nav_menu_items', $endpoints );

			foreach ( $endpoints as $endpoint => $title ) {
				$items[] = array(
					'id'         => $endpoint,
					'title'      => $title,
					'type_label' => __( 'Custom Link', 'woocommerce' ),
					'url'        => esc_url_raw( wc_get_account_endpoint_url( $endpoint ) ),
				);
			}

			return $items;
		}
	}

endif;

return new WC_Admin_Customize();
admin/class-wc-admin-attributes.php000064400000046250151336044770013354 0ustar00<?php
/**
 * Attributes Page
 *
 * The attributes section lets users add custom attributes to assign to products - they can also be used in the "Filter Products by Attribute" widget.
 *
 * @package WooCommerce\Admin
 * @version 2.3.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * WC_Admin_Attributes Class.
 */
class WC_Admin_Attributes {

	/**
	 * Edited attribute ID.
	 *
	 * @var int
	 */
	private static $edited_attribute_id;

	/**
	 * Handles output of the attributes page in admin.
	 *
	 * Shows the created attributes and lets you add new ones or edit existing ones.
	 * The added attributes are stored in the database and can be used for layered navigation.
	 */
	public static function output() {
		$result = '';
		$action = '';

		// Action to perform: add, edit, delete or none.
		if ( ! empty( $_POST['add_new_attribute'] ) ) { // WPCS: CSRF ok.
			$action = 'add';
		} elseif ( ! empty( $_POST['save_attribute'] ) && ! empty( $_GET['edit'] ) ) { // WPCS: CSRF ok.
			$action = 'edit';
		} elseif ( ! empty( $_GET['delete'] ) ) {
			$action = 'delete';
		}

		switch ( $action ) {
			case 'add':
				$result = self::process_add_attribute();
				break;
			case 'edit':
				$result = self::process_edit_attribute();
				break;
			case 'delete':
				$result = self::process_delete_attribute();
				break;
		}

		if ( is_wp_error( $result ) ) {
			echo '<div id="woocommerce_errors" class="error"><p>' . wp_kses_post( $result->get_error_message() ) . '</p></div>';
		}

		// Show admin interface.
		if ( ! empty( $_GET['edit'] ) ) {
			self::edit_attribute();
		} else {
			self::add_attribute();
		}
	}

	/**
	 * Get and sanitize posted attribute data.
	 *
	 * @return array
	 */
	private static function get_posted_attribute() {
		$attribute = array(
			'attribute_label'   => isset( $_POST['attribute_label'] ) ? wc_clean( wp_unslash( $_POST['attribute_label'] ) ) : '', // WPCS: input var ok, CSRF ok.
			'attribute_name'    => isset( $_POST['attribute_name'] ) ? wc_sanitize_taxonomy_name( wp_unslash( $_POST['attribute_name'] ) ) : '', // WPCS: input var ok, CSRF ok, sanitization ok.
			'attribute_type'    => isset( $_POST['attribute_type'] ) ? wc_clean( wp_unslash( $_POST['attribute_type'] ) ) : 'select', // WPCS: input var ok, CSRF ok.
			'attribute_orderby' => isset( $_POST['attribute_orderby'] ) ? wc_clean( wp_unslash( $_POST['attribute_orderby'] ) ) : '', // WPCS: input var ok, CSRF ok.
			'attribute_public'  => isset( $_POST['attribute_public'] ) ? 1 : 0, // WPCS: input var ok, CSRF ok.
		);

		if ( empty( $attribute['attribute_type'] ) ) {
			$attribute['attribute_type'] = 'select';
		}
		if ( empty( $attribute['attribute_label'] ) ) {
			$attribute['attribute_label'] = ucfirst( $attribute['attribute_name'] );
		}
		if ( empty( $attribute['attribute_name'] ) ) {
			$attribute['attribute_name'] = wc_sanitize_taxonomy_name( $attribute['attribute_label'] );
		}

		return $attribute;
	}

	/**
	 * Add an attribute.
	 *
	 * @return bool|WP_Error
	 */
	private static function process_add_attribute() {
		check_admin_referer( 'woocommerce-add-new_attribute' );

		$attribute = self::get_posted_attribute();
		$args      = array(
			'name'         => $attribute['attribute_label'],
			'slug'         => $attribute['attribute_name'],
			'type'         => $attribute['attribute_type'],
			'order_by'     => $attribute['attribute_orderby'],
			'has_archives' => $attribute['attribute_public'],
		);

		$id = wc_create_attribute( $args );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		return true;
	}

	/**
	 * Edit an attribute.
	 *
	 * @return bool|WP_Error
	 */
	private static function process_edit_attribute() {
		$attribute_id = isset( $_GET['edit'] ) ? absint( $_GET['edit'] ) : 0;
		check_admin_referer( 'woocommerce-save-attribute_' . $attribute_id );

		$attribute = self::get_posted_attribute();
		$args      = array(
			'name'         => $attribute['attribute_label'],
			'slug'         => $attribute['attribute_name'],
			'type'         => $attribute['attribute_type'],
			'order_by'     => $attribute['attribute_orderby'],
			'has_archives' => $attribute['attribute_public'],
		);

		$id = wc_update_attribute( $attribute_id, $args );

		if ( is_wp_error( $id ) ) {
			return $id;
		}

		self::$edited_attribute_id = $id;

		return true;
	}

	/**
	 * Delete an attribute.
	 *
	 * @return bool
	 */
	private static function process_delete_attribute() {
		$attribute_id = isset( $_GET['delete'] ) ? absint( $_GET['delete'] ) : 0;
		check_admin_referer( 'woocommerce-delete-attribute_' . $attribute_id );

		return wc_delete_attribute( $attribute_id );
	}

	/**
	 * Edit Attribute admin panel.
	 *
	 * Shows the interface for changing an attributes type between select and text.
	 */
	public static function edit_attribute() {
		global $wpdb;

		$edit = isset( $_GET['edit'] ) ? absint( $_GET['edit'] ) : 0;

		$attribute_to_edit = $wpdb->get_row(
			$wpdb->prepare(
				"
				SELECT attribute_type, attribute_label, attribute_name, attribute_orderby, attribute_public
				FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_id = %d
				",
				$edit
			)
		);

		?>
		<div class="wrap woocommerce">
			<h1><?php esc_html_e( 'Edit attribute', 'woocommerce' ); ?></h1>

			<?php
			if ( ! $attribute_to_edit ) {
				echo '<div id="woocommerce_errors" class="error"><p>' . esc_html__( 'Error: non-existing attribute ID.', 'woocommerce' ) . '</p></div>';
			} else {
				if ( self::$edited_attribute_id > 0 ) {
					echo '<div id="message" class="updated"><p>' . esc_html__( 'Attribute updated successfully', 'woocommerce' ) . '</p><p><a href="' . esc_url( admin_url( 'edit.php?post_type=product&amp;page=product_attributes' ) ) . '">' . esc_html__( 'Back to Attributes', 'woocommerce' ) . '</a></p></div>';
					self::$edited_attribute_id = null;
				}
				$att_type    = $attribute_to_edit->attribute_type;
				$att_label   = format_to_edit( $attribute_to_edit->attribute_label );
				$att_name    = $attribute_to_edit->attribute_name;
				$att_orderby = $attribute_to_edit->attribute_orderby;
				$att_public  = $attribute_to_edit->attribute_public;
				?>
				<form action="edit.php?post_type=product&amp;page=product_attributes&amp;edit=<?php echo absint( $edit ); ?>" method="post">
					<table class="form-table">
						<tbody>
							<?php do_action( 'woocommerce_before_edit_attribute_fields' ); ?>
							<tr class="form-field form-required">
								<th scope="row" valign="top">
									<label for="attribute_label"><?php esc_html_e( 'Name', 'woocommerce' ); ?></label>
								</th>
								<td>
									<input name="attribute_label" id="attribute_label" type="text" value="<?php echo esc_attr( $att_label ); ?>" />
									<p class="description"><?php esc_html_e( 'Name for the attribute (shown on the front-end).', 'woocommerce' ); ?></p>
								</td>
							</tr>
							<tr class="form-field form-required">
								<th scope="row" valign="top">
									<label for="attribute_name"><?php esc_html_e( 'Slug', 'woocommerce' ); ?></label>
								</th>
								<td>
									<input name="attribute_name" id="attribute_name" type="text" value="<?php echo esc_attr( $att_name ); ?>" maxlength="28" />
									<p class="description"><?php esc_html_e( 'Unique slug/reference for the attribute; must be no more than 28 characters.', 'woocommerce' ); ?></p>
								</td>
							</tr>
							<tr class="form-field form-required">
								<th scope="row" valign="top">
									<label for="attribute_public"><?php esc_html_e( 'Enable archives?', 'woocommerce' ); ?></label>
								</th>
								<td>
									<input name="attribute_public" id="attribute_public" type="checkbox" value="1" <?php checked( $att_public, 1 ); ?> />
									<p class="description"><?php esc_html_e( 'Enable this if you want this attribute to have product archives in your store.', 'woocommerce' ); ?></p>
								</td>
							</tr>
							<?php
							/**
							 * Attribute types can change the way attributes are displayed on the frontend and admin.
							 *
							 * By Default WooCommerce only includes the `select` type. Others can be added with the
							 * `product_attributes_type_selector` filter. If there is only the default type registered,
							 * this setting will be hidden.
							 */
							if ( wc_has_custom_attribute_types() ) {
								?>
								<tr class="form-field form-required">
									<th scope="row" valign="top">
										<label for="attribute_type"><?php esc_html_e( 'Type', 'woocommerce' ); ?></label>
									</th>
									<td>
										<select name="attribute_type" id="attribute_type">
											<?php foreach ( wc_get_attribute_types() as $key => $value ) : ?>
												<option value="<?php echo esc_attr( $key ); ?>" <?php selected( $att_type, $key ); ?>><?php echo esc_html( $value ); ?></option>
											<?php endforeach; ?>
											<?php
												/**
												 * Deprecated action in favor of product_attributes_type_selector filter.
												 *
												 * @todo Remove in 4.0.0
												 * @deprecated 2.4.0
												 */
												do_action( 'woocommerce_admin_attribute_types' );
											?>
										</select>
										<p class="description"><?php esc_html_e( "Determines how this attribute's values are displayed.", 'woocommerce' ); ?></p>
									</td>
								</tr>
								<?php
							}
							?>
							<tr class="form-field form-required">
								<th scope="row" valign="top">
									<label for="attribute_orderby"><?php esc_html_e( 'Default sort order', 'woocommerce' ); ?></label>
								</th>
								<td>
									<select name="attribute_orderby" id="attribute_orderby">
										<option value="menu_order" <?php selected( $att_orderby, 'menu_order' ); ?>><?php esc_html_e( 'Custom ordering', 'woocommerce' ); ?></option>
										<option value="name" <?php selected( $att_orderby, 'name' ); ?>><?php esc_html_e( 'Name', 'woocommerce' ); ?></option>
										<option value="name_num" <?php selected( $att_orderby, 'name_num' ); ?>><?php esc_html_e( 'Name (numeric)', 'woocommerce' ); ?></option>
										<option value="id" <?php selected( $att_orderby, 'id' ); ?>><?php esc_html_e( 'Term ID', 'woocommerce' ); ?></option>
									</select>
									<p class="description"><?php esc_html_e( 'Determines the sort order of the terms on the frontend shop product pages. If using custom ordering, you can drag and drop the terms in this attribute.', 'woocommerce' ); ?></p>
								</td>
							</tr>
							<?php do_action( 'woocommerce_after_edit_attribute_fields' ); ?>
						</tbody>
					</table>
					<p class="submit"><button type="submit" name="save_attribute" id="submit" class="button-primary" value="<?php esc_attr_e( 'Update', 'woocommerce' ); ?>"><?php esc_html_e( 'Update', 'woocommerce' ); ?></button></p>
					<?php wp_nonce_field( 'woocommerce-save-attribute_' . $edit ); ?>
				</form>
			<?php } ?>
		</div>
		<?php
	}

	/**
	 * Add Attribute admin panel.
	 *
	 * Shows the interface for adding new attributes.
	 */
	public static function add_attribute() {
		?>
		<div class="wrap woocommerce">
			<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>

			<br class="clear" />
			<div id="col-container">
				<div id="col-right">
					<div class="col-wrap">
						<table class="widefat attributes-table wp-list-table ui-sortable" style="width:100%">
							<thead>
								<tr>
									<th scope="col"><?php esc_html_e( 'Name', 'woocommerce' ); ?></th>
									<th scope="col"><?php esc_html_e( 'Slug', 'woocommerce' ); ?></th>
									<?php if ( wc_has_custom_attribute_types() ) : ?>
										<th scope="col"><?php esc_html_e( 'Type', 'woocommerce' ); ?></th>
									<?php endif; ?>
									<th scope="col"><?php esc_html_e( 'Order by', 'woocommerce' ); ?></th>
									<th scope="col"><?php esc_html_e( 'Terms', 'woocommerce' ); ?></th>
								</tr>
							</thead>
							<tbody>
								<?php
								$attribute_taxonomies = wc_get_attribute_taxonomies();
								if ( $attribute_taxonomies ) :
									foreach ( $attribute_taxonomies as $tax ) :
										?>
										<tr>
												<td>
													<strong><a href="edit-tags.php?taxonomy=<?php echo esc_attr( wc_attribute_taxonomy_name( $tax->attribute_name ) ); ?>&amp;post_type=product"><?php echo esc_html( $tax->attribute_label ); ?></a></strong>

													<div class="row-actions"><span class="edit"><a href="<?php echo esc_url( add_query_arg( 'edit', $tax->attribute_id, 'edit.php?post_type=product&amp;page=product_attributes' ) ); ?>"><?php esc_html_e( 'Edit', 'woocommerce' ); ?></a> | </span><span class="delete"><a class="delete" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'delete', $tax->attribute_id, 'edit.php?post_type=product&amp;page=product_attributes' ), 'woocommerce-delete-attribute_' . $tax->attribute_id ) ); ?>"><?php esc_html_e( 'Delete', 'woocommerce' ); ?></a></span></div>
												</td>
												<td><?php echo esc_html( $tax->attribute_name ); ?></td>
												<?php if ( wc_has_custom_attribute_types() ) : ?>
													<td><?php echo esc_html( wc_get_attribute_type_label( $tax->attribute_type ) ); ?> <?php echo $tax->attribute_public ? esc_html__( '(Public)', 'woocommerce' ) : ''; ?></td>
												<?php endif; ?>
												<td>
													<?php
													switch ( $tax->attribute_orderby ) {
														case 'name':
															esc_html_e( 'Name', 'woocommerce' );
															break;
														case 'name_num':
															esc_html_e( 'Name (numeric)', 'woocommerce' );
															break;
														case 'id':
															esc_html_e( 'Term ID', 'woocommerce' );
															break;
														default:
															esc_html_e( 'Custom ordering', 'woocommerce' );
															break;
													}
													?>
												</td>
												<td class="attribute-terms">
													<?php
													$taxonomy = wc_attribute_taxonomy_name( $tax->attribute_name );

													if ( taxonomy_exists( $taxonomy ) ) {
														$terms        = get_terms( $taxonomy, 'hide_empty=0' );
														$terms_string = implode( ', ', wp_list_pluck( $terms, 'name' ) );
														if ( $terms_string ) {
															echo esc_html( $terms_string );
														} else {
															echo '<span class="na">&ndash;</span>';
														}
													} else {
															echo '<span class="na">&ndash;</span>';
													}
													?>
													<br /><a href="edit-tags.php?taxonomy=<?php echo esc_attr( wc_attribute_taxonomy_name( $tax->attribute_name ) ); ?>&amp;post_type=product" class="configure-terms"><?php esc_html_e( 'Configure terms', 'woocommerce' ); ?></a>
												</td>
											</tr>
											<?php
										endforeach;
									else :
										?>
										<tr>
											<td colspan="6"><?php esc_html_e( 'No attributes currently exist.', 'woocommerce' ); ?></td>
										</tr>
										<?php
									endif;
									?>
							</tbody>
						</table>
					</div>
				</div>
				<div id="col-left">
					<div class="col-wrap">
						<div class="form-wrap">
							<h2><?php esc_html_e( 'Add new attribute', 'woocommerce' ); ?></h2>
							<p><?php esc_html_e( 'Attributes let you define extra product data, such as size or color. You can use these attributes in the shop sidebar using the "layered nav" widgets.', 'woocommerce' ); ?></p>
							<form action="edit.php?post_type=product&amp;page=product_attributes" method="post">
								<?php do_action( 'woocommerce_before_add_attribute_fields' ); ?>

								<div class="form-field">
									<label for="attribute_label"><?php esc_html_e( 'Name', 'woocommerce' ); ?></label>
									<input name="attribute_label" id="attribute_label" type="text" value="" />
									<p class="description"><?php esc_html_e( 'Name for the attribute (shown on the front-end).', 'woocommerce' ); ?></p>
								</div>

								<div class="form-field">
									<label for="attribute_name"><?php esc_html_e( 'Slug', 'woocommerce' ); ?></label>
									<input name="attribute_name" id="attribute_name" type="text" value="" maxlength="28" />
									<p class="description"><?php esc_html_e( 'Unique slug/reference for the attribute; must be no more than 28 characters.', 'woocommerce' ); ?></p>
								</div>

								<div class="form-field">
									<label for="attribute_public"><input name="attribute_public" id="attribute_public" type="checkbox" value="1" /> <?php esc_html_e( 'Enable Archives?', 'woocommerce' ); ?></label>

									<p class="description"><?php esc_html_e( 'Enable this if you want this attribute to have product archives in your store.', 'woocommerce' ); ?></p>
								</div>

								<?php
								/**
								 * Attribute types can change the way attributes are displayed on the frontend and admin.
								 *
								 * By Default WooCommerce only includes the `select` type. Others can be added with the
								 * `product_attributes_type_selector` filter. If there is only the default type registered,
								 * this setting will be hidden.
								 */
								if ( wc_has_custom_attribute_types() ) {
									?>
									<div class="form-field">
										<label for="attribute_type"><?php esc_html_e( 'Type', 'woocommerce' ); ?></label>
										<select name="attribute_type" id="attribute_type">
											<?php foreach ( wc_get_attribute_types() as $key => $value ) : ?>
												<option value="<?php echo esc_attr( $key ); ?>"><?php echo esc_html( $value ); ?></option>
											<?php endforeach; ?>
											<?php
												/**
												 * Deprecated action in favor of product_attributes_type_selector filter.
												 *
												 * @todo Remove in 4.0.0
												 * @deprecated 2.4.0
												 */
												do_action( 'woocommerce_admin_attribute_types' );
											?>
										</select>
										<p class="description"><?php esc_html_e( "Determines how this attribute's values are displayed.", 'woocommerce' ); ?></p>
									</div>
									<?php
								}
								?>

								<div class="form-field">
									<label for="attribute_orderby"><?php esc_html_e( 'Default sort order', 'woocommerce' ); ?></label>
									<select name="attribute_orderby" id="attribute_orderby">
										<option value="menu_order"><?php esc_html_e( 'Custom ordering', 'woocommerce' ); ?></option>
										<option value="name"><?php esc_html_e( 'Name', 'woocommerce' ); ?></option>
										<option value="name_num"><?php esc_html_e( 'Name (numeric)', 'woocommerce' ); ?></option>
										<option value="id"><?php esc_html_e( 'Term ID', 'woocommerce' ); ?></option>
									</select>
									<p class="description"><?php esc_html_e( 'Determines the sort order of the terms on the frontend shop product pages. If using custom ordering, you can drag and drop the terms in this attribute.', 'woocommerce' ); ?></p>
								</div>

								<?php do_action( 'woocommerce_after_add_attribute_fields' ); ?>

								<p class="submit"><button type="submit" name="add_new_attribute" id="submit" class="button button-primary" value="<?php esc_attr_e( 'Add attribute', 'woocommerce' ); ?>"><?php esc_html_e( 'Add attribute', 'woocommerce' ); ?></button></p>
								<?php wp_nonce_field( 'woocommerce-add-new_attribute' ); ?>
							</form>
						</div>
					</div>
				</div>
			</div>
			<script type="text/javascript">
			/* <![CDATA[ */

				jQuery( 'a.delete' ).on( 'click', function() {
					if ( window.confirm( '<?php esc_html_e( 'Are you sure you want to delete this attribute?', 'woocommerce' ); ?>' ) ) {
						return true;
					}
					return false;
				});

			/* ]]> */
			</script>
		</div>
		<?php
	}
}
admin/list-tables/class-wc-admin-list-table-orders.php000064400000073266151336044770016754 0ustar00<?php
/**
 * List tables: orders.
 *
 * @package WooCommerce\Admin
 * @version 3.3.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( class_exists( 'WC_Admin_List_Table_Orders', false ) ) {
	return;
}

if ( ! class_exists( 'WC_Admin_List_Table', false ) ) {
	include_once __DIR__ . '/abstract-class-wc-admin-list-table.php';
}

/**
 * WC_Admin_List_Table_Orders Class.
 */
class WC_Admin_List_Table_Orders extends WC_Admin_List_Table {

	/**
	 * Post type.
	 *
	 * @var string
	 */
	protected $list_table_type = 'shop_order';

	/**
	 * Constructor.
	 */
	public function __construct() {
		parent::__construct();
		add_action( 'admin_notices', array( $this, 'bulk_admin_notices' ) );
		add_action( 'admin_footer', array( $this, 'order_preview_template' ) );
		add_filter( 'get_search_query', array( $this, 'search_label' ) );
		add_filter( 'query_vars', array( $this, 'add_custom_query_var' ) );
		add_action( 'parse_query', array( $this, 'search_custom_fields' ) );
	}

	/**
	 * Render blank state.
	 */
	protected function render_blank_state() {
		echo '<div class="woocommerce-BlankState">';

		echo '<h2 class="woocommerce-BlankState-message">' . esc_html__( 'When you receive a new order, it will appear here.', 'woocommerce' ) . '</h2>';

		echo '<div class="woocommerce-BlankState-buttons">';
		echo '<a class="woocommerce-BlankState-cta button-primary button" target="_blank" href="https://docs.woocommerce.com/document/managing-orders/?utm_source=blankslate&utm_medium=product&utm_content=ordersdoc&utm_campaign=woocommerceplugin">' . esc_html__( 'Learn more about orders', 'woocommerce' ) . '</a>';
		echo '</div>';

		do_action( 'wc_marketplace_suggestions_orders_empty_state' );

		echo '</div>';
	}

	/**
	 * Define primary column.
	 *
	 * @return string
	 */
	protected function get_primary_column() {
		return 'order_number';
	}

	/**
	 * Get row actions to show in the list table.
	 *
	 * @param array   $actions Array of actions.
	 * @param WP_Post $post Current post object.
	 * @return array
	 */
	protected function get_row_actions( $actions, $post ) {
		return array();
	}

	/**
	 * Define hidden columns.
	 *
	 * @return array
	 */
	protected function define_hidden_columns() {
		return array(
			'shipping_address',
			'billing_address',
			'wc_actions',
		);
	}

	/**
	 * Define which columns are sortable.
	 *
	 * @param array $columns Existing columns.
	 * @return array
	 */
	public function define_sortable_columns( $columns ) {
		$custom = array(
			'order_number' => 'ID',
			'order_total'  => 'order_total',
			'order_date'   => 'date',
		);
		unset( $columns['comments'] );

		return wp_parse_args( $custom, $columns );
	}

	/**
	 * Define which columns to show on this screen.
	 *
	 * @param array $columns Existing columns.
	 * @return array
	 */
	public function define_columns( $columns ) {
		$show_columns                     = array();
		$show_columns['cb']               = $columns['cb'];
		$show_columns['order_number']     = __( 'Order', 'woocommerce' );
		$show_columns['order_date']       = __( 'Date', 'woocommerce' );
		$show_columns['order_status']     = __( 'Status', 'woocommerce' );
		$show_columns['billing_address']  = __( 'Billing', 'woocommerce' );
		$show_columns['shipping_address'] = __( 'Ship to', 'woocommerce' );
		$show_columns['order_total']      = __( 'Total', 'woocommerce' );
		$show_columns['wc_actions']       = __( 'Actions', 'woocommerce' );

		wp_enqueue_script( 'wc-orders' );

		return $show_columns;
	}

	/**
	 * Define bulk actions.
	 *
	 * @param array $actions Existing actions.
	 * @return array
	 */
	public function define_bulk_actions( $actions ) {
		if ( isset( $actions['edit'] ) ) {
			unset( $actions['edit'] );
		}

		$actions['mark_processing'] = __( 'Change status to processing', 'woocommerce' );
		$actions['mark_on-hold']    = __( 'Change status to on-hold', 'woocommerce' );
		$actions['mark_completed']  = __( 'Change status to completed', 'woocommerce' );
		$actions['mark_cancelled']  = __( 'Change status to cancelled', 'woocommerce' );

		if ( wc_string_to_bool( get_option( 'woocommerce_allow_bulk_remove_personal_data', 'no' ) ) ) {
			$actions['remove_personal_data'] = __( 'Remove personal data', 'woocommerce' );
		}

		return $actions;
	}

	/**
	 * Pre-fetch any data for the row each column has access to it. the_order global is there for bw compat.
	 *
	 * @param int $post_id Post ID being shown.
	 */
	protected function prepare_row_data( $post_id ) {
		global $the_order;

		if ( empty( $this->object ) || $this->object->get_id() !== $post_id ) {
			$this->object = wc_get_order( $post_id );
			$the_order    = $this->object;
		}
	}

	/**
	 * Render columm: order_number.
	 */
	protected function render_order_number_column() {
		$buyer = '';

		if ( $this->object->get_billing_first_name() || $this->object->get_billing_last_name() ) {
			/* translators: 1: first name 2: last name */
			$buyer = trim( sprintf( _x( '%1$s %2$s', 'full name', 'woocommerce' ), $this->object->get_billing_first_name(), $this->object->get_billing_last_name() ) );
		} elseif ( $this->object->get_billing_company() ) {
			$buyer = trim( $this->object->get_billing_company() );
		} elseif ( $this->object->get_customer_id() ) {
			$user  = get_user_by( 'id', $this->object->get_customer_id() );
			$buyer = ucwords( $user->display_name );
		}

		/**
		 * Filter buyer name in list table orders.
		 *
		 * @since 3.7.0
		 * @param string   $buyer Buyer name.
		 * @param WC_Order $order Order data.
		 */
		$buyer = apply_filters( 'woocommerce_admin_order_buyer_name', $buyer, $this->object );

		if ( $this->object->get_status() === 'trash' ) {
			echo '<strong>#' . esc_attr( $this->object->get_order_number() ) . ' ' . esc_html( $buyer ) . '</strong>';
		} else {
			echo '<a href="#" class="order-preview" data-order-id="' . absint( $this->object->get_id() ) . '" title="' . esc_attr( __( 'Preview', 'woocommerce' ) ) . '">' . esc_html( __( 'Preview', 'woocommerce' ) ) . '</a>';
			echo '<a href="' . esc_url( admin_url( 'post.php?post=' . absint( $this->object->get_id() ) ) . '&action=edit' ) . '" class="order-view"><strong>#' . esc_attr( $this->object->get_order_number() ) . ' ' . esc_html( $buyer ) . '</strong></a>';
		}
	}

	/**
	 * Render columm: order_status.
	 */
	protected function render_order_status_column() {
		$tooltip                 = '';
		$comment_count           = get_comment_count( $this->object->get_id() );
		$approved_comments_count = absint( $comment_count['approved'] );

		if ( $approved_comments_count ) {
			$latest_notes = wc_get_order_notes(
				array(
					'order_id' => $this->object->get_id(),
					'limit'    => 1,
					'orderby'  => 'date_created_gmt',
				)
			);

			$latest_note = current( $latest_notes );

			if ( isset( $latest_note->content ) && 1 === $approved_comments_count ) {
				$tooltip = wc_sanitize_tooltip( $latest_note->content );
			} elseif ( isset( $latest_note->content ) ) {
				/* translators: %d: notes count */
				$tooltip = wc_sanitize_tooltip( $latest_note->content . '<br/><small style="display:block">' . sprintf( _n( 'Plus %d other note', 'Plus %d other notes', ( $approved_comments_count - 1 ), 'woocommerce' ), $approved_comments_count - 1 ) . '</small>' );
			} else {
				/* translators: %d: notes count */
				$tooltip = wc_sanitize_tooltip( sprintf( _n( '%d note', '%d notes', $approved_comments_count, 'woocommerce' ), $approved_comments_count ) );
			}
		}

		if ( $tooltip ) {
			printf( '<mark class="order-status %s tips" data-tip="%s"><span>%s</span></mark>', esc_attr( sanitize_html_class( 'status-' . $this->object->get_status() ) ), wp_kses_post( $tooltip ), esc_html( wc_get_order_status_name( $this->object->get_status() ) ) );
		} else {
			printf( '<mark class="order-status %s"><span>%s</span></mark>', esc_attr( sanitize_html_class( 'status-' . $this->object->get_status() ) ), esc_html( wc_get_order_status_name( $this->object->get_status() ) ) );
		}
	}

	/**
	 * Render columm: order_date.
	 */
	protected function render_order_date_column() {
		$order_timestamp = $this->object->get_date_created() ? $this->object->get_date_created()->getTimestamp() : '';

		if ( ! $order_timestamp ) {
			echo '&ndash;';
			return;
		}

		// Check if the order was created within the last 24 hours, and not in the future.
		if ( $order_timestamp > strtotime( '-1 day', time() ) && $order_timestamp <= time() ) {
			$show_date = sprintf(
				/* translators: %s: human-readable time difference */
				_x( '%s ago', '%s = human-readable time difference', 'woocommerce' ),
				human_time_diff( $this->object->get_date_created()->getTimestamp(), time() )
			);
		} else {
			$show_date = $this->object->get_date_created()->date_i18n( apply_filters( 'woocommerce_admin_order_date_format', __( 'M j, Y', 'woocommerce' ) ) );
		}
		printf(
			'<time datetime="%1$s" title="%2$s">%3$s</time>',
			esc_attr( $this->object->get_date_created()->date( 'c' ) ),
			esc_html( $this->object->get_date_created()->date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) ) ),
			esc_html( $show_date )
		);
	}

	/**
	 * Render columm: order_total.
	 */
	protected function render_order_total_column() {
		if ( $this->object->get_payment_method_title() ) {
			/* translators: %s: method */
			echo '<span class="tips" data-tip="' . esc_attr( sprintf( __( 'via %s', 'woocommerce' ), $this->object->get_payment_method_title() ) ) . '">' . wp_kses_post( $this->object->get_formatted_order_total() ) . '</span>';
		} else {
			echo wp_kses_post( $this->object->get_formatted_order_total() );
		}
	}

	/**
	 * Render columm: wc_actions.
	 */
	protected function render_wc_actions_column() {
		echo '<p>';

		do_action( 'woocommerce_admin_order_actions_start', $this->object );

		$actions = array();

		if ( $this->object->has_status( array( 'pending', 'on-hold' ) ) ) {
			$actions['processing'] = array(
				'url'    => wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_mark_order_status&status=processing&order_id=' . $this->object->get_id() ), 'woocommerce-mark-order-status' ),
				'name'   => __( 'Processing', 'woocommerce' ),
				'action' => 'processing',
			);
		}

		if ( $this->object->has_status( array( 'pending', 'on-hold', 'processing' ) ) ) {
			$actions['complete'] = array(
				'url'    => wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_mark_order_status&status=completed&order_id=' . $this->object->get_id() ), 'woocommerce-mark-order-status' ),
				'name'   => __( 'Complete', 'woocommerce' ),
				'action' => 'complete',
			);
		}

		$actions = apply_filters( 'woocommerce_admin_order_actions', $actions, $this->object );

		echo wc_render_action_buttons( $actions ); // WPCS: XSS ok.

		do_action( 'woocommerce_admin_order_actions_end', $this->object );

		echo '</p>';
	}

	/**
	 * Render columm: billing_address.
	 */
	protected function render_billing_address_column() {
		$address = $this->object->get_formatted_billing_address();

		if ( $address ) {
			echo esc_html( preg_replace( '#<br\s*/?>#i', ', ', $address ) );

			if ( $this->object->get_payment_method() ) {
				/* translators: %s: payment method */
				echo '<span class="description">' . sprintf( __( 'via %s', 'woocommerce' ), esc_html( $this->object->get_payment_method_title() ) ) . '</span>'; // WPCS: XSS ok.
			}
		} else {
			echo '&ndash;';
		}
	}

	/**
	 * Render columm: shipping_address.
	 */
	protected function render_shipping_address_column() {
		$address = $this->object->get_formatted_shipping_address();

		if ( $address ) {
			echo '<a target="_blank" href="' . esc_url( $this->object->get_shipping_address_map_url() ) . '">' . esc_html( preg_replace( '#<br\s*/?>#i', ', ', $address ) ) . '</a>';
			if ( $this->object->get_shipping_method() ) {
				/* translators: %s: shipping method */
				echo '<span class="description">' . sprintf( __( 'via %s', 'woocommerce' ), esc_html( $this->object->get_shipping_method() ) ) . '</span>'; // WPCS: XSS ok.
			}
		} else {
			echo '&ndash;';
		}
	}

	/**
	 * Template for order preview.
	 *
	 * @since 3.3.0
	 */
	public function order_preview_template() {
		?>
		<script type="text/template" id="tmpl-wc-modal-view-order">
			<div class="wc-backbone-modal wc-order-preview">
				<div class="wc-backbone-modal-content">
					<section class="wc-backbone-modal-main" role="main">
						<header class="wc-backbone-modal-header">
							<mark class="order-status status-{{ data.status }}"><span>{{ data.status_name }}</span></mark>
							<?php /* translators: %s: order ID */ ?>
							<h1><?php echo esc_html( sprintf( __( 'Order #%s', 'woocommerce' ), '{{ data.order_number }}' ) ); ?></h1>
							<button class="modal-close modal-close-link dashicons dashicons-no-alt">
								<span class="screen-reader-text"><?php esc_html_e( 'Close modal panel', 'woocommerce' ); ?></span>
							</button>
						</header>
						<article>
							<?php do_action( 'woocommerce_admin_order_preview_start' ); ?>

							<div class="wc-order-preview-addresses">
								<div class="wc-order-preview-address">
									<h2><?php esc_html_e( 'Billing details', 'woocommerce' ); ?></h2>
									{{{ data.formatted_billing_address }}}

									<# if ( data.data.billing.email ) { #>
										<strong><?php esc_html_e( 'Email', 'woocommerce' ); ?></strong>
										<a href="mailto:{{ data.data.billing.email }}">{{ data.data.billing.email }}</a>
									<# } #>

									<# if ( data.data.billing.phone ) { #>
										<strong><?php esc_html_e( 'Phone', 'woocommerce' ); ?></strong>
										<a href="tel:{{ data.data.billing.phone }}">{{ data.data.billing.phone }}</a>
									<# } #>

									<# if ( data.payment_via ) { #>
										<strong><?php esc_html_e( 'Payment via', 'woocommerce' ); ?></strong>
										{{{ data.payment_via }}}
									<# } #>
								</div>
								<# if ( data.needs_shipping ) { #>
									<div class="wc-order-preview-address">
										<h2><?php esc_html_e( 'Shipping details', 'woocommerce' ); ?></h2>
										<# if ( data.ship_to_billing ) { #>
											{{{ data.formatted_billing_address }}}
										<# } else { #>
											<a href="{{ data.shipping_address_map_url }}" target="_blank">{{{ data.formatted_shipping_address }}}</a>
										<# } #>

										<# if ( data.shipping_via ) { #>
											<strong><?php esc_html_e( 'Shipping method', 'woocommerce' ); ?></strong>
											{{ data.shipping_via }}
										<# } #>
									</div>
								<# } #>

								<# if ( data.data.customer_note ) { #>
									<div class="wc-order-preview-note">
										<strong><?php esc_html_e( 'Note', 'woocommerce' ); ?></strong>
										{{ data.data.customer_note }}
									</div>
								<# } #>
							</div>

							{{{ data.item_html }}}

							<?php do_action( 'woocommerce_admin_order_preview_end' ); ?>
						</article>
						<footer>
							<div class="inner">
								{{{ data.actions_html }}}

								<a class="button button-primary button-large" aria-label="<?php esc_attr_e( 'Edit this order', 'woocommerce' ); ?>" href="<?php echo esc_url( admin_url( 'post.php?action=edit' ) ); ?>&post={{ data.data.id }}"><?php esc_html_e( 'Edit', 'woocommerce' ); ?></a>
							</div>
						</footer>
					</section>
				</div>
			</div>
			<div class="wc-backbone-modal-backdrop modal-close"></div>
		</script>
		<?php
	}

	/**
	 * Get items to display in the preview as HTML.
	 *
	 * @param  WC_Order $order Order object.
	 * @return string
	 */
	public static function get_order_preview_item_html( $order ) {
		$hidden_order_itemmeta = apply_filters(
			'woocommerce_hidden_order_itemmeta',
			array(
				'_qty',
				'_tax_class',
				'_product_id',
				'_variation_id',
				'_line_subtotal',
				'_line_subtotal_tax',
				'_line_total',
				'_line_tax',
				'method_id',
				'cost',
				'_reduced_stock',
				'_restock_refunded_items',
			)
		);

		$line_items = apply_filters( 'woocommerce_admin_order_preview_line_items', $order->get_items(), $order );
		$columns    = apply_filters(
			'woocommerce_admin_order_preview_line_item_columns',
			array(
				'product'  => __( 'Product', 'woocommerce' ),
				'quantity' => __( 'Quantity', 'woocommerce' ),
				'tax'      => __( 'Tax', 'woocommerce' ),
				'total'    => __( 'Total', 'woocommerce' ),
			),
			$order
		);

		if ( ! wc_tax_enabled() ) {
			unset( $columns['tax'] );
		}

		$html = '
		<div class="wc-order-preview-table-wrapper">
			<table cellspacing="0" class="wc-order-preview-table">
				<thead>
					<tr>';

		foreach ( $columns as $column => $label ) {
			$html .= '<th class="wc-order-preview-table__column--' . esc_attr( $column ) . '">' . esc_html( $label ) . '</th>';
		}

		$html .= '
					</tr>
				</thead>
				<tbody>';

		foreach ( $line_items as $item_id => $item ) {

			$product_object = is_callable( array( $item, 'get_product' ) ) ? $item->get_product() : null;
			$row_class      = apply_filters( 'woocommerce_admin_html_order_preview_item_class', '', $item, $order );

			$html .= '<tr class="wc-order-preview-table__item wc-order-preview-table__item--' . esc_attr( $item_id ) . ( $row_class ? ' ' . esc_attr( $row_class ) : '' ) . '">';

			foreach ( $columns as $column => $label ) {
				$html .= '<td class="wc-order-preview-table__column--' . esc_attr( $column ) . '">';
				switch ( $column ) {
					case 'product':
						$html .= wp_kses_post( $item->get_name() );

						if ( $product_object ) {
							$html .= '<div class="wc-order-item-sku">' . esc_html( $product_object->get_sku() ) . '</div>';
						}

						$meta_data = $item->get_formatted_meta_data( '' );

						if ( $meta_data ) {
							$html .= '<table cellspacing="0" class="wc-order-item-meta">';

							foreach ( $meta_data as $meta_id => $meta ) {
								if ( in_array( $meta->key, $hidden_order_itemmeta, true ) ) {
									continue;
								}
								$html .= '<tr><th>' . wp_kses_post( $meta->display_key ) . ':</th><td>' . wp_kses_post( force_balance_tags( $meta->display_value ) ) . '</td></tr>';
							}
							$html .= '</table>';
						}
						break;
					case 'quantity':
						$html .= esc_html( $item->get_quantity() );
						break;
					case 'tax':
						$html .= wc_price( $item->get_total_tax(), array( 'currency' => $order->get_currency() ) );
						break;
					case 'total':
						$html .= wc_price( $item->get_total(), array( 'currency' => $order->get_currency() ) );
						break;
					default:
						$html .= apply_filters( 'woocommerce_admin_order_preview_line_item_column_' . sanitize_key( $column ), '', $item, $item_id, $order );
						break;
				}
				$html .= '</td>';
			}

			$html .= '</tr>';
		}

		$html .= '
				</tbody>
			</table>
		</div>';

		return $html;
	}

	/**
	 * Get actions to display in the preview as HTML.
	 *
	 * @param  WC_Order $order Order object.
	 * @return string
	 */
	public static function get_order_preview_actions_html( $order ) {
		$actions        = array();
		$status_actions = array();

		if ( $order->has_status( array( 'pending' ) ) ) {
			$status_actions['on-hold'] = array(
				'url'    => wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_mark_order_status&status=on-hold&order_id=' . $order->get_id() ), 'woocommerce-mark-order-status' ),
				'name'   => __( 'On-hold', 'woocommerce' ),
				'title'  => __( 'Change order status to on-hold', 'woocommerce' ),
				'action' => 'on-hold',
			);
		}

		if ( $order->has_status( array( 'pending', 'on-hold' ) ) ) {
			$status_actions['processing'] = array(
				'url'    => wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_mark_order_status&status=processing&order_id=' . $order->get_id() ), 'woocommerce-mark-order-status' ),
				'name'   => __( 'Processing', 'woocommerce' ),
				'title'  => __( 'Change order status to processing', 'woocommerce' ),
				'action' => 'processing',
			);
		}

		if ( $order->has_status( array( 'pending', 'on-hold', 'processing' ) ) ) {
			$status_actions['complete'] = array(
				'url'    => wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_mark_order_status&status=completed&order_id=' . $order->get_id() ), 'woocommerce-mark-order-status' ),
				'name'   => __( 'Completed', 'woocommerce' ),
				'title'  => __( 'Change order status to completed', 'woocommerce' ),
				'action' => 'complete',
			);
		}

		if ( $status_actions ) {
			$actions['status'] = array(
				'group'   => __( 'Change status: ', 'woocommerce' ),
				'actions' => $status_actions,
			);
		}

		return wc_render_action_buttons( apply_filters( 'woocommerce_admin_order_preview_actions', $actions, $order ) );
	}

	/**
	 * Get order details to send to the ajax endpoint for previews.
	 *
	 * @param  WC_Order $order Order object.
	 * @return array
	 */
	public static function order_preview_get_order_details( $order ) {
		if ( ! $order ) {
			return array();
		}

		$payment_via      = $order->get_payment_method_title();
		$payment_method   = $order->get_payment_method();
		$payment_gateways = WC()->payment_gateways() ? WC()->payment_gateways->payment_gateways() : array();
		$transaction_id   = $order->get_transaction_id();

		if ( $transaction_id ) {

			$url = isset( $payment_gateways[ $payment_method ] ) ? $payment_gateways[ $payment_method ]->get_transaction_url( $order ) : false;

			if ( $url ) {
				$payment_via .= ' (<a href="' . esc_url( $url ) . '" target="_blank">' . esc_html( $transaction_id ) . '</a>)';
			} else {
				$payment_via .= ' (' . esc_html( $transaction_id ) . ')';
			}
		}

		$billing_address  = $order->get_formatted_billing_address();
		$shipping_address = $order->get_formatted_shipping_address();

		return apply_filters(
			'woocommerce_admin_order_preview_get_order_details',
			array(
				'data'                       => $order->get_data(),
				'order_number'               => $order->get_order_number(),
				'item_html'                  => self::get_order_preview_item_html( $order ),
				'actions_html'               => self::get_order_preview_actions_html( $order ),
				'ship_to_billing'            => wc_ship_to_billing_address_only(),
				'needs_shipping'             => $order->needs_shipping_address(),
				'formatted_billing_address'  => $billing_address ? $billing_address : __( 'N/A', 'woocommerce' ),
				'formatted_shipping_address' => $shipping_address ? $shipping_address : __( 'N/A', 'woocommerce' ),
				'shipping_address_map_url'   => $order->get_shipping_address_map_url(),
				'payment_via'                => $payment_via,
				'shipping_via'               => $order->get_shipping_method(),
				'status'                     => $order->get_status(),
				'status_name'                => wc_get_order_status_name( $order->get_status() ),
			),
			$order
		);
	}

	/**
	 * Handle bulk actions.
	 *
	 * @param  string $redirect_to URL to redirect to.
	 * @param  string $action      Action name.
	 * @param  array  $ids         List of ids.
	 * @return string
	 */
	public function handle_bulk_actions( $redirect_to, $action, $ids ) {
		$ids     = apply_filters( 'woocommerce_bulk_action_ids', array_reverse( array_map( 'absint', $ids ) ), $action, 'order' );
		$changed = 0;

		if ( 'remove_personal_data' === $action ) {
			$report_action = 'removed_personal_data';

			foreach ( $ids as $id ) {
				$order = wc_get_order( $id );

				if ( $order ) {
					do_action( 'woocommerce_remove_order_personal_data', $order );
					$changed++;
				}
			}
		} elseif ( false !== strpos( $action, 'mark_' ) ) {
			$order_statuses = wc_get_order_statuses();
			$new_status     = substr( $action, 5 ); // Get the status name from action.
			$report_action  = 'marked_' . $new_status;

			// Sanity check: bail out if this is actually not a status, or is not a registered status.
			if ( isset( $order_statuses[ 'wc-' . $new_status ] ) ) {
				// Initialize payment gateways in case order has hooked status transition actions.
				WC()->payment_gateways();

				foreach ( $ids as $id ) {
					$order = wc_get_order( $id );
					$order->update_status( $new_status, __( 'Order status changed by bulk edit:', 'woocommerce' ), true );
					do_action( 'woocommerce_order_edit_status', $id, $new_status );
					$changed++;
				}
			}
		}

		if ( $changed ) {
			$redirect_to = add_query_arg(
				array(
					'post_type'   => $this->list_table_type,
					'bulk_action' => $report_action,
					'changed'     => $changed,
					'ids'         => join( ',', $ids ),
				),
				$redirect_to
			);
		}

		return esc_url_raw( $redirect_to );
	}

	/**
	 * Show confirmation message that order status changed for number of orders.
	 */
	public function bulk_admin_notices() {
		global $post_type, $pagenow;

		// Bail out if not on shop order list page.
		if ( 'edit.php' !== $pagenow || 'shop_order' !== $post_type || ! isset( $_REQUEST['bulk_action'] ) ) { // WPCS: input var ok, CSRF ok.
			return;
		}

		$order_statuses = wc_get_order_statuses();
		$number         = isset( $_REQUEST['changed'] ) ? absint( $_REQUEST['changed'] ) : 0; // WPCS: input var ok, CSRF ok.
		$bulk_action    = wc_clean( wp_unslash( $_REQUEST['bulk_action'] ) ); // WPCS: input var ok, CSRF ok.

		// Check if any status changes happened.
		foreach ( $order_statuses as $slug => $name ) {
			if ( 'marked_' . str_replace( 'wc-', '', $slug ) === $bulk_action ) { // WPCS: input var ok, CSRF ok.
				/* translators: %d: orders count */
				$message = sprintf( _n( '%d order status changed.', '%d order statuses changed.', $number, 'woocommerce' ), number_format_i18n( $number ) );
				echo '<div class="updated"><p>' . esc_html( $message ) . '</p></div>';
				break;
			}
		}

		if ( 'removed_personal_data' === $bulk_action ) { // WPCS: input var ok, CSRF ok.
			/* translators: %d: orders count */
			$message = sprintf( _n( 'Removed personal data from %d order.', 'Removed personal data from %d orders.', $number, 'woocommerce' ), number_format_i18n( $number ) );
			echo '<div class="updated"><p>' . esc_html( $message ) . '</p></div>';
		}
	}

	/**
	 * See if we should render search filters or not.
	 */
	public function restrict_manage_posts() {
		global $typenow;

		if ( in_array( $typenow, wc_get_order_types( 'order-meta-boxes' ), true ) ) {
			$this->render_filters();
		}
	}

	/**
	 * Render any custom filters and search inputs for the list table.
	 */
	protected function render_filters() {
		$user_string = '';
		$user_id     = '';

		if ( ! empty( $_GET['_customer_user'] ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended
			$user_id = absint( $_GET['_customer_user'] ); // WPCS: input var ok, sanitization ok.
			$user    = get_user_by( 'id', $user_id );

			$user_string = sprintf(
				/* translators: 1: user display name 2: user ID 3: user email */
				esc_html__( '%1$s (#%2$s &ndash; %3$s)', 'woocommerce' ),
				$user->display_name,
				absint( $user->ID ),
				$user->user_email
			);
		}
		?>
		<select class="wc-customer-search" name="_customer_user" data-placeholder="<?php esc_attr_e( 'Filter by registered customer', 'woocommerce' ); ?>" data-allow_clear="true">
			<option value="<?php echo esc_attr( $user_id ); ?>" selected="selected"><?php echo htmlspecialchars( wp_kses_post( $user_string ) ); // htmlspecialchars to prevent XSS when rendered by selectWoo. ?></option>
		</select>
		<?php
	}

	/**
	 * Handle any filters.
	 *
	 * @param array $query_vars Query vars.
	 * @return array
	 */
	public function request_query( $query_vars ) {
		global $typenow;

		if ( in_array( $typenow, wc_get_order_types( 'order-meta-boxes' ), true ) ) {
			return $this->query_filters( $query_vars );
		}

		return $query_vars;
	}

	/**
	 * Handle any custom filters.
	 *
	 * @param array $query_vars Query vars.
	 * @return array
	 */
	protected function query_filters( $query_vars ) {
		global $wp_post_statuses;

		// Filter the orders by the posted customer.
		if ( ! empty( $_GET['_customer_user'] ) ) { // WPCS: input var ok.
			// @codingStandardsIgnoreStart.
			$query_vars['meta_query'] = array(
				array(
					'key'     => '_customer_user',
					'value'   => (int) $_GET['_customer_user'], // WPCS: input var ok, sanitization ok.
					'compare' => '=',
				),
			);
			// @codingStandardsIgnoreEnd
		}

		// Sorting.
		if ( isset( $query_vars['orderby'] ) ) {
			if ( 'order_total' === $query_vars['orderby'] ) {
				// @codingStandardsIgnoreStart
				$query_vars = array_merge( $query_vars, array(
					'meta_key'  => '_order_total',
					'orderby'   => 'meta_value_num',
				) );
				// @codingStandardsIgnoreEnd
			}
		}

		// Status.
		if ( empty( $query_vars['post_status'] ) ) {
			$post_statuses = wc_get_order_statuses();

			foreach ( $post_statuses as $status => $value ) {
				if ( isset( $wp_post_statuses[ $status ] ) && false === $wp_post_statuses[ $status ]->show_in_admin_all_list ) {
					unset( $post_statuses[ $status ] );
				}
			}

			$query_vars['post_status'] = array_keys( $post_statuses );
		}
		return $query_vars;
	}

	/**
	 * Change the label when searching orders.
	 *
	 * @param mixed $query Current search query.
	 * @return string
	 */
	public function search_label( $query ) {
		global $pagenow, $typenow;

		if ( 'edit.php' !== $pagenow || 'shop_order' !== $typenow || ! get_query_var( 'shop_order_search' ) || ! isset( $_GET['s'] ) ) { // phpcs:ignore  WordPress.Security.NonceVerification.Recommended
			return $query;
		}

		return wc_clean( wp_unslash( $_GET['s'] ) ); // WPCS: input var ok, sanitization ok.
	}

	/**
	 * Query vars for custom searches.
	 *
	 * @param mixed $public_query_vars Array of query vars.
	 * @return array
	 */
	public function add_custom_query_var( $public_query_vars ) {
		$public_query_vars[] = 'shop_order_search';
		return $public_query_vars;
	}

	/**
	 * Search custom fields as well as content.
	 *
	 * @param WP_Query $wp Query object.
	 */
	public function search_custom_fields( $wp ) {
		global $pagenow;

		if ( 'edit.php' !== $pagenow || empty( $wp->query_vars['s'] ) || 'shop_order' !== $wp->query_vars['post_type'] || ! isset( $_GET['s'] ) ) { // phpcs:ignore  WordPress.Security.NonceVerification.Recommended
			return;
		}

		$post_ids = wc_order_search( wc_clean( wp_unslash( $_GET['s'] ) ) ); // WPCS: input var ok, sanitization ok.

		if ( ! empty( $post_ids ) ) {
			// Remove "s" - we don't want to search order name.
			unset( $wp->query_vars['s'] );

			// so we know we're doing this.
			$wp->query_vars['shop_order_search'] = true;

			// Search by found posts.
			$wp->query_vars['post__in'] = array_merge( $post_ids, array( 0 ) );
		}
	}
}
admin/list-tables/class-wc-admin-list-table-coupons.php000064400000014436151336044770017136 0ustar00<?php
/**
 * List tables: coupons.
 *
 * @package  WooCommerce\Admin
 * @version  3.3.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( class_exists( 'WC_Admin_List_Table_Coupons', false ) ) {
	return;
}

if ( ! class_exists( 'WC_Admin_List_Table', false ) ) {
	include_once __DIR__ . '/abstract-class-wc-admin-list-table.php';
}

/**
 * WC_Admin_List_Table_Coupons Class.
 */
class WC_Admin_List_Table_Coupons extends WC_Admin_List_Table {

	/**
	 * Post type.
	 *
	 * @var string
	 */
	protected $list_table_type = 'shop_coupon';

	/**
	 * Constructor.
	 */
	public function __construct() {
		parent::__construct();
		add_filter( 'disable_months_dropdown', '__return_true' );
	}

	/**
	 * Render blank state.
	 */
	protected function render_blank_state() {
		echo '<div class="woocommerce-BlankState">';
		echo '<h2 class="woocommerce-BlankState-message">' . esc_html__( 'Coupons are a great way to offer discounts and rewards to your customers. They will appear here once created.', 'woocommerce' ) . '</h2>';
		echo '<a class="woocommerce-BlankState-cta button-primary button" href="' . esc_url( admin_url( 'post-new.php?post_type=shop_coupon' ) ) . '">' . esc_html__( 'Create your first coupon', 'woocommerce' ) . '</a>';
		echo '<a class="woocommerce-BlankState-cta button" target="_blank" href="https://docs.woocommerce.com/document/coupon-management/?utm_source=blankslate&utm_medium=product&utm_content=couponsdoc&utm_campaign=woocommerceplugin">' . esc_html__( 'Learn more about coupons', 'woocommerce' ) . '</a>';
		echo '</div>';
	}

	/**
	 * Define primary column.
	 *
	 * @return string
	 */
	protected function get_primary_column() {
		return 'coupon_code';
	}

	/**
	 * Get row actions to show in the list table.
	 *
	 * @param array   $actions Array of actions.
	 * @param WP_Post $post Current post object.
	 * @return array
	 */
	protected function get_row_actions( $actions, $post ) {
		unset( $actions['inline hide-if-no-js'] );
		return $actions;
	}

	/**
	 * Define which columns to show on this screen.
	 *
	 * @param array $columns Existing columns.
	 * @return array
	 */
	public function define_columns( $columns ) {
		$show_columns                = array();
		$show_columns['cb']          = $columns['cb'];
		$show_columns['coupon_code'] = __( 'Code', 'woocommerce' );
		$show_columns['type']        = __( 'Coupon type', 'woocommerce' );
		$show_columns['amount']      = __( 'Coupon amount', 'woocommerce' );
		$show_columns['description'] = __( 'Description', 'woocommerce' );
		$show_columns['products']    = __( 'Product IDs', 'woocommerce' );
		$show_columns['usage']       = __( 'Usage / Limit', 'woocommerce' );
		$show_columns['expiry_date'] = __( 'Expiry date', 'woocommerce' );

		return $show_columns;
	}

	/**
	 * Pre-fetch any data for the row each column has access to it. the_coupon global is there for bw compat.
	 *
	 * @param int $post_id Post ID being shown.
	 */
	protected function prepare_row_data( $post_id ) {
		global $the_coupon;

		if ( empty( $this->object ) || $this->object->get_id() !== $post_id ) {
			$this->object = new WC_Coupon( $post_id );
			$the_coupon   = $this->object;
		}
	}

	/**
	 * Render columm: coupon_code.
	 */
	protected function render_coupon_code_column() {
		global $post;

		$edit_link = get_edit_post_link( $this->object->get_id() );
		$title     = $this->object->get_code();

		echo '<strong><a class="row-title" href="' . esc_url( $edit_link ) . '">' . esc_html( $title ) . '</a>';
		_post_states( $post );
		echo '</strong>';
	}

	/**
	 * Render columm: type.
	 */
	protected function render_type_column() {
		echo esc_html( wc_get_coupon_type( $this->object->get_discount_type() ) );
	}

	/**
	 * Render columm: amount.
	 */
	protected function render_amount_column() {
		echo esc_html( wc_format_localized_price( $this->object->get_amount() ) );
	}
	/**
	 * Render columm: products.
	 */
	protected function render_products_column() {
		$product_ids = $this->object->get_product_ids();

		if ( count( $product_ids ) > 0 ) {
			echo esc_html( implode( ', ', $product_ids ) );
		} else {
			echo '&ndash;';
		}
	}

	/**
	 * Render columm: usage_limit.
	 */
	protected function render_usage_limit_column() {
		$usage_limit = $this->object->get_usage_limit();

		if ( $usage_limit ) {
			echo esc_html( $usage_limit );
		} else {
			echo '&ndash;';
		}
	}

	/**
	 * Render columm: usage.
	 */
	protected function render_usage_column() {
		$usage_count = $this->object->get_usage_count();
		$usage_limit = $this->object->get_usage_limit();

		printf(
			/* translators: 1: count 2: limit */
			__( '%1$s / %2$s', 'woocommerce' ),
			esc_html( $usage_count ),
			$usage_limit ? esc_html( $usage_limit ) : '&infin;'
		);
	}

	/**
	 * Render columm: expiry_date.
	 */
	protected function render_expiry_date_column() {
		$expiry_date = $this->object->get_date_expires();

		if ( $expiry_date ) {
			echo esc_html( $expiry_date->date_i18n( 'F j, Y' ) );
		} else {
			echo '&ndash;';
		}
	}

	/**
	 * Render columm: description.
	 */
	protected function render_description_column() {
		echo wp_kses_post( $this->object->get_description() ? $this->object->get_description() : '&ndash;' );
	}

	/**
	 * Render any custom filters and search inputs for the list table.
	 */
	protected function render_filters() {
		?>
		<select name="coupon_type" id="dropdown_shop_coupon_type">
			<option value=""><?php esc_html_e( 'Show all types', 'woocommerce' ); ?></option>
			<?php
			$types = wc_get_coupon_types();

			foreach ( $types as $name => $type ) {
				echo '<option value="' . esc_attr( $name ) . '"';

				if ( isset( $_GET['coupon_type'] ) ) { // WPCS: input var ok.
					selected( $name, wc_clean( wp_unslash( $_GET['coupon_type'] ) ) ); // WPCS: input var ok, sanitization ok.
				}

				echo '>' . esc_html( $type ) . '</option>';
			}
			?>
		</select>
		<?php
	}

	/**
	 * Handle any custom filters.
	 *
	 * @param array $query_vars Query vars.
	 * @return array
	 */
	protected function query_filters( $query_vars ) {
		if ( ! empty( $_GET['coupon_type'] ) ) { // WPCS: input var ok, sanitization ok.
			$query_vars['meta_key']   = 'discount_type'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
			$query_vars['meta_value'] = wc_clean( wp_unslash( $_GET['coupon_type'] ) ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value, WordPress.VIP.SuperGlobalInputUsage.AccessDetected
		}
		return $query_vars;
	}
}
admin/list-tables/class-wc-admin-list-table-products.php000064400000056212151336044770017311 0ustar00<?php
/**
 * List tables: products.
 *
 * @package  WooCommerce\Admin
 * @version  3.3.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( class_exists( 'WC_Admin_List_Table_Products', false ) ) {
	return;
}

if ( ! class_exists( 'WC_Admin_List_Table', false ) ) {
	include_once __DIR__ . '/abstract-class-wc-admin-list-table.php';
}

/**
 * WC_Admin_List_Table_Products Class.
 */
class WC_Admin_List_Table_Products extends WC_Admin_List_Table {

	/**
	 * Post type.
	 *
	 * @var string
	 */
	protected $list_table_type = 'product';

	/**
	 * Constructor.
	 */
	public function __construct() {
		parent::__construct();
		add_filter( 'disable_months_dropdown', '__return_true' );
		add_filter( 'query_vars', array( $this, 'add_custom_query_var' ) );
		add_filter( 'views_edit-product', array( $this, 'product_views' ) );
		add_filter( 'get_search_query', array( $this, 'search_label' ) );
		add_filter( 'posts_clauses', array( $this, 'posts_clauses' ), 10, 2 );
	}

	/**
	 * Render blank state.
	 */
	protected function render_blank_state() {
		echo '<div class="woocommerce-BlankState">';

		echo '<h2 class="woocommerce-BlankState-message">' . esc_html__( 'Ready to start selling something awesome?', 'woocommerce' ) . '</h2>';

		echo '<div class="woocommerce-BlankState-buttons">';

		echo '<a class="woocommerce-BlankState-cta button-primary button" href="' . esc_url( admin_url( 'post-new.php?post_type=product&tutorial=true' ) ) . '">' . esc_html__( 'Create Product', 'woocommerce' ) . '</a>';
		echo '<a class="woocommerce-BlankState-cta button" href="' . esc_url( admin_url( 'edit.php?post_type=product&page=product_importer' ) ) . '">' . esc_html__( 'Start Import', 'woocommerce' ) . '</a>';

		echo '</div>';

		do_action( 'wc_marketplace_suggestions_products_empty_state' );

		echo '</div>';
	}

	/**
	 * Define primary column.
	 *
	 * @return string
	 */
	protected function get_primary_column() {
		return 'name';
	}

	/**
	 * Get row actions to show in the list table.
	 *
	 * @param array   $actions Array of actions.
	 * @param WP_Post $post Current post object.
	 * @return array
	 */
	protected function get_row_actions( $actions, $post ) {
		/* translators: %d: product ID. */
		return array_merge( array( 'id' => sprintf( __( 'ID: %d', 'woocommerce' ), $post->ID ) ), $actions );
	}

	/**
	 * Define which columns are sortable.
	 *
	 * @param array $columns Existing columns.
	 * @return array
	 */
	public function define_sortable_columns( $columns ) {
		$custom = array(
			'price' => 'price',
			'sku'   => 'sku',
			'name'  => 'title',
		);
		return wp_parse_args( $custom, $columns );
	}

	/**
	 * Define which columns to show on this screen.
	 *
	 * @param array $columns Existing columns.
	 * @return array
	 */
	public function define_columns( $columns ) {
		if ( empty( $columns ) && ! is_array( $columns ) ) {
			$columns = array();
		}

		unset( $columns['title'], $columns['comments'], $columns['date'] );

		$show_columns          = array();
		$show_columns['cb']    = '<input type="checkbox" />';
		$show_columns['thumb'] = '<span class="wc-image tips" data-tip="' . esc_attr__( 'Image', 'woocommerce' ) . '">' . __( 'Image', 'woocommerce' ) . '</span>';
		$show_columns['name']  = __( 'Name', 'woocommerce' );

		if ( wc_product_sku_enabled() ) {
			$show_columns['sku'] = __( 'SKU', 'woocommerce' );
		}

		if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
			$show_columns['is_in_stock'] = __( 'Stock', 'woocommerce' );
		}

		$show_columns['price']       = __( 'Price', 'woocommerce' );
		$show_columns['product_cat'] = __( 'Categories', 'woocommerce' );
		$show_columns['product_tag'] = __( 'Tags', 'woocommerce' );
		$show_columns['featured']    = '<span class="wc-featured parent-tips" data-tip="' . esc_attr__( 'Featured', 'woocommerce' ) . '">' . __( 'Featured', 'woocommerce' ) . '</span>';
		$show_columns['date']        = __( 'Date', 'woocommerce' );

		return array_merge( $show_columns, $columns );
	}

	/**
	 * Pre-fetch any data for the row each column has access to it. the_product global is there for bw compat.
	 *
	 * @param int $post_id Post ID being shown.
	 */
	protected function prepare_row_data( $post_id ) {
		global $the_product;

		if ( empty( $this->object ) || $this->object->get_id() !== $post_id ) {
			$the_product  = wc_get_product( $post_id );
			$this->object = $the_product;
		}
	}

	/**
	 * Render column: thumb.
	 */
	protected function render_thumb_column() {
		echo '<a href="' . esc_url( get_edit_post_link( $this->object->get_id() ) ) . '">' . $this->object->get_image( 'thumbnail' ) . '</a>'; // WPCS: XSS ok.
	}

	/**
	 * Render column: name.
	 */
	protected function render_name_column() {
		global $post;

		$edit_link = get_edit_post_link( $this->object->get_id() );
		$title     = _draft_or_post_title();

		echo '<strong><a class="row-title" href="' . esc_url( $edit_link ) . '">' . esc_html( $title ) . '</a>';

		_post_states( $post );

		echo '</strong>';

		if ( $this->object->get_parent_id() > 0 ) {
			echo '&nbsp;&nbsp;&larr; <a href="' . esc_url( get_edit_post_link( $this->object->get_parent_id() ) ) . '">' . get_the_title( $this->object->get_parent_id() ) . '</a>'; // @codingStandardsIgnoreLine.
		}

		get_inline_data( $post );

		/* Custom inline data for woocommerce. */
		echo '
			<div class="hidden" id="woocommerce_inline_' . absint( $this->object->get_id() ) . '">
				<div class="menu_order">' . esc_html( $this->object->get_menu_order() ) . '</div>
				<div class="sku">' . esc_html( $this->object->get_sku() ) . '</div>
				<div class="regular_price">' . esc_html( $this->object->get_regular_price() ) . '</div>
				<div class="sale_price">' . esc_html( $this->object->get_sale_price() ) . '</div>
				<div class="weight">' . esc_html( $this->object->get_weight() ) . '</div>
				<div class="length">' . esc_html( $this->object->get_length() ) . '</div>
				<div class="width">' . esc_html( $this->object->get_width() ) . '</div>
				<div class="height">' . esc_html( $this->object->get_height() ) . '</div>
				<div class="shipping_class">' . esc_html( $this->object->get_shipping_class() ) . '</div>
				<div class="visibility">' . esc_html( $this->object->get_catalog_visibility() ) . '</div>
				<div class="stock_status">' . esc_html( $this->object->get_stock_status() ) . '</div>
				<div class="stock">' . esc_html( $this->object->get_stock_quantity() ) . '</div>
				<div class="manage_stock">' . esc_html( wc_bool_to_string( $this->object->get_manage_stock() ) ) . '</div>
				<div class="featured">' . esc_html( wc_bool_to_string( $this->object->get_featured() ) ) . '</div>
				<div class="product_type">' . esc_html( $this->object->get_type() ) . '</div>
				<div class="product_is_virtual">' . esc_html( wc_bool_to_string( $this->object->get_virtual() ) ) . '</div>
				<div class="tax_status">' . esc_html( $this->object->get_tax_status() ) . '</div>
				<div class="tax_class">' . esc_html( $this->object->get_tax_class() ) . '</div>
				<div class="backorders">' . esc_html( $this->object->get_backorders() ) . '</div>
				<div class="low_stock_amount">' . esc_html( $this->object->get_low_stock_amount() ) . '</div>
			</div>
		';
	}

	/**
	 * Render column: sku.
	 */
	protected function render_sku_column() {
		echo $this->object->get_sku() ? esc_html( $this->object->get_sku() ) : '<span class="na">&ndash;</span>';
	}

	/**
	 * Render column: price.
	 */
	protected function render_price_column() {
		echo $this->object->get_price_html() ? wp_kses_post( $this->object->get_price_html() ) : '<span class="na">&ndash;</span>';
	}

	/**
	 * Render column: product_cat.
	 */
	protected function render_product_cat_column() {
		$terms = get_the_terms( $this->object->get_id(), 'product_cat' );
		if ( ! $terms ) {
			echo '<span class="na">&ndash;</span>';
		} else {
			$termlist = array();
			foreach ( $terms as $term ) {
				$termlist[] = '<a href="' . esc_url( admin_url( 'edit.php?product_cat=' . $term->slug . '&post_type=product' ) ) . ' ">' . esc_html( $term->name ) . '</a>';
			}

			echo apply_filters( 'woocommerce_admin_product_term_list', implode( ', ', $termlist ), 'product_cat', $this->object->get_id(), $termlist, $terms ); // WPCS: XSS ok.
		}
	}

	/**
	 * Render column: product_tag.
	 */
	protected function render_product_tag_column() {
		$terms = get_the_terms( $this->object->get_id(), 'product_tag' );
		if ( ! $terms ) {
			echo '<span class="na">&ndash;</span>';
		} else {
			$termlist = array();
			foreach ( $terms as $term ) {
				$termlist[] = '<a href="' . esc_url( admin_url( 'edit.php?product_tag=' . $term->slug . '&post_type=product' ) ) . ' ">' . esc_html( $term->name ) . '</a>';
			}

			echo apply_filters( 'woocommerce_admin_product_term_list', implode( ', ', $termlist ), 'product_tag', $this->object->get_id(), $termlist, $terms ); // WPCS: XSS ok.
		}
	}

	/**
	 * Render column: featured.
	 */
	protected function render_featured_column() {
		$url = wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_feature_product&product_id=' . $this->object->get_id() ), 'woocommerce-feature-product' );
		echo '<a href="' . esc_url( $url ) . '" aria-label="' . esc_attr__( 'Toggle featured', 'woocommerce' ) . '">';
		if ( $this->object->is_featured() ) {
			echo '<span class="wc-featured tips" data-tip="' . esc_attr__( 'Yes', 'woocommerce' ) . '">' . esc_html__( 'Yes', 'woocommerce' ) . '</span>';
		} else {
			echo '<span class="wc-featured not-featured tips" data-tip="' . esc_attr__( 'No', 'woocommerce' ) . '">' . esc_html__( 'No', 'woocommerce' ) . '</span>';
		}
		echo '</a>';
	}

	/**
	 * Render column: is_in_stock.
	 */
	protected function render_is_in_stock_column() {
		if ( $this->object->is_on_backorder() ) {
			$stock_html = '<mark class="onbackorder">' . __( 'On backorder', 'woocommerce' ) . '</mark>';
		} elseif ( $this->object->is_in_stock() ) {
			$stock_html = '<mark class="instock">' . __( 'In stock', 'woocommerce' ) . '</mark>';
		} else {
			$stock_html = '<mark class="outofstock">' . __( 'Out of stock', 'woocommerce' ) . '</mark>';
		}

		if ( $this->object->managing_stock() ) {
			$stock_html .= ' (' . wc_stock_amount( $this->object->get_stock_quantity() ) . ')';
		}

		echo wp_kses_post( apply_filters( 'woocommerce_admin_stock_html', $stock_html, $this->object ) );
	}

	/**
	 * Query vars for custom searches.
	 *
	 * @param mixed $public_query_vars Array of query vars.
	 * @return array
	 */
	public function add_custom_query_var( $public_query_vars ) {
		$public_query_vars[] = 'sku';
		return $public_query_vars;
	}

	/**
	 * Render any custom filters and search inputs for the list table.
	 */
	protected function render_filters() {
		$filters = apply_filters(
			'woocommerce_products_admin_list_table_filters',
			array(
				'product_category' => array( $this, 'render_products_category_filter' ),
				'product_type'     => array( $this, 'render_products_type_filter' ),
				'stock_status'     => array( $this, 'render_products_stock_status_filter' ),
			)
		);

		ob_start();
		foreach ( $filters as $filter_callback ) {
			call_user_func( $filter_callback );
		}
		$output = ob_get_clean();

		echo apply_filters( 'woocommerce_product_filters', $output ); // WPCS: XSS ok.
	}

	/**
	 * Render the product category filter for the list table.
	 *
	 * @since 3.5.0
	 */
	protected function render_products_category_filter() {
		$categories_count = (int) wp_count_terms( 'product_cat' );

		if ( $categories_count <= apply_filters( 'woocommerce_product_category_filter_threshold', 100 ) ) {
			wc_product_dropdown_categories(
				array(
					'option_select_text' => __( 'Filter by category', 'woocommerce' ),
					'hide_empty'         => 0,
				)
			);
		} else {
			$current_category_slug = isset( $_GET['product_cat'] ) ? wc_clean( wp_unslash( $_GET['product_cat'] ) ) : false; // WPCS: input var ok, CSRF ok.
			$current_category      = $current_category_slug ? get_term_by( 'slug', $current_category_slug, 'product_cat' ) : false;
			?>
			<select class="wc-category-search" name="product_cat" data-placeholder="<?php esc_attr_e( 'Filter by category', 'woocommerce' ); ?>" data-allow_clear="true">
				<?php if ( $current_category_slug && $current_category ) : ?>
					<option value="<?php echo esc_attr( $current_category_slug ); ?>" selected="selected"><?php echo esc_html( htmlspecialchars( wp_kses_post( $current_category->name ) ) ); ?></option>
				<?php endif; ?>
			</select>
			<?php
		}
	}

	/**
	 * Render the product type filter for the list table.
	 *
	 * @since 3.5.0
	 */
	protected function render_products_type_filter() {
		$current_product_type = isset( $_REQUEST['product_type'] ) ? wc_clean( wp_unslash( $_REQUEST['product_type'] ) ) : false; // WPCS: input var ok, sanitization ok.
		$output               = '<select name="product_type" id="dropdown_product_type"><option value="">' . esc_html__( 'Filter by product type', 'woocommerce' ) . '</option>';

		foreach ( wc_get_product_types() as $value => $label ) {
			$output .= '<option value="' . esc_attr( $value ) . '" ';
			$output .= selected( $value, $current_product_type, false );
			$output .= '>' . esc_html( $label ) . '</option>';

			if ( 'simple' === $value ) {

				$output .= '<option value="downloadable" ';
				$output .= selected( 'downloadable', $current_product_type, false );
				$output .= '> ' . ( is_rtl() ? '&larr;' : '&rarr;' ) . ' ' . esc_html__( 'Downloadable', 'woocommerce' ) . '</option>';

				$output .= '<option value="virtual" ';
				$output .= selected( 'virtual', $current_product_type, false );
				$output .= '> ' . ( is_rtl() ? '&larr;' : '&rarr;' ) . ' ' . esc_html__( 'Virtual', 'woocommerce' ) . '</option>';
			}
		}

		$output .= '</select>';
		echo $output; // WPCS: XSS ok.
	}

	/**
	 * Render the stock status filter for the list table.
	 *
	 * @since 3.5.0
	 */
	public function render_products_stock_status_filter() {
		$current_stock_status = isset( $_REQUEST['stock_status'] ) ? wc_clean( wp_unslash( $_REQUEST['stock_status'] ) ) : false; // WPCS: input var ok, sanitization ok.
		$stock_statuses       = wc_get_product_stock_status_options();
		$output               = '<select name="stock_status"><option value="">' . esc_html__( 'Filter by stock status', 'woocommerce' ) . '</option>';

		foreach ( $stock_statuses as $status => $label ) {
			$output .= '<option ' . selected( $status, $current_stock_status, false ) . ' value="' . esc_attr( $status ) . '">' . esc_html( $label ) . '</option>';
		}

		$output .= '</select>';
		echo $output; // WPCS: XSS ok.
	}

	/**
	 * Search by SKU or ID for products.
	 *
	 * @deprecated 4.4.0 Logic moved to query_filters.
	 * @param string $where Where clause SQL.
	 * @return string
	 */
	public function sku_search( $where ) {
		wc_deprecated_function( 'WC_Admin_List_Table_Products::sku_search', '4.4.0', 'Logic moved to query_filters.' );
		return $where;
	}

	/**
	 * Change views on the edit product screen.
	 *
	 * @param  array $views Array of views.
	 * @return array
	 */
	public function product_views( $views ) {
		global $wp_query;

		// Products do not have authors.
		unset( $views['mine'] );

		// Add sorting link.
		if ( current_user_can( 'edit_others_products' ) ) {
			$class            = ( isset( $wp_query->query['orderby'] ) && 'menu_order title' === $wp_query->query['orderby'] ) ? 'current' : '';
			$query_string     = remove_query_arg( array( 'orderby', 'order' ) );
			$query_string     = add_query_arg( 'orderby', rawurlencode( 'menu_order title' ), $query_string );
			$query_string     = add_query_arg( 'order', rawurlencode( 'ASC' ), $query_string );
			$views['byorder'] = '<a href="' . esc_url( $query_string ) . '" class="' . esc_attr( $class ) . '">' . __( 'Sorting', 'woocommerce' ) . '</a>';
		}

		return $views;
	}

	/**
	 * Change the label when searching products
	 *
	 * @param string $query Search Query.
	 * @return string
	 */
	public function search_label( $query ) {
		global $pagenow, $typenow;

		if ( 'edit.php' !== $pagenow || 'product' !== $typenow || ! get_query_var( 'product_search' ) || ! isset( $_GET['s'] ) ) { // WPCS: input var ok.
			return $query;
		}

		return wc_clean( wp_unslash( $_GET['s'] ) ); // WPCS: input var ok, sanitization ok.
	}

	/**
	 * Handle any custom filters.
	 *
	 * @param array $query_vars Query vars.
	 * @return array
	 */
	protected function query_filters( $query_vars ) {
		$this->remove_ordering_args();
		// Custom order by arguments.
		if ( isset( $query_vars['orderby'] ) ) {
			$orderby = strtolower( $query_vars['orderby'] );
			$order   = isset( $query_vars['order'] ) ? strtoupper( $query_vars['order'] ) : 'DESC';

			if ( 'price' === $orderby ) {
				$callback = 'DESC' === $order ? 'order_by_price_desc_post_clauses' : 'order_by_price_asc_post_clauses';
				add_filter( 'posts_clauses', array( $this, $callback ) );
			}

			if ( 'sku' === $orderby ) {
				$callback = 'DESC' === $order ? 'order_by_sku_desc_post_clauses' : 'order_by_sku_asc_post_clauses';
				add_filter( 'posts_clauses', array( $this, $callback ) );
			}
		}

		// Type filtering.
		if ( isset( $query_vars['product_type'] ) ) {
			if ( 'downloadable' === $query_vars['product_type'] ) {
				$query_vars['product_type'] = '';
				add_filter( 'posts_clauses', array( $this, 'filter_downloadable_post_clauses' ) );
			} elseif ( 'virtual' === $query_vars['product_type'] ) {
				$query_vars['product_type'] = '';
				add_filter( 'posts_clauses', array( $this, 'filter_virtual_post_clauses' ) );
			}
		}

		// Stock status filter.
		if ( ! empty( $_GET['stock_status'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			add_filter( 'posts_clauses', array( $this, 'filter_stock_status_post_clauses' ) );
		}

		// Shipping class taxonomy.
		if ( ! empty( $_GET['product_shipping_class'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$query_vars['tax_query'][] = array(
				'taxonomy' => 'product_shipping_class',
				'field'    => 'slug',
				'terms'    => sanitize_title( wp_unslash( $_GET['product_shipping_class'] ) ),
				'operator' => 'IN',
			);
		}

		// Search using CRUD.
		if ( ! empty( $query_vars['s'] ) ) {
			$data_store                   = WC_Data_Store::load( 'product' );
			$ids                          = $data_store->search_products( wc_clean( wp_unslash( $query_vars['s'] ) ), '', true, true );
			$query_vars['post__in']       = array_merge( $ids, array( 0 ) );
			$query_vars['product_search'] = true;
			unset( $query_vars['s'] );
		}

		return $query_vars;
	}

	/**
	 * Undocumented function
	 *
	 * @param array    $args  Array of SELECT statement pieces (from, where, etc).
	 * @param WP_Query $query WP_Query instance.
	 * @return array
	 */
	public function posts_clauses( $args, $query ) {

		return $args;
	}

	/**
	 * Remove ordering queries.
	 *
	 * @param array $posts Posts array, keeping this for backwards compatibility defaulting to empty array.
	 * @return array
	 */
	public function remove_ordering_args( $posts = array() ) {
		remove_filter( 'posts_clauses', array( $this, 'order_by_price_asc_post_clauses' ) );
		remove_filter( 'posts_clauses', array( $this, 'order_by_price_desc_post_clauses' ) );
		remove_filter( 'posts_clauses', array( $this, 'order_by_sku_asc_post_clauses' ) );
		remove_filter( 'posts_clauses', array( $this, 'order_by_sku_desc_post_clauses' ) );
		remove_filter( 'posts_clauses', array( $this, 'filter_downloadable_post_clauses' ) );
		remove_filter( 'posts_clauses', array( $this, 'filter_virtual_post_clauses' ) );
		remove_filter( 'posts_clauses', array( $this, 'filter_stock_status_post_clauses' ) );
		return $posts; // Keeping this here for backward compatibility.
	}

	/**
	 * Handle numeric price sorting.
	 *
	 * @param array $args Query args.
	 * @return array
	 */
	public function order_by_price_asc_post_clauses( $args ) {
		$args['join']    = $this->append_product_sorting_table_join( $args['join'] );
		$args['orderby'] = ' wc_product_meta_lookup.min_price ASC, wc_product_meta_lookup.product_id ASC ';
		return $args;
	}

	/**
	 * Handle numeric price sorting.
	 *
	 * @param array $args Query args.
	 * @return array
	 */
	public function order_by_price_desc_post_clauses( $args ) {
		$args['join']    = $this->append_product_sorting_table_join( $args['join'] );
		$args['orderby'] = ' wc_product_meta_lookup.max_price DESC, wc_product_meta_lookup.product_id DESC ';
		return $args;
	}

	/**
	 * Handle sku sorting.
	 *
	 * @param array $args Query args.
	 * @return array
	 */
	public function order_by_sku_asc_post_clauses( $args ) {
		$args['join']    = $this->append_product_sorting_table_join( $args['join'] );
		$args['orderby'] = ' wc_product_meta_lookup.sku ASC, wc_product_meta_lookup.product_id ASC ';
		return $args;
	}

	/**
	 * Handle sku sorting.
	 *
	 * @param array $args Query args.
	 * @return array
	 */
	public function order_by_sku_desc_post_clauses( $args ) {
		$args['join']    = $this->append_product_sorting_table_join( $args['join'] );
		$args['orderby'] = ' wc_product_meta_lookup.sku DESC, wc_product_meta_lookup.product_id DESC ';
		return $args;
	}

	/**
	 * Filter by type.
	 *
	 * @param array $args Query args.
	 * @return array
	 */
	public function filter_downloadable_post_clauses( $args ) {
		$args['join']   = $this->append_product_sorting_table_join( $args['join'] );
		$args['where'] .= ' AND wc_product_meta_lookup.downloadable=1 ';
		return $args;
	}

	/**
	 * Filter by type.
	 *
	 * @param array $args Query args.
	 * @return array
	 */
	public function filter_virtual_post_clauses( $args ) {
		$args['join']   = $this->append_product_sorting_table_join( $args['join'] );
		$args['where'] .= ' AND wc_product_meta_lookup.virtual=1 ';
		return $args;
	}

	/**
	 * Filter by stock status.
	 *
	 * @param array $args Query args.
	 * @return array
	 */
	public function filter_stock_status_post_clauses( $args ) {
		global $wpdb;
		if ( ! empty( $_GET['stock_status'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$args['join']   = $this->append_product_sorting_table_join( $args['join'] );
			$args['where'] .= $wpdb->prepare( ' AND wc_product_meta_lookup.stock_status=%s ', wc_clean( wp_unslash( $_GET['stock_status'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		}
		return $args;
	}

	/**
	 * Join wc_product_meta_lookup to posts if not already joined.
	 *
	 * @param string $sql SQL join.
	 * @return string
	 */
	private function append_product_sorting_table_join( $sql ) {
		global $wpdb;

		if ( ! strstr( $sql, 'wc_product_meta_lookup' ) ) {
			$sql .= " LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON $wpdb->posts.ID = wc_product_meta_lookup.product_id ";
		}
		return $sql;
	}

	/**
	 * Modifies post query so that it includes parent products whose variations have particular shipping class assigned.
	 *
	 * @param array    $pieces   Array of SELECT statement pieces (from, where, etc).
	 * @param WP_Query $wp_query WP_Query instance.
	 * @return array             Array of products, including parents of variations.
	 */
	public function add_variation_parents_for_shipping_class( $pieces, $wp_query ) {
		global $wpdb;
		if ( isset( $_GET['product_shipping_class'] ) && '0' !== $_GET['product_shipping_class'] ) { // WPCS: input var ok.
			$replaced_where   = str_replace( ".post_type = 'product'", ".post_type = 'product_variation'", $pieces['where'] );
			$pieces['where'] .= " OR {$wpdb->posts}.ID in (
				SELECT {$wpdb->posts}.post_parent FROM
				{$wpdb->posts} LEFT JOIN {$wpdb->term_relationships} ON ({$wpdb->posts}.ID = {$wpdb->term_relationships}.object_id)
				WHERE 1=1 $replaced_where
			)";
			return $pieces;
		}
		return $pieces;
	}

}
admin/list-tables/abstract-class-wc-admin-list-table.php000064400000015371151336044770017252 0ustar00<?php
/**
 * List tables.
 *
 * @package  WooCommerce\Admin
 * @version  3.3.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( class_exists( 'WC_Admin_List_Table', false ) ) {
	return;
}

/**
 * WC_Admin_List_Table Class.
 */
abstract class WC_Admin_List_Table {

	/**
	 * Post type.
	 *
	 * @var string
	 */
	protected $list_table_type = '';

	/**
	 * Object being shown on the row.
	 *
	 * @var object|null
	 */
	protected $object = null;

	/**
	 * Constructor.
	 */
	public function __construct() {
		if ( $this->list_table_type ) {
			add_action( 'manage_posts_extra_tablenav', array( $this, 'maybe_render_blank_state' ) );
			add_filter( 'view_mode_post_types', array( $this, 'disable_view_mode' ) );
			add_action( 'restrict_manage_posts', array( $this, 'restrict_manage_posts' ) );
			add_filter( 'request', array( $this, 'request_query' ) );
			add_filter( 'post_row_actions', array( $this, 'row_actions' ), 100, 2 );
			add_filter( 'default_hidden_columns', array( $this, 'default_hidden_columns' ), 10, 2 );
			add_filter( 'list_table_primary_column', array( $this, 'list_table_primary_column' ), 10, 2 );
			add_filter( 'manage_edit-' . $this->list_table_type . '_sortable_columns', array( $this, 'define_sortable_columns' ) );
			add_filter( 'manage_' . $this->list_table_type . '_posts_columns', array( $this, 'define_columns' ) );
			add_filter( 'bulk_actions-edit-' . $this->list_table_type, array( $this, 'define_bulk_actions' ) );
			add_action( 'manage_' . $this->list_table_type . '_posts_custom_column', array( $this, 'render_columns' ), 10, 2 );
			add_filter( 'handle_bulk_actions-edit-' . $this->list_table_type, array( $this, 'handle_bulk_actions' ), 10, 3 );
		}
	}

	/**
	 * Show blank slate.
	 *
	 * @param string $which String which tablenav is being shown.
	 */
	public function maybe_render_blank_state( $which ) {
		global $post_type;

		if ( $post_type === $this->list_table_type && 'bottom' === $which ) {
			$counts = (array) wp_count_posts( $post_type );
			unset( $counts['auto-draft'] );
			$count = array_sum( $counts );

			if ( 0 < $count ) {
				return;
			}

			$this->render_blank_state();

			echo '<style type="text/css">#posts-filter .wp-list-table, #posts-filter .tablenav.top, .tablenav.bottom .actions, .wrap .subsubsub  { display: none; } #posts-filter .tablenav.bottom { height: auto; } </style>';
		}
	}

	/**
	 * Render blank state. Extend to add content.
	 */
	protected function render_blank_state() {}

	/**
	 * Removes this type from list of post types that support "View Mode" switching.
	 * View mode is seen on posts where you can switch between list or excerpt. Our post types don't support
	 * it, so we want to hide the useless UI from the screen options tab.
	 *
	 * @param  array $post_types Array of post types supporting view mode.
	 * @return array             Array of post types supporting view mode, without this type.
	 */
	public function disable_view_mode( $post_types ) {
		unset( $post_types[ $this->list_table_type ] );
		return $post_types;
	}

	/**
	 * See if we should render search filters or not.
	 */
	public function restrict_manage_posts() {
		global $typenow;

		if ( $this->list_table_type === $typenow ) {
			$this->render_filters();
		}
	}

	/**
	 * Handle any filters.
	 *
	 * @param array $query_vars Query vars.
	 * @return array
	 */
	public function request_query( $query_vars ) {
		global $typenow;

		if ( $this->list_table_type === $typenow ) {
			return $this->query_filters( $query_vars );
		}

		return $query_vars;
	}

	/**
	 * Render any custom filters and search inputs for the list table.
	 */
	protected function render_filters() {}

	/**
	 * Handle any custom filters.
	 *
	 * @param array $query_vars Query vars.
	 * @return array
	 */
	protected function query_filters( $query_vars ) {
		return $query_vars;
	}

	/**
	 * Set row actions.
	 *
	 * @param array   $actions Array of actions.
	 * @param WP_Post $post Current post object.
	 * @return array
	 */
	public function row_actions( $actions, $post ) {
		if ( $this->list_table_type === $post->post_type ) {
			return $this->get_row_actions( $actions, $post );
		}
		return $actions;
	}

	/**
	 * Get row actions to show in the list table.
	 *
	 * @param array   $actions Array of actions.
	 * @param WP_Post $post Current post object.
	 * @return array
	 */
	protected function get_row_actions( $actions, $post ) {
		return $actions;
	}

	/**
	 * Adjust which columns are displayed by default.
	 *
	 * @param array  $hidden Current hidden columns.
	 * @param object $screen Current screen.
	 * @return array
	 */
	public function default_hidden_columns( $hidden, $screen ) {
		if ( isset( $screen->id ) && 'edit-' . $this->list_table_type === $screen->id ) {
			$hidden = array_merge( $hidden, $this->define_hidden_columns() );
		}
		return $hidden;
	}

	/**
	 * Set list table primary column.
	 *
	 * @param  string $default Default value.
	 * @param  string $screen_id Current screen ID.
	 * @return string
	 */
	public function list_table_primary_column( $default, $screen_id ) {
		if ( 'edit-' . $this->list_table_type === $screen_id && $this->get_primary_column() ) {
			return $this->get_primary_column();
		}
		return $default;
	}

	/**
	 * Define primary column.
	 *
	 * @return array
	 */
	protected function get_primary_column() {
		return '';
	}

	/**
	 * Define hidden columns.
	 *
	 * @return array
	 */
	protected function define_hidden_columns() {
		return array();
	}

	/**
	 * Define which columns are sortable.
	 *
	 * @param array $columns Existing columns.
	 * @return array
	 */
	public function define_sortable_columns( $columns ) {
		return $columns;
	}

	/**
	 * Define which columns to show on this screen.
	 *
	 * @param array $columns Existing columns.
	 * @return array
	 */
	public function define_columns( $columns ) {
		return $columns;
	}

	/**
	 * Define bulk actions.
	 *
	 * @param array $actions Existing actions.
	 * @return array
	 */
	public function define_bulk_actions( $actions ) {
		return $actions;
	}

	/**
	 * Pre-fetch any data for the row each column has access to it.
	 *
	 * @param int $post_id Post ID being shown.
	 */
	protected function prepare_row_data( $post_id ) {}

	/**
	 * Render individual columns.
	 *
	 * @param string $column Column ID to render.
	 * @param int    $post_id Post ID being shown.
	 */
	public function render_columns( $column, $post_id ) {
		$this->prepare_row_data( $post_id );

		if ( ! $this->object ) {
			return;
		}

		if ( is_callable( array( $this, 'render_' . $column . '_column' ) ) ) {
			$this->{"render_{$column}_column"}();
		}
	}

	/**
	 * Handle bulk actions.
	 *
	 * @param  string $redirect_to URL to redirect to.
	 * @param  string $action      Action name.
	 * @param  array  $ids         List of ids.
	 * @return string
	 */
	public function handle_bulk_actions( $redirect_to, $action, $ids ) {
		return esc_url_raw( $redirect_to );
	}
}
admin/class-wc-admin-importers.php000064400000024134151336044770013207 0ustar00<?php
/**
 * Init WooCommerce data importers.
 *
 * @package WooCommerce\Admin
 */

use Automattic\Jetpack\Constants;

defined( 'ABSPATH' ) || exit;

/**
 * WC_Admin_Importers Class.
 */
class WC_Admin_Importers {

	/**
	 * Array of importer IDs.
	 *
	 * @var string[]
	 */
	protected $importers = array();

	/**
	 * Constructor.
	 */
	public function __construct() {
		if ( ! $this->import_allowed() ) {
			return;
		}

		add_action( 'admin_menu', array( $this, 'add_to_menus' ) );
		add_action( 'admin_init', array( $this, 'register_importers' ) );
		add_action( 'admin_head', array( $this, 'hide_from_menus' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'admin_scripts' ) );
		add_action( 'wp_ajax_woocommerce_do_ajax_product_import', array( $this, 'do_ajax_product_import' ) );

		// Register WooCommerce importers.
		$this->importers['product_importer'] = array(
			'menu'       => 'edit.php?post_type=product',
			'name'       => __( 'Product Import', 'woocommerce' ),
			'capability' => 'import',
			'callback'   => array( $this, 'product_importer' ),
		);
	}

	/**
	 * Return true if WooCommerce imports are allowed for current user, false otherwise.
	 *
	 * @return bool Whether current user can perform imports.
	 */
	protected function import_allowed() {
		return current_user_can( 'edit_products' ) && current_user_can( 'import' );
	}

	/**
	 * Add menu items for our custom importers.
	 */
	public function add_to_menus() {
		foreach ( $this->importers as $id => $importer ) {
			add_submenu_page( $importer['menu'], $importer['name'], $importer['name'], $importer['capability'], $id, $importer['callback'] );
		}
	}

	/**
	 * Hide menu items from view so the pages exist, but the menu items do not.
	 */
	public function hide_from_menus() {
		global $submenu;

		foreach ( $this->importers as $id => $importer ) {
			if ( isset( $submenu[ $importer['menu'] ] ) ) {
				foreach ( $submenu[ $importer['menu'] ] as $key => $menu ) {
					if ( $id === $menu[2] ) {
						unset( $submenu[ $importer['menu'] ][ $key ] );
					}
				}
			}
		}
	}

	/**
	 * Register importer scripts.
	 */
	public function admin_scripts() {
		$suffix  = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min';
		$version = Constants::get_constant( 'WC_VERSION' );
		wp_register_script( 'wc-product-import', WC()->plugin_url() . '/assets/js/admin/wc-product-import' . $suffix . '.js', array( 'jquery' ), $version, true );
	}

	/**
	 * The product importer.
	 *
	 * This has a custom screen - the Tools > Import item is a placeholder.
	 * If we're on that screen, redirect to the custom one.
	 */
	public function product_importer() {
		if ( Constants::is_defined( 'WP_LOAD_IMPORTERS' ) ) {
			wp_safe_redirect( admin_url( 'edit.php?post_type=product&page=product_importer' ) );
			exit;
		}

		include_once WC_ABSPATH . 'includes/import/class-wc-product-csv-importer.php';
		include_once WC_ABSPATH . 'includes/admin/importers/class-wc-product-csv-importer-controller.php';

		$importer = new WC_Product_CSV_Importer_Controller();
		$importer->dispatch();
	}

	/**
	 * Register WordPress based importers.
	 */
	public function register_importers() {
		if ( Constants::is_defined( 'WP_LOAD_IMPORTERS' ) ) {
			add_action( 'import_start', array( $this, 'post_importer_compatibility' ) );
			register_importer( 'woocommerce_product_csv', __( 'WooCommerce products (CSV)', 'woocommerce' ), __( 'Import <strong>products</strong> to your store via a csv file.', 'woocommerce' ), array( $this, 'product_importer' ) );
			register_importer( 'woocommerce_tax_rate_csv', __( 'WooCommerce tax rates (CSV)', 'woocommerce' ), __( 'Import <strong>tax rates</strong> to your store via a csv file.', 'woocommerce' ), array( $this, 'tax_rates_importer' ) );
		}
	}

	/**
	 * The tax rate importer which extends WP_Importer.
	 */
	public function tax_rates_importer() {
		require_once ABSPATH . 'wp-admin/includes/import.php';

		if ( ! class_exists( 'WP_Importer' ) ) {
			$class_wp_importer = ABSPATH . 'wp-admin/includes/class-wp-importer.php';

			if ( file_exists( $class_wp_importer ) ) {
				require $class_wp_importer;
			}
		}

		require dirname( __FILE__ ) . '/importers/class-wc-tax-rate-importer.php';

		$importer = new WC_Tax_Rate_Importer();
		$importer->dispatch();
	}

	/**
	 * When running the WP XML importer, ensure attributes exist.
	 *
	 * WordPress import should work - however, it fails to import custom product attribute taxonomies.
	 * This code grabs the file before it is imported and ensures the taxonomies are created.
	 */
	public function post_importer_compatibility() {
		global $wpdb;

		if ( empty( $_POST['import_id'] ) || ! class_exists( 'WXR_Parser' ) ) { // PHPCS: input var ok, CSRF ok.
			return;
		}

		$id          = absint( $_POST['import_id'] ); // PHPCS: input var ok.
		$file        = get_attached_file( $id );
		$parser      = new WXR_Parser();
		$import_data = $parser->parse( $file );

		if ( isset( $import_data['posts'] ) && ! empty( $import_data['posts'] ) ) {
			foreach ( $import_data['posts'] as $post ) {
				if ( 'product' === $post['post_type'] && ! empty( $post['terms'] ) ) {
					foreach ( $post['terms'] as $term ) {
						if ( strstr( $term['domain'], 'pa_' ) ) {
							if ( ! taxonomy_exists( $term['domain'] ) ) {
								$attribute_name = wc_attribute_taxonomy_slug( $term['domain'] );

								// Create the taxonomy.
								if ( ! in_array( $attribute_name, wc_get_attribute_taxonomies(), true ) ) {
									wc_create_attribute(
										array(
											'name'         => $attribute_name,
											'slug'         => $attribute_name,
											'type'         => 'select',
											'order_by'     => 'menu_order',
											'has_archives' => false,
										)
									);
								}

								// Register the taxonomy now so that the import works!
								register_taxonomy(
									$term['domain'],
									apply_filters( 'woocommerce_taxonomy_objects_' . $term['domain'], array( 'product' ) ),
									apply_filters(
										'woocommerce_taxonomy_args_' . $term['domain'],
										array(
											'hierarchical' => true,
											'show_ui'      => false,
											'query_var'    => true,
											'rewrite'      => false,
										)
									)
								);
							}
						}
					}
				}
			}
		}
	}

	/**
	 * Ajax callback for importing one batch of products from a CSV.
	 */
	public function do_ajax_product_import() {
		global $wpdb;

		check_ajax_referer( 'wc-product-import', 'security' );

		if ( ! $this->import_allowed() || ! isset( $_POST['file'] ) ) { // PHPCS: input var ok.
			wp_send_json_error( array( 'message' => __( 'Insufficient privileges to import products.', 'woocommerce' ) ) );
		}

		include_once WC_ABSPATH . 'includes/admin/importers/class-wc-product-csv-importer-controller.php';
		include_once WC_ABSPATH . 'includes/import/class-wc-product-csv-importer.php';

		$file   = wc_clean( wp_unslash( $_POST['file'] ) ); // PHPCS: input var ok.
		$params = array(
			'delimiter'       => ! empty( $_POST['delimiter'] ) ? wc_clean( wp_unslash( $_POST['delimiter'] ) ) : ',', // PHPCS: input var ok.
			'start_pos'       => isset( $_POST['position'] ) ? absint( $_POST['position'] ) : 0, // PHPCS: input var ok.
			'mapping'         => isset( $_POST['mapping'] ) ? (array) wc_clean( wp_unslash( $_POST['mapping'] ) ) : array(), // PHPCS: input var ok.
			'update_existing' => isset( $_POST['update_existing'] ) ? (bool) $_POST['update_existing'] : false, // PHPCS: input var ok.
			'lines'           => apply_filters( 'woocommerce_product_import_batch_size', 30 ),
			'parse'           => true,
		);

		// Log failures.
		if ( 0 !== $params['start_pos'] ) {
			$error_log = array_filter( (array) get_user_option( 'product_import_error_log' ) );
		} else {
			$error_log = array();
		}

		$importer         = WC_Product_CSV_Importer_Controller::get_importer( $file, $params );
		$results          = $importer->import();
		$percent_complete = $importer->get_percent_complete();
		$error_log        = array_merge( $error_log, $results['failed'], $results['skipped'] );

		update_user_option( get_current_user_id(), 'product_import_error_log', $error_log );

		if ( 100 === $percent_complete ) {
			// @codingStandardsIgnoreStart.
			$wpdb->delete( $wpdb->postmeta, array( 'meta_key' => '_original_id' ) );
			$wpdb->delete( $wpdb->posts, array(
				'post_type'   => 'product',
				'post_status' => 'importing',
			) );
			$wpdb->delete( $wpdb->posts, array(
				'post_type'   => 'product_variation',
				'post_status' => 'importing',
			) );
			// @codingStandardsIgnoreEnd.

			// Clean up orphaned data.
			$wpdb->query(
				"
				DELETE {$wpdb->posts}.* FROM {$wpdb->posts}
				LEFT JOIN {$wpdb->posts} wp ON wp.ID = {$wpdb->posts}.post_parent
				WHERE wp.ID IS NULL AND {$wpdb->posts}.post_type = 'product_variation'
			"
			);
			$wpdb->query(
				"
				DELETE {$wpdb->postmeta}.* FROM {$wpdb->postmeta}
				LEFT JOIN {$wpdb->posts} wp ON wp.ID = {$wpdb->postmeta}.post_id
				WHERE wp.ID IS NULL
			"
			);
			// @codingStandardsIgnoreStart.
			$wpdb->query( "
				DELETE tr.* FROM {$wpdb->term_relationships} tr
				LEFT JOIN {$wpdb->posts} wp ON wp.ID = tr.object_id
				LEFT JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
				WHERE wp.ID IS NULL
				AND tt.taxonomy IN ( '" . implode( "','", array_map( 'esc_sql', get_object_taxonomies( 'product' ) ) ) . "' )
			" );
			// @codingStandardsIgnoreEnd.

			// Send success.
			wp_send_json_success(
				array(
					'position'   => 'done',
					'percentage' => 100,
					'url'        => add_query_arg( array( '_wpnonce' => wp_create_nonce( 'woocommerce-csv-importer' ) ), admin_url( 'edit.php?post_type=product&page=product_importer&step=done' ) ),
					'imported'   => count( $results['imported'] ),
					'failed'     => count( $results['failed'] ),
					'updated'    => count( $results['updated'] ),
					'skipped'    => count( $results['skipped'] ),
				)
			);
		} else {
			wp_send_json_success(
				array(
					'position'   => $importer->get_file_position(),
					'percentage' => $percent_complete,
					'imported'   => count( $results['imported'] ),
					'failed'     => count( $results['failed'] ),
					'updated'    => count( $results['updated'] ),
					'skipped'    => count( $results['skipped'] ),
				)
			);
		}
	}
}

new WC_Admin_Importers();
admin/class-wc-admin-reports.php000064400000012350151336044770012656 0ustar00<?php
/**
 * Admin Reports
 *
 * Functions used for displaying sales and customer reports in admin.
 *
 * @author      WooThemes
 * @category    Admin
 * @package     WooCommerce\Admin\Reports
 * @version     2.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( class_exists( 'WC_Admin_Reports', false ) ) {
	return;
}

/**
 * WC_Admin_Reports Class.
 */
class WC_Admin_Reports {

	/**
	 * Handles output of the reports page in admin.
	 */
	public static function output() {
		$reports        = self::get_reports();
		$first_tab      = array_keys( $reports );
		$current_tab    = ! empty( $_GET['tab'] ) && array_key_exists( $_GET['tab'], $reports ) ? sanitize_title( $_GET['tab'] ) : $first_tab[0];
		$current_report = isset( $_GET['report'] ) ? sanitize_title( $_GET['report'] ) : current( array_keys( $reports[ $current_tab ]['reports'] ) );

		include_once dirname( __FILE__ ) . '/reports/class-wc-admin-report.php';
		include_once dirname( __FILE__ ) . '/views/html-admin-page-reports.php';
	}

	/**
	 * Returns the definitions for the reports to show in admin.
	 *
	 * @return array
	 */
	public static function get_reports() {
		$reports = array(
			'orders'    => array(
				'title'   => __( 'Orders', 'woocommerce' ),
				'reports' => array(
					'sales_by_date'     => array(
						'title'       => __( 'Sales by date', 'woocommerce' ),
						'description' => '',
						'hide_title'  => true,
						'callback'    => array( __CLASS__, 'get_report' ),
					),
					'sales_by_product'  => array(
						'title'       => __( 'Sales by product', 'woocommerce' ),
						'description' => '',
						'hide_title'  => true,
						'callback'    => array( __CLASS__, 'get_report' ),
					),
					'sales_by_category' => array(
						'title'       => __( 'Sales by category', 'woocommerce' ),
						'description' => '',
						'hide_title'  => true,
						'callback'    => array( __CLASS__, 'get_report' ),
					),
					'coupon_usage'      => array(
						'title'       => __( 'Coupons by date', 'woocommerce' ),
						'description' => '',
						'hide_title'  => true,
						'callback'    => array( __CLASS__, 'get_report' ),
					),
					'downloads'         => array(
						'title'       => __( 'Customer downloads', 'woocommerce' ),
						'description' => '',
						'hide_title'  => true,
						'callback'    => array( __CLASS__, 'get_report' ),
					),
				),
			),
			'customers' => array(
				'title'   => __( 'Customers', 'woocommerce' ),
				'reports' => array(
					'customers'     => array(
						'title'       => __( 'Customers vs. guests', 'woocommerce' ),
						'description' => '',
						'hide_title'  => true,
						'callback'    => array( __CLASS__, 'get_report' ),
					),
					'customer_list' => array(
						'title'       => __( 'Customer list', 'woocommerce' ),
						'description' => '',
						'hide_title'  => true,
						'callback'    => array( __CLASS__, 'get_report' ),
					),
				),
			),
			'stock'     => array(
				'title'   => __( 'Stock', 'woocommerce' ),
				'reports' => array(
					'low_in_stock' => array(
						'title'       => __( 'Low in stock', 'woocommerce' ),
						'description' => '',
						'hide_title'  => true,
						'callback'    => array( __CLASS__, 'get_report' ),
					),
					'out_of_stock' => array(
						'title'       => __( 'Out of stock', 'woocommerce' ),
						'description' => '',
						'hide_title'  => true,
						'callback'    => array( __CLASS__, 'get_report' ),
					),
					'most_stocked' => array(
						'title'       => __( 'Most stocked', 'woocommerce' ),
						'description' => '',
						'hide_title'  => true,
						'callback'    => array( __CLASS__, 'get_report' ),
					),
				),
			),
		);

		if ( wc_tax_enabled() ) {
			$reports['taxes'] = array(
				'title'   => __( 'Taxes', 'woocommerce' ),
				'reports' => array(
					'taxes_by_code' => array(
						'title'       => __( 'Taxes by code', 'woocommerce' ),
						'description' => '',
						'hide_title'  => true,
						'callback'    => array( __CLASS__, 'get_report' ),
					),
					'taxes_by_date' => array(
						'title'       => __( 'Taxes by date', 'woocommerce' ),
						'description' => '',
						'hide_title'  => true,
						'callback'    => array( __CLASS__, 'get_report' ),
					),
				),
			);
		}

		$reports = apply_filters( 'woocommerce_admin_reports', $reports );
		$reports = apply_filters( 'woocommerce_reports_charts', $reports ); // Backwards compatibility.

		foreach ( $reports as $key => $report_group ) {
			if ( isset( $reports[ $key ]['charts'] ) ) {
				$reports[ $key ]['reports'] = $reports[ $key ]['charts'];
			}

			foreach ( $reports[ $key ]['reports'] as $report_key => $report ) {
				if ( isset( $reports[ $key ]['reports'][ $report_key ]['function'] ) ) {
					$reports[ $key ]['reports'][ $report_key ]['callback'] = $reports[ $key ]['reports'][ $report_key ]['function'];
				}
			}
		}

		return $reports;
	}

	/**
	 * Get a report from our reports subfolder.
	 *
	 * @param string $name
	 */
	public static function get_report( $name ) {
		$name  = sanitize_title( str_replace( '_', '-', $name ) );
		$class = 'WC_Report_' . str_replace( '-', '_', $name );

		include_once apply_filters( 'wc_admin_reports_path', 'reports/class-wc-report-' . $name . '.php', $name, $class );

		if ( ! class_exists( $class ) ) {
			return;
		}

		$report = new $class();
		$report->output_report();
	}
}
admin/class-wc-admin-log-table-list.php000064400000022432151336044770014001 0ustar00<?php
/**
 * WooCommerce Log Table List
 *
 * @author   WooThemes
 * @category Admin
 * @package  WooCommerce\Admin
 * @version  1.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'WP_List_Table' ) ) {
	require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}

class WC_Admin_Log_Table_List extends WP_List_Table {

	/**
	 * Initialize the log table list.
	 */
	public function __construct() {
		parent::__construct(
			array(
				'singular' => 'log',
				'plural'   => 'logs',
				'ajax'     => false,
			)
		);
	}

	/**
	 * Display level dropdown
	 *
	 * @global wpdb $wpdb
	 */
	public function level_dropdown() {

		$levels = array(
			array(
				'value' => WC_Log_Levels::EMERGENCY,
				'label' => __( 'Emergency', 'woocommerce' ),
			),
			array(
				'value' => WC_Log_Levels::ALERT,
				'label' => __( 'Alert', 'woocommerce' ),
			),
			array(
				'value' => WC_Log_Levels::CRITICAL,
				'label' => __( 'Critical', 'woocommerce' ),
			),
			array(
				'value' => WC_Log_Levels::ERROR,
				'label' => __( 'Error', 'woocommerce' ),
			),
			array(
				'value' => WC_Log_Levels::WARNING,
				'label' => __( 'Warning', 'woocommerce' ),
			),
			array(
				'value' => WC_Log_Levels::NOTICE,
				'label' => __( 'Notice', 'woocommerce' ),
			),
			array(
				'value' => WC_Log_Levels::INFO,
				'label' => __( 'Info', 'woocommerce' ),
			),
			array(
				'value' => WC_Log_Levels::DEBUG,
				'label' => __( 'Debug', 'woocommerce' ),
			),
		);

		$selected_level = isset( $_REQUEST['level'] ) ? $_REQUEST['level'] : '';
		?>
			<label for="filter-by-level" class="screen-reader-text"><?php esc_html_e( 'Filter by level', 'woocommerce' ); ?></label>
			<select name="level" id="filter-by-level">
				<option<?php selected( $selected_level, '' ); ?> value=""><?php esc_html_e( 'All levels', 'woocommerce' ); ?></option>
				<?php
				foreach ( $levels as $l ) {
					printf(
						'<option%1$s value="%2$s">%3$s</option>',
						selected( $selected_level, $l['value'], false ),
						esc_attr( $l['value'] ),
						esc_html( $l['label'] )
					);
				}
				?>
			</select>
		<?php
	}

	/**
	 * Get list columns.
	 *
	 * @return array
	 */
	public function get_columns() {
		return array(
			'cb'        => '<input type="checkbox" />',
			'timestamp' => __( 'Timestamp', 'woocommerce' ),
			'level'     => __( 'Level', 'woocommerce' ),
			'message'   => __( 'Message', 'woocommerce' ),
			'source'    => __( 'Source', 'woocommerce' ),
		);
	}

	/**
	 * Column cb.
	 *
	 * @param  array $log
	 * @return string
	 */
	public function column_cb( $log ) {
		return sprintf( '<input type="checkbox" name="log[]" value="%1$s" />', esc_attr( $log['log_id'] ) );
	}

	/**
	 * Timestamp column.
	 *
	 * @param  array $log
	 * @return string
	 */
	public function column_timestamp( $log ) {
		return esc_html(
			mysql2date(
				'Y-m-d H:i:s',
				$log['timestamp']
			)
		);
	}

	/**
	 * Level column.
	 *
	 * @param  array $log
	 * @return string
	 */
	public function column_level( $log ) {
		$level_key = WC_Log_Levels::get_severity_level( $log['level'] );
		$levels    = array(
			'emergency' => __( 'Emergency', 'woocommerce' ),
			'alert'     => __( 'Alert', 'woocommerce' ),
			'critical'  => __( 'Critical', 'woocommerce' ),
			'error'     => __( 'Error', 'woocommerce' ),
			'warning'   => __( 'Warning', 'woocommerce' ),
			'notice'    => __( 'Notice', 'woocommerce' ),
			'info'      => __( 'Info', 'woocommerce' ),
			'debug'     => __( 'Debug', 'woocommerce' ),
		);

		if ( ! isset( $levels[ $level_key ] ) ) {
			return '';
		}

		$level       = $levels[ $level_key ];
		$level_class = sanitize_html_class( 'log-level--' . $level_key );
		return '<span class="log-level ' . $level_class . '">' . esc_html( $level ) . '</span>';
	}

	/**
	 * Message column.
	 *
	 * @param  array $log
	 * @return string
	 */
	public function column_message( $log ) {
		return esc_html( $log['message'] );
	}

	/**
	 * Source column.
	 *
	 * @param  array $log
	 * @return string
	 */
	public function column_source( $log ) {
		return esc_html( $log['source'] );
	}

	/**
	 * Get bulk actions.
	 *
	 * @return array
	 */
	protected function get_bulk_actions() {
		return array(
			'delete' => __( 'Delete', 'woocommerce' ),
		);
	}

	/**
	 * Extra controls to be displayed between bulk actions and pagination.
	 *
	 * @param string $which
	 */
	protected function extra_tablenav( $which ) {
		if ( 'top' === $which ) {
			echo '<div class="alignleft actions">';
				$this->level_dropdown();
				$this->source_dropdown();
				submit_button( __( 'Filter', 'woocommerce' ), '', 'filter-action', false );
			echo '</div>';
		}
	}

	/**
	 * Get a list of sortable columns.
	 *
	 * @return array
	 */
	protected function get_sortable_columns() {
		return array(
			'timestamp' => array( 'timestamp', true ),
			'level'     => array( 'level', true ),
			'source'    => array( 'source', true ),
		);
	}

	/**
	 * Display source dropdown
	 *
	 * @global wpdb $wpdb
	 */
	protected function source_dropdown() {
		global $wpdb;

		$sources = $wpdb->get_col(
			"SELECT DISTINCT source
			FROM {$wpdb->prefix}woocommerce_log
			WHERE source != ''
			ORDER BY source ASC"
		);

		if ( ! empty( $sources ) ) {
			$selected_source = isset( $_REQUEST['source'] ) ? $_REQUEST['source'] : '';
			?>
				<label for="filter-by-source" class="screen-reader-text"><?php esc_html_e( 'Filter by source', 'woocommerce' ); ?></label>
				<select name="source" id="filter-by-source">
					<option<?php selected( $selected_source, '' ); ?> value=""><?php esc_html_e( 'All sources', 'woocommerce' ); ?></option>
					<?php
					foreach ( $sources as $s ) {
						printf(
							'<option%1$s value="%2$s">%3$s</option>',
							selected( $selected_source, $s, false ),
							esc_attr( $s ),
							esc_html( $s )
						);
					}
					?>
				</select>
			<?php
		}
	}

	/**
	 * Prepare table list items.
	 *
	 * @global wpdb $wpdb
	 */
	public function prepare_items() {
		global $wpdb;

		$this->prepare_column_headers();

		$per_page = $this->get_items_per_page( 'woocommerce_status_log_items_per_page', 10 );

		$where  = $this->get_items_query_where();
		$order  = $this->get_items_query_order();
		$limit  = $this->get_items_query_limit();
		$offset = $this->get_items_query_offset();

		$query_items = "
			SELECT log_id, timestamp, level, message, source
			FROM {$wpdb->prefix}woocommerce_log
			{$where} {$order} {$limit} {$offset}
		";

		$this->items = $wpdb->get_results( $query_items, ARRAY_A );

		$query_count = "SELECT COUNT(log_id) FROM {$wpdb->prefix}woocommerce_log {$where}";
		$total_items = $wpdb->get_var( $query_count );

		$this->set_pagination_args(
			array(
				'total_items' => $total_items,
				'per_page'    => $per_page,
				'total_pages' => ceil( $total_items / $per_page ),
			)
		);
	}

	/**
	 * Get prepared LIMIT clause for items query
	 *
	 * @global wpdb $wpdb
	 *
	 * @return string Prepared LIMIT clause for items query.
	 */
	protected function get_items_query_limit() {
		global $wpdb;

		$per_page = $this->get_items_per_page( 'woocommerce_status_log_items_per_page', 10 );
		return $wpdb->prepare( 'LIMIT %d', $per_page );
	}

	/**
	 * Get prepared OFFSET clause for items query
	 *
	 * @global wpdb $wpdb
	 *
	 * @return string Prepared OFFSET clause for items query.
	 */
	protected function get_items_query_offset() {
		global $wpdb;

		$per_page     = $this->get_items_per_page( 'woocommerce_status_log_items_per_page', 10 );
		$current_page = $this->get_pagenum();
		if ( 1 < $current_page ) {
			$offset = $per_page * ( $current_page - 1 );
		} else {
			$offset = 0;
		}

		return $wpdb->prepare( 'OFFSET %d', $offset );
	}

	/**
	 * Get prepared ORDER BY clause for items query
	 *
	 * @return string Prepared ORDER BY clause for items query.
	 */
	protected function get_items_query_order() {
		$valid_orders = array( 'level', 'source', 'timestamp' );
		if ( ! empty( $_REQUEST['orderby'] ) && in_array( $_REQUEST['orderby'], $valid_orders ) ) {
			$by = wc_clean( $_REQUEST['orderby'] );
		} else {
			$by = 'timestamp';
		}
		$by = esc_sql( $by );

		if ( ! empty( $_REQUEST['order'] ) && 'asc' === strtolower( $_REQUEST['order'] ) ) {
			$order = 'ASC';
		} else {
			$order = 'DESC';
		}

		return "ORDER BY {$by} {$order}, log_id {$order}";
	}

	/**
	 * Get prepared WHERE clause for items query
	 *
	 * @global wpdb $wpdb
	 *
	 * @return string Prepared WHERE clause for items query.
	 */
	protected function get_items_query_where() {
		global $wpdb;

		$where_conditions = array();
		$where_values     = array();
		if ( ! empty( $_REQUEST['level'] ) && WC_Log_Levels::is_valid_level( $_REQUEST['level'] ) ) {
			$where_conditions[] = 'level >= %d';
			$where_values[]     = WC_Log_Levels::get_level_severity( $_REQUEST['level'] );
		}
		if ( ! empty( $_REQUEST['source'] ) ) {
			$where_conditions[] = 'source = %s';
			$where_values[]     = wc_clean( $_REQUEST['source'] );
		}
		if ( ! empty( $_REQUEST['s'] ) ) {
			$where_conditions[] = 'message like %s';
			$where_values[]     = '%' . $wpdb->esc_like( wc_clean( wp_unslash( $_REQUEST['s'] ) ) ) . '%';
		}

		if ( empty( $where_conditions ) ) {
			return '';
		}

		return $wpdb->prepare( 'WHERE 1 = 1 AND ' . implode( ' AND ', $where_conditions ), $where_values );
	}

	/**
	 * Set _column_headers property for table list
	 */
	protected function prepare_column_headers() {
		$this->_column_headers = array(
			$this->get_columns(),
			array(),
			$this->get_sortable_columns(),
		);
	}
}
admin/class-wc-admin-api-keys.php000064400000017464151336044770012715 0ustar00<?php
/**
 * WooCommerce Admin API Keys Class
 *
 * @package WooCommerce\Admin
 * @version 2.4.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * WC_Admin_API_Keys.
 */
class WC_Admin_API_Keys {

	/**
	 * Initialize the API Keys admin actions.
	 */
	public function __construct() {
		add_action( 'admin_init', array( $this, 'actions' ) );
		add_action( 'woocommerce_settings_page_init', array( $this, 'screen_option' ) );
		add_filter( 'woocommerce_save_settings_advanced_keys', array( $this, 'allow_save_settings' ) );
	}

	/**
	 * Check if should allow save settings.
	 * This prevents "Your settings have been saved." notices on the table list.
	 *
	 * @param  bool $allow If allow save settings.
	 * @return bool
	 */
	public function allow_save_settings( $allow ) {
		if ( ! isset( $_GET['create-key'], $_GET['edit-key'] ) ) { // WPCS: input var okay, CSRF ok.
			return false;
		}

		return $allow;
	}

	/**
	 * Check if is API Keys settings page.
	 *
	 * @return bool
	 */
	private function is_api_keys_settings_page() {
		return isset( $_GET['page'], $_GET['tab'], $_GET['section'] ) && 'wc-settings' === $_GET['page'] && 'advanced' === $_GET['tab'] && 'keys' === $_GET['section']; // WPCS: input var okay, CSRF ok.
	}

	/**
	 * Page output.
	 */
	public static function page_output() {
		// Hide the save button.
		$GLOBALS['hide_save_button'] = true;

		if ( isset( $_GET['create-key'] ) || isset( $_GET['edit-key'] ) ) {
			$key_id   = isset( $_GET['edit-key'] ) ? absint( $_GET['edit-key'] ) : 0; // WPCS: input var okay, CSRF ok.
			$key_data = self::get_key_data( $key_id );
			$user_id  = (int) $key_data['user_id'];

			if ( $key_id && $user_id && ! current_user_can( 'edit_user', $user_id ) ) {
				if ( get_current_user_id() !== $user_id ) {
					wp_die( esc_html__( 'You do not have permission to edit this API Key', 'woocommerce' ) );
				}
			}

			include dirname( __FILE__ ) . '/settings/views/html-keys-edit.php';
		} else {
			self::table_list_output();
		}
	}

	/**
	 * Add screen option.
	 */
	public function screen_option() {
		global $keys_table_list;

		if ( ! isset( $_GET['create-key'] ) && ! isset( $_GET['edit-key'] ) && $this->is_api_keys_settings_page() ) { // WPCS: input var okay, CSRF ok.
			$keys_table_list = new WC_Admin_API_Keys_Table_List();

			// Add screen option.
			add_screen_option(
				'per_page',
				array(
					'default' => 10,
					'option'  => 'woocommerce_keys_per_page',
				)
			);
		}
	}

	/**
	 * Table list output.
	 */
	private static function table_list_output() {
		global $wpdb, $keys_table_list;

		echo '<h2 class="wc-table-list-header">' . esc_html__( 'REST API', 'woocommerce' ) . ' <a href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=advanced&section=keys&create-key=1' ) ) . '" class="add-new-h2">' . esc_html__( 'Add key', 'woocommerce' ) . '</a></h2>';

		// Get the API keys count.
		$count = $wpdb->get_var( "SELECT COUNT(key_id) FROM {$wpdb->prefix}woocommerce_api_keys WHERE 1 = 1;" );

		if ( absint( $count ) && $count > 0 ) {
			$keys_table_list->prepare_items();

			echo '<input type="hidden" name="page" value="wc-settings" />';
			echo '<input type="hidden" name="tab" value="advanced" />';
			echo '<input type="hidden" name="section" value="keys" />';

			$keys_table_list->views();
			$keys_table_list->search_box( __( 'Search key', 'woocommerce' ), 'key' );
			$keys_table_list->display();
		} else {
			echo '<div class="woocommerce-BlankState woocommerce-BlankState--api">';
			?>
			<h2 class="woocommerce-BlankState-message"><?php esc_html_e( 'The WooCommerce REST API allows external apps to view and manage store data. Access is granted only to those with valid API keys.', 'woocommerce' ); ?></h2>
			<a class="woocommerce-BlankState-cta button-primary button" href="<?php echo esc_url( admin_url( 'admin.php?page=wc-settings&tab=advanced&section=keys&create-key=1' ) ); ?>"><?php esc_html_e( 'Create an API key', 'woocommerce' ); ?></a>
			<style type="text/css">#posts-filter .wp-list-table, #posts-filter .tablenav.top, .tablenav.bottom .actions { display: none; }</style>
			<?php
		}
	}

	/**
	 * Get key data.
	 *
	 * @param  int $key_id API Key ID.
	 * @return array
	 */
	private static function get_key_data( $key_id ) {
		global $wpdb;

		$empty = array(
			'key_id'        => 0,
			'user_id'       => '',
			'description'   => '',
			'permissions'   => '',
			'truncated_key' => '',
			'last_access'   => '',
		);

		if ( 0 === $key_id ) {
			return $empty;
		}

		$key = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT key_id, user_id, description, permissions, truncated_key, last_access
				FROM {$wpdb->prefix}woocommerce_api_keys
				WHERE key_id = %d",
				$key_id
			),
			ARRAY_A
		);

		if ( is_null( $key ) ) {
			return $empty;
		}

		return $key;
	}

	/**
	 * API Keys admin actions.
	 */
	public function actions() {
		if ( $this->is_api_keys_settings_page() ) {
			// Revoke key.
			if ( isset( $_REQUEST['revoke-key'] ) ) { // WPCS: input var okay, CSRF ok.
				$this->revoke_key();
			}

			// Bulk actions.
			if ( isset( $_REQUEST['action'] ) && isset( $_REQUEST['key'] ) ) { // WPCS: input var okay, CSRF ok.
				$this->bulk_actions();
			}
		}
	}

	/**
	 * Notices.
	 */
	public static function notices() {
		if ( isset( $_GET['revoked'] ) ) { // WPCS: input var okay, CSRF ok.
			$revoked = absint( $_GET['revoked'] ); // WPCS: input var okay, CSRF ok.

			/* translators: %d: count */
			WC_Admin_Settings::add_message( sprintf( _n( '%d API key permanently revoked.', '%d API keys permanently revoked.', $revoked, 'woocommerce' ), $revoked ) );
		}
	}

	/**
	 * Revoke key.
	 */
	private function revoke_key() {
		global $wpdb;

		check_admin_referer( 'revoke' );

		if ( isset( $_REQUEST['revoke-key'] ) ) { // WPCS: input var okay, CSRF ok.
			$key_id  = absint( $_REQUEST['revoke-key'] ); // WPCS: input var okay, CSRF ok.
			$user_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT user_id FROM {$wpdb->prefix}woocommerce_api_keys WHERE key_id = %d", $key_id ) );

			if ( $key_id && $user_id && ( current_user_can( 'edit_user', $user_id ) || get_current_user_id() === $user_id ) ) {
				$this->remove_key( $key_id );
			} else {
				wp_die( esc_html__( 'You do not have permission to revoke this API Key', 'woocommerce' ) );
			}
		}

		wp_safe_redirect( esc_url_raw( add_query_arg( array( 'revoked' => 1 ), admin_url( 'admin.php?page=wc-settings&tab=advanced&section=keys' ) ) ) );
		exit();
	}

	/**
	 * Bulk actions.
	 */
	private function bulk_actions() {
		check_admin_referer( 'woocommerce-settings' );

		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			wp_die( esc_html__( 'You do not have permission to edit API Keys', 'woocommerce' ) );
		}

		if ( isset( $_REQUEST['action'] ) ) { // WPCS: input var okay, CSRF ok.
			$action = sanitize_text_field( wp_unslash( $_REQUEST['action'] ) ); // WPCS: input var okay, CSRF ok.
			$keys   = isset( $_REQUEST['key'] ) ? array_map( 'absint', (array) $_REQUEST['key'] ) : array(); // WPCS: input var okay, CSRF ok.

			if ( 'revoke' === $action ) {
				$this->bulk_revoke_key( $keys );
			}
		}
	}

	/**
	 * Bulk revoke key.
	 *
	 * @param array $keys API Keys.
	 */
	private function bulk_revoke_key( $keys ) {
		if ( ! current_user_can( 'remove_users' ) ) {
			wp_die( esc_html__( 'You do not have permission to revoke API Keys', 'woocommerce' ) );
		}

		$qty = 0;
		foreach ( $keys as $key_id ) {
			$result = $this->remove_key( $key_id );

			if ( $result ) {
				$qty++;
			}
		}

		// Redirect to webhooks page.
		wp_safe_redirect( esc_url_raw( add_query_arg( array( 'revoked' => $qty ), admin_url( 'admin.php?page=wc-settings&tab=advanced&section=keys' ) ) ) );
		exit();
	}

	/**
	 * Remove key.
	 *
	 * @param  int $key_id API Key ID.
	 * @return bool
	 */
	private function remove_key( $key_id ) {
		global $wpdb;

		$delete = $wpdb->delete( $wpdb->prefix . 'woocommerce_api_keys', array( 'key_id' => $key_id ), array( '%d' ) );

		return $delete;
	}
}

new WC_Admin_API_Keys();
admin/class-wc-admin.php000064400000024057151336044770011171 0ustar00<?php
/**
 * WooCommerce Admin
 *
 * @class    WC_Admin
 * @package  WooCommerce\Admin
 * @version  2.6.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * WC_Admin class.
 */
class WC_Admin {

	/**
	 * Constructor.
	 */
	public function __construct() {
		add_action( 'init', array( $this, 'includes' ) );
		add_action( 'current_screen', array( $this, 'conditional_includes' ) );
		add_action( 'admin_init', array( $this, 'buffer' ), 1 );
		add_action( 'admin_init', array( $this, 'preview_emails' ) );
		add_action( 'admin_init', array( $this, 'prevent_admin_access' ) );
		add_action( 'admin_init', array( $this, 'admin_redirects' ) );
		add_action( 'admin_footer', 'wc_print_js', 25 );
		add_filter( 'admin_footer_text', array( $this, 'admin_footer_text' ), 1 );
		add_action( 'init', array( 'WC_Site_Tracking', 'init' ) );

		// Disable WXR export of schedule action posts.
		add_filter( 'action_scheduler_post_type_args', array( $this, 'disable_webhook_post_export' ) );

		// Add body class for WP 5.3+ compatibility.
		add_filter( 'admin_body_class', array( $this, 'include_admin_body_class' ), 9999 );

		// Add body class for Marketplace and My Subscriptions pages.
		if ( isset( $_GET['page'] ) && 'wc-addons' === $_GET['page'] ) {
			add_filter( 'admin_body_class', array( 'WC_Admin_Addons', 'filter_admin_body_classes' ) );
		}
	}

	/**
	 * Output buffering allows admin screens to make redirects later on.
	 */
	public function buffer() {
		ob_start();
	}

	/**
	 * Include any classes we need within admin.
	 */
	public function includes() {
		include_once __DIR__ . '/wc-admin-functions.php';
		include_once __DIR__ . '/wc-meta-box-functions.php';
		include_once __DIR__ . '/class-wc-admin-post-types.php';
		include_once __DIR__ . '/class-wc-admin-taxonomies.php';
		include_once __DIR__ . '/class-wc-admin-menus.php';
		include_once __DIR__ . '/class-wc-admin-customize.php';
		include_once __DIR__ . '/class-wc-admin-notices.php';
		include_once __DIR__ . '/class-wc-admin-assets.php';
		include_once __DIR__ . '/class-wc-admin-api-keys.php';
		include_once __DIR__ . '/class-wc-admin-webhooks.php';
		include_once __DIR__ . '/class-wc-admin-pointers.php';
		include_once __DIR__ . '/class-wc-admin-importers.php';
		include_once __DIR__ . '/class-wc-admin-exporters.php';

		include_once WC_ABSPATH . 'includes/tracks/class-wc-tracks.php';
		include_once WC_ABSPATH . 'includes/tracks/class-wc-tracks-event.php';
		include_once WC_ABSPATH . 'includes/tracks/class-wc-tracks-client.php';
		include_once WC_ABSPATH . 'includes/tracks/class-wc-tracks-footer-pixel.php';
		include_once WC_ABSPATH . 'includes/tracks/class-wc-site-tracking.php';

		// Help Tabs.
		if ( apply_filters( 'woocommerce_enable_admin_help_tab', true ) ) {
			include_once __DIR__ . '/class-wc-admin-help.php';
		}

		// Helper.
		include_once __DIR__ . '/helper/class-wc-helper.php';

		// Marketplace suggestions & related REST API.
		include_once __DIR__ . '/marketplace-suggestions/class-wc-marketplace-suggestions.php';
		include_once __DIR__ . '/marketplace-suggestions/class-wc-marketplace-updater.php';
	}

	/**
	 * Include admin files conditionally.
	 */
	public function conditional_includes() {
		$screen = get_current_screen();

		if ( ! $screen ) {
			return;
		}

		switch ( $screen->id ) {
			case 'dashboard':
			case 'dashboard-network':
				include __DIR__ . '/class-wc-admin-dashboard-setup.php';
				include __DIR__ . '/class-wc-admin-dashboard.php';
				break;
			case 'options-permalink':
				include __DIR__ . '/class-wc-admin-permalink-settings.php';
				break;
			case 'plugins':
				include __DIR__ . '/plugin-updates/class-wc-plugins-screen-updates.php';
				break;
			case 'update-core':
				include __DIR__ . '/plugin-updates/class-wc-updates-screen-updates.php';
				break;
			case 'users':
			case 'user':
			case 'profile':
			case 'user-edit':
				include __DIR__ . '/class-wc-admin-profile.php';
				break;
		}
	}

	/**
	 * Handle redirects to setup/welcome page after install and updates.
	 *
	 * The user must have access rights, and we must ignore the network/bulk plugin updaters.
	 */
	public function admin_redirects() {
		// Don't run this fn from Action Scheduler requests, as it would clear _wc_activation_redirect transient.
		// That means OBW would never be shown.
		if ( wc_is_running_from_async_action_scheduler() ) {
			return;
		}

		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		// Nonced plugin install redirects.
		if ( ! empty( $_GET['wc-install-plugin-redirect'] ) ) {
			$plugin_slug = wc_clean( wp_unslash( $_GET['wc-install-plugin-redirect'] ) );

			if ( current_user_can( 'install_plugins' ) && in_array( $plugin_slug, array( 'woocommerce-gateway-stripe' ), true ) ) {
				$nonce = wp_create_nonce( 'install-plugin_' . $plugin_slug );
				$url   = self_admin_url( 'update.php?action=install-plugin&plugin=' . $plugin_slug . '&_wpnonce=' . $nonce );
			} else {
				$url = admin_url( 'plugin-install.php?tab=search&type=term&s=' . $plugin_slug );
			}

			wp_safe_redirect( $url );
			exit;
		}

		// phpcs:enable WordPress.Security.NonceVerification.Recommended
	}

	/**
	 * Prevent any user who cannot 'edit_posts' (subscribers, customers etc) from accessing admin.
	 */
	public function prevent_admin_access() {
		$prevent_access = false;

		if ( apply_filters( 'woocommerce_disable_admin_bar', true ) && ! is_ajax() && isset( $_SERVER['SCRIPT_FILENAME'] ) && basename( sanitize_text_field( wp_unslash( $_SERVER['SCRIPT_FILENAME'] ) ) ) !== 'admin-post.php' ) {
			$has_cap     = false;
			$access_caps = array( 'edit_posts', 'manage_woocommerce', 'view_admin_dashboard' );

			foreach ( $access_caps as $access_cap ) {
				if ( current_user_can( $access_cap ) ) {
					$has_cap = true;
					break;
				}
			}

			if ( ! $has_cap ) {
				$prevent_access = true;
			}
		}

		if ( apply_filters( 'woocommerce_prevent_admin_access', $prevent_access ) ) {
			wp_safe_redirect( wc_get_page_permalink( 'myaccount' ) );
			exit;
		}
	}

	/**
	 * Preview email template.
	 */
	public function preview_emails() {

		if ( isset( $_GET['preview_woocommerce_mail'] ) ) {
			if ( ! ( isset( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ), 'preview-mail' ) ) ) {
				die( 'Security check' );
			}

			// load the mailer class.
			$mailer = WC()->mailer();

			// get the preview email subject.
			$email_heading = __( 'HTML email template', 'woocommerce' );

			// get the preview email content.
			ob_start();
			include __DIR__ . '/views/html-email-template-preview.php';
			$message = ob_get_clean();

			// create a new email.
			$email = new WC_Email();

			// wrap the content with the email template and then add styles.
			$message = apply_filters( 'woocommerce_mail_content', $email->style_inline( $mailer->wrap_message( $email_heading, $message ) ) );

			// print the preview email.
			// phpcs:ignore WordPress.Security.EscapeOutput
			echo $message;
			// phpcs:enable
			exit;
		}
	}

	/**
	 * Change the admin footer text on WooCommerce admin pages.
	 *
	 * @since  2.3
	 * @param  string $footer_text text to be rendered in the footer.
	 * @return string
	 */
	public function admin_footer_text( $footer_text ) {
		if ( ! current_user_can( 'manage_woocommerce' ) || ! function_exists( 'wc_get_screen_ids' ) ) {
			return $footer_text;
		}
		$current_screen = get_current_screen();
		$wc_pages       = wc_get_screen_ids();

		// Set only WC pages.
		$wc_pages = array_diff( $wc_pages, array( 'profile', 'user-edit' ) );

		// Check to make sure we're on a WooCommerce admin page.
		if ( isset( $current_screen->id ) && apply_filters( 'woocommerce_display_admin_footer_text', in_array( $current_screen->id, $wc_pages, true ) ) ) {
			// Change the footer text.
			if ( ! get_option( 'woocommerce_admin_footer_text_rated' ) ) {
				$footer_text = sprintf(
					/* translators: 1: WooCommerce 2:: five stars */
					__( 'If you like %1$s please leave us a %2$s rating. A huge thanks in advance!', 'woocommerce' ),
					sprintf( '<strong>%s</strong>', esc_html__( 'WooCommerce', 'woocommerce' ) ),
					'<a href="https://wordpress.org/support/plugin/woocommerce/reviews?rate=5#new-post" target="_blank" class="wc-rating-link" aria-label="' . esc_attr__( 'five star', 'woocommerce' ) . '" data-rated="' . esc_attr__( 'Thanks :)', 'woocommerce' ) . '">&#9733;&#9733;&#9733;&#9733;&#9733;</a>'
				);
				wc_enqueue_js(
					"jQuery( 'a.wc-rating-link' ).on( 'click', function() {
						jQuery.post( '" . WC()->ajax_url() . "', { action: 'woocommerce_rated' } );
						jQuery( this ).parent().text( jQuery( this ).data( 'rated' ) );
					});"
				);
			} else {
				$footer_text = __( 'Thank you for selling with WooCommerce.', 'woocommerce' );
			}
		}

		return $footer_text;
	}

	/**
	 * Check on a Jetpack install queued by the Setup Wizard.
	 *
	 * See: WC_Admin_Setup_Wizard::install_jetpack()
	 */
	public function setup_wizard_check_jetpack() {
		$jetpack_active = class_exists( 'Jetpack' );

		wp_send_json_success(
			array(
				'is_active' => $jetpack_active ? 'yes' : 'no',
			)
		);
	}

	/**
	 * Disable WXR export of scheduled action posts.
	 *
	 * @since 3.6.2
	 *
	 * @param array $args Scehduled action post type registration args.
	 *
	 * @return array
	 */
	public function disable_webhook_post_export( $args ) {
		$args['can_export'] = false;
		return $args;
	}

	/**
	 * Include admin classes.
	 *
	 * @since 4.2.0
	 * @param string $classes Body classes string.
	 * @return string
	 */
	public function include_admin_body_class( $classes ) {
		if ( in_array( array( 'wc-wp-version-gte-53', 'wc-wp-version-gte-55' ), explode( ' ', $classes ), true ) ) {
			return $classes;
		}

		$raw_version   = get_bloginfo( 'version' );
		$version_parts = explode( '-', $raw_version );
		$version       = count( $version_parts ) > 1 ? $version_parts[0] : $raw_version;

		// Add WP 5.3+ compatibility class.
		if ( $raw_version && version_compare( $version, '5.3', '>=' ) ) {
			$classes .= ' wc-wp-version-gte-53';
		}

		// Add WP 5.5+ compatibility class.
		if ( $raw_version && version_compare( $version, '5.5', '>=' ) ) {
			$classes .= ' wc-wp-version-gte-55';
		}

		return $classes;
	}
}

return new WC_Admin();
admin/class-wc-admin-webhooks.php000064400000026347151336044770013014 0ustar00<?php
/**
 * WooCommerce Admin Webhooks Class
 *
 * @package WooCommerce\Admin
 * @version 3.3.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * WC_Admin_Webhooks.
 */
class WC_Admin_Webhooks {

	/**
	 * Initialize the webhooks admin actions.
	 */
	public function __construct() {
		add_action( 'admin_init', array( $this, 'actions' ) );
		add_action( 'woocommerce_settings_page_init', array( $this, 'screen_option' ) );
		add_filter( 'woocommerce_save_settings_advanced_webhooks', array( $this, 'allow_save_settings' ) );
	}

	/**
	 * Check if should allow save settings.
	 * This prevents "Your settings have been saved." notices on the table list.
	 *
	 * @param  bool $allow If allow save settings.
	 * @return bool
	 */
	public function allow_save_settings( $allow ) {
		if ( ! isset( $_GET['edit-webhook'] ) ) { // WPCS: input var okay, CSRF ok.
			return false;
		}

		return $allow;
	}

	/**
	 * Check if is webhook settings page.
	 *
	 * @return bool
	 */
	private function is_webhook_settings_page() {
		return isset( $_GET['page'], $_GET['tab'], $_GET['section'] ) && 'wc-settings' === $_GET['page'] && 'advanced' === $_GET['tab'] && 'webhooks' === $_GET['section']; // WPCS: input var okay, CSRF ok.
	}

	/**
	 * Save method.
	 */
	private function save() {
		check_admin_referer( 'woocommerce-settings' );

		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			wp_die( esc_html__( 'You do not have permission to update Webhooks', 'woocommerce' ) );
		}

		$errors     = array();
		$webhook_id = isset( $_POST['webhook_id'] ) ? absint( $_POST['webhook_id'] ) : 0;  // WPCS: input var okay, CSRF ok.
		$webhook    = new WC_Webhook( $webhook_id );

		// Name.
		if ( ! empty( $_POST['webhook_name'] ) ) { // WPCS: input var okay, CSRF ok.
			$name = sanitize_text_field( wp_unslash( $_POST['webhook_name'] ) ); // WPCS: input var okay, CSRF ok.
		} else {
			$name = sprintf(
				/* translators: %s: date */
				__( 'Webhook created on %s', 'woocommerce' ),
				// @codingStandardsIgnoreStart
				strftime( _x( '%b %d, %Y @ %I:%M %p', 'Webhook created on date parsed by strftime', 'woocommerce' ) )
				// @codingStandardsIgnoreEnd
			);
		}

		$webhook->set_name( $name );

		if ( ! $webhook->get_user_id() ) {
			$webhook->set_user_id( get_current_user_id() );
		}

		// Status.
		$webhook->set_status( ! empty( $_POST['webhook_status'] ) ? sanitize_text_field( wp_unslash( $_POST['webhook_status'] ) ) : 'disabled' ); // WPCS: input var okay, CSRF ok.

		// Delivery URL.
		$delivery_url = ! empty( $_POST['webhook_delivery_url'] ) ? esc_url_raw( wp_unslash( $_POST['webhook_delivery_url'] ) ) : ''; // WPCS: input var okay, CSRF ok.

		if ( wc_is_valid_url( $delivery_url ) ) {
			$webhook->set_delivery_url( $delivery_url );
		}

		// Secret.
		$secret = ! empty( $_POST['webhook_secret'] ) ? sanitize_text_field( wp_unslash( $_POST['webhook_secret'] ) ) : wp_generate_password( 50, true, true ); // WPCS: input var okay, CSRF ok.
		$webhook->set_secret( $secret );

		// Topic.
		if ( ! empty( $_POST['webhook_topic'] ) ) { // WPCS: input var okay, CSRF ok.
			$resource = '';
			$event    = '';

			switch ( $_POST['webhook_topic'] ) { // WPCS: input var okay, CSRF ok.
				case 'action':
					$resource = 'action';
					$event    = ! empty( $_POST['webhook_action_event'] ) ? sanitize_text_field( wp_unslash( $_POST['webhook_action_event'] ) ) : ''; // WPCS: input var okay, CSRF ok.
					break;

				default:
					list( $resource, $event ) = explode( '.', sanitize_text_field( wp_unslash( $_POST['webhook_topic'] ) ) ); // WPCS: input var okay, CSRF ok.
					break;
			}

			$topic = $resource . '.' . $event;

			if ( wc_is_webhook_valid_topic( $topic ) ) {
				$webhook->set_topic( $topic );
			} else {
				$errors[] = __( 'Webhook topic unknown. Please select a valid topic.', 'woocommerce' );
			}
		}

		// API version.
		$rest_api_versions = wc_get_webhook_rest_api_versions();
		$webhook->set_api_version( ! empty( $_POST['webhook_api_version'] ) ? sanitize_text_field( wp_unslash( $_POST['webhook_api_version'] ) ) : end( $rest_api_versions ) ); // WPCS: input var okay, CSRF ok.

		$webhook->save();

		// Run actions.
		do_action( 'woocommerce_webhook_options_save', $webhook->get_id() );
		if ( $errors ) {
			// Redirect to webhook edit page to avoid settings save actions.
			wp_safe_redirect( admin_url( 'admin.php?page=wc-settings&tab=advanced&section=webhooks&edit-webhook=' . $webhook->get_id() . '&error=' . rawurlencode( implode( '|', $errors ) ) ) );
			exit();
		} elseif ( isset( $_POST['webhook_status'] ) && 'active' === $_POST['webhook_status'] && $webhook->get_pending_delivery() ) { // WPCS: input var okay, CSRF ok.
			// Ping the webhook at the first time that is activated.
			$result = $webhook->deliver_ping();

			if ( is_wp_error( $result ) ) {
				// Redirect to webhook edit page to avoid settings save actions.
				wp_safe_redirect( admin_url( 'admin.php?page=wc-settings&tab=advanced&section=webhooks&edit-webhook=' . $webhook->get_id() . '&error=' . rawurlencode( $result->get_error_message() ) ) );
				exit();
			}
		}

		// Redirect to webhook edit page to avoid settings save actions.
		wp_safe_redirect( admin_url( 'admin.php?page=wc-settings&tab=advanced&section=webhooks&edit-webhook=' . $webhook->get_id() . '&updated=1' ) );
		exit();
	}

	/**
	 * Bulk delete.
	 *
	 * @param array $webhooks List of webhooks IDs.
	 */
	public static function bulk_delete( $webhooks ) {
		foreach ( $webhooks as $webhook_id ) {
			$webhook = new WC_Webhook( (int) $webhook_id );
			$webhook->delete( true );
		}

		$qty    = count( $webhooks );
		$status = isset( $_GET['status'] ) ? '&status=' . sanitize_text_field( wp_unslash( $_GET['status'] ) ) : ''; // WPCS: input var okay, CSRF ok.

		// Redirect to webhooks page.
		wp_safe_redirect( admin_url( 'admin.php?page=wc-settings&tab=advanced&section=webhooks' . $status . '&deleted=' . $qty ) );
		exit();
	}

	/**
	 * Delete webhook.
	 */
	private function delete() {
		check_admin_referer( 'delete-webhook' );

		if ( isset( $_GET['delete'] ) ) { // WPCS: input var okay, CSRF ok.
			$webhook_id = absint( $_GET['delete'] ); // WPCS: input var okay, CSRF ok.

			if ( $webhook_id ) {
				$this->bulk_delete( array( $webhook_id ) );
			}
		}
	}

	/**
	 * Webhooks admin actions.
	 */
	public function actions() {
		if ( $this->is_webhook_settings_page() ) {
			// Save.
			if ( isset( $_POST['save'] ) && isset( $_POST['webhook_id'] ) ) { // WPCS: input var okay, CSRF ok.
				$this->save();
			}

			// Delete webhook.
			if ( isset( $_GET['delete'] ) ) { // WPCS: input var okay, CSRF ok.
				$this->delete();
			}
		}
	}

	/**
	 * Page output.
	 */
	public static function page_output() {
		// Hide the save button.
		$GLOBALS['hide_save_button'] = true;

		if ( isset( $_GET['edit-webhook'] ) ) { // WPCS: input var okay, CSRF ok.
			$webhook_id = absint( $_GET['edit-webhook'] ); // WPCS: input var okay, CSRF ok.
			$webhook    = new WC_Webhook( $webhook_id );

			include __DIR__ .  '/settings/views/html-webhooks-edit.php';
			return;
		}

		self::table_list_output();
	}

	/**
	 * Notices.
	 */
	public static function notices() {
		if ( isset( $_GET['deleted'] ) ) { // WPCS: input var okay, CSRF ok.
			$deleted = absint( $_GET['deleted'] ); // WPCS: input var okay, CSRF ok.

			/* translators: %d: count */
			WC_Admin_Settings::add_message( sprintf( _n( '%d webhook permanently deleted.', '%d webhooks permanently deleted.', $deleted, 'woocommerce' ), $deleted ) );
		}

		if ( isset( $_GET['updated'] ) ) { // WPCS: input var okay, CSRF ok.
			WC_Admin_Settings::add_message( __( 'Webhook updated successfully.', 'woocommerce' ) );
		}

		if ( isset( $_GET['created'] ) ) { // WPCS: input var okay, CSRF ok.
			WC_Admin_Settings::add_message( __( 'Webhook created successfully.', 'woocommerce' ) );
		}

		if ( isset( $_GET['error'] ) ) { // WPCS: input var okay, CSRF ok.
			foreach ( explode( '|', sanitize_text_field( wp_unslash( $_GET['error'] ) ) ) as $message ) { // WPCS: input var okay, CSRF ok.
				WC_Admin_Settings::add_error( trim( $message ) );
			}
		}
	}

	/**
	 * Add screen option.
	 */
	public function screen_option() {
		global $webhooks_table_list;

		if ( ! isset( $_GET['edit-webhook'] ) && $this->is_webhook_settings_page() ) { // WPCS: input var okay, CSRF ok.
			$webhooks_table_list = new WC_Admin_Webhooks_Table_List();

			// Add screen option.
			add_screen_option(
				'per_page',
				array(
					'default' => 10,
					'option'  => 'woocommerce_webhooks_per_page',
				)
			);
		}
	}

	/**
	 * Table list output.
	 */
	private static function table_list_output() {
		global $webhooks_table_list;

		echo '<h2 class="wc-table-list-header">' . esc_html__( 'Webhooks', 'woocommerce' ) . ' <a href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=advanced&section=webhooks&edit-webhook=0' ) ) . '" class="add-new-h2">' . esc_html__( 'Add webhook', 'woocommerce' ) . '</a></h2>';

		// Get the webhooks count.
		$data_store   = WC_Data_Store::load( 'webhook' );
		$num_webhooks = $data_store->get_count_webhooks_by_status();
		$count        = array_sum( $num_webhooks );

		if ( 0 < $count ) {
			$webhooks_table_list->process_bulk_action();
			$webhooks_table_list->prepare_items();

			echo '<input type="hidden" name="page" value="wc-settings" />';
			echo '<input type="hidden" name="tab" value="advanced" />';
			echo '<input type="hidden" name="section" value="webhooks" />';

			$webhooks_table_list->views();
			$webhooks_table_list->search_box( __( 'Search webhooks', 'woocommerce' ), 'webhook' );
			$webhooks_table_list->display();
		} else {
			echo '<div class="woocommerce-BlankState woocommerce-BlankState--webhooks">';
			?>
			<h2 class="woocommerce-BlankState-message"><?php esc_html_e( 'Webhooks are event notifications sent to URLs of your choice. They can be used to integrate with third-party services which support them.', 'woocommerce' ); ?></h2>
			<a class="woocommerce-BlankState-cta button-primary button" href="<?php echo esc_url( admin_url( 'admin.php?page=wc-settings&tab=advanced&section=webhooks&edit-webhook=0' ) ); ?>"><?php esc_html_e( 'Create a new webhook', 'woocommerce' ); ?></a>
			<style type="text/css">#posts-filter .wp-list-table, #posts-filter .tablenav.top, .tablenav.bottom .actions { display: none; }</style>
			<?php
		}
	}

	/**
	 * Logs output.
	 *
	 * @deprecated 3.3.0
	 * @param WC_Webhook $webhook Deprecated.
	 */
	public static function logs_output( $webhook = 'deprecated' ) {
		wc_deprecated_function( 'WC_Admin_Webhooks::logs_output', '3.3' );
	}

	/**
	 * Get the webhook topic data.
	 *
	 * @param WC_Webhook $webhook Webhook instance.
	 *
	 * @return array
	 */
	public static function get_topic_data( $webhook ) {
		$topic    = $webhook->get_topic();
		$event    = '';
		$resource = '';

		if ( $topic ) {
			list( $resource, $event ) = explode( '.', $topic );

			if ( 'action' === $resource ) {
				$topic = 'action';
			} elseif ( ! in_array( $resource, array( 'coupon', 'customer', 'order', 'product' ), true ) ) {
				$topic = 'custom';
			}
		}

		return array(
			'topic'    => $topic,
			'event'    => $event,
			'resource' => $resource,
		);
	}

	/**
	 * Get the logs navigation.
	 *
	 * @deprecated 3.3.0
	 * @param int        $total Deprecated.
	 * @param WC_Webhook $webhook Deprecated.
	 */
	public static function get_logs_navigation( $total, $webhook ) {
		wc_deprecated_function( 'WC_Admin_Webhooks::get_logs_navigation', '3.3' );
	}
}

new WC_Admin_Webhooks();
admin/class-wc-admin-help.php000064400000011273151336044770012113 0ustar00<?php
/**
 * Add some content to the help tab
 *
 * @package     WooCommerce\Admin
 * @version     2.1.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( class_exists( 'WC_Admin_Help', false ) ) {
	return new WC_Admin_Help();
}

/**
 * WC_Admin_Help Class.
 */
class WC_Admin_Help {

	/**
	 * Hook in tabs.
	 */
	public function __construct() {
		add_action( 'current_screen', array( $this, 'add_tabs' ), 50 );
	}

	/**
	 * Add help tabs.
	 */
	public function add_tabs() {
		$screen = get_current_screen();

		if ( ! $screen || ! in_array( $screen->id, wc_get_screen_ids() ) ) {
			return;
		}

		$screen->add_help_tab(
			array(
				'id'      => 'woocommerce_support_tab',
				'title'   => __( 'Help &amp; Support', 'woocommerce' ),
				'content' =>
					'<h2>' . __( 'Help &amp; Support', 'woocommerce' ) . '</h2>' .
					'<p>' . sprintf(
						/* translators: %s: Documentation URL */
						__( 'Should you need help understanding, using, or extending WooCommerce, <a href="%s">please read our documentation</a>. You will find all kinds of resources including snippets, tutorials and much more.', 'woocommerce' ),
						'https://docs.woocommerce.com/documentation/plugins/woocommerce/?utm_source=helptab&utm_medium=product&utm_content=docs&utm_campaign=woocommerceplugin'
					) . '</p>' .
					'<p>' . sprintf(
						/* translators: %s: Forum URL */
						__( 'For further assistance with WooCommerce core, use the <a href="%1$s">community forum</a>. For help with premium extensions sold on WooCommerce.com, <a href="%2$s">open a support request at WooCommerce.com</a>.', 'woocommerce' ),
						'https://wordpress.org/support/plugin/woocommerce',
						'https://woocommerce.com/my-account/create-a-ticket/?utm_source=helptab&utm_medium=product&utm_content=tickets&utm_campaign=woocommerceplugin'
					) . '</p>' .
					'<p>' . __( 'Before asking for help, we recommend checking the system status page to identify any problems with your configuration.', 'woocommerce' ) . '</p>' .
					'<p><a href="' . admin_url( 'admin.php?page=wc-status' ) . '" class="button button-primary">' . __( 'System status', 'woocommerce' ) . '</a> <a href="https://wordpress.org/support/plugin/woocommerce" class="button">' . __( 'Community forum', 'woocommerce' ) . '</a> <a href="https://woocommerce.com/my-account/create-a-ticket/?utm_source=helptab&utm_medium=product&utm_content=tickets&utm_campaign=woocommerceplugin" class="button">' . __( 'WooCommerce.com support', 'woocommerce' ) . '</a></p>',
			)
		);

		$screen->add_help_tab(
			array(
				'id'      => 'woocommerce_bugs_tab',
				'title'   => __( 'Found a bug?', 'woocommerce' ),
				'content' =>
					'<h2>' . __( 'Found a bug?', 'woocommerce' ) . '</h2>' .
					/* translators: 1: GitHub issues URL 2: GitHub contribution guide URL 3: System status report URL */
					'<p>' . sprintf( __( 'If you find a bug within WooCommerce core you can create a ticket via <a href="%1$s">Github issues</a>. Ensure you read the <a href="%2$s">contribution guide</a> prior to submitting your report. To help us solve your issue, please be as descriptive as possible and include your <a href="%3$s">system status report</a>.', 'woocommerce' ), 'https://github.com/woocommerce/woocommerce/issues?state=open', 'https://github.com/woocommerce/woocommerce/blob/trunk/.github/CONTRIBUTING.md', admin_url( 'admin.php?page=wc-status' ) ) . '</p>' .
					'<p><a href="https://github.com/woocommerce/woocommerce/issues/new?template=4-Bug-report.md" class="button button-primary">' . __( 'Report a bug', 'woocommerce' ) . '</a> <a href="' . admin_url( 'admin.php?page=wc-status' ) . '" class="button">' . __( 'System status', 'woocommerce' ) . '</a></p>',

			)
		);

		$screen->set_help_sidebar(
			'<p><strong>' . __( 'For more information:', 'woocommerce' ) . '</strong></p>' .
			'<p><a href="https://woocommerce.com/?utm_source=helptab&utm_medium=product&utm_content=about&utm_campaign=woocommerceplugin" target="_blank">' . __( 'About WooCommerce', 'woocommerce' ) . '</a></p>' .
			'<p><a href="https://wordpress.org/plugins/woocommerce/" target="_blank">' . __( 'WordPress.org project', 'woocommerce' ) . '</a></p>' .
			'<p><a href="https://github.com/woocommerce/woocommerce/" target="_blank">' . __( 'Github project', 'woocommerce' ) . '</a></p>' .
			'<p><a href="https://woocommerce.com/storefront/?utm_source=helptab&utm_medium=product&utm_content=wcthemes&utm_campaign=woocommerceplugin" target="_blank">' . __( 'Official theme', 'woocommerce' ) . '</a></p>' .
			'<p><a href="https://woocommerce.com/product-category/woocommerce-extensions/?utm_source=helptab&utm_medium=product&utm_content=wcextensions&utm_campaign=woocommerceplugin" target="_blank">' . __( 'Official extensions', 'woocommerce' ) . '</a></p>'
		);
	}
}

return new WC_Admin_Help();
admin/wc-meta-box-functions.php000064400000026506151336044770012521 0ustar00<?php
/**
 * WooCommerce Meta Box Functions
 *
 * @author      WooThemes
 * @category    Core
 * @package     WooCommerce\Admin\Functions
 * @version     2.3.0
 */
if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

/**
 * Output a text input box.
 *
 * @param array $field
 */
function woocommerce_wp_text_input( $field ) {
	global $thepostid, $post;

	$thepostid              = empty( $thepostid ) ? $post->ID : $thepostid;
	$field['placeholder']   = isset( $field['placeholder'] ) ? $field['placeholder'] : '';
	$field['class']         = isset( $field['class'] ) ? $field['class'] : 'short';
	$field['style']         = isset( $field['style'] ) ? $field['style'] : '';
	$field['wrapper_class'] = isset( $field['wrapper_class'] ) ? $field['wrapper_class'] : '';
	$field['value']         = isset( $field['value'] ) ? $field['value'] : get_post_meta( $thepostid, $field['id'], true );
	$field['name']          = isset( $field['name'] ) ? $field['name'] : $field['id'];
	$field['type']          = isset( $field['type'] ) ? $field['type'] : 'text';
	$field['desc_tip']      = isset( $field['desc_tip'] ) ? $field['desc_tip'] : false;
	$data_type              = empty( $field['data_type'] ) ? '' : $field['data_type'];

	switch ( $data_type ) {
		case 'price':
			$field['class'] .= ' wc_input_price';
			$field['value']  = wc_format_localized_price( $field['value'] );
			break;
		case 'decimal':
			$field['class'] .= ' wc_input_decimal';
			$field['value']  = wc_format_localized_decimal( $field['value'] );
			break;
		case 'stock':
			$field['class'] .= ' wc_input_stock';
			$field['value']  = wc_stock_amount( $field['value'] );
			break;
		case 'url':
			$field['class'] .= ' wc_input_url';
			$field['value']  = esc_url( $field['value'] );
			break;

		default:
			break;
	}

	// Custom attribute handling
	$custom_attributes = array();

	if ( ! empty( $field['custom_attributes'] ) && is_array( $field['custom_attributes'] ) ) {

		foreach ( $field['custom_attributes'] as $attribute => $value ) {
			$custom_attributes[] = esc_attr( $attribute ) . '="' . esc_attr( $value ) . '"';
		}
	}

	echo '<p class="form-field ' . esc_attr( $field['id'] ) . '_field ' . esc_attr( $field['wrapper_class'] ) . '">
		<label for="' . esc_attr( $field['id'] ) . '">' . wp_kses_post( $field['label'] ) . '</label>';

	if ( ! empty( $field['description'] ) && false !== $field['desc_tip'] ) {
		echo wc_help_tip( $field['description'] );
	}

	echo '<input type="' . esc_attr( $field['type'] ) . '" class="' . esc_attr( $field['class'] ) . '" style="' . esc_attr( $field['style'] ) . '" name="' . esc_attr( $field['name'] ) . '" id="' . esc_attr( $field['id'] ) . '" value="' . esc_attr( $field['value'] ) . '" placeholder="' . esc_attr( $field['placeholder'] ) . '" ' . implode( ' ', $custom_attributes ) . ' /> ';

	if ( ! empty( $field['description'] ) && false === $field['desc_tip'] ) {
		echo '<span class="description">' . wp_kses_post( $field['description'] ) . '</span>';
	}

	echo '</p>';
}

/**
 * Output a hidden input box.
 *
 * @param array $field
 */
function woocommerce_wp_hidden_input( $field ) {
	global $thepostid, $post;

	$thepostid      = empty( $thepostid ) ? $post->ID : $thepostid;
	$field['value'] = isset( $field['value'] ) ? $field['value'] : get_post_meta( $thepostid, $field['id'], true );
	$field['class'] = isset( $field['class'] ) ? $field['class'] : '';

	echo '<input type="hidden" class="' . esc_attr( $field['class'] ) . '" name="' . esc_attr( $field['id'] ) . '" id="' . esc_attr( $field['id'] ) . '" value="' . esc_attr( $field['value'] ) . '" /> ';
}

/**
 * Output a textarea input box.
 *
 * @param array $field
 */
function woocommerce_wp_textarea_input( $field ) {
	global $thepostid, $post;

	$thepostid              = empty( $thepostid ) ? $post->ID : $thepostid;
	$field['placeholder']   = isset( $field['placeholder'] ) ? $field['placeholder'] : '';
	$field['class']         = isset( $field['class'] ) ? $field['class'] : 'short';
	$field['style']         = isset( $field['style'] ) ? $field['style'] : '';
	$field['wrapper_class'] = isset( $field['wrapper_class'] ) ? $field['wrapper_class'] : '';
	$field['value']         = isset( $field['value'] ) ? $field['value'] : get_post_meta( $thepostid, $field['id'], true );
	$field['desc_tip']      = isset( $field['desc_tip'] ) ? $field['desc_tip'] : false;
	$field['name']          = isset( $field['name'] ) ? $field['name'] : $field['id'];
	$field['rows']          = isset( $field['rows'] ) ? $field['rows'] : 2;
	$field['cols']          = isset( $field['cols'] ) ? $field['cols'] : 20;

	// Custom attribute handling
	$custom_attributes = array();

	if ( ! empty( $field['custom_attributes'] ) && is_array( $field['custom_attributes'] ) ) {

		foreach ( $field['custom_attributes'] as $attribute => $value ) {
			$custom_attributes[] = esc_attr( $attribute ) . '="' . esc_attr( $value ) . '"';
		}
	}

	echo '<p class="form-field ' . esc_attr( $field['id'] ) . '_field ' . esc_attr( $field['wrapper_class'] ) . '">
		<label for="' . esc_attr( $field['id'] ) . '">' . wp_kses_post( $field['label'] ) . '</label>';

	if ( ! empty( $field['description'] ) && false !== $field['desc_tip'] ) {
		echo wc_help_tip( $field['description'] );
	}

	echo '<textarea class="' . esc_attr( $field['class'] ) . '" style="' . esc_attr( $field['style'] ) . '"  name="' . esc_attr( $field['name'] ) . '" id="' . esc_attr( $field['id'] ) . '" placeholder="' . esc_attr( $field['placeholder'] ) . '" rows="' . esc_attr( $field['rows'] ) . '" cols="' . esc_attr( $field['cols'] ) . '" ' . implode( ' ', $custom_attributes ) . '>' . esc_textarea( $field['value'] ) . '</textarea> ';

	if ( ! empty( $field['description'] ) && false === $field['desc_tip'] ) {
		echo '<span class="description">' . wp_kses_post( $field['description'] ) . '</span>';
	}

	echo '</p>';
}

/**
 * Output a checkbox input box.
 *
 * @param array $field
 */
function woocommerce_wp_checkbox( $field ) {
	global $thepostid, $post;

	$thepostid              = empty( $thepostid ) ? $post->ID : $thepostid;
	$field['class']         = isset( $field['class'] ) ? $field['class'] : 'checkbox';
	$field['style']         = isset( $field['style'] ) ? $field['style'] : '';
	$field['wrapper_class'] = isset( $field['wrapper_class'] ) ? $field['wrapper_class'] : '';
	$field['value']         = isset( $field['value'] ) ? $field['value'] : get_post_meta( $thepostid, $field['id'], true );
	$field['cbvalue']       = isset( $field['cbvalue'] ) ? $field['cbvalue'] : 'yes';
	$field['name']          = isset( $field['name'] ) ? $field['name'] : $field['id'];
	$field['desc_tip']      = isset( $field['desc_tip'] ) ? $field['desc_tip'] : false;

	// Custom attribute handling
	$custom_attributes = array();

	if ( ! empty( $field['custom_attributes'] ) && is_array( $field['custom_attributes'] ) ) {

		foreach ( $field['custom_attributes'] as $attribute => $value ) {
			$custom_attributes[] = esc_attr( $attribute ) . '="' . esc_attr( $value ) . '"';
		}
	}

	echo '<p class="form-field ' . esc_attr( $field['id'] ) . '_field ' . esc_attr( $field['wrapper_class'] ) . '">
		<label for="' . esc_attr( $field['id'] ) . '">' . wp_kses_post( $field['label'] ) . '</label>';

	if ( ! empty( $field['description'] ) && false !== $field['desc_tip'] ) {
		echo wc_help_tip( $field['description'] );
	}

	echo '<input type="checkbox" class="' . esc_attr( $field['class'] ) . '" style="' . esc_attr( $field['style'] ) . '" name="' . esc_attr( $field['name'] ) . '" id="' . esc_attr( $field['id'] ) . '" value="' . esc_attr( $field['cbvalue'] ) . '" ' . checked( $field['value'], $field['cbvalue'], false ) . '  ' . implode( ' ', $custom_attributes ) . '/> ';

	if ( ! empty( $field['description'] ) && false === $field['desc_tip'] ) {
		echo '<span class="description">' . wp_kses_post( $field['description'] ) . '</span>';
	}

	echo '</p>';
}

/**
 * Output a select input box.
 *
 * @param array $field Data about the field to render.
 */
function woocommerce_wp_select( $field ) {
	global $thepostid, $post;

	$thepostid = empty( $thepostid ) ? $post->ID : $thepostid;
	$field     = wp_parse_args(
		$field, array(
			'class'             => 'select short',
			'style'             => '',
			'wrapper_class'     => '',
			'value'             => get_post_meta( $thepostid, $field['id'], true ),
			'name'              => $field['id'],
			'desc_tip'          => false,
			'custom_attributes' => array(),
		)
	);

	$wrapper_attributes = array(
		'class' => $field['wrapper_class'] . " form-field {$field['id']}_field",
	);

	$label_attributes = array(
		'for' => $field['id'],
	);

	$field_attributes          = (array) $field['custom_attributes'];
	$field_attributes['style'] = $field['style'];
	$field_attributes['id']    = $field['id'];
	$field_attributes['name']  = $field['name'];
	$field_attributes['class'] = $field['class'];

	$tooltip     = ! empty( $field['description'] ) && false !== $field['desc_tip'] ? $field['description'] : '';
	$description = ! empty( $field['description'] ) && false === $field['desc_tip'] ? $field['description'] : '';
	?>
	<p <?php echo wc_implode_html_attributes( $wrapper_attributes ); // WPCS: XSS ok. ?>>
		<label <?php echo wc_implode_html_attributes( $label_attributes ); // WPCS: XSS ok. ?>><?php echo wp_kses_post( $field['label'] ); ?></label>
		<?php if ( $tooltip ) : ?>
			<?php echo wc_help_tip( $tooltip ); // WPCS: XSS ok. ?>
		<?php endif; ?>
		<select <?php echo wc_implode_html_attributes( $field_attributes ); // WPCS: XSS ok. ?>>
			<?php
			foreach ( $field['options'] as $key => $value ) {
				echo '<option value="' . esc_attr( $key ) . '"' . wc_selected( $key, $field['value'] ) . '>' . esc_html( $value ) . '</option>';
			}
			?>
		</select>
		<?php if ( $description ) : ?>
			<span class="description"><?php echo wp_kses_post( $description ); ?></span>
		<?php endif; ?>
	</p>
	<?php
}

/**
 * Output a radio input box.
 *
 * @param array $field
 */
function woocommerce_wp_radio( $field ) {
	global $thepostid, $post;

	$thepostid              = empty( $thepostid ) ? $post->ID : $thepostid;
	$field['class']         = isset( $field['class'] ) ? $field['class'] : 'select short';
	$field['style']         = isset( $field['style'] ) ? $field['style'] : '';
	$field['wrapper_class'] = isset( $field['wrapper_class'] ) ? $field['wrapper_class'] : '';
	$field['value']         = isset( $field['value'] ) ? $field['value'] : get_post_meta( $thepostid, $field['id'], true );
	$field['name']          = isset( $field['name'] ) ? $field['name'] : $field['id'];
	$field['desc_tip']      = isset( $field['desc_tip'] ) ? $field['desc_tip'] : false;

	echo '<fieldset class="form-field ' . esc_attr( $field['id'] ) . '_field ' . esc_attr( $field['wrapper_class'] ) . '"><legend>' . wp_kses_post( $field['label'] ) . '</legend>';

	if ( ! empty( $field['description'] ) && false !== $field['desc_tip'] ) {
		echo wc_help_tip( $field['description'] );
	}

	echo '<ul class="wc-radios">';

	foreach ( $field['options'] as $key => $value ) {

		echo '<li><label><input
				name="' . esc_attr( $field['name'] ) . '"
				value="' . esc_attr( $key ) . '"
				type="radio"
				class="' . esc_attr( $field['class'] ) . '"
				style="' . esc_attr( $field['style'] ) . '"
				' . checked( esc_attr( $field['value'] ), esc_attr( $key ), false ) . '
				/> ' . esc_html( $value ) . '</label>
		</li>';
	}
	echo '</ul>';

	if ( ! empty( $field['description'] ) && false === $field['desc_tip'] ) {
		echo '<span class="description">' . wp_kses_post( $field['description'] ) . '</span>';
	}

	echo '</fieldset>';
}
admin/class-wc-admin-assets.php000064400000075626151336044770012501 0ustar00<?php
/**
 * Load assets
 *
 * @package WooCommerce\Admin
 * @version 3.7.0
 */

use Automattic\Jetpack\Constants;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'WC_Admin_Assets', false ) ) :

	/**
	 * WC_Admin_Assets Class.
	 */
	class WC_Admin_Assets {

		/**
		 * Hook in tabs.
		 */
		public function __construct() {
			add_action( 'admin_enqueue_scripts', array( $this, 'admin_styles' ) );
			add_action( 'admin_enqueue_scripts', array( $this, 'admin_scripts' ) );
		}

		/**
		 * Enqueue styles.
		 */
		public function admin_styles() {
			global $wp_scripts;

			$version   = Constants::get_constant( 'WC_VERSION' );
			$screen    = get_current_screen();
			$screen_id = $screen ? $screen->id : '';

			// Register admin styles.
			wp_register_style( 'woocommerce_admin_menu_styles', WC()->plugin_url() . '/assets/css/menu.css', array(), $version );
			wp_register_style( 'woocommerce_admin_styles', WC()->plugin_url() . '/assets/css/admin.css', array(), $version );
			wp_register_style( 'jquery-ui-style', WC()->plugin_url() . '/assets/css/jquery-ui/jquery-ui.min.css', array(), $version );
			wp_register_style( 'woocommerce_admin_dashboard_styles', WC()->plugin_url() . '/assets/css/dashboard.css', array(), $version );
			wp_register_style( 'woocommerce_admin_print_reports_styles', WC()->plugin_url() . '/assets/css/reports-print.css', array(), $version, 'print' );
			wp_register_style( 'woocommerce_admin_marketplace_styles', WC()->plugin_url() . '/assets/css/marketplace-suggestions.css', array(), $version );
			wp_register_style( 'woocommerce_admin_privacy_styles', WC()->plugin_url() . '/assets/css/privacy.css', array(), $version );

			// Add RTL support for admin styles.
			wp_style_add_data( 'woocommerce_admin_menu_styles', 'rtl', 'replace' );
			wp_style_add_data( 'woocommerce_admin_styles', 'rtl', 'replace' );
			wp_style_add_data( 'woocommerce_admin_dashboard_styles', 'rtl', 'replace' );
			wp_style_add_data( 'woocommerce_admin_print_reports_styles', 'rtl', 'replace' );
			wp_style_add_data( 'woocommerce_admin_marketplace_styles', 'rtl', 'replace' );
			wp_style_add_data( 'woocommerce_admin_privacy_styles', 'rtl', 'replace' );

			if ( $screen && $screen->is_block_editor() ) {
				wp_register_style( 'woocommerce-general', WC()->plugin_url() . '/assets/css/woocommerce.css', array(), $version );
				wp_style_add_data( 'woocommerce-general', 'rtl', 'replace' );
			}

			// Sitewide menu CSS.
			wp_enqueue_style( 'woocommerce_admin_menu_styles' );

			// Admin styles for WC pages only.
			if ( in_array( $screen_id, wc_get_screen_ids() ) ) {
				wp_enqueue_style( 'woocommerce_admin_styles' );
				wp_enqueue_style( 'jquery-ui-style' );
				wp_enqueue_style( 'wp-color-picker' );
			}

			if ( in_array( $screen_id, array( 'dashboard' ) ) ) {
				wp_enqueue_style( 'woocommerce_admin_dashboard_styles' );
			}

			if ( in_array( $screen_id, array( 'woocommerce_page_wc-reports', 'toplevel_page_wc-reports' ) ) ) {
				wp_enqueue_style( 'woocommerce_admin_print_reports_styles' );
			}

			// Privacy Policy Guide css for back-compat.
			if ( isset( $_GET['wp-privacy-policy-guide'] ) || in_array( $screen_id, array( 'privacy-policy-guide' ) ) ) {
				wp_enqueue_style( 'woocommerce_admin_privacy_styles' );
			}

			// @deprecated 2.3.
			if ( has_action( 'woocommerce_admin_css' ) ) {
				do_action( 'woocommerce_admin_css' );
				wc_deprecated_function( 'The woocommerce_admin_css action', '2.3', 'admin_enqueue_scripts' );
			}

			if ( WC_Marketplace_Suggestions::show_suggestions_for_screen( $screen_id ) ) {
				wp_enqueue_style( 'woocommerce_admin_marketplace_styles' );
			}
		}


		/**
		 * Enqueue scripts.
		 */
		public function admin_scripts() {
			global $wp_query, $post;

			$screen       = get_current_screen();
			$screen_id    = $screen ? $screen->id : '';
			$wc_screen_id = sanitize_title( __( 'WooCommerce', 'woocommerce' ) );
			$suffix       = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min';
			$version      = Constants::get_constant( 'WC_VERSION' );

			// Register scripts.
			wp_register_script( 'woocommerce_admin', WC()->plugin_url() . '/assets/js/admin/woocommerce_admin' . $suffix . '.js', array( 'jquery', 'jquery-blockui', 'jquery-ui-sortable', 'jquery-ui-widget', 'jquery-ui-core', 'jquery-tiptip' ), $version );
			wp_register_script( 'jquery-blockui', WC()->plugin_url() . '/assets/js/jquery-blockui/jquery.blockUI' . $suffix . '.js', array( 'jquery' ), '2.70', true );
			wp_register_script( 'jquery-tiptip', WC()->plugin_url() . '/assets/js/jquery-tiptip/jquery.tipTip' . $suffix . '.js', array( 'jquery' ), $version, true );
			wp_register_script( 'round', WC()->plugin_url() . '/assets/js/round/round' . $suffix . '.js', array( 'jquery' ), $version );
			wp_register_script( 'wc-admin-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes' . $suffix . '.js', array( 'jquery', 'jquery-ui-datepicker', 'jquery-ui-sortable', 'accounting', 'round', 'wc-enhanced-select', 'plupload-all', 'stupidtable', 'jquery-tiptip' ), $version );
			wp_register_script( 'zeroclipboard', WC()->plugin_url() . '/assets/js/zeroclipboard/jquery.zeroclipboard' . $suffix . '.js', array( 'jquery' ), $version );
			wp_register_script( 'qrcode', WC()->plugin_url() . '/assets/js/jquery-qrcode/jquery.qrcode' . $suffix . '.js', array( 'jquery' ), $version );
			wp_register_script( 'stupidtable', WC()->plugin_url() . '/assets/js/stupidtable/stupidtable' . $suffix . '.js', array( 'jquery' ), $version );
			wp_register_script( 'serializejson', WC()->plugin_url() . '/assets/js/jquery-serializejson/jquery.serializejson' . $suffix . '.js', array( 'jquery' ), '2.8.1' );
			wp_register_script( 'flot', WC()->plugin_url() . '/assets/js/jquery-flot/jquery.flot' . $suffix . '.js', array( 'jquery' ), $version );
			wp_register_script( 'flot-resize', WC()->plugin_url() . '/assets/js/jquery-flot/jquery.flot.resize' . $suffix . '.js', array( 'jquery', 'flot' ), $version );
			wp_register_script( 'flot-time', WC()->plugin_url() . '/assets/js/jquery-flot/jquery.flot.time' . $suffix . '.js', array( 'jquery', 'flot' ), $version );
			wp_register_script( 'flot-pie', WC()->plugin_url() . '/assets/js/jquery-flot/jquery.flot.pie' . $suffix . '.js', array( 'jquery', 'flot' ), $version );
			wp_register_script( 'flot-stack', WC()->plugin_url() . '/assets/js/jquery-flot/jquery.flot.stack' . $suffix . '.js', array( 'jquery', 'flot' ), $version );
			wp_register_script( 'wc-settings-tax', WC()->plugin_url() . '/assets/js/admin/settings-views-html-settings-tax' . $suffix . '.js', array( 'jquery', 'wp-util', 'underscore', 'backbone', 'jquery-blockui' ), $version );
			wp_register_script( 'wc-backbone-modal', WC()->plugin_url() . '/assets/js/admin/backbone-modal' . $suffix . '.js', array( 'underscore', 'backbone', 'wp-util' ), $version );
			wp_register_script( 'wc-shipping-zones', WC()->plugin_url() . '/assets/js/admin/wc-shipping-zones' . $suffix . '.js', array( 'jquery', 'wp-util', 'underscore', 'backbone', 'jquery-ui-sortable', 'wc-enhanced-select', 'wc-backbone-modal' ), $version );
			wp_register_script( 'wc-shipping-zone-methods', WC()->plugin_url() . '/assets/js/admin/wc-shipping-zone-methods' . $suffix . '.js', array( 'jquery', 'wp-util', 'underscore', 'backbone', 'jquery-ui-sortable', 'wc-backbone-modal' ), $version );
			wp_register_script( 'wc-shipping-classes', WC()->plugin_url() . '/assets/js/admin/wc-shipping-classes' . $suffix . '.js', array( 'jquery', 'wp-util', 'underscore', 'backbone' ), $version );
			wp_register_script( 'wc-clipboard', WC()->plugin_url() . '/assets/js/admin/wc-clipboard' . $suffix . '.js', array( 'jquery' ), $version );
			wp_register_script( 'select2', WC()->plugin_url() . '/assets/js/select2/select2.full' . $suffix . '.js', array( 'jquery' ), '4.0.3' );
			wp_register_script( 'selectWoo', WC()->plugin_url() . '/assets/js/selectWoo/selectWoo.full' . $suffix . '.js', array( 'jquery' ), '1.0.6' );
			wp_register_script( 'wc-enhanced-select', WC()->plugin_url() . '/assets/js/admin/wc-enhanced-select' . $suffix . '.js', array( 'jquery', 'selectWoo' ), $version );
			wp_register_script( 'js-cookie', WC()->plugin_url() . '/assets/js/js-cookie/js.cookie' . $suffix . '.js', array(), '2.1.4', true );

			wp_localize_script(
				'wc-enhanced-select',
				'wc_enhanced_select_params',
				array(
					'i18n_no_matches'           => _x( 'No matches found', 'enhanced select', 'woocommerce' ),
					'i18n_ajax_error'           => _x( 'Loading failed', 'enhanced select', 'woocommerce' ),
					'i18n_input_too_short_1'    => _x( 'Please enter 1 or more characters', 'enhanced select', 'woocommerce' ),
					'i18n_input_too_short_n'    => _x( 'Please enter %qty% or more characters', 'enhanced select', 'woocommerce' ),
					'i18n_input_too_long_1'     => _x( 'Please delete 1 character', 'enhanced select', 'woocommerce' ),
					'i18n_input_too_long_n'     => _x( 'Please delete %qty% characters', 'enhanced select', 'woocommerce' ),
					'i18n_selection_too_long_1' => _x( 'You can only select 1 item', 'enhanced select', 'woocommerce' ),
					'i18n_selection_too_long_n' => _x( 'You can only select %qty% items', 'enhanced select', 'woocommerce' ),
					'i18n_load_more'            => _x( 'Loading more results&hellip;', 'enhanced select', 'woocommerce' ),
					'i18n_searching'            => _x( 'Searching&hellip;', 'enhanced select', 'woocommerce' ),
					'ajax_url'                  => admin_url( 'admin-ajax.php' ),
					'search_products_nonce'     => wp_create_nonce( 'search-products' ),
					'search_customers_nonce'    => wp_create_nonce( 'search-customers' ),
					'search_categories_nonce'   => wp_create_nonce( 'search-categories' ),
					'search_pages_nonce'        => wp_create_nonce( 'search-pages' ),
				)
			);

			wp_register_script( 'accounting', WC()->plugin_url() . '/assets/js/accounting/accounting' . $suffix . '.js', array( 'jquery' ), '0.4.2' );
			wp_localize_script(
				'accounting',
				'accounting_params',
				array(
					'mon_decimal_point' => wc_get_price_decimal_separator(),
				)
			);

			wp_register_script( 'wc-orders', WC()->plugin_url() . '/assets/js/admin/wc-orders' . $suffix . '.js', array( 'jquery', 'wp-util', 'underscore', 'backbone', 'jquery-blockui' ), $version );
			wp_localize_script(
				'wc-orders',
				'wc_orders_params',
				array(
					'ajax_url'      => admin_url( 'admin-ajax.php' ),
					'preview_nonce' => wp_create_nonce( 'woocommerce-preview-order' ),
				)
			);

			// WooCommerce admin pages.
			if ( in_array( $screen_id, wc_get_screen_ids() ) ) {
				wp_enqueue_script( 'iris' );
				wp_enqueue_script( 'woocommerce_admin' );
				wp_enqueue_script( 'wc-enhanced-select' );
				wp_enqueue_script( 'jquery-ui-sortable' );
				wp_enqueue_script( 'jquery-ui-autocomplete' );

				$locale  = localeconv();
				$decimal = isset( $locale['decimal_point'] ) ? $locale['decimal_point'] : '.';

				$params = array(
					/* translators: %s: decimal */
					'i18n_decimal_error'                => sprintf( __( 'Please enter with one decimal point (%s) without thousand separators.', 'woocommerce' ), $decimal ),
					/* translators: %s: price decimal separator */
					'i18n_mon_decimal_error'            => sprintf( __( 'Please enter with one monetary decimal point (%s) without thousand separators and currency symbols.', 'woocommerce' ), wc_get_price_decimal_separator() ),
					'i18n_country_iso_error'            => __( 'Please enter in country code with two capital letters.', 'woocommerce' ),
					'i18n_sale_less_than_regular_error' => __( 'Please enter in a value less than the regular price.', 'woocommerce' ),
					'i18n_delete_product_notice'        => __( 'This product has produced sales and may be linked to existing orders. Are you sure you want to delete it?', 'woocommerce' ),
					'i18n_remove_personal_data_notice'  => __( 'This action cannot be reversed. Are you sure you wish to erase personal data from the selected orders?', 'woocommerce' ),
					'decimal_point'                     => $decimal,
					'mon_decimal_point'                 => wc_get_price_decimal_separator(),
					'ajax_url'                          => admin_url( 'admin-ajax.php' ),
					'strings'                           => array(
						'import_products' => __( 'Import', 'woocommerce' ),
						'export_products' => __( 'Export', 'woocommerce' ),
					),
					'nonces'                            => array(
						'gateway_toggle' => wp_create_nonce( 'woocommerce-toggle-payment-gateway-enabled' ),
					),
					'urls'                              => array(
						'import_products' => current_user_can( 'import' ) ? esc_url_raw( admin_url( 'edit.php?post_type=product&page=product_importer' ) ) : null,
						'export_products' => current_user_can( 'export' ) ? esc_url_raw( admin_url( 'edit.php?post_type=product&page=product_exporter' ) ) : null,
					),
				);

				wp_localize_script( 'woocommerce_admin', 'woocommerce_admin', $params );
			}

			// Edit product category pages.
			if ( in_array( $screen_id, array( 'edit-product_cat' ) ) ) {
				wp_enqueue_media();
			}

			// Products.
			if ( in_array( $screen_id, array( 'edit-product' ) ) ) {
				wp_enqueue_script( 'woocommerce_quick-edit', WC()->plugin_url() . '/assets/js/admin/quick-edit' . $suffix . '.js', array( 'jquery', 'woocommerce_admin' ), $version );

				$params = array(
					'strings' => array(
						'allow_reviews' => esc_js( __( 'Enable reviews', 'woocommerce' ) ),
					),
				);

				wp_localize_script( 'woocommerce_quick-edit', 'woocommerce_quick_edit', $params );
			}

			// Meta boxes.
			if ( in_array( $screen_id, array( 'product', 'edit-product' ) ) ) {
				wp_enqueue_media();
				wp_register_script( 'wc-admin-product-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-product' . $suffix . '.js', array( 'wc-admin-meta-boxes', 'media-models' ), $version );
				wp_register_script( 'wc-admin-variation-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-product-variation' . $suffix . '.js', array( 'wc-admin-meta-boxes', 'serializejson', 'media-models' ), $version );

				wp_enqueue_script( 'wc-admin-product-meta-boxes' );
				wp_enqueue_script( 'wc-admin-variation-meta-boxes' );

				$params = array(
					'post_id'                             => isset( $post->ID ) ? $post->ID : '',
					'plugin_url'                          => WC()->plugin_url(),
					'ajax_url'                            => admin_url( 'admin-ajax.php' ),
					'woocommerce_placeholder_img_src'     => wc_placeholder_img_src(),
					'add_variation_nonce'                 => wp_create_nonce( 'add-variation' ),
					'link_variation_nonce'                => wp_create_nonce( 'link-variations' ),
					'delete_variations_nonce'             => wp_create_nonce( 'delete-variations' ),
					'load_variations_nonce'               => wp_create_nonce( 'load-variations' ),
					'save_variations_nonce'               => wp_create_nonce( 'save-variations' ),
					'bulk_edit_variations_nonce'          => wp_create_nonce( 'bulk-edit-variations' ),
					/* translators: %d: Number of variations */
					'i18n_link_all_variations'            => esc_js( sprintf( __( 'Are you sure you want to link all variations? This will create a new variation for each and every possible combination of variation attributes (max %d per run).', 'woocommerce' ), Constants::is_defined( 'WC_MAX_LINKED_VARIATIONS' ) ? Constants::get_constant( 'WC_MAX_LINKED_VARIATIONS' ) : 50 ) ),
					'i18n_enter_a_value'                  => esc_js( __( 'Enter a value', 'woocommerce' ) ),
					'i18n_enter_menu_order'               => esc_js( __( 'Variation menu order (determines position in the list of variations)', 'woocommerce' ) ),
					'i18n_enter_a_value_fixed_or_percent' => esc_js( __( 'Enter a value (fixed or %)', 'woocommerce' ) ),
					'i18n_delete_all_variations'          => esc_js( __( 'Are you sure you want to delete all variations? This cannot be undone.', 'woocommerce' ) ),
					'i18n_last_warning'                   => esc_js( __( 'Last warning, are you sure?', 'woocommerce' ) ),
					'i18n_choose_image'                   => esc_js( __( 'Choose an image', 'woocommerce' ) ),
					'i18n_set_image'                      => esc_js( __( 'Set variation image', 'woocommerce' ) ),
					'i18n_variation_added'                => esc_js( __( 'variation added', 'woocommerce' ) ),
					'i18n_variations_added'               => esc_js( __( 'variations added', 'woocommerce' ) ),
					'i18n_no_variations_added'            => esc_js( __( 'No variations added', 'woocommerce' ) ),
					'i18n_remove_variation'               => esc_js( __( 'Are you sure you want to remove this variation?', 'woocommerce' ) ),
					'i18n_scheduled_sale_start'           => esc_js( __( 'Sale start date (YYYY-MM-DD format or leave blank)', 'woocommerce' ) ),
					'i18n_scheduled_sale_end'             => esc_js( __( 'Sale end date (YYYY-MM-DD format or leave blank)', 'woocommerce' ) ),
					'i18n_edited_variations'              => esc_js( __( 'Save changes before changing page?', 'woocommerce' ) ),
					'i18n_variation_count_single'         => esc_js( __( '%qty% variation', 'woocommerce' ) ),
					'i18n_variation_count_plural'         => esc_js( __( '%qty% variations', 'woocommerce' ) ),
					'variations_per_page'                 => absint( apply_filters( 'woocommerce_admin_meta_boxes_variations_per_page', 15 ) ),
				);

				wp_localize_script( 'wc-admin-variation-meta-boxes', 'woocommerce_admin_meta_boxes_variations', $params );
			}
			if ( in_array( str_replace( 'edit-', '', $screen_id ), wc_get_order_types( 'order-meta-boxes' ) ) ) {
				$default_location = wc_get_customer_default_location();

				wp_enqueue_script( 'wc-admin-order-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-order' . $suffix . '.js', array( 'wc-admin-meta-boxes', 'wc-backbone-modal', 'selectWoo', 'wc-clipboard' ), $version );
				wp_localize_script(
					'wc-admin-order-meta-boxes',
					'woocommerce_admin_meta_boxes_order',
					array(
						'countries'              => wp_json_encode( array_merge( WC()->countries->get_allowed_country_states(), WC()->countries->get_shipping_country_states() ) ),
						'i18n_select_state_text' => esc_attr__( 'Select an option&hellip;', 'woocommerce' ),
						'default_country'        => isset( $default_location['country'] ) ? $default_location['country'] : '',
						'default_state'          => isset( $default_location['state'] ) ? $default_location['state'] : '',
						'placeholder_name'       => esc_attr__( 'Name (required)', 'woocommerce' ),
						'placeholder_value'      => esc_attr__( 'Value (required)', 'woocommerce' ),
					)
				);
			}
			if ( in_array( $screen_id, array( 'shop_coupon', 'edit-shop_coupon' ) ) ) {
				wp_enqueue_script( 'wc-admin-coupon-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-coupon' . $suffix . '.js', array( 'wc-admin-meta-boxes' ), $version );
				wp_localize_script(
					'wc-admin-coupon-meta-boxes',
					'woocommerce_admin_meta_boxes_coupon',
					array(
						'generate_button_text' => esc_html__( 'Generate coupon code', 'woocommerce' ),
						'characters'           => apply_filters( 'woocommerce_coupon_code_generator_characters', 'ABCDEFGHJKMNPQRSTUVWXYZ23456789' ),
						'char_length'          => apply_filters( 'woocommerce_coupon_code_generator_character_length', 8 ),
						'prefix'               => apply_filters( 'woocommerce_coupon_code_generator_prefix', '' ),
						'suffix'               => apply_filters( 'woocommerce_coupon_code_generator_suffix', '' ),
					)
				);
			}
			if ( in_array( str_replace( 'edit-', '', $screen_id ), array_merge( array( 'shop_coupon', 'product' ), wc_get_order_types( 'order-meta-boxes' ) ) ) ) {
				$post_id                = isset( $post->ID ) ? $post->ID : '';
				$currency               = '';
				$remove_item_notice     = __( 'Are you sure you want to remove the selected items?', 'woocommerce' );
				$remove_fee_notice      = __( 'Are you sure you want to remove the selected fees?', 'woocommerce' );
				$remove_shipping_notice = __( 'Are you sure you want to remove the selected shipping?', 'woocommerce' );

				if ( $post_id && in_array( get_post_type( $post_id ), wc_get_order_types( 'order-meta-boxes' ) ) ) {
					$order = wc_get_order( $post_id );
					if ( $order ) {
						$currency = $order->get_currency();

						if ( ! $order->has_status( array( 'pending', 'failed', 'cancelled' ) ) ) {
							$remove_item_notice = $remove_item_notice . ' ' . __( "You may need to manually restore the item's stock.", 'woocommerce' );
						}
					}
				}

				$params = array(
					'remove_item_notice'            => $remove_item_notice,
					'remove_fee_notice'             => $remove_fee_notice,
					'remove_shipping_notice'        => $remove_shipping_notice,
					'i18n_select_items'             => __( 'Please select some items.', 'woocommerce' ),
					'i18n_do_refund'                => __( 'Are you sure you wish to process this refund? This action cannot be undone.', 'woocommerce' ),
					'i18n_delete_refund'            => __( 'Are you sure you wish to delete this refund? This action cannot be undone.', 'woocommerce' ),
					'i18n_delete_tax'               => __( 'Are you sure you wish to delete this tax column? This action cannot be undone.', 'woocommerce' ),
					'remove_item_meta'              => __( 'Remove this item meta?', 'woocommerce' ),
					'remove_attribute'              => __( 'Remove this attribute?', 'woocommerce' ),
					'name_label'                    => __( 'Name', 'woocommerce' ),
					'remove_label'                  => __( 'Remove', 'woocommerce' ),
					'click_to_toggle'               => __( 'Click to toggle', 'woocommerce' ),
					'values_label'                  => __( 'Value(s)', 'woocommerce' ),
					'text_attribute_tip'            => __( 'Enter some text, or some attributes by pipe (|) separating values.', 'woocommerce' ),
					'visible_label'                 => __( 'Visible on the product page', 'woocommerce' ),
					'used_for_variations_label'     => __( 'Used for variations', 'woocommerce' ),
					'new_attribute_prompt'          => __( 'Enter a name for the new attribute term:', 'woocommerce' ),
					'calc_totals'                   => __( 'Recalculate totals? This will calculate taxes based on the customers country (or the store base country) and update totals.', 'woocommerce' ),
					'copy_billing'                  => __( 'Copy billing information to shipping information? This will remove any currently entered shipping information.', 'woocommerce' ),
					'load_billing'                  => __( "Load the customer's billing information? This will remove any currently entered billing information.", 'woocommerce' ),
					'load_shipping'                 => __( "Load the customer's shipping information? This will remove any currently entered shipping information.", 'woocommerce' ),
					'featured_label'                => __( 'Featured', 'woocommerce' ),
					'prices_include_tax'            => esc_attr( get_option( 'woocommerce_prices_include_tax' ) ),
					'tax_based_on'                  => esc_attr( get_option( 'woocommerce_tax_based_on' ) ),
					'round_at_subtotal'             => esc_attr( get_option( 'woocommerce_tax_round_at_subtotal' ) ),
					'no_customer_selected'          => __( 'No customer selected', 'woocommerce' ),
					'plugin_url'                    => WC()->plugin_url(),
					'ajax_url'                      => admin_url( 'admin-ajax.php' ),
					'order_item_nonce'              => wp_create_nonce( 'order-item' ),
					'add_attribute_nonce'           => wp_create_nonce( 'add-attribute' ),
					'save_attributes_nonce'         => wp_create_nonce( 'save-attributes' ),
					'calc_totals_nonce'             => wp_create_nonce( 'calc-totals' ),
					'get_customer_details_nonce'    => wp_create_nonce( 'get-customer-details' ),
					'search_products_nonce'         => wp_create_nonce( 'search-products' ),
					'grant_access_nonce'            => wp_create_nonce( 'grant-access' ),
					'revoke_access_nonce'           => wp_create_nonce( 'revoke-access' ),
					'add_order_note_nonce'          => wp_create_nonce( 'add-order-note' ),
					'delete_order_note_nonce'       => wp_create_nonce( 'delete-order-note' ),
					'calendar_image'                => WC()->plugin_url() . '/assets/images/calendar.png',
					'post_id'                       => isset( $post->ID ) ? $post->ID : '',
					'base_country'                  => WC()->countries->get_base_country(),
					'currency_format_num_decimals'  => wc_get_price_decimals(),
					'currency_format_symbol'        => get_woocommerce_currency_symbol( $currency ),
					'currency_format_decimal_sep'   => esc_attr( wc_get_price_decimal_separator() ),
					'currency_format_thousand_sep'  => esc_attr( wc_get_price_thousand_separator() ),
					'currency_format'               => esc_attr( str_replace( array( '%1$s', '%2$s' ), array( '%s', '%v' ), get_woocommerce_price_format() ) ), // For accounting JS.
					'rounding_precision'            => wc_get_rounding_precision(),
					'tax_rounding_mode'             => wc_get_tax_rounding_mode(),
					'product_types'                 => array_unique( array_merge( array( 'simple', 'grouped', 'variable', 'external' ), array_keys( wc_get_product_types() ) ) ),
					'i18n_download_permission_fail' => __( 'Could not grant access - the user may already have permission for this file or billing email is not set. Ensure the billing email is set, and the order has been saved.', 'woocommerce' ),
					'i18n_permission_revoke'        => __( 'Are you sure you want to revoke access to this download?', 'woocommerce' ),
					'i18n_tax_rate_already_exists'  => __( 'You cannot add the same tax rate twice!', 'woocommerce' ),
					'i18n_delete_note'              => __( 'Are you sure you wish to delete this note? This action cannot be undone.', 'woocommerce' ),
					'i18n_apply_coupon'             => __( 'Enter a coupon code to apply. Discounts are applied to line totals, before taxes.', 'woocommerce' ),
					'i18n_add_fee'                  => __( 'Enter a fixed amount or percentage to apply as a fee.', 'woocommerce' ),
				);

				wp_localize_script( 'wc-admin-meta-boxes', 'woocommerce_admin_meta_boxes', $params );
			}

			// Term ordering - only when sorting by term_order.
			if ( ( strstr( $screen_id, 'edit-pa_' ) || ( ! empty( $_GET['taxonomy'] ) && in_array( wp_unslash( $_GET['taxonomy'] ), apply_filters( 'woocommerce_sortable_taxonomies', array( 'product_cat' ) ) ) ) ) && ! isset( $_GET['orderby'] ) ) {

				wp_register_script( 'woocommerce_term_ordering', WC()->plugin_url() . '/assets/js/admin/term-ordering' . $suffix . '.js', array( 'jquery-ui-sortable' ), $version );
				wp_enqueue_script( 'woocommerce_term_ordering' );

				$taxonomy = isset( $_GET['taxonomy'] ) ? wc_clean( wp_unslash( $_GET['taxonomy'] ) ) : '';

				$woocommerce_term_order_params = array(
					'taxonomy' => $taxonomy,
				);

				wp_localize_script( 'woocommerce_term_ordering', 'woocommerce_term_ordering_params', $woocommerce_term_order_params );
			}

			// Product sorting - only when sorting by menu order on the products page.
			if ( current_user_can( 'edit_others_pages' ) && 'edit-product' === $screen_id && isset( $wp_query->query['orderby'] ) && 'menu_order title' === $wp_query->query['orderby'] ) {
				wp_register_script( 'woocommerce_product_ordering', WC()->plugin_url() . '/assets/js/admin/product-ordering' . $suffix . '.js', array( 'jquery-ui-sortable' ), $version, true );
				wp_enqueue_script( 'woocommerce_product_ordering' );
			}

			// Reports Pages.
			if ( in_array( $screen_id, apply_filters( 'woocommerce_reports_screen_ids', array( $wc_screen_id . '_page_wc-reports', 'toplevel_page_wc-reports', 'dashboard' ) ) ) ) {
				wp_register_script( 'wc-reports', WC()->plugin_url() . '/assets/js/admin/reports' . $suffix . '.js', array( 'jquery', 'jquery-ui-datepicker' ), $version );

				wp_enqueue_script( 'wc-reports' );
				wp_enqueue_script( 'flot' );
				wp_enqueue_script( 'flot-resize' );
				wp_enqueue_script( 'flot-time' );
				wp_enqueue_script( 'flot-pie' );
				wp_enqueue_script( 'flot-stack' );
			}

			// API settings.
			if ( $wc_screen_id . '_page_wc-settings' === $screen_id && isset( $_GET['section'] ) && 'keys' == $_GET['section'] ) {
				wp_register_script( 'wc-api-keys', WC()->plugin_url() . '/assets/js/admin/api-keys' . $suffix . '.js', array( 'jquery', 'woocommerce_admin', 'underscore', 'backbone', 'wp-util', 'qrcode', 'wc-clipboard' ), $version, true );
				wp_enqueue_script( 'wc-api-keys' );
				wp_localize_script(
					'wc-api-keys',
					'woocommerce_admin_api_keys',
					array(
						'ajax_url'         => admin_url( 'admin-ajax.php' ),
						'update_api_nonce' => wp_create_nonce( 'update-api-key' ),
						'clipboard_failed' => esc_html__( 'Copying to clipboard failed. Please press Ctrl/Cmd+C to copy.', 'woocommerce' ),
					)
				);
			}

			// System status.
			if ( $wc_screen_id . '_page_wc-status' === $screen_id ) {
				wp_register_script( 'wc-admin-system-status', WC()->plugin_url() . '/assets/js/admin/system-status' . $suffix . '.js', array( 'wc-clipboard' ), $version );
				wp_enqueue_script( 'wc-admin-system-status' );
				wp_localize_script(
					'wc-admin-system-status',
					'woocommerce_admin_system_status',
					array(
						'delete_log_confirmation' => esc_js( __( 'Are you sure you want to delete this log?', 'woocommerce' ) ),
						'run_tool_confirmation'   => esc_js( __( 'Are you sure you want to run this tool?', 'woocommerce' ) ),
					)
				);
			}

			if ( in_array( $screen_id, array( 'user-edit', 'profile' ) ) ) {
				wp_register_script( 'wc-users', WC()->plugin_url() . '/assets/js/admin/users' . $suffix . '.js', array( 'jquery', 'wc-enhanced-select', 'selectWoo' ), $version, true );
				wp_enqueue_script( 'wc-users' );
				wp_localize_script(
					'wc-users',
					'wc_users_params',
					array(
						'countries'              => wp_json_encode( array_merge( WC()->countries->get_allowed_country_states(), WC()->countries->get_shipping_country_states() ) ),
						'i18n_select_state_text' => esc_attr__( 'Select an option&hellip;', 'woocommerce' ),
					)
				);
			}

			if ( WC_Marketplace_Suggestions::show_suggestions_for_screen( $screen_id ) ) {
				$active_plugin_slugs = array_map( 'dirname', get_option( 'active_plugins' ) );
				wp_register_script(
					'marketplace-suggestions',
					WC()->plugin_url() . '/assets/js/admin/marketplace-suggestions' . $suffix . '.js',
					array( 'jquery', 'underscore', 'js-cookie' ),
					$version,
					true
				);
				wp_localize_script(
					'marketplace-suggestions',
					'marketplace_suggestions',
					array(
						'dismiss_suggestion_nonce' => wp_create_nonce( 'add_dismissed_marketplace_suggestion' ),
						'active_plugins'           => $active_plugin_slugs,
						'dismissed_suggestions'    => WC_Marketplace_Suggestions::get_dismissed_suggestions(),
						'suggestions_data'         => WC_Marketplace_Suggestions::get_suggestions_api_data(),
						'manage_suggestions_url'   => admin_url( 'admin.php?page=wc-settings&tab=advanced&section=woocommerce_com' ),
						'in_app_purchase_params'   => WC_Admin_Addons::get_in_app_purchase_url_params(),
						'i18n_marketplace_suggestions_default_cta'
							=> esc_html__( 'Learn More', 'woocommerce' ),
						'i18n_marketplace_suggestions_dismiss_tooltip'
							=> esc_attr__( 'Dismiss this suggestion', 'woocommerce' ),
						'i18n_marketplace_suggestions_manage_suggestions'
							=> esc_html__( 'Manage suggestions', 'woocommerce' ),
					)
				);
				wp_enqueue_script( 'marketplace-suggestions' );
			}

		}

	}

endif;

return new WC_Admin_Assets();
admin/class-wc-admin-status.php000064400000033646151336044770012516 0ustar00<?php
/**
 * Debug/Status page
 *
 * @package WooCommerce\Admin\System Status
 * @version 2.2.0
 */

use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Utilities\ArrayUtil;

defined( 'ABSPATH' ) || exit;

/**
 * WC_Admin_Status Class.
 */
class WC_Admin_Status {

	/**
	 * Handles output of the reports page in admin.
	 */
	public static function output() {
		include_once __DIR__ . '/views/html-admin-page-status.php';
	}

	/**
	 * Handles output of report.
	 */
	public static function status_report() {
		include_once __DIR__ . '/views/html-admin-page-status-report.php';
	}

	/**
	 * Handles output of tools.
	 */
	public static function status_tools() {
		if ( ! class_exists( 'WC_REST_System_Status_Tools_Controller' ) ) {
			wp_die( 'Cannot load the REST API to access WC_REST_System_Status_Tools_Controller.' );
		}

		$tools                 = self::get_tools();
		$tool_requires_refresh = false;

		if ( ! empty( $_GET['action'] ) && ! empty( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( wp_unslash( $_REQUEST['_wpnonce'] ), 'debug_action' ) ) { // WPCS: input var ok, sanitization ok.
			$tools_controller = new WC_REST_System_Status_Tools_Controller();
			$action           = wc_clean( wp_unslash( $_GET['action'] ) ); // WPCS: input var ok.

			if ( array_key_exists( $action, $tools ) ) {
				$response = $tools_controller->execute_tool( $action );

				$tool                  = $tools[ $action ];
				$tool_requires_refresh = ArrayUtil::get_value_or_default( $tool, 'requires_refresh', false );
				$tool                  = array(
					'id'          => $action,
					'name'        => $tool['name'],
					'action'      => $tool['button'],
					'description' => $tool['desc'],
					'disabled'    => ArrayUtil::get_value_or_default( $tool, 'disabled', false ),
				);
				$tool                  = array_merge( $tool, $response );

				/**
				 * Fires after a WooCommerce system status tool has been executed.
				 *
				 * @param array  $tool  Details about the tool that has been executed.
				 */
				do_action( 'woocommerce_system_status_tool_executed', $tool );
			} else {
				$response = array(
					'success' => false,
					'message' => __( 'Tool does not exist.', 'woocommerce' ),
				);
			}

			if ( $response['success'] ) {
				echo '<div class="updated inline"><p>' . esc_html( $response['message'] ) . '</p></div>';
			} else {
				echo '<div class="error inline"><p>' . esc_html( $response['message'] ) . '</p></div>';
			}
		}

		// Display message if settings settings have been saved.
		if ( isset( $_REQUEST['settings-updated'] ) ) { // WPCS: input var ok.
			echo '<div class="updated inline"><p>' . esc_html__( 'Your changes have been saved.', 'woocommerce' ) . '</p></div>';
		}

		if ( $tool_requires_refresh ) {
			$tools = self::get_tools();
		}

		include_once __DIR__ . '/views/html-admin-page-status-tools.php';
	}

	/**
	 * Get tools.
	 *
	 * @return array of tools
	 */
	public static function get_tools() {
		$tools_controller = new WC_REST_System_Status_Tools_Controller();
		return $tools_controller->get_tools();
	}

	/**
	 * Show the logs page.
	 */
	public static function status_logs() {
		$log_handler = Constants::get_constant( 'WC_LOG_HANDLER' );

		if ( 'WC_Log_Handler_DB' === $log_handler ) {
			self::status_logs_db();
		} else {
			self::status_logs_file();
		}
	}

	/**
	 * Show the log page contents for file log handler.
	 */
	public static function status_logs_file() {
		$logs = self::scan_log_files();

		if ( ! empty( $_REQUEST['log_file'] ) && isset( $logs[ sanitize_title( wp_unslash( $_REQUEST['log_file'] ) ) ] ) ) { // WPCS: input var ok, CSRF ok.
			$viewed_log = $logs[ sanitize_title( wp_unslash( $_REQUEST['log_file'] ) ) ]; // WPCS: input var ok, CSRF ok.
		} elseif ( ! empty( $logs ) ) {
			$viewed_log = current( $logs );
		}

		$handle = ! empty( $viewed_log ) ? self::get_log_file_handle( $viewed_log ) : '';

		if ( ! empty( $_REQUEST['handle'] ) ) { // WPCS: input var ok, CSRF ok.
			self::remove_log();
		}

		include_once __DIR__ . '/views/html-admin-page-status-logs.php';
	}

	/**
	 * Show the log page contents for db log handler.
	 */
	public static function status_logs_db() {
		if ( ! empty( $_REQUEST['flush-logs'] ) ) { // WPCS: input var ok, CSRF ok.
			self::flush_db_logs();
		}

		if ( isset( $_REQUEST['action'] ) && isset( $_REQUEST['log'] ) ) { // WPCS: input var ok, CSRF ok.
			self::log_table_bulk_actions();
		}

		$log_table_list = new WC_Admin_Log_Table_List();
		$log_table_list->prepare_items();

		include_once __DIR__ . '/views/html-admin-page-status-logs-db.php';
	}

	/**
	 * Retrieve metadata from a file. Based on WP Core's get_file_data function.
	 *
	 * @since  2.1.1
	 * @param  string $file Path to the file.
	 * @return string
	 */
	public static function get_file_version( $file ) {

		// Avoid notices if file does not exist.
		if ( ! file_exists( $file ) ) {
			return '';
		}

		// We don't need to write to the file, so just open for reading.
		$fp = fopen( $file, 'r' ); // @codingStandardsIgnoreLine.

		// Pull only the first 8kiB of the file in.
		$file_data = fread( $fp, 8192 ); // @codingStandardsIgnoreLine.

		// PHP will close file handle, but we are good citizens.
		fclose( $fp ); // @codingStandardsIgnoreLine.

		// Make sure we catch CR-only line endings.
		$file_data = str_replace( "\r", "\n", $file_data );
		$version   = '';

		if ( preg_match( '/^[ \t\/*#@]*' . preg_quote( '@version', '/' ) . '(.*)$/mi', $file_data, $match ) && $match[1] ) {
			$version = _cleanup_header_comment( $match[1] );
		}

		return $version;
	}

	/**
	 * Return the log file handle.
	 *
	 * @param string $filename Filename to get the handle for.
	 * @return string
	 */
	public static function get_log_file_handle( $filename ) {
		return substr( $filename, 0, strlen( $filename ) > 48 ? strlen( $filename ) - 48 : strlen( $filename ) - 4 );
	}

	/**
	 * Scan the template files.
	 *
	 * @param  string $template_path Path to the template directory.
	 * @return array
	 */
	public static function scan_template_files( $template_path ) {
		$files  = @scandir( $template_path ); // @codingStandardsIgnoreLine.
		$result = array();

		if ( ! empty( $files ) ) {

			foreach ( $files as $key => $value ) {

				if ( ! in_array( $value, array( '.', '..' ), true ) ) {

					if ( is_dir( $template_path . DIRECTORY_SEPARATOR . $value ) ) {
						$sub_files = self::scan_template_files( $template_path . DIRECTORY_SEPARATOR . $value );
						foreach ( $sub_files as $sub_file ) {
							$result[] = $value . DIRECTORY_SEPARATOR . $sub_file;
						}
					} else {
						$result[] = $value;
					}
				}
			}
		}
		return $result;
	}

	/**
	 * Scan the log files.
	 *
	 * @return array
	 */
	public static function scan_log_files() {
		return WC_Log_Handler_File::get_log_files();
	}

	/**
	 * Get latest version of a theme by slug.
	 *
	 * @param  object $theme WP_Theme object.
	 * @return string Version number if found.
	 */
	public static function get_latest_theme_version( $theme ) {
		include_once ABSPATH . 'wp-admin/includes/theme.php';

		$api = themes_api(
			'theme_information',
			array(
				'slug'   => $theme->get_stylesheet(),
				'fields' => array(
					'sections' => false,
					'tags'     => false,
				),
			)
		);

		$update_theme_version = 0;

		// Check .org for updates.
		if ( is_object( $api ) && ! is_wp_error( $api ) ) {
			$update_theme_version = $api->version;
		} elseif ( strstr( $theme->{'Author URI'}, 'woothemes' ) ) { // Check WooThemes Theme Version.
			$theme_dir          = substr( strtolower( str_replace( ' ', '', $theme->Name ) ), 0, 45 ); // @codingStandardsIgnoreLine.
			$theme_version_data = get_transient( $theme_dir . '_version_data' );

			if ( false === $theme_version_data ) {
				$theme_changelog = wp_safe_remote_get( 'http://dzv365zjfbd8v.cloudfront.net/changelogs/' . $theme_dir . '/changelog.txt' );
				$cl_lines        = explode( "\n", wp_remote_retrieve_body( $theme_changelog ) );
				if ( ! empty( $cl_lines ) ) {
					foreach ( $cl_lines as $line_num => $cl_line ) {
						if ( preg_match( '/^[0-9]/', $cl_line ) ) {
							$theme_date         = str_replace( '.', '-', trim( substr( $cl_line, 0, strpos( $cl_line, '-' ) ) ) );
							$theme_version      = preg_replace( '~[^0-9,.]~', '', stristr( $cl_line, 'version' ) );
							$theme_update       = trim( str_replace( '*', '', $cl_lines[ $line_num + 1 ] ) );
							$theme_version_data = array(
								'date'      => $theme_date,
								'version'   => $theme_version,
								'update'    => $theme_update,
								'changelog' => $theme_changelog,
							);
							set_transient( $theme_dir . '_version_data', $theme_version_data, DAY_IN_SECONDS );
							break;
						}
					}
				}
			}

			if ( ! empty( $theme_version_data['version'] ) ) {
				$update_theme_version = $theme_version_data['version'];
			}
		}

		return $update_theme_version;
	}

	/**
	 * Remove/delete the chosen file.
	 */
	public static function remove_log() {
		if ( empty( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( wp_unslash( $_REQUEST['_wpnonce'] ), 'remove_log' ) ) { // WPCS: input var ok, sanitization ok.
			wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) );
		}

		if ( ! empty( $_REQUEST['handle'] ) ) {  // WPCS: input var ok.
			$log_handler = new WC_Log_Handler_File();
			$log_handler->remove( wp_unslash( $_REQUEST['handle'] ) ); // WPCS: input var ok, sanitization ok.
		}

		wp_safe_redirect( esc_url_raw( admin_url( 'admin.php?page=wc-status&tab=logs' ) ) );
		exit();
	}

	/**
	 * Clear DB log table.
	 *
	 * @since 3.0.0
	 */
	private static function flush_db_logs() {
		if ( empty( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'woocommerce-status-logs' ) ) { // WPCS: input var ok, sanitization ok.
			wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) );
		}

		WC_Log_Handler_DB::flush();

		wp_safe_redirect( esc_url_raw( admin_url( 'admin.php?page=wc-status&tab=logs' ) ) );
		exit();
	}

	/**
	 * Bulk DB log table actions.
	 *
	 * @since 3.0.0
	 */
	private static function log_table_bulk_actions() {
		if ( empty( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'woocommerce-status-logs' ) ) { // WPCS: input var ok, sanitization ok.
			wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) );
		}

		$log_ids = array_map( 'absint', (array) isset( $_REQUEST['log'] ) ? wp_unslash( $_REQUEST['log'] ) : array() ); // WPCS: input var ok, sanitization ok.

		if ( ( isset( $_REQUEST['action'] ) && 'delete' === $_REQUEST['action'] ) || ( isset( $_REQUEST['action2'] ) && 'delete' === $_REQUEST['action2'] ) ) { // WPCS: input var ok, sanitization ok.
			WC_Log_Handler_DB::delete( $log_ids );
			wp_safe_redirect( esc_url_raw( admin_url( 'admin.php?page=wc-status&tab=logs' ) ) );
			exit();
		}
	}

	/**
	 * Prints table info if a base table is not present.
	 */
	private static function output_tables_info() {
		$missing_tables = WC_Install::verify_base_tables( false );
		if ( 0 === count( $missing_tables ) ) {
			return;
		}
		?>

		<br>
		<strong style="color:#a00;">
			<span class="dashicons dashicons-warning"></span>
			<?php
				echo esc_html(
					sprintf(
					// translators: Comma seperated list of missing tables.
						__( 'Missing base tables: %s. Some WooCommerce functionality may not work as expected.', 'woocommerce' ),
						implode( ', ', $missing_tables )
					)
				);
			?>
		</strong>

		<?php
	}

	/**
	 * Prints the information about plugins for the system status report.
	 * Used for both active and inactive plugins sections.
	 *
	 * @param array $plugins List of plugins to display.
	 * @param array $untested_plugins List of plugins that haven't been tested with the current WooCommerce version.
	 * @return void
	 */
	private static function output_plugins_info( $plugins, $untested_plugins ) {
		$wc_version = Constants::get_constant( 'WC_VERSION' );

		if ( 'major' === Constants::get_constant( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE' ) ) {
			// Since we're only testing against major, we don't need to show minor and patch version.
			$wc_version = $wc_version[0] . '.0';
		}

		foreach ( $plugins as $plugin ) {
			if ( ! empty( $plugin['name'] ) ) {
				// Link the plugin name to the plugin url if available.
				$plugin_name = esc_html( $plugin['name'] );
				if ( ! empty( $plugin['url'] ) ) {
					$plugin_name = '<a href="' . esc_url( $plugin['url'] ) . '" aria-label="' . esc_attr__( 'Visit plugin homepage', 'woocommerce' ) . '" target="_blank">' . $plugin_name . '</a>';
				}

				$has_newer_version = false;
				$version_string    = $plugin['version'];
				$network_string    = '';
				if ( strstr( $plugin['url'], 'woothemes.com' ) || strstr( $plugin['url'], 'woocommerce.com' ) ) {
					if ( ! empty( $plugin['version_latest'] ) && version_compare( $plugin['version_latest'], $plugin['version'], '>' ) ) {
						/* translators: 1: current version. 2: latest version */
						$version_string = sprintf( __( '%1$s (update to version %2$s is available)', 'woocommerce' ), $plugin['version'], $plugin['version_latest'] );
					}

					if ( false !== $plugin['network_activated'] ) {
						$network_string = ' &ndash; <strong style="color: black;">' . esc_html__( 'Network enabled', 'woocommerce' ) . '</strong>';
					}
				}
				$untested_string = '';
				if ( array_key_exists( $plugin['plugin'], $untested_plugins ) ) {
					$untested_string = ' &ndash; <strong style="color: #a00;">';

					/* translators: %s: version */
					$untested_string .= esc_html( sprintf( __( 'Installed version not tested with active version of WooCommerce %s', 'woocommerce' ), $wc_version ) );

					$untested_string .= '</strong>';
				}
				?>
				<tr>
					<td><?php echo wp_kses_post( $plugin_name ); ?></td>
					<td class="help">&nbsp;</td>
					<td>
						<?php
						/* translators: %s: plugin author */
						printf( esc_html__( 'by %s', 'woocommerce' ), esc_html( $plugin['author_name'] ) );
						echo ' &ndash; ' . esc_html( $version_string ) . $untested_string . $network_string; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
						?>
					</td>
				</tr>
				<?php
			}
		}
	}
}
admin/class-wc-admin-menus.php000064400000041220151336044770012305 0ustar00<?php
/**
 * Setup menus in WP admin.
 *
 * @package WooCommerce\Admin
 * @version 2.5.0
 */

defined( 'ABSPATH' ) || exit;

if ( class_exists( 'WC_Admin_Menus', false ) ) {
	return new WC_Admin_Menus();
}

/**
 * WC_Admin_Menus Class.
 */
class WC_Admin_Menus {

	/**
	 * Hook in tabs.
	 */
	public function __construct() {
		// Add menus.
		add_action( 'admin_menu', array( $this, 'menu_highlight' ) );
		add_action( 'admin_menu', array( $this, 'menu_order_count' ) );
		add_action( 'admin_menu', array( $this, 'admin_menu' ), 9 );
		add_action( 'admin_menu', array( $this, 'reports_menu' ), 20 );
		add_action( 'admin_menu', array( $this, 'settings_menu' ), 50 );
		add_action( 'admin_menu', array( $this, 'status_menu' ), 60 );

		if ( apply_filters( 'woocommerce_show_addons_page', true ) ) {
			add_action( 'admin_menu', array( $this, 'addons_menu' ), 70 );
		}

		add_filter( 'menu_order', array( $this, 'menu_order' ) );
		add_filter( 'custom_menu_order', array( $this, 'custom_menu_order' ) );
		add_filter( 'set-screen-option', array( $this, 'set_screen_option' ), 10, 3 );
		add_filter( 'submenu_file', array( $this, 'update_menu_highlight' ), 10, 2 );
		add_filter( 'admin_title', array( $this, 'update_my_subscriptions_title' ) );

		// Add endpoints custom URLs in Appearance > Menus > Pages.
		add_action( 'admin_head-nav-menus.php', array( $this, 'add_nav_menu_meta_boxes' ) );

		// Admin bar menus.
		if ( apply_filters( 'woocommerce_show_admin_bar_visit_store', true ) ) {
			add_action( 'admin_bar_menu', array( $this, 'admin_bar_menus' ), 31 );
		}

		// Handle saving settings earlier than load-{page} hook to avoid race conditions in conditional menus.
		add_action( 'wp_loaded', array( $this, 'save_settings' ) );
	}

	/**
	 * Add menu items.
	 */
	public function admin_menu() {
		global $menu;

		$woocommerce_icon = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiPjxwYXRoIGZpbGw9IiNhMmFhYjIiIGQ9Ik02MTIuMTkyIDQyNi4zMzZjMC02Ljg5Ni0zLjEzNi01MS42LTI4LTUxLjYtMzcuMzYgMC00Ni43MDQgNzIuMjU2LTQ2LjcwNCA4Mi42MjQgMCAzLjQwOCAzLjE1MiA1OC40OTYgMjguMDMyIDU4LjQ5NiAzNC4xOTItLjAzMiA0Ni42NzItNzIuMjg4IDQ2LjY3Mi04OS41MnptMjAyLjE5MiAwYzAtNi44OTYtMy4xNTItNTEuNi0yOC4wMzItNTEuNi0zNy4yOCAwLTQ2LjYwOCA3Mi4yNTYtNDYuNjA4IDgyLjYyNCAwIDMuNDA4IDMuMDcyIDU4LjQ5NiAyNy45NTIgNTguNDk2IDM0LjE5Mi0uMDMyIDQ2LjY4OC03Mi4yODggNDYuNjg4LTg5LjUyek0xNDEuMjk2Ljc2OGMtNjguMjI0IDAtMTIzLjUwNCA1NS40ODgtMTIzLjUwNCAxMjMuOTJ2NjUwLjcyYzAgNjguNDMyIDU1LjI5NiAxMjMuOTIgMTIzLjUwNCAxMjMuOTJoMzM5LjgwOGwxMjMuNTA0IDEyMy45MzZWODk5LjMyOGgyNzguMDQ4YzY4LjIyNCAwIDEyMy41Mi01NS40NzIgMTIzLjUyLTEyMy45MnYtNjUwLjcyYzAtNjguNDMyLTU1LjI5Ni0xMjMuOTItMTIzLjUyLTEyMy45MmgtNzQxLjM2em01MjYuODY0IDQyMi4xNmMwIDU1LjA4OC0zMS4wODggMTU0Ljg4LTEwMi42NCAxNTQuODgtNi4yMDggMC0xOC40OTYtMy42MTYtMjUuNDI0LTYuMDE2LTMyLjUxMi0xMS4xNjgtNTAuMTkyLTQ5LjY5Ni01Mi4zNTItNjYuMjU2IDAgMC0zLjA3Mi0xNy43OTItMy4wNzItNDAuNzUyIDAtMjIuOTkyIDMuMDcyLTQ1LjMyOCAzLjA3Mi00NS4zMjggMTUuNTUyLTc1LjcyOCA0My41NTItMTA2LjczNiA5Ni40NDgtMTA2LjczNiA1OS4wNzItLjAzMiA4My45NjggNTguNTI4IDgzLjk2OCAxMTAuMjA4ek00ODYuNDk2IDMwMi40YzAgMy4zOTItNDMuNTUyIDE0MS4xNjgtNDMuNTUyIDIxMy40MjR2NzUuNzEyYy0yLjU5MiAxMi4wOC00LjE2IDI0LjE0NC0yMS44MjQgMjQuMTQ0LTQ2LjYwOCAwLTg4Ljg4LTE1MS40NzItOTIuMDE2LTE2MS44NC02LjIwOCA2Ljg5Ni02Mi4yNCAxNjEuODQtOTYuNDQ4IDE2MS44NC0yNC44NjQgMC00My41NTItMTEzLjY0OC00Ni42MDgtMTIzLjkzNkMxNzYuNzA0IDQzNi42NzIgMTYwIDMzNC4yMjQgMTYwIDMyNy4zMjhjMC0yMC42NzIgMS4xNTItMzguNzM2IDI2LjA0OC0zOC43MzYgNi4yMDggMCAyMS42IDYuMDY0IDIzLjcxMiAxNy4xNjggMTEuNjQ4IDYyLjAzMiAxNi42ODggMTIwLjUxMiAyOS4xNjggMTg1Ljk2OCAxLjg1NiAyLjkyOCAxLjUwNCA3LjAwOCA0LjU2IDEwLjQzMiAzLjE1Mi0xMC4yODggNjYuOTI4LTE2OC43ODQgOTQuOTYtMTY4Ljc4NCAyMi41NDQgMCAzMC40IDQ0LjU5MiAzMy41MzYgNjEuODI0IDYuMjA4IDIwLjY1NiAxMy4wODggNTUuMjE2IDIyLjQxNiA4Mi43NTIgMC0xMy43NzYgMTIuNDgtMjAzLjEyIDY1LjM5Mi0yMDMuMTIgMTguNTkyLjAzMiAyNi43MDQgNi45MjggMjYuNzA0IDI3LjU2OHpNODcwLjMyIDQyMi45MjhjMCA1NS4wODgtMzEuMDg4IDE1NC44OC0xMDIuNjQgMTU0Ljg4LTYuMTkyIDAtMTguNDQ4LTMuNjE2LTI1LjQyNC02LjAxNi0zMi40MzItMTEuMTY4LTUwLjE3Ni00OS42OTYtNTIuMjg4LTY2LjI1NiAwIDAtMy44ODgtMTcuOTItMy44ODgtNDAuODk2czMuODg4LTQ1LjE4NCAzLjg4OC00NS4xODRjMTUuNTUyLTc1LjcyOCA0My40ODgtMTA2LjczNiA5Ni4zODQtMTA2LjczNiA1OS4xMDQtLjAzMiA4My45NjggNTguNTI4IDgzLjk2OCAxMTAuMjA4eiIvPjwvc3ZnPg==';

		if ( current_user_can( 'edit_others_shop_orders' ) ) {
			$menu[] = array( '', 'read', 'separator-woocommerce', '', 'wp-menu-separator woocommerce' ); // WPCS: override ok.
		}

		add_menu_page( __( 'WooCommerce', 'woocommerce' ), __( 'WooCommerce', 'woocommerce' ), 'edit_others_shop_orders', 'woocommerce', null, $woocommerce_icon, '55.5' );

		add_submenu_page( 'edit.php?post_type=product', __( 'Attributes', 'woocommerce' ), __( 'Attributes', 'woocommerce' ), 'manage_product_terms', 'product_attributes', array( $this, 'attributes_page' ) );
	}

	/**
	 * Add menu item.
	 */
	public function reports_menu() {
		if ( current_user_can( 'edit_others_shop_orders' ) ) {
			add_submenu_page( 'woocommerce', __( 'Reports', 'woocommerce' ), __( 'Reports', 'woocommerce' ), 'view_woocommerce_reports', 'wc-reports', array( $this, 'reports_page' ) );
		} else {
			add_menu_page( __( 'Sales reports', 'woocommerce' ), __( 'Sales reports', 'woocommerce' ), 'view_woocommerce_reports', 'wc-reports', array( $this, 'reports_page' ), 'dashicons-chart-bar', '55.6' );
		}
	}

	/**
	 * Add menu item.
	 */
	public function settings_menu() {
		$settings_page = add_submenu_page( 'woocommerce', __( 'WooCommerce settings', 'woocommerce' ), __( 'Settings', 'woocommerce' ), 'manage_woocommerce', 'wc-settings', array( $this, 'settings_page' ) );

		add_action( 'load-' . $settings_page, array( $this, 'settings_page_init' ) );
	}

	/**
	 * Loads gateways and shipping methods into memory for use within settings.
	 */
	public function settings_page_init() {
		WC()->payment_gateways();
		WC()->shipping();

		// Include settings pages.
		WC_Admin_Settings::get_settings_pages();

		// Add any posted messages.
		if ( ! empty( $_GET['wc_error'] ) ) { // WPCS: input var okay, CSRF ok.
			WC_Admin_Settings::add_error( wp_kses_post( wp_unslash( $_GET['wc_error'] ) ) ); // WPCS: input var okay, CSRF ok.
		}

		if ( ! empty( $_GET['wc_message'] ) ) { // WPCS: input var okay, CSRF ok.
			WC_Admin_Settings::add_message( wp_kses_post( wp_unslash( $_GET['wc_message'] ) ) ); // WPCS: input var okay, CSRF ok.
		}

		do_action( 'woocommerce_settings_page_init' );
	}

	/**
	 * Handle saving of settings.
	 *
	 * @return void
	 */
	public function save_settings() {
		global $current_tab, $current_section;

		// We should only save on the settings page.
		if ( ! is_admin() || ! isset( $_GET['page'] ) || 'wc-settings' !== $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			return;
		}

		// Include settings pages.
		WC_Admin_Settings::get_settings_pages();

		// Get current tab/section.
		$current_tab     = empty( $_GET['tab'] ) ? 'general' : sanitize_title( wp_unslash( $_GET['tab'] ) ); // WPCS: input var okay, CSRF ok.
		$current_section = empty( $_REQUEST['section'] ) ? '' : sanitize_title( wp_unslash( $_REQUEST['section'] ) ); // WPCS: input var okay, CSRF ok.

		// Save settings if data has been posted.
		if ( '' !== $current_section && apply_filters( "woocommerce_save_settings_{$current_tab}_{$current_section}", ! empty( $_POST['save'] ) ) ) { // WPCS: input var okay, CSRF ok.
			WC_Admin_Settings::save();
		} elseif ( '' === $current_section && apply_filters( "woocommerce_save_settings_{$current_tab}", ! empty( $_POST['save'] ) ) ) { // WPCS: input var okay, CSRF ok.
			WC_Admin_Settings::save();
		}
	}

	/**
	 * Add menu item.
	 */
	public function status_menu() {
		add_submenu_page( 'woocommerce', __( 'WooCommerce status', 'woocommerce' ), __( 'Status', 'woocommerce' ), 'manage_woocommerce', 'wc-status', array( $this, 'status_page' ) );
	}

	/**
	 * Addons menu item.
	 */
	public function addons_menu() {
		$count_html = WC_Helper_Updater::get_updates_count_html();
		/* translators: %s: extensions count */
		$menu_title = sprintf( __( 'My Subscriptions %s', 'woocommerce' ), $count_html );
		add_submenu_page( 'woocommerce', __( 'WooCommerce Marketplace', 'woocommerce' ), __( 'Marketplace', 'woocommerce' ), 'manage_woocommerce', 'wc-addons', array( $this, 'addons_page' ) );
		add_submenu_page( 'woocommerce', __( 'My WooCommerce.com Subscriptions', 'woocommerce' ), $menu_title, 'manage_woocommerce', 'wc-addons&section=helper', array( $this, 'addons_page' ) );
	}

	/**
	 * Highlights the correct top level admin menu item for post type add screens.
	 */
	public function menu_highlight() {
		global $parent_file, $submenu_file, $post_type;

		switch ( $post_type ) {
			case 'shop_order':
			case 'shop_coupon':
				$parent_file = 'woocommerce'; // WPCS: override ok.
				break;
			case 'product':
				$screen = get_current_screen();
				if ( $screen && taxonomy_is_product_attribute( $screen->taxonomy ) ) {
					$submenu_file = 'product_attributes'; // WPCS: override ok.
					$parent_file  = 'edit.php?post_type=product'; // WPCS: override ok.
				}
				break;
		}
	}

	/**
	 * Adds the order processing count to the menu.
	 */
	public function menu_order_count() {
		global $submenu;

		if ( isset( $submenu['woocommerce'] ) ) {
			// Remove 'WooCommerce' sub menu item.
			unset( $submenu['woocommerce'][0] );

			// Add count if user has access.
			if ( apply_filters( 'woocommerce_include_processing_order_count_in_menu', true ) && current_user_can( 'edit_others_shop_orders' ) ) {
				$order_count = apply_filters( 'woocommerce_menu_order_count', wc_processing_order_count() );

				if ( $order_count ) {
					foreach ( $submenu['woocommerce'] as $key => $menu_item ) {
						if ( 0 === strpos( $menu_item[0], _x( 'Orders', 'Admin menu name', 'woocommerce' ) ) ) {
							$submenu['woocommerce'][ $key ][0] .= ' <span class="awaiting-mod update-plugins count-' . esc_attr( $order_count ) . '"><span class="processing-count">' . number_format_i18n( $order_count ) . '</span></span>'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
							break;
						}
					}
				}
			}
		}
	}

	/**
	 * Reorder the WC menu items in admin.
	 *
	 * @param int $menu_order Menu order.
	 * @return array
	 */
	public function menu_order( $menu_order ) {
		// Initialize our custom order array.
		$woocommerce_menu_order = array();

		// Get the index of our custom separator.
		$woocommerce_separator = array_search( 'separator-woocommerce', $menu_order, true );

		// Get index of product menu.
		$woocommerce_product = array_search( 'edit.php?post_type=product', $menu_order, true );

		// Loop through menu order and do some rearranging.
		foreach ( $menu_order as $index => $item ) {

			if ( 'woocommerce' === $item ) {
				$woocommerce_menu_order[] = 'separator-woocommerce';
				$woocommerce_menu_order[] = $item;
				$woocommerce_menu_order[] = 'edit.php?post_type=product';
				unset( $menu_order[ $woocommerce_separator ] );
				unset( $menu_order[ $woocommerce_product ] );
			} elseif ( ! in_array( $item, array( 'separator-woocommerce' ), true ) ) {
				$woocommerce_menu_order[] = $item;
			}
		}

		// Return order.
		return $woocommerce_menu_order;
	}

	/**
	 * Custom menu order.
	 *
	 * @param bool $enabled Whether custom menu ordering is already enabled.
	 * @return bool
	 */
	public function custom_menu_order( $enabled ) {
		return $enabled || current_user_can( 'edit_others_shop_orders' );
	}

	/**
	 * Validate screen options on update.
	 *
	 * @param bool|int $status Screen option value. Default false to skip.
	 * @param string   $option The option name.
	 * @param int      $value  The number of rows to use.
	 */
	public function set_screen_option( $status, $option, $value ) {
		if ( in_array( $option, array( 'woocommerce_keys_per_page', 'woocommerce_webhooks_per_page' ), true ) ) {
			return $value;
		}

		return $status;
	}

	/**
	 * Init the reports page.
	 */
	public function reports_page() {
		WC_Admin_Reports::output();
	}

	/**
	 * Init the settings page.
	 */
	public function settings_page() {
		WC_Admin_Settings::output();
	}

	/**
	 * Init the attributes page.
	 */
	public function attributes_page() {
		WC_Admin_Attributes::output();
	}

	/**
	 * Init the status page.
	 */
	public function status_page() {
		WC_Admin_Status::output();
	}

	/**
	 * Init the addons page.
	 */
	public function addons_page() {
		WC_Admin_Addons::output();
	}

	/**
	 * Add custom nav meta box.
	 *
	 * Adapted from http://www.johnmorrisonline.com/how-to-add-a-fully-functional-custom-meta-box-to-wordpress-navigation-menus/.
	 */
	public function add_nav_menu_meta_boxes() {
		add_meta_box( 'woocommerce_endpoints_nav_link', __( 'WooCommerce endpoints', 'woocommerce' ), array( $this, 'nav_menu_links' ), 'nav-menus', 'side', 'low' );
	}

	/**
	 * Output menu links.
	 */
	public function nav_menu_links() {
		// Get items from account menu.
		$endpoints = wc_get_account_menu_items();

		// Remove dashboard item.
		if ( isset( $endpoints['dashboard'] ) ) {
			unset( $endpoints['dashboard'] );
		}

		// Include missing lost password.
		$endpoints['lost-password'] = __( 'Lost password', 'woocommerce' );

		$endpoints = apply_filters( 'woocommerce_custom_nav_menu_items', $endpoints );

		?>
		<div id="posttype-woocommerce-endpoints" class="posttypediv">
			<div id="tabs-panel-woocommerce-endpoints" class="tabs-panel tabs-panel-active">
				<ul id="woocommerce-endpoints-checklist" class="categorychecklist form-no-clear">
					<?php
					$i = -1;
					foreach ( $endpoints as $key => $value ) :
						?>
						<li>
							<label class="menu-item-title">
								<input type="checkbox" class="menu-item-checkbox" name="menu-item[<?php echo esc_attr( $i ); ?>][menu-item-object-id]" value="<?php echo esc_attr( $i ); ?>" /> <?php echo esc_html( $value ); ?>
							</label>
							<input type="hidden" class="menu-item-type" name="menu-item[<?php echo esc_attr( $i ); ?>][menu-item-type]" value="custom" />
							<input type="hidden" class="menu-item-title" name="menu-item[<?php echo esc_attr( $i ); ?>][menu-item-title]" value="<?php echo esc_attr( $value ); ?>" />
							<input type="hidden" class="menu-item-url" name="menu-item[<?php echo esc_attr( $i ); ?>][menu-item-url]" value="<?php echo esc_url( wc_get_account_endpoint_url( $key ) ); ?>" />
							<input type="hidden" class="menu-item-classes" name="menu-item[<?php echo esc_attr( $i ); ?>][menu-item-classes]" />
						</li>
						<?php
						$i--;
					endforeach;
					?>
				</ul>
			</div>
			<p class="button-controls">
				<span class="list-controls">
					<a href="<?php echo esc_url( admin_url( 'nav-menus.php?page-tab=all&selectall=1#posttype-woocommerce-endpoints' ) ); ?>" class="select-all"><?php esc_html_e( 'Select all', 'woocommerce' ); ?></a>
				</span>
				<span class="add-to-menu">
					<button type="submit" class="button-secondary submit-add-to-menu right" value="<?php esc_attr_e( 'Add to menu', 'woocommerce' ); ?>" name="add-post-type-menu-item" id="submit-posttype-woocommerce-endpoints"><?php esc_html_e( 'Add to menu', 'woocommerce' ); ?></button>
					<span class="spinner"></span>
				</span>
			</p>
		</div>
		<?php
	}

	/**
	 * Add the "Visit Store" link in admin bar main menu.
	 *
	 * @since 2.4.0
	 * @param WP_Admin_Bar $wp_admin_bar Admin bar instance.
	 */
	public function admin_bar_menus( $wp_admin_bar ) {
		if ( ! is_admin() || ! is_admin_bar_showing() ) {
			return;
		}

		// Show only when the user is a member of this site, or they're a super admin.
		if ( ! is_user_member_of_blog() && ! is_super_admin() ) {
			return;
		}

		// Don't display when shop page is the same of the page on front.
		if ( intval( get_option( 'page_on_front' ) ) === wc_get_page_id( 'shop' ) ) {
			return;
		}

		// Add an option to visit the store.
		$wp_admin_bar->add_node(
			array(
				'parent' => 'site-name',
				'id'     => 'view-store',
				'title'  => __( 'Visit Store', 'woocommerce' ),
				'href'   => wc_get_page_permalink( 'shop' ),
			)
		);
	}

	/**
	 * Highlight the My Subscriptions menu item when on that page
	 *
	 * @param string $submenu_file The submenu file.
	 * @param string $parent_file  currently opened page.
	 *
	 * @return string
	 */
	public function update_menu_highlight( $submenu_file, $parent_file ) {
		if ( 'woocommerce' === $parent_file && isset( $_GET['section'] ) && 'helper' === $_GET['section'] ) {
			$submenu_file = 'wc-addons&section=helper';
		}
		return $submenu_file;
	}

	/**
	 * Update the My Subscriptions document title when on that page.
	 * We want to maintain existing page URL but add it as a separate page,
	 * which requires updating it manually.
	 *
	 * @param  string $admin_title existing page title.
	 * @return string
	 */
	public function update_my_subscriptions_title( $admin_title ) {
		if (
			isset( $_GET['page'] ) && 'wc-addons' === $_GET['page'] &&
			isset( $_GET['section'] ) && 'helper' === $_GET['section']
		) {
			$admin_title = 'My WooCommerce.com Subscriptions';
		}
		return $admin_title;
	}
}

return new WC_Admin_Menus();
admin/notes/class-wc-notes-refund-returns.php000064400000003642151336044770015337 0ustar00<?php
/**
 * Refund and Returns Policy Page Note Provider.
 *
 * Adds notes to the merchant's inbox concerning the created page.
 *
 * @package WooCommerce
 */

defined( 'ABSPATH' ) || exit;

use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Admin\Notes\Note;

/**
 * WC_Notes_Refund_Returns.
 */
class WC_Notes_Refund_Returns {
	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-refund-returns-page';

	/**
	 * Maybe add a note to the inbox.
	 *
	 * @param int $page_id The ID of the page.
	 */
	public static function possibly_add_note( $page_id ) {
		$data_store = \WC_Data_Store::load( 'admin-note' );

		// Do we already have this note?
		$note_id = $data_store->get_notes_with_name( self::NOTE_NAME );

		if ( ! empty( $note_id ) ) {
			$note = new Note( $note_id );

			if ( false !== $note || $note::E_WC_ADMIN_NOTE_ACTIONED === $note->get_status() ) {
				// note actioned -> don't show it.
				return;
			}
		}

		// Add note.
		$note = self::get_note( $page_id );
		$note->save();
		delete_option( 'woocommerce_refund_returns_page_created' );
	}

	/**
	 * Get the note.
	 *
	 * @param int $page_id The ID of the page.
	 * @return object $note The note object.
	 */
	public static function get_note( $page_id ) {
		$note = new Note();
		$note->set_title( __( 'Setup a Refund and Returns Policy page to boost your store\'s credibility.', 'woocommerce' ) );
		$note->set_content( __( 'We have created a sample draft Refund and Returns Policy page for you. Please have a look and update it to fit your store.', 'woocommerce' ) );
		$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
		$note->set_name( self::NOTE_NAME );
		$note->set_content_data( (object) array() );
		$note->set_source( 'woocommerce-core' );
		$note->add_action(
			'notify-refund-returns-page',
			__( 'Edit page', 'woocommerce' ),
			admin_url( sprintf( 'post.php?post=%d&action=edit', (int) $page_id ) )
		);

		return $note;
	}
}
admin/notes/class-wc-notes-run-db-update.php000064400000024642151336044770015026 0ustar00<?php
/**
 * WooCommerce: Db update note.
 *
 * Adds a note to complete the WooCommerce db update after the upgrade in the WC Admin context.
 *
 * @package WooCommerce
 */

defined( 'ABSPATH' ) || exit;

use \Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Admin\Notes\Note;

/**
 * WC_Notes_Run_Db_Update.
 */
class WC_Notes_Run_Db_Update {
	const NOTE_NAME = 'wc-update-db-reminder';

	/**
	 * Attach hooks.
	 */
	public function __construct() {
		// If the old notice gets dismissed, also hide this new one.
		add_action( 'woocommerce_hide_update_notice', array( __CLASS__, 'set_notice_actioned' ) );

		// Not using Jetpack\Constants here as it can run before 'plugin_loaded' is done.
		if ( defined( 'DOING_AJAX' ) && DOING_AJAX
			|| defined( 'DOING_CRON' ) && DOING_CRON
			|| ! is_admin() ) {
			return;
		}

		add_action( 'current_screen', array( __CLASS__, 'show_reminder' ) );
	}

	/**
	 * Get current notice id from the database.
	 *
	 * Retrieves the first notice of this type.
	 *
	 * @return int|void Note id or null in case no note was found.
	 */
	private static function get_current_notice() {
		try {
			$data_store = \WC_Data_Store::load( 'admin-note' );
		} catch ( Exception $e ) {
			return;
		}
		$note_ids = $data_store->get_notes_with_name( self::NOTE_NAME );

		if ( empty( $note_ids ) ) {
			return;
		}

		if ( count( $note_ids ) > 1 ) {
			// Remove weird duplicates. Leave the first one.
			$current_notice = array_shift( $note_ids );
			foreach ( $note_ids as $note_id ) {
				$note = new Note( $note_id );
				$data_store->delete( $note );
			}
			return $current_notice;
		}

		return current( $note_ids );
	}

	/**
	 * Set this notice to an actioned one, so that it's no longer displayed.
	 */
	public static function set_notice_actioned() {
		$note_id = self::get_current_notice();

		if ( ! $note_id ) {
			return;
		}

		$note = new Note( $note_id );
		$note->set_status( Note::E_WC_ADMIN_NOTE_ACTIONED );
		$note->save();
	}

	/**
	 * Check whether the note is up to date for a fresh display.
	 *
	 * The check tests if
	 *  - actions are set up for the first 'Update database' notice, and
	 *  - URL for note's action is equal to the given URL (to check for potential nonce update).
	 *
	 * @param Note               $note            Note to check.
	 * @param string             $update_url      URL to check the note against.
	 * @param array<int, string> $current_actions List of actions to check for.
	 * @return bool
	 */
	private static function note_up_to_date( $note, $update_url, $current_actions ) {
		$actions = $note->get_actions();
		return count( $current_actions ) === count( array_intersect( wp_list_pluck( $actions, 'name' ), $current_actions ) )
			&& in_array( $update_url, wp_list_pluck( $actions, 'query' ), true );
	}

	/**
	 * Create and set up the first (out of 3) 'Database update needed' notice and store it in the database.
	 *
	 * If a $note_id is given, the method updates the note instead of creating a new one.
	 *
	 * @param integer $note_id Note db record to update.
	 * @return int Created/Updated note id
	 */
	private static function update_needed_notice( $note_id = null ) {
		$update_url =
			add_query_arg(
				array(
					'do_update_woocommerce' => 'true',
				),
				wc_get_current_admin_url() ? wc_get_current_admin_url() : admin_url( 'admin.php?page=wc-settings' )
			);

		$note_actions = array(
			array(
				'name'         => 'update-db_run',
				'label'        => __( 'Update WooCommerce Database', 'woocommerce' ),
				'url'          => $update_url,
				'status'       => 'unactioned',
				'primary'      => true,
				'nonce_action' => 'wc_db_update',
				'nonce_name'   => 'wc_db_update_nonce',
			),
			array(
				'name'    => 'update-db_learn-more',
				'label'   => __( 'Learn more about updates', 'woocommerce' ),
				'url'     => 'https://docs.woocommerce.com/document/how-to-update-woocommerce/',
				'status'  => 'unactioned',
				'primary' => false,
			),
		);

		if ( $note_id ) {
			$note = new Note( $note_id );
		} else {
			$note = new Note();
		}

		// Check if the note needs to be updated (e.g. expired nonce or different note type stored in the previous run).
		if ( self::note_up_to_date( $note, $update_url, wp_list_pluck( $note_actions, 'name' ) ) ) {
			return $note_id;
		}

		$note->set_title( __( 'WooCommerce database update required', 'woocommerce' ) );
		$note->set_content(
			__( 'WooCommerce has been updated! To keep things running smoothly, we have to update your database to the newest version.', 'woocommerce' )
			/* translators: %1$s: opening <a> tag %2$s: closing </a> tag*/
			. sprintf( ' ' . esc_html__( 'The database update process runs in the background and may take a little while, so please be patient. Advanced users can alternatively update via %1$sWP CLI%2$s.', 'woocommerce' ), '<a href="https://github.com/woocommerce/woocommerce/wiki/Upgrading-the-database-using-WP-CLI">', '</a>' )
		);
		$note->set_type( Note::E_WC_ADMIN_NOTE_UPDATE );
		$note->set_name( self::NOTE_NAME );
		$note->set_content_data( (object) array() );
		$note->set_source( 'woocommerce-core' );
		// In case db version is out of sync with WC version or during the next update, the notice needs to show up again,
		// so set it to unactioned.
		$note->set_status( Note::E_WC_ADMIN_NOTE_UNACTIONED );

		// Set new actions.
		$note->clear_actions();
		foreach ( $note_actions as $note_action ) {
			$note->add_action( ...array_values( $note_action ) );

			if ( isset( $note_action['nonce_action'] ) ) {
				$note->add_nonce_to_action( $note_action['name'], $note_action['nonce_action'], $note_action['nonce_name'] );
			}
		}

		return $note->save();
	}

	/**
	 * Update the existing note with $note_id with information about the db upgrade being in progress.
	 *
	 * This is the second out of 3 notices displayed to the user.
	 *
	 * @param int $note_id Note id to update.
	 */
	private static function update_in_progress_notice( $note_id ) {
		// Same actions as in includes/admin/views/html-notice-updating.php. This just redirects, performs no action, so without nonce.
		$pending_actions_url = admin_url( 'admin.php?page=wc-status&tab=action-scheduler&s=woocommerce_run_update&status=pending' );
		$cron_disabled       = Constants::is_true( 'DISABLE_WP_CRON' );
		$cron_cta            = $cron_disabled ? __( 'You can manually run queued updates here.', 'woocommerce' ) : __( 'View progress →', 'woocommerce' );

		$note = new Note( $note_id );
		$note->set_title( __( 'WooCommerce database update in progress', 'woocommerce' ) );
		$note->set_content( __( 'WooCommerce is updating the database in the background. The database update process may take a little while, so please be patient.', 'woocommerce' ) );

		$note->clear_actions();
		$note->add_action(
			'update-db_see-progress',
			$cron_cta,
			$pending_actions_url,
			'unactioned',
			false
		);

		$note->save();
	}

	/**
	 * Update the existing note with $note_id with information that db upgrade is done.
	 *
	 * This is the last notice (3 out of 3 notices) displayed to the user.
	 *
	 * @param int $note_id Note id to update.
	 */
	private static function update_done_notice( $note_id ) {
		$hide_notices_url = html_entity_decode( // to convert &amp;s to normal &, otherwise produces invalid link.
			add_query_arg(
				array(
					'wc-hide-notice' => 'update',
				),
				wc_get_current_admin_url() ? remove_query_arg( 'do_update_woocommerce', wc_get_current_admin_url() ) : admin_url( 'admin.php?page=wc-settings' )
			)
		);

		$note_actions = array(
			array(
				'name'         => 'update-db_done',
				'label'        => __( 'Thanks!', 'woocommerce' ),
				'url'          => $hide_notices_url,
				'status'       => 'actioned',
				'primary'      => true,
				'nonce_action' => 'woocommerce_hide_notices_nonce',
				'nonce_name'   => '_wc_notice_nonce',
			),
		);

		$note = new Note( $note_id );

		// Check if the note needs to be updated (e.g. expired nonce or different note type stored in the previous run).
		if ( self::note_up_to_date( $note, $hide_notices_url, wp_list_pluck( $note_actions, 'name' ) ) ) {
			return $note_id;
		}

		$note->set_title( __( 'WooCommerce database update done', 'woocommerce' ) );
		$note->set_content( __( 'WooCommerce database update complete. Thank you for updating to the latest version!', 'woocommerce' ) );

		$note->clear_actions();
		foreach ( $note_actions as $note_action ) {
			$note->add_action( ...array_values( $note_action ) );

			if ( isset( $note_action['nonce_action'] ) ) {
				$note->add_nonce_to_action( $note_action['name'], $note_action['nonce_action'], $note_action['nonce_name'] );
			}
		}

		$note->save();
	}

	/**
	 * Prepare the correct content of the db update note to be displayed by WC Admin.
	 *
	 * This one gets called on each page load, so try to bail quickly.
	 *
	 * If the db needs an update, the notice should be always shown.
	 * If the db does not need an update, but the notice has *not* been actioned (i.e. after the db update, when
	 * store owner hasn't acknowledged the successful db update), still show the Thanks notice.
	 * If the db does not need an update, and the notice has been actioned, then notice should *not* be shown.
	 * The notice should also be hidden if the db does not need an update and the notice does not exist.
	 */
	public static function show_reminder() {
		$needs_db_update = \WC_Install::needs_db_update();

		$note_id = self::get_current_notice();
		if ( ! $needs_db_update ) {
			// Db update not needed && note does not exist -> don't show it.
			if ( ! $note_id ) {
				return;
			}

			$note = new Note( $note_id );
			if ( $note::E_WC_ADMIN_NOTE_ACTIONED === $note->get_status() ) {
				// Db update not needed && note actioned -> don't show it.
				return;
			} else {
				// Db update not needed && notice is unactioned -> Thank you note.
				self::update_done_notice( $note_id );
				return;
			}
		} else {
			// Db needs update &&.
			if ( ! $note_id ) {
				// Db needs update && no notice exists -> create one that shows Nudge to update.
				$note_id = self::update_needed_notice();
			}

			$next_scheduled_date = WC()->queue()->get_next( 'woocommerce_run_update_callback', null, 'woocommerce-db-updates' );

			if ( $next_scheduled_date || ! empty( $_GET['do_update_woocommerce'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
				// Db needs update && db update is scheduled -> update note to In progress.
				self::update_in_progress_notice( $note_id );
			} else {
				// Db needs update && db update is not scheduled -> Nudge to run the db update.
				self::update_needed_notice( $note_id );
			}
		}
	}

}
admin/class-wc-admin-duplicate-product.php000064400000022464151336044770014617 0ustar00<?php
/**
 * Duplicate product functionality
 *
 * @package     WooCommerce\Admin
 * @version     3.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( class_exists( 'WC_Admin_Duplicate_Product', false ) ) {
	return new WC_Admin_Duplicate_Product();
}

/**
 * WC_Admin_Duplicate_Product Class.
 */
class WC_Admin_Duplicate_Product {

	/**
	 * Constructor.
	 */
	public function __construct() {
		add_action( 'admin_action_duplicate_product', array( $this, 'duplicate_product_action' ) );
		add_filter( 'post_row_actions', array( $this, 'dupe_link' ), 10, 2 );
		add_action( 'post_submitbox_start', array( $this, 'dupe_button' ) );
	}

	/**
	 * Show the "Duplicate" link in admin products list.
	 *
	 * @param array   $actions Array of actions.
	 * @param WP_Post $post Post object.
	 * @return array
	 */
	public function dupe_link( $actions, $post ) {
		global $the_product;

		if ( ! current_user_can( apply_filters( 'woocommerce_duplicate_product_capability', 'manage_woocommerce' ) ) ) {
			return $actions;
		}

		if ( 'product' !== $post->post_type ) {
			return $actions;
		}

		// Add Class to Delete Permanently link in row actions.
		if ( empty( $the_product ) || $the_product->get_id() !== $post->ID ) {
			$the_product = wc_get_product( $post );
		}

		if ( 'publish' === $post->post_status && $the_product && 0 < $the_product->get_total_sales() ) {
			$actions['trash'] = sprintf(
				'<a href="%s" class="submitdelete trash-product" aria-label="%s">%s</a>',
				get_delete_post_link( $the_product->get_id(), '', false ),
				/* translators: %s: post title */
				esc_attr( sprintf( __( 'Move &#8220;%s&#8221; to the Trash', 'woocommerce' ), $the_product->get_name() ) ),
				esc_html__( 'Trash', 'woocommerce' )
			);
		}

		$actions['duplicate'] = '<a href="' . wp_nonce_url( admin_url( 'edit.php?post_type=product&action=duplicate_product&amp;post=' . $post->ID ), 'woocommerce-duplicate-product_' . $post->ID ) . '" aria-label="' . esc_attr__( 'Make a duplicate from this product', 'woocommerce' )
			. '" rel="permalink">' . esc_html__( 'Duplicate', 'woocommerce' ) . '</a>';

		return $actions;
	}

	/**
	 * Show the dupe product link in admin.
	 */
	public function dupe_button() {
		global $post;

		if ( ! current_user_can( apply_filters( 'woocommerce_duplicate_product_capability', 'manage_woocommerce' ) ) ) {
			return;
		}

		if ( ! is_object( $post ) ) {
			return;
		}

		if ( 'product' !== $post->post_type ) {
			return;
		}

		$notify_url = wp_nonce_url( admin_url( 'edit.php?post_type=product&action=duplicate_product&post=' . absint( $post->ID ) ), 'woocommerce-duplicate-product_' . $post->ID );
		?>
		<div id="duplicate-action"><a class="submitduplicate duplication" href="<?php echo esc_url( $notify_url ); ?>"><?php esc_html_e( 'Copy to a new draft', 'woocommerce' ); ?></a></div>
		<?php
	}

	/**
	 * Duplicate a product action.
	 */
	public function duplicate_product_action() {
		if ( empty( $_REQUEST['post'] ) ) {
			wp_die( esc_html__( 'No product to duplicate has been supplied!', 'woocommerce' ) );
		}

		$product_id = isset( $_REQUEST['post'] ) ? absint( $_REQUEST['post'] ) : '';

		check_admin_referer( 'woocommerce-duplicate-product_' . $product_id );

		$product = wc_get_product( $product_id );

		if ( false === $product ) {
			/* translators: %s: product id */
			wp_die( sprintf( esc_html__( 'Product creation failed, could not find original product: %s', 'woocommerce' ), esc_html( $product_id ) ) );
		}

		$duplicate = $this->product_duplicate( $product );

		// Hook rename to match other woocommerce_product_* hooks, and to move away from depending on a response from the wp_posts table.
		do_action( 'woocommerce_product_duplicate', $duplicate, $product );
		wc_do_deprecated_action( 'woocommerce_duplicate_product', array( $duplicate->get_id(), $this->get_product_to_duplicate( $product_id ) ), '3.0', 'Use woocommerce_product_duplicate action instead.' );

		// Redirect to the edit screen for the new draft page.
		wp_redirect( admin_url( 'post.php?action=edit&post=' . $duplicate->get_id() ) );
		exit;
	}

	/**
	 * Function to create the duplicate of the product.
	 *
	 * @param WC_Product $product The product to duplicate.
	 * @return WC_Product The duplicate.
	 */
	public function product_duplicate( $product ) {
		/**
		 * Filter to allow us to exclude meta keys from product duplication..
		 *
		 * @param array $exclude_meta The keys to exclude from the duplicate.
		 * @param array $existing_meta_keys The meta keys that the product already has.
		 * @since 2.6
		 */
		$meta_to_exclude = array_filter(
			apply_filters(
				'woocommerce_duplicate_product_exclude_meta',
				array(),
				array_map(
					function ( $datum ) {
						return $datum->key;
					},
					$product->get_meta_data()
				)
			)
		);

		$duplicate = clone $product;
		$duplicate->set_id( 0 );
		/* translators: %s contains the name of the original product. */
		$duplicate->set_name( sprintf( esc_html__( '%s (Copy)', 'woocommerce' ), $duplicate->get_name() ) );
		$duplicate->set_total_sales( 0 );
		if ( '' !== $product->get_sku( 'edit' ) ) {
			$duplicate->set_sku( wc_product_generate_unique_sku( 0, $product->get_sku( 'edit' ) ) );
		}
		$duplicate->set_status( 'draft' );
		$duplicate->set_date_created( null );
		$duplicate->set_slug( '' );
		$duplicate->set_rating_counts( 0 );
		$duplicate->set_average_rating( 0 );
		$duplicate->set_review_count( 0 );

		foreach ( $meta_to_exclude as $meta_key ) {
			$duplicate->delete_meta_data( $meta_key );
		}

		/**
		 * This action can be used to modify the object further before it is created - it will be passed by reference.
		 *
		 * @since 3.0
		 */
		do_action( 'woocommerce_product_duplicate_before_save', $duplicate, $product );

		// Save parent product.
		$duplicate->save();

		// Duplicate children of a variable product.
		if ( ! apply_filters( 'woocommerce_duplicate_product_exclude_children', false, $product ) && $product->is_type( 'variable' ) ) {
			foreach ( $product->get_children() as $child_id ) {
				$child           = wc_get_product( $child_id );
				$child_duplicate = clone $child;
				$child_duplicate->set_parent_id( $duplicate->get_id() );
				$child_duplicate->set_id( 0 );
				$child_duplicate->set_date_created( null );

				// If we wait and let the insertion generate the slug, we will see extreme performance degradation
				// in the case where a product is used as a template. Every time the template is duplicated, each
				// variation will query every consecutive slug until it finds an empty one. To avoid this, we can
				// optimize the generation ourselves, avoiding the issue altogether.
				$this->generate_unique_slug( $child_duplicate );

				if ( '' !== $child->get_sku( 'edit' ) ) {
					$child_duplicate->set_sku( wc_product_generate_unique_sku( 0, $child->get_sku( 'edit' ) ) );
				}

				foreach ( $meta_to_exclude as $meta_key ) {
					$child_duplicate->delete_meta_data( $meta_key );
				}

				/**
				 * This action can be used to modify the object further before it is created - it will be passed by reference.
				 *
				 * @since 3.0
				 */
				do_action( 'woocommerce_product_duplicate_before_save', $child_duplicate, $child );

				$child_duplicate->save();
			}

			// Get new object to reflect new children.
			$duplicate = wc_get_product( $duplicate->get_id() );
		}

		return $duplicate;
	}

	/**
	 * Get a product from the database to duplicate.
	 *
	 * @deprecated 3.0.0
	 * @param mixed $id The ID of the product to duplicate.
	 * @return object|bool
	 * @see duplicate_product
	 */
	private function get_product_to_duplicate( $id ) {
		global $wpdb;

		$id = absint( $id );

		if ( ! $id ) {
			return false;
		}

		$post = $wpdb->get_row( $wpdb->prepare( "SELECT {$wpdb->posts}.* FROM {$wpdb->posts} WHERE ID = %d", $id ) );

		if ( isset( $post->post_type ) && 'revision' === $post->post_type ) {
			$id   = $post->post_parent;
			$post = $wpdb->get_row( $wpdb->prepare( "SELECT {$wpdb->posts}.* FROM {$wpdb->posts} WHERE ID = %d", $id ) );
		}

		return $post;
	}

	/**
	 * Generates a unique slug for a given product. We do this so that we can override the
	 * behavior of wp_unique_post_slug(). The normal slug generation will run single
	 * select queries on every non-unique slug, resulting in very bad performance.
	 *
	 * @param WC_Product $product The product to generate a slug for.
	 * @since 3.9.0
	 */
	private function generate_unique_slug( $product ) {
		global $wpdb;

		// We want to remove the suffix from the slug so that we can find the maximum suffix using this root slug.
		// This will allow us to find the next-highest suffix that is unique. While this does not support gap
		// filling, this shouldn't matter for our use-case.
		$root_slug = preg_replace( '/-[0-9]+$/', '', $product->get_slug() );

		$results = $wpdb->get_results(
			$wpdb->prepare( "SELECT post_name FROM $wpdb->posts WHERE post_name LIKE %s AND post_type IN ( 'product', 'product_variation' )", $root_slug . '%' )
		);

		// The slug is already unique!
		if ( empty( $results ) ) {
			return;
		}

		// Find the maximum suffix so we can ensure uniqueness.
		$max_suffix = 1;
		foreach ( $results as $result ) {
			// Pull a numerical suffix off the slug after the last hyphen.
			$suffix = intval( substr( $result->post_name, strrpos( $result->post_name, '-' ) + 1 ) );
			if ( $suffix > $max_suffix ) {
				$max_suffix = $suffix;
			}
		}

		$product->set_slug( $root_slug . '-' . ( $max_suffix + 1 ) );
	}
}

return new WC_Admin_Duplicate_Product();
admin/class-wc-admin-pointers.php000064400000022473151336044770013032 0ustar00<?php
/**
 * Adds and controls pointers for contextual help/tutorials
 *
 * @package WooCommerce\Admin\Pointers
 * @version 2.4.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC_Admin_Pointers Class.
 */
class WC_Admin_Pointers {

	/**
	 * Constructor.
	 */
	public function __construct() {
		add_action( 'admin_enqueue_scripts', array( $this, 'setup_pointers_for_screen' ) );
	}

	/**
	 * Setup pointers for screen.
	 */
	public function setup_pointers_for_screen() {
		$screen = get_current_screen();

		if ( ! $screen ) {
			return;
		}

		switch ( $screen->id ) {
			case 'product':
				$this->create_product_tutorial();
				break;
		}
	}

	/**
	 * Pointers for creating a product.
	 */
	public function create_product_tutorial() {
		if ( ! isset( $_GET['tutorial'] ) || ! current_user_can( 'manage_options' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			return;
		}

		// These pointers will chain - they will not be shown at once.
		$pointers = array(
			'pointers' => array(
				'title'          => array(
					'target'       => '#title',
					'next'         => 'content',
					'next_trigger' => array(
						'target' => '#title',
						'event'  => 'input',
					),
					'options'      => array(
						'content'  => '<h3>' . esc_html__( 'Product name', 'woocommerce' ) . '</h3>' .
										'<p>' . esc_html__( 'Give your new product a name here. This is a required field and will be what your customers will see in your store.', 'woocommerce' ) . '</p>',
						'position' => array(
							'edge'  => 'top',
							'align' => 'left',
						),
					),
				),
				'content'        => array(
					'target'       => '#wp-content-editor-container',
					'next'         => 'product-type',
					'next_trigger' => array(),
					'options'      => array(
						'content'  => '<h3>' . esc_html__( 'Product description', 'woocommerce' ) . '</h3>' .
										'<p>' . esc_html__( 'This is your products main body of content. Here you should describe your product in detail.', 'woocommerce' ) . '</p>',
						'position' => array(
							'edge'  => 'bottom',
							'align' => 'middle',
						),
					),
				),
				'product-type'   => array(
					'target'       => '#product-type',
					'next'         => 'virtual',
					'next_trigger' => array(
						'target' => '#product-type',
						'event'  => 'change blur click',
					),
					'options'      => array(
						'content'  => '<h3>' . esc_html__( 'Choose product type', 'woocommerce' ) . '</h3>' .
										'<p>' . esc_html__( 'Choose a type for this product. Simple is suitable for most physical goods and services (we recommend setting up a simple product for now).', 'woocommerce' ) . '</p>' .
										'<p>' . esc_html__( 'Variable is for more complex products such as t-shirts with multiple sizes.', 'woocommerce' ) . '</p>' .
										'<p>' . esc_html__( 'Grouped products are for grouping several simple products into one.', 'woocommerce' ) . '</p>' .
										'<p>' . esc_html__( 'Finally, external products are for linking off-site.', 'woocommerce' ) . '</p>',
						'position' => array(
							'edge'  => 'bottom',
							'align' => 'middle',
						),
					),
				),
				'virtual'        => array(
					'target'       => '#_virtual',
					'next'         => 'downloadable',
					'next_trigger' => array(
						'target' => '#_virtual',
						'event'  => 'change',
					),
					'options'      => array(
						'content'  => '<h3>' . esc_html__( 'Virtual products', 'woocommerce' ) . '</h3>' .
										'<p>' . esc_html__( 'Check the "Virtual" box if this is a non-physical item, for example a service, which does not need shipping.', 'woocommerce' ) . '</p>',
						'position' => array(
							'edge'  => 'bottom',
							'align' => 'middle',
						),
					),
				),
				'downloadable'   => array(
					'target'       => '#_downloadable',
					'next'         => 'regular_price',
					'next_trigger' => array(
						'target' => '#_downloadable',
						'event'  => 'change',
					),
					'options'      => array(
						'content'  => '<h3>' . esc_html__( 'Downloadable products', 'woocommerce' ) . '</h3>' .
										'<p>' . esc_html__( 'If purchasing this product gives a customer access to a downloadable file, e.g. software, check this box.', 'woocommerce' ) . '</p>',
						'position' => array(
							'edge'  => 'bottom',
							'align' => 'middle',
						),
					),
				),
				'regular_price'  => array(
					'target'       => '#_regular_price',
					'next'         => 'postexcerpt',
					'next_trigger' => array(
						'target' => '#_regular_price',
						'event'  => 'input',
					),
					'options'      => array(
						'content'  => '<h3>' . esc_html__( 'Prices', 'woocommerce' ) . '</h3>' .
										'<p>' . esc_html__( 'Next you need to give your product a price.', 'woocommerce' ) . '</p>',
						'position' => array(
							'edge'  => 'bottom',
							'align' => 'middle',
						),
					),
				),
				'postexcerpt'    => array(
					'target'       => '#postexcerpt',
					'next'         => 'postimagediv',
					'next_trigger' => array(
						'target' => '#postexcerpt',
						'event'  => 'input',
					),
					'options'      => array(
						'content'  => '<h3>' . esc_html__( 'Product short description', 'woocommerce' ) . '</h3>' .
										'<p>' . esc_html__( 'Add a quick summary for your product here. This will appear on the product page under the product name.', 'woocommerce' ) . '</p>',
						'position' => array(
							'edge'  => 'bottom',
							'align' => 'middle',
						),
					),
				),
				'postimagediv'   => array(
					'target'  => '#postimagediv',
					'next'    => 'product_tag',
					'options' => array(
						'content'  => '<h3>' . esc_html__( 'Product images', 'woocommerce' ) . '</h3>' .
										'<p>' . esc_html__( "Upload or assign an image to your product here. This image will be shown in your store's catalog.", 'woocommerce' ) . '</p>',
						'position' => array(
							'edge'  => 'right',
							'align' => 'middle',
						),
					),
				),
				'product_tag'    => array(
					'target'  => '#tagsdiv-product_tag',
					'next'    => 'product_catdiv',
					'options' => array(
						'content'  => '<h3>' . esc_html__( 'Product tags', 'woocommerce' ) . '</h3>' .
										'<p>' . esc_html__( 'You can optionally "tag" your products here. Tags are a method of labeling your products to make them easier for customers to find.', 'woocommerce' ) . '</p>',
						'position' => array(
							'edge'  => 'right',
							'align' => 'middle',
						),
					),
				),
				'product_catdiv' => array(
					'target'  => '#product_catdiv',
					'next'    => 'submitdiv',
					'options' => array(
						'content'  => '<h3>' . esc_html__( 'Product categories', 'woocommerce' ) . '</h3>' .
										'<p>' . esc_html__( 'Optionally assign categories to your products to make them easier to browse through and find in your store.', 'woocommerce' ) . '</p>',
						'position' => array(
							'edge'  => 'right',
							'align' => 'middle',
						),
					),
				),
				'submitdiv'      => array(
					'target'  => '#submitdiv',
					'next'    => '',
					'options' => array(
						'content'  => '<h3>' . esc_html__( 'Publish your product!', 'woocommerce' ) . '</h3>' .
										'<p>' . esc_html__( 'When you are finished editing your product, hit the "Publish" button to publish your product to your store.', 'woocommerce' ) . '</p>',
						'position' => array(
							'edge'  => 'right',
							'align' => 'middle',
						),
					),
				),
			),
		);

		$this->enqueue_pointers( $pointers );
	}

	/**
	 * Enqueue pointers and add script to page.
	 *
	 * @param array $pointers Pointers data.
	 */
	public function enqueue_pointers( $pointers ) {
		$pointers = rawurlencode( wp_json_encode( $pointers ) );
		wp_enqueue_style( 'wp-pointer' );
		wp_enqueue_script( 'wp-pointer' );
		wc_enqueue_js(
			"jQuery( function( $ ) {
				var wc_pointers = JSON.parse( decodeURIComponent( '{$pointers}' ) );

				setTimeout( init_wc_pointers, 800 );

				function init_wc_pointers() {
					$.each( wc_pointers.pointers, function( i ) {
						show_wc_pointer( i );
						return false;
					});
				}

				function show_wc_pointer( id ) {
					var pointer = wc_pointers.pointers[ id ];
					var options = $.extend( pointer.options, {
						pointerClass: 'wp-pointer wc-pointer',
						close: function() {
							if ( pointer.next ) {
								show_wc_pointer( pointer.next );
							}
						},
						buttons: function( event, t ) {
							var close   = '" . esc_js( __( 'Dismiss', 'woocommerce' ) ) . "',
								next    = '" . esc_js( __( 'Next', 'woocommerce' ) ) . "',
								button  = $( '<a class=\"close\" href=\"#\">' + close + '</a>' ),
								button2 = $( '<a class=\"button button-primary\" href=\"#\">' + next + '</a>' ),
								wrapper = $( '<div class=\"wc-pointer-buttons\" />' );

							button.on( 'click.pointer', function(e) {
								e.preventDefault();
								t.element.pointer('destroy');
							});

							button2.on( 'click.pointer', function(e) {
								e.preventDefault();
								t.element.pointer('close');
							});

							wrapper.append( button );
							wrapper.append( button2 );

							return wrapper;
						},
					} );
					var this_pointer = $( pointer.target ).pointer( options );
					this_pointer.pointer( 'open' );

					if ( pointer.next_trigger ) {
						$( pointer.next_trigger.target ).on( pointer.next_trigger.event, function() {
							setTimeout( function() { this_pointer.pointer( 'close' ); }, 400 );
						});
					}
				}
			});"
		);
	}
}

new WC_Admin_Pointers();
admin/class-wc-admin-profile.php000064400000022125151336044770012621 0ustar00<?php
/**
 * Add extra profile fields for users in admin
 *
 * @author   WooThemes
 * @category Admin
 * @package  WooCommerce\Admin
 * @version  2.4.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

if ( ! class_exists( 'WC_Admin_Profile', false ) ) :

	/**
	 * WC_Admin_Profile Class.
	 */
	class WC_Admin_Profile {

		/**
		 * Hook in tabs.
		 */
		public function __construct() {
			add_action( 'show_user_profile', array( $this, 'add_customer_meta_fields' ) );
			add_action( 'edit_user_profile', array( $this, 'add_customer_meta_fields' ) );

			add_action( 'personal_options_update', array( $this, 'save_customer_meta_fields' ) );
			add_action( 'edit_user_profile_update', array( $this, 'save_customer_meta_fields' ) );
		}

		/**
		 * Get Address Fields for the edit user pages.
		 *
		 * @return array Fields to display which are filtered through woocommerce_customer_meta_fields before being returned
		 */
		public function get_customer_meta_fields() {
			$show_fields = apply_filters(
				'woocommerce_customer_meta_fields',
				array(
					'billing'  => array(
						'title'  => __( 'Customer billing address', 'woocommerce' ),
						'fields' => array(
							'billing_first_name' => array(
								'label'       => __( 'First name', 'woocommerce' ),
								'description' => '',
							),
							'billing_last_name'  => array(
								'label'       => __( 'Last name', 'woocommerce' ),
								'description' => '',
							),
							'billing_company'    => array(
								'label'       => __( 'Company', 'woocommerce' ),
								'description' => '',
							),
							'billing_address_1'  => array(
								'label'       => __( 'Address line 1', 'woocommerce' ),
								'description' => '',
							),
							'billing_address_2'  => array(
								'label'       => __( 'Address line 2', 'woocommerce' ),
								'description' => '',
							),
							'billing_city'       => array(
								'label'       => __( 'City', 'woocommerce' ),
								'description' => '',
							),
							'billing_postcode'   => array(
								'label'       => __( 'Postcode / ZIP', 'woocommerce' ),
								'description' => '',
							),
							'billing_country'    => array(
								'label'       => __( 'Country / Region', 'woocommerce' ),
								'description' => '',
								'class'       => 'js_field-country',
								'type'        => 'select',
								'options'     => array( '' => __( 'Select a country / region&hellip;', 'woocommerce' ) ) + WC()->countries->get_allowed_countries(),
							),
							'billing_state'      => array(
								'label'       => __( 'State / County', 'woocommerce' ),
								'description' => __( 'State / County or state code', 'woocommerce' ),
								'class'       => 'js_field-state',
							),
							'billing_phone'      => array(
								'label'       => __( 'Phone', 'woocommerce' ),
								'description' => '',
							),
							'billing_email'      => array(
								'label'       => __( 'Email address', 'woocommerce' ),
								'description' => '',
							),
						),
					),
					'shipping' => array(
						'title'  => __( 'Customer shipping address', 'woocommerce' ),
						'fields' => array(
							'copy_billing'        => array(
								'label'       => __( 'Copy from billing address', 'woocommerce' ),
								'description' => '',
								'class'       => 'js_copy-billing',
								'type'        => 'button',
								'text'        => __( 'Copy', 'woocommerce' ),
							),
							'shipping_first_name' => array(
								'label'       => __( 'First name', 'woocommerce' ),
								'description' => '',
							),
							'shipping_last_name'  => array(
								'label'       => __( 'Last name', 'woocommerce' ),
								'description' => '',
							),
							'shipping_company'    => array(
								'label'       => __( 'Company', 'woocommerce' ),
								'description' => '',
							),
							'shipping_address_1'  => array(
								'label'       => __( 'Address line 1', 'woocommerce' ),
								'description' => '',
							),
							'shipping_address_2'  => array(
								'label'       => __( 'Address line 2', 'woocommerce' ),
								'description' => '',
							),
							'shipping_city'       => array(
								'label'       => __( 'City', 'woocommerce' ),
								'description' => '',
							),
							'shipping_postcode'   => array(
								'label'       => __( 'Postcode / ZIP', 'woocommerce' ),
								'description' => '',
							),
							'shipping_country'    => array(
								'label'       => __( 'Country / Region', 'woocommerce' ),
								'description' => '',
								'class'       => 'js_field-country',
								'type'        => 'select',
								'options'     => array( '' => __( 'Select a country / region&hellip;', 'woocommerce' ) ) + WC()->countries->get_allowed_countries(),
							),
							'shipping_state'      => array(
								'label'       => __( 'State / County', 'woocommerce' ),
								'description' => __( 'State / County or state code', 'woocommerce' ),
								'class'       => 'js_field-state',
							),
							'shipping_phone'      => array(
								'label'       => __( 'Phone', 'woocommerce' ),
								'description' => '',
							),
						),
					),
				)
			);
			return $show_fields;
		}

		/**
		 * Show Address Fields on edit user pages.
		 *
		 * @param WP_User $user
		 */
		public function add_customer_meta_fields( $user ) {
			if ( ! apply_filters( 'woocommerce_current_user_can_edit_customer_meta_fields', current_user_can( 'manage_woocommerce' ), $user->ID ) ) {
				return;
			}

			$show_fields = $this->get_customer_meta_fields();

			foreach ( $show_fields as $fieldset_key => $fieldset ) :
				?>
				<h2><?php echo $fieldset['title']; ?></h2>
				<table class="form-table" id="<?php echo esc_attr( 'fieldset-' . $fieldset_key ); ?>">
					<?php foreach ( $fieldset['fields'] as $key => $field ) : ?>
						<tr>
							<th>
								<label for="<?php echo esc_attr( $key ); ?>"><?php echo esc_html( $field['label'] ); ?></label>
							</th>
							<td>
								<?php if ( ! empty( $field['type'] ) && 'select' === $field['type'] ) : ?>
									<select name="<?php echo esc_attr( $key ); ?>" id="<?php echo esc_attr( $key ); ?>" class="<?php echo esc_attr( $field['class'] ); ?>" style="width: 25em;">
										<?php
											$selected = esc_attr( get_user_meta( $user->ID, $key, true ) );
										foreach ( $field['options'] as $option_key => $option_value ) :
											?>
											<option value="<?php echo esc_attr( $option_key ); ?>" <?php selected( $selected, $option_key, true ); ?>><?php echo esc_html( $option_value ); ?></option>
										<?php endforeach; ?>
									</select>
								<?php elseif ( ! empty( $field['type'] ) && 'checkbox' === $field['type'] ) : ?>
									<input type="checkbox" name="<?php echo esc_attr( $key ); ?>" id="<?php echo esc_attr( $key ); ?>" value="1" class="<?php echo esc_attr( $field['class'] ); ?>" <?php checked( (int) get_user_meta( $user->ID, $key, true ), 1, true ); ?> />
								<?php elseif ( ! empty( $field['type'] ) && 'button' === $field['type'] ) : ?>
									<button type="button" id="<?php echo esc_attr( $key ); ?>" class="button <?php echo esc_attr( $field['class'] ); ?>"><?php echo esc_html( $field['text'] ); ?></button>
								<?php else : ?>
									<input type="text" name="<?php echo esc_attr( $key ); ?>" id="<?php echo esc_attr( $key ); ?>" value="<?php echo esc_attr( $this->get_user_meta( $user->ID, $key ) ); ?>" class="<?php echo ( ! empty( $field['class'] ) ? esc_attr( $field['class'] ) : 'regular-text' ); ?>" />
								<?php endif; ?>
								<p class="description"><?php echo wp_kses_post( $field['description'] ); ?></p>
							</td>
						</tr>
					<?php endforeach; ?>
				</table>
				<?php
			endforeach;
		}

		/**
		 * Save Address Fields on edit user pages.
		 *
		 * @param int $user_id User ID of the user being saved
		 */
		public function save_customer_meta_fields( $user_id ) {
			if ( ! apply_filters( 'woocommerce_current_user_can_edit_customer_meta_fields', current_user_can( 'manage_woocommerce' ), $user_id ) ) {
				return;
			}

			$save_fields = $this->get_customer_meta_fields();

			foreach ( $save_fields as $fieldset ) {

				foreach ( $fieldset['fields'] as $key => $field ) {

					if ( isset( $field['type'] ) && 'checkbox' === $field['type'] ) {
						update_user_meta( $user_id, $key, isset( $_POST[ $key ] ) );
					} elseif ( isset( $_POST[ $key ] ) ) {
						update_user_meta( $user_id, $key, wc_clean( $_POST[ $key ] ) );
					}
				}
			}
		}

		/**
		 * Get user meta for a given key, with fallbacks to core user info for pre-existing fields.
		 *
		 * @since 3.1.0
		 * @param int    $user_id User ID of the user being edited
		 * @param string $key     Key for user meta field
		 * @return string
		 */
		protected function get_user_meta( $user_id, $key ) {
			$value           = get_user_meta( $user_id, $key, true );
			$existing_fields = array( 'billing_first_name', 'billing_last_name' );
			if ( ! $value && in_array( $key, $existing_fields ) ) {
				$value = get_user_meta( $user_id, str_replace( 'billing_', '', $key ), true );
			} elseif ( ! $value && ( 'billing_email' === $key ) ) {
				$user  = get_userdata( $user_id );
				$value = $user->user_email;
			}

			return $value;
		}
	}

endif;

return new WC_Admin_Profile();
admin/helper/class-wc-helper-options.php000064400000002550151336044770014322 0ustar00<?php
/**
 * WooCommerce Admin Helper Options
 *
 * @package WooCommerce\Admin\Helper
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC_Helper_Options Class
 *
 * An interface to the woocommerce_helper_data entry in the wp_options table.
 */
class WC_Helper_Options {
	/**
	 * The option name used to store the helper data.
	 *
	 * @var string
	 */
	private static $option_name = 'woocommerce_helper_data';

	/**
	 * Update an option by key
	 *
	 * All helper options are grouped in a single options entry. This method
	 * is not thread-safe, use with caution.
	 *
	 * @param string $key The key to update.
	 * @param mixed  $value The new option value.
	 *
	 * @return bool True if the option has been updated.
	 */
	public static function update( $key, $value ) {
		$options         = get_option( self::$option_name, array() );
		$options[ $key ] = $value;
		return update_option( self::$option_name, $options, true );
	}

	/**
	 * Get an option by key
	 *
	 * @see self::update
	 *
	 * @param string $key The key to fetch.
	 * @param mixed  $default The default option to return if the key does not exist.
	 *
	 * @return mixed An option or the default.
	 */
	public static function get( $key, $default = false ) {
		$options = get_option( self::$option_name, array() );
		if ( array_key_exists( $key, $options ) ) {
			return $options[ $key ];
		}

		return $default;
	}
}
admin/helper/class-wc-helper-api.php000064400000011067151336044770013403 0ustar00<?php
/**
 * WooCommerce Admin Helper API
 *
 * @package WooCommerce\Admin\Helper
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC_Helper_API Class
 *
 * Provides a communication interface with the WooCommerce.com Helper API.
 */
class WC_Helper_API {
	/**
	 * Base path for API routes.
	 *
	 * @var $api_base
	 */
	public static $api_base;

	/**
	 * Load
	 *
	 * Allow devs to point the API base to a local API development or staging server.
	 * Note that sslverify will be turned off for the woocommerce.dev + WP_DEBUG combination.
	 * The URL can be changed on plugins_loaded before priority 10.
	 */
	public static function load() {
		self::$api_base = apply_filters( 'woocommerce_helper_api_base', 'https://woocommerce.com/wp-json/helper/1.0' );
	}

	/**
	 * Perform an HTTP request to the Helper API.
	 *
	 * @param string $endpoint The endpoint to request.
	 * @param array  $args Additional data for the request. Set authenticated to a truthy value to enable auth.
	 *
	 * @return array|WP_Error The response from wp_safe_remote_request()
	 */
	public static function request( $endpoint, $args = array() ) {
		$url = self::url( $endpoint );

		if ( ! empty( $args['authenticated'] ) ) {
			if ( ! self::_authenticate( $url, $args ) ) {
				return new WP_Error( 'authentication', 'Authentication failed.' );
			}
		}

		/**
		 * Allow developers to filter the request args passed to wp_safe_remote_request().
		 * Useful to remove sslverify when working on a local api dev environment.
		 */
		$args = apply_filters( 'woocommerce_helper_api_request_args', $args, $endpoint );

		// TODO: Check response signatures on certain endpoints.
		return wp_safe_remote_request( $url, $args );
	}

	/**
	 * Adds authentication headers to an HTTP request.
	 *
	 * @param string $url The request URI.
	 * @param array  $args By-ref, the args that will be passed to wp_remote_request().
	 * @return bool Were the headers added?
	 */
	private static function _authenticate( &$url, &$args ) {
		$auth = WC_Helper_Options::get( 'auth' );

		if ( empty( $auth['access_token'] ) || empty( $auth['access_token_secret'] ) ) {
			return false;
		}

		$request_uri  = parse_url( $url, PHP_URL_PATH );
		$query_string = parse_url( $url, PHP_URL_QUERY );

		if ( is_string( $query_string ) ) {
			$request_uri .= '?' . $query_string;
		}

		$data = array(
			'host'        => parse_url( $url, PHP_URL_HOST ),
			'request_uri' => $request_uri,
			'method'      => ! empty( $args['method'] ) ? $args['method'] : 'GET',
		);

		if ( ! empty( $args['body'] ) ) {
			$data['body'] = $args['body'];
		}

		$signature = hash_hmac( 'sha256', json_encode( $data ), $auth['access_token_secret'] );
		if ( empty( $args['headers'] ) ) {
			$args['headers'] = array();
		}

		$headers         = array(
			'Authorization'   => 'Bearer ' . $auth['access_token'],
			'X-Woo-Signature' => $signature,
		);
		$args['headers'] = wp_parse_args( $headers, $args['headers'] );

		$url = add_query_arg(
			array(
				'token'     => $auth['access_token'],
				'signature' => $signature,
			),
			$url
		);

		return true;
	}

	/**
	 * Wrapper for self::request().
	 *
	 * @param string $endpoint The helper API endpoint to request.
	 * @param array  $args Arguments passed to wp_remote_request().
	 *
	 * @return array The response object from wp_safe_remote_request().
	 */
	public static function get( $endpoint, $args = array() ) {
		$args['method'] = 'GET';
		return self::request( $endpoint, $args );
	}

	/**
	 * Wrapper for self::request().
	 *
	 * @param string $endpoint The helper API endpoint to request.
	 * @param array  $args Arguments passed to wp_remote_request().
	 *
	 * @return array The response object from wp_safe_remote_request().
	 */
	public static function post( $endpoint, $args = array() ) {
		$args['method'] = 'POST';
		return self::request( $endpoint, $args );
	}

	/**
	 * Wrapper for self::request().
	 *
	 * @param string $endpoint The helper API endpoint to request.
	 * @param array  $args Arguments passed to wp_remote_request().
	 *
	 * @return array The response object from wp_safe_remote_request().
	 */
	public static function put( $endpoint, $args = array() ) {
		$args['method'] = 'PUT';
		return self::request( $endpoint, $args );
	}

	/**
	 * Using the API base, form a request URL from a given endpoint.
	 *
	 * @param string $endpoint The endpoint to request.
	 *
	 * @return string The absolute endpoint URL.
	 */
	public static function url( $endpoint ) {
		$endpoint = ltrim( $endpoint, '/' );
		$endpoint = sprintf( '%s/%s', self::$api_base, $endpoint );
		$endpoint = esc_url_raw( $endpoint );
		return $endpoint;
	}
}

WC_Helper_API::load();
admin/helper/class-wc-helper-compat.php000064400000013277151336044770014122 0ustar00<?php
/**
 * WooCommerce Admin Helper Compat
 *
 * @package WooCommerce\Admin\Helper
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC_Helper_Compat Class
 *
 * Some level of compatibility with the legacy WooCommerce Helper plugin.
 */
class WC_Helper_Compat {

	/**
	 * Loads the class, runs on init.
	 */
	public static function load() {
		add_action( 'woocommerce_helper_loaded', array( __CLASS__, 'helper_loaded' ) );
	}

	/**
	 * Runs during woocommerce_helper_loaded
	 */
	public static function helper_loaded() {
		// Stop the nagging about WooThemes Updater
		remove_action( 'admin_notices', 'woothemes_updater_notice' );

		// A placeholder dashboard menu for legacy helper users.
		add_action( 'admin_menu', array( __CLASS__, 'admin_menu' ) );

		if ( empty( $GLOBALS['woothemes_updater'] ) ) {
			return;
		}

		self::remove_actions();
		self::migrate_connection();
		self::deactivate_plugin();
	}

	/**
	 * Remove legacy helper actions (notices, menus, etc.)
	 */
	public static function remove_actions() {
		// Remove WooThemes Updater notices
		remove_action( 'network_admin_notices', array( $GLOBALS['woothemes_updater']->admin, 'maybe_display_activation_notice' ) );
		remove_action( 'admin_notices', array( $GLOBALS['woothemes_updater']->admin, 'maybe_display_activation_notice' ) );
		remove_action( 'network_admin_menu', array( $GLOBALS['woothemes_updater']->admin, 'register_settings_screen' ) );
		remove_action( 'admin_menu', array( $GLOBALS['woothemes_updater']->admin, 'register_settings_screen' ) );
	}

	/**
	 * Attempt to migrate a legacy connection to a new one.
	 */
	public static function migrate_connection() {
		// Don't attempt to migrate if attempted before.
		if ( WC_Helper_Options::get( 'did-migrate' ) ) {
			return;
		}

		$auth = WC_Helper_Options::get( 'auth' );
		if ( ! empty( $auth ) ) {
			return;
		}

		WC_Helper::log( 'Attempting oauth/migrate' );
		WC_Helper_Options::update( 'did-migrate', true );

		$master_key = get_option( 'woothemes_helper_master_key' );
		if ( empty( $master_key ) ) {
			WC_Helper::log( 'Master key not found, aborting' );
			return;
		}

		$request = WC_Helper_API::post(
			'oauth/migrate',
			array(
				'body' => array(
					'home_url'   => home_url(),
					'master_key' => $master_key,
				),
			)
		);

		if ( is_wp_error( $request ) || wp_remote_retrieve_response_code( $request ) !== 200 ) {
			WC_Helper::log( 'Call to oauth/migrate returned a non-200 response code' );
			return;
		}

		$request_token = json_decode( wp_remote_retrieve_body( $request ) );
		if ( empty( $request_token ) ) {
			WC_Helper::log( 'Call to oauth/migrate returned an empty token' );
			return;
		}

		// Obtain an access token.
		$request = WC_Helper_API::post(
			'oauth/access_token',
			array(
				'body' => array(
					'request_token' => $request_token,
					'home_url'      => home_url(),
					'migrate'       => true,
				),
			)
		);

		if ( is_wp_error( $request ) || wp_remote_retrieve_response_code( $request ) !== 200 ) {
			WC_Helper::log( 'Call to oauth/access_token returned a non-200 response code' );
			return;
		}

		$access_token = json_decode( wp_remote_retrieve_body( $request ), true );
		if ( empty( $access_token ) ) {
			WC_Helper::log( 'Call to oauth/access_token returned an invalid token' );
			return;
		}

		WC_Helper_Options::update(
			'auth',
			array(
				'access_token'        => $access_token['access_token'],
				'access_token_secret' => $access_token['access_token_secret'],
				'site_id'             => $access_token['site_id'],
				'user_id'             => null, // Set this later
				'updated'             => time(),
			)
		);

		// Obtain the connected user info.
		if ( ! WC_Helper::_flush_authentication_cache() ) {
			WC_Helper::log( 'Could not obtain connected user info in migrate_connection' );
			WC_Helper_Options::update( 'auth', array() );
			return;
		}
	}

	/**
	 * Attempt to deactivate the legacy helper plugin.
	 */
	public static function deactivate_plugin() {
		include_once ABSPATH . 'wp-admin/includes/plugin.php';
		if ( ! function_exists( 'deactivate_plugins' ) ) {
			return;
		}

		if ( is_plugin_active( 'woothemes-updater/woothemes-updater.php' ) ) {
			deactivate_plugins( 'woothemes-updater/woothemes-updater.php' );

			// Notify the user when the plugin is deactivated.
			add_action( 'pre_current_active_plugins', array( __CLASS__, 'plugin_deactivation_notice' ) );
		}
	}

	/**
	 * Display admin notice directing the user where to go.
	 */
	public static function plugin_deactivation_notice() {
		?>
		<div id="message" class="error is-dismissible">
			<p><?php printf( __( 'The WooCommerce Helper plugin is no longer needed. <a href="%s">Manage subscriptions</a> from the extensions tab instead.', 'woocommerce' ), esc_url( admin_url( 'admin.php?page=wc-addons&section=helper' ) ) ); ?></p>
		</div>
		<?php
	}

	/**
	 * Register menu item.
	 */
	public static function admin_menu() {
		// No additional menu items for users who did not have a connected helper before.
		$master_key = get_option( 'woothemes_helper_master_key' );
		if ( empty( $master_key ) ) {
			return;
		}

		// Do not show the menu item if user has already seen the new screen.
		$auth = WC_Helper_Options::get( 'auth' );
		if ( ! empty( $auth['user_id'] ) ) {
			return;
		}

		add_dashboard_page( __( 'WooCommerce Helper', 'woocommerce' ), __( 'WooCommerce Helper', 'woocommerce' ), 'manage_options', 'woothemes-helper', array( __CLASS__, 'render_compat_menu' ) );
	}

	/**
	 * Render the legacy helper compat view.
	 */
	public static function render_compat_menu() {
		$helper_url = add_query_arg(
			array(
				'page'    => 'wc-addons',
				'section' => 'helper',
			),
			admin_url( 'admin.php' )
		);
		include WC_Helper::get_view_filename( 'html-helper-compat.php' );
	}
}

WC_Helper_Compat::load();
admin/helper/class-wc-helper-plugin-info.php000064400000003560151336044770015060 0ustar00<?php
/**
 * WooCommerce Admin Helper Plugin Info
 *
 * @package WooCommerce\Admin\Helper
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC_Helper_Plugin_Info Class
 *
 * Provides the "View Information" core modals with data for WooCommerce.com
 * hosted extensions.
 */
class WC_Helper_Plugin_Info {

	/**
	 * Loads the class, runs on init.
	 */
	public static function load() {
		add_filter( 'plugins_api', array( __CLASS__, 'plugins_api' ), 20, 3 );
	}

	/**
	 * Plugin information callback for Woo extensions.
	 *
	 * @param object $response The response core needs to display the modal.
	 * @param string $action The requested plugins_api() action.
	 * @param object $args Arguments passed to plugins_api().
	 *
	 * @return object An updated $response.
	 */
	public static function plugins_api( $response, $action, $args ) {
		if ( 'plugin_information' !== $action ) {
			return $response;
		}

		if ( empty( $args->slug ) ) {
			return $response;
		}

		// Only for slugs that start with woo-
		if ( 0 !== strpos( $args->slug, 'woocommerce-com-' ) ) {
			return $response;
		}

		$clean_slug = str_replace( 'woocommerce-com-', '', $args->slug );

		// Look through update data by slug.
		$update_data = WC_Helper_Updater::get_update_data();
		$products    = wp_list_filter( $update_data, array( 'slug' => $clean_slug ) );

		if ( empty( $products ) ) {
			return $response;
		}

		$product_id = array_keys( $products );
		$product_id = array_shift( $product_id );

		// Fetch the product information from the Helper API.
		$request = WC_Helper_API::get(
			add_query_arg(
				array(
					'product_id' => absint( $product_id ),
				),
				'info'
			),
			array( 'authenticated' => true )
		);

		$results = json_decode( wp_remote_retrieve_body( $request ), true );
		if ( ! empty( $results ) ) {
			$response = (object) $results;
		}

		return $response;
	}
}

WC_Helper_Plugin_Info::load();
admin/helper/class-wc-helper.php000064400000146502151336044770012637 0ustar00<?php
/**
 * WooCommerce Admin Helper
 *
 * @package WooCommerce\Admin\Helper
 */

use Automattic\Jetpack\Constants;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC_Helper Class
 *
 * The main entry-point for all things related to the Helper.
 */
class WC_Helper {
	/**
	 * A log object returned by wc_get_logger().
	 *
	 * @var $log
	 */
	public static $log;

	/**
	 * Get an absolute path to the requested helper view.
	 *
	 * @param string $view The requested view file.
	 *
	 * @return string The absolute path to the view file.
	 */
	public static function get_view_filename( $view ) {
		return dirname( __FILE__ ) . "/views/$view";
	}

	/**
	 * Loads the helper class, runs on init.
	 */
	public static function load() {
		self::includes();

		add_action( 'current_screen', array( __CLASS__, 'current_screen' ) );
		add_action( 'woocommerce_helper_output', array( __CLASS__, 'render_helper_output' ) );
		add_action( 'admin_enqueue_scripts', array( __CLASS__, 'admin_enqueue_scripts' ) );
		add_action( 'admin_notices', array( __CLASS__, 'admin_notices' ) );

		do_action( 'woocommerce_helper_loaded' );
	}

	/**
	 * Include supporting helper classes.
	 */
	protected static function includes() {
		include_once dirname( __FILE__ ) . '/class-wc-helper-options.php';
		include_once dirname( __FILE__ ) . '/class-wc-helper-api.php';
		include_once dirname( __FILE__ ) . '/class-wc-helper-updater.php';
		include_once dirname( __FILE__ ) . '/class-wc-helper-plugin-info.php';
		include_once dirname( __FILE__ ) . '/class-wc-helper-compat.php';
	}

	/**
	 * Render the helper section content based on context.
	 */
	public static function render_helper_output() {
		$auth           = WC_Helper_Options::get( 'auth' );
		$auth_user_data = WC_Helper_Options::get( 'auth_user_data' );

		// Return success/error notices.
		$notices = self::_get_return_notices();

		// No active connection.
		if ( empty( $auth['access_token'] ) ) {
			$connect_url = add_query_arg(
				array(
					'page'              => 'wc-addons',
					'section'           => 'helper',
					'wc-helper-connect' => 1,
					'wc-helper-nonce'   => wp_create_nonce( 'connect' ),
				),
				admin_url( 'admin.php' )
			);

			include self::get_view_filename( 'html-oauth-start.php' );
			return;
		}
		$disconnect_url = add_query_arg(
			array(
				'page'                 => 'wc-addons',
				'section'              => 'helper',
				'wc-helper-disconnect' => 1,
				'wc-helper-nonce'      => wp_create_nonce( 'disconnect' ),
			),
			admin_url( 'admin.php' )
		);

		$current_filter = self::get_current_filter();
		$refresh_url    = add_query_arg(
			array(
				'page'              => 'wc-addons',
				'section'           => 'helper',
				'filter'            => $current_filter,
				'wc-helper-refresh' => 1,
				'wc-helper-nonce'   => wp_create_nonce( 'refresh' ),
			),
			admin_url( 'admin.php' )
		);

		// Installed plugins and themes, with or without an active subscription.
		$woo_plugins = self::get_local_woo_plugins();
		$woo_themes  = self::get_local_woo_themes();

		$site_id                   = absint( $auth['site_id'] );
		$subscriptions             = self::get_subscriptions();
		$updates                   = WC_Helper_Updater::get_update_data();
		$subscriptions_product_ids = wp_list_pluck( $subscriptions, 'product_id' );

		foreach ( $subscriptions as &$subscription ) {
			$subscription['active'] = in_array( $site_id, $subscription['connections'] );

			$subscription['activate_url'] = add_query_arg(
				array(
					'page'                  => 'wc-addons',
					'section'               => 'helper',
					'filter'                => $current_filter,
					'wc-helper-activate'    => 1,
					'wc-helper-product-key' => $subscription['product_key'],
					'wc-helper-product-id'  => $subscription['product_id'],
					'wc-helper-nonce'       => wp_create_nonce( 'activate:' . $subscription['product_key'] ),
				),
				admin_url( 'admin.php' )
			);

			$subscription['deactivate_url'] = add_query_arg(
				array(
					'page'                  => 'wc-addons',
					'section'               => 'helper',
					'filter'                => $current_filter,
					'wc-helper-deactivate'  => 1,
					'wc-helper-product-key' => $subscription['product_key'],
					'wc-helper-product-id'  => $subscription['product_id'],
					'wc-helper-nonce'       => wp_create_nonce( 'deactivate:' . $subscription['product_key'] ),
				),
				admin_url( 'admin.php' )
			);

			$subscription['local'] = array(
				'installed' => false,
				'active'    => false,
				'version'   => null,
			);

			$subscription['update_url'] = admin_url( 'update-core.php' );

			$local = wp_list_filter( array_merge( $woo_plugins, $woo_themes ), array( '_product_id' => $subscription['product_id'] ) );

			if ( ! empty( $local ) ) {
				$local                              = array_shift( $local );
				$subscription['local']['installed'] = true;
				$subscription['local']['version']   = $local['Version'];

				if ( 'plugin' == $local['_type'] ) {
					if ( is_plugin_active( $local['_filename'] ) ) {
						$subscription['local']['active'] = true;
					} elseif ( is_multisite() && is_plugin_active_for_network( $local['_filename'] ) ) {
						$subscription['local']['active'] = true;
					}

					// A magic update_url.
					$subscription['update_url'] = wp_nonce_url( self_admin_url( 'update.php?action=upgrade-plugin&plugin=' ) . $local['_filename'], 'upgrade-plugin_' . $local['_filename'] );

				} elseif ( 'theme' == $local['_type'] ) {
					if ( in_array( $local['_stylesheet'], array( get_stylesheet(), get_template() ) ) ) {
						$subscription['local']['active'] = true;
					}

					// Another magic update_url.
					$subscription['update_url'] = wp_nonce_url( self_admin_url( 'update.php?action=upgrade-theme&theme=' . $local['_stylesheet'] ), 'upgrade-theme_' . $local['_stylesheet'] );
				}
			}

			$subscription['has_update'] = false;
			if ( $subscription['local']['installed'] && ! empty( $updates[ $subscription['product_id'] ] ) ) {
				$subscription['has_update'] = version_compare( $updates[ $subscription['product_id'] ]['version'], $subscription['local']['version'], '>' );
			}

			$subscription['download_primary'] = true;
			$subscription['download_url']     = 'https://woocommerce.com/my-account/downloads/';
			if ( ! $subscription['local']['installed'] && ! empty( $updates[ $subscription['product_id'] ] ) ) {
				$subscription['download_url'] = $updates[ $subscription['product_id'] ]['package'];
			}

			$subscription['actions'] = array();

			if ( $subscription['has_update'] && ! $subscription['expired'] ) {
				$action = array(
					/* translators: %s: version number */
					'message'      => sprintf( __( 'Version %s is <strong>available</strong>.', 'woocommerce' ), esc_html( $updates[ $subscription['product_id'] ]['version'] ) ),
					'button_label' => __( 'Update', 'woocommerce' ),
					'button_url'   => $subscription['update_url'],
					'status'       => 'update-available',
					'icon'         => 'dashicons-update',
				);

				// Subscription is not active on this site.
				if ( ! $subscription['active'] ) {
					$action['message']     .= ' ' . __( 'To enable this update you need to <strong>activate</strong> this subscription.', 'woocommerce' );
					$action['button_label'] = null;
					$action['button_url']   = null;
				}

				$subscription['actions'][] = $action;
			}

			if ( $subscription['has_update'] && $subscription['expired'] ) {
				$action = array(
					/* translators: %s: version number */
					'message' => sprintf( __( 'Version %s is <strong>available</strong>.', 'woocommerce' ), esc_html( $updates[ $subscription['product_id'] ]['version'] ) ),
					'status'  => 'expired',
					'icon'    => 'dashicons-info',
				);

				$action['message']     .= ' ' . __( 'To enable this update you need to <strong>purchase</strong> a new subscription.', 'woocommerce' );
				$action['button_label'] = __( 'Purchase', 'woocommerce' );
				$action['button_url']   = $subscription['product_url'];

				$subscription['actions'][] = $action;
			} elseif ( $subscription['expired'] && ! empty( $subscription['master_user_email'] ) ) {
				$action = array(
					'message' => sprintf( __( 'This subscription has expired. Contact the owner to <strong>renew</strong> the subscription to receive updates and support.', 'woocommerce' ) ),
					'status'  => 'expired',
					'icon'    => 'dashicons-info',
				);

				$subscription['actions'][] = $action;
			} elseif ( $subscription['expired'] ) {
				$action = array(
					'message'      => sprintf( __( 'This subscription has expired. Please <strong>renew</strong> to receive updates and support.', 'woocommerce' ) ),
					'button_label' => __( 'Renew', 'woocommerce' ),
					'button_url'   => 'https://woocommerce.com/my-account/my-subscriptions/',
					'status'       => 'expired',
					'icon'         => 'dashicons-info',
				);

				$subscription['actions'][] = $action;
			}

			if ( $subscription['expiring'] && ! $subscription['autorenew'] ) {
				$action = array(
					'message'      => __( 'Subscription is <strong>expiring</strong> soon.', 'woocommerce' ),
					'button_label' => __( 'Enable auto-renew', 'woocommerce' ),
					'button_url'   => 'https://woocommerce.com/my-account/my-subscriptions/',
					'status'       => 'expired',
					'icon'         => 'dashicons-info',
				);

				$subscription['download_primary'] = false;
				$subscription['actions'][]        = $action;
			} elseif ( $subscription['expiring'] ) {
				$action = array(
					'message'      => sprintf( __( 'This subscription is expiring soon. Please <strong>renew</strong> to continue receiving updates and support.', 'woocommerce' ) ),
					'button_label' => __( 'Renew', 'woocommerce' ),
					'button_url'   => 'https://woocommerce.com/my-account/my-subscriptions/',
					'status'       => 'expired',
					'icon'         => 'dashicons-info',
				);

				$subscription['download_primary'] = false;
				$subscription['actions'][]        = $action;
			}

			// Mark the first action primary.
			foreach ( $subscription['actions'] as $key => $action ) {
				if ( ! empty( $action['button_label'] ) ) {
					$subscription['actions'][ $key ]['primary'] = true;
					break;
				}
			}
		}

		// Break the by-ref.
		unset( $subscription );

		// Installed products without a subscription.
		$no_subscriptions = array();
		foreach ( array_merge( $woo_plugins, $woo_themes ) as $filename => $data ) {
			if ( in_array( $data['_product_id'], $subscriptions_product_ids ) ) {
				continue;
			}

			$data['_product_url'] = '#';
			$data['_has_update']  = false;

			if ( ! empty( $updates[ $data['_product_id'] ] ) ) {
				$data['_has_update'] = version_compare( $updates[ $data['_product_id'] ]['version'], $data['Version'], '>' );

				if ( ! empty( $updates[ $data['_product_id'] ]['url'] ) ) {
					$data['_product_url'] = $updates[ $data['_product_id'] ]['url'];
				} elseif ( ! empty( $data['PluginURI'] ) ) {
					$data['_product_url'] = $data['PluginURI'];
				}
			}

			$data['_actions'] = array();

			if ( $data['_has_update'] ) {
				$action = array(
					/* translators: %s: version number */
					'message'      => sprintf( __( 'Version %s is <strong>available</strong>. To enable this update you need to <strong>purchase</strong> a new subscription.', 'woocommerce' ), esc_html( $updates[ $data['_product_id'] ]['version'] ) ),
					'button_label' => __( 'Purchase', 'woocommerce' ),
					'button_url'   => $data['_product_url'],
					'status'       => 'expired',
					'icon'         => 'dashicons-info',
				);

				$data['_actions'][] = $action;
			} else {
				$action = array(
					/* translators: 1: subscriptions docs 2: subscriptions docs */
					'message'      => sprintf( __( 'To receive updates and support for this extension, you need to <strong>purchase</strong> a new subscription or consolidate your extensions to one connected account by <strong><a href="%1$s" title="Sharing Docs">sharing</a> or <a href="%2$s" title="Transferring Docs">transferring</a></strong> this extension to this connected account.', 'woocommerce' ), 'https://docs.woocommerce.com/document/managing-woocommerce-com-subscriptions/#section-10', 'https://docs.woocommerce.com/document/managing-woocommerce-com-subscriptions/#section-5' ),
					'button_label' => __( 'Purchase', 'woocommerce' ),
					'button_url'   => $data['_product_url'],
					'status'       => 'expired',
					'icon'         => 'dashicons-info',
				);

				$data['_actions'][] = $action;
			}

			$no_subscriptions[ $filename ] = $data;
		}

		// Update the user id if it came from a migrated connection.
		if ( empty( $auth['user_id'] ) ) {
			$auth['user_id'] = get_current_user_id();
			WC_Helper_Options::update( 'auth', $auth );
		}

		// Sort alphabetically.
		uasort( $subscriptions, array( __CLASS__, '_sort_by_product_name' ) );
		uasort( $no_subscriptions, array( __CLASS__, '_sort_by_name' ) );

		// Filters.
		self::get_filters_counts( $subscriptions ); // Warm it up.
		self::_filter( $subscriptions, self::get_current_filter() );

		// We have an active connection.
		include self::get_view_filename( 'html-main.php' );
		return;
	}

	/**
	 * Get available subscriptions filters.
	 *
	 * @return array An array of filter keys and labels.
	 */
	public static function get_filters() {
		$filters = array(
			'all'              => __( 'All', 'woocommerce' ),
			'active'           => __( 'Active', 'woocommerce' ),
			'inactive'         => __( 'Inactive', 'woocommerce' ),
			'installed'        => __( 'Installed', 'woocommerce' ),
			'update-available' => __( 'Update Available', 'woocommerce' ),
			'expiring'         => __( 'Expiring Soon', 'woocommerce' ),
			'expired'          => __( 'Expired', 'woocommerce' ),
			'download'         => __( 'Download', 'woocommerce' ),
		);

		return $filters;
	}

	/**
	 * Get counts data for the filters array.
	 *
	 * @param array $subscriptions The array of all available subscriptions.
	 *
	 * @return array Filter counts (filter => count).
	 */
	public static function get_filters_counts( $subscriptions = null ) {
		static $filters;

		if ( isset( $filters ) ) {
			return $filters;
		}

		$filters = array_fill_keys( array_keys( self::get_filters() ), 0 );
		if ( empty( $subscriptions ) ) {
			return array();
		}

		foreach ( $filters as $key => $count ) {
			$_subs = $subscriptions;
			self::_filter( $_subs, $key );
			$filters[ $key ] = count( $_subs );
		}

		return $filters;
	}

	/**
	 * Get current filter.
	 *
	 * @return string The current filter.
	 */
	public static function get_current_filter() {
		$current_filter = 'all';
		$valid_filters  = array_keys( self::get_filters() );

		if ( ! empty( $_GET['filter'] ) && in_array( wp_unslash( $_GET['filter'] ), $valid_filters ) ) {
			$current_filter = wc_clean( wp_unslash( $_GET['filter'] ) );
		}

		return $current_filter;
	}

	/**
	 * Filter an array of subscriptions by $filter.
	 *
	 * @param array  $subscriptions The subscriptions array, passed by ref.
	 * @param string $filter The filter.
	 */
	private static function _filter( &$subscriptions, $filter ) {
		switch ( $filter ) {
			case 'active':
				$subscriptions = wp_list_filter( $subscriptions, array( 'active' => true ) );
				break;

			case 'inactive':
				$subscriptions = wp_list_filter( $subscriptions, array( 'active' => false ) );
				break;

			case 'installed':
				foreach ( $subscriptions as $key => $subscription ) {
					if ( empty( $subscription['local']['installed'] ) ) {
						unset( $subscriptions[ $key ] );
					}
				}
				break;

			case 'update-available':
				$subscriptions = wp_list_filter( $subscriptions, array( 'has_update' => true ) );
				break;

			case 'expiring':
				$subscriptions = wp_list_filter( $subscriptions, array( 'expiring' => true ) );
				break;

			case 'expired':
				$subscriptions = wp_list_filter( $subscriptions, array( 'expired' => true ) );
				break;

			case 'download':
				foreach ( $subscriptions as $key => $subscription ) {
					if ( $subscription['local']['installed'] || $subscription['expired'] ) {
						unset( $subscriptions[ $key ] );
					}
				}
				break;
		}
	}

	/**
	 * Enqueue admin scripts and styles.
	 */
	public static function admin_enqueue_scripts() {
		$screen       = get_current_screen();
		$screen_id    = $screen ? $screen->id : '';
		$wc_screen_id = sanitize_title( __( 'WooCommerce', 'woocommerce' ) );

		if ( $wc_screen_id . '_page_wc-addons' === $screen_id && isset( $_GET['section'] ) && 'helper' === $_GET['section'] ) {
			wp_enqueue_style( 'woocommerce-helper', WC()->plugin_url() . '/assets/css/helper.css', array(), Constants::get_constant( 'WC_VERSION' ) );
			wp_style_add_data( 'woocommerce-helper', 'rtl', 'replace' );
		}
	}

	/**
	 * Various success/error notices.
	 *
	 * Runs during admin page render, so no headers/redirects here.
	 *
	 * @return array Array pairs of message/type strings with notices.
	 */
	private static function _get_return_notices() {
		$return_status = isset( $_GET['wc-helper-status'] ) ? wc_clean( wp_unslash( $_GET['wc-helper-status'] ) ) : null;
		$notices       = array();

		switch ( $return_status ) {
			case 'activate-success':
				$product_id   = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0;
				$subscription = self::_get_subscriptions_from_product_id( $product_id );
				$notices[]    = array(
					'type'    => 'updated',
					'message' => sprintf(
						/* translators: %s: product name */
						__( '%s activated successfully. You will now receive updates for this product.', 'woocommerce' ),
						'<strong>' . esc_html( $subscription['product_name'] ) . '</strong>'
					),
				);
				break;

			case 'activate-error':
				$product_id   = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0;
				$subscription = self::_get_subscriptions_from_product_id( $product_id );
				$notices[]    = array(
					'type'    => 'error',
					'message' => sprintf(
						/* translators: %s: product name */
						__( 'An error has occurred when activating %s. Please try again later.', 'woocommerce' ),
						'<strong>' . esc_html( $subscription['product_name'] ) . '</strong>'
					),
				);
				break;

			case 'deactivate-success':
				$product_id   = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0;
				$subscription = self::_get_subscriptions_from_product_id( $product_id );
				$local        = self::_get_local_from_product_id( $product_id );

				$message = sprintf(
					/* translators: %s: product name */
					__( 'Subscription for %s deactivated successfully. You will no longer receive updates for this product.', 'woocommerce' ),
					'<strong>' . esc_html( $subscription['product_name'] ) . '</strong>'
				);

				if ( $local && is_plugin_active( $local['_filename'] ) && current_user_can( 'activate_plugins' ) ) {
					$deactivate_plugin_url = add_query_arg(
						array(
							'page'                        => 'wc-addons',
							'section'                     => 'helper',
							'filter'                      => self::get_current_filter(),
							'wc-helper-deactivate-plugin' => 1,
							'wc-helper-product-id'        => $subscription['product_id'],
							'wc-helper-nonce'             => wp_create_nonce( 'deactivate-plugin:' . $subscription['product_id'] ),
						),
						admin_url( 'admin.php' )
					);

					$message = sprintf(
						/* translators: %1$s: product name, %2$s: deactivate url */
						__( 'Subscription for %1$s deactivated successfully. You will no longer receive updates for this product. <a href="%2$s">Click here</a> if you wish to deactivate the plugin as well.', 'woocommerce' ),
						'<strong>' . esc_html( $subscription['product_name'] ) . '</strong>',
						esc_url( $deactivate_plugin_url )
					);
				}

				$notices[] = array(
					'message' => $message,
					'type'    => 'updated',
				);
				break;

			case 'deactivate-error':
				$product_id   = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0;
				$subscription = self::_get_subscriptions_from_product_id( $product_id );
				$notices[]    = array(
					'type'    => 'error',
					'message' => sprintf(
						/* translators: %s: product name */
						__( 'An error has occurred when deactivating the subscription for %s. Please try again later.', 'woocommerce' ),
						'<strong>' . esc_html( $subscription['product_name'] ) . '</strong>'
					),
				);
				break;

			case 'deactivate-plugin-success':
				$product_id   = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0;
				$subscription = self::_get_subscriptions_from_product_id( $product_id );
				$notices[]    = array(
					'type'    => 'updated',
					'message' => sprintf(
						/* translators: %s: product name */
						__( 'The extension %s has been deactivated successfully.', 'woocommerce' ),
						'<strong>' . esc_html( $subscription['product_name'] ) . '</strong>'
					),
				);
				break;

			case 'deactivate-plugin-error':
				$product_id   = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0;
				$subscription = self::_get_subscriptions_from_product_id( $product_id );
				$notices[]    = array(
					'type'    => 'error',
					'message' => sprintf(
						/* translators: %1$s: product name, %2$s: plugins screen url */
						__( 'An error has occurred when deactivating the extension %1$s. Please proceed to the <a href="%2$s">Plugins screen</a> to deactivate it manually.', 'woocommerce' ),
						'<strong>' . esc_html( $subscription['product_name'] ) . '</strong>',
						admin_url( 'plugins.php' )
					),
				);
				break;

			case 'helper-connected':
				$notices[] = array(
					'message' => __( 'You have successfully connected your store to WooCommerce.com', 'woocommerce' ),
					'type'    => 'updated',
				);
				break;

			case 'helper-disconnected':
				$notices[] = array(
					'message' => __( 'You have successfully disconnected your store from WooCommerce.com', 'woocommerce' ),
					'type'    => 'updated',
				);
				break;

			case 'helper-refreshed':
				$notices[] = array(
					'message' => __( 'Authentication and subscription caches refreshed successfully.', 'woocommerce' ),
					'type'    => 'updated',
				);
				break;
		}

		return $notices;
	}

	/**
	 * Various early-phase actions with possible redirects.
	 *
	 * @param object $screen WP screen object.
	 */
	public static function current_screen( $screen ) {
		$wc_screen_id = sanitize_title( __( 'WooCommerce', 'woocommerce' ) );

		if ( $wc_screen_id . '_page_wc-addons' !== $screen->id ) {
			return;
		}

		if ( empty( $_GET['section'] ) || 'helper' !== $_GET['section'] ) {
			return;
		}

		if ( ! empty( $_GET['wc-helper-connect'] ) ) {
			return self::_helper_auth_connect();
		}

		if ( ! empty( $_GET['wc-helper-return'] ) ) {
			return self::_helper_auth_return();
		}

		if ( ! empty( $_GET['wc-helper-disconnect'] ) ) {
			return self::_helper_auth_disconnect();
		}

		if ( ! empty( $_GET['wc-helper-refresh'] ) ) {
			return self::_helper_auth_refresh();
		}

		if ( ! empty( $_GET['wc-helper-activate'] ) ) {
			return self::_helper_subscription_activate();
		}

		if ( ! empty( $_GET['wc-helper-deactivate'] ) ) {
			return self::_helper_subscription_deactivate();
		}

		if ( ! empty( $_GET['wc-helper-deactivate-plugin'] ) ) {
			return self::_helper_plugin_deactivate();
		}
	}

	/**
	 * Initiate a new OAuth connection.
	 */
	private static function _helper_auth_connect() {
		if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'connect' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			self::log( 'Could not verify nonce in _helper_auth_connect' );
			wp_die( 'Could not verify nonce' );
		}

		$redirect_uri = add_query_arg(
			array(
				'page'             => 'wc-addons',
				'section'          => 'helper',
				'wc-helper-return' => 1,
				'wc-helper-nonce'  => wp_create_nonce( 'connect' ),
			),
			admin_url( 'admin.php' )
		);

		$request = WC_Helper_API::post(
			'oauth/request_token',
			array(
				'body' => array(
					'home_url'     => home_url(),
					'redirect_uri' => $redirect_uri,
				),
			)
		);

		$code = wp_remote_retrieve_response_code( $request );

		if ( 200 !== $code ) {
			self::log( sprintf( 'Call to oauth/request_token returned a non-200 response code (%d)', $code ) );
			wp_die( 'Something went wrong' );
		}

		$secret = json_decode( wp_remote_retrieve_body( $request ) );
		if ( empty( $secret ) ) {
			self::log( sprintf( 'Call to oauth/request_token returned an invalid body: %s', wp_remote_retrieve_body( $request ) ) );
			wp_die( 'Something went wrong' );
		}

		/**
		 * Fires when the Helper connection process is initiated.
		 */
		do_action( 'woocommerce_helper_connect_start' );

		$connect_url = add_query_arg(
			array(
				'home_url'     => rawurlencode( home_url() ),
				'redirect_uri' => rawurlencode( $redirect_uri ),
				'secret'       => rawurlencode( $secret ),
			),
			WC_Helper_API::url( 'oauth/authorize' )
		);

		wp_redirect( esc_url_raw( $connect_url ) );
		die();
	}

	/**
	 * Return from WooCommerce.com OAuth flow.
	 */
	private static function _helper_auth_return() {
		if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'connect' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			self::log( 'Could not verify nonce in _helper_auth_return' );
			wp_die( 'Something went wrong' );
		}

		// Bail if the user clicked deny.
		if ( ! empty( $_GET['deny'] ) ) {
			/**
			 * Fires when the Helper connection process is denied/cancelled.
			 */
			do_action( 'woocommerce_helper_denied' );
			wp_safe_redirect( admin_url( 'admin.php?page=wc-addons&section=helper' ) );
			die();
		}

		// We do need a request token...
		if ( empty( $_GET['request_token'] ) ) {
			self::log( 'Request token not found in _helper_auth_return' );
			wp_die( 'Something went wrong' );
		}

		// Obtain an access token.
		$request = WC_Helper_API::post(
			'oauth/access_token',
			array(
				'body' => array(
					'request_token' => wp_unslash( $_GET['request_token'] ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
					'home_url'      => home_url(),
				),
			)
		);

		$code = wp_remote_retrieve_response_code( $request );

		if ( 200 !== $code ) {
			self::log( sprintf( 'Call to oauth/access_token returned a non-200 response code (%d)', $code ) );
			wp_die( 'Something went wrong' );
		}

		$access_token = json_decode( wp_remote_retrieve_body( $request ), true );
		if ( ! $access_token ) {
			self::log( sprintf( 'Call to oauth/access_token returned an invalid body: %s', wp_remote_retrieve_body( $request ) ) );
			wp_die( 'Something went wrong' );
		}

		WC_Helper_Options::update(
			'auth',
			array(
				'access_token'        => $access_token['access_token'],
				'access_token_secret' => $access_token['access_token_secret'],
				'site_id'             => $access_token['site_id'],
				'user_id'             => get_current_user_id(),
				'updated'             => time(),
			)
		);

		// Obtain the connected user info.
		if ( ! self::_flush_authentication_cache() ) {
			self::log( 'Could not obtain connected user info in _helper_auth_return' );
			WC_Helper_Options::update( 'auth', array() );
			wp_die( 'Something went wrong.' );
		}

		self::_flush_subscriptions_cache();
		self::_flush_updates_cache();

		/**
		 * Fires when the Helper connection process has completed successfully.
		 */
		do_action( 'woocommerce_helper_connected' );

		// Enable tracking when connected.
		if ( class_exists( 'WC_Tracker' ) ) {
			update_option( 'woocommerce_allow_tracking', 'yes' );
			WC_Tracker::send_tracking_data( true );
		}

		// If connecting through in-app purchase, redirects back to WooCommerce.com
		// for product installation.
		if ( ! empty( $_GET['wccom-install-url'] ) ) {
			wp_redirect( wp_unslash( $_GET['wccom-install-url'] ) );
			exit;
		}

		wp_safe_redirect(
			add_query_arg(
				array(
					'page'             => 'wc-addons',
					'section'          => 'helper',
					'wc-helper-status' => 'helper-connected',
				),
				admin_url( 'admin.php' )
			)
		);
		die();
	}

	/**
	 * Disconnect from WooCommerce.com, clear OAuth tokens.
	 */
	private static function _helper_auth_disconnect() {
		if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'disconnect' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			self::log( 'Could not verify nonce in _helper_auth_disconnect' );
			wp_die( 'Could not verify nonce' );
		}

		/**
		 * Fires when the Helper has been disconnected.
		 */
		do_action( 'woocommerce_helper_disconnected' );

		$redirect_uri = add_query_arg(
			array(
				'page'             => 'wc-addons',
				'section'          => 'helper',
				'wc-helper-status' => 'helper-disconnected',
			),
			admin_url( 'admin.php' )
		);

		WC_Helper_API::post(
			'oauth/invalidate_token',
			array(
				'authenticated' => true,
			)
		);

		WC_Helper_Options::update( 'auth', array() );
		WC_Helper_Options::update( 'auth_user_data', array() );

		self::_flush_subscriptions_cache();
		self::_flush_updates_cache();

		wp_safe_redirect( $redirect_uri );
		die();
	}

	/**
	 * User hit the Refresh button, clear all caches.
	 */
	private static function _helper_auth_refresh() {
		if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'refresh' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			self::log( 'Could not verify nonce in _helper_auth_refresh' );
			wp_die( 'Could not verify nonce' );
		}

		/**
		 * Fires when Helper subscriptions are refreshed.
		 */
		do_action( 'woocommerce_helper_subscriptions_refresh' );

		$redirect_uri = add_query_arg(
			array(
				'page'             => 'wc-addons',
				'section'          => 'helper',
				'filter'           => self::get_current_filter(),
				'wc-helper-status' => 'helper-refreshed',
			),
			admin_url( 'admin.php' )
		);

		self::_flush_authentication_cache();
		self::_flush_subscriptions_cache();
		self::_flush_updates_cache();

		wp_safe_redirect( $redirect_uri );
		die();
	}

	/**
	 * Active a product subscription.
	 */
	private static function _helper_subscription_activate() {
		$product_key = isset( $_GET['wc-helper-product-key'] ) ? wc_clean( wp_unslash( $_GET['wc-helper-product-key'] ) ) : '';
		$product_id  = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0;

		if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'activate:' . $product_key ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			self::log( 'Could not verify nonce in _helper_subscription_activate' );
			wp_die( 'Could not verify nonce' );
		}

		// Activate subscription.
		$activation_response = WC_Helper_API::post(
			'activate',
			array(
				'authenticated' => true,
				'body'          => wp_json_encode(
					array(
						'product_key' => $product_key,
					)
				),
			)
		);

		$activated = wp_remote_retrieve_response_code( $activation_response ) === 200;
		$body      = json_decode( wp_remote_retrieve_body( $activation_response ), true );

		if ( ! $activated && ! empty( $body['code'] ) && 'already_connected' === $body['code'] ) {
			$activated = true;
		}

		if ( $activated ) {
			/**
			 * Fires when the Helper activates a product successfully.
			 *
			 * @param int    $product_id Product ID being activated.
			 * @param string $product_key Subscription product key.
			 * @param array  $activation_response The response object from wp_safe_remote_request().
			 */
			do_action( 'woocommerce_helper_subscription_activate_success', $product_id, $product_key, $activation_response );
		} else {
			/**
			 * Fires when the Helper fails to activate a product.
			 *
			 * @param int    $product_id Product ID being activated.
			 * @param string $product_key Subscription product key.
			 * @param array  $activation_response The response object from wp_safe_remote_request().
			 */
			do_action( 'woocommerce_helper_subscription_activate_error', $product_id, $product_key, $activation_response );
		}

		// Attempt to activate this plugin.
		$local = self::_get_local_from_product_id( $product_id );
		if ( $local && 'plugin' == $local['_type'] && current_user_can( 'activate_plugins' ) && ! is_plugin_active( $local['_filename'] ) ) {
			activate_plugin( $local['_filename'] );
		}

		self::_flush_subscriptions_cache();
		self::_flush_updates_cache();

		$redirect_uri = add_query_arg(
			array(
				'page'                 => 'wc-addons',
				'section'              => 'helper',
				'filter'               => self::get_current_filter(),
				'wc-helper-status'     => $activated ? 'activate-success' : 'activate-error',
				'wc-helper-product-id' => $product_id,
			),
			admin_url( 'admin.php' )
		);

		wp_safe_redirect( $redirect_uri );
		die();
	}

	/**
	 * Deactivate a product subscription.
	 */
	private static function _helper_subscription_deactivate() {
		$product_key = isset( $_GET['wc-helper-product-key'] ) ? wc_clean( wp_unslash( $_GET['wc-helper-product-key'] ) ) : '';
		$product_id  = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0;

		if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'deactivate:' . $product_key ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			self::log( 'Could not verify nonce in _helper_subscription_deactivate' );
			wp_die( 'Could not verify nonce' );
		}

		$deactivation_response = WC_Helper_API::post(
			'deactivate',
			array(
				'authenticated' => true,
				'body'          => wp_json_encode(
					array(
						'product_key' => $product_key,
					)
				),
			)
		);

		$code        = wp_remote_retrieve_response_code( $deactivation_response );
		$deactivated = 200 === $code;

		if ( $deactivated ) {
			/**
			 * Fires when the Helper activates a product successfully.
			 *
			 * @param int    $product_id Product ID being deactivated.
			 * @param string $product_key Subscription product key.
			 * @param array  $deactivation_response The response object from wp_safe_remote_request().
			 */
			do_action( 'woocommerce_helper_subscription_deactivate_success', $product_id, $product_key, $deactivation_response );
		} else {
			self::log( sprintf( 'Deactivate API call returned a non-200 response code (%d)', $code ) );

			/**
			 * Fires when the Helper fails to activate a product.
			 *
			 * @param int    $product_id Product ID being deactivated.
			 * @param string $product_key Subscription product key.
			 * @param array  $deactivation_response The response object from wp_safe_remote_request().
			 */
			do_action( 'woocommerce_helper_subscription_deactivate_error', $product_id, $product_key, $deactivation_response );
		}

		self::_flush_subscriptions_cache();

		$redirect_uri = add_query_arg(
			array(
				'page'                 => 'wc-addons',
				'section'              => 'helper',
				'filter'               => self::get_current_filter(),
				'wc-helper-status'     => $deactivated ? 'deactivate-success' : 'deactivate-error',
				'wc-helper-product-id' => $product_id,
			),
			admin_url( 'admin.php' )
		);

		wp_safe_redirect( $redirect_uri );
		die();
	}

	/**
	 * Deactivate a plugin.
	 */
	private static function _helper_plugin_deactivate() {
		$product_id  = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0;
		$deactivated = false;

		if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'deactivate-plugin:' . $product_id ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			self::log( 'Could not verify nonce in _helper_plugin_deactivate' );
			wp_die( 'Could not verify nonce' );
		}

		if ( ! current_user_can( 'activate_plugins' ) ) {
			wp_die( 'You are not allowed to manage plugins on this site.' );
		}

		$local = wp_list_filter(
			array_merge(
				self::get_local_woo_plugins(),
				self::get_local_woo_themes()
			),
			array( '_product_id' => $product_id )
		);

		// Attempt to deactivate this plugin or theme.
		if ( ! empty( $local ) ) {
			$local = array_shift( $local );
			if ( is_plugin_active( $local['_filename'] ) ) {
				deactivate_plugins( $local['_filename'] );
			}

			$deactivated = ! is_plugin_active( $local['_filename'] );
		}

		$redirect_uri = add_query_arg(
			array(
				'page'                 => 'wc-addons',
				'section'              => 'helper',
				'filter'               => self::get_current_filter(),
				'wc-helper-status'     => $deactivated ? 'deactivate-plugin-success' : 'deactivate-plugin-error',
				'wc-helper-product-id' => $product_id,
			),
			admin_url( 'admin.php' )
		);

		wp_safe_redirect( $redirect_uri );
		die();
	}

	/**
	 * Get a local plugin/theme entry from product_id.
	 *
	 * @param int $product_id The product id.
	 *
	 * @return array|bool The array containing the local plugin/theme data or false.
	 */
	private static function _get_local_from_product_id( $product_id ) {
		$local = wp_list_filter(
			array_merge(
				self::get_local_woo_plugins(),
				self::get_local_woo_themes()
			),
			array( '_product_id' => $product_id )
		);

		if ( ! empty( $local ) ) {
			return array_shift( $local );
		}

		return false;
	}

	/**
	 * Checks whether current site has product subscription of a given ID.
	 *
	 * @since 3.7.0
	 *
	 * @param int $product_id The product id.
	 *
	 * @return bool Returns true if product subscription exists, false otherwise.
	 */
	public static function has_product_subscription( $product_id ) {
		$subscription = self::_get_subscriptions_from_product_id( $product_id, true );
		return ! empty( $subscription );
	}

	/**
	 * Get a subscription entry from product_id. If multiple subscriptions are
	 * found with the same product id and $single is set to true, will return the
	 * first one in the list, so you can use this method to get things like extension
	 * name, version, etc.
	 *
	 * @param int  $product_id The product id.
	 * @param bool $single Whether to return a single subscription or all matching a product id.
	 *
	 * @return array|bool The array containing sub data or false.
	 */
	private static function _get_subscriptions_from_product_id( $product_id, $single = true ) {
		$subscriptions = wp_list_filter( self::get_subscriptions(), array( 'product_id' => $product_id ) );
		if ( ! empty( $subscriptions ) ) {
			return $single ? array_shift( $subscriptions ) : $subscriptions;
		}

		return false;
	}

	/**
	 * Obtain a list of data about locally installed Woo extensions.
	 */
	public static function get_local_woo_plugins() {
		if ( ! function_exists( 'get_plugins' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}

		$plugins = get_plugins();

		/**
		 * Check if plugins have WC headers, if not then clear cache and fetch again.
		 * WC Headers will not be present if `wc_enable_wc_plugin_headers` hook was added after a `get_plugins` call -- for example when WC is activated/updated.
		 * Also, get_plugins call is expensive so we should clear this cache very conservatively.
		 */
		if ( ! empty( $plugins ) && ! array_key_exists( 'Woo', current( $plugins ) ) ) {
			wp_clean_plugins_cache( false );
			$plugins = get_plugins();
		}

		$woo_plugins = array();

		// Backwards compatibility for woothemes_queue_update().
		$_compat = array();
		if ( ! empty( $GLOBALS['woothemes_queued_updates'] ) ) {
			foreach ( $GLOBALS['woothemes_queued_updates'] as $_compat_plugin ) {
				$_compat[ $_compat_plugin->file ] = array(
					'product_id' => $_compat_plugin->product_id,
					'file_id'    => $_compat_plugin->file_id,
				);
			}
		}

		foreach ( $plugins as $filename => $data ) {
			if ( empty( $data['Woo'] ) && ! empty( $_compat[ $filename ] ) ) {
				$data['Woo'] = sprintf( '%d:%s', $_compat[ $filename ]['product_id'], $_compat[ $filename ]['file_id'] );
			}

			if ( empty( $data['Woo'] ) ) {
				continue;
			}

			list( $product_id, $file_id ) = explode( ':', $data['Woo'] );
			if ( empty( $product_id ) || empty( $file_id ) ) {
				continue;
			}

			$data['_filename']        = $filename;
			$data['_product_id']      = absint( $product_id );
			$data['_file_id']         = $file_id;
			$data['_type']            = 'plugin';
			$data['slug']             = dirname( $filename );
			$woo_plugins[ $filename ] = $data;
		}

		return $woo_plugins;
	}

	/**
	 * Get locally installed Woo themes.
	 */
	public static function get_local_woo_themes() {
		$themes     = wp_get_themes();
		$woo_themes = array();

		foreach ( $themes as $theme ) {
			$header = $theme->get( 'Woo' );

			// Backwards compatibility for theme_info.txt.
			if ( ! $header ) {
				$txt = $theme->get_stylesheet_directory() . '/theme_info.txt';
				if ( is_readable( $txt ) ) {
					$txt = file_get_contents( $txt );
					$txt = preg_split( '#\s#', $txt );
					if ( count( $txt ) >= 2 ) {
						$header = sprintf( '%d:%s', $txt[0], $txt[1] );
					}
				}
			}

			if ( empty( $header ) ) {
				continue;
			}

			list( $product_id, $file_id ) = explode( ':', $header );
			if ( empty( $product_id ) || empty( $file_id ) ) {
				continue;
			}

			$data = array(
				'Name'        => $theme->get( 'Name' ),
				'Version'     => $theme->get( 'Version' ),
				'Woo'         => $header,

				'_filename'   => $theme->get_stylesheet() . '/style.css',
				'_stylesheet' => $theme->get_stylesheet(),
				'_product_id' => absint( $product_id ),
				'_file_id'    => $file_id,
				'_type'       => 'theme',
			);

			$woo_themes[ $data['_filename'] ] = $data;
		}

		return $woo_themes;
	}

	/**
	 * Get the connected user's subscriptions.
	 *
	 * @return array
	 */
	public static function get_subscriptions() {
		$cache_key = '_woocommerce_helper_subscriptions';
		$data      = get_transient( $cache_key );
		if ( false !== $data ) {
			return $data;
		}

		// Obtain the connected user info.
		$request = WC_Helper_API::get(
			'subscriptions',
			array(
				'authenticated' => true,
			)
		);

		if ( wp_remote_retrieve_response_code( $request ) !== 200 ) {
			set_transient( $cache_key, array(), 15 * MINUTE_IN_SECONDS );
			return array();
		}

		$data = json_decode( wp_remote_retrieve_body( $request ), true );
		if ( empty( $data ) || ! is_array( $data ) ) {
			$data = array();
		}

		set_transient( $cache_key, $data, 1 * HOUR_IN_SECONDS );
		return $data;
	}

	/**
	 * Runs when any plugin is activated.
	 *
	 * Depending on the activated plugin attempts to look through available
	 * subscriptions and auto-activate one if possible, so the user does not
	 * need to visit the Helper UI at all after installing a new extension.
	 *
	 * @param string $filename The filename of the activated plugin.
	 */
	public static function activated_plugin( $filename ) {
		$plugins = self::get_local_woo_plugins();

		// Not a local woo plugin.
		if ( empty( $plugins[ $filename ] ) ) {
			return;
		}

		// Make sure we have a connection.
		$auth = WC_Helper_Options::get( 'auth' );
		if ( empty( $auth ) ) {
			return;
		}

		$plugin        = $plugins[ $filename ];
		$product_id    = $plugin['_product_id'];
		$subscriptions = self::_get_subscriptions_from_product_id( $product_id, false );

		// No valid subscriptions for this product.
		if ( empty( $subscriptions ) ) {
			return;
		}

		$subscription = null;
		foreach ( $subscriptions as $_sub ) {

			// Don't attempt to activate expired subscriptions.
			if ( $_sub['expired'] ) {
				continue;
			}

			// No more sites available in this subscription.
			if ( $_sub['sites_max'] && $_sub['sites_active'] >= $_sub['sites_max'] ) {
				continue;
			}

			// Looks good.
			$subscription = $_sub;
			break;
		}

		// No valid subscription found.
		if ( ! $subscription ) {
			return;
		}

		$product_key         = $subscription['product_key'];
		$activation_response = WC_Helper_API::post(
			'activate',
			array(
				'authenticated' => true,
				'body'          => wp_json_encode(
					array(
						'product_key' => $product_key,
					)
				),
			)
		);

		$activated = wp_remote_retrieve_response_code( $activation_response ) === 200;
		$body      = json_decode( wp_remote_retrieve_body( $activation_response ), true );

		if ( ! $activated && ! empty( $body['code'] ) && 'already_connected' === $body['code'] ) {
			$activated = true;
		}

		if ( $activated ) {
			self::log( 'Auto-activated a subscription for ' . $filename );
			/**
			 * Fires when the Helper activates a product successfully.
			 *
			 * @param int    $product_id Product ID being activated.
			 * @param string $product_key Subscription product key.
			 * @param array  $activation_response The response object from wp_safe_remote_request().
			 */
			do_action( 'woocommerce_helper_subscription_activate_success', $product_id, $product_key, $activation_response );
		} else {
			self::log( 'Could not activate a subscription upon plugin activation: ' . $filename );

			/**
			 * Fires when the Helper fails to activate a product.
			 *
			 * @param int    $product_id Product ID being activated.
			 * @param string $product_key Subscription product key.
			 * @param array  $activation_response The response object from wp_safe_remote_request().
			 */
			do_action( 'woocommerce_helper_subscription_activate_error', $product_id, $product_key, $activation_response );
		}

		self::_flush_subscriptions_cache();
		self::_flush_updates_cache();
	}

	/**
	 * Runs when any plugin is deactivated.
	 *
	 * When a user deactivates a plugin, attempt to deactivate any subscriptions
	 * associated with the extension.
	 *
	 * @param string $filename The filename of the deactivated plugin.
	 */
	public static function deactivated_plugin( $filename ) {
		$plugins = self::get_local_woo_plugins();

		// Not a local woo plugin.
		if ( empty( $plugins[ $filename ] ) ) {
			return;
		}

		// Make sure we have a connection.
		$auth = WC_Helper_Options::get( 'auth' );
		if ( empty( $auth ) ) {
			return;
		}

		$plugin        = $plugins[ $filename ];
		$product_id    = $plugin['_product_id'];
		$subscriptions = self::_get_subscriptions_from_product_id( $product_id, false );
		$site_id       = absint( $auth['site_id'] );

		// No valid subscriptions for this product.
		if ( empty( $subscriptions ) ) {
			return;
		}

		$deactivated = 0;

		foreach ( $subscriptions as $subscription ) {
			// Don't touch subscriptions that aren't activated on this site.
			if ( ! in_array( $site_id, $subscription['connections'], true ) ) {
				continue;
			}

			$product_key           = $subscription['product_key'];
			$deactivation_response = WC_Helper_API::post(
				'deactivate',
				array(
					'authenticated' => true,
					'body'          => wp_json_encode(
						array(
							'product_key' => $product_key,
						)
					),
				)
			);

			if ( wp_remote_retrieve_response_code( $deactivation_response ) === 200 ) {
				$deactivated++;

				/**
				 * Fires when the Helper activates a product successfully.
				 *
				 * @param int    $product_id Product ID being deactivated.
				 * @param string $product_key Subscription product key.
				 * @param array  $deactivation_response The response object from wp_safe_remote_request().
				 */
				do_action( 'woocommerce_helper_subscription_deactivate_success', $product_id, $product_key, $deactivation_response );
			} else {
				/**
				 * Fires when the Helper fails to activate a product.
				 *
				 * @param int    $product_id Product ID being deactivated.
				 * @param string $product_key Subscription product key.
				 * @param array  $deactivation_response The response object from wp_safe_remote_request().
				 */
				do_action( 'woocommerce_helper_subscription_deactivate_error', $product_id, $product_key, $deactivation_response );
			}
		}

		if ( $deactivated ) {
			self::log( sprintf( 'Auto-deactivated %d subscription(s) for %s', $deactivated, $filename ) );
			self::_flush_subscriptions_cache();
			self::_flush_updates_cache();
		}
	}

	/**
	 * Various Helper-related admin notices.
	 */
	public static function admin_notices() {
		if ( apply_filters( 'woocommerce_helper_suppress_admin_notices', false ) ) {
			return;
		}

		$screen    = get_current_screen();
		$screen_id = $screen ? $screen->id : '';

		if ( 'update-core' !== $screen_id ) {
			return;
		}

		// Don't nag if Woo doesn't have an update available.
		if ( ! self::_woo_core_update_available() ) {
			return;
		}

		// Add a note about available extension updates if Woo core has an update available.
		$notice = self::_get_extensions_update_notice();
		if ( ! empty( $notice ) ) {
			echo '<div class="updated woocommerce-message"><p>' . $notice . '</p></div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		}
	}

	/**
	 * Get an update notice if one or more Woo extensions has an update available.
	 *
	 * @return string|null The update notice or null if everything is up to date.
	 */
	private static function _get_extensions_update_notice() {
		$plugins   = self::get_local_woo_plugins();
		$updates   = WC_Helper_Updater::get_update_data();
		$available = 0;

		foreach ( $plugins as $data ) {
			if ( empty( $updates[ $data['_product_id'] ] ) ) {
				continue;
			}

			$product_id = $data['_product_id'];
			if ( version_compare( $updates[ $product_id ]['version'], $data['Version'], '>' ) ) {
				$available++;
			}
		}

		if ( ! $available ) {
			return;
		}

		return sprintf(
			/* translators: %1$s: helper url, %2$d: number of extensions */
			_n( 'Note: You currently have <a href="%1$s">%2$d paid extension</a> which should be updated first before updating WooCommerce.', 'Note: You currently have <a href="%1$s">%2$d paid extensions</a> which should be updated first before updating WooCommerce.', $available, 'woocommerce' ),
			admin_url( 'admin.php?page=wc-addons&section=helper' ),
			$available
		);
	}

	/**
	 * Whether WooCommerce has an update available.
	 *
	 * @return bool True if a Woo core update is available.
	 */
	private static function _woo_core_update_available() {
		$updates = get_site_transient( 'update_plugins' );
		if ( empty( $updates->response ) ) {
			return false;
		}

		if ( empty( $updates->response['woocommerce/woocommerce.php'] ) ) {
			return false;
		}

		$data = $updates->response['woocommerce/woocommerce.php'];
		if ( version_compare( Constants::get_constant( 'WC_VERSION' ), $data->new_version, '>=' ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Flush subscriptions cache.
	 */
	public static function _flush_subscriptions_cache() {
		delete_transient( '_woocommerce_helper_subscriptions' );
	}

	/**
	 * Flush auth cache.
	 */
	public static function _flush_authentication_cache() {
		$request = WC_Helper_API::get(
			'oauth/me',
			array(
				'authenticated' => true,
			)
		);

		if ( wp_remote_retrieve_response_code( $request ) !== 200 ) {
			return false;
		}

		$user_data = json_decode( wp_remote_retrieve_body( $request ), true );
		if ( ! $user_data ) {
			return false;
		}

		WC_Helper_Options::update(
			'auth_user_data',
			array(
				'name'  => $user_data['name'],
				'email' => $user_data['email'],
			)
		);

		return true;
	}

	/**
	 * Flush updates cache.
	 */
	private static function _flush_updates_cache() {
		WC_Helper_Updater::flush_updates_cache();
	}

	/**
	 * Sort subscriptions by the product_name.
	 *
	 * @param array $a Subscription array.
	 * @param array $b Subscription array.
	 *
	 * @return int
	 */
	public static function _sort_by_product_name( $a, $b ) {
		return strcmp( $a['product_name'], $b['product_name'] );
	}

	/**
	 * Sort subscriptions by the Name.
	 *
	 * @param array $a Product array.
	 * @param array $b Product array.
	 *
	 * @return int
	 */
	public static function _sort_by_name( $a, $b ) {
		return strcmp( $a['Name'], $b['Name'] );
	}

	/**
	 * Log a helper event.
	 *
	 * @param string $message Log message.
	 * @param string $level Optional, defaults to info, valid levels: emergency|alert|critical|error|warning|notice|info|debug.
	 */
	public static function log( $message, $level = 'info' ) {
		if ( ! Constants::is_true( 'WP_DEBUG' ) ) {
			return;
		}

		if ( ! isset( self::$log ) ) {
			self::$log = wc_get_logger();
		}

		self::$log->log( $level, $message, array( 'source' => 'helper' ) );
	}
}

WC_Helper::load();
admin/helper/class-wc-helper-updater.php000064400000035251151336044770014277 0ustar00<?php
/**
 * The update helper for WooCommerce.com plugins.
 *
 * @class WC_Helper_Updater
 * @package WooCommerce\Admin\Helper
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC_Helper_Updater Class
 *
 * Contains the logic to fetch available updates and hook into Core's update
 * routines to serve WooCommerce.com-provided packages.
 */
class WC_Helper_Updater {

	/**
	 * Loads the class, runs on init.
	 */
	public static function load() {
		add_action( 'pre_set_site_transient_update_plugins', array( __CLASS__, 'transient_update_plugins' ), 21, 1 );
		add_action( 'pre_set_site_transient_update_themes', array( __CLASS__, 'transient_update_themes' ), 21, 1 );
		add_action( 'upgrader_process_complete', array( __CLASS__, 'upgrader_process_complete' ) );
		add_action( 'upgrader_pre_download', array( __CLASS__, 'block_expired_updates' ), 10, 2 );
	}

	/**
	 * Runs in a cron thread, or in a visitor thread if triggered
	 * by _maybe_update_plugins(), or in an auto-update thread.
	 *
	 * @param object $transient The update_plugins transient object.
	 *
	 * @return object The same or a modified version of the transient.
	 */
	public static function transient_update_plugins( $transient ) {
		$update_data = self::get_update_data();

		foreach ( WC_Helper::get_local_woo_plugins() as $plugin ) {
			if ( empty( $update_data[ $plugin['_product_id'] ] ) ) {
				continue;
			}

			$data     = $update_data[ $plugin['_product_id'] ];
			$filename = $plugin['_filename'];

			$item = array(
				'id'             => 'woocommerce-com-' . $plugin['_product_id'],
				'slug'           => 'woocommerce-com-' . $data['slug'],
				'plugin'         => $filename,
				'new_version'    => $data['version'],
				'url'            => $data['url'],
				'package'        => $data['package'],
				'upgrade_notice' => $data['upgrade_notice'],
			);

			if ( isset( $data['requires_php'] ) ) {
				$item['requires_php'] = $data['requires_php'];
			}

			// We don't want to deliver a valid upgrade package when their subscription has expired.
			// To avoid the generic "no_package" error that empty strings give, we will store an
			// indication of expiration for the `upgrader_pre_download` filter to error on.
			if ( ! self::_has_active_subscription( $plugin['_product_id'] ) ) {
				$item['package'] = 'woocommerce-com-expired-' . $plugin['_product_id'];
			}

			if ( version_compare( $plugin['Version'], $data['version'], '<' ) ) {
				$transient->response[ $filename ] = (object) $item;
				unset( $transient->no_update[ $filename ] );
			} else {
				$transient->no_update[ $filename ] = (object) $item;
				unset( $transient->response[ $filename ] );
			}
		}

		$translations = self::get_translations_update_data();
		$transient->translations = array_merge( isset( $transient->translations ) ? $transient->translations : array(), $translations );

		return $transient;
	}

	/**
	 * Runs on pre_set_site_transient_update_themes, provides custom
	 * packages for WooCommerce.com-hosted extensions.
	 *
	 * @param object $transient The update_themes transient object.
	 *
	 * @return object The same or a modified version of the transient.
	 */
	public static function transient_update_themes( $transient ) {
		$update_data = self::get_update_data();

		foreach ( WC_Helper::get_local_woo_themes() as $theme ) {
			if ( empty( $update_data[ $theme['_product_id'] ] ) ) {
				continue;
			}

			$data = $update_data[ $theme['_product_id'] ];
			$slug = $theme['_stylesheet'];

			$item = array(
				'theme'       => $slug,
				'new_version' => $data['version'],
				'url'         => $data['url'],
				'package'     => '',
			);

			if ( self::_has_active_subscription( $theme['_product_id'] ) ) {
				$item['package'] = $data['package'];
			}

			if ( version_compare( $theme['Version'], $data['version'], '<' ) ) {
				$transient->response[ $slug ] = $item;
			} else {
				unset( $transient->response[ $slug ] );
				$transient->checked[ $slug ] = $data['version'];
			}
		}

		return $transient;
	}

	/**
	 * Get update data for all extensions.
	 *
	 * Scans through all subscriptions for the connected user, as well
	 * as all Woo extensions without a subscription, and obtains update
	 * data for each product.
	 *
	 * @return array Update data {product_id => data}
	 */
	public static function get_update_data() {
		$payload = array();

		// Scan subscriptions.
		foreach ( WC_Helper::get_subscriptions() as $subscription ) {
			$payload[ $subscription['product_id'] ] = array(
				'product_id' => $subscription['product_id'],
				'file_id'    => '',
			);
		}

		// Scan local plugins which may or may not have a subscription.
		foreach ( WC_Helper::get_local_woo_plugins() as $data ) {
			if ( ! isset( $payload[ $data['_product_id'] ] ) ) {
				$payload[ $data['_product_id'] ] = array(
					'product_id' => $data['_product_id'],
				);
			}

			$payload[ $data['_product_id'] ]['file_id'] = $data['_file_id'];
		}

		// Scan local themes.
		foreach ( WC_Helper::get_local_woo_themes() as $data ) {
			if ( ! isset( $payload[ $data['_product_id'] ] ) ) {
				$payload[ $data['_product_id'] ] = array(
					'product_id' => $data['_product_id'],
				);
			}

			$payload[ $data['_product_id'] ]['file_id'] = $data['_file_id'];
		}

		return self::_update_check( $payload );
	}

	/**
	 * Get translations updates informations.
	 *
	 * Scans through all subscriptions for the connected user, as well
	 * as all Woo extensions without a subscription, and obtains update
	 * data for each product.
	 *
	 * @return array Update data {product_id => data}
	 */
	public static function get_translations_update_data() {
		$payload = array();

		$installed_translations = wp_get_installed_translations( 'plugins' );

		$locales = array_values( get_available_languages() );
		/**
		 * Filters the locales requested for plugin translations.
		 *
		 * @since 3.7.0
		 * @since 4.5.0 The default value of the `$locales` parameter changed to include all locales.
		 *
		 * @param array $locales Plugin locales. Default is all available locales of the site.
		 */
		$locales = apply_filters( 'plugins_update_check_locales', $locales );
		$locales = array_unique( $locales );

		// No locales, the respone will be empty, we can return now.
		if ( empty( $locales ) ) {
			return array();
		}

		// Scan local plugins which may or may not have a subscription.
		$plugins                 = WC_Helper::get_local_woo_plugins();
		$active_woo_plugins      = array_intersect( array_keys( $plugins ), get_option( 'active_plugins', array() ) );

		/*
		* Use only plugins that are subscribed to the automatic translations updates.
		*/
		$active_for_translations = array_filter(
			$active_woo_plugins,
			function( $plugin ) use ( $plugins ) {
				return apply_filters( 'woocommerce_translations_updates_for_' . $plugins[ $plugin ]['slug'], false );
			}
		);

		// Nothing to check for, exit.
		if ( empty( $active_for_translations ) ) {
			return array();
		}

		if ( wp_doing_cron() ) {
			$timeout = 30;
		} else {
			// Three seconds, plus one extra second for every 10 plugins.
			$timeout = 3 + (int) ( count( $active_for_translations ) / 10 );
		}

		$request_body = array(
			'locales' => $locales,
			'plugins' => array(),
		);

		foreach ( $active_for_translations as $active_plugin ) {
			$plugin = $plugins[ $active_plugin ];
			$request_body['plugins'][ $plugin['slug'] ] = array( 'version' => $plugin['Version'] );
		}

		$raw_response = wp_remote_post(
			'https://translate.wordpress.com/api/translations-updates/woocommerce',
			array(
				'body'        => json_encode( $request_body ),
				'headers'     => array( 'Content-Type: application/json' ),
				'timeout'     => $timeout,
			)
		);

		// Something wrong happened on the translate server side.
		$response_code = wp_remote_retrieve_response_code( $raw_response );
		if ( 200 !== $response_code ) {
			return array();
		}

		$response = json_decode( wp_remote_retrieve_body( $raw_response ), true );

		// API error, api returned but something was wrong.
		if ( array_key_exists( 'success', $response ) && false === $response['success'] ) {
			return array();
		}

		$translations = array();

		foreach ( $response['data'] as $plugin_name => $language_packs ) {
			foreach ( $language_packs as $language_pack ) {
				// Maybe we have this language pack already installed so lets check revision date.
				if ( array_key_exists( $plugin_name, $installed_translations ) && array_key_exists( $language_pack['wp_locale'], $installed_translations[ $plugin_name ] ) ) {
					$installed_translation_revision_time = new DateTime( $installed_translations[ $plugin_name ][ $language_pack['wp_locale'] ]['PO-Revision-Date'] );
					$new_translation_revision_time       = new DateTime( $language_pack['last_modified'] );
					// Skip if translation language pack is not newer than what is installed already.
					if ( $new_translation_revision_time <= $installed_translation_revision_time ) {
						continue;
					}
				}
				$translations[] = array(
					'type'       => 'plugin',
					'slug'       => $plugin_name,
					'language'   => $language_pack['wp_locale'],
					'version'    => $language_pack['version'],
					'updated'    => $language_pack['last_modified'],
					'package'    => $language_pack['package'],
					'autoupdate' => true,
				);
			}
		}

		return $translations;
	}

	/**
	 * Run an update check API call.
	 *
	 * The call is cached based on the payload (product ids, file ids). If
	 * the payload changes, the cache is going to miss.
	 *
	 * @param array $payload Information about the plugin to update.
	 * @return array Update data for each requested product.
	 */
	private static function _update_check( $payload ) {
		ksort( $payload );
		$hash = md5( wp_json_encode( $payload ) );

		$cache_key = '_woocommerce_helper_updates';
		$data      = get_transient( $cache_key );
		if ( false !== $data ) {
			if ( hash_equals( $hash, $data['hash'] ) ) {
				return $data['products'];
			}
		}

		$data = array(
			'hash'     => $hash,
			'updated'  => time(),
			'products' => array(),
			'errors'   => array(),
		);

		$request = WC_Helper_API::post(
			'update-check',
			array(
				'body'          => wp_json_encode( array( 'products' => $payload ) ),
				'authenticated' => true,
			)
		);

		if ( wp_remote_retrieve_response_code( $request ) !== 200 ) {
			$data['errors'][] = 'http-error';
		} else {
			$data['products'] = json_decode( wp_remote_retrieve_body( $request ), true );
		}

		set_transient( $cache_key, $data, 12 * HOUR_IN_SECONDS );
		return $data['products'];
	}

	/**
	 * Check for an active subscription.
	 *
	 * Checks a given product id against all subscriptions on
	 * the current site. Returns true if at least one active
	 * subscription is found.
	 *
	 * @param int $product_id The product id to look for.
	 *
	 * @return bool True if active subscription found.
	 */
	private static function _has_active_subscription( $product_id ) {
		if ( ! isset( $auth ) ) {
			$auth = WC_Helper_Options::get( 'auth' );
		}

		if ( ! isset( $subscriptions ) ) {
			$subscriptions = WC_Helper::get_subscriptions();
		}

		if ( empty( $auth['site_id'] ) || empty( $subscriptions ) ) {
			return false;
		}

		// Check for an active subscription.
		foreach ( $subscriptions as $subscription ) {
			if ( $subscription['product_id'] != $product_id ) {
				continue;
			}

			if ( in_array( absint( $auth['site_id'] ), $subscription['connections'] ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Get the number of products that have updates.
	 *
	 * @return int The number of products with updates.
	 */
	public static function get_updates_count() {
		$cache_key = '_woocommerce_helper_updates_count';
		$count     = get_transient( $cache_key );
		if ( false !== $count ) {
			return $count;
		}

		// Don't fetch any new data since this function in high-frequency.
		if ( ! get_transient( '_woocommerce_helper_subscriptions' ) ) {
			return 0;
		}

		if ( ! get_transient( '_woocommerce_helper_updates' ) ) {
			return 0;
		}

		$count       = 0;
		$update_data = self::get_update_data();

		if ( empty( $update_data ) ) {
			set_transient( $cache_key, $count, 12 * HOUR_IN_SECONDS );
			return $count;
		}

		// Scan local plugins.
		foreach ( WC_Helper::get_local_woo_plugins() as $plugin ) {
			if ( empty( $update_data[ $plugin['_product_id'] ] ) ) {
				continue;
			}

			if ( version_compare( $plugin['Version'], $update_data[ $plugin['_product_id'] ]['version'], '<' ) ) {
				$count++;
			}
		}

		// Scan local themes.
		foreach ( WC_Helper::get_local_woo_themes() as $theme ) {
			if ( empty( $update_data[ $theme['_product_id'] ] ) ) {
				continue;
			}

			if ( version_compare( $theme['Version'], $update_data[ $theme['_product_id'] ]['version'], '<' ) ) {
				$count++;
			}
		}

		set_transient( $cache_key, $count, 12 * HOUR_IN_SECONDS );
		return $count;
	}

	/**
	 * Return the updates count markup.
	 *
	 * @return string Updates count markup, empty string if no updates avairable.
	 */
	public static function get_updates_count_html() {
		$count = self::get_updates_count();
		if ( ! $count ) {
			return '';
		}

		$count_html = sprintf( '<span class="update-plugins count-%d"><span class="update-count">%d</span></span>', $count, number_format_i18n( $count ) );
		return $count_html;
	}

	/**
	 * Flushes cached update data.
	 */
	public static function flush_updates_cache() {
		delete_transient( '_woocommerce_helper_updates' );
		delete_transient( '_woocommerce_helper_updates_count' );
		delete_site_transient( 'update_plugins' );
		delete_site_transient( 'update_themes' );
	}

	/**
	 * Fires when a user successfully updated a theme or a plugin.
	 */
	public static function upgrader_process_complete() {
		delete_transient( '_woocommerce_helper_updates_count' );
	}

	/**
	 * Hooked into the upgrader_pre_download filter in order to better handle error messaging around expired
	 * plugin updates. Initially we were using an empty string, but the error message that no_package
	 * results in does not fit the cause.
	 *
	 * @since 4.1.0
	 * @param bool   $reply Holds the current filtered response.
	 * @param string $package The path to the package file for the update.
	 * @return false|WP_Error False to proceed with the update as normal, anything else to be returned instead of updating.
	 */
	public static function block_expired_updates( $reply, $package ) {
		// Don't override a reply that was set already.
		if ( false !== $reply ) {
			return $reply;
		}

		// Only for packages with expired subscriptions.
		if ( 0 !== strpos( $package, 'woocommerce-com-expired-' ) ) {
			return false;
		}

		return new WP_Error(
			'woocommerce_subscription_expired',
			sprintf(
				// translators: %s: URL of WooCommerce.com subscriptions tab.
				__( 'Please visit the <a href="%s" target="_blank">subscriptions page</a> and renew to continue receiving updates.', 'woocommerce' ),
				esc_url( admin_url( 'admin.php?page=wc-addons&section=helper' ) )
			)
		);
	}
}

WC_Helper_Updater::load();
admin/helper/views/html-section-account.php000064400000001667151336044770015045 0ustar00<?php defined( 'ABSPATH' ) or exit(); ?>

<a class="button button-update" href="<?php echo esc_url( $refresh_url ); ?>"><span class="dashicons dashicons-image-rotate"></span> <?php _e( 'Update', 'woocommerce' ); ?></a>
<div class="user-info">
	<header>
		<p><?php printf( __( 'Connected to WooCommerce.com', 'woocommerce' ) ); ?> <span class="chevron dashicons dashicons-arrow-down-alt2"></span></p>
	</header>
	<section>
		<p><?php echo get_avatar( $auth_user_data['email'], 48 ); ?> <?php echo esc_html( $auth_user_data['email'] ); ?></p>
		<div class="actions">
			<a class="" href="https://woocommerce.com/my-account/my-subscriptions/" target="_blank"><span class="dashicons dashicons-admin-generic"></span> <?php _e( 'My Subscriptions', 'woocommerce' ); ?></a>
			<a class="" href="<?php echo esc_url( $disconnect_url ); ?>"><span class="dashicons dashicons-no"></span> <?php _e( 'Disconnect', 'woocommerce' ); ?></a>
		</div>
	</section>
</div>
admin/helper/views/html-oauth-start.php000064400000002660151336044770014214 0ustar00<?php
/**
 * Admin -> WooCommerce -> Extensions -> WooCommerce.com Subscriptions main page.
 *
 * @package WooCommerce\Views
 */

defined( 'ABSPATH' ) || exit();

?>

<div class="wrap woocommerce wc-addons-wrap wc-helper">
	<h1 class="screen-reader-text"><?php esc_html_e( 'WooCommerce Extensions', 'woocommerce' ); ?></h1>
	<?php require WC_Helper::get_view_filename( 'html-section-notices.php' ); ?>

		<div class="start-container">
			<div class="text">
				<img src="<?php echo esc_url( WC()->plugin_url() . '/assets/images/woocommerce_logo.png' ); ?>" alt="<?php esc_attr_e( 'WooCommerce', 'woocommerce' ); ?>" style="width:180px;">

				<?php if ( ! empty( $_GET['wc-helper-status'] ) && 'helper-disconnected' === $_GET['wc-helper-status'] ) : ?>
					<p><strong><?php esc_html_e( 'Sorry to see you go.', 'woocommerce' ); ?></strong> <?php esc_html_e( 'Feel free to reconnect again using the button below.', 'woocommerce' ); ?></p>
				<?php endif; ?>

				<h2><?php esc_html_e( 'Manage your subscriptions, get important product notifications, and updates, all from the convenience of your WooCommerce dashboard', 'woocommerce' ); ?></h2>
				<p><?php esc_html_e( 'Once connected, your WooCommerce.com purchases will be listed here.', 'woocommerce' ); ?></p>
				<p><a class="button button-primary button-helper-connect" href="<?php echo esc_url( $connect_url ); ?>"><?php esc_html_e( 'Connect', 'woocommerce' ); ?></a></p>
			</div>
		</div>
</div>
admin/helper/views/html-main.php000064400000025727151336044770012676 0ustar00<?php
/**
 * Helper main view
 *
 * @package WooCommerce\Helper
 */

?>
<?php defined( 'ABSPATH' ) || exit(); ?>

<div class="wrap woocommerce wc-subscriptions-wrap wc-helper">
	<h1 class="screen-reader-text"><?php esc_html_e( 'My Subscriptions', 'woocommerce' ); ?></h1>

	<?php require WC_Helper::get_view_filename( 'html-section-notices.php' ); ?>

	<div class="subscriptions-header">
		<h2><?php esc_html_e( 'Subscriptions', 'woocommerce' ); ?></h2>
		<?php require WC_Helper::get_view_filename( 'html-section-account.php' ); ?>
		<p>
			<?php
			printf(
				wp_kses(
					/* translators: Introduction to list of WooCommerce.com extensions the merchant has subscriptions for. */
					__(
						'Below is a list of extensions available on your WooCommerce.com account. To receive extension updates please make sure the extension is installed, and its subscription activated and connected to your WooCommerce.com account. Extensions can be activated from the <a href="%s">Plugins</a> screen.',
						'woocommerce'
					),
					array(
						'a' => array(
							'href' => array(),
						),
					)
				),
				esc_url(
					admin_url( 'plugins.php' )
				)
			);
			?>
		</p>
	</div>

	<ul class="subscription-filter">
		<label><?php esc_html_e( 'Sort by:', 'woocommerce' ); ?> <span class="chevron dashicons dashicons-arrow-up-alt2"></span></label>
		<?php
			$filters        = array_keys( WC_Helper::get_filters() );
			$last_filter    = array_pop( $filters );
			$current_filter = WC_Helper::get_current_filter();
			$counts         = WC_Helper::get_filters_counts();
		?>

		<?php
		foreach ( WC_Helper::get_filters() as $key => $label ) :
			// Don't show empty filters.
			if ( empty( $counts[ $key ] ) ) {
				continue;
			}

			$url        = admin_url( 'admin.php?page=wc-addons&section=helper&filter=' . $key );
			$class_html = $current_filter === $key ? 'class="current"' : '';
			?>
			<li>
				<a <?php echo esc_html( $class_html ); ?> href="<?php echo esc_url( $url ); ?>">
					<?php echo esc_html( $label ); ?>
					<span class="count">(<?php echo absint( $counts[ $key ] ); ?>)</span>
				</a>
			</li>
		<?php endforeach; ?>
	</ul>

	<table class="wp-list-table widefat fixed striped">
		<?php if ( ! empty( $subscriptions ) ) : ?>
			<?php foreach ( $subscriptions as $subscription ) : ?>
				<tbody>
				<tr class="wp-list-table__row is-ext-header">
					<td class="wp-list-table__ext-details">
						<div class="wp-list-table__ext-title">
							<a href="<?php echo esc_url( $subscription['product_url'] ); ?>" target="_blank">
								<?php echo esc_html( $subscription['product_name'] ); ?>
							</a>
						</div>

						<div class="wp-list-table__ext-description">
							<?php if ( $subscription['lifetime'] ) : ?>
								<span class="renews">
									<?php esc_html_e( 'Lifetime Subscription', 'woocommerce' ); ?>
								</span>
							<?php elseif ( $subscription['expired'] ) : ?>
								<span class="renews">
									<strong><?php esc_html_e( 'Expired :(', 'woocommerce' ); ?></strong>
									<?php echo esc_html( date_i18n( 'F jS, Y', $subscription['expires'] ) ); ?>
								</span>
							<?php elseif ( $subscription['autorenew'] ) : ?>
								<span class="renews">
									<?php esc_html_e( 'Auto renews on:', 'woocommerce' ); ?>
									<?php echo esc_html( date_i18n( 'F jS, Y', $subscription['expires'] ) ); ?>
								</span>
							<?php elseif ( $subscription['expiring'] ) : ?>
								<span class="renews">
									<strong><?php esc_html_e( 'Expiring soon!', 'woocommerce' ); ?></strong>
									<?php echo esc_html( date_i18n( 'F jS, Y', $subscription['expires'] ) ); ?>
								</span>
							<?php else : ?>
								<span class="renews">
									<?php esc_html_e( 'Expires on:', 'woocommerce' ); ?>
									<?php echo esc_html( date_i18n( 'F jS, Y', $subscription['expires'] ) ); ?>
								</span>
							<?php endif; ?>

							<br/>
							<span class="subscription">
								<?php
								if ( ! $subscription['active'] && $subscription['maxed'] ) {
									/* translators: %1$d: sites active, %2$d max sites active */
									printf( esc_html__( 'Subscription: Not available - %1$d of %2$d already in use', 'woocommerce' ), absint( $subscription['sites_active'] ), absint( $subscription['sites_max'] ) );
								} elseif ( $subscription['sites_max'] > 0 ) {
									/* translators: %1$d: sites active, %2$d max sites active */
									printf( esc_html__( 'Subscription: Using %1$d of %2$d sites available', 'woocommerce' ), absint( $subscription['sites_active'] ), absint( $subscription['sites_max'] ) );
								} else {
									esc_html_e( 'Subscription: Unlimited', 'woocommerce' );
								}

								// Check shared.
								if ( ! empty( $subscription['is_shared'] ) && ! empty( $subscription['owner_email'] ) ) {
									/* translators: Email address of person who shared the subscription. */
									printf( '</br>' . esc_html__( 'Shared by %s', 'woocommerce' ), esc_html( $subscription['owner_email'] ) );
								} elseif ( isset( $subscription['master_user_email'] ) ) {
									/* translators: Email address of person who shared the subscription. */
									printf( '</br>' . esc_html__( 'Shared by %s', 'woocommerce' ), esc_html( $subscription['master_user_email'] ) );
								}
								?>
							</span>
						</div>
					</td>
					<td class="wp-list-table__ext-actions">
						<?php if ( ! $subscription['active'] && $subscription['maxed'] ) : ?>
							<a class="button" href="https://woocommerce.com/my-account/my-subscriptions/" target="_blank"><?php esc_html_e( 'Upgrade', 'woocommerce' ); ?></a>
						<?php elseif ( ! $subscription['local']['installed'] && ! $subscription['expired'] ) : ?>
							<a class="button <?php echo empty( $subscription['download_primary'] ) ? 'button-secondary' : ''; ?>" href="<?php echo esc_url( $subscription['download_url'] ); ?>" target="_blank"><?php esc_html_e( 'Download', 'woocommerce' ); ?></a>
						<?php elseif ( $subscription['active'] ) : ?>
							<span class="form-toggle__wrapper">
								<a href="<?php echo esc_url( $subscription['deactivate_url'] ); ?>" class="form-toggle active is-compact" role="link" aria-checked="true"><?php esc_html_e( 'Active', 'woocommerce' ); ?></a>
								<label class="form-toggle__label" for="activate-extension">
									<span class="form-toggle__label-content">
										<label for="activate-extension"><?php esc_html_e( 'Active', 'woocommerce' ); ?></label>
									</span>
									<span class="form-toggle__switch"></span>
								</label>
							</span>
						<?php elseif ( ! $subscription['expired'] ) : ?>
							<span class="form-toggle__wrapper">
								<a href="<?php echo esc_url( $subscription['activate_url'] ); ?>" class="form-toggle is-compact" role="link" aria-checked="false"><?php esc_html_e( 'Inactive', 'woocommerce' ); ?></a>
								<label class="form-toggle__label" for="activate-extension">
									<span class="form-toggle__label-content">
										<label for="activate-extension"><?php esc_html_e( 'Inactive', 'woocommerce' ); ?></label>
									</span>
									<span class="form-toggle__switch"></span>
								</label>
							</span>
						<?php else : ?>
							<span class="form-toggle__wrapper">
								<span class="form-toggle disabled is-compact"><?php esc_html_e( 'Inactive', 'woocommerce' ); ?></span>
								<label class="form-toggle__label" for="activate-extension">
									<span class="form-toggle__label-content">
										<label for="activate-extension"><?php esc_html_e( 'Inactive', 'woocommerce' ); ?></label>
									</span>
								</label>
							</span>
						<?php endif; ?>
					</td>
				</tr>

				<?php foreach ( $subscription['actions'] as $subscription_action ) : ?>
				<tr class="wp-list-table__row wp-list-table__ext-updates">
					<td class="wp-list-table__ext-status <?php echo sanitize_html_class( $subscription_action['status'] ); ?>">
						<p><span class="dashicons <?php echo sanitize_html_class( $subscription_action['icon'] ); ?>"></span>
							<?php echo wp_kses_post( $subscription_action['message'] ); ?>
						</p>
					</td>
					<td class="wp-list-table__ext-actions">
						<?php if ( ! empty( $subscription_action['button_label'] ) && ! empty( $subscription_action['button_url'] ) ) : ?>
						<a class="button <?php echo empty( $subscription_action['primary'] ) ? 'button-secondary' : ''; ?>" href="<?php echo esc_url( $subscription_action['button_url'] ); ?>"><?php echo esc_html( $subscription_action['button_label'] ); ?></a>
						<?php endif; ?>
					</td>
				</tr>
				<?php endforeach; ?>

				</tbody>
			<?php endforeach; ?>
		<?php else : ?>
			<tr>
				<td colspan="3"><em><?php esc_html_e( 'Could not find any subscriptions on your WooCommerce.com account', 'woocommerce' ); ?></td>
			</tr>
		<?php endif; ?>
		</tbody>
	</table>

	<?php if ( ! empty( $no_subscriptions ) ) : ?>
		<h2><?php esc_html_e( 'Installed Extensions without a Subscription', 'woocommerce' ); ?></h2>
		<p>Below is a list of WooCommerce.com products available on your site - but are either out-dated or do not have a valid subscription.</p>

		<table class="wp-list-table widefat fixed striped">
			<?php /* Extensions without a subscription. */ ?>
			<?php foreach ( $no_subscriptions as $filename => $data ) : ?>
				<tbody>
					<tr class="wp-list-table__row is-ext-header">
						<td class="wp-list-table__ext-details color-bar autorenews">
							<div class="wp-list-table__ext-title">
								<a href="<?php echo esc_url( $data['_product_url'] ); ?>" target="_blank"><?php echo esc_html( $data['Name'] ); ?></a>
							</div>
							<div class="wp-list-table__ext-description">
							</div>
						</td>
						<td class="wp-list-table__ext-actions">
							<span class="form-toggle__wrapper">
								<span class="form-toggle disabled is-compact" ><?php esc_html_e( 'Inactive', 'woocommerce' ); ?></span>
								<label class="form-toggle__label" for="activate-extension">
									<span class="form-toggle__label-content">
										<label for="activate-extension"><?php esc_html_e( 'Inactive', 'woocommerce' ); ?></label>
									</span>
								</label>
							</span>
						</td>
					</tr>

					<?php foreach ( $data['_actions'] as $subscription_action ) : ?>
					<tr class="wp-list-table__row wp-list-table__ext-updates">
						<td class="wp-list-table__ext-status <?php echo sanitize_html_class( $subscription_action['status'] ); ?>">
							<p><span class="dashicons <?php echo sanitize_html_class( $subscription_action['icon'] ); ?>"></span>
								<?php
									echo wp_kses(
										$subscription_action['message'],
										array(
											'a'      => array(
												'href'  => array(),
												'title' => array(),
											),
											'br'     => array(),
											'em'     => array(),
											'strong' => array(),
										)
									);
								?>
							</p>
						</td>
						<td class="wp-list-table__ext-actions">
							<a class="button" href="<?php echo esc_url( $subscription_action['button_url'] ); ?>" target="_blank"><?php echo esc_html( $subscription_action['button_label'] ); ?></a>
						</td>
					</tr>
					<?php endforeach; ?>

				</tbody>

			<?php endforeach; ?>
		</table>
	<?php endif; ?>
</div>
admin/helper/views/html-section-nav.php000064400000001371151336044770014165 0ustar00<?php
/**
 * Helper admin navigation.
 *
 * @package WooCommerce\Helper
 *
 * @deprecated 5.7.0
 */

defined( 'ABSPATH' ) || exit(); ?>

<nav class="nav-tab-wrapper woo-nav-tab-wrapper">
	<a href="<?php echo esc_url( admin_url( 'admin.php?page=wc-addons' ) ); ?>" class="nav-tab"><?php esc_html_e( 'Browse Extensions', 'woocommerce' ); ?></a>

	<?php
		$count_html = WC_Helper_Updater::get_updates_count_html();
		/* translators: %s: WooCommerce.com Subscriptions tab count HTML. */
		$menu_title = sprintf( __( 'WooCommerce.com Subscriptions %s', 'woocommerce' ), $count_html );
	?>
	<a href="<?php echo esc_url( admin_url( 'admin.php?page=wc-addons&section=helper' ) ); ?>" class="nav-tab nav-tab-active"><?php echo wp_kses_post( $menu_title ); ?></a>
</nav>
admin/helper/views/html-helper-compat.php000064400000000726151336044770014502 0ustar00<?php defined( 'ABSPATH' ) or exit(); ?>

<div class="wrap">
	<h1><?php _e( 'Looking for the WooCommerce Helper?', 'woocommerce' ); ?></h1>
	<p><?php printf( __( 'We\'ve made things simpler and easier to manage moving forward. From now on you can manage all your WooCommerce purchases directly from the Extensions menu within the WooCommerce plugin itself. <a href="%s">View and manage</a> your extensions now.', 'woocommerce' ), esc_url( $helper_url ) ); ?></p>
</div>
admin/helper/views/html-section-notices.php000064400000000355151336044770015046 0ustar00<?php defined( 'ABSPATH' ) or exit(); ?>

<?php foreach ( $notices as $notice ) : ?>
	<div class="notice <?php echo sanitize_html_class( $notice['type'] ); ?>">
		<?php echo wpautop( $notice['message'] ); ?>
	</div>
<?php endforeach; ?>
admin/settings/class-wc-settings-page.php000064400000016560151336044770014513 0ustar00<?php
/**
 * WooCommerce Settings Page/Tab
 *
 * @package     WooCommerce\Admin
 * @version     2.1.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

if ( ! class_exists( 'WC_Settings_Page', false ) ) :

	/**
	 * WC_Settings_Page.
	 */
	abstract class WC_Settings_Page {

		/**
		 * Setting page id.
		 *
		 * @var string
		 */
		protected $id = '';

		/**
		 * Setting page label.
		 *
		 * @var string
		 */
		protected $label = '';

		/**
		 * Constructor.
		 */
		public function __construct() {
			add_filter( 'woocommerce_settings_tabs_array', array( $this, 'add_settings_page' ), 20 );
			add_action( 'woocommerce_sections_' . $this->id, array( $this, 'output_sections' ) );
			add_action( 'woocommerce_settings_' . $this->id, array( $this, 'output' ) );
			add_action( 'woocommerce_settings_save_' . $this->id, array( $this, 'save' ) );
		}

		/**
		 * Get settings page ID.
		 *
		 * @since 3.0.0
		 * @return string
		 */
		public function get_id() {
			return $this->id;
		}

		/**
		 * Get settings page label.
		 *
		 * @since 3.0.0
		 * @return string
		 */
		public function get_label() {
			return $this->label;
		}

		/**
		 * Add this page to settings.
		 *
		 * @param array $pages The setings array where we'll add ourselves.
		 *
		 * @return mixed
		 */
		public function add_settings_page( $pages ) {
			$pages[ $this->id ] = $this->label;

			return $pages;
		}

		/**
		 * Get settings array for the default section.
		 *
		 * External settings classes (registered via 'woocommerce_get_settings_pages' filter)
		 * might have redefined this method as "get_settings($section_id='')", thus we need
		 * to use this method internally instead of 'get_settings_for_section' to register settings
		 * and render settings pages.
		 *
		 * *But* we can't just redefine the method as "get_settings($section_id='')" here, since this
		 * will break on PHP 8 if any external setting class have it as 'get_settings()'.
		 *
		 * Thus we leave the method signature as is and use 'func_get_arg' to get the setting id
		 * if it's supplied, and we use this method internally; but it's deprecated and should
		 * otherwise never be used.
		 *
		 * @deprecated 5.4.0 Use 'get_settings_for_section' (passing an empty string for default section)
		 *
		 * @return array Settings array, each item being an associative array representing a setting.
		 */
		public function get_settings() {
			$section_id = 0 === func_num_args() ? '' : func_get_arg( 0 );
			return $this->get_settings_for_section( $section_id );
		}

		/**
		 * Get settings array.
		 *
		 * The strategy for getting the settings is as follows:
		 *
		 * - If a method named 'get_settings_for_{section_id}_section' exists in the class
		 *   it will be invoked (for the default '' section, the method name is 'get_settings_for_default_section').
		 *   Derived classes can implement these methods as required.
		 *
		 * - Otherwise, 'get_settings_for_section_core' will be invoked. Derived classes can override it
		 *   as an alternative to implementing 'get_settings_for_{section_id}_section' methods.
		 *
		 * @param string $section_id The id of the section to return settings for, an empty string for the default section.
		 *
		 * @return array Settings array, each item being an associative array representing a setting.
		 */
		final public function get_settings_for_section( $section_id ) {
			if ( '' === $section_id ) {
				$method_name = 'get_settings_for_default_section';
			} else {
				$method_name = "get_settings_for_{$section_id}_section";
			}

			if ( method_exists( $this, $method_name ) ) {
				$settings = $this->$method_name();
			} else {
				$settings = $this->get_settings_for_section_core( $section_id );
			}

			return apply_filters( 'woocommerce_get_settings_' . $this->id, $settings, $section_id );
		}

		/**
		 * Get the settings for a given section.
		 * This method is invoked from 'get_settings_for_section' when no 'get_settings_for_{current_section}_section'
		 * method exists in the class.
		 *
		 * When overriding, note that the 'woocommerce_get_settings_' filter must NOT be triggered,
		 * as this is already done by 'get_settings_for_section'.
		 *
		 * @param string $section_id The section name to get the settings for.
		 *
		 * @return array Settings array, each item being an associative array representing a setting.
		 */
		protected function get_settings_for_section_core( $section_id ) {
			return array();
		}

		/**
		 * Get all sections for this page, both the own ones and the ones defined via filters.
		 *
		 * @return array
		 */
		public function get_sections() {
			$sections = $this->get_own_sections();
			return apply_filters( 'woocommerce_get_sections_' . $this->id, $sections );
		}

		/**
		 * Get own sections for this page.
		 * Derived classes should override this method if they define sections.
		 * There should always be one default section with an empty string as identifier.
		 *
		 * Example:
		 * return array(
		 *   ''        => __( 'General', 'woocommerce' ),
		 *   'foobars' => __( 'Foos & Bars', 'woocommerce' ),
		 * );
		 *
		 * @return array An associative array where keys are section identifiers and the values are translated section names.
		 */
		protected function get_own_sections() {
			return array( '' => __( 'General', 'woocommerce' ) );
		}

		/**
		 * Output sections.
		 */
		public function output_sections() {
			global $current_section;

			$sections = $this->get_sections();

			if ( empty( $sections ) || 1 === count( $sections ) ) {
				return;
			}

			echo '<ul class="subsubsub">';

			$array_keys = array_keys( $sections );

			foreach ( $sections as $id => $label ) {
				$url       = admin_url( 'admin.php?page=wc-settings&tab=' . $this->id . '&section=' . sanitize_title( $id ) );
				$class     = ( $current_section === $id ? 'current' : '' );
				$separator = ( end( $array_keys ) === $id ? '' : '|' );
				$text      = esc_html( $label );
				echo "<li><a href='$url' class='$class'>$text</a> $separator </li>"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			}

			echo '</ul><br class="clear" />';
		}

		/**
		 * Output the HTML for the settings.
		 */
		public function output() {
			global $current_section;

			// We can't use "get_settings_for_section" here
			// for compatibility with derived classes overriding "get_settings".
			$settings = $this->get_settings( $current_section );

			WC_Admin_Settings::output_fields( $settings );
		}

		/**
		 * Save settings and trigger the 'woocommerce_update_options_'.id action.
		 */
		public function save() {
			$this->save_settings_for_current_section();
			$this->do_update_options_action();
		}

		/**
		 * Save settings for current section.
		 */
		protected function save_settings_for_current_section() {
			global $current_section;

			// We can't use "get_settings_for_section" here
			// for compatibility with derived classes overriding "get_settings".
			$settings = $this->get_settings( $current_section );
			WC_Admin_Settings::save_fields( $settings );
		}

		/**
		 * Trigger the 'woocommerce_update_options_'.id action.
		 *
		 * @param string $section_id Section to trigger the action for, or null for current section.
		 */
		protected function do_update_options_action( $section_id = null ) {
			global $current_section;

			if ( is_null( $section_id ) ) {
				$section_id = $current_section;
			}

			if ( $section_id ) {
				do_action( 'woocommerce_update_options_' . $this->id . '_' . $section_id );
			}
		}
	}

endif;
admin/settings/class-wc-settings-products.php000064400000037371151336044770015445 0ustar00<?php
/**
 * WooCommerce Product Settings
 *
 * @package WooCommerce\Admin
 * @version 2.4.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( class_exists( 'WC_Settings_Products', false ) ) {
	return new WC_Settings_Products();
}

/**
 * WC_Settings_Products.
 */
class WC_Settings_Products extends WC_Settings_Page {

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->id    = 'products';
		$this->label = __( 'Products', 'woocommerce' );

		parent::__construct();
	}

	/**
	 * Get own sections.
	 *
	 * @return array
	 */
	protected function get_own_sections() {
		return array(
			''             => __( 'General', 'woocommerce' ),
			'inventory'    => __( 'Inventory', 'woocommerce' ),
			'downloadable' => __( 'Downloadable products', 'woocommerce' ),
		);
	}

	/**
	 * Get settings for the detault section.
	 *
	 * @return array
	 */
	protected function get_settings_for_default_section() {
		$settings =
			array(
				array(
					'title' => __( 'Shop pages', 'woocommerce' ),
					'type'  => 'title',
					'desc'  => '',
					'id'    => 'catalog_options',
				),
				array(
					'title'    => __( 'Shop page', 'woocommerce' ),
					/* translators: %s: URL to settings. */
					'desc'     => sprintf( __( 'The base page can also be used in your <a href="%s">product permalinks</a>.', 'woocommerce' ), admin_url( 'options-permalink.php' ) ),
					'id'       => 'woocommerce_shop_page_id',
					'type'     => 'single_select_page',
					'default'  => '',
					'class'    => 'wc-enhanced-select-nostd',
					'css'      => 'min-width:300px;',
					'desc_tip' => __( 'This sets the base page of your shop - this is where your product archive will be.', 'woocommerce' ),
				),
				array(
					'title'         => __( 'Add to cart behaviour', 'woocommerce' ),
					'desc'          => __( 'Redirect to the cart page after successful addition', 'woocommerce' ),
					'id'            => 'woocommerce_cart_redirect_after_add',
					'default'       => 'no',
					'type'          => 'checkbox',
					'checkboxgroup' => 'start',
				),
				array(
					'desc'          => __( 'Enable AJAX add to cart buttons on archives', 'woocommerce' ),
					'id'            => 'woocommerce_enable_ajax_add_to_cart',
					'default'       => 'yes',
					'type'          => 'checkbox',
					'checkboxgroup' => 'end',
				),
				array(
					'title'       => __( 'Placeholder image', 'woocommerce' ),
					'id'          => 'woocommerce_placeholder_image',
					'type'        => 'text',
					'default'     => '',
					'class'       => '',
					'css'         => '',
					'placeholder' => __( 'Enter attachment ID or URL to an image', 'woocommerce' ),
					'desc_tip'    => __( 'This is the attachment ID, or image URL, used for placeholder images in the product catalog. Products with no image will use this.', 'woocommerce' ),
				),
				array(
					'type' => 'sectionend',
					'id'   => 'catalog_options',
				),

				array(
					'title' => __( 'Measurements', 'woocommerce' ),
					'type'  => 'title',
					'id'    => 'product_measurement_options',
				),

				array(
					'title'    => __( 'Weight unit', 'woocommerce' ),
					'desc'     => __( 'This controls what unit you will define weights in.', 'woocommerce' ),
					'id'       => 'woocommerce_weight_unit',
					'class'    => 'wc-enhanced-select',
					'css'      => 'min-width:300px;',
					'default'  => 'kg',
					'type'     => 'select',
					'options'  => array(
						'kg'  => __( 'kg', 'woocommerce' ),
						'g'   => __( 'g', 'woocommerce' ),
						'lbs' => __( 'lbs', 'woocommerce' ),
						'oz'  => __( 'oz', 'woocommerce' ),
					),
					'desc_tip' => true,
				),

				array(
					'title'    => __( 'Dimensions unit', 'woocommerce' ),
					'desc'     => __( 'This controls what unit you will define lengths in.', 'woocommerce' ),
					'id'       => 'woocommerce_dimension_unit',
					'class'    => 'wc-enhanced-select',
					'css'      => 'min-width:300px;',
					'default'  => 'cm',
					'type'     => 'select',
					'options'  => array(
						'm'  => __( 'm', 'woocommerce' ),
						'cm' => __( 'cm', 'woocommerce' ),
						'mm' => __( 'mm', 'woocommerce' ),
						'in' => __( 'in', 'woocommerce' ),
						'yd' => __( 'yd', 'woocommerce' ),
					),
					'desc_tip' => true,
				),

				array(
					'type' => 'sectionend',
					'id'   => 'product_measurement_options',
				),

				array(
					'title' => __( 'Reviews', 'woocommerce' ),
					'type'  => 'title',
					'desc'  => '',
					'id'    => 'product_rating_options',
				),

				array(
					'title'           => __( 'Enable reviews', 'woocommerce' ),
					'desc'            => __( 'Enable product reviews', 'woocommerce' ),
					'id'              => 'woocommerce_enable_reviews',
					'default'         => 'yes',
					'type'            => 'checkbox',
					'checkboxgroup'   => 'start',
					'show_if_checked' => 'option',
				),

				array(
					'desc'            => __( 'Show "verified owner" label on customer reviews', 'woocommerce' ),
					'id'              => 'woocommerce_review_rating_verification_label',
					'default'         => 'yes',
					'type'            => 'checkbox',
					'checkboxgroup'   => '',
					'show_if_checked' => 'yes',
					'autoload'        => false,
				),

				array(
					'desc'            => __( 'Reviews can only be left by "verified owners"', 'woocommerce' ),
					'id'              => 'woocommerce_review_rating_verification_required',
					'default'         => 'no',
					'type'            => 'checkbox',
					'checkboxgroup'   => 'end',
					'show_if_checked' => 'yes',
					'autoload'        => false,
				),

				array(
					'title'           => __( 'Product ratings', 'woocommerce' ),
					'desc'            => __( 'Enable star rating on reviews', 'woocommerce' ),
					'id'              => 'woocommerce_enable_review_rating',
					'default'         => 'yes',
					'type'            => 'checkbox',
					'checkboxgroup'   => 'start',
					'show_if_checked' => 'option',
				),

				array(
					'desc'            => __( 'Star ratings should be required, not optional', 'woocommerce' ),
					'id'              => 'woocommerce_review_rating_required',
					'default'         => 'yes',
					'type'            => 'checkbox',
					'checkboxgroup'   => 'end',
					'show_if_checked' => 'yes',
					'autoload'        => false,
				),

				array(
					'type' => 'sectionend',
					'id'   => 'product_rating_options',
				),
			);

		$settings = apply_filters( 'woocommerce_products_general_settings', $settings );
		return apply_filters( 'woocommerce_product_settings', $settings );
	}

	/**
	 * Get settings for the inventory section.
	 *
	 * @return array
	 */
	protected function get_settings_for_inventory_section() {
		$settings =
			array(
				array(
					'title' => __( 'Inventory', 'woocommerce' ),
					'type'  => 'title',
					'desc'  => '',
					'id'    => 'product_inventory_options',
				),

				array(
					'title'   => __( 'Manage stock', 'woocommerce' ),
					'desc'    => __( 'Enable stock management', 'woocommerce' ),
					'id'      => 'woocommerce_manage_stock',
					'default' => 'yes',
					'type'    => 'checkbox',
				),

				array(
					'title'             => __( 'Hold stock (minutes)', 'woocommerce' ),
					'desc'              => __( 'Hold stock (for unpaid orders) for x minutes. When this limit is reached, the pending order will be cancelled. Leave blank to disable.', 'woocommerce' ),
					'id'                => 'woocommerce_hold_stock_minutes',
					'type'              => 'number',
					'custom_attributes' => array(
						'min'  => 0,
						'step' => 1,
					),
					'css'               => 'width: 80px;',
					'default'           => '60',
					'autoload'          => false,
					'class'             => 'manage_stock_field',
				),

				array(
					'title'         => __( 'Notifications', 'woocommerce' ),
					'desc'          => __( 'Enable low stock notifications', 'woocommerce' ),
					'id'            => 'woocommerce_notify_low_stock',
					'default'       => 'yes',
					'type'          => 'checkbox',
					'checkboxgroup' => 'start',
					'autoload'      => false,
					'class'         => 'manage_stock_field',
				),

				array(
					'desc'          => __( 'Enable out of stock notifications', 'woocommerce' ),
					'id'            => 'woocommerce_notify_no_stock',
					'default'       => 'yes',
					'type'          => 'checkbox',
					'checkboxgroup' => 'end',
					'autoload'      => false,
					'class'         => 'manage_stock_field',
				),

				array(
					'title'    => __( 'Notification recipient(s)', 'woocommerce' ),
					'desc'     => __( 'Enter recipients (comma separated) that will receive this notification.', 'woocommerce' ),
					'id'       => 'woocommerce_stock_email_recipient',
					'type'     => 'text',
					'default'  => get_option( 'admin_email' ),
					'css'      => 'width: 250px;',
					'autoload' => false,
					'desc_tip' => true,
					'class'    => 'manage_stock_field',
				),

				array(
					'title'             => __( 'Low stock threshold', 'woocommerce' ),
					'desc'              => __( 'When product stock reaches this amount you will be notified via email.', 'woocommerce' ),
					'id'                => 'woocommerce_notify_low_stock_amount',
					'css'               => 'width:50px;',
					'type'              => 'number',
					'custom_attributes' => array(
						'min'  => 0,
						'step' => 1,
					),
					'default'           => '2',
					'autoload'          => false,
					'desc_tip'          => true,
					'class'             => 'manage_stock_field',
				),

				array(
					'title'             => __( 'Out of stock threshold', 'woocommerce' ),
					'desc'              => __( 'When product stock reaches this amount the stock status will change to "out of stock" and you will be notified via email. This setting does not affect existing "in stock" products.', 'woocommerce' ),
					'id'                => 'woocommerce_notify_no_stock_amount',
					'css'               => 'width:50px;',
					'type'              => 'number',
					'custom_attributes' => array(
						'min'  => 0,
						'step' => 1,
					),
					'default'           => '0',
					'desc_tip'          => true,
					'class'             => 'manage_stock_field',
				),

				array(
					'title'   => __( 'Out of stock visibility', 'woocommerce' ),
					'desc'    => __( 'Hide out of stock items from the catalog', 'woocommerce' ),
					'id'      => 'woocommerce_hide_out_of_stock_items',
					'default' => 'no',
					'type'    => 'checkbox',
				),

				array(
					'title'    => __( 'Stock display format', 'woocommerce' ),
					'desc'     => __( 'This controls how stock quantities are displayed on the frontend.', 'woocommerce' ),
					'id'       => 'woocommerce_stock_format',
					'css'      => 'min-width:150px;',
					'class'    => 'wc-enhanced-select',
					'default'  => '',
					'type'     => 'select',
					'options'  => array(
						''           => __( 'Always show quantity remaining in stock e.g. "12 in stock"', 'woocommerce' ),
						'low_amount' => __( 'Only show quantity remaining in stock when low e.g. "Only 2 left in stock"', 'woocommerce' ),
						'no_amount'  => __( 'Never show quantity remaining in stock', 'woocommerce' ),
					),
					'desc_tip' => true,
				),

				array(
					'type' => 'sectionend',
					'id'   => 'product_inventory_options',
				),
			);

		return apply_filters( 'woocommerce_inventory_settings', $settings );
	}

	/**
	 * Get settings for the downloadable section.
	 *
	 * @return array
	 */
	protected function get_settings_for_downloadable_section() {
		$settings =
			array(
				array(
					'title' => __( 'Downloadable products', 'woocommerce' ),
					'type'  => 'title',
					'id'    => 'digital_download_options',
				),

				array(
					'title'    => __( 'File download method', 'woocommerce' ),
					'desc_tip' => sprintf(
					/* translators: 1: X-Accel-Redirect 2: X-Sendfile 3: mod_xsendfile */
						__( 'Forcing downloads will keep URLs hidden, but some servers may serve large files unreliably. If supported, %1$s / %2$s can be used to serve downloads instead (server requires %3$s).', 'woocommerce' ),
						'<code>X-Accel-Redirect</code>',
						'<code>X-Sendfile</code>',
						'<code>mod_xsendfile</code>'
					),
					'id'       => 'woocommerce_file_download_method',
					'type'     => 'select',
					'class'    => 'wc-enhanced-select',
					'css'      => 'min-width:300px;',
					'default'  => 'force',
					'desc'     => sprintf(
					// translators: Link to WooCommerce Docs.
						__( "If you are using X-Accel-Redirect download method along with NGINX server, make sure that you have applied settings as described in <a href='%s'>Digital/Downloadable Product Handling</a> guide.", 'woocommerce' ),
						'https://docs.woocommerce.com/document/digital-downloadable-product-handling#nginx-setting'
					),
					'options'  => array(
						'force'     => __( 'Force downloads', 'woocommerce' ),
						'xsendfile' => __( 'X-Accel-Redirect/X-Sendfile', 'woocommerce' ),
						'redirect'  => apply_filters( 'woocommerce_redirect_only_method_is_secure', false ) ? __( 'Redirect only', 'woocommerce' ) : __( 'Redirect only (Insecure)', 'woocommerce' ),
					),
					'autoload' => false,
				),

				array(
					'desc'          => __( 'Allow using redirect mode (insecure) as a last resort', 'woocommerce' ),
					'id'            => 'woocommerce_downloads_redirect_fallback_allowed',
					'type'          => 'checkbox',
					'default'       => 'no',
					'desc_tip'      => sprintf(
						/* translators: %1$s is a link to the WooCommerce documentation. */
						__( 'If the "Force Downloads" or "X-Accel-Redirect/X-Sendfile" download method is selected but does not work, the system will use the "Redirect" method as a last resort. <a href="%1$s">See this guide</a> for more details.', 'woocommerce' ),
						'https://docs.woocommerce.com/document/digital-downloadable-product-handling/'
					),
					'checkboxgroup' => 'start',
					'autoload'      => false,
				),

				array(
					'title'         => __( 'Access restriction', 'woocommerce' ),
					'desc'          => __( 'Downloads require login', 'woocommerce' ),
					'id'            => 'woocommerce_downloads_require_login',
					'type'          => 'checkbox',
					'default'       => 'no',
					'desc_tip'      => __( 'This setting does not apply to guest purchases.', 'woocommerce' ),
					'checkboxgroup' => 'start',
					'autoload'      => false,
				),

				array(
					'desc'          => __( 'Grant access to downloadable products after payment', 'woocommerce' ),
					'id'            => 'woocommerce_downloads_grant_access_after_payment',
					'type'          => 'checkbox',
					'default'       => 'yes',
					'desc_tip'      => __( 'Enable this option to grant access to downloads when orders are "processing", rather than "completed".', 'woocommerce' ),
					'checkboxgroup' => 'end',
					'autoload'      => false,
				),

				array(
					'title'    => __( 'Filename', 'woocommerce' ),
					'desc'     => __( 'Append a unique string to filename for security', 'woocommerce' ),
					'id'       => 'woocommerce_downloads_add_hash_to_filename',
					'type'     => 'checkbox',
					'default'  => 'yes',
					'desc_tip' => sprintf(
					// translators: Link to WooCommerce Docs.
						__( "Not required if your download directory is protected. <a href='%s'>See this guide</a> for more details. Files already uploaded will not be affected.", 'woocommerce' ),
						'https://docs.woocommerce.com/document/digital-downloadable-product-handling#unique-string'
					),
				),

				array(
					'type' => 'sectionend',
					'id'   => 'digital_download_options',
				),
			);

		return apply_filters( 'woocommerce_downloadable_products_settings', $settings );
	}

	/**
	 * Save settings and trigger the 'woocommerce_update_options_'.id action.
	 */
	public function save() {
		$this->save_settings_for_current_section();

		/*
		 * Product->Inventory has a setting `Out of stock visibility`.
		 * Because of this, we need to recount the terms to keep them in-sync.
		 */
		WC()->call_function( 'wc_recount_all_terms' );

		$this->do_update_options_action();
	}
}

return new WC_Settings_Products();
admin/settings/class-wc-settings-tax.php000064400000024222151336044770014365 0ustar00<?php
/**
 * WooCommerce Tax Settings
 *
 * @package     WooCommerce\Admin
 * @version     2.1.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( class_exists( 'WC_Settings_Tax', false ) ) {
	return new WC_Settings_Tax();
}

/**
 * WC_Settings_Tax.
 */
class WC_Settings_Tax extends WC_Settings_Page {

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->id    = 'tax';
		$this->label = __( 'Tax', 'woocommerce' );

		add_filter( 'woocommerce_settings_tabs_array', array( $this, 'add_settings_page' ), 20 );

		if ( wc_tax_enabled() ) {
			add_action( 'woocommerce_sections_' . $this->id, array( $this, 'output_sections' ) );
			add_action( 'woocommerce_settings_' . $this->id, array( $this, 'output' ) );
			add_action( 'woocommerce_settings_save_' . $this->id, array( $this, 'save' ) );
		}
	}

	/**
	 * Add this page to settings.
	 *
	 * @param array $pages Existing pages.
	 * @return array|mixed
	 */
	public function add_settings_page( $pages ) {
		if ( wc_tax_enabled() ) {
			return parent::add_settings_page( $pages );
		} else {
			return $pages;
		}
	}

	/**
	 * Get own sections.
	 *
	 * @return array
	 */
	protected function get_own_sections() {
		$sections = array(
			''         => __( 'Tax options', 'woocommerce' ),
			'standard' => __( 'Standard rates', 'woocommerce' ),
		);

		// Get tax classes and display as links.
		$tax_classes = WC_Tax::get_tax_classes();

		foreach ( $tax_classes as $class ) {
			/* translators: $s tax rate section name */
			$sections[ sanitize_title( $class ) ] = sprintf( __( '%s rates', 'woocommerce' ), $class );
		}

		return $sections;
	}

	/**
	 * Get settings array.
	 *
	 * @return array
	 */
	public function get_settings_for_default_section() {
		return include __DIR__ . '/views/settings-tax.php';
	}

	/**
	 * Output the settings.
	 */
	public function output() {
		global $current_section;

		$tax_classes = WC_Tax::get_tax_class_slugs();

		if ( 'standard' === $current_section || in_array( $current_section, array_filter( $tax_classes ), true ) ) {
			$this->output_tax_rates();
		} else {
			parent::output();
		}
	}

	/**
	 * Save settings.
	 */
	public function save() {
		// phpcs:disable WordPress.Security.NonceVerification.Missing
		global $current_section;

		if ( ! $current_section ) {
			$this->save_settings_for_current_section();

			if ( isset( $_POST['woocommerce_tax_classes'] ) ) {
				$this->save_tax_classes( wp_unslash( $_POST['woocommerce_tax_classes'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			}
		} elseif ( ! empty( $_POST['tax_rate_country'] ) ) {
			$this->save_tax_rates();
		} else {
			$this->save_settings_for_current_section();
		}

		$this->do_update_options_action();

		// Invalidate caches.
		WC_Cache_Helper::invalidate_cache_group( 'taxes' );
		WC_Cache_Helper::get_transient_version( 'shipping', true );
		// phpcs:enable WordPress.Security.NonceVerification.Missing
	}

	/**
	 * Saves tax classes defined in the textarea to the tax class table instead of an option.
	 *
	 * @param string $raw_tax_classes Posted value.
	 * @return null
	 */
	public function save_tax_classes( $raw_tax_classes ) {
		$tax_classes          = array_filter( array_map( 'trim', explode( "\n", $raw_tax_classes ) ) );
		$existing_tax_classes = WC_Tax::get_tax_classes();
		$removed              = array_diff( $existing_tax_classes, $tax_classes );
		$added                = array_diff( $tax_classes, $existing_tax_classes );

		foreach ( $removed as $name ) {
			WC_Tax::delete_tax_class_by( 'name', $name );
		}

		foreach ( $added as $name ) {
			$tax_class = WC_Tax::create_tax_class( $name );

			// Display any error that could be triggered while creating tax classes.
			if ( is_wp_error( $tax_class ) ) {
				WC_Admin_Settings::add_error(
					sprintf(
						/* translators: 1: tax class name 2: error message */
						esc_html__( 'Additional tax class "%1$s" couldn\'t be saved. %2$s.', 'woocommerce' ),
						esc_html( $name ),
						$tax_class->get_error_message()
					)
				);
			}
		}

		return null;
	}

	/**
	 * Output tax rate tables.
	 */
	public function output_tax_rates() {
		global $current_section;

		$current_class = $this->get_current_tax_class();

		$countries = array();
		foreach ( WC()->countries->get_allowed_countries() as $value => $label ) {
			$countries[] = array(
				'value' => $value,
				'label' => esc_js( html_entity_decode( $label ) ),
			);
		}

		$states = array();
		foreach ( WC()->countries->get_allowed_country_states() as $label ) {
			foreach ( $label as $code => $state ) {
				$states[] = array(
					'value' => $code,
					'label' => esc_js( html_entity_decode( $state ) ),
				);
			}
		}

		$base_url = admin_url(
			add_query_arg(
				array(
					'page'    => 'wc-settings',
					'tab'     => 'tax',
					'section' => $current_section,
				),
				'admin.php'
			)
		);

		// Localize and enqueue our js.
		wp_localize_script(
			'wc-settings-tax',
			'htmlSettingsTaxLocalizeScript',
			array(
				'current_class' => $current_class,
				'wc_tax_nonce'  => wp_create_nonce( 'wc_tax_nonce-class:' . $current_class ),
				'base_url'      => $base_url,
				'rates'         => array_values( WC_Tax::get_rates_for_tax_class( $current_class ) ),
				'page'          => ! empty( $_GET['p'] ) ? absint( $_GET['p'] ) : 1, // phpcs:ignore WordPress.Security.NonceVerification.Recommended
				'limit'         => 100,
				'countries'     => $countries,
				'states'        => $states,
				'default_rate'  => array(
					'tax_rate_id'       => 0,
					'tax_rate_country'  => '',
					'tax_rate_state'    => '',
					'tax_rate'          => '',
					'tax_rate_name'     => '',
					'tax_rate_priority' => 1,
					'tax_rate_compound' => 0,
					'tax_rate_shipping' => 1,
					'tax_rate_order'    => null,
					'tax_rate_class'    => $current_class,
				),
				'strings'       => array(
					'no_rows_selected'        => __( 'No row(s) selected', 'woocommerce' ),
					'unload_confirmation_msg' => __( 'Your changed data will be lost if you leave this page without saving.', 'woocommerce' ),
					'csv_data_cols'           => array(
						__( 'Country code', 'woocommerce' ),
						__( 'State code', 'woocommerce' ),
						__( 'Postcode / ZIP', 'woocommerce' ),
						__( 'City', 'woocommerce' ),
						__( 'Rate %', 'woocommerce' ),
						__( 'Tax name', 'woocommerce' ),
						__( 'Priority', 'woocommerce' ),
						__( 'Compound', 'woocommerce' ),
						__( 'Shipping', 'woocommerce' ),
						__( 'Tax class', 'woocommerce' ),
					),
				),
			)
		);
		wp_enqueue_script( 'wc-settings-tax' );

		include __DIR__ . '/views/html-settings-tax.php';
	}

	/**
	 * Get tax class being edited.
	 *
	 * @return string
	 */
	private static function get_current_tax_class() {
		global $current_section;

		$tax_classes   = WC_Tax::get_tax_classes();
		$current_class = '';

		foreach ( $tax_classes as $class ) {
			if ( sanitize_title( $class ) === $current_section ) {
				$current_class = $class;
			}
		}

		return $current_class;
	}

	/**
	 * Get a posted tax rate.
	 *
	 * @param string $key   Key of tax rate in the post data array.
	 * @param int    $order Position/order of rate.
	 * @param string $class Tax class for rate.
	 * @return array
	 */
	private function get_posted_tax_rate( $key, $order, $class ) {
		// phpcs:disable WordPress.Security.NonceVerification.Missing -- this is called from 'save_tax_rates' only, where nonce is already verified.
		$tax_rate      = array();
		$tax_rate_keys = array(
			'tax_rate_country',
			'tax_rate_state',
			'tax_rate',
			'tax_rate_name',
			'tax_rate_priority',
		);

		// phpcs:disable WordPress.Security.NonceVerification.Missing
		foreach ( $tax_rate_keys as $tax_rate_key ) {
			if ( isset( $_POST[ $tax_rate_key ], $_POST[ $tax_rate_key ][ $key ] ) ) {
				$tax_rate[ $tax_rate_key ] = wc_clean( wp_unslash( $_POST[ $tax_rate_key ][ $key ] ) );
			}
		}

		$tax_rate['tax_rate_compound'] = isset( $_POST['tax_rate_compound'][ $key ] ) ? 1 : 0;
		$tax_rate['tax_rate_shipping'] = isset( $_POST['tax_rate_shipping'][ $key ] ) ? 1 : 0;
		$tax_rate['tax_rate_order']    = $order;
		$tax_rate['tax_rate_class']    = $class;
		// phpcs:enable WordPress.Security.NonceVerification.Missing

		return $tax_rate;
		// phpcs:enable WordPress.Security.NonceVerification.Missing
	}

	/**
	 * Save tax rates.
	 */
	public function save_tax_rates() {
		// phpcs:disable WordPress.Security.NonceVerification.Missing -- this is called via "do_action('woocommerce_settings_save_'...") in base class, where nonce is verified first.
		global $wpdb;

		$current_class = sanitize_title( $this->get_current_tax_class() );
		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.NonceVerification.Missing
		$posted_countries = wc_clean( wp_unslash( $_POST['tax_rate_country'] ) );

		// get the tax rate id of the first submited row.
		$first_tax_rate_id = key( $posted_countries );

		// get the order position of the first tax rate id.
		$tax_rate_order = absint( $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_order FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $first_tax_rate_id ) ) );

		$index = isset( $tax_rate_order ) ? $tax_rate_order : 0;

		// Loop posted fields.
		// phpcs:disable WordPress.Security.NonceVerification.Missing
		foreach ( $posted_countries as $key => $value ) {
			$mode     = ( 0 === strpos( $key, 'new-' ) ) ? 'insert' : 'update';
			$tax_rate = $this->get_posted_tax_rate( $key, $index ++, $current_class );

			if ( 'insert' === $mode ) {
				$tax_rate_id = WC_Tax::_insert_tax_rate( $tax_rate );
			} elseif ( isset( $_POST['remove_tax_rate'][ $key ] ) && 1 === absint( $_POST['remove_tax_rate'][ $key ] ) ) {
				$tax_rate_id = absint( $key );
				WC_Tax::_delete_tax_rate( $tax_rate_id );
				continue;
			} else {
				$tax_rate_id = absint( $key );
				WC_Tax::_update_tax_rate( $tax_rate_id, $tax_rate );
			}

			if ( isset( $_POST['tax_rate_postcode'][ $key ] ) ) {
				WC_Tax::_update_tax_rate_postcodes( $tax_rate_id, wc_clean( wp_unslash( $_POST['tax_rate_postcode'][ $key ] ) ) );
			}
			if ( isset( $_POST['tax_rate_city'][ $key ] ) ) {
				WC_Tax::_update_tax_rate_cities( $tax_rate_id, wc_clean( wp_unslash( $_POST['tax_rate_city'][ $key ] ) ) );
			}
		}
		// phpcs:enable WordPress.Security.NonceVerification.Missing
	}
}

return new WC_Settings_Tax();
admin/settings/class-wc-settings-integrations.php000064400000004170151336044770016277 0ustar00<?php
/**
 * WooCommerce Integration Settings
 *
 * @package     WooCommerce\Admin
 * @version     2.1.0
 */

use Automattic\Jetpack\Constants;

defined( 'ABSPATH' ) || exit;

if ( ! class_exists( 'WC_Settings_Integrations', false ) ) :

	/**
	 * WC_Settings_Integrations.
	 */
	class WC_Settings_Integrations extends WC_Settings_Page {

		/**
		 * Constructor.
		 */
		public function __construct() {
			$this->id    = 'integration';
			$this->label = __( 'Integration', 'woocommerce' );

			if ( isset( WC()->integrations ) && WC()->integrations->get_integrations() ) {
				parent::__construct();
			}
		}

		/**
		 * Get own sections.
		 *
		 * @return array
		 */
		protected function get_own_sections() {
			global $current_section;

			$sections = array();

			if ( ! $this->wc_is_installing() ) {
				$integrations = $this->get_integrations();

				if ( ! $current_section && ! empty( $integrations ) ) {
					$current_section = current( $integrations )->id;
				}

				if ( count( $integrations ) > 1 ) {
					foreach ( $integrations as $integration ) {
						$title                                      = empty( $integration->method_title ) ? ucfirst( $integration->id ) : $integration->method_title;
						$sections[ strtolower( $integration->id ) ] = esc_html( $title );
					}
				}
			}

			return $sections;
		}

		/**
		 * Is WC_INSTALLING constant defined?
		 * This method exists to ease unit testing.
		 *
		 * @return bool True is the WC_INSTALLING constant is defined.
		 */
		protected function wc_is_installing() {
			return Constants::is_defined( 'WC_INSTALLING' );
		}

		/**
		 * Get the currently available integrations.
		 * This method exists to ease unit testing.
		 *
		 * @return array Currently available integrations.
		 */
		protected function get_integrations() {
			return WC()->integrations->get_integrations();
		}

		/**
		 * Output the settings.
		 */
		public function output() {
			global $current_section;

			$integrations = $this->get_integrations();

			if ( isset( $integrations[ $current_section ] ) ) {
				$integrations[ $current_section ]->admin_options();
			}
		}
	}

endif;

return new WC_Settings_Integrations();
admin/settings/class-wc-settings-emails.php000064400000027670151336044770015055 0ustar00<?php
/**
 * WooCommerce Email Settings
 *
 * @package WooCommerce\Admin
 * @version 2.1.0
 */

defined( 'ABSPATH' ) || exit;

if ( class_exists( 'WC_Settings_Emails', false ) ) {
	return new WC_Settings_Emails();
}

/**
 * WC_Settings_Emails.
 */
class WC_Settings_Emails extends WC_Settings_Page {

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->id    = 'email';
		$this->label = __( 'Emails', 'woocommerce' );

		add_action( 'woocommerce_admin_field_email_notification', array( $this, 'email_notification_setting' ) );
		parent::__construct();
	}

	/**
	 * Get own sections.
	 *
	 * @return array
	 */
	protected function get_own_sections() {
		return array(
			'' => __( 'Email options', 'woocommerce' ),
		);
	}

	/**
	 * Get settings array.
	 *
	 * @return array
	 */
	protected function get_settings_for_default_section() {
		$desc_help_text = sprintf(
		/* translators: %1$s: Link to WP Mail Logging plugin, %2$s: Link to Email FAQ support page. */
			__( 'To ensure your store&rsquo;s notifications arrive in your and your customers&rsquo; inboxes, we recommend connecting your email address to your domain and setting up a dedicated SMTP server. If something doesn&rsquo;t seem to be sending correctly, install the <a href="%1$s">WP Mail Logging Plugin</a> or check the <a href="%2$s">Email FAQ page</a>.', 'woocommerce' ),
			'https://wordpress.org/plugins/wp-mail-logging/',
			'https://docs.woocommerce.com/document/email-faq'
		);
		$settings =
			array(
				array(
					'title' => __( 'Email notifications', 'woocommerce' ),
					/* translators: %s: help description with link to WP Mail logging and support page. */
					'desc'  => sprintf( __( 'Email notifications sent from WooCommerce are listed below. Click on an email to configure it.<br>%s', 'woocommerce' ), $desc_help_text ),
					'type'  => 'title',
					'id'    => 'email_notification_settings',
				),

				array( 'type' => 'email_notification' ),

				array(
					'type' => 'sectionend',
					'id'   => 'email_notification_settings',
				),

				array(
					'type' => 'sectionend',
					'id'   => 'email_recipient_options',
				),

				array(
					'title' => __( 'Email sender options', 'woocommerce' ),
					'type'  => 'title',
					'desc'  => '',
					'id'    => 'email_options',
				),

				array(
					'title'    => __( '"From" name', 'woocommerce' ),
					'desc'     => __( 'How the sender name appears in outgoing WooCommerce emails.', 'woocommerce' ),
					'id'       => 'woocommerce_email_from_name',
					'type'     => 'text',
					'css'      => 'min-width:400px;',
					'default'  => esc_attr( get_bloginfo( 'name', 'display' ) ),
					'autoload' => false,
					'desc_tip' => true,
				),

				array(
					'title'             => __( '"From" address', 'woocommerce' ),
					'desc'              => __( 'How the sender email appears in outgoing WooCommerce emails.', 'woocommerce' ),
					'id'                => 'woocommerce_email_from_address',
					'type'              => 'email',
					'custom_attributes' => array(
						'multiple' => 'multiple',
					),
					'css'               => 'min-width:400px;',
					'default'           => get_option( 'admin_email' ),
					'autoload'          => false,
					'desc_tip'          => true,
				),

				array(
					'type' => 'sectionend',
					'id'   => 'email_options',
				),

				array(
					'title' => __( 'Email template', 'woocommerce' ),
					'type'  => 'title',
					/* translators: %s: Nonced email preview link */
					'desc'  => sprintf( __( 'This section lets you customize the WooCommerce emails. <a href="%s" target="_blank">Click here to preview your email template</a>.', 'woocommerce' ), wp_nonce_url( admin_url( '?preview_woocommerce_mail=true' ), 'preview-mail' ) ),
					'id'    => 'email_template_options',
				),

				array(
					'title'       => __( 'Header image', 'woocommerce' ),
					'desc'        => __( 'URL to an image you want to show in the email header. Upload images using the media uploader (Admin > Media).', 'woocommerce' ),
					'id'          => 'woocommerce_email_header_image',
					'type'        => 'text',
					'css'         => 'min-width:400px;',
					'placeholder' => __( 'N/A', 'woocommerce' ),
					'default'     => '',
					'autoload'    => false,
					'desc_tip'    => true,
				),

				array(
					'title'       => __( 'Footer text', 'woocommerce' ),
					/* translators: %s: Available placeholders for use */
					'desc'        => __( 'The text to appear in the footer of all WooCommerce emails.', 'woocommerce' ) . ' ' . sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '{site_title} {site_url}' ),
					'id'          => 'woocommerce_email_footer_text',
					'css'         => 'width:400px; height: 75px;',
					'placeholder' => __( 'N/A', 'woocommerce' ),
					'type'        => 'textarea',
					'default'     => '{site_title} &mdash; Built with {WooCommerce}',
					'autoload'    => false,
					'desc_tip'    => true,
				),

				array(
					'title'    => __( 'Base color', 'woocommerce' ),
					/* translators: %s: default color */
					'desc'     => sprintf( __( 'The base color for WooCommerce email templates. Default %s.', 'woocommerce' ), '<code>#96588a</code>' ),
					'id'       => 'woocommerce_email_base_color',
					'type'     => 'color',
					'css'      => 'width:6em;',
					'default'  => '#96588a',
					'autoload' => false,
					'desc_tip' => true,
				),

				array(
					'title'    => __( 'Background color', 'woocommerce' ),
					/* translators: %s: default color */
					'desc'     => sprintf( __( 'The background color for WooCommerce email templates. Default %s.', 'woocommerce' ), '<code>#f7f7f7</code>' ),
					'id'       => 'woocommerce_email_background_color',
					'type'     => 'color',
					'css'      => 'width:6em;',
					'default'  => '#f7f7f7',
					'autoload' => false,
					'desc_tip' => true,
				),

				array(
					'title'    => __( 'Body background color', 'woocommerce' ),
					/* translators: %s: default color */
					'desc'     => sprintf( __( 'The main body background color. Default %s.', 'woocommerce' ), '<code>#ffffff</code>' ),
					'id'       => 'woocommerce_email_body_background_color',
					'type'     => 'color',
					'css'      => 'width:6em;',
					'default'  => '#ffffff',
					'autoload' => false,
					'desc_tip' => true,
				),

				array(
					'title'    => __( 'Body text color', 'woocommerce' ),
					/* translators: %s: default color */
					'desc'     => sprintf( __( 'The main body text color. Default %s.', 'woocommerce' ), '<code>#3c3c3c</code>' ),
					'id'       => 'woocommerce_email_text_color',
					'type'     => 'color',
					'css'      => 'width:6em;',
					'default'  => '#3c3c3c',
					'autoload' => false,
					'desc_tip' => true,
				),

				array(
					'type' => 'sectionend',
					'id'   => 'email_template_options',
				),

				array(
					'title' => __( 'Store management insights', 'woocommerce' ),
					'type'  => 'title',
					'id'    => 'email_merchant_notes',
				),

				array(
					'title'         => __( 'Enable email insights', 'woocommerce' ),
					'desc'          => __( 'Receive email notifications with additional guidance to complete the basic store setup and helpful insights', 'woocommerce' ),
					'id'            => 'woocommerce_merchant_email_notifications',
					'type'          => 'checkbox',
					'checkboxgroup' => 'start',
					'default'       => 'no',
					'autoload'      => false,
				),

				array(
					'type' => 'sectionend',
					'id'   => 'email_merchant_notes',
				),
			);

		return apply_filters( 'woocommerce_email_settings', $settings );
	}

	/**
	 * Output the settings.
	 */
	public function output() {
		global $current_section;

		// Define emails that can be customised here.
		$mailer          = WC()->mailer();
		$email_templates = $mailer->get_emails();

		if ( $current_section ) {
			foreach ( $email_templates as $email_key => $email ) {
				if ( strtolower( $email_key ) === $current_section ) {
					$this->run_email_admin_options( $email );
					break;
				}
			}
		}

		parent::output();
	}

	/**
	 * Run the 'admin_options' method on a given email.
	 * This method exists to easy unit testing.
	 *
	 * @param object $email The email object to run the method on.
	 */
	protected function run_email_admin_options( $email ) {
		$email->admin_options();
	}

	/**
	 * Save settings.
	 */
	public function save() {
		global $current_section;

		if ( ! $current_section ) {
			$this->save_settings_for_current_section();
			$this->do_update_options_action();
		} else {
			$wc_emails = WC_Emails::instance();

			if ( in_array( $current_section, array_map( 'sanitize_title', array_keys( $wc_emails->get_emails() ) ), true ) ) {
				foreach ( $wc_emails->get_emails() as $email_id => $email ) {
					if ( sanitize_title( $email_id ) === $current_section ) {
						$this->do_update_options_action( $email->id );
					}
				}
			} else {
				$this->save_settings_for_current_section();
				$this->do_update_options_action();
			}
		}
	}

	/**
	 * Output email notification settings.
	 */
	public function email_notification_setting() {
		// Define emails that can be customised here.
		$mailer          = WC()->mailer();
		$email_templates = $mailer->get_emails();

		?>
		<tr valign="top">
		<td class="wc_emails_wrapper" colspan="2">
			<table class="wc_emails widefat" cellspacing="0">
				<thead>
					<tr>
						<?php
						$columns = apply_filters(
							'woocommerce_email_setting_columns',
							array(
								'status'     => '',
								'name'       => __( 'Email', 'woocommerce' ),
								'email_type' => __( 'Content type', 'woocommerce' ),
								'recipient'  => __( 'Recipient(s)', 'woocommerce' ),
								'actions'    => '',
							)
						);
						foreach ( $columns as $key => $column ) {
							echo '<th class="wc-email-settings-table-' . esc_attr( $key ) . '">' . esc_html( $column ) . '</th>';
						}
						?>
						</tr>
					</thead>
					<tbody>
						<?php
						foreach ( $email_templates as $email_key => $email ) {
							echo '<tr>';

							foreach ( $columns as $key => $column ) {

								switch ( $key ) {
									case 'name':
										echo '<td class="wc-email-settings-table-' . esc_attr( $key ) . '">
										<a href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=email&section=' . strtolower( $email_key ) ) ) . '">' . esc_html( $email->get_title() ) . '</a>
										' . wc_help_tip( $email->get_description() ) . '
									</td>';
										break;
									case 'recipient':
										echo '<td class="wc-email-settings-table-' . esc_attr( $key ) . '">
										' . esc_html( $email->is_customer_email() ? __( 'Customer', 'woocommerce' ) : $email->get_recipient() ) . '
									</td>';
										break;
									case 'status':
										echo '<td class="wc-email-settings-table-' . esc_attr( $key ) . '">';

										if ( $email->is_manual() ) {
											echo '<span class="status-manual tips" data-tip="' . esc_attr__( 'Manually sent', 'woocommerce' ) . '">' . esc_html__( 'Manual', 'woocommerce' ) . '</span>';
										} elseif ( $email->is_enabled() ) {
											echo '<span class="status-enabled tips" data-tip="' . esc_attr__( 'Enabled', 'woocommerce' ) . '">' . esc_html__( 'Yes', 'woocommerce' ) . '</span>';
										} else {
											echo '<span class="status-disabled tips" data-tip="' . esc_attr__( 'Disabled', 'woocommerce' ) . '">-</span>';
										}

										echo '</td>';
										break;
									case 'email_type':
										echo '<td class="wc-email-settings-table-' . esc_attr( $key ) . '">
										' . esc_html( $email->get_content_type() ) . '
									</td>';
										break;
									case 'actions':
										echo '<td class="wc-email-settings-table-' . esc_attr( $key ) . '">
										<a class="button alignright" href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=email&section=' . strtolower( $email_key ) ) ) . '">' . esc_html__( 'Manage', 'woocommerce' ) . '</a>
									</td>';
										break;
									default:
										do_action( 'woocommerce_email_setting_column_' . $key, $email );
										break;
								}
							}

							echo '</tr>';
						}
						?>
					</tbody>
				</table>
			</td>
		</tr>
		<?php
	}
}

return new WC_Settings_Emails();
admin/settings/views/html-admin-page-shipping-zones.php000064400000014071151336044770017276 0ustar00<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>

<h2 class="wc-shipping-zones-heading">
	<?php _e( 'Shipping zones', 'woocommerce' ); ?>
	<a href="<?php echo admin_url( 'admin.php?page=wc-settings&tab=shipping&zone_id=new' ); ?>" class="page-title-action"><?php esc_html_e( 'Add shipping zone', 'woocommerce' ); ?></a>
</h2>
<p><?php echo __( 'A shipping zone is a geographic region where a certain set of shipping methods are offered.', 'woocommerce' ) . ' ' . __( 'WooCommerce will match a customer to a single zone using their shipping address and present the shipping methods within that zone to them.', 'woocommerce' ); ?></p>
<table class="wc-shipping-zones widefat">
	<thead>
		<tr>
			<th class="wc-shipping-zone-sort"><?php echo wc_help_tip( __( 'Drag and drop to re-order your custom zones. This is the order in which they will be matched against the customer address.', 'woocommerce' ) ); ?></th>
			<th class="wc-shipping-zone-name"><?php esc_html_e( 'Zone name', 'woocommerce' ); ?></th>
			<th class="wc-shipping-zone-region"><?php esc_html_e( 'Region(s)', 'woocommerce' ); ?></th>
			<th class="wc-shipping-zone-methods"><?php esc_html_e( 'Shipping method(s)', 'woocommerce' ); ?></th>
		</tr>
	</thead>
	<tbody class="wc-shipping-zone-rows"></tbody>
	<tbody>
		<tr data-id="0" class="wc-shipping-zone-worldwide">
			<td width="1%" class="wc-shipping-zone-worldwide"></td>
			<td class="wc-shipping-zone-name">
				<a href="<?php echo esc_url( admin_url( 'admin.php?page=wc-settings&tab=shipping&zone_id=0' ) ); ?>"><?php esc_html_e( 'Locations not covered by your other zones', 'woocommerce' ); ?></a>
				<div class="row-actions">
					<a href="admin.php?page=wc-settings&amp;tab=shipping&amp;zone_id=0"><?php _e( 'Manage shipping methods', 'woocommerce' ); ?></a>
				</div>
			</td>
			<td class="wc-shipping-zone-region"><?php _e( 'This zone is <b>optionally</b> used for regions that are not included in any other shipping zone.', 'woocommerce' ); ?></td>
			<td class="wc-shipping-zone-methods">
				<ul>
					<?php
					$worldwide = new WC_Shipping_Zone( 0 );
					$methods   = $worldwide->get_shipping_methods();
					uasort( $methods, 'wc_shipping_zone_method_order_uasort_comparison' );

					if ( ! empty( $methods ) ) {
						foreach ( $methods as $method ) {
							$class_name = 'yes' === $method->enabled ? 'method_enabled' : 'method_disabled';
							echo '<li class="wc-shipping-zone-method ' . esc_attr( $class_name ) . '">' . esc_html( $method->get_title() ) . '</li>';
						}
					} else {
						echo '<li class="wc-shipping-zone-method">' . __( 'No shipping methods offered to this zone.', 'woocommerce' ) . '</li>';
					}
					?>
				</ul>
			</td>
		</tr>
	</tbody>
</table>

<script type="text/html" id="tmpl-wc-shipping-zone-row-blank">
	<?php if ( 0 === $method_count ) : ?>
		<tr>
			<td class="wc-shipping-zones-blank-state" colspan="4">
				<p class="main"><?php _e( 'A shipping zone is a geographic region where a certain set of shipping methods and rates apply.', 'woocommerce' ); ?></p>
				<p><?php _e( 'For example:', 'woocommerce' ); ?></p>
				<ul>
					<li><?php _e( 'Local zone = California ZIP 90210 = Local pickup', 'woocommerce' ); ?>
					<li><?php _e( 'US domestic zone = All US states = Flat rate shipping', 'woocommerce' ); ?>
					<li><?php _e( 'Europe zone = Any country in Europe = Flat rate shipping', 'woocommerce' ); ?>
				</ul>
				<p><?php _e( 'Add as many zones as you need &ndash; customers will only see the methods available for their address.', 'woocommerce' ); ?></p>
				<a class="button button-primary wc-shipping-zone-add" href="<?php echo esc_url( admin_url( 'admin.php?page=wc-settings&tab=shipping&zone_id=new' ) ); ?>"><?php _e( 'Add shipping zone', 'woocommerce' ); ?></a>
			</td>
		</tr>
	<?php endif; ?>
</script>

<script type="text/html" id="tmpl-wc-shipping-zone-row">
	<tr data-id="{{ data.zone_id }}">
		<td width="1%" class="wc-shipping-zone-sort"></td>
		<td class="wc-shipping-zone-name">
			<a href="admin.php?page=wc-settings&amp;tab=shipping&amp;zone_id={{ data.zone_id }}">{{ data.zone_name }}</a>
			<div class="row-actions">
				<a href="admin.php?page=wc-settings&amp;tab=shipping&amp;zone_id={{ data.zone_id }}"><?php _e( 'Edit', 'woocommerce' ); ?></a> | <a href="#" class="wc-shipping-zone-delete"><?php _e( 'Delete', 'woocommerce' ); ?></a>
			</div>
		</td>
		<td class="wc-shipping-zone-region">
			{{ data.formatted_zone_location }}
		</td>
		<td class="wc-shipping-zone-methods">
			<div><ul></ul></div>
		</td>
	</tr>
</script>

<script type="text/template" id="tmpl-wc-modal-add-shipping-method">
	<div class="wc-backbone-modal">
		<div class="wc-backbone-modal-content">
			<section class="wc-backbone-modal-main" role="main">
				<header class="wc-backbone-modal-header">
					<h1><?php _e( 'Add shipping method', 'woocommerce' ); ?></h1>
					<button class="modal-close modal-close-link dashicons dashicons-no-alt">
						<span class="screen-reader-text"><?php _e( 'Close modal panel', 'woocommerce' ); ?></span>
					</button>
				</header>
				<article>
					<form action="" method="post">
						<div class="wc-shipping-zone-method-selector">
							<p><?php esc_html_e( 'Choose the shipping method you wish to add. Only shipping methods which support zones are listed.', 'woocommerce' ); ?></p>

							<select name="add_method_id">
								<?php
								foreach ( WC()->shipping()->load_shipping_methods() as $method ) {
									if ( ! $method->supports( 'shipping-zones' ) ) {
										continue;
									}
									echo '<option data-description="' . esc_attr( wp_kses_post( wpautop( $method->get_method_description() ) ) ) . '" value="' . esc_attr( $method->id ) . '">' . esc_html( $method->get_method_title() ) . '</li>';
								}
								?>
							</select>
							<input type="hidden" name="zone_id" value="{{{ data.zone_id }}}" />
						</div>
					</form>
				</article>
				<footer>
					<div class="inner">
						<button id="btn-ok" class="button button-primary button-large"><?php _e( 'Add shipping method', 'woocommerce' ); ?></button>
					</div>
				</footer>
			</section>
		</div>
	</div>
	<div class="wc-backbone-modal-backdrop modal-close"></div>
</script>
admin/settings/views/html-admin-page-shipping-zones-instance.php000064400000000746151336044770021104 0ustar00<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>
<h2>
	<a href="<?php echo admin_url( 'admin.php?page=wc-settings&tab=shipping' ); ?>"><?php _e( 'Shipping zones', 'woocommerce' ); ?></a> &gt;
	<a href="<?php echo admin_url( 'admin.php?page=wc-settings&tab=shipping&zone_id=' . absint( $zone->get_id() ) ); ?>"><?php echo esc_html( $zone->get_zone_name() ); ?></a> &gt;
	<?php echo esc_html( $shipping_method->get_method_title() ); ?>
</h2>

<?php $shipping_method->admin_options(); ?>
admin/settings/views/html-admin-page-shipping-zone-methods.php000064400000023226151336044770020556 0ustar00<?php
/**
 * Shipping zone admin
 *
 * @package WooCommerce\Admin\Shipping
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>

<h2>
	<a href="<?php echo esc_url( admin_url( 'admin.php?page=wc-settings&tab=shipping' ) ); ?>"><?php esc_html_e( 'Shipping zones', 'woocommerce' ); ?></a> &gt;
	<span class="wc-shipping-zone-name"><?php echo esc_html( $zone->get_zone_name() ? $zone->get_zone_name() : __( 'Zone', 'woocommerce' ) ); ?></span>
</h2>

<?php do_action( 'woocommerce_shipping_zone_before_methods_table', $zone ); ?>

<table class="form-table wc-shipping-zone-settings">
	<tbody>
		<?php if ( 0 !== $zone->get_id() ) : ?>
			<tr valign="top" class="">
				<th scope="row" class="titledesc">
					<label for="zone_name">
						<?php esc_html_e( 'Zone name', 'woocommerce' ); ?>
						<?php echo wc_help_tip( __( 'This is the name of the zone for your reference.', 'woocommerce' ) ); // @codingStandardsIgnoreLine ?>
					</label>
				</th>
				<td class="forminp">
					<input type="text" data-attribute="zone_name" name="zone_name" id="zone_name" value="<?php echo esc_attr( $zone->get_zone_name( 'edit' ) ); ?>" placeholder="<?php esc_attr_e( 'Zone name', 'woocommerce' ); ?>">
				</td>
			</tr>
			<tr valign="top" class="">
				<th scope="row" class="titledesc">
					<label for="zone_locations">
						<?php esc_html_e( 'Zone regions', 'woocommerce' ); ?>
						<?php echo wc_help_tip( __( 'These are regions inside this zone. Customers will be matched against these regions.', 'woocommerce' ) ); // @codingStandardsIgnoreLine ?>
					</label>
				</th>
				<td class="forminp">
					<select multiple="multiple" data-attribute="zone_locations" id="zone_locations" name="zone_locations" data-placeholder="<?php esc_attr_e( 'Select regions within this zone', 'woocommerce' ); ?>" class="wc-shipping-zone-region-select chosen_select">
						<?php
						foreach ( $shipping_continents as $continent_code => $continent ) {
							echo '<option value="continent:' . esc_attr( $continent_code ) . '"' . wc_selected( "continent:$continent_code", $locations ) . '>' . esc_html( $continent['name'] ) . '</option>';

							$countries = array_intersect( array_keys( $allowed_countries ), $continent['countries'] );

							foreach ( $countries as $country_code ) {
								echo '<option value="country:' . esc_attr( $country_code ) . '"' . wc_selected( "country:$country_code", $locations ) . '>' . esc_html( '&nbsp;&nbsp; ' . $allowed_countries[ $country_code ] ) . '</option>';

								$states = WC()->countries->get_states( $country_code );

								if ( $states ) {
									foreach ( $states as $state_code => $state_name ) {
										echo '<option value="state:' . esc_attr( $country_code . ':' . $state_code ) . '"' . wc_selected( "state:$country_code:$state_code", $locations ) . '>' . esc_html( '&nbsp;&nbsp;&nbsp;&nbsp; ' . $state_name . ', ' . $allowed_countries[ $country_code ] ) . '</option>';
									}
								}
							}
						}
						?>
					</select>
					<?php if ( empty( $postcodes ) ) : ?>
						<a class="wc-shipping-zone-postcodes-toggle" href="#"><?php esc_html_e( 'Limit to specific ZIP/postcodes', 'woocommerce' ); ?></a>
					<?php endif; ?>
					<div class="wc-shipping-zone-postcodes">
						<textarea name="zone_postcodes" data-attribute="zone_postcodes" id="zone_postcodes" placeholder="<?php esc_attr_e( 'List 1 postcode per line', 'woocommerce' ); ?>" class="input-text large-text" cols="25" rows="5"><?php echo esc_textarea( implode( "\n", $postcodes ) ); ?></textarea>
						<?php /* translators: WooCommerce link to setting up shipping zones */ ?>
						<span class="description"><?php printf( __( 'Postcodes containing wildcards (e.g. CB23*) or fully numeric ranges (e.g. <code>90210...99000</code>) are also supported. Please see the shipping zones <a href="%s" target="_blank">documentation</a> for more information.', 'woocommerce' ), 'https://docs.woocommerce.com/document/setting-up-shipping-zones/#section-3' ); ?></span><?php // @codingStandardsIgnoreLine. ?>
					</div>
				</td>
			<?php endif; ?>
		</tr>
		<tr valign="top" class="">
			<th scope="row" class="titledesc">
				<label>
					<?php esc_html_e( 'Shipping methods', 'woocommerce' ); ?>
					<?php echo wc_help_tip( __( 'The following shipping methods apply to customers with shipping addresses within this zone.', 'woocommerce' ) ); // @codingStandardsIgnoreLine ?>
				</label>
			</th>
			<td class="">
				<table class="wc-shipping-zone-methods widefat">
					<thead>
						<tr>
							<th class="wc-shipping-zone-method-sort"></th>
							<th class="wc-shipping-zone-method-title"><?php esc_html_e( 'Title', 'woocommerce' ); ?></th>
							<th class="wc-shipping-zone-method-enabled"><?php esc_html_e( 'Enabled', 'woocommerce' ); ?></th>
							<th class="wc-shipping-zone-method-description"><?php esc_html_e( 'Description', 'woocommerce' ); ?></th>
						</tr>
					</thead>
					<tfoot>
						<tr>
							<td colspan="4">
								<button type="submit" class="button wc-shipping-zone-add-method" value="<?php esc_attr_e( 'Add shipping method', 'woocommerce' ); ?>"><?php esc_html_e( 'Add shipping method', 'woocommerce' ); ?></button>
							</td>
						</tr>
					</tfoot>
					<tbody class="wc-shipping-zone-method-rows"></tbody>
				</table>
			</td>
		</tr>
	</tbody>
</table>

<?php do_action( 'woocommerce_shipping_zone_after_methods_table', $zone ); ?>

<p class="submit">
	<button type="submit" name="submit" id="submit" class="button button-primary button-large wc-shipping-zone-method-save" value="<?php esc_attr_e( 'Save changes', 'woocommerce' ); ?>" disabled><?php esc_html_e( 'Save changes', 'woocommerce' ); ?></button>
</p>

<script type="text/html" id="tmpl-wc-shipping-zone-method-row-blank">
	<tr>
		<td class="wc-shipping-zone-method-blank-state" colspan="4">
			<p><?php esc_html_e( 'You can add multiple shipping methods within this zone. Only customers within the zone will see them.', 'woocommerce' ); ?></p>
		</td>
	</tr>
</script>

<script type="text/html" id="tmpl-wc-shipping-zone-method-row">
	<tr data-id="{{ data.instance_id }}" data-enabled="{{ data.enabled }}">
		<td width="1%" class="wc-shipping-zone-method-sort"></td>
		<td class="wc-shipping-zone-method-title">
			<a class="wc-shipping-zone-method-settings" href="admin.php?page=wc-settings&amp;tab=shipping&amp;instance_id={{ data.instance_id }}">{{{ data.title }}}</a>
			<div class="row-actions">
				<a class="wc-shipping-zone-method-settings" href="admin.php?page=wc-settings&amp;tab=shipping&amp;instance_id={{ data.instance_id }}"><?php esc_html_e( 'Edit', 'woocommerce' ); ?></a> | <a href="#" class="wc-shipping-zone-method-delete"><?php esc_html_e( 'Delete', 'woocommerce' ); ?></a>
			</div>
		</td>
		<td width="1%" class="wc-shipping-zone-method-enabled"><a href="#">{{{ data.enabled_icon }}}</a></td>
		<td class="wc-shipping-zone-method-description">
			<strong class="wc-shipping-zone-method-type">{{ data.method_title }}</strong>
			{{{ data.method_description }}}
		</td>
	</tr>
</script>

<script type="text/template" id="tmpl-wc-modal-shipping-method-settings">
	<div class="wc-backbone-modal wc-backbone-modal-shipping-method-settings">
		<div class="wc-backbone-modal-content">
			<section class="wc-backbone-modal-main" role="main">
				<header class="wc-backbone-modal-header">
					<h1>
						<?php
						printf(
							/* translators: %s: shipping method title */
							esc_html__( '%s Settings', 'woocommerce' ),
							'{{{ data.method.method_title }}}'
						);
						?>
					</h1>
					<button class="modal-close modal-close-link dashicons dashicons-no-alt">
						<span class="screen-reader-text"><?php esc_html_e( 'Close modal panel', 'woocommerce' ); ?></span>
					</button>
				</header>
				<article class="wc-modal-shipping-method-settings">
					<form action="" method="post">
						{{{ data.method.settings_html }}}
						<input type="hidden" name="instance_id" value="{{{ data.instance_id }}}" />
					</form>
				</article>
				<footer>
					<div class="inner">
						<button id="btn-ok" class="button button-primary button-large"><?php esc_html_e( 'Save changes', 'woocommerce' ); ?></button>
					</div>
				</footer>
			</section>
		</div>
	</div>
	<div class="wc-backbone-modal-backdrop modal-close"></div>
</script>

<script type="text/template" id="tmpl-wc-modal-add-shipping-method">
	<div class="wc-backbone-modal">
		<div class="wc-backbone-modal-content">
			<section class="wc-backbone-modal-main" role="main">
				<header class="wc-backbone-modal-header">
					<h1><?php esc_html_e( 'Add shipping method', 'woocommerce' ); ?></h1>
					<button class="modal-close modal-close-link dashicons dashicons-no-alt">
						<span class="screen-reader-text"><?php esc_html_e( 'Close modal panel', 'woocommerce' ); ?></span>
					</button>
				</header>
				<article>
					<form action="" method="post">
						<div class="wc-shipping-zone-method-selector">
							<p><?php esc_html_e( 'Choose the shipping method you wish to add. Only shipping methods which support zones are listed.', 'woocommerce' ); ?></p>

							<select name="add_method_id">
								<?php
								foreach ( WC()->shipping()->load_shipping_methods() as $method ) {
									if ( ! $method->supports( 'shipping-zones' ) ) {
										continue;
									}
									echo '<option data-description="' . esc_attr( wp_kses_post( wpautop( $method->get_method_description() ) ) ) . '" value="' . esc_attr( $method->id ) . '">' . esc_html( $method->get_method_title() ) . '</li>';
								}
								?>
							</select>
						</div>
					</form>
				</article>
				<footer>
					<div class="inner">
						<button id="btn-ok" class="button button-primary button-large"><?php esc_html_e( 'Add shipping method', 'woocommerce' ); ?></button>
					</div>
				</footer>
			</section>
		</div>
	</div>
	<div class="wc-backbone-modal-backdrop modal-close"></div>
</script>
admin/settings/views/html-keys-edit.php000064400000013441151336044770014217 0ustar00<?php
/**
 * Admin view: Edit API keys
 *
 * @package WooCommerce\Admin\Settings
 */

defined( 'ABSPATH' ) || exit;
?>

<div id="key-fields" class="settings-panel">
	<h2><?php esc_html_e( 'Key details', 'woocommerce' ); ?></h2>

	<input type="hidden" id="key_id" value="<?php echo esc_attr( $key_id ); ?>" />

	<table id="api-keys-options" class="form-table">
		<tbody>
			<tr valign="top">
				<th scope="row" class="titledesc">
					<label for="key_description">
						<?php esc_html_e( 'Description', 'woocommerce' ); ?>
						<?php echo wc_help_tip( __( 'Friendly name for identifying this key.', 'woocommerce' ) ); ?>
					</label>
				</th>
				<td class="forminp">
					<input id="key_description" type="text" class="input-text regular-input" value="<?php echo esc_attr( $key_data['description'] ); ?>" />
				</td>
			</tr>
			<tr valign="top">
				<th scope="row" class="titledesc">
					<label for="key_user">
						<?php esc_html_e( 'User', 'woocommerce' ); ?>
						<?php echo wc_help_tip( __( 'Owner of these keys.', 'woocommerce' ) ); ?>
					</label>
				</th>
				<td class="forminp">
					<?php
					$current_user_id = get_current_user_id();
					$user_id        = ! empty( $key_data['user_id'] ) ? absint( $key_data['user_id'] ) : $current_user_id;
					$user           = get_user_by( 'id', $user_id );
					$user_string    = sprintf(
						/* translators: 1: user display name 2: user ID 3: user email */
						esc_html__( '%1$s (#%2$s &ndash; %3$s)', 'woocommerce' ),
						$user->display_name,
						absint( $user->ID ),
						$user->user_email
					);
					?>
					<select class="wc-customer-search" id="key_user" data-placeholder="<?php esc_attr_e( 'Search for a user&hellip;', 'woocommerce' ); ?>" data-allow_clear="true">
						<option value="<?php echo esc_attr( $user_id ); ?>" selected="selected"><?php echo htmlspecialchars( wp_kses_post( $user_string ) ); // htmlspecialchars to prevent XSS when rendered by selectWoo. ?></option>
					</select>
				</td>
			</tr>
			<tr valign="top">
				<th scope="row" class="titledesc">
					<label for="key_permissions">
						<?php esc_html_e( 'Permissions', 'woocommerce' ); ?>
						<?php echo wc_help_tip( __( 'Select the access type of these keys.', 'woocommerce' ) ); ?>
					</label>
				</th>
				<td class="forminp">
					<select id="key_permissions" class="wc-enhanced-select">
						<?php
						$permissions = array(
							'read'       => __( 'Read', 'woocommerce' ),
							'write'      => __( 'Write', 'woocommerce' ),
							'read_write' => __( 'Read/Write', 'woocommerce' ),
						);

						foreach ( $permissions as $permission_id => $permission_name ) :
							?>
							<option value="<?php echo esc_attr( $permission_id ); ?>" <?php selected( $key_data['permissions'], $permission_id, true ); ?>><?php echo esc_html( $permission_name ); ?></option>
						<?php endforeach; ?>
					</select>
				</td>
			</tr>

			<?php if ( 0 !== $key_id ) : ?>
				<tr valign="top">
					<th scope="row" class="titledesc">
						<?php esc_html_e( 'Consumer key ending in', 'woocommerce' ); ?>
					</th>
					<td class="forminp">
						<code>&hellip;<?php echo esc_html( $key_data['truncated_key'] ); ?></code>
					</td>
				</tr>
				<tr valign="top">
					<th scope="row" class="titledesc">
						<?php esc_html_e( 'Last access', 'woocommerce' ); ?>
					</th>
					<td class="forminp">
						<span>
						<?php
						if ( ! empty( $key_data['last_access'] ) ) {
							/* translators: 1: last access date 2: last access time */
							$date = sprintf( __( '%1$s at %2$s', 'woocommerce' ), date_i18n( wc_date_format(), strtotime( $key_data['last_access'] ) ), date_i18n( wc_time_format(), strtotime( $key_data['last_access'] ) ) );

							echo esc_html( apply_filters( 'woocommerce_api_key_last_access_datetime', $date, $key_data['last_access'] ) );
						} else {
							esc_html_e( 'Unknown', 'woocommerce' );
						}
						?>
						</span>
					</td>
				</tr>
			<?php endif ?>
		</tbody>
	</table>

	<?php do_action( 'woocommerce_admin_key_fields', $key_data ); ?>

	<?php
	if ( 0 === intval( $key_id ) ) {
		submit_button( __( 'Generate API key', 'woocommerce' ), 'primary', 'update_api_key' );
	} else {
		?>
		<p class="submit">
			<?php submit_button( __( 'Save changes', 'woocommerce' ), 'primary', 'update_api_key', false ); ?>
			<a style="color: #a00; text-decoration: none; margin-left: 10px;" href="<?php echo esc_url( wp_nonce_url( add_query_arg( array( 'revoke-key' => $key_id ), admin_url( 'admin.php?page=wc-settings&tab=advanced&section=keys' ) ), 'revoke' ) ); ?>"><?php esc_html_e( 'Revoke key', 'woocommerce' ); ?></a>
		</p>
		<?php
	}
	?>
</div>

<script type="text/template" id="tmpl-api-keys-template">
	<p id="copy-error"></p>
	<table class="form-table">
		<tbody>
			<tr valign="top">
				<th scope="row" class="titledesc">
					<?php esc_html_e( 'Consumer key', 'woocommerce' ); ?>
				</th>
				<td class="forminp">
					<input id="key_consumer_key" type="text" value="{{ data.consumer_key }}" size="55" readonly="readonly"> <button type="button" class="button-secondary copy-key" data-tip="<?php esc_attr_e( 'Copied!', 'woocommerce' ); ?>"><?php esc_html_e( 'Copy', 'woocommerce' ); ?></button>
				</td>
			</tr>
			<tr valign="top">
				<th scope="row" class="titledesc">
					<?php esc_html_e( 'Consumer secret', 'woocommerce' ); ?>
				</th>
				<td class="forminp">
					<input id="key_consumer_secret" type="text" value="{{ data.consumer_secret }}" size="55" readonly="readonly"> <button type="button" class="button-secondary copy-secret" data-tip="<?php esc_attr_e( 'Copied!', 'woocommerce' ); ?>"><?php esc_html_e( 'Copy', 'woocommerce' ); ?></button>
				</td>
			</tr>
			<tr valign="top">
				<th scope="row" class="titledesc">
					<?php esc_html_e( 'QRCode', 'woocommerce' ); ?>
				</th>
				<td class="forminp">
					<div id="keys-qrcode"></div>
				</td>
			</tr>
		</tbody>
	</table>
</script>
admin/settings/views/html-admin-page-shipping-classes.php000064400000007000151336044770017567 0ustar00<?php
/**
 * Shipping classes admin
 *
 * @package WooCommerce\Admin\Shipping
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>

<h2>
	<?php esc_html_e( 'Shipping classes', 'woocommerce' ); ?>
	<?php echo wc_help_tip( __( 'Shipping classes can be used to group products of similar type and can be used by some Shipping Methods (such as "Flat rate shipping") to provide different rates to different classes of product.', 'woocommerce' ) ); // @codingStandardsIgnoreLine ?>
</h2>

<table class="wc-shipping-classes widefat">
	<thead>
		<tr>
			<?php foreach ( $shipping_class_columns as $class => $heading ) : ?>
				<th class="<?php echo esc_attr( $class ); ?>"><?php echo esc_html( $heading ); ?></th>
			<?php endforeach; ?>
		</tr>
	</thead>
	<tfoot>
		<tr>
			<td colspan="<?php echo absint( count( $shipping_class_columns ) ); ?>">
				<button type="submit" name="save" class="button button-primary wc-shipping-class-save" value="<?php esc_attr_e( 'Save shipping classes', 'woocommerce' ); ?>" disabled><?php esc_html_e( 'Save shipping classes', 'woocommerce' ); ?></button>
				<a class="button button-secondary wc-shipping-class-add" href="#"><?php esc_html_e( 'Add shipping class', 'woocommerce' ); ?></a>
			</td>
		</tr>
	</tfoot>
	<tbody class="wc-shipping-class-rows"></tbody>
</table>

<script type="text/html" id="tmpl-wc-shipping-class-row-blank">
	<tr>
		<td class="wc-shipping-classes-blank-state" colspan="<?php echo absint( count( $shipping_class_columns ) ); ?>"><p><?php esc_html_e( 'No shipping classes have been created.', 'woocommerce' ); ?></p></td>
	</tr>
</script>

<script type="text/html" id="tmpl-wc-shipping-class-row">
	<tr data-id="{{ data.term_id }}">
		<?php
		foreach ( $shipping_class_columns as $class => $heading ) {
			echo '<td class="' . esc_attr( $class ) . '">';
			switch ( $class ) {
				case 'wc-shipping-class-name':
					?>
					<div class="view">
						{{ data.name }}
						<div class="row-actions">
							<a class="wc-shipping-class-edit" href="#"><?php esc_html_e( 'Edit', 'woocommerce' ); ?></a> | <a href="#" class="wc-shipping-class-delete"><?php esc_html_e( 'Remove', 'woocommerce' ); ?></a>
						</div>
					</div>
					<div class="edit">
						<input type="text" name="name[{{ data.term_id }}]" data-attribute="name" value="{{ data.name }}" placeholder="<?php esc_attr_e( 'Shipping class name', 'woocommerce' ); ?>" />
						<div class="row-actions">
							<a class="wc-shipping-class-cancel-edit" href="#"><?php esc_html_e( 'Cancel changes', 'woocommerce' ); ?></a>
						</div>
					</div>
					<?php
					break;
				case 'wc-shipping-class-slug':
					?>
					<div class="view">{{ data.slug }}</div>
					<div class="edit"><input type="text" name="slug[{{ data.term_id }}]" data-attribute="slug" value="{{ data.slug }}" placeholder="<?php esc_attr_e( 'Slug', 'woocommerce' ); ?>" /></div>
					<?php
					break;
				case 'wc-shipping-class-description':
					?>
					<div class="view">{{ data.description }}</div>
					<div class="edit"><input type="text" name="description[{{ data.term_id }}]" data-attribute="description" value="{{ data.description }}" placeholder="<?php esc_attr_e( 'Description for your reference', 'woocommerce' ); ?>" /></div>
					<?php
					break;
				case 'wc-shipping-class-count':
					?>
					<a href="<?php echo esc_url( admin_url( 'edit.php?post_type=product&product_shipping_class=' ) ); ?>{{data.slug}}">{{ data.count }}</a>
					<?php
					break;
				default:
					do_action( 'woocommerce_shipping_classes_column_' . $class );
					break;
			}
			echo '</td>';
		}
		?>
	</tr>
</script>
admin/settings/views/settings-tax.php000064400000010456151336044770014014 0ustar00<?php
/**
 * Tax settings.
 *
 * @package WooCommerce\Admin\Settings.
 */

defined( 'ABSPATH' ) || exit;

$settings = array(

	array(
		'title' => __( 'Tax options', 'woocommerce' ),
		'type'  => 'title',
		'desc'  => '',
		'id'    => 'tax_options',
	),

	array(
		'title'    => __( 'Prices entered with tax', 'woocommerce' ),
		'id'       => 'woocommerce_prices_include_tax',
		'default'  => 'no',
		'type'     => 'radio',
		'desc_tip' => __( 'This option is important as it will affect how you input prices. Changing it will not update existing products.', 'woocommerce' ),
		'options'  => array(
			'yes' => __( 'Yes, I will enter prices inclusive of tax', 'woocommerce' ),
			'no'  => __( 'No, I will enter prices exclusive of tax', 'woocommerce' ),
		),
	),

	array(
		'title'    => __( 'Calculate tax based on', 'woocommerce' ),
		'id'       => 'woocommerce_tax_based_on',
		'desc_tip' => __( 'This option determines which address is used to calculate tax.', 'woocommerce' ),
		'default'  => 'shipping',
		'type'     => 'select',
		'class'    => 'wc-enhanced-select',
		'options'  => array(
			'shipping' => __( 'Customer shipping address', 'woocommerce' ),
			'billing'  => __( 'Customer billing address', 'woocommerce' ),
			'base'     => __( 'Shop base address', 'woocommerce' ),
		),
	),

	'shipping-tax-class' => array(
		'title'    => __( 'Shipping tax class', 'woocommerce' ),
		'desc'     => __( 'Optionally control which tax class shipping gets, or leave it so shipping tax is based on the cart items themselves.', 'woocommerce' ),
		'id'       => 'woocommerce_shipping_tax_class',
		'css'      => 'min-width:150px;',
		'default'  => 'inherit',
		'type'     => 'select',
		'class'    => 'wc-enhanced-select',
		'options'  => array( 'inherit' => __( 'Shipping tax class based on cart items', 'woocommerce' ) ) + wc_get_product_tax_class_options(),
		'desc_tip' => true,
	),

	array(
		'title'   => __( 'Rounding', 'woocommerce' ),
		'desc'    => __( 'Round tax at subtotal level, instead of rounding per line', 'woocommerce' ),
		'id'      => 'woocommerce_tax_round_at_subtotal',
		'default' => 'no',
		'type'    => 'checkbox',
	),

	array(
		'title'     => __( 'Additional tax classes', 'woocommerce' ),
		'desc_tip'  => __( 'List additional tax classes you need below (1 per line, e.g. Reduced Rates). These are in addition to "Standard rate" which exists by default.', 'woocommerce' ),
		'id'        => 'woocommerce_tax_classes',
		'css'       => 'height: 65px;',
		'type'      => 'textarea',
		'default'   => '',
		'is_option' => false,
		'value'     => implode( "\n", WC_Tax::get_tax_classes() ),
	),

	array(
		'title'   => __( 'Display prices in the shop', 'woocommerce' ),
		'id'      => 'woocommerce_tax_display_shop',
		'default' => 'excl',
		'type'    => 'select',
		'class'   => 'wc-enhanced-select',
		'options' => array(
			'incl' => __( 'Including tax', 'woocommerce' ),
			'excl' => __( 'Excluding tax', 'woocommerce' ),
		),
	),

	array(
		'title'   => __( 'Display prices during cart and checkout', 'woocommerce' ),
		'id'      => 'woocommerce_tax_display_cart',
		'default' => 'excl',
		'type'    => 'select',
		'class'   => 'wc-enhanced-select',
		'options' => array(
			'incl' => __( 'Including tax', 'woocommerce' ),
			'excl' => __( 'Excluding tax', 'woocommerce' ),
		),
	),

	array(
		'title'       => __( 'Price display suffix', 'woocommerce' ),
		'id'          => 'woocommerce_price_display_suffix',
		'default'     => '',
		'placeholder' => __( 'N/A', 'woocommerce' ),
		'type'        => 'text',
		'desc_tip'    => __( 'Define text to show after your product prices. This could be, for example, "inc. Vat" to explain your pricing. You can also have prices substituted here using one of the following: {price_including_tax}, {price_excluding_tax}.', 'woocommerce' ),
	),

	array(
		'title'    => __( 'Display tax totals', 'woocommerce' ),
		'id'       => 'woocommerce_tax_total_display',
		'default'  => 'itemized',
		'type'     => 'select',
		'class'    => 'wc-enhanced-select',
		'options'  => array(
			'single'   => __( 'As a single total', 'woocommerce' ),
			'itemized' => __( 'Itemized', 'woocommerce' ),
		),
		'autoload' => false,
	),

	array(
		'type' => 'sectionend',
		'id'   => 'tax_options',
	),

);

if ( ! wc_shipping_enabled() ) {
	unset( $settings['shipping-tax-class'] );
}

return apply_filters( 'woocommerce_tax_settings', $settings );
admin/settings/views/html-settings-tax.php000064400000017173151336044770014761 0ustar00<?php
/**
 * Admin view: Settings tax
 *
 * @package WooCommerce\Admin\Settings
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>

<div class="wc-tax-rates-search" id="rates-search">
	<input type="search" class="wc-tax-rates-search-field" placeholder="<?php esc_attr_e( 'Search&hellip;', 'woocommerce' ); ?>" value="<?php echo isset( $_GET['s'] ) ? esc_attr( $_GET['s'] ) : ''; ?>" />
</div>

<div id="rates-pagination"></div>

<h3>
	<?php
	/* translators: %s: tax rate */
	printf(
		__( '"%s" tax rates', 'woocommerce' ),
		$current_class ? esc_html( $current_class ) : __( 'Standard', 'woocommerce' )
	);
	?>
</h3>

<table class="wc_tax_rates wc_input_table widefat">
	<thead>
		<tr>
			<th width="8%"><a href="https://en.wikipedia.org/wiki/ISO_3166-1#Current_codes" target="_blank"><?php _e( 'Country&nbsp;code', 'woocommerce' ); ?></a>&nbsp;<?php echo wc_help_tip( __( 'A 2 digit country code, e.g. US. Leave blank to apply to all.', 'woocommerce' ) ); ?></th>
			<th width="8%"><?php _e( 'State code', 'woocommerce' ); ?>&nbsp;<?php echo wc_help_tip( __( 'A 2 digit state code, e.g. AL. Leave blank to apply to all.', 'woocommerce' ) ); ?></th>
			<th><?php _e( 'Postcode / ZIP', 'woocommerce' ); ?>&nbsp;<?php echo wc_help_tip( __( 'Postcode for this rule. Semi-colon (;) separate multiple values. Leave blank to apply to all areas. Wildcards (*) and ranges for numeric postcodes (e.g. 12345...12350) can also be used.', 'woocommerce' ) ); ?></th>
			<th><?php _e( 'City', 'woocommerce' ); ?>&nbsp;<?php echo wc_help_tip( __( 'Cities for this rule. Semi-colon (;) separate multiple values. Leave blank to apply to all cities.', 'woocommerce' ) ); ?></th>
			<th width="8%"><?php _e( 'Rate&nbsp;%', 'woocommerce' ); ?>&nbsp;<?php echo wc_help_tip( __( 'Enter a tax rate (percentage) to 4 decimal places.', 'woocommerce' ) ); ?></th>
			<th width="8%"><?php _e( 'Tax name', 'woocommerce' ); ?>&nbsp;<?php echo wc_help_tip( __( 'Enter a name for this tax rate.', 'woocommerce' ) ); ?></th>
			<th width="8%"><?php _e( 'Priority', 'woocommerce' ); ?>&nbsp;<?php echo wc_help_tip( __( 'Choose a priority for this tax rate. Only 1 matching rate per priority will be used. To define multiple tax rates for a single area you need to specify a different priority per rate.', 'woocommerce' ) ); ?></th>
			<th width="8%"><?php _e( 'Compound', 'woocommerce' ); ?>&nbsp;<?php echo wc_help_tip( __( 'Choose whether or not this is a compound rate. Compound tax rates are applied on top of other tax rates.', 'woocommerce' ) ); ?></th>
			<th width="8%"><?php _e( 'Shipping', 'woocommerce' ); ?>&nbsp;<?php echo wc_help_tip( __( 'Choose whether or not this tax rate also gets applied to shipping.', 'woocommerce' ) ); ?></th>
		</tr>
	</thead>
	<tfoot>
		<tr>
			<th colspan="9">
				<a href="#" class="button plus insert"><?php _e( 'Insert row', 'woocommerce' ); ?></a>
				<a href="#" class="button minus remove_tax_rates"><?php _e( 'Remove selected row(s)', 'woocommerce' ); ?></a>
				<a href="#" download="tax_rates.csv" class="button export"><?php _e( 'Export CSV', 'woocommerce' ); ?></a>
				<a href="<?php echo admin_url( 'admin.php?import=woocommerce_tax_rate_csv' ); ?>" class="button import"><?php _e( 'Import CSV', 'woocommerce' ); ?></a>
			</th>
		</tr>
	</tfoot>
	<tbody id="rates">
		<tr>
			<th colspan="9" style="text-align: center;"><?php esc_html_e( 'Loading&hellip;', 'woocommerce' ); ?></th>
		</tr>
	</tbody>
</table>

<script type="text/html" id="tmpl-wc-tax-table-row">
	<tr class="tips" data-tip="<?php printf( esc_attr__( 'Tax rate ID: %s', 'woocommerce' ), '{{ data.tax_rate_id }}' ); ?>" data-id="{{ data.tax_rate_id }}">
		<td class="country">
			<input type="text" value="{{ data.tax_rate_country }}" placeholder="*" name="tax_rate_country[{{ data.tax_rate_id }}]" class="wc_input_country_iso" data-attribute="tax_rate_country" style="text-transform:uppercase" />
		</td>

		<td class="state">
			<input type="text" value="{{ data.tax_rate_state }}" placeholder="*" name="tax_rate_state[{{ data.tax_rate_id }}]" data-attribute="tax_rate_state" />
		</td>

		<td class="postcode">
			<input type="text" value="<# if ( data.postcode ) print( _.escape( data.postcode.join( '; ' ) ) ); #>" placeholder="*" data-name="tax_rate_postcode[{{ data.tax_rate_id }}]" data-attribute="postcode" />
		</td>

		<td class="city">
			<input type="text" value="<# if ( data.city ) print( _.escape( data.city.join( '; ' ) ) ); #>" placeholder="*" data-name="tax_rate_city[{{ data.tax_rate_id }}]" data-attribute="city" />
		</td>

		<td class="rate">
			<input type="text" value="{{ data.tax_rate }}" placeholder="0" name="tax_rate[{{ data.tax_rate_id }}]" data-attribute="tax_rate" />
		</td>

		<td class="name">
			<input type="text" value="{{ data.tax_rate_name }}" name="tax_rate_name[{{ data.tax_rate_id }}]" data-attribute="tax_rate_name" />
		</td>

		<td class="priority">
			<input type="number" step="1" min="1" value="{{ data.tax_rate_priority }}" name="tax_rate_priority[{{ data.tax_rate_id }}]" data-attribute="tax_rate_priority" />
		</td>

		<td class="compound">
			<input type="checkbox" class="checkbox" name="tax_rate_compound[{{ data.tax_rate_id }}]" <# if ( parseInt( data.tax_rate_compound, 10 ) ) { #> checked="checked" <# } #> data-attribute="tax_rate_compound" />
		</td>

		<td class="apply_to_shipping">
			<input type="checkbox" class="checkbox" name="tax_rate_shipping[{{ data.tax_rate_id }}]" <# if ( parseInt( data.tax_rate_shipping, 10 ) ) { #> checked="checked" <# } #> data-attribute="tax_rate_shipping" />
		</td>
	</tr>
</script>

<script type="text/html" id="tmpl-wc-tax-table-row-empty">
	<tr>
		<th colspan="9" style="text-align:center"><?php esc_html_e( 'No matching tax rates found.', 'woocommerce' ); ?></th>
	</tr>
</script>

<script type="text/html" id="tmpl-wc-tax-table-pagination">
	<div class="tablenav">
		<div class="tablenav-pages">
			<span class="displaying-num">
				<?php
				/* translators: %s: number */
				printf(
					__( '%s items', 'woocommerce' ), // %s will be a number eventually, but must be a string for now.
					'{{ data.qty_rates }}'
				);
				?>
			</span>
			<span class="pagination-links">

				<a class="tablenav-pages-navspan" data-goto="1">
					<span class="screen-reader-text"><?php esc_html_e( 'First page', 'woocommerce' ); ?></span>
					<span aria-hidden="true">&laquo;</span>
				</a>
				<a class="tablenav-pages-navspan" data-goto="<# print( Math.max( 1, parseInt( data.current_page, 10 ) - 1 ) ) #>">
					<span class="screen-reader-text"><?php esc_html_e( 'Previous page', 'woocommerce' ); ?></span>
					<span aria-hidden="true">&lsaquo;</span>
				</a>

				<span class="paging-input">
					<label for="current-page-selector" class="screen-reader-text"><?php esc_html_e( 'Current page', 'woocommerce' ); ?></label>
					<?php
						/* translators: 1: current page 2: total pages */
						printf(
							esc_html_x( '%1$s of %2$s', 'Pagination', 'woocommerce' ),
							'<input class="current-page" id="current-page-selector" type="text" name="paged" value="{{ data.current_page }}" size="<# print( data.qty_pages.toString().length ) #>" aria-describedby="table-paging">',
							'<span class="total-pages">{{ data.qty_pages }}</span>'
						);
					?>
				</span>

				<a class="tablenav-pages-navspan" data-goto="<# print( Math.min( data.qty_pages, parseInt( data.current_page, 10 ) + 1 ) ) #>">
					<span class="screen-reader-text"><?php esc_html_e( 'Next page', 'woocommerce' ); ?></span>
					<span aria-hidden="true">&rsaquo;</span>
				</a>
				<a class="tablenav-pages-navspan" data-goto="{{ data.qty_pages }}">
					<span class="screen-reader-text"><?php esc_html_e( 'Last page', 'woocommerce' ); ?></span>
					<span aria-hidden="true">&raquo;</span>
				</a>

			</span>
		</div>
	</div>
</script>
admin/settings/views/html-webhooks-edit.php000064400000022546151336044770015073 0ustar00<?php
/**
 * Admin View: Edit Webhooks
 *
 * @package WooCommerce\Admin\Webhooks\Views
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>

<input type="hidden" name="webhook_id" value="<?php echo esc_attr( $webhook->get_id() ); ?>" />

<div id="webhook-options" class="settings-panel">
	<h2><?php esc_html_e( 'Webhook data', 'woocommerce' ); ?></h2>
	<table class="form-table">
		<tbody>
			<tr valign="top">
				<th scope="row" class="titledesc">
					<label for="webhook_name">
						<?php esc_html_e( 'Name', 'woocommerce' ); ?>
						<?php
						/* translators: %s: date */
						echo wc_help_tip( sprintf( __( 'Friendly name for identifying this webhook, defaults to Webhook created on %s.', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Webhook created on date parsed by strftime', 'woocommerce' ) ) ) ); // @codingStandardsIgnoreLine
						?>
					</label>
				</th>
				<td class="forminp">
					<input name="webhook_name" id="webhook_name" type="text" class="input-text regular-input" value="<?php echo esc_attr( $webhook->get_name() ); ?>" />
				</td>
			</tr>
			<tr valign="top">
				<th scope="row" class="titledesc">
					<label for="webhook_status">
						<?php esc_html_e( 'Status', 'woocommerce' ); ?>
						<?php echo wc_help_tip( __( 'The options are &quot;Active&quot; (delivers payload), &quot;Paused&quot; (does not deliver), or &quot;Disabled&quot; (does not deliver due delivery failures).', 'woocommerce' ) ); ?>
					</label>
				</th>
				<td class="forminp">
					<select name="webhook_status" id="webhook_status" class="wc-enhanced-select">
						<?php
						$statuses       = wc_get_webhook_statuses();
						$current_status = $webhook->get_status();

						foreach ( $statuses as $status_slug => $status_name ) :
							?>
							<option value="<?php echo esc_attr( $status_slug ); ?>" <?php selected( $current_status, $status_slug, true ); ?>><?php echo esc_html( $status_name ); ?></option>
						<?php endforeach; ?>
					</select>
				</td>
			</tr>
			<tr valign="top">
				<th scope="row" class="titledesc">
					<label for="webhook_topic">
						<?php esc_html_e( 'Topic', 'woocommerce' ); ?>
						<?php echo wc_help_tip( __( 'Select when the webhook will fire.', 'woocommerce' ) ); ?>
					</label>
				</th>
				<td class="forminp">
					<select name="webhook_topic" id="webhook_topic" class="wc-enhanced-select">
						<?php
							$topic_data = WC_Admin_Webhooks::get_topic_data( $webhook );

							$topics = apply_filters(
								'woocommerce_webhook_topics',
								array(
									''                 => __( 'Select an option&hellip;', 'woocommerce' ),
									'coupon.created'   => __( 'Coupon created', 'woocommerce' ),
									'coupon.updated'   => __( 'Coupon updated', 'woocommerce' ),
									'coupon.deleted'   => __( 'Coupon deleted', 'woocommerce' ),
									'coupon.restored'  => __( 'Coupon restored', 'woocommerce' ),
									'customer.created' => __( 'Customer created', 'woocommerce' ),
									'customer.updated' => __( 'Customer updated', 'woocommerce' ),
									'customer.deleted' => __( 'Customer deleted', 'woocommerce' ),
									'order.created'    => __( 'Order created', 'woocommerce' ),
									'order.updated'    => __( 'Order updated', 'woocommerce' ),
									'order.deleted'    => __( 'Order deleted', 'woocommerce' ),
									'order.restored'   => __( 'Order restored', 'woocommerce' ),
									'product.created'  => __( 'Product created', 'woocommerce' ),
									'product.updated'  => __( 'Product updated', 'woocommerce' ),
									'product.deleted'  => __( 'Product deleted', 'woocommerce' ),
									'product.restored' => __( 'Product restored', 'woocommerce' ),
									'action'           => __( 'Action', 'woocommerce' ),
								)
							);

							foreach ( $topics as $topic_slug => $topic_name ) :

								$selected = $topic_slug === $topic_data['topic'] || $topic_slug === $topic_data['resource'] . '.' . $topic_data['event'];

								?>
								<option value="<?php echo esc_attr( $topic_slug ); ?>" <?php selected( $selected, true, true ); ?>><?php echo esc_html( $topic_name ); ?></option>
						<?php endforeach; ?>
					</select>
				</td>
			</tr>
			<tr valign="top" id="webhook-action-event-wrap">
				<th scope="row" class="titledesc">
					<label for="webhook_action_event">
						<?php esc_html_e( 'Action event', 'woocommerce' ); ?>
						<?php echo wc_help_tip( __( 'Enter the action that will trigger this webhook.', 'woocommerce' ) ); ?>
					</label>
				</th>
				<td class="forminp">
					<input name="webhook_action_event" id="webhook_action_event" type="text" class="input-text regular-input" value="<?php echo esc_attr( $topic_data['event'] ); ?>" />
				</td>
			</tr>
			<tr valign="top">
				<th scope="row" class="titledesc">
					<label for="webhook_delivery_url">
						<?php esc_html_e( 'Delivery URL', 'woocommerce' ); ?>
						<?php echo wc_help_tip( __( 'URL where the webhook payload is delivered.', 'woocommerce' ) ); ?>
					</label>
				</th>
				<td class="forminp">
					<input name="webhook_delivery_url" id="webhook_delivery_url" type="text" class="input-text regular-input" value="<?php echo esc_attr( $webhook->get_delivery_url() ); ?>" />
				</td>
			</tr>
			<tr valign="top">
				<th scope="row" class="titledesc">
					<label for="webhook_secret">
						<?php esc_html_e( 'Secret', 'woocommerce' ); ?>
						<?php echo wc_help_tip( __( 'The secret key is used to generate a hash of the delivered webhook and provided in the request headers.', 'woocommerce' ) ); ?>
					</label>
				</th>
				<td class="forminp">
					<input name="webhook_secret" id="webhook_secret" type="text" class="input-text regular-input" value="<?php echo esc_attr( $webhook->get_secret() ); ?>" />
				</td>
			</tr>
			<tr valign="top">
				<th scope="row" class="titledesc">
					<label for="webhook_api_version">
						<?php esc_html_e( 'API Version', 'woocommerce' ); ?>
						<?php echo wc_help_tip( __( 'REST API version used in the webhook deliveries.', 'woocommerce' ) ); ?>
					</label>
				</th>
				<td class="forminp">
					<select name="webhook_api_version" id="webhook_api_version">
						<?php foreach ( array_reverse( wc_get_webhook_rest_api_versions() ) as $version ) : ?>
							<option value="<?php echo esc_attr( $version ); ?>" <?php selected( $version, $webhook->get_api_version(), true ); ?>>
								<?php
									/* translators: %d: rest api version number */
									echo esc_html( sprintf( __( 'WP REST API Integration v%d', 'woocommerce' ), str_replace( 'wp_api_v', '', $version ) ) );
								?>
							</option>
						<?php endforeach; ?>
						<option value="legacy_v3" <?php selected( 'legacy_v3', $webhook->get_api_version(), true ); ?>><?php esc_html_e( 'Legacy API v3 (deprecated)', 'woocommerce' ); ?></option>
					</select>
				</td>
			</tr>
		</tbody>
	</table>

	<?php do_action( 'woocommerce_webhook_options' ); ?>
</div>

<div id="webhook-actions" class="settings-panel">
	<h2><?php esc_html_e( 'Webhook actions', 'woocommerce' ); ?></h2>
	<table class="form-table">
		<tbody>
			<?php if ( $webhook->get_date_created() && '0000-00-00 00:00:00' !== $webhook->get_date_created()->date( 'Y-m-d H:i:s' ) ) : ?>
				<?php if ( is_null( $webhook->get_date_modified() ) ) : ?>
					<tr valign="top">
						<th scope="row" class="titledesc">
							<?php esc_html_e( 'Created at', 'woocommerce' ); ?>
						</th>
						<td class="forminp">
							<?php echo esc_html( date_i18n( __( 'M j, Y @ G:i', 'woocommerce' ), strtotime( $webhook->get_date_created()->date( 'Y-m-d H:i:s' ) ) ) ); ?>
						</td>
					</tr>
				<?php else : ?>
				<tr valign="top">
						<th scope="row" class="titledesc">
							<?php esc_html_e( 'Created at', 'woocommerce' ); ?>
						</th>
						<td class="forminp">
							<?php echo esc_html( date_i18n( __( 'M j, Y @ G:i', 'woocommerce' ), strtotime( $webhook->get_date_created()->date( 'Y-m-d H:i:s' ) ) ) ); ?>
						</td>
					</tr>
				<tr valign="top">
						<th scope="row" class="titledesc">
							<?php esc_html_e( 'Updated at', 'woocommerce' ); ?>
						</th>
						<td class="forminp">
							<?php echo esc_html( date_i18n( __( 'M j, Y @ G:i', 'woocommerce' ), strtotime( $webhook->get_date_modified()->date( 'Y-m-d H:i:s' ) ) ) ); ?>
						</td>
					</tr>
				<?php endif; ?>
			<?php endif; ?>
			<tr valign="top">
				<td colspan="2" scope="row" style="padding-left: 0;">
					<p class="submit">
						<button type="submit" class="button button-primary button-large" name="save" id="publish" accesskey="p"><?php esc_html_e( 'Save webhook', 'woocommerce' ); ?></button>
						<?php
						if ( $webhook->get_id() ) :
							$delete_url = wp_nonce_url(
								add_query_arg(
									array(
										'delete' => $webhook->get_id(),
									),
									admin_url( 'admin.php?page=wc-settings&tab=advanced&section=webhooks' )
								),
								'delete-webhook'
							);
							?>
							<a style="color: #a00; text-decoration: none; margin-left: 10px;" href="<?php echo esc_url( $delete_url ); ?>"><?php esc_html_e( 'Delete permanently', 'woocommerce' ); ?></a>
						<?php endif; ?>
					</p>
				</td>
			</tr>
		</tbody>
	</table>
</div>

<script type="text/javascript">
	jQuery( function ( $ ) {
		$( '#webhook-options' ).find( '#webhook_topic' ).on( 'change', function() {
			var current            = $( this ).val(),
				action_event_field = $( '#webhook-options' ).find( '#webhook-action-event-wrap' );

			action_event_field.hide();

			if ( 'action' === current ) {
				action_event_field.show();
			}
		}).trigger( 'change' );
	});
</script>
admin/settings/views/class-wc-settings-rest-api.php000064400000000377151336044770016457 0ustar00<?php // @codingStandardsIgnoreFile.
/**
 * Settings class file.
 *
 * @deprecated 3.4.0 Replaced with class-wc-settings-advanced.php.
 * @todo remove in 4.0.
 */

defined( 'ABSPATH' ) || exit;

return include __DIR__ . '/class-wc-settings-advanced.php';
admin/settings/class-wc-settings-accounts.php000064400000023525151336044770015415 0ustar00<?php
/**
 * WooCommerce Account Settings.
 *
 * @package WooCommerce\Admin
 */

defined( 'ABSPATH' ) || exit;

if ( class_exists( 'WC_Settings_Accounts', false ) ) {
	return new WC_Settings_Accounts();
}

/**
 * WC_Settings_Accounts.
 */
class WC_Settings_Accounts extends WC_Settings_Page {

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->id    = 'account';
		$this->label = __( 'Accounts &amp; Privacy', 'woocommerce' );
		parent::__construct();
	}

	/**
	 * Get settings array.
	 *
	 * @return array
	 */
	protected function get_settings_for_default_section() {

		$erasure_text = esc_html__( 'account erasure request', 'woocommerce' );
		$privacy_text = esc_html__( 'privacy page', 'woocommerce' );
		if ( current_user_can( 'manage_privacy_options' ) ) {
			if ( version_compare( get_bloginfo( 'version' ), '5.3', '<' ) ) {
				$erasure_text = sprintf( '<a href="%s">%s</a>', esc_url( admin_url( 'tools.php?page=remove_personal_data' ) ), $erasure_text );
			} else {
				$erasure_text = sprintf( '<a href="%s">%s</a>', esc_url( admin_url( 'erase-personal-data.php' ) ), $erasure_text );
			}
			$privacy_text = sprintf( '<a href="%s">%s</a>', esc_url( admin_url( 'options-privacy.php' ) ), $privacy_text );
		}

		$account_settings = array(
			array(
				'title' => '',
				'type'  => 'title',
				'id'    => 'account_registration_options',
			),
			array(
				'title'         => __( 'Guest checkout', 'woocommerce' ),
				'desc'          => __( 'Allow customers to place orders without an account', 'woocommerce' ),
				'id'            => 'woocommerce_enable_guest_checkout',
				'default'       => 'yes',
				'type'          => 'checkbox',
				'checkboxgroup' => 'start',
				'autoload'      => false,
			),
			array(
				'title'         => __( 'Login', 'woocommerce' ),
				'desc'          => __( 'Allow customers to log into an existing account during checkout', 'woocommerce' ),
				'id'            => 'woocommerce_enable_checkout_login_reminder',
				'default'       => 'no',
				'type'          => 'checkbox',
				'checkboxgroup' => 'end',
				'autoload'      => false,
			),
			array(
				'title'         => __( 'Account creation', 'woocommerce' ),
				'desc'          => __( 'Allow customers to create an account during checkout', 'woocommerce' ),
				'id'            => 'woocommerce_enable_signup_and_login_from_checkout',
				'default'       => 'no',
				'type'          => 'checkbox',
				'checkboxgroup' => 'start',
				'autoload'      => false,
			),
			array(
				'desc'          => __( 'Allow customers to create an account on the "My account" page', 'woocommerce' ),
				'id'            => 'woocommerce_enable_myaccount_registration',
				'default'       => 'no',
				'type'          => 'checkbox',
				'checkboxgroup' => '',
				'autoload'      => false,
			),
			array(
				'desc'          => __( 'When creating an account, automatically generate an account username for the customer based on their name, surname or email', 'woocommerce' ),
				'id'            => 'woocommerce_registration_generate_username',
				'default'       => 'yes',
				'type'          => 'checkbox',
				'checkboxgroup' => '',
				'autoload'      => false,
			),
			array(
				'desc'          => __( 'When creating an account, automatically generate an account password', 'woocommerce' ),
				'id'            => 'woocommerce_registration_generate_password',
				'default'       => 'yes',
				'type'          => 'checkbox',
				'checkboxgroup' => 'end',
				'autoload'      => false,
			),
			array(
				'title'         => __( 'Account erasure requests', 'woocommerce' ),
				'desc'          => __( 'Remove personal data from orders on request', 'woocommerce' ),
				/* Translators: %s URL to erasure request screen. */
				'desc_tip'      => sprintf( esc_html__( 'When handling an %s, should personal data within orders be retained or removed?', 'woocommerce' ), $erasure_text ),
				'id'            => 'woocommerce_erasure_request_removes_order_data',
				'type'          => 'checkbox',
				'default'       => 'no',
				'checkboxgroup' => 'start',
				'autoload'      => false,
			),
			array(
				'desc'          => __( 'Remove access to downloads on request', 'woocommerce' ),
				/* Translators: %s URL to erasure request screen. */
				'desc_tip'      => sprintf( esc_html__( 'When handling an %s, should access to downloadable files be revoked and download logs cleared?', 'woocommerce' ), $erasure_text ),
				'id'            => 'woocommerce_erasure_request_removes_download_data',
				'type'          => 'checkbox',
				'default'       => 'no',
				'checkboxgroup' => 'end',
				'autoload'      => false,
			),
			array(
				'title'         => __( 'Personal data removal', 'woocommerce' ),
				'desc'          => __( 'Allow personal data to be removed in bulk from orders', 'woocommerce' ),
				'desc_tip'      => __( 'Adds an option to the orders screen for removing personal data in bulk. Note that removing personal data cannot be undone.', 'woocommerce' ),
				'id'            => 'woocommerce_allow_bulk_remove_personal_data',
				'type'          => 'checkbox',
				'checkboxgroup' => 'start',
				'default'       => 'no',
				'autoload'      => false,
			),
			array(
				'type' => 'sectionend',
				'id'   => 'account_registration_options',
			),
			array(
				'title' => __( 'Privacy policy', 'woocommerce' ),
				'type'  => 'title',
				'id'    => 'privacy_policy_options',
				/* translators: %s: privacy page link. */
				'desc'  => sprintf( esc_html__( 'This section controls the display of your website privacy policy. The privacy notices below will not show up unless a %s is set.', 'woocommerce' ), $privacy_text ),
			),

			array(
				'title'    => __( 'Registration privacy policy', 'woocommerce' ),
				'desc_tip' => __( 'Optionally add some text about your store privacy policy to show on account registration forms.', 'woocommerce' ),
				'id'       => 'woocommerce_registration_privacy_policy_text',
				/* translators: %s privacy policy page name and link */
				'default'  => sprintf( __( 'Your personal data will be used to support your experience throughout this website, to manage access to your account, and for other purposes described in our %s.', 'woocommerce' ), '[privacy_policy]' ),
				'type'     => 'textarea',
				'css'      => 'min-width: 50%; height: 75px;',
			),

			array(
				'title'    => __( 'Checkout privacy policy', 'woocommerce' ),
				'desc_tip' => __( 'Optionally add some text about your store privacy policy to show during checkout.', 'woocommerce' ),
				'id'       => 'woocommerce_checkout_privacy_policy_text',
				/* translators: %s privacy policy page name and link */
				'default'  => sprintf( __( 'Your personal data will be used to process your order, support your experience throughout this website, and for other purposes described in our %s.', 'woocommerce' ), '[privacy_policy]' ),
				'type'     => 'textarea',
				'css'      => 'min-width: 50%; height: 75px;',
			),
			array(
				'type' => 'sectionend',
				'id'   => 'privacy_policy_options',
			),
			array(
				'title' => __( 'Personal data retention', 'woocommerce' ),
				'desc'  => __( 'Choose how long to retain personal data when it\'s no longer needed for processing. Leave the following options blank to retain this data indefinitely.', 'woocommerce' ),
				'type'  => 'title',
				'id'    => 'personal_data_retention',
			),
			array(
				'title'       => __( 'Retain inactive accounts ', 'woocommerce' ),
				'desc_tip'    => __( 'Inactive accounts are those which have not logged in, or placed an order, for the specified duration. They will be deleted. Any orders will be converted into guest orders.', 'woocommerce' ),
				'id'          => 'woocommerce_delete_inactive_accounts',
				'type'        => 'relative_date_selector',
				'placeholder' => __( 'N/A', 'woocommerce' ),
				'default'     => array(
					'number' => '',
					'unit'   => 'months',
				),
				'autoload'    => false,
			),
			array(
				'title'       => __( 'Retain pending orders ', 'woocommerce' ),
				'desc_tip'    => __( 'Pending orders are unpaid and may have been abandoned by the customer. They will be trashed after the specified duration.', 'woocommerce' ),
				'id'          => 'woocommerce_trash_pending_orders',
				'type'        => 'relative_date_selector',
				'placeholder' => __( 'N/A', 'woocommerce' ),
				'default'     => '',
				'autoload'    => false,
			),
			array(
				'title'       => __( 'Retain failed orders', 'woocommerce' ),
				'desc_tip'    => __( 'Failed orders are unpaid and may have been abandoned by the customer. They will be trashed after the specified duration.', 'woocommerce' ),
				'id'          => 'woocommerce_trash_failed_orders',
				'type'        => 'relative_date_selector',
				'placeholder' => __( 'N/A', 'woocommerce' ),
				'default'     => '',
				'autoload'    => false,
			),
			array(
				'title'       => __( 'Retain cancelled orders', 'woocommerce' ),
				'desc_tip'    => __( 'Cancelled orders are unpaid and may have been cancelled by the store owner or customer. They will be trashed after the specified duration.', 'woocommerce' ),
				'id'          => 'woocommerce_trash_cancelled_orders',
				'type'        => 'relative_date_selector',
				'placeholder' => __( 'N/A', 'woocommerce' ),
				'default'     => '',
				'autoload'    => false,
			),
			array(
				'title'       => __( 'Retain completed orders', 'woocommerce' ),
				'desc_tip'    => __( 'Retain completed orders for a specified duration before anonymizing the personal data within them.', 'woocommerce' ),
				'id'          => 'woocommerce_anonymize_completed_orders',
				'type'        => 'relative_date_selector',
				'placeholder' => __( 'N/A', 'woocommerce' ),
				'default'     => array(
					'number' => '',
					'unit'   => 'months',
				),
				'autoload'    => false,
			),
			array(
				'type' => 'sectionend',
				'id'   => 'personal_data_retention',
			),
		);

		return apply_filters(
			'woocommerce_' . $this->id . '_settings',
			$account_settings
		);
	}
}

return new WC_Settings_Accounts();
admin/settings/class-wc-settings-general.php000064400000024547151336044770015220 0ustar00<?php
/**
 * WooCommerce General Settings
 *
 * @package WooCommerce\Admin
 */

defined( 'ABSPATH' ) || exit;

if ( class_exists( 'WC_Settings_General', false ) ) {
	return new WC_Settings_General();
}

/**
 * WC_Admin_Settings_General.
 */
class WC_Settings_General extends WC_Settings_Page {

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->id    = 'general';
		$this->label = __( 'General', 'woocommerce' );

		parent::__construct();
	}

	/**
	 * Get settings or the default section.
	 *
	 * @return array
	 */
	protected function get_settings_for_default_section() {

		$currency_code_options = get_woocommerce_currencies();

		foreach ( $currency_code_options as $code => $name ) {
			$currency_code_options[ $code ] = $name . ' (' . get_woocommerce_currency_symbol( $code ) . ')';
		}

		$settings =
			array(

				array(
					'title' => __( 'Store Address', 'woocommerce' ),
					'type'  => 'title',
					'desc'  => __( 'This is where your business is located. Tax rates and shipping rates will use this address.', 'woocommerce' ),
					'id'    => 'store_address',
				),

				array(
					'title'    => __( 'Address line 1', 'woocommerce' ),
					'desc'     => __( 'The street address for your business location.', 'woocommerce' ),
					'id'       => 'woocommerce_store_address',
					'default'  => '',
					'type'     => 'text',
					'desc_tip' => true,
				),

				array(
					'title'    => __( 'Address line 2', 'woocommerce' ),
					'desc'     => __( 'An additional, optional address line for your business location.', 'woocommerce' ),
					'id'       => 'woocommerce_store_address_2',
					'default'  => '',
					'type'     => 'text',
					'desc_tip' => true,
				),

				array(
					'title'    => __( 'City', 'woocommerce' ),
					'desc'     => __( 'The city in which your business is located.', 'woocommerce' ),
					'id'       => 'woocommerce_store_city',
					'default'  => '',
					'type'     => 'text',
					'desc_tip' => true,
				),

				array(
					'title'    => __( 'Country / State', 'woocommerce' ),
					'desc'     => __( 'The country and state or province, if any, in which your business is located.', 'woocommerce' ),
					'id'       => 'woocommerce_default_country',
					'default'  => 'US:CA',
					'type'     => 'single_select_country',
					'desc_tip' => true,
				),

				array(
					'title'    => __( 'Postcode / ZIP', 'woocommerce' ),
					'desc'     => __( 'The postal code, if any, in which your business is located.', 'woocommerce' ),
					'id'       => 'woocommerce_store_postcode',
					'css'      => 'min-width:50px;',
					'default'  => '',
					'type'     => 'text',
					'desc_tip' => true,
				),

				array(
					'type' => 'sectionend',
					'id'   => 'store_address',
				),

				array(
					'title' => __( 'General options', 'woocommerce' ),
					'type'  => 'title',
					'desc'  => '',
					'id'    => 'general_options',
				),

				array(
					'title'    => __( 'Selling location(s)', 'woocommerce' ),
					'desc'     => __( 'This option lets you limit which countries you are willing to sell to.', 'woocommerce' ),
					'id'       => 'woocommerce_allowed_countries',
					'default'  => 'all',
					'type'     => 'select',
					'class'    => 'wc-enhanced-select',
					'css'      => 'min-width: 350px;',
					'desc_tip' => true,
					'options'  => array(
						'all'        => __( 'Sell to all countries', 'woocommerce' ),
						'all_except' => __( 'Sell to all countries, except for&hellip;', 'woocommerce' ),
						'specific'   => __( 'Sell to specific countries', 'woocommerce' ),
					),
				),

				array(
					'title'   => __( 'Sell to all countries, except for&hellip;', 'woocommerce' ),
					'desc'    => '',
					'id'      => 'woocommerce_all_except_countries',
					'css'     => 'min-width: 350px;',
					'default' => '',
					'type'    => 'multi_select_countries',
				),

				array(
					'title'   => __( 'Sell to specific countries', 'woocommerce' ),
					'desc'    => '',
					'id'      => 'woocommerce_specific_allowed_countries',
					'css'     => 'min-width: 350px;',
					'default' => '',
					'type'    => 'multi_select_countries',
				),

				array(
					'title'    => __( 'Shipping location(s)', 'woocommerce' ),
					'desc'     => __( 'Choose which countries you want to ship to, or choose to ship to all locations you sell to.', 'woocommerce' ),
					'id'       => 'woocommerce_ship_to_countries',
					'default'  => '',
					'type'     => 'select',
					'class'    => 'wc-enhanced-select',
					'desc_tip' => true,
					'options'  => array(
						''         => __( 'Ship to all countries you sell to', 'woocommerce' ),
						'all'      => __( 'Ship to all countries', 'woocommerce' ),
						'specific' => __( 'Ship to specific countries only', 'woocommerce' ),
						'disabled' => __( 'Disable shipping &amp; shipping calculations', 'woocommerce' ),
					),
				),

				array(
					'title'   => __( 'Ship to specific countries', 'woocommerce' ),
					'desc'    => '',
					'id'      => 'woocommerce_specific_ship_to_countries',
					'css'     => '',
					'default' => '',
					'type'    => 'multi_select_countries',
				),

				array(
					'title'    => __( 'Default customer location', 'woocommerce' ),
					'id'       => 'woocommerce_default_customer_address',
					'desc_tip' => __( 'This option determines a customers default location. The MaxMind GeoLite Database will be periodically downloaded to your wp-content directory if using geolocation.', 'woocommerce' ),
					'default'  => 'base',
					'type'     => 'select',
					'class'    => 'wc-enhanced-select',
					'options'  => array(
						''                 => __( 'No location by default', 'woocommerce' ),
						'base'             => __( 'Shop base address', 'woocommerce' ),
						'geolocation'      => __( 'Geolocate', 'woocommerce' ),
						'geolocation_ajax' => __( 'Geolocate (with page caching support)', 'woocommerce' ),
					),
				),

				array(
					'title'    => __( 'Enable taxes', 'woocommerce' ),
					'desc'     => __( 'Enable tax rates and calculations', 'woocommerce' ),
					'id'       => 'woocommerce_calc_taxes',
					'default'  => 'no',
					'type'     => 'checkbox',
					'desc_tip' => __( 'Rates will be configurable and taxes will be calculated during checkout.', 'woocommerce' ),
				),

				array(
					'title'           => __( 'Enable coupons', 'woocommerce' ),
					'desc'            => __( 'Enable the use of coupon codes', 'woocommerce' ),
					'id'              => 'woocommerce_enable_coupons',
					'default'         => 'yes',
					'type'            => 'checkbox',
					'checkboxgroup'   => 'start',
					'show_if_checked' => 'option',
					'desc_tip'        => __( 'Coupons can be applied from the cart and checkout pages.', 'woocommerce' ),
				),

				array(
					'desc'            => __( 'Calculate coupon discounts sequentially', 'woocommerce' ),
					'id'              => 'woocommerce_calc_discounts_sequentially',
					'default'         => 'no',
					'type'            => 'checkbox',
					'desc_tip'        => __( 'When applying multiple coupons, apply the first coupon to the full price and the second coupon to the discounted price and so on.', 'woocommerce' ),
					'show_if_checked' => 'yes',
					'checkboxgroup'   => 'end',
					'autoload'        => false,
				),

				array(
					'type' => 'sectionend',
					'id'   => 'general_options',
				),

				array(
					'title' => __( 'Currency options', 'woocommerce' ),
					'type'  => 'title',
					'desc'  => __( 'The following options affect how prices are displayed on the frontend.', 'woocommerce' ),
					'id'    => 'pricing_options',
				),

				array(
					'title'    => __( 'Currency', 'woocommerce' ),
					'desc'     => __( 'This controls what currency prices are listed at in the catalog and which currency gateways will take payments in.', 'woocommerce' ),
					'id'       => 'woocommerce_currency',
					'default'  => 'USD',
					'type'     => 'select',
					'class'    => 'wc-enhanced-select',
					'desc_tip' => true,
					'options'  => $currency_code_options,
				),

				array(
					'title'    => __( 'Currency position', 'woocommerce' ),
					'desc'     => __( 'This controls the position of the currency symbol.', 'woocommerce' ),
					'id'       => 'woocommerce_currency_pos',
					'class'    => 'wc-enhanced-select',
					'default'  => 'left',
					'type'     => 'select',
					'options'  => array(
						'left'        => __( 'Left', 'woocommerce' ),
						'right'       => __( 'Right', 'woocommerce' ),
						'left_space'  => __( 'Left with space', 'woocommerce' ),
						'right_space' => __( 'Right with space', 'woocommerce' ),
					),
					'desc_tip' => true,
				),

				array(
					'title'    => __( 'Thousand separator', 'woocommerce' ),
					'desc'     => __( 'This sets the thousand separator of displayed prices.', 'woocommerce' ),
					'id'       => 'woocommerce_price_thousand_sep',
					'css'      => 'width:50px;',
					'default'  => ',',
					'type'     => 'text',
					'desc_tip' => true,
				),

				array(
					'title'    => __( 'Decimal separator', 'woocommerce' ),
					'desc'     => __( 'This sets the decimal separator of displayed prices.', 'woocommerce' ),
					'id'       => 'woocommerce_price_decimal_sep',
					'css'      => 'width:50px;',
					'default'  => '.',
					'type'     => 'text',
					'desc_tip' => true,
				),

				array(
					'title'             => __( 'Number of decimals', 'woocommerce' ),
					'desc'              => __( 'This sets the number of decimal points shown in displayed prices.', 'woocommerce' ),
					'id'                => 'woocommerce_price_num_decimals',
					'css'               => 'width:50px;',
					'default'           => '2',
					'desc_tip'          => true,
					'type'              => 'number',
					'custom_attributes' => array(
						'min'  => 0,
						'step' => 1,
					),
				),

				array(
					'type' => 'sectionend',
					'id'   => 'pricing_options',
				),
			);

		return apply_filters( 'woocommerce_general_settings', $settings );
	}

	/**
	 * Output a color picker input box.
	 *
	 * @param mixed  $name Name of input.
	 * @param string $id ID of input.
	 * @param mixed  $value Value of input.
	 * @param string $desc (default: '') Description for input.
	 */
	public function color_picker( $name, $id, $value, $desc = '' ) {
		echo '<div class="color_box">' . wc_help_tip( $desc ) . '
			<input name="' . esc_attr( $id ) . '" id="' . esc_attr( $id ) . '" type="text" value="' . esc_attr( $value ) . '" class="colorpick" /> <div id="colorPickerDiv_' . esc_attr( $id ) . '" class="colorpickdiv"></div>
		</div>';
	}
}

return new WC_Settings_General();
admin/settings/class-wc-settings-checkout.php000064400000000417151336044770015376 0ustar00<?php // @codingStandardsIgnoreFile.
/**
 * Settings class file.
 *
 * @deprecated 3.4.0 Replaced with class-wc-settings-payment-gateways.php.
 * @todo remove in 4.0.
 */

defined( 'ABSPATH' ) || exit;

return include __DIR__ . '/class-wc-settings-payment-gateways.php';
admin/settings/class-wc-settings-payment-gateways.php000064400000021056151336044770017072 0ustar00<?php // @codingStandardsIgnoreLine.
/**
 * WooCommerce Checkout Settings
 *
 * @package WooCommerce\Admin
 */

defined( 'ABSPATH' ) || exit;

if ( class_exists( 'WC_Settings_Payment_Gateways', false ) ) {
	return new WC_Settings_Payment_Gateways();
}

/**
 * WC_Settings_Payment_Gateways.
 */
class WC_Settings_Payment_Gateways extends WC_Settings_Page {

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->id    = 'checkout'; // @todo In future versions this may make more sense as 'payment' however to avoid breakage lets leave this alone until we refactor settings APIs in general.
		$this->label = _x( 'Payments', 'Settings tab label', 'woocommerce' );

		add_action( 'woocommerce_admin_field_payment_gateways', array( $this, 'payment_gateways_setting' ) );
		parent::__construct();
	}

	/**
	 * Get own sections.
	 *
	 * @return array
	 */
	protected function get_own_sections() {
		return array(
			'' => __( 'Payment methods', 'woocommerce' ),
		);
	}

	/**
	 * Get settings array.
	 *
	 * @return array
	 */
	protected function get_settings_for_default_section() {
		$settings =
			array(
				array(
					'title' => __( 'Payment methods', 'woocommerce' ),
					'desc'  => __( 'Installed payment methods are listed below and can be sorted to control their display order on the frontend.', 'woocommerce' ),
					'type'  => 'title',
					'id'    => 'payment_gateways_options',
				),
				array(
					'type' => 'payment_gateways',
				),
				array(
					'type' => 'sectionend',
					'id'   => 'payment_gateways_options',
				),
			);

		return apply_filters( 'woocommerce_payment_gateways_settings', $settings );
	}

	/**
	 * Output the settings.
	 */
	public function output() {
		//phpcs:disable WordPress.Security.NonceVerification.Recommended
		global $current_section;

		// Load gateways so we can show any global options they may have.
		$payment_gateways = WC()->payment_gateways->payment_gateways();

		if ( $current_section ) {
			foreach ( $payment_gateways as $gateway ) {
				if ( in_array( $current_section, array( $gateway->id, sanitize_title( get_class( $gateway ) ) ), true ) ) {
					if ( isset( $_GET['toggle_enabled'] ) ) {
						$enabled = $gateway->get_option( 'enabled' );

						if ( $enabled ) {
							$gateway->settings['enabled'] = wc_string_to_bool( $enabled ) ? 'no' : 'yes';
						}
					}
					$this->run_gateway_admin_options( $gateway );
					break;
				}
			}
		}

		parent::output();
		//phpcs:enable
	}

	/**
	 * Run the 'admin_options' method on a given gateway.
	 * This method exists to easy unit testing.
	 *
	 * @param object $gateway The gateway object to run the method on.
	 */
	protected function run_gateway_admin_options( $gateway ) {
		$gateway->admin_options();
	}

	/**
	 * Output payment gateway settings.
	 */
	public function payment_gateways_setting() {
		?>
		<tr valign="top">
		<td class="wc_payment_gateways_wrapper" colspan="2">
			<table class="wc_gateways widefat" cellspacing="0" aria-describedby="payment_gateways_options-description">
				<thead>
					<tr>
						<?php
						$default_columns = array(
							'sort'        => '',
							'name'        => __( 'Method', 'woocommerce' ),
							'status'      => __( 'Enabled', 'woocommerce' ),
							'description' => __( 'Description', 'woocommerce' ),
							'action'      => '',
						);

						$columns = apply_filters( 'woocommerce_payment_gateways_setting_columns', $default_columns );

						foreach ( $columns as $key => $column ) {
							echo '<th class="' . esc_attr( $key ) . '">' . esc_html( $column ) . '</th>';
						}
						?>
						</tr>
					</thead>
					<tbody>
						<?php
						foreach ( WC()->payment_gateways->payment_gateways() as $gateway ) {

							echo '<tr data-gateway_id="' . esc_attr( $gateway->id ) . '">';

							foreach ( $columns as $key => $column ) {
								if ( ! array_key_exists( $key, $default_columns ) ) {
									do_action( 'woocommerce_payment_gateways_setting_column_' . $key, $gateway );
									continue;
								}

								$width = '';

								if ( in_array( $key, array( 'sort', 'status', 'action' ), true ) ) {
									$width = '1%';
								}

								$method_title = $gateway->get_method_title() ? $gateway->get_method_title() : $gateway->get_title();
								$custom_title = $gateway->get_title();

								echo '<td class="' . esc_attr( $key ) . '" width="' . esc_attr( $width ) . '">';

								switch ( $key ) {
									case 'sort':
										?>
										<div class="wc-item-reorder-nav">
											<button type="button" class="wc-move-up" tabindex="0" aria-hidden="false" aria-label="<?php /* Translators: %s Payment gateway name. */ echo esc_attr( sprintf( __( 'Move the "%s" payment method up', 'woocommerce' ), $method_title ) ); ?>"><?php esc_html_e( 'Move up', 'woocommerce' ); ?></button>
											<button type="button" class="wc-move-down" tabindex="0" aria-hidden="false" aria-label="<?php /* Translators: %s Payment gateway name. */ echo esc_attr( sprintf( __( 'Move the "%s" payment method down', 'woocommerce' ), $method_title ) ); ?>"><?php esc_html_e( 'Move down', 'woocommerce' ); ?></button>
											<input type="hidden" name="gateway_order[]" value="<?php echo esc_attr( $gateway->id ); ?>" />
										</div>
										<?php
										break;
									case 'name':
										echo '<a href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=checkout&section=' . strtolower( $gateway->id ) ) ) . '" class="wc-payment-gateway-method-title">' . wp_kses_post( $method_title ) . '</a>';

										if ( $method_title !== $custom_title ) {
											echo '<span class="wc-payment-gateway-method-name">&nbsp;&ndash;&nbsp;' . wp_kses_post( $custom_title ) . '</span>';
										}
										break;
									case 'description':
										echo wp_kses_post( $gateway->get_method_description() );
										break;
									case 'action':
										if ( wc_string_to_bool( $gateway->enabled ) ) {
											/* Translators: %s Payment gateway name. */
											echo '<a class="button alignright" aria-label="' . esc_attr( sprintf( __( 'Manage the "%s" payment method', 'woocommerce' ), $method_title ) ) . '" href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=checkout&section=' . strtolower( $gateway->id ) ) ) . '">' . esc_html__( 'Manage', 'woocommerce' ) . '</a>';
										} else {
											/* Translators: %s Payment gateway name. */
											echo '<a class="button alignright" aria-label="' . esc_attr( sprintf( __( 'Set up the "%s" payment method', 'woocommerce' ), $method_title ) ) . '" href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=checkout&section=' . strtolower( $gateway->id ) ) ) . '">' . esc_html__( 'Set up', 'woocommerce' ) . '</a>';
										}
										break;
									case 'status':
										echo '<a class="wc-payment-gateway-method-toggle-enabled" href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=checkout&section=' . strtolower( $gateway->id ) ) ) . '">';
										if ( wc_string_to_bool( $gateway->enabled ) ) {
											/* Translators: %s Payment gateway name. */
											echo '<span class="woocommerce-input-toggle woocommerce-input-toggle--enabled" aria-label="' . esc_attr( sprintf( __( 'The "%s" payment method is currently enabled', 'woocommerce' ), $method_title ) ) . '">' . esc_attr__( 'Yes', 'woocommerce' ) . '</span>';
										} else {
											/* Translators: %s Payment gateway name. */
											echo '<span class="woocommerce-input-toggle woocommerce-input-toggle--disabled" aria-label="' . esc_attr( sprintf( __( 'The "%s" payment method is currently disabled', 'woocommerce' ), $method_title ) ) . '">' . esc_attr__( 'No', 'woocommerce' ) . '</span>';
										}
										echo '</a>';
										break;
								}

								echo '</td>';
							}

							echo '</tr>';
						}
						?>
					</tbody>
				</table>
			</td>
		</tr>
		<?php
	}

	/**
	 * Save settings.
	 */
	public function save() {
		global $current_section;

		$wc_payment_gateways = WC_Payment_Gateways::instance();

		$this->save_settings_for_current_section();

		if ( ! $current_section ) {
			// If section is empty, we're on the main settings page. This makes sure 'gateway ordering' is saved.
			$wc_payment_gateways->process_admin_options();
			$wc_payment_gateways->init();
		} else {
			// There is a section - this may be a gateway or custom section.
			foreach ( $wc_payment_gateways->payment_gateways() as $gateway ) {
				if ( in_array( $current_section, array( $gateway->id, sanitize_title( get_class( $gateway ) ) ), true ) ) {
					do_action( 'woocommerce_update_options_payment_gateways_' . $gateway->id );
					$wc_payment_gateways->init();
				}
			}

			$this->do_update_options_action();
		}
	}
}

return new WC_Settings_Payment_Gateways();
admin/settings/class-wc-settings-advanced.php000064400000037533151336044770015347 0ustar00<?php
/**
 * WooCommerce advanced settings
 *
 * @package  WooCommerce\Admin
 */

defined( 'ABSPATH' ) || exit;

/**
 * Settings for API.
 */
if ( class_exists( 'WC_Settings_Advanced', false ) ) {
	return new WC_Settings_Advanced();
}

/**
 * WC_Settings_Advanced.
 */
class WC_Settings_Advanced extends WC_Settings_Page {

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->id    = 'advanced';
		$this->label = __( 'Advanced', 'woocommerce' );

		parent::__construct();
		$this->notices();
	}

	/**
	 * Get own sections.
	 *
	 * @return array
	 */
	protected function get_own_sections() {
		return array(
			''                => __( 'Page setup', 'woocommerce' ),
			'keys'            => __( 'REST API', 'woocommerce' ),
			'webhooks'        => __( 'Webhooks', 'woocommerce' ),
			'legacy_api'      => __( 'Legacy API', 'woocommerce' ),
			'woocommerce_com' => __( 'WooCommerce.com', 'woocommerce' ),
		);
	}

	/**
	 * Get settings for the default section.
	 *
	 * @return array
	 */
	protected function get_settings_for_default_section() {
		$settings =
			array(
				array(
					'title' => __( 'Page setup', 'woocommerce' ),
					'desc'  => __( 'These pages need to be set so that WooCommerce knows where to send users to checkout.', 'woocommerce' ),
					'type'  => 'title',
					'id'    => 'advanced_page_options',
				),

				array(
					'title'    => __( 'Cart page', 'woocommerce' ),
					/* Translators: %s Page contents. */
					'desc'     => sprintf( __( 'Page contents: [%s]', 'woocommerce' ), apply_filters( 'woocommerce_cart_shortcode_tag', 'woocommerce_cart' ) ),
					'id'       => 'woocommerce_cart_page_id',
					'type'     => 'single_select_page_with_search',
					'default'  => '',
					'class'    => 'wc-page-search',
					'css'      => 'min-width:300px;',
					'args'     => array(
						'exclude' =>
							array(
								wc_get_page_id( 'checkout' ),
								wc_get_page_id( 'myaccount' ),
							),
					),
					'desc_tip' => true,
					'autoload' => false,
				),

				array(
					'title'    => __( 'Checkout page', 'woocommerce' ),
					/* Translators: %s Page contents. */
					'desc'     => sprintf( __( 'Page contents: [%s]', 'woocommerce' ), apply_filters( 'woocommerce_checkout_shortcode_tag', 'woocommerce_checkout' ) ),
					'id'       => 'woocommerce_checkout_page_id',
					'type'     => 'single_select_page_with_search',
					'default'  => wc_get_page_id( 'checkout' ),
					'class'    => 'wc-page-search',
					'css'      => 'min-width:300px;',
					'args'     => array(
						'exclude' =>
							array(
								wc_get_page_id( 'cart' ),
								wc_get_page_id( 'myaccount' ),
							),
					),
					'desc_tip' => true,
					'autoload' => false,
				),

				array(
					'title'    => __( 'My account page', 'woocommerce' ),
					/* Translators: %s Page contents. */
					'desc'     => sprintf( __( 'Page contents: [%s]', 'woocommerce' ), apply_filters( 'woocommerce_my_account_shortcode_tag', 'woocommerce_my_account' ) ),
					'id'       => 'woocommerce_myaccount_page_id',
					'type'     => 'single_select_page_with_search',
					'default'  => '',
					'class'    => 'wc-page-search',
					'css'      => 'min-width:300px;',
					'args'     => array(
						'exclude' =>
							array(
								wc_get_page_id( 'cart' ),
								wc_get_page_id( 'checkout' ),
							),
					),
					'desc_tip' => true,
					'autoload' => false,
				),

				array(
					'title'    => __( 'Terms and conditions', 'woocommerce' ),
					'desc'     => __( 'If you define a "Terms" page the customer will be asked if they accept them when checking out.', 'woocommerce' ),
					'id'       => 'woocommerce_terms_page_id',
					'default'  => '',
					'class'    => 'wc-page-search',
					'css'      => 'min-width:300px;',
					'type'     => 'single_select_page_with_search',
					'args'     => array( 'exclude' => wc_get_page_id( 'checkout' ) ),
					'desc_tip' => true,
					'autoload' => false,
				),

				array(
					'type' => 'sectionend',
					'id'   => 'advanced_page_options',
				),

				array(
					'title' => '',
					'type'  => 'title',
					'id'    => 'checkout_process_options',
				),

				'force_ssl_checkout'   => array(
					'title'           => __( 'Secure checkout', 'woocommerce' ),
					'desc'            => __( 'Force secure checkout', 'woocommerce' ),
					'id'              => 'woocommerce_force_ssl_checkout',
					'default'         => 'no',
					'type'            => 'checkbox',
					'checkboxgroup'   => 'start',
					'show_if_checked' => 'option',
					/* Translators: %s Docs URL. */
					'desc_tip'        => sprintf( __( 'Force SSL (HTTPS) on the checkout pages (<a href="%s" target="_blank">an SSL Certificate is required</a>).', 'woocommerce' ), 'https://docs.woocommerce.com/document/ssl-and-https/#section-3' ),
				),

				'unforce_ssl_checkout' => array(
					'desc'            => __( 'Force HTTP when leaving the checkout', 'woocommerce' ),
					'id'              => 'woocommerce_unforce_ssl_checkout',
					'default'         => 'no',
					'type'            => 'checkbox',
					'checkboxgroup'   => 'end',
					'show_if_checked' => 'yes',
				),

				array(
					'type' => 'sectionend',
					'id'   => 'checkout_process_options',
				),

				array(
					'title' => __( 'Checkout endpoints', 'woocommerce' ),
					'type'  => 'title',
					'desc'  => __( 'Endpoints are appended to your page URLs to handle specific actions during the checkout process. They should be unique.', 'woocommerce' ),
					'id'    => 'checkout_endpoint_options',
				),

				array(
					'title'    => __( 'Pay', 'woocommerce' ),
					'desc'     => __( 'Endpoint for the "Checkout &rarr; Pay" page.', 'woocommerce' ),
					'id'       => 'woocommerce_checkout_pay_endpoint',
					'type'     => 'text',
					'default'  => 'order-pay',
					'desc_tip' => true,
				),

				array(
					'title'    => __( 'Order received', 'woocommerce' ),
					'desc'     => __( 'Endpoint for the "Checkout &rarr; Order received" page.', 'woocommerce' ),
					'id'       => 'woocommerce_checkout_order_received_endpoint',
					'type'     => 'text',
					'default'  => 'order-received',
					'desc_tip' => true,
				),

				array(
					'title'    => __( 'Add payment method', 'woocommerce' ),
					'desc'     => __( 'Endpoint for the "Checkout &rarr; Add payment method" page.', 'woocommerce' ),
					'id'       => 'woocommerce_myaccount_add_payment_method_endpoint',
					'type'     => 'text',
					'default'  => 'add-payment-method',
					'desc_tip' => true,
				),

				array(
					'title'    => __( 'Delete payment method', 'woocommerce' ),
					'desc'     => __( 'Endpoint for the delete payment method page.', 'woocommerce' ),
					'id'       => 'woocommerce_myaccount_delete_payment_method_endpoint',
					'type'     => 'text',
					'default'  => 'delete-payment-method',
					'desc_tip' => true,
				),

				array(
					'title'    => __( 'Set default payment method', 'woocommerce' ),
					'desc'     => __( 'Endpoint for the setting a default payment method page.', 'woocommerce' ),
					'id'       => 'woocommerce_myaccount_set_default_payment_method_endpoint',
					'type'     => 'text',
					'default'  => 'set-default-payment-method',
					'desc_tip' => true,
				),

				array(
					'type' => 'sectionend',
					'id'   => 'checkout_endpoint_options',
				),

				array(
					'title' => __( 'Account endpoints', 'woocommerce' ),
					'type'  => 'title',
					'desc'  => __( 'Endpoints are appended to your page URLs to handle specific actions on the accounts pages. They should be unique and can be left blank to disable the endpoint.', 'woocommerce' ),
					'id'    => 'account_endpoint_options',
				),

				array(
					'title'    => __( 'Orders', 'woocommerce' ),
					'desc'     => __( 'Endpoint for the "My account &rarr; Orders" page.', 'woocommerce' ),
					'id'       => 'woocommerce_myaccount_orders_endpoint',
					'type'     => 'text',
					'default'  => 'orders',
					'desc_tip' => true,
				),

				array(
					'title'    => __( 'View order', 'woocommerce' ),
					'desc'     => __( 'Endpoint for the "My account &rarr; View order" page.', 'woocommerce' ),
					'id'       => 'woocommerce_myaccount_view_order_endpoint',
					'type'     => 'text',
					'default'  => 'view-order',
					'desc_tip' => true,
				),

				array(
					'title'    => __( 'Downloads', 'woocommerce' ),
					'desc'     => __( 'Endpoint for the "My account &rarr; Downloads" page.', 'woocommerce' ),
					'id'       => 'woocommerce_myaccount_downloads_endpoint',
					'type'     => 'text',
					'default'  => 'downloads',
					'desc_tip' => true,
				),

				array(
					'title'    => __( 'Edit account', 'woocommerce' ),
					'desc'     => __( 'Endpoint for the "My account &rarr; Edit account" page.', 'woocommerce' ),
					'id'       => 'woocommerce_myaccount_edit_account_endpoint',
					'type'     => 'text',
					'default'  => 'edit-account',
					'desc_tip' => true,
				),

				array(
					'title'    => __( 'Addresses', 'woocommerce' ),
					'desc'     => __( 'Endpoint for the "My account &rarr; Addresses" page.', 'woocommerce' ),
					'id'       => 'woocommerce_myaccount_edit_address_endpoint',
					'type'     => 'text',
					'default'  => 'edit-address',
					'desc_tip' => true,
				),

				array(
					'title'    => __( 'Payment methods', 'woocommerce' ),
					'desc'     => __( 'Endpoint for the "My account &rarr; Payment methods" page.', 'woocommerce' ),
					'id'       => 'woocommerce_myaccount_payment_methods_endpoint',
					'type'     => 'text',
					'default'  => 'payment-methods',
					'desc_tip' => true,
				),

				array(
					'title'    => __( 'Lost password', 'woocommerce' ),
					'desc'     => __( 'Endpoint for the "My account &rarr; Lost password" page.', 'woocommerce' ),
					'id'       => 'woocommerce_myaccount_lost_password_endpoint',
					'type'     => 'text',
					'default'  => 'lost-password',
					'desc_tip' => true,
				),

				array(
					'title'    => __( 'Logout', 'woocommerce' ),
					'desc'     => __( 'Endpoint for the triggering logout. You can add this to your menus via a custom link: yoursite.com/?customer-logout=true', 'woocommerce' ),
					'id'       => 'woocommerce_logout_endpoint',
					'type'     => 'text',
					'default'  => 'customer-logout',
					'desc_tip' => true,
				),

				array(
					'type' => 'sectionend',
					'id'   => 'account_endpoint_options',
				),
			);

		$settings = apply_filters( 'woocommerce_settings_pages', $settings );

		if ( wc_site_is_https() ) {
			unset( $settings['unforce_ssl_checkout'], $settings['force_ssl_checkout'] );
		}

		return $settings;
	}

	/**
	 * Get settings for the WooCommerce.com section.
	 *
	 * @return array
	 */
	protected function get_settings_for_woocommerce_com_section() {
		$tracking_info_text = sprintf( '<a href="%s" target="_blank">%s</a>', 'https://woocommerce.com/usage-tracking', esc_html__( 'WooCommerce.com Usage Tracking Documentation', 'woocommerce' ) );

		$settings =
			array(
				array(
					'title' => esc_html__( 'Usage Tracking', 'woocommerce' ),
					'type'  => 'title',
					'id'    => 'tracking_options',
					'desc'  => __( 'Gathering usage data allows us to make WooCommerce better — your store will be considered as we evaluate new features, judge the quality of an update, or determine if an improvement makes sense.', 'woocommerce' ),
				),
				array(
					'title'         => __( 'Enable tracking', 'woocommerce' ),
					'desc'          => __( 'Allow usage of WooCommerce to be tracked', 'woocommerce' ),
					/* Translators: %s URL to tracking info screen. */
					'desc_tip'      => sprintf( esc_html__( 'To opt out, leave this box unticked. Your store remains untracked, and no data will be collected. Read about what usage data is tracked at: %s.', 'woocommerce' ), $tracking_info_text ),
					'id'            => 'woocommerce_allow_tracking',
					'type'          => 'checkbox',
					'checkboxgroup' => 'start',
					'default'       => 'no',
					'autoload'      => false,
				),
				array(
					'type' => 'sectionend',
					'id'   => 'tracking_options',
				),
				array(
					'title' => esc_html__( 'Marketplace suggestions', 'woocommerce' ),
					'type'  => 'title',
					'id'    => 'marketplace_suggestions',
					'desc'  => __( 'We show contextual suggestions for official extensions that may be helpful to your store.', 'woocommerce' ),
				),
				array(
					'title'         => __( 'Show Suggestions', 'woocommerce' ),
					'desc'          => __( 'Display suggestions within WooCommerce', 'woocommerce' ),
					'desc_tip'      => esc_html__( 'Leave this box unchecked if you do not want to see suggested extensions.', 'woocommerce' ),
					'id'            => 'woocommerce_show_marketplace_suggestions',
					'type'          => 'checkbox',
					'checkboxgroup' => 'start',
					'default'       => 'yes',
					'autoload'      => false,
				),
				array(
					'type' => 'sectionend',
					'id'   => 'marketplace_suggestions',
				),
			);

		return apply_filters( 'woocommerce_com_integration_settings', $settings );
	}

	/**
	 * Get settings for the legacy API section.
	 *
	 * @return array
	 */
	protected function get_settings_for_legacy_api_section() {
		$settings =
			array(
				array(
					'title' => '',
					'type'  => 'title',
					'desc'  => '',
					'id'    => 'legacy_api_options',
				),
				array(
					'title'   => __( 'Legacy API', 'woocommerce' ),
					'desc'    => __( 'Enable the legacy REST API', 'woocommerce' ),
					'id'      => 'woocommerce_api_enabled',
					'type'    => 'checkbox',
					'default' => 'no',
				),
				array(
					'type' => 'sectionend',
					'id'   => 'legacy_api_options',
				),
			);

		return apply_filters( 'woocommerce_settings_rest_api', $settings );
	}

	/**
	 * Form method.
	 *
	 * @deprecated 3.4.4
	 *
	 * @param  string $method Method name.
	 *
	 * @return string
	 */
	public function form_method( $method ) {
		return 'post';
	}

	/**
	 * Notices.
	 */
	private function notices() {
		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		if ( isset( $_GET['section'] ) && 'webhooks' === $_GET['section'] ) {
			WC_Admin_Webhooks::notices();
		}
		if ( isset( $_GET['section'] ) && 'keys' === $_GET['section'] ) {
			WC_Admin_API_Keys::notices();
		}
		// phpcs:enable
	}

	/**
	 * Output the settings.
	 */
	public function output() {
		global $current_section;

		if ( 'webhooks' === $current_section ) {
			WC_Admin_Webhooks::page_output();
		} elseif ( 'keys' === $current_section ) {
			WC_Admin_API_Keys::page_output();
		} else {
			parent::output();
		}
	}

	/**
	 * Save settings.
	 */
	public function save() {
		// phpcs:disable WordPress.Security.NonceVerification.Missing
		global $current_section;

		if ( apply_filters( 'woocommerce_rest_api_valid_to_save', ! in_array( $current_section, array( 'keys', 'webhooks' ), true ) ) ) {
			// Prevent the T&Cs and checkout page from being set to the same page.
			if ( isset( $_POST['woocommerce_terms_page_id'], $_POST['woocommerce_checkout_page_id'] ) && $_POST['woocommerce_terms_page_id'] === $_POST['woocommerce_checkout_page_id'] ) {
				$_POST['woocommerce_terms_page_id'] = '';
			}

			// Prevent the Cart, checkout and my account page from being set to the same page.
			if ( isset( $_POST['woocommerce_cart_page_id'], $_POST['woocommerce_checkout_page_id'], $_POST['woocommerce_myaccount_page_id'] ) ) {
				if ( $_POST['woocommerce_cart_page_id'] === $_POST['woocommerce_checkout_page_id'] ) {
					$_POST['woocommerce_checkout_page_id'] = '';
				}
				if ( $_POST['woocommerce_cart_page_id'] === $_POST['woocommerce_myaccount_page_id'] ) {
					$_POST['woocommerce_myaccount_page_id'] = '';
				}
				if ( $_POST['woocommerce_checkout_page_id'] === $_POST['woocommerce_myaccount_page_id'] ) {
					$_POST['woocommerce_myaccount_page_id'] = '';
				}
			}

			$this->save_settings_for_current_section();
			$this->do_update_options_action();
		}
		// phpcs:enable
	}
}

// phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound, Generic.Commenting.Todo.CommentFound
/**
 * WC_Settings_Rest_API class.
 *
 * @deprecated 3.4 in favour of WC_Settings_Advanced.
 */
class WC_Settings_Rest_API extends WC_Settings_Advanced {
}

return new WC_Settings_Advanced();
// phpcs:enable
admin/settings/class-wc-settings-shipping.php000064400000031075151336044770015416 0ustar00<?php
/**
 * WooCommerce Shipping Settings
 *
 * @package     WooCommerce\Admin
 * @version     2.6.0
 */

use Automattic\Jetpack\Constants;

defined( 'ABSPATH' ) || exit;

if ( class_exists( 'WC_Settings_Shipping', false ) ) {
	return new WC_Settings_Shipping();
}

/**
 * WC_Settings_Shipping.
 */
class WC_Settings_Shipping extends WC_Settings_Page {

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->id    = 'shipping';
		$this->label = __( 'Shipping', 'woocommerce' );

		parent::__construct();
	}

	/**
	 * Add this page to settings.
	 *
	 * @param array $pages Current pages.
	 * @return array|mixed
	 */
	public function add_settings_page( $pages ) {
		return wc_shipping_enabled() ? parent::add_settings_page( $pages ) : $pages;
	}

	/**
	 * Get own sections.
	 *
	 * @return array
	 */
	protected function get_own_sections() {
		$sections = array(
			''        => __( 'Shipping zones', 'woocommerce' ),
			'options' => __( 'Shipping options', 'woocommerce' ),
			'classes' => __( 'Shipping classes', 'woocommerce' ),
		);

		if ( ! $this->wc_is_installing() ) {
			// Load shipping methods so we can show any global options they may have.
			$shipping_methods = $this->get_shipping_methods();

			foreach ( $shipping_methods as $method ) {
				if ( ! $method->has_settings() ) {
					continue;
				}
				$title                                 = empty( $method->method_title ) ? ucfirst( $method->id ) : $method->method_title;
				$sections[ strtolower( $method->id ) ] = esc_html( $title );
			}
		}

		return $sections;
	}

	/**
	 * Is WC_INSTALLING constant defined?
	 * This method exists to ease unit testing.
	 *
	 * @return bool True is the WC_INSTALLING constant is defined.
	 */
	protected function wc_is_installing() {
		return Constants::is_defined( 'WC_INSTALLING' );
	}

	/**
	 * Get the currently available shipping methods.
	 * This method exists to ease unit testing.
	 *
	 * @return array Currently available shipping methods.
	 */
	protected function get_shipping_methods() {
		return WC()->shipping()->get_shipping_methods();
	}

	/**
	 * Get settings for the default section.
	 *
	 * The original implementation of 'get_settings' was returning the settings for the "Options" section
	 * when the supplied value for $current_section was ''.
	 *
	 * @return array
	 */
	protected function get_settings_for_default_section() {
		return $this->get_settings_for_options_section();
	}

	/**
	 * Get settings for the options section.
	 *
	 * @return array
	 */
	protected function get_settings_for_options_section() {
		$settings =
			array(
				array(
					'title' => __( 'Shipping options', 'woocommerce' ),
					'type'  => 'title',
					'id'    => 'shipping_options',
				),

				array(
					'title'         => __( 'Calculations', 'woocommerce' ),
					'desc'          => __( 'Enable the shipping calculator on the cart page', 'woocommerce' ),
					'id'            => 'woocommerce_enable_shipping_calc',
					'default'       => 'yes',
					'type'          => 'checkbox',
					'checkboxgroup' => 'start',
					'autoload'      => false,
				),

				array(
					'desc'          => __( 'Hide shipping costs until an address is entered', 'woocommerce' ),
					'id'            => 'woocommerce_shipping_cost_requires_address',
					'default'       => 'no',
					'type'          => 'checkbox',
					'checkboxgroup' => 'end',
				),

				array(
					'title'           => __( 'Shipping destination', 'woocommerce' ),
					'desc'            => __( 'This controls which shipping address is used by default.', 'woocommerce' ),
					'id'              => 'woocommerce_ship_to_destination',
					'default'         => 'billing',
					'type'            => 'radio',
					'options'         => array(
						'shipping'     => __( 'Default to customer shipping address', 'woocommerce' ),
						'billing'      => __( 'Default to customer billing address', 'woocommerce' ),
						'billing_only' => __( 'Force shipping to the customer billing address', 'woocommerce' ),
					),
					'autoload'        => false,
					'desc_tip'        => true,
					'show_if_checked' => 'option',
				),

				array(
					'title'    => __( 'Debug mode', 'woocommerce' ),
					'desc'     => __( 'Enable debug mode', 'woocommerce' ),
					'desc_tip' => __( 'Enable shipping debug mode to show matching shipping zones and to bypass shipping rate cache.', 'woocommerce' ),
					'id'       => 'woocommerce_shipping_debug_mode',
					'default'  => 'no',
					'type'     => 'checkbox',
				),

				array(
					'type' => 'sectionend',
					'id'   => 'shipping_options',
				),
			);

		return apply_filters( 'woocommerce_shipping_settings', $settings );
	}

	/**
	 * Output the settings.
	 */
	public function output() {
		global $current_section, $hide_save_button;

		// Load shipping methods so we can show any global options they may have.
		$shipping_methods = $this->get_shipping_methods();

		if ( '' === $current_section ) {
			$this->output_zones_screen();
		} elseif ( 'classes' === $current_section ) {
			$hide_save_button = true;
			$this->output_shipping_class_screen();
		} else {
			$is_shipping_method = false;
			foreach ( $shipping_methods as $method ) {
				if ( in_array( $current_section, array( $method->id, sanitize_title( get_class( $method ) ) ), true ) && $method->has_settings() ) {
					$is_shipping_method = true;
					$method->admin_options();
				}
			}
			if ( ! $is_shipping_method ) {
				parent::output();
			}
		}
	}

	/**
	 * Save settings.
	 */
	public function save() {
		global $current_section;

		switch ( $current_section ) {
			case 'options':
				$this->save_settings_for_current_section();
				$this->do_update_options_action();
				break;
			case 'classes':
				$this->do_update_options_action();
				break;
			case '':
				break;
			default:
				$is_shipping_method = false;

				foreach ( $this->get_shipping_methods() as $method_id => $method ) {
					if ( in_array( $current_section, array( $method->id, sanitize_title( get_class( $method ) ) ), true ) ) {
						$is_shipping_method = true;
						$this->do_update_options_action( $method->id );
					}
				}
				if ( ! $is_shipping_method ) {
					$this->save_settings_for_current_section();
				}
				break;
		}

		// Increments the transient version to invalidate cache.
		WC_Cache_Helper::get_transient_version( 'shipping', true );
	}

	/**
	 * Handles output of the shipping zones page in admin.
	 */
	protected function output_zones_screen() {
		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		global $hide_save_button;

		if ( isset( $_REQUEST['zone_id'] ) ) {
			$hide_save_button = true;
			$this->zone_methods_screen( wc_clean( wp_unslash( $_REQUEST['zone_id'] ) ) );
		} elseif ( isset( $_REQUEST['instance_id'] ) ) {
			$this->instance_settings_screen( absint( wp_unslash( $_REQUEST['instance_id'] ) ) );
		} else {
			$hide_save_button = true;
			$this->zones_screen();
		}
		// phpcs:enable WordPress.Security.NonceVerification.Recommended
	}

	/**
	 * Show method for a zone
	 *
	 * @param int $zone_id Zone ID.
	 */
	protected function zone_methods_screen( $zone_id ) {
		if ( 'new' === $zone_id ) {
			$zone = new WC_Shipping_Zone();
		} else {
			$zone = WC_Shipping_Zones::get_zone( absint( $zone_id ) );
		}

		if ( ! $zone ) {
			wp_die( esc_html__( 'Zone does not exist!', 'woocommerce' ) );
		}

		$allowed_countries   = WC()->countries->get_shipping_countries();
		$shipping_continents = WC()->countries->get_shipping_continents();

		// Prepare locations.
		$locations = array();
		$postcodes = array();

		foreach ( $zone->get_zone_locations() as $location ) {
			if ( 'postcode' === $location->type ) {
				$postcodes[] = $location->code;
			} else {
				$locations[] = $location->type . ':' . $location->code;
			}
		}

		wp_localize_script(
			'wc-shipping-zone-methods',
			'shippingZoneMethodsLocalizeScript',
			array(
				'methods'                 => $zone->get_shipping_methods( false, 'json' ),
				'zone_name'               => $zone->get_zone_name(),
				'zone_id'                 => $zone->get_id(),
				'wc_shipping_zones_nonce' => wp_create_nonce( 'wc_shipping_zones_nonce' ),
				'strings'                 => array(
					'unload_confirmation_msg' => __( 'Your changed data will be lost if you leave this page without saving.', 'woocommerce' ),
					'save_changes_prompt'     => __( 'Do you wish to save your changes first? Your changed data will be discarded if you choose to cancel.', 'woocommerce' ),
					'save_failed'             => __( 'Your changes were not saved. Please retry.', 'woocommerce' ),
					'add_method_failed'       => __( 'Shipping method could not be added. Please retry.', 'woocommerce' ),
					'yes'                     => __( 'Yes', 'woocommerce' ),
					'no'                      => __( 'No', 'woocommerce' ),
					'default_zone_name'       => __( 'Zone', 'woocommerce' ),
				),
			)
		);
		wp_enqueue_script( 'wc-shipping-zone-methods' );

		include_once dirname( __FILE__ ) . '/views/html-admin-page-shipping-zone-methods.php';
	}

	/**
	 * Show zones
	 */
	protected function zones_screen() {
		$method_count = wc_get_shipping_method_count( false, true );

		wp_localize_script(
			'wc-shipping-zones',
			'shippingZonesLocalizeScript',
			array(
				'zones'                   => WC_Shipping_Zones::get_zones( 'json' ),
				'default_zone'            => array(
					'zone_id'    => 0,
					'zone_name'  => '',
					'zone_order' => null,
				),
				'wc_shipping_zones_nonce' => wp_create_nonce( 'wc_shipping_zones_nonce' ),
				'strings'                 => array(
					'unload_confirmation_msg'     => __( 'Your changed data will be lost if you leave this page without saving.', 'woocommerce' ),
					'delete_confirmation_msg'     => __( 'Are you sure you want to delete this zone? This action cannot be undone.', 'woocommerce' ),
					'save_failed'                 => __( 'Your changes were not saved. Please retry.', 'woocommerce' ),
					'no_shipping_methods_offered' => __( 'No shipping methods offered to this zone.', 'woocommerce' ),
				),
			)
		);
		wp_enqueue_script( 'wc-shipping-zones' );

		include_once dirname( __FILE__ ) . '/views/html-admin-page-shipping-zones.php';
	}

	/**
	 * Show instance settings
	 *
	 * @param int $instance_id Shipping instance ID.
	 */
	protected function instance_settings_screen( $instance_id ) {
		$zone            = WC_Shipping_Zones::get_zone_by( 'instance_id', $instance_id );
		$shipping_method = WC_Shipping_Zones::get_shipping_method( $instance_id );

		if ( ! $shipping_method ) {
			wp_die( esc_html__( 'Invalid shipping method!', 'woocommerce' ) );
		}
		if ( ! $zone ) {
			wp_die( esc_html__( 'Zone does not exist!', 'woocommerce' ) );
		}
		if ( ! $shipping_method->has_settings() ) {
			wp_die( esc_html__( 'This shipping method does not have any settings to configure.', 'woocommerce' ) );
		}

		if ( ! empty( $_POST['save'] ) ) {

			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			if ( empty( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( wp_unslash( $_REQUEST['_wpnonce'] ), 'woocommerce-settings' ) ) {
				echo '<div class="updated error"><p>' . esc_html__( 'Edit failed. Please try again.', 'woocommerce' ) . '</p></div>';
			}

			$shipping_method->process_admin_options();
			$shipping_method->display_errors();
		}

		include_once dirname( __FILE__ ) . '/views/html-admin-page-shipping-zones-instance.php';
	}

	/**
	 * Handles output of the shipping class settings screen.
	 */
	protected function output_shipping_class_screen() {
		$wc_shipping = WC_Shipping::instance();
		wp_localize_script(
			'wc-shipping-classes',
			'shippingClassesLocalizeScript',
			array(
				'classes'                   => $wc_shipping->get_shipping_classes(),
				'default_shipping_class'    => array(
					'term_id'     => 0,
					'name'        => '',
					'description' => '',
				),
				'wc_shipping_classes_nonce' => wp_create_nonce( 'wc_shipping_classes_nonce' ),
				'strings'                   => array(
					'unload_confirmation_msg' => __( 'Your changed data will be lost if you leave this page without saving.', 'woocommerce' ),
					'save_failed'             => __( 'Your changes were not saved. Please retry.', 'woocommerce' ),
				),
			)
		);
		wp_enqueue_script( 'wc-shipping-classes' );

		// Extendable columns to show on the shipping classes screen.
		$shipping_class_columns = apply_filters(
			'woocommerce_shipping_classes_columns',
			array(
				'wc-shipping-class-name'        => __( 'Shipping class', 'woocommerce' ),
				'wc-shipping-class-slug'        => __( 'Slug', 'woocommerce' ),
				'wc-shipping-class-description' => __( 'Description', 'woocommerce' ),
				'wc-shipping-class-count'       => __( 'Product count', 'woocommerce' ),
			)
		);

		include_once dirname( __FILE__ ) . '/views/html-admin-page-shipping-classes.php';
	}
}

return new WC_Settings_Shipping();
admin/class-wc-admin-permalink-settings.php000064400000022153151336044770015002 0ustar00<?php
/**
 * Adds settings to the permalinks admin settings page
 *
 * @class       WC_Admin_Permalink_Settings
 * @package     WooCommerce\Admin
 * @version     2.3.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( class_exists( 'WC_Admin_Permalink_Settings', false ) ) {
	return new WC_Admin_Permalink_Settings();
}

/**
 * WC_Admin_Permalink_Settings Class.
 */
class WC_Admin_Permalink_Settings {

	/**
	 * Permalink settings.
	 *
	 * @var array
	 */
	private $permalinks = array();

	/**
	 * Hook in tabs.
	 */
	public function __construct() {
		$this->settings_init();
		$this->settings_save();
	}

	/**
	 * Init our settings.
	 */
	public function settings_init() {
		add_settings_section( 'woocommerce-permalink', __( 'Product permalinks', 'woocommerce' ), array( $this, 'settings' ), 'permalink' );

		add_settings_field(
			'woocommerce_product_category_slug',
			__( 'Product category base', 'woocommerce' ),
			array( $this, 'product_category_slug_input' ),
			'permalink',
			'optional'
		);
		add_settings_field(
			'woocommerce_product_tag_slug',
			__( 'Product tag base', 'woocommerce' ),
			array( $this, 'product_tag_slug_input' ),
			'permalink',
			'optional'
		);
		add_settings_field(
			'woocommerce_product_attribute_slug',
			__( 'Product attribute base', 'woocommerce' ),
			array( $this, 'product_attribute_slug_input' ),
			'permalink',
			'optional'
		);

		$this->permalinks = wc_get_permalink_structure();
	}

	/**
	 * Show a slug input box.
	 */
	public function product_category_slug_input() {
		?>
		<input name="woocommerce_product_category_slug" type="text" class="regular-text code" value="<?php echo esc_attr( $this->permalinks['category_base'] ); ?>" placeholder="<?php echo esc_attr_x( 'product-category', 'slug', 'woocommerce' ); ?>" />
		<?php
	}

	/**
	 * Show a slug input box.
	 */
	public function product_tag_slug_input() {
		?>
		<input name="woocommerce_product_tag_slug" type="text" class="regular-text code" value="<?php echo esc_attr( $this->permalinks['tag_base'] ); ?>" placeholder="<?php echo esc_attr_x( 'product-tag', 'slug', 'woocommerce' ); ?>" />
		<?php
	}

	/**
	 * Show a slug input box.
	 */
	public function product_attribute_slug_input() {
		?>
		<input name="woocommerce_product_attribute_slug" type="text" class="regular-text code" value="<?php echo esc_attr( $this->permalinks['attribute_base'] ); ?>" /><code>/attribute-name/attribute/</code>
		<?php
	}

	/**
	 * Show the settings.
	 */
	public function settings() {
		/* translators: %s: Home URL */
		echo wp_kses_post( wpautop( sprintf( __( 'If you like, you may enter custom structures for your product URLs here. For example, using <code>shop</code> would make your product links like <code>%sshop/sample-product/</code>. This setting affects product URLs only, not things such as product categories.', 'woocommerce' ), esc_url( home_url( '/' ) ) ) ) );

		$shop_page_id = wc_get_page_id( 'shop' );
		$base_slug    = urldecode( ( $shop_page_id > 0 && get_post( $shop_page_id ) ) ? get_page_uri( $shop_page_id ) : _x( 'shop', 'default-slug', 'woocommerce' ) );
		$product_base = _x( 'product', 'default-slug', 'woocommerce' );

		$structures = array(
			0 => '',
			1 => '/' . trailingslashit( $base_slug ),
			2 => '/' . trailingslashit( $base_slug ) . trailingslashit( '%product_cat%' ),
		);
		?>
		<table class="form-table wc-permalink-structure">
			<tbody>
				<tr>
					<th><label><input name="product_permalink" type="radio" value="<?php echo esc_attr( $structures[0] ); ?>" class="wctog" <?php checked( $structures[0], $this->permalinks['product_base'] ); ?> /> <?php esc_html_e( 'Default', 'woocommerce' ); ?></label></th>
					<td><code class="default-example"><?php echo esc_html( home_url() ); ?>/?product=sample-product</code> <code class="non-default-example"><?php echo esc_html( home_url() ); ?>/<?php echo esc_html( $product_base ); ?>/sample-product/</code></td>
				</tr>
				<?php if ( $shop_page_id ) : ?>
					<tr>
						<th><label><input name="product_permalink" type="radio" value="<?php echo esc_attr( $structures[1] ); ?>" class="wctog" <?php checked( $structures[1], $this->permalinks['product_base'] ); ?> /> <?php esc_html_e( 'Shop base', 'woocommerce' ); ?></label></th>
						<td><code><?php echo esc_html( home_url() ); ?>/<?php echo esc_html( $base_slug ); ?>/sample-product/</code></td>
					</tr>
					<tr>
						<th><label><input name="product_permalink" type="radio" value="<?php echo esc_attr( $structures[2] ); ?>" class="wctog" <?php checked( $structures[2], $this->permalinks['product_base'] ); ?> /> <?php esc_html_e( 'Shop base with category', 'woocommerce' ); ?></label></th>
						<td><code><?php echo esc_html( home_url() ); ?>/<?php echo esc_html( $base_slug ); ?>/product-category/sample-product/</code></td>
					</tr>
				<?php endif; ?>
				<tr>
					<th><label><input name="product_permalink" id="woocommerce_custom_selection" type="radio" value="custom" class="tog" <?php checked( in_array( $this->permalinks['product_base'], $structures, true ), false ); ?> />
						<?php esc_html_e( 'Custom base', 'woocommerce' ); ?></label></th>
					<td>
						<input name="product_permalink_structure" id="woocommerce_permalink_structure" type="text" value="<?php echo esc_attr( $this->permalinks['product_base'] ? trailingslashit( $this->permalinks['product_base'] ) : '' ); ?>" class="regular-text code"> <span class="description"><?php esc_html_e( 'Enter a custom base to use. A base must be set or WordPress will use default instead.', 'woocommerce' ); ?></span>
					</td>
				</tr>
			</tbody>
		</table>
		<?php wp_nonce_field( 'wc-permalinks', 'wc-permalinks-nonce' ); ?>
		<script type="text/javascript">
			jQuery( function() {
				jQuery('input.wctog').on( 'change', function() {
					jQuery('#woocommerce_permalink_structure').val( jQuery( this ).val() );
				});
				jQuery('.permalink-structure input').on( 'change', function() {
					jQuery('.wc-permalink-structure').find('code.non-default-example, code.default-example').hide();
					if ( jQuery(this).val() ) {
						jQuery('.wc-permalink-structure code.non-default-example').show();
						jQuery('.wc-permalink-structure input').prop('disabled', false);
					} else {
						jQuery('.wc-permalink-structure code.default-example').show();
						jQuery('.wc-permalink-structure input:eq(0)').trigger( 'click' );
						jQuery('.wc-permalink-structure input').attr('disabled', 'disabled');
					}
				});
				jQuery('.permalink-structure input:checked').trigger( 'change' );
				jQuery('#woocommerce_permalink_structure').on( 'focus', function(){
					jQuery('#woocommerce_custom_selection').trigger( 'click' );
				} );
			} );
		</script>
		<?php
	}

	/**
	 * Save the settings.
	 */
	public function settings_save() {
		if ( ! is_admin() ) {
			return;
		}

		// We need to save the options ourselves; settings api does not trigger save for the permalinks page.
		if ( isset( $_POST['permalink_structure'], $_POST['wc-permalinks-nonce'], $_POST['woocommerce_product_category_slug'], $_POST['woocommerce_product_tag_slug'], $_POST['woocommerce_product_attribute_slug'] ) && wp_verify_nonce( wp_unslash( $_POST['wc-permalinks-nonce'] ), 'wc-permalinks' ) ) { // WPCS: input var ok, sanitization ok.
			wc_switch_to_site_locale();

			$permalinks                   = (array) get_option( 'woocommerce_permalinks', array() );
			$permalinks['category_base']  = wc_sanitize_permalink( wp_unslash( $_POST['woocommerce_product_category_slug'] ) ); // WPCS: input var ok, sanitization ok.
			$permalinks['tag_base']       = wc_sanitize_permalink( wp_unslash( $_POST['woocommerce_product_tag_slug'] ) ); // WPCS: input var ok, sanitization ok.
			$permalinks['attribute_base'] = wc_sanitize_permalink( wp_unslash( $_POST['woocommerce_product_attribute_slug'] ) ); // WPCS: input var ok, sanitization ok.

			// Generate product base.
			$product_base = isset( $_POST['product_permalink'] ) ? wc_clean( wp_unslash( $_POST['product_permalink'] ) ) : ''; // WPCS: input var ok, sanitization ok.

			if ( 'custom' === $product_base ) {
				if ( isset( $_POST['product_permalink_structure'] ) ) { // WPCS: input var ok.
					$product_base = preg_replace( '#/+#', '/', '/' . str_replace( '#', '', trim( wp_unslash( $_POST['product_permalink_structure'] ) ) ) ); // WPCS: input var ok, sanitization ok.
				} else {
					$product_base = '/';
				}

				// This is an invalid base structure and breaks pages.
				if ( '/%product_cat%/' === trailingslashit( $product_base ) ) {
					$product_base = '/' . _x( 'product', 'slug', 'woocommerce' ) . $product_base;
				}
			} elseif ( empty( $product_base ) ) {
				$product_base = _x( 'product', 'slug', 'woocommerce' );
			}

			$permalinks['product_base'] = wc_sanitize_permalink( $product_base );

			// Shop base may require verbose page rules if nesting pages.
			$shop_page_id   = wc_get_page_id( 'shop' );
			$shop_permalink = ( $shop_page_id > 0 && get_post( $shop_page_id ) ) ? get_page_uri( $shop_page_id ) : _x( 'shop', 'default-slug', 'woocommerce' );

			if ( $shop_page_id && stristr( trim( $permalinks['product_base'], '/' ), $shop_permalink ) ) {
				$permalinks['use_verbose_page_rules'] = true;
			}

			update_option( 'woocommerce_permalinks', $permalinks );
			wc_restore_locale();
		}
	}
}

return new WC_Admin_Permalink_Settings();
admin/class-wc-admin-exporters.php000064400000015057151336044770013222 0ustar00<?php
/**
 * Init WooCommerce data exporters.
 *
 * @package     WooCommerce\Admin
 * @version     3.1.0
 */

use Automattic\Jetpack\Constants;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC_Admin_Exporters Class.
 */
class WC_Admin_Exporters {

	/**
	 * Array of exporter IDs.
	 *
	 * @var string[]
	 */
	protected $exporters = array();

	/**
	 * Constructor.
	 */
	public function __construct() {
		if ( ! $this->export_allowed() ) {
			return;
		}

		add_action( 'admin_menu', array( $this, 'add_to_menus' ) );
		add_action( 'admin_head', array( $this, 'hide_from_menus' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'admin_scripts' ) );
		add_action( 'admin_init', array( $this, 'download_export_file' ) );
		add_action( 'wp_ajax_woocommerce_do_ajax_product_export', array( $this, 'do_ajax_product_export' ) );

		// Register WooCommerce exporters.
		$this->exporters['product_exporter'] = array(
			'menu'       => 'edit.php?post_type=product',
			'name'       => __( 'Product Export', 'woocommerce' ),
			'capability' => 'export',
			'callback'   => array( $this, 'product_exporter' ),
		);
	}

	/**
	 * Return true if WooCommerce export is allowed for current user, false otherwise.
	 *
	 * @return bool Whether current user can perform export.
	 */
	protected function export_allowed() {
		return current_user_can( 'edit_products' ) && current_user_can( 'export' );
	}

	/**
	 * Add menu items for our custom exporters.
	 */
	public function add_to_menus() {
		foreach ( $this->exporters as $id => $exporter ) {
			add_submenu_page( $exporter['menu'], $exporter['name'], $exporter['name'], $exporter['capability'], $id, $exporter['callback'] );
		}
	}

	/**
	 * Hide menu items from view so the pages exist, but the menu items do not.
	 */
	public function hide_from_menus() {
		global $submenu;

		foreach ( $this->exporters as $id => $exporter ) {
			if ( isset( $submenu[ $exporter['menu'] ] ) ) {
				foreach ( $submenu[ $exporter['menu'] ] as $key => $menu ) {
					if ( $id === $menu[2] ) {
						unset( $submenu[ $exporter['menu'] ][ $key ] );
					}
				}
			}
		}
	}

	/**
	 * Enqueue scripts.
	 */
	public function admin_scripts() {
		$suffix  = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min';
		$version = Constants::get_constant( 'WC_VERSION' );
		wp_register_script( 'wc-product-export', WC()->plugin_url() . '/assets/js/admin/wc-product-export' . $suffix . '.js', array( 'jquery' ), $version );
		wp_localize_script(
			'wc-product-export',
			'wc_product_export_params',
			array(
				'export_nonce' => wp_create_nonce( 'wc-product-export' ),
			)
		);
	}

	/**
	 * Export page UI.
	 */
	public function product_exporter() {
		include_once WC_ABSPATH . 'includes/export/class-wc-product-csv-exporter.php';
		include_once dirname( __FILE__ ) . '/views/html-admin-page-product-export.php';
	}

	/**
	 * Serve the generated file.
	 */
	public function download_export_file() {
		if ( isset( $_GET['action'], $_GET['nonce'] ) && wp_verify_nonce( wp_unslash( $_GET['nonce'] ), 'product-csv' ) && 'download_product_csv' === wp_unslash( $_GET['action'] ) ) { // WPCS: input var ok, sanitization ok.
			include_once WC_ABSPATH . 'includes/export/class-wc-product-csv-exporter.php';
			$exporter = new WC_Product_CSV_Exporter();

			if ( ! empty( $_GET['filename'] ) ) { // WPCS: input var ok.
				$exporter->set_filename( wp_unslash( $_GET['filename'] ) ); // WPCS: input var ok, sanitization ok.
			}

			$exporter->export();
		}
	}

	/**
	 * AJAX callback for doing the actual export to the CSV file.
	 */
	public function do_ajax_product_export() {
		check_ajax_referer( 'wc-product-export', 'security' );

		if ( ! $this->export_allowed() ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient privileges to export products.', 'woocommerce' ) ) );
		}

		include_once WC_ABSPATH . 'includes/export/class-wc-product-csv-exporter.php';

		$step     = isset( $_POST['step'] ) ? absint( $_POST['step'] ) : 1; // WPCS: input var ok, sanitization ok.
		$exporter = new WC_Product_CSV_Exporter();

		if ( ! empty( $_POST['columns'] ) ) { // WPCS: input var ok.
			$exporter->set_column_names( wp_unslash( $_POST['columns'] ) ); // WPCS: input var ok, sanitization ok.
		}

		if ( ! empty( $_POST['selected_columns'] ) ) { // WPCS: input var ok.
			$exporter->set_columns_to_export( wp_unslash( $_POST['selected_columns'] ) ); // WPCS: input var ok, sanitization ok.
		}

		if ( ! empty( $_POST['export_meta'] ) ) { // WPCS: input var ok.
			$exporter->enable_meta_export( true );
		}

		if ( ! empty( $_POST['export_types'] ) ) { // WPCS: input var ok.
			$exporter->set_product_types_to_export( wp_unslash( $_POST['export_types'] ) ); // WPCS: input var ok, sanitization ok.
		}

		if ( ! empty( $_POST['export_category'] ) && is_array( $_POST['export_category'] ) ) {// WPCS: input var ok.
			$exporter->set_product_category_to_export( wp_unslash( array_values( $_POST['export_category'] ) ) ); // WPCS: input var ok, sanitization ok.
		}

		if ( ! empty( $_POST['filename'] ) ) { // WPCS: input var ok.
			$exporter->set_filename( wp_unslash( $_POST['filename'] ) ); // WPCS: input var ok, sanitization ok.
		}

		$exporter->set_page( $step );
		$exporter->generate_file();

		$query_args = apply_filters(
			'woocommerce_export_get_ajax_query_args',
			array(
				'nonce'    => wp_create_nonce( 'product-csv' ),
				'action'   => 'download_product_csv',
				'filename' => $exporter->get_filename(),
			)
		);

		if ( 100 === $exporter->get_percent_complete() ) {
			wp_send_json_success(
				array(
					'step'       => 'done',
					'percentage' => 100,
					'url'        => add_query_arg( $query_args, admin_url( 'edit.php?post_type=product&page=product_exporter' ) ),
				)
			);
		} else {
			wp_send_json_success(
				array(
					'step'       => ++$step,
					'percentage' => $exporter->get_percent_complete(),
					'columns'    => $exporter->get_column_names(),
				)
			);
		}
	}

	/**
	 * Gets the product types that can be exported.
	 *
	 * @since 5.1.0
	 * @return array The product types keys and labels.
	 */
	public static function get_product_types() {
		$product_types = wc_get_product_types();
		$product_types['variation'] = __( 'Product variations', 'woocommerce' );

		/**
		 * Allow third-parties to filter the exportable product types.
		 *
		 * @since 5.1.0
		 * @param array $product_types {
		 *     The product type key and label.
		 *
		 *     @type string Product type key - eg 'variable', 'simple' etc.
		 *     @type string A translated product label which appears in the export product type dropdown.
		 * }
		 */
		return apply_filters( 'woocommerce_exporter_product_types', $product_types );
	}
}

new WC_Admin_Exporters();
admin/meta-boxes/class-wc-meta-box-coupon-data.php000064400000040410151336044770016060 0ustar00<?php
/**
 * Coupon Data
 *
 * Display the coupon data meta box.
 *
 * @author      WooThemes
 * @category    Admin
 * @package     WooCommerce\Admin\Meta Boxes
 * @version     2.1.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

/**
 * WC_Meta_Box_Coupon_Data Class.
 */
class WC_Meta_Box_Coupon_Data {

	/**
	 * Output the metabox.
	 *
	 * @param WP_Post $post
	 */
	public static function output( $post ) {
		wp_nonce_field( 'woocommerce_save_data', 'woocommerce_meta_nonce' );

		$coupon_id = absint( $post->ID );
		$coupon    = new WC_Coupon( $coupon_id );

		?>

		<style type="text/css">
			#edit-slug-box, #minor-publishing-actions { display:none }
		</style>
		<div id="coupon_options" class="panel-wrap coupon_data">

			<div class="wc-tabs-back"></div>

			<ul class="coupon_data_tabs wc-tabs" style="display:none;">
				<?php
				$coupon_data_tabs = apply_filters(
					'woocommerce_coupon_data_tabs',
					array(
						'general'           => array(
							'label'  => __( 'General', 'woocommerce' ),
							'target' => 'general_coupon_data',
							'class'  => 'general_coupon_data',
						),
						'usage_restriction' => array(
							'label'  => __( 'Usage restriction', 'woocommerce' ),
							'target' => 'usage_restriction_coupon_data',
							'class'  => '',
						),
						'usage_limit'       => array(
							'label'  => __( 'Usage limits', 'woocommerce' ),
							'target' => 'usage_limit_coupon_data',
							'class'  => '',
						),
					)
				);

				foreach ( $coupon_data_tabs as $key => $tab ) :
					?>
					<li class="<?php echo $key; ?>_options <?php echo $key; ?>_tab <?php echo implode( ' ', (array) $tab['class'] ); ?>">
						<a href="#<?php echo $tab['target']; ?>">
							<span><?php echo esc_html( $tab['label'] ); ?></span>
						</a>
					</li>
				<?php endforeach; ?>
			</ul>
			<div id="general_coupon_data" class="panel woocommerce_options_panel">
				<?php

				// Type.
				woocommerce_wp_select(
					array(
						'id'      => 'discount_type',
						'label'   => __( 'Discount type', 'woocommerce' ),
						'options' => wc_get_coupon_types(),
						'value'   => $coupon->get_discount_type( 'edit' ),
					)
				);

				// Amount.
				woocommerce_wp_text_input(
					array(
						'id'          => 'coupon_amount',
						'label'       => __( 'Coupon amount', 'woocommerce' ),
						'placeholder' => wc_format_localized_price( 0 ),
						'description' => __( 'Value of the coupon.', 'woocommerce' ),
						'data_type'   => 'percent' === $coupon->get_discount_type( 'edit' ) ? 'decimal' : 'price',
						'desc_tip'    => true,
						'value'       => $coupon->get_amount( 'edit' ),
					)
				);

				// Free Shipping.
				if ( wc_shipping_enabled() ) {
					woocommerce_wp_checkbox(
						array(
							'id'          => 'free_shipping',
							'label'       => __( 'Allow free shipping', 'woocommerce' ),
							'description' => sprintf( __( 'Check this box if the coupon grants free shipping. A <a href="%s" target="_blank">free shipping method</a> must be enabled in your shipping zone and be set to require "a valid free shipping coupon" (see the "Free Shipping Requires" setting).', 'woocommerce' ), 'https://docs.woocommerce.com/document/free-shipping/' ),
							'value'       => wc_bool_to_string( $coupon->get_free_shipping( 'edit' ) ),
						)
					);
				}

				// Expiry date.
				$expiry_date = $coupon->get_date_expires( 'edit' ) ? $coupon->get_date_expires( 'edit' )->date( 'Y-m-d' ) : '';
				woocommerce_wp_text_input(
					array(
						'id'                => 'expiry_date',
						'value'             => esc_attr( $expiry_date ),
						'label'             => __( 'Coupon expiry date', 'woocommerce' ),
						'placeholder'       => 'YYYY-MM-DD',
						'description'       => __( 'The coupon will expire at 00:00:00 of this date.', 'woocommerce' ),
						'desc_tip'          => true,
						'class'             => 'date-picker',
						'custom_attributes' => array(
							'pattern' => apply_filters( 'woocommerce_date_input_html_pattern', '[0-9]{4}-(0[1-9]|1[012])-(0[1-9]|1[0-9]|2[0-9]|3[01])' ),
						),
					)
				);

				do_action( 'woocommerce_coupon_options', $coupon->get_id(), $coupon );

				?>
			</div>
			<div id="usage_restriction_coupon_data" class="panel woocommerce_options_panel">
				<?php

				echo '<div class="options_group">';

				// minimum spend.
				woocommerce_wp_text_input(
					array(
						'id'          => 'minimum_amount',
						'label'       => __( 'Minimum spend', 'woocommerce' ),
						'placeholder' => __( 'No minimum', 'woocommerce' ),
						'description' => __( 'This field allows you to set the minimum spend (subtotal) allowed to use the coupon.', 'woocommerce' ),
						'data_type'   => 'price',
						'desc_tip'    => true,
						'value'       => $coupon->get_minimum_amount( 'edit' ),
					)
				);

				// maximum spend.
				woocommerce_wp_text_input(
					array(
						'id'          => 'maximum_amount',
						'label'       => __( 'Maximum spend', 'woocommerce' ),
						'placeholder' => __( 'No maximum', 'woocommerce' ),
						'description' => __( 'This field allows you to set the maximum spend (subtotal) allowed when using the coupon.', 'woocommerce' ),
						'data_type'   => 'price',
						'desc_tip'    => true,
						'value'       => $coupon->get_maximum_amount( 'edit' ),
					)
				);

				// Individual use.
				woocommerce_wp_checkbox(
					array(
						'id'          => 'individual_use',
						'label'       => __( 'Individual use only', 'woocommerce' ),
						'description' => __( 'Check this box if the coupon cannot be used in conjunction with other coupons.', 'woocommerce' ),
						'value'       => wc_bool_to_string( $coupon->get_individual_use( 'edit' ) ),
					)
				);

				// Exclude Sale Products.
				woocommerce_wp_checkbox(
					array(
						'id'          => 'exclude_sale_items',
						'label'       => __( 'Exclude sale items', 'woocommerce' ),
						'description' => __( 'Check this box if the coupon should not apply to items on sale. Per-item coupons will only work if the item is not on sale. Per-cart coupons will only work if there are items in the cart that are not on sale.', 'woocommerce' ),
						'value'       => wc_bool_to_string( $coupon->get_exclude_sale_items( 'edit' ) ),
					)
				);

				echo '</div><div class="options_group">';

				// Product ids.
				?>
				<p class="form-field">
					<label><?php _e( 'Products', 'woocommerce' ); ?></label>
					<select class="wc-product-search" multiple="multiple" style="width: 50%;" name="product_ids[]" data-placeholder="<?php esc_attr_e( 'Search for a product&hellip;', 'woocommerce' ); ?>" data-action="woocommerce_json_search_products_and_variations">
						<?php
						$product_ids = $coupon->get_product_ids( 'edit' );

						foreach ( $product_ids as $product_id ) {
							$product = wc_get_product( $product_id );
							if ( is_object( $product ) ) {
								echo '<option value="' . esc_attr( $product_id ) . '"' . selected( true, true, false ) . '>' . esc_html( wp_strip_all_tags( $product->get_formatted_name() ) ) . '</option>';
							}
						}
						?>
					</select>
					<?php echo wc_help_tip( __( 'Products that the coupon will be applied to, or that need to be in the cart in order for the "Fixed cart discount" to be applied.', 'woocommerce' ) ); ?>
				</p>

				<?php // Exclude Product ids. ?>
				<p class="form-field">
					<label><?php _e( 'Exclude products', 'woocommerce' ); ?></label>
					<select class="wc-product-search" multiple="multiple" style="width: 50%;" name="exclude_product_ids[]" data-placeholder="<?php esc_attr_e( 'Search for a product&hellip;', 'woocommerce' ); ?>" data-action="woocommerce_json_search_products_and_variations">
						<?php
						$product_ids = $coupon->get_excluded_product_ids( 'edit' );

						foreach ( $product_ids as $product_id ) {
							$product = wc_get_product( $product_id );
							if ( is_object( $product ) ) {
								echo '<option value="' . esc_attr( $product_id ) . '"' . selected( true, true, false ) . '>' . esc_html( wp_strip_all_tags( $product->get_formatted_name() ) ) . '</option>';
							}
						}
						?>
					</select>
					<?php echo wc_help_tip( __( 'Products that the coupon will not be applied to, or that cannot be in the cart in order for the "Fixed cart discount" to be applied.', 'woocommerce' ) ); ?>
				</p>
				<?php

				echo '</div><div class="options_group">';

				// Categories.
				?>
				<p class="form-field">
					<label for="product_categories"><?php _e( 'Product categories', 'woocommerce' ); ?></label>
					<select id="product_categories" name="product_categories[]" style="width: 50%;"  class="wc-enhanced-select" multiple="multiple" data-placeholder="<?php esc_attr_e( 'Any category', 'woocommerce' ); ?>">
						<?php
						$category_ids = $coupon->get_product_categories( 'edit' );
						$categories   = get_terms( 'product_cat', 'orderby=name&hide_empty=0' );

						if ( $categories ) {
							foreach ( $categories as $cat ) {
								echo '<option value="' . esc_attr( $cat->term_id ) . '"' . wc_selected( $cat->term_id, $category_ids ) . '>' . esc_html( $cat->name ) . '</option>';
							}
						}
						?>
					</select> <?php echo wc_help_tip( __( 'Product categories that the coupon will be applied to, or that need to be in the cart in order for the "Fixed cart discount" to be applied.', 'woocommerce' ) ); ?>
				</p>

				<?php // Exclude Categories. ?>
				<p class="form-field">
					<label for="exclude_product_categories"><?php _e( 'Exclude categories', 'woocommerce' ); ?></label>
					<select id="exclude_product_categories" name="exclude_product_categories[]" style="width: 50%;"  class="wc-enhanced-select" multiple="multiple" data-placeholder="<?php esc_attr_e( 'No categories', 'woocommerce' ); ?>">
						<?php
						$category_ids = $coupon->get_excluded_product_categories( 'edit' );
						$categories   = get_terms( 'product_cat', 'orderby=name&hide_empty=0' );

						if ( $categories ) {
							foreach ( $categories as $cat ) {
								echo '<option value="' . esc_attr( $cat->term_id ) . '"' . wc_selected( $cat->term_id, $category_ids ) . '>' . esc_html( $cat->name ) . '</option>';
							}
						}
						?>
					</select>
					<?php echo wc_help_tip( __( 'Product categories that the coupon will not be applied to, or that cannot be in the cart in order for the "Fixed cart discount" to be applied.', 'woocommerce' ) ); ?>
				</p>
			</div>
			<div class="options_group">
				<?php
				// Customers.
				woocommerce_wp_text_input(
					array(
						'id'                => 'customer_email',
						'label'             => __( 'Allowed emails', 'woocommerce' ),
						'placeholder'       => __( 'No restrictions', 'woocommerce' ),
						'description'       => __( 'List of allowed billing emails to check against when an order is placed. Separate email addresses with commas. You can also use an asterisk (*) to match parts of an email. For example "*@gmail.com" would match all gmail addresses.', 'woocommerce' ),
						'value'             => implode( ', ', (array) $coupon->get_email_restrictions( 'edit' ) ),
						'desc_tip'          => true,
						'type'              => 'email',
						'class'             => '',
						'custom_attributes' => array(
							'multiple' => 'multiple',
						),
					)
				);
				?>
			</div>
			<?php do_action( 'woocommerce_coupon_options_usage_restriction', $coupon->get_id(), $coupon ); ?>
			</div>
			<div id="usage_limit_coupon_data" class="panel woocommerce_options_panel">
				<div class="options_group">
					<?php
					// Usage limit per coupons.
					woocommerce_wp_text_input(
						array(
							'id'                => 'usage_limit',
							'label'             => __( 'Usage limit per coupon', 'woocommerce' ),
							'placeholder'       => esc_attr__( 'Unlimited usage', 'woocommerce' ),
							'description'       => __( 'How many times this coupon can be used before it is void.', 'woocommerce' ),
							'type'              => 'number',
							'desc_tip'          => true,
							'class'             => 'short',
							'custom_attributes' => array(
								'step' => 1,
								'min'  => 0,
							),
							'value'             => $coupon->get_usage_limit( 'edit' ) ? $coupon->get_usage_limit( 'edit' ) : '',
						)
					);

					// Usage limit per product.
					woocommerce_wp_text_input(
						array(
							'id'                => 'limit_usage_to_x_items',
							'label'             => __( 'Limit usage to X items', 'woocommerce' ),
							'placeholder'       => esc_attr__( 'Apply to all qualifying items in cart', 'woocommerce' ),
							'description'       => __( 'The maximum number of individual items this coupon can apply to when using product discounts. Leave blank to apply to all qualifying items in cart.', 'woocommerce' ),
							'desc_tip'          => true,
							'class'             => 'short',
							'type'              => 'number',
							'custom_attributes' => array(
								'step' => 1,
								'min'  => 0,
							),
							'value'             => $coupon->get_limit_usage_to_x_items( 'edit' ) ? $coupon->get_limit_usage_to_x_items( 'edit' ) : '',
						)
					);

					// Usage limit per users.
					woocommerce_wp_text_input(
						array(
							'id'                => 'usage_limit_per_user',
							'label'             => __( 'Usage limit per user', 'woocommerce' ),
							'placeholder'       => esc_attr__( 'Unlimited usage', 'woocommerce' ),
							'description'       => __( 'How many times this coupon can be used by an individual user. Uses billing email for guests, and user ID for logged in users.', 'woocommerce' ),
							'desc_tip'          => true,
							'class'             => 'short',
							'type'              => 'number',
							'custom_attributes' => array(
								'step' => 1,
								'min'  => 0,
							),
							'value'             => $coupon->get_usage_limit_per_user( 'edit' ) ? $coupon->get_usage_limit_per_user( 'edit' ) : '',
						)
					);
					?>
				</div>
				<?php do_action( 'woocommerce_coupon_options_usage_limit', $coupon->get_id(), $coupon ); ?>
			</div>
			<?php do_action( 'woocommerce_coupon_data_panels', $coupon->get_id(), $coupon ); ?>
			<div class="clear"></div>
		</div>
		<?php
	}

	/**
	 * Save meta box data.
	 *
	 * @param int     $post_id
	 * @param WP_Post $post
	 */
	public static function save( $post_id, $post ) {
		// Check for dupe coupons.
		$coupon_code  = wc_format_coupon_code( $post->post_title );
		$id_from_code = wc_get_coupon_id_by_code( $coupon_code, $post_id );

		if ( $id_from_code ) {
			WC_Admin_Meta_Boxes::add_error( __( 'Coupon code already exists - customers will use the latest coupon with this code.', 'woocommerce' ) );
		}

		$product_categories         = isset( $_POST['product_categories'] ) ? (array) $_POST['product_categories'] : array();
		$exclude_product_categories = isset( $_POST['exclude_product_categories'] ) ? (array) $_POST['exclude_product_categories'] : array();

		$coupon = new WC_Coupon( $post_id );
		$coupon->set_props(
			array(
				'code'                        => $post->post_title,
				'discount_type'               => wc_clean( $_POST['discount_type'] ),
				'amount'                      => wc_format_decimal( $_POST['coupon_amount'] ),
				'date_expires'                => wc_clean( $_POST['expiry_date'] ),
				'individual_use'              => isset( $_POST['individual_use'] ),
				'product_ids'                 => isset( $_POST['product_ids'] ) ? array_filter( array_map( 'intval', (array) $_POST['product_ids'] ) ) : array(),
				'excluded_product_ids'        => isset( $_POST['exclude_product_ids'] ) ? array_filter( array_map( 'intval', (array) $_POST['exclude_product_ids'] ) ) : array(),
				'usage_limit'                 => absint( $_POST['usage_limit'] ),
				'usage_limit_per_user'        => absint( $_POST['usage_limit_per_user'] ),
				'limit_usage_to_x_items'      => absint( $_POST['limit_usage_to_x_items'] ),
				'free_shipping'               => isset( $_POST['free_shipping'] ),
				'product_categories'          => array_filter( array_map( 'intval', $product_categories ) ),
				'excluded_product_categories' => array_filter( array_map( 'intval', $exclude_product_categories ) ),
				'exclude_sale_items'          => isset( $_POST['exclude_sale_items'] ),
				'minimum_amount'              => wc_format_decimal( $_POST['minimum_amount'] ),
				'maximum_amount'              => wc_format_decimal( $_POST['maximum_amount'] ),
				'email_restrictions'          => array_filter( array_map( 'trim', explode( ',', wc_clean( $_POST['customer_email'] ) ) ) ),
			)
		);
		$coupon->save();
		do_action( 'woocommerce_coupon_options_save', $post_id, $coupon );
	}
}
admin/meta-boxes/views/html-product-attribute.php000064400000011370151336044770016205 0ustar00<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>
<div data-taxonomy="<?php echo esc_attr( $attribute->get_taxonomy() ); ?>" class="woocommerce_attribute wc-metabox postbox closed <?php echo esc_attr( implode( ' ', $metabox_class ) ); ?>" rel="<?php echo esc_attr( $attribute->get_position() ); ?>">
	<h3>
		<a href="#" class="remove_row delete"><?php esc_html_e( 'Remove', 'woocommerce' ); ?></a>
		<div class="handlediv" title="<?php esc_attr_e( 'Click to toggle', 'woocommerce' ); ?>"></div>
		<div class="tips sort" data-tip="<?php esc_attr_e( 'Drag and drop to set admin attribute order', 'woocommerce' ); ?>"></div>			
		<strong class="attribute_name"><?php echo wc_attribute_label( $attribute->get_name() ); ?></strong>
	</h3>
	<div class="woocommerce_attribute_data wc-metabox-content hidden">
		<table cellpadding="0" cellspacing="0">
			<tbody>
				<tr>
					<td class="attribute_name">
						<label><?php esc_html_e( 'Name', 'woocommerce' ); ?>:</label>

						<?php if ( $attribute->is_taxonomy() ) : ?>
							<strong><?php echo wc_attribute_label( $attribute->get_name() ); ?></strong>
							<input type="hidden" name="attribute_names[<?php echo esc_attr( $i ); ?>]" value="<?php echo esc_attr( $attribute->get_name() ); ?>" />
						<?php else : ?>
							<input type="text" class="attribute_name" name="attribute_names[<?php echo esc_attr( $i ); ?>]" value="<?php echo esc_attr( $attribute->get_name() ); ?>" />
						<?php endif; ?>

						<input type="hidden" name="attribute_position[<?php echo esc_attr( $i ); ?>]" class="attribute_position" value="<?php echo esc_attr( $attribute->get_position() ); ?>" />
					</td>
					<td rowspan="3">
						<label><?php esc_html_e( 'Value(s)', 'woocommerce' ); ?>:</label>
						<?php
						if ( $attribute->is_taxonomy() && $attribute_taxonomy = $attribute->get_taxonomy_object() ) {
							$attribute_types = wc_get_attribute_types();

							if ( ! array_key_exists( $attribute_taxonomy->attribute_type, $attribute_types ) ) {
								$attribute_taxonomy->attribute_type = 'select';
							}

							if ( 'select' === $attribute_taxonomy->attribute_type ) {
								?>
								<select multiple="multiple" data-placeholder="<?php esc_attr_e( 'Select terms', 'woocommerce' ); ?>" class="multiselect attribute_values wc-enhanced-select" name="attribute_values[<?php echo esc_attr( $i ); ?>][]">
									<?php
									$args      = array(
										'orderby'    => ! empty( $attribute_taxonomy->attribute_orderby ) ? $attribute_taxonomy->attribute_orderby : 'name',
										'hide_empty' => 0,
									);
									$all_terms = get_terms( $attribute->get_taxonomy(), apply_filters( 'woocommerce_product_attribute_terms', $args ) );
									if ( $all_terms ) {
										foreach ( $all_terms as $term ) {
											$options = $attribute->get_options();
											$options = ! empty( $options ) ? $options : array();
											echo '<option value="' . esc_attr( $term->term_id ) . '"' . wc_selected( $term->term_id, $options ) . '>' . esc_html( apply_filters( 'woocommerce_product_attribute_term_name', $term->name, $term ) ) . '</option>';
										}
									}
									?>
								</select>
								<button class="button plus select_all_attributes"><?php esc_html_e( 'Select all', 'woocommerce' ); ?></button>
								<button class="button minus select_no_attributes"><?php esc_html_e( 'Select none', 'woocommerce' ); ?></button>
								<button class="button fr plus add_new_attribute"><?php esc_html_e( 'Add new', 'woocommerce' ); ?></button>
								<?php
							}

							do_action( 'woocommerce_product_option_terms', $attribute_taxonomy, $i, $attribute );
						} else {
							/* translators: %s: WC_DELIMITER */
							?>
							<textarea name="attribute_values[<?php echo esc_attr( $i ); ?>]" cols="5" rows="5" placeholder="<?php printf( esc_attr__( 'Enter some text, or some attributes by "%s" separating values.', 'woocommerce' ), WC_DELIMITER ); ?>"><?php echo esc_textarea( wc_implode_text_attributes( $attribute->get_options() ) ); ?></textarea>
							<?php
						}
						?>
					</td>
				</tr>
				<tr>
					<td>
						<label><input type="checkbox" class="checkbox" <?php checked( $attribute->get_visible(), true ); ?> name="attribute_visibility[<?php echo esc_attr( $i ); ?>]" value="1" /> <?php esc_html_e( 'Visible on the product page', 'woocommerce' ); ?></label>
					</td>
				</tr>
				<tr>
					<td>
						<div class="enable_variation show_if_variable">
							<label><input type="checkbox" class="checkbox" <?php checked( $attribute->get_variation(), true ); ?> name="attribute_variation[<?php echo esc_attr( $i ); ?>]" value="1" /> <?php esc_html_e( 'Used for variations', 'woocommerce' ); ?></label>
						</div>
					</td>
				</tr>
				<?php do_action( 'woocommerce_after_product_attribute_settings', $attribute, $i ); ?>
			</tbody>
		</table>
	</div>
</div>
admin/meta-boxes/views/html-product-data-panel.php000064400000004434151336044770016213 0ustar00<?php
/**
 * Product data meta box.
 *
 * @package WooCommerce\Admin
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>

<div class="panel-wrap product_data">

	<span class="type_box hidden"> &mdash;
		<label for="product-type">
			<select id="product-type" name="product-type">
				<optgroup label="<?php esc_attr_e( 'Product Type', 'woocommerce' ); ?>">
				<?php foreach ( wc_get_product_types() as $value => $label ) : ?>
					<option value="<?php echo esc_attr( $value ); ?>" <?php echo selected( $product_object->get_type(), $value, false ); ?>><?php echo esc_html( $label ); ?></option>
				<?php endforeach; ?>
				</optgroup>
			</select>
		</label>

		<?php
		foreach ( self::get_product_type_options() as $key => $option ) :
			if ( metadata_exists( 'post', $post->ID, '_' . $key ) ) {
				$selected_value = is_callable( array( $product_object, "is_$key" ) ) ? $product_object->{"is_$key"}() : 'yes' === get_post_meta( $post->ID, '_' . $key, true );
			} else {
				$selected_value = 'yes' === ( isset( $option['default'] ) ? $option['default'] : 'no' );
			}
			?>
			<label for="<?php echo esc_attr( $option['id'] ); ?>" class="<?php echo esc_attr( $option['wrapper_class'] ); ?> tips" data-tip="<?php echo esc_attr( $option['description'] ); ?>">
				<?php echo esc_html( $option['label'] ); ?>:
				<input type="checkbox" name="<?php echo esc_attr( $option['id'] ); ?>" id="<?php echo esc_attr( $option['id'] ); ?>" <?php echo checked( $selected_value, true, false ); ?> />
			</label>
		<?php endforeach; ?>
	</span>

	<ul class="product_data_tabs wc-tabs">
		<?php foreach ( self::get_product_data_tabs() as $key => $tab ) : ?>
			<li class="<?php echo esc_attr( $key ); ?>_options <?php echo esc_attr( $key ); ?>_tab <?php echo esc_attr( isset( $tab['class'] ) ? implode( ' ', (array) $tab['class'] ) : '' ); ?>">
				<a href="#<?php echo esc_attr( $tab['target'] ); ?>"><span><?php echo esc_html( $tab['label'] ); ?></span></a>
			</li>
		<?php endforeach; ?>
		<?php do_action( 'woocommerce_product_write_panel_tabs' ); ?>
	</ul>

	<?php
		self::output_tabs();
		self::output_variations();
		do_action( 'woocommerce_product_data_panels' );
		wc_do_deprecated_action( 'woocommerce_product_write_panels', array(), '2.6', 'Use woocommerce_product_data_panels action instead.' );
	?>
	<div class="clear"></div>
</div>
admin/meta-boxes/views/html-order-download-permission.php000064400000007034151336044770017634 0ustar00<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>
<div class="wc-metabox closed">
	<h3 class="fixed">
		<button type="button" data-permission_id="<?php echo esc_attr( $download->get_id() ); ?>" rel="<?php echo esc_attr( $download->get_product_id() ) . ',' . esc_attr( $download->get_download_id() ); ?>" class="revoke_access button"><?php esc_html_e( 'Revoke access', 'woocommerce' ); ?></button>
		<div class="handlediv" aria-label="<?php esc_attr_e( 'Click to toggle', 'woocommerce' ); ?>"></div>
		<strong>
			<?php
			printf(
				'#%s &mdash; %s &mdash; %s: %s &mdash; ',
				esc_html( $product->get_id() ),
				esc_html( apply_filters( 'woocommerce_admin_download_permissions_title', $product->get_name(), $download->get_product_id(), $download->get_order_id(), $download->get_order_key(), $download->get_download_id() ) ),
				esc_html( $file_count ),
				esc_html( wc_get_filename_from_url( $product->get_file_download_path( $download->get_download_id() ) ) )
			);
			printf( _n( 'Downloaded %s time', 'Downloaded %s times', $download->get_download_count(), 'woocommerce' ), esc_html( $download->get_download_count() ) )
			?>
		</strong>
	</h3>
	<table cellpadding="0" cellspacing="0" class="wc-metabox-content">
		<tbody>
			<tr>
				<td>
					<label><?php esc_html_e( 'Downloads remaining', 'woocommerce' ); ?></label>
					<input type="hidden" name="permission_id[<?php echo esc_attr( $loop ); ?>]" value="<?php echo esc_attr( $download->get_id() ); ?>" />
					<input type="number" step="1" min="0" class="short" name="downloads_remaining[<?php echo esc_attr( $loop ); ?>]" value="<?php echo esc_attr( $download->get_downloads_remaining() ); ?>" placeholder="<?php esc_attr_e( 'Unlimited', 'woocommerce' ); ?>" />
				</td>
				<td>
					<label><?php esc_html_e( 'Access expires', 'woocommerce' ); ?></label>
					<input type="text" class="short date-picker" name="access_expires[<?php echo esc_attr( $loop ); ?>]" value="<?php echo ! is_null( $download->get_access_expires() ) ? esc_attr( date_i18n( 'Y-m-d', $download->get_access_expires()->getTimestamp() ) ) : ''; ?>" maxlength="10" placeholder="<?php esc_attr_e( 'Never', 'woocommerce' ); ?>" pattern="<?php echo esc_attr( apply_filters( 'woocommerce_date_input_html_pattern', '[0-9]{4}-(0[1-9]|1[012])-(0[1-9]|1[0-9]|2[0-9]|3[01])' ) ); ?>" />
				</td>
				<td>
					<label><?php esc_html_e( 'Customer download link', 'woocommerce' ); ?></label>
					<?php
					$download_link = add_query_arg(
						array(
							'download_file' => $download->get_product_id(),
							'order'         => $download->get_order_key(),
							'email'         => urlencode( $download->get_user_email() ),
							'key'           => $download->get_download_id(),
						),
						trailingslashit( home_url() )
					);
					?>
					<a id="copy-download-link" class="button" href="<?php echo esc_url( $download_link ); ?>" data-tip="<?php esc_attr_e( 'Copied!', 'woocommerce' ); ?>" data-tip-failed="<?php esc_attr_e( 'Copying to clipboard failed. You should be able to right-click the button and copy.', 'woocommerce' ); ?>"><?php esc_html_e( 'Copy link', 'woocommerce' ); ?></a>
				</td>
				<td>
					<label><?php esc_html_e( 'Customer download log', 'woocommerce' ); ?></label>
					<?php
					$report_url = add_query_arg(
						'permission_id',
						rawurlencode( $download->get_id() ),
						admin_url( 'admin.php?page=wc-reports&tab=orders&report=downloads' )
					);
					echo '<a class="button" href="' . esc_url( $report_url ) . '">';
					esc_html_e( 'View report', 'woocommerce' );
					echo '</a>';
					?>
				</td>
			</tr>
		</tbody>
	</table>
</div>
admin/meta-boxes/views/html-order-notes.php000064400000003076151336044770014771 0ustar00<?php
/**
 * Order notes HTML for meta box.
 *
 * @package WooCommerce\Admin
 */

defined( 'ABSPATH' ) || exit;

?>
<ul class="order_notes">
	<?php
	if ( $notes ) {
		foreach ( $notes as $note ) {
			$css_class   = array( 'note' );
			$css_class[] = $note->customer_note ? 'customer-note' : '';
			$css_class[] = 'system' === $note->added_by ? 'system-note' : '';
			$css_class   = apply_filters( 'woocommerce_order_note_class', array_filter( $css_class ), $note );
			?>
			<li rel="<?php echo absint( $note->id ); ?>" class="<?php echo esc_attr( implode( ' ', $css_class ) ); ?>">
				<div class="note_content">
					<?php echo wpautop( wptexturize( wp_kses_post( $note->content ) ) ); // @codingStandardsIgnoreLine ?>
				</div>
				<p class="meta">
					<abbr class="exact-date" title="<?php echo esc_attr( $note->date_created->date( 'Y-m-d H:i:s' ) ); ?>">
						<?php
						/* translators: %1$s: note date %2$s: note time */
						echo esc_html( sprintf( __( '%1$s at %2$s', 'woocommerce' ), $note->date_created->date_i18n( wc_date_format() ), $note->date_created->date_i18n( wc_time_format() ) ) );
						?>
					</abbr>
					<?php
					if ( 'system' !== $note->added_by ) :
						/* translators: %s: note author */
						echo esc_html( sprintf( ' ' . __( 'by %s', 'woocommerce' ), $note->added_by ) );
					endif;
					?>
					<a href="#" class="delete_note" role="button"><?php esc_html_e( 'Delete note', 'woocommerce' ); ?></a>
				</p>
			</li>
			<?php
		}
	} else {
		?>
		<li class="no-items"><?php esc_html_e( 'There are no notes yet.', 'woocommerce' ); ?></li>
		<?php
	}
	?>
</ul>
admin/meta-boxes/views/html-product-data-attributes.php000064400000004171151336044770017300 0ustar00<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>
<div id="product_attributes" class="panel wc-metaboxes-wrapper hidden">
	<div class="toolbar toolbar-top">
		<span class="expand-close">
			<a href="#" class="expand_all"><?php esc_html_e( 'Expand', 'woocommerce' ); ?></a> / <a href="#" class="close_all"><?php esc_html_e( 'Close', 'woocommerce' ); ?></a>
		</span>
		<select name="attribute_taxonomy" class="attribute_taxonomy">
			<option value=""><?php esc_html_e( 'Custom product attribute', 'woocommerce' ); ?></option>
			<?php
			global $wc_product_attributes;

			// Array of defined attribute taxonomies.
			$attribute_taxonomies = wc_get_attribute_taxonomies();

			if ( ! empty( $attribute_taxonomies ) ) {
				foreach ( $attribute_taxonomies as $tax ) {
					$attribute_taxonomy_name = wc_attribute_taxonomy_name( $tax->attribute_name );
					$label                   = $tax->attribute_label ? $tax->attribute_label : $tax->attribute_name;
					echo '<option value="' . esc_attr( $attribute_taxonomy_name ) . '">' . esc_html( $label ) . '</option>';
				}
			}
			?>
		</select>
		<button type="button" class="button add_attribute"><?php esc_html_e( 'Add', 'woocommerce' ); ?></button>
	</div>
	<div class="product_attributes wc-metaboxes">
		<?php
		// Product attributes - taxonomies and custom, ordered, with visibility and variation attributes set.
		$attributes = $product_object->get_attributes( 'edit' );
		$i          = -1;

		foreach ( $attributes as $attribute ) {
			$i++;
			$metabox_class = array();

			if ( $attribute->is_taxonomy() ) {
				$metabox_class[] = 'taxonomy';
				$metabox_class[] = $attribute->get_name();
			}

			include __DIR__ . '/html-product-attribute.php';
		}
		?>
	</div>
	<div class="toolbar">
		<span class="expand-close">
			<a href="#" class="expand_all"><?php esc_html_e( 'Expand', 'woocommerce' ); ?></a> / <a href="#" class="close_all"><?php esc_html_e( 'Close', 'woocommerce' ); ?></a>
		</span>
		<button type="button" class="button save_attributes button-primary"><?php esc_html_e( 'Save attributes', 'woocommerce' ); ?></button>
	</div>
	<?php do_action( 'woocommerce_product_options_attributes' ); ?>
</div>
admin/meta-boxes/views/html-variation-admin.php000064400000055274151336044770015621 0ustar00<?php
/**
 * Outputs a variation for editing.
 *
 * @package WooCommerce\Admin
 * @var int $variation_id
 * @var WP_POST $variation
 * @var WC_Product_Variation $variation_object
 * @var array $variation_data array of variation data @deprecated 4.4.0.
 */

defined( 'ABSPATH' ) || exit;

?>
<div class="woocommerce_variation wc-metabox closed">
	<h3>
		<a href="#" class="remove_variation delete" rel="<?php echo esc_attr( $variation_id ); ?>"><?php esc_html_e( 'Remove', 'woocommerce' ); ?></a>
		<div class="handlediv" aria-label="<?php esc_attr_e( 'Click to toggle', 'woocommerce' ); ?>"></div>
		<div class="tips sort" data-tip="<?php esc_attr_e( 'Drag and drop, or click to set admin variation order', 'woocommerce' ); ?>"></div>
		<strong>#<?php echo esc_html( $variation_id ); ?> </strong>
		<?php
		$attribute_values = $variation_object->get_attributes( 'edit' );

		foreach ( $product_object->get_attributes( 'edit' ) as $attribute ) {
			if ( ! $attribute->get_variation() ) {
				continue;
			}
			$selected_value = isset( $attribute_values[ sanitize_title( $attribute->get_name() ) ] ) ? $attribute_values[ sanitize_title( $attribute->get_name() ) ] : '';
			?>
			<select name="attribute_<?php echo esc_attr( sanitize_title( $attribute->get_name() ) . "[{$loop}]" ); ?>">
				<option value="">
					<?php
					/* translators: %s: attribute label */
					printf( esc_html__( 'Any %s&hellip;', 'woocommerce' ), wc_attribute_label( $attribute->get_name() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
					?>
				</option>
				<?php if ( $attribute->is_taxonomy() ) : ?>
					<?php foreach ( $attribute->get_terms() as $option ) : ?>
						<option <?php selected( $selected_value, $option->slug ); ?> value="<?php echo esc_attr( $option->slug ); ?>"><?php echo esc_html( apply_filters( 'woocommerce_variation_option_name', $option->name, $option, $attribute->get_name(), $product_object ) ); ?></option>
					<?php endforeach; ?>
				<?php else : ?>
					<?php foreach ( $attribute->get_options() as $option ) : ?>
						<option <?php selected( $selected_value, $option ); ?> value="<?php echo esc_attr( $option ); ?>"><?php echo esc_html( apply_filters( 'woocommerce_variation_option_name', $option, null, $attribute->get_name(), $product_object ) ); ?></option>
					<?php endforeach; ?>
				<?php endif; ?>
			</select>
			<?php
		}
		?>
		<input type="hidden" class="variable_post_id" name="variable_post_id[<?php echo esc_attr( $loop ); ?>]" value="<?php echo esc_attr( $variation_id ); ?>" />
		<input type="hidden" class="variation_menu_order" name="variation_menu_order[<?php echo esc_attr( $loop ); ?>]" value="<?php echo esc_attr( $variation_object->get_menu_order( 'edit' ) ); ?>" />

		<?php
		/**
		 * Variations header action.
		 *
		 * @since 3.6.0
		 *
		 * @param WP_Post $variation Post data.
		 */
		do_action( 'woocommerce_variation_header', $variation );
		?>
	</h3>
	<div class="woocommerce_variable_attributes wc-metabox-content" style="display: none;">
		<div class="data">
			<p class="form-row form-row-first upload_image">
				<a href="#" class="upload_image_button tips <?php echo $variation_object->get_image_id( 'edit' ) ? 'remove' : ''; ?>" data-tip="<?php echo $variation_object->get_image_id( 'edit' ) ? esc_attr__( 'Remove this image', 'woocommerce' ) : esc_attr__( 'Upload an image', 'woocommerce' ); ?>" rel="<?php echo esc_attr( $variation_id ); ?>">
					<img src="<?php echo $variation_object->get_image_id( 'edit' ) ? esc_url( wp_get_attachment_thumb_url( $variation_object->get_image_id( 'edit' ) ) ) : esc_url( wc_placeholder_img_src() ); ?>" /><input type="hidden" name="upload_image_id[<?php echo esc_attr( $loop ); ?>]" class="upload_image_id" value="<?php echo esc_attr( $variation_object->get_image_id( 'edit' ) ); ?>" />
				</a>
			</p>
			<?php
			if ( wc_product_sku_enabled() ) {
				woocommerce_wp_text_input(
					array(
						'id'            => "variable_sku{$loop}",
						'name'          => "variable_sku[{$loop}]",
						'value'         => $variation_object->get_sku( 'edit' ),
						'placeholder'   => $variation_object->get_sku(),
						'label'         => '<abbr title="' . esc_attr__( 'Stock Keeping Unit', 'woocommerce' ) . '">' . esc_html__( 'SKU', 'woocommerce' ) . '</abbr>',
						'desc_tip'      => true,
						'description'   => __( 'SKU refers to a Stock-keeping unit, a unique identifier for each distinct product and service that can be purchased.', 'woocommerce' ),
						'wrapper_class' => 'form-row form-row-last',
					)
				);
			}
			?>
			<p class="form-row form-row-full options">
				<label>
					<?php esc_html_e( 'Enabled', 'woocommerce' ); ?>
					<input type="checkbox" class="checkbox" name="variable_enabled[<?php echo esc_attr( $loop ); ?>]" <?php checked( in_array( $variation_object->get_status( 'edit' ), array( 'publish', false ), true ), true ); ?> />
				</label>
				<label class="tips" data-tip="<?php esc_attr_e( 'Enable this option if access is given to a downloadable file upon purchase of a product', 'woocommerce' ); ?>">
					<?php esc_html_e( 'Downloadable', 'woocommerce' ); ?>
					<input type="checkbox" class="checkbox variable_is_downloadable" name="variable_is_downloadable[<?php echo esc_attr( $loop ); ?>]" <?php checked( $variation_object->get_downloadable( 'edit' ), true ); ?> />
				</label>
				<label class="tips" data-tip="<?php esc_attr_e( 'Enable this option if a product is not shipped or there is no shipping cost', 'woocommerce' ); ?>">
					<?php esc_html_e( 'Virtual', 'woocommerce' ); ?>
					<input type="checkbox" class="checkbox variable_is_virtual" name="variable_is_virtual[<?php echo esc_attr( $loop ); ?>]" <?php checked( $variation_object->get_virtual( 'edit' ), true ); ?> />
				</label>

				<?php if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) : ?>
					<label class="tips" data-tip="<?php esc_attr_e( 'Enable this option to enable stock management at variation level', 'woocommerce' ); ?>">
						<?php esc_html_e( 'Manage stock?', 'woocommerce' ); ?>
						<input type="checkbox" class="checkbox variable_manage_stock" name="variable_manage_stock[<?php echo esc_attr( $loop ); ?>]" <?php checked( $variation_object->get_manage_stock(), true ); // Use view context so 'parent' is considered. ?> />
					</label>
				<?php endif; ?>

				<?php do_action( 'woocommerce_variation_options', $loop, $variation_data, $variation ); ?>
			</p>

			<div class="variable_pricing">
				<?php
				$label = sprintf(
					/* translators: %s: currency symbol */
					__( 'Regular price (%s)', 'woocommerce' ),
					get_woocommerce_currency_symbol()
				);

				woocommerce_wp_text_input(
					array(
						'id'            => "variable_regular_price_{$loop}",
						'name'          => "variable_regular_price[{$loop}]",
						'value'         => wc_format_localized_price( $variation_object->get_regular_price( 'edit' ) ),
						'label'         => $label,
						'data_type'     => 'price',
						'wrapper_class' => 'form-row form-row-first',
						'placeholder'   => __( 'Variation price (required)', 'woocommerce' ),
					)
				);

				$label = sprintf(
					/* translators: %s: currency symbol */
					__( 'Sale price (%s)', 'woocommerce' ),
					get_woocommerce_currency_symbol()
				);

				woocommerce_wp_text_input(
					array(
						'id'            => "variable_sale_price{$loop}",
						'name'          => "variable_sale_price[{$loop}]",
						'value'         => wc_format_localized_price( $variation_object->get_sale_price( 'edit' ) ),
						'data_type'     => 'price',
						'label'         => $label . ' <a href="#" class="sale_schedule">' . esc_html__( 'Schedule', 'woocommerce' ) . '</a><a href="#" class="cancel_sale_schedule hidden">' . esc_html__( 'Cancel schedule', 'woocommerce' ) . '</a>',
						'wrapper_class' => 'form-row form-row-last',
					)
				);

				$sale_price_dates_from_timestamp = $variation_object->get_date_on_sale_from( 'edit' ) ? $variation_object->get_date_on_sale_from( 'edit' )->getOffsetTimestamp() : false;
				$sale_price_dates_to_timestamp   = $variation_object->get_date_on_sale_to( 'edit' ) ? $variation_object->get_date_on_sale_to( 'edit' )->getOffsetTimestamp() : false;

				$sale_price_dates_from = $sale_price_dates_from_timestamp ? date_i18n( 'Y-m-d', $sale_price_dates_from_timestamp ) : '';
				$sale_price_dates_to   = $sale_price_dates_to_timestamp ? date_i18n( 'Y-m-d', $sale_price_dates_to_timestamp ) : '';

				echo '<div class="form-field sale_price_dates_fields hidden">
					<p class="form-row form-row-first">
						<label>' . esc_html__( 'Sale start date', 'woocommerce' ) . '</label>
						<input type="text" class="sale_price_dates_from" name="variable_sale_price_dates_from[' . esc_attr( $loop ) . ']" value="' . esc_attr( $sale_price_dates_from ) . '" placeholder="' . esc_attr_x( 'From&hellip;', 'placeholder', 'woocommerce' ) . ' YYYY-MM-DD" maxlength="10" pattern="' . esc_attr( apply_filters( 'woocommerce_date_input_html_pattern', '[0-9]{4}-(0[1-9]|1[012])-(0[1-9]|1[0-9]|2[0-9]|3[01])' ) ) . '" />
					</p>
					<p class="form-row form-row-last">
						<label>' . esc_html__( 'Sale end date', 'woocommerce' ) . '</label>
						<input type="text" class="sale_price_dates_to" name="variable_sale_price_dates_to[' . esc_attr( $loop ) . ']" value="' . esc_attr( $sale_price_dates_to ) . '" placeholder="' . esc_attr_x( 'To&hellip;', 'placeholder', 'woocommerce' ) . '  YYYY-MM-DD" maxlength="10" pattern="' . esc_attr( apply_filters( 'woocommerce_date_input_html_pattern', '[0-9]{4}-(0[1-9]|1[012])-(0[1-9]|1[0-9]|2[0-9]|3[01])' ) ) . '" />
					</p>
				</div>';

				/**
				 * Variation options pricing action.
				 *
				 * @since 2.5.0
				 *
				 * @param int     $loop           Position in the loop.
				 * @param array   $variation_data Variation data.
				 * @param WP_Post $variation      Post data.
				 */
				do_action( 'woocommerce_variation_options_pricing', $loop, $variation_data, $variation );
				?>
			</div>

			<?php if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) : ?>
				<div class="show_if_variation_manage_stock" style="display: none;">
					<?php
					woocommerce_wp_text_input(
						array(
							'id'                => "variable_stock{$loop}",
							'name'              => "variable_stock[{$loop}]",
							'value'             => wc_stock_amount( $variation_object->get_stock_quantity( 'edit' ) ),
							'label'             => __( 'Stock quantity', 'woocommerce' ),
							'desc_tip'          => true,
							'description'       => __( "Enter a number to set stock quantity at the variation level. Use a variation's 'Manage stock?' check box above to enable/disable stock management at the variation level.", 'woocommerce' ),
							'type'              => 'number',
							'custom_attributes' => array(
								'step' => 'any',
							),
							'data_type'         => 'stock',
							'wrapper_class'     => 'form-row form-row-first',
						)
					);

					echo '<input type="hidden" name="variable_original_stock[' . esc_attr( $loop ) . ']" value="' . esc_attr( wc_stock_amount( $variation_object->get_stock_quantity( 'edit' ) ) ) . '" />';

					woocommerce_wp_select(
						array(
							'id'            => "variable_backorders{$loop}",
							'name'          => "variable_backorders[{$loop}]",
							'value'         => $variation_object->get_backorders( 'edit' ),
							'label'         => __( 'Allow backorders?', 'woocommerce' ),
							'options'       => wc_get_product_backorder_options(),
							'desc_tip'      => true,
							'description'   => __( 'If managing stock, this controls whether or not backorders are allowed. If enabled, stock quantity can go below 0.', 'woocommerce' ),
							'wrapper_class' => 'form-row form-row-last',
						)
					);

					$low_stock_placeholder = ( $product_object->get_manage_stock() && '' !== $product_object->get_low_stock_amount() )
						? sprintf(
							/* translators: %d: Amount of stock left */
							esc_attr__( 'Parent product\'s threshold (%d)', 'woocommerce' ),
							esc_attr( $product_object->get_low_stock_amount() )
						)
						: sprintf(
							/* translators: %d: Amount of stock left */
							esc_attr__( 'Store-wide threshold (%d)', 'woocommerce' ),
							esc_attr( get_option( 'woocommerce_notify_low_stock_amount' ) )
						);

					woocommerce_wp_text_input(
						array(
							'id'                => "variable_low_stock_amount{$loop}",
							'name'              => "variable_low_stock_amount[{$loop}]",
							'value'             => $variation_object->get_low_stock_amount( 'edit' ),
							'placeholder'       => $low_stock_placeholder,
							'label'             => __( 'Low stock threshold', 'woocommerce' ),
							'desc_tip'          => true,
							'description'       => __( 'When variation stock reaches this amount you will be notified by email. The default value for all variations can be set in the product Inventory tab. The shop default value can be set in Settings > Products > Inventory.', 'woocommerce' ),
							'type'              => 'number',
							'custom_attributes' => array(
								'step' => 'any',
							),
							'wrapper_class' => 'form-row',
						)
					);

					/**
					 * Variation options inventory action.
					 *
					 * @since 2.5.0
					 *
					 * @param int     $loop           Position in the loop.
					 * @param array   $variation_data Variation data.
					 * @param WP_Post $variation      Post data.
					 */
					do_action( 'woocommerce_variation_options_inventory', $loop, $variation_data, $variation );
					?>
				</div>
			<?php endif; ?>

			<div>
				<?php
				woocommerce_wp_select(
					array(
						'id'            => "variable_stock_status{$loop}",
						'name'          => "variable_stock_status[{$loop}]",
						'value'         => $variation_object->get_stock_status( 'edit' ),
						'label'         => __( 'Stock status', 'woocommerce' ),
						'options'       => wc_get_product_stock_status_options(),
						'desc_tip'      => true,
						'description'   => __( 'Controls whether or not the product is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ),
						'wrapper_class' => 'form-row form-row-full variable_stock_status',
					)
				);

				if ( wc_product_weight_enabled() ) {
					$label = sprintf(
						/* translators: %s: weight unit */
						__( 'Weight (%s)', 'woocommerce' ),
						esc_html( get_option( 'woocommerce_weight_unit' ) )
					);

					woocommerce_wp_text_input(
						array(
							'id'            => "variable_weight{$loop}",
							'name'          => "variable_weight[{$loop}]",
							'value'         => wc_format_localized_decimal( $variation_object->get_weight( 'edit' ) ),
							'placeholder'   => wc_format_localized_decimal( $product_object->get_weight() ),
							'label'         => $label,
							'desc_tip'      => true,
							'description'   => __( 'Weight in decimal form', 'woocommerce' ),
							'type'          => 'text',
							'data_type'     => 'decimal',
							'wrapper_class' => 'form-row form-row-first hide_if_variation_virtual',
						)
					);
				}

				if ( wc_product_dimensions_enabled() ) {
					$parent_length = wc_format_localized_decimal( $product_object->get_length() );
					$parent_width  = wc_format_localized_decimal( $product_object->get_width() );
					$parent_height = wc_format_localized_decimal( $product_object->get_height() );

					?>
					<p class="form-field form-row dimensions_field hide_if_variation_virtual form-row-last">
						<label for="product_length">
							<?php
							printf(
								/* translators: %s: dimension unit */
								esc_html__( 'Dimensions (L&times;W&times;H) (%s)', 'woocommerce' ),
								esc_html( get_option( 'woocommerce_dimension_unit' ) )
							);
							?>
						</label>
						<?php echo wc_help_tip( __( 'Length x width x height in decimal form', 'woocommerce' ) ); ?>
						<span class="wrap">
							<input id="product_length" placeholder="<?php echo $parent_length ? esc_attr( $parent_length ) : esc_attr__( 'Length', 'woocommerce' ); ?>" class="input-text wc_input_decimal" size="6" type="text" name="variable_length[<?php echo esc_attr( $loop ); ?>]" value="<?php echo esc_attr( wc_format_localized_decimal( $variation_object->get_length( 'edit' ) ) ); ?>" />
							<input placeholder="<?php echo $parent_width ? esc_attr( $parent_width ) : esc_attr__( 'Width', 'woocommerce' ); ?>" class="input-text wc_input_decimal" size="6" type="text" name="variable_width[<?php echo esc_attr( $loop ); ?>]" value="<?php echo esc_attr( wc_format_localized_decimal( $variation_object->get_width( 'edit' ) ) ); ?>" />
							<input placeholder="<?php echo $parent_height ? esc_attr( $parent_height ) : esc_attr__( 'Height', 'woocommerce' ); ?>" class="input-text wc_input_decimal last" size="6" type="text" name="variable_height[<?php echo esc_attr( $loop ); ?>]" value="<?php echo esc_attr( wc_format_localized_decimal( $variation_object->get_height( 'edit' ) ) ); ?>" />
						</span>
					</p>
					<?php
				}

				/**
				 * Variation options dimensions action.
				 *
				 * @since 2.5.0
				 *
				 * @param int     $loop           Position in the loop.
				 * @param array   $variation_data Variation data.
				 * @param WP_Post $variation      Post data.
				 */
				do_action( 'woocommerce_variation_options_dimensions', $loop, $variation_data, $variation );
				?>
			</div>

			<div>
				<p class="form-row hide_if_variation_virtual form-row-full">
					<label><?php esc_html_e( 'Shipping class', 'woocommerce' ); ?></label>
					<?php
					wp_dropdown_categories(
						array(
							'taxonomy'         => 'product_shipping_class',
							'hide_empty'       => 0,
							'show_option_none' => __( 'Same as parent', 'woocommerce' ),
							'name'             => 'variable_shipping_class[' . $loop . ']',
							'id'               => '',
							'selected'         => $variation_object->get_shipping_class_id( 'edit' ),
						)
					);
					?>
				</p>

				<?php
				if ( wc_tax_enabled() ) {
					woocommerce_wp_select(
						array(
							'id'            => "variable_tax_class{$loop}",
							'name'          => "variable_tax_class[{$loop}]",
							'value'         => $variation_object->get_tax_class( 'edit' ),
							'label'         => __( 'Tax class', 'woocommerce' ),
							'options'       => array( 'parent' => __( 'Same as parent', 'woocommerce' ) ) + wc_get_product_tax_class_options(),
							'desc_tip'      => 'true',
							'description'   => __( 'Choose a tax class for this product. Tax classes are used to apply different tax rates specific to certain types of product.', 'woocommerce' ),
							'wrapper_class' => 'form-row form-row-full',
						)
					);

					/**
					 * Variation options tax action.
					 *
					 * @since 2.5.0
					 *
					 * @param int     $loop           Position in the loop.
					 * @param array   $variation_data Variation data.
					 * @param WP_Post $variation      Post data.
					 */
					do_action( 'woocommerce_variation_options_tax', $loop, $variation_data, $variation );
				}
				?>
			</div>
			<div>
				<?php
				woocommerce_wp_textarea_input(
					array(
						'id'            => "variable_description{$loop}",
						'name'          => "variable_description[{$loop}]",
						'value'         => $variation_object->get_description( 'edit' ),
						'label'         => __( 'Description', 'woocommerce' ),
						'desc_tip'      => true,
						'description'   => __( 'Enter an optional description for this variation.', 'woocommerce' ),
						'wrapper_class' => 'form-row form-row-full',
					)
				);
				?>
			</div>
			<div class="show_if_variation_downloadable" style="display: none;">
				<div class="form-row form-row-full downloadable_files">
					<label><?php esc_html_e( 'Downloadable files', 'woocommerce' ); ?></label>
					<table class="widefat">
						<thead>
							<div>
								<th><?php esc_html_e( 'Name', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'This is the name of the download shown to the customer.', 'woocommerce' ) ); ?></th>
								<th colspan="2"><?php esc_html_e( 'File URL', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'This is the URL or absolute path to the file which customers will get access to. URLs entered here should already be encoded.', 'woocommerce' ) ); ?></th>
								<th>&nbsp;</th>
							</div>
						</thead>
						<tbody>
							<?php
							$downloads = $variation_object->get_downloads( 'edit' );

							if ( $downloads ) {
								foreach ( $downloads as $key => $file ) {
									include __DIR__ . '/html-product-variation-download.php';
								}
							}
							?>
						</tbody>
						<tfoot>
							<div>
								<th colspan="4">
									<a href="#" class="button insert" data-row="
									<?php
									$key  = '';
									$file = array(
										'file' => '',
										'name' => '',
									);
									ob_start();
									require __DIR__ . '/html-product-variation-download.php';
									echo esc_attr( ob_get_clean() );
									?>
									"><?php esc_html_e( 'Add file', 'woocommerce' ); ?></a>
								</th>
							</div>
						</tfoot>
					</table>
				</div>
			</div>
			<div class="show_if_variation_downloadable" style="display: none;">
				<?php
				woocommerce_wp_text_input(
					array(
						'id'                => "variable_download_limit{$loop}",
						'name'              => "variable_download_limit[{$loop}]",
						'value'             => $variation_object->get_download_limit( 'edit' ) < 0 ? '' : $variation_object->get_download_limit( 'edit' ),
						'label'             => __( 'Download limit', 'woocommerce' ),
						'placeholder'       => __( 'Unlimited', 'woocommerce' ),
						'description'       => __( 'Leave blank for unlimited re-downloads.', 'woocommerce' ),
						'type'              => 'number',
						'desc_tip'          => true,
						'custom_attributes' => array(
							'step' => '1',
							'min'  => '0',
						),
						'wrapper_class'     => 'form-row form-row-first',
					)
				);

				woocommerce_wp_text_input(
					array(
						'id'                => "variable_download_expiry{$loop}",
						'name'              => "variable_download_expiry[{$loop}]",
						'value'             => $variation_object->get_download_expiry( 'edit' ) < 0 ? '' : $variation_object->get_download_expiry( 'edit' ),
						'label'             => __( 'Download expiry', 'woocommerce' ),
						'placeholder'       => __( 'Never', 'woocommerce' ),
						'description'       => __( 'Enter the number of days before a download link expires, or leave blank.', 'woocommerce' ),
						'type'              => 'number',
						'desc_tip'          => true,
						'custom_attributes' => array(
							'step' => '1',
							'min'  => '0',
						),
						'wrapper_class'     => 'form-row form-row-last',
					)
				);

				/**
				 * Variation options download action.
				 *
				 * @since 2.5.0
				 *
				 * @param int     $loop           Position in the loop.
				 * @param array   $variation_data Variation data.
				 * @param WP_Post $variation      Post data.
				 */
				do_action( 'woocommerce_variation_options_download', $loop, $variation_data, $variation );
				?>
			</div>
			<?php do_action( 'woocommerce_product_after_variable_attributes', $loop, $variation_data, $variation ); ?>
		</div>
	</div>
</div>
admin/meta-boxes/views/html-order-refund.php000064400000004655151336044770015130 0ustar00<?php
/**
 * Show order refund
 *
 * @var object $refund The refund object.
 * @package WooCommerce\Admin
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

$who_refunded = new WP_User( $refund->get_refunded_by() );
?>
<tr class="refund <?php echo ( ! empty( $class ) ) ? esc_attr( $class ) : ''; ?>" data-order_refund_id="<?php echo esc_attr( $refund->get_id() ); ?>">
	<td class="thumb"><div></div></td>

	<td class="name">
		<?php
		if ( $who_refunded->exists() ) {
			printf(
				/* translators: 1: refund id 2: refund date 3: username */
				esc_html__( 'Refund #%1$s - %2$s by %3$s', 'woocommerce' ),
				esc_html( $refund->get_id() ),
				esc_html( wc_format_datetime( $refund->get_date_created(), get_option( 'date_format' ) . ', ' . get_option( 'time_format' ) ) ),
				sprintf(
					'<abbr class="refund_by" title="%1$s">%2$s</abbr>',
					/* translators: 1: ID who refunded */
					sprintf( esc_attr__( 'ID: %d', 'woocommerce' ), absint( $who_refunded->ID ) ),
					esc_html( $who_refunded->display_name )
				)
			);
		} else {
			printf(
				/* translators: 1: refund id 2: refund date */
				esc_html__( 'Refund #%1$s - %2$s', 'woocommerce' ),
				esc_html( $refund->get_id() ),
				esc_html( wc_format_datetime( $refund->get_date_created(), get_option( 'date_format' ) . ', ' . get_option( 'time_format' ) ) )
			);
		}
		?>
		<?php if ( $refund->get_reason() ) : ?>
			<p class="description"><?php echo esc_html( $refund->get_reason() ); ?></p>
		<?php endif; ?>
		<input type="hidden" class="order_refund_id" name="order_refund_id[]" value="<?php echo esc_attr( $refund->get_id() ); ?>" />

		<?php do_action( 'woocommerce_after_order_refund_item_name', $refund ); ?>
	</td>

	<?php do_action( 'woocommerce_admin_order_item_values', null, $refund, $refund->get_id() ); ?>

	<td class="item_cost" width="1%">&nbsp;</td>
	<td class="quantity" width="1%">&nbsp;</td>

	<td class="line_cost" width="1%">
		<div class="view">
			<?php
			echo wp_kses_post(
				wc_price( '-' . $refund->get_amount(), array( 'currency' => $refund->get_currency() ) )
			);
			?>
		</div>
	</td>

	<?php
	if ( wc_tax_enabled() ) :
		$total_taxes = count( $order_taxes );
		?>
		<?php for ( $i = 0;  $i < $total_taxes; $i++ ) : ?>
			<td class="line_tax" width="1%"></td>
		<?php endfor; ?>
	<?php endif; ?>

	<td class="wc-order-edit-line-item">
		<div class="wc-order-edit-line-item-actions">
			<a class="delete_refund" href="#"></a>
		</div>
	</td>
</tr>
admin/meta-boxes/views/html-order-shipping.php000064400000012141151336044770015453 0ustar00<?php
/**
 * Shows a shipping line
 *
 * @package WooCommerce\Admin
 *
 * @var object $item The item being displayed
 * @var int $item_id The id of the item being displayed
 *
 * @package WooCommerce\Admin\Views
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>
<tr class="shipping <?php echo ( ! empty( $class ) ) ? esc_attr( $class ) : ''; ?>" data-order_item_id="<?php echo esc_attr( $item_id ); ?>">
	<td class="thumb"><div></div></td>

	<td class="name">
		<div class="view">
			<?php echo esc_html( $item->get_name() ? $item->get_name() : __( 'Shipping', 'woocommerce' ) ); ?>
		</div>
		<div class="edit" style="display: none;">
			<input type="hidden" name="shipping_method_id[]" value="<?php echo esc_attr( $item_id ); ?>" />
			<input type="text" class="shipping_method_name" placeholder="<?php esc_attr_e( 'Shipping name', 'woocommerce' ); ?>" name="shipping_method_title[<?php echo esc_attr( $item_id ); ?>]" value="<?php echo esc_attr( $item->get_name() ); ?>" />
			<select class="shipping_method" name="shipping_method[<?php echo esc_attr( $item_id ); ?>]">
				<optgroup label="<?php esc_attr_e( 'Shipping method', 'woocommerce' ); ?>">
					<option value=""><?php esc_html_e( 'N/A', 'woocommerce' ); ?></option>
					<?php
					$found_method = false;

					foreach ( $shipping_methods as $method ) {
						$is_active = $item->get_method_id() === $method->id;

						echo '<option value="' . esc_attr( $method->id ) . '" ' . selected( true, $is_active, false ) . '>' . esc_html( $method->get_method_title() ) . '</option>';

						if ( $is_active ) {
							$found_method = true;
						}
					}

					if ( ! $found_method && $item->get_method_id() ) {
						echo '<option value="' . esc_attr( $item->get_method_id() ) . '" selected="selected">' . esc_html__( 'Other', 'woocommerce' ) . '</option>';
					} else {
						echo '<option value="other">' . esc_html__( 'Other', 'woocommerce' ) . '</option>';
					}
					?>
				</optgroup>
			</select>
		</div>

		<?php do_action( 'woocommerce_before_order_itemmeta', $item_id, $item, null ); ?>
		<?php require __DIR__ . '/html-order-item-meta.php'; ?>
		<?php do_action( 'woocommerce_after_order_itemmeta', $item_id, $item, null ); ?>
	</td>

	<?php do_action( 'woocommerce_admin_order_item_values', null, $item, absint( $item_id ) ); ?>

	<td class="item_cost" width="1%">&nbsp;</td>
	<td class="quantity" width="1%">&nbsp;</td>

	<td class="line_cost" width="1%">
		<div class="view">
			<?php
			echo wp_kses_post( wc_price( $item->get_total(), array( 'currency' => $order->get_currency() ) ) );
			$refunded = $order->get_total_refunded_for_item( $item_id, 'shipping' );
			if ( $refunded ) {
				echo wp_kses_post( '<small class="refunded">-' . wc_price( $refunded, array( 'currency' => $order->get_currency() ) ) . '</small>' );
			}
			?>
		</div>
		<div class="edit" style="display: none;">
			<input type="text" name="shipping_cost[<?php echo esc_attr( $item_id ); ?>]" placeholder="<?php echo esc_attr( wc_format_localized_price( 0 ) ); ?>" value="<?php echo esc_attr( wc_format_localized_price( $item->get_total() ) ); ?>" class="line_total wc_input_price" />
		</div>
		<div class="refund" style="display: none;">
			<input type="text" name="refund_line_total[<?php echo absint( $item_id ); ?>]" placeholder="<?php echo esc_attr( wc_format_localized_price( 0 ) ); ?>" class="refund_line_total wc_input_price" />
		</div>
	</td>

	<?php
	$tax_data = $item->get_taxes();
	if ( $tax_data && wc_tax_enabled() ) {
		foreach ( $order_taxes as $tax_item ) {
			$tax_item_id    = $tax_item->get_rate_id();
			$tax_item_total = isset( $tax_data['total'][ $tax_item_id ] ) ? $tax_data['total'][ $tax_item_id ] : '';
			?>
			<td class="line_tax" width="1%">
				<div class="view">
					<?php
					echo wp_kses_post( ( '' !== $tax_item_total ) ? wc_price( $tax_item_total, array( 'currency' => $order->get_currency() ) ) : '&ndash;' );
					$refunded = $order->get_tax_refunded_for_item( $item_id, $tax_item_id, 'shipping' );
					if ( $refunded ) {
						echo wp_kses_post( '<small class="refunded">-' . wc_price( $refunded, array( 'currency' => $order->get_currency() ) ) . '</small>' );
					}
					?>
				</div>
				<div class="edit" style="display: none;">
					<input type="text" name="shipping_taxes[<?php echo absint( $item_id ); ?>][<?php echo esc_attr( $tax_item_id ); ?>]" placeholder="<?php echo esc_attr( wc_format_localized_price( 0 ) ); ?>" value="<?php echo ( isset( $tax_item_total ) ) ? esc_attr( wc_format_localized_price( $tax_item_total ) ) : ''; ?>" class="line_tax wc_input_price" />
				</div>
				<div class="refund" style="display: none;">
					<input type="text" name="refund_line_tax[<?php echo absint( $item_id ); ?>][<?php echo esc_attr( $tax_item_id ); ?>]" placeholder="<?php echo esc_attr( wc_format_localized_price( 0 ) ); ?>" class="refund_line_tax wc_input_price" data-tax_id="<?php echo esc_attr( $tax_item_id ); ?>" />
				</div>
			</td>
			<?php
		}
	}
	?>
	<td class="wc-order-edit-line-item">
		<?php if ( $order->is_editable() ) : ?>
			<div class="wc-order-edit-line-item-actions">
				<a class="edit-order-item" href="#"></a><a class="delete-order-item" href="#"></a>
			</div>
		<?php endif; ?>
	</td>
</tr>
admin/meta-boxes/views/html-product-data-variations.php000064400000024751151336044770017277 0ustar00<?php
/**
 * Product data variations
 *
 * @package WooCommerce\Admin\Metaboxes\Views
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>
<div id="variable_product_options" class="panel wc-metaboxes-wrapper hidden">
	<div id="variable_product_options_inner">

		<?php if ( ! count( $variation_attributes ) ) : ?>

			<div id="message" class="inline notice woocommerce-message">
				<p><?php echo wp_kses_post( __( 'Before you can add a variation you need to add some variation attributes on the <strong>Attributes</strong> tab.', 'woocommerce' ) ); ?></p>
				<p><a class="button-primary" href="<?php echo esc_url( apply_filters( 'woocommerce_docs_url', 'https://docs.woocommerce.com/document/variable-product/', 'product-variations' ) ); ?>" target="_blank"><?php esc_html_e( 'Learn more', 'woocommerce' ); ?></a></p>
			</div>

		<?php else : ?>

			<div class="toolbar toolbar-variations-defaults">
				<div class="variations-defaults">
					<strong><?php esc_html_e( 'Default Form Values', 'woocommerce' ); ?>: <?php echo wc_help_tip( __( 'These are the attributes that will be pre-selected on the frontend.', 'woocommerce' ) ); ?></strong>
					<?php
					foreach ( $variation_attributes as $attribute ) {
						$selected_value = isset( $default_attributes[ sanitize_title( $attribute->get_name() ) ] ) ? $default_attributes[ sanitize_title( $attribute->get_name() ) ] : '';
						?>
						<select name="default_attribute_<?php echo esc_attr( sanitize_title( $attribute->get_name() ) ); ?>" data-current="<?php echo esc_attr( $selected_value ); ?>">
							<?php /* translators: WooCommerce attribute label */ ?>
							<option value=""><?php echo esc_html( sprintf( __( 'No default %s&hellip;', 'woocommerce' ), wc_attribute_label( $attribute->get_name() ) ) ); ?></option>
							<?php if ( $attribute->is_taxonomy() ) : ?>
								<?php foreach ( $attribute->get_terms() as $option ) : ?>
									<option <?php selected( $selected_value, $option->slug ); ?> value="<?php echo esc_attr( $option->slug ); ?>"><?php echo esc_html( apply_filters( 'woocommerce_variation_option_name', $option->name, $option, $attribute->get_name(), $product_object ) ); ?></option>
								<?php endforeach; ?>
							<?php else : ?>
								<?php foreach ( $attribute->get_options() as $option ) : ?>
									<option <?php selected( $selected_value, $option ); ?> value="<?php echo esc_attr( $option ); ?>"><?php echo esc_html( apply_filters( 'woocommerce_variation_option_name', $option, null, $attribute->get_name(), $product_object ) ); ?></option>
								<?php endforeach; ?>
							<?php endif; ?>
						</select>
						<?php
					}
					?>
				</div>
				<div class="clear"></div>
			</div>

			<?php do_action( 'woocommerce_variable_product_before_variations' ); ?>

			<div class="toolbar toolbar-top">
				<select id="field_to_edit" class="variation_actions">
					<option data-global="true" value="add_variation"><?php esc_html_e( 'Add variation', 'woocommerce' ); ?></option>
					<option data-global="true" value="link_all_variations"><?php esc_html_e( 'Create variations from all attributes', 'woocommerce' ); ?></option>
					<option value="delete_all"><?php esc_html_e( 'Delete all variations', 'woocommerce' ); ?></option>
					<optgroup label="<?php esc_attr_e( 'Status', 'woocommerce' ); ?>">
						<option value="toggle_enabled"><?php esc_html_e( 'Toggle &quot;Enabled&quot;', 'woocommerce' ); ?></option>
						<option value="toggle_downloadable"><?php esc_html_e( 'Toggle &quot;Downloadable&quot;', 'woocommerce' ); ?></option>
						<option value="toggle_virtual"><?php esc_html_e( 'Toggle &quot;Virtual&quot;', 'woocommerce' ); ?></option>
					</optgroup>
					<optgroup label="<?php esc_attr_e( 'Pricing', 'woocommerce' ); ?>">
						<option value="variable_regular_price"><?php esc_html_e( 'Set regular prices', 'woocommerce' ); ?></option>
						<option value="variable_regular_price_increase"><?php esc_html_e( 'Increase regular prices (fixed amount or percentage)', 'woocommerce' ); ?></option>
						<option value="variable_regular_price_decrease"><?php esc_html_e( 'Decrease regular prices (fixed amount or percentage)', 'woocommerce' ); ?></option>
						<option value="variable_sale_price"><?php esc_html_e( 'Set sale prices', 'woocommerce' ); ?></option>
						<option value="variable_sale_price_increase"><?php esc_html_e( 'Increase sale prices (fixed amount or percentage)', 'woocommerce' ); ?></option>
						<option value="variable_sale_price_decrease"><?php esc_html_e( 'Decrease sale prices (fixed amount or percentage)', 'woocommerce' ); ?></option>
						<option value="variable_sale_schedule"><?php esc_html_e( 'Set scheduled sale dates', 'woocommerce' ); ?></option>
					</optgroup>
					<optgroup label="<?php esc_attr_e( 'Inventory', 'woocommerce' ); ?>">
						<option value="toggle_manage_stock"><?php esc_html_e( 'Toggle &quot;Manage stock&quot;', 'woocommerce' ); ?></option>
						<option value="variable_stock"><?php esc_html_e( 'Stock', 'woocommerce' ); ?></option>
						<option value="variable_stock_status_instock"><?php esc_html_e( 'Set Status - In stock', 'woocommerce' ); ?></option>
						<option value="variable_stock_status_outofstock"><?php esc_html_e( 'Set Status - Out of stock', 'woocommerce' ); ?></option>
						<option value="variable_stock_status_onbackorder"><?php esc_html_e( 'Set Status - On backorder', 'woocommerce' ); ?></option>
						<option value="variable_low_stock_amount"><?php esc_html_e( 'Low stock threshold', 'woocommerce' ); ?></option>
					</optgroup>
					<optgroup label="<?php esc_attr_e( 'Shipping', 'woocommerce' ); ?>">
						<option value="variable_length"><?php esc_html_e( 'Length', 'woocommerce' ); ?></option>
						<option value="variable_width"><?php esc_html_e( 'Width', 'woocommerce' ); ?></option>
						<option value="variable_height"><?php esc_html_e( 'Height', 'woocommerce' ); ?></option>
						<option value="variable_weight"><?php esc_html_e( 'Weight', 'woocommerce' ); ?></option>
					</optgroup>
					<optgroup label="<?php esc_attr_e( 'Downloadable products', 'woocommerce' ); ?>">
						<option value="variable_download_limit"><?php esc_html_e( 'Download limit', 'woocommerce' ); ?></option>
						<option value="variable_download_expiry"><?php esc_html_e( 'Download expiry', 'woocommerce' ); ?></option>
					</optgroup>
					<?php do_action( 'woocommerce_variable_product_bulk_edit_actions' ); ?>
				</select>
				<a class="button bulk_edit do_variation_action"><?php esc_html_e( 'Go', 'woocommerce' ); ?></a>

				<div class="variations-pagenav">
					<?php /* translators: variations count */ ?>
					<span class="displaying-num"><?php echo esc_html( sprintf( _n( '%s item', '%s items', $variations_count, 'woocommerce' ), $variations_count ) ); ?></span>
					<span class="expand-close">
						(<a href="#" class="expand_all"><?php esc_html_e( 'Expand', 'woocommerce' ); ?></a> / <a href="#" class="close_all"><?php esc_html_e( 'Close', 'woocommerce' ); ?></a>)
					</span>
					<span class="pagination-links">
						<a class="first-page disabled" title="<?php esc_attr_e( 'Go to the first page', 'woocommerce' ); ?>" href="#">&laquo;</a>
						<a class="prev-page disabled" title="<?php esc_attr_e( 'Go to the previous page', 'woocommerce' ); ?>" href="#">&lsaquo;</a>
						<span class="paging-select">
							<label for="current-page-selector-1" class="screen-reader-text"><?php esc_html_e( 'Select Page', 'woocommerce' ); ?></label>
							<select class="page-selector" id="current-page-selector-1" title="<?php esc_attr_e( 'Current page', 'woocommerce' ); ?>">
								<?php for ( $i = 1; $i <= $variations_total_pages; $i++ ) : ?>
									<option value="<?php echo $i; // WPCS: XSS ok. ?>"><?php echo $i; // WPCS: XSS ok. ?></option>
								<?php endfor; ?>
							</select>
							<?php echo esc_html_x( 'of', 'number of pages', 'woocommerce' ); ?> <span class="total-pages"><?php echo esc_html( $variations_total_pages ); ?></span>
						</span>
						<a class="next-page" title="<?php esc_attr_e( 'Go to the next page', 'woocommerce' ); ?>" href="#">&rsaquo;</a>
						<a class="last-page" title="<?php esc_attr_e( 'Go to the last page', 'woocommerce' ); ?>" href="#">&raquo;</a>
					</span>
				</div>
				<div class="clear"></div>
			</div>

			<div class="woocommerce_variations wc-metaboxes" data-attributes="<?php echo wc_esc_json( wp_json_encode( wc_list_pluck( $variation_attributes, 'get_data' ) ) ); // WPCS: XSS ok. ?>" data-total="<?php echo esc_attr( $variations_count ); ?>" data-total_pages="<?php echo esc_attr( $variations_total_pages ); ?>" data-page="1" data-edited="false"></div>

			<div class="toolbar">
				<button type="button" class="button-primary save-variation-changes" disabled="disabled"><?php esc_html_e( 'Save changes', 'woocommerce' ); ?></button>
				<button type="button" class="button cancel-variation-changes" disabled="disabled"><?php esc_html_e( 'Cancel', 'woocommerce' ); ?></button>

				<div class="variations-pagenav">
					<?php /* translators: variations count*/ ?>
					<span class="displaying-num"><?php echo esc_html( sprintf( _n( '%s item', '%s items', $variations_count, 'woocommerce' ), $variations_count ) ); ?></span>
					<span class="expand-close">
						(<a href="#" class="expand_all"><?php esc_html_e( 'Expand', 'woocommerce' ); ?></a> / <a href="#" class="close_all"><?php esc_html_e( 'Close', 'woocommerce' ); ?></a>)
					</span>
					<span class="pagination-links">
						<a class="first-page disabled" title="<?php esc_attr_e( 'Go to the first page', 'woocommerce' ); ?>" href="#">&laquo;</a>
						<a class="prev-page disabled" title="<?php esc_attr_e( 'Go to the previous page', 'woocommerce' ); ?>" href="#">&lsaquo;</a>
						<span class="paging-select">
							<label for="current-page-selector-1" class="screen-reader-text"><?php esc_html_e( 'Select Page', 'woocommerce' ); ?></label>
							<select class="page-selector" id="current-page-selector-1" title="<?php esc_attr_e( 'Current page', 'woocommerce' ); ?>">
								<?php for ( $i = 1; $i <= $variations_total_pages; $i++ ) : ?>
									<option value="<?php echo $i; // WPCS: XSS ok. ?>"><?php echo $i; // WPCS: XSS ok. ?></option>
								<?php endfor; ?>
							</select>
							<?php echo esc_html_x( 'of', 'number of pages', 'woocommerce' ); ?> <span class="total-pages"><?php echo esc_html( $variations_total_pages ); ?></span>
						</span>
						<a class="next-page" title="<?php esc_attr_e( 'Go to the next page', 'woocommerce' ); ?>" href="#">&rsaquo;</a>
						<a class="last-page" title="<?php esc_attr_e( 'Go to the last page', 'woocommerce' ); ?>" href="#">&raquo;</a>
					</span>
				</div>
				<div class="clear"></div>
			</div>

		<?php endif; ?>
	</div>
</div>
admin/meta-boxes/views/html-order-fee.php000064400000007742151336044770014404 0ustar00<?php
/**
 * Shows an order item fee
 *
 * @var object $item The item being displayed
 * @var int $item_id The id of the item being displayed
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>
<tr class="fee <?php echo ( ! empty( $class ) ) ? esc_attr( $class ) : ''; ?>" data-order_item_id="<?php echo esc_attr( $item_id ); ?>">
	<td class="thumb"><div></div></td>

	<td class="name">
		<div class="view">
			<?php echo esc_html( $item->get_name() ? $item->get_name() : __( 'Fee', 'woocommerce' ) ); ?>
		</div>
		<div class="edit" style="display: none;">
			<input type="text" placeholder="<?php esc_attr_e( 'Fee name', 'woocommerce' ); ?>" name="order_item_name[<?php echo absint( $item_id ); ?>]" value="<?php echo ( $item->get_name() ) ? esc_attr( $item->get_name() ) : ''; ?>" />
			<input type="hidden" class="order_item_id" name="order_item_id[]" value="<?php echo esc_attr( $item_id ); ?>" />
			<input type="hidden" name="order_item_tax_class[<?php echo absint( $item_id ); ?>]" value="<?php echo esc_attr( $item->get_tax_class() ); ?>" />
		</div>
		<?php do_action( 'woocommerce_after_order_fee_item_name', $item_id, $item, null ); ?>
	</td>

	<?php do_action( 'woocommerce_admin_order_item_values', null, $item, absint( $item_id ) ); ?>

	<td class="item_cost" width="1%">&nbsp;</td>
	<td class="quantity" width="1%">&nbsp;</td>

	<td class="line_cost" width="1%">
		<div class="view">
			<?php
			echo wc_price( $item->get_total(), array( 'currency' => $order->get_currency() ) );

			if ( $refunded = $order->get_total_refunded_for_item( $item_id, 'fee' ) ) {
				echo '<small class="refunded">-' . wc_price( $refunded, array( 'currency' => $order->get_currency() ) ) . '</small>';
			}
			?>
		</div>
		<div class="edit" style="display: none;">
			<input type="text" name="line_total[<?php echo absint( $item_id ); ?>]" placeholder="<?php echo esc_attr( wc_format_localized_price( 0 ) ); ?>" value="<?php echo esc_attr( wc_format_localized_price( $item->get_total() ) ); ?>" class="line_total wc_input_price" />
		</div>
		<div class="refund" style="display: none;">
			<input type="text" name="refund_line_total[<?php echo absint( $item_id ); ?>]" placeholder="<?php echo esc_attr( wc_format_localized_price( 0 ) ); ?>" class="refund_line_total wc_input_price" />
		</div>
	</td>

	<?php
	if ( ( $tax_data = $item->get_taxes() ) && wc_tax_enabled() ) {
		foreach ( $order_taxes as $tax_item ) {
			$tax_item_id    = $tax_item->get_rate_id();
			$tax_item_total = isset( $tax_data['total'][ $tax_item_id ] ) ? $tax_data['total'][ $tax_item_id ] : '';
			?>
			<td class="line_tax" width="1%">
				<div class="view">
					<?php
					echo ( '' !== $tax_item_total ) ? wc_price( wc_round_tax_total( $tax_item_total ), array( 'currency' => $order->get_currency() ) ) : '&ndash;';

					if ( $refunded = $order->get_tax_refunded_for_item( $item_id, $tax_item_id, 'fee' ) ) {
						echo '<small class="refunded">-' . wc_price( $refunded, array( 'currency' => $order->get_currency() ) ) . '</small>';
					}
					?>
				</div>
				<div class="edit" style="display: none;">
					<input type="text" name="line_tax[<?php echo absint( $item_id ); ?>][<?php echo esc_attr( $tax_item_id ); ?>]" placeholder="<?php echo esc_attr( wc_format_localized_price( 0 ) ); ?>" value="<?php echo ( isset( $tax_item_total ) ) ? esc_attr( wc_format_localized_price( $tax_item_total ) ) : ''; ?>" class="line_tax wc_input_price" />
				</div>
				<div class="refund" style="display: none;">
					<input type="text" name="refund_line_tax[<?php echo absint( $item_id ); ?>][<?php echo esc_attr( $tax_item_id ); ?>]" placeholder="<?php echo esc_attr( wc_format_localized_price( 0 ) ); ?>" class="refund_line_tax wc_input_price" data-tax_id="<?php echo esc_attr( $tax_item_id ); ?>" />
				</div>
			</td>
			<?php
		}
	}
	?>
	<td class="wc-order-edit-line-item">
		<?php if ( $order->is_editable() ) : ?>
			<div class="wc-order-edit-line-item-actions">
				<a class="edit-order-item" href="#"></a><a class="delete-order-item" href="#"></a>
			</div>
		<?php endif; ?>
	</td>
</tr>
admin/meta-boxes/views/html-order-items.php000064400000051715151336044770014765 0ustar00<?php
/**
 * Order items HTML for meta box.
 *
 * @package WooCommerce\Admin
 */

defined( 'ABSPATH' ) || exit;

global $wpdb;

$payment_gateway     = wc_get_payment_gateway_by_order( $order );
$line_items          = $order->get_items( apply_filters( 'woocommerce_admin_order_item_types', 'line_item' ) );
$discounts           = $order->get_items( 'discount' );
$line_items_fee      = $order->get_items( 'fee' );
$line_items_shipping = $order->get_items( 'shipping' );

if ( wc_tax_enabled() ) {
	$order_taxes      = $order->get_taxes();
	$tax_classes      = WC_Tax::get_tax_classes();
	$classes_options  = wc_get_product_tax_class_options();
	$show_tax_columns = count( $order_taxes ) === 1;
}
?>
<div class="woocommerce_order_items_wrapper wc-order-items-editable">
	<table cellpadding="0" cellspacing="0" class="woocommerce_order_items">
		<thead>
			<tr>
				<th class="item sortable" colspan="2" data-sort="string-ins"><?php esc_html_e( 'Item', 'woocommerce' ); ?></th>
				<?php do_action( 'woocommerce_admin_order_item_headers', $order ); ?>
				<th class="item_cost sortable" data-sort="float"><?php esc_html_e( 'Cost', 'woocommerce' ); ?></th>
				<th class="quantity sortable" data-sort="int"><?php esc_html_e( 'Qty', 'woocommerce' ); ?></th>
				<th class="line_cost sortable" data-sort="float"><?php esc_html_e( 'Total', 'woocommerce' ); ?></th>
				<?php
				if ( ! empty( $order_taxes ) ) :
					foreach ( $order_taxes as $tax_id => $tax_item ) :
						$tax_class      = wc_get_tax_class_by_tax_id( $tax_item['rate_id'] );
						$tax_class_name = isset( $classes_options[ $tax_class ] ) ? $classes_options[ $tax_class ] : __( 'Tax', 'woocommerce' );
						$column_label   = ! empty( $tax_item['label'] ) ? $tax_item['label'] : __( 'Tax', 'woocommerce' );
						/* translators: %1$s: tax item name %2$s: tax class name  */
						$column_tip = sprintf( esc_html__( '%1$s (%2$s)', 'woocommerce' ), $tax_item['name'], $tax_class_name );
						?>
						<th class="line_tax tips" data-tip="<?php echo esc_attr( $column_tip ); ?>">
							<?php echo esc_attr( $column_label ); ?>
							<input type="hidden" class="order-tax-id" name="order_taxes[<?php echo esc_attr( $tax_id ); ?>]" value="<?php echo esc_attr( $tax_item['rate_id'] ); ?>">
							<?php if ( $order->is_editable() ) : ?>
								<a class="delete-order-tax" href="#" data-rate_id="<?php echo esc_attr( $tax_id ); ?>"></a>
							<?php endif; ?>
						</th>
						<?php
					endforeach;
				endif;
				?>
				<th class="wc-order-edit-line-item" width="1%">&nbsp;</th>
			</tr>
		</thead>
		<tbody id="order_line_items">
			<?php
			foreach ( $line_items as $item_id => $item ) {
				do_action( 'woocommerce_before_order_item_' . $item->get_type() . '_html', $item_id, $item, $order );

				include __DIR__ . '/html-order-item.php';

				do_action( 'woocommerce_order_item_' . $item->get_type() . '_html', $item_id, $item, $order );
			}
			do_action( 'woocommerce_admin_order_items_after_line_items', $order->get_id() );
			?>
		</tbody>
		<tbody id="order_fee_line_items">
			<?php
			foreach ( $line_items_fee as $item_id => $item ) {
				include __DIR__ . '/html-order-fee.php';
			}
			do_action( 'woocommerce_admin_order_items_after_fees', $order->get_id() );
			?>
		</tbody>
		<tbody id="order_shipping_line_items">
			<?php
			$shipping_methods = WC()->shipping() ? WC()->shipping()->load_shipping_methods() : array();
			foreach ( $line_items_shipping as $item_id => $item ) {
				include __DIR__ . '/html-order-shipping.php';
			}
			do_action( 'woocommerce_admin_order_items_after_shipping', $order->get_id() );
			?>
		</tbody>
		<tbody id="order_refunds">
			<?php
			$refunds = $order->get_refunds();

			if ( $refunds ) {
				foreach ( $refunds as $refund ) {
					include __DIR__ . '/html-order-refund.php';
				}
				do_action( 'woocommerce_admin_order_items_after_refunds', $order->get_id() );
			}
			?>
		</tbody>
	</table>
</div>
<div class="wc-order-data-row wc-order-totals-items wc-order-items-editable">
	<?php
	$coupons = $order->get_items( 'coupon' );
	if ( $coupons ) :
		?>
		<div class="wc-used-coupons">
			<ul class="wc_coupon_list">
				<li><strong><?php esc_html_e( 'Coupon(s)', 'woocommerce' ); ?></strong></li>
				<?php
				foreach ( $coupons as $item_id => $item ) :
					$post_id = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish' LIMIT 1;", $item->get_code() ) ); // phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited
					$class   = $order->is_editable() ? 'code editable' : 'code';
					?>
					<li class="<?php echo esc_attr( $class ); ?>">
						<?php if ( $post_id ) : ?>
							<?php
							$post_url = apply_filters(
								'woocommerce_admin_order_item_coupon_url',
								add_query_arg(
									array(
										'post'   => $post_id,
										'action' => 'edit',
									),
									admin_url( 'post.php' )
								),
								$item,
								$order
							);
							?>
							<a href="<?php echo esc_url( $post_url ); ?>" class="tips" data-tip="<?php echo esc_attr( wc_price( $item->get_discount(), array( 'currency' => $order->get_currency() ) ) ); ?>">
								<span><?php echo esc_html( $item->get_code() ); ?></span>
							</a>
						<?php else : ?>
							<span class="tips" data-tip="<?php echo esc_attr( wc_price( $item->get_discount(), array( 'currency' => $order->get_currency() ) ) ); ?>">
								<span><?php echo esc_html( $item->get_code() ); ?></span>
							</span>
						<?php endif; ?>
						<?php if ( $order->is_editable() ) : ?>
							<a class="remove-coupon" href="javascript:void(0)" aria-label="Remove" data-code="<?php echo esc_attr( $item->get_code() ); ?>"></a>
						<?php endif; ?>
					</li>
				<?php endforeach; ?>
			</ul>
		</div>
	<?php endif; ?>
	<table class="wc-order-totals">
			<tr>
				<td class="label"><?php esc_html_e( 'Items Subtotal:', 'woocommerce' ); ?></td>
				<td width="1%"></td>
				<td class="total">
					<?php echo wc_price( $order->get_subtotal(), array( 'currency' => $order->get_currency() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
				</td>
			</tr>
		<?php if ( 0 < $order->get_total_discount() ) : ?>
			<tr>
				<td class="label"><?php esc_html_e( 'Coupon(s):', 'woocommerce' ); ?></td>
				<td width="1%"></td>
				<td class="total">-
					<?php echo wc_price( $order->get_total_discount(), array( 'currency' => $order->get_currency() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
				</td>
			</tr>
		<?php endif; ?>
		<?php if ( 0 < $order->get_total_fees() ) : ?>
			<tr>
				<td class="label"><?php esc_html_e( 'Fees:', 'woocommerce' ); ?></td>
				<td width="1%"></td>
				<td class="total">
					<?php echo wc_price( $order->get_total_fees(), array( 'currency' => $order->get_currency() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
				</td>
			</tr>
		<?php endif; ?>

		<?php do_action( 'woocommerce_admin_order_totals_after_discount', $order->get_id() ); ?>

		<?php if ( $order->get_shipping_methods() ) : ?>
			<tr>
				<td class="label"><?php esc_html_e( 'Shipping:', 'woocommerce' ); ?></td>
				<td width="1%"></td>
				<td class="total">
					<?php echo wc_price( $order->get_shipping_total(), array( 'currency' => $order->get_currency() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
				</td>
			</tr>
		<?php endif; ?>

		<?php do_action( 'woocommerce_admin_order_totals_after_shipping', $order->get_id() ); ?>

		<?php if ( wc_tax_enabled() ) : ?>
			<?php foreach ( $order->get_tax_totals() as $code => $tax_total ) : ?>
				<tr>
					<td class="label"><?php echo esc_html( $tax_total->label ); ?>:</td>
					<td width="1%"></td>
					<td class="total">
						<?php
							// We use wc_round_tax_total here because tax may need to be round up or round down depending upon settings, whereas wc_price alone will always round it down.
							echo wc_price( wc_round_tax_total( $tax_total->amount ), array( 'currency' => $order->get_currency() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
						?>
					</td>
				</tr>
			<?php endforeach; ?>
		<?php endif; ?>

		<?php do_action( 'woocommerce_admin_order_totals_after_tax', $order->get_id() ); ?>

		<tr>
			<td class="label"><?php esc_html_e( 'Order Total', 'woocommerce' ); ?>:</td>
			<td width="1%"></td>
			<td class="total">
				<?php echo wc_price( $order->get_total(), array( 'currency' => $order->get_currency() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
			</td>
		</tr>

	</table>

	<div class="clear"></div>

	<?php if ( in_array( $order->get_status(), array( 'processing', 'completed', 'refunded' ), true ) && ! empty( $order->get_date_paid() ) ) : ?>

		<table class="wc-order-totals" style="border-top: 1px solid #999; margin-top:12px; padding-top:12px">
			<tr>
				<td class="<?php echo $order->get_total_refunded() ? 'label' : 'label label-highlight'; ?>"><?php esc_html_e( 'Paid', 'woocommerce' ); ?>: <br /></td>
				<td width="1%"></td>
				<td class="total">
					<?php echo wc_price( $order->get_total(), array( 'currency' => $order->get_currency() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
				</td>
			</tr>
			<tr>
				<td>
					<span class="description">
					<?php
					if ( $order->get_payment_method_title() ) {
						/* translators: 1: payment date. 2: payment method */
						echo esc_html( sprintf( __( '%1$s via %2$s', 'woocommerce' ), $order->get_date_paid()->date_i18n( get_option( 'date_format' ) ), $order->get_payment_method_title() ) );
					} else {
						echo esc_html( $order->get_date_paid()->date_i18n( get_option( 'date_format' ) ) );
					}
					?>
					</span>
				</td>
				<td colspan="2"></td>
			</tr>
		</table>

	<div class="clear"></div>

	<?php endif; ?>

	<?php if ( $order->get_total_refunded() ) : ?>
		<table class="wc-order-totals" style="border-top: 1px solid #999; margin-top:12px; padding-top:12px">
			<tr>
				<td class="label refunded-total"><?php esc_html_e( 'Refunded', 'woocommerce' ); ?>:</td>
				<td width="1%"></td>
				<td class="total refunded-total">-<?php echo wc_price( $order->get_total_refunded(), array( 'currency' => $order->get_currency() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></td>
			</tr>

			<?php do_action( 'woocommerce_admin_order_totals_after_refunded', $order->get_id() ); ?>

			<tr>
				<td class="label label-highlight"><?php esc_html_e( 'Net Payment', 'woocommerce' ); ?>:</td>
				<td width="1%"></td>
				<td class="total">
				<?php echo wc_price( $order->get_total() - $order->get_total_refunded(), array( 'currency' => $order->get_currency() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
				</td>
			</tr>

		</table>
	<?php endif; ?>

	<div class="clear"></div>

	<table class="wc-order-totals">
		<?php do_action( 'woocommerce_admin_order_totals_after_total', $order->get_id() ); ?>
	</table>

	<div class="clear"></div>
</div>
<div class="wc-order-data-row wc-order-bulk-actions wc-order-data-row-toggle">
	<p class="add-items">
		<?php if ( $order->is_editable() ) : ?>
			<button type="button" class="button add-line-item"><?php esc_html_e( 'Add item(s)', 'woocommerce' ); ?></button>
			<?php if ( wc_coupons_enabled() ) : ?>
				<button type="button" class="button add-coupon"><?php esc_html_e( 'Apply coupon', 'woocommerce' ); ?></button>
			<?php endif; ?>
		<?php else : ?>
			<span class="description"><?php echo wc_help_tip( __( 'To edit this order change the status back to "Pending payment"', 'woocommerce' ) ); ?> <?php esc_html_e( 'This order is no longer editable.', 'woocommerce' ); ?></span>
		<?php endif; ?>
		<?php if ( 0 < $order->get_total() - $order->get_total_refunded() || 0 < absint( $order->get_item_count() - $order->get_item_count_refunded() ) ) : ?>
			<button type="button" class="button refund-items"><?php esc_html_e( 'Refund', 'woocommerce' ); ?></button>
		<?php endif; ?>
		<?php
			// Allow adding custom buttons.
			do_action( 'woocommerce_order_item_add_action_buttons', $order );
		?>
		<?php if ( $order->is_editable() ) : ?>
			<button type="button" class="button button-primary calculate-action"><?php esc_html_e( 'Recalculate', 'woocommerce' ); ?></button>
		<?php endif; ?>
	</p>
</div>
<div class="wc-order-data-row wc-order-add-item wc-order-data-row-toggle" style="display:none;">
	<button type="button" class="button add-order-item"><?php esc_html_e( 'Add product(s)', 'woocommerce' ); ?></button>
	<button type="button" class="button add-order-fee"><?php esc_html_e( 'Add fee', 'woocommerce' ); ?></button>
	<button type="button" class="button add-order-shipping"><?php esc_html_e( 'Add shipping', 'woocommerce' ); ?></button>
	<?php if ( wc_tax_enabled() ) : ?>
		<button type="button" class="button add-order-tax"><?php esc_html_e( 'Add tax', 'woocommerce' ); ?></button>
	<?php endif; ?>
	<?php
		// Allow adding custom buttons.
		do_action( 'woocommerce_order_item_add_line_buttons', $order );
	?>
	<button type="button" class="button cancel-action"><?php esc_html_e( 'Cancel', 'woocommerce' ); ?></button>
	<button type="button" class="button button-primary save-action"><?php esc_html_e( 'Save', 'woocommerce' ); ?></button>
</div>
<?php if ( 0 < $order->get_total() - $order->get_total_refunded() || 0 < absint( $order->get_item_count() - $order->get_item_count_refunded() ) ) : ?>
<div class="wc-order-data-row wc-order-refund-items wc-order-data-row-toggle" style="display: none;">
	<table class="wc-order-totals">
		<?php if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) : ?>
			<tr>
				<td class="label"><label for="restock_refunded_items"><?php esc_html_e( 'Restock refunded items', 'woocommerce' ); ?>:</label></td>
				<td class="total"><input type="checkbox" id="restock_refunded_items" name="restock_refunded_items" <?php checked( apply_filters( 'woocommerce_restock_refunded_items', true ) ); ?> /></td>
			</tr>
		<?php endif; ?>
		<tr>
			<td class="label"><?php esc_html_e( 'Amount already refunded', 'woocommerce' ); ?>:</td>
			<td class="total">-<?php echo wc_price( $order->get_total_refunded(), array( 'currency' => $order->get_currency() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></td>
		</tr>
		<tr>
			<td class="label"><?php esc_html_e( 'Total available to refund', 'woocommerce' ); ?>:</td>
			<td class="total"><?php echo wc_price( $order->get_total() - $order->get_total_refunded(), array( 'currency' => $order->get_currency() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></td>
		</tr>
		<tr>
			<td class="label">
				<label for="refund_amount">
					<?php echo wc_help_tip( __( 'Refund the line items above. This will show the total amount to be refunded', 'woocommerce' ) ); ?>
					<?php esc_html_e( 'Refund amount', 'woocommerce' ); ?>:
				</label>
			</td>
			<td class="total">
				<input type="text" id="refund_amount" name="refund_amount" class="wc_input_price"
				<?php
				if ( wc_tax_enabled() ) {
					// If taxes are enabled, using this refund amount can cause issues due to taxes not being refunded also.
					// The refunds should be added to the line items, not the order as a whole.
					echo 'readonly';
				}
				?>
				/>
				<div class="clear"></div>
			</td>
		</tr>
		<tr>
			<td class="label">
				<label for="refund_reason">
					<?php echo wc_help_tip( __( 'Note: the refund reason will be visible by the customer.', 'woocommerce' ) ); ?>
					<?php esc_html_e( 'Reason for refund (optional):', 'woocommerce' ); ?>
				</label>
			</td>
			<td class="total">
				<input type="text" id="refund_reason" name="refund_reason" />
				<div class="clear"></div>
			</td>
		</tr>
	</table>
	<div class="clear"></div>
	<div class="refund-actions">
		<?php
		$refund_amount = '<span class="wc-order-refund-amount">' . wc_price( 0, array( 'currency' => $order->get_currency() ) ) . '</span>';
		$gateway_name  = false !== $payment_gateway ? ( ! empty( $payment_gateway->method_title ) ? $payment_gateway->method_title : $payment_gateway->get_title() ) : __( 'Payment gateway', 'woocommerce' );

		if ( false !== $payment_gateway && $payment_gateway->can_refund_order( $order ) ) {
			/* translators: refund amount, gateway name */
			echo '<button type="button" class="button button-primary do-api-refund">' . sprintf( esc_html__( 'Refund %1$s via %2$s', 'woocommerce' ), wp_kses_post( $refund_amount ), esc_html( $gateway_name ) ) . '</button>';
		}
		?>
		<?php /* translators: refund amount  */ ?>
		<button type="button" class="button button-primary do-manual-refund tips" data-tip="<?php esc_attr_e( 'You will need to manually issue a refund through your payment gateway after using this.', 'woocommerce' ); ?>"><?php printf( esc_html__( 'Refund %s manually', 'woocommerce' ), wp_kses_post( $refund_amount ) ); ?></button>
		<button type="button" class="button cancel-action"><?php esc_html_e( 'Cancel', 'woocommerce' ); ?></button>
		<input type="hidden" id="refunded_amount" name="refunded_amount" value="<?php echo esc_attr( $order->get_total_refunded() ); ?>" />
		<div class="clear"></div>
	</div>
</div>
<?php endif; ?>

<script type="text/template" id="tmpl-wc-modal-add-products">
	<div class="wc-backbone-modal">
		<div class="wc-backbone-modal-content">
			<section class="wc-backbone-modal-main" role="main">
				<header class="wc-backbone-modal-header">
					<h1><?php esc_html_e( 'Add products', 'woocommerce' ); ?></h1>
					<button class="modal-close modal-close-link dashicons dashicons-no-alt">
						<span class="screen-reader-text">Close modal panel</span>
					</button>
				</header>
				<article>
					<form action="" method="post">
						<table class="widefat">
							<thead>
								<tr>
									<th><?php esc_html_e( 'Product', 'woocommerce' ); ?></th>
									<th><?php esc_html_e( 'Quantity', 'woocommerce' ); ?></th>
								</tr>
							</thead>
							<?php
								$row = '
									<td><select class="wc-product-search" name="item_id" data-allow_clear="true" data-display_stock="true" data-exclude_type="variable" data-placeholder="' . esc_attr__( 'Search for a product&hellip;', 'woocommerce' ) . '"></select></td>
									<td><input type="number" step="1" min="0" max="9999" autocomplete="off" name="item_qty" placeholder="1" size="4" class="quantity" /></td>';
							?>
							<tbody data-row="<?php echo esc_attr( $row ); ?>">
								<tr>
									<?php echo $row; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
								</tr>
							</tbody>
						</table>
					</form>
				</article>
				<footer>
					<div class="inner">
						<button id="btn-ok" class="button button-primary button-large"><?php esc_html_e( 'Add', 'woocommerce' ); ?></button>
					</div>
				</footer>
			</section>
		</div>
	</div>
	<div class="wc-backbone-modal-backdrop modal-close"></div>
</script>

<script type="text/template" id="tmpl-wc-modal-add-tax">
	<div class="wc-backbone-modal">
		<div class="wc-backbone-modal-content">
			<section class="wc-backbone-modal-main" role="main">
				<header class="wc-backbone-modal-header">
					<h1><?php esc_html_e( 'Add tax', 'woocommerce' ); ?></h1>
					<button class="modal-close modal-close-link dashicons dashicons-no-alt">
						<span class="screen-reader-text">Close modal panel</span>
					</button>
				</header>
				<article>
					<form action="" method="post">
						<table class="widefat">
							<thead>
								<tr>
									<th>&nbsp;</th>
									<th><?php esc_html_e( 'Rate name', 'woocommerce' ); ?></th>
									<th><?php esc_html_e( 'Tax class', 'woocommerce' ); ?></th>
									<th><?php esc_html_e( 'Rate code', 'woocommerce' ); ?></th>
									<th><?php esc_html_e( 'Rate %', 'woocommerce' ); ?></th>
								</tr>
							</thead>
						<?php
						$rates = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}woocommerce_tax_rates ORDER BY tax_rate_name LIMIT 100" );
						foreach ( $rates as $rate ) {
							echo '
									<tr>
										<td><input type="radio" id="add_order_tax_' . absint( $rate->tax_rate_id ) . '" name="add_order_tax" value="' . absint( $rate->tax_rate_id ) . '" /></td>
										<td><label for="add_order_tax_' . absint( $rate->tax_rate_id ) . '">' . esc_html( WC_Tax::get_rate_label( $rate ) ) . '</label></td>
										<td>' . ( isset( $classes_options[ $rate->tax_rate_class ] ) ? esc_html( $classes_options[ $rate->tax_rate_class ] ) : '-' ) . '</td>
										<td>' . esc_html( WC_Tax::get_rate_code( $rate ) ) . '</td>
										<td>' . esc_html( WC_Tax::get_rate_percent( $rate ) ) . '</td>
									</tr>
								';
						}
						?>
						</table>
						<?php if ( absint( $wpdb->get_var( "SELECT COUNT(tax_rate_id) FROM {$wpdb->prefix}woocommerce_tax_rates;" ) ) > 100 ) : ?>
							<p>
								<label for="manual_tax_rate_id"><?php esc_html_e( 'Or, enter tax rate ID:', 'woocommerce' ); ?></label><br/>
								<input type="number" name="manual_tax_rate_id" id="manual_tax_rate_id" step="1" placeholder="<?php esc_attr_e( 'Optional', 'woocommerce' ); ?>" />
							</p>
						<?php endif; ?>
					</form>
				</article>
				<footer>
					<div class="inner">
						<button id="btn-ok" class="button button-primary button-large"><?php esc_html_e( 'Add', 'woocommerce' ); ?></button>
					</div>
				</footer>
			</section>
		</div>
	</div>
	<div class="wc-backbone-modal-backdrop modal-close"></div>
</script>
admin/meta-boxes/views/html-product-data-advanced.php000064400000003053151336044770016655 0ustar00<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>
<div id="advanced_product_data" class="panel woocommerce_options_panel hidden">

	<div class="options_group hide_if_external hide_if_grouped">
		<?php
		woocommerce_wp_textarea_input(
			array(
				'id'          => '_purchase_note',
				'value'       => $product_object->get_purchase_note( 'edit' ),
				'label'       => __( 'Purchase note', 'woocommerce' ),
				'desc_tip'    => true,
				'description' => __( 'Enter an optional note to send the customer after purchase.', 'woocommerce' ),
			)
		);
		?>
	</div>

	<div class="options_group">
		<?php
		woocommerce_wp_text_input(
			array(
				'id'                => 'menu_order',
				'value'             => $product_object->get_menu_order( 'edit' ),
				'label'             => __( 'Menu order', 'woocommerce' ),
				'desc_tip'          => true,
				'description'       => __( 'Custom ordering position.', 'woocommerce' ),
				'type'              => 'number',
				'custom_attributes' => array(
					'step' => '1',
				),
			)
		);
		?>
	</div>

	<?php if ( post_type_supports( 'product', 'comments' ) ) : ?>
		<div class="options_group reviews">
			<?php
				woocommerce_wp_checkbox(
					array(
						'id'      => 'comment_status',
						'value'   => $product_object->get_reviews_allowed( 'edit' ) ? 'open' : 'closed',
						'label'   => __( 'Enable reviews', 'woocommerce' ),
						'cbvalue' => 'open',
					)
				);
				do_action( 'woocommerce_product_options_reviews' );
			?>
		</div>
	<?php endif; ?>

	<?php do_action( 'woocommerce_product_options_advanced' ); ?>
</div>
admin/meta-boxes/views/html-product-data-shipping.php000064400000005506151336044770016736 0ustar00<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>
<div id="shipping_product_data" class="panel woocommerce_options_panel hidden">
	<div class="options_group">
		<?php
		if ( wc_product_weight_enabled() ) {
			woocommerce_wp_text_input(
				array(
					'id'          => '_weight',
					'value'       => $product_object->get_weight( 'edit' ),
					'label'       => __( 'Weight', 'woocommerce' ) . ' (' . get_option( 'woocommerce_weight_unit' ) . ')',
					'placeholder' => wc_format_localized_decimal( 0 ),
					'desc_tip'    => true,
					'description' => __( 'Weight in decimal form', 'woocommerce' ),
					'type'        => 'text',
					'data_type'   => 'decimal',
				)
			);
		}

		if ( wc_product_dimensions_enabled() ) {
			?>
			<p class="form-field dimensions_field">
				<?php /* translators: WooCommerce dimension unit*/ ?>
				<label for="product_length"><?php printf( __( 'Dimensions (%s)', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ); ?></label>
				<span class="wrap">
					<input id="product_length" placeholder="<?php esc_attr_e( 'Length', 'woocommerce' ); ?>" class="input-text wc_input_decimal" size="6" type="text" name="_length" value="<?php echo esc_attr( wc_format_localized_decimal( $product_object->get_length( 'edit' ) ) ); ?>" />
					<input id="product_width" placeholder="<?php esc_attr_e( 'Width', 'woocommerce' ); ?>" class="input-text wc_input_decimal" size="6" type="text" name="_width" value="<?php echo esc_attr( wc_format_localized_decimal( $product_object->get_width( 'edit' ) ) ); ?>" />
					<input id="product_height" placeholder="<?php esc_attr_e( 'Height', 'woocommerce' ); ?>" class="input-text wc_input_decimal last" size="6" type="text" name="_height" value="<?php echo esc_attr( wc_format_localized_decimal( $product_object->get_height( 'edit' ) ) ); ?>" />
				</span>
				<?php echo wc_help_tip( __( 'LxWxH in decimal form', 'woocommerce' ) ); ?>
			</p>
			<?php
		}

		do_action( 'woocommerce_product_options_dimensions' );
		?>
	</div>

	<div class="options_group">
		<?php
		$args = array(
			'taxonomy'         => 'product_shipping_class',
			'hide_empty'       => 0,
			'show_option_none' => __( 'No shipping class', 'woocommerce' ),
			'name'             => 'product_shipping_class',
			'id'               => 'product_shipping_class',
			'selected'         => $product_object->get_shipping_class_id( 'edit' ),
			'class'            => 'select short',
			'orderby'          => 'name',
		);
		?>
		<p class="form-field shipping_class_field">
			<label for="product_shipping_class"><?php esc_html_e( 'Shipping class', 'woocommerce' ); ?></label>
			<?php wp_dropdown_categories( $args ); ?>
			<?php echo wc_help_tip( __( 'Shipping classes are used by certain shipping methods to group similar products.', 'woocommerce' ) ); ?>
		</p>
		<?php

		do_action( 'woocommerce_product_options_shipping' );
		?>
	</div>
</div>
admin/meta-boxes/views/html-product-data-linked-products.php000064400000006724151336044770020227 0ustar00<?php
/**
 * Linked product options.
 *
 * @package WooCommerce\Admin
 */

defined( 'ABSPATH' ) || exit;
?>
<div id="linked_product_data" class="panel woocommerce_options_panel hidden">

	<div class="options_group show_if_grouped">
		<p class="form-field">
			<label for="grouped_products"><?php esc_html_e( 'Grouped products', 'woocommerce' ); ?></label>
			<select class="wc-product-search" multiple="multiple" style="width: 50%;" id="grouped_products" name="grouped_products[]" data-sortable="true" data-placeholder="<?php esc_attr_e( 'Search for a product&hellip;', 'woocommerce' ); ?>" data-action="woocommerce_json_search_products" data-exclude="<?php echo intval( $post->ID ); ?>">
				<?php
				$product_ids = $product_object->is_type( 'grouped' ) ? $product_object->get_children( 'edit' ) : array();

				foreach ( $product_ids as $product_id ) {
					$product = wc_get_product( $product_id );
					if ( is_object( $product ) ) {
						echo '<option value="' . esc_attr( $product_id ) . '"' . selected( true, true, false ) . '>' . esc_html( wp_strip_all_tags( $product->get_formatted_name() ) ) . '</option>';
					}
				}
				?>
			</select> <?php echo wc_help_tip( __( 'This lets you choose which products are part of this group.', 'woocommerce' ) ); // WPCS: XSS ok. ?>
		</p>
	</div>

	<div class="options_group">
		<p class="form-field">
			<label for="upsell_ids"><?php esc_html_e( 'Upsells', 'woocommerce' ); ?></label>
			<select class="wc-product-search" multiple="multiple" style="width: 50%;" id="upsell_ids" name="upsell_ids[]" data-placeholder="<?php esc_attr_e( 'Search for a product&hellip;', 'woocommerce' ); ?>" data-action="woocommerce_json_search_products_and_variations" data-exclude="<?php echo intval( $post->ID ); ?>">
				<?php
				$product_ids = $product_object->get_upsell_ids( 'edit' );

				foreach ( $product_ids as $product_id ) {
					$product = wc_get_product( $product_id );
					if ( is_object( $product ) ) {
						echo '<option value="' . esc_attr( $product_id ) . '"' . selected( true, true, false ) . '>' . esc_html( wp_strip_all_tags( $product->get_formatted_name() ) ) . '</option>';
					}
				}
				?>
			</select> <?php echo wc_help_tip( __( 'Upsells are products which you recommend instead of the currently viewed product, for example, products that are more profitable or better quality or more expensive.', 'woocommerce' ) ); // WPCS: XSS ok. ?>
		</p>

		<p class="form-field hide_if_grouped hide_if_external">
			<label for="crosssell_ids"><?php esc_html_e( 'Cross-sells', 'woocommerce' ); ?></label>
			<select class="wc-product-search" multiple="multiple" style="width: 50%;" id="crosssell_ids" name="crosssell_ids[]" data-placeholder="<?php esc_attr_e( 'Search for a product&hellip;', 'woocommerce' ); ?>" data-action="woocommerce_json_search_products_and_variations" data-exclude="<?php echo intval( $post->ID ); ?>">
				<?php
				$product_ids = $product_object->get_cross_sell_ids( 'edit' );

				foreach ( $product_ids as $product_id ) {
					$product = wc_get_product( $product_id );
					if ( is_object( $product ) ) {
						echo '<option value="' . esc_attr( $product_id ) . '"' . selected( true, true, false ) . '>' . esc_html( wp_strip_all_tags( $product->get_formatted_name() ) ) . '</option>';
					}
				}
				?>
			</select> <?php echo wc_help_tip( __( 'Cross-sells are products which you promote in the cart, based on the current product.', 'woocommerce' ) ); // WPCS: XSS ok. ?>
		</p>
	</div>

	<?php do_action( 'woocommerce_product_options_related' ); ?>
</div>
admin/meta-boxes/views/html-order-item.php000064400000025513151336044770014577 0ustar00<?php
/**
 * Shows an order item
 *
 * @package WooCommerce\Admin
 * @var object $item The item being displayed
 * @var int $item_id The id of the item being displayed
 */

defined( 'ABSPATH' ) || exit;

$product      = $item->get_product();
$product_link = $product ? admin_url( 'post.php?post=' . $item->get_product_id() . '&action=edit' ) : '';
$thumbnail    = $product ? apply_filters( 'woocommerce_admin_order_item_thumbnail', $product->get_image( 'thumbnail', array( 'title' => '' ), false ), $item_id, $item ) : '';
$row_class    = apply_filters( 'woocommerce_admin_html_order_item_class', ! empty( $class ) ? $class : '', $item, $order );
?>
<tr class="item <?php echo esc_attr( $row_class ); ?>" data-order_item_id="<?php echo esc_attr( $item_id ); ?>">
	<td class="thumb">
		<?php echo '<div class="wc-order-item-thumbnail">' . wp_kses_post( $thumbnail ) . '</div>'; ?>
	</td>
	<td class="name" data-sort-value="<?php echo esc_attr( $item->get_name() ); ?>">
		<?php
		echo $product_link ? '<a href="' . esc_url( $product_link ) . '" class="wc-order-item-name">' . wp_kses_post( $item->get_name() ) . '</a>' : '<div class="wc-order-item-name">' . wp_kses_post( $item->get_name() ) . '</div>';

		if ( $product && $product->get_sku() ) {
			echo '<div class="wc-order-item-sku"><strong>' . esc_html__( 'SKU:', 'woocommerce' ) . '</strong> ' . esc_html( $product->get_sku() ) . '</div>';
		}

		if ( $item->get_variation_id() ) {
			echo '<div class="wc-order-item-variation"><strong>' . esc_html__( 'Variation ID:', 'woocommerce' ) . '</strong> ';
			if ( 'product_variation' === get_post_type( $item->get_variation_id() ) ) {
				echo esc_html( $item->get_variation_id() );
			} else {
				/* translators: %s: variation id */
				printf( esc_html__( '%s (No longer exists)', 'woocommerce' ), esc_html( $item->get_variation_id() ) );
			}
			echo '</div>';
		}
		?>
		<input type="hidden" class="order_item_id" name="order_item_id[]" value="<?php echo esc_attr( $item_id ); ?>" />
		<input type="hidden" name="order_item_tax_class[<?php echo absint( $item_id ); ?>]" value="<?php echo esc_attr( $item->get_tax_class() ); ?>" />

		<?php do_action( 'woocommerce_before_order_itemmeta', $item_id, $item, $product ); ?>
		<?php require __DIR__ . '/html-order-item-meta.php'; ?>
		<?php do_action( 'woocommerce_after_order_itemmeta', $item_id, $item, $product ); ?>
	</td>

	<?php do_action( 'woocommerce_admin_order_item_values', $product, $item, absint( $item_id ) ); ?>

	<td class="item_cost" width="1%" data-sort-value="<?php echo esc_attr( $order->get_item_subtotal( $item, false, true ) ); ?>">
		<div class="view">
			<?php
			echo wc_price( $order->get_item_subtotal( $item, false, true ), array( 'currency' => $order->get_currency() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			?>
		</div>
	</td>
	<td class="quantity" width="1%">
		<div class="view">
			<?php
			echo '<small class="times">&times;</small> ' . esc_html( $item->get_quantity() );

			$refunded_qty = $order->get_qty_refunded_for_item( $item_id );

			if ( $refunded_qty ) {
				echo '<small class="refunded">-' . esc_html( $refunded_qty * -1 ) . '</small>';
			}
			?>
		</div>
		<?php
			$step = apply_filters( 'woocommerce_quantity_input_step', '1', $product );

			/**
			* Filter to change the product quantity stepping in the order editor of the admin area.
			*
			* @since   5.8.0
			* @param   string      $step    The current step amount to be used in the quantity editor.
			* @param   WC_Product  $product The product that is being edited.
			* @param   string      $context The context in which the quantity editor is shown, 'edit' or 'refund'.
			*/
			$step_edit   = apply_filters( 'woocommerce_quantity_input_step_admin', $step, $product, 'edit' );
			$step_refund = apply_filters( 'woocommerce_quantity_input_step_admin', $step, $product, 'refund' );

			/**
			* Filter to change the product quantity minimum in the order editor of the admin area.
			*
			* @since   5.8.0
			* @param   string      $step    The current minimum amount to be used in the quantity editor.
			* @param   WC_Product  $product The product that is being edited.
			* @param   string      $context The context in which the quantity editor is shown, 'edit' or 'refund'.
			*/
			$min_edit   = apply_filters( 'woocommerce_quantity_input_min_admin', '0', $product, 'edit' );
			$min_refund = apply_filters( 'woocommerce_quantity_input_min_admin', '0', $product, 'refund' );
		?>
		<div class="edit" style="display: none;">
			<input type="number" step="<?php echo esc_attr( $step_edit ); ?>" min="<?php echo esc_attr( $min_edit ); ?>" autocomplete="off" name="order_item_qty[<?php echo absint( $item_id ); ?>]" placeholder="0" value="<?php echo esc_attr( $item->get_quantity() ); ?>" data-qty="<?php echo esc_attr( $item->get_quantity() ); ?>" size="4" class="quantity" />
		</div>
		<div class="refund" style="display: none;">
			<input type="number" step="<?php echo esc_attr( $step_refund ); ?>" min="<?php echo esc_attr( $min_refund ); ?>" max="<?php echo absint( $item->get_quantity() ); ?>" autocomplete="off" name="refund_order_item_qty[<?php echo absint( $item_id ); ?>]" placeholder="0" size="4" class="refund_order_item_qty" />
		</div>
	</td>
	<td class="line_cost" width="1%" data-sort-value="<?php echo esc_attr( $item->get_total() ); ?>">
		<div class="view">
			<?php
			echo wc_price( $item->get_total(), array( 'currency' => $order->get_currency() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped

			if ( $item->get_subtotal() !== $item->get_total() ) {
				/* translators: %s: discount amount */
				echo '<span class="wc-order-item-discount">' . sprintf( esc_html__( '%s discount', 'woocommerce' ), wc_price( wc_format_decimal( $item->get_subtotal() - $item->get_total(), '' ), array( 'currency' => $order->get_currency() ) ) ) . '</span>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			}

			$refunded = $order->get_total_refunded_for_item( $item_id );

			if ( $refunded ) {
				echo '<small class="refunded">-' . wc_price( $refunded, array( 'currency' => $order->get_currency() ) ) . '</small>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			}
			?>
		</div>
		<div class="edit" style="display: none;">
			<div class="split-input">
				<div class="input">
					<label><?php esc_attr_e( 'Before discount', 'woocommerce' ); ?></label>
					<input type="text" name="line_subtotal[<?php echo absint( $item_id ); ?>]" placeholder="<?php echo esc_attr( wc_format_localized_price( 0 ) ); ?>" value="<?php echo esc_attr( wc_format_localized_price( $item->get_subtotal() ) ); ?>" class="line_subtotal wc_input_price" data-subtotal="<?php echo esc_attr( wc_format_localized_price( $item->get_subtotal() ) ); ?>" />
				</div>
				<div class="input">
					<label><?php esc_attr_e( 'Total', 'woocommerce' ); ?></label>
					<input type="text" name="line_total[<?php echo absint( $item_id ); ?>]" placeholder="<?php echo esc_attr( wc_format_localized_price( 0 ) ); ?>" value="<?php echo esc_attr( wc_format_localized_price( $item->get_total() ) ); ?>" class="line_total wc_input_price" data-tip="<?php esc_attr_e( 'After pre-tax discounts.', 'woocommerce' ); ?>" data-total="<?php echo esc_attr( wc_format_localized_price( $item->get_total() ) ); ?>" />
				</div>
			</div>
		</div>
		<div class="refund" style="display: none;">
			<input type="text" name="refund_line_total[<?php echo absint( $item_id ); ?>]" placeholder="<?php echo esc_attr( wc_format_localized_price( 0 ) ); ?>" class="refund_line_total wc_input_price" />
		</div>
	</td>

	<?php
	$tax_data = wc_tax_enabled() ? $item->get_taxes() : false;

	if ( $tax_data ) {
		foreach ( $order_taxes as $tax_item ) {
			$tax_item_id       = $tax_item->get_rate_id();
			$tax_item_total    = isset( $tax_data['total'][ $tax_item_id ] ) ? $tax_data['total'][ $tax_item_id ] : '';
			$tax_item_subtotal = isset( $tax_data['subtotal'][ $tax_item_id ] ) ? $tax_data['subtotal'][ $tax_item_id ] : '';

			if ( '' !== $tax_item_subtotal ) {
				$round_at_subtotal = 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' );
				$tax_item_total    = wc_round_tax_total( $tax_item_total, $round_at_subtotal ? wc_get_rounding_precision() : null );
				$tax_item_subtotal = wc_round_tax_total( $tax_item_subtotal, $round_at_subtotal ? wc_get_rounding_precision() : null );
			}
			?>
			<td class="line_tax" width="1%">
				<div class="view">
					<?php
					if ( '' !== $tax_item_total ) {
						echo wc_price( wc_round_tax_total( $tax_item_total ), array( 'currency' => $order->get_currency() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
					} else {
						echo '&ndash;';
					}

					$refunded = $order->get_tax_refunded_for_item( $item_id, $tax_item_id );

					if ( $refunded ) {
						echo '<small class="refunded">-' . wc_price( $refunded, array( 'currency' => $order->get_currency() ) ) . '</small>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
					}
					?>
				</div>
				<div class="edit" style="display: none;">
					<div class="split-input">
						<div class="input">
							<label><?php esc_attr_e( 'Before discount', 'woocommerce' ); ?></label>
							<input type="text" name="line_subtotal_tax[<?php echo absint( $item_id ); ?>][<?php echo esc_attr( $tax_item_id ); ?>]" placeholder="<?php echo esc_attr( wc_format_localized_price( 0 ) ); ?>" value="<?php echo esc_attr( wc_format_localized_price( $tax_item_subtotal ) ); ?>" class="line_subtotal_tax wc_input_price" data-subtotal_tax="<?php echo esc_attr( wc_format_localized_price( $tax_item_subtotal ) ); ?>" data-tax_id="<?php echo esc_attr( $tax_item_id ); ?>" />
						</div>
						<div class="input">
							<label><?php esc_attr_e( 'Total', 'woocommerce' ); ?></label>
							<input type="text" name="line_tax[<?php echo absint( $item_id ); ?>][<?php echo esc_attr( $tax_item_id ); ?>]" placeholder="<?php echo esc_attr( wc_format_localized_price( 0 ) ); ?>" value="<?php echo esc_attr( wc_format_localized_price( $tax_item_total ) ); ?>" class="line_tax wc_input_price" data-total_tax="<?php echo esc_attr( wc_format_localized_price( $tax_item_total ) ); ?>" data-tax_id="<?php echo esc_attr( $tax_item_id ); ?>" />
						</div>
					</div>
				</div>
				<div class="refund" style="display: none;">
					<input type="text" name="refund_line_tax[<?php echo absint( $item_id ); ?>][<?php echo esc_attr( $tax_item_id ); ?>]" placeholder="<?php echo esc_attr( wc_format_localized_price( 0 ) ); ?>" class="refund_line_tax wc_input_price" data-tax_id="<?php echo esc_attr( $tax_item_id ); ?>" />
				</div>
			</td>
			<?php
		}
	}
	?>
	<td class="wc-order-edit-line-item" width="1%">
		<div class="wc-order-edit-line-item-actions">
			<?php if ( $order->is_editable() ) : ?>
				<a class="edit-order-item tips" href="#" data-tip="<?php esc_attr_e( 'Edit item', 'woocommerce' ); ?>"></a><a class="delete-order-item tips" href="#" data-tip="<?php esc_attr_e( 'Delete item', 'woocommerce' ); ?>"></a>
			<?php endif; ?>
		</div>
	</td>
</tr>
admin/meta-boxes/views/html-product-data-inventory.php000064400000011450151336044770017145 0ustar00<?php
/**
 * Displays the inventory tab in the product data meta box.
 *
 * @package WooCommerce\Admin
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>
<div id="inventory_product_data" class="panel woocommerce_options_panel hidden">

	<div class="options_group">
		<?php
		if ( wc_product_sku_enabled() ) {
			woocommerce_wp_text_input(
				array(
					'id'          => '_sku',
					'value'       => $product_object->get_sku( 'edit' ),
					'label'       => '<abbr title="' . esc_attr__( 'Stock Keeping Unit', 'woocommerce' ) . '">' . esc_html__( 'SKU', 'woocommerce' ) . '</abbr>',
					'desc_tip'    => true,
					'description' => __( 'SKU refers to a Stock-keeping unit, a unique identifier for each distinct product and service that can be purchased.', 'woocommerce' ),
				)
			);
		}

		do_action( 'woocommerce_product_options_sku' );

		if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {

			woocommerce_wp_checkbox(
				array(
					'id'            => '_manage_stock',
					'value'         => $product_object->get_manage_stock( 'edit' ) ? 'yes' : 'no',
					'wrapper_class' => 'show_if_simple show_if_variable',
					'label'         => __( 'Manage stock?', 'woocommerce' ),
					'description'   => __( 'Enable stock management at product level', 'woocommerce' ),
				)
			);

			do_action( 'woocommerce_product_options_stock' );

			echo '<div class="stock_fields show_if_simple show_if_variable">';

			woocommerce_wp_text_input(
				array(
					'id'                => '_stock',
					'value'             => wc_stock_amount( $product_object->get_stock_quantity( 'edit' ) ),
					'label'             => __( 'Stock quantity', 'woocommerce' ),
					'desc_tip'          => true,
					'description'       => __( 'Stock quantity. If this is a variable product this value will be used to control stock for all variations, unless you define stock at variation level.', 'woocommerce' ),
					'type'              => 'number',
					'custom_attributes' => array(
						'step' => 'any',
					),
					'data_type'         => 'stock',
				)
			);

			echo '<input type="hidden" name="_original_stock" value="' . esc_attr( wc_stock_amount( $product_object->get_stock_quantity( 'edit' ) ) ) . '" />';

			woocommerce_wp_select(
				array(
					'id'          => '_backorders',
					'value'       => $product_object->get_backorders( 'edit' ),
					'label'       => __( 'Allow backorders?', 'woocommerce' ),
					'options'     => wc_get_product_backorder_options(),
					'desc_tip'    => true,
					'description' => __( 'If managing stock, this controls whether or not backorders are allowed. If enabled, stock quantity can go below 0.', 'woocommerce' ),
				)
			);

			woocommerce_wp_text_input(
				array(
					'id'                => '_low_stock_amount',
					'value'             => $product_object->get_low_stock_amount( 'edit' ),
					'placeholder'       => sprintf(
						/* translators: %d: Amount of stock left */
						esc_attr__( 'Store-wide threshold (%d)', 'woocommerce' ),
						esc_attr( get_option( 'woocommerce_notify_low_stock_amount' ) )
					),
					'label'             => __( 'Low stock threshold', 'woocommerce' ),
					'desc_tip'          => true,
					'description'       => __( 'When product stock reaches this amount you will be notified by email. It is possible to define different values for each variation individually. The shop default value can be set in Settings > Products > Inventory.', 'woocommerce' ),
					'type'              => 'number',
					'custom_attributes' => array(
						'step' => 'any',
					),
				)
			);

			do_action( 'woocommerce_product_options_stock_fields' );

			echo '</div>';
		}

		woocommerce_wp_select(
			array(
				'id'            => '_stock_status',
				'value'         => $product_object->get_stock_status( 'edit' ),
				'wrapper_class' => 'stock_status_field hide_if_variable hide_if_external hide_if_grouped',
				'label'         => __( 'Stock status', 'woocommerce' ),
				'options'       => wc_get_product_stock_status_options(),
				'desc_tip'      => true,
				'description'   => __( 'Controls whether or not the product is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ),
			)
		);

		do_action( 'woocommerce_product_options_stock_status' );
		?>
	</div>

	<div class="options_group show_if_simple show_if_variable">
		<?php
		woocommerce_wp_checkbox(
			array(
				'id'            => '_sold_individually',
				'value'         => $product_object->get_sold_individually( 'edit' ) ? 'yes' : 'no',
				'wrapper_class' => 'show_if_simple show_if_variable',
				'label'         => __( 'Sold individually', 'woocommerce' ),
				'description'   => __( 'Enable this to only allow one of this item to be bought in a single order', 'woocommerce' ),
			)
		);

		do_action( 'woocommerce_product_options_sold_individually' );
		?>
	</div>

	<?php do_action( 'woocommerce_product_options_inventory_product_data' ); ?>
</div>
admin/meta-boxes/views/html-product-variation-download.php000064400000002125151336044770020001 0ustar00<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>
<tr>
	<td class="file_name">
		<input type="text" class="input_text" placeholder="<?php esc_attr_e( 'File name', 'woocommerce' ); ?>" name="_wc_variation_file_names[<?php echo esc_attr( $variation_id ); ?>][]" value="<?php echo esc_attr( $file['name'] ); ?>" />
		<input type="hidden" name="_wc_variation_file_hashes[<?php echo esc_attr( $variation_id ); ?>][]" value="<?php echo esc_attr( $key ); ?>" />
	</td>
	<td class="file_url"><input type="text" class="input_text" placeholder="<?php esc_attr_e( 'http://', 'woocommerce' ); ?>" name="_wc_variation_file_urls[<?php echo esc_attr( $variation_id ); ?>][]" value="<?php echo esc_attr( $file['file'] ); ?>" /></td>
	<td class="file_url_choose" width="1%"><a href="#" class="button upload_file_button" data-choose="<?php esc_attr_e( 'Choose file', 'woocommerce' ); ?>" data-update="<?php esc_attr_e( 'Insert file URL', 'woocommerce' ); ?>"><?php esc_html_e( 'Choose file', 'woocommerce' ); ?></a></td>
	<td width="1%"><a href="#" class="delete"><?php esc_html_e( 'Delete', 'woocommerce' ); ?></a></td>
</tr>
admin/meta-boxes/views/html-product-data-general.php000064400000017205151336044770016531 0ustar00<?php
/**
 * Product general data panel.
 *
 * @package WooCommerce\Admin
 */

defined( 'ABSPATH' ) || exit;

?>
<div id="general_product_data" class="panel woocommerce_options_panel">

	<div class="options_group show_if_external">
		<?php
		woocommerce_wp_text_input(
			array(
				'id'          => '_product_url',
				'value'       => is_callable( array( $product_object, 'get_product_url' ) ) ? $product_object->get_product_url( 'edit' ) : '',
				'label'       => __( 'Product URL', 'woocommerce' ),
				'placeholder' => 'https://',
				'description' => __( 'Enter the external URL to the product.', 'woocommerce' ),
			)
		);

		woocommerce_wp_text_input(
			array(
				'id'          => '_button_text',
				'value'       => is_callable( array( $product_object, 'get_button_text' ) ) ? $product_object->get_button_text( 'edit' ) : '',
				'label'       => __( 'Button text', 'woocommerce' ),
				'placeholder' => _x( 'Buy product', 'placeholder', 'woocommerce' ),
				'description' => __( 'This text will be shown on the button linking to the external product.', 'woocommerce' ),
			)
		);

		do_action( 'woocommerce_product_options_external' );
		?>
	</div>

	<div class="options_group pricing show_if_simple show_if_external hidden">
		<?php
		woocommerce_wp_text_input(
			array(
				'id'        => '_regular_price',
				'value'     => $product_object->get_regular_price( 'edit' ),
				'label'     => __( 'Regular price', 'woocommerce' ) . ' (' . get_woocommerce_currency_symbol() . ')',
				'data_type' => 'price',
			)
		);

		woocommerce_wp_text_input(
			array(
				'id'          => '_sale_price',
				'value'       => $product_object->get_sale_price( 'edit' ),
				'data_type'   => 'price',
				'label'       => __( 'Sale price', 'woocommerce' ) . ' (' . get_woocommerce_currency_symbol() . ')',
				'description' => '<a href="#" class="sale_schedule">' . __( 'Schedule', 'woocommerce' ) . '</a>',
			)
		);

		$sale_price_dates_from_timestamp = $product_object->get_date_on_sale_from( 'edit' ) ? $product_object->get_date_on_sale_from( 'edit' )->getOffsetTimestamp() : false;
		$sale_price_dates_to_timestamp   = $product_object->get_date_on_sale_to( 'edit' ) ? $product_object->get_date_on_sale_to( 'edit' )->getOffsetTimestamp() : false;

		$sale_price_dates_from = $sale_price_dates_from_timestamp ? date_i18n( 'Y-m-d', $sale_price_dates_from_timestamp ) : '';
		$sale_price_dates_to   = $sale_price_dates_to_timestamp ? date_i18n( 'Y-m-d', $sale_price_dates_to_timestamp ) : '';

		echo '<p class="form-field sale_price_dates_fields">
				<label for="_sale_price_dates_from">' . esc_html__( 'Sale price dates', 'woocommerce' ) . '</label>
				<input type="text" class="short" name="_sale_price_dates_from" id="_sale_price_dates_from" value="' . esc_attr( $sale_price_dates_from ) . '" placeholder="' . esc_html( _x( 'From&hellip;', 'placeholder', 'woocommerce' ) ) . ' YYYY-MM-DD" maxlength="10" pattern="' . esc_attr( apply_filters( 'woocommerce_date_input_html_pattern', '[0-9]{4}-(0[1-9]|1[012])-(0[1-9]|1[0-9]|2[0-9]|3[01])' ) ) . '" />
				<input type="text" class="short" name="_sale_price_dates_to" id="_sale_price_dates_to" value="' . esc_attr( $sale_price_dates_to ) . '" placeholder="' . esc_html( _x( 'To&hellip;', 'placeholder', 'woocommerce' ) ) . '  YYYY-MM-DD" maxlength="10" pattern="' . esc_attr( apply_filters( 'woocommerce_date_input_html_pattern', '[0-9]{4}-(0[1-9]|1[012])-(0[1-9]|1[0-9]|2[0-9]|3[01])' ) ) . '" />
				<a href="#" class="description cancel_sale_schedule">' . esc_html__( 'Cancel', 'woocommerce' ) . '</a>' . wc_help_tip( __( 'The sale will start at 00:00:00 of "From" date and end at 23:59:59 of "To" date.', 'woocommerce' ) ) . '
			</p>';

		do_action( 'woocommerce_product_options_pricing' );
		?>
	</div>

	<div class="options_group show_if_downloadable hidden">
		<div class="form-field downloadable_files">
			<label><?php esc_html_e( 'Downloadable files', 'woocommerce' ); ?></label>
			<table class="widefat">
				<thead>
					<tr>
						<th class="sort">&nbsp;</th>
						<th><?php esc_html_e( 'Name', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'This is the name of the download shown to the customer.', 'woocommerce' ) ); ?></th>
						<th colspan="2"><?php esc_html_e( 'File URL', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'This is the URL or absolute path to the file which customers will get access to. URLs entered here should already be encoded.', 'woocommerce' ) ); ?></th>
						<th>&nbsp;</th>
					</tr>
				</thead>
				<tbody>
					<?php
					$downloadable_files = $product_object->get_downloads( 'edit' );
					if ( $downloadable_files ) {
						foreach ( $downloadable_files as $key => $file ) {
							include __DIR__ . '/html-product-download.php';
						}
					}
					?>
				</tbody>
				<tfoot>
					<tr>
						<th colspan="5">
							<a href="#" class="button insert" data-row="
							<?php
								$key  = '';
								$file = array(
									'file' => '',
									'name' => '',
								);
								ob_start();
								require __DIR__ . '/html-product-download.php';
								echo esc_attr( ob_get_clean() );
								?>
							"><?php esc_html_e( 'Add File', 'woocommerce' ); ?></a>
						</th>
					</tr>
				</tfoot>
			</table>
		</div>
		<?php
		woocommerce_wp_text_input(
			array(
				'id'                => '_download_limit',
				'value'             => -1 === $product_object->get_download_limit( 'edit' ) ? '' : $product_object->get_download_limit( 'edit' ),
				'label'             => __( 'Download limit', 'woocommerce' ),
				'placeholder'       => __( 'Unlimited', 'woocommerce' ),
				'description'       => __( 'Leave blank for unlimited re-downloads.', 'woocommerce' ),
				'type'              => 'number',
				'custom_attributes' => array(
					'step' => '1',
					'min'  => '0',
				),
			)
		);

		woocommerce_wp_text_input(
			array(
				'id'                => '_download_expiry',
				'value'             => -1 === $product_object->get_download_expiry( 'edit' ) ? '' : $product_object->get_download_expiry( 'edit' ),
				'label'             => __( 'Download expiry', 'woocommerce' ),
				'placeholder'       => __( 'Never', 'woocommerce' ),
				'description'       => __( 'Enter the number of days before a download link expires, or leave blank.', 'woocommerce' ),
				'type'              => 'number',
				'custom_attributes' => array(
					'step' => '1',
					'min'  => '0',
				),
			)
		);

		do_action( 'woocommerce_product_options_downloads' );
		?>
	</div>

	<?php if ( wc_tax_enabled() ) : ?>
		<div class="options_group show_if_simple show_if_external show_if_variable">
			<?php
			woocommerce_wp_select(
				array(
					'id'          => '_tax_status',
					'value'       => $product_object->get_tax_status( 'edit' ),
					'label'       => __( 'Tax status', 'woocommerce' ),
					'options'     => array(
						'taxable'  => __( 'Taxable', 'woocommerce' ),
						'shipping' => __( 'Shipping only', 'woocommerce' ),
						'none'     => _x( 'None', 'Tax status', 'woocommerce' ),
					),
					'desc_tip'    => 'true',
					'description' => __( 'Define whether or not the entire product is taxable, or just the cost of shipping it.', 'woocommerce' ),
				)
			);

			woocommerce_wp_select(
				array(
					'id'          => '_tax_class',
					'value'       => $product_object->get_tax_class( 'edit' ),
					'label'       => __( 'Tax class', 'woocommerce' ),
					'options'     => wc_get_product_tax_class_options(),
					'desc_tip'    => 'true',
					'description' => __( 'Choose a tax class for this product. Tax classes are used to apply different tax rates specific to certain types of product.', 'woocommerce' ),
				)
			);

			do_action( 'woocommerce_product_options_tax' );
			?>
		</div>
	<?php endif; ?>

	<?php do_action( 'woocommerce_product_options_general_product_data' ); ?>
</div>
admin/meta-boxes/views/html-product-download.php000064400000001726151336044770016015 0ustar00<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
?>
<tr>
	<td class="sort"></td>
	<td class="file_name">
		<input type="text" class="input_text" placeholder="<?php esc_attr_e( 'File name', 'woocommerce' ); ?>" name="_wc_file_names[]" value="<?php echo esc_attr( $file['name'] ); ?>" />
		<input type="hidden" name="_wc_file_hashes[]" value="<?php echo esc_attr( $key ); ?>" />
	</td>
	<td class="file_url"><input type="text" class="input_text" placeholder="<?php esc_attr_e( 'http://', 'woocommerce' ); ?>" name="_wc_file_urls[]" value="<?php echo esc_attr( $file['file'] ); ?>" /></td>
	<td class="file_url_choose" width="1%"><a href="#" class="button upload_file_button" data-choose="<?php esc_attr_e( 'Choose file', 'woocommerce' ); ?>" data-update="<?php esc_attr_e( 'Insert file URL', 'woocommerce' ); ?>"><?php echo esc_html__( 'Choose file', 'woocommerce' ); ?></a></td>
	<td width="1%"><a href="#" class="delete"><?php esc_html_e( 'Delete', 'woocommerce' ); ?></a></td>
</tr>
admin/meta-boxes/views/html-order-item-meta.php000064400000004134151336044770015517 0ustar00<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

$hidden_order_itemmeta = apply_filters(
	'woocommerce_hidden_order_itemmeta',
	array(
		'_qty',
		'_tax_class',
		'_product_id',
		'_variation_id',
		'_line_subtotal',
		'_line_subtotal_tax',
		'_line_total',
		'_line_tax',
		'method_id',
		'cost',
		'_reduced_stock',
		'_restock_refunded_items',
	)
);
?><div class="view">
	<?php if ( $meta_data = $item->get_formatted_meta_data( '' ) ) : ?>
		<table cellspacing="0" class="display_meta">
			<?php
			foreach ( $meta_data as $meta_id => $meta ) :
				if ( in_array( $meta->key, $hidden_order_itemmeta, true ) ) {
					continue;
				}
				?>
				<tr>
					<th><?php echo wp_kses_post( $meta->display_key ); ?>:</th>
					<td><?php echo wp_kses_post( force_balance_tags( $meta->display_value ) ); ?></td>
				</tr>
			<?php endforeach; ?>
		</table>
	<?php endif; ?>
</div>
<div class="edit" style="display: none;">
	<table class="meta" cellspacing="0">
		<tbody class="meta_items">
			<?php if ( $meta_data = $item->get_formatted_meta_data( '' ) ) : ?>
				<?php
				foreach ( $meta_data as $meta_id => $meta ) :
					if ( in_array( $meta->key, $hidden_order_itemmeta, true ) ) {
						continue;
					}
					?>
					<tr data-meta_id="<?php echo esc_attr( $meta_id ); ?>">
						<td>
							<input type="text" maxlength="255" placeholder="<?php esc_attr_e( 'Name (required)', 'woocommerce' ); ?>" name="meta_key[<?php echo esc_attr( $item_id ); ?>][<?php echo esc_attr( $meta_id ); ?>]" value="<?php echo esc_attr( $meta->key ); ?>" />
							<textarea placeholder="<?php esc_attr_e( 'Value (required)', 'woocommerce' ); ?>" name="meta_value[<?php echo esc_attr( $item_id ); ?>][<?php echo esc_attr( $meta_id ); ?>]"><?php echo esc_textarea( rawurldecode( $meta->value ) ); ?></textarea>
						</td>
						<td width="1%"><button class="remove_order_item_meta button">&times;</button></td>
					</tr>
				<?php endforeach; ?>
			<?php endif; ?>
		</tbody>
		<tfoot>
			<tr>
				<td colspan="4"><button class="add_order_item_meta button"><?php esc_html_e( 'Add&nbsp;meta', 'woocommerce' ); ?></button></td>
			</tr>
		</tfoot>
	</table>
</div>
admin/meta-boxes/class-wc-meta-box-product-data.php000064400000061734151336044770016251 0ustar00<?php
/**
 * Product Data
 *
 * Displays the product data box, tabbed, with several panels covering price, stock etc.
 *
 * @package  WooCommerce\Admin\Meta Boxes
 * @version  3.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC_Meta_Box_Product_Data Class.
 */
class WC_Meta_Box_Product_Data {

	/**
	 * Output the metabox.
	 *
	 * @param WP_Post $post Post object.
	 */
	public static function output( $post ) {
		global $thepostid, $product_object;

		$thepostid      = $post->ID;
		$product_object = $thepostid ? wc_get_product( $thepostid ) : new WC_Product();

		wp_nonce_field( 'woocommerce_save_data', 'woocommerce_meta_nonce' );

		include __DIR__ . '/views/html-product-data-panel.php';
	}

	/**
	 * Show tab content/settings.
	 */
	private static function output_tabs() {
		global $post, $thepostid, $product_object;

		include __DIR__ . '/views/html-product-data-general.php';
		include __DIR__ . '/views/html-product-data-inventory.php';
		include __DIR__ . '/views/html-product-data-shipping.php';
		include __DIR__ . '/views/html-product-data-linked-products.php';
		include __DIR__ . '/views/html-product-data-attributes.php';
		include __DIR__ . '/views/html-product-data-advanced.php';
	}

	/**
	 * Return array of product type options.
	 *
	 * @return array
	 */
	private static function get_product_type_options() {
		return apply_filters(
			'product_type_options',
			array(
				'virtual'      => array(
					'id'            => '_virtual',
					'wrapper_class' => 'show_if_simple',
					'label'         => __( 'Virtual', 'woocommerce' ),
					'description'   => __( 'Virtual products are intangible and are not shipped.', 'woocommerce' ),
					'default'       => 'no',
				),
				'downloadable' => array(
					'id'            => '_downloadable',
					'wrapper_class' => 'show_if_simple',
					'label'         => __( 'Downloadable', 'woocommerce' ),
					'description'   => __( 'Downloadable products give access to a file upon purchase.', 'woocommerce' ),
					'default'       => 'no',
				),
			)
		);
	}

	/**
	 * Return array of tabs to show.
	 *
	 * @return array
	 */
	private static function get_product_data_tabs() {
		$tabs = apply_filters(
			'woocommerce_product_data_tabs',
			array(
				'general'        => array(
					'label'    => __( 'General', 'woocommerce' ),
					'target'   => 'general_product_data',
					'class'    => array( 'hide_if_grouped' ),
					'priority' => 10,
				),
				'inventory'      => array(
					'label'    => __( 'Inventory', 'woocommerce' ),
					'target'   => 'inventory_product_data',
					'class'    => array( 'show_if_simple', 'show_if_variable', 'show_if_grouped', 'show_if_external' ),
					'priority' => 20,
				),
				'shipping'       => array(
					'label'    => __( 'Shipping', 'woocommerce' ),
					'target'   => 'shipping_product_data',
					'class'    => array( 'hide_if_virtual', 'hide_if_grouped', 'hide_if_external' ),
					'priority' => 30,
				),
				'linked_product' => array(
					'label'    => __( 'Linked Products', 'woocommerce' ),
					'target'   => 'linked_product_data',
					'class'    => array(),
					'priority' => 40,
				),
				'attribute'      => array(
					'label'    => __( 'Attributes', 'woocommerce' ),
					'target'   => 'product_attributes',
					'class'    => array(),
					'priority' => 50,
				),
				'variations'     => array(
					'label'    => __( 'Variations', 'woocommerce' ),
					'target'   => 'variable_product_options',
					'class'    => array( 'show_if_variable' ),
					'priority' => 60,
				),
				'advanced'       => array(
					'label'    => __( 'Advanced', 'woocommerce' ),
					'target'   => 'advanced_product_data',
					'class'    => array(),
					'priority' => 70,
				),
			)
		);

		// Sort tabs based on priority.
		uasort( $tabs, array( __CLASS__, 'product_data_tabs_sort' ) );

		return $tabs;
	}

	/**
	 * Callback to sort product data tabs on priority.
	 *
	 * @since 3.1.0
	 * @param int $a First item.
	 * @param int $b Second item.
	 *
	 * @return bool
	 */
	private static function product_data_tabs_sort( $a, $b ) {
		if ( ! isset( $a['priority'], $b['priority'] ) ) {
			return -1;
		}

		if ( $a['priority'] === $b['priority'] ) {
			return 0;
		}

		return $a['priority'] < $b['priority'] ? -1 : 1;
	}

	/**
	 * Filter callback for finding variation attributes.
	 *
	 * @param  WC_Product_Attribute $attribute Product attribute.
	 * @return bool
	 */
	private static function filter_variation_attributes( $attribute ) {
		return true === $attribute->get_variation();
	}

	/**
	 * Show options for the variable product type.
	 */
	public static function output_variations() {
		global $post, $wpdb, $product_object;

		$variation_attributes   = array_filter( $product_object->get_attributes(), array( __CLASS__, 'filter_variation_attributes' ) );
		$default_attributes     = $product_object->get_default_attributes();
		$variations_count       = absint( apply_filters( 'woocommerce_admin_meta_boxes_variations_count', $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(ID) FROM $wpdb->posts WHERE post_parent = %d AND post_type = 'product_variation' AND post_status IN ('publish', 'private')", $post->ID ) ), $post->ID ) );
		$variations_per_page    = absint( apply_filters( 'woocommerce_admin_meta_boxes_variations_per_page', 15 ) );
		$variations_total_pages = ceil( $variations_count / $variations_per_page );

		include __DIR__ . '/views/html-product-data-variations.php';
	}

	/**
	 * Prepare downloads for save.
	 *
	 * @param array $file_names File names.
	 * @param array $file_urls File urls.
	 * @param array $file_hashes File hashes.
	 *
	 * @return array
	 */
	private static function prepare_downloads( $file_names, $file_urls, $file_hashes ) {
		$downloads = array();

		if ( ! empty( $file_urls ) ) {
			$file_url_size = count( $file_urls );

			for ( $i = 0; $i < $file_url_size; $i ++ ) {
				if ( ! empty( $file_urls[ $i ] ) ) {
					$downloads[] = array(
						'name'        => wc_clean( $file_names[ $i ] ),
						'file'        => wp_unslash( trim( $file_urls[ $i ] ) ),
						'download_id' => wc_clean( $file_hashes[ $i ] ),
					);
				}
			}
		}
		return $downloads;
	}

	/**
	 * Prepare children for save.
	 *
	 * @return array
	 */
	private static function prepare_children() {
		return isset( $_POST['grouped_products'] ) ? array_filter( array_map( 'intval', (array) $_POST['grouped_products'] ) ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Missing
	}

	/**
	 * Prepare attributes for save.
	 *
	 * @param array $data Attribute data.
	 *
	 * @return array
	 */
	public static function prepare_attributes( $data = false ) {
		$attributes = array();

		if ( ! $data ) {
			$data = stripslashes_deep( $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
		}

		if ( isset( $data['attribute_names'], $data['attribute_values'] ) ) {
			$attribute_names         = $data['attribute_names'];
			$attribute_values        = $data['attribute_values'];
			$attribute_visibility    = isset( $data['attribute_visibility'] ) ? $data['attribute_visibility'] : array();
			$attribute_variation     = isset( $data['attribute_variation'] ) ? $data['attribute_variation'] : array();
			$attribute_position      = $data['attribute_position'];
			$attribute_names_max_key = max( array_keys( $attribute_names ) );

			for ( $i = 0; $i <= $attribute_names_max_key; $i++ ) {
				if ( empty( $attribute_names[ $i ] ) || ! isset( $attribute_values[ $i ] ) ) {
					continue;
				}
				$attribute_id   = 0;
				$attribute_name = wc_clean( esc_html( $attribute_names[ $i ] ) );

				if ( 'pa_' === substr( $attribute_name, 0, 3 ) ) {
					$attribute_id = wc_attribute_taxonomy_id_by_name( $attribute_name );
				}

				$options = isset( $attribute_values[ $i ] ) ? $attribute_values[ $i ] : '';

				if ( is_array( $options ) ) {
					// Term ids sent as array.
					$options = wp_parse_id_list( $options );
				} else {
					// Terms or text sent in textarea.
					$options = 0 < $attribute_id ? wc_sanitize_textarea( esc_html( wc_sanitize_term_text_based( $options ) ) ) : wc_sanitize_textarea( esc_html( $options ) );
					$options = wc_get_text_attributes( $options );
				}

				if ( empty( $options ) ) {
					continue;
				}

				$attribute = new WC_Product_Attribute();
				$attribute->set_id( $attribute_id );
				$attribute->set_name( $attribute_name );
				$attribute->set_options( $options );
				$attribute->set_position( $attribute_position[ $i ] );
				$attribute->set_visible( isset( $attribute_visibility[ $i ] ) );
				$attribute->set_variation( isset( $attribute_variation[ $i ] ) );
				$attributes[] = apply_filters( 'woocommerce_admin_meta_boxes_prepare_attribute', $attribute, $data, $i );
			}
		}
		return $attributes;
	}

	/**
	 * Prepare attributes for a specific variation or defaults.
	 *
	 * @param  array  $all_attributes List of attribute keys.
	 * @param  string $key_prefix Attribute key prefix.
	 * @param  int    $index Attribute array index.
	 * @return array
	 */
	private static function prepare_set_attributes( $all_attributes, $key_prefix = 'attribute_', $index = null ) {
		$attributes = array();

		if ( $all_attributes ) {
			foreach ( $all_attributes as $attribute ) {
				if ( $attribute->get_variation() ) {
					$attribute_key = sanitize_title( $attribute->get_name() );

					if ( ! is_null( $index ) ) {
						$value = isset( $_POST[ $key_prefix . $attribute_key ][ $index ] ) ? wp_unslash( $_POST[ $key_prefix . $attribute_key ][ $index ] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
					} else {
						$value = isset( $_POST[ $key_prefix . $attribute_key ] ) ? wp_unslash( $_POST[ $key_prefix . $attribute_key ] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
					}

					if ( $attribute->is_taxonomy() ) {
						// Don't use wc_clean as it destroys sanitized characters.
						$value = sanitize_title( $value );
					} else {
						$value = html_entity_decode( wc_clean( $value ), ENT_QUOTES, get_bloginfo( 'charset' ) ); // WPCS: sanitization ok.
					}

					$attributes[ $attribute_key ] = $value;
				}
			}
		}

		return $attributes;
	}

	/**
	 * Save meta box data.
	 *
	 * @param int     $post_id WP post id.
	 * @param WP_Post $post Post object.
	 */
	public static function save( $post_id, $post ) {
		// phpcs:disable WordPress.Security.NonceVerification.Missing
		// Process product type first so we have the correct class to run setters.
		$product_type = empty( $_POST['product-type'] ) ? WC_Product_Factory::get_product_type( $post_id ) : sanitize_title( wp_unslash( $_POST['product-type'] ) );
		$classname    = WC_Product_Factory::get_product_classname( $post_id, $product_type ? $product_type : 'simple' );
		$product      = new $classname( $post_id );
		$attributes   = self::prepare_attributes();
		$stock        = null;

		// Handle stock changes.
		if ( isset( $_POST['_stock'] ) ) {
			if ( isset( $_POST['_original_stock'] ) && wc_stock_amount( $product->get_stock_quantity( 'edit' ) ) !== wc_stock_amount( wp_unslash( $_POST['_original_stock'] ) ) ) {
				/* translators: 1: product ID 2: quantity in stock */
				WC_Admin_Meta_Boxes::add_error( sprintf( __( 'The stock has not been updated because the value has changed since editing. Product %1$d has %2$d units in stock.', 'woocommerce' ), $product->get_id(), $product->get_stock_quantity( 'edit' ) ) );
			} else {
				$stock = wc_stock_amount( wp_unslash( $_POST['_stock'] ) );
			}
		}

		// Handle dates.
		$date_on_sale_from = '';
		$date_on_sale_to   = '';

		// Force date from to beginning of day.
		if ( isset( $_POST['_sale_price_dates_from'] ) ) {
			$date_on_sale_from = wc_clean( wp_unslash( $_POST['_sale_price_dates_from'] ) );

			if ( ! empty( $date_on_sale_from ) ) {
				$date_on_sale_from = date( 'Y-m-d 00:00:00', strtotime( $date_on_sale_from ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
			}
		}

		// Force date to to the end of the day.
		if ( isset( $_POST['_sale_price_dates_to'] ) ) {
			$date_on_sale_to = wc_clean( wp_unslash( $_POST['_sale_price_dates_to'] ) );

			if ( ! empty( $date_on_sale_to ) ) {
				$date_on_sale_to = date( 'Y-m-d 23:59:59', strtotime( $date_on_sale_to ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
			}
		}

		$errors = $product->set_props(
			array(
				'sku'                => isset( $_POST['_sku'] ) ? wc_clean( wp_unslash( $_POST['_sku'] ) ) : null,
				'purchase_note'      => isset( $_POST['_purchase_note'] ) ? wp_kses_post( wp_unslash( $_POST['_purchase_note'] ) ) : '',
				'downloadable'       => isset( $_POST['_downloadable'] ),
				'virtual'            => isset( $_POST['_virtual'] ),
				'featured'           => isset( $_POST['_featured'] ),
				'catalog_visibility' => isset( $_POST['_visibility'] ) ? wc_clean( wp_unslash( $_POST['_visibility'] ) ) : null,
				'tax_status'         => isset( $_POST['_tax_status'] ) ? wc_clean( wp_unslash( $_POST['_tax_status'] ) ) : null,
				'tax_class'          => isset( $_POST['_tax_class'] ) ? sanitize_title( wp_unslash( $_POST['_tax_class'] ) ) : null,
				'weight'             => isset( $_POST['_weight'] ) ? wc_clean( wp_unslash( $_POST['_weight'] ) ) : null,
				'length'             => isset( $_POST['_length'] ) ? wc_clean( wp_unslash( $_POST['_length'] ) ) : null,
				'width'              => isset( $_POST['_width'] ) ? wc_clean( wp_unslash( $_POST['_width'] ) ) : null,
				'height'             => isset( $_POST['_height'] ) ? wc_clean( wp_unslash( $_POST['_height'] ) ) : null,
				'shipping_class_id'  => isset( $_POST['product_shipping_class'] ) ? absint( wp_unslash( $_POST['product_shipping_class'] ) ) : null,
				'sold_individually'  => ! empty( $_POST['_sold_individually'] ),
				'upsell_ids'         => isset( $_POST['upsell_ids'] ) ? array_map( 'intval', (array) wp_unslash( $_POST['upsell_ids'] ) ) : array(),
				'cross_sell_ids'     => isset( $_POST['crosssell_ids'] ) ? array_map( 'intval', (array) wp_unslash( $_POST['crosssell_ids'] ) ) : array(),
				'regular_price'      => isset( $_POST['_regular_price'] ) ? wc_clean( wp_unslash( $_POST['_regular_price'] ) ) : null,
				'sale_price'         => isset( $_POST['_sale_price'] ) ? wc_clean( wp_unslash( $_POST['_sale_price'] ) ) : null,
				'date_on_sale_from'  => $date_on_sale_from,
				'date_on_sale_to'    => $date_on_sale_to,
				'manage_stock'       => ! empty( $_POST['_manage_stock'] ),
				'backorders'         => isset( $_POST['_backorders'] ) ? wc_clean( wp_unslash( $_POST['_backorders'] ) ) : null,
				'stock_status'       => isset( $_POST['_stock_status'] ) ? wc_clean( wp_unslash( $_POST['_stock_status'] ) ) : null,
				'stock_quantity'     => $stock,
				'low_stock_amount'   => isset( $_POST['_low_stock_amount'] ) && '' !== $_POST['_low_stock_amount'] ? wc_stock_amount( wp_unslash( $_POST['_low_stock_amount'] ) ) : '',
				'download_limit'     => isset( $_POST['_download_limit'] ) && '' !== $_POST['_download_limit'] ? absint( wp_unslash( $_POST['_download_limit'] ) ) : '',
				'download_expiry'    => isset( $_POST['_download_expiry'] ) && '' !== $_POST['_download_expiry'] ? absint( wp_unslash( $_POST['_download_expiry'] ) ) : '',
				// Those are sanitized inside prepare_downloads.
				'downloads'          => self::prepare_downloads(
					isset( $_POST['_wc_file_names'] ) ? wp_unslash( $_POST['_wc_file_names'] ) : array(), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
					isset( $_POST['_wc_file_urls'] ) ? wp_unslash( $_POST['_wc_file_urls'] ) : array(), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
					isset( $_POST['_wc_file_hashes'] ) ? wp_unslash( $_POST['_wc_file_hashes'] ) : array() // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
				),
				'product_url'        => isset( $_POST['_product_url'] ) ? esc_url_raw( wp_unslash( $_POST['_product_url'] ) ) : '',
				'button_text'        => isset( $_POST['_button_text'] ) ? wc_clean( wp_unslash( $_POST['_button_text'] ) ) : '',
				'children'           => 'grouped' === $product_type ? self::prepare_children() : null,
				'reviews_allowed'    => ! empty( $_POST['comment_status'] ) && 'open' === $_POST['comment_status'],
				'attributes'         => $attributes,
				'default_attributes' => self::prepare_set_attributes( $attributes, 'default_attribute_' ),
			)
		);

		if ( is_wp_error( $errors ) ) {
			WC_Admin_Meta_Boxes::add_error( $errors->get_error_message() );
		}

		/**
		 * Set props before save.
		 *
		 * @since 3.0.0
		 */
		do_action( 'woocommerce_admin_process_product_object', $product );

		$product->save();

		if ( $product->is_type( 'variable' ) ) {
			$original_post_title = isset( $_POST['original_post_title'] ) ? wc_clean( wp_unslash( $_POST['original_post_title'] ) ) : '';
			$post_title          = isset( $_POST['post_title'] ) ? wc_clean( wp_unslash( $_POST['post_title'] ) ) : '';

			$product->get_data_store()->sync_variation_names( $product, $original_post_title, $post_title );
		}

		do_action( 'woocommerce_process_product_meta_' . $product_type, $post_id );
		// phpcs:enable WordPress.Security.NonceVerification.Missing
	}

	/**
	 * Save variation meta box data.
	 *
	 * @param int     $post_id WP post id.
	 * @param WP_Post $post Post object.
	 */
	public static function save_variations( $post_id, $post ) {
		global $wpdb;

		// phpcs:disable WordPress.Security.NonceVerification.Missing
		if ( isset( $_POST['variable_post_id'] ) ) {
			$parent = wc_get_product( $post_id );
			$parent->set_default_attributes( self::prepare_set_attributes( $parent->get_attributes(), 'default_attribute_' ) );
			$parent->save();

			$max_loop   = max( array_keys( wp_unslash( $_POST['variable_post_id'] ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			$data_store = $parent->get_data_store();
			$data_store->sort_all_product_variations( $parent->get_id() );
			$new_variation_menu_order_id = ! empty( $_POST['new_variation_menu_order_id'] ) ? wc_clean( wp_unslash( $_POST['new_variation_menu_order_id'] ) ) : false;
			$new_variation_menu_order_value = ! empty( $_POST['new_variation_menu_order_value'] ) ? wc_clean( wp_unslash( $_POST['new_variation_menu_order_value'] ) ) : false;

			// Only perform this operation if setting menu order via the prompt.
			if ( $new_variation_menu_order_id && $new_variation_menu_order_value ) {
				/*
				 * We need to gather all the variations with menu order that is
				 * equal or greater than the menu order that is newly set and
				 * increment them by one so that we can correctly insert the updated
				 * variation menu order.
				 */
				$wpdb->query(
					$wpdb->prepare(
						"UPDATE {$wpdb->posts} SET menu_order = menu_order + 1 WHERE post_type = 'product_variation' AND post_parent = %d AND post_status = 'publish' AND menu_order >= %d AND ID != %d",
						$post_id,
						$new_variation_menu_order_value,
						$new_variation_menu_order_id
					)
				);
			}

			for ( $i = 0; $i <= $max_loop; $i++ ) {

				if ( ! isset( $_POST['variable_post_id'][ $i ] ) ) {
					continue;
				}
				$variation_id = absint( $_POST['variable_post_id'][ $i ] );
				$variation    = wc_get_product_object( 'variation', $variation_id );
				$stock        = null;

				// Handle stock changes.
				if ( isset( $_POST['variable_stock'], $_POST['variable_stock'][ $i ] ) ) {
					if ( isset( $_POST['variable_original_stock'], $_POST['variable_original_stock'][ $i ] ) && wc_stock_amount( $variation->get_stock_quantity( 'edit' ) ) !== wc_stock_amount( wp_unslash( $_POST['variable_original_stock'][ $i ] ) ) ) {
						/* translators: 1: product ID 2: quantity in stock */
						WC_Admin_Meta_Boxes::add_error( sprintf( __( 'The stock has not been updated because the value has changed since editing. Product %1$d has %2$d units in stock.', 'woocommerce' ), $variation->get_id(), $variation->get_stock_quantity( 'edit' ) ) );
					} else {
						$stock = wc_stock_amount( wp_unslash( $_POST['variable_stock'][ $i ] ) );
					}
				}

				// Handle dates.
				$date_on_sale_from = '';
				$date_on_sale_to   = '';

				// Force date from to beginning of day.
				if ( isset( $_POST['variable_sale_price_dates_from'][ $i ] ) ) {
					$date_on_sale_from = wc_clean( wp_unslash( $_POST['variable_sale_price_dates_from'][ $i ] ) );

					if ( ! empty( $date_on_sale_from ) ) {
						$date_on_sale_from = date( 'Y-m-d 00:00:00', strtotime( $date_on_sale_from ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
					}
				}

				// Force date to to the end of the day.
				if ( isset( $_POST['variable_sale_price_dates_to'][ $i ] ) ) {
					$date_on_sale_to = wc_clean( wp_unslash( $_POST['variable_sale_price_dates_to'][ $i ] ) );

					if ( ! empty( $date_on_sale_to ) ) {
						$date_on_sale_to = date( 'Y-m-d 23:59:59', strtotime( $date_on_sale_to ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
					}
				}

				$errors = $variation->set_props(
					array(
						'status'            => isset( $_POST['variable_enabled'][ $i ] ) ? 'publish' : 'private',
						'menu_order'        => isset( $_POST['variation_menu_order'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variation_menu_order'][ $i ] ) ) : null,
						'regular_price'     => isset( $_POST['variable_regular_price'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_regular_price'][ $i ] ) ) : null,
						'sale_price'        => isset( $_POST['variable_sale_price'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_sale_price'][ $i ] ) ) : null,
						'virtual'           => isset( $_POST['variable_is_virtual'][ $i ] ),
						'downloadable'      => isset( $_POST['variable_is_downloadable'][ $i ] ),
						'date_on_sale_from' => $date_on_sale_from,
						'date_on_sale_to'   => $date_on_sale_to,
						'description'       => isset( $_POST['variable_description'][ $i ] ) ? wp_kses_post( wp_unslash( $_POST['variable_description'][ $i ] ) ) : null,
						'download_limit'    => isset( $_POST['variable_download_limit'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_download_limit'][ $i ] ) ) : null,
						'download_expiry'   => isset( $_POST['variable_download_expiry'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_download_expiry'][ $i ] ) ) : null,
						// Those are sanitized inside prepare_downloads.
						'downloads'         => self::prepare_downloads(
							isset( $_POST['_wc_variation_file_names'][ $variation_id ] ) ? wp_unslash( $_POST['_wc_variation_file_names'][ $variation_id ] ) : array(), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
							isset( $_POST['_wc_variation_file_urls'][ $variation_id ] ) ? wp_unslash( $_POST['_wc_variation_file_urls'][ $variation_id ] ) : array(), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
							isset( $_POST['_wc_variation_file_hashes'][ $variation_id ] ) ? wp_unslash( $_POST['_wc_variation_file_hashes'][ $variation_id ] ) : array() // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
						),
						'manage_stock'      => isset( $_POST['variable_manage_stock'][ $i ] ),
						'stock_quantity'    => $stock,
						'low_stock_amount'  => isset( $_POST['variable_low_stock_amount'][ $i ] ) && '' !== $_POST['variable_low_stock_amount'][ $i ] ? wc_stock_amount( wp_unslash( $_POST['variable_low_stock_amount'][ $i ] ) ) : '',
						'backorders'        => isset( $_POST['variable_backorders'], $_POST['variable_backorders'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_backorders'][ $i ] ) ) : null,
						'stock_status'      => isset( $_POST['variable_stock_status'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_stock_status'][ $i ] ) ) : null,
						'image_id'          => isset( $_POST['upload_image_id'][ $i ] ) ? wc_clean( wp_unslash( $_POST['upload_image_id'][ $i ] ) ) : null,
						'attributes'        => self::prepare_set_attributes( $parent->get_attributes(), 'attribute_', $i ),
						'sku'               => isset( $_POST['variable_sku'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_sku'][ $i ] ) ) : '',
						'weight'            => isset( $_POST['variable_weight'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_weight'][ $i ] ) ) : '',
						'length'            => isset( $_POST['variable_length'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_length'][ $i ] ) ) : '',
						'width'             => isset( $_POST['variable_width'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_width'][ $i ] ) ) : '',
						'height'            => isset( $_POST['variable_height'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_height'][ $i ] ) ) : '',
						'shipping_class_id' => isset( $_POST['variable_shipping_class'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_shipping_class'][ $i ] ) ) : null,
						'tax_class'         => isset( $_POST['variable_tax_class'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_tax_class'][ $i ] ) ) : null,
					)
				);

				if ( is_wp_error( $errors ) ) {
					WC_Admin_Meta_Boxes::add_error( $errors->get_error_message() );
				}

				/**
				 * Set variation props before save.
				 *
				 * @param object $variation WC_Product_Variation object.
				 * @param int $i
				 * @since 3.8.0
				 */
				do_action( 'woocommerce_admin_process_variation_object', $variation, $i );

				$variation->save();
				do_action( 'woocommerce_save_product_variation', $variation_id, $i );
			}
		}
		// phpcs:enable WordPress.Security.NonceVerification.Missing
	}
}
admin/meta-boxes/class-wc-meta-box-product-reviews.php000064400000002757151336044770017024 0ustar00<?php
/**
 * Product Reviews
 *
 * Functions for displaying product reviews data meta box.
 *
 * @package WooCommerce\Admin\Meta Boxes
 */

defined( 'ABSPATH' ) || exit;

/**
 * WC_Meta_Box_Product_Reviews
 */
class WC_Meta_Box_Product_Reviews {

	/**
	 * Output the metabox.
	 *
	 * @param object $comment Comment being shown.
	 */
	public static function output( $comment ) {
		wp_nonce_field( 'woocommerce_save_data', 'woocommerce_meta_nonce' );

		$current = get_comment_meta( $comment->comment_ID, 'rating', true );
		?>
		<select name="rating" id="rating">
			<?php
			for ( $rating = 1; $rating <= 5; $rating ++ ) {
				printf( '<option value="%1$s"%2$s>%1$s</option>', $rating, selected( $current, $rating, false ) ); // WPCS: XSS ok.
			}
			?>
		</select>
		<?php
	}

	/**
	 * Save meta box data
	 *
	 * @param mixed $data Data to save.
	 * @return mixed
	 */
	public static function save( $data ) {
		// Not allowed, return regular value without updating meta.
		if ( ! isset( $_POST['woocommerce_meta_nonce'], $_POST['rating'] ) || ! wp_verify_nonce( wp_unslash( $_POST['woocommerce_meta_nonce'] ), 'woocommerce_save_data' ) ) { // WPCS: input var ok, sanitization ok.
			return $data;
		}

		if ( $_POST['rating'] > 5 || $_POST['rating'] < 0 ) { // WPCS: input var ok.
			return $data;
		}

		$comment_id = $data['comment_ID'];

		update_comment_meta( $comment_id, 'rating', intval( wp_unslash( $_POST['rating'] ) ) ); // WPCS: input var ok.

		// Return regular value after updating.
		return $data;
	}
}
admin/meta-boxes/class-wc-meta-box-order-data.php000064400000052564151336044770015705 0ustar00<?php
/**
 * Order Data
 *
 * Functions for displaying the order data meta box.
 *
 * @author      WooThemes
 * @category    Admin
 * @package     WooCommerce\Admin\Meta Boxes
 * @version     2.2.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

/**
 * WC_Meta_Box_Order_Data Class.
 */
class WC_Meta_Box_Order_Data {

	/**
	 * Billing fields.
	 *
	 * @var array
	 */
	protected static $billing_fields = array();

	/**
	 * Shipping fields.
	 *
	 * @var array
	 */
	protected static $shipping_fields = array();

	/**
	 * Init billing and shipping fields we display + save.
	 */
	public static function init_address_fields() {

		self::$billing_fields = apply_filters(
			'woocommerce_admin_billing_fields',
			array(
				'first_name' => array(
					'label' => __( 'First name', 'woocommerce' ),
					'show'  => false,
				),
				'last_name'  => array(
					'label' => __( 'Last name', 'woocommerce' ),
					'show'  => false,
				),
				'company'    => array(
					'label' => __( 'Company', 'woocommerce' ),
					'show'  => false,
				),
				'address_1'  => array(
					'label' => __( 'Address line 1', 'woocommerce' ),
					'show'  => false,
				),
				'address_2'  => array(
					'label' => __( 'Address line 2', 'woocommerce' ),
					'show'  => false,
				),
				'city'       => array(
					'label' => __( 'City', 'woocommerce' ),
					'show'  => false,
				),
				'postcode'   => array(
					'label' => __( 'Postcode / ZIP', 'woocommerce' ),
					'show'  => false,
				),
				'country'    => array(
					'label'   => __( 'Country / Region', 'woocommerce' ),
					'show'    => false,
					'class'   => 'js_field-country select short',
					'type'    => 'select',
					'options' => array( '' => __( 'Select a country / region&hellip;', 'woocommerce' ) ) + WC()->countries->get_allowed_countries(),
				),
				'state'      => array(
					'label' => __( 'State / County', 'woocommerce' ),
					'class' => 'js_field-state select short',
					'show'  => false,
				),
				'email'      => array(
					'label' => __( 'Email address', 'woocommerce' ),
				),
				'phone'      => array(
					'label' => __( 'Phone', 'woocommerce' ),
				),
			)
		);

		self::$shipping_fields = apply_filters(
			'woocommerce_admin_shipping_fields',
			array(
				'first_name' => array(
					'label' => __( 'First name', 'woocommerce' ),
					'show'  => false,
				),
				'last_name'  => array(
					'label' => __( 'Last name', 'woocommerce' ),
					'show'  => false,
				),
				'company'    => array(
					'label' => __( 'Company', 'woocommerce' ),
					'show'  => false,
				),
				'address_1'  => array(
					'label' => __( 'Address line 1', 'woocommerce' ),
					'show'  => false,
				),
				'address_2'  => array(
					'label' => __( 'Address line 2', 'woocommerce' ),
					'show'  => false,
				),
				'city'       => array(
					'label' => __( 'City', 'woocommerce' ),
					'show'  => false,
				),
				'postcode'   => array(
					'label' => __( 'Postcode / ZIP', 'woocommerce' ),
					'show'  => false,
				),
				'country'    => array(
					'label'   => __( 'Country / Region', 'woocommerce' ),
					'show'    => false,
					'type'    => 'select',
					'class'   => 'js_field-country select short',
					'options' => array( '' => __( 'Select a country / region&hellip;', 'woocommerce' ) ) + WC()->countries->get_shipping_countries(),
				),
				'state'      => array(
					'label' => __( 'State / County', 'woocommerce' ),
					'class' => 'js_field-state select short',
					'show'  => false,
				),
				'phone'      => array(
					'label' => __( 'Phone', 'woocommerce' ),
				),
			)
		);
	}

	/**
	 * Output the metabox.
	 *
	 * @param WP_Post $post
	 */
	public static function output( $post ) {
		global $theorder;

		if ( ! is_object( $theorder ) ) {
			$theorder = wc_get_order( $post->ID );
		}

		$order = $theorder;

		self::init_address_fields();

		if ( WC()->payment_gateways() ) {
			$payment_gateways = WC()->payment_gateways->payment_gateways();
		} else {
			$payment_gateways = array();
		}

		$payment_method = $order->get_payment_method();

		$order_type_object = get_post_type_object( $post->post_type );
		wp_nonce_field( 'woocommerce_save_data', 'woocommerce_meta_nonce' );
		?>
		<style type="text/css">
			#post-body-content, #titlediv { display:none }
		</style>
		<div class="panel-wrap woocommerce">
			<input name="post_title" type="hidden" value="<?php echo empty( $post->post_title ) ? __( 'Order', 'woocommerce' ) : esc_attr( $post->post_title ); ?>" />
			<input name="post_status" type="hidden" value="<?php echo esc_attr( $post->post_status ); ?>" />
			<div id="order_data" class="panel woocommerce-order-data">
				<h2 class="woocommerce-order-data__heading">
					<?php

					/* translators: 1: order type 2: order number */
					printf(
						esc_html__( '%1$s #%2$s details', 'woocommerce' ),
						esc_html( $order_type_object->labels->singular_name ),
						esc_html( $order->get_order_number() )
					);

					?>
				</h2>
				<p class="woocommerce-order-data__meta order_number">
					<?php

					$meta_list = array();

					if ( $payment_method && 'other' !== $payment_method ) {
						/* translators: %s: payment method */
						$payment_method_string = sprintf(
							__( 'Payment via %s', 'woocommerce' ),
							esc_html( isset( $payment_gateways[ $payment_method ] ) ? $payment_gateways[ $payment_method ]->get_title() : $payment_method )
						);

						if ( $transaction_id = $order->get_transaction_id() ) {
							if ( isset( $payment_gateways[ $payment_method ] ) && ( $url = $payment_gateways[ $payment_method ]->get_transaction_url( $order ) ) ) {
								$payment_method_string .= ' (<a href="' . esc_url( $url ) . '" target="_blank">' . esc_html( $transaction_id ) . '</a>)';
							} else {
								$payment_method_string .= ' (' . esc_html( $transaction_id ) . ')';
							}
						}

						$meta_list[] = $payment_method_string;
					}

					if ( $order->get_date_paid() ) {
						/* translators: 1: date 2: time */
						$meta_list[] = sprintf(
							__( 'Paid on %1$s @ %2$s', 'woocommerce' ),
							wc_format_datetime( $order->get_date_paid() ),
							wc_format_datetime( $order->get_date_paid(), get_option( 'time_format' ) )
						);
					}

					if ( $ip_address = $order->get_customer_ip_address() ) {
						/* translators: %s: IP address */
						$meta_list[] = sprintf(
							__( 'Customer IP: %s', 'woocommerce' ),
							'<span class="woocommerce-Order-customerIP">' . esc_html( $ip_address ) . '</span>'
						);
					}

					echo wp_kses_post( implode( '. ', $meta_list ) );

					?>
				</p>
				<div class="order_data_column_container">
					<div class="order_data_column">
						<h3><?php esc_html_e( 'General', 'woocommerce' ); ?></h3>

						<p class="form-field form-field-wide">
							<label for="order_date"><?php _e( 'Date created:', 'woocommerce' ); ?></label>
							<input type="text" class="date-picker" name="order_date" maxlength="10" value="<?php echo esc_attr( date_i18n( 'Y-m-d', strtotime( $post->post_date ) ) ); ?>" pattern="<?php echo esc_attr( apply_filters( 'woocommerce_date_input_html_pattern', '[0-9]{4}-(0[1-9]|1[012])-(0[1-9]|1[0-9]|2[0-9]|3[01])' ) ); ?>" />@
							&lrm;
							<input type="number" class="hour" placeholder="<?php esc_attr_e( 'h', 'woocommerce' ); ?>" name="order_date_hour" min="0" max="23" step="1" value="<?php echo esc_attr( date_i18n( 'H', strtotime( $post->post_date ) ) ); ?>" pattern="([01]?[0-9]{1}|2[0-3]{1})" />:
							<input type="number" class="minute" placeholder="<?php esc_attr_e( 'm', 'woocommerce' ); ?>" name="order_date_minute" min="0" max="59" step="1" value="<?php echo esc_attr( date_i18n( 'i', strtotime( $post->post_date ) ) ); ?>" pattern="[0-5]{1}[0-9]{1}" />
							<input type="hidden" name="order_date_second" value="<?php echo esc_attr( date_i18n( 's', strtotime( $post->post_date ) ) ); ?>" />
						</p>

						<p class="form-field form-field-wide wc-order-status">
							<label for="order_status">
								<?php
								_e( 'Status:', 'woocommerce' );
								if ( $order->needs_payment() ) {
									printf(
										'<a href="%s">%s</a>',
										esc_url( $order->get_checkout_payment_url() ),
										__( 'Customer payment page &rarr;', 'woocommerce' )
									);
								}
								?>
							</label>
							<select id="order_status" name="order_status" class="wc-enhanced-select">
								<?php
								$statuses = wc_get_order_statuses();
								foreach ( $statuses as $status => $status_name ) {
									echo '<option value="' . esc_attr( $status ) . '" ' . selected( $status, 'wc-' . $order->get_status( 'edit' ), false ) . '>' . esc_html( $status_name ) . '</option>';
								}
								?>
							</select>
						</p>

						<p class="form-field form-field-wide wc-customer-user">
							<!--email_off--> <!-- Disable CloudFlare email obfuscation -->
							<label for="customer_user">
								<?php
								_e( 'Customer:', 'woocommerce' );
								if ( $order->get_user_id( 'edit' ) ) {
									$args = array(
										'post_status'    => 'all',
										'post_type'      => 'shop_order',
										'_customer_user' => $order->get_user_id( 'edit' ),
									);
									printf(
										'<a href="%s">%s</a>',
										esc_url( add_query_arg( $args, admin_url( 'edit.php' ) ) ),
										' ' . __( 'View other orders &rarr;', 'woocommerce' )
									);
									printf(
										'<a href="%s">%s</a>',
										esc_url( add_query_arg( 'user_id', $order->get_user_id( 'edit' ), admin_url( 'user-edit.php' ) ) ),
										' ' . __( 'Profile &rarr;', 'woocommerce' )
									);
								}
								?>
							</label>
							<?php
							$user_string = '';
							$user_id     = '';
							if ( $order->get_user_id() ) {
								$user_id = absint( $order->get_user_id() );
								$user    = get_user_by( 'id', $user_id );
								/* translators: 1: user display name 2: user ID 3: user email */
								$user_string = sprintf(
									esc_html__( '%1$s (#%2$s &ndash; %3$s)', 'woocommerce' ),
									$user->display_name,
									absint( $user->ID ),
									$user->user_email
								);
							}
							?>
							<select class="wc-customer-search" id="customer_user" name="customer_user" data-placeholder="<?php esc_attr_e( 'Guest', 'woocommerce' ); ?>" data-allow_clear="true">
								<option value="<?php echo esc_attr( $user_id ); ?>" selected="selected"><?php echo htmlspecialchars( wp_kses_post( $user_string ) ); // htmlspecialchars to prevent XSS when rendered by selectWoo. ?></option>
							</select>
							<!--/email_off-->
						</p>
						<?php do_action( 'woocommerce_admin_order_data_after_order_details', $order ); ?>
					</div>
					<div class="order_data_column">
						<h3>
							<?php esc_html_e( 'Billing', 'woocommerce' ); ?>
							<a href="#" class="edit_address"><?php esc_html_e( 'Edit', 'woocommerce' ); ?></a>
							<span>
								<a href="#" class="load_customer_billing" style="display:none;"><?php esc_html_e( 'Load billing address', 'woocommerce' ); ?></a>
							</span>
						</h3>
						<div class="address">
							<?php

							// Display values.
							if ( $order->get_formatted_billing_address() ) {
								echo '<p>' . wp_kses( $order->get_formatted_billing_address(), array( 'br' => array() ) ) . '</p>';
							} else {
								echo '<p class="none_set"><strong>' . __( 'Address:', 'woocommerce' ) . '</strong> ' . __( 'No billing address set.', 'woocommerce' ) . '</p>';
							}

							foreach ( self::$billing_fields as $key => $field ) {
								if ( isset( $field['show'] ) && false === $field['show'] ) {
									continue;
								}

								$field_name = 'billing_' . $key;

								if ( isset( $field['value'] ) ) {
									$field_value = $field['value'];
								} elseif ( is_callable( array( $order, 'get_' . $field_name ) ) ) {
									$field_value = $order->{"get_$field_name"}( 'edit' );
								} else {
									$field_value = $order->get_meta( '_' . $field_name );
								}

								if ( 'billing_phone' === $field_name ) {
									$field_value = wc_make_phone_clickable( $field_value );
								} elseif ( 'billing_email' === $field_name ) {
									$field_value = '<a href="' . esc_url( 'mailto:' . $field_value ) . '">' . $field_value . '</a>';
								} else {
									$field_value = make_clickable( esc_html( $field_value ) );
								}

								if ( $field_value ) {
									echo '<p><strong>' . esc_html( $field['label'] ) . ':</strong> ' . wp_kses_post( $field_value ) . '</p>';
								}
							}
							?>
						</div>

						<div class="edit_address">
							<?php

							// Display form.
							foreach ( self::$billing_fields as $key => $field ) {
								if ( ! isset( $field['type'] ) ) {
									$field['type'] = 'text';
								}
								if ( ! isset( $field['id'] ) ) {
									$field['id'] = '_billing_' . $key;
								}

								$field_name = 'billing_' . $key;

								if ( ! isset( $field['value'] ) ) {
									if ( is_callable( array( $order, 'get_' . $field_name ) ) ) {
										$field['value'] = $order->{"get_$field_name"}( 'edit' );
									} else {
										$field['value'] = $order->get_meta( '_' . $field_name );
									}
								}

								switch ( $field['type'] ) {
									case 'select':
										woocommerce_wp_select( $field );
										break;
									default:
										woocommerce_wp_text_input( $field );
										break;
								}
							}
							?>
							<p class="form-field form-field-wide">
								<label><?php esc_html_e( 'Payment method:', 'woocommerce' ); ?></label>
								<select name="_payment_method" id="_payment_method" class="first">
									<option value=""><?php esc_html_e( 'N/A', 'woocommerce' ); ?></option>
									<?php
									$found_method = false;

									foreach ( $payment_gateways as $gateway ) {
										if ( 'yes' === $gateway->enabled ) {
											echo '<option value="' . esc_attr( $gateway->id ) . '" ' . selected( $payment_method, $gateway->id, false ) . '>' . esc_html( $gateway->get_title() ) . '</option>';
											if ( $payment_method == $gateway->id ) {
												$found_method = true;
											}
										}
									}

									if ( ! $found_method && ! empty( $payment_method ) ) {
										echo '<option value="' . esc_attr( $payment_method ) . '" selected="selected">' . esc_html__( 'Other', 'woocommerce' ) . '</option>';
									} else {
										echo '<option value="other">' . esc_html__( 'Other', 'woocommerce' ) . '</option>';
									}
									?>
								</select>
							</p>
							<?php

							woocommerce_wp_text_input(
								array(
									'id'    => '_transaction_id',
									'label' => __( 'Transaction ID', 'woocommerce' ),
									'value' => $order->get_transaction_id( 'edit' ),
								)
							);
							?>

						</div>
						<?php do_action( 'woocommerce_admin_order_data_after_billing_address', $order ); ?>
					</div>
					<div class="order_data_column">
						<h3>
							<?php esc_html_e( 'Shipping', 'woocommerce' ); ?>
							<a href="#" class="edit_address"><?php esc_html_e( 'Edit', 'woocommerce' ); ?></a>
							<span>
								<a href="#" class="load_customer_shipping" style="display:none;"><?php esc_html_e( 'Load shipping address', 'woocommerce' ); ?></a>
								<a href="#" class="billing-same-as-shipping" style="display:none;"><?php esc_html_e( 'Copy billing address', 'woocommerce' ); ?></a>
							</span>
						</h3>
						<div class="address">
							<?php

							// Display values.
							if ( $order->get_formatted_shipping_address() ) {
								echo '<p>' . wp_kses( $order->get_formatted_shipping_address(), array( 'br' => array() ) ) . '</p>';
							} else {
								echo '<p class="none_set"><strong>' . __( 'Address:', 'woocommerce' ) . '</strong> ' . __( 'No shipping address set.', 'woocommerce' ) . '</p>';
							}

							if ( ! empty( self::$shipping_fields ) ) {
								foreach ( self::$shipping_fields as $key => $field ) {
									if ( isset( $field['show'] ) && false === $field['show'] ) {
										continue;
									}

									$field_name = 'shipping_' . $key;

									if ( is_callable( array( $order, 'get_' . $field_name ) ) ) {
										$field_value = $order->{"get_$field_name"}( 'edit' );
									} else {
										$field_value = $order->get_meta( '_' . $field_name );
									}

									if ( 'shipping_phone' === $field_name ) {
										$field_value = wc_make_phone_clickable( $field_value );
									}

									if ( $field_value ) {
										echo '<p><strong>' . esc_html( $field['label'] ) . ':</strong> ' . wp_kses_post( $field_value ) . '</p>';
									}
								}
							}

							if ( apply_filters( 'woocommerce_enable_order_notes_field', 'yes' == get_option( 'woocommerce_enable_order_comments', 'yes' ) ) && $post->post_excerpt ) {
								echo '<p class="order_note"><strong>' . __( 'Customer provided note:', 'woocommerce' ) . '</strong> ' . nl2br( esc_html( $post->post_excerpt ) ) . '</p>';
							}
							?>
						</div>
						<div class="edit_address">
							<?php

							// Display form.
							if ( ! empty( self::$shipping_fields ) ) {
								foreach ( self::$shipping_fields as $key => $field ) {
									if ( ! isset( $field['type'] ) ) {
										$field['type'] = 'text';
									}
									if ( ! isset( $field['id'] ) ) {
										$field['id'] = '_shipping_' . $key;
									}

									$field_name = 'shipping_' . $key;

									if ( is_callable( array( $order, 'get_' . $field_name ) ) ) {
										$field['value'] = $order->{"get_$field_name"}( 'edit' );
									} else {
										$field['value'] = $order->get_meta( '_' . $field_name );
									}

									switch ( $field['type'] ) {
										case 'select':
											woocommerce_wp_select( $field );
											break;
										default:
											woocommerce_wp_text_input( $field );
											break;
									}
								}
							}

							if ( apply_filters( 'woocommerce_enable_order_notes_field', 'yes' == get_option( 'woocommerce_enable_order_comments', 'yes' ) ) ) :
								?>
								<p class="form-field form-field-wide">
									<label for="excerpt"><?php _e( 'Customer provided note', 'woocommerce' ); ?>:</label>
									<textarea rows="1" cols="40" name="excerpt" tabindex="6" id="excerpt" placeholder="<?php esc_attr_e( 'Customer notes about the order', 'woocommerce' ); ?>"><?php echo wp_kses_post( $post->post_excerpt ); ?></textarea>
								</p>
							<?php endif; ?>
						</div>

						<?php do_action( 'woocommerce_admin_order_data_after_shipping_address', $order ); ?>
					</div>
				</div>
				<div class="clear"></div>
			</div>
		</div>
		<?php
	}

	/**
	 * Save meta box data.
	 *
	 * @param int $order_id Order ID.
	 */
	public static function save( $order_id ) {
		self::init_address_fields();

		// Ensure gateways are loaded in case they need to insert data into the emails.
		WC()->payment_gateways();
		WC()->shipping();

		// Get order object.
		$order = wc_get_order( $order_id );
		$props = array();

		// Create order key.
		if ( ! $order->get_order_key() ) {
			$props['order_key'] = wc_generate_order_key();
		}

		// Update customer.
		$customer_id = isset( $_POST['customer_user'] ) ? absint( $_POST['customer_user'] ) : 0;
		if ( $customer_id !== $order->get_customer_id() ) {
			$props['customer_id'] = $customer_id;
		}

		// Update billing fields.
		if ( ! empty( self::$billing_fields ) ) {
			foreach ( self::$billing_fields as $key => $field ) {
				if ( ! isset( $field['id'] ) ) {
					$field['id'] = '_billing_' . $key;
				}

				if ( ! isset( $_POST[ $field['id'] ] ) ) {
					continue;
				}

				if ( is_callable( array( $order, 'set_billing_' . $key ) ) ) {
					$props[ 'billing_' . $key ] = wc_clean( wp_unslash( $_POST[ $field['id'] ] ) );
				} else {
					$order->update_meta_data( $field['id'], wc_clean( wp_unslash( $_POST[ $field['id'] ] ) ) );
				}
			}
		}

		// Update shipping fields.
		if ( ! empty( self::$shipping_fields ) ) {
			foreach ( self::$shipping_fields as $key => $field ) {
				if ( ! isset( $field['id'] ) ) {
					$field['id'] = '_shipping_' . $key;
				}

				if ( ! isset( $_POST[ $field['id'] ] ) ) {
					continue;
				}

				if ( is_callable( array( $order, 'set_shipping_' . $key ) ) ) {
					$props[ 'shipping_' . $key ] = wc_clean( wp_unslash( $_POST[ $field['id'] ] ) );
				} else {
					$order->update_meta_data( $field['id'], wc_clean( wp_unslash( $_POST[ $field['id'] ] ) ) );
				}
			}
		}

		if ( isset( $_POST['_transaction_id'] ) ) {
			$props['transaction_id'] = wc_clean( wp_unslash( $_POST['_transaction_id'] ) );
		}

		// Payment method handling.
		if ( $order->get_payment_method() !== wp_unslash( $_POST['_payment_method'] ) ) {
			$methods              = WC()->payment_gateways->payment_gateways();
			$payment_method       = wc_clean( wp_unslash( $_POST['_payment_method'] ) );
			$payment_method_title = $payment_method;

			if ( isset( $methods ) && isset( $methods[ $payment_method ] ) ) {
				$payment_method_title = $methods[ $payment_method ]->get_title();
			}

			if ( $payment_method == 'other') {
				$payment_method_title = esc_html__( 'Other', 'woocommerce' );
			}
			
			$props['payment_method']       = $payment_method;
			$props['payment_method_title'] = $payment_method_title;
		}

		// Update date.
		if ( empty( $_POST['order_date'] ) ) {
			$date = time();
		} else {
			$date = gmdate( 'Y-m-d H:i:s', strtotime( $_POST['order_date'] . ' ' . (int) $_POST['order_date_hour'] . ':' . (int) $_POST['order_date_minute'] . ':' . (int) $_POST['order_date_second'] ) );
		}

		$props['date_created'] = $date;

		// Set created via prop if new post.
		if ( isset( $_POST['original_post_status'] ) && $_POST['original_post_status'] === 'auto-draft' ) {
			$props['created_via'] = 'admin';
		}

		// Save order data.
		$order->set_props( $props );
		$order->set_status( wc_clean( wp_unslash( $_POST['order_status'] ) ), '', true );
		$order->save();
	}
}
admin/meta-boxes/class-wc-meta-box-order-items.php000064400000002015151336044770016077 0ustar00<?php
/**
 * Order Data
 *
 * Functions for displaying the order items meta box.
 *
 * @author      WooThemes
 * @category    Admin
 * @package     WooCommerce\Admin\Meta Boxes
 * @version     2.1.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

/**
 * WC_Meta_Box_Order_Items Class.
 */
class WC_Meta_Box_Order_Items {

	/**
	 * Output the metabox.
	 *
	 * @param WP_Post $post
	 */
	public static function output( $post ) {
		global $post, $thepostid, $theorder;

		if ( ! is_int( $thepostid ) ) {
			$thepostid = $post->ID;
		}

		if ( ! is_object( $theorder ) ) {
			$theorder = wc_get_order( $thepostid );
		}

		$order = $theorder;
		$data  = get_post_meta( $post->ID );

		include __DIR__ . '/views/html-order-items.php';
	}

	/**
	 * Save meta box data.
	 *
	 * @param int $post_id
	 */
	public static function save( $post_id ) {
		/**
		 * This $_POST variable's data has been validated and escaped
		 * inside `wc_save_order_items()` function.
		 */
		wc_save_order_items( $post_id, $_POST );
	}
}
admin/meta-boxes/class-wc-meta-box-order-actions.php000064400000013541151336044770016424 0ustar00<?php
/**
 * Order Actions
 *
 * Functions for displaying the order actions meta box.
 *
 * @package     WooCommerce\Admin\Meta Boxes
 * @version     2.1.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * WC_Meta_Box_Order_Actions Class.
 */
class WC_Meta_Box_Order_Actions {

	/**
	 * Output the metabox.
	 *
	 * @param WP_Post $post Post object.
	 */
	public static function output( $post ) {
		global $theorder;

		// This is used by some callbacks attached to hooks such as woocommerce_order_actions which rely on the global to determine if actions should be displayed for certain orders.
		// Avoid using this global with the `woocommerce_order_actions` filter, instead use the $order filter arg.
		if ( ! is_object( $theorder ) ) {
			$theorder = wc_get_order( $post->ID );
		}

		$theorder = $theorder instanceof WC_Order ? $theorder : null;
		$order_actions = self::get_available_order_actions_for_order( $theorder );
		?>
		<ul class="order_actions submitbox">

			<?php do_action( 'woocommerce_order_actions_start', $post->ID ); ?>

			<li class="wide" id="actions">
				<select name="wc_order_action">
					<option value=""><?php esc_html_e( 'Choose an action...', 'woocommerce' ); ?></option>
					<?php foreach ( $order_actions as $action => $title ) { ?>
						<option value="<?php echo esc_attr( $action ); ?>"><?php echo esc_html( $title ); ?></option>
					<?php } ?>
				</select>
				<button class="button wc-reload"><span><?php esc_html_e( 'Apply', 'woocommerce' ); ?></span></button>
			</li>

			<li class="wide">
				<div id="delete-action">
					<?php
					if ( current_user_can( 'delete_post', $post->ID ) ) {

						if ( ! EMPTY_TRASH_DAYS ) {
							$delete_text = __( 'Delete permanently', 'woocommerce' );
						} else {
							$delete_text = __( 'Move to Trash', 'woocommerce' );
						}
						?>
						<a class="submitdelete deletion" href="<?php echo esc_url( get_delete_post_link( $post->ID ) ); ?>"><?php echo esc_html( $delete_text ); ?></a>
						<?php
					}
					?>
				</div>

				<button type="submit" class="button save_order button-primary" name="save" value="<?php echo 'auto-draft' === $post->post_status ? esc_attr__( 'Create', 'woocommerce' ) : esc_attr__( 'Update', 'woocommerce' ); ?>"><?php echo 'auto-draft' === $post->post_status ? esc_html__( 'Create', 'woocommerce' ) : esc_html__( 'Update', 'woocommerce' ); ?></button>
			</li>

			<?php do_action( 'woocommerce_order_actions_end', $post->ID ); ?>

		</ul>
		<?php
	}

	/**
	 * Save meta box data.
	 *
	 * @param int     $post_id Post ID.
	 * @param WP_Post $post Post Object.
	 */
	public static function save( $post_id, $post ) {
		// Order data saved, now get it so we can manipulate status.
		$order = wc_get_order( $post_id );

		// Handle button actions.
		if ( ! empty( $_POST['wc_order_action'] ) ) { // @codingStandardsIgnoreLine

			$action = wc_clean( wp_unslash( $_POST['wc_order_action'] ) ); // @codingStandardsIgnoreLine

			if ( 'send_order_details' === $action ) {
				do_action( 'woocommerce_before_resend_order_emails', $order, 'customer_invoice' );

				// Send the customer invoice email.
				WC()->payment_gateways();
				WC()->shipping();
				WC()->mailer()->customer_invoice( $order );

				// Note the event.
				$order->add_order_note( __( 'Order details manually sent to customer.', 'woocommerce' ), false, true );

				do_action( 'woocommerce_after_resend_order_email', $order, 'customer_invoice' );

				// Change the post saved message.
				add_filter( 'redirect_post_location', array( __CLASS__, 'set_email_sent_message' ) );

			} elseif ( 'send_order_details_admin' === $action ) {

				do_action( 'woocommerce_before_resend_order_emails', $order, 'new_order' );

				WC()->payment_gateways();
				WC()->shipping();
				add_filter( 'woocommerce_new_order_email_allows_resend', '__return_true' );
				WC()->mailer()->emails['WC_Email_New_Order']->trigger( $order->get_id(), $order, true );
				remove_filter( 'woocommerce_new_order_email_allows_resend', '__return_true' );

				do_action( 'woocommerce_after_resend_order_email', $order, 'new_order' );

				// Change the post saved message.
				add_filter( 'redirect_post_location', array( __CLASS__, 'set_email_sent_message' ) );

			} elseif ( 'regenerate_download_permissions' === $action ) {

				$data_store = WC_Data_Store::load( 'customer-download' );
				$data_store->delete_by_order_id( $post_id );
				wc_downloadable_product_permissions( $post_id, true );

			} else {

				if ( ! did_action( 'woocommerce_order_action_' . sanitize_title( $action ) ) ) {
					do_action( 'woocommerce_order_action_' . sanitize_title( $action ), $order );
				}
			}
		}
	}

	/**
	 * Set the correct message ID.
	 *
	 * @param string $location Location.
	 * @since  2.3.0
	 * @static
	 * @return string
	 */
	public static function set_email_sent_message( $location ) {
		return add_query_arg( 'message', 11, $location );
	}

	/**
	 * Get the available order actions for a given order.
	 *
	 * @since 5.8.0
	 *
	 * @param WC_Order|null $order The order object or null if no order is available.
	 *
	 * @return array
	 */
	private static function get_available_order_actions_for_order( $order ) {
		$actions = array(
			'send_order_details'              => __( 'Email invoice / order details to customer', 'woocommerce' ),
			'send_order_details_admin'        => __( 'Resend new order notification', 'woocommerce' ),
			'regenerate_download_permissions' => __( 'Regenerate download permissions', 'woocommerce' ),
		);

		/**
		 * Filter: woocommerce_order_actions
		 * Allows filtering of the available order actions for an order.
		 *
		 * @since 2.1.0 Filter was added.
		 * @since 5.8.0 The $order param was added.
		 *
		 * @param array         $actions The available order actions for the order.
		 * @param WC_Order|null $order   The order object or null if no order is available.
		 */
		return apply_filters( 'woocommerce_order_actions', $actions, $order );
	}
}
admin/meta-boxes/class-wc-meta-box-product-images.php000064400000006675151336044770016610 0ustar00<?php
/**
 * Product Images
 *
 * Display the product images meta box.
 *
 * @author      WooThemes
 * @category    Admin
 * @package     WooCommerce\Admin\Meta Boxes
 * @version     2.1.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

/**
 * WC_Meta_Box_Product_Images Class.
 */
class WC_Meta_Box_Product_Images {

	/**
	 * Output the metabox.
	 *
	 * @param WP_Post $post
	 */
	public static function output( $post ) {
		global $thepostid, $product_object;

		$thepostid      = $post->ID;
		$product_object = $thepostid ? wc_get_product( $thepostid ) : new WC_Product();
		wp_nonce_field( 'woocommerce_save_data', 'woocommerce_meta_nonce' );
		?>
		<div id="product_images_container">
			<ul class="product_images">
				<?php
				$product_image_gallery = $product_object->get_gallery_image_ids( 'edit' );

				$attachments         = array_filter( $product_image_gallery );
				$update_meta         = false;
				$updated_gallery_ids = array();

				if ( ! empty( $attachments ) ) {
					foreach ( $attachments as $attachment_id ) {
						$attachment = wp_get_attachment_image( $attachment_id, 'thumbnail' );

						// if attachment is empty skip.
						if ( empty( $attachment ) ) {
							$update_meta = true;
							continue;
						}
						?>
						<li class="image" data-attachment_id="<?php echo esc_attr( $attachment_id ); ?>">
							<?php echo $attachment; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
							<ul class="actions">
								<li><a href="#" class="delete tips" data-tip="<?php esc_attr_e( 'Delete image', 'woocommerce' ); ?>"><?php esc_html_e( 'Delete', 'woocommerce' ); ?></a></li>
							</ul>
							<?php
							// Allow for extra info to be exposed or extra action to be executed for this attachment.
							do_action( 'woocommerce_admin_after_product_gallery_item', $thepostid, $attachment_id );
							?>
						</li>
						<?php

						// rebuild ids to be saved.
						$updated_gallery_ids[] = $attachment_id;
					}

					// need to update product meta to set new gallery ids
					if ( $update_meta ) {
						update_post_meta( $post->ID, '_product_image_gallery', implode( ',', $updated_gallery_ids ) );
					}
				}
				?>
			</ul>

			<input type="hidden" id="product_image_gallery" name="product_image_gallery" value="<?php echo esc_attr( implode( ',', $updated_gallery_ids ) ); ?>" />

		</div>
		<p class="add_product_images hide-if-no-js">
			<a href="#" data-choose="<?php esc_attr_e( 'Add images to product gallery', 'woocommerce' ); ?>" data-update="<?php esc_attr_e( 'Add to gallery', 'woocommerce' ); ?>" data-delete="<?php esc_attr_e( 'Delete image', 'woocommerce' ); ?>" data-text="<?php esc_attr_e( 'Delete', 'woocommerce' ); ?>"><?php esc_html_e( 'Add product gallery images', 'woocommerce' ); ?></a>
		</p>
		<?php
	}

	/**
	 * Save meta box data.
	 *
	 * @param int     $post_id
	 * @param WP_Post $post
	 */
	public static function save( $post_id, $post ) {
		$product_type   = empty( $_POST['product-type'] ) ? WC_Product_Factory::get_product_type( $post_id ) : sanitize_title( stripslashes( $_POST['product-type'] ) );
		$classname      = WC_Product_Factory::get_product_classname( $post_id, $product_type ? $product_type : 'simple' );
		$product        = new $classname( $post_id );
		$attachment_ids = isset( $_POST['product_image_gallery'] ) ? array_filter( explode( ',', wc_clean( $_POST['product_image_gallery'] ) ) ) : array();

		$product->set_gallery_image_ids( $attachment_ids );
		$product->save();
	}
}
admin/meta-boxes/class-wc-meta-box-order-notes.php000064400000002527151336044770016116 0ustar00<?php
/**
 * Order Notes
 *
 * @package WooCommerce\Admin\Meta Boxes
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC_Meta_Box_Order_Notes Class.
 */
class WC_Meta_Box_Order_Notes {

	/**
	 * Output the metabox.
	 *
	 * @param WP_Post $post Post object.
	 */
	public static function output( $post ) {
		global $post;

		$args = array(
			'order_id' => $post->ID,
		);

		$notes = wc_get_order_notes( $args );

		include __DIR__ . '/views/html-order-notes.php';
		?>
		<div class="add_note">
			<p>
				<label for="add_order_note"><?php esc_html_e( 'Add note', 'woocommerce' ); ?> <?php echo wc_help_tip( __( 'Add a note for your reference, or add a customer note (the user will be notified).', 'woocommerce' ) ); ?></label>
				<textarea type="text" name="order_note" id="add_order_note" class="input-text" cols="20" rows="5"></textarea>
			</p>
			<p>
				<label for="order_note_type" class="screen-reader-text"><?php esc_html_e( 'Note type', 'woocommerce' ); ?></label>
				<select name="order_note_type" id="order_note_type">
					<option value=""><?php esc_html_e( 'Private note', 'woocommerce' ); ?></option>
					<option value="customer"><?php esc_html_e( 'Note to customer', 'woocommerce' ); ?></option>
				</select>
				<button type="button" class="add_note button"><?php esc_html_e( 'Add', 'woocommerce' ); ?></button>
			</p>
		</div>
		<?php
	}
}
admin/meta-boxes/class-wc-meta-box-product-short-description.php000064400000002114151336044770021003 0ustar00<?php
/**
 * Product Short Description
 *
 * Replaces the standard excerpt box.
 *
 * @package     WooCommerce\Admin\Meta Boxes
 * @version     2.1.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * WC_Meta_Box_Product_Short_Description Class.
 */
class WC_Meta_Box_Product_Short_Description {

	/**
	 * Output the metabox.
	 *
	 * @param WP_Post $post Post object.
	 */
	public static function output( $post ) {

		$settings = array(
			'textarea_name' => 'excerpt',
			'quicktags'     => array( 'buttons' => 'em,strong,link' ),
			'tinymce'       => array(
				'theme_advanced_buttons1' => 'bold,italic,strikethrough,separator,bullist,numlist,separator,blockquote,separator,justifyleft,justifycenter,justifyright,separator,link,unlink,separator,undo,redo,separator',
				'theme_advanced_buttons2' => '',
			),
			'editor_css'    => '<style>#wp-excerpt-editor-container .wp-editor-area{height:175px; width:100%;}</style>',
		);

		wp_editor( htmlspecialchars_decode( $post->post_excerpt, ENT_QUOTES ), 'excerpt', apply_filters( 'woocommerce_product_short_description_editor_settings', $settings ) );
	}
}
admin/meta-boxes/class-wc-meta-box-order-downloads.php000064400000006062151336044770016756 0ustar00<?php
/**
 * Order Downloads
 *
 * @author      WooThemes
 * @category    Admin
 * @package     WooCommerce\Admin\Meta Boxes
 * @version     2.1.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

/**
 * WC_Meta_Box_Order_Downloads Class.
 */
class WC_Meta_Box_Order_Downloads {

	/**
	 * Output the metabox.
	 *
	 * @param WP_Post $post
	 */
	public static function output( $post ) {
		?>
		<div class="order_download_permissions wc-metaboxes-wrapper">

			<div class="wc-metaboxes">
				<?php
				$data_store           = WC_Data_Store::load( 'customer-download' );
				$download_permissions = $data_store->get_downloads(
					array(
						'order_id' => $post->ID,
						'orderby'  => 'product_id',
					)
				);

				$product      = null;
				$loop         = 0;
				$file_counter = 1;

				if ( $download_permissions && sizeof( $download_permissions ) > 0 ) {
					foreach ( $download_permissions as $download ) {
						if ( ! $product || $product->get_id() !== $download->get_product_id() ) {
							$product      = wc_get_product( $download->get_product_id() );
							$file_counter = 1;
						}

						// don't show permissions to files that have since been removed.
						if ( ! $product || ! $product->exists() || ! $product->has_file( $download->get_download_id() ) ) {
							continue;
						}

						// Show file title instead of count if set.
						$file       = $product->get_file( $download->get_download_id() );
						$file_count = isset( $file['name'] ) ? $file['name'] : sprintf( __( 'File %d', 'woocommerce' ), $file_counter );

						include __DIR__ . '/views/html-order-download-permission.php';

						$loop++;
						$file_counter++;
					}
				}
				?>
			</div>

			<div class="toolbar">
				<p class="buttons">
					<select id="grant_access_id" class="wc-product-search" name="grant_access_id[]" multiple="multiple" style="width: 400px;" data-placeholder="<?php esc_attr_e( 'Search for a downloadable product&hellip;', 'woocommerce' ); ?>" data-action="woocommerce_json_search_downloadable_products_and_variations"></select>
					<button type="button" class="button grant_access">
						<?php _e( 'Grant access', 'woocommerce' ); ?>
					</button>
				</p>
				<div class="clear"></div>
			</div>

		</div>
		<?php
	}

	/**
	 * Save meta box data.
	 *
	 * @param int     $post_id
	 * @param WP_Post $post
	 */
	public static function save( $post_id, $post ) {
		if ( isset( $_POST['permission_id'] ) ) {
			$permission_ids      = $_POST['permission_id'];
			$downloads_remaining = $_POST['downloads_remaining'];
			$access_expires      = $_POST['access_expires'];
			$max                 = max( array_keys( $permission_ids ) );

			for ( $i = 0; $i <= $max; $i ++ ) {
				if ( ! isset( $permission_ids[ $i ] ) ) {
					continue;
				}
				$download = new WC_Customer_Download( $permission_ids[ $i ] );
				$download->set_downloads_remaining( wc_clean( $downloads_remaining[ $i ] ) );
				$download->set_access_expires( array_key_exists( $i, $access_expires ) && '' !== $access_expires[ $i ] ? strtotime( $access_expires[ $i ] ) : '' );
				$download->save();
			}
		}
	}
}
class-wc-cli.php000064400000002246151336044770007554 0ustar00<?php
/**
 * Enables WooCommerce, via the the command line.
 *
 * @package WooCommerce\CLI
 * @version 3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * CLI class.
 */
class WC_CLI {
	/**
	 * Load required files and hooks to make the CLI work.
	 */
	public function __construct() {
		$this->includes();
		$this->hooks();
	}

	/**
	 * Load command files.
	 */
	private function includes() {
		require_once dirname( __FILE__ ) . '/cli/class-wc-cli-runner.php';
		require_once dirname( __FILE__ ) . '/cli/class-wc-cli-rest-command.php';
		require_once dirname( __FILE__ ) . '/cli/class-wc-cli-tool-command.php';
		require_once dirname( __FILE__ ) . '/cli/class-wc-cli-update-command.php';
		require_once dirname( __FILE__ ) . '/cli/class-wc-cli-tracker-command.php';
	}

	/**
	 * Sets up and hooks WP CLI to our CLI code.
	 */
	private function hooks() {
		WP_CLI::add_hook( 'after_wp_load', 'WC_CLI_Runner::after_wp_load' );
		WP_CLI::add_hook( 'after_wp_load', 'WC_CLI_Tool_Command::register_commands' );
		WP_CLI::add_hook( 'after_wp_load', 'WC_CLI_Update_Command::register_commands' );
		WP_CLI::add_hook( 'after_wp_load', 'WC_CLI_Tracker_Command::register_commands' );
	}
}

new WC_CLI();
class-wc-checkout.php000064400000130007151336044770010607 0ustar00<?php
/**
 * Checkout functionality
 *
 * The WooCommerce checkout class handles the checkout process, collecting user data and processing the payment.
 *
 * @package WooCommerce\Classes
 * @version 3.4.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Checkout class.
 */
class WC_Checkout {

	/**
	 * The single instance of the class.
	 *
	 * @var WC_Checkout|null
	 */
	protected static $instance = null;

	/**
	 * Checkout fields are stored here.
	 *
	 * @var array|null
	 */
	protected $fields = null;

	/**
	 * Holds posted data for backwards compatibility.
	 *
	 * @var array
	 */
	protected $legacy_posted_data = array();

	/**
	 * Caches customer object. @see get_value.
	 *
	 * @var WC_Customer
	 */
	private $logged_in_customer = null;

	/**
	 * Gets the main WC_Checkout Instance.
	 *
	 * @since 2.1
	 * @static
	 * @return WC_Checkout Main instance
	 */
	public static function instance() {
		if ( is_null( self::$instance ) ) {
			self::$instance = new self();

			// Hook in actions once.
			add_action( 'woocommerce_checkout_billing', array( self::$instance, 'checkout_form_billing' ) );
			add_action( 'woocommerce_checkout_shipping', array( self::$instance, 'checkout_form_shipping' ) );

			// woocommerce_checkout_init action is ran once when the class is first constructed.
			do_action( 'woocommerce_checkout_init', self::$instance );
		}
		return self::$instance;
	}

	/**
	 * See if variable is set. Used to support legacy public variables which are no longer defined.
	 *
	 * @param string $key Key.
	 * @return bool
	 */
	public function __isset( $key ) {
		return in_array(
			$key,
			array(
				'enable_signup',
				'enable_guest_checkout',
				'must_create_account',
				'checkout_fields',
				'posted',
				'shipping_method',
				'payment_method',
				'customer_id',
				'shipping_methods',
			),
			true
		);
	}

	/**
	 * Sets the legacy public variables for backwards compatibility.
	 *
	 * @param string $key   Key.
	 * @param mixed  $value Value.
	 */
	public function __set( $key, $value ) {
		switch ( $key ) {
			case 'enable_signup':
				$bool_value = wc_string_to_bool( $value );

				if ( $bool_value !== $this->is_registration_enabled() ) {
					remove_filter( 'woocommerce_checkout_registration_enabled', '__return_true', 0 );
					remove_filter( 'woocommerce_checkout_registration_enabled', '__return_false', 0 );
					add_filter( 'woocommerce_checkout_registration_enabled', $bool_value ? '__return_true' : '__return_false', 0 );
				}
				break;
			case 'enable_guest_checkout':
				$bool_value = wc_string_to_bool( $value );

				if ( $bool_value === $this->is_registration_required() ) {
					remove_filter( 'woocommerce_checkout_registration_required', '__return_true', 0 );
					remove_filter( 'woocommerce_checkout_registration_required', '__return_false', 0 );
					add_filter( 'woocommerce_checkout_registration_required', $bool_value ? '__return_false' : '__return_true', 0 );
				}
				break;
			case 'checkout_fields':
				$this->fields = $value;
				break;
			case 'shipping_methods':
				WC()->session->set( 'chosen_shipping_methods', $value );
				break;
			case 'posted':
				$this->legacy_posted_data = $value;
				break;
		}
	}

	/**
	 * Gets the legacy public variables for backwards compatibility.
	 *
	 * @param string $key Key.
	 * @return array|string
	 */
	public function __get( $key ) {
		if ( in_array( $key, array( 'posted', 'shipping_method', 'payment_method' ), true ) && empty( $this->legacy_posted_data ) ) {
			$this->legacy_posted_data = $this->get_posted_data();
		}

		switch ( $key ) {
			case 'enable_signup':
				return $this->is_registration_enabled();
			case 'enable_guest_checkout':
				return ! $this->is_registration_required();
			case 'must_create_account':
				return $this->is_registration_required() && ! is_user_logged_in();
			case 'checkout_fields':
				return $this->get_checkout_fields();
			case 'posted':
				wc_doing_it_wrong( 'WC_Checkout->posted', 'Use $_POST directly.', '3.0.0' );
				return $this->legacy_posted_data;
			case 'shipping_method':
				return $this->legacy_posted_data['shipping_method'];
			case 'payment_method':
				return $this->legacy_posted_data['payment_method'];
			case 'customer_id':
				return apply_filters( 'woocommerce_checkout_customer_id', get_current_user_id() );
			case 'shipping_methods':
				return (array) WC()->session->get( 'chosen_shipping_methods' );
		}
	}

	/**
	 * Cloning is forbidden.
	 */
	public function __clone() {
		wc_doing_it_wrong( __FUNCTION__, __( 'Cloning is forbidden.', 'woocommerce' ), '2.1' );
	}

	/**
	 * Unserializing instances of this class is forbidden.
	 */
	public function __wakeup() {
		wc_doing_it_wrong( __FUNCTION__, __( 'Unserializing instances of this class is forbidden.', 'woocommerce' ), '2.1' );
	}

	/**
	 * Is registration required to checkout?
	 *
	 * @since  3.0.0
	 * @return boolean
	 */
	public function is_registration_required() {
		return apply_filters( 'woocommerce_checkout_registration_required', 'yes' !== get_option( 'woocommerce_enable_guest_checkout' ) );
	}

	/**
	 * Is registration enabled on the checkout page?
	 *
	 * @since  3.0.0
	 * @return boolean
	 */
	public function is_registration_enabled() {
		return apply_filters( 'woocommerce_checkout_registration_enabled', 'yes' === get_option( 'woocommerce_enable_signup_and_login_from_checkout' ) );
	}

	/**
	 * Get an array of checkout fields.
	 *
	 * @param  string $fieldset to get.
	 * @return array
	 */
	public function get_checkout_fields( $fieldset = '' ) {
		if ( ! is_null( $this->fields ) ) {
			return $fieldset ? $this->fields[ $fieldset ] : $this->fields;
		}

		// Fields are based on billing/shipping country. Grab those values but ensure they are valid for the store before using.
		$billing_country   = $this->get_value( 'billing_country' );
		$billing_country   = empty( $billing_country ) ? WC()->countries->get_base_country() : $billing_country;
		$allowed_countries = WC()->countries->get_allowed_countries();

		if ( ! array_key_exists( $billing_country, $allowed_countries ) ) {
			$billing_country = current( array_keys( $allowed_countries ) );
		}

		$shipping_country  = $this->get_value( 'shipping_country' );
		$shipping_country  = empty( $shipping_country ) ? WC()->countries->get_base_country() : $shipping_country;
		$allowed_countries = WC()->countries->get_shipping_countries();

		if ( ! array_key_exists( $shipping_country, $allowed_countries ) ) {
			$shipping_country = current( array_keys( $allowed_countries ) );
		}

		$this->fields = array(
			'billing'  => WC()->countries->get_address_fields(
				$billing_country,
				'billing_'
			),
			'shipping' => WC()->countries->get_address_fields(
				$shipping_country,
				'shipping_'
			),
			'account'  => array(),
			'order'    => array(
				'order_comments' => array(
					'type'        => 'textarea',
					'class'       => array( 'notes' ),
					'label'       => __( 'Order notes', 'woocommerce' ),
					'placeholder' => esc_attr__(
						'Notes about your order, e.g. special notes for delivery.',
						'woocommerce'
					),
				),
			),
		);

		if ( 'no' === get_option( 'woocommerce_registration_generate_username' ) ) {
			$this->fields['account']['account_username'] = array(
				'type'        => 'text',
				'label'       => __( 'Account username', 'woocommerce' ),
				'required'    => true,
				'placeholder' => esc_attr__( 'Username', 'woocommerce' ),
			);
		}

		if ( 'no' === get_option( 'woocommerce_registration_generate_password' ) ) {
			$this->fields['account']['account_password'] = array(
				'type'        => 'password',
				'label'       => __( 'Create account password', 'woocommerce' ),
				'required'    => true,
				'placeholder' => esc_attr__( 'Password', 'woocommerce' ),
			);
		}
		$this->fields = apply_filters( 'woocommerce_checkout_fields', $this->fields );

		foreach ( $this->fields as $field_type => $fields ) {
			// Sort each of the checkout field sections based on priority.
			uasort( $this->fields[ $field_type ], 'wc_checkout_fields_uasort_comparison' );

			// Add accessibility labels to fields that have placeholders.
			foreach ( $fields as $single_field_type => $field ) {
				if ( empty( $field['label'] ) && ! empty( $field['placeholder'] ) ) {
					$this->fields[ $field_type ][ $single_field_type ]['label']       = $field['placeholder'];
					$this->fields[ $field_type ][ $single_field_type ]['label_class'] = array( 'screen-reader-text' );
				}
			}
		}

		return $fieldset ? $this->fields[ $fieldset ] : $this->fields;
	}

	/**
	 * When we process the checkout, lets ensure cart items are rechecked to prevent checkout.
	 */
	public function check_cart_items() {
		do_action( 'woocommerce_check_cart_items' );
	}

	/**
	 * Output the billing form.
	 */
	public function checkout_form_billing() {
		wc_get_template( 'checkout/form-billing.php', array( 'checkout' => $this ) );
	}

	/**
	 * Output the shipping form.
	 */
	public function checkout_form_shipping() {
		wc_get_template( 'checkout/form-shipping.php', array( 'checkout' => $this ) );
	}

	/**
	 * Create an order. Error codes:
	 *      520 - Cannot insert order into the database.
	 *      521 - Cannot get order after creation.
	 *      522 - Cannot update order.
	 *      525 - Cannot create line item.
	 *      526 - Cannot create fee item.
	 *      527 - Cannot create shipping item.
	 *      528 - Cannot create tax item.
	 *      529 - Cannot create coupon item.
	 *
	 * @throws Exception When checkout validation fails.
	 * @param  array $data Posted data.
	 * @return int|WP_ERROR
	 */
	public function create_order( $data ) {
		// Give plugins the opportunity to create an order themselves.
		$order_id = apply_filters( 'woocommerce_create_order', null, $this );
		if ( $order_id ) {
			return $order_id;
		}

		try {
			$order_id           = absint( WC()->session->get( 'order_awaiting_payment' ) );
			$cart_hash          = WC()->cart->get_cart_hash();
			$available_gateways = WC()->payment_gateways->get_available_payment_gateways();
			$order              = $order_id ? wc_get_order( $order_id ) : null;

			/**
			 * If there is an order pending payment, we can resume it here so
			 * long as it has not changed. If the order has changed, i.e.
			 * different items or cost, create a new order. We use a hash to
			 * detect changes which is based on cart items + order total.
			 */
			if ( $order && $order->has_cart_hash( $cart_hash ) && $order->has_status( array( 'pending', 'failed' ) ) ) {
				// Action for 3rd parties.
				do_action( 'woocommerce_resume_order', $order_id );

				// Remove all items - we will re-add them later.
				$order->remove_order_items();
			} else {
				$order = new WC_Order();
			}

			$fields_prefix = array(
				'shipping' => true,
				'billing'  => true,
			);

			$shipping_fields = array(
				'shipping_method' => true,
				'shipping_total'  => true,
				'shipping_tax'    => true,
			);
			foreach ( $data as $key => $value ) {
				if ( is_callable( array( $order, "set_{$key}" ) ) ) {
					$order->{"set_{$key}"}( $value );
					// Store custom fields prefixed with wither shipping_ or billing_. This is for backwards compatibility with 2.6.x.
				} elseif ( isset( $fields_prefix[ current( explode( '_', $key ) ) ] ) ) {
					if ( ! isset( $shipping_fields[ $key ] ) ) {
						$order->update_meta_data( '_' . $key, $value );
					}
				}
			}

			$order->hold_applied_coupons( $data['billing_email'] );
			$order->set_created_via( 'checkout' );
			$order->set_cart_hash( $cart_hash );
			$order->set_customer_id( apply_filters( 'woocommerce_checkout_customer_id', get_current_user_id() ) );
			$order->set_currency( get_woocommerce_currency() );
			$order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) );
			$order->set_customer_ip_address( WC_Geolocation::get_ip_address() );
			$order->set_customer_user_agent( wc_get_user_agent() );
			$order->set_customer_note( isset( $data['order_comments'] ) ? $data['order_comments'] : '' );
			$order->set_payment_method( isset( $available_gateways[ $data['payment_method'] ] ) ? $available_gateways[ $data['payment_method'] ] : $data['payment_method'] );
			$this->set_data_from_cart( $order );

			/**
			 * Action hook to adjust order before save.
			 *
			 * @since 3.0.0
			 */
			do_action( 'woocommerce_checkout_create_order', $order, $data );

			// Save the order.
			$order_id = $order->save();

			/**
			 * Action hook fired after an order is created used to add custom meta to the order.
			 *
			 * @since 3.0.0
			 */
			do_action( 'woocommerce_checkout_update_order_meta', $order_id, $data );

			/**
			 * Action hook fired after an order is created.
			 *
			 * @since 4.3.0
			 */
			do_action( 'woocommerce_checkout_order_created', $order );

			return $order_id;
		} catch ( Exception $e ) {
			if ( $order && $order instanceof WC_Order ) {
				$order->get_data_store()->release_held_coupons( $order );
				/**
				 * Action hook fired when an order is discarded due to Exception.
				 *
				 * @since 4.3.0
				 */
				do_action( 'woocommerce_checkout_order_exception', $order );
			}
			return new WP_Error( 'checkout-error', $e->getMessage() );
		}
	}

	/**
	 * Copy line items, tax, totals data from cart to order.
	 *
	 * @param WC_Order $order Order object.
	 *
	 * @throws Exception When unable to create order.
	 */
	public function set_data_from_cart( &$order ) {
		$order_vat_exempt = WC()->cart->get_customer()->get_is_vat_exempt() ? 'yes' : 'no';
		$order->add_meta_data( 'is_vat_exempt', $order_vat_exempt, true );
		$order->set_shipping_total( WC()->cart->get_shipping_total() );
		$order->set_discount_total( WC()->cart->get_discount_total() );
		$order->set_discount_tax( WC()->cart->get_discount_tax() );
		$order->set_cart_tax( WC()->cart->get_cart_contents_tax() + WC()->cart->get_fee_tax() );
		$order->set_shipping_tax( WC()->cart->get_shipping_tax() );
		$order->set_total( WC()->cart->get_total( 'edit' ) );
		$this->create_order_line_items( $order, WC()->cart );
		$this->create_order_fee_lines( $order, WC()->cart );
		$this->create_order_shipping_lines( $order, WC()->session->get( 'chosen_shipping_methods' ), WC()->shipping()->get_packages() );
		$this->create_order_tax_lines( $order, WC()->cart );
		$this->create_order_coupon_lines( $order, WC()->cart );
	}
	/**
	 * Add line items to the order.
	 *
	 * @param WC_Order $order Order instance.
	 * @param WC_Cart  $cart  Cart instance.
	 */
	public function create_order_line_items( &$order, $cart ) {
		foreach ( $cart->get_cart() as $cart_item_key => $values ) {
			/**
			 * Filter hook to get initial item object.
			 *
			 * @since 3.1.0
			 */
			$item                       = apply_filters( 'woocommerce_checkout_create_order_line_item_object', new WC_Order_Item_Product(), $cart_item_key, $values, $order );
			$product                    = $values['data'];
			$item->legacy_values        = $values; // @deprecated 4.4.0 For legacy actions.
			$item->legacy_cart_item_key = $cart_item_key; // @deprecated 4.4.0 For legacy actions.
			$item->set_props(
				array(
					'quantity'     => $values['quantity'],
					'variation'    => $values['variation'],
					'subtotal'     => $values['line_subtotal'],
					'total'        => $values['line_total'],
					'subtotal_tax' => $values['line_subtotal_tax'],
					'total_tax'    => $values['line_tax'],
					'taxes'        => $values['line_tax_data'],
				)
			);

			if ( $product ) {
				$item->set_props(
					array(
						'name'         => $product->get_name(),
						'tax_class'    => $product->get_tax_class(),
						'product_id'   => $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id(),
						'variation_id' => $product->is_type( 'variation' ) ? $product->get_id() : 0,
					)
				);
			}

			$item->set_backorder_meta();

			/**
			 * Action hook to adjust item before save.
			 *
			 * @since 3.0.0
			 */
			do_action( 'woocommerce_checkout_create_order_line_item', $item, $cart_item_key, $values, $order );

			// Add item to order and save.
			$order->add_item( $item );
		}
	}

	/**
	 * Add fees to the order.
	 *
	 * @param WC_Order $order Order instance.
	 * @param WC_Cart  $cart  Cart instance.
	 */
	public function create_order_fee_lines( &$order, $cart ) {
		foreach ( $cart->get_fees() as $fee_key => $fee ) {
			$item                 = new WC_Order_Item_Fee();
			$item->legacy_fee     = $fee; // @deprecated 4.4.0 For legacy actions.
			$item->legacy_fee_key = $fee_key; // @deprecated 4.4.0 For legacy actions.
			$item->set_props(
				array(
					'name'      => $fee->name,
					'tax_class' => $fee->taxable ? $fee->tax_class : 0,
					'amount'    => $fee->amount,
					'total'     => $fee->total,
					'total_tax' => $fee->tax,
					'taxes'     => array(
						'total' => $fee->tax_data,
					),
				)
			);

			/**
			 * Action hook to adjust item before save.
			 *
			 * @since 3.0.0
			 */
			do_action( 'woocommerce_checkout_create_order_fee_item', $item, $fee_key, $fee, $order );

			// Add item to order and save.
			$order->add_item( $item );
		}
	}

	/**
	 * Add shipping lines to the order.
	 *
	 * @param WC_Order $order                   Order Instance.
	 * @param array    $chosen_shipping_methods Chosen shipping methods.
	 * @param array    $packages                Packages.
	 */
	public function create_order_shipping_lines( &$order, $chosen_shipping_methods, $packages ) {
		foreach ( $packages as $package_key => $package ) {
			if ( isset( $chosen_shipping_methods[ $package_key ], $package['rates'][ $chosen_shipping_methods[ $package_key ] ] ) ) {
				$shipping_rate            = $package['rates'][ $chosen_shipping_methods[ $package_key ] ];
				$item                     = new WC_Order_Item_Shipping();
				$item->legacy_package_key = $package_key; // @deprecated 4.4.0 For legacy actions.
				$item->set_props(
					array(
						'method_title' => $shipping_rate->label,
						'method_id'    => $shipping_rate->method_id,
						'instance_id'  => $shipping_rate->instance_id,
						'total'        => wc_format_decimal( $shipping_rate->cost ),
						'taxes'        => array(
							'total' => $shipping_rate->taxes,
						),
					)
				);

				foreach ( $shipping_rate->get_meta_data() as $key => $value ) {
					$item->add_meta_data( $key, $value, true );
				}

				/**
				 * Action hook to adjust item before save.
				 *
				 * @since 3.0.0
				 */
				do_action( 'woocommerce_checkout_create_order_shipping_item', $item, $package_key, $package, $order );

				// Add item to order and save.
				$order->add_item( $item );
			}
		}
	}

	/**
	 * Add tax lines to the order.
	 *
	 * @param WC_Order $order Order instance.
	 * @param WC_Cart  $cart  Cart instance.
	 */
	public function create_order_tax_lines( &$order, $cart ) {
		foreach ( array_keys( $cart->get_cart_contents_taxes() + $cart->get_shipping_taxes() + $cart->get_fee_taxes() ) as $tax_rate_id ) {
			if ( $tax_rate_id && apply_filters( 'woocommerce_cart_remove_taxes_zero_rate_id', 'zero-rated' ) !== $tax_rate_id ) {
				$item = new WC_Order_Item_Tax();
				$item->set_props(
					array(
						'rate_id'            => $tax_rate_id,
						'tax_total'          => $cart->get_tax_amount( $tax_rate_id ),
						'shipping_tax_total' => $cart->get_shipping_tax_amount( $tax_rate_id ),
						'rate_code'          => WC_Tax::get_rate_code( $tax_rate_id ),
						'label'              => WC_Tax::get_rate_label( $tax_rate_id ),
						'compound'           => WC_Tax::is_compound( $tax_rate_id ),
						'rate_percent'       => WC_Tax::get_rate_percent_value( $tax_rate_id ),
					)
				);

				/**
				 * Action hook to adjust item before save.
				 *
				 * @since 3.0.0
				 */
				do_action( 'woocommerce_checkout_create_order_tax_item', $item, $tax_rate_id, $order );

				// Add item to order and save.
				$order->add_item( $item );
			}
		}
	}

	/**
	 * Add coupon lines to the order.
	 *
	 * @param WC_Order $order Order instance.
	 * @param WC_Cart  $cart  Cart instance.
	 */
	public function create_order_coupon_lines( &$order, $cart ) {
		foreach ( $cart->get_coupons() as $code => $coupon ) {
			$item = new WC_Order_Item_Coupon();
			$item->set_props(
				array(
					'code'         => $code,
					'discount'     => $cart->get_coupon_discount_amount( $code ),
					'discount_tax' => $cart->get_coupon_discount_tax_amount( $code ),
				)
			);

			// Avoid storing used_by - it's not needed and can get large.
			$coupon_data = $coupon->get_data();
			unset( $coupon_data['used_by'] );
			$item->add_meta_data( 'coupon_data', $coupon_data );

			/**
			 * Action hook to adjust item before save.
			 *
			 * @since 3.0.0
			 */
			do_action( 'woocommerce_checkout_create_order_coupon_item', $item, $code, $coupon, $order );

			// Add item to order and save.
			$order->add_item( $item );
		}
	}

	/**
	 * See if a fieldset should be skipped.
	 *
	 * @since 3.0.0
	 * @param string $fieldset_key Fieldset key.
	 * @param array  $data         Posted data.
	 * @return bool
	 */
	protected function maybe_skip_fieldset( $fieldset_key, $data ) {
		if ( 'shipping' === $fieldset_key && ( ! $data['ship_to_different_address'] || ! WC()->cart->needs_shipping_address() ) ) {
			return true;
		}

		if ( 'account' === $fieldset_key && ( is_user_logged_in() || ( ! $this->is_registration_required() && empty( $data['createaccount'] ) ) ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Get posted data from the checkout form.
	 *
	 * @since  3.1.0
	 * @return array of data.
	 */
	public function get_posted_data() {
		// phpcs:disable WordPress.Security.NonceVerification.Missing
		$data = array(
			'terms'                              => (int) isset( $_POST['terms'] ),
			'createaccount'                      => (int) ( $this->is_registration_enabled() ? ! empty( $_POST['createaccount'] ) : false ),
			'payment_method'                     => isset( $_POST['payment_method'] ) ? wc_clean( wp_unslash( $_POST['payment_method'] ) ) : '',
			'shipping_method'                    => isset( $_POST['shipping_method'] ) ? wc_clean( wp_unslash( $_POST['shipping_method'] ) ) : '',
			'ship_to_different_address'          => ! empty( $_POST['ship_to_different_address'] ) && ! wc_ship_to_billing_address_only(),
			'woocommerce_checkout_update_totals' => isset( $_POST['woocommerce_checkout_update_totals'] ),
		);
		// phpcs:enable WordPress.Security.NonceVerification.Missing

		$skipped = array();
		$form_was_shown = isset( $_POST['woocommerce-process-checkout-nonce'] ); // phpcs:disable WordPress.Security.NonceVerification.Missing

		foreach ( $this->get_checkout_fields() as $fieldset_key => $fieldset ) {
			if ( $this->maybe_skip_fieldset( $fieldset_key, $data ) ) {
				$skipped[] = $fieldset_key;
				continue;
			}

			foreach ( $fieldset as $key => $field ) {
				$type = sanitize_title( isset( $field['type'] ) ? $field['type'] : 'text' );

				if ( isset( $_POST[ $key ] ) && '' !== $_POST[ $key ] ) { // phpcs:disable WordPress.Security.NonceVerification.Missing
					$value = wp_unslash( $_POST[ $key ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
				} elseif ( isset( $field['default'] ) && 'checkbox' !== $type && ! $form_was_shown ) {
					$value = $field['default'];
				} else {
					$value = '';
				}

				if ( '' !== $value ) {
					switch ( $type ) {
						case 'checkbox':
							$value = 1;
							break;
						case 'multiselect':
							$value = implode( ', ', wc_clean( $value ) );
							break;
						case 'textarea':
							$value = wc_sanitize_textarea( $value );
							break;
						case 'password':
							break;
						default:
							$value = wc_clean( $value );
							break;
					}
				}

				$data[ $key ] = apply_filters( 'woocommerce_process_checkout_' . $type . '_field', apply_filters( 'woocommerce_process_checkout_field_' . $key, $value ) );
			}
		}

		if ( in_array( 'shipping', $skipped, true ) && ( WC()->cart->needs_shipping_address() || wc_ship_to_billing_address_only() ) ) {
			foreach ( $this->get_checkout_fields( 'shipping' ) as $key => $field ) {
				$data[ $key ] = isset( $data[ 'billing_' . substr( $key, 9 ) ] ) ? $data[ 'billing_' . substr( $key, 9 ) ] : '';
			}
		}

		// BW compatibility.
		$this->legacy_posted_data = $data;

		return apply_filters( 'woocommerce_checkout_posted_data', $data );
	}

	/**
	 * Validates the posted checkout data based on field properties.
	 *
	 * @since  3.0.0
	 * @param  array    $data   An array of posted data.
	 * @param  WP_Error $errors Validation error.
	 */
	protected function validate_posted_data( &$data, &$errors ) {
		foreach ( $this->get_checkout_fields() as $fieldset_key => $fieldset ) {
			$validate_fieldset = true;
			if ( $this->maybe_skip_fieldset( $fieldset_key, $data ) ) {
				$validate_fieldset = false;
			}

			foreach ( $fieldset as $key => $field ) {
				if ( ! isset( $data[ $key ] ) ) {
					continue;
				}
				$required    = ! empty( $field['required'] );
				$format      = array_filter( isset( $field['validate'] ) ? (array) $field['validate'] : array() );
				$field_label = isset( $field['label'] ) ? $field['label'] : '';

				if ( $validate_fieldset &&
					( isset( $field['type'] ) && 'country' === $field['type'] && '' !== $data[ $key ] ) &&
					! WC()->countries->country_exists( $data[ $key ] ) ) {
						/* translators: ISO 3166-1 alpha-2 country code */
						$errors->add( $key . '_validation', sprintf( __( "'%s' is not a valid country code.", 'woocommerce' ), $data[ $key ] ) );
				}

				switch ( $fieldset_key ) {
					case 'shipping':
						/* translators: %s: field name */
						$field_label = sprintf( _x( 'Shipping %s', 'checkout-validation', 'woocommerce' ), $field_label );
						break;
					case 'billing':
						/* translators: %s: field name */
						$field_label = sprintf( _x( 'Billing %s', 'checkout-validation', 'woocommerce' ), $field_label );
						break;
				}

				if ( in_array( 'postcode', $format, true ) ) {
					$country      = isset( $data[ $fieldset_key . '_country' ] ) ? $data[ $fieldset_key . '_country' ] : WC()->customer->{"get_{$fieldset_key}_country"}();
					$data[ $key ] = wc_format_postcode( $data[ $key ], $country );

					if ( $validate_fieldset && '' !== $data[ $key ] && ! WC_Validation::is_postcode( $data[ $key ], $country ) ) {
						switch ( $country ) {
							case 'IE':
								/* translators: %1$s: field name, %2$s finder.eircode.ie URL */
								$postcode_validation_notice = sprintf( __( '%1$s is not valid. You can look up the correct Eircode <a target="_blank" href="%2$s">here</a>.', 'woocommerce' ), '<strong>' . esc_html( $field_label ) . '</strong>', 'https://finder.eircode.ie' );
								break;
							default:
								/* translators: %s: field name */
								$postcode_validation_notice = sprintf( __( '%s is not a valid postcode / ZIP.', 'woocommerce' ), '<strong>' . esc_html( $field_label ) . '</strong>' );
						}
						$errors->add( $key . '_validation', apply_filters( 'woocommerce_checkout_postcode_validation_notice', $postcode_validation_notice, $country, $data[ $key ] ), array( 'id' => $key ) );
					}
				}

				if ( in_array( 'phone', $format, true ) ) {
					if ( $validate_fieldset && '' !== $data[ $key ] && ! WC_Validation::is_phone( $data[ $key ] ) ) {
						/* translators: %s: phone number */
						$errors->add( $key . '_validation', sprintf( __( '%s is not a valid phone number.', 'woocommerce' ), '<strong>' . esc_html( $field_label ) . '</strong>' ), array( 'id' => $key ) );
					}
				}

				if ( in_array( 'email', $format, true ) && '' !== $data[ $key ] ) {
					$email_is_valid = is_email( $data[ $key ] );
					$data[ $key ]   = sanitize_email( $data[ $key ] );

					if ( $validate_fieldset && ! $email_is_valid ) {
						/* translators: %s: email address */
						$errors->add( $key . '_validation', sprintf( __( '%s is not a valid email address.', 'woocommerce' ), '<strong>' . esc_html( $field_label ) . '</strong>' ), array( 'id' => $key ) );
						continue;
					}
				}

				if ( '' !== $data[ $key ] && in_array( 'state', $format, true ) ) {
					$country      = isset( $data[ $fieldset_key . '_country' ] ) ? $data[ $fieldset_key . '_country' ] : WC()->customer->{"get_{$fieldset_key}_country"}();
					$valid_states = WC()->countries->get_states( $country );

					if ( ! empty( $valid_states ) && is_array( $valid_states ) && count( $valid_states ) > 0 ) {
						$valid_state_values = array_map( 'wc_strtoupper', array_flip( array_map( 'wc_strtoupper', $valid_states ) ) );
						$data[ $key ]       = wc_strtoupper( $data[ $key ] );

						if ( isset( $valid_state_values[ $data[ $key ] ] ) ) {
							// With this part we consider state value to be valid as well, convert it to the state key for the valid_states check below.
							$data[ $key ] = $valid_state_values[ $data[ $key ] ];
						}

						if ( $validate_fieldset && ! in_array( $data[ $key ], $valid_state_values, true ) ) {
							/* translators: 1: state field 2: valid states */
							$errors->add( $key . '_validation', sprintf( __( '%1$s is not valid. Please enter one of the following: %2$s', 'woocommerce' ), '<strong>' . esc_html( $field_label ) . '</strong>', implode( ', ', $valid_states ) ), array( 'id' => $key ) );
						}
					}
				}

				if ( $validate_fieldset && $required && '' === $data[ $key ] ) {
					/* translators: %s: field name */
					$errors->add( $key . '_required', apply_filters( 'woocommerce_checkout_required_field_notice', sprintf( __( '%s is a required field.', 'woocommerce' ), '<strong>' . esc_html( $field_label ) . '</strong>' ), $field_label ), array( 'id' => $key ) );
				}
			}
		}
	}

	/**
	 * Validates that the checkout has enough info to proceed.
	 *
	 * @since  3.0.0
	 * @param  array    $data   An array of posted data.
	 * @param  WP_Error $errors Validation errors.
	 */
	protected function validate_checkout( &$data, &$errors ) {
		$this->validate_posted_data( $data, $errors );
		$this->check_cart_items();

		// phpcs:ignore WordPress.Security.NonceVerification.Missing
		if ( empty( $data['woocommerce_checkout_update_totals'] ) && empty( $data['terms'] ) && ! empty( $_POST['terms-field'] ) ) {
			$errors->add( 'terms', __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' ) );
		}

		if ( WC()->cart->needs_shipping() ) {
			$shipping_country = isset( $data['shipping_country'] ) ? $data['shipping_country'] : WC()->customer->get_shipping_country();

			if ( empty( $shipping_country ) ) {
				$errors->add( 'shipping', __( 'Please enter an address to continue.', 'woocommerce' ) );
			} elseif ( ! in_array( $shipping_country, array_keys( WC()->countries->get_shipping_countries() ), true ) ) {
				if ( WC()->countries->country_exists( $shipping_country ) ) {
					/* translators: %s: shipping location (prefix e.g. 'to' + ISO 3166-1 alpha-2 country code) */
					$errors->add( 'shipping', sprintf( __( 'Unfortunately <strong>we do not ship %s</strong>. Please enter an alternative shipping address.', 'woocommerce' ), WC()->countries->shipping_to_prefix() . ' ' . $shipping_country ) );
				}
			} else {
				$chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' );

				foreach ( WC()->shipping()->get_packages() as $i => $package ) {
					if ( ! isset( $chosen_shipping_methods[ $i ], $package['rates'][ $chosen_shipping_methods[ $i ] ] ) ) {
						$errors->add( 'shipping', __( 'No shipping method has been selected. Please double check your address, or contact us if you need any help.', 'woocommerce' ) );
					}
				}
			}
		}

		if ( WC()->cart->needs_payment() ) {
			$available_gateways = WC()->payment_gateways->get_available_payment_gateways();

			if ( ! isset( $available_gateways[ $data['payment_method'] ] ) ) {
				$errors->add( 'payment', __( 'Invalid payment method.', 'woocommerce' ) );
			} else {
				$available_gateways[ $data['payment_method'] ]->validate_fields();
			}
		}

		do_action( 'woocommerce_after_checkout_validation', $data, $errors );
	}

	/**
	 * Set address field for customer.
	 *
	 * @since 3.0.7
	 * @param string $field String to update.
	 * @param string $key   Field key.
	 * @param array  $data  Array of data to get the value from.
	 */
	protected function set_customer_address_fields( $field, $key, $data ) {
		$billing_value  = null;
		$shipping_value = null;

		if ( isset( $data[ "billing_{$field}" ] ) && is_callable( array( WC()->customer, "set_billing_{$field}" ) ) ) {
			$billing_value  = $data[ "billing_{$field}" ];
			$shipping_value = $data[ "billing_{$field}" ];
		}

		if ( isset( $data[ "shipping_{$field}" ] ) && is_callable( array( WC()->customer, "set_shipping_{$field}" ) ) ) {
			$shipping_value = $data[ "shipping_{$field}" ];
		}

		if ( ! is_null( $billing_value ) && is_callable( array( WC()->customer, "set_billing_{$field}" ) ) ) {
			WC()->customer->{"set_billing_{$field}"}( $billing_value );
		}

		if ( ! is_null( $shipping_value ) && is_callable( array( WC()->customer, "set_shipping_{$field}" ) ) ) {
			WC()->customer->{"set_shipping_{$field}"}( $shipping_value );
		}
	}

	/**
	 * Update customer and session data from the posted checkout data.
	 *
	 * @since 3.0.0
	 * @param array $data Posted data.
	 */
	protected function update_session( $data ) {
		// Update both shipping and billing to the passed billing address first if set.
		$address_fields = array(
			'first_name',
			'last_name',
			'company',
			'email',
			'phone',
			'address_1',
			'address_2',
			'city',
			'postcode',
			'state',
			'country',
		);

		array_walk( $address_fields, array( $this, 'set_customer_address_fields' ), $data );
		WC()->customer->save();

		// Update customer shipping and payment method to posted method.
		$chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' );

		if ( is_array( $data['shipping_method'] ) ) {
			foreach ( $data['shipping_method'] as $i => $value ) {
				$chosen_shipping_methods[ $i ] = $value;
			}
		}

		WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods );
		WC()->session->set( 'chosen_payment_method', $data['payment_method'] );

		// Update cart totals now we have customer address.
		WC()->cart->calculate_totals();
	}


	/**
	 * Process an order that does require payment.
	 *
	 * @since 3.0.0
	 * @param int    $order_id       Order ID.
	 * @param string $payment_method Payment method.
	 */
	protected function process_order_payment( $order_id, $payment_method ) {
		$available_gateways = WC()->payment_gateways->get_available_payment_gateways();

		if ( ! isset( $available_gateways[ $payment_method ] ) ) {
			return;
		}

		// Store Order ID in session so it can be re-used after payment failure.
		WC()->session->set( 'order_awaiting_payment', $order_id );

		// Process Payment.
		$result = $available_gateways[ $payment_method ]->process_payment( $order_id );

		// Redirect to success/confirmation/payment page.
		if ( isset( $result['result'] ) && 'success' === $result['result'] ) {
			$result['order_id'] = $order_id;

			$result = apply_filters( 'woocommerce_payment_successful_result', $result, $order_id );

			if ( ! is_ajax() ) {
				// phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect
				wp_redirect( $result['redirect'] );
				exit;
			}

			wp_send_json( $result );
		}
	}

	/**
	 * Process an order that doesn't require payment.
	 *
	 * @since 3.0.0
	 * @param int $order_id Order ID.
	 */
	protected function process_order_without_payment( $order_id ) {
		$order = wc_get_order( $order_id );
		$order->payment_complete();
		wc_empty_cart();

		if ( ! is_ajax() ) {
			wp_safe_redirect(
				apply_filters( 'woocommerce_checkout_no_payment_needed_redirect', $order->get_checkout_order_received_url(), $order )
			);
			exit;
		}

		wp_send_json(
			array(
				'result'   => 'success',
				'redirect' => apply_filters( 'woocommerce_checkout_no_payment_needed_redirect', $order->get_checkout_order_received_url(), $order ),
			)
		);
	}

	/**
	 * Create a new customer account if needed.
	 *
	 * @throws Exception When not able to create customer.
	 * @param array $data Posted data.
	 */
	protected function process_customer( $data ) {
		$customer_id = apply_filters( 'woocommerce_checkout_customer_id', get_current_user_id() );

		if ( ! is_user_logged_in() && ( $this->is_registration_required() || ! empty( $data['createaccount'] ) ) ) {
			$username    = ! empty( $data['account_username'] ) ? $data['account_username'] : '';
			$password    = ! empty( $data['account_password'] ) ? $data['account_password'] : '';
			$customer_id = wc_create_new_customer(
				$data['billing_email'],
				$username,
				$password,
				array(
					'first_name' => ! empty( $data['billing_first_name'] ) ? $data['billing_first_name'] : '',
					'last_name'  => ! empty( $data['billing_last_name'] ) ? $data['billing_last_name'] : '',
				)
			);

			if ( is_wp_error( $customer_id ) ) {
				throw new Exception( $customer_id->get_error_message() );
			}

			wc_set_customer_auth_cookie( $customer_id );

			// As we are now logged in, checkout will need to refresh to show logged in data.
			WC()->session->set( 'reload_checkout', true );

			// Also, recalculate cart totals to reveal any role-based discounts that were unavailable before registering.
			WC()->cart->calculate_totals();
		}

		// On multisite, ensure user exists on current site, if not add them before allowing login.
		if ( $customer_id && is_multisite() && is_user_logged_in() && ! is_user_member_of_blog() ) {
			add_user_to_blog( get_current_blog_id(), $customer_id, 'customer' );
		}

		// Add customer info from other fields.
		if ( $customer_id && apply_filters( 'woocommerce_checkout_update_customer_data', true, $this ) ) {
			$customer = new WC_Customer( $customer_id );

			if ( ! empty( $data['billing_first_name'] ) && '' === $customer->get_first_name() ) {
				$customer->set_first_name( $data['billing_first_name'] );
			}

			if ( ! empty( $data['billing_last_name'] ) && '' === $customer->get_last_name() ) {
				$customer->set_last_name( $data['billing_last_name'] );
			}

			// If the display name is an email, update to the user's full name.
			if ( is_email( $customer->get_display_name() ) ) {
				$customer->set_display_name( $customer->get_first_name() . ' ' . $customer->get_last_name() );
			}

			foreach ( $data as $key => $value ) {
				// Use setters where available.
				if ( is_callable( array( $customer, "set_{$key}" ) ) ) {
					$customer->{"set_{$key}"}( $value );

					// Store custom fields prefixed with wither shipping_ or billing_.
				} elseif ( 0 === stripos( $key, 'billing_' ) || 0 === stripos( $key, 'shipping_' ) ) {
					$customer->update_meta_data( $key, $value );
				}
			}

			/**
			 * Action hook to adjust customer before save.
			 *
			 * @since 3.0.0
			 */
			do_action( 'woocommerce_checkout_update_customer', $customer, $data );

			$customer->save();
		}

		do_action( 'woocommerce_checkout_update_user_meta', $customer_id, $data );
	}

	/**
	 * If checkout failed during an AJAX call, send failure response.
	 */
	protected function send_ajax_failure_response() {
		if ( is_ajax() ) {
			// Only print notices if not reloading the checkout, otherwise they're lost in the page reload.
			if ( ! isset( WC()->session->reload_checkout ) ) {
				$messages = wc_print_notices( true );
			}

			$response = array(
				'result'   => 'failure',
				'messages' => isset( $messages ) ? $messages : '',
				'refresh'  => isset( WC()->session->refresh_totals ),
				'reload'   => isset( WC()->session->reload_checkout ),
			);

			unset( WC()->session->refresh_totals, WC()->session->reload_checkout );

			wp_send_json( $response );
		}
	}

	/**
	 * Process the checkout after the confirm order button is pressed.
	 *
	 * @throws Exception When validation fails.
	 */
	public function process_checkout() {
		try {
			$nonce_value = wc_get_var( $_REQUEST['woocommerce-process-checkout-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // phpcs:ignore

			if ( empty( $nonce_value ) || ! wp_verify_nonce( $nonce_value, 'woocommerce-process_checkout' ) ) {
				WC()->session->set( 'refresh_totals', true );
				throw new Exception( __( 'We were unable to process your order, please try again.', 'woocommerce' ) );
			}

			wc_maybe_define_constant( 'WOOCOMMERCE_CHECKOUT', true );
			wc_set_time_limit( 0 );

			do_action( 'woocommerce_before_checkout_process' );

			if ( WC()->cart->is_empty() ) {
				/* translators: %s: shop cart url */
				throw new Exception( sprintf( __( 'Sorry, your session has expired. <a href="%s" class="wc-backward">Return to shop</a>', 'woocommerce' ), esc_url( wc_get_page_permalink( 'shop' ) ) ) );
			}

			do_action( 'woocommerce_checkout_process' );

			$errors      = new WP_Error();
			$posted_data = $this->get_posted_data();

			// Update session for customer and totals.
			$this->update_session( $posted_data );

			// Validate posted data and cart items before proceeding.
			$this->validate_checkout( $posted_data, $errors );

			foreach ( $errors->errors as $code => $messages ) {
				$data = $errors->get_error_data( $code );
				foreach ( $messages as $message ) {
					wc_add_notice( $message, 'error', $data );
				}
			}

			if ( empty( $posted_data['woocommerce_checkout_update_totals'] ) && 0 === wc_notice_count( 'error' ) ) {
				$this->process_customer( $posted_data );
				$order_id = $this->create_order( $posted_data );
				$order    = wc_get_order( $order_id );

				if ( is_wp_error( $order_id ) ) {
					throw new Exception( $order_id->get_error_message() );
				}

				if ( ! $order ) {
					throw new Exception( __( 'Unable to create order.', 'woocommerce' ) );
				}

				do_action( 'woocommerce_checkout_order_processed', $order_id, $posted_data, $order );

				/**
				 * Note that woocommerce_cart_needs_payment is only used in
				 * WC_Checkout::process_checkout() to keep backwards compatibility.
				 * Use woocommerce_order_needs_payment instead.
				 *
				 * Note that at this point you can't rely on the Cart Object anymore,
				 * since it could be empty see:
				 * https://github.com/woocommerce/woocommerce/issues/24631
				 */
				if ( apply_filters( 'woocommerce_cart_needs_payment', $order->needs_payment(), WC()->cart ) ) {
					$this->process_order_payment( $order_id, $posted_data['payment_method'] );
				} else {
					$this->process_order_without_payment( $order_id );
				}
			}
		} catch ( Exception $e ) {
			wc_add_notice( $e->getMessage(), 'error' );
		}
		$this->send_ajax_failure_response();
	}

	/**
	 * Get a posted address field after sanitization and validation.
	 *
	 * @param string $key  Field key.
	 * @param string $type Type of address. Available options: 'billing' or 'shipping'.
	 * @return string
	 */
	public function get_posted_address_data( $key, $type = 'billing' ) {
		if ( 'billing' === $type || false === $this->legacy_posted_data['ship_to_different_address'] ) {
			$return = isset( $this->legacy_posted_data[ 'billing_' . $key ] ) ? $this->legacy_posted_data[ 'billing_' . $key ] : '';
		} else {
			$return = isset( $this->legacy_posted_data[ 'shipping_' . $key ] ) ? $this->legacy_posted_data[ 'shipping_' . $key ] : '';
		}
		return $return;
	}

	/**
	 * Gets the value either from POST, or from the customer object. Sets the default values in checkout fields.
	 *
	 * @param string $input Name of the input we want to grab data for. e.g. billing_country.
	 * @return string The default value.
	 */
	public function get_value( $input ) {
		// If the form was posted, get the posted value. This will only tend to happen when JavaScript is disabled client side.
		if ( ! empty( $_POST[ $input ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
			return wc_clean( wp_unslash( $_POST[ $input ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
		}

		// Allow 3rd parties to short circuit the logic and return their own default value.
		$value = apply_filters( 'woocommerce_checkout_get_value', null, $input );

		if ( ! is_null( $value ) ) {
			return $value;
		}

		/**
		 * For logged in customers, pull data from their account rather than the session which may contain incomplete data.
		 * Another reason is that WC sets shipping address to the billing address on the checkout updates unless the
		 * "ship to another address" box is checked. @see issue #20975.
		 */
		$customer_object = false;

		if ( is_user_logged_in() ) {
			// Load customer object, but keep it cached to avoid reloading it multiple times.
			if ( is_null( $this->logged_in_customer ) ) {
				$this->logged_in_customer = new WC_Customer( get_current_user_id(), true );
			}
			$customer_object = $this->logged_in_customer;
		}

		if ( ! $customer_object ) {
			$customer_object = WC()->customer;
		}

		if ( is_callable( array( $customer_object, "get_$input" ) ) ) {
			$value = $customer_object->{"get_$input"}();
		} elseif ( $customer_object->meta_exists( $input ) ) {
			$value = $customer_object->get_meta( $input, true );
		}

		if ( '' === $value ) {
			$value = null;
		}

		return apply_filters( 'default_checkout_' . $input, $value, $input );
	}
}
class-wc-session-handler.php000064400000027277151336044770012116 0ustar00<?php
/**
 * Handle data for the current customers session.
 * Implements the WC_Session abstract class.
 *
 * From 2.5 this uses a custom table for session storage. Based on https://github.com/kloon/woocommerce-large-sessions.
 *
 * @class    WC_Session_Handler
 * @version  2.5.0
 * @package  WooCommerce\Classes
 */

use Automattic\Jetpack\Constants;

defined( 'ABSPATH' ) || exit;

/**
 * Session handler class.
 */
class WC_Session_Handler extends WC_Session {

	/**
	 * Cookie name used for the session.
	 *
	 * @var string cookie name
	 */
	protected $_cookie;

	/**
	 * Stores session expiry.
	 *
	 * @var string session due to expire timestamp
	 */
	protected $_session_expiring;

	/**
	 * Stores session due to expire timestamp.
	 *
	 * @var string session expiration timestamp
	 */
	protected $_session_expiration;

	/**
	 * True when the cookie exists.
	 *
	 * @var bool Based on whether a cookie exists.
	 */
	protected $_has_cookie = false;

	/**
	 * Table name for session data.
	 *
	 * @var string Custom session table name
	 */
	protected $_table;

	/**
	 * Constructor for the session class.
	 */
	public function __construct() {
		$this->_cookie = apply_filters( 'woocommerce_cookie', 'wp_woocommerce_session_' . COOKIEHASH );
		$this->_table  = $GLOBALS['wpdb']->prefix . 'woocommerce_sessions';
	}

	/**
	 * Init hooks and session data.
	 *
	 * @since 3.3.0
	 */
	public function init() {
		$this->init_session_cookie();

		add_action( 'woocommerce_set_cart_cookies', array( $this, 'set_customer_session_cookie' ), 10 );
		add_action( 'shutdown', array( $this, 'save_data' ), 20 );
		add_action( 'wp_logout', array( $this, 'destroy_session' ) );

		if ( ! is_user_logged_in() ) {
			add_filter( 'nonce_user_logged_out', array( $this, 'maybe_update_nonce_user_logged_out' ), 10, 2 );
		}
	}

	/**
	 * Setup cookie and customer ID.
	 *
	 * @since 3.6.0
	 */
	public function init_session_cookie() {
		$cookie = $this->get_session_cookie();

		if ( $cookie ) {
			$this->_customer_id        = $cookie[0];
			$this->_session_expiration = $cookie[1];
			$this->_session_expiring   = $cookie[2];
			$this->_has_cookie         = true;
			$this->_data               = $this->get_session_data();

			// If the user logs in, update session.
			if ( is_user_logged_in() && strval( get_current_user_id() ) !== $this->_customer_id ) {
				$guest_session_id   = $this->_customer_id;
				$this->_customer_id = strval( get_current_user_id() );
				$this->_dirty       = true;
				$this->save_data( $guest_session_id );
				$this->set_customer_session_cookie( true );
			}

			// Update session if its close to expiring.
			if ( time() > $this->_session_expiring ) {
				$this->set_session_expiration();
				$this->update_session_timestamp( $this->_customer_id, $this->_session_expiration );
			}
		} else {
			$this->set_session_expiration();
			$this->_customer_id = $this->generate_customer_id();
			$this->_data        = $this->get_session_data();
		}
	}

	/**
	 * Sets the session cookie on-demand (usually after adding an item to the cart).
	 *
	 * Since the cookie name (as of 2.1) is prepended with wp, cache systems like batcache will not cache pages when set.
	 *
	 * Warning: Cookies will only be set if this is called before the headers are sent.
	 *
	 * @param bool $set Should the session cookie be set.
	 */
	public function set_customer_session_cookie( $set ) {
		if ( $set ) {
			$to_hash           = $this->_customer_id . '|' . $this->_session_expiration;
			$cookie_hash       = hash_hmac( 'md5', $to_hash, wp_hash( $to_hash ) );
			$cookie_value      = $this->_customer_id . '||' . $this->_session_expiration . '||' . $this->_session_expiring . '||' . $cookie_hash;
			$this->_has_cookie = true;

			if ( ! isset( $_COOKIE[ $this->_cookie ] ) || $_COOKIE[ $this->_cookie ] !== $cookie_value ) {
				wc_setcookie( $this->_cookie, $cookie_value, $this->_session_expiration, $this->use_secure_cookie(), true );
			}
		}
	}

	/**
	 * Should the session cookie be secure?
	 *
	 * @since 3.6.0
	 * @return bool
	 */
	protected function use_secure_cookie() {
		return apply_filters( 'wc_session_use_secure_cookie', wc_site_is_https() && is_ssl() );
	}

	/**
	 * Return true if the current user has an active session, i.e. a cookie to retrieve values.
	 *
	 * @return bool
	 */
	public function has_session() {
		return isset( $_COOKIE[ $this->_cookie ] ) || $this->_has_cookie || is_user_logged_in(); // @codingStandardsIgnoreLine.
	}

	/**
	 * Set session expiration.
	 */
	public function set_session_expiration() {
		$this->_session_expiring   = time() + intval( apply_filters( 'wc_session_expiring', 60 * 60 * 47 ) ); // 47 Hours.
		$this->_session_expiration = time() + intval( apply_filters( 'wc_session_expiration', 60 * 60 * 48 ) ); // 48 Hours.
	}

	/**
	 * Generate a unique customer ID for guests, or return user ID if logged in.
	 *
	 * Uses Portable PHP password hashing framework to generate a unique cryptographically strong ID.
	 *
	 * @return string
	 */
	public function generate_customer_id() {
		$customer_id = '';

		if ( is_user_logged_in() ) {
			$customer_id = strval( get_current_user_id() );
		}

		if ( empty( $customer_id ) ) {
			require_once ABSPATH . 'wp-includes/class-phpass.php';
			$hasher      = new PasswordHash( 8, false );
			$customer_id = md5( $hasher->get_random_bytes( 32 ) );
		}

		return $customer_id;
	}

	/**
	 * Get session unique ID for requests if session is initialized or user ID if logged in.
	 * Introduced to help with unit tests.
	 *
	 * @since 5.3.0
	 * @return string
	 */
	public function get_customer_unique_id() {
		$customer_id = '';

		if ( $this->has_session() && $this->_customer_id ) {
			$customer_id = $this->_customer_id;
		} elseif ( is_user_logged_in() ) {
			$customer_id = (string) get_current_user_id();
		}

		return $customer_id;
	}

	/**
	 * Get the session cookie, if set. Otherwise return false.
	 *
	 * Session cookies without a customer ID are invalid.
	 *
	 * @return bool|array
	 */
	public function get_session_cookie() {
		$cookie_value = isset( $_COOKIE[ $this->_cookie ] ) ? wp_unslash( $_COOKIE[ $this->_cookie ] ) : false; // @codingStandardsIgnoreLine.

		if ( empty( $cookie_value ) || ! is_string( $cookie_value ) ) {
			return false;
		}

		list( $customer_id, $session_expiration, $session_expiring, $cookie_hash ) = explode( '||', $cookie_value );

		if ( empty( $customer_id ) ) {
			return false;
		}

		// Validate hash.
		$to_hash = $customer_id . '|' . $session_expiration;
		$hash    = hash_hmac( 'md5', $to_hash, wp_hash( $to_hash ) );

		if ( empty( $cookie_hash ) || ! hash_equals( $hash, $cookie_hash ) ) {
			return false;
		}

		return array( $customer_id, $session_expiration, $session_expiring, $cookie_hash );
	}

	/**
	 * Get session data.
	 *
	 * @return array
	 */
	public function get_session_data() {
		return $this->has_session() ? (array) $this->get_session( $this->_customer_id, array() ) : array();
	}

	/**
	 * Gets a cache prefix. This is used in session names so the entire cache can be invalidated with 1 function call.
	 *
	 * @return string
	 */
	private function get_cache_prefix() {
		return WC_Cache_Helper::get_cache_prefix( WC_SESSION_CACHE_GROUP );
	}

	/**
	 * Save data and delete guest session.
	 *
	 * @param int $old_session_key session ID before user logs in.
	 */
	public function save_data( $old_session_key = 0 ) {
		// Dirty if something changed - prevents saving nothing new.
		if ( $this->_dirty && $this->has_session() ) {
			global $wpdb;

			$wpdb->query(
				$wpdb->prepare(
					"INSERT INTO {$wpdb->prefix}woocommerce_sessions (`session_key`, `session_value`, `session_expiry`) VALUES (%s, %s, %d)
 					ON DUPLICATE KEY UPDATE `session_value` = VALUES(`session_value`), `session_expiry` = VALUES(`session_expiry`)",
					$this->_customer_id,
					maybe_serialize( $this->_data ),
					$this->_session_expiration
				)
			);

			wp_cache_set( $this->get_cache_prefix() . $this->_customer_id, $this->_data, WC_SESSION_CACHE_GROUP, $this->_session_expiration - time() );
			$this->_dirty = false;
			if ( get_current_user_id() != $old_session_key && ! is_object( get_user_by( 'id', $old_session_key ) ) ) {
				$this->delete_session( $old_session_key );
			}
		}
	}

	/**
	 * Destroy all session data.
	 */
	public function destroy_session() {
		$this->delete_session( $this->_customer_id );
		$this->forget_session();
	}

	/**
	 * Forget all session data without destroying it.
	 */
	public function forget_session() {
		wc_setcookie( $this->_cookie, '', time() - YEAR_IN_SECONDS, $this->use_secure_cookie(), true );

		wc_empty_cart();

		$this->_data        = array();
		$this->_dirty       = false;
		$this->_customer_id = $this->generate_customer_id();
	}

	/**
	 * When a user is logged out, ensure they have a unique nonce by using the customer/session ID.
	 *
	 * @deprecated 5.3.0
	 * @param int $uid User ID.
	 * @return int|string
	 */
	public function nonce_user_logged_out( $uid ) {
		wc_deprecated_function( 'WC_Session_Handler::nonce_user_logged_out', '5.3', 'WC_Session_Handler::maybe_update_nonce_user_logged_out' );

		return $this->has_session() && $this->_customer_id ? $this->_customer_id : $uid;
	}

	/**
	 * When a user is logged out, ensure they have a unique nonce to manage cart and more using the customer/session ID.
	 * This filter runs everything `wp_verify_nonce()` and `wp_create_nonce()` gets called.
	 *
	 * @since 5.3.0
	 * @param int    $uid    User ID.
	 * @param string $action The nonce action.
	 * @return int|string
	 */
	public function maybe_update_nonce_user_logged_out( $uid, $action ) {
		if ( Automattic\WooCommerce\Utilities\StringUtil::starts_with( $action, 'woocommerce' ) ) {
			return $this->has_session() && $this->_customer_id ? $this->_customer_id : $uid;
		}

		return $uid;
	}

	/**
	 * Cleanup session data from the database and clear caches.
	 */
	public function cleanup_sessions() {
		global $wpdb;

		$wpdb->query( $wpdb->prepare( "DELETE FROM $this->_table WHERE session_expiry < %d", time() ) ); // @codingStandardsIgnoreLine.

		if ( class_exists( 'WC_Cache_Helper' ) ) {
			WC_Cache_Helper::invalidate_cache_group( WC_SESSION_CACHE_GROUP );
		}
	}

	/**
	 * Returns the session.
	 *
	 * @param string $customer_id Custo ID.
	 * @param mixed  $default Default session value.
	 * @return string|array
	 */
	public function get_session( $customer_id, $default = false ) {
		global $wpdb;

		if ( Constants::is_defined( 'WP_SETUP_CONFIG' ) ) {
			return false;
		}

		// Try to get it from the cache, it will return false if not present or if object cache not in use.
		$value = wp_cache_get( $this->get_cache_prefix() . $customer_id, WC_SESSION_CACHE_GROUP );

		if ( false === $value ) {
			$value = $wpdb->get_var( $wpdb->prepare( "SELECT session_value FROM $this->_table WHERE session_key = %s", $customer_id ) ); // @codingStandardsIgnoreLine.

			if ( is_null( $value ) ) {
				$value = $default;
			}

			$cache_duration = $this->_session_expiration - time();
			if ( 0 < $cache_duration ) {
				wp_cache_add( $this->get_cache_prefix() . $customer_id, $value, WC_SESSION_CACHE_GROUP, $cache_duration );
			}
		}

		return maybe_unserialize( $value );
	}

	/**
	 * Delete the session from the cache and database.
	 *
	 * @param int $customer_id Customer ID.
	 */
	public function delete_session( $customer_id ) {
		global $wpdb;

		wp_cache_delete( $this->get_cache_prefix() . $customer_id, WC_SESSION_CACHE_GROUP );

		$wpdb->delete(
			$this->_table,
			array(
				'session_key' => $customer_id,
			)
		);
	}

	/**
	 * Update the session expiry timestamp.
	 *
	 * @param string $customer_id Customer ID.
	 * @param int    $timestamp Timestamp to expire the cookie.
	 */
	public function update_session_timestamp( $customer_id, $timestamp ) {
		global $wpdb;

		$wpdb->update(
			$this->_table,
			array(
				'session_expiry' => $timestamp,
			),
			array(
				'session_key' => $customer_id,
			),
			array(
				'%d',
			)
		);
	}
}
class-wc-embed.php000064400000010274151336044770010061 0ustar00<?php
/**
 * WooCommerce product embed
 *
 * @version  2.4.11
 * @package  WooCommerce\Classes\Embed
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Embed Class which handles any WooCommerce Products that are embedded on this site or another site.
 */
class WC_Embed {

	/**
	 * Init embed class.
	 *
	 * @since 2.4.11
	 */
	public static function init() {

		// Filter all of the content that's going to be embedded.
		add_filter( 'the_excerpt_embed', array( __CLASS__, 'the_excerpt' ), 10 );

		// Make sure no comments display. Doesn't make sense for products.
		add_action( 'embed_content_meta', array( __CLASS__, 'remove_comments_button' ), 5 );

		// In the comments place let's display the product rating.
		add_action( 'embed_content_meta', array( __CLASS__, 'get_ratings' ), 5 );

		// Add some basic styles.
		add_action( 'embed_head', array( __CLASS__, 'print_embed_styles' ) );
	}

	/**
	 * Remove comments button on product embeds.
	 *
	 * @since 2.6.0
	 */
	public static function remove_comments_button() {
		if ( self::is_embedded_product() ) {
			remove_action( 'embed_content_meta', 'print_embed_comments_button' );
		}
	}

	/**
	 * Check if this is an embedded product - to make sure we don't mess up regular posts.
	 *
	 * @since 2.4.11
	 * @return bool
	 */
	public static function is_embedded_product() {
		if ( function_exists( 'is_embed' ) && is_embed() && is_product() ) {
			return true;
		}
		return false;
	}

	/**
	 * Create the excerpt for embedded products - we want to add the buy button to it.
	 *
	 * @since 2.4.11
	 * @param string $excerpt Embed short description.
	 * @return string
	 */
	public static function the_excerpt( $excerpt ) {
		global $post;

		// Get product.
		$_product = wc_get_product( get_the_ID() );

		// Make sure we're only affecting embedded products.
		if ( self::is_embedded_product() ) {
			echo '<p><span class="wc-embed-price">' . $_product->get_price_html() . '</span></p>'; // WPCS: XSS ok.

			if ( ! empty( $post->post_excerpt ) ) {
				ob_start();
				woocommerce_template_single_excerpt();
				$excerpt = ob_get_clean();
			}

			// Add the button.
			$excerpt .= self::product_buttons();
		}
		return $excerpt;
	}

	/**
	 * Create the button to go to the product page for embedded products.
	 *
	 * @since 2.4.11
	 * @return string
	 */
	public static function product_buttons() {
		$_product = wc_get_product( get_the_ID() );
		$buttons  = array();
		$button   = '<a href="%s" class="wp-embed-more wc-embed-button">%s</a>';

		if ( $_product->is_type( 'simple' ) && $_product->is_purchasable() && $_product->is_in_stock() ) {
			$buttons[] = sprintf( $button, esc_url( add_query_arg( 'add-to-cart', get_the_ID(), wc_get_cart_url() ) ), esc_html__( 'Buy now', 'woocommerce' ) );
		}

		$buttons[] = sprintf( $button, get_the_permalink(), esc_html__( 'Read more', 'woocommerce' ) );

		return '<p>' . implode( ' ', $buttons ) . '</p>';
	}

	/**
	 * Prints the markup for the rating stars.
	 *
	 * @since 2.4.11
	 */
	public static function get_ratings() {
		// Make sure we're only affecting embedded products.
		if ( ! self::is_embedded_product() ) {
			return;
		}

		$_product = wc_get_product( get_the_ID() );

		if ( $_product && $_product->get_average_rating() > 0 ) {
			?>
			<div class="wc-embed-rating">
				<?php
					printf(
						/* translators: %s: average rating */
						esc_html__( 'Rated %s out of 5', 'woocommerce' ),
						esc_html( $_product->get_average_rating() )
					);
				?>
			</div>
			<?php
		}
	}

	/**
	 * Basic styling.
	 */
	public static function print_embed_styles() {
		if ( ! self::is_embedded_product() ) {
			return;
		}
		?>
		<style type="text/css">
			a.wc-embed-button {
				border-radius: 4px;
				border: 1px solid #ddd;
				box-shadow: 0px 1px 0 0px rgba(0, 0, 0, 0.05);
				display:inline-block;
				padding: .5em;
			}
			a.wc-embed-button:hover, a.wc-embed-button:focus {
				border: 1px solid #ccc;
				box-shadow: 0px 1px 0 0px rgba(0, 0, 0, 0.1);
				color: #999;
				text-decoration: none;
			}
			.wp-embed-excerpt p {
				margin: 0 0 1em;
			}
			.wc-embed-price {
				display: block;
				opacity: .75;
				font-weight: 700;
				margin-top: -.75em;
			}
			.wc-embed-rating {
				display: inline-block;
			}
		</style>
		<?php
	}
}

WC_Embed::init();
export/abstract-wc-csv-exporter.php000064400000027043151336044770013467 0ustar00<?php
/**
 * Handles CSV export.
 *
 * @package  WooCommerce\Export
 * @version  3.1.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC_CSV_Exporter Class.
 */
abstract class WC_CSV_Exporter {

	/**
	 * Type of export used in filter names.
	 *
	 * @var string
	 */
	protected $export_type = '';

	/**
	 * Filename to export to.
	 *
	 * @var string
	 */
	protected $filename = 'wc-export.csv';

	/**
	 * Batch limit.
	 *
	 * @var integer
	 */
	protected $limit = 50;

	/**
	 * Number exported.
	 *
	 * @var integer
	 */
	protected $exported_row_count = 0;

	/**
	 * Raw data to export.
	 *
	 * @var array
	 */
	protected $row_data = array();

	/**
	 * Total rows to export.
	 *
	 * @var integer
	 */
	protected $total_rows = 0;

	/**
	 * Columns ids and names.
	 *
	 * @var array
	 */
	protected $column_names = array();

	/**
	 * List of columns to export, or empty for all.
	 *
	 * @var array
	 */
	protected $columns_to_export = array();

	/**
	 * The delimiter parameter sets the field delimiter (one character only).
	 *
	 * @var string
	 */
	protected $delimiter = ',';

	/**
	 * Prepare data that will be exported.
	 */
	abstract public function prepare_data_to_export();

	/**
	 * Return an array of supported column names and ids.
	 *
	 * @since 3.1.0
	 * @return array
	 */
	public function get_column_names() {
		return apply_filters( "woocommerce_{$this->export_type}_export_column_names", $this->column_names, $this );
	}

	/**
	 * Set column names.
	 *
	 * @since 3.1.0
	 * @param array $column_names Column names array.
	 */
	public function set_column_names( $column_names ) {
		$this->column_names = array();

		foreach ( $column_names as $column_id => $column_name ) {
			$this->column_names[ wc_clean( $column_id ) ] = wc_clean( $column_name );
		}
	}

	/**
	 * Return an array of columns to export.
	 *
	 * @since 3.1.0
	 * @return array
	 */
	public function get_columns_to_export() {
		return $this->columns_to_export;
	}

	/**
	 * Return the delimiter to use in CSV file
	 *
	 * @since 3.9.0
	 * @return string
	 */
	public function get_delimiter() {
		return apply_filters( "woocommerce_{$this->export_type}_export_delimiter", $this->delimiter );
	}

	/**
	 * Set columns to export.
	 *
	 * @since 3.1.0
	 * @param array $columns Columns array.
	 */
	public function set_columns_to_export( $columns ) {
		$this->columns_to_export = array_map( 'wc_clean', $columns );
	}

	/**
	 * See if a column is to be exported or not.
	 *
	 * @since 3.1.0
	 * @param  string $column_id ID of the column being exported.
	 * @return boolean
	 */
	public function is_column_exporting( $column_id ) {
		$column_id         = strstr( $column_id, ':' ) ? current( explode( ':', $column_id ) ) : $column_id;
		$columns_to_export = $this->get_columns_to_export();

		if ( empty( $columns_to_export ) ) {
			return true;
		}

		if ( in_array( $column_id, $columns_to_export, true ) || 'meta' === $column_id ) {
			return true;
		}

		return false;
	}

	/**
	 * Return default columns.
	 *
	 * @since 3.1.0
	 * @return array
	 */
	public function get_default_column_names() {
		return array();
	}

	/**
	 * Do the export.
	 *
	 * @since 3.1.0
	 */
	public function export() {
		$this->prepare_data_to_export();
		$this->send_headers();
		$this->send_content( chr( 239 ) . chr( 187 ) . chr( 191 ) . $this->export_column_headers() . $this->get_csv_data() );
		die();
	}

	/**
	 * Set the export headers.
	 *
	 * @since 3.1.0
	 */
	public function send_headers() {
		if ( function_exists( 'gc_enable' ) ) {
			gc_enable(); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.gc_enableFound
		}
		if ( function_exists( 'apache_setenv' ) ) {
			@apache_setenv( 'no-gzip', 1 ); // @codingStandardsIgnoreLine
		}
		@ini_set( 'zlib.output_compression', 'Off' ); // @codingStandardsIgnoreLine
		@ini_set( 'output_buffering', 'Off' ); // @codingStandardsIgnoreLine
		@ini_set( 'output_handler', '' ); // @codingStandardsIgnoreLine
		ignore_user_abort( true );
		wc_set_time_limit( 0 );
		wc_nocache_headers();
		header( 'Content-Type: text/csv; charset=utf-8' );
		header( 'Content-Disposition: attachment; filename=' . $this->get_filename() );
		header( 'Pragma: no-cache' );
		header( 'Expires: 0' );
	}

	/**
	 * Set filename to export to.
	 *
	 * @param  string $filename Filename to export to.
	 */
	public function set_filename( $filename ) {
		$this->filename = sanitize_file_name( str_replace( '.csv', '', $filename ) . '.csv' );
	}

	/**
	 * Generate and return a filename.
	 *
	 * @return string
	 */
	public function get_filename() {
		return sanitize_file_name( apply_filters( "woocommerce_{$this->export_type}_export_get_filename", $this->filename ) );
	}

	/**
	 * Set the export content.
	 *
	 * @since 3.1.0
	 * @param string $csv_data All CSV content.
	 */
	public function send_content( $csv_data ) {
		echo $csv_data; // @codingStandardsIgnoreLine
	}

	/**
	 * Get CSV data for this export.
	 *
	 * @since 3.1.0
	 * @return string
	 */
	protected function get_csv_data() {
		return $this->export_rows();
	}

	/**
	 * Export column headers in CSV format.
	 *
	 * @since 3.1.0
	 * @return string
	 */
	protected function export_column_headers() {
		$columns    = $this->get_column_names();
		$export_row = array();
		$buffer     = fopen( 'php://output', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen
		ob_start();

		foreach ( $columns as $column_id => $column_name ) {
			if ( ! $this->is_column_exporting( $column_id ) ) {
				continue;
			}
			$export_row[] = $this->format_data( $column_name );
		}

		$this->fputcsv( $buffer, $export_row );

		return ob_get_clean();
	}

	/**
	 * Get data that will be exported.
	 *
	 * @since 3.1.0
	 * @return array
	 */
	protected function get_data_to_export() {
		return $this->row_data;
	}

	/**
	 * Export rows in CSV format.
	 *
	 * @since 3.1.0
	 * @return string
	 */
	protected function export_rows() {
		$data   = $this->get_data_to_export();
		$buffer = fopen( 'php://output', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen
		ob_start();

		array_walk( $data, array( $this, 'export_row' ), $buffer );

		return apply_filters( "woocommerce_{$this->export_type}_export_rows", ob_get_clean(), $this );
	}

	/**
	 * Export rows to an array ready for the CSV.
	 *
	 * @since 3.1.0
	 * @param array    $row_data Data to export.
	 * @param string   $key Column being exported.
	 * @param resource $buffer Output buffer.
	 */
	protected function export_row( $row_data, $key, $buffer ) {
		$columns    = $this->get_column_names();
		$export_row = array();

		foreach ( $columns as $column_id => $column_name ) {
			if ( ! $this->is_column_exporting( $column_id ) ) {
				continue;
			}
			if ( isset( $row_data[ $column_id ] ) ) {
				$export_row[] = $this->format_data( $row_data[ $column_id ] );
			} else {
				$export_row[] = '';
			}
		}

		$this->fputcsv( $buffer, $export_row );

		++ $this->exported_row_count;
	}

	/**
	 * Get batch limit.
	 *
	 * @since 3.1.0
	 * @return int
	 */
	public function get_limit() {
		return apply_filters( "woocommerce_{$this->export_type}_export_batch_limit", $this->limit, $this );
	}

	/**
	 * Set batch limit.
	 *
	 * @since 3.1.0
	 * @param int $limit Limit to export.
	 */
	public function set_limit( $limit ) {
		$this->limit = absint( $limit );
	}

	/**
	 * Get count of records exported.
	 *
	 * @since 3.1.0
	 * @return int
	 */
	public function get_total_exported() {
		return $this->exported_row_count;
	}

	/**
	 * Escape a string to be used in a CSV context
	 *
	 * Malicious input can inject formulas into CSV files, opening up the possibility
	 * for phishing attacks and disclosure of sensitive information.
	 *
	 * Additionally, Excel exposes the ability to launch arbitrary commands through
	 * the DDE protocol.
	 *
	 * @see http://www.contextis.com/resources/blog/comma-separated-vulnerabilities/
	 * @see https://hackerone.com/reports/72785
	 *
	 * @since 3.1.0
	 * @param string $data CSV field to escape.
	 * @return string
	 */
	public function escape_data( $data ) {
		$active_content_triggers = array( '=', '+', '-', '@' );

		if ( in_array( mb_substr( $data, 0, 1 ), $active_content_triggers, true ) ) {
			$data = "'" . $data;
		}

		return $data;
	}

	/**
	 * Format and escape data ready for the CSV file.
	 *
	 * @since 3.1.0
	 * @param  string $data Data to format.
	 * @return string
	 */
	public function format_data( $data ) {
		if ( ! is_scalar( $data ) ) {
			if ( is_a( $data, 'WC_Datetime' ) ) {
				$data = $data->date( 'Y-m-d G:i:s' );
			} else {
				$data = ''; // Not supported.
			}
		} elseif ( is_bool( $data ) ) {
			$data = $data ? 1 : 0;
		}

		$use_mb = function_exists( 'mb_convert_encoding' );

		if ( $use_mb ) {
			$encoding = mb_detect_encoding( $data, 'UTF-8, ISO-8859-1', true );
			$data     = 'UTF-8' === $encoding ? $data : utf8_encode( $data );
		}

		return $this->escape_data( $data );
	}

	/**
	 * Format term ids to names.
	 *
	 * @since 3.1.0
	 * @param  array  $term_ids Term IDs to format.
	 * @param  string $taxonomy Taxonomy name.
	 * @return string
	 */
	public function format_term_ids( $term_ids, $taxonomy ) {
		$term_ids = wp_parse_id_list( $term_ids );

		if ( ! count( $term_ids ) ) {
			return '';
		}

		$formatted_terms = array();

		if ( is_taxonomy_hierarchical( $taxonomy ) ) {
			foreach ( $term_ids as $term_id ) {
				$formatted_term = array();
				$ancestor_ids   = array_reverse( get_ancestors( $term_id, $taxonomy ) );

				foreach ( $ancestor_ids as $ancestor_id ) {
					$term = get_term( $ancestor_id, $taxonomy );
					if ( $term && ! is_wp_error( $term ) ) {
						$formatted_term[] = $term->name;
					}
				}

				$term = get_term( $term_id, $taxonomy );

				if ( $term && ! is_wp_error( $term ) ) {
					$formatted_term[] = $term->name;
				}

				$formatted_terms[] = implode( ' > ', $formatted_term );
			}
		} else {
			foreach ( $term_ids as $term_id ) {
				$term = get_term( $term_id, $taxonomy );

				if ( $term && ! is_wp_error( $term ) ) {
					$formatted_terms[] = $term->name;
				}
			}
		}

		return $this->implode_values( $formatted_terms );
	}

	/**
	 * Implode CSV cell values using commas by default, and wrapping values
	 * which contain the separator.
	 *
	 * @since  3.2.0
	 * @param  array $values Values to implode.
	 * @return string
	 */
	protected function implode_values( $values ) {
		$values_to_implode = array();

		foreach ( $values as $value ) {
			$value               = (string) is_scalar( $value ) ? $value : '';
			$values_to_implode[] = str_replace( ',', '\\,', $value );
		}

		return implode( ', ', $values_to_implode );
	}

	/**
	 * Write to the CSV file, ensuring escaping works across versions of
	 * PHP.
	 *
	 * PHP 5.5.4 uses '\' as the default escape character. This is not RFC-4180 compliant.
	 * \0 disables the escape character.
	 *
	 * @see https://bugs.php.net/bug.php?id=43225
	 * @see https://bugs.php.net/bug.php?id=50686
	 * @see https://github.com/woocommerce/woocommerce/issues/19514
	 * @since 3.4.0
	 * @see https://github.com/woocommerce/woocommerce/issues/24579
	 * @since 3.9.0
	 * @param resource $buffer Resource we are writing to.
	 * @param array    $export_row Row to export.
	 */
	protected function fputcsv( $buffer, $export_row ) {

		if ( version_compare( PHP_VERSION, '5.5.4', '<' ) ) {
			ob_start();
			$temp = fopen( 'php://output', 'w' ); // @codingStandardsIgnoreLine
    		fputcsv( $temp, $export_row, $this->get_delimiter(), '"' ); // @codingStandardsIgnoreLine
			fclose( $temp ); // @codingStandardsIgnoreLine
			$row = ob_get_clean();
			$row = str_replace( '\\"', '\\""', $row );
			fwrite( $buffer, $row ); // @codingStandardsIgnoreLine
		} else {
			fputcsv( $buffer, $export_row, $this->get_delimiter(), '"', "\0" ); // @codingStandardsIgnoreLine
		}
	}
}
export/abstract-wc-csv-batch-exporter.php000064400000012275151336044770014547 0ustar00<?php
/**
 * Handles Batch CSV export.
 *
 * Based on https://pippinsplugins.com/batch-processing-for-big-data/
 *
 * @package  WooCommerce\Export
 * @version  3.1.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Include dependencies.
 */
if ( ! class_exists( 'WC_CSV_Exporter', false ) ) {
	require_once WC_ABSPATH . 'includes/export/abstract-wc-csv-exporter.php';
}

/**
 * WC_CSV_Exporter Class.
 */
abstract class WC_CSV_Batch_Exporter extends WC_CSV_Exporter {

	/**
	 * Page being exported
	 *
	 * @var integer
	 */
	protected $page = 1;

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->column_names = $this->get_default_column_names();
	}

	/**
	 * Get file path to export to.
	 *
	 * @return string
	 */
	protected function get_file_path() {
		$upload_dir = wp_upload_dir();
		return trailingslashit( $upload_dir['basedir'] ) . $this->get_filename();
	}

	/**
	 * Get CSV headers row file path to export to.
	 *
	 * @return string
	 */
	protected function get_headers_row_file_path() {
		return $this->get_file_path() . '.headers';
	}

	/**
	 * Get the contents of the CSV headers row file. Defaults to the original known headers.
	 *
	 * @since 3.1.0
	 * @return string
	 */
	public function get_headers_row_file() {

		$file = chr( 239 ) . chr( 187 ) . chr( 191 ) . $this->export_column_headers();

		if ( @file_exists( $this->get_headers_row_file_path() ) ) { // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
			$file = @file_get_contents( $this->get_headers_row_file_path() ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, WordPress.WP.AlternativeFunctions.file_system_read_file_get_contents
		}

		return $file;
	}

	/**
	 * Get the file contents.
	 *
	 * @since 3.1.0
	 * @return string
	 */
	public function get_file() {
		$file = '';
		if ( @file_exists( $this->get_file_path() ) ) { // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
			$file = @file_get_contents( $this->get_file_path() ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, WordPress.WP.AlternativeFunctions.file_system_read_file_get_contents
		} else {
			@file_put_contents( $this->get_file_path(), '' ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_file_put_contents, Generic.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents
			@chmod( $this->get_file_path(), 0664 ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.chmod_chmod, WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents, Generic.PHP.NoSilencedErrors.Discouraged
		}
		return $file;
	}

	/**
	 * Serve the file and remove once sent to the client.
	 *
	 * @since 3.1.0
	 */
	public function export() {
		$this->send_headers();
		$this->send_content( $this->get_headers_row_file() . $this->get_file() );
		@unlink( $this->get_file_path() ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_unlink, Generic.PHP.NoSilencedErrors.Discouraged
		@unlink( $this->get_headers_row_file_path() ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_unlink, Generic.PHP.NoSilencedErrors.Discouraged
		die();
	}

	/**
	 * Generate the CSV file.
	 *
	 * @since 3.1.0
	 */
	public function generate_file() {
		if ( 1 === $this->get_page() ) {
			@unlink( $this->get_file_path() ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_unlink, Generic.PHP.NoSilencedErrors.Discouraged,

			// We need to initialize the file here.
			$this->get_file();
		}
		$this->prepare_data_to_export();
		$this->write_csv_data( $this->get_csv_data() );
	}

	/**
	 * Write data to the file.
	 *
	 * @since 3.1.0
	 * @param string $data Data.
	 */
	protected function write_csv_data( $data ) {

		if ( ! file_exists( $this->get_file_path() ) || ! is_writeable( $this->get_file_path() ) ) {
			return false;
		}

		$fp = fopen( $this->get_file_path(), 'a+' );

		if ( $fp ) {
			fwrite( $fp, $data );
			fclose( $fp );
		}

		// Add all columns when finished.
		if ( 100 === $this->get_percent_complete() ) {
			$header = chr( 239 ) . chr( 187 ) . chr( 191 ) . $this->export_column_headers();

			// We need to use a temporary file to store headers, this will make our life so much easier.
			@file_put_contents( $this->get_headers_row_file_path(), $header ); //phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_file_put_contents, Generic.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents
		}

	}

	/**
	 * Get page.
	 *
	 * @since 3.1.0
	 * @return int
	 */
	public function get_page() {
		return $this->page;
	}

	/**
	 * Set page.
	 *
	 * @since 3.1.0
	 * @param int $page Page Nr.
	 */
	public function set_page( $page ) {
		$this->page = absint( $page );
	}

	/**
	 * Get count of records exported.
	 *
	 * @since 3.1.0
	 * @return int
	 */
	public function get_total_exported() {
		return ( ( $this->get_page() - 1 ) * $this->get_limit() ) + $this->exported_row_count;
	}

	/**
	 * Get total % complete.
	 *
	 * @since 3.1.0
	 * @return int
	 */
	public function get_percent_complete() {
		return $this->total_rows ? floor( ( $this->get_total_exported() / $this->total_rows ) * 100 ) : 100;
	}
}
export/class-wc-product-csv-exporter.php000064400000053551151336044770014452 0ustar00<?php
/**
 * Handles product CSV export.
 *
 * @package WooCommerce\Export
 * @version 3.1.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Include dependencies.
 */
if ( ! class_exists( 'WC_CSV_Batch_Exporter', false ) ) {
	include_once WC_ABSPATH . 'includes/export/abstract-wc-csv-batch-exporter.php';
}

/**
 * WC_Product_CSV_Exporter Class.
 */
class WC_Product_CSV_Exporter extends WC_CSV_Batch_Exporter {

	/**
	 * Type of export used in filter names.
	 *
	 * @var string
	 */
	protected $export_type = 'product';

	/**
	 * Should meta be exported?
	 *
	 * @var boolean
	 */
	protected $enable_meta_export = false;

	/**
	 * Which product types are being exported.
	 *
	 * @var array
	 */
	protected $product_types_to_export = array();

	/**
	 * Products belonging to what category should be exported.
	 *
	 * @var string
	 */
	protected $product_category_to_export = array();

	/**
	 * Constructor.
	 */
	public function __construct() {
		parent::__construct();
		$this->set_product_types_to_export( array_keys( WC_Admin_Exporters::get_product_types() ) );
	}

	/**
	 * Should meta be exported?
	 *
	 * @param bool $enable_meta_export Should meta be exported.
	 *
	 * @since 3.1.0
	 */
	public function enable_meta_export( $enable_meta_export ) {
		$this->enable_meta_export = (bool) $enable_meta_export;
	}

	/**
	 * Product types to export.
	 *
	 * @param array $product_types_to_export List of types to export.
	 *
	 * @since 3.1.0
	 */
	public function set_product_types_to_export( $product_types_to_export ) {
		$this->product_types_to_export = array_map( 'wc_clean', $product_types_to_export );
	}

	/**
	 * Product category to export
	 *
	 * @param string $product_category_to_export Product category slug to export, empty string exports all.
	 *
	 * @since  3.5.0
	 * @return void
	 */
	public function set_product_category_to_export( $product_category_to_export ) {
		$this->product_category_to_export = array_map( 'sanitize_title_with_dashes', $product_category_to_export );
	}

	/**
	 * Return an array of columns to export.
	 *
	 * @since  3.1.0
	 * @return array
	 */
	public function get_default_column_names() {
		return apply_filters(
			"woocommerce_product_export_{$this->export_type}_default_columns",
			array(
				'id'                 => __( 'ID', 'woocommerce' ),
				'type'               => __( 'Type', 'woocommerce' ),
				'sku'                => __( 'SKU', 'woocommerce' ),
				'name'               => __( 'Name', 'woocommerce' ),
				'published'          => __( 'Published', 'woocommerce' ),
				'featured'           => __( 'Is featured?', 'woocommerce' ),
				'catalog_visibility' => __( 'Visibility in catalog', 'woocommerce' ),
				'short_description'  => __( 'Short description', 'woocommerce' ),
				'description'        => __( 'Description', 'woocommerce' ),
				'date_on_sale_from'  => __( 'Date sale price starts', 'woocommerce' ),
				'date_on_sale_to'    => __( 'Date sale price ends', 'woocommerce' ),
				'tax_status'         => __( 'Tax status', 'woocommerce' ),
				'tax_class'          => __( 'Tax class', 'woocommerce' ),
				'stock_status'       => __( 'In stock?', 'woocommerce' ),
				'stock'              => __( 'Stock', 'woocommerce' ),
				'low_stock_amount'   => __( 'Low stock amount', 'woocommerce' ),
				'backorders'         => __( 'Backorders allowed?', 'woocommerce' ),
				'sold_individually'  => __( 'Sold individually?', 'woocommerce' ),
				/* translators: %s: weight */
				'weight'             => sprintf( __( 'Weight (%s)', 'woocommerce' ), get_option( 'woocommerce_weight_unit' ) ),
				/* translators: %s: length */
				'length'             => sprintf( __( 'Length (%s)', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ),
				/* translators: %s: width */
				'width'              => sprintf( __( 'Width (%s)', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ),
				/* translators: %s: Height */
				'height'             => sprintf( __( 'Height (%s)', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ),
				'reviews_allowed'    => __( 'Allow customer reviews?', 'woocommerce' ),
				'purchase_note'      => __( 'Purchase note', 'woocommerce' ),
				'sale_price'         => __( 'Sale price', 'woocommerce' ),
				'regular_price'      => __( 'Regular price', 'woocommerce' ),
				'category_ids'       => __( 'Categories', 'woocommerce' ),
				'tag_ids'            => __( 'Tags', 'woocommerce' ),
				'shipping_class_id'  => __( 'Shipping class', 'woocommerce' ),
				'images'             => __( 'Images', 'woocommerce' ),
				'download_limit'     => __( 'Download limit', 'woocommerce' ),
				'download_expiry'    => __( 'Download expiry days', 'woocommerce' ),
				'parent_id'          => __( 'Parent', 'woocommerce' ),
				'grouped_products'   => __( 'Grouped products', 'woocommerce' ),
				'upsell_ids'         => __( 'Upsells', 'woocommerce' ),
				'cross_sell_ids'     => __( 'Cross-sells', 'woocommerce' ),
				'product_url'        => __( 'External URL', 'woocommerce' ),
				'button_text'        => __( 'Button text', 'woocommerce' ),
				'menu_order'         => __( 'Position', 'woocommerce' ),
			)
		);
	}

	/**
	 * Prepare data for export.
	 *
	 * @since 3.1.0
	 */
	public function prepare_data_to_export() {
		$args = array(
			'status'   => array( 'private', 'publish', 'draft', 'future', 'pending' ),
			'type'     => $this->product_types_to_export,
			'limit'    => $this->get_limit(),
			'page'     => $this->get_page(),
			'orderby'  => array(
				'ID' => 'ASC',
			),
			'return'   => 'objects',
			'paginate' => true,
		);

		if ( ! empty( $this->product_category_to_export ) ) {
			$args['category'] = $this->product_category_to_export;
		}
		$products = wc_get_products( apply_filters( "woocommerce_product_export_{$this->export_type}_query_args", $args ) );

		$this->total_rows  = $products->total;
		$this->row_data    = array();
		$variable_products = array();

		foreach ( $products->products as $product ) {
			// Check if the category is set, this means we need to fetch variations seperately as they are not tied to a category.
			if ( ! empty( $args['category'] ) && $product->is_type( 'variable' ) ) {
				$variable_products[] = $product->get_id();
			}

			$this->row_data[] = $this->generate_row_data( $product );
		}

		// If a category was selected we loop through the variations as they are not tied to a category so will be excluded by default.
		if ( ! empty( $variable_products ) ) {
			foreach ( $variable_products as $parent_id ) {
				$products = wc_get_products(
					array(
						'parent' => $parent_id,
						'type'   => array( 'variation' ),
						'return' => 'objects',
						'limit'  => -1,
					)
				);

				if ( ! $products ) {
					continue;
				}

				foreach ( $products as $product ) {
					$this->row_data[] = $this->generate_row_data( $product );
				}
			}
		}
	}

	/**
	 * Take a product and generate row data from it for export.
	 *
	 * @param WC_Product $product WC_Product object.
	 *
	 * @return array
	 */
	protected function generate_row_data( $product ) {
		$columns = $this->get_column_names();
		$row     = array();
		foreach ( $columns as $column_id => $column_name ) {
			$column_id = strstr( $column_id, ':' ) ? current( explode( ':', $column_id ) ) : $column_id;
			$value     = '';

			// Skip some columns if dynamically handled later or if we're being selective.
			if ( in_array( $column_id, array( 'downloads', 'attributes', 'meta' ), true ) || ! $this->is_column_exporting( $column_id ) ) {
				continue;
			}

			if ( has_filter( "woocommerce_product_export_{$this->export_type}_column_{$column_id}" ) ) {
				// Filter for 3rd parties.
				$value = apply_filters( "woocommerce_product_export_{$this->export_type}_column_{$column_id}", '', $product, $column_id );

			} elseif ( is_callable( array( $this, "get_column_value_{$column_id}" ) ) ) {
				// Handle special columns which don't map 1:1 to product data.
				$value = $this->{"get_column_value_{$column_id}"}( $product );

			} elseif ( is_callable( array( $product, "get_{$column_id}" ) ) ) {
				// Default and custom handling.
				$value = $product->{"get_{$column_id}"}( 'edit' );
			}

			if ( 'description' === $column_id || 'short_description' === $column_id ) {
				$value = $this->filter_description_field( $value );
			}

			$row[ $column_id ] = $value;
		}

		$this->prepare_downloads_for_export( $product, $row );
		$this->prepare_attributes_for_export( $product, $row );
		$this->prepare_meta_for_export( $product, $row );
		return apply_filters( 'woocommerce_product_export_row_data', $row, $product );
	}

	/**
	 * Get published value.
	 *
	 * @param WC_Product $product Product being exported.
	 *
	 * @since  3.1.0
	 * @return int
	 */
	protected function get_column_value_published( $product ) {
		$statuses = array(
			'draft'   => -1,
			'private' => 0,
			'publish' => 1,
		);

		// Fix display for variations when parent product is a draft.
		if ( 'variation' === $product->get_type() ) {
			$parent = $product->get_parent_data();
			$status = 'draft' === $parent['status'] ? $parent['status'] : $product->get_status( 'edit' );
		} else {
			$status = $product->get_status( 'edit' );
		}

		return isset( $statuses[ $status ] ) ? $statuses[ $status ] : -1;
	}

	/**
	 * Get formatted sale price.
	 *
	 * @param WC_Product $product Product being exported.
	 *
	 * @return string
	 */
	protected function get_column_value_sale_price( $product ) {
		return wc_format_localized_price( $product->get_sale_price( 'view' ) );
	}

	/**
	 * Get formatted regular price.
	 *
	 * @param WC_Product $product Product being exported.
	 *
	 * @return string
	 */
	protected function get_column_value_regular_price( $product ) {
		return wc_format_localized_price( $product->get_regular_price() );
	}

	/**
	 * Get product_cat value.
	 *
	 * @param WC_Product $product Product being exported.
	 *
	 * @since  3.1.0
	 * @return string
	 */
	protected function get_column_value_category_ids( $product ) {
		$term_ids = $product->get_category_ids( 'edit' );
		return $this->format_term_ids( $term_ids, 'product_cat' );
	}

	/**
	 * Get product_tag value.
	 *
	 * @param WC_Product $product Product being exported.
	 *
	 * @since  3.1.0
	 * @return string
	 */
	protected function get_column_value_tag_ids( $product ) {
		$term_ids = $product->get_tag_ids( 'edit' );
		return $this->format_term_ids( $term_ids, 'product_tag' );
	}

	/**
	 * Get product_shipping_class value.
	 *
	 * @param WC_Product $product Product being exported.
	 *
	 * @since  3.1.0
	 * @return string
	 */
	protected function get_column_value_shipping_class_id( $product ) {
		$term_ids = $product->get_shipping_class_id( 'edit' );
		return $this->format_term_ids( $term_ids, 'product_shipping_class' );
	}

	/**
	 * Get images value.
	 *
	 * @param WC_Product $product Product being exported.
	 *
	 * @since  3.1.0
	 * @return string
	 */
	protected function get_column_value_images( $product ) {
		$image_ids = array_merge( array( $product->get_image_id( 'edit' ) ), $product->get_gallery_image_ids( 'edit' ) );
		$images    = array();

		foreach ( $image_ids as $image_id ) {
			$image = wp_get_attachment_image_src( $image_id, 'full' );

			if ( $image ) {
				$images[] = $image[0];
			}
		}

		return $this->implode_values( $images );
	}

	/**
	 * Prepare linked products for export.
	 *
	 * @param int[] $linked_products Array of linked product ids.
	 *
	 * @since  3.1.0
	 * @return string
	 */
	protected function prepare_linked_products_for_export( $linked_products ) {
		$product_list = array();

		foreach ( $linked_products as $linked_product ) {
			if ( $linked_product->get_sku() ) {
				$product_list[] = $linked_product->get_sku();
			} else {
				$product_list[] = 'id:' . $linked_product->get_id();
			}
		}

		return $this->implode_values( $product_list );
	}

	/**
	 * Get cross_sell_ids value.
	 *
	 * @param WC_Product $product Product being exported.
	 *
	 * @since  3.1.0
	 * @return string
	 */
	protected function get_column_value_cross_sell_ids( $product ) {
		return $this->prepare_linked_products_for_export( array_filter( array_map( 'wc_get_product', (array) $product->get_cross_sell_ids( 'edit' ) ) ) );
	}

	/**
	 * Get upsell_ids value.
	 *
	 * @param WC_Product $product Product being exported.
	 *
	 * @since  3.1.0
	 * @return string
	 */
	protected function get_column_value_upsell_ids( $product ) {
		return $this->prepare_linked_products_for_export( array_filter( array_map( 'wc_get_product', (array) $product->get_upsell_ids( 'edit' ) ) ) );
	}

	/**
	 * Get parent_id value.
	 *
	 * @param WC_Product $product Product being exported.
	 *
	 * @since  3.1.0
	 * @return string
	 */
	protected function get_column_value_parent_id( $product ) {
		if ( $product->get_parent_id( 'edit' ) ) {
			$parent = wc_get_product( $product->get_parent_id( 'edit' ) );
			if ( ! $parent ) {
				return '';
			}

			return $parent->get_sku( 'edit' ) ? $parent->get_sku( 'edit' ) : 'id:' . $parent->get_id();
		}
		return '';
	}

	/**
	 * Get grouped_products value.
	 *
	 * @param WC_Product $product Product being exported.
	 *
	 * @since  3.1.0
	 * @return string
	 */
	protected function get_column_value_grouped_products( $product ) {
		if ( 'grouped' !== $product->get_type() ) {
			return '';
		}

		$grouped_products = array();
		$child_ids        = $product->get_children( 'edit' );
		foreach ( $child_ids as $child_id ) {
			$child = wc_get_product( $child_id );
			if ( ! $child ) {
				continue;
			}

			$grouped_products[] = $child->get_sku( 'edit' ) ? $child->get_sku( 'edit' ) : 'id:' . $child_id;
		}
		return $this->implode_values( $grouped_products );
	}

	/**
	 * Get download_limit value.
	 *
	 * @param WC_Product $product Product being exported.
	 *
	 * @since  3.1.0
	 * @return string
	 */
	protected function get_column_value_download_limit( $product ) {
		return $product->is_downloadable() && $product->get_download_limit( 'edit' ) ? $product->get_download_limit( 'edit' ) : '';
	}

	/**
	 * Get download_expiry value.
	 *
	 * @param WC_Product $product Product being exported.
	 *
	 * @since  3.1.0
	 * @return string
	 */
	protected function get_column_value_download_expiry( $product ) {
		return $product->is_downloadable() && $product->get_download_expiry( 'edit' ) ? $product->get_download_expiry( 'edit' ) : '';
	}

	/**
	 * Get stock value.
	 *
	 * @param WC_Product $product Product being exported.
	 *
	 * @since  3.1.0
	 * @return string
	 */
	protected function get_column_value_stock( $product ) {
		$manage_stock   = $product->get_manage_stock( 'edit' );
		$stock_quantity = $product->get_stock_quantity( 'edit' );

		if ( $product->is_type( 'variation' ) && 'parent' === $manage_stock ) {
			return 'parent';
		} elseif ( $manage_stock ) {
			return $stock_quantity;
		} else {
			return '';
		}
	}

	/**
	 * Get stock status value.
	 *
	 * @param WC_Product $product Product being exported.
	 *
	 * @since  3.1.0
	 * @return string
	 */
	protected function get_column_value_stock_status( $product ) {
		$status = $product->get_stock_status( 'edit' );

		if ( 'onbackorder' === $status ) {
			return 'backorder';
		}

		return 'instock' === $status ? 1 : 0;
	}

	/**
	 * Get backorders.
	 *
	 * @param WC_Product $product Product being exported.
	 *
	 * @since  3.1.0
	 * @return string
	 */
	protected function get_column_value_backorders( $product ) {
		$backorders = $product->get_backorders( 'edit' );

		switch ( $backorders ) {
			case 'notify':
				return 'notify';
			default:
				return wc_string_to_bool( $backorders ) ? 1 : 0;
		}
	}

	/**
	 * Get low stock amount value.
	 *
	 * @param WC_Product $product Product being exported.
	 *
	 * @since  3.5.0
	 * @return int|string Empty string if value not set
	 */
	protected function get_column_value_low_stock_amount( $product ) {
		return $product->managing_stock() && $product->get_low_stock_amount( 'edit' ) ? $product->get_low_stock_amount( 'edit' ) : '';
	}

	/**
	 * Get type value.
	 *
	 * @param WC_Product $product Product being exported.
	 *
	 * @since  3.1.0
	 * @return string
	 */
	protected function get_column_value_type( $product ) {
		$types   = array();
		$types[] = $product->get_type();

		if ( $product->is_downloadable() ) {
			$types[] = 'downloadable';
		}

		if ( $product->is_virtual() ) {
			$types[] = 'virtual';
		}

		return $this->implode_values( $types );
	}

	/**
	 * Filter description field for export.
	 * Convert newlines to '\n'.
	 *
	 * @param string $description Product description text to filter.
	 *
	 * @since  3.5.4
	 * @return string
	 */
	protected function filter_description_field( $description ) {
		$description = str_replace( '\n', "\\\\n", $description );
		$description = str_replace( "\n", '\n', $description );
		return $description;
	}
	/**
	 * Export downloads.
	 *
	 * @param WC_Product $product Product being exported.
	 * @param array      $row     Row being exported.
	 *
	 * @since 3.1.0
	 */
	protected function prepare_downloads_for_export( $product, &$row ) {
		if ( $product->is_downloadable() && $this->is_column_exporting( 'downloads' ) ) {
			$downloads = $product->get_downloads( 'edit' );

			if ( $downloads ) {
				$i = 1;
				foreach ( $downloads as $download ) {
					/* translators: %s: download number */
					$this->column_names[ 'downloads:id' . $i ] = sprintf( __( 'Download %d ID', 'woocommerce' ), $i );
					/* translators: %s: download number */
					$this->column_names[ 'downloads:name' . $i ] = sprintf( __( 'Download %d name', 'woocommerce' ), $i );
					/* translators: %s: download number */
					$this->column_names[ 'downloads:url' . $i ] = sprintf( __( 'Download %d URL', 'woocommerce' ), $i );
					$row[ 'downloads:id' . $i ]                 = $download->get_id();
					$row[ 'downloads:name' . $i ]               = $download->get_name();
					$row[ 'downloads:url' . $i ]                = $download->get_file();
					$i++;
				}
			}
		}
	}

	/**
	 * Export attributes data.
	 *
	 * @param WC_Product $product Product being exported.
	 * @param array      $row     Row being exported.
	 *
	 * @since 3.1.0
	 */
	protected function prepare_attributes_for_export( $product, &$row ) {
		if ( $this->is_column_exporting( 'attributes' ) ) {
			$attributes         = $product->get_attributes();
			$default_attributes = $product->get_default_attributes();

			if ( count( $attributes ) ) {
				$i = 1;
				foreach ( $attributes as $attribute_name => $attribute ) {
					/* translators: %s: attribute number */
					$this->column_names[ 'attributes:name' . $i ] = sprintf( __( 'Attribute %d name', 'woocommerce' ), $i );
					/* translators: %s: attribute number */
					$this->column_names[ 'attributes:value' . $i ] = sprintf( __( 'Attribute %d value(s)', 'woocommerce' ), $i );
					/* translators: %s: attribute number */
					$this->column_names[ 'attributes:visible' . $i ] = sprintf( __( 'Attribute %d visible', 'woocommerce' ), $i );
					/* translators: %s: attribute number */
					$this->column_names[ 'attributes:taxonomy' . $i ] = sprintf( __( 'Attribute %d global', 'woocommerce' ), $i );

					if ( is_a( $attribute, 'WC_Product_Attribute' ) ) {
						$row[ 'attributes:name' . $i ] = wc_attribute_label( $attribute->get_name(), $product );

						if ( $attribute->is_taxonomy() ) {
							$terms  = $attribute->get_terms();
							$values = array();

							foreach ( $terms as $term ) {
								$values[] = $term->name;
							}

							$row[ 'attributes:value' . $i ]    = $this->implode_values( $values );
							$row[ 'attributes:taxonomy' . $i ] = 1;
						} else {
							$row[ 'attributes:value' . $i ]    = $this->implode_values( $attribute->get_options() );
							$row[ 'attributes:taxonomy' . $i ] = 0;
						}

						$row[ 'attributes:visible' . $i ] = $attribute->get_visible();
					} else {
						$row[ 'attributes:name' . $i ] = wc_attribute_label( $attribute_name, $product );

						if ( 0 === strpos( $attribute_name, 'pa_' ) ) {
							$option_term = get_term_by( 'slug', $attribute, $attribute_name ); // @codingStandardsIgnoreLine.
							$row[ 'attributes:value' . $i ]    = $option_term && ! is_wp_error( $option_term ) ? str_replace( ',', '\\,', $option_term->name ) : str_replace( ',', '\\,', $attribute );
							$row[ 'attributes:taxonomy' . $i ] = 1;
						} else {
							$row[ 'attributes:value' . $i ]    = str_replace( ',', '\\,', $attribute );
							$row[ 'attributes:taxonomy' . $i ] = 0;
						}

						$row[ 'attributes:visible' . $i ] = '';
					}

					if ( $product->is_type( 'variable' ) && isset( $default_attributes[ sanitize_title( $attribute_name ) ] ) ) {
						/* translators: %s: attribute number */
						$this->column_names[ 'attributes:default' . $i ] = sprintf( __( 'Attribute %d default', 'woocommerce' ), $i );
						$default_value                                   = $default_attributes[ sanitize_title( $attribute_name ) ];

						if ( 0 === strpos( $attribute_name, 'pa_' ) ) {
							$option_term = get_term_by( 'slug', $default_value, $attribute_name ); // @codingStandardsIgnoreLine.
							$row[ 'attributes:default' . $i ] = $option_term && ! is_wp_error( $option_term ) ? $option_term->name : $default_value;
						} else {
							$row[ 'attributes:default' . $i ] = $default_value;
						}
					}
					$i++;
				}
			}
		}
	}

	/**
	 * Export meta data.
	 *
	 * @param WC_Product $product Product being exported.
	 * @param array      $row Row data.
	 *
	 * @since 3.1.0
	 */
	protected function prepare_meta_for_export( $product, &$row ) {
		if ( $this->enable_meta_export ) {
			$meta_data = $product->get_meta_data();

			if ( count( $meta_data ) ) {
				$meta_keys_to_skip = apply_filters( 'woocommerce_product_export_skip_meta_keys', array(), $product );

				$i = 1;
				foreach ( $meta_data as $meta ) {
					if ( in_array( $meta->key, $meta_keys_to_skip, true ) ) {
						continue;
					}

					// Allow 3rd parties to process the meta, e.g. to transform non-scalar values to scalar.
					$meta_value = apply_filters( 'woocommerce_product_export_meta_value', $meta->value, $meta, $product, $row );

					if ( ! is_scalar( $meta_value ) ) {
						continue;
					}

					$column_key = 'meta:' . esc_attr( $meta->key );
					/* translators: %s: meta data name */
					$this->column_names[ $column_key ] = sprintf( __( 'Meta: %s', 'woocommerce' ), $meta->key );
					$row[ $column_key ]                = $meta_value;
					$i ++;
				}
			}
		}
	}
}
class-wc-emails.php000064400000054750151336044770010266 0ustar00<?php
/**
 * Transactional Emails Controller
 *
 * WooCommerce Emails Class which handles the sending on transactional emails and email templates. This class loads in available emails.
 *
 * @package WooCommerce\Classes\Emails
 * @version 2.3.0
 */

use Automattic\Jetpack\Constants;

defined( 'ABSPATH' ) || exit;

/**
 * Emails class.
 */
class WC_Emails {

	/**
	 * Array of email notification classes
	 *
	 * @var WC_Email[]
	 */
	public $emails = array();

	/**
	 * The single instance of the class
	 *
	 * @var WC_Emails
	 */
	protected static $_instance = null;

	/**
	 * Background emailer class.
	 *
	 * @var WC_Background_Emailer
	 */
	protected static $background_emailer = null;

	/**
	 * Main WC_Emails Instance.
	 *
	 * Ensures only one instance of WC_Emails is loaded or can be loaded.
	 *
	 * @since 2.1
	 * @static
	 * @return WC_Emails Main instance
	 */
	public static function instance() {
		if ( is_null( self::$_instance ) ) {
			self::$_instance = new self();
		}
		return self::$_instance;
	}

	/**
	 * Cloning is forbidden.
	 *
	 * @since 2.1
	 */
	public function __clone() {
		wc_doing_it_wrong( __FUNCTION__, __( 'Cloning is forbidden.', 'woocommerce' ), '2.1' );
	}

	/**
	 * Unserializing instances of this class is forbidden.
	 *
	 * @since 2.1
	 */
	public function __wakeup() {
		wc_doing_it_wrong( __FUNCTION__, __( 'Unserializing instances of this class is forbidden.', 'woocommerce' ), '2.1' );
	}

	/**
	 * Hook in all transactional emails.
	 */
	public static function init_transactional_emails() {
		$email_actions = apply_filters(
			'woocommerce_email_actions',
			array(
				'woocommerce_low_stock',
				'woocommerce_no_stock',
				'woocommerce_product_on_backorder',
				'woocommerce_order_status_pending_to_processing',
				'woocommerce_order_status_pending_to_completed',
				'woocommerce_order_status_processing_to_cancelled',
				'woocommerce_order_status_pending_to_failed',
				'woocommerce_order_status_pending_to_on-hold',
				'woocommerce_order_status_failed_to_processing',
				'woocommerce_order_status_failed_to_completed',
				'woocommerce_order_status_failed_to_on-hold',
				'woocommerce_order_status_cancelled_to_processing',
				'woocommerce_order_status_cancelled_to_completed',
				'woocommerce_order_status_cancelled_to_on-hold',
				'woocommerce_order_status_on-hold_to_processing',
				'woocommerce_order_status_on-hold_to_cancelled',
				'woocommerce_order_status_on-hold_to_failed',
				'woocommerce_order_status_completed',
				'woocommerce_order_fully_refunded',
				'woocommerce_order_partially_refunded',
				'woocommerce_new_customer_note',
				'woocommerce_created_customer',
			)
		);

		if ( apply_filters( 'woocommerce_defer_transactional_emails', false ) ) {
			self::$background_emailer = new WC_Background_Emailer();

			foreach ( $email_actions as $action ) {
				add_action( $action, array( __CLASS__, 'queue_transactional_email' ), 10, 10 );
			}
		} else {
			foreach ( $email_actions as $action ) {
				add_action( $action, array( __CLASS__, 'send_transactional_email' ), 10, 10 );
			}
		}
	}

	/**
	 * Queues transactional email so it's not sent in current request if enabled,
	 * otherwise falls back to send now.
	 *
	 * @param mixed ...$args Optional arguments.
	 */
	public static function queue_transactional_email( ...$args ) {
		if ( is_a( self::$background_emailer, 'WC_Background_Emailer' ) ) {
			self::$background_emailer->push_to_queue(
				array(
					'filter' => current_filter(),
					'args'   => func_get_args(),
				)
			);
		} else {
			self::send_transactional_email( ...$args );
		}
	}

	/**
	 * Init the mailer instance and call the notifications for the current filter.
	 *
	 * @internal
	 *
	 * @param string $filter Filter name.
	 * @param array  $args Email args (default: []).
	 */
	public static function send_queued_transactional_email( $filter = '', $args = array() ) {
		if ( apply_filters( 'woocommerce_allow_send_queued_transactional_email', true, $filter, $args ) ) {
			self::instance(); // Init self so emails exist.

			// Ensure gateways are loaded in case they need to insert data into the emails.
			WC()->payment_gateways();
			WC()->shipping();

			do_action_ref_array( $filter . '_notification', $args );
		}
	}

	/**
	 * Init the mailer instance and call the notifications for the current filter.
	 *
	 * @internal
	 *
	 * @param array $args Email args (default: []).
	 */
	public static function send_transactional_email( $args = array() ) {
		try {
			$args = func_get_args();
			self::instance(); // Init self so emails exist.
			do_action_ref_array( current_filter() . '_notification', $args );
		} catch ( Exception $e ) {
			$error  = 'Transactional email triggered fatal error for callback ' . current_filter();
			$logger = wc_get_logger();
			$logger->critical(
				$error . PHP_EOL,
				array(
					'source' => 'transactional-emails',
				)
			);
			if ( Constants::is_true( 'WP_DEBUG' ) ) {
				trigger_error( $error, E_USER_WARNING ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
			}
		}
	}

	/**
	 * Constructor for the email class hooks in all emails that can be sent.
	 */
	public function __construct() {
		$this->init();

		// Email Header, Footer and content hooks.
		add_action( 'woocommerce_email_header', array( $this, 'email_header' ) );
		add_action( 'woocommerce_email_footer', array( $this, 'email_footer' ) );
		add_action( 'woocommerce_email_order_details', array( $this, 'order_downloads' ), 10, 4 );
		add_action( 'woocommerce_email_order_details', array( $this, 'order_details' ), 10, 4 );
		add_action( 'woocommerce_email_order_meta', array( $this, 'order_meta' ), 10, 3 );
		add_action( 'woocommerce_email_customer_details', array( $this, 'customer_details' ), 10, 3 );
		add_action( 'woocommerce_email_customer_details', array( $this, 'email_addresses' ), 20, 3 );

		// Hooks for sending emails during store events.
		add_action( 'woocommerce_low_stock_notification', array( $this, 'low_stock' ) );
		add_action( 'woocommerce_no_stock_notification', array( $this, 'no_stock' ) );
		add_action( 'woocommerce_product_on_backorder_notification', array( $this, 'backorder' ) );
		add_action( 'woocommerce_created_customer_notification', array( $this, 'customer_new_account' ), 10, 3 );

		// Hook for replacing {site_title} in email-footer.
		add_filter( 'woocommerce_email_footer_text', array( $this, 'replace_placeholders' ) );

		// Let 3rd parties unhook the above via this hook.
		do_action( 'woocommerce_email', $this );
	}

	/**
	 * Init email classes.
	 */
	public function init() {
		// Include email classes.
		include_once dirname( __FILE__ ) . '/emails/class-wc-email.php';

		$this->emails['WC_Email_New_Order']                 = include __DIR__ . '/emails/class-wc-email-new-order.php';
		$this->emails['WC_Email_Cancelled_Order']           = include __DIR__ . '/emails/class-wc-email-cancelled-order.php';
		$this->emails['WC_Email_Failed_Order']              = include __DIR__ . '/emails/class-wc-email-failed-order.php';
		$this->emails['WC_Email_Customer_On_Hold_Order']    = include __DIR__ . '/emails/class-wc-email-customer-on-hold-order.php';
		$this->emails['WC_Email_Customer_Processing_Order'] = include __DIR__ . '/emails/class-wc-email-customer-processing-order.php';
		$this->emails['WC_Email_Customer_Completed_Order']  = include __DIR__ . '/emails/class-wc-email-customer-completed-order.php';
		$this->emails['WC_Email_Customer_Refunded_Order']   = include __DIR__ . '/emails/class-wc-email-customer-refunded-order.php';
		$this->emails['WC_Email_Customer_Invoice']          = include __DIR__ . '/emails/class-wc-email-customer-invoice.php';
		$this->emails['WC_Email_Customer_Note']             = include __DIR__ . '/emails/class-wc-email-customer-note.php';
		$this->emails['WC_Email_Customer_Reset_Password']   = include __DIR__ . '/emails/class-wc-email-customer-reset-password.php';
		$this->emails['WC_Email_Customer_New_Account']      = include __DIR__ . '/emails/class-wc-email-customer-new-account.php';

		$this->emails = apply_filters( 'woocommerce_email_classes', $this->emails );
	}

	/**
	 * Return the email classes - used in admin to load settings.
	 *
	 * @return WC_Email[]
	 */
	public function get_emails() {
		return $this->emails;
	}

	/**
	 * Get from name for email.
	 *
	 * @return string
	 */
	public function get_from_name() {
		return wp_specialchars_decode( get_option( 'woocommerce_email_from_name' ), ENT_QUOTES );
	}

	/**
	 * Get from email address.
	 *
	 * @return string
	 */
	public function get_from_address() {
		return sanitize_email( get_option( 'woocommerce_email_from_address' ) );
	}

	/**
	 * Get the email header.
	 *
	 * @param mixed $email_heading Heading for the email.
	 */
	public function email_header( $email_heading ) {
		wc_get_template( 'emails/email-header.php', array( 'email_heading' => $email_heading ) );
	}

	/**
	 * Get the email footer.
	 */
	public function email_footer() {
		wc_get_template( 'emails/email-footer.php' );
	}

	/**
	 * Replace placeholder text in strings.
	 *
	 * @since  3.7.0
	 * @param  string $string Email footer text.
	 * @return string         Email footer text with any replacements done.
	 */
	public function replace_placeholders( $string ) {
		$domain = wp_parse_url( home_url(), PHP_URL_HOST );

		return str_replace(
			array(
				'{site_title}',
				'{site_address}',
				'{site_url}',
				'{woocommerce}',
				'{WooCommerce}',
			),
			array(
				$this->get_blogname(),
				$domain,
				$domain,
				'<a href="https://woocommerce.com">WooCommerce</a>',
				'<a href="https://woocommerce.com">WooCommerce</a>',
			),
			$string
		);
	}

	/**
	 * Filter callback to replace {site_title} in email footer
	 *
	 * @since  3.3.0
	 * @deprecated 3.7.0
	 * @param  string $string Email footer text.
	 * @return string         Email footer text with any replacements done.
	 */
	public function email_footer_replace_site_title( $string ) {
		wc_deprecated_function( 'WC_Emails::email_footer_replace_site_title', '3.7.0', 'WC_Emails::replace_placeholders' );
		return $this->replace_placeholders( $string );
	}

	/**
	 * Wraps a message in the woocommerce mail template.
	 *
	 * @param string $email_heading Heading text.
	 * @param string $message       Email message.
	 * @param bool   $plain_text    Set true to send as plain text. Default to false.
	 *
	 * @return string
	 */
	public function wrap_message( $email_heading, $message, $plain_text = false ) {
		// Buffer.
		ob_start();

		do_action( 'woocommerce_email_header', $email_heading, null );

		echo wpautop( wptexturize( $message ) ); // WPCS: XSS ok.

		do_action( 'woocommerce_email_footer', null );

		// Get contents.
		$message = ob_get_clean();

		return $message;
	}

	/**
	 * Send the email.
	 *
	 * @param mixed  $to          Receiver.
	 * @param mixed  $subject     Email subject.
	 * @param mixed  $message     Message.
	 * @param string $headers     Email headers (default: "Content-Type: text/html\r\n").
	 * @param string $attachments Attachments (default: "").
	 * @return bool
	 */
	public function send( $to, $subject, $message, $headers = "Content-Type: text/html\r\n", $attachments = '' ) {
		// Send.
		$email = new WC_Email();
		return $email->send( $to, $subject, $message, $headers, $attachments );
	}

	/**
	 * Prepare and send the customer invoice email on demand.
	 *
	 * @param int|WC_Order $order Order instance or ID.
	 */
	public function customer_invoice( $order ) {
		$email = $this->emails['WC_Email_Customer_Invoice'];

		if ( ! is_object( $order ) ) {
			$order = wc_get_order( absint( $order ) );
		}

		$email->trigger( $order->get_id(), $order );
	}

	/**
	 * Customer new account welcome email.
	 *
	 * @param int   $customer_id        Customer ID.
	 * @param array $new_customer_data  New customer data.
	 * @param bool  $password_generated If password is generated.
	 */
	public function customer_new_account( $customer_id, $new_customer_data = array(), $password_generated = false ) {
		if ( ! $customer_id ) {
			return;
		}

		$user_pass = ! empty( $new_customer_data['user_pass'] ) ? $new_customer_data['user_pass'] : '';

		$email = $this->emails['WC_Email_Customer_New_Account'];
		$email->trigger( $customer_id, $user_pass, $password_generated );
	}

	/**
	 * Show the order details table
	 *
	 * @param WC_Order $order         Order instance.
	 * @param bool     $sent_to_admin If should sent to admin.
	 * @param bool     $plain_text    If is plain text email.
	 * @param string   $email         Email address.
	 */
	public function order_details( $order, $sent_to_admin = false, $plain_text = false, $email = '' ) {
		if ( $plain_text ) {
			wc_get_template(
				'emails/plain/email-order-details.php',
				array(
					'order'         => $order,
					'sent_to_admin' => $sent_to_admin,
					'plain_text'    => $plain_text,
					'email'         => $email,
				)
			);
		} else {
			wc_get_template(
				'emails/email-order-details.php',
				array(
					'order'         => $order,
					'sent_to_admin' => $sent_to_admin,
					'plain_text'    => $plain_text,
					'email'         => $email,
				)
			);
		}
	}

	/**
	 * Show order downloads in a table.
	 *
	 * @since 3.2.0
	 * @param WC_Order $order         Order instance.
	 * @param bool     $sent_to_admin If should sent to admin.
	 * @param bool     $plain_text    If is plain text email.
	 * @param string   $email         Email address.
	 */
	public function order_downloads( $order, $sent_to_admin = false, $plain_text = false, $email = '' ) {
		$show_downloads = $order->has_downloadable_item() && $order->is_download_permitted() && ! $sent_to_admin && ! is_a( $email, 'WC_Email_Customer_Refunded_Order' );

		if ( ! $show_downloads ) {
			return;
		}

		$downloads = $order->get_downloadable_items();
		$columns   = apply_filters(
			'woocommerce_email_downloads_columns',
			array(
				'download-product' => __( 'Product', 'woocommerce' ),
				'download-expires' => __( 'Expires', 'woocommerce' ),
				'download-file'    => __( 'Download', 'woocommerce' ),
			)
		);

		if ( $plain_text ) {
			wc_get_template(
				'emails/plain/email-downloads.php',
				array(
					'order'         => $order,
					'sent_to_admin' => $sent_to_admin,
					'plain_text'    => $plain_text,
					'email'         => $email,
					'downloads'     => $downloads,
					'columns'       => $columns,
				)
			);
		} else {
			wc_get_template(
				'emails/email-downloads.php',
				array(
					'order'         => $order,
					'sent_to_admin' => $sent_to_admin,
					'plain_text'    => $plain_text,
					'email'         => $email,
					'downloads'     => $downloads,
					'columns'       => $columns,
				)
			);
		}
	}

	/**
	 * Add order meta to email templates.
	 *
	 * @param WC_Order $order         Order instance.
	 * @param bool     $sent_to_admin If should sent to admin.
	 * @param bool     $plain_text    If is plain text email.
	 */
	public function order_meta( $order, $sent_to_admin = false, $plain_text = false ) {
		$fields = apply_filters( 'woocommerce_email_order_meta_fields', array(), $sent_to_admin, $order );

		/**
		 * Deprecated woocommerce_email_order_meta_keys filter.
		 *
		 * @since 2.3.0
		 */
		$_fields = apply_filters( 'woocommerce_email_order_meta_keys', array(), $sent_to_admin );

		if ( $_fields ) {
			foreach ( $_fields as $key => $field ) {
				if ( is_numeric( $key ) ) {
					$key = $field;
				}

				$fields[ $key ] = array(
					'label' => wptexturize( $key ),
					'value' => wptexturize( get_post_meta( $order->get_id(), $field, true ) ),
				);
			}
		}

		if ( $fields ) {

			if ( $plain_text ) {

				foreach ( $fields as $field ) {
					if ( isset( $field['label'] ) && isset( $field['value'] ) && $field['value'] ) {
						echo $field['label'] . ': ' . $field['value'] . "\n"; // WPCS: XSS ok.
					}
				}
			} else {

				foreach ( $fields as $field ) {
					if ( isset( $field['label'] ) && isset( $field['value'] ) && $field['value'] ) {
						echo '<p><strong>' . $field['label'] . ':</strong> ' . $field['value'] . '</p>'; // WPCS: XSS ok.
					}
				}
			}
		}
	}

	/**
	 * Is customer detail field valid?
	 *
	 * @param  array $field Field data to check if is valid.
	 * @return boolean
	 */
	public function customer_detail_field_is_valid( $field ) {
		return isset( $field['label'] ) && ! empty( $field['value'] );
	}

	/**
	 * Allows developers to add additional customer details to templates.
	 *
	 * In versions prior to 3.2 this was used for notes, phone and email but this data has moved.
	 *
	 * @param WC_Order $order         Order instance.
	 * @param bool     $sent_to_admin If should sent to admin.
	 * @param bool     $plain_text    If is plain text email.
	 */
	public function customer_details( $order, $sent_to_admin = false, $plain_text = false ) {
		if ( ! is_a( $order, 'WC_Order' ) ) {
			return;
		}

		$fields = array_filter( apply_filters( 'woocommerce_email_customer_details_fields', array(), $sent_to_admin, $order ), array( $this, 'customer_detail_field_is_valid' ) );

		if ( ! empty( $fields ) ) {
			if ( $plain_text ) {
				wc_get_template( 'emails/plain/email-customer-details.php', array( 'fields' => $fields ) );
			} else {
				wc_get_template( 'emails/email-customer-details.php', array( 'fields' => $fields ) );
			}
		}
	}

	/**
	 * Get the email addresses.
	 *
	 * @param WC_Order $order         Order instance.
	 * @param bool     $sent_to_admin If should sent to admin.
	 * @param bool     $plain_text    If is plain text email.
	 */
	public function email_addresses( $order, $sent_to_admin = false, $plain_text = false ) {
		if ( ! is_a( $order, 'WC_Order' ) ) {
			return;
		}
		if ( $plain_text ) {
			wc_get_template(
				'emails/plain/email-addresses.php',
				array(
					'order'         => $order,
					'sent_to_admin' => $sent_to_admin,
				)
			);
		} else {
			wc_get_template(
				'emails/email-addresses.php',
				array(
					'order'         => $order,
					'sent_to_admin' => $sent_to_admin,
				)
			);
		}
	}

	/**
	 * Get blog name formatted for emails.
	 *
	 * @return string
	 */
	private function get_blogname() {
		return wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
	}

	/**
	 * Low stock notification email.
	 *
	 * @param WC_Product $product Product instance.
	 */
	public function low_stock( $product ) {
		if ( 'no' === get_option( 'woocommerce_notify_low_stock', 'yes' ) ) {
			return;
		}

		/**
		 * Determine if the current product should trigger a low stock notification
		 *
		 * @param int $product_id - The low stock product id
		 *
		 * @since 4.7.0
		 */
		if ( false === apply_filters( 'woocommerce_should_send_low_stock_notification', true, $product->get_id() ) ) {
			return;
		}

		$subject = sprintf( '[%s] %s', $this->get_blogname(), __( 'Product low in stock', 'woocommerce' ) );
		$message = sprintf(
			/* translators: 1: product name 2: items in stock */
			__( '%1$s is low in stock. There are %2$d left.', 'woocommerce' ),
			html_entity_decode( wp_strip_all_tags( $product->get_formatted_name() ), ENT_QUOTES, get_bloginfo( 'charset' ) ),
			html_entity_decode( wp_strip_all_tags( $product->get_stock_quantity() ) )
		);

		wp_mail(
			apply_filters( 'woocommerce_email_recipient_low_stock', get_option( 'woocommerce_stock_email_recipient' ), $product, null ),
			apply_filters( 'woocommerce_email_subject_low_stock', $subject, $product, null ),
			apply_filters( 'woocommerce_email_content_low_stock', $message, $product ),
			apply_filters( 'woocommerce_email_headers', '', 'low_stock', $product, null ),
			apply_filters( 'woocommerce_email_attachments', array(), 'low_stock', $product, null )
		);
	}

	/**
	 * No stock notification email.
	 *
	 * @param WC_Product $product Product instance.
	 */
	public function no_stock( $product ) {
		if ( 'no' === get_option( 'woocommerce_notify_no_stock', 'yes' ) ) {
			return;
		}

		/**
		 * Determine if the current product should trigger a no stock notification
		 *
		 * @param int $product_id - The out of stock product id
		 *
		 * @since 4.6.0
		 */
		if ( false === apply_filters( 'woocommerce_should_send_no_stock_notification', true, $product->get_id() ) ) {
			return;
		}

		$subject = sprintf( '[%s] %s', $this->get_blogname(), __( 'Product out of stock', 'woocommerce' ) );
		/* translators: %s: product name */
		$message = sprintf( __( '%s is out of stock.', 'woocommerce' ), html_entity_decode( wp_strip_all_tags( $product->get_formatted_name() ), ENT_QUOTES, get_bloginfo( 'charset' ) ) );

		wp_mail(
			apply_filters( 'woocommerce_email_recipient_no_stock', get_option( 'woocommerce_stock_email_recipient' ), $product, null ),
			apply_filters( 'woocommerce_email_subject_no_stock', $subject, $product, null ),
			apply_filters( 'woocommerce_email_content_no_stock', $message, $product ),
			apply_filters( 'woocommerce_email_headers', '', 'no_stock', $product, null ),
			apply_filters( 'woocommerce_email_attachments', array(), 'no_stock', $product, null )
		);
	}

	/**
	 * Backorder notification email.
	 *
	 * @param array $args Arguments.
	 */
	public function backorder( $args ) {
		$args = wp_parse_args(
			$args,
			array(
				'product'  => '',
				'quantity' => '',
				'order_id' => '',
			)
		);

		$order = wc_get_order( $args['order_id'] );
		if (
			! $args['product'] ||
			! is_object( $args['product'] ) ||
			! $args['quantity'] ||
			! $order
		) {
			return;
		}

		$subject = sprintf( '[%s] %s', $this->get_blogname(), __( 'Product backorder', 'woocommerce' ) );
		/* translators: 1: product quantity 2: product name 3: order number */
		$message = sprintf( __( '%1$s units of %2$s have been backordered in order #%3$s.', 'woocommerce' ), $args['quantity'], html_entity_decode( wp_strip_all_tags( $args['product']->get_formatted_name() ), ENT_QUOTES, get_bloginfo( 'charset' ) ), $order->get_order_number() );

		wp_mail(
			apply_filters( 'woocommerce_email_recipient_backorder', get_option( 'woocommerce_stock_email_recipient' ), $args, null ),
			apply_filters( 'woocommerce_email_subject_backorder', $subject, $args, null ),
			apply_filters( 'woocommerce_email_content_backorder', $message, $args ),
			apply_filters( 'woocommerce_email_headers', '', 'backorder', $args, null ),
			apply_filters( 'woocommerce_email_attachments', array(), 'backorder', $args, null )
		);
	}

	/**
	 * Adds Schema.org markup for order in JSON-LD format.
	 *
	 * @deprecated 3.0.0
	 * @see WC_Structured_Data::generate_order_data()
	 *
	 * @since 2.6.0
	 * @param WC_Order $order         Order instance.
	 * @param bool     $sent_to_admin If should sent to admin.
	 * @param bool     $plain_text    If is plain text email.
	 */
	public function order_schema_markup( $order, $sent_to_admin = false, $plain_text = false ) {
		wc_deprecated_function( 'WC_Emails::order_schema_markup', '3.0', 'WC_Structured_Data::generate_order_data' );

		WC()->structured_data->generate_order_data( $order, $sent_to_admin, $plain_text );
		WC()->structured_data->output_structured_data();
	}
}
wc-page-functions.php000064400000015657151336044770010636 0ustar00<?php
/**
 * WooCommerce Page Functions
 *
 * Functions related to pages and menus.
 *
 * @package  WooCommerce\Functions
 * @version  2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Replace a page title with the endpoint title.
 *
 * @param  string $title Post title.
 * @return string
 */
function wc_page_endpoint_title( $title ) {
	global $wp_query;

	if ( ! is_null( $wp_query ) && ! is_admin() && is_main_query() && in_the_loop() && is_page() && is_wc_endpoint_url() ) {
		$endpoint       = WC()->query->get_current_endpoint();
		$action         = isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : '';
		$endpoint_title = WC()->query->get_endpoint_title( $endpoint, $action );
		$title          = $endpoint_title ? $endpoint_title : $title;

		remove_filter( 'the_title', 'wc_page_endpoint_title' );
	}

	return $title;
}

add_filter( 'the_title', 'wc_page_endpoint_title' );

/**
 * Retrieve page ids - used for myaccount, edit_address, shop, cart, checkout, pay, view_order, terms. returns -1 if no page is found.
 *
 * @param string $page Page slug.
 * @return int
 */
function wc_get_page_id( $page ) {
	if ( 'pay' === $page || 'thanks' === $page ) {
		wc_deprecated_argument( __FUNCTION__, '2.1', 'The "pay" and "thanks" pages are no-longer used - an endpoint is added to the checkout instead. To get a valid link use the WC_Order::get_checkout_payment_url() or WC_Order::get_checkout_order_received_url() methods instead.' );

		$page = 'checkout';
	}
	if ( 'change_password' === $page || 'edit_address' === $page || 'lost_password' === $page ) {
		wc_deprecated_argument( __FUNCTION__, '2.1', 'The "change_password", "edit_address" and "lost_password" pages are no-longer used - an endpoint is added to the my-account instead. To get a valid link use the wc_customer_edit_account_url() function instead.' );

		$page = 'myaccount';
	}

	$page = apply_filters( 'woocommerce_get_' . $page . '_page_id', get_option( 'woocommerce_' . $page . '_page_id' ) );

	return $page ? absint( $page ) : -1;
}

/**
 * Retrieve page permalink.
 *
 * @param string      $page page slug.
 * @param string|bool $fallback Fallback URL if page is not set. Defaults to home URL. @since 3.4.0.
 * @return string
 */
function wc_get_page_permalink( $page, $fallback = null ) {
	$page_id   = wc_get_page_id( $page );
	$permalink = 0 < $page_id ? get_permalink( $page_id ) : '';

	if ( ! $permalink ) {
		$permalink = is_null( $fallback ) ? get_home_url() : $fallback;
	}

	return apply_filters( 'woocommerce_get_' . $page . '_page_permalink', $permalink );
}

/**
 * Get endpoint URL.
 *
 * Gets the URL for an endpoint, which varies depending on permalink settings.
 *
 * @param  string $endpoint  Endpoint slug.
 * @param  string $value     Query param value.
 * @param  string $permalink Permalink.
 *
 * @return string
 */
function wc_get_endpoint_url( $endpoint, $value = '', $permalink = '' ) {
	if ( ! $permalink ) {
		$permalink = get_permalink();
	}

	// Map endpoint to options.
	$query_vars = WC()->query->get_query_vars();
	$endpoint   = ! empty( $query_vars[ $endpoint ] ) ? $query_vars[ $endpoint ] : $endpoint;
	$value      = ( get_option( 'woocommerce_myaccount_edit_address_endpoint', 'edit-address' ) === $endpoint ) ? wc_edit_address_i18n( $value ) : $value;

	if ( get_option( 'permalink_structure' ) ) {
		if ( strstr( $permalink, '?' ) ) {
			$query_string = '?' . wp_parse_url( $permalink, PHP_URL_QUERY );
			$permalink    = current( explode( '?', $permalink ) );
		} else {
			$query_string = '';
		}
		$url = trailingslashit( $permalink );

		if ( $value ) {
			$url .= trailingslashit( $endpoint ) . user_trailingslashit( $value );
		} else {
			$url .= user_trailingslashit( $endpoint );
		}

		$url .= $query_string;
	} else {
		$url = add_query_arg( $endpoint, $value, $permalink );
	}

	return apply_filters( 'woocommerce_get_endpoint_url', $url, $endpoint, $value, $permalink );
}

/**
 * Hide menu items conditionally.
 *
 * @param array $items Navigation items.
 * @return array
 */
function wc_nav_menu_items( $items ) {
	if ( ! is_user_logged_in() ) {
		$customer_logout = get_option( 'woocommerce_logout_endpoint', 'customer-logout' );

		if ( ! empty( $customer_logout ) && ! empty( $items ) && is_array( $items ) ) {
			foreach ( $items as $key => $item ) {
				if ( empty( $item->url ) ) {
					continue;
				}
				$path  = wp_parse_url( $item->url, PHP_URL_PATH );
				$query = wp_parse_url( $item->url, PHP_URL_QUERY );

				if ( strstr( $path, $customer_logout ) || strstr( $query, $customer_logout ) ) {
					unset( $items[ $key ] );
				}
			}
		}
	}

	return $items;
}
add_filter( 'wp_nav_menu_objects', 'wc_nav_menu_items', 10 );


/**
 * Fix active class in nav for shop page.
 *
 * @param array $menu_items Menu items.
 * @return array
 */
function wc_nav_menu_item_classes( $menu_items ) {
	if ( ! is_woocommerce() ) {
		return $menu_items;
	}

	$shop_page      = wc_get_page_id( 'shop' );
	$page_for_posts = (int) get_option( 'page_for_posts' );

	if ( ! empty( $menu_items ) && is_array( $menu_items ) ) {
		foreach ( $menu_items as $key => $menu_item ) {
			$classes = (array) $menu_item->classes;
			$menu_id = (int) $menu_item->object_id;

			// Unset active class for blog page.
			if ( $page_for_posts === $menu_id ) {
				$menu_items[ $key ]->current = false;

				if ( in_array( 'current_page_parent', $classes, true ) ) {
					unset( $classes[ array_search( 'current_page_parent', $classes, true ) ] );
				}

				if ( in_array( 'current-menu-item', $classes, true ) ) {
					unset( $classes[ array_search( 'current-menu-item', $classes, true ) ] );
				}
			} elseif ( is_shop() && $shop_page === $menu_id && 'page' === $menu_item->object ) {
				// Set active state if this is the shop page link.
				$menu_items[ $key ]->current = true;
				$classes[]                   = 'current-menu-item';
				$classes[]                   = 'current_page_item';

			} elseif ( is_singular( 'product' ) && $shop_page === $menu_id ) {
				// Set parent state if this is a product page.
				$classes[] = 'current_page_parent';
			}

			$menu_items[ $key ]->classes = array_unique( $classes );
		}
	}

	return $menu_items;
}
add_filter( 'wp_nav_menu_objects', 'wc_nav_menu_item_classes', 2 );


/**
 * Fix active class in wp_list_pages for shop page.
 *
 * See details in https://github.com/woocommerce/woocommerce/issues/177.
 *
 * @param string $pages Pages list.
 * @return string
 */
function wc_list_pages( $pages ) {
	if ( ! is_woocommerce() ) {
		return $pages;
	}

	// Remove current_page_parent class from any item.
	$pages = str_replace( 'current_page_parent', '', $pages );
	// Find shop_page_id through woocommerce options.
	$shop_page = 'page-item-' . wc_get_page_id( 'shop' );

	if ( is_shop() ) {
		// Add current_page_item class to shop page.
		return str_replace( $shop_page, $shop_page . ' current_page_item', $pages );
	}

	// Add current_page_parent class to shop page.
	return str_replace( $shop_page, $shop_page . ' current_page_parent', $pages );
}
add_filter( 'wp_list_pages', 'wc_list_pages' );
customizer/class-wc-shop-customizer.php000064400000073257151336044770014376 0ustar00<?php
/**
 * Adds options to the customizer for WooCommerce.
 *
 * @version 3.3.0
 * @package WooCommerce
 */

defined( 'ABSPATH' ) || exit;

/**
 * WC_Shop_Customizer class.
 */
class WC_Shop_Customizer {

	/**
	 * Constructor.
	 */
	public function __construct() {
		add_action( 'customize_register', array( $this, 'add_sections' ) );
		add_action( 'customize_controls_print_styles', array( $this, 'add_styles' ) );
		add_action( 'customize_controls_print_scripts', array( $this, 'add_scripts' ), 30 );
		add_action( 'wp_enqueue_scripts', array( $this, 'add_frontend_scripts' ) );
		add_action( 'admin_menu', array( $this, 'add_fse_customize_link' ) );
	}

	/**
	 * Add settings to the customizer.
	 *
	 * @param WP_Customize_Manager $wp_customize Theme Customizer object.
	 */
	public function add_sections( $wp_customize ) {
		$wp_customize->add_panel(
			'woocommerce',
			array(
				'priority'       => 200,
				'capability'     => 'manage_woocommerce',
				'theme_supports' => '',
				'title'          => __( 'WooCommerce', 'woocommerce' ),
			)
		);

		$this->add_store_notice_section( $wp_customize );
		$this->add_product_catalog_section( $wp_customize );
		$this->add_product_images_section( $wp_customize );
		$this->add_checkout_section( $wp_customize );
	}

	/**
	 * Frontend CSS styles.
	 */
	public function add_frontend_scripts() {
		if ( ! is_customize_preview() || ! is_store_notice_showing() ) {
			return;
		}

		$css = '.woocommerce-store-notice, p.demo_store { display: block !important; }';
		wp_add_inline_style( 'customize-preview', $css );
	}

	/**
	 * CSS styles to improve our form.
	 */
	public function add_styles() {
		?>
		<style type="text/css">
			.woocommerce-cropping-control {
				margin: 0 40px 1em 0;
				padding: 0;
				display:inline-block;
				vertical-align: top;
			}

			.woocommerce-cropping-control input[type=radio] {
				margin-top: 1px;
			}

			.woocommerce-cropping-control span.woocommerce-cropping-control-aspect-ratio {
				margin-top: .5em;
				display:block;
			}

			.woocommerce-cropping-control span.woocommerce-cropping-control-aspect-ratio input {
				width: auto;
				display: inline-block;
			}
			<?php
			// For FSE themes hide the back button so we only surface WooCommerce options.
			if ( function_exists( 'gutenberg_is_fse_theme' ) && gutenberg_is_fse_theme() ) {
				?>
					#sub-accordion-panel-woocommerce .customize-panel-back{
						display: none;
					}
					#customize-controls #sub-accordion-panel-woocommerce .panel-meta.customize-info .accordion-section-title {
						margin-left: 0;
					}
				<?php
			}
			?>
		</style>
		<?php
	}

	/**
	 * Scripts to improve our form.
	 */
	public function add_scripts() {
		$min_rows    = wc_get_theme_support( 'product_grid::min_rows', 1 );
		$max_rows    = wc_get_theme_support( 'product_grid::max_rows', '' );
		$min_columns = wc_get_theme_support( 'product_grid::min_columns', 1 );
		$max_columns = wc_get_theme_support( 'product_grid::max_columns', '' );

		/* translators: %d: Setting value */
		$min_notice = __( 'The minimum allowed setting is %d', 'woocommerce' );
		/* translators: %d: Setting value */
		$max_notice = __( 'The maximum allowed setting is %d', 'woocommerce' );
		?>
		<script type="text/javascript">
			jQuery( function( $ ) {
				$( document.body ).on( 'change', '.woocommerce-cropping-control input[type="radio"]', function() {
					var $wrapper = $( this ).closest( '.woocommerce-cropping-control' ),
						value    = $wrapper.find( 'input:checked' ).val();

					if ( 'custom' === value ) {
						$wrapper.find( '.woocommerce-cropping-control-aspect-ratio' ).slideDown( 200 );
					} else {
						$wrapper.find( '.woocommerce-cropping-control-aspect-ratio' ).hide();
					}

					return false;
				} );

				wp.customize.bind( 'ready', function() { // Ready?
					$( '.woocommerce-cropping-control' ).find( 'input:checked' ).trigger( 'change' );
				} );

				wp.customize( 'woocommerce_demo_store', function( setting ) {
					setting.bind( function( value ) {
						var notice = wp.customize( 'woocommerce_demo_store_notice' );

						if ( value && ! notice.callbacks.has( notice.preview ) ) {
							notice.bind( notice.preview );
						} else if ( ! value ) {
							notice.unbind( notice.preview );
						}
					} );
				} );

				wp.customize( 'woocommerce_demo_store_notice', function( setting ) {
					setting.bind( function( value ) {
						var checkbox = wp.customize( 'woocommerce_demo_store' );

						if ( checkbox.get() ) {
							$( '.woocommerce-store-notice' ).text( value );
						}
					} );
				} );

				wp.customize.section( 'woocommerce_store_notice', function( section ) {
					section.expanded.bind( function( isExpanded ) {
						if ( isExpanded ) {
							var notice   = wp.customize( 'woocommerce_demo_store_notice' ),
								checkbox = wp.customize( 'woocommerce_demo_store' );

							if ( checkbox.get() && ! notice.callbacks.has( notice.preview ) ) {
								notice.bind( notice.preview );
							} else if ( ! checkbox.get() ) {
								notice.unbind( notice.preview );
							}
						}
					} );
				} );

				wp.customize.section( 'woocommerce_product_catalog', function( section ) {
					section.expanded.bind( function( isExpanded ) {
						if ( isExpanded ) {
							wp.customize.previewer.previewUrl.set( '<?php echo esc_js( wc_get_page_permalink( 'shop' ) ); ?>' );
						}
					} );
				} );

				wp.customize.section( 'woocommerce_product_images', function( section ) {
					section.expanded.bind( function( isExpanded ) {
						if ( isExpanded ) {
							wp.customize.previewer.previewUrl.set( '<?php echo esc_js( wc_get_page_permalink( 'shop' ) ); ?>' );
						}
					} );
				} );

				wp.customize.section( 'woocommerce_checkout', function( section ) {
					section.expanded.bind( function( isExpanded ) {
						if ( isExpanded ) {
							wp.customize.previewer.previewUrl.set( '<?php echo esc_js( wc_get_page_permalink( 'checkout' ) ); ?>' );
						}
					} );
				} );

				wp.customize( 'woocommerce_catalog_columns', function( setting ) {
					setting.bind( function( value ) {
						var min = parseInt( '<?php echo esc_js( $min_columns ); ?>', 10 );
						var max = parseInt( '<?php echo esc_js( $max_columns ); ?>', 10 );

						value = parseInt( value, 10 );

						if ( max && value > max ) {
							setting.notifications.add( 'max_columns_error', new wp.customize.Notification(
								'max_columns_error',
								{
									type   : 'error',
									message: '<?php echo esc_js( sprintf( $max_notice, $max_columns ) ); ?>'
								}
							) );
						} else {
							setting.notifications.remove( 'max_columns_error' );
						}

						if ( min && value < min ) {
							setting.notifications.add( 'min_columns_error', new wp.customize.Notification(
								'min_columns_error',
								{
									type   : 'error',
									message: '<?php echo esc_js( sprintf( $min_notice, $min_columns ) ); ?>'
								}
							) );
						} else {
							setting.notifications.remove( 'min_columns_error' );
						}
					} );
				} );

				wp.customize( 'woocommerce_catalog_rows', function( setting ) {
					setting.bind( function( value ) {
						var min = parseInt( '<?php echo esc_js( $min_rows ); ?>', 10 );
						var max = parseInt( '<?php echo esc_js( $max_rows ); ?>', 10 );

						value = parseInt( value, 10 );

						if ( max && value > max ) {
							setting.notifications.add( 'max_rows_error', new wp.customize.Notification(
								'max_rows_error',
								{
									type   : 'error',
									message: '<?php echo esc_js( sprintf( $max_notice, $max_rows ) ); ?>'
								}
							) );
						} else {
							setting.notifications.remove( 'max_rows_error' );
						}

						if ( min && value < min ) {
							setting.notifications.add( 'min_rows_error', new wp.customize.Notification(
								'min_rows_error',
								{
									type   : 'error',
									message: '<?php echo esc_js( sprintf( $min_notice, $min_rows ) ); ?>'
								}
							) );
						} else {
							setting.notifications.remove( 'min_rows_error' );
						}
					} );
				} );
			} );
		</script>
		<?php
	}

	/**
	 * For FSE themes add a "Customize WooCommerce" link to the Appearance menu.
	 *
	 * FSE themes hide the "Customize" link in the Appearance menu. In WooCommerce we have several options that can currently
	 * only be edited via the Customizer. For now, we are thus adding a new link for WooCommerce specific Customizer options.
	 */
	public function add_fse_customize_link() {

		// Exit early if the FSE theme feature isn't present or the current theme is not a FSE theme.
		if ( ! function_exists( 'gutenberg_is_fse_theme' ) || function_exists( 'gutenberg_is_fse_theme' ) && ! gutenberg_is_fse_theme() ) {
			return;
		}

		// Add a link to the WooCommerce panel in the Customizer.
		add_submenu_page(
			'themes.php',
			__( 'Customize WooCommerce', 'woocommerce' ),
			__( 'Customize WooCommerce', 'woocommerce' ),
			'edit_theme_options',
			admin_url( 'customize.php?autofocus[panel]=woocommerce' )
		);
	}

	/**
	 * Sanitize the shop page & category display setting.
	 *
	 * @param string $value '', 'subcategories', or 'both'.
	 * @return string
	 */
	public function sanitize_archive_display( $value ) {
		$options = array( '', 'subcategories', 'both' );

		return in_array( $value, $options, true ) ? $value : '';
	}

	/**
	 * Sanitize the catalog orderby setting.
	 *
	 * @param string $value An array key from the below array.
	 * @return string
	 */
	public function sanitize_default_catalog_orderby( $value ) {
		$options = apply_filters(
			'woocommerce_default_catalog_orderby_options',
			array(
				'menu_order' => __( 'Default sorting (custom ordering + name)', 'woocommerce' ),
				'popularity' => __( 'Popularity (sales)', 'woocommerce' ),
				'rating'     => __( 'Average rating', 'woocommerce' ),
				'date'       => __( 'Sort by most recent', 'woocommerce' ),
				'price'      => __( 'Sort by price (asc)', 'woocommerce' ),
				'price-desc' => __( 'Sort by price (desc)', 'woocommerce' ),
			)
		);

		return array_key_exists( $value, $options ) ? $value : 'menu_order';
	}

	/**
	 * Store notice section.
	 *
	 * @param WP_Customize_Manager $wp_customize Theme Customizer object.
	 */
	private function add_store_notice_section( $wp_customize ) {
		$wp_customize->add_section(
			'woocommerce_store_notice',
			array(
				'title'    => __( 'Store Notice', 'woocommerce' ),
				'priority' => 10,
				'panel'    => 'woocommerce',
			)
		);

		$wp_customize->add_setting(
			'woocommerce_demo_store',
			array(
				'default'              => 'no',
				'type'                 => 'option',
				'capability'           => 'manage_woocommerce',
				'sanitize_callback'    => 'wc_bool_to_string',
				'sanitize_js_callback' => 'wc_string_to_bool',
			)
		);

		$wp_customize->add_setting(
			'woocommerce_demo_store_notice',
			array(
				'default'           => __( 'This is a demo store for testing purposes &mdash; no orders shall be fulfilled.', 'woocommerce' ),
				'type'              => 'option',
				'capability'        => 'manage_woocommerce',
				'sanitize_callback' => 'wp_kses_post',
				'transport'         => 'postMessage',
			)
		);

		$wp_customize->add_control(
			'woocommerce_demo_store_notice',
			array(
				'label'       => __( 'Store notice', 'woocommerce' ),
				'description' => __( 'If enabled, this text will be shown site-wide. You can use it to show events or promotions to visitors!', 'woocommerce' ),
				'section'     => 'woocommerce_store_notice',
				'settings'    => 'woocommerce_demo_store_notice',
				'type'        => 'textarea',
			)
		);

		$wp_customize->add_control(
			'woocommerce_demo_store',
			array(
				'label'    => __( 'Enable store notice', 'woocommerce' ),
				'section'  => 'woocommerce_store_notice',
				'settings' => 'woocommerce_demo_store',
				'type'     => 'checkbox',
			)
		);

		if ( isset( $wp_customize->selective_refresh ) ) {
			$wp_customize->selective_refresh->add_partial(
				'woocommerce_demo_store_notice',
				array(
					'selector'            => '.woocommerce-store-notice',
					'container_inclusive' => true,
					'render_callback'     => 'woocommerce_demo_store',
				)
			);
		}
	}

	/**
	 * Product catalog section.
	 *
	 * @param WP_Customize_Manager $wp_customize Theme Customizer object.
	 */
	public function add_product_catalog_section( $wp_customize ) {
		$wp_customize->add_section(
			'woocommerce_product_catalog',
			array(
				'title'    => __( 'Product Catalog', 'woocommerce' ),
				'priority' => 10,
				'panel'    => 'woocommerce',
			)
		);

		$wp_customize->add_setting(
			'woocommerce_shop_page_display',
			array(
				'default'           => '',
				'type'              => 'option',
				'capability'        => 'manage_woocommerce',
				'sanitize_callback' => array( $this, 'sanitize_archive_display' ),
			)
		);

		$wp_customize->add_control(
			'woocommerce_shop_page_display',
			array(
				'label'       => __( 'Shop page display', 'woocommerce' ),
				'description' => __( 'Choose what to display on the main shop page.', 'woocommerce' ),
				'section'     => 'woocommerce_product_catalog',
				'settings'    => 'woocommerce_shop_page_display',
				'type'        => 'select',
				'choices'     => array(
					''              => __( 'Show products', 'woocommerce' ),
					'subcategories' => __( 'Show categories', 'woocommerce' ),
					'both'          => __( 'Show categories &amp; products', 'woocommerce' ),
				),
			)
		);

		$wp_customize->add_setting(
			'woocommerce_category_archive_display',
			array(
				'default'           => '',
				'type'              => 'option',
				'capability'        => 'manage_woocommerce',
				'sanitize_callback' => array( $this, 'sanitize_archive_display' ),
			)
		);

		$wp_customize->add_control(
			'woocommerce_category_archive_display',
			array(
				'label'       => __( 'Category display', 'woocommerce' ),
				'description' => __( 'Choose what to display on product category pages.', 'woocommerce' ),
				'section'     => 'woocommerce_product_catalog',
				'settings'    => 'woocommerce_category_archive_display',
				'type'        => 'select',
				'choices'     => array(
					''              => __( 'Show products', 'woocommerce' ),
					'subcategories' => __( 'Show subcategories', 'woocommerce' ),
					'both'          => __( 'Show subcategories &amp; products', 'woocommerce' ),
				),
			)
		);

		$wp_customize->add_setting(
			'woocommerce_default_catalog_orderby',
			array(
				'default'           => 'menu_order',
				'type'              => 'option',
				'capability'        => 'manage_woocommerce',
				'sanitize_callback' => array( $this, 'sanitize_default_catalog_orderby' ),
			)
		);

		$wp_customize->add_control(
			'woocommerce_default_catalog_orderby',
			array(
				'label'       => __( 'Default product sorting', 'woocommerce' ),
				'description' => __( 'How should products be sorted in the catalog by default?', 'woocommerce' ),
				'section'     => 'woocommerce_product_catalog',
				'settings'    => 'woocommerce_default_catalog_orderby',
				'type'        => 'select',
				'choices'     => apply_filters(
					'woocommerce_default_catalog_orderby_options',
					array(
						'menu_order' => __( 'Default sorting (custom ordering + name)', 'woocommerce' ),
						'popularity' => __( 'Popularity (sales)', 'woocommerce' ),
						'rating'     => __( 'Average rating', 'woocommerce' ),
						'date'       => __( 'Sort by most recent', 'woocommerce' ),
						'price'      => __( 'Sort by price (asc)', 'woocommerce' ),
						'price-desc' => __( 'Sort by price (desc)', 'woocommerce' ),
					)
				),
			)
		);

		// The following settings should be hidden if the theme is declaring the values.
		if ( has_filter( 'loop_shop_columns' ) ) {
			return;
		}

		$wp_customize->add_setting(
			'woocommerce_catalog_columns',
			array(
				'default'              => 4,
				'type'                 => 'option',
				'capability'           => 'manage_woocommerce',
				'sanitize_callback'    => 'absint',
				'sanitize_js_callback' => 'absint',
			)
		);

		$wp_customize->add_control(
			'woocommerce_catalog_columns',
			array(
				'label'       => __( 'Products per row', 'woocommerce' ),
				'description' => __( 'How many products should be shown per row?', 'woocommerce' ),
				'section'     => 'woocommerce_product_catalog',
				'settings'    => 'woocommerce_catalog_columns',
				'type'        => 'number',
				'input_attrs' => array(
					'min'  => wc_get_theme_support( 'product_grid::min_columns', 1 ),
					'max'  => wc_get_theme_support( 'product_grid::max_columns', '' ),
					'step' => 1,
				),
			)
		);

		// Only add this setting if something else isn't managing the number of products per page.
		if ( ! has_filter( 'loop_shop_per_page' ) ) {
			$wp_customize->add_setting(
				'woocommerce_catalog_rows',
				array(
					'default'              => 4,
					'type'                 => 'option',
					'capability'           => 'manage_woocommerce',
					'sanitize_callback'    => 'absint',
					'sanitize_js_callback' => 'absint',
				)
			);
		}

		$wp_customize->add_control(
			'woocommerce_catalog_rows',
			array(
				'label'       => __( 'Rows per page', 'woocommerce' ),
				'description' => __( 'How many rows of products should be shown per page?', 'woocommerce' ),
				'section'     => 'woocommerce_product_catalog',
				'settings'    => 'woocommerce_catalog_rows',
				'type'        => 'number',
				'input_attrs' => array(
					'min'  => wc_get_theme_support( 'product_grid::min_rows', 1 ),
					'max'  => wc_get_theme_support( 'product_grid::max_rows', '' ),
					'step' => 1,
				),
			)
		);
	}

	/**
	 * Product images section.
	 *
	 * @param WP_Customize_Manager $wp_customize Theme Customizer object.
	 */
	private function add_product_images_section( $wp_customize ) {
		if ( class_exists( 'Jetpack' ) && Jetpack::is_module_active( 'photon' ) ) {
			$regen_description = ''; // Nothing to report; Jetpack will handle magically.
		} elseif ( apply_filters( 'woocommerce_background_image_regeneration', true ) && ! is_multisite() ) {
			$regen_description = __( 'After publishing your changes, new image sizes will be generated automatically.', 'woocommerce' );
		} elseif ( apply_filters( 'woocommerce_background_image_regeneration', true ) && is_multisite() ) {
			/* translators: 1: tools URL 2: regen thumbs url */
			$regen_description = sprintf( __( 'After publishing your changes, new image sizes may not be shown until you regenerate thumbnails. You can do this from the <a href="%1$s" target="_blank">tools section in WooCommerce</a> or by using a plugin such as <a href="%2$s" target="_blank">Regenerate Thumbnails</a>.', 'woocommerce' ), admin_url( 'admin.php?page=wc-status&tab=tools' ), 'https://en-gb.wordpress.org/plugins/regenerate-thumbnails/' );
		} else {
			/* translators: %s: regen thumbs url */
			$regen_description = sprintf( __( 'After publishing your changes, new image sizes may not be shown until you <a href="%s" target="_blank">Regenerate Thumbnails</a>.', 'woocommerce' ), 'https://en-gb.wordpress.org/plugins/regenerate-thumbnails/' );
		}

		$wp_customize->add_section(
			'woocommerce_product_images',
			array(
				'title'       => __( 'Product Images', 'woocommerce' ),
				'description' => $regen_description,
				'priority'    => 20,
				'panel'       => 'woocommerce',
			)
		);

		if ( ! wc_get_theme_support( 'single_image_width' ) ) {
			$wp_customize->add_setting(
				'woocommerce_single_image_width',
				array(
					'default'              => 600,
					'type'                 => 'option',
					'capability'           => 'manage_woocommerce',
					'sanitize_callback'    => 'absint',
					'sanitize_js_callback' => 'absint',
				)
			);

			$wp_customize->add_control(
				'woocommerce_single_image_width',
				array(
					'label'       => __( 'Main image width', 'woocommerce' ),
					'description' => __( 'Image size used for the main image on single product pages. These images will remain uncropped.', 'woocommerce' ),
					'section'     => 'woocommerce_product_images',
					'settings'    => 'woocommerce_single_image_width',
					'type'        => 'number',
					'input_attrs' => array(
						'min'  => 0,
						'step' => 1,
					),
				)
			);
		}

		if ( ! wc_get_theme_support( 'thumbnail_image_width' ) ) {
			$wp_customize->add_setting(
				'woocommerce_thumbnail_image_width',
				array(
					'default'              => 300,
					'type'                 => 'option',
					'capability'           => 'manage_woocommerce',
					'sanitize_callback'    => 'absint',
					'sanitize_js_callback' => 'absint',
				)
			);

			$wp_customize->add_control(
				'woocommerce_thumbnail_image_width',
				array(
					'label'       => __( 'Thumbnail width', 'woocommerce' ),
					'description' => __( 'Image size used for products in the catalog.', 'woocommerce' ),
					'section'     => 'woocommerce_product_images',
					'settings'    => 'woocommerce_thumbnail_image_width',
					'type'        => 'number',
					'input_attrs' => array(
						'min'  => 0,
						'step' => 1,
					),
				)
			);
		}

		include_once WC_ABSPATH . 'includes/customizer/class-wc-customizer-control-cropping.php';

		$wp_customize->add_setting(
			'woocommerce_thumbnail_cropping',
			array(
				'default'           => '1:1',
				'type'              => 'option',
				'capability'        => 'manage_woocommerce',
				'sanitize_callback' => 'wc_clean',
			)
		);

		$wp_customize->add_setting(
			'woocommerce_thumbnail_cropping_custom_width',
			array(
				'default'              => '4',
				'type'                 => 'option',
				'capability'           => 'manage_woocommerce',
				'sanitize_callback'    => 'absint',
				'sanitize_js_callback' => 'absint',
			)
		);

		$wp_customize->add_setting(
			'woocommerce_thumbnail_cropping_custom_height',
			array(
				'default'              => '3',
				'type'                 => 'option',
				'capability'           => 'manage_woocommerce',
				'sanitize_callback'    => 'absint',
				'sanitize_js_callback' => 'absint',
			)
		);

		$wp_customize->add_control(
			new WC_Customizer_Control_Cropping(
				$wp_customize,
				'woocommerce_thumbnail_cropping',
				array(
					'section'  => 'woocommerce_product_images',
					'settings' => array(
						'cropping'      => 'woocommerce_thumbnail_cropping',
						'custom_width'  => 'woocommerce_thumbnail_cropping_custom_width',
						'custom_height' => 'woocommerce_thumbnail_cropping_custom_height',
					),
					'label'    => __( 'Thumbnail cropping', 'woocommerce' ),
					'choices'  => array(
						'1:1'       => array(
							'label'       => __( '1:1', 'woocommerce' ),
							'description' => __( 'Images will be cropped into a square', 'woocommerce' ),
						),
						'custom'    => array(
							'label'       => __( 'Custom', 'woocommerce' ),
							'description' => __( 'Images will be cropped to a custom aspect ratio', 'woocommerce' ),
						),
						'uncropped' => array(
							'label'       => __( 'Uncropped', 'woocommerce' ),
							'description' => __( 'Images will display using the aspect ratio in which they were uploaded', 'woocommerce' ),
						),
					),
				)
			)
		);
	}

	/**
	 * Checkout section.
	 *
	 * @param WP_Customize_Manager $wp_customize Theme Customizer object.
	 */
	public function add_checkout_section( $wp_customize ) {
		$wp_customize->add_section(
			'woocommerce_checkout',
			array(
				'title'       => __( 'Checkout', 'woocommerce' ),
				'priority'    => 20,
				'panel'       => 'woocommerce',
				'description' => __( 'These options let you change the appearance of the WooCommerce checkout.', 'woocommerce' ),
			)
		);

		// Checkout field controls.
		$fields = array(
			'company'   => __( 'Company name', 'woocommerce' ),
			'address_2' => __( 'Address line 2', 'woocommerce' ),
			'phone'     => __( 'Phone', 'woocommerce' ),
		);
		foreach ( $fields as $field => $label ) {
			$wp_customize->add_setting(
				'woocommerce_checkout_' . $field . '_field',
				array(
					'default'           => 'phone' === $field ? 'required' : 'optional',
					'type'              => 'option',
					'capability'        => 'manage_woocommerce',
					'sanitize_callback' => array( $this, 'sanitize_checkout_field_display' ),
				)
			);
			$wp_customize->add_control(
				'woocommerce_checkout_' . $field . '_field',
				array(
					/* Translators: %s field name. */
					'label'    => sprintf( __( '%s field', 'woocommerce' ), $label ),
					'section'  => 'woocommerce_checkout',
					'settings' => 'woocommerce_checkout_' . $field . '_field',
					'type'     => 'select',
					'choices'  => array(
						'hidden'   => __( 'Hidden', 'woocommerce' ),
						'optional' => __( 'Optional', 'woocommerce' ),
						'required' => __( 'Required', 'woocommerce' ),
					),
				)
			);
		}

		// Register settings.
		$wp_customize->add_setting(
			'woocommerce_checkout_highlight_required_fields',
			array(
				'default'              => 'yes',
				'type'                 => 'option',
				'capability'           => 'manage_woocommerce',
				'sanitize_callback'    => 'wc_bool_to_string',
				'sanitize_js_callback' => 'wc_string_to_bool',
			)
		);

		$wp_customize->add_setting(
			'woocommerce_checkout_terms_and_conditions_checkbox_text',
			array(
				/* translators: %s terms and conditions page name and link */
				'default'           => sprintf( __( 'I have read and agree to the website %s', 'woocommerce' ), '[terms]' ),
				'type'              => 'option',
				'capability'        => 'manage_woocommerce',
				'sanitize_callback' => 'wp_kses_post',
				'transport'         => 'postMessage',
			)
		);

		$wp_customize->add_setting(
			'woocommerce_checkout_privacy_policy_text',
			array(
				/* translators: %s privacy policy page name and link */
				'default'           => sprintf( __( 'Your personal data will be used to process your order, support your experience throughout this website, and for other purposes described in our %s.', 'woocommerce' ), '[privacy_policy]' ),
				'type'              => 'option',
				'capability'        => 'manage_woocommerce',
				'sanitize_callback' => 'wp_kses_post',
				'transport'         => 'postMessage',
			)
		);

		// Register controls.
		$wp_customize->add_control(
			'woocommerce_checkout_highlight_required_fields',
			array(
				'label'    => __( 'Highlight required fields with an asterisk', 'woocommerce' ),
				'section'  => 'woocommerce_checkout',
				'settings' => 'woocommerce_checkout_highlight_required_fields',
				'type'     => 'checkbox',
			)
		);

		if ( current_user_can( 'manage_privacy_options' ) ) {
			$choose_pages = array(
				'wp_page_for_privacy_policy' => __( 'Privacy policy', 'woocommerce' ),
				'woocommerce_terms_page_id'  => __( 'Terms and conditions', 'woocommerce' ),
			);
		} else {
			$choose_pages = array(
				'woocommerce_terms_page_id' => __( 'Terms and conditions', 'woocommerce' ),
			);
		}
		$pages        = get_pages(
			array(
				'post_type'   => 'page',
				'post_status' => 'publish,private,draft',
				'child_of'    => 0,
				'parent'      => -1,
				'exclude'     => array(
					wc_get_page_id( 'cart' ),
					wc_get_page_id( 'checkout' ),
					wc_get_page_id( 'myaccount' ),
				),
				'sort_order'  => 'asc',
				'sort_column' => 'post_title',
			)
		);
		$page_choices = array( '' => __( 'No page set', 'woocommerce' ) ) + array_combine( array_map( 'strval', wp_list_pluck( $pages, 'ID' ) ), wp_list_pluck( $pages, 'post_title' ) );

		foreach ( $choose_pages as $id => $name ) {
			$wp_customize->add_setting(
				$id,
				array(
					'default'    => '',
					'type'       => 'option',
					'capability' => 'manage_woocommerce',
				)
			);
			$wp_customize->add_control(
				$id,
				array(
					/* Translators: %s: page name. */
					'label'    => sprintf( __( '%s page', 'woocommerce' ), $name ),
					'section'  => 'woocommerce_checkout',
					'settings' => $id,
					'type'     => 'select',
					'choices'  => $page_choices,
				)
			);
		}

		$wp_customize->add_control(
			'woocommerce_checkout_privacy_policy_text',
			array(
				'label'           => __( 'Privacy policy', 'woocommerce' ),
				'description'     => __( 'Optionally add some text about your store privacy policy to show during checkout.', 'woocommerce' ),
				'section'         => 'woocommerce_checkout',
				'settings'        => 'woocommerce_checkout_privacy_policy_text',
				'active_callback' => array( $this, 'has_privacy_policy_page_id' ),
				'type'            => 'textarea',
			)
		);

		$wp_customize->add_control(
			'woocommerce_checkout_terms_and_conditions_checkbox_text',
			array(
				'label'           => __( 'Terms and conditions', 'woocommerce' ),
				'description'     => __( 'Optionally add some text for the terms checkbox that customers must accept.', 'woocommerce' ),
				'section'         => 'woocommerce_checkout',
				'settings'        => 'woocommerce_checkout_terms_and_conditions_checkbox_text',
				'active_callback' => array( $this, 'has_terms_and_conditions_page_id' ),
				'type'            => 'text',
			)
		);

		if ( isset( $wp_customize->selective_refresh ) ) {
			$wp_customize->selective_refresh->add_partial(
				'woocommerce_checkout_privacy_policy_text',
				array(
					'selector'            => '.woocommerce-privacy-policy-text',
					'container_inclusive' => true,
					'render_callback'     => 'wc_checkout_privacy_policy_text',
				)
			);
			$wp_customize->selective_refresh->add_partial(
				'woocommerce_checkout_terms_and_conditions_checkbox_text',
				array(
					'selector'            => '.woocommerce-terms-and-conditions-checkbox-text',
					'container_inclusive' => false,
					'render_callback'     => 'wc_terms_and_conditions_checkbox_text',
				)
			);
		}
	}

	/**
	 * Sanitize field display.
	 *
	 * @param string $value '', 'subcategories', or 'both'.
	 * @return string
	 */
	public function sanitize_checkout_field_display( $value ) {
		$options = array( 'hidden', 'optional', 'required' );
		return in_array( $value, $options, true ) ? $value : '';
	}

	/**
	 * Whether or not a page has been chose for the privacy policy.
	 *
	 * @return bool
	 */
	public function has_privacy_policy_page_id() {
		return wc_privacy_policy_page_id() > 0;
	}

	/**
	 * Whether or not a page has been chose for the terms and conditions.
	 *
	 * @return bool
	 */
	public function has_terms_and_conditions_page_id() {
		return wc_terms_and_conditions_page_id() > 0;
	}
}

new WC_Shop_Customizer();
customizer/class-wc-customizer-control-cropping.php000064400000003773151336044770016720 0ustar00<?php
/**
 * Custom control for radio buttons with nested options.
 *
 * Used for our image cropping settings.
 *
 * @version 3.3.0
 * @package WooCommerce
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC_Customizer_Control_Cropping class.
 */
class WC_Customizer_Control_Cropping extends WP_Customize_Control {

	/**
	 * Declare the control type.
	 *
	 * @var string
	 */
	public $type = 'woocommerce-cropping-control';

	/**
	 * Render control.
	 */
	public function render_content() {
		if ( empty( $this->choices ) ) {
			return;
		}

		$value         = $this->value( 'cropping' );
		$custom_width  = $this->value( 'custom_width' );
		$custom_height = $this->value( 'custom_height' );
		?>

		<span class="customize-control-title">
			<?php echo esc_html( $this->label ); ?>
		</span>

		<?php if ( ! empty( $this->description ) ) : ?>
			<span class="description customize-control-description"><?php echo esc_html( $this->description ); ?></span>
		<?php endif; ?>

		<ul id="input_<?php echo esc_attr( $this->id ); ?>" class="woocommerce-cropping-control">
			<?php foreach ( $this->choices as $key => $radio ) : ?>
				<li>
					<input type="radio" name="<?php echo esc_attr( $this->id ); ?>" value="<?php echo esc_attr( $key ); ?>" id="<?php echo esc_attr( $this->id . $key ); ?>" <?php $this->link( 'cropping' ); ?> <?php checked( $value, $key ); ?> />
					<label for="<?php echo esc_attr( $this->id . $key ); ?>"><?php echo esc_html( $radio['label'] ); ?><br/><span class="description"><?php echo esc_html( $radio['description'] ); ?></span></label>

					<?php if ( 'custom' === $key ) : ?>
						<span class="woocommerce-cropping-control-aspect-ratio">
							<input type="text" pattern="\d*" size="3" value="<?php echo esc_attr( $custom_width ); ?>" <?php $this->link( 'custom_width' ); ?> /> : <input type="text" pattern="\d*" size="3" value="<?php echo esc_attr( $custom_height ); ?>" <?php $this->link( 'custom_height' ); ?> />
						</span>
					<?php endif; ?>
				</li>
			<?php endforeach; ?>
		</ul>
		<?php
	}
}
wc-widget-functions.php000064400000004017151336044770011171 0ustar00<?php
/**
 * WooCommerce Widget Functions
 *
 * Widget related functions and widget registration.
 *
 * @package WooCommerce\Functions
 * @version 2.3.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// Include widget classes.
require_once dirname( __FILE__ ) . '/abstracts/abstract-wc-widget.php';
require_once dirname( __FILE__ ) . '/widgets/class-wc-widget-cart.php';
require_once dirname( __FILE__ ) . '/widgets/class-wc-widget-layered-nav-filters.php';
require_once dirname( __FILE__ ) . '/widgets/class-wc-widget-layered-nav.php';
require_once dirname( __FILE__ ) . '/widgets/class-wc-widget-price-filter.php';
require_once dirname( __FILE__ ) . '/widgets/class-wc-widget-product-categories.php';
require_once dirname( __FILE__ ) . '/widgets/class-wc-widget-product-search.php';
require_once dirname( __FILE__ ) . '/widgets/class-wc-widget-product-tag-cloud.php';
require_once dirname( __FILE__ ) . '/widgets/class-wc-widget-products.php';
require_once dirname( __FILE__ ) . '/widgets/class-wc-widget-rating-filter.php';
require_once dirname( __FILE__ ) . '/widgets/class-wc-widget-recent-reviews.php';
require_once dirname( __FILE__ ) . '/widgets/class-wc-widget-recently-viewed.php';
require_once dirname( __FILE__ ) . '/widgets/class-wc-widget-top-rated-products.php';

/**
 * Register Widgets.
 *
 * @since 2.3.0
 */
function wc_register_widgets() {
	register_widget( 'WC_Widget_Cart' );
	register_widget( 'WC_Widget_Layered_Nav_Filters' );
	register_widget( 'WC_Widget_Layered_Nav' );
	register_widget( 'WC_Widget_Price_Filter' );
	register_widget( 'WC_Widget_Product_Categories' );
	register_widget( 'WC_Widget_Product_Search' );
	register_widget( 'WC_Widget_Product_Tag_Cloud' );
	register_widget( 'WC_Widget_Products' );
	register_widget( 'WC_Widget_Recently_Viewed' );

	if ( 'yes' === get_option( 'woocommerce_enable_reviews', 'yes' ) ) {
		register_widget( 'WC_Widget_Top_Rated_Products' );
		register_widget( 'WC_Widget_Recent_Reviews' );
		register_widget( 'WC_Widget_Rating_Filter' );
	}
}
add_action( 'widgets_init', 'wc_register_widgets' );
wc-template-hooks.php000064400000030611151336044770010633 0ustar00<?php
/**
 * WooCommerce Template Hooks
 *
 * Action/filter hooks used for WooCommerce functions/templates.
 *
 * @package WooCommerce\Templates
 * @version 2.1.0
 */

defined( 'ABSPATH' ) || exit;

add_filter( 'body_class', 'wc_body_class' );
add_filter( 'post_class', 'wc_product_post_class', 20, 3 );

/**
 * WP Header.
 *
 * @see wc_generator_tag()
 */
add_filter( 'get_the_generator_html', 'wc_generator_tag', 10, 2 );
add_filter( 'get_the_generator_xhtml', 'wc_generator_tag', 10, 2 );

/**
 * Content Wrappers.
 *
 * @see woocommerce_output_content_wrapper()
 * @see woocommerce_output_content_wrapper_end()
 */
add_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper', 10 );
add_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end', 10 );

/**
 * Sale flashes.
 *
 * @see woocommerce_show_product_loop_sale_flash()
 * @see woocommerce_show_product_sale_flash()
 */
add_action( 'woocommerce_before_shop_loop_item_title', 'woocommerce_show_product_loop_sale_flash', 10 );
add_action( 'woocommerce_before_single_product_summary', 'woocommerce_show_product_sale_flash', 10 );

/**
 * Breadcrumbs.
 *
 * @see woocommerce_breadcrumb()
 */
add_action( 'woocommerce_before_main_content', 'woocommerce_breadcrumb', 20, 0 );

/**
 * Sidebar.
 *
 * @see woocommerce_get_sidebar()
 */
add_action( 'woocommerce_sidebar', 'woocommerce_get_sidebar', 10 );

/**
 * Archive descriptions.
 *
 * @see woocommerce_taxonomy_archive_description()
 * @see woocommerce_product_archive_description()
 */
add_action( 'woocommerce_archive_description', 'woocommerce_taxonomy_archive_description', 10 );
add_action( 'woocommerce_archive_description', 'woocommerce_product_archive_description', 10 );

/**
 * Product loop start.
 */
add_filter( 'woocommerce_product_loop_start', 'woocommerce_maybe_show_product_subcategories' );

/**
 * Products Loop.
 *
 * @see woocommerce_result_count()
 * @see woocommerce_catalog_ordering()
 */
add_action( 'woocommerce_before_shop_loop', 'woocommerce_result_count', 20 );
add_action( 'woocommerce_before_shop_loop', 'woocommerce_catalog_ordering', 30 );
add_action( 'woocommerce_no_products_found', 'wc_no_products_found' );

/**
 * Product Loop Items.
 *
 * @see woocommerce_template_loop_product_link_open()
 * @see woocommerce_template_loop_product_link_close()
 * @see woocommerce_template_loop_add_to_cart()
 * @see woocommerce_template_loop_product_thumbnail()
 * @see woocommerce_template_loop_product_title()
 * @see woocommerce_template_loop_category_link_open()
 * @see woocommerce_template_loop_category_title()
 * @see woocommerce_template_loop_category_link_close()
 * @see woocommerce_template_loop_price()
 * @see woocommerce_template_loop_rating()
 */
add_action( 'woocommerce_before_shop_loop_item', 'woocommerce_template_loop_product_link_open', 10 );
add_action( 'woocommerce_after_shop_loop_item', 'woocommerce_template_loop_product_link_close', 5 );
add_action( 'woocommerce_after_shop_loop_item', 'woocommerce_template_loop_add_to_cart', 10 );
add_action( 'woocommerce_before_shop_loop_item_title', 'woocommerce_template_loop_product_thumbnail', 10 );
add_action( 'woocommerce_shop_loop_item_title', 'woocommerce_template_loop_product_title', 10 );

add_action( 'woocommerce_before_subcategory', 'woocommerce_template_loop_category_link_open', 10 );
add_action( 'woocommerce_shop_loop_subcategory_title', 'woocommerce_template_loop_category_title', 10 );
add_action( 'woocommerce_after_subcategory', 'woocommerce_template_loop_category_link_close', 10 );

add_action( 'woocommerce_after_shop_loop_item_title', 'woocommerce_template_loop_price', 10 );
add_action( 'woocommerce_after_shop_loop_item_title', 'woocommerce_template_loop_rating', 5 );

/**
 * Subcategories.
 *
 * @see woocommerce_subcategory_thumbnail()
 */
add_action( 'woocommerce_before_subcategory_title', 'woocommerce_subcategory_thumbnail', 10 );

/**
 * Before Single Products Summary Div.
 *
 * @see woocommerce_show_product_images()
 * @see woocommerce_show_product_thumbnails()
 */
add_action( 'woocommerce_before_single_product_summary', 'woocommerce_show_product_images', 20 );
add_action( 'woocommerce_product_thumbnails', 'woocommerce_show_product_thumbnails', 20 );

/**
 * After Single Products Summary Div.
 *
 * @see woocommerce_output_product_data_tabs()
 * @see woocommerce_upsell_display()
 * @see woocommerce_output_related_products()
 */
add_action( 'woocommerce_after_single_product_summary', 'woocommerce_output_product_data_tabs', 10 );
add_action( 'woocommerce_after_single_product_summary', 'woocommerce_upsell_display', 15 );
add_action( 'woocommerce_after_single_product_summary', 'woocommerce_output_related_products', 20 );

/**
 * Product Summary Box.
 *
 * @see woocommerce_template_single_title()
 * @see woocommerce_template_single_rating()
 * @see woocommerce_template_single_price()
 * @see woocommerce_template_single_excerpt()
 * @see woocommerce_template_single_meta()
 * @see woocommerce_template_single_sharing()
 */
add_action( 'woocommerce_single_product_summary', 'woocommerce_template_single_title', 5 );
add_action( 'woocommerce_single_product_summary', 'woocommerce_template_single_rating', 10 );
add_action( 'woocommerce_single_product_summary', 'woocommerce_template_single_price', 10 );
add_action( 'woocommerce_single_product_summary', 'woocommerce_template_single_excerpt', 20 );
add_action( 'woocommerce_single_product_summary', 'woocommerce_template_single_meta', 40 );
add_action( 'woocommerce_single_product_summary', 'woocommerce_template_single_sharing', 50 );

/**
 * Reviews
 *
 * @see woocommerce_review_display_gravatar()
 * @see woocommerce_review_display_rating()
 * @see woocommerce_review_display_meta()
 * @see woocommerce_review_display_comment_text()
 */
add_action( 'woocommerce_review_before', 'woocommerce_review_display_gravatar', 10 );
add_action( 'woocommerce_review_before_comment_meta', 'woocommerce_review_display_rating', 10 );
add_action( 'woocommerce_review_meta', 'woocommerce_review_display_meta', 10 );
add_action( 'woocommerce_review_comment_text', 'woocommerce_review_display_comment_text', 10 );

/**
 * Product Add to cart.
 *
 * @see woocommerce_template_single_add_to_cart()
 * @see woocommerce_simple_add_to_cart()
 * @see woocommerce_grouped_add_to_cart()
 * @see woocommerce_variable_add_to_cart()
 * @see woocommerce_external_add_to_cart()
 * @see woocommerce_single_variation()
 * @see woocommerce_single_variation_add_to_cart_button()
 */
add_action( 'woocommerce_single_product_summary', 'woocommerce_template_single_add_to_cart', 30 );
add_action( 'woocommerce_simple_add_to_cart', 'woocommerce_simple_add_to_cart', 30 );
add_action( 'woocommerce_grouped_add_to_cart', 'woocommerce_grouped_add_to_cart', 30 );
add_action( 'woocommerce_variable_add_to_cart', 'woocommerce_variable_add_to_cart', 30 );
add_action( 'woocommerce_external_add_to_cart', 'woocommerce_external_add_to_cart', 30 );
add_action( 'woocommerce_single_variation', 'woocommerce_single_variation', 10 );
add_action( 'woocommerce_single_variation', 'woocommerce_single_variation_add_to_cart_button', 20 );

/**
 * Pagination after shop loops.
 *
 * @see woocommerce_pagination()
 */
add_action( 'woocommerce_after_shop_loop', 'woocommerce_pagination', 10 );

/**
 * Product page tabs.
 */
add_filter( 'woocommerce_product_tabs', 'woocommerce_default_product_tabs' );
add_filter( 'woocommerce_product_tabs', 'woocommerce_sort_product_tabs', 99 );

/**
 * Additional Information tab.
 *
 * @see wc_display_product_attributes()
 */
add_action( 'woocommerce_product_additional_information', 'wc_display_product_attributes', 10 );

/**
 * Checkout.
 *
 * @see woocommerce_checkout_login_form()
 * @see woocommerce_checkout_coupon_form()
 * @see woocommerce_order_review()
 * @see woocommerce_checkout_payment()
 * @see wc_checkout_privacy_policy_text()
 * @see wc_terms_and_conditions_page_content()
 * @see wc_get_pay_buttons()
 */
add_action( 'woocommerce_before_checkout_form', 'woocommerce_checkout_login_form', 10 );
add_action( 'woocommerce_before_checkout_form', 'woocommerce_checkout_coupon_form', 10 );
add_action( 'woocommerce_checkout_order_review', 'woocommerce_order_review', 10 );
add_action( 'woocommerce_checkout_order_review', 'woocommerce_checkout_payment', 20 );
add_action( 'woocommerce_checkout_terms_and_conditions', 'wc_checkout_privacy_policy_text', 20 );
add_action( 'woocommerce_checkout_terms_and_conditions', 'wc_terms_and_conditions_page_content', 30 );
add_action( 'woocommerce_checkout_before_customer_details', 'wc_get_pay_buttons', 30 );

/**
 * Cart widget
 */
add_action( 'woocommerce_widget_shopping_cart_buttons', 'woocommerce_widget_shopping_cart_button_view_cart', 10 );
add_action( 'woocommerce_widget_shopping_cart_buttons', 'woocommerce_widget_shopping_cart_proceed_to_checkout', 20 );
add_action( 'woocommerce_widget_shopping_cart_total', 'woocommerce_widget_shopping_cart_subtotal', 10 );

/**
 * Cart.
 *
 * @see woocommerce_cross_sell_display()
 * @see woocommerce_cart_totals()
 * @see wc_get_pay_buttons()
 * @see woocommerce_button_proceed_to_checkout()
 * @see wc_empty_cart_message()
 */
add_action( 'woocommerce_cart_collaterals', 'woocommerce_cross_sell_display' );
add_action( 'woocommerce_cart_collaterals', 'woocommerce_cart_totals', 10 );
add_action( 'woocommerce_proceed_to_checkout', 'wc_get_pay_buttons', 10 );
add_action( 'woocommerce_proceed_to_checkout', 'woocommerce_button_proceed_to_checkout', 20 );
add_action( 'woocommerce_cart_is_empty', 'wc_empty_cart_message', 10 );

/**
 * Footer.
 *
 * @see  wc_print_js()
 * @see woocommerce_demo_store()
 */
add_action( 'wp_footer', 'wc_print_js', 25 );
add_action( 'wp_footer', 'woocommerce_demo_store' );

/**
 * Order details.
 *
 * @see woocommerce_order_details_table()
 * @see woocommerce_order_again_button()
 */
add_action( 'woocommerce_view_order', 'woocommerce_order_details_table', 10 );
add_action( 'woocommerce_thankyou', 'woocommerce_order_details_table', 10 );
add_action( 'woocommerce_order_details_after_order_table', 'woocommerce_order_again_button' );

/**
 * Order downloads.
 *
 * @see woocommerce_order_downloads_table()
 */
add_action( 'woocommerce_available_downloads', 'woocommerce_order_downloads_table', 10 );

/**
 * Auth.
 *
 * @see woocommerce_output_auth_header()
 * @see woocommerce_output_auth_footer()
 */
add_action( 'woocommerce_auth_page_header', 'woocommerce_output_auth_header', 10 );
add_action( 'woocommerce_auth_page_footer', 'woocommerce_output_auth_footer', 10 );

/**
 * Comments.
 *
 * Disable Jetpack comments.
 */
add_filter( 'jetpack_comment_form_enabled_for_product', '__return_false' );

/**
 * My Account.
 */
add_action( 'woocommerce_account_navigation', 'woocommerce_account_navigation' );
add_action( 'woocommerce_account_content', 'woocommerce_account_content' );
add_action( 'woocommerce_account_orders_endpoint', 'woocommerce_account_orders' );
add_action( 'woocommerce_account_view-order_endpoint', 'woocommerce_account_view_order' );
add_action( 'woocommerce_account_downloads_endpoint', 'woocommerce_account_downloads' );
add_action( 'woocommerce_account_edit-address_endpoint', 'woocommerce_account_edit_address' );
add_action( 'woocommerce_account_payment-methods_endpoint', 'woocommerce_account_payment_methods' );
add_action( 'woocommerce_account_add-payment-method_endpoint', 'woocommerce_account_add_payment_method' );
add_action( 'woocommerce_account_edit-account_endpoint', 'woocommerce_account_edit_account' );
add_action( 'woocommerce_register_form', 'wc_registration_privacy_policy_text', 20 );

/**
 * Notices.
 */
add_action( 'woocommerce_cart_is_empty', 'woocommerce_output_all_notices', 5 );
add_action( 'woocommerce_shortcode_before_product_cat_loop', 'woocommerce_output_all_notices', 10 );
add_action( 'woocommerce_before_shop_loop', 'woocommerce_output_all_notices', 10 );
add_action( 'woocommerce_before_single_product', 'woocommerce_output_all_notices', 10 );
add_action( 'woocommerce_before_cart', 'woocommerce_output_all_notices', 10 );
add_action( 'woocommerce_before_checkout_form_cart_notices', 'woocommerce_output_all_notices', 10 );
add_action( 'woocommerce_before_checkout_form', 'woocommerce_output_all_notices', 10 );
add_action( 'woocommerce_account_content', 'woocommerce_output_all_notices', 5 );
add_action( 'woocommerce_before_customer_login_form', 'woocommerce_output_all_notices', 10 );
add_action( 'woocommerce_before_lost_password_form', 'woocommerce_output_all_notices', 10 );
add_action( 'before_woocommerce_pay', 'woocommerce_output_all_notices', 10 );
add_action( 'woocommerce_before_reset_password_form', 'woocommerce_output_all_notices', 10 );
class-wc-rest-authentication.php000064400000047024151336044770013002 0ustar00<?php
/**
 * REST API Authentication
 *
 * @package  WooCommerce\RestApi
 * @since    2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * REST API authentication class.
 */
class WC_REST_Authentication {

	/**
	 * Authentication error.
	 *
	 * @var WP_Error
	 */
	protected $error = null;

	/**
	 * Logged in user data.
	 *
	 * @var stdClass
	 */
	protected $user = null;

	/**
	 * Current auth method.
	 *
	 * @var string
	 */
	protected $auth_method = '';

	/**
	 * Initialize authentication actions.
	 */
	public function __construct() {
		add_filter( 'determine_current_user', array( $this, 'authenticate' ), 15 );
		add_filter( 'rest_authentication_errors', array( $this, 'authentication_fallback' ) );
		add_filter( 'rest_authentication_errors', array( $this, 'check_authentication_error' ), 15 );
		add_filter( 'rest_post_dispatch', array( $this, 'send_unauthorized_headers' ), 50 );
		add_filter( 'rest_pre_dispatch', array( $this, 'check_user_permissions' ), 10, 3 );
	}

	/**
	 * Check if is request to our REST API.
	 *
	 * @return bool
	 */
	protected function is_request_to_rest_api() {
		if ( empty( $_SERVER['REQUEST_URI'] ) ) {
			return false;
		}

		$rest_prefix = trailingslashit( rest_get_url_prefix() );
		$request_uri = esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) );

		// Check if the request is to the WC API endpoints.
		$woocommerce = ( false !== strpos( $request_uri, $rest_prefix . 'wc/' ) );

		// Allow third party plugins use our authentication methods.
		$third_party = ( false !== strpos( $request_uri, $rest_prefix . 'wc-' ) );

		return apply_filters( 'woocommerce_rest_is_request_to_rest_api', $woocommerce || $third_party );
	}

	/**
	 * Authenticate user.
	 *
	 * @param int|false $user_id User ID if one has been determined, false otherwise.
	 * @return int|false
	 */
	public function authenticate( $user_id ) {
		// Do not authenticate twice and check if is a request to our endpoint in the WP REST API.
		if ( ! empty( $user_id ) || ! $this->is_request_to_rest_api() ) {
			return $user_id;
		}

		if ( is_ssl() ) {
			$user_id = $this->perform_basic_authentication();
		}

		if ( $user_id ) {
			return $user_id;
		}

		return $this->perform_oauth_authentication();
	}

	/**
	 * Authenticate the user if authentication wasn't performed during the
	 * determine_current_user action.
	 *
	 * Necessary in cases where wp_get_current_user() is called before WooCommerce is loaded.
	 *
	 * @see https://github.com/woocommerce/woocommerce/issues/26847
	 *
	 * @param WP_Error|null|bool $error Error data.
	 * @return WP_Error|null|bool
	 */
	public function authentication_fallback( $error ) {
		if ( ! empty( $error ) ) {
			// Another plugin has already declared a failure.
			return $error;
		}
		if ( empty( $this->error ) && empty( $this->auth_method ) && empty( $this->user ) && 0 === get_current_user_id() ) {
			// Authentication hasn't occurred during `determine_current_user`, so check auth.
			$user_id = $this->authenticate( false );
			if ( $user_id ) {
				wp_set_current_user( $user_id );
				return true;
			}
		}
		return $error;
	}

	/**
	 * Check for authentication error.
	 *
	 * @param WP_Error|null|bool $error Error data.
	 * @return WP_Error|null|bool
	 */
	public function check_authentication_error( $error ) {
		// Pass through other errors.
		if ( ! empty( $error ) ) {
			return $error;
		}

		return $this->get_error();
	}

	/**
	 * Set authentication error.
	 *
	 * @param WP_Error $error Authentication error data.
	 */
	protected function set_error( $error ) {
		// Reset user.
		$this->user = null;

		$this->error = $error;
	}

	/**
	 * Get authentication error.
	 *
	 * @return WP_Error|null.
	 */
	protected function get_error() {
		return $this->error;
	}

	/**
	 * Basic Authentication.
	 *
	 * SSL-encrypted requests are not subject to sniffing or man-in-the-middle
	 * attacks, so the request can be authenticated by simply looking up the user
	 * associated with the given consumer key and confirming the consumer secret
	 * provided is valid.
	 *
	 * @return int|bool
	 */
	private function perform_basic_authentication() {
		$this->auth_method = 'basic_auth';
		$consumer_key      = '';
		$consumer_secret   = '';

		// If the $_GET parameters are present, use those first.
		if ( ! empty( $_GET['consumer_key'] ) && ! empty( $_GET['consumer_secret'] ) ) { // WPCS: CSRF ok.
			$consumer_key    = $_GET['consumer_key']; // WPCS: CSRF ok, sanitization ok.
			$consumer_secret = $_GET['consumer_secret']; // WPCS: CSRF ok, sanitization ok.
		}

		// If the above is not present, we will do full basic auth.
		if ( ! $consumer_key && ! empty( $_SERVER['PHP_AUTH_USER'] ) && ! empty( $_SERVER['PHP_AUTH_PW'] ) ) {
			$consumer_key    = $_SERVER['PHP_AUTH_USER']; // WPCS: CSRF ok, sanitization ok.
			$consumer_secret = $_SERVER['PHP_AUTH_PW']; // WPCS: CSRF ok, sanitization ok.
		}

		// Stop if don't have any key.
		if ( ! $consumer_key || ! $consumer_secret ) {
			return false;
		}

		// Get user data.
		$this->user = $this->get_user_data_by_consumer_key( $consumer_key );
		if ( empty( $this->user ) ) {
			return false;
		}

		// Validate user secret.
		if ( ! hash_equals( $this->user->consumer_secret, $consumer_secret ) ) { // @codingStandardsIgnoreLine
			$this->set_error( new WP_Error( 'woocommerce_rest_authentication_error', __( 'Consumer secret is invalid.', 'woocommerce' ), array( 'status' => 401 ) ) );

			return false;
		}

		return $this->user->user_id;
	}

	/**
	 * Parse the Authorization header into parameters.
	 *
	 * @since 3.0.0
	 *
	 * @param string $header Authorization header value (not including "Authorization: " prefix).
	 *
	 * @return array Map of parameter values.
	 */
	public function parse_header( $header ) {
		if ( 'OAuth ' !== substr( $header, 0, 6 ) ) {
			return array();
		}

		// From OAuth PHP library, used under MIT license.
		$params = array();
		if ( preg_match_all( '/(oauth_[a-z_-]*)=(:?"([^"]*)"|([^,]*))/', $header, $matches ) ) {
			foreach ( $matches[1] as $i => $h ) {
				$params[ $h ] = urldecode( empty( $matches[3][ $i ] ) ? $matches[4][ $i ] : $matches[3][ $i ] );
			}
			if ( isset( $params['realm'] ) ) {
				unset( $params['realm'] );
			}
		}

		return $params;
	}

	/**
	 * Get the authorization header.
	 *
	 * On certain systems and configurations, the Authorization header will be
	 * stripped out by the server or PHP. Typically this is then used to
	 * generate `PHP_AUTH_USER`/`PHP_AUTH_PASS` but not passed on. We use
	 * `getallheaders` here to try and grab it out instead.
	 *
	 * @since 3.0.0
	 *
	 * @return string Authorization header if set.
	 */
	public function get_authorization_header() {
		if ( ! empty( $_SERVER['HTTP_AUTHORIZATION'] ) ) {
			return wp_unslash( $_SERVER['HTTP_AUTHORIZATION'] ); // WPCS: sanitization ok.
		}

		if ( function_exists( 'getallheaders' ) ) {
			$headers = getallheaders();
			// Check for the authoization header case-insensitively.
			foreach ( $headers as $key => $value ) {
				if ( 'authorization' === strtolower( $key ) ) {
					return $value;
				}
			}
		}

		return '';
	}

	/**
	 * Get oAuth parameters from $_GET, $_POST or request header.
	 *
	 * @since 3.0.0
	 *
	 * @return array|WP_Error
	 */
	public function get_oauth_parameters() {
		$params = array_merge( $_GET, $_POST ); // WPCS: CSRF ok.
		$params = wp_unslash( $params );
		$header = $this->get_authorization_header();

		if ( ! empty( $header ) ) {
			// Trim leading spaces.
			$header        = trim( $header );
			$header_params = $this->parse_header( $header );

			if ( ! empty( $header_params ) ) {
				$params = array_merge( $params, $header_params );
			}
		}

		$param_names = array(
			'oauth_consumer_key',
			'oauth_timestamp',
			'oauth_nonce',
			'oauth_signature',
			'oauth_signature_method',
		);

		$errors   = array();
		$have_one = false;

		// Check for required OAuth parameters.
		foreach ( $param_names as $param_name ) {
			if ( empty( $params[ $param_name ] ) ) {
				$errors[] = $param_name;
			} else {
				$have_one = true;
			}
		}

		// All keys are missing, so we're probably not even trying to use OAuth.
		if ( ! $have_one ) {
			return array();
		}

		// If we have at least one supplied piece of data, and we have an error,
		// then it's a failed authentication.
		if ( ! empty( $errors ) ) {
			$message = sprintf(
				/* translators: %s: amount of errors */
				_n( 'Missing OAuth parameter %s', 'Missing OAuth parameters %s', count( $errors ), 'woocommerce' ),
				implode( ', ', $errors )
			);

			$this->set_error( new WP_Error( 'woocommerce_rest_authentication_missing_parameter', $message, array( 'status' => 401 ) ) );

			return array();
		}

		return $params;
	}

	/**
	 * Perform OAuth 1.0a "one-legged" (http://oauthbible.com/#oauth-10a-one-legged) authentication for non-SSL requests.
	 *
	 * This is required so API credentials cannot be sniffed or intercepted when making API requests over plain HTTP.
	 *
	 * This follows the spec for simple OAuth 1.0a authentication (RFC 5849) as closely as possible, with two exceptions:
	 *
	 * 1) There is no token associated with request/responses, only consumer keys/secrets are used.
	 *
	 * 2) The OAuth parameters are included as part of the request query string instead of part of the Authorization header,
	 *    This is because there is no cross-OS function within PHP to get the raw Authorization header.
	 *
	 * @link http://tools.ietf.org/html/rfc5849 for the full spec.
	 *
	 * @return int|bool
	 */
	private function perform_oauth_authentication() {
		$this->auth_method = 'oauth1';

		$params = $this->get_oauth_parameters();
		if ( empty( $params ) ) {
			return false;
		}

		// Fetch WP user by consumer key.
		$this->user = $this->get_user_data_by_consumer_key( $params['oauth_consumer_key'] );

		if ( empty( $this->user ) ) {
			$this->set_error( new WP_Error( 'woocommerce_rest_authentication_error', __( 'Consumer key is invalid.', 'woocommerce' ), array( 'status' => 401 ) ) );

			return false;
		}

		// Perform OAuth validation.
		$signature = $this->check_oauth_signature( $this->user, $params );
		if ( is_wp_error( $signature ) ) {
			$this->set_error( $signature );
			return false;
		}

		$timestamp_and_nonce = $this->check_oauth_timestamp_and_nonce( $this->user, $params['oauth_timestamp'], $params['oauth_nonce'] );
		if ( is_wp_error( $timestamp_and_nonce ) ) {
			$this->set_error( $timestamp_and_nonce );
			return false;
		}

		return $this->user->user_id;
	}

	/**
	 * Verify that the consumer-provided request signature matches our generated signature,
	 * this ensures the consumer has a valid key/secret.
	 *
	 * @param stdClass $user   User data.
	 * @param array    $params The request parameters.
	 * @return true|WP_Error
	 */
	private function check_oauth_signature( $user, $params ) {
		$http_method  = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( $_SERVER['REQUEST_METHOD'] ) : ''; // WPCS: sanitization ok.
		$request_path = isset( $_SERVER['REQUEST_URI'] ) ? wp_parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ) : ''; // WPCS: sanitization ok.
		$wp_base      = get_home_url( null, '/', 'relative' );
		if ( substr( $request_path, 0, strlen( $wp_base ) ) === $wp_base ) {
			$request_path = substr( $request_path, strlen( $wp_base ) );
		}
		$base_request_uri = rawurlencode( get_home_url( null, $request_path, is_ssl() ? 'https' : 'http' ) );

		// Get the signature provided by the consumer and remove it from the parameters prior to checking the signature.
		$consumer_signature = rawurldecode( str_replace( ' ', '+', $params['oauth_signature'] ) );
		unset( $params['oauth_signature'] );

		// Sort parameters.
		if ( ! uksort( $params, 'strcmp' ) ) {
			return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid signature - failed to sort parameters.', 'woocommerce' ), array( 'status' => 401 ) );
		}

		// Normalize parameter key/values.
		$params         = $this->normalize_parameters( $params );
		$query_string   = implode( '%26', $this->join_with_equals_sign( $params ) ); // Join with ampersand.
		$string_to_sign = $http_method . '&' . $base_request_uri . '&' . $query_string;

		if ( 'HMAC-SHA1' !== $params['oauth_signature_method'] && 'HMAC-SHA256' !== $params['oauth_signature_method'] ) {
			return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid signature - signature method is invalid.', 'woocommerce' ), array( 'status' => 401 ) );
		}

		$hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) );
		$secret         = $user->consumer_secret . '&';
		$signature      = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $secret, true ) );

		if ( ! hash_equals( $signature, $consumer_signature ) ) { // @codingStandardsIgnoreLine
			return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid signature - provided signature does not match.', 'woocommerce' ), array( 'status' => 401 ) );
		}

		return true;
	}

	/**
	 * Creates an array of urlencoded strings out of each array key/value pairs.
	 *
	 * @param  array  $params       Array of parameters to convert.
	 * @param  array  $query_params Array to extend.
	 * @param  string $key          Optional Array key to append.
	 * @return string               Array of urlencoded strings.
	 */
	private function join_with_equals_sign( $params, $query_params = array(), $key = '' ) {
		foreach ( $params as $param_key => $param_value ) {
			if ( $key ) {
				$param_key = $key . '%5B' . $param_key . '%5D'; // Handle multi-dimensional array.
			}

			if ( is_array( $param_value ) ) {
				$query_params = $this->join_with_equals_sign( $param_value, $query_params, $param_key );
			} else {
				$string         = $param_key . '=' . $param_value; // Join with equals sign.
				$query_params[] = wc_rest_urlencode_rfc3986( $string );
			}
		}

		return $query_params;
	}

	/**
	 * Normalize each parameter by assuming each parameter may have already been
	 * encoded, so attempt to decode, and then re-encode according to RFC 3986.
	 *
	 * Note both the key and value is normalized so a filter param like:
	 *
	 * 'filter[period]' => 'week'
	 *
	 * is encoded to:
	 *
	 * 'filter%255Bperiod%255D' => 'week'
	 *
	 * This conforms to the OAuth 1.0a spec which indicates the entire query string
	 * should be URL encoded.
	 *
	 * @see rawurlencode()
	 * @param array $parameters Un-normalized parameters.
	 * @return array Normalized parameters.
	 */
	private function normalize_parameters( $parameters ) {
		$keys       = wc_rest_urlencode_rfc3986( array_keys( $parameters ) );
		$values     = wc_rest_urlencode_rfc3986( array_values( $parameters ) );
		$parameters = array_combine( $keys, $values );

		return $parameters;
	}

	/**
	 * Verify that the timestamp and nonce provided with the request are valid. This prevents replay attacks where
	 * an attacker could attempt to re-send an intercepted request at a later time.
	 *
	 * - A timestamp is valid if it is within 15 minutes of now.
	 * - A nonce is valid if it has not been used within the last 15 minutes.
	 *
	 * @param stdClass $user      User data.
	 * @param int      $timestamp The unix timestamp for when the request was made.
	 * @param string   $nonce     A unique (for the given user) 32 alphanumeric string, consumer-generated.
	 * @return bool|WP_Error
	 */
	private function check_oauth_timestamp_and_nonce( $user, $timestamp, $nonce ) {
		global $wpdb;

		$valid_window = 15 * 60; // 15 minute window.

		if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) ) {
			return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid timestamp.', 'woocommerce' ), array( 'status' => 401 ) );
		}

		$used_nonces = maybe_unserialize( $user->nonces );

		if ( empty( $used_nonces ) ) {
			$used_nonces = array();
		}

		if ( in_array( $nonce, $used_nonces, true ) ) {
			return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid nonce - nonce has already been used.', 'woocommerce' ), array( 'status' => 401 ) );
		}

		$used_nonces[ $timestamp ] = $nonce;

		// Remove expired nonces.
		foreach ( $used_nonces as $nonce_timestamp => $nonce ) {
			if ( $nonce_timestamp < ( time() - $valid_window ) ) {
				unset( $used_nonces[ $nonce_timestamp ] );
			}
		}

		$used_nonces = maybe_serialize( $used_nonces );

		$wpdb->update(
			$wpdb->prefix . 'woocommerce_api_keys',
			array( 'nonces' => $used_nonces ),
			array( 'key_id' => $user->key_id ),
			array( '%s' ),
			array( '%d' )
		);

		return true;
	}

	/**
	 * Return the user data for the given consumer_key.
	 *
	 * @param string $consumer_key Consumer key.
	 * @return array
	 */
	private function get_user_data_by_consumer_key( $consumer_key ) {
		global $wpdb;

		$consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) );
		$user         = $wpdb->get_row(
			$wpdb->prepare(
				"
			SELECT key_id, user_id, permissions, consumer_key, consumer_secret, nonces
			FROM {$wpdb->prefix}woocommerce_api_keys
			WHERE consumer_key = %s
		",
				$consumer_key
			)
		);

		return $user;
	}

	/**
	 * Check that the API keys provided have the proper key-specific permissions to either read or write API resources.
	 *
	 * @param string $method Request method.
	 * @return bool|WP_Error
	 */
	private function check_permissions( $method ) {
		$permissions = $this->user->permissions;

		switch ( $method ) {
			case 'HEAD':
			case 'GET':
				if ( 'read' !== $permissions && 'read_write' !== $permissions ) {
					return new WP_Error( 'woocommerce_rest_authentication_error', __( 'The API key provided does not have read permissions.', 'woocommerce' ), array( 'status' => 401 ) );
				}
				break;
			case 'POST':
			case 'PUT':
			case 'PATCH':
			case 'DELETE':
				if ( 'write' !== $permissions && 'read_write' !== $permissions ) {
					return new WP_Error( 'woocommerce_rest_authentication_error', __( 'The API key provided does not have write permissions.', 'woocommerce' ), array( 'status' => 401 ) );
				}
				break;
			case 'OPTIONS':
				return true;

			default:
				return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Unknown request method.', 'woocommerce' ), array( 'status' => 401 ) );
		}

		return true;
	}

	/**
	 * Updated API Key last access datetime.
	 */
	private function update_last_access() {
		global $wpdb;

		$wpdb->update(
			$wpdb->prefix . 'woocommerce_api_keys',
			array( 'last_access' => current_time( 'mysql' ) ),
			array( 'key_id' => $this->user->key_id ),
			array( '%s' ),
			array( '%d' )
		);
	}

	/**
	 * If the consumer_key and consumer_secret $_GET parameters are NOT provided
	 * and the Basic auth headers are either not present or the consumer secret does not match the consumer
	 * key provided, then return the correct Basic headers and an error message.
	 *
	 * @param WP_REST_Response $response Current response being served.
	 * @return WP_REST_Response
	 */
	public function send_unauthorized_headers( $response ) {
		if ( is_wp_error( $this->get_error() ) && 'basic_auth' === $this->auth_method ) {
			$auth_message = __( 'WooCommerce API. Use a consumer key in the username field and a consumer secret in the password field.', 'woocommerce' );
			$response->header( 'WWW-Authenticate', 'Basic realm="' . $auth_message . '"', true );
		}

		return $response;
	}

	/**
	 * Check for user permissions and register last access.
	 *
	 * @param mixed           $result  Response to replace the requested version with.
	 * @param WP_REST_Server  $server  Server instance.
	 * @param WP_REST_Request $request Request used to generate the response.
	 * @return mixed
	 */
	public function check_user_permissions( $result, $server, $request ) {
		if ( $this->user ) {
			// Check API Key permissions.
			$allowed = $this->check_permissions( $request->get_method() );
			if ( is_wp_error( $allowed ) ) {
				return $allowed;
			}

			// Register last access.
			$this->update_last_access();
		}

		return $result;
	}
}

new WC_REST_Authentication();
class-wc-rate-limiter.php000064400000004130151336044770011375 0ustar00<?php
/**
 * Provide basic rate limiting functionality via WP Options API.
 *
 * Currently only provides a simple limit by delaying action by X seconds.
 *
 * Example usage:
 *
 * When an action runs, call set_rate_limit, e.g.:
 *
 * WC_Rate_Limiter::set_rate_limit( "{$my_action_name}_{$user_id}", $delay );
 *
 * This sets a timestamp for future timestamp after which action can run again.
 *
 *
 * Then before running the action again, check if the action is allowed to run, e.g.:
 *
 * if ( WC_Rate_Limiter::retried_too_soon( "{$my_action_name}_{$user_id}" ) ) {
 *     add_notice( 'Sorry, too soon!' );
 * }
 *
 * @package WooCommerce\Classes
 * @version 3.9.0
 * @since   3.9.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Rate limit class.
 */
class WC_Rate_Limiter {

	/**
	 * Constructs Option name from action identifier.
	 *
	 * @param string $action_id Identifier of the action.
	 * @return string
	 */
	public static function storage_id( $action_id ) {
		return 'woocommerce_rate_limit_' . $action_id;
	}

	/**
	 * Returns true if the action is not allowed to be run by the rate limiter yet, false otherwise.
	 *
	 * @param string $action_id Identifier of the action.
	 * @return bool
	 */
	public static function retried_too_soon( $action_id ) {
		$next_try_allowed_at = get_option( self::storage_id( $action_id ) );

		// No record of action running, so action is allowed to run.
		if ( false === $next_try_allowed_at ) {
			return false;
		}

		// Before the next run is allowed, retry forbidden.
		if ( time() <= $next_try_allowed_at ) {
			return true;
		}

		// After the next run is allowed, retry allowed.
		return false;
	}

	/**
	 * Sets the rate limit delay in seconds for action with identifier $id.
	 *
	 * @param string $action_id Identifier of the action.
	 * @param int    $delay Delay in seconds.
	 * @return bool True if the option setting was successful, false otherwise.
	 */
	public static function set_rate_limit( $action_id, $delay ) {
		$option_name         = self::storage_id( $action_id );
		$next_try_allowed_at = time() + $delay;
		return update_option( $option_name, $next_try_allowed_at );
	}
}
class-wc-order-item.php000064400000025264151336044770011061 0ustar00<?php
/**
 * Order Item
 *
 * A class which represents an item within an order and handles CRUD.
 * Uses ArrayAccess to be BW compatible with WC_Orders::get_items().
 *
 * @package WooCommerce\Classes
 * @version 3.0.0
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Order item class.
 */
class WC_Order_Item extends WC_Data implements ArrayAccess {

	/**
	 * Order Data array. This is the core order data exposed in APIs since 3.0.0.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $data = array(
		'order_id' => 0,
		'name'     => '',
	);

	/**
	 * Stores meta in cache for future reads.
	 * A group must be set to to enable caching.
	 *
	 * @var string
	 */
	protected $cache_group = 'order-items';

	/**
	 * Meta type. This should match up with
	 * the types available at https://developer.wordpress.org/reference/functions/add_metadata/.
	 * WP defines 'post', 'user', 'comment', and 'term'.
	 *
	 * @var string
	 */
	protected $meta_type = 'order_item';

	/**
	 * This is the name of this object type.
	 *
	 * @var string
	 */
	protected $object_type = 'order_item';

	/**
	 * Constructor.
	 *
	 * @param int|object|array $item ID to load from the DB, or WC_Order_Item object.
	 */
	public function __construct( $item = 0 ) {
		parent::__construct( $item );

		if ( $item instanceof WC_Order_Item ) {
			$this->set_id( $item->get_id() );
		} elseif ( is_numeric( $item ) && $item > 0 ) {
			$this->set_id( $item );
		} else {
			$this->set_object_read( true );
		}

		$type             = 'line_item' === $this->get_type() ? 'product' : $this->get_type();
		$this->data_store = WC_Data_Store::load( 'order-item-' . $type );
		if ( $this->get_id() > 0 ) {
			$this->data_store->read( $this );
		}
	}

	/**
	 * Merge changes with data and clear.
	 * Overrides WC_Data::apply_changes.
	 * array_replace_recursive does not work well for order items because it merges taxes instead
	 * of replacing them.
	 *
	 * @since 3.2.0
	 */
	public function apply_changes() {
		if ( function_exists( 'array_replace' ) ) {
			$this->data = array_replace( $this->data, $this->changes ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.array_replaceFound
		} else { // PHP 5.2 compatibility.
			foreach ( $this->changes as $key => $change ) {
				$this->data[ $key ] = $change;
			}
		}
		$this->changes = array();
	}

	/*
	|--------------------------------------------------------------------------
	| Getters
	|--------------------------------------------------------------------------
	*/

	/**
	 * Get order ID this meta belongs to.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return int
	 */
	public function get_order_id( $context = 'view' ) {
		return $this->get_prop( 'order_id', $context );
	}

	/**
	 * Get order item name.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_name( $context = 'view' ) {
		return $this->get_prop( 'name', $context );
	}

	/**
	 * Get order item type. Overridden by child classes.
	 *
	 * @return string
	 */
	public function get_type() {
		return '';
	}

	/**
	 * Get quantity.
	 *
	 * @return int
	 */
	public function get_quantity() {
		return 1;
	}

	/**
	 * Get tax status.
	 *
	 * @return string
	 */
	public function get_tax_status() {
		return 'taxable';
	}

	/**
	 * Get tax class.
	 *
	 * @return string
	 */
	public function get_tax_class() {
		return '';
	}

	/**
	 * Get parent order object.
	 *
	 * @return WC_Order
	 */
	public function get_order() {
		return wc_get_order( $this->get_order_id() );
	}

	/*
	|--------------------------------------------------------------------------
	| Setters
	|--------------------------------------------------------------------------
	*/

	/**
	 * Set order ID.
	 *
	 * @param int $value Order ID.
	 */
	public function set_order_id( $value ) {
		$this->set_prop( 'order_id', absint( $value ) );
	}

	/**
	 * Set order item name.
	 *
	 * @param string $value Item name.
	 */
	public function set_name( $value ) {
		$this->set_prop( 'name', wp_check_invalid_utf8( $value ) );
	}

	/*
	|--------------------------------------------------------------------------
	| Other Methods
	|--------------------------------------------------------------------------
	*/

	/**
	 * Type checking.
	 *
	 * @param  string|array $type Type.
	 * @return boolean
	 */
	public function is_type( $type ) {
		return is_array( $type ) ? in_array( $this->get_type(), $type, true ) : $type === $this->get_type();
	}

	/**
	 * Calculate item taxes.
	 *
	 * @since  3.2.0
	 * @param  array $calculate_tax_for Location data to get taxes for. Required.
	 * @return bool  True if taxes were calculated.
	 */
	public function calculate_taxes( $calculate_tax_for = array() ) {
		if ( ! isset( $calculate_tax_for['country'], $calculate_tax_for['state'], $calculate_tax_for['postcode'], $calculate_tax_for['city'] ) ) {
			return false;
		}
		if ( '0' !== $this->get_tax_class() && 'taxable' === $this->get_tax_status() && wc_tax_enabled() ) {
			$calculate_tax_for['tax_class'] = $this->get_tax_class();
			$tax_rates                      = WC_Tax::find_rates( $calculate_tax_for );
			$taxes                          = WC_Tax::calc_tax( $this->get_total(), $tax_rates, false );

			if ( method_exists( $this, 'get_subtotal' ) ) {
				$subtotal_taxes = WC_Tax::calc_tax( $this->get_subtotal(), $tax_rates, false );
				$this->set_taxes(
					array(
						'total'    => $taxes,
						'subtotal' => $subtotal_taxes,
					)
				);
			} else {
				$this->set_taxes( array( 'total' => $taxes ) );
			}
		} else {
			$this->set_taxes( false );
		}

		do_action( 'woocommerce_order_item_after_calculate_taxes', $this, $calculate_tax_for );

		return true;
	}

	/*
	|--------------------------------------------------------------------------
	| Meta Data Handling
	|--------------------------------------------------------------------------
	*/

	/**
	 * Expands things like term slugs before return.
	 *
	 * @param string $hideprefix  Meta data prefix, (default: _).
	 * @param bool   $include_all Include all meta data, this stop skip items with values already in the product name.
	 * @return array
	 */
	public function get_formatted_meta_data( $hideprefix = '_', $include_all = false ) {
		$formatted_meta    = array();
		$meta_data         = $this->get_meta_data();
		$hideprefix_length = ! empty( $hideprefix ) ? strlen( $hideprefix ) : 0;
		$product           = is_callable( array( $this, 'get_product' ) ) ? $this->get_product() : false;
		$order_item_name   = $this->get_name();

		foreach ( $meta_data as $meta ) {
			if ( empty( $meta->id ) || '' === $meta->value || ! is_scalar( $meta->value ) || ( $hideprefix_length && substr( $meta->key, 0, $hideprefix_length ) === $hideprefix ) ) {
				continue;
			}

			$meta->key     = rawurldecode( (string) $meta->key );
			$meta->value   = rawurldecode( (string) $meta->value );
			$attribute_key = str_replace( 'attribute_', '', $meta->key );
			$display_key   = wc_attribute_label( $attribute_key, $product );
			$display_value = wp_kses_post( $meta->value );

			if ( taxonomy_exists( $attribute_key ) ) {
				$term = get_term_by( 'slug', $meta->value, $attribute_key );
				if ( ! is_wp_error( $term ) && is_object( $term ) && $term->name ) {
					$display_value = $term->name;
				}
			}

			// Skip items with values already in the product details area of the product name.
			if ( ! $include_all && $product && $product->is_type( 'variation' ) && wc_is_attribute_in_product_name( $display_value, $order_item_name ) ) {
				continue;
			}

			$formatted_meta[ $meta->id ] = (object) array(
				'key'           => $meta->key,
				'value'         => $meta->value,
				'display_key'   => apply_filters( 'woocommerce_order_item_display_meta_key', $display_key, $meta, $this ),
				'display_value' => wpautop( make_clickable( apply_filters( 'woocommerce_order_item_display_meta_value', $display_value, $meta, $this ) ) ),
			);
		}

		return apply_filters( 'woocommerce_order_item_get_formatted_meta_data', $formatted_meta, $this );
	}

	/*
	|--------------------------------------------------------------------------
	| Array Access Methods
	|--------------------------------------------------------------------------
	|
	| For backwards compatibility with legacy arrays.
	|
	*/

	/**
	 * OffsetSet for ArrayAccess.
	 *
	 * @param string $offset Offset.
	 * @param mixed  $value  Value.
	 */
	public function offsetSet( $offset, $value ) {
		if ( 'item_meta_array' === $offset ) {
			foreach ( $value as $meta_id => $meta ) {
				$this->update_meta_data( $meta->key, $meta->value, $meta_id );
			}
			return;
		}

		if ( array_key_exists( $offset, $this->data ) ) {
			$setter = "set_$offset";
			if ( is_callable( array( $this, $setter ) ) ) {
				$this->$setter( $value );
			}
			return;
		}

		$this->update_meta_data( $offset, $value );
	}

	/**
	 * OffsetUnset for ArrayAccess.
	 *
	 * @param string $offset Offset.
	 */
	public function offsetUnset( $offset ) {
		$this->maybe_read_meta_data();

		if ( 'item_meta_array' === $offset || 'item_meta' === $offset ) {
			$this->meta_data = array();
			return;
		}

		if ( array_key_exists( $offset, $this->data ) ) {
			unset( $this->data[ $offset ] );
		}

		if ( array_key_exists( $offset, $this->changes ) ) {
			unset( $this->changes[ $offset ] );
		}

		$this->delete_meta_data( $offset );
	}

	/**
	 * OffsetExists for ArrayAccess.
	 *
	 * @param string $offset Offset.
	 * @return bool
	 */
	public function offsetExists( $offset ) {
		$this->maybe_read_meta_data();
		if ( 'item_meta_array' === $offset || 'item_meta' === $offset || array_key_exists( $offset, $this->data ) ) {
			return true;
		}
		return array_key_exists( $offset, wp_list_pluck( $this->meta_data, 'value', 'key' ) ) || array_key_exists( '_' . $offset, wp_list_pluck( $this->meta_data, 'value', 'key' ) );
	}

	/**
	 * OffsetGet for ArrayAccess.
	 *
	 * @param string $offset Offset.
	 * @return mixed
	 */
	public function offsetGet( $offset ) {
		$this->maybe_read_meta_data();

		if ( 'item_meta_array' === $offset ) {
			$return = array();

			foreach ( $this->meta_data as $meta ) {
				$return[ $meta->id ] = $meta;
			}

			return $return;
		}

		$meta_values = wp_list_pluck( $this->meta_data, 'value', 'key' );

		if ( 'item_meta' === $offset ) {
			return $meta_values;
		} elseif ( 'type' === $offset ) {
			return $this->get_type();
		} elseif ( array_key_exists( $offset, $this->data ) ) {
			$getter = "get_$offset";
			if ( is_callable( array( $this, $getter ) ) ) {
				return $this->$getter();
			}
		} elseif ( array_key_exists( '_' . $offset, $meta_values ) ) {
			// Item meta was expanded in previous versions, with prefixes removed. This maintains support.
			return $meta_values[ '_' . $offset ];
		} elseif ( array_key_exists( $offset, $meta_values ) ) {
			return $meta_values[ $offset ];
		}

		return null;
	}
}
wc-deprecated-functions.php000064400000101265151336044770012011 0ustar00<?php
/**
 * Deprecated functions
 *
 * Where functions come to die.
 *
 * @author   Automattic
 * @category Core
 * @package  WooCommerce\Functions
 * @version  3.3.0
 */

use Automattic\Jetpack\Constants;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Runs a deprecated action with notice only if used.
 *
 * @since 3.0.0
 * @param string $tag         The name of the action hook.
 * @param array  $args        Array of additional function arguments to be passed to do_action().
 * @param string $version     The version of WooCommerce that deprecated the hook.
 * @param string $replacement The hook that should have been used.
 * @param string $message     A message regarding the change.
 */
function wc_do_deprecated_action( $tag, $args, $version, $replacement = null, $message = null ) {
	if ( ! has_action( $tag ) ) {
		return;
	}

	wc_deprecated_hook( $tag, $version, $replacement, $message );
	do_action_ref_array( $tag, $args );
}

/**
 * Wrapper for deprecated functions so we can apply some extra logic.
 *
 * @since 3.0.0
 * @param string $function Function used.
 * @param string $version Version the message was added in.
 * @param string $replacement Replacement for the called function.
 */
function wc_deprecated_function( $function, $version, $replacement = null ) {
	// @codingStandardsIgnoreStart
	if ( is_ajax() || WC()->is_rest_api_request() ) {
		do_action( 'deprecated_function_run', $function, $replacement, $version );
		$log_string  = "The {$function} function is deprecated since version {$version}.";
		$log_string .= $replacement ? " Replace with {$replacement}." : '';
		error_log( $log_string );
	} else {
		_deprecated_function( $function, $version, $replacement );
	}
	// @codingStandardsIgnoreEnd
}

/**
 * Wrapper for deprecated hook so we can apply some extra logic.
 *
 * @since 3.3.0
 * @param string $hook        The hook that was used.
 * @param string $version     The version of WordPress that deprecated the hook.
 * @param string $replacement The hook that should have been used.
 * @param string $message     A message regarding the change.
 */
function wc_deprecated_hook( $hook, $version, $replacement = null, $message = null ) {
	// @codingStandardsIgnoreStart
	if ( is_ajax() || WC()->is_rest_api_request() ) {
		do_action( 'deprecated_hook_run', $hook, $replacement, $version, $message );

		$message    = empty( $message ) ? '' : ' ' . $message;
		$log_string = "{$hook} is deprecated since version {$version}";
		$log_string .= $replacement ? "! Use {$replacement} instead." : ' with no alternative available.';

		error_log( $log_string . $message );
	} else {
		_deprecated_hook( $hook, $version, $replacement, $message );
	}
	// @codingStandardsIgnoreEnd
}

/**
 * When catching an exception, this allows us to log it if unexpected.
 *
 * @since 3.3.0
 * @param Exception $exception_object The exception object.
 * @param string    $function The function which threw exception.
 * @param array     $args The args passed to the function.
 */
function wc_caught_exception( $exception_object, $function = '', $args = array() ) {
	// @codingStandardsIgnoreStart
	$message  = $exception_object->getMessage();
	$message .= '. Args: ' . print_r( $args, true ) . '.';

	do_action( 'woocommerce_caught_exception', $exception_object, $function, $args );
	error_log( "Exception caught in {$function}. {$message}." );
	// @codingStandardsIgnoreEnd
}

/**
 * Wrapper for _doing_it_wrong().
 *
 * @since  3.0.0
 * @param string $function Function used.
 * @param string $message Message to log.
 * @param string $version Version the message was added in.
 */
function wc_doing_it_wrong( $function, $message, $version ) {
	// @codingStandardsIgnoreStart
	$message .= ' Backtrace: ' . wp_debug_backtrace_summary();

	if ( is_ajax() || WC()->is_rest_api_request() ) {
		do_action( 'doing_it_wrong_run', $function, $message, $version );
		error_log( "{$function} was called incorrectly. {$message}. This message was added in version {$version}." );
	} else {
		_doing_it_wrong( $function, $message, $version );
	}
	// @codingStandardsIgnoreEnd
}

/**
 * Wrapper for deprecated arguments so we can apply some extra logic.
 *
 * @since  3.0.0
 * @param  string $argument
 * @param  string $version
 * @param  string $replacement
 */
function wc_deprecated_argument( $argument, $version, $message = null ) {
	if ( is_ajax() || WC()->is_rest_api_request() ) {
		do_action( 'deprecated_argument_run', $argument, $message, $version );
		error_log( "The {$argument} argument is deprecated since version {$version}. {$message}" );
	} else {
		_deprecated_argument( $argument, $version, $message );
	}
}

/**
 * @deprecated 2.1
 */
function woocommerce_show_messages() {
	wc_deprecated_function( 'woocommerce_show_messages', '2.1', 'wc_print_notices' );
	wc_print_notices();
}

/**
 * @deprecated 2.1
 */
function woocommerce_weekend_area_js() {
	wc_deprecated_function( 'woocommerce_weekend_area_js', '2.1' );
}

/**
 * @deprecated 2.1
 */
function woocommerce_tooltip_js() {
	wc_deprecated_function( 'woocommerce_tooltip_js', '2.1' );
}

/**
 * @deprecated 2.1
 */
function woocommerce_datepicker_js() {
	wc_deprecated_function( 'woocommerce_datepicker_js', '2.1' );
}

/**
 * @deprecated 2.1
 */
function woocommerce_admin_scripts() {
	wc_deprecated_function( 'woocommerce_admin_scripts', '2.1' );
}

/**
 * @deprecated 2.1
 */
function woocommerce_create_page( $slug, $option = '', $page_title = '', $page_content = '', $post_parent = 0 ) {
	wc_deprecated_function( 'woocommerce_create_page', '2.1', 'wc_create_page' );
	return wc_create_page( $slug, $option, $page_title, $page_content, $post_parent );
}

/**
 * @deprecated 2.1
 */
function woocommerce_readfile_chunked( $file, $retbytes = true ) {
	wc_deprecated_function( 'woocommerce_readfile_chunked', '2.1', 'WC_Download_Handler::readfile_chunked()' );
	return WC_Download_Handler::readfile_chunked( $file );
}

/**
 * Formal total costs - format to the number of decimal places for the base currency.
 *
 * @access public
 * @param mixed $number
 * @deprecated 2.1
 * @return string
 */
function woocommerce_format_total( $number ) {
	wc_deprecated_function( __FUNCTION__, '2.1', 'wc_format_decimal()' );
	return wc_format_decimal( $number, wc_get_price_decimals(), false );
}

/**
 * Get product name with extra details such as SKU price and attributes. Used within admin.
 *
 * @access public
 * @param WC_Product $product
 * @deprecated 2.1
 * @return string
 */
function woocommerce_get_formatted_product_name( $product ) {
	wc_deprecated_function( __FUNCTION__, '2.1', 'WC_Product::get_formatted_name()' );
	return $product->get_formatted_name();
}

/**
 * Handle IPN requests for the legacy paypal gateway by calling gateways manually if needed.
 *
 * @access public
 */
function woocommerce_legacy_paypal_ipn() {
	if ( ! empty( $_GET['paypalListener'] ) && 'paypal_standard_IPN' === $_GET['paypalListener'] ) {
		WC()->payment_gateways();
		do_action( 'woocommerce_api_wc_gateway_paypal' );
	}
}
add_action( 'init', 'woocommerce_legacy_paypal_ipn' );

/**
 * @deprecated 3.0
 */
function get_product( $the_product = false, $args = array() ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_product' );
	return wc_get_product( $the_product, $args );
}

/**
 * @deprecated 3.0
 */
function woocommerce_protected_product_add_to_cart( $passed, $product_id ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_protected_product_add_to_cart' );
	return wc_protected_product_add_to_cart( $passed, $product_id );
}

/**
 * @deprecated 3.0
 */
function woocommerce_empty_cart() {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_empty_cart' );
	wc_empty_cart();
}

/**
 * @deprecated 3.0
 */
function woocommerce_load_persistent_cart( $user_login, $user = 0 ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_load_persistent_cart' );
	return wc_load_persistent_cart( $user_login, $user );
}

/**
 * @deprecated 3.0
 */
function woocommerce_add_to_cart_message( $product_id ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_add_to_cart_message' );
	wc_add_to_cart_message( $product_id );
}

/**
 * @deprecated 3.0
 */
function woocommerce_clear_cart_after_payment() {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_clear_cart_after_payment' );
	wc_clear_cart_after_payment();
}

/**
 * @deprecated 3.0
 */
function woocommerce_cart_totals_subtotal_html() {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cart_totals_subtotal_html' );
	wc_cart_totals_subtotal_html();
}

/**
 * @deprecated 3.0
 */
function woocommerce_cart_totals_shipping_html() {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cart_totals_shipping_html' );
	wc_cart_totals_shipping_html();
}

/**
 * @deprecated 3.0
 */
function woocommerce_cart_totals_coupon_html( $coupon ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cart_totals_coupon_html' );
	wc_cart_totals_coupon_html( $coupon );
}

/**
 * @deprecated 3.0
 */
function woocommerce_cart_totals_order_total_html() {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cart_totals_order_total_html' );
	wc_cart_totals_order_total_html();
}

/**
 * @deprecated 3.0
 */
function woocommerce_cart_totals_fee_html( $fee ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cart_totals_fee_html' );
	wc_cart_totals_fee_html( $fee );
}

/**
 * @deprecated 3.0
 */
function woocommerce_cart_totals_shipping_method_label( $method ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cart_totals_shipping_method_label' );
	return wc_cart_totals_shipping_method_label( $method );
}

/**
 * @deprecated 3.0
 */
function woocommerce_get_template_part( $slug, $name = '' ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_template_part' );
	wc_get_template_part( $slug, $name );
}

/**
 * @deprecated 3.0
 */
function woocommerce_get_template( $template_name, $args = array(), $template_path = '', $default_path = '' ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_template' );
	wc_get_template( $template_name, $args, $template_path, $default_path );
}

/**
 * @deprecated 3.0
 */
function woocommerce_locate_template( $template_name, $template_path = '', $default_path = '' ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_locate_template' );
	return wc_locate_template( $template_name, $template_path, $default_path );
}

/**
 * @deprecated 3.0
 */
function woocommerce_mail( $to, $subject, $message, $headers = "Content-Type: text/html\r\n", $attachments = "" ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_mail' );
	wc_mail( $to, $subject, $message, $headers, $attachments );
}

/**
 * @deprecated 3.0
 */
function woocommerce_disable_admin_bar( $show_admin_bar ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_disable_admin_bar' );
	return wc_disable_admin_bar( $show_admin_bar );
}

/**
 * @deprecated 3.0
 */
function woocommerce_create_new_customer( $email, $username = '', $password = '' ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_create_new_customer' );
	return wc_create_new_customer( $email, $username, $password );
}

/**
 * @deprecated 3.0
 */
function woocommerce_set_customer_auth_cookie( $customer_id ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_set_customer_auth_cookie' );
	wc_set_customer_auth_cookie( $customer_id );
}

/**
 * @deprecated 3.0
 */
function woocommerce_update_new_customer_past_orders( $customer_id ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_update_new_customer_past_orders' );
	return wc_update_new_customer_past_orders( $customer_id );
}

/**
 * @deprecated 3.0
 */
function woocommerce_paying_customer( $order_id ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_paying_customer' );
	wc_paying_customer( $order_id );
}

/**
 * @deprecated 3.0
 */
function woocommerce_customer_bought_product( $customer_email, $user_id, $product_id ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_customer_bought_product' );
	return wc_customer_bought_product( $customer_email, $user_id, $product_id );
}

/**
 * @deprecated 3.0
 */
function woocommerce_customer_has_capability( $allcaps, $caps, $args ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_customer_has_capability' );
	return wc_customer_has_capability( $allcaps, $caps, $args );
}

/**
 * @deprecated 3.0
 */
function woocommerce_sanitize_taxonomy_name( $taxonomy ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_sanitize_taxonomy_name' );
	return wc_sanitize_taxonomy_name( $taxonomy );
}

/**
 * @deprecated 3.0
 */
function woocommerce_get_filename_from_url( $file_url ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_filename_from_url' );
	return wc_get_filename_from_url( $file_url );
}

/**
 * @deprecated 3.0
 */
function woocommerce_get_dimension( $dim, $to_unit ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_dimension' );
	return wc_get_dimension( $dim, $to_unit );
}

/**
 * @deprecated 3.0
 */
function woocommerce_get_weight( $weight, $to_unit ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_weight' );
	return wc_get_weight( $weight, $to_unit );
}

/**
 * @deprecated 3.0
 */
function woocommerce_trim_zeros( $price ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_trim_zeros' );
	return wc_trim_zeros( $price );
}

/**
 * @deprecated 3.0
 */
function woocommerce_round_tax_total( $tax ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_round_tax_total' );
	return wc_round_tax_total( $tax );
}

/**
 * @deprecated 3.0
 */
function woocommerce_format_decimal( $number, $dp = false, $trim_zeros = false ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_format_decimal' );
	return wc_format_decimal( $number, $dp, $trim_zeros );
}

/**
 * @deprecated 3.0
 */
function woocommerce_clean( $var ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_clean' );
	return wc_clean( $var );
}

/**
 * @deprecated 3.0
 */
function woocommerce_array_overlay( $a1, $a2 ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_array_overlay' );
	return wc_array_overlay( $a1, $a2 );
}

/**
 * @deprecated 3.0
 */
function woocommerce_price( $price, $args = array() ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_price' );
	return wc_price( $price, $args );
}

/**
 * @deprecated 3.0
 */
function woocommerce_let_to_num( $size ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_let_to_num' );
	return wc_let_to_num( $size );
}

/**
 * @deprecated 3.0
 */
function woocommerce_date_format() {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_date_format' );
	return wc_date_format();
}

/**
 * @deprecated 3.0
 */
function woocommerce_time_format() {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_time_format' );
	return wc_time_format();
}

/**
 * @deprecated 3.0
 */
function woocommerce_timezone_string() {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_timezone_string' );
	return wc_timezone_string();
}

if ( ! function_exists( 'woocommerce_rgb_from_hex' ) ) {
	/**
	 * @deprecated 3.0
	 */
	function woocommerce_rgb_from_hex( $color ) {
		wc_deprecated_function( __FUNCTION__, '3.0', 'wc_rgb_from_hex' );
		return wc_rgb_from_hex( $color );
	}
}

if ( ! function_exists( 'woocommerce_hex_darker' ) ) {
	/**
	 * @deprecated 3.0
	 */
	function woocommerce_hex_darker( $color, $factor = 30 ) {
		wc_deprecated_function( __FUNCTION__, '3.0', 'wc_hex_darker' );
		return wc_hex_darker( $color, $factor );
	}
}

if ( ! function_exists( 'woocommerce_hex_lighter' ) ) {
	/**
	 * @deprecated 3.0
	 */
	function woocommerce_hex_lighter( $color, $factor = 30 ) {
		wc_deprecated_function( __FUNCTION__, '3.0', 'wc_hex_lighter' );
		return wc_hex_lighter( $color, $factor );
	}
}

if ( ! function_exists( 'woocommerce_light_or_dark' ) ) {
	/**
	 * @deprecated 3.0
	 */
	function woocommerce_light_or_dark( $color, $dark = '#000000', $light = '#FFFFFF' ) {
		wc_deprecated_function( __FUNCTION__, '3.0', 'wc_light_or_dark' );
		return wc_light_or_dark( $color, $dark, $light );
	}
}

if ( ! function_exists( 'woocommerce_format_hex' ) ) {
	/**
	 * @deprecated 3.0
	 */
	function woocommerce_format_hex( $hex ) {
		wc_deprecated_function( __FUNCTION__, '3.0', 'wc_format_hex' );
		return wc_format_hex( $hex );
	}
}

/**
 * @deprecated 3.0
 */
function woocommerce_get_order_id_by_order_key( $order_key ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_order_id_by_order_key' );
	return wc_get_order_id_by_order_key( $order_key );
}

/**
 * @deprecated 3.0
 */
function woocommerce_downloadable_file_permission( $download_id, $product_id, $order ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_downloadable_file_permission' );
	return wc_downloadable_file_permission( $download_id, $product_id, $order );
}

/**
 * @deprecated 3.0
 */
function woocommerce_downloadable_product_permissions( $order_id ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_downloadable_product_permissions' );
	wc_downloadable_product_permissions( $order_id );
}

/**
 * @deprecated 3.0
 */
function woocommerce_add_order_item( $order_id, $item ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_add_order_item' );
	return wc_add_order_item( $order_id, $item );
}

/**
 * @deprecated 3.0
 */
function woocommerce_delete_order_item( $item_id ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_delete_order_item' );
	return wc_delete_order_item( $item_id );
}

/**
 * @deprecated 3.0
 */
function woocommerce_update_order_item_meta( $item_id, $meta_key, $meta_value, $prev_value = '' ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_update_order_item_meta' );
	return wc_update_order_item_meta( $item_id, $meta_key, $meta_value, $prev_value );
}

/**
 * @deprecated 3.0
 */
function woocommerce_add_order_item_meta( $item_id, $meta_key, $meta_value, $unique = false ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_add_order_item_meta' );
	return wc_add_order_item_meta( $item_id, $meta_key, $meta_value, $unique );
}

/**
 * @deprecated 3.0
 */
function woocommerce_delete_order_item_meta( $item_id, $meta_key, $meta_value = '', $delete_all = false ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_delete_order_item_meta' );
	return wc_delete_order_item_meta( $item_id, $meta_key, $meta_value, $delete_all );
}

/**
 * @deprecated 3.0
 */
function woocommerce_get_order_item_meta( $item_id, $key, $single = true ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_order_item_meta' );
	return wc_get_order_item_meta( $item_id, $key, $single );
}

/**
 * @deprecated 3.0
 */
function woocommerce_cancel_unpaid_orders() {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cancel_unpaid_orders' );
	wc_cancel_unpaid_orders();
}

/**
 * @deprecated 3.0
 */
function woocommerce_processing_order_count() {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_processing_order_count' );
	return wc_processing_order_count();
}

/**
 * @deprecated 3.0
 */
function woocommerce_get_page_id( $page ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_page_id' );
	return wc_get_page_id( $page );
}

/**
 * @deprecated 3.0
 */
function woocommerce_get_endpoint_url( $endpoint, $value = '', $permalink = '' ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_endpoint_url' );
	return wc_get_endpoint_url( $endpoint, $value, $permalink );
}

/**
 * @deprecated 3.0
 */
function woocommerce_lostpassword_url( $url ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_lostpassword_url' );
	return wc_lostpassword_url( $url );
}

/**
 * @deprecated 3.0
 */
function woocommerce_customer_edit_account_url() {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_customer_edit_account_url' );
	return wc_customer_edit_account_url();
}

/**
 * @deprecated 3.0
 */
function woocommerce_nav_menu_items( $items, $args ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_nav_menu_items' );
	return wc_nav_menu_items( $items );
}

/**
 * @deprecated 3.0
 */
function woocommerce_nav_menu_item_classes( $menu_items, $args ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_nav_menu_item_classes' );
	return wc_nav_menu_item_classes( $menu_items );
}

/**
 * @deprecated 3.0
 */
function woocommerce_list_pages( $pages ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_list_pages' );
	return wc_list_pages( $pages );
}

/**
 * @deprecated 3.0
 */
function woocommerce_product_dropdown_categories( $args = array(), $deprecated_hierarchical = 1, $deprecated_show_uncategorized = 1, $deprecated_orderby = '' ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_product_dropdown_categories' );
	return wc_product_dropdown_categories( $args, $deprecated_hierarchical, $deprecated_show_uncategorized, $deprecated_orderby );
}

/**
 * @deprecated 3.0
 */
function woocommerce_walk_category_dropdown_tree( $a1 = '', $a2 = '', $a3 = '' ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_walk_category_dropdown_tree' );
	return wc_walk_category_dropdown_tree( $a1, $a2, $a3 );
}

/**
 * @deprecated 3.0
 */
function woocommerce_taxonomy_metadata_wpdbfix() {
	wc_deprecated_function( __FUNCTION__, '3.0' );
}

/**
 * @deprecated 3.0
 */
function wc_taxonomy_metadata_wpdbfix() {
	wc_deprecated_function( __FUNCTION__, '3.0' );
}

/**
 * @deprecated 3.0
 */
function woocommerce_order_terms( $the_term, $next_id, $taxonomy, $index = 0, $terms = null ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_reorder_terms' );
	return wc_reorder_terms( $the_term, $next_id, $taxonomy, $index, $terms );
}

/**
 * @deprecated 3.0
 */
function woocommerce_set_term_order( $term_id, $index, $taxonomy, $recursive = false ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_set_term_order' );
	return wc_set_term_order( $term_id, $index, $taxonomy, $recursive );
}

/**
 * @deprecated 3.0
 */
function woocommerce_terms_clauses( $clauses, $taxonomies, $args ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_terms_clauses' );
	return wc_terms_clauses( $clauses, $taxonomies, $args );
}

/**
 * @deprecated 3.0
 */
function _woocommerce_term_recount( $terms, $taxonomy, $callback, $terms_are_term_taxonomy_ids ) {
	wc_deprecated_function( __FUNCTION__, '3.0', '_wc_term_recount' );
	return _wc_term_recount( $terms, $taxonomy, $callback, $terms_are_term_taxonomy_ids );
}

/**
 * @deprecated 3.0
 */
function woocommerce_recount_after_stock_change( $product_id ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_recount_after_stock_change' );
	return wc_recount_after_stock_change( $product_id );
}

/**
 * @deprecated 3.0
 */
function woocommerce_change_term_counts( $terms, $taxonomies, $args ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_change_term_counts' );
	return wc_change_term_counts( $terms, $taxonomies );
}

/**
 * @deprecated 3.0
 */
function woocommerce_get_product_ids_on_sale() {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_product_ids_on_sale' );
	return wc_get_product_ids_on_sale();
}

/**
 * @deprecated 3.0
 */
function woocommerce_get_featured_product_ids() {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_featured_product_ids' );
	return wc_get_featured_product_ids();
}

/**
 * @deprecated 3.0
 */
function woocommerce_get_product_terms( $object_id, $taxonomy, $fields = 'all' ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_product_terms' );
	return wc_get_product_terms( $object_id, $taxonomy, array( 'fields' => $fields ) );
}

/**
 * @deprecated 3.0
 */
function woocommerce_product_post_type_link( $permalink, $post ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_product_post_type_link' );
	return wc_product_post_type_link( $permalink, $post );
}

/**
 * @deprecated 3.0
 */
function woocommerce_placeholder_img_src() {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_placeholder_img_src' );
	return wc_placeholder_img_src();
}

/**
 * @deprecated 3.0
 */
function woocommerce_placeholder_img( $size = 'woocommerce_thumbnail' ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_placeholder_img' );
	return wc_placeholder_img( $size );
}

/**
 * @deprecated 3.0
 */
function woocommerce_get_formatted_variation( $variation = '', $flat = false ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_formatted_variation' );
	return wc_get_formatted_variation( $variation, $flat );
}

/**
 * @deprecated 3.0
 */
function woocommerce_scheduled_sales() {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_scheduled_sales' );
	return wc_scheduled_sales();
}

/**
 * @deprecated 3.0
 */
function woocommerce_get_attachment_image_attributes( $attr ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_attachment_image_attributes' );
	return wc_get_attachment_image_attributes( $attr );
}

/**
 * @deprecated 3.0
 */
function woocommerce_prepare_attachment_for_js( $response ) {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_prepare_attachment_for_js' );
	return wc_prepare_attachment_for_js( $response );
}

/**
 * @deprecated 3.0
 */
function woocommerce_track_product_view() {
	wc_deprecated_function( __FUNCTION__, '3.0', 'wc_track_product_view' );
	return wc_track_product_view();
}

/**
 * @deprecated 2.3 has no replacement
 */
function woocommerce_compile_less_styles() {
	wc_deprecated_function( 'woocommerce_compile_less_styles', '2.3' );
}

/**
 * woocommerce_calc_shipping was an option used to determine if shipping was enabled prior to version 2.6.0. This has since been replaced with wc_shipping_enabled() function and
 * the woocommerce_ship_to_countries setting.
 * @deprecated 2.6.0
 * @return string
 */
function woocommerce_calc_shipping_backwards_compatibility( $value ) {
	if ( Constants::is_defined( 'WC_UPDATING' ) ) {
		return $value;
	}
	return 'disabled' === get_option( 'woocommerce_ship_to_countries' ) ? 'no' : 'yes';
}
add_filter( 'pre_option_woocommerce_calc_shipping', 'woocommerce_calc_shipping_backwards_compatibility' );

/**
 * @deprecated 3.0.0
 * @see WC_Structured_Data class
 *
 * @return string
 */
function woocommerce_get_product_schema() {
	wc_deprecated_function( 'woocommerce_get_product_schema', '3.0' );

	global $product;

	$schema = "Product";

	// Downloadable product schema handling
	if ( $product->is_downloadable() ) {
		switch ( $product->download_type ) {
			case 'application' :
				$schema = "SoftwareApplication";
				break;
			case 'music' :
				$schema = "MusicAlbum";
				break;
			default :
				$schema = "Product";
				break;
		}
	}

	return 'http://schema.org/' . $schema;
}

/**
 * Save product price.
 *
 * This is a private function (internal use ONLY) used until a data manipulation api is built.
 *
 * @deprecated 3.0.0
 * @param int $product_id
 * @param float $regular_price
 * @param float $sale_price
 * @param string $date_from
 * @param string $date_to
 */
function _wc_save_product_price( $product_id, $regular_price, $sale_price = '', $date_from = '', $date_to = '' ) {
	wc_doing_it_wrong( '_wc_save_product_price()', 'This function is not for developer use and is deprecated.', '3.0' );

	$product_id    = absint( $product_id );
	$regular_price = wc_format_decimal( $regular_price );
	$sale_price    = '' === $sale_price ? '' : wc_format_decimal( $sale_price );
	$date_from     = wc_clean( $date_from );
	$date_to       = wc_clean( $date_to );

	update_post_meta( $product_id, '_regular_price', $regular_price );
	update_post_meta( $product_id, '_sale_price', $sale_price );

	// Save Dates
	update_post_meta( $product_id, '_sale_price_dates_from', $date_from ? strtotime( $date_from ) : '' );
	update_post_meta( $product_id, '_sale_price_dates_to', $date_to ? strtotime( $date_to ) : '' );

	if ( $date_to && ! $date_from ) {
		$date_from = strtotime( 'NOW', current_time( 'timestamp' ) );
		update_post_meta( $product_id, '_sale_price_dates_from', $date_from );
	}

	// Update price if on sale
	if ( '' !== $sale_price && '' === $date_to && '' === $date_from ) {
		update_post_meta( $product_id, '_price', $sale_price );
	} else {
		update_post_meta( $product_id, '_price', $regular_price );
	}

	if ( '' !== $sale_price && $date_from && strtotime( $date_from ) < strtotime( 'NOW', current_time( 'timestamp' ) ) ) {
		update_post_meta( $product_id, '_price', $sale_price );
	}

	if ( $date_to && strtotime( $date_to ) < strtotime( 'NOW', current_time( 'timestamp' ) ) ) {
		update_post_meta( $product_id, '_price', $regular_price );
		update_post_meta( $product_id, '_sale_price_dates_from', '' );
		update_post_meta( $product_id, '_sale_price_dates_to', '' );
	}
}

/**
 * Return customer avatar URL.
 *
 * @deprecated 3.1.0
 * @since 2.6.0
 * @param string $email the customer's email.
 * @return string the URL to the customer's avatar.
 */
function wc_get_customer_avatar_url( $email ) {
	// Deprecated in favor of WordPress get_avatar_url() function.
	wc_deprecated_function( 'wc_get_customer_avatar_url()', '3.1', 'get_avatar_url()' );

	return get_avatar_url( $email );
}

/**
 * WooCommerce Core Supported Themes.
 *
 * @deprecated 3.3.0
 * @since 2.2
 * @return string[]
 */
function wc_get_core_supported_themes() {
	wc_deprecated_function( 'wc_get_core_supported_themes()', '3.3' );
	return array( 'twentyseventeen', 'twentysixteen', 'twentyfifteen', 'twentyfourteen', 'twentythirteen', 'twentyeleven', 'twentytwelve', 'twentyten' );
}

/**
 * Get min/max price meta query args.
 *
 * @deprecated 3.6.0
 * @since 3.0.0
 * @param array $args Min price and max price arguments.
 * @return array
 */
function wc_get_min_max_price_meta_query( $args ) {
	wc_deprecated_function( 'wc_get_min_max_price_meta_query()', '3.6' );

	$current_min_price = isset( $args['min_price'] ) ? floatval( $args['min_price'] ) : 0;
	$current_max_price = isset( $args['max_price'] ) ? floatval( $args['max_price'] ) : PHP_INT_MAX;

	return apply_filters(
		'woocommerce_get_min_max_price_meta_query',
		array(
			'key'     => '_price',
			'value'   => array( $current_min_price, $current_max_price ),
			'compare' => 'BETWEEN',
			'type'    => 'DECIMAL(10,' . wc_get_price_decimals() . ')',
		),
		$args
	);
}

/**
 * When a term is split, ensure meta data maintained.
 *
 * @deprecated 3.6.0
 * @param  int    $old_term_id      Old term ID.
 * @param  int    $new_term_id      New term ID.
 * @param  string $term_taxonomy_id Term taxonomy ID.
 * @param  string $taxonomy         Taxonomy.
 */
function wc_taxonomy_metadata_update_content_for_split_terms( $old_term_id, $new_term_id, $term_taxonomy_id, $taxonomy ) {
	wc_deprecated_function( 'wc_taxonomy_metadata_update_content_for_split_terms', '3.6' );
}

/**
 * WooCommerce Term Meta API.
 *
 * WC tables for storing term meta are deprecated from WordPress 4.4 since 4.4 has its own table.
 * This function serves as a wrapper, using the new table if present, or falling back to the WC table.
 *
 * @deprecated 3.6.0
 * @param int    $term_id    Term ID.
 * @param string $meta_key   Meta key.
 * @param mixed  $meta_value Meta value.
 * @param string $prev_value Previous value. (default: '').
 * @return bool
 */
function update_woocommerce_term_meta( $term_id, $meta_key, $meta_value, $prev_value = '' ) {
	wc_deprecated_function( 'update_woocommerce_term_meta', '3.6', 'update_term_meta' );
	return function_exists( 'update_term_meta' ) ? update_term_meta( $term_id, $meta_key, $meta_value, $prev_value ) : update_metadata( 'woocommerce_term', $term_id, $meta_key, $meta_value, $prev_value );
}

/**
 * WooCommerce Term Meta API.
 *
 * WC tables for storing term meta are deprecated from WordPress 4.4 since 4.4 has its own table.
 * This function serves as a wrapper, using the new table if present, or falling back to the WC table.
 *
 * @deprecated 3.6.0
 * @param int    $term_id    Term ID.
 * @param string $meta_key   Meta key.
 * @param mixed  $meta_value Meta value.
 * @param bool   $unique     Make meta key unique. (default: false).
 * @return bool
 */
function add_woocommerce_term_meta( $term_id, $meta_key, $meta_value, $unique = false ) {
	wc_deprecated_function( 'add_woocommerce_term_meta', '3.6', 'add_term_meta' );
	return function_exists( 'add_term_meta' ) ? add_term_meta( $term_id, $meta_key, $meta_value, $unique ) : add_metadata( 'woocommerce_term', $term_id, $meta_key, $meta_value, $unique );
}

/**
 * WooCommerce Term Meta API
 *
 * WC tables for storing term meta are deprecated from WordPress 4.4 since 4.4 has its own table.
 * This function serves as a wrapper, using the new table if present, or falling back to the WC table.
 *
 * @deprecated 3.6.0
 * @param int    $term_id    Term ID.
 * @param string $meta_key   Meta key.
 * @param string $meta_value Meta value (default: '').
 * @param bool   $deprecated Deprecated param (default: false).
 * @return bool
 */
function delete_woocommerce_term_meta( $term_id, $meta_key, $meta_value = '', $deprecated = false ) {
	wc_deprecated_function( 'delete_woocommerce_term_meta', '3.6', 'delete_term_meta' );
	return function_exists( 'delete_term_meta' ) ? delete_term_meta( $term_id, $meta_key, $meta_value ) : delete_metadata( 'woocommerce_term', $term_id, $meta_key, $meta_value );
}

/**
 * WooCommerce Term Meta API
 *
 * WC tables for storing term meta are deprecated from WordPress 4.4 since 4.4 has its own table.
 * This function serves as a wrapper, using the new table if present, or falling back to the WC table.
 *
 * @deprecated 3.6.0
 * @param int    $term_id Term ID.
 * @param string $key     Meta key.
 * @param bool   $single  Whether to return a single value. (default: true).
 * @return mixed
 */
function get_woocommerce_term_meta( $term_id, $key, $single = true ) {
	wc_deprecated_function( 'get_woocommerce_term_meta', '3.6', 'get_term_meta' );
	return function_exists( 'get_term_meta' ) ? get_term_meta( $term_id, $key, $single ) : get_metadata( 'woocommerce_term', $term_id, $key, $single );
}
class-wc-product-download.php000064400000016073151336044770012275 0ustar00<?php
/**
 * Represents a file which can be downloaded.
 *
 * @package WooCommerce\Classes
 * @version 3.0.0
 * @since   3.0.0
 */

use Automattic\Jetpack\Constants;

defined( 'ABSPATH' ) || exit;

/**
 * Product download class.
 */
class WC_Product_Download implements ArrayAccess {

	/**
	 * Data array.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $data = array(
		'id'   => '',
		'name' => '',
		'file' => '',
	);

	/**
	 * Returns all data for this object.
	 *
	 * @return array
	 */
	public function get_data() {
		return $this->data;
	}

	/**
	 * Get allowed mime types.
	 *
	 * @return array
	 */
	public function get_allowed_mime_types() {
		return apply_filters( 'woocommerce_downloadable_file_allowed_mime_types', get_allowed_mime_types() );
	}

	/**
	 * Get type of file path set.
	 *
	 * @param  string $file_path optional.
	 * @return string absolute, relative, or shortcode.
	 */
	public function get_type_of_file_path( $file_path = '' ) {
		$file_path = $file_path ? $file_path : $this->get_file();
		$parsed_url = parse_url( $file_path );
		if (
			$parsed_url &&
			isset( $parsed_url['host'] ) && // Absolute url means that it has a host.
			( // Theoretically we could permit any scheme (like ftp as well), but that has not been the case before. So we allow none or http(s).
				! isset( $parsed_url['scheme'] ) ||
				in_array( $parsed_url['scheme'], array( 'http', 'https' ) )
			)
		) {
			return 'absolute';
		} elseif ( '[' === substr( $file_path, 0, 1 ) && ']' === substr( $file_path, -1 ) ) {
			return 'shortcode';
		} else {
			return 'relative';
		}
	}

	/**
	 * Get file type.
	 *
	 * @return string
	 */
	public function get_file_type() {
		$type = wp_check_filetype( strtok( $this->get_file(), '?' ), $this->get_allowed_mime_types() );
		return $type['type'];
	}

	/**
	 * Get file extension.
	 *
	 * @return string
	 */
	public function get_file_extension() {
		$parsed_url = wp_parse_url( $this->get_file(), PHP_URL_PATH );
		return pathinfo( $parsed_url, PATHINFO_EXTENSION );
	}

	/**
	 * Check if file is allowed.
	 *
	 * @return boolean
	 */
	public function is_allowed_filetype() {
		$file_path = $this->get_file();

		// File types for URL-based files located on the server should get validated.
		$parsed_file_path  = WC_Download_Handler::parse_file_path( $file_path );
		$is_file_on_server = ! $parsed_file_path['remote_file'];
		$file_path_type    = $this->get_type_of_file_path( $file_path );

		// Shortcodes are allowed, validations should be done by the shortcode provider in this case.
		if ( 'shortcode' === $file_path_type ) {
			return true;
		}

		// Remote paths are allowed.
		if ( ! $is_file_on_server && 'relative' !== $file_path_type ) {
			return true;
		}

		// On windows system, local files ending with `.` are not allowed.
		// @link https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN#naming-conventions.
		if ( $is_file_on_server && ! $this->get_file_extension() && 'WIN' === strtoupper( substr( Constants::get_constant( 'PHP_OS' ), 0, 3 ) ) ) {
			if ( '.' === substr( $file_path, -1 ) ) {
				return false;
			}
		}

		return ! $this->get_file_extension() || in_array( $this->get_file_type(), $this->get_allowed_mime_types(), true );
	}

	/**
	 * Validate file exists.
	 *
	 * @return boolean
	 */
	public function file_exists() {
		if ( 'relative' !== $this->get_type_of_file_path() ) {
			return true;
		}
		$file_url = $this->get_file();
		if ( '..' === substr( $file_url, 0, 2 ) || '/' !== substr( $file_url, 0, 1 ) ) {
			$file_url = realpath( ABSPATH . $file_url );
		} elseif ( substr( WP_CONTENT_DIR, strlen( untrailingslashit( ABSPATH ) ) ) === substr( $file_url, 0, strlen( substr( WP_CONTENT_DIR, strlen( untrailingslashit( ABSPATH ) ) ) ) ) ) {
			$file_url = realpath( WP_CONTENT_DIR . substr( $file_url, 11 ) );
		}
		return apply_filters( 'woocommerce_downloadable_file_exists', file_exists( $file_url ), $this->get_file() );
	}

	/*
	|--------------------------------------------------------------------------
	| Setters
	|--------------------------------------------------------------------------
	*/

	/**
	 * Set ID.
	 *
	 * @param string $value Download ID.
	 */
	public function set_id( $value ) {
		$this->data['id'] = wc_clean( $value );
	}

	/**
	 * Set name.
	 *
	 * @param string $value Download name.
	 */
	public function set_name( $value ) {
		$this->data['name'] = wc_clean( $value );
	}

	/**
	 * Set previous_hash.
	 *
	 * @deprecated 3.3.0 No longer using filename based hashing to keep track of files.
	 * @param string $value Previous hash.
	 */
	public function set_previous_hash( $value ) {
		wc_deprecated_function( __FUNCTION__, '3.3' );
		$this->data['previous_hash'] = wc_clean( $value );
	}

	/**
	 * Set file.
	 *
	 * @param string $value File URL/Path.
	 */
	public function set_file( $value ) {
		// A `///` is recognized as an "absolute", but on the filesystem, so it bypasses the mime check in `self::is_allowed_filetype`.
		// This will strip extra prepending / to the maximum of 2.
		if ( preg_match( '#^//+(/[^/].+)$#i', $value, $matches ) ) {
			$value = $matches[1];
		}
		switch ( $this->get_type_of_file_path( $value ) ) {
			case 'absolute':
				$this->data['file'] = esc_url_raw( $value );
				break;
			default:
				$this->data['file'] = wc_clean( $value );
				break;
		}
	}

	/*
	|--------------------------------------------------------------------------
	| Getters
	|--------------------------------------------------------------------------
	*/

	/**
	 * Get id.
	 *
	 * @return string
	 */
	public function get_id() {
		return $this->data['id'];
	}

	/**
	 * Get name.
	 *
	 * @return string
	 */
	public function get_name() {
		return $this->data['name'];
	}

	/**
	 * Get previous_hash.
	 *
	 * @deprecated 3.3.0 No longer using filename based hashing to keep track of files.
	 * @return string
	 */
	public function get_previous_hash() {
		wc_deprecated_function( __FUNCTION__, '3.3' );
		return $this->data['previous_hash'];
	}

	/**
	 * Get file.
	 *
	 * @return string
	 */
	public function get_file() {
		return $this->data['file'];
	}

	/*
	|--------------------------------------------------------------------------
	| ArrayAccess/Backwards compatibility.
	|--------------------------------------------------------------------------
	*/

	/**
	 * OffsetGet.
	 *
	 * @param string $offset Offset.
	 * @return mixed
	 */
	public function offsetGet( $offset ) {
		switch ( $offset ) {
			default:
				if ( is_callable( array( $this, "get_$offset" ) ) ) {
					return $this->{"get_$offset"}();
				}
				break;
		}
		return '';
	}

	/**
	 * OffsetSet.
	 *
	 * @param string $offset Offset.
	 * @param mixed  $value Offset value.
	 */
	public function offsetSet( $offset, $value ) {
		switch ( $offset ) {
			default:
				if ( is_callable( array( $this, "set_$offset" ) ) ) {
					return $this->{"set_$offset"}( $value );
				}
				break;
		}
	}

	/**
	 * OffsetUnset.
	 *
	 * @param string $offset Offset.
	 */
	public function offsetUnset( $offset ) {}

	/**
	 * OffsetExists.
	 *
	 * @param string $offset Offset.
	 * @return bool
	 */
	public function offsetExists( $offset ) {
		return in_array( $offset, array_keys( $this->data ), true );
	}
}
class-wc-ajax.php000064400000307370151336044770007736 0ustar00<?php
/**
 * WooCommerce WC_AJAX. AJAX Event Handlers.
 *
 * @class   WC_AJAX
 * @package WooCommerce\Classes
 */

use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Utilities\NumberUtil;

defined( 'ABSPATH' ) || exit;

/**
 * WC_Ajax class.
 */
class WC_AJAX {

	/**
	 * Hook in ajax handlers.
	 */
	public static function init() {
		add_action( 'init', array( __CLASS__, 'define_ajax' ), 0 );
		add_action( 'template_redirect', array( __CLASS__, 'do_wc_ajax' ), 0 );
		self::add_ajax_events();
	}

	/**
	 * Get WC Ajax Endpoint.
	 *
	 * @param string $request Optional.
	 *
	 * @return string
	 */
	public static function get_endpoint( $request = '' ) {
		return esc_url_raw( apply_filters( 'woocommerce_ajax_get_endpoint', add_query_arg( 'wc-ajax', $request, remove_query_arg( array( 'remove_item', 'add-to-cart', 'added-to-cart', 'order_again', '_wpnonce' ), home_url( '/', 'relative' ) ) ), $request ) );
	}

	/**
	 * Set WC AJAX constant and headers.
	 */
	public static function define_ajax() {
		// phpcs:disable
		if ( ! empty( $_GET['wc-ajax'] ) ) {
			wc_maybe_define_constant( 'DOING_AJAX', true );
			wc_maybe_define_constant( 'WC_DOING_AJAX', true );
			if ( ! WP_DEBUG || ( WP_DEBUG && ! WP_DEBUG_DISPLAY ) ) {
				@ini_set( 'display_errors', 0 ); // Turn off display_errors during AJAX events to prevent malformed JSON.
			}
			$GLOBALS['wpdb']->hide_errors();
		}
		// phpcs:enable
	}

	/**
	 * Send headers for WC Ajax Requests.
	 *
	 * @since 2.5.0
	 */
	private static function wc_ajax_headers() {
		if ( ! headers_sent() ) {
			send_origin_headers();
			send_nosniff_header();
			wc_nocache_headers();
			header( 'Content-Type: text/html; charset=' . get_option( 'blog_charset' ) );
			header( 'X-Robots-Tag: noindex' );
			status_header( 200 );
		} elseif ( Constants::is_true( 'WP_DEBUG' ) ) {
			headers_sent( $file, $line );
			trigger_error( "wc_ajax_headers cannot set headers - headers already sent by {$file} on line {$line}", E_USER_NOTICE ); // @codingStandardsIgnoreLine
		}
	}

	/**
	 * Check for WC Ajax request and fire action.
	 */
	public static function do_wc_ajax() {
		global $wp_query;

		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		if ( ! empty( $_GET['wc-ajax'] ) ) {
			$wp_query->set( 'wc-ajax', sanitize_text_field( wp_unslash( $_GET['wc-ajax'] ) ) );
		}

		$action = $wp_query->get( 'wc-ajax' );

		if ( $action ) {
			self::wc_ajax_headers();
			$action = sanitize_text_field( $action );
			do_action( 'wc_ajax_' . $action );
			wp_die();
		}
		// phpcs:enable
	}

	/**
	 * Hook in methods - uses WordPress ajax handlers (admin-ajax).
	 */
	public static function add_ajax_events() {
		$ajax_events_nopriv = array(
			'get_refreshed_fragments',
			'apply_coupon',
			'remove_coupon',
			'update_shipping_method',
			'get_cart_totals',
			'update_order_review',
			'add_to_cart',
			'remove_from_cart',
			'checkout',
			'get_variation',
			'get_customer_location',
		);

		foreach ( $ajax_events_nopriv as $ajax_event ) {
			add_action( 'wp_ajax_woocommerce_' . $ajax_event, array( __CLASS__, $ajax_event ) );
			add_action( 'wp_ajax_nopriv_woocommerce_' . $ajax_event, array( __CLASS__, $ajax_event ) );

			// WC AJAX can be used for frontend ajax requests.
			add_action( 'wc_ajax_' . $ajax_event, array( __CLASS__, $ajax_event ) );
		}

		$ajax_events = array(
			'feature_product',
			'mark_order_status',
			'get_order_details',
			'add_attribute',
			'add_new_attribute',
			'remove_variations',
			'save_attributes',
			'add_variation',
			'link_all_variations',
			'revoke_access_to_download',
			'grant_access_to_download',
			'get_customer_details',
			'add_order_item',
			'add_order_fee',
			'add_order_shipping',
			'add_order_tax',
			'add_coupon_discount',
			'remove_order_coupon',
			'remove_order_item',
			'remove_order_tax',
			'reduce_order_item_stock',
			'increase_order_item_stock',
			'add_order_item_meta',
			'remove_order_item_meta',
			'calc_line_taxes',
			'save_order_items',
			'load_order_items',
			'add_order_note',
			'delete_order_note',
			'json_search_products',
			'json_search_products_and_variations',
			'json_search_downloadable_products_and_variations',
			'json_search_customers',
			'json_search_categories',
			'json_search_pages',
			'term_ordering',
			'product_ordering',
			'refund_line_items',
			'delete_refund',
			'rated',
			'update_api_key',
			'load_variations',
			'save_variations',
			'bulk_edit_variations',
			'tax_rates_save_changes',
			'shipping_zones_save_changes',
			'shipping_zone_add_method',
			'shipping_zone_methods_save_changes',
			'shipping_zone_methods_save_settings',
			'shipping_classes_save_changes',
			'toggle_gateway_enabled',
		);

		foreach ( $ajax_events as $ajax_event ) {
			add_action( 'wp_ajax_woocommerce_' . $ajax_event, array( __CLASS__, $ajax_event ) );
		}
	}

	/**
	 * Get a refreshed cart fragment, including the mini cart HTML.
	 */
	public static function get_refreshed_fragments() {
		ob_start();

		woocommerce_mini_cart();

		$mini_cart = ob_get_clean();

		$data = array(
			'fragments' => apply_filters(
				'woocommerce_add_to_cart_fragments',
				array(
					'div.widget_shopping_cart_content' => '<div class="widget_shopping_cart_content">' . $mini_cart . '</div>',
				)
			),
			'cart_hash' => WC()->cart->get_cart_hash(),
		);

		wp_send_json( $data );
	}

	/**
	 * AJAX apply coupon on checkout page.
	 */
	public static function apply_coupon() {

		check_ajax_referer( 'apply-coupon', 'security' );

		if ( ! empty( $_POST['coupon_code'] ) ) {
			WC()->cart->add_discount( wc_format_coupon_code( wp_unslash( $_POST['coupon_code'] ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		} else {
			wc_add_notice( WC_Coupon::get_generic_coupon_error( WC_Coupon::E_WC_COUPON_PLEASE_ENTER ), 'error' );
		}

		wc_print_notices();
		wp_die();
	}

	/**
	 * AJAX remove coupon on cart and checkout page.
	 */
	public static function remove_coupon() {
		check_ajax_referer( 'remove-coupon', 'security' );

		$coupon = isset( $_POST['coupon'] ) ? wc_format_coupon_code( wp_unslash( $_POST['coupon'] ) ) : false; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

		if ( empty( $coupon ) ) {
			wc_add_notice( __( 'Sorry there was a problem removing this coupon.', 'woocommerce' ), 'error' );
		} else {
			WC()->cart->remove_coupon( $coupon );
			wc_add_notice( __( 'Coupon has been removed.', 'woocommerce' ) );
		}

		wc_print_notices();
		wp_die();
	}

	/**
	 * AJAX update shipping method on cart page.
	 */
	public static function update_shipping_method() {
		check_ajax_referer( 'update-shipping-method', 'security' );

		wc_maybe_define_constant( 'WOOCOMMERCE_CART', true );

		$chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' );
		$posted_shipping_methods = isset( $_POST['shipping_method'] ) ? wc_clean( wp_unslash( $_POST['shipping_method'] ) ) : array();

		if ( is_array( $posted_shipping_methods ) ) {
			foreach ( $posted_shipping_methods as $i => $value ) {
				$chosen_shipping_methods[ $i ] = $value;
			}
		}

		WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods );

		self::get_cart_totals();
	}

	/**
	 * AJAX receive updated cart_totals div.
	 */
	public static function get_cart_totals() {
		wc_maybe_define_constant( 'WOOCOMMERCE_CART', true );
		WC()->cart->calculate_totals();
		woocommerce_cart_totals();
		wp_die();
	}

	/**
	 * Session has expired.
	 */
	private static function update_order_review_expired() {
		wp_send_json(
			array(
				'fragments' => apply_filters(
					'woocommerce_update_order_review_fragments',
					array(
						'form.woocommerce-checkout' => '<div class="woocommerce-error">' . __( 'Sorry, your session has expired.', 'woocommerce' ) . ' <a href="' . esc_url( wc_get_page_permalink( 'shop' ) ) . '" class="wc-backward">' . __( 'Return to shop', 'woocommerce' ) . '</a></div>',
					)
				),
			)
		);
	}

	/**
	 * AJAX update order review on checkout.
	 */
	public static function update_order_review() {
		check_ajax_referer( 'update-order-review', 'security' );

		wc_maybe_define_constant( 'WOOCOMMERCE_CHECKOUT', true );

		if ( WC()->cart->is_empty() && ! is_customize_preview() && apply_filters( 'woocommerce_checkout_update_order_review_expired', true ) ) {
			self::update_order_review_expired();
		}

		do_action( 'woocommerce_checkout_update_order_review', isset( $_POST['post_data'] ) ? wp_unslash( $_POST['post_data'] ) : '' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

		$chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' );
		$posted_shipping_methods = isset( $_POST['shipping_method'] ) ? wc_clean( wp_unslash( $_POST['shipping_method'] ) ) : array();

		if ( is_array( $posted_shipping_methods ) ) {
			foreach ( $posted_shipping_methods as $i => $value ) {
				$chosen_shipping_methods[ $i ] = $value;
			}
		}

		WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods );
		WC()->session->set( 'chosen_payment_method', empty( $_POST['payment_method'] ) ? '' : wc_clean( wp_unslash( $_POST['payment_method'] ) ) );
		WC()->customer->set_props(
			array(
				'billing_country'   => isset( $_POST['country'] ) ? wc_clean( wp_unslash( $_POST['country'] ) ) : null,
				'billing_state'     => isset( $_POST['state'] ) ? wc_clean( wp_unslash( $_POST['state'] ) ) : null,
				'billing_postcode'  => isset( $_POST['postcode'] ) ? wc_clean( wp_unslash( $_POST['postcode'] ) ) : null,
				'billing_city'      => isset( $_POST['city'] ) ? wc_clean( wp_unslash( $_POST['city'] ) ) : null,
				'billing_address_1' => isset( $_POST['address'] ) ? wc_clean( wp_unslash( $_POST['address'] ) ) : null,
				'billing_address_2' => isset( $_POST['address_2'] ) ? wc_clean( wp_unslash( $_POST['address_2'] ) ) : null,
			)
		);

		if ( wc_ship_to_billing_address_only() ) {
			WC()->customer->set_props(
				array(
					'shipping_country'   => isset( $_POST['country'] ) ? wc_clean( wp_unslash( $_POST['country'] ) ) : null,
					'shipping_state'     => isset( $_POST['state'] ) ? wc_clean( wp_unslash( $_POST['state'] ) ) : null,
					'shipping_postcode'  => isset( $_POST['postcode'] ) ? wc_clean( wp_unslash( $_POST['postcode'] ) ) : null,
					'shipping_city'      => isset( $_POST['city'] ) ? wc_clean( wp_unslash( $_POST['city'] ) ) : null,
					'shipping_address_1' => isset( $_POST['address'] ) ? wc_clean( wp_unslash( $_POST['address'] ) ) : null,
					'shipping_address_2' => isset( $_POST['address_2'] ) ? wc_clean( wp_unslash( $_POST['address_2'] ) ) : null,
				)
			);
		} else {
			WC()->customer->set_props(
				array(
					'shipping_country'   => isset( $_POST['s_country'] ) ? wc_clean( wp_unslash( $_POST['s_country'] ) ) : null,
					'shipping_state'     => isset( $_POST['s_state'] ) ? wc_clean( wp_unslash( $_POST['s_state'] ) ) : null,
					'shipping_postcode'  => isset( $_POST['s_postcode'] ) ? wc_clean( wp_unslash( $_POST['s_postcode'] ) ) : null,
					'shipping_city'      => isset( $_POST['s_city'] ) ? wc_clean( wp_unslash( $_POST['s_city'] ) ) : null,
					'shipping_address_1' => isset( $_POST['s_address'] ) ? wc_clean( wp_unslash( $_POST['s_address'] ) ) : null,
					'shipping_address_2' => isset( $_POST['s_address_2'] ) ? wc_clean( wp_unslash( $_POST['s_address_2'] ) ) : null,
				)
			);
		}

		if ( isset( $_POST['has_full_address'] ) && wc_string_to_bool( wc_clean( wp_unslash( $_POST['has_full_address'] ) ) ) ) {
			WC()->customer->set_calculated_shipping( true );
		} else {
			WC()->customer->set_calculated_shipping( false );
		}

		WC()->customer->save();

		// Calculate shipping before totals. This will ensure any shipping methods that affect things like taxes are chosen prior to final totals being calculated. Ref: #22708.
		WC()->cart->calculate_shipping();
		WC()->cart->calculate_totals();

		// Get order review fragment.
		ob_start();
		woocommerce_order_review();
		$woocommerce_order_review = ob_get_clean();

		// Get checkout payment fragment.
		ob_start();
		woocommerce_checkout_payment();
		$woocommerce_checkout_payment = ob_get_clean();

		// Get messages if reload checkout is not true.
		$reload_checkout = isset( WC()->session->reload_checkout );
		if ( ! $reload_checkout ) {
			$messages = wc_print_notices( true );
		} else {
			$messages = '';
		}

		unset( WC()->session->refresh_totals, WC()->session->reload_checkout );

		wp_send_json(
			array(
				'result'    => empty( $messages ) ? 'success' : 'failure',
				'messages'  => $messages,
				'reload'    => $reload_checkout,
				'fragments' => apply_filters(
					'woocommerce_update_order_review_fragments',
					array(
						'.woocommerce-checkout-review-order-table' => $woocommerce_order_review,
						'.woocommerce-checkout-payment' => $woocommerce_checkout_payment,
					)
				),
			)
		);
	}

	/**
	 * AJAX add to cart.
	 */
	public static function add_to_cart() {
		ob_start();

		// phpcs:disable WordPress.Security.NonceVerification.Missing
		if ( ! isset( $_POST['product_id'] ) ) {
			return;
		}

		$product_id        = apply_filters( 'woocommerce_add_to_cart_product_id', absint( $_POST['product_id'] ) );
		$product           = wc_get_product( $product_id );
		$quantity          = empty( $_POST['quantity'] ) ? 1 : wc_stock_amount( wp_unslash( $_POST['quantity'] ) );
		$passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity );
		$product_status    = get_post_status( $product_id );
		$variation_id      = 0;
		$variation         = array();

		if ( $product && 'variation' === $product->get_type() ) {
			$variation_id = $product_id;
			$product_id   = $product->get_parent_id();
			$variation    = $product->get_variation_attributes();
		}

		if ( $passed_validation && false !== WC()->cart->add_to_cart( $product_id, $quantity, $variation_id, $variation ) && 'publish' === $product_status ) {

			do_action( 'woocommerce_ajax_added_to_cart', $product_id );

			if ( 'yes' === get_option( 'woocommerce_cart_redirect_after_add' ) ) {
				wc_add_to_cart_message( array( $product_id => $quantity ), true );
			}

			self::get_refreshed_fragments();

		} else {

			// If there was an error adding to the cart, redirect to the product page to show any errors.
			$data = array(
				'error'       => true,
				'product_url' => apply_filters( 'woocommerce_cart_redirect_after_error', get_permalink( $product_id ), $product_id ),
			);

			wp_send_json( $data );
		}
		// phpcs:enable
	}

	/**
	 * AJAX remove from cart.
	 */
	public static function remove_from_cart() {
		ob_start();

		// phpcs:ignore WordPress.Security.NonceVerification.Missing
		$cart_item_key = wc_clean( isset( $_POST['cart_item_key'] ) ? wp_unslash( $_POST['cart_item_key'] ) : '' );

		if ( $cart_item_key && false !== WC()->cart->remove_cart_item( $cart_item_key ) ) {
			self::get_refreshed_fragments();
		} else {
			wp_send_json_error();
		}
	}

	/**
	 * Process ajax checkout form.
	 */
	public static function checkout() {
		wc_maybe_define_constant( 'WOOCOMMERCE_CHECKOUT', true );
		WC()->checkout()->process_checkout();
		wp_die( 0 );
	}

	/**
	 * Get a matching variation based on posted attributes.
	 */
	public static function get_variation() {
		ob_start();

		// phpcs:disable WordPress.Security.NonceVerification.Missing
		if ( empty( $_POST['product_id'] ) ) {
			wp_die();
		}

		$variable_product = wc_get_product( absint( $_POST['product_id'] ) );

		if ( ! $variable_product ) {
			wp_die();
		}

		$data_store   = WC_Data_Store::load( 'product' );
		$variation_id = $data_store->find_matching_product_variation( $variable_product, wp_unslash( $_POST ) );
		$variation    = $variation_id ? $variable_product->get_available_variation( $variation_id ) : false;
		wp_send_json( $variation );
		// phpcs:enable
	}

	/**
	 * Locate user via AJAX.
	 */
	public static function get_customer_location() {
		$location_hash = WC_Cache_Helper::geolocation_ajax_get_location_hash();
		wp_send_json_success( array( 'hash' => $location_hash ) );
	}

	/**
	 * Toggle Featured status of a product from admin.
	 */
	public static function feature_product() {
		if ( current_user_can( 'edit_products' ) && check_admin_referer( 'woocommerce-feature-product' ) && isset( $_GET['product_id'] ) ) {
			$product = wc_get_product( absint( $_GET['product_id'] ) );

			if ( $product ) {
				$product->set_featured( ! $product->get_featured() );
				$product->save();
			}
		}

		wp_safe_redirect( wp_get_referer() ? remove_query_arg( array( 'trashed', 'untrashed', 'deleted', 'ids' ), wp_get_referer() ) : admin_url( 'edit.php?post_type=product' ) );
		exit;
	}

	/**
	 * Mark an order with a status.
	 */
	public static function mark_order_status() {
		if ( current_user_can( 'edit_shop_orders' ) && check_admin_referer( 'woocommerce-mark-order-status' ) && isset( $_GET['status'], $_GET['order_id'] ) ) {
			$status = sanitize_text_field( wp_unslash( $_GET['status'] ) );
			$order  = wc_get_order( absint( wp_unslash( $_GET['order_id'] ) ) );

			if ( wc_is_order_status( 'wc-' . $status ) && $order ) {
				// Initialize payment gateways in case order has hooked status transition actions.
				WC()->payment_gateways();

				$order->update_status( $status, '', true );
				do_action( 'woocommerce_order_edit_status', $order->get_id(), $status );
			}
		}

		wp_safe_redirect( wp_get_referer() ? wp_get_referer() : admin_url( 'edit.php?post_type=shop_order' ) );
		exit;
	}

	/**
	 * Get order details.
	 */
	public static function get_order_details() {
		check_admin_referer( 'woocommerce-preview-order', 'security' );

		if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_GET['order_id'] ) ) {
			wp_die( -1 );
		}

		$order = wc_get_order( absint( $_GET['order_id'] ) );

		if ( $order ) {
			include_once __DIR__ . '/admin/list-tables/class-wc-admin-list-table-orders.php';

			wp_send_json_success( WC_Admin_List_Table_Orders::order_preview_get_order_details( $order ) );
		}
		wp_die();
	}

	/**
	 * Add an attribute row.
	 */
	public static function add_attribute() {
		ob_start();

		check_ajax_referer( 'add-attribute', 'security' );

		if ( ! current_user_can( 'edit_products' ) || ! isset( $_POST['taxonomy'], $_POST['i'] ) ) {
			wp_die( -1 );
		}

		$i             = absint( $_POST['i'] );
		$metabox_class = array();
		$attribute     = new WC_Product_Attribute();

		$attribute->set_id( wc_attribute_taxonomy_id_by_name( sanitize_text_field( wp_unslash( $_POST['taxonomy'] ) ) ) );
		$attribute->set_name( sanitize_text_field( wp_unslash( $_POST['taxonomy'] ) ) );
		$attribute->set_visible( apply_filters( 'woocommerce_attribute_default_visibility', 1 ) );
		$attribute->set_variation( apply_filters( 'woocommerce_attribute_default_is_variation', 0 ) );

		if ( $attribute->is_taxonomy() ) {
			$metabox_class[] = 'taxonomy';
			$metabox_class[] = $attribute->get_name();
		}

		include __DIR__ . '/admin/meta-boxes/views/html-product-attribute.php';
		wp_die();
	}

	/**
	 * Add a new attribute via ajax function.
	 */
	public static function add_new_attribute() {
		check_ajax_referer( 'add-attribute', 'security' );

		if ( current_user_can( 'manage_product_terms' ) && isset( $_POST['taxonomy'], $_POST['term'] ) ) {
			$taxonomy = esc_attr( wp_unslash( $_POST['taxonomy'] ) ); // phpcs:ignore
			$term     = wc_clean( wp_unslash( $_POST['term'] ) );

			if ( taxonomy_exists( $taxonomy ) ) {

				$result = wp_insert_term( $term, $taxonomy );

				if ( is_wp_error( $result ) ) {
					wp_send_json(
						array(
							'error' => $result->get_error_message(),
						)
					);
				} else {
					$term = get_term_by( 'id', $result['term_id'], $taxonomy );
					wp_send_json(
						array(
							'term_id' => $term->term_id,
							'name'    => $term->name,
							'slug'    => $term->slug,
						)
					);
				}
			}
		}
		wp_die( -1 );
	}

	/**
	 * Delete variations via ajax function.
	 */
	public static function remove_variations() {
		check_ajax_referer( 'delete-variations', 'security' );

		if ( current_user_can( 'edit_products' ) && isset( $_POST['variation_ids'] ) ) {
			$variation_ids = array_map( 'absint', (array) wp_unslash( $_POST['variation_ids'] ) );

			foreach ( $variation_ids as $variation_id ) {
				if ( 'product_variation' === get_post_type( $variation_id ) ) {
					$variation = wc_get_product( $variation_id );
					$variation->delete( true );
				}
			}
		}

		wp_die( -1 );
	}

	/**
	 * Save attributes via ajax.
	 */
	public static function save_attributes() {
		check_ajax_referer( 'save-attributes', 'security' );

		if ( ! current_user_can( 'edit_products' ) || ! isset( $_POST['data'], $_POST['post_id'] ) ) {
			wp_die( -1 );
		}

		$response = array();

		try {
			parse_str( wp_unslash( $_POST['data'] ), $data ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

			$attributes   = WC_Meta_Box_Product_Data::prepare_attributes( $data );
			$product_id   = absint( wp_unslash( $_POST['post_id'] ) );
			$product_type = ! empty( $_POST['product_type'] ) ? wc_clean( wp_unslash( $_POST['product_type'] ) ) : 'simple';
			$classname    = WC_Product_Factory::get_product_classname( $product_id, $product_type );
			$product      = new $classname( $product_id );

			$product->set_attributes( $attributes );
			$product->save();

			ob_start();
			$attributes = $product->get_attributes( 'edit' );
			$i          = -1;
			if ( ! empty( $data['attribute_names'] ) ) {
				foreach ( $data['attribute_names'] as $attribute_name ) {
					$attribute = isset( $attributes[ sanitize_title( $attribute_name ) ] ) ? $attributes[ sanitize_title( $attribute_name ) ] : false;
					if ( ! $attribute ) {
						continue;
					}
					$i++;
					$metabox_class = array();

					if ( $attribute->is_taxonomy() ) {
						$metabox_class[] = 'taxonomy';
						$metabox_class[] = $attribute->get_name();
					}

					include __DIR__ . '/admin/meta-boxes/views/html-product-attribute.php';
				}
			}

			$response['html'] = ob_get_clean();
		} catch ( Exception $e ) {
			wp_send_json_error( array( 'error' => $e->getMessage() ) );
		}

		// wp_send_json_success must be outside the try block not to break phpunit tests.
		wp_send_json_success( $response );
	}

	/**
	 * Add variation via ajax function.
	 */
	public static function add_variation() {
		check_ajax_referer( 'add-variation', 'security' );

		if ( ! current_user_can( 'edit_products' ) || ! isset( $_POST['post_id'], $_POST['loop'] ) ) {
			wp_die( -1 );
		}

		global $post; // Set $post global so its available, like within the admin screens.

		$product_id       = intval( $_POST['post_id'] );
		$post             = get_post( $product_id ); // phpcs:ignore
		$loop             = intval( $_POST['loop'] );
		$product_object   = wc_get_product_object( 'variable', $product_id ); // Forces type to variable in case product is unsaved.
		$variation_object = wc_get_product_object( 'variation' );
		$variation_object->set_parent_id( $product_id );
		$variation_object->set_attributes( array_fill_keys( array_map( 'sanitize_title', array_keys( $product_object->get_variation_attributes() ) ), '' ) );
		$variation_id   = $variation_object->save();
		$variation      = get_post( $variation_id );
		$variation_data = array_merge( get_post_custom( $variation_id ), wc_get_product_variation_attributes( $variation_id ) ); // kept for BW compatibility.
		include __DIR__ . '/admin/meta-boxes/views/html-variation-admin.php';
		wp_die();
	}

	/**
	 * Link all variations via ajax function.
	 */
	public static function link_all_variations() {
		check_ajax_referer( 'link-variations', 'security' );

		if ( ! current_user_can( 'edit_products' ) ) {
			wp_die( -1 );
		}

		wc_maybe_define_constant( 'WC_MAX_LINKED_VARIATIONS', 50 );
		wc_set_time_limit( 0 );

		$post_id = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0;

		if ( ! $post_id ) {
			wp_die();
		}

		$product    = wc_get_product( $post_id );
		$data_store = $product->get_data_store();

		if ( ! is_callable( array( $data_store, 'create_all_product_variations' ) ) ) {
			wp_die();
		}

		echo esc_html( $data_store->create_all_product_variations( $product, Constants::get_constant( 'WC_MAX_LINKED_VARIATIONS' ) ) );

		$data_store->sort_all_product_variations( $product->get_id() );
		wp_die();
	}

	/**
	 * Delete download permissions via ajax function.
	 */
	public static function revoke_access_to_download() {
		check_ajax_referer( 'revoke-access', 'security' );

		if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['download_id'], $_POST['product_id'], $_POST['order_id'], $_POST['permission_id'] ) ) {
			wp_die( -1 );
		}
		$download_id   = wc_clean( wp_unslash( $_POST['download_id'] ) );
		$product_id    = intval( $_POST['product_id'] );
		$order_id      = intval( $_POST['order_id'] );
		$permission_id = absint( $_POST['permission_id'] );
		$data_store    = WC_Data_Store::load( 'customer-download' );
		$data_store->delete_by_id( $permission_id );

		do_action( 'woocommerce_ajax_revoke_access_to_product_download', $download_id, $product_id, $order_id, $permission_id );

		wp_die();
	}

	/**
	 * Grant download permissions via ajax function.
	 */
	public static function grant_access_to_download() {

		check_ajax_referer( 'grant-access', 'security' );

		if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['loop'], $_POST['order_id'], $_POST['product_ids'] ) ) {
			wp_die( -1 );
		}

		global $wpdb;

		$wpdb->hide_errors();

		$order_id     = intval( $_POST['order_id'] );
		$product_ids  = array_filter( array_map( 'absint', (array) wp_unslash( $_POST['product_ids'] ) ) );
		$loop         = intval( $_POST['loop'] );
		$file_counter = 0;
		$order        = wc_get_order( $order_id );

		if ( ! $order->get_billing_email() ) {
			wp_die();
		}

		$data  = array();
		$items = $order->get_items();

		// Check against order items first.
		foreach ( $items as $item ) {
			$product = $item->get_product();

			if ( $product && $product->exists() && in_array( $product->get_id(), $product_ids, true ) && $product->is_downloadable() ) {
				$data[ $product->get_id() ] = array(
					'files'      => $product->get_downloads(),
					'quantity'   => $item->get_quantity(),
					'order_item' => $item,
				);
			}
		}

		foreach ( $product_ids as $product_id ) {
			$product = wc_get_product( $product_id );

			if ( isset( $data[ $product->get_id() ] ) ) {
				$download_data = $data[ $product->get_id() ];
			} else {
				$download_data = array(
					'files'      => $product->get_downloads(),
					'quantity'   => 1,
					'order_item' => null,
				);
			}

			if ( ! empty( $download_data['files'] ) ) {
				foreach ( $download_data['files'] as $download_id => $file ) {
					$inserted_id = wc_downloadable_file_permission( $download_id, $product->get_id(), $order, $download_data['quantity'], $download_data['order_item'] );
					if ( $inserted_id ) {
						$download = new WC_Customer_Download( $inserted_id );
						$loop ++;
						$file_counter ++;

						if ( $file->get_name() ) {
							$file_count = $file->get_name();
						} else {
							/* translators: %d file count */
							$file_count = sprintf( __( 'File %d', 'woocommerce' ), $file_counter );
						}
						include __DIR__ . '/admin/meta-boxes/views/html-order-download-permission.php';
					}
				}
			}
		}
		wp_die();
	}

	/**
	 * Get customer details via ajax.
	 */
	public static function get_customer_details() {
		check_ajax_referer( 'get-customer-details', 'security' );

		if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['user_id'] ) ) {
			wp_die( -1 );
		}

		$user_id  = absint( $_POST['user_id'] );
		$customer = new WC_Customer( $user_id );

		if ( has_filter( 'woocommerce_found_customer_details' ) ) {
			wc_deprecated_function( 'The woocommerce_found_customer_details filter', '3.0', 'woocommerce_ajax_get_customer_details' );
		}

		$data                  = $customer->get_data();
		$data['date_created']  = $data['date_created'] ? $data['date_created']->getTimestamp() : null;
		$data['date_modified'] = $data['date_modified'] ? $data['date_modified']->getTimestamp() : null;

		$customer_data = apply_filters( 'woocommerce_ajax_get_customer_details', $data, $customer, $user_id );
		wp_send_json( $customer_data );
	}

	/**
	 * Add order item via ajax. Used on the edit order screen in WP Admin.
	 *
	 * @throws Exception If order is invalid.
	 */
	public static function add_order_item() {
		check_ajax_referer( 'order-item', 'security' );

		if ( ! current_user_can( 'edit_shop_orders' ) ) {
			wp_die( -1 );
		}

		if ( ! isset( $_POST['order_id'] ) ) {
			throw new Exception( __( 'Invalid order', 'woocommerce' ) );
		}
		$order_id = absint( wp_unslash( $_POST['order_id'] ) );

		// If we passed through items it means we need to save first before adding a new one.
		$items = ( ! empty( $_POST['items'] ) ) ? wp_unslash( $_POST['items'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

		$items_to_add = isset( $_POST['data'] ) ? array_filter( wp_unslash( (array) $_POST['data'] ) ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

		try {
			$response = self::maybe_add_order_item( $order_id, $items, $items_to_add );
			wp_send_json_success( $response );
		} catch ( Exception $e ) {
			wp_send_json_error( array( 'error' => $e->getMessage() ) );
		}
	}

	/**
	 * Add order item via AJAX. This is refactored for better unit testing.
	 *
	 * @param int          $order_id     ID of order to add items to.
	 * @param string|array $items        Existing items in order. Empty string if no items to add.
	 * @param array        $items_to_add Array of items to add.
	 *
	 * @return array     Fragments to render and notes HTML.
	 * @throws Exception When unable to add item.
	 */
	private static function maybe_add_order_item( $order_id, $items, $items_to_add ) {
		try {
			$order = wc_get_order( $order_id );

			if ( ! $order ) {
				throw new Exception( __( 'Invalid order', 'woocommerce' ) );
			}

			if ( ! empty( $items ) ) {
				$save_items = array();
				parse_str( $items, $save_items );
				wc_save_order_items( $order->get_id(), $save_items );
			}

			// Add items to order.
			$order_notes = array();
			$added_items = array();

			foreach ( $items_to_add as $item ) {
				if ( ! isset( $item['id'], $item['qty'] ) || empty( $item['id'] ) ) {
					continue;
				}
				$product_id = absint( $item['id'] );
				$qty        = wc_stock_amount( $item['qty'] );
				$product    = wc_get_product( $product_id );

				if ( ! $product ) {
					throw new Exception( __( 'Invalid product ID', 'woocommerce' ) . ' ' . $product_id );
				}
				if ( 'variable' === $product->get_type() ) {
					/* translators: %s product name */
					throw new Exception( sprintf( __( '%s is a variable product parent and cannot be added.', 'woocommerce' ), $product->get_name() ) );
				}
				$validation_error = new WP_Error();
				$validation_error = apply_filters( 'woocommerce_ajax_add_order_item_validation', $validation_error, $product, $order, $qty );

				if ( $validation_error->get_error_code() ) {
					/* translators: %s: error message */
					throw new Exception( sprintf( __( 'Error: %s', 'woocommerce' ), $validation_error->get_error_message() ) );
				}
				$item_id                 = $order->add_product( $product, $qty, array( 'order' => $order ) );
				$item                    = apply_filters( 'woocommerce_ajax_order_item', $order->get_item( $item_id ), $item_id, $order, $product );
				$added_items[ $item_id ] = $item;
				$order_notes[ $item_id ] = $product->get_formatted_name();

				// We do not perform any stock operations here because they will be handled when order is moved to a status where stock operations are applied (like processing, completed etc).

				do_action( 'woocommerce_ajax_add_order_item_meta', $item_id, $item, $order );
			}

			/* translators: %s item name. */
			$order->add_order_note( sprintf( __( 'Added line items: %s', 'woocommerce' ), implode( ', ', $order_notes ) ), false, true );

			do_action( 'woocommerce_ajax_order_items_added', $added_items, $order );

			$data = get_post_meta( $order_id );

			// Get HTML to return.
			ob_start();
			include __DIR__ . '/admin/meta-boxes/views/html-order-items.php';
			$items_html = ob_get_clean();

			ob_start();
			$notes = wc_get_order_notes( array( 'order_id' => $order_id ) );
			include __DIR__ . '/admin/meta-boxes/views/html-order-notes.php';
			$notes_html = ob_get_clean();

			return array(
				'html'       => $items_html,
				'notes_html' => $notes_html,
			);
		} catch ( Exception $e ) {
			throw $e; // Forward exception to caller.
		}
	}

	/**
	 * Add order fee via ajax.
	 *
	 * @throws Exception If order is invalid.
	 */
	public static function add_order_fee() {
		check_ajax_referer( 'order-item', 'security' );

		if ( ! current_user_can( 'edit_shop_orders' ) ) {
			wp_die( -1 );
		}

		$response = array();

		try {
			$order_id = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0;
			$order    = wc_get_order( $order_id );

			if ( ! $order ) {
				throw new Exception( __( 'Invalid order', 'woocommerce' ) );
			}

			$amount = isset( $_POST['amount'] ) ? wc_clean( wp_unslash( $_POST['amount'] ) ) : 0;

			$calculate_tax_args = array(
				'country'  => isset( $_POST['country'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['country'] ) ) ) : '',
				'state'    => isset( $_POST['state'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['state'] ) ) ) : '',
				'postcode' => isset( $_POST['postcode'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['postcode'] ) ) ) : '',
				'city'     => isset( $_POST['city'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['city'] ) ) ) : '',
			);

			if ( strstr( $amount, '%' ) ) {
				// We need to calculate totals first, so that $order->get_total() is correct.
				$order->calculate_totals( false );
				$formatted_amount = $amount;
				$percent          = floatval( trim( $amount, '%' ) );
				$amount           = $order->get_total() * ( $percent / 100 );
			} else {
				$amount           = floatval( $amount );
				$formatted_amount = wc_price( $amount, array( 'currency' => $order->get_currency() ) );
			}

			$fee = new WC_Order_Item_Fee();
			$fee->set_amount( $amount );
			$fee->set_total( $amount );
			/* translators: %s fee amount */
			$fee->set_name( sprintf( __( '%s fee', 'woocommerce' ), wc_clean( $formatted_amount ) ) );

			$order->add_item( $fee );
			$order->calculate_taxes( $calculate_tax_args );
			$order->calculate_totals( false );
			$order->save();

			ob_start();
			include __DIR__ . '/admin/meta-boxes/views/html-order-items.php';
			$response['html'] = ob_get_clean();
		} catch ( Exception $e ) {
			wp_send_json_error( array( 'error' => $e->getMessage() ) );
		}

		// wp_send_json_success must be outside the try block not to break phpunit tests.
		wp_send_json_success( $response );
	}

	/**
	 * Add order shipping cost via ajax.
	 *
	 * @throws Exception If order is invalid.
	 */
	public static function add_order_shipping() {
		check_ajax_referer( 'order-item', 'security' );

		if ( ! current_user_can( 'edit_shop_orders' ) ) {
			wp_die( -1 );
		}

		$response = array();

		try {
			$order_id = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0;
			$order    = wc_get_order( $order_id );

			if ( ! $order ) {
				throw new Exception( __( 'Invalid order', 'woocommerce' ) );
			}

			$order_taxes      = $order->get_taxes();
			$shipping_methods = WC()->shipping() ? WC()->shipping()->load_shipping_methods() : array();

			// Add new shipping.
			$item = new WC_Order_Item_Shipping();
			$item->set_shipping_rate( new WC_Shipping_Rate() );
			$item->set_order_id( $order_id );
			$item_id = $item->save();

			ob_start();
			include __DIR__ . '/admin/meta-boxes/views/html-order-shipping.php';
			$response['html'] = ob_get_clean();
		} catch ( Exception $e ) {
			wp_send_json_error( array( 'error' => $e->getMessage() ) );
		}

		// wp_send_json_success must be outside the try block not to break phpunit tests.
		wp_send_json_success( $response );
	}

	/**
	 * Add order tax column via ajax.
	 *
	 * @throws Exception If order or tax rate is invalid.
	 */
	public static function add_order_tax() {
		check_ajax_referer( 'order-item', 'security' );

		if ( ! current_user_can( 'edit_shop_orders' ) ) {
			wp_die( -1 );
		}

		$response = array();

		try {
			$order_id = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0;
			$order    = wc_get_order( $order_id );

			if ( ! $order ) {
				throw new Exception( __( 'Invalid order', 'woocommerce' ) );
			}

			$rate_id = isset( $_POST['rate_id'] ) ? absint( $_POST['rate_id'] ) : '';

			if ( ! $rate_id ) {
				throw new Exception( __( 'Invalid rate', 'woocommerce' ) );
			}

			$data = get_post_meta( $order_id );

			// Add new tax.
			$item = new WC_Order_Item_Tax();
			$item->set_rate( $rate_id );
			$item->set_order_id( $order_id );
			$item->save();

			ob_start();
			include __DIR__ . '/admin/meta-boxes/views/html-order-items.php';
			$response['html'] = ob_get_clean();
		} catch ( Exception $e ) {
			wp_send_json_error( array( 'error' => $e->getMessage() ) );
		}

		// wp_send_json_success must be outside the try block not to break phpunit tests.
		wp_send_json_success( $response );
	}

	/**
	 * Add order discount via ajax.
	 *
	 * @throws Exception If order or coupon is invalid.
	 */
	public static function add_coupon_discount() {
		check_ajax_referer( 'order-item', 'security' );

		if ( ! current_user_can( 'edit_shop_orders' ) ) {
			wp_die( -1 );
		}

		$response = array();

		try {
			$order_id           = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0;
			$order              = wc_get_order( $order_id );
			$calculate_tax_args = array(
				'country'  => isset( $_POST['country'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['country'] ) ) ) : '',
				'state'    => isset( $_POST['state'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['state'] ) ) ) : '',
				'postcode' => isset( $_POST['postcode'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['postcode'] ) ) ) : '',
				'city'     => isset( $_POST['city'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['city'] ) ) ) : '',
			);

			if ( ! $order ) {
				throw new Exception( __( 'Invalid order', 'woocommerce' ) );
			}

			if ( empty( $_POST['coupon'] ) ) {
				throw new Exception( __( 'Invalid coupon', 'woocommerce' ) );
			}

			// Add user ID and/or email so validation for coupon limits works.
			$user_id_arg    = isset( $_POST['user_id'] ) ? absint( $_POST['user_id'] ) : 0;
			$user_email_arg = isset( $_POST['user_email'] ) ? sanitize_email( wp_unslash( $_POST['user_email'] ) ) : '';

			if ( $user_id_arg ) {
				$order->set_customer_id( $user_id_arg );
			}
			if ( $user_email_arg ) {
				$order->set_billing_email( $user_email_arg );
			}

			$result = $order->apply_coupon( wc_format_coupon_code( wp_unslash( $_POST['coupon'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

			if ( is_wp_error( $result ) ) {
				throw new Exception( html_entity_decode( wp_strip_all_tags( $result->get_error_message() ) ) );
			}

			$order->calculate_taxes( $calculate_tax_args );
			$order->calculate_totals( false );

			ob_start();
			include __DIR__ . '/admin/meta-boxes/views/html-order-items.php';
			$response['html'] = ob_get_clean();
		} catch ( Exception $e ) {
			wp_send_json_error( array( 'error' => $e->getMessage() ) );
		}

		// wp_send_json_success must be outside the try block not to break phpunit tests.
		wp_send_json_success( $response );
	}

	/**
	 * Remove coupon from an order via ajax.
	 *
	 * @throws Exception If order or coupon is invalid.
	 */
	public static function remove_order_coupon() {
		check_ajax_referer( 'order-item', 'security' );

		if ( ! current_user_can( 'edit_shop_orders' ) ) {
			wp_die( -1 );
		}

		$response = array();

		try {
			$order_id           = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0;
			$order              = wc_get_order( $order_id );
			$calculate_tax_args = array(
				'country'  => isset( $_POST['country'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['country'] ) ) ) : '',
				'state'    => isset( $_POST['state'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['state'] ) ) ) : '',
				'postcode' => isset( $_POST['postcode'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['postcode'] ) ) ) : '',
				'city'     => isset( $_POST['city'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['city'] ) ) ) : '',
			);

			if ( ! $order ) {
				throw new Exception( __( 'Invalid order', 'woocommerce' ) );
			}

			if ( empty( $_POST['coupon'] ) ) {
				throw new Exception( __( 'Invalid coupon', 'woocommerce' ) );
			}

			$order->remove_coupon( wc_format_coupon_code( wp_unslash( $_POST['coupon'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			$order->calculate_taxes( $calculate_tax_args );
			$order->calculate_totals( false );

			ob_start();
			include __DIR__ . '/admin/meta-boxes/views/html-order-items.php';
			$response['html'] = ob_get_clean();
		} catch ( Exception $e ) {
			wp_send_json_error( array( 'error' => $e->getMessage() ) );
		}

		// wp_send_json_success must be outside the try block not to break phpunit tests.
		wp_send_json_success( $response );
	}

	/**
	 * Remove an order item.
	 *
	 * @throws Exception If order is invalid.
	 */
	public static function remove_order_item() {
		check_ajax_referer( 'order-item', 'security' );

		if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['order_id'], $_POST['order_item_ids'] ) ) {
			wp_die( -1 );
		}

		$response = array();

		try {
			$order_id = absint( $_POST['order_id'] );
			$order    = wc_get_order( $order_id );

			if ( ! $order ) {
				throw new Exception( __( 'Invalid order', 'woocommerce' ) );
			}

			if ( ! isset( $_POST['order_item_ids'] ) ) {
				throw new Exception( __( 'Invalid items', 'woocommerce' ) );
			}

			$order_item_ids     = wp_unslash( $_POST['order_item_ids'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			$items              = ( ! empty( $_POST['items'] ) ) ? wp_unslash( $_POST['items'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			$calculate_tax_args = array(
				'country'  => isset( $_POST['country'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['country'] ) ) ) : '',
				'state'    => isset( $_POST['state'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['state'] ) ) ) : '',
				'postcode' => isset( $_POST['postcode'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['postcode'] ) ) ) : '',
				'city'     => isset( $_POST['city'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['city'] ) ) ) : '',
			);

			if ( ! is_array( $order_item_ids ) && is_numeric( $order_item_ids ) ) {
				$order_item_ids = array( $order_item_ids );
			}

			// If we passed through items it means we need to save first before deleting.
			if ( ! empty( $items ) ) {
				$save_items = array();
				parse_str( $items, $save_items );
				wc_save_order_items( $order->get_id(), $save_items );
			}

			if ( ! empty( $order_item_ids ) ) {

				foreach ( $order_item_ids as $item_id ) {
					$item_id = absint( $item_id );
					$item    = $order->get_item( $item_id );

					// Before deleting the item, adjust any stock values already reduced.
					if ( $item->is_type( 'line_item' ) ) {
						$changed_stock = wc_maybe_adjust_line_item_product_stock( $item, 0 );

						if ( $changed_stock && ! is_wp_error( $changed_stock ) ) {
							/* translators: %1$s: item name %2$s: stock change */
							$order->add_order_note( sprintf( __( 'Deleted %1$s and adjusted stock (%2$s)', 'woocommerce' ), $item->get_name(), $changed_stock['from'] . '&rarr;' . $changed_stock['to'] ), false, true );
						} else {
							/* translators: %s item name. */
							$order->add_order_note( sprintf( __( 'Deleted %s', 'woocommerce' ), $item->get_name() ), false, true );
						}
					}

					wc_delete_order_item( $item_id );
				}
			}

			$order = wc_get_order( $order_id );
			$order->calculate_taxes( $calculate_tax_args );
			$order->calculate_totals( false );

			/**
			 * Fires after order items are removed.
			 *
			 * @since 5.2.0
			 *
			 * @param int $item_id WC item ID.
			 * @param WC_Order_Item|false $item As returned by $order->get_item( $item_id ).
			 * @param bool|array|WP_Error $changed_store Result of wc_maybe_adjust_line_item_product_stock().
			 * @param bool|WC_Order|WC_Order_Refund $order As returned by wc_get_order().
			 */
			do_action( 'woocommerce_ajax_order_items_removed', $item_id, $item, $changed_stock, $order );

			// Get HTML to return.
			ob_start();
			include __DIR__ . '/admin/meta-boxes/views/html-order-items.php';
			$items_html = ob_get_clean();

			ob_start();
			$notes = wc_get_order_notes( array( 'order_id' => $order_id ) );
			include __DIR__ . '/admin/meta-boxes/views/html-order-notes.php';
			$notes_html = ob_get_clean();

			wp_send_json_success(
				array(
					'html'       => $items_html,
					'notes_html' => $notes_html,
				)
			);
		} catch ( Exception $e ) {
			wp_send_json_error( array( 'error' => $e->getMessage() ) );
		}

		// wp_send_json_success must be outside the try block not to break phpunit tests.
		wp_send_json_success( $response );
	}

	/**
	 * Remove an order tax.
	 *
	 * @throws Exception If there is an error whilst deleting the rate.
	 */
	public static function remove_order_tax() {
		check_ajax_referer( 'order-item', 'security' );

		if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['order_id'], $_POST['rate_id'] ) ) {
			wp_die( -1 );
		}

		$response = array();

		try {
			$order_id = absint( $_POST['order_id'] );
			$rate_id  = absint( $_POST['rate_id'] );

			$order = wc_get_order( $order_id );
			if ( ! $order->is_editable() ) {
				throw new Exception( __( 'Order not editable', 'woocommerce' ) );
			}

			wc_delete_order_item( $rate_id );

			// Need to load order again after deleting to have latest items before calculating.
			$order = wc_get_order( $order_id );
			$order->calculate_totals( false );

			ob_start();
			include __DIR__ . '/admin/meta-boxes/views/html-order-items.php';
			$response['html'] = ob_get_clean();
		} catch ( Exception $e ) {
			wp_send_json_error( array( 'error' => $e->getMessage() ) );
		}

		// wp_send_json_success must be outside the try block not to break phpunit tests.
		wp_send_json_success( $response );
	}

	/**
	 * Calc line tax.
	 */
	public static function calc_line_taxes() {
		check_ajax_referer( 'calc-totals', 'security' );

		if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['order_id'], $_POST['items'] ) ) {
			wp_die( -1 );
		}

		$order_id           = absint( $_POST['order_id'] );
		$calculate_tax_args = array(
			'country'  => isset( $_POST['country'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['country'] ) ) ) : '',
			'state'    => isset( $_POST['state'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['state'] ) ) ) : '',
			'postcode' => isset( $_POST['postcode'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['postcode'] ) ) ) : '',
			'city'     => isset( $_POST['city'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['city'] ) ) ) : '',
		);

		// Parse the jQuery serialized items.
		$items = array();
		parse_str( wp_unslash( $_POST['items'] ), $items ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

		// Save order items first.
		wc_save_order_items( $order_id, $items );

		// Grab the order and recalculate taxes.
		$order = wc_get_order( $order_id );
		$order->calculate_taxes( $calculate_tax_args );
		$order->calculate_totals( false );
		include __DIR__ . '/admin/meta-boxes/views/html-order-items.php';
		wp_die();
	}

	/**
	 * Save order items via ajax.
	 */
	public static function save_order_items() {
		check_ajax_referer( 'order-item', 'security' );

		if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['order_id'], $_POST['items'] ) ) {
			wp_die( -1 );
		}

		if ( isset( $_POST['order_id'], $_POST['items'] ) ) {
			$order_id = absint( $_POST['order_id'] );

			// Parse the jQuery serialized items.
			$items = array();
			parse_str( wp_unslash( $_POST['items'] ), $items ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

			// Save order items.
			wc_save_order_items( $order_id, $items );

			// Return HTML items.
			$order = wc_get_order( $order_id );

			// Get HTML to return.
			ob_start();
			include __DIR__ . '/admin/meta-boxes/views/html-order-items.php';
			$items_html = ob_get_clean();

			ob_start();
			$notes = wc_get_order_notes( array( 'order_id' => $order_id ) );
			include __DIR__ . '/admin/meta-boxes/views/html-order-notes.php';
			$notes_html = ob_get_clean();

			wp_send_json_success(
				array(
					'html'       => $items_html,
					'notes_html' => $notes_html,
				)
			);
		}
		wp_die();
	}

	/**
	 * Load order items via ajax.
	 */
	public static function load_order_items() {
		check_ajax_referer( 'order-item', 'security' );

		if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['order_id'] ) ) {
			wp_die( -1 );
		}

		// Return HTML items.
		$order_id = absint( $_POST['order_id'] );
		$order    = wc_get_order( $order_id );
		include __DIR__ . '/admin/meta-boxes/views/html-order-items.php';
		wp_die();
	}

	/**
	 * Add order note via ajax.
	 */
	public static function add_order_note() {
		check_ajax_referer( 'add-order-note', 'security' );

		if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['post_id'], $_POST['note'], $_POST['note_type'] ) ) {
			wp_die( -1 );
		}

		$post_id   = absint( $_POST['post_id'] );
		$note      = wp_kses_post( trim( wp_unslash( $_POST['note'] ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$note_type = wc_clean( wp_unslash( $_POST['note_type'] ) );

		$is_customer_note = ( 'customer' === $note_type ) ? 1 : 0;

		if ( $post_id > 0 ) {
			$order      = wc_get_order( $post_id );
			$comment_id = $order->add_order_note( $note, $is_customer_note, true );
			$note       = wc_get_order_note( $comment_id );

			$note_classes   = array( 'note' );
			$note_classes[] = $is_customer_note ? 'customer-note' : '';
			$note_classes   = apply_filters( 'woocommerce_order_note_class', array_filter( $note_classes ), $note );
			?>
			<li rel="<?php echo absint( $note->id ); ?>" class="<?php echo esc_attr( implode( ' ', $note_classes ) ); ?>">
				<div class="note_content">
					<?php echo wp_kses_post( wpautop( wptexturize( make_clickable( $note->content ) ) ) ); ?>
				</div>
				<p class="meta">
					<abbr class="exact-date" title="<?php echo esc_attr( $note->date_created->date( 'y-m-d h:i:s' ) ); ?>">
						<?php
						/* translators: $1: Date created, $2 Time created */
						printf( esc_html__( 'added on %1$s at %2$s', 'woocommerce' ), esc_html( $note->date_created->date_i18n( wc_date_format() ) ), esc_html( $note->date_created->date_i18n( wc_time_format() ) ) );
						?>
					</abbr>
					<?php
					if ( 'system' !== $note->added_by ) :
						/* translators: %s: note author */
						printf( ' ' . esc_html__( 'by %s', 'woocommerce' ), esc_html( $note->added_by ) );
					endif;
					?>
					<a href="#" class="delete_note" role="button"><?php esc_html_e( 'Delete note', 'woocommerce' ); ?></a>
				</p>
			</li>
			<?php
		}
		wp_die();
	}

	/**
	 * Delete order note via ajax.
	 */
	public static function delete_order_note() {
		check_ajax_referer( 'delete-order-note', 'security' );

		if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['note_id'] ) ) {
			wp_die( -1 );
		}

		$note_id = (int) $_POST['note_id'];

		if ( $note_id > 0 ) {
			wc_delete_order_note( $note_id );
		}
		wp_die();
	}

	/**
	 * Search for products and echo json.
	 *
	 * @param string $term (default: '') Term to search for.
	 * @param bool   $include_variations in search or not.
	 */
	public static function json_search_products( $term = '', $include_variations = false ) {
		check_ajax_referer( 'search-products', 'security' );

		if ( empty( $term ) && isset( $_GET['term'] ) ) {
			$term = (string) wc_clean( wp_unslash( $_GET['term'] ) );
		}

		if ( empty( $term ) ) {
			wp_die();
		}

		if ( ! empty( $_GET['limit'] ) ) {
			$limit = absint( $_GET['limit'] );
		} else {
			$limit = absint( apply_filters( 'woocommerce_json_search_limit', 30 ) );
		}

		$include_ids = ! empty( $_GET['include'] ) ? array_map( 'absint', (array) wp_unslash( $_GET['include'] ) ) : array();
		$exclude_ids = ! empty( $_GET['exclude'] ) ? array_map( 'absint', (array) wp_unslash( $_GET['exclude'] ) ) : array();

		$exclude_types = array();
		if ( ! empty( $_GET['exclude_type'] ) ) {
			// Support both comma-delimited and array format inputs.
			$exclude_types = wp_unslash( $_GET['exclude_type'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			if ( ! is_array( $exclude_types ) ) {
				$exclude_types = explode( ',', $exclude_types );
			}

			// Sanitize the excluded types against valid product types.
			foreach ( $exclude_types as &$exclude_type ) {
				$exclude_type = strtolower( trim( $exclude_type ) );
			}
			$exclude_types = array_intersect(
				array_merge( array( 'variation' ), array_keys( wc_get_product_types() ) ),
				$exclude_types
			);
		}

		$data_store = WC_Data_Store::load( 'product' );
		$ids        = $data_store->search_products( $term, '', (bool) $include_variations, false, $limit, $include_ids, $exclude_ids );

		$products = array();

		foreach ( $ids as $id ) {
			$product_object = wc_get_product( $id );

			if ( ! wc_products_array_filter_readable( $product_object ) ) {
				continue;
			}

			$formatted_name = $product_object->get_formatted_name();
			$managing_stock = $product_object->managing_stock();

			if ( in_array( $product_object->get_type(), $exclude_types, true ) ) {
				continue;
			}

			if ( $managing_stock && ! empty( $_GET['display_stock'] ) ) {
				$stock_amount = $product_object->get_stock_quantity();
				/* Translators: %d stock amount */
				$formatted_name .= ' &ndash; ' . sprintf( __( 'Stock: %d', 'woocommerce' ), wc_format_stock_quantity_for_display( $stock_amount, $product_object ) );
			}

			$products[ $product_object->get_id() ] = rawurldecode( wp_strip_all_tags( $formatted_name ) );
		}

		wp_send_json( apply_filters( 'woocommerce_json_search_found_products', $products ) );
	}

	/**
	 * Search for product variations and return json.
	 *
	 * @see WC_AJAX::json_search_products()
	 */
	public static function json_search_products_and_variations() {
		self::json_search_products( '', true );
	}

	/**
	 * Search for downloadable product variations and return json.
	 *
	 * @see WC_AJAX::json_search_products()
	 */
	public static function json_search_downloadable_products_and_variations() {
		check_ajax_referer( 'search-products', 'security' );

		if ( ! empty( $_GET['limit'] ) ) {
			$limit = absint( $_GET['limit'] );
		} else {
			$limit = absint( apply_filters( 'woocommerce_json_search_limit', 30 ) );
		}

		$include_ids = ! empty( $_GET['include'] ) ? array_map( 'absint', (array) wp_unslash( $_GET['include'] ) ) : array();
		$exclude_ids = ! empty( $_GET['exclude'] ) ? array_map( 'absint', (array) wp_unslash( $_GET['exclude'] ) ) : array();

		$term       = isset( $_GET['term'] ) ? (string) wc_clean( wp_unslash( $_GET['term'] ) ) : '';
		$data_store = WC_Data_Store::load( 'product' );
		$ids        = $data_store->search_products( $term, 'downloadable', true, false, $limit );

		$product_objects = array_filter( array_map( 'wc_get_product', $ids ), 'wc_products_array_filter_readable' );
		$products        = array();

		foreach ( $product_objects as $product_object ) {
			$products[ $product_object->get_id() ] = rawurldecode( wp_strip_all_tags( $product_object->get_formatted_name() ) );
		}

		wp_send_json( $products );
	}

	/**
	 * Search for customers and return json.
	 */
	public static function json_search_customers() {
		ob_start();

		check_ajax_referer( 'search-customers', 'security' );

		if ( ! current_user_can( 'edit_shop_orders' ) ) {
			wp_die( -1 );
		}

		$term  = isset( $_GET['term'] ) ? (string) wc_clean( wp_unslash( $_GET['term'] ) ) : '';
		$limit = 0;

		if ( empty( $term ) ) {
			wp_die();
		}

		$ids = array();
		// Search by ID.
		if ( is_numeric( $term ) ) {
			$customer = new WC_Customer( intval( $term ) );

			// Customer does not exists.
			if ( 0 !== $customer->get_id() ) {
				$ids = array( $customer->get_id() );
			}
		}

		// Usernames can be numeric so we first check that no users was found by ID before searching for numeric username, this prevents performance issues with ID lookups.
		if ( empty( $ids ) ) {
			$data_store = WC_Data_Store::load( 'customer' );

			// If search is smaller than 3 characters, limit result set to avoid
			// too many rows being returned.
			if ( 3 > strlen( $term ) ) {
				$limit = 20;
			}
			$ids = $data_store->search_customers( $term, $limit );
		}

		$found_customers = array();

		if ( ! empty( $_GET['exclude'] ) ) {
			$ids = array_diff( $ids, array_map( 'absint', (array) wp_unslash( $_GET['exclude'] ) ) );
		}

		foreach ( $ids as $id ) {
			$customer = new WC_Customer( $id );
			/* translators: 1: user display name 2: user ID 3: user email */
			$found_customers[ $id ] = sprintf(
				/* translators: $1: customer name, $2 customer id, $3: customer email */
				esc_html__( '%1$s (#%2$s &ndash; %3$s)', 'woocommerce' ),
				$customer->get_first_name() . ' ' . $customer->get_last_name(),
				$customer->get_id(),
				$customer->get_email()
			);
		}

		wp_send_json( apply_filters( 'woocommerce_json_search_found_customers', $found_customers ) );
	}

	/**
	 * Search for categories and return json.
	 */
	public static function json_search_categories() {
		ob_start();

		check_ajax_referer( 'search-categories', 'security' );

		if ( ! current_user_can( 'edit_products' ) ) {
			wp_die( -1 );
		}

		$search_text = isset( $_GET['term'] ) ? wc_clean( wp_unslash( $_GET['term'] ) ) : '';

		if ( ! $search_text ) {
			wp_die();
		}

		$found_categories = array();
		$args             = array(
			'taxonomy'   => array( 'product_cat' ),
			'orderby'    => 'id',
			'order'      => 'ASC',
			'hide_empty' => true,
			'fields'     => 'all',
			'name__like' => $search_text,
		);

		$terms = get_terms( $args );

		if ( $terms ) {
			foreach ( $terms as $term ) {
				$term->formatted_name = '';

				if ( $term->parent ) {
					$ancestors = array_reverse( get_ancestors( $term->term_id, 'product_cat' ) );
					foreach ( $ancestors as $ancestor ) {
						$ancestor_term = get_term( $ancestor, 'product_cat' );
						if ( $ancestor_term ) {
							$term->formatted_name .= $ancestor_term->name . ' > ';
						}
					}
				}

				$term->formatted_name              .= $term->name . ' (' . $term->count . ')';
				$found_categories[ $term->term_id ] = $term;
			}
		}

		wp_send_json( apply_filters( 'woocommerce_json_search_found_categories', $found_categories ) );
	}

	/**
	 * Ajax request handling for page searching.
	 */
	public static function json_search_pages() {
		ob_start();

		check_ajax_referer( 'search-pages', 'security' );

		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			wp_die( -1 );
		}

		$search_text = isset( $_GET['term'] ) ? wc_clean( wp_unslash( $_GET['term'] ) ) : '';
		$limit       = isset( $_GET['limit'] ) ? absint( wp_unslash( $_GET['limit'] ) ) : -1;
		$exclude_ids = ! empty( $_GET['exclude'] ) ? array_map( 'absint', (array) wp_unslash( $_GET['exclude'] ) ) : array();

		$args                 = array(
			'no_found_rows'          => true,
			'update_post_meta_cache' => false,
			'update_post_term_cache' => false,
			'posts_per_page'         => $limit,
			'post_type'              => 'page',
			'post_status'            => array( 'publish', 'private', 'draft' ),
			's'                      => $search_text,
			'post__not_in'           => $exclude_ids,
		);
		$search_results_query = new WP_Query( $args );

		$pages_results = array();
		foreach ( $search_results_query->get_posts() as $post ) {
			$pages_results[ $post->ID ] = sprintf(
				/* translators: 1: page name 2: page ID */
				__( '%1$s (ID: %2$s)', 'woocommerce' ),
				get_the_title( $post ),
				$post->ID
			);
		}

		wp_send_json( apply_filters( 'woocommerce_json_search_found_pages', $pages_results ) );
	}

	/**
	 * Ajax request handling for categories ordering.
	 */
	public static function term_ordering() {
		// phpcs:disable WordPress.Security.NonceVerification.Missing
		if ( ! current_user_can( 'edit_products' ) || empty( $_POST['id'] ) ) {
			wp_die( -1 );
		}

		$id       = (int) $_POST['id'];
		$next_id  = isset( $_POST['nextid'] ) && (int) $_POST['nextid'] ? (int) $_POST['nextid'] : null;
		$taxonomy = isset( $_POST['thetaxonomy'] ) ? esc_attr( wp_unslash( $_POST['thetaxonomy'] ) ) : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$term     = get_term_by( 'id', $id, $taxonomy );

		if ( ! $id || ! $term || ! $taxonomy ) {
			wp_die( 0 );
		}

		wc_reorder_terms( $term, $next_id, $taxonomy );

		$children = get_terms( $taxonomy, "child_of=$id&menu_order=ASC&hide_empty=0" );

		if ( $term && count( $children ) ) {
			echo 'children';
			wp_die();
		}
		// phpcs:enable
	}

	/**
	 * Ajax request handling for product ordering.
	 *
	 * Based on Simple Page Ordering by 10up (https://wordpress.org/plugins/simple-page-ordering/).
	 */
	public static function product_ordering() {
		global $wpdb;

		// phpcs:disable WordPress.Security.NonceVerification.Missing
		if ( ! current_user_can( 'edit_products' ) || empty( $_POST['id'] ) ) {
			wp_die( -1 );
		}

		$sorting_id  = absint( $_POST['id'] );
		$previd      = absint( isset( $_POST['previd'] ) ? $_POST['previd'] : 0 );
		$nextid      = absint( isset( $_POST['nextid'] ) ? $_POST['nextid'] : 0 );
		$menu_orders = wp_list_pluck( $wpdb->get_results( "SELECT ID, menu_order FROM {$wpdb->posts} WHERE post_type = 'product' ORDER BY menu_order ASC, post_title ASC" ), 'menu_order', 'ID' );
		$index       = 0;

		foreach ( $menu_orders as $id => $menu_order ) {
			$id = absint( $id );

			if ( $sorting_id === $id ) {
				continue;
			}
			if ( $nextid === $id ) {
				$index ++;
			}
			$index ++;
			$menu_orders[ $id ] = $index;
			$wpdb->update( $wpdb->posts, array( 'menu_order' => $index ), array( 'ID' => $id ) );

			/**
			 * When a single product has gotten it's ordering updated.
			 * $id The product ID
			 * $index The new menu order
			*/
			do_action( 'woocommerce_after_single_product_ordering', $id, $index );
		}

		if ( isset( $menu_orders[ $previd ] ) ) {
			$menu_orders[ $sorting_id ] = $menu_orders[ $previd ] + 1;
		} elseif ( isset( $menu_orders[ $nextid ] ) ) {
			$menu_orders[ $sorting_id ] = $menu_orders[ $nextid ] - 1;
		} else {
			$menu_orders[ $sorting_id ] = 0;
		}

		$wpdb->update( $wpdb->posts, array( 'menu_order' => $menu_orders[ $sorting_id ] ), array( 'ID' => $sorting_id ) );

		WC_Post_Data::delete_product_query_transients();

		do_action( 'woocommerce_after_product_ordering', $sorting_id, $menu_orders );
		wp_send_json( $menu_orders );
		// phpcs:enable
	}

	/**
	 * Handle a refund via the edit order screen.
	 *
	 * @throws Exception To return errors.
	 */
	public static function refund_line_items() {
		ob_start();

		check_ajax_referer( 'order-item', 'security' );

		if ( ! current_user_can( 'edit_shop_orders' ) ) {
			wp_die( -1 );
		}

		$order_id               = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0;
		$refund_amount          = isset( $_POST['refund_amount'] ) ? wc_format_decimal( sanitize_text_field( wp_unslash( $_POST['refund_amount'] ) ), wc_get_price_decimals() ) : 0;
		$refunded_amount        = isset( $_POST['refunded_amount'] ) ? wc_format_decimal( sanitize_text_field( wp_unslash( $_POST['refunded_amount'] ) ), wc_get_price_decimals() ) : 0;
		$refund_reason          = isset( $_POST['refund_reason'] ) ? sanitize_text_field( wp_unslash( $_POST['refund_reason'] ) ) : '';
		$line_item_qtys         = isset( $_POST['line_item_qtys'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['line_item_qtys'] ) ), true ) : array();
		$line_item_totals       = isset( $_POST['line_item_totals'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['line_item_totals'] ) ), true ) : array();
		$line_item_tax_totals   = isset( $_POST['line_item_tax_totals'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['line_item_tax_totals'] ) ), true ) : array();
		$api_refund             = isset( $_POST['api_refund'] ) && 'true' === $_POST['api_refund'];
		$restock_refunded_items = isset( $_POST['restock_refunded_items'] ) && 'true' === $_POST['restock_refunded_items'];
		$refund                 = false;
		$response               = array();

		try {
			$order      = wc_get_order( $order_id );
			$max_refund = wc_format_decimal( $order->get_total() - $order->get_total_refunded(), wc_get_price_decimals() );

			if ( ( ! $refund_amount && ( wc_format_decimal( 0, wc_get_price_decimals() ) !== $refund_amount ) ) || $max_refund < $refund_amount || 0 > $refund_amount ) {
				throw new Exception( __( 'Invalid refund amount', 'woocommerce' ) );
			}

			if ( wc_format_decimal( $order->get_total_refunded(), wc_get_price_decimals() ) !== $refunded_amount ) {
				throw new Exception( __( 'Error processing refund. Please try again.', 'woocommerce' ) );
			}

			// Prepare line items which we are refunding.
			$line_items = array();
			$item_ids   = array_unique( array_merge( array_keys( $line_item_qtys ), array_keys( $line_item_totals ) ) );

			foreach ( $item_ids as $item_id ) {
				$line_items[ $item_id ] = array(
					'qty'          => 0,
					'refund_total' => 0,
					'refund_tax'   => array(),
				);
			}
			foreach ( $line_item_qtys as $item_id => $qty ) {
				$line_items[ $item_id ]['qty'] = max( $qty, 0 );
			}
			foreach ( $line_item_totals as $item_id => $total ) {
				$line_items[ $item_id ]['refund_total'] = wc_format_decimal( $total );
			}
			foreach ( $line_item_tax_totals as $item_id => $tax_totals ) {
				$line_items[ $item_id ]['refund_tax'] = array_filter( array_map( 'wc_format_decimal', $tax_totals ) );
			}

			// Create the refund object.
			$refund = wc_create_refund(
				array(
					'amount'         => $refund_amount,
					'reason'         => $refund_reason,
					'order_id'       => $order_id,
					'line_items'     => $line_items,
					'refund_payment' => $api_refund,
					'restock_items'  => $restock_refunded_items,
				)
			);

			if ( is_wp_error( $refund ) ) {
				throw new Exception( $refund->get_error_message() );
			}

			if ( did_action( 'woocommerce_order_fully_refunded' ) ) {
				$response['status'] = 'fully_refunded';
			}
		} catch ( Exception $e ) {
			wp_send_json_error( array( 'error' => $e->getMessage() ) );
		}

		// wp_send_json_success must be outside the try block not to break phpunit tests.
		wp_send_json_success( $response );
	}

	/**
	 * Delete a refund.
	 */
	public static function delete_refund() {
		check_ajax_referer( 'order-item', 'security' );

		if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['refund_id'] ) ) {
			wp_die( -1 );
		}

		$refund_ids = array_map( 'absint', is_array( $_POST['refund_id'] ) ? wp_unslash( $_POST['refund_id'] ) : array( wp_unslash( $_POST['refund_id'] ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		foreach ( $refund_ids as $refund_id ) {
			if ( $refund_id && 'shop_order_refund' === get_post_type( $refund_id ) ) {
				$refund   = wc_get_order( $refund_id );
				$order_id = $refund->get_parent_id();
				$refund->delete( true );
				do_action( 'woocommerce_refund_deleted', $refund_id, $order_id );
			}
		}
		wp_die();
	}

	/**
	 * Triggered when clicking the rating footer.
	 */
	public static function rated() {
		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			wp_die( -1 );
		}
		update_option( 'woocommerce_admin_footer_text_rated', 1 );
		wp_die();
	}

	/**
	 * Create/Update API key.
	 *
	 * @throws Exception On invalid or empty description, user, or permissions.
	 */
	public static function update_api_key() {
		ob_start();

		global $wpdb;

		check_ajax_referer( 'update-api-key', 'security' );

		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			wp_die( -1 );
		}

		$response = array();

		try {
			if ( empty( $_POST['description'] ) ) {
				throw new Exception( __( 'Description is missing.', 'woocommerce' ) );
			}
			if ( empty( $_POST['user'] ) ) {
				throw new Exception( __( 'User is missing.', 'woocommerce' ) );
			}
			if ( empty( $_POST['permissions'] ) ) {
				throw new Exception( __( 'Permissions is missing.', 'woocommerce' ) );
			}

			$key_id      = isset( $_POST['key_id'] ) ? absint( $_POST['key_id'] ) : 0;
			$description = sanitize_text_field( wp_unslash( $_POST['description'] ) );
			$permissions = ( in_array( wp_unslash( $_POST['permissions'] ), array( 'read', 'write', 'read_write' ), true ) ) ? sanitize_text_field( wp_unslash( $_POST['permissions'] ) ) : 'read';
			$user_id     = absint( $_POST['user'] );

			// Check if current user can edit other users.
			if ( $user_id && ! current_user_can( 'edit_user', $user_id ) ) {
				if ( get_current_user_id() !== $user_id ) {
					throw new Exception( __( 'You do not have permission to assign API Keys to the selected user.', 'woocommerce' ) );
				}
			}

			if ( 0 < $key_id ) {
				$data = array(
					'user_id'     => $user_id,
					'description' => $description,
					'permissions' => $permissions,
				);

				$wpdb->update(
					$wpdb->prefix . 'woocommerce_api_keys',
					$data,
					array( 'key_id' => $key_id ),
					array(
						'%d',
						'%s',
						'%s',
					),
					array( '%d' )
				);

				$response                    = $data;
				$response['consumer_key']    = '';
				$response['consumer_secret'] = '';
				$response['message']         = __( 'API Key updated successfully.', 'woocommerce' );
			} else {
				$consumer_key    = 'ck_' . wc_rand_hash();
				$consumer_secret = 'cs_' . wc_rand_hash();

				$data = array(
					'user_id'         => $user_id,
					'description'     => $description,
					'permissions'     => $permissions,
					'consumer_key'    => wc_api_hash( $consumer_key ),
					'consumer_secret' => $consumer_secret,
					'truncated_key'   => substr( $consumer_key, -7 ),
				);

				$wpdb->insert(
					$wpdb->prefix . 'woocommerce_api_keys',
					$data,
					array(
						'%d',
						'%s',
						'%s',
						'%s',
						'%s',
						'%s',
					)
				);

				$key_id                      = $wpdb->insert_id;
				$response                    = $data;
				$response['consumer_key']    = $consumer_key;
				$response['consumer_secret'] = $consumer_secret;
				$response['message']         = __( 'API Key generated successfully. Make sure to copy your new keys now as the secret key will be hidden once you leave this page.', 'woocommerce' );
				$response['revoke_url']      = '<a style="color: #a00; text-decoration: none;" href="' . esc_url( wp_nonce_url( add_query_arg( array( 'revoke-key' => $key_id ), admin_url( 'admin.php?page=wc-settings&tab=advanced&section=keys' ) ), 'revoke' ) ) . '">' . __( 'Revoke key', 'woocommerce' ) . '</a>';
			}
		} catch ( Exception $e ) {
			wp_send_json_error( array( 'message' => $e->getMessage() ) );
		}

		// wp_send_json_success must be outside the try block not to break phpunit tests.
		wp_send_json_success( $response );
	}

	/**
	 * Load variations via AJAX.
	 */
	public static function load_variations() {
		ob_start();

		check_ajax_referer( 'load-variations', 'security' );

		if ( ! current_user_can( 'edit_products' ) || empty( $_POST['product_id'] ) ) {
			wp_die( -1 );
		}

		// Set $post global so its available, like within the admin screens.
		global $post;

		$loop           = 0;
		$product_id     = absint( $_POST['product_id'] );
		$post           = get_post( $product_id ); // phpcs:ignore
		$product_object = wc_get_product( $product_id );
		$per_page       = ! empty( $_POST['per_page'] ) ? absint( $_POST['per_page'] ) : 10;
		$page           = ! empty( $_POST['page'] ) ? absint( $_POST['page'] ) : 1;
		$variations     = wc_get_products(
			array(
				'status'  => array( 'private', 'publish' ),
				'type'    => 'variation',
				'parent'  => $product_id,
				'limit'   => $per_page,
				'page'    => $page,
				'orderby' => array(
					'menu_order' => 'ASC',
					'ID'         => 'DESC',
				),
				'return'  => 'objects',
			)
		);

		if ( $variations ) {
			wc_render_invalid_variation_notice( $product_object );

			foreach ( $variations as $variation_object ) {
				$variation_id   = $variation_object->get_id();
				$variation      = get_post( $variation_id );
				$variation_data = array_merge( get_post_custom( $variation_id ), wc_get_product_variation_attributes( $variation_id ) ); // kept for BW compatibility.
				include __DIR__ . '/admin/meta-boxes/views/html-variation-admin.php';
				$loop++;
			}
		}
		wp_die();
	}

	/**
	 * Save variations via AJAX.
	 */
	public static function save_variations() {
		ob_start();

		check_ajax_referer( 'save-variations', 'security' );

		// Check permissions again and make sure we have what we need.
		if ( ! current_user_can( 'edit_products' ) || empty( $_POST ) || empty( $_POST['product_id'] ) ) {
			wp_die( -1 );
		}

		$product_id                           = absint( $_POST['product_id'] );
		WC_Admin_Meta_Boxes::$meta_box_errors = array();
		WC_Meta_Box_Product_Data::save_variations( $product_id, get_post( $product_id ) );

		do_action( 'woocommerce_ajax_save_product_variations', $product_id );

		$errors = WC_Admin_Meta_Boxes::$meta_box_errors;

		if ( $errors ) {
			echo '<div class="error notice is-dismissible">';

			foreach ( $errors as $error ) {
				echo '<p>' . wp_kses_post( $error ) . '</p>';
			}

			echo '<button type="button" class="notice-dismiss"><span class="screen-reader-text">' . esc_html__( 'Dismiss this notice.', 'woocommerce' ) . '</span></button>';
			echo '</div>';

			delete_option( 'woocommerce_meta_box_errors' );
		}

		wp_die();
	}

	/**
	 * Bulk action - Toggle Enabled.
	 *
	 * @param array $variations List of variations.
	 * @param array $data Data to set.
	 *
	 * @used-by bulk_edit_variations
	 */
	private static function variation_bulk_action_toggle_enabled( $variations, $data ) {
		foreach ( $variations as $variation_id ) {
			$variation = wc_get_product( $variation_id );
			$variation->set_status( 'private' === $variation->get_status( 'edit' ) ? 'publish' : 'private' );
			$variation->save();
		}
	}

	/**
	 * Bulk action - Toggle Downloadable Checkbox.
	 *
	 * @param array $variations List of variations.
	 * @param array $data Data to set.
	 *
	 * @used-by bulk_edit_variations
	 */
	private static function variation_bulk_action_toggle_downloadable( $variations, $data ) {
		self::variation_bulk_toggle( $variations, 'downloadable' );
	}

	/**
	 * Bulk action - Toggle Virtual Checkbox.
	 *
	 * @param array $variations List of variations.
	 * @param array $data Data to set.
	 *
	 * @used-by bulk_edit_variations
	 */
	private static function variation_bulk_action_toggle_virtual( $variations, $data ) {
		self::variation_bulk_toggle( $variations, 'virtual' );
	}

	/**
	 * Bulk action - Toggle Manage Stock Checkbox.
	 *
	 * @param array $variations List of variations.
	 * @param array $data Data to set.
	 *
	 * @used-by bulk_edit_variations
	 */
	private static function variation_bulk_action_toggle_manage_stock( $variations, $data ) {
		self::variation_bulk_toggle( $variations, 'manage_stock' );
	}

	/**
	 * Bulk action - Set Regular Prices.
	 *
	 * @param array $variations List of variations.
	 * @param array $data Data to set.
	 *
	 * @used-by bulk_edit_variations
	 */
	private static function variation_bulk_action_variable_regular_price( $variations, $data ) {
		self::variation_bulk_set( $variations, 'regular_price', $data['value'] );
	}

	/**
	 * Bulk action - Set Sale Prices.
	 *
	 * @param array $variations List of variations.
	 * @param array $data Data to set.
	 *
	 * @used-by bulk_edit_variations
	 */
	private static function variation_bulk_action_variable_sale_price( $variations, $data ) {
		self::variation_bulk_set( $variations, 'sale_price', $data['value'] );
	}

	/**
	 * Bulk action - Set Stock Status as In Stock.
	 *
	 * @param array $variations List of variations.
	 * @param array $data Data to set.
	 *
	 * @used-by bulk_edit_variations
	 */
	private static function variation_bulk_action_variable_stock_status_instock( $variations, $data ) {
		self::variation_bulk_set( $variations, 'stock_status', 'instock' );
	}

	/**
	 * Bulk action - Set Stock Status as Out of Stock.
	 *
	 * @param array $variations List of variations.
	 * @param array $data Data to set.
	 *
	 * @used-by bulk_edit_variations
	 */
	private static function variation_bulk_action_variable_stock_status_outofstock( $variations, $data ) {
		self::variation_bulk_set( $variations, 'stock_status', 'outofstock' );
	}

	/**
	 * Bulk action - Set Stock Status as On Backorder.
	 *
	 * @param array $variations List of variations.
	 * @param array $data Data to set.
	 *
	 * @used-by bulk_edit_variations
	 */
	private static function variation_bulk_action_variable_stock_status_onbackorder( $variations, $data ) {
		self::variation_bulk_set( $variations, 'stock_status', 'onbackorder' );
	}

	/**
	 * Bulk action - Set Stock.
	 *
	 * @param array $variations List of variations.
	 * @param array $data Data to set.
	 *
	 * @used-by bulk_edit_variations
	 */
	private static function variation_bulk_action_variable_stock( $variations, $data ) {
		if ( ! isset( $data['value'] ) ) {
			return;
		}

		$quantity = wc_stock_amount( wc_clean( $data['value'] ) );

		foreach ( $variations as $variation_id ) {
			$variation = wc_get_product( $variation_id );
			if ( $variation->managing_stock() ) {
				$variation->set_stock_quantity( $quantity );
			} else {
				$variation->set_stock_quantity( null );
			}
			$variation->save();
		}
	}

	/**
	 * Bulk action - Set Low Stock Amount.
	 *
	 * @param array $variations List of variations.
	 * @param array $data Data to set.
	 *
	 * @used-by bulk_edit_variations
	 */
	private static function variation_bulk_action_variable_low_stock_amount( $variations, $data ) {
		if ( ! isset( $data['value'] ) ) {
			return;
		}

		$low_stock_amount = wc_stock_amount( wc_clean( $data['value'] ) );

		foreach ( $variations as $variation_id ) {
			$variation = wc_get_product( $variation_id );
			if ( $variation->managing_stock() ) {
				$variation->set_low_stock_amount( $low_stock_amount );
			} else {
				$variation->set_low_stock_amount( '' );
			}
			$variation->save();
		}
	}

	/**
	 * Bulk action - Set Weight.
	 *
	 * @param array $variations List of variations.
	 * @param array $data Data to set.
	 *
	 * @used-by bulk_edit_variations
	 */
	private static function variation_bulk_action_variable_weight( $variations, $data ) {
		self::variation_bulk_set( $variations, 'weight', $data['value'] );
	}

	/**
	 * Bulk action - Set Length.
	 *
	 * @param array $variations List of variations.
	 * @param array $data Data to set.
	 *
	 * @used-by bulk_edit_variations
	 */
	private static function variation_bulk_action_variable_length( $variations, $data ) {
		self::variation_bulk_set( $variations, 'length', $data['value'] );
	}

	/**
	 * Bulk action - Set Width.
	 *
	 * @param array $variations List of variations.
	 * @param array $data Data to set.
	 *
	 * @used-by bulk_edit_variations
	 */
	private static function variation_bulk_action_variable_width( $variations, $data ) {
		self::variation_bulk_set( $variations, 'width', $data['value'] );
	}

	/**
	 * Bulk action - Set Height.
	 *
	 * @param array $variations List of variations.
	 * @param array $data Data to set.
	 *
	 * @used-by bulk_edit_variations
	 */
	private static function variation_bulk_action_variable_height( $variations, $data ) {
		self::variation_bulk_set( $variations, 'height', $data['value'] );
	}

	/**
	 * Bulk action - Set Download Limit.
	 *
	 * @param array $variations List of variations.
	 * @param array $data Data to set.
	 *
	 * @used-by bulk_edit_variations
	 */
	private static function variation_bulk_action_variable_download_limit( $variations, $data ) {
		self::variation_bulk_set( $variations, 'download_limit', $data['value'] );
	}

	/**
	 * Bulk action - Set Download Expiry.
	 *
	 * @param array $variations List of variations.
	 * @param array $data Data to set.
	 *
	 * @used-by bulk_edit_variations
	 */
	private static function variation_bulk_action_variable_download_expiry( $variations, $data ) {
		self::variation_bulk_set( $variations, 'download_expiry', $data['value'] );
	}

	/**
	 * Bulk action - Delete all.
	 *
	 * @param array $variations List of variations.
	 * @param array $data Data to set.
	 *
	 * @used-by bulk_edit_variations
	 */
	private static function variation_bulk_action_delete_all( $variations, $data ) {
		if ( isset( $data['allowed'] ) && 'true' === $data['allowed'] ) {
			foreach ( $variations as $variation_id ) {
				$variation = wc_get_product( $variation_id );
				$variation->delete( true );
			}
		}
	}

	/**
	 * Bulk action - Sale Schedule.
	 *
	 * @param array $variations List of variations.
	 * @param array $data Data to set.
	 *
	 * @used-by bulk_edit_variations
	 */
	private static function variation_bulk_action_variable_sale_schedule( $variations, $data ) {
		if ( ! isset( $data['date_from'] ) && ! isset( $data['date_to'] ) ) {
			return;
		}

		foreach ( $variations as $variation_id ) {
			$variation = wc_get_product( $variation_id );

			if ( 'false' !== $data['date_from'] ) {
				$variation->set_date_on_sale_from( wc_clean( $data['date_from'] ) );
			}

			if ( 'false' !== $data['date_to'] ) {
				$variation->set_date_on_sale_to( wc_clean( $data['date_to'] ) );
			}

			$variation->save();
		}
	}

	/**
	 * Bulk action - Increase Regular Prices.
	 *
	 * @param array $variations List of variations.
	 * @param array $data Data to set.
	 *
	 * @used-by bulk_edit_variations
	 */
	private static function variation_bulk_action_variable_regular_price_increase( $variations, $data ) {
		self::variation_bulk_adjust_price( $variations, 'regular_price', '+', wc_clean( $data['value'] ) );
	}

	/**
	 * Bulk action - Decrease Regular Prices.
	 *
	 * @param array $variations List of variations.
	 * @param array $data Data to set.
	 *
	 * @used-by bulk_edit_variations
	 */
	private static function variation_bulk_action_variable_regular_price_decrease( $variations, $data ) {
		self::variation_bulk_adjust_price( $variations, 'regular_price', '-', wc_clean( $data['value'] ) );
	}

	/**
	 * Bulk action - Increase Sale Prices.
	 *
	 * @param array $variations List of variations.
	 * @param array $data Data to set.
	 *
	 * @used-by bulk_edit_variations
	 */
	private static function variation_bulk_action_variable_sale_price_increase( $variations, $data ) {
		self::variation_bulk_adjust_price( $variations, 'sale_price', '+', wc_clean( $data['value'] ) );
	}

	/**
	 * Bulk action - Decrease Sale Prices.
	 *
	 * @param array $variations List of variations.
	 * @param array $data Data to set.
	 *
	 * @used-by bulk_edit_variations
	 */
	private static function variation_bulk_action_variable_sale_price_decrease( $variations, $data ) {
		self::variation_bulk_adjust_price( $variations, 'sale_price', '-', wc_clean( $data['value'] ) );
	}

	/**
	 * Bulk action - Set Price.
	 *
	 * @param array  $variations List of variations.
	 * @param string $field price being adjusted _regular_price or _sale_price.
	 * @param string $operator + or -.
	 * @param string $value Price or Percent.
	 *
	 * @used-by bulk_edit_variations
	 */
	private static function variation_bulk_adjust_price( $variations, $field, $operator, $value ) {
		foreach ( $variations as $variation_id ) {
			$variation   = wc_get_product( $variation_id );
			$field_value = $variation->{"get_$field"}( 'edit' );

			if ( '%' === substr( $value, -1 ) ) {
				$percent      = wc_format_decimal( substr( $value, 0, -1 ) );
				$field_value += NumberUtil::round( ( $field_value / 100 ) * $percent, wc_get_price_decimals() ) * "{$operator}1";
			} else {
				$field_value += $value * "{$operator}1";
			}

			$variation->{"set_$field"}( $field_value );
			$variation->save();
		}
	}

	/**
	 * Bulk set convenience function.
	 *
	 * @param array  $variations List of variations.
	 * @param string $field Field to set.
	 * @param string $value to set.
	 */
	private static function variation_bulk_set( $variations, $field, $value ) {
		foreach ( $variations as $variation_id ) {
			$variation = wc_get_product( $variation_id );
			$variation->{ "set_$field" }( wc_clean( $value ) );
			$variation->save();
		}
	}

	/**
	 * Bulk toggle convenience function.
	 *
	 * @param array  $variations List of variations.
	 * @param string $field Field to toggle.
	 */
	private static function variation_bulk_toggle( $variations, $field ) {
		foreach ( $variations as $variation_id ) {
			$variation  = wc_get_product( $variation_id );
			$prev_value = $variation->{ "get_$field" }( 'edit' );
			$variation->{ "set_$field" }( ! $prev_value );
			$variation->save();
		}
	}

	/**
	 * Bulk edit variations via AJAX.
	 *
	 * @uses WC_AJAX::variation_bulk_set()
	 * @uses WC_AJAX::variation_bulk_adjust_price()
	 * @uses WC_AJAX::variation_bulk_action_variable_sale_price_decrease()
	 * @uses WC_AJAX::variation_bulk_action_variable_sale_price_increase()
	 * @uses WC_AJAX::variation_bulk_action_variable_regular_price_decrease()
	 * @uses WC_AJAX::variation_bulk_action_variable_regular_price_increase()
	 * @uses WC_AJAX::variation_bulk_action_variable_sale_schedule()
	 * @uses WC_AJAX::variation_bulk_action_delete_all()
	 * @uses WC_AJAX::variation_bulk_action_variable_download_expiry()
	 * @uses WC_AJAX::variation_bulk_action_variable_download_limit()
	 * @uses WC_AJAX::variation_bulk_action_variable_height()
	 * @uses WC_AJAX::variation_bulk_action_variable_width()
	 * @uses WC_AJAX::variation_bulk_action_variable_length()
	 * @uses WC_AJAX::variation_bulk_action_variable_weight()
	 * @uses WC_AJAX::variation_bulk_action_variable_stock()
	 * @uses WC_AJAX::variation_bulk_action_variable_sale_price()
	 * @uses WC_AJAX::variation_bulk_action_variable_regular_price()
	 * @uses WC_AJAX::variation_bulk_action_toggle_manage_stock()
	 * @uses WC_AJAX::variation_bulk_action_toggle_virtual()
	 * @uses WC_AJAX::variation_bulk_action_toggle_downloadable()
	 * @uses WC_AJAX::variation_bulk_action_toggle_enabled
	 * @uses WC_AJAX::variation_bulk_action_variable_low_stock_amount()
	 */
	public static function bulk_edit_variations() {
		ob_start();

		check_ajax_referer( 'bulk-edit-variations', 'security' );

		// Check permissions again and make sure we have what we need.
		if ( ! current_user_can( 'edit_products' ) || empty( $_POST['product_id'] ) || empty( $_POST['bulk_action'] ) ) {
			wp_die( -1 );
		}

		$product_id  = absint( $_POST['product_id'] );
		$bulk_action = wc_clean( wp_unslash( $_POST['bulk_action'] ) );
		$data        = ! empty( $_POST['data'] ) ? wc_clean( wp_unslash( $_POST['data'] ) ) : array();
		$variations  = array();

		if ( apply_filters( 'woocommerce_bulk_edit_variations_need_children', true ) ) {
			$variations = get_posts(
				array(
					'post_parent'    => $product_id,
					'posts_per_page' => -1,
					'post_type'      => 'product_variation',
					'fields'         => 'ids',
					'post_status'    => array( 'publish', 'private' ),
				)
			);
		}

		if ( method_exists( __CLASS__, "variation_bulk_action_$bulk_action" ) ) {
			call_user_func( array( __CLASS__, "variation_bulk_action_$bulk_action" ), $variations, $data );
		} else {
			do_action( 'woocommerce_bulk_edit_variations_default', $bulk_action, $data, $product_id, $variations );
		}

		do_action( 'woocommerce_bulk_edit_variations', $bulk_action, $data, $product_id, $variations );
		WC_Product_Variable::sync( $product_id );
		wc_delete_product_transients( $product_id );
		wp_die();
	}

	/**
	 * Handle submissions from assets/js/settings-views-html-settings-tax.js Backbone model.
	 */
	public static function tax_rates_save_changes() {
		// phpcs:disable WordPress.Security.NonceVerification.Missing
		if ( ! isset( $_POST['wc_tax_nonce'], $_POST['changes'] ) ) {
			wp_send_json_error( 'missing_fields' );
			wp_die();
		}

		$current_class = ! empty( $_POST['current_class'] ) ? wp_unslash( $_POST['current_class'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

		if ( ! wp_verify_nonce( wp_unslash( $_POST['wc_tax_nonce'] ), 'wc_tax_nonce-class:' . $current_class ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			wp_send_json_error( 'bad_nonce' );
			wp_die();
		}

		$current_class = WC_Tax::format_tax_rate_class( $current_class );

		// Check User Caps.
		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			wp_send_json_error( 'missing_capabilities' );
			wp_die();
		}

		$changes = wp_unslash( $_POST['changes'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		foreach ( $changes as $tax_rate_id => $data ) {
			if ( isset( $data['deleted'] ) ) {
				if ( isset( $data['newRow'] ) ) {
					// So the user added and deleted a new row.
					// That's fine, it's not in the database anyways. NEXT!
					continue;
				}
				WC_Tax::_delete_tax_rate( $tax_rate_id );
			}

			$tax_rate = array_intersect_key(
				$data,
				array(
					'tax_rate_country'  => 1,
					'tax_rate_state'    => 1,
					'tax_rate'          => 1,
					'tax_rate_name'     => 1,
					'tax_rate_priority' => 1,
					'tax_rate_compound' => 1,
					'tax_rate_shipping' => 1,
					'tax_rate_order'    => 1,
				)
			);

			if ( isset( $tax_rate['tax_rate'] ) ) {
				$tax_rate['tax_rate'] = wc_format_decimal( $tax_rate['tax_rate'] );
			}

			if ( isset( $data['newRow'] ) ) {
				$tax_rate['tax_rate_class'] = $current_class;
				$tax_rate_id                = WC_Tax::_insert_tax_rate( $tax_rate );
			} elseif ( ! empty( $tax_rate ) ) {
				WC_Tax::_update_tax_rate( $tax_rate_id, $tax_rate );
			}

			if ( isset( $data['postcode'] ) ) {
				$postcode = array_map( 'wc_clean', $data['postcode'] );
				$postcode = array_map( 'wc_normalize_postcode', $postcode );
				WC_Tax::_update_tax_rate_postcodes( $tax_rate_id, $postcode );
			}
			if ( isset( $data['city'] ) ) {
				WC_Tax::_update_tax_rate_cities( $tax_rate_id, array_map( 'wc_clean', array_map( 'wp_unslash', $data['city'] ) ) );
			}
		}

		WC_Cache_Helper::invalidate_cache_group( 'taxes' );
		WC_Cache_Helper::get_transient_version( 'shipping', true );

		wp_send_json_success(
			array(
				'rates' => WC_Tax::get_rates_for_tax_class( $current_class ),
			)
		);
		// phpcs:enable
	}

	/**
	 * Handle submissions from assets/js/wc-shipping-zones.js Backbone model.
	 */
	public static function shipping_zones_save_changes() {
		if ( ! isset( $_POST['wc_shipping_zones_nonce'], $_POST['changes'] ) ) {
			wp_send_json_error( 'missing_fields' );
			wp_die();
		}

		if ( ! wp_verify_nonce( wp_unslash( $_POST['wc_shipping_zones_nonce'] ), 'wc_shipping_zones_nonce' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			wp_send_json_error( 'bad_nonce' );
			wp_die();
		}

		// Check User Caps.
		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			wp_send_json_error( 'missing_capabilities' );
			wp_die();
		}

		$changes = wp_unslash( $_POST['changes'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		foreach ( $changes as $zone_id => $data ) {
			if ( isset( $data['deleted'] ) ) {
				if ( isset( $data['newRow'] ) ) {
					// So the user added and deleted a new row.
					// That's fine, it's not in the database anyways. NEXT!
					continue;
				}
				WC_Shipping_Zones::delete_zone( $zone_id );
				continue;
			}

			$zone_data = array_intersect_key(
				$data,
				array(
					'zone_id'    => 1,
					'zone_order' => 1,
				)
			);

			if ( isset( $zone_data['zone_id'] ) ) {
				$zone = new WC_Shipping_Zone( $zone_data['zone_id'] );

				if ( isset( $zone_data['zone_order'] ) ) {
					$zone->set_zone_order( $zone_data['zone_order'] );
				}

				$zone->save();
			}
		}

		wp_send_json_success(
			array(
				'zones' => WC_Shipping_Zones::get_zones( 'json' ),
			)
		);
	}

	/**
	 * Handle submissions from assets/js/wc-shipping-zone-methods.js Backbone model.
	 */
	public static function shipping_zone_add_method() {
		if ( ! isset( $_POST['wc_shipping_zones_nonce'], $_POST['zone_id'], $_POST['method_id'] ) ) {
			wp_send_json_error( 'missing_fields' );
			wp_die();
		}

		if ( ! wp_verify_nonce( wp_unslash( $_POST['wc_shipping_zones_nonce'] ), 'wc_shipping_zones_nonce' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			wp_send_json_error( 'bad_nonce' );
			wp_die();
		}

		// Check User Caps.
		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			wp_send_json_error( 'missing_capabilities' );
			wp_die();
		}

		$zone_id     = wc_clean( wp_unslash( $_POST['zone_id'] ) );
		$zone        = new WC_Shipping_Zone( $zone_id );
		$instance_id = $zone->add_shipping_method( wc_clean( wp_unslash( $_POST['method_id'] ) ) );

		wp_send_json_success(
			array(
				'instance_id' => $instance_id,
				'zone_id'     => $zone->get_id(),
				'zone_name'   => $zone->get_zone_name(),
				'methods'     => $zone->get_shipping_methods( false, 'json' ),
			)
		);
	}

	/**
	 * Handle submissions from assets/js/wc-shipping-zone-methods.js Backbone model.
	 */
	public static function shipping_zone_methods_save_changes() {
		if ( ! isset( $_POST['wc_shipping_zones_nonce'], $_POST['zone_id'], $_POST['changes'] ) ) {
			wp_send_json_error( 'missing_fields' );
			wp_die();
		}

		if ( ! wp_verify_nonce( wp_unslash( $_POST['wc_shipping_zones_nonce'] ), 'wc_shipping_zones_nonce' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			wp_send_json_error( 'bad_nonce' );
			wp_die();
		}

		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			wp_send_json_error( 'missing_capabilities' );
			wp_die();
		}

		global $wpdb;

		$zone_id = wc_clean( wp_unslash( $_POST['zone_id'] ) );
		$zone    = new WC_Shipping_Zone( $zone_id );
		$changes = wp_unslash( $_POST['changes'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

		if ( isset( $changes['zone_name'] ) ) {
			$zone->set_zone_name( wc_clean( $changes['zone_name'] ) );
		}

		if ( isset( $changes['zone_locations'] ) ) {
			$zone->clear_locations( array( 'state', 'country', 'continent' ) );
			$locations = array_filter( array_map( 'wc_clean', (array) $changes['zone_locations'] ) );
			foreach ( $locations as $location ) {
				// Each posted location will be in the format type:code.
				$location_parts = explode( ':', $location );
				switch ( $location_parts[0] ) {
					case 'state':
						$zone->add_location( $location_parts[1] . ':' . $location_parts[2], 'state' );
						break;
					case 'country':
						$zone->add_location( $location_parts[1], 'country' );
						break;
					case 'continent':
						$zone->add_location( $location_parts[1], 'continent' );
						break;
				}
			}
		}

		if ( isset( $changes['zone_postcodes'] ) ) {
			$zone->clear_locations( 'postcode' );
			$postcodes = array_filter( array_map( 'strtoupper', array_map( 'wc_clean', explode( "\n", $changes['zone_postcodes'] ) ) ) );
			foreach ( $postcodes as $postcode ) {
				$zone->add_location( $postcode, 'postcode' );
			}
		}

		if ( isset( $changes['methods'] ) ) {
			foreach ( $changes['methods'] as $instance_id => $data ) {
				$method_id = $wpdb->get_var( $wpdb->prepare( "SELECT method_id FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE instance_id = %d", $instance_id ) );

				if ( isset( $data['deleted'] ) ) {
					$shipping_method = WC_Shipping_Zones::get_shipping_method( $instance_id );
					$option_key      = $shipping_method->get_instance_option_key();
					if ( $wpdb->delete( "{$wpdb->prefix}woocommerce_shipping_zone_methods", array( 'instance_id' => $instance_id ) ) ) {
						delete_option( $option_key );
						do_action( 'woocommerce_shipping_zone_method_deleted', $instance_id, $method_id, $zone_id );
					}
					continue;
				}

				$method_data = array_intersect_key(
					$data,
					array(
						'method_order' => 1,
						'enabled'      => 1,
					)
				);

				if ( isset( $method_data['method_order'] ) ) {
					$wpdb->update( "{$wpdb->prefix}woocommerce_shipping_zone_methods", array( 'method_order' => absint( $method_data['method_order'] ) ), array( 'instance_id' => absint( $instance_id ) ) );
				}

				if ( isset( $method_data['enabled'] ) ) {
					$is_enabled = absint( 'yes' === $method_data['enabled'] );
					if ( $wpdb->update( "{$wpdb->prefix}woocommerce_shipping_zone_methods", array( 'is_enabled' => $is_enabled ), array( 'instance_id' => absint( $instance_id ) ) ) ) {
						do_action( 'woocommerce_shipping_zone_method_status_toggled', $instance_id, $method_id, $zone_id, $is_enabled );
					}
				}
			}
		}

		$zone->save();

		wp_send_json_success(
			array(
				'zone_id'   => $zone->get_id(),
				'zone_name' => $zone->get_zone_name(),
				'methods'   => $zone->get_shipping_methods( false, 'json' ),
			)
		);
	}

	/**
	 * Save method settings
	 */
	public static function shipping_zone_methods_save_settings() {
		if ( ! isset( $_POST['wc_shipping_zones_nonce'], $_POST['instance_id'], $_POST['data'] ) ) {
			wp_send_json_error( 'missing_fields' );
			wp_die();
		}

		if ( ! wp_verify_nonce( wp_unslash( $_POST['wc_shipping_zones_nonce'] ), 'wc_shipping_zones_nonce' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			wp_send_json_error( 'bad_nonce' );
			wp_die();
		}

		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			wp_send_json_error( 'missing_capabilities' );
			wp_die();
		}

		$instance_id     = absint( $_POST['instance_id'] );
		$zone            = WC_Shipping_Zones::get_zone_by( 'instance_id', $instance_id );
		$shipping_method = WC_Shipping_Zones::get_shipping_method( $instance_id );
		$shipping_method->set_post_data( wp_unslash( $_POST['data'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$shipping_method->process_admin_options();

		WC_Cache_Helper::get_transient_version( 'shipping', true );

		wp_send_json_success(
			array(
				'zone_id'   => $zone->get_id(),
				'zone_name' => $zone->get_zone_name(),
				'methods'   => $zone->get_shipping_methods( false, 'json' ),
				'errors'    => $shipping_method->get_errors(),
			)
		);
	}

	/**
	 * Handle submissions from assets/js/wc-shipping-classes.js Backbone model.
	 */
	public static function shipping_classes_save_changes() {
		if ( ! isset( $_POST['wc_shipping_classes_nonce'], $_POST['changes'] ) ) {
			wp_send_json_error( 'missing_fields' );
			wp_die();
		}

		if ( ! wp_verify_nonce( wp_unslash( $_POST['wc_shipping_classes_nonce'] ), 'wc_shipping_classes_nonce' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			wp_send_json_error( 'bad_nonce' );
			wp_die();
		}

		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			wp_send_json_error( 'missing_capabilities' );
			wp_die();
		}

		$changes = wp_unslash( $_POST['changes'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

		foreach ( $changes as $term_id => $data ) {
			$term_id = absint( $term_id );

			if ( isset( $data['deleted'] ) ) {
				if ( isset( $data['newRow'] ) ) {
					// So the user added and deleted a new row.
					// That's fine, it's not in the database anyways. NEXT!
					continue;
				}
				wp_delete_term( $term_id, 'product_shipping_class' );
				continue;
			}

			$update_args = array();

			if ( isset( $data['name'] ) ) {
				$update_args['name'] = wc_clean( $data['name'] );
			}

			if ( isset( $data['slug'] ) ) {
				$update_args['slug'] = wc_clean( $data['slug'] );
			}

			if ( isset( $data['description'] ) ) {
				$update_args['description'] = wc_clean( $data['description'] );
			}

			if ( isset( $data['newRow'] ) ) {
				$update_args = array_filter( $update_args );
				if ( empty( $update_args['name'] ) ) {
					continue;
				}
				$inserted_term = wp_insert_term( $update_args['name'], 'product_shipping_class', $update_args );
				$term_id       = is_wp_error( $inserted_term ) ? 0 : $inserted_term['term_id'];
			} else {
				wp_update_term( $term_id, 'product_shipping_class', $update_args );
			}

			do_action( 'woocommerce_shipping_classes_save_class', $term_id, $data );
		}

		$wc_shipping = WC_Shipping::instance();

		wp_send_json_success(
			array(
				'shipping_classes' => $wc_shipping->get_shipping_classes(),
			)
		);
	}

	/**
	 * Toggle payment gateway on or off via AJAX.
	 *
	 * @since 3.4.0
	 */
	public static function toggle_gateway_enabled() {
		if ( current_user_can( 'manage_woocommerce' ) && check_ajax_referer( 'woocommerce-toggle-payment-gateway-enabled', 'security' ) && isset( $_POST['gateway_id'] ) ) {
			// Load gateways.
			$payment_gateways = WC()->payment_gateways->payment_gateways();

			// Get posted gateway.
			$gateway_id = wc_clean( wp_unslash( $_POST['gateway_id'] ) );

			foreach ( $payment_gateways as $gateway ) {
				if ( ! in_array( $gateway_id, array( $gateway->id, sanitize_title( get_class( $gateway ) ) ), true ) ) {
					continue;
				}
				$enabled = $gateway->get_option( 'enabled', 'no' );

				if ( ! wc_string_to_bool( $enabled ) ) {
					if ( $gateway->needs_setup() ) {
						wp_send_json_error( 'needs_setup' );
						wp_die();
					} else {
						$gateway->update_option( 'enabled', 'yes' );
					}
				} else {
					// Disable the gateway.
					$gateway->update_option( 'enabled', 'no' );
				}

				wp_send_json_success( ! wc_string_to_bool( $enabled ) );
				wp_die();
			}
		}

		wp_send_json_error( 'invalid_gateway_id' );
		wp_die();
	}
}

WC_AJAX::init();
class-wc-order-item-coupon.php000064400000010001151336044770012341 0ustar00<?php
/**
 * Order Line Item (coupon)
 *
 * @package WooCommerce\Classes
 * @version 3.0.0
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Order item coupon class.
 */
class WC_Order_Item_Coupon extends WC_Order_Item {

	/**
	 * Order Data array. This is the core order data exposed in APIs since 3.0.0.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	protected $extra_data = array(
		'code'         => '',
		'discount'     => 0,
		'discount_tax' => 0,
	);

	/*
	|--------------------------------------------------------------------------
	| Setters
	|--------------------------------------------------------------------------
	*/

	/**
	 * Set order item name.
	 *
	 * @param string $value Coupon code.
	 */
	public function set_name( $value ) {
		return $this->set_code( $value );
	}

	/**
	 * Set code.
	 *
	 * @param string $value Coupon code.
	 */
	public function set_code( $value ) {
		$this->set_prop( 'code', wc_format_coupon_code( $value ) );
	}

	/**
	 * Set discount amount.
	 *
	 * @param string $value Discount.
	 */
	public function set_discount( $value ) {
		$this->set_prop( 'discount', wc_format_decimal( $value ) );
	}

	/**
	 * Set discounted tax amount.
	 *
	 * @param string $value Discount tax.
	 */
	public function set_discount_tax( $value ) {
		$this->set_prop( 'discount_tax', wc_format_decimal( $value ) );
	}

	/*
	|--------------------------------------------------------------------------
	| Getters
	|--------------------------------------------------------------------------
	*/

	/**
	 * Get order item type.
	 *
	 * @return string
	 */
	public function get_type() {
		return 'coupon';
	}

	/**
	 * Get order item name.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_name( $context = 'view' ) {
		return $this->get_code( $context );
	}

	/**
	 * Get coupon code.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_code( $context = 'view' ) {
		return $this->get_prop( 'code', $context );
	}

	/**
	 * Get discount amount.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_discount( $context = 'view' ) {
		return $this->get_prop( 'discount', $context );
	}

	/**
	 * Get discounted tax amount.
	 *
	 * @param string $context What the value is for. Valid values are 'view' and 'edit'.
	 *
	 * @return string
	 */
	public function get_discount_tax( $context = 'view' ) {
		return $this->get_prop( 'discount_tax', $context );
	}

	/*
	|--------------------------------------------------------------------------
	| Array Access Methods
	|--------------------------------------------------------------------------
	|
	| For backwards compatibility with legacy arrays.
	|
	*/

	/**
	 * OffsetGet for ArrayAccess/Backwards compatibility.
	 *
	 * @deprecated 4.4.0
	 * @param string $offset Offset.
	 * @return mixed
	 */
	public function offsetGet( $offset ) {
		wc_deprecated_function( 'WC_Order_Item_Coupon::offsetGet', '4.4.0', '' );
		if ( 'discount_amount' === $offset ) {
			$offset = 'discount';
		} elseif ( 'discount_amount_tax' === $offset ) {
			$offset = 'discount_tax';
		}
		return parent::offsetGet( $offset );
	}

	/**
	 * OffsetSet for ArrayAccess/Backwards compatibility.
	 *
	 * @deprecated 4.4.0
	 * @param string $offset Offset.
	 * @param mixed  $value  Value.
	 */
	public function offsetSet( $offset, $value ) {
		wc_deprecated_function( 'WC_Order_Item_Coupon::offsetSet', '4.4.0', '' );
		if ( 'discount_amount' === $offset ) {
			$offset = 'discount';
		} elseif ( 'discount_amount_tax' === $offset ) {
			$offset = 'discount_tax';
		}
		parent::offsetSet( $offset, $value );
	}

	/**
	 * OffsetExists for ArrayAccess.
	 *
	 * @param string $offset Offset.
	 * @return bool
	 */
	public function offsetExists( $offset ) {
		if ( in_array( $offset, array( 'discount_amount', 'discount_amount_tax' ), true ) ) {
			return true;
		}
		return parent::offsetExists( $offset );
	}
}
class-wc-register-wp-admin-settings.php000064400000012063151336044770014177 0ustar00<?php
/**
 * Take settings registered for WP-Admin and hooks them up to the REST API
 *
 * @package  WooCommerce\Classes
 * @version  3.0.0
 * @since    3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Register WP admin settings class.
 */
class WC_Register_WP_Admin_Settings {

	/**
	 * Contains the current class to pull settings from.
	 * Either a admin page object or WC_Email object
	 *
	 * @var WC_Register_WP_Admin_Settings
	 */
	protected $object;

	/**
	 * Hooks into the settings API and starts registering our settings.
	 *
	 * @since 3.0.0
	 * @param WC_Email|WC_Settings_Page $object The object that contains the settings to register.
	 * @param string                    $type   Type of settings to register (email or page).
	 */
	public function __construct( $object, $type ) {
		if ( ! is_object( $object ) ) {
			return;
		}

		$this->object = $object;

		if ( 'page' === $type ) {
			add_filter( 'woocommerce_settings_groups', array( $this, 'register_page_group' ) );
			add_filter( 'woocommerce_settings-' . $this->object->get_id(), array( $this, 'register_page_settings' ) );
		} elseif ( 'email' === $type ) {
			add_filter( 'woocommerce_settings_groups', array( $this, 'register_email_group' ) );
			add_filter( 'woocommerce_settings-email_' . $this->object->id, array( $this, 'register_email_settings' ) );
		}
	}

	/**
	 * Register's all of our different notification emails as sub groups
	 * of email settings.
	 *
	 * @since  3.0.0
	 * @param  array $groups Existing registered groups.
	 * @return array
	 */
	public function register_email_group( $groups ) {
		$groups[] = array(
			'id'          => 'email_' . $this->object->id,
			'label'       => $this->object->title,
			'description' => $this->object->description,
			'parent_id'   => 'email',
		);
		return $groups;
	}

	/**
	 * Registers all of the setting form fields for emails to each email type's group.
	 *
	 * @since  3.0.0
	 * @param  array $settings Existing registered settings.
	 * @return array
	 */
	public function register_email_settings( $settings ) {
		foreach ( $this->object->form_fields as $id => $setting ) {
			$setting['id']         = $id;
			$setting['option_key'] = array( $this->object->get_option_key(), $id );
			$new_setting           = $this->register_setting( $setting );
			if ( $new_setting ) {
				$settings[] = $new_setting;
			}
		}
		return $settings;
	}

	/**
	 * Registers a setting group, based on admin page ID & label as parent group.
	 *
	 * @since  3.0.0
	 * @param  array $groups Array of previously registered groups.
	 * @return array
	 */
	public function register_page_group( $groups ) {
		$groups[] = array(
			'id'    => $this->object->get_id(),
			'label' => $this->object->get_label(),
		);
		return $groups;
	}

	/**
	 * Registers settings to a specific group.
	 *
	 * @since  3.0.0
	 * @param  array $settings Existing registered settings.
	 * @return array
	 */
	public function register_page_settings( $settings ) {
		/**
		 * WP admin settings can be broken down into separate sections from
		 * a UI standpoint. This will grab all the sections associated with
		 * a particular setting group (like 'products') and register them
		 * to the REST API.
		 */
		$sections = $this->object->get_sections();
		if ( empty( $sections ) ) {
			// Default section is just an empty string, per admin page classes.
			$sections = array( ''  => '' );
		}

		/**
		 * We are using 'WC_Settings_Page::get_settings' on purpose even thought it's deprecated.
		 * See the method documentation for an explanation.
		 */

		foreach ( $sections as $section => $section_label ) {
			$settings_from_section = $this->object->get_settings( $section );
			foreach ( $settings_from_section as $setting ) {
				if ( ! isset( $setting['id'] ) ) {
					continue;
				}
				$setting['option_key'] = $setting['id'];
				$new_setting           = $this->register_setting( $setting );
				if ( $new_setting ) {
					$settings[] = $new_setting;
				}
			}
		}
		return $settings;
	}

	/**
	 * Register a setting into the format expected for the Settings REST API.
	 *
	 * @since 3.0.0
	 * @param  array $setting Setting data.
	 * @return array|bool
	 */
	public function register_setting( $setting ) {
		if ( ! isset( $setting['id'] ) ) {
			return false;
		}

		$description = '';
		if ( ! empty( $setting['desc'] ) ) {
			$description = $setting['desc'];
		} elseif ( ! empty( $setting['description'] ) ) {
			$description = $setting['description'];
		}

		$new_setting = array(
			'id'          => $setting['id'],
			'label'       => ( ! empty( $setting['title'] ) ? $setting['title'] : '' ),
			'description' => $description,
			'type'        => $setting['type'],
			'option_key'  => $setting['option_key'],
		);

		if ( isset( $setting['default'] ) ) {
			$new_setting['default'] = $setting['default'];
		}
		if ( isset( $setting['options'] ) ) {
			$new_setting['options'] = $setting['options'];
		}
		if ( isset( $setting['desc_tip'] ) ) {
			if ( true === $setting['desc_tip'] ) {
				$new_setting['tip'] = $description;
			} elseif ( ! empty( $setting['desc_tip'] ) ) {
				$new_setting['tip'] = $setting['desc_tip'];
			}
		}

		return $new_setting;
	}

}
class-wc-log-levels.php000064400000005050151336044770011052 0ustar00<?php
/**
 * Standard log levels
 *
 * @version 3.2.0
 * @package WooCommerce\Classes
 */

defined( 'ABSPATH' ) || exit;

/**
 * Log levels class.
 */
abstract class WC_Log_Levels {

	/**
	 * Log Levels
	 *
	 * Description of levels:
	 *     'emergency': System is unusable.
	 *     'alert': Action must be taken immediately.
	 *     'critical': Critical conditions.
	 *     'error': Error conditions.
	 *     'warning': Warning conditions.
	 *     'notice': Normal but significant condition.
	 *     'info': Informational messages.
	 *     'debug': Debug-level messages.
	 *
	 * @see @link {https://tools.ietf.org/html/rfc5424}
	 */
	const EMERGENCY = 'emergency';
	const ALERT     = 'alert';
	const CRITICAL  = 'critical';
	const ERROR     = 'error';
	const WARNING   = 'warning';
	const NOTICE    = 'notice';
	const INFO      = 'info';
	const DEBUG     = 'debug';

	/**
	 * Level strings mapped to integer severity.
	 *
	 * @var array
	 */
	protected static $level_to_severity = array(
		self::EMERGENCY => 800,
		self::ALERT     => 700,
		self::CRITICAL  => 600,
		self::ERROR     => 500,
		self::WARNING   => 400,
		self::NOTICE    => 300,
		self::INFO      => 200,
		self::DEBUG     => 100,
	);

	/**
	 * Severity integers mapped to level strings.
	 *
	 * This is the inverse of $level_severity.
	 *
	 * @var array
	 */
	protected static $severity_to_level = array(
		800 => self::EMERGENCY,
		700 => self::ALERT,
		600 => self::CRITICAL,
		500 => self::ERROR,
		400 => self::WARNING,
		300 => self::NOTICE,
		200 => self::INFO,
		100 => self::DEBUG,
	);


	/**
	 * Validate a level string.
	 *
	 * @param string $level Log level.
	 * @return bool True if $level is a valid level.
	 */
	public static function is_valid_level( $level ) {
		return array_key_exists( strtolower( $level ), self::$level_to_severity );
	}

	/**
	 * Translate level string to integer.
	 *
	 * @param string $level Log level, options: emergency|alert|critical|error|warning|notice|info|debug.
	 * @return int 100 (debug) - 800 (emergency) or 0 if not recognized
	 */
	public static function get_level_severity( $level ) {
		return self::is_valid_level( $level ) ? self::$level_to_severity[ strtolower( $level ) ] : 0;
	}

	/**
	 * Translate severity integer to level string.
	 *
	 * @param int $severity Severity level.
	 * @return bool|string False if not recognized. Otherwise string representation of level.
	 */
	public static function get_severity_level( $severity ) {
		if ( ! array_key_exists( $severity, self::$severity_to_level ) ) {
			return false;
		}
		return self::$severity_to_level[ $severity ];
	}

}
class-wc-geolocation.php000064400000024532151336044770011312 0ustar00<?php
/**
 * Geolocation class
 *
 * Handles geolocation and updating the geolocation database.
 *
 * This product includes GeoLite data created by MaxMind, available from http://www.maxmind.com.
 *
 * @package WooCommerce\Classes
 * @version 3.9.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * WC_Geolocation Class.
 */
class WC_Geolocation {

	/**
	 * GeoLite IPv4 DB.
	 *
	 * @deprecated 3.4.0
	 */
	const GEOLITE_DB = 'http://geolite.maxmind.com/download/geoip/database/GeoLiteCountry/GeoIP.dat.gz';

	/**
	 * GeoLite IPv6 DB.
	 *
	 * @deprecated 3.4.0
	 */
	const GEOLITE_IPV6_DB = 'http://geolite.maxmind.com/download/geoip/database/GeoIPv6.dat.gz';

	/**
	 * GeoLite2 DB.
	 *
	 * @since 3.4.0
	 * @deprecated 3.9.0
	 */
	const GEOLITE2_DB = 'http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz';

	/**
	 * API endpoints for looking up user IP address.
	 *
	 * @var array
	 */
	private static $ip_lookup_apis = array(
		'ipify'             => 'http://api.ipify.org/',
		'ipecho'            => 'http://ipecho.net/plain',
		'ident'             => 'http://ident.me',
		'whatismyipaddress' => 'http://bot.whatismyipaddress.com',
	);

	/**
	 * API endpoints for geolocating an IP address
	 *
	 * @var array
	 */
	private static $geoip_apis = array(
		'ipinfo.io'  => 'https://ipinfo.io/%s/json',
		'ip-api.com' => 'http://ip-api.com/json/%s',
	);

	/**
	 * Check if geolocation is enabled.
	 *
	 * @since 3.4.0
	 * @param string $current_settings Current geolocation settings.
	 * @return bool
	 */
	private static function is_geolocation_enabled( $current_settings ) {
		return in_array( $current_settings, array( 'geolocation', 'geolocation_ajax' ), true );
	}

	/**
	 * Get current user IP Address.
	 *
	 * @return string
	 */
	public static function get_ip_address() {
		if ( isset( $_SERVER['HTTP_X_REAL_IP'] ) ) {
			return sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_REAL_IP'] ) );
		} elseif ( isset( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
			// Proxy servers can send through this header like this: X-Forwarded-For: client1, proxy1, proxy2
			// Make sure we always only send through the first IP in the list which should always be the client IP.
			return (string) rest_is_ip_address( trim( current( preg_split( '/,/', sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) ) ) ) );
		} elseif ( isset( $_SERVER['REMOTE_ADDR'] ) ) {
			return sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) );
		}
		return '';
	}

	/**
	 * Get user IP Address using an external service.
	 * This can be used as a fallback for users on localhost where
	 * get_ip_address() will be a local IP and non-geolocatable.
	 *
	 * @return string
	 */
	public static function get_external_ip_address() {
		$external_ip_address = '0.0.0.0';

		if ( '' !== self::get_ip_address() ) {
			$transient_name      = 'external_ip_address_' . self::get_ip_address();
			$external_ip_address = get_transient( $transient_name );
		}

		if ( false === $external_ip_address ) {
			$external_ip_address     = '0.0.0.0';
			$ip_lookup_services      = apply_filters( 'woocommerce_geolocation_ip_lookup_apis', self::$ip_lookup_apis );
			$ip_lookup_services_keys = array_keys( $ip_lookup_services );
			shuffle( $ip_lookup_services_keys );

			foreach ( $ip_lookup_services_keys as $service_name ) {
				$service_endpoint = $ip_lookup_services[ $service_name ];
				$response         = wp_safe_remote_get( $service_endpoint, array( 'timeout' => 2 ) );

				if ( ! is_wp_error( $response ) && rest_is_ip_address( $response['body'] ) ) {
					$external_ip_address = apply_filters( 'woocommerce_geolocation_ip_lookup_api_response', wc_clean( $response['body'] ), $service_name );
					break;
				}
			}

			set_transient( $transient_name, $external_ip_address, DAY_IN_SECONDS );
		}

		return $external_ip_address;
	}

	/**
	 * Geolocate an IP address.
	 *
	 * @param  string $ip_address   IP Address.
	 * @param  bool   $fallback     If true, fallbacks to alternative IP detection (can be slower).
	 * @param  bool   $api_fallback If true, uses geolocation APIs if the database file doesn't exist (can be slower).
	 * @return array
	 */
	public static function geolocate_ip( $ip_address = '', $fallback = false, $api_fallback = true ) {
		// Filter to allow custom geolocation of the IP address.
		$country_code = apply_filters( 'woocommerce_geolocate_ip', false, $ip_address, $fallback, $api_fallback );

		if ( false !== $country_code ) {
			return array(
				'country'  => $country_code,
				'state'    => '',
				'city'     => '',
				'postcode' => '',
			);
		}

		if ( empty( $ip_address ) ) {
			$ip_address = self::get_ip_address();
		}

		$country_code = self::get_country_code_from_headers();

		/**
		 * Get geolocation filter.
		 *
		 * @since 3.9.0
		 * @param array  $geolocation Geolocation data, including country, state, city, and postcode.
		 * @param string $ip_address  IP Address.
		 */
		$geolocation = apply_filters(
			'woocommerce_get_geolocation',
			array(
				'country'  => $country_code,
				'state'    => '',
				'city'     => '',
				'postcode' => '',
			),
			$ip_address
		);

		// If we still haven't found a country code, let's consider doing an API lookup.
		if ( '' === $geolocation['country'] && $api_fallback ) {
			$geolocation['country'] = self::geolocate_via_api( $ip_address );
		}

		// It's possible that we're in a local environment, in which case the geolocation needs to be done from the
		// external address.
		if ( '' === $geolocation['country'] && $fallback ) {
			$external_ip_address = self::get_external_ip_address();

			// Only bother with this if the external IP differs.
			if ( '0.0.0.0' !== $external_ip_address && $external_ip_address !== $ip_address ) {
				return self::geolocate_ip( $external_ip_address, false, $api_fallback );
			}
		}

		return array(
			'country'  => $geolocation['country'],
			'state'    => $geolocation['state'],
			'city'     => $geolocation['city'],
			'postcode' => $geolocation['postcode'],
		);
	}

	/**
	 * Path to our local db.
	 *
	 * @deprecated 3.9.0
	 * @param  string $deprecated Deprecated since 3.4.0.
	 * @return string
	 */
	public static function get_local_database_path( $deprecated = '2' ) {
		wc_deprecated_function( 'WC_Geolocation::get_local_database_path', '3.9.0' );
		$integration = wc()->integrations->get_integration( 'maxmind_geolocation' );
		return $integration->get_database_service()->get_database_path();
	}

	/**
	 * Update geoip database.
	 *
	 * @deprecated 3.9.0
	 * Extract files with PharData. Tool built into PHP since 5.3.
	 */
	public static function update_database() {
		wc_deprecated_function( 'WC_Geolocation::update_database', '3.9.0' );
		$integration = wc()->integrations->get_integration( 'maxmind_geolocation' );
		$integration->update_database();
	}

	/**
	 * Fetches the country code from the request headers, if one is available.
	 *
	 * @since 3.9.0
	 * @return string The country code pulled from the headers, or empty string if one was not found.
	 */
	private static function get_country_code_from_headers() {
		$country_code = '';

		$headers = array(
			'MM_COUNTRY_CODE',
			'GEOIP_COUNTRY_CODE',
			'HTTP_CF_IPCOUNTRY',
			'HTTP_X_COUNTRY_CODE',
		);

		foreach ( $headers as $header ) {
			if ( empty( $_SERVER[ $header ] ) ) {
				continue;
			}

			$country_code = strtoupper( sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) ) );
			break;
		}

		return $country_code;
	}

	/**
	 * Use APIs to Geolocate the user.
	 *
	 * Geolocation APIs can be added through the use of the woocommerce_geolocation_geoip_apis filter.
	 * Provide a name=>value pair for service-slug=>endpoint.
	 *
	 * If APIs are defined, one will be chosen at random to fulfil the request. After completing, the result
	 * will be cached in a transient.
	 *
	 * @param  string $ip_address IP address.
	 * @return string
	 */
	private static function geolocate_via_api( $ip_address ) {
		$country_code = get_transient( 'geoip_' . $ip_address );

		if ( false === $country_code ) {
			$geoip_services = apply_filters( 'woocommerce_geolocation_geoip_apis', self::$geoip_apis );

			if ( empty( $geoip_services ) ) {
				return '';
			}

			$geoip_services_keys = array_keys( $geoip_services );

			shuffle( $geoip_services_keys );

			foreach ( $geoip_services_keys as $service_name ) {
				$service_endpoint = $geoip_services[ $service_name ];
				$response         = wp_safe_remote_get( sprintf( $service_endpoint, $ip_address ), array( 'timeout' => 2 ) );

				if ( ! is_wp_error( $response ) && $response['body'] ) {
					switch ( $service_name ) {
						case 'ipinfo.io':
							$data         = json_decode( $response['body'] );
							$country_code = isset( $data->country ) ? $data->country : '';
							break;
						case 'ip-api.com':
							$data         = json_decode( $response['body'] );
							$country_code = isset( $data->countryCode ) ? $data->countryCode : ''; // @codingStandardsIgnoreLine
							break;
						default:
							$country_code = apply_filters( 'woocommerce_geolocation_geoip_response_' . $service_name, '', $response['body'] );
							break;
					}

					$country_code = sanitize_text_field( strtoupper( $country_code ) );

					if ( $country_code ) {
						break;
					}
				}
			}

			set_transient( 'geoip_' . $ip_address, $country_code, DAY_IN_SECONDS );
		}

		return $country_code;
	}

	/**
	 * Hook in geolocation functionality.
	 *
	 * @deprecated 3.9.0
	 * @return null
	 */
	public static function init() {
		wc_deprecated_function( 'WC_Geolocation::init', '3.9.0' );
		return null;
	}

	/**
	 * Prevent geolocation via MaxMind when using legacy versions of php.
	 *
	 * @deprecated 3.9.0
	 * @since 3.4.0
	 * @param string $default_customer_address current value.
	 * @return string
	 */
	public static function disable_geolocation_on_legacy_php( $default_customer_address ) {
		wc_deprecated_function( 'WC_Geolocation::disable_geolocation_on_legacy_php', '3.9.0' );

		if ( self::is_geolocation_enabled( $default_customer_address ) ) {
			$default_customer_address = 'base';
		}

		return $default_customer_address;
	}

	/**
	 * Maybe trigger a DB update for the first time.
	 *
	 * @deprecated 3.9.0
	 * @param  string $new_value New value.
	 * @param  string $old_value Old value.
	 * @return string
	 */
	public static function maybe_update_database( $new_value, $old_value ) {
		wc_deprecated_function( 'WC_Geolocation::maybe_update_database', '3.9.0' );
		if ( $new_value !== $old_value && self::is_geolocation_enabled( $new_value ) ) {
			self::update_database();
		}

		return $new_value;
	}
}
wc-stock-functions.php000064400000032334151336044770011034 0ustar00<?php
/**
 * WooCommerce Stock Functions
 *
 * Functions used to manage product stock levels.
 *
 * @package WooCommerce\Functions
 * @version 3.4.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Update a product's stock amount.
 *
 * Uses queries rather than update_post_meta so we can do this in one query (to avoid stock issues).
 *
 * @since  3.0.0 this supports set, increase and decrease.
 *
 * @param  int|WC_Product $product        Product ID or product instance.
 * @param  int|null       $stock_quantity Stock quantity.
 * @param  string         $operation      Type of opertion, allows 'set', 'increase' and 'decrease'.
 * @param  bool           $updating       If true, the product object won't be saved here as it will be updated later.
 * @return bool|int|null
 */
function wc_update_product_stock( $product, $stock_quantity = null, $operation = 'set', $updating = false ) {
	if ( ! is_a( $product, 'WC_Product' ) ) {
		$product = wc_get_product( $product );
	}

	if ( ! $product ) {
		return false;
	}

	if ( ! is_null( $stock_quantity ) && $product->managing_stock() ) {
		// Some products (variations) can have their stock managed by their parent. Get the correct object to be updated here.
		$product_id_with_stock = $product->get_stock_managed_by_id();
		$product_with_stock    = $product_id_with_stock !== $product->get_id() ? wc_get_product( $product_id_with_stock ) : $product;
		$data_store            = WC_Data_Store::load( 'product' );

		// Fire actions to let 3rd parties know the stock is about to be changed.
		if ( $product_with_stock->is_type( 'variation' ) ) {
			do_action( 'woocommerce_variation_before_set_stock', $product_with_stock );
		} else {
			do_action( 'woocommerce_product_before_set_stock', $product_with_stock );
		}

		// Update the database.
		$new_stock = $data_store->update_product_stock( $product_id_with_stock, $stock_quantity, $operation );

		// Update the product object.
		$data_store->read_stock_quantity( $product_with_stock, $new_stock );

		// If this is not being called during an update routine, save the product so stock status etc is in sync, and caches are cleared.
		if ( ! $updating ) {
			$product_with_stock->save();
		}

		// Fire actions to let 3rd parties know the stock changed.
		if ( $product_with_stock->is_type( 'variation' ) ) {
			do_action( 'woocommerce_variation_set_stock', $product_with_stock );
		} else {
			do_action( 'woocommerce_product_set_stock', $product_with_stock );
		}

		return $product_with_stock->get_stock_quantity();
	}
	return $product->get_stock_quantity();
}

/**
 * Update a product's stock status.
 *
 * @param int    $product_id Product ID.
 * @param string $status     Status.
 */
function wc_update_product_stock_status( $product_id, $status ) {
	$product = wc_get_product( $product_id );

	if ( $product ) {
		$product->set_stock_status( $status );
		$product->save();
	}
}

/**
 * When a payment is complete, we can reduce stock levels for items within an order.
 *
 * @since 3.0.0
 * @param int $order_id Order ID.
 */
function wc_maybe_reduce_stock_levels( $order_id ) {
	$order = wc_get_order( $order_id );

	if ( ! $order ) {
		return;
	}

	$stock_reduced  = $order->get_data_store()->get_stock_reduced( $order_id );
	$trigger_reduce = apply_filters( 'woocommerce_payment_complete_reduce_order_stock', ! $stock_reduced, $order_id );

	// Only continue if we're reducing stock.
	if ( ! $trigger_reduce ) {
		return;
	}

	wc_reduce_stock_levels( $order );

	// Ensure stock is marked as "reduced" in case payment complete or other stock actions are called.
	$order->get_data_store()->set_stock_reduced( $order_id, true );
}
add_action( 'woocommerce_payment_complete', 'wc_maybe_reduce_stock_levels' );
add_action( 'woocommerce_order_status_completed', 'wc_maybe_reduce_stock_levels' );
add_action( 'woocommerce_order_status_processing', 'wc_maybe_reduce_stock_levels' );
add_action( 'woocommerce_order_status_on-hold', 'wc_maybe_reduce_stock_levels' );

/**
 * When a payment is cancelled, restore stock.
 *
 * @since 3.0.0
 * @param int $order_id Order ID.
 */
function wc_maybe_increase_stock_levels( $order_id ) {
	$order = wc_get_order( $order_id );

	if ( ! $order ) {
		return;
	}

	$stock_reduced    = $order->get_data_store()->get_stock_reduced( $order_id );
	$trigger_increase = (bool) $stock_reduced;

	// Only continue if we're increasing stock.
	if ( ! $trigger_increase ) {
		return;
	}

	wc_increase_stock_levels( $order );

	// Ensure stock is not marked as "reduced" anymore.
	$order->get_data_store()->set_stock_reduced( $order_id, false );
}
add_action( 'woocommerce_order_status_cancelled', 'wc_maybe_increase_stock_levels' );
add_action( 'woocommerce_order_status_pending', 'wc_maybe_increase_stock_levels' );

/**
 * Reduce stock levels for items within an order, if stock has not already been reduced for the items.
 *
 * @since 3.0.0
 * @param int|WC_Order $order_id Order ID or order instance.
 */
function wc_reduce_stock_levels( $order_id ) {
	if ( is_a( $order_id, 'WC_Order' ) ) {
		$order    = $order_id;
		$order_id = $order->get_id();
	} else {
		$order = wc_get_order( $order_id );
	}
	// We need an order, and a store with stock management to continue.
	if ( ! $order || 'yes' !== get_option( 'woocommerce_manage_stock' ) || ! apply_filters( 'woocommerce_can_reduce_order_stock', true, $order ) ) {
		return;
	}

	$changes = array();

	// Loop over all items.
	foreach ( $order->get_items() as $item ) {
		if ( ! $item->is_type( 'line_item' ) ) {
			continue;
		}

		// Only reduce stock once for each item.
		$product            = $item->get_product();
		$item_stock_reduced = $item->get_meta( '_reduced_stock', true );

		if ( $item_stock_reduced || ! $product || ! $product->managing_stock() ) {
			continue;
		}

		/**
		 * Filter order item quantity.
		 *
		 * @param int|float             $quantity Quantity.
		 * @param WC_Order              $order    Order data.
		 * @param WC_Order_Item_Product $item Order item data.
		 */
		$qty       = apply_filters( 'woocommerce_order_item_quantity', $item->get_quantity(), $order, $item );
		$item_name = $product->get_formatted_name();
		$new_stock = wc_update_product_stock( $product, $qty, 'decrease' );

		if ( is_wp_error( $new_stock ) ) {
			/* translators: %s item name. */
			$order->add_order_note( sprintf( __( 'Unable to reduce stock for item %s.', 'woocommerce' ), $item_name ) );
			continue;
		}

		$item->add_meta_data( '_reduced_stock', $qty, true );
		$item->save();

		$changes[] = array(
			'product' => $product,
			'from'    => $new_stock + $qty,
			'to'      => $new_stock,
		);
	}

	wc_trigger_stock_change_notifications( $order, $changes );

	do_action( 'woocommerce_reduce_order_stock', $order );
}

/**
 * After stock change events, triggers emails and adds order notes.
 *
 * @since 3.5.0
 * @param WC_Order $order order object.
 * @param array    $changes Array of changes.
 */
function wc_trigger_stock_change_notifications( $order, $changes ) {
	if ( empty( $changes ) ) {
		return;
	}

	$order_notes     = array();
	$no_stock_amount = absint( get_option( 'woocommerce_notify_no_stock_amount', 0 ) );

	foreach ( $changes as $change ) {
		$order_notes[]    = $change['product']->get_formatted_name() . ' ' . $change['from'] . '&rarr;' . $change['to'];
		$low_stock_amount = absint( wc_get_low_stock_amount( wc_get_product( $change['product']->get_id() ) ) );
		if ( $change['to'] <= $no_stock_amount ) {
			do_action( 'woocommerce_no_stock', wc_get_product( $change['product']->get_id() ) );
		} elseif ( $change['to'] <= $low_stock_amount ) {
			do_action( 'woocommerce_low_stock', wc_get_product( $change['product']->get_id() ) );
		}

		if ( $change['to'] < 0 ) {
			do_action(
				'woocommerce_product_on_backorder',
				array(
					'product'  => wc_get_product( $change['product']->get_id() ),
					'order_id' => $order->get_id(),
					'quantity' => abs( $change['from'] - $change['to'] ),
				)
			);
		}
	}

	$order->add_order_note( __( 'Stock levels reduced:', 'woocommerce' ) . ' ' . implode( ', ', $order_notes ) );
}

/**
 * Increase stock levels for items within an order.
 *
 * @since 3.0.0
 * @param int|WC_Order $order_id Order ID or order instance.
 */
function wc_increase_stock_levels( $order_id ) {
	if ( is_a( $order_id, 'WC_Order' ) ) {
		$order    = $order_id;
		$order_id = $order->get_id();
	} else {
		$order = wc_get_order( $order_id );
	}

	// We need an order, and a store with stock management to continue.
	if ( ! $order || 'yes' !== get_option( 'woocommerce_manage_stock' ) || ! apply_filters( 'woocommerce_can_restore_order_stock', true, $order ) ) {
		return;
	}

	$changes = array();

	// Loop over all items.
	foreach ( $order->get_items() as $item ) {
		if ( ! $item->is_type( 'line_item' ) ) {
			continue;
		}

		// Only increase stock once for each item.
		$product            = $item->get_product();
		$item_stock_reduced = $item->get_meta( '_reduced_stock', true );

		if ( ! $item_stock_reduced || ! $product || ! $product->managing_stock() ) {
			continue;
		}

		$item_name = $product->get_formatted_name();
		$new_stock = wc_update_product_stock( $product, $item_stock_reduced, 'increase' );

		if ( is_wp_error( $new_stock ) ) {
			/* translators: %s item name. */
			$order->add_order_note( sprintf( __( 'Unable to restore stock for item %s.', 'woocommerce' ), $item_name ) );
			continue;
		}

		$item->delete_meta_data( '_reduced_stock' );
		$item->save();

		$changes[] = $item_name . ' ' . ( $new_stock - $item_stock_reduced ) . '&rarr;' . $new_stock;
	}

	if ( $changes ) {
		$order->add_order_note( __( 'Stock levels increased:', 'woocommerce' ) . ' ' . implode( ', ', $changes ) );
	}

	do_action( 'woocommerce_restore_order_stock', $order );
}

/**
 * See how much stock is being held in pending orders.
 *
 * @since 3.5.0
 * @param WC_Product $product Product to check.
 * @param integer    $exclude_order_id Order ID to exclude.
 * @return int
 */
function wc_get_held_stock_quantity( WC_Product $product, $exclude_order_id = 0 ) {
	/**
	 * Filter: woocommerce_hold_stock_for_checkout
	 * Allows enable/disable hold stock functionality on checkout.
	 *
	 * @since 4.3.0
	 * @param bool $enabled Default to true if managing stock globally.
	 */
	if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', wc_string_to_bool( get_option( 'woocommerce_manage_stock', 'yes' ) ) ) ) {
		return 0;
	}

	return ( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->get_reserved_stock( $product, $exclude_order_id );
}

/**
 * Hold stock for an order.
 *
 * @throws ReserveStockException If reserve stock fails.
 *
 * @since 4.1.0
 * @param \WC_Order|int $order Order ID or instance.
 */
function wc_reserve_stock_for_order( $order ) {
	/**
	 * Filter: woocommerce_hold_stock_for_checkout
	 * Allows enable/disable hold stock functionality on checkout.
	 *
	 * @since @since 4.1.0
	 * @param bool $enabled Default to true if managing stock globally.
	 */
	if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', wc_string_to_bool( get_option( 'woocommerce_manage_stock', 'yes' ) ) ) ) {
		return;
	}

	$order = $order instanceof WC_Order ? $order : wc_get_order( $order );

	if ( $order ) {
		( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->reserve_stock_for_order( $order );
	}
}
add_action( 'woocommerce_checkout_order_created', 'wc_reserve_stock_for_order' );

/**
 * Release held stock for an order.
 *
 * @since 4.3.0
 * @param \WC_Order|int $order Order ID or instance.
 */
function wc_release_stock_for_order( $order ) {
	/**
	 * Filter: woocommerce_hold_stock_for_checkout
	 * Allows enable/disable hold stock functionality on checkout.
	 *
	 * @since 4.3.0
	 * @param bool $enabled Default to true if managing stock globally.
	 */
	if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', wc_string_to_bool( get_option( 'woocommerce_manage_stock', 'yes' ) ) ) ) {
		return;
	}

	$order = $order instanceof WC_Order ? $order : wc_get_order( $order );

	if ( $order ) {
		( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->release_stock_for_order( $order );
	}
}
add_action( 'woocommerce_checkout_order_exception', 'wc_release_stock_for_order' );
add_action( 'woocommerce_payment_complete', 'wc_release_stock_for_order', 11 );
add_action( 'woocommerce_order_status_cancelled', 'wc_release_stock_for_order', 11 );
add_action( 'woocommerce_order_status_completed', 'wc_release_stock_for_order', 11 );
add_action( 'woocommerce_order_status_processing', 'wc_release_stock_for_order', 11 );
add_action( 'woocommerce_order_status_on-hold', 'wc_release_stock_for_order', 11 );

/**
 * Return low stock amount to determine if notification needs to be sent
 *
 * Since 5.2.0, this function no longer redirects from variation to its parent product.
 * Low stock amount can now be attached to the variation itself and if it isn't, only
 * then we check the parent product, and if it's not there, then we take the default
 * from the store-wide setting.
 *
 * @param  WC_Product $product Product to get data from.
 * @since  3.5.0
 * @return int
 */
function wc_get_low_stock_amount( WC_Product $product ) {
	$low_stock_amount = $product->get_low_stock_amount();

	if ( '' === $low_stock_amount && $product->is_type( 'variation' ) ) {
		$product          = wc_get_product( $product->get_parent_id() );
		$low_stock_amount = $product->get_low_stock_amount();
	}

	if ( '' === $low_stock_amount ) {
		$low_stock_amount = get_option( 'woocommerce_notify_low_stock_amount', 2 );
	}

	return (int) $low_stock_amount;
}
widgets/class-wc-widget-recent-reviews.php000064400000004361151336044770014676 0ustar00<?php
/**
 * Recent Reviews Widget.
 *
 * @package WooCommerce\Widgets
 * @version 2.3.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Widget recent reviews class.
 */
class WC_Widget_Recent_Reviews extends WC_Widget {

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->widget_cssclass    = 'woocommerce widget_recent_reviews';
		$this->widget_description = __( 'Display a list of recent reviews from your store.', 'woocommerce' );
		$this->widget_id          = 'woocommerce_recent_reviews';
		$this->widget_name        = __( 'Recent Product Reviews', 'woocommerce' );
		$this->settings           = array(
			'title'  => array(
				'type'  => 'text',
				'std'   => __( 'Recent reviews', 'woocommerce' ),
				'label' => __( 'Title', 'woocommerce' ),
			),
			'number' => array(
				'type'  => 'number',
				'step'  => 1,
				'min'   => 1,
				'max'   => '',
				'std'   => 10,
				'label' => __( 'Number of reviews to show', 'woocommerce' ),
			),
		);

		parent::__construct();
	}

	/**
	 * Output widget.
	 *
	 * @see WP_Widget
	 * @param array $args     Arguments.
	 * @param array $instance Widget instance.
	 */
	public function widget( $args, $instance ) {
		global $comments, $comment;

		if ( $this->get_cached_widget( $args ) ) {
			return;
		}

		ob_start();

		$number   = ! empty( $instance['number'] ) ? absint( $instance['number'] ) : $this->settings['number']['std'];
		$comments = get_comments(
			array(
				'number'      => $number,
				'status'      => 'approve',
				'post_status' => 'publish',
				'post_type'   => 'product',
				'parent'      => 0,
			)
		); // WPCS: override ok.

		if ( $comments ) {
			$this->widget_start( $args, $instance );

			echo wp_kses_post( apply_filters( 'woocommerce_before_widget_product_review_list', '<ul class="product_list_widget">' ) );

			foreach ( (array) $comments as $comment ) {
				wc_get_template(
					'content-widget-reviews.php',
					array(
						'comment' => $comment,
						'product' => wc_get_product( $comment->comment_post_ID ),
					)
				);
			}

			echo wp_kses_post( apply_filters( 'woocommerce_after_widget_product_review_list', '</ul>' ) );

			$this->widget_end( $args );

		}

		$content = ob_get_clean();

		echo $content; // WPCS: XSS ok.

		$this->cache_widget( $args, $content );
	}
}
widgets/class-wc-widget-products.php000064400000014175151336044770013603 0ustar00<?php
/**
 * List products. One widget to rule them all.
 *
 * @package WooCommerce\Widgets
 * @version 3.3.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Widget products.
 */
class WC_Widget_Products extends WC_Widget {

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->widget_cssclass    = 'woocommerce widget_products';
		$this->widget_description = __( "A list of your store's products.", 'woocommerce' );
		$this->widget_id          = 'woocommerce_products';
		$this->widget_name        = __( 'Products list', 'woocommerce' );
		$this->settings           = array(
			'title'       => array(
				'type'  => 'text',
				'std'   => __( 'Products', 'woocommerce' ),
				'label' => __( 'Title', 'woocommerce' ),
			),
			'number'      => array(
				'type'  => 'number',
				'step'  => 1,
				'min'   => 1,
				'max'   => '',
				'std'   => 5,
				'label' => __( 'Number of products to show', 'woocommerce' ),
			),
			'show'        => array(
				'type'    => 'select',
				'std'     => '',
				'label'   => __( 'Show', 'woocommerce' ),
				'options' => array(
					''         => __( 'All products', 'woocommerce' ),
					'featured' => __( 'Featured products', 'woocommerce' ),
					'onsale'   => __( 'On-sale products', 'woocommerce' ),
				),
			),
			'orderby'     => array(
				'type'    => 'select',
				'std'     => 'date',
				'label'   => __( 'Order by', 'woocommerce' ),
				'options' => array(
					'date'  => __( 'Date', 'woocommerce' ),
					'price' => __( 'Price', 'woocommerce' ),
					'rand'  => __( 'Random', 'woocommerce' ),
					'sales' => __( 'Sales', 'woocommerce' ),
				),
			),
			'order'       => array(
				'type'    => 'select',
				'std'     => 'desc',
				'label'   => _x( 'Order', 'Sorting order', 'woocommerce' ),
				'options' => array(
					'asc'  => __( 'ASC', 'woocommerce' ),
					'desc' => __( 'DESC', 'woocommerce' ),
				),
			),
			'hide_free'   => array(
				'type'  => 'checkbox',
				'std'   => 0,
				'label' => __( 'Hide free products', 'woocommerce' ),
			),
			'show_hidden' => array(
				'type'  => 'checkbox',
				'std'   => 0,
				'label' => __( 'Show hidden products', 'woocommerce' ),
			),
		);

		parent::__construct();
	}

	/**
	 * Query the products and return them.
	 *
	 * @param array $args     Arguments.
	 * @param array $instance Widget instance.
	 *
	 * @return WP_Query
	 */
	public function get_products( $args, $instance ) {
		$number                      = ! empty( $instance['number'] ) ? absint( $instance['number'] ) : $this->settings['number']['std'];
		$show                        = ! empty( $instance['show'] ) ? sanitize_title( $instance['show'] ) : $this->settings['show']['std'];
		$orderby                     = ! empty( $instance['orderby'] ) ? sanitize_title( $instance['orderby'] ) : $this->settings['orderby']['std'];
		$order                       = ! empty( $instance['order'] ) ? sanitize_title( $instance['order'] ) : $this->settings['order']['std'];
		$product_visibility_term_ids = wc_get_product_visibility_term_ids();

		$query_args = array(
			'posts_per_page' => $number,
			'post_status'    => 'publish',
			'post_type'      => 'product',
			'no_found_rows'  => 1,
			'order'          => $order,
			'meta_query'     => array(),
			'tax_query'      => array(
				'relation' => 'AND',
			),
		); // WPCS: slow query ok.

		if ( empty( $instance['show_hidden'] ) ) {
			$query_args['tax_query'][] = array(
				'taxonomy' => 'product_visibility',
				'field'    => 'term_taxonomy_id',
				'terms'    => is_search() ? $product_visibility_term_ids['exclude-from-search'] : $product_visibility_term_ids['exclude-from-catalog'],
				'operator' => 'NOT IN',
			);
			$query_args['post_parent'] = 0;
		}

		if ( ! empty( $instance['hide_free'] ) ) {
			$query_args['meta_query'][] = array(
				'key'     => '_price',
				'value'   => 0,
				'compare' => '>',
				'type'    => 'DECIMAL',
			);
		}

		if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) {
			$query_args['tax_query'][] = array(
				array(
					'taxonomy' => 'product_visibility',
					'field'    => 'term_taxonomy_id',
					'terms'    => $product_visibility_term_ids['outofstock'],
					'operator' => 'NOT IN',
				),
			); // WPCS: slow query ok.
		}

		switch ( $show ) {
			case 'featured':
				$query_args['tax_query'][] = array(
					'taxonomy' => 'product_visibility',
					'field'    => 'term_taxonomy_id',
					'terms'    => $product_visibility_term_ids['featured'],
				);
				break;
			case 'onsale':
				$product_ids_on_sale    = wc_get_product_ids_on_sale();
				$product_ids_on_sale[]  = 0;
				$query_args['post__in'] = $product_ids_on_sale;
				break;
		}

		switch ( $orderby ) {
			case 'price':
				$query_args['meta_key'] = '_price'; // WPCS: slow query ok.
				$query_args['orderby']  = 'meta_value_num';
				break;
			case 'rand':
				$query_args['orderby'] = 'rand';
				break;
			case 'sales':
				$query_args['meta_key'] = 'total_sales'; // WPCS: slow query ok.
				$query_args['orderby']  = 'meta_value_num';
				break;
			default:
				$query_args['orderby'] = 'date';
		}

		return new WP_Query( apply_filters( 'woocommerce_products_widget_query_args', $query_args ) );
	}

	/**
	 * Output widget.
	 *
	 * @param array $args     Arguments.
	 * @param array $instance Widget instance.
	 *
	 * @see WP_Widget
	 */
	public function widget( $args, $instance ) {
		if ( $this->get_cached_widget( $args ) ) {
			return;
		}

		ob_start();

		wc_set_loop_prop( 'name', 'widget' );

		$products = $this->get_products( $args, $instance );
		if ( $products && $products->have_posts() ) {
			$this->widget_start( $args, $instance );

			echo wp_kses_post( apply_filters( 'woocommerce_before_widget_product_list', '<ul class="product_list_widget">' ) );

			$template_args = array(
				'widget_id'   => isset( $args['widget_id'] ) ? $args['widget_id'] : $this->widget_id,
				'show_rating' => true,
			);

			while ( $products->have_posts() ) {
				$products->the_post();
				wc_get_template( 'content-widget-product.php', $template_args );
			}

			echo wp_kses_post( apply_filters( 'woocommerce_after_widget_product_list', '</ul>' ) );

			$this->widget_end( $args );
		}

		wp_reset_postdata();

		echo $this->cache_widget( $args, ob_get_clean() ); // WPCS: XSS ok.
	}
}
widgets/class-wc-widget-recently-viewed.php000064400000005656151336044770015052 0ustar00<?php
/**
 * Recent Products Widget.
 *
 * @package WooCommerce\Widgets
 * @version 3.3.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Widget recently viewed.
 */
class WC_Widget_Recently_Viewed extends WC_Widget {

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->widget_cssclass    = 'woocommerce widget_recently_viewed_products';
		$this->widget_description = __( "Display a list of a customer's recently viewed products.", 'woocommerce' );
		$this->widget_id          = 'woocommerce_recently_viewed_products';
		$this->widget_name        = __( 'Recently Viewed Products list', 'woocommerce' );
		$this->settings           = array(
			'title'  => array(
				'type'  => 'text',
				'std'   => __( 'Recently Viewed Products', 'woocommerce' ),
				'label' => __( 'Title', 'woocommerce' ),
			),
			'number' => array(
				'type'  => 'number',
				'step'  => 1,
				'min'   => 1,
				'max'   => 15,
				'std'   => 10,
				'label' => __( 'Number of products to show', 'woocommerce' ),
			),
		);

		parent::__construct();
	}

	/**
	 * Output widget.
	 *
	 * @see WP_Widget
	 * @param array $args     Arguments.
	 * @param array $instance Widget instance.
	 */
	public function widget( $args, $instance ) {
		$viewed_products = ! empty( $_COOKIE['woocommerce_recently_viewed'] ) ? (array) explode( '|', wp_unslash( $_COOKIE['woocommerce_recently_viewed'] ) ) : array(); // @codingStandardsIgnoreLine
		$viewed_products = array_reverse( array_filter( array_map( 'absint', $viewed_products ) ) );

		if ( empty( $viewed_products ) ) {
			return;
		}

		ob_start();

		$number = ! empty( $instance['number'] ) ? absint( $instance['number'] ) : $this->settings['number']['std'];

		$query_args = array(
			'posts_per_page' => $number,
			'no_found_rows'  => 1,
			'post_status'    => 'publish',
			'post_type'      => 'product',
			'post__in'       => $viewed_products,
			'orderby'        => 'post__in',
		);

		if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) {
			$query_args['tax_query'] = array(
				array(
					'taxonomy' => 'product_visibility',
					'field'    => 'name',
					'terms'    => 'outofstock',
					'operator' => 'NOT IN',
				),
			); // WPCS: slow query ok.
		}

		$r = new WP_Query( apply_filters( 'woocommerce_recently_viewed_products_widget_query_args', $query_args ) );

		if ( $r->have_posts() ) {

			$this->widget_start( $args, $instance );

			echo wp_kses_post( apply_filters( 'woocommerce_before_widget_product_list', '<ul class="product_list_widget">' ) );

			$template_args = array(
				'widget_id' => isset( $args['widget_id'] ) ? $args['widget_id'] : $this->widget_id,
			);

			while ( $r->have_posts() ) {
				$r->the_post();
				wc_get_template( 'content-widget-product.php', $template_args );
			}

			echo wp_kses_post( apply_filters( 'woocommerce_after_widget_product_list', '</ul>' ) );

			$this->widget_end( $args );
		}

		wp_reset_postdata();

		$content = ob_get_clean();

		echo $content; // WPCS: XSS ok.
	}
}
widgets/class-wc-widget-product-tag-cloud.php000064400000005604151336044770015272 0ustar00<?php
/**
 * Tag Cloud Widget.
 *
 * @package WooCommerce\Widgets
 * @version 3.4.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Widget product tag cloud
 */
class WC_Widget_Product_Tag_Cloud extends WC_Widget {

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->widget_cssclass    = 'woocommerce widget_product_tag_cloud';
		$this->widget_description = __( 'A cloud of your most used product tags.', 'woocommerce' );
		$this->widget_id          = 'woocommerce_product_tag_cloud';
		$this->widget_name        = __( 'Product Tag Cloud', 'woocommerce' );
		$this->settings           = array(
			'title' => array(
				'type'  => 'text',
				'std'   => __( 'Product tags', 'woocommerce' ),
				'label' => __( 'Title', 'woocommerce' ),
			),
		);

		parent::__construct();
	}

	/**
	 * Output widget.
	 *
	 * @see WP_Widget
	 *
	 * @param array $args     Arguments.
	 * @param array $instance Widget instance.
	 */
	public function widget( $args, $instance ) {
		$current_taxonomy = $this->get_current_taxonomy( $instance );

		if ( empty( $instance['title'] ) ) {
			$taxonomy          = get_taxonomy( $current_taxonomy );
			$instance['title'] = $taxonomy->labels->name;
		}

		$this->widget_start( $args, $instance );

		echo '<div class="tagcloud">';

		wp_tag_cloud(
			apply_filters(
				'woocommerce_product_tag_cloud_widget_args',
				array(
					'taxonomy'                  => $current_taxonomy,
					'topic_count_text_callback' => array( $this, 'topic_count_text' ),
				)
			)
		);

		echo '</div>';

		$this->widget_end( $args );
	}

	/**
	 * Return the taxonomy being displayed.
	 *
	 * @param  object $instance Widget instance.
	 * @return string
	 */
	public function get_current_taxonomy( $instance ) {
		return 'product_tag';
	}

	/**
	 * Returns topic count text.
	 *
	 * @since 3.4.0
	 * @param int $count Count text.
	 * @return string
	 */
	public function topic_count_text( $count ) {
		/* translators: %s: product count */
		return sprintf( _n( '%s product', '%s products', $count, 'woocommerce' ), number_format_i18n( $count ) );
	}

	// Ignore whole block to avoid warnings about PSR2.Methods.MethodDeclaration.Underscore violation.
	// @codingStandardsIgnoreStart
	/**
	 * Return the taxonomy being displayed.
	 *
	 * @deprecated 3.4.0
	 * @param  object $instance Widget instance.
	 * @return string
	 */
	public function _get_current_taxonomy( $instance ) {
		wc_deprecated_function( '_get_current_taxonomy', '3.4.0', 'WC_Widget_Product_Tag_Cloud->get_current_taxonomy' );
		return $this->get_current_taxonomy( $instance );
	}

	/**
	 * Returns topic count text.
	 *
	 * @deprecated 3.4.0
	 * @since 2.6.0
	 * @param int $count Count text.
	 * @return string
	 */
	public function _topic_count_text( $count ) {
		wc_deprecated_function( '_topic_count_text', '3.4.0', 'WC_Widget_Product_Tag_Cloud->topic_count_text' );
		return $this->topic_count_text( $count );
	}
	// @codingStandardsIgnoreEnd
}
widgets/class-wc-widget-price-filter.php000064400000015557151336044770014332 0ustar00<?php
/**
 * Price Filter Widget and related functions.
 *
 * Generates a range slider to filter products by price.
 *
 * @package WooCommerce\Widgets
 * @version 2.3.0
 */

use Automattic\Jetpack\Constants;

defined( 'ABSPATH' ) || exit;

/**
 * Widget price filter class.
 */
class WC_Widget_Price_Filter extends WC_Widget {

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->widget_cssclass    = 'woocommerce widget_price_filter';
		$this->widget_description = __( 'Display a slider to filter products in your store by price.', 'woocommerce' );
		$this->widget_id          = 'woocommerce_price_filter';
		$this->widget_name        = __( 'Filter Products by Price', 'woocommerce' );
		$this->settings           = array(
			'title' => array(
				'type'  => 'text',
				'std'   => __( 'Filter by price', 'woocommerce' ),
				'label' => __( 'Title', 'woocommerce' ),
			),
		);
		$suffix                   = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min';
		$version                  = Constants::get_constant( 'WC_VERSION' );
		wp_register_script( 'accounting', WC()->plugin_url() . '/assets/js/accounting/accounting' . $suffix . '.js', array( 'jquery' ), '0.4.2', true );
		wp_register_script( 'wc-jquery-ui-touchpunch', WC()->plugin_url() . '/assets/js/jquery-ui-touch-punch/jquery-ui-touch-punch' . $suffix . '.js', array( 'jquery-ui-slider' ), $version, true );
		wp_register_script( 'wc-price-slider', WC()->plugin_url() . '/assets/js/frontend/price-slider' . $suffix . '.js', array( 'jquery-ui-slider', 'wc-jquery-ui-touchpunch', 'accounting' ), $version, true );
		wp_localize_script(
			'wc-price-slider',
			'woocommerce_price_slider_params',
			array(
				'currency_format_num_decimals' => 0,
				'currency_format_symbol'       => get_woocommerce_currency_symbol(),
				'currency_format_decimal_sep'  => esc_attr( wc_get_price_decimal_separator() ),
				'currency_format_thousand_sep' => esc_attr( wc_get_price_thousand_separator() ),
				'currency_format'              => esc_attr( str_replace( array( '%1$s', '%2$s' ), array( '%s', '%v' ), get_woocommerce_price_format() ) ),
			)
		);

		if ( is_customize_preview() ) {
			wp_enqueue_script( 'wc-price-slider' );
		}

		parent::__construct();
	}

	/**
	 * Output widget.
	 *
	 * @see WP_Widget
	 *
	 * @param array $args     Arguments.
	 * @param array $instance Widget instance.
	 */
	public function widget( $args, $instance ) {
		global $wp;

		// Requires lookup table added in 3.6.
		if ( version_compare( get_option( 'woocommerce_db_version', null ), '3.6', '<' ) ) {
			return;
		}

		if ( ! is_shop() && ! is_product_taxonomy() ) {
			return;
		}

		// If there are not posts and we're not filtering, hide the widget.
		if ( ! WC()->query->get_main_query()->post_count && ! isset( $_GET['min_price'] ) && ! isset( $_GET['max_price'] ) ) { // WPCS: input var ok, CSRF ok.
			return;
		}

		wp_enqueue_script( 'wc-price-slider' );

		// Round values to nearest 10 by default.
		$step = max( apply_filters( 'woocommerce_price_filter_widget_step', 10 ), 1 );

		// Find min and max price in current result set.
		$prices    = $this->get_filtered_price();
		$min_price = $prices->min_price;
		$max_price = $prices->max_price;

		// Check to see if we should add taxes to the prices if store are excl tax but display incl.
		$tax_display_mode = get_option( 'woocommerce_tax_display_shop' );

		if ( wc_tax_enabled() && ! wc_prices_include_tax() && 'incl' === $tax_display_mode ) {
			$tax_class = apply_filters( 'woocommerce_price_filter_widget_tax_class', '' ); // Uses standard tax class.
			$tax_rates = WC_Tax::get_rates( $tax_class );

			if ( $tax_rates ) {
				$min_price += WC_Tax::get_tax_total( WC_Tax::calc_exclusive_tax( $min_price, $tax_rates ) );
				$max_price += WC_Tax::get_tax_total( WC_Tax::calc_exclusive_tax( $max_price, $tax_rates ) );
			}
		}

		$min_price = apply_filters( 'woocommerce_price_filter_widget_min_amount', floor( $min_price / $step ) * $step );
		$max_price = apply_filters( 'woocommerce_price_filter_widget_max_amount', ceil( $max_price / $step ) * $step );

		// If both min and max are equal, we don't need a slider.
		if ( $min_price === $max_price ) {
			return;
		}

		$current_min_price = isset( $_GET['min_price'] ) ? floor( floatval( wp_unslash( $_GET['min_price'] ) ) / $step ) * $step : $min_price; // WPCS: input var ok, CSRF ok.
		$current_max_price = isset( $_GET['max_price'] ) ? ceil( floatval( wp_unslash( $_GET['max_price'] ) ) / $step ) * $step : $max_price; // WPCS: input var ok, CSRF ok.

		$this->widget_start( $args, $instance );

		if ( '' === get_option( 'permalink_structure' ) ) {
			$form_action = remove_query_arg( array( 'page', 'paged', 'product-page' ), add_query_arg( $wp->query_string, '', home_url( $wp->request ) ) );
		} else {
			$form_action = preg_replace( '%\/page/[0-9]+%', '', home_url( trailingslashit( $wp->request ) ) );
		}

		wc_get_template(
			'content-widget-price-filter.php',
			array(
				'form_action'       => $form_action,
				'step'              => $step,
				'min_price'         => $min_price,
				'max_price'         => $max_price,
				'current_min_price' => $current_min_price,
				'current_max_price' => $current_max_price,
			)
		);

		$this->widget_end( $args );
	}

	/**
	 * Get filtered min price for current products.
	 *
	 * @return int
	 */
	protected function get_filtered_price() {
		global $wpdb;

		$args       = WC()->query->get_main_query()->query_vars;
		$tax_query  = isset( $args['tax_query'] ) ? $args['tax_query'] : array();
		$meta_query = isset( $args['meta_query'] ) ? $args['meta_query'] : array();

		if ( ! is_post_type_archive( 'product' ) && ! empty( $args['taxonomy'] ) && ! empty( $args['term'] ) ) {
			$tax_query[] = WC()->query->get_main_tax_query();
		}

		foreach ( $meta_query + $tax_query as $key => $query ) {
			if ( ! empty( $query['price_filter'] ) || ! empty( $query['rating_filter'] ) ) {
				unset( $meta_query[ $key ] );
			}
		}

		$meta_query = new WP_Meta_Query( $meta_query );
		$tax_query  = new WP_Tax_Query( $tax_query );
		$search     = WC_Query::get_main_search_query_sql();

		$meta_query_sql   = $meta_query->get_sql( 'post', $wpdb->posts, 'ID' );
		$tax_query_sql    = $tax_query->get_sql( $wpdb->posts, 'ID' );
		$search_query_sql = $search ? ' AND ' . $search : '';

		$sql = "
			SELECT min( min_price ) as min_price, MAX( max_price ) as max_price
			FROM {$wpdb->wc_product_meta_lookup}
			WHERE product_id IN (
				SELECT ID FROM {$wpdb->posts}
				" . $tax_query_sql['join'] . $meta_query_sql['join'] . "
				WHERE {$wpdb->posts}.post_type IN ('" . implode( "','", array_map( 'esc_sql', apply_filters( 'woocommerce_price_filter_post_type', array( 'product' ) ) ) ) . "')
				AND {$wpdb->posts}.post_status = 'publish'
				" . $tax_query_sql['where'] . $meta_query_sql['where'] . $search_query_sql . '
			)';

		$sql = apply_filters( 'woocommerce_price_filter_sql', $sql, $meta_query_sql, $tax_query_sql );

		return $wpdb->get_row( $sql ); // WPCS: unprepared SQL ok.
	}
}
widgets/class-wc-widget-layered-nav.php000064400000035053151336044770014145 0ustar00<?php
/**
 * Layered nav widget
 *
 * @package WooCommerce\Widgets
 * @version 2.6.0
 */

use Automattic\WooCommerce\Internal\ProductAttributesLookup\Filterer;

defined( 'ABSPATH' ) || exit;

/**
 * Widget layered nav class.
 */
class WC_Widget_Layered_Nav extends WC_Widget {

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->widget_cssclass    = 'woocommerce widget_layered_nav woocommerce-widget-layered-nav';
		$this->widget_description = __( 'Display a list of attributes to filter products in your store.', 'woocommerce' );
		$this->widget_id          = 'woocommerce_layered_nav';
		$this->widget_name        = __( 'Filter Products by Attribute', 'woocommerce' );
		parent::__construct();
	}

	/**
	 * Updates a particular instance of a widget.
	 *
	 * @see WP_Widget->update
	 *
	 * @param array $new_instance New Instance.
	 * @param array $old_instance Old Instance.
	 *
	 * @return array
	 */
	public function update( $new_instance, $old_instance ) {
		$this->init_settings();
		return parent::update( $new_instance, $old_instance );
	}

	/**
	 * Outputs the settings update form.
	 *
	 * @see WP_Widget->form
	 *
	 * @param array $instance Instance.
	 */
	public function form( $instance ) {
		$this->init_settings();
		parent::form( $instance );
	}

	/**
	 * Init settings after post types are registered.
	 */
	public function init_settings() {
		$attribute_array      = array();
		$std_attribute        = '';
		$attribute_taxonomies = wc_get_attribute_taxonomies();

		if ( ! empty( $attribute_taxonomies ) ) {
			foreach ( $attribute_taxonomies as $tax ) {
				if ( taxonomy_exists( wc_attribute_taxonomy_name( $tax->attribute_name ) ) ) {
					$attribute_array[ $tax->attribute_name ] = $tax->attribute_name;
				}
			}
			$std_attribute = current( $attribute_array );
		}

		$this->settings = array(
			'title'        => array(
				'type'  => 'text',
				'std'   => __( 'Filter by', 'woocommerce' ),
				'label' => __( 'Title', 'woocommerce' ),
			),
			'attribute'    => array(
				'type'    => 'select',
				'std'     => $std_attribute,
				'label'   => __( 'Attribute', 'woocommerce' ),
				'options' => $attribute_array,
			),
			'display_type' => array(
				'type'    => 'select',
				'std'     => 'list',
				'label'   => __( 'Display type', 'woocommerce' ),
				'options' => array(
					'list'     => __( 'List', 'woocommerce' ),
					'dropdown' => __( 'Dropdown', 'woocommerce' ),
				),
			),
			'query_type'   => array(
				'type'    => 'select',
				'std'     => 'and',
				'label'   => __( 'Query type', 'woocommerce' ),
				'options' => array(
					'and' => __( 'AND', 'woocommerce' ),
					'or'  => __( 'OR', 'woocommerce' ),
				),
			),
		);
	}

	/**
	 * Get this widgets taxonomy.
	 *
	 * @param array $instance Array of instance options.
	 * @return string
	 */
	protected function get_instance_taxonomy( $instance ) {
		if ( isset( $instance['attribute'] ) ) {
			return wc_attribute_taxonomy_name( $instance['attribute'] );
		}

		$attribute_taxonomies = wc_get_attribute_taxonomies();

		if ( ! empty( $attribute_taxonomies ) ) {
			foreach ( $attribute_taxonomies as $tax ) {
				if ( taxonomy_exists( wc_attribute_taxonomy_name( $tax->attribute_name ) ) ) {
					return wc_attribute_taxonomy_name( $tax->attribute_name );
				}
			}
		}

		return '';
	}

	/**
	 * Get this widgets query type.
	 *
	 * @param array $instance Array of instance options.
	 * @return string
	 */
	protected function get_instance_query_type( $instance ) {
		return isset( $instance['query_type'] ) ? $instance['query_type'] : 'and';
	}

	/**
	 * Get this widgets display type.
	 *
	 * @param array $instance Array of instance options.
	 * @return string
	 */
	protected function get_instance_display_type( $instance ) {
		return isset( $instance['display_type'] ) ? $instance['display_type'] : 'list';
	}

	/**
	 * Output widget.
	 *
	 * @see WP_Widget
	 *
	 * @param array $args Arguments.
	 * @param array $instance Instance.
	 */
	public function widget( $args, $instance ) {
		if ( ! is_shop() && ! is_product_taxonomy() ) {
			return;
		}

		$_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes();
		$taxonomy           = $this->get_instance_taxonomy( $instance );
		$query_type         = $this->get_instance_query_type( $instance );
		$display_type       = $this->get_instance_display_type( $instance );

		if ( ! taxonomy_exists( $taxonomy ) ) {
			return;
		}

		$terms = get_terms( $taxonomy, array( 'hide_empty' => '1' ) );

		if ( 0 === count( $terms ) ) {
			return;
		}

		ob_start();

		$this->widget_start( $args, $instance );

		if ( 'dropdown' === $display_type ) {
			wp_enqueue_script( 'selectWoo' );
			wp_enqueue_style( 'select2' );
			$found = $this->layered_nav_dropdown( $terms, $taxonomy, $query_type );
		} else {
			$found = $this->layered_nav_list( $terms, $taxonomy, $query_type );
		}

		$this->widget_end( $args );

		// Force found when option is selected - do not force found on taxonomy attributes.
		if ( ! is_tax() && is_array( $_chosen_attributes ) && array_key_exists( $taxonomy, $_chosen_attributes ) ) {
			$found = true;
		}

		if ( ! $found ) {
			ob_end_clean();
		} else {
			echo ob_get_clean(); // @codingStandardsIgnoreLine
		}
	}

	/**
	 * Return the currently viewed taxonomy name.
	 *
	 * @return string
	 */
	protected function get_current_taxonomy() {
		return is_tax() ? get_queried_object()->taxonomy : '';
	}

	/**
	 * Return the currently viewed term ID.
	 *
	 * @return int
	 */
	protected function get_current_term_id() {
		return absint( is_tax() ? get_queried_object()->term_id : 0 );
	}

	/**
	 * Return the currently viewed term slug.
	 *
	 * @return int
	 */
	protected function get_current_term_slug() {
		return absint( is_tax() ? get_queried_object()->slug : 0 );
	}

	/**
	 * Show dropdown layered nav.
	 *
	 * @param  array  $terms Terms.
	 * @param  string $taxonomy Taxonomy.
	 * @param  string $query_type Query Type.
	 * @return bool Will nav display?
	 */
	protected function layered_nav_dropdown( $terms, $taxonomy, $query_type ) {
		global $wp;
		$found = false;

		if ( $taxonomy !== $this->get_current_taxonomy() ) {
			$term_counts          = $this->get_filtered_term_product_counts( wp_list_pluck( $terms, 'term_id' ), $taxonomy, $query_type );
			$_chosen_attributes   = WC_Query::get_layered_nav_chosen_attributes();
			$taxonomy_filter_name = wc_attribute_taxonomy_slug( $taxonomy );
			$taxonomy_label       = wc_attribute_label( $taxonomy );

			/* translators: %s: taxonomy name */
			$any_label      = apply_filters( 'woocommerce_layered_nav_any_label', sprintf( __( 'Any %s', 'woocommerce' ), $taxonomy_label ), $taxonomy_label, $taxonomy );
			$multiple       = 'or' === $query_type;
			$current_values = isset( $_chosen_attributes[ $taxonomy ]['terms'] ) ? $_chosen_attributes[ $taxonomy ]['terms'] : array();

			if ( '' === get_option( 'permalink_structure' ) ) {
				$form_action = remove_query_arg( array( 'page', 'paged' ), add_query_arg( $wp->query_string, '', home_url( $wp->request ) ) );
			} else {
				$form_action = preg_replace( '%\/page/[0-9]+%', '', home_url( user_trailingslashit( $wp->request ) ) );
			}

			echo '<form method="get" action="' . esc_url( $form_action ) . '" class="woocommerce-widget-layered-nav-dropdown">';
			echo '<select class="woocommerce-widget-layered-nav-dropdown dropdown_layered_nav_' . esc_attr( $taxonomy_filter_name ) . '"' . ( $multiple ? 'multiple="multiple"' : '' ) . '>';
			echo '<option value="">' . esc_html( $any_label ) . '</option>';

			foreach ( $terms as $term ) {

				// If on a term page, skip that term in widget list.
				if ( $term->term_id === $this->get_current_term_id() ) {
					continue;
				}

				// Get count based on current view.
				$option_is_set = in_array( $term->slug, $current_values, true );
				$count         = isset( $term_counts[ $term->term_id ] ) ? $term_counts[ $term->term_id ] : 0;

				// Only show options with count > 0.
				if ( 0 < $count ) {
					$found = true;
				} elseif ( 0 === $count && ! $option_is_set ) {
					continue;
				}

				echo '<option value="' . esc_attr( urldecode( $term->slug ) ) . '" ' . selected( $option_is_set, true, false ) . '>' . esc_html( $term->name ) . '</option>';
			}

			echo '</select>';

			if ( $multiple ) {
				echo '<button class="woocommerce-widget-layered-nav-dropdown__submit" type="submit" value="' . esc_attr__( 'Apply', 'woocommerce' ) . '">' . esc_html__( 'Apply', 'woocommerce' ) . '</button>';
			}

			if ( 'or' === $query_type ) {
				echo '<input type="hidden" name="query_type_' . esc_attr( $taxonomy_filter_name ) . '" value="or" />';
			}

			echo '<input type="hidden" name="filter_' . esc_attr( $taxonomy_filter_name ) . '" value="' . esc_attr( implode( ',', $current_values ) ) . '" />';
			echo wc_query_string_form_fields( null, array( 'filter_' . $taxonomy_filter_name, 'query_type_' . $taxonomy_filter_name ), '', true ); // @codingStandardsIgnoreLine
			echo '</form>';

			wc_enqueue_js(
				"
				// Update value on change.
				jQuery( '.dropdown_layered_nav_" . esc_js( $taxonomy_filter_name ) . "' ).on( 'change', function() {
					var slug = jQuery( this ).val();
					jQuery( ':input[name=\"filter_" . esc_js( $taxonomy_filter_name ) . "\"]' ).val( slug );

					// Submit form on change if standard dropdown.
					if ( ! jQuery( this ).attr( 'multiple' ) ) {
						jQuery( this ).closest( 'form' ).trigger( 'submit' );
					}
				});

				// Use Select2 enhancement if possible
				if ( jQuery().selectWoo ) {
					var wc_layered_nav_select = function() {
						jQuery( '.dropdown_layered_nav_" . esc_js( $taxonomy_filter_name ) . "' ).selectWoo( {
							placeholder: decodeURIComponent('" . rawurlencode( (string) wp_specialchars_decode( $any_label ) ) . "'),
							minimumResultsForSearch: 5,
							width: '100%',
							allowClear: " . ( $multiple ? 'false' : 'true' ) . ",
							language: {
								noResults: function() {
									return '" . esc_js( _x( 'No matches found', 'enhanced select', 'woocommerce' ) ) . "';
								}
							}
						} );
					};
					wc_layered_nav_select();
				}
			"
			);
		}

		return $found;
	}

	/**
	 * Count products within certain terms, taking the main WP query into consideration.
	 *
	 * This query allows counts to be generated based on the viewed products, not all products.
	 *
	 * @param  array  $term_ids Term IDs.
	 * @param  string $taxonomy Taxonomy.
	 * @param  string $query_type Query Type.
	 * @return array
	 */
	protected function get_filtered_term_product_counts( $term_ids, $taxonomy, $query_type ) {
		return wc_get_container()->get( Filterer::class )->get_filtered_term_product_counts( $term_ids, $taxonomy, $query_type );
	}

	/**
	 * Wrapper for WC_Query::get_main_tax_query() to ease unit testing.
	 *
	 * @since 4.4.0
	 * @return array
	 */
	protected function get_main_tax_query() {
		return WC_Query::get_main_tax_query();
	}

	/**
	 * Wrapper for WC_Query::get_main_search_query_sql() to ease unit testing.
	 *
	 * @since 4.4.0
	 * @return string
	 */
	protected function get_main_search_query_sql() {
		return WC_Query::get_main_search_query_sql();
	}

	/**
	 * Wrapper for WC_Query::get_main_search_queryget_main_meta_query to ease unit testing.
	 *
	 * @since 4.4.0
	 * @return array
	 */
	protected function get_main_meta_query() {
		return WC_Query::get_main_meta_query();
	}

	/**
	 * Show list based layered nav.
	 *
	 * @param  array  $terms Terms.
	 * @param  string $taxonomy Taxonomy.
	 * @param  string $query_type Query Type.
	 * @return bool   Will nav display?
	 */
	protected function layered_nav_list( $terms, $taxonomy, $query_type ) {
		// List display.
		echo '<ul class="woocommerce-widget-layered-nav-list">';

		$term_counts        = $this->get_filtered_term_product_counts( wp_list_pluck( $terms, 'term_id' ), $taxonomy, $query_type );
		$_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes();
		$found              = false;
		$base_link          = $this->get_current_page_url();

		foreach ( $terms as $term ) {
			$current_values = isset( $_chosen_attributes[ $taxonomy ]['terms'] ) ? $_chosen_attributes[ $taxonomy ]['terms'] : array();
			$option_is_set  = in_array( $term->slug, $current_values, true );
			$count          = isset( $term_counts[ $term->term_id ] ) ? $term_counts[ $term->term_id ] : 0;

			// Skip the term for the current archive.
			if ( $this->get_current_term_id() === $term->term_id ) {
				continue;
			}

			// Only show options with count > 0.
			if ( 0 < $count ) {
				$found = true;
			} elseif ( 0 === $count && ! $option_is_set ) {
				continue;
			}

			$filter_name = 'filter_' . wc_attribute_taxonomy_slug( $taxonomy );
			// phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$current_filter = isset( $_GET[ $filter_name ] ) ? explode( ',', wc_clean( wp_unslash( $_GET[ $filter_name ] ) ) ) : array();
			$current_filter = array_map( 'sanitize_title', $current_filter );

			if ( ! in_array( $term->slug, $current_filter, true ) ) {
				$current_filter[] = $term->slug;
			}

			$link = remove_query_arg( $filter_name, $base_link );

			// Add current filters to URL.
			foreach ( $current_filter as $key => $value ) {
				// Exclude query arg for current term archive term.
				if ( $value === $this->get_current_term_slug() ) {
					unset( $current_filter[ $key ] );
				}

				// Exclude self so filter can be unset on click.
				if ( $option_is_set && $value === $term->slug ) {
					unset( $current_filter[ $key ] );
				}
			}

			if ( ! empty( $current_filter ) ) {
				asort( $current_filter );
				$link = add_query_arg( $filter_name, implode( ',', $current_filter ), $link );

				// Add Query type Arg to URL.
				if ( 'or' === $query_type && ! ( 1 === count( $current_filter ) && $option_is_set ) ) {
					$link = add_query_arg( 'query_type_' . wc_attribute_taxonomy_slug( $taxonomy ), 'or', $link );
				}
				$link = str_replace( '%2C', ',', $link );
			}

			if ( $count > 0 || $option_is_set ) {
				$link      = apply_filters( 'woocommerce_layered_nav_link', $link, $term, $taxonomy );
				$term_html = '<a rel="nofollow" href="' . esc_url( $link ) . '">' . esc_html( $term->name ) . '</a>';
			} else {
				$link      = false;
				$term_html = '<span>' . esc_html( $term->name ) . '</span>';
			}

			$term_html .= ' ' . apply_filters( 'woocommerce_layered_nav_count', '<span class="count">(' . absint( $count ) . ')</span>', $count, $term );

			echo '<li class="woocommerce-widget-layered-nav-list__item wc-layered-nav-term ' . ( $option_is_set ? 'woocommerce-widget-layered-nav-list__item--chosen chosen' : '' ) . '">';
			// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.EscapeOutput.OutputNotEscaped
			echo apply_filters( 'woocommerce_layered_nav_term_html', $term_html, $term, $link, $count );
			echo '</li>';
		}

		echo '</ul>';

		return $found;
	}
}
widgets/class-wc-widget-product-categories.php000064400000021072151336044770015535 0ustar00<?php
/**
 * Product Categories Widget
 *
 * @package WooCommerce\Widgets
 * @version 2.3.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Product categories widget class.
 *
 * @extends WC_Widget
 */
class WC_Widget_Product_Categories extends WC_Widget {

	/**
	 * Category ancestors.
	 *
	 * @var array
	 */
	public $cat_ancestors;

	/**
	 * Current Category.
	 *
	 * @var bool
	 */
	public $current_cat;

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->widget_cssclass    = 'woocommerce widget_product_categories';
		$this->widget_description = __( 'A list or dropdown of product categories.', 'woocommerce' );
		$this->widget_id          = 'woocommerce_product_categories';
		$this->widget_name        = __( 'Product Categories', 'woocommerce' );
		$this->settings           = array(
			'title'              => array(
				'type'  => 'text',
				'std'   => __( 'Product categories', 'woocommerce' ),
				'label' => __( 'Title', 'woocommerce' ),
			),
			'orderby'            => array(
				'type'    => 'select',
				'std'     => 'name',
				'label'   => __( 'Order by', 'woocommerce' ),
				'options' => array(
					'order' => __( 'Category order', 'woocommerce' ),
					'name'  => __( 'Name', 'woocommerce' ),
				),
			),
			'dropdown'           => array(
				'type'  => 'checkbox',
				'std'   => 0,
				'label' => __( 'Show as dropdown', 'woocommerce' ),
			),
			'count'              => array(
				'type'  => 'checkbox',
				'std'   => 0,
				'label' => __( 'Show product counts', 'woocommerce' ),
			),
			'hierarchical'       => array(
				'type'  => 'checkbox',
				'std'   => 1,
				'label' => __( 'Show hierarchy', 'woocommerce' ),
			),
			'show_children_only' => array(
				'type'  => 'checkbox',
				'std'   => 0,
				'label' => __( 'Only show children of the current category', 'woocommerce' ),
			),
			'hide_empty'         => array(
				'type'  => 'checkbox',
				'std'   => 0,
				'label' => __( 'Hide empty categories', 'woocommerce' ),
			),
			'max_depth'          => array(
				'type'  => 'text',
				'std'   => '',
				'label' => __( 'Maximum depth', 'woocommerce' ),
			),
		);

		parent::__construct();
	}

	/**
	 * Output widget.
	 *
	 * @see WP_Widget
	 * @param array $args     Widget arguments.
	 * @param array $instance Widget instance.
	 */
	public function widget( $args, $instance ) {
		global $wp_query, $post;

		$count              = isset( $instance['count'] ) ? $instance['count'] : $this->settings['count']['std'];
		$hierarchical       = isset( $instance['hierarchical'] ) ? $instance['hierarchical'] : $this->settings['hierarchical']['std'];
		$show_children_only = isset( $instance['show_children_only'] ) ? $instance['show_children_only'] : $this->settings['show_children_only']['std'];
		$dropdown           = isset( $instance['dropdown'] ) ? $instance['dropdown'] : $this->settings['dropdown']['std'];
		$orderby            = isset( $instance['orderby'] ) ? $instance['orderby'] : $this->settings['orderby']['std'];
		$hide_empty         = isset( $instance['hide_empty'] ) ? $instance['hide_empty'] : $this->settings['hide_empty']['std'];
		$dropdown_args      = array(
			'hide_empty' => $hide_empty,
		);
		$list_args          = array(
			'show_count'   => $count,
			'hierarchical' => $hierarchical,
			'taxonomy'     => 'product_cat',
			'hide_empty'   => $hide_empty,
		);
		$max_depth          = absint( isset( $instance['max_depth'] ) ? $instance['max_depth'] : $this->settings['max_depth']['std'] );

		$list_args['menu_order'] = false;
		$dropdown_args['depth']  = $max_depth;
		$list_args['depth']      = $max_depth;

		if ( 'order' === $orderby ) {
			$list_args['orderby']      = 'meta_value_num';
			$dropdown_args['orderby']  = 'meta_value_num';
			$list_args['meta_key']     = 'order';
			$dropdown_args['meta_key'] = 'order';
		}

		$this->current_cat   = false;
		$this->cat_ancestors = array();

		if ( is_tax( 'product_cat' ) ) {
			$this->current_cat   = $wp_query->queried_object;
			$this->cat_ancestors = get_ancestors( $this->current_cat->term_id, 'product_cat' );

		} elseif ( is_singular( 'product' ) ) {
			$terms = wc_get_product_terms(
				$post->ID,
				'product_cat',
				apply_filters(
					'woocommerce_product_categories_widget_product_terms_args',
					array(
						'orderby' => 'parent',
						'order'   => 'DESC',
					)
				)
			);

			if ( $terms ) {
				$main_term           = apply_filters( 'woocommerce_product_categories_widget_main_term', $terms[0], $terms );
				$this->current_cat   = $main_term;
				$this->cat_ancestors = get_ancestors( $main_term->term_id, 'product_cat' );
			}
		}

		// Show Siblings and Children Only.
		if ( $show_children_only && $this->current_cat ) {
			if ( $hierarchical ) {
				$include = array_merge(
					$this->cat_ancestors,
					array( $this->current_cat->term_id ),
					get_terms(
						'product_cat',
						array(
							'fields'       => 'ids',
							'parent'       => 0,
							'hierarchical' => true,
							'hide_empty'   => false,
						)
					),
					get_terms(
						'product_cat',
						array(
							'fields'       => 'ids',
							'parent'       => $this->current_cat->term_id,
							'hierarchical' => true,
							'hide_empty'   => false,
						)
					)
				);
				// Gather siblings of ancestors.
				if ( $this->cat_ancestors ) {
					foreach ( $this->cat_ancestors as $ancestor ) {
						$include = array_merge(
							$include,
							get_terms(
								'product_cat',
								array(
									'fields'       => 'ids',
									'parent'       => $ancestor,
									'hierarchical' => false,
									'hide_empty'   => false,
								)
							)
						);
					}
				}
			} else {
				// Direct children.
				$include = get_terms(
					'product_cat',
					array(
						'fields'       => 'ids',
						'parent'       => $this->current_cat->term_id,
						'hierarchical' => true,
						'hide_empty'   => false,
					)
				);
			}

			$list_args['include']     = implode( ',', $include );
			$dropdown_args['include'] = $list_args['include'];

			if ( empty( $include ) ) {
				return;
			}
		} elseif ( $show_children_only ) {
			$dropdown_args['depth']        = 1;
			$dropdown_args['child_of']     = 0;
			$dropdown_args['hierarchical'] = 1;
			$list_args['depth']            = 1;
			$list_args['child_of']         = 0;
			$list_args['hierarchical']     = 1;
		}

		$this->widget_start( $args, $instance );

		if ( $dropdown ) {
			wc_product_dropdown_categories(
				apply_filters(
					'woocommerce_product_categories_widget_dropdown_args',
					wp_parse_args(
						$dropdown_args,
						array(
							'show_count'         => $count,
							'hierarchical'       => $hierarchical,
							'show_uncategorized' => 0,
							'selected'           => $this->current_cat ? $this->current_cat->slug : '',
						)
					)
				)
			);

			wp_enqueue_script( 'selectWoo' );
			wp_enqueue_style( 'select2' );

			wc_enqueue_js(
				"
				jQuery( '.dropdown_product_cat' ).on( 'change', function() {
					if ( jQuery(this).val() != '' ) {
						var this_page = '';
						var home_url  = '" . esc_js( home_url( '/' ) ) . "';
						if ( home_url.indexOf( '?' ) > 0 ) {
							this_page = home_url + '&product_cat=' + jQuery(this).val();
						} else {
							this_page = home_url + '?product_cat=' + jQuery(this).val();
						}
						location.href = this_page;
					} else {
						location.href = '" . esc_js( wc_get_page_permalink( 'shop' ) ) . "';
					}
				});

				if ( jQuery().selectWoo ) {
					var wc_product_cat_select = function() {
						jQuery( '.dropdown_product_cat' ).selectWoo( {
							placeholder: '" . esc_js( __( 'Select a category', 'woocommerce' ) ) . "',
							minimumResultsForSearch: 5,
							width: '100%',
							allowClear: true,
							language: {
								noResults: function() {
									return '" . esc_js( _x( 'No matches found', 'enhanced select', 'woocommerce' ) ) . "';
								}
							}
						} );
					};
					wc_product_cat_select();
				}
			"
			);
		} else {
			include_once WC()->plugin_path() . '/includes/walkers/class-wc-product-cat-list-walker.php';

			$list_args['walker']                     = new WC_Product_Cat_List_Walker();
			$list_args['title_li']                   = '';
			$list_args['pad_counts']                 = 1;
			$list_args['show_option_none']           = __( 'No product categories exist.', 'woocommerce' );
			$list_args['current_category']           = ( $this->current_cat ) ? $this->current_cat->term_id : '';
			$list_args['current_category_ancestors'] = $this->cat_ancestors;
			$list_args['max_depth']                  = $max_depth;

			echo '<ul class="product-categories">';

			wp_list_categories( apply_filters( 'woocommerce_product_categories_widget_args', $list_args ) );

			echo '</ul>';
		}

		$this->widget_end( $args );
	}
}
widgets/class-wc-widget-rating-filter.php000064400000010545151336044770014504 0ustar00<?php
/**
 * Rating Filter Widget and related functions.
 *
 * @package WooCommerce\Widgets
 * @version 2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Widget rating filter class.
 */
class WC_Widget_Rating_Filter extends WC_Widget {

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->widget_cssclass    = 'woocommerce widget_rating_filter';
		$this->widget_description = __( 'Display a list of star ratings to filter products in your store.', 'woocommerce' );
		$this->widget_id          = 'woocommerce_rating_filter';
		$this->widget_name        = __( 'Filter Products by Rating', 'woocommerce' );
		$this->settings           = array(
			'title' => array(
				'type'  => 'text',
				'std'   => __( 'Average rating', 'woocommerce' ),
				'label' => __( 'Title', 'woocommerce' ),
			),
		);
		parent::__construct();
	}

	/**
	 * Count products after other filters have occurred by adjusting the main query.
	 *
	 * @param  int $rating Rating.
	 * @return int
	 */
	protected function get_filtered_product_count( $rating ) {
		global $wpdb;

		$tax_query  = WC_Query::get_main_tax_query();
		$meta_query = WC_Query::get_main_meta_query();

		// Unset current rating filter.
		foreach ( $tax_query as $key => $query ) {
			if ( ! empty( $query['rating_filter'] ) ) {
				unset( $tax_query[ $key ] );
				break;
			}
		}

		// Set new rating filter.
		$product_visibility_terms = wc_get_product_visibility_term_ids();
		$tax_query[]              = array(
			'taxonomy'      => 'product_visibility',
			'field'         => 'term_taxonomy_id',
			'terms'         => $product_visibility_terms[ 'rated-' . $rating ],
			'operator'      => 'IN',
			'rating_filter' => true,
		);

		$meta_query     = new WP_Meta_Query( $meta_query );
		$tax_query      = new WP_Tax_Query( $tax_query );
		$meta_query_sql = $meta_query->get_sql( 'post', $wpdb->posts, 'ID' );
		$tax_query_sql  = $tax_query->get_sql( $wpdb->posts, 'ID' );

		$sql  = "SELECT COUNT( DISTINCT {$wpdb->posts}.ID ) FROM {$wpdb->posts} ";
		$sql .= $tax_query_sql['join'] . $meta_query_sql['join'];
		$sql .= " WHERE {$wpdb->posts}.post_type = 'product' AND {$wpdb->posts}.post_status = 'publish' ";
		$sql .= $tax_query_sql['where'] . $meta_query_sql['where'];

		$search = WC_Query::get_main_search_query_sql();
		if ( $search ) {
			$sql .= ' AND ' . $search;
		}

		return absint( $wpdb->get_var( $sql ) ); // WPCS: unprepared SQL ok.
	}

	/**
	 * Widget function.
	 *
	 * @see WP_Widget
	 * @param array $args     Arguments.
	 * @param array $instance Widget instance.
	 */
	public function widget( $args, $instance ) {
		if ( ! is_shop() && ! is_product_taxonomy() ) {
			return;
		}

		if ( ! WC()->query->get_main_query()->post_count ) {
			return;
		}

		ob_start();

		$found         = false;
		$rating_filter = isset( $_GET['rating_filter'] ) ? array_filter( array_map( 'absint', explode( ',', wp_unslash( $_GET['rating_filter'] ) ) ) ) : array(); // WPCS: input var ok, CSRF ok, sanitization ok.
		$base_link     = remove_query_arg( 'paged', $this->get_current_page_url() );

		$this->widget_start( $args, $instance );

		echo '<ul>';

		for ( $rating = 5; $rating >= 1; $rating-- ) {
			$count = $this->get_filtered_product_count( $rating );
			if ( empty( $count ) ) {
				continue;
			}
			$found = true;
			$link  = $base_link;

			if ( in_array( $rating, $rating_filter, true ) ) {
				$link_ratings = implode( ',', array_diff( $rating_filter, array( $rating ) ) );
			} else {
				$link_ratings = implode( ',', array_merge( $rating_filter, array( $rating ) ) );
			}

			$class       = in_array( $rating, $rating_filter, true ) ? 'wc-layered-nav-rating chosen' : 'wc-layered-nav-rating';
			$link        = apply_filters( 'woocommerce_rating_filter_link', $link_ratings ? add_query_arg( 'rating_filter', $link_ratings, $link ) : remove_query_arg( 'rating_filter' ) );
			$rating_html = wc_get_star_rating_html( $rating );
			$count_html  = wp_kses(
				apply_filters( 'woocommerce_rating_filter_count', "({$count})", $count, $rating ),
				array(
					'em'     => array(),
					'span'   => array(),
					'strong' => array(),
				)
			);

			printf( '<li class="%s"><a href="%s"><span class="star-rating">%s</span> %s</a></li>', esc_attr( $class ), esc_url( $link ), $rating_html, $count_html ); // WPCS: XSS ok.
		}

		echo '</ul>';

		$this->widget_end( $args );

		if ( ! $found ) {
			ob_end_clean();
		} else {
			echo ob_get_clean(); // WPCS: XSS ok.
		}
	}
}
widgets/class-wc-widget-layered-nav-filters.php000064400000010765151336044770015616 0ustar00<?php
/**
 * Layered Navigation Filters Widget.
 *
 * @package WooCommerce\Widgets
 * @version 2.3.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Widget layered nav filters.
 */
class WC_Widget_Layered_Nav_Filters extends WC_Widget {

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->widget_cssclass    = 'woocommerce widget_layered_nav_filters';
		$this->widget_description = __( 'Display a list of active product filters.', 'woocommerce' );
		$this->widget_id          = 'woocommerce_layered_nav_filters';
		$this->widget_name        = __( 'Active Product Filters', 'woocommerce' );
		$this->settings           = array(
			'title' => array(
				'type'  => 'text',
				'std'   => __( 'Active filters', 'woocommerce' ),
				'label' => __( 'Title', 'woocommerce' ),
			),
		);

		parent::__construct();
	}

	/**
	 * Output widget.
	 *
	 * @see WP_Widget
	 * @param array $args     Arguments.
	 * @param array $instance Widget instance.
	 */
	public function widget( $args, $instance ) {
		if ( ! is_shop() && ! is_product_taxonomy() ) {
			return;
		}

		$_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes();
		$min_price          = isset( $_GET['min_price'] ) ? wc_clean( wp_unslash( $_GET['min_price'] ) ) : 0; // WPCS: input var ok, CSRF ok.
		$max_price          = isset( $_GET['max_price'] ) ? wc_clean( wp_unslash( $_GET['max_price'] ) ) : 0; // WPCS: input var ok, CSRF ok.
		$rating_filter      = isset( $_GET['rating_filter'] ) ? array_filter( array_map( 'absint', explode( ',', wp_unslash( $_GET['rating_filter'] ) ) ) ) : array(); // WPCS: sanitization ok, input var ok, CSRF ok.
		$base_link          = $this->get_current_page_url();

		if ( 0 < count( $_chosen_attributes ) || 0 < $min_price || 0 < $max_price || ! empty( $rating_filter ) ) {

			$this->widget_start( $args, $instance );

			echo '<ul>';

			// Attributes.
			if ( ! empty( $_chosen_attributes ) ) {
				foreach ( $_chosen_attributes as $taxonomy => $data ) {
					foreach ( $data['terms'] as $term_slug ) {
						$term = get_term_by( 'slug', $term_slug, $taxonomy );
						if ( ! $term ) {
							continue;
						}

						$filter_name    = 'filter_' . wc_attribute_taxonomy_slug( $taxonomy );
						$current_filter = isset( $_GET[ $filter_name ] ) ? explode( ',', wc_clean( wp_unslash( $_GET[ $filter_name ] ) ) ) : array(); // WPCS: input var ok, CSRF ok.
						$current_filter = array_map( 'sanitize_title', $current_filter );
						$new_filter     = array_diff( $current_filter, array( $term_slug ) );

						$link = remove_query_arg( array( 'add-to-cart', $filter_name ), $base_link );

						if ( count( $new_filter ) > 0 ) {
							$link = add_query_arg( $filter_name, implode( ',', $new_filter ), $link );
						}

						$filter_classes = array( 'chosen', 'chosen-' . sanitize_html_class( str_replace( 'pa_', '', $taxonomy ) ), 'chosen-' . sanitize_html_class( str_replace( 'pa_', '', $taxonomy ) . '-' . $term_slug ) );

						echo '<li class="' . esc_attr( implode( ' ', $filter_classes ) ) . '"><a rel="nofollow" aria-label="' . esc_attr__( 'Remove filter', 'woocommerce' ) . '" href="' . esc_url( $link ) . '">' . esc_html( $term->name ) . '</a></li>';
					}
				}
			}

			if ( $min_price ) {
				$link = remove_query_arg( 'min_price', $base_link );
				/* translators: %s: minimum price */
				echo '<li class="chosen"><a rel="nofollow" aria-label="' . esc_attr__( 'Remove filter', 'woocommerce' ) . '" href="' . esc_url( $link ) . '">' . sprintf( __( 'Min %s', 'woocommerce' ), wc_price( $min_price ) ) . '</a></li>'; // WPCS: XSS ok.
			}

			if ( $max_price ) {
				$link = remove_query_arg( 'max_price', $base_link );
				/* translators: %s: maximum price */
				echo '<li class="chosen"><a rel="nofollow" aria-label="' . esc_attr__( 'Remove filter', 'woocommerce' ) . '" href="' . esc_url( $link ) . '">' . sprintf( __( 'Max %s', 'woocommerce' ), wc_price( $max_price ) ) . '</a></li>'; // WPCS: XSS ok.
			}

			if ( ! empty( $rating_filter ) ) {
				foreach ( $rating_filter as $rating ) {
					$link_ratings = implode( ',', array_diff( $rating_filter, array( $rating ) ) );
					$link         = $link_ratings ? add_query_arg( 'rating_filter', $link_ratings ) : remove_query_arg( 'rating_filter', $base_link );

					/* translators: %s: rating */
					echo '<li class="chosen"><a rel="nofollow" aria-label="' . esc_attr__( 'Remove filter', 'woocommerce' ) . '" href="' . esc_url( $link ) . '">' . sprintf( esc_html__( 'Rated %s out of 5', 'woocommerce' ), esc_html( $rating ) ) . '</a></li>';
				}
			}

			echo '</ul>';

			$this->widget_end( $args );
		}
	}
}
widgets/class-wc-widget-cart.php000064400000003443151336044770012665 0ustar00<?php
/**
 * Shopping Cart Widget.
 *
 * Displays shopping cart widget.
 *
 * @package WooCommerce\Widgets
 * @version 2.3.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Widget cart class.
 */
class WC_Widget_Cart extends WC_Widget {

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->widget_cssclass    = 'woocommerce widget_shopping_cart';
		$this->widget_description = __( 'Display the customer shopping cart.', 'woocommerce' );
		$this->widget_id          = 'woocommerce_widget_cart';
		$this->widget_name        = __( 'Cart', 'woocommerce' );
		$this->settings           = array(
			'title'         => array(
				'type'  => 'text',
				'std'   => __( 'Cart', 'woocommerce' ),
				'label' => __( 'Title', 'woocommerce' ),
			),
			'hide_if_empty' => array(
				'type'  => 'checkbox',
				'std'   => 0,
				'label' => __( 'Hide if cart is empty', 'woocommerce' ),
			),
		);

		if ( is_customize_preview() ) {
			wp_enqueue_script( 'wc-cart-fragments' );
		}

		parent::__construct();
	}

	/**
	 * Output widget.
	 *
	 * @see WP_Widget
	 *
	 * @param array $args     Arguments.
	 * @param array $instance Widget instance.
	 */
	public function widget( $args, $instance ) {
		if ( apply_filters( 'woocommerce_widget_cart_is_hidden', is_cart() || is_checkout() ) ) {
			return;
		}

		$hide_if_empty = empty( $instance['hide_if_empty'] ) ? 0 : 1;

		if ( ! isset( $instance['title'] ) ) {
			$instance['title'] = __( 'Cart', 'woocommerce' );
		}

		$this->widget_start( $args, $instance );

		if ( $hide_if_empty ) {
			echo '<div class="hide_cart_widget_if_empty">';
		}

		// Insert cart widget placeholder - code in woocommerce.js will update this on page load.
		echo '<div class="widget_shopping_cart_content"></div>';

		if ( $hide_if_empty ) {
			echo '</div>';
		}

		$this->widget_end( $args );
	}
}
widgets/class-wc-widget-top-rated-products.php000064400000005221151336044770015470 0ustar00<?php
/**
 * Top Rated Products Widget.
 * Gets and displays top rated products in an unordered list.
 *
 * @package WooCommerce\Widgets
 * @version 3.3.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Widget top rated products class.
 */
class WC_Widget_Top_Rated_Products extends WC_Widget {

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->widget_cssclass    = 'woocommerce widget_top_rated_products';
		$this->widget_description = __( "A list of your store's top-rated products.", 'woocommerce' );
		$this->widget_id          = 'woocommerce_top_rated_products';
		$this->widget_name        = __( 'Products by Rating list', 'woocommerce' );
		$this->settings           = array(
			'title'  => array(
				'type'  => 'text',
				'std'   => __( 'Top rated products', 'woocommerce' ),
				'label' => __( 'Title', 'woocommerce' ),
			),
			'number' => array(
				'type'  => 'number',
				'step'  => 1,
				'min'   => 1,
				'max'   => '',
				'std'   => 5,
				'label' => __( 'Number of products to show', 'woocommerce' ),
			),
		);

		parent::__construct();
	}

	/**
	 * Output widget.
	 *
	 * @see WP_Widget
	 * @param array $args     Arguments.
	 * @param array $instance Widget instance.
	 */
	public function widget( $args, $instance ) {

		if ( $this->get_cached_widget( $args ) ) {
			return;
		}

		ob_start();

		$number = ! empty( $instance['number'] ) ? absint( $instance['number'] ) : $this->settings['number']['std'];

		$query_args = apply_filters(
			'woocommerce_top_rated_products_widget_args',
			array(
				'posts_per_page' => $number,
				'no_found_rows'  => 1,
				'post_status'    => 'publish',
				'post_type'      => 'product',
				'meta_key'       => '_wc_average_rating',
				'orderby'        => 'meta_value_num',
				'order'          => 'DESC',
				'meta_query'     => WC()->query->get_meta_query(),
				'tax_query'      => WC()->query->get_tax_query(),
			)
		); // WPCS: slow query ok.

		$r = new WP_Query( $query_args );

		if ( $r->have_posts() ) {

			$this->widget_start( $args, $instance );

			echo wp_kses_post( apply_filters( 'woocommerce_before_widget_product_list', '<ul class="product_list_widget">' ) );

			$template_args = array(
				'widget_id'   => isset( $args['widget_id'] ) ? $args['widget_id'] : $this->widget_id,
				'show_rating' => true,
			);

			while ( $r->have_posts() ) {
				$r->the_post();
				wc_get_template( 'content-widget-product.php', $template_args );
			}

			echo wp_kses_post( apply_filters( 'woocommerce_after_widget_product_list', '</ul>' ) );

			$this->widget_end( $args );
		}

		wp_reset_postdata();

		$content = ob_get_clean();

		echo $content; // WPCS: XSS ok.

		$this->cache_widget( $args, $content );
	}
}
widgets/class-wc-widget-product-search.php000064400000002014151336044770014650 0ustar00<?php
/**
 * Product Search Widget.
 *
 * @package WooCommerce\Widgets
 * @version 2.3.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Widget product search class.
 */
class WC_Widget_Product_Search extends WC_Widget {

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->widget_cssclass    = 'woocommerce widget_product_search';
		$this->widget_description = __( 'A search form for your store.', 'woocommerce' );
		$this->widget_id          = 'woocommerce_product_search';
		$this->widget_name        = __( 'Product Search', 'woocommerce' );
		$this->settings           = array(
			'title' => array(
				'type'  => 'text',
				'std'   => '',
				'label' => __( 'Title', 'woocommerce' ),
			),
		);

		parent::__construct();
	}

	/**
	 * Output widget.
	 *
	 * @see WP_Widget
	 *
	 * @param array $args     Arguments.
	 * @param array $instance Widget instance.
	 */
	public function widget( $args, $instance ) {
		$this->widget_start( $args, $instance );

		get_product_search_form();

		$this->widget_end( $args );
	}
}
class-wc-shipping.php000064400000026525151336044770010634 0ustar00<?php
/**
 * WooCommerce Shipping
 *
 * Handles shipping and loads shipping methods via hooks.
 *
 * @version 2.6.0
 * @package WooCommerce\Classes\Shipping
 */

use Automattic\Jetpack\Constants;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Shipping class.
 */
class WC_Shipping {

	/**
	 * True if shipping is enabled.
	 *
	 * @var bool
	 */
	public $enabled = false;

	/**
	 * Stores methods loaded into woocommerce.
	 *
	 * @var array|null
	 */
	public $shipping_methods = null;

	/**
	 * Stores the shipping classes.
	 *
	 * @var array
	 */
	public $shipping_classes = array();

	/**
	 * Stores packages to ship and to get quotes for.
	 *
	 * @var array
	 */
	public $packages = array();

	/**
	 * The single instance of the class
	 *
	 * @var WC_Shipping
	 * @since 2.1
	 */
	protected static $_instance = null;

	/**
	 * Main WC_Shipping Instance.
	 *
	 * Ensures only one instance of WC_Shipping is loaded or can be loaded.
	 *
	 * @since 2.1
	 * @return WC_Shipping Main instance
	 */
	public static function instance() {
		if ( is_null( self::$_instance ) ) {
			self::$_instance = new self();
		}
		return self::$_instance;
	}

	/**
	 * Cloning is forbidden.
	 *
	 * @since 2.1
	 */
	public function __clone() {
		wc_doing_it_wrong( __FUNCTION__, __( 'Cloning is forbidden.', 'woocommerce' ), '2.1' );
	}

	/**
	 * Unserializing instances of this class is forbidden.
	 *
	 * @since 2.1
	 */
	public function __wakeup() {
		wc_doing_it_wrong( __FUNCTION__, __( 'Unserializing instances of this class is forbidden.', 'woocommerce' ), '2.1' );
	}

	/**
	 * Magic getter.
	 *
	 * @param string $name Property name.
	 * @return mixed
	 */
	public function __get( $name ) {
		// Grab from cart for backwards compatibility with versions prior to 3.2.
		if ( 'shipping_total' === $name ) {
			return WC()->cart->get_shipping_total();
		}
		if ( 'shipping_taxes' === $name ) {
			return WC()->cart->get_shipping_taxes();
		}
	}

	/**
	 * Initialize shipping.
	 */
	public function __construct() {
		$this->enabled = wc_shipping_enabled();

		if ( $this->enabled ) {
			$this->init();
		}
	}

	/**
	 * Initialize shipping.
	 */
	public function init() {
		do_action( 'woocommerce_shipping_init' );
	}

	/**
	 * Shipping methods register themselves by returning their main class name through the woocommerce_shipping_methods filter.
	 *
	 * @return array
	 */
	public function get_shipping_method_class_names() {
		// Unique Method ID => Method Class name.
		$shipping_methods = array(
			'flat_rate'     => 'WC_Shipping_Flat_Rate',
			'free_shipping' => 'WC_Shipping_Free_Shipping',
			'local_pickup'  => 'WC_Shipping_Local_Pickup',
		);

		// For backwards compatibility with 2.5.x we load any ENABLED legacy shipping methods here.
		$maybe_load_legacy_methods = array( 'flat_rate', 'free_shipping', 'international_delivery', 'local_delivery', 'local_pickup' );

		foreach ( $maybe_load_legacy_methods as $method ) {
			$options = get_option( 'woocommerce_' . $method . '_settings' );
			if ( $options && isset( $options['enabled'] ) && 'yes' === $options['enabled'] ) {
				$shipping_methods[ 'legacy_' . $method ] = 'WC_Shipping_Legacy_' . $method;
			}
		}

		return apply_filters( 'woocommerce_shipping_methods', $shipping_methods );
	}

	/**
	 * Loads all shipping methods which are hooked in.
	 * If a $package is passed, some methods may add themselves conditionally and zones will be used.
	 *
	 * @param array $package Package information.
	 * @return WC_Shipping_Method[]
	 */
	public function load_shipping_methods( $package = array() ) {
		if ( ! empty( $package ) ) {
			$debug_mode             = 'yes' === get_option( 'woocommerce_shipping_debug_mode', 'no' );
			$shipping_zone          = WC_Shipping_Zones::get_zone_matching_package( $package );
			$this->shipping_methods = $shipping_zone->get_shipping_methods( true );

			// translators: %s: shipping zone name.
			$matched_zone_notice = sprintf( __( 'Customer matched zone "%s"', 'woocommerce' ), $shipping_zone->get_zone_name() );

			// Debug output.
			if ( $debug_mode && ! Constants::is_defined( 'WOOCOMMERCE_CHECKOUT' ) && ! Constants::is_defined( 'WC_DOING_AJAX' ) && ! wc_has_notice( $matched_zone_notice ) ) {
				wc_add_notice( $matched_zone_notice );
			}
		} else {
			$this->shipping_methods = array();
		}

		// For the settings in the backend, and for non-shipping zone methods, we still need to load any registered classes here.
		foreach ( $this->get_shipping_method_class_names() as $method_id => $method_class ) {
			$this->register_shipping_method( $method_class );
		}

		// Methods can register themselves manually through this hook if necessary.
		do_action( 'woocommerce_load_shipping_methods', $package );

		// Return loaded methods.
		return $this->get_shipping_methods();
	}

	/**
	 * Register a shipping method.
	 *
	 * @param object|string $method Either the name of the method's class, or an instance of the method's class.
	 *
	 * @return bool|void
	 */
	public function register_shipping_method( $method ) {
		if ( ! is_object( $method ) ) {
			if ( ! class_exists( $method ) ) {
				return false;
			}
			$method = new $method();
		}
		if ( is_null( $this->shipping_methods ) ) {
			$this->shipping_methods = array();
		}
		$this->shipping_methods[ $method->id ] = $method;
	}

	/**
	 * Unregister shipping methods.
	 */
	public function unregister_shipping_methods() {
		$this->shipping_methods = null;
	}

	/**
	 * Returns all registered shipping methods for usage.
	 *
	 * @return WC_Shipping_Method[]
	 */
	public function get_shipping_methods() {
		if ( is_null( $this->shipping_methods ) ) {
			$this->load_shipping_methods();
		}
		return $this->shipping_methods;
	}

	/**
	 * Get an array of shipping classes.
	 *
	 * @return array
	 */
	public function get_shipping_classes() {
		if ( empty( $this->shipping_classes ) ) {
			$classes                = get_terms(
				'product_shipping_class',
				array(
					'hide_empty' => '0',
					'orderby'    => 'name',
				)
			);
			$this->shipping_classes = ! is_wp_error( $classes ) ? $classes : array();
		}
		return apply_filters( 'woocommerce_get_shipping_classes', $this->shipping_classes );
	}

	/**
	 * Calculate shipping for (multiple) packages of cart items.
	 *
	 * @param array $packages multi-dimensional array of cart items to calc shipping for.
	 * @return array Array of calculated packages.
	 */
	public function calculate_shipping( $packages = array() ) {
		$this->packages = array();

		if ( ! $this->enabled || empty( $packages ) ) {
			return array();
		}

		// Calculate costs for passed packages.
		foreach ( $packages as $package_key => $package ) {
			$this->packages[ $package_key ] = $this->calculate_shipping_for_package( $package, $package_key );
		}

		/**
		 * Allow packages to be reorganized after calculating the shipping.
		 *
		 * This filter can be used to apply some extra manipulation after the shipping costs are calculated for the packages
		 * but before WooCommerce does anything with them. A good example of usage is to merge the shipping methods for multiple
		 * packages for marketplaces.
		 *
		 * @since 2.6.0
		 *
		 * @param array $packages The array of packages after shipping costs are calculated.
		 */
		$this->packages = array_filter( (array) apply_filters( 'woocommerce_shipping_packages', $this->packages ) );

		return $this->packages;
	}

	/**
	 * See if package is shippable.
	 *
	 * Packages are shippable until proven otherwise e.g. after getting a shipping country.
	 *
	 * @param  array $package Package of cart items.
	 * @return bool
	 */
	public function is_package_shippable( $package ) {
		// Packages are shippable until proven otherwise.
		if ( empty( $package['destination']['country'] ) ) {
			return true;
		}

		$allowed = array_keys( WC()->countries->get_shipping_countries() );
		return in_array( $package['destination']['country'], $allowed, true );
	}

	/**
	 * Calculate shipping rates for a package,
	 *
	 * Calculates each shipping methods cost. Rates are stored in the session based on the package hash to avoid re-calculation every page load.
	 *
	 * @param array $package Package of cart items.
	 * @param int   $package_key Index of the package being calculated. Used to cache multiple package rates.
	 *
	 * @return array|bool
	 */
	public function calculate_shipping_for_package( $package = array(), $package_key = 0 ) {
		// If shipping is disabled or the package is invalid, return false.
		if ( ! $this->enabled || empty( $package ) ) {
			return false;
		}

		$package['rates'] = array();

		// If the package is not shippable, e.g. trying to ship to an invalid country, do not calculate rates.
		if ( ! $this->is_package_shippable( $package ) ) {
			return $package;
		}

		// Check if we need to recalculate shipping for this package.
		$package_to_hash = $package;

		// Remove data objects so hashes are consistent.
		foreach ( $package_to_hash['contents'] as $item_id => $item ) {
			unset( $package_to_hash['contents'][ $item_id ]['data'] );
		}

		// Get rates stored in the WC session data for this package.
		$wc_session_key = 'shipping_for_package_' . $package_key;
		$stored_rates   = WC()->session->get( $wc_session_key );

		// Calculate the hash for this package so we can tell if it's changed since last calculation.
		$package_hash = 'wc_ship_' . md5( wp_json_encode( $package_to_hash ) . WC_Cache_Helper::get_transient_version( 'shipping' ) );

		if ( ! is_array( $stored_rates ) || $package_hash !== $stored_rates['package_hash'] || 'yes' === get_option( 'woocommerce_shipping_debug_mode', 'no' ) ) {
			foreach ( $this->load_shipping_methods( $package ) as $shipping_method ) {
				if ( ! $shipping_method->supports( 'shipping-zones' ) || $shipping_method->get_instance_id() ) {
					/**
					 * Fires before getting shipping rates for a package.
					 *
					 * @since 4.3.0
					 * @param array $package Package of cart items.
					 * @param WC_Shipping_Method $shipping_method Shipping method instance.
					 */
					do_action( 'woocommerce_before_get_rates_for_package', $package, $shipping_method );

					// Use + instead of array_merge to maintain numeric keys.
					$package['rates'] = $package['rates'] + $shipping_method->get_rates_for_package( $package );

					/**
					 * Fires after getting shipping rates for a package.
					 *
					 * @since 4.3.0
					 * @param array $package Package of cart items.
					 * @param WC_Shipping_Method $shipping_method Shipping method instance.
					 */
					do_action( 'woocommerce_after_get_rates_for_package', $package, $shipping_method );
				}
			}

			// Filter the calculated rates.
			$package['rates'] = apply_filters( 'woocommerce_package_rates', $package['rates'], $package );

			// Store in session to avoid recalculation.
			WC()->session->set(
				$wc_session_key,
				array(
					'package_hash' => $package_hash,
					'rates'        => $package['rates'],
				)
			);
		} else {
			$package['rates'] = $stored_rates['rates'];
		}

		return $package;
	}

	/**
	 * Get packages.
	 *
	 * @return array
	 */
	public function get_packages() {
		return $this->packages;
	}

	/**
	 * Reset shipping.
	 *
	 * Reset the totals for shipping as a whole.
	 */
	public function reset_shipping() {
		unset( WC()->session->chosen_shipping_methods );
		$this->packages = array();
	}

	/**
	 * Deprecated
	 *
	 * @deprecated 2.6.0 Was previously used to determine sort order of methods, but this is now controlled by zones and thus unused.
	 */
	public function sort_shipping_methods() {
		wc_deprecated_function( 'sort_shipping_methods', '2.6' );
		return $this->shipping_methods;
	}
}
class-wc-deprecated-filter-hooks.php000064400000015515151336044770013514 0ustar00<?php
/**
 * Deprecated filter hooks
 *
 * @package WooCommerce\Abstracts
 * @since   3.0.0
 * @version 3.3.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Handles deprecation notices and triggering of legacy filter hooks
 */
class WC_Deprecated_Filter_Hooks extends WC_Deprecated_Hooks {

	/**
	 * Array of deprecated hooks we need to handle.
	 * Format of 'new' => 'old'.
	 *
	 * @var array
	 */
	protected $deprecated_hooks = array(
		'woocommerce_structured_data_order'          => 'woocommerce_email_order_schema_markup',
		'woocommerce_add_to_cart_fragments'          => 'add_to_cart_fragments',
		'woocommerce_add_to_cart_redirect'           => 'add_to_cart_redirect',
		'woocommerce_product_get_width'              => 'woocommerce_product_width',
		'woocommerce_product_get_height'             => 'woocommerce_product_height',
		'woocommerce_product_get_length'             => 'woocommerce_product_length',
		'woocommerce_product_get_weight'             => 'woocommerce_product_weight',
		'woocommerce_product_get_sku'                => 'woocommerce_get_sku',
		'woocommerce_product_get_price'              => 'woocommerce_get_price',
		'woocommerce_product_get_regular_price'      => 'woocommerce_get_regular_price',
		'woocommerce_product_get_sale_price'         => 'woocommerce_get_sale_price',
		'woocommerce_product_get_tax_class'          => 'woocommerce_product_tax_class',
		'woocommerce_product_get_stock_quantity'     => 'woocommerce_get_stock_quantity',
		'woocommerce_product_get_attributes'         => 'woocommerce_get_product_attributes',
		'woocommerce_product_get_gallery_image_ids'  => 'woocommerce_product_gallery_attachment_ids',
		'woocommerce_product_get_review_count'       => 'woocommerce_product_review_count',
		'woocommerce_product_get_downloads'          => 'woocommerce_product_files',
		'woocommerce_order_get_currency'             => 'woocommerce_get_currency',
		'woocommerce_order_get_discount_total'       => 'woocommerce_order_amount_discount_total',
		'woocommerce_order_get_discount_tax'         => 'woocommerce_order_amount_discount_tax',
		'woocommerce_order_get_shipping_total'       => 'woocommerce_order_amount_shipping_total',
		'woocommerce_order_get_shipping_tax'         => 'woocommerce_order_amount_shipping_tax',
		'woocommerce_order_get_cart_tax'             => 'woocommerce_order_amount_cart_tax',
		'woocommerce_order_get_total'                => 'woocommerce_order_amount_total',
		'woocommerce_order_get_total_tax'            => 'woocommerce_order_amount_total_tax',
		'woocommerce_order_get_total_discount'       => 'woocommerce_order_amount_total_discount',
		'woocommerce_order_get_subtotal'             => 'woocommerce_order_amount_subtotal',
		'woocommerce_order_get_tax_totals'           => 'woocommerce_order_tax_totals',
		'woocommerce_get_order_refund_get_amount'    => 'woocommerce_refund_amount',
		'woocommerce_get_order_refund_get_reason'    => 'woocommerce_refund_reason',
		'default_checkout_billing_country'           => 'default_checkout_country',
		'default_checkout_billing_state'             => 'default_checkout_state',
		'default_checkout_billing_postcode'          => 'default_checkout_postcode',
		'woocommerce_system_status_environment_rows' => 'woocommerce_debug_posting',
		'woocommerce_credit_card_type_labels'        => 'wocommerce_credit_card_type_labels',
		'woocommerce_settings_tabs_advanced'         => 'woocommerce_settings_tabs_api',
		'woocommerce_settings_advanced'              => 'woocommerce_settings_api',
	);

	/**
	 * Array of versions on each hook has been deprecated.
	 *
	 * @var array
	 */
	protected $deprecated_version = array(
		'woocommerce_email_order_schema_markup'      => '3.0.0',
		'add_to_cart_fragments'                      => '3.0.0',
		'add_to_cart_redirect'                       => '3.0.0',
		'woocommerce_product_width'                  => '3.0.0',
		'woocommerce_product_height'                 => '3.0.0',
		'woocommerce_product_length'                 => '3.0.0',
		'woocommerce_product_weight'                 => '3.0.0',
		'woocommerce_get_sku'                        => '3.0.0',
		'woocommerce_get_price'                      => '3.0.0',
		'woocommerce_get_regular_price'              => '3.0.0',
		'woocommerce_get_sale_price'                 => '3.0.0',
		'woocommerce_product_tax_class'              => '3.0.0',
		'woocommerce_get_stock_quantity'             => '3.0.0',
		'woocommerce_get_product_attributes'         => '3.0.0',
		'woocommerce_product_gallery_attachment_ids' => '3.0.0',
		'woocommerce_product_review_count'           => '3.0.0',
		'woocommerce_product_files'                  => '3.0.0',
		'woocommerce_get_currency'                   => '3.0.0',
		'woocommerce_order_amount_discount_total'    => '3.0.0',
		'woocommerce_order_amount_discount_tax'      => '3.0.0',
		'woocommerce_order_amount_shipping_total'    => '3.0.0',
		'woocommerce_order_amount_shipping_tax'      => '3.0.0',
		'woocommerce_order_amount_cart_tax'          => '3.0.0',
		'woocommerce_order_amount_total'             => '3.0.0',
		'woocommerce_order_amount_total_tax'         => '3.0.0',
		'woocommerce_order_amount_total_discount'    => '3.0.0',
		'woocommerce_order_amount_subtotal'          => '3.0.0',
		'woocommerce_order_tax_totals'               => '3.0.0',
		'woocommerce_refund_amount'                  => '3.0.0',
		'woocommerce_refund_reason'                  => '3.0.0',
		'default_checkout_country'                   => '3.0.0',
		'default_checkout_state'                     => '3.0.0',
		'default_checkout_postcode'                  => '3.0.0',
		'woocommerce_debug_posting'                  => '3.0.0',
		'wocommerce_credit_card_type_labels'         => '3.0.0',
		'woocommerce_settings_tabs_api'              => '3.4.0',
		'woocommerce_settings_api'                   => '3.4.0',
	);

	/**
	 * Hook into the new hook so we can handle deprecated hooks once fired.
	 *
	 * @param string $hook_name Hook name.
	 */
	public function hook_in( $hook_name ) {
		add_filter( $hook_name, array( $this, 'maybe_handle_deprecated_hook' ), -1000, 8 );
	}

	/**
	 * If the old hook is in-use, trigger it.
	 *
	 * @param  string $new_hook          New hook name.
	 * @param  string $old_hook          Old hook name.
	 * @param  array  $new_callback_args New callback args.
	 * @param  mixed  $return_value      Returned value.
	 * @return mixed
	 */
	public function handle_deprecated_hook( $new_hook, $old_hook, $new_callback_args, $return_value ) {
		if ( has_filter( $old_hook ) ) {
			$this->display_notice( $old_hook, $new_hook );
			$return_value = $this->trigger_hook( $old_hook, $new_callback_args );
		}
		return $return_value;
	}

	/**
	 * Fire off a legacy hook with it's args.
	 *
	 * @param  string $old_hook          Old hook name.
	 * @param  array  $new_callback_args New callback args.
	 * @return mixed
	 */
	protected function trigger_hook( $old_hook, $new_callback_args ) {
		return apply_filters_ref_array( $old_hook, $new_callback_args );
	}
}
import/class-wc-product-csv-importer.php000064400000075755151336044770014446 0ustar00<?php
/**
 * WooCommerce Product CSV importer
 *
 * @package WooCommerce\Import
 * @version 3.1.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Include dependencies.
 */
if ( ! class_exists( 'WC_Product_Importer', false ) ) {
	include_once dirname( __FILE__ ) . '/abstract-wc-product-importer.php';
}

if ( ! class_exists( 'WC_Product_CSV_Importer_Controller', false ) ) {
	include_once WC_ABSPATH . 'includes/admin/importers/class-wc-product-csv-importer-controller.php';
}

/**
 * WC_Product_CSV_Importer Class.
 */
class WC_Product_CSV_Importer extends WC_Product_Importer {

	/**
	 * Tracks current row being parsed.
	 *
	 * @var integer
	 */
	protected $parsing_raw_data_index = 0;

	/**
	 * Initialize importer.
	 *
	 * @param string $file   File to read.
	 * @param array  $params Arguments for the parser.
	 */
	public function __construct( $file, $params = array() ) {
		$default_args = array(
			'start_pos'        => 0, // File pointer start.
			'end_pos'          => -1, // File pointer end.
			'lines'            => -1, // Max lines to read.
			'mapping'          => array(), // Column mapping. csv_heading => schema_heading.
			'parse'            => false, // Whether to sanitize and format data.
			'update_existing'  => false, // Whether to update existing items.
			'delimiter'        => ',', // CSV delimiter.
			'prevent_timeouts' => true, // Check memory and time usage and abort if reaching limit.
			'enclosure'        => '"', // The character used to wrap text in the CSV.
			'escape'           => "\0", // PHP uses '\' as the default escape character. This is not RFC-4180 compliant. This disables the escape character.
		);

		$this->params = wp_parse_args( $params, $default_args );
		$this->file   = $file;

		if ( isset( $this->params['mapping']['from'], $this->params['mapping']['to'] ) ) {
			$this->params['mapping'] = array_combine( $this->params['mapping']['from'], $this->params['mapping']['to'] );
		}

		// Import mappings for CSV data.
		include_once dirname( dirname( __FILE__ ) ) . '/admin/importers/mappings/mappings.php';

		$this->read_file();
	}

	/**
	 * Read file.
	 */
	protected function read_file() {
		if ( ! WC_Product_CSV_Importer_Controller::is_file_valid_csv( $this->file ) ) {
			wp_die( esc_html__( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) );
		}

		$handle = fopen( $this->file, 'r' ); // @codingStandardsIgnoreLine.

		if ( false !== $handle ) {
			$this->raw_keys = version_compare( PHP_VERSION, '5.3', '>=' ) ? array_map( 'trim', fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'], $this->params['escape'] ) ) : array_map( 'trim', fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'] ) ); // @codingStandardsIgnoreLine

			// Remove BOM signature from the first item.
			if ( isset( $this->raw_keys[0] ) ) {
				$this->raw_keys[0] = $this->remove_utf8_bom( $this->raw_keys[0] );
			}

			if ( 0 !== $this->params['start_pos'] ) {
				fseek( $handle, (int) $this->params['start_pos'] );
			}

			while ( 1 ) {
				$row = version_compare( PHP_VERSION, '5.3', '>=' ) ? fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'], $this->params['escape'] ) : fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'] ); // @codingStandardsIgnoreLine

				if ( false !== $row ) {
					$this->raw_data[]                                 = $row;
					$this->file_positions[ count( $this->raw_data ) ] = ftell( $handle );

					if ( ( $this->params['end_pos'] > 0 && ftell( $handle ) >= $this->params['end_pos'] ) || 0 === --$this->params['lines'] ) {
						break;
					}
				} else {
					break;
				}
			}

			$this->file_position = ftell( $handle );
		}

		if ( ! empty( $this->params['mapping'] ) ) {
			$this->set_mapped_keys();
		}

		if ( $this->params['parse'] ) {
			$this->set_parsed_data();
		}
	}

	/**
	 * Remove UTF-8 BOM signature.
	 *
	 * @param string $string String to handle.
	 *
	 * @return string
	 */
	protected function remove_utf8_bom( $string ) {
		if ( 'efbbbf' === substr( bin2hex( $string ), 0, 6 ) ) {
			$string = substr( $string, 3 );
		}

		return $string;
	}

	/**
	 * Set file mapped keys.
	 */
	protected function set_mapped_keys() {
		$mapping = $this->params['mapping'];

		foreach ( $this->raw_keys as $key ) {
			$this->mapped_keys[] = isset( $mapping[ $key ] ) ? $mapping[ $key ] : $key;
		}
	}

	/**
	 * Parse relative field and return product ID.
	 *
	 * Handles `id:xx` and SKUs.
	 *
	 * If mapping to an id: and the product ID does not exist, this link is not
	 * valid.
	 *
	 * If mapping to a SKU and the product ID does not exist, a temporary object
	 * will be created so it can be updated later.
	 *
	 * @param string $value Field value.
	 *
	 * @return int|string
	 */
	public function parse_relative_field( $value ) {
		global $wpdb;

		if ( empty( $value ) ) {
			return '';
		}

		// IDs are prefixed with id:.
		if ( preg_match( '/^id:(\d+)$/', $value, $matches ) ) {
			$id = intval( $matches[1] );

			// If original_id is found, use that instead of the given ID since a new placeholder must have been created already.
			$original_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_original_id' AND meta_value = %s;", $id ) ); // WPCS: db call ok, cache ok.

			if ( $original_id ) {
				return absint( $original_id );
			}

			// See if the given ID maps to a valid product allready.
			$existing_id = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type IN ( 'product', 'product_variation' ) AND ID = %d;", $id ) ); // WPCS: db call ok, cache ok.

			if ( $existing_id ) {
				return absint( $existing_id );
			}

			// If we're not updating existing posts, we may need a placeholder product to map to.
			if ( ! $this->params['update_existing'] ) {
				$product = wc_get_product_object( 'simple' );
				$product->set_name( 'Import placeholder for ' . $id );
				$product->set_status( 'importing' );
				$product->add_meta_data( '_original_id', $id, true );
				$id = $product->save();
			}

			return $id;
		}

		$id = wc_get_product_id_by_sku( $value );

		if ( $id ) {
			return $id;
		}

		try {
			$product = wc_get_product_object( 'simple' );
			$product->set_name( 'Import placeholder for ' . $value );
			$product->set_status( 'importing' );
			$product->set_sku( $value );
			$id = $product->save();

			if ( $id && ! is_wp_error( $id ) ) {
				return $id;
			}
		} catch ( Exception $e ) {
			return '';
		}

		return '';
	}

	/**
	 * Parse the ID field.
	 *
	 * If we're not doing an update, create a placeholder product so mapping works
	 * for rows following this one.
	 *
	 * @param string $value Field value.
	 *
	 * @return int
	 */
	public function parse_id_field( $value ) {
		global $wpdb;

		$id = absint( $value );

		if ( ! $id ) {
			return 0;
		}

		// See if this maps to an ID placeholder already.
		$original_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_original_id' AND meta_value = %s;", $id ) ); // WPCS: db call ok, cache ok.

		if ( $original_id ) {
			return absint( $original_id );
		}

		// Not updating? Make sure we have a new placeholder for this ID.
		if ( ! $this->params['update_existing'] ) {
			$mapped_keys      = $this->get_mapped_keys();
			$sku_column_index = absint( array_search( 'sku', $mapped_keys, true ) );
			$row_sku          = isset( $this->raw_data[ $this->parsing_raw_data_index ][ $sku_column_index ] ) ? $this->raw_data[ $this->parsing_raw_data_index ][ $sku_column_index ] : '';
			$id_from_sku      = $row_sku ? wc_get_product_id_by_sku( $row_sku ) : '';

			// If row has a SKU, make sure placeholder was not made already.
			if ( $id_from_sku ) {
				return $id_from_sku;
			}

			$product = wc_get_product_object( 'simple' );
			$product->set_name( 'Import placeholder for ' . $id );
			$product->set_status( 'importing' );
			$product->add_meta_data( '_original_id', $id, true );

			// If row has a SKU, make sure placeholder has it too.
			if ( $row_sku ) {
				$product->set_sku( $row_sku );
			}
			$id = $product->save();
		}

		return $id && ! is_wp_error( $id ) ? $id : 0;
	}

	/**
	 * Parse relative comma-delineated field and return product ID.
	 *
	 * @param string $value Field value.
	 *
	 * @return array
	 */
	public function parse_relative_comma_field( $value ) {
		if ( empty( $value ) ) {
			return array();
		}

		return array_filter( array_map( array( $this, 'parse_relative_field' ), $this->explode_values( $value ) ) );
	}

	/**
	 * Parse a comma-delineated field from a CSV.
	 *
	 * @param string $value Field value.
	 *
	 * @return array
	 */
	public function parse_comma_field( $value ) {
		if ( empty( $value ) && '0' !== $value ) {
			return array();
		}

		$value = $this->unescape_data( $value );
		return array_map( 'wc_clean', $this->explode_values( $value ) );
	}

	/**
	 * Parse a field that is generally '1' or '0' but can be something else.
	 *
	 * @param string $value Field value.
	 *
	 * @return bool|string
	 */
	public function parse_bool_field( $value ) {
		if ( '0' === $value ) {
			return false;
		}

		if ( '1' === $value ) {
			return true;
		}

		// Don't return explicit true or false for empty fields or values like 'notify'.
		return wc_clean( $value );
	}

	/**
	 * Parse a float value field.
	 *
	 * @param string $value Field value.
	 *
	 * @return float|string
	 */
	public function parse_float_field( $value ) {
		if ( '' === $value ) {
			return $value;
		}

		// Remove the ' prepended to fields that start with - if needed.
		$value = $this->unescape_data( $value );

		return floatval( $value );
	}

	/**
	 * Parse the stock qty field.
	 *
	 * @param string $value Field value.
	 *
	 * @return float|string
	 */
	public function parse_stock_quantity_field( $value ) {
		if ( '' === $value ) {
			return $value;
		}

		// Remove the ' prepended to fields that start with - if needed.
		$value = $this->unescape_data( $value );

		return wc_stock_amount( $value );
	}

	/**
	 * Parse the tax status field.
	 *
	 * @param string $value Field value.
	 *
	 * @return string
	 */
	public function parse_tax_status_field( $value ) {
		if ( '' === $value ) {
			return $value;
		}

		// Remove the ' prepended to fields that start with - if needed.
		$value = $this->unescape_data( $value );

		if ( 'true' === strtolower( $value ) || 'false' === strtolower( $value ) ) {
			$value = wc_string_to_bool( $value ) ? 'taxable' : 'none';
		}

		return wc_clean( $value );
	}

	/**
	 * Parse a category field from a CSV.
	 * Categories are separated by commas and subcategories are "parent > subcategory".
	 *
	 * @param string $value Field value.
	 *
	 * @return array of arrays with "parent" and "name" keys.
	 */
	public function parse_categories_field( $value ) {
		if ( empty( $value ) ) {
			return array();
		}

		$row_terms  = $this->explode_values( $value );
		$categories = array();

		foreach ( $row_terms as $row_term ) {
			$parent = null;
			$_terms = array_map( 'trim', explode( '>', $row_term ) );
			$total  = count( $_terms );

			foreach ( $_terms as $index => $_term ) {
				// Don't allow users without capabilities to create new categories.
				if ( ! current_user_can( 'manage_product_terms' ) ) {
					break;
				}

				$term = wp_insert_term( $_term, 'product_cat', array( 'parent' => intval( $parent ) ) );

				if ( is_wp_error( $term ) ) {
					if ( $term->get_error_code() === 'term_exists' ) {
						// When term exists, error data should contain existing term id.
						$term_id = $term->get_error_data();
					} else {
						break; // We cannot continue on any other error.
					}
				} else {
					// New term.
					$term_id = $term['term_id'];
				}

				// Only requires assign the last category.
				if ( ( 1 + $index ) === $total ) {
					$categories[] = $term_id;
				} else {
					// Store parent to be able to insert or query categories based in parent ID.
					$parent = $term_id;
				}
			}
		}

		return $categories;
	}

	/**
	 * Parse a tag field from a CSV.
	 *
	 * @param string $value Field value.
	 *
	 * @return array
	 */
	public function parse_tags_field( $value ) {
		if ( empty( $value ) ) {
			return array();
		}

		$value = $this->unescape_data( $value );
		$names = $this->explode_values( $value );
		$tags  = array();

		foreach ( $names as $name ) {
			$term = get_term_by( 'name', $name, 'product_tag' );

			if ( ! $term || is_wp_error( $term ) ) {
				$term = (object) wp_insert_term( $name, 'product_tag' );
			}

			if ( ! is_wp_error( $term ) ) {
				$tags[] = $term->term_id;
			}
		}

		return $tags;
	}

	/**
	 * Parse a tag field from a CSV with space separators.
	 *
	 * @param string $value Field value.
	 *
	 * @return array
	 */
	public function parse_tags_spaces_field( $value ) {
		if ( empty( $value ) ) {
			return array();
		}

		$value = $this->unescape_data( $value );
		$names = $this->explode_values( $value, ' ' );
		$tags  = array();

		foreach ( $names as $name ) {
			$term = get_term_by( 'name', $name, 'product_tag' );

			if ( ! $term || is_wp_error( $term ) ) {
				$term = (object) wp_insert_term( $name, 'product_tag' );
			}

			if ( ! is_wp_error( $term ) ) {
				$tags[] = $term->term_id;
			}
		}

		return $tags;
	}

	/**
	 * Parse a shipping class field from a CSV.
	 *
	 * @param string $value Field value.
	 *
	 * @return int
	 */
	public function parse_shipping_class_field( $value ) {
		if ( empty( $value ) ) {
			return 0;
		}

		$term = get_term_by( 'name', $value, 'product_shipping_class' );

		if ( ! $term || is_wp_error( $term ) ) {
			$term = (object) wp_insert_term( $value, 'product_shipping_class' );
		}

		if ( is_wp_error( $term ) ) {
			return 0;
		}

		return $term->term_id;
	}

	/**
	 * Parse images list from a CSV. Images can be filenames or URLs.
	 *
	 * @param string $value Field value.
	 *
	 * @return array
	 */
	public function parse_images_field( $value ) {
		if ( empty( $value ) ) {
			return array();
		}

		$images    = array();
		$separator = apply_filters( 'woocommerce_product_import_image_separator', ',' );

		foreach ( $this->explode_values( $value, $separator ) as $image ) {
			if ( stristr( $image, '://' ) ) {
				$images[] = esc_url_raw( $image );
			} else {
				$images[] = sanitize_file_name( $image );
			}
		}

		return $images;
	}

	/**
	 * Parse dates from a CSV.
	 * Dates requires the format YYYY-MM-DD and time is optional.
	 *
	 * @param string $value Field value.
	 *
	 * @return string|null
	 */
	public function parse_date_field( $value ) {
		if ( empty( $value ) ) {
			return null;
		}

		if ( preg_match( '/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])([ 01-9:]*)$/', $value ) ) {
			// Don't include the time if the field had time in it.
			return current( explode( ' ', $value ) );
		}

		return null;
	}

	/**
	 * Parse backorders from a CSV.
	 *
	 * @param string $value Field value.
	 *
	 * @return string
	 */
	public function parse_backorders_field( $value ) {
		if ( empty( $value ) ) {
			return 'no';
		}

		$value = $this->parse_bool_field( $value );

		if ( 'notify' === $value ) {
			return 'notify';
		} elseif ( is_bool( $value ) ) {
			return $value ? 'yes' : 'no';
		}

		return 'no';
	}

	/**
	 * Just skip current field.
	 *
	 * By default is applied wc_clean() to all not listed fields
	 * in self::get_formatting_callback(), use this method to skip any formatting.
	 *
	 * @param string $value Field value.
	 *
	 * @return string
	 */
	public function parse_skip_field( $value ) {
		return $value;
	}

	/**
	 * Parse download file urls, we should allow shortcodes here.
	 *
	 * Allow shortcodes if present, othersiwe esc_url the value.
	 *
	 * @param string $value Field value.
	 *
	 * @return string
	 */
	public function parse_download_file_field( $value ) {
		// Absolute file paths.
		if ( 0 === strpos( $value, 'http' ) ) {
			return esc_url_raw( $value );
		}
		// Relative and shortcode paths.
		return wc_clean( $value );
	}

	/**
	 * Parse an int value field
	 *
	 * @param int $value field value.
	 *
	 * @return int
	 */
	public function parse_int_field( $value ) {
		// Remove the ' prepended to fields that start with - if needed.
		$value = $this->unescape_data( $value );

		return intval( $value );
	}

	/**
	 * Parse a description value field
	 *
	 * @param string $description field value.
	 *
	 * @return string
	 */
	public function parse_description_field( $description ) {
		$parts = explode( "\\\\n", $description );
		foreach ( $parts as $key => $part ) {
			$parts[ $key ] = str_replace( '\n', "\n", $part );
		}

		return implode( '\\\n', $parts );
	}

	/**
	 * Parse the published field. 1 is published, 0 is private, -1 is draft.
	 * Alternatively, 'true' can be used for published and 'false' for draft.
	 *
	 * @param string $value Field value.
	 *
	 * @return float|string
	 */
	public function parse_published_field( $value ) {
		if ( '' === $value ) {
			return $value;
		}

		// Remove the ' prepended to fields that start with - if needed.
		$value = $this->unescape_data( $value );

		if ( 'true' === strtolower( $value ) || 'false' === strtolower( $value ) ) {
			return wc_string_to_bool( $value ) ? 1 : -1;
		}

		return floatval( $value );
	}

	/**
	 * Deprecated get formatting callback method.
	 *
	 * @deprecated 4.3.0
	 * @return array
	 */
	protected function get_formating_callback() {
		return $this->get_formatting_callback();
	}

	/**
	 * Get formatting callback.
	 *
	 * @since 4.3.0
	 * @return array
	 */
	protected function get_formatting_callback() {

		/**
		 * Columns not mentioned here will get parsed with 'wc_clean'.
		 * column_name => callback.
		 */
		$data_formatting = array(
			'id'                => array( $this, 'parse_id_field' ),
			'type'              => array( $this, 'parse_comma_field' ),
			'published'         => array( $this, 'parse_published_field' ),
			'featured'          => array( $this, 'parse_bool_field' ),
			'date_on_sale_from' => array( $this, 'parse_date_field' ),
			'date_on_sale_to'   => array( $this, 'parse_date_field' ),
			'name'              => array( $this, 'parse_skip_field' ),
			'short_description' => array( $this, 'parse_description_field' ),
			'description'       => array( $this, 'parse_description_field' ),
			'manage_stock'      => array( $this, 'parse_bool_field' ),
			'low_stock_amount'  => array( $this, 'parse_stock_quantity_field' ),
			'backorders'        => array( $this, 'parse_backorders_field' ),
			'stock_status'      => array( $this, 'parse_bool_field' ),
			'sold_individually' => array( $this, 'parse_bool_field' ),
			'width'             => array( $this, 'parse_float_field' ),
			'length'            => array( $this, 'parse_float_field' ),
			'height'            => array( $this, 'parse_float_field' ),
			'weight'            => array( $this, 'parse_float_field' ),
			'reviews_allowed'   => array( $this, 'parse_bool_field' ),
			'purchase_note'     => 'wp_filter_post_kses',
			'price'             => 'wc_format_decimal',
			'regular_price'     => 'wc_format_decimal',
			'stock_quantity'    => array( $this, 'parse_stock_quantity_field' ),
			'category_ids'      => array( $this, 'parse_categories_field' ),
			'tag_ids'           => array( $this, 'parse_tags_field' ),
			'tag_ids_spaces'    => array( $this, 'parse_tags_spaces_field' ),
			'shipping_class_id' => array( $this, 'parse_shipping_class_field' ),
			'images'            => array( $this, 'parse_images_field' ),
			'parent_id'         => array( $this, 'parse_relative_field' ),
			'grouped_products'  => array( $this, 'parse_relative_comma_field' ),
			'upsell_ids'        => array( $this, 'parse_relative_comma_field' ),
			'cross_sell_ids'    => array( $this, 'parse_relative_comma_field' ),
			'download_limit'    => array( $this, 'parse_int_field' ),
			'download_expiry'   => array( $this, 'parse_int_field' ),
			'product_url'       => 'esc_url_raw',
			'menu_order'        => 'intval',
			'tax_status'        => array( $this, 'parse_tax_status_field' ),
		);

		/**
		 * Match special column names.
		 */
		$regex_match_data_formatting = array(
			'/attributes:value*/'    => array( $this, 'parse_comma_field' ),
			'/attributes:visible*/'  => array( $this, 'parse_bool_field' ),
			'/attributes:taxonomy*/' => array( $this, 'parse_bool_field' ),
			'/downloads:url*/'       => array( $this, 'parse_download_file_field' ),
			'/meta:*/'               => 'wp_kses_post', // Allow some HTML in meta fields.
		);

		$callbacks = array();

		// Figure out the parse function for each column.
		foreach ( $this->get_mapped_keys() as $index => $heading ) {
			$callback = 'wc_clean';

			if ( isset( $data_formatting[ $heading ] ) ) {
				$callback = $data_formatting[ $heading ];
			} else {
				foreach ( $regex_match_data_formatting as $regex => $callback ) {
					if ( preg_match( $regex, $heading ) ) {
						$callback = $callback;
						break;
					}
				}
			}

			$callbacks[] = $callback;
		}

		return apply_filters( 'woocommerce_product_importer_formatting_callbacks', $callbacks, $this );
	}

	/**
	 * Check if strings starts with determined word.
	 *
	 * @param string $haystack Complete sentence.
	 * @param string $needle   Excerpt.
	 *
	 * @return bool
	 */
	protected function starts_with( $haystack, $needle ) {
		return substr( $haystack, 0, strlen( $needle ) ) === $needle;
	}

	/**
	 * Expand special and internal data into the correct formats for the product CRUD.
	 *
	 * @param array $data Data to import.
	 *
	 * @return array
	 */
	protected function expand_data( $data ) {
		$data = apply_filters( 'woocommerce_product_importer_pre_expand_data', $data );

		// Images field maps to image and gallery id fields.
		if ( isset( $data['images'] ) ) {
			$images               = $data['images'];
			$data['raw_image_id'] = array_shift( $images );

			if ( ! empty( $images ) ) {
				$data['raw_gallery_image_ids'] = $images;
			}
			unset( $data['images'] );
		}

		// Type, virtual and downloadable are all stored in the same column.
		if ( isset( $data['type'] ) ) {
			$data['type']         = array_map( 'strtolower', $data['type'] );
			$data['virtual']      = in_array( 'virtual', $data['type'], true );
			$data['downloadable'] = in_array( 'downloadable', $data['type'], true );

			// Convert type to string.
			$data['type'] = current( array_diff( $data['type'], array( 'virtual', 'downloadable' ) ) );

			if ( ! $data['type'] ) {
				$data['type'] = 'simple';
			}
		}

		// Status is mapped from a special published field.
		if ( isset( $data['published'] ) ) {
			$statuses       = array(
				-1 => 'draft',
				0  => 'private',
				1  => 'publish',
			);
			$data['status'] = isset( $statuses[ $data['published'] ] ) ? $statuses[ $data['published'] ] : 'draft';

			// Fix draft status of variations.
			if ( isset( $data['type'] ) && 'variation' === $data['type'] && -1 === $data['published'] ) {
				$data['status'] = 'publish';
			}

			unset( $data['published'] );
		}

		if ( isset( $data['stock_quantity'] ) ) {
			if ( '' === $data['stock_quantity'] ) {
				$data['manage_stock'] = false;
				$data['stock_status'] = isset( $data['stock_status'] ) ? $data['stock_status'] : true;
			} else {
				$data['manage_stock'] = true;
			}
		}

		// Stock is bool or 'backorder'.
		if ( isset( $data['stock_status'] ) ) {
			if ( 'backorder' === $data['stock_status'] ) {
				$data['stock_status'] = 'onbackorder';
			} else {
				$data['stock_status'] = $data['stock_status'] ? 'instock' : 'outofstock';
			}
		}

		// Prepare grouped products.
		if ( isset( $data['grouped_products'] ) ) {
			$data['children'] = $data['grouped_products'];
			unset( $data['grouped_products'] );
		}

		// Tag ids.
		if ( isset( $data['tag_ids_spaces'] ) ) {
			$data['tag_ids'] = $data['tag_ids_spaces'];
			unset( $data['tag_ids_spaces'] );
		}

		// Handle special column names which span multiple columns.
		$attributes = array();
		$downloads  = array();
		$meta_data  = array();

		foreach ( $data as $key => $value ) {
			if ( $this->starts_with( $key, 'attributes:name' ) ) {
				if ( ! empty( $value ) ) {
					$attributes[ str_replace( 'attributes:name', '', $key ) ]['name'] = $value;
				}
				unset( $data[ $key ] );

			} elseif ( $this->starts_with( $key, 'attributes:value' ) ) {
				$attributes[ str_replace( 'attributes:value', '', $key ) ]['value'] = $value;
				unset( $data[ $key ] );

			} elseif ( $this->starts_with( $key, 'attributes:taxonomy' ) ) {
				$attributes[ str_replace( 'attributes:taxonomy', '', $key ) ]['taxonomy'] = wc_string_to_bool( $value );
				unset( $data[ $key ] );

			} elseif ( $this->starts_with( $key, 'attributes:visible' ) ) {
				$attributes[ str_replace( 'attributes:visible', '', $key ) ]['visible'] = wc_string_to_bool( $value );
				unset( $data[ $key ] );

			} elseif ( $this->starts_with( $key, 'attributes:default' ) ) {
				if ( ! empty( $value ) ) {
					$attributes[ str_replace( 'attributes:default', '', $key ) ]['default'] = $value;
				}
				unset( $data[ $key ] );

			} elseif ( $this->starts_with( $key, 'downloads:id' ) ) {
				if ( ! empty( $value ) ) {
					$downloads[ str_replace( 'downloads:id', '', $key ) ]['id'] = $value;
				}
				unset( $data[ $key ] );

			} elseif ( $this->starts_with( $key, 'downloads:name' ) ) {
				if ( ! empty( $value ) ) {
					$downloads[ str_replace( 'downloads:name', '', $key ) ]['name'] = $value;
				}
				unset( $data[ $key ] );

			} elseif ( $this->starts_with( $key, 'downloads:url' ) ) {
				if ( ! empty( $value ) ) {
					$downloads[ str_replace( 'downloads:url', '', $key ) ]['url'] = $value;
				}
				unset( $data[ $key ] );

			} elseif ( $this->starts_with( $key, 'meta:' ) ) {
				$meta_data[] = array(
					'key'   => str_replace( 'meta:', '', $key ),
					'value' => $value,
				);
				unset( $data[ $key ] );
			}
		}

		if ( ! empty( $attributes ) ) {
			// Remove empty attributes and clear indexes.
			foreach ( $attributes as $attribute ) {
				if ( empty( $attribute['name'] ) ) {
					continue;
				}

				$data['raw_attributes'][] = $attribute;
			}
		}

		if ( ! empty( $downloads ) ) {
			$data['downloads'] = array();

			foreach ( $downloads as $key => $file ) {
				if ( empty( $file['url'] ) ) {
					continue;
				}

				$data['downloads'][] = array(
					'download_id' => isset( $file['id'] ) ? $file['id'] : null,
					'name'        => $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['url'] ),
					'file'        => $file['url'],
				);
			}
		}

		if ( ! empty( $meta_data ) ) {
			$data['meta_data'] = $meta_data;
		}

		return $data;
	}

	/**
	 * Map and format raw data to known fields.
	 */
	protected function set_parsed_data() {
		$parse_functions = $this->get_formatting_callback();
		$mapped_keys     = $this->get_mapped_keys();
		$use_mb          = function_exists( 'mb_convert_encoding' );

		// Parse the data.
		foreach ( $this->raw_data as $row_index => $row ) {
			// Skip empty rows.
			if ( ! count( array_filter( $row ) ) ) {
				continue;
			}

			$this->parsing_raw_data_index = $row_index;

			$data = array();

			do_action( 'woocommerce_product_importer_before_set_parsed_data', $row, $mapped_keys );

			foreach ( $row as $id => $value ) {
				// Skip ignored columns.
				if ( empty( $mapped_keys[ $id ] ) ) {
					continue;
				}

				// Convert UTF8.
				if ( $use_mb ) {
					$encoding = mb_detect_encoding( $value, mb_detect_order(), true );
					if ( $encoding ) {
						$value = mb_convert_encoding( $value, 'UTF-8', $encoding );
					} else {
						$value = mb_convert_encoding( $value, 'UTF-8', 'UTF-8' );
					}
				} else {
					$value = wp_check_invalid_utf8( $value, true );
				}

				$data[ $mapped_keys[ $id ] ] = call_user_func( $parse_functions[ $id ], $value );
			}

			/**
			 * Filter product importer parsed data.
			 *
			 * @param array $parsed_data Parsed data.
			 * @param WC_Product_Importer $importer Importer instance.
			 */
			$this->parsed_data[] = apply_filters( 'woocommerce_product_importer_parsed_data', $this->expand_data( $data ), $this );
		}
	}

	/**
	 * Get a string to identify the row from parsed data.
	 *
	 * @param array $parsed_data Parsed data.
	 *
	 * @return string
	 */
	protected function get_row_id( $parsed_data ) {
		$id       = isset( $parsed_data['id'] ) ? absint( $parsed_data['id'] ) : 0;
		$sku      = isset( $parsed_data['sku'] ) ? esc_attr( $parsed_data['sku'] ) : '';
		$name     = isset( $parsed_data['name'] ) ? esc_attr( $parsed_data['name'] ) : '';
		$row_data = array();

		if ( $name ) {
			$row_data[] = $name;
		}
		if ( $id ) {
			/* translators: %d: product ID */
			$row_data[] = sprintf( __( 'ID %d', 'woocommerce' ), $id );
		}
		if ( $sku ) {
			/* translators: %s: product SKU */
			$row_data[] = sprintf( __( 'SKU %s', 'woocommerce' ), $sku );
		}

		return implode( ', ', $row_data );
	}

	/**
	 * Process importer.
	 *
	 * Do not import products with IDs or SKUs that already exist if option
	 * update existing is false, and likewise, if updating products, do not
	 * process rows which do not exist if an ID/SKU is provided.
	 *
	 * @return array
	 */
	public function import() {
		$this->start_time = time();
		$index            = 0;
		$update_existing  = $this->params['update_existing'];
		$data             = array(
			'imported' => array(),
			'failed'   => array(),
			'updated'  => array(),
			'skipped'  => array(),
		);

		foreach ( $this->parsed_data as $parsed_data_key => $parsed_data ) {
			do_action( 'woocommerce_product_import_before_import', $parsed_data );

			$id         = isset( $parsed_data['id'] ) ? absint( $parsed_data['id'] ) : 0;
			$sku        = isset( $parsed_data['sku'] ) ? $parsed_data['sku'] : '';
			$id_exists  = false;
			$sku_exists = false;

			if ( $id ) {
				$product   = wc_get_product( $id );
				$id_exists = $product && 'importing' !== $product->get_status();
			}

			if ( $sku ) {
				$id_from_sku = wc_get_product_id_by_sku( $sku );
				$product     = $id_from_sku ? wc_get_product( $id_from_sku ) : false;
				$sku_exists  = $product && 'importing' !== $product->get_status();
			}

			if ( $id_exists && ! $update_existing ) {
				$data['skipped'][] = new WP_Error(
					'woocommerce_product_importer_error',
					esc_html__( 'A product with this ID already exists.', 'woocommerce' ),
					array(
						'id'  => $id,
						'row' => $this->get_row_id( $parsed_data ),
					)
				);
				continue;
			}

			if ( $sku_exists && ! $update_existing ) {
				$data['skipped'][] = new WP_Error(
					'woocommerce_product_importer_error',
					esc_html__( 'A product with this SKU already exists.', 'woocommerce' ),
					array(
						'sku' => esc_attr( $sku ),
						'row' => $this->get_row_id( $parsed_data ),
					)
				);
				continue;
			}

			if ( $update_existing && ( isset( $parsed_data['id'] ) || isset( $parsed_data['sku'] ) ) && ! $id_exists && ! $sku_exists ) {
				$data['skipped'][] = new WP_Error(
					'woocommerce_product_importer_error',
					esc_html__( 'No matching product exists to update.', 'woocommerce' ),
					array(
						'id'  => $id,
						'sku' => esc_attr( $sku ),
						'row' => $this->get_row_id( $parsed_data ),
					)
				);
				continue;
			}

			$result = $this->process_item( $parsed_data );

			if ( is_wp_error( $result ) ) {
				$result->add_data( array( 'row' => $this->get_row_id( $parsed_data ) ) );
				$data['failed'][] = $result;
			} elseif ( $result['updated'] ) {
				$data['updated'][] = $result['id'];
			} else {
				$data['imported'][] = $result['id'];
			}

			$index ++;

			if ( $this->params['prevent_timeouts'] && ( $this->time_exceeded() || $this->memory_exceeded() ) ) {
				$this->file_position = $this->file_positions[ $index ];
				break;
			}
		}

		return $data;
	}
}
import/abstract-wc-product-importer.php000064400000053610151336044770014335 0ustar00<?php
/**
 * Abstract Product importer
 *
 * @package  WooCommerce\Import
 * @version  3.1.0
 */

use Automattic\WooCommerce\Utilities\NumberUtil;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Include dependencies.
 */
if ( ! class_exists( 'WC_Importer_Interface', false ) ) {
	include_once WC_ABSPATH . 'includes/interfaces/class-wc-importer-interface.php';
}

/**
 * WC_Product_Importer Class.
 */
abstract class WC_Product_Importer implements WC_Importer_Interface {

	/**
	 * CSV file.
	 *
	 * @var string
	 */
	protected $file = '';

	/**
	 * The file position after the last read.
	 *
	 * @var int
	 */
	protected $file_position = 0;

	/**
	 * Importer parameters.
	 *
	 * @var array
	 */
	protected $params = array();

	/**
	 * Raw keys - CSV raw headers.
	 *
	 * @var array
	 */
	protected $raw_keys = array();

	/**
	 * Mapped keys - CSV headers.
	 *
	 * @var array
	 */
	protected $mapped_keys = array();

	/**
	 * Raw data.
	 *
	 * @var array
	 */
	protected $raw_data = array();

	/**
	 * Raw data.
	 *
	 * @var array
	 */
	protected $file_positions = array();

	/**
	 * Parsed data.
	 *
	 * @var array
	 */
	protected $parsed_data = array();

	/**
	 * Start time of current import.
	 *
	 * (default value: 0)
	 *
	 * @var int
	 */
	protected $start_time = 0;

	/**
	 * Get file raw headers.
	 *
	 * @return array
	 */
	public function get_raw_keys() {
		return $this->raw_keys;
	}

	/**
	 * Get file mapped headers.
	 *
	 * @return array
	 */
	public function get_mapped_keys() {
		return ! empty( $this->mapped_keys ) ? $this->mapped_keys : $this->raw_keys;
	}

	/**
	 * Get raw data.
	 *
	 * @return array
	 */
	public function get_raw_data() {
		return $this->raw_data;
	}

	/**
	 * Get parsed data.
	 *
	 * @return array
	 */
	public function get_parsed_data() {
		/**
		 * Filter product importer parsed data.
		 *
		 * @param array $parsed_data Parsed data.
		 * @param WC_Product_Importer $importer Importer instance.
		 */
		return apply_filters( 'woocommerce_product_importer_parsed_data', $this->parsed_data, $this );
	}

	/**
	 * Get importer parameters.
	 *
	 * @return array
	 */
	public function get_params() {
		return $this->params;
	}

	/**
	 * Get file pointer position from the last read.
	 *
	 * @return int
	 */
	public function get_file_position() {
		return $this->file_position;
	}

	/**
	 * Get file pointer position as a percentage of file size.
	 *
	 * @return int
	 */
	public function get_percent_complete() {
		$size = filesize( $this->file );
		if ( ! $size ) {
			return 0;
		}

		return absint( min( NumberUtil::round( ( $this->file_position / $size ) * 100 ), 100 ) );
	}

	/**
	 * Prepare a single product for create or update.
	 *
	 * @param  array $data     Item data.
	 * @return WC_Product|WP_Error
	 */
	protected function get_product_object( $data ) {
		$id = isset( $data['id'] ) ? absint( $data['id'] ) : 0;

		// Type is the most important part here because we need to be using the correct class and methods.
		if ( isset( $data['type'] ) ) {

			if ( ! array_key_exists( $data['type'], WC_Admin_Exporters::get_product_types() ) ) {
				return new WP_Error( 'woocommerce_product_importer_invalid_type', __( 'Invalid product type.', 'woocommerce' ), array( 'status' => 401 ) );
			}

			try {
				// Prevent getting "variation_invalid_id" error message from Variation Data Store.
				if ( 'variation' === $data['type'] ) {
					$id = wp_update_post(
						array(
							'ID'        => $id,
							'post_type' => 'product_variation',
						)
					);
				}

				$product = wc_get_product_object( $data['type'], $id );
			} catch ( WC_Data_Exception $e ) {
				return new WP_Error( 'woocommerce_product_csv_importer_' . $e->getErrorCode(), $e->getMessage(), array( 'status' => 401 ) );
			}
		} elseif ( ! empty( $data['id'] ) ) {
			$product = wc_get_product( $id );

			if ( ! $product ) {
				return new WP_Error(
					'woocommerce_product_csv_importer_invalid_id',
					/* translators: %d: product ID */
					sprintf( __( 'Invalid product ID %d.', 'woocommerce' ), $id ),
					array(
						'id'     => $id,
						'status' => 401,
					)
				);
			}
		} else {
			$product = wc_get_product_object( 'simple', $id );
		}

		return apply_filters( 'woocommerce_product_import_get_product_object', $product, $data );
	}

	/**
	 * Process a single item and save.
	 *
	 * @throws Exception If item cannot be processed.
	 * @param  array $data Raw CSV data.
	 * @return array|WP_Error
	 */
	protected function process_item( $data ) {
		try {
			do_action( 'woocommerce_product_import_before_process_item', $data );
			$data = apply_filters( 'woocommerce_product_import_process_item_data', $data );

			// Get product ID from SKU if created during the importation.
			if ( empty( $data['id'] ) && ! empty( $data['sku'] ) ) {
				$product_id = wc_get_product_id_by_sku( $data['sku'] );

				if ( $product_id ) {
					$data['id'] = $product_id;
				}
			}

			$object   = $this->get_product_object( $data );
			$updating = false;

			if ( is_wp_error( $object ) ) {
				return $object;
			}

			if ( $object->get_id() && 'importing' !== $object->get_status() ) {
				$updating = true;
			}

			if ( 'external' === $object->get_type() ) {
				unset( $data['manage_stock'], $data['stock_status'], $data['backorders'], $data['low_stock_amount'] );
			}

			if ( 'variation' === $object->get_type() ) {
				if ( isset( $data['status'] ) && -1 === $data['status'] ) {
					$data['status'] = 0; // Variations cannot be drafts - set to private.
				}
			}

			if ( 'importing' === $object->get_status() ) {
				$object->set_status( 'publish' );
				$object->set_slug( '' );
			}

			$result = $object->set_props( array_diff_key( $data, array_flip( array( 'meta_data', 'raw_image_id', 'raw_gallery_image_ids', 'raw_attributes' ) ) ) );

			if ( is_wp_error( $result ) ) {
				throw new Exception( $result->get_error_message() );
			}

			if ( 'variation' === $object->get_type() ) {
				$this->set_variation_data( $object, $data );
			} else {
				$this->set_product_data( $object, $data );
			}

			$this->set_image_data( $object, $data );
			$this->set_meta_data( $object, $data );

			$object = apply_filters( 'woocommerce_product_import_pre_insert_product_object', $object, $data );
			$object->save();

			do_action( 'woocommerce_product_import_inserted_product_object', $object, $data );

			return array(
				'id'      => $object->get_id(),
				'updated' => $updating,
			);
		} catch ( Exception $e ) {
			return new WP_Error( 'woocommerce_product_importer_error', $e->getMessage(), array( 'status' => $e->getCode() ) );
		}
	}

	/**
	 * Convert raw image URLs to IDs and set.
	 *
	 * @param WC_Product $product Product instance.
	 * @param array      $data    Item data.
	 */
	protected function set_image_data( &$product, $data ) {
		// Image URLs need converting to IDs before inserting.
		if ( isset( $data['raw_image_id'] ) ) {
			$product->set_image_id( $this->get_attachment_id_from_url( $data['raw_image_id'], $product->get_id() ) );
		}

		// Gallery image URLs need converting to IDs before inserting.
		if ( isset( $data['raw_gallery_image_ids'] ) ) {
			$gallery_image_ids = array();

			foreach ( $data['raw_gallery_image_ids'] as $image_id ) {
				$gallery_image_ids[] = $this->get_attachment_id_from_url( $image_id, $product->get_id() );
			}
			$product->set_gallery_image_ids( $gallery_image_ids );
		}
	}

	/**
	 * Append meta data.
	 *
	 * @param WC_Product $product Product instance.
	 * @param array      $data    Item data.
	 */
	protected function set_meta_data( &$product, $data ) {
		if ( isset( $data['meta_data'] ) ) {
			foreach ( $data['meta_data'] as $meta ) {
				$product->update_meta_data( $meta['key'], $meta['value'] );
			}
		}
	}

	/**
	 * Set product data.
	 *
	 * @param WC_Product $product Product instance.
	 * @param array      $data    Item data.
	 * @throws Exception If data cannot be set.
	 */
	protected function set_product_data( &$product, $data ) {
		if ( isset( $data['raw_attributes'] ) ) {
			$attributes          = array();
			$default_attributes  = array();
			$existing_attributes = $product->get_attributes();

			foreach ( $data['raw_attributes'] as $position => $attribute ) {
				$attribute_id = 0;

				// Get ID if is a global attribute.
				if ( ! empty( $attribute['taxonomy'] ) ) {
					$attribute_id = $this->get_attribute_taxonomy_id( $attribute['name'] );
				}

				// Set attribute visibility.
				if ( isset( $attribute['visible'] ) ) {
					$is_visible = $attribute['visible'];
				} else {
					$is_visible = 1;
				}

				// Get name.
				$attribute_name = $attribute_id ? wc_attribute_taxonomy_name_by_id( $attribute_id ) : $attribute['name'];

				// Set if is a variation attribute based on existing attributes if possible so updates via CSV do not change this.
				$is_variation = 0;

				if ( $existing_attributes ) {
					foreach ( $existing_attributes as $existing_attribute ) {
						if ( $existing_attribute->get_name() === $attribute_name ) {
							$is_variation = $existing_attribute->get_variation();
							break;
						}
					}
				}

				if ( $attribute_id ) {
					if ( isset( $attribute['value'] ) ) {
						$options = array_map( 'wc_sanitize_term_text_based', $attribute['value'] );
						$options = array_filter( $options, 'strlen' );
					} else {
						$options = array();
					}

					// Check for default attributes and set "is_variation".
					if ( ! empty( $attribute['default'] ) && in_array( $attribute['default'], $options, true ) ) {
						$default_term = get_term_by( 'name', $attribute['default'], $attribute_name );

						if ( $default_term && ! is_wp_error( $default_term ) ) {
							$default = $default_term->slug;
						} else {
							$default = sanitize_title( $attribute['default'] );
						}

						$default_attributes[ $attribute_name ] = $default;
						$is_variation                          = 1;
					}

					if ( ! empty( $options ) ) {
						$attribute_object = new WC_Product_Attribute();
						$attribute_object->set_id( $attribute_id );
						$attribute_object->set_name( $attribute_name );
						$attribute_object->set_options( $options );
						$attribute_object->set_position( $position );
						$attribute_object->set_visible( $is_visible );
						$attribute_object->set_variation( $is_variation );
						$attributes[] = $attribute_object;
					}
				} elseif ( isset( $attribute['value'] ) ) {
					// Check for default attributes and set "is_variation".
					if ( ! empty( $attribute['default'] ) && in_array( $attribute['default'], $attribute['value'], true ) ) {
						$default_attributes[ sanitize_title( $attribute['name'] ) ] = $attribute['default'];
						$is_variation = 1;
					}

					$attribute_object = new WC_Product_Attribute();
					$attribute_object->set_name( $attribute['name'] );
					$attribute_object->set_options( $attribute['value'] );
					$attribute_object->set_position( $position );
					$attribute_object->set_visible( $is_visible );
					$attribute_object->set_variation( $is_variation );
					$attributes[] = $attribute_object;
				}
			}

			$product->set_attributes( $attributes );

			// Set variable default attributes.
			if ( $product->is_type( 'variable' ) ) {
				$product->set_default_attributes( $default_attributes );
			}
		}
	}

	/**
	 * Set variation data.
	 *
	 * @param WC_Product $variation Product instance.
	 * @param array      $data    Item data.
	 * @return WC_Product|WP_Error
	 * @throws Exception If data cannot be set.
	 */
	protected function set_variation_data( &$variation, $data ) {
		$parent = false;

		// Check if parent exist.
		if ( isset( $data['parent_id'] ) ) {
			$parent = wc_get_product( $data['parent_id'] );

			if ( $parent ) {
				$variation->set_parent_id( $parent->get_id() );
			}
		}

		// Stop if parent does not exists.
		if ( ! $parent ) {
			return new WP_Error( 'woocommerce_product_importer_missing_variation_parent_id', __( 'Variation cannot be imported: Missing parent ID or parent does not exist yet.', 'woocommerce' ), array( 'status' => 401 ) );
		}

		// Stop if parent is a product variation.
		if ( $parent->is_type( 'variation' ) ) {
			return new WP_Error( 'woocommerce_product_importer_parent_set_as_variation', __( 'Variation cannot be imported: Parent product cannot be a product variation', 'woocommerce' ), array( 'status' => 401 ) );
		}

		if ( isset( $data['raw_attributes'] ) ) {
			$attributes        = array();
			$parent_attributes = $this->get_variation_parent_attributes( $data['raw_attributes'], $parent );

			foreach ( $data['raw_attributes'] as $attribute ) {
				$attribute_id = 0;

				// Get ID if is a global attribute.
				if ( ! empty( $attribute['taxonomy'] ) ) {
					$attribute_id = $this->get_attribute_taxonomy_id( $attribute['name'] );
				}

				if ( $attribute_id ) {
					$attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
				} else {
					$attribute_name = sanitize_title( $attribute['name'] );
				}

				if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) {
					continue;
				}

				$attribute_key   = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() );
				$attribute_value = isset( $attribute['value'] ) ? current( $attribute['value'] ) : '';

				if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) {
					// If dealing with a taxonomy, we need to get the slug from the name posted to the API.
					$term = get_term_by( 'name', $attribute_value, $attribute_name );

					if ( $term && ! is_wp_error( $term ) ) {
						$attribute_value = $term->slug;
					} else {
						$attribute_value = sanitize_title( $attribute_value );
					}
				}

				$attributes[ $attribute_key ] = $attribute_value;
			}

			$variation->set_attributes( $attributes );
		}
	}

	/**
	 * Get variation parent attributes and set "is_variation".
	 *
	 * @param  array      $attributes Attributes list.
	 * @param  WC_Product $parent     Parent product data.
	 * @return array
	 */
	protected function get_variation_parent_attributes( $attributes, $parent ) {
		$parent_attributes = $parent->get_attributes();
		$require_save      = false;

		foreach ( $attributes as $attribute ) {
			$attribute_id = 0;

			// Get ID if is a global attribute.
			if ( ! empty( $attribute['taxonomy'] ) ) {
				$attribute_id = $this->get_attribute_taxonomy_id( $attribute['name'] );
			}

			if ( $attribute_id ) {
				$attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
			} else {
				$attribute_name = sanitize_title( $attribute['name'] );
			}

			// Check if attribute handle variations.
			if ( isset( $parent_attributes[ $attribute_name ] ) && ! $parent_attributes[ $attribute_name ]->get_variation() ) {
				// Re-create the attribute to CRUD save and generate again.
				$parent_attributes[ $attribute_name ] = clone $parent_attributes[ $attribute_name ];
				$parent_attributes[ $attribute_name ]->set_variation( 1 );

				$require_save = true;
			}
		}

		// Save variation attributes.
		if ( $require_save ) {
			$parent->set_attributes( array_values( $parent_attributes ) );
			$parent->save();
		}

		return $parent_attributes;
	}

	/**
	 * Get attachment ID.
	 *
	 * @param  string $url        Attachment URL.
	 * @param  int    $product_id Product ID.
	 * @return int
	 * @throws Exception If attachment cannot be loaded.
	 */
	public function get_attachment_id_from_url( $url, $product_id ) {
		if ( empty( $url ) ) {
			return 0;
		}

		$id         = 0;
		$upload_dir = wp_upload_dir( null, false );
		$base_url   = $upload_dir['baseurl'] . '/';

		// Check first if attachment is inside the WordPress uploads directory, or we're given a filename only.
		if ( false !== strpos( $url, $base_url ) || false === strpos( $url, '://' ) ) {
			// Search for yyyy/mm/slug.extension or slug.extension - remove the base URL.
			$file = str_replace( $base_url, '', $url );
			$args = array(
				'post_type'   => 'attachment',
				'post_status' => 'any',
				'fields'      => 'ids',
				'meta_query'  => array( // @codingStandardsIgnoreLine.
					'relation' => 'OR',
					array(
						'key'     => '_wp_attached_file',
						'value'   => '^' . $file,
						'compare' => 'REGEXP',
					),
					array(
						'key'     => '_wp_attached_file',
						'value'   => '/' . $file,
						'compare' => 'LIKE',
					),
					array(
						'key'     => '_wc_attachment_source',
						'value'   => '/' . $file,
						'compare' => 'LIKE',
					),
				),
			);
		} else {
			// This is an external URL, so compare to source.
			$args = array(
				'post_type'   => 'attachment',
				'post_status' => 'any',
				'fields'      => 'ids',
				'meta_query'  => array( // @codingStandardsIgnoreLine.
					array(
						'value' => $url,
						'key'   => '_wc_attachment_source',
					),
				),
			);
		}

		$ids = get_posts( $args ); // @codingStandardsIgnoreLine.

		if ( $ids ) {
			$id = current( $ids );
		}

		// Upload if attachment does not exists.
		if ( ! $id && stristr( $url, '://' ) ) {
			$upload = wc_rest_upload_image_from_url( $url );

			if ( is_wp_error( $upload ) ) {
				throw new Exception( $upload->get_error_message(), 400 );
			}

			$id = wc_rest_set_uploaded_image_as_attachment( $upload, $product_id );

			if ( ! wp_attachment_is_image( $id ) ) {
				/* translators: %s: image URL */
				throw new Exception( sprintf( __( 'Not able to attach "%s".', 'woocommerce' ), $url ), 400 );
			}

			// Save attachment source for future reference.
			update_post_meta( $id, '_wc_attachment_source', $url );
		}

		if ( ! $id ) {
			/* translators: %s: image URL */
			throw new Exception( sprintf( __( 'Unable to use image "%s".', 'woocommerce' ), $url ), 400 );
		}

		return $id;
	}

	/**
	 * Get attribute taxonomy ID from the imported data.
	 * If does not exists register a new attribute.
	 *
	 * @param  string $raw_name Attribute name.
	 * @return int
	 * @throws Exception If taxonomy cannot be loaded.
	 */
	public function get_attribute_taxonomy_id( $raw_name ) {
		global $wpdb, $wc_product_attributes;

		// These are exported as labels, so convert the label to a name if possible first.
		$attribute_labels = wp_list_pluck( wc_get_attribute_taxonomies(), 'attribute_label', 'attribute_name' );
		$attribute_name   = array_search( $raw_name, $attribute_labels, true );

		if ( ! $attribute_name ) {
			$attribute_name = wc_sanitize_taxonomy_name( $raw_name );
		}

		$attribute_id = wc_attribute_taxonomy_id_by_name( $attribute_name );

		// Get the ID from the name.
		if ( $attribute_id ) {
			return $attribute_id;
		}

		// If the attribute does not exist, create it.
		$attribute_id = wc_create_attribute(
			array(
				'name'         => $raw_name,
				'slug'         => $attribute_name,
				'type'         => 'select',
				'order_by'     => 'menu_order',
				'has_archives' => false,
			)
		);

		if ( is_wp_error( $attribute_id ) ) {
			throw new Exception( $attribute_id->get_error_message(), 400 );
		}

		// Register as taxonomy while importing.
		$taxonomy_name = wc_attribute_taxonomy_name( $attribute_name );
		register_taxonomy(
			$taxonomy_name,
			apply_filters( 'woocommerce_taxonomy_objects_' . $taxonomy_name, array( 'product' ) ),
			apply_filters(
				'woocommerce_taxonomy_args_' . $taxonomy_name,
				array(
					'labels'       => array(
						'name' => $raw_name,
					),
					'hierarchical' => true,
					'show_ui'      => false,
					'query_var'    => true,
					'rewrite'      => false,
				)
			)
		);

		// Set product attributes global.
		$wc_product_attributes = array();

		foreach ( wc_get_attribute_taxonomies() as $taxonomy ) {
			$wc_product_attributes[ wc_attribute_taxonomy_name( $taxonomy->attribute_name ) ] = $taxonomy;
		}

		return $attribute_id;
	}

	/**
	 * Memory exceeded
	 *
	 * Ensures the batch process never exceeds 90%
	 * of the maximum WordPress memory.
	 *
	 * @return bool
	 */
	protected function memory_exceeded() {
		$memory_limit   = $this->get_memory_limit() * 0.9; // 90% of max memory
		$current_memory = memory_get_usage( true );
		$return         = false;
		if ( $current_memory >= $memory_limit ) {
			$return = true;
		}
		return apply_filters( 'woocommerce_product_importer_memory_exceeded', $return );
	}

	/**
	 * Get memory limit
	 *
	 * @return int
	 */
	protected function get_memory_limit() {
		if ( function_exists( 'ini_get' ) ) {
			$memory_limit = ini_get( 'memory_limit' );
		} else {
			// Sensible default.
			$memory_limit = '128M';
		}

		if ( ! $memory_limit || -1 === intval( $memory_limit ) ) {
			// Unlimited, set to 32GB.
			$memory_limit = '32000M';
		}
		return intval( $memory_limit ) * 1024 * 1024;
	}

	/**
	 * Time exceeded.
	 *
	 * Ensures the batch never exceeds a sensible time limit.
	 * A timeout limit of 30s is common on shared hosting.
	 *
	 * @return bool
	 */
	protected function time_exceeded() {
		$finish = $this->start_time + apply_filters( 'woocommerce_product_importer_default_time_limit', 20 ); // 20 seconds
		$return = false;
		if ( time() >= $finish ) {
			$return = true;
		}
		return apply_filters( 'woocommerce_product_importer_time_exceeded', $return );
	}

	/**
	 * Explode CSV cell values using commas by default, and handling escaped
	 * separators.
	 *
	 * @since  3.2.0
	 * @param  string $value     Value to explode.
	 * @param  string $separator Separator separating each value. Defaults to comma.
	 * @return array
	 */
	protected function explode_values( $value, $separator = ',' ) {
		$value  = str_replace( '\\,', '::separator::', $value );
		$values = explode( $separator, $value );
		$values = array_map( array( $this, 'explode_values_formatter' ), $values );

		return $values;
	}

	/**
	 * Remove formatting and trim each value.
	 *
	 * @since  3.2.0
	 * @param  string $value Value to format.
	 * @return string
	 */
	protected function explode_values_formatter( $value ) {
		return trim( str_replace( '::separator::', ',', $value ) );
	}

	/**
	 * The exporter prepends a ' to escape fields that start with =, +, - or @.
	 * Remove the prepended ' character preceding those characters.
	 *
	 * @since 3.5.2
	 * @param  string $value A string that may or may not have been escaped with '.
	 * @return string
	 */
	protected function unescape_data( $value ) {
		$active_content_triggers = array( "'=", "'+", "'-", "'@" );

		if ( in_array( mb_substr( $value, 0, 2 ), $active_content_triggers, true ) ) {
			$value = mb_substr( $value, 1 );
		}

		return $value;
	}

}
wc-formatting-functions.php000064400000125721151336044770012066 0ustar00<?php
/**
 * WooCommerce Formatting
 *
 * Functions for formatting data.
 *
 * @package WooCommerce\Functions
 * @version 2.1.0
 */

use Automattic\WooCommerce\Utilities\NumberUtil;

defined( 'ABSPATH' ) || exit;

/**
 * Converts a string (e.g. 'yes' or 'no') to a bool.
 *
 * @since 3.0.0
 * @param string|bool $string String to convert. If a bool is passed it will be returned as-is.
 * @return bool
 */
function wc_string_to_bool( $string ) {
	return is_bool( $string ) ? $string : ( 'yes' === strtolower( $string ) || 1 === $string || 'true' === strtolower( $string ) || '1' === $string );
}

/**
 * Converts a bool to a 'yes' or 'no'.
 *
 * @since 3.0.0
 * @param bool|string $bool Bool to convert. If a string is passed it will first be converted to a bool.
 * @return string
 */
function wc_bool_to_string( $bool ) {
	if ( ! is_bool( $bool ) ) {
		$bool = wc_string_to_bool( $bool );
	}
	return true === $bool ? 'yes' : 'no';
}

/**
 * Explode a string into an array by $delimiter and remove empty values.
 *
 * @since 3.0.0
 * @param string $string    String to convert.
 * @param string $delimiter Delimiter, defaults to ','.
 * @return array
 */
function wc_string_to_array( $string, $delimiter = ',' ) {
	return is_array( $string ) ? $string : array_filter( explode( $delimiter, $string ) );
}

/**
 * Sanitize taxonomy names. Slug format (no spaces, lowercase).
 * Urldecode is used to reverse munging of UTF8 characters.
 *
 * @param string $taxonomy Taxonomy name.
 * @return string
 */
function wc_sanitize_taxonomy_name( $taxonomy ) {
	return apply_filters( 'sanitize_taxonomy_name', urldecode( sanitize_title( urldecode( $taxonomy ) ) ), $taxonomy );
}

/**
 * Sanitize permalink values before insertion into DB.
 *
 * Cannot use wc_clean because it sometimes strips % chars and breaks the user's setting.
 *
 * @since  2.6.0
 * @param  string $value Permalink.
 * @return string
 */
function wc_sanitize_permalink( $value ) {
	global $wpdb;

	$value = $wpdb->strip_invalid_text_for_column( $wpdb->options, 'option_value', $value );

	if ( is_wp_error( $value ) ) {
		$value = '';
	}

	$value = esc_url_raw( trim( $value ) );
	$value = str_replace( 'http://', '', $value );
	return untrailingslashit( $value );
}

/**
 * Gets the filename part of a download URL.
 *
 * @param string $file_url File URL.
 * @return string
 */
function wc_get_filename_from_url( $file_url ) {
	$parts = wp_parse_url( $file_url );
	if ( isset( $parts['path'] ) ) {
		return basename( $parts['path'] );
	}
}

/**
 * Normalise dimensions, unify to cm then convert to wanted unit value.
 *
 * Usage:
 * wc_get_dimension( 55, 'in' );
 * wc_get_dimension( 55, 'in', 'm' );
 *
 * @param int|float $dimension    Dimension.
 * @param string    $to_unit      Unit to convert to.
 *                                Options: 'in', 'm', 'cm', 'm'.
 * @param string    $from_unit    Unit to convert from.
 *                                Defaults to ''.
 *                                Options: 'in', 'm', 'cm', 'm'.
 * @return float
 */
function wc_get_dimension( $dimension, $to_unit, $from_unit = '' ) {
	$to_unit = strtolower( $to_unit );

	if ( empty( $from_unit ) ) {
		$from_unit = strtolower( get_option( 'woocommerce_dimension_unit' ) );
	}

	// Unify all units to cm first.
	if ( $from_unit !== $to_unit ) {
		switch ( $from_unit ) {
			case 'in':
				$dimension *= 2.54;
				break;
			case 'm':
				$dimension *= 100;
				break;
			case 'mm':
				$dimension *= 0.1;
				break;
			case 'yd':
				$dimension *= 91.44;
				break;
		}

		// Output desired unit.
		switch ( $to_unit ) {
			case 'in':
				$dimension *= 0.3937;
				break;
			case 'm':
				$dimension *= 0.01;
				break;
			case 'mm':
				$dimension *= 10;
				break;
			case 'yd':
				$dimension *= 0.010936133;
				break;
		}
	}

	return ( $dimension < 0 ) ? 0 : $dimension;
}

/**
 * Normalise weights, unify to kg then convert to wanted unit value.
 *
 * Usage:
 * wc_get_weight(55, 'kg');
 * wc_get_weight(55, 'kg', 'lbs');
 *
 * @param int|float $weight    Weight.
 * @param string    $to_unit   Unit to convert to.
 *                             Options: 'g', 'kg', 'lbs', 'oz'.
 * @param string    $from_unit Unit to convert from.
 *                             Defaults to ''.
 *                             Options: 'g', 'kg', 'lbs', 'oz'.
 * @return float
 */
function wc_get_weight( $weight, $to_unit, $from_unit = '' ) {
	$weight  = (float) $weight;
	$to_unit = strtolower( $to_unit );

	if ( empty( $from_unit ) ) {
		$from_unit = strtolower( get_option( 'woocommerce_weight_unit' ) );
	}

	// Unify all units to kg first.
	if ( $from_unit !== $to_unit ) {
		switch ( $from_unit ) {
			case 'g':
				$weight *= 0.001;
				break;
			case 'lbs':
				$weight *= 0.453592;
				break;
			case 'oz':
				$weight *= 0.0283495;
				break;
		}

		// Output desired unit.
		switch ( $to_unit ) {
			case 'g':
				$weight *= 1000;
				break;
			case 'lbs':
				$weight *= 2.20462;
				break;
			case 'oz':
				$weight *= 35.274;
				break;
		}
	}

	return ( $weight < 0 ) ? 0 : $weight;
}

/**
 * Trim trailing zeros off prices.
 *
 * @param string|float|int $price Price.
 * @return string
 */
function wc_trim_zeros( $price ) {
	return preg_replace( '/' . preg_quote( wc_get_price_decimal_separator(), '/' ) . '0++$/', '', $price );
}

/**
 * Round a tax amount.
 *
 * @param  double $value Amount to round.
 * @param  int    $precision DP to round. Defaults to wc_get_price_decimals.
 * @return float
 */
function wc_round_tax_total( $value, $precision = null ) {
	$precision = is_null( $precision ) ? wc_get_price_decimals() : intval( $precision );

	if ( version_compare( PHP_VERSION, '5.3.0', '>=' ) ) {
		$rounded_tax = NumberUtil::round( $value, $precision, wc_get_tax_rounding_mode() ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctionParameters.round_modeFound
	} elseif ( 2 === wc_get_tax_rounding_mode() ) {
		$rounded_tax = wc_legacy_round_half_down( $value, $precision );
	} else {
		$rounded_tax = NumberUtil::round( $value, $precision );
	}

	return apply_filters( 'wc_round_tax_total', $rounded_tax, $value, $precision, WC_TAX_ROUNDING_MODE );
}

/**
 * Round half down in PHP 5.2.
 *
 * @since 3.2.6
 * @param float $value Value to round.
 * @param int   $precision Precision to round down to.
 * @return float
 */
function wc_legacy_round_half_down( $value, $precision ) {
	$value = wc_float_to_string( $value );

	if ( false !== strstr( $value, '.' ) ) {
		$value = explode( '.', $value );

		if ( strlen( $value[1] ) > $precision && substr( $value[1], -1 ) === '5' ) {
			$value[1] = substr( $value[1], 0, -1 ) . '4';
		}

		$value = implode( '.', $value );
	}

	return NumberUtil::round( floatval( $value ), $precision );
}

/**
 * Make a refund total negative.
 *
 * @param float $amount Refunded amount.
 *
 * @return float
 */
function wc_format_refund_total( $amount ) {
	return $amount * -1;
}

/**
 * Format decimal numbers ready for DB storage.
 *
 * Sanitize, optionally remove decimals, and optionally round + trim off zeros.
 *
 * This function does not remove thousands - this should be done before passing a value to the function.
 *
 * @param  float|string $number     Expects either a float or a string with a decimal separator only (no thousands).
 * @param  mixed        $dp number  Number of decimal points to use, blank to use woocommerce_price_num_decimals, or false to avoid all rounding.
 * @param  bool         $trim_zeros From end of string.
 * @return string
 */
function wc_format_decimal( $number, $dp = false, $trim_zeros = false ) {
	$locale   = localeconv();
	$decimals = array( wc_get_price_decimal_separator(), $locale['decimal_point'], $locale['mon_decimal_point'] );

	// Remove locale from string.
	if ( ! is_float( $number ) ) {
		$number = str_replace( $decimals, '.', $number );

		// Convert multiple dots to just one.
		$number = preg_replace( '/\.(?![^.]+$)|[^0-9.-]/', '', wc_clean( $number ) );
	}

	if ( false !== $dp ) {
		$dp     = intval( '' === $dp ? wc_get_price_decimals() : $dp );
		$number = number_format( floatval( $number ), $dp, '.', '' );
	} elseif ( is_float( $number ) ) {
		// DP is false - don't use number format, just return a string using whatever is given. Remove scientific notation using sprintf.
		$number = str_replace( $decimals, '.', sprintf( '%.' . wc_get_rounding_precision() . 'f', $number ) );
		// We already had a float, so trailing zeros are not needed.
		$trim_zeros = true;
	}

	if ( $trim_zeros && strstr( $number, '.' ) ) {
		$number = rtrim( rtrim( $number, '0' ), '.' );
	}

	return $number;
}

/**
 * Convert a float to a string without locale formatting which PHP adds when changing floats to strings.
 *
 * @param  float $float Float value to format.
 * @return string
 */
function wc_float_to_string( $float ) {
	if ( ! is_float( $float ) ) {
		return $float;
	}

	$locale = localeconv();
	$string = strval( $float );
	$string = str_replace( $locale['decimal_point'], '.', $string );

	return $string;
}

/**
 * Format a price with WC Currency Locale settings.
 *
 * @param  string $value Price to localize.
 * @return string
 */
function wc_format_localized_price( $value ) {
	return apply_filters( 'woocommerce_format_localized_price', str_replace( '.', wc_get_price_decimal_separator(), strval( $value ) ), $value );
}

/**
 * Format a decimal with PHP Locale settings.
 *
 * @param  string $value Decimal to localize.
 * @return string
 */
function wc_format_localized_decimal( $value ) {
	$locale = localeconv();
	return apply_filters( 'woocommerce_format_localized_decimal', str_replace( '.', $locale['decimal_point'], strval( $value ) ), $value );
}

/**
 * Format a coupon code.
 *
 * @since  3.0.0
 * @param  string $value Coupon code to format.
 * @return string
 */
function wc_format_coupon_code( $value ) {
	return apply_filters( 'woocommerce_coupon_code', $value );
}

/**
 * Sanitize a coupon code.
 *
 * Uses sanitize_post_field since coupon codes are stored as
 * post_titles - the sanitization and escaping must match.
 *
 * @since  3.6.0
 * @param  string $value Coupon code to format.
 * @return string
 */
function wc_sanitize_coupon_code( $value ) {
	return wp_filter_kses( sanitize_post_field( 'post_title', $value, 0, 'db' ) );
}

/**
 * Clean variables using sanitize_text_field. Arrays are cleaned recursively.
 * Non-scalar values are ignored.
 *
 * @param string|array $var Data to sanitize.
 * @return string|array
 */
function wc_clean( $var ) {
	if ( is_array( $var ) ) {
		return array_map( 'wc_clean', $var );
	} else {
		return is_scalar( $var ) ? sanitize_text_field( $var ) : $var;
	}
}

/**
 * Function wp_check_invalid_utf8 with recursive array support.
 *
 * @param string|array $var Data to sanitize.
 * @return string|array
 */
function wc_check_invalid_utf8( $var ) {
	if ( is_array( $var ) ) {
		return array_map( 'wc_check_invalid_utf8', $var );
	} else {
		return wp_check_invalid_utf8( $var );
	}
}

/**
 * Run wc_clean over posted textarea but maintain line breaks.
 *
 * @since  3.0.0
 * @param  string $var Data to sanitize.
 * @return string
 */
function wc_sanitize_textarea( $var ) {
	return implode( "\n", array_map( 'wc_clean', explode( "\n", $var ) ) );
}

/**
 * Sanitize a string destined to be a tooltip.
 *
 * @since  2.3.10 Tooltips are encoded with htmlspecialchars to prevent XSS. Should not be used in conjunction with esc_attr()
 * @param  string $var Data to sanitize.
 * @return string
 */
function wc_sanitize_tooltip( $var ) {
	return htmlspecialchars(
		wp_kses(
			html_entity_decode( $var ),
			array(
				'br'     => array(),
				'em'     => array(),
				'strong' => array(),
				'small'  => array(),
				'span'   => array(),
				'ul'     => array(),
				'li'     => array(),
				'ol'     => array(),
				'p'      => array(),
			)
		)
	);
}

/**
 * Merge two arrays.
 *
 * @param array $a1 First array to merge.
 * @param array $a2 Second array to merge.
 * @return array
 */
function wc_array_overlay( $a1, $a2 ) {
	foreach ( $a1 as $k => $v ) {
		if ( ! array_key_exists( $k, $a2 ) ) {
			continue;
		}
		if ( is_array( $v ) && is_array( $a2[ $k ] ) ) {
			$a1[ $k ] = wc_array_overlay( $v, $a2[ $k ] );
		} else {
			$a1[ $k ] = $a2[ $k ];
		}
	}
	return $a1;
}

/**
 * Formats a stock amount by running it through a filter.
 *
 * @param  int|float $amount Stock amount.
 * @return int|float
 */
function wc_stock_amount( $amount ) {
	return apply_filters( 'woocommerce_stock_amount', $amount );
}

/**
 * Get the price format depending on the currency position.
 *
 * @return string
 */
function get_woocommerce_price_format() {
	$currency_pos = get_option( 'woocommerce_currency_pos' );
	$format       = '%1$s%2$s';

	switch ( $currency_pos ) {
		case 'left':
			$format = '%1$s%2$s';
			break;
		case 'right':
			$format = '%2$s%1$s';
			break;
		case 'left_space':
			$format = '%1$s&nbsp;%2$s';
			break;
		case 'right_space':
			$format = '%2$s&nbsp;%1$s';
			break;
	}

	return apply_filters( 'woocommerce_price_format', $format, $currency_pos );
}

/**
 * Return the thousand separator for prices.
 *
 * @since  2.3
 * @return string
 */
function wc_get_price_thousand_separator() {
	return stripslashes( apply_filters( 'wc_get_price_thousand_separator', get_option( 'woocommerce_price_thousand_sep' ) ) );
}

/**
 * Return the decimal separator for prices.
 *
 * @since  2.3
 * @return string
 */
function wc_get_price_decimal_separator() {
	$separator = apply_filters( 'wc_get_price_decimal_separator', get_option( 'woocommerce_price_decimal_sep' ) );
	return $separator ? stripslashes( $separator ) : '.';
}

/**
 * Return the number of decimals after the decimal point.
 *
 * @since  2.3
 * @return int
 */
function wc_get_price_decimals() {
	return absint( apply_filters( 'wc_get_price_decimals', get_option( 'woocommerce_price_num_decimals', 2 ) ) );
}

/**
 * Format the price with a currency symbol.
 *
 * @param  float $price Raw price.
 * @param  array $args  Arguments to format a price {
 *     Array of arguments.
 *     Defaults to empty array.
 *
 *     @type bool   $ex_tax_label       Adds exclude tax label.
 *                                      Defaults to false.
 *     @type string $currency           Currency code.
 *                                      Defaults to empty string (Use the result from get_woocommerce_currency()).
 *     @type string $decimal_separator  Decimal separator.
 *                                      Defaults the result of wc_get_price_decimal_separator().
 *     @type string $thousand_separator Thousand separator.
 *                                      Defaults the result of wc_get_price_thousand_separator().
 *     @type string $decimals           Number of decimals.
 *                                      Defaults the result of wc_get_price_decimals().
 *     @type string $price_format       Price format depending on the currency position.
 *                                      Defaults the result of get_woocommerce_price_format().
 * }
 * @return string
 */
function wc_price( $price, $args = array() ) {
	$args = apply_filters(
		'wc_price_args',
		wp_parse_args(
			$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(),
			)
		)
	);

	$original_price = $price;

	// Convert to float to avoid issues on PHP 8.
	$price = (float) $price;

	$unformatted_price = $price;
	$negative          = $price < 0;

	/**
	 * Filter raw price.
	 *
	 * @param float        $raw_price      Raw price.
	 * @param float|string $original_price Original price as float, or empty string. Since 5.0.0.
	 */
	$price = apply_filters( 'raw_woocommerce_price', $negative ? $price * -1 : $price, $original_price );

	/**
	 * Filter formatted price.
	 *
	 * @param float        $formatted_price    Formatted price.
	 * @param float        $price              Unformatted price.
	 * @param int          $decimals           Number of decimals.
	 * @param string       $decimal_separator  Decimal separator.
	 * @param string       $thousand_separator Thousand separator.
	 * @param float|string $original_price     Original price as float, or empty string. Since 5.0.0.
	 */
	$price = apply_filters( 'formatted_woocommerce_price', number_format( $price, $args['decimals'], $args['decimal_separator'], $args['thousand_separator'] ), $price, $args['decimals'], $args['decimal_separator'], $args['thousand_separator'], $original_price );

	if ( apply_filters( 'woocommerce_price_trim_zeros', false ) && $args['decimals'] > 0 ) {
		$price = wc_trim_zeros( $price );
	}

	$formatted_price = ( $negative ? '-' : '' ) . sprintf( $args['price_format'], '<span class="woocommerce-Price-currencySymbol">' . get_woocommerce_currency_symbol( $args['currency'] ) . '</span>', $price );
	$return          = '<span class="woocommerce-Price-amount amount"><bdi>' . $formatted_price . '</bdi></span>';

	if ( $args['ex_tax_label'] && wc_tax_enabled() ) {
		$return .= ' <small class="woocommerce-Price-taxLabel tax_label">' . WC()->countries->ex_tax_or_vat() . '</small>';
	}

	/**
	 * Filters the string of price markup.
	 *
	 * @param string       $return            Price HTML markup.
	 * @param string       $price             Formatted price.
	 * @param array        $args              Pass on the args.
	 * @param float        $unformatted_price Price as float to allow plugins custom formatting. Since 3.2.0.
	 * @param float|string $original_price    Original price as float, or empty string. Since 5.0.0.
	 */
	return apply_filters( 'wc_price', $return, $price, $args, $unformatted_price, $original_price );
}

/**
 * Notation to numbers.
 *
 * This function transforms the php.ini notation for numbers (like '2M') to an integer.
 *
 * @param  string $size Size value.
 * @return int
 */
function wc_let_to_num( $size ) {
	$l   = substr( $size, -1 );
	$ret = (int) substr( $size, 0, -1 );
	switch ( strtoupper( $l ) ) {
		case 'P':
			$ret *= 1024;
			// No break.
		case 'T':
			$ret *= 1024;
			// No break.
		case 'G':
			$ret *= 1024;
			// No break.
		case 'M':
			$ret *= 1024;
			// No break.
		case 'K':
			$ret *= 1024;
			// No break.
	}
	return $ret;
}

/**
 * WooCommerce Date Format - Allows to change date format for everything WooCommerce.
 *
 * @return string
 */
function wc_date_format() {
	$date_format = get_option( 'date_format' );
	if ( empty( $date_format ) ) {
		// Return default date format if the option is empty.
		$date_format = 'F j, Y';
	}
	return apply_filters( 'woocommerce_date_format', $date_format );
}

/**
 * WooCommerce Time Format - Allows to change time format for everything WooCommerce.
 *
 * @return string
 */
function wc_time_format() {
	$time_format = get_option( 'time_format' );
	if ( empty( $time_format ) ) {
		// Return default time format if the option is empty.
		$time_format = 'g:i a';
	}
	return apply_filters( 'woocommerce_time_format', $time_format );
}

/**
 * Convert mysql datetime to PHP timestamp, forcing UTC. Wrapper for strtotime.
 *
 * Based on wcs_strtotime_dark_knight() from WC Subscriptions by Prospress.
 *
 * @since  3.0.0
 * @param  string   $time_string    Time string.
 * @param  int|null $from_timestamp Timestamp to convert from.
 * @return int
 */
function wc_string_to_timestamp( $time_string, $from_timestamp = null ) {
	$original_timezone = date_default_timezone_get();

	// @codingStandardsIgnoreStart
	date_default_timezone_set( 'UTC' );

	if ( null === $from_timestamp ) {
		$next_timestamp = strtotime( $time_string );
	} else {
		$next_timestamp = strtotime( $time_string, $from_timestamp );
	}

	date_default_timezone_set( $original_timezone );
	// @codingStandardsIgnoreEnd

	return $next_timestamp;
}

/**
 * Convert a date string to a WC_DateTime.
 *
 * @since  3.1.0
 * @param  string $time_string Time string.
 * @return WC_DateTime
 */
function wc_string_to_datetime( $time_string ) {
	// Strings are defined in local WP timezone. Convert to UTC.
	if ( 1 === preg_match( '/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(Z|((-|\+)\d{2}:\d{2}))$/', $time_string, $date_bits ) ) {
		$offset    = ! empty( $date_bits[7] ) ? iso8601_timezone_to_offset( $date_bits[7] ) : wc_timezone_offset();
		$timestamp = gmmktime( $date_bits[4], $date_bits[5], $date_bits[6], $date_bits[2], $date_bits[3], $date_bits[1] ) - $offset;
	} else {
		$timestamp = wc_string_to_timestamp( get_gmt_from_date( gmdate( 'Y-m-d H:i:s', wc_string_to_timestamp( $time_string ) ) ) );
	}
	$datetime = new WC_DateTime( "@{$timestamp}", new DateTimeZone( 'UTC' ) );

	// Set local timezone or offset.
	if ( get_option( 'timezone_string' ) ) {
		$datetime->setTimezone( new DateTimeZone( wc_timezone_string() ) );
	} else {
		$datetime->set_utc_offset( wc_timezone_offset() );
	}

	return $datetime;
}

/**
 * WooCommerce Timezone - helper to retrieve the timezone string for a site until.
 * a WP core method exists (see https://core.trac.wordpress.org/ticket/24730).
 *
 * Adapted from https://secure.php.net/manual/en/function.timezone-name-from-abbr.php#89155.
 *
 * @since 2.1
 * @return string PHP timezone string for the site
 */
function wc_timezone_string() {
	// Added in WordPress 5.3 Ref https://developer.wordpress.org/reference/functions/wp_timezone_string/.
	if ( function_exists( 'wp_timezone_string' ) ) {
		return wp_timezone_string();
	}

	// If site timezone string exists, return it.
	$timezone = get_option( 'timezone_string' );
	if ( $timezone ) {
		return $timezone;
	}

	// Get UTC offset, if it isn't set then return UTC.
	$utc_offset = floatval( get_option( 'gmt_offset', 0 ) );
	if ( ! is_numeric( $utc_offset ) || 0.0 === $utc_offset ) {
		return 'UTC';
	}

	// Adjust UTC offset from hours to seconds.
	$utc_offset = (int) ( $utc_offset * 3600 );

	// Attempt to guess the timezone string from the UTC offset.
	$timezone = timezone_name_from_abbr( '', $utc_offset );
	if ( $timezone ) {
		return $timezone;
	}

	// Last try, guess timezone string manually.
	foreach ( timezone_abbreviations_list() as $abbr ) {
		foreach ( $abbr as $city ) {
			// WordPress restrict the use of date(), since it's affected by timezone settings, but in this case is just what we need to guess the correct timezone.
			if ( (bool) date( 'I' ) === (bool) $city['dst'] && $city['timezone_id'] && intval( $city['offset'] ) === $utc_offset ) { // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
				return $city['timezone_id'];
			}
		}
	}

	// Fallback to UTC.
	return 'UTC';
}

/**
 * Get timezone offset in seconds.
 *
 * @since  3.0.0
 * @return float
 */
function wc_timezone_offset() {
	$timezone = get_option( 'timezone_string' );

	if ( $timezone ) {
		$timezone_object = new DateTimeZone( $timezone );
		return $timezone_object->getOffset( new DateTime( 'now' ) );
	} else {
		return floatval( get_option( 'gmt_offset', 0 ) ) * HOUR_IN_SECONDS;
	}
}

/**
 * Callback which can flatten post meta (gets the first value if it's an array).
 *
 * @since  3.0.0
 * @param  array $value Value to flatten.
 * @return mixed
 */
function wc_flatten_meta_callback( $value ) {
	return is_array( $value ) ? current( $value ) : $value;
}

if ( ! function_exists( 'wc_rgb_from_hex' ) ) {

	/**
	 * Convert RGB to HEX.
	 *
	 * @param mixed $color Color.
	 *
	 * @return array
	 */
	function wc_rgb_from_hex( $color ) {
		$color = str_replace( '#', '', $color );
		// Convert shorthand colors to full format, e.g. "FFF" -> "FFFFFF".
		$color = preg_replace( '~^(.)(.)(.)$~', '$1$1$2$2$3$3', $color );

		$rgb      = array();
		$rgb['R'] = hexdec( $color[0] . $color[1] );
		$rgb['G'] = hexdec( $color[2] . $color[3] );
		$rgb['B'] = hexdec( $color[4] . $color[5] );

		return $rgb;
	}
}

if ( ! function_exists( 'wc_hex_darker' ) ) {

	/**
	 * Make HEX color darker.
	 *
	 * @param mixed $color  Color.
	 * @param int   $factor Darker factor.
	 *                      Defaults to 30.
	 * @return string
	 */
	function wc_hex_darker( $color, $factor = 30 ) {
		$base  = wc_rgb_from_hex( $color );
		$color = '#';

		foreach ( $base as $k => $v ) {
			$amount      = $v / 100;
			$amount      = NumberUtil::round( $amount * $factor );
			$new_decimal = $v - $amount;

			$new_hex_component = dechex( $new_decimal );
			if ( strlen( $new_hex_component ) < 2 ) {
				$new_hex_component = '0' . $new_hex_component;
			}
			$color .= $new_hex_component;
		}

		return $color;
	}
}

if ( ! function_exists( 'wc_hex_lighter' ) ) {

	/**
	 * Make HEX color lighter.
	 *
	 * @param mixed $color  Color.
	 * @param int   $factor Lighter factor.
	 *                      Defaults to 30.
	 * @return string
	 */
	function wc_hex_lighter( $color, $factor = 30 ) {
		$base  = wc_rgb_from_hex( $color );
		$color = '#';

		foreach ( $base as $k => $v ) {
			$amount      = 255 - $v;
			$amount      = $amount / 100;
			$amount      = NumberUtil::round( $amount * $factor );
			$new_decimal = $v + $amount;

			$new_hex_component = dechex( $new_decimal );
			if ( strlen( $new_hex_component ) < 2 ) {
				$new_hex_component = '0' . $new_hex_component;
			}
			$color .= $new_hex_component;
		}

		return $color;
	}
}

if ( ! function_exists( 'wc_hex_is_light' ) ) {

	/**
	 * Determine whether a hex color is light.
	 *
	 * @param mixed $color Color.
	 * @return bool  True if a light color.
	 */
	function wc_hex_is_light( $color ) {
		$hex = str_replace( '#', '', $color );

		$c_r = hexdec( substr( $hex, 0, 2 ) );
		$c_g = hexdec( substr( $hex, 2, 2 ) );
		$c_b = hexdec( substr( $hex, 4, 2 ) );

		$brightness = ( ( $c_r * 299 ) + ( $c_g * 587 ) + ( $c_b * 114 ) ) / 1000;

		return $brightness > 155;
	}
}

if ( ! function_exists( 'wc_light_or_dark' ) ) {

	/**
	 * Detect if we should use a light or dark color on a background color.
	 *
	 * @param mixed  $color Color.
	 * @param string $dark  Darkest reference.
	 *                      Defaults to '#000000'.
	 * @param string $light Lightest reference.
	 *                      Defaults to '#FFFFFF'.
	 * @return string
	 */
	function wc_light_or_dark( $color, $dark = '#000000', $light = '#FFFFFF' ) {
		return wc_hex_is_light( $color ) ? $dark : $light;
	}
}

if ( ! function_exists( 'wc_format_hex' ) ) {

	/**
	 * Format string as hex.
	 *
	 * @param string $hex HEX color.
	 * @return string|null
	 */
	function wc_format_hex( $hex ) {
		$hex = trim( str_replace( '#', '', $hex ) );

		if ( strlen( $hex ) === 3 ) {
			$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
		}

		return $hex ? '#' . $hex : null;
	}
}

/**
 * Format the postcode according to the country and length of the postcode.
 *
 * @param string $postcode Unformatted postcode.
 * @param string $country  Base country.
 * @return string
 */
function wc_format_postcode( $postcode, $country ) {
	$postcode = wc_normalize_postcode( $postcode );

	switch ( $country ) {
		case 'CA':
		case 'GB':
			$postcode = substr_replace( $postcode, ' ', -3, 0 );
			break;
		case 'IE':
			$postcode = substr_replace( $postcode, ' ', 3, 0 );
			break;
		case 'BR':
		case 'PL':
			$postcode = substr_replace( $postcode, '-', -3, 0 );
			break;
		case 'JP':
			$postcode = substr_replace( $postcode, '-', 3, 0 );
			break;
		case 'PT':
			$postcode = substr_replace( $postcode, '-', 4, 0 );
			break;
		case 'PR':
		case 'US':
			$postcode = rtrim( substr_replace( $postcode, '-', 5, 0 ), '-' );
			break;
		case 'NL':
			$postcode = substr_replace( $postcode, ' ', 4, 0 );
			break;
	}

	return apply_filters( 'woocommerce_format_postcode', trim( $postcode ), $country );
}

/**
 * Normalize postcodes.
 *
 * Remove spaces and convert characters to uppercase.
 *
 * @since 2.6.0
 * @param string $postcode Postcode.
 * @return string
 */
function wc_normalize_postcode( $postcode ) {
	return preg_replace( '/[\s\-]/', '', trim( wc_strtoupper( $postcode ) ) );
}

/**
 * Format phone numbers.
 *
 * @param string $phone Phone number.
 * @return string
 */
function wc_format_phone_number( $phone ) {
	if ( ! WC_Validation::is_phone( $phone ) ) {
		return '';
	}
	return preg_replace( '/[^0-9\+\-\(\)\s]/', '-', preg_replace( '/[\x00-\x1F\x7F-\xFF]/', '', $phone ) );
}

/**
 * Sanitize phone number.
 * Allows only numbers and "+" (plus sign).
 *
 * @since 3.6.0
 * @param string $phone Phone number.
 * @return string
 */
function wc_sanitize_phone_number( $phone ) {
	return preg_replace( '/[^\d+]/', '', $phone );
}

/**
 * Wrapper for mb_strtoupper which see's if supported first.
 *
 * @since  3.1.0
 * @param  string $string String to format.
 * @return string
 */
function wc_strtoupper( $string ) {
	return function_exists( 'mb_strtoupper' ) ? mb_strtoupper( $string ) : strtoupper( $string );
}

/**
 * Make a string lowercase.
 * Try to use mb_strtolower() when available.
 *
 * @since  2.3
 * @param  string $string String to format.
 * @return string
 */
function wc_strtolower( $string ) {
	return function_exists( 'mb_strtolower' ) ? mb_strtolower( $string ) : strtolower( $string );
}

/**
 * Trim a string and append a suffix.
 *
 * @param  string  $string String to trim.
 * @param  integer $chars  Amount of characters.
 *                         Defaults to 200.
 * @param  string  $suffix Suffix.
 *                         Defaults to '...'.
 * @return string
 */
function wc_trim_string( $string, $chars = 200, $suffix = '...' ) {
	if ( strlen( $string ) > $chars ) {
		if ( function_exists( 'mb_substr' ) ) {
			$string = mb_substr( $string, 0, ( $chars - mb_strlen( $suffix ) ) ) . $suffix;
		} else {
			$string = substr( $string, 0, ( $chars - strlen( $suffix ) ) ) . $suffix;
		}
	}
	return $string;
}

/**
 * Format content to display shortcodes.
 *
 * @since  2.3.0
 * @param  string $raw_string Raw string.
 * @return string
 */
function wc_format_content( $raw_string ) {
	return apply_filters( 'woocommerce_format_content', apply_filters( 'woocommerce_short_description', $raw_string ), $raw_string );
}

/**
 * Format product short description.
 * Adds support for Jetpack Markdown.
 *
 * @codeCoverageIgnore
 * @since  2.4.0
 * @param  string $content Product short description.
 * @return string
 */
function wc_format_product_short_description( $content ) {
	// Add support for Jetpack Markdown.
	if ( class_exists( 'WPCom_Markdown' ) ) {
		$markdown = WPCom_Markdown::get_instance();

		return wpautop(
			$markdown->transform(
				$content,
				array(
					'unslash' => false,
				)
			)
		);
	}

	return $content;
}

/**
 * Formats curency symbols when saved in settings.
 *
 * @codeCoverageIgnore
 * @param  string $value     Option value.
 * @param  array  $option    Option name.
 * @param  string $raw_value Raw value.
 * @return string
 */
function wc_format_option_price_separators( $value, $option, $raw_value ) {
	return wp_kses_post( $raw_value );
}
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_price_decimal_sep', 'wc_format_option_price_separators', 10, 3 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_price_thousand_sep', 'wc_format_option_price_separators', 10, 3 );

/**
 * Formats decimals when saved in settings.
 *
 * @codeCoverageIgnore
 * @param  string $value     Option value.
 * @param  array  $option    Option name.
 * @param  string $raw_value Raw value.
 * @return string
 */
function wc_format_option_price_num_decimals( $value, $option, $raw_value ) {
	return is_null( $raw_value ) ? 2 : absint( $raw_value );
}
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_price_num_decimals', 'wc_format_option_price_num_decimals', 10, 3 );

/**
 * Formats hold stock option and sets cron event up.
 *
 * @codeCoverageIgnore
 * @param  string $value     Option value.
 * @param  array  $option    Option name.
 * @param  string $raw_value Raw value.
 * @return string
 */
function wc_format_option_hold_stock_minutes( $value, $option, $raw_value ) {
	$value = ! empty( $raw_value ) ? absint( $raw_value ) : ''; // Allow > 0 or set to ''.

	wp_clear_scheduled_hook( 'woocommerce_cancel_unpaid_orders' );

	if ( '' !== $value ) {
		$cancel_unpaid_interval = apply_filters( 'woocommerce_cancel_unpaid_orders_interval_minutes', absint( $value ) );
		wp_schedule_single_event( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders' );
	}

	return $value;
}
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_hold_stock_minutes', 'wc_format_option_hold_stock_minutes', 10, 3 );

/**
 * Sanitize terms from an attribute text based.
 *
 * @since  2.4.5
 * @param  string $term Term value.
 * @return string
 */
function wc_sanitize_term_text_based( $term ) {
	return trim( wp_strip_all_tags( wp_unslash( $term ) ) );
}

if ( ! function_exists( 'wc_make_numeric_postcode' ) ) {
	/**
	 * Make numeric postcode.
	 *
	 * Converts letters to numbers so we can do a simple range check on postcodes.
	 * E.g. PE30 becomes 16050300 (P = 16, E = 05, 3 = 03, 0 = 00)
	 *
	 * @since 2.6.0
	 * @param string $postcode Regular postcode.
	 * @return string
	 */
	function wc_make_numeric_postcode( $postcode ) {
		$postcode           = str_replace( array( ' ', '-' ), '', $postcode );
		$postcode_length    = strlen( $postcode );
		$letters_to_numbers = array_merge( array( 0 ), range( 'A', 'Z' ) );
		$letters_to_numbers = array_flip( $letters_to_numbers );
		$numeric_postcode   = '';

		for ( $i = 0; $i < $postcode_length; $i ++ ) {
			if ( is_numeric( $postcode[ $i ] ) ) {
				$numeric_postcode .= str_pad( $postcode[ $i ], 2, '0', STR_PAD_LEFT );
			} elseif ( isset( $letters_to_numbers[ $postcode[ $i ] ] ) ) {
				$numeric_postcode .= str_pad( $letters_to_numbers[ $postcode[ $i ] ], 2, '0', STR_PAD_LEFT );
			} else {
				$numeric_postcode .= '00';
			}
		}

		return $numeric_postcode;
	}
}

/**
 * Format the stock amount ready for display based on settings.
 *
 * @since  3.0.0
 * @param  WC_Product $product Product object for which the stock you need to format.
 * @return string
 */
function wc_format_stock_for_display( $product ) {
	$display      = __( 'In stock', 'woocommerce' );
	$stock_amount = $product->get_stock_quantity();

	switch ( get_option( 'woocommerce_stock_format' ) ) {
		case 'low_amount':
			if ( $stock_amount <= wc_get_low_stock_amount( $product ) ) {
				/* translators: %s: stock amount */
				$display = sprintf( __( 'Only %s left in stock', 'woocommerce' ), wc_format_stock_quantity_for_display( $stock_amount, $product ) );
			}
			break;
		case '':
			/* translators: %s: stock amount */
			$display = sprintf( __( '%s in stock', 'woocommerce' ), wc_format_stock_quantity_for_display( $stock_amount, $product ) );
			break;
	}

	if ( $product->backorders_allowed() && $product->backorders_require_notification() ) {
		$display .= ' ' . __( '(can be backordered)', 'woocommerce' );
	}

	return $display;
}

/**
 * Format the stock quantity ready for display.
 *
 * @since  3.0.0
 * @param  int        $stock_quantity Stock quantity.
 * @param  WC_Product $product        Product instance so that we can pass through the filters.
 * @return string
 */
function wc_format_stock_quantity_for_display( $stock_quantity, $product ) {
	return apply_filters( 'woocommerce_format_stock_quantity', $stock_quantity, $product );
}

/**
 * Format a sale price for display.
 *
 * @since  3.0.0
 * @param  string $regular_price Regular price.
 * @param  string $sale_price    Sale price.
 * @return string
 */
function wc_format_sale_price( $regular_price, $sale_price ) {
	$price = '<del aria-hidden="true">' . ( is_numeric( $regular_price ) ? wc_price( $regular_price ) : $regular_price ) . '</del> <ins>' . ( is_numeric( $sale_price ) ? wc_price( $sale_price ) : $sale_price ) . '</ins>';
	return apply_filters( 'woocommerce_format_sale_price', $price, $regular_price, $sale_price );
}

/**
 * Format a price range for display.
 *
 * @param  string $from Price from.
 * @param  string $to   Price to.
 * @return string
 */
function wc_format_price_range( $from, $to ) {
	/* translators: 1: price from 2: price to */
	$price = sprintf( _x( '%1$s &ndash; %2$s', 'Price range: from-to', 'woocommerce' ), is_numeric( $from ) ? wc_price( $from ) : $from, is_numeric( $to ) ? wc_price( $to ) : $to );
	return apply_filters( 'woocommerce_format_price_range', $price, $from, $to );
}

/**
 * Format a weight for display.
 *
 * @since  3.0.0
 * @param  float $weight Weight.
 * @return string
 */
function wc_format_weight( $weight ) {
	$weight_string = wc_format_localized_decimal( $weight );

	if ( ! empty( $weight_string ) ) {
		$weight_string .= ' ' . get_option( 'woocommerce_weight_unit' );
	} else {
		$weight_string = __( 'N/A', 'woocommerce' );
	}

	return apply_filters( 'woocommerce_format_weight', $weight_string, $weight );
}

/**
 * Format dimensions for display.
 *
 * @since  3.0.0
 * @param  array $dimensions Array of dimensions.
 * @return string
 */
function wc_format_dimensions( $dimensions ) {
	$dimension_string = implode( ' &times; ', array_filter( array_map( 'wc_format_localized_decimal', $dimensions ) ) );

	if ( ! empty( $dimension_string ) ) {
		$dimension_string .= ' ' . get_option( 'woocommerce_dimension_unit' );
	} else {
		$dimension_string = __( 'N/A', 'woocommerce' );
	}

	return apply_filters( 'woocommerce_format_dimensions', $dimension_string, $dimensions );
}

/**
 * Format a date for output.
 *
 * @since  3.0.0
 * @param  WC_DateTime $date   Instance of WC_DateTime.
 * @param  string      $format Data format.
 *                             Defaults to the wc_date_format function if not set.
 * @return string
 */
function wc_format_datetime( $date, $format = '' ) {
	if ( ! $format ) {
		$format = wc_date_format();
	}
	if ( ! is_a( $date, 'WC_DateTime' ) ) {
		return '';
	}
	return $date->date_i18n( $format );
}

/**
 * Process oEmbeds.
 *
 * @since  3.1.0
 * @param  string $content Content.
 * @return string
 */
function wc_do_oembeds( $content ) {
	global $wp_embed;

	$content = $wp_embed->autoembed( $content );

	return $content;
}

/**
 * Get part of a string before :.
 *
 * Used for example in shipping methods ids where they take the format
 * method_id:instance_id
 *
 * @since  3.2.0
 * @param  string $string String to extract.
 * @return string
 */
function wc_get_string_before_colon( $string ) {
	return trim( current( explode( ':', (string) $string ) ) );
}

/**
 * Array merge and sum function.
 *
 * Source:  https://gist.github.com/Nickology/f700e319cbafab5eaedc
 *
 * @since 3.2.0
 * @return array
 */
function wc_array_merge_recursive_numeric() {
	$arrays = func_get_args();

	// If there's only one array, it's already merged.
	if ( 1 === count( $arrays ) ) {
		return $arrays[0];
	}

	// Remove any items in $arrays that are NOT arrays.
	foreach ( $arrays as $key => $array ) {
		if ( ! is_array( $array ) ) {
			unset( $arrays[ $key ] );
		}
	}

	// We start by setting the first array as our final array.
	// We will merge all other arrays with this one.
	$final = array_shift( $arrays );

	foreach ( $arrays as $b ) {
		foreach ( $final as $key => $value ) {
			// If $key does not exist in $b, then it is unique and can be safely merged.
			if ( ! isset( $b[ $key ] ) ) {
				$final[ $key ] = $value;
			} else {
				// If $key is present in $b, then we need to merge and sum numeric values in both.
				if ( is_numeric( $value ) && is_numeric( $b[ $key ] ) ) {
					// If both values for these keys are numeric, we sum them.
					$final[ $key ] = $value + $b[ $key ];
				} elseif ( is_array( $value ) && is_array( $b[ $key ] ) ) {
					// If both values are arrays, we recursively call ourself.
					$final[ $key ] = wc_array_merge_recursive_numeric( $value, $b[ $key ] );
				} else {
					// If both keys exist but differ in type, then we cannot merge them.
					// In this scenario, we will $b's value for $key is used.
					$final[ $key ] = $b[ $key ];
				}
			}
		}

		// Finally, we need to merge any keys that exist only in $b.
		foreach ( $b as $key => $value ) {
			if ( ! isset( $final[ $key ] ) ) {
				$final[ $key ] = $value;
			}
		}
	}

	return $final;
}

/**
 * Implode and escape HTML attributes for output.
 *
 * @since 3.3.0
 * @param array $raw_attributes Attribute name value pairs.
 * @return string
 */
function wc_implode_html_attributes( $raw_attributes ) {
	$attributes = array();
	foreach ( $raw_attributes as $name => $value ) {
		$attributes[] = esc_attr( $name ) . '="' . esc_attr( $value ) . '"';
	}
	return implode( ' ', $attributes );
}

/**
 * Escape JSON for use on HTML or attribute text nodes.
 *
 * @since 3.5.5
 * @param string $json JSON to escape.
 * @param bool   $html True if escaping for HTML text node, false for attributes. Determines how quotes are handled.
 * @return string Escaped JSON.
 */
function wc_esc_json( $json, $html = false ) {
	return _wp_specialchars(
		$json,
		$html ? ENT_NOQUOTES : ENT_QUOTES, // Escape quotes in attribute nodes only.
		'UTF-8',                           // json_encode() outputs UTF-8 (really just ASCII), not the blog's charset.
		true                               // Double escape entities: `&amp;` -> `&amp;amp;`.
	);
}

/**
 * Parse a relative date option from the settings API into a standard format.
 *
 * @since 3.4.0
 * @param mixed $raw_value Value stored in DB.
 * @return array Nicely formatted array with number and unit values.
 */
function wc_parse_relative_date_option( $raw_value ) {
	$periods = array(
		'days'   => __( 'Day(s)', 'woocommerce' ),
		'weeks'  => __( 'Week(s)', 'woocommerce' ),
		'months' => __( 'Month(s)', 'woocommerce' ),
		'years'  => __( 'Year(s)', 'woocommerce' ),
	);

	$value = wp_parse_args(
		(array) $raw_value,
		array(
			'number' => '',
			'unit'   => 'days',
		)
	);

	$value['number'] = ! empty( $value['number'] ) ? absint( $value['number'] ) : '';

	if ( ! in_array( $value['unit'], array_keys( $periods ), true ) ) {
		$value['unit'] = 'days';
	}

	return $value;
}

/**
 * Format the endpoint slug, strip out anything not allowed in a url.
 *
 * @since 3.5.0
 * @param string $raw_value The raw value.
 * @return string
 */
function wc_sanitize_endpoint_slug( $raw_value ) {
	return sanitize_title( $raw_value );
}
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_checkout_pay_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_checkout_order_received_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_add_payment_method_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_delete_payment_method_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_set_default_payment_method_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_orders_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_view_order_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_downloads_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_edit_account_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_edit_address_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_payment_methods_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_lost_password_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_logout_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
class-wc-product-external.php000064400000011533151336044770012304 0ustar00<?php
/**
 * External Product
 *
 * External products cannot be bought; they link offsite. Extends simple products.
 *
 * @package WooCommerce\Classes\Products
 * @version 3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Product external class.
 */
class WC_Product_External extends WC_Product {

	/**
	 * Stores product data.
	 *
	 * @var array
	 */
	protected $extra_data = array(
		'product_url' => '',
		'button_text' => '',
	);

	/**
	 * Get internal type.
	 *
	 * @return string
	 */
	public function get_type() {
		return 'external';
	}

	/*
	|--------------------------------------------------------------------------
	| Getters
	|--------------------------------------------------------------------------
	|
	| Methods for getting data from the product object.
	*/

	/**
	 * Get product url.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_product_url( $context = 'view' ) {
		return esc_url_raw( $this->get_prop( 'product_url', $context ) );
	}

	/**
	 * Get button text.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return string
	 */
	public function get_button_text( $context = 'view' ) {
		return $this->get_prop( 'button_text', $context );
	}

	/*
	|--------------------------------------------------------------------------
	| Setters
	|--------------------------------------------------------------------------
	|
	| Functions for setting product data. These should not update anything in the
	| database itself and should only change what is stored in the class
	| object.
	*/

	/**
	 * Set product URL.
	 *
	 * @since 3.0.0
	 * @param string $product_url Product URL.
	 */
	public function set_product_url( $product_url ) {
		$this->set_prop( 'product_url', htmlspecialchars_decode( $product_url ) );
	}

	/**
	 * Set button text.
	 *
	 * @since 3.0.0
	 * @param string $button_text Button text.
	 */
	public function set_button_text( $button_text ) {
		$this->set_prop( 'button_text', $button_text );
	}

	/**
	 * External products cannot be stock managed.
	 *
	 * @since 3.0.0
	 * @param bool $manage_stock If manage stock.
	 */
	public function set_manage_stock( $manage_stock ) {
		$this->set_prop( 'manage_stock', false );

		if ( true === $manage_stock ) {
			$this->error( 'product_external_invalid_manage_stock', __( 'External products cannot be stock managed.', 'woocommerce' ) );
		}
	}

	/**
	 * External products cannot be stock managed.
	 *
	 * @since 3.0.0
	 *
	 * @param string $stock_status Stock status.
	 */
	public function set_stock_status( $stock_status = '' ) {
		$this->set_prop( 'stock_status', 'instock' );

		if ( 'instock' !== $stock_status ) {
			$this->error( 'product_external_invalid_stock_status', __( 'External products cannot be stock managed.', 'woocommerce' ) );
		}
	}

	/**
	 * External products cannot be backordered.
	 *
	 * @since 3.0.0
	 * @param string $backorders Options: 'yes', 'no' or 'notify'.
	 */
	public function set_backorders( $backorders ) {
		$this->set_prop( 'backorders', 'no' );

		if ( 'no' !== $backorders ) {
			$this->error( 'product_external_invalid_backorders', __( 'External products cannot be backordered.', 'woocommerce' ) );
		}
	}

	/*
	|--------------------------------------------------------------------------
	| Other Actions
	|--------------------------------------------------------------------------
	*/

	/**
	 * Returns false if the product cannot be bought.
	 *
	 * @access public
	 * @return bool
	 */
	public function is_purchasable() {
		return apply_filters( 'woocommerce_is_purchasable', false, $this );
	}

	/**
	 * Get the add to url used mainly in loops.
	 *
	 * @access public
	 * @return string
	 */
	public function add_to_cart_url() {
		return apply_filters( 'woocommerce_product_add_to_cart_url', $this->get_product_url(), $this );
	}

	/**
	 * Get the add to cart button text for the single page.
	 *
	 * @access public
	 * @return string
	 */
	public function single_add_to_cart_text() {
		return apply_filters( 'woocommerce_product_single_add_to_cart_text', $this->get_button_text() ? $this->get_button_text() : _x( 'Buy product', 'placeholder', 'woocommerce' ), $this );
	}

	/**
	 * Get the add to cart button text.
	 *
	 * @access public
	 * @return string
	 */
	public function add_to_cart_text() {
		return apply_filters( 'woocommerce_product_add_to_cart_text', $this->get_button_text() ? $this->get_button_text() : _x( 'Buy product', 'placeholder', 'woocommerce' ), $this );
	}

	/**
	 * Get the add to cart button text description - used in aria tags.
	 *
	 * @since 3.3.0
	 * @return string
	 */
	public function add_to_cart_description() {
		/* translators: %s: Product title */
		return apply_filters( 'woocommerce_product_add_to_cart_description', $this->get_button_text() ? $this->get_button_text() : sprintf( __( 'Buy &ldquo;%s&rdquo;', 'woocommerce' ), $this->get_name() ), $this );
	}
}
class-wc-payment-gateways.php000064400000012651151336044770012305 0ustar00<?php
/**
 * WooCommerce Payment Gateways
 *
 * Loads payment gateways via hooks for use in the store.
 *
 * @version 2.2.0
 * @package WooCommerce\Classes\Payment
 */

defined( 'ABSPATH' ) || exit;

/**
 * Payment gateways class.
 */
class WC_Payment_Gateways {

	/**
	 * Payment gateway classes.
	 *
	 * @var array
	 */
	public $payment_gateways = array();

	/**
	 * The single instance of the class.
	 *
	 * @var WC_Payment_Gateways
	 * @since 2.1.0
	 */
	protected static $_instance = null;

	/**
	 * Main WC_Payment_Gateways Instance.
	 *
	 * Ensures only one instance of WC_Payment_Gateways is loaded or can be loaded.
	 *
	 * @since 2.1
	 * @return WC_Payment_Gateways Main instance
	 */
	public static function instance() {
		if ( is_null( self::$_instance ) ) {
			self::$_instance = new self();
		}
		return self::$_instance;
	}

	/**
	 * Cloning is forbidden.
	 *
	 * @since 2.1
	 */
	public function __clone() {
		wc_doing_it_wrong( __FUNCTION__, __( 'Cloning is forbidden.', 'woocommerce' ), '2.1' );
	}

	/**
	 * Unserializing instances of this class is forbidden.
	 *
	 * @since 2.1
	 */
	public function __wakeup() {
		wc_doing_it_wrong( __FUNCTION__, __( 'Unserializing instances of this class is forbidden.', 'woocommerce' ), '2.1' );
	}

	/**
	 * Initialize payment gateways.
	 */
	public function __construct() {
		$this->init();
	}

	/**
	 * Load gateways and hook in functions.
	 */
	public function init() {
		$load_gateways = array(
			'WC_Gateway_BACS',
			'WC_Gateway_Cheque',
			'WC_Gateway_COD',
		);

		if ( $this->should_load_paypal_standard() ) {
			$load_gateways[] = 'WC_Gateway_Paypal';
		}

		// Filter.
		$load_gateways = apply_filters( 'woocommerce_payment_gateways', $load_gateways );

		// Get sort order option.
		$ordering  = (array) get_option( 'woocommerce_gateway_order' );
		$order_end = 999;

		// Load gateways in order.
		foreach ( $load_gateways as $gateway ) {
			if ( is_string( $gateway ) && class_exists( $gateway ) ) {
				$gateway = new $gateway();
			}

			// Gateways need to be valid and extend WC_Payment_Gateway.
			if ( ! is_a( $gateway, 'WC_Payment_Gateway' ) ) {
				continue;
			}

			if ( isset( $ordering[ $gateway->id ] ) && is_numeric( $ordering[ $gateway->id ] ) ) {
				// Add in position.
				$this->payment_gateways[ $ordering[ $gateway->id ] ] = $gateway;
			} else {
				// Add to end of the array.
				$this->payment_gateways[ $order_end ] = $gateway;
				$order_end++;
			}
		}

		ksort( $this->payment_gateways );
	}

	/**
	 * Get gateways.
	 *
	 * @return array
	 */
	public function payment_gateways() {
		$_available_gateways = array();

		if ( count( $this->payment_gateways ) > 0 ) {
			foreach ( $this->payment_gateways as $gateway ) {
				$_available_gateways[ $gateway->id ] = $gateway;
			}
		}

		return $_available_gateways;
	}

	/**
	 * Get array of registered gateway ids
	 *
	 * @since 2.6.0
	 * @return array of strings
	 */
	public function get_payment_gateway_ids() {
		return wp_list_pluck( $this->payment_gateways, 'id' );
	}

	/**
	 * Get available gateways.
	 *
	 * @return array
	 */
	public function get_available_payment_gateways() {
		$_available_gateways = array();

		foreach ( $this->payment_gateways as $gateway ) {
			if ( $gateway->is_available() ) {
				if ( ! is_add_payment_method_page() ) {
					$_available_gateways[ $gateway->id ] = $gateway;
				} elseif ( $gateway->supports( 'add_payment_method' ) || $gateway->supports( 'tokenization' ) ) {
					$_available_gateways[ $gateway->id ] = $gateway;
				}
			}
		}

		return array_filter( (array) apply_filters( 'woocommerce_available_payment_gateways', $_available_gateways ), array( $this, 'filter_valid_gateway_class' ) );
	}

	/**
	 * Callback for array filter. Returns true if gateway is of correct type.
	 *
	 * @since 3.6.0
	 * @param object $gateway Gateway to check.
	 * @return bool
	 */
	protected function filter_valid_gateway_class( $gateway ) {
		return $gateway && is_a( $gateway, 'WC_Payment_Gateway' );
	}

	/**
	 * Set the current, active gateway.
	 *
	 * @param array $gateways Available payment gateways.
	 */
	public function set_current_gateway( $gateways ) {
		// Be on the defensive.
		if ( ! is_array( $gateways ) || empty( $gateways ) ) {
			return;
		}

		$current_gateway = false;

		if ( WC()->session ) {
			$current = WC()->session->get( 'chosen_payment_method' );

			if ( $current && isset( $gateways[ $current ] ) ) {
				$current_gateway = $gateways[ $current ];
			}
		}

		if ( ! $current_gateway ) {
			$current_gateway = current( $gateways );
		}

		// Ensure we can make a call to set_current() without triggering an error.
		if ( $current_gateway && is_callable( array( $current_gateway, 'set_current' ) ) ) {
			$current_gateway->set_current();
		}
	}

	/**
	 * Save options in admin.
	 */
	public function process_admin_options() {
		$gateway_order = isset( $_POST['gateway_order'] ) ? wc_clean( wp_unslash( $_POST['gateway_order'] ) ) : ''; // WPCS: input var ok, CSRF ok.
		$order         = array();

		if ( is_array( $gateway_order ) && count( $gateway_order ) > 0 ) {
			$loop = 0;
			foreach ( $gateway_order as $gateway_id ) {
				$order[ esc_attr( $gateway_id ) ] = $loop;
				$loop++;
			}
		}

		update_option( 'woocommerce_gateway_order', $order );
	}

	/**
	 * Determines if PayPal Standard should be loaded.
	 *
	 * @since 5.5.0
	 * @return bool Whether PayPal Standard should be loaded or not.
	 */
	protected function should_load_paypal_standard() {
		$paypal = new WC_Gateway_Paypal();
		return $paypal->should_load();
	}
}
class-wc-integrations.php000064400000002434151336044770011512 0ustar00<?php
/**
 * WooCommerce Integrations class
 *
 * Loads Integrations into WooCommerce.
 *
 * @version 3.9.0
 * @package WooCommerce\Classes\Integrations
 */

defined( 'ABSPATH' ) || exit;

/**
 * Integrations class.
 */
class WC_Integrations {

	/**
	 * Array of integrations.
	 *
	 * @var array
	 */
	public $integrations = array();

	/**
	 * Initialize integrations.
	 */
	public function __construct() {

		do_action( 'woocommerce_integrations_init' );

		$load_integrations = array(
			'WC_Integration_MaxMind_Geolocation',
		);

		$load_integrations = apply_filters( 'woocommerce_integrations', $load_integrations );

		// Load integration classes.
		foreach ( $load_integrations as $integration ) {

			$load_integration = new $integration();

			$this->integrations[ $load_integration->id ] = $load_integration;
		}
	}

	/**
	 * Return loaded integrations.
	 *
	 * @return array
	 */
	public function get_integrations() {
		return $this->integrations;
	}

	/**
	 * Return a desired integration.
	 *
	 * @since 3.9.0
	 * @param string $id The id of the integration to get.
	 * @return mixed|null The integration if one is found, otherwise null.
	 */
	public function get_integration( $id ) {
		if ( isset( $this->integrations[ $id ] ) ) {
			return $this->integrations[ $id ];
		}

		return null;
	}
}
class-wc-background-updater.php000064400000006717151336044770012575 0ustar00<?php
/**
 * Background Updater
 *
 * @version 2.6.0
 * @deprecated 3.6.0 Replaced with queue.
 * @package WooCommerce\Classes
 */

defined( 'ABSPATH' ) || exit;

if ( ! class_exists( 'WC_Background_Process', false ) ) {
	include_once dirname( __FILE__ ) . '/abstracts/class-wc-background-process.php';
}

/**
 * WC_Background_Updater Class.
 */
class WC_Background_Updater extends WC_Background_Process {

	/**
	 * Initiate new background process.
	 */
	public function __construct() {
		// Uses unique prefix per blog so each blog has separate queue.
		$this->prefix = 'wp_' . get_current_blog_id();
		$this->action = 'wc_updater';

		parent::__construct();
	}

	/**
	 * Dispatch updater.
	 *
	 * Updater will still run via cron job if this fails for any reason.
	 */
	public function dispatch() {
		$dispatched = parent::dispatch();
		$logger     = wc_get_logger();

		if ( is_wp_error( $dispatched ) ) {
			$logger->error(
				sprintf( 'Unable to dispatch WooCommerce updater: %s', $dispatched->get_error_message() ),
				array( 'source' => 'wc_db_updates' )
			);
		}
	}

	/**
	 * Handle cron healthcheck
	 *
	 * Restart the background process if not already running
	 * and data exists in the queue.
	 */
	public function handle_cron_healthcheck() {
		if ( $this->is_process_running() ) {
			// Background process already running.
			return;
		}

		if ( $this->is_queue_empty() ) {
			// No data to process.
			$this->clear_scheduled_event();
			return;
		}

		$this->handle();
	}

	/**
	 * Schedule fallback event.
	 */
	protected function schedule_event() {
		if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) {
			wp_schedule_event( time() + 10, $this->cron_interval_identifier, $this->cron_hook_identifier );
		}
	}

	/**
	 * Is the updater running?
	 *
	 * @return boolean
	 */
	public function is_updating() {
		return false === $this->is_queue_empty();
	}

	/**
	 * Task
	 *
	 * Override this method to perform any actions required on each
	 * queue item. Return the modified item for further processing
	 * in the next pass through. Or, return false to remove the
	 * item from the queue.
	 *
	 * @param string $callback Update callback function.
	 * @return string|bool
	 */
	protected function task( $callback ) {
		wc_maybe_define_constant( 'WC_UPDATING', true );

		$logger = wc_get_logger();

		include_once dirname( __FILE__ ) . '/wc-update-functions.php';

		$result = false;

		if ( is_callable( $callback ) ) {
			$logger->info( sprintf( 'Running %s callback', $callback ), array( 'source' => 'wc_db_updates' ) );
			$result = (bool) call_user_func( $callback, $this );

			if ( $result ) {
				$logger->info( sprintf( '%s callback needs to run again', $callback ), array( 'source' => 'wc_db_updates' ) );
			} else {
				$logger->info( sprintf( 'Finished running %s callback', $callback ), array( 'source' => 'wc_db_updates' ) );
			}
		} else {
			$logger->notice( sprintf( 'Could not find %s callback', $callback ), array( 'source' => 'wc_db_updates' ) );
		}

		return $result ? $callback : false;
	}

	/**
	 * Complete
	 *
	 * Override if applicable, but ensure that the below actions are
	 * performed, or, call parent::complete().
	 */
	protected function complete() {
		$logger = wc_get_logger();
		$logger->info( 'Data update complete', array( 'source' => 'wc_db_updates' ) );
		WC_Install::update_db_version();
		parent::complete();
	}

	/**
	 * See if the batch limit has been exceeded.
	 *
	 * @return bool
	 */
	public function is_memory_exceeded() {
		return $this->memory_exceeded();
	}
}
class-wc-rest-exception.php000064400000000424151336044770011752 0ustar00<?php
/**
 * WooCommerce REST Exception Class
 *
 * Extends Exception to provide additional data.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * WC_REST_Exception class.
 */
class WC_REST_Exception extends WC_Data_Exception {}
gateways/cod/class-wc-gateway-cod.php000064400000031205151336044770013577 0ustar00<?php
/**
 * Class WC_Gateway_COD file.
 *
 * @package WooCommerce\Gateways
 */

use Automattic\Jetpack\Constants;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Cash on Delivery Gateway.
 *
 * Provides a Cash on Delivery Payment Gateway.
 *
 * @class       WC_Gateway_COD
 * @extends     WC_Payment_Gateway
 * @version     2.1.0
 * @package     WooCommerce\Classes\Payment
 */
class WC_Gateway_COD extends WC_Payment_Gateway {

	/**
	 * Constructor for the gateway.
	 */
	public function __construct() {
		// Setup general properties.
		$this->setup_properties();

		// Load the settings.
		$this->init_form_fields();
		$this->init_settings();

		// Get settings.
		$this->title              = $this->get_option( 'title' );
		$this->description        = $this->get_option( 'description' );
		$this->instructions       = $this->get_option( 'instructions' );
		$this->enable_for_methods = $this->get_option( 'enable_for_methods', array() );
		$this->enable_for_virtual = $this->get_option( 'enable_for_virtual', 'yes' ) === 'yes';

		add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) );
		add_action( 'woocommerce_thankyou_' . $this->id, array( $this, 'thankyou_page' ) );
		add_filter( 'woocommerce_payment_complete_order_status', array( $this, 'change_payment_complete_order_status' ), 10, 3 );

		// Customer Emails.
		add_action( 'woocommerce_email_before_order_table', array( $this, 'email_instructions' ), 10, 3 );
	}

	/**
	 * Setup general properties for the gateway.
	 */
	protected function setup_properties() {
		$this->id                 = 'cod';
		$this->icon               = apply_filters( 'woocommerce_cod_icon', '' );
		$this->method_title       = __( 'Cash on delivery', 'woocommerce' );
		$this->method_description = __( 'Have your customers pay with cash (or by other means) upon delivery.', 'woocommerce' );
		$this->has_fields         = false;
	}

	/**
	 * Initialise Gateway Settings Form Fields.
	 */
	public function init_form_fields() {
		$this->form_fields = array(
			'enabled'            => array(
				'title'       => __( 'Enable/Disable', 'woocommerce' ),
				'label'       => __( 'Enable cash on delivery', 'woocommerce' ),
				'type'        => 'checkbox',
				'description' => '',
				'default'     => 'no',
			),
			'title'              => array(
				'title'       => __( 'Title', 'woocommerce' ),
				'type'        => 'text',
				'description' => __( 'Payment method description that the customer will see on your checkout.', 'woocommerce' ),
				'default'     => __( 'Cash on delivery', 'woocommerce' ),
				'desc_tip'    => true,
			),
			'description'        => array(
				'title'       => __( 'Description', 'woocommerce' ),
				'type'        => 'textarea',
				'description' => __( 'Payment method description that the customer will see on your website.', 'woocommerce' ),
				'default'     => __( 'Pay with cash upon delivery.', 'woocommerce' ),
				'desc_tip'    => true,
			),
			'instructions'       => array(
				'title'       => __( 'Instructions', 'woocommerce' ),
				'type'        => 'textarea',
				'description' => __( 'Instructions that will be added to the thank you page.', 'woocommerce' ),
				'default'     => __( 'Pay with cash upon delivery.', 'woocommerce' ),
				'desc_tip'    => true,
			),
			'enable_for_methods' => array(
				'title'             => __( 'Enable for shipping methods', 'woocommerce' ),
				'type'              => 'multiselect',
				'class'             => 'wc-enhanced-select',
				'css'               => 'width: 400px;',
				'default'           => '',
				'description'       => __( 'If COD is only available for certain methods, set it up here. Leave blank to enable for all methods.', 'woocommerce' ),
				'options'           => $this->load_shipping_method_options(),
				'desc_tip'          => true,
				'custom_attributes' => array(
					'data-placeholder' => __( 'Select shipping methods', 'woocommerce' ),
				),
			),
			'enable_for_virtual' => array(
				'title'   => __( 'Accept for virtual orders', 'woocommerce' ),
				'label'   => __( 'Accept COD if the order is virtual', 'woocommerce' ),
				'type'    => 'checkbox',
				'default' => 'yes',
			),
		);
	}

	/**
	 * Check If The Gateway Is Available For Use.
	 *
	 * @return bool
	 */
	public function is_available() {
		$order          = null;
		$needs_shipping = false;

		// Test if shipping is needed first.
		if ( WC()->cart && WC()->cart->needs_shipping() ) {
			$needs_shipping = true;
		} elseif ( is_page( wc_get_page_id( 'checkout' ) ) && 0 < get_query_var( 'order-pay' ) ) {
			$order_id = absint( get_query_var( 'order-pay' ) );
			$order    = wc_get_order( $order_id );

			// Test if order needs shipping.
			if ( $order && 0 < count( $order->get_items() ) ) {
				foreach ( $order->get_items() as $item ) {
					$_product = $item->get_product();
					if ( $_product && $_product->needs_shipping() ) {
						$needs_shipping = true;
						break;
					}
				}
			}
		}

		$needs_shipping = apply_filters( 'woocommerce_cart_needs_shipping', $needs_shipping );

		// Virtual order, with virtual disabled.
		if ( ! $this->enable_for_virtual && ! $needs_shipping ) {
			return false;
		}

		// Only apply if all packages are being shipped via chosen method, or order is virtual.
		if ( ! empty( $this->enable_for_methods ) && $needs_shipping ) {
			$order_shipping_items            = is_object( $order ) ? $order->get_shipping_methods() : false;
			$chosen_shipping_methods_session = WC()->session->get( 'chosen_shipping_methods' );

			if ( $order_shipping_items ) {
				$canonical_rate_ids = $this->get_canonical_order_shipping_item_rate_ids( $order_shipping_items );
			} else {
				$canonical_rate_ids = $this->get_canonical_package_rate_ids( $chosen_shipping_methods_session );
			}

			if ( ! count( $this->get_matching_rates( $canonical_rate_ids ) ) ) {
				return false;
			}
		}

		return parent::is_available();
	}

	/**
	 * Checks to see whether or not the admin settings are being accessed by the current request.
	 *
	 * @return bool
	 */
	private function is_accessing_settings() {
		if ( is_admin() ) {
			// phpcs:disable WordPress.Security.NonceVerification
			if ( ! isset( $_REQUEST['page'] ) || 'wc-settings' !== $_REQUEST['page'] ) {
				return false;
			}
			if ( ! isset( $_REQUEST['tab'] ) || 'checkout' !== $_REQUEST['tab'] ) {
				return false;
			}
			if ( ! isset( $_REQUEST['section'] ) || 'cod' !== $_REQUEST['section'] ) {
				return false;
			}
			// phpcs:enable WordPress.Security.NonceVerification

			return true;
		}

		if ( Constants::is_true( 'REST_REQUEST' ) ) {
			global $wp;
			if ( isset( $wp->query_vars['rest_route'] ) && false !== strpos( $wp->query_vars['rest_route'], '/payment_gateways' ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Loads all of the shipping method options for the enable_for_methods field.
	 *
	 * @return array
	 */
	private function load_shipping_method_options() {
		// Since this is expensive, we only want to do it if we're actually on the settings page.
		if ( ! $this->is_accessing_settings() ) {
			return array();
		}

		$data_store = WC_Data_Store::load( 'shipping-zone' );
		$raw_zones  = $data_store->get_zones();

		foreach ( $raw_zones as $raw_zone ) {
			$zones[] = new WC_Shipping_Zone( $raw_zone );
		}

		$zones[] = new WC_Shipping_Zone( 0 );

		$options = array();
		foreach ( WC()->shipping()->load_shipping_methods() as $method ) {

			$options[ $method->get_method_title() ] = array();

			// Translators: %1$s shipping method name.
			$options[ $method->get_method_title() ][ $method->id ] = sprintf( __( 'Any &quot;%1$s&quot; method', 'woocommerce' ), $method->get_method_title() );

			foreach ( $zones as $zone ) {

				$shipping_method_instances = $zone->get_shipping_methods();

				foreach ( $shipping_method_instances as $shipping_method_instance_id => $shipping_method_instance ) {

					if ( $shipping_method_instance->id !== $method->id ) {
						continue;
					}

					$option_id = $shipping_method_instance->get_rate_id();

					// Translators: %1$s shipping method title, %2$s shipping method id.
					$option_instance_title = sprintf( __( '%1$s (#%2$s)', 'woocommerce' ), $shipping_method_instance->get_title(), $shipping_method_instance_id );

					// Translators: %1$s zone name, %2$s shipping method instance name.
					$option_title = sprintf( __( '%1$s &ndash; %2$s', 'woocommerce' ), $zone->get_id() ? $zone->get_zone_name() : __( 'Other locations', 'woocommerce' ), $option_instance_title );

					$options[ $method->get_method_title() ][ $option_id ] = $option_title;
				}
			}
		}

		return $options;
	}

	/**
	 * Converts the chosen rate IDs generated by Shipping Methods to a canonical 'method_id:instance_id' format.
	 *
	 * @since  3.4.0
	 *
	 * @param  array $order_shipping_items  Array of WC_Order_Item_Shipping objects.
	 * @return array $canonical_rate_ids    Rate IDs in a canonical format.
	 */
	private function get_canonical_order_shipping_item_rate_ids( $order_shipping_items ) {

		$canonical_rate_ids = array();

		foreach ( $order_shipping_items as $order_shipping_item ) {
			$canonical_rate_ids[] = $order_shipping_item->get_method_id() . ':' . $order_shipping_item->get_instance_id();
		}

		return $canonical_rate_ids;
	}

	/**
	 * Converts the chosen rate IDs generated by Shipping Methods to a canonical 'method_id:instance_id' format.
	 *
	 * @since  3.4.0
	 *
	 * @param  array $chosen_package_rate_ids Rate IDs as generated by shipping methods. Can be anything if a shipping method doesn't honor WC conventions.
	 * @return array $canonical_rate_ids  Rate IDs in a canonical format.
	 */
	private function get_canonical_package_rate_ids( $chosen_package_rate_ids ) {

		$shipping_packages  = WC()->shipping()->get_packages();
		$canonical_rate_ids = array();

		if ( ! empty( $chosen_package_rate_ids ) && is_array( $chosen_package_rate_ids ) ) {
			foreach ( $chosen_package_rate_ids as $package_key => $chosen_package_rate_id ) {
				if ( ! empty( $shipping_packages[ $package_key ]['rates'][ $chosen_package_rate_id ] ) ) {
					$chosen_rate          = $shipping_packages[ $package_key ]['rates'][ $chosen_package_rate_id ];
					$canonical_rate_ids[] = $chosen_rate->get_method_id() . ':' . $chosen_rate->get_instance_id();
				}
			}
		}

		return $canonical_rate_ids;
	}

	/**
	 * Indicates whether a rate exists in an array of canonically-formatted rate IDs that activates this gateway.
	 *
	 * @since  3.4.0
	 *
	 * @param array $rate_ids Rate ids to check.
	 * @return boolean
	 */
	private function get_matching_rates( $rate_ids ) {
		// First, match entries in 'method_id:instance_id' format. Then, match entries in 'method_id' format by stripping off the instance ID from the candidates.
		return array_unique( array_merge( array_intersect( $this->enable_for_methods, $rate_ids ), array_intersect( $this->enable_for_methods, array_unique( array_map( 'wc_get_string_before_colon', $rate_ids ) ) ) ) );
	}

	/**
	 * Process the payment and return the result.
	 *
	 * @param int $order_id Order ID.
	 * @return array
	 */
	public function process_payment( $order_id ) {
		$order = wc_get_order( $order_id );

		if ( $order->get_total() > 0 ) {
			// Mark as processing or on-hold (payment won't be taken until delivery).
			$order->update_status( apply_filters( 'woocommerce_cod_process_payment_order_status', $order->has_downloadable_item() ? 'on-hold' : 'processing', $order ), __( 'Payment to be made upon delivery.', 'woocommerce' ) );
		} else {
			$order->payment_complete();
		}

		// Remove cart.
		WC()->cart->empty_cart();

		// Return thankyou redirect.
		return array(
			'result'   => 'success',
			'redirect' => $this->get_return_url( $order ),
		);
	}

	/**
	 * Output for the order received page.
	 */
	public function thankyou_page() {
		if ( $this->instructions ) {
			echo wp_kses_post( wpautop( wptexturize( $this->instructions ) ) );
		}
	}

	/**
	 * Change payment complete order status to completed for COD orders.
	 *
	 * @since  3.1.0
	 * @param  string         $status Current order status.
	 * @param  int            $order_id Order ID.
	 * @param  WC_Order|false $order Order object.
	 * @return string
	 */
	public function change_payment_complete_order_status( $status, $order_id = 0, $order = false ) {
		if ( $order && 'cod' === $order->get_payment_method() ) {
			$status = 'completed';
		}
		return $status;
	}

	/**
	 * Add content to the WC emails.
	 *
	 * @param WC_Order $order Order object.
	 * @param bool     $sent_to_admin  Sent to admin.
	 * @param bool     $plain_text Email format: plain text or HTML.
	 */
	public function email_instructions( $order, $sent_to_admin, $plain_text = false ) {
		if ( $this->instructions && ! $sent_to_admin && $this->id === $order->get_payment_method() ) {
			echo wp_kses_post( wpautop( wptexturize( $this->instructions ) ) . PHP_EOL );
		}
	}
}
gateways/bacs/class-wc-gateway-bacs.php000064400000033634151336044770014115 0ustar00<?php
/**
 * Class WC_Gateway_BACS file.
 *
 * @package WooCommerce\Gateways
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Bank Transfer Payment Gateway.
 *
 * Provides a Bank Transfer Payment Gateway. Based on code by Mike Pepper.
 *
 * @class       WC_Gateway_BACS
 * @extends     WC_Payment_Gateway
 * @version     2.1.0
 * @package     WooCommerce\Classes\Payment
 */
class WC_Gateway_BACS extends WC_Payment_Gateway {

	/**
	 * Array of locales
	 *
	 * @var array
	 */
	public $locale;

	/**
	 * Constructor for the gateway.
	 */
	public function __construct() {

		$this->id                 = 'bacs';
		$this->icon               = apply_filters( 'woocommerce_bacs_icon', '' );
		$this->has_fields         = false;
		$this->method_title       = __( 'Direct bank transfer', 'woocommerce' );
		$this->method_description = __( 'Take payments in person via BACS. More commonly known as direct bank/wire transfer.', 'woocommerce' );

		// Load the settings.
		$this->init_form_fields();
		$this->init_settings();

		// Define user set variables.
		$this->title        = $this->get_option( 'title' );
		$this->description  = $this->get_option( 'description' );
		$this->instructions = $this->get_option( 'instructions' );

		// BACS account fields shown on the thanks page and in emails.
		$this->account_details = get_option(
			'woocommerce_bacs_accounts',
			array(
				array(
					'account_name'   => $this->get_option( 'account_name' ),
					'account_number' => $this->get_option( 'account_number' ),
					'sort_code'      => $this->get_option( 'sort_code' ),
					'bank_name'      => $this->get_option( 'bank_name' ),
					'iban'           => $this->get_option( 'iban' ),
					'bic'            => $this->get_option( 'bic' ),
				),
			)
		);

		// Actions.
		add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) );
		add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'save_account_details' ) );
		add_action( 'woocommerce_thankyou_bacs', array( $this, 'thankyou_page' ) );

		// Customer Emails.
		add_action( 'woocommerce_email_before_order_table', array( $this, 'email_instructions' ), 10, 3 );
	}

	/**
	 * Initialise Gateway Settings Form Fields.
	 */
	public function init_form_fields() {

		$this->form_fields = array(
			'enabled'         => array(
				'title'   => __( 'Enable/Disable', 'woocommerce' ),
				'type'    => 'checkbox',
				'label'   => __( 'Enable bank transfer', 'woocommerce' ),
				'default' => 'no',
			),
			'title'           => array(
				'title'       => __( 'Title', 'woocommerce' ),
				'type'        => 'text',
				'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ),
				'default'     => __( 'Direct bank transfer', 'woocommerce' ),
				'desc_tip'    => true,
			),
			'description'     => array(
				'title'       => __( 'Description', 'woocommerce' ),
				'type'        => 'textarea',
				'description' => __( 'Payment method description that the customer will see on your checkout.', 'woocommerce' ),
				'default'     => __( 'Make your payment directly into our bank account. Please use your Order ID as the payment reference. Your order will not be shipped until the funds have cleared in our account.', 'woocommerce' ),
				'desc_tip'    => true,
			),
			'instructions'    => array(
				'title'       => __( 'Instructions', 'woocommerce' ),
				'type'        => 'textarea',
				'description' => __( 'Instructions that will be added to the thank you page and emails.', 'woocommerce' ),
				'default'     => '',
				'desc_tip'    => true,
			),
			'account_details' => array(
				'type' => 'account_details',
			),
		);

	}

	/**
	 * Generate account details html.
	 *
	 * @return string
	 */
	public function generate_account_details_html() {

		ob_start();

		$country = WC()->countries->get_base_country();
		$locale  = $this->get_country_locale();

		// Get sortcode label in the $locale array and use appropriate one.
		$sortcode = isset( $locale[ $country ]['sortcode']['label'] ) ? $locale[ $country ]['sortcode']['label'] : __( 'Sort code', 'woocommerce' );

		?>
		<tr valign="top">
			<th scope="row" class="titledesc"><?php esc_html_e( 'Account details:', 'woocommerce' ); ?></th>
			<td class="forminp" id="bacs_accounts">
				<div class="wc_input_table_wrapper">
					<table class="widefat wc_input_table sortable" cellspacing="0">
						<thead>
							<tr>
								<th class="sort">&nbsp;</th>
								<th><?php esc_html_e( 'Account name', 'woocommerce' ); ?></th>
								<th><?php esc_html_e( 'Account number', 'woocommerce' ); ?></th>
								<th><?php esc_html_e( 'Bank name', 'woocommerce' ); ?></th>
								<th><?php echo esc_html( $sortcode ); ?></th>
								<th><?php esc_html_e( 'IBAN', 'woocommerce' ); ?></th>
								<th><?php esc_html_e( 'BIC / Swift', 'woocommerce' ); ?></th>
							</tr>
						</thead>
						<tbody class="accounts">
							<?php
							$i = -1;
							if ( $this->account_details ) {
								foreach ( $this->account_details as $account ) {
									$i++;

									echo '<tr class="account">
										<td class="sort"></td>
										<td><input type="text" value="' . esc_attr( wp_unslash( $account['account_name'] ) ) . '" name="bacs_account_name[' . esc_attr( $i ) . ']" /></td>
										<td><input type="text" value="' . esc_attr( $account['account_number'] ) . '" name="bacs_account_number[' . esc_attr( $i ) . ']" /></td>
										<td><input type="text" value="' . esc_attr( wp_unslash( $account['bank_name'] ) ) . '" name="bacs_bank_name[' . esc_attr( $i ) . ']" /></td>
										<td><input type="text" value="' . esc_attr( $account['sort_code'] ) . '" name="bacs_sort_code[' . esc_attr( $i ) . ']" /></td>
										<td><input type="text" value="' . esc_attr( $account['iban'] ) . '" name="bacs_iban[' . esc_attr( $i ) . ']" /></td>
										<td><input type="text" value="' . esc_attr( $account['bic'] ) . '" name="bacs_bic[' . esc_attr( $i ) . ']" /></td>
									</tr>';
								}
							}
							?>
						</tbody>
						<tfoot>
							<tr>
								<th colspan="7"><a href="#" class="add button"><?php esc_html_e( '+ Add account', 'woocommerce' ); ?></a> <a href="#" class="remove_rows button"><?php esc_html_e( 'Remove selected account(s)', 'woocommerce' ); ?></a></th>
							</tr>
						</tfoot>
					</table>
				</div>
				<script type="text/javascript">
					jQuery(function() {
						jQuery('#bacs_accounts').on( 'click', 'a.add', function(){

							var size = jQuery('#bacs_accounts').find('tbody .account').length;

							jQuery('<tr class="account">\
									<td class="sort"></td>\
									<td><input type="text" name="bacs_account_name[' + size + ']" /></td>\
									<td><input type="text" name="bacs_account_number[' + size + ']" /></td>\
									<td><input type="text" name="bacs_bank_name[' + size + ']" /></td>\
									<td><input type="text" name="bacs_sort_code[' + size + ']" /></td>\
									<td><input type="text" name="bacs_iban[' + size + ']" /></td>\
									<td><input type="text" name="bacs_bic[' + size + ']" /></td>\
								</tr>').appendTo('#bacs_accounts table tbody');

							return false;
						});
					});
				</script>
			</td>
		</tr>
		<?php
		return ob_get_clean();

	}

	/**
	 * Save account details table.
	 */
	public function save_account_details() {

		$accounts = array();

		// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verification already handled in WC_Admin_Settings::save()
		if ( isset( $_POST['bacs_account_name'] ) && isset( $_POST['bacs_account_number'] ) && isset( $_POST['bacs_bank_name'] )
			 && isset( $_POST['bacs_sort_code'] ) && isset( $_POST['bacs_iban'] ) && isset( $_POST['bacs_bic'] ) ) {

			$account_names   = wc_clean( wp_unslash( $_POST['bacs_account_name'] ) );
			$account_numbers = wc_clean( wp_unslash( $_POST['bacs_account_number'] ) );
			$bank_names      = wc_clean( wp_unslash( $_POST['bacs_bank_name'] ) );
			$sort_codes      = wc_clean( wp_unslash( $_POST['bacs_sort_code'] ) );
			$ibans           = wc_clean( wp_unslash( $_POST['bacs_iban'] ) );
			$bics            = wc_clean( wp_unslash( $_POST['bacs_bic'] ) );

			foreach ( $account_names as $i => $name ) {
				if ( ! isset( $account_names[ $i ] ) ) {
					continue;
				}

				$accounts[] = array(
					'account_name'   => $account_names[ $i ],
					'account_number' => $account_numbers[ $i ],
					'bank_name'      => $bank_names[ $i ],
					'sort_code'      => $sort_codes[ $i ],
					'iban'           => $ibans[ $i ],
					'bic'            => $bics[ $i ],
				);
			}
		}
		// phpcs:enable

		update_option( 'woocommerce_bacs_accounts', $accounts );
	}

	/**
	 * Output for the order received page.
	 *
	 * @param int $order_id Order ID.
	 */
	public function thankyou_page( $order_id ) {

		if ( $this->instructions ) {
			echo wp_kses_post( wpautop( wptexturize( wp_kses_post( $this->instructions ) ) ) );
		}
		$this->bank_details( $order_id );

	}

	/**
	 * Add content to the WC emails.
	 *
	 * @param WC_Order $order Order object.
	 * @param bool     $sent_to_admin Sent to admin.
	 * @param bool     $plain_text Email format: plain text or HTML.
	 */
	public function email_instructions( $order, $sent_to_admin, $plain_text = false ) {

		if ( ! $sent_to_admin && 'bacs' === $order->get_payment_method() && $order->has_status( 'on-hold' ) ) {
			if ( $this->instructions ) {
				echo wp_kses_post( wpautop( wptexturize( $this->instructions ) ) . PHP_EOL );
			}
			$this->bank_details( $order->get_id() );
		}

	}

	/**
	 * Get bank details and place into a list format.
	 *
	 * @param int $order_id Order ID.
	 */
	private function bank_details( $order_id = '' ) {

		if ( empty( $this->account_details ) ) {
			return;
		}

		// Get order and store in $order.
		$order = wc_get_order( $order_id );

		// Get the order country and country $locale.
		$country = $order->get_billing_country();
		$locale  = $this->get_country_locale();

		// Get sortcode label in the $locale array and use appropriate one.
		$sortcode = isset( $locale[ $country ]['sortcode']['label'] ) ? $locale[ $country ]['sortcode']['label'] : __( 'Sort code', 'woocommerce' );

		$bacs_accounts = apply_filters( 'woocommerce_bacs_accounts', $this->account_details, $order_id );

		if ( ! empty( $bacs_accounts ) ) {
			$account_html = '';
			$has_details  = false;

			foreach ( $bacs_accounts as $bacs_account ) {
				$bacs_account = (object) $bacs_account;

				if ( $bacs_account->account_name ) {
					$account_html .= '<h3 class="wc-bacs-bank-details-account-name">' . wp_kses_post( wp_unslash( $bacs_account->account_name ) ) . ':</h3>' . PHP_EOL;
				}

				$account_html .= '<ul class="wc-bacs-bank-details order_details bacs_details">' . PHP_EOL;

				// BACS account fields shown on the thanks page and in emails.
				$account_fields = apply_filters(
					'woocommerce_bacs_account_fields',
					array(
						'bank_name'      => array(
							'label' => __( 'Bank', 'woocommerce' ),
							'value' => $bacs_account->bank_name,
						),
						'account_number' => array(
							'label' => __( 'Account number', 'woocommerce' ),
							'value' => $bacs_account->account_number,
						),
						'sort_code'      => array(
							'label' => $sortcode,
							'value' => $bacs_account->sort_code,
						),
						'iban'           => array(
							'label' => __( 'IBAN', 'woocommerce' ),
							'value' => $bacs_account->iban,
						),
						'bic'            => array(
							'label' => __( 'BIC', 'woocommerce' ),
							'value' => $bacs_account->bic,
						),
					),
					$order_id
				);

				foreach ( $account_fields as $field_key => $field ) {
					if ( ! empty( $field['value'] ) ) {
						$account_html .= '<li class="' . esc_attr( $field_key ) . '">' . wp_kses_post( $field['label'] ) . ': <strong>' . wp_kses_post( wptexturize( $field['value'] ) ) . '</strong></li>' . PHP_EOL;
						$has_details   = true;
					}
				}

				$account_html .= '</ul>';
			}

			if ( $has_details ) {
				echo '<section class="woocommerce-bacs-bank-details"><h2 class="wc-bacs-bank-details-heading">' . esc_html__( 'Our bank details', 'woocommerce' ) . '</h2>' . wp_kses_post( PHP_EOL . $account_html ) . '</section>';
			}
		}

	}

	/**
	 * Process the payment and return the result.
	 *
	 * @param int $order_id Order ID.
	 * @return array
	 */
	public function process_payment( $order_id ) {

		$order = wc_get_order( $order_id );

		if ( $order->get_total() > 0 ) {
			// Mark as on-hold (we're awaiting the payment).
			$order->update_status( apply_filters( 'woocommerce_bacs_process_payment_order_status', 'on-hold', $order ), __( 'Awaiting BACS payment', 'woocommerce' ) );
		} else {
			$order->payment_complete();
		}

		// Remove cart.
		WC()->cart->empty_cart();

		// Return thankyou redirect.
		return array(
			'result'   => 'success',
			'redirect' => $this->get_return_url( $order ),
		);

	}

	/**
	 * Get country locale if localized.
	 *
	 * @return array
	 */
	public function get_country_locale() {

		if ( empty( $this->locale ) ) {

			// Locale information to be used - only those that are not 'Sort Code'.
			$this->locale = apply_filters(
				'woocommerce_get_bacs_locale',
				array(
					'AU' => array(
						'sortcode' => array(
							'label' => __( 'BSB', 'woocommerce' ),
						),
					),
					'CA' => array(
						'sortcode' => array(
							'label' => __( 'Bank transit number', 'woocommerce' ),
						),
					),
					'IN' => array(
						'sortcode' => array(
							'label' => __( 'IFSC', 'woocommerce' ),
						),
					),
					'IT' => array(
						'sortcode' => array(
							'label' => __( 'Branch sort', 'woocommerce' ),
						),
					),
					'NZ' => array(
						'sortcode' => array(
							'label' => __( 'Bank code', 'woocommerce' ),
						),
					),
					'SE' => array(
						'sortcode' => array(
							'label' => __( 'Bank code', 'woocommerce' ),
						),
					),
					'US' => array(
						'sortcode' => array(
							'label' => __( 'Routing number', 'woocommerce' ),
						),
					),
					'ZA' => array(
						'sortcode' => array(
							'label' => __( 'Branch code', 'woocommerce' ),
						),
					),
				)
			);

		}

		return $this->locale;

	}
}
gateways/cheque/class-wc-gateway-cheque.php000064400000010376151336044770015017 0ustar00<?php
/**
 * Class WC_Gateway_Cheque file.
 *
 * @package WooCommerce\Gateways
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Cheque Payment Gateway.
 *
 * Provides a Cheque Payment Gateway, mainly for testing purposes.
 *
 * @class       WC_Gateway_Cheque
 * @extends     WC_Payment_Gateway
 * @version     2.1.0
 * @package     WooCommerce\Classes\Payment
 */
class WC_Gateway_Cheque extends WC_Payment_Gateway {

	/**
	 * Constructor for the gateway.
	 */
	public function __construct() {
		$this->id                 = 'cheque';
		$this->icon               = apply_filters( 'woocommerce_cheque_icon', '' );
		$this->has_fields         = false;
		$this->method_title       = _x( 'Check payments', 'Check payment method', 'woocommerce' );
		$this->method_description = __( 'Take payments in person via checks. This offline gateway can also be useful to test purchases.', 'woocommerce' );

		// Load the settings.
		$this->init_form_fields();
		$this->init_settings();

		// Define user set variables.
		$this->title        = $this->get_option( 'title' );
		$this->description  = $this->get_option( 'description' );
		$this->instructions = $this->get_option( 'instructions' );

		// Actions.
		add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) );
		add_action( 'woocommerce_thankyou_cheque', array( $this, 'thankyou_page' ) );

		// Customer Emails.
		add_action( 'woocommerce_email_before_order_table', array( $this, 'email_instructions' ), 10, 3 );
	}

	/**
	 * Initialise Gateway Settings Form Fields.
	 */
	public function init_form_fields() {

		$this->form_fields = array(
			'enabled'      => array(
				'title'   => __( 'Enable/Disable', 'woocommerce' ),
				'type'    => 'checkbox',
				'label'   => __( 'Enable check payments', 'woocommerce' ),
				'default' => 'no',
			),
			'title'        => array(
				'title'       => __( 'Title', 'woocommerce' ),
				'type'        => 'text',
				'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ),
				'default'     => _x( 'Check payments', 'Check payment method', 'woocommerce' ),
				'desc_tip'    => true,
			),
			'description'  => array(
				'title'       => __( 'Description', 'woocommerce' ),
				'type'        => 'textarea',
				'description' => __( 'Payment method description that the customer will see on your checkout.', 'woocommerce' ),
				'default'     => __( 'Please send a check to Store Name, Store Street, Store Town, Store State / County, Store Postcode.', 'woocommerce' ),
				'desc_tip'    => true,
			),
			'instructions' => array(
				'title'       => __( 'Instructions', 'woocommerce' ),
				'type'        => 'textarea',
				'description' => __( 'Instructions that will be added to the thank you page and emails.', 'woocommerce' ),
				'default'     => '',
				'desc_tip'    => true,
			),
		);
	}

	/**
	 * Output for the order received page.
	 */
	public function thankyou_page() {
		if ( $this->instructions ) {
			echo wp_kses_post( wpautop( wptexturize( $this->instructions ) ) );
		}
	}

	/**
	 * Add content to the WC emails.
	 *
	 * @access public
	 * @param WC_Order $order Order object.
	 * @param bool     $sent_to_admin Sent to admin.
	 * @param bool     $plain_text Email format: plain text or HTML.
	 */
	public function email_instructions( $order, $sent_to_admin, $plain_text = false ) {
		if ( $this->instructions && ! $sent_to_admin && 'cheque' === $order->get_payment_method() && $order->has_status( 'on-hold' ) ) {
			echo wp_kses_post( wpautop( wptexturize( $this->instructions ) ) . PHP_EOL );
		}
	}

	/**
	 * Process the payment and return the result.
	 *
	 * @param int $order_id Order ID.
	 * @return array
	 */
	public function process_payment( $order_id ) {

		$order = wc_get_order( $order_id );

		if ( $order->get_total() > 0 ) {
			// Mark as on-hold (we're awaiting the cheque).
			$order->update_status( apply_filters( 'woocommerce_cheque_process_payment_order_status', 'on-hold', $order ), _x( 'Awaiting check payment', 'Check payment method', 'woocommerce' ) );
		} else {
			$order->payment_complete();
		}

		// Remove cart.
		WC()->cart->empty_cart();

		// Return thankyou redirect.
		return array(
			'result'   => 'success',
			'redirect' => $this->get_return_url( $order ),
		);
	}
}
gateways/class-wc-payment-gateway-cc.php000064400000007461151336044770014334 0ustar00<?php
/**
 * Class WC_Payment_Gateway_CC file.
 *
 * @package WooCommerce\Gateways
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Credit Card Payment Gateway
 *
 * @since       2.6.0
 * @package     WooCommerce\Classes
 */
class WC_Payment_Gateway_CC extends WC_Payment_Gateway {

	/**
	 * Builds our payment fields area - including tokenization fields for logged
	 * in users, and the actual payment fields.
	 *
	 * @since 2.6.0
	 */
	public function payment_fields() {
		if ( $this->supports( 'tokenization' ) && is_checkout() ) {
			$this->tokenization_script();
			$this->saved_payment_methods();
			$this->form();
			$this->save_payment_method_checkbox();
		} else {
			$this->form();
		}
	}

	/**
	 * Output field name HTML
	 *
	 * Gateways which support tokenization do not require names - we don't want the data to post to the server.
	 *
	 * @since  2.6.0
	 * @param  string $name Field name.
	 * @return string
	 */
	public function field_name( $name ) {
		return $this->supports( 'tokenization' ) ? '' : ' name="' . esc_attr( $this->id . '-' . $name ) . '" ';
	}

	/**
	 * Outputs fields for entering credit card information.
	 *
	 * @since 2.6.0
	 */
	public function form() {
		wp_enqueue_script( 'wc-credit-card-form' );

		$fields = array();

		$cvc_field = '<p class="form-row form-row-last">
			<label for="' . esc_attr( $this->id ) . '-card-cvc">' . esc_html__( 'Card code', 'woocommerce' ) . '&nbsp;<span class="required">*</span></label>
			<input id="' . esc_attr( $this->id ) . '-card-cvc" class="input-text wc-credit-card-form-card-cvc" inputmode="numeric" autocomplete="off" autocorrect="no" autocapitalize="no" spellcheck="no" type="tel" maxlength="4" placeholder="' . esc_attr__( 'CVC', 'woocommerce' ) . '" ' . $this->field_name( 'card-cvc' ) . ' style="width:100px" />
		</p>';

		$default_fields = array(
			'card-number-field' => '<p class="form-row form-row-wide">
				<label for="' . esc_attr( $this->id ) . '-card-number">' . esc_html__( 'Card number', 'woocommerce' ) . '&nbsp;<span class="required">*</span></label>
				<input id="' . esc_attr( $this->id ) . '-card-number" class="input-text wc-credit-card-form-card-number" inputmode="numeric" autocomplete="cc-number" autocorrect="no" autocapitalize="no" spellcheck="no" type="tel" placeholder="&bull;&bull;&bull;&bull; &bull;&bull;&bull;&bull; &bull;&bull;&bull;&bull; &bull;&bull;&bull;&bull;" ' . $this->field_name( 'card-number' ) . ' />
			</p>',
			'card-expiry-field' => '<p class="form-row form-row-first">
				<label for="' . esc_attr( $this->id ) . '-card-expiry">' . esc_html__( 'Expiry (MM/YY)', 'woocommerce' ) . '&nbsp;<span class="required">*</span></label>
				<input id="' . esc_attr( $this->id ) . '-card-expiry" class="input-text wc-credit-card-form-card-expiry" inputmode="numeric" autocomplete="cc-exp" autocorrect="no" autocapitalize="no" spellcheck="no" type="tel" placeholder="' . esc_attr__( 'MM / YY', 'woocommerce' ) . '" ' . $this->field_name( 'card-expiry' ) . ' />
			</p>',
		);

		if ( ! $this->supports( 'credit_card_form_cvc_on_saved_method' ) ) {
			$default_fields['card-cvc-field'] = $cvc_field;
		}

		$fields = wp_parse_args( $fields, apply_filters( 'woocommerce_credit_card_form_fields', $default_fields, $this->id ) );
		?>

		<fieldset id="wc-<?php echo esc_attr( $this->id ); ?>-cc-form" class='wc-credit-card-form wc-payment-form'>
			<?php do_action( 'woocommerce_credit_card_form_start', $this->id ); ?>
			<?php
			foreach ( $fields as $field ) {
				echo $field; // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped
			}
			?>
			<?php do_action( 'woocommerce_credit_card_form_end', $this->id ); ?>
			<div class="clear"></div>
		</fieldset>
		<?php

		if ( $this->supports( 'credit_card_form_cvc_on_saved_method' ) ) {
			echo '<fieldset>' . $cvc_field . '</fieldset>'; // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped
		}
	}
}
gateways/class-wc-payment-gateway-echeck.php000064400000004516151336044770015167 0ustar00<?php
/**
 * Class WC_Payment_Gateway_eCheck file.
 *
 * @package WooCommerce\Gateways
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Class for eCheck Payment Gateway
 *
 * @since       2.6.0
 * @package     WooCommerce\Classes
 */
class WC_Payment_Gateway_ECheck extends WC_Payment_Gateway {

	/**
	 * Builds our payment fields area - including tokenization fields for logged
	 * in users, and the actual payment fields.
	 *
	 * @since 2.6.0
	 */
	public function payment_fields() {
		if ( $this->supports( 'tokenization' ) && is_checkout() ) {
			$this->tokenization_script();
			$this->saved_payment_methods();
			$this->form();
			$this->save_payment_method_checkbox();
		} else {
			$this->form();
		}
	}

	/**
	 * Outputs fields for entering eCheck information.
	 *
	 * @since 2.6.0
	 */
	public function form() {
		$fields = array();

		$default_fields = array(
			'routing-number' => '<p class="form-row form-row-first">
				<label for="' . esc_attr( $this->id ) . '-routing-number">' . esc_html__( 'Routing number', 'woocommerce' ) . '&nbsp;<span class="required">*</span></label>
				<input id="' . esc_attr( $this->id ) . '-routing-number" class="input-text wc-echeck-form-routing-number" type="text" maxlength="9" autocomplete="off" placeholder="&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;" name="' . esc_attr( $this->id ) . '-routing-number" />
			</p>',
			'account-number' => '<p class="form-row form-row-wide">
				<label for="' . esc_attr( $this->id ) . '-account-number">' . esc_html__( 'Account number', 'woocommerce' ) . '&nbsp;<span class="required">*</span></label>
				<input id="' . esc_attr( $this->id ) . '-account-number" class="input-text wc-echeck-form-account-number" type="text" autocomplete="off" name="' . esc_attr( $this->id ) . '-account-number" maxlength="17" />
			</p>',
		);

		$fields = wp_parse_args( $fields, apply_filters( 'woocommerce_echeck_form_fields', $default_fields, $this->id ) );
		?>

		<fieldset id="<?php echo esc_attr( $this->id ); ?>-cc-form" class='wc-echeck-form wc-payment-form'>
			<?php do_action( 'woocommerce_echeck_form_start', $this->id ); ?>
			<?php
			foreach ( $fields as $field ) {
				echo $field; // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped
			}
			?>
			<?php do_action( 'woocommerce_echeck_form_end', $this->id ); ?>
			<div class="clear"></div>
		</fieldset>
		<?php
	}
}
gateways/paypal/includes/class-wc-gateway-paypal-request.php000064400000044174151336044770020346 0ustar00<?php
/**
 * Class WC_Gateway_Paypal_Request file.
 *
 * @package WooCommerce\Gateways
 */

use Automattic\WooCommerce\Utilities\NumberUtil;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Generates requests to send to PayPal.
 */
class WC_Gateway_Paypal_Request {

	/**
	 * Stores line items to send to PayPal.
	 *
	 * @var array
	 */
	protected $line_items = array();

	/**
	 * Pointer to gateway making the request.
	 *
	 * @var WC_Gateway_Paypal
	 */
	protected $gateway;

	/**
	 * Endpoint for requests from PayPal.
	 *
	 * @var string
	 */
	protected $notify_url;

	/**
	 * Endpoint for requests to PayPal.
	 *
	 * @var string
	 */
	protected $endpoint;


	/**
	 * Constructor.
	 *
	 * @param WC_Gateway_Paypal $gateway Paypal gateway object.
	 */
	public function __construct( $gateway ) {
		$this->gateway    = $gateway;
		$this->notify_url = WC()->api_request_url( 'WC_Gateway_Paypal' );
	}

	/**
	 * Get the PayPal request URL for an order.
	 *
	 * @param  WC_Order $order Order object.
	 * @param  bool     $sandbox Whether to use sandbox mode or not.
	 * @return string
	 */
	public function get_request_url( $order, $sandbox = false ) {
		$this->endpoint    = $sandbox ? 'https://www.sandbox.paypal.com/cgi-bin/webscr?test_ipn=1&' : 'https://www.paypal.com/cgi-bin/webscr?';
		$paypal_args       = $this->get_paypal_args( $order );
		$paypal_args['bn'] = 'WooThemes_Cart'; // Append WooCommerce PayPal Partner Attribution ID. This should not be overridden for this gateway.

		// Mask (remove) PII from the logs.
		$mask = array(
			'first_name'    => '***',
			'last_name'     => '***',
			'address1'      => '***',
			'address2'      => '***',
			'city'          => '***',
			'state'         => '***',
			'zip'           => '***',
			'country'       => '***',
			'email'         => '***@***',
			'night_phone_a' => '***',
			'night_phone_b' => '***',
			'night_phone_c' => '***',
		);

		WC_Gateway_Paypal::log( 'PayPal Request Args for order ' . $order->get_order_number() . ': ' . wc_print_r( array_merge( $paypal_args, array_intersect_key( $mask, $paypal_args ) ), true ) );

		return $this->endpoint . http_build_query( $paypal_args, '', '&' );
	}

	/**
	 * Limit length of an arg.
	 *
	 * @param  string  $string Argument to limit.
	 * @param  integer $limit Limit size in characters.
	 * @return string
	 */
	protected function limit_length( $string, $limit = 127 ) {
		$str_limit = $limit - 3;
		if ( function_exists( 'mb_strimwidth' ) ) {
			if ( mb_strlen( $string ) > $limit ) {
				$string = mb_strimwidth( $string, 0, $str_limit ) . '...';
			}
		} else {
			if ( strlen( $string ) > $limit ) {
				$string = substr( $string, 0, $str_limit ) . '...';
			}
		}
		return $string;
	}

	/**
	 * Get transaction args for paypal request, except for line item args.
	 *
	 * @param WC_Order $order Order object.
	 * @return array
	 */
	protected function get_transaction_args( $order ) {
		return array_merge(
			array(
				'cmd'           => '_cart',
				'business'      => $this->gateway->get_option( 'email' ),
				'no_note'       => 1,
				'currency_code' => get_woocommerce_currency(),
				'charset'       => 'utf-8',
				'rm'            => is_ssl() ? 2 : 1,
				'upload'        => 1,
				'return'        => esc_url_raw( add_query_arg( 'utm_nooverride', '1', $this->gateway->get_return_url( $order ) ) ),
				'cancel_return' => esc_url_raw( $order->get_cancel_order_url_raw() ),
				'image_url'     => esc_url_raw( $this->gateway->get_option( 'image_url' ) ),
				'paymentaction' => $this->gateway->get_option( 'paymentaction' ),
				'invoice'       => $this->limit_length( $this->gateway->get_option( 'invoice_prefix' ) . $order->get_order_number(), 127 ),
				'custom'        => wp_json_encode(
					array(
						'order_id'  => $order->get_id(),
						'order_key' => $order->get_order_key(),
					)
				),
				'notify_url'    => $this->limit_length( $this->notify_url, 255 ),
				'first_name'    => $this->limit_length( $order->get_billing_first_name(), 32 ),
				'last_name'     => $this->limit_length( $order->get_billing_last_name(), 64 ),
				'address1'      => $this->limit_length( $order->get_billing_address_1(), 100 ),
				'address2'      => $this->limit_length( $order->get_billing_address_2(), 100 ),
				'city'          => $this->limit_length( $order->get_billing_city(), 40 ),
				'state'         => $this->get_paypal_state( $order->get_billing_country(), $order->get_billing_state() ),
				'zip'           => $this->limit_length( wc_format_postcode( $order->get_billing_postcode(), $order->get_billing_country() ), 32 ),
				'country'       => $this->limit_length( $order->get_billing_country(), 2 ),
				'email'         => $this->limit_length( $order->get_billing_email() ),
			),
			$this->get_phone_number_args( $order ),
			$this->get_shipping_args( $order )
		);
	}

	/**
	 * If the default request with line items is too long, generate a new one with only one line item.
	 *
	 * If URL is longer than 2,083 chars, ignore line items and send cart to Paypal as a single item.
	 * One item's name can only be 127 characters long, so the URL should not be longer than limit.
	 * URL character limit via:
	 * https://support.microsoft.com/en-us/help/208427/maximum-url-length-is-2-083-characters-in-internet-explorer.
	 *
	 * @param WC_Order $order Order to be sent to Paypal.
	 * @param array    $paypal_args Arguments sent to Paypal in the request.
	 * @return array
	 */
	protected function fix_request_length( $order, $paypal_args ) {
		$max_paypal_length = 2083;
		$query_candidate   = http_build_query( $paypal_args, '', '&' );

		if ( strlen( $this->endpoint . $query_candidate ) <= $max_paypal_length ) {
			return $paypal_args;
		}

		return apply_filters(
			'woocommerce_paypal_args',
			array_merge(
				$this->get_transaction_args( $order ),
				$this->get_line_item_args( $order, true )
			),
			$order
		);

	}

	/**
	 * Get PayPal Args for passing to PP.
	 *
	 * @param  WC_Order $order Order object.
	 * @return array
	 */
	protected function get_paypal_args( $order ) {
		WC_Gateway_Paypal::log( 'Generating payment form for order ' . $order->get_order_number() . '. Notify URL: ' . $this->notify_url );

		$force_one_line_item = apply_filters( 'woocommerce_paypal_force_one_line_item', false, $order );

		if ( ( wc_tax_enabled() && wc_prices_include_tax() ) || ! $this->line_items_valid( $order ) ) {
			$force_one_line_item = true;
		}

		$paypal_args = apply_filters(
			'woocommerce_paypal_args',
			array_merge(
				$this->get_transaction_args( $order ),
				$this->get_line_item_args( $order, $force_one_line_item )
			),
			$order
		);

		return $this->fix_request_length( $order, $paypal_args );
	}

	/**
	 * Get phone number args for paypal request.
	 *
	 * @param  WC_Order $order Order object.
	 * @return array
	 */
	protected function get_phone_number_args( $order ) {
		$phone_number = wc_sanitize_phone_number( $order->get_billing_phone() );

		if ( in_array( $order->get_billing_country(), array( 'US', 'CA' ), true ) ) {
			$phone_number = ltrim( $phone_number, '+1' );
			$phone_args   = array(
				'night_phone_a' => substr( $phone_number, 0, 3 ),
				'night_phone_b' => substr( $phone_number, 3, 3 ),
				'night_phone_c' => substr( $phone_number, 6, 4 ),
			);
		} else {
			$calling_code = WC()->countries->get_country_calling_code( $order->get_billing_country() );
			$calling_code = is_array( $calling_code ) ? $calling_code[0] : $calling_code;

			if ( $calling_code ) {
				$phone_number = str_replace( $calling_code, '', preg_replace( '/^0/', '', $order->get_billing_phone() ) );
			}

			$phone_args = array(
				'night_phone_a' => $calling_code,
				'night_phone_b' => $phone_number,
			);
		}
		return $phone_args;
	}

	/**
	 * Get shipping args for paypal request.
	 *
	 * @param  WC_Order $order Order object.
	 * @return array
	 */
	protected function get_shipping_args( $order ) {
		$shipping_args = array();
		if ( $order->needs_shipping_address() ) {
			$shipping_args['address_override'] = $this->gateway->get_option( 'address_override' ) === 'yes' ? 1 : 0;
			$shipping_args['no_shipping']      = 0;
			if ( 'yes' === $this->gateway->get_option( 'send_shipping' ) ) {
				// If we are sending shipping, send shipping address instead of billing.
				$shipping_args['first_name'] = $this->limit_length( $order->get_shipping_first_name(), 32 );
				$shipping_args['last_name']  = $this->limit_length( $order->get_shipping_last_name(), 64 );
				$shipping_args['address1']   = $this->limit_length( $order->get_shipping_address_1(), 100 );
				$shipping_args['address2']   = $this->limit_length( $order->get_shipping_address_2(), 100 );
				$shipping_args['city']       = $this->limit_length( $order->get_shipping_city(), 40 );
				$shipping_args['state']      = $this->get_paypal_state( $order->get_shipping_country(), $order->get_shipping_state() );
				$shipping_args['country']    = $this->limit_length( $order->get_shipping_country(), 2 );
				$shipping_args['zip']        = $this->limit_length( wc_format_postcode( $order->get_shipping_postcode(), $order->get_shipping_country() ), 32 );
			}
		} else {
			$shipping_args['no_shipping'] = 1;
		}
		return $shipping_args;
	}

	/**
	 * Get shipping cost line item args for paypal request.
	 *
	 * @param  WC_Order $order Order object.
	 * @param  bool     $force_one_line_item Whether one line item was forced by validation or URL length.
	 * @return array
	 */
	protected function get_shipping_cost_line_item( $order, $force_one_line_item ) {
		$line_item_args = array();
		$shipping_total = $order->get_shipping_total();
		if ( $force_one_line_item ) {
			$shipping_total += $order->get_shipping_tax();
		}

		// Add shipping costs. Paypal ignores anything over 5 digits (999.99 is the max).
		// We also check that shipping is not the **only** cost as PayPal won't allow payment
		// if the items have no cost.
		if ( $order->get_shipping_total() > 0 && $order->get_shipping_total() < 999.99 && $this->number_format( $order->get_shipping_total() + $order->get_shipping_tax(), $order ) !== $this->number_format( $order->get_total(), $order ) ) {
			$line_item_args['shipping_1'] = $this->number_format( $shipping_total, $order );
		} elseif ( $order->get_shipping_total() > 0 ) {
			/* translators: %s: Order shipping method */
			$this->add_line_item( sprintf( __( 'Shipping via %s', 'woocommerce' ), $order->get_shipping_method() ), 1, $this->number_format( $shipping_total, $order ) );
		}

		return $line_item_args;
	}

	/**
	 * Get line item args for paypal request as a single line item.
	 *
	 * @param  WC_Order $order Order object.
	 * @return array
	 */
	protected function get_line_item_args_single_item( $order ) {
		$this->delete_line_items();

		$all_items_name = $this->get_order_item_names( $order );
		$this->add_line_item( $all_items_name ? $all_items_name : __( 'Order', 'woocommerce' ), 1, $this->number_format( $order->get_total() - $this->round( $order->get_shipping_total() + $order->get_shipping_tax(), $order ), $order ), $order->get_order_number() );
		$line_item_args = $this->get_shipping_cost_line_item( $order, true );

		return array_merge( $line_item_args, $this->get_line_items() );
	}

	/**
	 * Get line item args for paypal request.
	 *
	 * @param  WC_Order $order Order object.
	 * @param  bool     $force_one_line_item Create only one item for this order.
	 * @return array
	 */
	protected function get_line_item_args( $order, $force_one_line_item = false ) {
		$line_item_args = array();

		if ( $force_one_line_item ) {
			/**
			 * Send order as a single item.
			 *
			 * For shipping, we longer use shipping_1 because paypal ignores it if *any* shipping rules are within paypal, and paypal ignores anything over 5 digits (999.99 is the max).
			 */
			$line_item_args = $this->get_line_item_args_single_item( $order );
		} else {
			/**
			 * Passing a line item per product if supported.
			 */
			$this->prepare_line_items( $order );
			$line_item_args['tax_cart'] = $this->number_format( $order->get_total_tax(), $order );

			if ( $order->get_total_discount() > 0 ) {
				$line_item_args['discount_amount_cart'] = $this->number_format( $this->round( $order->get_total_discount(), $order ), $order );
			}

			$line_item_args = array_merge( $line_item_args, $this->get_shipping_cost_line_item( $order, false ) );
			$line_item_args = array_merge( $line_item_args, $this->get_line_items() );

		}

		return $line_item_args;
	}

	/**
	 * Get order item names as a string.
	 *
	 * @param  WC_Order $order Order object.
	 * @return string
	 */
	protected function get_order_item_names( $order ) {
		$item_names = array();

		foreach ( $order->get_items() as $item ) {
			$item_name = $item->get_name();
			$item_meta = wp_strip_all_tags(
				wc_display_item_meta(
					$item,
					array(
						'before'    => '',
						'separator' => ', ',
						'after'     => '',
						'echo'      => false,
						'autop'     => false,
					)
				)
			);

			if ( $item_meta ) {
				$item_name .= ' (' . $item_meta . ')';
			}

			$item_names[] = $item_name . ' x ' . $item->get_quantity();
		}

		return apply_filters( 'woocommerce_paypal_get_order_item_names', implode( ', ', $item_names ), $order );
	}

	/**
	 * Get order item names as a string.
	 *
	 * @param  WC_Order      $order Order object.
	 * @param  WC_Order_Item $item Order item object.
	 * @return string
	 */
	protected function get_order_item_name( $order, $item ) {
		$item_name = $item->get_name();
		$item_meta = wp_strip_all_tags(
			wc_display_item_meta(
				$item,
				array(
					'before'    => '',
					'separator' => ', ',
					'after'     => '',
					'echo'      => false,
					'autop'     => false,
				)
			)
		);

		if ( $item_meta ) {
			$item_name .= ' (' . $item_meta . ')';
		}

		return apply_filters( 'woocommerce_paypal_get_order_item_name', $item_name, $order, $item );
	}

	/**
	 * Return all line items.
	 */
	protected function get_line_items() {
		return $this->line_items;
	}

	/**
	 * Remove all line items.
	 */
	protected function delete_line_items() {
		$this->line_items = array();
	}

	/**
	 * Check if the order has valid line items to use for PayPal request.
	 *
	 * The line items are invalid in case of mismatch in totals or if any amount < 0.
	 *
	 * @param WC_Order $order Order to be examined.
	 * @return bool
	 */
	protected function line_items_valid( $order ) {
		$negative_item_amount = false;
		$calculated_total     = 0;

		// Products.
		foreach ( $order->get_items( array( 'line_item', 'fee' ) ) as $item ) {
			if ( 'fee' === $item['type'] ) {
				$item_line_total   = $this->number_format( $item['line_total'], $order );
				$calculated_total += $item_line_total;
			} else {
				$item_line_total   = $this->number_format( $order->get_item_subtotal( $item, false ), $order );
				$calculated_total += $item_line_total * $item->get_quantity();
			}

			if ( $item_line_total < 0 ) {
				$negative_item_amount = true;
			}
		}
		$mismatched_totals = $this->number_format( $calculated_total + $order->get_total_tax() + $this->round( $order->get_shipping_total(), $order ) - $this->round( $order->get_total_discount(), $order ), $order ) !== $this->number_format( $order->get_total(), $order );
		return ! $negative_item_amount && ! $mismatched_totals;
	}

	/**
	 * Get line items to send to paypal.
	 *
	 * @param  WC_Order $order Order object.
	 */
	protected function prepare_line_items( $order ) {
		$this->delete_line_items();

		// Products.
		foreach ( $order->get_items( array( 'line_item', 'fee' ) ) as $item ) {
			if ( 'fee' === $item['type'] ) {
				$item_line_total = $this->number_format( $item['line_total'], $order );
				$this->add_line_item( $item->get_name(), 1, $item_line_total );
			} else {
				$product         = $item->get_product();
				$sku             = $product ? $product->get_sku() : '';
				$item_line_total = $this->number_format( $order->get_item_subtotal( $item, false ), $order );
				$this->add_line_item( $this->get_order_item_name( $order, $item ), $item->get_quantity(), $item_line_total, $sku );
			}
		}
	}

	/**
	 * Add PayPal Line Item.
	 *
	 * @param  string $item_name Item name.
	 * @param  int    $quantity Item quantity.
	 * @param  float  $amount Amount.
	 * @param  string $item_number Item number.
	 */
	protected function add_line_item( $item_name, $quantity = 1, $amount = 0.0, $item_number = '' ) {
		$index = ( count( $this->line_items ) / 4 ) + 1;

		$item = apply_filters(
			'woocommerce_paypal_line_item',
			array(
				'item_name'   => html_entity_decode( wc_trim_string( $item_name ? wp_strip_all_tags( $item_name ) : __( 'Item', 'woocommerce' ), 127 ), ENT_NOQUOTES, 'UTF-8' ),
				'quantity'    => (int) $quantity,
				'amount'      => wc_float_to_string( (float) $amount ),
				'item_number' => $item_number,
			),
			$item_name,
			$quantity,
			$amount,
			$item_number
		);

		$this->line_items[ 'item_name_' . $index ]   = $this->limit_length( $item['item_name'], 127 );
		$this->line_items[ 'quantity_' . $index ]    = $item['quantity'];
		$this->line_items[ 'amount_' . $index ]      = $item['amount'];
		$this->line_items[ 'item_number_' . $index ] = $this->limit_length( $item['item_number'], 127 );
	}

	/**
	 * Get the state to send to paypal.
	 *
	 * @param  string $cc Country two letter code.
	 * @param  string $state State code.
	 * @return string
	 */
	protected function get_paypal_state( $cc, $state ) {
		if ( 'US' === $cc ) {
			return $state;
		}

		$states = WC()->countries->get_states( $cc );

		if ( isset( $states[ $state ] ) ) {
			return $states[ $state ];
		}

		return $state;
	}

	/**
	 * Check if currency has decimals.
	 *
	 * @param  string $currency Currency to check.
	 * @return bool
	 */
	protected function currency_has_decimals( $currency ) {
		if ( in_array( $currency, array( 'HUF', 'JPY', 'TWD' ), true ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Round prices.
	 *
	 * @param  double   $price Price to round.
	 * @param  WC_Order $order Order object.
	 * @return double
	 */
	protected function round( $price, $order ) {
		$precision = 2;

		if ( ! $this->currency_has_decimals( $order->get_currency() ) ) {
			$precision = 0;
		}

		return NumberUtil::round( $price, $precision );
	}

	/**
	 * Format prices.
	 *
	 * @param  float|int $price Price to format.
	 * @param  WC_Order  $order Order object.
	 * @return string
	 */
	protected function number_format( $price, $order ) {
		$decimals = 2;

		if ( ! $this->currency_has_decimals( $order->get_currency() ) ) {
			$decimals = 0;
		}

		return number_format( $price, $decimals, '.', '' );
	}
}
gateways/paypal/includes/class-wc-gateway-paypal-api-handler.php000064400000013043151336044770021031 0ustar00<?php
/**
 * Class WC_Gateway_Paypal_API_Handler file.
 *
 * @package WooCommerce\Gateways
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Handles Refunds and other API requests such as capture.
 *
 * @since 3.0.0
 */
class WC_Gateway_Paypal_API_Handler {

	/**
	 * API Username
	 *
	 * @var string
	 */
	public static $api_username;

	/**
	 * API Password
	 *
	 * @var string
	 */
	public static $api_password;

	/**
	 * API Signature
	 *
	 * @var string
	 */
	public static $api_signature;

	/**
	 * Sandbox
	 *
	 * @var bool
	 */
	public static $sandbox = false;

	/**
	 * Get capture request args.
	 * See https://developer.paypal.com/docs/classic/api/merchant/DoCapture_API_Operation_NVP/.
	 *
	 * @param  WC_Order $order Order object.
	 * @param  float    $amount Amount.
	 * @return array
	 */
	public static function get_capture_request( $order, $amount = null ) {
		$request = array(
			'VERSION'         => '84.0',
			'SIGNATURE'       => self::$api_signature,
			'USER'            => self::$api_username,
			'PWD'             => self::$api_password,
			'METHOD'          => 'DoCapture',
			'AUTHORIZATIONID' => $order->get_transaction_id(),
			'AMT'             => number_format( is_null( $amount ) ? $order->get_total() : $amount, 2, '.', '' ),
			'CURRENCYCODE'    => $order->get_currency(),
			'COMPLETETYPE'    => 'Complete',
		);
		return apply_filters( 'woocommerce_paypal_capture_request', $request, $order, $amount );
	}

	/**
	 * Get refund request args.
	 *
	 * @param  WC_Order $order Order object.
	 * @param  float    $amount Refund amount.
	 * @param  string   $reason Refund reason.
	 * @return array
	 */
	public static function get_refund_request( $order, $amount = null, $reason = '' ) {
		$request = array(
			'VERSION'       => '84.0',
			'SIGNATURE'     => self::$api_signature,
			'USER'          => self::$api_username,
			'PWD'           => self::$api_password,
			'METHOD'        => 'RefundTransaction',
			'TRANSACTIONID' => $order->get_transaction_id(),
			'NOTE'          => html_entity_decode( wc_trim_string( $reason, 255 ), ENT_NOQUOTES, 'UTF-8' ),
			'REFUNDTYPE'    => 'Full',
		);
		if ( ! is_null( $amount ) ) {
			$request['AMT']          = number_format( $amount, 2, '.', '' );
			$request['CURRENCYCODE'] = $order->get_currency();
			$request['REFUNDTYPE']   = 'Partial';
		}
		return apply_filters( 'woocommerce_paypal_refund_request', $request, $order, $amount, $reason );
	}

	/**
	 * Capture an authorization.
	 *
	 * @param  WC_Order $order Order object.
	 * @param  float    $amount Amount.
	 * @return object Either an object of name value pairs for a success, or a WP_ERROR object.
	 */
	public static function do_capture( $order, $amount = null ) {
		$raw_response = wp_safe_remote_post(
			self::$sandbox ? 'https://api-3t.sandbox.paypal.com/nvp' : 'https://api-3t.paypal.com/nvp',
			array(
				'method'      => 'POST',
				'body'        => self::get_capture_request( $order, $amount ),
				'timeout'     => 70,
				'user-agent'  => 'WooCommerce/' . WC()->version,
				'httpversion' => '1.1',
			)
		);

		WC_Gateway_Paypal::log( 'DoCapture Response: ' . wc_print_r( $raw_response, true ) );

		if ( is_wp_error( $raw_response ) ) {
			return $raw_response;
		} elseif ( empty( $raw_response['body'] ) ) {
			return new WP_Error( 'paypal-api', 'Empty Response' );
		}

		parse_str( $raw_response['body'], $response );

		return (object) $response;
	}

	/**
	 * Refund an order via PayPal.
	 *
	 * @param  WC_Order $order Order object.
	 * @param  float    $amount Refund amount.
	 * @param  string   $reason Refund reason.
	 * @return object Either an object of name value pairs for a success, or a WP_ERROR object.
	 */
	public static function refund_transaction( $order, $amount = null, $reason = '' ) {
		$raw_response = wp_safe_remote_post(
			self::$sandbox ? 'https://api-3t.sandbox.paypal.com/nvp' : 'https://api-3t.paypal.com/nvp',
			array(
				'method'      => 'POST',
				'body'        => self::get_refund_request( $order, $amount, $reason ),
				'timeout'     => 70,
				'user-agent'  => 'WooCommerce/' . WC()->version,
				'httpversion' => '1.1',
			)
		);

		WC_Gateway_Paypal::log( 'Refund Response: ' . wc_print_r( $raw_response, true ) );

		if ( is_wp_error( $raw_response ) ) {
			return $raw_response;
		} elseif ( empty( $raw_response['body'] ) ) {
			return new WP_Error( 'paypal-api', 'Empty Response' );
		}

		parse_str( $raw_response['body'], $response );

		return (object) $response;
	}
}

/**
 * Here for backwards compatibility.
 *
 * @since 3.0.0
 */
class WC_Gateway_Paypal_Refund extends WC_Gateway_Paypal_API_Handler {
	/**
	 * Get refund request args. Proxy to WC_Gateway_Paypal_API_Handler::get_refund_request().
	 *
	 * @param WC_Order $order Order object.
	 * @param float    $amount Refund amount.
	 * @param string   $reason Refund reason.
	 *
	 * @return array
	 */
	public static function get_request( $order, $amount = null, $reason = '' ) {
		return self::get_refund_request( $order, $amount, $reason );
	}

	/**
	 * Process an order refund.
	 *
	 * @param  WC_Order $order Order object.
	 * @param  float    $amount Refund amount.
	 * @param  string   $reason Refund reason.
	 * @param  bool     $sandbox Whether to use sandbox mode or not.
	 * @return object Either an object of name value pairs for a success, or a WP_ERROR object.
	 */
	public static function refund_order( $order, $amount = null, $reason = '', $sandbox = false ) {
		if ( $sandbox ) {
			self::$sandbox = $sandbox;
		}
		$result = self::refund_transaction( $order, $amount, $reason );
		if ( is_wp_error( $result ) ) {
			return $result;
		} else {
			return (array) $result;
		}
	}
}
gateways/paypal/includes/settings-paypal.php000064400000020217151336044770015335 0ustar00<?php
/**
 * Settings for PayPal Standard Gateway.
 *
 * @package WooCommerce\Classes\Payment
 */

defined( 'ABSPATH' ) || exit;

return array(
	'enabled'               => array(
		'title'   => __( 'Enable/Disable', 'woocommerce' ),
		'type'    => 'checkbox',
		'label'   => __( 'Enable PayPal Standard', 'woocommerce' ),
		'default' => 'no',
	),
	'title'                 => array(
		'title'       => __( 'Title', 'woocommerce' ),
		'type'        => 'text',
		'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ),
		'default'     => __( 'PayPal', 'woocommerce' ),
		'desc_tip'    => true,
	),
	'description'           => array(
		'title'       => __( 'Description', 'woocommerce' ),
		'type'        => 'text',
		'desc_tip'    => true,
		'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce' ),
		'default'     => __( "Pay via PayPal; you can pay with your credit card if you don't have a PayPal account.", 'woocommerce' ),
	),
	'email'                 => array(
		'title'       => __( 'PayPal email', 'woocommerce' ),
		'type'        => 'email',
		'description' => __( 'Please enter your PayPal email address; this is needed in order to take payment.', 'woocommerce' ),
		'default'     => get_option( 'admin_email' ),
		'desc_tip'    => true,
		'placeholder' => 'you@youremail.com',
	),
	'advanced'              => array(
		'title'       => __( 'Advanced options', 'woocommerce' ),
		'type'        => 'title',
		'description' => '',
	),
	'testmode'              => array(
		'title'       => __( 'PayPal sandbox', 'woocommerce' ),
		'type'        => 'checkbox',
		'label'       => __( 'Enable PayPal sandbox', 'woocommerce' ),
		'default'     => 'no',
		/* translators: %s: URL */
		'description' => sprintf( __( 'PayPal sandbox can be used to test payments. Sign up for a <a href="%s">developer account</a>.', 'woocommerce' ), 'https://developer.paypal.com/' ),
	),
	'debug'                 => array(
		'title'       => __( 'Debug log', 'woocommerce' ),
		'type'        => 'checkbox',
		'label'       => __( 'Enable logging', 'woocommerce' ),
		'default'     => 'no',
		/* translators: %s: URL */
		'description' => sprintf( __( 'Log PayPal events, such as IPN requests, inside %s Note: this may log personal information. We recommend using this for debugging purposes only and deleting the logs when finished.', 'woocommerce' ), '<code>' . WC_Log_Handler_File::get_log_file_path( 'paypal' ) . '</code>' ),
	),
	'ipn_notification'      => array(
		'title'       => __( 'IPN email notifications', 'woocommerce' ),
		'type'        => 'checkbox',
		'label'       => __( 'Enable IPN email notifications', 'woocommerce' ),
		'default'     => 'yes',
		'description' => __( 'Send notifications when an IPN is received from PayPal indicating refunds, chargebacks and cancellations.', 'woocommerce' ),
	),
	'receiver_email'        => array(
		'title'       => __( 'Receiver email', 'woocommerce' ),
		'type'        => 'email',
		'description' => __( 'If your main PayPal email differs from the PayPal email entered above, input your main receiver email for your PayPal account here. This is used to validate IPN requests.', 'woocommerce' ),
		'default'     => '',
		'desc_tip'    => true,
		'placeholder' => 'you@youremail.com',
	),
	'identity_token'        => array(
		'title'       => __( 'PayPal identity token', 'woocommerce' ),
		'type'        => 'text',
		'description' => __( 'Optionally enable "Payment Data Transfer" (Profile > Profile and Settings > My Selling Tools > Website Preferences) and then copy your identity token here. This will allow payments to be verified without the need for PayPal IPN.', 'woocommerce' ),
		'default'     => '',
		'desc_tip'    => true,
		'placeholder' => '',
	),
	'invoice_prefix'        => array(
		'title'       => __( 'Invoice prefix', 'woocommerce' ),
		'type'        => 'text',
		'description' => __( 'Please enter a prefix for your invoice numbers. If you use your PayPal account for multiple stores ensure this prefix is unique as PayPal will not allow orders with the same invoice number.', 'woocommerce' ),
		'default'     => 'WC-',
		'desc_tip'    => true,
	),
	'send_shipping'         => array(
		'title'       => __( 'Shipping details', 'woocommerce' ),
		'type'        => 'checkbox',
		'label'       => __( 'Send shipping details to PayPal instead of billing.', 'woocommerce' ),
		'description' => __( 'PayPal allows us to send one address. If you are using PayPal for shipping labels you may prefer to send the shipping address rather than billing. Turning this option off may prevent PayPal Seller protection from applying.', 'woocommerce' ),
		'default'     => 'yes',
	),
	'address_override'      => array(
		'title'       => __( 'Address override', 'woocommerce' ),
		'type'        => 'checkbox',
		'label'       => __( 'Enable "address_override" to prevent address information from being changed.', 'woocommerce' ),
		'description' => __( 'PayPal verifies addresses therefore this setting can cause errors (we recommend keeping it disabled).', 'woocommerce' ),
		'default'     => 'no',
	),
	'paymentaction'         => array(
		'title'       => __( 'Payment action', 'woocommerce' ),
		'type'        => 'select',
		'class'       => 'wc-enhanced-select',
		'description' => __( 'Choose whether you wish to capture funds immediately or authorize payment only.', 'woocommerce' ),
		'default'     => 'sale',
		'desc_tip'    => true,
		'options'     => array(
			'sale'          => __( 'Capture', 'woocommerce' ),
			'authorization' => __( 'Authorize', 'woocommerce' ),
		),
	),
	'image_url'             => array(
		'title'       => __( 'Image url', 'woocommerce' ),
		'type'        => 'text',
		'description' => __( 'Optionally enter the URL to a 150x50px image displayed as your logo in the upper left corner of the PayPal checkout pages.', 'woocommerce' ),
		'default'     => '',
		'desc_tip'    => true,
		'placeholder' => __( 'Optional', 'woocommerce' ),
	),
	'api_details'           => array(
		'title'       => __( 'API credentials', 'woocommerce' ),
		'type'        => 'title',
		/* translators: %s: URL */
		'description' => sprintf( __( 'Enter your PayPal API credentials to process refunds via PayPal. Learn how to access your <a href="%s">PayPal API Credentials</a>.', 'woocommerce' ), 'https://developer.paypal.com/webapps/developer/docs/classic/api/apiCredentials/#create-an-api-signature' ),
	),
	'api_username'          => array(
		'title'       => __( 'Live API username', 'woocommerce' ),
		'type'        => 'text',
		'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ),
		'default'     => '',
		'desc_tip'    => true,
		'placeholder' => __( 'Optional', 'woocommerce' ),
	),
	'api_password'          => array(
		'title'       => __( 'Live API password', 'woocommerce' ),
		'type'        => 'password',
		'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ),
		'default'     => '',
		'desc_tip'    => true,
		'placeholder' => __( 'Optional', 'woocommerce' ),
	),
	'api_signature'         => array(
		'title'       => __( 'Live API signature', 'woocommerce' ),
		'type'        => 'password',
		'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ),
		'default'     => '',
		'desc_tip'    => true,
		'placeholder' => __( 'Optional', 'woocommerce' ),
	),
	'sandbox_api_username'  => array(
		'title'       => __( 'Sandbox API username', 'woocommerce' ),
		'type'        => 'text',
		'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ),
		'default'     => '',
		'desc_tip'    => true,
		'placeholder' => __( 'Optional', 'woocommerce' ),
	),
	'sandbox_api_password'  => array(
		'title'       => __( 'Sandbox API password', 'woocommerce' ),
		'type'        => 'password',
		'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ),
		'default'     => '',
		'desc_tip'    => true,
		'placeholder' => __( 'Optional', 'woocommerce' ),
	),
	'sandbox_api_signature' => array(
		'title'       => __( 'Sandbox API signature', 'woocommerce' ),
		'type'        => 'password',
		'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ),
		'default'     => '',
		'desc_tip'    => true,
		'placeholder' => __( 'Optional', 'woocommerce' ),
	),
);
gateways/paypal/includes/class-wc-gateway-paypal-pdt-handler.php000064400000014343151336044770021053 0ustar00<?php
/**
 * Class WC_Gateway_Paypal_PDT_Handler file.
 *
 * @package WooCommerce\Gateways
 */

use Automattic\Jetpack\Constants;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

require_once dirname( __FILE__ ) . '/class-wc-gateway-paypal-response.php';

/**
 * Handle PDT Responses from PayPal.
 */
class WC_Gateway_Paypal_PDT_Handler extends WC_Gateway_Paypal_Response {

	/**
	 * Identity token for PDT support
	 *
	 * @var string
	 */
	protected $identity_token;

	/**
	 * Receiver email address to validate.
	 *
	 * @var string Receiver email address.
	 */
	protected $receiver_email;

	/**
	 * Constructor.
	 *
	 * @param bool   $sandbox Whether to use sandbox mode or not.
	 * @param string $identity_token Identity token for PDT support.
	 */
	public function __construct( $sandbox = false, $identity_token = '' ) {
		add_action( 'woocommerce_thankyou_paypal', array( $this, 'check_response_for_order' ) );
		$this->identity_token = $identity_token;
		$this->sandbox        = $sandbox;
	}

	/**
	 * Set receiver email to enable more strict validation.
	 *
	 * @param string $receiver_email Email to receive PDT notification from.
	 */
	public function set_receiver_email( $receiver_email = '' ) {
		$this->receiver_email = $receiver_email;
	}

	/**
	 * Validate a PDT transaction to ensure its authentic.
	 *
	 * @param  string $transaction TX ID.
	 * @return bool|array False or result array if successful and valid.
	 */
	protected function validate_transaction( $transaction ) {
		$pdt = array(
			'body'        => array(
				'cmd' => '_notify-synch',
				'tx'  => $transaction,
				'at'  => $this->identity_token,
			),
			'timeout'     => 60,
			'httpversion' => '1.1',
			'user-agent'  => 'WooCommerce/' . Constants::get_constant( 'WC_VERSION' ),
		);

		// Post back to get a response.
		$response = wp_safe_remote_post( $this->sandbox ? 'https://www.sandbox.paypal.com/cgi-bin/webscr' : 'https://www.paypal.com/cgi-bin/webscr', $pdt );

		if ( is_wp_error( $response ) || strpos( $response['body'], 'SUCCESS' ) !== 0 ) {
			return false;
		}

		// Parse transaction result data.
		$transaction_result  = array_map( 'wc_clean', array_map( 'urldecode', explode( "\n", $response['body'] ) ) );
		$transaction_results = array();

		foreach ( $transaction_result as $line ) {
			$line                            = explode( '=', $line );
			$transaction_results[ $line[0] ] = isset( $line[1] ) ? $line[1] : '';
		}

		if ( ! empty( $transaction_results['charset'] ) && function_exists( 'iconv' ) ) {
			foreach ( $transaction_results as $key => $value ) {
				$transaction_results[ $key ] = iconv( $transaction_results['charset'], 'utf-8', $value );
			}
		}

		return $transaction_results;
	}

	/**
	 * Check Response for PDT, taking the order id from the request.
	 *
	 * @deprecated 6.4 Use check_response_for_order instead.
	 */
	public function check_response() {
		global $wp;
		$order_id = apply_filters( 'woocommerce_thankyou_order_id', absint( $wp->query_vars['order-received'] ) );

		$this->check_response_for_order( $order_id );
	}

	/**
	 * Check Response for PDT.
	 *
	 * @since 6.4
	 *
	 * @param mixed $wc_order_id The order id to check the response against.
	 */
	public function check_response_for_order( $wc_order_id ) {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( empty( $_REQUEST['tx'] ) ) {
			return;
		}

		$wc_order = wc_get_order( $wc_order_id );
		if ( ! $wc_order->needs_payment() ) {
			return;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$transaction        = wc_clean( wp_unslash( $_REQUEST['tx'] ) );
		$transaction_result = $this->validate_transaction( $transaction );

		if ( $transaction_result ) {
			$status = strtolower( $transaction_result['payment_status'] );
			$amount = isset( $transaction_result['mc_gross'] ) ? $transaction_result['mc_gross'] : 0;
			$order  = $this->get_paypal_order( $transaction_result['custom'] );

			if ( ! $order ) {
				// No valid WC order found on tx data.
				return;
			}

			if ( $wc_order->get_id() !== $order->get_id() ) {
				/* translators: 1: order ID, 2: order ID. */
				WC_Gateway_Paypal::log( sprintf( __( 'Received PDT notification for order %1$d on endpoint for order %2$d.', 'woocommerce' ), $order->get_id(), $wc_order_id ), 'error' );
				return;
			}

			if ( 0 !== strcasecmp( trim( $transaction_result['receiver_email'] ), trim( $this->receiver_email ) ) ) {
				/* translators: 1: email address, 2: order ID . */
				WC_Gateway_Paypal::log( sprintf( __( 'Received PDT notification for another account: %1$s. Order ID: %2$d.', 'woocommerce' ), $transaction_result['receiver_email'], $order->get_id() ), 'error' );
				return;
			}

			// We have a valid response from PayPal.
			WC_Gateway_Paypal::log( 'PDT Transaction Status: ' . wc_print_r( $status, true ) );

			$order->add_meta_data( '_paypal_status', $status );
			$order->set_transaction_id( $transaction );

			if ( 'completed' === $status ) {
				if ( number_format( $order->get_total(), 2, '.', '' ) !== number_format( $amount, 2, '.', '' ) ) {
					WC_Gateway_Paypal::log( 'Payment error: Amounts do not match (amt ' . $amount . ')', 'error' );
					/* translators: 1: Payment amount */
					$this->payment_on_hold( $order, sprintf( __( 'Validation error: PayPal amounts do not match (amt %s).', 'woocommerce' ), $amount ) );
				} else {
					// Log paypal transaction fee and payment type.
					if ( ! empty( $transaction_result['mc_fee'] ) ) {
						$order->add_meta_data( 'PayPal Transaction Fee', wc_clean( $transaction_result['mc_fee'] ) );
					}
					if ( ! empty( $transaction_result['payment_type'] ) ) {
						$order->add_meta_data( 'Payment type', wc_clean( $transaction_result['payment_type'] ) );
					}

					$this->payment_complete( $order, $transaction, __( 'PDT payment completed', 'woocommerce' ) );
				}
			} else {
				if ( 'authorization' === $transaction_result['pending_reason'] ) {
					$this->payment_on_hold( $order, __( 'Payment authorized. Change payment status to processing or complete to capture funds.', 'woocommerce' ) );
				} else {
					/* translators: 1: Pending reason */
					$this->payment_on_hold( $order, sprintf( __( 'Payment pending (%s).', 'woocommerce' ), $transaction_result['pending_reason'] ) );
				}
			}
		} else {
			WC_Gateway_Paypal::log( 'Received invalid response from PayPal PDT' );
		}
	}
}
gateways/paypal/includes/class-wc-gateway-paypal-response.php000064400000004077151336044770020512 0ustar00<?php
/**
 * Class WC_Gateway_Paypal_Response file.
 *
 * @package WooCommerce\Gateways
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Handles Responses.
 */
abstract class WC_Gateway_Paypal_Response {

	/**
	 * Sandbox mode
	 *
	 * @var bool
	 */
	protected $sandbox = false;

	/**
	 * Get the order from the PayPal 'Custom' variable.
	 *
	 * @param  string $raw_custom JSON Data passed back by PayPal.
	 * @return bool|WC_Order object
	 */
	protected function get_paypal_order( $raw_custom ) {
		// We have the data in the correct format, so get the order.
		$custom = json_decode( $raw_custom );
		if ( $custom && is_object( $custom ) ) {
			$order_id  = $custom->order_id;
			$order_key = $custom->order_key;
		} else {
			// Nothing was found.
			WC_Gateway_Paypal::log( 'Order ID and key were not found in "custom".', 'error' );
			return false;
		}

		$order = wc_get_order( $order_id );

		if ( ! $order ) {
			// We have an invalid $order_id, probably because invoice_prefix has changed.
			$order_id = wc_get_order_id_by_order_key( $order_key );
			$order    = wc_get_order( $order_id );
		}

		if ( ! $order || ! hash_equals( $order->get_order_key(), $order_key ) ) {
			WC_Gateway_Paypal::log( 'Order Keys do not match.', 'error' );
			return false;
		}

		return $order;
	}

	/**
	 * Complete order, add transaction ID and note.
	 *
	 * @param  WC_Order $order Order object.
	 * @param  string   $txn_id Transaction ID.
	 * @param  string   $note Payment note.
	 */
	protected function payment_complete( $order, $txn_id = '', $note = '' ) {
		if ( ! $order->has_status( array( 'processing', 'completed' ) ) ) {
			$order->add_order_note( $note );
			$order->payment_complete( $txn_id );

			if ( isset( WC()->cart ) ) {
				WC()->cart->empty_cart();
			}
		}
	}

	/**
	 * Hold order and add note.
	 *
	 * @param  WC_Order $order Order object.
	 * @param  string   $reason Reason why the payment is on hold.
	 */
	protected function payment_on_hold( $order, $reason = '' ) {
		$order->update_status( 'on-hold', $reason );

		if ( isset( WC()->cart ) ) {
			WC()->cart->empty_cart();
		}
	}
}
gateways/paypal/includes/class-wc-gateway-paypal-ipn-handler.php000064400000032273151336044770021054 0ustar00<?php
/**
 * Handles responses from PayPal IPN.
 *
 * @package WooCommerce\PayPal
 * @version 3.3.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

require_once dirname( __FILE__ ) . '/class-wc-gateway-paypal-response.php';

/**
 * WC_Gateway_Paypal_IPN_Handler class.
 */
class WC_Gateway_Paypal_IPN_Handler extends WC_Gateway_Paypal_Response {

	/**
	 * Receiver email address to validate.
	 *
	 * @var string Receiver email address.
	 */
	protected $receiver_email;

	/**
	 * Constructor.
	 *
	 * @param bool   $sandbox Use sandbox or not.
	 * @param string $receiver_email Email to receive IPN from.
	 */
	public function __construct( $sandbox = false, $receiver_email = '' ) {
		add_action( 'woocommerce_api_wc_gateway_paypal', array( $this, 'check_response' ) );
		add_action( 'valid-paypal-standard-ipn-request', array( $this, 'valid_response' ) );

		$this->receiver_email = $receiver_email;
		$this->sandbox        = $sandbox;
	}

	/**
	 * Check for PayPal IPN Response.
	 */
	public function check_response() {
		if ( ! empty( $_POST ) && $this->validate_ipn() ) { // WPCS: CSRF ok.
			$posted = wp_unslash( $_POST ); // WPCS: CSRF ok, input var ok.

			// phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
			do_action( 'valid-paypal-standard-ipn-request', $posted );
			exit;
		}

		wp_die( 'PayPal IPN Request Failure', 'PayPal IPN', array( 'response' => 500 ) );
	}

	/**
	 * There was a valid response.
	 *
	 * @param  array $posted Post data after wp_unslash.
	 */
	public function valid_response( $posted ) {
		$order = ! empty( $posted['custom'] ) ? $this->get_paypal_order( $posted['custom'] ) : false;

		if ( $order ) {

			// Lowercase returned variables.
			$posted['payment_status'] = strtolower( $posted['payment_status'] );

			WC_Gateway_Paypal::log( 'Found order #' . $order->get_id() );
			WC_Gateway_Paypal::log( 'Payment status: ' . $posted['payment_status'] );

			if ( method_exists( $this, 'payment_status_' . $posted['payment_status'] ) ) {
				call_user_func( array( $this, 'payment_status_' . $posted['payment_status'] ), $order, $posted );
			}
		}
	}

	/**
	 * Check PayPal IPN validity.
	 */
	public function validate_ipn() {
		WC_Gateway_Paypal::log( 'Checking IPN response is valid' );

		// Get received values from post data.
		$validate_ipn        = wp_unslash( $_POST ); // WPCS: CSRF ok, input var ok.
		$validate_ipn['cmd'] = '_notify-validate';

		// Send back post vars to paypal.
		$params = array(
			'body'        => $validate_ipn,
			'timeout'     => 60,
			'httpversion' => '1.1',
			'compress'    => false,
			'decompress'  => false,
			'user-agent'  => 'WooCommerce/' . WC()->version,
		);

		// Post back to get a response.
		$response = wp_safe_remote_post( $this->sandbox ? 'https://www.sandbox.paypal.com/cgi-bin/webscr' : 'https://www.paypal.com/cgi-bin/webscr', $params );

		WC_Gateway_Paypal::log( 'IPN Response: ' . wc_print_r( $response, true ) );

		// Check to see if the request was valid.
		if ( ! is_wp_error( $response ) && $response['response']['code'] >= 200 && $response['response']['code'] < 300 && strstr( $response['body'], 'VERIFIED' ) ) {
			WC_Gateway_Paypal::log( 'Received valid response from PayPal IPN' );
			return true;
		}

		WC_Gateway_Paypal::log( 'Received invalid response from PayPal IPN' );

		if ( is_wp_error( $response ) ) {
			WC_Gateway_Paypal::log( 'Error response: ' . $response->get_error_message() );
		}

		return false;
	}

	/**
	 * Check for a valid transaction type.
	 *
	 * @param string $txn_type Transaction type.
	 */
	protected function validate_transaction_type( $txn_type ) {
		$accepted_types = array( 'cart', 'instant', 'express_checkout', 'web_accept', 'masspay', 'send_money', 'paypal_here' );

		if ( ! in_array( strtolower( $txn_type ), $accepted_types, true ) ) {
			WC_Gateway_Paypal::log( 'Aborting, Invalid type:' . $txn_type );
			exit;
		}
	}

	/**
	 * Check currency from IPN matches the order.
	 *
	 * @param WC_Order $order    Order object.
	 * @param string   $currency Currency code.
	 */
	protected function validate_currency( $order, $currency ) {
		if ( $order->get_currency() !== $currency ) {
			WC_Gateway_Paypal::log( 'Payment error: Currencies do not match (sent "' . $order->get_currency() . '" | returned "' . $currency . '")' );

			/* translators: %s: currency code. */
			$order->update_status( 'on-hold', sprintf( __( 'Validation error: PayPal currencies do not match (code %s).', 'woocommerce' ), $currency ) );
			exit;
		}
	}

	/**
	 * Check payment amount from IPN matches the order.
	 *
	 * @param WC_Order $order  Order object.
	 * @param int      $amount Amount to validate.
	 */
	protected function validate_amount( $order, $amount ) {
		if ( number_format( $order->get_total(), 2, '.', '' ) !== number_format( $amount, 2, '.', '' ) ) {
			WC_Gateway_Paypal::log( 'Payment error: Amounts do not match (gross ' . $amount . ')' );

			/* translators: %s: Amount. */
			$order->update_status( 'on-hold', sprintf( __( 'Validation error: PayPal amounts do not match (gross %s).', 'woocommerce' ), $amount ) );
			exit;
		}
	}

	/**
	 * Check receiver email from PayPal. If the receiver email in the IPN is different than what is stored in.
	 * WooCommerce -> Settings -> Checkout -> PayPal, it will log an error about it.
	 *
	 * @param WC_Order $order          Order object.
	 * @param string   $receiver_email Email to validate.
	 */
	protected function validate_receiver_email( $order, $receiver_email ) {
		if ( strcasecmp( trim( $receiver_email ), trim( $this->receiver_email ) ) !== 0 ) {
			WC_Gateway_Paypal::log( "IPN Response is for another account: {$receiver_email}. Your email is {$this->receiver_email}" );

			/* translators: %s: email address . */
			$order->update_status( 'on-hold', sprintf( __( 'Validation error: PayPal IPN response from a different email address (%s).', 'woocommerce' ), $receiver_email ) );
			exit;
		}
	}

	/**
	 * Handle a completed payment.
	 *
	 * @param WC_Order $order  Order object.
	 * @param array    $posted Posted data.
	 */
	protected function payment_status_completed( $order, $posted ) {
		if ( $order->has_status( wc_get_is_paid_statuses() ) ) {
			WC_Gateway_Paypal::log( 'Aborting, Order #' . $order->get_id() . ' is already complete.' );
			exit;
		}

		$this->validate_transaction_type( $posted['txn_type'] );
		$this->validate_currency( $order, $posted['mc_currency'] );
		$this->validate_amount( $order, $posted['mc_gross'] );
		$this->validate_receiver_email( $order, $posted['receiver_email'] );
		$this->save_paypal_meta_data( $order, $posted );

		if ( 'completed' === $posted['payment_status'] ) {
			if ( $order->has_status( 'cancelled' ) ) {
				$this->payment_status_paid_cancelled_order( $order, $posted );
			}

			if ( ! empty( $posted['mc_fee'] ) ) {
				$order->add_meta_data( 'PayPal Transaction Fee', wc_clean( $posted['mc_fee'] ) );
			}

			$this->payment_complete( $order, ( ! empty( $posted['txn_id'] ) ? wc_clean( $posted['txn_id'] ) : '' ), __( 'IPN payment completed', 'woocommerce' ) );
		} else {
			if ( 'authorization' === $posted['pending_reason'] ) {
				$this->payment_on_hold( $order, __( 'Payment authorized. Change payment status to processing or complete to capture funds.', 'woocommerce' ) );
			} else {
				/* translators: %s: pending reason. */
				$this->payment_on_hold( $order, sprintf( __( 'Payment pending (%s).', 'woocommerce' ), $posted['pending_reason'] ) );
			}
		}
	}

	/**
	 * Handle a pending payment.
	 *
	 * @param WC_Order $order  Order object.
	 * @param array    $posted Posted data.
	 */
	protected function payment_status_pending( $order, $posted ) {
		$this->payment_status_completed( $order, $posted );
	}

	/**
	 * Handle a failed payment.
	 *
	 * @param WC_Order $order  Order object.
	 * @param array    $posted Posted data.
	 */
	protected function payment_status_failed( $order, $posted ) {
		/* translators: %s: payment status. */
		$order->update_status( 'failed', sprintf( __( 'Payment %s via IPN.', 'woocommerce' ), wc_clean( $posted['payment_status'] ) ) );
	}

	/**
	 * Handle a denied payment.
	 *
	 * @param WC_Order $order  Order object.
	 * @param array    $posted Posted data.
	 */
	protected function payment_status_denied( $order, $posted ) {
		$this->payment_status_failed( $order, $posted );
	}

	/**
	 * Handle an expired payment.
	 *
	 * @param WC_Order $order  Order object.
	 * @param array    $posted Posted data.
	 */
	protected function payment_status_expired( $order, $posted ) {
		$this->payment_status_failed( $order, $posted );
	}

	/**
	 * Handle a voided payment.
	 *
	 * @param WC_Order $order  Order object.
	 * @param array    $posted Posted data.
	 */
	protected function payment_status_voided( $order, $posted ) {
		$this->payment_status_failed( $order, $posted );
	}

	/**
	 * When a user cancelled order is marked paid.
	 *
	 * @param WC_Order $order  Order object.
	 * @param array    $posted Posted data.
	 */
	protected function payment_status_paid_cancelled_order( $order, $posted ) {
		$this->send_ipn_email_notification(
			/* translators: %s: order link. */
			sprintf( __( 'Payment for cancelled order %s received', 'woocommerce' ), '<a class="link" href="' . esc_url( $order->get_edit_order_url() ) . '">' . $order->get_order_number() . '</a>' ),
			/* translators: %s: order ID. */
			sprintf( __( 'Order #%s has been marked paid by PayPal IPN, but was previously cancelled. Admin handling required.', 'woocommerce' ), $order->get_order_number() )
		);
	}

	/**
	 * Handle a refunded order.
	 *
	 * @param WC_Order $order  Order object.
	 * @param array    $posted Posted data.
	 */
	protected function payment_status_refunded( $order, $posted ) {
		// Only handle full refunds, not partial.
		if ( $order->get_total() === wc_format_decimal( $posted['mc_gross'] * -1, wc_get_price_decimals() ) ) {

			/* translators: %s: payment status. */
			$order->update_status( 'refunded', sprintf( __( 'Payment %s via IPN.', 'woocommerce' ), strtolower( $posted['payment_status'] ) ) );

			$this->send_ipn_email_notification(
				/* translators: %s: order link. */
				sprintf( __( 'Payment for order %s refunded', 'woocommerce' ), '<a class="link" href="' . esc_url( $order->get_edit_order_url() ) . '">' . $order->get_order_number() . '</a>' ),
				/* translators: %1$s: order ID, %2$s: reason code. */
				sprintf( __( 'Order #%1$s has been marked as refunded - PayPal reason code: %2$s', 'woocommerce' ), $order->get_order_number(), $posted['reason_code'] )
			);
		}
	}

	/**
	 * Handle a reversal.
	 *
	 * @param WC_Order $order  Order object.
	 * @param array    $posted Posted data.
	 */
	protected function payment_status_reversed( $order, $posted ) {
		/* translators: %s: payment status. */
		$order->update_status( 'on-hold', sprintf( __( 'Payment %s via IPN.', 'woocommerce' ), wc_clean( $posted['payment_status'] ) ) );

		$this->send_ipn_email_notification(
			/* translators: %s: order link. */
			sprintf( __( 'Payment for order %s reversed', 'woocommerce' ), '<a class="link" href="' . esc_url( $order->get_edit_order_url() ) . '">' . $order->get_order_number() . '</a>' ),
			/* translators: %1$s: order ID, %2$s: reason code. */
			sprintf( __( 'Order #%1$s has been marked on-hold due to a reversal - PayPal reason code: %2$s', 'woocommerce' ), $order->get_order_number(), wc_clean( $posted['reason_code'] ) )
		);
	}

	/**
	 * Handle a cancelled reversal.
	 *
	 * @param WC_Order $order  Order object.
	 * @param array    $posted Posted data.
	 */
	protected function payment_status_canceled_reversal( $order, $posted ) {
		$this->send_ipn_email_notification(
			/* translators: %s: order link. */
			sprintf( __( 'Reversal cancelled for order #%s', 'woocommerce' ), $order->get_order_number() ),
			/* translators: %1$s: order ID, %2$s: order link. */
			sprintf( __( 'Order #%1$s has had a reversal cancelled. Please check the status of payment and update the order status accordingly here: %2$s', 'woocommerce' ), $order->get_order_number(), esc_url( $order->get_edit_order_url() ) )
		);
	}

	/**
	 * Save important data from the IPN to the order.
	 *
	 * @param WC_Order $order  Order object.
	 * @param array    $posted Posted data.
	 */
	protected function save_paypal_meta_data( $order, $posted ) {
		if ( ! empty( $posted['payment_type'] ) ) {
			update_post_meta( $order->get_id(), 'Payment type', wc_clean( $posted['payment_type'] ) );
		}
		if ( ! empty( $posted['txn_id'] ) ) {
			update_post_meta( $order->get_id(), '_transaction_id', wc_clean( $posted['txn_id'] ) );
		}
		if ( ! empty( $posted['payment_status'] ) ) {
			update_post_meta( $order->get_id(), '_paypal_status', wc_clean( $posted['payment_status'] ) );
		}
	}

	/**
	 * Send a notification to the user handling orders.
	 *
	 * @param string $subject Email subject.
	 * @param string $message Email message.
	 */
	protected function send_ipn_email_notification( $subject, $message ) {
		$new_order_settings = get_option( 'woocommerce_new_order_settings', array() );
		$mailer             = WC()->mailer();
		$message            = $mailer->wrap_message( $subject, $message );

		$woocommerce_paypal_settings = get_option( 'woocommerce_paypal_settings' );
		if ( ! empty( $woocommerce_paypal_settings['ipn_notification'] ) && 'no' === $woocommerce_paypal_settings['ipn_notification'] ) {
			return;
		}

		$mailer->send( ! empty( $new_order_settings['recipient'] ) ? $new_order_settings['recipient'] : get_option( 'admin_email' ), strip_tags( $subject ), $message );
	}
}
gateways/paypal/assets/images/paypal.png000064400000004626151336044770014443 0ustar00�PNG


IHDR3 ��@	]IDATX	�Yil\W��6o�c�v�f�,ơ�i��	�j�*��
�(BTU��
Z~��	EB?��J"�ҒR��P� (UH�XF���i��n�Lf<�̛���;w:�8Nl��\k�v��~��c�رc*�"C�irWJ��.��X#Dȁ�0a����*��rvh��#Te�m����\��Y����]�Ee�*<_�8~#�F���L�Ž��͘�9������'�O��$�R�P�Ša��&�	�cR�5�(a��Y�W;e��^�L���Uа�G�D|(�%�FIs-�͟�>���dcjĒ	50R�@�Gi�_Y32��dy�
�$L�
# ����_�o_8#j�Jz*E+��,��x���q϶uԧ0-�A��1�K]��93�$^:��HK@ֆ�y�i��ο;N�ۓ&�q���̙��1���}{v}B��*�`pEc%��%�����J��XEw���;�B,0;�������6y#��gJ>�pM��F�߿�l&Km�w�r`�P32w��ij��h��k���M���4�bACq�Y�[d��`\��1���|N����l�	�[���x�f�z�}0K�4|!WB`�'�2�
Y�|������3=�1Ɔ�9�CS���qΜeVIê0#���<~�\D���MI\�r'S�b�ȑ�4���n���i���~B������53�q�
P%{��K\��#|�[�DC�[������Z`o��E���w&��DB�e�U�jf� ��D��+%_J��Ѓ��Y<A���4Q�؉ Չ�ʣ�-�.��RH`@]I�$��E��H���o��)��H!�L�A�m���6X{�9t&�7%��6[���d\33b<���S���~z��D$�J��A���]4;B����om��I`��{E쾹	��EkÇO3M��Z=i��X����ą�� ���0�Pm���fd���X����ٟ��u1<sk��N�r�R�Z�٢jg�J1�H;��t�|?�r�^4$�@���YD������p�G:�`WQ�G&��8>Y3WF��fU�'�܌g�(�߉��#11�yFjLP̎�l7'\��XJKܕL�2e"�^����Z�̘�Җ�008B"H
}BsPO��&7�aqIq�����%
Q�cC���M����E4�m d_�$FU�^�ļך�1����É�x��[�+���S��[�4@>��kC㼍ˇ�yk|���@rB�BS���)M�F8D������y���F�W�	��6��1�aa�DG��,Y�%L_�L=k����/b�Z�ؙ�3�	�pxHk�	��I��M.@1t0���%�XR'`��Y�t6-�ߴ\��u�W���,��ϧ������_����a�K�`3��8,l}���s�h��}p$�ԁK�����8~}���Ū
D�R�������?��@�	j مTr$��,ުЌ�� j���Gї��/��}�^=6���q����Z�C���rǭI�\���w�1�4�-�p�f�{x�]���6�{,��
q¸�K�$�F�1
���1�(�@V��V��Y`�ri����F\|q�~�6�����w��O߄}/�����3�>��
�3�ȑ��Lun@|��(nJ���D]$�;:L�$ԹLJ��"�T.Ϡ�s��e��$$<#;����,6�u��^��=�ȣ�k-^���ؾ9���(�3��r��~�3C�@����8t_�8|��}k�8�%\�Б� iG4��<}JR&Q��m37�L���؈�Lg���׾�?���xpG�w��Ĺa���8����h��-=m������xI���<����űs]=IC̴��F/���Ÿ4:e�5��L�3¾�Yc���s�Fq���l��H]
`R;��:�����)���w��.4�1	a�,�-���y�)>�jb���O�Ǔ_؎oe2<a}�\E;�+����"^%���G*V������86ƈ�em,��uppf��	�r�*���xJ��琿"煥/W���R��+*��8�#�����F�1a�w�����/߮.�ԓ�-��ɓj�n�w"Ս˳R AIERb�ЧKI��e�HH���ȝ����^וu9�Z��6N)��x��rdV��Q�4-
\KYU�ŕ��+ϖ�{���#ץn�ڃ)>+�r���N&g$;��W�½A(w	G_"�Lg��}E=�E?r/�-+;�DN����](�����;�w�}�q9��61:c:����"�Ҭ>P�Q�,�؀�1���ʍ�@���<m2@�:yge@�e��p�^���x�r���F���?H�^��W0IEND�B`�gateways/paypal/assets/js/paypal-admin.min.js000064400000002133151336044770015301 0ustar00jQuery(function($){'use strict';var wc_paypal_admin={isTestMode:function(){return $('#woocommerce_paypal_testmode').is(':checked')},init:function(){$(document.body).on('change','#woocommerce_paypal_testmode',function(){var test_api_username=$('#woocommerce_paypal_sandbox_api_username').parents('tr').eq(0),test_api_password=$('#woocommerce_paypal_sandbox_api_password').parents('tr').eq(0),test_api_signature=$('#woocommerce_paypal_sandbox_api_signature').parents('tr').eq(0),live_api_username=$('#woocommerce_paypal_api_username').parents('tr').eq(0),live_api_password=$('#woocommerce_paypal_api_password').parents('tr').eq(0),live_api_signature=$('#woocommerce_paypal_api_signature').parents('tr').eq(0);if($(this).is(':checked')){test_api_username.show();test_api_password.show();test_api_signature.show();live_api_username.hide();live_api_password.hide();live_api_signature.hide()}else{test_api_username.hide();test_api_password.hide();test_api_signature.hide();live_api_username.show();live_api_password.show();live_api_signature.show()}});$('#woocommerce_paypal_testmode').change()}};wc_paypal_admin.init()})gateways/paypal/assets/js/paypal-admin.js000064400000002666151336044770014532 0ustar00jQuery( function( $ ) {
	'use strict';

	/**
	 * Object to handle PayPal admin functions.
	 */
	var wc_paypal_admin = {
		isTestMode: function() {
			return $( '#woocommerce_paypal_testmode' ).is( ':checked' );
		},

		/**
		 * Initialize.
		 */
		init: function() {
			$( document.body ).on( 'change', '#woocommerce_paypal_testmode', function() {
				var test_api_username = $( '#woocommerce_paypal_sandbox_api_username' ).parents( 'tr' ).eq( 0 ),
					test_api_password = $( '#woocommerce_paypal_sandbox_api_password' ).parents( 'tr' ).eq( 0 ),
					test_api_signature = $( '#woocommerce_paypal_sandbox_api_signature' ).parents( 'tr' ).eq( 0 ),
					live_api_username = $( '#woocommerce_paypal_api_username' ).parents( 'tr' ).eq( 0 ),
					live_api_password = $( '#woocommerce_paypal_api_password' ).parents( 'tr' ).eq( 0 ),
					live_api_signature = $( '#woocommerce_paypal_api_signature' ).parents( 'tr' ).eq( 0 );

				if ( $( this ).is( ':checked' ) ) {
					test_api_username.show();
					test_api_password.show();
					test_api_signature.show();
					live_api_username.hide();
					live_api_password.hide();
					live_api_signature.hide();
				} else {
					test_api_username.hide();
					test_api_password.hide();
					test_api_signature.hide();
					live_api_username.show();
					live_api_password.show();
					live_api_signature.show();
				}
			} );

			$( '#woocommerce_paypal_testmode' ).trigger( 'change' );
		}
	};

	wc_paypal_admin.init();
});
gateways/paypal/class-wc-gateway-paypal.php000064400000042721151336044770015046 0ustar00<?php
/**
 * PayPal Standard Payment Gateway.
 *
 * Provides a PayPal Standard Payment Gateway.
 *
 * @class       WC_Gateway_Paypal
 * @extends     WC_Payment_Gateway
 * @version     2.3.0
 * @package     WooCommerce\Classes\Payment
 */

use Automattic\Jetpack\Constants;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WC_Gateway_Paypal Class.
 */
class WC_Gateway_Paypal extends WC_Payment_Gateway {

	/**
	 * Whether or not logging is enabled
	 *
	 * @var bool
	 */
	public static $log_enabled = false;

	/**
	 * Logger instance
	 *
	 * @var WC_Logger
	 */
	public static $log = false;

	/**
	 * Constructor for the gateway.
	 */
	public function __construct() {
		$this->id                = 'paypal';
		$this->has_fields        = false;
		$this->order_button_text = __( 'Proceed to PayPal', 'woocommerce' );
		$this->method_title      = __( 'PayPal Standard', 'woocommerce' );
		/* translators: %s: Link to WC system status page */
		$this->method_description = __( 'PayPal Standard redirects customers to PayPal to enter their payment information.', 'woocommerce' );
		$this->supports           = array(
			'products',
			'refunds',
		);

		// Load the settings.
		$this->init_form_fields();
		$this->init_settings();

		// Define user set variables.
		$this->title          = $this->get_option( 'title' );
		$this->description    = $this->get_option( 'description' );
		$this->testmode       = 'yes' === $this->get_option( 'testmode', 'no' );
		$this->debug          = 'yes' === $this->get_option( 'debug', 'no' );
		$this->email          = $this->get_option( 'email' );
		$this->receiver_email = $this->get_option( 'receiver_email', $this->email );
		$this->identity_token = $this->get_option( 'identity_token' );
		self::$log_enabled    = $this->debug;

		if ( $this->testmode ) {
			/* translators: %s: Link to PayPal sandbox testing guide page */
			$this->description .= ' ' . sprintf( __( 'SANDBOX ENABLED. You can use sandbox testing accounts only. See the <a href="%s">PayPal Sandbox Testing Guide</a> for more details.', 'woocommerce' ), 'https://developer.paypal.com/docs/classic/lifecycle/ug_sandbox/' );
			$this->description  = trim( $this->description );
		}

		add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) );
		add_action( 'woocommerce_order_status_processing', array( $this, 'capture_payment' ) );
		add_action( 'woocommerce_order_status_completed', array( $this, 'capture_payment' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'admin_scripts' ) );

		if ( ! $this->is_valid_for_use() ) {
			$this->enabled = 'no';
		} else {
			include_once dirname( __FILE__ ) . '/includes/class-wc-gateway-paypal-ipn-handler.php';
			new WC_Gateway_Paypal_IPN_Handler( $this->testmode, $this->receiver_email );

			if ( $this->identity_token ) {
				include_once dirname( __FILE__ ) . '/includes/class-wc-gateway-paypal-pdt-handler.php';
				$pdt_handler = new WC_Gateway_Paypal_PDT_Handler( $this->testmode, $this->identity_token );
				$pdt_handler->set_receiver_email( $this->receiver_email );
			}
		}

		if ( 'yes' === $this->enabled ) {
			add_filter( 'woocommerce_thankyou_order_received_text', array( $this, 'order_received_text' ), 10, 2 );
		}
	}

	/**
	 * Return whether or not this gateway still requires setup to function.
	 *
	 * When this gateway is toggled on via AJAX, if this returns true a
	 * redirect will occur to the settings page instead.
	 *
	 * @since 3.4.0
	 * @return bool
	 */
	public function needs_setup() {
		return ! is_email( $this->email );
	}

	/**
	 * Logging method.
	 *
	 * @param string $message Log message.
	 * @param string $level Optional. Default 'info'. Possible values:
	 *                      emergency|alert|critical|error|warning|notice|info|debug.
	 */
	public static function log( $message, $level = 'info' ) {
		if ( self::$log_enabled ) {
			if ( empty( self::$log ) ) {
				self::$log = wc_get_logger();
			}
			self::$log->log( $level, $message, array( 'source' => 'paypal' ) );
		}
	}

	/**
	 * Processes and saves options.
	 * If there is an error thrown, will continue to save and validate fields, but will leave the erroring field out.
	 *
	 * @return bool was anything saved?
	 */
	public function process_admin_options() {
		$saved = parent::process_admin_options();

		// Maybe clear logs.
		if ( 'yes' !== $this->get_option( 'debug', 'no' ) ) {
			if ( empty( self::$log ) ) {
				self::$log = wc_get_logger();
			}
			self::$log->clear( 'paypal' );
		}

		return $saved;
	}

	/**
	 * Get gateway icon.
	 *
	 * @return string
	 */
	public function get_icon() {
		// We need a base country for the link to work, bail if in the unlikely event no country is set.
		$base_country = WC()->countries->get_base_country();
		if ( empty( $base_country ) ) {
			return '';
		}
		$icon_html = '';
		$icon      = (array) $this->get_icon_image( $base_country );

		foreach ( $icon as $i ) {
			$icon_html .= '<img src="' . esc_attr( $i ) . '" alt="' . esc_attr__( 'PayPal acceptance mark', 'woocommerce' ) . '" />';
		}

		$icon_html .= sprintf( '<a href="%1$s" class="about_paypal" onclick="javascript:window.open(\'%1$s\',\'WIPaypal\',\'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=yes, resizable=yes, width=1060, height=700\'); return false;">' . esc_attr__( 'What is PayPal?', 'woocommerce' ) . '</a>', esc_url( $this->get_icon_url( $base_country ) ) );

		return apply_filters( 'woocommerce_gateway_icon', $icon_html, $this->id );
	}

	/**
	 * Get the link for an icon based on country.
	 *
	 * @param  string $country Country two letter code.
	 * @return string
	 */
	protected function get_icon_url( $country ) {
		$url           = 'https://www.paypal.com/' . strtolower( $country );
		$home_counties = array( 'BE', 'CZ', 'DK', 'HU', 'IT', 'JP', 'NL', 'NO', 'ES', 'SE', 'TR', 'IN' );
		$countries     = array( 'DZ', 'AU', 'BH', 'BQ', 'BW', 'CA', 'CN', 'CW', 'FI', 'FR', 'DE', 'GR', 'HK', 'ID', 'JO', 'KE', 'KW', 'LU', 'MY', 'MA', 'OM', 'PH', 'PL', 'PT', 'QA', 'IE', 'RU', 'BL', 'SX', 'MF', 'SA', 'SG', 'SK', 'KR', 'SS', 'TW', 'TH', 'AE', 'GB', 'US', 'VN' );

		if ( in_array( $country, $home_counties, true ) ) {
			return $url . '/webapps/mpp/home';
		} elseif ( in_array( $country, $countries, true ) ) {
			return $url . '/webapps/mpp/paypal-popup';
		} else {
			return $url . '/cgi-bin/webscr?cmd=xpt/Marketing/general/WIPaypal-outside';
		}
	}

	/**
	 * Get PayPal images for a country.
	 *
	 * @param string $country Country code.
	 * @return array of image URLs
	 */
	protected function get_icon_image( $country ) {
		switch ( $country ) {
			case 'US':
			case 'NZ':
			case 'CZ':
			case 'HU':
			case 'MY':
				$icon = 'https://www.paypalobjects.com/webstatic/mktg/logo/AM_mc_vs_dc_ae.jpg';
				break;
			case 'TR':
				$icon = 'https://www.paypalobjects.com/webstatic/mktg/logo-center/logo_paypal_odeme_secenekleri.jpg';
				break;
			case 'GB':
				$icon = 'https://www.paypalobjects.com/webstatic/mktg/Logo/AM_mc_vs_ms_ae_UK.png';
				break;
			case 'MX':
				$icon = array(
					'https://www.paypal.com/es_XC/Marketing/i/banner/paypal_visa_mastercard_amex.png',
					'https://www.paypal.com/es_XC/Marketing/i/banner/paypal_debit_card_275x60.gif',
				);
				break;
			case 'FR':
				$icon = 'https://www.paypalobjects.com/webstatic/mktg/logo-center/logo_paypal_moyens_paiement_fr.jpg';
				break;
			case 'AU':
				$icon = 'https://www.paypalobjects.com/webstatic/en_AU/mktg/logo/Solutions-graphics-1-184x80.jpg';
				break;
			case 'DK':
				$icon = 'https://www.paypalobjects.com/webstatic/mktg/logo-center/logo_PayPal_betalingsmuligheder_dk.jpg';
				break;
			case 'RU':
				$icon = 'https://www.paypalobjects.com/webstatic/ru_RU/mktg/business/pages/logo-center/AM_mc_vs_dc_ae.jpg';
				break;
			case 'NO':
				$icon = 'https://www.paypalobjects.com/webstatic/mktg/logo-center/banner_pl_just_pp_319x110.jpg';
				break;
			case 'CA':
				$icon = 'https://www.paypalobjects.com/webstatic/en_CA/mktg/logo-image/AM_mc_vs_dc_ae.jpg';
				break;
			case 'HK':
				$icon = 'https://www.paypalobjects.com/webstatic/en_HK/mktg/logo/AM_mc_vs_dc_ae.jpg';
				break;
			case 'SG':
				$icon = 'https://www.paypalobjects.com/webstatic/en_SG/mktg/Logos/AM_mc_vs_dc_ae.jpg';
				break;
			case 'TW':
				$icon = 'https://www.paypalobjects.com/webstatic/en_TW/mktg/logos/AM_mc_vs_dc_ae.jpg';
				break;
			case 'TH':
				$icon = 'https://www.paypalobjects.com/webstatic/en_TH/mktg/Logos/AM_mc_vs_dc_ae.jpg';
				break;
			case 'JP':
				$icon = 'https://www.paypal.com/ja_JP/JP/i/bnr/horizontal_solution_4_jcb.gif';
				break;
			case 'IN':
				$icon = 'https://www.paypalobjects.com/webstatic/mktg/logo/AM_mc_vs_dc_ae.jpg';
				break;
			default:
				$icon = WC_HTTPS::force_https_url( WC()->plugin_url() . '/includes/gateways/paypal/assets/images/paypal.png' );
				break;
		}
		return apply_filters( 'woocommerce_paypal_icon', $icon );
	}

	/**
	 * Check if this gateway is available in the user's country based on currency.
	 *
	 * @return bool
	 */
	public function is_valid_for_use() {
		return in_array(
			get_woocommerce_currency(),
			apply_filters(
				'woocommerce_paypal_supported_currencies',
				array( 'AUD', 'BRL', 'CAD', 'MXN', 'NZD', 'HKD', 'SGD', 'USD', 'EUR', 'JPY', 'TRY', 'NOK', 'CZK', 'DKK', 'HUF', 'ILS', 'MYR', 'PHP', 'PLN', 'SEK', 'CHF', 'TWD', 'THB', 'GBP', 'RMB', 'RUB', 'INR' )
			),
			true
		);
	}

	/**
	 * Admin Panel Options.
	 * - Options for bits like 'title' and availability on a country-by-country basis.
	 *
	 * @since 1.0.0
	 */
	public function admin_options() {
		if ( $this->is_valid_for_use() ) {
			parent::admin_options();
		} else {
			?>
			<div class="inline error">
				<p>
					<strong><?php esc_html_e( 'Gateway disabled', 'woocommerce' ); ?></strong>: <?php esc_html_e( 'PayPal Standard does not support your store currency.', 'woocommerce' ); ?>
				</p>
			</div>
			<?php
		}
	}

	/**
	 * Initialise Gateway Settings Form Fields.
	 */
	public function init_form_fields() {
		$this->form_fields = include __DIR__ . '/includes/settings-paypal.php';
	}

	/**
	 * Get the transaction URL.
	 *
	 * @param  WC_Order $order Order object.
	 * @return string
	 */
	public function get_transaction_url( $order ) {
		if ( $this->testmode ) {
			$this->view_transaction_url = 'https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_view-a-trans&id=%s';
		} else {
			$this->view_transaction_url = 'https://www.paypal.com/cgi-bin/webscr?cmd=_view-a-trans&id=%s';
		}
		return parent::get_transaction_url( $order );
	}

	/**
	 * Process the payment and return the result.
	 *
	 * @param  int $order_id Order ID.
	 * @return array
	 */
	public function process_payment( $order_id ) {
		include_once dirname( __FILE__ ) . '/includes/class-wc-gateway-paypal-request.php';

		$order          = wc_get_order( $order_id );
		$paypal_request = new WC_Gateway_Paypal_Request( $this );

		return array(
			'result'   => 'success',
			'redirect' => $paypal_request->get_request_url( $order, $this->testmode ),
		);
	}

	/**
	 * Can the order be refunded via PayPal?
	 *
	 * @param  WC_Order $order Order object.
	 * @return bool
	 */
	public function can_refund_order( $order ) {
		$has_api_creds = false;

		if ( $this->testmode ) {
			$has_api_creds = $this->get_option( 'sandbox_api_username' ) && $this->get_option( 'sandbox_api_password' ) && $this->get_option( 'sandbox_api_signature' );
		} else {
			$has_api_creds = $this->get_option( 'api_username' ) && $this->get_option( 'api_password' ) && $this->get_option( 'api_signature' );
		}

		return $order && $order->get_transaction_id() && $has_api_creds;
	}

	/**
	 * Init the API class and set the username/password etc.
	 */
	protected function init_api() {
		include_once dirname( __FILE__ ) . '/includes/class-wc-gateway-paypal-api-handler.php';

		WC_Gateway_Paypal_API_Handler::$api_username  = $this->testmode ? $this->get_option( 'sandbox_api_username' ) : $this->get_option( 'api_username' );
		WC_Gateway_Paypal_API_Handler::$api_password  = $this->testmode ? $this->get_option( 'sandbox_api_password' ) : $this->get_option( 'api_password' );
		WC_Gateway_Paypal_API_Handler::$api_signature = $this->testmode ? $this->get_option( 'sandbox_api_signature' ) : $this->get_option( 'api_signature' );
		WC_Gateway_Paypal_API_Handler::$sandbox       = $this->testmode;
	}

	/**
	 * Process a refund if supported.
	 *
	 * @param  int    $order_id Order ID.
	 * @param  float  $amount Refund amount.
	 * @param  string $reason Refund reason.
	 * @return bool|WP_Error
	 */
	public function process_refund( $order_id, $amount = null, $reason = '' ) {
		$order = wc_get_order( $order_id );

		if ( ! $this->can_refund_order( $order ) ) {
			return new WP_Error( 'error', __( 'Refund failed.', 'woocommerce' ) );
		}

		$this->init_api();

		$result = WC_Gateway_Paypal_API_Handler::refund_transaction( $order, $amount, $reason );

		if ( is_wp_error( $result ) ) {
			$this->log( 'Refund Failed: ' . $result->get_error_message(), 'error' );
			return new WP_Error( 'error', $result->get_error_message() );
		}

		$this->log( 'Refund Result: ' . wc_print_r( $result, true ) );

		switch ( strtolower( $result->ACK ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
			case 'success':
			case 'successwithwarning':
				$order->add_order_note(
					/* translators: 1: Refund amount, 2: Refund ID */
					sprintf( __( 'Refunded %1$s - Refund ID: %2$s', 'woocommerce' ), $result->GROSSREFUNDAMT, $result->REFUNDTRANSACTIONID ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
				);
				return true;
		}

		return isset( $result->L_LONGMESSAGE0 ) ? new WP_Error( 'error', $result->L_LONGMESSAGE0 ) : false; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
	}

	/**
	 * Capture payment when the order is changed from on-hold to complete or processing
	 *
	 * @param  int $order_id Order ID.
	 */
	public function capture_payment( $order_id ) {
		$order = wc_get_order( $order_id );

		if ( 'paypal' === $order->get_payment_method() && 'pending' === $order->get_meta( '_paypal_status', true ) && $order->get_transaction_id() ) {
			$this->init_api();
			$result = WC_Gateway_Paypal_API_Handler::do_capture( $order );

			if ( is_wp_error( $result ) ) {
				$this->log( 'Capture Failed: ' . $result->get_error_message(), 'error' );
				/* translators: %s: Paypal gateway error message */
				$order->add_order_note( sprintf( __( 'Payment could not be captured: %s', 'woocommerce' ), $result->get_error_message() ) );
				return;
			}

			$this->log( 'Capture Result: ' . wc_print_r( $result, true ) );

			// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
			if ( ! empty( $result->PAYMENTSTATUS ) ) {
				switch ( $result->PAYMENTSTATUS ) {
					case 'Completed':
						/* translators: 1: Amount, 2: Authorization ID, 3: Transaction ID */
						$order->add_order_note( sprintf( __( 'Payment of %1$s was captured - Auth ID: %2$s, Transaction ID: %3$s', 'woocommerce' ), $result->AMT, $result->AUTHORIZATIONID, $result->TRANSACTIONID ) );
						update_post_meta( $order->get_id(), '_paypal_status', $result->PAYMENTSTATUS );
						update_post_meta( $order->get_id(), '_transaction_id', $result->TRANSACTIONID );
						break;
					default:
						/* translators: 1: Authorization ID, 2: Payment status */
						$order->add_order_note( sprintf( __( 'Payment could not be captured - Auth ID: %1$s, Status: %2$s', 'woocommerce' ), $result->AUTHORIZATIONID, $result->PAYMENTSTATUS ) );
						break;
				}
			}
			// phpcs:enable
		}
	}

	/**
	 * Load admin scripts.
	 *
	 * @since 3.3.0
	 */
	public function admin_scripts() {
		$screen    = get_current_screen();
		$screen_id = $screen ? $screen->id : '';

		if ( 'woocommerce_page_wc-settings' !== $screen_id ) {
			return;
		}

		$suffix  = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min';
		$version = Constants::get_constant( 'WC_VERSION' );

		wp_enqueue_script( 'woocommerce_paypal_admin', WC()->plugin_url() . '/includes/gateways/paypal/assets/js/paypal-admin' . $suffix . '.js', array(), $version, true );
	}

	/**
	 * Custom PayPal order received text.
	 *
	 * @since 3.9.0
	 * @param string   $text Default text.
	 * @param WC_Order $order Order data.
	 * @return string
	 */
	public function order_received_text( $text, $order ) {
		if ( $order && $this->id === $order->get_payment_method() ) {
			return esc_html__( 'Thank you for your payment. Your transaction has been completed, and a receipt for your purchase has been emailed to you. Log into your PayPal account to view transaction details.', 'woocommerce' );
		}

		return $text;
	}

	/**
	 * Determines whether PayPal Standard should be loaded or not.
	 *
	 * By default PayPal Standard isn't loaded on new installs or on existing sites which haven't set up the gateway.
	 *
	 * @since 5.5.0
	 *
	 * @return bool Whether PayPal Standard should be loaded.
	 */
	public function should_load() {
		$option_key  = '_should_load';
		$should_load = $this->get_option( $option_key );

		if ( '' === $should_load ) {

			// New installs without PayPal Standard enabled don't load it.
			if ( 'no' === $this->enabled && WC_Install::is_new_install() ) {
				$should_load = false;
			} else {
				$should_load = true;
			}

			$this->update_option( $option_key, wc_bool_to_string( $should_load ) );
		} else {
			$should_load = wc_string_to_bool( $should_load );
		}

		/**
		 * Allow third-parties to filter whether PayPal Standard should be loaded or not.
		 *
		 * @since 5.5.0
		 *
		 * @param bool              $should_load Whether PayPal Standard should be loaded.
		 * @param WC_Gateway_Paypal $this        The WC_Gateway_Paypal instance.
		 */
		return apply_filters( 'woocommerce_should_load_paypal_standard', $should_load, $this );
	}
}

Youez - 2016 - github.com/yon3zu
LinuXploit